- client --- message1 ---> server
-
client <--- ack1 --- server
-
client --- message2 ---> server
-
client <--- ack2 --- server
-
client --- message3 ---> server
-
client <--- ack3 --- server
异步发送API的消息交互流程图:
- client --- message1 ---> server
-
client --- message2 ---> server
-
client <--- ack1 --- server
-
client <--- ack2 --- server
-
client --- message3 ---> server
-
client <--- ack3 --- server
异步发送API实现的时候,主要是两点:
1> message的发送没有前后依赖关系,后面的message发送的时候不用等待前面message的ack。
2> 收到应答的时候进行处理,调用设置的callback。
这里面第1点很容易实现,发送线程不停的发送就可以了,但是对于第2点如何判断收到应答,这个比较麻烦。因为传统的同步API是阻塞等待对方的应答,对于异步发送API,需要有一定的机制来获得应答。后面的几种异步发送API的实现方法中,分别采用不同的方式来获取应答。
1. 专门的发送线程,专门的接收线程
使用libpcap和libnet编写过程序的同学对这种模式不会陌生,业务线程或者是专门的发送线程使用libnet直接发送message,接收线程使用libpcap接收并进行处理。
这种方法的本质是,通过另外的接收线程同步接收应答。
2. event loop线程
业务线程构造发送数据,添加到event线程的req_list中。并通过Pipe通知event线程,event线程进行异步发送,有应答后,调用相应的callback处理。
event loop线程中实现消息的状态机读取,读取到完整的消息,进行相应的逻辑处理后,执行设置的callback函数。
这种方法的本质是,依靠select, poll, epoll等事件通知API实现,但是在linux下面只能拿到IO就绪事件,而不是IO完成事件,所以需要辅以协议状态机,转化为IO完成事件。
3. 后台发送线程
业务线程构造发送数据,添加到后台发送线程的req_list中,并通过pthread_cond来通知后台发送线程。后台线程直接进行发送,并检查是否有应答,如果有应答就进行处理。
这种方法的本质是,发送线程每发送1个message,就检测一些是否有应答到来。具体检测跟2的一样,也是通过select等实现。
伪代码如下:
- send:
-
req_queue.get_idx(snd_idx);
-
send();
-
select();
-
if(SUCCESS)
-
recv();
-
ack_idx=check_ack();
-
if(OK)
-
process();
-
req_queue.erase(ack_idx);
-
else
-
goto failed;
-
else if(ETIMEOUT)
- snd_idx++;
-
else
- goto failed;
-
-
goto send;
-
failed:
-
disconnect();
但是需要注意的一点是:方法2中select的等待时间是永远,但是这里面等待时间一般设置为0,使其立刻返回。有些实现中也会设置一个较小的值,例如10ms,也就是说每次发送都会select等待10ms,如果10ms内有应答,就处理应答;如果10ms内没有应答,就继续发送。
设想一下,如果每次应答时间为15ms,那么采用这种异步发送方式,启动后前10ms,发送完一个message后,就会阻塞在select上,导致select超时,然后发送下一个message。发送第2个message后的select会在5ms后被唤醒,因为第1个message的ack到达了,这样启动后满负荷跑的话,每次send后,select都会很快返回(小于10ms,甚至是立刻返回),接受到前面message的ack。这样发送的性能就会有所提升。
对于上面3种实现方式,推荐使用第2种。当多个线程调用异步发送API的时候,只需要1个event loop线程就好了。如果第3种,select的timeout为0的话,空select系统调用执行的次数也会比较多。
参考: