网络

网络主机

多数网络应用基于 Client-Server 模型。一个 server 进程管理某类资源,client 进程通过请求让 server 对资源执行操作,然后接收响应。Client 和 server 都是运行在主机上的进程,可以位于同一台主机,也可以位于不同主机。

网络主机由 CPU、主存、I/O bridge、I/O bus、磁盘控制器、图形适配器、USB 控制器和 network adapter 等部件组成。网络适配器连接主机与网络,协议软件运行在内核与用户程序之间,负责把用户数据封装为网络帧并交给硬件发送。

从程序员视角看,网络 I/O 最终通过描述符读写;从硬件视角看,网络数据经由 network adapter 进入主机,再通过中断、DMA 等机制交给操作系统处理。

局域网与 Ethernet

计算机网络是按地理邻近性组织的层次系统,由通信链路和网络设备构成。底层通常是 LAN(Local Area Network),覆盖一个房间、楼层、建筑或校园。常见 LAN 技术是 Ethernet。

Ethernet segment 由主机、双绞线和 switch 组成。主机发送的数据按 frame 传输:

1
Ethernet frame = frame header + payload

Frame header 至少包含:

  • 源 MAC 地址。
  • 目的 MAC 地址。
  • frame 长度等控制信息。

每块 Ethernet adapter 有唯一 48-bit MAC 地址,例如 00:16:ea:e3:54:e6

switch 如何区分发送方和接收方?

Switch 使用 MAC 地址。发送 frame 时,frame header 中包含 source MAC 和 destination MAC。Switch 根据 destination MAC 决定转发端口。

switch 如何构造 MAC table?

Switch 可通过 source MAC 学习端口映射。若从端口 1 收到源地址为 Mac-A 的 frame,就记录 Mac-A -> port 1。后续收到目的地址为 Mac-A 的 frame 时,可直接转发到端口 1。

如果 switch 尚未建立可用的 MAC table,仍可通过广播方式转发 frame:把收到的 frame 发到除入口外的其他端口,由各主机检查 destination MAC 是否匹配自己。该方法能工作,但会增加无关主机收到的 frame 数量,效率较低。MAC table 的作用是把广播式转发优化为定向转发。

只用 MAC 地址能否支撑大规模网络?

不适合。MAC 地址是网卡制造时确定的平面标识,不体现网络位置。若网络规模达到较多主机,路由设备难以用平面 MAC 地址表完成可扩展转发。因此需要具有层次结构的网络地址,即 IP 地址。

IP 地址在主机接入网络时由网络配置分配,能反映当前所在网络的前缀;MAC 地址随网卡固定,不随物理位置变化。路由器可以按 IP 前缀聚合路由项,而不必记录每个网卡的全局位置。

子网、IP 地址与路由

网络规模达到较多主机,路由设备难以用平面 MAC 地址表完成可扩展转发,因此需要进行划分。IP 地址用于描述主机在网络层的位置。处于同一 subnet 的主机通常具有相同地址前缀,例如:

1
2
3
202.120.40.1
202.120.40.2
202.120.40.3

路由器根据 IP 地址前缀转发 packet。Subnet 使 routing table 可按前缀聚合,避免为每台主机维护独立路由。

MAC 地址和 IP 地址的分工:

地址 层次 作用
MAC address 数据链路层 标识同一链路上的网卡
IP address 网络层 标识主机或路由接口在 IP 网络中的位置

发送数据时,IP header 描述端到端的源 IP 和目的 IP;MAC header 描述当前链路上的下一跳。

同子网与跨子网转发

设主机 A 为 202.120.40.1 / Mac-A,主机 C 为 202.120.40.2 / Mac-C,主机 X 为 202.120.41.1 / Mac-X,路由器在 A 所在子网的接口为 202.120.40.255 / Mac-R1,在 X 所在子网的接口为 202.120.41.255 / Mac-R2

A 发送给同子网主机 C

A 使用子网掩码判断 C 与自己在同一子网。Frame 的目的 MAC 是 C 的 MAC。

1
2
Mac Header: Src = Mac-A, Dst = Mac-C
IP Header: Src = 202.120.40.1, Dst = 202.120.40.2

A 发送给跨子网主机 X

A 使用子网掩码判断 X 不在本子网,因此把 frame 发给默认网关。第一跳目的 MAC 是路由器接口 Mac-R1,但 IP header 的目的 IP 仍是 X。

1
2
3
4
5
6
7
Host A -> Router R1
Mac Header: Src = Mac-A, Dst = Mac-R1
IP Header: Src = 202.120.40.1, Dst = 202.120.41.1

Router R2 -> Host X
Mac Header: Src = Mac-R2, Dst = Mac-X
IP Header: Src = 202.120.40.1, Dst = 202.120.41.1
  • IP header 在端到端路径上保持源 IP 和目的 IP。
  • MAC header 只在单条链路上有效,每经过一个 router 都会更新。
  • 主机通过 subnet mask 判断目的主机是否在本子网。
  • 主机通过 gateway 找到跨子网转发的下一跳。

应用只给出目的 IP,如何得到下一跳 MAC?

使用 ARP(Address Resolution Protocol)。在同一链路内,主机可通过 ARP 查询某个 IP 对应的 MAC 地址。若目的 IP 不在本子网,则查询 gateway IP 对应的 MAC 地址。

ARP 的基本流程:发送方在本子网广播“谁拥有某个 IP”,拥有该 IP 的主机返回自己的 MAC 地址。发送方记录 IP -> MAC 映射后,再构造 Ethernet frame。ARP 只能在本链路广播范围内解析地址,因此跨子网时解析的是下一跳 gateway 的 MAC,而不是最终目的主机的 MAC。

Internet Protocol

多个 LAN 可通过 router 连接成 internet。IP 协议提供两个核心功能:

  • 命名机制:定义统一的 host address 格式。
  • 传输机制:定义标准传输单元 packet。

IP packet 由 header 和 payload 组成:

1
IP packet = IP header + payload

IP header 包含 packet 大小、源地址、目的地址等信息。Payload 存放上层协议数据。

在不同 LAN 之间转发时,IP packet 会被封装进当前 LAN 的 frame 中。Router 收到 frame 后去掉当前链路层 header,根据 IP header 查路由,再封装为下一条链路的 frame。

1
2
Host A --LAN1 frame--> Router --LAN2 frame--> Host B
IP packet 在路径中继续承载端到端信息

NAT

IPv4 地址为 32 位,地址数量有限。局域网常使用私有地址,例如 192.168.1.10。不同局域网可以使用相同私有地址,因为这些地址不直接作为公网唯一地址使用。

NAT(Network Address Translation)在私有地址和公网地址之间转换。

SNAT

SNAT 修改 packet 的 source address,常用于内网主机访问公网。

1
2
3
4
5
6
7
Before NAT:
Src = 192.168.1.10:10086
Dst = 100.101.102.1:80

After SNAT:
Src = 202.120.40.82:233
Dst = 100.101.102.1:80

NAT 设备维护映射表:

Local Global
192.168.1.10:10086 202.120.40.82:233
192.168.1.11:10086 202.120.40.82:234

返回 packet 到达 NAT 设备后,NAT 根据 global port 找回 local address 和 local port。

DNAT

DNAT 修改 packet 的 destination address,常用于把内网服务暴露给公网。

Public port Local service
8080 192.168.1.10:10086

公网访问 202.120.40.82:8080 时,NAT 可把目的地址改写为 192.168.1.10:10086

协议栈

TCP/IP 协议族是 Global IP Internet 的基础。

协议 基于 提供的抽象
UDP IP 不可靠 datagram,从进程到进程
TCP IP 可靠 byte stream,从进程到进程

应用通过 Berkeley sockets interface 访问网络。Socket I/O 与 Unix file I/O 使用相同的描述符抽象,区别在于 socket descriptor 的打开方式不同。

可把网络协议栈理解为四层:

层次 主要内容 作用
Link layer Ethernet、MAC、switch 在同一链路或局域网内传 frame
Network layer IP、router 通过 IP 地址和路由完成 host-to-host delivery
Transport layer TCP、UDP 在进程之间提供传输抽象
Application layer HTTP、FTP、SMTP 等 由应用约定 request/response 的格式和语义

IP 只提供命名和尽力而为的转发。TCP 在 IP 之上处理丢包、乱序和重传,为应用提供可靠、有序的 byte stream;UDP 更轻量,不提供同等可靠性保证,应用可自行处理丢包和顺序问题。

程序员视角下的 Internet

程序员通常使用三类标识:

  • 32-bit IP address,例如 0xca7828bc
  • Domain name,例如 ipads.se.sjtu.edu.cn
  • Socket address,即 IP address:port

IP 地址通常写为 dotted decimal notation。每个字节转为十进制并用点分隔,例如:

1
0xca7828bc = 202.120.40.188

IP 地址在内存中按 network byte order 存储,即 big-endian byte order。

DNS

DNS(Domain Name System)是在 Internet 中维护 domain name 与 IP address 映射的分布式数据库。

DNS host entry 可视为 domain name 和 IP address 的等价类。常见映射关系包括:

类型 含义 示例
1-1 一个域名对应一个 IP whaleshark.ics.cs.cmu.edu -> 128.2.210.175
M-1 多个域名对应同一 IP cs.mit.edueecs.mit.edu 可对应同一 IP
M-N 多个域名对应多个 IP twitter.comwww.twitter.com 可对应多个地址

Client 若只知道 domain name,通常先通过 DNS 得到 server IP,再发起连接。

Internet Connection

TCP connection 是两个进程之间的通信通道,具有以下性质:

  • point-to-point:连接一对进程。
  • full-duplex:两个方向可同时传输数据。
  • reliable:源进程发送的数据最终按发送顺序到达目的进程。

Socket 是连接的端点。Socket address 由 IP address 和 port 组成:

1
socket address = IP address : port

Port 是 16-bit 整数,用于标识主机上的进程或服务。Client port 通常是系统自动分配的 ephemeral port;server port 通常是 well-known port,例如 Web server 使用 port 80,SMTP 使用 port 25。

一条连接由两个端点唯一确定:

Client-Server 通信

Socket 接口

Client 和 server 通过 socket descriptor 使用 Unix I/O 读写网络。

封装函数:

  • open_clientfd(host, port):client 端创建连接。
  • open_listenfd(port):server 端创建监听描述符。

通信流程:

flowchart TD
    subgraph Client[客户端 Client]
        C1[getaddrinfo<br/>获取服务器地址信息]
        C2[socket<br/>创建客户端套接字]
        C3[connect<br/>向服务器发起连接请求]
        C4[writen<br/>发送请求数据]
        C5[readline<br/>读取服务器响应]
        C6{是否继续通信?}
        C7[close<br/>关闭连接]
    end

    subgraph Server[服务器 Server]
        S1[getaddrinfo<br/>获取本机监听地址信息]
        S2[socket<br/>创建监听套接字]
        S3[bind<br/>绑定 IP 和端口]
        S4[listen<br/>进入监听状态]
        S5[accept<br/>等待并接受客户端连接]
        S6[readline<br/>读取客户端请求]
        S7[writen<br/>发送响应数据]
        S8{客户端是否关闭?}
        S9[readline 读到 EOF]
        S10[close<br/>关闭本次连接]
    end

    C1 --> C2 --> C3
    S1 --> S2 --> S3 --> S4 --> S5

    C3 -- connection request --> S5

    S5 --> S6
    C3 --> C4

    C4 -- 请求数据 --> S6
    S6 --> S7
    S7 -- 响应数据 --> C5

    C5 --> C6
    C6 -- 是 --> C4
    C6 -- 否 --> C7

    C7 -- EOF --> S9
    S8 -->|是| S9
    S9 --> S10
    S10 --> S5

Echo Client

Echo client 从标准输入读取一行,写入 server,再读取 server 返回并输出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
int main(int argc, char **argv)
{
int clientfd;
char *host, *port, buf[MAXLINE];
rio_t rio;

if (argc != 3) {
/* client 需要服务器主机名和端口号两个参数。 */
fprintf(stderr, "usage:%s <host> <port>\n", argv[0]);
exit(0);
}

host = argv[1];
port = argv[2];
/* 建立到服务器的 TCP 连接,返回已连接 socket 描述符。 */
clientfd = open_clientfd(host, port);
/* 将 clientfd 绑定到 RIO 缓冲区,便于按行读取响应。 */
Rio_readinitb(&rio, clientfd);

while (Fgets(buf, MAXLINE, stdin) != NULL) {
/* 把用户输入的一行发送给服务器。 */
Rio_writen(clientfd, buf, strlen(buf));
/* 从服务器读取一行响应。 */
Rio_readlineb(&rio, buf, MAXLINE);
Fputs(buf, stdout);
}

/* 输入结束后关闭连接。 */
Close(clientfd);
}

open_clientfd 的核心步骤:

  1. getaddrinfo(hostname, port, &hints, &listp) 取得候选 server address。
  2. 遍历候选 address。
  3. 对每个 address 调用 socket 创建 socket descriptor。
  4. 调用 connect 连接 server。
  5. 若连接失败,关闭 descriptor 并尝试下一个 address。
1
2
3
4
/* 创建 socket,协议族、socket 类型和协议参数来自 getaddrinfo 结果。 */
clientfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol);
/* 主动连接服务器地址;成功后 clientfd 成为已连接描述符。 */
connect(clientfd, p->ai_addr, p->ai_addrlen);

connect 会阻塞 client,直到连接建立或失败。连接建立后,client 可对 clientfd 使用普通 Unix I/O。

getaddrinfo 中常见 flag:

flag 作用
AI_NUMERICSERV 要求 service 参数是数字端口
AI_ADDRCONFIG 按本机网络配置返回 IPv4 / IPv6 地址

Echo Server

Iterative echo server 的主循环:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int main(int argc, char **argv)
{
int listenfd, connfd;
socklen_t clientlen;
struct sockaddr_storage clientaddr;
char client_hostname[MAXLINE], client_port[MAXLINE];

/* 创建监听描述符,绑定到 argv[1] 指定端口。 */
listenfd = Open_listenfd(argv[1]);
while (1) {
clientlen = sizeof(struct sockaddr_storage);
/* 阻塞等待客户端连接;成功后返回连接描述符 connfd。 */
connfd = accept(listenfd, (SA *)&clientaddr, &clientlen);
/* 将客户端 socket 地址转换为主机名和端口字符串。 */
getnameinfo((SA *)&clientaddr, clientlen,
client_hostname, MAXLINE,
client_port, MAXLINE, 0);
printf("Connected to (%s, %s)\n", client_hostname, client_port);
/* 在当前进程中服务该连接;此迭代期间不能接受其他连接。 */
echo(connfd);
Close(connfd);
}
}

Server 使用两个不同的 descriptor:

descriptor 含义
listenfd listening descriptor,用于接收连接请求
connfd connected descriptor,用于和某个 client 通信

open_listenfd 的核心步骤:

  1. getaddrinfo(NULL, port, &hints, &listp) 取得可用于监听的本机地址。AI_PASSIVE 表示返回 server listening socket 可绑定的地址。
  2. socket 创建 descriptor。
  3. setsockopt(..., SO_REUSEADDR, ...) 避免重启 server 时常见的 Address already in use
  4. bind 把 socket descriptor 绑定到地址和端口。
  5. listen 把 descriptor 转为 listening socket。
  6. 主循环中调用 accept 等待连接请求。
1
2
3
4
5
6
/* 将监听 socket 绑定到本地 IP/端口。 */
bind(listenfd, p->ai_addr, p->ai_addrlen);
/* 将主动 socket 转为监听 socket,并设置等待队列长度。 */
listen(listenfd, LISTENQ);
/* 从等待队列中取出一个连接,返回专门用于通信的 connfd。 */
connfd = accept(listenfd, (SA *)&clientaddr, &clientlen);

listen 的 backlog 参数提示内核可排队的未完成连接请求数量。accept 阻塞等待连接请求,成功后返回 connected descriptor,并填充 client address。

为什么 accept 返回的通常不是 listenfd

listenfd 只负责监听连接请求,可以长期存在并被反复传给 accept。一次 accept 成功后,内核会新建一个 connected descriptor,例如普通进程已占用 0/1/2open_listenfd 得到 listenfd = 3,第一次 accept 常返回最低空闲描述符 4

后续读写 client 数据应使用 connfd,不能使用 listenfdlistenfd 留给 server 主循环继续接收新的连接请求;connfd 表示本次 TCP connection 的一个端点。

Echo 函数用 Robust I/O 逐行读取,直到遇到 EOF:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void echo(int connfd)
{
size_t n;
char buf[MAXLINE];
rio_t rio;

/* 为连接描述符建立带缓冲输入状态。 */
Rio_readinitb(&rio, connfd);
/* 按行读取客户端数据;返回 0 表示客户端关闭写端并产生 EOF。 */
while ((n = Rio_readlineb(&rio, buf, MAXLINE)) != 0) {
printf("server received %d bytes\n", n);
/* 将收到的同一行原样写回客户端。 */
Rio_writen(connfd, buf, n);
}
}

EOF 是连接状态,不是一个数据字节。通常由 client 调用 close(clientfd) 触发。

HTTP

Web 事务流程

HTTP(HyperText Transfer Protocol)运行在 TCP 之上。典型 Web 事务流程:

  1. Client 和 server 建立 TCP connection。
  2. Client 发送 HTTP request。
  3. Server 返回 HTTP response 和 content。
  4. 双方关闭连接或复用连接。
1
2
3
IP   -> datagrams
TCP -> streams
HTTP -> web content

HTTP response 返回的 content 是字节序列,并带有 MIME type。

常见 MIME type:

MIME type 内容
text/html HTML page
text/plain plain text
application/postscript PostScript document
image/gif GIF image
image/jpg JPG image

Web content 可分为:

  • static content:存储在文件中,请求到达时读取文件返回,例如 HTML、图片、音频。
  • dynamic content:请求到达时由 server 执行程序生成,例如 CGI 程序。

URL

URL(Uniform Resource Locator)标识 server 管理的资源。

静态内容示例:

1
2
http://ipads.se.sjtu.edu.cn:80/courses/ics/index.shtml
http://ipads.se.sjtu.edu.cn/courses/ics/

动态内容示例:

1
http://www.cs.cmu.edu:8000/cgi-bin/adder?15000&213

Client 使用 URL prefix 推断:

  • 使用何种协议,例如 HTTP。
  • Server 域名或 IP。
  • Server port,例如 80。

Server 使用 URL suffix 判断请求资源:

  • 是否为 static content 或 dynamic content。
  • 文件系统路径。
  • 若 suffix 为 /,通常扩展到默认主页,例如 index.html

约定上,动态可执行程序常放在 cgi-bin 目录中。

HTTP Request

HTTP request 由 request line、若干 request header 和空行组成:

1
2
3
<method> <uri> <version>
<header name>: <header data>

示例:

1
2
3
GET /courses/ics/index.shtml HTTP/1.1
host: ipads.se.sjtu.edu.cn

字段含义:

字段 含义
method 请求方法,例如 GETPOSTOPTIONSHEADPUTDELETETRACE
uri 资源标识;对 proxy 常是完整 URL,对 server 常是 URL suffix
version HTTP 版本,例如 HTTP/1.0HTTP/1.1

常见方法:

  • GET:获取 static 或 dynamic content。动态参数通常放在 URI 中。
  • POST:把参数放在 request body 中,请 server 接收并处理。

HTTP/1.1 请求通常需要 Host header。

HTTP Response

HTTP response 由 response line、若干 response header、空行和 response body 组成:

1
2
3
4
5
6
7
HTTP/1.1 200 OK
Server: nginx/1.0.4
Date: Thu, 29 Nov 2012 10:15:38 GMT
Content-Type: text/html
Content-Length: 11560

<!DOCTYPE html ...>

Response line 格式:

1
<version> <status code> <status msg>

常见状态码:

状态码 含义
200 OK 请求成功
301 Moved 提供替代 URL
403 Forbidden server 没有权限访问该文件
404 Not Found server 找不到该文件
501 Not Implemented server 不支持该 request method
505 HTTP Version Not Supported server 不支持该 HTTP 版本

常见 response header:

  • Content-Type:response body 的 MIME type。
  • Content-Length:response body 的字节长度。

HTTP 事务示例:

可用 telnet 手工发起 HTTP 请求:

1
2
3
4
5
6
7
8
unix> telnet ipads.se.sjtu.edu.cn 80
Trying 202.120.40.85...
Connected to ipads.se.sjtu.edu.cn.
Escape character is '^]'.

GET /courses/ics/index.shtml HTTP/1.1
host: ipads.se.sjtu.edu.cn

流程解析:

  1. telnet 建立到 ipads.se.sjtu.edu.cn:80 的 TCP connection。
  2. Client 输入 request line。
  3. Client 输入 Host header。
  4. 空行结束 request header。
  5. Server 返回 response line、headers、空行和 body。