【学习记录】C&C++网络编程——用C++标准线程库实现跨平台

Friday, June 9, 2023
本文共1790字
4分钟阅读时长

⚠️本文是作者P3troL1er原创,首发于https://peterliuzhi.top/posts/%E5%AD%A6%E4%B9%A0%E8%AE%B0%E5%BD%95cc++%E7%BD%91%E7%BB%9C%E7%BC%96%E7%A8%8B%E7%94%A8c++%E6%A0%87%E5%87%86%E7%BA%BF%E7%A8%8B%E5%BA%93%E5%AE%9E%E7%8E%B0%E8%B7%A8%E5%B9%B3%E5%8F%B0/。商业转载请联系作者获得授权,非商业转载请注明出处!

The only real mistake is the one from which we learn nothing. — John Powell

引入

【学习记录】C&C++网络编程——文件传输(兼容Windows和Linux) - P3troL1er 的个人博客我提到兼容Windows和Linux太累了,所以想用C++标准库代替。但是C++暂时没有标准网络库,而且socket库在Windows和Linux差别并不大,又暂时不想引入外部库,同时代码中用到的api都还比较浅,所以暂时只能替代多线程相关的部分。

完整代码

其实代码逻辑差别并不大,只是把能替换的C-style api全部替换成C++风格了

server

#ifdef _WIN32
#define _CRT_SECURE_NO_WARNINGS
#endif
// C库
#include <cerrno>
#include <csignal>
#include <cstring>
// C++库
#include <string>
#include <thread>
#include <chrono>
#include <iostream>
#include <fstream>

// ---web---
#ifdef _WIN32
#include <winsock2.h>
#include <ws2tcpip.h>
// ---
#include <windows.h>
#pragma comment(lib, "ws2_32.lib")
#else
#include <arpa/inet.h>
// #include <unistd.h>
#endif

#define readonly const
#define BUFSIZE 1024


// 几个宏用来防止手动写入过多错误判断代码
#define CKEX(EXP)                                                                        \
    do {                                                                                 \
        if ((EXP)) {                                                                     \
            std::cerr << "[EXIT ERROR] " << #EXP << " raise error: " << strerror(errno) << std::endl; \
            exit(127 + errno);                                                           \
        }                                                                                \
    } while (0)

#define CKE(EXP) CKEX((EXP) == -1)
#define CKEN(EXP) CKEX((EXP) != 0)
#define CKEZ(EXP) CKEX((EXP) == 0)

// 用来保存通信线程所需的相关信息
typedef struct {
#ifdef _WIN32
    SOCKET cfd;
#else
    int cfd; // 通信的文件描述符
#endif // _WIN32
    struct sockaddr_in details; // 客户端的相关信息
    socklen_t details_len; // 上面那个结构体的长度,如果通信线程调用recvfrom就可以用到
    char ip[16]; // 保存客户端的IP字符串
    int port; // 保存客户端的端口
    std:🧵:id tid;
} netfd;

static void communicate(netfd* arg)
{
    netfd& netinfo = *arg;
    netinfo.tid = std::this_thread::get_id();
    // 每个已连接的通信线程都有1kb的缓冲区
    char fname[BUFSIZE] { 0 };
    // 固定的回复
    char response[BUFSIZE] { 0 };
    // TODOrecv & send
    // 获取收到的字节数量
    int len = recv(netinfo.cfd, fname, sizeof(fname) - 1, 0);
    if (len > 0) {
        std::cout   << "client from "
                    << netinfo.ip
                    << ':'
                    << netinfo.port
                    << " ask for file: "
                    << fname
                    << std::endl;
        std::ifstream inFile(fname, std::ios::in | std::ios::binary);
        CKEX(!inFile.is_open());
        while (!inFile.eof()) {
            inFile.read(response, BUFSIZE);
            send(netinfo.cfd, response, sizeof(char) * (inFile.gcount()), 0);
        }
        std::cout << "Transformation Done!" << std::endl;
        inFile.close();
    } else if (len == 0) {
        // TODO接收到客户端终止连接请求
        std::cerr << "the client has ended the connection..." << std::endl;
    } else {
        // 接收到错误就打印出来,并且退出线程
        std::cerr << "[EXIT ERROR] recv() raise error: " << strerror(errno) << std::endl;
    }
    std::cout   << "exiting thread "
                << netinfo.tid
                << " (communication with "
                << netinfo.ip
                << ':'
                << netinfo.port
                << ")..."
                << std::endl;
    delete &netinfo;
#ifdef _WIN32
    closesocket(netinfo.cfd);
#else
    close(netinfo.cfd);
#endif // _WIN32
}

int main(int argc, char const* argv[])
{
    // 解除std::cout和std::cin之间的同步
    std::ios_base::sync_with_stdio(false);
    int port = 10000;
    if (argc >= 2)
    {
        std::string tmp(argv[1]);
        port = std::stoi(tmp);
    }
    // 声明局部变量
#ifdef _WIN32
    SOCKET lfd;
    WSADATA wsaData;
    WSAStartup(MAKEWORD(2, 2), &wsaData);
#else
    int lfd;
#endif // _WIN32
    struct sockaddr_in server_addr; // 保存服务器的相关信息
    constexpr readonly int connection_num = 8; // 设置最大连接数
    // 设置信号处理函数
    auto sig_int = [](int signo) -> void {
        std::cout << "thread " << std::this_thread::get_id() << "received: ";
        #ifdef _WIN32
            std::cout << _sys_errlist[signo] << std::endl;
        #else
            std::cout << strsignal(signo) << std::endl;
        #endif // _WIN32
        std::cout << "server exiting..." << std::endl;
        exit(signo);
    };
    signal(SIGINT, sig_int);
    // TODO设置监听的套接字
    // 设置TCP协议
    CKE(lfd = socket(AF_INET, SOCK_STREAM, 0));
    // TODO绑定端口
    server_addr.sin_family = AF_INET;
    // from host byte order to network byte order
    server_addr.sin_port = htons(port);
    // 代表本机的任意地址
    // 或者设置其中一个IP,使用inet_pton将IP地址字符串转换为二进制格式
    // inet_pton - convert IPv4 and IPv6 addresses from text to binary form
    server_addr.sin_addr.s_addr = INADDR_ANY;
    CKEN(bind(lfd, (struct sockaddr*)&server_addr, sizeof(server_addr)));
    // TODO设置端口监听
    CKEN(listen(lfd, connection_num));
    // TODO开始接收连接请求
    // 阻塞直到有客户端连接
    // 在栈空间中保存该结构体的大小,因为后面的api需要传指针进去,可能会更改这个值
    // 不停地检测是否有连接
    // 如果没有连接就阻塞
    while (true) {
        // 这个结构体包括了传给线程的参数,因此不能是局部变量,可以在堆上分配
        netfd* netinfo = new netfd;
        netinfo->details_len = sizeof(netinfo->details);
        /**
        On  success,  these system calls return a file descriptor for the accepted socket (a nonnegative integer).  On error, -1 is returned, errno is set appropriately, and addrlen is left unchanged.
        */
        CKE(netinfo->cfd = accept(lfd, (struct sockaddr*)&(netinfo->details), &(netinfo->details_len)));
        // 此时应该已经收到了客户端的相关信息
        netinfo->port = ntohs(netinfo->details.sin_port);
#ifdef _WIN32
        inet_ntop(AF_INET, &(netinfo->details.sin_addr), netinfo->ip, netinfo->details_len);
#else
        inet_ntop(AF_INET, &(netinfo->details.sin_addr.s_addr), netinfo->ip, netinfo->details_len);
#endif // _WIN32

        // 打印客户端相关信息
        std::cout   << "client's IP: " 
                    << netinfo->ip
                    << ", port: "
                    << netinfo->port
                    << std::endl;
        // 创建信息
        std::thread t(communicate, netinfo);
        t.detach();
    }
    // TODO关闭两个端口
    // 在主线程只用关闭监听的文件描述符
#ifdef _WIN32
    closesocket(lfd);
    WSACleanup();
#else
    close(lfd);
#endif // _WIN32
    return 0;
}

client

#ifdef _WIN32
#define _CRT_SECURE_NO_WARNINGS
#endif
#include <cerrno>
#include <cstring>
#include <climits>
#include <iostream>
#include <fstream>
#include <string>
#include <filesystem>
#ifdef _WIN32
#include <winsock2.h>
#include <ws2tcpip.h>
// ---
#include <windows.h>
#pragma comment(lib, "ws2_32.lib")
#define REMOTE_PATH_SEP '/'
#else
#include <arpa/inet.h>
#include <unistd.h>
#define REMOTE_PATH_SEP '\\'
#endif

#define LOCAL_PATH_SEP std::filesystem::path::preferred_separator

#define  readonly  const


// 几个宏用来防止手动写入过多错误判断代码
#define CKEX(EXP)                                                                        \
    do {                                                                                 \
        if ((EXP)) {                                                                     \
            std::cerr << "[EXIT ERROR] " << #EXP << " raise error: " << strerror(errno) << std::endl; \
            exit(127 + errno);                                                           \
        }                                                                                \
    } while (0)

#define CKE(EXP) CKEX((EXP) == -1)
#define CKEN(EXP) CKEX((EXP) != 0)
#define CKEZ(EXP) CKEX((EXP) == 0)

int main(int argc, char const* argv[])
{
    std::ios_base::sync_with_stdio(false);
    if (argc < 3) {
        std::cerr   << "Usage: "
                    << argv[0]
                    << " <IP address> <filename>"
                    << std::endl;
        exit(127);
    }
    std::string IP = argv[1];
    // 尝试从IP中分离出port
    size_t port_place = IP.find(':');
    int port = 10000;
    if (port_place != std::string::npos)
    {
        IP = IP.substr(0, port_place);
        port_place++;
        port = std::stoi(IP.substr(port_place));
    }
    std::string fname = argv[2];
    size_t sep_place = fname.rfind(REMOTE_PATH_SEP) + 1;
    if (sep_place == std::string::npos)
    {
        sep_place = 0;
    }
    int path_ptr = 0;
    std::filesystem::path currentPath = std::filesystem::current_path();
    std::string path = currentPath.generic_string();
    path += LOCAL_PATH_SEP;
    path += fname.substr(sep_place);
    readonly char* store_fname = path.c_str();
    std::cout << "Writing to " << path << std::endl;
    // 声明局部变量
    char buf[1024] { 0 };
#ifdef _WIN32
    struct WSAData wsa;
    SOCKET fd;
    // 初始化套接字库
    WSAStartup(MAKEWORD(2, 2), &wsa);
#else
    int fd;
#endif
    // TODO 建立套接字
#ifdef _WIN32
    CKEX((fd = socket(AF_INET, SOCK_STREAM, 0)) == INVALID_SOCKET);
#else
    CKE(fd = socket(AF_INET, SOCK_STREAM, 0));
#endif // _WIN32

    // TODO 建立连接
    // 设置服务器地址、端口
    struct sockaddr_in server_addr;
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(port);
    inet_pton(AF_INET, IP.c_str(), &(server_addr.sin_addr));
    CKEX(connect(fd, (struct sockaddr*)&server_addr, sizeof(server_addr)));
    // TODO send & recv
    int num = fname.size();
    send(fd, fname.c_str(), sizeof(char) * fname.size(), 0);
    std::ofstream outFile(path, std::ios::out | std::ios::binary);
    struct timeval timeout;
    timeout.tv_sec = 2;
    timeout.tv_usec = 0;
    fd_set read_fds;
    FD_ZERO(&read_fds);
    FD_SET(fd, &read_fds);
    while (true) {
        int result = select(fd + 1, &read_fds, NULL, NULL, &timeout);
        if (result == -1) {
            std::cerr << "[EXIT ERROR] select() raise error: " << strerror(errno) << std::endl;
            exit(127 + errno);
        } else if (result == 0) {
            std::cerr << "Timeout waiting for data." << std::endl;
            break;
        }
        int received = recv(fd, buf, sizeof(buf)-1, 0);
        if (received == 0) {
            std::cout   << "the server has ended its service...\n"
                        << "check your file ("
                        << path
                        << ") to find out if it went well..."
                        << std::endl;
            break;
        } else if (received == -1){
            std::cerr << "[EXIT ERROR] recv() raise error: " << strerror(errno) << std::endl;
            exit(127 + errno);
        } else{
            outFile.write(buf, sizeof(char)*received);
            std::cout.write(buf, sizeof(char)*received);
        }
    }
    std::cout << "Transformation Done!" << std::endl;
    outFile.close();
    // TODO 关闭连接
#ifdef _WIN32
    closesocket(fd);
    // 注销Winsock相关库
    WSACleanup();
#else
    close(fd);
#endif
    return 0;
}