每个网络应用都是基于 客户端-服务器模型 ,一个应用是由一个服务器进程和一个或者多个客户端进程组成。服务器管理某种资源,并且通过操作这种资源为它的客户端提供某种服务。
客户端-服务器模型中的基本操作是事务,一个事务由四步组成:
客户端和服务器都是 进程 ,而不是机器。一台主机可以同时运行许多不同的客户端和服务器,而且一个客户端和服务器的事务可以在同一台或是不同的主机上。
客户端和服务器通常运行在不同的主机上,并且通过计算机网络的硬件和软件资源来通信。
对主机而言, 网络是一种I/O设备 ,是数据源和数据接收方。
假设一个客户端运行在主机A上,主机A与LAN1相连,发送数据到运行在主机B上的服务器端,主机B与LAN2相连,这个过程分为8步:
可以把因特网看做是一个世界范围的主机集合:
一个IP地址是一个32位无符号整数,网络程序将IP存放在如下IP地址结构中:
struct in_addr { uint32_t s_addr; /* Address in network byte order(big-endian) */ };
TCP/IP为任意整数数据项定义了统一的网络字节顺序( 大端字节顺序 ),IP地址结构中存放的地址总是以大端法顺序存放的。Unix提供了一些函数在网络和主机字节顺序间实现转换。
#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);
Linux中可以使用HOSTNAME
命令确定主机的点分十进制IP地址:
linux> hostname -i 127.0.1.1
应用程序可以使用inet_pton
和inet_ntop
函数实现IP地址和点分十进制串之间的转换:
#include <arpa/inet.h> // AF_NET表示32位的IPv4地址,若使用AF_INET6则表示128位的IPv6地址 // 将点分十进制串src转换为二进制的网络字节顺序的IP地址dst // 若成功则返回1,若src为非法点分十进制地址则返回0,出错时返回-1 int inet_pton(AF_INET, const char *src, void *dst); // 将二进制的网络字节顺序的IP地址src转换为对应的点分十进制表示 // 并把得到的以null结尾的字符串最多size字节复制到dst中 // 若成功则返回指向点分十进制字符串的指针,出错则返回NULL const char *inet_ntop(AF_INET, const void *src, char *dst, socklen_t size);
因特网客户端和服务器互相通信使用的是IP地址,因特网还定义了域名以及一种将域名映射到IP地址的机制。
因特网定义了域名集合和IP地址集合之间的映射,这个映射通过分布世界范围内的数据库(DNS,域名系统)维护。
DNS数据库由上百万的主机条目结构组成,每条定义了一组域名和一组IP地址之间的映射:
因特网客户端和服务器通过在 连接 上发送和接收字节流来通信:
一个 套接字 是连接的一个端点,每个套接字都有相应的套接字地址,是由一个因特网地址和一个16位的整数端口组成的,用地址:端口
表示。客户端套接字地址中的端口是由内核自动分配的,称为 临时端口 ;服务器套接字地址中的端口通常是某个 知名端口 ,是和这个服务相对应的。文件/etc/services
包含一张知名名字和知名端口之间的映射。
一个连接是由它两端的套接字地址唯一确定的,这对套接字地址叫做套接字对,可以用下列元组表示:
(cliaddr:cliport, servaddr:servport)
套接字接口是一组函数,它们和Unix I/O函数结合起来,用以创建网络应用。
一个套接字是一个通信的端点,从程序角度来看,一个套接字是一个有 相应描述符的打开文件 。
因特网套接字地址存放在类型为sockaddr_in
的16字节结构中,但是connect
、bind
和accept
函数要求一个指向与协议相关的套接字地址结构的指针,因此定义了套接字函数指向通用的sockaddr
结构的指针,应用程序将与协议特定的结构的指针强制转换为这个通用结构。
/* IP socket address structure */ struct sockaddr_in { uint16_t sin_family; // Protocol family (always AF_INET) uint16_t sin_port; // Port 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) }; /* Generic socket address structure (for connect, bind and accept) */ struct sockaddr { uint16_t sa_family; // Protocol family char sa_data[14]; // Address data };
客户端和服务器使用socket
函数来 创建一个套接字描述符 ,其参数最好使用getaddrinfo
函数生成,这样就与协议无关了。socket
返回的clientfd描述符仅是部分打开的,还不能用于读写。
#include <sys/types.h> #include <sys/socket.h> // 成功时返回非负描述符,出错返回-1 int socket(int domain, int type, int protocol); // 使用硬编码使套接字成为连接的一个端点 clientfd = Socket(AF_INET, SOCK_STREAM, 0);
客户端通过调用connect
函数 建立和服务器的连接 。connect
函数试图与套接字地址为addr的服务器建立一个因特网连接,其中addrlen是sizeof(sockaddr_in)
。connect
函数会阻塞,一直到连接成功建立或是发生错误。如果成功,clientfd描述符现在就可以读写了,连接是由套接字对(x:y, addr.sin_addr:addr.sin_port)
确定的。
#include <sys/socket.h> // 成功时返回0,出错返回-1 int connect(int clientfd, const struct sockaddr *addr, socklen_t addrlen);
服务器使用bind
、listen
和accept
函数来和客户端建立连接。
bind
函数告诉内核将addr中的 服务器套接字地址和套接字描述符sockfd联系 起来。
参数addrlen是sizeof(sockaddr_in),最好使用getaddrinfo
函数为bind
提供相关参数。
#include <sys/socket.h> // 成功返回0,出错返回-1 int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
客户端是发起连接请求的主动实体,服务器是等待来自客户端的连接请求的被动实体。默认内核认为socket
函数创建的描述符对应于主动套接字,服务器调用listen
函数告诉内核,描述符是被服务器使用而不是客户端使用的。
listen
函数将sockfd从一个主动套接字 转化为一个监听套接字 ,该套接字可以接受来自客户端的连接请求。backlog参数暗示了内核在开始拒绝连接请求之前,队列中要排队的未完成的连接请求数量。
#include <sys/socket.h> // 成功返回0,出错返回-1 int listen(int sockfd, int backlog);
服务器通过调用accept
函数 等待来自客户端的连接请求 。
accept
函数等待来自客户端的连接请求到达侦听描述符listenfd,然后在addr中填写客户端的套接字地址,并返回一个 已连接描述符 ,这个描述符可以被用来利用Unix I/O函数与客户端通信。
#include <sys/socket.h> // 成功时返回非负连接描述符,出错返回-1 int accept(int listenfd, struct sockaddr *addr, int *addrlen);
为了编写 独立于任何特定版本的IP协议 的网络程序,可以使用getaddrinfo
和getnameinfo
函数。
getaddrinfo
函数将主机名、主机地址、服务名和端口号的字符串表示 转化成套接字地址结构 。
给定host和service,getaddrinfo
返回一个result,它指向addrinfo
结构的链表,其中每个结构指向一个对应于host和service的套接字地址结构。客户端调用getaddrinfo
后,会遍历这个列表,依次尝试每个套接字地址,直到调用socket
和connect
成功。服务器也会尝试遍历列表中的每个套接字地址,直到调用socket
和bind
成功。
为了避免内存泄漏,应用程序最后要调用freeaddrinfo
释放该链表。
gai_strerror
可以把getaddrinfo
返回的非零错误代码转化为消息字符串。
#include <sys/types.h> #include <sys/socket.h> #include <netdb.h> // 成功时返回0, 错误时返回非0错误码 // host参数可以是域名或点分十进制IP地址 // service可以是服务名(如http),或十进制端口号 // host和service中必须指定两者中的一个 // hints是一个addrinfo结构,实际中用memset将结构清零,然后设置一些字段 // 默认最多返回三个addrinfo结构,每个ai_socktype字段不同:连接/数据报/原始套接字 // hints的ai_flags设置AI_PASSIVE时说明套接字地址用作监听套接字,此时host参数应该为NUNLL // hints的ai_flags(位掩码)设置AI_ADDRCONFIG是使用连接推荐使用的标志 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); // getaddrinfo创建输出列表中的addrinfo结构时会填写除ai_flags外的每个字段 // addrinfo中的字段是不透明的,可以直接传递给套接字接口中的函数,应用程序代码无需再做任何处理 struct addrinfo { int ai_flags; // Hints argument flags int ai_family; // First arg to socket function int ai_socktype; // Second arg to socket function 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 };
getnameinfo
函数将一个套接字地址结构 转换成相应的主机和服务名字符串 。
getnameinfo
将套接字地址结构sa转换为对应的主机和服务名字符串,并复制到host和service缓冲区。
#include <sys/socket.h> #include <netdb.h> // 成功时返回0,错误时返回非零的错误代码 // sa指向大小为salen字节的套接字地址结构 // host指向大小为hostlen字节的缓冲区 // service指向大小为servlen字节的缓冲区,host和service至少设置其中之一不为NULL int getnameinfo(const struct sockaddr *sa, socklen_t salen, char *host, size_t hostlen, char *service, size_t servlen, int flags);
客户端和服务器互相通信时可以使用高级的辅助函数。
客户端调用open_clientfd
建立与服务器的连接。
// 客户端与运行在hostname主机port端口上的服务器建立连接 // 返回一个打开的套接字描述符,该描述符可以用Unix I/O函数做输入输出 int open_clientfd(char *hostname, char *port) { int clientfd; 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 */ Getaddrinfo(hostname, port, &hints, &listp); /* Walk the list for one that we can successfully connect to */ for(p = listp; p; p = p->ai_next) { /* Create a socket descriptor */ if((clientfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol)) < 0) continue; /* Socket failed, try the next */ if(connect(clientfd, p->ai_addr, p->ai_addrlen) != -1) break; /* Success */ Close(clientfd); /* Connect failed, try another */ } /* Clean up*/ Freeaddrinfo(listp); if(!p) /* All connects failed */ return -1; else /* The last connect succeeded */ return clientfd; }
服务器调用open_listenfd
函数创建一个监听描述符,准备好接收连接请求。
// 打开和返回一个监听描述符,这个描述符准备好在端口port上接收连接请求 int open_listenfd(char *port) { struct addrinfo hints, *listp, *p; int listenfd, optval = 1; /* Get a list of potential server addresses */ memset(&hints, 0, sizeof(struct addrinfo)); hints.ai_socktype = SOCK_STREAM; /* Accept connections */ hints.ai_flags = AI_PASSIVE | AI_ADDRCONFIG; /* ... on any IP address */ hints.ai_flags |= AI_NUMERICSERV; /* ... using port number */ Getaddrinfo(NULL, port, &hints, &listp); /* Walk the list for one that we can bind to */ for(p = listp; p; p = p->ai_next) { /* Create a socket descriptor */ if((listenfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol)) < 0) continue; /* Socket failed, try the next */ /* Eliminates "Address already in use" error from bind */ Setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, (const void *)&optval, sizeof(int)); /* Bind the descriptor to the address */ if(bind(listenfd, p->ai_addr, p->ai_addrlen) == 0) break; /* Success */ Close(listenfd); /* Bind failed, try the next */ } /* Clean up */ Freeaddrinfo(listp); if(!p) /* No address worked */ return -1; /* Make it a listening socket ready to accept connection requests */ if(listen(listenfd,, LISTENQ) < 0) { Close(listenfd); return -1; } return listenfd; }
Web客户端和服务器之间的交互用的是一个基于文本的应用级协议: HTTP 。
一个Web客户端(浏览器)打开一个到服务器的因特网连接,并且请求某些内容,服务器响应所请求的内容,然后关闭连接,浏览器读取这些内容并把它显示在屏幕上。
对于Web客户端和服务器而言,内容是与一个MIME(Multipurpose Internet Mail Extensions,多用途的网际邮件扩充协议)类型相关的字节序列。
Web服务器有两种方式向客户端提供内容:
Web服务器返回的内容与它管理的某个文件相关联,这些文件都有一个唯一的名字,叫做URL(Universal Resource Locator,通用资源定位符)。
一个HTTP请求行的形式是:
method URI version
HTTP支持许多方法,包括GET、POST、HEAD等。
URI(Uniform Resource Identifier,统一资源标识符)包括文件名和可选的参数。
version表明了请求遵循的HTTP版本,最新的HTTP版本为HTTP/2.0。
HTTP标准要求每个文本行都由一对回车和换行符来结束。
一个HTTP相应行的格式是:
version status-code status-message
状态码是一个3位的正整数,常见的状态码有:
TINY是一个简单的Web服务器,假设静态内容的主目录就是它的当前目录,可执行文件目录是./cgi-bin。
构造一个长时间运行而不崩溃的健壮Web服务器是一件困难的任务,比如一个健壮的服务器必须捕获SIGPIPE信号,并且检查write
函数调用是否有EPIPE错误。
/* * tiny.c - A simple, iterative HTTP/1.0 Web server that uses the * GET method to serve static and dynamic content */ #include "csapp.h" void doit(int fd); void clienterror(int fd, char *cause, char *errnum, char *shortmsg, char *longmsg); void read_requesthdrs(rio_t *rp); int parse_uri(char *uri, char *filename, char *cgiargs); void serve_static(int fd, char *filename, int filesize); void serve_dynamic(int fd, char *filename, char *cgiargs); void get_filetype(char *filename, char *filetype); int main(int argc, char **argv) { int listenfd, connfd; char hostname[MAXLINE], port[MAXLINE]; socklen_t clientlen; struct sockaddr_storage clientaddr; /* Check command-line args */ if(argc != 2) { fprintf(stderr, "usage: %s <port>\n", argv[0]); exit(1); } listenfd = Open_listenfd(argv[1]); while(1) { clientlen = sizeof(clientaddr); connfd = Accept(listenfd, (SA *)&clientaddr, &clientlen); Getnameinfo((SA *)&clientaddr, clientlen, hostname, MAXLINE, port, MAXLINE, 0); printf("Accept connection from (%s, %s)\n", hostname, port); doit(connfd); Close(connfd); } } 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; /* Read request line and headers */ Rio_readinitb(&rio, fd); Rio_readlineb(&rio, buf, MAXLINE); printf("Request headers:\n"); printf("%s", buf); sscanf(buf, "%s %s %s", method, uri, version); if(strcasecmp(method, "GET")) { clienterror(fd, method, "501", "Not implemented", "Tiny does not implement this method"); return; } read_requesthdrs(&rio); /* Parse URI from GET request */ 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 content */ { if(!(S_ISREG(sbuf.st_mode)) || !(S_IRUSR & sbuf.st_mode)) { clienterror(fd, filename, "403", "Forbidden", "Tiny couldn't read the file"); return; } serve_static(fd, filename, sbuf.st_size); } else /* Serve dynamic content */ { if(!(S_ISREG(sbuf.st_mode)) || !(S_IXUSR & sbuf.st_mode)) { clienterror(fd, filename, "403", "Forbidden", "Tiny couldn't run the CGI program"); return; } serve_dynamic(fd, filename, cgiargs); } } void clienterror(int fd, char *cause, char *errnum, char *shortmsg, char *longmsg) { char buf[MAXLINE], body[MAXBUF]; /* Build the HTTP response body */ sprintf(body, "<html><title>Tiny Error</title>"); sprintf(body, "%s<body bgcolor=""ffffff"">\r\n", body); sprintf(body, "%s%s: %s\r\n", body, errnum, shortmsg); sprintf(body, "%s<p>%s: %s</p>\r\n", body, longmsg, cause); sprintf(body, "%s<hr><em>The Tiny Web server</em>\r\n", body); /* Print the HTTP response */ sprintf(buf, "HTTP/1.0 %s %s\r\n", errnum, shortmsg); Rio_writen(fd, buf, strlen(buf)); sprintf(buf, "Content-type: text/html\r\n"); Rio_writen(fd, buf, strlen(buf)); sprintf(buf, "Content-length: %d\r\n\r\n", (int)strlen(body)); Rio_writen(fd, buf, strlen(buf)); Rio_writen(fd, body, strlen(body)); } void read_requesthdrs(rio_t *rp) { char buf[MAXLINE]; Rio_readlineb(rp, buf, MAXLINE); while(strcmp(buf, "\r\n")) { Rio_readlineb(rp, buf, MAXLINE); printf("%s", buf); } return; } int parse_uri(char *uri, char *filename, char *cgiargs) { char *ptr; if(!strstr(uri, "cgi-bin")) { /* Static content */ strcpy(cgiargs, ""); strcpy(filename, "."); strcat(filename, uri); if(uri[strlen(uri) - 1] == '/') strcat(filename, "home.html"); return 1; } else { /* Dynamic content */ ptr = index(uri, '?'); if(ptr) { strcpy(cgiargs, ptr+1); *ptr = '\0'; } else strcpy(cgiargs, ""); strcpy(filename, "."); strcat(filename, uri); return 0; } } 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"); sprintf(buf, "%sServer: Tiny Web Servre\r\n", buf); sprintf(buf, "%sConnection: close\r\n", buf); sprintf(buf, "%sContent-length: %d\r\n", buf, filesize); sprintf(buf, "%sContent-type: %s\r\n\r\n", buf, filetype); Rio_writen(fd, buf, strlen(buf)); printf("Response header:\n"); printf("%s", buf); /* Send response body to client */ srcfd = Open(filename, O_RDONLY, 0); srcp = Mmap(0, filesize, PROT_READ, MAP_PRIVATE, srcfd, 0); Close(srcfd); Rio_writen(fd, srcp, filesize); Munmap(srcp, filesize); } /* * get_filetype - Derive file type from filename */ void get_filetype(char *filename, char *filetype) { if(strstr(filename, ".html")) strcpy(filetype, "text/html"); else if(strstr(filename, ".gif")) strcpy(filetype, "image/gif"); else if(strstr(filename, ".png")) strcpy(filetype, "image/png"); else if(strstr(filename, ".jpg")) strcpy(filetype, "image/jpeg"); else strcpy(filetype, "text/plain"); } 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 */ }
a
--
123456789
更改id为3
--
test
更改id为2
--
commentor
伪造名称???
--
hhh
伪造名称???
--
yayay