当前位置: 首页 > news >正文

【Linux网络编程】应用层:HTTP协议 | URL | 简单实现一个HTTP服务器 | 永久重定向与临时重定向


前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站


在这里插入图片描述

🌈个人主页: 南桥几晴秋
🌈C++专栏: 南桥谈C++
🌈C语言专栏: C语言学习系列
🌈Linux学习专栏: 南桥谈Linux
🌈数据结构学习专栏: 数据结构杂谈
🌈数据库学习专栏: 南桥谈MySQL
🌈Qt学习专栏: 南桥谈Qt
🌈菜鸡代码练习: 练习随想记录
🌈git学习: 南桥谈Git

🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈
本科在读菜鸡一枚,指出问题及时改正


文章目录

  • 前言
  • HTTP协议
  • URL
    • urlencode 和 urldecode(了解)
  • HTTP协议请求与响应格式
  • 简单实现一个HTTP服务器
    • HTTP请求及反序列化
      • HTTP请求的基本框架
      • 封装反序列化
      • 获取每一行:Getline
      • 进一步解析请求行
      • 进一步解析请求报头
      • 测试
    • HTTP响应及序列化
      • HTTP响应基本框架
      • 封装序列化
      • 使用telnet进行抓包
      • 完整代码
      • 测试
      • 为什么只访问首页时但是依然有下面的图片
    • 理解网站页面跳转
    • HTTP 常见 Header
      • 关于connection报头
    • HTTP的状态码
    • 永久重定向与临时重定向
    • HTTP请求方法
      • HTTP常见方法
        • GET方法
        • POST方法
    • 完整HTTP代码



前言

实现一个简单的HTTP服务器点击链接获取源码

在这里插入图片描述

在这里插入图片描述

HTTP协议

应用层有很多协议,其中HTTP协议就是其中一个,HTTP协议(超文本传输协议)也是较为重要的一个协议。

在互联网世界中, HTTP(HyperText Transfer Protocol, 超文本传输协议) 是一个至关重要的协议。 它定义了客户端(如浏览器) 与服务器之间如何通信, 以交换或传输超文本(如 HTML 文档)。

HTTP 协议是客户端与服务器之间通信的基础。 客户端通过 HTTP 协议向服务器发送请求, 服务器收到请求后处理并返回响应。 HTTP 协议是一个无连接、 无状态的协议, 即每次请求都需要建立新的连接, 且服务器不会保存客户端的状态信息。

URL

说到URL大家可能不熟悉,但是说到“网址”大家就一目了然,实际上,“网址”就是URL。

在这里插入图片描述

在这里插入图片描述

  • 域名会自动转化成IP地址,称为DNS
  • 协议名称和端口号是强关联的,在浏览器这个网址中将端口号默认忽略了,当浏览器发起请求时会自动拼接端口号80(指明端口)

HTTP做的工作:用户在网络上获取资源(图片、文本、视频等)时是在服务器端获取,所有的资源都是在服务器端。通过某种协议(如http协议或者https协议)来标识用户所需要的资源,然后返回给用户。

我们知道Linux适合做后端开发,那么这些服务器端的资源是在Linux操作系统中。而Linux操作系统一切皆文件,必须得找到对应的资源,需要通过路径来标识,因此在URL后半部分就是路径。

因此,URL就是统一资源定位符,域名+文件路径,标识互联网中唯一的文件资源。

urlencode 和 urldecode(了解)

/ ? : 等这样的字符, 已经被 url 当做特殊意义理解了. 因此这些字符不能随意出现。比如, 某个参数中需要带有这些特殊字符, 就必须先对特殊字符进行转义。
转义的规则如下:
将需要转码的字符转为 16 进制, 然后从右到左, 取 4 位(不足 4 位直接处理), 每 2 位做一位, 前面加上%, 编码成%XY 格式。

在这里插入图片描述

HTTP协议请求与响应格式

HTTP请求

在HTTP请求中,格式如下:

在这里插入图片描述

  • 首行:称之为请求行,格式为 方法(POST、GET、HEAD…)+URI(即URL后半部分—>用户访问资源的路径)+版本号

  • HTTP的方法:
    在这里插入图片描述

HTTP响应

在HTTP请求中,格式如下:

在这里插入图片描述

  • 这里的DATA是用户需要的资源内容:网页,图片的二进制、视频的二进制、音频的二进制等

HTTP中如何将报头和有效载荷进行分离(封装)? 通过空行即\r\n

如何HTTP的请求和响应读到一个完整的报头?通过换行符可以知道读到一个完整的报文。

如果请求和响应中包含了正文(DATA),如何保证读到一个完整的报头?怎么知道正文部分的长度?在之前的网络版本计算器中只有一个字段,拥有有效载荷长度的字段。在HTTP的请求和应答中报头属性中有一个公共属性叫做Content-Length: xxx,无论是请求还是响应,如果有正文部分,那么这个字段一定包含,这样就能读到一个完整的报文。

简单实现一个HTTP服务器

HTTP请求及反序列化

HTTP请求的基本框架

在HTTP请求中,需要基本的属性(请求行、请求报头、空行、请求正文)以及进一步显示出具体属性(使用的方法、使用的URL、使用的HTTP版本、请求报头以KV形式显示)

class HttpRequest
{
public:HttpRequest():_blank_line(base_sep) {}void Deserialize(std::string &reqstr){}~HttpRequest() {}
private:// 请求的基本格式std::string _req_line;std::vector<std::string> _req_handers;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;
};

封装反序列化

在反序列化中:

  1. 首先需要通过Getlinereqstr中获取请求行
  2. 获取请求报头:通过一个 do-while 循环来读取所有的请求头部信息。每次循环调用 Getline(reqstr) 获取一行数据,并将它存储在 header
  3. 获取请求正文
  4. 进一步解析请求行和请求头
void Deserialize(std::string &reqstr)
{// 基本的反序列化_req_line=Getline(reqstr);std::string header;do{header=Getline(reqstr);if(header.empty()) break;else if(header==base_sep) break;_req_handers.push_back(header);}while(true);if(!reqstr.empty())_body_text=reqstr;//进一步反序列化ParseReqLine();ParseReqHeader();
}

获取每一行:Getline

寻找base_sep的位置,如果找不到说明没有分隔符,是一个空串;如果找到了base_sep就分割从0base_sep之间的字符串。然后更新reqstr,即删除已经提取的部分。如果 line 是空字符串(即没有有效的行内容),则返回 base_sep,表示一个空行或分隔符。否则,返回提取的有效行内容 line

const static std::string base_sep="\r\n";std::string Getline(std::string &reqstr)
{auto pos=reqstr.find(base_sep);if(pos==std::string::npos) return std::string();std::string line=reqstr.substr(0,pos);reqstr.erase(0,line.size()+base_sep.size());return line.empty()?base_sep:line;
}

进一步解析请求行

解析一个请求行,并将解析的结果存储到类的成员变量 _method_url _version 中。

代码通过 std::stringstream 提供的流提取操作符 (>>) 来从 ss 中依次提取数据并赋值给 _method_url _version

  • 第一个 >> _method:将从流中提取的第一个单词(如 GET)赋值给 _method。通常情况下,_method 代表 HTTP 请求的方法(例如 GET、POST 等)。
  • 第二个 >> _url:提取流中的第二个单词(如 /index.html),并将其赋值给 _url。这个字符串通常代表请求的 URL 路径。
  • 第三个 >> _version:提取流中的第三个单词(如 HTTP/1.1),并将其赋值给 _version。这个字符串代表请求的 HTTP 版本。
void ParseReqLine()
{std::stringstream ss(_req_line);ss>>_method>>_url>>_version;
}

进一步解析请求报头

未解析的解析报头是以: 分割的,因此这里使用: 作为分隔符。

将每个请求头的键值对存储到 _headers_kv 容器中。它通过遍历 _req_handers,提取出每个请求头的键和值,并将它们插入到一个键值对容器中。

const static std::string line_sep=": ";void ParseReqHeader(){for(auto &header:_req_handers){auto pos=header.find(line_sep);if(pos==std::string::npos) continue;std::string k=header.substr(0,pos); // [)std::string v=header.substr(pos+line_sep.size());if(k.empty()||v.empty()) continue;_headers_kv.insert(std::make_pair(k,v));}}

测试

在这里插入图片描述


HTTP响应及序列化

HTTP响应基本框架

class HttpResponse
{
public:HttpResponse():_version(httpversion),_blank_line(base_sep){}void AddCode(int code){_status_code=code;_desc="OK";}void AddHeader(const std::string &k,const std::string &v){_headers_kv[k]=v;}void AddBodyText(const std::string body_text){_resp_body_text=body_text;}std::string Serialize(){}~HttpResponse(){}private:// 基本属性std::string _version;int _status_code;std::string _desc;std::unordered_map<std::string,std::string> _headers_kv;// 响应的基本格式std::string _status_line;std::vector<std::string> _resp_handers;std::string _blank_line;std::string _resp_body_text;
};

封装序列化

在HTTP响应序列化中:

  1. 构建状态行,并将其存储在 _status_line 字符串中
  2. 构建响应头,遍历 _headers_kv 容器,它是一个存储 HTTP 响应头的键值对容器。每一行头部字符串会被 push_back _resp_handers 容器中,最终该容器保存了所有响应头的字符串
  3. 正式序列化,将之前构建的各部分拼接成最终的响应报文字符串 responsestr
std::string Serialize()
{// 构建状态行_status_line=_version+spacesep+std::to_string(_status_code)+spacesep+_desc+base_sep;// 构建应答报头for(auto &hander:_headers_kv){std::string header_line=hander.first+line_sep+hander.second+base_sep;_resp_handers.push_back(header_line);}// 空行和正文// 空行已经初始化,正文已经保存在_resp_body_text中// 正式序列化std::string responsestr=_status_line;for(auto &line:_resp_handers){responsestr+=line;}responsestr+=_blank_line;responsestr+=_resp_body_text;return responsestr;
}

使用telnet进行抓包

telnet 127.0.0.1 8888
在这里插入图片描述

通过这个测试,得到响应

完整代码

class HttpResponse
{
public:HttpResponse():_version(httpversion),_blank_line(base_sep){}void AddCode(int code){_status_code=code;_desc="OK";}void AddHeader(const std::string &k,const std::string &v){_headers_kv[k]=v;}void AddBodyText(const std::string body_text){_resp_body_text=body_text;}std::string Serialize(){// 构建状态行_status_line=_version+spacesep+std::to_string(_status_code)+spacesep+_desc+base_sep;// 构建应答报头for(auto &hander:_headers_kv){std::string header_line=hander.first+line_sep+hander.second+base_sep;_resp_handers.push_back(header_line);}// 空行和正文// 空行已经初始化,正文已经保存在_resp_body_text中// 正式序列化std::string responsestr=_status_line;for(auto &line:_resp_handers){responsestr+=line;}responsestr+=_blank_line;responsestr+=_resp_body_text;return responsestr;}~HttpResponse(){}private:// 基本属性std::string _version;int _status_code;std::string _desc;std::unordered_map<std::string,std::string> _headers_kv;// 响应的基本格式std::string _status_line;std::vector<std::string> _resp_handers;std::string _blank_line;std::string _resp_body_text;
};

html

<!DOCTYPE html>
<html>
<head><title>南桥几晴秋(gwj.cn)</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 © gwj.com</div></div><div><img src="/image/1.jpg" alt="一张图片"></div>
</body>
</html>

测试

在浏览器中输入:http://119.3.220.34:8888/,得到如下界面:

在这里插入图片描述

为什么只访问首页时但是依然有下面的图片

在这里插入图片描述

获得一个完整的网页,浏览器要先得到html,根据html的标签检测出我们还要获取的其他资源,浏览器会继续发起HTTP请求。

理解网站页面跳转

我们在使用网站时,点击某个按钮进行跳转,实际上是在访问wwwroot中的文件

在这里插入图片描述

在我自己的HTTP服务器中显示如下:

在这里插入图片描述

在现代Web开发中,HTML文件的内容通常属于前端部分,而通过后端调用和渲染这些内容是常见的架构设计。

在这里插入图片描述

HTTP 常见 Header

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

关于connection报头

HTTP 中的 Connection 字段是 HTTP 报文头的一部分, 它主要用于控制和管理客户端与服务器之间的连接状态。

  • 管理持久连接: Connection 字段还用于管理持久连接(也称为长连接) 。 持久连接允许客户端和服务器在请求/响应完成后不立即关闭 TCP 连接, 以便在同一个连接上发送多个请求和接收多个响应。
  • HTTP/1.1: 在 HTTP/1.1 协议中, 默认使用持久连接。 当客户端和服务器都不明确指定关闭连接时, 连接将保持打开状态, 以便后续的请求和响应可以复用同一个连接。
  • HTTP/1.0: 在 HTTP/1.0 协议中, 默认连接是非持久的。 如果希望在 HTTP/1.0上实现持久连接, 需要在请求头中显式设置 Connection: keep-alive

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

HTTP的状态码

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

在这里插入图片描述

永久重定向与临时重定向

什么叫做重定向?

用户浏览器想目标服务器发起请求,但是目标服务器想用户浏览器推送了一个服务器,此时用户浏览器向推送的服务器进行请求,重定向的地址在location字段中。

在这里插入图片描述

所谓的永久与临时无非就是字面意思,永久即下次再请求时直接去新的服务器,会修改客户的地址,临时不对客户做任何影响。

永久重定向:当你决定将一个页面永久迁移到新地址时,使用永久重定向是正确的做法。例如,网站改版、页面URL结构优化或域名更换时。

临时重定向:当你需要在短期内将页面临时指向另一个URL时使用,比如进行系统维护、A/B测试或者临时更新页面内容时。

if(req.Path()=="wwwroot/redir")
{std::string reire_path="https://www.qq.com";resp.AddCode(302,_code_to_desc[302]);resp.AddHeader("Location",reire_path);
}

在这里插入图片描述

HTTP请求方法

在这里插入图片描述

HTTP常见方法

GET方法
  • 用途:用于请求URL指定的资源
  • 特性:指定资源经服务器端解析后返回响应内容

GET一般用来获取静态资源,也可以通过URL向服务器传递参数

std::string Method()
{LOG(DEBUG,"Client request method is %s\n",_method.c_str());return _method;
}

在这里插入图片描述

在这里插入图片描述

POST方法
  • 用途:用于传输实体的主体, 通常用于提交表单数据
  • 特性:可以发送大量的数据给服务器, 并且数据包含在请求体中

POST方法可以通过httprequest的正文来进行参数传递

在这里插入图片描述

POST方法传递参数比GET方法更私密,登录信息不回显示在浏览器中。但是都不安全。要想保证安全必须对http的参数部分进行加密。

完整HTTP代码

#pragma once#include<iostream>
#include<string>
#include<vector>
#include<functional>
#include<sstream>
#include<fstream>
#include<unordered_map>const static std::string base_sep="\r\n";
const static std::string line_sep=": ";
const static std::string prefixpath="wwwroot"; // web根目录
const static std::string homepage="index.html"; // web根目录
const static std::string httpversion="HTTP/gwj_1.0";
const static std::string spacesep=" ";
const static std::string suffixsep=".";
const static std::string html_404="404.html";
const static std::string arg_sep="?";class HttpRequest
{
private:std::string Getline(std::string &reqstr){auto pos=reqstr.find(base_sep);if(pos==std::string::npos) return std::string();std::string line=reqstr.substr(0,pos);reqstr.erase(0,line.size()+base_sep.size());return line.empty()?base_sep:line;}void ParseReqLine(){std::stringstream ss(_req_line);ss>>_method>>_url>>_version;if(strcasecmp(_method.c_str(),"GET")==0){auto pos=_url.find(arg_sep);if(pos!=std::string::npos){_body_text=_url.substr(pos+arg_sep.size());_url.resize(pos);  // 只保留了?前面的字符}}_path+=_url;if(_path[_path.size()-1]=='/'){_path+=homepage;}auto pos=_path.rfind(suffixsep);if(pos!=std::string::npos){_suffix=_path.substr(pos);}else{_suffix=".default";}}void ParseReqHeader(){for(auto &header:_req_handers){auto pos=header.find(line_sep);if(pos==std::string::npos) continue;std::string k=header.substr(0,pos); // [)std::string v=header.substr(pos+line_sep.size());if(k.empty()||v.empty()) continue;_headers_kv.insert(std::make_pair(k,v));}}public:HttpRequest():_blank_line(base_sep),_path(prefixpath){}void Deserialize(std::string &reqstr){// 基本的反序列化_req_line=Getline(reqstr);std::string header;do{header=Getline(reqstr);if(header.empty()) break;else if(header==base_sep) break;_req_handers.push_back(header);}while(true);if(!reqstr.empty())_body_text=reqstr;//进一步反序列化ParseReqLine();ParseReqHeader();}std::string Url(){LOG(DEBUG,"Client Want path %s\n",_path.c_str());return _url;}std::string Path(){LOG(DEBUG,"Client Want %s\n",_url.c_str());return _path;}std::string Suffix(){return _suffix;}std::string Method(){LOG(DEBUG,"Client request method is %s\n",_method.c_str());return _method;}std::string GetRequestBody(){LOG(DEBUG,"Client request method is %s, args: %s, request path: %s\n",_method.c_str(),_body_text.c_str(),_path.c_str());return _body_text;}void Print(){std::cout<<"-----------------------------------------------"<<std::endl;std::cout<<"###"<<_req_line<<std::endl;for(auto &header:_req_handers){std::cout<<"@@@"<<header<<std::endl;}std::cout<<"***"<<_blank_line;std::cout<<">>>"<<_body_text<<std::endl;std::cout<<"Method: "<<_method<<std::endl;std::cout<<"URL: "<<_url<<std::endl;std::cout<<"Version: "<<_version<<std::endl;for(auto header_kv:_headers_kv){std::cout<<"< "<<header_kv.first<<" > : < "<<header_kv.second<<" >"<<std::endl;}}~HttpRequest() {}
private:// 请求的基本格式std::string _req_line;std::vector<std::string> _req_handers;std::string _blank_line;std::string _body_text;//具体属性std::string _method;std::string _url;std::string _path;std::string _suffix; // 资源后缀std::string _version;std::unordered_map<std::string,std::string> _headers_kv;
};class HttpResponse
{
public:HttpResponse():_version(httpversion),_blank_line(base_sep){}void AddCode(int code,const std::string &desc){_status_code=code;_desc=desc;}void AddHeader(const std::string &k,const std::string &v){_headers_kv[k]=v;}void AddBodyText(const std::string body_text){_resp_body_text=body_text;}std::string Serialize(){// 构建状态行_status_line=_version+spacesep+std::to_string(_status_code)+spacesep+_desc+base_sep;// 构建应答报头for(auto &hander:_headers_kv){std::string header_line=hander.first+line_sep+hander.second+base_sep;_resp_handers.push_back(header_line);}// 空行和正文// 空行已经初始化,正文已经保存在_resp_body_text中// 正式序列化std::string responsestr=_status_line;for(auto &line:_resp_handers){responsestr+=line;}responsestr+=_blank_line;responsestr+=_resp_body_text;return responsestr;}~HttpResponse(){}private:// 基本属性std::string _version;int _status_code;std::string _desc;std::unordered_map<std::string,std::string> _headers_kv;// 响应的基本格式std::string _status_line;std::vector<std::string> _resp_handers;std::string _blank_line;std::string _resp_body_text;
};using func_t=std::function<HttpResponse(HttpRequest&)>;class HttpServer
{
private:std::string GetFileContent(const std::string path){std::ifstream in(path,std::ios::binary);if(!in.is_open()) return std::string();in.seekg(0,in.end);int filesize=in.tellg(); //读写偏移量,计算文件大小in.seekg(0,in.beg);std::string content;content.resize(filesize);in.read((char*)content.c_str(),filesize);in.close();return content;}public:HttpServer(){_mime_type.insert(std::make_pair(".html","text/html"));_mime_type.insert(std::make_pair(".jpg","image/jpeg"));_mime_type.insert(std::make_pair(".png","image/png"));_mime_type.insert(std::make_pair(".default","text/html"));_code_to_desc.insert(std::make_pair(100,"Continue"));_code_to_desc.insert(std::make_pair(200,"OK"));_code_to_desc.insert(std::make_pair(201,"Created"));_code_to_desc.insert(std::make_pair(301,"Moved Permanently"));_code_to_desc.insert(std::make_pair(302,"Found"));}
// #define TESTstd::string HandlerHttpRequest(std::string &reqstr) //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 gwj!</h1></html>";return responsestr;#elsestd::cout << "---------------------------------------" << std::endl;std::cout << reqstr;std::cout << "---------------------------------------" << std::endl;HttpRequest req;HttpResponse resp;  req.Deserialize(reqstr);// req.Method();if(req.Path()=="wwwroot/redir"){std::string reire_path="https://www.qq.com";resp.AddCode(302,_code_to_desc[302]);resp.AddHeader("Location",reire_path);}else if (!req.GetRequestBody().empty()){if(IsServiceExists(req.Path())){resp=_service_lists[req.Path()](req);}}else{std::string content = GetFileContent(req.Path());if (content.empty()){content = GetFileContent("wwwroot/404.html");resp.AddCode(404, _code_to_desc[404]);resp.AddHeader("Content-Length", std::to_string(content.size()));resp.AddHeader("Content-Type", _mime_type[".html"]);resp.AddBodyText(content);}else{resp.AddCode(200, _code_to_desc[200]);resp.AddHeader("Content-Length", std::to_string(content.size()));resp.AddHeader("Content-Type", _mime_type[req.Suffix()]);resp.AddBodyText(content);}}return resp.Serialize();#endif}void InsertService(const std::string servicename,func_t f){std::string s=prefixpath+servicename;_service_lists[s]=f;}bool IsServiceExists(const std::string &servicename){auto iter=_service_lists.find(servicename);if(iter==_service_lists.end()) return false;else return true;}~HttpServer() {}private:std::unordered_map<std::string,std::string> _mime_type;std::unordered_map<int,std::string> _code_to_desc;std::unordered_map<std::string,func_t> _service_lists;
};


在这里插入图片描述

http://www.lryc.cn/news/501851.html

相关文章:

  • 电压调整电路汇总
  • day28 文件IO及进程线程基础
  • 【Azure 架构师学习笔记】- Azure Function (1) --环境搭建和背景介绍
  • 前端文件下载
  • 前端成长之路:HTML(3)
  • 无人机自动机库的功能与作用!
  • ubuntu 新建脚本shell并增加图标 双击应用实现python运行
  • ANR 分析SOP
  • COLA学习之环境搭建(三)
  • CSS输入框动态伸缩动效
  • hbuilder 安卓app手机调试中基座如何设置
  • 探索视觉与语言模型的可扩展性
  • sock_recvmsg函数
  • HCIA笔记8--DHCP、Telnet协议
  • Scala的单例对象
  • 【笔记】分布式任务调度平台XXL-JOB
  • PDFMathTranslate,PDF多语言翻译,批量处理,学术论文,双语对照(WIN/MAC)
  • zerotier实现内网穿透(访问内网服务器)
  • Formality:set_svf命令
  • IDEA报错:无效的源发行版、无效的目标发行版
  • #渗透测试#红蓝对抗#SRC漏洞挖掘# Yakit插件使用及编写01
  • Scala中求斐波那契数列的第n项
  • ORACLE修改序列值为表内某字段(主键)最大值
  • 前端html,vue使用第三方地图详细教程,以百度地图为例,实现地图标注,导航,定位,路线规划,坐标转换
  • 【入门】加密四位数
  • [游戏开发] Unity中使用FlatBuffer
  • 云计算IaaS-PaaS-SaaS三种服务模式转至元数据结尾
  • 【数据结构——查找】二叉排序树(头歌实践教学平台习题)【合集】
  • 代码随想录第43天
  • LeetCode - #158 用 Read4 读取 N 个字符 II