Linux下各类TCP网络服务器的实现源代码

Linux下各类TCP网络服务器的实现源代码

 

 

 





好文不得不转:

http://bbs.chinaunix.net/viewthread.php?tid=786283

作者:zhoulifa, safedead



zhoulifa

发表于: 2006-7-5 23:12      [引用]  [投诉] [快速回复]  

 



--------------------------------------------------------------------------------

 

大家都知道各类网络服务器程序的编写步骤,并且都知道网络服务器就两大类:循环服务和并发服务。这里附上源代码来个小结吧。



首先,循环网络服务器编程实现的步骤是这样的:









这种服务器模型是典型循环服务,如果不加上多进程/线程技术,此种服务吞吐量有限,大家都可以看到,如果前一个连接服务数据没有收发完毕后面的连接没办法处理。所以一般有多进程技术,对一个新连接启用一个新进程去处理,而监听socket继续监听。



/************关于本文档********************************************

*filename: Linux下各类TCP网络服务器的实现源代码

*purpose: 记录Linux下各类tcp服务程序源代码

*wrote by: zhoulifa(zhoulifa@163.com) 周立发(http://zhoulifa.bokee.com)

Linux爱好者 Linux知识传播者 SOHO族 开发者 最擅长C语言

*date time:2006-07-04 22:00:00

*Note: 任何人可以任意复制代码并运用这些文档,当然包括你的商业用途

* 但请遵循GPL

*Hope:希望越来越多的人贡献自己的力量,为科学技术发展出力

*********************************************************************/



一个循环TCP服务源代码(因为用fork进行多进程服务了,所以这种服务现实中也有用)如下:





CODE:[Copy to clipboard]/*----------------------源代码开始--------------------------------------------*/

#include <stdio.h>

#include <stdlib.h>

#include <errno.h>

#include <string.h>

#include <sys/types.h>

#include <netinet/in.h>

#include <sys/socket.h>

#include <sys/wait.h>

/*********************************************************************

*filename: cycletcpserver.c

*purpose: 循环tcp服务端程序

*tidied by: zhoulifa(zhoulifa@163.com) 周立发(http://zhoulifa.bokee.com)

Linux爱好者 Linux知识传播者 SOHO族 开发者 最擅长C语言

*date time:2006-07-04 22:00:00

*Note: 任何人可以任意复制代码并运用这些文档,当然包括你的商业用途

* 但请遵循GPL

*Thanks to: Google.com

*********************************************************************/

int main(int argc, char ** argv)

{

    int sockfd,new_fd; /* 监听socket: sock_fd,数据传输socket: new_fd */

    struct sockaddr_in my_addr; /* 本机地址信息 */

    struct sockaddr_in their_addr; /* 客户地址信息 */

    unsigned int sin_size, myport, lisnum;



    if(argv[1])  myport = atoi(argv[1]);

    else myport = 7838;



    if(argv[2])  lisnum = atoi(argv[2]);

    else lisnum = 2;



    if ((sockfd = socket(PF_INET, SOCK_STREAM, 0)) == -1) {

        perror("socket");

        exit(1);

    }

    my_addr.sin_family=PF_INET;

    my_addr.sin_port=htons(myport);

    my_addr.sin_addr.s_addr = INADDR_ANY;

    bzero(&(my_addr.sin_zero), 0);

    if (bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr)) == -1) {

        perror("bind");

        exit(1);

    }



    if (listen(sockfd, lisnum) == -1) {

        perror("listen");

        exit(1);

    }

    while(1) {

        sin_size = sizeof(struct sockaddr_in);

        if ((new_fd = accept(sockfd, (struct sockaddr *)&their_addr, &sin_size)) == -1) {

            perror("accept");

            continue;

        }

        printf("server: got connection from %s/n",inet_ntoa(their_addr.sin_addr));

        if (!fork()) { /* 子进程代码段 */

            if (send(new_fd, "Hello, world!/n", 14, 0) == -1) {

                perror("send");

                close(new_fd);

                exit(0);

            }

        }

        close(new_fd); /*父进程不再需要该socket*/

        waitpid(-1,NULL,WNOHANG);/*等待子进程结束,清除子进程所占用资源*/

    }

}

/*----------------------源代码结束--------------------------------------------*/

一个测试客户端代码如下:





CODE:[Copy to clipboard]/*----------------------源代码开始--------------------------------------------*/

#include <stdio.h>

#include <stdlib.h>

#include <errno.h>

#include <string.h>

#include <netdb.h>

#include <sys/types.h>

#include <netinet/in.h>

#include <sys/socket.h>

#define MAXDATASIZE 100 /*每次最大数据传输量 */

/*********************************************************************

*filename: cycletcpclient.c

*purpose: 循环tcp客户端程序

*tidied by: zhoulifa(zhoulifa@163.com) 周立发(http://zhoulifa.bokee.com)

Linux爱好者 Linux知识传播者 SOHO族 开发者 最擅长C语言

*date time:2006-07-04 22:20:00

*Note: 任何人可以任意复制代码并运用这些文档,当然包括你的商业用途

* 但请遵循GPL

*Thanks to: Google.com

*Hope:希望越来越多的人贡献自己的力量,为科学技术发展出力

*********************************************************************/



int main(int argc, char *argv[])

{

    int sockfd, numbytes;

    char buf[MAXDATASIZE];

    struct hostent *he;

    struct sockaddr_in their_addr;

    unsigned int myport;



    if(argv[2]) myport = atoi(argv[2]);

    else myport = 7838;



    if (argc != 3) {

        fprintf(stderr,"usage: %s hostname port/n", argv[0]);

        exit(1);

    }

    if((he=gethostbyname(argv[1]))==NULL) {

        herror("gethostbyname");

        exit(1);

    }

    if ((sockfd = socket(PF_INET, SOCK_STREAM, 0)) == -1) {

        perror("socket");

        exit(1);

    }

    their_addr.sin_family=PF_INET;

    their_addr.sin_port=htons(myport);

    their_addr.sin_addr = *((struct in_addr *)he->h_addr);

    bzero(&(their_addr.sin_zero),0);

    if (connect(sockfd, (struct sockaddr *)&their_addr, sizeof(struct sockaddr)) == -1) {

        perror("connect");

        exit(1);

    }

    if ((numbytes=recv(sockfd, buf, MAXDATASIZE, 0)) == -1) {

        perror("recv");

        exit(1);

    }

    buf[numbytes] = 0;

    printf("Received: %s/n",buf);

    close(sockfd);

    return 0;

}

/*----------------------源代码结束--------------------------------------------*/

用gcc cycletcpserver.c -o tcpserver和gcc cycletcpclient.c -o tcpclient分别编译上述代码后运行情况如下:

服务端运行显示:





QUOTE:

administrator@ubuzlf:/data/example/c$ ./tcpserver

server: got connection from 127.0.0.1

server: got connection from 127.0.0.1

server: got connection from 127.0.0.1

客户端运行显示:





QUOTE:

administrator@ubuzlf:/data/example/c$ ./tcpclient 127.0.0.1 7838

Received: Hello, world!



administrator@ubuzlf:/data/example/c$ ./tcpclient 127.0.0.1 7838

Received: Hello, world!



administrator@ubuzlf:/data/example/c$ ./tcpclient 127.0.0.1 7838

Received: Hello, world!

不得不说的一个概念性问题:阻塞与非阻塞

在阻塞服务中,当服务器运行到accept语句而没有客户连接服务请求到来,那么会发生什么情况? 这时服务器就会停止在accept语句上等待连接服务请求的到来;同样,当程序运行到接收数据语句recv时,如果没有数据可以读取,则程序同样会停止在接收语句上。这种情况称为阻塞(blocking)。

但如果你希望服务器仅仅注意检查是否有客户在等待连接,有就接受连接;否则就继续做其他事情,则可以通过将 socket设置为非阻塞方式来实现:非阻塞socket在没有客户在等待时就使accept调用立即返回 。

通过设置socket为非阻塞方式,可以实现“轮询”若干socket。当企图从一个没有数据等待处理的非阻塞socket读入数据时,函数将立即返回,并且返回值置为-1,并且errno置为EWOULDBLOCK。但是这种“轮询”会使CPU处于忙等待方式,从而降低性能。考虑到这种情况,假设你希望服务器监听连接服务请求的同时从已经建立的连接读取数据,你也许会想到用一个accept语句和多个recv()语句,但是由于accept及recv都是会阻塞的,所以这个想法显然不会成功。

调用非阻塞的socket会大大地浪费系统资源。而调用select()会有效地解决这个问题,它允许你把进程本身挂起来,而同时使系统内核监听所要求的一组文件描述符的任何活动,只要确认在任何被监控的文件描述符上出现活动,select()调用将返回指示该文件描述符已准备好的信息,从而实现了为进程选出随机的变化,而不必由进程本身对输入进行测试而浪费CPU开销。



其次,并发服务器,在上述cycletcpserver.c中,由于使用了fork技术也可以称之为并发服务器,但这种服务器并不是真正意义上的IO多路复用的并发服务器,并且由于没有处理阻塞问题,实际应用有各种各样的问题。



一个典型IO多路复用的单进程并发服务器流程如下:

/*IO多路复用并发服务流程图*/









下面是一个演示IO多路复用的源程序,是一个端口转发程序,但它的用处相当大,实际应用中的各类代理软件或端口映射软件都是基于这样的代码的,比如Windows下的WinGate、WinProxy等都是在此基础上实现的。源代码如下:





CODE:[Copy to clipboard]/*----------------------源代码开始--------------------------------------------*/

#include <stdlib.h>

#include <stdio.h>

#include <unistd.h>

#include <sys/time.h>

#include <sys/types.h>

#include <string.h>

#include <signal.h>

#include <sys/socket.h>

#include <netinet/in.h>

#include <arpa/inet.h>

#include <errno.h>



static int forward_port;



#undef max

#define max(x,y) ((x) > (y) ? (x) : (y))



/*************************关于本文档************************************

*filename: tcpforwardport.c

*purpose: 演示了select的用法,这是一个极好的代理软件核心,专门作端口映射用

*tidied by: zhoulifa(zhoulifa@163.com) 周立发(http://zhoulifa.bokee.com)

Linux爱好者 Linux知识传播者 SOHO族 开发者 最擅长C语言

*date time:2006-07-05 19:00:00

*Note: 任何人可以任意复制代码并运用这些文档,当然包括你的商业用途

* 但请遵循GPL

*Thanks to: Paul Sheer 感谢Paul Sheer在select_tut的man手册里提供了这份源代码

*Hope:希望越来越多的人贡献自己的力量,为科学技术发展出力

*********************************************************************/



static int listen_socket (int listen_port) {

    struct sockaddr_in a;

    int s;

    int yes;

    if ((s = socket (AF_INET, SOCK_STREAM, 0)) < 0) {

        perror ("socket");

        return -1;

    }

    yes = 1;

    if (setsockopt(s, SOL_SOCKET, SO_REUSEADDR, (char *) &yes, sizeof (yes)) <

0) {

        perror ("setsockopt");

        close (s);

        return -1;

    }

    memset (&a, 0, sizeof (a));

    a.sin_port = htons (listen_port);

    a.sin_family = AF_INET;

    if (bind(s, (struct sockaddr *) &a, sizeof (a)) < 0) {

        perror ("bind");

        close (s);

        return -1;

    }

    printf ("accepting connections on port %d/n", (int) listen_port);

    listen (s, 10);

    return s;

}



static int connect_socket (int connect_port, char *address) {

    struct sockaddr_in a;

    int s;

    if ((s = socket (AF_INET, SOCK_STREAM, 0)) < 0) {

        perror ("socket");

        close (s);

        return -1;

    }



    memset (&a, 0, sizeof (a));

    a.sin_port = htons (connect_port);

    a.sin_family = AF_INET;



    if (!inet_aton(address, (struct in_addr *) &a.sin_addr.s_addr)) {

        perror ("bad IP address format");

        close (s);

        return -1;

    }



    if (connect(s, (struct sockaddr *) &a, sizeof (a)) < 0) {

        perror ("connect()");

        shutdown (s, SHUT_RDWR);

        close (s);

        return -1;

    }

    return s;

}



#define SHUT_FD1 { /

    if (fd1 >= 0) {   /

        shutdown (fd1, SHUT_RDWR);  /

        close (fd1);  /

        fd1 = -1;     /

    }   /

}



#define SHUT_FD2 { /

    if (fd2 >= 0) {   /

        shutdown (fd2, SHUT_RDWR);  /

        close (fd2);  /

        fd2 = -1;     /

    }   /

}



#define BUF_SIZE 1024



int main (int argc, char **argv) {

    int h;

    int fd1 = -1, fd2 = -1;

    char buf1[BUF_SIZE], buf2[BUF_SIZE];

    int buf1_avail, buf1_written;

    int buf2_avail, buf2_written;



    if (argc != 4) {

        fprintf (stderr, "Usage/n/tfwd   /n");

        exit (1);

    }



    signal (SIGPIPE, SIG_IGN);



    forward_port = atoi (argv[2]);



    /*建立监听socket*/

    h = listen_socket (atoi (argv[1]));

    if (h < 0) exit (1);



    for (;;) {

        int r, nfds = 0;

        fd_set rd, wr, er;

        FD_ZERO (&rd);

        FD_ZERO (&wr);

        FD_ZERO (&er);

        FD_SET (h, &rd);



        /*把监听socket和可读socket三个一起放入select的可读句柄列表里*/

        nfds = max (nfds, h);

        if (fd1 > 0 && buf1_avail < BUF_SIZE) {

            FD_SET (fd1, &rd);

            nfds = max (nfds, fd1);

        }

        if (fd2 > 0 && buf2_avail < BUF_SIZE) {

            FD_SET (fd2, &rd);

            nfds = max (nfds, fd2);

        }



        /*把可写socket两个一起放入select的可写句柄列表里*/

        if (fd1 > 0 && buf2_avail - buf2_written > 0) {

            FD_SET (fd1, &wr);

            nfds = max (nfds, fd1);

        }

        if (fd2 > 0 && buf1_avail - buf1_written > 0) {

            FD_SET (fd2, &wr);

            nfds = max (nfds, fd2);

        }



        /*把有异常数据的socket两个一起放入select的异常句柄列表里*/

        if (fd1 > 0) {

            FD_SET (fd1, &er);

            nfds = max (nfds, fd1);

        }

        if (fd2 > 0) {

            FD_SET (fd2, &er);

            nfds = max (nfds, fd2);

        }



        /*开始select*/

        r = select (nfds + 1, &rd, &wr, &er, NULL);



        if (r == -1 && errno == EINTR) continue;

        if (r < 0) {

            perror ("select()");

            exit (1);

        }



        /*处理新连接*/

        if (FD_ISSET (h, &rd)) {

            unsigned int l;

            struct sockaddr_in client_address;

            memset (&client_address, 0, l = sizeof (client_address));

            r = accept (h, (struct sockaddr *)&client_address, &l);

            if (r < 0) {

                perror ("accept()");

            } else {

                /*关闭原有连接,把新连接作为fd1,同时连接新的目标fd2*/

                SHUT_FD1;

                SHUT_FD2;

                buf1_avail = buf1_written = 0;

                buf2_avail = buf2_written = 0;

                fd1 = r;

                fd2 = connect_socket (forward_port, argv[3]);

                if (fd2 < 0) {

                    SHUT_FD1;

                } else

                    printf ("connect from %s/n", inet_ntoa(client_address.sin_addr));

            }

        }



        /* NB: read oob data before normal reads */

        if (fd1 > 0)

        if (FD_ISSET (fd1, &er)) {

            char c;

            errno = 0;

            r = recv (fd1, &c, 1, MSG_OOB);

            if (r < 1) {

                SHUT_FD1;

            } else

                send (fd2, &c, 1, MSG_OOB);

        }



        if (fd2 > 0)

        if (FD_ISSET (fd2, &er)) {

            char c;

            errno = 0;

            r = recv (fd2, &c, 1, MSG_OOB);

            if (r < 1) {

                SHUT_FD1;

            } else

                send (fd1, &c, 1, MSG_OOB);

        }



        /* NB: read data from fd1 */

        if (fd1 > 0)

        if (FD_ISSET (fd1, &rd)) {

            r = read (fd1, buf1 + buf1_avail, BUF_SIZE - buf1_avail);

            if (r < 1) {

                SHUT_FD1;

            } else

                buf1_avail += r;

        }



        /* NB: read data from fd2 */

        if (fd2 > 0)

        if (FD_ISSET (fd2, &rd)) {

            r = read (fd2, buf2 + buf2_avail, BUF_SIZE - buf2_avail);

            if (r < 1) {

                SHUT_FD2;

            } else

                buf2_avail += r;

        }



        /* NB: write data to fd1 */

        if (fd1 > 0)

        if (FD_ISSET (fd1, &wr)) {

            r = write (fd1, buf2 + buf2_written, buf2_avail - buf2_written);

            if (r < 1) {

                SHUT_FD1;

            } else

                buf2_written += r;

        }



        /* NB: write data to fd1 */

        if (fd2 > 0)

        if (FD_ISSET (fd2, &wr)) {

            r = write (fd2, buf1 + buf1_written, buf1_avail - buf1_written);

            if (r < 1) {

                SHUT_FD2;

            } else

                buf1_written += r;

        }



        /* check if write data has caught read data */

        if (buf1_written == buf1_avail) buf1_written = buf1_avail = 0;

        if (buf2_written == buf2_avail) buf2_written = buf2_avail = 0;



        /* one side has closed the connection, keep writing to the other side until empty */

        if (fd1 < 0 && buf1_avail - buf1_written == 0) {

            SHUT_FD2;

        }

        if (fd2 < 0 && buf2_avail - buf2_written == 0) {

            SHUT_FD1;

        }

    }

    return 0;

}

/*----------------------源代码结束--------------------------------------------*/ 

zhoulifa发表于: 2006-7-5 23:13    主题:   [引用]  [投诉] [快速回复]  

 



--------------------------------------------------------------------------------

 

用gcc tcpforwardport.c -o MyProxy编译此程序后运行效果如下:





QUOTE:

./MyProxy 8000 80 172.16.100.218

accepting connections on port 8000

connect from 127.0.0.1

当有用户访问本机的8000端口时,MyProxy程序将把此请求转发到172.16.100.218主机的80端口,即实现了一个http代理。



关于select函数:

其函数原型为:

int select(int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

此函数的功能是由内核检测在timeout时间内,是否有readfds,writefds,exceptfds三个句柄集(file descriptors)里的某个句柄(file descriptor)的状态符合寻求,即readfds句柄集里有句柄可读或writefds句柄集里有可写或exceptfds句柄集里有例外发生,任何一个有变化函数就立即返回,返回值为timeout发生状态变化的句柄个数。

n是所有readfds,writefds,exceptfds三个句柄集(file descriptors)里编号最大值加1。比如:要检测两个socket句柄fd1和fd2在timeout时间内是否分别可读和可写就可以这样:

先把两个句柄集(file descriptors)清零:

        FD_ZERO (&readfds);

        FD_ZERO (&writefds);

然后把fd1加入读检测集:

        FD_SET (fd1, &readfds);

然后把fd2加入写检测集:

        FD_SET (fd2, &writefds);

再给timeout设置值,timeout是这样的一个结构:

              struct timeval {

                  long    tv_sec;         /* seconds */

                  long    tv_usec;        /* microseconds */

              };

你可以这样赋值:

        timeout.tv_sec=1;

        timeout.tv_uec=0;

表示检测在1秒钟内是否有句柄状态发生变化。

如果有句柄发生变化,就可以用FD_ISSET检测各个句柄,比如:

                FD_ISSET (fd1, &readfds);//检测是否fd1变成可读的了

                FD_ISSET (fd2, &writefds);//检测是否fd2变成可写的了

示意程序代码如下:





CODE:[Copy to clipboard]/*----------------------示意代码开始--------------------------------------------*/

    fd1 = socket();//创建一个socket

    fd2 = socket();//创建一个socket

    while(1)  {

        FD_ZERO (&readfds);

        FD_ZERO (&writefds);

        FD_SET (fd1, &readfds);

        FD_SET (fd2, &writefds);

        timeout.tv_sec=1;

        timeout.tv_uec=0;

        ret = select(fd1>fd2?(fd1+1):(fd2+1), &readfds, &writefds, NULL, &timeout);

        if(ret < 0) {printf("系统错误,select出错,错误代码:%d, 错误信息:%s", errno, strerror(errno));}

        else if(ret == 0) {printf("select超时返回,没有任何句柄状态发生变化!");}

        //有句柄状态发生了变化

        if(FD_ISSET(fd1, &readfds)) {

            fd1有数据可读;

            fd1里的数据被读出来;

        }

        if(FD_ISSET(fd2, &writefds)) {

            fd2可写;

            fd2里发送数据给对方;

        }

    }

/*----------------------示意代码结束--------------------------------------------*/

经常用到的几个自定义函数:

1、开启监听的函数





CODE:[Copy to clipboard]/*----------------------源代码代码开始--------------------------------------------*/

int

OpenSCPServer(int port, int total, int sendbuflen, int recvbuflen, int blockORnot, int reuseORnot)    {

/*************************关于本函数************************************

*function_name: OpenSCPServer

*参数说明:port整数型监听端口号,total整数型监听个数,sendbuflen整数型发送缓冲区大小

*          recvbuflen整数型接收缓冲区大小,blockORnot整数型是否阻塞,reuseORnot整数型是否端口重用

*purpose: 用来建立一个tcp服务端socket

*tidied by: zhoulifa(zhoulifa@163.com) 周立发(http://zhoulifa.bokee.com)

Linux爱好者 Linux知识传播者 SOHO族 开发者 最擅长C语言

*date time:2006-07-05 20:00:00

*Note: 任何人可以任意复制代码并运用这些文档,当然包括你的商业用途

* 但请遵循GPL

*Thanks to: Paul Sheer 感谢Paul Sheer在select_tut的man手册里提供了这份源代码

*Hope:希望越来越多的人贡献自己的力量,为科学技术发展出力

*Note:要使用此函数需要自定义一个全局变量char errorMessage[1024];并包含GetCurrentTime.h头文件

*********************************************************************/

    int    sockfd = 0, ret = 0, opt = 0, flags=1;

    struct sockaddr_in    laddr;



    ret = sockfd = socket(PF_INET, SOCK_STREAM, 0);

    if(ret < 0)    {

        sprintf(errorMessage, "OpenTCPServer socket() error! return:%d, errno=%d, errortext:'%s' %s", ret, errno, strerror(errno), GetCurrentTime(0, 0));

        return -1;

    }



    ret = setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &reuseORnot, sizeof(int));

    if(ret < 0)    {

        sprintf(errorMessage, "OpenTCPServer setsockopt() reuse error! return:%d, errno=%d, errortext:'%s' %s", ret, errno, strerror(errno), GetCurrentTime(0, 0));

        return -2;

    }



    ret = setsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &recvbuflen, sizeof(int));

    if ( ret < 0)    {

        sprintf(errorMessage, "OpenTCPServer setsockopt() recvbuf error! return:%d, errno=%d, errortext:'%s' %s", ret, errno, strerror(errno), GetCurrentTime(0, 0));

        return -3;

    }



    ret = setsockopt(sockfd, SOL_SOCKET, SO_SNDBUF, &sendbuflen, sizeof(int));

    if (ret < 0)    {

        sprintf(errorMessage, "OpenTCPServer setsockopt() sendbuf error! return:%d, errno=%d, errortext:'%s' %s", ret, errno, strerror(errno), GetCurrentTime(0, 0));

        return -4;

    }



    ioctl(sockfd,FIONBIO,&blockORnot);/*block or not*/



    laddr.sin_family = PF_INET;

    laddr.sin_port = htons(port);

    laddr.sin_addr.s_addr = INADDR_ANY;

    bzero(&(laddr.sin_zero), 8);



    ret = bind(sockfd, (struct sockaddr *)&laddr, sizeof(struct sockaddr));

    if(ret < 0)    {

        sprintf(errorMessage, "OpenTCPServer bind() error! return:%d, errno=%d, errortext:'%s' %s", ret, errno, strerror(errno), GetCurrentTime(0, 0));

        close(sockfd);

        return -5;

    }

    ret = listen(sockfd, total);

    if(ret < 0)    {

        sprintf(errorMessage, "OpenTCPServer listen() error! return:%d, errno=%d, errortext:'%s' %s", ret, errno, strerror(errno), GetCurrentTime(0, 0));

        close(sockfd);

        return -6;

    }

    sprintf(errorMessage, "OpenTCPServer opened on port.%d(%d) OK, socket(%d), buf(%d:%d)! %s", port, total, sockfd, sendbuflen, recvbuflen, GetCurrentTime(0, 0));

    return sockfd;

}

/*----------------------源代码代码结束--------------------------------------------*/

2、连接服务器的函数





CODE:[Copy to clipboard]/*----------------------源代码代码开始--------------------------------------------*/

int

ConnectSCPServer(char * serverip, int serverport, int blockORnot)    {

/*************************关于本函数************************************

*function_name: ConnectSCPServer

*参数说明:serverip服务器IP地址或主机名,serverport服务器端口,blockORnot整数型是否阻塞

*purpose: 用来建立一个tcp客户端socket

*tidied by: zhoulifa(zhoulifa@163.com) 周立发(http://zhoulifa.bokee.com)

Linux爱好者 Linux知识传播者 SOHO族 开发者 最擅长C语言

*date time:2006-07-05 20:40:00

*Note: 任何人可以任意复制代码并运用这些文档,当然包括你的商业用途

* 但请遵循GPL

*Thanks to: Paul Sheer 感谢Paul Sheer在select_tut的man手册里提供了这份源代码

*Hope:希望越来越多的人贡献自己的力量,为科学技术发展出力

*Note:要使用此函数需要自定义一个全局变量char errorMessage[1024];并包含自己编写的GetCurrentTime.h头文件

*********************************************************************/

    int    serversock = 0, ret = 0;

    unsigned long    addr;

    struct sockaddr_in    sin;

    struct hostent *he;



    if((he=gethostbyname(serverip))== 0) {

        sprintf(errorMessage, "ConnectSCPServer IP address '%s' error! return:-1 %s", serverip, GetCurrentTime(0, 0));

        return -1;

    }



    serversock = socket(PF_INET, SOCK_STREAM, 0);

    if(serversock == -1)    {

        sprintf(errorMessage, "ConnectSCPServer socket() error! return:-2, errno=%d, errortext:'%s' %s", errno, strerror(errno), GetCurrentTime(0, 0));

        return -2;

    }



    ioctl(serversock, FIONBIO, &blockORnot);  //block or not



    memset((char*)&sin, 0, sizeof(struct sockaddr_in));

    sin.sin_family = PF_INET;

    sin.sin_port = htons(serverport);

    sin.sin_addr = *((struct in_addr *)he->h_addr);



    ret = connect(serversock, (struct sockaddr *)&sin, sizeof(sin));



    if(ret == -1)    {

        sprintf(errorMessage, "ConnectSCPServer connect() error! return:-3, errno=%d, errortext:'%s' %s", errno, strerror(errno), GetCurrentTime(0, 0));

        close(serversock);

        return -3;

    }



    return serversock;

}

/*----------------------源代码代码结束--------------------------------------------*/

3、发送数据函数Send





CODE:[Copy to clipboard]/*----------------------源代码代码开始--------------------------------------------*/

int

Send(int sock, char * buf, size_t size, int flag, int timeout)    {

/*************************关于本函数************************************

*function_name: Send

*参数说明:sock整数型socket,buf待发送的内容,size要发送的大小,flag发送选项,timeout超时时间值

*purpose: 用来通过一个socket在指定时间内发送数据

*tidied by: zhoulifa(zhoulifa@163.com) 周立发(http://zhoulifa.bokee.com)

Linux爱好者 Linux知识传播者 SOHO族 开发者 最擅长C语言

*date time:2006-07-05 20:58:00

*Note: 任何人可以任意复制代码并运用这些文档,当然包括你的商业用途

* 但请遵循GPL

*Thanks to: Paul Sheer 感谢Paul Sheer在select_tut的man手册里提供了这份源代码

*Hope:希望越来越多的人贡献自己的力量,为科学技术发展出力

*Note:要使用此函数需要自定义一个全局变量char errorMessage[1024];并包含自己编写的GetCurrentTime.h头文件

*********************************************************************/

    int i = 0, ret = 0, intretry = 0;



    struct timeval tival;

    fd_set writefds;

    int maxfds = 0;



    tival.tv_sec = timeout;

    tival.tv_usec = 0;



    FD_ZERO(&writefds);



    if(sock > 0) {

        FD_SET(sock, &writefds);

        maxfds=((sock > maxfds)?sock:maxfds);

    }

    else    {

        sprintf(errorMessage, "Send socket:%d error! return:-2 %s", sock, GetCurrentTime(0, 0));

        return -2;

    }



    ret = select(maxfds + 1, NULL, &writefds, NULL, &tival);

    if(ret <= 0) {

        if(ret < 0)    sprintf(errorMessage, "Send socket:%d select() error! return:%d, errno=%d, errortext:'%s' %s", sock, ret, errno, strerror(errno), GetCurrentTime(0, 0));

        else sprintf(errorMessage, "Send socket:%d select timeout(%d)! %s", sock, timeout, GetCurrentTime(0, 0));

        close(sock);

        return -3;

    }

    if(!(FD_ISSET(sock, &writefds)))    {

        sprintf(errorMessage, "Send socket:%d not in writefds! %s", sock, GetCurrentTime(0, 0));

        close(sock);

        return -4;

    }



    while(i < size)    {

        ret = send(sock, buf + i, size - i, flag);

        if(ret <= 0)    {

            sprintf(errorMessage, "Send socket:%d send() error! return:%d, errno=%d, errortext:'%s' %s", sock, ret, errno, strerror(errno), GetCurrentTime(0, 0));



            if (EINTR == errno)

              if(intretry < 10)  {intretry++;continue;}

              else sprintf(errorMessage, "Send socket:%d send() error!EINTR 10 times! %s", sock, GetCurrentTime(0, 0));



            close(sock);

            return -1;

        }

        else i += ret;

    }

    sprintf(errorMessage, "Send socket:%d send() OK! %d/%d bytes sent! %s", sock, i, size, GetCurrentTime(0, 0));

    return i;

}

/*----------------------源代码代码结束--------------------------------------------*/

4、接收数据函数Recv





CODE:[Copy to clipboard]/*----------------------源代码代码开始--------------------------------------------*/

int

Recv(int sock, char * buf, size_t size, int flag, int timeout)    {

/*************************关于本函数************************************

*function_name: Recv

*参数说明:sock整数型socket,buf接收数据的缓冲区,size要接收数据的大小,flag接收选项,timeout超时时间值

*purpose: 用来从一个socket在指定时间内读取数据

*tidied by: zhoulifa(zhoulifa@163.com) 周立发(http://zhoulifa.bokee.com)

Linux爱好者 Linux知识传播者 SOHO族 开发者 最擅长C语言

*date time:2006-07-05 21:10:00

*Note: 任何人可以任意复制代码并运用这些文档,当然包括你的商业用途

* 但请遵循GPL

*Thanks to: Paul Sheer 感谢Paul Sheer在select_tut的man手册里提供了这份源代码

*Hope:希望越来越多的人贡献自己的力量,为科学技术发展出力

*Note:要使用此函数需要自定义一个全局变量char errorMessage[1024];并包含自己编写的GetCurrentTime.h头文件

*********************************************************************/

    int i = 0, ret = 0, intretry = 0;



    struct timeval tival;

    fd_set readfds;

    int maxfds = 0;



    tival.tv_sec = timeout;

    tival.tv_usec = 0;



    FD_ZERO(&readfds);



    if(sock > 0) {

        FD_SET(sock, &readfds);

        maxfds=((sock > maxfds)?sock:maxfds);

    }

    else    {

        sprintf(errorMessage, "Recv socket:%d error! return:-2 %s", sock, GetCurrentTime(0, 0));

        return -2;

    }



    ret = select(maxfds + 1, &readfds, NULL, NULL, &tival);

    if(ret <= 0) {

        if(ret < 0)    sprintf(errorMessage, "Recv socket:%d select() error! return:%d, errno=%d, errortext:'%s' %s", sock, ret, errno, strerror(errno), GetCurrentTime(0, 0));

        else sprintf(errorMessage, "Recv socket:%d select timeout(%d)! %s", sock, timeout, GetCurrentTime(0, 0));

        close(sock);

        return -3;

    }

    if(!(FD_ISSET(sock, &readfds)))    {

        sprintf(errorMessage, "Recv socket:%d not in readfds! %s", sock, GetCurrentTime(0, 0));

        close(sock);

        return -4;

    }

    while(i < size)    {

        ret = recv(sock, buf + i, size - i, flag);

        if(ret <= 0){

            sprintf(errorMessage, "Recv socket:%d recv() error! return:%d, errno=%d, errortext:'%s' %s", sock, ret, errno, strerror(errno), GetCurrentTime(0, 0));

            if(errno == EINTR)   

                if(intretry < 10)  {intretry++;continue;}

                else sprintf(errorMessage, "Recv socket:%d recv() error! EINTR 10 times! %s", sock, GetCurrentTime(0, 0));

            close(sock);

            return -1;

        }

        else i += ret;

    }

    sprintf(errorMessage, "Recv socket:%d recv() OK! %d/%d bytes received! %s", sock, i, size, GetCurrentTime(0, 0));

    return i;

}

最后需要说明的是:我这里讲到的源程序并不能实际地作为一个产品程序来用,实际情况下可能会有其它许多工作要做,比如可能要建立共享队列来存放 socket里读到的消息,也可能把发送消息先进行排队然后再调用Send函数。还有,如果不是全数字,在发送前一定要htonl转换为网络字节序,同理接收到后一定要先ntohl由网络字节序转换为主机字节序,否则对方发送过来的0x00000001在你这里可能是0x00010000,因为高低位顺序不同。 



 



safedead发表于: 2006-7-6 08:12    主题:   [引用]  [投诉] [快速回复]  

 



--------------------------------------------------------------------------------

 

进入2.6内核时代, select应该进垃圾堆了

高并发服务器用select效率极低, 特别是使用非阻塞IO时更是慢得一蹋糊涂

改用epoll会大大改善

我一个程序监听从8000到18000共计1万个端口, 启动1万个LISTEN

用epoll来阻塞, 系统非常轻松, 完全没有惊群现象



epoll用法比select简单



初始化:创建epoll描述字; 向epoll描述字添加需要响应的套接字, 初始化过程只要一次即可



使用: 等待epoll事件发生, 提取事件的套接字进行相应的读写操作





static int        s_epfd;//epoll描述字



{//初始化epoll

        struct epoll_event        ev;



        //设置epoll

        s_epfd = epoll_create(65535);



        {//这个过程可以循环以便加入多个LISTEN套接字进入epoll事件集合

                //服务器监听创建

                rc = listen();//listen参数这里省略



                //加入epoll事件集合

                ev.events = EPOLLIN;

                ev.data.fd = rc;

                if (epoll_ctl(s_epfd, EPOLL_CTL_ADD, rc, &ev) < 0) {

                        fprintf(stderr, "epoll set insertion error: fd=%d", rc);

                        return(-1);

                }

        }

}



{//epoll事件处理

        int        i, nfds, sock_new;

        struct epoll_event        events[16384];

        for( ; ; ) {

                //等待epoll事件

                nfds = epoll_wait(s_epfd, events, 16384, -1);

                //处理epoll事件

                for(i = 0; i < nfds; i++) {

                        //events[i].data.fd是epoll事件中弹出的套接字

                        //接收连接

                        sock_new = accept(events[i].data.fd);//accept其它参数这里省略了

                        if(0 > sock_new) {

                                fprintf(stderr, "接收客户端连接失败/n");

                                continue;

                        }

                }

        }

} 





zhoulifa发表于: 2006-7-6 22:36    主题:Linux 2.6内核中提高网络I/O性能的新方法epoll   [引用]  [投诉] [快速回复]  

 



--------------------------------------------------------------------------------

 

正如我昨天在“Linux下各类TCP网络服务器的实现源代码”(http://zhoulifa.bokee.com/5345930.html)一文中提到的那样,I/O多路复用技术在比较多的TCP网络服务器中有使用,即比较多的用到select函数。



感谢chinaunix.net上朋友safedead(http://bbs.chinaunix.net/viewpro.php?uid=407631)提醒,我今天仔细研究了一下,证实了在2.6内核中的新的I/O技术epoll。







1、为什么select是落后的?



首先,在Linux内核中,select所用到的FD_SET是有限的,即内核中有个参数__FD_SETSIZE定义了每个FD_SET的句柄个数,在我用的2.6.15-25-386内核中,该值是1024,搜索内核源代码得到:



include/linux/posix_types.h:#define __FD_SETSIZE        1024



也就是说,如果想要同时检测1025个句柄的可读状态是不可能用select实现的。或者同时检测1025个句柄的可写状态也是不可能的。



其次,内核中实现select是用轮询方法,即每次检测都会遍历所有FD_SET中的句柄,显然,select函数执行时间与FD_SET中的句柄个数有一个比例关系,即select要检测的句柄数越多就会越费时。



当然,在前文中我并没有提及poll方法,事实上用select的朋友一定也试过poll,我个人觉得select和poll大同小异,个人偏好于用select而已。







/************关于本文档********************************************



*filename: Linux 2.6内核中提高网络I/O性能的新方法epoll



*purpose: 补充“Linux下各类TCP网络服务器的实现源代码”一文的不足之处



*wrote by: zhoulifa(zhoulifa@163.com) 周立发(http://zhoulifa.bokee.com)



Linux爱好者 Linux知识传播者 SOHO族 开发者 最擅长C语言



*date time:2006-07-06 22:30:00



*Note: 任何人可以任意复制代码并运用这些文档,当然包括你的商业用途



* 但请遵循GPL



*Hope:希望越来越多的人贡献自己的力量,为科学技术发展出力



*********************************************************************/







2、2.6内核中提高I/O性能的新方法epoll







epoll是什么?按照man手册的说法:是为处理大批量句柄而作了改进的poll。要使用epoll只需要这三个系统调用:epoll_create(2), epoll_ctl(2), epoll_wait(2)。



当然,这不是2.6内核才有的,它是在2.5.44内核中被引进的(epoll(4) is a new API introduced in Linux kernel 2.5.44)







以下文章转自滕昱的Web Log http://mechgouki.spaces.msn.com/blog/PersonalSpace.aspx





QUOTE:

/*********************************引用开始******************************/



Linux2.6内核epoll介绍---我的blog 2005/3/30







[作者]:滕昱,2005/3/30,0.1版本







[版权声明]:此文档遵循GNU自由文档许可证(GNU Free Documentation License).任何人可以自由复制,分发,修改,不过如果方便,请注明出处和作者







(1)导言:







首先,我强烈建议大家阅读Richard Stevens著作《TCP/IP Illustracted Volume 1,2,3》和《UNIX Network Programming Volume 1,2》。虽然他离开我们大家已经5年多了,但是他的书依然是进入网络编程的最直接的道路。其中的3卷的《TCP/IP Illustracted》卷1是必读-如果你不了解tcp协议各个选项的详细定义,你就失去了优化程序重要的一个手段。卷2,3可以选读一下。比如卷2 讲解的是4.4BSD内核TCP/IP协议栈实现----这个版本的协议栈几乎影响了现在所有的主流os,但是因为年代久远,内容不一定那么vogue. 在这里我多推荐一本《The Linux Networking Architecture--Design and Implementation of Network Protocols in the Linux Kernel》,以2.4内核讲解Linux TCP/IP实现,相当不错.作为一个现实世界中的实现,很多时候你必须作很多权衡,这时候参考一个久经考验的系统更有实际意义。举个例子,linux内核中sk_buff结构为了追求速度和安全,牺牲了部分内存,所以在发送TCP包的时候,无论应用层数据多大,sk_buff最小也有272的字节.







其实对于socket应用层程序来说,《UNIX Network Programming Volume 1》意义更大一点.2003年的时候,这本书出了最新的第3版本,不过主要还是修订第2版本。其中第6章《I/O Multiplexing》是最重要的。Stevens给出了网络IO的基本模型。在这里最重要的莫过于select模型和Asynchronous I/O模型.从理论上说,AIO似乎是最高效的,你的IO操作可以立即返回,然后等待os告诉你IO操作完成。但是一直以来,如何实现就没有一个完美的方案。最著名的windows完成端口实现的AIO,实际上也是内部用线程池实现的罢了,最后的结果是IO有个线程池,你应用也需要一个线程池...... 很多文档其实已经指出了这带来的线程context-switch带来的代价。







在linux 平台上,关于网络AIO一直是改动最多的地方,2.4的年代就有很多AIO内核patch,最著名的应该算是SGI那个。但是一直到2.6内核发布,网络模块的AIO一直没有进入稳定内核版本(大部分都是使用用户线程模拟方法,在使用了NPTL的linux上面其实和windows的完成端口基本上差不多了)。2.6内核所支持的AIO特指磁盘的AIO---支持io_submit(),io_getevents()以及对Direct IO的支持(就是绕过VFS系统buffer直接写硬盘,对于流服务器在内存平稳性上有相当帮助)。







所以,剩下的select模型基本上就是我们在linux上面的唯一选择,其实,如果加上no-block socket的配置,可以完成一个"伪"AIO的实现,只不过推动力在于你而不是os而已。不过传统的select/poll函数有着一些无法忍受的缺点,所以改进一直是2.4-2.5开发版本内核的任务,包括/dev/poll,realtime signal等等。最终,Davide Libenzi开发的epoll进入2.6内核成为正式的解决方案







(2)epoll的优点







<1>支持一个进程打开大数目的socket描述符(FD)







select 最不能忍受的是一个进程所打开的FD是有一定限制的,由FD_SETSIZE设置,默认值是2048。对于那些需要支持的上万连接数目的IM服务器来说显然太少了。这时候你一是可以选择修改这个宏然后重新编译内核,不过资料也同时指出这样会带来网络效率的下降,二是可以选择多进程的解决方案(传统的 Apache方案),不过虽然linux上面创建进程的代价比较小,但仍旧是不可忽视的,加上进程间数据同步远比不上线程间同步的高效,所以也不是一种完美的方案。不过 epoll则没有这个限制,它所支持的FD上限是最大可以打开文件的数目,这个数字一般远大于2048,举个例子,在1GB内存的机器上大约是10万左右,具体数目可以cat /proc/sys/fs/file-max察看,一般来说这个数目和系统内存关系很大。







<2>IO效率不随FD数目增加而线性下降







传统的select/poll另一个致命弱点就是当你拥有一个很大的socket集合,不过由于网络延时,任一时间只有部分的socket是"活跃"的,但是select/poll每次调用都会线性扫描全部的集合,导致效率呈现线性下降。但是epoll不存在这个问题,它只会对"活跃"的socket进行操作---这是因为在内核实现中epoll是根据每个fd上面的callback函数实现的。那么,只有"活跃"的socket才会主动的去调用 callback函数,其他idle状态socket则不会,在这点上,epoll实现了一个"伪"AIO,因为这时候推动力在os内核。在一些 benchmark中,如果所有的socket基本上都是活跃的---比如一个高速LAN环境,epoll并不比select/poll有什么效率,相反,如果过多使用epoll_ctl,效率相比还有稍微的下降。但是一旦使用idle connections模拟WAN环境,epoll的效率就远在select/poll之上了。







<3>使用mmap加速内核与用户空间的消息传递。







这点实际上涉及到epoll的具体实现了。无论是select,poll还是epoll都需要内核把FD消息通知给用户空间,如何避免不必要的内存拷贝就很重要,在这点上,epoll是通过内核于用户空间mmap同一块内存实现的。而如果你想我一样从2.5内核就关注epoll的话,一定不会忘记手工 mmap这一步的。







<4>内核微调







这一点其实不算epoll的优点了,而是整个linux平台的优点。也许你可以怀疑linux平台,但是你无法回避linux平台赋予你微调内核的能力。比如,内核TCP/IP协议栈使用内存池管理sk_buff结构,那么可以在运行时期动态调整这个内存pool(skb_head_pool)的大小-- - 通过echo XXXX>/proc/sys/net/core/hot_list_length完成。再比如listen函数的第2个参数(TCP完成3次握手的数据包队列长度),也可以根据你平台内存大小动态调整。更甚至在一个数据包面数目巨大但同时每个数据包本身大小却很小的特殊系统上尝试最新的NAPI网卡驱动架构。







(3)epoll的使用







令人高兴的是,2.6内核的epoll比其2.5开发版本的/dev/epoll简洁了许多,所以,大部分情况下,强大的东西往往是简单的。唯一有点麻烦是epoll有2种工作方式:LT和ET。







LT(level triggered)是缺省的工作方式,并且同时支持block和no-block socket.在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的,所以,这种模式编程出错误可能性要小一点。传统的select/poll都是这种模型的代表.







ET (edge-triggered)是高速工作方式,只支持no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了(比如,你在发送,接收或者接收请求,或者发送接收的数据少于一定量时导致了一个EWOULDBLOCK 错误)。但是请注意,如果一直不对这个fd作IO操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once),不过在TCP协议中,ET模式的加速效用仍需要更多的benchmark确认。







epoll只有epoll_create,epoll_ctl,epoll_wait 3个系统调用,具体用法请参考http://www.xmailserver.org/linux-patches/nio-improve.html ,



在http://www.kegel.com/rn/也有一个完整的例子,大家一看就知道如何使用了







(4)Leader/follower模式线程pool实现,以及和epoll的配合







.....未完成,主要是要避免过多的epoll_ctl调用,以及尝试使用EPOLLONESHOT加速......







(5)benchmark







.......未完成



/*********************************引用结束******************************/

3、epoll的使用方法



这是epoll的man手册提供的一个例子,这段代码假设一个非阻塞的socket监听listener被建立并且一个epoll句柄kdpfd已经提前用epoll_create建立了:





CODE:[Copy to clipboard]       struct epoll_event ev, *events;







       for(;;) {



           nfds = epoll_wait(kdpfd, events, maxevents, -1);/*wait for an I/O event. All notes here added by zhoulifa(http://zhoulifa.bokee.com) on 2006-7-6 22:10:00*/







           for(n = 0; n < nfds; ++n) {



               if(events[n].data.fd == listener) {/*if listen socket has an I/O, accept the new connect*/



                   client = accept(listener, (struct sockaddr *) &local,



                                   &addrlen);



                   if(client < 0){



                       perror("accept");



                       continue;



                   }



                   setnonblocking(client);



                   ev.events = EPOLLIN | EPOLLET;/*EPOLLIN-available for read*/



                   ev.data.fd = client;



                   if (epoll_ctl(kdpfd, EPOLL_CTL_ADD, client, &ev) < 0) {/*add the new socket into the epoll file descriptors*/



                       fprintf(stderr, "epoll set insertion error: fd=%d/n",



                               client);



                       return -1;



                   }



               }



               else



                   do_use_fd(events[n].data.fd);/*read from a socket which has data come*/



           }



       }

4、epoll使用方法示意代码



以下代码由chinaunix.net上BBS用户safedead(http://bbs.chinaunix.net/viewpro.php?uid=407631)提供:





CODE:[Copy to clipboard]static int        s_epfd;//epoll描述字







{//初始化epoll



        struct epoll_event        ev;







        //设置epoll



        s_epfd = epoll_create(65535);







        {//这个过程可以循环以便加入多个LISTEN套接字进入epoll事件集合



                //服务器监听创建



                rc = listen();//listen参数这里省略







                //加入epoll事件集合



                ev.events = EPOLLIN;



                ev.data.fd = rc;



                if (epoll_ctl(s_epfd, EPOLL_CTL_ADD, rc, &ev) < 0) {



                        fprintf(stderr, "epoll set insertion error: fd=%d", rc);



                        return(-1);



                }



        }



}







{//epoll事件处理



        int        i, nfds, sock_new;



        struct epoll_event        events[16384];



        for( ; ; ) {



                //等待epoll事件



                nfds = epoll_wait(s_epfd, events, 16384, -1);



                //处理epoll事件



                for(i = 0; i < nfds; i++) {



                        //events[i].data.fd是epoll事件中弹出的套接字



                        //接收连接



                        sock_new = accept(events[i].data.fd);//accept其它参数这里省略了



                        if(0 > sock_new) {



                                fprintf(stderr, "接收客户端连接失败/n");



                                continue;



                        }



                }



        }



}

对照safedead和前面的一份代码,我想大家一定是明白了的。







5、参考文档



Improving (network) I/O performance ...



http://www.xmailserver.org/linux-patches/nio-improve.htm 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值