一、引言

C++的webserver项目是自己在学完网络编程后根据网课的内容做的一个初级的网络编程项目。

这个项目的效果是可以在浏览器通过输入网络IP地址和端口,然后打开对应的文件目录

效果如下:

也可以打开文件夹后点击目录,打开到对应的文件夹中去。

这个就是简单的webserver功能,后期自己也可以修改代码实现更多可能性的玩法,比如做一个简单的前端交互式的界面。

二、代码开发流程

我这个项目主要用到的实现方式,是用epoll,epoll是可以实现网络服务器编程有下面几个优点

1. 高效:epoll使用事件驱动模型,只有当IO事件发生时才会被激活,避免了轮询的开销,提高了服务器的效率。

2. 可扩展:epoll支持较大的并发连接数,可以处理成千上万个连接,而且在连接数量增加时,性能下降较慢。

3. 高可靠性:epoll使用边缘触发模式,只有在数据可读或可写时才会通知应用程序,避免了因为网络拥塞等原因导致的误报,提高了服务器的可靠性。

4. 灵活性:epoll支持多种事件类型,包括读、写、异常等,可以根据不同的需求进行定制。

5. 跨平台:epoll是Linux系统内核提供的机制,可以在不同的Linux系统上使用,实现跨平台开发。

下面是epoll开发webserver项目的流程图(不包括具体函数的实现)

int main(){ //若web服务器给浏览器发送数据的时候, 浏览器已经关闭连接, //则web服务器就会收到SIGPIPE信号struct sigaction act;act.sa_handler = SIG_IGN;sigemptyset(&act.sa_mask);act.sa_flags = 0;sigaction(SIGPIPE, &act, NULL);int lfd = tcp4bind(9999,NULL);Listen(lfd,128);int epfd = epoll_create(1024);if(epfd < 0){perror("epoll_create error");close(lfd);return -1;}struct epoll_event ev;struct epoll_event events[1024];ev.data.fd = lfd;ev.events = EPOLLIN;epoll_ctl(epfd,EPOLL_CTL_ADD,lfd,&ev);int nready;int i;int cfd;int sockfd;while(1){nready = epoll_wait(epfd,events,1024,-1);if(nready < 0){perror("epoll wait error");if(nready == EINTR){continue;}break; }for(i = 0;i  0)处阻塞 //设置cfd为非阻塞int flag = fcntl(cfd, F_GETFL);flag |= O_NONBLOCK;fcntl(cfd, F_SETFL, flag);ev.data.fd = cfd;ev.events = EPOLLIN;epoll_ctl(epfd,EPOLL_CTL_ADD,cfd,&ev);}else {http_request(sockfd,epfd);}}}}

上面的tcp4bind是封装好的函数,如果想看具体实现,可以看一下文章后面的全部代码wrap.c

代码。类似的Listen,Accept也是封装好的函数。

三、http_request 函数

这个函数是具体实现,打开文件和打开文件夹的函数。

当客户端发起HTTP请求时,服务器会调用http_request函数来处理请求。函数流程如下:

函数流程如下:

  1. 读取请求行数据,分析出要请求的资源文件名。
  2. 判断请求的文件是否存在,若不存在则发送404 NOT FOUND的头部信息和error.html文件内容。
  3. 若文件存在,判断文件类型,如果是普通文件则发送200 OK的头部信息和文件内容;如果是目录文件则发送200 OK的头部信息和目录文件列表信息的html内容。
  4. 发送完数据后关闭连接,并将文件描述符从epoll树上删除。

代码

int http_request(int cfd, int epfd){int n;char buf[1024];//读取请求行数据, 分析出要请求的资源文件名memset(buf, 0x00, sizeof(buf));n = Readline(cfd, buf, sizeof(buf));if(n<=0){//printf("read error or client closed, n==[%d]\n", n);//关闭连接close(cfd); //将文件描述符从epoll树上删除epoll_ctl(epfd, EPOLL_CTL_DEL, cfd, NULL);return -1;}printf("buf==[%s]\n", buf);//GET /hanzi.c HTTP/1.1char reqType[16] = {0};char fileName[255] = {0};char protocal[16] = {0};sscanf(buf, "%[^ ] %[^ ] %[^ \r\n]", reqType, fileName, protocal);//printf("[%s]\n", reqType);printf("--[%s]--\n", fileName);//printf("[%s]\n", protocal); char *pFile = fileName;if(strlen(fileName)0); //判断文件是否存在struct stat st;if(stat(pFile, &st)<0){printf("file not exist\n"); //发送头部信息send_header(cfd, "404", "NOT FOUND", get_mime_type(".html"), 0); //发送文件内容send_file(cfd, "error.html"); }else //若文件存在{//判断文件类型//普通文件if(S_ISREG(st.st_mode))//man 2 stat查询,S_ISREG表示普通文件{printf("file exist\n");//发送头部信息send_header(cfd, "200", "OK", get_mime_type(pFile), st.st_size); //发送文件内容send_file(cfd, pFile);}//目录文件else if(S_ISDIR(st.st_mode)){printf("目录文件\n"); char buffer[1024];//发送头部信息send_header(cfd, "200", "OK", get_mime_type(".html"), 0);//发送html文件头部send_file(cfd, "html/dir_header.html");//文件列表信息struct dirent **namelist;int num; num = scandir(pFile, &namelist, NULL, alphasort);if (num d_name); memset(buffer, 0x00, sizeof(buffer)); if(namelist[num]->d_type==DT_DIR) {sprintf(buffer, "
  • %s
  • ", namelist[num]->d_name, namelist[num]->d_name); } else {sprintf(buffer, "
  • %s
  • ", namelist[num]->d_name, namelist[num]->d_name); } free(namelist[num]); Write(cfd, buffer, strlen(buffer)); } free(namelist);}//发送html尾部sleep(10);send_file(cfd, "html/dir_tail.html"); }} return 0;}

    四、细节

    1.cfd要设置为非阻塞

    //设置cfd为非阻塞 int flag = fcntl(cfd, F_GETFL); flag |= O_NONBLOCK; fcntl(cfd, F_SETFL, flag);

    这段代码的作用是将文件描述符cfd设置为非阻塞模式。

    首先,使用fcntl函数和F_GETFL命令获取cfd的文件状态标志。这些标志包括文件的读写模式、是否阻塞等信息。获取后的标志保存在flag变量中。

    接着,使用按位或运算符(|)将O_NONBLOCK标志(表示非阻塞模式)添加到flag变量中。这样做是为了将O_NONBLOCK标志添加到文件描述符的状态标志中,表示将该文件描述符设置为非阻塞模式。

    最后,使用fcntl函数和F_SETFL命令将修改后的flag标志设置回文件描述符cfd,以实现将cfd设置为非阻塞模式。

    因此,这段代码的作用是将文件描述符cfd设置为非阻塞模式,以便在进行I/O操作时,如果没有数据可读或没有足够的空间可写,不会阻塞进程的执行,而是立即返回一个错误或一个特殊的状态,使得进程可以继续执行其他任务。

    2.要改变环境工作目录

    前提是把webpath设置在家目录下

    char path[255] = {0};sprintf(path, "%s/%s", getenv("HOME"), "webpath");chdir(path);

    这段代码的作用是构造一个路径并将当前工作目录切换到该路径。

    让我们逐步解释这段代码:

    char path[255] = {0};

    – 定义一个长度为255的字符数组path,并初始化为0。这个数组将用来存储构造的路径。

    sprintf(path, “%s/%s”, getenv(“HOME”), “webpath”);

    – 使用sprintf函数将路径构造为$HOME/webpath的形式。getenv(“HOME”)用于获取当前用户的主目录路径,然后将其与”webpath”拼接起来,得到完整的路径。

    chdir(path);

    – 使用chdir函数将当前工作目录切换到构造的路径。这样,程序的当前工作目录就会变成$HOME/webpath。

    综合起来,这段代码的作用是构造一个路径,并将当前工作目录切换到该路径。通常情况下,这样的操作用于确保程序在正确的目录下执行,以便正确地访问和处理文件。

    3.fileName 读取位置+1,略过“/“

    不然就是下面这样

    4.scandir函数

    scandir 函数是用于扫描指定目录并返回目录中的文件列表的函数。它返回一个指向 dirent 结构的指针数组,每个结构包含一个目录中的一个条目的信息。

    以下是 scandir 函数的原型:

    int scandir(const char *dirp, struct dirent ***namelist,int (*filter)(const struct dirent *),int (*compar)(const struct dirent **, const struct dirent **));

    dirp:要扫描的目录的路径名。

    namelist:指向指针数组的指针,用于存储指向每个目录条目的指针。

    filter:一个可选的过滤函数,用于决定哪些目录条目应该被返回。如果不需要过滤,可以将其设置为 NULL。

    compar:一个可选的比较函数,用于对返回的目录条目进行排序。如果不需要排序,可以将其设置为 NULL。

    以下是一个简单的示例,演示了如何使用 scandir 函数来列出目录中的文件:

    #include #include #include int main() {struct dirent **namelist;int n;n = scandir(".", &namelist, NULL, alphasort);if (n < 0) {perror("scandir");exit(EXIT_FAILURE);} else {for (int i = 0; i d_name);free(namelist[i]);}free(namelist);}return 0;}

    在这个示例中,scandir 函数扫描当前目录,并使用 alphasort 函数对返回的文件列表进行排序。然后,它遍历列表并打印每个文件的名称。

    5.添加默认路径

    比如http://192.168.44.3:9999 可以访问默认的主目录下面的文件夹内容

    char *pFile = fileName;if(strlen(fileName)<=1)//添加默认为主目录下面 { strcpy(pFile, "./");}else{ pFile = fileName+1;}

    注意不能将char *pFile fileName = NULL 设置为这样,否则会产生段错误

    6.解决遇到汉字的问题

    在webserver代码中调用了一个函数

    strdecode(pFile, pFile); 这个函数在pub.c中,然后写了一个”编码”,用作回写浏览器的时候,将除字母数字及/_.-~以外的字符转义后回写。

    //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';}

    5.对 SIGPIPE 信号的处理方式设置为忽略

    对 SIGPIPE 信号的处理方式设置为忽略,即当进程收到 SIGPIPE 信号时,不做任何处理。这通常用于避免在网络编程中出现 SIGPIPE 错误,因为当一个进程向一个已经关闭的 socket 发送数据时,系统会向该进程发送 SIGPIPE 信号,如果不处理该信号,进程会终止。通过将 SIGPIPE 信号的处理方式设置为忽略,可以避免进程因此而终止。

    //若web服务器给浏览器发送数据的时候, 浏览器已经关闭连接, //则web服务器就会收到SIGPIPE信号struct sigaction act;act.sa_handler = SIG_IGN;sigemptyset(&act.sa_mask);act.sa_flags = 0;sigaction(SIGPIPE, &act, NULL);

    五、完整代码

    webserver.c

    //web服务端程序--使用epoll模型#include #include #include #include #include #include #include  #include "pub.h"#include "wrap.h" int http_request(int cfd, int epfd); int main(){//若web服务器给浏览器发送数据的时候, 浏览器已经关闭连接, //则web服务器就会收到SIGPIPE信号struct sigaction act;act.sa_handler = SIG_IGN;sigemptyset(&act.sa_mask);act.sa_flags = 0;sigaction(SIGPIPE, &act, NULL); //改变当前进程的工作目录char path[255] = {0};sprintf(path, "%s/%s", getenv("HOME"), "webpath");chdir(path); //创建socket--设置端口复用---bindint lfd = tcp4bind(9999, NULL); //设置监听 Listen(lfd, 128); //创建epoll树int epfd = epoll_create(1024);if(epfd<0){perror("epoll_create error");close(lfd);return -1;} //将监听文件描述符lfd上树struct epoll_event ev;ev.data.fd = lfd;ev.events = EPOLLIN;epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev); int i;int cfd;int nready;int sockfd;struct epoll_event events[1024];while(1){//等待事件发生nready = epoll_wait(epfd, events, 1024, -1);if(nready<0){if(errno==EINTR){continue;}break;} for(i=0; i0){sprintf(buf+strlen(buf), "Content-Length:%d\r\n", len);}strcat(buf, "\r\n");Write(cfd, buf, strlen(buf));return 0;} int send_file(int cfd, char *fileName){//打开文件int fd = open(fileName, O_RDONLY);if(fd<0){perror("open error");return -1;} //循环读文件, 然后发送int n;char buf[1024];while(1){memset(buf, 0x00, sizeof(buf));n = read(fd, buf, sizeof(buf));if(n<=0){break;}else{Write(cfd, buf, n);}}} int http_request(int cfd, int epfd){int n;char buf[1024];//读取请求行数据, 分析出要请求的资源文件名memset(buf, 0x00, sizeof(buf));n = Readline(cfd, buf, sizeof(buf));if(n<=0){//printf("read error or client closed, n==[%d]\n", n);//关闭连接close(cfd); //将文件描述符从epoll树上删除epoll_ctl(epfd, EPOLL_CTL_DEL, cfd, NULL);return -1;}printf("buf==[%s]\n", buf);//GET /hanzi.c HTTP/1.1char reqType[16] = {0};char fileName[255] = {0};char protocal[16] = {0};sscanf(buf, "%[^ ] %[^ ] %[^ \r\n]", reqType, fileName, protocal);//printf("[%s]\n", reqType);printf("--[%s]--\n", fileName);//printf("[%s]\n", protocal); char *pFile = fileName;if(strlen(fileName)0); //判断文件是否存在struct stat st;if(stat(pFile, &st)<0){printf("file not exist\n"); //发送头部信息send_header(cfd, "404", "NOT FOUND", get_mime_type(".html"), 0); //发送文件内容send_file(cfd, "error.html"); }else //若文件存在{//判断文件类型//普通文件if(S_ISREG(st.st_mode))//man 2 stat查询,S_ISREG表示普通文件{printf("file exist\n");//发送头部信息send_header(cfd, "200", "OK", get_mime_type(pFile), st.st_size); //发送文件内容send_file(cfd, pFile);}//目录文件else if(S_ISDIR(st.st_mode)){printf("目录文件\n"); char buffer[1024];//发送头部信息send_header(cfd, "200", "OK", get_mime_type(".html"), 0);//发送html文件头部send_file(cfd, "html/dir_header.html");//文件列表信息struct dirent **namelist;int num; num = scandir(pFile, &namelist, NULL, alphasort);if (num d_name); memset(buffer, 0x00, sizeof(buffer)); if(namelist[num]->d_type==DT_DIR) {sprintf(buffer, "
  • %s
  • ", namelist[num]->d_name, namelist[num]->d_name); } else {sprintf(buffer, "
  • %s
  • ", namelist[num]->d_name, namelist[num]->d_name); } free(namelist[num]); Write(cfd, buffer, strlen(buffer)); } free(namelist);}//发送html尾部sleep(10);send_file(cfd, "html/dir_tail.html"); }} return 0;}

    pub.c

    #include "pub.h"//通过文件名字获得文件类型char *get_mime_type(char *name){char* dot; dot = strrchr(name, '.'); //自右向左查找‘.’字符;如不存在返回NULL/* *charset=iso-8859-1西欧的编码,说明网站采用的编码是英文; *charset=gb2312说明网站采用的编码是简体中文; *charset=utf-8 代表世界通用的语言编码; *可以用到中文、韩文、日文等世界上所有语言编码上 *charset=euc-kr说明网站采用的编码是韩文; *charset=big5说明网站采用的编码是繁体中文; * *以下是依据传递进来的文件名,使用后缀判断是何种文件类型 *将对应的文件类型按照http定义的关键字发送回去 */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";}/**********************************************************************//* Get a line from a socket, whether the line ends in a newline, * carriage return, or a CRLF combination.Terminates the string read * with a null character.If no newline indicator is found before the * end of the buffer, the string is terminated with a null.If any of * the above three line terminators is read, the last character of the * string will be a linefeed and the string will be terminated with a * null character. * Parameters: the socket descriptor * the buffer to save the data in * the size of the buffer * Returns: the number of bytes stored (excluding null) *//**********************************************************************///获得一行数据,每行以\r\n作为结束标记int get_line(int sock, char *buf, int size){int i = 0;char c = '\0';int n; while ((i  0){if (c == '\r'){n = recv(sock, &c, 1, MSG_PEEK);//MSG_PEEK 从缓冲区读数据,但是数据不从缓冲区清除/* DEBUG printf("%02X\n", c); */if ((n > 0) && (c == '\n'))recv(sock, &c, 1, 0);elsec = '\n';}buf[i] = c;i++;}elsec = '\n';}buf[i] = '\0'; return(i);} //下面的函数第二天使用/* * 这里的内容是处理%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]);//字符串E8变成了真正的16进制的E8from += 2;//移过已经处理的两个字符(%21指针指向1),表达式3的++from还会再向后移一个字符} else*to = *from;}*to = '\0';} //16进制数转化为10进制, return 0不会出现int hexit(char c){if (c >= '0' && c = 'a' && 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';}

    wrap.c

    #include #include #include #include #include #include #include #include //绑定错误显示和退出void perr_exit(const char *s){perror(s);exit(-1);} int Accept(int fd, struct sockaddr *sa, socklen_t *salenptr){int n; again:if ((n = accept(fd, sa, salenptr)) < 0) {if ((errno == ECONNABORTED) || (errno == EINTR))//ECONNABORTED 代表连接失败 ETINTR 代表被信号打断goto again;elseperr_exit("accept error");}return n;} int Bind(int fd, const struct sockaddr *sa, socklen_t salen){int n; if ((n = bind(fd, sa, salen)) < 0)perr_exit("bind error"); return n;} int Connect(int fd, const struct sockaddr *sa, socklen_t salen){int n; if ((n = connect(fd, sa, salen)) < 0)perr_exit("connect error"); return n;} int Listen(int fd, int backlog){int n; if ((n = listen(fd, backlog)) < 0)perr_exit("listen error"); return n;} int Socket(int family, int type, int protocol){int n; if ((n = socket(family, type, protocol))  0) {if ((nread = read(fd, ptr, nleft))  0) {if ( (nwritten = write(fd, ptr, nleft)) <= 0) {if (nwritten < 0 && errno == EINTR)nwritten = 0;elsereturn -1;} nleft -= nwritten;ptr += nwritten;}return n;} static ssize_t my_read(int fd, char *ptr){static int read_cnt;static char *read_ptr;static char read_buf[100];//定义了100的缓冲区 if (read_cnt <= 0) {again://使用缓冲区可以避免多次从底层缓冲读取数据--为了提高效率if ( (read_cnt = read(fd, read_buf, sizeof(read_buf))) < 0) {if (errno == EINTR)goto again;return -1;} else if (read_cnt == 0)return 0;read_ptr = read_buf;}read_cnt--;*ptr = *read_ptr++;//从缓冲区取数据 return 1;}//读取一行ssize_t Readline(int fd, void *vptr, size_t maxlen){ssize_t n, rc;charc, *ptr; ptr = vptr;for (n = 1; n < maxlen; n++) {if ( (rc = my_read(fd, &c)) == 1) {*ptr++ = c;if (c== '\n')//代表任务完成break;} else if (rc == 0) {//对端关闭*ptr = 0;//0 = '\0'return n - 1;} elsereturn -1;}*ptr= 0; return n;} int tcp4bind(short port,const char *IP){struct sockaddr_in serv_addr;int lfd = Socket(AF_INET,SOCK_STREAM,0);bzero(&serv_addr,sizeof(serv_addr));//清空serv_addr地址 对比 memset()if(IP == NULL){//如果这样使用 0.0.0.0,任意ip将可以连接serv_addr.sin_addr.s_addr = INADDR_ANY;}else{if(inet_pton(AF_INET,IP,&serv_addr.sin_addr.s_addr) <= 0){perror(IP);//转换失败exit(1);}}serv_addr.sin_family = AF_INET;serv_addr.sin_port = htons(port);int opt = 1;setsockopt(lfd,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));Bind(lfd,(struct sockaddr *)&serv_addr,sizeof(serv_addr));return lfd;}

    完整项目包上篇文章有,自取。感谢支持