duanjigang 发表于 2011-01-01 22:15

谈谈你接触的通讯程序的模型

本帖最后由 duanjigang 于 2011-01-10 16:33 编辑

################2011-01-06
在1,2,4,5,67,22楼对目前涉及到的三种模型或者说程序的实现方法,做了详尽的描述,欢迎大家积极拍砖,提出改进建议,或者发起实践活动来实现这样的设计思路,并与我们分享您的经验。

推荐下34楼的做法,个人感觉也很不错
########################

应用程序也做了不少了,特别是在通讯这块做了好几年,好几个项目,来来回回,思想和选择经过了好几番转变,经常会想起一些技术上的选择问题,不知道该如何取舍,今晚屋子比较安静,整理下思绪,发起这个话题,顺便听听大家的看法。

   刚开始接触网络编程时,写的最简单的通讯程序,就是server-client一对一的模型,可能还不知道有非阻塞和select等概念,只是觉得测试数据发送过去了,接收到了就很高兴,当面临多连接,多种消息类型时,就开始头大了。大概07年初的时候,我当时的想法,就跟前不久一个同学的设计思路一样,对于一对多的通讯程序,服务器监听,接受到一个客户端请求时,就为该连接创建一个线程,并且把获取到的连接套接字交给线程去使用,当子线程完成通讯或者出错后,就会关闭连接套接字,然后退出。这个模型在我当时看来很厉害了(应该比同学问那个,给每个新获取的连接套接字创建一个进程强点吧,呵呵),当时这个程序也是在客户那里跑起来,基本上线了,但是印象里出了不少问题,多线程程序在我印象里更不好调试。。反正是被这个写法折腾的够苦,后来一提到多线程处理多连接,每个线程一个连接的做法,我就头大。呵呵,不知道有没有人这样写,谈谈自己巧妙的设计或者遇到的问题?
第一种模型,是否可以用以下图来表示?

duanjigang 发表于 2011-01-01 22:29

本帖最后由 duanjigang 于 2011-01-02 10:43 编辑

呵呵,早上看看昨天的文字,发现用词很土。:wink:
后来看到了第二种写法,觉得不错。

客户端程序分为两个线程:连接线程和数据处理线程。
在提出select和poll或者epoll之前,针对多个连接的通讯,大都使用的非阻塞socket吧。

   先说说连接线程,连接线程和数据线程共享一个server列表,或者叫一个server list.这个server列表的每一个节点,代表了
一个将要连接过去的主机,同时还存储了该跟主机相关的信息,最常见的是IP地址,编号,socket描述符,发送缓冲区,发送数据指针,数据总长度等信息。连接线程要做的事情很简单,遍历主机列表的每一个节点,如果发现某个主机的连接状态处于“未连接”状态,则尝试连接该主机,当然这个连接也是有超时限制的。如果连接成功,存储该到该主机的连接的socket描述符,修改连接标志位。标明该主机已经被连接上,其实也就是通知数据处理线程,该主机可以进行通讯了。
      再说数据处理线程,数据处理线程顾名思义,就是处理数据线程,从客户端的角度来说,数据处理线程大多数情况下是做出发送数据的状态。在这个模型中,也是如此,数据处理线程也遍历主机列表,检查每个主机的连接状态,如果该主机已经连接上,并且发送缓冲区有数据未发送出去,则使用非阻塞的方式向该主机发送数据,如果发送失败,则关闭socket,并且修改主机的连接状态为未连接状态,以告知连接线程,这个主机已经断开了,你丫需要重新去连接它了。如果发送成功,则更新发送缓缓冲区的数据指针,注意,这种发送没必要在一个while循环中一直发送,直到发送完成,个人觉得发送一次就可以了,不过好像也有人尝试发送最多三次。个人理解,为了减少在一台主机上的长时间发送而造成对其它主机数据发送的延迟影响,还是只发送一次好。
第二种方式的客户端程序可以用这个图来描述下。

yangsf5 发表于 2011-01-02 01:25

图是红叉叉。。
没必要1个连接一个线程把。

duanjigang 发表于 2011-01-02 09:01

图是红叉叉。。
没必要1个连接一个线程把。
yangsf5 发表于 2011-01-02 01:25 http://bbs2.chinaunix.net/images/common/back.gif


    嗯,是的,呵呵,最初是这么设计的,而且现在有些刚接触网络通讯类程序的人也会下意识的去这么考虑。
我是想从一开始到现在,把所有遇到过的写法大致上都总结下,大家一起讨论每种的问题,优缺点和性能比较。

duanjigang 发表于 2011-01-02 11:03

本帖最后由 duanjigang 于 2011-01-02 11:06 编辑

接着来说第二种模型的服务端程序,对于服务端程序来说,跟客户端程序类似,也是两个线程工作。
      一个连接接收线程,一般来说,都是主线程来负责,创建socket后,bind,listen。。然后在一个死循环当中尝试accept来自不同主机的连接请求。每收到一个连接请求,就把该连接的IP地址,端口等信息存储到连接列表当中,并且初始化接收缓冲区等信息。
    另外一个是数据接收线程,该线程遍历连接列表,尝试从每一个连接读取数据,如果读取失败,则关闭连接套接字,释放该连接节点。如果接收成功,则检测是否是一个完整的消息,是的话,则进行处理,不是一个完整的消息,则继续缓存,更新接收缓冲区的指针。
服务器端程序相对客户端程序来说,处理的事务少点。
服务端程序的结构和工作流程可以用下图来表示下:

duanjigang 发表于 2011-01-02 11:39

第二种双线程的方式已经是一种不错的模型了,在实际中应用也不少,上面量段文字和图片说明了服务端和客户端的基本工作方式和数据结构梗概。不过有些细节还是需要注意并加以处理的:
我们都知道,服务端和客户端都是采用非阻塞方式进行数据的发送和接收的。这样就有一个特点:对于专注从事于数据发送的客户端程序来说,如果客户端到服务端的网络出现中断,客户端的发送操作会出错,这时,客户端程序能够发现这个故障,并且能够进行正确的处理(关闭socket,后面尝试重新连接)。而对于服务器端程序来说,因为它遍历连接列表进行数据接收,采用的是非阻塞方式,即使客户端的连接已经断开,他调用recv操作,返回的结果跟没有收到数据是一样的,都是0(这个结论我应该没有记错吧:em17:,如果有错,望指出)。也就是说,服务端通过只读取的操作是无法发现客户端的断开状态的。从系统资源角度考虑,会造成很多未完全关闭的连接,另外,从服务端程序来考虑,也是无法忍受的,比如服务端采用的连接列表长度为1000,如果有几十个客户端断开连接并且重新连接几次,这样在服务端的连接列表里会增加上百个貌似正常实际上却是无效的连接节点,时间再长点,就会导致服务端列表的溢出,另外,从后面的话题的角度出发,列表过长,会导致接收线程遍历的节点次数增大,浪费过多的CPU时间。

duanjigang 发表于 2011-01-02 11:56

本帖最后由 duanjigang 于 2011-01-02 19:10 编辑

接着6楼的问题。
既然只读取的操作不能发现连接的断开,为什么不增加一个发送操作?是的,这个想法很好,不过我们要明白,这个发送操作的目的只是为了检测客户端连接的状态。
既然服务端会通过发送数据的方法来检测客户端的在线状态,那么客户端就不能置之不理,礼尚往来还是有必要的,因为如果客户端不理会服务端的发送数据的话,服务端发送到客户端接收缓冲区的数据就不会被读取,如果不存在超时丢弃或者覆盖的话,接收缓冲区总会被写满的。因此,客户端还是需要去读取服务端发送过来的检验数据的。
   跟据上面的情形,可以增加这样一项工作,在服务器端的主机列表的每个节点上存储接收到客户端数据的最新时间,然后定义一个超时时间,如果超过这个时限没有收到来自某个主机的数据,则尝试向该主机发送一个握手消息,这时就能检测是否连接已经断开了。
   对于客户端而言,为每一个服务器保存一个发送数据的最新时间,如果最后一次发送数据的时间到目前的时间差超过这个时限,则尝试从服务端读取握手消息,这样就能把服务器端发过来的握手消息接收过来,比较理想的解决了上面讨论中可能出现的问题。
   这种非阻塞的模型,有些人认为是比较浪费CPU时间的,不过通过一些方法可以适当降低CPU使用的,在连接列表不是特别长,并且每个连接都不是特别空闲的情况下,性能还是很不错的。
后面有时间,我把这种模型的实现整理下,放出来大家可以参考下。

Godbach 发表于 2011-01-02 17:58

duan 兄好文章,受益匪浅。

duanjigang 发表于 2011-01-02 18:59

duan 兄好文章,受益匪浅。
Godbach 发表于 2011-01-02 17:58 http://bbs.chinaunix.net/images/common/back.gif


    呵呵,只是想把自己遇到的各种情形列举下,希望大家能够讨论每种模型的应用范畴和优缺点。今天还没写完,明天接着总结:wink:
老赵多多提点意见啊

duanjigang 发表于 2011-01-02 19:12

到目前为止,提到了两种模型,欢迎大家积极拍砖讨论喔。
关于select 和epoll等模型,欢迎熟悉的朋友积极发表感受。我明天写写基于ICE的模型(也是我目前正在使用的方式)。:wink:
页: [1] 2 3 4 5 6 7 8 9 10
查看完整版本: 谈谈你接触的通讯程序的模型