1V1實現了,1V多也就容易了。不過相對於1V1的程序,我經過大改,采用鏈表來動態管理。這樣效率真的提升不少,至少CPU使用率穩穩的在20以下,不會飙到100了。用C語言寫這個還是挺費時間的,因為什麼功能函數都要自己寫,不像C++有STL庫可以用,MFC寫就更簡單了,接下來我還會更新MFC版本的多人聊天程序。好了,廢話少說,進入主題。
這個程序要解決的問題如下:
1.CPU使用率飙升問題 –>用鏈表動態管理
2.用戶自定義聊天,就是想跟誰聊跟誰聊 –> _Client結構體中新增一個ChatName字段,用來表示要和誰聊天,這個字段很重要,因為server轉發消息的時候就是按照這個字段來轉發的。
3.中途換人聊天,就是聊著聊著,想和別人聊,而且自己還一樣能接收到其它人發的消息 –> 這個就要小改客戶端的代碼了,可以在發送聊天消息之前插入一段代碼,用來切換聊天用戶。具體做法就是,用getch()函數讀取ESC鍵,如果用戶按了這個鍵,則表示想切換用戶,然後會輸出一行提示,請輸入chat name,就是想要和誰聊天的名字,發送這個名字過去之前要加一個標識符,表示這個消息是切換聊天用戶消息。然後server接收到這個消息後會判斷第一個字符是不是標識符,第二個字符不能是標識符,則根據這個name來查找當前在線的用戶,然後修改想切換聊天用戶的ChatName為name這個用戶。(可能有點繞,不懂的看代碼就清晰易懂了~)
4.下線後提醒對方 –> 還是老套路,只要send對方不通就當對方下線了。
編寫環境:WIN10,VS2015
效果圖:
為了方便就不用虛擬機演示了,但是在虛擬機是肯定可以的,應該說只要是局域網,能互相ping通就可以使用這個程序。
Server code:
鏈表頭文件:
#ifndef _CLIENT_LINK_LIST_H_ #define _CLIENT_LINK_LIST_H_ #include#include //客戶端信息結構體 typedef struct _Client { SOCKET sClient; //客戶端套接字 char buf[128]; //數據緩沖區 char userName[16]; //客戶端用戶名 char IP[20]; //客戶端IP unsigned short Port; //客戶端端口 UINT_PTR flag; //標記客戶端,用來區分不同的客戶端 char ChatName[16]; //指定要和哪個客戶端聊天 _Client* next; //指向下一個結點 }Client, *pClient; /* * function 初始化鏈表 * return 無返回值 */ void Init(); /* * function 獲取頭節點 * return 返回頭節點 */ pClient GetHeadNode(); /* * function 添加一個客戶端 * param client表示一個客戶端對象 * return 無返回值 */ void AddClient(pClient client); /* * function 刪除一個客戶端 * param flag標識一個客戶端對象 * return 返回true表示刪除成功,false表示失敗 */ bool RemoveClient(UINT_PTR flag); /* * function 根據name查找指定客戶端 * param name是指定客戶端的用戶名 * return 返回一個client表示查找成功,返回INVALID_SOCKET表示無此用戶 */ SOCKET FindClient(char* name); /* * function 根據SOCKET查找指定客戶端 * param client是指定客戶端的套接字 * return 返回一個pClient表示查找成功,返回NULL表示無此用戶 */ pClient FindClient(SOCKET client); /* * function 計算客戶端連接數 * param client表示一個客戶端對象 * return 返回連接數 */ int CountCon(); /* * function 清空鏈表 * return 無返回值 */ void ClearClient(); /* * function 檢查連接狀態並關閉一個連接 * return 返回值 */ void CheckConnection(); /* * function 指定發送給哪個客戶端 * param FromName,發信人 * param ToName, 收信人 * param data, 發送的消息 */ void SendData(char* FromName, char* ToName, char* data); #endif //_CLIENT_LINK_LIST_H_
鏈表cpp文件:
#include "ClientLinkList.h" pClient head = (pClient)malloc(sizeof(_Client)); //創建一個頭結點 /* * function 初始化鏈表 * return 無返回值 */ void Init() { head->next = NULL; } /* * function 獲取頭節點 * return 返回頭節點 */ pClient GetHeadNode() { return head; } /* * function 添加一個客戶端 * param client表示一個客戶端對象 * return 無返回值 */ void AddClient(pClient client) { client->next = head->next; //比如:head->1->2,然後添加一個3進來後是 head->next = client; //3->1->2,head->3->1->2 } /* * function 刪除一個客戶端 * param flag標識一個客戶端對象 * return 返回true表示刪除成功,false表示失敗 */ bool RemoveClient(UINT_PTR flag) { //從頭遍歷,一個個比較 pClient pCur = head->next;//pCur指向第一個結點 pClient pPre = head; //pPre指向head while (pCur) { // head->1->2->3->4,要刪除2,則直接讓1->3 if (pCur->flag == flag) { pPre->next = pCur->next; closesocket(pCur->sClient); //關閉套接字 free(pCur); //釋放該結點 return true; } pPre = pCur; pCur = pCur->next; } return false; } /* * function 查找指定客戶端 * param name是指定客戶端的用戶名 * return 返回socket表示查找成功,返回INVALID_SOCKET表示無此用戶 */ SOCKET FindClient(char* name) { //從頭遍歷,一個個比較 pClient pCur = head; while (pCur = pCur->next) { if (strcmp(pCur->userName, name) == 0) return pCur->sClient; } return INVALID_SOCKET; } /* * function 根據SOCKET查找指定客戶端 * param client是指定客戶端的套接字 * return 返回一個pClient表示查找成功,返回NULL表示無此用戶 */ pClient FindClient(SOCKET client) { //從頭遍歷,一個個比較 pClient pCur = head; while (pCur = pCur->next) { if (pCur->sClient == client) return pCur; } return NULL; } /* * function 計算客戶端連接數 * param client表示一個客戶端對象 * return 返回連接數 */ int CountCon() { int iCount = 0; pClient pCur = head; while (pCur = pCur->next) iCount++; return iCount; } /* * function 清空鏈表 * return 無返回值 */ void ClearClient() { pClient pCur = head->next; pClient pPre = head; while (pCur) { //head->1->2->3->4,先刪除1,head->2,然後free 1 pClient p = pCur; pPre->next = p->next; free(p); pCur = pPre->next; } } /* * function 檢查連接狀態並關閉一個連接 * return 返回值 */ void CheckConnection() { pClient pclient = GetHeadNode(); while (pclient = pclient->next) { if (send(pclient->sClient, "", sizeof(""), 0) == SOCKET_ERROR) { if (pclient->sClient != 0) { printf("Disconnect from IP: %s,UserName: %s\n", pclient->IP, pclient->userName); char error[128] = { 0 }; //發送下線消息給發消息的人 sprintf(error, "The %s was downline.\n", pclient->userName); send(FindClient(pclient->ChatName), error, sizeof(error), 0); closesocket(pclient->sClient); //這裡簡單的判斷:若發送消息失敗,則認為連接中斷(其原因有多種),關閉該套接字 RemoveClient(pclient->flag); break; } } } } /* * function 指定發送給哪個客戶端 * param FromName,發信人 * param ToName, 收信人 * param data, 發送的消息 */ void SendData(char* FromName, char* ToName, char* data) { SOCKET client = FindClient(ToName); //查找是否有此用戶 char error[128] = { 0 }; int ret = 0; if (client != INVALID_SOCKET && strlen(data) != 0) { char buf[128] = { 0 }; sprintf(buf, "%s: %s", FromName, data); //添加發送消息的用戶名 ret = send(client, buf, sizeof(buf), 0); } else//發送錯誤消息給發消息的人 { if(client == INVALID_SOCKET) sprintf(error, "The %s was downline.\n", ToName); else sprintf(error, "Send to %s message not allow empty, Please try again!\n", ToName); send(FindClient(FromName), error, sizeof(error), 0); } if (ret == SOCKET_ERROR)//發送下線消息給發消息的人 { sprintf(error, "The %s was downline.\n", ToName); send(FindClient(FromName), error, sizeof(error), 0); } }
server cpp:
/* #include#include #include #include "ClientLinkList.h" #pragma comment(lib,"ws2_32.lib") SOCKET g_ServerSocket = INVALID_SOCKET; //服務端套接字 SOCKADDR_IN g_ClientAddr = { 0 }; //客戶端地址 int g_iClientAddrLen = sizeof(g_ClientAddr); typedef struct _Send { char FromName[16]; char ToName[16]; char data[128]; }Send,*pSend; //發送數據線程 unsigned __stdcall ThreadSend(void* param) { pSend psend = (pSend)param; //轉換為Send類型 SendData(psend->FromName, psend->ToName, psend->data); //發送數據 return 0; } //接受數據 unsigned __stdcall ThreadRecv(void* param) { int ret = 0; while (1) { pClient pclient = (pClient)param; if (!pclient) return 1; ret = recv(pclient->sClient, pclient->buf, sizeof(pclient->buf), 0); if (ret == SOCKET_ERROR) return 1; if (pclient->buf[0] == '#' && pclient->buf[1] != '#') //#表示用戶要指定另一個用戶進行聊天 { SOCKET socket = FindClient(&pclient->buf[1]); //驗證一下客戶是否存在 if (socket != INVALID_SOCKET) { pClient c = (pClient)malloc(sizeof(_Client)); c = FindClient(socket); //只要改變ChatName,發送消息的時候就會自動發給指定的用戶了 memset(pclient->ChatName, 0, sizeof(pclient->ChatName)); memcpy(pclient->ChatName , c->userName,sizeof(pclient->ChatName)); } else send(pclient->sClient, "The user have not online or not exits.",64,0); continue; } pSend psend = (pSend)malloc(sizeof(_Send)); //把發送人的用戶名和接收消息的用戶和消息賦值給結構體,然後當作參數傳進發送消息進程中 memcpy(psend->FromName, pclient->userName, sizeof(psend->FromName)); memcpy(psend->ToName, pclient->ChatName, sizeof(psend->ToName)); memcpy(psend->data, pclient->buf, sizeof(psend->data)); _beginthreadex(NULL, 0, ThreadSend, psend, 0, NULL); Sleep(200); } return 0; } //開啟接收消息線程 void StartRecv() { pClient pclient = GetHeadNode(); while (pclient = pclient->next) _beginthreadex(NULL, 0, ThreadRecv, pclient, 0, NULL); } //管理連接 unsigned __stdcall ThreadManager(void* param) { while (1) { CheckConnection(); //檢查連接狀況 Sleep(2000); //2s檢查一次 } return 0; } //接受請求 unsigned __stdcall ThreadAccept(void* param) { _beginthreadex(NULL, 0, ThreadManager, NULL, 0, NULL); Init(); //初始化一定不要再while裡面做,否則head會一直為NULL!!! while (1) { //創建一個新的客戶端對象 pClient pclient = (pClient)malloc(sizeof(_Client)); //如果有客戶端申請連接就接受連接 if ((pclient->sClient = accept(g_ServerSocket, (SOCKADDR*)&g_ClientAddr, &g_iClientAddrLen)) == INVALID_SOCKET) { printf("accept failed with error code: %d\n", WSAGetLastError()); closesocket(g_ServerSocket); WSACleanup(); return -1; } recv(pclient->sClient, pclient->userName, sizeof(pclient->userName), 0); //接收用戶名和指定聊天對象的用戶名 recv(pclient->sClient, pclient->ChatName, sizeof(pclient->ChatName), 0); memcpy(pclient->IP, inet_ntoa(g_ClientAddr.sin_addr), sizeof(pclient->IP)); //記錄客戶端IP pclient->flag = pclient->sClient; //不同的socke有不同UINT_PTR類型的數字來標識 pclient->Port = htons(g_ClientAddr.sin_port); AddClient(pclient); //把新的客戶端加入鏈表中 printf("Successfuuly got a connection from IP:%s ,Port: %d,UerName: %s , ChatName: %s\n", pclient->IP, pclient->Port, pclient->userName,pclient->ChatName); if (CountCon() >= 2) //當至少兩個用戶都連接上服務器後才進行消息轉發 StartRecv(); Sleep(2000); } return 0; } //啟動服務器 int StartServer() { //存放套接字信息的結構 WSADATA wsaData = { 0 }; SOCKADDR_IN ServerAddr = { 0 }; //服務端地址 USHORT uPort = 18000; //服務器監聽端口 //初始化套接字 if (WSAStartup(MAKEWORD(2, 2), &wsaData)) { printf("WSAStartup failed with error code: %d\n", WSAGetLastError()); return -1; } //判斷版本 if (LOBYTE(wsaData.wVersion) != 2 || HIBYTE(wsaData.wVersion) != 2) { printf("wVersion was not 2.2\n"); return -1; } //創建套接字 g_ServerSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (g_ServerSocket == INVALID_SOCKET) { printf("socket failed with error code: %d\n", WSAGetLastError()); return -1; } //設置服務器地址 ServerAddr.sin_family = AF_INET;//連接方式 ServerAddr.sin_port = htons(uPort);//服務器監聽端口 ServerAddr.sin_addr.S_un.S_addr = htonl(INADDR_ANY);//任何客戶端都能連接這個服務器 //綁定服務器 if (SOCKET_ERROR == bind(g_ServerSocket, (SOCKADDR*)&ServerAddr, sizeof(ServerAddr))) { printf("bind failed with error code: %d\n", WSAGetLastError()); closesocket(g_ServerSocket); return -1; } //設置監聽客戶端連接數 if (SOCKET_ERROR == listen(g_ServerSocket, 20000)) { printf("listen failed with error code: %d\n", WSAGetLastError()); closesocket(g_ServerSocket); WSACleanup(); return -1; } _beginthreadex(NULL, 0, ThreadAccept, NULL, 0, 0); for (int k = 0;k < 100;k++) //讓主線程休眠,不讓它關閉TCP連接. Sleep(10000000); //關閉套接字 ClearClient(); closesocket(g_ServerSocket); WSACleanup(); return 0; } int main() { StartServer(); //啟動服務器 return 0; }
Client code:
#define _WINSOCK_DEPRECATED_NO_WARNINGS #include#include #include #include #include #pragma comment(lib,"ws2_32.lib") #define RECV_OVER 1 #define RECV_YET 0 char userName[16] = { 0 }; char chatName[16] = { 0 }; int iStatus = RECV_YET; //接受數據 unsigned __stdcall ThreadRecv(void* param) { char buf[128] = { 0 }; while (1) { int ret = recv(*(SOCKET*)param, buf, sizeof(buf), 0); if (ret == SOCKET_ERROR) { Sleep(500); continue; } if (strlen(buf) != 0) { printf("%s\n", buf); iStatus = RECV_OVER; } else Sleep(100); } return 0; } //發送數據 unsigned __stdcall ThreadSend(void* param) { char buf[128] = { 0 }; int ret = 0; while (1) { int c = getch(); if (c == 27) //ESC ASCII是27 { memset(buf, 0, sizeof(buf)); printf("Please input the chat name:"); gets_s(buf); char b[17] = { 0 }; sprintf(b, "#%s", buf); ret = send(*(SOCKET*)param,b , sizeof(b), 0); if (ret == SOCKET_ERROR) return 1; continue; } if(c == 72 || c == 0 || c == 68)//為了顯示美觀,加一個無回顯的讀取字符函數 continue; //getch返回值我是經過實驗得出如果是返回這幾個值,則getch就會自動跳過,具體我也不懂。 printf("%s: ", userName); gets_s(buf); ret = send(*(SOCKET*)param, buf, sizeof(buf), 0); if (ret == SOCKET_ERROR) return 1; } return 0; } //連接服務器 int ConnectServer() { WSADATA wsaData = { 0 };//存放套接字信息 SOCKET ClientSocket = INVALID_SOCKET;//客戶端套接字 SOCKADDR_IN ServerAddr = { 0 };//服務端地址 USHORT uPort = 18000;//服務端端口 //初始化套接字 if (WSAStartup(MAKEWORD(2, 2), &wsaData)) { printf("WSAStartup failed with error code: %d\n", WSAGetLastError()); return -1; } //判斷套接字版本 if (LOBYTE(wsaData.wVersion) != 2 || HIBYTE(wsaData.wVersion) != 2) { printf("wVersion was not 2.2\n"); return -1; } //創建套接字 ClientSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (ClientSocket == INVALID_SOCKET) { printf("socket failed with error code: %d\n", WSAGetLastError()); return -1; } //輸入服務器IP printf("Please input server IP:"); char IP[32] = { 0 }; gets_s(IP); //設置服務器地址 ServerAddr.sin_family = AF_INET; ServerAddr.sin_port = htons(uPort);//服務器端口 ServerAddr.sin_addr.S_un.S_addr = inet_addr(IP);//服務器地址 printf("connecting......\n"); //連接服務器 if (SOCKET_ERROR == connect(ClientSocket, (SOCKADDR*)&ServerAddr, sizeof(ServerAddr))) { printf("connect failed with error code: %d\n", WSAGetLastError()); closesocket(ClientSocket); WSACleanup(); return -1; } printf("Connecting server successfully IP:%s Port:%d\n", IP, htons(ServerAddr.sin_port)); printf("Please input your UserName: "); gets_s(userName); send(ClientSocket, userName, sizeof(userName), 0); printf("Please input the ChatName: "); gets_s(chatName); send(ClientSocket, chatName, sizeof(chatName), 0); printf("\n\n"); _beginthreadex(NULL, 0, ThreadRecv, &ClientSocket, 0, NULL); //啟動接收和發送消息線程 _beginthreadex(NULL, 0, ThreadSend, &ClientSocket, 0, NULL); for (int k = 0;k < 1000;k++) Sleep(10000000); closesocket(ClientSocket); WSACleanup(); return 0; } int main() { ConnectServer(); //連接服務器 return 0; }
最後,需要改進的有以下幾點:
1.沒有消息記錄,所以最好用文件或者數據庫的方式記錄,個人推薦數據庫。
2.沒有用戶注冊,登陸的操作,也是用文件或者數據庫來弄。程序一運行就讀取數據庫信息就行。
3.群聊功能沒有弄,這個其實很簡單,就是服務器不管3721,把接收到的消息轉發給所有在線用戶。
4.沒有離線消息,這個就用數據庫存儲離線消息,然後用戶上線後立即發送過去就行。
最後總結一下,沒有數據庫的聊天程序果然功能簡陋~,C語言寫的程序要注意對內存的操作。還有TCP方式的連接太費時費內存(用戶量達的時候)。