自主Web服务器Http_Server

Posted 谜一样的男人1

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了自主Web服务器Http_Server相关的知识,希望对你有一定的参考价值。

目录

自主web服务器

背景

http协议被广泛使用,从移动端,pc端浏览器,http协议无疑是打开互联网应用窗口的重要协议,http在网络应用层中的地位不可撼动,是能准确区分前后台的重要协议。

目标

在对http协议的理论学习的基础上,从零开始完成web服务器开发,坐拥下三层协议,从技术到应用,让网络难点无处遁形。

描述

采用C/S模型,编写支持中小型应用的http,并结合mysql,理解常见互联网应用行为,做完该项目,你可以从技术上 完全理解从你上网开始,到关闭浏览器的所有操作中的技术细节!

技术特点

  • 网络编程(TCP/IP协议, socket流式套接字,http协议)
  • 多线程技术
  • cgi技术
  • 线程池

项目定位

研发岗

  • 开发环境 centos 7 + vim/gcc/gdb + C/C++;

项目实现过程

由于我们编写的是HTTP_SERVER,因此我们只需要编写s端,c端我们使用浏览器进行访问即可;

我们需要对**应用层(主要)**和传输层进行代码编写,网络层及一下,会有对应的TCP/IP协议来保证数据的交互;

下图表示短连接下,C端发起请求,S端响应请求,一来一回 之后关闭sock;

创建HttpServer基础框架

先创建一个能接收到浏览器HTTP报文的socket框架;

TcpServer.hpp

这里将TcpServer中的socker,bind,listen进行了封装,用Init启动,同时设计了单例模式,一个HttpServer只需要一个监听listen_sock即可!

#pragma once

#include <iostream>
#include <cstdlib>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <pthread.h>
#include "Log.hpp"
using std::cout;
using std::endl;
#define BACKLOG 5
enum ERR

    SOCK_ERR = 1,
    BIND_ERR,
    LISTEN_ERR,
    USAGE
;
class TcpServer

private:
    int port;
    int listen_sock;
    static TcpServer* svr;
private:                             //单例模式
TcpServer(int _port):port(_port)  //私有构造
      
    
    TcpServer(const TcpServer &s) //私有拷贝构造
    
    

public:
    static TcpServer *getinstance(int port)//单例模式
    
        static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
        if (nullptr == svr)
        
            pthread_mutex_lock(&lock);
            if (nullptr == svr)
            
                svr = new TcpServer(port);
                svr -> InitServer();//getinstance的时候就搞定了sock bind listen了;
            
            pthread_mutex_unlock(&lock);
        
        return svr;
    

public:
    void InitServer()
    
        Socket();
        Bind();
        Listen();
         
        LOG(INFO, "TcpServer begin");//日志
    
    void Socket()
    
        listen_sock = socket(AF_INET, SOCK_STREAM, 0);
        if (listen_sock < 0)
        
            LOG(FATAL, "socket error");
            exit(SOCK_ERR);
        
        //防止bind error
        int opt = 1;
        setsockopt(listen_sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
    
    void Bind()
    
        sockaddr_in local;
        bzero(&local, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(port);
        local.sin_addr.s_addr = INADDR_ANY;//云服务器这样绑

        if (bind(listen_sock, (sockaddr *)&local, sizeof(local)) < 0)
        
            LOG(FATAL, "bind error");
            exit(BIND_ERR);
        
    
    void Listen()
    
        if (listen(listen_sock, BACKLOG) < 0)
        
            LOG(FATAL, "listen error");
            exit(LISTEN_ERR);
        
    
    int Sock()
    
        return listen_sock;
    
    ~TcpServer()
    
        if (listen_sock > 0)
            close(listen_sock);
    
;
//单例
TcpServer *TcpServer::svr = nullptr;

HttpServer.hpp

#pragma once

#include <iostream>
#include <signal.h>
#include <pthread.h>
#include "Log.hpp"
#include "TcpServer.hpp"
#include "Protocol.hpp"

#define PORT 8080//默认端口号

class HttpServer

private:
    int port;
    bool stop;

public:
    HttpServer(int _port = PORT) : port(_port), stop(false)
    
    
    void InitServer()
    
        // singal(SIGPIPE,SIG_IGN);
    
    void Loop()//循环监听c端逻辑
    
        TcpServer *tsvr = TcpServer::getinstance(port); // TcpServer里面就处理了,sock bind listen TcpServer里面就处理了

        LOG(INFO, "Loop Begin");

        while (!stop)
        

            sockaddr_in peer;
            socklen_t len = sizeof(peer);

            int sock = accept(tsvr->Sock(), (sockaddr *)&peer, &len);
            if (sock < 0)
                continue;

            LOG(INFO, "Get a new link"); //到这里 httpserver整体就能接收新连接了!

            //创建handler线程,将连接的sock甩进去,再loop循环以后的c端链接
            pthread_t tid;
            int *psock = new int(sock);//注意局部变量的传参
            pthread_create(&tid,nullptr,Entrance::HandlerRequest,psock);
            pthread_detach(tid);
        
    
    ~HttpServer() 
;

Log.hpp

建议的日志系统

#pragma once

#include <iostream>
#include <string>
#include <ctime>

//日志处理
#define INFO    
#define WARNING  
#define ERROR   
#define FATAL   

#define LOG(level, message) Log(#level, message, __FILE__, __LINE__)//替换下列函数的宏,方便日志的传参

void Log(std::string level, std::string message, std::string file_name, int line)

    std::cout << "[" << level << "] " << "[" << time(nullptr) << "] " << "[" << message << "] " << "[" << file_name << "] " << "[" << line << "] " << std::endl;


Protocol.hpp

订制一系列的协议,用于才做http报文。构建响应等;

#pragma once

#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
using std::cout;
using std::endl;
class Entrance//临时方案

public:
    //loop创建的线程执行任务的函数
    static void *HandlerRequest(void *psock)
    
        int sock = *(int *)psock;
        delete (int *)psock;
        char buff[4022];
        int s = recv(sock, buff, 4022, 0);
        buff[s-1] = '\\0';
        cout << "===============begin===============" << endl;
        cout << buff << endl;
        cout << "===============end===============" << endl;

        return nullptr;
    
;

运行结果

前三行是打印的日志信息,后面是c端浏览器访问我们server的时候发送的报文,我们将它打印出来了;

解析C端发来的HTTP报文

可见,报文都是一行一行的,我们需要按行读取,先来个按行读取的工具!

MSG_PEEK标志位

recv(sock, &c, 1, MSG_PEEK);

我们一般是设置为0,如果设置MSG_PEEK标志位,则仅仅是把tcp缓冲区中的数据拷贝式的读取到buf中,并没有把已读取的数据从tcp缓冲区中移除,相当于peek窥探一下; 这样我们就可以处理的同时,防止破坏下个报文的报头,造成数据报文不完整了;

Util.hpp

工具类Util

#pragma once

#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
using std::string;
//工具类
class Util

public:
    static int ReadLine(int sock, string &out) //按一行读取报文,返回长度;
    
        char c = 'X';
        while (c != '\\n')
        
            ssize_t s = recv(sock, &c, 1, 0); //(注意,有的报文以\\r\\n 或者 \\r结尾,统一处理为\\n,同时考虑数据粘包问题进行读取!)
            if (s > 0)
            
                if (c == '\\r')
                
                    recv(sock, &c, 1, MSG_PEEK); //窥探一下
                    if (c == '\\n')
                     //窥探成功!大胆拿走这个\\n 放入c中
                        recv(sock, &c, 1, 0);
                    
                    else
                     //窥探失败,直接换掉这个\\r
                        c = '\\n';
                    
                
                out += c;
            
            else if (s == 0)
            
                return 0;
            
            else
             
                return -1;
            
        

        return out.size();
    
;

用Entrance收到报文测试,然后调用按行读取一次,结果如下(调用一次,读取一行,即便请求行)

构建请求与响应类

Protocol.hpp

//请求类
class HttpRequest

public:
    string request_line;           //读取请求行
    vector<string> request_header; //读取请求报头
    string blank;                  //空行分隔符
    string request_body;           //请求报文主体(可能没有)

    //解析完毕之后的结果

    //解析请求行三部分
    string method;
    string uri; // path?args
    string version;

    //解析请求报头
    unordered_map<string, string> header_kv;
    int content_length; //请求body的大小
    string path;        //请求路径
    string suffix;      //后缀 .html  <-> query_string: type/html
    string query_string;

    bool cgi; // cgi技术开关
    int size; //响应的html文件的size大小

public:
    HttpRequest() : content_length(0), cgi(false) 
    ~HttpRequest() 
;

//响应类
class HttpResponse

public:
    string status_line;                  //状态行
    vector<std::string> response_header; //响应报头
    string blank;                        //空行分隔符
    string response_body;                //响应报文主体(html)

    int status_code;
    int fd;

public:
    HttpResponse() : blank(LINE_END), status_code(OK), fd(-1) 
    ~HttpResponse() 
;

上述部分成员后续解析报文详细讲解;

读取,解析请求构建响应

读取请求

读取请求的目的为将整个报文按照一定的格式读入请求类中;

  • 请求行放入string request_line
  • 请求报头存入vector<string> request_header;
  • 空行分隔符放入string blank
  • 请求正文(如果有)放入request_body;
//读取请求,分析请求,构建响应
// IO通信
class EndPoint

private:
    int sock;
    HttpRequest http_request;
    HttpResponse http_response;
    bool stop;

public:
    EndPoint(int _sock) : sock(_sock), stop(false)
    
    

public:
    bool RecvHttpRequestLine() //读取请求行
    
        auto &line = http_request.request_line;
        if (Util::ReadLine(sock, line) <= 0)
        
            stop = true;
        
        else
        
            line.resize(line.size() - 1); //去掉多余的'\\n',塞入日志;
            LOG(INFO, http_request.request_line);
        

        // cout << "RecvHttpRequestLine: " << stop << endl;
        return stop;
    
    bool RecvHttpRequestHeader() //读取请求报头 去掉多余的\\n
    

        auto &v = http_request.request_header;
        while (1) //注意 vector[0]没有值的时候只能push_back进去噢  v[0]=? 会段错误 越界;
        
            string line;
            if (Util::ReadLine(sock, line) <= 0)
            
                stop = true;
                break;
            

            if (line == "\\n")
            
                http_request.blank = line; //空行
                break;
            
            //正常 k:v \\n

            line.resize(line.size() - 1); //去\\n
            http_request.request_header.push_back(line);
            LOG(INFO, line);
        
        return stop;
    
;

bool IsNeedRecvHttpRequestBody()//判断需不需要读 POST方法+存在contentlength,就要读取body了
     
        auto& method = http_request.method;
        auto& mp = http_request.header_kv;
        if(method == "POST")
            if(mp.find("Content-Lenght")!=mp.end())
                http_request.size = atoi(mp["Content-Lenght"].c_str());//记录一下body的size
                return true;
            

            return true;
        
     
     bool RecvHttpRequestBody()
     
        if(IsNeedRecvHttpRequestBody())
            int len = http_request.size;//这里不能&,不然下面循环 原来的size就减没了,为啥这么精确 -->防止粘包
            auto body = http_request.request_body;
            for(int i = 0;i<len;i++)
                char c;
                int s = recv(sock,c,1,0);
                if(s>0)
                    body+=c;
                自主web服务器

介绍自主小型Web服务器实现

自主实现的web服务器

实战项目自主web服务器

实战项目自主web服务器

自主阅读笔记03《基于web 服务器的网站性能优化研究》