目錄
一、前言
二、設計需求
1.服務器需求?
2.客戶端需求
三、服務端設計
1.項目準備
?2.初始化網絡庫
3.SOCKET創建服務器套接字
4.?bind 綁定套接字
?5. listen監聽套接字
?6. accept接受客戶端連接
7.建立套接字數組
8. 建立多線程與客戶端通信
9. 處理線程函數,收消息
10. 發消息給客戶端
?11.處理斷開的客戶端
四、客戶端設計
1.項目準備
2. 處理main函數參數
?3.初始化網絡庫
4.SOCKET創建客戶端套接字
5. 配置IP地址和端口號,連接服務器
6.創建兩線程,發送和接收
?7.處理發送消息線程函數
五、項目運行
1.編譯生成可執行文件
2.運行可執行程序
3.進行通訊
六、總代碼展示
1.服務端代碼:
2.客戶端代碼:
七、最后
一、前言
? ? ? ? 今天我們不學習其他的知識點,主要是復習之前學習過的TCP網絡通信和多線程以及線程同步互斥,然后結合這以上知識點設計實現一個小的項目,主要仿照qq群聊的服務器可客戶端的實現,下面我將會說明一下設計需求,以下是整個設計示意圖。
二、設計需求
1.服務器需求?
? ? ? ? 需求一:對于每一個上線連接的客戶端,服務端會起一個線程去維護。????????
? ? ? ? 需求二:將服務器受到的消息轉發給全部的客戶端。例如:服務器接收客戶端A的消息后,將立即發送給客戶端A,B,C...
????????需求三:當某個客戶端斷開(下線),需要處理斷開的鏈接。
2.客戶端需求
????????需求一:請求連接上線,???
? ? ? ? 需求二:發消息給服務器。
????????需求三:客戶端等待服務端的消息。
????????需求四:等待用戶自己的關閉(下線)。
三、服務端設計
1.項目準備
? ? ? ? 在創建項目后,引入一些必需的頭文件以及創建項目需要的宏,例如:允許客戶端連接的最大數量,接收文件字節的大小,客戶端連接的個數等等。
#include <stdio.h>
#include <windows.h>
#include <process.h>
#include <iostream>
#pragma comment(lib, "ws2_32.lib")#define MAX_CLEN 256 // 最大連接數量
#define MAX_BUF_SIZE 1024 // 接收文件大小SOCKET clnSockets[MAX_CLEN]; // 所有的連接客戶端的socket
int clnCnt = 0; // 客戶端連接的個數// 互斥的句柄
HANDLE hMutex;
?2.初始化網絡庫
????????WSAStartup初始化Winsock,這個函數用于初始化網絡環境,都是固定寫法,必須要有的,直接復制粘貼即可。
// 1. 初始化庫WSADATA wsaData;int stu = WSAStartup(MAKEWORD(2, 2), &wsaData);if (stu != 0) {std::cout << "WSAStartup 錯誤:" << stu << std::endl;return 0;}
3.SOCKET創建服務器套接字
? ? ? ? ?這和我們之前學的windwos網絡一樣都是固定寫法,重點時查看函數原型以及它的參數,代碼如下:
// 2. socket 創建套接字SOCKET sockSrv = socket(AF_INET, SOCK_STREAM, 0);if (sockSrv == INVALID_SOCKET){std::cout << "socket failed!" << GetLastError() << std::endl;WSACleanup(); //釋放Winsock庫資源return 1;}
4.?bind 綁定套接字
? ? ? ? 這個流程主要是綁定服務器的IP地址,端口號,以及協議版本。?
// 3 bind 綁定套接字SOCKADDR_IN addrSrv;addrSrv.sin_addr.S_un.S_addr = htonl(INADDR_ANY); // 地址 IP地址anyaddrSrv.sin_family = AF_INET; // ipv4協議addrSrv.sin_port = htons(6000); // 端口號if ( SOCKET_ERROR == bind(sockSrv, (sockaddr*)&addrSrv, sizeof(SOCKADDR))){std::cout << "bind failed!" << GetLastError() << std::endl;WSACleanup(); //釋放Winsock庫資源return 1;}
?5. listen監聽套接字
? ? ? ? listen函數最重要的是理解它的第二個參數,為等待連接的最大隊列長度 ,這個解釋我有專門出過一篇文章windows網絡進階之listen參數含義。
// 4. 監聽if (listen(sockSrv, 5) == SOCKET_ERROR) // 5 是指最大的監聽數目,執行到listen{printf("listen error = %d\n", GetLastError());return -1;}
?6. accept接受客戶端連接
????????對于每一個被接受的連接請求,accept
函數都會創建一個新的套接字,用于與該客戶端的后續通信。也都是固定流程,后面互斥和多線程就比較難理解了。
// 5. accept接受客戶端連接SOCKADDR_IN addrCli;int len = sizeof(SOCKADDR);while (true){// 接受客戶端的連接SOCKET sockCon = accept(sockSrv, (sockaddr*)&addrCli, &len);}
7.建立套接字數組
? ? ? ? 將accept生成的套接字放入全局套接字數組中,同時加上互斥鎖。
//創建一個互斥對象hMutex = CreateMutex(NULL, false, NULL);while (true){// 接受客戶端的連接SOCKET sockCon = accept(sockSrv, (sockaddr*)&addrCli, &len);// 全局變量要加鎖WaitForSingleObject(hMutex, INFINITE);// 將連接放到數組里面clnSockets[clnCnt++] = sockCon;// 解鎖ReleaseMutex(hMutex);}closesocket(sockSrv);CloseHandle(hMutex);WSACleanup();return 0;
8. 建立多線程與客戶端通信
? ? ? ? 每通過accept
函數返回的新創建的套接字,就建立一個線程去維護。
//創建一個互斥對象
hMutex = CreateMutex(NULL, false, NULL);
while (true){// 接受客戶端的連接SOCKET sockCon = accept(sockSrv, (sockaddr*)&addrCli, &len);// 全局變量要加鎖WaitForSingleObject(hMutex, INFINITE);// 將連接放到數組里面clnSockets[clnCnt++] = sockCon;// 解鎖ReleaseMutex(hMutex);// 每接收一個客戶端的連接,都安排一個線程去維護hThread = (HANDLE)_beginthreadex(NULL, 0, &handleCln, (void*)&sockCon, 0, NULL);printf("Connect client IP = %s\n, Num = %d \n", inet_ntoa(addrCli.sin_addr), clnCnt);}closesocket(sockSrv);CloseHandle(hMutex);WSACleanup();return 0;
9. 處理線程函數,收消息
? ? ? ? 上個步驟我們對每一個接受連接的套接字都創建了線程,現在我們開始來寫線程函數中的邏輯代碼,主要有三個部分:收到客戶端的消息,將收到的消息再發給所有客戶端,處理斷開的客戶端。
? ? ? ? 下面我們開始完成第一個部分:?收到客戶端的消息。
????????因為客戶端發消息會不止一個,所以我們要建立while循環,通關判斷接收到的消息來判斷,如果為0就退出循環。
// 處理線程函數, 收發消息
unsigned WINAPI handleCln(void *arg)
{SOCKET hClnSock = *((SOCKET *)arg);int iLen = 0;char recvBuff[MAX_BUF_SIZE] = { 0 };while (1){// iLen 成功時返回接收的字節數(收到EOF時為0),失敗時返回SOCKETERROR。iLen = recv(hClnSock, recvBuff, MAX_BUF_SIZE, 0);// if (iLen >= 0){// 將收到的消息轉發給所有客戶端SendMsg(recvBuff,iLen);}else{break;}}
10. 發消息給客戶端
? ? ? ??完成第二個部分:?將收到的消息再發給所有客戶端。
? ? ? ? ?因為是仿照qq的小demo,所以服務器一旦收到消息,就要再發送給所有的客戶端。這段邏輯寫在SendMsg 函數中,同時還需要注意因為在多線程中,所以要避免多個線程同時訪問共享資源時產生數據不一致的問題,需要加互斥鎖和解鎖。
// 將收到的消息轉發給所有客戶端
void SendMsg(char* msg, int len)
{int i;WaitForSingleObject(hMutex, INFINITE);for (i = 0; i < clnCnt; i++){send(clnSockets[i], msg, len, 0);}ReleaseMutex(hMutex);
}
?11.處理斷開的客戶端
????????完成第三個部分:?處理斷開的客戶端。
? ? ? ? 這里也是通過 for 循環遍歷 socket 數組,通過匹配每一項,如果相匹配,就然后斷開連接。同時? socket 數組 中的數量減 1。
// 處理消息, 收發消息
unsigned WINAPI handleCln(void *arg)
{SOCKET hClnSock = *((SOCKET *)arg);int iLen = 0;char recvBuff[MAX_BUF_SIZE] = { 0 };while (1){// iLen 成功時返回接收的字節數(收到EOF時為0),失敗時返回SOCKETERROR。iLen = recv(hClnSock, recvBuff, MAX_BUF_SIZE, 0);// if (iLen >= 0){// 將收到的消息轉發給所有客戶端SendMsg(recvBuff,iLen);}else{break;}}printf("此時連接的客戶端數量 = %d\n", clnCnt);WaitForSingleObject(hMutex, INFINITE);for (int i = 0; i < clnCnt; i++){// 找到哪個連接下線的,移除這個連接if (hClnSock == clnSockets[i]){while (i++ < clnCnt){clnSockets[i] = clnSockets[i + 1];}break;}}// 斷開連接減 1 clnCnt--;printf("斷開連接后連接的客戶端數量 = %d\n", clnCnt);ReleaseMutex(hMutex);// 斷開連接closesocket(hClnSock);return 0;
}
四、客戶端設計
1.項目準備
? ? ? ? 客戶端設計和服務器端其實差別不大,代碼有些基本都相同,邏輯也大多一致,所以有些代碼不在過多贅述。
????????項目準備代碼:
#include <stdio.h>
#include <windows.h>
#include <process.h>
#include <iostream>
#pragma comment(lib, "ws2_32.lib")#define NAME_SIZE 256
#define MAX_BUF_SIZE 1024char szName[NAME_SIZE] = "[DEFAULT]"; // 默認的昵稱
char szMsg[MAX_BUF_SIZE]; // 收發數據的大小
2. 處理main函數參數
? ? ? ? 項目為仿qq群聊,所以我用main函數中的命令行參數作為我們輸入的每一個客戶端的名字,項目啟動在終端開始啟動,否則就退出程序。
int main(int argc, char* argv[])
{if (argc != 2){printf("必須輸入兩個參數,包括昵稱\n");printf("例如: WXS\n");system("pause");return -1;}sprintf_s(szName, "[%s]", argv[1]);printf("this is Client");
}
?3.初始化網絡庫
? ? ? ? 和服務器端代碼一樣。
// 初始化庫WSADATA wsaData;int stu = WSAStartup(MAKEWORD(2, 2), &wsaData);if (stu != 0) {std::cout << "WSAStartup 錯誤:" << stu << std::endl;return 0;}
4.SOCKET創建客戶端套接字
? ? ? ? 以服務器類似。?
SOCKET sockCli = socket(AF_INET, SOCK_STREAM, 0);if (sockCli == INVALID_SOCKET){std::cout << "socket failed!" << GetLastError() << std::endl;WSACleanup(); //釋放Winsock庫資源return 1;}
5. 配置IP地址和端口號,連接服務器
? ? ? ? 也是基本固定寫法。
// 配置IP地址 和 端口號SOCKADDR_IN addrSrv;addrSrv.sin_family = AF_INET; // ipv4協議addrSrv.sin_addr.S_un.S_addr = inet_addr("192.168.1.7"); // 地址 IP地址anyaddrSrv.sin_port = htons(6000); // 端口號// 連接服務器int res = connect(sockCli, (sockaddr*)&addrSrv, sizeof(sockaddr));
6.創建兩線程,發送和接收
? ? ? ? ?這里我們創建了兩個線程,分別處理發送消息給客戶端同時接收消息。同時這個函數WaitForSingleObject 會阻塞主進程代碼,直到子進程結束。
// 定義兩個線程 HANDLE hSendThread, hRecvThread;// 發送消息hSendThread = (HANDLE)_beginthreadex(NULL, 0, &SendMsg, (void*)&sockCli, 0, NULL);// 接收消息hRecvThread = (HANDLE)_beginthreadex(NULL, 0, &RecvMsg, (void*)&sockCli, 0, NULL);// 阻塞代碼,處理子線程執行完后再執行WaitForSingleObject(hSendThread,INFINITE);WaitForSingleObject(hRecvThread, INFINITE);
?7.處理發送消息線程函數
? ? ? ? 我們客戶端發送消息是通過控制臺程序進行發送的,所以要用到用戶輸入。同時發送的時候帶上自己的名字前綴,也要處理快捷鍵客戶端下線的邏輯,不能一致發送消息。
unsigned WINAPI SendMsg(void* arg)
{SOCKET hClnSock = *((SOCKET*)arg);char szNameMsg[NAME_SIZE + MAX_BUF_SIZE] = { 0 }; // 昵稱和消息while (1){memset(szMsg, 0, MAX_BUF_SIZE);// 阻塞這一句,等待控制臺的消息//fgets(szMsg, MAX_BUF_SIZE, stdin);// 第二種寫法std::cin >> szMsg;if (!strcmp(szMsg, "Q\n") || !strcmp(szMsg, "q\n")){// 處理下線closesocket(hClnSock);exit(0);}// 拼接 名字和字符串一起發送sprintf_s(szNameMsg, "%s %s", szName, szMsg);send(hClnSock, szNameMsg, strlen(szNameMsg) + 1, 0);}
}
?7.處理接收消息線程函數
? ? ? ? 這里接收消息比較簡單,和正常接收客戶端消息的邏輯差不多,代碼如下:
unsigned WINAPI RecvMsg(void* arg)
{SOCKET hClnSock = *((SOCKET*)arg);char szNameMsg[NAME_SIZE + MAX_BUF_SIZE] = { 0 }; // 昵稱和消息int len;while (1){len = recv(hClnSock, szNameMsg, sizeof(szNameMsg), 0);if (len <= 0){break;return -2;}szNameMsg[len] = 0;std::cout << szNameMsg << std::endl;// fputs(szNameMsg, stdout);}
}
五、項目運行
? ? ? ? 以上我們分別講解了服務器和客戶端代碼的實現邏輯,現在我們來進行步驟驗證我們的操作結果。
1.編譯生成可執行文件
? ? ? ? 如圖所示:
2.運行可執行程序
? ? ? ? 這里要注意服務器直接運行exe文件即可,而客戶端要通過命令行輸入運行。
? ? ? ? 服務器端:
? ? ? ? 客戶端運行需要打開終端,輸入exe文件的路徑,以及名字。另外進行通訊還需要打開多個客戶端。
3.進行通訊
? ? ? ? ?結果展示為:
六、總代碼展示
1.服務端代碼:
? ? ? ? 如下所示:
// 1. 對于每一個上線的客戶端,服務端會起一個線程去維護
// 2. 將受到的消息轉發給全部的客戶端
// 3. 當某個客戶端斷開(下線),需要處理斷開的鏈接。怎么處理呢?
#include <stdio.h>
#include <windows.h>
#include <process.h>
#include <iostream>
#pragma comment(lib, "ws2_32.lib")#define MAX_CLEN 256
#define MAX_BUF_SIZE 1024SOCKET clnSockets[MAX_CLEN]; // 所有的連接客戶端的socket
int clnCnt = 0; // 客戶端連接的個數HANDLE hMutex; // 將收到的消息轉發給所有客戶端
void SendMsg(char* msg, int len)
{int i;WaitForSingleObject(hMutex, INFINITE);for (i = 0; i < clnCnt; i++){send(clnSockets[i], msg, len, 0);}ReleaseMutex(hMutex);
}// 處理消息, 收發消息
unsigned WINAPI handleCln(void *arg)
{SOCKET hClnSock = *((SOCKET *)arg);int iLen = 0;char recvBuff[MAX_BUF_SIZE] = { 0 };while (1){// iLen 成功時返回接收的字節數(收到EOF時為0),失敗時返回SOCKETERROR。iLen = recv(hClnSock, recvBuff, MAX_BUF_SIZE, 0);// if (iLen >= 0){// 將收到的消息轉發給所有客戶端SendMsg(recvBuff,iLen);}else{break;}}printf("此時連接的客戶端數量 = %d\n", clnCnt);WaitForSingleObject(hMutex, INFINITE);for (int i = 0; i < clnCnt; i++){// 找到哪個連接下線的,移除這個連接if (hClnSock == clnSockets[i]){while (i++ < clnCnt){clnSockets[i] = clnSockets[i + 1];}break;}}// 斷開連接減 1 clnCnt--;printf("斷開連接后連接的客戶端數量 = %d\n", clnCnt);ReleaseMutex(hMutex);// 斷開連接closesocket(hClnSock);return 0;
}int main(int argc, char* argv[])
{ printf("this is Server\n");//0. 初始化網絡
#if 1
// 0 初始化網絡庫
// 初始化庫WSADATA wsaData;int stu = WSAStartup(MAKEWORD(2, 2), &wsaData);if (stu != 0) {std::cout << "WSAStartup 錯誤:" << stu << std::endl;return 0;}
#endifHANDLE hThread;// 1. 創建一個互斥對象hMutex = CreateMutex(NULL, false, NULL);// 2. socket 創建套接字SOCKET sockSrv = socket(AF_INET, SOCK_STREAM, 0);if (sockSrv == INVALID_SOCKET){std::cout << "socket failed!" << GetLastError() << std::endl;WSACleanup(); //釋放Winsock庫資源return 1;}// 3 bind 綁定套接字SOCKADDR_IN addrSrv;addrSrv.sin_addr.S_un.S_addr = htonl(INADDR_ANY); // 地址 IP地址anyaddrSrv.sin_family = AF_INET; // ipv4協議addrSrv.sin_port = htons(6000); // 端口號if ( SOCKET_ERROR == bind(sockSrv, (sockaddr*)&addrSrv, sizeof(SOCKADDR))){std::cout << "bind failed!" << GetLastError() << std::endl;WSACleanup(); //釋放Winsock庫資源return 1;}// 4. 監聽if (listen(sockSrv, 5) == SOCKET_ERROR) // 5 是指最大的監聽數目,執行到listen{printf("listen error = %d\n", GetLastError());return -1;}// 5SOCKADDR_IN addrCli;int len = sizeof(SOCKADDR);while (true){// 接受客戶端的連接SOCKET sockCon = accept(sockSrv, (sockaddr*)&addrCli, &len);// 全局變量要加鎖WaitForSingleObject(hMutex, INFINITE);// 將連接放到數組里面clnSockets[clnCnt++] = sockCon;// 解鎖ReleaseMutex(hMutex);// 每接收一個客戶端的連接,都安排一個線程去維護hThread = (HANDLE)_beginthreadex(NULL, 0, &handleCln, (void*)&sockCon, 0, NULL);printf("Connect client IP = %s\n, Num = %d \n", inet_ntoa(addrCli.sin_addr), clnCnt);}closesocket(sockSrv);CloseHandle(hMutex);WSACleanup();return 0;
}
2.客戶端代碼:
? ? ? ? 如下所示:
// 客戶端做的事情:
//1 請求連接上線,
//2 發消息
//3 客戶端等待服務端的消息
//4 等待用戶自己的關閉(下線)
#include <stdio.h>
#include <windows.h>
#include <process.h>
#include <iostream>
#pragma comment(lib, "ws2_32.lib")#define NAME_SIZE 256
#define MAX_BUF_SIZE 1024char szName[NAME_SIZE] = "[DEFAULT]"; // 默認的昵稱
char szMsg[MAX_BUF_SIZE]; // 收發數據的大小unsigned WINAPI SendMsg(void* arg)
{SOCKET hClnSock = *((SOCKET*)arg);char szNameMsg[NAME_SIZE + MAX_BUF_SIZE] = { 0 }; // 昵稱和消息while (1){memset(szMsg, 0, MAX_BUF_SIZE);// 阻塞這一句,等待控制臺的消息//fgets(szMsg, MAX_BUF_SIZE, stdin);std::cin >> szMsg;if (!strcmp(szMsg, "Q\n") || !strcmp(szMsg, "q\n")){// 處理下線closesocket(hClnSock);exit(0);}// 拼接 名字和字符串一起發送sprintf_s(szNameMsg, "%s %s", szName, szMsg);send(hClnSock, szNameMsg, strlen(szNameMsg) + 1, 0);}
}unsigned WINAPI RecvMsg(void* arg)
{SOCKET hClnSock = *((SOCKET*)arg);char szNameMsg[NAME_SIZE + MAX_BUF_SIZE] = { 0 }; // 昵稱和消息int len;while (1){len = recv(hClnSock, szNameMsg, sizeof(szNameMsg), 0);if (len <= 0){break;return -2;}szNameMsg[len] = 0;std::cout << szNameMsg << std::endl;// fputs(szNameMsg, stdout);}}
int main(int argc, char* argv[])
{if (argc != 2){printf("必須輸入兩個參數,包括昵稱\n");printf("例如: WXS\n");system("pause");return -1;}sprintf_s(szName, "[%s]", argv[1]);printf("this is Client");//0. 初始化網絡
#if 1
// 0 初始化網絡庫
// 初始化庫WSADATA wsaData;int stu = WSAStartup(MAKEWORD(2, 2), &wsaData);if (stu != 0) {std::cout << "WSAStartup 錯誤:" << stu << std::endl;return 0;}
#endif// 定義兩個線程 HANDLE hSendThread, hRecvThread;// 1. 建立 socketSOCKET sockCli = socket(AF_INET, SOCK_STREAM, 0);if (sockCli == INVALID_SOCKET){std::cout << "socket failed!" << GetLastError() << std::endl;WSACleanup(); //釋放Winsock庫資源return 1;}// 2, 配置IP地址 和 端口號SOCKADDR_IN addrSrv;addrSrv.sin_family = AF_INET; // ipv4協議addrSrv.sin_addr.S_un.S_addr = inet_addr("192.168.1.7"); // 地址 IP地址anyaddrSrv.sin_port = htons(6000); // 端口號// 3. 連接服務器int res = connect(sockCli, (sockaddr*)&addrSrv, sizeof(sockaddr));// 4. 發送服務器消息,啟動線程hSendThread = (HANDLE)_beginthreadex(NULL, 0, &SendMsg, (void*)&sockCli, 0, NULL);// 5. 等待hRecvThread = (HANDLE)_beginthreadex(NULL, 0, &RecvMsg, (void*)&sockCli, 0, NULL);WaitForSingleObject(hSendThread,INFINITE);WaitForSingleObject(hRecvThread, INFINITE);closesocket(sockCli);WSACleanup();return 0;
}
七、最后
? ? ? ? 制作不易,熬夜肝的,還請多多點贊,拯救下禿頭的博主吧!!? ?? ? ? ?