参考:“深入理解计算机系统” 第663页
web简介:
web服务器其实就是用来响应浏览器(客户端)的请求,他们之间的通讯都遵循着HTTP协议。HTTP请求格式是 <方法><统一资源标识符><HTTP协议版本>,<方法>有GET,POST,PUT等等,其中用得最为广泛得是GET请求,占全世界所有HTTP请求的99%;<统一资源标识符>说白了就是文件的路径,比如index.html。如果你用浏览器输入http://127.0.0.1/index.html,它就会用TCP协议连接上127.0.0.1:80的服务器,发送GET /index.html HTTP/1.1的数据到服务器;服务器收到请求后,就会返回index.html的文件内容,然后断开对浏览器的连接;最后浏览器收到index.html的文件内容后把它显示在屏幕上。既然HTTP协议是建基於TCP的,那我们当然就可以用我们在c语言中熟悉的socket来制作一个属于自己的浏览器。
我们的微型web服务器简介:
不要看它叫微型,就小看它,麻雀虽小但五脏俱全,有错误处理,有文字内容,有图片内容。看到这里一定会有朋友问:你这些都是静态服务,像登入操作的那种动态服务就没了吧?那你就错了,这个小服务器连动态服务都有,你能用c语言或python写动态响应程序/脚本,也就是我们常说的CGI程序,牛逼了吧?事不宜迟,我们马上开始实现一个属于自己的web服务器。
效果图:
要准备的东西:
线程池:下面我们会用到这个线程池模块,是我之前写的,你可以直接复制来用,不用也可以,就把线程池的函数去掉就可以了。不过用法很简单,三个函数init,add_event,destroy。http://blog.csdn.net/sumkee911/article/details/50231891
重定位标准输出:CGI程序是用printf来返回数据的,所以我们必须在调用之前把它的stdout重新定位到客户端socket的描述符上,我之前又写下过这个代码,你们可以参考这里。http://blog.csdn.net/sumkee911/article/details/50238169
我的源码:这个就是我写的微型web服务器,里面有makefile,make一下就能用;还有测试用的html和cgi,都在bin文件夹里。http://download.csdn.net/detail/sumkee911/9367809
这里也放源码(你们从main函数跟着看下去就可以明白。我打了很多注释,而且代码很浅显易明的,请放心。如果实在有函数不懂的话就上百度,我这里就不再详细说明了):
tiny_web_server.cpp
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/socket.h> #include <netinet/in.h> #include <sys/epoll.h> #include <sys/ioctl.h> #include <sys/mman.h> #include <sys/fcntl.h> #include <sys/stat.h> #include <sys/wait.h> #include "threadpool/threadpool.h" // 服务器配置 #define PORT 80 #define MAX_LISTEN 128 #define MAX_EPOLL MAX_LISTEN+1 #define THREAD_COUNT 10 #define DEFAULT_PAGE "index.html" #define SEND_BUF 2048 #define RECV_BUF 2048 #define SERVE_STATIC 0 // 处理静态请求 #define SERVE_DYNAMIC 1 // 处理动态请求 // 服务器根目录 static char g_root[256]=""; // 线程池 static ThreadPool g_pool; // 多路复用io接口(epoll) static int g_epoll_fd; static void del_from_epoll(int efd, int fd); void get_file_type(char *type, char *filepath) { if(strstr(filepath,".html")) strcpy(type, "text/html"); else if(strstr(filepath, ".gif")) strcpy(type, "image/gif"); else if(strstr(filepath, ".jpg")) strcpy(type, "image/jpeg"); else if(strstr(filepath, ".png")) strcpy(type, "image/png"); else if(strstr(filepath, ".css")) strcpy(type, "text/css"); else if(strstr(filepath, ".js")) strcpy(type, "application/x-javascript"); else strcpy(type, "text/plain"); } void get_absolute_path(char *abfilepath, char *rt, char *filepath) { sprintf(abfilepath, "%s%s", rt, filepath); } void serve_error(int fd, char *filepath,const char *errnum, const char *shortmsg, const char *longmsg) { char head[SEND_BUF], body[SEND_BUF]; memset(body, 0, sizeof(body)); memset(head, 0 ,sizeof(head)); // 网页错误页面 sprintf(body, "<html><head><title>Tiny Web Server Error</title></head>"); sprintf(body, "%s<body><h1><font color='red'>Tiny Web Server Error</font></h1>", body); sprintf(body, "%s<p>Error code: %s</p>",body, errnum); sprintf(body, "%s<p>Cause: %s %s</p></body></html>", body,longmsg, filepath); // http 头 sprintf(head, "HTTP/1.1 %s %s\r\n", errnum, shortmsg); sprintf(head, "%sContent-type: text/html\r\n", head); sprintf(head, "%sContent-length: %d\r\n\r\n", head,(int)strlen(body)); // 发送 send(fd, head, strlen(head), MSG_NOSIGNAL); send(fd, body, strlen(body), MSG_NOSIGNAL); } void serve_static(int fd, char *filepath, long filesize) { int filefd; char buf[SEND_BUF], filetype[128],*filemap; memset(filetype, 0, sizeof(filetype)); memset(buf, 0, sizeof(buf)); // 发送http头 get_file_type(filetype, filepath); // 获取文件类型 sprintf(buf, "HTTP/1.1 200 OK\r\n"); sprintf(buf, "%sServer: Tiny Web Server\r\n", buf); sprintf(buf, "%sContent-length: %ld\r\n",buf, filesize); sprintf(buf, "%sContent-type: %s\r\n\r\n", buf, filetype); send(fd, buf, strlen(buf), MSG_NOSIGNAL); // 发送文件内容 filefd = open(filepath, O_RDONLY); filemap = (char *)mmap(0, filesize, PROT_READ, MAP_PRIVATE, filefd, 0); close(filefd); send(fd, filemap, filesize, MSG_NOSIGNAL); munmap(filemap,filesize); } void serve_dynamic(int fd, char *filepath, char *args) { // 创建一条进程;设置环境变量QUERY_STRING,将args设置进去; 最后将子进程的标准输出重新定位到客户端的socket描述符中, // 只要子程序printf,就能把printf得数据传送到客户端。 pid_t pid; char head[SEND_BUF]; // 发送http头 memset(head, 0, sizeof(head)); sprintf(head, "HTTP/1.1 200 OK\r\n"); sprintf(head, "%sServer: Tiny Web Server\r\n", head); send(fd, head, strlen(head), MSG_NOSIGNAL); if((pid=fork()) == 0) { // 子进程 // 设置环境变量 setenv("QUERY_STRING", args, 1); // 重定向标准输出, 默认输出fd是1 dup2(fd, 1); // 启动CGI execl(filepath, "" , (char*)0); } else { // 等待子进程结束 waitpid(pid, 0, 0); } } int serve_type(const char *filepath) { const char str[] = "/cgi-bin/\0"; if(strncmp(filepath, str, strlen(str)) == 0) { return SERVE_DYNAMIC; } return SERVE_STATIC; } void parse_url(char *filepath, char *args, char *url) { char *file_start, *args_start; file_start = index(url, '/'); args_start = index(url, '?'); if(args_start != 0) { memcpy(filepath, file_start, args_start-url); memcpy(args, args_start+1, strlen(args_start)-1); } else if(file_start != 0) { memcpy(filepath, file_start, strlen(file_start)); } } void process_command(void *tp_args) { char data[RECV_BUF], request[16], filepath[256], \ new_abfilepath[256],args[256], url[512]; struct stat filestat; int res, type; int fd = *(int*)tp_args; free(tp_args); memset(data, 0, sizeof(data)); memset(request, 0, sizeof(request)); memset(filepath, 0, sizeof(filepath)); memset(url, 0, sizeof(url)); memset(args, 0, sizeof(args)); memset(new_abfilepath, 0, sizeof(new_abfilepath)); // 获取请求 res = recv(fd, data, sizeof(data), MSG_NOSIGNAL); if(res == 0 || res == -1) { // 删除连接 goto __end; } printf("%s\n", data); // 解析请求 sscanf(data, "%s %s", request, url); if(strcasecmp("GET", request) != 0) { // 无法识别请求 serve_error(fd, request, "501", "Not implememted", "Tiny does not implement this method"); goto __end; } // 解析url parse_url(filepath, args, url); // 如果文件位置为'/', 就把它设置为默认页面 if(strlen(filepath) == 1) { // 默认页面 strcat(filepath, DEFAULT_PAGE); } get_absolute_path(new_abfilepath, g_root, filepath); // 设置文件的绝对路径 // 获取文件属性 res = stat(new_abfilepath, &filestat); if(res == -1) { // 找不到相关文件 serve_error(fd, filepath,"404", "Not found", "Tiny couldn't find this file"); goto __end; } // 判断是静态请求还是动态请求,动态请求也就是我们常说的CGI程序 type = serve_type(filepath); if(type == SERVE_STATIC) { if(!(S_ISREG(filestat.st_mode)) || !(S_IRUSR & filestat.st_mode)) { // 错误,不能读取这文件 serve_error(fd, filepath, "403", "Forbidden", "Tiny couldn't read this file"); goto __end; } // 开始回复静态请求 serve_static(fd, new_abfilepath, filestat.st_size); } else { if(!(S_ISREG(filestat.st_mode)) || !(S_IXUSR & filestat.st_mode)) { // 错误,不能运行这文件 serve_error(fd, filepath, "403", "Forbidden", "Tiny couldn't run this cgi file"); goto __end; } // 开始回复动态请求 serve_dynamic(fd, new_abfilepath, args); } // 删除连接 __end: close(fd); } void add_to_epoll(int efd, int fd) { int res; struct epoll_event epe; epe.data.fd = fd; epe.events = EPOLLIN | EPOLLET; res = epoll_ctl(efd, EPOLL_CTL_ADD, fd, &epe); if(res == -1) { perror("epoll_ctl"); } } void del_from_epoll(int efd, int fd) { int res = epoll_ctl(efd, EPOLL_CTL_DEL, fd, 0); if(res == -1) { perror("epoll_ctl"); } } void set_nonblock(int fd) { int tmp = 1; if(ioctl(fd, FIONBIO, &tmp) == -1) { perror("ioctl"); } } void set_root(char *rt, char *filepath) { char *end = rindex(filepath, '/'); memcpy(rt, filepath, end-filepath); strcat(rt, "\0"); } int main(int, char *argv[]) { int server,res, blreuse; struct sockaddr_in addr; bool blres; // 设置根目录, 也就是该服务器程序的主目录 set_root(g_root, argv[0]); // 创建服务器 server = socket(AF_INET, SOCK_STREAM, 0); if(server == -1) { perror("socket"); return -1; } addr.sin_family = AF_INET; addr.sin_port = htons(PORT); addr.sin_addr.s_addr = htonl(INADDR_ANY); // 将bind地址设置为可重用 blreuse = 1; res = setsockopt(server, SOL_SOCKET, SO_REUSEADDR, &blreuse, sizeof(blreuse)); if(res == -1) { perror("setsockopt"); return -1; } res = bind(server, (struct sockaddr*)&addr, sizeof(struct sockaddr_in)); if(res == -1) { perror("bind"); return -1; } listen(server, MAX_LISTEN); if(res == -1) { perror("listen"); return -1; } // 将服务器设置为非阻塞 set_nonblock(server); // 创建epoll g_epoll_fd = epoll_create(MAX_LISTEN); if(g_epoll_fd == -1) { perror("epoll_create"); return -1; } // 将服务器加入epoll add_to_epoll(g_epoll_fd, server); // 初始化线程池, 用来处理url请求 blres = g_pool.init(THREAD_COUNT); if(blres == false) { return -1; } // 进入接收事件循环 while(1) { struct epoll_event epes[MAX_EPOLL]; int i, n; n = epoll_wait(g_epoll_fd, epes, MAX_EPOLL, -1); for(i=0; i<n; ++i) { if(epes[i].events & EPOLLERR || epes[i].events & EPOLLHUP || !(epes[i].events & EPOLLIN)) { del_from_epoll(g_epoll_fd,epes[i].data.fd); continue; } else if(epes[i].data.fd == server) { // 服务器接收到请求 struct sockaddr_in addr_client; socklen_t len; int fd; len = sizeof(struct sockaddr_in); while(true) { fd = accept(server, (struct sockaddr*)&addr_client, &len); if(fd == -1) { break; } // 将新来得客户加入到epoll add_to_epoll(g_epoll_fd, fd); } } else { // 接收到来自客户端的数据 int *fd = (int*)malloc(sizeof(int)); *fd = epes[i].data.fd; // 将客户从epoll中删除 del_from_epoll(g_epoll_fd, epes[i].data.fd); // 处理url请求 g_pool.add_event(process_command, (void*)fd); } } } g_pool.destroy(); close(g_epoll_fd); close(server); return 0; }