【学习记录】C&C++网络编程——文件传输(兼容Windows和Linux)

Monday, June 5, 2023
本文共2465字
5分钟阅读时长

⚠️本文是作者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%E6%96%87%E4%BB%B6%E4%BC%A0%E8%BE%93%E5%85%BC%E5%AE%B9windows%E5%92%8Clinux/。商业转载请联系作者获得授权,非商业转载请注明出处!

In the depth of winter, I finally learned that there was within me an invincible summer. — Albert Camus

引入

之前在【学习记录】C&C++网络编程初步的时候写了如何初步通过socket api进行服务器和客户端之间的通信,于是我就想,能不能做一个文件传输系统,客户端发送给服务器一个文件路径,然后服务器在自己的系统上打开然后输出给客户端?就类似于scp那样的。

同时这个文件传输系统必须同时在Linux和Windows上兼容,因为很多情况下人们测试是在Windows上打开Linux虚拟机进行测试的。做到这一点可以使用宏控制,但是会比较麻烦,我决定将来要用C++标准库重写一遍。

基本原理

在客户端的shell中通过命令行参数指定IP地址、port和主机系统文件路径,然后通过IP:port连接服务器,再将主机系统文件路径传递给服务器,然后服务器在自己的系统上打开文件并传输给客户端,同时客户端使用select(IO多路转接)检测在一定时间内是否还有接收,如果超时则停止。

如何与Windows兼容

代码能使用C标准库就使用标准库(比如Windows上没有open/read/write而是另一套API,这时我选择标准输入输出流),尽量不使用外部第三方库(比如我先在Linux上用pthread写了服务器,虽然Windows上也有第三方pthread库,但是尽量不使用),使用宏控制代码生成:

#ifdef _WIN32
	...
#else
	...
#endif

同时CMakeLists.txt也对两个平台进行适配:

cmake_minimum_required(VERSION 3.25)
set(CMAKE_C_STANDARD 11)
set(CMAKE_CXX_STANDARD 17)
project(cweb)

set(EXECUTABLE_OUTPUT_PATH ${CMAKE_SOURCE_DIR}/bin)

add_executable(file_client file_client.c)
add_executable(file_server file_server.c)
if(CMAKE_HOST_SYSTEM_NAME MATCHES "Windows")
    target_link_libraries(file_client PRIVATE wsock32)
    target_link_libraries(file_client PRIVATE Ws2_32)
    target_link_libraries(file_server PRIVATE wsock32)
    target_link_libraries(file_server PRIVATE Ws2_32)
endif(CMAKE_HOST_SYSTEM_NAME "Windows")

如何确定服务器和客户端的IP和port

服务器的IP不用指定,但是port可以通过命令行指定,如不指定则默认端口为10000

客户端通过命令行参数指定IP:port,其第一个命令行参数可以是"127.0.0.1"这种不带port的(默认port为10000),也可以是"127.0.0.1:12345"这种加一个冒号后面跟port的

完整代码

服务器

#ifdef _WIN32
#define _CRT_SECURE_NO_WARNINGS
#endif
#include <errno.h>
#include <fcntl.h>
#include <signal.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// ---web---
#ifdef _WIN32
#include <winsock2.h>
#include <ws2tcpip.h>
// --- windows.h要求要在winsock2.h下面,为防止格式化程序更换次序所以加注释隔开
#include <windows.h>
#pragma comment(lib, "ws2_32.lib")
#else
#include <arpa/inet.h>
#include <pthread.h>
#include <unistd.h>

#endif

// 几个宏用来防止手动写入过多错误判断代码
#define CKEX(EXP)                                                                        \
    do {                                                                                 \
        if ((EXP)) {                                                                     \
            fprintf(stderr, "[EXIT ERROR] %s raise error: %s\n", #EXP, strerror(errno)); \
            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; // 保存客户端的端口
#ifdef _WIN32
    DWORD tid;
#else
    pthread_t tid;
#endif // _WIN32
} netfd;

#ifdef _WIN32
static DWORD WINAPI communicate(void* arg)
#else
static void* communicate(void* arg)
#endif // _WIN32
{
    // 通过强制转换
    netfd* netinfo = (netfd*)arg;
    // 每个已连接的通信线程都有1kb的缓冲区
    char fname[1024] = { 0 };
    // 固定的回复
    char response[1024] = { 0 };
    // TODOrecv & send
    // 获取收到的字节数量
    int len = recv(netinfo->cfd, fname, sizeof(fname) - 1, 0);
    if (len > 0) {
        printf("client from %s:%d ask for file: %s\n", netinfo->ip, netinfo->port, fname);
        long int num;
        FILE* fp = fopen(fname, "rb");
        CKEX(fp == NULL);
        while ((num = fread(response, sizeof(char), sizeof(response)/sizeof(char)-1, fp)) > 0) {
            // response[status] = 0;
            send(netinfo->cfd, response, sizeof(char) * num, 0);
// #ifdef _WIN32
//             Sleep(1000);
// #else
//             sleep(1);
// #endif // _WIN32
            // Sleep(1000);
        }
        // puts("b 1");
        CKEN((!feof(fp)) && ferror(fp));
        if (fp != NULL){
            fclose(fp);
            fp = NULL;
        }
    } else if (len == 0) {
        // TODO接收到客户端终止连接请求
        puts("the client has ended the connection...");
    } else {
        // 接收到错误就打印出来,并且退出线程
        fprintf(stderr, "[EXIT ERROR] %s raise error: %s\n", "recv()", strerror(errno));
    }
    printf("exiting thread %ld (communication with %s:%d)...", netinfo->tid, netinfo->ip, netinfo->port);
#ifdef _WIN32
    CloseHandle((HANDLE)netinfo->tid);
#endif // _WIN32
    free(netinfo);
    netinfo = NULL;
#ifdef _WIN32
    closesocket(netinfo->cfd);
    return 0;
#else
    close(netinfo->cfd);
    pthread_exit((void*)0);
#endif // _WIN32
}

static void sig_int(int signo)
{
#ifdef _WIN32
    printf("thread %ld received: %s\n", GetCurrentThreadId(), _sys_errlist[signo]);
#else
    printf("thread %ld received: %s\n", pthread_self(), strsignal(signo));
#endif // _WIN32
    puts("server exiting...");
    exit(signo);
}

int main(int argc, char const* argv[])
{
    int port = 10000;
    if (argc >= 2)
    {
        port = atoi(argv[1]);
    }
    // 声明局部变量
#ifdef _WIN32
    SOCKET lfd;
    WSADATA wsaData;
    WSAStartup(MAKEWORD(2, 2), &wsaData);
    HANDLE err;
#else
    int lfd;
    pthread_attr_t tid_attr;
    int err;
#endif // _WIN32
    struct sockaddr_in server_addr; // 保存服务器的相关信息
    const int connection_num = 8; // 设置最大连接数
    // 设置信号处理函数
    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));
#ifndef _WIN32
    // 初始化线程属性,设置为线程启动即分离
    CKEN((err = pthread_attr_init(&tid_attr)));
    CKEN((err = pthread_attr_setdetachstate(&tid_attr, PTHREAD_CREATE_DETACHED)));
#endif //_WIN32
    // TODO开始接收连接请求
    // 阻塞直到有客户端连接
    // 在栈空间中保存该结构体的大小,因为后面的api需要传指针进去,可能会更改这个值
    // 不停地检测是否有连接
    // 如果没有连接就阻塞
    while (true) {
        // 这个结构体包括了传给线程的参数,因此不能是局部变量,可以在堆上分配
        netfd* netinfo = (netfd*)malloc(sizeof(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

        // 打印客户端相关信息
        printf(
            "client's IP: %s, port: %d\n",
            netinfo->ip,
            netinfo->port);
        // 创建信息
#ifdef _WIN32
        CKEZ(err = CreateThread(NULL, 0, communicate, (LPVOID)netinfo, DETACHED_PROCESS, &netinfo->tid));
        // CloseHandle((HANDLE)tid);
#else
        CKEN(err = pthread_create(&netinfo->tid, &tid_attr, communicate, (void*)netinfo));
#endif // _WIN32
    }
    // TODO关闭两个端口
    // 在主线程只用关闭监听的文件描述符
#ifdef _WIN32
    closesocket(lfd);
    WSACleanup();
#else
    close(lfd);
#endif // _WIN32
    return 0;
}

客户端

#ifdef _WIN32
#define _CRT_SECURE_NO_WARNINGS
#endif
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#ifdef _WIN32
#include <winsock2.h>
#include <ws2tcpip.h>
// --- windows.h要求要在winsock2.h下面,为防止格式化程序更换次序所以加注释隔开
#include <windows.h>
#pragma comment(lib, "ws2_32.lib")
#define REMOTE_PATH_SEP '/'
#define LOCAL_PATH_SEP '\\'
#else
#include <arpa/inet.h>
#include <unistd.h>
#include <limits.h>
#define REMOTE_PATH_SEP '\\'
#define LOCAL_PATH_SEP '/'
#endif

#define CKEX(EXP)                                                                        \
    do {                                                                                 \
        if ((EXP)) {                                                                     \
            fprintf(stderr, "[EXIT ERROR] %s raise error: %s\n", #EXP, strerror(errno)); \
            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[])
{
    if (argc < 3) {
        fprintf(stderr, "Usage: %s <IP address[:port]> <filename>", argv[0]);
        exit(127);
    }
    const char* IP = argv[1];
    // 尝试从IP中分离出port
    char* port_str = strchr(IP, ':');
    int port = 10000;
    if (port_str != NULL)
    {
	    *port_str = 0;
        port_str++;
        port = atoi(port_str);
    }
    const char* fname = argv[2];
    char* store_fname = strrchr(fname, REMOTE_PATH_SEP)+1;
    int path_ptr = 0;
#ifdef _WIN32
    char path[MAX_PATH] = { 0 };
    path_ptr = MAX_PATH - strlen(store_fname) - 2;
    CKEZ(GetCurrentDirectory(path_ptr, path));
#else
    char path[PATH_MAX] = {0};
    path_ptr = PATH_MAX - strlen(store_fname) - 2;
    CKEZ(getcwd(path, path_ptr));
#endif
    path_ptr = strlen(path);
    path[path_ptr++] = LOCAL_PATH_SEP;
    // puts("b1");
    strncpy(path + path_ptr, store_fname, strlen(store_fname) + 1);
    store_fname = path;
    printf("Writing to %s\n", store_fname);
    // 声明局部变量
    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, &(server_addr.sin_addr));
    CKEX(connect(fd, (struct sockaddr*)&server_addr, sizeof(server_addr)));
    // TODO send & recv
    int num = strlen(fname);
    send(fd, fname, sizeof(char) * (num + 1), 0);
    FILE* fp = fopen(store_fname, "wb");
    // 超时时间定为5秒
    struct timeval timeout;
    timeout.tv_sec = 5;
    timeout.tv_usec = 0;
    // 设置监控的文件描述符集合
    fd_set read_fds;
    FD_ZERO(&read_fds);
    FD_SET(fd, &read_fds);
    while (1) {
        int result = select(fd + 1, &read_fds, NULL, NULL, &timeout);
        if (result == -1) {
            fprintf(stderr, "[EXIT ERROR] %s raise error: %s\n", "select()", strerror(errno));
            exit(127 + errno);
        } else if (result == 0) {
            fprintf(stderr, "Timeout waiting for data.\n");
            break;
        }
        int received = recv(fd, buf, sizeof(buf)-1, 0);
        if (received == 0) {
            puts("the server has ended its service...");
            printf("check your file (%s) to find out if it went well...\n", store_fname);
            break;
        } else if (received == -1){
            fprintf(stderr, "[EXIT ERROR] %s raise error: %s\n", "recv()", strerror(errno));
            exit(127 + errno);
        } else{
            fwrite(buf, sizeof(char), received, fp);
            fwrite(buf, sizeof(char), received, stdout);
        }
    }
    puts("transformation done!");
    fclose(fp);
    // TODO 关闭连接
#ifdef _WIN32
    closesocket(fd);
    // 注销Winsock相关库
    WSACleanup();
#else
    close(fd);
#endif
    return 0;
}