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

【Linux】应用层自定义协议 + 序列化和反序列化

应用层自定义协议 + 序列化和反序列化

  • 一.应用层
    • 1.再谈 "协议"
    • 2.序列化 和 反序列化
  • 二. Jsoncpp
    • 1.序列化
    • 2.反序列化
  • 三. Tcp全双工 + 面向字节流
  • 四.自定义协议 + 保证报文的完整性
    • 1.Makefile
    • 2.Mutex.hpp
    • 3.Cond.hpp
    • 4.Log.hpp
    • 5.Thread.hpp
    • 6.ThreadPool.hpp
    • 7.Common.hpp
    • 8.InetAddr.hpp
    • 9.Protocol.hpp
    • 10.Calculator.hpp
    • 11.Deamon.hpp
    • 12.TcpServer.hpp
    • 13.TcpServer.cc
    • 14.TcpClient.cc
    • 15.运行操作

一.应用层

我们程序员写的一个个解决我们实际问题,满足我们日常需求的网络程序,都是在应用层。

1.再谈 “协议”

  • 协议是一种 “约定”。Socket API 的接口,在读写数据时,都是按 “字符串” 的方式来发送接收的。如果我们要传输一些 “结构化的数据” 怎么办呢?
  • 其实,协议就是双方约定好的结构化的数据。

2.序列化 和 反序列化

问题:如何传输结构化数据?

  • 序列化:是指将结构化信息转换为字符串的过程。简单来说,就是把内存中的对象变成一种可以保存到文件或者在网络上传输的格式。
  • 反序列化:是序列化的逆过程,即把序列化后的字符串恢复为结构化信息。当接收方收到序列化的数据后,通过反序列化操作将其还原为原始的对象,以便在程序中继续使用。
  • 网络传输:由于网络传输的是字节流,因此需要将对象序列化为字节序列进行传输,接收方再将其反序列化得到原始对象。

在这里插入图片描述

二. Jsoncpp

  • Jsoncpp 是一个用于处理 JSON 数据的 C++ 库。它提供了将 JSON 数据序列化为字符串以及从字符串反序列化为 C++ 数据结构的功能。Jsoncpp 是开源的,广泛用于各种需要处理 JSON 数据的 C++ 项目中。

特性:

  1. 简单易用:Jsoncpp 提供了直观的 API,使得处理 JSON 数据变得简单。
  2. 高性能:Jsoncpp 的性能经过优化,能够高效地处理大量 JSON 数据。
  3. 全面支持:支持 JSON 标准中的所有数据类型,包括对象、数组、字符串、数字、布尔值和 null。
  4. 错误处理:在解析 JSON 数据时,Jsoncpp 提供了详细的错误信息和位置,方便开发者调试。

当使用 Jsoncpp 库进行 JSON 的序列化和反序列化时,确实存在不同的做法和工具类可供选择。以下是对 Jsoncpp 中序列化和反序列化操作的详细介绍:

安装:

# C++
Ubuntu: sudo apt-get install libjsoncpp-dev
Centos: sudo yum install jsoncpp-devel

1.序列化

序列化指的是将数据结构或对象转换为一种格式,以便在网络上传输或存储到文件中。

#include <iostream>
#include <string>
#include <sstream>
#include <memory>
#include <jsoncpp/json/json.h>int main()
{Json::Value root;root["name"] = "xzy";root["sex"] = "男";// 创建一个 Json::StreamWriterBuilder 对象Json::StreamWriterBuilder wb;// 设置禁止 Unicode 转义wb.settings_["emitUTF8"] = true;// 创建一个 Json::StreamWriter 对象std::unique_ptr<Json::StreamWriter> w(wb.newStreamWriter());// 将 JSON 数据写入流对象 ss 中std::stringstream ss;w->write(root, &ss);std::string str =  ss.str();std::cout << str << std::endl;std::cout << str.size() << std::endl;return 0;
}
xzy@hcss-ecs-b3aa:~$ g++ test.cc -ljsoncpp
xzy@hcss-ecs-b3aa:~$ ./a.out 
{"name" : "xzy","sex" : "男"
}
35

2.反序列化

反序列化指的是将序列化后的数据重新转换为原来的数据结构或对象。

#include <iostream>
#include <string>
#include <jsoncpp/json/json.h>int main()
{// JSON 字符串std::string json_string = "{\"name\":\"张三\",\"age\":30, \"city\":\"北京\"}";// {"name":"张三","age":30, "city":"北京"}// 解析 JSON 字符串Json::Reader reader;Json::Value root;// 从字符串中读取 JSON 数据bool parsingSuccessful = reader.parse(json_string, root);if (!parsingSuccessful){// 解析失败,输出错误信息std::cout << "Failed to parse JSON: " << reader.getFormattedErrorMessages() << std::endl;return 1;}// 访问 JSON 数据std::string name = root["name"].asString();int age = root["age"].asInt();std::string city = root["city"].asString();// 输出结果std::cout << "Name: " << name << std::endl;std::cout << "Age: " << age << std::endl;std::cout << "City: " << city << std::endl;return 0;
}
xzy@hcss-ecs-b3aa:~$ ./a.out 
Name: 张三
Age: 30
City: 北京

三. Tcp全双工 + 面向字节流

在这里插入图片描述

  • write本质是将用户缓冲区buffer数据拷贝到发送缓冲区中、read本质是将接受缓冲区数据拷贝到用户缓冲区buffer中。
  • TCP全双工:在任何一台主机上,TCP 连接既有发送缓冲区,又有接受缓冲区,所以在内核中,可以在发消息的同时,也可以收消息。这就是为什么一个 TCP sockfd 读写都是它的原因!
  • TCP传输控制协议:实际数据什么时候发,发多少,出错了怎么办,由 TCP 控制。
  • TCP面向字节流:发送/接收的报文是不完整的,需要应用层保证报文的完整性!
  • UDP面向数据报:发送/接收的报文是完整的。

在网络通信的过程中,操作系统内部可能存在大量的报文,操作系统需要管理报文:先描述再组织!下面是部分内核代码:

在这里插入图片描述

四.自定义协议 + 保证报文的完整性

期望的报文格式:

在这里插入图片描述

  • 其中有效载荷的内容我们用 JSON 数据!
  • 如何保证获取完整的请求报文,需要我们处理!
  • 如何将服务器脱离终端,用户退出登入/注销时,服务器不受影响?将服务器设计为守护进程!

1.Makefile

.PHONY:all
all:server_tcp client_tcpclient_tcp:TcpClient.ccg++ -o $@ $^ -std=c++17 -ljsoncpp
server_tcp:TcpServer.ccg++ -o $@ $^ -std=c++17 -lpthread -ljsoncpp.PHONY:clean
clean:rm -f client_tcp server_tcp

2.Mutex.hpp

#pragma once#include <pthread.h>namespace MutexModule
{class Mutex{Mutex(const Mutex &m) = delete;const Mutex &operator=(const Mutex &m) = delete;public:Mutex(){::pthread_mutex_init(&_mutex, nullptr);}~Mutex(){::pthread_mutex_destroy(&_mutex);}void Lock(){::pthread_mutex_lock(&_mutex);}void Unlock(){::pthread_mutex_unlock(&_mutex);}pthread_mutex_t *LockAddr() { return &_mutex; }private:pthread_mutex_t _mutex;};class LockGuard{public:LockGuard(Mutex &mutex): _mutex(mutex){_mutex.Lock();}~LockGuard(){_mutex.Unlock();}private:Mutex &_mutex; // 使用引用: 互斥锁不支持拷贝};
}

3.Cond.hpp

#pragma#include <pthread.h>
#include "Mutex.hpp"namespace CondModule
{using namespace MutexModule;class Cond{public:Cond() {::pthread_cond_init(&_cond, nullptr);}~Cond() {::pthread_cond_destroy(&_cond);}void Wait(Mutex &mutex) // 线程释放曾经持有的锁, 不能拷贝{::pthread_cond_wait(&_cond, mutex.LockAddr());}void Signal(){::pthread_cond_signal(&_cond);}void Broadcast(){::pthread_cond_broadcast(&_cond);}private:pthread_cond_t _cond;};
}

4.Log.hpp

#pragma once#include <iostream>
#include <cstdio>
#include <string>
#include <filesystem>
#include <fstream>
#include <sstream>
#include <memory>
#include <unistd.h>
#include <time.h>
#include "Mutex.hpp"namespace LogModule
{using namespace MutexModule;// 获取系统时间std::string CurrentTime(){time_t time_stamp = ::time(nullptr); // 获取时间戳struct tm curr;localtime_r(&time_stamp, &curr); // 将时间戳转化为可读性强的信息char buffer[1024];snprintf(buffer, sizeof(buffer), "%4d-%02d-%02d %02d:%02d:%02d",curr.tm_year + 1900,curr.tm_mon + 1,curr.tm_mday,curr.tm_hour,curr.tm_min,curr.tm_sec);return buffer;}// 日志文件: 默认路径和默认文件名const std::string defaultlogpath = "./log/";const std::string defaultlogname = "log.txt";// 日志等级enum class LogLevel{DEBUG = 1,INFO,WARNING,ERROR,FATAL};std::string Level2String(LogLevel level){switch (level){case LogLevel::DEBUG:return "DEBUG";case LogLevel::INFO:return "INFO";case LogLevel::WARNING:return "WARNING";case LogLevel::ERROR:return "ERROR";case LogLevel::FATAL:return "FATAL";default:return "NONE";}}// 3. 策略模式: 刷新策略class LogStrategy{public:virtual ~LogStrategy() = default;// 纯虚函数: 无法实例化对象, 派生类可以重载该函数, 实现不同的刷新方式virtual void SyncLog(const std::string &message) = 0;};// 3.1 控制台策略class ConsoleLogStrategy : public LogStrategy{public:ConsoleLogStrategy() {}~ConsoleLogStrategy() {}void SyncLog(const std::string &message) override{LockGuard lockguard(_mutex);std::cout << message << std::endl;}private:Mutex _mutex;};// 3.2 文件级(磁盘)策略class FileLogStrategy : public LogStrategy{public:FileLogStrategy(const std::string &logpath = defaultlogpath, const std::string &logname = defaultlogname): _logpath(logpath), _logname(logname){// 判断_logpath目录是否存在if (std::filesystem::exists(_logpath)){return;}try{std::filesystem::create_directories(_logpath);}catch (std::filesystem::filesystem_error &e){std::cerr << e.what() << std::endl;}}~FileLogStrategy() {}void SyncLog(const std::string &message) override{LockGuard lockguard(_mutex);std::string log = _logpath + _logname;std::ofstream out(log, std::ios::app); // 以追加的方式打开文件if (!out.is_open()){return;}out << message << "\n"; // 将信息刷新到out流中out.close();}private:std::string _logpath;std::string _logname;Mutex _mutex;};// 4. 日志类: 构建日志字符串, 根据策略进行刷新class Logger{public:Logger(){// 默认往控制台上刷新_strategy = std::make_shared<ConsoleLogStrategy>();}~Logger() {}void EnableConsoleLog(){_strategy = std::make_shared<ConsoleLogStrategy>();}void EnableFileLog(){_strategy = std::make_shared<FileLogStrategy>();}// 内部类: 记录完整的日志信息class LogMessage{public:LogMessage(LogLevel level, const std::string &filename, int line, Logger &logger): _currtime(CurrentTime()), _level(level), _pid(::getpid()), _filename(filename), _line(line), _logger(logger){std::stringstream ssbuffer;ssbuffer << "[" << _currtime << "] "<< "[" << Level2String(_level) << "] "<< "[" << _pid << "] "<< "[" << _filename << "] "<< "[" << _line << "] - ";_loginfo = ssbuffer.str();}~LogMessage(){if(_logger._strategy){_logger._strategy->SyncLog(_loginfo);}}template <class T>LogMessage &operator<<(const T &info){std::stringstream ssbuffer;ssbuffer << info;_loginfo += ssbuffer.str();return *this;}private:std::string _currtime;  // 当前日志时间LogLevel _level;       // 日志水平pid_t _pid;            // 进程pidstd::string _filename; // 文件名uint32_t _line;        // 日志行号Logger &_logger;       // 负责根据不同的策略进行刷新std::string _loginfo;  // 日志信息};// 故意拷贝, 形成LogMessage临时对象, 后续在被<<时,会被持续引用,// 直到完成输入,才会自动析构临时LogMessage, 至此完成了日志的刷新,// 同时形成的临时对象内包含独立日志数据, 未来采用宏替换, 获取文件名和代码行数LogMessage operator()(LogLevel level, const std::string &filename, int line){return LogMessage(level, filename, line, *this);}private:// 纯虚类不能实例化对象, 但是可以定义指针std::shared_ptr<LogStrategy> _strategy; // 日志刷新策略方案};// 定义全局logger对象Logger logger;// 编译时进行宏替换: 方便随时获取行号和文件名
#define LOG(level) logger(level, __FILE__, __LINE__)// 提供选择使用何种日志策略的方法
#define ENABLE_CONSOLE_LOG() logger.EnableConsoleLog()
#define ENABLE_FILE_LOG() logger.EnableFileLog()
}

5.Thread.hpp

#pragma once#include <iostream>
#include <string>
#include <functional>
#include <pthread.h>
#include <sys/types.h>
#include <unistd.h>namespace ThreadModule
{using func_t = std::function<void(std::string)>;static int number = 1;// 强类型枚举: 枚举的成员名称被限定在枚举类型的作用域内enum class TSTATUS{NEW,RUNNING,STOP};class Thread{private:// 成员方法: 需要加上static表示不需要this指针, 否则回调函数报错// 而要执行_func()函数又需要由this指针, 所以Routine函数传this指针static void *Routine(void *args){Thread *t = static_cast<Thread *>(args);t->_func(t->Name());return nullptr;}void EnableDetach() { _joinable = false; }public:Thread(func_t func): _func(func), _status(TSTATUS::NEW), _joinable(true){_name = "Thread-" + std::to_string(number++);_pid = getpid();}~Thread() {}// 线程创建bool Start(){if (_status != TSTATUS::RUNNING){int n = pthread_create(&_tid, nullptr, Routine, this);if (n != 0)return false;_status = TSTATUS::RUNNING;return true;}return false;}// 线程退出bool Stop(){if (_status == TSTATUS::RUNNING){int n = ::pthread_cancel(_tid);if (n != 0)return false;_status = TSTATUS::STOP;return true;}return false;}// 线程等待bool Join(){if (_joinable){int n = ::pthread_join(_tid, nullptr);if (n != 0)return false;_status = TSTATUS::STOP;return true;}return false;}// 线程分离bool Detach(){EnableDetach();int n = ::pthread_detach(_tid);if (n != 0)return false;return true;}// 线程是否分离bool IsJoinable() {  return _joinable; }std::string Name() { return _name; }private:std::string _name;pthread_t _tid;pid_t _pid;bool _joinable; // 线程是否是分离的, 默认不是func_t _func;TSTATUS _status;};
}

6.ThreadPool.hpp

#pragma once#include <iostream>
#include <string>
#include <vector>
#include <queue>
#include <memory>
#include "Mutex.hpp"
#include "Cond.hpp"
#include "Thread.hpp"
#include "Log.hpp"namespace ThreadPoolModule
{using namespace MutexModule;using namespace CondModule;using namespace ThreadModule;using namespace LogModule;using thread_t = std::shared_ptr<Thread>;const static int defaultnum = 15;template <class T>class ThreadPool{private:bool IsEmpty() { return _taskq.empty(); }void HandlerTask(std::string name){LOG(LogLevel::INFO) << "线程: " << name << ", 进入HandlerTask执行逻辑";while (true){// 1. 拿任务: 访问共享资源, 需要加锁T task;{LockGuard lockguard(_mutex);while (IsEmpty() && _isrunning) // while替代if: 防止伪唤醒{_wait_num++;_cond.Wait(_mutex); // 没任务时: 线程在条件变量上阻塞等待_wait_num--;}// 2. 任务队列不为空 && 线程池退出if (IsEmpty() && !_isrunning)break;task = _taskq.front();_taskq.pop();}// 3. 处理任务: 并发处理, 不需要持有锁task();}LOG(LogLevel::INFO) << "线程: " << name << ", 退出";}ThreadPool(int num = defaultnum): _num(num), _wait_num(0), _isrunning(false){for (int i = 0; i < _num; i++){// 在类中: bind类的公有方法, 需要取地址 + 传入this指针// 在类外: bind类的公有方法, 需要取地址 + 传入类的匿名对象_threads.push_back(std::make_shared<Thread>(std::bind(&ThreadPool::HandlerTask, this, std::placeholders::_1))); // push_back()会调用移动构造LOG(LogLevel::INFO) << "构建线程" << _threads.back()->Name() << "对象...成功";}}ThreadPool<T>(const ThreadPool<T> &) = delete;ThreadPool<T> &operator=(const ThreadPool<T> &) = delete;public:~ThreadPool() {}// 获取单例对象static ThreadPool<T> *GetInstance(){// 若单例为空: 需要加锁创建单例对象if(instance == nullptr){LockGuard lockguard(_lock);if(instance == nullptr){LOG(LogLevel::INFO) << "单例首次被执行, 需要加载对象...";instance = new ThreadPool<T>();instance->Start();}}// 若单例不为空: 直接返回单例对象return instance;}void Equeue(T in){LockGuard lockguard(_mutex);if (!_isrunning) return;_taskq.push(in);if (_wait_num > 0){_cond.Signal(); // 唤醒线程}}void Start(){if (_isrunning) return;_isrunning = true;for (auto &thread_ptr : _threads){thread_ptr->Start();LOG(LogLevel::INFO) << "启动线程" << thread_ptr->Name() << "...成功";}}void Stop(){LockGuard lockguard(_mutex);if (_isrunning){// 1. 不能再新增任务了_isrunning = false;// 2. 让线程自己退出(唤醒所有的线程) && 历史任务被执行完if (_wait_num > 0){_cond.Broadcast();}}}void Wait(){for (auto &thread_ptr : _threads){thread_ptr->Join();LOG(LogLevel::INFO) << "回收线程" << thread_ptr->Name() << "...成功";}}private:int _num;                       // 线程的个数std::vector<thread_t> _threads; // 线程池std::queue<T> _taskq;           // 共享资源: 任务队列int _wait_num;                  // 等待的线程数目bool _isrunning;                // 线程池是否运行Mutex _mutex; // 锁Cond _cond;   // 条件变量static ThreadPool<T> *instance; // 单例对象static Mutex _lock;             // 用来保护单例};// 静态成员: 类内声明, 类外定义template<class T>ThreadPool<T> *ThreadPool<T>::instance = nullptr;template<class T>Mutex ThreadPool<T>::_lock;
}

7.Common.hpp

#pragma once#include <iostream>
#include <string>#define Die(code)   \do              \{               \exit(code); \} while (0)#define CONV(v) (struct sockaddr *)(v)enum
{USAGE_ERR = 1,SOCKET_ERR,BIND_ERR,LISTEN_ERR
};bool ParseOneLine(std::string &str, std::string *out, const std::string &sep)
{auto pos = str.find(sep);if (pos == std::string::npos)return false;*out = str.substr(0, pos);str.erase(0, pos + sep.size());return true;
}// Connection: keep-alive
// 解析后: key = Connection; value = keep-alive
bool SplitString(const std::string &header, const std::string sep, std::string *key, std::string *value)
{auto pos = header.find(sep);if (pos == std::string::npos)return false;*key = header.substr(0, pos);*value = header.substr(pos + sep.size());return true;
}

8.InetAddr.hpp

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>#include "Common.hpp"class InetAddr
{
private:// 端口号: 网络序列->主机序列void PortNetToHost(){_port = ::ntohs(_net_addr.sin_port);}// IP: 网络序列->主机序列void IpNetToHost(){char ipbuffer[64];::inet_ntop(AF_INET, &_net_addr.sin_addr, ipbuffer, sizeof(ipbuffer));_ip = ipbuffer;}public:InetAddr() {}InetAddr(const struct sockaddr_in &addr): _net_addr(addr){PortNetToHost();IpNetToHost();}InetAddr(uint16_t port): _port(port), _ip(""){_net_addr.sin_family = AF_INET;_net_addr.sin_port = ::htons(_port);_net_addr.sin_addr.s_addr = INADDR_ANY;}~InetAddr() {}bool operator==(const InetAddr &addr) { return _ip == addr._ip && _port == addr._port; }struct sockaddr *NetAddr() { return CONV(&_net_addr); }socklen_t NetAddrLen() { return sizeof(_net_addr); }std::string Ip() { return _ip; }uint16_t Port() { return _port; }std::string Addr() { return Ip() + ":" + std::to_string(Port()); }void SetAddr(sockaddr_in &client){_net_addr = client;PortNetToHost();IpNetToHost();}private:struct sockaddr_in _net_addr;std::string _ip; // 主机序列: IPuint16_t _port;  // 主机序列: 端口号
};

9.Protocol.hpp

#pragma once#include <iostream>
#include <string>
#include <memory>
#include <jsoncpp/json/json.h>const std::string Sep = "\r\n"; // 回车换行符// 封装: {json} -> len\r\n{json}\r\n
bool Encode(std::string &message)
{if (message.size() == 0)return false;// 添加报头, 形成完整的报文std::string package = std::to_string(message.size()) + Sep + message + Sep;message = package;return true;
}// 解包: len\r\n{json}\r\n -> {json}
// 同时需要保证报文的完整性!
// 123\r\n
// 123\r\n{json}
// 123\r\n{json}\r\n
// 123\r\n{json}\r\n123\r\n{json}\r\n
bool Decode(std::string &package, std::string *content)
{auto pos = package.find(Sep);if (pos == std::string::npos)return false;std::string content_len_str = package.substr(0, pos);int content_len = std::stoi(content_len_str);// 完整报文的长度int full_len = content_len_str.size() + content_len + 2 * Sep.size();if (package.size() < full_len)return false;// {json}字符串的长度*content = package.substr(pos + Sep.size(), content_len);package.erase(0, full_len);return true;
}class Request
{
public:Request(): _x(0), _y(0), _oper(0){}Request(int x, int y, char oper): _x(x), _y(y), _oper(oper){}// 序列化bool Serialize(std::string &out_string){Json::Value root;root["x"] = _x;root["y"] = _y;root["oper"] = _oper;Json::StreamWriterBuilder wb;std::unique_ptr<Json::StreamWriter> w(wb.newStreamWriter());std::stringstream ss;w->write(root, &ss);out_string = ss.str();return true;}// 反序列化bool Deserialize(std::string &in_string){Json::Value root;Json::Reader reader;bool parsingSuccessful = reader.parse(in_string, root);if (!parsingSuccessful){std::cout << "Failed to parse JSON: " << reader.getFormattedErrorMessages() << std::endl;return false;}_x = root["x"].asInt();_y = root["y"].asInt();_oper = root["oper"].asInt();return true;}void Print(){std::cout << "x: " << _x << std::endl;std::cout << "oper: " << _oper << std::endl;std::cout << "y: " << _y << std::endl;}int X() const { return _x; }int Y() const { return _y; }char Oper() const { return _oper; }private:int _x;int _y;char _oper;
};class Response
{
public:Response(): _result(0), _code(0){}Response(int result, int code): _result(result), _code(code){}// 序列化bool Serialize(std::string &out_string){Json::Value root;root["result"] = _result;root["code"] = _code;Json::StreamWriterBuilder wb;std::unique_ptr<Json::StreamWriter> w(wb.newStreamWriter());std::stringstream ss;w->write(root, &ss);out_string = ss.str();return true;}// 反序列化bool Deserialize(std::string &in_string){Json::Value root;Json::Reader reader;bool parsingSuccessful = reader.parse(in_string, root);if (!parsingSuccessful){std::cout << "Failed to parse JSON: " << reader.getFormattedErrorMessages() << std::endl;return false;}_result = root["result"].asInt();_code = root["code"].asInt();return true;}void Print(){std::cout << "result: " << _result << std::endl;std::cout << "code: " << _code << std::endl;}int Result() const { return _result; }int Code() const { return _code; }void SetResult(int result) { _result = result; }void SetCode(int code) { _code = code; }private:int _result; // 结果int _code;   // 错误码
};

10.Calculator.hpp

#pragma once#include <iostream>
#include "Protocol.hpp"class Calculator
{
public:Calculator() {}~Calculator() {}Response Execute(const Request &req){// 拿到的是结构化的对象Response resp;switch (req.Oper()){case '+':resp.SetResult(req.X() + req.Y());break;case '-':resp.SetResult(req.X() - req.Y());break;case '*':resp.SetResult(req.X() * req.Y());break;case '/':{if (req.Y() == 0){resp.SetCode(1); // 1: 除零}else{resp.SetResult(req.X() / req.Y());}}break;case '%':{if (req.Y() == 0){resp.SetCode(2); // 2: 模零}else{resp.SetResult(req.X() % req.Y());}}break;default:resp.SetCode(3); // 3: 类型无法识别break;}return resp;}
};

11.Deamon.hpp

#pragma once#include <iostream>
#include <cstdlib>
#include <signal.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>#define ROOT "/"
#define devnull "/dev/null"void Deamon(bool ischdir, bool isclose)
{// 1. 守护进程一般要屏蔽一些特定的信号signal(SIGCHLD, SIG_IGN);signal(SIGPIPE, SIG_IGN);// 2. 成为非组长进程: 创建子进程if(fork()) exit(0);// 3. 建立新会话setsid();// 4. 每一个进程都有自己的CWD, 是否将其修改为根目录if(ischdir) chdir(ROOT);// 5. 脱离终端: 将标准输入、输出重定向到字符文件"/dev/null"中if(isclose){::close(0);::close(1);::close(2);}else{// 建议这样!int fd = ::open(devnull, O_WRONLY);if(fd > 0){::dup2(fd, 0);::dup2(fd, 1);::dup2(fd, 2);::close(fd);}}
}

12.TcpServer.hpp

#pragma once#include <iostream>
#include <string>
#include <cstring>
#include <cerrno>
#include <functional>#include <pthread.h>
#include <signal.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>#include "Log.hpp"
#include "Common.hpp"
#include "InetAddr.hpp"
#include "ThreadPool.hpp"using namespace LogModule;
using namespace ThreadPoolModule;using task_t = std::function<void()>;
using handler_t = std::function<std::string(std::string&)>;#define BACKLOG 8uint16_t gport = 8080;class TcpServer
{
public:TcpServer(handler_t handler, int port = gport): _handler(handler), _port(port), _isrunning(false){}~TcpServer() {}void InitServer(){// 1. 创建TCP套接字_listensockfd = ::socket(AF_INET, SOCK_STREAM, 0);if (_listensockfd < 0){LOG(LogLevel::FATAL) << "socket error";Die(SOCKET_ERR);}LOG(LogLevel::INFO) << "socket create success, listensockfd is: " << _listensockfd;// 2. 绑定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(_listensockfd, CONV(&local), sizeof(local));if (n < 0){LOG(LogLevel::FATAL) << "bind error";Die(BIND_ERR);}LOG(LogLevel::INFO) << "bind success";// 3. Tcp是面向连接的, 就要求Tcp随时随地等待被客户端连接, Tcp需要将socket设置为监听状态//    客户端请求连接时: 将客户端连接请求放入一个监听队列中n = ::listen(_listensockfd, BACKLOG);if (n < 0){LOG(LogLevel::FATAL) << "listen error";Die(LISTEN_ERR);}LOG(LogLevel::INFO) << "listen success";// ::signal(SIGCHLD, SIG_IGN); // 子进程退出, 父进程无需wait, 操作系统自动回收资源}// Tcp也是全双工的: 在同一个文件描述符中, 既可以读又可以写void HandlerRequest(int sockfd){LOG(LogLevel::INFO) << "开始处理客户端请求...";char inbuffer[4096];std::string package;while (true){// 约定: 客户端发过来的是一条完整的命令string// 1. 读取客户端发送来的消息ssize_t n = ::recv(sockfd, inbuffer, sizeof(inbuffer) - 1, 0); // 读取是不完善的if (n > 0) {inbuffer[n] = 0;package += inbuffer; // len\r\n{json}\r\n// 保证了报文的完整性std::string reault = _handler(package);if(reault.empty()) continue;// 2. 向客户端发送消息::send(sockfd, reault.c_str(), reault.size(), 0); // 写入也是不完善的}else if (n == 0){// read如果读取的返回值是0, 表示客户端退出LOG(LogLevel::INFO) << "客户端退出: " << sockfd;break;}else{// 读取失败break;}}::close(sockfd); // 关闭fd, 防止fd泄漏问题}void Start(){_isrunning = true;while (_isrunning){// 1. Tcp不能直接获取数据: 需要获取新连接// 阻塞等待, 直到有客户端连接请求进入监听队列, 然后从队列中取出一个请求, 为该客户端建立连接,// 并返回一个新的套接字描述符, 通过这个新的套接字描述符就可以与客户端进行数据的发送和接收struct sockaddr_in peer;socklen_t peerlen = sizeof(peer);int sockfd = ::accept(_listensockfd, CONV(&peer), &peerlen);if (sockfd < 0){LOG(LogLevel::WARNING) << "accept error: " << strerror(errno);continue;}LOG(LogLevel::INFO) << "accept success, sockfd is: " << sockfd;// 获取客户端的信息: IP + 端口号InetAddr addr(peer);LOG(LogLevel::INFO) << "client info: " << addr.Addr();// version1->单进程版本: 单客户端访问// HandlerRequest(sockfd);// version2->多进程版本: 多客户端访问// pid_t id = fork();// if (id == 0)// {//     // 子进程: 继承父进程的文件描述符表, 有两张表//     ::close(_listensockfd); // 关闭不需要的文件描述符: 监听套接字//     if(fork() > 0) exit(0); // 子进程退出//     // 孙子进程->孤儿进程: 不断与客户端的数据传输, 退出后被操作系统自动回收//     HandlerRequest(sockfd);//     exit(0);// }// // 父进程: 不断与客户端建立连接// ::close(sockfd); // 关闭不需要的文件描述符: socket// // waitpid不会阻塞// int rid = ::waitpid(id, nullptr, 0);// if(rid < 0)// {//     LOG(LogLevel::WARNING) << "waitpid error";// }// version3->多线程版本: 多客户端访问// 主线程和新线程共享同一张文件描述符表// pthread_t tid;// ThreadData *data = new ThreadData();// data->sockfd = sockfd;// data->self = this;// pthread_create(&tid, nullptr, ThreadEntry, (void *)data);// version4->线程池版本: 多客户端访问(适合处理短任务)task_t task = std::bind(&TcpServer::HandlerRequest, this, sockfd); // 构建任务ThreadPool<task_t>::GetInstance()->Equeue(task);// ThreadPool<task_t>::GetInstance()->Equeue([this, &sockfd](){//     this->HandlerRequest(sockfd);// });}}struct ThreadData{int sockfd;TcpServer *self;};// 类中ThreadEntry函数带有this指针, 需要加上static// 而没有this指针, 又无法调用HandlerReques函数// 解决方法: 封装ThreadData结构体static void *ThreadEntry(void *argc){pthread_detach(pthread_self()); // 线程分离: 线程退出时由操作系统自动回收, 防止类似僵尸进程的问题ThreadData *data = (ThreadData *)argc;data->self->HandlerRequest(data->sockfd);return nullptr;}void Stop(){_isrunning = false;}private:int _listensockfd; // 监听套接字uint16_t _port;bool _isrunning;handler_t _handler; // 处理客户端发来的任务
};

13.TcpServer.cc

#include <memory>#include "TcpServer.hpp"
#include "Protocol.hpp"
#include "Calculator.hpp"
#include "Deamon.hpp"using cal_t = std::function<Response(Request &req)>;// package不一定是完整的!
// 1. 不完整: 继续读取
// 2. 完整: 直接提取, 执行以下步骤
class Parse
{
public:Parse() {}Parse(cal_t cal): _cal(cal){}std::string Entry(std::string &package){std::string message;std::string respstr;// 1. 删除报头字段(连续处理多个报文)LOG(LogLevel::DEBUG) << "接收的报文: \n" << package;while (Decode(package, &message)){LOG(LogLevel::DEBUG) << "删除报头字段: \n" << message;if (message.empty())break;// 2. 反序列化Request req;req.Deserialize(message);LOG(LogLevel::DEBUG) << "反序列化: ";req.Print();// 3. 响应请求Response resp = _cal(req);LOG(LogLevel::DEBUG) << "计算: ";resp.Print();// 4. 序列化std::string res;resp.Serialize(res);LOG(LogLevel::DEBUG) << "序列化: \n" << res;// 5. 添加报头字段Encode(res);LOG(LogLevel::DEBUG) << "添加报头字段: \n" << res;// 6. 拼接应答respstr += res;}LOG(LogLevel::DEBUG) << "发送的报文: \n" << respstr;return respstr;}private:cal_t _cal;
};int main()
{ENABLE_FILE_LOG(); // 往文件中打印Deamon(false, false); // 守护进程// 1. 计算模块: 应用层Calculator cal;// 2. 解析模块: 表示层(序列化和反序列化)Parse parse([&cal](const Request &req){return cal.Execute(req);});// Parse parse(std::bind(&Calculator::Execute, std::ref(cal), std::placeholders::_1));// 3. 通信模块: 网络层std::shared_ptr<TcpServer> tsvr = std::make_shared<TcpServer>([&parse](std::string &package){return parse.Entry(package);});// std::shared_ptr<TcpServer> tsvr = std::make_shared<TcpServer>(std::bind(&Parse::Entry, std::ref(parse), std::placeholders::_1));tsvr->InitServer();tsvr->Start();return 0;
}

14.TcpClient.cc

#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>#include "Common.hpp"
#include "Protocol.hpp"// ./client_tcp server_ip server_port
int main(int argc, char *argv[])
{if (argc != 3){std::cout << "Usage: ./client_tcp server_ip server_port" << std::endl;return 1;}std::string server_ip = argv[1];int server_port = std::stoi(argv[2]);// 1. 创建套接字int sockfd = ::socket(AF_INET, SOCK_STREAM, 0);if (sockfd < 0){std::cout << "socket error" << std::endl;return 2;}// 2. 客户端不需要显示的进行绑定, 但是需要连接服务器, 在建立连接的过程由操作系统进行绑定IP和端口号struct sockaddr_in server;memset(&server, 0, sizeof(server));server.sin_family = AF_INET;server.sin_port = ::htons(server_port);server.sin_addr.s_addr = ::inet_addr(server_ip.c_str());int n = ::connect(sockfd, CONV(&server), sizeof(server));if (n < 0){std::cout << "connect error" << std::endl;return 3;}std::string message;while (true){int x, y;char oper;std::cout << "input x: ";std::cin >> x;std::cout << "input y: ";std::cin >> y;std::cout << "input oper: ";std::cin >> oper;Request req(x, y, oper);// 1. 序列化req.Serialize(message);// 2. 添加报头字段Encode(message);// 3. 向服务器发送报文n = ::send(sockfd, message.c_str(), message.size(), 0);if (n > 0){// 4. 获取服务器响应后的报文char inbuffer[1024];int m = ::recv(sockfd, inbuffer, sizeof(inbuffer) - 1, 0);if (m > 0){inbuffer[m] = 0;std::string package = inbuffer; // 认为该是报文完整// 5. 删除报头字段std::string content;Decode(package, &content);// 6. 反序列化Response resp;resp.Deserialize(content);// 7. 得到结构化数据std::cout << resp.Result() << "[" << resp.Code() << "]" << std::endl;}else break;}else break;}::close(sockfd);return 0;
}

15.运行操作

xzy@hcss-ecs-b3aa:~$ ./server_tcp xzy@hcss-ecs-b3aa:~$ ps -axj | head -1 && ps -axj | grep server_tcpPPID     PID    PGID     SID TTY        TPGID STAT   UID   TIME COMMAND1 1369094 1369094 1369094 ?             -1 Ss    1000   0:00 ./server_tcp
1365157 1369098 1369097 1365157 pts/2    1369097 S+    1000   0:00 grep --color=auto server_tcpxzy@hcss-ecs-b3aa:~$ ./client_tcp 127.0.0.1 8080
input x: 100
input y: 200
input oper: +
300[0]
...

相关文章:

【Linux】应用层自定义协议 + 序列化和反序列化

应用层自定义协议 序列化和反序列化 一.应用层1.再谈 "协议"2.序列化 和 反序列化 二. Jsoncpp1.序列化2.反序列化 三. Tcp全双工 面向字节流四.自定义协议 保证报文的完整性1.Makefile2.Mutex.hpp3.Cond.hpp4.Log.hpp5.Thread.hpp6.ThreadPool.hpp7.Common.hpp8.…...

框架的CVE漏洞利用 php类 java类 手工操作和自动化操作蓝队分析漏洞利用的流量特征

前言 php重要框架和基本的识别特征 php的主要是 tp框架 和 laravel 当然还有 yii等 tp的主要特征 1\报错信息&#xff1a; 2、图标 3、请求头 Laravel特征 1、报错信息 2、请求头 php框架CVE利用 lavarvel 工具 https://github.com/zhzyker/CVE-2021-3129 https://git…...

使用 .github/realse.yml 自动生成 release change log

🚀 发现 gone-io/gone:一个优雅的 Go 依赖注入框架!💻 它让您的代码更简洁、更易测试。🔍 框架轻量却功能强大,完美平衡了灵活性与易用性。⭐ 如果您喜欢这个项目,请给我们点个星!🌟 您的支持是我们前进的动力!🤝 欢迎贡献代码或提出建议,一起让 gone 变得更好…...

python学习笔记--实现简单的爬虫(一)

任务&#xff1a;爬取豆瓣最受欢迎的250个电影的资料 链接&#xff1a;豆瓣电影 Top 250 用浏览器打开后&#xff0c;使用F12或鼠标右键--检查&#xff0c;查看网页的源代码&#xff0c;分析网页结构&#xff0c;如下图所示&#xff1a; 分析后得知&#xff1a; 1.电影名位于…...

[学习笔记] 部署Docker搭建靶场

前言 我们需要部署Docker来搭建靶场题目&#xff0c;他可以提供一个隔离的环境&#xff0c;方便在不同的机器上部署&#xff0c;接下来&#xff0c;我会记录我的操作过程&#xff0c;简单的部署一道题目 Docker安装 不推荐在物理机上部署&#xff0c;可能会遇到一些问题&…...

二分查找-在排序数组中查找元素的第一个和最后一个位置

34.在排序数组中查找元素的第一个和最后一个位置 给你一个按照非递减顺序排列的整数数组 nums&#xff0c;和一个目标值 target。请你找出给定目标值在数组中的开始位置和结束位置。如果数组中不存在目标值 target&#xff0c;返回 [-1, -1]。你必须设计并实现时间复杂度为 O(…...

蓝桥杯真题 2109.统计子矩阵

原题地址:1.统计子矩阵 - 蓝桥云课 问题描述 给定一个 NMNM 的矩阵 AA, 请你统计有多少个子矩阵 (最小 1111, 最大 NM)NM) 满足子矩阵中所有数的和不超过给定的整数 KK ? 输入格式 第一行包含三个整数 N,MN,M 和 KK. 之后 NN 行每行包含 MM 个整数, 代表矩阵 AA. 输出格…...

学术型ppt制作经验分享 - 如何美化科研ppt?

WordinPPT / 持续为双一流高校、科研院所、企业等提供PPT制作系统服务。 - 院士增选、国家科技奖、杰青、长江学者特聘教授、校企联聘教授、重点研发、优青、青长、青拔... / 学术型ppt制作经验分享 部分页面内容 / PPT全文共101页 - 科学技术奖ppt&#xff1a;自然科学奖…...

Python数据可视化实战:从基础图表到高级分析

Python数据可视化实战&#xff1a;从基础图表到高级分析 数据可视化是数据分析的重要环节&#xff0c;通过直观的图表可以快速洞察数据规律。本文将通过5个实际案例&#xff0c;手把手教你使用Python的Matplotlib库完成各类数据可视化任务&#xff0c;涵盖条形图、堆积面积图、…...

稳定运行的以Oracle NoSQL数据库为数据源和目标的ETL性能变差时提高性能方法和步骤

提高基于Oracle NoSQL数据库的ETL&#xff08;提取、转换、加载&#xff09;性能时&#xff0c;主要从多个角度进行优化。 提高基于Oracle NoSQL数据库的ETL性能需要综合考虑多个方面&#xff0c;关键是减少不必要的I/O操作、优化数据转换和加载过程、合理配置Oracle NoSQL数据…...

ORACLE 19.8版本数据库环境EXPDP导数据的报错处理

近期用户在做EXPDP导出时&#xff0c;报错异常termination终止;EXPDP本身是简单的功能并且这个环境也是经常做导出的&#xff0c;到底是什么原因导致了这个问题呢&#xff1f; 导出脚本报错&#xff1a; 分析导出日志&#xff0c;当时系统资源充足但是进程启动失败&#xff0c;…...

GPT-5 将免费向所有用户开放?

GPT-5 将免费向所有用户开放&#xff1f; 硅谷知名分析师 Ben Thompson 最近与 OpenAI CEO Sam Altman 进行了一场深度对谈&#xff0c;其中Sam Altman透漏GPT-5将免费向大家发放。 OpenAI 这波操作可不是一时冲动&#xff0c;而是被逼出来的。DeepSeek 这个新秀横空出世&am…...

【AI模型】深度解析:DeepSeek的联网搜索的实现原理与认知误区

一、大模型的“联网魔法”&#xff1a;原来你是这样上网的&#xff01; 在人工智能这个舞台上&#xff0c;大模型们可是妥妥的明星。像DeepSeek、QWen这些大模型&#xff0c;个个都是知识渊博的“学霸”&#xff0c;推理、生成文本那叫一个厉害。不过&#xff0c;要是论起上网…...

学习笔记--基于Sa-Token 实现Java项目单点登录+同端互斥检测

目录 同端互斥登录 单点登录SSO 架构选型 模式二: URL重定向传播 前后端分离 整体流程 准备工作 搭建客户端 搭建认证中心SSO Server 环境配置 开放认证接口 启动类 跨域处理 同端互斥登录 同端互斥登陆 模块 同端互斥登录指&#xff1a;同一类型设备上只允许单地…...

Can通信流程

下面给出一个更详细的 CAN 发送报文的程序流程说明&#xff0c;结合 HAL 库的使用及代码示例&#xff0c;帮助你了解每一步的具体操作和内部原理。 一、系统与外设初始化 1.1 HAL 库初始化 在 main() 函数开头&#xff0c;首先调用 HAL 库初始化函数&#xff1a; HAL_Init()…...

基于BClinux8部署Ceph 19.2(squid)集群

#作者&#xff1a;闫乾苓 文章目录 1.版本选择Ceph版本发布历史目前官方在维护的版本 2.部署方法3.服务器规划4.前置配置4.1系统更新4.2配置hosts cat >> /etc/hosts << EOFssh-keygenssh-copy-id ceph01ssh-copy-id ceph02ssh-copy-id ceph034.5 Python34.6 Syst…...

Workerman5.0如何实现一对一聊天

文章精选推荐 1 JetBrains Ai assistant 编程工具让你的工作效率翻倍 2 Extra Icons&#xff1a;JetBrains IDE的图标增强神器 3 IDEA插件推荐-SequenceDiagram&#xff0c;自动生成时序图 4 BashSupport Pro 这个ides插件主要是用来干嘛的 &#xff1f; 5 IDEA必装的插件&…...

c#难点整理2

1.对象池的使用 就是先定义一系列的对象&#xff0c;用一个&#xff0c;调一个。 public class ObjectPool<T> where T : new(){private Queue<T> pool; // 用于存储对象的队列private int maxSize; // 对象池的最大容量// 构造函数public ObjectPool(int maxSi…...

STM32基础教程——定时器

前言 TIM定时器&#xff08;Timer&#xff09;:STM32的TIM定时器是一种功能强大的外设模块&#xff0c;通过时基单元&#xff08;包含预分频器、计数器和自动重载寄存器&#xff09;实现精准定时和计数功能。其核心原理是&#xff1a;内部时钟&#xff08;CK_INT&#xff09;或…...

How to share files with Windows via samba in Linux mint 22

概述 Windows是大家日常使用最多的操作系统&#xff0c;在Windows主机之间&#xff0c;可以共享文件&#xff0c;那么如何在Windows主机与Linux主机之间共享文件呢&#xff1f; 要在Windows主机与Linux主机之间共享文件&#xff0c;我们可以借助Samba协议完成。借助Samba协议…...

[AI速读]如何构建高效的AMBA协议检查器(Checker IP)

在芯片验证过程中,检查器(Checker)是确保设计符合协议规范的关键工具。本文基于一篇技术论文,分享如何为AMBA协议(如AXI、AHB)构建可重用的检查器IP(Checker IP,简称CIP),并简化其核心思路,帮助工程师快速上手。 一、什么是Checker IP? Checker IP是一组用SystemVe…...

VBA-Excel

VBA 一、数据类型与变量 常用数据类型&#xff1a; Byte&#xff1a;字节型&#xff0c;0~255。Integer&#xff1a;整数型&#xff0c;用于存储整数值&#xff0c;范围 -32768 到 32767。Long&#xff1a;长整型&#xff0c;可存储更大范围的整数&#xff0c;范围 -214748364…...

网络华为HCIA+HCIP IPv6

目录 IPv4现状 IPv6基本报头 IPv6扩展报头 IPv6地址 IPv6地址缩写规范 ​编辑 IPv6地址分配 IPv6单播地址分配 IPv6单播地址接口标识 IPv6常见单播地址 - GUA &#xff08;2 / 3 开头&#xff09; IPv6常见单播地址 - ULA IPv6常见单播地址 - LLA IPv6组播地…...

LeetCode 每日一题 2025/3/17-2025/3/23

记录了初步解题思路 以及本地实现代码&#xff1b;并不一定为最优 也希望大家能一起探讨 一起进步 目录 3/17 1963. 使字符串平衡的最小交换次数3/18 2614. 对角线上的质数3/19 2610. 转换二维数组3/20 2612. 最少翻转操作数3/21 2680. 最大或值3/22 2643. 一最多的行3/23 2116…...

git tag以及git

git tag 以及git 一、先说收获吧 1. git bash 在windows上 类似于linux的bash提供的shell命令行窗口&#xff0c;可以执行很多linux命令&#xff0c;cd pwd ls vim cat touch mkdir&#xff0c;还可以用正则匹配查看标签。相当于在windows上装了一个小的linux。git init myproj…...

Android 自定义变形 MD5 算法

版权归作者所有&#xff0c;如有转发&#xff0c;请注明文章出处&#xff1a;https://cyrus-studio.github.io/blog/ MD5是一种哈希函数&#xff0c;用于将任意长度的数据映射为一个固定长度的哈希值。它由 Ron Rivest 在 1991 年设计&#xff0c;是继 MD4 之后的改进版本。 M…...

【SpringBoot】MorningBox小程序的完整后端接口文档

以下是「晨光宅配」小程序的完整接口文档,涵盖了所有12个表的接口。 每个接口包括请求方法、URL、请求参数、响应格式和示例 接口文档 1. 用户模块 1.1 获取用户信息 URL: /user/{userId}方法: GET请求参数: userId (路径参数): 用户ID响应格式:{"userId": 1,&qu…...

2025年01月03日微创网络(杭州银行外包)前端面试

目录 html 块级元素和行内元素有哪些阴影的几个属性垂直水平居中的实现方式定位的几种方式盒子模型的方式js的数组方法有哪些vue2 vue3 区别vuex哈希路由和浏览器路由的区别浏览器缓存的几个方式react hooks的优势react 组件传值vue 组件传值如何进行性能优化前端监控get post…...

工单分类总结

微调BERT-base模型,构建层次化分类器,Top-3准确率达97.2%,并自动识别出问题的关键类别 1. 具体微调的BERT-base模型是什么模型? BERT-base模型是一个预训练的Transformer模型,包含12个Transformer块、12个自注意头和隐藏大小为768。该模型在大规模文本数据上进行了预训练…...

2025年了,5G还有三个新变化

最近舆论开始讨论5G为什么不火了&#xff1f;5G是不是停滞发展了。 实际上&#xff0c;5G不仅在发展&#xff0c;还迎来了它的升级版5G-A。 在今年西班牙举行的世界移动通信大会上&#xff0c;5G-A就是焦点之一。 被誉为全球通信领域风向标的MWC&#xff0c;汇聚了华为、中兴通…...

区块链交易所平台开发全解析

在数字化飞速发展的今天&#xff0c;区块链技术已成为金融领域的核心驱动力之一。作为数字货币交易的关键平台&#xff0c;区块链交易所的开发不仅涉及复杂的技术环节&#xff0c;还需要兼顾用户体验、安全性、合规性等多个方面。本文将深入探讨区块链交易所平台的开发流程、关…...

hexo+butterfly博客功能完善和美化(四)---博客上传

hexobutterfly博客功能完善和美化&#xff08;四&#xff09;—博客上传 这是最后一期讲美化和功能完善了&#xff0c;笔者会陆续把csdn上面的博客转移到我的博客网站上面&#xff0c;大家可以来访问 Darlingの妙妙屋 文章目录 hexobutterfly博客功能完善和美化&#xff08;…...

源码分析之Leaflet中dom模块DomEvent.DoubleTap的实现原理

概述 DomEvent.DoubleTap模块是Leaflet中用于模拟双击(dbclick)事件的模块&#xff0c;主要解决移动端浏览器对双击事件支持不完善或延迟的问题&#xff0c;同时避免与标签(<label>)关联的表单元素误触发。 源码分析 源码实现如下 DomEvent.DoubleTap的源码实现如下&…...

记录一次,rabbitmq开启stomp插件之后,还是连不上15674端口的问题

原因是装在docker 里面的rabbitmq 没有映射15674端口&#xff0c;需重新删除容器之后重新运行 docker run -d --name rabbitmq -p 5672:5672 -p 15672:15672 -p 15674:15674 -p 1883:1883 -p 15675:15675 rabbitmq:版本号 进入docker容器开启插件 docker exec -it rabbitm…...

git clone项目报错fatal: fetch-pack: invalid index-pack output问题

前情回顾&#xff1a;git项目放在公司服务器上面&#xff0c;克隆等操作需要连接VPN才能操作。由于项目比较大&#xff0c;网速比较慢&#xff0c;克隆项目经常出现fetch-pack: invalid index-pack output。在网上查找各种解决方法。也就这一种有点效果。仅供参考&#xff0c;不…...

【access开发】导入excel 并生成表

hi&#xff0c;大家好呀&#xff01; 最近天气越来越暖了&#xff0c;在这个春暖花开的季节了&#xff0c;每天心情应该都是美美的&#xff0c;正所谓一年之计在于春&#xff0c;在这个美好的季节&#xff0c;大家一起努力学习学习吧&#xff01;那我们来看看今天学点啥呢&…...

利用knn算法实现手写数字分类

利用knn算法实现手写数字分类 1.作者介绍2.KNN算法2.1KNN&#xff08;K-Nearest Neighbors&#xff09;算法核心思想2.2KNN算法的工作流程2.3优缺点2.4 KNN算法图示介绍 3.实验过程3.1安装所需库3.2 MNIST数据集3.3 导入手写数字图像进行分类3.4 完整代码3.5 实验结果 1.作者介…...

从零开始实现 C++ TinyWebServer 处理请求 HttpRequest类详解

文章目录 HTTP 请求报文HttpRequest 类实现 Init() 函数实现 ParseRequestLine() 函数实现 ParseHeader() 函数实现 ParsePath() 函数实现 ParseBody() 函数实现 ParsePost() 函数实现 ParseFromUrlEncoded() 函数实现 UserVerify() 函数实现 Parse() 函数HttpRequest 代码Http…...

Android开发layer-list

Android开发layer-list 它的用处可以在drawable上进行多图拼接&#xff0c;比如启动页&#xff0c;不想图片被拉伸就这么做。还有做某些线突出来。 示例代码&#xff1a; <?xml version"1.0" encoding"utf-8"?> <layer-list xmlns:android&q…...

如何提升库存系统的高并发和稳定性:算法与设计模式

库存系统是企业运营的核心模块&#xff0c;尤其是在电商、零售和供应链管理中&#xff0c;系统的高并发和稳定性直接影响订单处理的准确性和效率。面对海量订单、复杂的库存管理需求&#xff0c;如何在高并发环境下确保库存数据的准确性和系统的稳定性&#xff1f;本文将从架构…...

关于CNN,RNN,GAN,GNN,DQN,Transformer,LSTM,DBN你了解多少

以下是神经网络中常见的几种模型的简要介绍&#xff1a; 1. ​CNN (Convolutional Neural Network, 卷积神经网络) ​用途: 主要用于图像处理和计算机视觉任务。​特点: 通过卷积核提取局部特征&#xff0c;具有平移不变性&#xff0c;能够有效处理高维数据&#xff08;如图像…...

设计模式之装饰器模式

装饰器模式(Decorator)依然是我们设计模式中的结构型模式&#xff0c;其中的构造思想仍然是对多个类进行组合使用&#xff0c;以达成系统调用实现指定功能的设计模式。装饰器模式不论在我们日常开发过程中还是在我们提升技术阅读源码过程中都是比较常见的&#xff0c;但是整体学…...

Mistral AI发布开源多模态模型Mistral Small 3.1:240亿参数实现超越GPT-4o Mini的性能

法国人工智能初创公司Mistral AI于2025年3月正式推出新一代开源模型Mistral Small 3.1 &#xff0c;该模型凭借240亿参数的轻量级设计&#xff0c;在多项基准测试中表现优异&#xff0c;甚至超越了Google的Gemma 3和OpenAI的GPT-4o Mini等主流专有模型。 1、核心特性与优势 多…...

SpringBoot3实战(SpringBoot3+Vue3基本增删改查、前后端通信交互、配置后端跨域请求、数据批量删除(超详细))(3)

目录 一、从0快速搭建SpringBoot3工程、SpringBoot3集成MyBatis、PageHelper分页查询的详细教程。(博客链接) 二、实现前端与后端通信对接数据。(axios工具) &#xff08;1&#xff09;安装axios。(vue工程目录) &#xff08;2&#xff09;封装请求工具类。(request.js) <1&…...

LabVIEW发电平台数据采集系统

本文详细介绍了基于LabVIEW的摇臂式波浪发电平台数据采集系统的设计与实现。通过整合LabVIEW软件与多种传感器技术&#xff0c;本系统能够有效提升数据采集的准确性和效率&#xff0c;为波浪能的利用和发电设备的优化提供科学依据。 ​ 项目背景 随着全球能源需求增长和环境保…...

人工智能(AI)系统化学习路线

一、为什么需要系统化学习AI&#xff1f; 人工智能技术正在重塑各行各业&#xff0c;但许多初学者容易陷入误区&#xff1a; ❌ 盲目跟风&#xff1a;直接学习TensorFlow/PyTorch&#xff0c;忽视数学与算法基础。 ❌ 纸上谈兵&#xff1a;只看理论不写代码&#xff0c;无法解…...

oracle事务的组成

1)数据库事务由以下的部分组成: 一个或多个DML 语句 ; 一个 DDL(Data Definition Language – 数据定义语言) 语句&#xff1b; 一个 DCL(Data Control Language – 数据控制语言)语句&#xff1b; 2)事务的执行开始&#xff1a; 以第一个 DML 语句的执行作为开始 &#xff0c;…...

第二十八篇 数据获取与数据分析:数仓体系下的专业化分工与协同

声明&#xff1a;文章内容仅供参考&#xff0c;需仔细甄别。文中技术名称属相关方商标&#xff0c;仅作技术描述&#xff1b;代码示例为交流学习用途&#xff0c;部分参考开源文档&#xff08;Apache 2.0/GPLv3&#xff09;&#xff1b;案例数据已脱敏&#xff0c;技术推荐保持…...

SpringSecurity——前后端分离登录认证

SpringSecurity——前后端分离登录认证的整个过程 前端&#xff1a; 使用Axios向后端发送请求 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><title>登录</title><script src"https://cdn…...

【AI】Orin Nano+ubuntu22.04上移植YoloV11,并使用DeepStream测试成功

1、准备工作 使用 sdk-manager 烧写 OrinNano, JetPack版本为6.0 DP,对应操作系统为:Ubuntu22.04 参见博客:【NVIDIA】Jetson Orin Nano系列:烧写Ubuntu22.04 2、安装 PyTorch 2.1 下载依赖 1)安装onnx pip install onnx -i https://pypi.tuna.tsinghua.edu.cn/sim…...