TCP原理
互联网由一整套协议构成。TCP 只是其中的一层,有着自己的分工。
来来来,我们先复习一下TCP/IP五层结构,详细请参考笔者之前分享的网络原理篇
-
以太网协议(
Ethernet)
位于数据链路层,规定了电子信号如何组成数据包(packet),解决了 子网内部的 点对点 通信。 但是,以太网协议不能解决多个局域网如何互通,这由 IP 协议解决。 -
IP协议
IP 协议位于 网络层 ,定义了一套自己的 地址规则,称为IP地址。它实现了 路由 功能,允许某个局域网的A主机,向另一个局域网的B主机发送消息。路由的原理很简单。市场上所有的 路由器,背后都有很多 网口,要接入多根 网线。路由器内部有一张 路由表,规定了
A段IP地址走出口 一,B段地址走出口二,......通过这套 指路牌,实现了数据包的 转发。IP 协议只是一个 地址协议,并不保证数据包的 完整。如果路由器 丢包,就需要发现 丢了哪一个包,以及 如何重新发送 这个包。这就要依靠
TCP协议。
TCP 报文
-
源端口和目的端口
分别占用16bit,表示 源端口号 和 目的端口号 ;用于区别主机中的 不同进程,而 IP地址 是用来 区分不同的主机的,源端口号和目的端口号 配合上IP首部中的源IP地址和目的IP地址就能唯一的确定一个TCP连接 -
序号
Seq 序号,32bit,用来标识从TCP源端向目的端发送的字节流,发起方发送数据时对此进行标记。 -
确认序号
Ack 序号,32bit,只有ACK标志位为1时,确认序号字段才有效。【确认方Ack=发起方Req+1,两端配对】 -
数据偏移
给出首部中32bit的数目,需要这个值是因为任选字段的长度是可变的。这个字段占4bit(最多能表示15个32bit的的字,即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探测主机脚本,判断目标主机是否可达 -
构造一系列待探测端口的
SYNTCP网络数据包
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],以及 知识星球[ 极客世界 ]
8116

被折叠的 条评论
为什么被折叠?



