客户端–服务端模型 (client–server model) 是所有网络应用程序的基础。
事务 (transaction) 是客户端与服务端交互的基本操作,含以下四步:
本质上仍属于客户端–服务端模型,只是其中的每一个进程既是客户端又是服务端。
对一台主机而言,网络适配器 (network adapter) 相当于另一种读写设备。
个人区域网 (personal area network, PAN):用无线(蓝牙)技术把个人使用的电子设备连接起来的网络。
局域网 (local area network, LAN):覆盖较小(数十到数千米)范围的网络。
目前最流行的局域网技术为以太网 (Ethernet)。
以太网段 (Ethernet segment):由多台主机通过以太网线(双绞线)连接到一台集线器所形成的小型网络,可覆盖一间或一层房屋。
桥接的以太网 (bridged Ethernet):由多个以太网段连接到多个网桥所形成的中型网络,可覆盖整栋建筑或整个校园。
城域网 (metropolitan area network, MAN):覆盖几个街区或整个城市的网络。
广域网 (wide area network, WAN):由多个局域网互连所形成的大型网络,可覆盖几十到几千公里。
军队、铁路、银行、电力等系统内部的网络。
互连网 (interconnected network, internet):由多个局域网、广域网经过路由器连接所形成的广域网,可覆盖全球。
全球 IP 互联网 (global IP internet),简称互联网或因特网 (Internet),是所有互连网中最著名、最成功的一个。
因特网中的每台主机都运行着遵循 TCP/IP 协议的软件。
对于普通程序员,Internet 可以理解为由世界上所有具备以下性质的主机所构成的 internet:
常用名词缩写:
ISP (Internet Service Provider):提供互联网服务的机构(企业),可分为主干 ISP、地区 ISP、本地 ISP。
IXP (Internet eXchange Point):允许两个网络直接相连并交换分组的设备,又称 NAP (Network Access Point)。
IPv4 地址可以用 uint32_t
表示。由于历史的原因,它被封装在 struct
内:
/* IP address structure */
struct in_addr {
uint32_t s_addr; /* in network (big-endian) byte order */
};
TCP/IP 规定所有整数都采用大端 (big-endian) 字节顺序。以下函数用于字节顺序的转换:
#include <arpa/inet.h>
/* Host byte order TO Network byte order */
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
/* Network byte order TO Host byte order */
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
IPv4 地址 的点分十进制记法 (dotted decimal notation):用 .
分割为 4 段的字符串,每段各表示一个取自 [0, 256)
的 8-bit 整数,以十进制表示。例如:IPv4 地址 (uint32_t) 0x8002c2f2
可以用 (char*) "128.2.194.242"
表示。
IPv6 地址 的冒号十六进制记法 (colon hexadecimal notation):用 .
分割为 8 段的字符串,每段各表示一个取自 [0, 65536)
的 16-bit 整数,以十六进制表示。
以下函数用于 IP 地址的字符串表示格式 (p
resentation format) 与网络格式 (n
etwork format) 之间的转换:
#include <arpa/inet.h>
int inet_pton(AF_INET, const char *src, void *dst); /* returns
`1` if OK, 0 if `src` is invalid, `−1` on error and sets `errno` */
const char *inet_ntop(AF_INET, const void *src, char *dst, socklen_t size); /* returns
pointer to a dotted-decimal string if OK, `NULL` on error
其中 AF_INET
表示 void *
指向 32-bit 的 IPv4 地址(若改为 AF_INET6
,则表示 void *
指向 128-bit 的 IPv6 地址)。
域名 (domain name):用 .
隔开的一组单词(字符、数字、下划线),例如:csapp.cs.cmu.edu
为本书网站的域名,从右向左依次为
edu
,同级的域名还有 gov
、com
等。cmu
,同级的域名还有 mit
、princeton
等。cs
,同级的域名还有 ee
、art
等。域名系统 (domain name system, DNS):
nslookup
命令可以查询已知域名所对应的 IP 地址(可能有零到多个)。连接 (connection):一对可以收发字节流的进程。
套接字 (socket):连接的一端,用形如 ip_address:port
的套接字地址表示,其中 (uint16_t) port
为端口号 (port number)。
/etc/services
中。/* 泛型套接字地址结构,用于 connect, bind, accept 等函数 */
struct sockaddr {
uint16_t sa_family; /* Protocol family */
char sa_data[14]; /* Address data */
};
typedef struct sockaddr SA;
/* IPv4 套接字地址结构 */
struct sockaddr_in {
uint16_t sin_family; /* 协议族,总是取 `AF_INET` */
uint16_t sin_port; /* 端口号,大端字节顺序 */
struct in_addr sin_addr; /* IPv4 地址,大端字节顺序 */
unsigned char sin_zero[8]; /* 对齐至 sizeof(struct sockaddr) */
};
socket()
客户端及服务端均用此函数获得(部分打开的)套接字。若成功则返回套接字描述符 (socket descriptor),否则返回 -1
。
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
/* 典型用法: */
int socket_fd =
Socket(AF_INET/* IPv4 */, SOCK_STREAM/* 作为连接的一端 */, 0);
建议用 getaddrinfo()
获得实参。
connect()
— only for client客户端用此函数向服务端发送连接请求并等待。若成功则返回 0
,否则返回 -1
。
#include <sys/socket.h>
int connect(int client_fd,
const SA *server_addr, socklen_t server_addr_len/* sizeof(sockaddr_in) */);
至此,客户端可通过在 client_fd
所表示的文件(套接字)上读写数据,实现与服务端的通信。
bind()
— only for server服务端用此函数将套接字描述符 server_fd
与套接字地址 server_addr
关联。 若成功则返回 0
,否则返回 -1
。
#include <sys/socket.h>
int bind(int server_fd,
const SA *server_addr, socklen_t server_addr_len/* sizeof(sockaddr_in) */);
listen()
— only for serversocket()
默认返回的套接字类型,客户端可直接使用。listen_fd
。服务端用此函数将活跃套接字转变为监听套接字。若成功则返回 0
,否则返回 -1
。
#include <sys/socket.h>
int listen(int active_fd,
int backlog/* 队列大小(请求个数)提示,通常为 1024 */);
accept()
— only for server服务端用此函数等待客户端发来的连接请求。 若成功,则返回异于 listen_fd
的 connect_fd
,并获取客户端地址;否则返回 -1
。
#include <sys/socket.h>
int accept(int listen_fd,
SA *client_addr, int *client_addr_len);
至此,服务端可通过在 connect_fd
所表示的文件(套接字)上读写数据,实现与客户端的通信。
getaddrinfo()
getaddrinfo()
返回一个链表(需用 freeaddrinfo()
释放),结点类型为 struct addrinfo
:
socket()
及 connect()
,直到成功返回。socket()
及 bind()
,直到成功返回。#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
struct addrinfo {
int ai_flags; /* AI_ADDRCONFIG | AI_CANONNAME | AI_NUMERICSERV | AI_PASSIVE */
int ai_family; /* AF_INET | AF_INET6 */
int ai_socktype; /* SOCK_STREAM */
int ai_protocol; /* Third arg to socket function */
char *ai_canonname; /* Canonical hostname */
size_t ai_addrlen; /* Size of ai_addr struct */
struct sockaddr *ai_addr; /* Ptr to socket address structure */
struct addrinfo *ai_next; /* Ptr to next item in linked list */
};
int getaddrinfo(
const char *host/* 域名 或 地址 */,
const char *service/* 服务名 或 端口号 */,
const struct addrinfo *hints/* NULL 或 只含前四项的 addrinfo */,
struct addrinfo **result/* 输出链表 */
);
void freeaddrinfo(struct addrinfo *result);
const char *gai_strerror(int errcode);
getnameinfo()
功能与 getaddrinfo()
相反,即由 sockaddr
获得 host
或 service
。
#include <sys/socket.h>
#include <netdb.h>
int getnameinfo(
const struct sockaddr *sa, socklen_t salen,
char *host, size_t hostlen, /* 可以为空,即 host = NULL, hostlen = 0 */
char *service, size_t servlen, /* 同上,但至多一行为空 */
int flags/* NI_NUMERICHOST | NI_NUMERICSERV */);
hostinfo.c
用例:
$ ./hostinfo www.baidu.com
182.61.200.6
182.61.200.7
关键代码:
#include "csapp.h"
int main(int argc, char **argv) {
struct addrinfo *p, *listp, hints;
char buf[MAXLINE];
int rc, flags;
/* ... */
memset(&hints, 0, sizeof(struct addrinfo));
hints.ai_family = AF_INET; /* 只用 IPv4 */
hints.ai_socktype = SOCK_STREAM; /* 只面向连接 */
Getaddrinfo(argv[1], NULL, &hints, &listp);
flags = NI_NUMERICHOST; /* 以十进制 IP 地址表示 host */
for (p = listp; p; p = p->ai_next) { /* 遍历链表 */
Getnameinfo(p->ai_addr, p->ai_addrlen, buf, MAXLINE, NULL, 0, flags);
printf("%s\n", buf);
}
/* ... */
}
open_clientfd()
此函数提供了对客户端调用 getaddrinfo()
、socket()
、connect()
的封装。
关键代码:
#include "csapp.h"
int open_clientfd(char *hostname, char *port) {
int clientfd, rc;
struct addrinfo hints, *listp, *p;
/* Get a list of potential server addresses */
memset(&hints, 0, sizeof(struct addrinfo));
hints.ai_socktype = SOCK_STREAM; /* Open a connection */
hints.ai_flags = AI_NUMERICSERV; /* ... using a numeric port arg. */
hints.ai_flags |= AI_ADDRCONFIG; /* Recommended for connections */
if ((rc = getaddrinfo(hostname, port, &hints, &listp)) != 0) {
fprintf(/* ... */);
return -2;
}
for (p = listp; p; p = p->ai_next) {
clientfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol);
if (clientfd < 0)
continue;
if (connect(clientfd, p->ai_addr, p->ai_addrlen) != -1)
break; /* 连接成功,则终止遍历 */
if (close(clientfd) < 0) { /* 连接失败,则关闭文件,再尝试下一个 */
fprintf(/* ... */);
return -1;
}
}
freeaddrinfo(listp);
return p ? clientfd : -1;
}
open_listenfd()
此函数提供了对服务端调用 getaddrinfo()
、socket()
、bind()
、listen()
的封装。
关键代码:
#include "csapp.h"
int open_listenfd(char *port) {
struct addrinfo hints, *listp, *p;
int listenfd, rc, optval=1;
memset(&hints, 0, sizeof(struct addrinfo));
hints.ai_socktype = SOCK_STREAM;
hints.ai_flags = AI_ADDRCONFIG | AI_NUMERICSERV
| AI_PASSIVE/* host=NULL, all ai_addr=*.*.*.* */;
if ((rc = getaddrinfo(NULL, port, &hints, &listp)) != 0) {
fprintf(/* ... */);
return -2;
}
for (p = listp; p; p = p->ai_next) {
listenfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol);
if (listenfd < 0)
continue;
/* Eliminates "Address already in use" error from bind() */
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR,
(const void *)&optval, sizeof(int));
if (bind(listenfd, p->ai_addr, p->ai_addrlen) == 0)
break;
if (close(listenfd) < 0) {
fprintf(/* .. */);
return -1;
}
}
freeaddrinfo(listp);
if (!p)
return -1;
if (listen(listenfd, LISTENQ/* defined in csapp.h */) < 0) {
close(listenfd);
return -1;
}
return listenfd;
}
echoclient.c
关键代码:
#include "csapp.h"
int main(int argc, char **argv) {
int clientfd;
char *host, *port, buf[MAXLINE];
rio_t rio;
/* ... */
clientfd = Open_clientfd(host/* argv[1] */, port/* argv[2] */);
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); // 客户端关闭套接字描述符,服务端会检测到 EOF
exit(0);
}
echoserveri.c
迭代型服务端 (iterative server):同一时间只能服务一个客户端,不同客户端要排成队列依次接受服务。
关键代码:
#include "csapp.h"
void echo(int connect_fd); // implemented in echo.c
int main(int argc, char **argv) {
int listenfd, connect_fd;
socklen_t client_len;
struct sockaddr_storage client_addr; /* Enough space for any address */
char client_hostname[MAXLINE], client_port[MAXLINE];
/* ... */
listenfd = Open_listenfd(/* port */argv[1]);
while (1) {
client_len = sizeof(struct sockaddr_storage);
connect_fd = Accept(listenfd, (SA *)&client_addr, &client_len);
Getnameinfo((SA *)&client_addr, client_len,
client_hostname, MAXLINE, client_port, MAXLINE, 0);
printf("Connected to (%s, %s)\n", client_hostname, client_port);
echo(connect_fd);
Close(connect_fd);
}
exit(0);
}
echo.c
#include "csapp.h"
void echo(int connect_fd) {
size_t n;
char buf[MAXLINE];
rio_t rio;
Rio_readinitb(&rio, connect_fd);
while((n = Rio_readlineb(&rio, buf, MAXLINE)) != 0) {
printf("server received %d bytes\n", (int)n);
Rio_writen(connect_fd, buf, n);
}
}
服务端 | 客户端 | |
---|---|---|
sudo ./echoserveri 23333 | ← 启动服务 | |
[sudo] password for user: | ← 输入密码 | |
发送请求 → | ./echoclient localhost 23333 | |
Connected to (localhost, 44250) | ← 建立连接 | |
发送信息 → | hello, world | |
server received 13 bytes | ← 处理信息 | |
回显信息 → | hello, world | |
结束连接 → | Ctrl+D | |
再次请求 → | ./echoclient localhost 23333 | |
Connected to (localhost, 51014) | ← 建立连接 | |
结束连接 → | Ctrl+D | |
Ctrl+C | ← 停止服务 |
超文本传输协议 (HyperText Transfer Protocol, HTTP):提供网页 (Web) 服务的协议
超文本标记语言 (HyperText Markup Language, HTML):
MIME (Multipurpose Internet Mail Extensions)
网页内容分类
URL (Universal Resource Locator):
http://www.google.com:80/index.html
,其中 www.google.com
为域名,80
为端口号(HTTP 服务的默认端口号为 80
,可省略)。/
表示服务端所在主机上用于存放静态内容的目录(如 /usr/httpd/html/
)。index.html
为静态内容文件,http://bluefish.ics.cs.cmu.edu:8000/cgi-bin/adder?15000&213
,其中 bluefish.ics.cs.cmu.edu
为域名,8000
为端口号。/cgi-bin/
表示服务端所在主机上用于存放动态内容生成程序的目录,如 /usr/httpd/cgi-bin/
。adder
为可执行文件,功能为加法。?
之后为 adder
的实参列表,&
用于分隔实参。telnet
请求 HTTP 服务在客户机的命令行终端内输入以下内容,以发起连接:
telnet csapp.cs.cmu.edu 80
服务端返回以下三行,显示在客户机的命令行终端:
Trying 128.2.100.230...
Connected to csapptest.cs.cmu.edu.
Escape character is '^]'.
在客户端输入以下内容(含空行):
GET / HTTP/1.1
Host: csapp.cs.cmu.edu
其中
method URI version
header_name: header_data
服务端返回以下内容,打印在客户端:
HTTP/1.1 200 OK
Date: Sat, 22 May 2021 16:43:27 GMT
Server: Apache/2.2.15 (Red Hat)
Last-Modified: Mon, 20 Jan 2020 21:53:54 GMT
ETag: "8440768-1882-59c9953d5c080"
Accept-Ranges: bytes
Content-Length: 6274
Connection: close
Content-Type: text/html; charset=UTF-8
其中
version status_code status_message
。接下来,客户端显示网页的 HTML 源代码:
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<title>CS:APP3e, Bryant and O'Hallaron</title>
<link href="http://csapp.cs.cmu.edu/3e/css/csapp.css" rel="stylesheet" type="text/css">
</head>
...
</body>
</html>
几秒钟后,客户端显示以下内容,并退出 telnet
:
Connection closed by foreign host.
CGI (common gateway interface):
客户端请求行中的 GET
方法将 URI 中的实参传送给服务端,其中
?
分隔可执行文件与实参列表。&
分割实参列表中的各项。%20
表示单项实参中的空格。服务端收到 GET /cgi-bin/adder?15000&213 HTTP/1.1
后,会依次
fork
创建一个子进程。QUERY_STRING
设为 15000&213
。execve
加载可执行文件 /cgi-bin/adder
。详见 serve_dynamic()
。
CGI 标准定义了一些环境变量,用于向子进程传递信息:
环境变量 | 作用 |
---|---|
QUERY_STRING | 程序实参 |
SERVER_PORT | 服务器的监听端口 |
REQUEST_METHOD | GET 或 POST |
REMOTE_HOST | 客户端域名 |
REMOTE_ADDR | 客户端 IP 地址 |
CONTENT_TYPE | 仅供 POST 使用,表示所请求对象的 MIME 类型 |
CONTENT_LENGTH | 仅供 POST 使用,表示所请求对象的字节数 |
加载 CGI 程序前,子进程会用 dup2()
将 stdout
重定向到 connect_fd
,从而传递给客户端。
详见 serve_dynamic()
。
adder.c
该程序由服务端子进程运行,负责解析 QUERY_STRING
,计算求和结果,构建响应文本,将向 stdout
输出。
关键代码:
#include "csapp.h"
int main(void) {
char *buf, *p;
char arg1[MAXLINE], arg2[MAXLINE], content[MAXLINE];
int n1=0, n2=0;
/* Extract the two arguments */
if ((buf = getenv("QUERY_STRING")) != NULL) {
p = strchr(buf, '&'); // 返回第一个 '&' 的地址
*p = '\0';
strcpy(arg1, buf); n1 = atoi(arg1);
strcpy(arg2, p+1); n2 = atoi(arg2);
}
/* Make the response body */
sprintf(content, /* ... */);
/* Generate the HTTP response */
printf("Connection: close\r\n");
printf("Content-length: %d\r\n", (int)strlen(content));
printf("Content-type: text/html\r\n\r\n");
printf("%s", content);
fflush(stdout); // 子进程将 stdout 重定向到 connect_fd
exit(0);
}
tiny.c
main()
负责创建监听套接字套接字,等待服务请求,建立连接后将主要工作转交给 doit()
。
关键代码:
int main(int argc, char **argv) {
int listen_fd, connect_fd;
char hostname[MAXLINE], port[MAXLINE];
socklen_t client_len;
struct sockaddr_storage client_addr;
if (argc != 2) { /* ... */ }
listen_fd = Open_listenfd(argv[1]);
while (1) { // 迭代型服务端
client_len = sizeof(client_addr);
connect_fd = Accept(listen_fd, (SA *)&client_addr, &client_len);
Getnameinfo((SA *)&client_addr, client_len,
hostname, MAXLINE, port, MAXLINE, 0);
printf("Accepted connection from (%s, %s)\n", hostname, port);
doit(connect_fd); // 处理请求
Close(connect_fd);
}
}
doit()
负责接收并解析单个请求,根据 parse_uri()
返回值选择执行 serve_static()
或 serve_dynamic()
。
若发现错误,则用 clienterror()
输出错误信息。
关键代码:
void doit(int fd) {
int is_static;
struct stat sbuf;
char buf[MAXLINE], method[MAXLINE], uri[MAXLINE], version[MAXLINE];
char filename[MAXLINE], cgiargs[MAXLINE];
rio_t rio;
Rio_readinitb(&rio, fd);
if (!Rio_readlineb(&rio, buf, MAXLINE))
return;
printf("%s", buf);
sscanf(buf, "%s %s %s", method, uri, version);
if (strcasecmp(method, "GET")) { // 只支持 GET
clienterror(fd, method, "501", "Not Implemented",
"Tiny does not implement this method");
return;
}
read_requesthdrs(&rio); // 打印到服务端 stdout
is_static = parse_uri(uri, filename, cgiargs);
if (stat(filename, &sbuf) < 0) {
clienterror(fd, filename, "404", "Not found",
"Tiny couldn't find this file");
return;
}
if (is_static) {
/* ... */
serve_static(fd, filename, sbuf.st_size);
}
else {
/* ... */
serve_dynamic(fd, filename, cgiargs);
}
}
clienterror()
向客户端输出错误信息。
关键代码:
void clienterror(int fd, char *cause, char *errnum,
char *shortmsg, char *longmsg) {
char buf[MAXLINE];
/* Print the HTTP response headers */
sprintf(buf, /* ... */);
Rio_writen(fd, buf, strlen(buf));
/* ... */
/* Print the HTTP response body */
sprintf(buf, /* ... */);
Rio_writen(fd, buf, strlen(buf));
/* ... */
}
read_requesthdrs()
读取并(向服务端 stdout
)打印 request headers。
关键代码:
void read_requesthdrs(rio_t *rp) {
char buf[MAXLINE];
Rio_readlineb(rp, buf, MAXLINE);
printf("%s", buf);
while(strcmp(buf, "\r\n")) {
Rio_readlineb(rp, buf, MAXLINE);
printf("%s", buf);
}
return;
}
parse_uri()
解析 URI,返回 is_static
。
关键代码:
int parse_uri(char *uri, char *filename, char *cgiargs) {
char *ptr;
if (!strstr(uri, "cgi-bin")) { /* Static content */
/* ... */
return 1;
}
else { /* Dynamic content */
/* ... */
return 0;
}
}
serve_static()
关键代码:
void serve_static(int fd, char *filename, int filesize) {
int srcfd;
char *srcp, filetype[MAXLINE], buf[MAXBUF];
/* Send response headers to client */
get_filetype(filename, filetype);
sprintf(buf, "HTTP/1.0 200 OK\r\n");
Rio_writen(fd, buf, strlen(buf));
/* ... */
/* Send response body to client */
srcfd = Open(filename, O_RDONLY, 0);
srcp = Mmap(0, filesize, PROT_READ, MAP_PRIVATE, srcfd, 0); // Section 9.8
Close(srcfd);
Rio_writen(fd, srcp, filesize);
Munmap(srcp, filesize);
}
serve_dynamic()
关键代码:
void serve_dynamic(int fd, char *filename, char *cgiargs) {
char buf[MAXLINE], *emptylist[] = { NULL };
/* Return first part of HTTP response */
sprintf(buf, "HTTP/1.0 200 OK\r\n");
Rio_writen(fd, buf, strlen(buf));
sprintf(buf, "Server: Tiny Web Server\r\n");
Rio_writen(fd, buf, strlen(buf));
if (Fork() == 0) { /* Child */
/* Real server would set all CGI vars here */
setenv("QUERY_STRING", cgiargs, 1);
Dup2(fd, STDOUT_FILENO); /* Redirect stdout to client */
Execve(filename, emptylist, environ); /* Run CGI program */
}
Wait(NULL); /* Parent waits for and reaps child */
}