C++跨平台开发——SOCKET网络编程中实现客户端对聊
一、案例
C++跨平臺開發——關于解決SOCKET網絡編程中客戶端對聊的問題
二、實現分析
1、為什么服務器開啟線程而不是進程?
????????線程的開銷小,啟動快,共享數據(不需要ipc就可以實現交互),所以一個線程一個客戶端(實際上效率提升將近10倍)。
2、創建線程的時候傳accepfd的原因
????????accepfd記錄客戶端,客戶端每上線一個就覆蓋一個accepfd,第一個accepfd就沒了,利用值傳參并保存下來。線程永遠都停留在死循環內,保存下來的fd就不會被替換掉。(知道客戶端下線)
3、自定義通信協議
????????創新頭文件寫入請求頭和請求體
4、客戶端如何發送數據
錯誤1:
????????客戶端連接上后在while循環中直接發出請求、接收數據(write 、 read)
錯誤分析:
????????read(阻塞函數)用來接收服務器返回來的結果,但如果服務器處理某個業務要花費很長時間,網絡很慢、數據很長,read就不能及時返回,卡了半小時還沒有回復。就會導致write發送完請求,然后進行read沒有收到結果,就不能繼續write(write 、 read相互制約)
????????write 完不一定就必須立刻read(就好像打游戲的時候你掛機(沒有write),但是依然會接收到服務器發回來的界面數據)
????????所以對于客戶端來說就需要讓write 和read同時存在,并且同時運行——創建讀、寫兩個線程并進行傳參
注意:
????????創建線程不能寫在死循環里面,但如果沒有死循環也是錯的。
????????如果加上循環變成循環開啟讀寫線程,造成電腦卡死;如果不在死循環內,創建線程完直接走到return ,導致main中的子進程結束,因為線程包含在進程當中(共享進程開辟的cpu、內存等資源),線程自然也消失了。(即進程沒了線程就沒了)——再次驗證了進程是資源分配的基本單位,線程共享進程的所有資源。
????????所以客戶端連接上必須開啟死循環
技巧:一旦到了線程執行函數就開啟死循環
寫線程函數體部分:結構體在內存中以什么樣的形式存放?
以字節的形式存放。(如果是字符,只能存char類型,沒有什么字符可以表示結構體),字節的長度用sizeof求得,如果用strlen就會導致客戶端收不到服務器發送的消息
結構體還遵循內存對齊的原則,連續存放
怎么把兩個結構體(用戶、請求頭)一起往外發?
創建一個buf數組來動態獲取結構體的長度(不定長,根據業務情況)
注意:
以下這樣調用memcpy會將前面的數據進行覆蓋,要利用指針的偏移計算
指針的偏移計算:
前面拷貝的是一個head結構體,buf從首元素開始,偏移整個head,將之前拷貝的數據跨過去,找到沒有數據的那個地址再進行拷貝user(如圖)
使得能正好存下兩個結構體的大小,節省空
注意:
沒有必要再創建一個結構體放請求頭和請求體,因為不同的業務有不同的長度,導致長度不能確定,如果你按照最大的業務長度來計算,而這些業務你又可能用不到,就會浪費空間(就像你打開qq客戶端,不能保證你會用到里面所有的業務,可能就只進行聊天或者說開了100k的空間只存放了50字節)
5、服務器如何接收數據?
讀的時候先讀請求頭
錯誤2:
?錯誤分析:
????????上圖中類型和長度分別對應 1、 40是正確的。但是后面一堆數據是有問題的,因為在服務器這邊僅僅只接收了請求頭, 后面的數據還沒有接收到就一直死循環打印
注意:
????????Socket通信中,存放的數據被讀走就沒了,沒被讀走的那部分還是保留在Socket中,那么這個時候還會觸發read,就打印了出來。所以接下來判斷是什么業務的時候還要繼續讀Socket剩下部分(用戶信息)
????????接著讀多少長度是根據業務長度,而不是直接利用sizeof(USERINFO),僅有當創建的緩沖區跟sizeof(USERINFO)一樣大的情況下才不會出現問題,否則哪怕就丟了一個字節也不行。
????????一方面,保證接收的剛剛好,另一方面,按照業務長度進行驗證(安全),發多少收多少,還可以根據read的返回值判斷接收是否成功,如果數據丟了,返回的res一定比給定長度小,如果發多了(粘包),也不會接收多余沒用的數據(告訴我長度20個字節,卻發了25個字節,后面5個字節不會接收,服從客戶端的要求)。
服務器判斷如果進行登錄業務的話,就進行驗證并加入在線用戶的容器
發送結果——反饋是否操作成功——客戶端添加并讀取反饋
寫線程怎么知道讀線程的數據??
聊天之前要先判斷登錄是否成功(客戶端讀線程),登錄成功才開始聊天(客戶端寫線程)——設置全局變量進行判斷
接著定義聊天結構體,定義請求頭,并拷貝數據
技巧:
在寫read和write的時候一定要記得帶上返回值,利用返回值來查看,至少可以避免一端的問題。
處理業務時,讀取完數據后先進行判斷,可以先做打印測試
服務器這邊的聊天業務怎么處理?
Map容器怎么通過key來查找值?
1、Find????????2、[]
錯誤3:
錯誤分析:
圖中服務器端打印的onlineMap[sev_chat.recvID] = 0,即查不到要接受者的id,所以map容器第一個參數不能定義成char*指針類型(地址),因為服務器操作針對所有的客戶端,地址就可能會改變(或者數據變),應修改為string類型
補充:
1、TCP存在問題(數據問題)
- 可能丟包
- 可能粘包
- 可能半包?
2、Linux中怎么同時編譯多個文件的方法(.h ?.cpp)
- 編寫makefile
- 直接執行vs2019的bin目錄下的可執行文件.out
?三、核心代碼
//客戶端 int main() {pthread_t read_thrad;pthread_t write_thrad;struct sockaddr_in s_addr;int socketfd = 0;int length = 0;int acceptfd = 0;//客戶端的文件描述符char cli_buf[120] = { 0 };//初始化網絡socketfd = socket(AF_INET, SOCK_STREAM, 0);//AF_INET表示使用ipv4,SOCK_STREAM表示流式套接字if (socketfd == -1){perror(" socket error");}else{//確定使用那個協議族 ipv4s_addr.sin_family = AF_INET;//連接服務器的地址s_addr.sin_addr.s_addr = inet_addr("127.0.0.1");//ip地址的轉換s_addr.sin_port = htons(10086);length = sizeof(s_addr);//準備通道不做綁定操作而是直接連接if (connect(socketfd, (struct sockaddr*)&s_addr, length) == -1){perror(" connect error");}else{cout << "客戶端連接成功 acceptfd = " << endl;//進入線程執行函數要開啟死循環if (pthread_create(&read_thrad, NULL, read_thread_func, &socketfd)){perror("pthread_create error");}if (pthread_create(&write_thrad, NULL, write_thread_func, &socketfd)){perror("pthread_create error");}//為了不讓線程馬上結束while (true){}}}return 0; }void* read_thread_func(void* p) {BACK resback;CHARMSG resmsg;int rfd = *(int*)p;while (true){read(rfd,&resback,sizeof(BACK));cout << "resback.businessType" << resback.businessType << endl;cout << "resback.flag" << resback.flag << endl;if (resback.businessType == 1){if (resback.flag == 1){isRun = 1;}else{isRun = 0;}}else if (resback.businessType == 2)//聊天{int res = read(rfd, &resmsg, sizeof(CHARMSG));cout << "chat content" << resmsg.content << endl;}} } //寫線程執行函數 void* write_thread_func(void* p) {//char buf[1024] = { 0 };HEAD myhead;USERINFO puser;CHARMSG pchat;int wfd = *(int*)p;cout << "請輸入賬號" << endl;cin >> puser.userName;cout << "請輸入密碼" << endl;cin >> puser.pwd;myhead.businessType = 1;//業務類型myhead.businessLen = sizeof(USERINFO);//根據業務情況動態獲取結構體長度//結構體對應字節,不能用strlenchar buf[sizeof(HEAD)+sizeof(USERINFO)] = { 0 };memcpy(buf,&myhead,sizeof(HEAD));memcpy(buf+sizeof(HEAD),&puser,sizeof(USERINFO));write(wfd,buf,sizeof(buf));//做聊天用while (true){if (isRun == 1){cout << "登錄成功 進行聊天" << endl;cout << "請輸入賬號" << endl;cin >> pchat.sendID;cout << "請輸入聊天對象" << endl;cin >> pchat.recvID;cout << "請輸入聊天內容" << endl;cin >> pchat.content;bzero(&myhead,sizeof(myhead));myhead.businessType = 2;myhead.businessLen = sizeof(CHARMSG);//這里的buf不能使用前面的char chat_buf[sizeof(HEAD) + sizeof(CHARMSG)] = { 0 };memcpy(chat_buf, &myhead, sizeof(HEAD));memcpy(chat_buf + sizeof(HEAD), &pchat, sizeof(CHARMSG));int res = write(wfd, chat_buf, sizeof(chat_buf));if (res > 0){cout << "-------------------聊天信息發送成功-------------------" << endl;}}}}int main() {struct sockaddr_in s_addr;int socketfd = 0;int length = 0;int acceptfd = 0;//客戶端的文件描述符char ser_buf[66] = { 0 };int pid = 0;//初始化網絡socketfd = socket(AF_INET,SOCK_STREAM,0);if (socketfd == -1){perror(" socket error");}else{//確定使用那個協議族 ipv4s_addr.sin_family = AF_INET;//系統自動獲取本機ip地址s_addr.sin_addr.s_addr = INADDR_ANY;//端口65535,10000以下是操作系統使用,自己定義需要10000以后s_addr.sin_port = htons(10086);length = sizeof(s_addr);//綁定ip地址和端口號if (bind(socketfd,(struct sockaddr*)&s_addr,length) == -1){perror(" bind error");}//監聽這個地址和端口有沒有客戶端連接if (listen(socketfd,10) == -1){perror(" listen error");}cout << "服務器網絡通道準備好了" << endl;//死循環保證服務器長時間在線pthread_t thread_id;while (true){cout << "等待客戶端上線" << endl;//等待客戶端上線,地址和端口號已經設置過了,所以為null,如果沒有客戶端訪問則一直被動等待//返回值就表示那個客戶端(給客戶端發消息不需要知道客戶端的ip地址)acceptfd = accept(socketfd, NULL, NULL);//阻塞函數cout << "客戶端連接成功 acceptfd = " << acceptfd << endl;if (pthread_create(&thread_id, NULL, pthread_func, &acceptfd)){perror("pthread_create error");}}}return 0; }//服務器 //1、接收客戶端請求,并為客戶端服務,2、處理客戶端請求 3、返回業務處理結果(發送數據)void* pthread_func(void* p) {HEAD h;USERINFO uinfo;CHARMSG sev_chat;int acptfd = *(int*)p;//賦值的是值,進入線程執行一次。//int *pfd = (int *)p;//賦值的是地址,覺得不可以使用指針while (true)//兩個while同時跑{read(acptfd,&h,sizeof(HEAD));cout << "h.businessType = " << h.businessType << endl;cout << "h.businessLen = " << h.businessLen << endl;if (h.businessType == 1){int res = read(acptfd,&uinfo, h.businessLen); if (res == h.businessLen){cout << ".userName = " << uinfo.userName << endl;cout << ".pwd = " << uinfo.pwd << endl;//進行驗證并加入在線用戶的容器 onlineMap[uinfo.userName] = acptfd;cout << ".onlineUser = " <<onlineMap.size()<< endl;//發送結果——通知是否成功——客戶端添加反饋BACK back;back.businessLen = 0;back.businessType = 1;back.flag = 1;//表示操作成功char buf[sizeof(BACK)] = { 0 };memcpy(buf,&back,sizeof(BACK));write(acptfd,buf,sizeof(buf));}}else if (h.businessType == 2)//處理聊天業務{int res = read(acptfd,&sev_chat,h.businessLen);if (res == h.businessLen){cout << "sev_chat.sendID = " << sev_chat.sendID << endl;cout << "sev_chat.recvID = " << sev_chat.recvID << endl;cout << "sev_chat.content = " << sev_chat.content << endl;cout << "長度 = " << sizeof(sev_chat.recvID) << endl;cout << "onlineMap[sev_chat.recvID] = " << onlineMap[sev_chat.recvID] << endl;char chat_buf[sizeof(BACK) + sizeof(CHARMSG)] = { 0 };BACK fbk;fbk.businessLen = sizeof(sev_chat);fbk.businessType = 2;fbk.flag = 1;//表示操作成功memcpy(chat_buf, &fbk, sizeof(BACK));memcpy(chat_buf + sizeof(BACK), &sev_chat, sizeof(sev_chat));write(acptfd,&chat_buf, sizeof(chat_buf));write(onlineMap[sev_chat.recvID], &chat_buf, sizeof(chat_buf));}}} }四、運行效果
客戶端:?
服務器:?
總結
以上是生活随笔為你收集整理的C++跨平台开发——SOCKET网络编程中实现客户端对聊的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: java学习(98):线程join使用中
- 下一篇: s3c2440移植MQTT