csapp之第11章:网络IO.md

1. 网络架构

需理解基本的客户端-服务器编程模型,以及如何编写使用因特网提供的服务的客户端-服务器程序。最后将所有概念结合开发小但功能全的web服务器。现今网络应用随处可见,有趣的是,所有的网络应用都基于相同的基本编程模型,有相似的整体逻辑结构、依赖相同的编程接口。大多数网络应用程序都基于客户端-服务器模型,一服务器进程和一或多客户端进程(客户端和服务器是进程而不是机器或主机,这点很重要),服务器管理资源,服务器为客户端提供服务,由客户端请求服务器。总的来说四步:请求、操作、响应、处理。

客户端进程发送请求给服务器进程,服务器进程获取所需资源并响应客户端进程的请求,客户端进程收到响应后展示给用户。对主机而言,网络只是一种I/O设备,是数据源和数据接收方,网络相关的处理,都是通过网络适配器来完成的,具体在硬件上为(右下角):

2. 网络

根据网络的应用范围和架构层级,可分成三个部分:

  • SAN - System Area Network

  • LAN - Local Area Network

  • WAN - Wide Area Network

2.1 最底层 - Ethernet Segment

一个以太网段(Ethernet Segment)包括电缆和集线器的小盒子。由若干主机(hosts)通过交换机(hub)连接,通常范围是房间或一层楼,如图所示:

  • 以太网段由一组主机组成,主机通过网线(双绞线)连接到一个集线器上

  • 每个 Ethernet 适配器有唯一的48位MAC 地址,主机以帧的形式向其他主机发送比特

  • Hub会把每个端口发来的数据复制到其他端口,所有的主机都可以看到所有的数据(注意安全问题)

2.2 下一层

2.2.1 Bridged Ethernet Segment

桥接以太网段是通常范围是一层楼,通过不同的 bridge 来连接不同的 ethernet segment。Bridge 知道从某端口出发可达的主机,并有选择的在端口间复制数据。

为了简化,可以认为,所有的 hubs, bridges和wires可以抽象为连接在一条线的主机集合,如下图所示:

2.2.2 internets

多个不兼容的LAN可通过 router来进行物理上的连接,连接起来的网络叫internet(注意是小写)

internet 的逻辑结构为:

  • Ad hoc interconnection of networks

  • 没有特定的拓扑结构

  • 不同的 router 和 link 差异可能很大

  • 通过在不同的网络间跳转来传递 packet

  • Router是不同网络间的连接

  • 不同的 packet 可能会走不同的路线

2.3 网络协议

在不同的 LAN 和 WAN 中传输数据,遵守的规矩就是协议。协议是一组管理主机和路由器在网络间传输数据时如何协作的规则,消除了不同网络之间的差异。协议负责做的事情有:

  • 提供命名机制:定义主机地址的统一格式、每个主机和路由器至少有个独立的internet地址标识

  • 提供传送机制:定义标准的传输单元packet,Packet 包含 header 和 payload,header 包括 packet size, source 和 destination address。payload 包括需要传输的数据

具体的数据传输如下图所示,这里 PH = Internet packet header, FH = LAN frame header

主机A发送数据到B的8个基本步骤:

  1. A上的客户端进行系统调用,从客户端虚拟地址空间复制数据到内核缓冲区

  2. A上的协议软件通过数据前附加包头和LAN1帧头,创建LAN1的帧,包头寻址到B,LAN1帧头寻址到路由器,然后传送此帧到适配器。注意LAN1帧有效载荷是互联网络包,而该包有效载荷是实际用户数据,这种封装是基本网络互联方法

  3. LAN1适配器复制该帧到网络上

  4. 此帧到达路由器时,路由器的LAN1适配器从电缆上读取它,并把它传到协议软件

  5. 路由器从包头取出目的地址,将此作为路由表索引,确定向哪里转发该包,本例是LAN2,路由器剥离旧的LAN1帧头,加上寻址到B的新的LAN2帧头,并把得到的帧传送到适配器

  6. 路由器的LAN2适配器复制该帧到网络上

  7. 此帧到B时,它的适配器从电缆读到此帧,将它传送到协议软件

  8. B上的协议软件剥落包头和帧头,服务器进行一个读取这些数据的系统调用时,协议软件最终将得到的数据复制到服务器的虚拟地址空间

其他问题:

如果不同的网络有不同的最大帧大小呢?

路由器如何知道在哪里转发帧?

当网络拓扑发生变化时,如何通知路由器?

如果数据包丢失了怎么办?

这些(和其他)问题由称为计算机网络的系统领域解决

2.4 TCP/IP协议

Internet是internet最著名的例子。主要基于 TCP/IP 协议族:

  • IP (Internet Protocal):提供主机基本命名方案和主机到主机的数据包的不可靠交付能力

  • UDP (Unreliable Datagram Protocol):使用IP在进程间提供不可靠的数据报

  • TCP (Transmission Control Protocol):使用IP在进程间提供可靠的字节流

通过混合使用Unix I/O和socket接口函数来访问

  • 主机有 32 位的 IP 地址 - 23.235.46.133

  • IP地址映射到称为Internet域名的标识符

  • 不同主机之间的进程,可以通过连接来交换数据

2.4.1 IP 地址

用IP address struct来存储,且IP地址是大端存储

// Internet address structure
struct in_addr {
    uint32_t s_addr;    // network byte order (big-endian)
}

为了方便读,一般用下面的形式来进行表示IP 地址:0x8002C2F2 = 128.2.194.242,具体的转换可以使用 getaddrinfogetnameinfo 函数

2.4.2 Internet主机域名

主要了解Domain Naming System(DNS)的概念,用来做IP地址到域名的映射。程序员可将DNS数据库看作是数百万个主机条目的集合。每个主机都有个本地定义的域名localhost,总是映射到环回地址

$ nslookup www.twitter.com
Server:     8.8.8.8
Address:    8.8.8.8#53
Non-authoritative answer:
www.twitter.com canonical name = twitter.com.
Name:   twitter.com
Address: 199.16.156.6
Name:   twitter.com
Address: 199.16.156.198
Name:   twitter.com
Address: 199.16.156.230
Name:   twitter.com
Address: 199.16.156.70

用hostname确定本地主机的真实域名,一个或多个域名可映射到同个IP地址,多个域名对应多个IP

2.4.3 Internet 连接

客户端和服务器通过连接(connection)来发送字节流,特点是:

  • 点对点: 连接一对进程

  • 全双工: 数据同时可以在两个方向流动

  • 可靠: 字节的发送的顺序和收到的一致

Socket 则可以认为是 connection 的 endpoint,socket 地址是一个 IPaddress:port 对。

Port(端口)是个 16 位的整数,用来标识不同的进程,利用不同的端口来连接不同的服务:

  • Well-known port: Associated with some

    service

    provided by a server(在 linux 系统上可以在

    /etc/services
    

    中查看具体的信息)

  • echo server: 7/echo

  • ssh server: 22/ssh

  • email server: 25/smtp

  • web servers: 80/http

连接

2.5 Socket 接口

一系列系统级的函数,和Unix I/O 配合构造网络应用。对于 kernel 来说,socket 是 endpoint of communication;对于应用程序来说,socket 是用来读写的文件描述符。客户端和服务器通过读写对应套接字描述符进行通信。普通文件I/O和套接字I/O主要区别是程序如何“打开”套接字描述符

  • 服务器:接受连接请求、重复输入的行

  • 客户端:向服务器请求连接、重复(终端读、发送给服务器、从服务器读应答、在终端打印)

2.5.1 套接字地址结构

从Linux内核看,一个socket是通信的一个端点,从Linux程序看,socket是一个有相应描述符的打开文件。通用sockaddr是connectbindaccept的参数,因为在涉及socket接口时,C没有通用指针,因此sockaddr很有必要。

特定套接字(IPv4)地址,对于接受套接字地址参数的函数,必须将(struct sockaddr_in *)(注意:_in是internet的缩写,而不是input的缩写) 转成(struct sockaddr *)作为函数参数

2.5.2 socket函数

int socket(intdomain, int type, int protocol)客户端和服务器使用socket函数来创建一个socket描述符。最佳实践是使用getaddrinfo自动生成参数,这样代码就独立于协议。

2.5.3 connect函数

int connect(int clientfd, const struct sockaddr *addr, sockel_t addrlen) 客户端通过调用connect函数来建立和服务器的连接。connect函数试图与套接字地址为addr的服务器建立一个因特网连接,addrlen是sizeof(sockaddr_in)。connect函数会阻塞,直到连接成功或者发生错误。若成功,clientfd描述符就准备好可以读写了,且得到的链接是套接字对(x:y, addr.sin_addr:addr.sin_port)。X表示客户端的IP地址,y表示临时端口,对唯一确定了客户端主机上的客户端进程。对于socket最好使用getaddrinfo来为connect提供参数。

2.5.4 bind函数

int bind(int sockfd, const struct sockaddr*addr, socklen_t addrlen)bind函数告诉内核将addr中的服务器套接字地址和套接字描述符sockfd联系起来,getaddrinfo为bind提供参数比较好。进程可通过读取描述符sockfd来读取到达端点为addr的连接的字节,类似地,对sockfd的写入是沿着端点为addr的连接传输的

2.5.5 listen函数

默认情况下,内核假设来自套接字函数的描述符是一个将活动的套接字。服务器调用listen函数告诉内核,描述符是被服务器使用的。

int listen(int sockfd, int backlog)函数将sockfd从一个主动socket转化为一个listeningsocket,该socket可以接受来自客户端的连接请求;backlog参数暗示内核在开始拒绝连接请求前,队列中要排队的未完成的连接请求数量。

2.5.6 accept函数

服务器调用accept函数来等待来自客户端的连接请求。

int accept(int listenfd, struct sockaddr *addr, int *addrlen)该函数等待来自客户端的连接请求到达侦听描述符listenfd,之后再addr中填写客户端的socket地址,返回一个已连接描述符,这个descriptor可以用来Unix I/O函数与客户端通信。

其中,listening descriptor是作为客户端连接请求的一个端点,通常被创建一次,并存在于服务器的整个生命周期,而已连接描述符是客户端和服务器间已经建立起来的连接的一个端点。服务器每次接受连接请求时都会创建一次,只存在于服务器为一个客户端服务的过程中。

注意:区分监听描述符和已连接描述符间的区别,看起来是不必要的复杂,但它使得可以建立并发服务器,能同时处理许多客户端连接。

2.5.7 主机和服务的转换

Linux提供一些强大的函数getaddrinfo,getnameinfo实现二进制套接字地址结构和主机名,主机地址,服务名和端口号的字符串表示之间的相互转化。和套接字接口一起使用时,这些函数能使我们编写独立于任何特定版本的IP协议的网络程序。

2.5.7.1 getaddrinfo函数

将主机名、主机地址、端口和服务名的字符串表示转换为套接字地址结构的现代方法,代替gethostbynamegetservbyname,优点是可重入能被线程安全使用,允许编写协议独立可移植的代码,但较复杂,幸运的是少数使用模式就够了。

给定host和service, getaddrinfo返回指向addrinfo结构体链表的result,每个结构体指向对应的套接字地址结构体,并且包含套接字接口函数的参数。辅助函数有freeadderinfogai_strerror

get_addrinfo返回的链接表

addrinfo结构体,getaddrinfo返回的每个addrinfo结构体都包含可直接传给套接字函数的参数,也指向可直接传递给connectbind套接字地址结构体

2.5.7.2 getnameinfo函数

getnameinfo是getaddrinfo相反功能的函数,将套接字地址转换为相应的主机和服务,代替gethostbyaddr和getservbyport函数,同样也是可重入和协议独立

3 简单服务器实现

3.1 架构总览

写服务器,最重要的就是理清思路,上节课我们介绍了诸多概念,尤其是最后提到的 getaddrinfogetnameinfo,都是我们在搭建过程中必不可少的工具。参考上面的流程图,整个的工作流程有 5 步:

  1. 开启服务器
  • 前面这种写法是协议相关的,建议使用 getaddrinfo 生成的参数来进行配置,这样就是协议无关的了

  • AF_INET 表示在使用 32 位 IPv4 地址

  • int socket(int domain, int type, int protocol)

  • getaddrinfo:设置服务器的相关信息

  • socket:创建 socket descriptor,也就是之后用来读写的 file descriptor

  • 例如 int clientfd = socket(AF_INET, SOCK_STREAM, 0);

  • SOCK_STREAM 表示这个 socket 将是 connection 的 endpoint

  • bind:请求 kernel 把 socket address 和 socket descriptor 绑定

  • sockfd 从 active socket 转换成 listening socket,用来接收客户端的请求

  • backlog 的数值表示 kernel 在接收多少个请求之后(队列缓存起来)开始拒绝请求

  • accept: 开始等待客户端请求

  • ``int accept(int listenfd, SA *addr, int *addrlen);`

  • 等待绑定到 listenfd 的连接接收到请求,然后把客户端的 socket address 写入到 addr,大小写入到 addrlen

  • listen:默认从 socket函数中得到的 descriptor 默认是 active socket(也就是客户端的连接),调用 listen函数告诉 kernel 这个 socket 是被服务器使用的

  • int bind(int sockfd, SA *addr, socklen_t addrlen);

  • The process can read bytes that arrive on the connection whose endpoint is addr by reading from descriptor sockfd

  • Similarly, writes to sockfd are transferred along connection whose endpoint is addr

  • 最好是用 getaddrinfo 生成的参数作为 addraddrlen

  • int listen(int sockfd, int backlog);

  • 返回一个 connected descriptor 用来进行信息传输(类似 Unix I/O)

  1. 开启客户端设定访问地址,尝试连接)
  • int connect(int clientfd, SA *addr, socklen_t addrlen);

  • 尝试与在 socker address addr 的服务器建立连接

  • 如果成功 clientfd 可以进行读写

  • connection 由 socket 对描述 (x:y, addr.sin_addr:addr.sin_port)

  • x 是客户端地址,y 是客户端临时端口,后面的两个是服务器的地址和端口

  • 最好是用 getaddrinfo 生成的参数作为 addraddrlen

  • getaddrinfo: 设置客户端的相关信息,具体可以参见 图1&2

  • socket: 创建 socket descriptor,也就是之后用来读写的 file descriptor

  • connect:客户端调用来建立和服务器的连接

  1. 交换数据(主要是一个流程循环,客户端向服务器写入,就是发送请求;服务器向客户端写入,就是发送响应)
  • [Client]rio_writen: 写入数据,相当于向服务器发送请求

  • [Client]rio_readlineb: 读取数据,相当于从服务器接收响应

  • [Server]rio_readlineb: 读取数据,相当于从客户端接收请求

  • [Server]rio_writen: 写入数据,相当于向客户端发送响应

  1. 关闭客户端
  • [Client]close: 关闭连接
  1. 断开客户端(服务接收到客户端发来的 EOF 消息之后,断开已有的和客户端的连接)
  • [Server]rio_readlineb: 收到客户端发来的关闭连接请求,直到遇到EOF

  • [Server]close: 关闭与客户端的连接

注意:EOF的概念使人迷惑,首先并没有EOF字符这个东西,其次EOF是由内核检测到的一种条件,程序接收到由read返回的零返回码时,磁盘文件若当前位置超出文件长度,网络中当一进程关闭连接它的那一端时,连接另一端的进程试图读取流中最后一个字节后的字节时,都会发生EOF

3.2 Client

用来建立和服务器的连接,协议无关

int open_clientfd(char *hostname, char *port) {
    int clientfd;
    struct addrinfo hints, *listp, *p;
    //Get a list of potential server address
    memset(&hints, 0, sizeof(struct addrinfo));
    hints.ai_socktype = SOCK_STREAM; // Open a connection
    hints.ai_flags = AI_NUMERICSERV; // using numeric port arguments
    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
        // 这里使用从 getaddrinfo 中得到的参数,实现协议无关
        if ((clientfd = socket(p->ai_family, p->ai_socktype,
                               p->ai_protocol)) < 0)
            continue; // Socket failed, try the next
        // Connect to the server
        // 这里使用从 getaddrinfo 中得到的参数,实现协议无关
        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 connections failed
        return -1;
    else // The last connect succeeded
        return clientfd;
}

3.3 Server

创建 listening descriptor,用来接收来自客户端的请求,协议无关

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 connection
    hints.ai_flags = AI_PASSIVE | AI_ADDRCONFIG; // on any IP address
    hints.ai_flags |= AI_NUMERICSERV; // using port number
    // 因为服务器不需要连接,所以原来填写地址的地方直接是 NULL
    getaddrinfo(NULL, 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
        // 这里使用从 getaddrinfo 中得到的参数,实现协议无关
        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 another
    }
    // 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;
}

3.4 简单socket 服务器实例

3.4.1 客户端

这个客户端做的事情很简单,就是把一段用户输入的文字发送到服务器,然后再把从服务器接收到的内容显示到输出中,具体可以参见注释

// echoclient.c
#include "csapp.h"
int main (int argc, char **argv) {
    int clientfd;
    char *host, *port, buf[MAXLINE];
    rio_t rio;
    host = argv[1];
    port = argv[2];
    // 建立连接(前面已经详细介绍)
    clientfd = Open_clientfd(host, port);
    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);
    exit(0);
}

3.4.2 服务器

服务器做的工作也很简单,接收到从客户端发送的信息,然后返回一个一模一样的。具体参加注释。

// echoserveri.c
#include "csapp.h"
void echo(int connfd);
int main(int argc, char **argv){
    int listenfd, connfd;
    socklen_t clientlen;
    struct sockaddr_storage clientaddr; // Enough room for any addr
    char client_hostname[MAXLINE], client_port[MAXLINE];
    // 开启监听端口,注意只开这么一次
    listenfd = Open_listenfd(argv[1]);
    while (1) {
        // 需要具体的大小
        clientlen = sizeof(struct sockaddr_storage); // Important!
        // 等待连接
        connfd = Accept(listenfd, (SA *)&clientaddr, &clientlen);
        // 获取客户端相关信息
        Getnameinfo((SA *) &clientaddr, clientlen, client_hostname,
                     MAXLINE, client_port, MAXLINE, 0);
        printf("Connected to (%s, %s)\n", client_hostname, client_port);
        // 服务器具体完成的工作
        echo(coonfd);
        Close(connfd);
    }
    exit(0);
}

void echo(int connfd) {
    size_t n;
    char buf[MAXLINE];
    rio_t rio;
    // 读取从客户端传输过来的数据
    Rio_readinitb(&rio, connfd);
    while((n = Rio_readlineb(&rio, buf, MAXLINE)) != 0) {
        printf("server received %d bytes\n", (int)n);
        // 把从 client 接收到的信息再写回去
        Rio_writen(connfd, buf, n);
    }
}

3.5 proxy代理

代理是客户端和服务器之间的中介,对客户端来说,代理像服务器,对服务器来说代理像客户端。需要代理的原因是当请求和响应通过时,可执行有用的功能。如缓存、日志、匿名化、过滤、代码转化

4. web服务器

4.1 web基础知识

客户端和服务器使用超文本传输协议(HTTP)进行通信。客户端与服务器端建立TCP连接,客户端请求内容,服务器响应请求的内容,客户端和服务器的紧密连接

4.2 web内容

web服务器将内容返回给客户端,内容是具有关联MIME(多用途Internet邮件扩展)类型的字节序列,内容由URL标识,MIME类型有HTML文档、无格式文本plain、GIF格式的二进制图片

4.2.1 静态和动态内容

  • 静态内容:存储在文件中并在响应HTTP请求时检索到的内容,如HTML文件、图片、视频、js程序

  • 动态内容:响应HTTP请求实时生成的内容,由服务器代表客户端执行的程序产生的内容,请求标识包含可执行代码的文件,任何URL都可引用静态或动态内容

4.2.2 URLs和客户端和服务器如何使用

URL(Universal Resource Locator):统一资源定位器,如:http://www.cmu.edu:80/index.html客户端用前缀http://www.cmu.edu:80表示协议类型(HTTP)、服务器是www.cmu.edu、端口号:80。服务器用后缀/index.html确定请求是静态还是动态(没有硬性规定,但通常约定可执行文件放在cgi-bin目录下)。后缀中的首字母/表示请求内容的主目录,最小后缀为" / ",服务器扩展为配置的默认文件名index.html

4.2.3 HTTP REQUEST RESPONSE

4.2.4 HTTP例子

4.2.5 HTTP(S)例子

5. Tiny web server

小型web server,239行C代码,但支持静态和动态内容展示,但没有实际的web服务器足够完整和鲁棒性,如可用\n代替\r\n。接受来自客户端的连接,从客户端读请求(通过已连接的socket),URL分成,若URI包含“cgi-bin”则fork创建子进程执行程序,否则静态内容拷贝内容到输出

动态内容展示的问题:

客户端如何将程序参数传递给服务器?

服务器如何将这些参数传递给子进程?

服务器如何将其他与请求相关的信息传递给子进程?

服务器如何捕获由子进程生成的内容?

5.1 CGI

答案是CGI(Common Gateway Interface),因为子进程是根据CGI规范编写的,所以它们通常被称为CGI程序。CGI定义了简单的标准,用于在客户机(浏览器)、服务器和子进程之间传输信息。CGI是生成动态内容的原始标准。已经被其他更快的技术所取代:如fastCGI、Apache modules、Java servlets、Rails controllers避免匆忙创建流程

客户端如何将参数传给服务器,参数通过URI传递。可以直接编码在一个URL输入到浏览器或一个URL在HTML链接,参数以?开始,被&分割,空格用+或%20。其他字节也有类似编码,注意POST请求中哦哦嗯的参数是在主体中而不是URI中传递(编程的本质是数据的可视化),其次服务器通过环境变量 QUERY_STRING将参数传给子进程。子进程在stdout上生成输出。服务器使用dup2将stdout重定向到它所连接的套接字,以此来捕获子进程生成的内容。注意,只有CGI子进程知道内容类型和长度,所以它必须生成那些头文件。

5.2 代码

5.2.1 main程序

tiny是一个迭代服务器,监听在命令行中传递来的端口上的连接请求。调用 open_listenfd 函数打开一个监听套接字以后,执行典型的无限服务器循环,不断地接受连接请求(Accept),执行事务(doit),并关 闭连接的它那一端(Close)。

/*
 * 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 read_requesthdrs(rio_t *rp);
int parse_uri(char *uri, char *filename, char *cgiargs);
void serve_static(int fd, char *filename, int filesize);
void get_filetype(char *filename, char *filetype);
void serve_dynamic(int fd, char *filename, char *cgiargs);
void clienterror(int fd, char *cause, char *errnum, 
         char *shortmsg, char *longmsg);
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:accept
        Getnameinfo((SA *) &clientaddr, clientlen, hostname, MAXLINE, 
                            port, MAXLINE, 0);
        printf("Accepted connection from (%s, %s)\n", hostname, port);
        doit(connfd);
        //line:netp:tiny:doit
        Close(connfd);
        //line:netp:tiny:close
    }
}

5.2.2 doit函数

然后将URI解析为一个文件名和一个可能为空的CGI参数字符串,并且设置 is_static标志,表明请求的是静态内容还是动态内容。如果文件在磁盘上不存在,立即发送一个错误信息给客户端并返回。最后,如果请求的是静态内容,就验证该文件是一个普通文件,而有读权限的就向客户端提供静态内容(serve_static)。相似地,如果请求的是动态内容,就验证该文件是可执行文件就继续,并且提供动态内容(serve_dynamic)。

/*
 * doit - handle one HTTP request/response transaction
 */
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);
    if (!Rio_readlineb(&rio, buf, MAXLINE))  //line:netp:doit:readrequest
    return;
    printf("%s", buf);
    sscanf(buf, "%s %s %s", method, uri, version);
    //line:netp:doit:parserequest
    if (strcasecmp(method, "GET")) {
        //line:netp:doit:beginrequesterr
        clienterror(fd, method, "501", "Not Implemented",
                            "Tiny does not implement this method");
        return;
    }
    //line:netp:doit:endrequesterr
    read_requesthdrs(&rio);
    //line:netp:doit:readrequesthdrs
    /* Parse URI from GET request */
    is_static = parse_uri(uri, filename, cgiargs);
    //line:netp:doit:staticcheck
    if (stat(filename, &sbuf) < 0) {
        //line:netp:doit:beginnotfound
        clienterror(fd, filename, "404", "Not found",
                    "Tiny couldn't find this file");
        return;
    }
    //line:netp:doit:endnotfound
    if (is_static) {
        /* Serve static content */
        if (!(S_ISREG(sbuf.st_mode)) || !(S_IRUSR & sbuf.st_mode)) {
            //line:netp:doit:readable
            clienterror(fd, filename, "403", "Forbidden",
                        "Tiny couldn't read the file");
            return;
        }
        serve_static(fd, filename, sbuf.st_size);
        //line:netp:doit:servestatic
    } else {
        /* Serve dynamic content */
        if (!(S_ISREG(sbuf.st_mode)) || !(S_IXUSR & sbuf.st_mode)) {
            //line:netp:doit:executable
            clienterror(fd, filename, "403", "Forbidden",
                        "Tiny couldn't run the CGI program");
            return;
        }
        serve_dynamic(fd, filename, cgiargs);
        //line:netp:doit:servedynamic
    }
}

5.2.3 clienterror函数

tiny缺乏实际服务器的许多错误处理特性。然而,它会检查一些明显的错误,并把它们报告给客户端。该函数发送一个 HTTP 响应到客户端,在响应行中包含相应的状态码和状态消息,响应主体中包含一个HTML文件,向浏览器用户解释错误。

回想一下,HTML响应应该指明主体中内容的大小和类型。因此选择创HTML内容为一个字符串,这样就可以简单地确定它的大小。还有,请注意所有的输出使用的都是健壮的 rio_writen 函数。

/*
 * clienterror - returns an error message to the client
 */
void clienterror(int fd, char *cause, char *errnum, 
         char *shortmsg, char *longmsg) {
    char buf[MAXLINE];
    /* Print the HTTP response headers */
    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\r\n");
    Rio_writen(fd, buf, strlen(buf));
    /* Print the HTTP response body */
    sprintf(buf, "<html><title>Tiny Error</title>");
    Rio_writen(fd, buf, strlen(buf));
    sprintf(buf, "<body bgcolor=""ffffff"">\r\n");
    Rio_writen(fd, buf, strlen(buf));
    sprintf(buf, "%s: %s\r\n", errnum, shortmsg);
    Rio_writen(fd, buf, strlen(buf));
    sprintf(buf, "<p>%s: %s\r\n", longmsg, cause);
    Rio_writen(fd, buf, strlen(buf));
    sprintf(buf, "<hr><em>The Tiny Web server</em>\r\n");
    Rio_writen(fd, buf, strlen(buf));
}

5.2.4 read_requesthdrs函数

tiny不使用请求报头中的任何信息。它仅调用read_requesthdrs函数来读取并忽略这些报头。注意,终止请求报头的空文本行是由回车和换行符对组成

/*
 * read_requesthdrs - read HTTP request headers
 */
void read_requesthdrs(rio_t *rp) {
    char buf[MAXLINE];
    Rio_readlineb(rp, buf, MAXLINE);
    printf("%s", buf);
    while(strcmp(buf, "\r\n")) {
        //line:netp:readhdrs:checkterm
        Rio_readlineb(rp, buf, MAXLINE);
        printf("%s", buf);
    }
    return;
}

5.2.5 parse_uri函数

tiny假设静态内容的主目录就是它的当前目录,而可执行文件的主目录是./cgi-bin。任何包含字符串 cgi-bin 的 URI都会被认为表示的是对动态内容的请求。默认的文件名是./home.html。该函数将 URI 解析为一个文件名和可选的CGI参数字符串。如果请求的是静态内容,将清除CGI参数字符串,然后将 URI转换为一个Linux 相对路径名,例如 ./index.html。如果URI是用“/”结尾的,将把默认的文件名加在后面。另一方面,如果请求的是动态内容,就会抽取出所有的CGI参数,并将URI剩下的部分转换为一个Linux 相对文件名。

/*
 * parse_uri - parse URI into filename and CGI args
 *             return 0 if dynamic content, 1 if static
 */
int parse_uri(char *uri, char *filename, char *cgiargs) {
    char *ptr;
    if (!strstr(uri, "cgi-bin")) {
        /* Static content */
        //line:netp:parseuri:isstatic
        strcpy(cgiargs, "");
        //line:netp:parseuri:clearcgi
        strcpy(filename, ".");
        //line:netp:parseuri:beginconvert1
        strcat(filename, uri);
        //line:netp:parseuri:endconvert1
        if (uri[strlen(uri)-1] == '/')                   //line:netp:parseuri:slashcheck
        strcat(filename, "home.html");
        //line:netp:parseuri:appenddefault
        return 1;
    } else {
        /* Dynamic content */
        //line:netp:parseuri:isdynamic
        ptr = index(uri, '?');
        //line:netp:parseuri:beginextract
        if (ptr) {
            strcpy(cgiargs, ptr+1);
            *ptr = '\0';
        } else 
                strcpy(cgiargs, "");
        //line:netp:parseuri:endextract
        strcpy(filename, ".");
        //line:netp:parseuri:beginconvert2
        strcat(filename, uri);
        //line:netp:parseuri:endconvert2
        return 0;
    }
}

5.2.6 serve_static函数

tiny提供五种常见类型的静态内容:HTML文件、无格式的文本文件,以及编码为GIF、PNG 和JPG 格式的图片。函数发送一个 HTTP 响应,其主体包含一个本地文件的内容。首先,通过检查文件名的后缀来判断文件类型,并且发送响应行和响应报头给客户端。注意用一个空行终止报头。

/*
 * serve_static - copy a file back to the client 
 */
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);
    //line:netp:servestatic:getfiletype
    sprintf(buf, "HTTP/1.0 200 OK\r\n");
    //line:netp:servestatic:beginserve
    Rio_writen(fd, buf, strlen(buf));
    sprintf(buf, "Server: Tiny Web Server\r\n");
    Rio_writen(fd, buf, strlen(buf));
    sprintf(buf, "Content-length: %d\r\n", filesize);
    Rio_writen(fd, buf, strlen(buf));
    sprintf(buf, "Content-type: %s\r\n\r\n", filetype);
    Rio_writen(fd, buf, strlen(buf));
    //line:netp:servestatic:endserve
    /* 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 file name
 */
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");
}

接着,将被请求文件的内容复制到已连接描述符 fd来发送响应主体。这里的代码是比较微妙的,需要仔细研究。以读方式打开 filename(Open),并获得它的描述符。Linux mmap函数将被请求文件映射到一个虚拟内存空间。调用mmap将文件srcfd的前filesize个字节映射到一个从地址srcp 开始的私有只读虚拟内存区域。一旦将文件映射到内存就不需要描述符,故关闭该文件(Close)。若是执行失败则导致潜在的致命的内存泄漏。接着是到客户端的实际文件传送,最后释放映射的虚拟内存区域,避免潜在的致命的内存泄漏

5.2.7 serve_dynamic函数

tiny通过派生一个子进程并在子进程的上下文中运行一个CGI程序,来提供各种类型的动态内容。serve_dynamic 函数一开始就向客户端发送一个表明成功的响应行,同时还包括带有信息的Server报头。CGI程序负责发送响应的剩余部分。注意,它没有考虑到CGI程序会遇到某些错误的可能性。

/*
 * serve_dynamic - run a CGI program on behalf of the client
 */
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);
        /* Redirect stdout to client */
        Dup2(fd, STDOUT_FILENO);
        /* Run CGI program */
        Execve(filename, emptylist, environ);
    }
    Wait(NULL);/* Parent waits for and reaps child */
}

在发送响应后,派生一个新的子进程。子进程用来自请求URI的CGI参数初始化QUERY_STRING 环境变量。注意,一个真正的服务器还会在此处设置其他的 CGI 环境变量。接着子进程重定向它的标准输出到已连接文件描述符,然后加载并运行CGI程序。因为CGI程序运行在子进程的上下文中,它能够访问所有在调用 execve 函数之前就存在的打开文件和环境变量。因此,CGI程序写到标准输出上的任何东西都将直接送到客户端进程,不会受到任何来自父进程的干涉。其间,父进程阻塞在对wait 的调用中,等待当子进程终止的时候,回收操作系统分配给子进程的资源

6. 总结

客户端和服务器通过使用套接字接口建立连接。一个套接字是连接的一个端点,连接以文件描述符的形式提供给应用程序。使用套接字接口进行通信

Web 服务器使用 HTTP 协议和客户端(例如浏览器)彼此通信。浏览器向服务器请求静态或者动态的内容。对静态内容的请求是通过从服务器磁盘取得文件并把它返回给客户端来服务的。对动态内容的请求是通过在服务器上一个子进程的上下文中运行一个程序并将它的输出返回给客户端来服务的。CGI标准提供了一组规则,来管理客户端如何将程序参数传递给服务器,服务器如何将这些参数以及其他信息传递给子进程,以及子进程如何将它的输出发送回客户端。最后实现一个简单但是有效的 Web 服务器,既可提供静态内容,也可提供动态内容。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值