C++实现基于http协议的epoll非阻塞模型的web服务器框架(支持访问服务器目录下文件的解析)

使用方法:

编译

例子:./httpserver 9999 ../ htmltest/

可执行文件  +端口 +要访问的目录下的

例子:http://192.168.88.130:9999/luffy.html

前提概要

http协议 :应用层协议,用于网络通信,封装要传输的数据,通过http协议组织的数据最终会是一个数据块多行数据,换行需要 \r\n

通信流程:

客户端:通过使用http传输数据发送给服务器

  通过http协议组织数据→得到一个字符串→发送给服务器

  接受数据→根据http协议解析→得到原始数据→处理

服务器端:

  接受数据→通过http协议解析→得到原始数据→处理

  回复数据→通过http协议组织数据→得到一个字符串→发送给客户端

http协议分成两部分:

http请求:

客户端发送给服务器的一种数据格式

http响应:

服务器端回复客户端的一种格式

http请求

客户端给服务器发送的一种数据格式,可以分为四部分

1.请求行 

  指定提交数据的方式

  有两种提交的方式:

    **get**:简单 

    **post**:复杂

2. 请求头 

  多个键值对

  客户端给服务器发送的身份的描述符

3. 空行 

4.请求的数据向服务器提交的数据
这是网页用GET 发过来的请求

第一行:请求行用的GET

第一部分:GET :提交的数据的方式

第二部分:中间的橙色的字符

/ :访问的服务器资源目录,/ →代表资源根目录

? :后面的内容:客户端向服务器端提交的数据

  key=value

第三部分 :HTTP/1.1 →http协议的版本

第二-第八行: 请求头

若干个键值对,每一个键值对占一行,使用\r\n换行

第九行是:空行

用post请求

第一行:请求行

post:提交数据的方式

/:作为客户端访问了服务器的什么目录,资源的根目录

http 、1.1http协议的版本

第二行-12请求头

第13行:空行

第14 行:客户端向服务器提交的数据

GET与POST的区别

功能上:

get

作为客户端向服务器申请访问静态资源(网页,图片,文件)

post :

向服务器提交动态数据

  用户登录信息

  上传下载文件

从操作的数据量来说:

get:

比较少,使用get向服务器提交的数据在请求行的第二部分

在请求第二部分的时候需要显示到浏览器的地址栏中

浏览器的地址栏的缓存很小,谷歌默认7k左右,数据量小

post :

可以操作大数据

  文件上传(大文件)

post 提交数据放到了请求协议的第四部分

安全性:

get :

提交的数据会显示到浏览器的地址栏中,容易泄露

post :

不会泄露,提交数据不再浏览器的地址栏中

http响应

服务器给客户端回复数据

http响应的组成部分→4个部分

状态行

响应头(包头)

n个键值对

里面的信息是服务器发送给客户端

空行

响应的数据,根据客户端请求给客户端回复的数据

第一行 :状态行

HTTP 、1.1 http协议版本

200:状态码

ok :对应状态码的描述

第二-九行 :响应头

content-type :服务器给客户端的数据快的格式==http协议的第四块的数据格式

text、plain→纯文本

charset =iso-8859-1→数据的字符编码

  iso-8859-1→不支持中文

  utf 支持中文

content-length :服务器给客户端的数据快的长度==http协议的第四块的数据块的长度,总字节数;不知道写-1;

http状态码:

3.web服务器实现

 客户端:浏览器

通过浏览器访问服务器: -访问方式:

服务器的IP地址:端口 应用层协议使用:http,数据需要在浏览器端使用该协议进行包装响应消息的处理也是浏览器完成的 => 程序猿不需要管-客户端通过ur1访问服务器资源

-客户端访问的路径:http://192.168.1.100:8989/或者http://192.168.1.100:8989

**[访问服务器提供的资源目录的根目录](http://192.168.1.100:8989/或者http://192.168.1.100:8989访问服务器提供的资源目录的根目录)**

并不是服务器的 / 目录

#### 服务器端:

提供服务器,让客户端访问

支持多客户端访问

-使用I0多路转接=>epo11

客户端发送给的请求消息是基于http的 -需要能够解析http请求 服务器回复客户端数据,使用http协议封装回复的数据=>http响应

服务器端需要提供一个资源目录,目录中的文件可以供客户端访问

客户端访问的文件没有在资源目录中,就不能访问了

假设服务器端提供的目录:/home/robin/luffy

 代码展示

main()函数

/*************************************************************************
    > File Name: main.cpp
    > Author:Wux1aoyu
    >  
    > Created Time: Fri 17 May 2024 05:02:16 AM PDT
 ************************************************************************/

#include"sever.h"
using namespace std;
// 原则上 main 函数只是逻辑函数调用,具体的内容不会写在这里面
//代码量少
int main(int argc,char *argv[]){
    //启动服务器->epoll
    if(argc<3){
        cout<<"./a.out port path\n"<<endl;
        exit(0);
    }
    //argv[2]是path的路径 
    //将进程进入到当前的目录相当于cd
    chdir(argv[2]);
    //启动服务器 -》基于epoll ET 非阻塞
    unsigned short port=atoi(argv[1]);// ./后面的参数
    epollrun(port);
}

头文件


#ifndef SERVER_H
#define SERVER_H

#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <arpa/inet.h>
#include <unistd.h>
#include <cstring>
#include <pthread.h>
#include <sys/epoll.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/stat.h>
#include <strings.h>
#include<dirent.h>

using namespace std;

#ifdef __cplusplus
extern "C" {
#endif

// 初始化监听的文件描述符
int initlistenFd(unsigned short port);

// 启动 epoll 模型
int epollrun(unsigned short port);

// 建立新连接
int acceptConn(int lfd, int epfd);

// 接收 HTTP 请求
int recvHttprequest(int cfd, int epfd);

// 解析请求行
int parserequestline(const char *requline, int cfd);

// 发送头信息
int sendHeadmsg(int cfd, int status, const char *descr, const char *type, int length);

//发送目录
int senddir(int cfd,char*dirname);

// 发送文件
int sendFile(int cfd, const char *file);

// 断开连接
int disconnect(int cfd, int epfd);


#ifdef __cplusplus
}
#endif

#endif // SERVER_H

 服务器端: sever.cpp

#include"sever.h"
//初始化监听套接字
int initlistenFd(unsigned short port)
{
    //1.创建监听的套接字
    int lfd=socket(AF_INET,SOCK_STREAM,0);
    if(lfd==-1){
        perror("socket");
        return -1;
    }

    //2. 端口复用
    //如果服务器主动断开链接,那么将会进入TIME_WAIT 状态,等待2msl,这个时间太长了,所以就设置端口复用,继续使用端口复用,使客户端用这个端口链接,但是上一个仍处于TIME_WAIT 
    int opt=1;
    int ret = setsockopt(lfd,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));
    if(ret==-1){
        perror("ret");
    }

    //3.绑定
    //设置文件描述符的地址ip端口
    struct sockaddr_in addr;
    addr.sin_family=AF_INET;//IPV4
    addr.sin_port=htons(port);
    addr.sin_addr.s_addr=INADDR_ANY; //0地址
    
    ret=bind(lfd,(sockaddr*)&addr,sizeof(addr));
    if(ret==-1){
        perror("bind");
        return -1;
    }

    //4.设置监听
    ret=listen(lfd,128);
     if(ret==-1){
        perror("listen");
        return -1;
    }
    
    //5.返回可用的监听的套接字
    return lfd;

}

//启动epoll模型
int epollrun(unsigned short port){
    //初始化epoll模型
    int epfd=epoll_create(1000);//创建epoll树
    if(epfd==-1){
        perror("create");
        return -1;
    }

    //初始化epoll树,将监听lfd添加上树
    int lfd=initlistenFd(port);

    struct epoll_event ev;//事件结构体
    ev.events=EPOLLIN;//检查读事件
    ev.data.fd=lfd;//将lfd添加属性中
    //添加上树
    int ret=epoll_ctl(epfd,EPOLL_CTL_ADD,lfd,&ev);
    if(ret==-1){
        perror("epoll_ctl-add");
        return -1;
    }

    //检测,循环检测,边沿ET模式,epoll非阻塞
    struct epoll_event evs[1024];
    int size=sizeof(evs)/sizeof(int);
    int flag =0;
    while (1)
    {
        if(flag==1){
            break;
        }
        int num=epoll_wait(epfd,evs,size,0);//非阻塞进行
        //遍历发生可读事件的变化的数组
        for (int  i = 0; i < num; i++)
        {
            int curfd=evs[i].data.fd;//临时变量找到变化的文件描述符
            if(curfd==lfd)//如果使监听套接字发生变化,一定是客户端请求链接
            {
                //建立链接
              int ret= acceptConn(curfd,epfd);
              if(ret==-1){
                //建立链接失败直接终止程序
                flag=1;
                break;
              }

            }
            else{
                //通信//接受http请求
                recvHttprequest(curfd,epfd);
            }
            
        }
        
    }
    


    return 0;
}

//和客户端建立新连接,并且将通信文件描述符设置成非阻塞属性
int acceptConn(int lfd,int epfd){

    //建立链接
    int cfd=accept(lfd,NULL,NULL);
    if(cfd==-1){
        perror("accept");
        return -1;
    }

    //设置通信文案描述属性为非阻塞
    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");
        return -1;
    }

}

//和客户端断开新链接
int disconnect(int cfd,int epfd){

    //将节点从epoll模型删除
    int ret =epoll_ctl(epfd,EPOLL_CTL_DEL,cfd,NULL);//删除操作最后一个制空
    if(ret==-1){
        perror("epoll_ctl_del");
        return -1;
    }
    //关闭通信套接字
    close(cfd);
    return 0;
}
//接受客户端http的请求消息
int recvHttprequest(int cfd,int epfd){
    //因为是边沿非阻塞模型,所以要一次性循环读

    char tmp[1024];//每次读1k数据
    char buf[4096];//每次把读的数据存到这个缓冲区里面
    //循环读数据
    int len,total=0;//total 是当前的buf的数据
    //客户端申请的都是静态资源,请求的资源内容,在请求行的第二部分
    //只需将请求完整的保存下来就可以
    //不需要解析请求头的数据,因此接受到之后不储存也是没问题的

    while((len=recv(cfd,tmp,sizeof(tmp),0))>0){
        if(len+total<sizeof(buf))//说明接受的和当前的还没超过缓冲区的大小
        {
            //有空间储存数据
            memcpy(buf+total,tmp,len);//从当前的数据往后加
        }
        total+=len;//当前的缓冲区的容量;
        buf[total] = '\0';
    }

    //循环结束了,说明读完了
    //非阻塞,缓存没有数据,返回-1,返回错误号
    if(len==-1&&errno==EAGAIN){
     //将请求行从接收的数据中拿出来 (http协议中他分了很多行,我们要拿第一行)
     //找到 \r\n就可以找到第一行
    char*pt= strstr(buf,"\r\n");//找到了\r\n之前的请求行
    int reqlen=pt-buf;//\r\n   的位置-首地址的位置
    //保留请求行
    buf[reqlen]='\0';//截断了
    //此时buf里面存在的是http的请求行的内容

     //解析请求行
    parserequestline(buf,cfd);
    }
    else if(len==0){
        cout<<"客户端断开连接了....."<<endl;
        //服务器和客户端也断开,cfd,从epoll删除文件描述符
        disconnect(cfd,epfd);
    }
    else{
        perror("recv");
        return -1;
    }
    return 0;

}

//解析请求行
int parserequestline(const char *requline,int cfd){
    //请求行分为三部分
    //GET /HELLO/WORLD/HTTP/1.1

    //1.拆分请求行,有用的是前两部分
    //提交数据的方式
    //客户端向服务器请求的文件名

    //拆分用正则表达式 sscanf
    char method[5]; //POST GET 
    char path[1024]; //存储的是目录文件地址
    sscanf(requline,"%[^ ] %[^ ]",method,path);

    //2. 判断请求的方式是不是get' ,不是get 直接忽略
    if(strcasecmp(method,"get")!=0){
        cout<<"用户提交不是get请求"<<endl;
        return -1;
    }

    //3. 判断用户访问的是文件还是目录
    // /HELLO/WORLD/ ,判断是不是  用stat
    char *file=NULL;
    if(strcmp(path,"/")==0){ //就是比较是不是/
        file="./";
    }
    else{
        file=path+1; //"./" +1 就是从h开始的
        //   hello/a.txt == ./hello/a.txt 这个目录等价   加.比较麻烦,如果什么都不加,就是从根目录找了
    }

    //属性判断 是不是文件或者目录
    struct stat st;//传出参数
    int ret=stat(file,&st);
    if(ret==-1){
        //判断失败
        //无文件发送404给客户端
        sendHeadmsg(cfd,404,"not found","text/html",-1);
        sendFile(cfd,"404.html");
    }
    if(S_ISDIR(st.st_mode)){
        //如果是目录的话将目录内容发送给客户端
    }
    else{
        //如果是普通文件,发送文件,把头信息发出去
        sendHeadmsg(cfd,200,"ok","text/html",st.st_size); //这里我们默认传输html文件
        sendFile(cfd,file);

    }

    return 0;
}

//发送头信息
int sendHeadmsg(int cfd,int status,const char *descr,const char*type,int length){
    //状态行 +消息包头 +空行
    char buf[4096];
    //http/1.1 200 ok
    sprintf(buf,"http/1.1 %d %s\r\n",status,descr);
    //消息包头 ->这里只需两个键值对
    //content-type /content-length   https://tool.oschina.net/commons去这里查
     sprintf(buf + strlen(buf), "Content-Type: %s\r\n", type);
    sprintf(buf + strlen(buf), "Content-Length: %d\r\n\r\n", length);

    // 空行
    //拼接完成之后发送
    send(cfd,buf,strlen(buf),0);//非阻塞


    return 0;

}

int sendFile(int cfd,const char *file){

    //读文件,发送给客户端
    //在发送内容之前应该有状态+消息包头,+空行+文件内容
    //这四部分数据组织好之后再发送数据吗?
    //不是 为什么,因为传输层默认人是tcp的
    //面向连接的流式传输协议-》只有最后全部发送完就可以

    int fd=open(file,O_RDONLY);//只读
    while (1)
    {
        char buf[1024];
        int len=read(fd,buf,sizeof(buf));
        if(len>0){
            //发送读出的数据
            send(cfd,buf,len,0);
        }
        else if(len==0){
            //文件读完了
            break;
        }
        else{
            perror("read");
            return -1;
        }
        
    }


    return 0;
}

  • 13
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值