近来仔细学习了windows的5种网络I/O模型,阻塞、选择、异步选择、事件选择都还比较好理解,编写程序也不是很难,但是完成端口模型就比较麻烦了,需要考虑很多的线程间同步问题,诸多异常情况的处理,主动和被动关闭客户端连接问题。经过接近一个来月的思考和实践,终于有了初步的实现方案,其中遇到了许多问题,走了许多的错路,吸取了许多的教训。现总结一下思路,方便以后继续完善和改进。
关于一些基本的问题就不说了,资料太多了。主要还是总结下一些需要仔细考虑的地方。
一:工作者线程应该做的事
看网友总结一般来说创建2*CPU数量左右的线程,且具体创建多少需要实际测试。《UNIX编程艺术》一书警告我们说,在你真正确定系统瓶颈之前不要盲目的优化,不要过早优化。具体创建多少个线程可以先不考虑,留待以后完善后在实际环境中测试。应该先确定好工作线程的职责。
因为完成端口为大量的并发连接和I/O而设计,而且由于CPU数量有限、线程切换代价等原因导致只能创建有限个工作者线程。有限的几个工作者线程想要管理大量的并发操作,就要求工作者线程必须得尽快的完成连接或收发数据的操作,不能过长的阻塞,阻塞太久可能导致之后的IO操作分配不到线程去处理。因此可以明确工作线程只需做自己必须得做的最少的事就可以了,包括检测客户端的连接通知、接收数据完成通知和发送数据完成通知。因为连接要先建立socket,再发出连接请求,有的可能还要求建立连接后立马接收一些数据才返回连接完成,因此可以将发出连接请求的操作放到专门的线程中去完成。接收连接的请求就放入对应连接的接收缓冲区就可以了。
二:需要哪些线程和模块,每个线程的任务是什么?
1.首先是上面的工作者线程,应该创建一个线程池来执行其中的任务,可以用windows vista以上版本微软专为完成端口设计的线程池来完成。需要用到的函数如下:
PTP_IO WINAPI CreateThreadpoolIo(
__in HANDLE fl, //socket句柄
__in PTP_WIN32_IO_CALLBACK pfnio, //线程执行的代码段指针
__inout_opt PVOID pv, //需要传递的参数
__in_opt PTP_CALLBACK_ENVIRON pcbe //一般设为NULL;
);
VOID WINAPI StartThreadpoolIo(
__inout PTP_IO pio
);
VOID WINAPI CancelThreadpoolIo(
__inout PTP_IO pio
);
VOID CALLBACK OverlappedCompletionRoutine( //线程执行的代码段回调函数
PTP_CALLBACK_INSTANCEpInstance,
PVOID pvContect,
PVOID pOverlapped,
ULONG IoResult,
ULONG_PTR NumberOfBytesTransferred,
PTP_IO pIo)
详情可查找MSDN
每次有socket连接上时都需要用socket句柄调用CreateThreadpoolIo,每次投递请求(接收、发送、连接)都需要调用StartThreadpoolIo,如果投递请求失败(投递失败指的是WSARecv、WSASend和WSAAccept调用返回失败且WSAGetLastError返回值不是WSA_IO_PENDING,另外还有一种情况,我这里没有使用不会出现) 则需要调用CancelThreadpoolIo,且CancelThreadpoolIo需要对应出错的StartThreadpoolIo,即在StartThreadpoolIo和CancelThreadpoolIo之间不要发出其他的StartThreadpoolIo。MSDN上的说明很详细。
2.投递连接请求的线程:专门用来保证服务端在每个时刻保留有一定数量的连接请求发出和创建socket,投递连接请求。
3.数据接收缓冲区:为每一个连接建立一个缓冲区,将接收到的数据按顺序存入其中。
4.组包线程和数据包队列:由于TCP是流式协议,存在粘包问题,因此需要自定义数据包来解决,因此需要一个或多个线程专门来为连接的客户端进行组包。线程从缓冲区中读取数据进行分析,如果成功组合一个数据包则放入一个数据包队列,由专门的数据包处理线程来读取和处理。
5.数据包处理线程:处理接收的数据包,具体任务由应用层来处理,暂时以回发的方式简单处理。
6.发送线程和发送队列:建立一个发送队列,当有数据需要发送时直接放入队列即可,由专门的线程去取出数据进行发送。
基本的部分大概就这些,具体的细节和其中的诸多问题后面再详细记录。
...