目录
🙋♂️ 图解 数据链路:使用交换机解决mac 地址映射问题
这时如果 a 给 f 发送一个数据包,能不能通呢?如果通的话整个过程是怎样的呢?
(四)确认序号(acknowledgment number)
问题(1):为什么关闭连接的需要四次挥手,而建立连接却只要三次握手呢?
问题(2):为什么连接建立的时候是三次握手,可以改成两次握手吗?
问题(3):为什么主动断开方在time-wait状态必须等待2msl的时间?
问题(4):如果已经建立了连接,但是client端突然出现故障了怎么办?
tcp/ip协议包含了一系列的协议,也叫tcp/ip协议族(tcp/ip protocol suite,或tcp/ip protocols),简称tcp/ip。tcp/ip协议族提供了点对点的连结机制,并且将传输数据帧的封装、寻址、传输、路由以及接收方式,都予以标准化。
相关资料参考:面试高频—tcp/ip十大问题—程序员必备基础素养
相关资料参考:通俗易懂tcp/ip协议 | |
在展开介绍tcp/ip协议之前,首先介绍一下七层iso模型。国际标准化组织iso为了使网络应用更为普及,推出了osi参考模型,即开放式系统互联(open system interconnect)模型,一般都叫osi参考模型。osi参考模型是iso组织在1985年发布的网络互连模型,其含义就是为所有公司使用一个统一的规范来控制网络,这样所有公司遵循相同的通信规范,网络就能互联互通了。
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协议的应用层的主要协议有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等。
你是一台电脑,你的名字叫 a
很久很久之前,你不与任何其他电脑相连接,孤苦伶仃。
直到有一天,你希望与另一台电脑 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 地址 | 端口 |
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)的映射全部记录在表中。
最终,两个交换机将分别记录 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 台电脑的时候还好,甚至在只有几百台电脑的时候,都还好,所以这种交换机的设计方式,已经足足支撑一阵子了。
但很遗憾,人是贪婪的动物,很快,电脑的数量就发展到几千、几万、几十万。
交换机已经无法记录如此庞大的映射关系了。
此时你动了歪脑筋,你发现了问题的根本在于,连出去的那根红色的网线,后面不知道有多少个设备不断地连接进来,从而使得地址表越来越大。
那我可不可以让那根红色的网线,接入一个新的设备,这个设备就跟电脑一样有自己独立的 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-?开头的,统统先发送给路由器。
这样是否可行呢?答案是否定的。
我们先从现实中 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 地址。
仅此而已!
路由器如何知道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 |
思考一分钟...
详细过程动画描述:
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 地址就是自己,于是收下了这个包
更详细且精准的过程:
读到这相信大家已经很累了,理解上述过程基本上网络层以下的部分主流程就基本疏通了,如果你想要本过程更为专业的过程描述,可以在公众号"低并发编程"后台回复"网络",获得我模拟这个过程的 cisco packet tracer 源文件。
每一步包的传输都会有各层的原始数据,以及专业的过程描述
同时在此基础之上你也可以设计自己的网络拓扑结构,进行各种实验,来加深网络传输过程的理解。
至此,经过物理层、数据链路层、网络层这前三层的协议,以及根据这些协议设计的各种网络设备(网线、集线器、交换机、路由器),理论上只要拥有对方的 ip 地址,就已经将地球上任意位置的两个节点连通了。
相关参考资料:
利用tcp/ip进行网络通信时,数据包会按照分层顺序与对方进行通信。发送端从应用层往下走,接收端从链路层往上走。从客户端到服务器的数据,每一帧数据的传输的顺序都为:应用层->运输层->网络层->链路层->链路层->网络层->运输层->应用层。
以一个http请求的传输为例,请求从http客户端(如浏览器)和http服务端应用的传输过程,大致如下图所示:

接下来,为大家介绍一下数据封装和分用。
数据通过互联网传输的时候不可能是光秃秃的不加标识,如果这样数据就会乱。所以数据在发送的时候,需要加上特定标识,加上特定标识的过程叫做数据的封装,在数据使用的时候再去掉特定标识,去掉特定标识的过程就叫做分用。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请求的传输为例,请求在不同物理网络之间的传输过程,大致如下图所示:

数据包在不同物理网络之间的传输过程中,网络层会通过路由器去对不同的网络之间的数据包进行存储、分组转发处理。构造互连网最简单的方法是把两个或多个网络通过路由器进行连接。路由器可以简单理解为一种特殊的用于网络互连的硬件盒,其作用是为不同类型的物理网络提供连接:以太网、令牌环网、点对点的链接和fddi(光纤分布式数据接口)等等。
物理网络之间通过路由器进行互连,随着增加不同类型的物理网络,可能会有很多个路由器,但是对于应用层来说仍然是一样的,tcp协议栈为大家屏蔽了物理层的复杂性。总之,物理细节和差异性的隐藏,使得互联网tcp/ip传输的功能变得非常强大。
接下来,开始为大家介绍与传输性能有密切关系的内容:tcp传输层的三次握手建立连接,四次挥手释放连接。不过在此之前,还得先介绍一下tcp报文协议。
在tcp/ip协议栈中,ip协议层只关心如何使数据能够跨越本地网络边界的问题,而不关心数据如何传输。整体tcp/ip协议栈,共同配合一起解决数据如何通过许许多多个点对点通路,顺利传输到达目的地。一个点对点通路被称为一“跳”(hop),通过tcp/ip协议栈,网络成员能够在许多“跳”的基础上建立相互的数据通路。
传输层tcp协议提供了一种面向连接的、可靠的字节流服务,其数据帧格式,大致如下图所示:

一个传输层tcp协议的数据帧,大致包含以下字段:
源端口号表示报文的发送端口,占16位。源端口和源ip地址组合起来,可以标识报文的发送地址。
目的端口号表示报文的接收端口,占16位。目的端口和目的ip地址相结合,可以标识报文的接收地址。
tcp协议是基于ip协议的基础上传输的,tcp报文中的源端口号+源ip,与tcp报文中的目的端口号+目的ip一起,组合起来唯一性的确定一条tcp连接。
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)标识了报文接收端期望接收的字节序列。如果设置了ack控制位,确认序号的值表示一个准备接收的包的序列码,注意,它所指向的是准备接收的包,也就是下一个期望接收的包的序列码。
举个例子,假设发送端(如client)发送3个净荷为1000byte、起始sn序号为1的数据包给server服务端,server每收到一个包之后,需要回复一个ack响应确认数据包给client。ack响应数据包的ack number值,为每个client包的为sn+包净荷,既表示server已经确认收到的字节数,还表示期望接收到的下一个client发送包的sn序号,具体的ack值如下图左边的正常传输部分所示。

在上图的左边部分,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位,作为保留字段,暂时没有什么用处。
控制标志(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连接的建立时,双方需要经过三次握手,而断开连接时,双方需要经过四次分手,那么,其三次握手和四次分手分别做了什么呢?又是如何进行的呢?
通常情况下,建立连接的双方,由一端打开一个监听套接字(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
serversocket serversocket = new serversocket(8080);
//循环监听等待客户端的连接
while(true){
//监听到客户端连接,传输套接字被动开启
socket socket = serversocket.accept();
//开启线程进行连接的io处理
serverthread thread = new serverthread(socket);
thread.start();
......
}
} catch (exception e) {
// 处理异常
e.printstacktrace();
}
}
}
客户端在发起连接建立时,java代码通过创建socket实例,调用底层的connect(…)方法,主动打开(active open)socket连接。套接字监听方在收到请求之后,监听方和发起方(客户端)之间就会建立一条的连接通道,该通道由双方ip和双方端口所唯一确定。
一段简单的客户端连接主动打开(active open)的java示例代码,具体如下:
public class socketclient {
public static void main(string[] args) throws interruptedexception {
try {
// 和服务器创建连接
socket socket = new socket("localhost",8080);
// 写入给监听方的输出流
outputstream os = socket.getoutputstream();
…..
// 读取监听方的输入流
inputstream is = socket.getinputstream();
…..
} 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的全双工连接建立完成。
三次握手的交互过程,具体如下图所示:

client和server完成了三次握手后,双方就进入了数据传输的阶段。数据传输完成后,连接将断开,连接断开的过程需要经历四次挥手。
业务数据通信完成之后,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报文以后,最终关闭了连接,自己啥也不管了。
四次挥手图解
四次挥手的全部交互过程,具体如下图所示:

处于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的连接建立的三次握手及拆除过程的四次挥手的面试问题,是技术面试过程中的出现频率很高的重点和难点问题,常见问题大致如下:
关闭连接时,被动断开方在收到对方的fin结束请求报文时,很可能业务数据没有发送完成,并不能立即关闭连接,被动方只能先回复一个ack响应报文,告诉主动断开方:“你发的fin报文我收到了,只有等到我所有的业务报文都发送完了,我才能真正的结束,在结束之前,我会发你fin+ack报文的,你先等着”。所以,被动断开方的确认报文,需要拆开成为两步,故总体就需要四步挥手。
而在建立连接场景中,server端的应答可以稍微简单一些。当server端收到client端的syn连接请求报文后,其中ack报文表示对请求报文的应答,syn报文用来表示服务端的连接也已经同步开启了,而ack报文和syn报文之间,不会有其他报文需要发送,故而可以合二为一,可以直接发送一个syn+ack报文。所以,在建立连接时,只需要三次握手即可。
三次握手完成两个重要的功能:一是双方都做好发送数据的准备工作,而且双方都知道对方已准备好;二是双方完成初始sn序列号的协商,双方的sn序列号在握手过程中被发送和确认。
如果把三次握手改成两次握手,可能发生死锁。两次握手的话,缺失了client的二次确认ack帧,假想的tcp建立的连接时二次挥手,可以如下图所示:

在假想的tcp建立的连接时二次握手过程中,client发送server发送一个syn请求帧,server收到后发送了确认应答syn+ack帧。按照两次握手的协定,server认为连接已经成功地建立了,可以开始发送数据帧。这个过程中,如果确认应答syn+ack帧在传输中被丢失,client没有收到,client将不知道server是否已准备好,也不知道server的sn序列号,client认为连接还未建立成功,将忽略server发来的任何数据分组,会一直等待server的syn+ack确认应答帧。而server在发出的数据帧后,一直没有收到对应的ack确认后就会产生超时,重复发送同样的数据帧。这样就形成了死锁。
原因之一:主动断开方等待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这段时间内,旧连接所产生的所有数据报文,都已经从网络中消失了,从而,确保了下一个新的连接中不会出现这种旧连接请求报文。
tcp还设有一个保活计时器,client端如果出现故障,server端不能一直等下去,这样会浪费系统资源。每收到一次client客户端的数据帧后,server端都的保活计时器会复位。计时器的超时时间通常是设置为2小时,若2小时还没有收到client端的任何数据帧,server端就会发送一个探测报文段,以后每隔75秒钟发送一次。若一连发送10个探测报文仍然没反应,server端就认为client端出了故障,接着就关闭连接。如果觉得保活计时器的两个多小时的间隔太长,可以自行调整tcp连接的保活参数。
📚 参考资料
清晰认识tcp/ip 协议,图解秒懂 | 图解tcp/ip详解(史上最全)
发表评论