QQ交流群:198941541
按理说每个demo都应该像是一个UT test,仅演示一个或者很少许的功能,但由于个人偷懒的原因,想演示得更多,又不想新增加demo工程的个数,造成demo变得巨复杂,感觉使用ascs库比直接使用asio还麻烦,为此专门出几期教程介绍这些demo,很多新接触ascs的朋友,喜欢从demo上修改得到自己想要的功能,本人并不推荐这么做,而是应该把 ascs 简明开发教程 先读完,再从前面四篇教程里面生成的代码上去添加自己的业务逻辑,这样不容易把自己搞晕。
这个demo演示了同步收发消息(宏ASCS_SYNC_RECV和ASCS_SYNC_SEND),定时器时间对齐(宏ASCS_ALIGNED_TIMER),自定义日志系统(宏ASCS_CUSTOM_LOG),选择非默认unpacker(宏ASCS_DEFAULT_UNPACKER),大消息支持(宏ASCS_HUGE_MSG,需要对方也支持大消息,因为大消息打包的格式不一样),开启心跳包(宏ASCS_HEARTBEAT_INTERVAL,表示发送时间间隔),下面我将按照client.cpp里面的代码顺序来介绍这些功能。
首先是自定义日志系统,需要在包涵ascs任意头文件(base.h除外)之前定义,需要5个静态或者全局函数,签名都是void (const char* fmt, ...)。
接下来引入了ascs/ext/tcp.h头文件,这个头文件在ext目录下,并不属于ascs库,它只是为了方便使用而产生的,比如引入必要的ascs库头文件,定义默认打包解包器(注意打包解包器也不属于ascs库,但ascs库提供了打包解包器接口,由具体的打包解包器来实现),定义无模板参数的各种对象(server, single_client, multi_client, server_socket, client_socket, unix domain socket)等。
short_connection + short_client演示了短连接发送端的实现(默认连接echo_server 的9727端口),注意短连接是由接收方来断开连接的,否则即便是在网络没有问题的情况下,也不能保证数据成功发送到对方并被对方接收。所以short_connection只需要让自己不做重连操作即可,于是它重写了on_connect并在里面调用close_reconnect函数来关闭自动重连,注意short_connection并没有处理接收到的消息,所以ascs库默认会打印到屏幕之上。至于short_client,它的任务就是,每次发送数据之前先新建一条连接然后在新连接上发送数据,注意一点是,连接建立是异步的,在连接建立起来之前,仍然可以向socket发送数据,当连接真正建立起来之后,ascs库会为你发送这些数据。大家可能会问,short_connection为什么从socks4::client_socket继承(socks4下面是支持socks4代理的客户端socket)?如果不给socks4::client_socket设置目标服务器地址,那么它将会退化成普通的client_socket(所以等于从普通client_socket继承),注意这行代码(注释状态):socket_ptr->set_target_addr(9527, "172.27.0.14");,这从侧面说明了和你一样,socks4::client_socket也只是继承自client_socket,你可以参考socks4::client_socket是如何定制化client_socket的。
函数create_sync_recv_thread用于创建一个线程(防止阻塞主线程)并在里面做同步消息读取。
main函数里面演示了如何使用single_service_pump,即一个service对应一个service_pump(相当于asio的io_context,早期叫io_service),同时single_service_pump也是一个service_pump,所以它可以用来构造short_client,代码是:short_client client2(client);
注意main函数里面还演示了一个最简单的客户端,即:single_service_pump<socks5::single_client> client; 这里面也是同样的原因,socket5::single_client会退化成普通的single_client,因为single_client继承自client_socket。这个客户端简单到消息都没有处理(没继承并重写任何虚函数),这个客户端默认连接echo_server的9627端口。这个客户端虽然很简单,但却引入了一个非常重要的行为,前面说了,我们开启了一个线程来在它上面同步读取消息,但它上面还有一个异步读取消息也同时在执行(异步读取是由ascs库自动产生的,如果想关闭自动异步读取,就得定义宏ASCS_PASSIVE_RECV(注释状态)),这样就会带来一个问题,有些消息会被异步读取到,然后由于我们没有处理,ascs会默认打印到屏幕上,有些消息会被同步读取到,然后显示为(多了个sync前辍):printf("sync recv(" ASCS_SF ") : %s\n", msg.size(), msg.data());,这些都是我故意为之的,只是为了演示功能,这种行为并不会带来其它问题,只是消息会被乱序而已。如果我们放开宏ASCS_PASSIVE_RECV的注释呢?也会有问题:第一,如果在调用sync_recv_msg之前,连接就建立好,single_service_pump将会自动结束,因为它没有事情可做(实际上不会,因为我们开启了心跳功能,所以会一直有个定时器存在),解决这个问题可以定义宏ASCS_AVOID_AUTO_STOP_SERVICE;第二,其它本来执行异步读取的客户端(比如short_connection),将只会做一次异步消息读取(一次读取得到多个消息是可以的),因为宏是影响整个工程的,这就需要在消息派发处手动触发异步读取(具体怎么触发我们留到以后讲其它demo时再细说)。那为什么需要引入关闭自动异步读取这个功能呢?原因之一是像本demo一样,只会有同步读取,之二是为了运行时替换解包器,这个我们也留到讲其它真正需要运行时替换解包器的demo时再细说。
最后就是开启同步读线程在上面那个最简单的客户端上读取消息,并接收键盘输入,如果输入 quit 就退出demo运行,其它非命令输入就通过两个客户端发给echo_server的9627和9727端口,同时演示了一下同步安全发送消息,代码是:client.sync_safe_send_msg(...),这里的安全指的是,如果发送缓存满了,会自旋直到成功发送(成功发送指的是发给了ascs的发送缓存,至于什么时候真正的发送由ascs决定,只要ascs当前没有发送且没有等待异步发送结果,就会马上发送)。
关于心跳,还有一个宏ASCS_HEARTBEAT_MAX_ABSENCE,表示丢失多少个心跳包就算异常,有默认值3,所以可以不用定义。心跳功能如果对方开启己方不开,则会被对方断开连接,反之己方ascs将断开自己,在没有收到心跳响应一定次数之后。除了宏,也可以调用start_heartbeat函数来开启心跳功能。另外宏ASCS_ALWAYS_SEND_HEARTBEAT表示当时间间隔到了就强行发一个心跳包而不关心是否有数据正在发送,默认关闭。
关于大消息支持,建议采用flexible_unpacker,对于超过4000的消息,它将动态分配内存,用完就释放,这样如果大消息数量很少的话,可以减少解包器内存占用,如果大消息很多,也至少能减少大消息上的内存拷贝。对于小于4000的消息,处理和默认的解包器一样,一次也可以读取多个消息,所以对于小消息,也不会像non_copy_unpacker那样损失效率(non_copy_unpacker处理小消息效率不高是因为虽然它完全避免了内存拷贝,但由于所有消息都动态分配,一个消息至少要读取两次,一次读取包头,一次读取消息体)。