服务端
通过对敏感词“蓝鲸”的判断,服务端主动关闭与客户端的连接,测试服务端发起的closesocket操作服务端的accept、recv都是阻塞的 #include <stdio.h> #include <stdlib.h> #include <iostream> #include <WinSock2.h> #pragma comment(lib, "ws2_32.lib") using namespace std; #define SER_IP "127.0.0.1" #define SER_PORT 26000 #define RECV_BUFFER_MAX_LEN 1024 #define SEND_BUFFER_MAX_LEN 1024 // Handle Client to Server Connect DWORD WINAPI HandleC2SCnn(LPVOID lpParameter) { SOCKET CliSocket = (SOCKET)lpParameter; if (CliSocket == INVALID_SOCKET) return -1; sockaddr_in CliAddr; memset(&CliAddr, 0, sizeof(CliAddr)); int len = sizeof(CliAddr); if(getpeername(CliSocket, (struct sockaddr*)&CliAddr, &len) != 0) { printf("Get IP address failed! Error %d", GetLastError()); closesocket(CliSocket); return -1; } char RecvBuffer[RECV_BUFFER_MAX_LEN]; while (true) { // waiting to receive data from client memset(RecvBuffer, 0, RECV_BUFFER_MAX_LEN); int Ret = recv(CliSocket, RecvBuffer, RECV_BUFFER_MAX_LEN, 0); if (Ret == 0) { printf("%s:%d Client have closed the connection!\n", inet_ntoa(CliAddr.sin_addr), CliAddr.sin_port); break; } else if (Ret == SOCKET_ERROR) { printf("Receive %s:%d Client data Failed! Error %d\n", inet_ntoa(CliAddr.sin_addr), CliAddr.sin_port, GetLastError()); break; } printf("Receive %s:%d Client data : %s\n", inet_ntoa(CliAddr.sin_addr), CliAddr.sin_port, RecvBuffer); if (strcmp(RecvBuffer, "蓝鲸") == 0) { char szWarning[] = "there is forbidden word!"; send(CliSocket, szWarning, strlen(szWarning), 0); printf("Server closed the %s:%d Client connection because of forbidden word %s\n", inet_ntoa(CliAddr.sin_addr), CliAddr.sin_port, RecvBuffer); break; } // 把消息原样返回给客户端 if (send(CliSocket, RecvBuffer, strlen(RecvBuffer), 0) == SOCKET_ERROR) { printf("Send %s to %s:%d Client Failed! Error %d\n", RecvBuffer, inet_ntoa(CliAddr.sin_addr), CliAddr.sin_port, GetLastError()); break; } } closesocket(CliSocket); return 0; } // Create Server bool CreateServer() { // Init Windows Socket WSADATA WSAData; if (WSAStartup(MAKEWORD(2, 2), &WSAData) != 0) { printf("Init Windows Socket Failed! Error %d\n", WSAGetLastError()); return false; }; // Create Socket SOCKET SerSocket = WSASocket(AF_INET, SOCK_STREAM, IPPROTO_TCP, NULL, 0, WSA_FLAG_OVERLAPPED); if (INVALID_SOCKET == SerSocket) { printf("Create Socket Failed! Error %d\n", WSAGetLastError()); closesocket(SerSocket); WSACleanup(); return false; } // Init SerAddr struct sockaddr_in SerAddr; memset(&SerAddr, 0, sizeof(SerAddr)); SerAddr.sin_family = AF_INET; SerAddr.sin_addr.s_addr = inet_addr(SER_IP); SerAddr.sin_port = htons(SER_PORT); // Bind Socket if (bind(SerSocket, (struct sockaddr*)&SerAddr, sizeof(SerAddr)) != 0) { printf("Bind Socket Failed! Error %d\n", WSAGetLastError()); closesocket(SerSocket); WSACleanup(); return false; } // listen if (listen(SerSocket, 10) != 0) { printf("Listen Socket Failed! Error %d\n", WSAGetLastError()); closesocket(SerSocket); WSACleanup(); return false; } printf("Start Server Success!\n"); sockaddr_in CliAddr; int len = sizeof(CliAddr); while (true) { memset(&CliAddr, 0, sizeof(CliAddr)); // waiting to client connect SOCKET CliSocket = accept(SerSocket, (struct sockaddr*)&CliAddr, &len); if (CliSocket == INVALID_SOCKET) { printf("Accept Socket Failed! Error %d\n", WSAGetLastError()); continue; } printf("%s:%d Client Connect!\n", inet_ntoa(CliAddr.sin_addr), CliAddr.sin_port); // Create thread to handle connect HANDLE hThread = CreateThread(NULL, 0, HandleC2SCnn, (LPVOID)CliSocket, 0, NULL); if (!hThread) { printf("Create Thread Failed! Error %d\n", GetLastError()); continue; } CloseHandle(hThread); } // close socket closesocket(SerSocket); // clean socket WSACleanup(); return true; }客户端 什么是“优雅”的关闭socket? 对于recv函数,如果没有错误发生,recv()返回收到的字节数。如果连接被关闭,返回0。否则它返回SOCKET_ERROR,错误码可通过调用WSAGetLastError()函数得到。 如果套接字是面向有连接的, 并且远程方已“优雅”地关闭了连接,所有数据也已经被接收,则recv()立即返回,接收0字节数据。如果连接被复位,recv()将失败,错误码为WSAECONNRESET。
CliCnnSocket、bExit为全局变量 用于主线程与子线程间的通信send和recv在各自的线程间运行 send会在cin等待控制台输入那里阻塞 recv本身就是阻塞模式为了客户端能够“优雅”的关闭连接,closesocket都交由子线程处理如果直接关闭终端/控制台窗口,也在HandleCloseConsole中拦截窗口关闭消息,在窗口关闭之前,向服务器发起关闭连接请求 closesocket如果在主线程中完成,需要留意具体的操作步骤,否则服务器就不会认为是正常的关闭,最后面会对此说明客户端逻辑代码
// client connect socket SOCKET CliCnnSocket; bool bExit; // Handle Server to Client Connect DWORD WINAPI HandleS2CCnn(LPVOID lpParameter) { if (CliCnnSocket == INVALID_SOCKET) return -1; char RecvBuffer[RECV_BUFFER_MAX_LEN]; while (true) { if (bExit) break; // waiting to receive data from server memset(RecvBuffer, 0, RECV_BUFFER_MAX_LEN); int Ret = recv(CliCnnSocket, RecvBuffer, RECV_BUFFER_MAX_LEN, 0); if (Ret == 0) { printf("Server have closed the connection!\n"); break; } else if (Ret == SOCKET_ERROR) { printf("Receive Server data Failed! Error %d\n", WSAGetLastError()); break; } printf("Receive Server data : %s\n", RecvBuffer); } closesocket(CliCnnSocket); WSACleanup(); return 0; } // 客户端“优雅”的断开连接 void ClientDoExit() { bExit = true; char szExit[] = "bye"; send(CliCnnSocket, szExit, strlen(szExit), 0); } BOOL HandleCloseConsole(DWORD dwCtrlType) { if (dwCtrlType == CTRL_CLOSE_EVENT) { ClientDoExit(); // 延迟一下 让子线程HandleS2CCnn能够执行完closesocket Sleep(100); return TRUE; } return FALSE; } // Create Client bool CreateClient() { // Init Windows Socket WSADATA WSAData; if (WSAStartup(MAKEWORD(2, 2), &WSAData) != 0) { printf("Init Windows Socket Failed! Error %d\n", WSAGetLastError()); return false; }; // Create Socket CliCnnSocket = WSASocket(AF_INET, SOCK_STREAM, IPPROTO_TCP, NULL, 0, WSA_FLAG_OVERLAPPED); if (CliCnnSocket == INVALID_SOCKET) { printf("Create Socket Failed! Error %d\n", WSAGetLastError()); closesocket(CliCnnSocket); WSACleanup(); return false; } // Init SerAddr struct sockaddr_in SerAddr; memset(&SerAddr, 0, sizeof(SerAddr)); SerAddr.sin_family = AF_INET; SerAddr.sin_addr.s_addr = inet_addr(SER_IP); SerAddr.sin_port = htons(SER_PORT); // connect server if(connect(CliCnnSocket,(struct sockaddr*)&SerAddr, sizeof(SerAddr)) == 0){ printf("Connect %s:%d Server Success!\n", SER_IP, SER_PORT); } else { printf("Connect %s:%d Server Failed! Error %d\n", SER_IP, SER_PORT, WSAGetLastError()); closesocket(CliCnnSocket); WSACleanup(); return false; } // exit flag bExit = false; // Create thread to handle connect HANDLE hThread = CreateThread(NULL, 0, HandleS2CCnn, (LPVOID)CliCnnSocket, 0, NULL); if (!hThread) { printf("Create Thread Failed! Error %d\n", GetLastError()); } CloseHandle(hThread); char SendBuffer[SEND_BUFFER_MAX_LEN]; while (true) { printf("Please Input : "); memset(SendBuffer, 0, SEND_BUFFER_MAX_LEN); cin.getline(SendBuffer, SEND_BUFFER_MAX_LEN); if (strlen(SendBuffer) == 0) { printf("The input cannot be empty! Please re-enter it!\n"); continue; } // client exit if (strcmp(SendBuffer,"exit") == 0) { ClientDoExit(); break; } // send data to server if (send(CliCnnSocket, SendBuffer, strlen(SendBuffer), 0) == SOCKET_ERROR) { printf("Send data Failed! Error %d\n", WSAGetLastError()); continue;; } // 延迟一下 等待服务器的回包在子线程中打印出来 Sleep(100); } return true; }主函数
1.编译生成Server
int main() { CreateServer(); system("pause"); return 0; }2.编译生成Client
int main() { // 禁用控制台关闭按钮 //DeleteMenu(GetSystemMenu(GetConsoleWindow(), FALSE), SC_CLOSE, MF_BYCOMMAND); //DrawMenuBar(GetConsoleWindow()); // 处理控制台消息 SetConsoleCtrlHandler((PHANDLER_ROUTINE)HandleCloseConsole, TRUE); CreateClient(); system("pause"); return 0; }测试截图
客户端通过“exit”命令符主动发起的关闭socket请求 服务端通过识别敏感词“蓝鲸”,关闭掉与客户端的连接,强制要求客户端下线 关闭客户端控制台窗口或客户端异常退出,客户端也“优雅”的告诉服务器 我关闭的了socket连接下面来测试一下在主线程closesocket出现的“不优雅”的关闭连接 CreateClient()修改如下: 从while循环break后 执行closesocket(CliCnnSocket); WSACleanup();
char SendBuffer[SEND_BUFFER_MAX_LEN]; while (true) { printf("Please Input : "); memset(SendBuffer, 0, SEND_BUFFER_MAX_LEN); cin.getline(SendBuffer, SEND_BUFFER_MAX_LEN); if (strlen(SendBuffer) == 0) { printf("The input cannot be empty! Please re-enter it!\n"); continue; } // client exit if (strcmp(SendBuffer,"exit") == 0) { ClientDoExit(); break; } // send data to server if (send(CliCnnSocket, SendBuffer, strlen(SendBuffer), 0) == SOCKET_ERROR) { printf("Send data Failed! Error %d\n", WSAGetLastError()); continue;; } Sleep(100); } closesocket(CliCnnSocket); WSACleanup();HandleS2CCnn修改如下: 注释掉WSACleanup()即可
// Handle Server to Client Connect DWORD WINAPI HandleS2CCnn(LPVOID lpParameter) { if (CliCnnSocket == INVALID_SOCKET) return -1; char RecvBuffer[RECV_BUFFER_MAX_LEN]; while (true) { if (bExit) break; // waiting to receive data from server memset(RecvBuffer, 0, RECV_BUFFER_MAX_LEN); int Ret = recv(CliCnnSocket, RecvBuffer, RECV_BUFFER_MAX_LEN, 0); if (Ret == 0) { printf("Server have closed the connection!\n"); break; } else if (Ret == SOCKET_ERROR) { printf("Receive Server data Failed! Error %d\n", WSAGetLastError()); break; } printf("Receive Server data : %s\n", RecvBuffer); } closesocket(CliCnnSocket); //WSACleanup(); return 0; }实验截图
查看错误码:
// // MessageId: WSAECONNABORTED // // MessageText: // // An established connection was aborted by the software in your host machine. // #define WSAECONNABORTED 10053L // // MessageId: WSAECONNRESET // // MessageText: // // An existing connection was forcibly closed by the remote host. // #define WSAECONNRESET 10054L10053:您的主机中的软件中止了一个已建立的连接 10054:一个连接被远程方强制关闭
分析原因: 客户端发送完“bye”之后立即关闭了连接,服务端收到“bye”之后再想回包时发现连接已被客户端关闭,所以服务端报出10054错误。而客户端子线程recv一直处于阻塞等待接收数据状态,在主线程关闭连接后,recv收到相关信息后,就报出了10053,连接已被主机中止。