c++socket编程之客户端编写

发布时间 2023-04-01 12:24:32作者: z5onk0

开头

  • 用WIN API完成了socket客户端的编写
  • cursor很适合用于写这种单文件的WIN API代码编写,写的很规范,它帮助我完成了API的调用,参数的选择和异常值处理,自己去写还挺费时间
  • 但不得不吐槽下,我提的几个处理中文和处理多任务的需求,无论我换何种说法,它实现的都不太好,甚至还有错误

功能

  • 消息收发
  • 文件传输
  • 远程命令执行

客户端设计

  • 主线程:建立连接后,创建一个能够发送消息的子线程,随后在死循环中不断读取网络消息,按照协议解析数据,分发到handleFile、handleCmd、handleMsg三个子线程中处理,主线程主要阻塞在接收消息的recv函数上
  • 发送消息的子线程:从命令行中读取Unicode字符串,转化为字节数组发送到远端
  • handleFile子线程:首先读取协议中的文件大小部分,然后按照每次1024字节的大小分块读取文件内容,在本地组装保存
  • handleCmd子线程:读取命令内容,通过CreateProcessA创建命令执行,将执行结果写入到管道,等待执行完毕或超时后,从管道读出结果,并发送到远端
  • handleMsg子线程:从远端读取字节数组,通过MultiByteToWideChar转化为Unicode字符串,打印到屏幕上

代码

#include <iostream>
#include <winsock2.h>
#include <ws2tcpip.h>
#include <windows.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <tchar.h>
#include <thread>
#include <io.h>
#include <fcntl.h>
#include<string>

#pragma comment(lib, "Ws2_32.lib")

#define DEFAULT_BUFLEN 512
#define DEFAULT_PORT "27015"

DWORD WINAPI sendToServer(LPVOID lpParam) {
    SOCKET ConnectSocket = *(SOCKET*)lpParam;
    char sendbuf[DEFAULT_BUFLEN];
    int iResult;

    while (true) {
        printf("[发送] ");
        fgets(sendbuf, sizeof(sendbuf), stdin);
        iResult = send(ConnectSocket, sendbuf, strlen(sendbuf), 0);
        if (iResult == SOCKET_ERROR) {
            printf("[错误] 发送失败: %d\n", WSAGetLastError());
            closesocket(ConnectSocket);
            WSACleanup();
            return 1;
        }
    }
    return 0;
}

VOID WINAPI handleFile(char* datalen, char* data_pre, int len_pre, SOCKET socket) {
    int file_len = strtol(datalen, NULL, 10);
    printf("[提示] 准备接收文件,大小为: %d\n", file_len);
    char* fileData = new char[file_len];
    int datalen_pre = len_pre - 6 - sizeof(int);
    char buffer[1024];
    int alreadyReceived = datalen_pre;
    int iResult;

    FILE* fp = fopen("hello", "wb");
    //第一轮接收
    fwrite(data_pre, 1, datalen_pre, fp);
    //继续接收完剩下的文件
    while (alreadyReceived < file_len)
    {
        int bytesToReceive = min((int)sizeof(buffer), file_len - alreadyReceived);
        iResult = recv(socket, buffer, bytesToReceive, 0);
        if (iResult == SOCKET_ERROR) {
            printf("[错误] 接收文件失败: %d\n", WSAGetLastError());
            closesocket(socket);
            WSACleanup();
        }
        alreadyReceived += iResult;
        fwrite(buffer, 1, iResult, fp);
    }
    fclose(fp);
    printf("[提示] 文件成功保存到本地\n");
    printf("[发送] ");
    char sendbuf[DEFAULT_BUFLEN];
    strcpy_s(sendbuf, "成功接收文件\n");
    iResult = send(socket, sendbuf, (int)strlen(sendbuf), 0);
    //在子线程中接受文件
    /*std::thread t([&fileData]() {
        for (int i = 0; i < strlen(fileData); i++) {
            printf("\\x%02x ", fileData[i]);
        }
    });*/
    //printf("[提示] 开启子线程 %d 来保存文件!\n", t.get_id());
}

VOID WINAPI handleMsg(char* content) {
    _setmode(_fileno(stdout), _O_U8TEXT);

    int length = MultiByteToWideChar(CP_UTF8, 0, content, -1, NULL, 0);

    // 分配内存空间
    wchar_t* wstr = new wchar_t[length];

    // 转换为 Unicode 字符串
    MultiByteToWideChar(CP_UTF8, 0, content, -1, wstr, length);

    // 输出 Unicode 字符串
    wprintf(L"[接收]: %s\n", wstr);

    _setmode(_fileno(stdout), _O_TEXT);

    // 释放内存空间
    delete[] wstr;
    printf("[发送]: ");
}

VOID WINAPI handleCmd(char* content, SOCKET socket) {
    char* cmd = content;
    SECURITY_ATTRIBUTES sa = { sizeof(SECURITY_ATTRIBUTES), NULL, TRUE };
    HANDLE hRead, hWrite;
    if (!CreatePipe(&hRead, &hWrite, &sa, 0)) {
        printf("[错误] 创建管道失败!\n");
    }
    STARTUPINFOA si = { sizeof(STARTUPINFOA) };
    si.hStdError = hWrite;
    si.hStdOutput = hWrite;
    si.dwFlags |= STARTF_USESTDHANDLES;
    PROCESS_INFORMATION pi;
    if (!CreateProcessA(NULL, cmd, NULL, NULL, TRUE, 0, NULL, NULL, &si, &pi)) {
        printf("[错误] 创建子进程失败\n");
    }
    CloseHandle(hWrite);
    DWORD dwWaitResult = WaitForSingleObject(pi.hProcess, 1000);
    if (dwWaitResult == WAIT_OBJECT_0) {
        CloseHandle(pi.hProcess);
        CloseHandle(pi.hThread);
        char buffer[4096];
        DWORD bytesRead;
        while (ReadFile(hRead, buffer, sizeof(buffer), &bytesRead, NULL)) {
            if (bytesRead == 0) break;
            printf("[提示] 本地命令执行结果为: %.*s\n", bytesRead, buffer);
            printf("[发送]: ");
            int iResult = send(socket, buffer, bytesRead, 0);
            if (iResult == SOCKET_ERROR) {
                printf("[错误] 发送命令执行结果失败: %d\n", WSAGetLastError());
                closesocket(socket);
                WSACleanup();
            }
        }
        CloseHandle(hRead);
    }
    else if (dwWaitResult == WAIT_TIMEOUT) {
        printf("[错误] 等待命令执行超时\n");
        TerminateProcess(pi.hProcess, 1);
        CloseHandle(pi.hProcess);
        CloseHandle(pi.hThread);
        CloseHandle(hRead);
    }
    else {
        printf("[错误] 等待命令执行失败\n");
        CloseHandle(pi.hProcess);
        CloseHandle(pi.hThread);
        CloseHandle(hRead);
    }
}

VOID WINAPI resolveData(char* data, int data_len, SOCKET socket) {
    data[data_len] = '\0';
    char* prefix = strtok(data, "|");
    char *content = strtok(NULL, "|");
    if (strcmp(prefix, "FILE") == 0) {
        char* token = strtok(NULL, "|");
        handleFile(content, token, data_len, socket);
    }
    if (strcmp(prefix, "MSG") == 0) {
        handleMsg(content);
    }
    if (strcmp(prefix, "CMD") == 0) {
        handleCmd(content, socket);
    }
}

int __cdecl main(int argc, char** argv)
{
    if (argc != 3) {
        printf("[错误] Usage: %s server_ip server_port\n", argv[0]);
        return 1;
    }

    WSADATA wsaData;
    SOCKET ConnectSocket = INVALID_SOCKET;
    struct addrinfo* result = NULL,
        * ptr = NULL,
        hints;
    char sendbuf[DEFAULT_BUFLEN];
    char recvbuf[DEFAULT_BUFLEN];
    int iResult;
    int recvbuflen = DEFAULT_BUFLEN;

    // Initialize Winsock
    iResult = WSAStartup(MAKEWORD(2, 2), &wsaData);
    if (iResult != 0) {
        printf("[错误] Socket环境初始化失败: %d\n", iResult);
        return 1;
    }

    ZeroMemory(&hints, sizeof(hints));
    hints.ai_family = AF_UNSPEC;
    hints.ai_socktype = SOCK_STREAM;
    hints.ai_protocol = IPPROTO_TCP;

    // Resolve the server address and port
    iResult = getaddrinfo(argv[1], argv[2], &hints, &result);
    if (iResult != 0) {
        printf("[错误] 获取套接字地址结构失败: %d\n", iResult);
        WSACleanup();
        return 1;
    }

    // Attempt to connect to an address until one succeeds
    for (ptr = result; ptr != NULL; ptr = ptr->ai_next) {

        // Create a SOCKET for connecting to server
        ConnectSocket = socket(ptr->ai_family, ptr->ai_socktype,
            ptr->ai_protocol);
        if (ConnectSocket == INVALID_SOCKET) {
            printf("[错误] socket建立失败: %ld\n", WSAGetLastError());
            WSACleanup();
            return 1;
        }

        // Connect to server.
        iResult = connect(ConnectSocket, ptr->ai_addr, (int)ptr->ai_addrlen);
        if (iResult == SOCKET_ERROR) {
            closesocket(ConnectSocket);
            ConnectSocket = INVALID_SOCKET;
            continue;
        }
        break;
    }

    freeaddrinfo(result);

    if (ConnectSocket == INVALID_SOCKET) {
        printf("[错误] 无法连接至服务器\n");
        WSACleanup();
        return 1;
    }

    /*获取服务器的IP地址*/
    char ip[INET6_ADDRSTRLEN];
    struct sockaddr_in* ipv4 = (struct sockaddr_in*)ptr->ai_addr;
    struct sockaddr_in6* ipv6 = (struct sockaddr_in6*)ptr->ai_addr;
    void* addr;
    const char* ipver;

    // get the pointer to the address itself,
    // different fields in IPv4 and IPv6:
    if (ipv4->sin_family == AF_INET) { // IPv4
        addr = &(ipv4->sin_addr);
        ipver = "IPv4";
    }
    else { // IPv6
        addr = &(ipv6->sin6_addr);
        ipver = "IPv6";
    }

    // convert the IP to a string and print it:
    inet_ntop(ptr->ai_family, addr, ip, sizeof ip);
    printf("[提示] 连接到服务器%s: %s\n", ipver, ip);

    // Send an initial buffer
    strcpy_s(sendbuf, "Hello, Server, I'm client\n");
    iResult = send(ConnectSocket, sendbuf, (int)strlen(sendbuf), 0);
    if (iResult == SOCKET_ERROR) {
        printf("send failed with error: %d\n", WSAGetLastError());
        closesocket(ConnectSocket);
        WSACleanup();
        return 1;
    }

    printf("[提示] 已给服务器发送初始问候\n");

    HANDLE hThread;
    DWORD dwThreadId;
    hThread = CreateThread(NULL, 0, sendToServer, &ConnectSocket, 0, &dwThreadId);
    if (hThread == NULL) {
        printf("[错误] 创建对话子线程失败%d\n", GetLastError());
        return 1;
    }
    printf("[提示] 在子线程 %d 中开始同服务端进行对话\n", dwThreadId);

    // Receive until the peer closes the connection
    do {
        iResult = recv(ConnectSocket, recvbuf, recvbuflen, 0);
        if (iResult > 0) {
            resolveData(recvbuf, iResult, ConnectSocket);
            memset(recvbuf, 0, DEFAULT_BUFLEN);
        }
        else if (iResult == 0)
            printf("[提示] 关闭连接\n");
        else
            printf("[错误] 接收数据失败: %d\n", WSAGetLastError());

    } while (iResult > 0);

    // cleanup
    closesocket(ConnectSocket);
    WSACleanup();

    return 0;
}

遇到的问题

  • 编译时头文件包含的问题:<winsock2.h>头文件必须放在代码最前面
  • 中文处理的问题:用MultiByteToWideChar将接收到的字节数组转Unicode字符串,并用wprintf来打印中文字符,同时还需要用_setmode(_fileno(stdout), _O_U8TEXT)临时设置stdout的中文显示格式,在wprintf后需要将stdout显示格式改回来,否则printf会报错
  • socket粘包分包的问题:
    • 重点说下这个,当时我在服务端明明是分两次发送的数据包,但是观察到在客户端被一次recv接收到了,必应查到是两次send的时机太近、前一次send的数据量太小,导致粘包,我还在两次send间加了sleep和函数调用,发现都没办法分开,内核就是判定这两次send要用同一个数据包
    • 解决办法:在服务端发送数据时加入了包头,用来区分不同的数据包,同时在文件数据包中加入了文件大小的字段;这样在客户端读时,就能根据数据包头区分不同的数据内容,同时按照文件大小来分块读文件数据,避免读到其它的数据内容
  • recv返回时机的问题:我这里用的是阻塞态的recv,只有读到数据后recv才会返回,还要注意虽然给recv指定了size参数,但这个size是指这次读取的最大容量,而非必须要读到这么多的容量,也就是说如果这次发送的数据包大小小于size,recv接收到这个数据包后也会立即返回,不会继续等待接受完所有size的数据

效果展示

  • vs编译,大小只有17KB
  • 运行截图