4.3 设计跨平台网络通信库时需要注意的一些 socket 函数用法
这里说的跨平台,指的是在 Windows 和 Linux 使用相关 socket 函数,虽然二者的设计均参考 Berkeley Sockets,但是二者在演化的过程中还是存在不少区别,本节为读者整理了一些常用的注意事项。
# 4.3.1 socket 数据类型
Windows 上一个 socket 对象其类型是 SOCKET,这是一个句柄对象(本质上也是一个 int 类型),Linux 上一个 socket 对象是 int 类型,习惯上在 Windows 上称为“socket”,在 Linux 上称为 fd。所以在很多网络库中使用如下定义来包裹出一个跨平台使用的 socket 类型:
#ifdef WIN32
typedef SOCKET SOCKET_TYPE;
#else
typedef int SOCKET_TYPE;
#endif
2
3
4
5
这样就可以同时在 Windows 和 Linux 使用 SOCKET_TYPE 这个类型代表 socket 了。
无论是 Windows 还是 Linux,创建一个套接字的函数 socket() 调用失败均会返回 -1,Windows 为这种情形定义了一个宏:
#define INVALID_HANDLE_VALUE (-1)
Linux 不存在,我们可以自己通过上述语句定义之。
# 4.3.2 Windows 系统调用相关 socket 函数需要先初始化 socket 相关 dll 文件
Linux 程序可以直接使用相关 socket 函数,但是对于 Windows 平台必须先调用 WSAStartup() 函数显式将与 socket 相关的 dll 加载到进程地址空间来,程序退出时,需要调用 WSACleanup() 卸载相关 dll。
这两个函数的用法示例如下:
//程序初始化时调用
bool InitSocket()
{
//指定版本号
WORD wVersionRequested = MAKEWORD(2, 2);
WSADATA wsaData;
int nErrorID = ::WSAStartup(wVersionRequested, &wsaData);
if(nErrorID != 0)
return false;
if (LOBYTE(wsaData.wVersion) != 2 || HIBYTE(wsaData.wVersion) != 2)
{
UnInitSocket();
return false;
}
return TRUE;
}
//程序退出时调用
void UnInitSocket()
{
::WSACleanup();
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
需要注意的是 WSAStartup() 和 WSACleanup() 是进程相关的,任何一个线程都可以调用,对于 WSAStartup() 函数某个线程调用一次之后,其他线程就可以正常使用了;反过来,如果某个线程自己不再使用相关 socket 函数,如果其调用 WSACleanup() 函数,会导致其他线程无法继续使用相关 socket 函数。因为,你调用 WSACleanup() 函数之前应该确保整个程序不再有使用 socket 函数的地方。鉴于此,一般在进程退出时才调用该函数。
# 4.3.3 关闭 socket 函数
由于历史原因,Windows 从来没有以良好兼容的方式实现 Berkeley 套接字 API。关闭套接字的接口,在 Linux(Unix) 中使用 close() 函数,在Windows 中使用 **closesocket() ** 函数,问题是 Windows 也定义了一个 close() 函数,且该函数还不能用于关闭 socket。如果调用该函数关闭 socket,会导致程序崩溃。这是让很多开发者容易犯错的地方。我们可以这样进行包装一下:
#ifndef WIN32
#define closesocket(s) close(s)
#endif
2
3
这样我们在 Linux 平台也可以使用 closesocket 来关闭 socket 了。
# 4.3.4 获取 socket 函数错误码
如果某个 socket 函数调用失败,Windows 上需要调用 WSAGetLastError() 获取错误码,Linux 直接使用 errno 变量获取错误码,所以你可能会在一些网络库中看到如下代码:
#ifdef WIN32
#define GetSocketError() ::WSAGetLastError();
#else
#define GetSocketError() errno;
#endif
2
3
4
5
这样就可以使用 GetSocketError() 函数统一进行处理了。
我们看下 libevent 网络库在这一块的相关实现。
//Windows上获取一个socket错误码的实现
#ifdef _WIN32
int evutil_socket_geterror(evutil_socket_t sock)
{
int optval, optvallen=sizeof(optval);
int err = WSAGetLastError();
if (err == WSAEWOULDBLOCK && sock >= 0) {
//不仅使用了WSAGetLastError,还通过 getsocketopt 选项来获取错误
if (getsockopt(sock, SOL_SOCKET, SO_ERROR, (void*)&optval,
&optvallen))
return err;
if (optval)
return optval;
}
return err;
}
#endif
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//Linux 上设置错误码
#define EVUTIL_SET_SOCKET_ERROR(errcode) \
do { errno = (errcode); } while (0)
2
3
//Windows上把错误码转换成错误描述信息的函数实现
/** Equivalent to strerror, but for windows socket errors. */
const char *evutil_socket_error_to_string(int errcode)
{
struct cached_sock_errs_entry *errs, *newerr, find;
char *msg = NULL;
EVLOCK_LOCK(windows_socket_errors_lock_, 0);
find.code = errcode;
errs = HT_FIND(cached_sock_errs_map, &windows_socket_errors, &find);
if (errs) {
msg = errs->msg;
goto done;
}
if (0 != FormatMessageA(FORMAT_MESSAGE_FROM_SYSTEM |
FORMAT_MESSAGE_IGNORE_INSERTS |
FORMAT_MESSAGE_ALLOCATE_BUFFER,
NULL, errcode, 0, (char *)&msg, 0, NULL))
chomp (msg); /* because message has trailing newline */
else {
size_t len = 50;
/* use LocalAlloc because FormatMessage does */
msg = LocalAlloc(LMEM_FIXED, len);
if (!msg) {
msg = (char *)"LocalAlloc failed during Winsock error";
goto done;
}
evutil_snprintf(msg, len, "winsock error 0x%08x", errcode);
}
newerr = (struct cached_sock_errs_entry *)
mm_malloc(sizeof (struct cached_sock_errs_entry));
if (!newerr) {
LocalFree(msg);
msg = (char *)"malloc failed during Winsock error";
goto done;
}
newerr->code = errcode;
newerr->msg = msg;
HT_INSERT(cached_sock_errs_map, &windows_socket_errors, newerr);
done:
EVLOCK_UNLOCK(windows_socket_errors_lock_, 0);
return msg;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
//Linux 上就比较简单了,直接调用strerror函数传入错误码即可
#define evutil_socket_error_to_string(errcode) (strerror(errcode))
2
libevent 提供了这些宏访问和操作 socket 错误代码,evutil_socket_geterror() 则返回某特定套接字的错误号,在类 Unix 系统中 EVUTIL_SET_SOCKET_ERROR() 修改当前套接字错误号,evutil_socket_error_to_string() 返回代表某给定套接字错误号的字符串。
# 4.3.5 套接字函数返回值
无论是 Windows 还是 Linux,大多数 socket 函数调用失败后会返回 -1,在 Windows 系统上为这种情形专门定义了一个宏 SOCKET_ERROR。
#define SOCKET_ERROR (-1)
可以为 Linux 上也定义这样一个宏,这样就方便统一书写了:
#ifndef WIN32
#define SOCKET_ERROR (-1)
#endif
2
3
# 4.3.6 select 函数第一个参数问题
select 函数的原型是:
int select(int nfds,
fd_set *readfds,
fd_set *writefds,
fd_set *exceptfds,
struct timeval *timeout);
2
3
4
5
使用示例:
fd_set writeset;
FD_ZERO(&writeset);
FD_SET(m_hSocket, &writeset);
struct timeval tv;
tv.tv_sec = 3;
tv.tv_usec = 100;
select(m_hSocket + 1, NULL, &writeset, NULL, &tv);
2
3
4
5
6
7
无论是 Windows 还是 Linux,这个函数的 readfds、writefds 和 exceptfds 参数都是一个包含一组 socket 描述符数组的结构体。在 Linux 下,第一个参数必须设置成这三个参数中,所有 socket 描述符句柄中的最大值加 1;但是 Windows 的 select 函数不使用第一个参数,可以随意设置,纯粹是为了保持与 Berkeley 套接字兼容才保留了这个参数。一般为了兼容,在 Windows 上也会将这个参数的值设置为这三个 fd_set 集合中最大套接字值加 1。
# 4.3.7 关于错误码 WSAEWOULDBLOCK 和 EWOULDBLOCK
对于某些套接字函数操作不能立即完成,Windows 上的错误码 WSAEWOULDBLOCK, 在 Linux 上对应 EWOULDBLOCK(Linux 还存在一个 EAGAIN 错误码与此同义)。
为了统一写法,我们可以在 Windows 上使用如下代码来兼容:
#ifdef WIN32
#define EWOULDBLOCK WSAEWOULDBLOCK
#endif
2
3
其他套接字函数
对于 IO 复用函数,Windows 和 Linux 都支持 select 函数,此外 Linux 有特有的 poll、epoll 模型,Windows 有 WSAPoll 函数和完成端口模型(IOCP)。
另外,Windows 和 Linux 上有许多自己特有的函数和网络通信模型,Windows 自我扩展的套接字函数一般以 WSA 开头(Windows Socket API 的缩写),例如 WSASend、WSARecv等等,Windows 上提供了方便的 WSAEventSelect 模式和 WSAAsyncSelect 等网络通信模型。Linux 上有 accept4、socketpair 等方便的函数。总之,二者的发展也是相互借鉴,无所谓孰优孰劣。
此小节对 Windows 和 Linux 套接字函数的一些重要区别和特性做了一些归纳性总结,后面章节会针对性地对相关内容进行介绍。