TCP的基本认识

TCP头格式

TCP握手与断开 - 图2
序列号:在建立连接时由计算机生成的随机数作为其初始值,通过 SYN 包传给接收端主机,每发送一次数据,就「累加」一次该「数据字节数」的大小。用来解决网络包乱序问题。

确认应答号:指下一次「期望」收到的数据的序列号,发送端收到这个确认应答以后可以认为在这个序号以前的数据都已经被正常接收。用来解决不丢包的问题。

控制位:

  • ACK:该位为 1 时,「确认应答」的字段变为有效,TCP 规定除了最初建立连接时的 SYN 包之外该位必须设置为 1 。
  • RST:该位为 1 时,表示 TCP 连接中出现异常必须强制断开连接。
  • SYN:该位为 1 时,表示希望建立连接,并在其「序列号」的字段进行序列号初始值的设定。
  • FIN:该位为 1 时,表示今后不会再有数据发送,希望断开连接。当通信结束希望断开连接时,通信双方的主机之间就可以相互交换 FIN 位置为 1 的 TCP 段。
  • PSH:发送端需要发送一段数据,这个数据需要接收端一收到就进行向上交付,而接收端在收到PSH标志位有效的数据时,迅速将数据交付给应用层。所以PSH被称为急迫比特。(不越过缓冲区)
  • URG :成为紧急指针,意为URG位有效的数据包,是一个紧急需要处理的数据包,需要接收端在接收到之后迅速处理。(越过缓冲区)

为什么需要TCP协议?TCP工作在哪一层?

IP 层是「不可靠」的,它不保证网络包的交付、不保证网络包的按序交付、也不保证网络包中的数据的完整性。
如果需要保障网络数据包的可靠性,那么就需要由上层(传输层)的 TCP 协议来负责。
因为 TCP 是一个工作在传输层可靠数据传输的服务,它能确保接收端接收的网络包是无损坏、无间隔、非冗余和按序的。

什么是TCP?

TCP 是面向连接**的、可靠的、基于字节流的**传输层通信协议。

  • 面向连接:一定是「一对一」才能连接,不能像 UDP 协议 可以一个主机同时向多个主机发送消息;

  • 可靠的:无论的网络链路中出现了怎样的链路变化,TCP 都可以保证一个报文一定能够到达接收端;

  • 字节流:消息是「没有边界」的,所以无论我们消息有多大都可以进行传输。并且消息是「有序的」,当「前一个」消息没有收到的时候,即使它先收到了后面的字节已经收到,那么也不能传给应用层去处理,同时对「重复」的报文会自动丢弃。

TCP连接:用于保证可靠性和流量控制维护的某些状态信息,这些信息的组合,包括Socket、序列号和窗口大小称为连接。

TCP如何保证可靠性?
1、数据包校验
2、对失序数据包重排序
3、丢失重复数据
4、应答机制
5、超时重发
6、流量控制

**

确定唯一的TCP连接

四元组

TCP和UDP的区别?

UDP 不提供复杂的控制机制,利用 IP 提供面向「无连接」的通信服务。
UDP 协议真的非常简单,头部只有 8 个字节( 64 位),UDP 的头部格式如下:

TCP握手与断开 - 图3
目标和源端口:主要告诉UDP协议应该把报文发给哪个进程
包长度:该字段保存了UDP首部的长度和数据的长度之和
校验和:校验和是为了提供可靠的UDP首部和数据而设计

区别**
1. 连接

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

TCP连接建立

TCP 是面向连接的协议,所以使用 TCP 前必须先建立连接,而建立连接是通过三次握手而进行的。
TCP握手与断开 - 图4

  • 一开始客户端和服务端都是close状态,先是服务端主动监听某个端口,处于listen状态
  • 客户端随机初始化序列号的值置于TCP首部的【序号】字段中,同时将SYN标志位置为1,表示SYN报文。接着将该报文发送给服务端,表示向服务端建立连接,该报文不带数据。之后客户端的状态变为SYN_SENT状态。
  • 服务端收到客户端的 SYN 报⽂后,⾸先服务端也随机初始化⾃⼰的序号( server_isn ),将此序号填⼊TCP ⾸部的「序号」字段中,其次把 TCP ⾸部的「确认应答号」字段填⼊ client_isn + 1 , 接着把 SYN和 ACK 标志位置为 1 。最后把该报⽂发给客户端,该报⽂也不包含应⽤层数据,之后服务端处于 SYN-RCVD 状态。
  • 客户端收到服务端报⽂后,还要向服务端回应最后⼀个应答报⽂,⾸先该应答报⽂ TCP ⾸部 ACK 标志位置为 1 ,其次「确认应答号」字段填⼊ server_isn + 1 ,最后把报⽂发送给服务端,这次报⽂可以携带客户到服务器的数据,之后客户端处于 ESTABLISHED 状态。
  • 服务器收到客户端的应答报⽂后,也进⼊ ESTABLISHED 状态。

为什么是三次握手?

接下来以三个方面分析三次握手的原因:

  • 三次握手才可以阻止历史重复连接的初始化(主要原因)
  • 三次握手才可以同步双方的初始序列号
  • 三次握手才可以避免资源浪费

1、避免历史连接(首要原因)
客户端连续发送多次 SYN 建立连接的报文,在网络拥堵等情况下:

  • 一个「旧 SYN 报文」比「最新的 SYN 」 报文早到达了服务端;
  • 那么此时服务端就会回一个 SYN + ACK 报文给客户端;
  • 客户端收到后可以根据自身的上下文,判断这是一个历史连接(序列号过期或超时),那么客户端就会发送 RST 报文给服务端,表示中止这一次连接。

    两次连接无法实现:如果是历史连接,第三次握手发送的是RST报文,如果是新连接,发送ACK报文(第三次握手的目的主要是用于分辨这是一个正常的链接还是一个异常的链接)

2、同步双方初始序列号
TCP 协议的通信双方, 都必须维护一个「序列号」, 序列号是可靠传输的一个关键因素,它的作用:

  • 接收方可以去除重复的数据;
  • 接收方可以根据数据包的序列号按序接收;
  • 可以标识发送出去的数据包中, 哪些是已经被对方收到的;

一来一回,才能确保双方的初始序列号能被可靠的同步。

3、避免资源浪费
如果只有「两次握手」,当客户端的 SYN 请求连接在网络中阻塞,客户端没有接收到 ACK 报文,就会重新发送 SYN ,由于没有第三次握手,服务器不清楚客户端是否收到了自己发送的建立连接的 ACK 确认信号,所以每收到一个 SYN 就只能先主动建立一个连接,这会造成什么情况呢?
如果客户端的 SYN 阻塞了,重复发送多次 SYN 报文,那么服务器在收到请求后就会建立**多个冗余的无效链接**,造成不必要的资源浪费。

为什么客户端和服务端的初始序列号不同?
如果一个已经失效的连接被重用了,但是该旧连接的历史报文还残留在网络中,如果序列号相同,那么就无法分辨出该报文是不是历史报文,如果历史报文被新的连接接受了,则会产生数据错乱。

所以,每次建⽴连接前重新初始化⼀个序列号主要是为了通信双⽅能够根据序号将不属于本连接的报⽂段丢弃。
另⼀⽅⾯是为了安全性,防⽌⿊客伪造的相同序列号的 TCP 报⽂被对⽅接收。

既然IP会进行分片,为什么TCP还要MSS呢?

当 IP 层有一个超过 MTU 大小的数据(TCP 头部 + TCP 数据)要发送,那么 IP 层就要进行分片,把数据分片成若干片,保证每一个分片都小于 MTU。把一份 IP 数据报进行分片以后,由目标主机的 IP 层来进行重新组装后,在交给上一层 TCP 传输层。这看起来井然有序,但其实这是存在隐患的,那么当如果一个 IP 分片丢失,整个 IP 报文的所有分片都得重传

因为 IP 层本身没有超时重传机制,它由传输层的 TCP 来负责超时和重传。

当接收⽅发现 TCP 报⽂(头部 + 数据)的某⼀⽚丢失后,则不会响应 ACK 给对⽅,那么发送⽅的 TCP 在超时后,就会重发「整个 TCP 报⽂(头部 + 数据)」。

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

经过 TCP 层分片后,如果一个 TCP 分片丢失后,进行重发时也是以 MSS 为单位,而不用重传所有的分片,大大增加了重传的效率。

分片后就意味着要重组,这个步骤都是在网络层执行的。重组失败就要重发整个报文。而MSS则不需要这个步骤。


SYN攻击

攻击者短时间内伪造不同IP地址的SYN报文(请求连接报文),服务器每接收到一个SYN报文就进入到SYN_RCVD状态,但是发送出去的ACK+SYN报文却无法得到响应,久而久之就会占满服务端的SYN接收队列(未连接队列)。

TCP握手与断开 - 图5
避免SYN攻击方式一:
通过修改Linux内核参数,控制队列大小和当队列满时应该如何处理。

避免SYN攻击方式二:
先了解Linux内核的SYN(未完成连接建立)队列与Accept(已完成连接建立)队列是如何工作的?
TCP握手与断开 - 图6
1、当服务端接收到客户端的SYN报文时,会将其加入到内核的SYN队列。
2、接着发送SYN+ACK给客户端,等待客户端回应ACK报文。
3、服务端接收到ACK报文后,从SYN队列移出放到Accept队列中。
4、应用通过调用accept()socket接口,从Accept队列中取出连接。

如果应用程序的ACK响应过慢的话,Accept队列就会占满。 如果不断的受到SYN攻击,就会导致SYN队列被占满。

tcp_sybcookies的方式可以应对SYN攻击的方法:

  1. net.ipv4.tcp_syncookies = 1

TCP握手与断开 - 图7

  • 当 「 SYN 队列」满之后,后续服务器收到 SYN 包,不进入「 SYN 队列」,
  • 计算出一个 cookie 值,再以 SYN + ACK 中的「序列号」返回客户端,
  • 服务端接收到客户端的应答报文时,服务器会检查这个 ACK 包的合法性。如果合法,直接放入到「 Accept 队列」。
  • 最后应用通过调用 accpet() socket 接口,从「 Accept 队列」取出的连接。

TCP断开连接

TCP握手与断开 - 图8
1、客户端打算关闭连接,此时发送一个FIN报文给服务端,客户端进入FIN_WAIT_1状态。
2、服务端收到报文,向客户端发送ACK应答报文,服务端进入CLOSED_WAIT状态。
3、客户端收到服务端的ACK应答报文后,进入FIN_WAIT_2状态。
4、等待服务端处理完数据后,也向客户端发送FIN报文,之后服务端进入LAST_ACK状态。
5、客户端收到服务端的FIN报文后,回一个ACK应答报文,之后进入TIME_WAIT状态
6、服务器收到了 ACK 应答报文后,就进入了 CLOSE 状态,至此服务端已经完成连接的关闭。

7、客户端在经过 2MSL 一段时间后,自动进入 CLOSE 状态,至此客户端也完成连接的关闭。

每个方向都需要一个FIN和一个ACK,因此被称为四次挥手。

为什么需要四次挥手?

  • 关闭连接的时候,客户端向服务端发送FIN报文时,表示不再发送数据但是还能接收数据(之前未返还的数据)
  • 服务端收到客户端的FIN报文时,先回一个ACK报文,而服务端还有数据要处理和发送,等服务端不再发送数据时,才发送FIN报文表示同意关闭连接。

    从上面的过程可知,服务端通常需要等待完成数据的发送和处理,所以服务端的ACK和FIN报文需要分开发送,从而比三次握手多了一次操作。

为什么TIME_WAIT等待的时间是2MSL?

MSL是报文最大生存时间,是任何报文在网络上存在的最长时间。IP头中有一个TTL字段,是IP数据报可以经过的最大路由数,每经过一个处理他的路由器此值就减 1,当此值为 0 则数据报将被丢弃,同时发送 ICMP 报文通知源主机。

MSL与TTL的区别:MSL的单位是时间,而TTL是经过路由跳数。所以MSL应该要大于等于TTL消耗为0的时间,以确保报文已被自然消亡。

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

例如:如果服务端没有收到断开连接的ACK报文,就会重传FIN报文,客户端收到后发送ACK报文,一来一回正好2个MSL。

2MSL 的时间是从客户端接收到 FIN 后发送 ACK 开始计时的。如果在 TIME-WAIT 时间内,因为客户端的 ACK 没有传输到服务端,客户端又接收到了服务端重发的 FIN 报文,那么 2MSL 时间将重新计时。

为什么需要TIME_WAIT状态?

主动发起关闭连接的一方,才会有TIME_WAIT状态。
需要 TIME-WAIT 状态,主要是两个原因:

  • 防止具有相同「四元组」的「旧」数据包被收到;
  • 保证「被动关闭连接」的一方能被正确的关闭,即保证最后的 ACK 能让被动关闭方接收,从而帮助其正常关闭;

    关于第一点的详细说明:一个TCP端口不能被同时打开两次及以上。当一个TCP连接处于TIME_WAIT状态时,我们将无法立即使用该连接占用的端口来创建一个新的连接。反之,如果没有TIME_WAIT,则应用程序可以立即建立一个和刚关闭的连接相似的连接(这里说的相似,是指他们具有相同的IP地址和端口号)。这个新的、和原来相似的连接被称为原来的连接的化身(incarnation)。新的化身可能接收到属于原来的那个连接的、携带应用程序数据的TCP报文段,从而造成数据错乱。经过2MSL的时间,足以让两个方向上的数据包都被丢弃,再出现的数据包就一定是新建立连接产生的。

    关于第二点的详细说明:TIME-WAIT 作用是等待足够的时间以确保最后的 ACK 能让被动关闭方接收,从而帮助其正常关闭。 如果服务端没有收到最后的ACK报文,服务端将一直处于LAST_ACK状态。

TIME_WAIT过多的危害?

如果服务器有处于 TIME-WAIT 状态的 TCP,则说明是由服务器方主动发起的断开请求。
过多的 TIME-WAIT 状态主要的危害有两种:
1、第一是内存资源占用;

2、第二是对端口资源的占用,一个 TCP 连接至少消耗一个本地端口;

可以设置复用处于TIME_WAIT的socket为新的连接所用,前提是打开对TCP的时间戳支持,时间戳的字段是在 TCP 头部的「选项」里,用于记录 TCP 发送方的当前时间戳和从对端接收到的最新时间戳。解决2MSL问题,重复数据回因为时间戳被自然丢弃。当客户端与服务端主机时间不同步时,客户端的发送消息会被直接拒绝。

已经建立连接,客户端突然故障怎么办?

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

总结来说,就是服务端一段时间收不到客户端的消息,就发送消息试探客户端,多次试探无果认定死亡。

如果开启了 TCP 保活,需要考虑以下几种情况:
第一种,对端程序是正常工作的。当 TCP 保活的探测报文发送给对端, 对端会正常响应,这样 TCP 保活时间会被重置,等待下一个 TCP 保活时间的到来。

第二种,对端程序崩溃并重启。当 TCP 保活的探测报文发送给对端后,对端是可以响应的,但由于没有该连接的有效信息,会产生一个 RST 报文,这样很快就会发现 TCP 连接已经被重置。

第三种,是对端程序崩溃,或对端由于其他原因导致报文不可达。当 TCP 保活的探测报文发送给对端后,石沉大海,没有响应,连续几次,达到保活探测次数后,TCP 会报告该 TCP 连接已经死亡

Socket编程

针对TCP应该如何Socket编程?

**TCP握手与断开 - 图9
基于TCP协议的客户端和服务器工作

  • 服务端和客户端初始化 socket,得到文件描述符;
  • 服务端调用 bind,将绑定在 IP 地址和端口;
  • 服务端调用 listen,进行监听;
  • 服务端调用 accept,等待客户端连接;
  • 客户端调用 connect,向服务器端的地址和端口发起连接请求;
  • 服务端 accept 返回用于传输的 socket 的文件描述符;
  • 客户端调用 write 写入数据;服务端调用 read 读取数据;
  • 客户端断开连接时,会调用 close,那么服务端 read 读取数据的时候,就会读取到了 EOF,待处理完数据后,服务端调用 close,表示连接关闭。

    文件描述符:内核用以表示一个特定进程正在访问的文件,当打开一个现有文件或创建一个新文件时,内核向进程返回一个文件描述符。

listen时候参数backlog的意义?

Linux内核中会维护两个队列:

  • 未完成连接队列(SYN 队列):接收到一个 SYN 建立连接请求,处于 SYN_RCVD 状态;
  • 已完成连接队列(Accpet 队列):已完成 TCP 三次握手过程,处于 ESTABLISHED 状态;

早期Linux内核backlog是SYN队列大小,也就是未完成的队列大小
在Linux内核2.2之后,backlog变成accept队列,也就是已完成连接建立的队列长度,所以现在通常认为 backlog 是 accept 队列。
**

accept发送在三次握手的哪一步?

**TCP握手与断开 - 图10

客户端 connect 成功返回是在第二次握手,服务端 accept 成功返回是在三次握手成功之后。

**

客户端调用close时,连接断开的流程是什么?

TCP握手与断开 - 图11

  • 客户端调用 close,表明客户端没有数据需要发送了,则此时会向服务端发送 FIN 报文,进入 FIN_WAIT_1 状态;
  • 服务端接收到了 FIN 报文,TCP 协议栈会为 FIN 包插入一个文件结束符 EOF 到接收缓冲区中,应用程序可以通过 read 调用来感知这个 FIN 包。这个 EOF 会被放在已排队等候的其他已接收的数据之后,这就意味着服务端需要处理这种异常情况,因为 EOF 表示在该连接上再无额外数据到达。此时,服务端进入 CLOSE_WAIT 状态;
  • 接着,当处理完数据后,自然就会读到 EOF,于是也调用 close 关闭它的套接字,这会使得会发出一个 FIN 包,之后处于 LAST_ACK 状态;
  • 客户端接收到服务端的 FIN 包,并发送 ACK 确认包给服务端,此时客户端将进入 TIME_WAIT 状态;
  • 服务端收到 ACK 确认包后,就进入了最后的 CLOSE 状态;
  • 客户端经过 2MSL 时间之后,也进入 CLOSED 状态;