8、网络编程基础:Socket API 的跨平台封装 (Berkeley sockets vs Winsock)
网络编程这块,说实话,是跨平台开发里最容易踩坑的地方之一。你想想看,Windows 和 Linux 都提供了 Socket API,但细节上就是不一样。我在早期做项目时,就吃过这个亏——代码在 Linux 上跑得好好的,移植到 Windows 上一编译,报错一大堆。
说白了,Berkeley sockets 是事实上的标准,几乎所有 Unix-like 系统都支持它。而 Winsock 是 Windows 上的实现,虽然大体上兼容,但总有些「小脾气」。今天我们就来聊聊怎么封装它们,让一套代码能在两边都跑起来。
核心思路:用条件编译 + 抽象层,把平台差异隔离在底层。上层业务代码只调用我们封装的统一接口。
8.1 两个世界的差异
先看看 Berkeley sockets 和 Winsock 到底差在哪。我列个表,你一看就明白:
| 对比项 | Berkeley sockets (Linux/Unix) | Winsock (Windows) |
|---|---|---|
| 头文件 | #include <sys/socket.h>#include <netinet/in.h> |
#include <winsock2.h> |
| 初始化 | 不需要 | 需要调用 WSAStartup() |
| 清理 | 不需要 | 需要调用 WSACleanup() |
| Socket 类型 | int (非负整数) |
SOCKET (实际上是 UINT_PTR) |
| 关闭 socket | close(fd) |
closesocket(s) |
| 错误码获取 | errno |
WSAGetLastError() |
| 阻塞/非阻塞 | fcntl() 设置 O_NONBLOCK |
ioctlsocket() 设置 FIONBIO |
看到没?光是头文件和关闭方式就不一样。我记得第一次把 Linux 上的网络代码搬到 Windows 时,close() 函数死活找不到,查了半天才发现 Windows 用的是 closesocket()。
8.2 封装策略:条件编译 + 抽象层
我的做法是:定义一个平台无关的接口,然后用条件编译分别实现。这样上层代码完全不用关心底层是哪个平台。
先定义一个抽象层头文件 net_common.h:
#ifndef NET_COMMON_H
#define NET_COMMON_H
#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
// 统一 socket 类型
#ifdef _WIN32
typedef SOCKET socket_t;
#define INVALID_SOCKET_VAL INVALID_SOCKET
#else
typedef int socket_t;
#define INVALID_SOCKET_VAL (-1)
#endif
// 统一关闭函数
static inline int net_close(socket_t fd) {
#ifdef _WIN32
return closesocket(fd);
#else
return close(fd);
#endif
}
// 统一错误码获取
static inline int net_get_error(void) {
#ifdef _WIN32
return WSAGetLastError();
#else
return errno;
#endif
}
// 统一初始化/清理
static inline int net_init(void) {
#ifdef _WIN32
WSADATA wsa;
return WSAStartup(MAKEWORD(2, 2), &wsa);
#else
return 0; // Linux 不需要初始化
#endif
}
static inline void net_cleanup(void) {
#ifdef _WIN32
WSACleanup();
#endif
}
#endif // NET_COMMON_H
我的习惯:把平台差异封装成内联函数,而不是宏。这样调试时能看到函数调用栈,比宏友好得多。
8.3 实战:跨平台 TCP 客户端
有了上面的封装层,写一个跨平台的 TCP 客户端就简单了。来看看代码:
#include "net_common.h"
#include <stdio.h>
#include <string.h>
int main() {
// 初始化网络库(Windows 需要)
if (net_init() != 0) {
printf("网络初始化失败\n");
return -1;
}
// 创建 socket
socket_t sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock == INVALID_SOCKET_VAL) {
printf("创建 socket 失败, 错误码: %d\n", net_get_error());
net_cleanup();
return -1;
}
// 连接服务器
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8080);
server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
if (connect(sock, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
printf("连接失败, 错误码: %d\n", net_get_error());
net_close(sock);
net_cleanup();
return -1;
}
printf("连接成功!\n");
// 发送数据
const char *msg = "Hello from cross-platform client!";
send(sock, msg, strlen(msg), 0);
// 接收数据
char buffer[1024] = {0};
int len = recv(sock, buffer, sizeof(buffer) - 1, 0);
if (len > 0) {
printf("收到: %s\n", buffer);
}
// 清理
net_close(sock);
net_cleanup();
return 0;
}
你看,这套代码在 Linux 和 Windows 上都能编译通过。唯一要注意的是,Windows 上需要链接 ws2_32.lib,我在头文件里已经用 #pragma comment(lib, ...) 处理了。
8.4 避坑指南
我曾经踩过的坑:
- Winsock 初始化顺序:必须在任何 socket 操作之前调用
WSAStartup()。我试过在创建 socket 之后才初始化,结果返回WSANOTINITIALISED,折腾了半天。 - socket 类型不一致:Linux 上用
int,Windows 上用SOCKET。如果你直接拿int去比较INVALID_SOCKET,在 Windows 上可能出问题。所以我统一用了socket_t。 - 错误码处理:Windows 的
WSAGetLastError()和 Linux 的errno值不一样。比如EAGAIN在 Linux 上是 11,在 Windows 上对应的是WSAEWOULDBLOCK(10035)。 - 关闭 socket:Windows 上不能用
close(),必须用closesocket()。而且closesocket()可能返回SOCKET_ERROR,不是 -1。
8.5 非阻塞模式封装
非阻塞模式也是常见需求。来看看怎么封装:
static inline int net_set_nonblock(socket_t fd, int nonblock) {
#ifdef _WIN32
u_long mode = nonblock ? 1 : 0;
return ioctlsocket(fd, FIONBIO, &mode);
#else
int flags = fcntl(fd, F_GETFL, 0);
if (flags == -1) return -1;
if (nonblock)
flags |= O_NONBLOCK;
else
flags &= ~O_NONBLOCK;
return fcntl(fd, F_SETFL, flags);
#endif
}
嗯,这里要注意:Windows 的 ioctlsocket() 用的是 u_long 指针,而 Linux 的 fcntl() 用的是位操作。封装之后,上层调用就统一了:
// 设置为非阻塞
net_set_nonblock(sock, 1);
// 设置为阻塞
net_set_nonblock(sock, 0);
8.6 知识体系总览
下面这张图,把 Socket API 跨平台封装的核心逻辑梳理了一遍:
说白了,整个封装的核心就是:上层统一,下层隔离。业务代码只跟统一接口打交道,平台差异全部藏在条件编译后面。
8.7 小结
跨平台网络编程,其实没那么玄乎。你只要记住几个关键点:
- 用
#ifdef _WIN32区分平台 - 把 socket 类型、关闭函数、错误码都封装起来
- Windows 别忘了
WSAStartup()和WSACleanup() - 非阻塞模式用统一的
net_set_nonblock()
我个人习惯是,每个新项目都先搭好这个封装层。虽然第一次花点时间,但后面移植起来就轻松多了。你想想看,如果代码里到处是 #ifdef,那维护起来得多痛苦?
一点建议:如果你用 CMake,可以在 CMakeLists.txt 里自动检测平台,然后定义宏。这样代码里只需要 #ifdef PLATFORM_WINDOWS 就行,更清晰。
公众号:蓝海资料掘金营,微信deep3321