陈兰若(我)网络编程不敢说是精通, 但也略有小成!有着基础网络封装的深入经验!不管Linux异步网络之基于select或是epoll的应用封装, 还是Window异步网络之基于WSAAsyncSelect、 WSAEventSelect, select(通用)的应用封装,亦或是完成端口IOCP的应用封装, 有人说IOCP也是异步网络, 对, 但又不全对! 下面我有详解!
我把网络归为三类(IO级别来分, 没有谈到形而上更高架构应用的), 阻塞的, 非阻塞的(异步的), IOCP(当然也是异步的, 但有别于其他异步网络,会讲到, 别急)!
阻塞的网络, 即阻塞socket IO 模型, 最简单,最low的, 哦对, 不能这样说(看我有这样), 实际上也有很多应用的。,为什么简单,换言之,每次处理网络输入输出操作(如send 或者 recv等)的时候, 线程都将挂起,直到操作完成为止; 通常可以这样设置socket
u_long nNoBlock = 1;
ioctlsocket(sockt, FIONBIO, &nNoBlock):
就如我上面说的那样, 阻塞socket IO 模型的优点就是简单, 程序易于编写, 可用于快速原型开发!机构内部网路连接数不是很多比较实用! 多用户连接时, 需为每个用户开辟一个线程。
非阻塞网络(异步网络,和IOCP不同的), 我通常将其命名为: 就绪通告socket IO模型, 实际上更为贴切。因为, socket IO 准备好了(接收网络数据时, 数据已经到了我们系统接收缓存队列中, 这时读取数据基本上都会成功;发送网络数据时, 数据已经到了我们系统发送缓存队列中, 这时发送就会基本成功),会通过各种方式通知我们的程序! 就是由于这种通知我们的方式不同, 将异步分为不同的类型。下面细说:
1, select的socket IO 模型, Linux和Window平台都可以应用,select模型的名字来源于select函数。当线程调用socket IO函数时, 比如
recv, 此时如果套接字(socket)处于阻塞模式, 则直接调用recv有可能会使线程阻塞; 如果套接字处于非阻塞模式。直接调用recv又有可
能会返回WSAEWOULDBLOCK--还是要在某个时候重新调用recv来读取数据,也就是说无论套接字是阻塞模式还是非阻塞模式, 直接调用
recv都有可能出现问题, select 模型, 线程要调用recv时, 不直接调用recv, 而是把recv的socket的句柄纳入select的监视之下(此处表达
非常贴切), 然后在调用select, 通常select:
int select(
int nfds, /*-------socket 句柄数-------*/
fd_set* readfds, /*-------希望读取数据时被监视的socket 句柄集合-------*/
fd_set* writefds, /*-------希望发送数据时被监视的socket 句柄集合-------*/
fd_set* exceptfds, /*-------希望发送数据时被监视的socket 句柄集合-------*/
const struct timeval* timeout) /*-----------超时设置---------------/
正是fd_set是集合性质的, select才具备了检测多个套接字(socket)的能力, 如果我们需要对某个套接字进行操作, 则需要把该套接字的
句柄填入这些fd_set集合中去, readfds 是针对读操作而言的, 可以包含accept, recv等, writefds则针对写操作,可以包含connect、
send等, 还是举个例子吧:
假设现在有两个套接字, 他们的句柄分别是 hSocket1和hSocket2, 我们需要对hSocket1进行写操作, 而对hSocket2则需要进行
读和写操作, 那么在调用select之前, 我们应该填充好readfds以及writefds, 如下:
readfds: hSocket2
writefds: hSocket1, hSocket2
当select返回时, 它会更新readfds和writefds, 使它们 只 包含那些就绪的套接字句柄,比如说, 如果select 返回时, 仅有hSocket2的写
操作就绪, 那么readfds 和 writefds就变成如下所示:
readfds: hSocket2
writefds: hSocket1, hSocket2
也就是说, 当select返回时, readfds 和 writefds 是原来的集合的子集, 这些子集中包含的就是已经就绪的套接字的句柄。因此在select返
回时,线程应该检查这些子集, 进而决定应该进行操作! 一般而言, 所有的socket(被select监视)的, 都应该填入exceptfds中, 进而确
定该套接字是否发送了异常和错误。
2.WSAAsyncSelect模型, 和 select模型一样, WSAAsyncSelect 模型也属于就绪通告socket IO模型的一种, 不一样的是select 模型使
用select函数和fd_set来通知线程I/O操作已经就绪, WSAAsyncSelect模型则使用Windows窗口过程和消息来通知:
int WSAAsyncSelect(
SOCKET s,
HWND hWnd,
u_int, wMsg,
long lEvent);
做过Windows编程的, 一目了然, WSAAsyncSelect就是把一个socket句柄关联到一个窗口句柄hWnd(消息是wMsg), 当然, 我们我们可
以关联多个sockets句柄到同一个窗口。 这样一个线程就可以同时服务多个客户端连接 , 第三个参数就是我们自定义的消息, 当socket操作就
绪时, 就会该消息, 用相应的窗口过程处理即可, 简单吧。 开发是需要注意的细节很多, 在此不再赘述!@我, 再回复!
3.WSAEventSelect模型, 它既有select模型的特点, 也有WSAAsyncSelect模型的特点, Event, 即该模型使用了Windows的事件内核对
象(Event Kernel Object) 来作为就绪通告的通知方式。
int WSAEventSelect(
SOCKET s,
WSAEVENT hEventObject, /*------------事件--------------*/
long lEvent);
s 自然代表socket, hEvnetObject, 我们定义的事件, 第三个参数和前面WSAAsyncSelect函数的最后一个参数的含义是完全一样的, 即代
表我们说感兴趣的I/O消息, 如FD_READ, FD_WRITE等(WSAAsyncSelect的补充)!,Windows有些东西做的很人性化。 在此不再赘
述!@我, 再回复!
IOCP( 完成端口), 前面已经说了, IOCP可以被认为是异步的, 不会让线程阻塞, 但是和前面讲的的就绪通告socket I/O 还是不一样。主要, 放大招
了! PSPS 注意:
在前面的阐述中, 我们知道, 使用异步(就绪通告)socket I/O模型时, 系统会也会通知我们的线程, 而线程在收到通知后, 会去再次调
用相关的I/O函函数。但是使用IOCP(属于我起名曰 完成就绪通告)模型时, 系统在 完成(注意)I/O操作后也会通知我们的线程, 但是线
程收到通知后, 不再需要向就绪通告(异步, 可以叫传统异步)I/O模型一样, 再去调用相关的I/O函数,因为IOCP(完成就绪通告的一种)
的通知意味着相关的I/O的操作已经完成了。所以我叫它, 完成就绪通告, 关键是完成!
完成就绪通告I/O(有些书叫重叠I/O)模型的代表又很多,譬如, 完成就绪通告I/O + 事件通告; 完成就绪通告I/O + 回调; 完成就绪通告I/O
+ 完成端口! 由于篇幅问题, 前面两个在此不展开讨论, 一句话: @我, 再回复!, 完成端口要重点说说:
那么什么是IOCP呢, 其实是一种内核对象(我们知道,Windows 内核对象有多种,包括事件内核CEvent对象, 线程内核对象CTHread, 文件内
核对象CFILE,IOCP也是一种),所以目前专属Windows(据说ACE,boost实现相应的功能) IOCP这种内核对象比较特殊,因为它用于通知应用
程序异步I/O完成的, 换句话说,当一个异步网络I/O 操作完成时,IOCP将通知应用程序 。 怎么实现的呢?
既然是一个内核对象, 那么就像事件内核对象的使用一样, 首先, 创建一个IOCP内核对象,API是:
HANDLE CreateIoCompletionPort( /*-----名字多么明白呀------*/
HANDLE hFile,
HANDLE hExistingCompletionPort,
ULONG_PTR CompletionKey,
DWORD dwNumberOfConcurrentThreads);
这个函数比较复杂牛逼, 关键是牛逼, 因为它有两个功能:创建一个IOCP以及把一个设备(在此是socket句柄)关联到一个存在的IOCP。当我们创
建IOCP时, 可以忽略前面三个参数, 因为这三个参数是用于把设备关联到IOCP的, 可以使用下面的代码来创建一个IOCP:
HANDLE hIocp = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL,0, dwNumberOfConcurrentThreads);
把套接字hSocket关联到IOCP上 则如下
hIop2 = CreateCompletionPort(hSocket, hIocp, 0, 0);
其中参数hIocp 是上面的创建的, 成功返回的就是hIocp 即 hIocp2 == hIocp;
那么怎样将IOCP帮到线程池呢,在线程函数里调用下面的函数即可
BOOL GetQueuedCompletionStatus(
HANDLE hCompletionPort,
PDWORD pdwNumberOfBytesTransferred,
PULONG_PTR pComletionKey,
OVERLAPPED** ppOverlapped,
DWORD dwMilliseconds);
此函数返回, 代表所在线程被唤醒来处理我们应用的逻辑!当然还有很多细节需要注意, 开发时,遇到疑问时, @我,一起讨论!
有人会说, 现在网上开源的网络的框架那么多, IOCP也有封装的现成的。为什么要搞懂这些底层的原理。 像我, 也封装过IOCP, 异步(客户端)网络,但是我还是要说, 我们要知其然也要知其所以然, 为什么要这样, 我看到很多人用开源的框架, 总是出问题, 还怪人家框架写的有问题, 其实是他没有搞懂原理, 用法不得当, 愚蠢!进步之心不容明灭。SSH也值得写篇稿子!
最近编辑记录 cgpsky (2017-09-09 22:48:58)
离线
强
离线
学习鹏哥的钻研精神
离线
离线
离线
厉害了
离线