在阅读之前,我认为你已经掌握了Unix系统上的非阻塞的Socket I/O。
同样的,在Windows系统上也能够找到select这个系统调用。但是,select 在文件描述上实现的是一个O(n)的算法,他并不像现在常用的实时多路复用的 epoll,这也让使 select在高并发服务器上没了用武之地。 接下来,我们将讲述的是Windows下的高并发服务器的设计.
除了epoll或者kqueue, Windows也有自己的多路复用I/O,叫做/O completion ports(IOCPs). IOCPs采用轮寻overlapped I/O方式。并且 IOCP 的消耗时间是一个常数 (REF?).
最根本的变化是,在Unix下,你一般要求内核等待一个文件描述符的可读性或writablity状态变化。使用overlapped I/O和IOCPs,我们将会等待异步方法调用的完成。举例来说,我们不是要等待一个socket的状态成为可写,然后用send(2)就可以了, 就像我们在 Unix 常做的一样, 使用overlapped I/O你应该使用 WSASend()发送消息,并且等待消息发送完全。
Unix非堵塞I/O不是完美的,在Unix系统中的原则抽象是将许多事情的处理统一看做对文件的操作(或者更准确的说是文件描述符)。write(2) 、read(2) 和close(2)在TCP协议下工作就像它们对常规文件的操作。同步操作工作在类似的不同文件描述符,但是一旦对性能的需求驱动你去使用O_NONBLOCK变量类型能起到完全不同的作用,甚至对于最基本的操作。尤其,常规系统文件不支持非阻塞操作。(令人不安的是没有人提及这个相当重要的事实)例如, 当它在做非阻塞读操作时,尽管安全的,一个人不可能在一个常规文件FD中轮询是否是可读的。常规文件总是可读的,并且read(2)调用总是有可能阻塞一个调用线程在一个未知的时间里。
POSIX 对于一些操作已经定义了 异步接口,但很多实现这些操作的 Unix,其情形并不明确。在 Linux 上,aio_* 例程使用 pthread,实现于 GNU libc 的用户态。io_submit(2) 没有 GNU libc 的封装,这被报告为非常缓慢,有可能阻塞。 Solaris 拥有真正的内核 AIO,但不清楚,与磁盘 I/O 相比,Socket I/O 的性能特性是什么。现代高性能 Unix socket 程序通过 I/O 复用器来使用非阻塞的文件描述符,而非 POSIX 的 AIO。异步访问磁盘的通常做法依然是通过使用定制的用户态线程池来完成,而非 POSIX 的AIO。
Windows 的 IOCP 不同时支持 socket 和 普通文件 I/O,后者可以极大的简化磁盘操作。 例如,ReadFileEx() 对两者都支持。第一个例子,我们先来看看 ReadFile() 是如何工作的。
typedef void* HANDLE; BOOL ReadFile(HANDLE file, void* buffer, DWORD numberOfBytesToRead, DWORD* numberOfBytesRead, OVERLAPPED* overlapped);
这个函数执行读取时可以用同步或异步两种方式。同步操作是通过返回0和 WSAGetLastError()返回 WSA_IO_PENDING 来表示的。当 ReadFile() 异步运行时,用户提供的 OVERLAPPED* 是一个不完全操作的句柄。
typedef struct { unsigned long* Internal; unsigned long* InternalHigh; union { struct { WORD Offset; WORD OffsetHigh; }; void* Pointer; }; HANDLE hEvent; } OVERLAPPED;
要调查这些函数的完成情况,可以使用IOCP,overlapped->hEvent, 和 GetQueuedCompletionStatus()。
简单的TCP连接例子
为了展示GetQueuedCompletionStatus()的使用,提出了一个本地连接到端口8000的例子
char* buffer[200]; WSABUF b = { buffer, 200 }; size_t bytes_recvd; int r, total_events; OVERLAPPED overlapped; HANDLE port; port = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, NULL, 0); if (!port) { goto error; } r = WSARecv(socket, &b, 1, &bytes_recvd, NULL, &overlapped, NULL); CreateIoCompletionPort(port, &overlapped.hEvent, if (r == 0) { if (WSAGetLastError() == WSA_IO_PENDING) { /* Asynchronous */ GetQueuedCompletionStatus() if (r == WAIT_TIMEOUT) { printf("Timeout "); } else { } } else { /* Error */ printf("Error %d ", WSAGetLastError()); } } else { /* Synchronous */ printf("read %ld bytes from socket ", bytes_recvd); }
先前工作
充分的跨越Unix和Window系统之上写代码是非常困难的, 这要求一个人去理解错综复杂的API和不同系统的非文档化细节。目前已有多个项目试图去提供一个抽象层,但是在作者的观念中,没有人能完全满意这种形式。
Marc Lehmann 的 libev 与 libeio. libev 是 UNIX I/O 多路复用的最小完美抽象。包括了一些有用的工具函数,如 ev_async,它用于异步通告,但主构件是 ev_io,用于通知用户文件描述符的状态。如前所述,一般通常不可能获得普通文件的状态变化 — 并且即使是 write(2) 和 read(2) 调用,也无法保证它们不阻塞。因此,libeio 被开发出来,用于在可管理的线程池中进行各种磁盘相关的系统调用。不幸的是,libev 做为目标的抽象层并不适合 IOCP — libev 的工作严重依赖于文件描述符,没有 socket 的概念。而且,Unix 的用户可能会将 libeio 用于文件 I/O,但移植到 Windows 上是不理想的。在 Windows 上,libev 当前使用的是 select()—每个线程不超过 64 个文件描述符。
libevent. 比 libev 更庞大,包含了 RPC,DNS,和 HTTP 代码。不支持文件 I/O。libev 在 Lehmann 评估 libevent 并加以拒绝之后开发了它 — 他的理由读起来很有趣。 关键性重写 在版本 2 完成,从而支持 Windows IOCP, 但大量的实例研究显示,它还没有正确的工作。
Boost ASIO. 它基本上满足你在 Windows 和 Unix 下使用 socket 的需要,即,Linux 的 epoll,Macintosh 的 kqueue,Windows 的 IOCP。它不支持文件 I/O。在笔者看来,对于一个并非十分复杂的问题,它过于庞大了(大约 300 个文件,大约 12000 个分号)。
几乎每一个你所熟悉socket操作都有一个异步的对应部分。下面的条目试着就Windows异步I/O系统调用与Unix的非阻塞配个对。
TCP Sockets是到目前为止最重要的保证正确性的流。服务器被期待去处理成千上万个这样同时发生的每一个线程。即使考虑避免类似Unix主义的文件描述符,在Windows下这种异步I/O也是可能的。(Windows有一个打开的文件描述符不能超过2048的硬限制——具体见 _setmaxstdio()。)
? send(2), write(2)
? Windows: WSASend() , WriteFileEx()
? recv(2), read(2)
? Windows: WSARecv() , ReadFileEx()
? connect(2)
? Windows: ConnectEx()和非阻塞 connect() 在Unix下有不同的含义。适当的连接一个远程主机方式是调用connect(2),当它返回 EINPROGRESS 时,轮询文件描述符是否可写。这个时候使用
int error; socklen_t len = sizeof(int); getsockopt(fd, SOL_SOCKET, SO_ERROR, &error, &len);
一个值为0的 error 变量来告诉我们连接成功。(文档在Linux man page的 connect(2) 的EINPROGRESS错误码中。)
? accept(2)
? Windows: AcceptEx()
? sendfile(2)
? Windows: TransmitFile() 在Unix中对应的确切API是sendfile(2),然而它却没有被同意。每一种操作系统都有些许的不同。Allsendfile(2) 的实现(可能出了FreeBSD吧?)是甚至是在非阻塞的socket上实现的阻塞。Marc Lehmann已经写了 一个libeio可移植性版本库。
? Linux sendfile(2)
? FreeBSD sendfile(2)
? Darwin sendfile(2)
? shutdown(2),优雅的关闭, 半双工连接
? 优雅的 Shutdown, Linger 选项组, 和Socket关闭 DisconnectEx()
? close(2)
? closesocket()
? 下面的条目中在Windows异步和Unix非阻塞几乎是一样的。唯一的不同是Unix中的文件描述符是一个整型,在Windows中使用的是SOCKET类型。
? sockaddr
? bind()
? getsockname()
Window已有的命名管道,它或多或少的和AF_Unix domain sockets相同
AF_Unix
存在于文件系统中的socket经常看起来像
/tmp/pipename
window的命名管道有一个路径,但是它们并不是文件系统的直接部分;而是
.pipepipename
socket(AF_Unix, SOCK_STREAM, 0), bind(2), listen(2)
CreateNamedPipe() Use FILE_FLAG_OVERLAPPED, PIPE_TYPE_BYTE,PIPE_NOWAIT.
recv(2), read(2)
connect(2)
accept(2)
例子:
在Unix文件系统中文件不能使用非阻塞I/O。有一些操作系统有异步I/O,但是它不是标准的,至少在Linux系统需要GNU libc的pthreads。为了此应用设计在不同的Unix下是方便的, 必须管理一个线程池为分配文件的I/O系统调用。
在window系统中的较好情况是真正重叠的I/O是可用的,当读或者写一个数据流到文件中。
write(2)
Windows:WriteFileEx()
Solaris系统的事件完成端口实现了真正的内核级异步写操作aio_write(3RT)
read(2)
Windows:ReadFileEx()
Solaris系统的事件完成端口实现了真正的内核级异步读操作aio_read(3RT)
它 (是不是常常?) 可能想一个TCP套接字那样轮询一个Unix TTY 文件描述器来获取可读和可写属性—这非常有益和美好. 在windows中这种境地是非常糟糕的,它不仅仅是一个完全不同的API,还要有读取和写入TTY的不重叠版本. 对可读属性进行轮询可以通过等待在另外一个线程中使用RegisterWaitForSingleObject()来完成.
read(2)
ReadConsole()和ReadConsoleInput()不支持重叠的I/O,并且没有重叠的计数部分. 解决这个问题的一种策略是
RegisterWaitForSingleObject(&tty_wait_handle, tty_handle, tty_want_poll, NULL, INFINITE, WT_EXECUTEINWAITTHREAD | WT_EXECUTEONLYONCE)
其将会在一个不同的线程中执行 tty_want_poll() . 你可以使用这个东西去通知被调用的线程ReadConsoleInput() 将不会阻塞.
write(2)
WriteConsole()也是阻塞的,但这可能是可以被接受的.
小贴士
重叠 = 无阻塞.
不存在重叠的 GetAddrInfoEx() 函数. 这似乎就以为这异步过程调用必须被使用.
IOCP:
CancelIoEx() — 取消一个重叠的操作.
TransmitFile()— windows的一个异步sendfile()
WSADuplicateSocket()— 描述了如何在两个进程之间共享一个socket.
_setmaxstdio()— 某些想设置文件描述符的最大计数以及setrlimit(3)AKA 不设限的 -n. 注意windows上的文件描述符限制是2048.
APC:
DNSQuery()— 通用的DNS查询函数,类似于Unix上的res_query().
管道:
CallNamedPipe— 像Unix管道的accept
WaitForMultipleObjectsEx
可以被解读为 "等待多个对象性". 同样很有用:
用户的 Visual C++ 介绍
119页中有趣的细节.
2KB项目(www.2kb.com,源码交易平台),提供担保交易、源码交易、虚拟商品、在家创业、在线创业、任务交易、网站设计、软件设计、网络兼职、站长交易、域名交易、链接买卖、网站交易、广告买卖、站长培训、建站美工等服务