文章目录:
一:超文本编辑语言HTML
二:HTTP超文本传输协议
1.请求和响应消息流程
myhttpd.c
getline函数,:用于读取http协议头
2.错误
2.1 处理出错返回disconnect
2.2 错误页面展示send_error
3.写出http应答协议头
写数据给浏览器send_file
4.浏览器请求目录
5.汉字字符编码和解码
6. http常见状态码
三:正则表达式
四:文件操作
1.文件类型区分get_file_type
2.单文件通信
3.判断文件是否存在
五:web服务器实现
1.基于epoll实现Web服务器
epoll_server.h
epoll_server.c
main.c
2.基于libevent实现Web服务器
libevent_http.h
libevent_http.c
main.c
一:超文本编辑语言HTML
大多数标签成对儿出现, 不成对儿出现的被称为**短标签
这里就是前端的网页相关的知识
二:HTTP超文本传输协议
1.请求和响应消息流程
http请求消息(浏览器发给服务器): 1. 请求行: 说明请求类型, 要访问的资源以及使用的http版本; 2. 请求头: 说明服务器要使用的附加信息; 3. 空行:必须!, 即使没有请求数据; 4. 请求数据: 也叫主体, 可以添加任意的其他数据; 以下是浏览器发送给服务器的http协议头内容举例: GET /hello.c HTTP/1.1 Host:localhost:2222 User-Agent:Mozilla/5.0(X11;Ubuntu;Linux i686;rv:24.0)Gecko/201001 01 Firefox/24.0 Accept:text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language:zh-cn,zh;q=0.8,en-us;q=0.5,en;q=0.3 Accept-Encoding:gzip,deflate Connection:keep-alive If-Modified-Since:Fri,18 Jul 2014 08:36:36 GMT \r\n http响应消息(服务器发给浏览器): 1. 状态行: 包括http协议版本号, 状态码, 状态信息; 2. 消息报头: 说明客户端要使用的一些附加信息; 3. 空行: 必须! 4. 响应正文: 服务器返回给客户端的文本信息(或数据流); 以下是服务器发送给浏览器的http协议头内容举例: HTTP/1.1 200 OK Server:xhttpd Date:Fri,18 Jul 2014 14:34:26 GMT Content-Type:text/plain;charset=iso-8859-1 Content-Length:32 Content-Language:zh-CN Last-Modified:Fri,18,Jul 2014 08:36:36 GMT Connection:close \r\n 提交密码信息用get请求,就会明文显示,而post则不会显示出涉密信息 Get 请求指定的页面信息,并返回实体主体 Post 向指定资源提交数据进行处理请求(例如提交表单或者上传文件) 数据被包含在请求体中。POST请求可能会导致新的资源的建立和/或已有资源的修改 Head 类似于get请求,但是响应消息没有内容,只是获得报头 Put 从客户端向浏览器传送的数据取代指定的文档内容 Delete 请求服务器删除指定的页面 Connect HTTP/1.1协议中预留给能够将连接改为管道方式的代理服务器 Options 允许客户端查看浏览器的性能 Trace 回显服务器收到的请求,主要用于测试和诊断
1. getline() 获取 http协议的第一行 2. 从首行中拆分 GET、文件名、协议版本。 获取用户请求的文件名 3. 判断文件是否存在。 stat() 4. 判断是文件还是目录 5. 是文件-- open -- read -- 写回给浏览器 6. 先写 http 应答协议头 : http/1.1 200 ok Content-Type:text/plain; charset=iso-8859-1 7. 写文件数据
myhttpd.c
#include <stdio.h> #include <string.h> #include <stdlib.h> #include <netinet/in.h> #include <arpa/inet.h> #include <sys/wait.h> #include <sys/types.h> #include <sys/stat.h> #include <sys/epoll.h> #include <unistd.h> #include <fcntl.h> #define MAXSIZE 2048 // 获取一行 \r\n 结尾的数据 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 (-1 == n) i = n; return i; } int init_listen_fd(int port, int epfd) { // 创建监听的套接字 lfd int lfd = socket(AF_INET, SOCK_STREAM, 0); if (lfd == -1) { perror("socket error"); exit(1); } // 创建服务器地址结构 IP+port 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)); // 给 lfd 绑定地址结构 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); } // lfd 添加到 epoll 树上 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; } 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); } // 打印客户端IP+port 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); // 设置 cfd 非阻塞 int flag = fcntl(cfd, F_GETFL); flag |= O_NONBLOCK; fcntl(cfd, F_SETFL, flag); // 将新节点cfd 挂到 epoll 监听树上 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); } } // 断开链接 void disconnect(int cfd, int epfd) { int ret = epoll_ctl(epfd, EPOLL_CTL_DEL, cfd, NULL); if (ret != 0) { perror("epoll_ctl error"); exit(1); } close(cfd); } // 客户端端的fd, 错误号,错误描述,回发文件类型, 文件长度 void send_respond(int cfd, int no, char *disp, char *type, int len) { char buf[4096] = {0}; sprintf(buf, "HTTP/1.1 %d %s\r\n", no, disp); send(cfd, buf, strlen(buf), 0); sprintf(buf, "Content-Type: %s\r\n", type); sprintf(buf+strlen(buf), "Content-Length:%d\r\n", len); send(cfd, buf, strlen(buf), 0); send(cfd, "\r\n", 2, 0); } // 发送服务器本地文件 给浏览器 void send_file(int cfd, const char *file) { int n = 0, ret; char buf[4096] = {0}; // 打开的服务器本地文件。 --- cfd 能访问客户端的 socket int fd = open(file, O_RDONLY); if (fd == -1) { // 404 错误页面 perror("open error"); exit(1); } while ((n = read(fd, buf, sizeof(buf))) > 0) { ret = send(cfd, buf, n, 0); if (ret == -1) { perror("send error"); exit(1); } if (ret < 4096) printf("-----send ret: %d\n", ret); } close(fd); } // 处理http请求, 判断文件是否存在, 回发 void http_request(int cfd, const char *file) { struct stat sbuf; // 判断文件是否存在 int ret = stat(file, &sbuf); if (ret != 0) { // 回发浏览器 404 错误页面 perror("stat"); exit(1); } if(S_ISREG(sbuf.st_mode)) { // 是一个普通文件 // 回发 http协议应答 //send_respond(cfd, 200, "OK", " Content-Type: text/plain; charset=iso-8859-1", sbuf.st_size); send_respond(cfd, 200, "OK", "Content-Type:image/jpeg", -1); //send_respond(cfd, 200, "OK", "audio/mpeg", -1); // 回发 给客户端请求数据内容。 send_file(cfd, file); } } void do_read(int cfd, int epfd) { // 读取一行http协议, 拆分, 获取 get 文件名 协议号 char line[1024] = {0}; char method[16], path[256], protocol[16]; int len = get_line(cfd, line, sizeof(line)); //读 http请求协议首行 GET /hello.c HTTP/1.1 if (len == 0) { printf("服务器,检查到客户端关闭....\n"); disconnect(cfd, epfd); } else { sscanf(line, "%[^ ] %[^ ] %[^ ]", method, path, protocol); printf("method=%s, path=%s, protocol=%s\n", method, path, protocol); while (1) { char buf[1024] = {0}; len = get_line(cfd, buf, sizeof(buf)); if (buf[0] == '\n') { break; } else if (len == -1) break; } } if (strncasecmp(method, "GET", 3) == 0) { char *file = path+1; // 取出 客户端要访问的文件名 http_request(cfd, file); disconnect(cfd, epfd); } } void epoll_run(int port) { int i = 0; struct epoll_event all_events[MAXSIZE]; // 创建一个epoll监听树根 int epfd = epoll_create(MAXSIZE); if (epfd == -1) { perror("epoll_create error"); exit(1); } // 创建lfd,并添加至监听树 int lfd = init_listen_fd(port, epfd); while (1) { // 监听节点对应事件 int ret = epoll_wait(epfd, all_events, MAXSIZE, 0); 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); } } } } int main(int argc, char *argv[]) { // 命令行参数获取 端口 和 server提供的目录 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监听 epoll_run(port); return 0; }
getline函数,:用于读取http协议头
int get_line(int cfd,char* buf,int size){ int i=0; char c='\0'; int n=0; 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; }
2.错误
2.1 处理出错返回disconnect
错误处理函数: void disconnect(int cfd,int epfd){ int ret=epoll_ctl(epfd,EPOLL_CTL_DEL,cfd,NULL); if(ret!=0) perr_exit("epoll_ctl del error"); close(cfd); return; } 读数据的思路: void do_read(int cfd,int epfd){ /*读取一行http协议,拆分,获取get文件名和协议号*/ char line[1024]={0}; int len=get_line(cfd,line,sizeof(line)); if(len==0){ printf("Client close\n"); disconnect(cfd,epfd); }else{ /*字符串拆分*/ } return; }
2.2 错误页面展示send_error
返回值一定要检查, 尤其在开发初期;
void send_error(int cfd,int status,char* title,char* text){ char buf[BUFSIZ]={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),"Contention:close\r\n"); send(cfd,buf,strlen(buf),0); send(cfd,"\r\n",2,0); memset(buf,0,BUFSIZ); sprintf(buf,"<html><head><title>%d %s</title></head>\n",status,title); sprintf(buf+strlen(buf),"<body bgcolor=\"#cc99cc\"><h3 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); return; }
3.写出http应答协议头
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:%d\r\n",len); send(cfd,buf,strlen(buf),0); send(cfd,"\r\n",2,0); return; }
写数据给浏览器send_file
void send_file(int cfd,const char* filename){ int n=0; int ret=0; int fd=0; char buf[BUFSIZ]={0}; fd=open(filename,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){ ret=send(cfd,buf,n,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); } } } if(n==-1) perr_exit("read file error"); close(fd); return; }
4.浏览器请求目录
/*发送目录数据-OK*/ void send_dir(int cfd,const char* dirname){ int i=0; int ret=0; int num=0; char buf[4096]={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; 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); /*编码生成Unicode编码:诸如%E5%A7...等*/ encode_str(enstr,sizeof(enstr),name); if(S_ISREG(st.st_mode)){ sprintf(buf+strlen(buf),"<tr><td><a href=\"%s\">%s</a></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"); return; }
5.汉字字符编码和解码
每一个汉字在浏览器前端中会被转码成Unicode码进行显示
因此在访问带有汉字的文件时, 应该在服务器回发数据给浏览器时进行编码操作, 在浏览器请求资源目录的汉字文件时进行解码操作;
/*16进制字符转化为10进制-OK*/ 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; } /*解码函数-OK*/ 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'; return; } /*编码函数-OK*/ void encode_str(char* to,int tosize,const char* from){ int tolen=0; 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'; return; }
6. http常见状态码
http状态码由三位数字组成,第一个数字代表响应的类别,有五种分类: 1xx 指示信息--表示请求已接收,继续处理 2xx 成功--表示请求已被成功接收、理解、接受 3xx 重定向--要完成请求必须进行更进一步的操作 4xx 客户端错误--请求有语法错误或请求无法实现 5xx 服务器端错误--服务器未能实现合法的请求 常见的状态码如下: 200 OK 客户端请求成功 301 Moved Permanently 重定向 400 Bad Request 客户端请求有语法错误,不能被服务器所理解 401 Unauthorized 请求未经授权,这个状态代码必须和WWW-Authenticate报头域一起使用 403 Forbidden 服务器收到请求,但是拒绝提供服务 404 Not Found 请求资源不存在,eg:输入了错误的URL 500 Internal Server Error 服务器发生不可预期的错误 503 Server Unavailable 服务器当前不能处理客户端的请求,一段时间后可能恢复正常
三:正则表达式
正则表达式
在线代码格式化
四:文件操作
1.文件类型区分get_file_type
http与浏览器交互时,为使浏览器能够识别文件信息,所以需要传递文件类型,这也是响应消息必填项,常见的类型如下:
普通文件: text/plain; charset=utf-8
*.html: text/html; charset=utf-8
*.jpg: image/jpeg
*.gif: image/gif
*.png: image/png
*.wav: audio/wav
*.avi: video/x-msvideo
*.mov: video/quicktime
*.mp3: audio/mpeg
/*判断文件类型*/ 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,".wav")==0) return "audio/wav"; if(strcmp(dot,".mp3")==0) return "audio/mpeg"; if(strcmp(dot,".avi")==0) return "video/x-msvideo"; /*其他的文件一律当作文本文件处理*/ return "text/plain; charset=utf-8"; }
2.单文件通信
单文件通信流程分析: - getline()获取http协议的第一行; - 从首行中拆分GET, 文件名, 协议版本. 获取用户请求的文件名; - 判断文件是否存在, 用stat函数; - 是文件, open打开, read内容, 写回给浏览器; - 先写http应答协议头: HTTP/1.1 200 OK Server:xhttpd Date:Fri,18 Jul 2014 14:34:26 GMT Content-Type:text/plain;charset=iso-8859-1 Content-Length:32 Content-Language:zh-CN Last-Modified:Fri,18,Jul 2014 08:36:36 GMT Connection:close \r\n - 再写文件数据;
3.判断文件是否存在
void do_read(int cfd,int epfd){ /*读取一行http协议,拆分,获取get文件名和协议号*/ char line[1024]={0}; int len=get_line(cfd,line,sizeof(line)); if(len==0){ printf("Client close\n"); disconnect(cfd,epfd); }else{ printf("----请求头----\n"); printf("请求行数据:%s\n",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("----请求尾----\n"); } /*确定是GET方法(忽略大小写比较字符串前n个字符)*/ if(strncasecmp("get",line,3)==0){ http_request(line,cfd); } disconnect(cfd,epfd); return; }
五:web服务器实现
可使用telnet命令, 借助IP和port, 模拟浏览器行为, 在终端中对访问的服务器进行调试, 方便查看服务器会发给浏览器的http协议数据: `telnet 127.0.0.1 9527` `GET /hello.c http/1.1` 此时在终端中可查看到服务器回发给浏览器的http应答协议及数据内容, 可根据该信息进行调试;
1.基于epoll实现Web服务器
epoll_server.h
#ifndef _EPOLL_SERVER_H #define _EPOLL_SERVER_H int init_listen_fd(int port, int epfd); void epoll_run(int port); void do_accept(int lfd, int epfd); void do_read(int cfd, int epfd); int get_line(int sock, char *buf, int size); void disconnect(int cfd, int epfd); void http_request(const char* request, int cfd); void send_respond_head(int cfd, int no, const char* desp, const char* type, long len); void send_file(int cfd, const char* filename); void send_dir(int cfd, const char* dirname); void encode_str(char* to, int tosize, const char* from); void decode_str(char *to, char *from); const char *get_file_type(const char *name); #endif
epoll_server.c
#include <stdio.h> #include <errno.h> #include <unistd.h> #include <stdlib.h> #include <sys/types.h> #include <string.h> #include <sys/epoll.h> #include <arpa/inet.h> #include <fcntl.h> #include <dirent.h> #include <sys/stat.h> #include <ctype.h> #include "epoll_server.h" #define MAXSIZE 2000 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\"><h2 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); return ; } void epoll_run(int port) { int i = 0; // 创建一个epoll树的根节点 int epfd = epoll_create(MAXSIZE); if(epfd == -1) { perror("epoll_create error"); exit(1); } // 添加要监听的节点 // 先添加监听lfd int lfd = init_listen_fd(port, epfd); // 委托内核检测添加到树上的节点 struct epoll_event all[MAXSIZE]; while(1) { int ret = epoll_wait(epfd, all, MAXSIZE, 0); if(ret == -1) { perror("epoll_wait error"); exit(1); } // 遍历发生变化的节点 for(i=0; i<ret; ++i) { // 只处理读事件, 其他事件默认不处理 struct epoll_event *pev = &all[i]; if(!(pev->events & EPOLLIN)) { // 不是读事件 continue; } if(pev->data.fd == lfd){ // 接受连接请求 do_accept(lfd, epfd); } else { // 读数据 printf("======================before do read, ret = %d\n", ret); do_read(pev->data.fd, epfd); printf("=========================================after do read\n"); } } } } // 读数据 void do_read(int cfd, int epfd) { // 将浏览器发过来的数据, 读到buf中 char line[1024] = {0}; // 读请求行 int len = get_line(cfd, line, sizeof(line)); if(len == 0) { printf("客户端断开了连接...\n"); // 关闭套接字, cfd从epoll上del disconnect(cfd, epfd); } else { printf("============= 请求头 ============\n"); 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"); } // 判断get请求 if(strncasecmp("get", line, 3) == 0) { // 请求行: get /hello.c http/1.1 // 处理http请求 http_request(line, cfd); // 关闭套接字, cfd从epoll上del disconnect(cfd, epfd); } } // 断开连接的函数 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); } // http请求处理 void http_request(const char* request, int cfd) { // 拆分http请求行 char method[12], path[1024], protocol[12]; sscanf(request, "%[^ ] %[^ ] %[^ ]", method, path, protocol); printf("method = %s, path = %s, protocol = %s\n", method, path, protocol); // 转码 将不能识别的中文乱码 -> 中文 // 解码 %23 %34 %5f decode_str(path, path); char* file = path+1; // 去掉path中的/ 获取访问文件名 // 如果没有指定访问的资源, 默认显示资源目录中的内容 if(strcmp(path, "/") == 0) { // file的值, 资源目录的当前位置 file = "./"; } // 获取文件属性 struct stat st; int ret = stat(file, &st); if(ret == -1) { send_error(cfd, 404, "Not Found", "NO such file or direntry"); return; } // 判断是目录还是文件 if(S_ISDIR(st.st_mode)) { // 目录 // 发送头信息 send_respond_head(cfd, 200, "OK", get_file_type(".html"), -1); // 发送目录信息 send_dir(cfd, file); } else if(S_ISREG(st.st_mode)) { // 文件 // 发送消息报头 send_respond_head(cfd, 200, "OK", get_file_type(file), st.st_size); // 发送文件内容 send_file(cfd, file); } } // 发送目录内容 void send_dir(int cfd, const char* dirname) { int i, ret; // 拼一个html页面<table></table> 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); // 编码生成 %E5 %A7 之类的东西 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"); #if 0 // 打开目录 DIR* dir = opendir(dirname); if(dir == NULL) { perror("opendir error"); exit(1); } // 读目录 struct dirent* ptr = NULL; while( (ptr = readdir(dir)) != NULL ) { char* name = ptr->d_name; } closedir(dir); #endif } // 发送响应头 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); } // 发送文件 void send_file(int cfd, const char* filename) { // 打开文件 int fd = open(filename, O_RDONLY); if(fd == -1) { send_error(cfd, 404, "Not Found", "NO such file or direntry"); exit(1); } // 循环读文件 char buf[4096] = {0}; int len = 0, ret = 0; while( (len = read(fd, buf, sizeof(buf))) > 0 ) { // 发送读出的数据 ret = send(cfd, buf, len, 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); } } } if(len == -1) { perror("read file error"); exit(1); } close(fd); } // 解析http请求消息的每一行内容 int get_line(int sock, char *buf, int size) { int i = 0; char c = '\0'; int n; while ((i < size - 1) && (c != '\n')) { n = recv(sock, &c, 1, 0); if (n > 0) { if (c == '\r') { n = recv(sock, &c, 1, MSG_PEEK); if ((n > 0) && (c == '\n')) { recv(sock, &c, 1, 0); } else { c = '\n'; } } buf[i] = c; i++; } else { c = '\n'; } } buf[i] = '\0'; return i; } // 接受新连接处理 void do_accept(int lfd, int epfd) { struct sockaddr_in client; socklen_t len = sizeof(client); int cfd = accept(lfd, (struct sockaddr*)&client, &len); if(cfd == -1) { perror("accept error"); exit(1); } // 打印客户端信息 char ip[64] = {0}; printf("New Client IP: %s, Port: %d, cfd = %d\n", inet_ntop(AF_INET, &client.sin_addr.s_addr, ip, sizeof(ip)), ntohs(client.sin_port), cfd); // 设置cfd为非阻塞 int flag = fcntl(cfd, F_GETFL); flag |= O_NONBLOCK; fcntl(cfd, F_SETFL, flag); // 得到的新节点挂到epoll树上 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); } } int init_listen_fd(int port, int epfd) { // 创建监听的套接字 int lfd = socket(AF_INET, SOCK_STREAM, 0); if(lfd == -1) { perror("socket error"); exit(1); } // lfd绑定本地IP和port struct sockaddr_in serv; memset(&serv, 0, sizeof(serv)); serv.sin_family = AF_INET; serv.sin_port = htons(port); serv.sin_addr.s_addr = htonl(INADDR_ANY); // 端口复用 int flag = 1; setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &flag, sizeof(flag)); int ret = bind(lfd, (struct sockaddr*)&serv, sizeof(serv)); if(ret == -1) { perror("bind error"); exit(1); } // 设置监听 ret = listen(lfd, 64); if(ret == -1) { perror("listen error"); exit(1); } // lfd添加到epoll树上 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; } // 16进制数转化为10进制 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; } /* * 这里的内容是处理%20之类的东西!是"解码"过程。 * %20 URL编码中的‘ ’(space) * %21 '!' %22 '"' %23 '#' %24 '$' * %25 '%' %26 '&' %27 ''' %28 '('...... * 相关知识html中的‘ ’(space)是  */ 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'; } // 通过文件名获取文件的类型 const char *get_file_type(const char *name) { char* dot; // 自右向左查找‘.’字符, 如不存在返回NULL 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"; }
main.c
#include <stdio.h> #include <unistd.h> #include <stdlib.h> #include "epoll_server.h" int main(int argc, const char* argv[]) { if(argc < 3) { printf("eg: ./a.out port path\n"); exit(1); } // 采用指定的端口 int port = atoi(argv[1]); // 修改进程工作目录, 方便后续操作 int ret = chdir(argv[2]); if(ret == -1) { perror("chdir error"); exit(1); } // 启动epoll模型 epoll_run(port); return 0; }
2.基于libevent实现Web服务器
libevent_http.h
#ifndef _LIBEVENT_HTTP_H #define _LIBEVENT_HTTP_H #include <event2/event.h> void conn_eventcb(struct bufferevent *bev, short events, void *user_data); void conn_readcb(struct bufferevent *bev, void *user_data); const char *get_file_type(char *name); int hexit(char c); void listener_cb(struct evconnlistener *listener, evutil_socket_t fd, struct sockaddr *sa, int socklen, void *user_data); int response_http(struct bufferevent *bev, const char *method, char *path); int send_dir(struct bufferevent *bev,const char *dirname); int send_error(struct bufferevent *bev); int send_file_to_http(const char *filename, struct bufferevent *bev); int send_header(struct bufferevent *bev, int no, const char* desp, const char *type, long len); void signal_cb(evutil_socket_t sig, short events, void *user_data); void strdecode(char *to, char *from); void strencode(char* to, size_t tosize, const char* from); #endif
libevent_http.c
#include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <fcntl.h> #include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> #include <sys/stat.h> #include <string.h> #include <dirent.h> #include <time.h> #include <signal.h> #include <ctype.h> #include <errno.h> #include <event2/bufferevent.h> #include <event2/buffer.h> #include <event2/listener.h> #include "libevent_http.h" #define _HTTP_CLOSE_ "Connection: close\r\n" int response_http(struct bufferevent *bev, const char *method, char *path) { if(strcasecmp("GET", method) == 0){ //get method ... strdecode(path, path); char *pf = &path[1]; if(strcmp(path, "/") == 0 || strcmp(path, "/.") == 0) { pf="./"; } printf("***** http Request Resource Path = %s, pf = %s\n", path, pf); struct stat sb; if(stat(pf,&sb) < 0) { perror("open file err:"); send_error(bev); return -1; } if(S_ISDIR(sb.st_mode))//处理目录 { //应该显示目录列表 send_header(bev, 200, "OK", get_file_type(".html"), -1); send_dir(bev, pf); } else //处理文件 { send_header(bev, 200, "OK", get_file_type(pf), sb.st_size); send_file_to_http(pf, bev); } } return 0; } /* *charset=iso-8859-1 西欧的编码,说明网站采用的编码是英文; *charset=gb2312 说明网站采用的编码是简体中文; *charset=utf-8 代表世界通用的语言编码; * 可以用到中文、韩文、日文等世界上所有语言编码上 *charset=euc-kr 说明网站采用的编码是韩文; *charset=big5 说明网站采用的编码是繁体中文; * *以下是依据传递进来的文件名,使用后缀判断是何种文件类型 *将对应的文件类型按照http定义的关键字发送回去 */ const char *get_file_type(char *name) { char* dot; dot = strrchr(name, '.'); //自右向左查找‘.’字符;如不存在返回NULL if (dot == (char*)0) 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"; } int send_file_to_http(const char *filename, struct bufferevent *bev) { int fd = open(filename, O_RDONLY); int ret = 0; char buf[4096] = {0}; while((ret = read(fd, buf, sizeof(buf)) ) ) { bufferevent_write(bev, buf, ret); memset(buf, 0, ret); } close(fd); return 0; } int send_header(struct bufferevent *bev, int no, const char* desp, const char *type, long len) { char buf[256]={0}; sprintf(buf, "HTTP/1.1 %d %s\r\n", no, desp); //HTTP/1.1 200 OK\r\n bufferevent_write(bev, buf, strlen(buf)); // 文件类型 sprintf(buf, "Content-Type:%s\r\n", type); bufferevent_write(bev, buf, strlen(buf)); // 文件大小 sprintf(buf, "Content-Length:%ld\r\n", len); bufferevent_write(bev, buf, strlen(buf)); // Connection: close bufferevent_write(bev, _HTTP_CLOSE_, strlen(_HTTP_CLOSE_)); //send \r\n bufferevent_write(bev, "\r\n", 2); return 0; } int send_error(struct bufferevent *bev) { send_header(bev,404, "File Not Found", "text/html", -1); send_file_to_http("404.html", bev); return 0; } int send_dir(struct bufferevent *bev,const char *dirname) { char encoded_name[1024]; char path[1024]; char timestr[64]; struct stat sb; struct dirent **dirinfo; int i; char buf[4096] = {0}; sprintf(buf, "<html><head><meta charset=\"utf-8\"><title>%s</title></head>", dirname); sprintf(buf+strlen(buf), "<body><h1>当前目录:%s</h1><table>", dirname); //添加目录内容 int num = scandir(dirname, &dirinfo, NULL, alphasort); for(i=0; i<num; ++i) { // 编码 strencode(encoded_name, sizeof(encoded_name), dirinfo[i]->d_name); sprintf(path, "%s%s", dirname, dirinfo[i]->d_name); printf("############# path = %s\n", path); if (lstat(path, &sb) < 0) { sprintf(buf+strlen(buf), "<tr><td><a href=\"%s\">%s</a></td></tr>\n", encoded_name, dirinfo[i]->d_name); } else { strftime(timestr, sizeof(timestr), " %d %b %Y %H:%M", localtime(&sb.st_mtime)); if(S_ISDIR(sb.st_mode)) { sprintf(buf+strlen(buf), "<tr><td><a href=\"%s/\">%s/</a></td><td>%s</td><td>%ld</td></tr>\n", encoded_name, dirinfo[i]->d_name, timestr, sb.st_size); } else { sprintf(buf+strlen(buf), "<tr><td><a href=\"%s\">%s</a></td><td>%s</td><td>%ld</td></tr>\n", encoded_name, dirinfo[i]->d_name, timestr, sb.st_size); } } bufferevent_write(bev, buf, strlen(buf)); memset(buf, 0, sizeof(buf)); } sprintf(buf+strlen(buf), "</table></body></html>"); bufferevent_write(bev, buf, strlen(buf)); printf("################# Dir Read OK !!!!!!!!!!!!!!\n"); return 0; } void conn_readcb(struct bufferevent *bev, void *user_data) { printf("******************** begin call %s.........\n",__FUNCTION__); char buf[4096]={0}; char method[50], path[4096], protocol[32]; bufferevent_read(bev, buf, sizeof(buf)); printf("buf[%s]\n", buf); sscanf(buf, "%[^ ] %[^ ] %[^ \r\n]", method, path, protocol); printf("method[%s], path[%s], protocol[%s]\n", method, path, protocol); if(strcasecmp(method, "GET") == 0) { response_http(bev, method, path); } printf("******************** end call %s.........\n", __FUNCTION__); } void conn_eventcb(struct bufferevent *bev, short events, void *user_data) { printf("******************** begin call %s.........\n", __FUNCTION__); if (events & BEV_EVENT_EOF) { printf("Connection closed.\n"); } else if (events & BEV_EVENT_ERROR) { printf("Got an error on the connection: %s\n", strerror(errno)); } bufferevent_free(bev); printf("******************** end call %s.........\n", __FUNCTION__); } void signal_cb(evutil_socket_t sig, short events, void *user_data) { struct event_base *base = user_data; struct timeval delay = { 1, 0 }; printf("Caught an interrupt signal; exiting cleanly in one seconds.\n"); event_base_loopexit(base, &delay); } void listener_cb(struct evconnlistener *listener, evutil_socket_t fd, struct sockaddr *sa, int socklen, void *user_data) { printf("******************** begin call-------%s\n",__FUNCTION__); struct event_base *base = user_data; struct bufferevent *bev; printf("fd is %d\n",fd); bev = bufferevent_socket_new(base, fd, BEV_OPT_CLOSE_ON_FREE); if (!bev) { fprintf(stderr, "Error constructing bufferevent!"); event_base_loopbreak(base); return; } bufferevent_flush(bev, EV_READ | EV_WRITE, BEV_NORMAL); bufferevent_setcb(bev, conn_readcb, NULL, conn_eventcb, NULL); bufferevent_enable(bev, EV_READ | EV_WRITE); printf("******************** end call-------%s\n",__FUNCTION__); } /* * 这里的内容是处理%20之类的东西!是"解码"过程。 * %20 URL编码中的‘ ’(space) * %21 '!' %22 '"' %23 '#' %24 '$' * %25 '%' %26 '&' %27 ''' %28 '('...... * 相关知识html中的‘ ’(space)是  */ void strdecode(char *to, char *from) { for ( ; *from != '\0'; ++to, ++from) { if (from[0] == '%' && isxdigit(from[1]) && isxdigit(from[2])) { // 依次判断from中 %20 三个字符 *to = hexit(from[1])*16 + hexit(from[2]); // 移过已经处理的两个字符(%21指针指向1),表达式3的++from还会再向后移一个字符 from += 2; } else { *to = *from; } } *to = '\0'; } //16进制数转化为10进制, return 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; } // "编码",用作回写浏览器的时候,将除字母数字及/_.-~以外的字符转义后回写。 // strencode(encoded_name, sizeof(encoded_name), name); void strencode(char* to, size_t 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'; }
main.c
#include <stdio.h> #include <unistd.h> #include <arpa/inet.h> #include <stdlib.h> #include <string.h> #include <signal.h> #include <event2/bufferevent.h> #include <event2/listener.h> #include <event2/event.h> #include "libevent_http.h" int main(int argc, char **argv) { if(argc < 3) { printf("./event_http port path\n"); return -1; } if(chdir(argv[2]) < 0) { printf("dir is not exists: %s\n", argv[2]); perror("chdir err:"); return -1; } struct event_base *base; struct evconnlistener *listener; struct event *signal_event; struct sockaddr_in sin; base = event_base_new(); if (!base) { fprintf(stderr, "Could not initialize libevent!\n"); return 1; } memset(&sin, 0, sizeof(sin)); sin.sin_family = AF_INET; sin.sin_port = htons(atoi(argv[1])); // 创建监听的套接字,绑定,监听,接受连接请求 listener = evconnlistener_new_bind(base, listener_cb, (void *)base, LEV_OPT_REUSEABLE | LEV_OPT_CLOSE_ON_FREE, -1, (struct sockaddr*)&sin, sizeof(sin)); if (!listener) { fprintf(stderr, "Could not create a listener!\n"); return 1; } // 创建信号事件, 捕捉并处理 signal_event = evsignal_new(base, SIGINT, signal_cb, (void *)base); if (!signal_event || event_add(signal_event, NULL)<0) { fprintf(stderr, "Could not create/add a signal event!\n"); return 1; } // 事件循环 event_base_dispatch(base); evconnlistener_free(listener); event_free(signal_event); event_base_free(base); printf("done\n"); return 0; }