代码改变世界

很幽默的讲解六种Socket I/O模型

2011-12-13 11:57  一人游走  阅读(236)  评论(0)    收藏  举报

本文简单介绍了当前 Windows 支持的各种 Socket I/O 模型,如果你发现其中存在什么错误请务必赐教。

一: select 模型
二: WSAAsyncSelect 模型
三: WSAEventSelect 模型
四: Overlapped I/O 事件通知模型
五: Overlapped I/O 完成例程模型
六: IOCP 模型

老陈有一个在外地工作的女儿,不能经常回来,老陈和她通过信件联系。他们的信会被邮递员投递到他们的信箱里。
这和 Socket 模型非常类似。下面我就以老陈接收信件为例讲解 Socket I/O 模型 ~~~

一: select 模型

老陈非常想看到女儿的信。以至于他每隔 10 分钟就下楼检查信箱,看是否有女儿的信 ~~~~~
在这种情况下, 下楼检查信箱然后回到楼上耽误了老陈太多的时间,以至于老陈无法做其他工作。
select 模型和老陈的这种情况非常相似:周而复始地去检查 ...... 如果有数据 ...... 接收 / 发送 .......

使用线程来 select 应该是通用的做法:

 

procedure TListenThread.Execute;
var
addr: TSockAddrIn;
fd_read: TFDSet;
timeout: TTimeVal;
ASock, MainSock: TSocket;
len, i: Integer;
begin
MainSock := socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
addr.sin_family: = AF_INET;
addr.sin_port: = htons(5678);
addr.sin_addr.S_addr: = htonl(INADDR_ANY);
bind(MainSock, @addr, sizeof(addr));
listen(MainSock, 5);

while (not Terminated) do
begin
FD_ZERO(fd_read);
FD_SET(MainSock, fd_read);
timeout.tv_sec: = 0;
timeout.tv_usec: = 500;
if select(0, @fd_read, nil, nil, @timeout) > 0 then // 至少有1个等待Accept的connection
begin
if FD_ISSET(MainSock, fd_read) then
begin
for i: = 0 to fd_read.fd_count - 1 do // 注意,fd_count <= 64 ,也就是说select只能同时管理最多64个连接
begin
len := sizeof(addr);
ASock := accept(MainSock, addr, len);
if ASock <> INVALID_SOCKET then
.. .. // 为ASock创建一个新的线程,在新的线程中再不停地select
end;
end;
end;
end; // while ( not self.Terminated)

shutdown(MainSock, SD_BOTH);
closesocket(MainSock);
end;

 

 

二: WSAAsyncSelect 模型

后来,老陈使用了微软公司的新式信箱。这种信箱非常先进,一旦信箱里有新的信件,盖茨就会给老陈打电话:喂,大爷,你有新的信件了!从此,老陈再也不必频繁上下楼检查信箱了,牙也不疼了,你瞅准了,蓝天 ...... 不是,微软 ~~~~~~~~
微软提供的 WSAAsyncSelect 模型就是这个意思。

WSAAsyncSelect 模型是 Windows 下最简单易用的一种 Socket I/O 模型。使用这种模型时, Windows 会把网络事件以消息的形势通知应用程序。
首先定义一个消息标示常量:
const WM_SOCKET = WM_USER + 55;
再在主 Form 的 private 域添加一个处理此消息的函数声明:
private
procedure WMSocket(var Msg: TMessage); message WM_SOCKET;
然后就可以使用 WSAAsyncSelect 了:

var
addr: TSockAddr;
sock: TSocket;

sock := socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
addr.sin_family := AF_INET;
addr.sin_port := htons(5678);
addr.sin_addr.S_addr := htonl(INADDR_ANY);
bind(m_sock, @addr, sizeof(SOCKADDR));

WSAAsyncSelect(m_sock, Handle, WM_SOCKET, FD_ACCEPT or FD_CLOSE);

listen(m_sock, 5);
.. ..



应用程序可以对收到 WM_SOCKET 消息进行分析,判断是哪一个 socket 产生了网络事件以及事件类型:

procedure TfmMain.WMSocket(var Msg: TMessage);
var
sock: TSocket;
addr: TSockAddrIn;
addrlen: Integer;
buf: Array [0 .. 4095] of Char;
begin
// Msg 的 WParam 是产生了网络事件的 socket 句柄, LParam 则包含了事件类型
case WSAGetSelectEvent(Msg.LParam) of
FD_ACCEPT:
begin
addrlen := sizeof(addr);
sock := accept(Msg.WParam, addr, addrlen);
if sock <> INVALID_SOCKET then
WSAAsyncSelect(sock, Handle, WM_SOCKET, FD_READ or FD_WRITE or FD_CLOSE);
end;

FD_CLOSE:
closesocket(Msg.WParam);
FD_READ:
recv(Msg.WParam, buf[0], 4096, 0);
FD_WRITE:
;
end;
end;




三: WSAEventSelect 模型

后来,微软的信箱非常畅销,购买微软信箱的人以百万计数 ...... 以至于盖茨每天 24 小时给客户打电话,累得腰酸背痛,喝蚁力神都不好使 ~~~~~~
微软改进了他们的信箱:在客户的家中添加一个附加装置,这个装置会监视客户的信箱,每当新的信件来临,此装置会发出新信件到达声,提醒老陈去收信。盖茨终于可以睡觉了。

同样要使用线程:

procedure TListenThread.Execute;
var
hEvent: WSAEvent;
ret: Integer;
ne: TWSANetworkEvents;
sock: TSocket;
adr: TSockAddrIn;
sMsg: String;
Index, EventTotal: DWORD;
EventArray: Array [0 .. WSA_MAXIMUM_WAIT_EVENTS - 1] of WSAEVENT;
begin
// .. .socket .. .bind .. .
hEvent := WSACreateEvent();
WSAEventSelect(ListenSock, hEvent, FD_ACCEPT or FD_CLOSE);
// .. .listen .. .

while (not Terminated) do
begin
Index := WSAWaitForMultipleEvents(EventTotal, @EventArray[0], FALSE, WSA_INFINITE, FALSE);
FillChar(ne, sizeof(ne), 0);
WSAEnumNetworkEvents(SockArray[Index - WSA_WAIT_EVENT_0], EventArray[Index - WSA_WAIT_EVENT_0], @ne);

if (ne.lNetworkEvents and FD_ACCEPT) > 0 then
begin
if ne.iErrorCode[FD_ACCEPT_BIT] <> 0 then
continue;

ret := sizeof(adr);
sock := accept(SockArray[Index - WSA_WAIT_EVENT_0], adr, ret);
if EventTotal > WSA_MAXIMUM_WAIT_EVENTS - 1 then // 这里 WSA_MAXIMUM_WAIT_EVENTS 同样是 64
begin
closesocket(sock);
continue;
end;

hEvent := WSACreateEvent();
WSAEventSelect(sock, hEvent, FD_READ or FD_WRITE or FD_CLOSE);
SockArray[EventTotal] := sock;
EventArray[EventTotal] := hEvent;
Inc(EventTotal);
end;

if (ne.lNetworkEvents and FD_READ) > 0 then
begin
if ne.iErrorCode[FD_READ_BIT] <> 0 then
continue;
FillChar(RecvBuf[0], PACK_SIZE_RECEIVE, 0);
ret := recv(SockArray[Index - WSA_WAIT_EVENT_0], RecvBuf[0], PACK_SIZE_RECEIVE, 0);
// .. .. ..
end;
end;
end;




四: Overlapped I/O 事件通知模型

后来,微软通过调查发现,老陈不喜欢上下楼收发信件,因为上下楼其实很浪费时间。于是微软再次改进他们的信箱。新式的信箱采用了更为先进的技术,只要用户告诉微软自己的家在几楼几号,新式信箱会把信件直接传送到用户的家中,然后告诉用户,你的信件已经放到你的家中了!老陈很高兴,因为他不必再亲自收发信件了!

Overlapped I/O 事件通知模型和 WSAEventSelect 模型在实现上非常相似,主要区别在 "Overlapped" , Overlapped 模型是让应用程序使用重叠数据结构 (WSAOVERLAPPED) ,一次投递一个或多个 Winsock I/O 请求。这些提交的请求完成后,应用程序会收到通知。什么意思呢?就是说,如果你想从 socket 上接收数据,只需要告诉系统,由系统为你接收数据,而你需要做的只是为系统提供一个缓冲区 ~~~~~
Listen 线程和 WSAEventSelect 模型一模一样, Recv/Send 线程则完全不同:

procedure TOverlapThread.Execute;
var
dwTemp: DWORD;
ret: Integer;
Index: DWORD;
begin
// .. .. ..

while (not Terminated) do
begin
Index := WSAWaitForMultipleEvents(FLinks.Count,
@FLinks.Events[0],
FALSE, RECV_TIME_OUT, FALSE);

Dec(Index, WSA_WAIT_EVENT_0);
if Index > WSA_MAXIMUM_WAIT_EVENTS - 1 then // 超时或者其他错误
continue;

WSAResetEvent(FLinks.Events[Index]);
WSAGetOverlappedResult(FLinks.Sockets[Index],
FLinks.pOverlaps[Index],
@dwTemp,
FALSE,
FLinks.pdwFlags[Index]^);

if dwTemp = 0 then // 连接已经关闭
begin
// .. .. ..
continue;
end
else
begin
fmMain.ListBox1.Items.Add(FLinks.pBufs[Index]^.buf);
end;

// 初始化缓冲区
FLinks.pdwFlags[Index]^ := 0;
FillChar(FLinks.pOverlaps[Index]^, sizeof(WSAOVERLAPPED), 0);
FLinks.pOverlaps[Index]^.hEvent := FLinks.Events[Index];
FillChar(FLinks.pBufs[Index]^.buf^, BUFFER_SIZE, 0);

// 递一个接收数据请求
WSARecv(FLinks.Sockets[Index],
FLinks.pBufs[Index],
1,
FLinks.pdwRecvd[Index]^,
FLinks.pdwFlags[Index]^,
FLinks.pOverlaps[Index],
nil);
end;
end;




五: Overlapped I/O 完成例程模型

老陈接收到新的信件后,一般的程序是:打开信封 ---- 掏出信纸 ---- 阅读信件 ---- 回复信件 ...... 为了进一步减轻用户负担,微软又开发了一种新的技术:用户只要告诉微软对信件的操作步骤,微软信箱将按照这些步骤去处理信件,不再需要用户亲自拆信 / 阅读 / 回复了!老陈终于过上了小资生活!

Overlapped I/O 完成例程要求用户提供一个回调函数,发生新的网络事件的时候系统将执行这个函数:
procedure WorkerRoutine( const dwError, cbTransferred : DWORD; const
lpOverlapped : LPWSAOVERLAPPED; const dwFlags : DWORD ); stdcall;
然后告诉系统用 WorkerRoutine 函数处理接收到的数据:
WSARecv( m_socket, @FBuf, 1, dwTemp, dwFlag, @m_overlap, WorkerRoutine );
然后 ...... 没有什么然后了,系统什么都给你做了!微软真实体贴!

while (not Terminated) do // 这就是一个 Recv/Send 线程要做的事情 ...... 什么都不用做啊!!!
begin
if SleepEx(RECV_TIME_OUT, True) = WAIT_IO_COMPLETION then //
begin
// ....
end
else
begin
continue;
end;
end;



六: IOCP 模型

微软信箱似乎很完美,老陈也很满意。但是在一些大公司情况却完全不同!这些大公司有数以万计的信箱,每秒钟都有数以百计的信件需要处理,以至于微软信箱经常因超负荷运转而崩溃!需要重新启动!微软不得不使出杀手锏 ......
微软给每个大公司派了一名名叫 "Completion Port" 的超级机器人,让这个机器人去处理那些信件!

"Windows NT</SPAN><SPAN>小组注意到这些应用程序的性能没有预料的那么高。特别的,处理很多同时的客户请求意味着很多线程并发地运行在系统中。因为所有这些线程都是可运行的< SPAN SPAN><SPAN>??-----< SPAN><SPAN>又怎会没有考虑到呢< Microsoft< SPAN> 摘自 nonocast 的《理解 I/O Completion Port 》

先看一下 IOCP 模型的实现:

// 创建一个完成端口
FCompletPort := CreateIoCompletionPort(INVALID_HANDLE_VALUE,
0,
0,
0);

// 接受远程连接,并把这个连接的 socket 句柄绑定到刚才创建的 IOCP 上
AConnect := accept(FListenSock,
addr,
len);
CreateIoCompletionPort(AConnect,
FCompletPort,
nil,
0);

// 创建 CPU 数 *2 + 2 个线程
for i := 1 to si.dwNumberOfProcessors * 2 + 2 do
begin
AThread := TRecvSendThread.Create(false);
AThread.CompletPort := FCompletPort; // 告诉这个线程,你要去这个 IOCP 去访问数据
end;



OK ,就这么简单,我们要做的就是建立一个 IOCP ,把远程连接的 socket 句柄绑定到刚才创建的 IOCP 上,最后创建 n 个线程,并告诉这 n 个线程到这个 IOCP 上去访问数据就可以了。

再看一下 TRecvSendThread 线程都干些什么:

procedure TRecvSendThread.Execute;
var
.. .. ..
begin
while (not self.Terminated) do
begin
// 查询 IOCP 状态(数据读写操作是否完成)
GetQueuedCompletionStatus(CompletPort,
BytesTransd,
CompletKey,
POVERLAPPED(pPerIoDat),
TIME_OUT);

if BytesTransd <> 0 then
.. ..; // 数据读写操作完成

// 再投递一个读数据请求
WSARecv(CompletKey,
@(pPerIoDat^.BufData),
1,
BytesRecv,
Flags,
@(pPerIoDat^.Overlap),
nil);
end;
end;




读写线程只是简单地检查 IOCP 是否完成了我们投递的读写操作,如果完成了则再投递一个新的读写请求。
应该注意到,我们创建的所有 TRecvSendThread 都在访问同一个 IOCP (因为我们只创建了一个 IOCP ),并且我们没有使用临界区!难道不会产生冲突吗?不用考虑同步问题吗?

呵呵,这正是 IOCP 的奥妙所在。 IOCP 不是一个普通的对象,不需要考虑线程安全问题。它会自动调配访问它的线程:如果某个 socket 上有一个线程 A 正在访问,那么线程 B 的访问请求会被分配到另外一个 socket 。这一切都是由系统自动调配的,我们无需过问。