> 文章列表 > TCP/UDP协议

TCP/UDP协议

TCP/UDP协议

写在前面

下面我们继续说我们传输层的协议,这个协议我们重点看TCP协议,注意TCP是我们经常使用的额,而且也是我们在面试最容易被问到的,所以我们要重点分析,下面是一张图,让我们回忆一下我们知识点到协议的哪一层了.

TCP/UDP协议

再谈端口号

我们知道端口号(Port)标识了一个主机上进行通信的不同的应用程序,今天补充点端口号的知识

TCP/UDP协议

端口号划分

  • 0-1023:知名端口号,HTTP,FTP,SSH等这些广为使用的应用层协议,他们的端口号都是固定的
  • 1024-65535:操作系统动态分配的端口号.客户端程序的端口号,就是由操作系统从这个范围分配的.

知名端口号

有些服务器是非常常用的,为了使用方便,人们约定一些常用的服务器,都是用以下这些固定的端口号:

  • ssh服务器,使用22端口
  • ftp服务器,使用21端口
  • telnet服务器,使用23端口
  • http服务器,使用80端口
  • https服务器,使用443

执行下面的命令,可以看到知名端口号

cat/etc/services

TCP/UDP协议

netstat

netstat是一个用来查看网络状态的重要工具.

  • 语法:netstat[选项]
  • 功能:查看网络状态

常用选项:

  • n拒绝显示别名,能显示数字的全部转化成数字
  • l仅列出有在Listen(监听)的服務状态
  • p显示建立相关链接的程序名
  • t(TCP)仅显示TCP相关选项
  • u(UDP)仅显示UDP相关选项
  • a(all)显示所有选项,默认不显示LISTEN相关

pidof

在查看服务器的进程id时非常方便.

语法:pidof[进程名]
功能:通过进程名,查看进程id

UDP协议

我们先来一点简单的,UDP协议.和http一样,我们也是先来看看UDP协议格式

报文格式

TCP/UDP协议

这里我们就知道了我们UDP报文的格式,先简绍一下报头里面的属性.

  • 16位源端口:数据从哪一个端口发出来的,也就是数据从哪一个进程发送出来的.
  • 16位目的端口:数据想要到哪一个端口去,也就是数据想要去往哪一个进程,就是上面的分佣
  • 16位UDP长度:表示整个数据报(UDP头部+UDP数据)的最大长度.
  • 16位的UDP校验和:校验数据在传输过程中是否失真,如果校验和出错,就会直接丢弃

我们看到上面存在一个属性,这里面存在一个16位UDP长度,也就是我们整体报文的大小只能是216位的数据,也就是说一个UDP能传输的数据最大长度是64K,然而64K在当今的互联网环境下, 是一个非常小的数字. 如果我们需要传输的数据超过64K, 就需要在应用层手动的分包, 多次发送, 并在接收端手动拼装.

解包&封包

说一下UDP是如何解包的,很简单,OS会先读取8字节的报头长度,此时OS会寻找到16位UDP长度,这里计算出整个报文的长度,减去报头的长度就是我们有效载荷的长度了.至于封包,那么更加简单了,这里假设存在一个位段,这个位段实现了我们的UDP报头,当我们进行封包的时候,有效载荷往后面移动一个位段的空间大小,把位段拼接到前面就可以了.

TCP/UDP协议

UDP特点

下面说一下UDP的特点.

  • 无连接:知道对端的IP和端口号就直接进行传输,不需要建立连接.
  • 不可靠:没有确认机制,没有重传机制.如果因为网络故障该段无法发到对方,UDP协议层也不会给应用层返回任何错误信息.
  • 面向数据报:不能够灵活的控制读写数据的次数和数量.

不可靠

由于网络层次不行,报文到达的速度不一样,所以UDP不保证可靠,想要正常顺序,可以,我们自己去定义.

面向数据报

应用层交给UDP多长的报文, UDP原样发送, 既不会拆分, 也不会合并…例如用UDP传输100个字节的数据: 如果发送端调用一次sendto, 发送100个字节, 那么接收端也必须调用对应的一次recvfrom, 接收100个 字节, 而不能循环调用10次recvfrom, 每次接收10个字节

缓冲区

UDP没有真正意义上的 发送缓冲区,调用sendto会直接交给内核, 由内核将数据传给网络层协议进行后 续的传输动作.但是 UDP具有接收缓冲区,不过这个接收缓冲区不能保证收到的UDP报的顺序和发送UDP报的顺序一致, 如果缓冲区满了, 再到达的UDP数据就会被丢弃.UDP的socket既能读, 也能写, 这个概念叫做 全双工

TCP协议

下面我们来看经常使用的协议,也是非常重要的协议.**TCP全称为"传输控制协议(TransmissionControlProtocol").**人如其名,要对数据的传输进行一个详细的控制.

报文格式

下面就是我们的TCP的报文的格式在,这里我先只谈前20个字节的报头,这是TCP标准的报头长度,至于下面的选项我们会涉及到一点.

TCP/UDP协议

解包&封包

我们知道TCP一定是上面的应用层传输或者下面的网络层向上面传递的,那么我想问的是TCP是如何解包和封包的,又是如何分用的?

先说我们如何把有效载荷和报头分开,很简单,我们直接拿到20个字节长度,可惜还是不够,我们报头中会存在选项,此时我们发现这里有一个属性,4位首部长度,这个长度就是我们TCP报头的大小.此时有人说不对,我们4位最大好像是15个字节,连标准的20个字节都表示不出来,这里是不对的,我们的单位不一样,这里的单位是4个字节,也就是我们TCP报文的长度是20到60字节.

如果我们不考虑选项,也就是我们的报头是20字节,请问我们的4位首部长度是是多少?不就是5吗,二进制也就是0101,这也是我们TCP的最小的长度.那么TCP是如何做到解包的呢?很简单,我们无脑的读取20字节,找到4位首部长度是多少,把剩余的选项重新读取来就可以了.

那么所谓的封装呢?这里也是位段,和UDP一样,我们把它拷贝到有效数据就可以了,此时更多的问题,例如大小端问题我们就不再考虑了.

这里就是我们源端口和目的端口的作用,我们在解包的时候,首选拿到前4个字节的数,不就是我们端口号吗,和UDP是一摸一样的.

我们还发现了一个问题,我们UDP存在有效的数据长度,为何TCP没有?因为它不需要,TCP只需要把数据拷贝到缓冲区就可以了.

可靠性问题

我们一直说TCP具有可靠性,那么什么是可靠性呢?这里我们谈什么是不可靠性,知道了什么是不可靠,那么可靠性就明白了.所谓的不可靠就是丢包,乱序,检验失败…我们这里都要谈.

确认应答机制

请问如何确定一个报文是丢包了还是没有丢?这里很简单,如果我给别人发了一个报文,如果它给我回复了信心,说自己接到了,那么我就知道它收到了我的报文了,这是TCP的最重要的思想.

那么也就是在TCP中只要我发了一个消息,只要对方给了我应答,那么我就知道自己的报文已经被别人收到了.那么我们看下面两个人的对话.我们知道A已经知道了B收到了自己的消息,那么B是如何确保自己发的消息A收到了呢?这里就我们就知道了在长距离数据交互的时候,始终会存在一个最新的报文是没有被应答得,也就是不存在100%可靠得传输,但是,只要我们发的消息被对方应答了,我们就认为我们发送得消息对方是收到的,如果我们没有收到,那么我们就不确定我们报文是不是被别人收到了还是在路上.

TCP/UDP协议

这里还有一个问题,我们如果连续发报文,你也每一个给我相应了,那么由于网络在传输的时候可能存在速度的不同,请问我们如何保证对放的回应是和我们问题匹配的,也就是不能逻辑循序不能乱.

所以TCP里面存在两个字段,分别是32位序号和32位确认需要,这里先和大家建立一个共识,我们TCP在进行通信的时候,绝对不要忘记我们的报文一定是携带TCP报头的.

TCP/UDP协议

只要客户端收到了11号确认序号,我们的客户端已经明白了我们前面10个报文服务端都收到了,下面我们考虑一道题,假设服务端收到了很多的报文,请问我们应该给客户端发送几号的确认序号,这里很简单,发送4号,告诉客户端我们的4之前已经全部收到了,4号还没有收到,可以考虑重新发送.

TCP/UDP协议

请问TCP为何存在两组序号,我好像用一组序号也可以,客户端给了发了一个5号序号报文,我们服务端修改一下这个序号,变成6发回去不久可以了吗?可以的,但是TCP是全双工的,服务端在确认客户端发送的消息后也像顺便给客户端发消息,我们服务端要有自己的序号,我们需要保证双方的可靠性.

这里还有一个问题,这些序号和确认序号是如何来的呢?如果我们序号有规律,纳闷黑客拿一些正确的序号不正确的内容就可以搞我们编译器.所以的序号的初识值是操作系统随机产生的,后买你的额递增和报文有关.那么既然序号是递增的,会不会溢出,这里有人已经帮助我们考虑好了,序号会进行回绕.

流量控制机制

TCP是存在发送缓冲区和接受缓冲的,我们前面说了所谓的write/send和read/recv本质就是拷贝函数,它的作用是把数据拷贝到自己的缓冲区的,至于数据什么时候发送那是OS决定的,所以TCP叫做传输控制协议.由于我们存在两对缓冲区,也就是别人给我发不耽误我给别人发,这就是TCP具有全双工的.

TCP/UDP协议

下面我们只考虑一个方向的发送,我么知道缓冲区是存在大小的.

TCP/UDP协议

如果我们客户端一次性给服务端发送大量的信心把服务端的接受缓冲区给直接打满了,但是客户端不知道,他还是一个劲的继续发送,此时这些数据就会丢包,对于TCP而言,丢包没有问题,只要我们后面重新发送就可以了,道理很对,但是这不合理,不能因为我来的晚没有位置把我就丢弃了,要知道我们从网络千辛万苦耗费了网络资源到了服务端,就因为服务端处理的数据太慢就让我们后面重新发送,这感觉有点不合理,服务端处理能力低你就告诉客户端吗?没有什么大不了的.所以TCP报头里面存在一个属性可以记录自己缓冲区剩余空间的大小,就是16位窗口大小.那么客户端如何知道服务端的接受能力呢?因为服务端它回应答,此时TCP报头就存在这个属性,那么客户端就知道了服务端的缓冲器大小了,反之客户端应答服务端的时候也会记录客户端的接后能力.

这里我们还有一个问题,我们客户端第一次给服务端发送数据,我们如何知道服务端的大小呢?如果我第一次就给它塞入大量的数据怎么办?这里到三次握手在解决.还有16位是不是有点少?一般就够了,如果不够选项里面也存在一些动态调整的因子,我们这里不做考虑了.

TCP中存在六个标志位,为何会有有若干的标志位,前面我们铺垫了一个概念,TCP三次握手,只有完成了三次握手,才算建立连接成功,只有建立连接成功,才能正式通信,也就是报文是存在类别的,报文的功能可能是建立连接,发送正文,和退出连接.这就是为何会存在了标记位的原因.

SYN & FIN

上面我们说了报文是存在类别的,那么建立链接和断开链接都应该存在标志位.

  • SYN 只要报文是建立连接的请求,改标记位被设置为1
  • FIN 断开连接的标志位

ACK & PSH

server是如何识别这是一个这是一个确认报文呢?这就要涉及到另外的标志位.

  • ACK确认标记位一旦被设置,是对历史报文的确认,给别人回消息的时候设置为1,表示server已经收到这个报文了.
  • PSH数据推送标记位,提示接收方立即把TCP缓冲区里面的数据读走

ACK不仅仅是对历史报文的确认,一般而言,确认报文中也会携带另一方想给我发的消息,所以ACK一般全部都会被设置成1.

至于PSH标志位我们又更多的要谈,我们还是涉及客户端给服务端发消息,我们也知道他们的报文中存在窗口大小,所以一般的通信我们不再谈了.

TCP/UDP协议

说一下几种比较极端的情况,假设我们服务端接受缓冲区中没有数据,那么服务端只会在这里阻塞式的等待读取,一旦出现了数据OS会把数据读取到上层.这就是当条件满足时我们的OS才会对缓冲区的数据做操作,那么请问什么是条件满足?这里很简单,就简单的依照这个情况而言,只要有数据我们就就可以read成功,可是这是我们自己调的,如果不是我们调呢?当数据就绪好了,OS才会调用read,我们应该如何做?那么就许哟啊系统自己轮询检测,当数据存在就调用read,例如旦缓冲区中的数据到达一定的数量,OS把这些数据读取到上层.

那么如果我非常想让OS里面读取缓冲区的内容呢?很简单,把PSH设置成1,当数据拷贝到接受缓冲区的时候,OS注意到这个标志位,它明白了这是要我把缓冲区的数据立马读走的意思,这不就是条件变量的意思吗?我们可以通过系统层面来理解.

如果上面的数据大家理解不了,我们这里再换一种说法,假设服务端数据被写满了,窗口大小为0,服务端已经来不急接受了,假设应用层是非常忙的,没有时间取检测缓冲区有没有数据?理想的情况是客户端等着服务端处理好了数据,但是等着等不及了,客户端PSH直接置为1发给服务端,让它赶紧处理数据,那么如果服务端不听话呢?那么服务端一般而言就有问题了.

URG & RST

请问为何TCP在通信的时候要有序号呢?这里谈过了,按照顺序确认应答.这里有另外一个功能,客户端按照顺序发,可不代表我们的服务端在接受的时候的时候是按序到达的,毕竟不同的报文选择的路由节点有可能不同.这里我们知道报文在发送的时候是可能乱序倒到达的,这是不可靠的一个特点.那么请问TCP如何做到的,这里确认应答机制.

那么我们为何问上面的问题呢?我们知道TCP是可以保证按序到达的,但是这里面存在一个问题,如果一个数据是非常重要的,但是它的序号是在是太大了,那么我们如何让这样的数据可以被优先紧急处理?此时URG标志位被设置,那么服务端会优先读取这个报文.

那么请问对于存在URG被设置的报文中,哪些是紧急数据呢?这里我们要谈的16位紧急指针,它是一个偏移量,也即是相对该报文初始地址的偏移量,数据大小是一个字节.

这里说一下紧急指针的应用场景,例如服务端处理数据变得很慢,有可能服务端快挂了,也有可能处理不过来,对于客户端而言,它想知道服务端怎么了,给他发了一个紧急指针,此时服务端和客户端约定,不同的紧急指针数据和回应的紧急指针的数据代表不一样的意思.这就是紧急指针的作用,获取对方的情况的,这些数据我们称之带外数据.

RST这个标志位的意思是一旦我们收到了这个报文,此时连接需要重置,我们后面在三次握手那里中点谈.

超时重传机制

我们知道一个报文发出去后会有下面的情况.

  • 主机A发送数据给B之后,可能因为网络拥堵等原因,数据无法到达主机B

  • 如果主机A在一个特定时间间隔内没有收到B发来的确认应答,就会进行重发

TCP/UDP协议

但是,主机A未收到B发来的确认应答,也可能是因为ACK丢失了

TCP/UDP协议

我们知道一旦我们的报文没有收到应答的报文,我们就认为数据丢了,过了一段时间我们重新发送,那么既然我们要重新发送数据,那么我们就要保留原始的数据,问保存到那里?这个后面继续说.

由于是确认应答的报文丢了,因此主机B会收到很多重复数据.那么TCP协议需要能够识别出那些包是重复的包,并且把重复的丢弃掉.这时候我们可以利用前面提到的序列号,就可以很容易做到去重的效果,这是序号的第三个作用.

还有一个问题,我们说了过了一段时间会超时重传,请问这段时间是多长?

时间的确定

  • 最理想的情况下,找到一个最小的时间,保证"确认应答一定能在这个时间内返回".
  • 但是这个时间的长短,随着网络环境的不同,是有差异的
  • 如果超时时间设的太长,会影响整体的重传效率
  • 如果超时时间设的太短,有可能会频繁发送重复的包

时间长度,TCP为了保证无论在任何环境下都能比较高性能的通信,因此会动态计算这个最大超时时间.

  • Linux中(BSDUnix和Windows也是如此),超时以500ms为一个单位进行控制,每次判定超时重发的超时时间都是500ms的整数倍.
  • 如果重发一次之后,仍然得不到应答,等待2*500ms后再进行重传.
  • 如果仍然得不到应答,等待4*500ms进行重传.依次类推,以指数形式递增.
  • 累计到一定的重传次数,TCP认为网络或者对端主机出现异常,强制关闭连接

连接管理机制

在他这个话题之前,我们先来谈另外一个知识点,TCP三次握手,前面我们已经笼统的和大家说过了TCP三次握手的大体情况.

TCP/UDP协议

TCP三次握手

此时,我们可以发现上面的很多的标志位我们都已经知道了,先看三次握手的情况,这里有很多的要谈.

TCP/UDP协议

第一次SYN,这里是建立链接的报文,此时服务端给客户端发送确认报文和SYN建立链接的两个功能的报文,我们发现客户端和服务端的状态是基于报文的发出变化的,这里的线是斜着划的,也就是存在时间,但是只要我们的报文发出了,我们的状态就会发生改变.

我们已经知道TCP是三次握手的,那么请问为何TCP为何要三次握手,主要它是面向链接的,既然是面向连接的,假设服务端是被1000个人连,请问OS如何管理这些连接?很简单,先描述,在组织.这就要求我们花时间和空间来管理这些连接.如果连接多了,服务器聚少成多,这就是大压力来了,服务器就有可能挂掉.但是说这些和三次握手有什么关系?双方三次握手成功后,双方都要有成本.

为何是三次握手?不能是1一次或者2次吗?我们发现最后一次ACK是没有应答的,我们可以确定前两个一定可以被收到,但是第三次不确定,也就是三次握手一定会成功吗?不一定.

为何不是2两次吗?一次行吗?我们先来谈一次,一次握手的成功率和3次差不多.一次容易被攻击.如果黑客循环的发送SYN,这个时候服务端会立马认识连接建立好了,我们主机只发SYN,什么都不做,这就造成服务端会消耗大量的空间,这就是SYN洪水.两次握手为何是不行呢?和一次是一样的,服务端只能接受一次SYN,它确认报文发回来报文的时候,服务端就认为连接建立好了,也有可能造成SYN洪水.

那么三次呢?前面的根本原因事服务端先认为连接建立好了,这里我们先让客户端先建立好,只有当服务端拿到第二次确认,服务端才会认为可以建立链接了,至于服务端确认报文是否丢失事是客户端的事,服务端不关心.这样即使有人攻击服务端,我也要把你拉下水.拼资源.这里黑客可能会洒下一些木马病毒,非法链接,邮件到网上的电脑,在规定的时间内统一访问一个特定的网站,也有可能造化SYN洪水.

TCP为何是三次握手呢?你会发现客户端和服务端发送和接受都存在,又是一个验证了全双工,3次是最小的成本.我们是可以握3次以上,但是解决不了问题,我们是根据资源的考量.

如果服务端没有收到最后一个ACK,但是客户端已经认为连接建立成功了,但是服务端却认为他没有建立成功,我们知道服务端会超时重传.那么在超时重传的这个时间呢?客户端认为连接建立好了,服务端没有,那么客户端会干什么?他会立马给服务器发消息,但是一旦服务器看到这些数据,他会立马认识到此时我们的连接出现了问题,此时服务端会里面给客户端发送一个报文,其中RST被设置成1,告诉客户端里面把链接经行重置,

这里回答一个问题,我们说再一次发送数据的时候是如何知道对方的接受能力的,在正式建立连接的时候我们已经进行了相互信息的交互,此时里面就有双方的窗口大小.

TCP四次挥手

当有一方进行要断开连接的时候,他会给对方发送FIN标志位,此时对方给他相应,对方也要发送FIN标志位,我们也需要给对方进行相应,这就是我想和你断开代表着我不想和你发送消息了,但是你想给我发还是可以的.

TCP/UDP协议

请问,为何是4次挥手,我们好像直接相互发送一个FIN就够了,理论上是要2次就够了.这是客户端发的是如何保证服务端知道的,这里必须要有ACK,所以是4次.那么如果我们服务端进行回应的时候,我也给客户端进行响应,顺便发送FIN链接不可以吗?但是你想和我断开连接,不代表我想和你断开连接但是这里有时间查,我还有数据没有处理完成,等我处理好后再和你断开连接也是可以的,如果非要巧合的,三次也是可以的.

服务端状态转化:

  • [CLOSED->LISTEN]服务器端调用listen后进入LISTEN状态,等待客户端连接.

  • [LISTEN->SYN_RCVD]一旦监听到连接请求(同步报文段),就将该连接放入内核等待队列中,并向客户端发送SYN确认报文.

  • [SYN_RCVD->ESTABLISHED]服务端一旦收到客户端的确认报文,就进入ESTABLISHED状态,可以进行读写数据了.

  • [ESTABLISHED->CLOSE_WAIT]当客户端主动关闭连接(调用close),服务器会收到结束报文段,服务器返回确认报文段并进入CLOSE_WAIT.

  • [CLOSE_WAIT->LAST_ACK]进入CLOSE_WAIT后说明服务器准备关闭连接(需要处理完之前的数据).当服务器真正调用close关闭连接时,会向客户端发送FIN,此时服务器进入LAST_ACK状态,等待最后一个ACK到来(这个ACK是客户端确认收到了FIN)

  • [LAST_ACK->CLOSED]服务器收到了对FIN的ACK,彻底关闭连接

客户端状态变换

  • [CLOSED->SYN_SENT]客户端调用connect,发送同步报文段.
  • [SYN_SENT->ESTABLISHED]connect调用成功,则进入ESTABLISHED状态,开始读写数据.
  • [ESTABLISHED->FIN_WAIT_1]客户端主动调用close时,向服务器发送结束报文段,同时进入FIN_WAIT_1.
  • [FIN_WAIT_1->FIN_WAIT_2]客户端收到服务器对结束报文段的确认,则进入FIN_WAIT_2,开始等待服务器的结束报文段.
  • [FIN_WAIT_2->TIME_WAIT]客户端收到服务器发来的结束报文段,进入TIME_WAIT,并发出LAST_ACK.
  • [TIME_WAIT->CLOSED]客户端要等待一个2MSL(MaxSegmentLife,报文最大生存时间)的时间,才会进入CLOSED状态.

请问,为何前面的3个链接我们都可以建立链接成功,一个被停留在建立的连接中,此时我们要解我们套接字那里遗留的一个问题.

TCP/UDP协议

TCP/UDP协议

CLOSE_WAIT

验证CLOSE_WAIT状态,当客户端主动关闭连接(调用close),服务器会收到结束报文段,服务器返回确认报文段并进入CLOSE_WAIT.

TCP/UDP协议

TIME_WAIT

主动断开连接的一方会进入TIME_WAIT状态,处于TIME_WAIT状态之间,我们的服务是没有办法重启的,处于timewait的连接虽然名存实亡,但是连接还在.为何还存在这个缓冲时间呢,我们最后一个ACK不知道对方是不是可以被收到,如果丢了,对方在前面发了FIN,这个对方会进行超时重传.

TCP/UDP协议

处于timewait后,当最后的一个ACK没有被对方收到,此时对方会尽心超时重传,也就是的没有消息就是好消息.

  • TCP协议规定,主动关闭连接的一方要处于TIME_WAIT状态,等待两个MSL(maximumsegmentlifetime)的时间后才能回到CLOSED状态.
  • 我们使用Ctrl-C终止了server,所以server是主动关闭连接的一方,在TIME_WAIT期间仍然不能再次监听同样的server端口.
  • MSL在RFC1122中规定为两分钟,但是各操作系统的实现不同,在Centos7上默认配置的值是60s.

想一想,为什么是TIME_WAIT的时间是2MSL?保证曾经的历史数据都能被对方收到,所以我们称之位最大时间MSL

TCP/UDP协议

滑动窗口机制

刚才我们讨论了确认应答策略,对每一个发送的数据段,都要给一个ACK确认应答.收到ACK后再发送下一个数据段.这样做有一个比较大的缺点,就是性能较差.尤其是数据往返的时间较长的时候.

TCP/UDP协议

既然这样一发一收的方式性能较低,那么我们一次发送多条数据,就可以大大的提高性能(其实是将多个段的等待时间重叠在一起了),这种机制就被称为滑动窗口机制

TCP/UDP协议

  • 窗口大小指的是无需等待确认应答而可以继续发送数据的最大值.上图的窗口大小就是4000个字节(四个段).

  • 发送前四个段的时候,不需要等待任何ACK,直接发送.

  • 收到第一个ACK后,滑动窗口向后移动,继续发送第五个段的数据.依次类推.

  • 操作系统内核为了维护这个滑动窗口,需要开辟发送缓冲区来记录当前还有哪些数据没有应答.只有确认应答过的数据,才能从缓冲区删掉

  • 窗口越大,则网络的吞吐率就越高

由于存在滑动窗口,由于我们数据可能会丢失,那么我们在滑动窗口的数据需要保存下来,只能在收到相关的相应之后我们我们才会进行数据的清洗,必须要支持超时重传机制.前面我们已经知道了TCP是具有缓冲区的,我们数据要保存在发送缓冲区中.

TCP/UDP协议

我们知道对于数据发送会存在两个情况,发送成功和发送失败.此时数据发送是否成功决定这我们数据是否被清洗.对于发送缓冲区而言,我们可以分为下面的几个部分

TCP/UDP协议

那么请问我们滑动窗口的如何移动,很简单,只要我们发送的数据收到相应的ACK时,我们把右侧的相应移动.

TCP/UDP协议

如何理解缓冲区

很简单,我们这样理解,我们直接把看成一个char类型的数组,所谓的缓冲区本质就是要两个下标,所谓的缓冲区移动就是数组下标增加的过程.这里还有一个问题,我们数组下标递增,难道不会越界吗?注意,我们这里看的是一个直线性的,实际底层是一个环状态的.

那么滑动窗口的大小是有谁决定呢?这里很简单,目前是由对方的接受能力决定的.会存在下面的一个情况,我接受能力不断减少,我们的滑动窗口右侧可能不会动.那start适合如何移动的?

TCP/UDP协议

那么如果出现了丢包,如何进行重传?这里分两种情况讨论.

数据包已经抵达,ACK被丢了,这种情况下,部分ACK丢了并不要紧,因为可以通过后续的ACK进行确认.

TCP/UDP协议

快重传机制

当某一段报文段丢失之后,发送端会一直收到1001这样的ACK,就像是在提醒发送端"我想要的是1001"一样.如果发送端主机连续三次收到了同样一个"1001"这样的应答,就会将对应的数据1001-2000重新发送.这个时候接收端收到了1001之后,再次返回的ACK就是7001了(因为2001-7000)接收端其实之前就已经收到了,被放到了接收端操作系统内核的接收缓冲区中.这种机制被称为"高速重发控制"(也叫"快重传").

TCP/UDP协议

拥塞控制

我们存在一个问题,我们把只是据给对方主机吗?不对的.我们是先给网络的,上面我们说了很多的东西都是考虑的主机,从来没有考虑过网络的.那么谁来考虑网络的感受呢?为何TCP要考虑网络呢?要知道网络是存在很多的主机的.比如我们在学校里面考试,如果100人有2个人挂科了,是自己的问题,可是要有98人挂课了就是老师的问题了,这里亦是如此.TCP是需要考虑网络的问题的,所以引入测拥塞控制.

虽然TCP有了滑动窗口这个大杀器,能够高效可靠的发送大量的数据.但是如果在刚开始阶段就发送大量的数据,仍然可能引发问题.
因为网络上有很多的计算机,可能当前的网络状态就已经比较拥堵.在不清楚当前网络状态下,贸然发送大量的数据,是很有可能引起雪上加霜的.TCP引入慢启动机制,先发少量的数据,探探路,摸清当前的网络拥堵状态,再决定按照多大的速度传输数据.

TCP/UDP协议

更新一下窗口大小的概念,以前一个发送方都对方发送数据时,最多可以以目标主机窗口大小的数量,此时窗口大小等于对方的接受能力,今天要考虑网路的承受能力,这里是他们滑动窗口的大小是两个的较小值.

此处引入一个概念程为拥塞窗口

  • 发送开始的时候,定义拥塞窗口大小为1.
  • 每次收到一个ACK应答,拥塞窗口加1.
  • 每次发送数据包的时候,将拥塞窗口和接收端主机反馈的窗口大小做比较,取较小的值作为实际发送的窗口.

像上面这样的拥塞窗口增长速度,是指数级别的."慢启动"只是指初使时慢,但是增长速度非常快.为了不增长的那么快,因此不能使拥塞窗口单纯的加倍.此处引入一个叫做慢启动的阈值当拥塞窗口超过这个阈值的时候,不再按照指数方式增长,而是按照线性方式增长

当TCP开始启动的时候,慢启动阈值等于窗口最大值,在每次超时重发的时候,慢启动阈值会变成原来的一半,同时拥塞窗口设置为1.

TCP/UDP协议

延迟应答

在不考虑拥塞的情况下,我们窗口大小越大此时我们数据的吞吐效率越高.如果接收数据的主机立刻返回ACK应答,这时候返回的窗口可能比较小.

  • 假设接收端缓冲区为1M.一次收到了500K的数据.如果立刻应答,返回的窗口就是500K.
  • 但实际上可能处理端处理的速度很快,10ms之内就把500K数据从缓冲区消费掉了.
  • 在这种情况下,接收端处理还远没有达到自己的极限,即使窗口再放大一些,也能处理过来.
  • 如果接收端稍微等一会再应答,比如等待200ms再应答,那么这个时候返回的窗口大小就是1M.

一定要记得,窗口越大,网络吞吐量就越大,传输效率就越高.我们的目标是在保证网络不拥塞的情况下尽量提高传输效率.那么所有的包都可以延迟应答么?肯定也不是.

  • 数量限制:每隔N个包就应答一次.
  • 时间限制:超过最大延迟时间就应答一次

具体的数量和超时时间,依操作系统不同也有差异.一般N取2,超时时间取200ms.

捎带应答

在延迟应答的基础上,我们发现,很多情况下,客户端服务器在应用层也是"一发一收"的.意味着客户端给服务器说了"Howareyou",服务器也会给客户端回一个"FINe,thankyou".那么这个时候ACK就可以搭顺风车,和服务器回应的"FINe,thankyou"一起回给客户端

粘包问题

首先要明确,粘包问题中的"包",是指的应用层的数据包.在TCP的协议头中,没有如同UDP一样的"报文长度"这样的字段,但是有一个序号这样的字段.站在传输层的角度,TCP是一个一个报文过来的.按照序号排好序放在缓冲区中.站在应用层的角度,看到的只是一串连续的字节数据.那么应用程序看到了这么一连串的字节数据,就不知道从哪个部分开始到哪个部分,是一个完整的应用层数据包.

那么如何避免粘包问题呢?归根结底就是一句话,明确两个包之间的边界.对于定长的包,保证每次都按固定大小读取即可.例如上面的Request结构,是固定大小的,那么就从缓冲区从头开始按sizeof(Request)依次读取即可.对于变长的包,可以在包头的位置,约定一个包总长度的字段,从而就知道了包的结束位置.对于变长的包,还可以在包和包之间使用明确的分隔符(应用层协议,是程序猿自己来定的,只要保证分隔符不和正文冲突即可).

思考:对于UDP协议来说,是否也存在"粘包问题"呢?

对于UDP,如果还没有上层交付数据,UDP的报文长度仍然在.同时,UDP是一个一个把数据交付给应用层.就有很明确的数据边界.站在应用层的站在应用层的角度,使用UDP的时候,要么收到完整的UDP报文,要么不收.不会出现"半个"的情况.

字节流

创建一个TCP的socket,同时在内核中创建一个发送缓冲区和一个接收缓冲区.调用write时,数据会先写入发送缓冲区中.如果发送的字节数太长,会被拆分成多个TCP的数据包发出.如果发送的字节数太短,就会先在缓冲区里等待,等到缓冲区长度差不多了,或者其他合适的时机发送出去.接收数据的时候,数据也是从网卡驱动程序到达内核的接收缓冲区.然后应用程序可以调用read从接收缓冲区拿数据.

另一方面,TCP的一个连接,既有发送缓冲区,也有接收缓冲区,那么对于这一个连接,既可以读数据,也可以写数据.这个概念叫做全双工.由于缓冲区的存在,TCP程序的读和写不需要一一匹配,例如:

写100个字节数据时,可以调用一次write写100个字节,也可以调用100次write,每次写一个字节.读100个字节数据时,也完全不需要考虑写的时候是怎么写的,既可以一次read100个字节,也可以一次read一个字节,重复100次.

TCP异常情况

下面说一下TCP一些异常的情况.

  • 进程终止:进程终止会释放文件描述符,仍然可以发送FIN.和正常关闭没有什么区别.
  • 机器重启:和进程终止的情况相同.
  • 机器掉电/网线断开:接收端认为连接还在,一旦接收端有写入操作,接收端发现连接已经不在了,就会进行reset.即使没有写入操作,TCP自己也内置了一个保活定时器,会定期询问对方是否还在.如果对方不在,也会把连接释放.

另外,应用层的某些协议,也有一些这样的检测机制.例如HTTP长连接中,也会定期检测对方的状态.例如QQ,在QQ断线之后,也会定期尝试重新连接.