Notes
*注意*: 网络标准这种东西随着时间是在往前走的, 此书出于1993年, 彼时的网络也就是个范围大点的局域网. 故标准可能已被替代更新, 实验可能过时. 本记录乃是本着"书都买了, 不看白不看"的精神, 本人在此做下.
-----
## 1, 2. 概述/链路层
-----
前两章比较水, 这里略过.
-----
## 3. IP 协议
-----
IP: 不可靠, 无连接的数据包传送服务.
**不可靠(unreliable)**: 没有确认的ACK机制 => 无法反馈成功/失败的数据包传输 => 不能保证数据包成功到达 => 不可靠 (对比TCP, TCP是可靠的, 因为有 ACK/重传机制)
**无连接(connectionless)**: IP 不维护任何关于数据报的状态信息. 每个数据报是相互独立的. (对比TCP: 建立连接时要握手, 消除连接时也要握手, 因此是有连接的)
**IP 首部**: 重点是 1. 生存时间 TTL(可以经过的最多路由器数, 一般为32/64. 字段减少到0时, 发送 ICMP 数据报通知源主机) 2. 协议号(描述上层内容应该用哪个协议来解) 3. IP source/dest 地址. 首部长 32bit, 传输顺序: 0-7, 8-15, 16-23, 24-31(网络字节序, big endian). 数据包最大值为 65535(2^16) Byte, 但一般都比较小(1024以下).
-----
## 4. ARP: 地址解析协议
-----
是一个映射: 32bit 的 ip addr => 48 bit 的 ether addr. ARP 一开始用于广播网络, 在同一个网络中根据 ip addr 查找 mac addr. 通过 `arp -a` 可以看到当前 arp cache, 用来看统一网络上挂的所有设备挺方便的. arp 请求是广播的, 应答是直接到请求端主机的.
arp 代理(or ARP hack/promiscuous ARP): 使用一个中介路由器, 把两个物理网络使用同样的网络号, 伪装成一个. 用法参考 arp 命令的 -s 和 pub 关键字.
-----
## 5. RARP: 逆地址解析协议
-----
一台 host 启动的时候, mac addr 是已知的, 而 ip addr 未知. mac addr => ip addr 刚好和 ARP 反过来, 做法就称为 RARP. 一般用来做无盘系统引导时 ip addr 的获取.
ARP 解析出来是这样:
arp who-has 140.252.13.36 tell bsdi
自 ARP(也就是书中的免费 ARP)是这样:
arp who-has 140.252.13.36 tell 140.252.13.36, 用来查找网络上是否有与自己 ip 冲突的设备.
RARP 是这样:
arp who-is 8:0:20:3:f6:42 tell 8:0:20:3:f6:42
与 ARP 相同的是, 请求是广播, 应答是单播. RARP 需要专门的 RARP server; 同时 RARP 的地址映射一般在磁盘上, 内核一般不读磁盘(really?), 那么得用用户进程而不是内核 TCP/IP 实现来做. 更麻烦的是 RARP 是一个特殊类型的 ethernet frame(和 ARP 像, 类型字段不同), 这里也比较麻烦.
-----
## 6. ICMP: Internet 控制报文协议 Internet Control Message Protocal
-----
经常被认为是和 IP 同一层的协议, 实际上也是用了 IP header 的:
| IP header(20bytes) | ICMP datagram |
ICMP datagram 的结构:
| type(8 bit) | code(8 bit) | checksum(16 bit) | content |
type + code 表示报文内容, 比如这样:
{type: 3 code: 5}: 源站选路失败
为了防止广播风暴, {ICMP 报文自己的差错/目的是广播/多播的 IP 数据报/非 IP 分片的第一片/源地址非单个地址的数据报} 不会产生 ICMP 数据报.
-----
## 7. Ping 程序
-----
走的是 ICMP. 其他没有太多可说的.
ping 的 -R 选项提供 record router 功能, 类似 traceroute. IP首部中留给该选项的空间有限, 最多9个地址. 再多就要上 traceroute 了.
```
$ ping -R www.douban.com
PING www.douban.com (211.147.4.49): 56 data bytes
64 bytes from 211.147.4.49: icmp_seq=0 ttl=50 time=326.495 ms
RR: 172.30.1.42
202.112.41.178
101.4.112.93
101.4.117.82
218.240.0.20
218.240.7.221
218.241.255.106
202.99.1.214
218.241.244.85
64 bytes from 211.147.4.49: icmp_seq=1 ttl=50 time=86.898 ms (same route)
$ traceroute www.douban.com
traceroute: Warning: www.douban.com has multiple addresses; using 211.147.4.49
traceroute to www.douban.com (211.147.4.49), 64 hops max, 52 byte packets
1 10.0.196.1 (10.0.196.1) 71.851 ms 1.863 ms 1.405 ms
2 172.30.1.41 (172.30.1.41) 1.500 ms 1.459 ms 1.393 ms
3 162.105.252.130 (162.105.252.130) 1.479 ms 1.590 ms 1.397 ms
4 202.112.41.185 (202.112.41.185) 1.915 ms 1.984 ms 1.831 ms
5 202.112.41.177 (202.112.41.177) 2.966 ms 3.601 ms 2.772 ms
6 101.4.112.94 (101.4.112.94) 4.207 ms 4.962 ms
101.4.117.82 (101.4.117.82) 4.540 ms
7 101.4.112.89 (101.4.112.89) 2.748 ms 2.744 ms 2.370 ms
8 101.4.116.194 (101.4.116.194) 3.543 ms 2.596 ms
101.4.118.206 (101.4.118.206) 3.445 ms
9 218.240.0.17 (218.240.0.17) 3.297 ms 3.919 ms 2.731 ms
10 218.240.7.222 (218.240.7.222) 63.430 ms 4.812 ms 3.782 ms
11 218.241.255.142 (218.241.255.142) 3.946 ms 3.432 ms 3.433 ms
12 202.99.1.173 (202.99.1.173) 3.681 ms 9.629 ms
202.99.1.169 (202.99.1.169) 3.529 ms
13 218.241.244.86 (218.241.244.86) 3.734 ms 3.280 ms 3.534 ms
14 124.202.11.89 (124.202.11.89) 3.031 ms 3.885 ms 3.205 ms
15 219.239.92.10 (219.239.92.10) 4.014 ms 3.613 ms 3.641 ms
16 * * *
17 * * *
18 * * *
(最多64跳, 之后19-64的 router 都是 *, 意味着无返回)
```
-----
## 8. Traceroute
-----
traceroute 也用的 ICMP, 但用的是 ttl 的选项. 从1开始, 下一份数据报的 ttl 逐渐增大. 注意返回的 ICMP 报文信源 IP 是路由器入口 IP, 而 ping -R 中是路由器的出口 IP, 因此可能不同; 又因为 a -> b 的入口就是 b -> a 的出口, 在 a 和 b 做 traceroute 看到的路由器 IP 可能是不同的.
-----
## 9. IP 选路
-----
根据的是内存中的路由表(由内核维护). 用 `netstat -rn` 可以查看, 像这样:
```
$ netstat -rn
Routing tables
Internet:
Destination Gateway Flags Refs Use Netif Expire
default 10.0.196.1 UGSc 69 0 en0
default 10.8.51.114 UGScI 1 0 utun0
10.0.196/22 link#4 UCS 2 0 en0
10.0.196.1/32 link#4 UCS 2 0 en0
10.0.196.1 0:19:7:33:e0:0 UHLWIir 70 40 en0 692
10.0.199.189/32 link#4 UCS 1 0 en0
10.0.199.255 link#4 UHLWbI 1 18 en0
10.8/16 10.8.51.114 UGSc 1 0 utun0
10.8.51.114 10.8.51.113 UHr 8 0 utun0
10.8.51.114/32 link#10 UCS 1 0 utun0
10.168/16 10.8.51.114 UGSc 3 0 utun0
115.182.201.68/32 10.8.51.114 UGSc 1 0 utun0
118.144.67.50/32 10.8.51.114 UGSc 1 0 utun0
127 127.0.0.1 UCS 1 0 lo0
127.0.0.1 127.0.0.1 UH 8 6512 lo0
169.254 link#4 UCS 1 0 en0
224.0.0 link#4 UmCS 2 0 en0
224.0.0.251 1:0:5e:0:0:fb UHmLWI 1 0 en0
255.255.255.255/32 link#4 UCS 1 0 en0
```
解释一下: Flag: {U: 可用, G: gateway(那么 destination 一般是一个网络地址), H: host(那么 destination 一般是一个主机地址)}, Ref: 使用路由的活动进程个数, Use: 发送的分组数.
ICMP 重定向报文: 告诉发送方: 以后经我转交给某某的报文, 您直接发给某某就好. 比如: r1/r2 在同一个网段, 由于 r0 路由表设置的问题, 目标是 r2 的报文它给了 r1, 那 r1 转发之后就再回复一个 ICMP 重定向给 r0, r0 据此修改路由表, 下次直接给 r2. 这使得 LAN 上的机器可以互相学习路由表. 值得注意的是, ICMP 重定向报文只能由路由器生成, 作用于主机.
ICMP 发现报文: 用于网络接口打开后初始化路由表(另一种方法是采用静态路由表). 主机引导的时候广播或者多播路由器请求报文, 可能收到一台或多台机器的响应(内容格式为: {路由器地址: 优先级})(类似的是, 路由器定期广播/多播其路由器通告报文). 路由器启动后, 定期在所有接口上发送通告报文.
-----
## 10. 动态选路协议
-----
RIP(Routing Information Protocal)/OSPF(Open Shortest Path First)/BGP(Border Gateway Protocal)
Internet 以一组自治系统(AS, Antonomous System)的方式组织, 每个自治系统内部各个路由器之间的选路协议称为 IGP(Interior Gateway Protocal), IGP 一开始用的是 RIP, 后来 OSPF 意在取代 RIP; 与之相对的是外部网关协议 EGP(Exterier Gateway Protocal), 现用的网关协议是 BGP.
RIP: 以路由器跳作为度量(最大为15, 16表示不可达). 每过30秒将完整路由表在网段上广播, 接收到这样的广播时触发更新, 在多条广播对同一目的的跳数中, 选择最小的.
可以预见的是, 跳数粒度的改变会在网络中蔓延开, 最终得到这个粒度下的最优解. 问题是路由器故障后可能发生环路(书上说的)(不能快速识别故障).
OSPF: 每个路由器主动测试与相邻路由器的状态, 并将这些信息发送给其他邻站. 比 RIP 多加了个主动探测的功能, 结果是在变化后链路状态收敛(->稳定状态)更快.
CIDR: 无类型域间选路: a/b/c 类网段每一个都需要一个路由表项, 这样下去路由表膨胀得很厉害. CIDR 的思路是: 打通 a/b/c 类, 只要地址最高几位相同就可以合并到一个路由项, 然后总是选取最长匹配(所谓"无类型"). 坏处就是现在选路需要 32bit 的 IP地址还不够, 还需要 32 bit 的 netmask.
-----
## 11. UDP: User Datagram Protocal
-----
UDP Datagram:
| IP header | UDP header | UDP data |
很简单: 无连接, 不提供可靠性(但还是有 checksum, 听说有些 NFS 通过关 checksum 来提速= =): 直接把数据扔出去.
IP 分片: 一直在 IP 层上, 对 TCP/UDP 是透明的(UDP/TCP 首部只在第一片中出现). 分片数据报可能失序, 但在到达目的地后可以组装起来. 坏处是由于 IP 层没有确认机制(不可靠), 一个分片丢失将导致重传整个分组(由上层决定), 是比较浪费的.
系统接受 IP 数据包的速率超过处理速率时, 发送源站抑制 ICMP报文. 注意 ICMP 是不会提哪个端口的, 同时新的 RFC 要求路由器不能产生源站抑制, 那么发送过快的 UDP 被路由器扔了是没有反馈的, 因此用 UDP 得在应用层建立应答机制.
netstat 的输出中, *.* 表示接受任何远端地址/端口. 本地监听端口时, 不同 ip + 相同端口是可以的; 相同 ip + 相同端口不能接受(多播似乎可以?)
-----
## 12, 13. 广播和多播, IGMP
-----
广播和多播仅面向 UDP(TCP 是一对一(连接)的). 广播(broadcast): 向网络的所有其他主机发送分组; 多播(multicast): 向特定的多播组发送分组. 多播通过专门的 IP 地址(D 类)界定多播组. 跨路由器的多播需要租管理协议 IGMP 来界定主机.
顺便查了一下, 现在的直播有走 UDP 有走 TCP 的(大多走 TCP), 组播在这里似乎用不上.
-----
## 14. DNS: 域名系统
-----
可以将主机换成 IP 地址, 也可以是反过来. 同时使用 TCP/UDP 端口 53. 大部分 DNS 查询在局域网上, 因此(局域网分组丢失率较小)大多用 UDP. 短程局域网一般用 UDP, 广域网上由于往返时延大, 差错率更大, 因此用TCP 或者自己实现容错机制.
-----
## 17, 18. TCP 连接
-----
连接(握手), 定时器(超时), 重排序, 流量控制(窗口大小). 传输的内容是8bit 字节流, 对内容不做任何解释. 全双工(数据在两个方向上独立传输). 校验和覆盖整个报文: header + content.
建立连接的3次握手都比较熟了, 说下断开的4次握手:
因为是全双工的, 两个方向都必须单独关闭(因为半关闭是允许的, 一个方向上收到 FIN 意味着这个方向关闭), 如图:
A ----FIN--------> B
<--FIN-ACK----
<--FIN----------
----FIN-ACK-->
半关闭最初是作为 remoteshell(rsh) 这样的程序通知 server 端自己的数据已经发送完毕出现的. 没有半关闭, 就需要其他的一些技术通知 server 端传输完成.
可以看到其上 HTTP 的影子: 总是有主动打开(send)/被动打开(listen) 的角色区分.
TCP 服务器设计: 举个例子 telnet 服务器, 监听 TCP23. 远端请求来了并不会完全占用这个端口:
Proto Recv-Q Send-Q Local Address Foreign Address (state)
tcp 0 0 140.252.13.33.23 140.252.13.65.1030 ESTABLISHED
tcp 0 0 140.252.13.33.23 140.252.13.65.1029 ESTABLISHED
tcp 0 0 *.23 *.* LISTEN
-----
## 19. TCP 交互数据流
-----
成块数据(FTP, 电子邮件, 网络页面) 交互数据(telnet/rlogin)
91年的数据: 按分组计算, 二者一半一半; 按字节计算, 9比1.
TCP 同时处理这两种数据, 但是算法不同. 对于 rlogin, 每次交互按键都会产生一个数据分组(字符为单位而不是行为单位). 每次按键会造成4个报文段:
1) 来自 client 的按键 2) server 的按键确认 3) server 回显 4) client 的回显确认. 通过时延确认技术, 2)3)可以一起发送
经过时延的确认: 大多数实现时延200ms, 在此期间等待是否有同方向可以合并的 ACK. RFC 声明 TCP 需要实现经受时延的 ACK, 但是时延小于 500ms
Nagle 算法: rlogin 等程序带来的大量小分组在 WAN 上会增加拥塞出现的可能. Nagle 算法要求一个 TCP 连接中最多有一个未被确认的小分组. 该分组的确认到达之前不能发送其他的小分组. -> 确认到达越快, 数据也就发送越快. 像 X Window Server 这样需要无延迟发送小消息的(比如鼠标移动), 就要关闭 Nagle, 比如打开 TCP_NODELAY.
-----
## 20. TCP 的成块数据流
-----
## Does TCP ACK packet carry payload?
quote from stackoverflow:
> This behavior is dependent on both the OS and the application. In linux, the kernel doesn't send an ACK directly, but instead waits a fixed number of milliseconds (around 200), hoping that is has some data to send back and can let the ACK piggyback the data.
=> So yes, kernel waits about 200ms for payload before sending ACK packet.
## 拥塞
TCP/IP 详解说得很明白(page220-221): 当数据到达一个大的管道(如一个快速局域网) 并向一个小的管道(如较慢的广域网) 发送时便会发生拥塞. 多个输入流到达一个路由器, 而路由器的输出流小于这些输入流的总和时也会发生拥塞.
(我)拥塞源于路由器高速输入和低速输出间的速度差. 多出来的数据包在填满路由器缓冲之后只能被丢掉. 因此丢包是判断网络拥塞的最明显特征.
## tcpdump
需要 su 权限
```
tcpdump -i en0
tcpdump ip(ip 数据包) host <hostA-ip> and not \(192.168.0.5 or 192.168.0.6\)
# hostname 发送的所有包
tcpdump -i en0 src host <hostname>
# 发给 hostname 的所有包
tcpdump -i en0 dst host <hostname>
# 监视 udp 端口 123
tcpdump udp port 123
```
## TCP 首部的 URG bit: 紧急数据
URG bit 被置1, 同时紧急指针被设为一个正的偏移量(与 TCP 首部中的序号相加, 得出紧急数据最后一个字节的偏移量). 紧急数据用于 `telnet`, `rlogin`, `ftp` 的中断/停止命令.
-----
## 21. TCP 的超时与重传
-----
## 重传与指数退避
书中这里举了个例子, 发送数据一半把网线拔了. 重传包的发送间隔为 1, 3, 6, 12, 24, 48, 64, 64,.. 显然的指数退避.
*2 的指数退避是基于初始 RTO (Retransmission TimeOut)的. RTO 具体参考`net/ipv4/tcp_input.c` 中的 `tcp_rtt_estimator()`(输出新的 rtt)和 `tcp_set_rto()`():
tcp_rtt_estimator() 中的核心算法是:
/* rtt = 7/8 rtt + 1/8 new */
/* mdev = 3/4 mdev + 1/4 new */
tcp_set_rto() 是:
usecs_to_jiffies((tp->srtt_us >> 3) + tp->rttvar_us);
srtt_us 是 rtt 估计值. rttvar_us 是方差因子.
问题:
1) 那实际上是 1/8*a+d?
2) 我没看到指数退避. 是否就是用的 [Jacobson1990] 的这个 RTO 估计算法, 不用指数退避?
算法中的想法:
1. 平滑化: 过去的 srtt 有 alpha 的权值, 当前 rtt 是 1-alpha 的权值. alpha: 平滑因子: .8-.9
SRTT = ( ALPHA * SRTT ) + ((1-ALPHA) * RTT) => 平滑化在 Linux 中的应用挺多, 还包括 load 的计算.
2. 乘以一个参数并限制在上下界内: beta: delay variance factor 延迟方差因子: 1.3-2.0
RTO = min[UBOUND,max[LBOUND,(BETA*SRTT)]
某个包丢失后(比如包 666), 接收方收到后序非重传的包, 返回的 ACK 值都是丢的包(666); 直到收到重传的包. 而发送方在第三个 ACK=666 之后进入快速重传和快速恢复.
### Ref:
1. [TCP/IP 重传超时--RTO](http://www.orczhou.com/index.php/2011/10/tcpip-protocol-start-rto/)
2. [Linux kernel source](
http://lxr.free-electrons.com/source/net/ipv4/tcp_input.c)
## 拥塞避免
对每个连接维护拥塞窗口 cwnd 和慢启动门限 ssthresh.
1. 初始化: cwnd = 1, ssthresh = 65535
2. output window size <= min(cwnd, receiver's window size)
3. 慢启动
4. 发生拥塞后, ssthresh = max(cwnd/2, 2), cwnd = 1, enter slow start
5. When cwnd >= ssthresh, enter congestion avoidance.
## 快速重传 & 快速恢复
为什么一定要3个重复 ACK 才重传? => 我们不知道包是 1) 丢失了 2) 只是顺序被打乱, 在后面能收到.
快速重传的算法针对3个重复 ACK 的情况, 基本是原算法直接跳过慢启动的过程, cwnd = ssthresh.
前两个重复 ACK 到达时, cwnd 保持不变(对应于书中图21-10 中几段较为平坦的部分). 第三个重复 ACK 到达时, ssthresh被置为 cwnd 的一半, cwnd = ssthresh + 重复 ACK 数量*报文段大小.
平滑化的 RTT, 均值偏差, ssthresh 在 TCP 连接关闭时以路由表项为 key 进行缓存. 下次使用路由表项时拿出来继续用.
-----
## 22. TCP 的坚持定时器
-----
如我们所知, TCP 不为其 ACK 包发送确认. 这可能导致死锁: 在 ACK 包丢失时, 发送方等待这个 ACK 包, 而接收方等待新的数据. 为了阻止这种情况, 发送方使用一个坚持定时器(persistent timer) 周期性向接收方查询. 定时器到期后发送放发包, 检查窗口是否增大(再通过返回的 ACK 就能看出当前 ACK 到多少了, 这样之前的 ACK 即使丢包也被弥补了). 这时发送方发出的报文称为窗口探查(window probe), 窗口探查也用的 TCP 的间隔时间指数退避, 60s 上限的做法.
窗口探查说明了 TCP 的 server/client 端除了正经的数据传输外还是有藕断丝连的, 只要一方想绝交(window=0), 另一方就会跑个 loop 去不断打探消息(当然, TCP 连接也是可以完全空白的, 双方建立了连接, 然后啥也不干, probe 都没有).
-----
## 23. TCP 的保活定时器
-----
保活定时器就是针对上述"建立连接后什么也不干"的一个 timeout, 这个 timeout 是系统级的, 作为应用设定 socket timeout 的上界. 书中这个 timeout 是两小时, Linux kernel 中对应 TCP_KEEPALIVE_TIME 也是两小时, TCP_KEEPALIVE_PROBES 指定超时后最多发送9次 probe, 如果都没有相应说明客户机已经关闭并终止连接.
probe 的结果可能有4种: 对端正常, 对端崩溃, 对端崩溃并重启, 无法连接到对端.
-----
## 24. TCP 的未来和性能
-----
首先是路径 MTU. Linux: TCP_MIN_MSS: 88, TCP_BASE_MSS: 1024 如果当前报文需要分片的话, 通过返回的 icmp: need to frag 报文通知. 一个发现是, 并非分组越大效率越高, 因为路由器是存储转发设备, 在完整地接受分组之后才能转发, 而大的分组+低速链路使得接受分组的时间过长, 分组在每个路由器上都会卡一下, 导致总传输时间变长.
这里举了个例子: 通过4个路由器发送8192字节, 链路速度为1544000b/s:
1) 使用4096字节的分组: 每一跳速度: (4096 + 40) * 8 / 1544000 = 21.4ms, 总共3跳两个分组: 21.4 * (3 + (2 -1)) = 85.6ms.
2) 使用 512字节的分组: 每一跳: (512 + 40) * 8 / 154000b/s = 2.9ms, 总共3跳16个分组: 2.9 * (3 + (16 - 1)) = 52.2ms, 小的分组反而快些.
然后讨论长肥管道. 长指的是时延长, 肥指的是带宽高.
-----
## 29/30. 网络文件系统/其他 TCP/IP 应用
-----
NFS 的 client/server 端实现都放在内核里, 为了减少 context switch 的开销.
写这本书的时候还有 whois/finger 等专门的应用, 占了低端口. 现在看来 HTTP 基本做到了通用, 把这些东西替代了. 通用接口效率低, 但随着时间带宽在提升, 随着带宽效率在提升. Archie/WAIS/Gopher/Veronica 这类索引/白页就是以后 WWW 需要搜索引擎的预兆. 现在入口从 web 转向移动应用, 我的理解是 web 是一手信息, 多, 杂; App 上的内容是直接交互/二手筛选的, 暂时还看不到一个新索引工具的必要.
-----
## 1, 2. 概述/链路层
-----
前两章比较水, 这里略过.
-----
## 3. IP 协议
-----
IP: 不可靠, 无连接的数据包传送服务.
**不可靠(unreliable)**: 没有确认的ACK机制 => 无法反馈成功/失败的数据包传输 => 不能保证数据包成功到达 => 不可靠 (对比TCP, TCP是可靠的, 因为有 ACK/重传机制)
**无连接(connectionless)**: IP 不维护任何关于数据报的状态信息. 每个数据报是相互独立的. (对比TCP: 建立连接时要握手, 消除连接时也要握手, 因此是有连接的)
**IP 首部**: 重点是 1. 生存时间 TTL(可以经过的最多路由器数, 一般为32/64. 字段减少到0时, 发送 ICMP 数据报通知源主机) 2. 协议号(描述上层内容应该用哪个协议来解) 3. IP source/dest 地址. 首部长 32bit, 传输顺序: 0-7, 8-15, 16-23, 24-31(网络字节序, big endian). 数据包最大值为 65535(2^16) Byte, 但一般都比较小(1024以下).
-----
## 4. ARP: 地址解析协议
-----
是一个映射: 32bit 的 ip addr => 48 bit 的 ether addr. ARP 一开始用于广播网络, 在同一个网络中根据 ip addr 查找 mac addr. 通过 `arp -a` 可以看到当前 arp cache, 用来看统一网络上挂的所有设备挺方便的. arp 请求是广播的, 应答是直接到请求端主机的.
arp 代理(or ARP hack/promiscuous ARP): 使用一个中介路由器, 把两个物理网络使用同样的网络号, 伪装成一个. 用法参考 arp 命令的 -s 和 pub 关键字.
-----
## 5. RARP: 逆地址解析协议
-----
一台 host 启动的时候, mac addr 是已知的, 而 ip addr 未知. mac addr => ip addr 刚好和 ARP 反过来, 做法就称为 RARP. 一般用来做无盘系统引导时 ip addr 的获取.
ARP 解析出来是这样:
arp who-has 140.252.13.36 tell bsdi
自 ARP(也就是书中的免费 ARP)是这样:
arp who-has 140.252.13.36 tell 140.252.13.36, 用来查找网络上是否有与自己 ip 冲突的设备.
RARP 是这样:
arp who-is 8:0:20:3:f6:42 tell 8:0:20:3:f6:42
与 ARP 相同的是, 请求是广播, 应答是单播. RARP 需要专门的 RARP server; 同时 RARP 的地址映射一般在磁盘上, 内核一般不读磁盘(really?), 那么得用用户进程而不是内核 TCP/IP 实现来做. 更麻烦的是 RARP 是一个特殊类型的 ethernet frame(和 ARP 像, 类型字段不同), 这里也比较麻烦.
-----
## 6. ICMP: Internet 控制报文协议 Internet Control Message Protocal
-----
经常被认为是和 IP 同一层的协议, 实际上也是用了 IP header 的:
| IP header(20bytes) | ICMP datagram |
ICMP datagram 的结构:
| type(8 bit) | code(8 bit) | checksum(16 bit) | content |
type + code 表示报文内容, 比如这样:
{type: 3 code: 5}: 源站选路失败
为了防止广播风暴, {ICMP 报文自己的差错/目的是广播/多播的 IP 数据报/非 IP 分片的第一片/源地址非单个地址的数据报} 不会产生 ICMP 数据报.
-----
## 7. Ping 程序
-----
走的是 ICMP. 其他没有太多可说的.
ping 的 -R 选项提供 record router 功能, 类似 traceroute. IP首部中留给该选项的空间有限, 最多9个地址. 再多就要上 traceroute 了.
```
$ ping -R www.douban.com
PING www.douban.com (211.147.4.49): 56 data bytes
64 bytes from 211.147.4.49: icmp_seq=0 ttl=50 time=326.495 ms
RR: 172.30.1.42
202.112.41.178
101.4.112.93
101.4.117.82
218.240.0.20
218.240.7.221
218.241.255.106
202.99.1.214
218.241.244.85
64 bytes from 211.147.4.49: icmp_seq=1 ttl=50 time=86.898 ms (same route)
$ traceroute www.douban.com
traceroute: Warning: www.douban.com has multiple addresses; using 211.147.4.49
traceroute to www.douban.com (211.147.4.49), 64 hops max, 52 byte packets
1 10.0.196.1 (10.0.196.1) 71.851 ms 1.863 ms 1.405 ms
2 172.30.1.41 (172.30.1.41) 1.500 ms 1.459 ms 1.393 ms
3 162.105.252.130 (162.105.252.130) 1.479 ms 1.590 ms 1.397 ms
4 202.112.41.185 (202.112.41.185) 1.915 ms 1.984 ms 1.831 ms
5 202.112.41.177 (202.112.41.177) 2.966 ms 3.601 ms 2.772 ms
6 101.4.112.94 (101.4.112.94) 4.207 ms 4.962 ms
101.4.117.82 (101.4.117.82) 4.540 ms
7 101.4.112.89 (101.4.112.89) 2.748 ms 2.744 ms 2.370 ms
8 101.4.116.194 (101.4.116.194) 3.543 ms 2.596 ms
101.4.118.206 (101.4.118.206) 3.445 ms
9 218.240.0.17 (218.240.0.17) 3.297 ms 3.919 ms 2.731 ms
10 218.240.7.222 (218.240.7.222) 63.430 ms 4.812 ms 3.782 ms
11 218.241.255.142 (218.241.255.142) 3.946 ms 3.432 ms 3.433 ms
12 202.99.1.173 (202.99.1.173) 3.681 ms 9.629 ms
202.99.1.169 (202.99.1.169) 3.529 ms
13 218.241.244.86 (218.241.244.86) 3.734 ms 3.280 ms 3.534 ms
14 124.202.11.89 (124.202.11.89) 3.031 ms 3.885 ms 3.205 ms
15 219.239.92.10 (219.239.92.10) 4.014 ms 3.613 ms 3.641 ms
16 * * *
17 * * *
18 * * *
(最多64跳, 之后19-64的 router 都是 *, 意味着无返回)
```
-----
## 8. Traceroute
-----
traceroute 也用的 ICMP, 但用的是 ttl 的选项. 从1开始, 下一份数据报的 ttl 逐渐增大. 注意返回的 ICMP 报文信源 IP 是路由器入口 IP, 而 ping -R 中是路由器的出口 IP, 因此可能不同; 又因为 a -> b 的入口就是 b -> a 的出口, 在 a 和 b 做 traceroute 看到的路由器 IP 可能是不同的.
-----
## 9. IP 选路
-----
根据的是内存中的路由表(由内核维护). 用 `netstat -rn` 可以查看, 像这样:
```
$ netstat -rn
Routing tables
Internet:
Destination Gateway Flags Refs Use Netif Expire
default 10.0.196.1 UGSc 69 0 en0
default 10.8.51.114 UGScI 1 0 utun0
10.0.196/22 link#4 UCS 2 0 en0
10.0.196.1/32 link#4 UCS 2 0 en0
10.0.196.1 0:19:7:33:e0:0 UHLWIir 70 40 en0 692
10.0.199.189/32 link#4 UCS 1 0 en0
10.0.199.255 link#4 UHLWbI 1 18 en0
10.8/16 10.8.51.114 UGSc 1 0 utun0
10.8.51.114 10.8.51.113 UHr 8 0 utun0
10.8.51.114/32 link#10 UCS 1 0 utun0
10.168/16 10.8.51.114 UGSc 3 0 utun0
115.182.201.68/32 10.8.51.114 UGSc 1 0 utun0
118.144.67.50/32 10.8.51.114 UGSc 1 0 utun0
127 127.0.0.1 UCS 1 0 lo0
127.0.0.1 127.0.0.1 UH 8 6512 lo0
169.254 link#4 UCS 1 0 en0
224.0.0 link#4 UmCS 2 0 en0
224.0.0.251 1:0:5e:0:0:fb UHmLWI 1 0 en0
255.255.255.255/32 link#4 UCS 1 0 en0
```
解释一下: Flag: {U: 可用, G: gateway(那么 destination 一般是一个网络地址), H: host(那么 destination 一般是一个主机地址)}, Ref: 使用路由的活动进程个数, Use: 发送的分组数.
ICMP 重定向报文: 告诉发送方: 以后经我转交给某某的报文, 您直接发给某某就好. 比如: r1/r2 在同一个网段, 由于 r0 路由表设置的问题, 目标是 r2 的报文它给了 r1, 那 r1 转发之后就再回复一个 ICMP 重定向给 r0, r0 据此修改路由表, 下次直接给 r2. 这使得 LAN 上的机器可以互相学习路由表. 值得注意的是, ICMP 重定向报文只能由路由器生成, 作用于主机.
ICMP 发现报文: 用于网络接口打开后初始化路由表(另一种方法是采用静态路由表). 主机引导的时候广播或者多播路由器请求报文, 可能收到一台或多台机器的响应(内容格式为: {路由器地址: 优先级})(类似的是, 路由器定期广播/多播其路由器通告报文). 路由器启动后, 定期在所有接口上发送通告报文.
-----
## 10. 动态选路协议
-----
RIP(Routing Information Protocal)/OSPF(Open Shortest Path First)/BGP(Border Gateway Protocal)
Internet 以一组自治系统(AS, Antonomous System)的方式组织, 每个自治系统内部各个路由器之间的选路协议称为 IGP(Interior Gateway Protocal), IGP 一开始用的是 RIP, 后来 OSPF 意在取代 RIP; 与之相对的是外部网关协议 EGP(Exterier Gateway Protocal), 现用的网关协议是 BGP.
RIP: 以路由器跳作为度量(最大为15, 16表示不可达). 每过30秒将完整路由表在网段上广播, 接收到这样的广播时触发更新, 在多条广播对同一目的的跳数中, 选择最小的.
可以预见的是, 跳数粒度的改变会在网络中蔓延开, 最终得到这个粒度下的最优解. 问题是路由器故障后可能发生环路(书上说的)(不能快速识别故障).
OSPF: 每个路由器主动测试与相邻路由器的状态, 并将这些信息发送给其他邻站. 比 RIP 多加了个主动探测的功能, 结果是在变化后链路状态收敛(->稳定状态)更快.
CIDR: 无类型域间选路: a/b/c 类网段每一个都需要一个路由表项, 这样下去路由表膨胀得很厉害. CIDR 的思路是: 打通 a/b/c 类, 只要地址最高几位相同就可以合并到一个路由项, 然后总是选取最长匹配(所谓"无类型"). 坏处就是现在选路需要 32bit 的 IP地址还不够, 还需要 32 bit 的 netmask.
-----
## 11. UDP: User Datagram Protocal
-----
UDP Datagram:
| IP header | UDP header | UDP data |
很简单: 无连接, 不提供可靠性(但还是有 checksum, 听说有些 NFS 通过关 checksum 来提速= =): 直接把数据扔出去.
IP 分片: 一直在 IP 层上, 对 TCP/UDP 是透明的(UDP/TCP 首部只在第一片中出现). 分片数据报可能失序, 但在到达目的地后可以组装起来. 坏处是由于 IP 层没有确认机制(不可靠), 一个分片丢失将导致重传整个分组(由上层决定), 是比较浪费的.
系统接受 IP 数据包的速率超过处理速率时, 发送源站抑制 ICMP报文. 注意 ICMP 是不会提哪个端口的, 同时新的 RFC 要求路由器不能产生源站抑制, 那么发送过快的 UDP 被路由器扔了是没有反馈的, 因此用 UDP 得在应用层建立应答机制.
netstat 的输出中, *.* 表示接受任何远端地址/端口. 本地监听端口时, 不同 ip + 相同端口是可以的; 相同 ip + 相同端口不能接受(多播似乎可以?)
-----
## 12, 13. 广播和多播, IGMP
-----
广播和多播仅面向 UDP(TCP 是一对一(连接)的). 广播(broadcast): 向网络的所有其他主机发送分组; 多播(multicast): 向特定的多播组发送分组. 多播通过专门的 IP 地址(D 类)界定多播组. 跨路由器的多播需要租管理协议 IGMP 来界定主机.
顺便查了一下, 现在的直播有走 UDP 有走 TCP 的(大多走 TCP), 组播在这里似乎用不上.
-----
## 14. DNS: 域名系统
-----
可以将主机换成 IP 地址, 也可以是反过来. 同时使用 TCP/UDP 端口 53. 大部分 DNS 查询在局域网上, 因此(局域网分组丢失率较小)大多用 UDP. 短程局域网一般用 UDP, 广域网上由于往返时延大, 差错率更大, 因此用TCP 或者自己实现容错机制.
-----
## 17, 18. TCP 连接
-----
连接(握手), 定时器(超时), 重排序, 流量控制(窗口大小). 传输的内容是8bit 字节流, 对内容不做任何解释. 全双工(数据在两个方向上独立传输). 校验和覆盖整个报文: header + content.
建立连接的3次握手都比较熟了, 说下断开的4次握手:
因为是全双工的, 两个方向都必须单独关闭(因为半关闭是允许的, 一个方向上收到 FIN 意味着这个方向关闭), 如图:
A ----FIN--------> B
<--FIN-ACK----
<--FIN----------
----FIN-ACK-->
半关闭最初是作为 remoteshell(rsh) 这样的程序通知 server 端自己的数据已经发送完毕出现的. 没有半关闭, 就需要其他的一些技术通知 server 端传输完成.
可以看到其上 HTTP 的影子: 总是有主动打开(send)/被动打开(listen) 的角色区分.
TCP 服务器设计: 举个例子 telnet 服务器, 监听 TCP23. 远端请求来了并不会完全占用这个端口:
Proto Recv-Q Send-Q Local Address Foreign Address (state)
tcp 0 0 140.252.13.33.23 140.252.13.65.1030 ESTABLISHED
tcp 0 0 140.252.13.33.23 140.252.13.65.1029 ESTABLISHED
tcp 0 0 *.23 *.* LISTEN
-----
## 19. TCP 交互数据流
-----
成块数据(FTP, 电子邮件, 网络页面) 交互数据(telnet/rlogin)
91年的数据: 按分组计算, 二者一半一半; 按字节计算, 9比1.
TCP 同时处理这两种数据, 但是算法不同. 对于 rlogin, 每次交互按键都会产生一个数据分组(字符为单位而不是行为单位). 每次按键会造成4个报文段:
1) 来自 client 的按键 2) server 的按键确认 3) server 回显 4) client 的回显确认. 通过时延确认技术, 2)3)可以一起发送
经过时延的确认: 大多数实现时延200ms, 在此期间等待是否有同方向可以合并的 ACK. RFC 声明 TCP 需要实现经受时延的 ACK, 但是时延小于 500ms
Nagle 算法: rlogin 等程序带来的大量小分组在 WAN 上会增加拥塞出现的可能. Nagle 算法要求一个 TCP 连接中最多有一个未被确认的小分组. 该分组的确认到达之前不能发送其他的小分组. -> 确认到达越快, 数据也就发送越快. 像 X Window Server 这样需要无延迟发送小消息的(比如鼠标移动), 就要关闭 Nagle, 比如打开 TCP_NODELAY.
-----
## 20. TCP 的成块数据流
-----
## Does TCP ACK packet carry payload?
quote from stackoverflow:
> This behavior is dependent on both the OS and the application. In linux, the kernel doesn't send an ACK directly, but instead waits a fixed number of milliseconds (around 200), hoping that is has some data to send back and can let the ACK piggyback the data.
=> So yes, kernel waits about 200ms for payload before sending ACK packet.
## 拥塞
TCP/IP 详解说得很明白(page220-221): 当数据到达一个大的管道(如一个快速局域网) 并向一个小的管道(如较慢的广域网) 发送时便会发生拥塞. 多个输入流到达一个路由器, 而路由器的输出流小于这些输入流的总和时也会发生拥塞.
(我)拥塞源于路由器高速输入和低速输出间的速度差. 多出来的数据包在填满路由器缓冲之后只能被丢掉. 因此丢包是判断网络拥塞的最明显特征.
## tcpdump
需要 su 权限
```
tcpdump -i en0
tcpdump ip(ip 数据包) host <hostA-ip> and not \(192.168.0.5 or 192.168.0.6\)
# hostname 发送的所有包
tcpdump -i en0 src host <hostname>
# 发给 hostname 的所有包
tcpdump -i en0 dst host <hostname>
# 监视 udp 端口 123
tcpdump udp port 123
```
## TCP 首部的 URG bit: 紧急数据
URG bit 被置1, 同时紧急指针被设为一个正的偏移量(与 TCP 首部中的序号相加, 得出紧急数据最后一个字节的偏移量). 紧急数据用于 `telnet`, `rlogin`, `ftp` 的中断/停止命令.
-----
## 21. TCP 的超时与重传
-----
## 重传与指数退避
书中这里举了个例子, 发送数据一半把网线拔了. 重传包的发送间隔为 1, 3, 6, 12, 24, 48, 64, 64,.. 显然的指数退避.
*2 的指数退避是基于初始 RTO (Retransmission TimeOut)的. RTO 具体参考`net/ipv4/tcp_input.c` 中的 `tcp_rtt_estimator()`(输出新的 rtt)和 `tcp_set_rto()`():
tcp_rtt_estimator() 中的核心算法是:
/* rtt = 7/8 rtt + 1/8 new */
/* mdev = 3/4 mdev + 1/4 new */
tcp_set_rto() 是:
usecs_to_jiffies((tp->srtt_us >> 3) + tp->rttvar_us);
srtt_us 是 rtt 估计值. rttvar_us 是方差因子.
问题:
1) 那实际上是 1/8*a+d?
2) 我没看到指数退避. 是否就是用的 [Jacobson1990] 的这个 RTO 估计算法, 不用指数退避?
算法中的想法:
1. 平滑化: 过去的 srtt 有 alpha 的权值, 当前 rtt 是 1-alpha 的权值. alpha: 平滑因子: .8-.9
SRTT = ( ALPHA * SRTT ) + ((1-ALPHA) * RTT) => 平滑化在 Linux 中的应用挺多, 还包括 load 的计算.
2. 乘以一个参数并限制在上下界内: beta: delay variance factor 延迟方差因子: 1.3-2.0
RTO = min[UBOUND,max[LBOUND,(BETA*SRTT)]
某个包丢失后(比如包 666), 接收方收到后序非重传的包, 返回的 ACK 值都是丢的包(666); 直到收到重传的包. 而发送方在第三个 ACK=666 之后进入快速重传和快速恢复.
### Ref:
1. [TCP/IP 重传超时--RTO](http://www.orczhou.com/index.php/2011/10/tcpip-protocol-start-rto/)
2. [Linux kernel source](
http://lxr.free-electrons.com/source/net/ipv4/tcp_input.c)
## 拥塞避免
对每个连接维护拥塞窗口 cwnd 和慢启动门限 ssthresh.
1. 初始化: cwnd = 1, ssthresh = 65535
2. output window size <= min(cwnd, receiver's window size)
3. 慢启动
4. 发生拥塞后, ssthresh = max(cwnd/2, 2), cwnd = 1, enter slow start
5. When cwnd >= ssthresh, enter congestion avoidance.
## 快速重传 & 快速恢复
为什么一定要3个重复 ACK 才重传? => 我们不知道包是 1) 丢失了 2) 只是顺序被打乱, 在后面能收到.
快速重传的算法针对3个重复 ACK 的情况, 基本是原算法直接跳过慢启动的过程, cwnd = ssthresh.
前两个重复 ACK 到达时, cwnd 保持不变(对应于书中图21-10 中几段较为平坦的部分). 第三个重复 ACK 到达时, ssthresh被置为 cwnd 的一半, cwnd = ssthresh + 重复 ACK 数量*报文段大小.
平滑化的 RTT, 均值偏差, ssthresh 在 TCP 连接关闭时以路由表项为 key 进行缓存. 下次使用路由表项时拿出来继续用.
-----
## 22. TCP 的坚持定时器
-----
如我们所知, TCP 不为其 ACK 包发送确认. 这可能导致死锁: 在 ACK 包丢失时, 发送方等待这个 ACK 包, 而接收方等待新的数据. 为了阻止这种情况, 发送方使用一个坚持定时器(persistent timer) 周期性向接收方查询. 定时器到期后发送放发包, 检查窗口是否增大(再通过返回的 ACK 就能看出当前 ACK 到多少了, 这样之前的 ACK 即使丢包也被弥补了). 这时发送方发出的报文称为窗口探查(window probe), 窗口探查也用的 TCP 的间隔时间指数退避, 60s 上限的做法.
窗口探查说明了 TCP 的 server/client 端除了正经的数据传输外还是有藕断丝连的, 只要一方想绝交(window=0), 另一方就会跑个 loop 去不断打探消息(当然, TCP 连接也是可以完全空白的, 双方建立了连接, 然后啥也不干, probe 都没有).
-----
## 23. TCP 的保活定时器
-----
保活定时器就是针对上述"建立连接后什么也不干"的一个 timeout, 这个 timeout 是系统级的, 作为应用设定 socket timeout 的上界. 书中这个 timeout 是两小时, Linux kernel 中对应 TCP_KEEPALIVE_TIME 也是两小时, TCP_KEEPALIVE_PROBES 指定超时后最多发送9次 probe, 如果都没有相应说明客户机已经关闭并终止连接.
probe 的结果可能有4种: 对端正常, 对端崩溃, 对端崩溃并重启, 无法连接到对端.
-----
## 24. TCP 的未来和性能
-----
首先是路径 MTU. Linux: TCP_MIN_MSS: 88, TCP_BASE_MSS: 1024 如果当前报文需要分片的话, 通过返回的 icmp: need to frag 报文通知. 一个发现是, 并非分组越大效率越高, 因为路由器是存储转发设备, 在完整地接受分组之后才能转发, 而大的分组+低速链路使得接受分组的时间过长, 分组在每个路由器上都会卡一下, 导致总传输时间变长.
这里举了个例子: 通过4个路由器发送8192字节, 链路速度为1544000b/s:
1) 使用4096字节的分组: 每一跳速度: (4096 + 40) * 8 / 1544000 = 21.4ms, 总共3跳两个分组: 21.4 * (3 + (2 -1)) = 85.6ms.
2) 使用 512字节的分组: 每一跳: (512 + 40) * 8 / 154000b/s = 2.9ms, 总共3跳16个分组: 2.9 * (3 + (16 - 1)) = 52.2ms, 小的分组反而快些.
然后讨论长肥管道. 长指的是时延长, 肥指的是带宽高.
-----
## 29/30. 网络文件系统/其他 TCP/IP 应用
-----
NFS 的 client/server 端实现都放在内核里, 为了减少 context switch 的开销.
写这本书的时候还有 whois/finger 等专门的应用, 占了低端口. 现在看来 HTTP 基本做到了通用, 把这些东西替代了. 通用接口效率低, 但随着时间带宽在提升, 随着带宽效率在提升. Archie/WAIS/Gopher/Veronica 这类索引/白页就是以后 WWW 需要搜索引擎的预兆. 现在入口从 web 转向移动应用, 我的理解是 web 是一手信息, 多, 杂; App 上的内容是直接交互/二手筛选的, 暂时还看不到一个新索引工具的必要.
有关键情节透露