时间:2023-05-19 06:09:01 | 来源:网站运营
时间:2023-05-19 06:09:01 来源:网站运营
用C语言制作Web服务器:本文,我们将使用C语言从零开始实现一个支持静态、动态网页的Web服务器。我们把这个服务器叫做Tiny。(cliaddr : cliport, servaddr : servport)
其中, cliaddr
和cliport
分别是客户端IP地址和客户端端口号,servaddr
和servport
分别是服务器IP地址和服务器端口。举例说明如下:socket
函数获取一个socket,然后调用bind
函数绑定本机的IP地址和端口,再调用listen
函数开启监听,最后调用accept
函数等待直到有客户端发起连接。socket
函数获取一个socket,然后调用connect
函数向指定服务器发起连接请求,当连接成功或出现错误后返回。若连接成功,服务器端的accept
函数也会成功返回,返回另一个已连接的socket(不是最初调用socket
函数得到的socket),该socket可以直接用于与客户端通信。而服务器最初的那个socket可以继续循环调用accept
函数,等待下一次连接的到来。rio_readlineb
和rio_written
是作者封装的I/O读写函数,与Linux系统提供的read
和write
作用基本相同,详细介绍见参考资料。<sys/socket.h>
头文件中,为了更清晰地帮助大家理解每个函数的使用方法,我们列出它们的函数声明。#include <sys/types.h>#include <sys/socket.h>/**获取一个socket descriptor@params: domain: 此处固定使用AF_INET type: 此处固定使用SOCK_STREAM protocol: 此处固定使用0@returns: nonnegative descriptor if OK, -1 on error.*/int socket(int domain, int type, int protocol);/**客户端socket向服务器发起连接@params: sockfd: 发起连接的socket descriptor serv_addr: 连接的目标地址和端口 addrlen: sizeof(*serv_addr)@returns: 0 if OK, -1 on error*/int connect(int sockfd, struct sockaddr *serv_addr, int addrlen);/**服务器socket绑定地址和端口@params: sockfd: 当前socket descriptor my_addr: 指定绑定的本机地址和端口 addrlen: sizeof(*my_addr)@returns: 0 if OK, -1 on error*/int bind(int sockfd, struct sockaddr *my_addr, int addrlen);/**将当前socket转变为可以监听外部连接请求的socket@params: sockfd: 当前socket descriptor backlog: 请求队列的最大长度@returns: 0 if OK, -1 on error*/int listen(int sockfd, int backlog);/**等待客户端请求到达,注意,成功返回得到的是一个新的socket descriptor,而不是输入参数listenfd。@params: listenfd: 当前正在用于监听的socket descriptor addr: 客户端请求地址(输出参数) addrlen: 客户端请求地址的长度(输出参数)@returns: 成功则返回一个非负的connected descriptor,出错则返回-1*/int accept(int listenfd, struct sockaddr *addr, int *addrlen);
int main(int argc, char **argv) { int listenfd, connfd; 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); doit(connfd); Close(connfd); }}
主函数参数需要传入服务器绑定的端口号码,得到这个号码后,调用Open_listenfd
函数,该函数完成socket
、bind
、listen
等一系列操作。接着调用accept
函数等待客户端请求。注意,Accept
是accept
的包装函数,用来自动处理可能发生的异常,我们只需把它们当成一样的就行了。当accept
成功返回后,我们拿到了connected socket descriptor,然后调用doit
函数处理请求。doit
函数定义如下。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)) return; 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); }}
为了更接近现实,假设现在接收到的HTTP请求如下。该请求的请求头是空的。GET /cgi-bin/adder?15000&213 HTTP/1.0
代码中,Rio_readlineb
和sscanf
负责读入请求行并解析出请求方法、请求URI和版本号。接下来调用parse_uri
函数,该函数利用请求uri得到访问的文件名、CGI参数,并返回是否按照静态网页处理。如果是,则调用serve_static
函数处理,否则调用serve_dynamic
函数处理。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"); 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)); printf("Response headers:/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);}
直接看最后几行代码。Open
以只读方式打开请求的文件,Mmap
将该文件直接读取到虚拟地址空间中的任意位置,然后关闭文件。接下来Rio_written
把内存中的文件写入fd
指定的connected socket descriptor,静态页面响应完成。Munmap
删除刚才在虚拟地址空间申请的内存。关于mmap
函数的更多介绍见参考资料。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 */}
对于动态网页请求,我们的方法是创建一个子进程,在子进程中执行CGI程序。看代码,Fork
函数创建子进程,熟悉Linux进程的朋友们应该知道,该函数会返回两次,一次在父进程中返回,返回值不等于0,另一次在子进程中返回,返回值为0,因此if
判断内部是子进程执行的代码。首先设置环境变量,用于把请求参数传递给CGI程序。接下来调用Dup2
函数将标准输出重定向到connected socket descriptor,这样一来使用标准输出输出的内容将会直接发送给客户端。然后调用Execve
函数在子进程中执行filename
指定的CGI程序。最后在父进程中调用了Wait
函数用于收割子进程,当子进程终止后该函数才会返回。因此该Web服务器不能同时处理多个访问,只能一个一个处理。/* * adder.c - a minimal CGI program that adds two numbers together */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); strcpy(arg2, p+1); n1 = atoi(arg1); n2 = atoi(arg2); } /* Make the response body */ sprintf(content, "Welcome to add.com: "); sprintf(content, "%sTHE Internet addition portal./r/n<p>", content); sprintf(content, "%sThe answer is: %d + %d = %d/r/n<p>", content, n1, n2, n1 + n2); sprintf(content, "%sThanks for visiting!/r/n", 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); exit(0);}
这段代码就非常简单了,从环境变量中取出请求参数,得到两个加数的值,相加后输出。需要注意的是,由于刚才已经重定向标准输出,因此使用printf
就可以把内容输出给客户端。输出内容需要遵照HTTP协议的格式,才能在浏览器中正确显示出来。./tiny 8000
静态网页效果:访问http://localhost:8000关键词:服务,语言