【项目实战】自主实现 HTTP 项目(一)——tcp、http的创建与实现较兼容的行读取

目录

项目背景

URI & URL & URN

创建tcp服务端

创建HTTP服务端

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 则是具体的资源标识的方式。 URLURN都是一种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,就会进行下一次的探测,不会直接读到接收缓冲区。

测试较兼容的行读取

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

我们用浏览器再进行一下测试:
我们可以看到:

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

  • 14
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 13
    评论
评论 13
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值