rinetd 端口转发工具技术原理

rinetd 端口转发工具技术原理

Refer :
deepwiki.com@liulilittle/rinetd
github.com@liulilittle/rinetd

一、工具概述

rinetd 是一款基于 C++ 开发的高性能端口转发工具,其核心设计理念围绕简洁性高效性展开。工具的主要功能是在不同网络端点之间透明地转发 TCP 和 UDP 流量,实现网络连接的桥接与重定向。

1.1 设计原则

rinetd设计原则
单进程架构
事件驱动模型
零拷贝转发
资源最小化
无锁并发
减少上下文切换
简化进程管理
异步I/O
回调机制
内存重用
缓冲区共享
固定内存分配
静态缓冲区
单线程处理
无竞争访问

二、系统组件

2.1 整体视图

rinetd 采用分层架构设计,各组件职责明确,耦合度低:
支持层
网络层
核心层
应用层
主程序入口
配置加载器
信号处理器
规则解析器
TCP转发服务
UDP转发服务
连接管理器
隧道管理器
Boost.Asio封装
原生Socket操作
日志系统
内存分配器
工具函数集

2.2 数据结构

2.2.1 网络地址表示
cpp 复制代码
typedef struct {
    bool        bv6;            // IPv6标识
    uint32_t    in4;            // IPv4地址(网络字节序)
    uint8_t     in6[16];        // IPv6地址
} ip_address;

这种设计实现了IPv4/IPv6的统一表示 ,通过bv6字段区分地址类型,in4in6共用内存空间,体现了C语言联合体的设计思想但更安全。

2.2.2 转发规则配置
cpp 复制代码
typedef struct {
    bool        tcp_or_udp;     // 协议类型:true=TCP, false=UDP
    ip_address  local_host;     // 本地监听地址
    uint16_t    local_port;     // 本地监听端口
    ip_address  remote_host;    // 远程目标地址
    uint16_t    remote_port;    // 远程目标端口
} listen_port;

typedef struct {
    std::vector<listen_port>    listen_ports;  // 转发规则列表
    std::string                 log_var;       // 日志文件路径
} rinetd_config;

配置结构体现了声明式编程思想,用户只需声明"什么"而不需关心"如何",系统自动处理转发逻辑。

三、配置系统实现原理

3.1 配置加载流程

配置存储 规则解析器 文件系统 路径解析器 命令行参数 配置存储 规则解析器 文件系统 路径解析器 命令行参数 阶段一:配置文件定位 alt [指定配置文件] [未指定配置文件] 阶段二:配置解析 alt [TCP转发规则] [UDP转发规则] [日志配置] 阶段三:配置验证与存储 传递argc, argv 解析-c/--conf-file参数 读取指定文件 生成默认路径 读取默认配置文件 提供配置文件内容 逐行解析 移除注释和空白 解析四元组:本地地址 端口/tcp 远程地址 端口/tcp 解析四元组:本地地址 端口/udp 远程地址 端口/udp 解析logfile路径 验证端口范围(1-65535) 验证IP地址有效性 存储有效规则 存储日志路径

3.2 配置解析实现

cpp 复制代码
static bool parse_config(std::vector<listen_port>& out_, std::string& log_path_, 
                        const std::string& config_str) noexcept {
    // 第一阶段:文本预处理
    std::vector<std::string> lines_;
    Tokenize<std::string>(config_str, lines_, "\r\n");
    
    for (size_t i = 0; i < lines_.size(); i++) {
        std::string& line_ = lines_[i];
        
        // 注释处理策略
        size_t comment_pos = line_.find_first_of('#');
        if (comment_pos != std::string::npos) {
            if (comment_pos == 0) continue;  // 整行注释
            line_ = line_.substr(0, comment_pos);  // 行内注释
        }
        
        // 空白字符规范化
        line_ = RTrim(LTrim(line_));
        if (line_.empty()) continue;
        
        // 第二阶段:模式匹配
        char local_host[128], remote_host[128];
        uint32_t local_port = 0, remote_port = 0;
        bool tcp_or_udp = false;
        
        // 协议类型识别算法
        line_ = ToLower(line_);
        if (sscanf(line_.data(), "%s %u/tcp %s %u/tcp", 
                  local_host, &local_port, remote_host, &remote_port) >= 4) {
            tcp_or_udp = true;  // TCP协议
        }
        else if (sscanf(line_.data(), "%s %u/udp %s %u/udp", 
                       local_host, &local_port, remote_host, &remote_port) >= 4) {
            tcp_or_udp = false;  // UDP协议
        }
        else if (sscanf(line_.data(), "logfile %s", local_host) >= 1) {
            // 日志配置处理
            log_path_ = std::string(local_host, strlen(local_host));
            continue;
        }
        else {
            continue;  // 未知格式
        }
        
        // 第三阶段:语义验证
        if ((local_port == 0 || local_port > 65535) || 
            (remote_port == 0 || remote_port > 65535)) {
            continue;  // 端口范围无效
        }
        
        // 地址解析与转换
        listen_port rule_;
        if (!parse_address(rule_.local_host, local_host) ||
            !parse_address(rule_.remote_host, remote_host)) {
            continue;  // 地址格式无效
        }
        
        // 规则组装
        rule_.tcp_or_udp = tcp_or_udp;
        rule_.local_port = local_port;
        rule_.remote_port = remote_port;
        
        out_.push_back(std::move(rule_));
    }
    
    return !out_.empty();  // 至少有一条有效规则
}

四、TCP 转发

4.1 TCP 连接管理

创建
引用
tcp_forward
-context_: io_context&
-config_: rinetd_config&
-forward_: listen_port&
-server_: ip::tcp::acceptor
-log_: stream_descriptor_ptr
+run() : bool
-accept_socket() : bool
tcp_connection
-forward_: tcp_forward_ptr
-timeout_: deadline_timer
-local_socket_: tcp::socket_ptr
-remote_socket_: tcp::socket
-local_buf[65536]: char
-remote_buf[65536]: char
+run() : bool
-socket_to_destination() : bool
-wirte_log(int) : void
-abort() : void

4.2 TCP 连接生命周期

4.2.1 连接建立过程
cpp 复制代码
// tcp_forward::accept_socket() - 监听循环
bool accept_socket() noexcept {
    std::shared_ptr<boost::asio::ip::tcp::socket> socket = 
        make_shared_object<boost::asio::ip::tcp::socket>(context_);
    
    server_.async_accept(*socket.get(), 
        [self, this, socket](boost::system::error_code ec_) noexcept {
            // 错误处理策略
            if (ec_ == boost::system::errc::operation_canceled) {
                return;  // 正常取消操作
            }
            
            if (ec_) {
                close_socket(*socket.get());
            } else {
                // 连接处理器创建
                std::shared_ptr<tcp_connection> connection_ = 
                    make_shared_object<tcp_connection>(self, socket);
                if (!connection_->run()) {
                    connection_->abort();
                }
            }
            // 继续监听新连接
            accept_socket();
        });
    
    return true;
}
4.2.2 数据双向转发
cpp 复制代码
// tcp_connection::socket_to_destination() - 零拷贝转发
bool socket_to_destination(
    boost::asio::ip::tcp::socket* socket, 
    boost::asio::ip::tcp::socket* to,
    char* buf) noexcept {
    
    // 异步接收数据
    socket->async_receive(boost::asio::buffer(buf, RINETD_BUFFER_SIZE), 
        [self, this, socket, to, buf](const boost::system::error_code& ec, uint32_t sz) noexcept {
            
            // 传输错误检测
            int by = std::max<int>(-1, ec ? -1 : sz);
            if (by < 1) {
                abort();  // 连接异常终止
                return;
            }
            
            // 异步发送数据
            boost::asio::async_write(*to, boost::asio::buffer(buf, sz), 
                [self, this, socket, to, buf](const boost::system::error_code& ec, uint32_t sz) noexcept {
                    if (ec) {
                        abort();  // 发送失败
                    } else {
                        // 继续下一轮数据接收
                        socket_to_destination(socket, to, buf);
                    }
                });
        });
    
    return true;
}

4.3 连接超时控制机制

cpp 复制代码
// tcp_connection::run() - 超时控制实现
bool run() noexcept {
    // 连接超时定时器设置
    timeout_.expires_from_now(boost::posix_time::seconds(RINETD_TCP_CONNECT_TIMEOUT));
    timeout_.async_wait([self, this](const boost::system::error_code& ec) noexcept {
        if (ec != boost::system::errc::operation_canceled) {
            abort();  // 超时强制终止
        }
    });
    
    // 异步连接远程端点
    remote_socket_.async_connect(connectEP, 
        [self, this](boost::system::error_code ec_) noexcept {
            if (ec_) {
                abort();  // 连接失败
                return;
            }
            
            // 连接成功,取消超时定时器
            try {
                boost::system::error_code ec;
                timeout_.cancel(ec);
            } catch (std::exception&) {}
            
            // 启动双向数据转发
            socket_to_destination(local_socket_.get(), &remote_socket_, local_socket_buf);
            socket_to_destination(&remote_socket_, local_socket_.get(), remote_socket_buf);
            
            // 记录连接日志
            if (forward_->log_ || !forward_->config_.log_var.empty()) {
                wirte_log(2);  // 连接建立日志
            }
        });
    
    return true;
}

五、UDP 转发

5.1 UDP 处理模型

UDP 转发采用单线程线性处理模型,这一设计的根本原因在于UDP协议的特性和内存访问模式:

5.1.1 内存访问模式
cpp 复制代码
// UDP转发内存布局
class udp_forward {
private:
    char* buf_;  // 关键设计:单缓冲区
    // ...
public:
    inline udp_forward(...) noexcept : ... {
        static char s_buf_[UINT16_MAX];  // 静态全局缓冲区
        buf_ = s_buf_;  // 所有实例共享同一缓冲区
    }
};

设计原理

  1. 内存局部性优化:单个缓冲区确保所有UDP数据包在缓存中连续存储
  2. 零内存竞争:单线程访问消除缓存行伪共享问题
  3. 确定性内存访问:可预测的内存访问模式提高CPU流水线效率
5.1.2 协议特性适配

UDP协议的无连接特性决定了其适合单线程处理:
线性处理优势
新会话
现有会话
UDP数据包
接收队列
包处理决策
创建隧道
复用隧道
隧道映射表
发送队列
目标服务器
无锁操作
顺序处理
状态一致

5.2 UDP 隧道管理机制

5.2.1 隧道数据结构
cpp 复制代码
class udp_tunnel {
private:
    std::shared_ptr<udp_forward>    owner_;
    boost::asio::ip::udp::socket    socket_;     // 独立出站socket
    boost::asio::ip::udp::endpoint  local_ep_;   // 客户端端点标识
    uint64_t                        last_ts_;    // 最后活动时间戳
};

设计特点

  • 端点隔离:每个客户端有独立的出站socket
  • 状态保持:维护客户端到目标的映射关系
  • 时间跟踪:支持会话超时清理
5.2.2 隧道生命周期管理

收到首包
隧道初始化
收发数据包
更新活动时间
定期检查
超时(72秒)
未超时
空闲状态
创建隧道
活动状态
数据交换
超时检测
清理隧道

5.3 UDP 线性处理

5.3.1 主接收循环
cpp 复制代码
// udp_forward::accept_socket() - 单线程事件循环
void accept_socket() noexcept {
    socket_.async_receive_from(
        boost::asio::buffer(buf_, UINT16_MAX),  // 使用共享缓冲区
        udp_ep_,  // 发送方端点信息
        [self, this](const boost::system::error_code& ec, uint32_t sz) noexcept {
            
            // 错误处理
            int by = std::max<int>(-1, ec ? -1 : sz);
            if (by < 1) {
                // 错误处理但不中断循环
                accept_socket();  // 继续处理下一个包
                return;
            }
            
            // 隧道查找或创建
            std::shared_ptr<udp_tunnel> tunnel_ = get_or_add_tunnel(udp_ep_);
            if (tunnel_) {
                // 通过隧道转发数据
                tunnel_->send_to(buf_, by);
            }
            
            // 立即继续处理下一个数据包
            accept_socket();
        });
}
5.3.2 隧道查找算法
cpp 复制代码
std::shared_ptr<udp_tunnel> get_or_add_tunnel(
    boost::asio::ip::udp::endpoint& endpoint_) noexcept {
    
    // 生成会话键:端点地址:端口
    std::string key = to_address(endpoint_);
    
    // 哈希表查找(O(1)复杂度)
    udp_tunnel_map::iterator it = tunnel_map_.find(key);
    if (it != tunnel_map_.end()) {
        return it->second;  // 返回现有隧道
    }
    
    // 创建新隧道
    std::shared_ptr<udp_tunnel> tunnel_ = 
        make_shared_object<udp_tunnel>(self_, endpoint_);
    
    if (!tunnel_->run()) {
        return NULL;  // 隧道初始化失败
    }
    
    // 插入隧道映射表
    tunnel_map_.insert(std::make_pair(key, tunnel_));
    return tunnel_;
}

5.4 UDP 超时清理机制

5.4.1 定时检查算法
cpp 复制代码
// udp_forward::check_timer() - 定期清理
void check_timer() noexcept {
    // 设置10秒检查间隔
    check_timer_.expires_from_now(boost::posix_time::seconds(10));
    check_timer_.async_wait([self, this](const boost::system::error_code& ec) noexcept {
        // 执行清理操作
        next_tick(GetTickCount(false));
        // 重新设置定时器
        check_timer();
    });
}

// 隧道老化检测算法
inline bool is_port_aging(uint64_t now) noexcept {
    // 时间回滚保护
    if (last_ts_ > now || !socket_.is_open()) {
        return true;
    }
    
    // 计算空闲时间(秒)
    uint64_t delta_ts_ = (now - last_ts_) / 1000;
    
    // 72秒超时阈值
    return delta_ts_ >= RINETD_DEFAULT_UDP_TIMEOUT;
}
5.4.2 批量清理策略
cpp 复制代码
void next_tick(uint64_t now) noexcept {
    std::vector<std::string> releases;
    
    // 第一阶段:识别待清理隧道
    udp_tunnel_map::iterator tail = tunnel_map_.begin();
    udp_tunnel_map::iterator endl = tunnel_map_.end();
    
    for (; tail != endl; tail++) {
        std::shared_ptr<udp_tunnel> tunnel_ = tail->second;
        if (!tunnel_ || tunnel_->is_port_aging(now)) {
            releases.push_back(tail->first);
        }
    }
    
    // 第二阶段:批量清理
    for (size_t i = 0, l = releases.size(); i < l; i++) {
        tail = tunnel_map_.find(std::move(releases[i]));
        if (tail != endl) {
            std::shared_ptr<udp_tunnel> tunnel_ = std::move(tail->second);
            if (tunnel_) {
                tunnel_->abort();  // 释放资源
            }
            tunnel_map_.erase(tail);  // 移除映射
        }
    }
}

六、网络栈优化

6.1 Socket 选项配置

cpp 复制代码
void syssocket_setsockopt(int sockfd, bool v4_or_v6) noexcept {
    if (sockfd != -1) {
        // 服务质量标记:AF31(保证转发类)
        uint8_t tos = 0x68;
        
        if (v4_or_v6) {
            ::setsockopt(sockfd, SOL_IP, IP_TOS, (char*)&tos, sizeof(tos));
            
            // 路径MTU发现配置
            #ifdef _WIN32
                int dont_frag = 0;
                ::setsockopt(sockfd, IPPROTO_IP, IP_DONTFRAGMENT, 
                            (char*)&dont_frag, sizeof(dont_frag));
            #elif IP_MTU_DISCOVER
                int dont_frag = IP_PMTUDISC_WANT;  // 希望但不强制
                ::setsockopt(sockfd, IPPROTO_IP, IP_MTU_DISCOVER, 
                            &dont_frag, sizeof(dont_frag));
            #endif
        } else {
            // IPv6类似配置
            ::setsockopt(sockfd, SOL_IPV6, IP_TOS, (char*)&tos, sizeof(tos));
            
            #ifdef _WIN32
                int dont_frag = 0;
                ::setsockopt(sockfd, IPPROTO_IPV6, IP_DONTFRAGMENT, 
                            (char*)&dont_frag, sizeof(dont_frag));
            #elif IPV6_MTU_DISCOVER
                int dont_frag = IPV6_PMTUDISC_WANT;
                ::setsockopt(sockfd, IPPROTO_IPV6, IPV6_MTU_DISCOVER, 
                            &dont_frag, sizeof(dont_frag));
            #endif
        }
        
        // 信号管道抑制(Unix-like系统)
        #ifdef SO_NOSIGPIPE
            int no_sigpipe = 1;
            ::setsockopt(sockfd, SOL_SOCKET, SO_NOSIGPIPE, 
                        &no_sigpipe, sizeof(no_sigpipe));
        #endif
    }
}

6.2 TCP快速打开优化

cpp 复制代码
// TCP_FASTOPEN选项启用
boost::system::error_code ec;
remote_socket_.set_option(
    boost::asio::detail::socket_option::boolean<IPPROTO_TCP, TCP_FASTOPEN>(true), 
    ec);

七、内存管理

7.1 自定义内存分配器

cpp 复制代码
// 内存分配策略选择
#ifdef JEMALLOC
    return (void*)::je_malloc(size_);  // jemalloc内存分配器
#else
    return (void*)::malloc(size_);     // 系统默认分配器
#endif

7.2 智能内存包装器

cpp 复制代码
template<typename T>
inline std::shared_ptr<T> make_shared_alloc(int length) noexcept {
    static_assert(sizeof(T) > 0, "不能创建不完整类型的指针");
    
    if (length < 1) {
        return NULL;
    }
    
    // 自定义分配内存
    T* p = (T*)Malloc(length * sizeof(T));
    
    // 使用自定义释放器
    return std::shared_ptr<T>(p, Mfree);
}

template<typename T, typename... A>
inline std::shared_ptr<T> make_shared_object(A&&... args) noexcept {
    static_assert(sizeof(T) > 0, "不能创建不完整类型的指针");
    
    // 分配原始内存
    T* p = (T*)Malloc(sizeof(T));
    
    // 原位构造对象
    return std::shared_ptr<T>(
        new (p) T(std::forward<A&&>(args)...),  // 完美转发参数
        [](T* p) noexcept {
            p->~T();    // 显式析构
            Mfree(p);   // 自定义释放
        });
}

八、日志实现

8.1 日志记录

文件路径存在
流描述符有效
日志请求
日志目标选择
文件日志记录器
异步流日志记录器
打开日志文件
追加时间戳
写入消息内容
刷新缓冲区
构建消息字符串
异步写入操作
自动内存管理

8.2 日志写入

cpp 复制代码
// 文件日志写入器
bool write_log(const std::string& path_, const std::string& msg_) noexcept {
    if (path_.empty() || msg_.empty()) {
        return false;
    }
    
    // 打开日志文件(追加模式)
    FILE* f = fopen(path_.data(), "ab+");
    if (!f) {
        return false;
    }
    
    // 构建完整日志行
    std::string line_("[" + GetCurrentTimeText<std::string>() + "]" + msg_ + "\r\n");
    
    // 原子写入操作
    fwrite(line_.data(), 1, line_.size(), f);
    fflush(f);  // 强制刷新缓冲区
    fclose(f);
    
    return true;
}

// 异步流日志写入器
bool write_log(boost::asio::posix::stream_descriptor& log_, 
               const std::string& msg_) noexcept {
    if (!log_.is_open() || msg_.empty()) {
        return false;
    }
    
    // 构建日志消息(智能指针管理生命周期)
    std::shared_ptr<std::string> line_ = 
        make_shared_object<std::string>("[" + GetCurrentTimeText<std::string>() + "]" + msg_ + "\r\n");
    
    // 异步写入操作
    log_.async_write_some(
        boost::asio::buffer(line_->data(), line_->size()),
        [line_](const boost::system::error_code& ec, std::size_t sz) noexcept {
            // 回调函数持有line_引用,确保写入期间内存有效
        });
    
    return true;
}

九、跨平台适配

9.1 平台抽象层

cpp 复制代码
// 平台相关类型定义
#ifdef _WIN32
namespace boost {
    namespace asio {
        namespace posix {
            // Windows平台使用stream_handle模拟stream_descriptor
            typedef boost::asio::windows::stream_handle stream_descriptor;
        }
    }
}
#include <WinSock2.h>
#else
namespace boost {
    namespace asio {
        // Unix-like平台使用标准io_context
        typedef io_service io_context;
    }
}
#endif

9.2 文件句柄

cpp 复制代码
std::shared_ptr<boost::asio::posix::stream_descriptor> 
open_log(boost::asio::io_context& context_, const std::string& path_) noexcept {
    if (path_.empty()) {
        return NULL;
    }
    
#ifdef _WIN32
    // Windows平台实现
    HANDLE handle_ = CreateFileA(path_.data(),
        GENERIC_READ | GENERIC_WRITE,
        FILE_SHARE_READ | FILE_SHARE_WRITE,
        NULL,
        OPEN_ALWAYS,        // 总是打开(不存在则创建)
        FILE_FLAG_OVERLAPPED,  // 重叠I/O(异步)
        NULL);
    
    if (handle_ == INVALID_HANDLE_VALUE) {
        return NULL;
    }
    
    // 定位到文件末尾
    LARGE_INTEGER li_ = { 0 };
    SetFilePointer(handle_, li_.LowPart, &li_.HighPart, FILE_END);
    
    return make_shared_object<boost::asio::posix::stream_descriptor>(
        context_, handle_);
#else
    // Unix-like平台实现
    int handle_ = open(path_.data(), O_RDWR | O_CREAT, S_IRWXU);
    if (handle_ == -1) {
        return NULL;
    }
    
    // 定位到文件末尾
    lseek64(handle_, 0, SEEK_END);
    
    return make_shared_object<boost::asio::posix::stream_descriptor>(
        context_, handle_);
#endif
}

十、工具函数库

10.1 字符串处理工具集

cpp 复制代码
// 安全字符串比较函数
inline int strncasecmp_(const void* x, const void* y, size_t length) noexcept {
    if (x == y || length == 0) {
        return 0;  // 相同指针或零长度
    }
    
    char* px = (char*)x;
    char* py = (char*)y;
    
    for (size_t i = 0; i < length; i++) {
        int xch = toupper(*px++);
        int ych = toupper(*py++);
        
        if (xch != ych) {
            return xch > ych ? 1 : -1;  // 差异比较
        }
    }
    return 0;  // 完全相等
}

10.2 时间处理工具

cpp 复制代码
// 跨平台时间戳获取
inline uint64_t GetTickCount(bool microseconds) noexcept {
#ifdef _WIN32
    static LARGE_INTEGER ticksPerSecond;
    LARGE_INTEGER ticks;
    
    if (!ticksPerSecond.QuadPart) {
        QueryPerformanceFrequency(&ticksPerSecond);
    }
    
    QueryPerformanceCounter(&ticks);
    
    if (microseconds) {
        double cpufreq = (double)(ticksPerSecond.QuadPart / 1000000);
        return (unsigned long long)(ticks.QuadPart / cpufreq);
    } else {
        unsigned long long seconds = ticks.QuadPart / ticksPerSecond.QuadPart;
        unsigned long long milliseconds = 1000 * 
            (ticks.QuadPart - (ticksPerSecond.QuadPart * seconds)) / 
            ticksPerSecond.QuadPart;
        return seconds * 1000 + milliseconds;
    }
#else
    struct timeval tv;
    gettimeofday(&tv, NULL);
    
    if (microseconds) {
        return ((unsigned long long)tv.tv_sec * 1000000) + tv.tv_usec;
    }
    return ((unsigned long long)tv.tv_usec / 1000) + 
           ((unsigned long long)tv.tv_sec * 1000);
#endif
}

总结

rinetd 工具的实现体现了多个重要的系统编程原则:

  1. 协议特性适配:根据TCP和UDP的不同特性,采用差异化的处理模型
  2. 资源最小化:通过单缓冲区和共享内存减少内存占用
  3. 无锁并发:单线程线性处理消除同步开销
  4. 零拷贝优化:减少不必要的数据移动
  5. 平台抽象:统一的接口适配不同操作系统

特别值得注意的是UDP处理的单线程线性模型,这一设计选择并非简单的技术妥协,而是基于对UDP协议特性、内存访问模式和系统调用开销的分析后做出的理性决策。它展示了在特定约束条件下,简单直接的解决方案往往比复杂的并发模型更有效。

相关推荐
镜中人★9 小时前
408计算机网络考纲知识点(更新中)
网络·网络协议·计算机网络
xflySnail9 小时前
nas服务域名高速访问-获取公网IP和端口
网络·tcp/ip·智能路由器
李日灐9 小时前
C++STL:stack,queue,详解!!:OJ题练手使用和手撕底层代码
开发语言·c++
fy zs9 小时前
应用层自定义协议和序列化
linux·网络·c++
末日汐10 小时前
库的制作与原理
linux·后端·restful
ba_pi10 小时前
每天写点什么2026-01-10-深度学习和网络原理
网络·人工智能·深度学习
tmacfrank10 小时前
Binder 预备知识
linux·运维·binder
王夏奇10 小时前
python在汽车电子行业中应用2—具体包的介绍和使用
网络·python·汽车
cnstartech10 小时前
esxi-vmware 虚拟机互相打开
linux·运维·服务器