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

服务端

  • 通过对敏感词“蓝鲸”的判断,服务端主动关闭与客户端的连接,测试服务端发起的closesocket操作
  • 服务端的accept、recv都是阻塞的
#include 
#include 
#include 
#include 
#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实现的服务端与客户端的通信_第1张图片
  2. 服务端通过识别敏感词“蓝鲸”,关闭掉与客户端的连接,强制要求客户端下线
    WinSocket实现的服务端与客户端的通信_第2张图片
  3. 关闭客户端控制台窗口或客户端异常退出,客户端也“优雅”的告诉服务器 我关闭的了socket连接
    WinSocket实现的服务端与客户端的通信_第3张图片

下面来测试一下在主线程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实现的服务端与客户端的通信_第4张图片

查看错误码:

//
// 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,连接已被主机中止。

你可能感兴趣的:(WinSocket实现的服务端与客户端的通信)