注:本文为 “TCP/IP 协议” 相关文章合辑。
未整理去重。
TCP/IP 协议图解
退休的汤姆 于 2021-07-01 16:14:25 发布
TCP/IP 协议简介
TCP/IP 协议包含了一系列的协议,也叫 TCP/IP 协议族(TCP/IP Protocol Suite,或 TCP/IP Protocols),简称 TCP/IP。TCP/IP 协议族提供了点对点的连结机制,并且将传输数据帧的封装、寻址、传输、路由以及接收方式,都予以标准化。
TCP/IP 协议的分层模型
在展开介绍 TCP/IP 协议之前,首先介绍一下七层 ISO 模型。国际标准化组织 ISO 为了使网络应用更为普及,推出了 OSI 参考模型,即开放式系统互联(Open System Interconnect)模型,一般都叫 OSI 参考模型。OSI 参考模型是 ISO 组织在 1985 年发布的网络互连模型,其含义就是为所有公司使用一个统一的规范来控制网络,这样所有公司遵循相同的通信规范,网络就能互联互通了。
OSI 模型的七层框架
OSI 模型定义了网络互连的七层框架(物理层、数据链路层、网络层、传输层、会话层、表示层、应用层),每一层实现各自的功能和协议,并完成与相邻层的接口通信。OSI 模型各层的通信协议,大致举例如下表所示:
表:OSI 模型各层的通信协议举例
应用层 | HTTP、SMTP、SNMP、FTP、Telnet、SIP、SSH、NFS、RTSP、XMPP、Whois、ENRP、等等 |
---|---|
表示层 | XDR、ASN.1、SMB、AFP、NCP、等等 |
会话层 | ASAP、SSH、RPC、NetBIOS、ASP、Winsock、BSD Sockets、等等 |
传输层 | TCP、UDP、TLS、RTP、SCTP、SPX、ATP、IL、等等 |
网络层 | IP、ICMP、IGMP、IPX、BGP、OSPF、RIP、IGRP、EIGRP、ARP、RARP、X.25、等等 |
数据链路层 | 以太网、令牌环、HDLC、帧中继、ISDN、ATM、IEEE 802.11、FDDI、PPP、等等 |
物理层 | 例如铜缆、网线、光缆、无线电等等 |
TCP/IP 协议是 Internet 互联网最基本的协议,其在一定程度上参考了七层 ISO 模型。OSI 模型共有七层,从下到上分别是物理层、数据链路层、网络层、运输层、会话层、表示层和应用层。但是这显然是有些复杂的,所以在 TCP/IP 协议中,七层被简化为了四个层次。TCP/IP 模型中的各种协议,依其功能不同,被分别归属到这四层之中,常被视为是简化过后的七层 OSI 模型。
TCP/IP 协议与七层 ISO 模型的对应关系
TCP/IP 协议与七层 ISO 模型的对应关系,大致如下图所示:
图:TCP/IP 协议与七层 ISO 模型的对应关系
TCP/IP 协议的应用层的主要协议有 HTTP、Telnet、FTP、SMTP 等,是用来读取来自传输层的数据或者将数据传输写入传输层;传输层的主要协议有 UDP、TCP,实现端对端的数据传输;网络层的主要协议有 ICMP、IP、IGMP,主要负责网络中数据包的传送等;链路层有时也称作数据链路层或网络接口层,主要协议有 ARP、RARP,
通常包括操作系统中的设备驱动程序和计算机中对应的网络接口卡,它们一起处理与传输媒介(如电缆或其他物理设备)的物理接口细节。
(一)TCP/IP 协议的应用层
应用层包括所有和应用程序协同工作,并利用基础网络交换应用程序的业务数据的协议。一些特定的程序被认为运行在这个层上,该层协议所提供的服务能直接支持用户应用。应用层协议包括 HTTP(万维网服务)、FTP(文件传输)、SMTP(电子邮件)、SSH(安全远程登陆)、DNS(域名解析)以及许多其他协议。
(二)TCP/IP 协议的传输层
传输层的协议,解决了诸如端到端可靠性问题,能确保数据可靠的到达目的地,甚至能保证数据按照正确的顺序到达目的地。传输层的主要功能大致如下:
(1)为端到端连接提供传输服务;
(2)这种传输服务分为可靠和不可靠的,其中 TCP 是典型的可靠传输,而 UDP 则是不可靠传输;
(3)为端到端连接提供流量控制、差错控制、QoS (Quality of
Service) 服务质量等管理服务。
传输层主要有两个性质不同的协议:TCP 传输控制协议和 UDP 用户数据报协议。
TCP 协议是一个面向连接的、可靠的传输协议,它提供一种可靠的字节流,能保证数据完整、无损并且按顺序到达。TCP 尽量连续不断地测试网络的负载并且控制发送数据的速度以避免网络过载。另外,TCP 试图将数据按照规定的顺序发送。
UDP 协议是一个无连接的数据报协议,是一个 “尽力传递” 和 “不可靠” 协议,不会对数据包是否已经到达目的地进行检查,并且不保证数据包按顺序到达。
总体来说,TCP 协议传输效率低,但可靠性强;UDP 协议传输效率高,但可靠性略低,适用于传输可靠性要求不高、体量小的数据(比如 QQ 聊天数据)。
(三)TCP/IP 协议的网络层
TCP/IP 协议网络层的作用是在复杂的网络环境中为要发送的数据报找到一个合适的路径进行传输。简单来说,网络层负责将数据传输到目标地址,目标地址可以是多个网络通过路由器连接而成的某一个地址。另外,网络层负责寻找合适的路径到达对方计算机,并把数据帧传送给对方,网络层还可以实现拥塞控制、网际互连等功能。网络层协议的代表包括:ICMP、IP、IGMP 等。
(四)TCP/IP 协议的链路层
链路层有时也称作数据链路层或网络接口层,用来处理连接网络的硬件部分。该层既包括操作系统硬件的设备驱动、NIC(网卡)、光纤等物理可见部分,还包括连接器等一切传输媒介。在这一层,数据的传输单位为比特。其主要协议有 ARP、RARP 等。
图解 物理层:使用 MAC 解决设备的身份证问题
通信的原始时代
很久很久之前,你不与任何其他电脑相连接,孤苦伶仃。
直到有一天,你希望与另一台电脑 B 建立通信,于是你们各开了一个网口,用一根网线连接了起来。
用一根网线连接起来怎么就能 “通信” 了?我可以给你讲 IO、讲中断、讲缓冲区,但这不是研究网络时该关心的问题。
如果你纠结,要么去研究一下操作系统是如何处理网络 IO 的,要么去研究一下包是如何被网卡转换成电信号发送出去的,要么就仅仅把它当做电脑里有个小人在开枪吧~
反正,你们就是连起来了,并且可以通信。
有一天,一个新伙伴 C 加入了,但聪明的你们很快发现,可以每个人开两个网口,用一共三根网线,彼此相连。
随着越来越多的人加入,你发现身上开的网口实在太多了,而且网线密密麻麻,混乱不堪。(而实际上一台电脑根本开不了这么多网口,所以这种连线只在理论上可行,所以连不上的我就用红色虚线表示了,就是这么严谨哈哈~)
集线器的诞生
于是你们发明了一个中间设备,你们将网线都插到这个设备上,由这个设备做转发,就可以彼此之间通信了,本质上和原来一样,只不过网口的数量和网线的数量减少了,不再那么混乱。
你给它取名叫集线器,它仅仅是无脑将电信号转发到所有出口(广播),不做任何处理,你觉得它是没有智商的,因此把人家定性在了物理层。
由于转发到了所有出口,那 BCDE 四台机器怎么知道数据包是不是发给自己的?
首先,你要给所有的连接到交换机的设备,都起个名字。原来你们叫 ABCD,但现在需要一个更专业的,全局唯一的名字作为标识,你把这个更高端的名字称为 MAC 地址。
你的 MAC 地址是 aa-aa-aa-aa-aa-aa,你的伙伴 b 的 MAC 地址是 bb-bb-bb-bb-bb-bb,以此类推,不重复就好。
这样,A 在发送数据包给 B 时,只要在头部拼接一个这样结构的数据,就可以了。
B 在收到数据包后,根据头部的目标 MAC 地址信息,判断这个数据包的确是发给自己的,于是便收下。
其他的 CDE 收到数据包后,根据头部的目标 MAC 地址信息,判断这个数据包并不是发给自己的,于是便丢弃。
虽然集线器使整个布局干净不少,但原来我只要发给电脑 B 的消息,现在却要发给连接到集线器中的所有电脑,这样既不安全,又不节省网络资源。
图解 数据链路:使用交换机解决 MAC 地址映射问题
集线器的问题
如果把这个集线器弄得更智能一些,只发给目标 MAC 地址指向的那台电脑,就好了。
交换机的诞生
虽然只比集线器多了这一点点区别,但看起来似乎有智能了,你把这东西叫做交换机。也正因为这一点点智能,你把它放在了另一个层级,数据链路层。
如上图所示,你是这样设计的。
交换机内部维护一张MAC 地址表,记录着每一个 MAC 地址的设备,连接在其哪一个端口上。
MAC 地址 | 端口 |
---|---|
bb-bb-bb-bb-bb-bb | 1 |
cc-cc-cc-cc-cc-cc | 3 |
aa-aa-aa-aa-aa-aa | 4 |
dd-dd-dd-dd-dd-dd | 5 |
假如你仍然要发给 B 一个数据包,构造了如下的数据结构从网口出去。
到达交换机时,交换机内部通过自己维护的 MAC 地址表,发现目标机器 B 的 MAC 地址 bb-bb-bb-bb-bb-bb 映射到了端口 1 上,于是把数据从 1 号端口发给了 B,完事~
你给这个通过这样传输方式而组成的小范围的网络,叫做以太网。
当然最开始的时候,MAC 地址表是空的,是怎么逐步建立起来的?
假如在 MAC 地址表为空是,你给 B 发送了如下数据
由于这个包从端口 4 进入的交换机,所以此时交换机就可以在 MAC 地址表记录第一条数据:
MAC:aa-aa-aa-aa-aa-aa-aa
端口:4
交换机看目标 MAC 地址(bb-bb-bb-bb-bb-bb)在地址表中并没有映射关系,于是将此包发给了所有端口,也即发给了所有机器。
之后,只有机器 B 收到了确实是发给自己的包,于是做出了响应,响应数据从端口 1 进入交换机,于是交换机此时在地址表中更新了第二条数据:
MAC:bb-bb-bb-bb-bb-bb
端口:1
过程如下
经过该网络中的机器不断地通信,交换机最终将 MAC 地址表建立完毕~
随着机器数量越多,交换机的端口也不够了,但聪明的你发现,只要将多个交换机连接起来,这个问题就轻而易举搞定~
你完全不需要设计额外的东西,只需要按照之前的设计和规矩来,按照上述的接线方式即可完成所有电脑的互联,所以交换机设计的这种规则,真的很巧妙。你想想看为什么(比如 A 要发数据给 F)。
但是要注意,上面那根红色的线,最终在 MAC 地址表中可不是一条记录,而是要把 EFGH 这四台机器与该端口(端口 6)的映射全部记录在表中。
MAC 地址和端口的映射记录
最终,两个交换机将分别记录 A ~ H 所有机器的映射记录。
左边的交换机
MAC 地址 | 端口 |
---|---|
bb-bb-bb-bb-bb-bb | 1 |
cc-cc-cc-cc-cc-cc | 3 |
aa-aa-aa-aa-aa-aa | 4 |
dd-dd-dd-dd-dd-dd | 5 |
ee-ee-ee-ee-ee-ee | 6 |
ff-ff-ff-ff-ff-ff | 6 |
gg-gg-gg-gg-gg-gg | 6 |
hh-hh-hh-hh-hh-hh | 6 |
右边的交换机
MAC 地址 | 端口 |
---|---|
bb-bb-bb-bb-bb-bb | 1 |
cc-cc-cc-cc-cc-cc | 1 |
aa-aa-aa-aa-aa-aa | 1 |
dd-dd-dd-dd-dd-dd | 1 |
ee-ee-ee-ee-ee-ee | 2 |
ff-ff-ff-ff-ff-ff | 3 |
gg-gg-gg-gg-gg-gg | 4 |
hh-hh-hh-hh-hh-hh | 6 |
这在只有 8 台电脑的时候还好,甚至在只有几百台电脑的时候,都还好,所以这种交换机的设计方式,已经足足支撑一阵子了。
但很遗憾,人是贪婪的动物,很快,电脑的数量就发展到几千、几万、几十万。
图解 传输层:IP 地址和路由器
二层交换机的问题
交换机已经无法记录如此庞大的映射关系了。
此时你动了歪脑筋,你发现了问题的根本在于,连出去的那根红色的网线,后面不知道有多少个设备不断地连接进来,从而使得地址表越来越大。
那我可不可以让那根红色的网线,接入一个新的设备,这个设备就跟电脑一样有自己独立的 MAC 地址,而且同时还能帮我把数据包做一次转发?
这个设备就是路由器,它的功能就是,作为一台独立的拥有 MAC 地址的设备,并且可以帮我把数据包做一次转发,你把它定在了网络层。
注意,路由器的每一个端口,都有独立的 MAC 地址
好了,现在交换机的 MAC 地址表中,只需要多出一条 MAC 地址 ABAB 与其端口的映射关系,就可以成功把数据包转交给路由器了,这条搞定。
那如何做到,把发送给 C 和 D,甚至是把发送给 DEFGH… 的数据包,统统先发送给路由器?
不难想到这样一个点子,假如电脑 C 和 D 的 MAC 地址拥有共同的前缀,比如分别是
C 的 MAC 地址:FFFF-FFFF-CCCC****D 的 MAC 地址:FFFF-FFFF-DDDD
那我们就可以说,将目标 MAC 地址为 FFFF-FFFF-?开头的,统统先发送给路由器。
这样是否可行?答案是否定的。
IP 地址的诞生
我们先从现实中 MAC 地址的结构入手,MAC 地址也叫物理地址、硬件地址,长度为 48 位,一般这样来表示
00-16-EA-AE-3C-40
它是由网络设备制造商生产时烧录在网卡的 EPROM(一种闪存芯片,通常可以通过程序擦写)。
其中前 24 位(00-16-EA)代表网络硬件制造商的编号,后 24 位(AE-3C-40)是该厂家自己分配的,一般表示系列号。
只要不更改自己的 MAC 地址,MAC 地址在世界是唯一的。形象地说,MAC 地址就如同身份证上的身份证号码,具有唯一性。
那如果你希望向上面那样表示将目标 MAC 地址为 FFFF-FFFF-?开头的,统一从路由器出去发给某一群设备(后面会提到这其实是子网的概念),那你就需要要求某一子网下统统买一个厂商制造的设备,要么你就需要要求厂商在生产网络设备烧录 MAC 地址时,提前按照你规划好的子网结构来定 MAC 地址,并且日后这个网络的结构都不能轻易改变。
这显然是不现实的。
于是你发明了一个新的地址,给每一台机器一个 32 位的编号,如:
11000000101010000000000000000001
你觉得有些不清晰,于是把它分成四个部分,中间用点相连。
11000000.10101000.00000000.00000001
你还觉得不清晰,于是把它转换成 10 进制。
192.168.0.1
最后你给了这个地址一个响亮的名字,IP 地址。现在每一台电脑,同时有自己的 MAC 地址,又有自己的 IP 地址,只不过 IP 地址是软件层面上的,可以随时修改,MAC 地址一般是无法修改的。
这样一个可以随时修改的 IP 地址,就可以根据你规划的网络拓扑结构,来调整了。
如上图所示,假如我想要发送数据包给 ABCD 其中一台设备,不论哪一台,我都可以这样描述,“将 IP 地址为 192.168.0 开头的全部发送给到路由器,之后再怎么转发,交给它!”,巧妙吧。
路由器的诞生
路由器诞生了,专门负责 IP 地址的寻找。那报文交给路由器之后,路由器又是怎么把数据包准确转发给指定设备的?
我们先给上面的组网方式中的每一台设备,加上自己的 IP 地址
现在两个设备之间传输,除了加上数据链路层的头部之外,还要再增加一个网络层的头部。
假如 A 给 B 发送数据,由于它们直接连着交换机,所以 A 直接发出如下数据包即可,其实网络层没有体现出作用。
但假如 A 给 C 发送数据,A 就需要先转交给路由器,然后再由路由器转交给 C。由于最底层的传输仍然需要依赖以太网,所以数据包是分成两段的。
A ~ 路由器这段的包如下:
路由器到 C 这段的包如下:
好了,上面说的两种情况(A->B,A->C),相信细心的读者应该会有不少疑问,下面我们一个个来展开。
子网的由来
A 给 C 发数据包,怎么知道是否要通过路由器转发?
答案:子网
如果源 IP 与目的 IP 处于一个子网,直接将包通过交换机发出去。
如果源 IP 与目的 IP 不处于一个子网,就交给路由器去处理。
好,那现在只需要解决,什么叫处于一个子网就好了。
-
192.168.0.1 和 192.168.0.2 处于同一个子网
-
192.168.0.1 和 192.168.1.1 处于不同子网
这两个是我们人为规定的,即我们想表示,对于 192.168.0.1 来说:
192.168.0.xxx 开头的,就算是在一个子网,否则就是在不同的子网。
那对于计算机来说,怎么表达这个意思?于是人们发明了子网掩码的概念
假如某台机器的子网掩码定为 255.255.255.0
这表示,将源 IP 与目的 IP 分别同这个子网掩码进行与运算,相等则是在一个子网,不相等就是在不同子网,就这么简单。
比如
-
A 电脑:192.168.0.1 & 255.255.255.0 = 192.168.0.0
-
B 电脑:192.168.0.2 & 255.255.255.0 = 192.168.0.0
-
C 电脑:192.168.1.1 & 255.255.255.0 = 192.168.1.0
-
D 电脑:192.168.1.2 & 255.255.255.0 = 192.168.1.0
那么 A 与 B 在同一个子网,C 与 D 在同一个子网,但是 A 与 C 就不在同一个子网,与 D 也不在同一个子网,以此类推。
所以如果 A 给 C 发消息,A 和 C 的 IP 地址分别 & A 机器配置的子网掩码,发现不相等,则 A 认为 C 和自己不在同一个子网,于是把包发给路由器,就不管了,之后怎么转发,A 不关心。
A 如何知道,哪个设备是路由器?
答案:在 A 上要设置默认网关
上一步 A 通过是否与 C 在同一个子网内,判断出自己应该把包发给路由器,那路由器的 IP 是多少?
其实说发给路由器不准确,应该说 A 会把包发给默认网关。
对 A 来说,A 只能直接把包发给同处于一个子网下的某个 IP 上,所以发给路由器还是发给某个电脑,对 A 来说也不关心,只要这个设备有个 IP 地址就行。
所以默认网关,就是 A 在自己电脑里配置的一个 IP 地址,以便在发给不同子网的机器时,发给这个 IP 地址。
仅此而已!
路由表的由来(和 Mac 表的由来好像,都是逼出来的)
路由器如何知道 C 在哪里?
答案:路由表
现在 A 要给 C 发数据包,已经可以成功发到路由器这里了,最后一个问题就是,路由器怎么知道,收到的这个数据包,该从自己的哪个端口出去,才能直接(或间接)地最终到达目的地 C 。
路由器收到的数据包有目的 IP 也就是 C 的 IP 地址,需要转化成从自己的哪个端口出去,很容易想到,应该有个表,就像 MAC 地址表一样。
这个表就叫路由表。
不同于 MAC 地址表的是,路由表并不是一对一这种明确关系,我们下面看一个路由表的结构。
目的地址 | 子网掩码 | 下一跳 | 端口 |
---|---|---|---|
192.168.0.0 | 255.255.255.0 | 0 | |
192.168.0.254 | 255.255.255.255 | 0 | |
192.168.1.0 | 255.255.255.0 | 1 | |
192.168.1.254 | 255.255.255.255 | 1 |
我们学习一种新的表示方法,由于子网掩码其实就表示前多少位表示子网的网段,所以如 192.168.0.0(255.255.255.0) 也可以简写为 192.168.0.0/24
目的地址 | 下一跳 | 端口 |
---|---|---|
192.168.0.0/24 | 0 | |
192.168.0.254/32 | 0 | |
192.168.1.0/24 | 1 | |
192.168.1.254/32 | 1 |
这就很好理解了,路由表就表示,192.168.0.xxx 这个子网下的,都转发到 0 号端口,192.168.1.xxx 这个子网下的,都转发到 1 号端口。下一跳列还没有值,我们先不管配合着结构图来看(这里把子网掩码和默认网关都补齐了)
刚才说的都是 IP 层,但发送数据包的数据链路层需要知道 MAC 地址,可是我只知道 IP 地址该怎么办?
答案:arp
假如你(A)此时不知道你同伴 B 的 MAC 地址(现实中就是不知道的,刚刚我们只是假设已知),你只知道它的 IP 地址,你该怎么把数据包准确传给 B ?
答案很简单,在网络层,我需要把 IP 地址对应的 MAC 地址找到,也就是通过某种方式,找到 192.168.0.2 对应的 MAC 地址BBBB。
这种方式就是arp 协议,同时电脑 A 和 B 里面也会有一张arp 缓存表,表中记录着 IP 与 MAC 地址的对应关系。
IP 地址 | MAC 地址 |
---|---|
192.168.0.2 | BBBB |
一开始的时候这个表是空的,电脑 A 为了知道电脑 B(192.168.0.2)的 MAC 地址,将会广播一条 arp 请求,B 收到请求后,带上自己的 MAC 地址给 A 一个响应。此时 A 便更新了自己的 arp 表。
这样通过大家不断广播 arp 请求,最终所有电脑里面都将 arp 缓存表更新完整。
图解:整个传输过程
从各个节点的视角来看
电脑视角:
-
首先我要知道我的 IP 以及对方的 IP
-
通过子网掩码判断我们是否在同一个子网
-
在同一个子网就通过 arp 获取对方 mac 地址直接扔出去
-
不在同一个子网就通过 arp 获取默认网关的 mac 地址直接扔出去
交换机视角:
-
我收到的数据包必须有目标 MAC 地址
-
通过 MAC 地址表查映射关系
-
查到了就按照映射关系从我的指定端口发出去
-
查不到就所有端口都发出去
路由器视角:
-
我收到的数据包必须有目标 IP 地址
-
通过路由表查映射关系
-
查到了就按照映射关系从我的指定端口发出去(不在任何一个子网范围,走其路由器的默认网关也是查到了)
-
查不到则返回一个路由不可达的数据包
注:网络层(IP 协议)本身没有传输包的功能,包的实际传输是委托给数据链路层(以太网中的交换机)来实现的。
涉及到的三张表分别是
-
交换机中有MAC 地址表用于映射 MAC 地址和它的端口
-
路由器中有路由表用于映射 IP 地址 (段) 和它的端口
-
电脑和路由器中都有arp 缓存表用于缓存 IP 和 MAC 地址的映射关系
这三张表是怎么来的
-
MAC 地址表是通过以太网内各节点之间不断通过交换机通信,不断完善起来的。
-
路由表是各种路由算法 + 人工配置逐步完善起来的。
-
arp 缓存表是不断通过 arp 协议的请求逐步完善起来的。
知道了以上这些,目前网络上两个节点是如何发送数据包的这个过程,就完全可以解释通了!
参考的网络拓扑图
那接下来我们就放上参考的最后一个网络拓扑图吧,请做好战斗准备!
这时路由器 1 连接了路由器 2,所以其路由表有了下一条地址这一个概念,所以它的路由表就变成了这个样子。如果匹配到了有下一跳地址的一项,则需要再次匹配,找到其端口,并找到下一跳 IP 的 MAC 地址。
也就是说找来找去,最终必须能映射到一个端口号,然后从这个端口号把数据包发出去。
目的地址 | 下一跳 | 端口 |
---|---|---|
192.168.0.0/24 | 0 | |
192.168.0.254/32 | 0 | |
192.168.1.0/24 | 1 | |
192.168.1.254/32 | 1 | |
192.168.2.0/24 | 192.168.100.5 | |
192.168.100.0/24 | 2 | |
192.168.100.4/32 | 2 |
这时如果 A 给 F 发送一个数据包,能不能通?如果通的话整个过程是怎样的?
思考一分钟…
详细过程动画描述:
详细过程文字描述:
1. 首先 A(192.168.0.1)通过子网掩码(255.255.255.0)计算出自己与 F(192.168.2.2)并不在同一个子网内,于是决定发送给默认网关(192.168.0.254)
2. A 通过 ARP 找到 默认网关 192.168.0.254 的 MAC 地址。
3. A 将源 MAC 地址(AAAA)与网关 MAC 地址(ABAB)封装在数据链路层头部,又将源 IP 地址(192.168.0.1)和目的 IP 地址(192.168.2.2)(注意这里千万不要以为填写的是默认网关的 IP 地址,从始至终这个数据包的两个 IP 地址都是不变的,只有 MAC 地址在不断变化)封装在网络层头部,然后发包
4. 交换机 1 收到数据包后,发现目标 MAC 地址是 ABAB,转发给路由器 1
5. 数据包来到了路由器 1,发现其目标 IP 地址是 192.168.2.2,查看其路由表,发现了下一跳的地址是 192.168.100.5*
6. 所以此时路由器 1 需要做两件事,第一件是再次匹配路由表,发现匹配到了端口为 2,于是将其封装到数据链路层,最后把包从 2 号口发出去。
7. 此时路由器 2 收到了数据包,看到其目的地址是 192.168.2.2,查询其路由表,匹配到端口号为 1,准备从 1 号口把数据包送出去。
8. 但此时路由器 2 需要知道 192.168.2.2 的 MAC 地址了,于是查看其 arp 缓存,找到其 MAC 地址为 FFFF,将其封装在数据链路层头部,并从 1 号端口把包发出去。
9. 交换机 3 收到了数据包,发现目的 MAC 地址为 FFFF,查询其 MAC 地址表,发现应该从其 6 号端口出去,于是从 6 号端口把数据包发出去。
10. F 最终收到了数据包!并且发现目的 MAC 地址就是自己,于是收下了这个包。
HTTP 报文传输原理
利用 TCP/IP 进行网络通信时,数据包会按照分层顺序与对方进行通信。发送端从应用层往下走,接收端从链路层往上走。从客户端到服务器的数据,每一帧数据的传输的顺序都为:应用层 -> 运输层 -> 网络层 -> 链路层 -> 链路层 -> 网络层 -> 运输层 -> 应用层。
HTTP 报文传输过程
以一个 HTTP 请求的传输为例,请求从 HTTP 客户端(如浏览器)和 HTTP 服务端应用的传输过程,大致如下图所示:
图:HTTP 请求报文的分层传输过程
数据封装和分用
接下来,为大家介绍一下数据封装和分用。
数据通过互联网传输的时候不可能是光秃秃的不加标识,如果这样数据就会乱。所以数据在发送的时候,需要加上特定标识,加上特定标识的过程叫做数据的封装,在数据使用的时候再去掉特定标识,去掉特定标识的过程就叫做分用。TCP/IP 协议的数据封装和分用过程,大致如下图所示:
图:TCP/IP 协议的数据封装和分用过程
在数据封装时,数据经过每个层都会打上该层特定标识,添加上头部。
在传输层封装时,添加的报文首部时要存入一个应用程序的标识符,无论 TCP 和 UDP 都用一个 16 位的端口号来表示不同的应用程序,并且都会将源端口和目的端口存入报文首部中。
在网络层封装时,IP 首部会标识处理数据的协议类型,或者说标识出网络层数据帧所携带的上层数据类型,如 TCP、UDP、ICMP、IP、IGMP 等等。
具体来说,会在 IP 首部中存入一个长度为 8 位的数值,称作协议域:
1 表示为 ICMP 协议、2 表示为 IGMP 协议、6 表示为 TCP 协议、17 表示为 UDP 协议、等等。IP 首部还会标识发送方地址(源 IP)和接收方地址(目标 IP)。
在链路层封装时,网络接口分别要发送和接收 IP、ARP 和 RARP 等多种不同协议的报文,因此也必须在以太网的帧首部中加入某种形式的标识,以指明所处理的协议类型,为此,以太网的报文帧的首部也有一个 16 位的类型域,标识出以太网数据帧所携带的上层数据类型,如 IPv4、ARP、IPV6、PPPoE 等等。
数据封装和分用的过程大致为:发送端每通过一层会增加该层的首部,接收端每通过一层则删除该层的首部。
总体来说,TCP/IP 分层管理、数据封装和分用的好处:分层之后若需改变相关设计,只需替换变动的层。各层之间的接口部分规划好之后,每个层次内部的设计就可以自由改动。层次化之后,设计也变得相对简单:各个层只需考虑分派给自己的传输任务。
TCP/IP 与 OSI 的区别主要有哪些?除了 TCP/IP 与 OSI 在分层模块上稍有区别,更重要的区别为:OSI 参考模型注重 “通信协议必要的功能是什么”,而 TCP/IP 则更强调 “在计算机上实现协议应该开发哪种程序”。
实际上,在传输过程中,数据报文会在不同的物理网络之间传递,还是以一个 HTTP 请求的传输为例,请求在不同物理网络之间的传输过程,大致如下图所示:
图:HTTP 请求在不同物理网络之间的传输过程
数据包在不同物理网络之间的传输过程中,网络层会通过路由器去对不同的网络之间的数据包进行存储、分组转发处理。构造互连网最简单的方法是把两个或多个网络通过路由器进行连接。路由器可以简单理解为一种特殊的用于网络互连的硬件盒,其作用是为不同类型的物理网络提供连接:以太网、令牌环网、点对点的链接和 FDDI(光纤分布式数据接口)等等。
物理网络之间通过路由器进行互连,随着增加不同类型的物理网络,可能会有很多个路由器,但是对于应用层来说仍然是一样的,TCP 协议栈为大家屏蔽了物理层的复杂性。总之,物理细节和差异性的隐藏,使得互联网 TCP/IP 传输的功能变得非常强大。
接下来,开始为大家介绍与传输性能有密切关系的内容:TCP 传输层的三次握手建立连接,四次挥手释放连接。不过在此之前,还得先介绍一下 TCP 报文协议。
TCP 协议的报文格式
在 TCP/IP 协议栈中,IP 协议层只关心如何使数据能够跨越本地网络边界的问题,而不关心数据如何传输。整体 TCP/IP 协议栈,共同配合一起解决数据如何通过许许多多个点对点通路,顺利传输到达目的地。一个点对点通路被称为一 “跳”(hop),通过 TCP/IP 协议栈,网络成员能够在许多 “跳” 的基础上建立相互的数据通路。
传输层 TCP 协议提供了一种面向连接的、可靠的字节流服务,其数据帧格式,大致如下图所示:
图:传输层 TCP 协议的数据帧格式
一个传输层 TCP 协议的数据帧,大致包含以下字段:
(一)源端口号
源端口号表示报文的发送端口,占 16 位。源端口和源 IP 地址组合起来,可以标识报文的发送地址。
(二)目的端口号
目的端口号表示报文的接收端口,占 16 位。目的端口和目的 IP 地址相结合,可以标识报文的接收地址。
TCP 协议是基于 IP 协议的基础上传输的,TCP 报文中的源端口号 + 源 IP,与 TCP 报文中的目的端口号 + 目的 IP 一起,组合起来唯一性的确定一条 TCP 连接。
(三)序号(Sequence Number)
TCP 传输过程中,在发送端出的字节流中,传输报文中的数据部分的每一个字节都有它的编号。序号(Sequence Number)占 32 位,发起方发送数据时,都需要标记序号。
序号(Sequence Number)的语义与 SYN 控制标志(Control Bits)的值有关。根据控制标志(Control Bits)中的 SYN 是否为 1,序号(Sequence Number)表达不同的含义:
(1)当 SYN = 1 时,当前为连接建立阶段,此时的序号为初始序号 ISN ((Initial Sequence Number),通过算法来随机生成序号;
(2)当 SYN = 0 时在数据传输正式开始时,第一个报文的序号为 ISN + 1,后面的报文的序号,为前一个报文的 SN 值 + TCP 报文的净荷字节数 (不包含 TCP 头)。比如,如果发送端发送的一个 TCP 帧的净荷为 12byte,序号为 5,则发送端接着发送的下一个数据包的时候,序号的值应该设置为 5+12=17。
在数据传输过程中,TCP 协议通过序号(Sequence Number)对上层提供有序的数据流。发送端可以用序号来跟踪发送的数据量;接收端可以用序号识别出重复接收到的 TCP 包,从而丢弃重复包;对于乱序的数据包,接收端也可以依靠序号对其进行排序。
(四)确认序号(Acknowledgment Number)
确认序号(Acknowledgment Number)标识了报文接收端期望接收的字节序列。如果设置了 ACK 控制位,确认序号的值表示一个准备接收的包的序列码,注意,它所指向的是准备接收的包,也就是下一个期望接收的包的序列码。
举个例子,假设发送端(如 Client)发送 3 个净荷为 1000byte、起始 SN 序号为 1 的数据包给 Server 服务端,Server 每收到一个包之后,需要回复一个 ACK 响应确认数据包给 Client。ACK 响应数据包的 ACK Number 值,为每个 Client 包的为 SN + 包净荷,既表示 Server 已经确认收到的字节数,还表示期望接收到的下一个 Client 发送包的 SN 序号,具体的 ACK 值如下图左边的正常传输部分所示。
图:传输过程的确认序号(Acknowledgment Number)值示例图
在上图的左边部分,Server 第 1 个 ACK 包的 ACK Number 值为 1001,是通过 Client 第 1 个包的 SN + 包净荷 = 1+1000 计算得到,表示期望第 2 个 Client 包的 SN 序号为 1001;Server 第 2 个 ACK 包的 ACK Number 值为 2001,为 Client 第 2 个包的 SN + 包净荷 = 2001,表示期望第 3 个 Server 包的 SN 为 2001,以此类推。
如果发生错误,假设 Server 在处理 Client 的第二个发送包异常,Server 仍然回复一个 ACK Number 值为 1001 的确认包,则 Client 的第二个数据包需要重复发送,具体的 ACK 值如上图右边的正常传输部分所示。
只有控制标志的 ACK 标志为 1 时,数据帧中的确认序号 ACK Number 才有效。TCP 协议规定,连接建立后,所有发送的报文的 ACK 必须为 1,也就是建立连接后,所有报文的确认序号有效。如果是 SYN 类型的报文,其 ACK 标志为 0,故没有确认序号。
(五)头部长度
该字段占用 4 位,用来表示 TCP 报文首部的长度,单位是 4bit 位。其值所表示的并不是字节数,而是头部的所含有的 32bit 的数目(或者倍数),或者 4 个字节的倍数,所以 TCP 头部最多可以有 60 字节(4*15=60)。没有任何选项字段的 TCP 头部长度为 20 字节,所以其头部长度为 5,可以通过 20/4=5 计算得到。
(六)预留 6 位
头部长度后面预留的字段长度为 6 位,作为保留字段,暂时没有什么用处。
(七)控制标志
控制标志(Control Bits)共 6 个 bit 位,具体的标志位为:URG、ACK、PSH、RST、SYN、FIN。6 个标志位的说明,如下表所示。
表:TCP 报文控制标志(Control Bits)说明
标志位 | 说明 |
---|---|
URG | 占 1 位,表示紧急指针字段有效。URG 位指示报文段里的上层实体(数据)标记为 “紧急” 数据。当 URG=1 时,其后的紧急指针指示紧急数据在当前数据段中的位置 (相对于当前序列号的字节偏移量),TCP 接收方必须通知上层实体。 |
ACK | 占 1 位,置位 ACK=1 表示确认号字段有效;TCP 协议规定,接建立后所有发送的报文的 ACK 必须为 1;当 ACK=0 时,表示该数据段不包含确认信息。当 ACK=1 时,表示该报文段包括一个对已被成功接收报文段的确认序号 Acknowledgment Number,该序号同时也是下一个报文的预期序号。 |
PSH | 占 1 位,表示当前报文需要请求推(push)操作;当 PSH=1 时,接收方在收到数据后立即将数据交给上层,而不是直到整个缓冲区满。 |
RST | 占 1 位,置位 RST=1 表示复位 TCP 连接;用于重置一个已经混乱的连接,也可用于拒绝一个无效的数据段或者拒绝一个连接请求。如果数据段被设置了 RST 位,说明报文发送方有问题发生。 |
SYN | 占 1 位,在连接建立时用来同步序号。当 SYN=1 而 ACK=0 时,表明这是一个连接请求报文。对方若同意建立连接,则应在响应报文中使 SYN=1 和 ACK=1。 综合一下,SYN 置 1 就表示这是一个连接请求或连接接受报文。 |
FIN | 占 1 位,用于在释放 TCP 连接时,标识发送方比特流结束,用来释放一个连接。当 FIN = 1 时,表明此报文的发送方的数据已经发送完毕,并要求释放连接。 |
在连接建立的三次握手过程中,若只是单个 SYN 置位,表示的只是建立连接请求。如果 SYN 和 ACK 同时置位为 1,表示的建立连接之后的响应。
(八)窗口大小:
长度为 16 位,共 2 个字节。此字段用来进行流量控制。流量控制的单位为字节数,这个值是本端期望一次接收的字节数。
(九)校验和:
长度为 16 位,共 2 个字节。对整个 TCP 报文段,即 TCP 头部和 TCP 数据进行校验和计算,接收端用于对收到的数据包进行验证。
(十)紧急指针:
长度为 16 位,2 个字节。它是一个偏移量,和 SN 序号值相加表示紧急数据最后一个字节的序号。
以上十项内容是 TCP 报文首部必须的字段,也称固有字段,长度为 20 个字节。接下来是 TCP 报文的可选项和填充部分。
(十一)可选项和填充部分
可选项和填充部分的长度为 4n 字节(n 是整数),该部分是根据需要而增加的选项。如果不足 4n 字节,要加填充位,使得选项长度为 32 位(4 字节)的整数倍,具体的做法是在这个字段中加入额外的零,以确保 TCP 头是 32 位(4 字节)的整数倍。
最常见的选项字段是 MSS(Maximum Segment Size 最长报文大小),每个连接方通常都在通信的第一个报文段(SYN 标志为 1 的那个段)中指明这个选项字段,表示当前连接方所能接受的最大报文段的长度。
由于可选项和填充部分不是必须的,所以 TCP 报文首部最小长度为 20 个字节。
至此,TCP 报文首部的字段,就全部介绍完了。TCP 报文首部的后面,接着的是数据部分,不过数据部分是可选的。在一个连接建立和一个连接终止时,双方交换的报文段仅有 TCP 首部。如果一方没有数据要发送,也使用没有任何数据的首部来确认收到的数据,比如在处理超时的过程中,也会发送不带任何数据的报文段。
总体来说,TCP 协议的可靠性,主要通过以下几点来保障:
(1)应用数据分割成 TCP 认为最适合发送的数据块。这部分是通过 MSS(最大数据包长度)选项来控制的,通常这种机制也被称为一种协商机制,MSS 规定了 TCP 传往另一端的最大数据块的长度。值得注意的是,MSS 只能出现在 SYN 报文段中,若一方不接收来自另一方的 MSS 值,则 MSS 就定为 536 字节。一般来讲,MSS 值还是越大越好,这样可以提高网络的利用率。
(2)重传机制。设置定时器,等待确认包,如果定时器超时还没有收到确认包,则报文重传。
(3)对首部和数据进行校验。
(4)接收端对收到的数据进行排序,然后交给应用层。
(5)接收端丢弃重复的数据。
(6)TCP 还提供流量控制,主要是通过滑动窗口来实现流量控制。
至此 TCP 协议的数据帧格式介绍完了。接下来开始为大家重点介绍:TCP 传输层的三次握手建立连接,四次挥手释放连接。
TCP 的三次握手
TCP 连接的建立时,双方需要经过三次握手,而断开连接时,双方需要经过四次分手,那么,其三次握手和四次分手分别做了什么?又是如何进行的?
通常情况下,建立连接的双方,由一端打开一个监听套接字(ServerSocket)来监听来自请求方的 TCP(Socket)连接,当服务器端监听开始时,必须做好准备接受外来的连接,在 Java 中该操作通过创建一个 ServerSocket 服务监听套接字实例来完成,此操作会调用底层操作系统(如 Linux)的 C 代码中三个函数 socket ()、bind ()、listen ()来完成。开始监听之后,服务器端就做好接受外来连接的准备,如果监听到建立新连接的请求,会开启一个传输套接字,称之为被动打开(Passive Open)。
一段简单的服务端监听新连接请求,并且被动打开(Passive Open)传输套接字的 Java 示例代码,具体如下:
public class SocketServer {
public static void main(String[] args) {
try {
// 创建服务端socket,监听8080端口
ServerSocket serverSocket = new ServerSocket(8080);
// 循环监听等待客户端的连接
while (true) {
System.out.println("服务器正在侦听...");
// 阻塞,直到有客户端连接
Socket socket = serverSocket.accept();
System.out.println("收到新的连接:");
// 开启线程进行连接的I/O处理
ServerThread thread = new ServerThread(socket);
thread.start();
}
} catch (Exception e) {
// 处理异常,如被中断异常、套接字相关异常等
System.out.println("服务器启动失败:" + e.getMessage());
e.printStackTrace();
}
}
}
客户端在发起连接建立时,Java 代码通过创建 Socket 实例,调用底层的 connect (…) 方法,主动打开 (Active Open) Socket 连接。套接字监听方在收到请求之后,监听方和发起方(客户端)之间就会建立一条的连接通道,该通道由双方 IP 和双方端口所唯一确定。
一段简单的客户端连接主动打开 (Active Open) 的 Java 示例代码,具体如下:
public class SocketClient {
public static void main(String[] args) {
try {
// 和服务器创建连接
try (Socket socket = new Socket("localhost", 8080)) {
System.out.println(" successfully connected to the server.");
// 写入给监听方的输出流
try (OutputStream os = socket.getOutputStream()) {
os.write("Hello Server".getBytes());
System.out.println("Message sent to the server.");
}
// 读取监听方的输入流
try (InputStream is = socket.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(is))) {
String received;
while ((received = reader.readLine()) != null) {
System.out.println("Received from server: " + received);
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
三次握手过程
TCP 连接的建立时,双方需要经过三次握手,具体过程如下:
(1)第一次握手:Client 进入 SYN_SENT 状态,发送一个 SYN 帧来主动打开传输通道,该帧的 SYN 标志位被设置为 1,同时会带上 Client 分配好的 SN 序列号,该 SN 是根据时间产生的一个随机值,通常情况下每间隔 4ms 会加 1。除此之外,SYN 帧还会带一个 MSS(最大报文段长度)可选项的值,表示客户端发送出去的最大数据块的长度。
(2)第二次握手:Server 端在收到 SYN 帧之后,会进入 SYN_RCVD 状态,同时返回 SYN+ACK 帧给 Client,主要目的在于通知 Client,Server 端已经收到 SYN 消息,现在需要进行确认。Server 端发出的 SYN+ACK 帧的 ACK 标志位被设置为 1,其确认序号 AN(Acknowledgment Number)值被设置为 Client 的 SN+1;SYN+ACK 帧的 SYN 标志位被设置为 1,SN 值为 Server 端生成的 SN 序号;SYN+ACK 帧的 MSS(最大报文段长度)表示的是 Server 端的最大数据块长度。
(3)第三次握手:Client 在收到 Server 的第二次握手 SYN+ACK 确认帧之后,首先将自己的状态会从 SYN_SENT 变成 ESTABLISHED,表示自己方向的连接通道已经建立成功,Client 可以发送数据给 Server 端了。然后,Client 发 ACK 帧给 Server 端,该 ACK 帧的 ACK 标志位被设置为 1,其确认序号 AN(Acknowledgment Number)值被设置为 Server 端的 SN 序列号 + 1。还有一种情况,Client 可能会将 ACK 帧和第一帧要发送的数据,合并到一起发送给 Server 端。
(4)Server 端在收到 Client 的 ACK 帧之后,会从 SYN_RCVD 状态会进入 ESTABLISHED 状态,至此,Server 方向的通道连接建立成功,Server 可以发送数据给 Client,TCP 的全双工连接建立完成。
三次握手的图解
三次握手的交互过程,具体如下图所示:
图:TCP 建立的连接时三次握手示意图
Client 和 Server 完成了三次握手后,双方就进入了数据传输的阶段。数据传输完成后,连接将断开,连接断开的过程需要经历四次挥手。
TCP 的四次挥手
业务数据通信完成之后,TCP 连接开始断开(或者拆接)的过程,在这个过程中连接的每个端的都能独立地、主动的发起,断开的过程 TCP 协议使用了四路挥手操作。
四次挥手具体过程
四次挥手具体过程,具体如下:
(1)第一次挥手:主动断开方(可以是客户端,也可以是服务器端),向对方发送一个 FIN 结束请求报文,此报文的 FIN 位被设置为 1,并且正确设置 Sequence Number(序列号)和 Acknowledgment Number(确认号)。发送完成后,主动断开方进入 FIN_WAIT_1 状态,这表示主动断开方没有业务数据要发送给对方,准备关闭 SOCKET 连接了。
(2)第二次挥手:正常情况下,在收到了主动断开方发送的 FIN 断开请求报文后,被动断开方会发送一个 ACK 响应报文,报文的 Acknowledgment
Number(确认号)值为断开请求报文的 Sequence Number(序列号)加 1,该 ACK 确认报文的含义是:“我同意你的连接断开请求”。之后,被动断开方就进入了 CLOSE-WAIT(关闭等待)状态,TCP 协议服务会通知高层的应用进程,对方向本地方向的连接已经关闭,对方已经没有数据要发送了,若本地还要发送数据给对方,对方依然会接受。被动断开方的 CLOSE-WAIT(关闭等待)还要持续一段时间,也就是整个 CLOSE-WAIT 状态持续的时间。
主动断开方在收到了 ACK 报文后,由 FIN_WAIT_1 转换成 FIN_WAIT_2 状态。
(3)第三次挥手:在发送完成 ACK 报文后,被动断开方还可以继续完成业务数据的发送,待剩余数据发送完成后,或者 CLOSE-WAIT(关闭等待)截止后,被动断开方会向主动断开方发送一个 FIN+ACK 结束响应报文,表示被动断开方的数据都发送完了,然后,被动断开方进入 LAST_ACK 状态。
(4)第四次挥手:主动断开方收在到 FIN+ACK 断开响应报文后,还需要进行最后的确认,向被动断开方发送一个 ACK 确认报文,然后,自己就进入 TIME_WAIT 状态,等待超时后最终关闭连接。处于 TIME_WAIT 状态的主动断开方,在等待完成 2MSL 的时间后,如果期间没有收到其他报文,则证明对方已正常关闭,主动断开方的连接最终关闭。
被动断开方在收到主动断开方的最后的 ACK 报文以后,最终关闭了连接,自己啥也不管了。
四次挥手图解
四次挥手的全部交互过程,具体如下图所示:
图:TCP 建立的连接时四次挥手的示意图
处于 TIME_WAIT 状态的主动断开方,在等待完成 2MSL 的时间后,才真正关闭连接通道,其等待的时间为什么是 2MSL ?
2MSL 翻译过来就是两倍的 MSL。MSL 全称为 Maximum Segment Lifetime,指的是一个 TCP 报文片段在网络中最大的存活时间,具体来说,2MSL 对应于一次消息的来回(一个发送和一个回复)所需的最大时间。如果直到 2MSL,主动断开方都没有再一次收到对方的报文(如 FIN 报文),则可以推断 ACK 已经被对方成功接收,此时,主动断开方将最终结束自己的 TCP 连接。所以,TCP 的 TIME_WAIT 状态也称为 2MSL 等待状态。
有关 MSL 的具体的时间长度,在 RFC1122 协议中推荐为 2 分钟。在 SICS(瑞典计算机科学院)开发的一个小型开源的 TCP/IP 协议栈 ——LwIP 开源协议栈中 MSL 默认为 1 分钟。在源自 Berkeley 的 TCP 协议栈实现中 MSL 默认长度为 30 秒。总体来说,TIME_WAIT(2MSL)等待状态的时间长度,一般维持在 1-4 分钟之间。
通过三次握手建立连接和四次挥手拆除连接,一次 TCP 的连接建立及拆除,至少进行 7 次通信,可见其成本是很高的。
三次握手、四次挥手的常见面试题
有关 TCP 的连接建立的三次握手及拆除过程的四次挥手的面试问题,是技术面试过程中的出现频率很高的重点和难点问题,常见问题大致如下:
问题(1):为什么关闭连接的需要四次挥手,而建立连接却只要三次握手?
关闭连接时,被动断开方在收到对方的 FIN 结束请求报文时,很可能业务数据没有发送完成,并不能立即关闭连接,被动方只能先回复一个 ACK 响应报文,告诉主动断开方:“你发的 FIN 报文我收到了,只有等到我所有的业务报文都发送完了,我才能真正的结束,在结束之前,我会发你 FIN+ACK 报文的,你先等着”。所以,被动断开方的确认报文,需要拆开成为两步,故总体就需要四步挥手。
而在建立连接场景中,Server 端的应答可以稍微简单一些。当 Server 端收到 Client 端的 SYN 连接请求报文后,其中 ACK 报文表示对请求报文的应答,SYN 报文用来表示服务端的连接也已经同步开启了,而 ACK 报文和 SYN 报文之间,不会有其他报文需要发送,故而可以合二为一,可以直接发送一个 SYN+ACK 报文。所以,在建立连接时,只需要三次握手即可。
问题(2):为什么连接建立的时候是三次握手,可以改成两次握手吗?
三次握手完成两个重要的功能:一是双方都做好发送数据的准备工作,而且双方都知道对方已准备好;二是双方完成初始 SN 序列号的协商,双方的 SN 序列号在握手过程中被发送和确认。
如果把三次握手改成两次握手,可能发生死锁。两次握手的话,缺失了 Client 的二次确认 ACK 帧,假想的 TCP 建立的连接时二次挥手,可以如下图所示:
图:假想的 TCP 建立的连接时二次握手的示意图
在假想的 TCP 建立的连接时二次握手过程中,Client 发送 Server 发送一个 SYN 请求帧,Server 收到后发送了确认应答 SYN+ACK 帧。按照两次握手的协定,Server 认为连接已经成功地建立了,可以开始发送数据帧。这个过程中,如果确认应答 SYN+ACK 帧在传输中被丢失,Client 没有收到,Client 将不知道 Server 是否已准备好,也不知道 Server 的 SN 序列号,Client 认为连接还未建立成功,将忽略 Server 发来的任何数据分组,会一直等待 Server 的 SYN+ACK 确认应答帧。而 Server 在发出的数据帧后,一直没有收到对应的 ACK 确认后就会产生超时,重复发送同样的数据帧。这样就形成了死锁。
问题(3):为什么主动断开方在 TIME-WAIT 状态必须等待 2MSL 的时间?
原因之一:主动断开方等待 2MSL 的时间,是为了确保两端都能最终关闭。假设网络是不可靠的,被动断开方发送 FIN+ACK 报文后,其主动方的 ACK 响应报文有可能丢失,这时候的被动断开方处于 LAST-ACK 状态的,由于收不到 ACK 确认被动方一直不能正常的进入 CLOSED 状态。在这种场景下,被动断开方会超时重传 FIN+ACK 断开响应报文,如果主动断开方在 2MSL 时间内,收到这个重传的 FIN+ACK 报文,会重传一次 ACK 报文,后再一次重新启动 2MSL 计时等待,这样,就能确保被动断开方能收到 ACK 报文,从而能确保被动方顺利进入到 CLOSED 状态。只有这样,双方都能够确保关闭。反过来说,如果主动断开方在发送完 ACK 响应报文后,不是进入 TIME_WAIT 状态去等待 2MSL 时间,而是立即释放连接,则将无法收到被动方重传的 FIN+ACK 报文,所以不会再发送一次 ACK 确认报文,此时处于 LAST-ACK 状态的被动断开方,无法正常进入到 CLOSED 状态。
原因之二:防止 “旧连接的已失效的数据报文” 出现在新连接中。主动断开方在发送完最后一个 ACK 报文后,再经过 2MSL,才能最终关闭和释放端口,这就意味着,相同端口的新 TCP 新连接,需要在 2MSL 的时间之后,才能够正常的建立。2MSL 这段时间内,旧连接所产生的所有数据报文,都已经从网络中消失了,从而,确保了下一个新的连接中不会出现这种旧连接请求报文。
问题(4):如果已经建立了连接,但是 Client 端突然出现故障了怎么办?
TCP 还设有一个保活计时器,Client 端如果出现故障,Server 端不能一直等下去,这样会浪费系统资源。每收到一次 Client 客户端的数据帧后,Server 端都的保活计时器会复位。计时器的超时时间通常是设置为 2 小时,若 2 小时还没有收到 Client 端的任何数据帧,Server 端就会发送一个探测报文段,以后每隔 75 秒钟发送一次。若一连发送 10 个探测报文仍然没反应,Server 端就认为 Client 端出了故障,接着就关闭连接。如果觉得保活计时器的两个多小时的间隔太长,可以自行调整 TCP 连接的保活参数。.
网络 - TCP 协议详解自学笔记(例题、代码、实战)
lady_killer9 于 2020 - 11 - 30 16:02:19 发布
简介
TCP 协议又叫传输控制协议 (Transport Control Protocal),是 面向连接的,可靠的 字节流服务。它的连接是 虚连接,连接的端点是 套接字(SOCKET)。它的可靠性体现在:3 次握手 建立连接,滑动窗口 机制,一定的 拥塞避免 算法,流量控制,以及一定的 超时重传 机制。基于 TCP 的协议有 HTTP、FTP、SMTP、TELNET、SSH、MQTT 等。
TCP 的服务与缓存
服务
谈 TCP 服务之前先说一下计时器,很多服务需要计时器的支持。
- 2MSL 计时器:测量一个连接处于 TIME_WAIT 状态的时间,四次挥手(连接释放)时有用到。
- 重传计时器:使用于当希望收到另一端的确认。超时重传、拥塞避免 时有用到。
- 坚持计时器:使窗口大小信息保持不断流动,即接收端发送了接收窗口为 0 的报文后启动。流量控制 时有用到。
- 保活计时器:可检测到一个空闲连接的另一端何时崩溃或重启。
接下来是 TCP 提供的服务:
数据块分割:应用数据被分割成 TCP 认为最适合发送的数据块,由 TCP 传递给 IP 的信息单位称为 报文段 或段(segment)。
重新排序:既然 TCP 报文段作为 IP 数据报来传输,而 IP 数据报的到达可能会 失序,因此 TCP 报文段的到达也可能会失序。如果必要,TCP 将对收到的数据进行 重新排序,将收到的数据以正确的顺序交给应用层。
全双工 (Full Duplex) 通信:又称为双向同时通信,即通信的 双方 可以 同时发送和接收 信息的信息交互方式。
可靠传输:通过校验和、序号、确认与重传进行可靠的传输。
流量控制:通过 滑动窗口 算法,设置 接收窗口 大小来进行流量控制。
拥塞控制:通过 慢开始 & 拥塞避免、快重传 & 快恢复 算法进行拥塞控制。
不提供广播或多播的服务。
缓存
发送缓存:准备发送 的数据和 已发送但未收到确认 的数据
发送缓存图解
图中红色部分为已发送但未收到确认的数据,发送缓存中的其余部分为准备发送的数据。
接收缓存:按序到达但未接受应用程序读取的 数据和 不按序到达的 数据
接收缓存图解
图中接收缓存中标识 “已收到” 的表示按序到达但未接受应用程序读取的数据,接收窗口中的红色部分是不按序到达的数据。
TCP 头部
TCP 首部格式
- 源端口和目的端口:各占 2 字节。端口是传输层与应用层的服务接口。传输层的复用和分用功能都要通过端口才能实现
- 序号 seq:占 4 字节。数据中首个字节在发送缓存中的序号。
- 确认序号 ack:占 4 字节。为 N 时表明序号 N - 1 及之前的报文段都已经收到,期待接收序号为 N 的报文段。
- 首部长度 / 数据偏移:占 4 位。它指出 TCP 报文段的 数据起始处 距离 TCP 报文段的起始处有多远。“数据偏移” 的单位是 32 位字 (以 4 字节为计算单位)。
- 紧急位 URG: 为 1 时,紧急指针有效,紧急数据优先级更高,优先发送。
- 确认位 ACK: 为 1 时,确认序号 ack 有效,连接建立后,传送的报文段都必须把 ACK 置为 1。
- 推送位 PSH: 为 1 时,接收方应该尽快将这个报文段交给应用层,不用等缓存填满,优先级更高,报文段优先交给应用层。
- 复位 RST: 为 1 时,表明 TCP 连接中出现严重差错,需要释放连接后,重建连接。
- 同步位 SYN: 为 1 时,用来 发起 / 确认一个连接。
- 终止位 FIN: 为 1 时,表明发端完成发送任务,要求释放连接。
- 窗口:接收窗口,即允许对方发送的数据量。
- 检验和:检验首部 + 数据
- 紧急指针:URG 为 1 时有效,指出本报文段中紧急数据的字节数(从数据的第一个字节开始)。
- 选项:
- 最大报文段长度 MSS (Maximum Segment Size):发送方告诉接收方,自身缓存所能接收的报文段的数据字段的最大长度是 MSS 个字节。
- 窗口扩大:占 3 字节,其中有一个字节表示移位值 S。新的窗口值等于 TCP 首部中的窗口位数增大到 (16 + S),相当于把窗口值向左移动 S 位后获得实际的窗口大小
- 时间戳:占 10 字节,其中最主要的字段时间戳值字段 (4 字节) 和时间戳回送回答字段 (4 字节)
- 选择确认:接收方收到了和前面的字节流不连续的两字节。如果这些字节的序号都在接收窗口之内,那么接收方就先收下这些数据,但要把这些信息准确地告诉发送方,使发送方不要再重复发送这些已收到的数据。
- 填充:这是为了使整个首部长度是 4 字节的整数倍。
TCP 的建立与终止
TCP 连接的建立采用 客户 / 服务器 方式,主动 发起连接建立的应用进程叫做 客户,而 被动 等待连接建立的应用进程叫 服务器。
TCP 连接四元组 (sip, sport, dip, dport),源 ip、源端口号、目的 ip、目的端口号。
三次握手
TCP 三次握手
客户端发送连接请求报文段,无应用层数据。同步位 SYN = 1,表示请求连接。序号 seq = x(随机)。确认序号无效,ACK = 0,因为客户端未收到服务器端的报文段。
- 服务器为该 TCP 连接分配 缓存 和变量,向客户端返回确认报文段,允许连接,无应用层数据。同步位 SYN = 1,表示确认连接。序号 seq = y(随机),确认位 ACK = 1,确认序号有效,确认序号 ack = x + 1,即 x + 1 之前的已经收到,期待接收 x + 1 开始的报文段。
- 客户端为该 TCP 连接分配 缓存 和变量,并向服务器端返回确认报文段,可以携带数据。同步位 SYN = 0,表示连接建立完毕,以后的也是 0。序号 seq = x + 1。确认位 ACK = 1,确认序号 ack = y + 1,即 y + 1 之前的已经收到,期待接收 y + 1 开始的报文段。
CLOSED: 表示初始关闭状态。
LISTEN(服务器): 这个也是非常容易理解的一个状态,表示服务器端的某个 SOCKET 处于监听状态,可以接受连接了。
SYN - SENT: 这个状态与 SYN - RCVD 呼应,当客户端 SOCKET 执行 CONNECT 连接时,它首先发送 SYN 报文,因此也随即它会进入到了 SYN - SENT 状态,并等待服务端的发送三次握手中的第 2 个报文。SYN - SENT 状态表示客户端已发送 SYN 报文。
SYN - RCVD(服务器): 这个状态表示接受到了 SYN 报文,在正常情况下,这个状态是服务器端的 SOCKET 在建立 TCP 连接时的三次握手会话过程中的一个中间状态,很短暂,基本上很难看到这种状态的,除非你特意写了一个客户端测试程序,故意将三次 TCP 握手过程中最后一个 ACK 报文不予发送。当收到客户端的 ACK 报文后,它会进入到 ESTABLISHED 状态。
ESTABLISHED: 这个容易理解了,表示连接已经建立了,可以发送数据。
SYN 洪泛攻击: 攻击者向服务器发送大量 TCP 连接请求,服务器确认后,攻击者 不确认,造成服务器处于 半连接 状态,消耗了过多的资源(CPU、内存等),造成死机等问题,无法为正常用户提供服务。可使用 SYN Cookie 进行防御。
四次挥手
客户端和服务器端都可以终止该连接,连接结束后,主机中的资源被释放。
以客户端主动终止连接为例。
TCP 四次挥手
- 客户端发送连接释放报文段,停止发送数据。结束位 FIN = 1,表明要释放报文段,序号 seq = u,假设上一个报文段序号为 u - 1。
- 服务器端回复一个确认报文段,客户到服务器这个方向的连接就释放了 —— 半关闭状态。确认位 ACK = 1,序号 seq = v,假设上一个报文段序号为 v - 1,确认序号 ack = u + 1。
- 服务器端发完数据,就发出连接释放报文段,主动关闭 TCP 连接。结束位 FIN = 1,确认位 ACK = 1,序号 seq = w(服务器端上次发送的数据序号为 v ~ w - 1),确认序号 ack = u + 1(因为客户端未发送数据)。
- 客户端回复一个确认报文段。确认位 ACK = 1,序号 seq = u + 1,确认序号 ack = w + 1。
ESTABLISHED: 建立连接成功,通信中。
CLOSE - WAIT: 表示被动关闭一方在 等待关闭。当主动关闭连接的一方关闭 SOCKET 后发送 FIN 报文给被动关闭一方,被动关闭一方回应一个 ACK 报文给对方,此时被动关闭一方则进入到 CLOSE - WAIT 状态
FIN - WAIT - 1: 是当 Socket 在已经连接的状态时 主动关闭连接,向对方发送了 FIN 报文,此时该 Socket 进入到 FIN - WAIT - 1 状态。而当对方回应 ACK 报文后,则进入到 FIN - WAIT - 2 状态
FIN - WAIT - 2: 表示半连接,挥了两次手的状态等待对方的 Fin 报文
TIM - WAIT: TCP 协议中主动关闭连接的一方要处于 TIME - WAIT 状态,等待两个 MSL(maximum segment lifetime)的时间后才能回到 CLOSED 状态,在 TIME - WAIT 期间仍然不能再次监听同样的 server 端口。
LAST - ACK:被动关闭一方在发送 FIN 报文后,最后等待对方的 ACK 报文。当收到 ACK 报文后进入 CLOSED 状态。
CLOSED:已经完全关闭。
服务器端收到确认报文段后关闭 TCP 连接。客户端等待两倍的 MSL (Maximum Segment Lifetime,报文段最长寿命) 时间后关闭连接,因为如果服务器端没有收到客户端回复的确认报文段会再次发出连接释放报文段,2MSL 后没有收到服务器端再次发出的连接释放报文段,说明服务器端已关闭。
例题
[例题] 主机甲与主机乙之间已建立一个 TCP 连接,主机甲向主机乙发送了 两个连续的 TCP 段,分别包含 300 字节和 500 字节 的有效载荷,第一个段的序列号为 200,主机乙 正确接收到两个段后,发送给主机甲的确认序列号是
A.500
B.700
C.800
D.1000
[解析] 确认序号即期待的下一个报文段的开始字节,由于主机乙正确接收了两个段,所以乙收到了 200 ~ 999(200 + 800 - 1)字节,期待第 1000 字节,所以选择 D 选项。
有限状态机
TCP 有限状态机
粗实线为客户端三次握手与四次挥手的状态转移,粗虚线为服务器端三次握手与四次挥手的状态转移。
可靠传输
校验
增加伪首部
滑动窗口
以 字节 为单位的滑动窗口,大小可变。
接收窗口 rwnd(recevier window):接收方根据接收缓存设置的值通知发送方,反映接收方容量。
拥塞窗口 cwnd(congestion window):发送方根据自己估算的网络拥塞程度而设置的窗口值,反应网络当前容量。初始值 IW 根据 SMSS(SENDER MAXIMUM SEGMENT SIZE,发送方最大报文段大小,不包含 TCP 头部)来设定。
发送窗口 = Min {接收窗口 rwnd,拥塞窗口 cwnd}
确认与重传
累计确认与超时重传
超时重传:TCP 发送方在规定的时间(重传时间,使用重传计时器)内 没有收到确认就要重传 已发送的报文段。【重传时间:采用自适应算法,动态改变重传时间 RTTs(加权平均往返时间)。RTT(round - trip time)为数据完全发送完(交给发送方 IP 层)到收到确认信号的时间。】
累计确认:收到多个确认一次。接收方 “痴心不改”,例如,收到序号 1、2、3、6 的报文段,到了时间会发送确认序号 ack = 4 的报文段,即使后续再收到了 7、5,也依然会通知发送方 “小老弟,我还是想要序号为 4 的报文段”。
超时重传解决的是传输可靠性问题,累计确认是为了更快速的传输。
冗余确认与快速重传
冗余确认:TCP 接收方已经发送过的确认报文段。由于 收到了比确认报文段所期待的报文段序号更大的报文段,而未收到确认报文段所期待的报文段,则再次发送确认报文段。如前面的序号 4。【冗余确认是原因可能是乱序到达和丢包,两次的话可能是乱序到达造成的,三次的话绝大部分是丢包了,需要快速重传。】
快速重传:TCP 发送方在未达到重传时间时收到接收方的 冗余 确认(三个重复的)就要重传已发送的报文段。
流量控制
目的:接收方希望发送方发慢一点,好来得及接收。
方法:利用 滑动窗口 机制实现流量控制。
在通信过程中,接收方 根据自己 接收缓存 的大小,动态地调整发送方的发送窗口大小,即接收窗口 rwnd(接收方设置 确认报文 段的 窗口字段 来将 rwnd 通知给发送方),发送方的发送窗口取 接收窗口 rwnd 和拥塞窗口 cwnd 的最小值。
举例:
开始时,B 通知 A“老弟,我的接收窗口是 400”。
流量控制 - 滑动窗口
注:填充黄色的为发送方 A 的发送窗口,绿色字体代表已发送的数据(接收方不一定接收,看 ack)。
最后,B 通知 A 接收窗口为 0,这时,B 可能在忙着给应用层传输数据。那么,什么时候 A 可以再给 B 发送数据呢?需要 B 再次给 A 发送一个报文段,通知 A“老弟,你可以发送数据了,rwnd = xxx”。
但是,假如 B 发送的这个报文,A 没有收到,这时 A 等待 B 通知,B 等待 A 发送数据,就会死锁。为了解决这个问题,需要使用 坚持计时器。
在发送方收到接收方的 rwnd = 0 时,坚持计时器启动。坚持计时器到期,发送方发送 零窗口探测报文段(仅 1 字节),询问接收方 “老哥,你还不接收数据吗?”,如果 B 给 A 发送过了通知,这时接收到发送方的探测报文段,就知道它的通知丢失了,A 没有收到,就会再发送通知,A 接收到后继续发送数据,双方正常通信。
注:TCP 规定,即使设置为 零窗口,也必须接收以下几种报文段:零窗口探测报文段、确认报文段和携带紧急数据的报文段。
例题
[例题] 主机甲和主机乙之间已建立了一个 TCP 连接,TCP 最大段长度为 1000 字节。若主机甲的当前拥塞窗口为 4000 字节,在主机甲向主机乙连续发送两个最大段后,成功收到主机乙发送的 第一个段的确认段,确认段中通告的接收窗口大小为 2000 字节,则此时主机甲还可以向主机乙发送的最大字节数是
A.1000
B.2000
C.3000
D.4000
[解析] 发送窗口 = Min {rwnd,cwnd} = Min {2000, 4000} = 2000。第一个 1000 字节已确认,窗口滑动,第一个 1000 字节不在发送窗口中。由于第二个 1000 字节没有被主机乙确认,可能丢失,存在需要重传的可能性,这 1000 字节还在发送窗口中,因此,还可以发送的最大字节数是 2000 - 1000 = 1000 字节。之后主机甲等待主机乙确认或超时重传。
拥塞控制
出现条件:资源需求总和 > 可用资源。
目的:防止过多的数据注入到网络中。
假设:数据 单方向传送,另一方向只传送确认,发送方接到确认后增加拥塞窗口大小。接收方有足够大的缓存空间,发送窗口取决于拥塞程度,即 发送窗口 = 拥塞窗口。
为了讨论方便,纵坐标的单位为 最大报文段,长度为 MSS。横纵标单位为发送了一批报文段并收到他们确认的时间,即 往返时延 RTT。
下面的算法,可查看 RFC 5681
慢开始与拥塞避免
ssthresh(slow start thresh):慢开始阈值,达到后 “加法增大”。
乘法减小:新的 ssthresh 为上次拥塞状态下 拥塞窗口的一半,RFC 5681 中写的是 ssthresh = max (FlightSize / 2, 2 * SMSS),我们这里就直接采用 FlightSize / 2 了。
慢开始:起始值特别低,例如 1,倍增,超时后回到起始值。
拥塞避免:达到 ssthresh 后,加法增大。达到网络拥塞状态后,拥塞窗口回归到慢开始状态即初始值。
慢开始和拥塞避免
开始值为 1,未达到 ssthresh 前,执行慢开始算法,倍增,1、2、4、8、16;达到 ssthresh,执行拥塞避免算法,加法增大,17、18、19、…、24,超时,新的 ssthresh = 24 / 2 = 12,回到 1,后续类似。
快重传与快恢复
快速重传:TCP 发送方在未达到重传时间时收到接收方的冗余确认(三个重复的,加上之前收到的,共 4 个一样的 ack)就要重传已发送的报文段。
快恢复:达到网络拥塞状态后,拥塞窗口不是回归到初始状态,而是从 新的 ssthresh(原来的减半)开始,加法增大。
快重传和快恢复
开始值为 1,未达到 ssthresh 前,执行慢开始算法,倍增,1、2、4、8、16;达到 ssthresh,执行拥塞避免算法,加法增大,17、18、19、…、24,收到 3 个重复的确认,新的 ssthresh = 24 / 2 = 12,回到新的 ssthresh 即 12,后续类似。
例题
[例题] 一个 TCP 连接总是以 1KB 的最大段长发送 TCP 段,发送方有足够多的数据要发送。当拥塞窗口为 16KB 时发生了 超时,如果接下来的 4 个 RTT 时间内的 TCP 段的传输都是成功的,那么当第四个 RTT 时间内发送的所有 TCP 段都得到肯定应答时,拥塞窗口大小是
A. 7KB
B. 8KB
C. 9KB
D. 16KB
[解析] 由题可知是超时而不是收到 3 个重复确认,所以采用的是慢开始与拥塞避免算法。超时后进行下一轮,新的 ssthresh = 16 / 2 = 8。4 个 RTT 拥塞窗口分别为 1、2、4、8,此时达到新的 ssthresh,所以拥塞窗口采用加法增大,应该是 9KB,选择 C 选项。
[例题] 主机甲和主机乙已建立了 TCP 连接,甲始终以 MSS = 1KB 大小的段发送数据,并一直有数据发送;乙每收到一个数据段都会发出一个 接收窗口为 10KB 的确认段。若甲在 t 时刻发生 超时时拥塞窗口为 8KB,则从 t 时刻起,不再发生超时 的情况下,经过 10 个 RTT 后,甲的 发送窗口 是
A. 10KB
B. 12KB
C. 14KB
D. 15KB
[解析] 超时后,新的 ssthresh = 8 / 2 = 4。拥塞窗口慢开始阶段变化为 1、2、4,之后进入拥塞避免阶段,5、6、7、8、9、10、11,之后拥塞窗口是 12。注意,题目问的是发送窗口,发送窗口大小为 Min {10, 12} = 10KB,选择选项 A。
TCP 和 UDP 的区别
比较项 | TCP | UDP |
---|---|---|
面向连接 | 是 | 否 |
可靠 / 安全 | 是 | 否 |
速度 | 慢 | 快 |
面向 | 字节流 | 报文 |
应用范围 | 大量数据 | 少量数据 |
实战
自己抓包,分析一下各个字段。以百度为例。
抓包
cmd 下,ping 命令,获取百度 ip
百度 ip
打开 wireshark,设置过滤器 tcp&&ip.addr==61.135.169.121,运行 wireshark,浏览器百度一下 lady_killer。
抓包结果
三次握手
第一次握手
标志位
选项
- 源端口和目的端口:源端口号 32371(7e 73)、目的端口号 433(01 bb),各占 2 字节。
- 序号 seq:序号 4181209650(f9 38 32 32),占 4 字节。
- 确认序号 ack:确认序号 0(00 00 00 00),占 4 字节。
- 头部长度:80,代表头部是 32 字节,占一个字节。
- 标志位:标志位中同步位 SYN = 1(02),占 1 个字节。
- 窗口:接收窗口 64240(fa f0),占 2 个字节。
- 检验和:检验和 b8 e1,占 2 个字节。
- 紧急指针:紧急指针 0(00 00),占 2 个字节。
- 选项:
- 最大报文段长度 MSS (Maximum Segment Size):1460(02 04 05 b4)字节,占 4 个字节。
- 窗口扩大:03 03 08,占 3 个字节。
- 选择确认:04 02,占 2 个字节。
第二次握手
标志位
选项
- 源端口和目的端口:源端口号 433(01 bb),目的端口号 32371(7e 73),各占 2 字节。
- 序号 seq:序号 1900885209(71 4d 34 d9),占 4 字节。
- 确认序号 ack:确认序号 4181209651(f9 38 32 33),占 4 字节。
- 头部长度:80,代表头部是 32 字节,占一个字节。
- 标志位:标志位中确认位 ACK = 1、同步位 SYN = 1(12),占 1 个字节。
- 窗口:接收窗口 8192(20 00),占 2 个字节。
- 检验和:检验和 44 9b,占 2 个字节。
- 紧急指针:紧急指针 0(00 00),占 2 个字节。
- 选项:
- 最大报文段长度 MSS (Maximum Segment Size):1440(02 04 05 a0)字节,占 4 个字节。
- 窗口扩大:03 03 05,占 3 个字节。
- 选择确认:04 02,占 2 个字节。
第三次握手
标志位
- 源端口和目的端口:源端口号 32371(7e 73)、目的端口号 433(01 bb),各占 2 字节。
- 序号 seq:序号 4181209651(f9 38 32 33),占 4 字节。
- 确认序号 ack:确认序号 1900885210(71 4d 34 da),占 4 字节。
- 头部长度:50,代表头部是 20 字节,占一个字节。
- 标志位:标志位中确认位 ACK = 1(10),占 1 个字节。
- 窗口:接收窗口 517(02 05),占 2 个字节。
- 检验和:检验和 a3 52,占 2 个字节。
- 紧急指针:紧急指针 0(00 00),占 2 个字节。
- 选项:无
之后的数据传输涉及到 TLS 协议,不在本篇文章探讨范围内了。
自己实现 TCP 客户端和服务器端可以查看文章:
- python - 网络编程之 socket
参考
- 《TCP/IP 详解卷 1》第 17 - 24 章
- 《计算机网络(谢希仁)第七版》5.3 - 5.9
- RFC 5681 - TCP Congestion Control
- RFC 2581 - TCP Congestion Control
- (传输层) TCP 协议 - kzangv - 博客园
网络 - IP 协议详解(报文格式、分类、NAT、子网、CIDR、抓包分析)
lady_killer9 于 2021-02-01 17:34:11 发布
简介
IP (网际互连协议,Internet Protocol) 是 TCP/IP 协议族中最为核心的协议。所有的 TCP、UDP、ICMP 及 IGMP 数据都以 IP 数据报格式传输。
网际协议 IP 又称为 Kahn - Cerf 协议,因为这个重要协议正是 Robert Kahn 和 Vint Cerf 二人共同研发的,这两位学者在 2005 年获得图灵奖。
报文格式
IP 报文格式
- 版本:占 4 位,指 IP 协议的版本。通信双方使用的 IP 协议的版本必须一致。目前广泛使用的 IP 协议版本号为 4(即 IPv4),以后要使用 IPv6(即版本 6 的 IP 协议)。
- 首部长度:占 4 位,可表示的范围是 0 ~ 15,单位是 4B。因为 IP 首部的固定长度是 20 字节,因此首部长度字段的 最小值是 5(即二进制表示的首部长度是 0101)。而当首部长度为最大值 1111 时(即十进制数的 15),就表明首部长度达到最大值 15 个 32 位字长,即 60 字节。当 IP 分组的首部长度不是 4 字节的整数倍时,必须利用最后的填充字段加以填充。因此 IP 数据报的数据部分永远在 4 字节的整数倍时开始。首部长度限制为 60 字节的缺点是有时可能不够用,但这样做是希望用户尽量减少开销。最常用的首部长度是 20 字节(即首部长度为 0101),这时不使用任何选项。
- 区分服务:占 8 位,用来获得更好的服务。这个字段在旧标准中叫做服务类型,但实际上一直没有被使用过。1998 年 IETF 把这个字段改名为区分服务 DS(Differentiated Services)。只有在使用区分服务时,这个字段才起作用,在一般的情况下都不使用这个字段。
- 总长度:占 16 位,指 首部和数据之和的长度,单位为字节。总长度字段为 16 位,因此数据报的最大长度为 2 16 − 1 = 65535 2^{16}-1 = 65535 216−1=65535 字节。
- 标识(identification):占 16 位。IP 软件在存储器中维持一个计数器,每产生一个数据报,计数器就加 1,并将此值赋给标识字段。当数据报由于长度超过网络的 MTU 而必须分片时,这个标识字段的值就被复制到所有的数据报片的标识字段中。相同的标识字段的值使分片后的各数据报片最后能正确地重装成为原来的数据报。
- 标志(flag):占 3 位,但目前只有低两位有意义。标志字段中的 最低位记为 MF(More Fragment)。MF = 1 即表示后面 “还有分片” 的数据报。MF = 0 表示这已是若干数据报片中的最后一个。标志字段 中间的一位记为 DF(Don’t Fragment),意思是 “不能分片”。只有当 DF = 0 时才允许分片。
- 片偏移:占 13 位。片偏移指出:较长的分组在分片后,某片在原分组中的相对位置。也就是说,相对于用户数据字段的起点,该片从何处开始。片偏移以 8 个字节为偏移单位。这就是说,除了最后一个分片,长度一定是 8 字节(64 位)的整数倍,最后一个分片可能是 8 字节(64 位)的整数倍。
- 生存时间:占 8 位,生存时间字段常用的英文缩写是 TTL(Time To Live),表明这是 数据报在网络中的寿命。由发出数据报的 源点设置 这个字段。其目的是防止无法交付的数据报无限制地在互联网中兜圈子(例如从路由器 R1 转发到 R2,再转发到 R3,然后又转发到 R1),因而白白消耗网络资源。最初的设计是以秒作为 TTL 值的单位。每经过一个路由器时,就把 TTL 减去数据报在路由器所消耗掉的一段时间。若数据报在路由器消耗的时间小于 1 秒,就把 TTL 值减 1。当 TTL 值减为零时,就丢弃这个数据报。
- 协议:占 8 位,协议字段指出此数据报携带的 数据是使用何种协议,以便使目的主机的 IP 层知道应将数据部分上交给哪个协议进行处理。至少记住 TCP 和 UDP(腾讯安全工程师笔试题)。
- 首部检验和:占 16 位。这个字段 只检验数据报的首部,但不包括数据部分。这是因为数据报每经过一个路由器,路由器都要重新计算一下首部检验和(一些字段,如生存时间、标志、片偏移等都可能发生变化)。不检验数据部分可减少计算的工作量。
- 源地址:占 32 位。
- 目的地址:占 32 位。
- 可选字段:0 ~ 40B,用来支持排错、测量以及安全等措施。
- 填充:全 0,把首部补成 4B 的整数倍。
IPv4 地址
分类
IP 地址表示如下:
I P 地址 : : = { ⟨ 网络号 ⟩ , ⟨ 主机号 ⟩ } IP 地址::= \{\langle 网络号 \rangle, \langle 主机号 \rangle\} IP地址::={⟨网络号⟩,⟨主机号⟩}
IP 分类
下面是一个 C 类 IP 地址
11011111 00000001 00000001 00000001
二进制对于人类来说很难记忆,所以我们将 IP 地址分为上面的 4 个部分,每个部分 8 位 (0 - 255),使用 .
分隔,就是 点分十进制。
- A 类 IP 地址:地址范围 1.0.0.1 - 126.255.255.254(二进制表示为:00000001 00000000 00000000 00000001 - 01111110 11111111 11111111 11111110)
- B 类 IP 地址:地址范围 128.1.0.1 - 191.254.255.254(二进制表示为:10000000 00000001 00000000 00000001 - 10111111 11111110 11111111 11111110)
- C 类 IP 地址:范围 192.0.1.1 - 223.255.254.254(二进制表示为: 11000000 00000000 00000001 00000001 - 11011111 11111111 11111110 11111110)
- D 类 IP 地址:范围 224.0.0.1 - 239.255.255.254(二进制表示为:11100000 00000001 00000001 00000001 - 11101111 11111111 11111111 11111110)
A 类是 7 位,应该是 0 - 127 ,为什么是 1 - 126 ?因为一些规定,有些 ip 作为了特殊 ip,不能作为 A 类地址的网络。
以下特殊 IP 需要单独记忆一下
特殊 IP
除去特殊 ip 后
可指派范围
网络号全 0 表示本网络
主机号全为 0 表示指向本网,主机号全为 1 表示广播地址,16777214 是 2 24 − 2 2^{24}-2 224−2,去掉全 0 和全 1,其他同理。
私有 IP 也需要注意一下, 私有 IP 就是本地网络的 IP,路由器不会转发目的地址是私有地址的数据报。
私有 IP
我们平常用的大多是 C 类的,如果在使用电脑,可以打开命令窗口查看一下,Windows 是使用 ipconfig、Linux 是使用 ifconfig。
网络地址转换 NAT
前面我们提到了私有 IP 地址,在电脑上查看到的也是私有 IP,那么如何与外网通信?这就需要 NAT。
网络地址转换 NAT(Network Address Translation):在专用网连接到因特网的路由器上安装 NAT 软件,安装了 NAT 软件的路由器叫 NAT 路由器,它至少有一个有效的外部全球 IP 地址。
以我当前的网络为例,家里的 WiFi,浏览器输入 192.168.31.1,进入小米路由器:
公网 ip
子网掩码可以看后面,DNS 学习可以查看:
- 网络 - DNS 域名系统详解与 DNS 攻击
局域网
本地网络:192.168.31.0
路由器本地端口 ip / 本地网关:192.168.31.1
公网 ip:110.255.250.111
NAT 路由器内部会维护一个 NAT 表,进行本地 ip:端口到外部网络 ip:端口的映射。例如,192.168.31.164:50001 -> 110.255.250.111:40001,那么我访问百度,就是本地数据包发给路由器,路由器转发数据包,百度响应后,发给路由器,路由器收到后再根据是局域网内的哪个设备请求的再给予分发数据包进行回应。
子网划分与子网掩码
两级 IP 有一些缺点:
- 第一,IP 地址空间的利用率有时很低。
- 第二,给每一个物理网络分配一个网络号会使路由表变得太大因而使网络性能变坏。
- 第三,两级 IP 地址不够灵活。
例如,一个单位申请了一个 B 类地址,但是公司没有那么多台电脑,但又可能会扩充,这个时候可能会按照部门进行子网的划分,增加部门内部人员或增加部门就比较方便,此时,单位内部的网络 IP 可表示为:
I P 地址 : : = { ⟨ 网络号 ⟩ , ⟨ 子网号 ⟩ , ⟨ 主机号 ⟩ } IP 地址::= \{\langle 网络号 \rangle, \langle 子网号 \rangle, \langle 主机号 \rangle\} IP地址::={⟨网络号⟩,⟨子网号⟩,⟨主机号⟩}
子网划分
下面用例子说明划分子网的概念。下图表示某单位拥有一个 B 类 IP 地址,网络地址是 145.13.0.0(网络号是 145.13)。目的地址为 145.13.x.x 的数据报都被送到这个网络上的路由器 R1。
公司网络
现把单位网络划分为三个子网。这里假定 子网号占用 8 位,因此在增加了子网号后,主机号就只有 8 位。所划分的三个子网分别是:145.13.3.0,145.13.7.0 和 145.13.21.0。在划分子网后,整个网络对外部仍表现为一个网络,其网络地址仍为 145.13.0.0。但网络 145.13.0.0 上的路由器 R1 在收到外来的数据报后,再根据数据报的目的地址把它转发到相应的子网。
子网可有 2 8 2^{8} 28 共 256 个,现分配了 3、7、21,还可分配 253 个。
子网 145.13.3.0 可有主机共 2 8 − 2 = 254 2^{8}-2 = 254 28−2=254 个,145.3.3.1 - 145.3.3.9 以及 145.3.3.102 - 145.3.3.254 可以分配给新来本部门的同事。其余两个子网同理。
子网掩码
子网掩码,主机号部分全 0,剩下全 1。可用于和 ip 相与,判断是否属于本网络。上面的子网掩码为 11111111 11111111 11111111 00000000,写成点分十进制为 255.255.255.0。
例题:
已知 IP 地址是 141.14.72.24,子网掩码是 255.255.192.0,求网络地址。
72 的二进制为 01001000,192 的二进制为 11000000,相与后为 01000000,即 64,故网络地址为 141.14.64.0。
主机起始 ip:01110010 00001110 01000000 00000001 141.14.64.1
主机结束 ip:01110010 00001110 01111111 111111110 141.14.127.254
例题:
某主机的 IP 地址为 180.80.77.55,子网掩码为 255.255.252.0。若该主机向其所在子网发送广播分组,则目的地址可以是()。
A.180.80.76.0
B.180.80.76.255
C.180.80.77.255
D.180.80.79.255
77 的二进制是 01001101,252 的二进制是 11111100,相与后为 01001100,十进制为 76。广播分组为主机号全 1,即第三个为 01001111(79),第四个为 255,所以选 D。
CIDR
CIDR (Classless Inter - Domain Routing,无类别域间路由) 是一个用于给用户分配 IP 地址以及在互联网上有效地路由 IP 数据包的对 IP 地址进行归类的方法。
I P 地址 : : = { ⟨ 网络前缀 ⟩ , ⟨ 主机号 ⟩ } IP 地址::= \{\langle 网络前缀\rangle, \langle主机号\rangle\} IP地址::={⟨网络前缀⟩,⟨主机号⟩}
CIDR 写法为 ip / 网络前缀位数,例如,128.14.32.7/20。
128.14.32.7 写成二进制 10000000 00001110 00100000 00000111,其中下划线加粗的 20 位就是网络前缀,本网络
开始地址:10000000 00001110 00100000 00000000 即 128.14.32.0
结束地址:10000000 00001110 00101111 11111111 即 128.14.47.255
当然,还是全 0 表示本网络,全 1 为广播地址,不能分配给主机。
最长前缀匹配
使用 CIDR 时,查找路由表可能得到几个匹配结果,应选择具有最长网络前缀的路由。前缀越长,地址块越小,路由越具体。
例题:
路由器 RO 的路由表见下表:若进入路由器 RO 的分组的目的地址为 132.19.237.5,请问该分组应该被转发到哪一个下一跳路由器()。
A.R1
B.R2
C.R3
D.R4
网络前缀为 8 位,132.19.237.5 所在目的网络为 132.0.0.0,匹配
网络前缀为 11 位,132.19.237.5 (19 的二进制为 00010011) 所在目的网络为 132.0.0.0,匹配
网络前缀为 2 位,132.19.237.5 (237 的二进制为 11101101) 所在目的网络为 132.19.236.0,不匹配
故,选择 B
例题:
某网络的 IP 地址空间为 192.168.5.0/24,采用定长子网划分,子网掩码为 255.255.255.248,则该网络中的最大子网个数、每个子网内的最大可分配地址个数分别是()。
A.32,8
B.32,6
C.8,32
D.8,30
248 的二进制为 11111000,故后三位为主机号, 2 3 = 8 2^{3}=8 23=8,由于全 0 和全 1 不可分配,故选 B。选项不是很好,应该有个 30 6,注意 CIDR 的子网是全可以用的,不用管全 0 和全 1。
实战
访问:http://www.cxtuku.com/search.php?q=lady_killer9
- 版本:0100,4,表示 IPv4
- 首部长度:0101,5,单位 4B,故首部长度 20B
- 总长度:02f5,757,总长度 757B
- 标志,片偏移:4000,二进制为 010000…,DF = 1,MF = 0,禁止分片,后面没有分片,片偏移为 0
- TTL:40 即 64s。
- 协议:06,即 TCP
- 校验和:0000
- 源 IP:c0a81fa4,二进制为 11000000 10101000 00011111 10100100,点分十进制为 192.168.31.164
- 目的 IP:65252bf9,步骤同上,IP 见图
参考
《计算机网络第七版 谢希仁》 第 4.2 - 4.3 节
via:
-
TCP/IP 协议 (图解 + 秒懂 + 史上最全)_图解 tcpip-CSDN 博客 退休的汤姆 于 2021-07-01 16:14:25 发布
https://blog.csdn.net/stone_tmp/article/details/118386484 -
网络 - TCP 协议详解自学笔记(例题、代码、实战)_tcp报文例题-CSDN博客 lady_killer9 于 2020 - 11 - 30 16:02:19 发布
https://blog.csdn.net/lady_killer9/article/details/109587532 -
网络 - IP 协议详解(报文格式、分类、NAT、子网、CIDR、抓包分析)_nat 报文 - CSDN 博客 lady_killer9 于 2021-02-01 17:34:11 发布
https://blog.csdn.net/lady_killer9/article/details/113482131