I/O 模型
2019-04-14 09:08 发布
生成海报
本文简单介绍了当前
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
小组注意到这些应用程序的性能没有预料的那么
高。特别的,处理很多同时的客户请求意味着很多线程并发地运行在系统中。因为所有这些线程都是可运行的
[
没
有被挂起和等待发生什么事
]
,
Microsoft
意
识到
NT
内核花费了太多的时间来转换运行线程的上下文
[Context]
,
线程就没有得到很多
CPU
时间来做它们的工作。大家可能也都感觉到并行模型的瓶颈在于它为每一个客
户请求都创建了一个新线程。创建线程比起创建进程开销要小,但也远不是没有开销的。我们不妨设想一下:如果事先开好
N
个
线程,让它们在那
hold[
堵塞
]
,然后可
以将所有用户的请求都投递到一个消息队列中去。然后那
N
个线程逐一从消息队列中去取出消息并加以处
理。就可以避免针对每一个用户请求都开线程。不仅减少了线程的资源,也提高了线程的利用率。理论上很不错,你想我等泛泛之辈都能想出来的问题,
Microsoft
又
怎会没有考虑到呢
?"-----
摘自
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
。
这一切都是由系统自动调配的,我们无需过问。
打开微信“扫一扫”,打开网页后点击屏幕右上角分享按钮