99、跨平台网络编程:Windows Winsock与Linux差异、条件编译

做网络编程这么多年,我踩过最深的坑,就是跨平台移植。你想想看,在Linux上跑得飞快的代码,拿到Windows上一编译,满屏报错。说白了,Winsock和Berkeley Socket虽然同根同源,但细节上处处是雷。今天我就把这两套API的差异掰开揉碎了讲,再教你用条件编译一劳永逸。

一、核心差异:Winsock vs Linux Socket

先看一张对比图,心里有个底:

Winsock vs Linux Socket 核心差异 Windows Winsock • 需要 WSAStartup() 初始化 • SOCKET 类型,不是 int • closesocket() 关闭 • WSAGetLastError() 获取错误 • ioctlsocket() 设置非阻塞 • 头文件 winsock2.h • 需要链接 ws2_32.lib Linux Socket • 无需初始化 • int 文件描述符 • close() 关闭 • errno 获取错误 • fcntl() 设置非阻塞 • 头文件 sys/socket.h • 链接 -lsocket (部分系统)

二、头文件与库的差异

我刚开始做跨平台项目时,第一件事就是被头文件搞懵了。Linux下只需要 #include <sys/socket.h>,Windows下却要 #include <winsock2.h>。更坑的是,Windows还得加一句 #pragma comment(lib, "ws2_32.lib") 或者手动链接库。

核心差异表:

项目 Windows (Winsock) Linux (Berkeley)
头文件 winsock2.h sys/socket.h, netinet/in.h, arpa/inet.h
库文件 ws2_32.lib (需链接) 无需额外链接 (部分系统需 -lsocket)
Socket类型 SOCKET (无符号句柄) int (文件描述符)
初始化 WSAStartup() / WSACleanup() 无需
关闭Socket closesocket() close()
错误码获取 WSAGetLastError() errno
非阻塞设置 ioctlsocket() fcntl()
发送/接收 send(), recv() (参数略有不同) send(), recv() (基本一致)

三、条件编译实战:一套代码跑通两个平台

我个人习惯用 #ifdef _WIN32 来做平台区分。为什么?因为 _WIN32 在Windows下默认定义,Linux下没有,干净利落。

来看一个完整的跨平台TCP客户端框架:

#ifdef _WIN32
    #include <winsock2.h>
    #include <windows.h>
    #pragma comment(lib, "ws2_32.lib")
#else
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <arpa/inet.h>
    #include <unistd.h>
    #include <fcntl.h>
    #include <errno.h>
#endif

// 统一类型定义
#ifdef _WIN32
    typedef SOCKET socket_t;
    #define INVALID_SOCKET_VAL INVALID_SOCKET
    #define SOCKET_ERROR_VAL SOCKET_ERROR
#else
    typedef int socket_t;
    #define INVALID_SOCKET_VAL (-1)
    #define SOCKET_ERROR_VAL (-1)
#endif

// 初始化与清理
int init_socket_lib() {
#ifdef _WIN32
    WSADATA wsa;
    if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0) {
        printf("WSAStartup failed\n");
        return -1;
    }
#endif
    return 0;
}

void cleanup_socket_lib() {
#ifdef _WIN32
    WSACleanup();
#endif
}

// 关闭socket
void close_socket(socket_t sock) {
#ifdef _WIN32
    closesocket(sock);
#else
    close(sock);
#endif
}

// 设置非阻塞
int set_nonblocking(socket_t sock) {
#ifdef _WIN32
    u_long mode = 1;
    return ioctlsocket(sock, FIONBIO, &mode);
#else
    int flags = fcntl(sock, F_GETFL, 0);
    if (flags == -1) return -1;
    return fcntl(sock, F_SETFL, flags | O_NONBLOCK);
#endif
}

// 获取错误码
int get_socket_error() {
#ifdef _WIN32
    return WSAGetLastError();
#else
    return errno;
#endif
}

// 打印错误信息
void print_socket_error(const char* msg) {
#ifdef _WIN32
    printf("%s: %d\n", msg, WSAGetLastError());
#else
    perror(msg);
#endif
}

我的经验: 把平台差异封装成统一接口,上层代码完全不用关心底层是Windows还是Linux。我在一个物联网网关项目里就是这么干的,后来从Windows移植到Linux,只改了条件编译部分,业务逻辑一行没动。

四、避坑指南:我踩过的那些雷

嗯,这里要注意几个容易翻车的地方:

  • Socket类型不一致: 我曾经在Linux下用 int 存socket,移植到Windows时忘了改,结果 SOCKET 是无符号的,赋值给 int 后比较 INVALID_SOCKET 永远不相等。后来统一用 socket_t 宏定义解决了。
  • 初始化顺序: Windows下必须先调 WSAStartup(),再调 socket()。我见过有人把初始化放在全局变量里,结果构造函数执行顺序不对,程序一启动就崩。
  • 关闭socket: Linux用 close(),Windows用 closesocket()。千万别混用,否则Windows下句柄泄漏,Linux下报错。
  • 错误码处理: Windows的 WSAGetLastError() 和Linux的 errno 值不同。比如 EAGAIN 在Linux是11,Windows下对应 WSAEWOULDBLOCK 是10035。我建议统一用宏封装。

警告: 不要直接用 #ifdef WIN32,标准写法是 #ifdef _WIN32(带下划线)。WIN32 在某些编译器里可能没定义,而 _WIN32 是MSVC和MinGW都保证定义的。

五、完整示例:跨平台TCP回显客户端

下面是一个完整的回显客户端,你直接拿去用:

#include <stdio.h>
#include <string.h>

// 包含上面的跨平台封装代码
// ... (init_socket_lib, close_socket, 等)

int main() {
    if (init_socket_lib() != 0) {
        return -1;
    }

    socket_t sock = socket(AF_INET, SOCK_STREAM, 0);
    if (sock == INVALID_SOCKET_VAL) {
        print_socket_error("socket");
        cleanup_socket_lib();
        return -1;
    }

    struct sockaddr_in addr;
    memset(&addr, 0, sizeof(addr));
    addr.sin_family = AF_INET;
    addr.sin_port = htons(8888);
    addr.sin_addr.s_addr = inet_addr("127.0.0.1");

    if (connect(sock, (struct sockaddr*)&addr, sizeof(addr)) == SOCKET_ERROR_VAL) {
        print_socket_error("connect");
        close_socket(sock);
        cleanup_socket_lib();
        return -1;
    }

    const char* msg = "Hello, cross-platform!";
    send(sock, msg, strlen(msg), 0);

    char buf[1024] = {0};
    recv(sock, buf, sizeof(buf) - 1, 0);
    printf("Received: %s\n", buf);

    close_socket(sock);
    cleanup_socket_lib();
    return 0;
}

这段代码在Windows和Linux下都能编译通过。你只需要把平台相关的部分用条件编译包起来,剩下的逻辑完全一致。说白了,跨平台编程的核心就是「抽象差异,统一接口」。

我个人习惯把平台封装单独放在一个 platform_socket.hplatform_socket.c 里,业务代码只调用封装后的函数。这样后期维护起来特别清爽,你想想看,如果哪天要支持macOS,只需要在条件编译里加个 __APPLE__ 分支就行。

好了,这一章的内容就到这里。记住:跨平台不是玄学,是工程。把差异封装好,你的代码就能跑遍天下。


公众号:蓝海资料掘金营,微信 deep3321