Tiny Web 服务器源码学习

2017/03/20 C/C++

Tiny Web服务器是《深入理解计算机系统》一书中11.6节的内容,该节演示了一个只有250行代码的服务器,非常适合对C语言和网络编程感兴趣的朋友进行学习!话不多说,让我们开始正式的学习!

基本使用

Tiny Web服务器的主要代码在原书上已经给出,如果要下载完整的代码,可以在官网上进行下载,具体地址请点击这里,除此之外,在Github上也可以搜到热心用户上传的代码。

下载好代码之后,我们要进行编译才可以使用。如果是官网代码的话,直接使用make命令进行编译就可以了,如果不是的话(没有Makefile文件),则具体编译操作如下:

gcc tiny.c csapp.c -o tiny 
cd cgi-bin
gcc adder.c -o adder -I../
cd ..

编译完成后,我们只要执行./tiny 8888,就可以开启Tiny Web服务器了!

首先输入127.0.0.1:8888,可以对Tiny Web服务器的静态页面进行测试,然后输入127.0.0.1:8888/cgi-bin/adder?1&2,可以对服务器的动态页面进行测试。这里值得一提的是,Tiny Web服务器只支持GET请求,如果是POST请求的话,会返回错误信息。

源码解读

套接字接口

套接字接口(socket interface)是一组函数,它们和Unix I/O函数结合起来,用以创建网络应用。学习socket编程能够让我们加深对网络框架的理解,socket编程的函数可以通过下图体现:

套接字接口概述

通过此图,我们可以大致了解socket包含哪些函数,接下来我们对几个重要的函数进行一一介绍:

socket函数

客户端和服务器使用socket函数来创建一个套接字描述符,具体使用如下:

#include <sys/socket.h>
int socket(int domain, int type, int protocol);

其中,domain指定socket的通信协议集(AF_UNIX和AF_INET等),AF_UNIX只能用于单一的*nix系统,而AF_INET是用于网络的,所以可以允许远程主机之间的通信;type指定socket类型(SOCK_STREAM和SOCK_DGRAM等),SOCK_STREAM表明使用TCP协议,SOCK_DGRAM表明使用UDP协议;protocol指定实际使用的传输协议,如果该项为0,则使用缺省协议。调用socket函数后,成功会返回文件描述符,失败返回-1。

bind函数

bind函数是服务器特有的函数,其使用如下:

#include <sys/socket.h>
int bind(int socket, const struct sockaddr *address, socklen_t address_len);

bind函数为socket分配地址,其中socket是套接字描述符,address指向sockaddr结构(用于表示所分配地址)的指针,address_len是sockaddr结构的长度。

listen函数

listen函数将sockfd从一个主动套接字转化为一个监听套接字,该套接字可以接受来自客户端的连接请求,具体使用如下:

#include <sys/socket.h>
int listen(int socket, int backlog);

其中backlog是一个决定监听队列大小的整数,当有连接请求到来,就会进入此监听队列,当队列满后,新的连接请求会返回错误。

accept函数

服务器通过调用accept函数来等待来自客户端的连接请求,具体使用如下:

#include <sys/socket.h>
int accept(int socket, struct sockaddr *restrict address, socklen_t *restrict address_len);

其中socket是监听的socket描述符,address是指向sockaddr结构体的指针,address_len是客户机地址结构体的大小。

connect函数

客户端通过调用connect函数来建立与服务器的连接,具体使用如下:

#include <sys/socket.h>
int connect(int socket, const struct sockaddr *address, socklen_t address_len);

socket类型

socket类型主要有SOCK_STREAM、SOCK_DGRAM、SOCK_RAW三种,其中SOCK_RAW工作在网络层,可以处理ICMP、IGMP等网络报文以及特殊的IPv4报文,而SOCK_STREAM(TCP)、SOCK_DGRAM(UDP)工作在传输层。

csapp.c的open_clientfd函数

具体代码

/*
 * open_clientfd - Open connection to server at <hostname, port> and
 *     return a socket descriptor ready for reading and writing. This
 *     function is reentrant and protocol-independent.
 * 
 *     On error, returns -1 and sets errno.
 */
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 the 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;
}

详细解析

  • 将socket和connect函数包装成一个叫做open_clientfd的辅助函数是很方便的,客户端可以用它来和服务器建立连接
  • 函数中需要使用到结构体addrinfo,这是头文件netdb.h中带有的结构体,netdb.h提供了设置及获取域名的函数
  • 函数先为结构体addrinfo变量hints动态分配内存空间,然后设置结构体成员的值,这里的“|=”运算符是表示或运算
  • 配置好hints后,open_clientfd函数将调用Getaddrinfo函数(用于通过域名获取IP)返回一个指向addrinfo结构体链表的指针,这个指针也就是listp
  • 接下来函数将遍历listp。每次遍历都是先调用socket函数,如果获取套接字描述符失败,则会执行下一次遍历,如果成功,则会尝试连接。如果连接成功,则会跳出循环,如果失败,则继续尝试
  • 结束遍历后,函数会释放listp占有的内存,并且判断是否获取到能够连接的socket,如果获取到直接返回,反之则返回-1

csapp.c的open_listenfd函数

具体代码

/*
 * open_listenfd - Open and return a listening socket on port. This
 *     function is reentrant and protocol-independent.
 *
 *     On error, returns -1 and sets errno.
 */
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 TCP connections */
    hints.ai_flags = AI_PASSIVE;      /* ... on any IP address */
    hints.ai_flags |= AI_NUMERICSERV; /* ... using a numeric port arg. */
    hints.ai_flags |= AI_ADDRCONFIG;  /* Recommended for connections */
    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)
	return -1;
    return listenfd;
}

详细解析

  • 将socket、bind和listen函数包装成一个叫做open_listenfd的辅助函数是很方便的,服务端可以用它来创建一个监听描述符
  • open_listenfd函数用与open_clientfd函数类似的方法获取到可能的地址列表listp
  • 然后遍历listp。每次遍历都是先调用socket函数,如果获取套接字描述符失败,则会执行下一次遍历,反之则进行套接字设置。设置完套接字后,则尝试绑定描述符,如果成功,则会跳出循环,如果失败,则继续尝试
  • 结束遍历后,函数会释放listp占有的内存,并且判断是否获取到能够套接字,如果获取不到则返回-1,反之则尝试调用listen函数,调用成功返回listenfd,反之则返回-1

tiny.c的main函数

具体代码

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
    }
}

详细解析

  • main函数中的argc表示命令行参数的数目,argv表示参数列表。参数数目默认大于等于1,因为至少有一个参数,也就是argv[0]为程序名
  • main函数定义完变量之后会判断传入参数是不是为2个,如果不是则输出错误信息并退出。这里需要的两个参数分别为程序名tiny和端口号。这里的fprintf函数的作用是格式化输出到流或文件中,其中第一个参数是文件指针,这里用的是stderr(Linux内核启动时默认打开三个I/O设备文件:标准输入文件stdin,标准输出文件stdout和标准错误输出文件stderr),后面两个参数分别是输出内容和参数列表
  • 如果参数数目满足要求的话,就可以调用Open_listendfd创建监听描述符
  • 打开监听套接字后,服务器将会执行典型的无限服务器循环,不断地接受连接请求,执行事务,并关闭连接它的那一端

tiny.c的doit函数

具体代码

/*
 * 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
    }
}

详细解析

  • doit函数负责处理HTTP事务,这里需要指出的是,Tiny Web服务器只支持GET方法,不支持POST方法,对于POST请求,会发送一个错误提示并返回程序
  • doit函数会首先调用Rio_readinitb函数进行初始化,将socket描述符和读缓存区和重置缓冲区关联起来
  • 然后读取数据,如果读取失败则返回,成功则利用sscanf函数将buf按照“%s %s %s”的格式分别读取到method、uri和version
  • 接下来doit函数将判断method是不是get,如果不是则提示错误信息并返回,如果是则调用read_requesthdrs函数读取请求报头信息并进行忽略,因为Tiny并不需要使用到
  • 接着doit函数将GET请求中的URI解析为一个文件名和一个可能为空的CGI参数字符串,并设置标志is_static标志是静态内容还是动态内容。如果文件不存在,函数将发送错误信息并返回
  • 如果请求的是静态内容,将验证是否是普通文件且拥有读权限,如果是,则提供静态内容。如果请求的是动态内容,则验证是否为可执行文件,如果是,则继续并提供动态内容

tiny.c的serve_static函数

具体代码

/*
 * 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
    sprintf(buf, "%sServer: Tiny Web Server\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));       //line:netp:servestatic:endserve
    printf("Response headers:\n");
    printf("%s", buf);

    /* Send response body to client */
    srcfd = Open(filename, O_RDONLY, 0);    //line:netp:servestatic:open
    srcp = Mmap(0, filesize, PROT_READ, MAP_PRIVATE, srcfd, 0);//line:netp:servestatic:mmap
    Close(srcfd);                           //line:netp:servestatic:close
    Rio_writen(fd, srcp, filesize);         //line:netp:servestatic:write
    Munmap(srcp, filesize);                 //line:netp:servestatic:munmap
}

详细解析

  • TINY提供四种不同类型的静态内容:HTML文件、无格式的文本文件,以及编码为GIF和JPEG格式的图片
  • serve_static函数首先判断文件的类型,然后发送响应行和响应报头给客户端。注意用一个空行终止报头
  • 接着函数以读形式打开请求文件,并获取它的描述符,然后用Mmap函数将被请求文件映射到一个虚拟存储器空间。一旦将文件映射到存储器,我们就可以关闭文件了。最后再用Rio_writen函数将文件传送到客户端并用Munmap函数释放映射的虚拟存储器区域

tiny.c的serve_dynamic函数

具体代码

/*
 * 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 */ //line:netp:servedynamic:fork
	/* Real server would set all CGI vars here */
	setenv("QUERY_STRING", cgiargs, 1); //line:netp:servedynamic:setenv
	Dup2(fd, STDOUT_FILENO);         /* Redirect stdout to client */ //line:netp:servedynamic:dup2
	Execve(filename, emptylist, environ); /* Run CGI program */ //line:netp:servedynamic:execve
    }
    Wait(NULL); /* Parent waits for and reaps child */ //line:netp:servedynamic:wait
}

详细解析

  • Tiny通过派生一个子进程并在子进程的上下文中运行一个CGI程序,来提供各种类型的动态内容
  • serve_dynamic函数首先向客户端发送一个表明成功的响应行,同时还包括带有信息的Server报头。CGI程序负责发送响应的剩余部分。注意,这并不像我们可能希望的那样健壮,因为它没有考虑到CGI程序会遇到某些错误的可能性
  • serve_dynamic函数会派生一个子进程,用来自请求URI的CGI参数初始化QUERY_STRING环境变量,接着子进程重定向它的标准输出到已连接文件描述符,然后加载并运行CGI程序
  • 等子进程终止后,父进程会回收操作系统分配给子进程的资源,函数执行结束

Search

    Table of Contents