协议的定制之序列化与反序列化 | 守护进程

ops/2024/10/11 13:28:39/

目录

一、再谈协议

二、序列化与反序列化

三、网络计算器的简单实现

四、网络计算器完整代码

五、代码改进

六、守护进程

七、Json序列化与反序列化

八、netstat


一、再谈协议

是对数据格式和计算机之间交换数据时必须遵守的规则的正式描述。简单的说了,网络中的计算机要能够互相顺利的通信,就必须讲同样的语言,语言就相当于协议。

为了使数据在网络上能够从源主机到达目的主机,网络通信的参与方必须遵循相同的规则,我们将这套规则称为协议。只有使用相同的协议,主机间才能进行通信。 

二、序列化与反序列化

今天,我们作为客户端,想要让服务端帮助我们进行计算,然后将结果返回给我们,这就是一个网络计算器了。如果我们要计算两数相加,比如 a+b,那么现在问题来了,我们应该如何将a,+,b这三个数据传输给服务器呢?是一个一个传呢,还是整体传输呢?

如果客户端将这些结构化的数据单独一个个的发送到网络当中,那么服务端从网络当中获取这些数据时也只能一个个获取,此时服务端还需要考虑,哪个是左操作数,哪个是操作符,哪个是右操作数。所以这样不合适。

对于一起发送。a,+,b三个数据组成一组结构化的数据,我们首先想到使用一个结构体将三个数据打包,一起发送给服务器,而我们所用的各种通信函数只允许我们传输字符串,所以我们必须要将结构化的数据先转化成字符串,然后才能发送给服务器。而服务器接收到字符串后,则必须将字符串进行解析,转成结构化的数据,进行计算,然后将结果转成字符串发回客户端。这个过程就叫做“序列化”和“反序列化”。

~ 序列化:就是将对象的状态信息转换为可以存储或传输的形式(字节序列)的过程。

~ 反序列化:就是把字节序列恢复为对象的过程。

具体怎么实现,我们通过编写网络计算器来进行讲解。

三、网络计算器的简单实现

对于网络计算器的客户端,我们使用一个结构体去存储左右操作数和操作符,这个结构体我们称为请求(Request),在向服务端发送请求的时候,我们需要转成字符串才能发送,于是我们需要有序列化的函数,当然,收到结果后,我们也需要反序列化函数,将字符串结果转成结构化结果。

对于服务端,我们使用一个结构体去存储计算结果和标识计算结果的正确性,因为客户端可能会发过来除0或者模0的请求。在收到客户端的请求后,我们需要反序列化函数将结果转成结构化数据,方便计算,发回结果的时候,需要序列化函数将结果转成字符串才能发送。

四、网络计算器完整代码

Sock.hpp

我们对网络套接字进行一个封装。

#pragma once
#include <iostream>
#include <string>
#include <cstdbool>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>class Sock
{
public:const static int gmv = 20;Sock(){}int Socket(){// 1.创建套接字int sock = socket(AF_INET, SOCK_STREAM, 0);if (sock < 0){std::cout << "创建套接字失败!" << std::endl;exit(1);}std::cout << "创建套接字成功!" << std::endl;return sock;}void Bind(int sock, uint16_t port, std::string ip = "0.0.0.0"){// 2.进行绑定struct sockaddr_in src_server;bzero(&src_server, sizeof(src_server));src_server.sin_family = AF_INET;src_server.sin_port = htons(port);inet_pton(AF_INET, ip.c_str(), &src_server.sin_addr);socklen_t len = sizeof(src_server);if (bind(sock, (struct sockaddr *)&src_server, len) < 0){std::cout << "绑定失败!" << std::endl;exit(2);}std::cout << "绑定成功!" << std::endl;}void Listen(int sock){// 3.开始监听,等待连接if (listen(sock, gmv) < 0){std::cout << "监听失败!" << std::endl;exit(3);}std::cout << "服务器监听成功!" << std::endl;}int Accept(int sock, std::string *ip, uint16_t *port){// 4.获取链接struct sockaddr_in client_sock;socklen_t len = sizeof(client_sock);int serversock = accept(sock, (struct sockaddr *)&client_sock, &len);if (serversock < 0){std::cout << "获取链接失败!" << std::endl;return -1;}if (port)*port = ntohs(client_sock.sin_port);if (ip)*ip = inet_ntoa(client_sock.sin_addr);return serversock;}bool Connect(int sock, const std::string &ip, const 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)return true;elsereturn false;}~Sock(){}
};

TcpServer.hpp: 

#pragma once
#include "Sock.hpp"
#include <functional>
#include <pthread.h>using func_t = std::function<void(int)>;class TcpServer;
class threaddata
{
public:threaddata(int sock, TcpServer *server) : sock_(sock), server_(server) {}~threaddata() {}public:int sock_;TcpServer *server_;
};class TcpServer
{
private:static void *threadRoutine(void *arg){pthread_detach(pthread_self());threaddata *td = (threaddata *)arg;td->server_->excute(td->sock_);close(td->sock_);delete td;}public:TcpServer(const uint16_t &port, const std::string &ip = "0.0.0.0"){listensock_ = sock_.Socket();sock_.Bind(listensock_, port, ip);sock_.Listen(listensock_);}void BindServer(func_t func){func_ = func;}void excute(int sock){func_(sock);}void Start(){for (;;){std::string clientip;uint16_t clientport;int sock = sock_.Accept(listensock_, &clientip, &clientport);if (sock == -1)continue;threaddata *td = new threaddata(sock, this);pthread_t tid;pthread_create(&tid, nullptr, threadRoutine, td);}}~TcpServer(){if (listensock_ >= 0)close(listensock_);}private:int listensock_;Sock sock_;func_t func_;
};

Protocol.hpp:协议定制

我们规定序列化的结果是"x_ op_ y_”,即:左操作数,空格,操作符,空格,左操作数。

#pragma once
#include <iostream>
#include <string>
#include <cstdbool>
#include <sys/types.h>
#include <sys/socket.h>
#include <cstdlib>
#include <cstring>#define SPACE " "
#define SPACELEN strlen(SPACE)std::string Recv(int sock)
{char buffer[1024];ssize_t s = recv(sock, buffer, sizeof buffer, 0);if (s == 0)std::cout << "客户端退出!" << std::endl;else if (s > 0)return buffer;elsestd::cerr << "客户端退出!" << std::endl;return "";
}void Send(int sock, const std::string &str)
{ssize_t s = send(sock, str.c_str(), str.size(), 0);
}class Request
{
public:std::string Serialize(){std::string requeststr = std::to_string(x_);requeststr += SPACE;requeststr += op_;requeststr += SPACE;requeststr += std::to_string(y_);return requeststr;}// x_ op_ y_bool Deserialization(const std::string &str){std::size_t left = str.find(SPACE);if (left == std::string::npos)return false;std::size_t right = str.rfind(SPACE);if (right == std::string::npos)return false;x_ = atoi(str.substr(0, left).c_str());y_ = atoi(str.substr(right + SPACELEN).c_str());op_ = str[left + SPACELEN];return true;}public:Request() {}Request(const int &x, const int &y, const char &op) : x_(x), y_(y), op_(op) {}~Request() {}public:int x_;int y_;char op_;
};class Response
{
public:// code_ result_std::string Serialize(){std::string str = std::to_string(code_);str += SPACE;str += std::to_string(result_);return str;}bool Deserialization(std::string &str){std::size_t pos = str.find(SPACE);if (pos == std::string::npos)return false;code_ = atoi(str.substr(0, pos).c_str());result_ = atoi(str.substr(pos + SPACELEN).c_str());return true;}public:Response(const int &result = 0, const int &code = 0) : result_(result), code_(code) {}~Response() {}public:int result_;int code_;
};

CalServer.cc:

#include "tcpserver.hpp"
#include "Protocol.hpp"
#include <signal.h>
#include <memory>static void usage(std::string proc)
{std::cout << proc << " port" << std::endl;
}Response CalculatorHelper(const Request &req)
{Response res(0, 0);switch (req.op_){case '+':res.result_ = req.x_ + req.y_;break;case '-':res.result_ = req.x_ - req.y_;break;case '*':res.result_ = req.x_ * req.y_;break;case '/':if (0 == req.y_)res.code_ = 1;elseres.result_ = req.x_ / req.y_;break;case '%':if (0 == req.y_)res.code_ = 2;elseres.result_ = req.x_ % req.y_;break;default:res.code_ = 3;break;}return res;
}void Calculator(int sock)
{while (true){std::string buffer = Recv(sock);if (!buffer.empty()){Request req;req.Deserialization(buffer);Response res = CalculatorHelper(req);std::string sendstr = res.Serialize();Send(sock, sendstr);}else break;}
}int main(int argc, char *argv[])
{if (argc != 2){usage(argv[0]);exit(1);}signal(SIGPIPE, SIG_IGN);uint16_t server_port = atoi(argv[1]);std::unique_ptr<TcpServer> sev(new TcpServer(server_port));sev->BindServer(Calculator);sev->Start();return 0;
}

CalClient.cc:

#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <cstdlib>
#include <unistd.h>
#include "Sock.hpp"
#include "Protocol.hpp"static void usage(std::string proc)
{std::cout << proc << " ip port" << std::endl;
}int main(int argc, char *argv[])
{if (argc != 3){usage(argv[0]);exit(1);}std::string ip = argv[1];uint16_t port = atoi(argv[2]);Sock sock;int sockfd = sock.Socket();if (!sock.Connect(sockfd, ip, port)){std::cout << "连接出错!" << std::endl;exit(2);}std::cout << "连接成功!" << std::endl;while (true){Request req;std::cout << "请输入x # ";std::cin >> req.x_;std::cout << "请输入y # ";std::cin >> req.y_;std::cout << "请输入op # ";std::cin >> req.op_;std::string sendstr = req.Serialize();Send(sockfd, sendstr);std::string recstr = Recv(sockfd);Response res;res.Deserialization(recstr);std::cout << "code_: " << res.code_ << std::endl;std::cout << "result_: " << res.result_ << std::endl;}return 0;
}

运行结果:

五、代码改进

其实,上面我们 Protocol.hpp 中的代码是有问题的?为什么呢?

我们早就说过,TCP是面向字节流的,所以在从网络中读取的时候,并不能保证发送和读取到的是一个完整的 x_ op_ y_ 结构,可能只发送或者读取了 x_ op_,也可能是 x_ op_ y_  x_ op_ y_  x_ op_ y_。所以客户端和服务器不能准确区分,因此我们需要对发送和读取进行控制,使得每次发送和读取到的都是一个完整的报文。

我们所使用的TCP协议是传输层协议。在TCP层,拥有两个缓冲区:发送缓冲区和接收缓冲区。我们调用的所有发送函数,并不是直接把数据发送到网络中,而是将数据由应用层拷贝到TCP的发送缓冲区中,由TCP协议决定如何发送这些数据和每次发送多少数据。接收函数也不是直接从网络中获取数据,而是从发送缓冲区拷贝数据到应用层。至于数据如何到TCP的接收缓冲区,也是完全由TCP协议决定。

因此,发送函数和接收函数本质上是拷贝函数。

因此我们可以将序列化的数据定成这种结构  length\r\nx_ op_ y_\r\n(或者 length\r\ncode_ result_\r\n) ,我们通过length来标定正文长度,使用\r\n来分隔length和正文。

知道length,就可以知道怎样读取多长的数据了,这样就可以读取到完整的报文。而发送的时候任然是面向字节流式的发送,不过我们需要添加 length 和特殊符号 \r\n,再发送。

在Protocol.hpp:协议定制里面,我们还需要修改下面的代码:Encode:将序列化的字符串转成  length\r\nx_ op_ y_\r\n。Decode:将 length\r\nx_ op_ y_\r\n 转成 x_ op_ y_(即拿到正文)。

#pragma once
#include <iostream>
#include <string>
#include <cstdbool>
#include <sys/types.h>
#include <sys/socket.h>
#include <cstdlib>
#include <cstring>#define SPACE " "
#define SPACELEN strlen(SPACE)
#define SEP "\r\n"
#define SEPLEN strlen(SEP)bool Recv(int sock, std::string *out)
{char buffer[1024];ssize_t s = recv(sock, buffer, sizeof buffer - 1, 0);if (s == 0){std::cout << "客户端退出!" << std::endl;return false;}else if (s > 0){buffer[s] = 0;*out += buffer;}else{std::cerr << "客户端退出!" << std::endl;return false;}return true;
}void Send(int sock, const std::string &str)
{ssize_t s = send(sock, str.c_str(), str.size(), 0);
}// len\r\nx op y\r\n
std::string Decode(std::string buffer)
{std::size_t pos = buffer.find(SEP);if (pos == std::string::npos)return "";int size = atoi(buffer.substr(0, pos).c_str());  // 完整正文的长度int mainsize = buffer.size() - pos - 2 * SEPLEN; // 正文长度if (mainsize >= size){buffer.erase(0, pos + SEPLEN);std::string result = buffer.substr(0, size);buffer.erase(0, size + SEPLEN);return result;}elsereturn "";
}std::string Encode(std::string &s)
{std::string ret = std::to_string(s.size());ret += SEP;ret += s;ret += SEP;return ret;
}class Request
{
public:std::string Serialize(){std::string requeststr = std::to_string(x_);requeststr += SPACE;requeststr += op_;requeststr += SPACE;requeststr += std::to_string(y_);return requeststr;}// x_ op_ y_bool Deserialization(const std::string &str){std::size_t left = str.find(SPACE);if (left == std::string::npos)return false;std::size_t right = str.rfind(SPACE);if (right == std::string::npos)return false;x_ = atoi(str.substr(0, left).c_str());y_ = atoi(str.substr(right + SPACELEN).c_str());op_ = str[left + SPACELEN];return true;}public:Request() {}Request(const int &x, const int &y, const char &op) : x_(x), y_(y), op_(op) {}~Request() {}public:int x_;int y_;char op_;
};class Response
{
public:// code_ result_std::string Serialize(){std::string str = std::to_string(code_);str += SPACE;str += std::to_string(result_);return str;}bool Deserialization(std::string &str){std::size_t pos = str.find(SPACE);if (pos == std::string::npos)return false;code_ = atoi(str.substr(0, pos).c_str());result_ = atoi(str.substr(pos + SPACELEN).c_str());return true;}public:Response(const int &result = 0, const int &code = 0) : result_(result), code_(code) {}~Response() {}public:int result_;int code_;
};

CalServer.cc:

#include "tcpserver.hpp"
#include "Protocol.hpp"
#include <signal.h>
#include <memory>static void usage(std::string proc)
{std::cout << proc << " port" << std::endl;
}Response CalculatorHelper(const Request &req)
{Response res(0, 0);switch (req.op_){case '+':res.result_ = req.x_ + req.y_;break;case '-':res.result_ = req.x_ - req.y_;break;case '*':res.result_ = req.x_ * req.y_;break;case '/':if (0 == req.y_)res.code_ = 1;elseres.result_ = req.x_ / req.y_;break;case '%':if (0 == req.y_)res.code_ = 2;elseres.result_ = req.x_ % req.y_;break;default:res.code_ = 3;break;}return res;
}void Calculator(int sock)
{std::string str;while (true){bool rest = Recv(sock, &str);if (!rest)break;std::string package = Decode(str);if (!package.empty()){Request req;req.Deserialization(package);Response res = CalculatorHelper(req);std::string sendstr = res.Serialize();sendstr = Encode(sendstr);Send(sock, sendstr);}elsecontinue;}
}int main(int argc, char *argv[])
{if (argc != 2){usage(argv[0]);exit(1);}signal(SIGPIPE, SIG_IGN);uint16_t server_port = atoi(argv[1]);std::unique_ptr<TcpServer> sev(new TcpServer(server_port));sev->BindServer(Calculator);sev->Start();return 0;
}

 CalClient.cc:

#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <cstdlib>
#include <unistd.h>
#include "Sock.hpp"
#include "Protocol.hpp"static void usage(std::string proc)
{std::cout << proc << " ip port" << std::endl;
}int main(int argc, char *argv[])
{if (argc != 3){usage(argv[0]);exit(1);}std::string ip = argv[1];uint16_t port = atoi(argv[2]);Sock sock;int sockfd = sock.Socket();if (!sock.Connect(sockfd, ip, port)){std::cout << "连接出错!" << std::endl;exit(2);}std::cout << "连接成功!" << std::endl;bool quit = false;std::string buffer;while (!quit){Request req;std::cout << "请输入x # ";std::cin >> req.x_;std::cout << "请输入y # ";std::cin >> req.y_;std::cout << "请输入op # ";std::cin >> req.op_;std::string sendstr = req.Serialize();sendstr = Encode(sendstr);Send(sockfd, sendstr);while (true){bool ret = Recv(sockfd, &buffer);if (!ret){quit = true;break;}std::string recstr = Decode(buffer);if (recstr.empty())continue;Response res;res.Deserialization(recstr);std::cout << "code_: " << res.code_ << std::endl;std::cout << "result_: " << res.result_ << std::endl;break;}}close(sockfd);return 0;
}

六、守护进程

后台进程:就是在后台运行,不占用用户终端的进程。

前台进程:前台进程是与用户直接交互的进程(和终端关联的进程)。可以直接获取键盘的输入。

如下:我们的bash就是一个最常见的前台进程,我们输入各种指令,他就能返回相应的结果。

而我们上面所写的服务器进程在启动后,也是在前台运行的,也是一个前台进程。如下:

从上图我们也看到:服务器进程在启动后,如果我们再输入各种指令的话,将不会有任何结果。因为xshell登录后,只允许有一个前台进程和多个后台进程。 

再如下:我们使用管道创建多个进程,

除了PID,PPID之外,PGID我们称为组ID,SID我们称为会话ID。

这三个被同时创建的进程组成了一个进程组,他们的PGID都是23440,也是第一个进程的PID。所以一个进程组的PGID是第一个进程的PID。

我们在登陆了xshell后,xshell会给用户提供一种会话机制,在会话中,包含了给用户提供服务的bash进程,终端,以及用户自己在该会话中启动的进程。当xshell退出登录,会话也会退出。

如下图:

而现在,我希望会话退出后,服务器进程任然也可以运行。那么我们就需要让服务器进程自成一个会话。这样服务器进程就成为了一个守护进程。守护进程也是一种后台进程。

我们使用 setsid()函数,就可以让某个进程变成自成会话。注:setsid要成功被调用,必须保证当前进程不是进程组的组长。

所以,我们自己写一个方法来让服务器进程变成一个守护进程。

一般步骤:

1、忽略信号:SIGPIPE,SIGCHLD

2、不要让自己是进程组的组长,fork()

3、调用setsid()函数

4、因为守护进程不能向显示器打印消息,所以我们需要将标准输出、标准错误和标准输入进行重定向。这里我们需要使用Linux中的一个文件:/dev/null。其特点就是,任何向其中写入的内容都会被其丢弃。因为文件中没有内容,所以读取时什么也读取不到。

daemon.hpp:

#pragma once
#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>void mydaemon()
{// 1.忽略信号signal(SIGPIPE, SIG_IGN);signal(SIGCHLD, SIG_IGN);// 2.不要让自己成为组长if (fork() > 0)exit(0);// 3.调用setsid()setsid();int devnull = open("/dev/null", O_RDONLY | O_WRONLY);if (devnull > 0){dup2(0, devnull);dup2(1, devnull);dup2(2, devnull);close(devnull);}
}

然后,我们只需要在服务器运行前调用这个函数即可。当然,服务器代码中任何向显示器打印的函数都不应该调用了。

CalServer.cc的main函数:

通过查询,发现 MyServer的父进程PPID为1,也就是操作系统。所以说,守护进程也是一种孤儿进程。 

七、Json序列化与反序列化

对于序列化和反序列化,上面我们自己的方案其实也很多问题,所以我们除了可以自定义方案以外,还可以使用别人的已经实现好了的方案。比如我们接下来讲到的:json。

使用前,我们需要安装json库: sudo yum install jsoncpp-devel

Protocol.hpp:

#include <jsoncpp/json/json.h>class Request
{
public:std::string Serialize(){Json::Value root;root["x"] = x_;root["y"] = y_;root["op"] = op_;Json::FastWriter writer;return writer.write(root);}// x_ op_ y_bool Deserialization(const std::string &str){Json::Value root;Json::Reader reader;reader.parse(str, root);x_ = root["x"].asInt();y_ = root["y"].asInt();op_ = root["op"].asInt();return true;}public:Request() {}Request(const int &x, const int &y, const char &op) : x_(x), y_(y), op_(op) {}~Request() {}public:int x_;int y_;char op_;
};class Response
{
public:// code_ result_std::string Serialize(){Json::Value root;root["code"] = code_;root["result"] = result_;Json::FastWriter writer;return writer.write(root);}bool Deserialization(std::string &str){Json::Value root;Json::Reader reader;reader.parse(str, root);code_ = root["code"].asInt();result_ = root["result"].asInt();return true;}public:Response(const int &result = 0, const int &code = 0) : result_(result), code_(code) {}~Response() {}public:int result_;int code_;
};

八、netstat

netstat:我们可以通过netstat命令来查看当前网络的状态,这里我们可以选择携带nlup选项。

netstat常用选项:

-n:直接使用IP地址,而不通过域名服务器。
-l:显示监控中的服务器的Socket。
-t:显示TCP传输协议的连线状况。
-u:显示UDP传输协议的连线状况。
-p:显示正在使用Socket的程序识别码和程序名称。


http://www.ppmy.cn/ops/13644.html

相关文章

解决:第一次用python的pip报错

报错内容如下&#xff1a; Fatal error in launcher: Unable to create process using "C:\Users\admin\AppData\Local\Programs\Python\Python312\python.exe" "C:\Program Files\Python\Python312\Scripts\pip.exe" : ??????????? 参考 如何…

强化训练:day4

文章目录 前言1. 简写单词1.1 题目描述1.2 解题思路1.3 代码实现 2. dd爱框框2.1 题目描述2.2 解题思路2.3 代码实现 3. 除2&#xff01;3.1 题目描述3.2 解题思路3.3 代码实现 总结 前言 今天的题目是&#xff1a;BC149 简写单词、dd爱框框、除2&#xff01;&#xff0c;分别涉…

OpenHarmony语言基础类库【@ohos.url (URL字符串解析)】

说明&#xff1a; 本模块首批接口从API version 7开始支持。后续版本的新增接口&#xff0c;采用上角标单独标记接口的起始版本。 导入模块 import Url from ohos.url URLParams9 URLParams接口定义了一些处理URL查询字符串的实用方法。 constructor9 constructor(init?…

ZooKeeper的分布式锁

ZooKeeper的分布式锁机制主要利用ZooKeeper的节点特性&#xff0c;通过创建和删除节点来实现锁的控制。 实现步骤&#xff1a; 创建锁节点&#xff1a;当一个进程需要访问共享资源时&#xff0c;它会在ZooKeeper中创建一个唯一的临时顺序节点作为锁。尝试获取锁&#xff1a;进…

vue+springboot实验个人信息,修改密码,忘记密码功能实现

前端部分 新增Person&#xff08;个人页面&#xff09;&#xff0c;Password&#xff08;修改密码页面&#xff09;&#xff0c;还需要对Manager&#xff0c;login页面进行修改 router文件夹下的index.js&#xff1a; import Vue from vue import VueRouter from vue-router i…

从图灵奖看计算中的随机性与伪随机性

从图灵奖看计算中的随机性与伪随机性 目录 从图灵奖看计算中的随机性与伪随机性 一、引言 二、随机性的本质与应用 三、图灵奖得主对随机性的研究 四、伪随机性的应用 五、案例研究&#xff1a;伪随机数生成器的发展 六、最佳实践 一、引言 在计算机科学的广阔天地中&…

Java 的 Apache Commons 工具库 助力开发

Apache Commons 是什么&#xff1f; Apache Commons 是由 Apache 软件基金会提供的一系列开源、高质量的 Java 组件集合。它包含了各种常用的、经过严格测试的工具类&#xff0c;弥补了 Java 标准库在功能上的不足。这些组件广泛应用于字符串处理、数据转换、集合操作、文件处…

TCP/IP协议—HTTP

TCP/IP协议—HTTP HTTP协议HTTP通讯特点HTTP通讯流程HTTPS通讯流程 HTTP请求报文请求方法 HTTP应答报文状态码 HTTP协议 超文本传输协议&#xff08;Hypertext Transfer Protocol&#xff0c;HTTP&#xff09;是一种请求-响应的协议&#xff0c;用户可以通过HTTP向服务器上传、…