1. 简介
epoll 是linux下的一个 I/O 多路复用机制,用于高效地监听多个文件描述符上的 I/O 事件。相较于 select 和 poll,epoll 在处理大量连接时具有更好的性能。而HTTP是应用层协议,同其他应用层协议一样,是为了实现某一类具体应用的协议,并由某一运行在用户空间的应用程序来实现其功能。它也是基于B/S架构进行通信的,下面就结合epoll来实现一个能够给浏览器提供服务,供用户借助浏览器访问服务器主机中文件的B/S模式。
2. 代码实现
2.1 主函数
主函数主要负责把启动程序时,传进来的参数用变量来接收,并检测,当传进来的参数不满3个时,就会报错,最后传入端口,来调用一个监听函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| int main(int argc, char *argv[]){ if(argc < 3){ printf("server port path\n"); } int port = atoi(argv[1]); int ret = chdir(argv[2]); if(ret != 0){ perror("chdir error"); exit(1); } epoll_run(port); return 0; }
|
2.2 核心函数
这部分负责了小项目的大部分代码任务,监听连接、创建红黑树、向红黑树上添加监听描述符和通信描述符、以及建立连接后,对协议头的检查、创建发送协议头等。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| void epoll_run(int port){ int i; struct epoll_event all_events[MAXSIZE]; int epfd = epoll_create(MAXSIZE); if(epfd == -1){ perror("epoll_create error"); exit(1); } int lfd = init_listen_fd(port, epfd); while(1){ int ret = epoll_wait(epfd,all_events,MAXSIZE,-1); if(ret == -1){ perror("epoll_wait error"); exit(1); } for(i=0; i<ret; i++){ struct epoll_event *pev = &all_events[i]; if(!(pev->events & EPOLLIN)){ continue; } if(pev->data.fd == lfd){ do_accept(lfd,epfd); }else{ do_read(pev->data.fd, epfd); } } } }
|
下面是init_listen_fd()
函数的实现,服务端绑定以及将监听的文件描述符挂到红黑数epoll上。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| int init_listen_fd(int port,int epfd){ int lfd = socket(AF_INET,SOCK_STREAM,0); if(lfd == -1){ perror("socket error"); exit(1); } struct sockaddr_in srv_addr; bzero(&srv_addr,sizeof(srv_addr)); srv_addr.sin_family = AF_INET; srv_addr.sin_port = htons(port); srv_addr.sin_addr.s_addr = htonl(INADDR_ANY); int opt = 1; setsockopt(lfd,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt)); int ret = bind(lfd,(struct sockaddr *)&srv_addr,sizeof(srv_addr)); if(ret == -1){ perror("bind error"); exit(1); } ret = listen(lfd,128); if(ret == -1){ perror("listen error"); exit(1); } struct epoll_event ev; ev.events = EPOLLIN; ev.data.fd = lfd; ret = epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev); if(ret == -1){ perror("epoll_ctl add lfd error"); exit(1); } return lfd; }
|
处理连接的函数:该函数负责与对端建立连接,由于是监听到有加内特描述符的读事件已就绪,所以执行该函数时,是不会阻塞的。与对端连接好连接后,就将与对端的通信描述符添加到epoll树上。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| void do_accept(int lfd,int epfd){ struct sockaddr_in clt_addr; socklen_t clt_addr_len = sizeof(clt_addr); int cfd = accept(lfd, (struct sockaddr*)&clt_addr, &clt_addr_len); if(cfd == -1){ perror("accept error"); exit(1); } char client_ip[64] = {0}; printf("New client ip: %s, port: %d, cfd = %d\n", inet_ntop(AF_INET,&clt_addr.sin_addr.s_addr,client_ip,sizeof(client_ip)), ntohs(clt_addr.sin_port),cfd); int flag = fcntl(cfd,F_GETFL); flag |= O_NONBLOCK; fcntl(cfd,F_SETFL,flag); struct epoll_event ev; ev.data.fd = cfd; ev.events = EPOLLIN | EPOLLET; int ret = epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev); if(ret == -1){ perror("epoll_ctl add cfd error"); exit(1); } }
|
通信函数实现:该函数负责的就是与对端的通信,其中需要判断对端发来的是什么类型请求,过程有点复杂。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| void do_read(int cfd, int epfd){ char line[1024] = {0}; int len = get_line(cfd,line,sizeof(line)); if(len == 0){ printf("服务器,检查到客户端关闭....\n"); disconnect(cfd,epfd); }else{ printf("================请求头================"); printf("请求行数据:%s",line); while(1){ char buf[1024] = {0}; len = get_line(cfd,buf,sizeof(buf)); if(buf[0] == '\n'){ break; }else if(len == -1){ break; } } printf("===============The end===============\n"); if(strncasecmp("get", line, 3)==0){ http_request(line, cfd); } } }
|
在得到对端传来的请求后,通过请求的第一行来查看对端需要的文件是什么类型,并检查看当前文件有无对端需要的文件,不同情况。采取不同处理方式。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| void http_request(const char* request, int cfd){ char method[12], path[1024], protocol[12]; sscanf(request, "%[^ ] %[^ ] %[^ ]", method, path, protocol); printf("method = %s, path = %s, protocol = %s\n", method, path, protocol); decode_str(path,path); char *file = path + 1; if(strcmp(path,"/")==0){ file = "./"; } struct stat sbuf; int ret = stat(file,&sbuf); if(ret == -1){ send_error(cfd, 404, "Not Found", "No such file or direntry"); return; } if(S_ISDIR(sbuf.st_mode)){ send_respond_head(cfd, 200, "OK", get_file_type(".html"), -1); send_dir(cfd,file); }else if(S_ISREG(sbuf.st_mode)){ send_respond_head(cfd,200,"OK", get_file_type(file), sbuf.st_size); send_file(cfd, file); } }
|
在得到对端请求文件的类型后,作为服务器,发送给对端内容,也需要创建一个响应头,其中下面程序就针对对端请求的文件类型,生成了对应的响应头,并先发送给对端。
1 2 3 4 5 6 7 8 9 10 11 12 13
| void send_respond_head(int cfd, int no, const char *desp, const char *type, long len){ char buf[1024] = {0}; sprintf(buf, "HTTP/1.1 %d %s\r\n", no, desp); send(cfd, buf, strlen(buf), 0); sprintf(buf, "Content-Type:%s\r\n", type); sprintf(buf+strlen(buf), "Content-Length:%ld\r\n", len); send(cfd, buf, strlen(buf), 0); send(cfd, "\r\n", 2, 0); }
|
在发送完响应头后,也知道了对端请求的是一个目录,而本地也存在该目录,接下来就是将该目录内容在满足http协议的前提下发送给对端。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
| void send_dir(int cfd, const char* dirname){ int i, ret; char buf[4094] = {0}; sprintf(buf, "<html><head><title>目录名:%s</title></head>", dirname); sprintf(buf+strlen(buf),"<body><h1>当前目录:%s</h1><table>", dirname); char enstr[1024] = {0}; char path[1024] = {0}; struct dirent** ptr; int num = scandir(dirname, &ptr, NULL, alphasort); for(i=0; i<num; i++){ char* name = ptr[i]->d_name; sprintf(path, "%s/%s", dirname, name); printf("path = %s =================\n", path); struct stat st; stat(path,&st); encode_str(enstr, sizeof(enstr), name); if(S_ISREG(st.st_mode)){ sprintf(buf+strlen(buf),"<tr><td><a href=\"%s\">%s</a></td><td>%ld</td></tr>", enstr, name, (long)st.st_size); }else if(S_ISDIR(st.st_mode)){ sprintf(buf+strlen(buf), "<tr><td><a href=\"%s/\">%s/</a></td><td>%ld</td></tr>", enstr, name, (long)st.st_size); } ret = send(cfd, buf, strlen(buf), 0); if(ret == -1){ if(errno == EAGAIN){ perror("send error"); continue; }else if(errno == EINTR){ perror("send error"); continue; }else{ perror("send error"); exit(1); } } memset(buf, 0, sizeof(buf)); } sprintf(buf+strlen(buf), "</table></body></html>"); send(cfd, buf, strlen(buf), 0); printf("dir message send OK!!!\n"); }
|
在发送完响应头后,也知道了对端请求的是一个具体文件,而本地也存在该文件,接下来服务器就是打开该文件读取,将文件内容在满足http协议的前提下发送给对端。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| void send_file(int cfd, const char *file){ int n = 0; char buf[1024]; int fd = open(file, O_RDONLY); if(fd == -1){ send_error(cfd, 404, "Not Found", "No such file or direntry"); exit(1); } while((n = read(fd, buf, sizeof(buf))) > 0){ int ret = send(cfd, buf, n, 0); if(ret == -1){ printf("errno = %d\n",errno); if(errno == EAGAIN){ printf("----------------EAGAIN\n"); continue; }else if(errno == EINTR){ printf("---------------EINTR\n"); continue; }else{ perror("send error"); exit(1); } } if(ret < 4096){ printf("----------send ret: %d\n", ret); } } close(fd); }
|
2.3 辅助函数
断开连接的函数:当对端请求关闭时,会调用该函数。
1 2 3 4 5 6 7 8 9
| void disconnect(int cfd, int epfd){ int ret = epoll_ctl(epfd,EPOLL_CTL_DEL,cfd,NULL); if(ret == -1){ perror("epoll_ctl del cfd error"); exit(1); } close(cfd); }
|
404错误页面函数:当服务器在本地没有找到对端的请求目录文件时或服务器打开本地文件出错都会调用该函数,会发送给对端一个404页面。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| void send_error(int cfd, int status, char *title, char *text){ char buf[4096] = {0}; sprintf(buf, "%s %d %s\r\n", "HTTP/1.1", status, title); sprintf(buf+strlen(buf), "Content-Type:%s\r\n", "text/html"); sprintf(buf+strlen(buf), "Content-Length:%d\r\n", -1); sprintf(buf+strlen(buf), "Connection: close\r\n"); send(cfd, buf, strlen(buf), 0); send(cfd, "\r\n", 2, 0); memset(buf, 0, sizeof(buf)); sprintf(buf, "<html><head><title>%d %s</title></head>\n", status, title); sprintf(buf+strlen(buf), "<body bgcolor=\"#cc99cc\"><h4 align=\"center\">%d %s</h4>\n", status,title); sprintf(buf+strlen(buf), "%s\n",text); sprintf(buf+strlen(buf), "<hr>\n</body>\n</html>\n"); send(cfd, buf, strlen(buf), 0); printf("-----------------------------错误页面啊----------------------------\n"); return ; }
|
获取协议第一行的函数:该函数是服务器在接收到对端发来的请求时,为了得到请求头的第一行而调用的函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| int get_line(int cfd, char *buf, int size){ int i=0; char c = '\0'; int n; while((i<size-1) && (c!='\n')){ n = recv(cfd,&c,1,0); if(n>0){ if(c == '\r'){ n = recv(cfd,&c,1,MSG_PEEK); if((n>0) && (c=='\n')){ recv(cfd,&c,1,0); }else{ c='\n'; } } buf[i] = c; i++; }else{ c = '\n'; } } buf[i]='\0'; if(n==-1){ i=n; } return i; }
|
编码和解码函数:编码是服务器将本地找到的文件目录(中文)转为浏览器的一些编码格式(unicode编码),发给对端;而解码就是从对端(浏览器)请求中显示的文件(unicode编格式)转为中文,这样服务器在本地也方便搜索寻找。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| void encode_str(char* to, int tosize, const char* from){ int tolen; for(tolen = 0; *from != '\0' && tolen+4 < tosize; from++){ if(isalnum(*from) || strchr("/_.-~", *from) != (char*)0){ *to = *from; to++; tolen++; }else{ sprintf(to, "%%%02x",(int) *from&0xff); to += 3; tolen += 3; } } *to = '\0'; }
void decode_str(char *to, char *from){ for( ; *from != '\0'; to++, from++){ if(from[0] == '%' && isxdigit(from[1]) && isxdigit(from[2])){ *to = hexit(from[1])*16 + hexit(from[2]); from += 2; }else{ *to = *from; } } *to = '\0'; }
int hexit(char c){ if(c >= '0' && c <= '9'){ return c - '0'; } if(c >= 'a' && c <= 'f'){ return c - 'a' + 10; } if(c >= 'A' && c <= 'F'){ return c - 'A' + 10; } return 0; }
|
文件类型函数:通过对端请求头的请求文件,来得到该请求文件的文件类型(响应头需要文件的类型)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
| const char *get_file_type(const char *name){ char *dot; dot = strrchr(name,'.'); if(dot == NULL){ return "text/plain; charset=utf-8"; } if(strcmp(dot,".html")==0 || strcmp(dot,".htm")==0){ return "text/html; charset=utf-8"; } if(strcmp(dot,".jpg")==0 || strcmp(dot,".jpeg")==0){ return "image/jpeg"; } if(strcmp(dot,".gif")==0){ return "image/gif"; } if(strcmp(dot,".png")==0){ return "image/png"; } if(strcmp(dot,".css")==0){ return "text/css"; } if(strcmp(dot,".au")==0){ return "audio/basic"; } if(strcmp(dot,".wav")==0){ return "audio/wav"; } if(strcmp(dot,".avi")==0){ return "video/x-msvideo"; } if(strcmp(dot,".mov")==0 || strcmp(dot,".qt")==0){ return "video/quicktime"; } if(strcmp(dot,".mpeg")==0 || strcmp(dot,".mpe")==0){ return "video/mpeg"; } if(strcmp(dot,".vrml")==0 || strcmp(dot,".wrl")==0){ return "model/vrml"; } if(strcmp(dot,".midi")==0 || strcmp(dot,".mid")==0){ return "audio/midi"; } if(strcmp(dot,".mp3")==0){ return "audio/mpeg"; } if(strcmp(dot,".ogg")==0){ return "application/ogg"; } if(strcmp(dot,".pac")==0){ return "application/x-ns-proxy-autoconfig"; } return "text/plain; charset=utf-8"; }
|
3. 执行效果
这是服务器本地的一个文件,具体有这些文件内容:
当服务器启动后,在本地的浏览器输入服务器ip和设置的端口,就能得到如下界面:
上面页面的所有内容都可以通过在浏览器端口后面输入对应的文件名或直接点击页面文件获得,如:
点击document3_文档就可以进入如下页面,当然,还可以再点击a.txt或b.txt,访问该txt文件里面的内容
点击图片就会显示如下页面:
点击歌曲也可以播放:
对于视频,因为这个centos系统没有下载对于的视频插件,所以不能播放。