HTTP协议详解

目录

前言

1、认识URL

2、HTTP请求与响应的报文格式

(1)请求格式

(2)响应格式

3、HTTP测试代码

4、HTTP的方法

(1)get与post的区别

5、HTTP的状态码

6、HTTP报头属性


前言

本文主要带着各位一起了解我们应用层一种重要的协议------HTTP协议;看完本文,你会了解,URL是什么、HTTP协议的报文组成、HTTP协议部分字段、GET与POST方法;

1、认识URL

想必大家可能都认识URL,通常我们把这个成为网址,实际上其全名为统一资源定位符。下面,我来带着大家一起认识URL;

其中端口号可以省略,因为这是众所周知的了,比如http协议默认端口号为80,https协议默认端口号为443,关于域名,就是IP地址,域名通过域名解析后会转化成IP地址,一个域名只有一个IP地址,而一个IP地址可以对应多个域名;我们通过域名(IP)+ 端口号的方式找到全网唯一进程,然后接着我们通过端口号后路径+资源文件名可以找到全网唯一一份资源,url同时也可以携带参数,?后为参数,其中参数之间用&分隔开;

补充:

encode与uncode

在我们url中可能携带一些符号与中文,那么对于这种情况又是如何进行处理的呢?我们做如下实现,我们在浏览器搜索框中输入搜索abcde,然后查看URL;

注意:不同搜索引擎生成的URL连接也不同,这里我分别用微软的bing和百度来进行搜索,分别生成如下两个URL;

我们可以查看到我们的检索词条出现在了URL的参数上,对于bing来说是q=检索词,对于百度来说是wd=检索词;接着我们在搜索C++;bing搜索结果如下;

我们发现,q后面的检索词条变成了C%2B%2B,我们将2B看作十六进制数,然后转换成十进制数为43,我们对比ASCII表发现,正是+;

我们将这个过程称为URL的Encode,具体规则为将数字和特殊字符转成ASCII值,以两个十六进制数为一位,然后转成%+ASCII值;
所谓的Uncode也就是刚才那个过程的逆向过程,网上有很多的URL Encode与 Uncode工具,大家也可以尝试着试一试;下面贴出了其中一个;

在线url网址编码、解码(ES JSON在线工具)

2、HTTP请求与响应的报文格式

(1)请求格式

请求格式主要由四部分构成,首先是请求行,请求行有三个字段,每个字段都由空格划分,其中第一个字段为请求方法,常见的方法有post、get方法;接着是URL,表示请求的资源,最后一个为协议版本,表示客户端使用的协议版本号,如HTTP/1.0,该行结束后有一个\r\n 表示结束;接着是请求报头,请求报头由若干个key value结构构成,表示这次请求报文所携带的属性;接着是一行换行符,这个换行符是为了以便区分正文与报头部分内容;

有了上面的理解,我们是可以很容易区分报头部分和正文部分,但是我们应该如何完整提取正文部分呢?我们又不知道正文部分有多大,实际上,在我们请求属性中,有一个属性叫Content-Length,该字段标识请求正文的大小;这样我们就可以完整的提取出一个报文了,如Conten-Length: 1024;

(2)响应格式

响应报文的格式与请求报文格式也类似,由四部分组成,分别为状态行、响应报头、空行以及相应正文;其中状态行中也有三个字段,以空格分割,以\r\n为结尾;第一个字段为协议版本,表示服务器端的协议版本号,第二个字段状态码,第三个字段为状态描述,如200,对应状态描述为 OK;接着又是若干个KeyValue结构的响应报头,表示该报文的若干属性;接着一行空行一分割报头与响应正文内容;最后就是响应正文,用来保存一些反馈给客户端的资源;

百闻不如一见,今天就带着各位写一个简单的小程序来观察HTTP协议;

3、HTTP测试代码

我们这个demo代码有如下文件,我们简单的就想看一个HTTP请求是什么样子,我们通过浏览器发送HTTP请求,我们收到请求后打印出来即可;

下面展示我们所写代码;

cpp 复制代码
// Makefile文件
httpserver:HttpServer.cc
	g++ -o $@ $^ -std=c++11

.PHONY:clean
clean:
	rm -f httpserver
cpp 复制代码
// Err.hpp文件
#pragma once

#define ARGS_ERR 1     // 参数错误
#define SOCK_ERR 2     // 创建套接字失败
#define BIND_ERR 3     // 绑定失败
#define LISTEN_ERR 4   // 监听失败
#define ACCEPT_ERR 5   // 接受连接失败
#define CONNECT_ERR 6  // 连接失败
cpp 复制代码
// Log.hpp文件
#pragma once

#include <cstdarg>
#include <cstdio>
#include <ctime>

#define Info 1
#define Debug 2
#define Warning 3
#define Error 4
#define Fatal 5

const char *getLevel(int level)
{
    switch (level)
    {
    case Info:
        return "Info";
    case Debug:
        return "Debug";
    case Warning:
        return "Warning";
    case Error:
        return "Error";
    case Fatal:
        return "Fatal";
    }
    return "Unknow";
}

bool LogMessage(int level, const char *fmt, ...)
{
    va_list args;
    va_start(args, fmt);
    char buf[1024];
    int n = vsprintf(buf, fmt, args);
    if (n < 0)
        return false;
    // 获取时间
    time_t timestamp = time(nullptr);
    struct tm *cur_time = localtime(&timestamp);
    
    // 打印输出
    printf("[%s][%d-%d-%d %d:%d:%d] %s\n", getLevel(level), 1900 + cur_time->tm_year, cur_time->tm_mon,\
        cur_time->tm_mday, cur_time->tm_hour, cur_time->tm_min, cur_time->tm_sec, buf);

    // 往文件打印
    // FILE* pf = fopen("./Log/server.log", "a");
    // fprintf(pf, "[%s][%d-%d-%d %d:%d:%d] %s\n", getLevel(level), 1900 + cur_time->tm_year, cur_time->tm_mon,\
    //      cur_time->tm_mday, cur_time->tm_hour, cur_time->tm_min, cur_time->tm_sec, buf);
    // fclose(pf);
    return true;
}
cpp 复制代码
// Sock.hpp文件
#pragma once
#include <iostream>
#include <cstring>
#include <cerrno>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Log.hpp"
#include "Err.hpp"

class Sock
{
    static const int g_backlog = 10;
public:
    Sock(){}
    int Socket()
    {
        int listenSock = socket(AF_INET, SOCK_STREAM, 0);
        if(listenSock < 0)
        {
            LogMessage(Fatal, "create socket fail, errno:%d, errstr:%s", errno, strerror(errno));
            exit(SOCK_ERR);
        }
        LogMessage(Info, "create socket success, listenSock:%d", listenSock);
        return listenSock;
    }
    int Bind(int listenSock, const std::string& ip, uint16_t port)
    {
        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_addr.s_addr = inet_addr(ip.c_str());
        local.sin_port = htons(port);
        int n = bind(listenSock, (struct sockaddr*)&local, sizeof(local));
        if(n < 0)
        {
            LogMessage(Fatal, "bind sock fail, errno:%d, errstr:%s", errno, strerror(errno));
            exit(BIND_ERR);
        }
        LogMessage(Info, "bind socket success!");
        return n;
    }
    int Listen(int listenSock)
    {
        int n = listen(listenSock, g_backlog);
        if(n < 0)
        {
            LogMessage(Fatal, "listen sock fail, errno:%d, errstr:%s", errno, strerror(errno));
            exit(LISTEN_ERR);
        }
        LogMessage(Info, "listen socket success!");
        return n;
    }
    int Accept(int listenSock, std::string* ip, uint16_t* port)
    {
        struct sockaddr_in peer;
        memset(&peer, 0, sizeof(peer));
        socklen_t len = sizeof(peer);
        int sockfd = accept(listenSock, (struct sockaddr*)&peer, &len);
        if(sockfd < 0) return -1;
        if(ip)
            *ip = inet_ntoa(peer.sin_addr);
        if(port)
            *port = ntohs(peer.sin_port);
        LogMessage(Info, "Accept socket success, sockfd:%d", sockfd);
        return sockfd;
    }

    int Connect(int sockfd, const std::string ip, uint16_t port)
    {
        struct sockaddr_in server;
        memset(&server, 0, sizeof(server));
        server.sin_family = AF_INET;
        server.sin_addr.s_addr = inet_addr(ip.c_str());
        server.sin_port = htons(port);
        int n = connect(sockfd, (struct sockaddr*)&server, sizeof(server));
        LogMessage(Info, "connect socket success!");
        return n;
    }

};
cpp 复制代码
// HttpServer.hpp文件
#pragma once
#include <functional>
#include <unistd.h>
#include <signal.h>
#include "Sock.hpp"
#include "Log.hpp"

using func_t = std::function<void(int)>;


class HttpServer
{
public:
    HttpServer(uint16_t port, func_t func, const std::string& ip = "0.0.0.0")
        :_func(func)
    {
        _listenSock = _sock.Socket();
        _sock.Bind(_listenSock, ip, port);
        _sock.Listen(_listenSock);
    }
    void start()
    {
        std::cout << "start begin\n";
        signal(SIGCHLD, SIG_IGN);
        while(true)
        {
            uint16_t port;
            std::string ip;
            int sockfd = _sock.Accept(_listenSock, &ip, &port);
            if(sockfd == -1)
            {
                LogMessage(Error, "Accept fail, errno:%d, errstr:%s", errno, strerror(errno));
                continue;
            }
            if(fork() == 0)
            {
                // child
                close(_listenSock);
                _func(sockfd);
                close(sockfd);
                exit(0);
            }
            // father
            close(sockfd);
        }
    }
private:
    Sock _sock;
    int _listenSock;
    func_t _func;  // 回调函数
};
cpp 复制代码
// HttpServer.cc文件
#include <iostream>
#include <vector>
#include <fstream>
#include <string>
#include <memory>
#include <cstdlib>
#include <cerrno>
#include <cstring>
#include "HttpServer.hpp"
#include "Err.hpp"
#include "Utility.hpp"

#define ROOT "./WWWROOT"

static void Usage(const std::string& str)
{
    std::cout << "Usage:\n" << str << " serverPort" << std::endl;
}

void transaction(int sockfd)
{
    char buf[1024];
    // 暂不考虑不能获取一个完整报文
    int n = recv(sockfd, buf, sizeof(buf) - 1, 0);
    if(n < 0)
    {
        LogMessage(Error, "recv fail, errno:%d, errstr:%s", errno, strerror(errno));
        return ;
    }
    buf[n] = 0;
    // 打印请求
    std::cout << buf << std::endl;
}

int main(int argc, char* argv[])
{
    if(argc != 2)
    {
        Usage(argv[0]);
        exit(ARGS_ERR);
    }
    std::unique_ptr<HttpServer> phs(new HttpServer(atoi(argv[1]), transaction));

    phs->start();

    return 0;
}

编译上述代码后我们运行我们的服务器并设置端口为8888,接着在浏览器中输入我们的IP:端口号,接着我们会发现确实会打印,如下所示;

我们发现请求中中的URL连接为反斜杠,这是因为我们在浏览器中未输入任何文件资源路径,因此表示默认访问web根目录,所以显示反斜杠;再仔细观察,发现我们使用的是HTTP/1.1协议,且我们使用get请求方法;接着是一个请求报头,存放都是本报文的属性;

4、HTTP的方法

前面我们提到的get与post是HTTP方法中的一种,接下来我来详细介绍一些HTTP常见方法;
get:获取资源;

post:上传资源;

head:获取报文头部
我们上网无非两个目的,一个是获取网络资源,通过这种我们采用get方法,另一个是将我们的数据资源上传,对于这种情况我们既使用get方法,又使用post方法,那么这两种方法到底有什么区别呢?

(1)get与post的区别

我们对上述代码进行拓展,当客户端访问服务端时,我们默认返回首页资源文件,通常这个首页资源文件未HTML文件,如下所示;

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>首页</title>
</head>
<body>
    <h1>这是首页</h1>
    <form action="./page1.html" method="get">
        User:<input type="text" name="User">
        <br>
        Password:<input type="password", name="password">
        <br>
        <input type="submit", name="提交">
    </form>
</body>
</html>

**注意:**上述文件,我们存在一个专门的目录下,这个目录我们便称之为web根目录,如下所示;

我们再添加一个文件Utility.hpp,该文件放着一些比较实用的函数,目前有切割字符串,如下所示;

cpp 复制代码
// Utility.hpp文件
#pragma once
#include <vector>
#include <string>

class Utility
{
public:
    static void cutString(const std::string& str, const std::string& sep, std::vector<std::string>* v)
    {
        size_t start = 0;
        size_t pos = 0;
        while(true)
        {
            pos = str.find(sep, start);
            if(pos == std::string::npos)
                break;
            v->push_back(str.substr(start, pos - start));
            start = pos + sep.size();   
        }
        if(start < str.size())
        {
            v->push_back(str.substr(start, pos - start));
        }
    }

};

我们再对业务处理函数进行一些修改,我们收到HTTP请求后,提取访问的资源路径,并找到对应资源文件,返回资源文件内容;

我们果然获取了一张网页,我们可以按下F12,返回的正是我们缩写的HTML源码;我们点击提交按钮,如下所示;

我们发现我们的刚才所提交的数据都完整的放在了URL中,我们将方法改为post,再次提交查看结果;如下所示;

我们发现,此时我们提交的数据并没有放在URL中,那么我们提交的数据又放在了哪里呢?我们使用post方法是否更加安全呢?

对于更加安全这种说法我并不是很赞同,我认为post方法相对于get方法,更加私密,post提交的数据并不会放在URL中,而会放在请求正文中,如下所示;

5、HTTP的状态码

关于HTTP的状态码,我们仅仅需要做一些了解即可;常见的有如下五种开头的;

1xx:信息性状态码

2xx:成功状态码

3xx:重定向状态码

4xx:客户端错误状态码

5xx:服务器错误状态码

这里介绍一些常见状态码;

状态码 对应描述 意义

200 OK 成功运行

301 Moved Permanently 永久移动

307 Temporary Redirect 临时移动

404 Not Found 找不到对应资源

6、HTTP报头属性

这里主要介绍一些比较常见的属性,如下所示;

Content-Length:正文大小;

Content-Type:正文资源的类型(比如HTML/Png/Jpg/CSS等);

Host:客户端请求资源的IP与端口号;

User-Agent:声明用户的操作系统和浏览器版本信息;

Reffer:从哪个页面跳转过来的;

location:搭配3xx字段,告诉客户端接下来应该去哪;

Cookie:用于客户端存储少量信息,通常用于会话功能的实现;
这里我想着重介绍其中Cookie这个字段;该字段用于会话管理,要理解会话管理首先要知道我们为什么要进行会话管理?

关于为什么要进行会话管理首先我们得清楚,HTTP协议的特点,HTTP协议具有无连接,无状态的特点;所谓无连接,就是HTTP请求与响应过程中,并没有建立连接;可有的小伙伴就疑惑了,HTTP不是基于TCP协议吗?怎么会没有连接呢?其实,TCP的连接是基于传输层的连接,跟我们应用层HTTP协议没有任何关系;关于HTTP的无状态就是我们HTTP协议并不会记录上一次请求或响应,也不会知道对方是否曾经向我发送过请求;
在聊聊我们的会话管理,我们用一个例子来理解我们的会话管理;例如我们的CSDN,当我们进行登录后,即使我们关闭浏览器后,我们再打开CSDN官网,我们会发现我们仍然是登录状态,这就是所谓的会话管理;若没有会话管理,则我们每次访问一个需要登录的资源时都需要重新登录;因为HTTP是无状态的,服务端并不知道你的信息,因此无法知道你是否有权限访问对应资源;因此我们每次请求对应资源都要进行身份验证,这样对用户来说,非常影响体验,因此早期诞生了如下方案;

在我们输入账号和密码之前,本地客户端会生成一个cookie文件来保存账号和密码(也有可能保存在内存中),接着每次当我们发送http请求时,客户端首先会从cookie文件中获取账号和密码,这样每次请求就都不用用户手动进行身份验证了;
可是这样也同时带来了不少的安全隐患,若用户的设备被植入了木马病毒,此时,黑客可以轻松获取cookie文件中的内容,此时用户的私人信息就会遭受泄露;这样是我们所不能容忍的;因此又有了另一种方案,如下所示;

当我们进行账号密码登录时,我们的客户端会发送http请求,当服务端收到http请求后,对用户身份进行验证,若验证通过,则会生成一个唯一session id,服务端会用该id生成一个类,并再里面保存该用户的信息,做完这一系列工作后,再将这个sid返回给客户端,客户端将这个sid保存在本地的cookie文件或内存中,从此以后客户端都用这个sid进行访问;
这时肯定有人就有疑惑了,那这个sid不同样也可以被盗取吗?若黑客将我们的sid盗取不同样也可以登录我们的账户吗?确实如此,当时即便登录了我们的账户,也无法知道我们的账户密码等私密信息,因此该信息保存在服务端,而且,实际上,服务端也可以获取客户端登录位置,若登录位置有异常通常会给客户端发送信息,想必大家也会见过这种现象吧,例如企鹅;
说了这么多,不是一次实操,我们可以通过在响应中设置set-Cookie属性,我们再查看之后的访问中,客户端是否会发送Cookie信息(客户端为浏览器);

相关推荐
萤火夜1 小时前
Linux之网络基础
网络
xxtzaaa1 小时前
住宅IP怎么在指纹浏览器设置运营矩阵账号
网络·网络协议·tcp/ip
okmacong1 小时前
3、集线器、交换机、路由器、ip的关系。
网络·tcp/ip·智能路由器
添砖java_8572 小时前
TCP流套接字编程
网络·网络协议·tcp/ip
TimberWill2 小时前
字符串-07-判断两个IP是否属于同一子网
java·网络协议·tcp/ip
ZachOn1y2 小时前
计算机网络:运输层 —— TCP 的超时重传机制
网络·网络协议·tcp/ip·计算机网络·tcp·超时重传
稻草人ZZ2 小时前
Keepalived部署
linux·服务器·网络·keepalived
黑客Ash2 小时前
网络安全提示
网络·安全·web安全
在路上走着走着3 小时前
clickhouse数据库,http请求访问,支持参数化
数据库·clickhouse·http