第十一章 网络编程
我们需要理解基本的客户端-服务端
编程模型,以及如何编写使用因特网提供的服务的客户端-服务端
程序。
最后,我们将把所有这些概念结合起来,开发一个小的但功能齐全的Web
服务器,能够为真实的Web
浏览器提供静态的和动态的文本和图形内容。
11.1 客户端 - 服务器编程模型
每个网络应用程序都是基于客户端 - 服务器模型
的
- 采用这种模型,一个应用是由一个
服务器
进程
和一个或多个客户端进程
组成。-
服务器
管理某种资源,并且通过操作这种资源为它的客户端提供某种服务。-
WEB
服务器,代表客户端
检索,执行磁盘内容。 -
FTP
服务器,为客户端
进行存储和检索。 -
电子邮件
服务器,为客户端
进行读和更新。
-
-
客户端-服务器
模型中的基本操作是事务(transaction)
.- 一个
客户端-服务器
事务由四步组成- 客户端需要服务的时候,向服务器发送请求,发送一个
事务
。 - 服务器收到请求后,解释它,并以适当方式操作它的资源。
- 服务器给客户端发送一个
响应
,并等待下一个请求。 - 客户端收到
响应
并处理它。
- 客户端需要服务的时候,向服务器发送请求,发送一个
- 一个
-
11.2 网络
客户端
和服务端
通常运行在不同的主机上,并且通过计算机网络
的硬件和软件资源来通信。
- 对于一个主机而言,
网络
只是又一种I/O
设备,作为数据源和数据接收方。
- 对于物理上而言,网络是一个按照地理远近组成的层次系统。
- 最低层是
LAN(Local Area Network,局域网)
:在一个建筑或校园范围内。- 迄今为止,最流行的
LAN
技术是以太网(Ethernet)
.-
由
Xerox PARC
公司在20世纪70年代中期提出。-
以太网
被证明是适应力极强的,从3 MB/s
到10 GB/s
。
-
-
一个
以太网段(Ethernet segment)
-
包括一些
电缆(通常是双绞线)
和一个叫做集线器
的小盒子。- 每根
电缆
都有相同的最大位带宽- 典型的是
100MB/s
或者1GB/S
. - 一端连接在主机的
适配器
,一端连接到集线器的一个端口
。
- 典型的是
-
集线器
不加分辨地将从一个端口收到的每个位复制到其他所有端口上。- 因此每台主机都能看到每个位。
- 每根
-
以太网段
通常跨越一些小的区域。- 例如某建筑物的一个房间或一个楼层。
-
- 迄今为止,最流行的
- 最低层是
扩展介绍以太网
每个以太网适配器(网卡)
都有一个全球唯一的48
位地址,它存储在这个适配器的ROM
上(MAC
)。
- 一台主机可以发送一段
位
,称为帧(frame)
,到这个网段
内其他任何主机。-
每个
帧
包括- 一些固定数量的
头部(header)
位- 用于表示此
帧
的源,和目的地址以及此帧
的长度。
- 用于表示此
- 此后就是数据位的
有效载荷
。
- 一些固定数量的
每个主机适配器都能看到这个
帧
,但是只有目的主机实际读取它。
-
使用一些电缆
和叫做网桥(bridge)
的小盒子,多个以太网段
可以连接称较大的局域网,称为桥接以太网(bridged Ethernet)
。
- 一些
电缆
连接网桥与网桥,或者 网桥与集线器。- 这些电缆的带宽可以是不同的。
在层次的更高级别,多个不兼容的局域网可以通过叫做路由器(router)
的特殊计算机连接起来,组成一个internet(互联网络)
Internet和internet
我们总是用小写字母的internet
表示一般概念,大写的Internet
表示一种具体实现,如全球IP因特网。
-
WAN
(Wide-Area Network
,广域网)
互联网至关重要的特性是:
- 它能由采用完全不同和不兼容技术的各种局域网和广域网组成。
Q
:如何能够让某台源主机
跨过所有这些不兼容的网络发送数据位到另一台目的主机
呢?
A
:解决办法是一层运行在每台主机和路由器上的协议软件
,消除不同网络之间的差异。
- 这个软件实现一种
协议
:控制主机和路由器如何协调工作来实现数据传输。- 必须提供两种基本能力:
-
命名机制
- 每台主机会被分配至少一个
互联网地址(internet address)
,这个地址唯一标识了这台主机。
- 每台主机会被分配至少一个
-
传送机制
-
协议
通过定义一种把数据位捆扎成不连续的片(称为包
)的方式。- 一个
包
是由包头
和有效载荷
组成的。-
包头
包的大小
-
源主机
和目的主机
地址
-
有效载荷
包括从源主机发出的数据位
-
- 一个
-
-
- 必须提供两种基本能力:
一个客户端
运行在主机A
上,主机A
与LAN1
相连,它发送了一串数据字节到运行在主机B上的服务器端,主机B则连接在LAN2
上。有如下8个步骤。
运行在主机
A
上的客户端进行系统调用,从客户端的虚拟地址空间
拷贝到内核缓冲区
。-
主机
A
上的协议软件
通过在数据前附加互联网络包头
和LAN1
帧头,创建了一个LAN1
的帧。-
互联网包头
寻址到互联网主机B。(最终目的) -
LAN1帧头
寻址到路由器
。(中转站) - 封装
-
LAN1帧
的有效载荷是互联网络包
。 -
互联网络包
的有效载荷是实际的用户数据。 - 这种
封装
是基本的网络互联方法之一。
-
-
LAN1
适配器拷贝该帧
到网络上。帧
到达路由器,路由器的LAN1适配器
从电缆上读取它,并传送到协议软件中。-
路由器从
互联网包头中
提取处目的互联网络地址,用它作为路由器的索引,确定向哪里转发这个包。- 路由器剥落旧的
LAN1
的帧头,加上寻址到主机B
的新的LAN2
帧头,并把得到的帧传送到适配器。
- 路由器剥落旧的
路由器的
LAN2适配器
拷贝该帧
到网络帧
到达主机B时,它的适配器从电缆上读到此帧,并将它传送到协议软件。最后,主机B上的协议软件剥落包头和帧头。服务器进行一个读取这些数据的
系统调用
。
当然,在这里,我们掩盖了许多非常艰难的问题。
- 如果不同的网络有不同
帧
大小的最大值,该怎么办。 - 路由器如何知道往哪里转发
帧
。 - 网络拓扑变化的时候,如何通知路由器。
- 包丢失了,会如何?
虽然如此,我们也能大概了解到互联网络思想的精髓。
11.3 全球IP 因特网
每台因特网主机都运行实现TCP/IP
协议 (Transmission Control Protocol/Intelnet Protocol,传输控制协议/互联网络协议)的软件,几乎所有计算机系统都支持这个协议
。
- 因特网的客户端和服务端混合使用
套接字接口
函数和Unix I/O
函数来进行通信。-
套接字函数
典型地是作为会陷入内核的系统调用
来实现的,并调用各种内核模式的TCP/IP
函数。
-
TCP/IP
协议实际上一个协议族
,每一个协议提供不同的功能。
- 例
-
IP
协议提供基本的命名方法,和传递机制。- 这种
传递机制
能够从一台因特网主机往其他主机发送包,也叫做数据报(datagram)
-
IP
机制从某种意义上是不可靠的,如果数据报在网络丢失或重复,并不会试图恢复。-
UDP(Unreliable Datagram Protocol,不可靠数据报协议)
稍微扩展了IP
协议。- 这样,
包
可以在进程
间,而不是主机
间传送。
- 这样,
-
- 这种
TCP
是一个构建在IP
之上的复杂协议,提供了进程间可靠地全双工(双向)
的连接。
-
为了简化讨论
- 我们将
TCP/IP
看作是一个单独的整体协议。 - 不讨论它的内部工作,只讨论
TCP
和IP
为应用程序提供的基本功能。 - 不讨论
UDP
从程序员的角度,我们可以把因特网看作世界范围内主机的集合,满足一下特性。
- 主机集合被映射为一组
32
位的IP
地址。 - 这组
IP
地址可以被映射为一组称为因特网域名(Internet domain name)
的标示符。 - 因特网主机上的进程能够通过
连接
和任何其他主机上的进程通信。
11.3.1 IP地址
一个IP
地址就是一个32位无符号整数。网络程序将IP
地址存放在一个IP地址结构
中。
/* Internet address structure */
struct in_addr{
unsigned int s_addr;
}
为什么要用结构来存放标量
IP
地址
是早期的不幸产物,但是现在更改太迟了。
主机字节序,和网络字节序
因为因特网主机可以有不同的主机字节顺序
TCP/IP
为任意整数数据项定义了统一的网络字节顺序(network byte order)
(大端,x86是小端)。
Unix
提供下面这样的函数实现转换。
IP地址通常是以一种称为点分十进制表示法
来表示的
这里,每个
字节
(8位)都是由它的十进制表示(0~255),并且用句点和其他字节间分开。-
在Linux系统上,你能够使用
hostname
命令来确定你自己主机的点分十进制:linux> hostname -i
10.174.204.145 -
可以使用
inet_aton
和inet_ntoa
函数来实现两者之间互相转换。
11.3.2 因特网域名
方便人们记忆的对于IP
的映射就是域名
。
域名集合
形成了一个层次结构,每个域名编码了它在层次中的位置。
叶子结点反向到根的
路径
就是域名
。层次结构第一层 : 未命名的根结点
-
层次结构第二层 :
一级域名(first-level domain name)
- 由非盈利组织
ICANN
(Internet Corporation for Assigned Names and Numbers,因特尔分配名字数字协会)定义。 - 常见的一级域名:
com
,edu
,gov
,org
和net
。
- 由非盈利组织
-
层次结构第三层:
二级域名(second-level)
- 例如:
cmu.edu
。 - 这些域名是由
ICANN
的各个授权代理按照先到先服务的基础分配的。 - 一旦一个组织得到一个二级域名,那么它就可以在这个子域中创建任何新的域名了。
- 例如:
因特网定义了域名集合
和IP
地址直接的映射。
-
HOSTS.TXT
- 直到
1988
年,这个映射都是通过一个叫做HOSTS.TXT
的文本文件来手工维护的。
- 直到
-
DNS
:- 之后,通过分布世界范围内的数据库(
DNS
,Domain Name System
,域名系统),来维护的。 -
DNS
数据库由上百万的主机条目结构(host entry structure)
组成的。- 定义了一组
域名
(一个官方名字和一个别名)和一组IP
地址之间的映射。
- 定义了一组
- 之后,通过分布世界范围内的数据库(
因特网应用程序通过调用 gethostbyname
和gethostbyaddr
函数,从DNS数据库中检索任意的主机条目。。
每台主机都有本地定义的域名localhost
- 这个域名总是映射
本地送回地址(loopback address)
:127.0.0.1
。 -
localhost
名字为引用运行在同一机器上的客户端和服务端提供了一种便利和可移植的方式。
11.3.3 因特网连接
Internet
服务端和客户端通过在连接
上发送和接收字节流
来通信。
- 从连接一对进程的意义上而言,连接是
点对点
的。 - 从数据可以同时双向流动的角度来说,它是
全双工
的。 - 并且从由源进程发出的字节流最终被目的进程按照发送的数据接收来说,它是
可靠
的
一个套接字
是连接的
一个端点。
-
每个套接字都有相应的
套接字地址
。- 是由一个
IP
地址和一个16位的整数端口
组成的,用地址:端口
来表示。
- 是由一个
-
当客户端发起一个连接请求时,客户端套接字地址中的
端口
由内核自动分配的。- 称为
临时端口
- 称为
-
然后,服务器套接字地址中的
端口
通常是某个知名的端口,和这个服务相对应的。-
例如:
-
Web服务器通常使用端口
80
-
电子邮件服务器使用端口
25
-
Web服务器通常使用端口
在
Unix
机器上,文件/etc/services
包含一张这台机器提供的服务和他们的知名端口号的综合列表。
-
一个连接
是由它两端的套接字地址唯一确定的。
- 这对套接字地址叫做
套接字对(socket pair)
,由下列元组来表示:(cliaddr:cliport,servaddr:servport)
11.4 套接字接口
套接字接口(socket interface)
是一组函数,他们和Unix I/O
函数结合起来,用以创建网络应用。
给出一个典型的客户端-服务器事务的上下文中套接字接口
概述,以此导向。
11.4.1 套接字地址结构
不同的角度:
- 从
Unix
内核角度来看,一个套接字就是通信的一个端点
。 - 从
Unix
程序来看,套接字就是一个有相应描述符的打开文件。
Internet
的套接字地址(Internet-sytle
)存放在上图所示的类型为sockaddr_in
的16字节结构中。
sin_family
成员是AF_INET
,ipv4还是ipv6。sin_port
成员是一个16位的端口号。-
sin_addr
成员就是一个32位的IP
地址。-
IP
地址和端口号总是以网络字节顺序(大端法)存放的。
-
sin_zero
是填充,使得sockaddr_in
和sockaddr
一样大。
sockaddr_in
给程序员操作的,sockaddr
交由套接字函数使用的,两者可以直接强制转换。
11.4.2 socket函数
客户端和服务端使用socket
函数来创建一个套接字描述符(socket descriptor)
跟open
差不多
#include<sys/types.h>
#include<sys/socket.h>
int socket(int domain, int type ,int protocol);
返回:若成功则为非负描述符,出错为-1
我们总是带这样的参数调用socket
函数:
clientfd = Socket(AF_INET,SOCK_STREAM,0);
-
AF_INET
表面我们在使用IPV4协议。 -
SOCK_STREAM
表示这个套接字是Internet连接
的一个端点。 -
socket
返回的clientfd
描述符,仅仅是部分打开,还不能用于读写。- 如何完成打开套接字的工作,取决于我们是客户端还是服务器。
- 下一节描述我们是客户端时如何打开套接字。
11.4.3 connect函数
客户端通过调用connect
函数来建立和服务器的连接
#include<sys/socket.h>
int connect(int sockfd,struct sockaddr *serv_addr,int addrlen);
返回:若成功则为0,若出错则为-1
connect
函数试图于套接字地址为serv_addr
的服务器建立一个因特网连接.
- 其中
addrlen
是sizeof(sockaddr_in)
. -
connect
函数会阻塞,一直到连接成功建立或是发生错误。 - 如果成功,
sockfd
描述符就可以读写了。- 并且得到链接是由套接字对
(x:y,serv_addr.sin_addr,serv_addr.sin_port)
刻画的。- 其中
x
是客户端IP地址,而y
表示临时端口。 - 它唯一地确立了客户端主机上的客户端进程。
- 其中
- 并且得到链接是由套接字对
11.4.4 open_clientfd函数
open_cilentfd
是socket
和connect
的包装函数(不是系统自带)
#include <csapp.h>
int open_clientfd(char *hostname, int port);
返回:若成功则为描述符,若`Unix`出错则为-1,DNS出错则为-2.
open_clientfd
函数和运行在hostname
上的服务器建立一个连接,并在知名端口port
上监听连接请求。
- 它返回一个打开的套接字描述符。
- 该描述符准备好了,可以用
Unix I/O
函数做输入和输出。
11.4.5 bind函数
剩下的套接字函数bind
,listen
和accept
被服务器用来和客户端建立链接。
#include<sys/socket.h>
int bind(int sockfd,struct sockaddr *my_addr,int addrlen);
//返回: 若成功则为0,若出错则为-1
bind
函数告诉内核将my_addr
中的服务器套接字地址和套接字描述符sockfd
联系起来。
- 参数
addrlen
就是sizeof(sockaddr_in)
?
11.4.6 listen函数(主动套接字->监听套接字)
客户端是发起连接请求的主动实体。服务器是等待来自客户端连接请求的被动实体。
-
默认情况下,内核会认为
socket
函数创建的描述符对应于主动套接字(active socket)
.- 它存在于一个连接的客户端。
服务器调用
listen
告诉内核,描述符是被服务器而不是客户端使用的
#include<sys/socket.h>
int listen(int sockfd,int backlog);
返回:若成功则为0,若出错则为-1
listen
函数将sockfd
从一个主动套接字
转化为一个监听套接字(listenning socket)
。
- 该套接字可以接收来自客户端的连接请求。
-
backlog
参数暗示了内核在开始拒绝连接请求之前,应该放入队列中等待的未完成连接请求的数量。-
backlog
参数的确切含义要求对TCP/IP
协议的理解,这超出了我们的讨论的范围。 - 通常我们会把它设置成一个较大的值,比如
1024
。
-
11.4.7 open_listenfd函数
用socket
,bind
和listen
函数结合称open_listenfd
的包装函数。
服务器可以用它来创建一个监听描述符
。
#include<csapp.h>
int open_listenfd(int port)
返回:若成功则为描述符,若Unix出错则为-1
open_listenfd
函数打开和返回一个监听描述符
- 这个描述符准备好在知名端口
port
上接收请求。
创建
listenfd
套接字描述符。-
使用
setsockopt
函数来配置服务器,使得它能被立即中止和重启。- 默认地,一个重启的服务器将在大约30秒内拒绝客户端的连接请求,严重阻碍调试。
-
接下来,初始化服务器的
套接字地址结构
。- 用
INADDR_ANY
来告诉内核这个服务器将接收任何IP地址到端口port
的请求。-
INADDR_ANY
通配符地址就是指定地址为0.0.0.0
的地址
-
- 用
调用
blind
,listen
。将其转换为监听套接字
。
11.4.8 accept函数
CSAPP
这里介绍的十分有问题,所以特地翻了UNIX 网络编程
的原话。
-
用于从已完成连接
队列
队头返回下一个已完成连接。- 如果已完成连接为空,那么进程进入阻塞(假定套接字为默认的阻塞方式)
-
返回三个值
已连接标示符
客户端地址
客户度地址长度
监听描述符
和已连接描述符
之间的区别是很多人迷惑。
-
监听描述符
是作为客户端连接请求的一个端点。- 它被创建一次,并存在于服务器的整个生命周期。
-
已连接描述符
是客户端和服务器之间已经建立起来的连接的一个端点。- 服务器每次接收连接请求时都会创建一次。
- 它只存在于服务器为一个客户端服务的过程中。
11.4.9 echo客户端和服务器的示例
学习套接字接口的最好办法是研究示例代码。
没办法。这个代码估计也有挺多疑惑,还是过段时间啃Unix网络编程把
11.5 WEB服务器
这块写过servlet
,就不用复述了,以后再详细补