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

news2024/12/26 23:04:49

使用方法:

编译

例子:./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;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1684608.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

LeetCode 79.单词搜索

原题链接&#xff1a;. - 力扣&#xff08;LeetCode&#xff09; 给定一个 m x n 二维字符网格 board 和一个字符串单词 word 。如果 word 存在于网格中&#xff0c;返回 true &#xff1b;否则&#xff0c;返回 false 。 单词必须按照字母顺序&#xff0c;通过相邻的单元格内…

本地centos7+docker+ollama+gpu部署

1、一台有 NVIDIA GPU 驱动的机器 2、Docker CE安装 # 删除旧版本的 Docker&#xff08;如果存在&#xff09; sudo yum remove -y docker docker-common docker-selinux docker-engine # 安装必要的软件包&#xff1a; sudo yum install -y yum-utils device-mapper-persiste…

PyCharm设置——用于调试虚拟环境中的django程序

使用Pycharm新建了一个项目。 项目目录&#xff1a;C:\Users\grace\PycharmProjects\learning_log 在该路径下安装虚拟环境ll_env&#xff0c;并在虚拟环境下安装Django。 为了调试该Django需要对PyCharm进行设置。 1、确保PyCharm使用正确的虚拟环境 打开PyCharm&#xff…

市面上前 11 名的 Android 数据恢复软件

Android数据恢复软件是恢复无意中删除的文件或文件夹的必要工具。该软件还将帮助您恢复丢失或损坏的信息。本文介绍提供数据备份和磁盘克隆选项的程序&#xff0c;这些选项有助于在Android设备上恢复文件的过程。 如果您正在寻找一种有效的方法来恢复图像&#xff0c;文档&…

qmt量化交易策略小白学习笔记第8期【qmt编程之获取股票资金流向数据--内置Python】

qmt编程之获取股票资金流向数据 qmt更加详细的教程方法&#xff0c;会持续慢慢梳理。 也可找寻博主的历史文章&#xff0c;搜索关键词查看解决方案 &#xff01; 感谢关注&#xff0c;需免费开通量化回测与咨询实盘权限&#xff0c;可以和博主联系&#xff01; 获取股票资金…

java学习四

Random 随机数 数组 静态初始化数组 数组在计算机中的基本原理 数组的访问 什么是遍历 数组的动态初始化 动态初始化数组元素默认值规则 Java内存分配介绍 数组在计算机中的执行原理 使用数组时常见的一个问题 案例求数组元素最大值 public class Test1 {public static void ma…

面试八股之MySQL篇5——主从同步原理篇

&#x1f308;hello&#xff0c;你好鸭&#xff0c;我是Ethan&#xff0c;一名不断学习的码农&#xff0c;很高兴你能来阅读。 ✔️目前博客主要更新Java系列、项目案例、计算机必学四件套等。 &#x1f3c3;人生之义&#xff0c;在于追求&#xff0c;不在成败&#xff0c;勤通…

Qt学习记录(14)线程

前言&#xff1a; 我的臀部已经翘到可以顶起一屁股债了 为什么要使用线程 什么时候用线程 复杂的数据处理 头文件.h #ifndef WIDGET_H #define WIDGET_H#include <QWidget> #include <QTimer>//定时器头文件QT_BEGIN_NAMESPACE namespace Ui { class Widget; }…

表现层框架设计之表现层设计模式_3.MVVM模式

1.MVVM模式 MVVM模式正是为解决MVP中UI种类变多&#xff0c;接口也会不断增加的问题而提出的。 MVVM模式全称是模型-视图-视图模型&#xff08;Model-View-ViewModel&#xff09;&#xff0c;它和MVC、MVP类似&#xff0c;主要目的都是为了实现视图和模型的分离&#xff0c;不…

Mac网线连接windows本【局域网互传文件】

Mac网线连接windows本【局域网互传文件】 两台电脑网线互联 Mac->网络->USP TCP/IP 手动配置IP&#xff0c;子网掩码&#xff0c;路由器 windows 网络和Internet配置->更改适配器选项->以太网->Internet协议版本4&#xff08;TCP/IPv4&#xff09;->属性 …

概率论统计——大数定律

大数定律 弱大数定律&#xff08;辛钦大数定律&#xff09; 利用切比雪夫不等式&#xff0c;证明弱大数定律 应用 伯努利大数定理&#xff0c;&#xff08;辛钦大数定理的推论&#xff09; 证明伯努利大数定理 注意&#xff1a;这里将二项分布转化成0,1分布来表示&#xff0c;…

跨境小白shopee被封号的原因?如何有效预防?

提到跨境电商平台&#xff0c;大家都知道亚马逊、Temu、TikTok shop这些是比较大的电商平台。但最近几年&#xff0c;在东南亚市场上&#xff0c;Shopee虾皮却是颇负盛名的一个跨境电商平台&#xff0c;这也让众多中国跨境小白蜂拥而至。目前shopee的商家正在不断增多&#xff…

奥威BI软件能做金蝶ERP以外的数据分析吗?

奥威BI软件能够进行金蝶ERP以外的数据分析。除了金蝶ERP外&#xff0c;奥威BI软件还可以对接用友等主流ERP&#xff1b;支持MS SQL、Oracle、Mysql等主流的关系型数据库&#xff0c;这允许用户直接从这些数据库中导入和分析数据&#xff1b;可以直接上传Excel文件作为数据源。 …

Prompt Engineering Guide

本文转载自&#xff1a;Prompt Engineering Guide https://www.promptingguide.ai/zh/introduction/basics 文章目录 提示工程简介1、基本概念1&#xff09;基础提示词2&#xff09;提示词格式 2、提示词要素3、设计提示的通用技巧从简单开始指令具体性避免不精确做还是不做&am…

编译aosp刷入pixel 真机得问题记录

编译aosp要做什么&#xff08;ubuntu下编译问题相对少&#xff09; 需要vmware并且已经安装了ubuntu镜像系统 直接切换到root 避免后期避免麻烦 参考地址 https://mp.weixin.qq.com/s/yJp3ijIxykiMmNVYr2V1nQ apt install git //安装git sudo apt install git//给git设置用户…

CDN用户平台安装说明

CDN用户平台安装说明 登录管理员系统 在”系统设置” – “高级设置” – “用户节点”中点击”添加节点” 如果所示&#xff1a; 节点名称 - 可以任意填写 进程监听端口 - 启动用户节点后&#xff0c;进程所监听的端口&#xff0c;通常是HTTP 80或者HTTPS 443&#xff0c;…

Django中使用Celery(通用方案、官方方案)

Django中使用Celery&#xff08;通用方案、官方方案&#xff09; 目录 Django中使用Celery&#xff08;通用方案、官方方案&#xff09;通用方案场景前置准备完整代码 Celery官方方案【1】注册celery配置【2】创建celery文件【3】init注册【4】添加任务【5】启动worker异步任务…

网络协议——Modbus-TCP

目录 1、简介 2、Modbus-TCP与Modbus-RTU的区别 3、消息格式 4、功能码01H 5、功能码02H 6、功能码03H 7、功能码04H 8、功能码05H 9、功能码06H 10、功能码0FH 11、功能码10H 1、简介 Modbus-TCP&#xff08;Modbus Transmission Control Protocol&#xff09;是一…

clickhouse 中的数组(array)和元组(Tuple)—— clickhouse 基础篇(二)

文章目录 数组判断是否为空计算数组长度获取数组元素判断某个元素是否存在数组切片数组元素展开数组元素去重删除连续重复元素连接多个数组数组倒序数组拍平数组元素映射数组元素过滤数组聚合分析计算数组交集计算数组并集计算数组差集SQL 子查询进行集合操作 元组创建元组获取…

我的心情JSP+Servlet+JDBC+MySQL

系统概述 本系统采用JSPServletJDBCMySQL技术进行开发&#xff0c;包括查看我的心情列表&#xff0c; 编辑我的心情信息、新增我的心情。使用方法 将项目从idea中导入&#xff0c;然后配置项目的结构&#xff0c;包括jdk,库&#xff0c;模块&#xff0c;项目&#xff0c;工件…