书到用时方恨少,绝知此事要躬行--谈TCP/UDP编程

原来以为自己对tcp(udp)/ip编程还算比较了解,因为自己也亲自拜读过《Unix环境高级编程》、《TCP/IP协议详解第一卷》这些计算机界圣经一般的数据。近来经历过一次和高中同学,现在已经是一个部门的同事,在一起解决他模块中的一个错误,才知道自己其实对TCP/UDP编程所知还不算深,竟然在一个不错的公司里面工作三年后,还是回答不了他的提问"在tcp连接的recv操作是否会出现一次返还多个物理的tcp package?!"因为TCP协议的基础协议是IP协议,我们在TCP上发送数据,特别在发送大量、大批数据的时候,就体现为多个IP数据报在这个连接上跑来跑去。

  他说TCP是否具有这样特性对他的应用有点影响,而他的程序也是由多个前辈在开发几年后遗留下来的。他在有一次大数据量测试的时间发现,自己的服务器程序竟然在有一次收包recv函数调用过程中会收到客户端发送过来的两次请求应用协议数据,前一个请求数据是完整的,后一个请求数据只有一部分的数据。他的程序原来设定recv缓冲区大于任何客户端发送应用协议数据的大小,假设一次recv操作就返还一次客户端信令请求发过来的数据。
  
  在原来客户端请求量不是很大的时间,每次客户端发送数据包过来,间隔性比较大,recv操作都返还一个完整的客户端请求数据出去,而不携带后续请求的数据。这样的现象具体原因可解释为,由于recv缓冲区大于任何一个客户端请求应用协议数据的大小,而且在网络条件比较好的情况下,通常客户端请求协议数据难于大于物理网络上要求IP数据报的最大值;同时,不要忘了前面的间歇性的特征,两次recv操作之间可能没有任何客户端请求数据上来!
  这样,recv操作会完整地返还一个客户端请求协议数据出去。但是,在我同学的模拟测试的情况下,客户端和服务器端都跑在本地机器上,模拟程序可以模拟客户端在短时间内发送多个包上来。这样,在客户端请求比较多的情况下,在上层业务两次调用recv操作之间时,可能tcp的操作系统缓冲区里面,已经有多于一条的客户端请求协议数据过来。发现由于tcp的recv机制是如果操作系统缓冲区内有数据或每次客户端数据过来即解除阻塞状态返还上层业务,而且数据的最大返还数据量为上层业务设定recv的最大缓冲区大小,也就是在某些情况下返还的数据可能小于应用程序指定的长度。由于TCP这样的机制,在我们前面所描述的场景中,如果在两次recv操作之间来了多于两个的客户端请求数据上来,就会导致一次recv返还多于一个的客户端请求协议数据出去,但是怎么解决这个问题呢?为什么tcp是这样的特性呢?后来也曾经迷茫很长时间!

   在后来,为了保持住自己自诩为高手的脸面,就耐下心来,详细研读了tcp/ip详解,unix网络编程和msdn,以及一些开源软件怎么处理客户端tcp请求应用协议数据。有时一点虚荣心,如果处理的得当,也会转化为强烈的求知欲的,呵呵!当初定位研究那些开源软件就选定那些客户端请求应用协议会在同一条tcp连接上一直发送请求数据,例如ftp 21端口上命令数据和短信的smpp协议。研究后,才发现自己对于tcp(udp)/ip的网络编程还了解的很少,有时还相当地无知,可怜自己一直还以为很了解这一领域,遂就联想到一句古语“书到用时方恨少,绝知此事要躬行”!
下面简单总结一下研究的精华内容,呵呵,文章写的臭长臭长估计看的人的数量就会下降。就象霍金在自己的科普读物《时间简史》中说到,科普书中出现数学公式会降低书的销售量一样。

tcp与udp一点简单区别:
1.首先tcp可以看成是一种流协议,没有报文大小,这也可以体现在tcp报文头的格式上,tcp报文头没有数据大小的定义;
2.与tcp通讯相对应的udp通讯,udp报文头具有数据大小的说法,具有报文边界。
  所以,tcp通讯一次recv只能是有多少数据取多少数据,通常意义上取数据的大小要由上层业务决定,但是udp通讯一次recv操作,一般来说也只能返还一个完整的udp数据包,而且最大也只能返还一个完整的udp数据包,不可能将recv操作返还数据跨越到下一个完整的udp包来。

tcp与udp recv操作的一点简单区别:
  由于tcp是可靠的数据链接,而且可以看成一种流协议,在现在Java的IO框架里面,就将socket体现为一种stream。如果在此流上连续发送多条客户端请求,而且客户端每个请求具有一定的独立性,那么分割每个客户端请求的就需要一定意义上的应用协议。通过前面的介绍,我们知道实际上我朋友的应用程序使用了近乎于简单和幼稚的的通许协议,它设定recv缓冲区为最大客户端请求信令缓冲区大小,认为每次recv操作取到的数据都是一次完整的客户端请求数据。虽然在理论上这个协议是错的,但是,我们会看到有时错的东西不一定在实际中是错误的,可能因为多种因素的误打误撞,非常巧合地成功了!对于这种程序取得的成功,我只能说他们是在利用某种巧合进行编程,而这种巧合就造成他们程序的依赖环境必定不是足够的简单、足够的通用的,而在应用到一个新的环境中时就会出现水土不服。我们经常有些程序员抱怨为什么程序在这里是对的,在那里就是错的,他们可能根本就没有想到,所有的错可能都是自己的错,因为计算机中没有灵异事件,所有的错几乎都可以看成是一种程序员的错误的累积和放大造成的!

  这个协议,我再次强调一下会在某些场景下表现的非常正常,例如客户端请求具有间歇性,且网络条件比较好,一次客户端请求很有可能就在一个IP包中就完整发送过来了,这样他这个工作逻辑就运行的毫发无错,每次读到的数据就是一个完整的包,他程序对自己运行环境的假定,就在这么多因素的扭转下变得满足了。但是,在我同学的本地机器测试中,由于利用模拟器在本机进行发包,这样就破坏了他的工作协议,即有很大可能性,在一次recv操作间歇有多于一条的客户端请求聚集到tcp缓冲区中;同时,客户端和服务器端跑在同一台机器上,在两次Server端recv操作间也会比在不同机器上运行时要大。在这种情况下,每次recv操作返还的数据就出现跨客户端请求协议数据的情况,就造成了它原来工作协议的不满足。
对于利用TCP这样的应用场景,我们如何来解决这个问题呢?为了快速地找到答案,我就选择ftp FileZilla和smpp API两种开源代码,发现他们对于tcp数据通讯的处理,这两种软件采取了截然不同、而又相对比较典型的处理策略。现总结如下:
 1.FileZilla ftp采用每次如果出现多的数据就进行缓存,缓存数据和下一次recv获得的数据再进行拼装,每次从这些可能拼装出来的数据中,取出客户端请求的ftp命令。
 2.SMPP API采用按照标准的协议,每次读取一个固定长度的报文头。它是通过封装recv操作,达到每次应用application_recv函数返还时就是返还应用程序指定大小的数据长度出来。读取出客户端请求报文头后,就按照报文头中指定的协议数据长度进行下次读取,依然是必须返还应用程序指定数据后才能返还。就象在一条不间断的流上进行滑行一样!
由于朋友的程序具有SMPP API工作特点,当时就简单地采用了SMPP API的tcp处理策略,毕竟每次判断缓存以及和缓存数据进行拼接,在我认为可能是一个稍微复杂一点的算法,呵呵!

  其实写到这里估计你还不意味写这篇文章有什么价值,有可能你已经在以前就了解到了tcp和upd通讯recv的处理策略。我最初其实也觉得如果只是自己再深层次地认识了一下tcp/udp编程的特性,也最多只能算作一次经验总结罢了!但是,真正触发我要把它写成一篇博客是因为后来随之发生的事情。就在我准备固定把TCP当作一种流概念去处理,当作一次知识储藏的时间,同事朋友发现我们现在的处理策略不能战胜一次数据错误,也就是如果出现一次客户端请求数据错误,即有可能在以后导致一步错步步错。因为根据流的特性,如果前面分割都已经错误的情况下,后面依据分割点的解释也就可能不对了。但是,我的朋友发现他以前的程序却有可能从这样的数据错误的困境中恢复过来,而相反,我们后来的处理策略却很难在灾难中重生。
  
  当时,我也很迷惑,就建议我同事朋友可能只有告诉客户端重新建立一次连接,就可以让数据通讯正常起来。但是,后来,在同事朋友研究了他以前的代码后发现,在TCP连接可以被当作一种流来处理的同时,还具有另外一个比较微妙的特性。他觉得在我们把TCP当作一种流来处理的时候,如果在这个流上数据具有间歇性,也就是这条流上不总是有数据,数据可能是时断时续、时有时无的,数据和数据间又具有某种独立性。在这种场景下,是虽然TCP可以逻辑可以看做一种流,但是却是一种非致密性、并不十分“连续”的流。这样,我们就在Server端接受客户端请求数据发现出错的情况下,可以快速地多“吞”几次客户端数据,这时必利用原始recv,只要有数据就返还,不一定必须返还应用程序指定长度大小,也就是读一次recv操作后,就进行抛弃,不管其是正确的、还是错误的,以达到清空操作系统TCP缓冲区的目的。进行这样的尝试行为后,我们后来改造的TCP Server端就有可能象以前的Server端实现,在数据间歇性的场景下获得自救。Server端很有可能在下一次执行recv的时候,就很有可能“幸运”地遇到一个客户端请求的开头数据,这样秩序就重新开始了,呵呵!
  
   正是这种TCP连接在数据间歇性场景下“自救”的发现,撬动了我去尝试写这篇博客!由此,感谢我那位朋友I_Will_Go!!!

   从这次经验获得中,我觉得世上却是很多事情具有很大的相像性。例如在人类历史上,一次困局和乱世的重生,大都经过一次比较有自伤性的行动,就象上面的举的TCP通讯例子,靠几次“猛吞”客户端数据,抛弃一切,无视良莠,从而成功获得自救!
  另外,从这里面我们也可以看到,如果一个模块运行多年依然还能够运行的话,一定是有它成功的地方。我们作为后来的维护者,千万不能随便都对那些代码嗤之以鼻,不愿意去研究研究,在以前曹雪芹就讲过世事洞明皆学问,不管好的模块和差的模块,去研究它,可能都有它作的比较好的地方,三人行必有我师。

阅读更多

没有更多推荐了,返回首页