同步请求
定义
命名空间:TouchSocket.Sockets
程序集:TouchSocket.dll
一、说明
有很多小伙伴一直有一些需求:
- 客户端发送一个数据,然后等待服务器回应。
- 服务器向客户端发送一个数据,然后等待客户端回应。
那针对这些需求,可以使用WaitingClient
其内部实现了IWaitSender
接口,能够在发送完成后,直接等待返回。
WaitingClient
是一种发送-响应机制,其原理是IReceiverClient
,只要实现该接口的组件均可以使用。
例如:TcpClient
、TcpService
、NamedPipeClient
、NamedPipeService
、SerialPortClient
等。
二、在客户端使用
在客户端工作时,支持很多组件,例如:TcpClient
、NamedPipeClient
、SerialPortClient
,下面仅以TcpClient
为例。
var client = new TcpClient();
await client.ConnectAsync("tcp://127.0.0.1:7789");
//调用CreateWaitingClient获取到IWaitingClient的对象。
var waitClient = client.CreateWaitingClient(new WaitingOptions()
{
FilterFunc = response => //设置用于筛选的fun委托,当返回为true时,才会响应返回
{
return true;
}
});
//然后使用SendThenReturn。
byte[] returnData = await waitClient.SendThenReturnAsync(Encoding.UTF8.GetBytes("RRQM"));
Console.WriteLine($"收到回应消息:{Encoding.UTF8.GetString(returnData)}");
//同时,如果适配器收到数据后,返回的并不是字节,而是IRequestInfo对象时,可以使用SendThenResponse.
ResponsedData responsedData = await waitClient.SendThenResponseAsync(Encoding.UTF8.GetBytes("RRQM"));
IRequestInfo requestInfo = responsedData.RequestInfo;//同步收到的RequestInfo
三、在服务器使用
同理,在客户端工作时,支持很多组件,例如:TcpService
、NamedPipeService
,下面仅以TcpService
为例。
var service = new TcpService();
await service.StartAsync(7789);//启动服务器
//在服务器中,找到指定Id的会话客户端
if (service.TryGetClient("targetId", out var tcpSessionClient))
{
//调用CreateWaitingClient获取到IWaitingClient的对象。
var waitClient = tcpSessionClient.CreateWaitingClient(new WaitingOptions()
{
FilterFunc = response => //设置用于筛选的fun委托,当返回为true时,才会响应返回
{
return true;
}
});
//然后使用SendThenReturn。
byte[] returnData = await waitClient.SendThenReturnAsync(Encoding.UTF8.GetBytes("RRQM"));
Console.WriteLine($"收到回应消息:{Encoding.UTF8.GetString(returnData)}");
//同时,如果适配器收到数据后,返回的并不是字节,而是IRequestInfo对象时,可以使用SendThenResponse.
ResponsedData responsedData = await waitClient.SendThenResponseAsync(Encoding.UTF8.GetBytes("RRQM"));
IRequestInfo responseRequestInfo = responsedData.RequestInfo;//同步收到的RequestInfo
}
WaitingClient
在创建以后,可以长久使用,直到原始的组件被Dispose
释放。所以即使是断线重连后,也会是有效的。所以没必要在使用时每次都创建。
四、配置
4.1 超时配置
在默认情况下,超时时间是5秒。
await waitingClient.SendThenResponseAsync("hello");//默认5秒超时
所以可以直接传参,设置超时时间。
await waitingClient.SendThenResponseAsync("hello", 1000*10);//设置10秒超时
但是有时候,我们希望不设置超时时间,而是由用户自己控制超时时间,也就是能有取消的等待。
var cts = new CancellationTokenSource();
_=Task.Run(async () =>
{
await Task.Delay(5000);
cts.Cancel();//5秒后取消等待,不再等待服务端的消息。这里模拟的是客户端主动取消等待
});
await waitingClient.SendThenResponseAsync("hello", cts.Token);
4.2 筛选配置
筛选函数,用于筛选符合要求的数据。因为WaitingClient
的响应机制是建立在一问一答的基础之上实现的,所以,在发送完数据后,可能收到之前的过期响应数据,那么这时候,会根据用户设置的筛选函数,判断是否响应。
在默认情况下,筛选函数为空,即不筛选。
var waitingClient = client.CreateWaitingClient(new WaitingOptions()
{
FilterFunc=default
});
所以可以设置筛选函数。
var waitingClient = client.CreateWaitingClient(new WaitingOptions()
{
FilterFunc = response => //设置用于筛选的fun委托,当返回为true时,才会响应返回
{
var requestInfo=response.RequestInfo;
var byteBlock=response.ByteBlock;
//这里可以根据服务端返回的信息,判断是否响应
return true;
}
});
可以设置异步筛选函数,可以返回一个Task
,用于异步筛选。
FilterFuncAsync=async response=>await Task.FromResult(true)
筛选函数的参数是Response
,它包含两个属性,分别为RequestInfo
和ByteBlock
,具体使用哪个属性,看适配器类型
五、使用注意事项
5.1 线程安全
WaitingClient
是线程安全的,可以多线程使用。但是实际上内部有SemaphoreSlim
锁,所以,如果使用在多线程中,可能会造成大量锁竞争,从而降低效率。
5.2 关于ReadAsync
WaitingClient
的原理实际上是使用了IReceiverClient
接口,这也就意味着它不能和ReadAsync
同时使用。
5.3 关于筛选函数
在筛选函数FilterFunc(Async)
中,不管返回true
还是false
,数据都不再向下传递。这也就意味着一旦使用WaitingClient
,在其等待返回的时段中,即使收到了无效数据,也不会再触发Receive
事件(或者相关插件)。
如果不在等待时段,则不受影响。
如果想要实现,当筛选函数返回false
时,将数据从插件再次传递,那么就需要在筛选函数中,自行触发插件。
例如:
var waitingClient = this.m_tcpClient.CreateWaitingClient(new WaitingOptions()
{
FilterFuncAsync = async (response) =>
{
var byteBlock = response.ByteBlock;
var requestInfo = response.RequestInfo;
if (true)//如果满足某个条件,则响应WaitingClient
{
return true;
}
else
{
//否则
//数据不符合要求,waitingClient继续等待
//如果需要在插件中继续处理,在此处触发插件
await this.m_tcpClient.PluginManager.RaiseAsync(typeof(ITcpReceivedPlugin), this.m_tcpClient, new ReceivedDataEventArgs(byteBlock, requestInfo));
return false;
}
}
});
5.4 关于SendThenReturn与SendThenReturnAsync
在WaitingClient
中,所有的同步方法,其实都是异步转换来的,所以,功能一致。但是强烈建议如有可能,请务必使用异步发送来提高效率。
在主线程是GUI线程
(例如:winform
、wpf
等),如果在主线程中调用同步代码,也可能会导致死锁。所以请务必使用异步代码。
5.5 关于使用时机
WaitingClient
的机制是发送一个数据,然后等待响应,所以,使用时机,绝对不可以在Received
事件(或者插件)中调用。这将导致死锁。
例如:
var tcpClient = new TcpClient();
var tcpClient.Received =async (client,e) =>
{
var waitingClient = client.CreateWaitingClient(new WaitingOptions());
//这里将导致死锁
var bytes = await waitingClient.SendThenReturnAsync("hello");
};
...
如果确实需要使用,请使用Task.Run
来异步处理。
例如:
this.m_tcpClient.Received =async (client,e) =>
{
//此处不能await,否则也会导致死锁
_ = Task.Run(async () =>
{
var waitingClient = client.CreateWaitingClient(new WaitingOptions());
var bytes = await waitingClient.SendThenReturnAsync("hello");
});
};
...
5.6 其他
- 发送完数据,在等待时,如果收到其他返回数据,则可能得到错误结果。
- 发送采用同步锁,一个事务没结束,另一个请求也发不出去。
- waitClient的使用不可以直接在
Received
相关触发中使用,因为必然会导致死锁,详见:#I9GCGT 。 - 在Net461及以下版本中,SendThenReturn与SendThenReturnAsync不能混合使用。即:要么全同步,要么全异步(这可能是.net bug)。