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

Linux网络——自定义序列化与反序列化

前言

之前我们学习过socket之tcp通信,知道了使用tcp建立连接的一系列操作,并通过write与read函数能让客户端与服务端进行通信,但是tcp是面向字节流的,有可能我们write时只写入了部分数据,此时另一端就来read了,可能会导致读取的数据不完整的问题。这就引入到tcp通信的一个重要操作了——序列化与反序列化

一、使用结构体进行传输

在我们之前学习的代码中,一般想传递很多数据,都会使用到结构体。

比如pthread_create()需要传递函数地址,你如果需要传递很多参数,就得创建一个结构体,将他们组织起来,传递这个结构体指针给到pthread_create(),因为他第四个参数只能接受void*指针。不是可变参数包。

 如下,制作一个网络计算器,我们客户端创建结构体数据并发送

服务端对数据进行接受

这样服务器就可以接受到数据并做相应的处理了,这里我们显示出来表示接收到了数据,处理起来也就很简单了。

结构体代码链接

二、自定义序列化与反序列化

tcp中的数据使用结构体传输在网络压力很大,或者传输内容很多时,就会发生问题。同样在不同的编译环境下,结构体内存对齐也不一致,并且有可能客户端是使用其他代码进行编写,C/C++的结构体也不会适合。

  • 如果我们将传输的数据统一处理成字符串,让字符串在tcp中进行传输,同时设置一些固定的字段,代表当前数据包的结束,是不是能解决上述问题呢?
  • 这就是数据序列化后进行传输,接收方将数据进行反序列化后提取再做处理序列化的本质就是对字符串作处理

一样的计算器,一样的需要传递两个整形数字与一个字符。

  • 如果我们将该结构体转为字符串,如 "x oper y\n" ,这样我们就可以通过读取空格来表示取到的整形,字符,整形,如果一直没读到"\n"证明当前数据包还没读完,读到"\n"证明取到了一个完整的数据包,这样就可以完美解决问题了。
  • 但是我们这个写法没有普适性,因为如果后面如果结构体内容比较复杂,带了一个"\n",就会导致序列化代码需要重新设计。如果在前面添加一个报头len,代表后面内容的长度,这样无论后面代码是什么,都可以统一看待处理。
  • 于是,我们得到如下的序列化公式。

"len\nx op y"

这里len后面的"\n"代表我们读取到了len数据,后面就是真正要传输并处理的数据了。

了解了这些,那么我们进行网络计算器的序列化与反序列化也就不难了,代码如下

Protocol.hpp  自定义协议

#pragma once#include <iostream>
#include <memory>
using namespace std;namespace Protocol
{const string ProtSep = " ";const string LineBreakSep = "\n";// message 现在我要给他头部添加len\n 尾部添加\n// 如"x op y"-> "len\nx op y\n"   "result code"-> "len\nresult code\n"string Encode(string &message){string len = to_string(message.size());string package = len + LineBreakSep + message + LineBreakSep;return package;}// 从一个完整的package进行解析,"len\nx op y\n"->"x op y"bool Decode(string &package, string *message){int left = package.find(LineBreakSep);if (left == string::npos)return false;int len = stoi(package.substr(0, left));int total = left + len + 2 * LineBreakSep.size();if (package.size() < total)return false;// 到这里至少有一个完整的报文 截取出来一个报文写入message,然后删除该报文*message = package.substr(left + LineBreakSep.size(), len);package.erase(0, total);return true;}class Request{public:Request() : _data_x(0), _data_y(0), _oper(0){}Request(int x, int y, char op): _data_x(x), _data_y(y), _oper(op){}void Debug(){cout << "_data_x: " << _data_x << endl;cout << "_data_y: " << _data_y << endl;cout << "_oper: " << _oper << endl;}void Inc(){_data_x++;_data_y++;}// 序列化  变成这种字符串"x op y"void Serialize(string *out){*out = to_string(_data_x) + ProtSep + _oper + ProtSep + to_string(_data_y);}// 反序列化  "x op y"字符串分别取出bool Deserialize(string &in){size_t left = in.find(ProtSep);if (left == string::npos)return false;size_t right = in.rfind(ProtSep);if (right == string::npos)return false;_data_x = stoi(in.substr(0, left));_data_y = stoi(in.substr(right + ProtSep.size()));string op = in.substr(left + ProtSep.size(), right - left - ProtSep.size());if (op.size() != 1)return false;_oper = op[0];return true;}int Getx(){return _data_x;}int Gety(){return _data_y;}char Getop(){return _oper;}string To_string(){return to_string(_data_x)+" "+_oper+ " "+to_string(_data_y)+" = ";}private:// x oper y 比如 10 + 20int _data_x;int _data_y;char _oper; // 符号};class Response{public:Response() : _result(0), _code(0){}Response(int result, int code): _result(result), _code(code){}void Serialize(string *out){//_result _code*out = to_string(_result) + ProtSep + to_string(_code);}bool Deserialize(string &in){size_t pos = in.find(ProtSep);if (pos == string::npos)return false;_result = stoi(in.substr(0, pos));_code = stoi(in.substr(pos + ProtSep.size()));return true;}int Getresult(){return _result;}int Getcode(){return _code;}void Setresult(int result){_result = result;}void Setcode(int code){_code = code;}private:int _result; // 运算结果int _code;   // 运算状态};// 工厂模式class Factory{public:shared_ptr<Request> BuildRequest(){return make_shared<Request>();}shared_ptr<Request> BuildRequest(int x, int y, char op){return make_shared<Request>(x, y, op);}shared_ptr<Response> BuildResponse(){return make_shared<Response>();}shared_ptr<Response> BuildResponse(int result, int code){return make_shared<Response>(result, code);}};
}

 Socket.hpp 套接字函数封装

#pragma once#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cstring>
#include <unistd.h>
using namespace std;
namespace Net_Work
{static const int default_backlog = 5;static const int default_sockfd = -1;using namespace std;enum{SocketError = 1,BindError,ListenError,ConnectError,};// 封装套接字接口基类class Socket{public:// 封装了socket相关方法virtual ~Socket() {}virtual void CreateSocket() = 0;virtual void BindSocket(uint16_t port) = 0;virtual void ListenSocket(int backlog) = 0;virtual bool ConnectSocket(string &serverip, uint16_t serverport) = 0;virtual Socket *AcceptSocket(string *peerip, uint16_t *peerport) = 0;virtual int GetSockFd() = 0;virtual void SetSockFd(int sockfd) = 0;virtual void CloseSocket() = 0;virtual bool Recv(string *buff, int size) = 0;virtual void Send(string &send_string) = 0;// 方法的集中在一起使用public:void BuildListenSocket(uint16_t port, int backlog = default_backlog){CreateSocket();BindSocket(port);ListenSocket(backlog);}bool BuildConnectSocket(string &serverip, uint16_t serverport){CreateSocket();return ConnectSocket(serverip, serverport);}void BuildNormalSocket(int sockfd){SetSockFd(sockfd);}};class TcpSocket : public Socket{public:TcpSocket(int sockfd = default_sockfd): _sockfd(sockfd){}~TcpSocket() {}void CreateSocket() override{_sockfd = socket(AF_INET, SOCK_STREAM, 0);if (_sockfd < 0)exit(SocketError);}void BindSocket(uint16_t port) override{int opt = 1;setsockopt(_sockfd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt));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 n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local));if (n < 0)exit(BindError);}void ListenSocket(int backlog) override{int n = listen(_sockfd, backlog);if (n < 0)exit(ListenError);}bool ConnectSocket(string &serverip, uint16_t serverport) override{struct sockaddr_in addr;memset(&addr, 0, sizeof(addr));addr.sin_family = AF_INET;addr.sin_port = htons(serverport);// addr.sin_addr.s_addr = inet_addr(serverip.c_str());inet_pton(AF_INET, serverip.c_str(), &addr.sin_addr);int n = connect(_sockfd, (sockaddr *)&addr, sizeof(addr));if (n == 0)return true;return false;}Socket *AcceptSocket(string *peerip, uint16_t *peerport) override{struct sockaddr_in addr;socklen_t len = sizeof(addr);int newsockfd = accept(_sockfd, (sockaddr *)&addr, &len);if (newsockfd < 0)return nullptr;// *peerip = inet_ntoa(addr.sin_addr);// INET_ADDRSTRLEN 是一个定义在头文件中的宏,表示 IPv4 地址的最大长度char ip_str[INET_ADDRSTRLEN];inet_ntop(AF_INET, &addr.sin_addr, ip_str, INET_ADDRSTRLEN);*peerip = ip_str;*peerport = ntohs(addr.sin_port);Socket *s = new TcpSocket(newsockfd);return s;}int GetSockFd() override{return _sockfd;}void SetSockFd(int sockfd) override{_sockfd = sockfd;}void CloseSocket() override{if (_sockfd > default_sockfd)close(_sockfd);}bool Recv(string *buff, int size) override{char inbuffer[size];ssize_t n = recv(_sockfd, inbuffer, size - 1, 0);if (n > 0){inbuffer[n] = 0;*buff += inbuffer;return true;}elsereturn false;}void Send(string &send_string) override{send(_sockfd, send_string.c_str(),send_string.size(),0);}private:int _sockfd;string _ip;uint16_t _port;};
}

Calculate.hpp  计算业务封装

#pragma once#include <iostream>
#include <memory>
#include "Protocol.hpp"enum ErrCode
{Sucess = 0,DivZeroErr,ModZeroErr,UnKnowOper,
};//计算业务
class Calculate
{
public:Calculate(){}shared_ptr<Protocol::Response> Cal(shared_ptr<Protocol::Request> req){shared_ptr<Protocol::Response> resp = factory.BuildResponse();resp->Setcode(Sucess);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);elseresp->Setresult(req->Getx() / req->Gety());}break;case '%':{if (req->Gety() == 0)resp->Setcode(ModZeroErr);elseresp->Setresult(req->Getx() % req->Gety());}break;default:resp->Setcode(UnKnowOper);break;}return resp;}private:Protocol::Factory factory;
};

 TcpServer.hpp  tcp服务端封装

#pragma once#include "Protocol.hpp"
#include "Socket.hpp"
#include <thread>
#include <functional>using func_t = function<string(string &inbufferstream, bool *error_code)>;class TcpServer
{
public:TcpServer(uint16_t port, func_t hander_request): _port(port), _listen_socket(new Net_Work::TcpSocket()), _handler_request(hander_request){_listen_socket->BuildListenSocket(port);}void ThreadRun(Net_Work::Socket *s){// cout<< " in HandlerRequest !"<<endl;string inbufferstream;while (true){bool resultcode = true;// 1.读取报文if (!s->Recv(&inbufferstream, 1024))break;// 2.报文处理string send_string = _handler_request(inbufferstream, &resultcode);if (resultcode){// 发送数据if (!send_string.empty()){s->Send(send_string);}}else{break;}}s->CloseSocket();delete s;}void Loop(){while (true){string peerip;uint16_t peerport;Net_Work::Socket *newsockfd = _listen_socket->AcceptSocket(&peerip, &peerport);if (newsockfd == nullptr)continue;cout << "获取一个新链接, sockfd: " << newsockfd->GetSockFd() << " ,client info: " << peerip << ":" << peerport << endl;thread t1(&TcpServer::ThreadRun, this, newsockfd);t1.detach();}}~TcpServer(){delete _listen_socket;}private:int _port;Net_Work::Socket *_listen_socket;func_t _handler_request;
};

Main.cc   服务端实现

#include "TcpServer.hpp"
#include "Calculate.hpp"
#include <unistd.h>
#include <memory>string HandlerRequest(string &inbufferstream, bool *error_code)
{Calculate calculate;// 1.创建请求对象unique_ptr<Protocol::Factory> factory(new Protocol::Factory());shared_ptr<Protocol::Request> req = factory->BuildRequest();string total_resp_string;// 2.判断字节流是否读取到了一个完整的报文  把报文读完string message;while (Protocol::Decode(inbufferstream, &message)){// 3.这里已经读到完整的报文,需要进行反序列化  反序列化失败证明是非法请求if (!req->Deserialize(message)){// 反序列化失败  不能忍受,因为客户端的请求是不合法的*error_code = false;return string();}// 4.业务处理shared_ptr<Protocol::Response> resp = calculate.Cal(req);// 5.序列化string send_string;resp->Serialize(&send_string);// 6.添加报头send_string = Protocol::Encode(send_string);total_resp_string+=send_string;}return total_resp_string;
}int main(int argc, char *argv[])
{if (argc != 2){cout << "Usage : " << argv[0] << " port" << endl;return 0;}uint16_t localport = stoi(argv[1]);unique_ptr<TcpServer> tsvr(new TcpServer(localport, HandlerRequest));tsvr->Loop();return 0;
}

TcpClient.cc 服务端实现 

#include <iostream>
#include <string>
#include <unistd.h>
#include <ctime>
#include <cstdlib>
#include "Socket.hpp"
#include "Protocol.hpp"using namespace std;
int main(int argc, char *argv[])
{if (argc != 3){cout << "Usage : " << argv[0] << " serverip serverport" << endl;return 0;}string serverip = argv[1];uint16_t serverport = stoi(argv[2]);// 创建套接字对象Net_Work::Socket *s = new Net_Work::TcpSocket();if (!s->BuildConnectSocket(serverip, serverport)){cerr << "connect [" << serverip << ":" << serverport << "] failed" << endl;return Net_Work::ConnectError;}cout << "connect [" << serverip << ":" << serverport << "] success" << endl;// 创建工厂unique_ptr<Protocol::Factory> factory(new Protocol::Factory());srand(time(nullptr) ^ getpid());const string opers = "+-*/%^&=";while (true){// 1.构建一个请求int x = rand() % 100;usleep(rand() % 5555);int y = rand() % 100;int oper = opers[rand() % opers.size()];shared_ptr<Protocol::Request> req = factory->BuildRequest(x, y, oper);// 2.序列化该请求string requeststr;req->Serialize(&requeststr);// 3.添加报头信息requeststr = Protocol::Encode(requeststr);// 4.发送请求s->Send(requeststr);string message;while (true){// 5.读取响应string responsestr;s->Recv(&responsestr, 1024);// 6.解析报文  收到的"len\n result code"  现在开始解析if(!Protocol::Decode(responsestr,&message))continue;// 7.拆分数据 目前是"result code"  现在开始写入结构体shared_ptr<Protocol::Response> resp = factory->BuildResponse();resp->Deserialize(message);// 8.resp已经得到的数据cout<<req->To_string()<<resp->Getresult()<<"\tcode: "<<resp->Getcode()<<endl;break;}sleep(1);}s->CloseSocket();return 0;
}

 Makefile

.PHONY:all
all:tcpserver tcpclienttcpserver:Main.ccg++ -o $@ $^ -std=c++11 -lpthread
tcpclient:TcpClient.ccg++ -o $@ $^ -std=c++11 -lpthread.PHONY:clean
clean:rm -f tcpclient tcpserver

代码链接

运行结果吐下

相关文章:

Linux网络——自定义序列化与反序列化

前言 之前我们学习过socket之tcp通信&#xff0c;知道了使用tcp建立连接的一系列操作&#xff0c;并通过write与read函数能让客户端与服务端进行通信&#xff0c;但是tcp是面向字节流的&#xff0c;有可能我们write时只写入了部分数据&#xff0c;此时另一端就来read了&#x…...

大模型介绍

大模型通常指的是参数量超过亿级别&#xff0c;甚至千亿级别的深度学习模型。这类模型能够处理更加复杂的任务&#xff0c;并在各项基准测试中取得了优异的成绩。大模型在自然语言处理、计算机视觉、推荐系统等领域都取得了显著的成果。 大模型的主要优势在于其强大的表征能力&…...

【思维】根号分治

写在前面的话&#xff1a; 个人理解 根号分治本身就是一种卡着评测机过题的做法&#xff0c;所以非必要不要写 #define int long long &#xff01;&#xff01;&#xff01; 本篇博客参考&#xff1a;暴力美学——浅谈根号分治 做到过两三题根号分治了&#xff0c;来总结一下…...

Linux线程(三)死锁与线程同步

目录 一、什么是死锁 死锁的四个必要条件 如何避免死锁 避免死锁算法 二、Linux线程同步 三 、条件变量 1、条件变量基本原理 2、条件变量的使用 3、条件变量使用示例 为什么 pthread_cond_wait 需要互斥量? 一、什么是死锁 死锁是计算机科学中的一个概念&#xff0c;…...

SpringAMQP 发布订阅-TopicExchange

根据这个模型编写代码: RabbitListener(bindings QueueBinding(value Queue(name "topic.queue1"),exchange Exchange(name "itcast.topic",type ExchangeTypes.TOPIC),key {"china.#"}))public void listenTopicQueue1(String msg){Syst…...

uniapp h5 配置代理服务器

"devServer": {"disableHostCheck": true,"proxy": {"/api": {// 需要被代理的后台地址"target": "http://自己的地址","changeOrigin": true,"secure": false,"pathRewrite": {&q…...

使用Apache Spark从MySQL到Kafka再到HDFS的数据转移

使用Apache Spark从MySQL到Kafka再到HDFS的数据转移 在本文中&#xff0c;将介绍如何构建一个实时数据pipeline&#xff0c;从MySQL数据库读取数据&#xff0c;通过Kafka传输数据&#xff0c;最终将数据存储到HDFS中。我们将使用Apache Spark的结构化流处理和流处理功能&#…...

一篇文章拿下Redis 通用命令

文章目录 Redis数据结构介绍Redis 通用命令命令演示KEYSDELEXISTSEXPIRE RedisTemplate 中的通用命令 本篇文章介绍 Redis 的通用命令, 通用命令在 Redis 的所有数据类型下都使用, 学好通用命令可以让我们更好的使用 Redis. Redis数据结构介绍 Redis 是一个key-value的数据库&…...

锂电池充电充放电曲线分析

前言 锂电池的充电曲线通常包括三个阶段:恒流充电阶段、恒压充电阶段和滞后充电阶段。在恒流充电阶段,电流保持恒定,电压逐渐增加;在恒压充电阶段,电压保持恒定,电流逐渐减小;在滞后充电阶段,电流进一步减小,电池开始充满。通过监测这些阶段的电流和电压变化,可以评…...

vue3 第二十九节 (vue3 事件循环之nextTick)

引言 vue 项目中为什么要使用 nextTick 这个函数&#xff0c;是做什么用的&#xff0c;解决了哪些问题 1、nextTick 作用 用于处理DOM更新完成之后&#xff0c;执行回调函数的方法&#xff1b; 2、实现方案 vue2 中 nextTick() 是基于浏览器的 异步队列和微任务队列而执行…...

使用Flask-SocketIO构建实时Web应用

文章目录 准备工作编写代码编写HTML模板运行应用 随着互联网的发展&#xff0c;实时性成为了许多Web应用的重要需求之一。传统的HTTP协议虽然可以实现实时通信&#xff0c;但是其长轮询等机制效率低下&#xff0c;无法满足高并发、低延迟的需求。为了解决这一问题&#xff0c;诞…...

可重构柔性装配产线:为工业制造领域注入了新的活力

随着科技的飞速发展&#xff0c;智能制造正逐渐成为引领工业革新的重要力量。在这一浪潮中&#xff0c;可重构柔性装配产线以其独特的技术优势和创新理念&#xff0c;为工业制造领域注入了新的活力&#xff0c;开启了创新驱动的智能制造新篇章。 可重构柔性装配产线是基于富唯智…...

懒人网址导航源码v3.9

测试环境 宝塔Nginx -Tengine2.2.3的PHP5.6 MySQL5.6.44 为防止调试错误&#xff0c;建议使用测试环境运行的php与mysql版本 首先用phpMyAdmin导入数据库文件db/db.sql 如果导入不行&#xff0c;请直接复制数据库内容运行sql语句也可以 再修改config.php来进行数据库配置…...

springboot 开启缓存 @EnableCaching(使用redis)

添加依赖 pom.xml <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId> </dependency>application.yml 配置redis连参数 spring:# redis 配置redis:# 地址host: 127.0.0.…...

Adobe After Effects AE v24.3.0 解锁版 (视频合成及视频特效制作)

Adobe系列软件安装目录 一、Adobe Photoshop PS 25.6.0 解锁版 (最流行的图像设计软件) 二、Adobe Media Encoder ME v24.3.0 解锁版 (视频和音频编码渲染工具) 三、Adobe Premiere Pro v24.3.0 解锁版 (领先的视频编辑软件) 四、Adobe After Effects AE v24.3.0 解锁版 (视…...

Qt---文件系统

一、基本文件操作 1. QFile对文件进行读和写 QFile file( path 文件路径) 读&#xff1a; file.open(打开方式) QlODevice::readOnly 全部读取->file.readAll()&#xff0c;按行读->file.readLine()&#xff0c;atend()->判断是否读到文件尾 …...

ruoyi-vue-pro 使用记录(2)

ruoyi-vue-pro 使用记录&#xff08;2&#xff09; 数据库商城商品模块数据表营销数据库交易数据库统计数据库 数据库 商城 参考官方文档 ruoyi-vue-pro yudao 项目商城 mall 模块启用及相关SQL脚本 商品模块&#xff08;中心&#xff09;以 product_ 作为前缀的表交易模块…...

centos7中如何全局搜索一下nginx的配置文件?

在CentOS 7中搜索Nginx的配置文件&#xff0c;你可以使用一些常用的命令行工具&#xff0c;比如find、grep等。这些工具可以帮助你在文件系统中查找文件&#xff0c;也可以用来查找Docker容器内部的文件&#xff0c;只要你知道如何访问容器的文件系统。 1. 搜索系统中的Nginx配…...

2024年5月10日有感复盘

2024年5月10日有感复盘 时间 今天是一个很美好的一天&#xff0c;原因是很平凡&#xff0c;读书很平凡&#xff0c;玩游戏很平凡&#xff0c;然后生活很平凡&#xff0c;未来可期&#xff0c;听歌很舒服&#xff0c;很喜欢一个人呆在图书馆的感觉&#xff0c;很喜欢发呆&…...

C++通过json文件配置参数

一、安装nlohmann json nlohmann json&#xff1a;安装_nlohmann安装-CSDN博客 依次执行下面指令&#xff1a; git clone https://gitee.com/cuihongxi/mov_from_github.gitcd json-developmkdir buildcd buildcmake ..makesudo make install 二、安装完成后使用 #include…...

[特殊字符] 第85课:戳气球

想系统提升编程能力、查看更完整的学习路线&#xff0c;欢迎访问 AI Compass&#xff1a;https://github.com/tingaicompass/AI-Compass 仓库持续更新刷题题解、Python 基础和 AI 实战内容&#xff0c;适合想高效进阶的你。&#x1f4d6; 第85课:戳气球模块:动态规划 | 难度:Ha…...

如何用HTML转Figma工具打破设计与开发之间的隔阂

如何用HTML转Figma工具打破设计与开发之间的隔阂 【免费下载链接】figma-html Convert any website to editable Figma designs 项目地址: https://gitcode.com/gh_mirrors/fi/figma-html 你是否曾经遇到过这样的情况&#xff1a;看到一个设计精美的网站&#xff0c;想要…...

Qwen3-TTS-12Hz-1.7B-Base快速部署:基于Jupyter+Gradio的极简开发环境搭建

Qwen3-TTS-12Hz-1.7B-Base快速部署&#xff1a;基于JupyterGradio的极简开发环境搭建 本文介绍如何在JupyterGradio环境中快速部署Qwen3-TTS-12Hz-1.7B-Base语音合成模型&#xff0c;无需复杂配置&#xff0c;10分钟即可实现声音克隆和语音生成功能。 1. 环境准备与快速部署 1…...

Go语言的context.WithCancel时机正确

...

DroidCam手机变电脑摄像头工具

DroidCam 这款免费工具&#xff0c;能让你的安卓或iPhone瞬间变成电脑的无线/USB摄像头。无论是开Zoom会议、上网课还是直播&#xff0c;画质直接碾压普通电脑摄像头。优点很明显&#xff1a;零成本&#xff1a;利用闲置旧手机&#xff0c;省下买新摄像头的钱。画质好&#xff…...

基于STM32的校园一卡通系统设计与实现

1. 项目概述1.1 项目开发背景作为一名嵌入式系统开发者&#xff0c;我最近完成了一个基于STM32的校园一卡通系统项目。这个项目的灵感来源于我在大学期间亲身经历的多卡困扰——每天要带着学生证、饭卡、图书证等一堆卡片&#xff0c;不仅容易丢失&#xff0c;使用起来也很不方…...

Phi-3-mini-4k-instruct-gguf GPU算力优化:q4 GGUF模型在消费级显卡上的表现

Phi-3-mini-4k-instruct-gguf GPU算力优化&#xff1a;q4 GGUF模型在消费级显卡上的表现 1. 模型概述 Phi-3-mini-4k-instruct-gguf 是微软Phi-3系列中的轻量级文本生成模型GGUF版本&#xff0c;专为问答、文本改写、摘要整理和简短创作等场景优化。这个经过量化的q4 GGUF模型…...

mootdx完全指南:金融数据获取与分析的7个实战技巧

mootdx完全指南&#xff1a;金融数据获取与分析的7个实战技巧 【免费下载链接】mootdx 通达信数据读取的一个简便使用封装 项目地址: https://gitcode.com/GitHub_Trending/mo/mootdx 副标题&#xff1a;量化交易 | 数据接口 | Python工具 你是否曾在量化交易策略开发中…...

CefFlashBrowser终极指南:5个步骤让Flash内容在现代系统重生

CefFlashBrowser终极指南&#xff1a;5个步骤让Flash内容在现代系统重生 【免费下载链接】CefFlashBrowser Flash浏览器 / Flash Browser 项目地址: https://gitcode.com/gh_mirrors/ce/CefFlashBrowser 当Adobe在2020年正式终止Flash Player支持时&#xff0c;无数经典…...

Dunst多显示器支持终极指南:在不同屏幕间智能分配通知

Dunst多显示器支持终极指南&#xff1a;在不同屏幕间智能分配通知 【免费下载链接】dunst Lightweight and customizable notification daemon 项目地址: https://gitcode.com/gh_mirrors/du/dunst Dunst是一款轻量级且高度可定制的通知守护进程&#xff08;notificatio…...