一、客户端-服务器编程模型
每个网络应用都是基于客户端-服务器模型的。采用这个模型,一个应用是由一个服务器进程和一个或者多个客户端进程组成。服务器管理某种资源,并且通过操作这种资源来为他的客户端提供某种服务。
客户端-服务器模型中的基本操作是事务。一个客户端-服务器事务由以下四步组成。
1)当一个客户端需要服务时,他向服务器发送一个请求,发起一个事务。
2)服务器收到请求后,解释它,并以适当的方式操作它的资源。
3)服务器给客户端发送一个响应,并等待下一个请求。
4)客户端收到响应并处理它。
二、网络
对主机而言,网络只是又一种I/O设备,是数据源和数据接收方,如图11-2所示。
一个插到I/O总线扩展槽的适配器提供了到网络的物理接口。从网络上接收到的数据从适配器经过I/O和内存总线复制到内存,通常是通过DMA传送。相似的,数据也能从内存复制到网络。
互联网络至关重要的特性是,它能由采用完全不同和不兼容技术的各种局域网和广域网组成。每台主机和其他每台主机都是物理相连的,但是如何能够让某台源主机跨过所有这些不兼容的网络发送数据位到另一台目的主机呢?
解决方法是一层运行在每台主机和路由器上的协议软件,它消除了不同网络之间的差异。这个软件实现一种协议,这种协议控制主机和路由器如何协同工作来实现数据传输。这种协议必须提供两种基本能力:
- 命名机制。不同的局域网技术有不同和不兼容的方式来为主机分配地址。互联网络协议通过定义一种一致的主机地址格式消除了这些差异。每台主机会被分配至少一个这种互联网络地址,这个地址唯一的标识了这台主机。
- 传送机制。在电缆上编码位和将这些位封装成帧方面,不同的联网技术有不同的和不兼容的方式。互联网络协议通过定义一种把数据位捆扎成不连续的片(称为包)的统一方式,从而消除了这些差异。一个包是由包头和有效载荷组成的,其中包头包括包的大小以及源主机和目的主机的地址,有效载荷包括从源主机发出的数据位。
三、全球IP因特网
每台因特网主机都运行实现TCP/IP协议的软件,几乎每个现代计算机系统都支持这个协议。因特网的客户端和服务期混合使用套接字接口函数和Unix I/O函数来进行通信。通常将套接字函数实现为系统调用,这些系统调用会陷入内核,并调用各种内核模式的TCP/IP函数。
从程序员的角度,我们可以把因特网看作一个世界范围的主机集合,满足以下特性:
- 主机集合被映射为一组32位的IP地址。
- 这组IP地址被映射为一组称为因特网域名的标识符。
- 因特网主机上的进程能够通过连接和任何其他因特网主机上的进程通信。
1. IP地址
IP地址通常是以一种称为点分十进制表示法来表示的,这里,每个字节由它的十进制值表示,并且用句点和其他字节间分开。
应用程序使用inet_pton和inet_ntop函数来实现IP地址和点分十进制串之间的转换。
#include <arpa/inet.h>
int inet_pton(AF_INET, const char *src, void *dst); // 若成功返回1,若src为非法点分十进制地址则返回0,若出错则为-1
const char *inet_ntop(AF_INET, const void *src, char *dst, socklen_t size); // 若成功则返回指向点分十进制字符串的指针,若出错则为null
2. 因特网域名
因特网客户端和服务器相互通信时使用的是IP地址。然而,对于人们而言,大整数是很难记住的,所以因特网也定义了一组更加人性化的域名,以及一种将域名映射到IP地址的机制。域名是一串用句点分隔的单词(字母、数字和破折号),例如www.baidu.com。
3. 因特网连接
因特网客户端和服务器通过在连接上发送和接收字节流来通信。从连接一对进程的意义上而言,连接是点对点的。从数据可以同时双向流动的角度来说,他是全双工的。并且从由源进程发出的字节流最终被目的进程以他发出的顺序收到它的角度来说,他也是可靠的。
一个套接字是连接的一个端点。每个套接字都有相应的套接字地址,是由一个因特网地址和一个16位的整数端口组成的,用“地址:端口”来表示。
当客户端发起一个连接请求时,客户端套接字地址中的端口是由内核自动分配的,称为临时端口。然而,服务器套接字地址中的端口通常是某个知名端口,适合这个服务相对应的。
四、套接字接口
套接字接口是一组函数,它们和Unix I/O函数结合起来,用以创建网络应用。大多数现代系统上都实现套接字接口,包括所有的Unix变种、Windows和Macintosh系统。图11-12给出了一个典型的客户端-服务器事务的上下文中的套接字接口概述。当讨论各个函数时,你可以使用这张图来作为向导图。
1. 套接字地址结构
从Linux内核的角度来看,一个套接字就是通信的一个端点。从Linux程序的角度来看,套接字就是一个有相应描述符的打开文件。
因特网的套接字地址存放在如下所示的类型为sockaddr_in的16字节结构中。对于因特网应用,sin_family成员是AF_INET,sin_port成员是一个16位的端口号,而sin_addr成员就是一个32位的IP地址。IP地址和端口号总是以网络字节顺序(大端法)存放的。
struct sockaddr_in {
unit16_t sin_family; /* Protocol family(always AF_INET) */
unit16_t sin_port; /* Prot number in network byte order */
struct in_addr sin_addr; /* IP address in network byte order */
unsigned char sin_zero[8]; /* Pad to sizeof(struct sockaddr) */
};
struct sockaddr {
unit16_t sa_family; /* Protocol family */
char sa_data[14]; /* Address data */
}
2. socket函数
客户端和服务器使用socket函数来传建一个套接字描述符。
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol); // 若成功返回非负描述符,若出错则为-1
3. connect函数
客户端调用connect函数来建立和服务器的连接。
#include <sys/socket.h>
int connet(int clientfd, const struct sockaddr *addr, socklen_t addrlen); // 若成功返回0,若出错则为-1
connect函数试图与套接字地址为addr的服务器建立一个因特网连接,其中addrlen是sizeof(sockaddr_in)。connect函数会阻塞,一直到连接成功建立或是发生错误。
4. bind函数
剩下的套接字函数——bind、listen和accept,服务器用它们来和客户端建立连接。
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); // 若成功返回0,若出错则为-1
bind函数告诉内核将addr中的服务器套接字地址和套接字描述符sockfd联系起来。参数addrlen就是sizeof(sockaddr_in)。对于socket和connect,最好的方法是用getaddrinfo来为bind提供参数。
5. listen函数
客户端是发起连接请求的主动实体。服务器是等待来自客户端的连接请求的被动实体。默认情况下,内核会认为socket函数创建的描述符对应于主动套接字,它存在于一个连接的客户端。服务器调用listen函数告诉内核,描述符是被服务器而不是客户端使用的。
#include <sys/socket.h>
int listen(int sockfd, int backlog); // 若成功返回0,若失败则为-1
6. accept函数
服务器通过调用accept函数来等待来自客户端的连接请求。
#include <sys/socket.h>
int accept(int listenfd, struct sockaddr *addr, int *addrlen); // 若成功返回非负连接描述符,若失败则为-1
accept函数等到来自客户端的连接请求到达监听描述符listenfd,然后在addr中填写客户端的套接字地址,并返回一个已连接描述符,这个描述符可被用来利用Unix I/O函数与客户端通信。
监听描述符和已连接描述符之间的区别使很多人感到迷惑。监听描述符是作为客户端连接请求的一个端点。它通常被创建一次,并存在于服务器的整个生命周期。已连接描述符是客户端和服务器之间已经建立起来了的连接的一个端点。服务器每次接收连接请求时都会创建一次,它只存在于服务器为一个客户端服务的过程中。
7. 主机和服务的转换
Linux提供了一些强大的函数(称为getaddrinfo和getnameinfo)实现二进制套接字地址结构和主机名、主机地址、服务名和端口号的字符串表示之间的相互转化。当和套接字接口一起使用时,这些函数能使我们编写独立于任何特定版本的IP协议的网络程序。
7.1 open_clientfd函数
客户端调用open_clientfd建立与服务器的连接。
#include "csapp.h"
int open_clientfd(char *hostname, char *port); // 若成功返回描述符,若失败则为-1
7.2 open_listenfd函数
调用open_listenfd函数,服务器创建一个监听描述符,准备好接收连接请求。
#include "csapp.h"
int open_instenfd(char *port); // 若成功返回描述符,若失败则为-1
五、Web服务器
1. Web基础
Web客户端和服务器之间的交互用的是一个基于文本的应用级协议,叫做HTTP(超文本传输协议)。HTTP是一个简单的协议。一个Web客户端(即浏览器)打开一个到服务器的因特网连接,并且请求某些内容。服务器响应所请求的内容,然后关闭连接。浏览器读取这些内容,并把它显示在屏幕上。
2. Web内容
Web服务器以两种不同的方式向客户端提供内容:
- 取一个磁盘文件,并将它的内容返回给客户端。磁盘文件称为静态内容,而返回文件给客户端的过程称为服务静态内容。
- 运行一个可执行文件,并将他的输出返回给客户端。运行时可执行文件产生的输出称为动态内容,而运行程序并返回它的输出到客户端的过程称为服务动态内容。
3. HTTP事务
因为HTTP是基于在因特网连接上传送的文本行的,我们可以使用Linux的TELNET程序来和因特网上的任何Web服务器执行事务。对于调试在连接上通过文本行来与客户端对话的服务器来说,TELNET程序是非常便利的。