全书基于一个简单场景:用户向浏览器输入一条URL到响应返回,一条网络请求的生命周期。
全书分六部分:
- 应用层客户端生成HTTP,委托给操作系统的协议栈
- 协议栈(TCP/IP模块)调用网卡驱动生成电信号
- 网卡如何通过路由器到达用于接入互联网的路由器
- 互联网内部接力传输
- 到达web服务器之后先通过防火墙的检查
- web服务器是如何收取数据的
第二章主要介绍,操作系统中的协议栈和网卡是如何将应用程序的消息发给服务器的:
- 创建套接字
- 连接服务器
- 收发数据
- 从服务器断开连接并删除套接字
- IP与以太网的包收发操作
- 用UDP收发数据的操作
本文介绍1至4,以及TCP模块的整个生命周期。
主要看点如下:
- 协议栈的内部结构
- 套接字的实体是什么?有什么工具能直接观察到吗?
- “连接”的过程中发生了什么?
- 收发数据时的具体工作流程。
- “断开”的过程中发生了什么?
0. 概述
开始探索之前梳理了几个概念:
协议栈的内部结构;
套接字实体
TCP的生命周期
0.1 协议栈的内部结构
所谓协议栈分成上下两部分,
- 接收应用程序委托收发数据的TCP UDP模块。
- 控制网络包发送的IP模块,IP模块包含ICMP协议和ARP协议
- 网卡驱动负责控制网卡硬件,用于监听发送网线中的光电信号
浏览器,邮件等一般应用程序一般用TCP
DNS查询等收发较短的控制数据一般用UDP
0.2 套接字的实体
先用 netstat 直观的感受一下
套接字:协议栈中用于存放控制信息的内存空间
控制信息:协议类型 ip地址 端口号 状态...
套接字中记录了通信双方的信息以及通信所处的状态,
而协议栈是根据套接字中的控制信息来工作的
0.3 TCP 生命周期
一条连接在其生命周期中会经历一系列的状态.
LISTEN, SYN-SENT, SYN-RECEIVED, ESTABLISHED,
FIN-WAIT-1, FIN-WAIT-2, CLOSE-WAIT, CLOSING, LAST-ACK, TIME-WAIT,
CLOSED.
CLOSED 是虚拟的一种状态。在状态机上表示连接不存在。
TCB: transmission control block, 通信控制块,即套接字中保存的通信信息
TCP 状态机:
+---------+ ---------\ active OPEN
| CLOSED | \ -----------
+---------+<---------\ \ create TCB
| ^ \ \ snd SYN
passive OPEN | | CLOSE \ \
------------ | | ---------- \ \
create TCB | | delete TCB \ \
V | \ \
+---------+ CLOSE | \
| LISTEN | ---------- | |
+---------+ delete TCB | |
rcv SYN | | SEND | |
----------- | | ------- | V
+---------+ snd SYN,ACK / \ snd SYN +---------+
| |<----------------- ------------------>| |
| SYN | rcv SYN | SYN |
| RCVD |<-----------------------------------------------| SENT |
| | snd ACK | |
| |------------------ -------------------| |
+---------+ rcv ACK of SYN \ / rcv SYN,ACK +---------+
| -------------- | | -----------
| x | | snd ACK
| V V
| CLOSE +---------+
| ------- | ESTAB |
| snd FIN +---------+
| CLOSE | | rcv FIN
V ------- | | -------
+---------+ snd FIN / \ snd ACK +---------+
| FIN |<----------------- ------------------>| CLOSE |
| WAIT-1 |------------------ | WAIT |
+---------+ rcv FIN \ +---------+
| rcv ACK of FIN ------- | CLOSE |
| -------------- snd ACK | ------- |
V x V snd FIN V
+---------+ +---------+ +---------+
|FINWAIT-2| | CLOSING | | LAST-ACK|
+---------+ +---------+ +---------+
| rcv ACK of FIN | rcv ACK of FIN |
| rcv FIN -------------- | Timeout=2MSL -------------- |
| ------- x V ------------ x V
\ snd ACK +---------+delete TCB +---------+
------------------------>|TIME WAIT|------------------>| CLOSED |
+---------+ +---------+
结合TCP的生命周期分析连接的创建、连接、收发、断开流程。
1. 创建套接字
int socket(int af, int type, int protocol);
af:地址族,即IP地址类型,常用的有 AF_INET 和 AF_INET6
AF_INET 代表 IPv4,例如 127.0.0.1
AF_INET6 代表 IPv6,例如 1030::C9B4:FF12:48AA:1A2B
type:数据传输方式/套接字类型,常用的有 SOCK_STREAM,SOCK_STREAM
SOCK_STREAM 流格式套接字/面向连接的套接字
SOCK_DGRAM 数据报套接字/无连接的套接字
protocol:传输协议,常用的有 IPPROTO_TCP 和 IPPTOTO_UDP
IPPROTO_TCP:TCP 传输协议
IPPTOTO_UDP:UDP 传输协议
返回值:描述符
详见 socket()函数详解,介绍了 linux windows 两种版本
创建套接字的工作流程
- 向内存管理器申请一块内存 --> malloc()
- 初始化控制信息(协议类型 ip地址 端口)
- 返回描述符给应用程序,用于唯一标识套接字(控制信息),后续通信应用程序与协议栈交互会带上描述符。
2. 连接服务器
连接是什么
int connect(int sock, struct sockaddr *serv_addr, socklen_t addrlen);
sock 套接字文件描述符
serv_addr 地址族 ip port
addrlen serv_addr 大小
这个连接的语境是协议栈,因为对于网线而言,信号一直是有的。
在连接阶段,套接字刚创建,对协议栈来说,还不知道通信对象是谁,这个阶段客户端会向服务端传达开始通信的请求,互相交换控制信息。
因此把这个阶段称为准备阶段,也许更加合适。
连接时做了什么:
- 应用程序将服务端ip port交给协议栈
- 协议栈发起开始通信的请求
- 交互控制信息
- 开辟收发数据的缓冲区
负责保存控制信息的头部
通信操作中的控制信息分两类:
- 客户端与服务器互相联络时交换的控制信息。即各类协议的头部,TCP、IP、MAC。头部用于记录和交换控制信息
- 保存于套接字中,用于控制协议栈操作的信息。从应用程序传递来的信息以及从通信对象接收到的信息。
对于套接字中的控制信息,不同协议栈有不同实现,只要通信时按规定生成协议头部即可。
TCP头部信息:
原文:https://www.rfc-editor.org/rfc/rfc793.html#section-3.1
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Source Port | Destination Port |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Sequence Number |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Acknowledgment Number |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Data | |U|A|P|R|S|F| |
| Offset| Reserved |R|C|S|S|Y|I| Window |
| | |G|K|H|T|N|N| |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Checksum | Urgent Pointer |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Options | Padding |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| data |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
控制信息:
通信操作中的控制信息分两类:
1. 头部中记录的信息
2. 套接字(协议栈中的内存空间)中记录的信息
可用netstat查看套接字
使用 wireshark(windows) 或 tcpdump(linux) 查看协议头
3.收发数据
ssize_t write(int fd, const void *buf, size_t nbytes);
fd 文件描述符
write() 函数会将缓冲区 buf 中的 nbytes 个字节写入文件描述符 fd 对应的缓冲区中,成功则返回写入的字节数,失败则返回 -1。
应用程序调用协议栈后,并不会立马发送。先存入的套接字对应的收发缓冲区,满足一定条件后再发:
- 缓冲区数据长度大于等于MSS再发送
- 时间:协议内部有个计时器,经过一定时间就发送
MTU = 头部大小(TCP IP头部,一般为40字节)+ MSS
MTU Maximun Transmission unit 最大传输单元
MSS Maximun Segment Size 最大分段大小
长度优先,则吞吐大,延时高
时间优先,则延迟低,吞吐小
<-------- MTU -------->
| IP头 | TCP头 | 数据 |
<- MSS ->
写缓冲,读缓存.
类似的应用场景:
数据库的持久化机制。
reids,mysql 的异步刷盘
kafka 生产者的异步发送等
对较大的数据进行拆分
如果请求的消息过大会被拆分成多个网络包。
由TCP模块添加TCP头部,IP模块添加IP头部和MAC头部。
ACK重传机制
TCP头部中有三个属性用于ACK的重传机制:
序号
ACK号
数据偏移量
序号:可以表示当前网络包数据的第一个字符在整个消息中的位置。
数据偏移量:数据开始的位置
通过TCP包总长度和数据偏移量可以算出数据的长度
ACK号:序号+数据长度
客户端发送数据后,服务端会返回ACK号。如果超过一定时间没收到则重发。
重发几次没有收到ACK包,返回报错给应用程序。
因为TCP模块的重传功能,网卡路由器收到错误的网络包后会直接丢弃。
调整ACK号的超时时间
当网络繁忙时会发生拥塞,ACK返回变慢,如果超时时间较短,会导致频繁的重试加重拥塞。
TCP采用了动态调整超时时间的的方法。如果ACK号返回变慢,相应的延长超时时间,如果ACK号返回快,缩短超时时间
由于计算机的时间精度低,过短的超时时间无法准确的测量。基本会调整到0.5秒至1秒
使用窗口管理ACK号
如果发送网络包后等待ACK号的到达,再发送发一个网络包。在等待ACK的时候什么也做不了,实在是太浪费时间了。
为了提升效率,TCP使用滑动窗口的方式管理数据发送和ACK号。
滑动窗口对应协议栈的收发缓冲区,对应TCP头部的窗口属性,连接双方会交互窗口大小(缓冲区的大小)。
工作方式:
接收方告知发送方,剩余窗口大小。
发送方根据窗口大小连续发送网络包。
接收方将缓冲区的数据处理完毕后,会告诉发送方缓冲区当前剩余的窗口大小。
ACK 与窗口的合并
接收方何时发送ACK和窗口给发送方?
一,更新窗口大小的时机?
- 接收方从缓存中取出数据并传给应用应用程序
- 接收方TCP连接阶段交互窗口大小
二,返回ACK的时机
数据到达接收方,存入缓冲区之后。就可以返回ACK号了。
每收到一个网络包就发送ACK和窗口,会导致网络效率下降。
因此,接收方发送ACK和窗口更新时会等待一段时间,需要连续发送多个ACK号和窗口更新包时发送最后一个ACK号或窗口大小即可。
4.从服务器断开并删除套接字
经过四次挥手连接进入TIME_WAIT状态,等待2MSL后,删除套接字。
MSL 是什么?
MSL:Maximum Segment Lifetime 网络包最长存活时间(见 RFC793)
TCP段在网络系统中存在的时间,定义为2分钟,是个工程经验值。linux通常默认设为30秒(若端口数量为60000,等待时间30秒,则短连接情况下,一个监听端口最大qps为2000)。
为什么需要等2MSL
发送网络包,返回ACK各需要一个MSL
为什么要等2MSL,再删除套接字
等待是为了防止误操作,举个简单的例子
- 客户端发送 FIN
- 服务端返回 ACK
- 服务器发送 FIN
- 客户端返回 ACK
若第四步客户端返回的ACK丢失了,服务端会重发FIN,此时客户端新建了具有相同端口号的套接字,收到了服务器重发的FIN之后,进入断开连接阶段。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。