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

【Linux | 网络】应用层

在这里插入图片描述

目录

  • 一、再谈“协议”
  • 二、序列化与反序列化
  • 三、网络计算器
    • 3.1 Socket.hpp(封装套接字)
    • 3.2 Protocol.hpp(自定义协议,有自定义序列化和反序列化方案和Json序列化和反序列化方案)
    • 3.3 Calculator.hpp(计算服务)
    • 3.4 Daemon.hpp(守护进程)
    • 3.5 Makefile
    • 3.6 TcpServer.hpp(服务端封装)
    • 3.7 TcpServerMain.cpp(服务端)
    • 3.8 TcpClient.cpp(客户端)
  • 四、DNS(域名解析系统)
    • 4.1 DNS背景
    • 4.2 域名的定义与结构
    • 4.3 域名解析过程
  • 结尾

一、再谈“协议”

我们在生活中与朋友聊天时,发送给朋友的有头像、昵称、时间、消息内容等等,这里我们就以昵称、时间和消息内容举例,我们给朋友发送的消息,需要通过服务器转发,假设我们的昵称、时间和消息内容是分开发送的,但是通过服务器转发的客户端并不只有我们一个人,这样可能会导致昵称、时间和消息内容随意组合,所以发送给服务器的信息通常是将昵称、时间和消息内容以特定形式组成的字节流(“字符串”),然后服务器将字节流转发给我们的朋友,这里我们假设朋友的机器读完了整个字节流,由于我们和朋友的机器都知道昵称、时间和消息内容的组成方式,它就可以从字节流中提取出昵称、时间和消息内容来。

昵称、时间和消息内容分别是三个字段,在应用层中我们可以将它们设计成一个结构化字段(Message),这个结构体字段是我们和朋友的机器都认识,只要我们将Message发送给朋友,他就可以知道其中的全部信息了。双方约定好的结构化字段在广义上来说就是协议

虽然我们在应用层中可以使用结构化字段来定义协议,但是结构化字段中包含了很多字段,由于平台差异,我们不方便直接将结构体字段在网络中发送,使用直接将结构体字段在网络中发送的方式在应用层中的可扩展性极差,所以我们需要将结构化字段转化为字节流(“字符串”)。

在这里插入图片描述


二、序列化与反序列化

我们上面讲述了将结构化数据转化为字节流,和从字节流中提取数据填充结构化数据。

序列化:将结构化字段转化为字节流。
反序列化:将字节流转化为结构化数据。

这里我们就有三个问题:

  1. 问:为什么要有序列化?
    答:将结构化字段转化为字节流,方便网络发送
  2. 问:为什么要用反序列化?
    答:从字节流中提取数据填充结构化数据,方便上层业务随时使用有效字段
  3. 问:为什么在应用层中我们不使用struct的方式,而是使用序列化的方式传递字节流?
    答:由于平台差异,我们不方便直接将结构体字段在网络中发送。

当用户使用read/send函数将数据发送给服务段时,实际上只是将数据拷贝到TCP中的发送缓冲区中,而发送缓冲区中的数据什么时候发?发多少?出错了怎么办?都是由TCP决定的。TCP实际通信的时候,其实是双方的操作系统进行通信,一方将数据从发送缓冲区通过网络发送给对方的接收缓冲区中,在接收缓冲区中的数据,用户可以通过write/recv函数读取上来。实际上来看read/send/write/recv函数就是拷贝函数。

一个TCP连接中就有两队发送缓冲区和接收缓冲区,也就是说双方都有一个发送缓冲区和接收缓冲区,所以一方在进行发送时,也可以接收,所以TCP是全双工通信。缓冲区中的数据有人写,也有人读,这很明显就是我们在系统部分学习到的生产者消费者模型了。

上面我们第一个标题的例子中假设了对方读完了,但是就目前而言,对方读没读完我们是不得而知的,因为用户认为自己发送了多少字节,内核不一定发送了这么多字节,对方也不一定读到了这么多字节,所以对方不知道是否已经信息读完。并且内核可能将多条信息一起发送,对方一下子可能读到后,他并不知道这些信息的边界,也就无法将这些信息进行分开,在下面的网络计算器中,我们会讲到使用自描述字段的方式来解决用户区分报文的边界问题,还有其他方式会在其他层中的协议中讲到。

在这里插入图片描述


三、网络计算器

3.1 Socket.hpp(封装套接字)

#pragma once#include <iostream>
#include <string>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <errno.h>#define CONV(addrptr) (struct sockaddr*)addrptrenum{Socket_err = 1,Bind_err,Listen_err
};const static int defalutsockfd = -1;
const int defalutbacklog = 5;class Socket
{
public:virtual ~Socket(){};virtual void CreateSocketOrDie() = 0;virtual void BindSocketOrDie(uint16_t port) = 0;virtual void ListenSocketOrDie(int backlog) = 0;virtual Socket* AcceptConnection(std::string* ip , uint16_t* port) = 0;virtual bool ConnectServer(const std::string& serverip , uint16_t serverport) = 0;virtual int GetSockFd() = 0;virtual void SetSockFd(int sockfd) = 0;virtual void CloseSockFd() = 0;virtual bool Recv(std::string& buffer,int size) = 0;virtual void Send(const std::string& send_string) = 0;public:void BuildListenSocketMethod(uint16_t port){CreateSocketOrDie();BindSocketOrDie(port);ListenSocketOrDie(defalutbacklog);}bool BuildConnectSocketMethod(const std::string& serverip , uint16_t serverport){CreateSocketOrDie();return ConnectServer(serverip,serverport);}void BuildNormalSocketMethod(int sockfd){SetSockFd(sockfd);}
};class TcpSocket : public Socket
{
public:TcpSocket(int sockfd = defalutsockfd):_sockfd(sockfd){}~TcpSocket(){};void CreateSocketOrDie() override{_sockfd = ::socket(AF_INET,SOCK_STREAM,0);if(_sockfd < 0) exit(Socket_err);}void BindSocketOrDie(uint16_t port) override{struct sockaddr_in addr;memset(&addr,0,sizeof(addr));addr.sin_family = AF_INET;addr.sin_addr.s_addr = INADDR_ANY;addr.sin_port = htons(port);socklen_t len = sizeof(addr);int n = ::bind(_sockfd,CONV(&addr),len);if(n < 0) exit(Bind_err);}void ListenSocketOrDie(int backlog) override{int n = ::listen(_sockfd,backlog);if(n < 0) exit(Listen_err);}Socket* AcceptConnection(std::string* clientip , uint16_t* clientport) override{struct sockaddr_in client;memset(&client,0,sizeof(client));socklen_t len = sizeof(client);int fd = ::accept(_sockfd,CONV(&client),&len);if(fd < 0) return nullptr;char buffer[64];inet_ntop(AF_INET,&client.sin_addr,buffer,len);*clientip = buffer;*clientport = ntohs(client.sin_port);Socket* s = new TcpSocket(fd);return s;}   bool ConnectServer(const std::string& serverip , uint16_t serverport) override{struct sockaddr_in server;memset(&server,0,sizeof(server));server.sin_family = AF_INET;// server.sin_addr.s_addr =  inet_addr(serverip.c_str());inet_pton(AF_INET,serverip.c_str(),&server.sin_addr);server.sin_port = htons(serverport);socklen_t len = sizeof(server);int n = connect(_sockfd,CONV(&server),len);if(n < 0) return false;else return true;}int GetSockFd() override{return _sockfd;}void SetSockFd(int sockfd) override{_sockfd = sockfd;}void CloseSockFd() override{if(_sockfd > defalutsockfd){close(_sockfd);}}bool Recv(std::string& buffer , int size)override{char inbuffer[size];int n = recv(_sockfd,inbuffer,sizeof(inbuffer)-1,0);if(n > 0){inbuffer[n] = 0;}else{return false;}buffer += inbuffer;return true;}void Send(const std::string& send_string){send(_sockfd,send_string.c_str(),send_string.size(),0);}private:int _sockfd;
};

3.2 Protocol.hpp(自定义协议,有自定义序列化和反序列化方案和Json序列化和反序列化方案)

// 安装Json库
sudo apt install libjsoncpp-dev  ubentu下安装
sudo yum install libjsoncpp-devel  centos下安装
#pragma once#include <memory>
#include <iostream>
#include <string>
#include <jsoncpp/json/json.h>const std::string ProtSep = " ";
const std::string LineBreakSep = "\n";// "len\nx op y\n"
// "len\nresult code\n"
// 添加自描述报头 len代表报文的长度,不包含后面的\n
// 解决用户区分报文边界问题
std::string EnCode(const std::string& info)
{std::string message = std::to_string(info.size()) + LineBreakSep + info + LineBreakSep;return message;
}// "l"
// "len"
// "len\n"
// "len\nx"
// "len\nx op "
// "len\nx op y"
// "len\nx op y\n"
// "len\nx op y\n""le"
// "len\nx op y\n""len\nx"
// "len\nx op y\n""len\nx op y\n"// "len\nresult code\n""len\nresult code\n"
// 取出报文
bool DeCode(std::string& message,std::string* info)
{// 读到lenauto pos = message.find(LineBreakSep);if(pos == std::string::npos) return false;std::string len = message.substr(0,pos);int messagelen = stoi(len);// 保证读到完整的报文int total = len.size() + messagelen + 2*LineBreakSep.size();if(message.size() < total) return false;*info = message.substr(pos + LineBreakSep.size());// 对已经读完的报文,再message中删除message.erase(0,total);return true;
}// 请求
class Request
{
public:Request(){}Request(int data_x ,int data_y, char op):_data_x(data_x),_data_y(data_y),_oper(op){}void Debug(){std::cout << _data_x << " " << _oper << " " << _data_y << std::endl;}void Test(){_data_x++;_data_y++;}// x op y// 序列化bool Serialize(std::string* out){#ifdef SelfDefine// 自己设计的反序列化方案*out = std::to_string(_data_x) + ProtSep + _oper + ProtSep + std::to_string(_data_y);return true;#else// 成熟的Json序列化方案Json::Value root;root["data_x"] = _data_x;root["data_y"] = _data_y;root["oper"] = _oper;Json::FastWriter writer;*out = writer.write(root);return true;#endif}// x op y// 反序列化bool Deserialize(const std::string& in){#ifdef SelfDefine// 自己设计的反序列化方案auto pos = in.find(ProtSep);if(pos == std::string::npos) return false;auto rpos = in.rfind(ProtSep);if(rpos == std::string::npos) return false;_data_x = stoi(in.substr(0,pos));_data_y = stoi(in.substr(rpos + ProtSep.size()));std::string op = in.substr(pos+ProtSep.size(),rpos - (pos+ProtSep.size()));if(op.size() != 1) return false;_oper = op[0];return true;#else// 成熟的Json反序列化方案Json::Value root;Json::Reader reader;reader.parse(in,root);_data_x = root["data_x"].asInt();_data_y = root["data_y"].asInt();_oper = root["oper"].asInt();return true;#endif}int GetX(){return _data_x;}int GetY(){return _data_y;}char GetOp(){return _oper;}
private:int _data_x; // 第一个参数int _data_y; // 第一个参数char _oper;  // 操作符
}; // 响应
class Response
{
public:Response():_result(0),_code(0){}Response(int result,int code):_result(result),_code(code){}// result code// 序列化bool Serialize(std::string* out){#ifdef SelfDefine// 自己设计的序列化方案*out = std::to_string(_result) + ProtSep + std::to_string(_code);return true;#else// 成熟的Json序列化方案Json::Value root;root["result"] = _result;root["code"] = _code;Json::FastWriter writer;*out = writer.write(root);return true;#endif}// result code// 反序列化bool Deserialize(const std::string& in){#ifdef SelfDefine// 自己设计的反序列化方案auto pos = in.find(ProtSep);if(pos == std::string::npos) return false;_result = stoi(in.substr(0,pos));_code = stoi(in.substr(pos + ProtSep.size()));return true;#else// 成熟的Json反序列化方案Json::Value root;Json::Reader reader;reader.parse(in,root);_result = root["result"].asInt();_code = root["code"].asInt();return true;#endif}void SetResult(int reslut){_result = reslut;}void SetCode(int code){_code = code;}int GetResult(){return _result;}int GetCode(){return _code;}private:int _result;  // 答案int _code;    // 答案是否有效
};// 工厂模式,建造类设计模式
class Factory
{
public:// 使用智能指针创建Request对象std::shared_ptr<Request> BuildRequest(){std::shared_ptr<Request> req = std::make_shared<Request>();return req;}std::shared_ptr<Request> BuildRequest(int data_x ,int data_y, char op){std::shared_ptr<Request> req = std::make_shared<Request>(data_x,data_y,op);return req;}// 使用智能指针创建Response对象std::shared_ptr<Response> BuildResponse(){std::shared_ptr<Response> resp = std::make_shared<Response>();return resp;}std::shared_ptr<Response> BuildResponse(int result,int code){std::shared_ptr<Response> resp = std::make_shared<Response>(result,code);return resp;}
};

3.3 Calculator.hpp(计算服务)

#pragma once#include <memory>
#include "Protocol.hpp"enum
{Success = 0,DivZeroErr,ModZeroErr,Unknown
};class Calculator
{
public:Calculator(){}std::shared_ptr<Response> Cal(std::shared_ptr<Request> req){std::shared_ptr<Response> resp = factory.BuildResponse();switch(req->GetOp()){case '+':{resp->SetResult(req->GetX()+req->GetY());break;}case '-':{resp->SetResult(req->GetX()-req->GetY());break;}case '*':{resp->SetResult(req->GetX()*req->GetY());break;}case '/':{if(req->GetY() == 0){resp->SetCode(DivZeroErr);}else{resp->SetResult(req->GetX()/req->GetY());}break;}case '%':{if(req->GetY() == 0){resp->SetCode(ModZeroErr);}else{resp->SetResult(req->GetX()%req->GetY());}break;}default:{resp->SetCode(Unknown);break;}}return resp;}~Calculator(){}
public:Factory factory;
};

3.4 Daemon.hpp(守护进程)

#pragma once#include <signal.h>
#include <unistd.h>
#include <stdlib.h> 
#include <sys/types.h>
#include <fcntl.h>const char* root = "/";
const char* dev_null = "/dev/null";bool Daemon(bool nochdir, bool noclose)
{// 1、忽略可能引起程序异常退出的信号 SIGCHLD SIGPIPEsignal(SIGCHLD,SIG_IGN);signal(SIGPIPE,SIG_IGN);// 2、创建子进程,让父进程退出,使得子进程不成为组长pid_t pid = fork();if(pid > 0) exit(0);// 3、设置自己成为一个新的会画,setsidsetsid();// 4、每一个进程都有自己的CWD(当前工作路径),是否将当前进程的CWD改为根目录// ​	改为根目录以后,进程可以以绝对路径的方式找到操作系统中的文件if(nochdir)chdir(root);// 5、变成守护进程以后,就不需要与用户的输入、输出和错误进行关联了// ​	可以将它们全部关闭,但难免服务器中会有输入、输出和错误// ​	向关闭的文件描述符中写入可能会导致进程退出// ​	所以这里将它们关闭不是最优解,而是将它们重定向到/dev/null中// ​	因为写入到/dev/null的数据会被直接丢弃,而从/dev/null读取信息,会默认读取到文件结尾if(noclose){int fd = open(dev_null,O_RDWR);if(fd > 0){dup2(fd,0);dup2(fd,1);dup2(fd,2);close(fd);}}else  // 不推荐{close(0);close(1);close(2);}return true;
}

3.5 Makefile

.PHONY:all
all:tcpserver tcpclient# -D 选项告诉编译器定义一个宏SelfDefine,宏的值是1
LDFLAG=#-DSelfDefine=1# $(LDFLAG) 变量引用 等同于 -DSelfDefine=1
tcpserver:TcpServerMain.cppg++ $^ -o $@ $(LDFLAG) -ljsoncpp -std=c++14 -lpthread
tcpclient:TcpClientMain.cppg++ $^ -o $@ $(LDFLAG) -ljsoncpp -std=c++14
.PHONY:clean
clean:rm -f tcpclient tcpserver

3.6 TcpServer.hpp(服务端封装)

#pragma once#include "Protocol.hpp"
#include "Socket.hpp"#include <string>
#include <functional>
#include <pthread.h>using func_t = std::function<std::string(std::string&,bool*)>;class TcpServer;class ThreadDate
{
public:ThreadDate(TcpServer* tser_this, Socket* socket):_this(tser_this),_socket(socket){}public:TcpServer* _this;Socket* _socket;
};class TcpServer
{
public:TcpServer(uint16_t port,func_t handler_request)    :_port(port),_listensock(new TcpSocket()),_handler_request(handler_request){_listensock->BuildListenSocketMethod(_port);}static void* HandlerRequest(void* arg){pthread_detach(pthread_self());ThreadDate* th = (ThreadDate*)arg;std::string inbufferstream;bool ok = true;while(1){// 1、读取报文if(!th->_socket->Recv(inbufferstream,1024)) break;// 2、调用函数处理报文std::string send_string = th->_this->_handler_request(inbufferstream,&ok);// 3、发送th->_socket->Send(send_string);}th->_socket->CloseSockFd();delete th->_socket;delete th;return nullptr;}void Loop(){while (1){std::string clientip;uint16_t clientport;// 接收连接会返回一个新的文件描述符Socket *NewSocket = _listensock->AcceptConnection(&clientip, &clientport);std::cout << "get a new sockfd , sockfd : " << NewSocket->GetSockFd() << " , clinet info " << clientip << ":" << clientport << std::endl;// 创建线程pthread_t pid;ThreadDate* td = new ThreadDate(this,NewSocket);pthread_create(&pid,nullptr,HandlerRequest,td);}_listensock->CloseSockFd();}~TcpServer(){}
private:TcpSocket* _listensock; // 监听套接字uint16_t _port;
public:func_t _handler_request; // 回调函数
};

3.7 TcpServerMain.cpp(服务端)

#include "Protocol.hpp"
#include "TcpServer.hpp"
#include "Calculator.hpp"
#include "Daemon.hpp"#include <iostream>
#include <unistd.h>
#include <string>
#include <string.h>
#include <memory>using namespace std;// 网络负责IO发送
// handler_request字节流数据解析和调用业务处理方法
string handler_request(string& inbufferstream,bool* ok)
{*ok = true;// 1. 构建响应对象和计算器对象Calculator calculator;unique_ptr<Factory> factory = make_unique<Factory>();std::shared_ptr<Request>req = factory->BuildRequest();std::string total_send_message;// 2、分析字节流,判断是否是一个完整的报文,同时处理多个报文std::string buffer;while(DeCode(inbufferstream,&buffer)){// 3、反序列化if(!req->Deserialize(buffer)){cout << "Deserialize fail" << endl;*ok = false;return string();}cout << "Deserialize success" << endl;req->Debug(); // 4、处理std::shared_ptr<Response>resp = calculator.Cal(req);// 5、对response进行序列化std::string send_message;resp->Serialize(&send_message);// 6、构建完成字符串级别的响应报文send_message = EnCode(send_message);total_send_message += send_message;}return total_send_message;
}void Usage(string proc)
{cout << proc << " serverport" << endl;
}int main(int argc , char* argv[])
{if(argc != 2){Usage(argv[0]);exit(1);}uint16_t serverport = stoi(argv[1]);Daemon(false,false);unique_ptr<TcpServer> up = make_unique<TcpServer>(serverport,handler_request);up->Loop();return 0;
}

3.8 TcpClient.cpp(客户端)

#include "Protocol.hpp"
#include "Socket.hpp"#include <iostream>
#include <unistd.h>
#include <string>
#include <string.h>
#include <stdlib.h>
#include <time.h>using namespace std;void Usage(string proc)
{cout << proc << " serverport serverport" << endl;
}int main(int argc , char* argv[])
{if(argc != 3){Usage(argv[0]);exit(1);}string serverip = argv[1];uint16_t serverport = stoi(argv[2]);// 建立连接Socket* conn = new TcpSocket();if(!conn->BuildConnectSocketMethod(serverip,serverport))delete conn;// 设置随机数种子srand(time(nullptr) ^ getpid());string op_str("+-*/%!#");unique_ptr<Factory> factory = make_unique<Factory>();string responsestr;while(1){// 1、构建请求int x = rand() % 200; // [0,199]usleep(rand()%500000);int y = rand() % 200; // [0,199]char op = op_str[rand()%op_str.size()];shared_ptr<Request> req = factory->BuildRequest(x,y,op);// 2、对请求进行序列化string requeststr;req->Serialize(&requeststr);string expression = requeststr + " = "; // 表达式// 3、添加自描述报头string send_request = EnCode(requeststr);// 4、发送请求conn->Send(send_request);// 5、接收响应while(conn->Recv(responsestr,1024)){// 6、对报文进行解析string message;if(!DeCode(responsestr,&message)) continue;// 7、反序列化shared_ptr<Response> resp = factory->BuildResponse();resp->Deserialize(message);// 8、得到结果cout << expression << resp->GetResult() << '[' << resp->GetCode() << ']' << endl;break;}}conn->CloseSockFd();delete conn;return 0;
}

四、DNS(域名解析系统)

4.1 DNS背景

TCP/IP中使用IP地址和端口号来确定网络上的一台主机的一个程序,但是IP地址不方便记忆。于是人们发明了一种叫主机名的东西,是一个字符串,并且使用hosts文件来描述主机名和IP地址的关系,但无法适应全球网络的动态扩展(HOSTS 需全网同步,效率极低)。

这样就太麻烦了,于是产生了DNS系统。一个组织的系统管理机构,维护系统内的每个主机的IP和主机名的对应关系。如果新计算机接入网络,将这个信息注册到数据库中。用户输入域名的时候,会自动查询DNS服务器,由DNS服务器检索数据库,得到对应的IP地址。

但是至今,我们的计算机上仍然保留了hosts文件,在域名解析的过程中仍然会优先查找hosts文件的内容。


4.2 域名的定义与结构

域名是由字符串组成的网络资源标识符,用于替代 IP 地址,符合人类语言习惯(如www.example.com)。

域名的层级结构(从右至左)

  • 顶级域名(TLD):最右端的部分,分为:
    • 通用顶级域名(gTLD):.com、.org、.net 等;
    • 国家 / 地区顶级域名(ccTLD):.cn(中国)、.jp(日本)、.uk(英国)等;
    • 新顶级域名(nTLD):.app、.xyz、.shop 等(2013 年后开放注册)。
  • 二级域名:顶级域名左侧部分,如example.com中的example(需注册获取)。
  • 子域名:二级域名左侧的层级,如www.example.com中的www(可自行定义,无需额外注册)。

4.3 域名解析过程

域名解析需通过 “递归查询” 与 “迭代查询” 结合的方式,分多层级完成,以下以查询www.example.com为例:

  1. 客户端发起查询:递归请求本地 DNS

    • 用户在浏览器输入域名后,操作系统先检查本地 HOSTS 文件,若存在映射则直接使用 IP
    • 若 HOSTS 无记录,则向本地 DNS 服务器发送递归查询请求,要求解析www.example.com
  2. 本地 DNS 的迭代查询:从根到权威的层级跳转

    1. 查询根域名服务器
      本地 DNS 服务器若无该域名缓存,会向全球 13 组根域名服务器发送查询,根服务器识别.com为顶级域名,返回.com顶级域名服务器的地址
    2. 查询顶级域名服务器
      本地 DNS 向.com顶级域名服务器发送查询,该服务器负责管理所有.com域名,返回example.com的权威域名服务器地址
    3. 查询权威域名服务器
      本地 DNS 向example.com的权威服务器发送查询,权威服务器存储该域名的解析记录(如 A 记录www.example.com --> 192.0.2.1),将 IP 地址返回给本地 DNS
  3. 结果返回与缓存

    • 本地 DNS 将解析得到的 IP 地址返回给客户端(浏览器),同时缓存该记录(缓存时间由域名的 TTL 值决定,如 86400 秒)
    • 客户端通过 IP 地址与目标服务器建立连接(如 HTTP 请求)

结尾

如果有什么建议和疑问,或是有什么错误,大家可以在评论区中提出。
希望大家以后也能和我一起进步!!🌹🌹
如果这篇文章对你有用的话,希望大家给一个三连支持一下!!🌹🌹

在这里插入图片描述

http://www.dtcms.com/a/276953.html

相关文章:

  • 003_了解Claude
  • 基于SpringBoot3集成Kafka集群
  • MongoDB性能优化实战指南:原理、实践与案例
  • 【设计模式】职责链模式(责任链模式) 行为型模式,纯与不纯的职责链模式
  • 前端框架状态管理对比:Redux、MobX、Vuex 等的优劣与选择
  • ALB、NLB、CLB 负载均衡深度剖析
  • 闲庭信步使用图像验证平台加速FPGA的开发:第十二课——图像增强的FPGA实现
  • axios拦截器
  • spring cloud负载均衡分析之FeignBlockingLoadBalancerClient、BlockingLoadBalancerClient
  • 【Qt开发】Qt的背景介绍(一)
  • 时序预测 | Matlab代码实现VMD-TCN-GRU-MATT变分模态分解时间卷积门控循环单元多头注意力多变量时序预测
  • [特殊字符] Python自动化办公 | 3步实现Excel数据清洗与可视化,效率提升300%
  • 开源链动2+1模式、AI智能名片与S2B2C商城小程序在私域运营中的协同创新研究
  • 从零开始跑通3DGS教程:(五)3DGS训练
  • 《区间dp》
  • 一文读懂现代卷积神经网络—深度卷积神经网络(AlexNet)
  • 深入理解观察者模式:构建松耦合的交互系统
  • Redis技术笔记-从三大缓存问题到高可用集群落地实战
  • ESP-Timer入门(基于ESP-IDF-5.4)
  • JVM:内存、类加载与垃圾回收
  • 每天一个前端小知识 Day 30 - 前端文件处理与浏览器存储机制实践
  • [Rust 基础课程]选一个合适的 Rust 编辑器
  • 通用定时器GPT
  • 输入npm install后发生了什么
  • # 通过wifi共享打印机只有手动翻页正反打印没有自动翻页正反打印,而通过网线连接的主机电脑可以自动翻页正反打印
  • OneCode3.0 VFS分布式文件管理API速查手册
  • Codeforces Round 855 (Div. 3)
  • 【iOS】方法与消息底层分析
  • 动物世界一语乾坤韵芳华 人工智能应用大学毕业论文 -仙界AI——仙盟创梦IDE
  • Docker Compose文件内容解释