HTTP协议初识·上篇

目录

认识URL

urlencode和urldecode

如何编码解码和验证过程

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

代码验证请求与响应

准备工作

HTTPServer.hpp

Protocol.hpp

makefile

1请求

HTTPServer.hpp

1.0函数handlerHttp-基本流程

再次处理

HttpServer.cc(新建文件)

测试1 -- 请求测试

云服务器响应1

云服务器响应2

云服务器响应2解析

手机测试解析

游览器推送解析

爬虫原理

2响应

一个简单的网页

修改一下

测试2 -- 响应测试

telnet,一个测试工具,有兴趣可以去了解

客户端视角

服务端视角

浏览器请求视角

乱码解决方法

添加报头

测试3 -- 乱码解决

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

3分割字段

准备工作

新文件Util.hpp

2.0新函数getOneline分割出一行一行的字段

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

HttpServer.cc修改

2.2打印工作放到外面来

测试结果4分割字段

注意细节

5修改默认web起始目录

补充知识

wwwroot是一个目录

http.conf是一个配置文件

5.1parse修改

6首页设置

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

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

6.3再加一个path的打印

测试5路径拼接测试

1拼接路径

2拼接首页

全部源码

HttpServer.cc

HttpServer.hpp

makefile

Protocol.hpp

Util.hpp

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


认识URL

HTTP协议初识·上篇_第1张图片

urlencode和urldecode

HTTP协议初识·上篇_第2张图片

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


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

实例

HTTP协议初识·上篇_第3张图片

wd:word缩写

如何编码解码和验证过程

HTTP协议初识·上篇_第4张图片

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

HTTP协议初识·上篇_第5张图片

代码验证请求与响应

准备工作

当前准备代码是基于上篇文章删改过来的,一个干净的多线程服务器

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

HTTPServer.hpp

#pragma once

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#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;    // 回调

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

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-基本流程

HTTP协议初识·上篇_第6张图片

再次处理

HTTP协议初识·上篇_第7张图片

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

HttpServer.cc(新建文件)

#include "HttpServer.hpp"
#include 

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 httpsvr(new HttpServer(Get, port));
    httpsvr->initServer();
    httpsvr->start();

    return 0;
}

测试1 -- 请求测试

云服务器响应1

HTTP协议初识·上篇_第8张图片

云服务器响应2

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

HTTP协议初识·上篇_第9张图片

云服务器响应2解析

HTTP协议初识·上篇_第10张图片

手机测试解析

HTTP协议初识·上篇_第11张图片

游览器推送解析

HTTP协议初识·上篇_第12张图片

爬虫原理

HTTP协议初识·上篇_第13张图片

2响应

一个简单的网页

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

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

 HTTP协议初识·上篇_第14张图片




    
    
    Document


    

修改一下

HTTP协议初识·上篇_第15张图片

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

测试2 -- 响应测试

telnet,一个测试工具,有兴趣可以去了解

客户端视角

HTTP协议初识·上篇_第16张图片

服务端视角

HTTP协议初识·上篇_第17张图片

浏览器请求视角

HTTP协议初识·上篇_第18张图片

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

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

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

乱码解决方法

HTTP协议初识·上篇_第19张图片

HTTP协议初识·上篇_第20张图片

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

添加报头

HTTP协议初识·上篇_第21张图片

测试3 -- 乱码解决

HTTP协议初识·上篇_第22张图片

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

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

HTTP协议初识·上篇_第23张图片

3分割字段

HTTP协议初识·上篇_第24张图片

 HTTP协议初识·上篇_第25张图片

准备工作

HTTP协议初识·上篇_第26张图片

新文件Util.hpp

2.0新函数getOneline分割出一行一行的字段

HTTP协议初识·上篇_第27张图片

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

后者是一种流 

HTTP协议初识·上篇_第28张图片

HTTP协议初识·上篇_第29张图片

HttpServer.cc修改

2.2打印工作放到外面来

HTTP协议初识·上篇_第30张图片

测试结果4分割字段

HTTP协议初识·上篇_第31张图片

注意细节

HTTP协议初识·上篇_第32张图片

5修改默认web起始目录

补充知识

HTTP协议初识·上篇_第33张图片

wwwroot是一个目录

HTTP协议初识·上篇_第34张图片

http.conf是一个配置文件

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

default-root

5.1parse修改

HTTP协议初识·上篇_第35张图片

6首页设置

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

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

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

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

HTTP协议初识·上篇_第36张图片

6.3再加一个path的打印

HTTP协议初识·上篇_第37张图片

测试5路径拼接测试

1拼接路径

HTTP协议初识·上篇_第38张图片

2拼接首页

HTTP协议初识·上篇_第39张图片

全部源码

HttpServer.cc

#include "HttpServer.hpp"
#include 

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 = "for test

七夕节日我竟然在搞这个?

好寂寞~

"; // 直接拼接就可以了,本身很简单 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 httpsvr(new HttpServer(Get, port)); httpsvr->initServer(); httpsvr->start(); return 0; }

HttpServer.hpp

#pragma once

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#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;    // 回调

    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 
#include 
#include 
#include  // 可以直接以空格作为分隔符来进行分割

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


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被骂的很惨,其实没必要,我们知道重新开发就意味着新的生态(参考以前的文章),没有人用就等于白搭,明白这一点我们就知道这其实是怎么一个情况了

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

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

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

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

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

你可能感兴趣的:(http,网络协议,网络)