1 服务器基础架构
1.1 背景知识
Web 服务器使用 HTTP 协议与客户端(即浏览器)通信,而 HTTP 协议又基于 TCP/IP 协议。因此我们要做的工作就是利用 Linux 系统提供的 TCP 通信接口来实现 HTTP 协议。
而 Linux 为我们提供了哪些网络编程接口呢?没错,就是 socket(套接字),我们会在后面详细介绍该接口的使用方式。
另外我们应该清楚 Linux 的系统 I/O 和文件系统的关系。在 Linux 中,所有 I/O 设备都被看作一个个文件,I/O 设备的输入输出被认做读写文件。网络作为一种 I/O 设备,同样被看作文件,而且是一类特殊的文件,即套接字文件。
我们还要对网络通信协议 TCP/IP 有一个大致的了解,知道 IP 地址和端口的作用。
1.2 客户端和服务器编程模型
客户端-服务器编程模型是一个典型的进程间通信模型。客户端进程和服务器进程通常分处两个不同的主机,如下图所示,客户端发送请求给服务器,服务器从本地资源库中查找需要的资源,然后发送响应给客户端,最后客户端(通常是浏览器)处理这个响应,把结果显示在浏览器上。通信示意图如下图所示:
这个过程看起来很简单,但是我们需要深入具体的实现细节。我们知道,TCP 是基于连接的,需要先建立连接才能互相通信。在 Linux 中,socket 为我们提供了方便的解决方案。
每一对网络连接称为一个 socket 对,包括两个端点的 socket 地址,表示如下:
(cliaddr : cliport, servaddr : servport
其中,cliaddr 和 cliport 分别是客户端 IP 地址和客户端端口,servaddr 和 servport 分别是服务器 IP 地址和服务器端口。客户端-服务器配对过程如下图所示:
这对地址和端口唯一确定了连接的双方,在 TCP/IP 协议网络中就能轻松地找到对方。
1.3 HTTP 基础知识
HTTP 是一个属于应用层的面向对象的协议,由于其简捷、快速的方式,适用于分布式超媒体信息系统。它于 1990 年提出,经过几年的使用与发展,得到不断地完善和扩展。
HTTP 协议的主要特点可概括如下:
1.支持客户/服务器模式。
2.简单快速:客户向服务器请求服务时,只需传送请求方法和路径。请求方法常用的有 GET、HEAD、POST。每种方法规定了客户与服务器联系的类型不同。由于 HTTP 协议简单,使得 HTTP 服务器的程序规模小,因而通信速度很快。
3.灵活:HTTP 允许传输任意类型的数据对象。正在传输的类型由 Content-Type 加以标记。
4.无连接:无连接的含义是限制每次连接只处理一个请求。服务器处理完客户的请求,并收到客户的应答后,即断开连接。采用这种方式可以节省传输时间。
5.无状态:HTTP 协议是无状态协议。无状态是指协议对于事务处理没有记忆能力。缺少状态意味着如果后续处理需要前面的信息,则它必须重传,这样可能导致每次连接传送的数据量增大。另一方面,在服务器不需要先前信息时它的应答就较快。
1.4 URL协议
HTTP URL (URL 是一种特殊类型的 URI,包含了用于查找某个资源的足够的信息)的格式如下:
http://host[":"port][abs_path]
http 表示要通过 HTTP 协议来定位网络资源;host 表示合法的 Internet 主机域名或者 IP 地址;port 指定一个端口号,为空则使用缺省端口 80;abs_path 指定请求资源的 URI;如果 URL 中没有给出 abs_path,那么当它作为请求 URI 时,必须以“/”的形式给出,通常这个工作浏览器自动帮我们完成。
例如:
1)输入:www.guet.edu.cn
浏览器自动转换成:http://www.guet.edu.cn/
2)http:192.168.0.116:8080/index.jsp
1.5 请求
http 请求由三部分组成,分别是:请求行、消息报头、请求正文。
请求行以一个方法符号开头,以空格分开,后面跟着请求的 URI 和协议的版本,格式如下:
Method Request-URI HTTP-Version CRLF
其中 Method 表示请求方法;Request-URI 是一个统一资源标识符;HTTP-Version 表示请求的 HTTP协议版本;CRLF 表示回车和换行(除了作为结尾的 CRLF 外,不允许出现单独的 CR 或 LF 字符)。
1.6 使用 socket 处理请求与响应
熟悉 TCP 协议的朋友们应该很容易理解下面的流程图,socket 通信流程如下图所示:
服务器调用 socket 函数获取一个 socket,然后调用 bind 函数绑定本机的 IP 地址和端口,再调用 listen函数开启监听,最后调用 accept 函数等待直到有客户端发起连接。
另一方面,客户端调用 socket 函数获取一个 socket,然后调用 connect 函数向指定服务器发起连接请求,当连接成功或出现错误后返回。若连接成功,服务器端的 accept 函数也会成功返回,返回另一个已连接的socket(不是最初调用 socket 函数得到的 socket),该 socket 可以直接用于与客户端通信。而服务器最初的那个 socket 可以继续循环调用 accept 函数,等待下一次连接的到来。
连接成功后,无论是客户端还是服务器,只要向 socket 读写数据就可以实现与对方 socket 的通信。图中 rio_readlineb 和 rio_written 是封装的 I/O 读写函数,与 Linux 系统提供的 read 和 write 作用基本相同,详情可见源代码。客户端关闭连接时会发送一个 EOF 到服务器,服务器读取后关闭连接,进入下一个循环。
这里面用到的所有 Linux 网络编程接口都定义在<sys/socket.h>头文件中。
1.7 CGI 介绍
CGI(Common Gateway Interface) 是 WWW 技术中最重要的技术之一,有着不可替代的重要地位。CGI是外部应用程序(CGI 程序)与 WEB 服务器之间的接口标准,是在 CGI 程序和 Web 服务器之间传递信息的过程。CGI 规范允许 Web 服务器执行外部程序,并将它们的输出发送给 Web 浏览器,CGI 将 Web 的一组简单的静态超媒体文档变成一个完整的新的交互式媒体。
Common Gateway Interface,简称 CGI。在物理上是一段程序,运行在服务器上,提供同客户端 HTML页面的接口。这样说大概还不好理解。那么我们看一个实际例子:现在的个人主页上大部分都有一个留言本。留言本的工作是这样的:先由用户在客户端输入一些信息,如评论之类的东西。接着用户按一下“发布或提交”(到目前为止工作都在客户端),浏览器把这些信息传送到服务器的 CGI 目录下特定的 CGI 程序中,于是 CGI 程序在服务器上按照预定的方法进行处理。在本例中就是把用户提交的信息存入指定的文件中。然后 CGI 程序给客户端发送一个信息,表示请求的任务已经结束。此时用户在浏览器里将看到“留言结束”的字样。整个过程结束。
CGI 处理步骤:
⑴通过 Internet 把用户请求送到 web 服务器。
⑵web 服务器接收用户请求并交给 CGI 程序处理。
⑶CGI 程序把处理结果传送给 web 服务器。
⑷web 服务器把结果送回到用户。
2 开源代码分析
2.1 主程序
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); //line:netp:tiny:acceptGetnameinfo((SA *) &clientaddr, clientlen, hostname, MAXLINE, port, MAXLINE, 0);printf("Accepted connection from (%s, %s)\n", hostname, port);doit(connfd); //line:netp:tiny:doitClose(connfd); //line:netp:tiny:close}
}
主函数参数需要传入服务器绑定的端口号码,得到这个号码后,调用 Open_listenfd 函数,该函数完成socket、bind、listen 等一系列操作。接着调用 accept 函数等待客户端请求。注意,Accept 是 accept 的包装函数,用来自动处理可能发生的异常,我们只需把它们当成一样的就行了。当 accept 成功返回后,我们拿到了 connected socket descriptor,然后调用 doit 函数处理请求。