首页 > 编程语言 >c++socket编程之客户端编写

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

时间:2023-04-01 12:35:20浏览次数:42  
标签:iResult socket int c++ ConnectSocket char printf 客户端

开头

  • 用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
  • 运行截图

标签:iResult,socket,int,c++,ConnectSocket,char,printf,客户端
From: https://www.cnblogs.com/z5onk0/p/17278359.html

相关文章

  • c++ socket编程之成品展示
    开头前面两篇介绍了服务端和客户端的编写,本篇展示运行效果多次测试,修复了bug,目前运行稳定,能够用于生产环境支持多个连接,能够同时处理多个数据传输任务效果展示服务端初始界面......
  • c++ socket编程之服务端编写
    开头想要写一个带界面、功能全面、传输高效、运行稳定的马儿,能够在生产环境下工作在cursor的帮助下,用一天时间完成了服务端和客户端的编写另外一天时间卡在了中文消息传输处理和大文件传输粘包、分包问题上功能收发消息,支持中文消息发送命令执行并显示命令执行结果任意......
  • c++基本用法学习
    1.保留小数的方式:cout<<setprecision(2)<<fixed<<p[0].sum<<endl;其中setprecision(n)填入想要保留的数字,fixed设置后可以保证保留的小数不会省略末尾的0同时也可以写成cout<<setprecision(2)<<fixed;cout<<p[0].sum<<endl;在程序中集体设置输出保留n......
  • 渡一教育_Java每日一练:建立Statement的作用是什么、前端Console.log( Boolean(‘‘))
    系列文章目录文章目录系列文章目录题目1java部分建立Statement的作用是什么(答案在最后公布)题目1-答案==解析====答案==题目2前端js部分==答案==题目3前端js部分如下代码输出的是什么答案和解析如下==解析==题目4如果希望1监听TCP端口为9000,服务端应该怎样创建socket题目答......
  • C++面试必备:常见C++面试题汇总及详细解析
    C++作为一门重要的编程语言,其在面试中常常是热门的考察对象。本文将会介绍一些常见的C++面试题,帮助C++面试者避免很多不必要的困惑和迷惑。每个问题都有相对应的答案,以便各位同学快速查阅。C++和C的区别是什么?C++是C的超集,也就是说,C++包括了C的所有基础特性,并且还增加了一些新的......
  • 关于linux环境下配置c/c++程序的编译器
    第一步:切换root用户     命令为:suroot然后输入密码即可第二步:输入命令  yum installgcc 和 yuminstall g++  第三步:通过查找路径来检查是否安装成功  whichgcc和 whichg++......
  • learn C++ for infrastructure software
    TolearnC++forinfrastructuresoftware,youcanfollowthesesteps:LearnthebasicsofC++:StartbylearningthebasicsofC++programminglanguage,includingsyntax,datatypes,controlstructures,functions,andobject-orientedprogrammingconcept......
  • RocketMQ-Windows安装-客户端rocketmq-dashboard 20230331
     一、Windows安装 https://github.com/apache/rocketmq/releases 1、解压rocketmq-all-4.8.0-bin-release.zip到D:\XXXA\rocketmq-all-4.8.0 2、RocketMQ配置环境变量:ROCKETMQ_HOMED:\XXXA\rocketmq-all-4.8.0NAMESRV_ADDRlocalhost:9876 PATH属性增加:......
  • jmeter中测试websocket接口
    一、jmeter安装对应的插件1、Jmeter不自带WebSocket功能,需要先安装WebSocket的插件,选项中选择pluginsManager; 2、在availableplugins搜索WebSocket进行插件下载即可,我是将搜索到的2个插件都安装了,待Jmeter重启后插件就是安装成功。二、WebSocketSampler组件添加方式:1、......
  • Unity客户端开发工程师的进阶之路
    UWA技能成长系统是UWA根据学员的职业发展目标,提供技能学习的推荐路径,再将所需学习内容按难易等多维度,设计分成多个学习阶段,可以循序渐进地进行学习。每个阶段学员完成学习任务后不但可以获得技能的提升,还将获得UWA社区相应的积分奖励(积分可兑换礼品和优惠券哦)。 进入技能成长......