一般情况下,IO操作的行为受两种因素的影响:
- IO操作对象的类型(阻塞还是非阻塞)
- 获取IO操作结果的方式(同步还是异步).
同步就是指操作的发起和操作结果的获取由调用者完成.
异步指操作发起由调用方完成,操作结果由服务方主动告知.
IO操作一般可以分为4种:
- 同步阻塞IO :调用方调用IO函数,并阻塞在这个函数上面.
- 同步非阻塞IO:调用方不断的调用IO函数,直到有”明确”的返回值.
- 异步阻塞IO:调用方发起IO操作后,阻塞在接收IO通知的API上.
- 异步非阻塞IO:调用方发起IO操作后继续其他工作,由内核负责回调或者发出IO通知信号.
阻塞IO对象在调用期间会阻塞IO函数,函数返回的时候,操作结果是明确的,因此不需要配合其他API来获取改操作的结果.
非阻塞IO对象在发生IO调用的时候,总是立即返回(返回的IO请求的结果,不是IO的执行结果),但执行结果不能马上得知,调用者可能需要使用配套的一系列API来获取IO结果(在什么时机,什么地点使用由调用方自己决定).
现在我们将IO对象放在套接字上,那么套接字有阻塞型套接字(Bolocking Socket)和非阻塞型套接字.再将目光放在windows操作系统上:
windows下套接字模型可以分为:
- Blocking Mode 阻塞型
- Non-blocking Mode 非阻塞型
windows下套接字IO模型:
- The blocking Model
- The select Model
- The WSAAsyncSelect Model
- The WSAEventSelect Model
- The Overlapped Model
- The Completion Port Model
备注:
- 上面六种基本上可以认为:按照从上到下的顺序,上面的使用最简单程度,依次递减,性能依次递增.
- 只要调用WSAEventSelect 或者WSAAsyncSelect, 套接字都会被自动设置为Non-Blocking.
- windows下典型的套接字API就是WSAXxxx家族函数,比如WSASend,WSARecv.因为WSAXxxx能够处理所有传统套接字API(如send,recv)的所有功能,我们只讨论前者.
Windwos下的WSAXxxx系列函数被设计成一套适合各种场景的API,看上去它非常的”聪明”,根据不同的场景有不同的语义:
- 如果操作的对象是一个阻塞型套接字,那么它的行为就跟传统的套接字API一样,调用方被阻塞在这个API上,直到IO操作返回结果.这种方式的最大好处是处理流程简单.
- 如果操作的对象是一个非阻塞套接字,它会立即返回结果(一般是WSAEWOULDBLOCK),这种方式的好处就是可以很方便的控制IO超时.
WSAEWOULDBLOCK其实算不上一种严重的错误,在不同场景有不同的语义:
Function Name
Description
WSAAccept and accept
The application has not received a connection request. Call again to check for a connection.
closesocket
In most cases, this means that setsockopt was called with the SO_LINGER option and a nonzero timeout was set.
WSAConnect and connect
The connection is initiated. Call again to check for completion.
WSARecv, recv, WSARecvFrom, and recvfrom
No data has been received. Check again later.
WSASend, send, WSASendTo, and sendto
No buffer space available for outgoing data. Try again later.
- 当操作的对象是一个重叠IO的套接字,情况就稍微复杂一些了,以WSASend为例子,先看其函数原型:
1: int WSASend(
2: __in SOCKET s,
3: __in LPWSABUF lpBuffers,
4: __in DWORD dwBufferCount,
5: __out LPDWORD lpNumberOfBytesSent,
6: __in DWORD dwFlags,
7: __in LPWSAOVERLAPPED lpOverlapped,
8: __in LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
9: );
A. lpCompletionRoutine 有效, hEvent 会被忽略.内核会在IO事件完成时调用该回调函数(lpCompletionRoutine ).B.lpCompletionRoutine 为NULL, hEvent 有效,内核会在IO事件完成时为这个 hEvent 设置信号.应用层可以用WSAWaitForMultipleEvents等方式等待信号.C. lpCompletionRoutine 和lpOverlapped都为NULL( hEvent 自然也是无效的),则视为非重叠IO套接字对待.
如果套接字句柄已经绑定到某个完成端口句柄上,回调函数必须设置为NULL,否则将得到10022(参数错误).如果IO结构体中的hEvent有效,内核仍然会为这个句柄设置信号.
重叠IO套接字上的操作通常都会立即返回,如果该操作可以立即完成则会返回字节数,否则返回(WSA_IO_PENDING),表示操作结果不能立即取得,从这个时候开始一直到完成事件到达,提交给内核的内存缓冲区将被锁定,调用方需要保证这期间该内存一直有效.因为在这种模式下,内核的策略是IO传输的数据直接缓冲到调用方的这块内存地址是,而不是套接字自身的缓冲区.这就为实现一个零拷贝(Zero Copy)的IO框架提供了可能性,试想一下,整个过程中不需要任何的memcpy,是不是很诱人?
此外,服务器也可能是一个CPU密集型服务,这个时候只需要改变一下套接字API(WSASend/WSARecv)的用法,很容易就从proactor模式切换为reactor模式,窍门就是使用零缓冲WSASend/WSARecv.
最后,一个常用的流程图:
图中的FIOBNIO=TRUE表示套接字设置是非阻塞,反之为阻塞套接字.