0


【项目实战】自主实现 HTTP 项目(一)

项目背景

目前主流的服务器协议是 http1.1,而我们这次要实现的是1.0,其主要的特点就是短链接,所谓短链接,就是请求,相应,客户端关闭连接,这样就完成了一次http请求,使用其主要的原因是因为其简单。

下面我们来谈一谈具体的几个1.0版本的特征:

1.简单快速,HTTP服务器的程序规模小,因而通信速度很快。

2.灵活,HTTP允许传输任意类型的数据对象,正在传输的类型由Content-Type加以标记。

3.无连接,每次连接只处理一个请求。服务器处理完客户的请求,并收到客户的应答后,即断开连接。采用 这种方式可以节省传输时间。(http/1.0具有的功能,http/1.1兼容)

众所周知,http的底层是基于tcp的,而tcp是面向连接的,为什么还说http是无连接的呢?

      **解释:这个地方可以这么理解,有链接是tcp的概念,而http对于连接是没有感知的,他只是把我的请求信息填写好之后,交给下层协议,而对端的http对于连接也是没有概念的,只是把我的相应信息写好之后交给我的下层协议,而下层协议怎么做,那是下层协议的工作,和我无关,所以对我们来讲,所谓的无连接,就是相当于对比tcp的,因为连接是tcp已经帮我们做了,http就不关心了。**

4.无状态 本身是不会记录对方任何状态。

解释:我们每次登录 B站 CSDN的时候,我们发现之前登录过的时候,不再需要身份认证。但是http本身是无状态的,所以http现在是无法满足我们现在的用户要求的。所以这个并不是我们http层来做的

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

可是,随着web的发展,因为无状态而导致业务处理变的棘手起来。比如保持用户的登陆状态。

http/1.1虽然也是无状态的协议,但是为了保持状态的功能,引入了cookie技术,后面我们就会学到。

URI & URL & URN

下面我们来区分三个概念:

URI,是uniform resource identififier,统一资源标识符,用来唯一的标识一个资源

URL,是uniform resource locator,统一资源定位符,它是一种具体的URI,即URL可以用来标识一个资源,而且还指明了如何locate这个资源。

简而言之,URL是URI的一种,他不仅仅可以向URI一样表示唯一的一个资源,而且还可以通过这个来找到这个资源,我们可以把现在的网址叫成URL。

URN,uniform resource name,统一资源命名,是通过名字来标识资源,比如mailto:java

net@java.sun.com。(比较少见)

URI是以一种抽象的,高层次概念定义统一资源标识,而URL和URN则是具体的资源标识的方式。URL和URN都是一种URI.

URL是 URI 的子集。任何东西,只要能够唯一地标识出来,都可以说这个标识是 URI 。如果这个标识是一个可获取到上述对象的路径,那么同时它也可以是一个 URL ;但如果这个标识不提供获取到对象的路径,那么它就必然不是URL 。

举个例子:

URI: /home/index.html

URL: www.xxx.com:/home/index.html

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时,必须以“/”的形式给出,通常这个工作浏览器自动帮我们完成。

如:

输入: www.baidu.com,浏览器自动转换成:http(s)://www.baidu.com/

如果用户的URL没有指明要访问的某种资源(路径),虽然浏览器默认会添加/,但是依旧没有告知服务器,要访问什么资源,此时默认返回对应服务的首页。

回车一下你会发现:

我们没有指明要访问baidu的什么,结果就是返回我们百度的首页。

接下来我们先开始写一点代码,逐步实现一下这个机制:


创建tcp服务端

来完成TCP套间字的
创建,绑定,监听

    1 #pragma once                                                                   
    2 
    3 #include <iostream>
    4 #include <cstdlib>
    5 #include <cstring>
    6 #include <sys/types.h>
    7 #include <sys/socket.h>
    8 #include <netinet/in.h>
    9 #include <arpa/inet.h>
   10 #include <unistd.h>
   11 #include <pthread.h>
   12 
   13 #define BACKLOG 5
   14 
   15 class TcpServer{
   16     private:
   17         int port;
   18         int listen_sock;
   19         static TcpServer *svr;
   20     private:
   21         TcpServer(int _port):port(_port),listen_sock(-1)
   22         {}
   23         TcpServer(const TcpServer &s){}
   24     public:
   25         static TcpServer *getinstance(int port)
   26         {
   27             static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
   28             if(nullptr == svr){
   29                 pthread_mutex_lock(&lock);
   30                 if(nullptr == svr){
   31                     svr = new TcpServer(port);
   32                     svr->InitServer();
   33                 }
   34                 pthread_mutex_unlock(&lock);
   35             }
   36             return svr;
   37         }
   38         void InitServer()
   39         {
   40             Socket();
   41             Bind();
   42             Listen();
   43         }
   44         void Socket()
   45         {
   46             listen_sock = socket(AF_INET, SOCK_STREAM, 0);                     
   47             if(listen_sock < 0){
   48              exit(1);
   49             }
   50             int opt = 1;
   51             setsockopt(listen_sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)      );

对于这个地方内容还是不太清楚的,可以看一下下面这个博主的分享:

网络编程套接字(二)_2021dragon的博客-CSDN博客

说明:这个地方我们先多一步,把它设计成单例模式,这样的话更有利于在后面的文件中直接使用tcpserver,而临界资源需要引入互斥锁。

tcp测试

我们可以对刚刚写的代码进行一下测试:

#include"TcpServer.hpp"
#include<iostream>
#include<string>

static void Usage(std::string proc)
{
   std::cout <<"Usage: \n\t" <<proc<<"  prot "<< std::endl; 
}

int main(int argc,char *argv[])

{
  if(argc != 2){
    Usage(argv[0]);
    exit(4);
  }
  int port =atoi(argv[1]);
  TcpServer *svr = TcpServer::getinstance(port);

  for(;;)
  {
  }
  return 0;

}

说明:

1.这里的Usage是使用手册,当启动客户端的时候输入参数不合法(参数错误或者参数缺少)时候,就会告诉你怎么使用,这就是手册,如下图:

** 这个时候我们没有输入端口号,手册会提示我们,要进行端口号的输入。**

2.这里我们先写一个死循环,仅仅是来测试我们之前写的内容没有什么问题。

这个时候我们通过监视看到,端口号8081的tcp启动,并且为监听状态,说明我们之前写的内容没有什么问题,接下来我们进行下一步的实现。


创建HTTP服务端

接下来,我们就开始写HTTP的请求:

#pragma once 

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

#define PORT 8081

class HttpServer{
private:
    int port;
    TcpServer *tcp_server;
    bool stop;
public:
    HttpServer(int _port = PORT):port(_port),tcp_server(nullptr),stop(false)
    {}
    
    void InitServer()
    {
      tcp_server =TcpServer::getinstance(port);
    }
     
    void Loop()
    {
      int listen_sock = tcp_server->Sock();
      while(!stop){
        struct sockaddr_in peer;
        socklen_t len =sizeof(peer);
        int sock =accept(listen_sock,(struct sockaddr*)&peer,&len);
        if(sock < 0 ){
          continue;
        }
        int *_sock = new int(sock);
        pthread_t tid;
        pthread_create(&tid,nullptr,Entrance:: HandlerRequest,_sock);
       
         pthread_detach(tid);   

      }
    }
    
    
    ~HttpServer()
    {}
};

说明:

1.我们先完成HTTP的初始化,获取一个tcp的对象,拿到其监听套件字,从底层连接队列拿到任务套件字,创建一个线程,线程分离,使得任务套件字去执行对应的任务。

*2.这个地方要说明一下的是, int _sock = new int(sock) 只是暂时的一个方案,sock在中间处理的过程中可能会被修改,这样我们先new一下,后面讲解到任务Task的时候再把这里优化掉。

HTTP服务端测试

执行方法

刚才 HandlerRequest,_sock 中的方法是什么呢,这里我们就是先让其去打印我们获取到的链接,然后打印对应的文件描述符,然后关闭我们对应的文件描述符。

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

class Entrance{
    public:
      static void *HandlerRequest(void *_sock)
      {
        int sock =*(int*)_sock;
        delete (int*)_sock;
        std::cout<<"get a new link ..."<<sock<<std::endl;
        close(sock);
        return nullptr;
      }

};

主文件修改调试:

那么对应的,主文件中的也要稍作调整,不再是测试TcpServer了,而是测试HttpServer,创建对应的一个对象,初始化,启动

#include"HttpServer.hpp"
#include<iostream>
#include<string>
#include<memory>

static void Usage(std::string proc)
{
   std::cout <<"Usage: \n\t" <<proc<<"  prot "<< std::endl; 
}

int main(int argc,char *argv[])

{
  if(argc != 2){
    Usage(argv[0]);
    exit(4);
  }
  int port =atoi(argv[1]);
  std::shared_ptr<HttpServer> http_server(new HttpServer(port));

  http_server->InitServer();
  http_server->Loop();

  return 0;

}

然后编译、链接通过,我们进行测试一下。

我们可以看到,现在已经启动起来了,我们尝试用浏览器对他进行一下访问。

发现有很多链接打印,证明链接成功了。

报文打印测试

这个时候我们其实可以做一个更加深入一点的测试,我们想看一看在服务器一段接收到的信息是什么样子的,我们可以具体来看一下。

我们可以设置一个接收的buffer,设置成4KB的大小,在套件字中读取数据,去获取报文的具体内容。

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

class Entrance{
    public:
      static void *HandlerRequest(void *_sock)
      {
        int sock =*(int*)_sock;
        delete (int*)_sock;  
       
        std::cout<<"get a new link ..."<<sock<<std::endl;
 
        //For Test
        char buffer[4096];
        recv(sock,buffer,sizeof(buffer),0);
        std::cout<<"-------------------begin--------------------"<<std::endl;
        std::cout<<buffer<<std::endl;
        std::cout<<"-------------------end--------------------"<<std::endl;

        close(sock);
        return nullptr;
      }

};

这个时候我们再编译链接通过,用我们的浏览器进行访问。

这个时候我们可以看到:

拿到了我们的报文,那么报文具体是什么意思呢,我们接着往下看

略谈报文

【说明】:

1.POST 是请求方法

2.HTTP 后面跟的是版本

2.剩下的都是以key:value 的形式的属性

包括 主机号:XXXX 连接方式:是长连接的XXX(Key:value)

这里重点是这个GET 后面的 / 是不是linux服务器的根目录开始呢?

不一定,通常不会设置成为根目录,我们通常由http服务器设置为自己的WEB根目录(就是Linux)下一个特定的路径。

              这个时候我们就要读取请求了,但是有的人心想,我们刚才不是已经读取了请求了吗,然而实际上,我们刚才读取的那种请求其实是不标准的,我们是按照4KB大小进行读取的,但是tcp是

面向字节流的,我们不能排除tcp同时有大量的连接请求或者大量的内容进行发送,在没有明确约定的情况下进行读取,很容易发生
数据包粘包的问题,这个时候我们就应该正确的去处理这些问题:

我们读取的基本单位,主要是按照行读取。

但是这里要注意,我们不能使用那些C/C++中按照行读取的接口,因为他们在有些平台是以“\n 或者“\r”来结尾的,有的是以‘\r\n‘来的所以,我们要兼容各种行分隔符

这个地方问题又来了,我们怎么这以\n ,\r,或者\r\n这三种形式的分隔符呢,显然系统调用是不可能实现的,我们只能自己来写

实现较兼容的行读取

#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);
                if(s > 0){
                    if(ch == '\r'){
                        recv(sock, &ch, 1, MSG_PEEK);
                        if(ch == '\n'){
                            //把\r\n->\n
                            //窥探成功,这个字符一定存在
                            recv(sock, &ch, 1, 0);
                        }
                        else{
                            ch = '\n';
                        }
                    }
                    //1. 普通字符
                    //2. \n
                    out.push_back(ch);
                }
                else if(s == 0){
                    return 0;
                }
                else{
                    return -1;
                }
            }
            return out.size();
        }
};

【说明】:

● 本机制的原理如上,如果是\n,则不用处理,为正常的一行分割为获取一个字符

● 首先判断是否是‘ \r ‘,这个时候有两种可能,一种是/r 还有一种是 /r/n,我们这里需要统一处理成\n,达到我们的目的。

● 当拿到\r的时候,这时候需要确定下一个字符是否为\n,我们这里需要进行探测,注意,我们不能直接读取下一个数据,如果下一个不是\n,那么读取后直接已经拿到接收缓冲区,相当于多读了下一行的开头,是绝对不能允许的,所以我们要进行探测,即把recv的第三个参数设成 MSG_PEEK,就会进行下一次的探测,不会直接读到接收缓冲区。

测试较兼容的行读取

简单的进行一下调整,测试读取一行,保存退出,运行。

我们用浏览器再进行一下测试:

我们可以看到:

每一次读取了一行,我们的兼容性行读取就实现了。

标签: http 服务器 网络

本文转载自: https://blog.csdn.net/m0_61703823/article/details/126505255
版权归原作者 萌小檬 所有, 如有侵权,请联系我们删除。

“【项目实战】自主实现 HTTP 项目(一)”的评论:

还没有评论