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