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

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

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

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

    tcp测试

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

    1. #include"TcpServer.hpp"
    2. #include
    3. #include
    4. static void Usage(std::string proc)
    5. {
    6. std::cout <<"Usage: \n\t" <" prot "<< std::endl;
    7. }
    8. int main(int argc,char *argv[])
    9. {
    10. if(argc != 2){
    11. Usage(argv[0]);
    12. exit(4);
    13. }
    14. int port =atoi(argv[1]);
    15. TcpServer *svr = TcpServer::getinstance(port);
    16. for(;;)
    17. {
    18. }
    19. return 0;
    20. }

    说明:

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

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

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

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


    创建HTTP服务端

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

    1. #pragma once
    2. #include
    3. #include
    4. #include"Protocol.hpp"
    5. #include "TcpServer.hpp"
    6. #define PORT 8081
    7. class HttpServer{
    8. private:
    9. int port;
    10. TcpServer *tcp_server;
    11. bool stop;
    12. public:
    13. HttpServer(int _port = PORT):port(_port),tcp_server(nullptr),stop(false)
    14. {}
    15. void InitServer()
    16. {
    17. tcp_server =TcpServer::getinstance(port);
    18. }
    19. void Loop()
    20. {
    21. int listen_sock = tcp_server->Sock();
    22. while(!stop){
    23. struct sockaddr_in peer;
    24. socklen_t len =sizeof(peer);
    25. int sock =accept(listen_sock,(struct sockaddr*)&peer,&len);
    26. if(sock < 0 ){
    27. continue;
    28. }
    29. int *_sock = new int(sock);
    30. pthread_t tid;
    31. pthread_create(&tid,nullptr,Entrance:: HandlerRequest,_sock);
    32. pthread_detach(tid);
    33. }
    34. }
    35. ~HttpServer()
    36. {}
    37. };

    说明:

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

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

    HTTP服务端测试

    执行方法

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

    1. #include
    2. #include
    3. #include
    4. #include
    5. class Entrance{
    6. public:
    7. static void *HandlerRequest(void *_sock)
    8. {
    9. int sock =*(int*)_sock;
    10. delete (int*)_sock;
    11. std::cout<<"get a new link ..."<
    12. close(sock);
    13. return nullptr;
    14. }
    15. };

     主文件修改调试:

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

    1. #include"HttpServer.hpp"
    2. #include
    3. #include
    4. #include
    5. static void Usage(std::string proc)
    6. {
    7. std::cout <<"Usage: \n\t" <" prot "<< std::endl;
    8. }
    9. int main(int argc,char *argv[])
    10. {
    11. if(argc != 2){
    12. Usage(argv[0]);
    13. exit(4);
    14. }
    15. int port =atoi(argv[1]);
    16. std::shared_ptr http_server(new HttpServer(port));
    17. http_server->InitServer();
    18. http_server->Loop();
    19. return 0;
    20. }

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

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

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

    报文打印测试

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

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

    1. #include
    2. #include
    3. #include
    4. #include
    5. class Entrance{
    6. public:
    7. static void *HandlerRequest(void *_sock)
    8. {
    9. int sock =*(int*)_sock;
    10. delete (int*)_sock;
    11. std::cout<<"get a new link ..."<
    12. //For Test
    13. char buffer[4096];
    14. recv(sock,buffer,sizeof(buffer),0);
    15. std::cout<<"-------------------begin--------------------"<
    16. std::cout<
    17. std::cout<<"-------------------end--------------------"<
    18. close(sock);
    19. return nullptr;
    20. }
    21. };

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

     这个时候我们可以看到:

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

     略谈报文

    【说明】:

    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这三种形式的分隔符呢,显然系统调用是不可能实现的,我们只能自己来写

    实现较兼容的行读取

    1. #pragma once
    2. #include
    3. #include
    4. #include
    5. #include
    6. //工具类
    7. class Util{
    8. public:
    9. static int ReadLine(int sock, std::string &out)
    10. {
    11. char ch = 'X';
    12. while(ch != '\n'){
    13. ssize_t s = recv(sock, &ch, 1, 0);
    14. if(s > 0){
    15. if(ch == '\r'){
    16. recv(sock, &ch, 1, MSG_PEEK);
    17. if(ch == '\n'){
    18. //把\r\n->\n
    19. //窥探成功,这个字符一定存在
    20. recv(sock, &ch, 1, 0);
    21. }
    22. else{
    23. ch = '\n';
    24. }
    25. }
    26. //1. 普通字符
    27. //2. \n
    28. out.push_back(ch);
    29. }
    30. else if(s == 0){
    31. return 0;
    32. }
    33. else{
    34. return -1;
    35. }
    36. }
    37. return out.size();
    38. }
    39. };
    【说明】:
    ● 本机制的原理如上,如果是\n,则不用处理,为正常的一行分割为获取一个字符
    ● 首先判断是否是‘ \r ‘,这个时候有两种可能,一种是/r  还有一种是 /r/n,我们这里需要统一处理成\n,达到我们的目的。
    ● 当拿到\r的时候,这时候需要确定下一个字符是否为\n,我们这里需要进行探测,注意,我们不能直接读取下一个数据,如果下一个不是\n,那么读取后直接已经拿到接收缓冲区,相当于多读了下一行的开头,是绝对不能允许的,所以我们要进行探测,即把recv的第三个参数设成 MSG_PEEK,就会进行下一次的探测,不会直接读到接收缓冲区。

    测试较兼容的行读取

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

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

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

  • 相关阅读:
    【FreeRTOS】【STM32】02 FreeRTOS 移植
    Jenkins配置及插件安装
    Tiger DAO VC产品正式上线,Seektiger生态的有力补充
    2120 -- 预警系统题解
    随机专享记录第一话 -- RustDesk的自我搭建和使用
    LG 选择 Flutter 来增强其智能电视操作系统 webOS
    我的创作纪念日——你知道这5年我是怎么过的吗?
    SSM+教学网站 毕业设计-附源码211611
    QT 联合opencv 易错点
    2024能源动力、机械自动化与航天航空技术国际学术会议(ICEPMAT2024)
  • 原文地址:https://blog.csdn.net/m0_61703823/article/details/126505255