找回密码
 立即注册→加入我们

QQ登录

只需一步,快速开始

搜索
热搜: 下载 VB C 实现 编写
查看: 569|回复: 4

【Socket】 关于WinSocket是否为文件描述符测试

[复制链接]
发表于 2024-2-6 17:28:39 | 显示全部楼层 |阅读模式

欢迎访问技术宅的结界,请注册或者登录吧。

您需要 登录 才可以下载或查看,没有账号?立即注册→加入我们

×
本帖最后由 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的版本:

  1. #ifdef _WIN32
  2. typedef ptrdiff_t ssize_t;

  3. // 转换 Win32 LastError 为 CRT errno
  4. errno_t _errno_cvt_win2crt(DWORD winerr);        // 这个是Windows错误转CRT错误码(不提供实现,请自行封装)

  5. // 把 Win32 LastError 转换并设置到 CRT errno
  6. static int _set_winerrno(int err)
  7. {
  8.         err = _errno_cvt_win2crt(err);
  9. #ifdef _MSC_VER
  10.         __if_exists(_set_errno) {
  11.                 // 如果存在 _set_errno 标识符,说明是新版 MSVCRT,最好是调用 _set_errno 函数来设置。
  12.                 _set_errno(err);
  13.         }
  14.         __if_not_exists(_set_errno) {
  15.                 // 如果不存在 _set_errno 标识符,说明是旧版 MSVCRT,直接对 errno 赋值就行了。
  16.                 errno = (err);
  17.         }
  18. #else
  19.         errno = (err);
  20. #endif
  21.         return -1;
  22. }

  23. // 从文件描述符读取数据
  24. ssize_t read(int fd, void *buf, size_t count)
  25. {
  26.         DWORD dwret;
  27.         OVERLAPPED ol = {};
  28.         // 使用重叠模式读取(注意这一步非常关键,否则无法成功读取 socket 数据)
  29.         BOOL issuc = ReadFile(HANDLE(fd), buf, DWORD(count), &dwret, &ol);
  30.         // 如果是非重叠模式对象会发生阻塞直到读取成功或失败,直接返回长度即可(注意:socket 对象只有用 WSASocket 才能创建非重叠 socket 对象)
  31.         if (issuc) return ssize_t(dwret);
  32.         // 获取错误码,如果不是处理重叠读取状态中,就说明是真的发生了错误。
  33.         dwret = GetLastError();
  34.         if (ERROR_IO_PENDING != dwret) return _set_winerrno(dwret);
  35.         // 等待重叠读取完成并获取结果(模拟成非重叠的阻塞读取模式)
  36.         issuc = GetOverlappedResult(HANDLE(fd), &ol, &dwret, TRUE);
  37.         // 如果获取成功就返回值读取长度
  38.         if (issuc) return ssize_t(dwret);
  39.         // 否则需将 Win32 LastError 转换成 CRT errno
  40.         dwret = GetLastError();
  41.         return _set_winerrno(dwret);
  42. }

  43. // 写入数据到文件描述符
  44. ssize_t write(int fd, const void *buf, size_t count)
  45. {
  46.         DWORD dwret;
  47.         OVERLAPPED ol = {};
  48.         // 使用重叠模式写入(注意这一步非常关键,否则无法成功写入 socket 数据)
  49.         BOOL issuc = WriteFile(HANDLE(fd), buf, DWORD(count), &dwret, &ol);
  50.         // 如果是非重叠模式对象会发生阻塞直到写入成功或失败,直接返回长度即可(注意:socket 对象只有用 WSASocket 才能创建非重叠 socket 对象)
  51.         if (issuc) return ssize_t(dwret);
  52.         // 获取错误码,如果不是处理重叠写入状态中,就说明是真的发生了错误。
  53.         dwret = GetLastError();
  54.         if (ERROR_IO_PENDING != dwret) return _set_winerrno(dwret);
  55.         // 等待重叠写入完成并获取结果(模拟成非重叠的阻塞写入模式)
  56.         issuc = GetOverlappedResult(HANDLE(fd), &ol, &dwret, TRUE);
  57.         // 如果获取成功就返回值写入长度
  58.         if (issuc) return ssize_t(dwret);
  59.         // 否则需将 Win32 LastError 转换成 CRT errno
  60.         dwret = GetLastError();
  61.         return _set_winerrno(dwret);
  62. }
  63. #endif
复制代码

  测试使用read和write来收发socket消息的代码:

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #ifdef _WIN32
  4. #define WIN32_LEAN_AND_MEAN
  5. #include <Windows.h>
  6. #include <WinSock2.h>
  7. #pragma comment(lib, "ws2_32")
  8. #else
  9. #include <sys/types.h>
  10. #include <sys/socket.h>
  11. // 让 Linux 和 Windows 通用的别名
  12. typedef struct sockaddr SOCKADDR, *PSOCKADDR;
  13. typedef struct sockaddr_in SOCKADDR_IN, *PSOCKADDR_IN;
  14. #define closesocket close
  15. #endif

  16. //#define TEST 0
  17. int main()
  18. {
  19.         int ret;
  20. #ifdef _WIN32
  21.         // Win Socket 初始化
  22.         WSADATA wsad;
  23.         ret = WSAStartup(MAKEWORD(2, 2), &wsad);
  24. #endif
  25.         // 创建 socket 对象(这里用本机UDP测试)
  26. #if !defined(TEST) || !defined(_WIN32)
  27.         auto sfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
  28.         //auto sfd2 = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
  29. #elif TEST == 1
  30.         auto sfd = WSASocket(AF_INET, SOCK_DGRAM, IPPROTO_UDP, nullptr, 0, WSA_FLAG_OVERLAPPED | WSA_FLAG_NO_HANDLE_INHERIT);
  31. #else
  32.         auto sfd = WSASocket(AF_INET, SOCK_DGRAM, IPPROTO_UDP, nullptr, 0, WSA_FLAG_NO_HANDLE_INHERIT);
  33. #endif

  34.         // 设定 IPv4 127.0.0.1:21930
  35.         SOCKADDR_IN si = { AF_INET, 0xAA55, };
  36.         si.sin_addr.s_addr = 0x0100007F;
  37.         // 让 UDP 对象同时绑定和连接同一个IP端口(实现自发自收)
  38.         ret = bind(sfd, PSOCKADDR(&si), sizeof(si));
  39.         ret = connect(sfd, PSOCKADDR(&si), sizeof(si));

  40.         // 发送消息(write 等于 send 的最后一个参数传0)
  41.         static const char send_data[] = "HelloWorld";
  42.         //ret = send(sfd, send_data, sizeof(send_data), 0);
  43.         ret = write(sfd, send_data, sizeof(send_data));

  44.         // 接收消息(read 等于 recv 的最后一个参数传0)
  45.         char recv_data[0x10];
  46.         //ret = recv(sfd, recv_data, sizeof(recv_data), 0);
  47.         ret = read(sfd, recv_data, sizeof(recv_data));

  48.         // 关闭 socket 句柄
  49.         ret = closesocket(sfd);
  50. #ifdef _WIN32
  51.         // Win Socket 清理
  52.         ret = WSACleanup();
  53. #endif
  54.         // 输出结果
  55.         printf("%s\n", recv_data);
  56.         return EXIT_SUCCESS;
  57. }
复制代码
回复

使用道具 举报

发表于 2024-2-6 23:27:02 | 显示全部楼层
在WINDOWS上,只要有OPEN/CREATE---USE---CLOSE流程的,肯定有句柄(不管名字叫啥)。但至于具体的句柄类型,就要用ARK查看了。

但有些名字里有“句柄”的反而不一定是,比如“窗口句柄”。
回复 赞! 靠!

使用道具 举报

 楼主| 发表于 2024-2-7 10:01:49 | 显示全部楼层
美俪女神 发表于 2024-2-6 23:27
在WINDOWS上,只要有OPEN/CREATE---USE---CLOSE流程的,肯定有句柄(不管名字叫啥)。但至于具体的句柄类型 ...

窗口句柄也是句柄,Windows的句柄一共分为三大类:内核对象句柄、用户对象句柄、GDI对象句柄。像文件句柄、Socket句柄、事件句柄、IOCP句柄这些都是内核对象句柄,HWND、HMONITOR、HICON、HCURSOR、HMENU、HACCEL都是用户对象句柄,HDC、HBITMAP、HMETAFILE、HPEN、HBRUSH、HPALETTE、HFONT、HRGN都是GDI对象句柄。这三大类句柄都是分别显示在任务管理器的三个不同列上的,其中用户对象句柄和GDI对象句柄存在最多一万个的上限,而内核对象句柄则没有。不过Windows确实还有一些不是句柄的句柄,比如:FindFirstFile返回的句柄(好像是指向保存当前查找状态数据的指针)、WSAAsyncGet返回的句柄(实际上只是个纯编号)、HINSTANCE(实际上的PE模块的内存基址)。
至于研究Windows的Socket对象是不是文件描述符这类句柄,最开始是因为看到A5大婶说到Windows的Socket对象和Linux的不一样,Linux的可以用read和write收发消息(因此它是文件描述符),Windows的不可以用ReadFile和WriteFile不可以(因此它不是),但我清晰记得MSDN曾经说过ReadFile和WriteFile是可以用于socket的对象的重叠IO操作的,但现在再去看MSDN结果那句话已经被巨硬删掉了。所有便做了个测试来验证,最后验证结果确实是可以用ReadFile和WriteFile来收发网络消息(只不过socket默认是重叠IO模式)。
回复 赞! 靠!

使用道具 举报

发表于 2024-2-11 00:01:05 | 显示全部楼层
本帖最后由 lichao 于 2024-2-11 00:04 编辑

一切皆文件,包括目录,管道,fifo,设备。这点Linux也一样
socket读写有send/recv/sendto/recvfrom/read/write/sengmsg/recvmsg
回复 赞! 靠!

使用道具 举报

本版积分规则

QQ|Archiver|小黑屋|技术宅的结界 ( 滇ICP备16008837号 )|网站地图

GMT+8, 2024-11-21 17:45 , Processed in 0.037982 second(s), 29 queries , Gzip On.

Powered by Discuz! X3.5

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表