Windows SOCKET 的几种 IO 模型

 

重叠IO的概念和阻塞/非阻塞的概念不同, 重叠属性在 SOCKET 一但创建之后无法改变

一, 阻塞模式 

这是最简单的IO模型,在程序中调用收发函数时, 如果缓冲区够用 (发的时候发送缓冲区有足够的空间 或者 收到时候接收缓冲区有足够多的数据) 从发收缓冲区和用户 buffer 之前复制数据然后立即返回, 如果缓冲区不够用会阻塞线程, 直到缓冲区够用, 然后在缓冲区和用户 buffer 之间完成数据复制, 完成后返回

可以通过 SetSockOpt/SO_RCVTIMEO/SO_SNDTIMEO 设置收发的超时时长

通常需要在一个单独的线程里进行收发操作, 处理多个套接字编程需要创建多个线程

二, 非阻塞模式

    概念上讲, 后面讲的几种模式的 SOCKET 为非阻塞模式, 这里使用 "非阻塞模式" 的名字是简单和第一种模式作区分

非阻塞模式下, 所有收发数据调用都会立即返回, 如果缓冲区够用返回成功操作, 如果缓冲区不够用返回 WSAEWOULDBLOCK 错误代码, 不管成功与否都不会阻塞调用线程, 所以可以在同一个线程中轮流处理多个 SOCKET 的数据收发

和阻塞模式相比, 非阻塞模式可以更方便地在同一个线程中处理多个 SOCKET, 但是非阻塞模式下需要不断以轮询的方式尝试发送或者接收, 代码效率上甚至会比阻塞模式还低。

阻塞模式和非阻塞之间的切换
在创建 socket 之后, 调用 ioctlsocket 通过传递 FIONBIO 参数可以让 SOCKET 变成阻塞或非阻塞的 SOCKET 

SOCKET s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
/* ==========
设置阻塞模式    
    如果 mode ==  0, 阻塞模式
    如果 mode !=  0,  非阻塞模式
 */
u_long mode = 1;     
ioctlsocket(s, FIONBIO, &mode);

三, select 模式, 选择模式

select 模式适合用于多个 SOCKET 进行收发操作,  select 函数可以同时对多个套接进行检查, 如果有套接字可以读、写, 它会返回这些套接字。 

int select(
    int nfds,  
    fd_set * readfds,  
    fd_set * writefds,   
    fd_set * exceptfds,   
    const struct timeval * timeout
);

fd_set 是一个 SOCKET 描述符集合

四, 窗口消息模式

  

WSAAsyncSelect 为 SOCKET 绑定一个接收通知的窗口, 当 SOCKET 可读、可写 或者 发生错误的时候, 会给这个窗口发送指定的消息, 然后可以在消息处理函数中对 SOCKET 进行操作
窗口消息模型可以把 SOCKET 和 窗口很好结合起来, 所有的 IO 操作可以在界面线程中完成, 不会阻塞界面线程

int WSAAsyncSelect( SOCKET s, HWND hWnd, u_int wMsg, long lEvent );

五, 事件模型

事件模式需要一个额外的手动重置的事件内核对象 WSAEVENT,需要在另一个线程在循环等等这个事件对象被触发
事件模式和窗口消息模型非常类似,只不过窗口消息使用消息循环来等待异步通知, 事件模型在新的线程里等待异步通知

WSAEVENT socketEvent;
while(true)
{
    ::WSAEventSelect(sock, socketEvent, xxx)
}



1, 创建 SOCKET
2, 创建 WSAEVENT 事件对象
和事件内核对象一样, 但是没有安全描述,不能命名对象, 不能跨进程使用, 并且只能手动复位, 创建后处于没有信号状态
WSAEVENT WSACreateEvent( ); //创建事件
BOOL WSASetEvent( WSAEVENT hEvent ); //设置事件状态为有信号状态
BOOL WSAResetEvent( WSAEVENT hEvent ); //设置事件状态为没有信号状态
BOOL WSACloseEvent( WSAEVENT hEvent ); //销毁事件

3, 关联 SOCKET 与事件
创建事件之后, 调用 WSAEventSelect 关联 SOCKET 和这个事件, lNetworkEvents 是一个位掩码, 标识需要获取的感兴趣的事件

int WSAAPI WSAEventSelect( SOCKET s, WSAEVENT hEventObject, long lNetworkEvents );

4, 在线程中等待事件
调用 WSAWaitForMultipleEvents 等等事件发生
DWORD WSAWaitForMultipleEvents( DWORD cEvents, const WSAEVENT *lphEvents, BOOL fWaitAll, DWORD dwTimeout, BOOL fAlertable );

当事件发生时, 调用 WSAEnumNetworkEvents 检查发生了什么事件
int WSAAPI WSAEnumNetworkEvents( SOCKET s, WSAEVENT hEventObject, LPWSANETWORKEVENTS lpNetworkEvents );


事件模型和窗口消息模型很相似, 事件模型是在单独的线程中等待事件, 比起窗口消息更灵活
如果 SOCKET 是阻塞的 调用 WSAAsyncSelect 或 WSAEventSelect 之后会变成非阻塞的,

 

六, 使用事件通知的重叠IO模型

重叠 IO 模型的所有读写操作都需要关联一个 WSAOVERLAPPED 结构体

typedef struct _WSAOVERLAPPED {
  DWORD Internal;
  DWORD InternalHigh;
  DWORD Offset;
  DWORD OffsetHigh;
  WSAEVENT hEvent;
} WSAOVERLAPPED, *LPWSAOVERLAPPED;


WSAOVERLAPPED 结构体兼容 Windows IO 的 OVERLAPPED 结构体, 在 WinSock 中 WSAOVERLAPPED 结构体只使用最后那个事件成员 hEvent, 如果这个事件是一个有效事件, 在IO完成后会 winsock 置为有信号状态, 应用程序可以使用 WSAWaitForMultipleEvents 或者 WSAGetOverlappedResult 去等待这个事件

在整个IO操作完成之前, 这个 WSAOVERLAPPED 结构体必须有效状态,  不能提前释放

 

七,使用完成例程的重叠IO模式

这种模式下需要为每个 IO 操作指定 WSAOVERLAPPED 结构体和一个完成例程回调, 其中 WSAOVERLAPPED 结构体中的 WSAEVENT 类型成员 hEvent 被忽略不使用, 当 IO 操作完成时会调用完成例程回调, 这个例程的原形如下

    VOID CompletionRoutine(
        DWORD dwErrorCode,
        DWORD dwNumberOfBytesTransfered,
        LPVOID lpOverlapped 
    ); 


为了确保完成路由会被调用, 线程在发起 IO 操作之后必须要进入可提醒的阻塞状态, 那些导致线程进入阻塞状态的等待API(比如 Sleep,WaitForSingleObject ,MsgWaitForMultipleObjects) 会有一个带 Ex 版本的( 比如 SleepEx, WaitForSingleObjectEx,MsgWaitForMultipleObjectsEx) 可以让线程进入可提醒的阻塞状态, 可提醒阻塞状态下, 如果有使用了完成例程的 IO 操作完成, 这个线程会调用完成例程然后从阻塞 API 中返回, 即使等待的时间没到或者等待的对象没有被设置为有信号状态也会返回, 如果是因为IO操作完成导致的从可提醒的状态返回, 这些等待API 会返回 WAIT_ABANDONED, 如果需要处理更多的完成例程, 必须让线程再次进入可提醒状态

必须让发起 IO 操作的那个线程进入可提醒状态, 才能调用完成例程, 也就是发起 IO 操作和执行完成例程的是同一个线程

下列 API 会让线程进入可提醒状态
SleepEX // bAlertable 参数为 TRUE
WaitForSingleObjectEx // bAlertable 参数为 TRUE
WaitForMultipleObjectsEx // bAlertable 参数为 TRUE
MsgWaitForMultipleObjectsEx // dwFlags 参数添加 MWMO_ALERTABLE 标记

WSAWaitForMutipleEvent 或者 SleepEx 等函数如果让线程进入可提醒的阻塞状态, 如果要进入可提醒的阻塞状态, 必须指定最后那个参数为 TRUE

在可提醒阻塞状态中, 完成路由会在这个线程中调用, 同时会结束阻塞状态 并返回 WAIT_IO_COMPLETION 即使 WSAWaitForMutipleEvent 中等待的对象处于未激活状态 也会立即返回, SleepEx 未等待指定的时候也会立即返回, 如果 WSAWaitForMutipleEvent 或者 SleepEx 等函数 的最后那个参数为 FALSE, 则完成路由不会被调用

八, IOCP模型,IO完成端口模式

 

这是最复杂的模型,

IO 完成端口需要先创建一个端口完成对象
然后把一些 SOCKET 添加到 端口完成对象中
然后再创建几个工作线程
IO 完成端口适用于大量 SOCKET 的 IO 操作, 有非常高的效率, 并且可伸缩性非常好

 

猜你喜欢

转载自www.cnblogs.com/Merlyn7/p/10907872.html