写在前面
这是一个基于linux系统的webserver开发,支持通过ip+port+filename查看服务器的文件,是对黑马程序员课程《linux网络编程》最后一章webserver开发的代码实现和补充,原视频链接:02-web大练习的概述_bilibili_哔哩哔哩_bilibili
模型原理分析
主函数main(int argc, char* argv[])
首先请求输入 ./webserver port /home/dir
./webserver:执行文件名
port:端口号
/home/dir:目标文件地址
chdir()是为了改变执行文件的文件地址,以便在相对路径下读取到对应的文件名
定义函数:int chdir(const char * path);
函数说明:chdir()用户将当前的工作目录改变成以参数路径所指的目录。
返回值执行成功则返回0,失败返回-1,errno为错误代码
封装一个epoll_run()函数处理接取http请求和返回文件问题
int main(int argc, char* argv[])
{if (argc < 3){printf("please pirntf ./a.out ,port,path");}int port = atoi(argv[1]);//获取 端口号和目标地址int ret = chdir(argv[2]);if (ret != 0){perror("chdir error");exit(1);}epoll_run(port);return 0;
}
epoll_run(int port):
void epoll_run(int port)
{int i = 0;struct epoll_event all_event[MAXSIZE]; //红黑树的主体int epfd = epoll_create(MAXSIZE); //MAXSIZE=1024if (epfd == -1){perror("epoll_create error");exit(1);}int lfd = init_listen_fd(port, epfd); //封装的listen函数,处理监听和lfd的初始化while (1){int ret = epoll_wait(epfd, all_event, MAXSIZE, -1);if (ret == -1){perror("epoll_wait error");exit(1);}for (i = 0; i < ret; ++i){struct epoll_event* pev = &all_event[i];if (!(pev->events & EPOLLIN)) //如果不是读操作则直接跳过{continue;}else if (pev->data.fd == lfd){do_accept(lfd, epfd);}else{do_read(pev->data.fd, epfd);}}}
}
init_listen_fd(port, epfd);
int init_listen_fd(int port, int etfd)
{int lfd = socket(AF_INET, SOCK_STREAM, 0);if (lfd == -1){perror("socket error");exit(1);}struct sockaddr_in serv_addr;memset(&serv_addr, 0, sizeof(serv_addr));serv_addr.sin_family = AF_INET;serv_addr.sin_port = htons(port);serv_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*)&serv_addr, sizeof(serv_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(etfd, EPOLL_CTL_ADD, lfd, &ev);if (ret == -1){perror("epoll_ctl error");exit(1);}return lfd;}
do_accept(int lfd, int etfd);
这里说明一下,此webserver采用的是epoll的ET边缘触发,所以需要把cfd改为非阻塞模式
inet_ntop()
功能:将IPv4或IPv6 Internet网络地址转换为 Internet标准格式的字符串。
参数:
PCWSTR WSAAPI InetNtopW(
INT Family, //地址家族 IPV4使用AF_INET IPV6使用AF_INET6
const VOID *pAddr, //指向网络字节中要转换为字符串的IP地址的指针
PWSTR pStringBuf,//指向缓冲区的指针,该缓冲区用于存储IP地址的以NULL终止的字符串表示形式。
size_t StringBufSize//输入时,由pStringBuf参数指向的缓冲区的长度(以字符为单位)
);
inet_ntop()函数链接:inet_pton()和inet_ntop()功能及使用方法_inet ntop头文件-CSDN博客
void do_accept(int lfd, int etfd)
{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);}//accept()创建cfd套接字char client_ip[64] = { 0 };//打印成功accept的cfd信息:IP,端口号和cfd文件描述符printf("New Client IP:%s, Port:%d, cfd=%d,accept succeed!!!
",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);struct epoll_event ev;ev.data.fd = cfd;ev.events = EPOLLIN | EPOLLET;//使用的ET边缘触发int ret = epoll_ctl(etfd, EPOLL_CTL_ADD, cfd, &ev);if (ret == -1){perror("epoll_ctl error");exit(1);}}
do_read(int cfd, int etfd)
对cfd的读请求(http报文请求)进行处理和返回
void do_read(int cfd, int etfd)
{char line[1024] = { 0 };int len = get_line(cfd, line, sizeof(line));//读取第一行的报文if (len == -1){perror("get_line error");exit(1);}else if (len == 0){printf("client closed connevtion~
");disconnect(cfd, etfd);}else{char method[16], path[256], protocol[16];sscanf(line, "%[^ ] %[^ ] %[^ ]", method, path, protocol);//正则表达式printf("method = %s ,path = %s , protocol = %s
", method, path, protocol);//对第一行的报文进行拆分if (strncasecmp(method, "GET", 3) == 0 && strcmp(path, "/favicon.ico") == 0)//当读取到/favicon.ico的请求文件时候,不进行处理,不然会结束服务器进程(因为dir中没//有这个文件。{ send_respond(cfd, 204, "No Content", "Content-Type:text/plain", 0);return;}while (1) //循环读取剩下的报文数据,不然会阻塞{char buf[1024] = { 0 };len = get_line(cfd, buf, sizeof(buf));if (len == '
'){break;}else if (len == -1){break;}//直到读取结束:break————>跳出循环}if (strncasecmp(method, "GET", 3) == 0){char* file = path + 1; // 跳过 '/' if (strcmp(path, "/") == 0) //如果是输入:127.0.0.1:8000,读取本地文件夹{file = "./";}http_request(cfd, file);}}}
使用get_line ()读取第一行的报文
一般的报文格式:
GET(Example)
GET /562f25980001b1b106000338.jpg HTTP/1.1
Host:img.mukewang.com
User-Agent:Mozilla/5.0 (Windows NT 10.0; WOW64)
AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.106 Safari/537.36
Accept:image/webp,image/*,*/*;q=0.8
Referer:http://www.imooc.com/
Accept-Encoding:gzip, deflate, sdch
Accept-Language:zh-CN,zh;q=0.8
空行
请求数据为空
POST(Example,注意POST的请求内容不为空)
POST / HTTP1.1
Host:www.wrox.com
User-Agent:Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 2.0.50727; .NET CLR 3.0.04506.648; .NET CLR 3.5.21022)
Content-Type:application/x-www-form-urlencoded
Content-Length:40
Connection: Keep-Alive
空行
name=Professional%20Ajax&publisher=Wiley
这里我们需要自己封装一个get_line(),因为普通getline()是读取到每行末尾的‘ ’结束,而报文的每行结束语句是‘ ‘’ ',因此需要自己封装。
get_line(int cfd, char* buf, int size)
当读取到’/r’'/n’之后才返回buf
int get_line(int cfd, char* buf, int size)
{int i = 0;char c = '';int n;while ((i < size - 1) && (c != '
')){n = recv(cfd, &c, 1, 0);if (n > 0){if (c == '
'){n = recv(cfd, &c, 1, MSG_PEEK);if ((n > 0) && (c == '
')){n = recv(cfd, &c, 1, 0);}else{c = '
';}}buf[i] = c;i++;}else{c = '
';}}buf[i] = '';if (-1 == n){i = n;}return i;
}
对端关闭,执行disconnect(cfd, etfd);
void disconnect(int cfd, int etfd)
{int ret = epoll_ctl(etfd, EPOLL_CTL_DEL, cfd, NULL);if (ret == -1){perror("epoll_ctl error");exit(1);}close(cfd);
}
正则表达式读取报文第一行
正则表达式:正则表达式 – 语法 | 菜鸟教程
报错文件处理
在执行read函数时,服务器在返回第一个读取目标文件的报文请求后,还会默认发送一个/favicon.ico文件。
当读取到/favicon.ico的请求文件时候,不进行处理,不然会结束服务器进程(因为dir中没有这个文件)
strncasecmp函数
int strncasecmp(const char *s1, const char *s2, size_t n);描述
strncasecmp()用来比较参数s1 和s2 字符串前n个字符,比较时会自动忽略大小写的差异。
若参数s1 和s2 字符串相同则返回0。s1 若大于s2 则返回大于0 的值,s1 若小于s2 则返回小于0 的值。
void http_request(int cfd, const char* file)
void http_request(int cfd, const char* file)
{struct stat sbuf;int ret = stat(file, &sbuf); //判断文件类型,是文件还是文件夹if (ret != 0){//404perror("stat"); exit(1);}if (S_ISREG(sbuf.st_mode)) //如果是读取到文件的话{printf("---------it s a file
");char* s = get_file_type(file);char* type = malloc(256);sprintf(type, "Content-Type:%s", s); //把Content-Type:和后面的type连接起来//send_respond(cfd,200,"OK","Content-Type:text/plain;charset=iso-8859-1",sbuf.st_size); //send_respond(cfd,200,"OK","Content-Type:text/plain;charset=uft-8",sbuf.st_size); send_respond(cfd, 200, "OK", type, sbuf.st_size);send_file(cfd, file);free(type);}else if (S_ISDIR(sbuf.st_mode))//如果读取到文件夹的话{printf("------------it s a dir
");char* s = get_file_type(".html");char* type = malloc(256);sprintf(type, "Content-Type:%s", s);//把Content-Type:和后面的type连接起来send_respond(cfd, 200, "OK", type, sbuf.st_size);send_dir(cfd, file);free(type);}return;
}
get_file_type函数
char* get_file_type(const char* name)
{char* dot;dot = strrchr(name, '.');if (dot == NULL){return "text/plain;charset=utf-8";}if (strcmp(dot, ".jpg") == 0 || strcmp(dot, ".jepg") == 0){return "image/jpeg";}if (strcmp(dot, ".html") == 0){return "text/html;charset=uft-8";}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){return "video/quicktime";}return "text/plain;charset=iso-8859-1";
}
根据不同的文件请求类型,通过判断文件名后缀来分发不同的type值来匹配报文返回请求
send_respond函数
void send_respond(int cfd, int no, char* disp, char* type, int len)
{char buf[1024] = { 0 };sprintf(buf, "HTTP/1.1 %d %s
", no, disp);sprintf(buf + strlen(buf), "%s
", type);sprintf(buf + strlen(buf), "Connect-Length:%d
", len);send(cfd, buf, strlen(buf), 0);send(cfd, "
", 2, 0);return;
}
把返回报文连接起来
send_file()函数
void send_file(int cfd, const char* file)
{int n = 0;char buf[1024];int fd = open(file, O_RDONLY);if (fd == -1){perror("open error");exit(1);}while ((n = read(fd, buf, sizeof(buf))) > 0){send(cfd, buf, n, 0);}close(fd);return;
}
读取文件内容并返回,做到这一步已经可以请求并返回简单的文件了
send_dir()函数
void send_dir(int cfd, const char* dirname)
{int i, ret;char buf[4096] = { 0 };sprintf(buf, "<html><head><title>dir:%s</title></head>", dirname);//html文件头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);if (num < 0){perror("scandir error");return;}for (i = 0; i < num; ++i){char* name = ptr[i]->d_name;sprintf(path, "%s/%s", dirname, name);printf("path = %s==============
", path); //打印路径struct stat st;stat(path, &st);if (S_ISREG(st.st_mode)) //继续判断文件夹内的文件类型{sprintf(buf + strlen(buf),"<tr><td><a href="%s">%s</a></td><td>%ld</td></tr>",name, 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>",name, name, (long)st.st_size);}}sprintf(buf + strlen(buf), "</table></body></html>"); //html文件尾部ret = send(cfd, buf, strlen(buf), 0); //发出到客户端if (ret == -1){if (errno == EAGAIN){perror("send error:");}else if (errno == EINTR){perror("send error:");}else{perror("send error:");exit(1);}}for (i = 0; i < num; ++i) //释放指针内存{free(ptr[i]);}free(ptr);printf("dir message send ok!!!
");
}
判断读取为文件夹时,应该循环文件夹内的每一项,遍历读出返回(不能使用递归)
读取文件夹函数
int scandir(const char *dir, struct dirent ***namelist,
int (*select)(const struct dirent *),
int (*compar)(const struct dirent **, const struct dirent **));
函数说明
scandir()会扫描参数dir指定的目录文件,经由参数select(转者注:通过Linux的man手册查看函数原型,该形参名为fliter(过滤)可能目的更明确)指定的函数来挑选目录结构至参数namelist数组中,最后再调用参数compar指定的函数来排序namelist数组中的目录数据。每次从目录文件中读取一个目录结构后便将此结构传给参数select所指的函数,select函数若不想要将此目录结构复制到namelist数组就返回0,若select为空指针则代表选择所有的目录结构。scandir()会调用qsort()来排序数据,参数compar则为qsort()的参数,若是要排列目录名称字母则可使用alphasort()。结构dirent定义请参考readdir()。
完整代码
#include<pthread.h>
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<arpa/inet.h>
#include<sys/socket.h>
#include<sys/epoll.h>
#include<fcntl.h>
#include<sys/stat.h>
#include<string.h>
#include<dirent.h>
#include<errno.h>
#define MAXSIZE 1024
void disconnect(int cfd, int etfd)
{int ret = epoll_ctl(etfd, EPOLL_CTL_DEL, cfd, NULL);if (ret == -1){perror("epoll_ctl error");exit(1);}close(cfd);
}
int get_line(int cfd, char* buf, int size)
{int i = 0;char c = '';int n;while ((i < size - 1) && (c != '
')){n = recv(cfd, &c, 1, 0);if (n > 0){if (c == '
'){n = recv(cfd, &c, 1, MSG_PEEK);if ((n > 0) && (c == '
')){n = recv(cfd, &c, 1, 0);}else{c = '
';}}buf[i] = c;i++;}else{c = '
';}}buf[i] = '';if (-1 == n){i = n;}return i;
}void send_respond(int cfd, int no, char* disp, char* type, int len)
{char buf[1024] = { 0 };sprintf(buf, "HTTP/1.1 %d %s
", no, disp);sprintf(buf + strlen(buf), "%s
", type);sprintf(buf + strlen(buf), "Connect-Length:%d
", len);send(cfd, buf, strlen(buf), 0);send(cfd, "
", 2, 0);return;
}
void send_dir(int cfd, const char* dirname)
{int i, ret;char buf[4096] = { 0 };sprintf(buf, "<html><head><title>dir:%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);if (num < 0){perror("scandir error");return;}for (i = 0; i < num; ++i){char* name = ptr[i]->d_name;sprintf(path, "%s/%s", dirname, name);printf("path = %s==============
", path);struct stat st;stat(path, &st);if (S_ISREG(st.st_mode)){sprintf(buf + strlen(buf),"<tr><td><a href="%s">%s</a></td><td>%ld</td></tr>",name, 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>",name, name, (long)st.st_size);}}sprintf(buf + strlen(buf), "</table></body></html>");ret = send(cfd, buf, strlen(buf), 0);if (ret == -1){if (errno == EAGAIN){perror("send error:");}else if (errno == EINTR){perror("send error:");}else{perror("send error:");exit(1);}}for (i = 0; i < num; ++i){free(ptr[i]);}free(ptr);printf("dir message send ok!!!
");
}void send_file(int cfd, const char* file)
{int n = 0;char buf[1024];int fd = open(file, O_RDONLY);if (fd == -1){perror("open error");exit(1);}while ((n = read(fd, buf, sizeof(buf))) > 0){send(cfd, buf, n, 0);}close(fd);return;
}
char* get_file_type(const char* name)
{char* dot;dot = strrchr(name, '.');if (dot == NULL){return "text/plain;charset=utf-8";}if (strcmp(dot, ".jpg") == 0 || strcmp(dot, ".jepg") == 0){return "image/jpeg";}if (strcmp(dot, ".html") == 0){return "text/html;charset=uft-8";}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){return "video/quicktime";}return "text/plain;charset=iso-8859-1";
}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)){printf("---------it s a file
");char* s = get_file_type(file);char* type = malloc(256);sprintf(type, "Content-Type:%s", s);//send_respond(cfd,200,"OK","Content-Type:text/plain;charset=iso-8859-1",sbuf.st_size); //send_respond(cfd,200,"OK","Content-Type:text/plain;charset=uft-8",sbuf.st_size); send_respond(cfd, 200, "OK", type, sbuf.st_size);send_file(cfd, file);free(type);}else if (S_ISDIR(sbuf.st_mode))//??{printf("------------it s a dir
");char* s = get_file_type(".html");char* type = malloc(256);sprintf(type, "Content-Type:%s", s);send_respond(cfd, 200, "OK", type, sbuf.st_size);send_dir(cfd, file);free(type);}return;
}void do_read(int cfd, int etfd)
{char line[1024] = { 0 };int len = get_line(cfd, line, sizeof(line));if (len == -1){perror("get_line error");exit(1);}else if (len == 0){printf("client closed connevtion~
");disconnect(cfd, etfd);}else{char method[16], path[256], protocol[16];sscanf(line, "%[^ ] %[^ ] %[^ ]", method, path, protocol);printf("method = %s ,path = %s , protocol = %s
", method, path, protocol);if (strncasecmp(method, "GET", 3) == 0 && strcmp(path, "/favicon.ico") == 0){send_respond(cfd, 204, "No Content", "Content-Type:text/plain", 0);return;}while (1){char buf[1024] = { 0 };len = get_line(cfd, buf, sizeof(buf));if (len == '
'){break;}else if (len == -1){break;}}if (strncasecmp(method, "GET", 3) == 0){//??-- char* file = path + 1; // ????? '/' if (strcmp(path, "/") == 0){file = "./";}http_request(cfd, file);}}}
void do_accept(int lfd, int etfd)
{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+portchar client_ip[64] = { 0 };printf("New Client IP:%s, Port:%d, cfd=%d,accept ?????????
",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);struct epoll_event ev;ev.data.fd = cfd;ev.events = EPOLLIN | EPOLLET;//????et???????int ret = epoll_ctl(etfd, EPOLL_CTL_ADD, cfd, &ev);if (ret == -1){perror("epoll_ctl error");exit(1);}}
int init_listen_fd(int port, int etfd)
{int lfd = socket(AF_INET, SOCK_STREAM, 0);if (lfd == -1){perror("socket error");exit(1);}struct sockaddr_in serv_addr;memset(&serv_addr, 0, sizeof(serv_addr));serv_addr.sin_family = AF_INET;serv_addr.sin_port = htons(port);serv_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*)&serv_addr, sizeof(serv_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(etfd, EPOLL_CTL_ADD, lfd, &ev);if (ret == -1){perror("epoll_ctl error");exit(1);}return lfd;}
void epoll_run(int port)
{int i = 0;struct epoll_event all_event[MAXSIZE];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_event, MAXSIZE, -1);if (ret == -1){perror("epoll_wait error");exit(1);}for (i = 0; i < ret; ++i){struct epoll_event* pev = &all_event[i];if (!(pev->events & EPOLLIN)){continue;}else if (pev->data.fd == lfd){do_accept(lfd, epfd);}else{do_read(pev->data.fd, epfd);}}}
}
int main(int argc, char* argv[])
{if (argc < 3){printf("please pirntf ./a.out ,port,path");}int port = atoi(argv[1]);//????????int ret = chdir(argv[2]);if (ret != 0){perror("chdir error");exit(1);}epoll_run(port);return 0;
}
运行服务器 :
正常服务器输入:
客户端显示:
客户端输入:127.0.0.1 (IP) :8000(端口号)/fork.c(文件名)
或者打开新终端,输入:telnet ip port
再输入:GET /filename http/1.1
读取文件夹:
读取图片:
读取普通文件: