Linux知识点 -- 网络基础(二)-- 应用层

Linux知识点 – 网络基础(二)-- 应用层

文章目录

  • Linux知识点 -- 网络基础(二)-- 应用层
  • 一、使用协议来实现一个网络版的计算器
    • 1.自定义协议
    • 2.守护进程
    • 3.使用json来完成序列化
  • 二、HTTP协议
    • 1.概念
    • 2.HTTP协议请求和响应的报文格式
    • 3.使用HTTP协议进行网络通信
    • 4.HTTP协议的方法
    • 5.HTTP协议的状态码
    • 6.HTTP协议的报头
    • 7.connetion选项


在这里插入图片描述

一、使用协议来实现一个网络版的计算器

1.自定义协议

定义结构体来表示我们需要交互的信息;
发送数据时将这个结构体按照一个规则转换成字符串, 接收到数据的时候再按照相同的规则把字符串转化回结构体;
这个过程叫做"序列化"和"反序列化”;

在这里插入图片描述

Sock.hpp
将套接字封装成对象,其中包含套接字的创建与连接成员函数

#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <cerrno>
#include <cassert>
#include <unistd.h>
#include <memory>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <ctype.h>
#include "Log.hpp"class Sock
{
private:const static int gbacklog = 20;public:Sock() {}int Socket(){int listensock = socket(AF_INET, SOCK_STREAM, 0);if (listensock < 0){logMessage(FATAL, "create socket error, %d:%s", errno, strerror(errno));exit(2);}logMessage(NORMAL, "create socket success, listensock: %d", listensock);return listensock;}void Bind(int sock, uint16_t port, std::string ip = "0.0.0.0"){struct sockaddr_in local;memset(&local, 0, sizeof local);local.sin_family = AF_INET;local.sin_port = htons(port);inet_pton(AF_INET, ip.c_str(), &local.sin_addr);if (bind(sock, (struct sockaddr *)&local, sizeof local) < 0){logMessage(FATAL, "bind error, %d:%s", errno, strerror(errno));exit(3);}}void Listen(int sock){if (listen(sock, gbacklog) < 0){logMessage(FATAL, "listen error, %d:%s", errno, strerror(errno));exit(4);}logMessage(NORMAL, "init server success");}// 一般经验:// const string& 输入型参数// string* 输出型参数// string& 输入输出型参数int Accept(int listensock, std::string *ip, uint16_t *port){struct sockaddr_in src;socklen_t len = sizeof src;int servicesock = accept(listensock, (struct sockaddr *)&src, &len);if (servicesock < 0){logMessage(ERROR, "accept error, %d:%s", errno, strerror(errno));return -1;}if(port){*port = ntohs(src.sin_port);}if(ip){*ip = inet_ntoa(src.sin_addr);return servicesock;}}bool Connect(int sock, const std::string& server_ip, const uint16_t& server_port){struct sockaddr_in server;memset(&server, 0, sizeof server);server.sin_family = AF_INET;server.sin_port = htons(server_port);server.sin_addr.s_addr = inet_addr(server_ip.c_str());if(connect(sock, (struct sockaddr*)&server, sizeof server) == 0){return true;}else{return false;}}~Sock() {}
};

TcpServer.hpp
封装TCP服务接口的类;
注意:类内回调函数由于参数有this指针,无法正-常回调,因此需要设置成static成员,再通过参数传进this指针,来访问类内非静态成员;

#pragma once#include "Sock.hpp"
#include <vector>
#include <functional>
#include <pthread.h>namespace ns_tcpserver
{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; // 里面有TcpServer对象的指针,由于回调函数是静态成员函数,无法访问非静态成员// 这里的TcpServer对象指针是用来在回调函数中访问非静态成员的};class TcpServer{private://如果是类内成员函数,参数中是有this指针的,多线程回调会出问题//因此需设置成静态成员,才可以回调static void* ThreadRoutine(void* args){pthread_detach(pthread_self());//线程分离ThreadData* td = static_cast<ThreadData*>(args);//类型转换td->_server->Excute(td->_sock);//通过对象this指针调用成员函数close(td->_sock);return nullptr;}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 BindService(func_t func){_func.push_back(func);}// 执行服务void Excute(int sock){for (auto &f : _func){f(sock);}}void Start(){for (;;){std::string cli_ip;uint16_t cli_port;int sock = _sock.Accept(_listensock, &cli_ip, &cli_port);if (sock == -1){continue;}logMessage(NORMAL, "create new link succsee, sock: %d", sock);// 多线程处理请求pthread_t tid;ThreadData *td = new ThreadData(sock, this);pthread_create(&tid, nullptr, ThreadRoutine, td);}}~TcpServer(){if (_listensock >= 0){close(_listensock);}}private:int _listensock;Sock _sock;std::vector<func_t> _func; // 回调函数列表};}

Protocol.hpp
定制协议:
分别有计算请求的序列化和计算结果的序列化;

  • TCP协议的读写接口(read和write)都是将数据拷贝到缓冲区或者从缓冲区拷贝出来,并不是直接发送到对方主机;发送给对方主机是由TCP传输控制协议决定的
  • 由于TCP是面向字节流的协议,因此,发送和接受的次数,每次发送多少字符,都不受控制(UDP协议每次发送和接受的都是完整的报文),有可能每次接收到的不一定是完整的报文,也有可能一次读取了多个报文,所以需要自己定制协议解包代码;在读取时不能简单地receive,而需要对读取的数据进行解析;
  • 自主定制的协议使用"length\r\nx_ op_ y_\r\n"协议,前面加上数据长度;
    在这里插入图片描述
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include "Sock.hpp"namespace ns_protocol
{
#define MYSELF 1
#define SPACE " "
#define SPACE_LEN strlen(SPACE)#define SEP "\r\n"
#define SEP_LEN strlen(SEP) // 不能是sizeof,会统计\0class Request // 计算请求序列{public:Request() {}Request(int x, int y, char op): _x(x), _y(y), _op(op){}~Request() {}std::string Serialize() // 序列化{
#ifdef MYSELF// 使用自定义序列化方案// 将请求传换成string:_x _op _y的形式std::string str;str = std::to_string(_x);str += SPACE;str += _op;str += SPACE;str += std::to_string(_y);return str;
#else// 使用现成方案std::cout << "to do" << std::endl;
#endif}bool Deserialized(const std::string &str) // 反序列化{
#ifdef MYSELFstd::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 + SPACE_LEN).c_str());if (left + SPACE_LEN > str.size()){return false;}else{_op = str[left + SPACE_LEN];}return true;#elsestd::cout << "to do" << std::endl;
#endif}public:int _x;int _y;char _op; // + - * / %};class Response // 计算结果响应序列{public:Response() {}Response(int result, int code): _result(result), _code(code){}~Response() {}std::string Serialize() // 序列化:_code _result{
#ifdef MYSELF// 使用自定义序列化方案// 将请求传换成string:_x _op _y的形式std::string str;str = std::to_string(_code);str += SPACE;str += std::to_string(_result);return str;
#else// 使用现成方案std::cout << "to do" << std::endl;
#endif}bool Deserialized(const std::string &str) // 反序列化{
#ifdef MYSELFstd::size_t pos = str.find(SPACE);if (pos == std::string::npos){return false;}_code = atoi(str.substr(0, pos).c_str());_code = atoi(str.substr(pos + SPACE_LEN).c_str());return true;#elsestd::cout << "to do" << std::endl;
#endif}public:int _result; // 计算结果int _code;   // 计算结果的状态码:运算是否成功};// 临时方案// 期望返回的是一个完整地报文bool Recv(int sock, std::string* out){//TCP是面向字节流的,无法保证独到的inbuffer是一个完整地请求//因此需要解析协议,查看数据是否完整char buffer[1024];ssize_t s = recv(sock, buffer, sizeof buffer - 1, 0);if (s > 0){buffer[s] = 0;*out += buffer;}else if(s == 0){//客户端退出return false;}else{//读取错误return false;}return true;}void Send(int sock, const std::string str){send(sock, str.c_str(), str.size(), 0);}//添加报文// "XXXXXX"// "123\r\nXXXXXX\r\n"std::string Encode(std::string &s){std::string new_package = std::to_string(s.size());new_package += SEP;new_package += s;new_package += SEP;return new_package;}//解析报文//规定报文的格式为:"length\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 surplus = buffer.size() - pos - 2*SEP_LEN;if(surplus >= size){//至少有一份合法的报文,可以手动提取了buffer.erase(0, pos + SEP_LEN);std::string s = buffer.substr(0, size);buffer.erase(0, size + SEP_LEN);return s;}else{return "";//没有完整地报文,继续接收}}
}

CalServer.cc
计算服务

  • 服务器运行时,对端如果直接关闭,我们收到的就是空的信息,send的也是已经关闭的文件描述符,就可能导致服务器关闭;
    方案一:对SIGPIPE信号忽略,这样即使正在发送信息时对方关闭,也可以保证服务器不退出;

    在这里插入图片描述
    方案二:接收到信息时,需要判断信息的完整性,读取是否成功
  • 一般经验:在server编写的时候,要有较为严谨的判断逻辑;
    一般服务器都是要忽略SIGPIPE信号的,防止在运行过程中出现非法写入的问题;
#include "TcpServer.hpp"
#include "Protocol.hpp"
#include <memory>
#include <signal.h>using namespace ns_protocol;
using namespace ns_tcpserver;static void Usage(const std::string &process)
{std::cout << "\nUsage: " << process << " port\n"<< std::endl;
}// 进行计算
static Response calculatorHelper(const Request &req)
{Response 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 (0 == req._y)resp._code = 1;elseresp._result = req._x / req._y;break;case '%':if (0 == req._y)resp._code = 2;elseresp._result = req._x % req._y;break;default:resp._code = 3;break;}return resp;
}void calculator(int sock)
{std::string inbuffer;//每次读取到的缓冲区while (true){//1.读取成功bool res = Recv(sock, &inbuffer); // 读到了一个请求if(!res){break;}//2.协议解析,保证得到一个完整的报文std::string package = Decode(inbuffer);if(package.empty()){continue; //如果读到的报文不完整,继续读取}logMessage(NORMAL, "%s", package.c_str());//3.保证该报文是一个完整的报文Request req;//4.反序列化,字节流->结构化req.Deserialized(package); // 反序列化//5.业务逻辑Response resp = calculatorHelper(req);//6.序列化std::string respString = resp.Serialize();//对计算结果进行序列化//7.添加长度信息,形成一个完整的报文respString = Encode(respString);//8.发送Send(sock, respString);//将结果序列发回给客户端}
}int main(int argc, char *argv[])
{if (argc != 2){Usage(argv[0]);exit(1);}signal(SIGPIPE, SIG_IGN);std::unique_ptr<TcpServer> server(new TcpServer(atoi(argv[1])));server->BindService(calculator);server->Start();return 0;
}

CalClient.cc
客户端

#include <iostream>
#include "Sock.hpp"
#include "Protocol.hpp"using namespace ns_protocol;
static void Usage(const std::string &process)
{std::cout << "\nUsage: " << process << " serverIp serverPort\n"<< std::endl;
}
// ./client server_ip server_port
int main(int argc, char *argv[])
{if (argc != 3){Usage(argv[0]);exit(1);}std::string server_ip = argv[1];uint16_t server_port = atoi(argv[2]);Sock sock;int sockfd = sock.Socket();if (!sock.Connect(sockfd, server_ip, server_port)){std::cerr << "Connect error" << std::endl;exit(2);}bool quit = false;std::string buffer;while (!quit){// 1. 获取需求Request req;std::cout << "Please Enter # ";std::cin >> req._x >> req._op >> req._y;// 2. 序列化std::string s = req.Serialize();// std::string temp = s;// 3. 添加长度报头s = Encode(s);// 4. 发送给服务端Send(sockfd, s);// 5. 正常读取while (true){bool res = Recv(sockfd, &buffer);if (!res){quit = true;break;}std::string package = Decode(buffer);if (package.empty())continue;Response resp;resp.Deserialized(package);std::string err;switch (resp._code){case 1:err = "除0错误";break;case 2:err = "模0错误";break;case 3:err = "非法操作";break;default:std::cout << resp._result << " [success]" << std::endl;break;}if(!err.empty()) std::cerr << err << std::endl;// sleep(1);break;//完整读取一个报文就退出}}close(sockfd);return 0;
}

运行结果:
在这里插入图片描述

2.守护进程

  • (1)前台进程:和终端关联的进程;在终端下能读取输入并作出反应(如bash);
    (2)任何xshell登陆,只允许一个前台进程和多个后台进程;
    (3)进程除了有自己的pid, ppid, 还有一个组ID;
    (4)在命令行中,同时用管道启动多个进程,多个进程是兄弟关系,父进程都是bash ->可以用匿名管道来进行通信;
    (5)而同时被创建的多个进程可以成为一个进程组的概念,组长一般是第一个进程
    (6)任何一次登陆,登陆的用户,需要有多个进程(组),来给这个用户提供服务的(bash),用户自己可以启动很多进程,或者进程组。我们把给用户提供服务的进程,或者用户自己启动的所有的进程或者服务,整体都是要属于一个叫做会话的机制中的。
    (7)当用户退出登陆的时候,整个会话中的进程组都会结束;
    想让一个进程不再属于用户的会话,而是自成一个会话,这个进程称为守护进程
    (8)如何将进程变为守护进程->setsid()接口;
    (9)setsid要成功被调用,必须保证当前进程不是进程组的组长,可以通过fork创建的子进程实现;
    (10)守护进程不能直接向显示器打印消息,一旦打印,会被暂停,终止;

  • 如何在Linux正确的写一个让进程守护进程化的代码:
    写一个函数,让进程调用这个函数,自动变成守护进程;

  • /dev/null文件
    可以理解为一个文件黑洞,可以向里面打印数据,也可以从里面读取,但都不会有实际的数据输入输出;
    因此可以将标准输入,标准输出,标准错误重定向到devnull文件中;

Daemon.hpp

#pragma once
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>void MyDaemon()
{//1.忽略信号,SIPPIPE, SIGCHIDsignal(SIGPIPE, SIG_IGN);signal(SIGCHLD, SIG_IGN);//2.不要让自己成为组长if(fork() > 0){exit(0);//父进程退出,剩下子进程其实是孤儿进程}//3.调用setsidsetsid();//4.标准输入,标准输出,标准错误的重定向,守护进程不能直接向显示器打印消息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
在服务器进程中调用守护进程函数,让服务器进程成为守护进程;

#include "TcpServer.hpp"
#include "Protocol.hpp"
#include <memory>
#include <signal.h>
#include "Daemon.hpp"using namespace ns_protocol;
using namespace ns_tcpserver;static void Usage(const std::string &process)
{std::cout << "\nUsage: " << process << " port\n"<< std::endl;
}// 进行计算
static Response calculatorHelper(const Request &req)
{Response 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 (0 == req._y)resp._code = 1;elseresp._result = req._x / req._y;break;case '%':if (0 == req._y)resp._code = 2;elseresp._result = req._x % req._y;break;default:resp._code = 3;break;}return resp;
}void calculator(int sock)
{std::string inbuffer;//每次读取到的缓冲区while (true){//1.读取成功bool res = Recv(sock, &inbuffer); // 读到了一个请求if(!res){break;}//2.协议解析,保证得到一个完整的报文std::string package = Decode(inbuffer);if(package.empty()){continue; //如果读到的报文不完整,继续读取}logMessage(NORMAL, "%s", package.c_str());//3.保证该报文是一个完整的报文Request req;//4.反序列化,字节流->结构化req.Deserialized(package); // 反序列化//5.业务逻辑Response resp = calculatorHelper(req);//6.序列化std::string respString = resp.Serialize();//对计算结果进行序列化//7.添加长度信息,形成一个完整的报文respString = Encode(respString);//8.发送Send(sock, respString);//将结果序列发回给客户端}
}int main(int argc, char *argv[])
{if (argc != 2){Usage(argv[0]);exit(1);}signal(SIGPIPE, SIG_IGN);MyDaemon();//让该进程成为守护进程,自成一个会话std::unique_ptr<TcpServer> server(new TcpServer(atoi(argv[1])));server->BindService(calculator);server->Start();return 0;
}

运行结果:
在这里插入图片描述
注:
在这里插入图片描述
守护进程实际上是孤儿进程,但是没有被系统领养,而是自成会话

这样下来,服务器进程成为了守护进程,自成一个会话,即使用户退出登录,该进程也不会退出;

3.使用json来完成序列化

json:网络通信的格式

  • 在Linux上安装json:
    在这里插入图片描述
  • json实际上是一个结构化数据格式,里面是很多的kv结构:
    在这里插入图片描述
  • json库的使用:
    在这里插入图片描述
    StyleWriter对象,两个kv对象之间有换行符;
    StyleWriter对象的write函数会将root中的kv内容直接转换为对应的string;

    运行结果:
    在这里插入图片描述
    在这里插入图片描述
    FastWriter对象,中间没有换行符
    运行结果:
    在这里插入图片描述
    json里面是可以套json的
    在这里插入图片描述

使用json协议完成序列化和反序列化:
由于使用的是非cpp官方库,因此需要添加编译选项:
makefile

.PHONY:all
all:CalClient CalServerCalClient:CalClient.ccg++ -o $@ $^ -std=c++11 -lpthread -ljsoncpp
CalServer:CalServer.ccg++ -o $@ $^ -std=c++11 -lpthread -ljsoncpp.PHONY:clean
clean:rm -f CalClient CalServer

Protocol.hpp

#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include "Sock.hpp"
#include <jsoncpp/json/json.h>namespace ns_protocol
{
//#define MYSELF 1
#define SPACE " "
#define SPACE_LEN strlen(SPACE)#define SEP "\r\n"
#define SEP_LEN strlen(SEP) // 不能是sizeof,会统计\0class Request // 计算请求序列{public:Request() {}Request(int x, int y, char op): _x(x), _y(y), _op(op){}~Request() {}std::string Serialize() // 序列化{
#ifdef MYSELF// 使用自定义序列化方案// 将请求传换成string:_x _op _y的形式std::string str;str = std::to_string(_x);str += SPACE;str += _op;str += SPACE;str += std::to_string(_y);return str;
#else// 使用现成方案Json::Value root;root["x"] = _x;root["y"] = _y;root["op"] = _op;Json::FastWriter writer;return writer.write(root);  
#endif}bool Deserialized(const std::string &str) // 反序列化{
#ifdef MYSELFstd::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 + SPACE_LEN).c_str());if (left + SPACE_LEN > str.size()){return false;}else{_op = str[left + SPACE_LEN];}return true;#elseJson::Value root;Json::Reader reader;reader.parse(str, root);//parse函数能够将序列化的json字符串直接读取到Value对象中_x = root["x"].asInt();_x = root["y"].asInt();_x = root["op"].asInt();//char类型实质也是intreturn true;
#endif}public:int _x;int _y;char _op; // + - * / %};class Response // 计算结果响应序列{public:Response() {}Response(int result, int code): _result(result), _code(code){}~Response() {}std::string Serialize() // 序列化:_code _result{
#ifdef MYSELF// 使用自定义序列化方案// 将请求传换成string:_x _op _y的形式std::string str;str = std::to_string(_code);str += SPACE;str += std::to_string(_result);return str;
#else// 使用现成方案Json::Value root;root["code"] = _code;root["result"] = _result;Json::FastWriter writer;return writer.write(root);
#endif}bool Deserialized(const std::string &str) // 反序列化{
#ifdef MYSELFstd::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 + SPACE_LEN).c_str());return true;#elseJson::Value root;Json::Reader reader;reader.parse(str, root);_code = root["code"].asInt();_result = root["result"].asInt();return true;
#endif}public:int _result; // 计算结果int _code;   // 计算结果的状态码:运算是否成功};// 临时方案// 期望返回的是一个完整地报文bool Recv(int sock, std::string* out){//TCP是面向字节流的,无法保证独到的inbuffer是一个完整地请求//因此需要解析协议,查看数据是否完整char buffer[1024];ssize_t s = recv(sock, buffer, sizeof(buffer) - 1, 0);if (s > 0){buffer[s] = 0;*out += buffer;}else if(s == 0){//客户端退出return false;}else{//读取错误return false;}return true;}void Send(int sock, const std::string str){int n = send(sock, str.c_str(), str.size(), 0);if(n < 0){std::cout << "send error" << std::endl;}}//添加报头// "XXXXXX"// "123\r\nXXXXXX\r\n"std::string Encode(std::string &s){std::string new_package = std::to_string(s.size());new_package += SEP;new_package += s;new_package += SEP;return new_package;}//解析报文//规定报文的格式为:"length\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 surplus = buffer.size() - pos - 2*SEP_LEN;if(surplus >= size){//至少有一份合法的报文,可以手动提取了buffer.erase(0, pos + SEP_LEN);std::string s = buffer.substr(0, size);buffer.erase(0, size + SEP_LEN);return s;}else{return "";//没有完整地报文,继续接收}}}

运行结果:
在这里插入图片描述

二、HTTP协议

1.概念

  • 应用层:就是程序员基于socket接口之上编写的具体逻辑,有很多和文本处理相关的工作;http协议一定会有大量的文本分析和处理;

  • URL:我们平时说的网址,其结构如下;
    在这里插入图片描述
    其中,服务器地址IP就是域名,用来标识唯一的主机;冒号后面是端口号,标识特定主机上的特定进程;
    端口号后面是带层次的文件路径,其中第一个文件夹叫做web根目录;文件路径标识客户想访问的资源路径;
    URL:union resource local统一资源定位符,代表本次访问请求的资源位置,定位互联网中唯一的一份资源;
    在用户访问网络资源时,先通过url找到服务器上的特定文件资源,在进行读取或写入;

  • 如果用户想在url中包含url本身作为特殊字符使用的字符时,浏览器会自动对该字符进行编码,在服务端收到后,需要转回特殊字符;
    在这里插入图片描述在这里插入图片描述

2.HTTP协议请求和响应的报文格式

在这里插入图片描述
单纯在报文角度,http可以是基于行的文本协议;

  • 请求报文:
    请求行:方法 URL 协议版本
    http的方法为:
    在这里插入图片描述
    请求报头Header:多行kv结构,都是属性;
    空行:用来区分报头和有效载荷;
    请求正文(可以没有);

  • 响应报文:
    状态行:协议版本 状态码 状态码描述;
    响应报头;
    空行;
    响应正文;

3.使用HTTP协议进行网络通信

Log.hpp

#pragma once#include <iostream>
#include <cstdio>
#include <cstdarg>
#include <ctime>
#include <string>// 日志是有日志级别的
#define DEBUG   0
#define NORMAL  1
#define WARNING 2
#define ERROR   3
#define FATAL   4const char *gLevelMap[] = {"DEBUG","NORMAL","WARNING","ERROR","FATAL"
};#define LOGFILE "./http.log"// 完整的日志功能,至少: 日志等级 时间 支持用户自定义(日志内容, 文件行,文件名)
void logMessage(int level, const char *format, ...)
{
#ifndef DEBUG_SHOWif(level== DEBUG) return;
#endif// va_list ap;// va_start(ap, format);// while()// int x = va_arg(ap, int);// va_end(ap); //ap=nullptrchar stdBuffer[1024]; //标准部分time_t timestamp = time(nullptr);// struct tm *localtime = localtime(&timestamp);snprintf(stdBuffer, sizeof stdBuffer, "[%s] [%ld] ", gLevelMap[level], timestamp);char logBuffer[1024]; //自定义部分va_list args;va_start(args, format);// vprintf(format, args);vsnprintf(logBuffer, sizeof logBuffer, format, args);va_end(args);FILE *fp = fopen(LOGFILE, "a");// printf("%s%s\n", stdBuffer, logBuffer);fprintf(fp, "%s%s\n", stdBuffer, logBuffer);fclose(fp);
}

Sock.hpp

#pragma once#include <iostream>
#include <string>
#include <cstring>
#include <cerrno>
#include <cassert>
#include <unistd.h>
#include <memory>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <ctype.h>
#include "Log.hpp"class Sock
{
private:const static int gbacklog = 20;public:Sock() {}int Socket(){int listensock = socket(AF_INET, SOCK_STREAM, 0);if (listensock < 0){logMessage(FATAL, "create socket error, %d:%s", errno, strerror(errno));exit(2);}logMessage(NORMAL, "create socket success, listensock: %d", listensock);return listensock;}void Bind(int sock, uint16_t port, std::string ip = "0.0.0.0"){struct sockaddr_in local;memset(&local, 0, sizeof local);local.sin_family = AF_INET;local.sin_port = htons(port);inet_pton(AF_INET, ip.c_str(), &local.sin_addr);if (bind(sock, (struct sockaddr *)&local, sizeof(local)) < 0){logMessage(FATAL, "bind error, %d:%s", errno, strerror(errno));exit(3);}}void Listen(int sock){if (listen(sock, gbacklog) < 0){logMessage(FATAL, "listen error, %d:%s", errno, strerror(errno));exit(4);}logMessage(NORMAL, "init server success");}// 一般经验// const std::string &: 输入型参数// std::string *: 输出型参数// std::string &: 输入输出型参数int Accept(int listensock, std::string *ip, uint16_t *port){struct sockaddr_in src;socklen_t len = sizeof(src);int servicesock = accept(listensock, (struct sockaddr *)&src, &len);if (servicesock < 0){logMessage(ERROR, "accept error, %d:%s", errno, strerror(errno));return -1;}if(port) *port = ntohs(src.sin_port);if(ip) *ip = inet_ntoa(src.sin_addr);return servicesock;}bool Connect(int sock, const std::string &server_ip, const uint16_t &server_port){struct sockaddr_in server;memset(&server, 0, sizeof(server));server.sin_family = AF_INET;server.sin_port = htons(server_port);server.sin_addr.s_addr = inet_addr(server_ip.c_str());if(connect(sock, (struct sockaddr*)&server, sizeof(server)) == 0) return true;else return false;}~Sock() {}
};

Usage.hpp

#pragma once
#include <iostream>
#include <string>
void Usage(std::string proc)
{std::cout << "\nUsage: " << proc <<  " port\n" << std::endl;
}

Util.hpp
工具类,分割字符串

#pragma once#include <iostream>
#include <vector>class Util
{
public:// aaaa\r\nbbbbb\r\nccc\r\n\r\nstatic void cutString(std::string s, const std::string &sep, std::vector<std::string> *out){std::size_t start = 0;while (start < s.size()){auto pos = s.find(sep, start);if (pos == std::string::npos) break;std::string sub = s.substr(start, pos - start);// std::cout << "----" << sub << std::endl;out->push_back(sub);start += sub.size();start += sep.size();}if(start < s.size()) out->push_back(s.substr(start));}
};

HttpServer.hpp

#pragma once#include <iostream>
#include <signal.h>
#include <functional>
#include "Sock.hpp"class HttpServer
{
public:using func_t = std::function<void(int)>;
private:int listensock_;uint16_t port_;Sock sock;func_t func_;
public:HttpServer(const uint16_t &port, func_t func): port_(port),func_(func){listensock_ = sock.Socket();sock.Bind(listensock_, port_);sock.Listen(listensock_);}void Start(){signal(SIGCHLD, SIG_IGN);for( ; ; ){std::string clientIp;uint16_t clientPort = 0;int sockfd = sock.Accept(listensock_, &clientIp, &clientPort);if(sockfd < 0) continue;if(fork() == 0){close(listensock_);func_(sockfd);close(sockfd);exit(0);}close(sockfd);}}~HttpServer(){if(listensock_ >= 0) close(listensock_);}
};

HttpServer.cc
这里是主要的对http协议进行解析的代码,逐行解析,提取首行url,访问目标资源;

#include <iostream>
#include <memory>
#include <cassert>
#include <fstream>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include "HttpServer.hpp"
#include "Usage.hpp"
#include "Util.hpp"
// 一般http都要有自己的web根目录
#define ROOT "./wwwroot" // ./wwwroot/index.html
// 如果客户端只请求了一个/,我们返回默认首页
#define HOMEPAGE "index.html"
void HandlerHttpRequest(int sockfd)
{// 1. 读取请求 for testchar buffer[10240];ssize_t s = recv(sockfd, buffer, sizeof(buffer) - 1, 0);if (s > 0){buffer[s] = 0;// std::cout << buffer << "--------------------\n" << std::endl;}std::vector<std::string> vline; // 取出http请求的每一行Util::cutString(buffer, "\n", &vline);std::vector<std::string> vblock; // 取出第一行的每一个子串Util::cutString(vline[0], " ", &vblock);std::string file = vblock[1]; // 请求的资源std::string target = ROOT;if(file == "/") file = "/index.html";target += file; //请求的资源从web根目录下开始,如果不指定web根目录,就会访问Linux根目录std::cout << target << std::endl;std::string content;std::ifstream in(target); // 打开文件if(in.is_open()){std::string line;while(std::getline(in, line)){content += line;}in.close();}std::string HttpResponse;if(content.empty()) HttpResponse = "HTTP/1.1 404 NotFound\r\n";else HttpResponse = "HTTP/1.1 200 OK\r\n";HttpResponse += "\r\n";HttpResponse += content;// std::cout << "####start################" << std::endl;// for(auto &iter : vblock)// {//     std::cout << "---" << iter << "\n" << std::endl;// }// std::cout << "#####end###############" << std::endl;// 2. 试着构建一个http的响应send(sockfd, HttpResponse.c_str(), HttpResponse.size(), 0);
}
int main(int argc, char *argv[])
{if (argc != 2){Usage(argv[0]);exit(0);}std::unique_ptr<HttpServer> httpserver(new HttpServer(atoi(argv[1]), HandlerHttpRequest));httpserver->Start();return 0;
}

在目录下创建web根目录wwwroot,里面创建首页index.html;
在这里插入图片描述
index.html
在vscode下装插件,!table会出现网页模板;

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>lmx</title>
</head>
<body><h3>这个一个Linux课程</h3><p>我是一个Linux的学习者,我正在进行http的测试工作!!</p><p>我是一个Linux的学习者,我正在进行http的测试工作!!</p><p>我是一个Linux的学习者,我正在进行http的测试工作!!</p><p>我是一个Linux的学习者,我正在进行http的测试工作!!</p>
</body>
</html>

运行结果:
在这里插入图片描述

4.HTTP协议的方法

在这里插入图片描述
其中最常用的是GET和POST方法;

  • 用户数据提交到服务器的流程:
    用户发起申请,形成表单,指明提交方法,表单中的数据,会被转成http request的一部分,之后收集用户数据,并把用户数据推送给服务器;

  • GET方法可以将数据从服务器端拿到客户端,也可以将客户端的数据提交到服务器;
    使用GET方法提交请求
    web目录结构:
    在这里插入图片描述
    index.html
    使用GET方法将进行提交
    在这里插入图片描述
    **input是按钮,其中的action是点击按钮后访问的文件,method是方法,这里是GET;
    下面的Username和Password是kv结构输入框,type是内容,name是标签;
    **
    运行结果:
    在这里插入图片描述
    使用浏览器访问建立好的网页,这是一个可以登陆的界面;
    在这里插入图片描述
    输入好用户名和密码后,点击登录;
    在这里插入图片描述
    跳转到如上界面,登陆的时候其实就是把用户信息提交给服务器;
    在这里插入图片描述
    在上面的网址栏可以看到自己输入的用户名和密码,?后面是参数,前面是提交的地址,就是将参数提交到目标文件中;
    服务器收到的请求:
    在这里插入图片描述
    这是因为get方法通过url传参,并将参数回显到url中;

  • POST方法用于将客户端的数据提交到服务器;
    使用POST方法提交请求
    insex.html
    在这里插入图片描述
    运行结果:
    在这里插入图片描述
    点击登录:
    在这里插入图片描述
    服务器收到的请求:
    在这里插入图片描述
    POST是不会通过URL传参的,它通过正文传参;

总结

  • GET方法通过URL传参,回显输入的私密信息,不够私密;
  • POST方法通过正文传参,不会回显私密信息,私密性有保证;
  • 私密性不是安全性;
  • 登录和注册一般常用的是POST方法;
    内容较大也建议使用POST方法,因为POST方法里面有正文长度,方便整段读取;

5.HTTP协议的状态码

在这里插入图片描述

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

  • 重定向当网页进行请求时,需要跳转到其他网页;
    301:永久移动,直接重定向到另一个网也,不会返回原来的网页,影响用户后续的请求策略;
    302:临时移动,临时重定向到另一个网页,比如登陆界面,处理好后再返回原始网页,不影响用户后续的请求策略;
    307:临时重定向;

  • 重定向过程
    客户端向服务器发起http请求 -> 服务器返回30X重定向状态码,并携带新的网页地址信息 -> 客户端浏览器拿到新的地址后,自动向新的地址发起请求;
    在这里插入图片描述

重定向实验
HttpServer.cc
在这里插入图片描述
如果读取的文件不存在,返回的状态码为301,会进行重定向操作;
其中Location属性就是重定向后的目标文件地址;

#include <iostream>
#include <memory>
#include <cassert>
#include <fstream>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include "HttpServer.hpp"
#include "Usage.hpp"
#include "Util.hpp"
// 一般http都要有自己的web根目录
#define ROOT "./wwwroot" // ./wwwroot/index.html
// 如果客户端只请求了一个/,我们返回默认首页
#define HOMEPAGE "index.html"
void HandlerHttpRequest(int sockfd)
{// 1. 读取请求 for testchar buffer[10240];ssize_t s = recv(sockfd, buffer, sizeof(buffer) - 1, 0);if (s > 0){buffer[s] = 0;std::cout << buffer << "\n--------------------\n"<< std::endl;}std::vector<std::string> vline; // 取出http请求的每一行Util::cutString(buffer, "\n", &vline);std::vector<std::string> vblock; // 取出第一行的每一个子串Util::cutString(vline[0], " ", &vblock);std::string file = vblock[1]; // 请求的资源std::string target = ROOT;if (file == "/")file = "/index.html";target += file; // 请求的资源从web根目录下开始,如果不指定web根目录,就会访问Linux根目录std::cout << target << std::endl;std::string content;      // 文件中的内容std::ifstream in(target); // 打开文件if (in.is_open()){std::string line;while (std::getline(in, line)){content += line;}in.close();}std::string HttpResponse;if (content.empty()){HttpResponse = "HTTP/1.1 301 NotFound\r\n";HttpResponse += "Location: http://47.115.213.66:8080/a/b/404.html\r\n";}elseHttpResponse = "HTTP/1.1 200 OK\r\n";HttpResponse += "\r\n";HttpResponse += content;// std::cout << "####start################" << std::endl;// for(auto &iter : vblock)// {//     std::cout << "---" << iter << "\n" << std::endl;// }// std::cout << "#####end###############" << std::endl;// 2. 试着构建一个http的响应send(sockfd, HttpResponse.c_str(), HttpResponse.size(), 0);
}
int main(int argc, char *argv[])
{if (argc != 2){Usage(argv[0]);exit(0);}std::unique_ptr<HttpServer> httpserver(new HttpServer(atoi(argv[1]), HandlerHttpRequest));httpserver->Start();return 0;
}

index.html
客户端点击登陆后,会跳转到/a/b/notexit.html这个地址的文件;

<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>lmx</title>
</head><body><h3>Hello Guests!</h3><form name="input" action="/a/b/notexit.html" method="POST">Username: <input type="text" name="user"> <br/>Password: <input type="password" name="pwd"> <br/><input type="submit" value="登陆"></form>
</body></html>

404.html
重定向的目标文件;

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>不存在</title>
</head>
<body><h2>你访问的页面不存在</h2>
</body>
</html>

运行结果:
客户端访问网页HOME地址:
在这里插入图片描述
点击登陆后,访问/a/b/notexit.html这个地址的文件,但是这个文件是不存在的,文件读取返回结果为空,状态码为301,触发重定向;
在这里插入图片描述
重定向到了a/b/404.html这个文件;

6.HTTP协议的报头

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

  • Content-Type、Content-Length
    添加内容类型及正文长度报头;
    在这里插入图片描述
  • Cookie会话管理
    http的特征:
    a.简单快速;
    b.无连接,指http不维护连接,连接是由TCP维护的;
    c.无状态,http不会记录用户曾经请求的网页,不会对用户的行为做记录;

    http协议是无状态的,但是我们平常在浏览器进行访问网页时,一般网站是会记录下我们的状态的,这是因为http协议为了支持常规用户的会话管理,支持两个报头属性Cookie(请求)、Set-Cookie(响应)
    用户登录后,曾经输入的用户名和密码等信息会保存为一个文件,在今后每次的http请求中,每次都会携带这个文件中的账户密码内容,这个文件就是cookie文件;
    cookie文件的创建与使用流程:
    当用户访问网站后,在网站上输入用户密码信息,之后服务器会将用户信息返回给客户端,客户端的浏览器会将用户信息保存,形成cookie文件,之后用户每次访问该网站,都会将cookie文件再次上传到服务器,进行用户星系比对,不用每次都重新输入信息了;
    在这里插入图片描述
    但是cookie文件中是将用户信息明文保存的,如果被黑客注入木马病毒,是能够盗取用户的私密信息;
    现在的新cookie方案:在网站认证用户信息后,服务端会形成一个用户唯一ID,session id,并返回给用户端,保存到cookie文件中;这样每次用户访问网站,上传的cookie文件都是用户在网站形成的唯一session id,就算被盗取,也不会暴露用户的私密信息;

验证cookie
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

7.connetion选项

在这里插入图片描述
keep-alive:长连接,网页该有的资源通过一个连接全部拿到;
close:短连接,处理完一个http请求后,就将连接关掉,每次都要建立连接获取图片等资源;

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/135960.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

2023/9/18 -- C++/QT

作业 完善登录框 点击登录按钮后&#xff0c;判断账号&#xff08;admin&#xff09;和密码&#xff08;123456&#xff09;是否一致&#xff0c;如果匹配失败&#xff0c;则弹出错误对话框&#xff0c;文本内容“账号密码不匹配&#xff0c;是否重新登录”&#xff0c;给定两…

WebGL 视图矩阵、模型视图矩阵

目录 立方体由三角形构成 视点和视线 视点、观察目标点和上方向 视点&#xff1a; 观察目标点&#xff1a; 上方向&#xff1a; 在WebGL中&#xff0c;观察者的默认状态应该是这样的&#xff1a; 视图矩阵程序&#xff08;LookAtTriangles.js&#xff09; 实际上&…

仅做笔记用:Stable Diffusion 通过 ControlNet 扩展图片 / 扩图

发觉之前的 Outpainting 脚本效果仍旧不是很理想。这里又找了一下有没有效果更好的途径来扩图。于是就找到了通过 ControlNet 的方式来实现效果更好的扩图。这里临时记录一下在 Stable Diffusion 怎么使用 ControlNet 来扩展图片。 下载 control_v11p_sd15_inpaint_fp16.safet…

多线程详解(上)

文章目录 一、线程的概念1&#xff09;线程是什么2&#xff09;为甚要有线程&#xff08;1&#xff09;“并发编程”成为“刚需”&#xff08;2&#xff09;在并发编程中, 线程比进程更轻量. 3&#xff09;线程和进程的区别 二、Thread的使用1&#xff09;线程的创建继承Thread…

自定义类型:结构体

自定义类型&#xff1a;结构体 一&#xff1a;引入二&#xff1a;结构体类型的声明1&#xff1a;正常声明2&#xff1a;特殊声明 三&#xff1a;结构体变量的创建和初始化1:结构体变量的创建2&#xff1a;结构体变量的初始化 三&#xff1a;结构体访问操作符四&#xff1a;结构…

【C语言】每日一题(半月斩)——day3

目录 一&#xff0c;选择题 1.已知函数的原型是&#xff1a; int fun(char b[10], int *a); 2、请问下列表达式哪些会被编译器禁止【多选】&#xff08; &#xff09; 3、以下程序的输出结果为&#xff08; &#xff09; 4、下面代码段的输出是&#xff08; &#xff09;…

大数据学习1.1-Centos8虚拟机安装

1.创建新的虚拟机 2.选择稍后安装OS 3.选择Linux的CentOS8 4.选择安装路径 5.分配20g存储空间 6.自定义硬件 7.分配2g内存 8.分配2核处理器 9.选择镜像位置 10.开启虚拟机安装 推荐密码设置为root

61、SpringBoot -----跨域资源的设置----局部设置和全局设置

★ 跨域资源共享的意义 ▲ 在前后端分离的开发架构中&#xff0c;前端应用和后端应用往往是彻底隔离的&#xff0c;二者不在同一个应用服务器内、甚至不再同一台物理节点上。 因此前端应用和后端应用就不在同一个域里。▲ 在这种架构下&#xff0c;前端应用可能采用前端框架&a…

序列化和反序列化:将数据变得更加通用化

序列化与反序列化简介 序列化和反序列化是计算机领域中常用的概念&#xff0c;用于将对象或数据结构转换为字节序列&#xff08;序列化&#xff09;和将字节序列转换回对象或数据结构&#xff08;反序列化&#xff09;。 序列化是指将对象或数据结构转换为字节序列的过程。通…

前端VUE---JS实现数据的模糊搜索

实现背景 因为后端实现人员列表返回&#xff0c;每次返回的数据量在100以内&#xff0c;要求前端自己进行模糊搜索 页面实现 因为是实时更新数据的&#xff0c;就不需要搜索和重置按钮了 代码 HTML <el-dialogtitle"团队人员详情":visible.sync"centerDi…

uni-app跳转到另一个app

第一步&#xff1a; 首先要知道 app的包名 获取方式如下 第二步&#xff1a; 在第一个 demo1 app 一个页面中需要一个按钮去跳转 方法如下 <template><view class"content"><button click"tz">跳转</button></view> </…

如何在微软Edge浏览器上一键观看高清视频?

编者按&#xff1a;视频是当下最流行的媒体形式之一。但由于视频压缩、网络不稳定等原因&#xff0c;我们常常可以看到互联网上的很多视频其画面质量并不理想&#xff0c;尤其是在浏览器端&#xff0c;这极大地影响了观看体验。不过&#xff0c;近期微软 Edge 浏览器推出了一项…

FPGA纯verilog实现8路视频拼接显示,提供工程源码和技术支持

目录 1、前言版本更新说明免责声明 2、我已有的FPGA视频拼接叠加融合方案3、设计思路框架视频源选择OV5640摄像头配置及采集静态彩条视频拼接算法图像缓存视频输出 4、vivado工程详解5、工程移植说明vivado版本不一致处理FPGA型号不一致处理其他注意事项 6、上板调试验证并演示…

Jmeter接口测试简易步骤

使用Jmeter接口测试 1、首先右键添加一个线程组&#xff0c;然后我们重命名接口测试 2、在线程组上添加一个Http默认请求&#xff0c;并配置服务器的IP地址端口等信息 3、在线程组中添加一个HTTP请求&#xff0c;这里我们重命名“增加信用卡账户信息接口” 4、配置接口请求信息…

使用延迟队列解决分布式事务问题——以订单未支付过期,解锁库存为例

目录 一、前言 二、库存 三、订单 一、前言 上一篇使用springcloud-seata解决分布式事务问题-2PC模式我们说到了使用springcloud-seata解决分布式的缺点——不适用于高并发场景 因此我们使用延迟队列来解决分布式事务问题&#xff0c;即使用柔性事务-可靠消息-最终一致性方…

Kotlin simple convert ArrayList CopyOnWriteArrayList MutableList

Kotlin simple convert ArrayList CopyOnWriteArrayList MutableList Kotlin读写分离CopyOnWriteArrayList_zhangphil的博客-CSDN博客Java并发多线程环境中&#xff0c;造成死锁的最简单的场景是&#xff1a;多线程中的一个线程T_A持有锁L1并且申请试图获得锁L2&#xff0c;而多…

Redis缓存实现及其常见问题解决方案

随着互联网技术的发展&#xff0c;数据处理的速度和效率成为了衡量一个系统性能的重要指标。在众多的数据处理技术中&#xff0c;缓存技术以其出色的性能优化效果&#xff0c;成为了不可或缺的一环。而在众多的缓存技术中&#xff0c;Redis 以其出色的性能和丰富的功能&#xf…

flutter开发实战-长按TextField输入框cut、copy设置为中文复制、粘贴

flutter开发实战-长按TextField输入框cut、copy设置为中文复制、粘贴 在开发过程中&#xff0c;需要长按TextField输入框cut、copy设置为中文“复制、粘贴”&#xff0c;这里记录一下设置的代码。 一、pubspec.yaml设置flutter_localizations 在pubspec.yaml中设置flutter_l…

23下半年学习计划

大二上学期计划 现在已经是大二了&#xff0c;java只学了些皮毛&#xff0c;要学的知识还有很多&#xff0c;新的学期要找准方向&#xff0c;把要学的知识罗列&#xff0c;按部就班地完成计划&#xff0c;合理安排时间&#xff0c;按时完成学习任务。 学习node.js&#xff0c…

企业架构LNMP学习笔记48

数据结构类型操作&#xff1a; 数据结构&#xff1a;存储数据的方式 数据类型 算法&#xff1a;取数据的方式&#xff0c;代码就把数据进行组合&#xff0c;计算、存储、取出。 排序算法&#xff1a;冒泡排序、堆排序 二分。 key&#xff1a; key的命名规则不同于一般语言…