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

news2024/11/29 9:33:40

在上一篇文章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;
}

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

java学习part39map

159-集合框架-Map不同实现类的对比与HashMap中元素的特点_哔哩哔哩_bilibili 1.Map 2.Entry 个人理解是c的pair&#xff0c;代表一个键值对。Map就是entry的叠加 3.常用方法 4.TreeMap 5.Properties

二叉搜索树——模拟

对于一个无穷的满二叉排序树&#xff08;如图&#xff09;&#xff0c;节点的编号是1,2,3&#xff0c;…。对于一棵树根为X的子树&#xff0c;沿着左节点一直往下到最后一层&#xff0c;可以获得该子树编号最小的节点&#xff1b;沿着右节点一直往下到最后一层&#xff0c;可以…

第十六届山东省职业院校技能大赛中职组网络安全赛项竞赛正式试题

第十六届山东省职业院校技能大赛中职组网络安全"赛项竞赛试题 一、竞赛时间 总计&#xff1a;360分钟 二、竞赛阶段 竞赛阶段任务阶段竞赛任务竞赛时间分值A、B模块A-1登录安全加固180分钟200分A-2本地安全策略设置A-3流量完整性保护A-4事件监控A-5服务加固A-6防火墙策…

企业微信开启调试模式

1.关闭企业微信&#xff0c;重新启动 2.启动后按快捷键 ctrlaltshiftD进入调试模式 3.在需要调试的页面点击右键&#xff0c;点击"ShowDevTools"&#xff0c;进入调试模式 企业微信 为企业打造的专业办公管理工具&#xff1b;企业微信开放生态平台&#xff1b;企业微…

UniApp H5 跨域代理配置并使用(配置manifest.json、vue.config.js)

UniApp 运行到浏览器的时候&#xff0c;接口会跨域报错&#xff0c;这里通过两种方式解决&#xff0c;第一&#xff1a;修改Uniapp自带的manifest.json 源码视图并进行配置h5设置。第二&#xff1a;在项目根目录新建vue.config.js并配置代理。 二选一即可。 修改或调整配置文件…

QT 中基于 TCP 的网络通信 (备查)

基础 基于 TCP 的套接字通信需要用到两个类&#xff1a; 1&#xff09;QTcpServer&#xff1a;服务器类&#xff0c;用于监听客户端连接以及和客户端建立连接。 2&#xff09;QTcpSocket&#xff1a;通信的套接字类&#xff0c;客户端、服务器端都需要使用。 这两个套接字通信类…

记一次mq消息没有收到的问题排查

快速定位和修复问题是程序员的一项基本功&#xff0c;而只有把问题定位准确&#xff0c;才能有针对性的修复。在程序的世界里&#xff0c;神马都是数据。当数据没有按照预期从源头到达目的地&#xff0c;那一定是中间的某个环节出了问题。搞清楚整个链路的模型&#xff08;包括…

Error: Could not create the Java Virtual Machine(Linux启动tomcat成功后找不到进程8080端口)

文章目录 问题解决问题过程Tomcat版本要求 问题解决 版本冲突&#xff0c;我的jdk是1.8.x&#xff0c; tomcat 是 10.1.x的&#xff0c;要求jdk是11。 问题过程 运行 ./startup.sh 显示如下&#xff1a; 还以为运行成功呢&#xff0c; 使用命令一查&#xff0c;根本查不到进…

Java - InetAddress#isReachable 方法解析

文章目录 前言代码资源 前言 在 Java 中&#xff0c;InetAddress 类提供一个方法来检查一个网络地址是否可达&#xff0c;其作用类似与在命令行执行 ping 命令&#xff0c; 这个方法就是 isReachable 方法。 代码 var baidu InetAddress.getByName("www.baidu.com&quo…

Python之html2text,清晰解读HTML内容!

更多Python学习内容&#xff1a;ipengtao.com 大家好&#xff0c;我是彭涛&#xff0c;今天为大家分享 Python之html2text&#xff0c;清晰解读HTML内容&#xff0c;全文3900字&#xff0c;阅读大约10分钟。 HTML是Web开发中常见的标记语言&#xff0c;但有时我们需要将HTML内容…

LabVIEW发开发电状态监测系统

LabVIEW发开发电状态监测系统 对发电设备的持续监测对于确保可靠的电力供应至消费者极为重要。它不仅能够及时提醒操作员注意发电设备的潜在损坏&#xff0c;还能减少由于设备故障造成的停机时间。为了达到这一目标&#xff0c;开发了一款基于LabVIEW的软件&#xff0c;专门用…

【基于openGauss5.0.0简单使用DBMind】

基于openGauss5.0.0简单使用DBMind 一、环境说明二、初始化tpch测试数据三、使用DBMind索引推荐功能四、使用DBMind实现SQL优化功能 一、环境说明 虚拟机&#xff1a;virtualbox操作系统&#xff1a;openEuler 20.03 TLS数据库&#xff1a;openGauss-5.0.0DBMind&#xff1a;d…

信道的极限容量

目录 信道的最高码元传输速率 限制码元在信道上的传输速率的因素&#xff1a; &#xff08;1&#xff09;信道能够通过的频率范围 &#xff08;2&#xff09; 信噪比 任何实际的信道都不是理想的&#xff0c;在传输信号时会产生各种失真以及带来多种干扰。 码元传输的速率越…

copilot的使用

&#x1f4d1;打牌 &#xff1a; da pai ge的个人主页 &#x1f324;️个人专栏 &#xff1a; da pai ge的博客专栏 ☁️宝剑锋从磨砺出&#xff0c;梅花香自苦寒来 &#x1f324;️安装和配置编辑器&a…

使用GPT-4V解决Pycharm设置问题

pycharm如何实现关联&#xff0c;用中文回答 在PyCharm中关联PDF文件类型&#xff0c;您可以按照以下步骤操作&#xff1a; 1. 打开PyCharm设置&#xff1a;点击菜单栏中的“File”&#xff08;文件&#xff09;&#xff0c;然后选择“Settings”&#xff08;设置&#xff09;。…

逆向修改Unity的安卓包资源并重新打包

在上一篇文章中,我已经讲过如何逆向获取unity打包出来的源代码和资源了,那么这一节我将介绍如何将解密出来的源代码进行修改并重新压缩到apk中。 其实在很多时候,我们不仅仅想要看Unity的源码,我们还要对他们的客户端源码进行修改和调整,比如替换资源,替换服务器连接地址…

机器的深度强化学习算法可以被诱导

设计一个好的奖励函数是机器深度强化学习算法的关键之一。奖励函数用于给予智能体&#xff08;机器&#xff09;在环境中采取不同行动时的反馈信号&#xff0c;以指导其学习过程。一个好的奖励函数应该能够引导智能体朝着期望的行为方向学习&#xff0c;并尽量避免潜在的问题&a…

案例059:基于微信小程序的在线投稿系统

文末获取源码 开发语言&#xff1a;Java 框架&#xff1a;SSM JDK版本&#xff1a;JDK1.8 数据库&#xff1a;mysql 5.7 开发软件&#xff1a;eclipse/myeclipse/idea Maven包&#xff1a;Maven3.5.4 小程序框架&#xff1a;uniapp 小程序开发软件&#xff1a;HBuilder X 小程序…

阿里云云通信短信申请教程免费试用3个月

目录 第一步 开通试用短信 第二步、设置调试参数 第三步、根据文档对接 第一步 开通试用短信 阿里云免费试用 - 阿里云 第二步、设置调试参数 打开试用文档 第三步、根据文档对接 SendSms_短信服务_API文档-阿里云OpenAPI开发者门户

C#核心笔记——(三)在C#中创建类型

3.1 类 类是最常见的一种引用类型&#xff0c;最简单的类的声明如下&#xff1a; class MyClass{}而复杂的类可能包含如下内容&#xff1a; 1.在class关键字之前&#xff1a;类的特性&#xff08;Attribute&#xff09;和修饰符。非嵌套的类修饰符有&#xff1a; public、int…