WinSocket实现的服务端与客户端的通信

时间:2021-02-18 14:57:13

服务端

  • 通过对敏感词“蓝鲸”的判断,服务端主动关闭与客户端的连接,测试服务端发起的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;
}

测试截图

  1. 客户端通过“exit”命令符主动发起的关闭socket请求
    WinSocket实现的服务端与客户端的通信
  2. 服务端通过识别敏感词“蓝鲸”,关闭掉与客户端的连接,强制要求客户端下线
    WinSocket实现的服务端与客户端的通信
  3. 关闭客户端控制台窗口或客户端异常退出,客户端也“优雅”的告诉服务器 我关闭的了socket连接
    WinSocket实现的服务端与客户端的通信

下面来测试一下在主线程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;
}

实验截图

WinSocket实现的服务端与客户端的通信

查看错误码:

//
// 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 10054L

10053:您的主机中的软件中止了一个已建立的连接
10054:一个连接被远程方强制关闭

分析原因:
客户端发送完“bye”之后立即关闭了连接,服务端收到“bye”之后再想回包时发现连接已被客户端关闭,所以服务端报出10054错误。而客户端子线程recv一直处于阻塞等待接收数据状态,在主线程关闭连接后,recv收到相关信息后,就报出了10053,连接已被主机中止。