# 协议专栏特别福利 | 答疑解惑第一期 你好,我是刘超。 首先,感谢大家关注并在留言区写下近3000条留言。留言太多,没有及时回复,一是每周写三篇文章压力真的挺大的。为了保质保量地产出,晚上和周末的时间基本上都搭进去了。二是很多人的留言非常有深度,水平很高,提的问题一两句话解释不清楚。 每一节结尾我基本都会留两个思考题,其中第一个问题是启发思考的,是对本节内容的延伸学习;第二个问题是为了引出下一节,下一节的内容其实就是答案。 所以我会回答一下每一节的第一个问题,并列出第一个同我的思路最相近的同学,并对留言中比较有代表性的问题,做一个统一的回答,顺便也实现之前要送知识图谱和奖励礼券的承诺。 当然,这并不能说明我的回答就是一定是正确的或者全面的,有很多同学的留言有非常大的信息量,甚至更广的思路,也对这些同学表示感谢。还有些同学指出了我的错误,也感谢你们。 ### [《第1讲 | 为什么要学习网络协议?》](https://time.geekbang.org/column/article/7581) ### 课后思考题 当网络包到达一个城关的时候,可以通过路由表得到下一个城关的 IP 地址,直接通过 IP地址找就可以了,为什么还要通过本地的MAC地址呢? ![](https://static001.geekbang.org/resource/image/16/69/16e0b76fe90ce10d8e5c16cad0010e69.png) 徐良红同学说的比较接近。在网络包里,有源IP地址和目标IP地址、源MAC地址和目标MAC地址。从路由表中取得下一跳的IP地址后,应该把这个地址放在哪里呢?如果放在目标IP地址里面,到了城关,谁知道最终的目标在哪里呢?所以要用MAC地址。 所谓的下一跳,看起来是IP地址,其实是要通过ARP得到MAC地址,将下一跳的MAC地址放在目标MAC地址里面。 ### 留言问题 1.MAC地址可以修改吗? ![](https://static001.geekbang.org/resource/image/b3/1b/b3591518b266a3558f9dd61061c6271b.png) ![](https://static001.geekbang.org/resource/image/3a/5d/3abde3f5607b0278cc87ccfadb88875d.png) 我查了一下,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重试有没有可能导致重复下单? ![](https://static001.geekbang.org/resource/image/ce/71/ced92fc92921a47918704f1657fb2771.png) 答案是不会的。这个在[TCP](https://time.geekbang.org/column/article/8975)那一节有详细的讲解。因为TCP层收到了重复包之后,TCP层自己会进行去重,发给应用层、HTTP层。还是一个唯一的下单请求,所以不会重复下单。 那什么时候会导致重复下单呢?因为网络原因或者服务端错误,导致TCP连接断了,这样会重新发送应用层的请求,也即HTTP的请求会重新发送一遍。 如果服务端设计的是无状态的,它记不住上一次已经发送了一次请求。如果处理不好,就会导致重复下单,这就需要服务端除了实现无状态,还需要根据传过来的订单号实现幂等,同一个订单只处理一次。 还会有的现象是请求被黑客拦截,发送多次,这在HTTPS层可以有很多种机制,例如通过 Timestamp和Nonce随机数联合起来,然后做一个不可逆的签名来保证。 3.TCP报平安的包是原路返回吗? ![](https://static001.geekbang.org/resource/image/36/ac/361fee74d932bac74f0b26bb280bf2ac.png) 谢谢语鬼同学的指正。这里的比喻不够严谨,容易让读者产生误会,这里的原路返回的意思是原样返回,也就是返回也是这个过程,不一定是完全一样的路径。 4.IP地址和MAC地址的关系? ![](https://static001.geekbang.org/resource/image/9c/e3/9c359d2c20ab3f77f5a7e87dcb6fd7e3.png) 芒果同学的理解非常准确,讲[IP和MAC的关系](https://time.geekbang.org/column/article/7772)的时候说了这个问题。IP是有远程定位功能的,MAC是没有远程定位功能的,只能通过本地ARP的方式找到。 我个人认为,即便有了IPv6,也不会改变当前的网络分层模式,还是IP层解决远程定位问题,只不过改成IPv6了,到了本地,还是通过MAC。 5.如果最后一跳的时候,IP改变了怎么办? ![](https://static001.geekbang.org/resource/image/08/c1/08029e8c3dce6413558c4819b380c5c1.png) 对于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层报平安,怎么确认浏览器收到呢? ![](https://static001.geekbang.org/resource/image/f3/48/f39ceca92d67db24b42ef9d31f9bff48.png) TCP报平安,只能保证TCP层能够收到,不保证浏览器能够收到。但是可以想象,如果浏览器是你写的一个程序,你也是通过socket编程写的,你是通过socket,建立一个TCP的连接,然后从这个连接里面读取数据,读取的数据就是TCP层确认收到的。 这个读取的动作是本地系统调用,大部分情况下不会失败的。如果读取失败呢,当然本地会报错,你的socket读取函数会返回错误,如果你是浏览器程序的实现者,你有两种选择,一个是将错误报告给用户,另一个是重新发送一次请求,获取结果显示给用户。 7.ARP协议属于哪一层? ![](https://static001.geekbang.org/resource/image/e7/9f/e77d85e7260df7611b90a45b7e77ce9f.png) ARP属于哪个层,一直是有争议的。比如《TCP/IP详解》把它放在了二层和三层之间,但是既然是协议,只要大家都遵守相同的格式、流程就可以了,在实际应用的时候,不会有歧义的,唯一有歧义的是参加各种考试,让你做选择题,ARP属于哪一层?平时工作中咱不用纠结这个。 ## [《第2讲 | 网络分层的真实含义是什么?》](https://time.geekbang.org/column/article/7724) ### 课后思考题 如果你也觉得总经理和员工的比喻不恰当,你有更恰当的比喻吗? ![](https://static001.geekbang.org/resource/image/21/27/21cd2dba241413bcfc5bb74fc8dd2527.png) ![](https://static001.geekbang.org/resource/image/3b/3e/3b69f685c893bfd6746ef03c395e8e3e.png) 我觉得,寄快递和寄信这两个比喻都挺好的。关键是有了封装和解封装的过程。有的同学举了爬楼,或者公司各层之间的沟通,都无法体现封装和解封装的过程。 ### 留言问题 1.为什么要分层? ![](https://static001.geekbang.org/resource/image/4d/92/4d6c7f391ca5a55fdde3dbf62d69ab92.png) 是的,仅仅用复杂性来解释分层,太过牵强了。 ![](https://static001.geekbang.org/resource/image/85/78/856b8040cd0a9f2206ad6f27ad3e9078.png) 其实这是一个架构设计的通用问题,不仅仅是网络协议的问题。一旦涉及到复杂的逻辑,或者软件需求需要经常变动,一般都会通过分层来解决问题。 假如我们将所有的代码都写在一起,但是产品经理突然想调整一下界面,这背后的业务逻辑变不变,那要不要一起修改呢?所以会拆成两层,把UI层从业务逻辑中分离出来,调用API来进行组合。API不变,仅仅界面变,是不是就不影响后台的代码了? 为什么要把一些原子的API放在基础服务层呢?将数据库、缓存、搜索引擎等,屏蔽到基础服务层以下,基础服务层之上的组合逻辑层、API层都只能调用基础服务层的API,不能直接访问数据库。 比如我们要将Oracle切换成MySQL。MySQL有一个库,分库分表成为4个库。难道所有的代码都要修改吗?当然只要把基础服务层屏蔽,提供一致的接口就可以了。 网络协议也是这样的。有的想基于TCP,自己不操心就能够保证到达;有的想自己实现可靠通信,不基于TCP,而使用UDP。一旦分了层就好办了,定制化后要依赖于下一层的接口,只要实现自己的逻辑就可以了。如果TCP的实现将所有的逻辑耦合在了整个七层,不用TCP的可靠传输机制都没有办法。 2.层级之间真实的调用方式是什么样的? ![](https://static001.geekbang.org/resource/image/95/57/95510f25eb463c4302fb58bd518a8957.png) 如果文中是一个逻辑图,这个问题其实已经到实现层面上来了,需要看TCP/IP的协议栈代码了。这里首先推荐一本书《深入理解Linux网络技术内幕》。 其实下层的协议知道上层协议的,因为在每一层的包头里面,都会有上一层是哪个协议的标识,所以不是一个回调函数,每一层的处理函数都会在操作系统启动的时候,注册到内核的一个数据结构里面,但是到某一层的时候,是通过判断到底是哪一层的哪一个协议,然后去找相应的处理函数去调用。 调用的大致过程我这里再讲一下。由于TCP比较复杂,我们以UDP为例子,其实发送的包就是一个sk\_buff结构。这个在[Socket](https://time.geekbang.org/column/article/9293)那一节讲过。 ``` 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) ``` 3.什么情况下会有下层没上层? ![](https://static001.geekbang.org/resource/image/f6/c3/f6414085ec89ba0dfae79436aad2b2c3.png) 有时候我们自己写应用的时候,不一定是直接调用应用层协议的接口,例如HTTP等,而是自己写Socket编程,来约定应用层的协议。再如,ping也是一个应用,但是它没有用传输层的协议,而是用了ICMP的协议。 * * * 最后,感谢留言次数前15名的同学,谢谢你们持之以恒的学习,相信你们一定有自己的收获。(统计数据截止到2018年8月8日) ![](https://static001.geekbang.org/resource/image/ca/91/caeec16c4e959e250da1db6c6543b791.jpg) 同时感谢第1讲、第2讲中对内容有深度思考和提出问题的同学。我会为你们送上奖励礼券和知识图谱。(稍后运营同学会发送短信通知。) 欢迎你继续提问! ![](https://static001.geekbang.org/resource/image/00/5b/0057c2d7c99924366c94c5ed58e3385b.jpg)