详解 TCP IP 网络协议栈底层原理到徒手实现
TCP/IP网络协议栈分为四层, 从下至上依次是:
链路层其实在链路层下面还有物理层, 指的是电信号的传输方式, 比如常见的双绞线网线, 光纤, 以及早期的同轴电缆等, 物理层的设计决定了电信号传输的带宽, 速率, 传输距离, 抗干扰性等等。在链路层本身, 主要负责将数据跟物理层交互, 常见工作包括网卡设备的驱动, 帧同步(检测什么信号算是一个新帧), 冲突检测(如果有冲突就自动重发), 数据差错校验等工作。链路层常见的有以太网, 令牌环网的标准。
网络层网络层的IP协议是构成Internet的基础。该层次负责将数据发送到对应的目标地址, 网络中有大量的路由器来负责做这个事情, 路由器往往会拆掉链路层和网络层对应的数据头部并重新封装。IP层不负责数据传输的可靠性, 传输的过程中数据可能会丢失, 需要由上层协议来保证这个事情。
传输层网络层负责的是点到点的协议, 即只到某台主机, 传输层要负责端到端的协议, 即要到达某个进程。典型的协议有TCP/UDP两种协议, 其中TCP协议是一种面向连接的, 稳定可靠的协议, 会负责做数据的检测, 分拆和重新按照顺序组装, 自动重发等。而UDP就只负责将数据送到对应进程, 几乎没有任何逻辑, 也就是说需要应用层自己来保证数据传输的可靠性。
应用层即我们常见的HTTP, FTP协议等。
这四层协议对应的数据包封装如下图:
四层协议对应的通信过程如下图:
链路层 以太网数据帧
以太网数据帧格式如下:
说明如下:
目的地址和源地址是指网卡的硬件地址(即MAC地址), 长度是48位, 出厂的时候固化的。
类型字段即上层协议类型, 目前有三种值: IP, ARP, RARP。
数据对应了上层协议传输的数据, 以太网规定数据大小是46~1500字节, 最大值1500即以太网的最大传输单元(MTU), 不同网络类型有不同MTU, 如果需要跨不同类型链路传输的话, 就需要对数据进行重新分片。
CRC是数据的校验码, 确保数据传输正确
ARP协议
在网络通信过程中, 源主机的应用程序只知道目的应用程序的IP地址, 并不知道对方主机的硬件地址, 所以在数据发送之前, 需要先找到目标及其的硬件地址, 这就是ARP协议所起的作用了。
每次在建立连接之前, 会在本地网络广播发送目的IP地址, 所有机器都会受到该请求, 目的机器发现该请求中的IP地址跟自己一样, 就把自己的硬件地址返回回去, 否则忽略该请求。
一般来说, 每台机器都维护的有一个ARP缓存表, 存储了近期的IP地址和硬件地址的映射关系, 可以用arp -a命令来查看缓存表中内容。
如果目的机器和本机器不在同一个网段之内的话, 会将数据发送给网关来处理, 一般网关就是路由器, 此时网关会进行IP路由, 将ARP请求发送到目的网络地址, 然后再依次将应答返回给该发起请求的机器。
【文章福利】小编推荐自己的Linux、C/C++技术交流群:【960994558】整理了一些个人觉得比较好的学习书籍、视频资料共享在里面(包括C/C++,Linux,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK等等.),有需要的可以自行添加哦!~
IP协议
IP协议数据包格式如下:
几个字段解释如下:
TOS, 一共有8位, 其中3位用来表示该数据包的优先级, 目前已经不用; 还有4位表示可选的服务类型(最小延迟, 最大吞吐, 最大可靠性, 最低成本), 还有一位总是0;
标志位: 用来对每个IP包的分片关系进行标识, 用于分片和重新组装数据包;
TTL(Time To Live), 是指一个数据包在网络上的最多经过多少次转发, 如果超过该数字, 就丢弃该包
8位协议, 上层可选协议为: TCP, UDP, ICMP, IGMP
IP地址的一共分为如下几类:
在互联网刚出来的时候, 大部分组织都申请的B类网络地址, 导致B类地址很快就用完了, 但是A类又有很多空闲的地址, 而每个路由器又必须掌握所有网络的信息, 随着C类网络的增多, 路由器中的路由表项数也就越来越多了。
针对这种情况, 后来人们发现, 绝大部分内部网络的机器都不需要一个独立的公网IP的, 这些机器通过一个公网IP跟外部连接, 在自己的网络内部为每台机器申请一个私有IP, 内部再建设一个路由器, 做内网IP地址的定位即可。
私有IP的出现大大解决了IP浪费的问题, 所以我们日常中可以看到很多如192.168.xx这样的IP, 这些IP都只是局域网内部IP, 不会浪费IP地址。
于是, RFC1918就规定了组建局域网的私有IP地址规范:
10.*, 前面8为是网络号, 共16,777,216个私有IP
172.16.*到172.31.*, 共1,048,576个私有IP
192.168.*, 共65536个私有IP
这些私有IP地址虽然没有公网IP, 但是仍然可以通过NAT等技术来跟公网进行连接交互。
除了私有IP之外, 还有几种特殊的IP地址:
127.*的IP地址用于本机环回测试, 这类地址的交互数据不会过网卡, 直接在内核过一遍协议就完成交互了
255.255.255.255, 这是个特殊IP, 代表在本地路由广播
主机号部分全是0的地址代表一个网络, 而不能代表某个主机(比如不能用192.168.0.0作为某台机器的IP)
主机号部分全是1的地址代表在该网络内部广播
TCP协议
数据包格式
TCP协议数据包如下:
部分字段解释如下:
源端口号和目的端口号: 用来标注数据交互双方进程
32位序号和32位确认序号: TCP是一个可靠的交互协议, 这两个序号用做传输过程中数据的标记, 保证数据的传输顺序以及重发
URG/ACK/PSH/RST/SYN/FIN: 用来标记该请求包位于TCP连接中的什么阶段, 这6个字段下面会详细解释
交互过程
上图中每次连接线上的数字标记了此次数据包中的关键信息, 比如
SYN,1000(0),<mss 1460>代表: 请求包包含SYN标记, 32位序号为1000, 不包含数据, 带有一个mss的选项, 其值为1460
SYN,8000(0),ACK,1001,<mss 1024>代表: 请求包包含SYN和ACK标记, 32位序号为8000, 不会包含数据, 32位确认序号为1001, 同样带有mss选项
那么接下来我们看TCP协议的交互过程:
建立连接客户端发送包1, SYN代表请求建立连接, 第一个包序号为1000, 该序号的大小由操作系统内核维护, 每次发送都会自增, 自增数值就是发送的字节数, 其中mss选项代表最大段尺寸, 这是为了避免不必要的底层协议的拆包解包;服务器返回包2, 包含的ACK 1001, 代表小于1001序号的包我都收到了, 下次请求发送大于等于1001包; 在该包中同时包含SYN 8000(0), 这段跟客户端交互的时候一样, 只是服务器端这头的序号为8000;客户端返回包3, 里面只包含ACK 8001的包, 代表收到服务器的建立连接的包了至此, 连接建立完毕, 可以发送数据了, 该过程包含了客户端和服务器各一次请求和应答, 服务器的请求和应答放到一个包中做了, 一共包含3次包发送, 所以该过程又被称为三次握手。
交换数据客户端发送包4, 包含ACK 8001, 以及序号从1001~1020的20个字节的数据服务器返回包5, 包含ACK 1021(因为包含20个字节), 以及序号从8001~8010的10个字节数据客户端返回包6, 因为数据已经交互完毕, 所以只包含一个ACK 8011这一段主要是要理解TCP交互的序号管理逻辑, 因为是全双工协议, 即服务器和客户端可以同时像对方发送数据, 所以需要客户端和服务器各维护一个序列号。如果是半双工协议的话, 就只需要一方维护一个序号即可。
关闭连接客户端发送包7, 包含FIN标记, 1021服务器返回包8, 只是应答ACK 1022服务器再次返回包9, 包含FIN标记, 8011序列客户端返回包10, 包含ACK 8012在建立连接的时候, 服务器的请求和应答是合并到了一个包当中。但是在关闭连接的过程中, 就必须分开两个包来, 因为客户端关闭连接之后就不能再发送数据了, 但是服务器还可以发送数据给客户端, 直到服务器也发送FIN标记。
滑动窗口
如上讲的都是一来一回的交互, 一般情况下可能会存在一方数据发得特别快, 另一方数据发得特别慢, 这种时候如果不做控制, 势必会让慢的这方数据处理不过来从而导致丢包。
TCP协议中采用了滑动窗口协议来解决该问题, 类似上面的mss, 再增加一个新的选项win, 告诉对方自己的滑动窗口大小, 对方在发送数据的时候每次发送数据就知道对方到底窗口空间还够不够, 如果不够了就不发了, 从而解决了一快一慢这种问题。
连接状态
如下图:
其他状态都还好, 在工作中常会碰到TIME_WAIT连接过多的问题, 这里把TIME_WAIT状态单独拿出来说一下。
TIME_WAIT是主动关闭方在收到被动关闭方发的FIN包之后处于的状态, 这个包是主动关闭方收到的最后一个包了, 在收到这个包之后还不能直接就把连接给关闭了, 还得等待一段时间才能关闭, 等待时间为2MSL。
为什么要等待一段时间呢? 主要是两个原因:
在收到最后一个包之后主动关闭方还得发一个ACK回去, 这个ACK可能会丢包, 如果丢包, 对方还需要重新发最后一个FIN包, 如果收到重新发过来的FIN包的时候这边厢链接已经关闭, 则会导致链接异常终止;
不过第1点也不会造成太大的问题, 毕竟数据已经正常交互了。但是有另外一点风险更高, 就是如果不等待2MSL的话, 那么如果正好一个新链接又建立在相同的端口上, 那么上次的FIN包可能因为网络原因而延时迷途的包这个时候才送达该端口, 导致下一次连接出现问题;
所以一定要有一个TIME_WAIT的状态等待一段时间, 等待的MSL时间RFC上面建议是2分钟, 但是笔者实际工作中测试往往是30秒。
但是如果你的服务是一个高并发短连接服务, TIME_WAIT可能会导致连接句柄被大量占用, 而你又相信服务内部是一个非常稳定的网络服务, 或者即使有两个连接交互出现故障也可以接受或者有应用层处理, 不希望有那么多的TIME_WAIT状态的连接, 一般有两种方式:
在建立连接的时候使用SO_REUSEADDR选项
在/etc/sysctl.conf中加入如下内容:net.ipv4.tcp_syncookies = 1net.ipv4.tcp_tw_reuse = 1net.ipv4.tcp_tw_recycle = 1net.ipv4.tcp_fin_timeout = 30然后执行/sbin/sysctl -p生效参数。
UDP协议
UDP协议就简单很多了, 基本上就只包含源地址, 目的地址, 长度, 校验, 数据。
交互过程也不再像TCP这样经过很复杂的建立连接和关闭连接的过程了, 就直接每次都发送数据了, 这样会有如下的一些问题:
发送端只管发送数据, 如果在茫茫路由中该包丢了, 接收端并不知道
发送的多个包中, 在经过不同路由的时候, 可能达到时序跟发送的时候并不一样, 所以接收端可能拿到的是不同顺序的包
如果发送端很快, 而接收端很慢, 接收端处理不过来, 就会丢包
所以如前面所说, UDP协议并不保证数据的可靠性, 他一般用于一些高性能的场景, 且需要应用层再做一些简单的封装处理。
版权声明: 本文为 InfoQ 作者【赖猫】的原创文章。
原文链接:【http://xie.infoq.cn/article/77379eaac00a4ce6eca62092f】。文章转载请联系作者。
评论