Linux socket编程(12):Unix套接字之socketpair、sendmsg和recvmsg详解

在上一篇文章Unix套接字编程及通信例子中,我们对Unix套接字编程有一个基本的了解。但在Unix套接字编程的领域中,有一组特殊而强大的工具:socketpairsendmsgrecvmsg,它们为实现本地进程间通信提供了便捷的方式。

文章目录

  • 1 socketpair
  • 2 sendmsg和recvmsg
    • 2.1 函数原型
    • 2.2 msghdr结构体
    • 2.3 cmsghdr结构体
    • 2.4 实例
      • 2.4.1 初始化
      • 2.4.2 子进程实现
      • 2.4.3 父进程实现
    • 2.4.4 实验结果
    • 2.4.5 完整代码

1 socketpair

socketpair是一个用于在同一台计算机上创建一对相互连接的套接字的系统调用。这对套接字可以用于进程间的本地通信,通常用于父子进程或兄弟进程之间。它创建的套接字对是相互连接的,因此数据可以直接在这两个套接字之间传递,而无需经过内核缓冲区,从而提高了通信的效率。

int socketpair(int domain, int type, int protocol, int sv[2]);
  • domain:地址族,通常设置为 AF_UNIX,表示使用Unix域套接字。
  • type:套接字类型,通常设置为 SOCK_STREAMSOCK_DGRAM
  • protocol:指定使用的协议,通常设置为 0,表示使用默认协议。
  • sv:一个包含两个整数的数组,用于存储创建的套接字描述符。

这和匿名管道(pipe)很像,但匿名管道中的文件描述符是单方向的,只能支持一个方向的数据流,其中描述符0固定用于读,描述符1固定用于写。而socketpair是一个全双工通信通道,它同时支持双向的数据流。两个文件描述符都支持双向通信,下面来看一个例子:

#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>

void send_message(int sockfd, const char* message) {
    send(sockfd, message, strlen(message), 0);
}

void receive_message(int sockfd, char* buffer, size_t buffer_size) {
    ssize_t received_bytes = recv(sockfd, buffer, buffer_size - 1, 0);
    if (received_bytes > 0) {
        buffer[received_bytes] = '\0';  // Null-terminate the received data
        printf("Received: %s\n", buffer);
    } else {
        perror("Error receiving message");
    }
}

int main() {
    int sv[2];

    if (socketpair(AF_UNIX, SOCK_STREAM, 0, sv) == -1) {
        perror("Error creating socket pair");
        return -1;
    }

    pid_t pid = fork();

    if (pid == -1) {
        perror("Error forking");
        return -1;
    }

    if (pid == 0) {// 子进程
        close(sv[0]);
        char buffer[1024];
        receive_message(sv[1], buffer, sizeof(buffer));
        send_message(sv[1], "get a message from father");
        close(sv[1]);  // 关闭写端
    } else {// 父进程
        close(sv[1]);
        send_message(sv[0], "123");
        char buffer[1024];
        receive_message(sv[0], buffer, sizeof(buffer));
        close(sv[0]);  // 关闭读端
    }
    return 0;
}

在这个例子中,创建了一个子进程,其中sv[0]用于表示子进程的套接字,sv[1]用于表示父进程的套接字。在父进程中,向子进程发送123后开始接收数据,而子进程收到123后发送get a message from father给父进程,然后退出程序。父进程收到后也退出程序。实验结果如下:

在这里插入图片描述

在这里sv[0]sv[1]既用来读也用来写,表明这两个套接字都是全双工的。

2 sendmsg和recvmsg

2.1 函数原型

sendmsg函数向套接字发送消息,允许同时发送多个缓冲区的数据以及附带文件描述符等辅助信息。

recvmsg函数用于接收通过套接字传输的消息,并允许接收辅助数据,如控制信息、文件描述符等。

ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
  • sockfd:套接字描述符
  • msg:指向 struct msghdr结构的指针,该结构定义了消息的各个部分,包括数据缓冲区、控制信息等
  • flags:标志参数,通常设置为 0

2.2 msghdr结构体

struct msghdr结构的定义如下:

struct msghdr {
    void         *msg_name;       /* optional address */
    socklen_t     msg_namelen;    /* size of address */
    struct iovec *msg_iov;        /* scatter/gather array */
    size_t        msg_iovlen;     /* # elements in msg_iov */
    void         *msg_control;    /* ancillary data, see below */
    size_t        msg_controllen; /* ancillary data buffer len */
    int           msg_flags;      /* flags on received message */
};
  1. msg_name(消息地址):

    用于指定消息的目标地址,通常在发送消息时为sendmsg提供目标地址信息,而在接收消息时,可以存储发送方的地址。通常设置为NULL,表示不指定目标地址。

  2. msg_namelen(地址长度):

    用于指定msg_name指向的地址结构的长度。通常在发送消息时为sendmsg提供地址结构的长度,而在接收消息时则用于存储实际接收到的地址的长度。

  3. msg_iov(I/O 向量):

    msg_iov是一个指向struct iovec结构的指针,该结构用于指定消息中的数据缓冲区,可以是多个缓冲区。通过msg_iovlen来指定缓冲区数组的长度。

    struct iovec {
        void  *iov_base; /* 指向缓冲区的起始地址 */
        size_t iov_len;  /* 缓冲区的大小 */
    };
    
  4. msg_iovlen(I/O 向量长度):

    用于指定msg_iov指向的缓冲区数组的长度,即消息中包含多少个缓冲区。

    • 所以sendmsg/recvmsgsendto/recvfrom最明显的不同是,前者可以通过msg_iovmsg_iovlen发送/接收多个缓冲区,而后者只能发送/接收一个。
  5. msg_control(控制信息):

    msg_control用于传递辅助信息,通常是控制信息或者辅助数据。这可以包括在套接字编程中使用的辅助信息,如辅助文件描述符等。通常设置为NULL,表示不传递控制信息。

  6. msg_controllen(控制信息长度):

    用于指定 msg_control 指向的控制信息的长度。在发送消息时,为sendmsg提供控制信息的长度,而在接收消息时,用于存储实际接收到的控制信息的长度。

  7. msg_flags(消息标志):

    用于存储消息的标志,包括一些操作的状态信息。在recvmsg函数中,可以通过msg_flags获取一些接收消息时的状态信息。

2.3 cmsghdr结构体

在使用msg_control时,通常会搭配使用struct cmsghdr结构,该结构定义了一种通用的、可扩展的辅助数据头部。

struct cmsghdr {
    socklen_t cmsg_len;    /* 辅助数据的总长度 */
    int       cmsg_level;  /* 源层协议,一般设置为 SOL_SOCKET */
    int       cmsg_type;   /* 辅助数据的类型 */
    /* 后续紧随辅助数据 */
    //unsigned char cmsg_data[];
};

cmsg_level常见取值:

  1. SOL_SOCKET 表示这是与套接字相关的辅助数据。
  2. 自定义层级: 除了 SOL_SOCKET,还可以定义其他自定义的层级,用于特定的应用或协议

cmsg_type 常见取值(对于SOL_SOCKET层级):

  1. SCM_RIGHTS 表示辅助数据用于传递文件描述符。
  2. SCM_CREDENTIALS 表示辅助数据用于传递进程凭证(例如用户标识)。

在Linux中提供了一些宏定义来使用这个结构体:

  1. CMSG_FIRSTHDR宏: 获取消息头的第一个辅助数据块。如果消息头中没有足够的空间来存储一个struct cmsghdr,则返回 NULL

    #define CMSG_FIRSTHDR(mhdr) ((mhdr)->msg_controllen >= sizeof(struct cmsghdr) ? (struct cmsghdr *)(mhdr)->msg_control : NULL)
    
  2. CMSG_NXTHDR宏: 获取下一个辅助数据块。通过传递当前的辅助数据块,可以获取下一个辅助数据块的指针。如果没有下一个块,返回 NULL

    #define CMSG_NXTHDR(mhdr, cmsg) ((char *)(cmsg) + CMSG_ALIGN((cmsg)->cmsg_len) + sizeof(struct cmsghdr) > (char *)(mhdr)->msg_control + (mhdr)->msg_controllen ? NULL : (struct cmsghdr *)((char *)(cmsg) + CMSG_ALIGN((cmsg)->cmsg_len)))
    
  3. CMSG_DATA宏: 获取辅助数据块中实际数据的指针。通过传递辅助数据块的指针,可以获取实际数据的起始位置。

    #define CMSG_DATA(cmsg) ((unsigned char *)(cmsg) + CMSG_ALIGN(sizeof(struct cmsghdr)))
    
  4. CMSG_LEN宏: 计算一个辅助数据块的总长度,包括头部和实际数据。

    • 它的值等于结构体cmsghdr中的cmsg_len字段的值
    #define CMSG_LEN(len) (_CMSG_HDR_ALIGN(sizeof(struct cmsghdr)) + (len))
    
  5. CMSG_SPACE宏: 计算辅助数据块所需的总空间,包括头部和实际数据,并进行对齐

    #define CMSG_SPACE(len) (_CMSG_HDR_ALIGN(sizeof(struct cmsghdr)) + _CMSG_DATA_ALIGN(len))
    

    如下图所示,为msg_control字段的示意图

在这里插入图片描述

  • 图中的pad是为了字节对齐的填充部分

2.4 实例

上面的理论挺复杂的,理论还是得通过实践才能更好的理解。

目的:使用多个struct iovec来发送和接收一个缓冲区的数据,并在msg_control字段中传递文件描述符作为辅助数据。

2.4.1 初始化

首先声明两个buffer,这里设置buffer1的初始值为0xab,而buffer2的初始值为0xcd,为了后续判断内容是否成功接收。然后创建用于父子进程通信的套接字,并fork子进程。

#define BUF_SIZE 1024
unsigned char buffer1[BUF_SIZE], buffer2[BUF_SIZE];
memset(buffer1, 0xab, sizeof(buffer1));
memset(buffer2, 0xcd, sizeof(buffer2));

int sockfd[2];
socketpair(AF_UNIX, SOCK_STREAM, 0, sockfd);
pid_t pid = fork();

2.4.2 子进程实现

子进程发送buffer1的内容给父进程,同时在辅助信息中传递一个文件描述符。

(1)声明msghdr结构体

struct msghdr message = {0};

(2)填充发送缓冲区

首先填充我们要发送的字段:struct iovec *msg_iov,这里声明一个字段iov[1],内容为buffer1

struct iovec iov[1];
iov[0].iov_base = buffer1;
iov[0].iov_len = sizeof(buffer1);

message.msg_iov = iov;
message.msg_iovlen = 1;

(3)填充辅助信息

我们希望通过辅助信息传递文件描述符,首先声明辅助信息字段:

char control_data[CMSG_SPACE(sizeof(int))];

message.msg_control = control_data;
message.msg_controllen = sizeof(control_data);
  • control_data用于存储辅助数据。CMSG_SPACE(sizeof(int))用于计算辅助数据所需的总空间,包括头部和实际数据的空间,并进行对齐。我们现在想传递一个文件描述符(int类型),所以使用 sizeof(int) 计算其大小。

填充辅助信息字段:

struct cmsghdr *cmsg = CMSG_FIRSTHDR(&message);
cmsg->cmsg_len = CMSG_LEN(sizeof(int));
cmsg->cmsg_level = SOL_SOCKET;
cmsg->cmsg_type = SCM_RIGHTS;

int file_descriptor = open("example.txt", O_RDONLY);
*((int *)CMSG_DATA(cmsg)) = file_descriptor;

这里的CMSG_FIRSTHDR(&message)实际上就指向message.msg_control,通过这个宏定义强制转换,然后填充cmsg_lencmsg_levelcmsg_type字段。

  • cmsg_level设置为SOL_SOCKET,表示这是一个与套接字相关的辅助数据
  • cmsg_type设置为SCM_RIGHTS, 表示这是一个用于传递文件描述符的辅助数据块
  • *((int *)CMSG_DATA(cmsg))设置cmsgdata字段为文件描述符,这里打开目录下的example.txt文件

(4)发送数据

sendmsg(sockfd[1], &message, 0);

2.4.3 父进程实现

父进程则接收子进程发来的消息

(1)声明接收缓冲区和辅助信息结构体

接收和发送的数据大小要匹配,这里设置接收的iov_basebuffer2

struct iovec iov[1];
iov[0].iov_base = buffer2;
iov[0].iov_len = sizeof(buffer2);

char control_data[CMSG_SPACE(sizeof(int))];
struct msghdr message = {0};
message.msg_iov = iov;
message.msg_iovlen = 1;
message.msg_control = control_data;
message.msg_controllen = sizeof(control_data);

(2)接收消息

recvmsg(sockfd[0], &message, 0);

(3)打印接收缓冲区内容

这里就打印前4字节的内容,如果接收成功buffer2的内容应该为0xab,而不是0xcd。

printf("buffer2[0]~buffer2[4] = %x %x %x %x\n", buffer2[0], buffer2[1], buffer2[2], buffer2[3]);

(4)使用辅助信息中的文件描述符

这里得到子进程传过来的文件描述符,然后打开这个文件并读取到buffer2中,然后输出文件的内容。

// 从辅助数据中获取文件描述符
struct cmsghdr *cmsg = CMSG_FIRSTHDR(&message);
int received_fd;
memcpy(&received_fd, CMSG_DATA(cmsg), sizeof(int));

ssize_t bytes_read = read(received_fd, buffer2, sizeof(buffer2));

2.4.4 实验结果

首先我们需要在目录下创建一个example.txt文件,随便输入一点内容:

在这里插入图片描述

接着我们运行程序,实验结果如下:

在这里插入图片描述

符合我们的预期。

2.4.5 完整代码

#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define BUF_SIZE 1024

int main() {
    int sockfd[2];
    unsigned char buffer1[BUF_SIZE], buffer2[BUF_SIZE];
	memset(buffer1, 0xab, sizeof(buffer1));
	memset(buffer2, 0xcd, sizeof(buffer2));

    if (socketpair(AF_UNIX, SOCK_STREAM, 0, sockfd) == -1) {
        perror("Error creating socket pair");
        return -1;
    }

    pid_t pid = fork();

    if (pid == -1) {
        perror("Error forking");
        return -1;
    }

    if (pid == 0) {
        // 子进程 (发送方)
        close(sockfd[0]);  // 关闭子进程中不需要的读端

        // 打开文件并获取文件描述符
        int file_descriptor = open("example.txt", O_RDONLY);
        if (file_descriptor == -1) {
            perror("Error opening file");
            return -1;
        }

        // 准备消息
        struct iovec iov[1];
        iov[0].iov_base = buffer1;
        iov[0].iov_len = sizeof(buffer1);

        char control_data[CMSG_SPACE(sizeof(int))];
        struct msghdr message = {0};
        message.msg_iov = iov;
        message.msg_iovlen = 1;
        message.msg_control = control_data;
        message.msg_controllen = sizeof(control_data);

        // 构建控制信息头部
        struct cmsghdr *cmsg = CMSG_FIRSTHDR(&message);
        cmsg->cmsg_len = CMSG_LEN(sizeof(int));
        cmsg->cmsg_level = SOL_SOCKET;
        cmsg->cmsg_type = SCM_RIGHTS;

        // 将文件描述符复制到辅助数据中
        *((int *)CMSG_DATA(cmsg)) = file_descriptor;

        // 发送消息
        if (sendmsg(sockfd[1], &message, 0) == -1) {
            perror("Error sending message");
            close(file_descriptor);
            return -1;
        }

        close(file_descriptor);  // 不再需要文件描述符

        close(sockfd[1]);  // 关闭写端
    } else {
        // 父进程 (接收方)
        close(sockfd[1]);  // 关闭父进程中不需要的写端

        struct iovec iov[1];
        iov[0].iov_base = buffer2;
        iov[0].iov_len = sizeof(buffer2);

        char control_data[CMSG_SPACE(sizeof(int))];
        struct msghdr message = {0};
        message.msg_iov = iov;
        message.msg_iovlen = 1;
        message.msg_control = control_data;
        message.msg_controllen = sizeof(control_data);

        // 接收消息
        if (recvmsg(sockfd[0], &message, 0) == -1) {
            perror("Error receiving message");
            return -1;
        }
		printf("buffer2[0]~buffer2[4] = %x %x %x %x\n", buffer2[0], buffer2[1], buffer2[2], buffer2[3]);
        // 从辅助数据中获取文件描述符
        struct cmsghdr *cmsg = CMSG_FIRSTHDR(&message);
        int received_fd;
        memcpy(&received_fd, CMSG_DATA(cmsg), sizeof(int));

        // 读取文件内容
        printf("Received file descriptor: %d\n", received_fd);
        ssize_t bytes_read = read(received_fd, buffer1, sizeof(buffer1));
        if (bytes_read == -1) {
            perror("Error reading file");
            return -1;
        }
        // 打印文件内容
        printf("Received data from file: %.*s\n", (int)bytes_read, buffer1);

        close(received_fd);  // 关闭接收到的文件描述符
        close(sockfd[0]);    // 关闭读端
    }

    return 0;
}

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

python二维数组创建赋值问题:更改单个值却更改了所有项的值

test_list [] dic1 {} test_list [dic1 for _ in range(3)] ll [1, 2, 3]for i in range(3):test_list[i][value] ll[i]print(test_list)运行结果&#xff1a;每次赋值都更改了所有项 原因&#xff1a;python的二位数据创建方式就是这样&#xff0c;官方文档中有描述Wha…

大话数据结构-查找-线性索引查找

注&#xff1a;本文同步发布于稀土掘金。 4 线性索引查找 4.1 概述 索引就是把一个关键字与它对应的记录相关联的过程&#xff0c;一个索引由若干个索引项构成&#xff0c;每个索引项至少应包含关键字和其对应的记录在存储器中的位置等信息。 索引按照结构可分为线性索引、树…

【SpringBoot】在SpringBoot中配置序列化的Redis

文章目录 前言展示包结构在SpringBoot中配置Redis测试总结 前言 在使用Java操作Redis时&#xff0c;如果不对Redis进行序列化操作&#xff0c;可能会导致存储的key和value与原来的数据不一致的问题 本文也借此机会来详细讲解一下SpringBoot中配置序列化Redis的步骤 展示包结构 …

AI助力智慧农业,基于YOLOv7【tiny/yolov7/yolov7x】开发构建不同参数量级农田场景下庄稼作物、杂草智能检测识别系统

智慧农业随着数字化信息化浪潮的演变有了新的定义&#xff0c;在前面的系列博文中&#xff0c;我们从一些现实世界里面的所见所想所感进行了很多对应的实践&#xff0c;感兴趣的话可以自行移步阅读即可&#xff1a; 《自建数据集&#xff0c;基于YOLOv7开发构建农田场景下杂草…

绘图 Seaborn 10个示例

绘图 Seaborn 是什么安装使用显示中文及负号散点图箱线图小提琴图堆叠柱状图分面绘图分类散点图热力图成对关系图线图直方图 是什么 Seaborn 是一个Python数据可视化库&#xff0c;它基于Matplotlib。Seaborn提供了高级的绘图接口&#xff0c;可以用来绘制各种统计图形&#xf…

nodejs+vue+微信小程序+python+PHP新闻发布系统的设计与实现-计算机毕业设计推荐

根据现实需要&#xff0c;此系统我们设计出一下功能&#xff0c;主要有以下功能模板。 &#xff08;1&#xff09;新闻发布系统前台&#xff1a;首页、时事新闻、公告资讯、个人中心。 &#xff08;2&#xff09;管理员功能&#xff1a;首页、个人中心、用户管理、新闻分类管理…

文本编辑软件:Ulysses mac介绍说明

Ulysses mac是面向 Mac、iPhone 和 iPad 的一站式写作环境。Ulysses 提供令人愉悦、专注的写作体验&#xff0c;加上高效文稿管理、无缝同步以及灵活导出。markdown 可以直接对于文本进行不同类型的分类、编辑&#xff0c;比如标题、注解、评论之类的内容。 Ulysses让注意力专…

rpm安装gitlab

1.rpm包下载 https://mirrors.tuna.tsinghua.edu.cn/gitlab-ce/yum/el7/ 2.进行安装 rpm -ivh gitlab-ce-15.9.7-ce.0.el7.x86_64.rpm --nodeps --force 3.配置访问地址 vim /etc/gitlab/gitlab.rb 4.重新加载配置以及重启服务 gitlab-ctl reconfiguregitlab-ctl resta…

Ubuntur编译ROS报错:error PCL requires C++14 or above

ubuntu20.04 编译ROS包 报错&#xff1a; error&#xff1a; PCL requires C14 or above&#xff1a; 修改Cmakelists.txt文件&#xff1a; set&#xff08;CMAKE_CXX_STANDARD 14&#xff09; 再次编译成功.

2023 IoTDB 用户大会成功举办,深入洞察工业互联网数据价值

2023 年 12 月 3 日&#xff0c;中国通信学会作为指导单位&#xff0c;Apache IoTDB Community、清华大学软件学院、中国通信学会开源技术委员会联合主办&#xff0c;“科创中国”开源产业科技服务团和天谋科技&#xff08;北京&#xff09;有限公司承办的 2023 IoTDB 用户大会…

AI 绘画 | Stable Diffusion 动漫人物真人化

前言 如何让一张动漫人物变成真实系列人物?Stable Diffusion WebUI五步即可实现。快来使用AI绘画打开异世界的大门吧!!! 动漫真人化 首先在图生图里上传一张二次元动漫人物图片,然后选择一个真实系人物画风的大模型,最后点击DeepBooru 反推,自动填充提示词,调整重绘…

【MySQL】:数据库基本认识

数据库基础 一.什么是数据库1.mysql是什么2.为什么要有数据库3.服务器&#xff0c;数据库&#xff0c;表关系4.Mysql架构5.SQL语句分类 二.存储引擎 一.什么是数据库 1.mysql是什么 1.mysql是数据库服务的客户端。 2.mysqld是数据库服务的服务器端。 3.mysql本质&#xff1a;基…

【Python】logging模块函数详解和示例

在Python中&#xff0c;LOGGER通常是指一个用于记录日志的模块或对象。它可以帮助你在程序中跟踪和记录事件&#xff0c;以便于调试、错误跟踪和日志分析。Python的标准库中包含了一个名为logging的模块&#xff0c;它提供了一个灵活且功能强大的日志记录系统。本文对相应的函数…

unity 2d 入门 飞翔小鸟 下坠功能且碰到地面要停止 刚体 胶囊碰撞器 (四)

1、实现对象要受重力 在对应的图层添加刚体 改成持续 2、设置胶囊碰撞器并设置水平方向 3、地面添加盒状碰撞器 运行则能看到小鸟下坠并落到地面上

【南京站-EI会议征稿中】第三届网络安全、人工智能与数字经济国际学术会议(CSAIDE 2024)

第三届网络安全、人工智能与数字经济国际学术会议&#xff08;CSAIDE 2024&#xff09; 2024 3rd International Conference on Cyber Security, Artificial Intelligence and Digital Economy 第三届网络安全、人工智能与数字经济国际学术会议&#xff08;CSAIDE 2024&…

基于高德API实现网络geoJSON功能(突出省份)

代码实现&#xff1a; <script>// 3、初始化一个高德图层const gaode new ol.layer.Tile({title: "高德地图",source: new ol.source.XYZ({url: http://wprd0{1-4}.is.autonavi.com/appmaptile?langzh_cn&size1&style7&x{x}&y{y}&z{z},w…

SpringBoot 启动加载器解析

计时器介绍 启动加载器实战 实现方式1 实现CommandLineRunner接口重写run方法通过Order进行排序 示例: Component Order(1) public class FirstCommandlineRunner implements CommandLineRunner {Overridepublic void run(String... args) throws Exception {System.out.pr…

Windows server 部署iSCSI共享磁盘搭建故障转移群集

在域环境下&#xff0c;在域控制器中配置iSCSI服务&#xff0c;配置共享网络磁盘&#xff0c;在节点服务器使用共享磁盘&#xff0c;并在节点服务器中搭建故障转移群集&#xff0c;实现故障转移 环境准备 准备3台服务器&#xff0c;配置都是8g2核&#xff0c;50g硬盘&#xf…

前端实现检索文本高亮实现

文章目录 一、前言二、实现三、最后 一、前言 使用搜索引擎时的搜索结果高亮&#xff0c;搜索文本在查询出来的结果内高亮显示&#xff0c;这种在全文检索应该很常见 二、实现 看了下百度检索的实现&#xff0c;是给内容加上了em标签&#xff0c;然后给em标签设置颜色&#x…

记录 | vscode pyhton c++调试launch.json配置

下面提供 vscode 中 python 和 c 调试配置的 launch.json (好用&#xff0c;已用好几年&#xff0c;建议收藏) {// 使用 IntelliSense 了解相关属性。 // 悬停以查看现有属性的描述。// 欲了解更多信息&#xff0c;请访问: https://go.microsoft.com/fwlink/?linkid830387&qu…