【C#】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

  • 5
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
好的,下面是一个简单的C# Socket服务端使用SocketAsyncEventArgs的示例代码: ```csharp using System; using System.Net; using System.Net.Sockets; using System.Threading; public class SocketServer { private Socket m_serverSocket; private Semaphore m_maxAcceptedClients; private SocketAsyncEventArgsPool m_readWritePool; public SocketServer(int maxConnections) { m_serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); m_serverSocket.Bind(new IPEndPoint(IPAddress.Any, 1234)); m_serverSocket.Listen(maxConnections); m_maxAcceptedClients = new Semaphore(maxConnections, maxConnections); m_readWritePool = new SocketAsyncEventArgsPool(maxConnections); for (int i = 0; i < maxConnections; i++) { SocketAsyncEventArgs readEventArgs = new SocketAsyncEventArgs(); readEventArgs.Completed += new EventHandler<SocketAsyncEventArgs>(IO_Completed); readEventArgs.UserToken = new AsyncUserToken(); m_readWritePool.Push(readEventArgs); } } public void Start() { StartAccept(null); } private void StartAccept(SocketAsyncEventArgs acceptEventArgs) { if (acceptEventArgs == null) { acceptEventArgs = new SocketAsyncEventArgs(); acceptEventArgs.Completed += new EventHandler<SocketAsyncEventArgs>(Accept_Completed); } else { acceptEventArgs.AcceptSocket = null; } m_maxAcceptedClients.WaitOne(); if (!m_serverSocket.AcceptAsync(acceptEventArgs)) { ProcessAccept(acceptEventArgs); } } private void ProcessAccept(SocketAsyncEventArgs acceptEventArgs) { SocketAsyncEventArgs readEventArgs = m_readWritePool.Pop(); AsyncUserToken userToken = (AsyncUserToken)readEventArgs.UserToken; userToken.Socket = acceptEventArgs.AcceptSocket; userToken.ReadEventArgs = readEventArgs; readEventArgs.AcceptSocket = userToken.Socket; if (!userToken.Socket.ReceiveAsync(readEventArgs)) { ProcessReceive(readEventArgs); } StartAccept(acceptEventArgs); } private void Accept_Completed(object sender, SocketAsyncEventArgs e) { ProcessAccept(e); } private void IO_Completed(object sender, SocketAsyncEventArgs e) { switch (e.LastOperation) { case SocketAsyncOperation.Receive: ProcessReceive(e); break; case SocketAsyncOperation.Send: ProcessSend(e); break; default: throw new ArgumentException("The last operation completed on the socket was not a receive or send"); } } private void ProcessReceive(SocketAsyncEventArgs e) { AsyncUserToken userToken = (AsyncUserToken)e.UserToken; if (e.BytesTransferred > 0 && e.SocketError == SocketError.Success) { userToken.Data.AddRange(e.Buffer.Take(e.BytesTransferred)); if (userToken.Data.Count >= 4) { int len = BitConverter.ToInt32(userToken.Data.Take(4).ToArray(), 0); if (userToken.Data.Count >= 4 + len) { byte[] buffer = userToken.Data.Skip(4).Take(len).ToArray(); userToken.Data.RemoveRange(0, 4 + len); // 处理请求并响应 byte[] response = HandleRequest(buffer); Send(userToken.Socket, response); return; } } if (!userToken.Socket.ReceiveAsync(e)) { ProcessReceive(e); } } else { CloseClientSocket(userToken); } } private void ProcessSend(SocketAsyncEventArgs e) { AsyncUserToken userToken = (AsyncUserToken)e.UserToken; if (e.SocketError == SocketError.Success) { if (!userToken.Socket.ReceiveAsync(userToken.ReadEventArgs)) { ProcessReceive(userToken.ReadEventArgs); } } else { CloseClientSocket(userToken); } } private void Send(Socket socket, byte[] buffer) { SocketAsyncEventArgs sendEventArgs = new SocketAsyncEventArgs(); sendEventArgs.Completed += new EventHandler<SocketAsyncEventArgs>(IO_Completed); sendEventArgs.SetBuffer(buffer, 0, buffer.Length); sendEventArgs.UserToken = socket; if (!socket.SendAsync(sendEventArgs)) { ProcessSend(sendEventArgs); } } private void CloseClientSocket(AsyncUserToken userToken) { try { userToken.Socket.Shutdown(SocketShutdown.Send); } catch (Exception) { } userToken.Socket.Close(); m_readWritePool.Push(userToken.ReadEventArgs); m_maxAcceptedClients.Release(); } private byte[] HandleRequest(byte[] request) { // 处理请求并响应 return null; } } public class AsyncUserToken { public Socket Socket { get; set; } public List<byte> Data { get; set; } public SocketAsyncEventArgs ReadEventArgs { get; set; } public AsyncUserToken() { Data = new List<byte>(); } } ``` 以上示例代码演示了如何使用SocketAsyncEventArgs实现一个简单的Socket服务端,其中包含了异步接收、异步发送和连接池等功能。当有客户端连接时,它会从连接池中获取一个SocketAsyncEventArgs对象,并使用它来进行通信。在通信完成后,它会将SocketAsyncEventArgs对象放回连接池中,以便于重用。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

code bean

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

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

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

打赏作者

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

抵扣说明:

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

余额充值