c_cpp Linux的下epoll的模型实现简单的HTTP服务器

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了c_cpp Linux的下epoll的模型实现简单的HTTP服务器相关的知识,希望对你有一定的参考价值。

#ifndef _PUB_H
#define _PUB_H
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <strings.h>
#include <ctype.h>
int tcp4bind(short port,const char *IP);
char *get_mime_type(char *name);//通过文件名字获得文件类型
int get_line(int sock, char *buf, int size);//获取一行
int hexit(char c);//16进制转10进制
void strencode(char* to, size_t tosize, const char* from);//编码
void strdecode(char *to, char *from);//解码
#endif

#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 < size - 1) && (c != '\n'))
    {
        n = recv(sock, &c, 1, 0);
        /* DEBUG printf("%02X\n", c); */
        if (n > 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);
                else
                    c = '\n';
            }
            buf[i] = c;
            i++;
        }
        else
            c = '\n';
    }
    buf[i] = '\0';

    return(i);
}
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));
    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));
    if(bind(lfd,(struct sockaddr *)&serv_addr,sizeof(serv_addr)) < 0){
    	perror("bind err");
    	exit(-1);
    }
    return lfd;
}

/*
 * 这里的内容是处理%20之类的东西!是"解码"过程。
 * %20 URL编码中的‘ ’(space)
 * %21 '!' %22 '"' %23 '#' %24 '$'
 * %25 '%' %26 '&' %27 ''' %28 '('......
 * 相关知识html中的‘ ’(space)是&nbsp
 */
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]);
            from += 2;                      //移过已经处理的两个字符(%21指针指向1),表达式3的++from还会再向后移一个字符 
        } 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';
}
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <errno.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <unistd.h>
#include <dirent.h>
#include <signal.h>

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#include <sys/epoll.h>
#include "pub.h"

#define MAX_CONNSIZE 1024

typedef struct _EventData
{
    int fd;
    int epfd;
    uint32_t events;
    void(*callback)(struct _EventData* pthis);
}EventData;

int send_header(int cfd, int xcode, char *xmsg, char *fileType, long lFileSize);
int send_file(int cfd, int resfd);
int send_dir(int cfd, DIR* dirfd);
void cb_response(EventData* pthis);
void cb_accept(EventData* pthis);
void send_404(int cfd);

int main(int argc, const char* argv[])
{
    //切换工作目录到 $HOME/webroot
    chdir(getenv("HOME"));
    chdir("./webroot");

    int lfd = tcp4bind(8080, NULL);//绑定socket --> bind
    listen(lfd, 128);//监听

    int epfd = epoll_create(1);//创建根节点

    //将lfd上树
    struct epoll_event ev;
    ev.events = EPOLLIN;//设置events
    ev.data.ptr = malloc(sizeof(EventData));//设置data
    ((EventData*)ev.data.ptr)->fd = lfd;
    ((EventData*)ev.data.ptr)->epfd = epfd;
    ((EventData*)ev.data.ptr)->events = ev.events;
    ((EventData*)ev.data.ptr)->callback = cb_accept;
    epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);

    //fcntl(lfd, F_SETFL, fcntl(lfd, F_GETFL) | O_NONBLOCK);//设置lfd为非阻塞

    //循环wait
    struct epoll_event evs[MAX_CONNSIZE + 1]; //最大连接数+lfd
    int nready;
    while (1)
    {
        nready = epoll_wait(epfd, evs, MAX_CONNSIZE, -1);//等待

        for (int i = 0; i < nready; i++)//循环处理事件
        {
            EventData* currev = evs[i].data.ptr;
            currev->callback(currev);
        }
    }

    return 0;
}


void send_404(int cfd)
{
    int fd404 = open("404.html", O_RDONLY);
    if (fd404 == -1)
    {
        perror("open 404.html");
        return;
    }
    //发送404
    struct stat st;
    fstat(fd404, &st);
    send_header(cfd, 404, "Not Found", get_mime_type("404.html"), st.st_size);
    //发送内容
    //open()
    send_file(cfd, fd404);
    close(fd404);
}

int send_header(int cfd, int xcode, char *xmsg, char *fileType, long lFileSize)
{
    char buf[1024];
    memset(buf, 0x00, sizeof(buf));
    sprintf(buf, "HTTP/1.1 %d %s\r\n", xcode, xmsg);
    send(cfd, buf, strlen(buf), 0);//发送响应第一行

    memset(buf, 0x00, sizeof(buf));
    sprintf(buf, "Content-Type:%s\r\n", fileType);
    send(cfd, buf, strlen(buf), 0);//发送响应第二行
    if (lFileSize > 0)//如果文件大小>0就发送长度数
    {
        memset(buf, 0x00, sizeof(buf));
        sprintf(buf, "Content-Length:%ld\r\n", lFileSize);
        send(cfd, buf, strlen(buf), 0);//发送响应文件长度
    }
    send(cfd, "\r\n", 2, 0);//发送空行
    return 0;
}
int send_file(int cfd, int resfd)
{
    int ret;
    char buf[256] = { 0x00 };
    do
    {
        ret = read(resfd, buf, sizeof(buf));
        if (ret != -1)//成功读到就发送
        {
            send(cfd, buf, ret, 0);
        }
    } while ((ret == -1 && errno == EINTR) || ret > 0);//被信号打断,重复进行

    return 0;
}

int send_dir(int cfd, DIR* dirfd)
{
    char headHtml[] = { "<!DOCTYPE html>"
        "<html lang = \"en\">"
        "<head>"
        "<meta charset = \"UTF-8\">"
        "<meta name = \"viewport\" content = \"width=device-width, initial-scale=1.0\">"
        "<meta http - equiv = \"X-UA-Compatible\" content = \"ie=edge\">"
        "<title>dir list</title>"
        "</head>"
        "<body bgcolor = \"#99cc99\">"
        "<h1>Index of / </h1>"
        "<ul>" };

    char tailHtml[] = { "</ul>"
        "<h3><a href=\" / \">返回主页</a></h3>"
        "</body>"
        "</html>" };

    send(cfd, headHtml, strlen(headHtml), 0);//发送头

    struct dirent *item;
    while ((item = readdir(dirfd)) != NULL)//循环读目录项
    {
        char li[256] = { 0x00 };// = { "<li><a href = "">< /a>< /li> " }
        if (item->d_type == DT_DIR)//是目录
        {
            sprintf(li, "<li><a href = \"%s/\">%s</a></li>", item->d_name, item->d_name);
        }
        else
        {
            sprintf(li, "<li><a href = \"%s\">%s</a></li>", item->d_name, item->d_name);
        }
        send(cfd, li, strlen(li), 0);//发送li
    }
    send(cfd, tailHtml, strlen(tailHtml), 0);//发送尾
    return 0;
}

void cb_response(EventData* pthis)
{
    char reqLine[512] = { 0x00 };
    int ret = get_line(pthis->fd, reqLine, sizeof(reqLine));//获取第一行
    if (ret <= 0)//读出错,关闭客户端,下树
    {
        close(pthis->fd);//关闭客户端
        struct epoll_event ev;
        ev.data.ptr = pthis;//设置data
        ev.events = pthis->events;
        epoll_ctl(pthis->epfd, EPOLL_CTL_DEL, pthis->fd, &ev);//下树
        free(pthis);//释放data结构体
        if (ret == -1)
        {
            perror("recv err");
        }
        return;
    }

    strdecode(reqLine, reqLine);
    write(STDOUT_FILENO, reqLine, ret);//将读取到的请求行打印

    //读取并丢弃其他行
    char buf[256] = { 0x00 };
    int ret_oth = 0;
    do
    {
        memset(buf, 0x00, sizeof(buf));
        ret_oth = get_line(pthis->fd, buf, sizeof(buf));
        if (ret_oth > 0)
        {
            strdecode(buf, buf);
            write(STDOUT_FILENO, buf, ret_oth);//将读取到的请求内容打印
        }
    } while (ret_oth > 0 || errno == EINTR);
    if (ret_oth <= 0 && errno != EAGAIN)//对端关闭或出错
    {
        close(pthis->fd);//关闭客户端
        struct epoll_event ev;
        ev.data.ptr = pthis;//设置data
        ev.events = pthis->events;
        epoll_ctl(pthis->epfd, EPOLL_CTL_DEL, pthis->fd, &ev);//下树
        free(pthis);//释放data结构体
        if (ret_oth == -1)
        {
            perror("recv err");
        }
        return;
    }

    //解析第一行数据
    char method[8] = { 0x00 };
    char res[256] = { 0x00 };
    sscanf(reqLine, "%s %s", method, res);

    //printf("method:[%s]\nres:[%s]\n", method, res);
    if (strncasecmp(method, "get", 3) == 0)//GET请求
    {
        char *filename;
        if (res[0] == '/')//去掉第一个'/'
        {
            filename = res + 1;
        }

        if (filename[0] == '\0')//如果是空串,直接等于当前路径
        {
            memset(filename, 0x00, sizeof(res));
            strncpy(filename, "./", 2);
        }


        //如何判断文件(目录)是否存在
        struct stat st;
        int ret_stat = stat(filename, &st);
        if (ret_stat != 0)//如果打开失败,返回404
        {
            send_404(pthis->fd);
            perror("stat err");
            return;
        }
        else//可以发送
        {
            int resfd = open(filename, O_RDONLY);
            if (resfd == -1)//无法打开,返回404
            {
                send_404(pthis->fd);
                perror("open err");
                return;
            }
            if (S_ISREG(st.st_mode))//代表普通文件
            {
                //发送头
                send_header(pthis->fd, 200, "OK", get_mime_type(filename), st.st_size);
                //发送内容
                send_file(pthis->fd, resfd);
                close(resfd);
            }
            if (S_ISDIR(st.st_mode))//代表目录文件
            {
                if (strncmp(filename, "..", 2) == 0)//请求webroot的上级目录
                {
                    send_404(pthis->fd);
                    return;
                }
                DIR* resfd = opendir(filename);//打开目录
                if (resfd == NULL)
                {
                    send_404(pthis->fd);
                    return;
                }
                //发送头
                //printf("will send dir\n");
                send_header(pthis->fd, 200, "OK", get_mime_type("dir.html"), -1);
                send_dir(pthis->fd, resfd);//发送目录内容
                closedir(resfd);
            }
        }
    }
    else//其他请求
    {
        //暂不处理
        return;
    }
}

void cb_accept(EventData* pthis)
{
    int cfd = accept(pthis->fd, NULL, NULL);//接受新连接
    if (cfd < 0)
    {
        perror("accept");
        close(cfd);//关闭客户端
        return;
    }

    //将cfd上树
    struct epoll_event ev;
    ev.events = EPOLLIN;//设置events
    ev.data.ptr = malloc(sizeof(EventData));//设置data
    ((EventData*)ev.data.ptr)->fd = cfd;
    ((EventData*)ev.data.ptr)->epfd = pthis->epfd;
    ((EventData*)ev.data.ptr)->events = ev.events;
    ((EventData*)ev.data.ptr)->callback = cb_response;
    epoll_ctl(pthis->epfd, EPOLL_CTL_ADD, cfd, &ev);

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

以上是关于c_cpp Linux的下epoll的模型实现简单的HTTP服务器的主要内容,如果未能解决你的问题,请参考以下文章

Linux高并发机制——epoll模型

Linux中epoll+线程池实现高并发

用Linux / C实现基于自动扩/减容线程池+epoll反应堆检测沉寂用户模型的服务器框架(含源码)

转一篇关于epoll模型的博文

c_cpp Linux的下UDP的Socket的编程示例

c_cpp Linux的下TCP的套接字编程示例