前言

这个系列的博客是针对阅读《网络是怎样连接的》([日] 户根勤, 译 [中] 周自恒, ISBN: 9787115441249)一书的记录与思路整理。
有些概念是做纯路由交换的网络工程师很少接触但又很重要的部分,因此整理到这里以做分享。

协议栈的内部结构

协议栈的工作我们从表面上是看不见的,可能比较难想象。因此,我们先来对协议栈做个解剖,看看里面到底有什么。

协议栈的内部上图,分为几个部分,分别承担不同的功能。在这张图中,上面的部分会向下面的部分委派工作,下面的部分接收委派的工作并实际执行。

对照上图我们从上到下捋一遍。

最上面的部分是网络应用程序,例如:浏览器、电子邮件客户端、web 服务器、电子邮件服务器等,它们会将收发数据等工作委派给下层来完成。可以说,尽管不同的应用程序收发的数据内容不同,但收发数据的操作是共通的。

应用程序下面是Socket 库,Socket 库中的应用程序,例如gethostbynamesocketconnect等开放给上层应用程序,可以由应用程序直接调用来获取相应的能力。

再下面就是操作系统内部了,其中包括协议栈。
协议栈的上半部分分为两块(OSI 模型的第四层),分别是负责用TCP 协议收发数据的部分和负责用UDP 协议收发数据的部分,它们会接受应用程序的委托执行收发数据的操作。(TCP 与UDP 的对比,不在此处赘述)

再往下是用IP 协议控制网络包收发操作的部分。在互联网上传送数据时,数据会被切分成一个个的网络包(package),而将网络包发送给通信对象的操作就是由IP 来负责的。此外,IP中还包括ICMP协议(用于告知网络包传送过程中产生的相关错误以及各种控制信息)和ARP 协议(根据IP 地址查询相应的以太网MAC 地址)。

IP 下面则是网卡驱动程序负责控制网卡硬件,最下面的网卡则负责完成实际的收发操作,也就是对网线中的信号执行发送和接收的操作。

套接字的实体就是通信控制信息

协议栈内部有一块用于存放控制信息的内存空间,包含IP、Port、Status、Protocol 等信息。我们可以说这些控制信息就是套接字的实体,或者说存放控制信息的内存空间就是套接字的实体。

“这里的控制信息类似于我们在笔记本上记录的日程表和备忘录。我们可以根据笔记本上的日程表和备忘录来决定下一步应该做些什么,同样地,协议栈也是根据这些控制信息来决定下一步操作内容的。”

示例

协议栈在执行操作时需要参阅这些控制信息。

  • 在发送数据时,需要看一看套接字中的通信对象IP 地址和端口号,以便向指定的目标发送数据。
  • 发送数据后,协议栈需要等待对方返回收到数据的响应信息(TCP),数据在传输过程中是有可能丢失的,所以发送方不可能永远等下去,需要在一定时间之后重新发送未收到回应信息的数据,所以我们的套接字(控制信息)中必须要记录是否已经收到响应,以及发送数据后经过了多长时间。协议栈才能根据这些信息执行相应的重发操作。

总结下来就是,套接字中记录了用于控制通信操作的各种控制信息,协议栈需要根据这些信息判断下一步的行动。

Ubuntu 中的套接字

在Ubuntu 中,使用ss命令可以查看套接字的相关信息(还可以继续使用netstat命令)

# 查看所有tcp 协议的套接字
root@VM-20-3-ubuntu:~# ss -atnw
Netid State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
icmp6 UNCONN 0 0 *%eth0:58 *:*
tcp LISTEN 0 4096 0.0.0.0:111 0.0.0.0:*
tcp LISTEN 0 4096 127.0.0.53%lo:53 0.0.0.0:*
tcp LISTEN 0 128 0.0.0.0:22 0.0.0.0:*
tcp LISTEN 0 1024 0.0.0.0:443 0.0.0.0:*
tcp ESTAB 0 0 10.0.20.3:48234 169.254.0.138:8086
tcp ESTAB 0 0 10.0.20.3:22 163.125.203.219:4958
tcp ESTAB 0 0 10.0.20.3:22 163.125.203.219:6081
tcp ESTAB 0 36 10.0.20.3:22 163.125.203.219:5597
tcp ESTAB 0 0 10.0.20.3:35592 169.254.0.55:5574
tcp LISTEN 0 4096 [::]:111 [::]:*
tcp LISTEN 0 1024 [::]:443 [::]:*
  • 第一列为协议
  • 第二列为状态
  • 第三列和第四列为显示排队等待接收和发送的数据量
  • 第五列为本地IP地址和端口组(0.0.0.0 和[::] 代表尚未开始通信的套接字)
  • 第六列为对端IP 地址和端口组(0.0.0.0 和[::] 代表尚未开始通信的套接字)

以第12行为例,一个进程在使用IP地址为10.0.20.3 的网卡与IP地址为163.125.203.219 的对象进行通信,对方使用了5597 端口,我方使用了22端口(是SSH 服务器的默认端口,因为这是在服务器上截取的socket 状态)。因此我们能看出来这个套接字是远端163.125.203.219 连接到本地10.0.20.3 的一个SSH 通道。

调用socket 时的操作(创建套接字)

当一个应用程序调用Socket 库中的socket、connect 等程序组建时,协议栈内部是如何工作的呢?

应用程序调用socket 申请创建套接字,协议栈根据应用程序的申请执行创建套接字的操作:

  1. 协议栈首先会分配用于存放一个套接字所需的内存空间给应用程序。即一个记录套接字控制信息的容器。
  2. 在套接字的内存空间中写入本地初始状态的控制信息。完成该步骤套接字就算创建成功了。
  3. 将表示这个套接字的描述符告知应用程序。描述符相当于用来区分协议栈中的多个套接字的号码牌。
  4. 收到描述符后,应用程序再向协议栈进行收发数据委托时只需要提供这个描述符即可。因为套接字中记录了通信双方的信息和通信处于什么状态,所以只要通过描述符确定了相应的套接字,协议栈就能获取所有的相关信息,这样应用程序就不需要每次都告诉协议栈和谁去通信。

其实很容易理解为何刚刚创建的套接字只有本端信息而没有对端信息。想想服务器上的网络程序,这些程序往往只能等待客户端的连接而无法主动连接客户端,如果设备创建的套接字中有对端的信息,那么很显然服务器会主动连接客户端,这与我们现在使用的C/S 模型是完全不相符的。