站点图标 AI技术聚合

【网络】http协议

🥁作者 华丞臧.
📕​​​​专栏【网络】
各位读者老爷如果觉得博主写的不错,请诸位多多支持(点赞+收藏+关注)。如果有错误的地方,欢迎在评论区指出。
推荐一款刷题网站 👉 LeetCode刷题网站

前言

在上一篇文章中我们见识了序列化和反序列化,其是处于应用层的,是进行网络通信双方进行一种约定;在网络发展至今,已经有了一些非常成熟的场景,并且有大佬自定义的一些协议,写的很好因此成为了应用层特定协议的标准,

一、http协议

虽然我们说,应用层协议是我们程序猿自己定的。但实际上,已经有大佬们定义了一些现成的,又非常好用的应用层协议,供我们直接参考使用,http(超文本传输协议)就是其中之一,底层主流采用的是tcp协议,http是无连接的。

1.1 认识URL

平时我们所说的网址就是URL。

服务器地址是域名,域名就是字符串类型的字段,域名在我们访问网网站的时候必须被转换称为IP地址,访问网络服务必须具有网络号即端口号,网络通信的本质就是socket,IP+port。
一般在使用确定协议的时候,再url上面会缺省端口号,在进行网络通信时端口号会被(浏览器)自动添加上,所以浏览器访问指定url时,浏览器必须给我们自动添加port,浏览器就必须知道这些端口,所以特定的众所周知的服务端口号都是并且必须是确定的。因此,在前面学习端口号时,我们说用户自己使用的套接字不能在0~1023。

http --> 80
https --> 443
sshd --> 22

http是做什么的

http是以网页的形式呈现的,比如说查阅文档、查看视频;所以http是获取网页资源的,视频、音频等也都是文件,而网页也是一个.html文件。

http是向特定服务器申请特定资源的,获取到本地进行展示或者某种使用的。

网页上的资源在该网页的网络服务器(软件)所在的服务器(硬件)上,而服务器都是Linux,资源文件存放在Linux服务器上;用户在网页上申请资源时,服务器需要根据路径找到该文件,所以URL中需要添加资源文件的路径。/是Linux下的路径分隔符,但是需要注意这个/不一定为根目录,因为软件层面可以做一些处理,比如在这个目录前添加一个路径。

urlencode和urldecode

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

“+” 被转义成了 “%2B”
urldecode就是urlencode的逆过程:
urlencode工具

网页的内容在开发者工具中可以看到,如下图:

1.2 http格式

http是基于行的协议。

http请求

http响应

1.3 http的方法

方法说明支持的HTTP协议版本
GET获取资源1.0、1.1
POST传输实体主体1.0、1.1
PUT传输文件1.0、1.1
HEAD获取报文首部1.0、1.1
DELETE删除文件1.0、1.1
OPTIONS询问支持的方法1.1
TRACE追踪路径1.1
CONNECT要求用隧道协议链接代理1.1
LINK建立和资源之间的联系1.0
UNLINE断开连接关系1.0

其中最常用的就是GET方法和POST方法.

1.4 http的状态码

类别原因短语
1XXInformational(信息状态码)接收的请求正在处理
2XXSuccess(成功状态码)请求正常处理完毕
3XXRedirection(重定向状态码)需要进行附加操作以完成请求
4XXClient Error(客户端错误状态码)服务器无法处理请求
5XXServer Error(服务器错误状态码)服务器处理请求出错

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

1.5 http常见Header

二、http服务器

2.1 最简单的http服务器

使用tcp协议的服务器端,在浏览器中可以连接该服务器,浏览器上默认使用http协议进行连接。在前几篇文章中已经编写了tcp套接字,这里只需要编写提供服务的代码即可。

#include <iostream>
#include <stdio.h>
#include <strings.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <pthread.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#include<sys/types.h>
#include<sys/wait.h>
#include "Log.hpp"

using namespace std;
volatile bool quitSer = false;

void Usage(void *vgs)
{
    std::cout << "Usage:./tcpserver port ip" << std::endl;
}
//提供服务
void Serverhttp(int sock)
{
    char buffer[10240];
    ssize_t s = read(sock, buffer, sizeof buffer);
    cout << s << endl;
    if(s > 0)
    {
        logMessage(DEBUG, "read success..");
        std::cout << buffer;
    }
}

class server
{
public:
    server(int port, std::string ip = "")
        : sockfd_(-1), ip_(ip), port_(port)
    {
    }
    ~server()
    {
    }

public:
    void init()
    {
        // 1. 创建套接字
        sockfd_ = socket(AF_INET, SOCK_STREAM, 0);
        if (sockfd_ < 0)
        {
            logMessage(FATAL, "socket:%s[%d]", strerror(errno), sockfd_);
            exit(SOCK_ERR);
        }
        logMessage(DEBUG, "socket success..");

        // 2.填充域
        struct sockaddr_in local;
        bzero(&local, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(port_);
        ip_.empty() ? (local.sin_addr.s_addr = INADDR_ANY) : (inet_aton(ip_.c_str(), &local.sin_addr));

        // 3. 绑定网络信息
        if (bind(sockfd_, (const struct sockaddr *)&local, sizeof(local)) < 0)
        {
            logMessage(FATAL, "bind:%s[%d]", strerror(errno), sockfd_);
            exit(BIND_ERR);
        }
        logMessage(DEBUG, "bind success...");

        // 4. 监听套接字
        if (listen(sockfd_, 5) < 0)
        {
            logMessage(FATAL, "listen:%s[%d]", strerror(errno), sockfd_);
            exit(LISTEN_ERR);
        }
        logMessage(DEBUG, "listen success...");

        // 完成
    }

    void start()
    {
        char inbuffer_[1024]; // 用来接收客户端发来的消息
        // 提供服务
        while (true)
        {
            quitSer = false;
            
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            // 5. 获取连接, accept 的返回值是一个新的socket fd
            logMessage(DEBUG,"start1");
            int serviceSock = accept(sockfd_, (struct sockaddr *)&peer, &len);
            logMessage(DEBUG,"start2");
            if (serviceSock < 0)
            {
                // 获取链接失败
                logMessage(WARINING, "accept: %s[%d]", strerror(errno), serviceSock);
                continue;
            }
            logMessage(DEBUG, "accept success");

            pid_t id = fork();
            if(id == 0)
            {
                pid_t tid = fork();
                if(tid == 0)
                {
                    uint16_t peerPort = htons(peer.sin_port);
                    std::string peerIp = inet_ntoa(peer.sin_addr);
                    Serverhttp(serviceSock);
                    exit(0);
                }

                exit(0);
            }
            close(serviceSock);
            int ret = waitpid(id, nullptr, 0);
            if(ret < 0)
            {
                logMessage(WARINING, "wait child error:%d", errno);
            }
            logMessage(DEBUG, "wait success...");
        }
    }

private:
    int sockfd_;
    uint16_t port_;
    std::string ip_;
};

// ./tcpserver port ip
int main(int argc, char *argv[])
{
    if (argc < 2 || argc > 3)
    {
        Usage(argv[0]);
        exit(USAGE_ERR);
    }

    uint16_t port = atoi(argv[1]);
    std::string ip;
    if (argc == 3)
        ip = argv[2];
    std::cout << port << ":" << ip << std::endl; 

    server tcpSer(port, ip);
    tcpSer.init();
    tcpSer.start();
    return 0;
}

服务端并没有提供服务,所以访问服务器时网页显示如下图:

在Linux服务端可以看到请求的相关属性,如下图:

2.2 简单的http服务器

服务器收到请求,提供服务向对应的套接字文件中写入一个“hello world”,注意响应的格式为首行+header+body,其中header和body之间需要空一行。

void Serverhttp(int sock)
{
    char buffer[10240];
    ssize_t s = read(sock, buffer, sizeof buffer);
    cout << s << endl;
    if(s > 0)
    {
        logMessage(DEBUG, "read success..");
        std::cout << buffer;
    }
	
	//开始响应
    string response;
    response = "HTTP/1.0 200 OK\r\n"; //响应首行
    response += "\r\n";   //空行
    response += "hello world!!";  //正文
    send(sock, response.c_str(), response.size(), 0);
}

服务器收到的请求报文如下图:

浏览器收到的响应如下图:

使用telnet可以查看服务器给用户的响应。

//安装telnet
sudo yum install -y telnet

//使用格式
telnet ip port
//connected后
//出现escape character时使用ctrl+}
//输入请求

2.3 html的http服务器

作为服务端,它并不关心html的信息也不需要管html的格式是什么,服务器只负责网络请求并且把网页信息给用户推过去就行了,所以实际上在进行网站开发时,网页的资源都是通过html文件推送给用户的,请求的文件在请求行中,其第二个字段就是用户需要访问的文件,如下图:

其中/并不是根目录,而是web根目录,可以设置成为根目录。

//获取资源路径
string getPath(string in)
{
    size_t pos = in.find(CRLF);
    string request = in.substr(0, pos);
    cout << "request:" << request << endl;
    // get path http/1.0
    size_t first = request.find(SPACE); //第一个空格
    cout << "first:" << first << endl;
    if(first == string:: npos) return nullptr;
    size_t second = request.rfind(SPACE); //第二个空格
    cout << "second:" << second << endl;

    if(second == string::npos) return nullptr;
    string path = request.substr(first + SPACE_LEN, second- (first + SPACE_LEN));
    if(path.size() == 1 && path[0] == '/') path += HOME_PAGE; //用户访问根目录,就返回主页
    cout << "getPath:" << path << endl;

    return path;
}
//获取资源
string readFile(string& recource)
{
    ifstream in(recource);
    if(!in.is_open()) return "404";
    string content;
    string line;
    while(getline(in, line)) content += line;
    return content;
}

//提供服务
void Serverhttp(int sock)
{
    //std::cout << peerIp << ":" << peerPort << std::endl;
    char buffer[10240];
    ssize_t s = read(sock, buffer, sizeof buffer);
    cout << s << endl;
    if(s > 0)
    {
        logMessage(DEBUG, "read success..");
        std::cout << buffer;
    }
    // 1. 获取路径
    string path = getPath(buffer);

    string recource = ROOT_PATH;
    recource += path;
    cout << recource << endl;
    
    // 获取对应文件内容
    string html = readFile(recource);

    // 开始响应
    string response;
    response = "HTTP/1.0 200 OK\r\n";
    response += "Content-Type:text/html\r\n";
    response += ("Content-Length:" + std::to_string(html.size()) + "\r\n");
    response += "\r\n";
    //response += "<html><h1>hello world!!</h1><html>\r\n";
    response += html;

    send(sock, response.c_str(), response.size(), 0);
}

关于html的编写方法可以参考👉菜鸟教程

//index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"> //
<title>华丞臧(runoob.com)</title> //网页名称
</head>
<body> //正文
    <h1>我的第一个标题</h1>   //标题
    <p>我的第一个段落。</p>
</body>
</html>

在浏览器中访问服务器可以看到网页显示如下图所示:


Liunx上收到的请求如下图,左为服务器收到的请求,右为客户端收到的响应。

2.4 表单

用户的网络行为无非有两种

  1. 用户将自己的远端资源拿到本地:get /index.html http/1.1;
  2. 用户想将自己的属性字段提交到远端。

在浏览器上,网页通常会让用户注册一个账号,用来标识该用户,用户的存放在远端的资源也可以使用该账号标识,在html中这种端口称为表单。

get

在http中get会以明文方式将用户对应的参数信息拼接到url中。

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>华丞臧(runoob.com)</title>
</head>
<body>
    <h1>hello world</h1>
    <p>我的第一个段落。</p>
    <form action="html_form_action.php" method="get">
        Username: <input type="text" name="user"><br>
        Password: <input type="password" name="passwd"><br>
        <input type="submit" value="Submit">
        </form>
</body>
</html>

表单形式如下图所示:

post

在http中post会以明文方式将用户对应的参数信息拼接到正文中。

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>华丞臧(runoob.com)</title>
</head>
<body>
    <h1>hello world</h1> 
    <p>我的第一个段落。</p>
    <form action="html_form_action.php" method="post">
        Username: <input type="text" name="user"><br>  //用户名,<br>标识换行
        Password: <input type="password" name="passwd"><br>  //密码
        <input type="submit" value="Submit">
        </form>
</body>
</html>

表单形式如下图所示:

在表单中填入用户名和密码后,可以看到url中并没有拼接用户参数,如下图:

使用progress telerik fiddler可以查看用户发送的请求报文(Raw),可以看到用户参数被拼接到正文当中,如下图:

在服务端也可以收到用户发来的请求,如下图:

get和post特点

不难发现,不管是get方法还是post方法都是不安全的,都是以明文的方式传输;如果有第三方来攻击用户,无疑能非常简单的获取用户的数据;与get方法相比,post方法只是更为私密但还是明文传输,不过post方法会将参数添加到请求的正文中,因此当用户需要传输大资源的时候无疑使用post方法更加合适。

2.5 永久和临时重定向

301:永久重定向。

void Serverhttp(int sock)
{
    char buffer[10240];
    ssize_t s = read(sock, buffer, sizeof buffer);
    cout << s << endl;
    if(s > 0)
    {
        logMessage(DEBUG, "read success..");
        std::cout << buffer;
    }
    string response = "HTTP/1.1 301 Permanent redirect\r\n";   //永久重定向
    response +="Location: https://blog.csdn.net/qq_59456417?spm=1011.2415.3001.5343r\n";
    response += "\r\n";
    send(sock, response.c_str(), response.size(), 0);
}

访问服务器结果如下:

302:临时重定向。

void Serverhttp(int sock)
{
    char buffer[10240];
    ssize_t s = read(sock, buffer, sizeof buffer);
    cout << s << endl;
    if(s > 0)
    {
        logMessage(DEBUG, "read success..");
        std::cout << buffer;
    }
    string response = "HTTP/1.0 302 Temporary redirect\r\n";   //临时重定向
    response +="Location: https://www.qq.com/r\n";
    response += "\r\n";
    send(sock, response.c_str(), response.size(), 0);
}

访问服务器结果如下:

2.6 Cookie

http状态特点之一:无状态,用户各种资源的请求http协议不会记录,也就是说http不知道用户访问服务器次数;虽然http不会记录,但http可以借助其他的手段来记录用户的访问。

为什么需要记录呢?

因为用户需要会话保持,网站中某些资源是需要进行用户认证的,如果访问不被记录,就会导致每次访问资源都需要进行用户认证,比如在腾讯视频上每次访问vip影视都需要用户进行一次登录这显然是非常麻烦的的,所以服务器需要保持用户会话。

我们可以登录一下bilibili如下所示,点击网址左边的锁,可以看到有一个Cookie,Cookie是一种浏览器会话保持的策略。

在Cookie类表中可以找到以bilibili开头的:

当你把Cookie中关于bilibili的全部删除,下一次再进入bilibili时就需要重新登陆了。

Cookie内容:

void Serverhttp(int sock)
{
    char buffer[10240];
    ssize_t s = read(sock, buffer, sizeof buffer);
    cout << s << endl;
    if(s > 0)
    {
        logMessage(DEBUG, "read success..");
        std::cout << buffer;
    }
    // 1. 获取路径
    string path = getPath(buffer);

    string recource = ROOT_PATH;
    recource += path;
    cout << recource << endl;
    
    string html = readFile(recource);

    // 开始响应
    string response;
    response = "HTTP/1.0 200 OK\r\n";
    response += "Content-Type:text/html\r\n";
    response += ("Content-Length:" + std::to_string(html.size()) + "\r\n");
    response += "Set-Cookie: this is my Cookie content\r\n";
    response += "\r\n";
    response += html;

    send(sock, response.c_str(), response.size(), 0);
}


Cookie就是浏览器给用户维护的一个文件,Cookie可以在磁盘上也可以在内存中,内存中的Cookie将浏览器关闭后Cookie就会释放,再次启动浏览器访问服务器时就需要重新登录。Cookie对用户的安全极大的影响,第三方如果拿到用户的Cookie,第三方就可以通过Cookie使用用户的身份登录服务器,更严重的第三方还是以通过Cookie拿到用户的用户名和密码。

Cookie策略存在极大的安全隐患,所以现在主流的方案不是Cookie,而是Cookie + session的方式。服务器将用户名和密码形成一个session文件,这个文件的文件名session_id在服务器上具有唯一性,服务器将session_id返回客户端,客户端将session_id写入本地的Cookie,后续访问都会通过session_id去访问服务器,此时用户的用户名和密码就是存放在服务器上,软件厂商来维护session和cookie,大公司有充足的动力和能力来保护自己不受攻击,并且还会有各种措施来防护。

补充:

文章出处登录后可见!

已经登录?立即刷新
退出移动版