跳到主要内容
版本:3.0

同步请求

定义

命名空间:TouchSocket.Sockets
程序集:TouchSocket.dll

一、说明

有很多小伙伴一直有一些需求:

  1. 客户端发送一个数据,然后等待服务器回应。
  2. 服务器向客户端发送一个数据,然后等待客户端回应。

那针对这些需求,可以使用WaitingClient其内部实现了IWaitSender接口,能够在发送完成后,直接等待返回。

提示

WaitingClient是一种发送-响应机制,其原理是IReceiverClient,只要实现该接口的组件均可以使用。

例如:TcpClientTcpServiceNamedPipeClientNamedPipeServiceSerialPortClient等。

二、在客户端使用

在客户端工作时,支持很多组件,例如:TcpClientNamedPipeClientSerialPortClient,下面仅以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

三、在服务器使用

同理,在客户端工作时,支持很多组件,例如:TcpServiceNamedPipeService,下面仅以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,它包含两个属性,分别为RequestInfoByteBlock,具体使用哪个属性,看适配器类型

五、使用注意事项

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线程(例如:winformwpf等),如果在主线程中调用同步代码,也可能会导致死锁。所以请务必使用异步代码。

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 其他

注意事项
  1. 发送完数据,在等待时,如果收到其他返回数据,则可能得到错误结果。
  2. 发送采用同步锁,一个事务没结束,另一个请求也发不出去。
  3. waitClient的使用不可以直接在Received相关触发中使用,因为必然会导致死锁,详见:#I9GCGT
  4. Net461及以下版本中,SendThenReturn与SendThenReturnAsync不能混合使用。即:要么全同步,要么全异步(这可能是.net bug)。

六、本文示例Demo