C# 优雅的为Tcp客户端设计心跳数据包


一、说明

为什么要设置心跳?

心跳机制是定时发送一个自定义的 结构体 (心跳包),让对方知道自己还活着,以确保连接的有效性的机制。 网络中的接收和发送数据都是使用操作系统中的 SOCKET 进行实现。 但是如果此 套接字 已经断开,那发送数据和接收数据的时候就一定会有问题。 可是如何判断这个套接字是否还可以使用呢? 这个就需要在系统中创建心跳机制。 其实TCP中已经为我们实现了一个叫做心跳的机制。

但是该机制受限于操作系统,而且很容易误报。所以很少被大家使用。

大家使用最多的,就是自己设计数据包,然后预留心跳格式,当对方收到心跳包时,直接返回响应包即可。

那么,按这个思路,让我们使用TouchSocket优雅的实现吧。


二、程序集源码

2.1 源码位置
2.2 说明文档

文档首页

三、安装

Nuget安装TouchSocket即可,具体步骤详看链接博客。

VS、Unity安装和使用Nuget包

四、数据格式

4.1 设计数据格式

使用心跳之前,必须要明确数据格式,绝对不能混淆业务数据。一般在适配Plc等现成模块时,他们是有固定的数据格式,这时候你可以参阅数据处理适配器,快速的解析数据。

但是在本文中,并没有规定的格式,所以我们需要先设计一种简单高效的数据格式。

如下:

数据长度数据类型载荷数据
2字节(Ushort)1字节(Byte)n字节(<65535)
4.2 解析数据格式

下列代码主要实现对上述数据格式的解析

internal class MyFixedHeaderDataHandlingAdapter : CustomFixedHeaderDataHandlingAdapter<MyRequestInfo>
{
    public override int HeaderLength => 3;

    public override bool CanSendRequestInfo => false;

    protected override MyRequestInfo GetInstance()
    {
        return new MyRequestInfo();
    }

    protected override void PreviewSend(IRequestInfo requestInfo)
    {
        throw new NotImplementedException();
    }
}

internal class MyRequestInfo : IFixedHeaderRequestInfo
{
    public DataType DataType { get; set; }
    public byte[] Data { get; set; }

    public int BodyLength { get; private set; }

    public bool OnParsingBody(byte[] body)
    {
        if (body.Length == this.BodyLength)
        {
            this.Data = body;
            return true;
        }
        return false;
    }

    public bool OnParsingHeader(byte[] header)
    {
        if (header.Length == 3)
        {
            this.BodyLength = TouchSocketBitConverter.Default.ToUInt16(header, 0) - 1;
            this.DataType = (DataType)header[2];
            return true;
        }
        return false;
    }

    public void Package(ByteBlock byteBlock)
    {
        byteBlock.Write((ushort)((this.Data == null ? 0 : this.Data.Length) + 1));
        byteBlock.Write((byte)this.DataType);
        if (this.Data != null)
        {
            byteBlock.Write(this.Data);
        }
    }

    public byte[] PackageAsBytes()
    {
        using var byteBlock = new ByteBlock();
        this.Package(byteBlock);
        return byteBlock.ToArray();
    }

    public override string ToString()
    {
        return $"数据类型={this.DataType},数据={(this.Data == null ? "null" : Encoding.UTF8.GetString(this.Data))}";
    }
}

internal enum DataType : byte
{
    Ping,
    Pong,
    Data
}

五、创建扩展类

下列代码可选,主要实现对Client增加Ping的扩展方法。方便调用。

/// <summary>
/// 一个心跳计数器扩展。
/// </summary>
internal static class DependencyExtensions
{
    public static readonly DependencyProperty<Timer> HeartbeatTimerProperty =
        DependencyProperty<Timer>.Register("HeartbeatTimer", null);

    public static bool Ping<TClient>(this TClient client) where TClient : ITcpClientBase
    {
        try
        {
            client.Send(new MyRequestInfo() { DataType = DataType.Ping }.PackageAsBytes());
            return true;
        }
        catch (Exception ex)
        {
            client.Logger.Exception(ex);
        }

        return false;
    }

    public static bool Pong<TClient>(this TClient client) where TClient : ITcpClientBase
    {
        try
        {
            client.Send(new MyRequestInfo() { DataType = DataType.Pong }.PackageAsBytes());
            return true;
        }
        catch (Exception ex)
        {
            client.Logger.Exception(ex);
        }

        return false;
    }
}

六、创建心跳插件类

下列代码主要实现心跳插件的功能。默认每五秒自动触发一次。且接收方收到Ping后,直接会回复Pong。

internal class HeartbeatAndReceivePlugin : PluginBase, ITcpConnectedPlugin<ITcpClientBase>, ITcpDisconnectedPlugin<ITcpClientBase>, ITcpReceivedPlugin<ITcpClientBase>
{
    private readonly int m_timeTick;
    private readonly ILog logger;

    [DependencyInject]
    public HeartbeatAndReceivePlugin( ILog logger,int timeTick= 1000 * 5)
    {
        this.m_timeTick = timeTick;
        this.logger = logger;
    }


    public async Task OnTcpConnected(ITcpClientBase client, ConnectedEventArgs e)
    {
        if (client is ISocketClient)
        {
            return;//此处可判断,如果为服务器,则不用使用心跳。
        }

        if (client.GetValue(DependencyExtensions.HeartbeatTimerProperty) is Timer timer)
        {
            timer.Dispose();
        }

        client.SetValue(DependencyExtensions.HeartbeatTimerProperty, new Timer((o) =>
        {
            client.Ping();
        }, null, 0, this.m_timeTick));
        await e.InvokeNext();
    }

    public async Task OnTcpDisconnected(ITcpClientBase client, DisconnectEventArgs e)
    {
        if (client.GetValue(DependencyExtensions.HeartbeatTimerProperty) is Timer timer)
        {
            timer.Dispose();
            client.SetValue(DependencyExtensions.HeartbeatTimerProperty, null);
        }

        await e.InvokeNext();
    }

    public async Task OnTcpReceived(ITcpClientBase client, ReceivedDataEventArgs e)
    {
        if (e.RequestInfo is MyRequestInfo myRequest)
        {
            this.logger.Info(myRequest.ToString());
            if (myRequest.DataType == DataType.Ping)
            {
                client.Pong();
            }
        }
        await e.InvokeNext();
    }
}

七、测试、启动

/// <summary>
/// 示例心跳。
/// 博客地址<see href="https://blog.csdn.net/qq_40374647/article/details/125598921"/>
/// </summary>
/// <param name="args"></param>
private static void Main(string[] args)
{
    var consoleAction = new ConsoleAction();

    //服务器
    var service = new TcpService();
    service.Setup(new TouchSocketConfig()//载入配置
            .SetListenIPHosts(new IPHost[] { new IPHost("127.0.0.1:7789"), new IPHost(7790) })//同时监听两个地址
            .SetTcpDataHandlingAdapter(() => new MyFixedHeaderDataHandlingAdapter())
            .ConfigureContainer(a =>
            {
                a.AddConsoleLogger();
            })
            .ConfigurePlugins(a =>
            {
                a.Add<HeartbeatAndReceivePlugin>();
            }))
            .Start();//启动
    service.Logger.Info("服务器成功启动");

    //客户端
    var tcpClient = new TcpClient();
    tcpClient.Setup(new TouchSocketConfig()
        .SetRemoteIPHost(new IPHost("127.0.0.1:7789"))
        .SetTcpDataHandlingAdapter(() => new MyFixedHeaderDataHandlingAdapter())
        .ConfigureContainer(a =>
        {
            a.AddConsoleLogger();
        })
        .ConfigurePlugins(a =>
        {
            a.Add<HeartbeatAndReceivePlugin>();
        }));
    tcpClient.Connect();
    tcpClient.Logger.Info("客户端成功连接");

    consoleAction.OnException += ConsoleAction_OnException;
    consoleAction.Add("1", "发送心跳", () =>
      {
          tcpClient.Ping();
      });
    consoleAction.Add("2", "发送数据", () =>
      {
          tcpClient.Send(new MyRequestInfo()
          {
              DataType = DataType.Data,
              Data = Encoding.UTF8.GetBytes(Console.ReadLine())
          }
          .PackageAsBytes());
      });
    consoleAction.ShowAll();
    while (true)
    {
        consoleAction.Run(Console.ReadLine());
    }
}

private static void ConsoleAction_OnException(Exception obj)
{
    Console.WriteLine(obj);
}

八、效果

在这里插入图片描述

本文示例demo

  • 7
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论
### 回答1: c是计算机编程语言中的一种。它是一种高级编程语言,由丹尼斯·里奇在1972年开发出来。C语言设计目标是提供高效、灵活和可移植的编程方式。C语言的特点包括: 1. 简洁而直观的语法:C语言的语法结构相对简单,容易理解和使用。它使用了大量的关键字和语法结构来实现各种功能。 2. 功能强大的标准库:C语言提供了丰富的标准库,包含了大量的函数和数据类型,可以方便地进行各种操作和处理。 3. 高效的执行速度:C语言编写的程序执行速度非常快,因为它是一种编译型语言,可以直接转换为机器代码执行。 4. 跨平台性:C语言的代码可以在不同的操作系统和平台上运行,只需稍作修改。这使得C语言成为编写移植性强的程序的理想选择。 5. 应用广泛:C语言广泛应用于系统级编程、嵌入式系统、游戏开发、网络编程等领域。许多操作系统和编译器都是用C语言编写的。 总而言之,C语言是一种强大而灵活的编程语言,具有高效、可移植和广泛应用等特点。它为程序员提供了丰富的功能和工具,使得开发复杂的软件变得更加简单和容易。无论是初学者还是经验丰富的开发者,学习和掌握C语言都是非常有价值的。 ### 回答2: c可以代表很多东西,如: 1. C语言:C语言是一种通用的高级程序设计语言,由贝尔实验室的Dennis M. Ritchie在20世纪70年代开发。它是一门强大且灵活的语言,广泛用于系统软件开发,嵌入式系统以及其他计算机应用程序的编写。 2. 久仰大名:C语言的名字很有名,因为它在计算机编程领域具有重要的地位,并且已成为许多计算机科学课程的基石。学习C语言可以帮助人们了解计算机的底层工作原理并提高编程能力。 3. 碳元素:C是化学元素周期表上的一个符号,代表碳元素。碳是地球上最常见的元素之一,它存在于各种有机化合物中,并且在生命的基础元素中起着重要作用。 4. 高音调:在西方音乐中,C是一个高音调的音符,它位于音阶的第一位。C音符也常用作调音参考,用来调整乐器的音高。 综上所述,C是一个多义词,可以指代C语言、久仰大名、碳元素以及高音调。具体取决于背景和语境。
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

若汝棋茗

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值