三十天学不会TCP,UDP/IP网络编程 - 绅士的开始
经过了过年的忙碌和年初的懈怠一切的日子,我又开始重新更新了~这是最新的一篇~完整版可以去gitbook(https://rogerzhu.gitbooks.io/-tcp-udp-ip/content/)看到。
如果对和程序员有关的计算机网络知识,和对计算机网络方面的编程有兴趣,虽然说现在这种“看不见”的东西真正能在实用中遇到的机会不多,但是我始终觉得无论计算机的语言,热点方向怎么变化,作为一个程序员,很多基本的知识都应该有所了解。而当时在网上搜索资料的时候,这方面的资料真的是少的可怜,所以,我有幸前两年接触了这方面的知识,我觉得我应该把我知道的记录下来,虽然写的不一定很好,但是希望能给需要帮助的人多个参考。我的计划是用半年时间来写完这一系列文章,这个标题也是我对太多速成文章的一种态度,好了,废话不再多扯了,下面是其中的一节内容,更多内容可以去gitbook上找到。
TCP与UDP
前面对于UDP已经阐述了有一些内容了,UDP可以完成一些数据的传输,那么为什么还要再研究出另外一种传输层协议呢?因为在很多时候,不可靠的传输会造成上面的应用层协议变得毫无意义,而且面对越来越复杂的网络,没有管理控制的传输层协议更是会导致网络拥堵不断加剧直至瘫痪。可以设想一下,UDP就像是寄信,当你把信寄出去的时候,你是无法知道这封信可不可以到达收信人的的,如果说唯一你能做的就是相信邮递机构。这封信在沿途是丢了还是寄到什么其他地方去了,你完全不知道(虽然说在现在这个快递信息极端透明的情况下看起来不太可能,但是在快递刚刚开始的时候,这种情况太常见了)。但是如果是打电话,这种模式就不行了,不能说你播出一个号码,说一段留言,然后还不知道对方能不能接收到这个留言,如果是这样,我要电话还有个什么用。再换个角度,即使我真的能有这么个留言的模式打电话,但是由于同时有太多留言,造成电话网络压力过大,你的留言可能因为延迟一年后才到达对方,很明显,电话不是这么设计的,就像TCP一样,设计他就是为了能提供可靠的通信。
TCP里最初级也是最重要的概念之一就是连接,UDP是没有连接的协议,通俗点说就是UDP的两端在通信的时候只要任一方想发送消息给其他方,他只要发就可以了。UDP是一个很任性的协议,想发就发,想断就断,不需要实现通知对方,也不需要做些什么准备工作。TCP就不同了,TCP是一个很绅士的协议,在发之前,发送方和接收方会先进行协调,结束的时候呢,双方同样也会进行相互的沟通并积极的做好自己的清理工作,英文中对这种行为有个很恰当的词语,叫做graceful。
绅士的开始
前面说过,TCP是一个绅士的协议,在发送数据之前,双方会进行友好的协商,这种协商也就是在所有介绍TCP的文章里都会提到的“三次握手”。现在有个普遍的现象,现在问面试者什么是“三次握手”,基本都没有答不出来但是如果再进一步,问一下,如果在某一个步骤的时候出现了丢失,那么会怎么样,基本上就只剩百分之二十的人能答的出来。这就是像你只知道别人的绅士行为是什么样的,却不知道这些行为的来源,所以如果盲目的学习只能给人一种学到皮毛的感受。不过,这也是一篇介绍TCP的文章,所以当然也绕不开TCP的三次握手,我用一张wireshark里真实抓包得到的TCP流来进行图示:
左边是发送方,右边是接收方,在介绍三次握手之前,首先大家得回忆下前面介绍过的TCP的报头中的标识符位。TCP报头中有6位标识符,在置1之后分别代表这一个TCP包有不同的含义。其中第五位是SYN位,当这一位置1时表示连接的开始或者同步序号请求,SYN就是英文同步synchronize的缩写。除了这一个之外,另一个会在三次握手中出现的就是ACK,这个是六个标识符中的第二个标识符,英文acknowledgement的缩写,主要用来表示对于对端消息的回应,简单粗暴的理解的话,可以理解为,“啊,我知道了”。
为什么我说TCP是一个绅士的协议呢?从其三次握手的过程中就可以体会的到,请求的发起方先发送一个编号为0的SYN包到接收方,接收方接收到这个SYN包之后,首先肯定是要通知发送方我已经接受到了你的SYN请求,也就是我们上面说的ACK。但同时按照上面描述的,如果想建立连接,就必须发送SYN,所以,对于接收方,就有两个需要发送的包,亦或是说两个被置不同标识的包,但是很明显,这两个包是可以合并的,所以说,发送方就会发送一个TCP包,这个包里,SYN位和ACK位同时被置上。回到发送方,在接收到这个对端发送来的SYN包之后,同样要回一个ACK包给对端。此时,TCP连接就建立好了,后面的通信中,两端就可以自由的发送数据和消息了。
这里还可以了解到的就是贯穿整个TCP的确认消息,TCP如何让对端知道自己已经收到了哪些包?前面一篇说过,TCP报头中是有一个序列号的字段的,这个字段用来给每一个报文编号,这个编号在一次通信中是不断递增的,所以理论上接收端只要告诉发送端自己已经收到的包的序号就等于告诉发送端我已经收到比这个序号小的所有的报文。回到上面的图中,可以看到第一个SYN包的序号是0,那么当接收端告知对方的ACK中所使用的序列号是1,表示标识符比1小的包我都接收到了。在这个特定情况下,也就等于发送端已经知道了接收端已经良好的收到了自己的SYN请求。当然,这个序列号,确认号具体在TCP报头的什么位置,在上一篇文章中,可以很容易的找出来。
TCP的三次握手其实给人和人之间的交流提供了一个很好的模型,就拿开车并道这件事来说,如果人人都能遵循三次握手的原则,那么我相信所有的因为并道而产生的事故都能避免。前车要并道之前先闪三次转向灯,后车闪灯表示自己已经收到前车并道信号并且可以并道,前车再次打转向灯,然后开始并道。简直是一个标准的三次握手过程,简洁而有效,可惜的就是在实际生活中,按照这样的规则做并道的人太少。
被打断的绅士
虽然说三次握手的设计是一个很绅士的设计,但是所有的时候理想和现实都是有差距的,由于网络的复杂性,三次握手的每一个消息都有可能在传递的过程中面临三种情况,丢失,延迟到达,重复。这三种情况贯穿于整个的TCP通信的每一步,而TCP中的很多设计也是因为解决这三个情况而应运而生。
在建立连接的阶段主要是丢失的问题,在介绍丢失问题的解决思路之前,先要介绍的一个概念是发送计时器。在TCP中,发送消息的时候会启动一个计时器,这个计时器在收到相应回复的时候会重置而重新计时,而如果一直没有收到相应的回复,在计时器到期的时候发送端就会重发消息,这是TCP重传机制里面第一层的保障。
因为TCP发起连接的时候只有三条消息,所以丢失也就三种情况:
第一个SYN消息丢失,也就是发起者的发起请求丢失了,所以接收者也就不会回送SYN-ACK消息,因为他没有得消息刺激他回应。所以过一段时间后发起者发现自己没有收到回应消息,于是在计时器到期后,发起端会重发SYN消息。如果在经过了几次重传仍然没有成功以后,尝试连接过程就终止了。
第二个SYN-ACK消息丢失,发送端本质上和上一种情况相同。接收者因为确实已经收到了SYN消息并发送了回复消息,所以其计时器已经启动了。在计时器到期之后,接收端会重发SYN-ACK消息,如果几次之后还没有成功,那么接收端会发送RST终止连接,RST的含义在后文中会详细介绍。
第三个来自发送端的ACK丢失,接收端本质上会上一种情况相同,最终会发送RST消息终止连接。
单独分别从两端看这三个错误的处理方式,并不难理解和理清楚其中的过程,但是如果从两端一起考虑,那么稍微想一下就知道过程变得极端的复杂,因为在发生丢失的情况下,两端都有定时器在计时。
在linux的TCP-IP协议的实现中,分别使用两个不同的计时器,在发送端启动是普通的超时计时器,在接收端启动的是SYN-ACK计时器。超时计时器就是在发送端发送SYN的时候开始计时,默认是1秒,如果过了1秒没有收到确认,会再次发送SYN,然后将计时器设置成为2秒,然后依4秒,8秒,16秒,以此类推。当然,在代码中有一个重试上限,在linux上的默认是设置为5次。同样的SYN-ACK计时器在接收端接收到SYN之后发出SYN-ACK消息之后启动,间隔和重试次数和普通计时器都是一致的,当然他会做一些其他的事情所以和普通计时器是有一些区别的。
我们考虑实际中的情况二,发送端发送SYN后未收到SYN-ACK消息,同时启动计时器A,过了一小段时间之后,接收端接收到了SYN消息,启动计时器B,发送SYN-ACK消息,但是这个消息丢失了。1秒钟后,发送端由于A到期,重发SYN,而几乎与此同时接收端也会由于B到期重发SYN-ACK消息。那么问题来了,假设这个时候重发的SYN又一次成功的到达了接收端会怎样?答案很简单,接收端会忽略它,因为seq序号重复了。接收端既不会再一次发送SYN-ACK消息,也不会重置计时器。于是就避免不断重复的重发,造成网络混乱甚至崩溃。
如果用一句话总结的话,就是通过超时计时器和序号的重复检测,TCP可以同样可以很绅士的解决这些不绅士的打断。