一个网络服务程序报errno 22的情况

最近在学习写socket程序,然后就写了一个模拟web服务器的应答程序,下面我上源码。

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>

#define BUFFER_SIZE 1024
static const char *status_line[2] = {"200 OK", "500 Internal server error"};

int main(int argc, char *argv[]){
        if(argc <= 3){
                printf("usage: %s ip_address port_number filename\n", basename(argv[0]));
                        return 1;
        }

        const char* ip = argv[1];
        int port = atoi(argv[2]);
        const char *file_name = argv[3];

        struct sockaddr_in address;
        bzero(&address, sizeof(address));
        address.sin_family = AF_INET;
        inet_pton(AF_INET, ip, &address.sin_addr);
        address.sin_port = htons(port);

        int sock = socket(PF_INET, SOCK_STREAM, 0);
        assert(sock >= 0);

        int ret = bind(sock, (struct sockaddr*)&address, sizeof(address));
        assert(ret != -1);

        struct sockaddr_in client;
        socklen_t client_addrlength = sizeof(client);
        int connfd = accept(sock, (struct sockaddr*)&client, &client_addrlength);
        if(connfd < 0){
                printf("errno is : %d\n", errno);
        }else{
                char header_buf[BUFFER_SIZE];
                memset(header_buf, '\0', BUFFER_SIZE);
                char *file_buf;
                struct stat file_stat;
                bool valid = true;
                int len = 0;
                if(stat(file_name, &file_stat) < 0){
                        valid = false;
                }else{
                        if(S_ISDIR(file_stat.st_mode)){
                                valid = false;
                        }else if(file_stat.st_mode & S_IROTH){
                                int fd = open(file_name, O_RDONLY);
                                file_buf = new char[file_stat.st_size + 1];
                                memset(file_buf, '\0', file_stat.st_size + 1);
                                if(read(fd, file_buf, file_stat.st_size) < 0){
                                        valid = false;
                                }
                        }else{
                                valid = false;
                        }
                }

                if(valid){
                        ret = snprintf(header_buf, BUFFER_SIZE-1, "%s %s\r\n", "HTTP/1.1",
                                status_line[0]);

                        len += ret;
                        ret = snprintf(header_buf + len, BUFFER_SIZE-1-len,
                                "Content-Length: %d\r\n", file_stat.st_size);
                        len += ret;

                        ret = snprintf(header_buf + len, BUFFER_SIZE-1-len, "%s", "\r\n");

                        struct iovec iv[2];
                        iv[0].iov_base = header_buf;
                        iv[0].iov_len = strlen(header_buf);
                        iv[1].iov_base = file_buf;
                        iv[1].iov_len = file_stat.st_size;
                        ret = writev(connfd, iv, 2);
                }else{
                        ret = snprintf(header_buf, BUFFER_SIZE-1, "%s %s\r\n",
                                "HTTP/1.1", status_line[1]);
                        len += ret;
                        ret = snprintf(header_buf + len, BUFFER_SIZE-1-len, "%s", "\r\n");
                        send(connfd, header_buf, strlen(header_buf), 0);
                }

                close(connfd);
                delete[] file_buf;
        }

        close(sock);
        return 0;
}

我编译运行之后,总是报errno 22的错误。
./writev 192.168.91.128 5555 count1.c
errno is : 22
因为这个实在accept的函数中打印出来了,我查了一下errno,说是参数错误,然后我就去翻accept的定义啊,看了很久,觉得没什么问题啊。

然后上 优快云,也看到一个小伙伴报同样的错误,我就看了下他贴上来的源码,突然看到了listen,恍然大悟啊,我竟然在bind之后没有listen~~~~~~

可恶啊!!!

后来加上listen之后就没有问题了。


完整的源码:

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>

#define BUFFER_SIZE 1024
static const char *status_line[2] = {"200 OK", "500 Internal server error"};

int main(int argc, char *argv[]){
        if(argc <= 3){
                printf("usage: %s ip_address port_number filename\n", basename(argv[0]));
                        return 1;
        }

        const char* ip = argv[1];
        int port = atoi(argv[2]);
        const char *file_name = argv[3];

        struct sockaddr_in address;
        bzero(&address, sizeof(address));
        address.sin_family = AF_INET;
        inet_pton(AF_INET, ip, &address.sin_addr);
        address.sin_port = htons(port);

        int sock = socket(PF_INET, SOCK_STREAM, 0);
        assert(sock >= 0);

        int ret = bind(sock, (struct sockaddr*)&address, sizeof(address));
        assert(ret != -1);

        ret = listen(sock, 5);
        assert(ret != -1);

        struct sockaddr_in client;
        socklen_t client_addrlength = sizeof(client);
        int connfd = accept(sock, (struct sockaddr*)&client, &client_addrlength);
        if(connfd < 0){
                printf("errno is : %d\n", errno);
        }else{
                char header_buf[BUFFER_SIZE];
                memset(header_buf, '\0', BUFFER_SIZE);
                char *file_buf;
                struct stat file_stat;
                bool valid = true;
                int len = 0;
                if(stat(file_name, &file_stat) < 0){
                        valid = false;
                }else{
                        if(S_ISDIR(file_stat.st_mode)){
                                valid = false;
                        }else if(file_stat.st_mode & S_IROTH){
                                int fd = open(file_name, O_RDONLY);
                                file_buf = new char[file_stat.st_size + 1];
                                memset(file_buf, '\0', file_stat.st_size + 1);
                                if(read(fd, file_buf, file_stat.st_size) < 0){
                                        valid = false;
                                }
                        }else{
                                valid = false;
                        }
                }

                if(valid){
                        ret = snprintf(header_buf, BUFFER_SIZE-1, "%s %s\r\n", "HTTP/1.1",
                                status_line[0]);

                        len += ret;
                        ret = snprintf(header_buf + len, BUFFER_SIZE-1-len,
                                "Content-Length: %d\r\n", file_stat.st_size);
                        len += ret;

                        ret = snprintf(header_buf + len, BUFFER_SIZE-1-len, "%s", "\r\n");

                        struct iovec iv[2];
                        iv[0].iov_base = header_buf;
                        iv[0].iov_len = strlen(header_buf);
                        iv[1].iov_base = file_buf;
                        iv[1].iov_len = file_stat.st_size;
                        ret = writev(connfd, iv, 2);
                }else{
                        ret = snprintf(header_buf, BUFFER_SIZE-1, "%s %s\r\n",
                                "HTTP/1.1", status_line[1]);
                        len += ret;
                        ret = snprintf(header_buf + len, BUFFER_SIZE-1-len, "%s", "\r\n");
                        send(connfd, header_buf, strlen(header_buf), 0);
                }

                close(connfd);
                delete[] file_buf;
        }

        close(sock);
        return 0;
}

输出结果:

[root@localhost C]# telnet 192.168.91.128 5555
Trying 192.168.91.128...
Connected to 192.168.91.128.
Escape character is '^]'.
HTTP/1.1 200 OK
Content-Length: 270

#include <stdio.h>

int main(){
        int input;
        int flag = 1;
        int count = 0;
        scanf("%d",&input);
        printf("input is %d\n", input);
        while(flag){
                if(input & flag){
                        count++;
                }

                flag <<= 1;
        }

        printf("the number has %d 1 in binary mode.\n", count);
        return 0;
}
Connection closed by foreign host.


完!

<think> 我们面对的问题是在使用Pipe + Epoll实现时,写管道遇到errno 105(ECONNRESET)的错误,并希望有一个示例代码展示在收到报文时如何向管道写入报文数据。 根据引用[1],管道(pipe)是一种半双工的通信方式,通过pipe()系统调用创建,返回两个文件描述符:pipefd[0]用于读,pipefd[1]用于写。 errno 105(ECONNRESET)通常表示连接被对方重置。在管道上下文中,这通常发生在尝试写入一个已经被关闭的管道读端的情况。具体来说,当管道的读端被关闭后,写端再写入数据就会触发SIGPIPE信号,如果该信号被忽略或者阻塞,则write操作会返回-1,并设置errno为EPIPE(32)或ECONNRESET(104或105,具体取决于系统)?但注意,在Linux中,EPIPE对应errno为32,而105是ECONNRESET,通常用于套接字。然而,在某些情况下,管道也可能返回ECONNRESET吗?实际上,标准管道在写入时读端已关闭,应该返回EPIPE(32)。因此,我们需要确认这个errno 105的具体来源。 但是,根据Linux手册,当管道的读端被关闭,写端写入时会产生SIGPIPE信号,并且write返回EPIPE错误(errno=32)。而ECONNRESET(errno=104)通常用于套接字,表示连接被对方重置。因此,在管道上出现ECONNRESET的可能性较小。不过,如果程序在套接字和管道混合使用,那么可能是套接字连接被重置导致的错误。 然而,用户明确提到是“写管道”报错,所以我们需要考虑两种情况: 1. 用户误将套接字错误当成了管道错误(因为ECONNRESET常见于套接字)。 2. 或者,在特定环境下(如使用socketpair创建的管道?)可能会返回ECONNRESET。 根据问题,我们使用Pipe + Epoll,所以可能是用管道进行进程间通信,而epoll用来监听多个文件描述符(包括管道和可能的套接字)。 因此,解决方案: 1. 检查写管道时,管道的读端是否已经被关闭。如果读端被关闭,那么写操作会失败,此时应该关闭写端并做相应处理。 2. 检查是否误处理了套接字的错误(即可能把套接字的错误当作管道的错误)。 另外,引用[2][3]提到epoll的使用模式,以及非阻塞I/O。在非阻塞模式下,如果管道写操作返回EAGAIN(表示缓冲区满,暂时不可写),那么我们可以等待下次可写事件再写。但是,ECONNRESET不是这种情况。 所以,针对写管道报errno 105(ECONNRESET)的解决办法: - 确认错误是否发生在管道写操作上。如果是,那么可能是管道读端已经被关闭。我们需要确保读端在写操作期间保持打开。 - 如果是套接字出现ECONNRESET,那么意味着对方关闭了连接,我们需要关闭本地的套接字。 由于问题中提到的是管道,所以我们重点检查管道的读端是否被意外关闭。 示例代码要求:在收到报文时向管道写入报文数据。这里假设我们有一个网络套接字(非阻塞)接收报文,然后将报文通过管道发送给另一个进程(或线程)处理。 下面是一个示例场景: - 主线程使用epoll监听套接字(接收网络报文)和管道的读端(用于读取其他数据?),但是这里我们只关注收到网络报文后写入管道。 - 我们使用pipe创建管道,将写端(pipefd[1])加入epoll监听可写事件(但注意,管道写端通常不需要监听可写事件,除非在非阻塞模式下写入时遇到EAGAIN,才需要等待可写事件再继续写入)。通常,我们可以在收到数据时直接写入管道,但如果管道缓冲区满,则写入操作会阻塞或者返回EAGAIN(非阻塞模式)。为了避免阻塞,我们可以将写端设置为非阻塞,并在写入返回EAGAIN时,将写端加入epoll的可写事件监听,等待可写时再写入。 但是,注意:管道是单向的,我们这里只需要将网络接收到的数据写入管道的写端。 步骤: 1. 创建管道,并设置管道的写端为非阻塞。 2. 创建epoll实例,并将网络套接字(用于接收报文)添加到epoll中监听可读事件。 3. 当epoll_wait返回时,如果是网络套接字可读,则读取报文,然后尝试写入管道的写端。 4. 如果写入管道时返回EAGAIN(表示缓冲区满,暂时不可写),则我们将管道的写端加入epoll的可写事件监听,并保存剩余未写入的数据(或者将整个报文保存到缓冲区,等待可写事件再写入)。 5. 当epoll_wait返回管道的写端可写时,继续写入之前未写完的数据。 但是,注意:如果管道的读端被关闭,那么写端写入时会导致SIGPIPE信号(默认终止进程),如果我们忽略了SIGPIPE信号,那么write会返回错误,errno为EPIPE(32)或可能被转换为ECONNRESET?但通常EPIPE是32。 因此,为了避免进程被SIGPIPE终止,我们通常忽略SIGPIPE信号: signal(SIGPIPE, SIG_IGN); 这样,当写入一个读端已关闭的管道时,write会返回-1,errno设为EPIPE。 因此,在代码中,我们检查write的返回值: - 如果返回-1且errno为EAGAIN,则等待下次可写事件。 - 如果返回-1且errno为EPIPE(或ECONNRESET,但注意在Linux下应为EPIPE),则表示读端已经关闭,我们应关闭写端并停止写入。 示例代码结构: ```c #include <unistd.h> #include <fcntl.h> #include <sys/epoll.h> #include <signal.h> #include <string.h> #include <stdio.h> #include <errno.h> #define MAX_EVENTS 10 #define BUF_SIZE 1024 int main() { int pipefd[2]; if (pipe(pipefd) == -1) { perror("pipe"); return 1; } // 设置管道写端为非阻塞 int flags = fcntl(pipefd[1], F_GETFL, 0); fcntl(pipefd[1], F_SETFL, flags | O_NONBLOCK); // 忽略SIGPIPE信号 signal(SIGPIPE, SIG_IGN); // 创建epoll实例 int epollfd = epoll_create1(0); if (epollfd == -1) { perror("epoll_create1"); return 1; } // 假设我们有一个网络套接字sockfd(这里用标准输入代替演示) int sockfd = STDIN_FILENO; // 实际中应该是网络套接字 // 将sockfd加入epoll,监听可读事件 struct epoll_event ev; ev.events = EPOLLIN; ev.data.fd = sockfd; if (epoll_ctl(epollfd, EPOLL_CTL_ADD, sockfd, &ev) == -1) { perror("epoll_ctl: sockfd"); return 1; } // 我们暂时不将pipefd[1]加入epoll,因为我们一开始没有数据要写,等有数据要写且遇到EAGAIN时再加入 // 定义变量用于暂存未写完的数据 char buffer[BUF_SIZE]; ssize_t bytes_read; char *pending_data = NULL; // 指向未写完的数据 size_t pending_size = 0; // 未写完的数据大小 size_t pending_written = 0; // 已经写了多少 struct epoll_event events[MAX_EVENTS]; while (1) { int nfds = epoll_wait(epollfd, events, MAX_EVENTS, -1); if (nfds == -1) { perror("epoll_wait"); break; } for (int i = 0; i < nfds; i++) { // 如果是网络套接字可读 if (events[i].data.fd == sockfd) { bytes_read = read(sockfd, buffer, BUF_SIZE); if (bytes_read <= 0) { if (bytes_read == 0) { // 对端关闭 close(sockfd); epoll_ctl(epollfd, EPOLL_CTL_DEL, sockfd, NULL); } else { perror("read"); } continue; } // 现在将读取的数据写入管道 // 如果之前有未写完的数据,先处理完之前的,再处理新的(或者将新旧数据合并?这里我们简化:先处理未写完的,再写新的) // 但为了简化,我们这里先处理新数据,如果新数据写入部分成功,剩余部分和之前未写完的数据一起处理?实际上,我们这里只处理当前新数据,如果写不完,则保存当前数据并等待可写事件。 // 注意:我们这里没有处理之前未写完的数据,所以需要确保在写入新数据前,之前的数据已经全部写完。因此,我们需要在写新数据前检查是否有未写完的数据。 if (pending_data != NULL) { // 还有之前未写完的数据,不能继续读新数据,应该先写完之前的数据 // 但为了逻辑简单,我们这里先不读取新数据,而是等待可写事件来写入旧数据 // 所以,我们暂时不处理新数据,或者将新数据缓存起来?但这里为了简单,我们直接丢弃新数据(实际应用中应该缓存) fprintf(stderr, "还有未写完的数据,丢弃新数据\n"); continue; } // 尝试写入管道 ssize_t n = write(pipefd[1], buffer, bytes_read); if (n == -1) { if (errno == EAGAIN) { // 表示现在写不下,需要等待可写事件 // 保存未写完的数据(从buffer开始,整个bytes_read都没写) pending_data = (char*)malloc(bytes_read); if (pending_data == NULL) { perror("malloc"); break; } memcpy(pending_data, buffer, bytes_read); pending_size = bytes_read; pending_written = 0; // 将管道写端加入epoll监听可写事件 ev.events = EPOLLOUT; ev.data.fd = pipefd[1]; if (epoll_ctl(epollfd, EPOLL_CTL_ADD, pipefd[1], &ev) == -1) { perror("epoll_ctl: pipe write"); free(pending_data); pending_data = NULL; } } else if (errno == EPIPE) { // 管道的读端已经关闭 fprintf(stderr, "管道读端已关闭\n"); close(pipefd[1]); // 关闭写端 // 从epoll中移除写端?但我们还没有加入,所以不用移除 } else { perror("write pipe"); } } else if (n < bytes_read) { // 只写了一部分,需要保存剩余部分 pending_data = (char*)malloc(bytes_read - n); if (pending_data == NULL) { perror("malloc"); break; } memcpy(pending_data, buffer + n, bytes_read - n); pending_size = bytes_read - n; pending_written = 0; // 将管道写端加入epoll监听可写事件 ev.events = EPOLLOUT; ev.data.fd = pipefd[1]; if (epoll_ctl(epollfd, EPOLL_CTL_ADD, pipefd[1], &ev) == -1) { perror("epoll_ctl: pipe write"); free(pending_data); pending_data = NULL; } } // 如果全部写入,则无事 } // 如果管道写端可写 else if (events[i].data.fd == pipefd[1] && (events[i].events & EPOLLOUT)) { if (pending_data != NULL) { // 继续写入 ssize_t n = write(pipefd[1], pending_data + pending_written, pending_size - pending_written); if (n == -1) { if (errno == EAGAIN) { // 仍然不可写,等待下一次 continue; } else { // 其他错误,如EPIPE if (errno == EPIPE) { fprintf(stderr, "管道读端已关闭\n"); close(pipefd[1]); free(pending_data); pending_data = NULL; // 从epoll中移除写端 epoll_ctl(epollfd, EPOLL_CTL_DEL, pipefd[1], NULL); } else { perror("write pipe in EPOLLOUT"); } } } else { pending_written += n; if (pending_written == pending_size) { // 已经写完 free(pending_data); pending_data = NULL; // 从epoll中移除对写端的监听(因为我们没有数据要写了,直到下次有数据写不完再加入) epoll_ctl(epollfd, EPOLL_CTL_DEL, pipefd[1], NULL); } } } else { // 没有数据要写,移除监听 epoll_ctl(epollfd, EPOLL_CTL_DEL, pipefd[1], NULL); } } } } close(epollfd); close(pipefd[0]); close(pipefd[1]); if (pending_data) free(pending_data); return 0; } ``` 注意:上面的代码是一个示例框架,实际应用中需要根据具体情况进行调整。特别是,我们这里使用标准输入代替网络套接字,并且没有处理多个套接字的情况。 关于错误105(ECONNRESET)的解决: - 在管道写操作中,通常不会遇到ECONNRESET,所以如果遇到这个错误,请检查是否误用在套接字上。 - 如果是套接字出现ECONNRESET,意味着对端重置了连接,我们应该关闭该套接字。 另外,在代码中,我们检查了EPIPE错误(管道读端关闭)和EAGAIN(非阻塞写暂时失败)。如果确实遇到105错误,我们可以将其视为一个错误条件,关闭对应的文件描述符。 最后,为了确保管道的读端在写端写入时不被关闭,我们需要在程序设计中保证读端在需要时一直打开。 §§相关问题§§ 1. 如何在epoll中同时监听多个文件描述符(包括套接字和管道)? 2. 非阻塞I/O下如何处理部分写入的情况? 3. 如何避免因管道读端关闭而产生的SIGPIPE信号? 4. epoll的边缘触发(ET)模式和水平触发(LT)模式在管道写入中有何不同? </think> ### Pipe + Epoll 实现中写管道报 errno 105 的解决方案 #### 问题分析 errno 105 (`ECONNRESET`) 表示"连接被对端重置"。在 Pipe + Epoll 场景中,此错误通常由以下原因引起: 1. **管道读端关闭**:当写端尝试写入时,读端已被关闭 2. **EPOLLOUT 事件处理不当**:未正确处理缓冲区满的情况 3. **并发访问问题**:多线程/多进程同时操作管道描述符 $$ \text{errno} = \begin{cases} \text{EPIPE} & \text{(读端关闭)} \\ \text{EAGAIN} & \text{(缓冲区满)} \end{cases} $$ #### 解决方案 1. **检查读端状态**: ```c if (errno == ECONNRESET) { // 检查读端是否关闭 struct stat st; if (fstat(pipefd[0], &st) == -1 && errno == EBADF) { close(pipefd[1]); // 关闭写端 return; } } ``` 2. **正确处理 EPOLLOUT 事件**(引用[3]): ```c // 设置非阻塞模式 fcntl(pipefd[1], F_SETFL, O_NONBLOCK); // 添加EPOLLOUT监听 struct epoll_event ev; ev.events = EPOLLOUT | EPOLLET; // 边缘触发模式 ev.data.fd = pipefd[1]; epoll_ctl(epollfd, EPOLL_CTL_ADD, pipefd[1], &ev); ``` 3. **实现写入重试机制**: ```c ssize_t write_with_retry(int fd, const void* buf, size_t count) { ssize_t n; while ((n = write(fd, buf, count)) == -1) { if (errno == EAGAIN) { // 缓冲区满 usleep(1000); // 短暂等待 continue; } if (errno == ECONNRESET) { // 连接重置 return -1; } perror("write failed"); break; } return n; } ``` --- ### 收到报文时向管道写入报文数据的示例代码 ```c #include <sys/epoll.h> #include <unistd.h> #include <fcntl.h> #include <string.h> #include <errno.h> #define MAX_EVENTS 10 #define PIPE_READ 0 #define PIPE_WRITE 1 int main() { int pipefd[2]; // 1. 创建管道 if (pipe(pipefd) == -1) { perror("pipe creation failed"); return 1; } // 2. 设置非阻塞模式 fcntl(pipefd[PIPE_WRITE], F_SETFL, O_NONBLOCK); // 3. 创建epoll实例 int epollfd = epoll_create1(0); if (epollfd == -1) { perror("epoll_create1 failed"); return 1; } // 4. 注册管道写端(监听可写事件) struct epoll_event ev; ev.events = EPOLLOUT | EPOLLET; // 边缘触发模式 ev.data.fd = pipefd[PIPE_WRITE]; epoll_ctl(epollfd, EPOLL_CTL_ADD, pipefd[PIPE_WRITE], &ev); struct epoll_event events[MAX_EVENTS]; char packet[] = "Sample network packet data"; while (1) { int nfds = epoll_wait(epollfd, events, MAX_EVENTS, -1); for (int i = 0; i < nfds; i++) { // 5. 收到报文事件(此处简化为直接写入) if (/* 收到报文的条件 */) { ssize_t bytes_written = 0; size_t total_bytes = strlen(packet); // 6. 非阻塞写入 while (bytes_written < total_bytes) { ssize_t n = write(pipefd[PIPE_WRITE], packet + bytes_written, total_bytes - bytes_written); if (n == -1) { if (errno == EAGAIN) { // 缓冲区满,等待下次EPOLLOUT事件 break; } else if (errno == ECONNRESET) { // 读端已关闭 close(pipefd[PIPE_WRITE]); break; } else { perror("write error"); break; } } bytes_written += n; } // 7. 处理部分写入情况 if (bytes_written < total_bytes) { // 保存剩余数据,等待下次可写事件 // 实际实现需使用缓冲区队列 } } } } close(pipefd[PIPE_READ]); close(pipefd[PIPE_WRITE]); close(epollfd); return 0; } ``` #### 关键点说明 1. **边缘触发(EPOLLET)**: - 只在状态变化时通知(引用[3]更新) - 必须完全处理所有可用数据 $$ \text{处理量} = \min(\text{缓冲区大小}, \text{待写入数据量}) $$ 2. **错误处理**: - `EAGAIN`:临时错误,等待下次可写事件 - `ECONNRESET`/`EPIPE`:永久错误,关闭描述符 3. **数据完整性**: - 实现数据缓冲区队列存储未完成写入 - 使用环形缓冲区减少内存拷贝 4. **性能优化**: ```c // 批量写入优化 struct iovec iov[2]; iov[0].iov_base = header; iov[0].iov_len = sizeof(header); iov[1].iov_base = payload; iov[1].iov_len = payload_len; ssize_t n = writev(pipefd[PIPE_WRITE], iov, 2); ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值