同步请求
定义
命名空间: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)。