recvmsg和sendmsg函数

2023-05-16

在unp第14章讲了这两个函数,但是只是讲了两个数据结构及参数而已,所以自己想根据介绍来重构udp回射的客户端程序。但是sendmsg和recvmsg都遇到了问题,并且纠结了很久,所以在此记录下。

1. 基础介绍

recvmsg和sendmsg是最通用的I/O函数,只要设置好参数,read、readv、recv、recvfrom和write、writev、send、sendto等函数都可以对应换成这两个函数来调用。

#include <sys/socket.h>
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
ssizt_t sendmsg(int sockfd, struct msghdr *msg, int flags);

函数的参数少,说明msghdr参数就比较复杂了,因为需要的参数都被封装到这个参数了。msghdr数据结构如下:

struct msghdr {
    void          *msg_name;            /* protocol address */
    socklen_t     msg_namelen;          /* sieze of protocol address */
    struct iovec  *msg_iov;             /* scatter/gather array */
    int           msg_iovlen;           /* # elements in msg_iov */
    void          *msg_control;         /* ancillary data ( cmsghdr struct) */
    socklen_t     msg_conntrollen;      /* length of ancillary data */
    int           msg_flags;            /* flags returned by recvmsg() */
}

msg_name和msg_namelen用于套接字未连接的时候(主要是未连接的UDP套接字),用来指定接收来源或者发送目的的地址。两个成员分别是套接字地址及其大小,类似recvfrom和sendto的第二和第三个参数。对于已连接套接字,则可直接将两个参数设置为NULL和0。而对于recvmsg,msg_name是一个值-结果参数,会返回发送端的套接字地址。
msg_iov和msg_iovlen两个成员用于指定数据缓冲区数组,即iovec结构数组。iovec结构如下:

#include <sys/uio.h>
struct iovec {
    void    *iov_base;      /* starting address of buffer */
    size_t  iov_len;        /* size of buffer */
}

其中iov_base就是一个缓冲区元素,事实上也是一个数组,而iov_len则是指定该数据的大小。也就是说,缓冲区是一个二维数组,并且每一维长度不是固定的。猜测这样子设置应该是方便传递多个结构类型不同,并且长度也是不固定的数据吧,这样子客户端就可以直接对每个位置的数据进行转换获取就行了。如果只是当存传送一个字符串,那只需要将msg_iovlen设置成1,然后将数据赋给iov[0].iov_base就行了。无论是sendmsg和recvmsg,都需要提前设置好这两项并且分配好内存。
msg_control和msg_controllen是用来设置辅助数据的位置和大小的,辅助数据(ancillary data)也叫作控制信息(control infomation)。这两个成员可以用来返回关于数据报文的其他指定信息,不过需要通过setsockopt函数指定要返回的辅助信息。对于sendmsg,这两项需要都设置成0,否则会导致发送数据失败。还未研究过sendmsg的辅助数据能够做什么。
关于两个函数的flags参数和msghdr的msghdr的msg_flags成员,目前没有研究。

2. 辅助数据

由于辅助数据涉及内容较多,故分出一节来讲。unp中给出了下面各种辅助数据的用途:

协议cmsg_levelcmsg_type说明
IPv4IPPROTO_IPIP_RECVDSTADDR随UDP数据报接收目的的地址
IP_RECVIF随UDP数据报接收接口的索引
IPv6IPPROTO_IPV6IPV6_DSTOPTS指定/接收目的地选项
IPV6_HOPLIMIT指定/接收跳限
IPV6_HOPOPTS指定/接收步跳选项
IPV6_NEXTHOP指定下一跳地址
IPV6_PKTINFO指定/接收分组信息
IPV6_PTHDR指定/接收路由首部
IPV6_TCLASS指定/接收分组流通类别
Unix域SOL_SOCKETSCM_RIGHTS发送/接收描述符
SCM_CREDS发送/接收用户凭证

其中cmsg_level和cmsg_type应该和调用setsockopt函数时传递的level和optname参数是一样的。那么我们怎么获取辅助数据呢,在msg_control辅助数据是通过一个或多个辅助数据对象保存的,辅助数据对象cmsghdr结构如下

#include <sys/socket.h>
struct cmsghdr {
    socklen_t   cmsg_len;   /* length in bytes, including this structure */
    int         cmsg_level; /* originating protocol */
    int         cmsg_type;  /* protocol-specific type */
    /* followed by unsigned char cmsg_data[] */
}

而辅助数据对象在实际的存储中是如下分布的(因为不知道在markdown中设置表格宽度,所以有点长):

cmsg_len
cmsg_level
cmsg_type
填充字节
数据

cmsghdr中实际上只有三个元素,而cmsg_data成员实际上并不存在,只是用来表明接下来都是数据,并且实际上数据和结构中还存在着填充数据。填充数据可能是为了对齐(unp中讲到msg_control指向的辅助数据必须为cmsghdr结构适当的对齐),在两个cmsghdr之间也存在着填充数据。
看到这里的时候我是很郁闷的,那我要怎么获取到辅助数据呢?一开始以为要自己手动给cms_data分配内存,但是我连cmsg_data成员都获取不到啊!然后仔细看了unp中的内容才发现可以通过下面5个CMSG_XXX宏来获取和设置辅助数据。

#include <sys/socket.h>
#include <sys/param.h>
struct cmsghdr *CMSG_FIRSTHDR(struct msghdr *mhdrptr);
    //返回:指向第一个cmsghdr结构的指针,若无辅助数据则为NULL
struct cmsghdr *CMSG_NXTHDR(struct msghdr *mhdrptr, struct cmsghdr *cmsghdr);
    //返回:指向下一个cmsghdr结构的指针,若不再有辅助数据对象则为NULL
unsigned char *CMSG_DATA(struct cmsghdr *cmsgptr);
    //返回:指向与cmsghdr结构关联的数据的第一个字节的指针
unsigned char *CMSG_LEN(unsigned int length);
    //返回:给定数据量下存放到cmsg_len中的值
unsigned char *CMSG_SPACE(unsigned int length);
    //返回:给定数据量下一个辅助数据对象总的大小。

通过上面五个宏我们可以很方便的为msg_control分配内存和遍历辅助对象、获取辅助数据。不过对于分配内存一般需要预先知道要获取的辅助数据结构的大小。而CMSG_LEN和CMSG_SPACE的区别在于后者会包含两个辅助数据之间的填充字节。

char contorl[CMSG_SPACE(size_of_struct1) + CMSG_SPACE(size_of_struct2)];
struct msghdr msg;
struct cmsghdr *cmsgptr;
for ( cmsgptr = CMSG_FIRSTHDR(&msg); cmsgptr != NULL; 
    cmsgptr = CMSG_NXTHDR(&msg, cmsgptr) ) {
    /* 判断是否是自己需要的msg_level和msg_type */
    u_char *ptr;
    ptr = CMSG_DATA(cmsgptr); /* 获取辅助数据 */
}

3. 琐碎

  1. 对于已连接的套接字,msghdr的msg_name直接设置为NULL,对于recvmsg,该成员会返回对端的套接字地址。
  2. 对于sendmsg,msghdr的msg_control和msg_controllen需要设置为0,不设置为似乎无法发送成功。
  3. 处理辅助数据可以直接用5个宏,并且需要根据msg_level和msg_type判断辅助数据的类型再进行相应的转换。unp中讲到的很多cmsg_type可能自己的系统中并没有移植,这点需要注意。比如我使用kubuntu,就没有移植IP_RECVDSTADDR和IP_RECVIF。最后我是参考网上的例子,改用IP_PKTINFO才完成了例子,也是在这里纠结和浪费了很多时间。实际上unp第7章的函数就可以用来判断这些设置项是否存在,也可以在调用setsockopt和判断msg_level、msg_type之前用#if defined语句来判断本系统是否兼容该项,如果不兼容的话会直接跳过接下来的处理(见例子)。
  4. msg_level和msg_type需要注意支持的协议。

4. 例子

下面是自己写的udp回射客户端程序,代码可能有点凌乱。但基本包含了上面所讲的知识点,可以直接与unp中的udp回射服务器端程序配合使用。

/* unpudpsendmsg.c */
#include <stdio.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/time.h>

#define SERV_PORT 51002
#define MAXLINE   256
#define SA struct sockaddr

void
err_quit(const char *sstr)
{
    printf("%s\n", sstr);
    exit(0);
}

int 
main(int argc, char **argv)
{
    int sockfd, n;
    struct sockaddr_in servaddr, dstaddr;
    char buff[MAXLINE], buff2[MAXLINE];
    struct msghdr msgsent, msgrecvd;
    struct cmsghdr cmsg, *cmsgtmp;
    struct iovec iov, iov2;
    const int on = 1;
    char control[CMSG_SPACE(64)];  // 使用CMSG_DATA分配cmsg_control内存,实际应该根据已知的结构分配。

    if ( argc < 2 ) {
        err_quit("usage: unpudpsendmsg <IPaddress>");
    }

    sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);

    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    inet_pton(AF_INET, argv[1], &servaddr.sin_addr);
    servaddr.sin_port = htons(SERV_PORT);

    // 处理sendmsg的msghdr结构
    msgsent.msg_name = NULL;
    msgsent.msg_namelen = 0;
    msgsent.msg_iovlen = 1;
    iov.iov_base = buff;         // 为iov[0]分配内存
    iov.iov_len = MAXLINE;
    msgsent.msg_iov = &iov;
    msgsent.msg_control = 0;     // 对sendmsg,msg_control要设置为0。
    msgsent.msg_controllen = 0;

    // 处理recvmsg的msghdr结构
    msgrecvd.msg_name = &dstaddr;
    msgrecvd.msg_control = control;
    msgrecvd.msg_controllen = sizeof(control);
    iov2.iov_base = (void *)buff2;
    iov2.iov_len = MAXLINE;
    msgrecvd.msg_iov = &iov2;
    msgrecvd.msg_iovlen = 1;
    msgrecvd.msg_flags = 0;

    connect(sockfd, (SA *)&servaddr, sizeof(servaddr));
#if defined(IP_PKTINFO)
    setsockopt(sockfd, IPPROTO_IP, IP_PKTINFO, &on, sizeof(on));
#elif defined(IP_RECVDSTADDR)
    setsockopt(sockfd, IPPROTO_IP, IP_RECVORIGDSTADDR, &on, sizeof(on));
#endif

    while ( 1 ) {
        fgets(buff, MAXLINE, stdin);
        n = sendmsg(sockfd, &msgsent, 0);
        if ( n <= 0 ) { 
            continue;
        }
        n = recvmsg(sockfd, &msgrecvd, 0);
        printf("recvmsg: %s", (char *)msgrecvd.msg_iov[0].iov_base);
            // 通过缓冲数据组获取服务器端返回的数据。
        printf("msg_controllen: %d\n", (int)msgrecvd.msg_controllen);
        for ( cmsgtmp = CMSG_FIRSTHDR(&msgrecvd); cmsgtmp != NULL; cmsgtmp = CMSG_NXTHDR(&msgrecvd, cmsgtmp) ) {
#if defined(IP_RECVDSTADDR)
            if ( cmsgtmp->cmsg_level == IPPROTO_IP && cmsgtmp->cmsg_type == IP_RECVDSTADDR ) {
            // 判断msg_level和msg_type再进行相应的处理。
                struct sockaddr_in *addrtmp;
                char ip[14];
                addrtmp = (struct sockaddr_in *)CMSG_DATA(cmsgtmp);
                inet_ntop(AF_INET, addrtmp, ip, sizeof(ip));
                printf("recv ip: %s, port: %d\n", ip,  ntohs(addrtmp->sin_port));
            }
#elif defined(IP_PKTINFO)
            if ( cmsgtmp->cmsg_level == IPPROTO_IP && cmsgtmp->cmsg_type == IP_PKTINFO ) {
                struct in_pktinfo *pktinfo;
                pktinfo = (struct in_pktinfo*)CMSG_DATA(cmsgtmp);
                printf("recv ip: %s, ifindex: %d\n", inet_ntoa(pktinfo->ipi_addr), pktinfo->ipi_ifindex);
            }
#endif
        }
    }
}

运行服务器程序,再运行unpudpsendmsg 127.0.0.1后,输入字符串,可以看到类似下面的输出:

walker@Walker-s $ ./unpudpsendmsg 127.0.0.1
11111
recvmsg: 11111
msg_controllen: 32
recv ip: 127.0.0.1, ifindex: 1
2222
recvmsg: 2222
msg_controllen: 32
recv ip: 127.0.0.1, ifindex: 1
333                                                                                                                                                             
recvmsg: 333                                                                                                                                                    
msg_controllen: 32           

根据辅助数据我们得到了对端的IP和接收数据报所用的接口索引。
但是该程序偶尔会出现获取不到返回的数据的问题,还未弄清楚为什么会出现这种现象。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

recvmsg和sendmsg函数 的相关文章

  • spring cloud 使用Eureka server :报错 java.net.ConnectException: Connection refused: connect

    记录一个深坑的问题 xff1a 小白在学习spring cloud 教程 xff0c 因为在教程中用的配置文件是yml格式 xff0c 而我的idea默认自动生成的配置文件是properties格式 xff0c 所以踩了两个坑 xff0c
  • spring RestTemplate

    原文地址 xff1a https blog csdn net u012702547 article details 77917939 https github com lenve SimpleSpringCloud tree master
  • win10 无法打开wlan 解决方案

    win10 xff0c 遇到了无法开启移动热点的情况 xff0c 开关呈灰色状态 xff0c 而且提示 xff1a 无法设置移动热点 请打开WLAN 解决办法 xff1a 1 设备管理器 网络适配器 确保无线网卡在启动状态 xff0c 能够
  • Jetson TK1学习(一) 刷机

    前言 最早接触TK1是在2015年7月份的DJI ROBOMASTER夏令营里 xff0c 当时DJI推出了用于无人机平台的skymind嵌入式计算机 xff0c 也就是后来的 妙算 34 当时我并没有关注这款计算机的架构 只是当时同组的算
  • Jetson TK1学习(二)安装无线网卡

    上一篇文章讲了Jetson TK1的刷机方法 由于TK1自身没有无线网卡 xff0c 单位里面也没有有线的路由器 xff0c 所以需要在TK1上安装个无线网卡 TK1上有PCIE的接口 xff0c 目前对Intel 7260AC这款无线网卡
  • 教你创建Custom Device自定义设备

    1 问题描述 在半物理仿真测试中需要调用仿真机上的硬件设备实现仿真设备间数据通信 模拟和数字信号的采集或驱动等功能 在Veristand实时测试配置环境中可以通过创建Custom Device自定义设备实现PXI机箱 xff08 仿真机 x
  • 汉字编码(【Unicode】 【UTF-8】 【Unicode与UTF-8之间的转换】 【汉字 Unicode 编码范围】【中文标点Unicode码】【GBK编码】【批量获取汉字UNICODE码】)

    参考博客 xff1a Unicode与UTF 8互转 C语言实现 xff1a http blog csdn net tge7618291 article details 7599902 汉字 Unicode 编码范围 xff1a http
  • Visual Stdio实现云+端跨平台开发优势,Windows Azure实现移动跨平台

    Visual Stdio实现云 43 端跨平台开发优势 xff08 csdn会议总结 xff09 现代企业的架构平台 xff0c 目前移动开发的平台有哪些 xff1a 如何实现跨平台 xff1a Windows Azure 云端服务在 上海
  • C++类库

    如果你有一定的C基础可能学起来比较容易些 但是学习C 43 43 的过程中又要尽量避免去使用一些C中的思想 平时还要多看一些高手写的代码 遇到问题多多思考 怎样才能把问题抽象化 以使自己头脑中有类的概念 最后别忘了经常上机自己调调程序 这是
  • Mac上安装node和npm

    通过安装包来安装 第 1 步 xff1a 下载适用于 macOS 的 NPM 包 去Node JS官网下载Mac操作系统的npm包 您可以单击此处打开下载页面并为您的系统选择合适的文件 第 2 步 xff1a 按照指南运行包管理器 打开下载
  • Java Maven项目问题整理

    文章目录 src main java文件夹src test java文件夹src main java和src test java文件夹区别读取maven项目中src test resources里的配置文件maven 项目导入本地jar包
  • Lombok使用总结

    文章目录 介绍Lombok原理常用注解 64 Data 64 Getter 64 Setter 64 ToString 64 EqualsAndHashCode 64 NoArgsConstructor 64 AllArgsConstruc
  • 解决:PDFBox报的java.io.IOException: Missing root object specification in trailer

    文章目录 问题描述原因分析解决方案 问题描述 使用pdfbox类库操作pdf文件时 xff0c 遇到下面的报错信息 xff1a java io IOException Missing root object specification in
  • KEIL每次都要编译全部文件并且每个文件编译三次

    SYD8801是一款低功耗高性能蓝牙低功耗SOC xff0c 集成了高性能2 4GHz射频收发机 32位ARM Cortex M0处理器 128kB Flash存储器 以及丰富的数字接口 SYD8801片上集成了Balun无需阻抗匹配网络
  • Mac上Golang语言环境搭建

    文章目录 官网其他参考安装golang源码安装安装包安装使用homebrew安装 配置GOROOTGOPATHGOPROXYGOPRIVATEGONOSUMDB 安装测试 官网 目前无法在家里的直接访问golang org网站 xff1a
  • [已解决] Mac上docker安装prometheus报错:Are you trying to mount a directory onto a file (or vice-versa)?

    文章目录 项目场景问题描述原因分析解决方案 项目场景 Mac上通过docker安装prometheus 问题描述 docker run时 xff0c 会出现下面的报错 xff0c 导致容器启动失败 xff1a docker Error re
  • Mac上安装Node Exporter

    文章目录 安装Node Exporter方法一 xff1a 手动安装方法二 xff1a docker安装 运行测试 node exporter 可以采集机器 xff08 物理机 虚拟机 云主机等 xff09 的监控指标数据 xff0c 能够
  • Docker安装Grafana

    文章目录 Grafana介绍拉取镜像准备相关挂载目录及文件启动容器访问测试添加 Prometheus 数据源常见问题 看板配置 Grafana介绍 上篇博客介绍了prometheus的安装 xff1a Docker部署Prometheus
  • Springboot应用接入Prometheus监控

    文章目录 接入介绍操作步骤修改应用的依赖及配置步骤1 xff1a 修改 pom 依赖步骤2 xff1a 修改配置 本地验证prometheus配置 接入介绍 在使用 Spring Boot 作为开发框架时 xff0c 需要监控应用的状态 x

随机推荐

  • Spring Boot自带监控组件—Actuator介绍

    文章目录 Actuator介绍启用与暴露的区别Spring Boot集成Actuator应用监控框架Actuator监控端点启用端点端点的默认暴露规则案例 自定义端点 Actuator介绍 Actuator是Spring Boot提供的应用
  • Git Commit提交规范总结

    文章目录 前言git commit 提交规范提交消息头 commit message header 提交消息具体内容 commit message body 提交消息尾述 commit message footer Revert 表情 Em
  • 常用kubectl命令总结

    文章目录 配置kubeconfig帮助信息命令查看具体某一个命令的帮助信息列出全局的选项参数 xff08 适用所有的命令 xff09 显示合并的 kubeconfig 配置或一个指定的 kubeconfig 文件 基本命令罗列所支持的完整资
  • 解决:org.apache.catalina.connector.ClientAbortException: java.io.IOException: 断开的管道

    文章目录 项目场景问题描述原因分析解决方案 项目场景 jdk11 Spring Boot 2 x 项目 xff0c Tomcat容器 Nginx 问题描述 系统日志中 xff0c 时不时会出现下面的异常信息 xff1a org apache
  • 解决:No converter for [xxxx] with preset Content-Type ‘text/plain;version=0.0.4;charset=utf-8‘

    文章目录 项目背景问题描述问题分析解决方案方案一 xff1a 修改Controller定义方案二 xff1a 修改Controller返回值方案三 xff1a 全局处理 项目背景 Spring Boot 2 X 问题描述 错误信息如下 xf
  • SYD8821 串口模块使用说明【串口0中断要屏蔽底层调用】

    SYD8821是具有全球领先低功耗 RX 2 4mA 64 94 5dBm灵敏度 xff0c TX 4 3mA 64 0dBm输出功率 的蓝牙低功耗SOC芯片 xff0c 在极低电流下实现了优异的射频性能 xff0c 搭配176kB SRA
  • MySQL的information_schema库下的常用sql

    文章目录 information schema TABLES查看该数据库实例下所有库大小 MB为单位 查看该实例下各个库大小 MB为单位 查看表大小 MB为单位 熟练使用 information schema库里的表 显示在库里的表 xff
  • shell脚本批量转文件格式:dos2unix

    文章目录 可以使用shell脚本实现 xff1a span class token shebang important bin sh span span class token assign left variable dir span s
  • 解决:com.atomikos.icatch.SysException: Error in init: Log already in use? tmlog in ./

    文章目录 项目场景问题描述原因分析详细分析 解决方案 项目场景 Spring Boot 2 x xff0c 集成 atomikos 问题描述 今天在同一个环境启动两个项目时报错 xff0c 因为两个项目同时涉及到分布式事物和切换数据源相关
  • Nginx日志介绍

    文章目录 access log日志流量统计 access log 日志文件一般存放在 var log nginx 下 xff0c 可以使用 tail f命令查看access日志 span class token function tail
  • JVM (Micrometer)-4701面板参数介绍

    文章目录 Quick Facts 概览 堆和非堆内存有以下几个概念 I O Overview xff08 服务黄金指标 xff09 JVM Memory xff08 JVM内存 xff09 JVM Misc xff08 JVM负载 xff0
  • curl文件传输命令

    CURL curl transfer a URL curl 是一个利用URL语法在命令行下工作的文件传输工具 支持文件上传和下载 格式 curl options URLs URL xff1a 通过大括号指定多个url 示例 xff1a cu
  • RS-485信号解析

    这次来看看RS 485信号 使用绿联的USB转RS485模块 线用的颜色不对 xff0c 类型也不对 xff0c 实际使用中请用带屏蔽层的双绞线 示波器CH1是R xff08 B xff09 示波器CH2是R 43 xff08 A xff0
  • T t与T t = T()的区别

    主要的区别就是默认构造函数对内置类型的初始化上 如果没有T中没有定义构造函数 xff0c 则对于 T t xff0c 并不会对 t 中内置类型设置初始值 xff0c 是一个随机值 但对于 T t 61 T xff0c 对 t 中内置类型会设
  • Effective STL:杂记(一)

    1 避免使用vector lt bool gt vector lt bool gt 实际上并不能算是一个STL容器 xff0c 实际上也并不存储bool 因为一个对象要成为STL容器 xff0c 就必须满足C 43 43 标准的第23 1节
  • 限制长度双向链表的插入操作

    面试遇到的问题 xff0c 一开始面试官是问我有什么方案可以实现排行榜 xff0c 当时给出了两个方案 后面面试官又在我的其中一种方案上让我手写代码实现排序双线链表的插入 xff0c 根据score值插入 xff0c 并且链表长度限制在10
  • SYD8821 IIC模块使用说明

    SYD8821是具有全球领先低功耗 RX 2 4mA 64 94 5dBm灵敏度 xff0c TX 4 3mA 64 0dBm输出功率 的蓝牙低功耗SOC芯片 xff0c 在极低电流下实现了优异的射频性能 xff0c 搭配176kB SRA
  • Python下载文件时出现乱码的解决方法之一:Content-Encoding: gzip

    之前写过一个简单的爬虫程序 xff0c 这次想试着再写一个下载固定文件的爬虫程序 写完之后发现下载的文件 xff0c 有些是可以正常打开的 xff0c 而有些是提示了编码错误 xff0c 用wireshark抓包 xff0c 过滤出http
  • Python爬虫判断url链接的是下载文件还是html文件

    最近在写一个网络爬虫的代码 xff0c 提供命令行来下载文件或者是打印根域名下指定节点及深度的子节点 用的是urllib2库 xff0c 算是比较简单 xff0c 但是功能并没有很强大 说重点吧 xff0c 在实际爬网页的过程中 xff0c
  • recvmsg和sendmsg函数

    在unp第14章讲了这两个函数 xff0c 但是只是讲了两个数据结构及参数而已 xff0c 所以自己想根据介绍来重构udp回射的客户端程序 但是sendmsg和recvmsg都遇到了问题 xff0c 并且纠结了很久 xff0c 所以在此记录