目錄
?項目演示:
1. 主界面
技術講解:
TCP連接
進程的并發
鏈表
SQLite3
IO對文件的讀寫
功能實現
實現邏輯
我遇到的問題:
服務器端代碼思路解析
必要條件
步驟詳解
客戶端代碼思路解析
步驟詳解
服務器源碼如下:
?客戶端源碼如下:
dictionary.h源碼如下:
dic.txt文件因為過于龐大,所以放到公眾號中,大家需要自取。
?項目演示:
1. 主界面
2. 注冊
3. 登錄
4. 查詢單詞
5. 查詢歷史記錄
6. 刪除歷史記錄
7. 退出
技術講解:
TCP連接
TCP(Transmission Control Protocol)連接是互聯網上最常用的一種面向連接、可靠的、基于字節流的傳輸層通信協議。建立TCP連接需要經過著名的“三次握手”過程:
- SYN(同步序列編號):客戶端發送一個SYN包給服務器,并進入SYN_SEND狀態,等待服務器確認。
- SYN-ACK:服務器收到SYN包后,回應一個SYN-ACK(SYN+ACKnowledgment)包,告訴客戶端其接收到了請求,并同意建立連接,此時服務器進入SYN_RECV狀態。
- ACK(確認字符):客戶端收到服務器的SYN-ACK包后,發送一個ACK包給服務器,確認收到了服務器的確認信息。此時,TCP連接正式建立,雙方進入ESTABLISHED狀態。
進程的并發
進程并發是指多個進程在同一時間段內交替執行的現象,操作系統通過時間片輪轉、優先級調度等策略來管理進程的執行,給予每個進程一定的CPU時間來執行任務,從而實現“同時”處理多個任務的效果。并發可以提高系統資源利用率和整體處理能力,但也會帶來資源競爭、死鎖等問題,需要通過同步機制(如互斥鎖、信號量等)來協調。
鏈表
鏈表是一種重要的數據結構,用于存儲線性集合的元素,每個元素(節點)包含數據和指向下一個節點的指針。鏈表分為單鏈表、雙鏈表、循環鏈表等類型。與數組相比,鏈表的優點在于插入和刪除操作更快,因為它不需要移動其他元素,只需修改相鄰節點的指針。缺點是訪問元素不如數組直接,需要從頭節點開始逐個遍歷。
SQLite3
SQLite3是一個輕量級、無服務器、零配置的嵌入式數據庫引擎,它允許程序將整個數據庫(包括定義、表、索引和數據)存儲在單一的文件中。SQLite支持SQL語言,可以用于各種應用開發,特別是在那些需要本地存儲、對數據庫服務器要求不高或不想管理數據庫服務器的場景下,如手機應用、桌面應用等。SQLite的特點是易于集成、跨平臺、占用資源少。
IO對文件的讀寫
在編程中,對文件的讀寫是基礎的IO操作,主要通過系統調用或庫函數實現。以C語言為例:
-
讀文件:通常使用
Cfopen
函數打開文件,然后通過fread
或fgets
等函數讀取內容。例如:1FILE *file = fopen("example.txt", "r"); 2if (file != NULL) { 3 char buffer[255]; 4 fgets(buffer, sizeof(buffer), file); 5 fclose(file); 6}
-
寫文件:同樣使用
Cfopen
函數以寫模式打開文件,然后通過fwrite
或fprintf
等函數寫入數據。例如:1FILE *file = fopen("output.txt", "w"); 2if (file != NULL) { 3 fprintf(file, "Hello, World!\n"); 4 fclose(file); 5}
這些操作涉及文件句柄、緩沖區管理及錯誤處理,是進行文件操作的基本步驟。
功能實現
- Linux系統編程:利用Linux環境下的高級編程技巧。
- 用戶的注冊和登錄:實現安全的用戶認證機制。
- 查詢單詞:高效檢索詞典數據庫中的詞匯信息。
- 查詢或刪除歷史記錄:提供用戶對過往查詢記錄的操作功能。
實現邏輯
本節將詳細介紹開發過程的關鍵步驟,幫助你從零開始構建這一系統。
-
創建SQLite3數據庫:用于存儲用戶賬戶信息及查詢歷史記錄,確保數據的持久化和安全性。
- 設計數據庫模式,包括用戶表和歷史記錄表。
- 實現增刪查改的基本操作。
-
定義請求類型:通過結構體區分注冊、登錄、查詢單詞和管理歷史記錄等不同操作。
- 結構體設計應包含所有必要的字段,如用戶名、密碼、操作類型等。
- 利用鏈表存儲結構體實例,便于動態管理。
-
搭建代碼框架:先構建整體框架,再逐步填充細節。
- 定義服務器和客戶端的主循環。
- 規劃函數調用流程,預留函數體以便后續填充。
-
確保TCP通信穩定:實現可靠的雙向數據傳輸。
- 初始化TCP套接字,監聽端口。
- 處理連接請求,接收和發送數據包。
- 驗證數據格式,防止解析錯誤。
-
準備單詞文檔:作為詞典數據源。
- 選擇合適的單詞列表文件。
- 使用標準I/O庫讀取和解析文檔。
-
實現核心邏輯:編寫關鍵算法和業務處理代碼。
- 注冊和登錄驗證:檢查用戶信息的正確性。
- 單詞查詢:采用高效的數據結構加速檢索。
- 注意:使用回調函數時,確保條件判斷準確無誤,避免查詢失敗或結果不匹配。
- 歷史記錄管理:記錄和展示用戶的查詢歷史,提供刪除選項。
我遇到的問題:
大概思路圖:
服務器端代碼思路解析
必要條件
- 并發能力:采用進程并發模型,確保服務器能同時處理來自多個客戶端的請求。
- 通信方式:基于TCP協議,實現可靠的數據傳輸。
步驟詳解
-
創建數據庫表
- 設計兩個SQLite3表:一個用于存儲用戶賬號信息(用戶名、密碼),另一個用于記錄歷史查詢數據。
- 實現基本的CRUD(創建、讀取、更新、刪除)操作,確保數據的準確性和完整性。
-
初始化網絡服務
- 創建TCP套接字,設置相關屬性。
- 綁定IP地址和端口號,啟動監聽,準備接受客戶端連接。
- 接收客戶端連接請求,為每個連接創建新進程以處理并發請求。
-
處理客戶端請求
- 讀取客戶端發送的完整結構體數據包。
- 使用
switch
語句分析請求類型,調用相應的處理函數。- 注冊/登錄:驗證用戶信息,更新數據庫狀態。
- 查詢單詞:搜索詞典數據庫,返回查詢結果。
- 歷史記錄:記錄或檢索用戶的歷史查詢記錄。
客戶端代碼思路解析
步驟詳解
-
初始化網絡連接
- 創建TCP套接字,指定服務器地址和端口。
- 連接到服務器,建立通信通道。
- 準備發送和接收數據。
-
交互邏輯
- 打印幫助菜單,指導用戶輸入命令。
- 根據用戶輸入,調用相應函數處理請求。
- 注冊/登錄:向服務器發送用戶信息,等待認證結果。
- 查詢單詞:提交單詞查詢請求,接收查詢結果。
- 歷史記錄:請求歷史記錄,顯示或刪除記錄。
服務器源碼如下:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/select.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include "dictionary.h"
#include <sqlite3.h>
#include <signal.h>
#include <sys/wait.h>
#include <time.h>int acceptfd;
int search_word(int acceptfd);
int num = 10;
time_t t;
struct tm *tm;
// 注冊函數
void register_sev(int acceptfd, char *p, sqlite3 *db)
{char sql[128] = " "; //定義數組來裝insert語句sprintf(sql, "insert into user values('%s','%s');", dic.name, dic.password);//如果插入不成功則給dic.text賦值失敗反之OKif (sqlite3_exec(db, sql, NULL, NULL, &p) != SQLITE_OK){strcpy(dic.text, "already exits!");}else{strcpy(dic.text, "OK");}send(acceptfd, &dic, sizeof(dic), 0); //發送
}// 登錄函數
void login_sev(int acceptfd, char *p, char **result, int row, int line, sqlite3 *db)
{// 定義一個輸入去接收這個查詢用戶名和密碼的語句char buf[128];sprintf(buf, "select * from user where name = '%s' and password = '%s';", dic.name, dic.password);// 先把輸入的數據查詢一下在表里,如果表里有數據則代表數據庫有密碼if (sqlite3_get_table(db, buf, &result, &row, &line, &p) != SQLITE_OK){perror("sev sqlite3_table error\n");return;}// 如果行大于零則代表有數據成功if (row > 0){strcpy(dic.text, "login successs");}else{strcpy(dic.text, "login loose,please check your name or password!");}send(acceptfd, &dic, sizeof(dic), 0);
}// 查詢單詞
void query_sev(int acceptfd, sqlite3 *db, char *p)
{time_t t;struct tm *tm;int a = 0;//定義一個語句去接受查詢的語句char sql2[128] = "";int found = search_word(acceptfd); //去接受查詢單詞這個函數的返回值 不為1則代表失敗 返回1則代表成功if (found != 1){strcpy(dic.text, "can't find \n");send(acceptfd, &dic, sizeof(dic), 0);}else{time(&t);tm = localtime(&t);sprintf(dic.time_sev, "%d-%d-%d %d:%d:%d", tm->tm_year + 1900,tm->tm_mon + 1, tm->tm_mday, tm->tm_hour, tm->tm_min, tm->tm_sec);sprintf(sql2, "insert into record values('%s','%s');", dic.password, dic.time_sev); // 成功后就把查詢成功的單詞插入到record這個表里if (sqlite3_exec(db, sql2, NULL, NULL, &p) != SQLITE_OK){perror("record insert error\n");return;}}
}// 查詢單詞函數
int search_word(int acceptfd)
{//定義每次接受來的密碼的長度int len = strlen(dic.password);//打開文件用標準IOFILE *sp = fopen("dic.txt", "r");if (NULL == sp){perror("sp error\n");return -1;}//如果dic.txt文件不為空時繼續執行while (fgets(dic.text, sizeof(dic.text), sp) != NULL){//拿密碼去比較文件中的前幾位 如果成功發送dic 并且返回1if (!strncmp(dic.text, dic.password, len)){send(acceptfd, &dic, sizeof(dic), 0);return 1;}}
}//回調函數
int history_callback(void *arg, int colunms, char **text, char **name)
{if (text != NULL){sprintf(dic.text, "%s %s", text[0], text[1]);send(acceptfd, &dic, sizeof(dic), 0);}return 0;
}//歷史函數
int history_sev(char *p, sqlite3 *db)
{if (sqlite3_exec(db, "select * from record;", history_callback, NULL, &p) != SQLITE_OK) // 此處用的是回調函數{fprintf(stderr, "select is error: %s\n", p); //如果不等于OK責代表沒成功,報錯return -1;}elseprintf("請求成功\n");dic.history_id = 1;send(acceptfd, &dic, sizeof(dic), 0);return 0;
}void delete_history(char *p, sqlite3 *db)
{if (sqlite3_exec(db, "delete from record;", NULL, NULL, &p) != SQLITE_OK){fprintf(stderr, "delete is error: %s\n", p); //如果不等于OK責代表沒成功,報錯}else{strcpy(dic.text, "刪除成功!");send(acceptfd, &dic, sizeof(dic), 0);}
}//處理僵尸進程
void hanlder(int arg)
{waitpid(-1, NULL, WNOHANG);
}int main(int argc, char const *argv[])
{//1. 創建sql表sqlite3 *db = NULL;if (sqlite3_open("./dictionary.db", &db) != 0){fprintf(stderr, "sqlite3 is error%s\n", sqlite3_errmsg(db));return -1;}// 創建兩個表char *p = NULL;if (sqlite3_exec(db, "create table user(name char primary key, password char);", NULL, NULL, &p) != SQLITE_OK){fprintf(stderr, "create stu1 is error %s\n", p);return -1;}if (sqlite3_exec(db, "create table record(password char,time char);", NULL, NULL, &p) != SQLITE_OK){fprintf(stderr, "create record is error %s\n", p);return -1;}// 創建指針以及行和列的變量char **result = NULL;int row = 0;int line = 0;// 創建套接字int sockfd = socket(AF_INET, SOCK_STREAM, 0);if (sockfd < 0){perror("sev sockfd error\n");return -1;}//填充結構體struct sockaddr_in saddr, caddr;saddr.sin_family = AF_INET;saddr.sin_port = htons(atoi(argv[1]));saddr.sin_addr.s_addr = inet_addr("0.0.0.0");int len = sizeof(caddr);// 綁定 和 監聽if (bind(sockfd, (struct sockaddr *)&saddr, sizeof(saddr)) < 0){perror("bind error\n");return -1;}if (listen(sockfd, 5) < 0){perror("sev listen error\n");return -1;}// 處理僵尸進程signal(SIGCHLD, hanlder);// 循環去接受while (1){acceptfd = accept(sockfd, (struct sockaddr *)&caddr, &len);if (acceptfd < 0){perror("sev acceptfd is error\n");return -1;}// 創建進程pid_t pid = fork();if (pid < 0){perror("sev pid error\n");return -1;}else if (pid == 0) // 子進程{while (recv(acceptfd, &dic, sizeof(dic), 0)){switch (dic.type){case 'r':register_sev(acceptfd, p, db); // 注冊函數break;case 'l':login_sev(acceptfd, p, result, row, line, db); // 登錄函數break;case 'q':query_sev(acceptfd, db, p); //查詢函數break;case 'h':history_sev(p, db); //歷史查詢函數break;case 'd':delete_history(p, db);// 歷史記錄刪除函數break;default:break;}}}else // 父進程{close(acceptfd);}}return 0;
}
?客戶端源碼如下:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/select.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include "dictionary.h"
#include <sqlite3.h>
// 聲明函數
void query(int sockfd);
void History(int sockfd);
void delete_history(int sockfd);
//注冊
void register_sev(int sockfd)
{printf("*******注冊頁面*******\n");dic.type = 'r'; // 令他的類型等于r 也就是我設置的注冊類型printf("請輸入你的用戶名: ");scanf("%s", dic.name);printf("請輸入你的密碼: ");scanf("%s", dic.password);send(sockfd, &dic, sizeof(dic), 0);int recvfd = recv(sockfd, &dic, sizeof(dic), 0);if (recvfd < 0)// 接受失敗{perror("recv error\n");return;}else if (recvfd == 0)// 服務器退出{printf("Sever already quit\n");return;}else // 接受成功{printf("%s\n", dic.text);}
}
//登錄
int login_sev(int sockfd)
{printf("*******登錄頁面*******\n");dic.type = 'l';printf("請輸入你的用戶名: ");scanf("%s", dic.name);getchar();printf("請輸入你的密碼: ");scanf("%s", dic.password);getchar();send(sockfd, &dic, sizeof(dic), 0);//將整個結構體發送過去//接受整個結構體 如果接受錯誤的話就返回-1如果 登錄成功就返回1int recvfd = recv(sockfd, &dic, sizeof(dic), 0);if (recvfd < 0){perror("recv error\n");return -1;}else if (recvfd == 0){printf("Sever already quit\n");return -1;}else{if (!strcmp(dic.text, "login successs")) // 如果接收到客戶端發過來的成功語句就打印成功{printf("登錄成功!\n");return 1;}else{printf("%s\n", dic.text);}}
}
// 進入登錄后的頁面
void next(int sockfd)
{int next;while (1){printf("*****************************查詢頁面***********************************\n");
printf("1.: query_password 2.: History_password 3:delete:History 4.:quit\n"); printf("***********************************************************************\n");printf("\n");printf("Please choose:");scanf("%d", &next);switch (next){case 1:query(sockfd); //查詢函數break;case 2:History(sockfd); //歷史函數break;case 3:delete_history(sockfd); // 刪除歷史記錄break;case 4:close(sockfd); //返回上一級exit(0);default:break;}}
}
// 查詢單詞
void query(int sockfd)
{dic.type = 'q';while (1){printf("Please input words that you want to search: ");scanf("%s", dic.password);putchar(10); //防止垃圾字符if (!strcmp(dic.password, "#")) //如果在查找單詞中輸入#就會退出{break;}send(sockfd, &dic, sizeof(dic), 0); // 將結構體發過去int recvfd = recv(sockfd, &dic, sizeof(dic), 0);if (recvfd < 0){perror("recv error\n");return;}else if (recvfd == 0){printf("Sever already quit\n");return;}else{printf("%s this word is expain: %s", dic.password, dic.text);printf("\n");}}
}
// 歷史記錄
void History(int sockfd)
{dic.type = 'h';send(sockfd, &dic, sizeof(dic), 0); // 目的是為了將結構體類型發過去while (1){int recvfd = recv(sockfd, &dic, sizeof(dic), 0);if (recvfd < 0){perror("recv error\n");return;}if(dic.history_id == 1)break;printf("%s\n",dic.text);printf("\n");}
}
// 刪除歷史記錄
void delete_history(int sockfd)
{dic.type = 'd';send(sockfd, &dic, sizeof(dic), 0); // 目的是為了將結構體類型發過去while (1){int recvfd = recv(sockfd, &dic, sizeof(dic), 0);if (recvfd < 0){perror("recv error\n");return;}else if (recvfd == 0){exit(0);}else{printf("%s \n", dic.text);printf("\n");break;}}
}int main(int argc, char const *argv[])
{//1. 創建套接字int sockfd = socket(AF_INET, SOCK_STREAM, 0);if (sockfd < 0){perror("cli sockfd error\n");return -1;}//2.填充結構體struct sockaddr_in saddr;saddr.sin_family = AF_INET;saddr.sin_port = htons(atoi(argv[2]));saddr.sin_addr.s_addr = inet_addr(argv[1]);//鏈接if (connect(sockfd, (struct sockaddr *)&saddr, sizeof(saddr)) < 0){perror("cli connect erroen\n");return -1;}int cli_type1;while (1){printf("**************************************\n");printf("1: register 2: login 3:quit\n");printf("**************************************\n");printf("Please choose (must number!!!):");scanf("%d", &cli_type1);if (cli_type1 != 1 && cli_type1 != 2 && cli_type1 != 3){printf("請重新輸入正確的數字!\n");return -1;}switch (cli_type1){//2.注冊case 1:register_sev(sockfd);break;//3.登錄case 2:if (login_sev(sockfd) == 1) //接受上面返回成功的1進行到下一個函數next(sockfd);break;//4.退出case 3:close(sockfd);exit(0);default:break;}}return 0;
}
dictionary.h源碼如下:
struct dictionary
{char type; // r代表注冊 l 代表登錄 q代表查詢 h代表歷史char name[128];// 用戶名char password[128];// 密碼char text[128];// 通知內容或者查找內容char time_sev[32];// 時間int history_id;
} dic;
dic.txt文件因為過于龐大,所以放到公眾號中,大家需要自取。
微信關注嵌入式工程之家發送? ?dic源碼? 即可獲得文件喲。