Linux下 应⽤层⾃定义协议与序列化(JSON方法)

欢迎来到我的频道!【点击跳转专栏】

本文所有代码已托管至码云:【点此转跳】

文章目录

  • [1. 应⽤层的黏包问题](#1. 应⽤层的黏包问题)
    • [1.1 read()&& write()为什么支持全双工、会阻塞?](#1.1 read()&& write()为什么支持全双工、会阻塞?)
    • [1.2 从内核视角理解 接受与发送缓冲区](#1.2 从内核视角理解 接受与发送缓冲区)
    • [1.3 TCP黏包问题](#1.3 TCP黏包问题)
    • [1.4 场景描述:网络版本计算器](#1.4 场景描述:网络版本计算器)
    • [1.5 序列化 和 反序列化、定制协议](#1.5 序列化 和 反序列化、定制协议)
    • [1.6 解决黏包问题](#1.6 解决黏包问题)
  • [2. 套接字的新接口(补充,可跳过)](#2. 套接字的新接口(补充,可跳过))
    • [2.1 recv()](#2.1 recv())
      • [常见 `flags`](#常见 flags)
      • [UDP 的区别](#UDP 的区别)
      • [与 read() 的关系](#与 read() 的关系)
    • [2.2 send()](#2.2 send())
  • [3. 如何序列化与反序列化](#3. 如何序列化与反序列化)
    • [3.1 手搓(不推荐)](#3.1 手搓(不推荐))
    • [3.2 JSONCPP](#3.2 JSONCPP)
      • [3.2.1 什么是JSON](#3.2.1 什么是JSON)
      • [3.2.2 什么是JSONCPP](#3.2.2 什么是JSONCPP)
      • [3.2.3 JSON库的安装与链接](#3.2.3 JSON库的安装与链接)
      • [3.2.4 序列化](#3.2.4 序列化)
      • [3.2.5 反序列化](#3.2.5 反序列化)
      • [3.2.6 Json::Value](#3.2.6 Json::Value)
        • [1. 构造函数](#1. 构造函数)
        • [2. 访问元素](#2. 访问元素)
        • [3. 类型检查](#3. 类型检查)
        • [4. 赋值和类型转换](#4. 赋值和类型转换)
        • [5. 数组和对象操作](#5. 数组和对象操作)
  • [4. 套接字封装与网络计算器实战!](#4. 套接字封装与网络计算器实战!)

1. 应⽤层的黏包问题

1.1 read()&& write()为什么支持全双工、会阻塞?

我们之前TCP通信时候 利用 read()和 write()时候 一开始我们都认为 write()是直接把内容写入到网络中 实际上 这些系统调用函数本质是应用层(用户层的内容) 本质是一个拷贝函数 是将内容拷贝到传输层,然后就返回了(OS内核中的缓冲区 ) 剩下的工作都是OS自动帮你完成的!

发送方调用 write(),将用户空间中的数据复制到内核的发送缓冲区,再由网络协议(OS内核 )自动刷新到网卡(外设 )发送到网络;接收方收到数据后,内核将数据放入接收缓冲区,应用程序调用 read() 将数据复制到自己的用户级缓冲区。如果接收缓冲区没有数据,read() 会阻塞等待;如果发送缓冲区已满,write() 也可能阻塞。整个过程由操作系统网络协议栈负责完成。

本质说是OS在进行通信也不为过!!

结论1: TCP socket 支持全双工 是因为通信双发有一对缓冲区 一方发送的同时可以接受。

结论2: 系统级别认识:用户拷贝数据到内核对应的发送缓冲区,用户拷贝接受缓冲区中的数据到用户空间,本质可以看作是一个 生产者消费者模型问题! 至此 ,write和read为什么 会阻塞问题,就不需要解释了 就是个同步的过程!


1.2 从内核视角理解 接受与发送缓冲区

问题:OS内部有没有可能有很多个报文(还在网络层、或者刚收到),还没有来得及放入到接收缓冲区中?

肯定有!

问题:那要不要对这些报文进行管理?

当然要 先描述再组织即可! 有一个专门的结构体 叫 sk_buff 负责管理报文(先了解即可) 它里面有一个指针会指向一段内存缓冲区 所谓的上下文交互本质就是 把这个结构体对象叫给下一层或者上一层

从内核视角看,进程通过 task_struct 找到 files_struct,再通过文件描述符 fd 找到对应的 struct file;套接字文件的 private_data 指向 struct socket,它再关联 struct sock以后会讲 ),其中维护接收队列 sk_receive_queue发送队列 sk_write_queue

当点进sk_buff_head 就是这些东西:

网络数据到达后以sk_buff形式进行管理,然后进入接收队列,read() 从中取数据复制到用户空间;write() 则把用户数据(同样以sk_buff的形式进行管理)复制到发送队列,再由协议栈发送到网卡,然后发送出去。

而这两个队列 就是传说中的接受和发送缓冲区!

1.3 TCP黏包问题

在TCB(TCP是面向字节流的;而UDP按数据报发送,会保留消息边界发一次 对方就会收一次)中 众所周知 write()本质只是一个拷贝函数 比如说主机A拷贝 ls -a -l到缓冲区后 就返回了 什么时候发 发多少 出错了write()就没有半毛钱的关系了! 如果只发了个 ls 给主机B 然后又发了个 -a -l 就会出现问题了!

因为TCP是面向字节流的 它才不管你是不是字符串!OS会根据各种个样的情况觉得发多少!

也就是说 用户读上来的数据 可能只是对方发送来数据的一部分。 读取和写入的过程并不一定是完全匹配的!这个问题叫 数据包黏包问题(偶发性问题) 那么解决这个问题就得上层用户自己来做!!

比如说 通信时候读到\n就停止 这种行为 其实就是在作 约定!用户层自定义协议!

我们以下面具体场景举例!

1.4 场景描述:网络版本计算器

例如, 我们需要实现⼀个服务器版的加法器. 我们需要客⼾端把要计算的两个加数发过去, 然后由服务器进⾏计算, 最后再把结果返回给客⼾端。

约定方案一:

  • 客户端发送一个形如"1+2"的字符串;
  • 这个字符串中有两个操作数,都是整形;
  • 两个数字之间会有一个字符是运算符,运算符只能是 + ;
  • 数字和运算符之间没有空格;
  • ...

约定方案二:

  • 定义结构体来表示我们需要交互的信息;(定制协议
  • 发送数据时将这个结构体按照一个规则转换成字符串,接收到数据的时候再按照相同的规则把字符串转化回结构体,这个过程叫做 "序列化" 和 "反序列化"!

补充:为什么我们并不推荐直接用结构体通信!必须通过"序列化" 和 "反序列化"

  1. 因为客户端服务端是在不同系统上跑的 就会出现各种 对齐和大小端问题(兼容问题)!
  2. 当然最重要的 可能二者就不是一个语言写的 你服务端用C++写;客户端用html网页写!如果 前端语言就没有对应这些二进制结构体的能力 那么你的客户端就很难访问你的服务器了! 所以采用这种方法就要求 双端的语言得是相近的!

1.5 序列化 和 反序列化、定制协议

  • 序列化:把消息由多变一,方便网络发送。
  • 反序列化:把消息由一变多,方便上层处理。

而像是上图红方框里面的东西,不就是我们定义好的结构体 或者 类嘛? 里面的信息不就是其成员属性里的值! 我们将这个类 通过某种 序列化 的方法 变成一个大字符串 然后再将这个大字符串发送到对端 然后再通过 反序列化 变成类 然后提取它属性里面的值就可以了!

而class 中我们约定好消息的类型、含义等 就是 协议!


问题:序列化和反序列化有什么好处?什么是定制协议?

会让程序具有很好的拓展性!不同语言里面只要通过 序列化和反序列化 转成的格式是一样的 双方就能进行通信!

相当于 在语言层 和 网络层之间 添加了 一个 序列化和反序列化的软件层!

而其中(以C++举例)所谓的协议定制 :本质就是在定制双方都能认识的,符合通信和业务需要的结构化数据 所谓的结构化数据,其实就是 class 或者 struct!


1.6 解决黏包问题

当然 除了 序列与反序列化 以及定制协议 我们还需要保证该报文的完整性! 我们可以在报文前面加个报头(假设4字节)每次读取的时候 都会先读取这4个字节 而报头里面就是 接下来要读取的长度;当然也可以直接采用固定长度读取来解决这个问题!! 如果读不完 我就不交给上层处理 不做 序列化、反序列化工作!

2. 套接字的新接口(补充,可跳过)

2.1 recv()

recv() 是 socket 编程中从套接字接收数据的函数。最常见的是 POSIX / C 的形式:

c 复制代码
#include<sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
  • sockfd:已连接或已绑定的 socket 描述符。
  • buf:接收数据写入的内存缓冲区。
  • len:最多接收多少字节。
  • flags:接收行为选项;通常写 0

返回值:

  • > 0:实际读到的字节数。不一定等于 请求的 len
  • 0:对端已正常关闭连接;对于 TCP 来说,这是 EOF。
  • -1:发生错误,查看 errno
    • EAGAIN / EWOULDBLOCK:非阻塞 socket 暂时没有数据。
    • EINTR:调用被信号中断,通常应重试。

默认 socket 通常是阻塞式的:

c 复制代码
char buffer[4096];
ssize_t count = recv(sockfd, buffer, sizeof(buffer), 0);

如果还没有数据,程序会在 recv() 停住,直到:

  1. 收到至少一些数据;
  2. 对端关闭连接;
  3. 出错或被中断。

将 socket 设置为非阻塞后,没数据时不会等待,而是立刻返回 -1,并设定 errnoEAGAINEWOULDBLOCK。实际项目常配合 selectpollepollkqueue 使用。


如果你需要恰好接收 n 字节,应循环读取:

cpp 复制代码
ssize_t recv_all(int fd, void *buffer, size_t length)
{
    char *cursor = buffer;
    size_t total = 0;

    while (total < length)
    {
        ssize_t count = recv(fd, cursor + total, length - total, 0);

        if (count > 0)
        {
            total += (size_t)count;
        }
        else if (count == 0)
        {
            return total; // 对端关闭
        }
        else if (errno != EINTR)
        {
            return -1; // 真正的错误
        }
    }

    return total;
}

常见 flags

  • 0:默认行为,最常用。
  • MSG_PEEK:查看数据但不从接收缓冲区移除
  • MSG_WAITALL:尽量等待直到读满 len 字节;仍可能因为关闭、信号或错误提前返回。
  • MSG_DONTWAIT:本次调用采用非阻塞行为(平台支持时)。

UDP 的区别

对于 UDP,recv() 接收的是一个数据报:

  • 一次通常对应一个 UDP 包;
  • 缓冲区太小,超出的部分可能被截断;
  • 如果 socket 未 connect() 到固定的对端,通常使用 recvfrom(),以同时获取发送方地址。

与 read() 的关系

在 socket 上,下面两者通常作用相近:

c 复制代码
recv(fd, buffer, size, 0);
read(fd, buffer, size);

recv() 的优势是支持 socket 专用的 flags;若不需要 flags,read() 也可以。

2.2 send()

send() 用于向已建立连接的套接字发送数据,最常用于 TCP。

cpp 复制代码
#include <sys/socket.h>

ssize_t send(int sockfd, const void *buf, size_t len, int flags);

参数含义:

  • sockfd:通信套接字,通常是 connect()accept() 返回的套接字
  • buf:待发送数据的起始地址
  • len:希望发送的字节数
  • flags:发送选项,通常填写 0

返回值:

  • > 0:实际发送的字节数
  • == 0:本次没有发送数据,常见于 len == 0
  • < 0:发送失败,具体原因查看 errno

需要注意,TCP 是字节流协议。调用一次 send(),不一定一次性发送完所有数据,因此更可靠的做法是循环发送:

cpp 复制代码
const char *data = message.data();
size_t total = 0;

while (total < message.size())
{
    ssize_t n = send(sockfd,
                     data + total,
                     message.size() - total,
                     0);

    if (n < 0)
    {
        perror("send");
        break;
    }

    total += n;
}

send() 成功,只表示数据已经从用户空间复制到内核的发送缓冲区,并不代表对方已经读取或处理了数据。


与UDP对比:

cpp 复制代码
send(sockfd, buf, len, 0);       // 已连接套接字
sendto(sockfd, buf, len, 0,
       address, address_len);    // 指定目标地址,常用于 UDP

UDP 调用 connect() 绑定默认目标地址后,也可以使用 send()。如果对端已经关闭 TCP 连接,继续发送可能失败,并设置 EPIPE,有些系统还会触发 SIGPIPE 信号。

3. 如何序列化与反序列化

3.1 手搓(不推荐)

比如以空格为分隔符转化成字符串 进行拼接 反序列化则是将字符串 按照一定规则 结构化

当然我们不推荐这么做!!如果这是我们这次请求简单 当然你没感觉 如果变量多 手动进行结构化 代码量大 拓展性差!

那咋办? 用别人现成的就可以了 常用的有: json(最常用 可读性好) protobufeer(可读性差 效率高) xml

3.2 JSONCPP

3.2.1 什么是JSON

JSON 是一种轻量级的数据交换格式 ,全称是 JavaScript Object Notation,中文常译为"JavaScript 对象表示法"。

JSON 可以理解成一张"程序之间都看得懂的表格"。

比如一个人的信息:

json 复制代码
{
  "name": "张三",
  "age": 20,
  "男": true
}

意思就是:

  • name 是姓名,值是"张三"
  • age 是年龄,值是 20
  • 是一个真假选项,值是 true

在网络通信中,客户端把数据整理成 JSON 字符串发送给服务器:

json 复制代码
{
  "type": "login",
  "username": "fcy"
}

服务器收到后,就能知道:

text 复制代码
请求类型:登录
用户名:fcy

它主要用于程序之间传递和保存数据,例如客户端向服务器发送用户信息,如:Python就有自己的原生JSON库

所以, JSON 本质上就是一种统一的数据表达方式 。它不是用来执行代码的,而是用来描述和传递数据的,类似于人与人约定好的一种"数据格式"。

⚠️:在 C++ 中,准库没有内置 JSON 功能,通常使用第三方库,借助 JSON 库把 C++ 数据转换成 JSON,或者把 JSON 解析成 C++ 数据。也就是所谓的序列化与反序列化 只不过格式不再是普通的字符串 而是序列化产出的字符串,遵守 JSON 语法规范,叫 JSON 字符串

3.2.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中序列化和反序列化操作的详细介绍。

3.2.3 JSON库的安装与链接

shell 复制代码
ubuntu:sudo apt-get install libjsoncpp-dev
Centos: sudo yum install jsoncpp-devel

在使用JSON库的时候编译链接时候必须要进行 -l指明库名

cpp 复制代码
g++ 目标文件 -jsoncpp

3.2.4 序列化

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

Jsoncpp提供了多种方式进行序列化:

法一(不推荐)

使用 Json::ValuetoStyledString 方法:

  • 优点:将 Json::Value 对象直接转换为格式化的JSON字符串。

示例:

cpp 复制代码
#include <iostream>
#include <jsoncpp/json/json.h>
#include <jsoncpp/json/value.h>
#include <string>
int main()
{
    Json::Value root;
    root["name"] = "张三";//key:value
    root["sex"] = "男";
    root["age"] = 18;
    std::string  s = root.toStyledString();
    std::cout << s << std::endl;
    return 0;
}

⚠️:这玩意是一行字符串!!显示器中只是把\n替换成了换行。

法二 (可以用)

使用 Json::FastWriter

  • 优点:比 StyledWriter 更快,因为它不添加额外的空格和换行符。

示例:

这是StyledWrite的:

cpp 复制代码
#include <iostream>
#include <jsoncpp/json/json.h>
#include <jsoncpp/json/value.h>
#include <jsoncpp/json/writer.h>
#include <string>
int main()
{
    Json::Value root;
    root["name"] = "张三"; // key:value
    root["sex"] = "男";
    root["age"] = 18;

    Json::StyledWriter writer;
    std::string s = writer.write(root);
    std::cout << s << std::endl;

}

这是 Json::FastWriter的:

cpp 复制代码
int main()
{
    Json::Value root;
    root["name"] = "张三"; // key:value
    root["sex"] = "男";
    root["age"] = 18;

    Json::FastWriter writer;
    std::string s = writer.write(root);
    std::cout << s << std::endl;

}
法三(推荐)

使用 Json::StreamWriter

  • 优点:提供了更多的定制选项,如缩进、换行符等。

示例:

cpp 复制代码
#include <iostream>
#include <jsoncpp/json/json.h>
#include <jsoncpp/json/value.h>
#include <jsoncpp/json/writer.h>
#include <memory>
#include <string>
#include <sstream>
int main()
{
    Json::Value root;
    root["name"] = "张三"; // key:value
    root["sex"] = "男";
    root["age"] = 18;

    Json::StreamWriterBuilder wbuilder;
    std::unique_ptr<Json::StreamWriter> swriter(wbuilder.newStreamWriter());
    std::stringstream ss;
    swriter->write(root, &ss);

    std::cout << ss.str() <<std::endl;
补充

让然value可以是字符串可以是整数也可以是其他类型 当然也可以是value本身!

cpp 复制代码
#include <iostream>
#include <jsoncpp/json/json.h>
#include <jsoncpp/json/value.h>
#include <jsoncpp/json/writer.h>
#include <memory>
#include <string>
#include <sstream>
int main()
{
    Json::Value sub_root1;
    sub_root1["name"] = "张三"; // key:value
    sub_root1["sex"] = "男";
    sub_root1["age"] = 18;
    
    Json::Value sub_root2;
    sub_root2["math"] = 88;
    sub_root2["chinese"] = 90;
   
    Json::Value root;
    root["who"] = sub_root1;
    root["score"] = sub_root2;
     
    Json::StreamWriterBuilder wbuilder;
    std::unique_ptr<Json::StreamWriter> swriter(wbuilder.newStreamWriter());
    std::stringstream ss;
    swriter->write(root, &ss);

    std::cout << ss.str() <<std::endl;

}

同时还直接支持用append()的形式添加

cpp 复制代码
  Json::Value sub_root1;
    sub_root1["name"] = "张三"; // key:value
    sub_root1["sex"] = "男";
    sub_root1["age"] = 18;
    
    Json::Value sub_root2;
    sub_root2["math"] = 88;
    sub_root2["chinese"] = 90;
   
    Json::Value one;
    one["who"] = sub_root1;
    one["score"] = sub_root2;
    
    Json::Value root;
    for(int i = 0; i<10 ;i++)
    {
        root.append(one);
    }

    Json::StreamWriterBuilder wbuilder;
    std::unique_ptr<Json::StreamWriter> swriter(wbuilder.newStreamWriter());
    std::stringstream ss;
    swriter->write(root, &ss);
    
    std::cout << ss.str() <<std::endl;

至此变成 一个JSON数组:

3.2.5 反序列化

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

Jsoncpp提供了以下⽅法进⾏反序列化:

法一(推荐)

使⽤ Json::Reader

  • 优点:提供详细的错误信息和位置,⽅便调试。
cpp 复制代码
// JSON 字符串
    std::string json_string =
        "{\"name\":\"张三\", \"age\":30,\"city\":\"北京\"}";

    Json::Reader reader;
    Json::Value root;
    bool parseok = reader.parse(json_string, root);
    //反序列化成功返回true
    if (!parseok)
    {
        std::cerr << "fail !" << std::endl;
    }

    std::string name = root["name"].asString();
    int age = root["age"].asInt();
    std::string city = root["city"].asString();

    std::cout << name << std::endl;
    std::cout << age << std::endl;
    std::cout << city << std::endl;
法二(不推荐了)

使用 Json::CharReader 的派生类(不推荐了,上面的足够了):

  • 在某些情况下,你可能需要更精细地控制解析过程,可以直接使用 Json::CharReader 的派生类。
  • 但通常情况下,使用 Json::parseFromStreamJson::Readerparse 方法就足够了。

3.2.6 Json::Value

⚠️:接口的详细使用可以自行查阅文档或者ai!!

Json::Value 是 JsonCpp 库中的一个重要类,用于表示和操作 JSON 数据结构。以下是一些常用的 Json::Value 操作列表:

1. 构造函数
  • Json::Value():默认构造函数,创建一个空的 Json::Value 对象。
  • Json::Value(ValueType type, bool allocated = false):根据给定的 ValueType(如 nullValueintValuestringValue 等)创建一个 Json::Value 对象。
  • Json::Value():默认构造函数,创建一个空的 Json::Value 对象。
  • Json::Value(ValueType type, bool allocated = false):根据给定的 ValueType(如 nullValueintValuestringValue 等)创建一个Json::Value 对象。
2. 访问元素
  • Json::Value& operator[](const char* key):通过键(字符串)访问对象中的元素。如果键不存在,则创建一个新的元素。
  • Json::Value& operator[](const std::string& key):同上,但使用 std::string 类型的键。
  • Json::Value& operator[](ArrayIndex index):通过索引访问数组中的元素。如果索引超出范围,则创建一个新的元素。
  • Json::Value& at(const char* key):通过键访问对象中的元素,如果键不存在则抛出异常。
  • Json::Value& at(const std::string& key):同上,但使用 std::string 类型的键。
  • Json::Value& operator[](const char* key, const Json::Value& defaultValue = Json::nullValue):在对象中插入或访问一个元素,如果键不存在则使用默认值。
  • Json::Value& operator[](const std::string& key, const Json::Value& defaultValue = Json::nullValue):同上,使用 std::string 类型的键。
3. 类型检查
  • bool isNull():检查值是否为 null
  • bool isBool():检查值是否为布尔类型。
  • bool isInt():检查值是否为整数类型。
  • bool isInt64():检查值是否为 64 位整数类型。
  • bool isUInt():检查值是否为无符号整数类型。
  • bool isUInt64():检查值是否为 64 位无符号整数类型。
  • bool isIntegral():检查值是否为整数或可转换为整数的浮点数。
  • bool isDouble():检查值是否为双精度浮点数。
  • bool isNumeric():检查值是否为数字(整数或浮点数)。
  • bool isString():检查值是否为字符串。
  • bool isArray():检查值是否为数组。
  • bool isObject():检查值是否为对象(键值对的集合)。
4. 赋值和类型转换
  • Json::Value& operator=(bool value):将布尔值赋给 Json::Value 对象。
  • Json::Value& operator=(int value):将整数赋给 Json::Value 对象。
  • Json::Value& operator=(unsigned int value):将无符号整数赋给 Json::Value 对象。
  • Json::Value& operator=(Int64 value):将 64 位整数赋给 Json::Value 对象。
  • Json::Value& operator=(UInt64 value):将 64 位无符号整数赋给 Json::Value 对象。
  • Json::Value& operator=(double value):将双精度浮点数赋给 Json::Value 对象。
  • Json::Value& operator=(const char* value):将 C 字符串赋给 Json::Value 对象。
  • Json::Value& operator=(const std::string& value):将 std::string 赋给 Json::Value 对象。
  • bool asBool():将值转换为布尔类型(如果可能)。
  • int asInt():将值转换为整数类型(如果可能)。
  • Int64 asInt64():将值转换为 64 位整数类型(如果可能)。
  • unsigned int asUInt():将值转换为无符号整数类型(如果可能)。
  • UInt64 asUInt64():将值转换为 64 位无符号整数类型(如果可能)。
  • double asDouble():将值转换为双精度浮点数类型(如果可能)。
  • std::string asString():将值转换为字符串类型(如果可能)。
5. 数组和对象操作
  • size_t size():返回数组或对象中的元素数量。
  • bool empty():检查数组或对象是否为空。
  • void resize(ArrayIndex newSize):调整数组的大小。
  • void clear():删除数组或对象中的所有元素。
  • void append(const Json::Value& value):在数组末尾添加一个新元素。

4. 套接字封装与网络计算器实战!

篇幅有限,敬请期待!

相关推荐
koi77u1 小时前
嵌入式学习————————TCP并发服务器(1)
linux·学习
上学的小垃圾9 小时前
基于docker安装MySQL(openEuler系统)
linux·mysql·docker·容器
一直C11 小时前
Linux网络编程|HTTP协议详解(报文、请求方法、状态码)
linux·http·wireshark·vim·visual studio
Brilliantwxx13 小时前
【Linux】 进程(10) 进程控制深度解析:创建、终止与等待
linux·运维·服务器·c语言·开发语言·网络
欧米欧14 小时前
Linux基础指令上
linux·运维·服务器
ARM|X86+FPGA工业主板厂家15 小时前
基于嵌入式 GPU 的智能服务机器人
linux·运维·arm开发·fpga开发·机器人
sulikey15 小时前
Linux 粘滞位(Sticky Bit)完全指南:为什么共享目录需要“防删除“?
linux·服务器
mengge.cloud15 小时前
网络技术基础小白教程
linux·服务器·网络·云计算
学代码的CJY15 小时前
Linux 重定向、管道与环境变量
linux·spring boot·spring