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

news2025/3/20 18:05:50

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

  • 一.应用层
    • 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_tcp

client_tcp:TcpClient.cc
	g++ -o $@ $^ -std=c++17 -ljsoncpp
server_tcp:TcpServer.cc
	g++ -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;            // 进程pid
            std::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>

#define Die(code)   \
    do              \
    {               \
        exit(code); \
    } while (0)

#define CONV(v) (struct sockaddr *)(v)

enum 
{
    USAGE_ERR = 1,
    SOCKET_ERR,
    BIND_ERR,
    LISTEN_ERR
};

8.InetAddr.hpp

#pragma once

#include <iostream>
#include <string>

#include <sys/types.h>
#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()); }

private:
    struct sockaddr_in _net_addr;
    std::string _ip; // 主机序列: IP
    uint16_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 8

uint16_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); // 系统提供的创建守护进程
    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_tcp
   PPID     PID    PGID     SID TTY        TPGID STAT   UID   TIME COMMAND
      1 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_tcp

xzy@hcss-ecs-b3aa:~$ ./client_tcp 127.0.0.1 8080
input x: 100
input y: 200
input oper: +
300[0]
...

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

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

相关文章

Matlab 雷达导引头伺服系统的建模与仿真研究

1、内容简介 Matlab 177-雷达导引头伺服系统的建模与仿真研究 可以交流、咨询、答疑 2、内容说明 略[摘 要]基于 Malah/Simuink 雷达导引|头同服系统的建模与仿真&#xff0c;首先对雷达导引头同服系统按照预定回路和跟踪回路的步骤分别进行建模以及相关控制参数计算,接着构建…

华为ipd流程华为流程体系管理华为数字化转型流程数字化管理解决方案介绍81页精品PPT

华为流程体系最佳实践主要包括构建完善的流程框架&#xff0c;明确各层级流程要素与职责&#xff0c;梳理涵盖研发、采购、营销、服务、资产管理等多领域的流程&#xff0c;通过梳理业务场景和核心能力搭建差异化流程框架&#xff0c;采用自上而下与自下而上相结合的建模方法&a…

网络流基本概念及实现算法

基本概念 流网络 对于一个有向图, 抽象成水管里的水的模型, 每根管子有容量限制, 计为 G ( V , E ) G (V, E) G(V,E), 首先不考虑反向边 对于任意无向图, 都可以将反向边转化为上述形式 如果一条边不存在, 定义为容量为 0 0 0, 形式上来说就是 c ( u , v ) 0 c(u, v) 0 c(…

SpringBoot对接DeepSeek

文章目录 Spring Boot 集成 DeepSeek API 详细步骤1. 创建API Key1.访问 [DeepSeek控制台](https://platform.deepseek.com/usage) 并登录。2.点击 Create API Key 生成新密钥。3.复制并保存密钥&#xff08;需在Spring Boot配置文件中使用&#xff09;。 2. 创建Spring Boot工…

大语言模型的多垂类快速评估与 A/B 测试

简介 行业领先的模型构建企业携手澳鹏&#xff08;Appen&#xff09;开展了一项极具挑战性的项目。针对 3 至 6 个大型语言模型&#xff08;LLM&#xff09;&#xff0c;在广泛的通用领域及复杂专业领域&#xff08;如医疗保健、法律、金融、编程、数学和汽车行业等&#xff0…

RAGFlow + LlamaIndex 本地知识库RAG增强架构与实现直播智能复盘

一、需求分析与架构设计 基于 RAGFlow LlamaIndex 本地知识库RAG 扩展直播话术合规与复盘系统&#xff0c;需构建 实时流处理、多模态合规引擎、智能复盘分析 三层能力。以下是完整架构图与技术方案&#xff1a; 二、核心模块技术方案 1. 直播流实时处理&#xff08;输入层→…

阿里云平台服务器操作以及发布静态项目

目录&#xff1a; 1、云服务器介绍2、云服务器界面3、发布静态项目1、启动nginx2、ngixn访问3、外网访问测试4、拷贝静态资源到nginx目录下并重启nginx 1、云服务器介绍 2、云服务器界面 实例详情&#xff1a;里面主要显示云服务的内外网地址以及一些启动/停止的操作。监控&…

【大模型实战篇】使用GPTQ量化QwQ-32B微调后的推理模型

1. 量化背景 之所以做量化&#xff0c;就是希望在现有的硬件条件下&#xff0c;提升性能。量化能将模型权重从高精度&#xff08;如FP32&#xff09;转换为低精度&#xff08;如INT8/FP16&#xff09;&#xff0c;内存占用可减少50%~75%。低精度运算&#xff08;如INT8&#xf…

基于springboot医疗平台系统(源码+lw+部署文档+讲解),源码可白嫖!

摘要 信息化时代&#xff0c;各行各业都以网络为基础飞速发展&#xff0c;而医疗服务行业的发展却进展缓慢&#xff0c;传统的医疗服务行业已经逐渐不满足民众的需求&#xff0c;有些还在以线下预约挂号的方式接待病人&#xff0c;为此设计一个医疗平台系统很有必要。此类系统…

Stable Diffusion lora训练(一)

一、不同维度的LoRA训练步数建议 2D风格训练 数据规模&#xff1a;建议20-50张高质量图片&#xff08;分辨率≥10241024&#xff09;&#xff0c;覆盖多角度、多表情的平面风格。步数范围&#xff1a;总步数控制在1000-2000步&#xff0c;公式为 总步数 Repeat Image Epoch …

网络空间安全(37)获取webshell方法总结

一、直接上传获取Webshell 这是最常见且直接的方法&#xff0c;利用网站对上传文件的过滤不严或存在漏洞&#xff0c;直接上传Webshell文件。 常见场景&#xff1a; 许多PHP和JSP程序存在此类漏洞。例如&#xff0c;一些论坛系统允许用户上传头像或心情图标&#xff0c;攻击者可…

第十三次CCF-CSP认证(含C++源码)

第十三次CCF-CSP认证 跳一跳满分题解 碰撞的小球满分题解遇到的问题 棋局评估满分题解 跳一跳 题目链接 满分题解 没什么好说的 基本思路就是如何用代码翻译题目所给的一些限制&#xff0c;以及变量应该如何更新&#xff0c;没像往常一样给一个n&#xff0c;怎么读入数据&…

swagger ui 界面清除登录信息的办法

我们在开发过程中&#xff0c;用swagger ui 测试接口的时候&#xff0c;可能会要修改当前登录的用户。 但是如果我们在谷歌中对调试的本地swagger ui 登录地址存储过账户密码&#xff0c;每次启动项目调试之后&#xff0c;都会自动登录swagger ui &#xff0c;登录界面一闪就…

TensorFlow 的基本概念和使用场景

TensorFlow 是一个由 Google 开发的开源机器学习框架&#xff0c;主要用于构建和训练深度学习模型。下面是一些 TensorFlow 的基本概念和使用场景&#xff1a; 基本概念&#xff1a; 张量&#xff08;Tensor&#xff09;&#xff1a;在 TensorFlow 中&#xff0c;数据以张量的…

基于x11vnc的ubuntu远程桌面

1、安装VNC服务 sudo apt install x11vnc -y2、创建连接密码 sudo x11vnc -storepasswd3、安装lightdm服务 x11vnc 在 默认的 GDM3 中不起作用&#xff0c;因此需要使用 lightdm 桌面管理环境 sudo apt install lightdm -y切换至lightdm&#xff0c;上一步已经切换则跳过该…

Cursor解锁Claude Max,助力AI编程新突破!

Cursor 最新推出的 Claude Max 模型&#xff0c;以其卓越的性能和创新的能力&#xff0c;正在重新定义我们对 AI 辅助编程的认知。这款搭载 Claude3.7 大脑的超级模型&#xff0c;不仅具备超强智能&#xff0c;还凭借一系列技术突破&#xff0c;向传统 AI 编程工具发起了挑战。…

ESP8266 与 ARM7 接口-LPC2148 创建 Web 服务器以控制 LED

ESP8266 与 ARM7 接口-LPC2148 创建 Web 服务器以控制 LED ESP8266 Wi-Fi 收发器提供了一种将微控制器连接到网络的方法。它被广泛用于物联网项目,因为它便宜、体积小且易于使用。 在本教程中,我们将 ESP8266 Wi-Fi 模块与 ARM7-LPC2148 微控制器连接,并创建一个 Web 服务…

通过C#脚本更改材质球的参数

// 设置贴图Texture mTexture Resources.Load("myTexture", typeof(Texture )) as Texture;material.SetTexture("_MainTex", mTexture );// 设置整数material.SetInt("_Int", 1);// 设置浮点material.SetFloat("_Float", 0.1f);// 设…

FPGA管脚约束

目录 前言 一、IO约束 二、延迟约束 前言 IO约束包括管脚约束和延迟约束。 一、IO约束 对管脚进行约束&#xff0c;对应的约束语句&#xff1a; set_property -dict {PACKAGE_PIN AJ16 IOSTANDARD LVCMOS18} [get_ports "led[0]" ] 上面是单端的管脚&…

实现前端.ttf字体包的压缩

前言 平常字体包都有1M的大小&#xff0c;所以网络请求耗时会比较长&#xff0c;所以对字体包的压缩也是前端优化的一个点。但是前端如果想要特点字符打包成字体包&#xff0c;网上查阅资料后&#xff0c;都是把前端代码里面的字符获取&#xff0c;但是对于动态的内容&#xf…