TCP篇2

本文最后更新于:2022年7月29日 下午

计算机网络中的TCP部分,篇一是仓促之间写的,不是很完整,本篇和篇一是补充关系。但是也可以完全跳过篇一直接看本文

TCP篇2

TCP基础认识

TCP协议头认识

还是拿出 维基百科传输协议控制 的图吧,相信很多人已经看过并完全了解了,这里只做一些简单的讲解TCP协议头

  1. 端口:包括了源端口和目标端口,
    1. 为什么不需要地址?是因为网络层的IP协议已经包含了目标地址。
    2. 源端口地址和目标端口地址从哪里来?源端口地址一般是由系统随机指定的一个地址(通常情况是当前最小的端口),而目标端口是人为指定的,比如说我们访问网页的时候输入 www.a.com/index.html 的时候,看似我们只输入了域名,没有输入端口,那是因为浏览器默认了http连接对方服务器的端口是80端口。
  2. 序列号码:在建立连接时由计算机生成的随机数字作为其初始值,通过SYN包传递给服务器主机,每发送一次数据就累加一次该数据字节数的大小。主要用来解决网络乱序问题
  3. 确认号码:指下一次期望收到的数据的序列号,发送收到这个ACK号码就可以认为该序列号之前的数据都已经正常接收。主要用来解决丢包问题
  4. 标志位:标志位为1时激活标志位
    1. ACK:确认应答码
    2. SYN:表示希望建立连接,并在其序列号吗的字段进行序列号码初始化
    3. FIN:表示以后不再有数据发送,希望关闭连接
    4. RST:表示连接中出现异常必须强制关闭连接

为什么需要TCP协议

为了保证网络数据包的可靠传输,因为工作在网络层的IP协议时不可靠的,它不保证网络包交互的可靠性、有序性、完整性等。

什么是TCP

TCP全称是传输控制协议,是面向连接的、可靠的、基于字节流的传输层通信协议。

  1. 面向连接:TCP协议必须是一对一连接,
  2. 可靠的:无论网络链路出现了什么变化,TCP协议都能够保证一个报文能够到达接收端
  3. 基于字节流:消息是没有边界的,所以无论我们消息多大都可以进行传输

什么是TCP连接

所谓TCP连接就是双方都维护了一个状态信息,并且在每次通信中都去更新这信息。这些状态信息包括了:

  1. Sockets:包括IP地址和端口号
  2. 序列号:用来解决乱序问题
  3. 窗口大小:用来做流量控制

如何唯一确定一个TCP连接

TCP四元组{目标地址、目标端口、源地址、源端口}可以确定一个TCP连接。

一台服务器的理论TCP最大连接数

我们假设服务端固定一个端口监听,等待客户端的请求,那么客户端的IP和客户端的端口是可变的,因此我们可以得到

1
最大TCP连接数 = 客户端IP数 * 客户端端口数

针对 IPv4 来说,客户端的IP数为 2322^{32},客户端的端口数为 2162^{16}。这也就是说服务端一个端口的最大连接数为2482^{48}。一个服务器的TCP最大连接数只需要再乘上服务端最大监听端口数即可。

当然,实际上一台服务器的最大连接数远远不能达到理论连接数。一般来说会受制于两个方面

  1. 文件描述符限制。Socket都是文件,所以有文件描述符限制
  2. 内存限制,每一个连接都要消耗一定的内存。

TCP和UDP的区别

上面说了那么多TC P,这里就先介绍下UDP。

UDP不提供复杂的控制机制,它利用IP提供无连接的通信服务。我们也来看一下 维基百科UDP 的头部描述图吧

维基百科UDP

这里可看到和TCP的庞然大物相比UDP的头部描述图真的非常简单。它的头部只占了8个字节64位。接下来我们一一看下这几个字段

  1. 端口:包括源端口和目标端口,这个没什么好说的。
  2. 报文长度:指定UDP报文头部和数据总共占用的长度。最小为8个字节(空数据部分)。有于这个字段的存在,所以UDP报文的总长度不能大于21612^{16}-1 字节。
  3. 校验和:校验和字段可以用于发现头部信息和数据中的传输错误

其实介绍了TCP和UDP的报文区别之后我们就可以大致了解区别了

  1. 连接
    1. TCP是面向连接的,传输数据之前需要建立连接
    2. UDP不需要连接,直接可以传输数据
  2. 服务对象
    1. TCP是一对一的
    2. UDP支持一对一、一对多、多对多
  3. 可靠性
    1. TCP是可靠的,可以保证数据无差错、不丢失、不重复、按需到达
    2. UDP不保证可靠的交付数据
  4. 拥塞控制、流量控制
    1. TCP 有拥塞控制和流量控制机制,保证数据传输的安全性。
    2. UDP 则没有,即使网络非常拥堵了,也不会影响 UDP 的发送速率。
  5. 首部开销
    1. UDP 首部只有 8 个字节,并且是固定不变的,开销较小。
    2. TCP 首部长度较长,会有一定的开销,首部在没有使用「选项」字段时是 20 个字节,如果使用了「选项」字段则会变长的。

为什么TCP有首部长度字段而UDP没有

因为TCP是有选项扩充的,如果没有首部长度无法定义出首部有多长,但是UDP的首部长度是固定的(8字节),所以不需要首部长度

TCP连接建立

TCP三次握手

三次握手是我们的老朋友了,TCP连接是通过三次握手建立的,照旧例先来看看三次握手的图吧

image-20210716094059229

其中图中的x、y指是客户端、服务端的ISN序列号

注意:第三次握手是可以传递数据的,前两次握手是不可以携带数据的。

如何在Linux中查询TCP状态

可以通过 netstat -anpt 命令查看

为什么是三次握手而不是四次

我们都知道三次握手是建立连接的,通过 上文 也知道了连接是什么,那么为什么三次握手才能确定Socket信息、窗口大小、序列号呢。

其实三次握手主要有三个原因:

  1. 三次握手才能阻止历史重复连接的初始化 (主要原因)
  2. 确保双方都能进行收发操作,同时同步双方的序列号
  3. 三次握手才能避免资源浪费

三次握手才能阻止历史重复连接初始化

The principle reason for the three-way handshake is to prevent old duplicate connection initiations from causing confusion.

​ — RFC 793

三方握手的主要原因是防止旧的重复连接启动造成混淆

网络环境是错综复杂的。往往并不是从如我们期望的一样,有可能后发送的包先到达服务器,或者先发送的包后到达服务器,那么有可能出现这种情况

  1. 客户端发送SYN请求,Seq填充100,此时网络拥堵,请求阻塞在某个不知名的小道
  2. 客户端发送新的SYN请求,Seq填充101,此时网络依然拥堵
  3. 服务端收到了Seq为100的SYN请求,并且发送了ACKNum为101的ACK响应
  4. 客户端接收到ACKNum为101的响应,发现本应该接受到的ACK响应应该是102,此时会发送RST中断当前连接
  5. 服务端收到Seq为101的请求并建立连接

我们发现,在三次握手时,客户端时可以通过上下文判断出当前连接是否时历史连接,从而选择发送RST报文或者ACK报文。

同步双方的序列号

TCP协议的通信双方,都必须维护一个序列号,通过这个序列号我们可以:

  1. 接收方可以去除重复数据
  2. 接受方可以根据数据包的序列号按序处理
  3. 可以标识出发送方哪些发送出去的数据包是被接受方接受的

由此可见,序列号在通信中至关重要,那么如果需要同步双方之间的序列号应该怎么做呢,

  1. 首先由客户端发送一条携带自身Seq信息的包给服务端
  2. 服务端这个时候就需要告诉客户端我已经知道了你的序列码,于是给客户端发送了一条ACK报文,里面携带了ACKNum信息,并且ACKNum=SeqNum+1
  3. 到这一步服务端已经知道了客户端的序列号,但是客户端还不知道服务端的序列号呢,于是服务端主动给客户端发送一条携带自身SeqNum信息的报文
  4. 客户端接收到了服务端的Seq号,发送一条ACK信息给服务端告诉我已经知道了你的序列号

这里就已经知道了为什么需要三次握手了。有些小伙伴看到这里就很奇怪了,你说的不是四次握手嘛。这就是一步优化了,看这里 给客户端发送了一条ACK报文,里面携带了ACKNum信息,服务端主动给客户端发送一条携带自身SeqNum信息的报文,仔细想一想TCP报文中是不是支持同时发送ACK、SYN报文。那么这两步可以合并成一步,也就是三次握手了。

这里在同步双方序列号的同时还可以知道,双方都是由发送和接收的功能

三次握手才能避免资源浪费

如果只有两次握手,那么可能会建立多个冗余的无效链接,造成不必要的资源浪费

  1. 客户端发送SYN请求,Seq填充100,此时网络拥堵,请求阻塞在某个不知名的小道(怎么感觉这话在哪里说过)
  2. 客户端认为发送超时了,重新发送了第二次请求,这时候服务端响应了请求并连接建立
  3. 一段时间之后,原本Seq为100的SYN服务又达到了服务端。服务端再次发送ACK请求到客户端,客户端又会建立一次连接

即两次握手会照成消息滞留情况下,服务器接受了重复无用的连接请求报文,从而造成资源浪费

小结

TCP建立连接时,只有三次握手才能防止历史连接的建立、减少双方不必要的资源开销、同步双方序列号并确保双方都有发送和接收的能力

为什么不是两次握手、四次握手

  • 三次握手就已经足够建立连接了,不需要进行四次握手
  • 两次握手无法避免资源情况、无法防止历史连接、无法同步双方序列号

为什么ISN不固定

ISN即 Initial Sequence Number (初始序列号),在TCP连接中是很重要的数据。为什么不能固定有两个方面的原因

  1. 从安全的角度:ISN设定为固定的值很容易被攻击者猜出后续的Seq,这样攻击者就可以模拟TCP协议发送RST协议强行中止连接或者做一些过分的事情
  2. 从稳定性角度:ISN为固定值的话,可能出现这种情况,上一个连接在发送报文时断开,下一次连接的时候接收到了上一次连接的报文,会引发异常

初始序列号ISN是如何随机产生的

ISN是基于时钟的,每4毫秒加1,转一圈之后归零

之后在 RFC 1948 中提出了另外一个ISN随机算法:

ISN = M+F(localhost, localport, remotehost, remoteport)

其中M是计时器,F是hash算法

既然IP层会分片,为什么TCP层还需要MSS

我们先来认识一下什么是MSS和MTU,其实MSS就是TCP数据部分的最大长度,MTU就是一个网络包的最大长度,图片引用敖丙的博客

MSS和MTU

我们可以思考一下如果我们只通过MTU来进行分包的话会发生什么,还记得之前我们说过TCP协议是可以可靠的吗?它永远(这里有点绝对了)都不会漏包,那么我真的漏包了怎么办呢,TCP协议会要求客户端重传,如果我们按照只使用MTU来进行分割的话,那么当一个分片丢失了,整个IP报文都必须要重传

因此,可以得知由 IP 层进行分片传输,是非常没有效率的。

所以,为了达到最佳的传输效能 TCP 协议在建立连接的时候通常要协商双方的 MSS 值,当 TCP 层发现数据超过 MSS 时,则就先会进行分片,当然由它形成的 IP 包的长度也就不会大于 MTU ,自然也就不用 IP 分片了。

连接队列

三次握手前,服务端的状态从 Closed 转为 Listen,同时会在内部创建两个队列:

  1. 半连接队列:又叫SYN队列,当客户端发送到SYN到服务端时,服务端收到请求并回复SYN+ACK,此时状态变为SYN_RCVD,同时把这个连接推送到了半连接队列中。
  2. 全连接队列:当客户端响应ACK之后,服务端接收到,三次握手完成。这个时候就等着这个连接被某个引用取走,被取走之前,他会被推送到另外一个队列,也就是全连接队列

什么是SYN攻击,如何避免SYN攻击

SYN攻击时典型的DDos攻击。通过创建大量的SYN请求到服务端,但是却不响应ACK+Seq。由于一直接收不到ACK,会占满半连接队列。这会导致服务器不能为正常用户工作

避免的方式也很简单,主要有以下几个思路

  1. 增加SYN队列的容量
  2. 利用SYN Cookie技术,在服务端接收到SYN请求后,不再进入SYN队列,而是计算出一个Cookie,连同第二次握手发送给客户端,在客户端恢复ACK之后带上Cookie,服务端严重Cookie合法之后才分配资源

TCP连接断开

TCP四次挥手

还是上图吧,和三次握手一样有名的图 图片引用敖丙的博客 ,欢迎大家访问原站,本文大致框架就是引用这篇文章

TCP四次挥手

从图中可以看到,每个方向都需要一个FIN报文和ACK报文,因此我们叫它为 四次挥手

只有主动关闭连接的,才会出现TIME_WAIT状态

为什么需要四次挥手

我们来理解一下FIN包是什么意思就能够理解为什么需要四次挥手了,这里引用维基百科的描述

FIN—为1表示发送方没有数据要传输了,要求释放连接。

注意,这里只是说没有数据传输了,但是不代表不可以接受数据了啊。所以发送方需要发送一次FIN报文告诉服务端我没有数据需要发送了,服务端回复一个ACK报文表示我明白你没有数据传输了,但是服务端本身可能还有数据没有处理完或者还有数据需要发送给客户端,所以还不能释放链接,等到服务端处理完数据或者发送完数据,才发送FIN报文给客户端表示同意关闭链接

从上面的过程可知。服务端通常需要等待数据的处理或者发送,因此服务端的FIN报文、ACK报文一般都会分开发送,从而比三次握手多了一次

为什么需要TIME_WAIT状态

我们可以看到客户端需要经历 FIN_WIAT_1、FIN_WAIT_2、TIME_WAIT状态才能关闭一个链接,其中FIN_WIAT_1、FIN_WAIT_2 应该都比较好理解,只是等待一些报文而已,但是为什么需要 TIME_WAIT 状态呢。

  1. 防止旧连接的数据包干扰。假设这么一个情况,服务端主动发送一个A报文被网络延时了,此时客户端主动关闭连接,并且客户端没有 TIME_WAIT ,在之后很短的时间之内,相同四元组的连接被复用,之前的A报文终于发送到了客户端,那么这个时候数据包就被扰乱了。而有 TIME_WAIT 这个机制的话,经过 2MSL 这个时间,足以让两个方向上的数据包都被丢弃,使得原来连接的数据包在网络中都自然消失,再出现的数据包一定都是新建立连接所产生的。
  2. 保证连接正确关闭。实际上我们知道网络并不是永远都可达的。我们看四次挥手的最后一步,客户端发送ACK请求到达服务端。那么这次ACK请求可能因为网络问题而丢弃了,这个时候就需要重传了,如果没有 TIME_WAIT 这个链接,服务端就不能正常关闭了

TIME_WAIT 的值 2MSL 有什么意义

首先我们来看下 MSL 是什么意思吧。MSL 全称 Maximum Segment Lifttime,即报文最大生存时间,它是任何报文在网络上的最大时间,超过这个时间报文就会被丢弃。

TIME_WAIT 等待 2MSL 比较合理的解释为:网络上可能存在来自发送方的数据包,当这些发送方的数据包被接受方处理后又会向对方发送响应,所以一来一回需要等待2倍的时间。

TIME_WAIT 时间多长有什么危害

主要的危害其实还是源于连接没有被完全关闭(至少发起方是这么认为的),那么如果是服务器主动发起的挥手操作,那么可能会导致

  1. 内存资源占用问题
  2. 对端口资源的占用,一个TCP连接至少要消耗一个本地端口

如果服务端的 TIME_WAIT 状态过多,会占满所有的端口资源导致无法创建新的连接

优化TIME_WAIT

这里给出优化的几种方式,都是有利有弊的

  1. 打开 net.ipv4.tcp_tw_reuse 和 net.ipv4.tcp_timestamps 选项;可以复用处于 TIME_WAIT 的 socket 为新的连接所用
  2. net.ipv4.tcp_max_tw_buckets,当系统中处于 TIME_WAIT 的连接一旦超过这个值时,系统就会将所有的 TIME_WAIT 连接状态重置。
  3. 程序中使用 SO_LINGER ,应用强制使用 RST 关闭。当服务端需要调用关闭连接的时候,会立该发送一个RST标志给对端,该 TCP 连接将跳过四次挥手,也就跳过了TIME_WAIT状态,直接关闭

TCP连接已建立,但是客户端崩溃了

TCP有一个保活机制,这个机制的原理大概是这样的:

定义一个时间段,在这个时间段内,如果没有任何连接相关的活动,TCP 保活机制会开始作用,每隔一个时间间隔,发送一个探测报文,该探测报文包含的数据非常少,如果连续几个探测报文都没有得到响应,则认为当前的 TCP 连接已经死亡,系统内核将错误信息通知给上层应用程序。

写在最后

2021-07-16 10:00

又到了我扯犊子说P话的时间了嘛。。。没想到这一篇文章在上班、下班时间断断续续写了那么久,也总算是写完了。

南昌最近几天是真的热,热到爆炸,我昨天回家在客厅写博客,字倒是没写几个,汗流了半斤。


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!