- UID
- 4293
- 精华
- 积分
- 11130
- 威望
- 点
- 宅币
- 个
- 贡献
- 次
- 宅之契约
- 份
- 最后登录
- 1970-1-1
- 在线时间
- 小时
|
本帖最后由 YY菌 于 2024-6-12 09:14 编辑
关于Socket在Windows下是否文件描述符的说法由来已久,有些人认为它不是(理由是不能用CreateFile创建、ReadFile接收消息、WriteFile发送数据、CloseHandle关闭句柄),有些人认为它是(理由是可以绑定到IOCP,可以使用WaitFor系列API等待),那到底是不是呢?我们需要经过比较严格的测试之后再得出结论。
首先测试socket函数的实现逻辑,经过反汇编单步调试发现,经过了一大堆“无关紧要”操作之后,最终走到NtCreateFile创建了一个名为"\\Device\\Afd\\Endpoint"的文件对象,最后返回给我们的SOCKET句柄也是这个NtCreateFile创建的文件对象句柄。接下来单步测试closesocket函数,发现这个SOCKET句柄最终走到NtClose上关闭的。既然WinSockRuntime内部都是使用NtCreateFile创建和NtClose关闭的,那为什么不能自己CreateFile创建和CloseHandle关闭呢?这个问题的关键就在于我们看似“无关紧要”的那些操作上,因为WinSockRuntime内部会记录一些额外的附加数据来和SOCKET句柄关联(大概是类似哈希表的结构,SOCKET句柄为Key,附加数据为Value)。如果我们使用CreateFile来创建"\\Device\\Afd\\Endpoint"的话,WinSockRuntime是不能识别的,而用CloseHandle关闭SOCKET句柄虽然调用结果是成功的,但WinSockRuntime的附加数据并没有得到相应的释放从而发生内存泄漏,最后在WSACleanup时引发0xC0000008异常。
至于send和recv的内部实现是通过NtDeviceIoControlFile来调用内核中Afd驱动的功能来实现的,由此可见这两个函数是不走ReadFile和WriteFile(或NtReadFile和NtWriteFile)的,当我们自己调用ReadFile和WriteFile来收发消息得到的结果是调用失败,于是很多人因此得出结论SOCKET对象在Windows下不是文件描述符,不能使用ReadFile和WriteFile来像Linux那样收发消息。然而,事实上真的是这样的吗?接下来我们再试试更高级版本的WSASocket函数来创建SOCKET对象(比socket函数多出来的参数全部传0或NULL),这时候再来使用ReadFile和WriteFile来收发消息试试,居然成功了,所以说Windows的SOCKET对象其实也是和Linux一样的文件描述符。可是socket函数创建的句柄为什么不行呢?再反汇编单步调试一下发现,原来socket函数也是调用的WSASocket函数,但它的最后一个参数dwFlags传的是1,1对应的常量则是WSA_FLAG_OVERLAPPED(重叠模式),而我们习惯的ReadFile和WriteFile用法都是非重叠阻塞调用方式,所以这就是我们使用ReadFile和WriteFile来收发SOCKET消息失败的重要原因。
接下来,我们分别测试非重叠SOCKET、重叠SOCKET、非重叠IO操作、重叠IO操作的组合操作结果:
| 非重叠SOCKET | 重叠SOCKET | 非重叠IO操作 | 成功:通过阻塞等待IO完成 | 失败:GetLastError结果为87(无效参数) | 重叠IO操作 | 成功:忽略重叠IO参数并通过阻塞等待IO完成 | 成功:函数立即返回,GetLastError为997,可通过GetOverlappedResult或其它异步方式来等待结果。 |
根据以上测试结果,我分别使用ReadFile和WriteFile来封装了模拟Linux的read和write的版本:
- #ifdef _WIN32
- typedef ptrdiff_t ssize_t;
- // 转换 Win32 LastError 为 CRT errno
- errno_t _errno_cvt_win2crt(DWORD winerr); // 这个是Windows错误转CRT错误码(不提供实现,请自行封装)
- // 把 Win32 LastError 转换并设置到 CRT errno
- static int _set_winerrno(int err)
- {
- err = _errno_cvt_win2crt(err);
- #ifdef _MSC_VER
- __if_exists(_set_errno) {
- // 如果存在 _set_errno 标识符,说明是新版 MSVCRT,最好是调用 _set_errno 函数来设置。
- _set_errno(err);
- }
- __if_not_exists(_set_errno) {
- // 如果不存在 _set_errno 标识符,说明是旧版 MSVCRT,直接对 errno 赋值就行了。
- errno = (err);
- }
- #else
- errno = (err);
- #endif
- return -1;
- }
- // 从文件描述符读取数据
- ssize_t read(int fd, void *buf, size_t count)
- {
- DWORD dwret;
- OVERLAPPED ol = {};
- // 使用重叠模式读取(注意这一步非常关键,否则无法成功读取 socket 数据)
- BOOL issuc = ReadFile(HANDLE(fd), buf, DWORD(count), &dwret, &ol);
- // 如果是非重叠模式对象会发生阻塞直到读取成功或失败,直接返回长度即可(注意:socket 对象只有用 WSASocket 才能创建非重叠 socket 对象)
- if (issuc) return ssize_t(dwret);
- // 获取错误码,如果不是处理重叠读取状态中,就说明是真的发生了错误。
- dwret = GetLastError();
- if (ERROR_IO_PENDING != dwret) return _set_winerrno(dwret);
- // 等待重叠读取完成并获取结果(模拟成非重叠的阻塞读取模式)
- issuc = GetOverlappedResult(HANDLE(fd), &ol, &dwret, TRUE);
- // 如果获取成功就返回值读取长度
- if (issuc) return ssize_t(dwret);
- // 否则需将 Win32 LastError 转换成 CRT errno
- dwret = GetLastError();
- return _set_winerrno(dwret);
- }
- // 写入数据到文件描述符
- ssize_t write(int fd, const void *buf, size_t count)
- {
- DWORD dwret;
- OVERLAPPED ol = {};
- // 使用重叠模式写入(注意这一步非常关键,否则无法成功写入 socket 数据)
- BOOL issuc = WriteFile(HANDLE(fd), buf, DWORD(count), &dwret, &ol);
- // 如果是非重叠模式对象会发生阻塞直到写入成功或失败,直接返回长度即可(注意:socket 对象只有用 WSASocket 才能创建非重叠 socket 对象)
- if (issuc) return ssize_t(dwret);
- // 获取错误码,如果不是处理重叠写入状态中,就说明是真的发生了错误。
- dwret = GetLastError();
- if (ERROR_IO_PENDING != dwret) return _set_winerrno(dwret);
- // 等待重叠写入完成并获取结果(模拟成非重叠的阻塞写入模式)
- issuc = GetOverlappedResult(HANDLE(fd), &ol, &dwret, TRUE);
- // 如果获取成功就返回值写入长度
- if (issuc) return ssize_t(dwret);
- // 否则需将 Win32 LastError 转换成 CRT errno
- dwret = GetLastError();
- return _set_winerrno(dwret);
- }
- #endif
复制代码
测试使用read和write来收发socket消息的代码:
- #include <stdio.h>
- #include <stdlib.h>
- #ifdef _WIN32
- #define WIN32_LEAN_AND_MEAN
- #include <Windows.h>
- #include <WinSock2.h>
- #pragma comment(lib, "ws2_32")
- #else
- #include <sys/types.h>
- #include <sys/socket.h>
- // 让 Linux 和 Windows 通用的别名
- typedef struct sockaddr SOCKADDR, *PSOCKADDR;
- typedef struct sockaddr_in SOCKADDR_IN, *PSOCKADDR_IN;
- #define closesocket close
- #endif
- //#define TEST 0
- int main()
- {
- int ret;
- #ifdef _WIN32
- // Win Socket 初始化
- WSADATA wsad;
- ret = WSAStartup(MAKEWORD(2, 2), &wsad);
- #endif
- // 创建 socket 对象(这里用本机UDP测试)
- #if !defined(TEST) || !defined(_WIN32)
- auto sfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
- //auto sfd2 = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
- #elif TEST == 1
- auto sfd = WSASocket(AF_INET, SOCK_DGRAM, IPPROTO_UDP, nullptr, 0, WSA_FLAG_OVERLAPPED | WSA_FLAG_NO_HANDLE_INHERIT);
- #else
- auto sfd = WSASocket(AF_INET, SOCK_DGRAM, IPPROTO_UDP, nullptr, 0, WSA_FLAG_NO_HANDLE_INHERIT);
- #endif
- // 设定 IPv4 127.0.0.1:21930
- SOCKADDR_IN si = { AF_INET, 0xAA55, };
- si.sin_addr.s_addr = 0x0100007F;
- // 让 UDP 对象同时绑定和连接同一个IP端口(实现自发自收)
- ret = bind(sfd, PSOCKADDR(&si), sizeof(si));
- ret = connect(sfd, PSOCKADDR(&si), sizeof(si));
- // 发送消息(write 等于 send 的最后一个参数传0)
- static const char send_data[] = "HelloWorld";
- //ret = send(sfd, send_data, sizeof(send_data), 0);
- ret = write(sfd, send_data, sizeof(send_data));
- // 接收消息(read 等于 recv 的最后一个参数传0)
- char recv_data[0x10];
- //ret = recv(sfd, recv_data, sizeof(recv_data), 0);
- ret = read(sfd, recv_data, sizeof(recv_data));
- // 关闭 socket 句柄
- ret = closesocket(sfd);
- #ifdef _WIN32
- // Win Socket 清理
- ret = WSACleanup();
- #endif
- // 输出结果
- printf("%s\n", recv_data);
- return EXIT_SUCCESS;
- }
复制代码 |
|