实战项目自主web服务器

Posted 小倪同学 -_-

tags:

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

文章目录

项目简介

什么是web服务器: 通过HTTP协议构建一个web服务器,该服务器能够处理浏览器发过来的http请求,并根据http请求返回相应的http响应给浏览器。相当于搭建个人网站,你可以在个人网站上存放各种资源,别人可以通过浏览器访问你的网站,并获取资源。

背景: 该项目主要用http协议,在网络中,http协议被广泛使用如移动端,pc端浏览器。http协议是打开互联网应用窗口的重要协议,它在网络应用层中的地位不可撼动,是能准确区分前后台的重要协议。

目标: 该项目的目标是,对http协议的理论有更深刻的理解,从零开始完成web服务器开发,连接下三层协议,从技术到应用,让网络难点无处遁形。

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

技术特点: 该项目是一个后端开发项目,开发环境为centos 7 + vim/gcc/gdb/VS Code,开发语言为C/C++,应用网络编程(TCP/IP协议, socket流式套接字,http协议),多线程,cgi,线程池等技术。

认识http协议

http分层概览

与http相关的重要协议有TCP,IP,DNS

这里介绍一下DNS

DNS(域名系统)是将域名和IP地址相互映射的一个分布式数据库,能够使人更方便地访问互联网。


协议之间是如何协同运作的呢?

http背景知识补充

目前主流服务器使用的是http/1.1版本,该项目是按照http/1.0版本来完成讲解,同时,我们还会对比1.1和1.0的区别。

http协议的特点如下

  • 简单快速,HTTP服务器的程序规模小,因而通信速度很快。
  • 灵活,HTTP允许传输任意类型的数据对象,正在传输的类型由Content-Type加以标记。
  • 无连接,每次连接只处理一个请求。服务器处理完客户的请求,并收到客户的应答后,即断开连接。采用这种方式可以节省传输时间。(http/1.0具有的功能,http/1.1兼容)。
  • 无状态(HTTP协议自身不具有保存之前发送过的请求或响应的功能)

注意: http协议每当有新的请求产生,就会有对应的新响应产生。协议本身并不会保留你之前的一切请求或者响应,这是为了更快的处理大量的事务,确保协议的可伸缩性。

URI & URL & URN

我们想访问web上资源需要用资源标志符(URI)进行定位,和URI相关的还有URL,URN。

  • URI,是uniform resource identifier,统一资源标识符,用来唯一的标识一个资源。
  • URL,是uniform resource locator,统一资源定位符,它是一种具体的URI,即URL可以用来标识一个资源,而且还指明了如何locate这个资源。
  • URN,uniform resource name,统一资源命名,是通过名字来标识资源。

URI是以一种抽象的,高层次概念定义统一资源标识,而URL和URN则是具体的资源标识的方式。URL和URN都是一种URI,URL是 URI 的子集。任何东西,只要能够唯一地标识出来,都可以说这个标识是 URI 。如果这个标识是一个可获取到上述对象的路径,那么同时它也可以是一个 URL ;但如果这个标识不提供获取到对象的路径,那么它就必然不是URL 。

HTTP URL (URL是一种特殊类型的URI,包含了如何获取指定资源)的格式如下:

http://host[":"port][abs_path]
  • http表示要通过HTTP协议来定位网络资源
  • host表示合法的Internet主机域名或者IP地址,本主机IP:127.0.0.1
  • port指定一个端口号,为空则使用缺省端口80。(通常会根据协议采用默认端口号)
  • abs_path指定请求资源的URI
  • 如果URL中没有给出abs_path,那么当它作为请求URI时,必须以“/”的形式给出,通常这个工作浏览器自动帮我们完成。(如果没有abs_path,会默认访问该服务端首页)。

例:

一个较为完整的http请求:

 http://www.aspxfans.com:8080/news/index.asp?boardID=5&ID=24618&page=1

构建tcp服务器

我们这里的tcp服务器是对网络套接字创建,绑定,监听等进行封装,方便之后的使用。

这里我们把 tcp server 类设计为单例模式,让程序访问到的 tcp server 是同一个。代码如下

#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<pthread.h>

#define PORT 8081
#define BACKLOG 5

class TcpServer
    private:
        int port;// 端口号
        int listen_sock;// 套接字
        static TcpServer *svr;
    private:
        TcpServer(int _port = PORT):port(_port),listen_sock(-1)
        
        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);// 创建tcpserver
                    svr -> InitServer();// 初始化
                
                pthread_mutex_unlock(&lock);
            
            return svr;
        

        void InitServer()// 初始化
        
            Socket();
            Bind();
            Listen();
        

        void Socket()
        
            listen_sock = socket(AF_INET,SOCK_STREAM,0);// 创建套接字
            if(listen_sock < 0)
                exit(1);
            
            int opt = 1;
            setsockopt(listen_sock,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));// 设置套接字选项,允许地址复用
        

        void Bind()
        
            struct sockaddr_in local;
            memset(&local,0,sizeof(local));// 清零
            local.sin_family = AF_INET;
            local.sin_port = htons(port);
            local.sin_addr.s_addr = INADDR_ANY;// 云服务器不能直接绑定公网IP

            if(bind(listen_sock,(struct sockaddr*)&local,sizeof(local))<0)// 绑定失败
                exit(2);
            
        

        void Listen()
        
            if(listen(listen_sock,BACKLOG)<0)
                exit(3);
            
        

        ~TcpServer()
        
;

TcpServer* TcpServer::svr = nullptr;

关于setsockopt函数可以参考这篇博客setsockopt()函数功能介绍

HTTP请求和响应

请求和响应过程

浏览器请求服务器资源需要给服务器发送http请求,服务器收到请求需要返回http响应给浏览器,我们需要详细了解请求和响应的格式,为之后编码做准备。

HTTP请求报文

HTTP请求报文分为 请求行,请求报头,空行,请求正文

  • 请求行: 请求方法,请求URL,HTTP协议和版本
  • 请求报头: 请求属性,每一行表示一个属性,遇到空行表示请求报头结束。
  • 空行:用于分割请求报头和请求正文
  • 请求正文:如果存在正文,请求报头中就需要Content-Length属性来标识正文的大小

HTTP响应报文


HTTP响应报文分为 响应行,响应报头,空行,响应正文

  • 响应行:http协议和版本,状态码,状态描述符
  • 响应报头:响应属性,每一行表示一个属性,遇到空行表示响应报头结束。
  • 空行:分割响应报头和响应正文
  • 响应正文:服务器返回的html页面或者json数据

工具类

我们在编写协议时难免会有一些程序经常用到,如:读取报头中的一行。我们可以设计一个工具类,将这些程序存放其中。

ReadLine()函数

ReadLine()函数的作用是读取sock套接字中的一行数据。无论是请求报头还是响应报头,它们的属性都是按行进行划分的。但是不同的浏览器发送过来的http请求,它们中的行分割符是不同的,大致分为如下三类

  1. xxxxx \\r\\n
  2. xxxxx \\n
  3. xxxxx \\r

我们要设计一个算法,兼容各种行分割符。

读取数据,如果读取到 \\n 表示该行已结束,如果读取到 \\r ,需要判断下一个字符是否为 \\n 。不是,那么该行已结束,否则需要将 \\r\\n 转变为 \\n。

这里我们需要用到 recv 函数

recv函数

功能: 接收已连接的数据报或流式套接口的数据。

函数原型:

int recv( int sockfd, void *buf, size_t len,int flags);

参数说明:

  • sockfd :发送数据的套接字
  • buf :存放recv函数接收到的数据
  • len :buf的长度
  • flags :recv发送数据的选项,一般设置为0

返回值: 成功返回0,错误返回-1

编码

#pragma once

#include<iostream>
#include<string>
#include<sys/types.h>
#include<sys/socket.h>

//工具类
class Util
    public:
        static int ReadLine(int sock,std::string &out)
        
            char ch = 'x';
            while(ch != '\\n')
                ssize_t s = recv(sock,&ch,1,0);// 从sock中读取一个字符到ch中
                if(s > 0)                 
                    if(ch == '\\r')
                        recv(sock,&ch,1,MSG_PEEK);// MSG_PEEK 窥探下一个字符(不从sock中取走)
                        if(ch == '\\n')
                            // 把\\r\\n -> \\n
                            // 窥探成功读取该字符
                            recv(sock,&ch,1,0);
                        
                        else
                            ch = '\\n';// \\r -> \\n
                        
                    
                    // 到这有两种情况 1.普通字符 2.\\n
                    out.push_back(ch);
                
                else if(s == 0)
                    return 0;
                
                else
                    return -1;
                
            
            return out.size();
        
;

CutString()函数

CutString()函数的功能是将字符串按特定字符切分成左右两部分,代码如下

#pragma once

#include<iostream>
#include<string>
#include<sys/types.h>
#include<sys/socket.h>

//工具类
class Util
    public:
        static int ReadLine(int sock,std::string &out)
        
        	...
        
        
        static bool CutString(const std::string &target, std::string &sub1_out, std::string &sub2_out, std::string sep)
        
            size_t pos = target.find(sep);// 查找分割符
            if(pos != std::string::npos)
                sub1_out = target.substr(0,pos);
                sub2_out = target.substr(pos+sep.size());
                return true;
            
            return false;
        			
		// 传入 
		// target = Content-Length : 18  
		// sep = " : "
		// 传出 
		// sub1_out = Content-Length
		// sub2_out = sep
;

日志信息

为了方便代码解析和调试,我们需要设计函数来记录日志信息,日志信息的内容如下

乍一看传入的参数是不是有点多,实际上我们只需传入 日志级别日志信息 这两个参数,剩下的参数我们可以让操作系统去查找,为此可以设置宏,通过宏替换来获取 日志文件代码行数

#pragma once

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

#define INFO 1
#define WARNING 2
#define ERROR 3
#define FATAL 4

#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;

请求和响应类的设计

http服务器收到一个请求时,服务器需要做如下工作:读取请求,分析请求,构建响应,发送响应。为了更好的管理请求报头和响应报头,我们需要设计一个类来存放请求报头和响应报头。

http请求类

class HttpRequest
    public:
        std::string request_line;// 请求行
        std::vector<std::string> request_header;// 请求报头
        std::string blank;// 请求空行
        std::string request_body;// 请求正文

        // 请求行的解析
        std::string method;
        std::string uri;
        std::string version;
			
		// 请求报头解析
        std::unordered_map<std::string, std::string> header_kv;// 属性名:属性信息
        int content_length;// 请求正文长度

		// URI解析
        std::string suffix;// 请求文件名的后缀
        std::string path;// URI路径
        std::string query_string;// URI参数

        bool cgi;// cgi处理标志符
        int size;// 打开的文件大小
    public:
        HttpRequest():content_length(0),cgi(false)
        ~HttpRequest()
;

http响应类

#define LINE_END "\\r\\n"

class HttpResponse
    public:
        std::string status_line;// 响应行
        std::vector<std::string> response_header;// 响应报头
        std::string blank;// 响应空行
        std::string response_body;// 响应正文  

        int status_code;// 响应状态码
        int fd;// 暂时保存文件描述符
    public:   
        HttpResponse():blank(LINE_END),status_code(OK),fd(-1)
        ~HttpResponse()
;

请求处理

请求报文

处理请求报头时,先将报头的数据读取出来,再对其进行处理

void RecvHttpRequest()

    // 读取请求行和报头
    if((!RecvHttpRequestLine()) && (!RecvHttpRequestHeader()))
        ParseHttpRequestLine();// 解析请求行
        ParseHttpRequestHeader();// 解析请求报头
        RecvHttpRequestBody();// 读取请求正文
    

读取请求行

利用工具类中的ReadLine()函数将请求行读入到请求类中

bool RecvHttpRequestLine()

    auto& line = http_request.request_line;
    if(Util::ReadLine(sock,line) > 0)
        line.resize(line.size()-1);// 去除行尾 \\n
        LOG(INFO,http_request.request_line);// 记录日志
    
    else
        stop = true;// 标记读取失败
    
    return stop;

读取请求报头

按行为单位,将读取到的报头属性插入到请求报头中。

bool RecvHttpRequestHeader()

    std::string line;
    while(line!="\\n")
        line.clear();// 读取前清空line
        if(Util::ReadLine(sock,line) <= 0)// 按行读取
            stop = true;
            break;
        
        if(line == "\\n")// 读取到空行,请求报头已读完
            http_request.blank = line;
            break;
        
        line.resize(line.size()-1);// 去除最后的'\\n'
        http_request.request_header.push_back(line);// 将读取到的报头属性尾插入请求报头中
        LOG(INFO,line);
    
    return stop;

解析请求行

请求行由 请求方法,请求URI,HTTP协议版本 构成,它们中间由空格隔开,如下图

解析请求行就是将字符串按空格分割。

将字符串按空格分割我们可以调用库函数中的stringstream

stringstream使用示例:

#include<iostream>
#include<string>
#include<sstream>

int main()

    std::string msg = "GET /a/b/c.html http/1.0";
    std::string method;
    std::string uri;
    std::string version;
    std::stringstream ss(msg);

    ss >> method >> uri >> version;
    std::cout << method << std::endl;
    std::cout << uri << std::endl;
    std::cout << version << std::endl;

运行结果

解析请求行代码

void ParseHttpRequestLine()

    auto &line = http_request.request_line;// 获取请求行
    std::stringstream ss(line);
    ss >> http_request.method >> http_request.uri >> http_request.version;
    auto &method = http_request.method;// 获取请求行中的请求方法
    std::transform(method.begin(),method.end(),method.begin(),::toupper);// 字母转大写,方

以上是关于实战项目自主web服务器的主要内容,如果未能解决你的问题,请参考以下文章

实战项目自主web服务器

实战项目:EMOS集成邮件平台

项目设计自主HTTP服务器

项目设计自主HTTP服务器

spring security实战项目笔记

自主Web服务器Http_Server