C语言写的超详细的wb服务器(基于linux)

wb服务器

  • 想法:用浏览器访问某个目录里面的内容,比如图片,音乐,文本…
    在这里插入图片描述
    效果展示:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
这是服务器要接收的http协议请求,下面那个是服务器要发送给客户端的http请求格式。

在这里插入图片描述

代码:

#include<iostream>
#include<stdlib.h>
#include<unistd.h>
#include<stdio.h>
#include<error.h>
#include<sys/epoll.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<string.h>
#include <arpa/inet.h>
#include<sys/epoll.h>
#include<fcntl.h>
#include<sys/stat.h>
#include<errno.h>
#include<dirent.h>

using std::cout;
using std::endl;

void eopll_run(int port);
int init_listenfd(int port, int efd);   //初始化连接
void do_accept(int lfd, int efd);       //进行连接
void do_read(int cfd, int efd);     //读数据
int get_line(int cfd, char *buf, int size);        //读取http协议头的一行的部分,是以\r\n结尾的
 void http_file(int cfd ,char *file);        //处理http请求,检查文件是否存在
void send_http(int cfd, int flag, const char *dircp, const char *type, int len);    //回发http协议头
void send_data(int cfd, char *file);       //回发请求的数据
const char *get_file_type(char *filename);           //获取文件类型
void send_error(int cfd, int flag, const char *dircp, const char *text);  //发送错误html界面
void send_Catalogue(int cfd, char *file);       //发送目录信息
int hex2dec(char c);
char dec2hex(short int c);
void urlencode(char *url);  //编码
void urldecode(char* url);      //解码




int main(int argc, char **argv)
{
    if(argc < 3)
    {
        cout << "需要传递三个三个参数,例如:\n ./server   port  path" << endl;
    }

    int port = atoi(argv[1]);           //将端口号转换成int型

    //改变当前的工作目录,服务器所要共享文件的目录,
    //例如:/home/web_server/     这个目录里面的东西是你想要共享的
    int ret = chdir(argv[2]);
    if(ret != 0)
    {
        perror("chdir error");
        exit(1);
    }

    //启动eopll监听
        eopll_run(port);

    return 0;
}

void eopll_run(int port)
{
    struct epoll_event all_events[2048];        //用于epoll_wait传出

    //创建监听树
    int efd = epoll_create(2048);
    if(efd == -1)
    {
        perror("epoll_creat error");
        exit(1);
    }

    cout << "等待客户端的连接..." <<endl;
    //创建lfd连接监听,并添加到监听树
    int lfd = init_listenfd(port, efd );

    while(1)
    {
        //监听节点对应的事件
        int ret = epoll_wait(efd, all_events, 2048, -1);
        if(ret == -1)
        {
            perror("epoll_wait error");
            exit(1);
        }

        for(int 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, efd);
            }
            else{               //否则就说明是读事件,进行读数据
                do_read(pev->data.fd, efd);
            }

        }
        
    }
}

int init_listenfd(int port, int efd)
{
    //创建监听套接字lfd
    int lfd = socket(AF_INET,SOCK_STREAM, 0 );
    if(lfd == -1)
    {
        perror("socket error");
        exit(1);
    }

    //创建服务器的地址结构 IP和端口
    struct sockaddr_in ser;
    //初始化地址结构
    bzero(&ser, sizeof(ser));       //将结构体置为0 
    ser.sin_family = AF_INET;
    ser.sin_port = htons(port);         //端口赋值
    ser.sin_addr.s_addr = htonl(INADDR_ANY);    //IP设为任一值

    //端口复用
    int opt = 1;
    setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
     
     //给lfd 绑定地址结构
     int ret = bind(lfd, (struct sockaddr *)&ser, sizeof(ser));
     if(ret == -1)
     {
        perror("bind error");
        exit(1);
     }

     //设置监听上限
     ret = listen(lfd , 1024);
     if(ret == -1)
     {
        perror("listen error");
        exit(1);
     }

    //借助结构体将lfd 添加到epoll树上
    struct epoll_event ev;
    ev.data.fd = lfd;
    ev.events = EPOLLIN;
    ret = epoll_ctl(efd, EPOLL_CTL_ADD, lfd, &ev);
    if(ret == -1)
    {
        perror("epoll_ctl add lfd error");
        exit(1);
    }

    return lfd;
}

void do_accept(int lfd, int efd)
{
    //创建客户端的地址结构
    struct sockaddr_in c_addr;
    socklen_t socklen = sizeof(c_addr);

    //进行服务器与客户端的连接
    int cfd = accept(lfd, (struct sockaddr *)&c_addr, &socklen);
    if(cfd == -1)
    {
        perror("accept error");
        exit(1);
    }

    //打印客户端的IP和端口
    char client_ip[64];
    cout << "新连接的客户端的IP:" << inet_ntop(AF_INET, &c_addr.sin_addr.s_addr, client_ip, sizeof(client_ip)) 
    << ", 端口:" << ntohs(c_addr.sin_port) << endl;

    //设置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(efd, EPOLL_CTL_ADD, cfd, &ev);
    if(ret == -1)
    {
        perror("epoll_ctl add cfd error");
        exit(1);
    }
}

void do_read(int cfd, int efd)
{
    // 读取http协议, 获取GET  文件名  协议号
    char line[4096] = {0};

    int len = get_line(cfd, line, sizeof(line));    //读取http协议最主要的开头行
    if(len == 0)
    {
        cout << "服务器检测到客户端已经关闭..." << endl;
        epoll_ctl(efd, EPOLL_CTL_DEL, cfd, 0);      //从监听树上摘除
        close(cfd); 
    }
    else
    {
        char mothod[16], path[128], httpnum[16];
        sscanf(line, "%[^ ] %[^ ] %[^ ]", mothod, path, httpnum);       //用正则表达式将协议头拆分成三部分
         urldecode(path);
        cout << "请求:" << mothod << ", 路径名:" << path << ",协议版本号:"<< httpnum << endl;

        //读取出来的路径名可能是中文,这样的话就会导致乱码,所以要将它进行解码操作
       

        while(1)        //接着循环读取协议其他内容
        {
            char buf[1024] = {0};
            len = get_line(cfd, buf, sizeof(buf));
            if(len == '\n'){
                break;
            }else if(len == -1)   {
                break;
            }
        }
   
        if(strncasecmp(mothod, "GET", 3) == 0)       //检查是否是GET请求
        {
            http_file(cfd, path);
            int ret = epoll_ctl(efd, EPOLL_CTL_DEL, cfd, NULL);     //完成一次通信,服务器就会断开连接
            //从监听树上摘下,关闭文件描述符
            if(ret == -1)
            {
                perror("epoll_ctl  del cfd error");
                exit(1);
            }
            close(cfd);
        }
    }
}

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);     //MSG_PEEK  读取下一个字符,但不会把上一个字符替换掉,相当于只是查看
                if((n > 0) && (c == '\n'))
                {
                    recv(cfd, &c, 1, 0);        //这里才是正式读取‘\n’
                }
                else
                    c = '\n';
            }
             buf[i] = c;
             i++;
        }
        else
        c = '\n';
    }
    buf[i] = '\0';

    if(-1 == n)
        i = n;
    
    return i;
}

 //处理http请求,检查文件是否存在
 void http_file(int cfd,char *path)
 {
        //将路径中的 / 去掉,获取文件名
        char *file = path + 1;
        if(strcmp(path, "/") == 0){     //如果是目录,就将文件名赋值为当前目录
            file = "./";
        }

        struct stat sbuf;       //这是一个判断文件是否存在所用到的结构体
        //开始判断
        int ret = stat(file, &sbuf);
        if(ret != 0)    //不存在
        {
           send_error(cfd, 404, "Not Found", "The file does not exist");
           return;
        //    回发浏览器 404 html界面
        }

        if(S_ISDIR(sbuf.st_mode))       //是否是目录
        {
            //发送头文件信息,显示html网页,点击超链接进入这个目录
            send_http(cfd, 200, "OK", get_file_type(".html"), -1);
            //发送目录信息
            send_Catalogue(cfd, file);
        }
        else if(S_ISREG(sbuf.st_mode))       //是否是普通文件
        {
            //回发http协议应答
            //send_http(cfd, 200, "OK",  " Content-Type: text/plain; charset=iso-8859-1", sbuf.st_size);
            send_http(cfd, 200, "OK",  get_file_type(file), sbuf.st_size);
            send_data(cfd, file);
        }
 }


 //回发http协议头
 //   cfd 客户端cfd     flag  错误号     dircp 错误描述    type  回发文件类型   len  文件长度
 void send_http(int cfd, int flag,  const char *dircp,const  char *type, int len)
 {
    //对http协议进行封装发送
    char buf[1024] = {0};
    sprintf(buf, "HTTP/1.1 %d %s\r\n", flag, dircp);
    sprintf(buf + strlen(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_data(int cfd, char *file)
{
    int n = 0;
    char buf[4000000];
    cout << file << endl;

    //打开服务器本地文件
    int fd = open(file, O_RDONLY);
    if(fd == -1)
    {
        send_error(cfd, 404, "Not Found", "File opening failure");  //打开失败
        return;
        //404错误界面
    }
    cout << "打开了" << file<< endl;
    while((n = read(fd, buf, sizeof(buf)))> 0)  //读取打开文件的数据
    {
        int ret = send(cfd, buf, n, 0);     //发送读取出来的数据给客户端
        if(ret == -1)
        {
            if(errno == EAGAIN)
                continue;
            else if(errno == EINTR)     //对错误类型进行判断,如果是这种类型就返回重新发送
                continue;
            else    {
                perror("send error");
                exit(1);
            }
        }
        if (ret < 4000000)
            cout << "---send ret : " << ret << endl;
    }
    
    close(fd);
}
 

 const char* get_file_type(char *filename)
 {
    char *sign;
     //从左到右查找字符'.',返回该字符出现的位置,用指针接收,就可以找到文件的后缀名,也就是文件类型了
    sign = strrchr(filename,  '.');     
    
    if(sign == NULL)
        return "text/plain; charset=utf-8";     //不知道的类型就全部设为文本类型

     if(strcmp(sign, ".html") == 0 || strcmp(sign, ".htx") == 0 || strcmp(sign, ".htm") == 0)
        return "text/html; charset=utf-8";  //HTML格式
    
     if(strcmp(sign, ".jpg") == 0 || strcmp(sign, ".jpeg") == 0 )
        return "image/jpeg";            //jpg图片格式        
    
    if(strcmp(sign, ".gif") == 0 )
        return "image/gif";         //gif图片格式

    if(strcmp(sign, ".png") == 0 )
        return "image/png";         //png 图片格式

    if(strcmp(sign, ".ico") == 0 )
        return "image/x-icon";      //图标      

     if(strcmp(sign, ".mp3") == 0 )
        return "audio/mp3";         //mp3音频格式
        
     return "text/plain; charset=utf-8";  //如果都不是,就返回纯文本格式

     //还有很多格式,大家可以自己去扩展一下
 }

    //          客户端描述符  错误码   错误描述    HTML文本描述 
void send_error(int cfd, int flag,const char *dircp, const char *text)
{
    send_http(cfd, flag, dircp, "text/html", -1);       //调用上面的函数发送协议头

    //html返回错误网页文本发送
    char buf[4096] = {0};
    sprintf(buf, "<html><head><title>%d %s</title></head>\n",flag, dircp);
    sprintf(buf + strlen(buf),"<body><div align=center><h1>%d %s</h1></div>\n", flag, dircp);
    sprintf(buf + strlen(buf), "<hr>\n<div align=center><font>%s</font></div>\n", text);
    sprintf(buf+ strlen(buf), "</body></html>");
    send(cfd, buf, strlen(buf), 0);
}


//发送目录信息
void send_Catalogue(int cfd, char *dirname)
{
    char buf[4096];
    //先写一个html界面
    sprintf(buf, "<html><head><title>目录名:%s</title></head>", dirname);
    sprintf(buf + strlen(buf), "<body><h1>当前目录: %s</h1><table>", dirname);

    char str[1024] = {0};
    char path[1024] = {0};

    //目录项的二级指针,就是一个二维数组
    struct dirent **ptr;
    int num = scandir(dirname, &ptr, NULL, alphasort);  //alphasort进行排列
    //这个函数就是将当前目录的内容储存在ptr这个二维数组中,返回值是该目录下有多少个文件或者目录

    //遍历得到每一个文件或者目录
    for(int i = 0; i < num; i++)
    {
        char * name = ptr[i]->d_name;    //文件名

        //拼接完成的文件路径
        sprintf(path, "%s/%s", dirname, name);
        
        //再次判断是文件还是目录
        struct stat st;
        stat(path, &st);

        strcpy(str, name);

        //对得到的目录或者文件进行编码
        //因为浏览器只识别URL编码
        urlencode(name);

        //如果是文件
        if(S_ISREG(st.st_mode))
            sprintf(buf + strlen(buf),"<tr><td><a href=\"%s\">%s</a></td></tr>", name, str);//html超链接
        else if(S_ISDIR(st.st_mode))        //如果是目录
            sprintf(buf + strlen(buf), "<tr><td><a href=\"%s/\">%s/</a></td></tr>", name, str);

        int ret = send(cfd, buf, strlen(buf), 0);
        if(ret == -1)
        {
            if(errno == EAGAIN)
                continue;
            else if(errno == EINTR)     //对错误类型进行判断,如果是这种类型就返回重新发送
                continue;
            else    {
                perror("send error");
                exit(1);
            }
        }

        memset(buf, 0, sizeof(buf));
    }
    sprintf(buf + strlen(buf), "</table></body></html>");
    send(cfd, buf, sizeof(buf),0);

    cout << "send OK!"<<endl;
}


//======编码和解码操作===============
int hex2dec(char c)
{
    if ('0' <= c && c <= '9') 
    {
        return c - '0';
    } 
    else if ('a' <= c && c <= 'f')
    {
        return c - 'a' + 10;
    } 
    else if ('A' <= c && c <= 'F')
    {
        return c - 'A' + 10;
    } 
    else 
    {
        return -1;
    }
}

char dec2hex(short int c)
{
    if (0 <= c && c <= 9) 
    {
        return c + '0';
    } 
    else if (10 <= c && c <= 15) 
    {
        return c + 'A' - 10;
    } 
    else 
    {
        return -1;
    }
}
//编码一个url
void urlencode(char *url)
{
    int i = 0;
    int len = strlen(url);
    int res_len = 0;
    char res[2048];
    for (i = 0; i < len; ++i) 
    {
        char c = url[i];
        if (    ('0' <= c && c <= '9') ||
                ('a' <= c && c <= 'z') ||
                ('A' <= c && c <= 'Z') || 
                c == '/' || c == '.') 
        {
            res[res_len++] = c;
        } 
        else 
        {
            int j = (short int)c;
            if (j < 0)
                j += 256;
            int i1, i0;
            i1 = j / 16;
            i0 = j - i1 * 16;
            res[res_len++] = '%';
            res[res_len++] = dec2hex(i1);
            res[res_len++] = dec2hex(i0);
        }
    }
    res[res_len] = '\0';
    strcpy(url, res);
}

// 解码url
void urldecode(char *url)
{
    int i = 0;
    int len = strlen(url);
    int res_len = 0;
    char res[2048];
    for (i = 0; i < len; ++i) 
    {
        char c = url[i];
        if (c != '%') 
        {
            res[res_len++] = c;
        }
        else 
        {
            char c1 = url[++i];
            char c0 = url[++i];
            int num = 0;
            num = hex2dec(c1) * 16 + hex2dec(c0);
            res[res_len++] = num;
        }
    }
    res[res_len] = '\0';
    strcpy(url, res);
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小白学编程*

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值