先来说一下同步/异步、阻塞/非阻塞的问题。
同步,就是在发出一个功能调用时,在没有得到结果之前,该调用就不返回。
异步的概念和同步相对。当一个异步过程调用发出后,调用者不能立刻得到结果。实际处理这个调用的部件在完成后,通过状态、通知和回调来通知调用者。
阻塞调用是指调用结果返回之前,当前线程会被挂起。函数只有在得到结果之后才会返回。有人也许会把阻塞调用和同步调用等同起来,实际上他是不同的。对于同步调用来说,很多时候当前线程还是激活的,只是从逻辑上当前函数没有返回而已。
非阻塞和阻塞的概念相对应,指在不能立刻得到结果之前,该函数不会阻塞当前线程,而会立刻返回。
这两组概念之间是相似而有区别的,在这里就不细究了。
在Winsock中实现异步的方法有很多,Winsock的IO模型有下面六种:
二:WSAAsyncSelect模型
三:WSAEventSelect模型
四:Overlapped I/O 事件通知模型
五:Overlapped I/O 完成例程模型
六:IOCP模型
一:select模型
老陈非常想看到女儿的信。以至于他每隔10分钟就下楼检查信箱,看是否有女儿的信~~~~~
在这种情况下,“下楼检查信箱”然后回到楼上耽误了老陈太多的时间,以至于老陈无法做其他工作。
二:WSAAsyncSelect模型
后来,老陈使用了微软公司的新式信箱。这种信箱非常先进,一旦信箱里有新的信件,盖茨就会给老陈打电话:喂,大爷,你有新的信件了!从此,老陈再也不必频繁上下楼检查信箱了,牙也不疼了,你瞅准了,蓝天……不是,微软~~~~~~~~
三:WSAEventSelect模型
后来,微软的信箱非常畅销,购买微软信箱的人以百万计数……以至于盖茨每天24小时给客户打电话,累得腰酸背痛,喝蚁力神都不好使~~~~~~
微软改进了他们的信箱:在客户的家中添加一个附加装置,这个装置会监视客户的信箱,每当新的信件来临,此装置会发出“新信件到达”声,提醒老陈去收信。盖茨终于可以睡觉了。
四:Overlapped I/O 事件通知模型
后来,微软通过调查发现,老陈不喜欢上下楼收发信件,因为上下楼其实很浪费时间。于是微软再次改进他们的信箱。新式的信箱采用了更为先进的技术,只要用户告诉微软自己的家在几楼几号,新式信箱会把信件直接传送到用户的家中,然后告诉用户,你的信件已经放到你的家中了!老陈很高兴,因为他不必再亲自收发信件了!
五:Overlapped I/O 完成例程模型
老陈接收到新的信件后,一般的程序是:打开信封—-掏出信纸—-阅读信件—-回复信件……为了进一步减轻用户负担,微软又开发了一种新的技术:用户只要告诉微软对信件的操作步骤,微软信箱将按照这些步骤去处理信件,不再需要用户亲自拆信/阅读/回复了!老陈终于过上了小资生活!
六:IOCP模型
微软信箱似乎很完美,老陈也很满意。但是在一些大公司情况却完全不同!这些大公司有数以万计的信箱,每秒钟都有数以百计的信件需要处理,以至于微软信箱经常因超负荷运转而崩溃!需要重新启动!微软不得不使出杀手锏……
微软给每个大公司派了一名名叫“Completion Port”的超级机器人,让这个机器人去处理那些信件!
其实,上面每种模型都有优点,要根据程序需求而适当选择合适的模型,前面三种模型效率已经比较高,实现起来难道不大,很多一般的网络程序都采用前三种模型,只有对网络要求特别高的一些服务器才会考虑用后面的那些模型。MFC中的CAsyncSocket类就是用的WSAAsyncSelect模型,电驴中也是用的这种,不过在寻找对应socket的时候进行了优化,查找更快,在GridCast中采用的是WSAEventSelect模型,等待。
select模型是WINSOCK 异步I/O模型里最简单的一种,其之所以叫做select模型,是因为该模型的核心就是一个WSA函数select.
该函数定义如下:
int select(int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
n:该参数忽略,之所以保留是因为和早期的berkeley函数兼容
readfds: 该参数为用于检查是否有数据需要读入(接收)的集合
writefds:该参数为用于检查是否有数据需要写出(发送)的集合
exceptfds:该参数为用于检查是否有"例外数据"需要读取的集合.
timeout:设置select函数检查的超时时间,如果设为NULL,则无限制等待直到上述集合有改变为止,如果设置为0,则select函数立即返回。
需要注意的是,上述三个集合之中,至少一个不为NULL,不为NULL的集合里,至少一个不为空,否则函数就没有等待的目标而导致该函数失去作用。
其中fd_set为WINSOCK提供的一个文件集描述符(简单的说就是一个集合而已),里面能够容纳数个SOCKET句柄。这就不得不提到select模型的另外几个核心的函数.
FD_SET(Socket s,fd_set *SET) //将s句柄放入集合SET
FD_ZERO(fd_set *SET) //清空SET
FD_CLR(Socket s,fd_set *SET) // 从SET中清除s
FD_ISSET(Socket s,fd_set *SET) //检查s是否在集合SET内,是返回TRUE,否返回FALSE
以上函数都非常便于使用,只需要将需要检查的SOCKET句柄按应用的需要放入相应的readfds,writefds,exceptfds即可,然后适时的调用select函数。select函数有个很重要的特性,就是会自动删除相应集合里不符合要求的SOCKET句柄,例如,SOCKET S作为需要检查的对象,用FD_SET放入readfds(当然readfds是自己申明的),然后用select函数进行检查,设置timeout为10s,那么如果10s内S没有需要读入的数据,select函数返回的时候会自动将S从readfds中删除,那么在用FD_ISSET检查的时候便可以分辨哪些是可读可写的。
一般程序大致走向如下。
1. SOCKET相关操作(create,bind,listen,accept……)
2. 建立readfds,writefds等集合
3. FD_ZERO初始化集合
4. FD_SET将SOCKET句柄放入感兴趣的集合内
5. 适时调用select
6. FD_ISSET检查感兴趣集合
7. 返回 3(一般都是有这一步)