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

系统编程_进程间通信机制_消息队列与共享内存

消息队列概述

  1. 消息有类型:每条消息都有一个类型,就像每封信都有一个标签,方便分类和查找。
  2. 消息有格式:消息的内容有固定的格式,就像每封信都有固定的信纸格式。
  3. 随机查询:你可以按类型读取消息,不一定要按顺序读取,就像你可以按标签找信,而不是按收到的顺序。
  4. 多进程读写:多个进程可以同时向消息队列写入或读取消息,就像多个邮递员可以同时投递或取走信件。
  5. 读出即删除:从消息队列中读出消息后,消息就会被删除,就像你从信箱里取出信后,信箱里就没有这封信了。
  6. 唯一标识符:每个消息队列都有一个唯一的标识符,就像每个信箱都有一个唯一的编号。
  7. 持久存在:消息队列会一直存在,直到内核重启或你手动删除它,就像信箱会一直在,除非你把它拆掉。

Ubuntu 12.04 中的消息队列限制

  • 每条消息最多可以有 8K 字节,就像每封信最多可以写 8K 字节的内容。
  • 每个消息队列最多可以存储 16K 字节的消息,就像每个信箱最多可以放 16K 字节的信件。
  • 系统中最多可以有 1609 个消息队列,就像一个城市最多可以有 1609 个信箱。
  • 系统中最多可以有 16384 条消息,就像一个城市最多可以有 16384 封信。

 消息队列的使用方法一般是:

发送者(邮递员)的步骤

  1. 获取消息队列的键值:

    • 就像邮递员要投递,首先需要知道信箱的编号(消息队列的键值)。
    • 通过 msgget 函数获取消息队列的消息队列的键值。
  2. 将数据放入结构体并发送

    • 你要发送一个信件(消息),需要把信件装进一个贴有邮票(类型)的盒子(结构体)。
    • 使用 msgsnd 函数将这个带有标识的消息发送到消息队列。

接收者(拿信的人)的步骤

  1. 获取消息队列的 ID

    • 就像你要从信箱取信件,首先需要知道信箱的编号(消息队列的键值)。
    • 通过 msgget 函数获取消息队列的键值。
  2. 读取指定标识的消息

    • 你要取出特定邮票(类型)的包裹(消息)。
    • 使用 msgrcv 函数从消息队列中读取带有指定类型的消息。

查看系统中已创建的消息队列的指令

使用 ipcs 命令可以查看系统中已经创建的消息队列,以及其他IPC资源(共享内存和信号量)。

ipcs 命令参数

  • -m:查看系统共享内存信息。
  • -q:查看系统消息队列信息。
  • -s:查看系统信号量信息。
  • -a:显示系统内所有的IPC信息。
# 查看系统中所有的消息队列 
ipcs -q

移除IPC资源

使用 ipcrm 命令可以移除指定的IPC资源,包括共享内存段、信号量和消息队列。

ipcrm 命令参数

  • -m shmid:移除用 shmid 标识的共享内存段。
  • -s semid:移除用 semid 标识的信号量。
  • -q msgid:移除用 msgid 标识的消息队列。
ipcrm -q 12345  # 删除消息队列 12345 12345 是共享内存段的标识符(msgid)

让我们来区分一下消息队列中的“标识”(msgid)和“类型”(mtype)。

标识(msgid)

  • 定义msgid 是消息队列的标识符,用于唯一标识一个消息队列。
  • 作用:当你创建或获取一个消息队列时,系统会返回一个 msgid,你可以通过这个 msgid 来进行后续的消息发送、接收和删除操作。
  • 示例:就像一个快递柜的编号,你需要知道这个编号才能往里面放包裹或取包裹。

类型(mtype)

  • 定义mtype 是消息的类型,用于标识消息的类别。
  • 作用:在发送和接收消息时,可以根据 mtype 来区分不同类型的消息。接收消息时,可以指定接收某一类型的消息。
  • 示例:就像包裹上的标签,用于区分不同的包裹类型,比如“普通包裹”、“快递包裹”等。

区别

  • msgid 是消息队列的唯一标识符,用于操作整个消息队列。
  • mtype 是消息的类型,用于区分消息队列中的不同消息。

 消息队列函数

ftok 函数

ftok 函数用于生成一个唯一的键值(key),这个键值用于标识IPC(进程间通信)资源,比如消息队列、共享内存和信号量。这个键值就像一个独特的“身份证号”,确保每个IPC资源都有一个唯一的标识。

函数原型

#include <sys/types.h>
#include <sys/ipc.h>key_t ftok(const char *pathname, int proj_id);

参数解释

  • pathname:这是一个文件路径名。ftok 函数会使用这个路径名来生成键值。
  • proj_id:这是一个项目ID,是一个非0的整数(只有低8位有效)。(随便填)

返回值

  • 成功:返回一个唯一的键值(key),就像生成了一个独特的身份证号。
  • 失败:返回 -1,表示生成键值失败。

msgget 函数的作用

msgget 函数用于创建一个新的消息队列或打开一个已经存在的消息队列。消息队列就像一个共享的邮箱,不同的进程可以通过这个邮箱来发送和接收消息。

函数原型

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>int msgget(key_t key, int msgflg);

参数解释

  • key:这是消息队列的“钥匙”。只要不同的进程使用相同的钥匙(key),它们就能访问同一个消息队列。这个钥匙可以通过 ftok 函数生成。
  • msgflg:这是一个标志,决定了函数的行为和消息队列的权限。它可以是以下几种值的组合:
    • IPC_CREAT:如果消息队列不存在,就创建一个新的。
    • IPC_EXCL:如果消息队列已经存在,并且同时指定了 IPC_CREAT,那么函数会返回错误。
    • 权限位:设置消息队列的访问权限,比如读写权限,类似于文件的权限设置。

返回值

  • 成功:返回消息队列的标识符(ID),这是一个非负整数。
  • 失败:返回 -1,并设置 errno 以指示错误类型。

msgsnd 函数的作用

msgsnd 函数用于将新消息添加到消息队列中。你可以把它想象成把一封信放进一个共享的邮箱里,其他进程可以从这个邮箱里取出这封信。

函数原型

#include <sys/msg.h>int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);

参数解释

  • msqid:消息队列的标识符。就像邮箱的编号,你需要知道这个编号才能往里面放信。
  • msgp:待发送消息结构体的地址。就像你要发送的信的地址。
  • msgsz:消息正文的字节数。就像信的内容有多长。
  • msgflg:函数的控制属性,决定了消息发送的行为。可以是以下值之一:
    • 0:如果消息队列已满,msgsnd 会阻塞(等待),直到有空间可以放入消息。就像你要等到邮箱有空位才能放信。
    • IPC_NOWAIT:如果消息队列已满,msgsnd 会立即返回,而不是等待。就像你看到邮箱满了,直接走开,不等空位。

返回值

  • 成功:返回 0,表示消息成功发送。
  • 失败:返回 -1,并设置 errno 以指示错误类型。

msgrcv 函数的作用

msgrcv 函数用于从消息队列中接收一个消息。一旦成功接收消息,这条消息就会从消息队列中删除。你可以把它想象成从共享的邮箱里取出一封信,取出后这封信就不在邮箱里了。

函数原型

#include <sys/types.h>
#inclde <sys/ipc.h>
#include <sys/msg.h>
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);

参数解释

  • msqid:消息队列的标识符。就像邮箱的编号,你需要知道这个编号才能从里面取信。
  • msgp:存放消息结构体的地址。就像你要把取出的信放到哪里。
  • msgsz:消息正文的字节数。就像信的内容有多长。
  • msgtyp:消息的类型,决定你要取哪种类型的消息。可以有以下几种情况:
    • msgtyp = 0:取队列中的第一个消息。
    • msgtyp > 0:取队列中类型为 msgtyp 的消息。
    • msgtyp < 0:取队列中类型值小于或等于 msgtyp 绝对值的消息,如果有多个这样的消息,则取类型值最小的消息。

msgflg:函数的控制属性

  • 0msgrcv 调用会阻塞,直到成功接收消息为止。就像你一直等到邮箱里有信为止。
  • MSG_NOERROR:如果返回的消息字节数比 msgsz 多,则消息会被截短到 msgsz 字节,不通知发送进程。
  • IPC_NOWAIT:调用进程会立即返回。如果没有收到消息则立即返回 -1。就像你看到邮箱里没有信,直接走开,不等信。

注意事项

  • 如果消息队列中有多种类型的消息,msgrcv 会按消息类型获取,而不是按先进先出的顺序。
  • 在获取某类型消息时,如果队列中有多条此类型的消息,则获取最先添加的消息,即先进先出。

返回值

  • 成功:返回读取消息的长度。
  • 失败:返回 -1,并设置 errno 以指示错误类型。 

msgctl 函数的作用

msgctl 函数就像是一个多功能的管理员,可以对消息队列进行各种操作,比如查看消息队列的状态、修改它的属性,或者直接删除它。

函数原型

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
intmsgctl(intmsqid,intcmd,structmsqid_ds*buf);

参数解释

  1. msqid:这是消息队列的编号,就像每个邮箱都有一个编号一样,你需要这个编号来操作特定的消息队列。

  2. cmd:这是你告诉管理员要做什么的指令。常见的指令有:

    • IPC_RMID:删除消息队列。就像把邮箱整个拆掉,里面的信件也都没了。
    • IPC_STAT:查看消息队列的当前状态。就像你让管理员把邮箱的详细信息告诉你,比如有多少信件、谁发的等等。
    • IPC_SET:修改消息队列的属性。就像你让管理员把邮箱的某些设置改一下,比如谁可以往里面放信,谁可以取信。
  3. buf:这是一个结构体的地址,用来存放或修改消息队列的属性。就像你给管理员一个表格,他会把邮箱的信息填到这个表格里,或者根据这个表格里的信息修改邮箱的设置。

返回值

  • 成功:返回 0,表示操作成功。
  • 失败:返回 -1,并设置 errno 来指示错误类型。就像管理员告诉你操作失败了,并给你一个错误代码,说明哪里出了问题。

练习:消息队列实现多人聊天程序 

下面 我们来根据这个流程图来完善这个练习吧。

主函数

根据流程图可以看到主函数首先检查命令行参数的数量。程序需要两个参数:用户名和接收类型。如果参数数量不对,程序会打印使用说明并退出。

  • argv[1] 是用户名,例如 "Tom"。
  • argv[2] 是接收类型,例如 "1"。

如果成功接下来,程序将这两个参数传递给 chat_process 函数,开始聊天进程。

 主函数部分代码

// 主函数,程序的入口点
int main(int argc, char *argv[]) {// 检查命令行参数数量是否正确if (argc != 3) {// 参数数量不正确时,输出使用说明并退出程序fprintf(stderr, "Usage: %s <name> <recv_type>\n", argv[0]);exit(1);}// 获取命令行参数:姓名和接收类型const char *name = argv[1];long recv_type = atol(argv[2]);// 调用函数处理聊天逻辑chat_process(name, recv_type);// 程序正常结束return 0;
}

详细解释:

这段代码的主要功能是作为聊天程序的入口点,负责解析命令行参数,检查参数的正确性,并启动聊天进程。具体来说,它:

  1. 检查命令行参数的数量是否正确。
  2. 从命令行参数中提取用户名和接收类型。
  3. 调用chat_process函数,启动聊天进程,实现消息的发送和接收。

通过这种方式,程序能够根据用户输入的参数启动相应的聊天功能,实现用户之间的消息传递。

结构体

typedef struct msg {long type;       // 接收者类型char text[100];  // 发送内容char name[20];   // 发送者姓名
} MSG;

聊天进程函数

接下面 我们就要开始创建消息队列,创建进程了。

基本逻辑就是

1. 创建消息队列

  • 使用 ftok 生成一个唯一的键值。

2. 创建子进程

  • 使用 fork 创建一个子进程。
  • 如果 fork 失败,打印错误信息并退出。

3. 子进程逻辑

  • 子进程调用 receive_message 函数,负责从消息队列中接收并打印消息。

4. 父进程逻辑

  • 父进程在一个无限循环中调用 send_message 函数,负责发送消息到消息队列。
  • 父进程在循环结束后,调用 wait 等待子进程结束。
/*** 聊天处理函数* 根据提供的名称和接收类型,初始化消息队列,并根据进程类型处理消息* * @param name 用户名,用于标识消息的发送者或接收者* @param recv_type 接收消息的类型,用于区分不同类型的接收逻辑*/
void chat_process(const char *name, long recv_type) {// 生成消息队列的键值,用于后续的消息队列识别key_t key = ftok("/tmp", 65);// 使用键值创建或获取消息队列,确保消息队列的访问权限int msgid = msgget(key, 0666 | IPC_CREAT);if (msgid < 0) {// 错误处理:获取消息队列失败perror("msgget");exit(1);}// 创建子进程,用于并行处理接收和发送消息pid_t pid = fork();if (pid < 0) {// 错误处理:进程创建失败perror("fork");exit(1);} else if (pid == 0) {// 子进程负责接收消息receive_message(msgid, name, recv_type);} else {// 父进程负责发送消息while (1) {send_message(msgid, name);}// 等待子进程结束,避免僵尸进程wait(NULL);}
}

详细讲解:

1. 生成消息队列键值

key_t key = ftok("/tmp", 65);

ftok 函数用于生成一个唯一的键值,用于创建消息队列。"/tmp" 是一个路径名,65 是一个项目 ID。这两个参数的组合确保了键值的唯一性。

2. 创建或获取消息队列

int msgid = msgget(key, 0666 | IPC_CREAT);
if (msgid < 0) {perror("msgget");exit(1);
}

msgget 函数用于创建或获取一个消息队列。key 是之前生成的键值,0666 | IPC_CREAT 指定了权限和标志。0666 表示所有用户都有读写权限,IPC_CREAT 表示如果消息队列不存在则创建。如果 msgget 失败,程序会打印错误信息并退出。

3. 创建子进程

pid_t pid = fork();
if (pid < 0) {perror("fork");exit(1);
} else if (pid == 0) {// 子进程负责接收消息receive_message(msgid, name, recv_type);
} else {// 父进程负责发送消息while (1) {send_message(msgid, name);}wait(NULL);
}

fork 函数用于创建一个子进程。fork 函数会返回两次:

在父进程中,返回子进程的 PID。

在子进程中,返回 0。

如果 fork 失败,程序会打印错误信息并退出。

子进程

子进程调用 receive_message 函数,负责接收消息。receive_message 函数会不断从消息队列中读取属于当前用户的消息,并打印出来。

父进程

父进程在一个无限循环中调用 send_message 函数,负责发送消息。send_message 函数会提示用户输入接收者名字和消息内容,然后根据接收者名字确定消息类型,并将消息发送到消息队列。

父进程在发送消息的循环结束后,调用 wait 函数等待子进程结束。

 发送消息函数

这个呢就是send_message 函数,首先提示用户输入接收者名字和消息内容。然后根据接收者名字确定消息类型,并将消息内容和发送者名字填充到 MSG 结构体中。最后,使用 msgsnd 函数将消息发送到消息队列。

// 发送消息到指定接收者
// 参数:
//   msgid: 消息队列标识符
//   name: 发送者姓名
void send_message(int msgid, const char *name) {MSG snd_msg; // 定义待发送的消息结构体char recipient[20]; // 用于存储接收者姓名的字符数组// 提示并获取用户输入的接收者姓名printf("Enter recipient name: ");fgets(recipient, sizeof(recipient), stdin);recipient[strcspn(recipient, "\n")] = 0; // 去掉输入中的换行符// 根据接收者名字确定消息类型if (strcmp(recipient, "Tom") == 0) {snd_msg.type = 1;} else if (strcmp(recipient, "Lili") == 0) {snd_msg.type = 2;} else if (strcmp(recipient, "Luxi") == 0) {snd_msg.type = 3;} else {// 如果接收者未知,则打印错误信息并返回printf("Unknown recipient: %s\n", recipient);return;}// 设置消息结构体中的发送者姓名strcpy(snd_msg.name, name);// 提示并获取用户输入的消息内容printf("Enter message to send: ");fgets(snd_msg.text, sizeof(snd_msg.text), stdin);snd_msg.text[strcspn(snd_msg.text, "\n")] = 0; // 去掉输入中的换行符// 发送消息到消息队列if (msgsnd(msgid, &snd_msg, sizeof(snd_msg) - sizeof(long), 0) < 0) {// 如果发送失败,则打印错误信息并退出程序perror("msgsnd");exit(1);}
}
  1. 函数声明和变量定义

    void send_message(int msgid, const char *name) {MSG snd_msg;char recipient[20];
    
    • send_message 函数接受两个参数:消息队列的标识符 msgid 和发送者的名字 name
    • 定义了一个 MSG 结构体变量 snd_msg 用于存储要发送的消息。
    • 定义了一个字符数组 recipient 用于存储接收者的名字。
  2. 输入接收者名字

    printf("Enter recipient name: ");
    fgets(recipient, sizeof(recipient), stdin);
    recipient[strcspn(recipient, "\n")] = 0; // 去掉换行符
    
    • 提示用户输入接收者的名字。
    • 使用 fgets 从标准输入读取接收者的名字,并存储在 recipient 数组中。
    • 使用 strcspn 去掉输入字符串末尾的换行符。
  3. 确定消息类型

    if (strcmp(recipient, "Tom") == 0) {snd_msg.type = 1;
    } else if (strcmp(recipient, "Lili") == 0) {snd_msg.type = 2;
    } else if (strcmp(recipient, "Luxi") == 0) {snd_msg.type = 3;
    } else {printf("Unknown recipient: %s\n", recipient);return;
    }
    
    • 根据接收者的名字确定消息类型:
      • 如果接收者是 "Tom",消息类型设为 1。
      • 如果接收者是 "Lili",消息类型设为 2。
      • 如果接收者是 "Luxi",消息类型设为 3。
      • 如果接收者名字不在上述列表中,打印错误信息并返回。
  4. 填充消息内容

    strcpy(snd_msg.name, name);
    printf("Enter message to send: ");
    fgets(snd_msg.text, sizeof(snd_msg.text), stdin);
    snd_msg.text[strcspn(snd_msg.text, "\n")] = 0; // 去掉换行符
    
    • 将发送者的名字复制到 snd_msg.name 中。
    • 提示用户输入要发送的消息内容。
    • 使用 fgets 从标准输入读取消息内容,并存储在 snd_msg.text 中。
    • 使用 strcspn 去掉输入字符串末尾的换行符。
  5. 发送消息

    if (msgsnd(msgid, &snd_msg, sizeof(snd_msg) - sizeof(long), 0) < 0) {perror("msgsnd");exit(1);
    }
    
    • 使用 msgsnd 系统调用将消息发送到消息队列。
    • 如果发送失败,打印错误信息并退出程序。

 接收消息函数

函数持续监听指定的消息队列,接收指定类型的消患当接收到消息时,将打印出接收者名称,消息发送者名称以及消息内容函数通过调用msgrcv接收消息,如果接收失败则输出错误信息并终止程序


void receive_message(int msgid, const char *name, long type) {MSG recv_msg;while (1) {if (msgrcv(msgid, &recv_msg, sizeof(recv_msg) - sizeof(long), type, 0) < 0) {perror("msgrcv");exit(1);}printf("%s received message from %s: %s\n", name, recv_msg.name, recv_msg.text);}
}
  1. 函数声明

    void receive_message(int msgid, const char *name, long type)
    

    这个函数接收三个参数:

    • msgid:消息队列的标识符。
    • name:接收者的名字。
    • type:消息类型,用于过滤接收到的消息。
  2. 变量声明

    MSG recv_msg;
    

    CopyInsert

    声明一个 MSG 类型的变量 recv_msg,用于存储接收到的消息。

  3. 无限循环

    while (1) {
    

    使用一个无限循环来持续接收消息。

  4. 接收消息

    if (msgrcv(msgid, &recv_msg, sizeof(recv_msg) - sizeof(long), type, 0) < 0) {perror("msgrcv");exit(1);
    }
    

    调用 msgrcv 函数从消息队列中接收消息。参数解释如下:

    • msgid:消息队列的标识符。
    • &recv_msg:指向接收消息的缓冲区。
    • sizeof(recv_msg) - sizeof(long):接收消息的大小,减去 long 类型的大小是因为 msgrcv 函数需要的是消息数据的大小,而不是整个结构体的大小。
    • type:消息类型,用于过滤消息。
    • 0:标志位,这里设置为0表示默认行为。

    如果 msgrcv 调用失败,打印错误信息并退出程序。

  5. 打印接收到的消息

    printf("%s received message from %s: %s\n", name, recv_msg.name, recv_msg.text);
    

    打印接收到的消息,包括接收者的名字、发送者的名字和消息内容。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sys/wait.h>typedef struct msg {long type;       // 接收者类型char text[100];  // 发送内容char name[20];   // 发送者姓名
} MSG;void send_message(int msgid, const char *name) {MSG snd_msg;char recipient[20];printf("Enter recipient name: ");fgets(recipient, sizeof(recipient), stdin);recipient[strcspn(recipient, "\n")] = 0; // 去掉换行符// 根据接收者名字确定消息类型if (strcmp(recipient, "Tom") == 0) {snd_msg.type = 1;} else if (strcmp(recipient, "Lili") == 0) {snd_msg.type = 2;} else if (strcmp(recipient, "Luxi") == 0) {snd_msg.type = 3;} else {printf("Unknown recipient: %s\n", recipient);return;}strcpy(snd_msg.name, name);printf("Enter message to send: ");fgets(snd_msg.text, sizeof(snd_msg.text), stdin);snd_msg.text[strcspn(snd_msg.text, "\n")] = 0; // 去掉换行符if (msgsnd(msgid, &snd_msg, sizeof(snd_msg) - sizeof(long), 0) < 0) {perror("msgsnd");exit(1);}
}void receive_message(int msgid, const char *name, long type) {MSG recv_msg;while (1) {if (msgrcv(msgid, &recv_msg, sizeof(recv_msg) - sizeof(long), type, 0) < 0) {perror("msgrcv");exit(1);}printf("%s received message from %s: %s\n", name, recv_msg.name, recv_msg.text);}
}void chat_process(const char *name, long recv_type) {key_t key = ftok("/tmp", 65);int msgid = msgget(key, 0666 | IPC_CREAT);if (msgid < 0) {perror("msgget");exit(1);}pid_t pid = fork();if (pid < 0) {perror("fork");exit(1);} else if (pid == 0) {// 子进程负责接收消息receive_message(msgid, name, recv_type);} else {// 父进程负责发送消息while (1) {send_message(msgid, name);}wait(NULL);}
}int main(int argc, char *argv[]) {if (argc != 3) {fprintf(stderr, "Usage: %s <name> <recv_type>\n", argv[0]);exit(1);}const char *name = argv[1];long recv_type = atol(argv[2]);chat_process(name, recv_type);return 0;
}

运行的时候把打开多个程序就可以了。

http://www.xdnf.cn/news/105211.html

相关文章:

  • 人工智能催化民航业变革:五大应用案例
  • redis client.ttl(key)
  • day001
  • 高等数学第一章---函数与极限(1.2 数列的极限2)
  • Cluely 使用指南:一款重新定义“作弊”的AI工具
  • URP-UGUI相关知识
  • 220V转直流非隔离传感器供电电源芯片WT5105
  • 国际化不生效
  • 【数字图像处理】机器视觉(1)
  • QT之Q_PROPERTY介绍以及在QWidget中的用法
  • 操作系统学习笔记
  • 2025年阅读论文的常用工具推荐
  • STM32F407 的通用定时器与串口配置深度解析
  • CSRF攻击原理与解决方法
  • 强化学习算法笔记【AMP】
  • 渗透测试中的信息收集:从入门到精通
  • 心智模式VS系统思考
  • 海外产能达产,威尔高一季度营收利润双双大增
  • 1.5软考系统架构设计师:架构师的角色与能力要求 - 超简记忆要点、知识体系全解、考点深度解析、真题训练附答案及解析
  • 【ROS2】机器人操作系统安装到Ubuntu简介
  • deepseek-php-client开源程序是强力维护的 PHP API 客户端,允许您与 deepseek API 交互
  • 第十五届蓝桥杯 2024 C/C++组 艺术与篮球
  • 【redis】哨兵模式
  • MACD红绿灯副图指标使用技巧,绿灯做多,MACD趋势线,周期共振等实战技术解密
  • 信息系统项目管理工程师备考计算类真题讲解六
  • DeepSeek+Mermaid:轻松实现可视化图表自动化生成(附实战演练)
  • 2025 Java 框架痛点全解析:如何避免性能瓶颈与依赖混乱
  • TI芯片ADS1299的代替品LHE7909其应用领域
  • kali安装切换jdk1.8.0_451java8详细教程
  • Docker配置带证书的远程访问监听