日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

【计算机网络】Socket聊天室程序

發(fā)布時(shí)間:2024/1/8 编程问答 24 豆豆
生活随笔 收集整理的這篇文章主要介紹了 【计算机网络】Socket聊天室程序 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

計(jì)算機(jī)網(wǎng)絡(luò)第一次實(shí)驗(yàn)報(bào)告

實(shí)驗(yàn)名稱:Socket聊天室程序

實(shí)驗(yàn)內(nèi)容

使用流式Socket設(shè)計(jì)聊天協(xié)議,聊天信息帶有時(shí)間標(biāo)簽和類型標(biāo)簽,本報(bào)告中將說明交互消息的類型、語法、語義、時(shí)序等具體的消息處理方式。對聊天程序進(jìn)行設(shè)計(jì),本報(bào)告將給出模塊劃分、模塊的功能、具體核心函數(shù)的展示和模塊的流程圖。在Windows系統(tǒng)下,利用C++對設(shè)計(jì)的程序進(jìn)行實(shí)現(xiàn),對實(shí)現(xiàn)的程序進(jìn)行測試,發(fā)現(xiàn)可以實(shí)現(xiàn)聊天室的功能。最后對實(shí)驗(yàn)過程中遇到的問題和產(chǎn)生的思考進(jìn)行總結(jié)。

協(xié)議設(shè)計(jì)

整體流程

在本次的實(shí)驗(yàn)中主要就是需要捋清楚客戶端和服務(wù)器端分別做了什么,由于本實(shí)驗(yàn)實(shí)現(xiàn)的是多客戶端的群聊,所以:

服務(wù)器接收多個(gè)客戶端的連接,多線程來解決多客戶端通信問題

對于服務(wù)器,首先應(yīng)加載和初始化socket,然后創(chuàng)建socket,綁定服務(wù)器端的socket和IP地址,開始監(jiān)聽等待客戶端的連接。如果有客戶端的連接,則啟動一個(gè)線程來和這個(gè)客戶端通信,并且在接收客戶端的連接后創(chuàng)建一個(gè)新的socket,這一點(diǎn)通過全局的socket類型的數(shù)組來實(shí)現(xiàn)。

在線程內(nèi)部,實(shí)現(xiàn)發(fā)送消息和接收消息,具體的收發(fā)方式在下面部分的“消息的發(fā)送接收”中解釋。首先接收客戶端發(fā)來的數(shù)據(jù),然后將數(shù)據(jù)廣播給當(dāng)前所有的連接到服務(wù)器的客戶端,這一點(diǎn)通過for循環(huán)來實(shí)現(xiàn)。最后關(guān)閉socket并清理環(huán)境。

對于客戶端,首先應(yīng)加載和初始化socket,然后創(chuàng)建socket,綁定socket和IP地址,然后連接服務(wù)器。發(fā)送消息是在主函數(shù)中完成向服務(wù)器發(fā)送消息,接收消息時(shí)需要創(chuàng)建線程來接收消息,這樣做可以避免在主函數(shù)接收消息而導(dǎo)致的主線程阻塞的情況。最后關(guān)閉socket并清理環(huán)境。

消息的類型

在協(xié)議中規(guī)定消息的類型為:

enum type {CHAT,EXIT };

CHAT為普通的客戶端與客戶端或客戶端與服務(wù)器端的聊天消息。

EXIT為客戶端斷開連接離開聊天室的消息。

消息的語法

定義消息的語法如下,包含type,time,content三個(gè)字段,每個(gè)字段通過’\n’分割,在“各模塊功能”部分將對message結(jié)構(gòu)體向string類型消息的轉(zhuǎn)換的函數(shù)進(jìn)行詳細(xì)介紹。

struct message {type type;string time;string content; };

消息的語義

字段中信息代表的具體含義如下:

**type:**表示消息的類型,有CHAT和EXIT兩種;

**time:**表示消息發(fā)送/接收的時(shí)間,格式為——年-月-日 時(shí):秒:分;

**content:**表示要發(fā)送的消息的文本內(nèi)容。

消息的發(fā)送接收

消息的發(fā)送:

1.在客戶端輸入要發(fā)送的消息,回車代表一條消息內(nèi)容的結(jié)束。

2.將消息通過加上時(shí)間戳、判斷和增加消息類型等方法后以message結(jié)構(gòu)體類型進(jìn)行保存。

3.再通過轉(zhuǎn)換函數(shù)將message結(jié)構(gòu)體轉(zhuǎn)換為字符串類型,并放入字符型數(shù)組中存儲和發(fā)送

4.以字符型數(shù)組形式發(fā)送消息后,判斷消息類型,對于EXIT類型的消息則打印日志后將該客戶端斷開連接;對于CHAT類型的普通消息則以規(guī)范格式進(jìn)行輸出打印。

消息的接收:

1.收到了以字符型數(shù)組發(fā)送的消息,將其存儲為字符串形式

2.通過轉(zhuǎn)換函數(shù)將字符串轉(zhuǎn)換為message結(jié)構(gòu)體類型保存。

3.判斷消息類型,對于EXIT類型的消息則打印日志后將該客戶端斷開連接;對于CHAT類型的普通消息則以規(guī)范格式進(jìn)行輸出打印。

各模塊功能

Sever主線程循環(huán)接收客戶端的連接

在服務(wù)器端,完成加載和初始化socket,然后創(chuàng)建socket,綁定服務(wù)器端的socket和IP地址,開始監(jiān)聽等待客戶端的連接等基本任務(wù)后,就進(jìn)入了主線程循環(huán)接收客戶端連接的過程。對不超過聊天室最大容量 MaxClientNum的情況進(jìn)行循環(huán),直到超出容量則打印聊天室已滿的日志。對于正常的客戶端連接情況,需要用accept函數(shù)完成對客戶端連接請求的接收,并對聊天室當(dāng)前人數(shù)進(jìn)行更新,然后判別連接是否正常。對于正常的連接,則創(chuàng)建線程完成消息的發(fā)送和接收

int i = 0;while (i < MaxClientNum){//接收客戶端的連接請求的socketsockaddr_in addrClient{};int len = sizeof addrClient;ClientSocket[i] = accept(sockSrv, (SOCKADDR*)&addrClient, &len);CNUM++;if (ClientSocket[i] == SOCKET_ERROR){cout << "[INFO]: wrong client" << endl;closesocket(sockSrv);WSACleanup();}cout << "[INFO]: ACCEPT SOCKET SUCCEED " << "client" << i << " join in" << endl;cout << "---------------------------------------------------------------------" << endl;CloseHandle( CreateThread(NULL, NULL,handlerRequest, (LPVOID)i, NULL, NULL));i++;}cout << "[INFO]: the chatroom is full!" << endl;

Sever發(fā)送和接收消息

在線程內(nèi)部實(shí)現(xiàn)和客戶端的通信,對于接收數(shù)據(jù),在while(1)循環(huán)中不斷使用recv函數(shù)接收消息并判斷消息是否接收成功。對沒有接收成功的情況分為兩部分進(jìn)行處理,如果錯誤編號為10054即client關(guān)閉則也自動退出,其他情況則打印錯誤日志并break。如果接收成功,則判斷消息類型,對于EXIT類型的消息則完成日志打印并關(guān)閉該socket,對于CHAT類型的消息則按照格式輸出。

在發(fā)送階段,遍歷此時(shí)的所有在線的客戶端,給每一個(gè)非消息發(fā)送者的客戶端轉(zhuǎn)發(fā)該條消息,并打印出消息發(fā)送者的名字。然后判斷消息是否發(fā)送成功,對沒有發(fā)送成功的情況分為兩部分進(jìn)行處理,如果錯誤編號為10054即client關(guān)閉則也自動退出,其他情況則打印錯誤日志并break,如果發(fā)送成功則打印日志即可。

DWORD WINAPI handlerRequest(LPVOID lparam) {int i = (int)lparam;int recvflag;//和客戶端通信,發(fā)送接收數(shù)據(jù)char Buf[MaxBufNum];char SBuf[MaxBufNum];char SendBuf[MaxBufNum];char tmp[MaxBufNum];char Send_content[MaxBufNum];while (1){memset(Buf, 0, MaxBufNum);memset(SendBuf, 0, MaxBufNum);memset(tmp, 0, MaxBufNum);//接收recvflag = recv(ClientSocket[i], Buf, MaxBufNum, 0);if (recvflag != SOCKET_ERROR){//退出標(biāo)志是exitstring sss = Buf;message mess_server_recv = stringtomessage(sss);if (mess_server_recv.type == EXIT){cout << "[INFO]: CLIENT EXIT " << "client" << i << " leave" << endl;cout << "---------------------------------------------------" << endl;send(ClientSocket[i], R"([INFO]: exit succeed)", MaxBufNum, 0);ClientSocket[i] = NULL;closesocket(ClientSocket[i]);break;}cout << mess_server_recv.time << "[CHAT]: " << "receive from client" << i << ":" << mess_server_recv.content << endl;//發(fā)送for (int j = 0; j <CNUM; j++){int sendflag=0;if (j != i&&ClientSocket[j]!=NULL){string s = "[RECV]: from client";char ch;ch = i + 48;s.push_back(ch);strncpy_s(tmp, s.c_str(), s.length());send(ClientSocket[j], tmp, MaxBufNum, 0);//sendflag = send(ClientSocket[j], Bufcon, sizeof(Buf), 0);sendflag = send(ClientSocket[j], Buf, MaxBufNum, 0);if (sendflag == SOCKET_ERROR){if (WSAGetLastError() == 10054){cout << "[CLIENT EXIT]: " << "client" << j << " leave" << endl;//CNUM--;closesocket(ClientSocket[j]);}else{cout << "[INFO]: fail to send message" << endl;}}elsecout << "[INFO]: send succeed" << endl;}}}else{//如果client關(guān)閉則也自動退出if (WSAGetLastError() == 10054){cout << "[INFO]: CLIENT EXIT " << "client" << i << " leave" << endl;closesocket(ClientSocket[i]);//CNUM--;break;}else{cout << "[INFO]: fail to receive message " << endl;break;}}}return 0; }

Client發(fā)送消息

在客戶端中,消息的發(fā)送是在主線程中進(jìn)行的,首先在客戶端中通過connet函數(shù)連接到服務(wù)器,然后創(chuàng)建線程進(jìn)行消息的接收。對于消息的發(fā)送,為保證可以多條消息連續(xù)發(fā)送,在while(1)循環(huán)中進(jìn)行,輸入發(fā)送內(nèi)容后,通過localtime函數(shù)獲取時(shí)間戳并判斷消息類型后封裝進(jìn)message結(jié)構(gòu)體并轉(zhuǎn)化為字符型數(shù)組完成發(fā)送。對發(fā)送不成功的情況則打印日志并退出,對發(fā)送成功的情況判斷消息類型并完成輸出。

int conflag=connect(sockCli, (SOCKADDR*)&addrCli, sizeof(addrCli));if (conflag == -1)cout << "[INFO]: fail to connect" << endl;else{cout << "[INFO]: CONNECT succeed" << endl;cout << "---------------------------------------------------" << endl;}CreateThread(NULL, 0, &receivemessage, LPVOID(sockCli), NULL, NULL);char SBuf[MaxBufNum] = {};memset(SBuf, 0, MaxBufNum);char Send_content[MaxBufNum] = {};memset(Send_content, 0, MaxBufNum);message mess_client_send;while (1){//發(fā)送cin.getline(Send_content, MaxBufNum);mess_client_send.content = Send_content;//獲取時(shí)間char timestamp[100] = { 0 };time_t t = time(0);strftime(timestamp, sizeof(timestamp), "%Y-%m-%d %H:%M:%S", localtime(&t));mess_client_send.time = timestamp;//判斷消息類型if (mess_client_send.content== "exit")mess_client_send.type = EXIT;elsemess_client_send.type = CHAT;strcpy_s(SBuf, messagetostring(mess_client_send).c_str());int sendflag = send(sockCli, SBuf, MaxBufNum, 0);if (sendflag ==SOCKET_ERROR){cout << "[INFO]: fail to send" << endl;cout << "---------------------------------------------------" << endl;}else{if (mess_client_send.type == EXIT){cout << "[INFO]: exit" << endl;cout << "---------------------------------------------------" << endl;break;}else if(mess_client_send.type==CHAT){cout << "[INFO]: send succeed" << endl;cout << "---------------------------------------------------" << endl;}}}

Client接收消息

為了避免在主函數(shù)中接收消息而導(dǎo)致的主線程阻塞的情況,客戶端在接收消息時(shí)需要創(chuàng)建線程來接收消息。線程內(nèi)部,在while(1)循環(huán)中用recv函數(shù)進(jìn)行消息的接收,對接收成功的情況完成message類型的轉(zhuǎn)換并判斷消息類型,對不同的消息類型以不同格式輸出。

DWORD WINAPI receivemessage(LPVOID IpParameter) {SOCKET sockCli = (SOCKET)(LPVOID)IpParameter;char RBuf[MaxBufNum] = {};memset(RBuf, 0, MaxBufNum);int recvflag = 0;while (1){recvflag = recv(sockCli, RBuf, MaxBufNum, 0);if (recvflag != SOCKET_ERROR){string ss = RBuf;message mess_client_recv = stringtomessage(ss);if (mess_client_recv.type == CHAT){cout << mess_client_recv.time << " " << "[CHAT]: " << mess_client_recv.content << endl;}else{cout << mess_client_recv.time << " " << "[EXIT]" << endl;break;}}else break;}return 0; }

消息處理函數(shù)

由于對于消息的結(jié)構(gòu)體格式需要在傳輸過程中修改為字符數(shù)組格式進(jìn)行接收和發(fā)送,比較重要的內(nèi)容是對結(jié)構(gòu)體message、字符串、字符型數(shù)組進(jìn)行轉(zhuǎn)換。對于messgae類型轉(zhuǎn)換為字符串,只需將不同消息類型以不同標(biāo)號代替并放在字符串首個(gè)字符,然后將時(shí)間字符串和消息內(nèi)容字符串連接在后面,三者之間以換行符‘\n’分割。對于字符串轉(zhuǎn)換為message類型,需要首先識別字符串的首個(gè)字符確定消息類型,然后以換行符為分割將字符串切割開,依次賦給時(shí)間和消息內(nèi)容部分。

//消息類型轉(zhuǎn)換為字符串 string messagetostring(message mes) {string ss;if (mes.type == CHAT)ss = '1';else if (mes.type == EXIT)ss = '2';ss += '\n' + mes.time + '\n' + mes.content + '\n' ;return ss; } //字符串轉(zhuǎn)換為消息類型 message stringtomessage(string ss) {message mes;int i = 2;int timelen = 0;if (ss[0] == '1')mes.type = CHAT;else if (ss[0] == '2')mes.type = EXIT;while (ss[i] != '\n'){timelen++;i++;}mes.time = ss.substr(2, timelen);mes.content = ss.substr(3 + static_cast<std::basic_string<char, std::char_traits<char>, std::allocator<char>>::size_type>(timelen), sizeof(ss) - timelen - 1);return mes; }

實(shí)現(xiàn)效果及說明

懶得上傳圖片了,以后再說
開啟階段:
發(fā)送和接收消息:

實(shí)現(xiàn)群聊功能:

可以多條信息連續(xù)發(fā)送:

服務(wù)器端日志信息:

退出后無法再發(fā)送和接收消息:

遇到的問題及解決

**Q1:**剛開始的設(shè)計(jì)對消息的發(fā)送格式和存儲格式等的轉(zhuǎn)化不清晰

**solution1:**需要明確的是在發(fā)送和接收消息的時(shí)候必須使用的是字符型數(shù)組,但是設(shè)計(jì)的存儲消息的message結(jié)構(gòu)體并不是字符型數(shù)組格式的。所以需要對這點(diǎn)進(jìn)行轉(zhuǎn)換,先把message結(jié)構(gòu)體通過確定方式轉(zhuǎn)換為字符串類型,這兩者的轉(zhuǎn)換直接封裝成函數(shù)了,返回再把字符串和字符型數(shù)組進(jìn)行轉(zhuǎn)換即可。

**Q2:**在服務(wù)器端給客戶端發(fā)送消息的時(shí)候條件判斷不明確

**solution2:**首先需要知道,服務(wù)器端要給所有的非消息來源的客戶端發(fā)送消息,也就是如果服務(wù)器收到的消息來源于客戶端i,則在發(fā)送時(shí)需要給除了i的所有在線客戶端發(fā)送消息,這里不能自己再給自己發(fā)了。然后需要對ClientSocket這個(gè)socket數(shù)組進(jìn)行判斷,對于非空的情況發(fā)送消息,因?yàn)槿绻麛?shù)組為空意味著給socket已經(jīng)沒有了,這個(gè)客戶端已經(jīng)斷開連接了,不能再向這個(gè)客戶端發(fā)消息了。

**Q3:**對于全局變量CNUM的定義不清楚

**solution3:**本來對CNUM的定義是當(dāng)前在服務(wù)器中的客戶端數(shù)目,但是由于客戶端的編號是一直增加的,所以即使有客戶端退出了,CNUM的值也不應(yīng)當(dāng)減,只有這樣才可以保證在服務(wù)器端發(fā)送消息的時(shí)候把所有的客戶端都發(fā)送到了而沒有漏掉一些客戶端。

**Q4:**addrSrv.sin_addr.s_addr = inet_addr(“127.0.0.1”);的使用報(bào)錯

**solution4:**改為

inet_pton(AF_INET, "127.0.0.1", &addrSrv.sin_addr.s_addr);addrSrv.sin_family = AF_INET;addrSrv.sin_port = htons(6666);

**Q5:**報(bào)錯問題:

bind(sockSrv, (SOCKADDR*)&addrSrv, sizeof(SOCKADDR));//綁定服務(wù)器端的socket和地址if (bind(sockSrv, (SOCKADDR*)&addrSrv, sizeof(SOCKADDR))==-1){cout << "[INFO]: BIND wrong" <<endl;}

**solution5:**這里相當(dāng)于bind進(jìn)行了兩遍,所以是會報(bào)錯的,應(yīng)該借用一個(gè)標(biāo)志來判斷,改成如下:

int bindflag=bind(sockSrv, (SOCKADDR*)&addrSrv, sizeof(SOCKADDR));//綁定服務(wù)器端的socket和地址if (bindflag==-1){cout << "[INFO]: BIND wrong" <<endl;}

**Q6:**對sockaddr&sockaddr_in的區(qū)別和關(guān)系搞不清楚

solution6:

sockaddr缺陷是sa_data把目標(biāo)地址和端口信息混在一起了

struct sockaddr {

? sa_family_t sin_family;//地址族

? char sa_data[14]; //14字節(jié),包含套接字中的目標(biāo)地址和端口信息

};

sockaddr_in:

typedef struct sockaddr_in {
short sin_family; //地址族
u_short sin_port; //16位TCP/UDP端口號
struct in_addr sin_addr; //32位IP地址
char sin_zero[8]; //不使用
} SOCKADDR_IN, *PSOCKADDR_IN, *LPSOCKADDR_IN;

其中的結(jié)構(gòu)體in_addr也有相應(yīng)的定義,用來存放32位IP地址

總結(jié):

二者長度一樣,都是16個(gè)字節(jié),即占用的內(nèi)存大小是一致的,因此可以互相轉(zhuǎn)化。二者是并列結(jié)構(gòu),指向sockaddr_in結(jié)構(gòu)的指針也可以指向sockaddr。sockaddr常用于bind、connect、recvfrom、sendto等函數(shù)的參數(shù),指明地址信息,是一種通用的套接字地址。
sockaddr_in 是internet環(huán)境下套接字的地址形式。所以在網(wǎng)絡(luò)編程中我們會對sockaddr_in結(jié)構(gòu)體進(jìn)行操作,使用sockaddr_in來建立所需的信息,最后使用類型轉(zhuǎn)化就可以了。一般先把sockaddr_in變量賦值后,強(qiáng)制類型轉(zhuǎn)換后傳入用sockaddr做參數(shù)的函數(shù)。

sin_addr; //32位IP地址
char sin_zero[8]; //不使用
} SOCKADDR_IN, *PSOCKADDR_IN, *LPSOCKADDR_IN;

其中的結(jié)構(gòu)體in_addr也有相應(yīng)的定義,用來存放32位IP地址

總結(jié):

二者長度一樣,都是16個(gè)字節(jié),即占用的內(nèi)存大小是一致的,因此可以互相轉(zhuǎn)化。二者是并列結(jié)構(gòu),指向sockaddr_in結(jié)構(gòu)的指針也可以指向sockaddr。sockaddr常用于bind、connect、recvfrom、sendto等函數(shù)的參數(shù),指明地址信息,是一種通用的套接字地址。
sockaddr_in 是internet環(huán)境下套接字的地址形式。所以在網(wǎng)絡(luò)編程中我們會對sockaddr_in結(jié)構(gòu)體進(jìn)行操作,使用sockaddr_in來建立所需的信息,最后使用類型轉(zhuǎn)化就可以了。一般先把sockaddr_in變量賦值后,強(qiáng)制類型轉(zhuǎn)換后傳入用sockaddr做參數(shù)的函數(shù)。

sockaddr_in用于socket定義和賦值;sockaddr用于函數(shù)參數(shù)。

總結(jié)

以上是生活随笔為你收集整理的【计算机网络】Socket聊天室程序的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。