【Linux】应用层自定义协议与序列化

news2024/9/22 7:29:28

一、应用层

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

1.1 再次认识协议

       协议是一种约定,是通信双方约定的一种数据结构。在之前写的UDP服务器和TCP服务器中,在读取数据的时候,都是按照“字符串”的方式来发送接收;如果我们想要发送的数据是结构化数据,怎么办?首先,要让通信双方约定好一种协议,创建一种数据结构使得通信双方都认识。

协议就是双方约定好的结构化的数据!!!!!!!

1.2 序列化和反序列化(重要)

       在网络通信中,通信双方会根据一种协议进行通信,这种协议一般都是一种数据结构,数据结构在网络中传输是比较困难的,进行传送是不方便的。因此在网络通信中,我们需要将数据进行过抽象成为一条数据,即字符串,进行发送;当接受方接受到这条字符串后,我们可以根据其抽象的过程将其还原为原本的数据结构的信息。

       通过上述的图片,第一层中表示的是应用层,应用层中负责的是创建消息的数据结构,消息由几部分构成,分别表示什么含义;在传输层中,通过序列化将消息由多变为一,方便网络发送;在网络通信中,网络只需要把其看成字节流即可。在通过网络传输到接收方时,再次通过接收方的协议栈,将字节流从下层依次传输到上层,在传输层中,通过反序列化将消息由一变多,方便上层处理。

       在这个过程中,其实也体现了网络分层的好处,进行解耦合,让网络各层完成各自的任务,不会打扰到其他层次。

jsoncpp序列化和反序列化

序列化函数:

思路:

       我们需要先实例化一个工厂类对象,通过一个工厂类对象来生产派生类对象,然后将json类型的数据写入字符串类型中,完成序列化。

代码:

// 实现数据的序列化
bool serialize(Json::Value &val, std::string &body)
{
    std::stringstream ss;
    // 实例化一个工厂类对象
    Json::StreamWriterBuilder swb;
    // 通过一个工厂类对象来生产派生类对象
    std::unique_ptr<Json::StreamWriter> sw(swb.newStreamWriter());
    // 将json类型写入字符串类型,完成序列化
    int ret = sw->write(val, &ss);

    if(ret != 0)
    {
        std::cout << "json error" << std::endl;
        return false;
    }

    body = ss.str();

    return true;
}
反序列化函数:

思路:与序列化思路相同,也是要通过工厂来产生派生类对象,然后将字符串类型转换为json类型的数据,完成反序列化。

代码:

// 实现json字符串的反序列化
bool unserialize(const std::string &body, Json::Value &val)
{
    // 实例化工厂类
    Json::CharReaderBuilder crb;
    // 生产CharReader对象
    std::string errs;
    std::unique_ptr<Json::CharReader> cr(crb.newCharReader());

    bool ret = cr->parse(body.c_str(), body.c_str() + body.size(), &val, &errs);

    if(ret == false)
    {
        std::cout << "Json unserialize error:" << errs << std::endl;
        return false;
    }
    return true;
}

1.3 重新理解read,write,recv,send和tcp为什么支持全双工??

       在理解完序列化和反序列后,将其与网络层结合在一起。在上述的IO系统调用中,会在传输层中创建出两个缓冲区:接受缓冲区和发送缓冲区。将序列化的消息从应用层传输给传输层,本质就是将序列化的消息拷贝到缓冲区中,由缓冲区重新向下层传输,通过网络传输给接收方的协议栈,重新从数据链路层传输到上层。

       tcp是传输控制协议,tcp决定了数据什么时候发送,数据怎么发送,数据发送出错了怎么办?由于在tcp中每一个对象都有一个接受缓冲区和发送缓冲区,因此一个套接字既可以读,也可以写,所以tcp支持全双工,本质就是因为发送方和接收方都有一对发送缓冲区和接受缓冲区。

       因为tcp层和ip层都在操作系统中,所以双方的通信是由操作系统完成的。tcp层发送数据的本质就是:将自己的发送缓冲区拷贝到接收方的接受缓冲区。通信的本质就是拷贝!!!

       在接受数据的时候,由于tcp协议接受的是字节流,所以,我们还需要解决粘包问题,保证我们接受的是一个完整的请求。

二、封装Socket

       在之前的博客中,由于创建套接字的流程是固定的,并且为了使得其具有开闭原则,所以我们将socket单独拿出来进行封装成一个类。

       在封装这个类时,我们所使用的是模版方法模式。在这个模式下,我们采用多态的方式将TcpSocket和UdpSocket进行分别的封装。

2.1 基类——Socket

       在Socket类中,创建套接字,绑定套接字,监听套接字,接收数据,连接函数是通用的,在TcpSocket和UdpSocket中已经全部包含了,我们只需选择套接字中所需要的函数即可。

    class Socket
    {
    public:
        virtual void CreateSocketOrDie() = 0;             // 创建套接字
        virtual void BindSocketOrDie(InetAddr &addr) = 0; // 绑定套接字
        virtual void ListenSocketOrDie() = 0;             // 监听套接字
        virtual socket_sptr Accepter(InetAddr *addr) = 0; // 接受数据
        virtual bool Connector(InetAddr &addr) = 0;       // 连接
    public:
        // 创建套接字的过程是比较固定的
        // 创建监听套接字
        void BuildListenSocket(InetAddr &addr)
        {
            CreateSocketOrDie();
            BindSocketOrDie(addr);
            ListenSocketOrDie();
        }

        // 创建客户端套接字
        bool BuildClientSocket(InetAddr &addr)
        {
            CreateSocketOrDie();
            return Connector(addr);
        }
    };

2.2 子类——TcpSocket

    class TcpSocket : public Socket // 公有继承
    {
    public:
        TcpSocket(int sockfd) : _sockfd(sockfd) {}
        void CreateSocketOrDie() override // 创建套接字
        {
            _sockfd = ::socket(AF_INET, SOCK_STREAM, 0);
            if (_sockfd < 0)
            {
                LOG(FATAL, "sockfd create failed!\n");
                exit(SOCKET_ERROR);
            }
            LOG(DEBUG, "sockfd create success!\n");
        }
        void BindSocketOrDie(InetAddr &addr) override // 绑定套接字    
                            // 传入的是服务端自己的详细地址
        {
            struct sockaddr_in local;
            memset(&local, 0, sizeof(local));
            local.sin_family = AF_INET;
            local.sin_addr.s_addr = inet_addr(addr.Ip().c_str());
            local.sin_port = htons(addr.Port());

            int n = ::bind(_sockfd, (struct sockaddr *)&addr, sizeof(addr));
            if (n < 0)
            {
                LOG(FATAL, "bind sockfd failed!\n");
                exit(BIND_ERROR);
            }
            LOG(DEBUG, "Bind sockfd success!\n");
        }
        void ListenSocketOrDie() override // 监听套接字
        {
            int n = ::listen(_sockfd, backlog);
            if (n < 0)
            {
                LOG(FATAL, "listen sockfd failed!\n");
                exit(LISTEN_ERROR);
            }
            LOG(DEBUG, "listen sockfd success!\n");
        }
        socket_sptr Accepter(InetAddr *addr) override // 接受数据 // 为什么将
        {
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            int sockfd = ::accept(_sockfd, (struct sockaddr *)&peer, &len);
            if (sockfd < 0)
            {
                LOG(WARNING, "accept error...\n");
                return nullptr;
            }
            *addr = peer;
            socket_sptr sock = std::make_shared<TcpSocket>(sockfd);
            return sock;
        }
        bool Connector(InetAddr &addr) override // 连接
                       // 传入的是服务器的详细地址
        {
            struct sockaddr_in server;
            memset(&server, 0, sizeof(server));
            server.sin_family = AF_INET;
            server.sin_port = htons(addr.Port());
            server.sin_addr.s_addr = inet_addr(addr.Ip().c_str());

            int n = ::connect(_sockfd, (struct sockaddr *)&server, sizeof(server));
            if (n < 0)
            {
                std::cerr << "connect error..." << std::endl;
                return false;
            }
            return true;
        }

2.3 公共变量

class Socket;
const static int backlog = 16;
using socket_sptr = std::shared_ptr<Socket>;

2.4 陌生函数

2.4.1 listen函数

函数的原型:

函数的功能:

  listen 函数的主要功能是将套接字设置为被动模式,以便它可以接收传入的连接请求。调用 listen 后,套接字会进入“监听”状态,等待客户端发起连接请求。

函数的参数:

  • sockfd: 套接字描述符。这个描述符是之前通过 socket 函数创建的,并且在调用 bind 函数后用来标识套接字。

  • backlog: 指定在套接字上允许的最大未完成连接队列的长度。这个队列用于存放尚未被 accept 函数接受的连接请求。当连接请求的数量超过这个值时,新连接请求可能会被拒绝或忽略。

函数的返回值:

  • 成功: 如果成功,返回 0
  • 失败: 如果调用失败,返回 -1,并设置 errno 以指示错误原因。常见的错误代码包括 EBADF(无效的套接字描述符)、ENOTSOCK(描述符不是一个套接字)、EADDRINUSE(地址已被使用)等。

2.4.2 accept函数

函数的原型:

函数的功能:

   accept 函数的主要功能是从已监听的套接字中接受一个传入的连接请求,创建一个新的套接字用于与客户端进行通信。调用 accept 函数后,服务器可以通过返回的新套接字与客户端交换数据。

函数的参数:

  • sockfd: 已监听的套接字描述符,即之前通过 socket 函数创建并通过 bindlisten 函数设置好的套接字。

  • addr: 指向 sockaddr 结构体的指针,用于存储客户端的地址信息。如果不需要地址信息,可以传入 NULL

  • addrlen: 指向 socklen_t 类型的变量的指针,该变量表示 addr 参数所指向的地址结构体的大小。accept 函数会更新这个变量以反映实际的地址长度。如果 addrNULL,这个参数可以被忽略。

函数的返回值:

  • 成功: 返回一个新的套接字描述符,这个描述符用于与客户端进行通信。新套接字是与连接相关的独立套接字,且继承了原套接字的属性。

  • 失败: 返回 -1,并设置 errno 以指示错误原因。常见的错误代码包括 EBADF(无效的套接字描述符)、EINTR(调用被中断)、ENOTSOCK(描述符不是一个套接字)、EOPNOTSUPP(套接字类型不支持)等。

2.4.3 connect函数

函数的原型:

函数的功能:

  connect 函数用于在客户端程序中与远程服务器建立连接。当客户端想要与服务器通信时,它使用 connect 函数来请求连接。成功调用 connect 后,客户端与服务器之间就可以进行数据传输了。

函数的参数:

  • sockfd: 这是一个套接字文件描述符,它是通过 socket 函数创建的。这个套接字用于建立连接。

  • addr: 这是一个指向 struct sockaddr 结构体的指针,它包含了要连接的远程主机的地址信息。这个结构体的具体类型通常取决于协议族,例如 struct sockaddr_in 用于 IPv4。

  • addrlen: 这是 addr 指向的地址结构的大小,以字节为单位。通常,可以使用 sizeof(struct sockaddr_in) 来获得这个大小。

函数的返回值:

  • 成功: 返回 0 表示连接成功。
  • 失败: 返回 -1,并且设置 errno 以指示错误原因。例如,errno 可能会被设置为 ECONNREFUSED 表示目标主机拒绝连接,或 ETIMEDOUT 表示连接超时等。

三、自定义网络协议

       在应用层中,我们解释了序列化和反序列化的操作,接下来,我们需要将请求和响应进行封装。根据我们自定义协议来进行序列化。

        下面,我们就以一个有关计算器的请求和响应的数据结构,为了解决粘包问题,我们需要先设计一下传递的报文格式:报头 + 有效载荷。报头中存放的是有效载荷的长度,有效载荷存放的是正文内容。

3.1 简单介绍一下Json

3.1.1 Jsoncpp

       在之后,我们有可能在肝出protobuf的博客,所以在这里简单介绍一下Json,并且可以使用Json实现序列化和反序列化的操作函数。

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

3.1.2 Jsoncpp特性

简单易用:Jsoncpp提供了直观的API,使得处理JSON数据变得简单

高性能:jsoncpp的性能经过优化,能够高效地处理大量的JSON数据

全面支持:支持JSON标准中的所有数据类型,包括对象、数组。字符串。数字、布尔值和null

错误处理:在解析JSON数据时,Jsoncpp提供了详细的错误信息和位置,方便开发者调试。

3.1.3 安装Jsoncpp

ubuntu:sudo apt-get install libjsoncpp-dev
Centos: sudo yum install jsoncpp-devel

3.1.4 序列化

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

#include <iostream>
#include <string>
#include <sstream>
#include <memory>
#include <jsoncpp/json/json.h>

int main()
{
    Json::Value root;
    root["name"] = "小名";
    root["age"] = "14";
    Json::FastWriter writer;
    std::string s = writer.write(root);
    std::cout << s << std::endl;
    return 0;
}

3.1.5 反序列化

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

3.2 协议类

       在这个类中,我们定义了请求类和响应类,我们需要将请求类和响应类进行序列化和反序列化。在完成了上面Jsoncpp简单的了解,我们现在可以轻松地写出来序列化和反序列化的函数。

3.2.1 请求类

    class Request
    {
    public:
        // 我们自定义协议
        // 报文 = 包头 + 有效载荷
        // LV格式  固定字段长——后续字符串的长度  正文内容\n\t
        // 对报文进行分析
        // "len\r\n"_x_op_y\r\n"
        Request()
        {
        }
        Request(int x, int y, char oper)
            : _x(x), _y(y), _oper(oper)
        {
        }

        bool Serialize(std::string *out) // 序列化
        {
            Json::Value root;
            root["x"] = _x;
            root["y"] = _y;
            root["oper"] = _oper;

            Json::FastWriter writer;
            *out = writer.write(root);
            return true;
        }

        bool Deserialize(std::string &in) // 反序列化
        {
            Json::Value root;
            Json::Reader reader;
            bool ret = reader.parse(in, root);
            _x = root["x"].asInt();
            _y = root["y"].asInt();
            _oper = root["oper"].asInt();
            return ret;
        }

        ~Request()
        {
        }

    private:
        int _x;
        int _y;
        char _oper; // "+-*/%"
    };

3.2.2 响应类

    class Response
    {
    public:
        Response()
        {
        }
        Response(int result, int code)
            : _result(result), _code(code)
        {
        }

        bool Serialize(std::string *out) // 序列化
        {
            Json::Value root;
            root["result"] = _result;
            root["code"] = _code;

            Json::FastWriter writer;
            *out = writer.write(root);
            return true;
        }

        bool Deserialize(std::string &in) // 反序列化
        {
            Json::Reader reader;
            Json::Value root;
            bool ret = reader.parse(in, root);
            _result = root["result"].asInt();
            _code = root["code"].asInt();
            return ret;
        }

        ~Response()
        {
        }

    private:
        int _result;
        int _code;
    };

3.2.3 解决粘包问题

       为了能够读出一份完整的报文,我们可以精心设计发送报文的格式——LV格式。报文等于报头加有效载荷,报头中存在的是有效载荷的长度,我们可以使用一些字符串作为分割报头和有效载荷。怎么读取出一份完整的报文呢??我们可以通过分割符来获取报头。我们首先在字符串中查找第一个出现的分隔符,这个分割符的前面一定是报头,然后再进行分割出报头,读取出有效载荷的长度,然后计算出一个完整的报文的长度,最后将整个报文截取出来即可。

    // 处理粘包问题
    std::string Decode(const std::string &inbuffer)
    {
        // 先找出SEP,然后截取出来len的长度,最后将完整报文截取出来
        auto pos = inbuffer.find(SEP);
        if (pos == std::string::npos)
            return "";
        std::string len_str = inbuffer.substr(0, pos);
        if (len_str.empty())
            return "";
        int len = std::stoi(len_str);
        int total = len + len_str.size() + SEP.size() * 2;
        if (inbuffer.size() < total)
            return "";

        std::string package = inbuffer.substr(pos + SEP.size(), len);
        inbuffer.erase(0, total); // yichu 
        return package;
    }

3.2.4 拼装报文

    // 进行拼装报文
    std::string Eecode(const std::string &json_str)
    {
        int json_str_len = json_str.size();
        std::string proto_str = std::to_string(json_str_len);
        proto_str += SEP;
        proto_str += json_str;
        proto_str += SEP;
        return proto_str;
    }

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

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

相关文章

C++和Python混合编程——C++调用Python入门

大纲 代码结构初始化 Python 解释器获取 GIL为什么需要 GIL&#xff1f;GIL 的影响 导入 Python 模块并执行代码释放 GIL终止 Python 解释器 完整代码编译执行结果项目地址 在《C和Python混合编程——Python调用C入门》一文中&#xff0c;我们熟悉了Python调用C编译的动态库的方…

UE5 C++ 读取图片插件(一)

原来UE可以使用 static,之前不知道&#xff0c;一用就报错。 static TSharedPtr<IImageWrapper> GetImageWrapperByExtention(const FString InImagePath); //智能指针&#xff0c;方便追寻引用C,加载ImageWrapperstatic UTexture2D* LoadTexture2D(const FString& …

算法-滑动窗口技巧

文章目录 基础理论介绍长度最小的子数组无重复字符的最长字串解法1 : 哈希表计数逐步缩进解法2 : 哈希表更新下标跳跃缩进 最小覆盖字串替换子串获得平衡字符串K个不同整数的子数组 基础理论介绍 1. 滑动窗口简介 : 滑动窗口其实就是维持了一段区间(l边界与r边界), 并且对于这…

C++:构造函数、析构函数

目录 一、类的默认成员函数 二、构造函数 构造函数的特点 三、析构函数 析构函数的特点 一、类的默认成员函数 默认成员函数就是用户没有显式实现&#xff0c;编译器会自动生成的成员函数称为默认成员函数&#xff0c;一个类&#xff0c;我们不写的情况下编译器会默认生成…

Unity(2022.3.41LTS) - 动画融合术

目录 一、动画融合的概念 二、动画融合的类型 三、动画融合的实现方法 1.简介 2.创建新的图层 3.创建遮罩 4.遮罩配置 5.预览效果 6.使用代码灵活控制 7.全部代码 四、动画融合的优化和注意事项 一、动画融合的概念 在 Unity 中&#xff0c;动画融合是一种强大的技…

安装 Let‘s Encrypt certbot 生成多个域名免费 https 证书实录(linux pip 方式)

本文记录了我在华为云 EulerOS linux 云主机使用 python pip 方式安装配置 Let’s Encrypt certbot, 并为我的网站的多个域名生成免费 https 证书的整个过程, 包括 python 环境配置, 下载 certbot 及 certbot-nginx, 一次性生成多个域名的证书及注意事项, 以及最后配置 certbot…

5G农业大数据中心顶层设计

1. 政策背景与规划 国家政策大力推动大数据发展和应用&#xff0c;特别是农业农村信息化发展规划&#xff0c;强调数字化在农业现代化中的关键作用。《数字农业农村发展规划&#xff08;2019-2025年&#xff09;》明确了农业农村数字化的重要性。 2. 国际农业大数据现状 美国…

超图iServer 11i在Java中调用REST接口点线面增删改查方法

一、引入iServer的Jar包 需要到iServer安装目录/webapps/iserver/WEB-INF/lib下寻找以下几个jar包&#xff0c;把它引入到项目里&#xff1a; iserver-all-*.jar service-model-*.jar rest-sdk-*.jar然后再引入几个maven依赖包&#xff1a; <dependency><groupId&g…

MyBaits 二级缓存原理

优质博文&#xff1a;IT-BLOG-CN 一级缓存原理 默认关闭&#xff0c;一般不建议使用。为什么不建议使用我们要清楚。 先给不建议使用的原因&#xff1a; MyBatis的二级缓存是和命名空间绑定的&#xff0c;所以通常情况下每一个Mapper映射文件都拥有自己的二级缓存&#xff0c;…

关于谷歌账号的三个“错误的”问题:谷歌有客服吗?登录不了的账号如何注销?登录不了的账号绑定的手机还能注册新账号吗?

这段时间GG账号服务收到很多朋友的反馈&#xff0c;其中有一些具有典型的问题&#xff0c;而且是错误的问题——主要是对谷歌账号或者谷歌账号使用的误解&#xff0c;从而浪费了时间&#xff0c;或者走了弯路&#xff0c;或者反复试错给账号带来了更大的风险。 今天就来给大家…

Spring 框架下 Redis 数据结构的全面解析

Hello , 大家好 , 这个专栏给大家带来的是 Redis 系列 ! 本篇文章给大家带来的是如何通过 Spring 来操作 Redis 中的常见数据结构 , 以及如何通过代码执行 Redis 中的原生命令 . 本专栏旨在为初学者提供一个全面的 Redis 学习路径&#xff0c;从基础概念到实际应用&#xff0c;…

【C++题解】1088 - 求两个数M和N的最大公约数

问题四&#xff1a;1088 - 求两个数M和N的最大公约数 类型&#xff1a;需要找规律的循环。 题目描述&#xff1a; 求两个正整整数 M 和 N 的最大公约数(M&#xff0c;N都在长整型范围内&#xff09; 输入&#xff1a; 输入一行&#xff0c;包括两个正整数。 输出&#xff…

Antv a-table 表格行/列合并,在合并后的td中使用插槽slot

【需求】 这次的表格需要实现行列合并&#xff0c;并且要在合并后的 td 中使用子组件或弹出弹窗&#xff0c;难点在于&#xff1a; 1. 根据提供的data&#xff0c;自行判断是否合并项的 getRowspan方法 2. customCell 、scopedSlots 冲突导致的子组件无法展示 &#xff08…

Cesium 实战 - 自定义纹理材质 - 流动线(精灵线)

Cesium 实战 - 自定义纹理材质 - 流动线(精灵线) 核心代码完整代码在线示例Cesium 给实体对象(Entity)提供了很多实用的样式,基本满足普通项目需求; 但是作为 WebGL 引擎,肯定不够丰富,尤其是动态效果样式。 对于实体对象(Entity),可以通过自定义材质,实现各种动…

【YOLOv8系列】YOLOv8的GUI界面设计;在电脑本地实现YOLOv8的可视化交互界面设计(对摄像头的实时画面进行分类)

背景: 最近在研究YOLOv8的应用,并且已经在自己的笔记本环境中跑通了YOLOv8的检测和分类算法,训练、验证、预测等功能均已实现;也通过自己的数据集训练出了自己的模型(权重);且之前也做了一个webUI界面,对YOLOv8检测和分类的结果进行展示;但是如果在本地的GUI界面调用摄…

Python pip 更换镜像源

文章目录 1 概述1.1 默认镜像&#xff0c;速度慢&#xff0c;易报错1.2 常用国内镜像源 2 更改镜像源2.1 临时更改2.2 永久更改2.2.1 查看配置源及配置文件2.2.2 编辑 pip.ini2.2.3 配置后的效果 1 概述 1.1 默认镜像&#xff0c;速度慢&#xff0c;易报错 默认镜像&#xff…

导出硬盘所有文件名到txt文本文件——C#学习笔记

下面的示例演示如何使用递归遍历目录树。递归方法很简洁&#xff0c;但如果目录树很大且嵌套很深&#xff0c;则有可能会引起堆栈溢出异常。 对于所处理的特定异常以及在每个文件和文件夹上执行的特定操作&#xff0c;都只是作为示例提供。您应该修改此代码来满足自己特定的需…

分类学习器(Classification Learner App)MATLAB

在MATLAB中&#xff0c;分类学习器用于构建和评估分类模型。MATLAB提供了一些工具和功能&#xff0c;帮助你进行分类任务&#xff0c;例如分类学习器应用程序、统计和机器学习工具箱中的函数等。 导入数据 我们在打开应用程序之前的第一步将是导入我们将在工作区使用的数据。…

新品上市丨科学级新款制冷相机sM4040A/sM4040B

sM4040B科学级显微制冷相机 特性 sM4040B搭载了 GSENSE4040BSI 3.2 英寸图像传感器&#xff0c;针对传感器固有的热噪声&#xff0c;专门设计了高效制冷模块&#xff0c;使得相机传感器的工作温度比环境温度低达 35-40 度。针对制冷相机常见的低温结雾现象设计了防结雾机制&a…

Serverless 应用引擎 SAE 助力袋拉拉研发提效 70%

作者&#xff1a;百潼 医院环保 IOT 设备的引领者&#xff1a;机汽猫 机汽猫是⼀家致⼒于通过投放⾃助取袋设备&#xff0c;为医院场景提供新型环保袋交付⽅式的科技公司。它成⽴于 2019 年&#xff0c;旗下品牌袋拉拉&#xff08;DaiLala&#xff09;通过投放⾃助取袋机&…