Linux网络编程_03_应用层HTTP协议

发布于:2023-01-30 ⋅ 阅读:(695) ⋅ 点赞:(0)

Linux网络编程总目录(点击下面链接即可到达对应章节)

Linux网络编程_01_网络基础
Linux网络编程_02_socket套接字
Linux网络编程_03_应用层HTTP协议
Linux网络编程_04_传输层UDP和TCP协议
Linux网络编程_05_网络层IP协议
Linux网络编程_06_数据链路层MAC帧协议
Linux网络编程_07_多路转接

一. 序列化反序列化

1.1 简述

序列化就是结构化的数据转换成便于网络传输的格式(一般是转换成字符串),反序列化就是将序列化的格式转换成格式化的格式,如下图。

在这里插入图片描述

1.2 代码演示

// 注意事项:
// 1. 如果你的服务器没有安装json库的话需要执行以下命令安装
// sudo yum -y install jsoncpp-devel

// 2. 编译时需要在g++或者gcc后面加上-ljsoncpp链接动态库
// 比如:g++ -o $@ $^ -std=c++11 -ljsoncpp
#include<iostream>
#include<string>
#include<jsoncpp/json/json.h>
using namespace std;

typedef struct Student
{
    string name;
    int age;
}Student;
int main()
{
    // 1. 序列化
    Student s1;
    s1.name = "李华";
    s1.age = 18;
    Json::Value root;  // json是一种kv结构
    root["name"] = s1.name;
    root["age"] = s1.age;

    // 两种写法:FastWriter   StyledWriter
    //Json::StyledWriter writer;
    Json::FastWriter writer;
    string json_string = writer.write(root);  // 将root写进字符串
    //cout << json_string << endl;


    // 2. 反序列化
    Json::Reader reader;
    Json::Value rroot;
    reader.parse(json_string, rroot);  // 将字符串的内容解析给rroot
    Student s2;
    s2.name = rroot["name"].asString();
    s2.age = rroot["age"].asInt();
    cout << "name:" << s2.name << "   age:" << s2.age << endl;
    return 0;
}

二. HTTP协议

2.1 认识RUL

如下图所示,一般我们把URL分为三大部分,分别是请求网站使用的协议,我们访问的IP(域名),还有我们需要访问的资源所在的路径。域名可以理解为是IP的一种符号化表示形式,是跟IP绑定起来的,访问这个域名就是访问这个域名对于绑定的IP。公网IP+端口号确定网络上的唯一一个进程,公网IP+路径确定网络上的唯一一份资源。

在这里插入图片描述

2.2 encode和decode

encode就是将一些特殊字符转义为16进制,后从右到左,取4位(不足4位直接处理),每2位做一位,前面加上%,编码成%XY格式。比如’+‘被转义成了’%2B’,decode就是encode的逆过程。

2.3 HTTP协议格式

在这里插入图片描述

2.3.1 HTTP请求

  1. 首行: [方法] + [url] + [版本]

  2. Header: 请求的属性,冒号分割的键值对,每组属性之间使用\n分隔

  3. 空行: 遇到空行表示Header部分结束

  4. Body: 空行后面的内容都是Body, Body允许为空字符串, 如果Body存在, 则在Header中会有一个Content-Length属性来标识Body的长度

2.3.2 HTTP响应

  1. 首行:[版本号] + [状态码] + [状态码解释]

  2. Header: 响应的属性,冒号分割的键值对,每组属性之间使用\n分隔

  3. 空行: 遇到空行表示Header部分结束

  4. Body: 跟上面一样,如果服务器返回了一个html页面, 那么html页面内容就是在body中.

2.3.3 HTTP的通信格式

由上面的HTTP请求和响应的数据来看**,HTTP请求和响应的内容可以看成是一个大的字符串,用换行符来分隔各种信息。其中空行可以将http报头和正文部分分开,解包和封装也是根据空行来判断的。**

2.3.4 http_demo代码演示

sock.hpp文件代码

#include<sys/socket.h>
#include<sys/types.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<unistd.h>
#include<iostream>
#include<string>
#include<cstdlib>

using namespace std;

class sock 
{
public:
    static int Socket()
    {
       int sockfd = socket(AF_INET, SOCK_STREAM, 0);
       if (sockfd < 0)
       {
           cerr << "socket error" << endl;
           exit(-1);
       }
       return sockfd;
    }

    static void Bind(const int& sockfd, const uint16_t& port)
    {
        struct sockaddr_in server;
        server.sin_family = AF_INET;
        server.sin_port = htons(port);
        server.sin_addr.s_addr= INADDR_ANY;
        int ret = bind(sockfd, (struct sockaddr*)&server, sizeof(server));
        if (ret < 0)
        {
            cerr << "bind error" << endl;
            exit(-1);
        }
    }
    static void Listen(const int& sockfd)
    {
        if (listen(sockfd, 5) < 0)
        {
            cerr << "listen error" << endl;
            exit(-1);
        }
    }

    static int Accept(const int& sockfd)
    {
        sockaddr_in client;
        socklen_t len;
        int accfd = accept(sockfd, (struct sockaddr*)&client, &len);
        if (accfd < 0)
        {
            cerr << "accept error" << endl;
            exit(-1);
        }
        else 
        {
            cout << "get a client... IP --> " << inet_ntoa(client.sin_addr) << "   Port --> " << client.sin_port << endl; 
            return accfd;
        }
    }

    static int Connect(const int& sockfd, const char* serverIP, const uint16_t& serverPort)
    {
        sockaddr_in server;
        server.sin_family = AF_INET;
        server.sin_port = htons(serverPort);
        server.sin_addr.s_addr = inet_addr(serverIP);
        int confd = connect(sockfd, (struct sockaddr*)&server, sizeof(server));
        if (confd < 0)
        {
            cerr << "connect error" << endl;
            exit(-1);
        }
        else 
        {
            return confd;
        }
    }
};

http_demo.cpp文件代码

#include"sock.hpp"

void Usage(const char* proc)
{
    cout << "Usage:" << proc << "Port" << endl;
}

void* Run(void* arg)
{
    int fd = *(int*)arg;
    delete (int*)arg;
    char buffer[10240];
    ssize_t recv_count = recv(fd, buffer, sizeof(buffer), 0);  // 接收http请求信息
    if (recv_count > 0)
    {
        buffer[recv_count] = '\0';
        cout << buffer;  // 打印请求信息

		// 构建响应信息(简单模拟一下)
        string body_str = "Helloworld, I am body!!!";  // 响应的正文
        string send_string = "HTTP/1.0 200 OK\n";
            send_string += "Content-Type: text/plain\n";  // 正文的类型,text/plain --> 正文是普通文本
            send_string += "Content-Length: ";  // 正文的长度,单位是字节
            send_string += to_string(body_str.size());
            send_string += "\n";  // 这个'\n'是Content-Length属性的换行符,不是空行
            send_string += "\n";  // 这里'\n'就是我们所说的空行
            send_string += body_str;
            send(fd, send_string.c_str(), send_string.size(), 0);  // 发送给用户
            cout << send_string << endl;
    }
    else 
    {
        cout << "recv....." << endl;
    }
    close(fd);
}
int main(int argc, char* argv[])
{
    if (argc != 2)
    {
        Usage(argv[0]);
        return 1;
    }

    int sockfd = sock::Socket();
    uint16_t port = (uint16_t)stoi(argv[1]);
    sock::Bind(sockfd, port);
    sock::Listen(sockfd);
    while (true)
    {
        int fd = sock::Accept(sockfd);
        if (fd > 0)
        {
            pthread_t tid;
            int* pfd = new int(fd);
            pthread_create(&tid, nullptr, Run, pfd);
        }
    }
    return 0;
}

// 编译运行后,在浏览器地址栏(注意:是地址栏,不是搜索框)输入IP:port
// 比如192.168.1.13:8080

抓包工具显示的请求信息和响应信息

在这里插入图片描述

浏览器显示的结果

在这里插入图片描述

2.4 GET和POST请求方法

2.4.1 代码

http_demo文件代码

sock.hpp文件代码跟上面的一样

#include"sock.hpp"
#include<sys/types.h>
#include<sys/stat.h>
#include<unistd.h>
#include<fstream> 
#define MAIN_PAGE "./main/index.html"
void Usage(const char* proc)
{
    cout << "Usage:" << proc << "Port" << endl;
}

void* Run(void* arg)
{
    int fd = *(int*)arg;
    delete (int*)arg;
    char buffer[10240];
    ssize_t recv_count = recv(fd, buffer, sizeof(buffer), 0);
    if (recv_count > 0)
    {
        buffer[recv_count] = '\0';
        cout << buffer << endl;
        string send_string = "HTTP/1.0 200 OK\n";
        struct stat stat_buf;
        stat(MAIN_PAGE, &stat_buf);
        string body_size = to_string(stat_buf.st_size);
        send_string += "Content-Type: text/html\n";  //  text/html--> 正文格式是html
        send_string += "Content-Length: ";
        send_string += body_size;
        send_string += "\n";  // 这里的'\n'是Content-Length的换行符,不是空行
        send_string += "\n";  // 这里'\n'就是我们上面所说的空行

        // 处理正文部分
        ifstream in(MAIN_PAGE);
        if (in.is_open())
        {
            string body_string;
            string line;
            
            // 按行全部读完
            while (getline(in, line))
            {
                body_string += line;
            }
            send_string += body_string;
            in.close();
        }
        ssize_t send_size = send(fd, send_string.c_str(), send_string.size(), 0);
        if (send_size == (ssize_t)send_string.size())
        {
            cout << send_string << endl;
        }
    }
    else 
    {
        cout << "recv....." << endl;
    }
    close(fd);
}
int main(int argc, char* argv[])
{
    if (argc != 2)
    {
        Usage(argv[0]);
        return 1;
    }

    int sockfd = sock::Socket();
    uint16_t port = (uint16_t)stoi(argv[1]);
    sock::Bind(sockfd, port);
    sock::Listen(sockfd);
    while (true)
    {
        int fd = sock::Accept(sockfd);
        if (fd > 0)
        {
            pthread_t tid;
            int* pfd = new int(fd);
            pthread_create(&tid, nullptr, Run, pfd);
        }
    }
    return 0;
}

index.html文件代码

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
    </head>
    <body>
        <h3>登录</h3>
        <!--这里的method填上get或者post,然后填上账号密码,登录抓包看请求信息-->
        <!--<form action="/" method="post">-->
        <form action="/" method="get">
            账号: <input type="text" name="name"><br/>
            密码: <input type="password" name="passwd"><br/>
            <input type="submit" value="登陆">
        </form>
    </body>
</html>

2.4.2 抓包显示的请求信息

在这里插入图片描述

2.4.3 结论

  1. GET方法叫获取,如果提交参数是通过URL方式进行提交的。POST方法叫推送,提交参数是通过正文提交的。
  2. 如果有参数POST方法会比较隐蔽一点,但是不代表POST方法安全,因为抓包抓到也是可以直接通过正文看到提交的参数。GET是通过URL提交的,一般URL会有大小限制,而POST以正文方式提交就不怎么在乎空间大小。
  3. 如果提交参数比较敏感一点或者非常多就使用POST,其他情况用GET就好了

2.5 HTTP状态码

2.5.1 状态码表

类别 原因短句
1xx Informational(信息性状态码) 接收的请求正在处理
2xx Success (成功状态码) 请求正常处理完毕
3xx Redirection(重定向状态码) 需要进行附加操作以完成请求
4xx Client Error(客户端错误状态码) 服务器无法处理请求
5xx Server Error (服务器错误状态码) 服务器请求出错

2.5.2 常见的状态码

200:OK

302:Found(临时重定向)

403:Forbidden(服务器拒绝该次访问,一般是访问权限出现问题)

404:Not Found

504:Gateway timeout(网关超时)

2.6 常见的Header

2.6.1 属性介绍

Content-Type: Body的数据类型(text/html等)

Content-Length: Body的长度,单位为字节

Host: 客户端告知服务器,所请求的资源是在哪个主机的哪个端口上

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

referer: 当前页面是从哪个页面跳转过来的

location: 搭配3xx状态码使用, 告诉客户端接下来要去哪里访问,实现重定向功能

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

Connection: keep-alive(表示一个长连接),长连接可以减少频繁建立TCP链接,达到高效的目的HTTP/1.0默认是短连接,HTTP/1.1默认是长连接。

2.6.2 location的使用


// 将上面的http_demo代码的send_string改成下面这样就好了
string send_string = "HTTP/1.0 301 Moved Permanently\n";  // 301为永久重定向,一般用于网站迁移
string send_string = "HTTP/1.0 302 Found\n";  // 302为临时重定向,一般用于页面跳转
send_string += "location:https://www.bilibili.com\n";  // location:后面跟的就是重定向到的url

2.6.4 Cookie和Session

2.6.4.1 Cookie

Cookie是在浏览器上的一个容器,可以帮用户存储访问网页提交的参数,或者其他信息。如图,当客户端第一次访问一个网页时向服务器提供了账号和密码(注意:因为实现起来比较繁琐,为了代码简易,我们没有实现这个,而是在直接在服务器端Set-Cookie,所以你看到下面第一次请求信息是没有提交账号密码的),然后服务器将账号和密码在响应头Header里的Set-Cookie属性里面返回去给客户端的Cookie里面保持着。后面的访问请求头的Header里的Cookie属性会自动携带保持好的账号密码的,也就不需要用户输入了。 Cookie存储内容的形式一般分两种,文件和内存,如果是内存的形式的话关闭浏览器之后,再次打开后就需要重新输入账号密码,文件的形式就看这个文件信息存在多长时间了。

在这里插入图片描述

2.6.4.2 Session

我们直接在Cookie文件里面直接保存账号密码,如果被别人获取到了我们的Cookie文件,别人就会知道我们的账号密码。为了避免账号密码在Cookie文件泄露出来,就提出了session会话。如图,当客户端向服务器提交账号密码给服务器后,服务器会形成一个session文件用来保存账号密码,这个session文件有一个会话id,如图中的001就是我们的会话id。服务器在Set-Cookie时不是将账号密码返回去,而是将session_id返回去给Cookie,客户端在后面请求服务器时,通过自动携带Cookie的session_id在服务器端找到对应文件,从而找到对应的账号密码。

在这里插入图片描述

三.HTTPS协议

3.1 对称和非对称加密

3.1.1 对称加密

对称加密就是密钥只有一个,即用X加密,用X解密。 就比如说下面的代码,key变量就可以看作是一把密钥,在数据发送时让data异或上key,接收时再异或上key,就可以得到原来的data值了。

#include<iostream>
int main()
{
	// 发送方
	int key = 10;
	int data = 81;
	int send = key ^ data;

	// 接收方
	int ret_data = send ^ key;
	std::cout << ret_data << std::endl;
	return 0;
}

3.1.2 非对称加密

非对称加密有一对密钥,公钥和私钥。用私钥加密只能用公钥解密,用公钥加密只能用私钥解密。 一般而言,公钥是对全世界公开的,私钥是私密的,不对外公开。相较于对称加密,非对称加密是非常耗时的。

3.2 证书

我们想要发送一份文章给好友,为了确保好友接收到的文章是完整的,即在通信途中没有被修改过,即便是一个标点符号也不能。如图我们首先是申请证书,将文章通过Hash散列处理得到一串唯一的字符序列(数据摘要或者数据指纹),通过加密算法加密得到数字签名,让文章和其对应的数字签名打包起来形成证书。然后将证书发给好友,好友接收到证书之后,将文章通过Hash散列重新生成数字指纹,将数字签名通过加密算法解密得到数字指。最后对比两份数字指纹,一样那就证明文章在途中没有被修改过。

在这里插入图片描述

3.3 https

3.3.1 简述

https是在http的基础上,通过TLS/SSL对数据进行加密,在传输的过程中,数据是被保护起来的,避免被抓包导致数据泄露。

3.3.2 对称加密法

如图所示,使用钥匙K对数据进行加密和解密,这种情况下一般需要客户端和服务器内置了K密钥,这种方法是不可取的,因为K太容易泄露了。

在这里插入图片描述

3.3.3 非对称加密法

如图所示,当客户端发起请求时,服务器会将公钥S发送给客户端,然后客户端用S对数据data进行加密,发给服务器。服务器收到后会用自己的私钥对数据进行解密,最后得到数据data。

在这里插入图片描述

3.3.4 复合加密法

上面我们说过,非对称式加密是非常耗时的,在通信的过程中使用纯非对称加密是不可取的,所以就有一种复合加密法。如图所示,客户端请求之后,服务器会将自己的公钥S给客户端,客户端拿到S并且形成一个对称密钥K。然后用S对对称密钥K加密得到K+,发送给服务器。服务器接K+后用私钥进行解密,得到K,最后他们正常通信的时候就可以用对称密钥K对数据进行加密和解密了。

在这里插入图片描述

3.3.5 中间人偷换公钥

无论是纯非对称法还是复合法,在服务器给公钥S的过程中,会存在一个很大的风险。如图所示,有一个中间人把服务器发给客户端的公钥S给截获,然后将自己的公钥M发给客户端。客户端接到M并且生成对称密钥K,用M给K加密成K+,发送给服务器。这时中间人又截获了K+,用自己的私钥M’解密得到K,最后用S对K再次加密得到K+',发送给服务器,服务器用私钥S’解密得到K。后面服务器跟客户端用K对数据进行通信,但是中间人也获取到了K,所以中间人也可以截获数据,然后进行解密。

在这里插入图片描述

3.3.6 https最终使用的加密法

上面的中间人偷换公钥使得我们的传输是不安全的,想要解决这个问题我们就要保证客户端拿到的公钥是服务器发来的S,而不是中间人的M。如图,我们可以对服务器的IP,公钥S进行哈希散列处理形成数据指纹,然后证书颁发的公司用私钥A’进行加密得到数字签名,最后将数据和签名合成证书。客户端请求之后,服务器发给客户端的是带有公钥S的证书,在中间即使中间人截取了也不会造成数据被更改。 第一点,上面我们说过的,数据和其数字签名应该是一一对应的,改了其中一个都不行。第二点,能不能两个都改了呢,答案是不能,以为数字签名最后需要客户端用证书公司的公钥A来解锁,这就说明数字签名只能由证书颁发机构的A’私钥来生成,这个中间人是没有的。
在这里插入图片描述
下一章节点击我直达 --> Linux网络编程_04_传输层UDP和TCP协议


网站公告

今日签到

点亮在社区的每一天
去签到