[linux環境編程] TCP通訊與多線程編程實現「多人在線聊天室」linux
一、TCP通訊編程
TCP(Transmission Control Protocol)就是傳輸控制通信協議,是TCP/IP體系結構中最主要的傳輸協議。其「三次握手」提供了可靠的傳送,高可靠性保證了數據傳輸不會出現丟失與亂序,再加之TCP鏈接兩端設有緩存用來臨時存放雙向通訊的數據,因此能夠支持全雙工傳輸。很是貼合「多人在線聊天室」對數據傳輸的需求。緩存
二、多線程編程服務器
多線程是指從軟件或者硬件上實現多個線程併發執行的技術。簡單來講就是可以在同一時間執行多於一個線程,每一個線程處理各自獨立的任務,進而簡化處理異步事件的代碼,改善響應時間,提高總體處理性。多線程
一個進程的全部信息對該進程的全部線程都是共享的,包括可執行代碼、程序的全局內存和堆內存、棧以及文件描述符。所以,相比進程與進程之間,線程之間的通訊免去了繁雜的規則,但同時也面臨着線程同步的問題,須要多加註意。架構
一、功能介紹併發
服務端:可限制聊天室在線最大人數與等待進入最大人數。當聊天室人數達到上限,申請進入聊天室的用戶將會排隊,聊天室內任意用戶退出時,排隊用戶會按照順序自動加入聊天室。框架
客戶端:實時顯示聊天室內成員所發消息以及成員暱稱,可主動退出聊天室,當聊天室有成員變更時會有系統提示。異步
二、測試流程:socket
2.1 編譯
-std=gnu99:以GNU99標準編譯代碼;
-o output_filename:將輸出文件的名稱命名爲output_filename,同時這個名稱不能和源文件同名。若是不給出這個選項,就會生成系統默認的「a.out」可執行文件,易被覆蓋。
-lpthread:在編譯的連接階段自動加載pthread庫。
gcc -std=gnu99 server_tcp.c -o server -lpthread
gcc -std=gnu99 client_tcp.c -o client -lpthread
2.2 開啓server服務端與三個客戶端(client1/2/3)
Ubuntu可經過Ctrl+Shift+T開啓多個終端標籤頁,分別執行如下四條指令。其中第一個參數爲可執行文件的路徑;第二個參數爲通信的串口號,1~1024已被系統使用,通常狀況下大於1024便可;第三個參數爲通信的IP地址,「127.0.0.1」會自動轉化爲本機的IP地址。
./server 2333 127.0.0.1 ./client 2333 127.0.0.1 ./client 2333 127.0.0.1 ./client 2333 127.0.0.1
三、測試效果
服務端設置:最大在線人數2人,排隊最大人數1人(實際中至少要設置5個,排隊人數上限過低則無實際意義,這裏設置1人僅作演示)。開啓服務器後依次鏈接三個客戶端,輸入用戶名後申請進入聊天室,前兩個用戶進入聊天室,第三個用戶排隊等候。輸入字符‘q’退出聊天室。
server(服務器):
client1(用戶123):
client2(用戶456):
client3(用戶789):
一、客戶端(server_tcp.c)
1.1 main 主函數
1 #include <stdio.h> 2 #include <sys/socket.h> 3 #include <stdbool.h> 4 #include <arpa/inet.h> 5 #include <sys/types.h> 6 #include <unistd.h> 7 #include <string.h> 8 #include <stdlib.h> 9 #include <netinet/in.h> 10 #include <pthread.h> 11 12 // 定義消息結構體,用於信息的傳輸 13 typedef struct Msg 14 { 15 char m_name[31]; 16 char m_buf[255]; 17 }Msg; 18 19 Msg msg = {}; 20 21 bool quit = false; // 該程序中暫無實際意義,用於後期拓展 22 int sockfd = 0; // socket標識符 23 int main(int argc,char** argv) 24 { 25 pthread_t ptid[2] = {}; 26 27 // 建立socket對象 28 sockfd = socket(AF_INET,SOCK_STREAM,0); 29 if(0 > sockfd) 30 { 31 perror("socket"); 32 return -1; 33 } 34 35 // 準備通訊地址 36 struct sockaddr_in addr = {AF_INET}; 37 addr.sin_port = htons(atoi(argv[1])); 38 addr.sin_addr.s_addr = inet_addr(argv[2]); 39 40 /*//等待鏈接 41 struct sockaddr_in src_addr = {}; 42 socklen_t addr_len = sizeof(src_addr);*/ 43 44 // 鏈接 45 int ret = connect(sockfd,(struct sockaddr*)&addr,sizeof(addr)); 46 if(0 > ret) 47 { 48 perror("connect"); 49 return -1; 50 } 51 52 printf("請輸入您的暱稱:"); 53 scanf("%s",msg.m_name); 54 55 // 建立讀數據進程 56 ret = pthread_create(&ptid[0],NULL,pthread_read,NULL); 57 if(0 > ret) 58 { 59 perror("pthread_create"); 60 return -1; 61 } 62 63 // 建立寫數據進程 64 ret = pthread_create(&ptid[1],NULL,pthread_write,NULL); 65 if(0 > ret) 66 { 67 perror("pthread_create"); 68 return -1; 69 } 70 71 while(!quit); 72 close(sockfd); 73 }
客戶端主函數主要用於創建起客戶端和服務器的鏈接,客戶端在申請鏈接後可能出現「進入聊天室(num<nmax_chat)」、「等待進入聊天室(num<nmax_chat+nmax_wait)」和「鏈接被拒(num>=nmax_chat+nmax_wait)」三種狀況。客戶端在獲取用戶的用戶名後將會創建起讀、寫數據兩個線程,實時接收和發送數據。
1.2 pthread_read 讀線程
1 void* pthread_read(void* arg) 2 { 3 while(1) 4 { 5 bzero(&msg.m_buf,sizeof(msg.m_buf)); 6 int ret = recv(sockfd,&msg,sizeof(msg),0); 7 if (0 < strlen(msg.m_buf)) 8 { 9 printf("%s:%s\n",msg.m_name,msg.m_buf); 10 } 11 /*if(!strcmp("q",msg)) 12 { 13 break; 14 }*/ 15 } 16 close(sockfd); 17 }
讀線程實時接收數據並顯示數據發送者的用戶名及其所發送的數據。
1.3 pthread_write 寫線程
1 void* pthread_write(void* arg) 2 { 3 while(1) 4 { 5 gets(msg.m_buf); 6 //sprintf(msg,"%s:%s",name,msg); // name貼進去時msg已經改變 7 printf("\33[1A"); 8 printf("\r \r"); 9 fflush(stdout); 10 send(sockfd,&msg,sizeof(msg),0); 11 if(!strcmp("q",msg.m_buf)) 12 { 13 quit = true; 14 break; 15 } 16 } 17 }
寫線程與讀線程之間存在互相干擾,由於兩者都必須保證明時性,因此沒法採用互斥鎖來保護數據,這裏將全局變量msg改成局部變量便可解決問題。而主函數中的msg.m_name則能夠經過線程建立函數傳遞給寫線程,但願代碼更加嚴謹的話能夠自行更改。
7~9行代碼組合實現了「消除己方殘留在終端顯示界面的所發送的數據」。
第7行代碼:將光標上移一行,即殘留顯示數據的行列;
第8行代碼:將光標移至該行行首,再輸出一段空格覆蓋原有數據,最後將光標移回行首;
第9行代碼:刷新標準輸出緩衝區,把輸出緩衝區裏的東西打印到標準輸出設備上(顯示終端)。目的是使七、8行代碼當即生效。
二、服務端
2.1 main 主函數
1 #include <stdio.h> 2 #include <sys/socket.h> 3 #include <stdbool.h> 4 #include <arpa/inet.h> 5 #include <sys/types.h> 6 #include <unistd.h> 7 #include <string.h> 8 #include <stdlib.h> 9 #include <netinet/in.h> 10 #include <pthread.h> 11 12 typedef struct Msg 13 { 14 char m_name[31]; 15 char m_buf[255]; 16 }Msg; 17 18 typedef struct Client 19 { 20 int c_fd; 21 bool c_flag; 22 }Client; 23 24 const int nmax_chat = 2; 25 const int nmax_wait = 1; 26 int num_chat = 0; 27 int num_wait = 0; 28 Client client[4] = {}; // nmax_wait + nmax_chat + 1; 29 30 bool quit = false; 31 int sockfd = 0; 32 pthread_t ptid[3] = {}; 33 34 int main(int argc,char** argv) 35 { 36 // 建立socket對象 37 sockfd = socket(AF_INET,SOCK_STREAM,0); 38 if(0 > sockfd) 39 { 40 perror("socket"); 41 return -1; 42 } 43 44 // 準備通訊地址 45 struct sockaddr_in addr = {AF_INET}; 46 addr.sin_port = htons(atoi(argv[1])); 47 addr.sin_addr.s_addr = inet_addr(argv[2]); 48 49 // 綁定對象與地址 50 int ret = bind(sockfd,(struct sockaddr*)&addr,sizeof(addr)); 51 if(0 > ret) 52 { 53 perror("bind"); 54 return -1; 55 } 56 57 // 設置排隊數量 58 listen(sockfd,num_wait); 59 60 // 建立線程 61 ret = pthread_create(&ptid[0],NULL,pthread_accept,NULL); 62 if(0 > ret) 63 { 64 perror("pthread_create"); 65 return -1; 66 } 67 68 while(!quit); 69 // 關閉鏈接 70 close(sockfd); 71 }
功能與客戶端主函數相似,主要用於創建起客戶端和服務器的鏈接的準備工做。客戶端在設立監聽數量(等待鏈接最大數量nmax_wait)後將會創建起接收線程。
2.2 pthread_accep 接收線程
1 void* pthread_accept(void* arg) 2 { 3 // 等待鏈接 4 struct sockaddr_in src_addr = {}; 5 socklen_t addr_len = sizeof(src_addr); 6 7 //printf("聊天室已建立!\n"); 8 while(1) 9 { 10 if (nmax_chat > num_chat) 11 { 12 int i = 0; 13 for ( i = 0; i < nmax_chat; i++) 14 { 15 if (false == client[i].c_flag) 16 { 17 client[i].c_fd = accept(sockfd,(struct sockaddr*)&src_addr,&addr_len); 18 client[i].c_flag = true; 19 break; 20 } 21 } 22 num_chat++; 23 // 建立線程 24 //printf("chat[%d] fd:%d flag:%d num_chat:%d\n",i,client[i].c_fd,client[i].c_flag,num_chat); 25 int ret = pthread_create(&ptid[1],NULL,pthread_com,&client[i]); 26 if(0 > ret) 27 { 28 perror("pthread_create"); 29 pthread_exit(NULL); 30 } 31 } 32 } 33 pthread_exit(NULL); 34 }
接收線程能夠實時接收申請鏈接服務器的客戶端,創建起服務器和客戶端的鏈接,經過for循環與標識符c_flag來判斷是否鏈接申請的客戶端。打個比方,就好像你去飯店吃飯,當你想進入飯店時,門口店員會先環視店內(for循環)、確認是否有座(num_chat<nmax_chat?)、有無被預約(client[i].flag),有則安排進店,無則確認店外的等候座椅是否還有空位(num_wait<nmax_wait?),有則安排座位在店外等候(listen(nmax_wait)),無則沒法安排。對於能夠進店的用戶,店員會安排好座位(client[i].c_fd)並遞上菜單(client[i].c_flag),點好菜後準備上菜(pthread_com)。
2.3 pthread_com 信息傳輸線程
void* pthread_com(void* arg) { Client* client2 = arg; Msg msg = {}; // 發送該線程對應用戶消息及進出聊天室狀況 char name[31] = {}; // 記錄、發送該線程對應用戶暱稱 int ret = recv(client2->c_fd,&msg,sizeof(msg),0); strcpy(name,msg.m_name); //printf("name:%s fd:%d flag:%d num_chat:%d\n",name,client2->c_fd,client2->c_flag,num_chat); char buf[34] = "***"; // sizeof(buf) = sizeof(name) + 3 bzero(&msg,sizeof(msg)); strcpy(msg.m_name,strcat(buf,name)); sprintf(msg.m_buf,"已進入聊天室[%d人]***",num_chat); for (int i = 0; i < nmax_chat; i++) { if(client[i].c_flag == true) send(client[i].c_fd,&msg,sizeof(msg),0); } //通訊 while(1) { bzero(&msg,sizeof(msg)); int ret = recv(client2->c_fd,&msg,sizeof(msg),0); if (0 < strlen(msg.m_buf)) { if(!strcmp("q",msg.m_buf)) { num_chat--; client2->c_flag = false; //printf("name:%s fd:%d flag:%d num_chat:%d\n",name,client2->c_fd,client2->c_flag,num_chat); char buf[34] = "***"; // sizeof(buf) = sizeof(name) + 3 bzero(&msg,sizeof(msg)); strcpy(msg.m_name,strcat(buf,name)); sprintf(msg.m_buf,"已退出聊天室[%d人]***",num_chat); for (int i = 0; i < nmax_chat; i++) { if(client[i].c_flag == true) send(client[i].c_fd,&msg,sizeof(msg),0); } break; } else { for (int i = 0; i < nmax_chat; i++) { //printf("返回了數據:%s\n",msg.m_buf); strcpy(msg.m_name,name); if(client[i].c_flag == true) send(client[i].c_fd,&msg,sizeof(msg),0); } } } } pthread_exit(NULL); }
信息傳輸線程主要承擔起郵局的功能,將各個客戶端投遞到郵局的信件(msg)配送至收件人(client n)手中,同時將一些意外事件(人員變更)轉達給寄件人收件人(client n)。不過相比真正的郵局仍是有很大差異的,這家郵局不只會複製你的信件(strcpy),還有可能夾雜私貨、更改內容(敏感詞屏蔽(未實現))。
4、總結
總的來講,最近此次編程讓我意識到一個很大的問題。之前老是在作以前想太多,老是但願可以一次性設計好整個架構,然而這是創建在必定的項目經驗上的。就我目前而言暫不具有這樣的水平,因此在編寫程序時就很容易致使代碼臃腫、邏輯混亂,從而致使難以調試。
因此此次編程更改了思路:先將項目拆分紅一個個小的功能模塊,底層搭好後再按照功能層級逐個實現,一個一個拼接、一塊磚一塊磚的搭建,邊搭建邊微調框架,最終完成程序的編寫。這種編程思路在此次練習中起到了很大的做用。整個編程過程思路清晰、編寫流暢,代碼的調試也輕鬆許多。
以上即是我此次練習的總結,發佈博客以供記錄、總結,博客中若有紕漏歡迎指出,歡迎討論、共同進步。