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

出处:http://bbs.chinaunix.net/thread-1840615-1-1.html

这篇博客是从一个帖子整理而来,很不错,记录下,以备将来之需。

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









呵呵,早上看看昨天的文字,发现用词很土。:wink: 

后来看到了第二种写法,觉得不错。


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


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











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











6楼

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

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

11楼
我觉得多进程也是一个可以考虑的设计方向,很多时候,比多线程优
势更大。线程的优势在于调度和资源量,问题在大量多线程系统本身
的复杂和调试的困难。
两个实例:1 是postgresql的服务模型,进程+shared mem。共享内存
只处理全局状态,每一个进程自我处理资源,现在大部分OS的任务切
换性能不在是瓶颈。代码很清晰,性能也很高,网上有个图,可以看看。
 不好的一点是内存消耗量稍大。如果需要控制进程数量,一般前面有
 连接池。
2 是基于actor模型的通讯方式,如erlang,(虽然此进程非彼进程,
但是实质是一样的"进程”)众所周知的是它的扩展性和分布特性。
另外,每进程的耦合较低,如果不需要处理事务的话,我更倾向于进程,
这样可以在适当的时候轻松的把计算分布到其它物理资源上处理(比如erlang)。
我做的关乎server的设计几乎是这个模式的。

15楼
回归到你给出的第一种图,不过线程换成进程。
我过去倾向于用线程,但是尝试很多之后设计和调试都随着状态或功能
越复杂而越复杂,再加上我个人目标领域的关键约束不在内存上,并且
感谢现代操作系统,进程和线程切换的代价已经十分靠近,所以慢慢回
归到最简单的方式,现在看来真是越简单越美。直到了解了erlang的处
理模式之后,更加深了这个认识。

呵呵,感冒耽搁了几天,今天把使用ICE中间件的这种方式补充上来:wink:


第三种方式,结合ICE中间件进行设计。
ICE提供了一个多系统,多语言互通的中间件,通过使用slice语言定义
一个.ice文件来实现不同程序的通讯数据结构和调用方法的定义。
然后调用slice2XX(xx=cpp,java,php..)来生成对语言的代码。对于开发
人员来说,只需要把生成的java或者cpp代码包含到自己的工程中来,客
户端填写数据到对象中,调用定义的函数就行,服务端继承接口中定义的
虚函数,实现数据处理即可。调用也十分简单,从感性上理解,客户端调
用数据处理方法,服务端的方法就被调用了,它是一个类似于RPC的机制。
    有了ICE这个强有力的助手,数据发送和接收变得异常容易了,然后
我们需要处理的就是连接的维护。
    服务器端不需要维护连接,因为对他来说,服务端就是一个类似回调
函数的机制,它并不关心或者不清楚在某一个时刻,自己会被哪个远程主
机调用。客户端需要维护连接,因此,类似于模型2中的方法,在客户端独
立一个线程,一直尝试连接未连接上的主机即可。
    讲讲这种模型下我的一般设计方法:
    主线程启动后创建两个线程,一个客户端线程,一个服务端线程,服
务端线程运行后,就会处于阻塞状态,一直等待消息的到来,客户端在运
行后则会一直尝试去发送数据,而主线程下来要做的事情就是:遍历它跟
客户端线程共享的那个主机列表,尝试连接每一个没有连接上的主机。
    这就是第三种模型,希望有改进意见的朋友积极发表看法和意见,谢谢!

第三种模型的一个好处就是,由于ICE使得客户端和服务端的代码变得
简单起来,我们可以很轻松的把客户端的功能和服务端的功能用一个程
序中的两个线程来实现。这相对于1和2的模型来说,方便多了。

24楼
如果是非阻塞,正常情况下如果没有数据,recv函数会返回-1,并且
将错误代码置为EAGAIN,如果recv返回0,其实代表的是远端socket被关闭了
我也对服务器程序设计感兴趣,大家多交流。

26楼
这个话题开的太好了,我对这个话题也非常感兴趣。
一般我常用的就是第二种方式,专门开一个监听线程来处理连接,对
已经连接好的分到空闲的工作线程中处理,在实际的过程中,我也遇
到了客户端不正常断开,造成资源浪费的情况,我采取的方式是再开
一个监控线程,来扫描判断每个连接的状态。

27楼
    呵呵,对您的说法我再次做了验证。
对于阻塞和非阻塞的socket,如果server端一直保持recv的操作,对
于发送端的异常退出,close并且正常退出,或者没有数据,server
端通过recv是不能检测到的,当返回值<0时,errno都是EAGIN,因此
能不能这么说,纯粹的recv工作的socket是不能检测对端的退出的?
如果这样的话,还是需要在没有收到数据一段时间后通过发送心跳
包来检测连接的状态的。:wink: 
不知道我的说法有无纰漏?或者您再做做实验,指出我可能存在的
问题,谢谢!

30楼
恩,心跳协议肯定是必不可少的
不过我想心跳协议重点不在于检查连接是否端掉,连接是否中断从系
统里面是肯定可以探测到的,
关键心跳协议能检测到“被远程调用端”是否“死”掉,比如说请求一个
远端的服务,连接虽然没断,
但是服务器已经死循环了,这个时候通过心跳协议的超时机制来检测,
这时就应该主动把连接断掉,然后重新请求一个可用的服务。

31楼
我觉得ESB是我见过的算是比较好的通讯模型了

33楼
我常用的通讯模型:
多reactor,用round_robin处理reactor的负载均衡,reactor与TCP连接为一对多的关系。
发送的数据使用消息队列,单独线程发送,处理超时重发以及EWOULDBLOCK重发问题。
心跳协议必须的,定时器可以集成到reactor里。
服务器间通讯现在直接用protocol buffer了,方便。

34楼







这个是我在最近打算换工作,总结以前项目的时候随便画的。
服务器主要分为两个层次:通讯层和业务处理层。(采用epoll网络模型)
在通讯层又分为两个层次:
1)与客户端的数据交互
      该模块只负责接受请求、接受数据、发送数据(各为一个独立的线程),
接收到客户端的数据之后将数据放入到数据队列中,发送数据的时候从数据发
送队列中读取数据进行发送;
2)与业务层的数据交互
      该模块负责将从客户端读取的数据发送给业务处理进程和从业务处理进
程读取数据,与业务处理进程通信采用的方式为:用fifo实现事件通知机制,
用共享内存实现queue达到数据传输的目的
      业务处理层比较简单,用select接收到事件通知后,读取数据,处理数
据,再讲处理结果放到数据队列中,然后在用fifo来通知通讯层去读取数据,
处理模型为reactor模型;
通讯层与业务处理层的数据是轮训方式nconnect++/业务处理层进程数。


同请教33楼和34楼两个问题:
第一:你们的通讯程序独立于应用程序吗???
也就是说把通讯做成纯粹的通讯程序,要增加应用业务,对通讯而言只需要修改配置文件或者根本什么都不需要做?

第二:通讯程序发送的数据是分包的吗?对于大数据量发送怎么做的?比如A应用通过我们的通讯程序想发送一个文件到B应用,怎么做的?
谢谢!

第一:你们的通讯程序独立于应用程序吗???
独立,一个前置通讯网关对应多个应用服务器。

第二:通讯程序发送的数据是分包的吗?对于大数据量发送怎么做的?比如A应用通过我们的通讯程序想发送一个文件到B应用,怎么做的?
应用层分包?如果是TCP,应用层不管分包的事,如果是UDP的话,就要考虑分包,要用到应用层协议里的frame_seq来维护包和包之间的上下文关系。发送文件,linux下用sendfile,windows下TransmitFile。

关于通讯模型,最近一直想用epoll_reactor来模拟proactor,boost::asio是实现了,可惜C用不上。


42楼

多线程模型就像人弹,有人要死,却拉上一大堆垫背的。

我再说一遍:多进程模型是用于每个进程干自己的事,互相不影响;多线程模型是用于所有线程协调干一件事,每个线程负责一个部分。

多线程模型中的线程,彼此的工作内容应该是不同的,同时又是互补的,大家完成的是同一件事的不同部分。基于这样的理解进行设计,就不存在“人弹”的可能性。

所以在一般通讯服务程序的场合,绝对应该使用多进程模型。

进程与线程,看着相似,实际的出发点和作用是不同的,所以不能乱用,要根据应用模型进行选择。再说得浅显点就是:在一个多线程模型中,任何两个线程都不应该使用同一个函数作为入口点!


   恩,大致明白了。有个问题喔,通讯程序相当于做成了一个独立的中间件了。这样的话,应用程序其实不必再关注通讯程序使用到的协议了吧?
如果你们的通讯已经独立于应用了,我想应该是这样吧?那么,这时是不是都应该用TCP来实现?为了可靠起见。谢谢

    一个进程崩溃,毁掉的是一大批事情,而不仅仅是出问题的那一个,这不合理。
而且在这种模型中,你无法中断一个已经出现问题的独立事情。

进程和线程在系统开销上的那点差别,远远小于你应用程序写得好与不好的差别,不同的人为同一个事情写程序,开销差别都很大,何必在乎进程和线程开销间的这一点差别呢?真在乎系统开销,就先认真写好自己的应用程序吧。

所以,一定要选择适合的模型做适合的事情。

    阿村说的很精辟,应该是多年的经验感言!
但是有些人出于节省代码量的想法还是违背这些原则了,比如为了使用公共的全局变量,节省代码量,把通讯的server端和client端做成了两个线程。
呵呵,我就这么搞过 
关于数据处理部分,现在完全做成了独立的进程,每个消息处理插件一个子进程,也是充分考虑到“人弹”这个因素了。
看样子通讯程序本身,还应该再分离出来。这样才合理。

多线程。。。一旦core掉就悲剧了。。。。。。

所以我们的都是多进程的,而且有专门的维护进程定期监控各任务进程的状态。

我们的模型很简单,tcpSvr接到链接后派生相应的XXXTask进程完成请求的功能,完成后推出。或者可以预先启动数个XXXTask进程等待tcpSvr的任务分派。
简单的异常监控,如果是unix下的采用父子模式,我目前用的一种监控方案是用子进程事件来管理,比如user1,user2,SIGCHLD,父进程就可以作为各子进程的维护进程。这样不用定期,反正子进程有异常了事件就过来了。
当然,这样做是比较粗略的监控方式。如果要做精细监控就不合适了。

优势不少,劣势在内存上。
一般用途的通讯服务器必然涉及并发处理,所以很难看到有单线程的通讯服务产品。只要涉及并发,不管是进程还是线程都要切换。比较新的unix/linux基本切换开销相差很小,当然并不是说没有差距。我测过适合我的服务负载的吞吐差别不大,PC 4核4G,测试目标好像是2000还是3000并发,记不太清楚了。
摩尔定理也是设计上的预测,如果可以通过加强机器性能,我宁愿选择代码的维护,扩展和进程任务的相对单一性。
就如同用烂的OOP一样,更多的设计增加了层,也增加了复杂性。当然,依据设计对象不同也可能是必须的。

关于线程和进程,从来没有说谁只能用来干什么,就像你用牛刀杀鸡,也未尝不可,方便不方便、顺手不顺手,自己感觉就是了。
看一看经典的教科书,讲线程部分的举例,都是用的生产者/消费者模型,从来没有用多通道并行模型的。再看看经典的开源软件,比如apache之类的,也都是用多进程而不是多线程的。
在很长一段时间里,我也同样对多进程和多线程之间的区别没有概念,直到有一天设计一个产品时我忽然明白了,多线程和多进程应该是面向不同应用模型的,它们的设计出发点就有不同,是用来干不同的事情的。于是我在这里说出来,就是希望别人少走这样的弯路。
如果你一时还接受不了,那就在以后的工作中慢慢摸索和体会吧。
   呵呵,有两点请斟酌:
第一:是否应该使用多线程来处理每个连接的请求?
第二:是否应该动态创建线程来处理每个请求?为什么不在初始化时创建好一个线程组,在接受连接的时候只是去分配任务给线程,这样效率高点吧。

关于第一个问题,我现在的做法是数据的接收和发送各用一个进程,因为他们独立,并不存在什么协作关系。(这大抵也是按照村夫说的原则来考虑的吧)
数据处理,是用别的进程来完成,也就是说,我并不支持用多线程来处理请求或者数据,具体原因,参考“一介村夫”等人的回帖。

关于第二个问题:我以前做过一个程序采用的是多线程处理,目标就是通过多线程来自动登录2000台服务器,检查这些机器上配置文件是否正常。
一开始想到动态线程,就觉得麻烦,因为我写代码喜欢按部就班的步骤,先设置初始状态,然后开启工作状态最后是结束状态。而且对于所有模块都是这个原则,如果在数据已经到来时,创建线程,初始线程的状态,这样对我个人来说,感觉逻辑很乱,而且动态创建线程对于响应要求较高的应用是有效率问题的。
我当时的做法是:2000台机器,配置文件中设置线程个数,比如50个,然后每个线程能分配40个节点,然后给每个线程都分配了独立的任务节点来存储数据,这样就能达到完全的并发独立的去执行,一台机器的执行任务需要3秒钟,40个节点最快120秒,当然最后实际时间是4分钟多(我想这是由于CPU个数不足50个,不能达到完全的并发和网络资源的限制引起的吧?),不过这个结果对我来说已经很好了。

说面得瑟了这么多,归根结底一个目标,设计程序时,尽量避免动态的开销和申请资源,在初始化时把能做的事情做完,这样可能你就能对程序执行的结果和时空消费做出估算了。而且也利于程序的稳定。呵呵


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值