在閱讀之前,我認為你已經掌握了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\n");
} else {
}
} else {
/* Error */
printf("Error %d\n", WSAGetLastError());
}
} else {
/* Synchronous */
printf("read %ld bytes from socket\n", 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 個分號)。
Window已有的命名管道,它或多或少的和AF_Unix domain sockets相同
AF_Unix
存在於文件系統中的socket經常看起來像
view source print?1
/tmp/pipename
window的命名管道有一個路徑,但是它們並不是文件系統的直接部分;而是
view source print?1
\\.\pipe\pipename
socket(AF_Unix, SOCK_STREAM, 0), bind(2), listen(2)
CreateNamedPipe() Use FILE_FLAG_OVERLAPPED, PIPE_TYPE_BYTE,PIPE_NOWAIT.
WriteFileEx()
recv(2), read(2)
ReadFileEx()
connect(2)
CreateNamedPipe()
accept(2)
ConnectNamedPipe()
例子:
Named Pipe Server Using Completion Routines
Named Pipe Server Using Overlapped I/O
在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)