目录
《第 1 讲 | 为什么要学习网络协议?》
课后思考题
当网络包到达一个城关的时候,可以通过路由表得到下一个城关的 IP 地址,直接通过 IP 地址找就可以了,为什么还要通过本地的 MAC 地址呢?
徐良红同学说的比较接近。在网络包里,有源 IP 地址和目标 IP 地址、源 MAC 地址和目标 MAC 地址。从路由表中取得下一跳的 IP 地址后,应该把这个地址放在哪里呢?如果放在目标 IP 地址里面,到了城关,谁知道最终的目标在哪里呢?所以要用 MAC 地址。
所谓的下一跳,看起来是 IP 地址,其实是要通过 ARP 得到 MAC 地址,将下一跳的 MAC 地址放在目标 MAC 地址里面。
留言问题
1.MAC 地址可以修改吗?
我查了一下,MAC(Media Access Control,介质访问控制)地址,也叫硬件地址,长度是 48 比特(6 字节),由 16 进制的数字组成,分为前 24 位和后 24 位。
前 24 位叫作组织唯一标志符(Organizationally Unique Identifier,OUI),是由 IEEE 的注册管理机构给不同厂家分配的代码,用于区分不同的厂家。后 24 位是厂家自己分配的,称为扩展标识符。同一个厂家生产的网卡中 MAC 地址后 24 位是不同的。
也就是说,MAC 本来设计为唯一性的,但是后来设备越来越多,而且还有虚拟化的设备和网卡,有很多工具可以修改,就很难保证不冲突了。但是至少应该保持一个局域网内是唯一的。
MAC 的设计,使得即便不能保证绝对唯一,但是能保证一个局域网内出现冲突的概率很小。这样,一台机器启动的时候,就能够在没有 IP 地址的情况下,先用 MAC 地址进行通信,获得 IP 地址。
好在 MAC 地址是工作在一个局域网中的,因而即便出现了冲突,网络工程师也能够在自己的范围内很快定位并解决这个问题。这就像我们生成 UUID 或者哈希值,大部分情况下是不会冲突的,但是如果碰巧出现冲突了,采取一定的机制解决冲突就好。
2.TCP 重试有没有可能导致重复下单?
答案是不会的。这个在TCP那一节有详细的讲解。因为 TCP 层收到了重复包之后,TCP 层自己会进行去重,发给应用层、HTTP 层。还是一个唯一的下单请求,所以不会重复下单。
那什么时候会导致重复下单呢?因为网络原因或者服务端错误,导致 TCP 连接断了,这样会重新发送应用层的请求,也即 HTTP 的请求会重新发送一遍。
如果服务端设计的是无状态的,它记不住上一次已经发送了一次请求。如果处理不好,就会导致重复下单,这就需要服务端除了实现无状态,还需要根据传过来的订单号实现幂等,同一个订单只处理一次。
还会有的现象是请求被黑客拦截,发送多次,这在 HTTPS 层可以有很多种机制,例如通过 Timestamp 和 Nonce 随机数联合起来,然后做一个不可逆的签名来保证。
3.TCP 报平安的包是原路返回吗?
谢谢语鬼同学的指正。这里的比喻不够严谨,容易让读者产生误会,这里的原路返回的意思是原样返回,也就是返回也是这个过程,不一定是完全一样的路径。
4.IP 地址和 MAC 地址的关系?
芒果同学的理解非常准确,讲IP 和 MAC 的关系的时候说了这个问题。IP 是有远程定位功能的,MAC 是没有远程定位功能的,只能通过本地 ARP 的方式找到。
我个人认为,即便有了 IPv6,也不会改变当前的网络分层模式,还是 IP 层解决远程定位问题,只不过改成 IPv6 了,到了本地,还是通过 MAC。
- 如果最后一跳的时候,IP 改变了怎么办?
对于 IP 层来讲,当包到达最后一跳的时候,原来的 IP 不存在了。比如网线拔掉了,或者服务器直接宕机了,则 ARP 就找不到了,所以这个包就会发送失败了。对于 IP 层的工作就结束了。
但是 IP 层之上还有 TCP 层,TCP 会重试的,包还是会重新发送,但是如果服务器没有启动起来,超过一定的次数,最终放弃。
如果服务器重启了,IP 还是原来的 IP 地址,这个时候 TCP 重新发送的一个包的时候,ARP 是能够得到这个地址的,因而会发到这台机器上来,但是机器上面没有启动服务端监听那个端口,于是会发送 ICMP 端口不可达。
如果服务器重启了,服务端也重新启动了,也在监听那个端口了,这个时候 TCP 的服务端由于是新的,Sequence Number 根本对不上,说明不是原来的连接,会发送 RST。
那有没有可能有特殊的场景 Sequence Number 也能对的上呢?按照 Sequence Number 的生成算法,是不可能的。
但是有一个非常特殊的方式,就是虚拟机的热迁移,从一台物理机迁移到另外一台物理机,IP 不变,MAC 不变,内存也拷贝过去,Sequence Number 在内存里面也保持住了,在迁移的过程中会丢失一两个包,但是从 TCP 来看,最终还是能够连接成功的。
6.TCP 层报平安,怎么确认浏览器收到呢?
TCP 报平安,只能保证 TCP 层能够收到,不保证浏览器能够收到。但是可以想象,如果浏览器是你写的一个程序,你也是通过 socket 编程写的,你是通过 socket,建立一个 TCP 的连接,然后从这个连接里面读取数据,读取的数据就是 TCP 层确认收到的。
这个读取的动作是本地系统调用,大部分情况下不会失败的。如果读取失败呢,当然本地会报错,你的 socket 读取函数会返回错误,如果你是浏览器程序的实现者,你有两种选择,一个是将错误报告给用户,另一个是重新发送一次请求,获取结果显示给用户。
7.ARP 协议属于哪一层?
ARP 属于哪个层,一直是有争议的。比如《TCP/IP 详解》把它放在了二层和三层之间,但是既然是协议,只要大家都遵守相同的格式、流程就可以了,在实际应用的时候,不会有歧义的,唯一有歧义的是参加各种考试,让你做选择题,ARP 属于哪一层?平时工作中咱不用纠结这个。
《第 2 讲 | 网络分层的真实含义是什么?》
课后思考题
如果你也觉得总经理和员工的比喻不恰当,你有更恰当的比喻吗?
我觉得,寄快递和寄信这两个比喻都挺好的。关键是有了封装和解封装的过程。有的同学举了爬楼,或者公司各层之间的沟通,都无法体现封装和解封装的过程。
留言问题
- 为什么要分层?
是的,仅仅用复杂性来解释分层,太过牵强了。
其实这是一个架构设计的通用问题,不仅仅是网络协议的问题。一旦涉及到复杂的逻辑,或者软件需求需要经常变动,一般都会通过分层来解决问题。
假如我们将所有的代码都写在一起,但是产品经理突然想调整一下界面,这背后的业务逻辑变不变,那要不要一起修改呢?所以会拆成两层,把 UI 层从业务逻辑中分离出来,调用 API 来进行组合。API 不变,仅仅界面变,是不是就不影响后台的代码了?
为什么要把一些原子的 API 放在基础服务层呢?将数据库、缓存、搜索引擎等,屏蔽到基础服务层以下,基础服务层之上的组合逻辑层、API 层都只能调用基础服务层的 API,不能直接访问数据库。
比如我们要将 Oracle 切换成 MySQL。MySQL 有一个库,分库分表成为 4 个库。难道所有的代码都要修改吗?当然只要把基础服务层屏蔽,提供一致的接口就可以了。
网络协议也是这样的。有的想基于 TCP,自己不操心就能够保证到达;有的想自己实现可靠通信,不基于 TCP,而使用 UDP。一旦分了层就好办了,定制化后要依赖于下一层的接口,只要实现自己的逻辑就可以了。如果 TCP 的实现将所有的逻辑耦合在了整个七层,不用 TCP 的可靠传输机制都没有办法。
- 层级之间真实的调用方式是什么样的?
如果文中是一个逻辑图,这个问题其实已经到实现层面上来了,需要看 TCP/IP 的协议栈代码了。这里首先推荐一本书《深入理解 Linux 网络技术内幕》。
其实下层的协议知道上层协议的,因为在每一层的包头里面,都会有上一层是哪个协议的标识,所以不是一个回调函数,每一层的处理函数都会在操作系统启动的时候,注册到内核的一个数据结构里面,但是到某一层的时候,是通过判断到底是哪一层的哪一个协议,然后去找相应的处理函数去调用。
调用的大致过程我这里再讲一下。由于 TCP 比较复杂,我们以 UDP 为例子,其实发送的包就是一个 sk_buff 结构。这个在Socket那一节讲过。
int udp_send_skb(struct sk_buff *skb, struct flowi4 *fl4)
接着,UDP 层会调用 IP 层的函数。
int ip_send_skb(struct net *net, struct sk_buff *skb)
然后,IP 层通过路由判断,最终将包发给下一层。
int ip_output(struct net *net, struct sock *sk, struct sk_buff *skb)
发送的时候,要进行 ARP。如果有 MAC,则调用二层的函数,neigh 其实就是邻居系统,是二层的意思。
int neigh_output(struct neighbour *n, struct sk_buff *skb)
接收的时候,会调用这里的接收函数。
int netif_receive_skb(struct sk_buff *skb)
这个函数会根据是 ARP 或者 IP 等,选择调用不同的函数。如果是 IP 协议的话,就调用这里的函数。
int ip_rcv(struct sk_buff *skb, struct net_device *dev, struct packet_type *pt, struct net_device *orig_dev)
这里也有路由判断。如果是本地的,则继续往上提交这个结构。
int ip_local_deliver(struct sk_buff *skb)
接着,还是根据 IP 头里面的协议号,来判断是什么协议,从而调用什么函数。下面这个是对 UDP 的调用。
int udp_rcv(struct sk_buff *skb)
- 什么情况下会有下层没上层?
有时候我们自己写应用的时候,不一定是直接调用应用层协议的接口,例如 HTTP 等,而是自己写 Socket 编程,来约定应用层的协议。再如,ping 也是一个应用,但是它没有用传输层的协议,而是用了 ICMP 的协议。