HTTP协议

目录

前言

1.HTTP协议

2.认识URL

3.urlencode和urldecode

4.HTTP的请求和响应格式

5.编码验证

6.HTTP的方法

7.HTTP的状态码

8.HTTP常见Header

总结


前言

之前基于传输层协议UDP和TCP我们在应用层实现了数据传输,并且使用UDP协议实现了一个翻译服务器,使用TCP协议实现了一个网络版本计算器,因为是使用TCP协议,所以在实现网络版本计算器的时候定制了协议,实现了序列化和反序列化的工作,而今天要为大家介绍的HTTP协议是基于传输层TCP协议,HTPP协议是已经有人在应用层实现的协议,主要用途是在web端,可以明确的一点是HTTP协议是基于传输层TCP协议实现的,那必然要解决协议定制和实现序列化和反序列化的工作,那么下面我们就一起来具体看看HTTP协议是如何实现的。

1.HTTP协议

HTTP协议是在应用层人们早已制定好了的成熟协议,主要用途是使用HTTP协议客户端可以向服务端请求一些资源,包含文本,音频,图片等资源,客户端发起请求,服务端根据客户端的请求向服务端返回资源,所以HTTP协议被称为是超文本传输协议。

2.认识URL

平时我们俗称的 "网址" 其实就是说的 URL,使用HTTP协议,客户端向服务端发起请求是以URL的形式进行请求。

如图所示:

一般URL中会包含服务器地址,服务器端口号,以及请求资源的路径,这里的路径本质上是Linux中存放资源的位置。

3.urlencode和urldecode

像 / ? : 等这样的字符, 已经被url当做特殊意义理解了. 因此这些字符不能随意出现.
比如, 某个参数中需要带有这些特殊字符, 就必须先对特殊字符进行转义.
转义的规则如下:
将需要转码的字符转为16进制,然后从右到左,取4位(不足4位直接处理),每2位做一位,前面加上%,编码成%XY格式

例:urlencode

"+" 被转义成了 "%2B"
urldecode就是urlencode的逆过程;

4.HTTP的请求和响应格式

HTTP请求和响应宏观结构如图所示:

HTTP请求包含四部分:请求行,请求报头,空行,请求正文

HTTP响应包含四部分:状态行,响应报头,空行,响应正文

既然HTTP协议是基于TCP协议实现的,那么必然要解决定制协议和序列化和反序列化的过程,首先定制协议是为了在应用层完整的读取到一个报文,按照HTTP的请求宏观结构:

a.以\r\n结尾,可以完整的读取到一行

b.以空行作为结尾,可以循环读取,知道空行,可以将所有的请求行和请求报头读完

c.如何保证把正文读完,报头有一个属性Content-Length:XXX正文的长度

d.解析出来内容长度,再根据内容长度,读取正文即可

请求和响应实现序列化和反序列化

HTTP自己实现的,将请求行和请求报头按照"\r\n",将一个个的字符串全部拼接起来就可以了

5.编码验证

说明:使用HTTP协议只需要实现一个服务端就可以了,客户端可以使用浏览器充当,客户端是默认支持HTTP协议的。

服务器实现:

因为是验证HTTP请求报头格式,所以我们的实现思路是,服务端和客户端建立连接成功后,此时只是为了测试,所以就不再进行定制协议,序列化和反序列化的过程,而是直接默认大概率可以读取到一个完整的报文,直接将读取上来的报文放到读取缓冲区中,然后采用回调的方式处理请求,为了测试,直接将请求格式打印出来,然后观察验证是否和前面所说的http请求宏观格式对应

cpp 复制代码
#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 "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;

    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);
            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);
            local.sin_addr.s_addr = INADDR_ANY;
            if (bind(_listensock, (struct sockaddr *)&local, sizeof(local)) < 0)
            {
                exit(BIND_ERR);
            }

            // 3. 设置socket 为监听状态
            if (listen(_listensock, gbacklog) < 0) // 第二个参数backlog后面在填这个坑
            {
                exit(LISTEN_ERR);
            }
        }
        void HandlerHttp(int sock)
        {
            // 1. 读到完整的http请求
            // 2. 反序列化
            // 3. httprequst, 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;
                _func(req, resp); // req -> resp
                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)
                pid_t id = fork();
                if (id == 0) // child
                {
                    close(_listensock);
                    if(fork()>0) exit(0);
                    HandlerHttp(sock);
                    close(sock);
                    exit(0);
                }
                close(sock);

                // father
                waitpid(id, nullptr, 0);
            }
        }
        ~HttpServer() {}

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

} // namespace server

运行截图:

如上图所示,可以观察到HTTP的请求格式与我们上面所说的是相符的,首行是请求行,接下来是请求报头,是冒号分割的键值对;每组属性之间使用\n分隔;遇到空行表示Header部分结束,然后是空行,然后是请求正文,因为是测试,所以没有请求正文的信息,接下来我们加上请求正文在进行观察。

说明:客户端向服务端发起请求的时候是需要获取资源的,所以需要客户端在访问的时候指明路径,当服务端收到客户端的请求后,对请求资源路径做提取,服务端上默认是存在一个根目录wwwroot,先给服务器访问资源的路径拼接上wwwroot,然后再追加url,然后根据这个路径访问Linux上的资源,如果是合法请求,并且该资源存在就客户端返回这个资源,如果该资源不存在,wwwroot路径下一定存在一个资源就是404.html,将该资源返回给客户端,告诉客户端你访问的资源不存在,如果是客户端直接访问根目录(/),服务器收到请求之后,wwwroot路径拼接好之后,后面再会追加一个wwwroot目录下一定存在的一个文件index.html,将该资源返回给服务端,下面我们来一起验证以下:

代码编写:

protocol.hpp

cpp 复制代码
#pragma once

#include <iostream>
#include <string>
#include <vector>
#include <sstream>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include "Util.hpp"
const std::string sep = "\r\n";
const std::string default_root = "./wwwroot"; //默认根目录
const std::string home_page = "index.html"; //默认根目录首页
const std::string html_404 = "404.html";

class HttpRequest
{
public:
    HttpRequest() {}
    ~HttpRequest() {}
public:
     //对字符串进行解析
    void parse()
    {
        // 1. 从inbuffer中拿到第一行,分隔符\r\n
        std::string line = Util::getOneLine(inbuffer, sep);
        if(line.empty()) return;
        // 2. 从请求行中提取三个字段
        //stringstream:默认以空格将字符串的内容写到里面
        std::stringstream ss(line);
        ss >> method >> url >> httpversion;

        // 3. 添加web默认路径
        path = default_root; // ./wwwroot, 
        path += url; //./wwwroot/a/b/c.html, ./wwwroot/
        //以/结尾,添加index.html
        if(path[path.size()-1] == '/') path += home_page;
        auto pos = path.rfind(".");
        if (pos == std::string::npos)
            suffix = ".html";
        else
            suffix = path.substr(pos);

        // 5. 得到资源的大小
        struct stat st;
        int n = stat(path.c_str(), &st);
        if(n == 0) size = st.st_size;
        else size = -1;   
    }
public:
    std::string inbuffer;
    std::string method;
    std::string url;
    std::string httpversion;
    std::string path;
    std::string suffix;
    size_t size;
    std::string parm;
};



class HttpResponse
{
public:
    HttpResponse() {}
    ~HttpResponse() {}
public:
    std::string outbuffer;
};

httpServer.cc:

cpp 复制代码
#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";
}
std::string suffixToDesc(const std::string suffix)
{
    std::string ct = "Content-Type: ";
    if (suffix == ".html")
        ct += "text/html";
    else if (suffix == ".jpg")
        ct += "application/x-jpg;image/jpeg";
    ct += "\r\n";
    return ct;
}
bool Get(const HttpRequest &req, HttpResponse &resp)
{
    //客户端向服务端的请求格式:
    cout << "----------------------http start---------------------------" << endl;
    cout << req.inbuffer << std::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;
    std::cout << "suffix: " << req.suffix << std::endl;
    std::cout << "size: " << req.size << "字节" << std::endl;
    cout << "----------------------http end---------------------------" << endl;

    //服务端给客户端返回的请求格式
    std::string respline = "HTTP/1.1 200 OK\r\n";
    std::string respheader = suffixToDesc(req.suffix);
    if (req.size > 0)
    {
        respheader += "Content-Length: ";
        respheader += std::to_string(req.size);
        respheader += "\r\n";
    }
    std::string respblank = "\r\n";
    std::string body;
    //在wwwroot路径下查找是否存在请求的资源,存在则将该资源返回
    if (!Util::readFile(req.path, &body))
    {
        //不存在就返回404.html
        Util::readFile(html_404, &body); // 一定能成功
    }
    resp.outbuffer += respline;
    resp.outbuffer += respheader;
    resp.outbuffer += respblank;
    resp.outbuffer += body;

    return true;
}

// ./httpServer 8080
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;
}

6.HTTP的方法

说明:客户端向服务端发起请求的时候,请求行第一个字段就是请求方法,请求方法不同代表着不同的含义,如下图所示:

一般常用的就是GET和POST方法

区别:

GET通过url传递参数,具体:http//ip:port/XXX/YY?name1=value1&name2=value2

POST提交参数通过http请求的正文提交参数

POST方法通过正文提交参数,所以一般用户看不到,私密性更好,GET方法不私秘

无论是GET和POST方法,都不安全,要谈安全,需要使用https协议

7.HTTP的状态码

最常见的状态码, 比如 200(OK), 404(Not Found), 403(Forbidden), 302(Redirect, 重定向), 504(Bad Gateway)

8.HTTP常见Header

Content-Type: 数据类型(text/html等)
Content-Length: Body的长度
Host: 客户端告知服务器, 所请求的资源是在哪个主机的哪个端口上;
User-Agent: 声明用户的操作系统和浏览器版本信息;
referer: 当前页面是从哪个页面跳转过来的;
location: 搭配3xx状态码使用, 告诉客户端接下来要去哪里访问;

如图所示:


Connection: keep-alive:标识长连接
通过浏览器向服务端请求资源,服务端返回资源,在请求的时候客户端可能同时请求多个资源,即进行多次http请求,但是http是基于tcp协议实现,而tcp是面向连接的,所以就可能会存在频繁创建连接的问题,为了解决这个问题,服务端和客户端都支持长连接,当获取一大份资源的时候,通过一条连接完成。

Cookie: 用于在客户端存储少量信息. 通常用于实现会话(session)的功能;

会话保持严格意义上不是http天然具备的,是后面使用发现需要的,因为http协议是无状态的。

如何理解http协议是无状态的,举个简单的例子,当我们访问bilibili服务端的时候,客户端可能需要登录,当登录完成之后,退出客户端之后,再次登录客户端,可以发现用户的登录信息任然是存在的,对于http协议来说,每次在访问的时候都应该重新发起请求,即重新登录,但是对于用户来说查看新的网页是常规操作,如果发生网页跳转,那么新的页面也就无法识别是哪一个用户了,为了让用户一经登录,可以在整个网站按照自己的身份进行随意访问,就需要解决http无状态的问题了

解决方式:

如图所示:

存在的问题:

可能会有黑客在你的服务上部署一些病毒,然后将你的用户名和密码获取到,此时黑客就可以拿着你的用户名和密码冒充你从事一些违法活动。

改进:

当浏览器向服务端进行登录之后,服务端会形成一个session文件,该文件中保存用的用户名和密码等认证信息,浏览器在cookie文件中保存sessionid,当再次请求的时候,请求报头中会携带sessionid,然后服务端进行认证,采用这种方案就基本上可以解决用户名和密码泄漏的问题,但是有小伙伴可能会问,黑客既然用户名和密码获取不了,但是可以获取sessionid,依旧可以冒充身份从事非法活动,对于这个问题的解决方式采取了相应的策略,其中包含会对sessionid的ip地址识别,当发现sessionid的ip地址和之前的不一样了,会将当前sessionid失效,通过这种相关策略就可以解决sessionid被别人获取冒充身份了。

总结

以上就是关于HTTP我们要介绍的所有内容了,包含什么是HTTP,如何使用HTTP请求资源,理解了HTTP的相关字段,以及获取网页资源是如何实现的等相关话题,感谢大家的阅读,我们下次再见。

相关推荐
hgdlip1 小时前
如何快速切换电脑的ip地址
网络·tcp/ip·电脑
程序员-珍2 小时前
虚拟机ip突然看不了了
linux·网络·网络协议·tcp/ip·centos
4647的码农历程3 小时前
Linux网络编程 -- 网络基础
linux·运维·网络
向李神看齐3 小时前
RTSP协议讲解
网络
Death2003 小时前
使用Qt进行TCP和UDP网络编程
网络·c++·qt·tcp/ip
魏大橙4 小时前
linux RCE本地/公网测试
网络·网络协议·udp
l1x1n04 小时前
No.3 笔记 | Web安全基础:Web1.0 - 3.0 发展史
前端·http·html
陈逸轩*^_^*4 小时前
Java 网络编程基础
java·网络·计算机网络
l1x1n05 小时前
网络安全概述:从认知到实践
网络
鄃鳕5 小时前
HTTP【网络】
网络·网络协议·http