如今,但凡说精通网络的,第二个意思就是“精通TCP”,事实上,很多自称精通TCP的家伙们只是精通socket接口而已,对TCP行为精通的并不多,笔者也不算精通,但绝对是中等以上水平。如果你真的精通TCP行为,那么本文不读也罢,直接发邮件给我,我们切磋一下,如果只是了解socket接口,那么建议读本文,然后一定再看一下《TCP协议疑难杂症全景解析
0.UDP协议和TCP协议 UDP是用户数据报协议的简称,对于分组交换网络,它实际上扮演了传统邮局的角色,而TCP则是扮演了电话运营商以及物流公司的角色,对于分组交换网络而言,UDP要比TCP更加基本一些,可以说,TCP则是实现一种基于流的通信过程,在IP这个数据报协议之上,TCP和UDP分别实现了更高层的“电路交换”和“分组交换”。
1.带连接的udp(connect udp) 很多人都以为UDP连接不连接无所谓,实则不是这么简单,但凡技术上的事,没有无所谓,除非你能给出比让你无所谓多一倍的理由。

1.1.效率

在操作TCP/IP协议的时候,说白了就是编写基于网络通信代码的时候,你必须知道你所使用的协议是在哪里实现的,对于大多数的操作系统,协议栈都是内核的一部分,因此它们是在内核空间运行的,这就涉及到一个会影响效率的问题,那就是内核空间和用户空间的切换过程你要了解,对于x86处理器以及大多数其它处理器,这个切换是比较耗时的,涉及到上下文的save/restore,因此如果你的应用是效率优先的,那么就要想办法最小化切换次数,这还不够,还有就是如果切换避免不了,那么就尽量减少拷贝次数,由于UDP是基于数据报的,数据只要准备好就会调用一次send或者sendto,这个是由应用逻辑决定的,然而我们可以决定的是到底是用send还是sendto,看看参数便知,sendto的参数要比send更多,因此这就意味着,如果使用sendto,需要拷贝的参数就会多一些,参数到了内核空间之后,内核还要准备数据结构暂时容纳这些参数,当数据发出之后,内核需要释放这些暂时存储的参数(当然可以使用栈来管理这些参数以实现自动释放)。
     如果一个UDP是connect的,那就可以在connect之后直接调用send了,内核在应用connect之后就永久维护了这次UDP的连接,以后每次收发数据,内核不再需要分配/删除这些数据,而只是查找就可以了,同时也减少了数据的拷贝量,既然connect的UDP在内核中已经存在了一个“连接”,那么无论何时,只要通信没有结束,内核总是能随时追踪到这个“连接”,因此就引出了1.2。

1.2.错误提示

如果是一个没有“连接”的UDP,数据在调用sendto发出后发送端就释放了关于目的地的任何信息,然而数据如果最终由IP封装(或者被任何有错误提示的下层协议封装),数据在半路上或者终点遇到某种问题不能到达目的地时,会有ICMP(对于非IP协议,可以是其它机制)错误信息返回,然则发送端已经不再可能知道该错误信息要发给哪个应用(源/目的地信息已经释放)。
     对于有“连接”的UDP通信,由于内核协议栈已经维护了从源到目的地的单向连接,因此当错误信息发来的时候,内核协议栈会准确定位到该转发给哪个应用。
     最后说明一点,UDP的连接是单向的,在调用connect的时候并不会产生任何通信流量,它只是在内核协议栈中绑定了一对五元组而已,该五元组是:UDP协议/源IP/源端口/目的IP/目的端口。
2.udp高效率的神话 如果你在网上询问,TCP和UDP的区别,得到的结果无非以下几种:UDP不需要处理确认,UDP比较高效,基于连接与无连接,TCP耗资源,...
     然而以上这些都是神话,是神话就是要破除它。UDP并不是一定比TCP高效,要知道,TCP发展到今天,其算法已经非常丰富,合理配置的话足以应付各种复杂的环境,大多数情况下都会比UDP更加高效。
3.udp的设计-多路复用的IP TCP/IP只是一个协议族,在这个协议族上,完成了所有应该完成的工作,UDP作为一种数据报协议,其更多的作用在于使用端口的概念进行应用复用,在实现上,它基本上是复制了IP协议,在复制的基础上,增加了一个可选的校验和,一定程度上保护了数据的完整性,当然你也可以不要它。
     IP协议完美的实现了数据报业务,发后不管结果,尽力而为-虽然ICMP一定程度提供了有限的反馈信息。有人说IP协议很不负责,然则分层模型的要旨就是每层仅提供单一的服务,这也是unix的哲学。IP协议仅仅提供了最底层的数据报分组交换通信,这是和电路交换完全并行的一个通信模型。有的时候,真的需要这样的“发后不管,尽力而为”的服务,TCP/IP的绝妙解决方式不是直接让IP来提供这个服务,而是在IP之上提供了多路复用的UDP,结果是,针对于主机,同一个IP可以承载多个“发后不管,尽力而为”的服务,IP最终仅仅提供传输服务。
4.udp的使用场合前奏 读懂了第3节,那么我们接着往后说,到底哪些业务需要“发后不管,尽力而为”的服务呢?在现实中,我们知道,平信是这样的业务,说起平信,值得还念,90后的几乎再也不需要写信了,当时我上大学时,一周要给远方的女朋友寄送最少一封信,可是有时候信件一周内就到了,可是有时,信件丢了,因此我要花费大量的电话时间和金钱来解释以证明自己真的写了信,只是没有送到而已,也有的时候,当我解释了之后,信件莫名其妙的到了,延迟了好久。这就是发后不管,尽力而为的服务,也有那么一次,为了澄清一个事实-这封信绝对不能丢,我使用了特快专递,虽然并没有表现出什么特快,然而我收到了回执,这就不是发后不管的服务,而是“带确认的服务”,因此是否发后不管和通信效率并没有什么必然关联,这是需要澄清的。
     很多人都认为udp应用在实时要求比较高的领域,然后还说什么自己完成顺序和重传,既然需要保序和重传,那为何不直接使用TCP呢,这些人实际上知道TCP之所以效率低,就是因为它需要处理按序交付以及处理重传。如果为UDP加上了这些功能,那岂不是UDP的优势也不再了?
     我想,上述这些人一定是教科书读太多了,加上各种教科书又都是从那么几本经典书籍以及不多的几篇RFC中摘录的,因此“天下教科书一大抄”本身成就了众口铄金的效果。实际情况远非这么简单。TCP真的没有UDP效率高吗?未必!
     要知道,我们网民生活的互联网的每一根通信管道并不是固定容量的,而是可以伸缩的,然而标准的UDP在发送时却是每包长度固定的,为了简单起见,使用UDP协议的应用程序之间都使用固定长度的包通信,这就有问题了...

4.1.问题一:无法利用空闲带宽导致资源浪费

很简单的一个场景,UDP双方每次以512字节定长包通信,这就意味着发送端发出512字节,接收端就接收512字节,每512字节封装一个IP包,除了端口复用这个优势之外,白白增加了一个UDP头的封装成本。即使能一次发送更多的数据,也不得不每512字节发一次,对于使用TCP的应用,数据到达内核协议栈以后,某些情况下,可以暂时积攒起来,这样就节省了封装的费用,但是并不会浪费时间导致交互问题,因为TCP会使用一种很聪明的算法,当发现数据必须积攒的时候,就说明此时不积攒也不行,TCP的复杂算法会在延迟和吞吐量之间达到一个很好的平衡,详见《TCP协议疑难杂症全景解析
     最简单的一个事实就是链路的MTU几乎不会影响到UDP,而会影响到TCP,而这直接影响IP的分片,这又是一个效率问题,极端点说,UDP可能每次发送的包是MTU的几百分之一,也可能是MTU的几百倍,前者太低效,后者将消耗转嫁给了IP。

4.2.问题二:网络拥塞或两端性能不匹配时无法被反馈导致大量丢包

对于TCP而言,它可以使用拥塞控制和流量控制算法智能控制该发送速率,而对于UDP,由于没有确认机制,即使网络拥堵了或者对端机器吃不消了,发送端还是不停的发送,这样就会加重病态的恶化程度,对于这种恶化,UDP的收发两端不必负任何责任,详见5.1。

4.3.改进UDP发包为动态长度代价太高

由于上述的两个问题,有必要对UDP在用户态做一些调整,然而因为有TCP的存在,到底是做调整还是直接用TCP,这是一个问题,该问题的最终解决涉及到时间成本和金钱成本。
5.UDP真正的使用场合

5.1.网络情况于公平性

虽然udp没有流量控制和拥塞控制,也不需要确认,但是这并不一定会提高效率,俗话说,磨刀不误砍柴工,有时候一些必要的维护工作是要做的,虽然udp比tcp简洁很多,但在这简洁的背后,其高效率的假象难免会有一些掩耳盗铃的意味,一个udp数据报发出去就不管了,你收不到确认,于是你默认它已经安全到达对端了,这就好比你不希望代码出错,于是你将代码中的错误提示删除是一样的道理。
     实际上,在网络极度拥堵的情况下,udp的丢包率极其高,这正是因为它没有拥塞控制导致的,由于同样没有流量控制,在两端速率不匹配的情况下,也会出现持续不减的高丢包率,而TCP就没有这个问题,因为它会自我调节。
     再谈公平性的影响。tcp天生就是公平的,而udp不是,它是毫无秩序的,就和真正的分组交换网的定义一样。由于没有秩序,网络情况丝毫不会反馈给端点,不但自身会造成高丢包率,还会挤压tcp流量的带宽。    
     用一个实例结束本节,那就是道路交通。北京交通可以看做udp,而上海交通则是tcp,虽然都面临拥堵,但是你会发现,北京的车一旦遭遇拥堵,几乎就卡死了,而上海虽然有的路段比北京还堵,然而不会卡死,车流即使再慢也仍会缓慢前行。

5.2.通讯持续性和交互性

对于分组交换网络通信,协议栈的成本主要表现在以下几方面:a.封装导致的空间复杂度;b.缓存导致的时间复杂度。对于封装,无疑和缓存是直接对立的,如果你想将数据马上发出去,那么就需要直接封装并发给下层,这样无疑消耗了更多的协议头空间,如果你不想如此消耗,那么就把载荷缓存,待缓存达到一定量时再一次性发出,这样“协议头/载荷”值将最小化,无疑节省了空间,但是浪费了时间。
     以上原理理解之后,紧接着你可能会想到两种通信类型,一类是短连接通信,一类是长连接通信。考虑通信效率的时候一定要考虑这种通信持续性所带来的影响,如果你只需要发一个包且该包可以发后不管且自己有重发/轮询机制,那么UDP比较好,如果此时用TCP的话,光握手就需要2个包(第三次握手可以携带数据),平均下来不划算,这样的例子就是DNS查询。反之若是长连接,那么TCP握手和挥手的额外时间会平摊到持久的通信中,在持久的通信中,应用程序可以从TCP流中得到额外的好处,比如积累发送,Nagel算法带来的好处等等,另外,大多数情况,确认并不是一种开销,因为很多TCP算法都是用捎带确认或者延迟确认,因此大多数情形下确认包就不会影响发送端的速率又不会占据带宽。
     在用户的交互体验上,UDP协议的通信完全取决于应用程序本身的发送和接收,但是TCP协议的通信则要受到协议栈的影响,应用程序发出了的数据并不一定等于发到了网络,比如Nagel算法就会影响实际发送。

5.3.通讯行为

你真的希望数据发出后就不管了吗?如果不是,那就使用TCP,不要自己实现确认和连接,当然在短连接情况下,你要仔细权衡TCP握手的开销和你自己实现的确认的开销。
     以DNS为例,这明显不是一个发后不管的应用,但是DNS客户端可以在一定时间没有收到回复后,再发出另一次查询,这并不影响最终的结果,这只是一个基础设施类型的单点查询任务,并不是电脑前用户最终的任务,因此使用UDP完全可以,但是对于HTTP就不一样了,HTTP本质上类似一次内容传输,然后浏览器解析传来的内容并给予展示,这对格式有严格的要求,并且事先并不知道结果的大小,结果的格式更是不固定,因此稍有差错就会影响到浏览器的解析。一次HTTP通信并不是一对一应用通信,而是牵扯到很多其它应用,比如服务器端的cgi以及客户端的script,因此绝对需要精确传输,此时就不能用UDP,即使是短连接也是要用TCP的。

5.4.多点通讯

我们知道,TCP是一个有连接的通信协议,在实际传输前,你必须和通信目的地建立一个双向的连接,并且只能和唯一的目的地建立连接,那么如果我们想将数据传给多个目的地,那么我们就需要建立多个这样的连接,在TCP之上,实现多点通信并不容易,这是TCP的握手协议以及挥手协议决定的。
     对于UDP,由于没有连接,就很好实现多点通信,对于使用了有连接的UDP,完全可以使用DNAT或者负载均衡之类的技术来实现多点通信。由于UDP协议不需要建立连接,那么完全可以向一个组播地址发送数据或者轮转地向多个目的地持续发送相同的数据。

5.5.数据边界

UDP是基于数据报的,这也就是说,每一个UDP包都是有边界的,这是和流式通信最大的不同,对于TCP而言,完全按照数据本身来界定边界,而对于UDP而言,则完全按照收发双方每次通信的实际内容界定边界。

5.6.即已存在的事实

我们在两个著名的开源代码中会遇到用UDP实现TCP的功能,它们是OpenSSL和Open×××,对于OpenSSL,DTLS完全是为了给基于UDP的应用提供安全保护而存在的,既然叫传输层安全,那就要要包容整个传输层(实际上,SSL协议在分层意义上跟TCP/IP的传输层一点关系都没有),对于Open×××,是历史原因才自己实现确认和重传的,那时还没有DTLS可以借鉴,这是无奈的。因此大家不要轻易把这两个事件作为可以借鉴的事实,在借鉴一件事的时候,一定要考虑它的历史背景,读史使人明智,事实在于如此。

5.7.结论

因此,只有你确认以下事实的时候,使用udp才是明智的

5.7.1.你的应用完全是udp完成,或者你不在乎tcp会受到的影响

5.7.2.你对网络状态很熟悉,确保udp网络中没有氓流行为,疯狂抢带宽

5.7.3.通信双方配置,负载匹配或者自己手工解决了这些问题

5.7.4.数据事实证明TCP真的比UDP更低效率或者你宗教般痴迷于UDP的高效率

5.7.5.你不在乎上述4点或者你根本不懂网络

5.7.6.TCP实在不方便实现多点传输的情况