应用层协议HTTP

        应用层协议中的 HTTP(超文本传输协议)。在互联网中,HTTP 协议是一个至关重要的一个协议,它定义了客户端与服务器之间如何进行通信,以交换或传输超文本。

        本篇介绍了有关 URL 的相关知识,http 的报文格式,http 报头中的对应的方法以及 http 中的状态码。最后还泄漏一份关于 http 的网页代码(若想使用该代码成功的在浏览器中访问,需要将自己的 ip 和端口开放)。

        HTTP 协议是客户端与服务器之间通信的基础,客户端通过 HTTP 协议向服务器发送请求,服务器收到请求后处理并返回响应。HTTP 协议是一个无连接(http 协议是基于连接的 tcp 协议,http 被称为无连接是因为在客户端和服务端不需要再次建立连接,直接忽略了向下的传输层)、无状态的协议,即每次请求都需要建立新的连接,且服务器不会保存客服端的信息状态信息。

目录

URL

urlencode urldecode

HTTP报文格式

1. 请求报文

2. 响应报文

HTTP方法

1. GET方法(常用)

2. POST方法(常用)

3. PUT方法

4. HEAD方法

5. DELETE方法

6. OPTIONS

HTTP状态码

1. 信息性状态码 1XX

2. 成功状态码 2XX

3. 重定向状态码 3XX

4. 客户端错误状态码 4XX

5. 服务器错误状态码 5XX

HTTP常见报头(header)

1. connection报头

HTTP Code

Http.hpp 代码思路

1. HttpRequest

2. HttpResponse

3. HTTP报文发送

URL

        URL 就是我们平时所指的网址,也被 称为统一资源定位符,如下:

        如上所示,对于一个 url 由如上部分组成,首先的是协议名称;接着是域名,域名会自动的被解析为 ip 地址;还有隐藏起来的服务器端口号,一般不会显示,因为识别到协议的时候会自动添加上对应的端口号(比较出名的协议都有着对立独立的端口号);

        其中最难理解的是带层次的文件路径,通常我们做服务器的是 Linux 系统,而在 Linux 系统下一切皆文件,想要把对应的资源传输到对应的客户端,就需要在对应的 Linux 系统下找到对应的文件目录,所以会有带层次从文件路径。接着是查询字符串,其实就是本地向对应的服务器提供的一些参数。

        通过 URL 中的域名找到对应的 ip,协议找到对应的端口号,就可以定位到唯一的一台主机,接着通过文件路径就可以找到互联网中的唯一一个文件,所以 URL 也被称为统一资源定位符

urlencode urldecode

        我们在上文中已经说过,在带层次的文件路径后也就是 “ ?” 的后面提交的是我们用户的参数,对于用户的参数会用 && 符号进行连接,但是当我们需要提交的参数中就存在着对应的一些会起冲突的一些符号呢?比如 &=:\ 等等。

        这个时候就会将对应特殊符号通过编码转换为对应的十六进制,如下图:

        如上的特殊符号就是通过 urlencode 进行的加密,当我们的浏览器想要处理的时候就会通过 urldecode 进行解码(urldecode 就是 urlencode 的逆向过程)。

HTTP报文格式

        HTTP 报文格式分为请求报文格式和响应报文格式。

1. 请求报文

        对于 HTTP 请求报文格式如下:

        其中首行:方法 + url + 版本;

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

        请求正文:空行后面的内柔都是正文,正文允许为空字符串,若正文存在,则在请求报头中会有一个 Content-Length 属性来标识正文的长度。

        如下为我使用代码捕捉的一个请求报文,如下:

        如上所示,该报文的正文部分为空。

2. 响应报文

        对于 HTTP 响应报文的格式如下:

        如上所示,响应报文的格式和请求报文的格式基本一致。

        其中,首航:版本 + 状态码 + 状态码解释

        响应报头:响应的属性,冒号分割的键值对,每组属性之间使用 \r\n 进行分隔,遇到空行标识响应报头结束

        正文:空行后面的内柔都是正文,正文允许为空字符串,若正文存在,则在报头中一定存在一个 Content-Length 属性标识正文的长度,通常对于服务前返回的报文的正文部分,可以说图片,可以是音频也可以是文本内柔。

        其实不管是请求报文还是响应报文,都是由一个字符串所组成(只不过经过客户端或者服务端进行了序列化),虽然将其打印出来显示的是一行一行的,但是都是由一整个字符串所组成

HTTP方法

        对于 HTTP 的方法而言就是在 HTTP 请求报文中请求行中的请求方法。对于 HTTP 的方法如下:

方法    说明                    支持的HTTP版本
GET     获取资源                1.0、1.1
POST    传输实体主体            1.0、1.1
PUT     传输文件                1.0、1.1
HEAD    获得报文首部            1.0、1.1
DELETE  删除文件                1.0、1.1
OPTIONS 询问支持的方法           1.1
TRACE   追踪路径                1.1
CONNECT 要求用隧道协议连接代理    1.1
LINK    建立和资源之间的联系     1.0
UNLINK  断开连接关系            1.0

        对于如上的方法,其中用得最多的就是 GET、POST 方法,对于各种方法的作用如下:

1. GET方法(常用)

        作用:用于请求 URL 指定的资源

        示例:GET /index.html HTTP/1.1

        特性:将指定资源经过服务器端解析后返回相应内容

2. POST方法(常用)

        作用:用于传输实体的主体,通常用于提交表单数据

        示例:POST /submit.cgi HTTP/1.1

        特性:可以传输大量的数据给服务器,并且将数据包含在请求体当中

        对于 GET 方法和 POST 方法而言,GET 方法一般获取静态资源,可以通过 URL 来向服务器传递参数,而对于 POST 方法而言,可以通过请求的正文来进行传递参数,如下:

        如上所示,当我们使用 GET 方法的时候,当我们使用一个密码登陆页面的时候,就会导致账户账号和密码直接显示在 URL 中,而当我们使用 POST 方法的时候,则不会将参数显示在 URL 中。同时也说明当我们想要传递参数,我们可以使用 POST 方法传递参数,因为 GET 方法使用 URL 传递参数,传递的参数量一定不大,而使用正文传递参数则可以很大。

3. PUT方法

        作用:用于传输文件,将请求报文主体中的文件保存到请求 URL 指定的位置

        示例:可以发送大量的数据给服务器, 并且数据包含在请求体中

        特性:可以发送大量的数据给服务器,并且数据包含在请求体当中

4. HEAD方法

        作用:与 GET 方法类似,但不返回报文主体部分,仅返回响应头

        示例:HEAD /index.html HTTP/1.1

        特性:用于确认 URL 的有效性集资源更新的日期时间等

5. DELETE方法

        作用:用于删除文件,是 PUT 的相反方法

        示例:DELETE /example.html HTTP/1.1

        特性:按请求 URL 删除指定的资源

6. OPTIONS

        作用:用于查询针对请求 URL 指定的资源支持的方法

        示例:OPTIONS * HTTP/1.1

        特性:返沪允许的方法,如 GET、POST 等等

HTTP状态码

        HTTP 的状态码是在响应报文中的状态行中包含的信息,对于不同的状态码对应着不同的状态表示,如下:

1. 信息性状态码 1XX

        对于信息性状态码只有一个,为:100     含义为:Continue   应用场景为:上传大文件的时候,服务器会告诉客户端可以继续上传。

2. 成功状态码 2XX

        成功状态码是以 2 开头的状态码,主要有三个,如下:

        200    含义为:OK       应用场景为:访问网站首页,服务器返回网页内容

        201    含义为:Created  应用场景为:发布新文章,服务器返回文章创建成功信息

        204     含义为:No Content   应用场景:删除文章之后,服务器返回 “无内容” 表示操作成功

3. 重定向状态码 3XX

        301        含义为:Moved Permanently   应用场景:网站更换域名之后,自动跳转到新域名;搜索引擎更新网站链接时使用                只要使用该状态码,则表示的是永久重定向

        302        含义为:Found 或 See Other   应用场景:用户登陆成功后,重定向到用户首页           只要使用该状态码,则表示的是临时重定向

        304        含义为:Not Modified             应用场景:浏览前的缓存机制,对未修改的资源返回 304 状态码            只要使用该状态码,则表示的是临时重定向

       307        含义为:Temporary Redirect    应用场景:临时重定向资源到新的位置(临时重定向)

        308        含义为:Permanent Redirect   应用场景:永久重定向资源到新的位置(永久重定向)

        对于临时重定向和永久重定向,临时重定向的网站是临时的,而永久重定向的网站则是永久都切换到该网站了。

        对于永久重定向以及临时重定向都依赖于报头中的 Location 选项,其中以 301 与 302 选项为例:

        HTTP 状态码为 301 时,表示请求的资源以及被永久移动到新的位置,在这种情况下,服务器会在响应中添加一个 Location 头部,用于指定资源的新位置。这个 Location 头部包含新的 URL 地址,浏览器会自动重定向到该地址,同时缓存该重定向

HTTP/1.1 301 Moved Permanently\r\n
Location: https://www.new-url.com\r\n

        HTTP 状态码为 302 时,表示请求的资源被临时的移动到新的位置,服务器同样会在响应中添加一个 Location 头部来指定资源的新位置,浏览器会暂时使用新的 URL 进行后续的请求,但不会缓存这个重定向

HTTP/1.1 302 Found\r\n
Location: https://www.new-url.com\r\n

4. 客户端错误状态码 4XX

        400        含义为:Bad Request        应用场景:填写表单时,格式不正确导致提交失败

        401        含义为:Unauthorized        应用场景:访问需要登陆的页面的时候,未登陆或认证失败

        403        含义为:Forbidden             应用场景:尝试访问没有查看权限的页面

        404        含义为:Not Found            应用场景:访问不存在的网页链接

5. 服务器错误状态码 5XX

        500        含义为:Internal Server Error        应用场景:服务器崩溃或数据错误导致页面无法加载

        502        含义为:Bad Gateway                   应用场景:使用代理服务器时,代理服务器无法从上游服务器获取有效响应

        503        含义为:Service Unavailable         应用场景:服务器维护或者过载,暂时无法处理请求

HTTP常见报头(header)

        在 HTTP 的请求报头以及响应报头之中都存在相应的报头,其中常见报头如下:

Content-Type: 数据类型(text/html/application/json等)
Content-Length: Body的长度
Host: 客户端告知服务器, 所请求的资源是在哪个主机的哪个端口上
User-Agent: 声明用户的操作系统和浏览器版本信息
referer: 当前页面是从哪个页面跳转过来的
Location: 搭配 3xx 状态码使用, 告诉客户端接下来要去哪里访问
Cookie: 用于在客户端存储少量信息. 通常用于实现会话(session)的功能
Connection:请求完之后是关闭还是保持连接
Accept-Encoding:客户端支持的数据压缩格式,只要客户端服务器相适应,在传输过程中可以压缩传输的数据
Accept-Language:客户端可接受的语言类型

1. connection报头

        对于 Connection 报头而言,主要作用是控制和管理客户端和服务器端之间的连接状态。

        管理持久连接:Connection 字段还用于管理持久连接(长连接)。持久连接允许客户端和服务器在请求/响应完成之后不立即关闭 TCP 连接,以便于在同一个连接上发送多个请求和接收多个响应。(该作用对于需要长时间传输数据的连接效率较高)        

        协议版本:对于 HTTP/1.1 版本协议,默认使用持久连接。对于 HTTP/1.0 版本协议,默认连接是非持久的。

        语法格式:

Connection: keep-alive: 表示希望保持连接以复用 TCP 连接。
Connection: close: 表示请求/响应完成后, 应该关闭 TCP 连接

        对于其他的报头都是一些不怎么重要的报头,由于其内柔较多,本篇便不一一列举了,可参考这篇文章:HTTP协议格式详解之报头(HTTP header)、请求正文(body)_前端请求区分请求头和body、icon-default.png?t=N7T8https://blog.csdn.net/m0_74209411/article/details/137247093#:~:text=HTTP%20%E8%AF%B7%E6%B1%82%E6%8A%A5%E5%A4%B4%E8%AF%A6

HTTP Code

        以下为使用 http 协议写的一个网页代码,如下:

        Sercer.cc

#include "TcpServer.hpp"   // 通信管理,负责建立和断开通信 -> 会话层
#include "Http.hpp"
#include <memory>
#include "Log.hpp"using namespace log_ns;HttpResponce Login(HttpRequest& req) {// 则执行这个任务HttpResponce resp;std::cout << "Have got external news" << std::endl;req.GetRequestBody();std::cout << "$$$$$$$$$$$$$$$$$$$$$$$$$$$$$" << std::endl;resp.AddStatuscode(200, "OK");resp.AddContent("<html><h1>result done!</h1></html>");// 在这里可以进行程序替换、重定向,执行任务// pipe -> dup2 -> fork -> exec*(python/java/php)return resp;
}// 自己建立端口号
int main(int argc, char* argv[]) {if (argc != 2) {LOG(ERROR, "please input: ./server port\n");return 1;}uint16_t port = std::stoi(argv[1]);HttpServer http_server;http_server.AddService("/login", Login);service_t task = std::bind(&HttpServer::HttpServerHandler, &http_server, std::placeholders::_1);std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(task, port);tsvr->Init();tsvr->Start();return 0;
}

        Http.hpp

#pragma once
#include <iostream>
#include <vector>
#include <string>
#include <unordered_map>
#include <functional>
#include <sstream>
#include <fstream>const static std::string base_sep = "\r\n";
const static std::string head_sep = ": ";
const static std::string space_sep = " ";
const static std::string httpversion = "HTTP/1.0";
const static std::string webrootdir = "wwwroot"; // web根目录
const static std::string suffix_sep = ".";
const static std::string defaultsuffix = ".default";
const static std::string homepages = "index.html";
const static std::string arg_sep = "?";class HttpRequest {
private:std::string Decode(std::string& packagestream) {// 对数据逐渐的减包auto pos = packagestream.find(base_sep);if (pos == std::string::npos) return std::string();std::string line = packagestream.substr(0, pos);// 将line从packagestream中删除packagestream.erase(0, line.size() + base_sep.size());return line.empty() ? base_sep : line;}void ParseRequestLine() {std::stringstream ss(_req_line);ss >> _method >> _url >> _version;ChangeGet();_path += _url;if (_path.back() == '/') {_path += homepages;}auto pos = _path.rfind(suffix_sep);if (pos == std::string::npos) {_suffix = defaultsuffix;} else {_suffix = _path.substr(pos);}}void ParseHeaderLine() {for (auto& head : _req_headers) {// 现在从中取出数据auto pos = head.find(head_sep);if (pos == std::string::npos) continue;std::string k = head.substr(0, pos);std::string v = head.substr(pos + head_sep.size());if (k.empty() || v.empty()) continue;_headers_kv[k] = v;}}// 若当前的方法是 GET,则将后面的参数都给放到正文中void ChangeGet() {if (strcasecmp(_method.c_str(), "GET") == 0) {auto pos = _url.find(arg_sep);if (pos != std::string::npos) {std::string arg_str = _url.substr(pos + arg_sep.size());_url.resize(pos);arg_str += base_sep;_body_text += arg_str;// std::cout << "body text: " << _body_text << std::endl;}}}public:HttpRequest() : _blank_line(base_sep),_path(webrootdir){}void Deserialize(std::string& packagestream) {_req_line = Decode(packagestream);do {std::string line = Decode(packagestream);if (line.empty()) break;else if (line == base_sep) break;_req_headers.push_back(line);} while (true);if (!packagestream.empty()) {_body_text = packagestream;}ParseRequestLine();ParseHeaderLine();}std::string Suffix() {return _suffix;}void GetRequestBody() {std::cout << "body text: " << _body_text << std::endl;}std::string Path() {return _path;}void PrintRequest() {// std::cout << _req_line << std::endl;// for (auto& head : _req_headers) {//     std::cout << head << std::endl;// }// std::cout << _blank_line;// std::cout << _body_text << std::endl;std::cout << _method << " " << _url << " " << _version << std::endl;for (auto& it : _headers_kv) {std::cout << it.first << head_sep << it.second << std::endl;}std::cout << _blank_line;std::cout << _body_text << std::endl;}~HttpRequest() {}
private:std::string _req_line;std::vector<std::string> _req_headers;std::string _blank_line;std::string _body_text;// 更将详细的解析std::string _method;std::string _url;std::string _version;std::unordered_map<std::string, std::string> _headers_kv;std::string _path;std::string _suffix;
};class HttpResponce {
private:public:HttpResponce(): _version(httpversion),_blank_line(base_sep){}void AddStatuscode(int code, const std::string& desc) {_status_code = code;_desc_code = desc;}void AddHead(const std::string& key, const std::string& value) {_headers_kv[key] = value;}void AddContent(const std::string& content) {_body_text += content;}std::string Serialize() {_status_line = _version + space_sep + std::to_string(_status_code) + space_sep + _desc_code + base_sep;for (auto& head : _headers_kv) {std::string line = head.first + head_sep + head.second + base_sep;_resp_headers.emplace_back(line);}std::string responcepackage = _status_line;for (auto& line : _resp_headers) {responcepackage += line;}responcepackage += _blank_line;responcepackage += _body_text;return responcepackage;}~HttpResponce() {}
private:std::string _status_line;std::vector<std::string> _resp_headers;std::string _blank_line;std::string _body_text;// 真正的状态的std::string _version;int _status_code;std::string _desc_code;std::unordered_map<std::string, std::string> _headers_kv;
};using func_t = std::function<HttpResponce(HttpRequest&)>;class HttpServer {
private:// 从目录中读取信息std::string ReadContentFromRootdir(const std::string& path) {// 将文件以二进制形式打开// std::cout << "------------" << std::endl;std::ifstream in(path, std::ios::binary);if (!in.is_open()) return std::string();std::string content;in.seekg(0, in.end);int filesize = in.tellg();in.seekg(0, in.beg);content.resize(filesize);in.read((char*)content.c_str(), filesize);// std::cout << "xxxxxxxxxxxxxxxxx" << std::endl;in.close();return content;}public:HttpServer() {_code_to_desc[100] = "Continue";_code_to_desc[200] = "OK";_code_to_desc[201] = "Created";_code_to_desc[204] = "No Content";_code_to_desc[301] = "Moved Permanently";_code_to_desc[302] = "Found";_code_to_desc[304] = "Not Modified";_code_to_desc[400] = "Bad Request";_code_to_desc[401] = "Unauthorized";_code_to_desc[403] = "Forbidden";_code_to_desc[404] = "Not Found";_code_to_desc[500] = "Internal Server Error";_code_to_desc[502] = "Bad Gateway";_code_to_desc[503] = "Service Unavailable";_mini_type[".html"] = "text/html";_mini_type[".jpg"] = "image/jpeg";_mini_type[".png"] = "image/png";_mini_type[".default"] = "text/html";}// #define TEST 1// 将数据进行转发std::string HttpServerHandler(std::string& reqstr) {
#ifdef TESTstd::cout << "-----------------------------" << std::endl;std::cout << reqstr;std::string responsestr = "HTTP/1.1 200 OK\r\n";responsestr += "Content-Type: text/html\r\n";responsestr += "\r\n";responsestr += "<html><h1>hello Linux, hello fans!</h1></html>";// Content-Length// return responsestr;return reqstr;
#elsestd::cout << "----------------------------------------" << std::endl;std::cout << reqstr << std::endl;HttpRequest Req;Req.Deserialize(reqstr);// 将正文消息打印出来// Req.GetRequestBody();// 将消息打印出来// std::cout << content << std::endl;HttpResponce Resp;// 要在这里测试重定向 --> 进入的我的主页面if (Req.Path() == "wwwroot/redir") {Resp.AddStatuscode(301, _code_to_desc[301]);std::string path = "https://blog.csdn.net/m0_74830524?spm=1000.2115.3001.5343";Resp.AddHead("Location", path);Resp.AddHead("Content-Type", defaultsuffix);return Resp.Serialize();}std::string content = ReadContentFromRootdir(Req.Path());if (content.empty()) {// 为空,为 404 not findif (!_service_list.count(Req.Path())) {Resp.AddStatuscode(404, _code_to_desc[404]);content = ReadContentFromRootdir("wwwroot/404.html");Resp.AddHead("Content-Length", std::to_string(content.size()));std::string suffix = Req.Suffix();Resp.AddHead("Content-Type", _mini_type[suffix]);Resp.AddHead("Set-Cookie", "username=zhangsan");Resp.AddContent(content);} else {// 存在我们则执行servicelist中的任务Resp = _service_list[Req.Path()](Req);}} else {// Resp.AddStatuscode(200, _code_to_desc[200]);Resp.AddHead("Content-Length", std::to_string(content.size()));Resp.AddHead("Content-Type", "text/html");Resp.AddContent(content);}return Resp.Serialize();
#endif}void AddService(const std::string& name, func_t service) {std::string servicename = webrootdir + name;_service_list[servicename] = service;}~HttpServer() {}private:std::unordered_map<int, std::string> _code_to_desc;std::unordered_map<std::string, std::string> _mini_type;std::unordered_map<std::string, func_t> _service_list;
};

        InetAddr.hpp

#pragma once
#include <iostream>
#include <string>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>class InetAdrr {void ToHost(const struct sockaddr_in& addr) {// inet_ntoa 函数不是线程安全的函数,推荐使用 inet_ntop 函数// _ip = inet_ntoa(addr.sin_addr);char ip_buff[32];// 该函数是网络序列转主机序列 :network to processinet_ntop(AF_INET, &addr.sin_addr, ip_buff, sizeof(ip_buff));// 若想要将主机序列转换成网络序列使用函数 :// inet_pton(AF_INET, _ip.c_str(), (void*)&addr.sin_addr.s_addr); _ip = ip_buff;_port = ntohs(addr.sin_port);}
public:InetAdrr() {}InetAdrr(const struct sockaddr_in& addr) : _addr(addr){ToHost(_addr);}InetAdrr& operator=(const struct sockaddr_in& addr) {_addr = addr;return *this;}std::string Ip() const {return _ip;}bool operator==(const InetAdrr& addr) {return (_port == addr._port && _ip == addr._ip);}struct sockaddr_in Addr() const {return _addr;}std::string AddrString() const {return _ip + ":" + std::to_string(_port);}uint16_t Port() const {return _port;}~InetAdrr() {}
private:uint16_t _port;std::string _ip;struct sockaddr_in _addr;
};

        Log.hpp

#pragma once
#include <iostream>
#include <string>
#include <cstdarg>
#include <cstring>
#include <fstream>
#include <sys/types.h>
#include <pthread.h>
#include <unistd.h>namespace log_ns {enum { DEBUG = 1, INFO, WARNING, ERROR, FATAL };// 定义日子真正需要记录的信息struct LogMessage {std::string _level;int _id;std::string _filename;int _filenumber;std::string _curtime;std::string _log_message;};#define SCREEN_TYPE 1#define FILE_TYPE   2const std::string defaultlogfile = "./log.txt";pthread_mutex_t log_lock = PTHREAD_MUTEX_INITIALIZER;class Log {private:std::string LevelToString(int level) {switch(level) {case DEBUG:return "DEBUG";case INFO:return "INFO";case WARNING:return "WARNING";case ERROR:return "ERROR";case FATAL:return "FATAL";default:return "UNKNOWN";}}std::string CurTime() {// 获取当前的时间戳time_t curtime = time(nullptr);// 将当前时间戳转换成结构体struct tm* now = localtime(&curtime);char buff[128];snprintf(buff, sizeof(buff), "%d-%02d-%02d %02d:%02d:%02d", now->tm_year + 1900,now->tm_mon + 1,now->tm_mday,now->tm_hour,now->tm_min,now->tm_sec);return buff;}void Flush(const LogMessage& lg) {// 打印日志的时候可能存在线程安全,使用锁lock住pthread_mutex_lock(&log_lock);switch(_type) {case SCREEN_TYPE:FlushToScreen(lg);break;case FILE_TYPE:FlushToFile(lg);break;}pthread_mutex_unlock(&log_lock);}void FlushToFile(const LogMessage& lg) {std::ofstream out;out.open(_logfile, std::ios::app); // 文件的操作使用追加if (!out.is_open()) return;char buff[2024];snprintf(buff ,sizeof(buff), "[%s][%d][%s][%d][%s] %s",lg._level.c_str(),lg._id,lg._filename.c_str(),lg._filenumber,lg._curtime.c_str(),lg._log_message.c_str());            out.write(buff, strlen(buff));out.close();}void FlushToScreen(const LogMessage& lg) {printf("[%s][%d][%s][%d][%s] %s",lg._level.c_str(),lg._id,lg._filename.c_str(),lg._filenumber,lg._curtime.c_str(),lg._log_message.c_str());}public:Log(std::string logfile = defaultlogfile): _type(SCREEN_TYPE),_logfile(logfile){}void Enable(int type) {_type = type;}void LoadMessage(std::string filename, int filenumber, int level, const char* format, ...) {LogMessage lg;lg._level = LevelToString(level);lg._filename = filename;lg._filenumber = filenumber;// 获取当前时间lg._curtime = CurTime();// std::cout << lg._curtime << std::endl;lg._id = getpid();// 获取可变参数va_list ap;va_start(ap, format);char buff[2048];vsnprintf(buff, sizeof(buff), format, ap);va_end(ap);lg._log_message = buff;// std::cout << lg._log_message;Flush(lg);}void ClearOurFile() {std::ofstream out;out.open(_logfile);out.close();}~Log() {}private:int _type;std::string _logfile;};Log lg;// LOG 宏
#define LOG(level, format, ...)                                           \do                                                                    \{                                                                     \lg.LoadMessage(__FILE__, __LINE__, level, format, ##__VA_ARGS__); \} while (0)#define EnableToScreen()        \do                          \{                           \lg.Enable(SCREEN_TYPE); \} while (0)#define EnableToFile()        \do                        \{                         \lg.Enable(FILE_TYPE); \} while (0)// 清理文件
#define ClearFile()        \do                     \{                      \lg.ClearOurFile(); \} while (0)
}

        Socket.hpp

#pragma once
#include <iostream>
#include <string>
#include <functional>
#include <memory>
#include <cstring>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
#include "Log.hpp"
#include "InetAddr.hpp"namespace socket_ns {using namespace log_ns;enum {SOCKET_ERROR = 1,BIND_ERROR,LISTEN_ERROR};const int gblcklog = 8;class TcpSocket;class Socket;using ScokSPtr = std::shared_ptr<Socket>;// 模板方法模式class Socket {public:Socket() {}~Socket() {}virtual int CreateSocketOrDie() = 0;virtual void CreateBindOrDie(uint16_t port) = 0;virtual void CreateListenOrDie(int blcklog = gblcklog) = 0;virtual int CreateAccepte(InetAdrr* addr) = 0;virtual bool CreateConnector(uint16_t server_port, std::string server_ip) = 0;virtual ssize_t Recv(std::string* out) = 0;virtual ssize_t Send(std::string& in) = 0;virtual int GetSockfd() = 0;virtual void Close() = 0;public:void BuildListenSocket(uint16_t port, int blcklog = gblcklog) {// 分别是创建sockfd,然后将其绑定,然后listenCreateSocketOrDie();CreateBindOrDie(port);CreateListenOrDie(blcklog);}bool BuildCilentSocket(uint16_t server_port, std::string server_ip) {// 分别是创建sockfd,然后绑定,然后connnectCreateSocketOrDie();return CreateConnector(server_port, server_ip);}};class TcpSocket : public Socket {public:TcpSocket() {}TcpSocket(int sockfd): _sockfd(sockfd){}// 创建 sockfdint CreateSocketOrDie() override {_sockfd = socket(AF_INET, SOCK_STREAM, 0);if (_sockfd < 0) {LOG(FATAL, "create sockfd fail\n");exit(SOCKET_ERROR);}LOG(INFO, "get listensockfd success, sockfd: %d\n", _sockfd);return _sockfd;}// 绑定void CreateBindOrDie(uint16_t port) override {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;int bind_n = bind(_sockfd, (struct sockaddr*)&local, sizeof(local));if (bind_n < 0) {LOG(FATAL, "bind listensockfd fail, the reason: %s\n", strerror(errno));exit(BIND_ERROR);}LOG(INFO, "bind success\n");}// 绑定之后listenvoid CreateListenOrDie(int blcklog = gblcklog) override {int n = listen(_sockfd, blcklog);if (n < 0) {LOG(FATAL, "listen socket fail\n");exit(LISTEN_ERROR);}LOG(INFO, "listen sucess\n");            }int CreateAccepte(InetAdrr* addr) override {struct sockaddr_in peer;socklen_t len = sizeof(peer);// std::cout << "start accept" << std::endl;int sockfd = accept(_sockfd, (struct sockaddr*)&peer, &len);*addr = peer;// std::cout << "accept done" << std::endl;return sockfd;}bool CreateConnector(uint16_t server_port, std::string server_ip) override {struct sockaddr_in server;memset(&server, 0, sizeof(server));socklen_t len = sizeof(server);server.sin_family = AF_INET;server.sin_port = htons(server_port);// server.sin_addr.s_addr = inet_addr(server_ip.c_str());inet_pton(AF_INET, server_ip.c_str(), &server.sin_addr);int n = connect(_sockfd, (struct sockaddr*)&server, sizeof(server));if (n < 0) {return false;                }return true;}ssize_t Recv(std::string* out) override {// 收消息,将收到的信息char buff[4096];ssize_t n = recv(_sockfd, buff, sizeof(buff), 0);if (n <= 0) return n;buff[n] = 0;*out += buff;return n;}ssize_t Send(std::string& in) override {ssize_t n = send(_sockfd, in.c_str(), in.size(), 0);return n;}int GetSockfd() override {return _sockfd;}void Close() override {close(_sockfd);}~TcpSocket() {// if (_sockfd < 0) close(_sockfd);}private:int _sockfd;};
}

         TcpServer.hpp

#pragma once
#include <iostream>
#include <string>
#include <functional>
#include <cstring>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
#include "Log.hpp"
#include "InetAddr.hpp"
#include "Socket.hpp"using namespace log_ns;
using namespace socket_ns;enum {SOCKET_ERROR = 1,BIND_ERROR,LISTEN_ERROR
};const int glistensockfd = -1;
const int gblcklog = 8;using service_t = std::function<std::string(std::string&)>;class TcpServer {
private:// 创建一个内部类struct ThreadData {ScokSPtr _tcp_socket;InetAdrr _addr;TcpServer* _tcp_point;ThreadData(const ScokSPtr& tcpsocket, const InetAdrr& addr, TcpServer* tcp): _tcp_socket(tcpsocket),_addr(addr),_tcp_point(tcp){}};static void* runServer(void* args) {// 将线程分离,就不用阻塞的join线程pthread_detach(pthread_self());ThreadData* td = static_cast<ThreadData*>(args);// LOG(INFO, "the sockfd: %d\n", td->_sockfd);// 在这里接收消息,然后将消息放入到任务中std::string packagestream;td->_tcp_socket->Recv(&packagestream);// 对接收的消息进行处理,然后将处理结束的数据发送回来std::string package = td->_tcp_point->_task(packagestream);// 现在将package发送出去td->_tcp_socket->Send(package);td->_tcp_socket->Close();// close(); // 将其转化为tcpsocket变量则不需要显示的close了,因为已经析构了delete td;return nullptr;}public:// TcpServer(){}TcpServer(service_t task, uint16_t port): _task(task),_port(port),_isrunning(false),_tcp_socket(std::make_shared<TcpSocket>()){}void Init() {_tcp_socket->BuildListenSocket(_port);}void Start() {_isrunning = true;while (_isrunning) {// std::cout << "start run" << std::endl;InetAdrr addr;int sockfd = _tcp_socket->CreateAccepte(&addr);if (sockfd < 0) {LOG(ERROR, "%s get sockfd fail, the reason is %s\n", addr.AddrString().c_str(), strerror(errno));continue;}LOG(INFO, "get sockfd success, sockfd: %d\n", sockfd);// 为accept建立一个tcpsocket变量ScokSPtr tcp_accept = std::make_shared<TcpSocket>(sockfd);// 2. 多线程pthread_t tid;ThreadData* data = new ThreadData(tcp_accept, addr, this);pthread_create(&tid, nullptr, runServer, (void*)data);}_isrunning = false;}~TcpServer() {}
private:uint16_t _port;// int _listensocked;bool _isrunning;service_t _task;ScokSPtr _tcp_socket;
};

        Thread.hpp

#pragma once
#include <iostream>
#include <functional>
#include <string>
#include <unistd.h>
#include <pthread.h>
#include <cstring>
#include <cerrno>
#include "Log.hpp"// using func_t = std::function<void(const std::string& name, pthread_mutex_t* lock)>;
using func_t = std::function<void(const std::string& name)>;
using namespace log_ns;
// typedef void*(*func_t)(void*);const pthread_t ctid = -1;class Thread {
private:void excute() {// std::cout << _name << " begin to run" << std::endl;// LOG(INFO, "%s begin to run\n", _name.c_str());_isrunning = true;_func(_name);_isrunning = false;}static void* ThreadRoutine(void* args) {Thread* self = static_cast<Thread*>(args);self->excute();return nullptr;}
public:Thread(func_t func, const std::string& name) : _func(func),_isrunning(false),_tid(ctid),_name(name){}~Thread() {}void Start() {// 创建之后就开始运行了int n = pthread_create(&_tid, nullptr, ThreadRoutine, (void*)this);if (n != 0) {std::cout << "thread create failed!!!" << std::endl;exit(1);}}void Stop() {// 将线程暂停,使用if (_isrunning == false) return;// std::cout << _name << " stop " << std::endl;int n = ::pthread_cancel(_tid);if (n != 0)  {std::cout << "thread stop failed" << std::endl;}_isrunning = false;}void Join() {// 线程等待,if (_isrunning) return;int n = pthread_join(_tid, nullptr);if (n != 0) {std::cout << "thread wait failed!!!" << strerror(errno) << std::endl;}// std::cout << _name << " join " << std::endl;}std::string Status() {if (_isrunning) return "running";else return "sleep";}
private:pthread_t _tid;func_t _func;bool _isrunning;std::string _name;
};

        makefile

.PHONY:all
all:server server:Server.ccg++ -o $@ $^ -std=c++14 -lpthread -ljsoncpp.PHONY:clean
clean:rm -f server

        404.html

<!DOCTYPE html>
<html>
<head><meta charset="utf-8"><title>(404) The page you were looking for doesn't exist.</title><link rel="stylesheet" type="text/css" href="//cloud.typography.com/746852/739588/css/fonts.css" /><style type="text/css">html,body {margin: 0;padding: 0;height: 100%;}body {font-family: "Whitney SSm A", "Whitney SSm B", "Helvetica Neue", Helvetica, Arial, Sans-Serif;background-color: #2D72D9;color: #fff;-moz-font-smoothing: antialiased;-webkit-font-smoothing: antialiased;}.error-container {text-align: center;height: 100%;}@media (max-width: 480px) {.error-container {position: relative;top: 50%;height: initial;-webkit-transform: translateY(-50%);-ms-transform: translateY(-50%);transform: translateY(-50%);}}.error-container h1 {margin: 0;font-size: 130px;font-weight: 300;}@media (min-width: 480px) {.error-container h1 {position: relative;top: 50%;-webkit-transform: translateY(-50%);-ms-transform: translateY(-50%);transform: translateY(-50%);}}@media (min-width: 768px) {.error-container h1 {font-size: 220px;}}.return {color: rgba(255, 255, 255, 0.6);font-weight: 400;letter-spacing: -0.04em;margin: 0;}@media (min-width: 480px) {.return {position: absolute;width: 100%;bottom: 30px;}}.return a {padding-bottom: 1px;color: #fff;text-decoration: none;border-bottom: 1px solid rgba(255, 255, 255, 0.6);-webkit-transition: border-color 0.1s ease-in;transition: border-color 0.1s ease-in;}.return a:hover {border-bottom-color: #fff;}</style>
</head><body><div class="error-container"><h1>404</h1><p class="return">Take me back to <a href="/">designernews.co</a></p>
</div></body>
</html>

        content.html

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>内容页面</title>
</head>
<body><h1>内容页面</h1><a href="/register.html">进入注册页面</a>
</body>
</html>

        index.html

<!DOCTYPE html>
<html>
<head><title>桀桀桀桀桀桀</title>        <meta charset="UTF-8">
</head>
<body><div id="container" style="width:800px"><div id="header" style="background-color:#FFA500;"><h1 style="margin-bottom:0;">我的网站</h1></div><div id="menu" style="background-color:#FFD700;height:200px;width:100px;float:left;"><b>Menu</b><br>HTML<br>CSS<br>JavaScript</div><div id="content" style="background-color:#EEEEEE;height:200px;width:700px;float:left;">内容就在这里</div><div id="footer" style="background-color:#ffa500;clear:both;text-align:center;">Copyright © 桀桀桀桀桀桀</div></div><a href="/login.html">点击测试: 登陆页面</a><div><img src="/image/1.png" alt="一张图片"><!-- <img src="/image/2.jpg" alt="第二张图片"> --></div><div></div>
</body>
</html>

        login.html

<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>登陆页面</title>
</head><body><h1>登陆页面</h1><a href="/content.html">进入内容页面</a><br><a href="/a/b/c.html">测试404</a><br><a href="/redir">测试重定向</a><br><div><!-- 默认就是GET --><form action="/login" method="POST">用户名: <input type="text" name="username" value=""><br>密码: <input type="password" name="userpasswd" value=""><br><input type="submit" value="提交"></form></div></body></html>

        register.html

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>注册页面</title>
</head>
<body><h1>注册页面</h1><a href="/">回到首页</a>
</body>
</html>

        当前目录树状图如下:

        对于如上的 wwwroot 目录下的 image 目录中的图片是啥都可以,但是想要成功的让图片显示在网页中,命名一定要一致,测试结果如下:

        如上在浏览器中填入的 ip 和 port(端口),是我运行程序的 ip,port 为自己设定的,若想要使用如上代码成功在浏览器中访问到对应的网页资源,需要将自己机器的 ip 和 port 开放(我的为华为云服务器,在华为云官方中开放的端口,想要开放自己的 ip 和 端口可在网上搜索。)

Http.hpp 代码思路

        http 协议代码实现的原理本质就是由 http 请求报文和 http 响应报文以及数据发送三个模块组成的。

1. HttpRequest

        按照请求报文的思路将发送过来的请求报文给解析,先获取请求行,然后获取请求报头的键值和关键值,接着获取请求正文。

        但是对于进行如上的步骤之前,我们需要先将在网络中传输的序列化的数据进行反序列化,反序列化就只需要按照 http 报文格式进行拆解获取即可,因为网络中传输的序列化的数据其实就是字符串。

        所以只需要将报文中的各种信息抽象为对应的变量即可。代码如下,对于每个模块的代码都有对应的注释

const static std::string base_sep = "\r\n";
const static std::string head_sep = ": ";
const static std::string space_sep = " ";
const static std::string httpversion = "HTTP/1.0";
const static std::string webrootdir = "wwwroot"; // web根目录
const static std::string suffix_sep = ".";
const static std::string defaultsuffix = ".default";
const static std::string homepages = "index.html";
const static std::string arg_sep = "?";class HttpRequest {
private:// 解包一行的http报文std::string Decode(std::string& packagestream) {// 对数据逐渐的解包auto pos = packagestream.find(base_sep);if (pos == std::string::npos) return std::string();std::string line = packagestream.substr(0, pos);// 将line从packagestream中删除packagestream.erase(0, line.size() + base_sep.size());return line.empty() ? base_sep : line;}// 解析请求行void ParseRequestLine() {std::stringstream ss(_req_line);ss >> _method >> _url >> _version;ChangeGet();_path += _url;// 若当前请求的资源为根目录下的资源,直接让其跳转到对应的主页面if (_path.back() == '/') {_path += homepages;}// 获取对应资源文件的后缀auto pos = _path.rfind(suffix_sep);if (pos == std::string::npos) {_suffix = defaultsuffix;} else {_suffix = _path.substr(pos);}}// 解析请求报头void ParseHeaderLine() {for (auto& head : _req_headers) {// 现在从中取出数据auto pos = head.find(head_sep);if (pos == std::string::npos) continue;std::string k = head.substr(0, pos);std::string v = head.substr(pos + head_sep.size());if (k.empty() || v.empty()) continue;_headers_kv[k] = v;}}// 若当前的方法是 GET,则将后面的参数都给放到正文中void ChangeGet() {if (strcasecmp(_method.c_str(), "GET") == 0) {auto pos = _url.find(arg_sep);if (pos != std::string::npos) {std::string arg_str = _url.substr(pos + arg_sep.size());_url.resize(pos);arg_str += base_sep;_body_text += arg_str;// std::cout << "body text: " << _body_text << std::endl;}}}public:HttpRequest() : _blank_line(base_sep),_path(webrootdir){}// 将请求报文进行反序列化void Deserialize(std::string& packagestream) {_req_line = Decode(packagestream);// 获取请求报头行do {std::string line = Decode(packagestream);if (line.empty()) break;else if (line == base_sep) break;_req_headers.push_back(line);} while (true);if (!packagestream.empty()) {_body_text = packagestream;}// 解析请求行和请求报头ParseRequestLine();ParseHeaderLine();}std::string Suffix() {return _suffix;}void GetRequestBody() {std::cout << "body text: " << _body_text << std::endl;}std::string Path() {return _path;}// DEBUG 打印解析出来的请求报文void PrintRequest() {// std::cout << _req_line << std::endl;// for (auto& head : _req_headers) {//     std::cout << head << std::endl;// }// std::cout << _blank_line;// std::cout << _body_text << std::endl;std::cout << _method << " " << _url << " " << _version << std::endl;for (auto& it : _headers_kv) {std::cout << it.first << head_sep << it.second << std::endl;}std::cout << _blank_line;std::cout << _body_text << std::endl;}~HttpRequest() {}
private:std::string _req_line;                  // 请求行std::vector<std::string> _req_headers;  // 请求报头std::string _blank_line;                // 空行std::string _body_text;                 // 请求正文// 更将详细的解析,将如上的四种变量解析如下std::string _method;                                       // 获取方法:GET、POST、DELETE……std::string _url;                                          // 获取urlstd::string _version;                                      // http版本std::unordered_map<std::string, std::string> _headers_kv;  // 获取http报头std::string _path;              // 解析url中的资源路径std::string _suffix;            // 获取资源文件的后缀
};

2. HttpResponse

        对于 http 的响应报文而言,同样和请求报文的处理方式一样,不过是相反的步骤,我们需要将需要发送出去的报文给序列化,首先需要按照响应报文的格式将对应的状态行,响应报头,空行以及正文按照对应的格式组装在一起,然后在返回即可。

        同时将对应的报文格式中的各种信息抽象为具体的变量,代码如下:

class HttpResponce {
private:public:HttpResponce(): _version(httpversion),_blank_line(base_sep){}// 增加状态码以及对应的状态描述void AddStatuscode(int code, const std::string& desc) {_status_code = code;_desc_code = desc;}// 添加对应的响应报头void AddHead(const std::string& key, const std::string& value) {_headers_kv[key] = value;}// 添加对应的文本信息void AddContent(const std::string& content) {_body_text += content;}// 将报文序列化,然后返回std::string Serialize() {// 序列化状态行_status_line = _version + space_sep + std::to_string(_status_code) + space_sep + _desc_code + base_sep;for (auto& head : _headers_kv) {std::string line = head.first + head_sep + head.second + base_sep;_resp_headers.emplace_back(line);}// 序列换响应报头std::string responcepackage = _status_line;for (auto& line : _resp_headers) {responcepackage += line;}responcepackage += _blank_line;responcepackage += _body_text;return responcepackage;}~HttpResponce() {}
private:std::string _status_line;               // 状态行std::vector<std::string> _resp_headers; // 响应报头std::string _blank_line;                // 空行std::string _body_text;                 // 正文// 真正的状态的std::string _version;                   // http版本int _status_code;                       // 状态码std::string _desc_code;                 // 状态描述std::unordered_map<std::string, std::string> _headers_kv; // 响应报头的键值和关键值
};

3. HTTP报文发送

        对于 http 报文转发,我们只需要调用 HttpRequest 中的反序列化,然后获取对应请求报文中的资源路径,通过路径获取对应的资源,然后在使用 HttpResponce 将资源和对应的报头组合在一起,将其发送出去。

        还在对应的代码中加入了重定向,测试 404,以及 Cookie 等操作,代码如下:

using func_t = std::function<HttpResponce(HttpRequest&)>;class HttpServer {
private:// 从目录中读取信息std::string ReadContentFromRootdir(const std::string& path) {// 将文件以二进制形式打开// std::cout << "------------" << std::endl;std::ifstream in(path, std::ios::binary);if (!in.is_open()) return std::string();std::string content;in.seekg(0, in.end);int filesize = in.tellg();in.seekg(0, in.beg);content.resize(filesize);in.read((char*)content.c_str(), filesize);// std::cout << "xxxxxxxxxxxxxxxxx" << std::endl;in.close();return content;}public:// 构造函数将状态码与状态描述  文件后缀与网页类型初始化HttpServer() {_code_to_desc[100] = "Continue";_code_to_desc[200] = "OK";_code_to_desc[201] = "Created";_code_to_desc[204] = "No Content";_code_to_desc[301] = "Moved Permanently";_code_to_desc[302] = "Found";_code_to_desc[304] = "Not Modified";_code_to_desc[400] = "Bad Request";_code_to_desc[401] = "Unauthorized";_code_to_desc[403] = "Forbidden";_code_to_desc[404] = "Not Found";_code_to_desc[500] = "Internal Server Error";_code_to_desc[502] = "Bad Gateway";_code_to_desc[503] = "Service Unavailable";_mini_type[".html"] = "text/html";_mini_type[".jpg"] = "image/jpeg";_mini_type[".png"] = "image/png";_mini_type[".default"] = "text/html";}// #define TEST 1// 将数据进行转发std::string HttpServerHandler(std::string& reqstr) {
#ifdef TESTstd::cout << "-----------------------------" << std::endl;std::cout << reqstr;std::string responsestr = "HTTP/1.1 200 OK\r\n";responsestr += "Content-Type: text/html\r\n";responsestr += "\r\n";responsestr += "<html><h1>hello Linux, hello fans!</h1></html>";// Content-Length// return responsestr;return reqstr;
#elsestd::cout << "----------------------------------------" << std::endl;std::cout << reqstr << std::endl;// 将对应的请求报文给反序列化HttpRequest Req;Req.Deserialize(reqstr);// 将正文消息打印出来// Req.GetRequestBody();// 将消息打印出来// std::cout << content << std::endl;HttpResponce Resp;// 这里测试重定向 --> 进入的我的主页面if (Req.Path() == "wwwroot/redir") {// 301 表示永久重定向Resp.AddStatuscode(301, _code_to_desc[301]);std::string path = "https://blog.csdn.net/m0_74830524?spm=1000.2115.3001.5343";Resp.AddHead("Location", path);Resp.AddHead("Content-Type", defaultsuffix);return Resp.Serialize();}std::string content = ReadContentFromRootdir(Req.Path());if (content.empty()) {// 为空,为 404 not findif (!_service_list.count(Req.Path())) {Resp.AddStatuscode(404, _code_to_desc[404]);content = ReadContentFromRootdir("wwwroot/404.html");Resp.AddHead("Content-Length", std::to_string(content.size()));std::string suffix = Req.Suffix();Resp.AddHead("Content-Type", _mini_type[suffix]);Resp.AddHead("Set-Cookie", "username=zhangsan");Resp.AddContent(content);} else {// 存在我们则执行servicelist中的任务Resp = _service_list[Req.Path()](Req);}} else {// 正文内容存在,直接获取Resp.AddStatuscode(200, _code_to_desc[200]);Resp.AddHead("Content-Length", std::to_string(content.size()));Resp.AddHead("Content-Type", "text/html");Resp.AddContent(content);}return Resp.Serialize();
#endif}// 增加任务void AddService(const std::string& name, func_t service) {std::string servicename = webrootdir + name;_service_list[servicename] = service;}~HttpServer() {}private:std::unordered_map<int, std::string> _code_to_desc;         // 状态码和状态描述的映射std::unordered_map<std::string, std::string> _mini_type;    // 文件后缀与网页类型的映射std::unordered_map<std::string, func_t> _service_list;      // 任务类型的映射
};

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

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

相关文章

Apache Pig

目录 一、配置说明1.本地模式2.集群模式 二、pig的数据模型三、pig的数据类型四、惰性执行五、pig的基本语法5.1语法说明5.2案例操作 六、pig的自定义函数 一、配置说明 1.本地模式 操作的是Linux系统文件 pig -x local关键日志 当前处于root目录下 2.集群模式 连接的是…

CentOS7单机环境安装k8s集群

目录 1、环境准备 2、安装依赖工具 3、配置 Kubernetes 的国内 Yum 源 4. 安装 Kubernetes 组件 5、初始化 Kubernetes 集群 1. 容器运行时没有正常运行 1.1. 可能的原因 1.2. 解决办法 2. 初始化拉取镜像卡住 2.1. 使用国内的镜像源&#xff08;无法解决问题&#x…

AI绘画工具排行榜:探索最受欢迎的AI绘图软件特点与选择指南

AI绘画工具各有优势&#xff0c;从开放性到对特定语言和文化的支持&#xff0c;以及对图像细节和艺术性的不同关注点&#xff0c;根据具体需求选择合适的工具 MidJourney 图片品质卓越&#xff0c;充满独特创意&#xff0c;初期能够免费获取数十账高质量图片&#xff0c;整个生…

ImportError: cannot import name ‘print_log‘ from ‘logging‘

mmcv升级到2.后删除了很多 解决 查FAQ文档&#xff0c;找到 添加到mmcv.utils下即可

海事行政执法证照片要求及尺寸格式修改方法

在海事行政执法领域&#xff0c;证件照片不仅是个人形象的展示&#xff0c;更是专业严谨态度的体现。一张符合规范的照片&#xff0c;不仅能够提升执法人员的权威性&#xff0c;还能在执行任务时获得更多的尊重和信任。本文将为您详细介绍海事行政执法证照片的要求&#xff0c;…

Windows系统安装node.js环境并创建本地服务使用内网穿透发布至公网

目录 前言 1.安装Node.js环境 2.创建node.js服务 3. 访问node.js 服务 4.内网穿透 4.1 安装配置cpolar内网穿透 4.2 创建隧道映射本地端口 5.固定公网地址 前言 作者简介&#xff1a; 懒大王敲代码&#xff0c;计算机专业应届生 今天给大家聊聊Windows系统安装node.js环…

网络安全知识科普:什么是网络准入控制系统?有哪些?

在当今数字化时代&#xff0c;网络安全已成为企业和组织不可忽视的重要议题。随着远程工作模式的普及和物联网设备的增加&#xff0c;网络边界越来越模糊&#xff0c;传统防火墙已经不足以应对日益复杂的威胁环境。在这种背景下&#xff0c;网络准入控制系统(Network Access Co…

Redis持久化机制—RDB与AOF

Redis持久化机制 RDB&#xff08;默认&#xff09; **思想&#xff1a;**保存整个数据库的快照&#xff0c;也就是RDB文件&#xff0c;有两种保存方式&#xff0c;前台保存save和后台保存bgsave&#xff0c;前者会阻塞主进程程&#xff0c;后者则是fork一个子进程去完成备份操…

C++入门9——list的使用

目录 1.什么是list&#xff1f; 2.list的构造 3.list迭代器的使用&#xff08;list iterator&#xff09; 4.list capacity 5.list modifiers 6.list的其他操作 1.什么是list&#xff1f; 在官网中&#xff0c;对list有这样的介绍&#xff1a; Lists are sequence co…

SLM561A​​系列 60V 10mA到50mA线性恒流LED驱动芯片 为智能家居照明注入新活力

SLM561A系列选型参考&#xff1a; SLM561A10ae-7G SOD123 SLM561A15ae-7G SOD123 SLM561A20ae-7G SOD123 SLM561A25ae-7G SOD123 SLM561A30ae-7G SOD123 SLM561A35ae-7G SOD123 SLM561A40ae-7G SOD123 SLM561A45ae-7G SOD123 SLM561A50ae-7G SOD123 …

【软件文档】软件系统需求管理规程(项目管理word原件)

软件资料清单列表部分文档清单&#xff1a;工作安排任务书&#xff0c;可行性分析报告&#xff0c;立项申请审批表&#xff0c;产品需求规格说明书&#xff0c;需求调研计划&#xff0c;用户需求调查单&#xff0c;用户需求说明书&#xff0c;概要设计说明书&#xff0c;技术解…

中秋之美——html5+css+js制作中秋网页

中秋之美——html5cssjs制作中秋网页 一、前言二、功能展示三、系统实现四、其它五、源码下载 一、前言 八月十五&#xff0c;秋已过半&#xff0c;是为中秋。 “但愿人长久&#xff0c;千里共婵娟”&#xff0c;中秋时节&#xff0c;气温已凉未寒&#xff0c;天高气爽&#x…

VS Code 调试go程序的相关配置说明

用 VS code 调试Go程序需要在.vscode/launch.json文件中增加如下配置&#xff1a; // launch.json {// Use IntelliSense to learn about possible attributes.// Hover to view descriptions of existing attributes.// For more information, visit: https://go.microsoft.…

改写二进制文件

以下是一些常见的方法和工具&#xff1a; 1. 使用十六进制编辑器 十六进制编辑器 是最直接的工具之一&#xff0c;用于查看和编辑二进制文件中的数据。它允许你以十六进制格式查看和修改文件内容。 常见十六进制编辑器&#xff1a; HxD&#xff08;Windows&#xff09;Hex F…

【LabVIEW学习篇 - 16】:文件操作

文章目录 CSV文件CSV写入CSV读取 TXT文件txt写入txt读取 INI文件INI文件写入INI文件读取 CSV文件 .csv (Comma-Separated Values&#xff09;是逗号分隔值文件格式&#xff0c;有时也称之为字符分隔值&#xff0c;因为分隔符也可以不是逗号(最常见的是逗号和制表符)&#xff0…

(一)十分简易快速 自己训练样本 opencv级联haar分类器 车牌识别

🍂1、不说废话,现象展示 🍃图片识别 🍃视频识别 自己训练样本 十分简易快速 opencv级联ha

小皮面板webman ai项目本地启动教程

1.前置条件 下载小皮面板 下载后&#xff0c;双击安装&#xff0c;一路next&#xff08;下一步&#xff09;&#xff0c;无需更改配置。 2.安装必须软件 在小皮面板的软件管理页&#xff0c;安装编号①②③④下面四个软件。 3.启动本地服务 进入到小皮面板的首页&#x…

空指针异常 (NullPointerException)怎么办

在 Java 编程中&#xff0c;空指针异常&#xff08;NullPointerException&#xff0c;简称 NPE&#xff09;是最常见且困扰开发人员的异常之一。尽管 Java 是一种强类型语言&#xff0c;设计上提供了类型安全的特性&#xff0c;但空指针问题依然是开发过程中最常见的运行时异常…

MES系统如何支持企业进行数字化转型

MES系统&#xff08;Manufacturing Execution System&#xff0c;制造执行系统&#xff09;在企业数字化转型中扮演着至关重要的角色&#xff0c;它通过提供实时的生产数据、优化生产流程、提升质量管理水平、实现设备智能化管理以及促进企业内部协同和沟通等多种方式&#xff…

Linux_kernel移植linux09

一、温故知新 1、分析uboot源码目录 每个目录基本上都会有自己的Makefile进行当前层级目录的编译&#xff0c;最后在整个uboot源码目录中会有一个Makefile文件进行整合&#xff0c;将每一层级编译出的目标文件&#xff0c;整合到一起&#xff0c;链接到一起&#xff0c;最终生成…