【C#】SuperSocket 服务端使用总结

news/2024/5/17 18:51:01 标签: 服务器, TCP, supersocket

简介

SuperSocket 是一个轻量级, 跨平台而且可扩展的 .Net/Mono Socket 服务器程序框架。你无须了解如何使用 Socket, 如何维护 Socket 连接和 Socket 如何工作,但是你却可以使用 SuperSocket 很容易的开发出一款 Socket 服务器端软件,例如游戏服务器,GPS 服务器, 工业控制服务和数据采集服务器等等。

官网地址:

Home - SuperSocket

我们目前使用的1.6的版本。目前已经出到2.0,支持.net core

安装

这个包专门用于构建服务端:

服务端简单构建

配置文件

App.config中添加代码如下:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
	<!-- superSocket 相关配置-->
	<configSections>
		<section name="superSocket"
			 type="SuperSocket.SocketEngine.Configuration.SocketServiceConfig, SuperSocket.SocketEngine" />
	</configSections>


	<superSocket>
		<servers>
			<!-- UTF-8  Ascii  gb2312-->
			<server name="TestSvr"
					textEncoding="UTF-8"
					serverType="TxSocketLib.Server.TestSvr, TxSocketLib"
					ip="Any"
					port="8053"
					maxConnectionNumber="100">
			</server>

			<!-- 可以配置多个Server-->
		</servers>
	</superSocket>


	<startup>
		<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.8" />
	</startup>
	

</configuration>

这里有个坑,要注意,就是SuperSocket的相关节点必须放到最前面,不然会导致服务启动失败

服务类

配置文件中指定的类:TestSvr

public class TestSvr: AppServer
{
}

构建类很简单,继承一下AppServer就OK了,其他的都不用写。

不过这是最简单的一种写法,后续再进行扩展。

这里其实是一个反射过程,serverType="TxSocketLib.Server.TestSvr, TxSocketLib"就是指定TestSvr位置。

配置的加载与服务的启动

这个配置文件是需要配合一段后台代码进行加载的。如下:

public void StartSvr()
        {
            IBootstrap bootstrap = BootstrapFactory.CreateBootstrap();
            if (!bootstrap.Initialize())//读取配置文件;  如果读取失败了;
            {
                MessageBox.Show("初始化服务失败了。。。。");
                return;
            }
            logger.Debug("开始启动服务~~");
            StartResult result = bootstrap.Start();//启动服务 
            foreach (var server in bootstrap.AppServers)
            {
                if (server.State == ServerState.Running)
                {
                     if (server.Name == "TestSvr")
                    {
                        TestSvr svr = server as TestSvr;
                        svr.NewRequestReceived += Svr_test; ;
                    }

                }
                else
                {

                    logger.Error($"{server.Name} 服务启动失败。");
                }
            }
        }


private void Svr_test(AppSession session, SuperSocket.SocketBase.Protocol.StringRequestInfo requestInfo)
        {
            //合起来才是全部的数据
            var result = requestInfo.Key + requestInfo.Body;
            logger.Debug($"{session.Config.Name}收到数据: {result} ");
        }

1、因为配置文件中是可以配置多个服务端的,使用这里用到了for循环,通过配置服务名称进行区分。

2、NewRequestReceived 事件,会在服务端接收到完整的数据后促发。

服务启动测试

接下来就调用StartSvr启动测试一下:

 我们开启一个TCP的客户端发送数据。

Session 和 RequestInfo 

我们首先要看的是事件里面的:

AppSession session, SuperSocket.SocketBase.Protocol.StringRequestInfo requestInfo

我们的TestSvr继承了 AppServer之后,就能订阅这个事件。

        每个连接的客户端都以Session的方式管理,发送数据给客户端也通过Session的Send方法,

AppSession就是默认的Session,我们可以自定义自己的Session。

        ReqestInfo包含了接收数据内容,他的目的是将接收到的数据进行解析,或者说是格式化。里面默认包含了Key和Body。StringRequestInfo 就是默认的数据格式。

Session 和 RequestInfo以及Server是一一对应的。

默认的格式

1 默认情况下,我们发送的数据需要以回车换行结尾,这样表示一条的结束。服务端才会触发接收事件。

2 数据中的一个或多个连续的空格会被当做分隔符,分割的首个字符串会被保存到StringRequestInfo的Key中,其他的会被保存到Body中。这个看上面的图,非常清楚。

自定义服务

之前我们构建服务的时候非常简单:

public class TestSvr: AppServer
{
}

其实这里省略了,Session 和 RequestInfo,Session默认的就是AppSession ,RequestInfo默认是的StringRequestInfo 。

如果想构建一个Server,就必须对于构建Session 和 RequestInfo。要构建一个Session,就必须构建一个RequestInfo。

自定义RequestInfo

自定义RequestInfo需要继承IRequestInfo:

/// <summary>
/// 简单的将过来的数据进行格式化
/// </summary>
public class SimpleRequestInfo : IRequestInfo
{
    public SimpleRequestInfo(byte[] header, byte[] body)
    {
        //消息包头部,大小端转换
        Key = (((int)header[0] << 8) + header[1]).ToString();
        //正文部分
        Body = Encoding.UTF8.GetString(body, 0, body.Length);
        //固定头含义(1:平台数据,2,表示心跳)
        IsHeart = string.Equals("2", Key);
    }

    //接口必须实现的部分
    public string Key { get; set; }

    public bool IsHeart { get; set; }

    public string Body { get; set; }
}

RequestInfo的职责就是将接收的数据进行格式化,或者说是解析。这里header,和body会按照规则传递过来,这个会引出过滤器的概念,后面再讲。类似之前提到的默认规则。

自定义Session

自定义Session就需要关联一个RequestInfo,我们就关联刚刚自定义的SimpleRequestInfo。

public class SimpleSession : AppSession<SimpleSession, SimpleRequestInfo>
{
    /// <summary>
    /// 异常处理
    /// </summary>
    /// <param name="e"></param>
    protected override void HandleException(Exception e)
    {
        this.Send("Application error: {0}", e.Message);
    }

    /// <summary>
    /// 有新的命令执行的时候触发;
    /// 只有服务不去订阅NewRequestReceived事件的时候,才会触发这个函数
    /// </summary>
    /// <param name="requestInfo"></param>
    protected override void HandleUnknownRequest(SimpleRequestInfo requestInfo)
    {
        base.HandleUnknownRequest(requestInfo);
    }

    protected override void OnSessionStarted()
    {
        base.OnSessionStarted();
    }

    protected override void OnSessionClosed(CloseReason reason)
    {
        //add you logics which will be executed after the session is closed
        base.OnSessionClosed(reason);
    }

}

连接的客户端都以Session的方式管理,自定义的Session,可以重写很多方法,这些方法提供了一些切面,来方便我们管控客户端连接。

自定义服务

有了Session 和 RequestInfo之后,我们就可以自定义服务了:

/// <summary>
/// 服务
/// </summary>
public class MySvr : AppServer<SimpleSession, SimpleRequestInfo>
{
}

这才是完整的写法。

过滤器

结束符协议

我们可以通过服务的构造函数装配过滤器。

/// <summary>
/// 服务
/// </summary>
public class MySvr : AppServer<SimpleSession, SimpleRequestInfo>
: base(new TerminatorReceiveFilterFactory("##"))
{
}

之前我们的数据的结束是回车换行,现在这么写的话,结束符就变成了##。

固定头协议的

自定义过滤器

using SuperSocket.Facility.Protocol;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using TxSocketLib.RequestInfo;

namespace TxSocketLib.Filter
{
    //数据格式:
    //  -------+----------+------------------------------------------------------+
    //  0001   | 00000010 |  4C36 3150 2D43 4D2B 4C30 3643 5055 2D43 4D2B 4C4A   |
    //  固定头 | 数据长度 |  数据                                                |
    //  2byte  |  4byte   |                                                      |
    //  -------+----------+------------------------------------------------------+
    public class MyFixedHeaderFilter : FixedHeaderReceiveFilter<SimpleRequestInfo>
    {
        public MyFixedHeaderFilter()
        : base(6)
        {

        }


        /// <summary>
        /// 获取数据长度部分
        /// </summary>
        /// <param name="header"></param>
        /// <param name="offset"></param>
        /// <param name="length"></param>
        /// <returns></returns>
        protected override int GetBodyLengthFromHeader(byte[] header, int offset, int length)
        {
            //大小端转换(从网络的大端转到小端)
            int l = (int)(header[offset + 2] << 3 * 8)
                  + (int)(header[offset + 3] << 2 * 8)
                  + (int)(header[offset + 4] << 1 * 8)
                  + (int)header[offset + 5];
            return l;
        }


        /// <summary>
        /// 加过滤器的好处是,会将没有用的信息自动跳出去
        /// 就体现在下面这段代码了!!!!
        /// </summary>
        /// <param name="header"></param>
        /// <param name="bodyBuffer"></param>
        /// <param name="offset"></param>
        /// <param name="length"></param>
        /// <returns></returns>
        protected override SimpleRequestInfo ResolveRequestInfo(ArraySegment<byte> header, byte[] bodyBuffer, int offset, int length)
        {
            if (bodyBuffer == null) return null;
            // 获取body内容,length就是body的长度
            var body = bodyBuffer.Skip(offset).Take(length).ToArray();
            // 构建消息实例
            var info = new SimpleRequestInfo(header.ToArray(), body);
            return info;
        }
    }
}

这里有几个注意的点:

1 大小端问题,网络应该用大端协议(C#是默认的小端,所以需要转换),这里的协议头还有数据长度,应该转换为大端。数据部分内容是规定的格式(如UTF8),不用转大小端。大小端是针对数字型变量,不针对字符串

2 这里描述数据长度的变量是四个字节,所以应该用uint,如果是两个字节应该用ushort。大小端就是针对uint和ushort类型的变量。

3 过滤器相当于是Session前方的筛子,所以它也和RequestInfo一一对应的,他会将过滤后的值构建成一个RequestInfo。

 拥有自定义过滤器的服务

/// <summary>
/// 固定头协议服务
/// </summary>
public class FixedHeaderSvr : AppServer<SimpleSession, SimpleRequestInfo>
{
    public FixedHeaderSvr()
        : base(new DefaultReceiveFilterFactory<MyFixedHeaderFilter, SimpleRequestInfo>()) //使用默认的接受过滤器工厂 (DefaultReceiveFilterFactory)
    {
        
    }
}

命令

更为优雅的处理方式是通过命令的方式,当服务不去订阅NewRequestReceived事件的时候,这个时候才有命令出场的机会。

这个以后再讲把,今天写的太累。

踩坑记录

2022年10月13日:(客户端无故被踢下线)

现象,数据过大时,服务端接收不到数据,客户端被踢下线。

通过调试:在Session中重写了OnSessionClosed,查看断线原因为 Protocol Error

protected override void OnSessionClosed(CloseReason reason)
        {
            //add you logics which will be executed after the session is closed
            base.OnSessionClosed(reason);
            TcpSvr._eventAggregator.GetEvent<LogEvent>().Publish("客户端断开原因: " + reason.ToString());
        }

最后定位到参数:maxRequestLength 

  • maxRequestLength: 最大允许的请求长度,默认值为1024;

随后修改配置文件,加上maxRequestLength给了一个较大的值,问题解决。

 也就是说,supersocket会判断接收数据的大小(一开始确实能收到信息),但是如果过大,是不接收事件的。并且会把这个链接断开。

以下是supersocket的其它设置,大家可以参考以下:

服务器实例配置

在根节点中,有一个名为 "servers" 的子节点,你可以定义一个或者多个server节点来代表服务器实例。 这些服务器实例可以是同一种 AppServer 类型, 也可以是不同的类型。

Server 节点的所有属性如下:

  • name: 服务器实例的名称;
  • serverType: 服务器实例的类型的完整名称;
  • serverTypeName: 所选用的服务器类型在 serverTypes 节点的名字,配置节点 serverTypes 用于定义所有可用的服务器类型,我们将在后面再做详细介绍;
  • ip: 服务器监听的ip地址。你可以设置具体的地址,也可以设置为下面的值 Any - 所有的IPv4地址 IPv6Any - 所有的IPv6地址
  • port: 服务器监听的端口;
  • listenBacklog: 监听队列的大小;
  • mode: Socket服务器运行的模式, Tcp (默认) 或者 Udp;
  • disabled: 服务器实例是否禁用了;
  • startupOrder: 服务器实例启动顺序, bootstrap 将按照此值的顺序来启动多个服务器实例;
  • sendTimeOut: 发送数据超时时间;
  • sendingQueueSize: 发送队列最大长度, 默认值为5;
  • maxConnectionNumber: 可允许连接的最大连接数;
  • receiveBufferSize: 接收缓冲区大小;
  • sendBufferSize: 发送缓冲区大小;
  • syncSend: 是否启用同步发送模式, 默认值: false;
  • logCommand: 是否记录命令执行的记录;
  • logBasicSessionActivity: 是否记录session的基本活动,如连接和断开;
  • clearIdleSession: true 或 false, 是否定时清空空闲会话,默认值是 false;
  • clearIdleSessionInterval: 清空空闲会话的时间间隔, 默认值是120, 单位为秒;
  • idleSessionTimeOut: 会话空闲超时时间; 当此会话空闲时间超过此值,同时clearIdleSession被配置成true时,此会话将会被关闭; 默认值为300,单位为秒;
  • security: Empty, Tls, Ssl3. Socket服务器所采用的传输层加密协议,默认值为空;
  • maxRequestLength: 最大允许的请求长度,默认值为1024;
  • textEncoding: 文本的默认编码,默认值是 ASCII;
  • defaultCulture: 此服务器实例的默认 thread culture, 只在.Net 4.5中可用而且在隔离级别为 'None' 时无效;
  • disableSessionSnapshot: 是否禁用会话快照, 默认值为 false.
  • sessionSnapshotInterval: 会话快照时间间隔, 默认值是 5, 单位为秒;
  • keepAliveTime: 网络连接正常情况下的keep alive数据的发送间隔, 默认值为 600, 单位为秒;
  • keepAliveInterval: Keep alive失败之后, keep alive探测包的发送间隔,默认值为 60, 单位为秒;
  • certificate: 这各节点用于定义用于此服务器实例的X509Certificate证书的信息

2022年10月25日 SuperSocket 中文乱码问题

写了一个Terminator的服务,修改了结束符,一改发现中文就乱码了!我在配置中配置了服务默认为UTF8:

<server name="TerminatorSvr"  textEncoding="UTF-8" serverType="TxSocketLib.Server.TerminatorSvr,  TxSocketLib" ip="Any" port="8054" maxConnectionNumber="100" maxRequestLength="1073741824">
			</server>

发送也是按UTF8发送的,但是接收就乱码了,后来发现,TerminatorReceiveFilterFactory有个重载函数,可以指定编码!改了就没乱码了!

public TerminatorSvr()
    : base(new TerminatorReceiveFilterFactory("##",Encoding.UTF8))
{

}

默认情况下:TerminatorReceiveFilterFactory是ASCII


http://www.niftyadmin.cn/n/1734694.html

相关文章

vs2022 将github项目转到gitee

背景 最近GitHub平凡的登录不上&#xff0c;推送经常不成功&#xff0c;累了累了&#xff0c;果断Gitee啊。 github项目转到gitee 首先登录自己的gitee&#xff0c;然后点击右上角的加号&#xff0c;选择从GitHub/GitHub导入仓库&#xff1a; 然后来到界面&#xff0c;注意这里…

【wpf】如何去掉这种虚线

背景 一次被客户吐槽这个虚线太丑了吧&#xff1f; 解决方案 这个是通过键盘获取焦点的时候&#xff0c;或者突然出现。 确实不好看&#xff0c;这里有个简单的办法&#xff1a;FocusVisualStyle 设置为NULL <Button.Resources><Style TargetType"Button&quo…

【wpf】 列标题过长 datagrid 没有数据时如何添加滚动

背景 列标题过长 datagrid 没有数据时不显示滚动条&#xff0c;如下图&#xff1a; 解决方案 外面套一个ScrollViewer 就能解决问题&#xff1a; <ScrollViewer Grid.Row"2" HorizontalScrollBarVisibility"Auto" VerticalScrollBarVisibility"…

【Wpf】设置Icon路径

背景 当前环境为.Net6.0&#xff0c;和Framework有一些区别。 解决法案 目录结构 首先需要将图片设置为资源&#xff0c;这一步在.Net6.0是需要手动设置的&#xff0c;Framework默认就是资源。 然后是寻址&#xff1a;如果是.Net6.0这个点是必须的&#xff0c;Framework下不…

【踩坑日记】double的精度还是不够啊!-> decimal

不过&#xff0c;我发现了另一个解决办法&#xff1a; contentData[item.columnChName] Math.Round(decimal.Parse(test.Value), 3); 这样的话 Round返回的是decimal&#xff0c;这样的话精度够高&#xff0c;就没有出现精度丢失的情况了。

【halcon】.Net6 wpf与Halcon

新建一个.net6的wpf工程 右键依赖项&#xff0c;选择添加项目引用 点击浏览&#xff0c;选择添加halcondotnet.dll&#xff1a; 前台添加关键代码&#xff1a; 伪代码部分&#xff1a; xmlns:halcon"clr-namespace:HalconDotNet;assemblyhalcondotnet" <Grid>…

【wpf】GridSplitter用法总结

前言 布局控件Grid 配合 GridSplitter 无需编写任何代码 就能实现网格大小可拖动。 纵向GridSplitter与横向GridSplitter 注意将GridSplitter放在你需要滑动的的位置 <Grid ShowGridLines"True"><Grid.RowDefinitions><RowDefinition/><RowDe…

【最全,带注释版】雷赛运动控制卡库函数C#导出

发现雷赛发的最新的资料还没以前的详细&#xff0c;这里备份一下&#xff1a; using System; using System.Collections.Generic; using System.Text; using System.Runtime.InteropServices;namespace Hardware.ImportExternal //命名空间根据应用程序修改 {public delegate …