CAsyncSocket多线程环境

来源:互联网 发布:商场导航软件 编辑:程序博客网 时间:2024/04/28 13:48
来自:http://blog.csdn.net/ydbcsdn/article/details/1804096
CAsyncSocket多线程环境
  • Windows网络编程
异步选择模型和普通的选择模型有着本质的区别,这个模型利用了Windows的窗口消息机制 。
在Linux网络编程里,最常用的是select模型,调用select函数后,线程进入阻塞状态,直到超时或socket的相关操作有可用性,比如可以网络对端发来数据,这时系统内核会告知现在SOCKET现在可读,此时select函数返回,线程继续执行。
WINDOWS网络里也提供这样的select调用,但是为了更好的利用WINDOWS消息机制,WINDOWS在传统Beckly socket的基础上进行了高度扩展,称为WINSOCK。WINSOCK里提供了异步选择机制,它的原理是将一个网络通知事件作为消息传递到一个接收窗口,利用WINDOWS的消息处理机制,将网络事件转给一个窗口过程来处理,程序员通过写这个窗口过程(实际上是个大的SWITCH-CASE语句)来实现对网络事件的处理。
MFC类是对WINDOWS API的封装,当然也少不了对WINSOCK API 的封装。
MFC对WINSOCK API的第一层封装类是 CAsyncSocket,他不仅封状了API,而且还让程序员省去了建立窗口、注册窗口消息和写窗口过程的麻烦,提供了回调接口,程序员只需要重写感兴趣的虚函数,就可以实现异步调用,这听起来似乎很方便. 但是MSDN却说:使用CAysncSocket需要自己管理阻塞,自己处理网络字节顺序等。因为我们执行CAysncSocket的每一次异步调用,都不能保证程序已按照我们的意愿正确执行了。比如你调用Send来发送网络数据,你可能得到的返回值是WSAEWOULDBLOCK,表示你的操作并不能马上完成,不能立刻返回,告诉你不用担心,但是以后是不是真正执行了,你是不知道的。这就需要你每次都检测返回值, 确定发送了多少, 没发送的, 先放到全局缓冲区( 不能在栈上),  在OnSend回调中发送.
CAsyncSocket使用一个隐藏的窗口来处理你感兴趣的网络事件(比如FD_READ, FD_WRITE, FD_CONNECT等), 既然是窗口, 就有线程属性。而一个线程中的所有操作都应该是线性化的;也就是说,这中个线程里,所有的IO操作都是串行的,而且因为是事件模型,所有,所有的操作都将是不阻塞的,事实上,这样的IO线程效率是很高的,当然,编写异步IO程序,必须主要的一点是,这个线程的任何操作都不要有明显延迟,因为你的延迟会影响这个线程所有IO操作。
为什么要这样设计呢, 因为:只有对同一个SOCKET的异步IO进行串行化,才能保证这个SOCKET的操作是严格的。试想,多个线程同时调用Receive(异步), 很可能每个线程得到一部分数据却都不完整
CSocket是为对异步操作的同步处理,比如Send(同步)调用了Send(异步); 而CSocket 对使用也做了限制,最大的限制就是在一个线程里创建的Socket, 不能在另一个线程中调用IO方法。
 上面都是理论说教: 实际使用时, 我是不会去用CSocket的, 异步的socket或是封装的异步类才是商业软件中最需要掌握好的。现在很多文章只提到异步非阻塞,提到receive就是"立刻返回... ...", 实际编程中, 可不是这样简单的。由同步转到异步,除了模型变化外, 还需要特别关注内存的管理, 因为很多操作都必须在全局堆中完成。
比如在一个线程中:
void Test()  // 不在OnRecieve里调用
{
      unsigned char tmp[1024];
      pSocket->Receive( tmp, 1024);
}
如果pSocket对象是同步类, 那OK, 收到的数据就在tmp数组里;
可是如果是异步类 CAsyncSocket则不一样了, 数据不会在 tmp数组中,  实际编程时, 是不直接调用Receive的, 而是在OnReceive中调用, 并把数据存放在与Socket相关连的堆缓冲区,  作为接收数据的缓冲区, 同样Send也应该对应有一个 发送缓冲区, 当OnSend触发时, 就循环调用Send发送缓冲区中的数据, 而且每次都发送到返回WSAEWOULDBLOCK, 系统会在操作系统底层socket发送缓冲区有空间时,再触发OnSend, 你就能再执行发送.对OnReceive事件的处理类似, 只要有数据, 你都循环调用Receive, 直到返回WSAEWOULDBLOCK, 才返回.
这些都是理论, 实际上, 我们还需要关心上层接口的处理. 比如你要实现这样一个基本流程:
  • 客户端发送一条指令给服务器, 服务器回给客户端一个响应, 以此来确定是否登录成功.
我们可能会写个函数
Login()
{
       Send();
       Receive;
}
但是现在是异步模式就需要这样写.
Login()
{
     while(1)
     {
            Send(..)   // 数据有多少, 就直接发多少
     } //  循环Send, 直到返回WSAEWOULDBLOCK或发送剩余字节为0;
      AddMsgToSendBuffer();          // (1)将还没发送的字节COPY到发送缓存区.
      WaitForSingleObject();            // (2)等待OnReceive函数被触发后, 设置的自定义事件对象
      GetMsgFromReceBuffer();     // (3)从接收缓冲区中取得一个包
      Prase();                                      // (4)解析包;
}
因为socket是异步模式,  所以在发送大量数据时,  这个循环Send代码段都不会耗时, 因为不用等待数据通过有限的带宽发送到tcp另一端, 而只是拷贝到网络缓冲区, 当Send返回的值>0时但小于总数时, 再次调用会触发WSAEWOULDBLOCK, 而触发WSAEWOULDBLOCK就是为了将来系统触发事件使OnSend 被调用.
(1)语句调用后(有剩余数据),  系统会在网络发送缓冲区有可用空间时触发OnSend事件, 因为只要连接上服务器, socket的OnSend就会被调用, 在OnSend中,  是循环调用Send的代码直到返回WSAEWOULDBLOCK;
(2)语句是为了正确等待回应,  可以在OnReceive被触发后, Receive数据, 然后根据情况设置一个Event对象有信号即可;
(3)语句完全是内存操作
从上面可以看出,  发送和接收的处理流程不是一样的, 事实上, 发送的流程更复杂, 更不好理解.
另外,实际异步IO代码中,会涉及到的生命周期很大的数据缓冲区,用户必须时刻保证可靠性,换句话说,异步IO编程和负责的内存控制是密切相关的两个知识点。
从上面的分析可以发现,数据的正确发送需要OnSend准确的触发。在OnReceive之外的任何地方多不需要手动调用Receive,Receive是被动的,这比较好理解,而OnSend就比较麻烦。事实上,也可以只在OnSend里调用Send,  其他地方需要发送数据时,只需要将数据添加到发送队列,然后想个办法使OnSend被调用。是否可以直接调用OnSend呢?如果你考虑到了线程的同步问题,你就不会认同这样做了。因为你可能多个线程同时操作发送队列。而且OnSend是protected类型,MICROSOFT这样设计应该是有道理的。那使用窗口消息呢?无法获取窗口消息。请看下面的分析,使用MsgWaitForMultipleObjects是个好办法:
void CAysncSocket::OnSend(int nErrorCode)
{
 // 首先要了解的是,Socket的处理函数都是在同一个窗口事件中, 也就就同一个线程上.
 // 死循环轮询   ---- 这样该线程就被这个函数永久占用,OnReceive不会被调用;
 // 1ms定时轮询  ---- 理论上讲, 最坏的情况下, 会引起发送的延迟( <1 ms)
 // WaitForSingleObject等待Event内核对象, 同样会导致Send和Receive的延迟;
 // MsgWaitForMultipleObjects等待Event内核对象,而在有其他窗口消息的时候能处理其他消息.这样就能够在需要发送的时候快速发送,保证了效率,也能确保OnReceive等函数及时触发。

}