C 基于UDP实现一个简易的聊天室

引言

  本文是围绕Linux udp api 构建一个简易的多人聊天室.重点看思路,帮助我们加深

对udp开发中一些api了解.相对而言udp socket开发相比tcp socket开发注意的细节要少很多.

但是水也很深. 本文就当是一个demo整合帮助开发者回顾和继续了解 linux udp开发的基本流程.

首先我们来看看 linux udp 和 tcp的异同.

复制代码
/*
这里简单比较一下TCP和UDP在编程实现上的一些区别:

TCP流程
     建立一个TCP连接需要三次握手,而断开一个TCP则需要四个分节。当某个应用进程调用close(主动端)后(可以是服务器端,也可以是客户 
端),这一端的TCP发送一个FIN,表示数据发送完毕;另一端(被动端)发送一个确认,当被动端待处理的应用进程都处理完毕后,发送一个FIN到主动 
端,并关闭套接口,主动端接收到这个FIN后再发送一个确认,到此为止这个TCP连接被断开。 

UDP套接口 
  UDP套接口是无连接的、不可靠的数据报协议;既然他不可靠为什么还要用呢?
  其一:当应用程序使用广播或多播是只能使用UDP协议;
  其二:由于它是无连接的,所以速度快。因为UDP套接口是无连接的,如果一方的数据报丢失,那另一方将无限等待,解决办法是设置一个超时。 
     在编写UDP套接口程序时,有几点要注意:建立套接口时socket函数的第二个参数应该是SOCK_DGRAM,说明是建立一个UDP套接口;
    由于UDP是无连接的,所以服务器端并不需要listen或accept函数;
    当UDP套接口调用connect函数时,内核只记录连接放的IP地址 和端口,并立即返回给调用进程.
*/
复制代码

 参照

    linux udp api简介   http://blog.csdn.net/wocjj/article/details/8315559

     tcp 和udp区别    http://www.cnblogs.com/Jessy/p/3536163.html

这里简单引述一下 udp相比tcp 用到的两个api .  recvfrom()/sendto() 具体细节如下

复制代码
#include <sys/types.h>  
#include <sys/socket.h> 

/*
 * 这两个函数基本等同于 一个 send 和 recv . 详细参数解释如下
 * s        : 文件描述符,等同于 socket返回的值
 * buf        : 数据其实地址
 * len        : 发送数据长度或接受数据缓冲区最大长度
 * flags    : 发送标识,默认就用O.带外数据使用 MSG_OOB, 偷窥用MSG_PEEK .....
 * addr     : 发送的网络地址或接收的网络地址
 * alen     : sento标识地址长度做输入参数, recvfrom表示输入和输出参数.可以为NULL此时addr也要为NULL
 *        : 返回0表示执行成功,否则返回<0 . 更多细节查询man手册
 */
extern int sendto (int s, const void *buf, int len, unsigned int flags, const struct sockaddr *addr, int alen);  
extern int recvfrom(int s, void *buf, int len, unsigned int flags, struct sockaddr *addr, int *alen); 
复制代码

上面就是两个函数的大致用法. 具体可以查看linux api帮助手册. 最好就用 man sendto / man recvfrom 把那一系列函数都看看.

现在很多文章都是转载,但是找不见转载的地址, 下面会举一个简易的UDP回显服务器的demo加深理解.

 

前言

  首先看设计图

有点low. 简单看看吧. 那我们先看 客户端代码  udpclt.c 代码

复制代码
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>
#include <fcntl.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/socket.h>

// 测试端口和网络地址
#define _INT_PORT (8088)
#define _INT_BUF 1024

// udp 服务器主函数
int main(int argc, char* argv[]) {
    int sd, len;
    struct sockaddr_in addr = { AF_INET };
    socklen_t alen = sizeof addr;
    char msg[_INT_BUF];    
    
    //创建服务器socket 地址,客户端给它发送信息
    if((sd = socket(PF_INET, SOCK_DGRAM, 0)) < 0) {
        perror("main socket ");
        exit(sd);
    }
    // 这里简单输出连接信息
    printf("udp server start [%d][0.0.0.0][%d] -------> \n", sd, _INT_PORT);    

    //拼接对方地址
    addr.sin_port = htons(_INT_PORT);
    addr.sin_addr.s_addr = INADDR_ANY;
    if(bind(sd, (struct sockaddr*)&addr, sizeof addr) < 0){
        perror("main bind ");
        exit(-1);
    }
    
    // 循环处理消息读取发送到客户端
    while((len = recvfrom(sd, msg, sizeof msg - 1, 0, (struct sockaddr*)&addr, &alen))>0){
        msg[len] = '\0';
        printf("read [%s:%d] mag-->%s\n",inet_ntoa(addr.sin_addr), ntohs(addr.sin_port),  msg);
        //这里发送信息过去, 也可以事先connect这里就不绑定了
        sendto(sd, msg, len, 0, (struct sockaddr*)&addr, alen);
    }    

    close(sd);
    puts("udp server end ------------------------------<");

    return 0;
}
复制代码

编译是

gcc -g -Wall -o udpclt.out udpclt.c

udp 服务器 udpsrv.c

复制代码
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>
#include <fcntl.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/socket.h>

// 测试端口和网络地址
#define _INT_PORT (8088)
#define _INT_BUF 1024

// udp 服务器主函数
int main(int argc, char* argv[]) {
    int sd, len;
    struct sockaddr_in addr = { AF_INET };
    socklen_t alen = sizeof addr;
    char msg[_INT_BUF];    
    
    //创建服务器socket 地址,客户端给它发送信息
    if((sd = socket(PF_INET, SOCK_DGRAM, 0)) < 0) {
        perror("main socket ");
        exit(sd);
    }
    // 这里简单输出连接信息
    printf("udp server start [%d][0.0.0.0][%d] -------> \n", sd, _INT_PORT);    

    //拼接对方地址
    addr.sin_port = htons(_INT_PORT);
    addr.sin_addr.s_addr = INADDR_ANY;
    if(bind(sd, (struct sockaddr*)&addr, sizeof addr) < 0){
        perror("main bind ");
        exit(-1);
    }
    
    // 循环处理消息读取发送到客户端
    while((len = recvfrom(sd, msg, sizeof msg - 1, 0, (struct sockaddr*)&addr, &alen))>0){
        msg[len] = '\0';
        printf("read [%s:%d] mag-->%s\n",inet_ntoa(addr.sin_addr), ntohs(addr.sin_port),  msg);
        //这里发送信息过去, 也可以事先connect这里就不绑定了
        sendto(sd, msg, len, 0, (struct sockaddr*)&addr, alen);
    }    

    close(sd);
    puts("udp server end ------------------------------<");

    return 0;
}
复制代码

编译是

gcc -g -Wall -o udpsrv.out udpsrv.c

后面运行结果如下 udp服务器如下 (Ctrl + C 退出)

udp 客户端如下 (Ctrl + D 结束输入)

到这里将上面代码 敲一遍基本上udp 一套api就会使用了. 后面进入正题设计聊天室代码.

 

正文

  首先看客户端设计代码. 主要思路是子进程处理数据的输出, 父进程处理服务器数据的接收. 具体设计如下(画的图有点low就不画了.../(ㄒoㄒ)/~~)

udpmulclt.c

复制代码
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>
#include <sys/wait.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/socket.h>

// 名字长度包含'\0'
#define _INT_NAME (64)
// 报文最大长度,包含'\0'
#define _INT_TEXT (512)

//4.0 控制台打印错误信息, fmt必须是双引号括起来的宏
#define CERR(fmt, ...) \
    fprintf(stderr,"[%s:%s:%d][error %d:%s]" fmt "\r\n",\
         __FILE__, __func__, __LINE__, errno, strerror(errno),##__VA_ARGS__)

//4.1 控制台打印错误信息并退出, t同样fmt必须是 ""括起来的字符串常量
#define CERR_EXIT(fmt,...) \
    CERR(fmt,##__VA_ARGS__),exit(EXIT_FAILURE)
/*
 * 简单的Linux上API错误判断检测宏, 好用值得使用
 */
#define IF_CHECK(code) \
    if((code) < 0) \
        CERR_EXIT(#code)

// 发送和接收的信息体
struct umsg{
    char type;                //协议 '1' => 向服务器发送名字, '2' => 向服务器发送信息, '3' => 向服务器发送退出信息
    char name[_INT_NAME];    //保存用户名字
    char text[_INT_TEXT];    //得到文本信息,空间换时间
};

/*
 * udp聊天室的客户端, 子进程发送信息,父进程接受信息
 */
int main(int argc, char* argv[]) {
    int sd, rt;
    struct sockaddr_in addr = { AF_INET };
    socklen_t alen = sizeof addr;
    pid_t pid;
    struct umsg msg = { '1' };    

    // 这里简单检测
    if(argc != 4) {
        fprintf(stderr, "uage : %s [ip] [port] [name]\n", argv[0]);
        exit(-1);
    }    
    // 下面对接数据
    if((rt = atoi(argv[2]))<1024 || rt > 65535)
        CERR("atoi port = %s is error!", argv[2]);
    // 接着判断ip数据
    IF_CHECK(inet_aton(argv[1], &addr.sin_addr));
    addr.sin_port = htons(rt);
    // 这里拼接用户名字
    strncpy(msg.name, argv[3], _INT_NAME - 1);
    
    //创建socket 连接
    IF_CHECK(sd = socket(PF_INET, SOCK_DGRAM, 0));
    // 这里就是发送登录信息给udp聊天服务器了
    IF_CHECK(sendto(sd, &msg, sizeof msg, 0, (struct sockaddr*)&addr, alen));    
    
    //开启一个进程, 子进程处理发送信息, 父进程接收信息
    IF_CHECK(pid = fork());
    if(pid == 0) { //子进程,先忽略退出处理防止成为僵尸进程
        signal(SIGCHLD, SIG_IGN);                
        while(fgets(msg.text, _INT_TEXT, stdin)){
            if(strcasecmp(msg.text, "quit\n") == 0){ //表示退出
                msg.type = '3';
                // 发送数据并检测
                IF_CHECK(sendto(sd, &msg, sizeof msg, 0, (struct sockaddr*)&addr, alen));
                break;
            }
            // 洗唛按发送普通信息
            msg.type = '2';
            IF_CHECK(sendto(sd, &msg, sizeof msg, 0, (struct sockaddr*)&addr, alen));
        }
        // 处理结算操作,并杀死父进程
        close(sd);
        kill(getppid(), SIGKILL);
        exit(0);
    }
    // 这里是父进程处理数据的读取
    for(;;){
        bzero(&msg, sizeof msg);
        IF_CHECK(recvfrom(sd, &msg, sizeof msg, 0, (struct sockaddr*)&addr, &alen));
        msg.name[_INT_NAME-1] = msg.text[_INT_TEXT-1] = '\0';
        switch(msg.type){
        case '1':printf("%s 登录了聊天室!\n", msg.name);break;
        case '2':printf("%s 说了: %s\n", msg.name, msg.text);break;
        case '3':printf("%s 退出了聊天室!\n", msg.name);break;
        default://未识别的异常报文,程序直接退出
            fprintf(stderr, "msg is error! [%s:%d] => [%c:%s:%s]\n", inet_ntoa(addr.sin_addr),
                    ntohs(addr.sin_port), msg.type, msg.name, msg.text);
            goto __exit;
        }
    }    

__exit:    
    // 杀死并等待子进程退出
    close(sd);
    kill(pid, SIGKILL);
    waitpid(pid, NULL, -1);    

    return 0;
}
复制代码

这里主要需要注意的是

// 发送和接收的信息体
struct umsg{
    char type;                //协议 '1' => 向服务器发送名字, '2' => 向服务器发送信息, '3' => 向服务器发送退出信息
    char name[_INT_NAME];    //保存用户名字
    char text[_INT_TEXT];    //得到文本信息,空间换时间
};

传输和接收的数据格式, type表示协议或行为. 我这里细心了处理 name, text最后一个字符必须是 '\0'. 其它都是业务代码.再扯一点

struct sockaddr_in addr = { AF_INET };

等价于

struct sockaddr_in addr;
memset(&addr, 0, sizeof addr);
addr.sin_family = AF_INET;

也是一个C开发中技巧吧. 再扯一点linux上提供 bzero函数, 但是window上没有. 写了个通用的如下

//7.0 置空操作
#ifndef BZERO
//v必须是个变量
#define BZERO(v) \
    memset(&v,0,sizeof(v))
#endif/* !BZERO */    

可以试试吧毕竟跨平台....

好了那我们说 udp 聊天室的服务器设计思路. 就是服务器会维护一个客户端链表. 有信息来就广播. 好简单吧.就是这样.正常的事都简单.

简单的是美的. 好了看代码总设计和实现. udpmulsrv.c

复制代码
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>
#include <sys/wait.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/socket.h>

// 名字长度包含'\0'
#define _INT_NAME (64)
// 报文最大长度,包含'\0'
#define _INT_TEXT (512)

//4.0 控制台打印错误信息, fmt必须是双引号括起来的宏
#define CERR(fmt, ...) \
    fprintf(stderr,"[%s:%s:%d][error %d:%s]" fmt "\r\n",\
         __FILE__, __func__, __LINE__, errno, strerror(errno),##__VA_ARGS__)

//4.1 控制台打印错误信息并退出, t同样fmt必须是 ""括起来的字符串常量
#define CERR_EXIT(fmt,...) \
    CERR(fmt,##__VA_ARGS__),exit(EXIT_FAILURE)
/*
 * 简单的Linux上API错误判断检测宏, 好用值得使用
 */
#define IF_CHECK(code) \
    if((code) < 0) \
        CERR_EXIT(#code)

// 发送和接收的信息体
struct umsg{
    char type;                //协议 '1' => 向服务器发送名字, '2' => 向服务器发送信息, '3' => 向服务器发送退出信息
    char name[_INT_NAME];    //保存用户名字
    char text[_INT_TEXT];    //得到文本信息,空间换时间
};

// 维护一个客户端链表信息,记录登录信息
typedef struct ucnode {
    struct sockaddr_in addr;
    struct ucnode* next;
} *ucnode_t ;


// 新建一个结点对象
static inline ucnode_t _new_ucnode(struct sockaddr_in* pa){
    ucnode_t node = calloc(sizeof(struct ucnode), 1);    
    if(NULL == node)
        CERR_EXIT("calloc sizeof struct ucnode is error. ");
    node->addr = *pa;
    return node;
}

// 插入数据,这里head默认头结点是当前服务器结点
static inline void _insert_ucnode(ucnode_t head, struct sockaddr_in* pa) {
    ucnode_t node = _new_ucnode(pa);
    node->next = head->next;
    head->next = node;    
}

// 这里是有用户登录处理
static void _login_ucnode(ucnode_t head, int sd, struct sockaddr_in* pa, struct umsg* msg) {
    _insert_ucnode(head, pa);
    head = head->next;
    // 从此之后才为以前的链表
    while(head->next){
        head = head->next;
        IF_CHECK(sendto(sd, msg, sizeof(*msg), 0, (struct sockaddr*)&head->addr, sizeof(struct sockaddr_in)));
    }
}

// 信息广播
static void _broadcast_ucnode(ucnode_t head, int sd, struct sockaddr_in* pa, struct umsg* msg) {
    int flag = 0; //1表示已经找到了
    while(head->next) {
        head = head->next;
        if((flag) || !(flag=memcmp(pa, &head->addr, sizeof(struct sockaddr_in))==0)){
            IF_CHECK(sendto(sd, msg, sizeof(*msg), 0, (struct sockaddr*)&head->addr, sizeof(struct sockaddr_in)));
        }
    }
}

// 有人退出群聊
static void _quit_ucnode(ucnode_t head, int sd, struct sockaddr_in* pa, struct umsg* msg) {
    int flag = 0;//1表示已经找到
    while(head->next) {
        if((flag) || !(flag = memcmp(pa, &head->next->addr, sizeof(struct sockaddr_in))==0)){
            IF_CHECK(sendto(sd, msg, sizeof(*msg), 0, (struct sockaddr*)&head->next->addr, sizeof(struct sockaddr_in)));
            head = head->next;
        }        
        else { //删除这个退出的用户
            ucnode_t tmp = head->next;
            head->next = tmp->next;
            free(tmp);
        }
    }        
}

// 销毁维护的对象池,没有往复杂的考虑了简单处理退出了
static void _destroy_ucnode(ucnode_t* phead) {
    ucnode_t head;
    if((!phead) || !(head=*phead)) return;    
    while(head){
        ucnode_t tmp = head->next;
        free(head);
        head = tmp;
    }    

    *phead = NULL;
}

/*
 * udp聊天室的服务器, 子进程广播信息,父进程接受信息
 */
int main(int argc, char* argv[]) {
    int sd, rt;
    struct sockaddr_in addr = { AF_INET };
    socklen_t alen = sizeof addr;
    struct umsg msg;    
    ucnode_t head;

    // 这里简单检测
    if(argc != 3) {
        fprintf(stderr, "uage : %s [ip] [port]\n", argv[0]);
        exit(-1);
    }    
    // 下面对接数据
    if((rt = atoi(argv[2]))<1024 || rt > 65535)
        CERR("atoi port = %s is error!", argv[2]);
    // 接着判断ip数据
    IF_CHECK(inet_aton(argv[1], &addr.sin_addr));
    addr.sin_port = htons(rt); //端口要采用网络字节序
    // 创建socket
    IF_CHECK(sd = socket(PF_INET, SOCK_DGRAM, 0));
    // 这里bind绑定设置的地址
    IF_CHECK(bind(sd, (struct sockaddr*)&addr, alen));
    
    //开始监听了
    head = _new_ucnode(&addr);    
    for(;;){
        bzero(&msg, sizeof msg);
        IF_CHECK(recvfrom(sd, &msg, sizeof msg, 0, (struct sockaddr*)&addr, &alen));
        msg.name[_INT_NAME-1] = msg.text[_INT_TEXT-1] = '\0';
        fprintf(stdout, "msg is [%s:%d] => [%c:%s:%s]\n", inet_ntoa(addr.sin_addr),
                    ntohs(addr.sin_port), msg.type, msg.name, msg.text);
        // 开始判断处理
        switch(msg.type) {
        case '1':_login_ucnode(head, sd, &addr, &msg);break;
        case '2':_broadcast_ucnode(head, sd, &addr, &msg);break;
        case '3':_quit_ucnode(head, sd, &addr, &msg);break;
        default://未识别的异常报文,程序把其踢走
            fprintf(stderr, "msg is error! [%s:%d] => [%c:%s:%s]\n", inet_ntoa(addr.sin_addr),
                    ntohs(addr.sin_port), msg.type, msg.name, msg.text);
            _quit_ucnode(head, sd, &addr, &msg);
            break;
        }        
    }
        
    // 这段代码是不会执行到这的, 可以加一些控制让其走到这. 看人
    close(sd);
    _destroy_ucnode(&head);    
    return 0;
}
复制代码

这里主要围绕的结构就是

// 维护一个客户端链表信息,记录登录信息
typedef struct ucnode {
    struct sockaddr_in addr;
    struct ucnode* next;
} *ucnode_t ;

注册添加登录广播退出等.这里再扯一下. 关于C static开发技巧. C中有一种 *.h 开发模式, 全部采用static 内嵌代码段. 这样

可以省略*.c 文件. 小巧的封装可以使用. 继续扯一点. 开发也写C++,虽然鄙视. C++ 中有个 *.hpp文件. 比较好. 它表达的意思

是这个代码是开源的. 全部采用充血模型. 类中代码都放在类中实现.非常值得提倡. 这也是学boost的时候学到的. 很实在.

好了说代码吧. 也比较随大流. 看看也都明白了. 简单分析一处吧

复制代码
// 这里是有用户登录处理
static void _login_ucnode(ucnode_t head, int sd, struct sockaddr_in* pa, struct umsg* msg) {
    _insert_ucnode(head, pa);
    head = head->next;
    // 从此之后才为以前的链表
    while(head->next){
        head = head->next;
        IF_CHECK(sendto(sd, msg, sizeof(*msg), 0, (struct sockaddr*)&head->addr, sizeof(struct sockaddr_in)));
    }
}
复制代码

因为我采用的头查法. 那就除了刚插入的头的下一个结点都需要发送登录信息. 比较精巧.

好看编译命令

gcc -g -Wall -o udpmulsrv.out udpmulsrv.c
gcc -g -Wall -o udpmulclt.out udpmulclt.c

最后测试截图如下

很好玩,欢迎尝试.到这里基本上udp基础api 应该都了解了.从上面代码也许能看出来. 设计比较重要. 设计决定大思路.

下次有机会 要么分享开源的网络库,要么分享数据库开发.

复制代码
src="http://player.youku.com/embed/XNzkxMTc3OTM2" allowfullscreen="" frameborder="0" height="498" width="510">
src="http://player.youku.com/embed/XNzkxMTc3OTM2" allowfullscreen="" frameborder="0" height="498" width="510">

 

  • 5
    点赞
  • 35
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
<计算机网络实验> 基于TCP的网络聊天室的设计 -实验指导 一、实验目的 1.掌握通信规范的制定及实现。 2.练习较复杂的网络编程,能够把协议设计思想应用到现实应用中。 二、实验内容和要求 1.进一步熟悉VC++6编程环境; 2.利用VC++6进行较复杂的网络编程,完成网络聊天室的设计及编写; 三、实验(设计)仪器设备和材料 1.计算机及操作系统:PC机,Windows; 2.网络环境:可以访问互联网; 四、 TCP/IP程序设计基础 基于TCP/IP的通信基本上都是利用SOCKET套接字进行数据通讯,程序一般分为服务器端和用户端两部分。设计思路(VC6.0下): 第一部分 服务器端 一、创建服务器套接字(create)。 二、服务器套接字进行信息绑定(bind),并开始监听连接(listen)。 三、接受来自用户端的连接请求(accept)。 四、开始数据传输(send/receive)。 五、关闭套接字(closesocket)。 第二部分 客户端 一、创建客户套接字(create)。 二、与远程服务器进行连接(connect),如被接受则创建接收进程。 三、开始数据传输(send/receive)。 四、关闭套接字(closesocket)。 CSocket的编程步骤:(注意我们一定要在创建MFC程序第二步的时候选上Windows Socket选项,其中ServerSocket是服务器端用到的,ClientSocket是客户端用的。) (1)构造CSocket对象,如下例: CSocket ServerSocket; CSocket ClientSocket; (2)CSocket对象的Create函数用来创建Windows Socket,Create()函数会自行调用Bind()函数将此Socket绑定到指定的地址上面。如下例: ServerSocket.Create(823); //服务器端需要指定一个端口号,我们用823。 ClientSocket.Create(); //客户端不用指定端口号。 (3)现在已经创建完基本的Socket对象了,现在我们来启动它,对于服务器端,我们需要这个Socket不停的监听是否有来自于网络上的连接请求,如下例: ServerSocket.Listen(5);//参数5是表示我们的待处理Socket队列中最多能有几个Socket。 (4)对于客户端我们就要实行连接了,具体实现如下例: ClientSocket.Connect(CString SerAddress,Unsinged int SerPort);//其中SerAddress是服务器的IP地址,SerPort是端口号。 (5)服务器是怎么来接受这份连接的呢?它会进一步调用Accept(ReceiveSocket)来接收它,而此时服务器端还须建立一个新的CSocket对象,用它来和客户端进行交流。如下例: CSocket ReceiveSocket; ServerSocket.Accept(ReceiveSocket); (6)如果想在两个程序之间接收或发送信息,MFC也提供了相应的函数。如下例: ServerSocket.Receive(String,Buffer); //String是你要发送的字符串,Buffer是发送字符串的缓冲区大小。ServerSocket.Send(String,Butter);//String是你要接收的字符串,Buffer是接收字符串的缓冲区大小。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值