【C#】SuperSocket 服务端使用总结
简介
SuperSocket 是一个轻量级, 跨平台而且可扩展的 .Net/Mono Socket 服务器程序框架。你无须了解如何使用 Socket, 如何维护 Socket 连接和 Socket 如何工作,但是你却可以使用 SuperSocket 很容易的开发出一款 Socket 服务器端软件,例如游戏服务器,GPS 服务器, 工业控制服务和数据采集服务器等等。
官网地址:
我们目前使用的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.cnblogs.com/douzi2/
如果您觉得阅读本文对您有帮助,请点一下“推荐”按钮,您的“推荐”将是我最大的写作动力!欢迎各位转载,但是未经作者本人同意,转载文章之后必须在文章页面明显位置给出作者和原文连接,否则保留追究法律责任的权利。