HTTP协议初识·上篇

目录就用自带的吧,这边一直会出错不知道为什么……

本篇介绍

认识URL

手写一个网络服务器用以验证流程

写一个基本网页用以验证

源码

认识URL

urlencode和urldecode

像 / ? : 等这样的字符, 已经被url当做特殊意义理解了. 因此这些字符不能随意出现.
比如, 某个参数中需要带有这些特殊字符, 就必须先对特殊字符进行转义.


转义的规则如下:
将需要转码的字符转为16进制,然后从右到左,取4位(不足4位直接处理),每2位做一位,前面加上%,编码成
%XY格式

实例

wd:word缩写

如何编码解码和验证过程

一个基本的网络服务器的流程

模拟请求与响应

为了防止重复代码,基于上篇文章删改过来的,一个干净的多线程服务器

参考 :一个简单的协议定制_清风玉骨的博客-CSDN博客

HTTPServer.hpp

#pragma once

#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <functional>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <signal.h>
#include <pthread.h>

#include "Protocol.hpp"

namespace server
{
    enum
    {
        USAGE_ERR = 1,
        SOCKET_ERR,
        BIND_ERR,
        LISTEN_ERR

    };

    static const uint16_t gport = 8080;
    static const int gbacklog = 5; // 10、20、50都可以,但是不要太大比如5千,5万

    using func_t = std::function<bool(const HttpRequest &, HttpResponse &)>;    // 回调

    class HttpServer
    {
    public:
        HttpServer(func_t func, const uint16_t &port = gport) : _func(func), _listensock(-1), _port(port)
        {
        }
        void initServer()
        {
            // 1. 创建socket文件套接字对象 -- 流式套接字
            _listensock = socket(AF_INET, SOCK_STREAM, 0); // 第三个参数默认 0
            if (_listensock < 0)
            {
                exit(SOCKET_ERR);
            }

            // 2.bind绑定自己的网路信息 -- 注意包含头文件
            struct sockaddr_in local;
            memset(&local, 0, sizeof(local));
            local.sin_family = AF_INET;
            local.sin_port = htons(_port);      // 这里有个细节,我们会发现当我们接受数据的时候是不需要主机转网路序列的,因为关于IO类的接口,内部都帮我们实现了这一功能,这里不帮我们做是因为我们传入的是一个结构体,系统做不到
            local.sin_addr.s_addr = INADDR_ANY; // 接受任意ip地址
            if (bind(_listensock, (struct sockaddr *)&local, sizeof(local)) < 0)
            {
                exit(BIND_ERR);
            }

            // 3. 设置socket 为监听状态 -- TCP与UDP不同,它先要建立链接之后,TCP是面向链接的,后面还会有“握手”过程
            if (listen(_listensock, gbacklog) < 0) // 第二个参数backlog后面再填这个坑
            {
                exit(LISTEN_ERR);
            }
        }

        void start()
        {
            for (;;) // 一个死循环
            {
                // 4. server 获取新链接
                // sock 和 client 进行通信的fd
                struct sockaddr_in peer;
                socklen_t len = sizeof(peer);
                int sock = accept(_listensock, (struct sockaddr *)&peer, &len);
                if (sock < 0)
                {
                    continue;
                }

                /* 这里直接使用多进程版的代码进行修改
                 version 2 多进程版(2) -- 注意子进程会继承父进程的一些东西,父进程的文件操作符就会被子进程继承,
                 即父进程的文件操作符那个数字会被继承下来,指向同一个文件,但是文件本身不会被拷贝一份
                 也就是说子进程可以看到父进程创建的文件描述符sock和打开的listensock     */
                pid_t id = fork();
                if (id == 0) // 当id为 0 的时候就代表这里是子进程
                {
                    /* 关闭不需要的文件描述符 listensock -- 子进程不需要监听,所以我们要关闭这个不需要的文件描述符
                       即使这里不关,有没有很大的关系,但是为了防止误操作我们还是关掉为好   */
                    close(_listensock);
                    if (fork() > 0)
                        exit(0); // 解决方法1: 利用孤儿进程特性
                    // TODO
                    close(sock);
                    exit(0);
                }
                /* 一定要关掉,否则就会造成文件描述符泄漏,但是这里的关掉要注意了,这里只是把文件描述符的计数-1
                 子进程已经继承过去了,所以这里也可以看做,父进程立马把文件描述符计数-1,只有当子进程关闭的时候,这个文件描述符真正的被关闭了
                 所以后面申请的链接使用的还是这个4号文件描述符,因为计算机太快了
                 close(sock);   */

                /* father
                  那么父进程干嘛呢? 直接等待吗? -- 显然不能,这样又会回归串行运行了,因为等待的时候会阻塞式等待
                  且这里并不能用非阻塞式等待,因为万一有一百个链接来了,就有一百个进程运行,如果这里非阻塞式等待
                  一但后面没有链接到来的话.那么accept这里就等不到了,这些进程就不会回收了 */

                // 不需要等待了 version 2
                waitpid(id, nullptr, 0);
            }
        }

        ~HttpServer() {}

    private:
        int _listensock;
        uint16_t _port;
        func_t _func;
    };

} // namespace server

Protocol.hpp

#pragma once

#include <iostream>
#include <string>

class HttpRequest
{
public:
    std::string inbuffer;
};

class HttpResponse
{
public:
    std::string outbuffer;
};

makefile

cc=g++
httpserver:HttpServer.cc
	$(cc) -o $@ $^ -std=c++11
 
.PHONY:clean
clean:
	rm -f httpserver

这里因为有天然的客户端(游览器),所以我们先暂时不需要写客户端

1请求

HTTPServer.hpp
1.0函数handlerHttp-基本流程

再次处理

记得把sock(套接字)传进来

HttpServer.cc(新建文件)

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

using namespace std;
using namespace server;

void Usage(std::string proc)
{
    cerr << "Usage:\n\t" << proc << " port\r\n\r\n";
}

bool Get(const HttpRequest &req, HttpResponse &resp)
{
    // for test
    cout << "----------------------- http start ------------------------------------" << endl;
    cout << req.inbuffer << endl;   // 暂时不做其他处理,直接打印出来看请求内容
    cout << "------------------------ http end -------------------------------------" << endl;
    return true;
}

// ./httpServer 8080    -- 这里实际是80端口号,不过这里是为了测试就不用了,并且80也绑定不了,前一千多号基本内部资源无法绑定
int main(int argc, char *argv[])
{
    if(argc != 2)
    {
        Usage(argv[0]);
        exit(0);
    }
    uint16_t port = atoi(argv[1]);
    unique_ptr<HttpServer> httpsvr(new HttpServer(Get, port));
    httpsvr->initServer();
    httpsvr->start();

    return 0;
}

测试1 -- 请求测试

云服务器响应1

云服务器响应2

实际中应该如下图,这其实是一个高并发的请求,它一次性会请求多个,并不是一个 

云服务器响应2解析

手机测试解析

游览器推送解析

爬虫原理

2响应

一个简单的网页

可以参考这个网页教程 HTML 简介_w3cschool

在VScode中创建一个这种后缀的文件,输入 ! 后按Tab键,得到一个网页的基本格式

 

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    
</body>
</html>
修改一下

        修剪成一行,因为是在c++中硬编码,所以使用起来比较麻烦,当遇到特殊字符的时候注意一下要斜杠转义一下,防止报错

测试2 -- 响应测试

telnet,一个测试工具,有兴趣可以去了解
客户端视角

服务端视角

浏览器请求视角

        这次测试,明明我们并没有填写响应报头,并且还没有报头的长度,但是明显浏览器可以很好的解决这几点,并且暂时还没有出现很大的问题,把内容解释出来了。

        现在在浏览器已经很智能了,它已经可以识别出内容是什么,是文本还是网页,甚至图片或者视频,这方面Chrome做的比较成熟,但是有些游览器并不会做这些,比如火狐它就会以文本的形式显示出来,网页并不会帮你做解释。

        基于这一点,我们还是要填写好自己的报头,告诉浏览器我们发送过去的是一个网页。

乱码解决方法

其实这里大概可以了,不过为了完善,我们先填写好报头再进一步测试

添加报头

测试3 -- 乱码解决

云服务器的配置较低可能申请失败

浏览器会一次性高并发申请很多很多,不过有结果就行了,这方面不是我们要考虑的,这次测试有几次出错了,我感觉就是这个原因

3分割字段

 

准备工作

新文件Util.hpp
2.0新函数getOneline分割出一行一行的字段

2.1新函数parse和认识新接口stringstream

后者是一种流 

HttpServer.cc修改
2.2打印工作放到外面来

测试结果4分割字段

注意细节

5修改默认web起始目录

补充知识

wwwroot是一个目录

http.conf是一个配置文件

这边我们为了方便就直接写入代码中,不进行配置了

default-root

5.1parse修改

6首页设置

这时候拼接会有一个问题出现

6.1首页默认放到wwwroot下的一级目录中

当访问根目录的情况下,会直接给path拼接首页路径

6.2加一个判断就可以解决访问首页的问题

6.3再加一个path的打印

测试5路径拼接测试

1拼接路径

2拼接首页

全部源码

HttpServer.cc

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

using namespace std;
using namespace server;

void Usage(std::string proc)
{
    cerr << "Usage:\n\t" << proc << " port\r\n\r\n";
}

// 1. 服务器和网页分离,html
// 2. url -> / : web根目录
bool Get(const HttpRequest &req, HttpResponse &resp)
{
    // for test
    cout << "----------------------- http start ------------------------------------" << endl;
    cout << req.inbuffer << endl; // 暂时不做其他处理,直接打印出来看请求内容
    std::cout << "method: " << req.method << std::endl;
    std::cout << "url: " << req.url << std::endl;
    std::cout << "httpversion: " << req.httpversion << std::endl;
    std::cout << "path: " << req.path << std::endl;
    cout << "------------------------ http end -------------------------------------" << endl;
    std::string respline = "HTTP/1.1 200 OK\r\n";
    std::string respheader = "Content-Type: text/html\r\n";
    std::string respblank = "\r\n"; // 空行
    // 网页 -- 自己写一个简单的, 不要在C++中写html,这里是测试,很不方便
    std::string body = "<html lang=\"en\"><head><meta charset=\"UTF-8\"><title>for test</title><h1>七夕节日我竟然在搞这个?</h1></head><body><p>好寂寞~</p></body></html>";

    // 直接拼接就可以了,本身很简单
    resp.outbuffer += respline;
    resp.outbuffer += respheader;
    resp.outbuffer += respblank;
    resp.outbuffer += body;

    return true;
}

// ./httpServer 8080    -- 这里实际是80端口号,不过这里是为了测试就不用了,并且80也绑定不了,前一千多号基本内部资源无法绑定
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        Usage(argv[0]);
        exit(0);
    }
    uint16_t port = atoi(argv[1]);
    unique_ptr<HttpServer> httpsvr(new HttpServer(Get, port));
    httpsvr->initServer();
    httpsvr->start();

    return 0;
}

HttpServer.hpp

#pragma once

#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <functional>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <signal.h>
#include <pthread.h>

#include "Protocol.hpp"

namespace server
{
    enum
    {
        USAGE_ERR = 1,
        SOCKET_ERR,
        BIND_ERR,
        LISTEN_ERR

    };

    static const uint16_t gport = 8080;
    static const int gbacklog = 5; // 10、20、50都可以,但是不要太大比如5千,5万

    using func_t = std::function<bool(const HttpRequest &, HttpResponse &)>;    // 回调

    class HttpServer
    {
    public:
        HttpServer(func_t func, const uint16_t &port = gport) : _func(func), _listensock(-1), _port(port)
        {
        }
        void initServer()
        {
            // 1. 创建socket文件套接字对象 -- 流式套接字
            _listensock = socket(AF_INET, SOCK_STREAM, 0); // 第三个参数默认 0
            if (_listensock < 0)
            {
                exit(SOCKET_ERR);
            }

            // 2.bind绑定自己的网路信息 -- 注意包含头文件
            struct sockaddr_in local;
            memset(&local, 0, sizeof(local));
            local.sin_family = AF_INET;
            local.sin_port = htons(_port);      // 这里有个细节,我们会发现当我们接受数据的时候是不需要主机转网路序列的,因为关于IO类的接口,内部都帮我们实现了这一功能,这里不帮我们做是因为我们传入的是一个结构体,系统做不到
            local.sin_addr.s_addr = INADDR_ANY; // 接受任意ip地址
            if (bind(_listensock, (struct sockaddr *)&local, sizeof(local)) < 0)
            {
                exit(BIND_ERR);
            }

            // 3. 设置socket 为监听状态 -- TCP与UDP不同,它先要建立链接之后,TCP是面向链接的,后面还会有“握手”过程
            if (listen(_listensock, gbacklog) < 0) // 第二个参数backlog后面再填这个坑
            {
                exit(LISTEN_ERR);
            }
        }

        void HandlerHttp(int sock)
        {
            // 1. 读到完整的http请求
            // 2. 反序列化
            // 3. 反序列化后得到httprequest, 回调填写httpresponse, 利用_func(req, resp)
            // 4. 序列化resp
            // 5. send
            char buffer[4096];
            HttpRequest req;
            HttpResponse resp;
            size_t n = recv(sock, buffer, sizeof(buffer) - 1, 0);   // 大概率我们直接能读取到完整的http请求
            if(n > 0)
            {
                buffer[n] = 0;
                req.inbuffer = buffer;
                req.parse();
                _func(req, resp); // 可以根据bool返回值进行判断,这里就不判断了
                send(sock, resp.outbuffer.c_str(), resp.outbuffer.size(), 0);
            }
        }

        void start()
        {
            for (;;) // 一个死循环
            {
                // 4. server 获取新链接
                // sock 和 client 进行通信的fd
                struct sockaddr_in peer;
                socklen_t len = sizeof(peer);
                int sock = accept(_listensock, (struct sockaddr *)&peer, &len);
                if (sock < 0)
                {
                    continue;
                }

                /* 这里直接使用多进程版的代码进行修改
                 version 2 多进程版(2) -- 注意子进程会继承父进程的一些东西,父进程的文件操作符就会被子进程继承,
                 即父进程的文件操作符那个数字会被继承下来,指向同一个文件,但是文件本身不会被拷贝一份
                 也就是说子进程可以看到父进程创建的文件描述符sock和打开的listensock     */
                pid_t id = fork();
                if (id == 0) // 当id为 0 的时候就代表这里是子进程
                {
                    /* 关闭不需要的文件描述符 listensock -- 子进程不需要监听,所以我们要关闭这个不需要的文件描述符
                       即使这里不关,有没有很大的关系,但是为了防止误操作我们还是关掉为好   */
                    close(_listensock);
                    if (fork() > 0) exit(0); // 解决方法1: 利用孤儿进程特性
                    HandlerHttp(sock);
                    close(sock);
                    exit(0);
                }
                /* 一定要关掉,否则就会造成文件描述符泄漏,但是这里的关掉要注意了,这里只是把文件描述符的计数-1
                 子进程已经继承过去了,所以这里也可以看做,父进程立马把文件描述符计数-1,只有当子进程关闭的时候,这个文件描述符真正的被关闭了
                 所以后面申请的链接使用的还是这个4号文件描述符,因为计算机太快了
                 close(sock);   */

                /* father
                  那么父进程干嘛呢? 直接等待吗? -- 显然不能,这样又会回归串行运行了,因为等待的时候会阻塞式等待
                  且这里并不能用非阻塞式等待,因为万一有一百个链接来了,就有一百个进程运行,如果这里非阻塞式等待
                  一但后面没有链接到来的话.那么accept这里就等不到了,这些进程就不会回收了 */

                // 不需要等待了 version 2
                waitpid(id, nullptr, 0);
            }
        }

        ~HttpServer() {}

    private:
        int _listensock;
        uint16_t _port;
        func_t _func;
    };

} // namespace server

makefile

cc=g++
httpserver:HttpServer.cc
	$(cc) -o $@ $^ -std=c++11
 
.PHONY:clean
clean:
	rm -f httpserver

Protocol.hpp

#pragma once

#include <iostream>
#include <string>
#include <vector>
#include <sstream> // 可以直接以空格作为分隔符来进行分割

#include "Util.hpp"

const std::string sep = "\r\n"; // 分隔符
const std::string default_root = "./wwwroot";    // web起始目录,前面的 ./ 加不加都可以
const std::string home_page = "index.html";     // 默认首页,任何服务器都会有这个默认首页

class HttpRequest
{
public:
    HttpRequest() {}
    ~HttpRequest() {}
    void parse()    // 解析
    {
        // 1. 从inbuffer中拿到第一行,分隔符\r\n
        std::string line = Util::getOneline(inbuffer, sep);
        if(line.empty()) return;
        // 2. 从请求行中提取三个字段 -- 下面放开的三个
        std::cout << "line: " << line << std::endl; // 打印出来显示一下
        std::stringstream ss(line);                 // 可以直接以空格作为分隔符来进行分割
        ss >> method >> url >> httpversion;

        // 3. 添加web默认路径
        path = default_root;             // 未来可以进行修改  变成 ./wwwroot
        path += url;                     // 到这一步之后就会  变成 ./wwwroot/a/b/c.html
                                         // 未来访问路径都会从这个路径下开始访问
                                         // 这边会遇到一个问题,当url是一个 / 的时候就不行,拼接的时候会变成 ./wwwroot/ 没有具体目标
        if(path[path.size()-1] == '/') path += home_page;   // 加一个判断就行了
    }

public:
    std::string inbuffer;

    /*  我们可以细分许多字段,当需要什么就可以添加什么,这里为了简洁就不做这些工作了
    std::string reqline; // 请求行
    std::vector<std::string> reqheader; // 请求报头
    std::string body;   // 请求正文
    */

    std::string method; // 请求方法
    std::string url;
    std::string httpversion; // 请求版本
    std::string path;       // web默认路径
};

class HttpResponse
{
public:
    std::string outbuffer;
};

Util.hpp

#pragma once

#include <iostream>
#include <string>


class Util
{
public:
    // XXXX XXX XXX\r\nYYYYY  -- 格式
    // 第二个参数是分隔符,暴露在外部,让外部传进来
    static std::string getOneline(std::string &buffer, const std::string &sep)  // 类内静态方法可以直接使用 -- 为了方便写,就定义成静态的
    {
        auto pos = buffer.find(sep);
        if(pos == std::string::npos) return "";     // 没有找到分隔符
        std::string sub = buffer.substr(0, pos);    // [ ) 左闭右开 拿到这一行字段
        buffer.erase(0, sub.size() + sep.size());   // 删除这一行
        return sub;

    }
};

关于浏览器引起的一些认识

浏览器是一款工业级软件

        浏览器是一款工业级别的软件,它是电脑默认安装的软件当中开发工作量最大的软件!!!

        它的开发难度非常大!即使是一款简单的基本浏览器都是几百万行起步(操作系统上千万行),开发成本特别高,这一点ps、vs studio、cad、12306之类的软件都是几百万行起步(工业级别),这些软件一般都是国外的(确是得承认,这一点国外做的比较好),从国内很多的游览器都是套壳Chrome(开源)中可以看出来(上图中有显示)

那么我们有能力做出来吗?为什么不做?

        有!但是没必要!就算是操作系统我们国内业界也是有能力做出来的,但是没有意义!

        就好比,做一款软件要三年,花上100人,每年30万,一年就算三千万,开发成本就将近一个亿就没了,并且投入市场,并不是就立马见效,在这个赛道上已经有人做了,做了几十年,比你的好并且稳定,那么结果显而易见,成绩并不会好,可能就直接噶掉了,这不就是打水漂吗?所以没必要!这一点之前华为的鸿蒙因为套壳Linux被骂的很惨,其实没必要,我们知道重新开发就意味着新的生态(参考以前的文章),没有人用就等于白搭,明白这一点我们就知道这其实是怎么一个情况了

那当别人突然不给我们用软件呢?

        那就拿以前的版本做二次开发,这才是编程世界是最普遍的现象,也是最适合的方法

那我们的开发资源到哪里去了呢?

        在那些没有人做的领域中去,大家都是从零开始,把较大的风险规避,把那些资源投入到可能让我们领先的地方,这才是正确的做法

身为一个有见识的程序员,我们得知道这一点,千万不能道听途说,随意相信那些自媒体(为黑而黑),我们得有自己的观点,知行合一

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
回答: Unity Tcp协议和Http协议有一些差别。首先,Tcp协议是一种面向连接的协议,而Http协议是一种无连接的协议。这意味着在使用Tcp协议时,需要先建立一个连接,然后才能进行数据传输,而Http协议则不需要建立连接,每次请求都是独立的。其次,Tcp协议提供可靠的字节流服务,确保数据的可靠传输,而Http协议则不提供可靠性,可能会丢失数据。此外,Tcp协议使用重传策略来防止丢包,而Http协议没有这个机制。最后,在连接和关闭方面,Tcp协议使用三次握手建立连接和四次挥手关闭连接,而Http协议没有这个过程。所以,虽然Tcp和Http都是网络协议,但它们在连接方式、可靠性和关闭机制等方面有一些差别。 #### 引用[.reference_title] - *1* *2* [Unity-TCP篇-初识TCP](https://blog.csdn.net/qq_30868065/article/details/124187430)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] - *3* [Unity Modbus Tcp 通讯协议](https://blog.csdn.net/cq786/article/details/124769619)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

清风玉骨

爱了!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值