第十一章 网络编程
11.1 客户端-服务器编程模型
网络应用都是基于客户端-服务器模型的。采用此模型,一个应用是由一个服务器进程和一个或多个客户端进程组成的。
服务器管理某种资源,并通过操作这种资源来为它的客户端提供服务。
客户端-服务器模型中的基本操作是事务。一个客户端-服务器事务包括以下四步:
- 客户端需要服务时,向服务器发送一个请求,发起一个事务。比如客户端请求下载某个文件。
- 服务器收到请求后,解释它并以适当方式操作自己的资源。比如服务器从磁盘读客户端所请求的文件。
- 服务器给客户端发送一个响应,并等待下一个请求。比如将客户端请求的文件发送给客户端。
- 客户端收到响应并处理它。比如客户端下载收到的文件。
注意客户端和服务器都是进程,两者可以在一台主机上也可以在不同的主机上。
11.2 网络
对主机而言,网络是一种 I/O 设备,是数据源和数据接收方。
一个插到 I/O 总线扩展槽的网络适配器提供了到网络的物理接口。从网络接收到的数据从适配器经过 I/O 和内存总线复制到内存,通常是 DMA 传送。
网络的最底层是 LAN(局域网),以太网是最流行的局域网技术。
在一个以太网中,多台主机通过电缆连接到一个集线器上。
多个以太网可以通过网桥连接起来构成一个桥接以太网。
网络的重要特性:它能由采用完全不同和不兼容技术的各种局域网和广域网组成。
网络协议要具备的两个基本能力:
- 命名机制:提供一种一致的主机地址格式来表示主机地址。
- 传送机制:定义了统一格式的协议数据单元来传送数据。
从客户端 A 发送数据到服务器端 B 的基本步骤
- 主机 A 上的客户端进行一个系统调用,从客户端的虚拟地址空间复制数据到内核缓冲区中。
- 主机 A 上的协议软件通过在数据前附加互联网络包头和 LAN1 帧头,创建了一个 LAN1 的帧,然后传送此帧到适配器。其中 LAN1 帧头寻址到路由器(理解:这应该是指链路层分组的首部),互联网络包头寻址到主机 B(理解:这应该指网络层 IP 数据报的首部)。
- LAN1 适配器把该帧复制到网络上。
- 此帧到达路由器时,路由器的 LAN1 适配器从电缆上读取它,并把它传送到协议软件。
- 路由器从互联网络包头中提取出目的互联网络地址,并用它作为路由表的索引,确定向哪里转发这个包(本例中为 LAN2),路由器剥落掉旧的 LAN1 帧头,加上寻址到主机 B 的 LAN2 帧头,并把得到的帧传送到适配器。
- 路由器的 LAN2 适配器复制该帧到网络上。
- 此帧到达主机 B 时,它的适配器从电缆上读到此帧,并将它传送到协议软件。
- 主机 B 上的协议软件剥落掉包头和帧头。当服务器进行一个读取这些数据的系统调用时,协议软件最终将得到的数据复制到服务器的虚拟地址空间。
总结:以上 8 个步骤可以分为两部分:数据在主机上通过系统调用在进程的虚拟地址空间与内核间传送,数据在网络间传送。
11.3 全球IP因特网
全球 IP 因特网即互联网。
互联网中的每台主机都有实现 TCP/IP 协议的软件。
互联网中的客户端和服务器混合使用套接字接口函数和 Unix I/O 函数来进行通信。通常将套接字函数实现为系统调用,这些系统调用会陷入内核,并调用各种内核模式的 TCP/IP 函数。
11.3.1 IP地址
一个 IP 地址就是一个 32 位无符号整数。网络程序将 IP 地址存放在下面的 IP 地址结构中。
struct in_addr {
uint32_t s_addr;
} // 以网络字节顺序(大端法)存储,即使主机字节顺序是小端法。
TCP/IP 定义的统一的网络字节顺序为大端字节顺序。Unix 提供了几个函数来在网络和主机字节顺序间实现转换
现在的 intel 系统主要都是小端序。
#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong); //返回网络字节顺序
uint16_t htons(uint16_t hostshort); //返回网络字节顺序
uint32_t ntohl(uint32_t netlong); //返回按照主机字节顺序的值
uint16_t ntohs(uint16_t netshort); //返回按照主机字节顺序的值
二进制IP地址与点分十进制串之间的转换
#include <arpa/inet.h>
int inet_pton(AF_INET, const char *src, void *dst); //将点分十进制转换为二进制的网络字节顺序的 IP 地址。成功则返回 1,如果 src 为非法地址则返回 0,出错返回 -1。
const char *inet_ntop(AF_INET, const void*src, char *dst, socklen_t size); //将二进制网络字节顺序的 IP 地址转换为点分十进制,并把得到的字符串最多 size 个字节复制到 dst。
//如果成功返回点分十进制串的指针,若出错返回 NULL。
11.3.2 因特网域名
域名到 IP 地址之间的映射通过分布在世界各地的数据库来维护。
DNS 数据库包含上百万条主机条目结构,每一条定义了一组域名和一组 IP 地址之间的映射。
每台主机都有本地定义的域名 localhost,这个域名总是映射为回送地址 127.0.0.1。
linux>>nslookup localhost // 查看域名 localhost 的地址
Address:127.0.0.1
linux>>hostname // 查看本机的域名
如 http://localhost:7474/browser/
和 127.0.0.1:7474/browser/
都会打开 neo4j 的窗口。
多个域名可以映射到同一个 IP 地址,最常见的情况是多个域名映射到同一组的多个 IP 地址。
11.3.3 因特网连接
客户端和服务器通过在连接上接收和发送字节流来通信。连接是点对点的与全双工的。
连接的端点是套接字,套接字地址由一个 IP 地址和一个 16 位的端口号组成。用 “地址:端口” 来表示。
客户端套接字地址中的端口是由内核自动分配的临时端口,服务器套接字地址中的端口通常是某个熟知端口。
Web 服务器常用端口 80,熟知名字为 http;邮件服务器使用端口 80,熟知名字为 smtp。
文件 /etc/services 中包含一张主机提供的熟知端口与熟知名字之间的映射。
一个连接由两端的套接字地址唯一确定。
11.4 套接字接口
套接字接口是一组函数,和 Unix I/O 函数结合起来创建网络应用。
11.4.1 套接字地址结构
从内核角度看,套接字是通信的端点;从程序的角度看,套接字是一个有相应描述符的打开文件。
套接字地址存放在 sockaddr_in 结构中。
'互联网套接字地址结构'
struct sockaddr_in {
uint16_t sin_family; // 协议族
uint16_t sin_port; // 网络字节顺序的端口号
struct in_addr sin_addr; // 网络字节顺序的 IP 地址
unsigned char sin_zero[8];
}
'通用套接字地址结构'
struct sockaddr {
uint16_t sa_family; // 协议族
char sa_data[14]; // 地址
}
connect, bind, accept 函数都接受一个指向通用 sockaddr 结构的指针,然后要求应用程序将于协议特定结构相关的指针强制转换成这个通用结构。
11.4.2 socket函数
客户端和服务器都使用 socket 函数来创建一个套接字描述符。
#include<sys/types.h>
#include<sys/socket.h>
int socket(int domain, int type, int protocol); // 如果成功返回描述符,出错返回 -1
socket 函数的使用
可以通过如下方式使套接字成为连接的一个端点。但最好使用 getaddrinfo 函数来自动生成这些参数,这样可以让代码与协议无关。
clientfd = socket(AF_INET, SOCK_STREAM, 0); // AF_INET 表示使用 32 位 IP 地址, SOCK_STREAM 表示这个套接字是连接的一个端点。
socket 返回的 clientfd 描述符仅是部分打开的,还不能用于读写。如何完成打开套接字的工作,取决于是客户端还是服务器。
11.4.3 connect函数
客户端通过调用 connect 函数来建立和服务器的连接。
#include <sys/socket.h>
int connect(int clientfd, const struct sockaddr* addr, socklen_t addrlen); // 若成功返回 0,若出错返回 -1。
connect 函数会阻塞,一直到连接成功建立或发生错误。
11.4.4 bind 函数
bind, listen 和 accept 函数都是服务器端用的函数。
bind 函数用来将套接字描述符和套接字地址关联起来。
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr* addr, socklen_t addrlen); // 若成功返回 0,若出错返回 -1。
11.4.5 listen 函数
listen 函数将套接字转换为监听状态。
#include <sys/socket.h>
int listen(int sockfd, int backlog) // 若成功返回 0,若出错返回 -1。
参数 backlog 通常会设置为一个较大的值,如 1024。
11.4.6 accept函数
accept 函数用来接受来自客户端的连接请求。
#include <sys/socket.h>
int accept(int listenfd, struct sockaddr* addr, int* addrlen); // 若成功返回已连接描述符,若出错返回 -1。
11.4.7 主机和服务的转换
1.getaddrinfo 函数
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
int getaddrinfo(const char *host, const char *service,
const struct addrinfo *hints,
struct addrinfo **result);
void freeaddrinfo(struct addrinfo *result);
const char *gai_strerror(int errcode);
下面是该函数的返回的数据结构:
2.getnameinfo 函数
#include <sys/socket.h>
#include <netdb.h>
int getnameinfo(const struct sockaddr *sa, socklen_t salen,
char *host, size_t hostlen,
char *service, size_t servlen, int flags);
11.4.8 套接字接口的辅助函数
1.open_clientfd 函数
#include 11csapp.h11
int open_clientfd(char•hostname, char•port);
2.open_listenfd 函数
#include "csapp.h"
int open_listenfd(char *port);
11.4.9 echo 客户端和服务器的示例
下面是echo客户端主程序:
下面是echo服务器主程序:
下面是读和回送文本行的echo函数:
11.5 Web服务器
11.5.1 Web基础
Web 客户端和服务器之间的交互用的是一个基于文本的应用级协议,叫做 HTTP CH邓pertext Transfer Protocol, 超文本传输协议)。 HTTP 是一个简单的协议。一个 Web 客户端(即浏览器)打开一个到服务器的因特网连接,并且请求某些内容。服务器响应所请 求的内容,然后关闭连接。浏览器读取这些内容,并把它显示在屏幕上。
11.5.2 Web内容
下面是一些常用 的 MIME类型。
Web 服务器以两种不同的方式向客户端提供内容:
- 取一个磁盘文件,并将它的内容返回给客户端。磁盘文件称为静态内容(static con tent) , 而返回文件给客户端的过程称为服务静态内容(serving static content) 。
- 运行一个可执行文件,并将它的输出返回给客户端。运行时可执行文件产生的输出 称为动态内容(dynamic content) , 而运行程序并返回它的输出到客户端的过程称为 服务动态内容(serving dynamic content) 。
11.5.3 HTTP事务
下图使用 TELNET 向 AOL Web 服务器请求主页。
1.HTTP请求
一个 HTTP 请求的组成是这样的:一个请求行(request line) (第 5 行),后面跟随零 个或更多个请求报头(request header) (第 6 行),再跟随一个空的文本行来终止报头列表 (第 7 行)。一个请求行的形式是
m
e
t
h
o
d
U
R
I
v
e
r
s
i
o
n
method \quad URI\quad version
methodURIversion
2.HTTP响应
HTTP 响应和 HTTP请求是相似的。 一个 HTTP 响应的组成是这样的:一个响应行 (response line) (第 8 行),后面跟随着零个或更多的响应报头 (response header) (第 9~13 行),再跟随一个终止报头的空行(第 14行),再跟随一个响应主体(response body) (第 15~17 行)。 一个响应行的格式是
v
e
r
s
i
o
n
s
t
a
t
u
s
−
c
o
d
e
s
t
a
t
u
s
−
m
e
s
s
a
g
e
version \quad status-code \quad status-message
versionstatus−codestatus−message
下面是常用的状态码:
行来终止报头列表 (第 7 行)。一个请求行的形式是
m
e
t
h
o
d
U
R
I
v
e
r
s
i
o
n
method \quad URI\quad version
methodURIversion
2.HTTP响应
HTTP 响应和 HTTP请求是相似的。 一个 HTTP 响应的组成是这样的:一个响应行 (response line) (第 8 行),后面跟随着零个或更多的响应报头 (response header) (第 9~13 行),再跟随一个终止报头的空行(第 14行),再跟随一个响应主体(response body) (第 15~17 行)。 一个响应行的格式是
v
e
r
s
i
o
n
s
t
a
t
u
s
−
c
o
d
e
s
t
a
t
u
s
−
m
e
s
s
a
g
e
version \quad status-code \quad status-message
versionstatus−codestatus−message
下面是常用的状态码: