TCP
原理
互联网由一整套协议构成。TCP
只是其中的一层,有着自己的分工。
来来来,我们先复习一下TCP/IP
五层结构,详细请参考笔者之前分享的网络原理篇
-
以太网协议(
但是,以太网协议不能解决多个局域网如何互通,这由 IP 协议解决。Ethernet
)
位于数据链路层,规定了电子信号如何组成数据包(packet
),解决了 子网内部的 点对点 通信。 -
IP
协议
IP 协议位于 网络层 ,定义了一套自己的 地址规则,称为IP
地址。它实现了 路由 功能,允许某个局域网的A
主机,向另一个局域网的B
主机发送消息。路由的原理很简单。市场上所有的 路由器,背后都有很多 网口,要接入多根 网线。路由器内部有一张 路由表,规定了
A
段IP
地址走出口 一,B
段地址走出口二,......通过这套 指路牌,实现了数据包的 转发。IP 协议只是一个 地址协议,并不保证数据包的 完整。如果路由器 丢包,就需要发现 丢了哪一个包,以及 如何重新发送 这个包。这就要依靠
TCP
协议。
TCP 报文
-
源端口和目的端口
分别占用16bit,表示 源端口号 和 目的端口号 ;用于区别主机中的 不同进程,而 IP地址 是用来 区分不同的主机的,源端口号和目的端口号 配合上IP
首部中的源IP地址
和目的IP地址
就能唯一的确定一个TCP
连接 -
序号
Seq 序号,32
bit,用来标识从TCP
源端向目的端发送的字节流,发起方发送数据时对此进行标记。 -
确认序号
Ack 序号,32
bit,只有ACK
标志位为1
时,确认序号字段才有效。【确认方Ack=发起方Req+1,两端配对】 -
数据偏移
给出首部中32
bit的数目,需要这个值是因为任选字段的长度是可变的。这个字段占4
bit(最多能表示15
个32
bit的的字,即4*15=60
个字节的首部长度),因此TCP最多有60字节的首部。然而,没有任选字段,正常的长度是20
字节; -
标志位
共6
个,即URG
、ACK
、PSH
、RST
、SYN
、FIN
等,具体含义如下:URG
:紧急指针(urgent pointer
)有效ACK
:确认序号有效PSH
:接收方 应该尽快将这个报文交给 应用层RST
:重置连接SYN
:发起一个 新连接FIN
:释放一个连接
-
窗口
窗口大小,也就是有名的滑动窗口,用来进行流量控制
TCP 数据包的大小
简单说,TCP
协议的作用是,保证数据通信的 完整性 和 可靠性,防止丢包。 以太网 数据包(packet
)的大小是固定的,最初是1518
字节,后来增加到1522
字节。其中, 1500
字节是负载(payload
),22
字节是 头信息(head
)。
IP 数据包在以太网数据包的负载里面,它也有自己的 头信息,最少需要20
字节,所以IP
数据包的负载最多为1480
(1500-20
)字节。
TCP 数据包在 IP
数据包的负载
里面。它的 头信息 最少也需要20
字节,因此TCP
数据包的最大负载是 1480 - 20 = 1460
字节。由于 IP
和 TCP
协议往往有 额外的头信息,所以TCP
负载实际为1400
字节左右。
因此,一条1500
字节的信息需要 两个 TCP
数据包。HTTP/2
协议的一大改进, 就是压缩 HTTP
协议的头信息,使得一个 HTTP
请求可以放在 一个 TCP
数据包里面,而不是分成多个,这样就提高了速度。
TCP 数据包的编号(SEQ
)
一个包1400
字节,那么一次性发送 大量数据,就必须分成多个包。比如,一个 10MB
的文件,需要发送7100
多个包。
发送的时候,TCP
协议为每个包 编号(sequence number
,简称 SEQ
),以便接收的一方 按照顺序还原。万一发生 丢包,也可以知道丢失的是哪一个包。
第一个包 的编号是一个 随机数。为了便于理解,这里就把它称为 1 号包。假定这个包的负载长度是 100 字节,那么可以推算出下一个包的编号应该是 101。这就是说,每个数据包都可以得到 两个 编号:自身的编号,以及 下一个包 的编号。接收方由此知道,应该按照什么 顺序 将它们 还原 成原始文件。
(ps :当前包的编号是45943
,下一个数据包的编号是46183
,由此可知,这个包的负载是240
字节。)
TCP 数据包的组装
收到TCP
数据包以后,组装还原 是操作系统内核完成的。应用程序 不会 直接处理TCP
数据包。
对于 应用程序 来说,不用关心 数据通信 的细节。除非线路异常,收到的总是完整的数据。应用程序需要的数据放在TCP
数据包里面,有自己的 格式(比如HTTP
协议)。
TCP 并没有提供任何 机制,表示 原始文件 的大小,这由 应用层 的协议来规定。比如,HTTP
协议就有一个头信息 Content-Length,表示 信息体 的大小。对于 操作系统 来说,就是持续地 接收 TCP
数据包,将它们按照顺序 组装好,一个包都不少。
操作系统 不会去处理 TCP
数据包里面的数据。一旦组装好 TCP
数据包,就把它们转交给 应用程序。TCP
数据包里面有一个 端口(port
)参数,就是用来指定转交给 监听该端口 的应用程序。
(PS:系统根据 TCP
数据包里面的端口,将组装好的数据转交给相应的应用程序。上图中,21
端口是 FTP
服务器,25
端口是 SMTP
服务,80
端口是 Web
服务器。)
应用程序 收到组装好的 原始数据,以 浏览器 为例,就会根据 HTTP
协议的Content-Length
字段正确读出一段段的数据。这也意味着,一次 TCP
通信可以包括 多个 HTTP
通信。
TCP 三次握手
-
第一次握手
建立连接, 客户端 发送连接 请求报文段,将SYN
位置为1
,Sequence Number
为x
;
然后,客户端进入SYN_SEND
状态,等待 服务器 的确认; -
第二次握手
服务器 收到SYN
报文段。服务器 收到 客户端 的SYN
报文段,需要对这个SYN
报文段进行 确认,设置 Acknowledgment Number 为x+1
(Sequence Number+1
);
同时,自己自己还要发送SYN
请求信息,将SYN
位置为1
,Sequence Number
为y
;
** 服务器端** 将上述所有信息放到一个 报文段(即SYN+ACK
报文段)中,一并发送给 客户端,此时服务器进入SYN_RECV
状态; -
第三次握手
客户端 收到 服务器 的SYN+ACK
报文段。然后将Acknowledgment Number
设置为y+1
,向服务器发送ACK
报文段,这个报文段发送完毕以后,客户端和服务器端 都进入ESTABLISHED
状态,完成TCP
三次握手。
TCP四次分手
当 客户端 和 服务器 通过 三次握手 建立了 TCP 连接以后,当数据 传送完毕,肯定是要 断开TCP连接 的啊。那对于TCP的断开连接,这里就有了神秘的 四次分手。(继续参考上图)
-
第一次分手
主机A(可以使客户端,也可以是服务器端),设置Sequence Number
和Acknowledgment Number
,向 主机B 发送一个FIN
报文段;
此时,主机A 进入FIN_WAIT_1
状态;
这表示主机1没有数据要发送给主机2了; -
第二次分手 主机B 收到了 主机A 发送的
FIN
报文段,向 主机A 回一个ACK
报文段,Acknowledgment Number
为Sequence Number
加1;
主机A 进入FIN_WAIT_2
状态;
主机B告诉主机A,我同意你的关闭请求 -
第三次分手
主机B 向 主机A 发送FIN
报文段,请求关闭连接,同时 主机B 进入 LAST_ACK 状态; -
第四次分手
主机A 收到 主机B 发送的FIN
报文段,向 主机B 发送ACK
报文段,然后主机A
进入TIME_WAIT
状态;
主机B 收到 主机A 的ACK
报文段以后,就 关闭连接;
此时,主机A 等待2MSL
后依然没有收到回复,则证明 Server端 已正常关闭,那好,主机A 也可以关闭连接了。
至此,TCP的四次分手就这么愉快的完成了。
为什么
-
为什么要三次握手 在谢希仁著《计算机网络》第四版中讲 三次握手 的目的是 为了防止已失效的连接请求报文段突然又传送到了服务端,因而产生错误 。在另一部经典的《计算机网络》一书中讲 三次握手 的目的是为了解决 网络中存在延迟的重复分组 的问题。
在谢希仁著《计算机网络》书中同时举了一个例子,如下:
“已失效的连接请求报文段”的产生在这样一种情况下:client发出的第一个连接请求报文段并没有丢失,而是在某个网络结点长时间的滞留了,以致延误到连接释放以后的某个时间才到达server。本来这是一个早已失效的报文段。但server收到此失效的连接请求报文段后,就误认为是client再次发出的一个新的连接请求。于是就向client发出确认报文段,同意建立连接。假设不采用“三次握手”,那么只要server发出确认,新的连接就建立了。由于现在client并没有发出建立连接的请求,因此不会理睬server的确认,也不会向server发送数据。但server却以为新的运输连接已经建立,并一直等待client发来数据。这样,server的很多资源就白白浪费掉了。采用“三次握手”的办法可以防止上述现象发生。例如刚才那种情况,client不会向server的确认发出确认。server由于收不到确认,就知道client并没有要求建立连接。”
这就很明白了,防止了服务器端的一直等待而浪费资源。
-
为什么要四次分手
那四次分手又是为何呢?
TCP 协议是一种面向 连接的、可靠的、基于字节流 的运输层通信协议。
TCP 是 全双工 模式,这就意味着,当主机A 发出 FIN 报文段时,只是表示 主机A 已经没有数据要发送了,主机A 告诉 主机B,它的数据已经 全部发送完毕了;
但是,这个时候 主机A 还是可以 接受 来自 主机B 的数据;
当 主机B 返回ACK
报文段时,表示它已经知道 主机A 没有数据发送了,但是 主机B 还是可以 发送 数据到 主机A 的;
当 主机B 也发送了FIN
报文段时,这个时候就表示主机B
也没有数据要 发送了,就会告诉 主机B,我也 没有 数据要发送了。
之后彼此就会愉快的 中断 这次TCP连接。-
FIN_WAIT_1
这个状态要好好解释一下,其实FIN_WAIT_1
和FIN_WAIT_2
状态的真正含义都是表示 等待对方的FIN报文。
而这两种状态的区别是:FIN_WAIT_1
状态实际上是当SOCKET
在ESTABLISHED
状态时,它想 主动 关闭连接,向对方发送了FIN
报文,此时该SOCKET
即进入到FIN_WAIT_1
状态。
而当 对方 回应ACK
报文后,则进入到FIN_WAIT_2
状态。
当然在实际的正常情况下,无论对方何种情况下,都应该 马上 回应ACK
报文,所以FIN_WAIT_1
状态一般是比较难见到的,而FIN_WAIT_2
状态还有时常常可以用netstat
看到。 -
FIN_WAIT_2
上面已经详细解释了这种状态,实际上FIN_WAIT_2
状态下的SOCKET,表示 半连接,也即 有一方 要求 close 连接,但另外还告诉对方,我暂时还有点数据需要传送给你(ACK信息),稍后再关闭连接。 -
CLOSE_WAIT
这种状态的含义其实是表示在 等待关闭。怎么理解呢?
当对方close
一个SOCKET
后发送FIN
报文给自己,你系统毫无疑问地会回应一个ACK
报文给对方,此时则进入到CLOSE_WAIT
状态。接下来呢,实际上你真正需要考虑的事情是察看你是否还有数据发送给对方,如果 没有 的话,那么你也就可以close
这个SOCKET
,发送FIN
报文给对方,也即 关闭连接。所以你在CLOSE_WAIT状态下,需要完成的事情是 等待你去关闭连接。 -
LAST_ACK
这个状态还是比较容易好理解的,它是 被动关闭一方 在发送FIN
报文后,最后等待对方的ACK
报文。
当收到ACK
报文后,也即可以进入到CLOSED
可用状态了。 -
TIME_WAIT
表示收到了对方的FIN
报文,并发送出了ACK
报文,就等2MSL
后即可回到CLOSED
可用状态了。如果FINWAIT1
状态下,收到了对方同时带FIN
标志和ACK
标志的报文时,可以直接进入到TIME_WAIT
状态,而无须经过FIN_WAIT_2
状态。 -
CLOSED 表示连接中断。
-
慢启动和 ACK
服务器 发送数据包,当然越快越好,最好 一次性 全发出去。但是,发得 太快,就有可能 丢包。带宽小、路由器过热、缓存溢出 等许多因素都会导致 丢包。线路不好的话,发得越快,丢得越多。
最 理想 的状态是,在线路 允许 的情况下,达到 最高速率。但是我们怎么知道,对方线路的 理想速率 是多少呢?答案就是 慢慢试。
TCP 协议为了做到 效率 与 可靠性 的统一,设计了一个 慢启动(slow start
)机制。开始 的时候,发送得 较慢,然后根据 丢包 的情况,调整速率;如果 不丢包,就 加快 发送速度;如果 丢包,就 降低 发送速度。
Linux 内核里面设定了(常量TCP_INIT_CWND
),刚开始通信的时候,发送方一次性发送10
个数据包,即 发送窗口 的大小为10。然后停下来,等待 接收方的确认,再 继续 发送。
默认情况下,接收方每收到 两个 TCP
数据包,就要发送 一个 确认消息。确认 的英语是 acknowledgement
,所以这个确认消息就简称 ACK
。
ACK 携带 两个 信息。
- 期待要收到下一个数据包的编号
- 接收方的接收窗口的剩余容量
发送方有了这 两个 信息,再加上自己 已经发出的数据包的最新编号,就会推测出 接收方大概的接收速度,从而 降低或增加 发送速率。这被称为 发送窗口,这个窗口的大小是 可变的。
(PS:每个 ACK 都带有下一个数据包的编号,以及接收窗口的剩余容量。双方都会发送 ACK。)
注意,由于TCP
通信是 双向 的,所以 双方 都需要发送 ACK
。两方的 窗口 大小,很可能是 不一样 的。而且 ACK
只是很简单的几个字段,通常与 数据 合并在一个 数据包 里面发送。
(PS:上图一共 4 次通信。第一次 通信,A
主机发给B
主机的数据包编号是1
,长度是100
字节。因此 第二次 通信B
主机的 ACK
编号是 1 + 100 = 101
,第三次通信 A
主机的数据包编号也是 101
。同理,第二次 通信 B
主机发给 A
主机的数据包编号是1
,长度是200
字节,因此 第三次通信 A
主机的 ACK
是201
,第四次通信 B
主机的数据包编号也是201
。)
即使对于带宽 很大、线路很好 的连接,TCP
也总是从10
个数据包开始慢慢试,过了一段时间以后,才达到 最高的传输速率。这就是 TCP
的 慢启动。
数据包的遗失处理
TCP 协议可以保证 数据通信 的 完整性,这是 怎么做到的?
前面说过,每一个数据包都带有 下一个数据包的编号。如果下一个数据包 没有收到,那么 ACK 的 编号 就不会发生变化。
EG,现在收到了4
号包,但是没有收到5
号包。ACK
就会记录,期待收到5
号包。过了一段时间,5
号包收到了,那么下一轮 ACK
会更新编号。如果5
号包还是没收到,但是收到了6
号包或7
号包,那么 ACK
里面的编号不会变化,总是显示5
号包。这会导致 大量重复内容 的 ACK
。
如果 发送方 发现收到 三个 连续的重复 ACK,或者 超时 了还 没有 收到任何 ACK
,就会确认 丢包,即5号包遗失了,从而 再次发送 这个包。通过这种机制,TCP
保证了 不会 有数据包丢失。
(PS:Host B
没有收到100
号数据包,会连续发出相同的 ACK
,触发 Host A
重发100
号数据包。)
TCP 探测
明白了TCP
原理,我们就可以对TCP
监听的端口进行端口探测。
其实很简单,我们构造一个第一次握手的SYN
包给探测主机,如果探测主机回复我们一个SYN+ACK
的回复包,我们就可以判定 此端口 为目标主机开放端口:
-
通过之前我们完成的
ping
探测主机脚本,判断目标主机是否可达 -
构造一系列待探测端口的
SYN
TCP网络数据包
TCP(dport=(int(lport),int(hport)),flags=2)
复制代码
解释一下:
lport: 起始探测端口
hport: 截止探测端口
flags:2表示SYN
包,来源于SYN
位置1,等于2
-
将
TCP
包封装到IP
包中 -
通过
sr
函数将这一系列包全部发出去,并将结果保存在一个List
中 -
遍历返回的结果,如果结果中
TCP
包是SYN+ACK
就表明 该端口开放
连串起来:
扫描结果:
发现我们的虚拟主机对外开放了22端口,那么我们就可以想点歪心思啦
关注笔者公众账号[mindev],并回复tcp,就能得到tcp扫描源码哟~~
愿意与大家分享交流各种技术,个人公众账号[mindev],以及 知识星球[ 极客世界 ]
欢迎订阅公众账号,日更哟~~~