【计算机网络】Socket聊天室程序
計(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)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 有零基础开始学习python的小伙伴吗?
- 下一篇: Photoshop生成320*320像素