IP协议详解
网络层的IP协议,就是让网络,拥有将数据从A主机发送到B主机的能力。
注意:拥有此能力,并不代表每次都能成功送达!(即可靠性非100%)
而失败的时候,就需要传输层来获取发生错误的原因,并进行错误重传或者超时重传等操作。
所以,在从网络层获取到信息成功送达到对方的反馈之前,传输层需要暂时保留已经发送的数据。如果网络层发送失败了,传输层需要进行重发操作,以保证可靠性。
IP协议的报文与TCP的报文有一定程度上的相似
下图为IPV4中报文的格式(IPV6的报文和下图是不同的)
四位首部长度 * 4字节
,即最大报头长度为15*4=60
字节)60-20=40
)IP协议的报文中没有端口号,因为端口号是传输层应该解决的事情(UDP和TCP的报头中才有端口号的字段)IP层只关注如何将报文发送给目标主机。也就是两个主机之间如何正常通信的问题。
MTU相当于发快递时对包裹尺寸的限制,这个限制是不同的数据链路层对应的物理层产生的限制。
所以,数据链路层不支持过大的数据,这就需要在网络层对数据进行分片。
而网络层IP协议会自动帮我们分片,并在接收端组装。这个行为对传输层来说是不需要关注的,在四层模型中,每一层之间的功能需要进行解耦。
分片之后,只要有一个分片报文丢失,这个报文的整体就会认为丢失了(因为没有办法拼出完整的报文数据)
但这样就引出了一个问题:分片会提高丢包的概率,影响传输速率(发一次快递和发三次快递,明显三次快递丢东西的可能性更高)
对于具有可靠性机制的TCP而言,丢包问题不大,我们可以在传输层进行重传。但UDP没有可靠性,此时出现分片后丢包了就没办法找回了。
所以,网络层进行分片并不是主流!相比之下,在传输层就进行分片才是更好的选择。
在IP报头中,如下字段就是用于分片和组装操作的
* 8
得到的。因此, 除了最后一个报文之外, 其他报文的长度必须是8的整数倍 (否则报文就不连续了)此时,只要将三位标志字段中的更多分片
置为1,就代表当前报文并不是一个完整的报文,而是已经被分片后的报文。
- 更多分片为0,且分片偏移为0,代表当前报文没有进行分片;
- 更多分片为1,且分片偏移为0,代表当前是分片后报文中的第一个;
- 更多分片为0,且分片偏移不为0,代表当前是分片报文中的最后一个;
此时就需要根据十六位标识来确定当前分片属于哪一个“组”,再将当前报文和后续收到的ID相同的报文集合在一起,通过十三位片偏移来进行排序,组装成完整数据!
比如,第一个报文的起始偏移量是0,第二个是1000,第三个是2000,这时候就根据片偏移排序进行拼接就可以了。(这只是个栗子)
根据十六位标识,按照片偏移进行排序,排序后发现缺失了部分的报文,那就代表没有被收完。因为每一个报文的偏移量+该报文长度
,就是下一个报文的偏移量
!只要数据对不上,那就代表丢东西了。
而开头和结尾的报文,就能通过上面提到的根据更多分片标记位+分片偏移
来确定有没有丢。
虽然说IP网络层会自动帮我们分片,但是否分片是可以通过传输层来进行控制的
只要传输层一次交付的数据没有超过需要分片的阈值,那网络层在传输的时候就不会进行分片了!
减少分片的方式,那就是在传输层就进行一定的分片,这样能更好将丢分片报文
这件事在传输层进行处理。而不是在网络层丢包后,没办法在传输层失败并处理。
如果是TCP协议,在三次握手的时候,就会协商双方单词传输数据的大小。从而避免网络层对数据进行分片,以规避数据链路层的MTU限制。同时也维护了滑动窗口,如果网络层的传输出现了丢包,由传输层来进行重传操作,以实现可靠传输。
一般建议将该大小设置为比网络中的最小MTU值小一些,以防止出现分片
后续会补上更多信息
在上文中提到,MTU的限制是最大1500字节,这个数据长度是包含IP协议的报头的(数据是从IP网络层向下交付给数据链路层的)
假设我们有一个网络层的3000字节的数据,此时网络层要进行分片,并不是简单的3000/1500=2
就能搞定了的。而是要计算上IP报头的长度(20字节)
每一个分片都是一个独立的IP报文,都会有自己独立的IP报头!否则缺少报头,在接收端没有办法进行数据组装操作。
20+1480
20+1480
20+20
一共需要分3片,才能将这网络层的3000字节的数据成功传输!
但是这里就有一个问题了,明明1480+1480+20 = 2980
,并不是3000字节啊?
注意!上面提到的是网络层的3000字节数据。实际上,传输层只向下交付了2980字节,加上IP报头20字节才是网络层的3000字节数据。因为要进行分片,原本这3000字节的统一报头肯定是要丢弃的,我们需要操作的是传输层向下交付的2980
字节数据,将其正确分片并添加上每一个分片的报头,再交给数据链路层。
上文提到TTL是用来控制报文的生命周期的,其为了避免报文在路由中出现死循环。
比如下图中,假设有一个报文路由到了路由器D,原本他应该被正常交付给主机C,但路由器D出现了一些问题,将这个报文交付给了路由器I,路由器I给J,J给H,H给C,路由器C又给D,路由器D还是有bug,又转发给了路由器I。
这时候,就出现了一个报文路由的死循环。
如果没有TTL来控制生命周期,报文就会一直在这个死循环中跑,白白浪费路由器的性能!
规定了TTL之后,当报文的生命周期已经到了,但却还没有发送到目的地,那就需要将这个报文丢弃了。(即超了TTL的时间就认为报文无效)
IP如何找到对方主机呢?
以IPV4的ip为例,其格式为1.1.1.1
,可以认为是下面的划分
1.1.1 .1
网段 .主机
这就好比你的学号,前X位里面是学院的代码,最后才是班级+班级内编号。先找到你所在学院,再找到班级,最后再找到你。
反馈到IP里面,就是先找到网段,再找到主机。
IP就是先找到目的的网段,再找这个网段中的目的主机。(先根据目的网段进行路由,找到目的网络,再通过主机号找到目的主机)
而查找目的主机的过程,本质是一个排除的过程。
先通过网段排除一个大类,再通过主机号来排除该网段中的单个主机。这样就能避免我们一个一个遍历在全网中查找主机,提高了查找的效率
子网划分的目的:就是提高查找目标主机的效率
这也是学校里面用学号的原因,除了为了给每个学生提供一个唯一标识,还能通过学号来提高查找到某一个学生的效率。
在全球互联网上,同样是通过IP地址的网段来划分国家,再划分到每个国家内部的不同区。这时候就会有一定IP地址资源的竞争。比如米国互联网发展早,下图中谷歌的服务器IP就老整齐了(不过这些服务器都在同一个地域,IP很接近是合理的)
具体框架可以查看下图,192.168.128
就是这个局域网的网段 ,而最后的10和11是不同主机的两个主机标识。一般情况下,网段中的1
号主机就是这个网段中的路由器。
我们家里的路由器除了进行路由转发,还有子网划分的功能。
如果出现了一个开头并非192.168.128
的IP,主机就能知道这不是当前局域网的IP,于是就会将报文直接转发给192.168.128.1
,让路由器去找这个IP的目标主机(进行跨局域网的下一层转发)
就好比我们的学号是学校的教务系统派发给每一位同学的,IP中的网段也被“某人”在一定程度上根据地区进行了划分。
通过A到E类不同的划分,会衍生出不同范围的IP号,然后再分配给不同的国家或地区
A类 0.0.0.0到127.255.255.255
B类 128.0.0.0到191.255.255.255
C类 192.0.0.0到223.255.255.255
D类 224.0.0.0到239.255.255.255
E类 240.0.0.0到247.255.255.255
依照上面的划分,如果我是一个大型企业,申请了一个B类的IP地址。此时就能支持我的局域网内2^16
台主机的ip分配。但实际上我顶多会有2w个主机,此时就出现了IP地址的浪费!
为了避免上文中出现的IP浪费问题,CIDR(Classless Interdomain Routing)就出现了
0
来结尾,一串 1
开头;如果我们需要更多主机,就可以将子网掩码中最后一个1置0,就能适配更多局域网主机。
所以,现在已经不用ABCDE
的类别划分方式了,都采用了子网掩码方式。
根据上图可见,IP地址与子网掩码做与运算可以得到网络号,主机号的二进制位从全0到全1就是子网的地址范围;
IP地址和子网掩码还有一种更简洁的表示方法:例如140.252.20.68/24
,表示IP地址为140.252.20.68
, 子网掩码的高24位是1,也就是子网掩码255.255.255.0
有了网段划分,给不同国家和地区划分了IP之后,就需要有人来建设网络的基础设施
在我们国家,搞基础设施就是三大运营商(移动 电信 联通)
比如几年前做的“光纤入户”就是基础设施建设的一部分。
基础设施搭建好了后,再通过子网掩码和已经获取到的IP的网段来划分不同省份、不同市区;最终再落到每个入网用户的头上。
有人肯定会问了,现在公网IPV4的资源那么匮乏,大部分家宽都是没有公网ip的,那我们平时的上网是怎么实现的?
换句话说,如何缓解IP地址的匮乏?
动态IP分配
技术,只给直接接入公网的设备分配IP地址,每一个设备接入网络时,其获取到的IP地址不一定和上次相同。IPV6使用16字节(128位)来标识一个IP地址,目前看来,2^128
位能给“地球上每一粒沙子”都分配一个IP地址了。虽然迟早也会有不够用的那一天,但至少是一个很不错的解决方案。我们国家就在大力推广IPV6
通过前文的报文结构图可知,IPV4的IP地址只有4字节(32位)
但是,直接采用CIDR的方式作为局域网控制的方式,就容易出现混乱。而且即便是采用了子网掩码的方式,依旧可能出现IP不够用的情况(一位网民很可能有多个设备, 再加上各类智能终端,现在需要上网的设备只会越来越多)
需要注意的是CIDR只能提高IP地址的利用率,并不能提高IP地址的上限。
所以,就衍生出了部分特殊的IP地址。这些IP地址被规定只能用于局域网,由此来减少对公网IP的地址消耗。
127.*
的IP地址用于本机回环。我们通常使用127.0.0.1
来进行本地服务的访问和测试,该IP地址是IPv4回环地址的标准规定,IPv6的回环地址为::1
。我们在计算一个局域网中有多少设备的时候,需要减掉上文提到的网络号和广播地址。
以下是特殊的只能用于局域网的私有IP地址,包含在这个范围中的, 都称为私有IP, 其余的则称为全局IP (或公网IP)
10.*
, 前8位是网络号,共16,777,216
个地址172.16.
到172.31.
,前12位是网络号,共1,048,576
个地址192.168.*
,前16位是网络号,共65,536
个地址这里要记住2的16次方为65536
,在网络的知识点里面经常会接触到这个数字。
上文提到了127.*
是用于本地环回的。下图是环回驱动程序针对IP地址的判断
在云服务器上执行ifconfig
,也能看到本地环回的配置项;这里我们能发现,本地环回的MTU是远高于网络的1500。毕竟是自己和自己通信,基本不会出现丢包,传输速度也是飞快,也就不用担心数据包太大的问题
同时也能发现,我们的云服务器被分配到的ip地址并不是云服务器的公网ip,这也就表明了我们的云服务器并没有被直接暴露在公网上,而是通过了云服务器厂家的入网服务器(或者也叫路由器)来进行公网ip的映射和数据包的转发操作。
eth0: flags=4163 mtu 1500
inet 10.0.12.2 netmask 255.255.252.0 broadcast 10.0.15.255
inet6 fe80::5054:ff:fec9:274f prefixlen 64 scopeid 0x20
ether 52:54:00:c9:27:4f txqueuelen 1000 (Ethernet)
RX packets 277674393 bytes 80031748700 (74.5 GiB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 302405663 bytes 162670581730 (151.4 GiB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
lo: flags=73 mtu 65536
inet 127.0.0.1 netmask 255.0.0.0
inet6 ::1 prefixlen 128 scopeid 0x10
loop txqueuelen 1000 (Local Loopback)
RX packets 111135687 bytes 27644436547 (25.7 GiB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 111135687 bytes 27644436547 (25.7 GiB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
上图中的环回驱动程序会直接和IP协议的接收端(即IP输入函数)相连,当检测到 127.0.0.1
的IP访问请求的时候,会直接把这个报文转发给IP输入函数,而不将其插入到以太网中。
就相当于你知道 127.0.0.1
这个IP地址代表的就是你自己,你想访问自己电脑上8080端口的程序,即便么有接入互联网的状况下也是能正常访问的!
Loopback环回接口对于测试和诊断本地主机上的网络服务和应用程序非常有用,因为它可以模拟网络通信而不涉及实际的网络传输。
ARP是一个在局域网数据链路层通过IP获取到局域网主机MAC地址的协议,具体请参考后文中的解析
下图中能看到我们家用主机是怎么来进行广域网的访问的基本流程;
我们家里的路由器除了进行路由转发,还有子网划分的功能。可以看到左下角虽然是两个不同的家庭,但其可以分配出完全一样的局域网的子网ip 192.168.1.1
,和不同的WAN口IP(WAN口就是路由器连接互联网的口)
这是因为我们的设备是直接和当前路由器相连的,访问的时候也只能通过当前路由器来进行局域网IP的转发。不可能会出现我访问一个局域网IP,却跑到了别人家里的设备上的情况。因为这个局域网IP访问的报文并不会被转发到上层路由器上,也就不可能凭空飞到其他局域网中
图中的 122.77.241.3
就是一个公网IP的服务器,当我们需要访问这个主机的时候,局域网的家用路由器在检测到这个目的IP的时候,发现其并不是局域网的IP地址,于是就会将这个报文给转发给上层的运营商路由器。
运营商路由器是直接接入了公网IP的,其就能通过网段划分+主机编号
来查找目标主机,将报文转发给 122.77.241.3
服务器,再将服务器返回的信息转发给你的家用路由器,再转发到你的主机上。
这也告诉我们,想绕过运营商直接获得公网IP是不可能的,因为从物理层面上,我们的家用路由器就不是接在公网IP上的!即便是可以申请到的家用公网IP,也和云服务器的入网服务器一样,是运营商的路由器分配给你的。
你会发现,家用申请的公网ip,很多端口都是被屏蔽的(比如
80/443/8080
)这些端口的屏蔽操作,以及海外网站的「墙」也是运营商的路由器进行检测和屏蔽的!
假设我们的模型如下
广域网
↓
运营商路由器C (公网IP是122.77.241.4)
↓
家用路由器B (运营商路由器分配私有IP 10.1.1.2)
↓
家用主机A (家用路由器分配私有IP 192.168.1.201)
下面是一个家用主机A,访问公网IP的主机 122.77.241.10
的具体步骤;
本地主机A发送IP报文给家庭路由器B
————————————————————————
| 源IP:192.168.1.201 |
| 目的IP:122.77.241.10 |
————————————————————————
路由器B收到报文后,检测目的IP,发现并不似局域网的IP
于是交付给上层的运营商路由器C;
报文的源IP被修改为家用路由器B的wan口IP
————————————————————————
| 源IP:10.1.1.2 |
| 目的IP:122.77.241.10 |
————————————————————————
运营商路由器C收到报文后,发现其也不是自己所在内网 10.1.1.* 的局域网IP
于是开始执行广域网IP寻址操作,找到目标公网IP的主机,再将报文发送给该主机;
此时发送的报文又被改成了
————————————————————————
| 源IP:122.77.241.4 |
| 目的IP:122.77.241.10 |
————————————————————————
这种不断替换源IP来进行路由转发的过程,就是NAT技术!
也正是NAT技术的存在,让我们能通过多级局域网来让更多的设备上网,大大缓解了公网IP的不足。
也正是因为IPV4地址不足的问题被大大缓解,推广IPV6就没有我们想象中的那么顺利了。毕竟IPV6的IP格式和V4完全不同,需要每个层级的路由器都进行功能升级,这可是一个巨大的工程!
当目标主机收到这个报文后,他的反馈报文如下
————————————————————————
| 源IP:122.77.241.10 |
| 目的IP:122.77.241.4 |
————————————————————————
同样是先到达运营商路由器,运营商路由器需要缓存每一个转发到公网的报文的来源信息;为此路由器会维护一个转换表,记录着局域网主机的私有IP地址:端口号
与对应的公网IP地址:NAT端口号
的映射关系。
比如此次TCP链接中,我将路由器公网IP的122.77.241.4:30000
映射给了局域网10.1.1.2:40000
;当从公网收到服务器的响应报文后,从转换表里面就能够查到这个映射(一次通信中这个NAT映射是不会变的)从而确定该报文的局域网流向。
需要注意的是,NAT技术在端口映射的时候不一定会映射到和内网主机相同的端口,此时不仅需要修改IP报文中的来源IP,还需要进一步修改传输层(比如TCP和UDP)中的源端口号
确定局域网IP后,就修改当前报文的目的IP,继续往局域网转发;后面的子路由器也是如此,不再赘述。
————————————————————————
| 源IP:122.77.241.10 |
| 目的IP:10.1.1.2 |
————————————————————————
这种IP:端口
的关联关系表,就是由支持NAT技术的路由器来维护的,这个转换表被称为NAPT;当这次链接结束后,这对映射关系就会从转换表中被删除。
代理服务器看上去和NAT设备有一定类似,客户端向代理服务器发送请求,代理服务器将请求转发给真正需要请求的服务器;服务器返回结果后,代理服务器把结果传回客户端。
代理服务器应用相对来说也比较广
代理服务器分为正向代理和反向代理,这里来说说反向代理,反向代理处于目标服务器和客户端之间,客户端通过反向代理访问目标服务器,而不会直接连接到目标服务器
反向代理的作用
总之好处多多!
正向代理是位于客户端和目标服务器之间的中间服务器。客户端通过正向代理来访问互联网上的资源,而不是直接连接到目标服务器。正向代理的作用主要有以下几点:
下文中的部分内容来自chatgpt,我对里面的内容进行了补充和修改
运营商的路由器还会检测我们的账户是否还有余额。我们的家用路由器一般是通过光猫登录了自己的宽带账户;也可以将光猫设置成桥接模式,将接入光猫的路由器设置为宽带帐号(PPPoE)上网方式,登录运营商提供的宽带账户和密码,来接入网络。
ISP的路由器检测你的宽带账户通常是通过以下步骤来完成的:
10.11.1.0/16
或10.11.1.0/24
等。此时,你的设备与路由器建立了一个大局域网内的连接。(运营商并不会直接给你分配192.168.*
这样的私有IP,因为这个IP一般是用于最底层局域网的,给你分配了,那家庭局域网的就没IP用了)这样,通过认证和IP地址分配的过程,ISP的路由器可以检测并识别你的宽带账户,从而让你的设备能够访问互联网。
人话就是,登录了宽带账户以后,当我们路由器发送的报文交付到运营商路由器的时候,该路由器就会检测你这个宽带账户的余额。如果没有余额了,就会直接丢弃掉你的IP报文。我们看到的结果就是无法上网!
这个操作并不是每次都会执行的,只要你的路由器能稳定的接入到运营商的路由器上,那就不会每次都进行宽带账户的验证,否则会增加网络的负担。
具体的验证流程都是运营商路由器和你的本地光猫自动完成的。
而手机没有话费余额的时候,我们依旧能拨通诸如120、119等紧急号码,这也是运营商的服务对这些特殊的电话号码做了类似于免费白名单
的操作(手机号码可以类比公网IP来理解)
上文讲述的都是关于IP协议的事情。但实际上我们日常生活中,一般都不会直接使用IP地址+端口号的方式来访问某一个服务,而是使用域名来访问。比如
www.baidu.com
www.google.com
所谓域名,就是这些英文字符串和IP的映射。
比如baidu.com
就是一个域名,而www.baidu.com
是该域名下的三级域名(几级域名可以看有几个点)
实际访问的时候,是百度在域名注册商哪里,将www.baidu.com
指向了自己的服务器的地址(假设指向的是1.1.1.1
) 我们访问百度,实际上访问的就是 IP地址 1.1.1.1
;
在主机本地,有一个hosts
文件,也可以用于设置主机到域名的映射,在linux里面就是/etc/hosts
这个文件。 在访问域名的时候,操作系统会先检查自己本机器的hosts,如果本地没有,就请求DNS服务器来获取解析结果。
当我们访问一个域名的时候,首先会去请求特殊的DNS服务器
8.8.8.8 谷歌公司的DNS服务器
119.29.29.29 腾讯提供的公共DNS
先请求这些DNS服务器,服务器内会针对域名查询对应的DNS解析,最后再访问该解析对应的IP地址
而使用域名的时候,默认访问的是该主机的 80(HTTP)/443(HTTPS)
端口 ,我们也可以像IP一样,在域名之后用:端口
来指定特定端口号进行访问,即域名:端口
。
一般情况下,我们的主机都可以通过自动配置DNS从上层路由器中获取到DNS服务器的地址(比如运营商会在路由器基站中内置DNS服务器)
所谓DNS污染和DNS劫持,就是因为我们访问的DNS服务器的时候,获取到的结果和预期不同,从而导致无法访问目标网站,或者访问了假的目标网站
www.baidu.com 明明应该指向 1.1.1.1
但被坏蛋劫持了DNS解析,变成了指向 1.1.1.3
坏蛋可以在 1.1.1.3
服务器上,搭建一个和百度「看起来」一模一样的页面,并将你的报文给转发到百度服务器上。此时他就通过中间转发,获取到了你报文中的用户信息、密码等等参数;
这时候因为这个假的服务器是直接给你提供服务了,使用的HTTPS证书也是这个假服务器的证书,对方可以直接通过自己的证书解密获取到你的信息,再转发给百度。
对于大公司而言,DNS解析还有一个重要的作用,就是通过不同区域的设置来实现负载的均衡。
假设百度在每一个省份都设立了一个自己的机房,那么它就可以通过DNS服务器,当不同省份的用户请求服务器的时候,返回他当前所处省份的机房地址。这时候就实现了每个机房的负载均衡。
在现实中,就是将你的请求转接到离你最近的拥有机房的省份,这样既能保证所有服务器的负载均衡,又能保证你的访问能较快地获取到响应(广东访问广州的服务器,肯定比访问北京服务器的延迟低一些)
DNS服务器不会存放所有已知域名的IP解析,因为互联网上存在大量的域名,数量庞大且不断增长,单一DNS服务器无法存储和处理所有域名的IP映射。
实际上,DNS服务器通过分层的架构来解决这个问题。在根DNS服务器层级,有一组全球性的顶级DNS服务器,它们存储顶级域名(例如.com、.org、.net
等)的IP地址。然后,在每个顶级域名下,有其他DNS服务器,负责管理该顶级域名下的子域名(例如,google.com、facebook.com
等)。这个过程继续向下,形成了一个层级结构。
当你的设备需要解析某个域名时,它首先会向本地DNS服务器(通常由你的ISP,即互联网服务提供商,人话就是国内的三大运营商提供)发起请求。如果本地DNS服务器知道所需域名的IP地址,它会直接返回该IP地址给你的设备。但如果本地DNS服务器不知道该域名的IP地址,它会向根DNS服务器发起请求。
根DNS服务器将指导本地DNS服务器转向相应的顶级DNS服务器。然后,顶级DNS服务器可能会进一步将请求转发给负责该特定域名的授权DNS服务器。最终,这个授权DNS服务器将返回所需域名的IP地址给本地DNS服务器,本地DNS服务器再将其返回给你的设备。
这个层级结构允许DNS系统更高效地处理大量的域名解析请求,并确保及时更新和管理域名与IP地址的映射。所以,单个DNS服务器并不会存放所有已知域名的IP解析,而是通过层级结构来分散和管理这些信息。
yum install bind-utils
安装了之后就可以使用dig命令来查看域名解析过程了
dig 域名
路由的过程,就是下图这样一跳一跳(Hop by Hop) “问路” 的过程
所谓 “一跳” 就是数据链路层中的一个区间。具体在以太网中,指从源MAC地址到目的MAC地址之间的帧传输区间。
拿日常生活中问路来举例子(请屏蔽现在有导航这件事)一般问路,会得到三种结果:
比如张三要去南京大学的仙林校区,他飞机落地南京后,不知道怎么走;他找了个机场的保安,问他“我是从xx省新来的大学生,应该怎么去南京大学?” 保安让他先坐xx路公交车去仙林大学城,到哪里再去问其他人。
张三到了仙林大学城后,又找到了路边的环卫工,又问“我从机场过来,应该怎么去南京大学。” 环卫工给他指了条明路,那里就是南京大学的教学楼,入口就在这附近。这时候张三获取了两个人(路由器)的帮助,成功递达了南京大学的仙林校区(目标主机)。
可以看到,问路和上面图示中IP报文的路由是很相似的,都是一个路由一个路由的“问路”,最终找到目标主机。
报文在各个路由器之间路由也是如此。当一个路由器遇到一个IP报文
默认路由
,报文继续转发,去下一个人那里问路了。这样的跳跃都会有一个前提条件:相邻的两个主机(或路由器)一定是在物理上相连,处于同一局域网之中。每次的跳跃,本质上是从一个子网跳到另外一个子网;广域网可以认为是最大的“子网”
在windows电脑上可以使用
route print
命令打印路由表,本文不关注windows平台。
在linux平台里面,可以通过route
命令查看当前主机的路由表,在Centos8主机上,命令显示如下。
可以看到,路由表的名字叫做 内核IP路由表,这也是linux系统中的一个内核数据结构。内部维护了路由表的目的地、Gateway网关、Genmask掩码、Flags标志位、Metric、Ref、Use、Iface接口
Kernel IP routing table
Destination Gateway Genmask Flags Metric Ref Use Iface
default _gateway 0.0.0.0 UG 100 0 0 eth0
172.16.0.0 0.0.0.0 255.255.0.0 U 100 0 0 eth0
172.17.0.0 0.0.0.0 255.255.0.0 U 0 0 0 docker0
172.18.0.0 0.0.0.0 255.255.0.0 U 0 0 0 br-96d69eeef1ac
172.19.0.0 0.0.0.0 255.255.0.0 U 0 0 0 br-fa4aff4c583e
_gateway
以及0.0.0.0
代表的都是默认网关eth0
是一个真正的物理网络接口,而docker0/br-*
都是docker容器虚拟出来的桥接网络接口假设我我们有一个目的IP是 172.16.0.2
,获取到这个IP后,系统就会将其和路由表中的子网掩码依次进行按位与;
在linux环境下,可以使用如下代码来进行这两个IP的按位与操作。其中inet_addr 和 inet_ntoa
是linux下的两个系统调用接口。用于IP字符串到无符号整数之间的转换,具体的介绍可以阅读我的 UDP博客
#include
#include
int main() {
// 将点分十进制的IP地址和子网掩码转换为无符号整数
unsigned int ipAddress = inet_addr("172.16.0.2");
unsigned int subnetMask = inet_addr("255.255.0.0");
// 进行按位与操作得到网络地址
unsigned int networkAddress = ipAddress & subnetMask;
// 将网络地址转换回点分十进制表示法并输出结果
struct in_addr addr;
addr.s_addr = networkAddress;
std::cout << "IP地址: " << "172.16.0.2" << std::endl;
std::cout << "子网掩码: " << "255.255.0.0" << std::endl;
std::cout << "网络地址: " << inet_ntoa(addr) << std::endl;
return 0;
}
// 非linux环境可以使用下方的代码进行处理
void ip_and_gmask_test() {
unsigned int ipAddress = 172 << 24 | 16 << 16 | 0 << 8 | 2; // 将 IP 地址转换为 32 位无符号整数
unsigned int subnetMask = 255 << 24 | 255 << 16 | 0 << 8 | 0; // 将子网掩码转换为 32 位无符号整数
unsigned int networkAddress = ipAddress & subnetMask; // 按位与
std::cout << "IP地址: 172.16.0.2" << std::endl;
std::cout << "子网掩码: 255.255.0.0" << std::endl;
std::cout << "网络地址: " << (networkAddress >> 24) << "." << ((networkAddress >> 16) & 255) << "." << ((networkAddress >> 8) & 255) << "." << (networkAddress & 255) << std::endl;
}
二者输出结果都是172.16.0.0
得到输出结果后,再和路由表中该项的Destination
进行对比,二者相等,代表当前IP就是需要通过该项进行路由,那就将这个报文通过Iface
接口eth0
发送出去就OK了
Destination Gateway Genmask Flags Metric Ref Use Iface
172.16.0.0 0.0.0.0 255.255.0.0 U 100 0 0 eth0
如果按位与的结果与Destination
匹配不上,那就继续往下一个条目的子网掩码进行按位与。如果整个表都按位与完毕了,还没找到该去的地方,那就将其通过default
默认路由发送出去。
前面谈了一大堆将数据从一个主机到路由器再跨越多个路由器递达目标主机的流程。
要想实现这一点,我们还需要数据链路层的帮助,即实现同一局域网内两台主机在物理层面上的相互通信。
数据链路层也有不同的传输方式,本文主要关注当前主流的以太网;
需要注意的是,以太网不是一种具体的网络,而是一种技术标准。它即包含了数据链路层的内容,也包含了一些物理层的规定,比如拓扑结构和访问控制方式,传输速率等。
不过在认识以太网之前,我们需要先知道局域网通信的一个基本情况;假设下图中就是一个局域网,其中包含了不同的主机
首先,如果想要两台主机能够通信,我们就需要先将其链接到同一根网线上(在这里暂且不管WIFI,其实本质上也是连在了这根网线上);这就好比进程间通信的时候,你需要先能看到同一份资源,才能实现对这份资源的共享访问。
虽然我们认为在局域网里面通信的时候,是两台主机直接交流,但实际上你可以把局域网当作一个教师,当张三和李四沟通的时候,其实会有很多其他的吃瓜群众都能接收到你发送的这个信息。
反馈到以太网报文上,因为每个主机都是知道自己的地址的,所以只要检测到以太网报头中的目的地址
不是自己的时候,就可以丢弃掉这个报文。因为目标并不在和你说话。这就好比在教室里面,你隔壁有俩人在聊天,他们并没有和你交谈,所以你可以不用管他们聊了什么。
这也反馈出了为什么某些公共WIFI会不安全,因为只要接入了这个WIFI,你就有办法检测到其他用户通过这个局域网发送了什么信息!
因为局域网内的主机是通过目的地址判断有没有人和自己聊天的,所以即便我们的电脑开机后什么事情都没有干,在操作系统底层(数据链路层)其实一直都在从局域网中拿到新的数据链路层报文,并检测是否是发给自己的报文:
由于数据链路层向下是直接交付给物理层的,在物理层(网线)中光电信号传输是不能同时传输多个数据的,这就要求我们同一个局域网的多台主机不能同时往局域网中发数据。为了解决这个问题,主机引入了休眠机制,通过不同时间的错开休眠,来避免两台主机同时往局域网中发数据的情况。
通过碰撞域
解决数据冲突问题,尽量达到理想情况;
比如我们的交换机就有划分碰撞域的功能。接到交换机上的设备,除了通过交换机进行路由转发,如果在交换机的这部分设备中出现了数据碰撞,那么交换机就能把碰撞控制在当前这个小的碰撞域内,而不会向更大的局域网中传播。
如果一个局域网里面只有一个交换机(路由器)那么这整个局域网共享碰撞域
所以大公司内为了避免局域网因为碰撞问题而导致的网络卡顿,一般都会将几台电脑接入一个小的交换机中来划分碰撞域。
如上是物理层面的事情,软件层面上,一个MAC帧不要太大,否则会大大增加碰撞的概率。所以MAC帧必须要对上层交付的数据大小提一个要求,不能交付太大的数据,这就是MTU的由来(一般都是1500字节,至于为什么是1500,那就是学术层面的事情了)
这里需要知道一个小知识,虽然MAC地址在一定程度上可以认为是全球唯一的,但实际上只需要保证同一个局域网内的MAC地址是唯一的,就OK了
IP、ARP、RARP
,所以只需要2个字节这三个就是以太网固定添加的报头,在进行解包的时候,我们只需要取走数据最前面的14个字节(6+6+2),再丢弃末尾的4个字节,就能取到上层的原始数据。
08:00:27:03:fb:19
MAC地址和IP地址的区别如下:
因为我们的主机不可能知道一个很远的内网主机的MAC地址,所以就需要MAC地址在小路由区间来标识起点和终点,并实现正确的数据传输。
在前文提到过,为了避免光电信号在物理层传输的时候出现冲突,需要限制网络层给数据链路层传输的单次的数据大小,MTU的具体说明可以参考本文 2.2.1 认识MTU;
因为MTU的存在,网络层IP协议中需要对较大的数据包进行分包(IP分片和组装问题在上文也谈过了,这里就不重复了)
但因为IP协议层分片和组装对于传输层来说是不可见的,如果IP分片后出现丢包导致数据丢失,那么传输层就必须得重传。所以传输层为了避免这种不受自己控制的事情,最终分片的操作应该是由传输层来进行处理才是最好的;
UDP最大可以传输数据是 2^16
字节,也就是64KB
,而1500字节是1.5KB
;也就是说,只要UDP携带的数据超过 1472 (1500 - 20 IP首部 - 8 UDP首部)
,那么就会在网络层被分为多个IP数据报。
一旦这个数据报中有一个IP报文丢失了,那么整个UDP报文就会丢失。再加上UDP并没有超时重传机制(不过可以根据具体的协议来定制应答和重传机制来保证数据可靠性),UDP的报文在IP层中被分片后丢包的概率远大于TCP
MSS(Max Segment Size)
;(kind=2)
;ARP协议属于数据链路层,是MAC帧协议的上层
因为在局域网内的传输时,我们是用mac地址来作为不同主机的标识符的,所以就必须存在一个IP地址到MAC地址的转换。
ARP协议也是包含在以太网帧格式中的,其中属于他自己的正文只有28个字节;因为MTU限制最小的数据长度是46字节,所以在发送ARP报文的时候,需要给这个28字节后面填补空位。
先来看看ARP请求/应答中的各个字段的含义吧
0X0800
为IP地址op
字段为1表示ARP请求,2表示ARP应答当我们的主机开始发送报文之前,我们的主机是不知道某一个IP对于的目标主机的MAC地址的。所以就需要用ARP协议向局域网内发送一个请求,并得到目标主机的ARP响应,响应中就包含了该主机的MAC地址
主机A需要给主机B发送数据,但是不知道主机B的MAC地址,它就需要发起一个ARP请求:
0806
代表ARP协议这个ARP请求的报文就开始在局域网内进行广播
于是主机B就收到了这个ARP请求,并开始构造ARP响应
0806
代表ARP协议此时这个ARP的响应就开始在局域网中传输,因为此时以太网的目的地址不再是全F,所以各个收到这个报文的主机,就可以直接通过MAC地址来判断是否是发给自己的MAC帧。如果不是就直接丢弃,不交付给上层;
主机A判断目的MAC帧是自己的,交付给上层的ARP协议
这时候主机A就得到了主机B的MAC地址,可以正常进行数据的发送了!
但如果每台主机都不知道IP和MAC的映射关系,岂不是每次发送数据之前,都需要来个ARP请求和响应来获取对方MAC地址?这样整个局域网内就得被ARP请求和响应给塞满了。
所以,当我们发送了一个ARP请求后,应该需要将ARP响应给暂时缓存到本机上,避免下次发送的时候不知道对方的MAC地址。操作系统中就有一张ARP缓存表,保存了局域网内部分主机的IP和MAC地址的映射关系。
如果一个主机想获取到局域网内所有的MAC地址,就可以写个循环,把局域网内的所有IP都发送一次ARP请求,再将收集到的ARP响应给缓存起来(因为局域网内的主机网络号都是相同的,主机号都是是从1到254,并不多,写个循环就行了)
但这里会有一个问题:如果某台主机B离开了你这个局域网,主机C接入后,路由器给主机C分配了原本是给主机B用的IP,这时候主机A里面的ARP缓存表没有更新,还是填了主机B的MAC地址(但主机B其实已经不在局域网里面了),这时候这个报文岂不是找不到目标主机了?
所以ARP不仅仅需要缓存,还需要保有一定的更新机制:可以设置一个定时器,定时向缓存表中已有IP的主机发送一条ARP请求,并得到对方的ARP响应。这时候就可以比对返回的MAC地址是否有变动,有变动则更新。
如果一个ARP请求长时间没有得到响应,则可以认为该IP地址目前没有对应的主机,将其从缓存表中删除。
上文讲述了ARP的请求和响应的格式,假设出现了下面的这个情况:
这也是为啥出现了https来避免中间人攻击!
RARP(Reverse Address Resolution Protocol,逆地址解析协议)是一种网络协议,用于在局域网(LAN)中通过已知的物理地址查找相应的IP地址。
与前面讲述的ARP(Address Resolution Protocol,地址解析协议)不同,ARP用于通过已知的IP地址查找相应的物理地址,通常用于将网络层(IP)地址映射到链路层(MAC)地址。RARP则执行相反的操作,它允许主机在启动时使用其物理地址来请求分配给它的IP地址。
RARP协议在过去的计算机网络中用于在没有人工配置的情况下为计算机分配IP地址。当计算机启动时,它会向网络发送一个RARP请求包,其中包含它的物理地址(MAC地址),以请求分配一个IP地址。网络中的RARP服务器会接收这个请求并回复包含IP地址的RARP响应包。
然而,随着时间的推移,RARP的使用逐渐减少,主要是因为它的局限性,例如不太适用于大型网络,以及需要特定的服务器来管理地址分配。现代的网络通常使用DHCP(Dynamic Host Configuration Protocol,动态主机配置协议)来实现类似的功能,它更灵活且易于管理,可以自动分配IP地址以及其他网络配置参数给主机。