当前位置:C++技术网 > 精选软件 > 云平台开发架构分析系列:3 libuv包装服务类回调机制分析

云平台开发架构分析系列:3 libuv包装服务类回调机制分析

更新时间:2017-06-19 09:02:24浏览次数:1+次

    在本系列文章的前两篇分析中,我已经将云平台架构的整体结构和技术架构都做了分析。现在进入具体的技术分析阶段。
    TCP服务器通信是建立在libuv库之上的。然而你可以不用了解libuv就可以用。因为我们已经将libuv进行了封装,所以可以不必太关注libuv库。不过,学习libuv当然是非常有帮助的。你可以通过我们包装的服务类先快速上手。
    我们这里就是要对封装了libuv库的服务类进行分析,让你掌握封装服务类的用法。源码在C++技术网都分享了,请看《libuv服务器端包装类源代码分享(修正Linux服务器连续死循环版)》。
    下面开始分析这个类。在文章最后会贴出来我制作的流程示意图。在看分析时,你可以先将网站上分析的代码对着看哦,效果会更好。
    程序开始于main函数,在main函数中也只是构建了服务类对象,也就是我这里分析的这个类的类对象,类名为CTcpServer。然后就会调用这个函数的Start成员函数。然后就开始了TCP服务程序的流程了。
    在开始铺开之前,还要说明调用和回调的区别。调用是进行顺序流程的执行,只是执行权进入了被调用的函数。而回调则不一样。回调的函数作为函数的一个参数,传入函数内部的某个函数指针变量。当前函数会继续往后执行。然后经过一系列的执行流程后,内部的函数指针执行了被传入的函数,才开始调用函数。所以调用函数是马上就会执行的,而回调则不是。当然还有其他方面的区别,就不再此展开。
    Start函数会调用下面几个函数,来完成服务程序的启动:
init()
bind()
listen()
run()
    写过TCP Socket编程的同学都很熟悉吧。这个虽然是包装了libuv的类,但是基本流程肯定还是TCP socket的模式。所以有bind函数来绑定地址,有listen函数来监听端口。我这里只是写示意,没有写参数等。然而并不是监听之后流程就停止了。这里执行到listen时,程序并没有真正的跑动起来。这也是libuv包装后的逻辑。当然,这几个函数也是我们这个类自己定义的,不是API函数。
    init()函数对相关参数做初始化,最后run()启动运行。除非程序退出运行,否则这里就会一直在run里面执行。
    写到这里,Tcp服务器就已经运行起来了。但是我们要怎么做通信处理呢?如何处理新连接的客户端呢?如何接受客户端发来的数据呢?
    这个就从listen()函数开始了。我们在socket编程学过,监听之后,如果有新连接过来,会创建一个新的套接字,然后将这个套接字交给一个新的线程来处理,处理完之后,关闭套接字。libuv已经包装了这些处理过程,我们这个类继续包装了libuv,所以这个细节你用不着关心了。
    在listen()函数中,我们将监听端口的套接字和函数acceptConnection()进行了绑定,这个函数也就成了回调函数了。当内部接收到新的客户端连接的时候,就会调用acceptConnection(),让上层可以去处理。不过,这个函数还是属于我们这个类的成员函数,是在类中实现好的。在这个类中,才是真正处理接受连接的地方。listen函数只是设置而已。在acceptConnection()中,会创建客户端数据对象,用来记录存储客户端的信息。其中一个重要的信息就是客户端列表。每一次连接进来的客户端,都会存入客户端数据对象的客户端列表里。这是对新连接的基本处理。
    这是类中对新连接的处理。而我们使用这个类的程序如何再进一步做自定义的处理呢?所以在这个函数中,会调用之前设置好的回调函数。这个回调函数是通过函数指针存储并调用的。这个函数指针是类的一个成员变量,通过成员函数setnewconnectcb来设置。所以我们会在CTcpServer对象创建后,Start()执行之前就要设置好新连接的回调函数,也就是调用setnewconnectcb()函数,并指定好用户自定义的回调函数。如果不事先设置,在acceptConnection()中就会崩溃掉的。这个用户自定义的新连接回调函数的声明如下:
typedef void(*newconnect)(int clientid);
    也就是一个int参数,返回值为void的函数类型。这个参数就是传入的客户端的ID号。CTcpServer对象中存储的客户端列表是通过客户端ID来寻找对应的套接字的,进而可以与对应的客户端通信。所以这里也仅仅需要一个客户端ID就行了。
    acceptConnection()在自定义新连接回调函数执行之后,就继续进行读取客户端发来数据的相关参数。其中就包含了设置接收数据的缓存回调函数onAllocBuffer和服务器接收了数据之后调用的处理回调函数AfterServerRecv。
    当读取到客户端发来的数据时,就会调用onAllocBuffer()函数。然后在处理好必要的工作之后,就会调用AfterServerRecv()函数做真正的数据的处理。如果读取出错或者没有读取到客户端的数据,执行流程就在这个函数终止。如果读取到了有效数据,那么就在这个函数里调用接受数据的回调函数。所以我们需要先设置好接受数据的回调函数。我们使用CTcpServer的成员函数setrecvcb()函数。我们需要在接受数据之前,接受连接之后,根据客户端ID来指定对应的回调函数。也就是说,我们是可以针对不同的客户端指定不同的回调函数的哦。所以我们一般就在自定义的接受客户端回调函数里面调用setrecvcb()函数设置接受数据回调函数。
    那么到此为止,从TCP服务监听开始到新连接,再到接收客户端发来数据的流程就完成了。这一个过程也就是一系列的回调函数组成的。如果要深入了解这些函数如何被回调的,那么就要阅读libuv源码了。如果只是想先用,那么我们只需要添加好两个自定义回调函数就可以了,第一个是自定义新连接回调函数,第二个就是自定义接受数据回调函数。
    所以基本的使用的代码是这样的:
CTcpServer g_srv;
void recv_cb(int client_id, const char* buf, int buf_size)
{
    //处理接受消息
    g_srv.send(client_id, buf, buf_size);
}
void new_conn_cb(int client_id)
{
    //设置新连接接收数据回调函数
    g_srv.setrecvcb(client_id, recv_cb);
}
int main()
{
    g_srv.setnewconnectcb(new_conn_cb);
    g_srv.Start("0.0.0.0", 5000);
    return 0;
}
    是不是用起来很简单呢!我们指定为0.0.0.0的IP,是代表服务器的任意IP。在公网服务器中,客户端都是直接连接服务器的公网IP。而公网IP在服务器本身是不方便获取的。对于服务器来讲,对于哪个IP来讲都无关紧要,只要关注端口即可。而端口一般要大于1024,以下的端口系统内部可能会用到,如果冲突了可能导致程序监听失败。
    我们接受了数据,自然也就要回复客户端,也就是要想客户端发送数据。我们调用CTcpServer类的成员函数send()就可以发送数据了。函数使用也很简单,就是指定客户端ID,缓存地址,缓存大小。有了客户端ID,send内部会查找客户端列表,然后使用对应的套接字来和客户端通信。send()函数执行后,并不一定就会立刻发送出去,它只是将数据放入了libuv的发送数据的队列里,底层会根据情况自动发送数据的。而send函数会绑定一个回调函数AfterSend(),当底层发送了数据时候,就会调用AfterSend(),只有这个函数被调用了,才说明数据发送操作完成了。至于是否发送成功,就看AfterSend()传入的第二个整型参数,如果参数值为0表示发送成功,如果小于0,表示发送失败。我们可以通过GetUVError函数来获取发送出错信息。
    另外还有两个回调系列,一个是DeleteClient函数绑定了回调函数AfterClientClose(),另一个是close(),会回调AfterClientClose()和AfterServerClose()。前者是将客户端删除后调用,后者是服务器对象关闭后调用。
    最后贴出来CTcpServer类执行的回调函数流程图,从这个图中就可以看到这个类的大致工作流程,也是最上面分析的一个总结:
    云平台开发架构分析系列3:libuv包装服务类回调机制分析