图 2‑1 socket 2.0体系结构
2.2. mfc对socket api的封装
在mfc中,有两个类对socket api进行了封装,一个是casyncsocket,另一个是csocket,其中csocket是从casyncsocket继承而来的,因此,准确地说,只有casyncsocket才是真正对socket api进行了封装。封装后的socket抽象成为一个对象,使得程序员可以将socket和mfc的其它类融合在一起。
如果要单独使用casyncsocket类,要求编程人员对windows的socket有较深刻的了解,因为它只是对socket api进行封装,编程人员还要处理应用当中可能碰到的问题,如阻塞,字节顺序以及unicode至多字节字符集(mbcs)的转换等。而且这些问题处理起来并不容易。
庆幸的是,csocket使用起来就方便多了。一方面,它从casyncsocket类继承而来,相当于封装了socket api;另一方面,该类处理了上述的问题,大大减少了编程人员的工作量。除此之外,csocket还可以与csocketfile和carchive类一起使用,在某些场合极其方便。
3. 实例浅析
用socket实现tcp/ip通信的方法有多种,从大体上来说,可分为mfc和非mfc两种。顾名思义,mfc方式就是利用mfc中定义的两个类来实现,而非mfc则是指不利用那两个类实现。就方式而言,有同步和异步之别。而且,客户端与服务端的实现也大不相同。
接下来,我要谈到的例子就是笔者实现的一个客户端通信模块。该模块功能不多,一个是数据的接收和发送,另一个是重连机制,因为考虑到应用场合,不允许出现网络连接断开后,重新启动程序,而要求程序有自动重连的能力。
这个实例中一共有涉及到三个类(如图 3‑1所示),其名称及功能如下:

图 3‑1模块类图
csockclient,完成连接建立,数据的接收和发送功能;ccommsvr,完成自定义的协议功能,数据打包、数据包解析及数据的分发等;csyncdata,处理线程间的数据同步问题。
在这里,我想结合一个客户端实例(mfc方式)以及实现过程中遇到的一些问题和读者一起探讨一下。
3.1. 由消息驱动数据接收
接收socket数据时,一般来说有几种方法,一是用一个线程循环调用select函数,通过返回值判断是否有数据到来或是出现socket错误。另外一种方式就是实现casyncsocket提供的虚拟函数onrecevie,在该函数中调用其它函数接收数据,还有就是通过调用asyncselect函数,请求socket将消息发送给指定的窗口,然后在窗口的消息处理函数中调用相应的函数接收数据。
对于第一种方法,大多数在非mfc方式下使用。而在mfc方式下,主要用后两种方法。对后两种方法而言,从根本上来说是一样的,只是接收消息的窗口不一样而已。也许大多数读者已经发现,实际上socket本身有一个叫socket notification窗口,它是不可见的,但是可以接收并处理消息。
那么究竟如何区别后两种方法的使用呢?笔者认为,对于简单的客户端来说,可能数据的接收与发送都是在一个线程中完成的,这种情况下,可以考虑请求socket将消息发送到主窗口,然后在主窗口中处理这些消息,如连接、接收数据等。即采用第三种方法
而对于得杂一点的客户端来说,由于数据的接收一般来说是在与主线程不同的线程中处理的,此时考虑到程序的模块化,不宜将socket消息发送到主窗口中处理,因此应采用第二种方法,即实现casyncsocket提供的虚拟函数onrecevie。
3.2. 线程间的数据同步
线程同步是一个多线程编程中的一个重点和难点。线程间的数据同步有一个原则,就是要把要同步的数据集中起来考虑,尽量减少要同步的数据。同步的目的是要保证数据的有效性和完整性,直观的说,就是不出现两个或两个以下线程同时访问同一数据块。