全书基于一个简单场景:用户向浏览器输入一条URL到响应返回,一条网络请求的生命周期。

image.png

全书分六部分:

  1. 应用层客户端生成HTTP,委托给操作系统的协议栈
  2. 协议栈(TCP/IP模块)调用网卡驱动生成电信号
  3. 网卡如何通过路由器到达用于接入互联网的路由器
  4. 互联网内部接力传输
  5. 到达web服务器之后先通过防火墙的检查
  6. web服务器是如何收取数据的

第二章主要介绍,操作系统中的协议栈和网卡是如何将应用程序的消息发给服务器的:

  1. 创建套接字
  2. 连接服务器
  3. 收发数据
  4. 从服务器断开连接并删除套接字
  5. IP与以太网的包收发操作
  6. 用UDP收发数据的操作
    本文介绍1至4,以及TCP模块的整个生命周期。

image.png

主要看点如下:

  1. 协议栈的内部结构
  2. 套接字的实体是什么?有什么工具能直接观察到吗?
  3. “连接”的过程中发生了什么?
  4. 收发数据时的具体工作流程。
  5. “断开”的过程中发生了什么?

0. 概述

开始探索之前梳理了几个概念:

协议栈的内部结构;
套接字实体
TCP的生命周期

0.1 协议栈的内部结构

image.png

所谓协议栈分成上下两部分,

  1. 接收应用程序委托收发数据的TCP UDP模块。
  2. 控制网络包发送的IP模块,IP模块包含ICMP协议和ARP协议
  3. 网卡驱动负责控制网卡硬件,用于监听发送网线中的光电信号
浏览器,邮件等一般应用程序一般用TCP
DNS查询等收发较短的控制数据一般用UDP

0.2 套接字的实体

先用 netstat 直观的感受一下
image.png

套接字:协议栈中用于存放控制信息的内存空间
控制信息:协议类型 ip地址 端口号 状态...

套接字中记录了通信双方的信息以及通信所处的状态,
而协议栈是根据套接字中的控制信息来工作的

0.3 TCP 生命周期

rfc793#section-3.2

 一条连接在其生命周期中会经历一系列的状态. 
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 两种版本

创建套接字的工作流程

  1. 向内存管理器申请一块内存 --> malloc()
  2. 初始化控制信息(协议类型 ip地址 端口)
  3. 返回描述符给应用程序,用于唯一标识套接字(控制信息),后续通信应用程序与协议栈交互会带上描述符。

2. 连接服务器

连接是什么

int connect(int sock, struct sockaddr *serv_addr, socklen_t addrlen);
sock        套接字文件描述符
serv_addr    地址族 ip port
addrlen        serv_addr 大小

这个连接的语境是协议栈,因为对于网线而言,信号一直是有的。
在连接阶段,套接字刚创建,对协议栈来说,还不知道通信对象是谁,这个阶段客户端会向服务端传达开始通信的请求,互相交换控制信息。
因此把这个阶段称为准备阶段,也许更加合适。

连接时做了什么:

  1. 应用程序将服务端ip port交给协议栈
  2. 协议栈发起开始通信的请求
  3. 交互控制信息
  4. 开辟收发数据的缓冲区

负责保存控制信息的头部

通信操作中的控制信息分两类:

  1. 客户端与服务器互相联络时交换的控制信息。即各类协议的头部,TCP、IP、MAC。头部用于记录和交换控制信息
  2. 保存于套接字中,用于控制协议栈操作的信息。从应用程序传递来的信息以及从通信对象接收到的信息。

对于套接字中的控制信息,不同协议栈有不同实现,只要通信时按规定生成协议头部即可。

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                              |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

493067477.jpg

控制信息:
1122380328.jpg

通信操作中的控制信息分两类:
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。

应用程序调用协议栈后,并不会立马发送。先存入的套接字对应的收发缓冲区,满足一定条件后再发:

  1. 缓冲区数据长度大于等于MSS再发送
  2. 时间:协议内部有个计时器,经过一定时间就发送

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和窗口给发送方?

一,更新窗口大小的时机?

  1. 接收方从缓存中取出数据并传给应用应用程序
  2. 接收方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,再删除套接字

等待是为了防止误操作,举个简单的例子

  1. 客户端发送 FIN
  2. 服务端返回 ACK
  3. 服务器发送 FIN
  4. 客户端返回 ACK
    若第四步客户端返回的ACK丢失了,服务端会重发FIN,此时客户端新建了具有相同端口号的套接字,收到了服务器重发的FIN之后,进入断开连接阶段。

wzyAcyy
9 声望2 粉丝