以下是Windows平台下两个函数的声明:
默认情况下,操作系统为每一个套接字分配两个缓冲区分别用于缓冲发送数据和接受数据,所谓缓冲就是:
这两个缓冲区由操作系统管理,并且属于内核地址空间,是非分页的(Non-paged pool ).
传统模式下,我们直接调用这个两个API进行重叠IO操作,并传递我们的应用层缓冲区地址,这个时候操作系统典型的处理方法如下:
无论是发还是收,一旦应用层内存被锁住,这块内存就不能从物理内存分页出去.操作系统会限制这些被锁住的内存的数量,一旦达到这个限制,就会返回WSAENOBUFS错误.如果应用层在每一个连接上发起大量重叠IO请求,随着连接数的增长,很可能就达到这个限制的值.一方面是因为重叠IO操作数量上的增长,另一方面是因为当前系统的分页单位是固定的,即使应用层只有一个字节的操作请求,操作系统仍然需要付出一页(一般是4K)的代价.
如果服务器希望能处理非常多并发连接,可以在每个连接的读请求时投递一个0字节的读操作,即在WSARecv的时候为lpBuffers和 dwBufferCount分别传递NULL和0参数.这样做就不会存在内存锁定带来的资源紧张问题,因为没有内存需要被锁定,一旦有数据被收到,操作系 统就会投递完成通知.这个时候服务端就可以去套接字接受缓冲区取数据了,有两种方法可以得知到底有多少数据可以读,一种是通过ioctlsocket结合 FIONREAD参数去"查询",另一种就是一直读,直到得到WSAEWOULDBLOCK错 误,就表示没有数据可读了.另一方面在发送数据的时候,仍然可以采用这种方案,原因在于对端的应用可能效率非常低下,或者陷入了某个死循环,导致对方的网 络IO层迟迟不调用recv/WSARecv,受TCP协议本身的限制,服务端需要发送的数据就会一直PENDING,进而导致内存被内核锁住.采用0字 节发送方式后,应用层先投递一个空的WSASend,表示希望发送数据,操作系统一旦判断这个连接可以写了,会投递一个完成通知,此时便可以放心投递数 据,并且发送缓冲区的大小是可知的,不会存在内存锁定的问题.
这种方案适合最大化并发量,但也存在短处,首先就是数据发送和接受的时候有一个数据拷贝的代价,从网络上收到的数据 并不是直接放到应用层提交的缓冲区里.另外一个代价就是每一次读和写要经过一个先请求后实施的操作,而传统的方案是要一步到位.但正是这些差异避免了对系 统资源严重占用.
提到windows平台上的高性能IO操作,就不得不提IOCP(完成端口),上面的方案是完全适合ICOP模型 的.顺带提一下在这种模型下对同一个套接字投递多个读和写操作的情况,IOCP可以保证多个同一个句柄上的多个重叠操作在数据处理上是有序的,也就是说先 提交的重叠操作先处理,但是不保证你收到的完成通知是有序的.