算是对之前学习的东西的一个阶段总结,东西虽然简陋,应该也有值得思考的地方。
文件如下:
rio 之前弄的rio库
Makefile Makefile
headerc.h 本意是所谓的“HeaderCollection”,包含必须用到的各种头文件
index.html 测试用的一个小页面,默认页面是当前目录的index.html文件
main.c 似乎这么做能够叫做Daemon,那么就试试了
whd.c 核心部分
whd.h 函数声明
大致流程如下(部分):
main.c:
1 #include "headerc.h" 2 #include "whd.h" 3 4 int main(void) 5 { 6 pid_t pid; 7 if((pid = fork()) == 0) 8 { 9 close(STDOUT_FILENO); 10 close(STDIN_FILENO); 11 close(STDERR_FILENO); 12 fake_main(); 13 } 14 else if(pid > 0) 15 exit(0); 16 else 17 FATAL("MAIN.FORK"); 18 19 exit(0); 20 }
(似乎这么做就能达到daemon的效果,事实也是这样)
仅仅fork一次,父进程退出,子进程执行fake_main函数。FATAL是定义在headerc.h中的一个错误宏(宏中一用到syslog就编译错误,所以暂时没有记录功能)。
main.c:
fake_main()
1 void fake_main(void) 2 { 3 signal(SIGPIPE, SIG_IGN); 4 int listenfd, err, i; 5 pid_t pid;//, contact[WORKER]; 6 struct sockaddr_in servaddr; 7 8 memset(&servaddr, 0, sizeof(servaddr)); 9 servaddr.sin_family = AF_INET; 10 servaddr.sin_addr.s_addr = htonl(INADDR_ANY); 11 servaddr.sin_port = htons((getuid() == 0) ? 80 : 8080); 12 13 if((listenfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) 14 FATAL("FAKE_MAIN.SOCKET"); 15 16 if((err = bind(listenfd, (SA *)&servaddr, sizeof(servaddr))) < 0) 17 FATAL("FAKE_MAIN.BIND"); 18 19 if((err = listen(listenfd, 1024)) < 0) 20 FATAL("FAKE_MAIN.LISTEN"); 21 22 for(i = 0; i < WORKER; ++i) 23 { 24 if((pid = fork()) == 0) 25 start_worker(listenfd); 26 else if(pid < 0) 27 //contact[i] = pid; 28 FATAL("FAKE_MAIN.FORK"); 29 } 30 31 for(;;) 32 { 33 //TODO 34 pause(); 35 } 36 }
该函数工作很简单,首先处理SIGPIPE信号(忽略)。然后socket,bind, listen来创建一个监听套接字,然后fork生成相应个worker并进入死循环。
start_worker(int listenfd):
1 void start_worker(int listenfd) 2 { 3 struct sockaddr_in cliaddr; 4 struct epoll_event ev, events[MAXLEN]; 5 socklen_t clilen; 6 int connfd, efd, nfds, n; 7 char buf[MAXLEN], content[MAXLEN]; 8 9 if((efd = epoll_create(MAXLEN)) < 0) 10 FATAL("WORKER.EPOLL_CREATE"); 11 12 ev.data.fd = listenfd; 13 ev.events = EPOLLIN; //EPOLLET 14 if(epoll_ctl(efd, EPOLL_CTL_ADD, listenfd, &ev) < 0) 15 FATAL("WORKER.EPOLL_CTL"); 16 17 18 for(;;) 19 { 20 clilen = sizeof(cliaddr); 21 22 //epoll_wait Thundering Herd? 23 //EINTR? 24 again: 25 if((nfds = epoll_wait(efd, events, MAXLEN, -1)) < 0) 26 { 27 if(errno == EINTR) 28 goto again; 29 else 30 FATAL("WORKER.EPOLL_WAIT"); 31 } 32 33 for(n = 0; n < nfds; ++n) 34 { 35 if(events[n].data.fd == listenfd) 36 { 37 pthread_mutex_lock(&ALOCK); 38 if((connfd = accept(listenfd, (SA *)&cliaddr, &clilen)) < 0) 39 FATAL("WORKER.ACCEPT"); 40 pthread_mutex_unlock(&ALOCK); 41 42 snprintf(content, MAXLEN, "Client : %s\n", inet_ntop(AF_INET, &clilen, buf, clilen)); 43 //syslog(LOG_INFO, content); 44 45 ev.data.fd = connfd; 46 ev.events = EPOLLIN; 47 48 if(epoll_ctl(efd, EPOLL_CTL_ADD, connfd, &ev) < 0) 49 FATAL("WORKER.EPOLL_CTL"); 50 } 51 else 52 { 53 process(events[n].data.fd, efd); 54 } 55 } 56 } 57 }
对于每个工作子进程,该函数首先创建一个epollfd,并将传入的listenfd加入epoll,并等待EPOLLIN可读事件发生,如果可读事件是之前的listenfd,说明有新的连接,则调用accept接受连接并也加入epoll的监听。如果是其他fd的可读事件说明有请求到来,则交给process函数处理请求。(这里尝试了一下accept锁,不过似乎epoll_wait也会造成惊群?)。
process(int fd, int efd):
1 void process(int fd, int efd) 2 { 3 char rstline[MAXLEN], method[MAXLEN], uri[MAXLEN], protocol[MAXLEN]; 4 char filename[MAXLEN], filetype[MAXLEN]; 5 char response[MAXLEN]; 6 rio_t rio; 7 int err; 8 struct stat sbuf; 9 char *file; 10 rio_readinitb(&rio, fd); 11 12 if((err = rio_readlineb(&rio, rstline, MAXLEN)) <= 0) 13 { 14 if((err = epoll_ctl(efd, EPOLL_CTL_DEL, fd, NULL)) < 0) 15 FATAL("PROCESS.EPOLL_CTL"); 16 close(fd); 17 } 18 else 19 { 20 sscanf(rstline, "%s %s %s", method, uri, protocol); 21 if(strcasecmp(method, "GET")) 22 //TEST 23 ERR(fd, efd); 24 if(uri[0] != '/') 25 ERR(fd, efd); 26 process_rstheader(&rio); 27 parse_uri(uri, filename, filetype); 28 if(get_fileinfo(filename) < 0) 29 call_403(fd); 30 else 31 { 32 lstat(filename, &sbuf); 33 err = open(filename, O_RDONLY, 0); 34 file = mmap(0 ,sbuf.st_size, PROT_READ, MAP_PRIVATE, err, 0); 35 close(err); 36 sprintf(response, "HTTP/1.1 200 OK\r\n"); 37 sprintf(response, "%sContent-length: %lu\r\n", response, sbuf.st_size); 38 sprintf(response, "%sContent-type: %s\r\n\r\n", response, filetype); 39 rio_writen(fd, response, strlen(response)); 40 rio_writen(fd, file, sbuf.st_size); 41 munmap(file, sbuf.st_size); 42 } 43 } 44 }
处理请求的主要函数。首先获取一行,如果无内容可读,则认为对方发送了FIN,则从epoll监听中删除并关闭这个描述符。
对于读取到的一行,先按照HTTP规范分成方法,URI,协议分成三个部分存入相应数组。然后直接判断方法和URI是否合法(目前仅支持GET方法),若不合法直接调用ERR函数处理,ERR直接返回403错误和一个提示消息,并把当前描述符从监听列表中删除+关闭。
当请求合法时,首先调用process_rstheader处理请求头(简单地“吃掉”)。然后在prase_uri中生成对应的文件路径和文件类型。
然后调用get_fileinfo判断文件是否存在且能够读取到,并且是一个“文件”,否则调用call_403返回错误页面。
当一切就绪时,将文件打开并mmap映射至内存,并生成响应头返回。最后关闭文件和映射内存。
所用到的辅助函数如下:
1 void ERR(int fd, int efd) 2 { 3 char re[] = "HTTP/1.1 403 Not Found\r\nContent-length: 14\r\nContent-type: text/html\r\n\r\n<p>ILLEGAL</p>"; 4 rio_writen(fd, re, strlen(re)); 5 if(epoll_ctl(efd, EPOLL_CTL_DEL, fd, NULL) < 0) 6 FATAL("ERR.EPOLL_CTL"); 7 close(fd); 8 } 9 10 void call_403(int fd) 11 { 12 char re[] = "HTTP/1.1 403 Not Found\r\nContent-length: 16\r\nContent-type: text/html\r\n\r\n<p>NOT FOUND</p>"; 13 rio_writen(fd, re, strlen(re)); 14 } 15 16 void parse_uri(char *uri, char *filename, char *filetype) 17 { 18 strcpy(filename, "."); 19 strcat(filename, uri); 20 if(uri[strlen(uri) - 1] == '/') 21 strcat(filename, "index.html"); 22 23 get_filetype(filename, filetype); 24 } 25 26 void get_filetype(char *filename, char *filetype) 27 { 28 if(strcasestr(filename, "html")) 29 strcpy(filetype, "text/html"); 30 else if(strcasestr(filename, "jpg")) 31 strcpy(filetype, "image/jpeg"); 32 else if(strcasestr(filename, "png")) 33 strcpy(filetype, "image/png"); 34 else if(strcasestr(filename, "gif")) 35 strcpy(filetype, "image/gif"); 36 else 37 strcpy(filetype, "texp/plain"); 38 } 39 40 void process_rstheader(rio_t *rp) 41 { 42 char buf[MAXLEN]; 43 rio_readlineb(rp, buf, MAXLEN); 44 while(strcmp(buf, "\r\n")) 45 rio_readlineb(rp, buf, MAXLEN); 46 } 47 48 int get_fileinfo(char *filename) 49 { 50 struct stat buf; 51 if(lstat(filename, &buf) < 0) 52 return -1; 53 if((S_ISREG(buf.st_mode)) && (S_IRUSR & buf.st_mode)) 54 return 0; 55 else 56 return -1; 57 }
编译通过,经siege测试如下(应该没错?):
1 zirconi@Zirx-Ubuntu:~$ siege -c 300 -r 30 http://localhost:8080 2 ** SIEGE 2.70 3 ** Preparing 300 concurrent users for battle. 4 The server is now under siege.. done. 5 Transactions: 9000 hits 6 Availability: 100.00 % 7 Elapsed time: 23.17 secs 8 Data transferred: 0.96 MB 9 Response time: 0.01 secs 10 Transaction rate: 388.43 trans/sec 11 Throughput: 0.04 MB/sec 12 Concurrency: 5.57 13 Successful transactions: 9000 14 Failed transactions: 0 15 Longest transaction: 1.07 16 Shortest transaction: 0.00
项目放在Github上:https://github.com/Zirconi/whd