IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 网络协议 -> 【Network】网络基础@应用层 —— 协议 | http | https -> 正文阅读

[网络协议]【Network】网络基础@应用层 —— 协议 | http | https

“like you do when you lie and I know it’s my imagination” @_zing_photograph

1. 认识协议

一条QQ消息经历了如下过程:结构化数据 → 长“字符串” → 结构化数据

struct message
{
 昵称:小边小边不秃头
 头像:我愚蠢的理想主义.png
 消息:GN ST
 时间:2022-08-26 15:02:55
}msg;

为什么要进行序列化和反序列化

  • 为了应用层网络通信的方便。因为这种结构化的数据是不便于网络传输的,而字符串便于网络传输。
  • 这种结构化的数据方便上层使用内部成员,例如图片界面显示。将应用和网络进行解耦。

之前我们做的udp、tcp通信,并没有进行任何序列化和反序列化,只是因为我们没去定义结构化数据。

事实上,这个结构化数据就是协议的表现。

怎么做到呢?我们可以自己造轮子,但其实真挺麻烦的;今天我们用轮子,就是用别人写好的组件(xml/json/protobuff)

网络计算器

例如, 我们需要实现一个服务器版的加法器. 我们需要客户端把要计算的两个加数发过去, 然后由服务器进行计算, 最后再把结果返回给客户端.

约定方案

客户端发送一个形如1+1的结构体; 这个字符串中有两个操作数, 都是整形; 两个数字之间会有一个字符是运算符, 运算符只能是 + ;

数字和运算符之间没有空格; 定义结构体来表示我们需要交互的信息; 发送数据时将这个结构体按照一个规则转换成字符串, 接收到数据的时候再按照相同的规则把字符串转化回结构体…

订制协议

目前就是订制数据化结构的过程

1. 原生版本

没有明显的序列化/反序列化的过程的版本

Protocol.hpp

#pragma once

#include<iostream>
#include<string>

using namespace std;

//订制协议

//请求格式
typedef struct request
{
    int x;
    int y;
    char op;  //"+-*/%"
}request_t;

//响应格式
typedef struct response
{
    int code;   //server运算完毕的计算状态:code(0:success) code(-1:div 0)...
    int result; //计算结果,你能否区分是正常的计算结果 还是 异常的退出结果
}response_t;

Sock.hpp

//对套接字接口做简单封装
#pragma once

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

using namespace std;

class Sock 
{
public:
    //创建套接字
    static int Socket()
    {
        int sock = socket(AF_INET, SOCK_STREAM, 0);
        if(sock < 0)
        {
            cerr << "socket error" << endl;
            exit(2);
        }
        return sock;
    }

    //绑定
    static void Bind(int sock, uint16_t port)
    {
        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(sock, (struct sockaddr*)&local, sizeof(local)) < 0)
        {
            cerr << "bind error" << endl;
            exit(3);
        }
    }

    //监听
    static void Listen(int sock)
    {
        if(listen(sock, 5) < 0)
        {
            cerr << "listen error" << endl;
            exit(4);
        }
    }

    static int Accept(int sock)
    {
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        int fd = accept(sock, (struct sockaddr*)&peer, &len);
        if(fd >= 0)
        {
            return fd;
        }
        else
        {
            return -1;
        }
    }

    static void Connect(int sock, 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);

        if(connect(sock, (struct sockaddr*)&server, sizeof(server)) == 0)
        {
            cout << "Connect Success!" << endl;
        }
        else
        {
            cout << "Connect Failed!" << endl;
            exit(5);
        }
    }    
};

CalServer.cc

#include"Protocol.hpp"
#include"Sock.hpp"
#include<pthread.h>

// ./CalServer port
static void Usage(string proc)
{
    cout << "Usage: " << proc << " port" << endl;
    exit(1);
}

void* HandlerRequest(void* args)
{
    int sock = *(int*)args;
    delete (int*)args;

    pthread_detach(pthread_self());

    //业务逻辑 - 做一个短服务
    // request → 分析处理 → 构建response → send(response) → close(sock)
    //1.读取请求
    request_t req;
    ssize_t s = read(sock, &req, sizeof(req));
    if(s == sizeof(req))
    {
        //读取到了完整的请求
        // req.x, req.y, req.op
        //2.分析请求 3.计算结果
        //4.构建响应,并进行返回
        response_t resp = {0, 0};
        switch (req.op)
        {
            case '+':
                resp.result = req.x + req.y;
                break;

            case '-':
                resp.result = req.x - req.y;
                break;

            case '*':
                resp.result = req.x * req.y;
                break;

            case '/':
                if(req.y == 0)
                    resp.code = -1; //代表除0
                else 
                    resp.result = req.x / req.y;
                break;

            case '%':
                if(req.y == 0)
                    resp.code = -2; //代表模0
                else 
                    resp.result = req.x % req.y;
                break;

            default:
                resp.code = -3; //代表请求方法异常
                break;  
        }
        cout << req.x << req.op << req.y << endl;
        write(sock, &resp, sizeof(resp));
        cout << "服务结束" << endl;
    }
    //5.关闭连接
    close(sock);
}

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

    int listen_sock = Sock::Socket();
    Sock::Bind(listen_sock, port);
    Sock::Listen(listen_sock);

    for(;;)
    {
        int sock = Sock::Accept(listen_sock);
        if(sock >= 0)
        {
            cout << "get a new client..." << endl;
            int* pram = new int(sock);
            pthread_t tid;
            pthread_create(&tid, nullptr, HandlerRequest, pram);
        }
    }
    return 0;
}

CalClient.cc

#include "Protocol.hpp"
#include "Sock.hpp"

void Usage(string proc)
{
    cout << "Usage: " << proc << " server_ip server_port" << endl;
}

// ./CalClient server_ip server_port
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        Usage(argv[0]);
        exit(1);
    }

    int sock = Sock::Socket();
    Sock::Connect(sock, argv[1], atoi(argv[2]));

    //业务逻辑
    request_t req;
    memset(&req, 0, sizeof(req));
    cout << "Please Enter Data One# ";
    cin >> req.x;
    cout << "Please Enter Data Two# ";
    cin >> req.y;
    cout << "Please Enter operator# ";
    cin >> req.op;

    ssize_t s = write(sock, &req, sizeof(req));

    response_t resp;
    s = read(sock, &resp, sizeof(resp));
    if (s == sizeof(resp))
    {
        cout << "code[0:success]: " << resp.code << endl;
        cout << "result: " << resp.result << endl;
    }
    return 0;
}

Makefile

.PHONY:all
all:CalClient CalServer

CalClient:CalClient.cc 
	g++ -o $@ $^ -std=c++11

CalServer:CalServer.cc 
	g++ -o $@ $^ -std=c++11 -lpthread

.PHONY:clean
clean:
	rm -rf CalClient CalServer 

我们自己定的协议,client 和 server都必须遵守,这就叫做自定义协议 ——

image-20220831213408779

但这个方案很cuo

2. 序列化反序列化版本

理解json.cpp

安装,本质上是在/usr/include/jsoncpp/json中添加头文件,在/usr/lib64/下添加libjson库 ——

sudo yum install -y jsoncpp-devel 

编译时,需要带-ljsoncpp~

仅仅是了解,我们目的是解释序列化和反序列化。

Protocal.hpp

#pragma once

#include<iostream>
#include<string>
#include<jsoncpp/json/json.h>

using namespace std;

//订制协议

//请求格式
typedef struct request
{
    int x;
    int y;
    char op;  //"+-*/%"
}request_t;

//响应格式
typedef struct response
{
    int code;   //server运算完毕的计算状态:code(0:success) code(-1:div 0)...
    int result; //计算结果,你能否区分是正常的计算结果 还是 异常的退出结果
}response_t;

//序列化 - 结构化数据→字符串
std::string SerializeRequest(const request_t& req)
{
    Json::Value root; //万金油对象,可以橙装任何对象,json是一种kv式的序列化方案
    // 盛装
    root["datax"] = req.x;
    root["datay"] = req.y;
    root["operator"] = req.op;

    //FastWriter, StyledWriter
    // Json::StyledWriter writer;
    Json::FastWriter writer;

    //返回值为string,即序列化后的结果
    return writer.write(root); 

}

//反序列化 - 字符串→结构化数据
void DeserializeRequest(const std::string& json_string, request_t& out)
{
    Json::Reader reader;
    Json::Value root;

    reader.parse(json_string, root);
    out.x = root["datax"].asInt();
    out.y = root["datay"].asInt();
    out.op = (char)root["operator"].asUInt();
}

//序列化 
std::string SerializeReponse(const response_t& resp)
{
    Json::Value root; //万金油对象,可以橙装任何对象,json是一种kv式的序列化方案
    // 盛装
    root["code"] = resp.code;
    root["result"] = resp.result;

    //FastWriter, StyledWriter
    // Json::StyledWriter writer;
    Json::FastWriter writer;

    //返回值为string,即序列化后的结果
    return writer.write(root); 
}

//反序列化
void DeserializeReponse(const std::string& json_string, response_t& out)
{
    Json::Reader reader;
    Json::Value root;

    reader.parse(json_string, root);
    out.code = root["code"].asInt();
    out.result = root["result"].asInt();
}

CalServer.cc

#include "Protocol.hpp"
#include "Sock.hpp"
#include <pthread.h>

// ./CalServer port
static void Usage(string proc)
{
    cout << "Usage: " << proc << " port" << endl;
    exit(1);
}

void *HandlerRequest(void *args)
{
    int sock = *(int *)args;
    delete (int *)args;

    pthread_detach(pthread_self());

    //业务逻辑 - 做一个短服务
    // request → 分析处理 → 构建response → send(response) → close(sock)
    // 1.读取请求
    char buffer[1024];
    request_t req;
    ssize_t s = read(sock, buffer, sizeof(buffer) - 1);
    if (s > 0)
    {
        buffer[s] = 0;
        std::cout << "get a new request:" << buffer << endl;
        std::string str = buffer;
        DeserializeRequest(str, req); //反序列化请求
    }

    response_t resp = {0, 0};
    switch (req.op)
    {
    case '+':
        resp.result = req.x + req.y;
        break;

    case '-':
        resp.result = req.x - req.y;
        break;

    case '*':
        resp.result = req.x * req.y;
        break;

    case '/':
        if (req.y == 0)
            resp.code = -1; //代表除0
        else
            resp.result = req.x / req.y;
        break;

    case '%':
        if (req.y == 0)
            resp.code = -2; //代表模0
        else
            resp.result = req.x % req.y;
        break;

    default:
        resp.code = -3; //代表请求方法异常
        break;
    }
    cout << req.x << req.op << req.y << endl;

    //序列化响应
    std::string send_string = SerializeReponse(resp); //序列化之后的字符串
    write(sock, send_string.c_str(), send_string.size());
    cout << "服务结束" << send_string << endl;

    // 5.关闭连接
    close(sock);
}

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

    int listen_sock = Sock::Socket();
    Sock::Bind(listen_sock, port);
    Sock::Listen(listen_sock);

    for (;;)
    {
        int sock = Sock::Accept(listen_sock);
        if (sock >= 0)
        {
            cout << "get a new client..." << endl;
            int *pram = new int(sock);
            pthread_t tid;
            pthread_create(&tid, nullptr, HandlerRequest, pram);
        }
    }
    return 0;
}

CalClient.cc

#include "Protocol.hpp"
#include "Sock.hpp"

void Usage(string proc)
{
    cout << "Usage: " << proc << " server_ip server_port" << endl;
}

// ./CalClient server_ip server_port
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        Usage(argv[0]);
        exit(1);
    }

    int sock = Sock::Socket();
    Sock::Connect(sock, argv[1], atoi(argv[2]));

    //业务逻辑
    request_t req;
    memset(&req, 0, sizeof(req));
    cout << "Please Enter Data One# ";
    cin >> req.x;
    cout << "Please Enter Data Two# ";
    cin >> req.y;
    cout << "Please Enter operator# ";
    cin >> req.op;

    // 序列化请求
    std::string json_string = SerializeRequest(req);
    ssize_t s = write(sock, json_string.c_str(), json_string.size());
    // ssize_t s = write(sock, &req, sizeof(req));

    char buffer[1024];
    s = read(sock, buffer,sizeof(buffer)-1);
    if(s > 0)
    {
        response_t resp;
        buffer[s] = 0;
        std::string str = buffer;
        //反序列化响应 至resp
        DeserializeReponse(str, resp);
        cout << "code[0:success]: " << resp.code << endl;
        cout << "result: " << resp.result << endl;
    }

    return 0;
}

Makefile

.PHONY:all
all:CalClient CalServer

CalClient:CalClient.cc 
	g++ -o $@ $^ -std=c++11 -ljsoncpp

CalServer:CalServer.cc 
	g++ -o $@ $^ -std=c++11 -lpthread -ljsoncpp

.PHONY:clean
clean:
	rm -rf CalClient CalServer 

小总结

如上重点在于演示在网络中是互发字符串 ——

image-20220903203245736

我们所写的cs模式的在线版本服务器,本质就是一个应用层网络服务 —— 基本通信代码;序列和反序列化(借助组件);业务逻辑是我们自己定的、请求结果格式 & code含义等约定是我们自己做的。

我们干的这些事儿,就完美对应了五层协议中的应用层 ——

image-20220903204542363

2. http

虽然说, 上面的应用层协议是我们自己定的. 但实际上, 已经有大佬们定义了一些现成的, 又非常好用的应用层协议, 供我们直接参考使用. HTTP(超文本传输协议)就是其中之一. 但本质上和我们刚刚写的网络计算器没有区别,都是应用层协议

  • 网络通信
  • 序列化和反序列化
  • 协议细节

在http内部都已实现。

2.1 认识url - 网址

我们请求的图片、html、css、jx、视频、音频、标签、文档等这些都称之为“资源”

我们可以用IP+Port确认一个进程,但是无法唯一确认一个资源,公网IP地址是唯一一台主机的,而网络“资源”是存在于网络中的一台Linux机器上。Linux或者传统的操作系统,都是以文件的方式保存资源的。单Linux系统,表示一个唯一资源的方式是通过路径的。

所以,IP + Linux路径,就可以唯一的确认一个网络资源

  • IP通常是以域名方式呈现的

  • 路径可以通过目录名+/确认

image-20220903212124794

urlencode和urldecode

像 / ? : 等这样的字符, 已经被url当做特殊意义理解了,因此这些字符不能随意出现.

如果某个参数中需要带有这些特殊字符, 就必须先对特殊字符进行转义 —— 转义的规则如下

将需要转码的字符转为16进制,然后从右到左,取4位(不足4位直接处理),每2位做一位,前面加上%,编码成%XY格式

encode编码:

image-20220903213000604

还需要decode解码。

2.2 http格式

简化认识:

  • 无论请求还是响应,http都是按照为单位(\n)构建请求或者响应的。

  • 无论是请求还是响应,几乎都是由3或4部分组成

image-20220904100339231

如何理解普通用户的上网行为 —— IO

  • 从目标拿到你要的资源
  • 向目标服务器上传你的数据

思考 —— http request 和 http response被如何看待~

我们可以把请求/响应看做一个大的字符串

  • http如何解包?如何封装?如何分用?

    • 解包/封装:用空行\n这个特殊字符,将请求和协议一分为二
    • 分用:不是http解决的,是用具体的应用代码解决的,http需要有接口来帮助上层获取参数 //TODO 等待详谈
  • http请求/响应,是如何被发送的?是如何被读取的?

我们接下来的demo中用recv来读,这种读法是不正确的,只不过现在没有被暴露出来罢了。。

我们需要保证 ——

  • 每次读取都是读取完整的一个http request
    • 如何判定我们将报头部分读完了呢?读到空行,分离报头和有效载荷(解包)。就能提取报头中的各种属性,包括**Content-Length**,自描述字段。
    • 决定后面还有没有正文?这和请求方法有关(我们接下来会验证);如果有正文,如何保证把正文全部读取完成呢? 上面那个字段表明正文部分有多少字节,帮助我们完整的读取http请求/响应。
    • 会有不存在Content-Length的情况,就是没有正文
    • 如上所说,是规定、协议!
  • 每次读取都不要将下一个http请求的一部分读到(后面详谈)

HTTP请求

  • 首行:[方法] + [url] + [版本]

  • Header: 请求的属性, 冒号分割的键值对;每组属性之间使用\n分隔;遇到空行表示Header部分结束

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

HTTP响应

  • 首行: [版本号] + [状态码] + [状态码描述]

  • Header: 请求的属性, 冒号分割的键值对;每组属性之间使用\n分隔;遇到空行表示Header部分结束

  • Body: 空行后面的内容都是Body. Body允许为空字符串. 如果Body存在, 则在Header中会有一个Content-Length属性来标识Body的长度; 如果服务器返回了一个html页面, 那么html页面内容就是在body中.

2.3 http的操作

  • 我想看看请求报头
  • 我想发送一个响应

为了方便测试,我们引入这两个为tcp订制的接口,面向字节流读取的函数 ——

#include <sys/types.h>
#include <sys/socket.h>

ssize_t recv(int sockfd, void *buf, size_t len, int flags);

ssize_t send(int sockfd, const void *buf, size_t len, int flags);

对比read/write函数,它们几乎没有差别,我们也不关心flag默认为0

#include <unistd.h>

ssize_t read(int fd, void *buf, size_t count);

ssize_t write(int fd, const void *buf, size_t count);

http.cc

#include"Sock.hpp"
#include<pthread.h>

void Usage(std::string proc)
{
    std::cout << "Usage: " << proc << "port" << std::endl; 
}

void* HandlerHttpRequest(void* args)
{
    int sock = *(int*)args;
    delete (int*)args;
    pthread_detach(pthread_self());
    
#define SIZE 1024*10
    char buffer[SIZE]; //一个大字符串
    memset(buffer, 0, sizeof(buffer));

    //接收请求响应
    ssize_t s = recv(sock, buffer, sizeof(buffer), 0);
    if(s > 0)
    {
        buffer[s] = 0;
        std::cout << buffer; //查看http的请求格式 - for test

        std::string http_response = "http/1.0 200 OK\n";//状态行
        http_response +="Content-Type: text/plain\n"; //有效载荷的类型 - 正文是普通的文本
        http_response += "\n"; //传说中的空行,用来区分报头和有效载荷
        http_response += "hello beatles";
        send(sock, http_response.c_str(), http_response.size(), 0); 
    }
    close(sock);
    return nullptr;
}

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

    uint16_t port = atoi(argv[1]);
    int listen_sock = Sock::Socket();
    Sock::Bind(listen_sock, port);
    Sock::Listen(listen_sock);

    for(;;)
    {
        int sock = Sock::Accept(listen_sock);
        if(sock > 0)
        {
            std::cout << "a new link" << std::endl;
            pthread_t tid;
            int* parm = new int(sock);
            pthread_create(&tid, nullptr, HandlerHttpRequest, parm);
        }
    }
    return 0;
}

这样就实实在在的看到了请求报头 ——

image-20220906164927622 image-20220906165016139

2.4 http的方法

早期http版本是短链接,一个请求,一次响应,close socket,一个请求一般就是请求一个资源,链接自动关闭。

image-20220906194601674

大部分服务器为了安全只会暴露 GET,POST,HEAD方法

http请求中的/不是根目录,而叫做web根目录

image-20220906202628869

准备工作 ——

http.cc

  • Content-Type类型:正文类型

  • Content-Length:获取文件大小,我们今天用stat获取文件特定属性 - 大小

    #include <sys/types.h>
    #include <sys/stat.h>
    #include <unistd.h>
    
    int stat(const char *path, struct stat *buf);
    
    //buf -输出型参数
    struct stat {
           dev_t     st_dev;     /* ID of device containing file */
           ino_t     st_ino;     /* inode number */
           mode_t    st_mode;    /* protection */
           nlink_t   st_nlink;   /* number of hard links */
           uid_t     st_uid;     /* user ID of owner */
           gid_t     st_gid;     /* group ID of owner */
           dev_t     st_rdev;    /* device ID (if special file) */
           off_t     st_size;    /* total size, in bytes */
           blksize_t st_blksize; /* blocksize for file system I/O */
           blkcnt_t  st_blocks;  /* number of 512B blocks allocated */
           time_t    st_atime;   /* time of last access */
           time_t    st_mtime;   /* time of last modification */
           time_t    st_ctime;   /* time of last status change */
       };
    

http.cc

#include"Sock.hpp"
#include<pthread.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include<fstream>

#define WWWPORT "./wwwroot/"
#define HOME_PAGE "index.html"

void Usage(std::string proc)
{
    std::cout << "Usage: " << proc << "port" << std::endl; 
}

void* HandlerHttpRequest(void* args)
{
    int sock = *(int*)args;
    delete (int*)args;
    pthread_detach(pthread_self());
    
#define SIZE 1024*10
    char buffer[SIZE]; //一个大字符串
    memset(buffer, 0, sizeof(buffer));

    //接收请求响应
    ssize_t s = recv(sock, buffer, sizeof(buffer), 0);
    if(s > 0)
    {
        buffer[s] = 0;
        std::cout << buffer; //查看http的请求格式 - for test

        //响应 - 返回首页
        std::string html_file = WWWPORT;
        html_file += HOME_PAGE;
        struct stat st;
        stat(html_file.c_str(), &st);

        //返回时,不仅要返回正文的网页信息,还要包括http请求
        //1.状态行
        std::string http_response = "http/1.0 200 OK\n";
        //2.响应报头
        http_response +="Content-Type: text/html; charset=utf8\n"; //正文(有效载荷)的类型 - 
        http_response += "Content-Length: ";
        http_response += std::to_string(st.st_size);
        http_response += "\n";
        //3.空行
        http_response += "\n";
        // 接下来才是正文
        std::ifstream in(html_file);
        if(!in.is_open())
        {
            std::cerr << "open html error" << std::endl;
        }
        else
        {
            std::string content;
            std::string line;
            while(std::getline(in, line))
            {
                content += line;
            }
            http_response += content;
            in.close();
            send(sock, http_response.c_str(), http_response.size(), 0); 
        }

    }
    close(sock);
    return nullptr;
}

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

    uint16_t port = atoi(argv[1]);
    int listen_sock = Sock::Socket();
    Sock::Bind(listen_sock, port);
    Sock::Listen(listen_sock);

    for(;;)
    {
        int sock = Sock::Accept(listen_sock);
        if(sock > 0)
        {
            pthread_t tid;
            int* parm = new int(sock);
            pthread_create(&tid, nullptr, HandlerHttpRequest, parm);
        }
    }
    return 0;
}
image-20220906214930105

Makefile 同上。如上只是把我们简陋的字符串版本改成了文件版。

wwwroot/index.html

  • wwwroot称为web根目录,wwwroot下放置的内容都叫做资源
  • wwwroot目录下的index.html就叫做网站的首页
<!DOCTYPE html>

<html>
    <head>
        <meta charset="utf-8">
    </head>
    <body>
        <h3>小边's Home</h3>
    </body>


</html>

接下来,就要验证GET和POST方法啦~

2.4.1 GET

HTML 教程 (w3school.com.cn)

index.html

<!DOCTYPE html>

<html>
    <head>
        <meta charset="utf-8">
    </head>
    <body>
        <h5>long time no see!! I am missing you~</h5>
        <h5>我是表单!您是谁</h5>
        <!-- /a/b/handler_form 这个路径并不存在,我们目前也不处理,因为我们今天的重点在GET -->
        <form action="/a/b/handler_form" method="GET">
            姓名: <input type="text" name="name">
            密码: <input type="password" name="password">
            <input type="submit" value="提交">
        </form>
    </body>
</html>

会把表单内容拼接到url后面,这样前端的数据就被后端C++程序拿到了 ——

image-20220907191024541

2.4.2 POST

index.html

只需将表单中的GET方法改为POST方法 ——

<!DOCTYPE html>

<html>
    <head>
        <meta charset="utf-8">
    </head>
    <body>
        <h5>long time no see!! I missed you~</h5>
        <h5>我是表单!您是谁</h5>
        <!-- /a/b/handler_form 这个路径并不存在,我们目前也不处理,因为我们今天的重点在GET -->
        <form action="/a/b/handler_form" method="POST">
            姓名: <input type="text" name="name">
            密码: <input type="password" name="password">
            <input type="submit" value="提交">
        </form>
    </body>
</html>

image-20220907191756843

2.4.3 小总结

概念

  • GET:获取方法,一般所有的网页默认的都是GET方法,但是GET也能提交参数 - 通过url参数拼接从而提交给server端。
  • POST:推送方法,以比较常用的参数提交方法,但是一般是通过正文部分提交的,Content-Length表示参数的长度。

区别

参数提交的位置不同

  • GET:GET方法不私密,会回显到浏览器url输入框,增加了被盗取的风险。GET通过url传参,而url是有大小限制的,这和具体的浏览器有关。
  • POST:POST方法比较私密(!=安全),因为不会回显到浏览器url输入框。POST由正文部分传参,一般大小没有限制。

如何选择

GET:提交的参数不敏感,数量非常少;否则用POST

http协议处理,本质是文本分析 ——

  • http协议本身的字段
  • 提取参数,如果有的话。GET和POST其实是前后端交互的一个重要方式。

2.5 http的状态码

应用层是人要参与的,这里的人“水平”参差不齐,很多人根本就不清楚如何使用http的状态码,又因为浏览器种类太多,导致大家对状态码的支持比较混乱。这样类似于404的状态码,对浏览器没什么指导意义,它就是“正常”显示网页。

image-20220907195929467

1XX:100, 101表示请求正在被处理,服务器收到你的请求,但是处理你的请求需要很多时间,给你返回一个响应
2XX:200请求正常处理完毕 - OK,我们的刚刚自己构建的就是200
3XX:重定向。有301, 302, 303, 307, 308不同的浏览器,不同的版本对于这种重定向的处理机制是不一样的,我们接下来会测试
4XX:403 (Forbidden) 禁止访问;404 (Not Found)你访问的资源不存在,属于客户端问题.
5XX:服务器错误。比如来了一个请求,创建线程或者进程失败;处理请求时,做字符串分析时出现问题,程序崩溃。比如,500, 503,504(Bad Gateway)

#include "Sock.hpp"
#include <pthread.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fstream>

#define WWWPORT "./wwwroot/"
#define HOME_PAGE "index.html-bak"

void Usage(std::string proc)
{
    std::cout << "Usage: " << proc << "port" << std::endl;
}

void *HandlerHttpRequest(void *args)
{
    int sock = *(int *)args;
    delete (int *)args;
    pthread_detach(pthread_self());

#define SIZE 1024 * 10
    char buffer[SIZE]; //一个大字符串
    memset(buffer, 0, sizeof(buffer));

    //接收请求响应
    ssize_t s = recv(sock, buffer, sizeof(buffer), 0);
    if (s > 0)
    {
        buffer[s] = 0;
        std::cout << buffer; //查看http的请求格式 - for test

        //响应 - 返回首页
        std::string html_file = WWWPORT;
        html_file += HOME_PAGE;

        // 接下来才是正文
        std::ifstream in(html_file);
        if (!in.is_open())
        {
            std::cerr << "open html error" << std::endl;
            // 1.状态行
            std::string http_response = "http/1.0 404 not Found\n";
            // 2.响应报头
            http_response += "Content-Type: text/html; charset=utf8\n"; //正文(有效载荷)的类型
            // 3.空行
            http_response += "\n";

            http_response += "<html><p>你访问的资源走丢了~~</p></html>";
            send(sock, http_response.c_str(), http_response.size(), 0);
        }
        else
        {
            struct stat st;
            stat(html_file.c_str(), &st);

            //返回时,不仅要返回正文的网页信息,还要包括http请求
            // 1.状态行
            std::string http_response = "http/1.0 200 ok\n";
            // 2.响应报头
            http_response += "Content-Type: text/html; charset=utf8\n"; //正文(有效载荷)的类型 -
            http_response += "Content-Length: ";
            http_response += std::to_string(st.st_size);
            http_response += "\n";
            // 3.空行
            http_response += "\n";
            std::string content;
            std::string line;
            while (std::getline(in, line))
            {
                content += line;
            }
            http_response += content;
            in.close();

            send(sock, http_response.c_str(), http_response.size(), 0);
        }
    }
    close(sock);
    return nullptr;
}

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

    uint16_t port = atoi(argv[1]);
    int listen_sock = Sock::Socket();
    Sock::Bind(listen_sock, port);
    Sock::Listen(listen_sock);

    for (;;)
    {
        int sock = Sock::Accept(listen_sock);
        if (sock > 0)
        {
            pthread_t tid;
            int *parm = new int(sock);
            pthread_create(&tid, nullptr, HandlerHttpRequest, parm);
        }
    }
    return 0;
}
image-20220907203717353

我们重点来谈3xx的状态码 —— 重定向

  • 永久重定向:301、
  • 临时重定向:302、307

有时,我们访问某一个网站,可能会跳转到另一个网址:比如当我访问某种资源时,提示我登陆,于是我便跳转到了登录页面;当我输完密码,会自动再跳转回来。这种现象,都叫做重定向。

所谓永久和临时,永久重定向通常用于网站搬迁、域名更换;临时重定向,每次都要经历跳转,属于业务环节。

模拟301

注意,重定向时需要浏览器给我们提供支持的:必须能识别301/302/307,server要告诉浏览器(客户端),接下来应该去哪里?

报头属性Location:新的地址

http.cc

#include "Sock.hpp"
#include <pthread.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fstream>

#define WWWPORT "./wwwroot/"
#define HOME_PAGE "index.html"

void Usage(std::string proc)
{
    std::cout << "Usage: " << proc << "port" << std::endl;
}

void *HandlerHttpRequest(void *args)
{
    int sock = *(int *)args;
    delete (int *)args;
    pthread_detach(pthread_self());

#define SIZE 1024 * 10
    char buffer[SIZE]; //一个大字符串
    memset(buffer, 0, sizeof(buffer));

    //接收请求响应
    ssize_t s = recv(sock, buffer, sizeof(buffer), 0);
    if(s > 0)
    {
        buffer[s] = 0;
        std::cout << buffer; //查看http的请求格式 
        std::string response = "http/1.1 301 Permantly moved\n";
        response += "Location: https://new.qq.com/\n"; 
        response += "\n";
        send(sock, response.c_str(), response.size(), 0);
    }
    close(sock);
    return nullptr;
}

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

    uint16_t port = atoi(argv[1]);
    int listen_sock = Sock::Socket();
    Sock::Bind(listen_sock, port);
    Sock::Listen(listen_sock);

    for (;;)
    {
        int sock = Sock::Accept(listen_sock);
        if (sock > 0)
        {
            pthread_t tid;
            int *parm = new int(sock);
            pthread_create(&tid, nullptr, HandlerHttpRequest, parm);
        }
    }
    return 0;
}

于是我们自动跳转到了腾讯网 ——

image-20220907214432417

2.6 http的常见header

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

我们之前所有的实验,全部都是请求→响应→断开连接 ——
http/1.0采用的网络请求方案是短链接,当我们访问一个由多个元素构成的一个大型的网页资源时,就要发起多次http请求(基于短链接),http协议是基于tcp协议的,所以每一次的http request都要执行 建立链接→ 传送数据→断开连接,但是这样效率较低。

这样http/1.1后支持长链接 ,需要设置Connection属性:keep-alive,通过减少频繁建立tcp链接,来达到提高效率的目的。

2.7 cookie 和 session

我们有这样的经验,我们网站中,各种页面跳转时,本质就是进行各种http请求,网站照样认识我。但是http协议本身是一种无状态的协议(因为简单),并不记录请求的历史信息。这样看似是矛盾的,但是“让网站认识我”并不是http协议本身要解决的问题,而是做网络资源获取,但是http可以提供一些技术支持来保证网站具有“会话保持”的功能 —— cookie & session。

cookie

会话保持

  • 浏览器角度:cookie其实是一个文件,该文件保存的是我们用户的私密信息
  • http协议角度:一旦该网站对应有cookie,在发起任何请求的时候,都会自动在request中携带该cookie信息

这个cookie就是在浏览器(客户端)中,首次登陆时浏览器会自动保存登录相关信息到cookie文件,后续的请求,浏览器会将每一个请求都会请求报头属性中,自动携带对应的cookie。

cookie有两种形式 ——

  • 文件版
  • 内存版

如果别人盗取了我的cookie文件,他就可以以我的身份进行认证,访问特定资源如果保存的是用户名和密码,那就很糟糕了~~

所以单纯使用cookie是有安全隐患的 —— session,但是不代表我们不用cookie~

session

将用户的私密信息,保存在服务端。

image-20220908203119158

后序所有的http请求,都会由浏览器自动携带cookie内容 —— 当前用户的session_id,server依旧可以做到认识client,这也是一种会话保持的功能。

由于客户端的cookie文件中,不再直接保存用户私密信息,就不会直接泄漏啦~ 但是的确还有cookie文件被泄漏的可能,如果别人也去访问我们对应的网址,还是会去访问我们对应的网址,但我们也没办法啦~ 但是也衍生了很多的防御方案 —— 比如异地登录重新生成session_id、短信认证等等

cookie+session本质是提高用户访问网站或者平台的体验。

3. https (了解)

3.1 “加密”

http = http +TLS/SSL (数据的加密解密层)

image-20220909204632170

加密方式

  • 对称加密 - 秘钥(只有一个)

    用X加密,也要用X解密

  • 非对称加密

    有一对秘钥:公钥和私钥。可以用公钥加密,但是只能用私钥解密;或者用私钥加密,只能用公钥解密。例如RSA。

    一般而言,公钥是群世界众所周知的,私钥是必须自己进行私有保存的。

如何识别/防止文本中的内容被篡改?

回忆起了上学期一门写字儿就给分儿的课 —— 信息安全概论,第几章来着,密码学。。幸好逃离了。。言晨而应该懂这个,不过好像挺难?!

image-20220909213034077

秘钥协商,采用对称的方式是有安全隐患的!

3.2 https通信过程

如何选择加密算法?

  • 对称
  • 非对称
image-20220910090222954

看起来有两对非对称秘钥,就能保证数据双向的安全,但事实并非如此 ——

  • 依旧有被非法窃取的风险
  • 非对称较密算法,特别费时间

实际上,采取的是非对称 + 对称方案 ——

  • 用非对称的方式交换对称秘钥

  • 用对称方案进行数据通信

image-20220910094250383

事实上,第一次把公钥S给client会不会出现问题呢?

image-20220910105411890

这样以来,中间人就拿到了就拿到了接下来用于通信的对称秘钥的私钥X. 然而client无法判断秘钥协商报文是不是从合法的服务方发来的。

那怎么办呢?于是有CA证书机构(Certificate Authority) —— 只有一个服务商经过权威机构认证,该机构才合法。

  • 申请证书:提供企业信息;域名;公钥
  • 创建证书:企业基本信息(域名,公钥);由这段文本(hash散列形成数据指纹)形成的数据签名
    • 权威
    • 有自己的公钥A 和 私钥A`
image-20220910112646568
  • 要求client必须知道CA机构的公钥信息用来解密数字签名。那它如何得知呢?这个一般是内置的。另外,证书颁发具有“传递性”,所以也有一部分在访问网址是,浏览器会提示用户进行安装

因为CA的公钥是全世界众所周知的,但是CA的私钥只有CA自己知道,换言之,只有CA机构能重新形成对应的数字签名,因此即便中间人可以改内容,但,无法更改数字签名,因为他没有CA机构的私钥,用公钥解密再更改也无法再加密。

那如果中间人也是一个合法的服务方呢。。不行~ 因为基本信息中有“域名”,你请求的域名和“合法中间人”的域名那一定是会变化而被察觉到的。。

今天才知道证书是这么回事儿啊~~

  网络协议 最新文章
使用Easyswoole 搭建简单的Websoket服务
常见的数据通信方式有哪些?
Openssl 1024bit RSA算法---公私钥获取和处
HTTPS协议的密钥交换流程
《小白WEB安全入门》03. 漏洞篇
HttpRunner4.x 安装与使用
2021-07-04
手写RPC学习笔记
K8S高可用版本部署
mySQL计算IP地址范围
上一篇文章      下一篇文章      查看所有文章
加:2022-09-13 11:52:32  更:2022-09-13 11:54:52 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/19 11:42:19-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码