现在的位置: 首页 > 综合 > 正文

select多路复用 : EasyMouse的通道模式设计(上)

2014年11月05日 ⁄ 综合 ⁄ 共 4752字 ⁄ 字号 评论关闭

此文章来自于EasyMouse项目实践中的设计框架,EasyMouse是一款安卓无线鼠标、键盘软件,可以让你的手机当做鼠标和键盘,远程操作电脑。

欢迎下载使用: EasyMouse官网!

下一篇《select多路复用 : EasyMouse的通道模式设计(下)

概述

        当我们需要同时监听多个IO的时候,就需要用到多路复用技术,它可以同时监听文件描述符,网络套接字。常用的多路复用技术有 select,  poll,  epoll, 他们的功能都差不多,只是内部原理、使用接口和场景不一样而已罢了。这里不做区分介绍,因为EasyMouse的通道设计模式,可以用于任何其中一种技术,这里使用select来做介绍。

 

       通过EasyMouse的通道设计模式,你可以简化程序设计的复杂度,优化程序的逻辑和框架,轻松实现对多并发连接的管理,认证登陆,文件传送,TCP和UDP同时监听等复杂的逻辑。而不会增加程序设计的复杂度,下面逐一介绍设计的各个部分;

 

通道链表

        EasyMouse只采用单个链表来管理所有的连接,而没有针对不同的连接(如监听链接,数据链接等)创建不同的链表,这样就减小了链表维护的复杂度。为此我们对每个连接进行了上一层封装(这里称为通道),通过通道我们保存更多链接的信息,方便管理的同时优化了程序的设计框架。通道链表保存的即是每个连接的封装,而不是原始的套接字信息。如:

typedef struct __channel{

        int                     socket;        // 通道链接套接字

        int                     type;           // 通道的类型,见下面

        sockaddr_in     addr;           // 终端地址

        string                ip;               // 终端的IP地址:点数字格式

        pFunc                pfunc;        // 通道处理回调函数

        pthread_t          pid;            // 通道处理线程ID(与pfunc互斥)

        /****下面内容为通道非核心字段,根据实际情况添加即可 ***/

        pDev                 pdev;          // 终端信息

        time_t                ctime;        // 通道创建的时间

}Channel, *pChannel;

 

说明:

pfunc和pid分别为回调处理函数和线程ID,针对不同的通道设计,我们会采用线程的方式来处理,如:数据通道是采用线程的方式而不是主进程中的回调函数;

pdev: 是自定义结构体,保存接入端的描述信息,如接入的名称,系统,类型等你想要获得信息(非必须);

 

有了上面对单个链接的封装,我们就可以针对通道创建一个全局链表了,用于管理客户端接入的所有链接。如:

vector<Channel*>        g_Channel;        // 所有的通道链表,存放所有的链接

 

通道类型

        我们需要对单个链接进行识别并分类,这样才能进行统一的管理和处理。根据实际需求,我们将设计以下不同的5种通道类型

    enum{

            CHANNEL_TYPE_UNKNOW = 0,        // 未识别的通道;

            CHANNEL_TYPE_TCP,                        // tcp监听通道,只有1个

            CHANNEL_TYPE_UDP,                      // udp监听通道,只有1个

            CHANNEL_TYPE_CMD,                     // 命令通道,可以有多个

            CHANNEL_TYPE_DATA                    // 数据通道,可以有多个

    };

    说明:

    CHANNEL_TYPE_UNKNOW: 

    未识别的通道,所有新建立的链接都初始化为此状态,经过识别之后(如认证)才能转换为命令或数据通道

    CHANNEL_TYPE_TCP

    TCP监听通道,用于接收所有的客户端的链接请求,用于创建新的通道。非特殊情况一个服务器只允许有1个;

    CHANNEL_TYPE_UDP:

   UDP监听通道,有的程序可能需要同时监听TCP和UDP链接,如EasyMouse采用UDP通道来实现广播的接收(局域网搜索服务器);非特殊情况只允许有1个;

    CHANNEL_TYPE_CMD:

    命令通道,用于传递一些服务或命令请求,可以拥有多个(如多个终端接入);

    CHANNEL_TYPE_DATA:

    数据通道,用于传递一些数据文件,如手机发送文件到电脑中;可以有多个(如多文件同时传送);


通道处理

        为了简化主进程的业务处理逻辑,我们为每一个通道设计了专门的回调处理函数,包括监听通道。数据通道我们采用线程的方式来处理。通过回调和线程的方式处理,主进线逻辑可以简单的认为除了接受新的连接什么也不需要干。

    回调函数原型:

    typedef void(*pFunc)(Channel* pChl);// 参数为通道结构体

 

    为此我们需要实现以下通道处理逻辑:

    未识别通道回调函数:voidOnChannelForUnknow(Channel* pChl);

    TCP监听通道回调函数:voidOnChannelForTcp(Channel* pChl);

    UDP监听通道回调函数:voidOnChannelForUdp(Channel* pChl);

    命令通道回调函数:voidOnChannelForCmd(Channel* pChl);

    数据通道回调函数:无(线程处理)

 

说明:

在后面我们会逐一介绍每个回调函数的实现逻辑(伪代码),以达到整体设计的目标;

 

服务器select

这里重点介绍服务器主线程的启动和运行的逻辑;

1,因为这里我们只允许TCP和UDP监听通道各有一个,因此程序启动的时候,我们应该首先创建一个TCP的监听通道。并加入到全局链表g_Channel中(非线程);

2,如果你有UDP的监听通道,那么在启动的时候。需要创建一个UDP通道,并加入到全局链表g_Channel中;

3,进行监听循环;

 

启动服务器的伪代码(根据实际情况拆分为多个函数):

int    StartServer(void)

{

    // 做一些安全校验,实际项目应该更完善一些,如是否已初始化,运行状态

    ASSERT(g_Server.running == false);

     

    // 一,创建TCP监听通道

    pChannel pChl = new Channel();

    pChl->type    = CHANNEL_TYPE_TCP;    // 通道类型

    pChl->pfunc   = OnChannelForTcp;    // 通道处理函数

        ...    /* 这里初始化包括创建套接字,绑定端口等信息 */

    g_Channel.push_back(pChl);    // 添加到全局链表队列g_Channel中

 

    // 二,创建UDP监听通道

    pChl = new Channel();

    pChl->type  = CHANNEL_TYPE_UDP;

    pChl->pfunc = OnChannelForUdp;

        .../* 这里创建udp套接字,绑定监听的端口信息等 */

    g_Channel.push_back(pChl);

 

    // 三,进入消息监听逻辑

    fd_setrfds;

    g_Server.running = true;

    while(g_Server.running){

        FD_ZERO(&rfds);

        // 这里是关键,只监听需要监听的通道

        vector<Channel*>::iteratoritr = g_Channel.begin();

        for( ; itr!=g_Channel.end(); ++itr){

            if ((*itr)->type != CHANNEL_TYPE_DATA){    // 非数据通道,跳过

                FD_SET((*itr)->socket, rfds);    // 加入监听序列

                        }

                } 

        // 监听链接[阻塞],Linux需要计算maxfd, Windows可以直接为0;

        int ret = select(maxfd, &rfds, NULL, NULL, NULL) ;

        if (ret <= 0){

            sleep(1); 

            continue;

                }

        if (!g_Server.running){

            break;     //唤醒时可能服务已被停止

        }

 

        // 查找有数据的链接,并进行处理 # Linux和Windows有所区别

        for(int i=0; i<rfds.fd_count; i++){

            if (FD_ISSET(rfds[i].fd_array[i], &rfds)){

                continue;

            }

            // 查找相应的通道处理函数。 搜索g_Channel

            pChl = find_channel_by_socket(rfds[i].fd_array[i]);

            if (pChl != NULL && pChl->pfunc != NULL){

                pChl->func(pChl);    // 调用通道回调处理函数

            }else{

                // something error;

            }

                }// end for

         }//end while();

}

 

说明:

    上面就是我们整个服务器的主运行逻辑,通过这样的设计,我们不需要再为UDP创建一个监听线程,新接入的链接并不会立即进行识别,需要通过OnChannelForTcp, OnChannelUnknow函数之后才有可能转换为命令或数据通道。这样的设计简化了服务器主逻辑的复杂度;


下面将接着介绍这个几个回调函数的原理,这样整个逻辑会更加清晰;

请看《select多路复用 : EasyMouse的通道模式设计(下)

你可以下载EmServer来体验实现的效果;

抱歉!评论已关闭.