网络 | http协议介绍 | cookie策略讲解

上篇博客定制了一个协议,该协议用来进行简单的计算,其包含了数据的序列化和反序列化,编码和解码的定制,并且该协议基于TCP通信,是一种客户端请求服务端响应的模型。如果你也实现了一个自己的协议,肯定会有疑问:自己定制的协议不够成熟,而且应用场景有限,有没有一个成熟且应用场景广泛的协议?这还真有,它就是上网必须使用的协议——http,https

url统一资源定位符

要了解http就要先了解url
在这里插入图片描述
协议名:表示该url采用的协议
登录信息:一般在url中被隐去,其体现在http请求的报头或者正文当中
服务器地址:也称域名,域名最终会被解析为IP地址
服务器端口号:网络通信的本质是进程间通信,所以需要告知IP+端口号,使通信能够进行。但是由于服务器的端口号通常是固定的,不写端口号时,端口号也能通过其他特殊方法得知,通信照样能进行
文件路径:用具体的路径来表示想要访问服务器的哪些资源
查询字符串:通常用来过滤网页中的信息,得到想要的信息
片段标识符:也称锚点,用来指定网页的停靠位置,或者音视频的播放位置

可以看到,url中不同字段需要用特殊符号进行分隔,这就意味着每一字段中不能出现这些特殊符号,或者说如果出现了这些特殊符号,需要对这些特殊符号做处理。比如搜索c++这这个关键字在这里插入图片描述
由于‘+‘是特殊字符,所以其被编码成其他格式。再者,搜索汉字时,汉字也会被重新编码
在这里插入图片描述
虽然url栏显式的是汉字,但是把url复制下来再粘贴,得到的url是

https://www.baidu.com/s?wd=%E5%93%88%E5%93%88

这里说明一下特殊字符的编码规则,将特殊字符转换成十六进制,以两个十六进制数为一组,从低到高一次取出每一组,再它们的前面加上%,编码成%XY的形式。查询ASCII码表,+的码值为43,表示成十六进制是2B,所以在url中,其被编码成%2B
在这里插入图片描述
由于url采用的是utf-8编码格式,在该格式下汉字被编码成3个字节,由于两个十六进制数表示1个字节,刚才“哈哈”被编码后,有6组%XY格式的数据,也就是6个字节。关于把特殊字符进行编码的过程,我们叫做urlencode,将%XY格式的数据解码的过程,我们叫做urldecode

再回过头来看url,其全称是Uniform Resource Locator,统一资源定位符,通过url我们就能定位互联网上某一台主机的某些资源。我们使用http协议进行请求,就是请求获取某一天主机(服务器)上的某些资源(音视频,文本),当然,服务器的响应就是将客户端请求获取的资源返回。所以说,我们访问的网页,看到的视频,文字,不是凭空产生的,这些资源都是存储在某些服务器的磁盘上,在我们请求资源时,由服务器发送给我们的。

http协议介绍

比起上篇博客,自己定制的协议,http协议能够使用的场景实在是太多了,同样的,关于http的协议格式也是更复杂的在这里插入图片描述
可以看到http协议格式使用\r\n作为不同字段的分隔符,以http请求为例,在第一个分隔符之前的数据就是http的请求行,里面含有

method:具体http请求的方法,如GET,POST
url:刚才说过的,url用来定位具体资源
http/1.1:http协议的版本

在第一行,请求行之后的字段就是请求报头,其中可能含有客户端主机的信息,连接的属性,有效载荷的长度等等信息,这些信息以key:value的格式保存在报头中,由于这些字段以\r\n分隔,所以可以直观的认为报头的每一行都是一对key:value信息。继续这样进行读取,我们会遇到一个空行,也就是说在报头的最后一个key:value后会有两个\r\n,前一个\r\n用来分隔最后的key:value字段,那么后一个key:value是用来划分有效载荷与报头的,读取http请求时,如果遇到了一个空行,就说明接下来的数据是这次请求的有效载荷了。客户端的请求无非两种,一是请求获取服务端的资源,二是请求将客户端的资源上传到服务端,这些信息都将在有效载荷中体现

至于服务端的响应,其格式与客户端的请求几乎相同,都是三个字段:响应行,响应报头,有效载荷。它们的分隔规则是一样的,不同的是响应行中的数据

http/1.1 状态码 状态码描述

可以看到请求和响应都含有协议的具体版本信息,这样做的目的是:为了使通信能够正常进行,通信双方要保证通信协议版本的一致性,所以客户端和服务端互相发送http协议的版本,如果两者的版本不同,将按照:以较低版本进行通信的原则,进行协议的调整。响应行还包括了状态码和其描述,通常我们请求的网页都是能正常访问的,但是也会有错误情况出现,错误码就表征了请求中的错误,最常见到的错误码就是404,如果你要访问的服务端资源不存在,那么服务端的状态码将被设置为404

GET vs POST

http协议中有很多方法:GET,POST,PUT,DELETE,OPTIONS…其中最经常使用的方法是GET,POST,至于其他方法就较为少见了,如果遇到就现学吧。这里主要说明GET和POST的区别,其中的依据来自
HTTP 方法:GET 对比 POST
。首先,GET和POST都是明文传送,两者都是不安全的,使用这两个方法时,我们的数据总是在网络上裸奔,只是方式不同罢了

(在之前的博客中,我搭建了一个TCP网络通信模型,由于http协议是基于tcp的,所以我这里就直接改造这个模型,实现一份服务端代码,对浏览器(客户端)的请求做出响应)

#include <iostream>
#include <string>
#include <stdlib.h>
#include <fstream>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>

using namespace std;

#define CRLF "\r\n"
#define HOME_PAGE "index.html"
#define ROOT_PAGE "wwwroot"
#define SPACE " "

string getPath(const string& req)
{
    // 请求行的获取
    size_t head_pos = req.find(CRLF);
    if (head_pos == string::npos)
        return "";
    string head = req.substr(0, head_pos);
    // 资源路径的获取
    size_t path_start = head.find(SPACE);
    size_t path_end = head.rfind(SPACE);
    if (path_start == string::npos || path_end == string::npos)
        return "";
    // 获取资源地址
    string path = head.substr(path_start + 1, path_end - path_start - 1);
    // 如果地址为/,默认访问家目录
    if (path[0] == '/' && path.size() == 1) path += HOME_PAGE;
    return path;
}

string readFile(const string& path)
{
    ifstream file(path);
    if (!file.is_open()) return "404";
    // 将资源的所有数据返回
    string line;
    string content;
    // 读取整份文件的内容
    while (file.peek() != EOF)
    {
        getline(file, line);
        content += line;
    }

    return content;
}

// 先获取请求报头中的资源位置
// 读取该资源,将其返回
void handlerRequest(int sock)
{
    char buf[10240] = {0};
    ssize_t r_ret = read(sock, buf, sizeof(buf));
    if (r_ret < 0)
        cout << "read fail" << endl;
    else if (r_ret == 0)
        cout << "client quit" << endl;
    // 读取成功
    else
    {
        string req_str = buf;
        // for test
        cout << req_str << endl;
        // 获取请求行中的资源
        string path = getPath(req_str);
        // 读取客户请求的资源,当前根路径的保存
        string resource_path = ROOT_PAGE;
        resource_path += path;
        // 获取文件资源
        string resource = readFile(resource_path);
        string suffix = "";
        // 获取文件的后缀
        size_t suffix_pos = resource_path.rfind('.');
        if (suffix_pos != string::npos)
            suffix = resource_path.substr(suffix_pos);
        // 创建响应
        string response = "HTTP/1.1 200 OK\r\n";
        // 根据后缀为响应报头添加不同字段
        if (suffix == ".jpg") response += "Content-type: image/jpeg\r\n";
        else response += "Content-type: text/html\r\n";
        // 请求正文长度字段的添加
        response += ("Content-Length: " + to_string(resource.size()) + "\r\n");
        response += "\r\n";
        // 响应正文
        response += resource;
        // 向客户端发送响应
        write(sock, response.c_str(), response.size());
    }
}

class tcpServer
{
public:
    tcpServer(uint16_t port, std::string ip = "") : _ip(ip), _port(port) {}
    ~tcpServer() {}

    void init()
    {
        // 创建套接字文件
        _listen_sockfd = socket(AF_INET, SOCK_STREAM, 0);
        if (_listen_sockfd < 0)
        {
            std::cerr << "socket: fail" << std::endl;
            exit(-1);
        }
        // 填充套接字信息
        struct sockaddr_in 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);
        // 将信息绑定到套接字文件中
        if (bind(_listen_sockfd, (const struct sockaddr *)&local, sizeof(local)) < 0)
        {
            std::cerr << "bind: fail" << std::endl;
            exit(-1);
        }
        // 至此,套接字创建完成,所有的步骤与udp通信一样
        // 使套接字进入监听状态
        if (listen(_listen_sockfd, 5) < 0)
        {
            std::cerr << "listen: fail" << std::endl;
            exit(-1);
        }
        // 套接字初始化完成
        std::cout << "listen done" << std::endl;
    }


    void loop()
    {
        // signal(SIGCHLD, SIG_IGN); // 设置SIGCHLD信号为忽略,这样子进程就会自动释放资源
        // 创建保存套接字信息的结构体
        struct sockaddr_in peer;
        socklen_t peer_len = sizeof(peer);
        // 接受监听队列中的套接字请求
        while (true)
        {
            int server_sockfd = accept(_listen_sockfd, (struct sockaddr *)&peer, &peer_len);
            if (server_sockfd < 0)
            {
                std::cerr << "accept: fail" << std::endl;
                continue;
            }
            std::cout << "accept done" << std::endl;

            // 提取请求方的套接字信息
            uint16_t peer_port = ntohs(peer.sin_port);
            std::string peer_ip = inet_ntoa(peer.sin_addr);
            // 打印请求方的套接字信息
            std::cout << "accept: " << peer_ip << " [" << peer_port << "]" << std::endl;

            // 使用孙子进程提供服务
            // grandparent
            pid_t id = fork();
            // 祖父进程
            if (id == 0)
            {
                // 父进程
                pid_t cid = fork();
                if (cid > 0)
                {
                    // 关闭父进程
                    exit(1);
                }
                // 孙子进程,孤儿,由1号进程管理
                handlerRequest(server_sockfd);
            }
            // 阻塞的回收进程资源
            waitpid(id, nullptr, 0);
        }
    }
private:
    std::string _ip;
    uint16_t _port;
    int _listen_sockfd;
};

static void Usage(std::string proc)
{
    std::cerr << "Usage:\n\t" << proc << " port" << std::endl;
    std::cerr << "example:\n\t" << proc << " 8080\n"
              << std::endl;
}

int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        Usage(argv[0]);
        exit(0);
    }
    uint16_t port = atoi(argv[1]);

    tcpServer svr(port);
    svr.init();
    svr.loop();
    return 0;
}

tcp通信的部分就不再赘述了。为防止僵尸进程,该通信模型创建孙子进程为客户端提供服务,而退出子进程,使孙子进程成为孤儿进程,由1号进程负责其资源的释放。服务端接收到客户端的请求,会对该请求进行解析,得到请求需要的资源路径,服务端会将该资源返回给客户端。下面这份网页就是服务端默认返回给用户的资源

<!-- index.html -->
<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8">
    <title>测试</title>
</head>

<body>
    <h3>hello my server!</h3>
    <p>我终于测试完了我的代码</p>
    <form action="/a/b/c.html" method="get">
        Username: <input type="text" name="user"><br>
        Password: <input type="password" name="passwd"><br>
        <input type="submit" value="Submit">
    </form>
</body>

</html>

这个网页中有一个表单,客户可以提交它们的用户名和密码信息,其中action标签表示将这些信息发送到指定网页上(当然是客户端再向服务端发送一次请求了),为了测试简单,这里将信息发送到一个不存在的网页中,我们只观察GET和POST的区别。
在这里插入图片描述
注意,服务端接收到客户端的请求后,会将客户端的请求打印出来,方便我们的测试,在浏览器的url中输入IP:port,访问服务在这里插入图片描述
我们输入用户名123,密码456,点击submit按钮,这些数据会被发送到action指定的网页上,并且方法是get
在这里插入图片描述

很正常,因为网页不存在,所以返回404。但是我们注意到,输入的用户名和密码在url中以查询字符串的方式显示了出来。在这里插入图片描述
将网页提交数据的方式修改为post,重新运行服务,重新连接到该服务,输入用户名密码,submit
在这里插入图片描述

在这里插入图片描述
可以看到,用户名和密码没有在url中体现,观察服务端打印的客户端请求,可以看到这些信息出现在请求的正文部分

总结一下:GET将数据以查询字符串的方式拼接到url中,而POST将数据以正文的方式置于请求中,两者都是明文传输,不同的只是POST较GET更隐蔽一些,但两者在本质上都是不安全的。

http状态码

1xx信息状态码Informational接收的请求正在处理,由于现在的网络响应快,这样的状态码很少使用了
2xx成功状态码Success请求正常处理完毕
3xx重定向状态码Redirection将请求重定向到其他资源上
4xx客户端错误状态码Client Error服务器无法处理请求,请求非法
5xx服务端错误状态码Server Error服务端运行出错

这些状态码也不需要具体记忆,记住几个常用的就行了,比如200(OK),404(Not Found),403(Forbidden),302(Redirect),504(Bad Gateway,服务器访问上游服务器超时)

void for_redirection(int sock)
{
    char buf[10240] = {0};
    ssize_t r_ret = read(sock, buf, sizeof(buf));
    if (r_ret < 0)
        cout << "read fail" << endl;
    else if (r_ret == 0)
        cout << "client quit" << endl;
    // 读取成功
    else
    {
        string req_str = buf;
        // 创建响应
        string response = "HTTP/1.1 302 Moved Temporarily\r\n";
        // 请求正文长度字段的添加
        response += "location: https://baike.baidu.com/item/302/878045?fr=aladdin\r\n";
        response += "\r\n";
        // 向客户端发送响应
        write(sock, response.c_str(), response.size());
    }
}

简单的,将服务端的响应修改,请求行修改为"HTTP/1.1 302 Moved Temporarily\r\n",然后在报头中添加location字段,表示重定向的url,如果服务端只提供以上服务的话,不论客户端向服务端发送什么,客户端都将跳转到location字段的url上。不过关于301和302的区别还是要注意一下的,301是当前资源的永久性转移,而302是资源的临时性转移。如果收藏夹中,有一个url301了,浏览器可能会将该url修改为新的url,而302就不会

http常见header

Content-type:正文数据类型(html/text,image/jpeg)
Content-Length:正文长度
Host:告知服务端,客户端要访问的资源在哪台主机上
User-Agenr:声明用户的操作系统和浏览器版本信息
referer:当前页面是从哪个页面跳转过来了
location:配合3xx使用,指定重定向的url,客户端将访问该url
Cookie:存储用户一些数据,用来进行会话的维持

header就是http中的报头字段,以上展示的是字段中的key,每一字段以key:value的方式呈现在这里插入图片描述
最后还剩一个值得讲解的header:Connection,http1.0的Connection默认为closed,http1.1的Connection默认为keep-alive,一个为短连接一个为长连接。在互联网发展早期,网页中的信息没有现在这么的密集,一次http请求就可以获取完整的网页并呈现给客户。但是随着互联网的发展,网页中的信息越来越多,越来越复杂,所以一次http协议无法获取完整的网页,而http是基于tcp通信的,多次http请求就代表这多次tcp的连接,这样的做法势必会导致网页加载速度的下降,由此http/1.1默认开启长连接,只有一个网页的资源请求完成,tcp连接才会关闭。关于长连接的深入学习,具体实现可以阅读这篇博客

cookie + session

http有一个特性:无状态,即无法进行状态的保持,每一次请求都是独立的。但在网页端浏览b站时,登录账号过后,每次访问b站都会保持你的登录信息,其中的原理就与cookie有关在这里插入图片描述
在这里插入图片描述
(上图来自网络)当然了,首次登录b站时,你还是需要输入你的账号密码的,一旦选择登录,浏览器就会将你的用户信息通过http协议传输到b站的服务端,服务端认证成功,确认该账号是有效的之后,会生成一个cookie文件,并发送一个set-Cookie的响应,使客户端在磁盘或者内存上保持该cookie文件。之后客户端的http请求就会携带这份cookie文件,服务端收到cookie文件后进行解析,得到有效数据认证成功之后,才会将特定的响应返回给客户端,此时客户端看到的网页就是已经登陆账号的网页了。

如果cookie存储在磁盘上,那么关闭浏览器,甚至关机之后再打开该网页,你的账号登录信息依旧能保持,因为cookie文件在磁盘上,除非你直接删除,否则该文件会一直存在。但是cookie文件存储在内存(浏览器)中时,关闭了浏览器(注意不是关闭网页),浏览器的进程资源被释放,属于浏览器资源的cookie文件当然也被释放,此时再打开浏览器,登录信息就无法保持。如果只是把网站关闭,浏览器没关闭,再打开该网站,登录信息依旧是能保存的,因为cookie文件没有被释放。

但由于安全性的问题,将cookie文件存储在客户端的做法。因为客户端容易遭到攻击,或者说信息容易泄漏,如果cookie被非法窃取,cookie所有者的权益将会受到损害。所以现在主流的策略都是cookie+session,将cookie文件存储在服务端(Linux系统安全性极高)。其主要实现是:客户端提交登录信息,服务端接收后在后台数据库上将这些信息存储起来,然后生成一个唯一的id,这个id可以理解为key,数据库中存储的信息可以理解为value,这就是一对key:value模型。服务端将id返回给客户端,使客户端set-Cookie,将该id值保存起来,此后客户端的http请求都会带上该id值,服务端收到id后,找到其对应value,就能解析出客户的信息,也就能根据这些信息进行会话保持了。

但是cookie+session的方式同样是不安全的,id值同样可以被窃取,被非法利用。但是被窃取的只是一个id值,你的具体信息没有得到泄漏,具体信息存储在服务端的数据库中,要攻击这样的数据库还是比较难的。这也是相对直接使用cookie的优势吧,或者这么说,http协议就是不安全的,它的目的是通信的进行,侧重于通信的实现,至于安不安全就是另外一回事了,因此不推荐用http进行私密数据的传输,要进行这样的传输可以使用更侧重安全性的https协议

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值