跳到主要内容
版本:2.1

创建TcpClient

定义

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

一、说明

TcpClient是Tcp系客户端基类,他直接参与tcp的连接、发送、接收、处理、断开等,他的业务与服务器的TcpSessionClient是一一对应的。

二、特点

  • 简单易用。
  • IOCP多线程。
  • 内存池支持
  • 高性能
  • 适配器预处理,一键式解决分包粘包、对象解析(如HTTP,Json)等。
  • 超简单的同步发送、异步发送、接收等操作。
  • 基于委托、插件驱动,让每一步都能执行AOP。

三、产品应用场景

  • 所有Tcp基础使用场景:可跨平台、跨语言使用。
  • 自定义协议解析场景:可解析任意数据格式的TCP数据报文。

四、可配置项

可配置项

SetMaxPackageSize

数据包最大值(单位:byte),默认1024×1024×10。该值会在适当时间,直接作用DataHandlingAdapter.MaxPackageSize。

SetRemoteIPHost

链接到的远程IPHost,支持域名。支持类型:

  1. 使用IPv4,传入形如:127.0.0.1:7789的字符串即可。
  2. 使用IPv6,传入形如:[*::*]:7789的字符串即可。
  3. 使用域名,必须包含协议类型,形如:http://baidu.com或者https://baidu.com:80
  4. 使用IPv6域名,必须包含协议类型,形如:http://[*::*]:80

SetClientSslOption

客户端Ssl配置,为Null时则不启用。 注意,当RemoteIPHost使用https、wss的域名时,该配置会使用系统默认配置生效。

SetKeepAliveValue

为Socket设置的属性。 注意:该配置仅在window平台生效。

SetBindIPHost

绑定端口。

  • 在UdpSessionBase中表示本地监听地址
  • 在TcpClient中表示固定客户端端口号。

UseNoDelay

设置Socket的NoDelay属性,默认false。

SetSendTimeout

设置发送超时时间,默认0ms,即禁用该配置。

五、支持插件

插件方法功能
ITcpConnectingPlugin此时Socket实际上已经完成连接,但是并没有启动接收,然后触发。
ITcpConnectedPlugin同意连接,且成功启动接收后触发
ITcpClosingPlugin当客户端主动调用Close时触发
ITcpClosedPlugin当客户端断开连接后触发
ITcpReceivingPlugin在收到原始数据时触发,所有的数据均在ByteBlock里面。
ITcpReceivedPlugin在收到适配器数据时触发,根据适配器类型,数据可能在ByteBlock或者IRequestInfo里面。
ITcpSendingPlugin当即将发送数据时,调用该方法在适配器之后,接下来即会发送数据。

六、创建TcpClient

6.1 简单创建

简单的处理逻辑可通过ConnectedClosedReceived等委托直接实现。

代码如下:

var tcpClient = new TcpClient();
tcpClient.Connecting = (client, e) => { return EasyTask.CompletedTask; };//即将连接到服务器,此时已经创建socket,但是还未建立tcp
tcpClient.Connected = (client, e) => { return EasyTask.CompletedTask; };//成功连接到服务器
tcpClient.Closing = (client, e) => { return EasyTask.CompletedTask; };//即将从服务器断开连接。此处仅主动断开才有效。
tcpClient.Closed = (client, e) => { return EasyTask.CompletedTask; };//从服务器断开连接,当连接不成功时不会触发。
tcpClient.Received = (client, e) =>
{
//从服务器收到信息。但是一般byteBlock和requestInfo会根据适配器呈现不同的值。
var mes = e.ByteBlock.Span.ToString(Encoding.UTF8);
tcpClient.Logger.Info($"客户端接收到信息:{mes}");
return EasyTask.CompletedTask;
};

//载入配置
await tcpClient.SetupAsync(new TouchSocketConfig()
.SetRemoteIPHost("127.0.0.1:7789")
.ConfigureContainer(a =>
{
a.AddConsoleLogger();//添加一个日志注入
}));

await tcpClient.ConnectAsync();//调用连接,当连接不成功时,会抛出异常。

Result result = await tcpClient.TryConnectAsync();//或者可以调用TryConnectAsync
if (result.IsSuccess())
{

}

tcpClient.Logger.Info("客户端成功连接");

6.2 继承实现

一般继承实现的话,可以从TcpClient继承。如果有特殊需求,也可以从TcpClientBase继承。

class MyTcpClient : TcpClient
{
protected override async Task OnTcpReceived(ReceivedDataEventArgs e)
{
//此处逻辑单线程处理。

//此处处理数据,功能相当于Received委托。
string mes =e.ByteBlock.Span.ToString(Encoding.UTF8);
Console.WriteLine($"已接收到信息:{mes}");
await base.OnTcpReceived(e);
}
}
var tcpClient = new MyTcpClient();
//载入配置
await tcpClient.SetupAsync(new TouchSocketConfig()
.SetRemoteIPHost("127.0.0.1:7789")
.ConfigureContainer(a =>
{
a.AddConsoleLogger();//添加一个日志注入
}));

await tcpClient.ConnectAsync();//调用连接,当连接不成功时,会抛出异常。

七、接收数据

TcpClient中,接收数据的方式有很多种。多种方式可以组合使用。

7.1 Received委托处理

当使用TcpClient创建客户端时,内部已经定义好了一个外置委托Received,可以通过该委托直接接收数据。

var tcpClient = new TcpClient();
tcpClient.Received = (client, e) =>
{
//从服务器收到信息
string mes = e.ByteBlock.Span.ToString(Encoding.UTF8);
Console.WriteLine($"接收到信息:{mes}");
return EasyTask.CompletedTask;
};

await tcpClient.ConnectAsync("127.0.0.1:7789");

7.2 插件处理

按照TouchSocket的设计理念,使用插件处理数据,是一项非常简单,且高度解耦的方式。步骤如下:

(1)声明插件

插件可以先继承PluginBase,然后再实现需要的功能插件接口,可以按需选择泛型或者非泛型实现。

如果已经有继承类,直接实现IPlugin接口即可。

public class MyPlugin : PluginBase, ITcpReceivedPlugin
{
public async Task OnTcpReceived(ITcpSession client, ReceivedDataEventArgs e)
{
//这里处理数据接收
//根据适配器类型,e.ByteBlock与e.RequestInfo会呈现不同的值,具体看文档=》适配器部分。
ByteBlock byteBlock = e.ByteBlock;
IRequestInfo requestInfo = e.RequestInfo;

//e.Handled = true;//表示该数据已经被本插件处理,无需再投递到其他插件。

await e.InvokeNext();
}
}

(2)创建使用插件处理的客户端

var client = new TcpClient();
await client.SetupAsync(new TouchSocketConfig()
.SetRemoteIPHost("127.0.0.1:7789")
.ConfigureContainer(a =>
{
a.AddConsoleLogger();
})
.ConfigurePlugins(a =>
{
a.Add<MyPlugin>();
}));

await client.ConnectAsync();
注意

当接收数据时,ByteBlock与RequestInfo的值会根据适配器类型不同而不同。

7.3 异步阻塞接收

异步阻塞接收,即使用await的方式接收数据。其特点是能在代码上下文中,直接获取到收到的数据。例如:

var client = new TcpClient();
await client.ConnectAsync("127.0.0.1:7789");//连接

//receiver可以复用,不需要每次接收都新建
using (var receiver = client.CreateReceiver())
{
while (true)
{
//receiverResult必须释放
using (var receiverResult = await receiver.ReadAsync(CancellationToken.None))
{
if (receiverResult.IsCompleted)
{
Console.WriteLine($"客户端已断开,信息:{receiverResult.Message}");
//断开连接了
return;
}

//如果是适配器信息,则可以直接获取receiverResult.RequestInfo;
var requestInfo = receiverResult.RequestInfo;
var byteBlock = receiverResult.ByteBlock;

//从服务器收到信息。
var mes = byteBlock.Span.ToString(Encoding.UTF8);
client.Logger.Info($"客户端接收到信息:{mes}");
}
}
}

在异步阻塞接收时,当接收的数据不满足解析条件时,还可以缓存起来,下次一起处理。

例如:下列将演示接收字符串,当没有发现“\r\n”时,将缓存数据,直到发现重要字符。

其中,CacheModeMaxCacheSize是启用缓存的重要属性。byteBlock.Seek则是将已读取的数据游标移动至指定位置。

var client = new TcpClient();
await client.ConnectAsync("127.0.0.1:7789");//连接

//receiver可以复用,不需要每次接收都新建
using (var receiver = client.CreateReceiver())
{
receiver.CacheMode = true;
receiver.MaxCacheSize = 1024 * 1024;

var rn = Encoding.UTF8.GetBytes("\r\n");
while (true)
{
//receiverResult每次接收完必须释放
using (var receiverResult = await receiver.ReadAsync(CancellationToken.None))
{
//收到的数据,此处的数据会根据适配器投递不同的数据。
var byteBlock = receiverResult.ByteBlock;
var requestInfo = receiverResult.RequestInfo;

if (receiverResult.IsCompleted)
{
//断开连接了
Console.WriteLine($"断开信息:{receiverResult.Message}");
return;
}

//在CacheMode下,byteBlock将不可能为null

var index = 0;
while (true)
{
var r = byteBlock.Span.Slice(index).IndexOf(rn);
if (r < 0)
{
break;
}

var str = byteBlock.Span.Slice(index, r).ToString(Encoding.UTF8);
Console.WriteLine(str);

index += rn.Length;
index += r;
}

byteBlock.Seek(index);
}
}
}
提示

异步阻塞接收,在等待接收数据时,不会阻塞线程资源,所以即使大量使用,也不会影响性能。

八、发送数据

TcpClient已经内置了发送方法,直接调用就可以发送,如果发送失败,则会立即抛出异常。

//原生
public Task SendAsync(string id, ReadOnlyMemory<byte> memory);
public Task SendAsync(string id, IRequestInfo requestInfo);
提示

框架不仅内置了字节的发送,也扩展了字符串等常见数据的发送。而且还包括了TrySend等不会抛出异常的发送方法。

注意

所有的发送,框架内部实际上只实现了异步发送,但是为了兼容性,仍然保留了同步发送的扩展。但是强烈建议如有可能,请务必使用异步发送来提高效率

九、断线重连

断线重连,即tcp客户端在断开服务器后,主动发起的再次连接请求。

9.1 触发型重连

触发型重连,依靠的是Tcp断开事件(Closed)发生时,再次尝试连接。所以,这就要求客户端在初始时,至少完成一次连接。

var tcpClient = new TcpClient();

//载入配置
await tcpClient.SetupAsync(new TouchSocketConfig()
.SetRemoteIPHost("127.0.0.1:7789")
.ConfigurePlugins(a =>
{
a.UseTcpReconnection();
}));

await tcpClient.ConnectAsync();//调用连接
注意

触发重连,必须满足以下几个要求:

  1. 必须完成第一次连接。
  2. 必须是被动断开,如果是客户端主动调用Close、Disposed等方法主动断开的话,一般不会生效。
  3. 必须有显式的断开信息,也就是说,直接拔网线的话,不会立即生效,会等tcp保活到期后再生效。

9.2 使用Polling轮询连接插件

使用Polling断线重连,是一种无人值守的连接方式,它不要求首次连接。

.ConfigurePlugins(a => 
{
a.UseTcpReconnection()
.UsePolling(TimeSpan.FromSeconds(1));
})
注意

Polling重连,必须满足以下几个要求:

  1. 必须有显式的断开信息,也就是说,直接拔网线的话,也不会立即生效,会等tcp保活到期后再生效。
提示

UseReconnection插件,可以通过设置SetActionForCheck,自己规定检查活性的方法。默认情况下,只会检验Online属性,所以无法检验出断网等情况。如果自己控制,则可以发送心跳包,以保证在线状态。

本文示例Demo