大致浏览了一遍,Scut 的网络模型采用的是 SAEA 模型, 它是 .NET Framework 3.5 开始支持的一种支持高性能 Socket 通信的实现。

  通过分析 Scut 的套接字监听控制,就能大致明白它是如何使用 SAEA 架构的。

1. 套接字缓冲区内存管理器

  先来看下 Scut 对套接字缓冲区的内存管理:

    class BufferManager
{
int capacity;
byte[] bufferBlock;
Stack<int> freeIndexPool;
int currentIndex;
int saeaSize; /*
* capacity 表示为所有套接字准备的内存容量
* saeaSzie 表示单个套接字所需的内存量
*/
public BufferManager(int capacity, int saeaSize)
{
this.capacity = capacity;
this.saeaSize = saeaSize;
this.freeIndexPool = new Stack<int>();
} //申请整份的内存空间
internal void InitBuffer()
{
this.bufferBlock = new byte[capacity];
} //为每个 SAEA 向缓存管理器申请缓存
internal bool SetBuffer(SocketAsyncEventArgs args)
{
if (this.freeIndexPool.Count > ) //用一个堆栈记录非顺序释放的内存块,优先使用这些内存块作为缓存
{
args.SetBuffer(this.bufferBlock, this.freeIndexPool.Pop(), this.saeaSize);
}
else
{
if ((capacity - this.saeaSize) < this.currentIndex)
{
return false;
}
args.SetBuffer(this.bufferBlock, this.currentIndex, this.saeaSize);
this.currentIndex += this.saeaSize;
}
return true;
} //为SAEA将缓存还给缓存管理器
internal void FreeBuffer(SocketAsyncEventArgs args)
{
this.freeIndexPool.Push(args.Offset);
args.SetBuffer(null, , );
}
}

  使用一个堆栈来管理”碎片大小相同、随时取用与释放”的内存块,这段代码算是十分高效与简介了。

2. SocketListener 的初始化

        private void Init()
{
this.bufferManager.InitBuffer(); for (int i = ; i < this.socketSettings.MaxAcceptOps; i++) //创建一个接受连接的SAEA池子
{
this.acceptEventArgsPool.Push(CreateAcceptEventArgs());
        private SocketAsyncEventArgs CreateAcceptEventArgs()
{
SocketAsyncEventArgs acceptEventArg = new SocketAsyncEventArgs();
acceptEventArg.Completed += new EventHandler<SocketAsyncEventArgs>(Accept_Completed);  //这部分SAEA绑定的都是“完成连接”事件处理API
return acceptEventArg;
}
            }

            SocketAsyncEventArgs ioEventArgs;
for (int i = ; i < this.socketSettings.NumOfSaeaForRecSend; i++) //创建一个处理IO的SAEA池子
{
ioEventArgs = new SocketAsyncEventArgs();
this.bufferManager.SetBuffer(ioEventArgs);
ioEventArgs.Completed += new EventHandler<SocketAsyncEventArgs>(IO_Completed); //这部分SAEA绑定的都是“IO”事件处理API
DataToken dataToken = new DataToken();
dataToken.bufferOffset = ioEventArgs.Offset;  //每个SAEA在缓存管理器中获取的内存块的起始偏移都是唯一的,可以用来做唯一标识
ioEventArgs.UserToken = dataToken;
this.ioEventArgsPool.Push(ioEventArgs);
}
_summaryTimer = new Timer(OnSummaryTrace, null, , );
       
    public class SummaryStatus     //日志定时记录连接的状态
{
/// <summary>
///
/// </summary>
public long TotalConnectCount;
/// <summary>
///
/// </summary>
public int CurrentConnectCount;
/// <summary>
///
/// </summary>
public int RejectedConnectCount;
/// <summary>
///
/// </summary>
public int CloseConnectCount;
}
        }

3. 监听-连接-数据传输流程

  那么,这么多SAEA是如何工作的呢?

        public void StartListen()
{
listenSocket = new Socket(this.socketSettings.LocalEndPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp); //建立TCP监听套接字
listenSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); //进一步设置监听套接字参数
listenSocket.Bind(this.socketSettings.LocalEndPoint); //绑定端口
listenSocket.Listen(socketSettings.Backlog); //开始监听,并设置最大排队连接数
_isStart = true;
requestHandler.Bind(this);
PostAccept();
}

看一下 SocketOptionLevel 的作用:

  SocketOptionLevel.IP:仅适用于 IP 套接字;

  SocketOptionLevel.IPv6:仅适用于 IPv6 套接字;

  SocketOptionLevel.Socket:适用于所有套接字;

  SocketOptionLevel.Tcp、SocketOptionLevel.Udp:适用于TCP、UDP套接字;

SocketOptionName.ReuseAddress:允许将套接字绑定到已在使用中的地址。

        private void PostAccept()
{
try
{
if (!_isStart)
{
return;
}
SocketAsyncEventArgs acceptEventArgs = acceptEventArgsPool.Pop() ?? CreateAcceptEventArgs(); //从accept SAEA池中取出一个SAEA交给监听套接字去获取连接参数
bool willRaiseEvent = listenSocket.AcceptAsync(acceptEventArgs);
if (!willRaiseEvent) //直接同步取得连接则顺序执行,异步取得则触发 Accept_Completed 事件处理函数
{
ProcessAccept(acceptEventArgs); //处理连接
}
}
catch (Exception ex)
{
TraceLog.WriteError("Post accept listen error:{0}", ex);
}
}

  我们可以看到在 Accept_Completed 中也是同样调用了 ProcessAccept;

        private void Accept_Completed(object sender, SocketAsyncEventArgs acceptEventArgs)
{
try
{
ProcessAccept(acceptEventArgs);
}
catch (Exception ex)
{
... ...
}
}

  继续看 ProcessAccept 是如何工作的:

        private void ProcessAccept(SocketAsyncEventArgs acceptEventArgs)
{
try
{
Interlocked.Increment(ref _summaryStatus.TotalConnectCount);    //向监控器提交“总连接数+1”
maxConnectionsEnforcer.WaitOne();                    //堵塞一个信号量,由此可知,信号量的总数控制了可并发处理的accept连接数 if (acceptEventArgs.SocketError != SocketError.Success)
{
Interlocked.Increment(ref _summaryStatus.RejectedConnectCount);   //向监控器提交“被拒绝连接数+1”
HandleBadAccept(acceptEventArgs);
}
else
{
Interlocked.Increment(ref _summaryStatus.CurrentConnectCount);   //向监控器提交“当前连接数+1” SocketAsyncEventArgs ioEventArgs = this.ioEventArgsPool.Pop(); //获取IO SAEA 池中的一个SAEA
ioEventArgs.AcceptSocket = acceptEventArgs.AcceptSocket; //将 accept 建立的 io 套接字交给该 SAEA
var dataToken = (DataToken)ioEventArgs.UserToken;
ioEventArgs.SetBuffer(dataToken.bufferOffset, socketSettings.BufferSize);  //为 io SAEA 提供缓存
var exSocket = new ExSocket(ioEventArgs.AcceptSocket); // 将 io 套接字用 ExSocket 管理起来
exSocket.LastAccessTime = DateTime.Now;
dataToken.Socket = exSocket;
acceptEventArgs.AcceptSocket = null; //release connect when socket has be closed.
ReleaseAccept(acceptEventArgs, false);   //该 accept SAEA 已经完成任务,释放其资源                
try
{
OnConnected(new ConnectionEventArgs { Socket = exSocket });  //OnConnected 是 SocketListener 的“连接事件订阅器”,成功连接时触发该订阅
}
catch (Exception ex)
{
TraceLog.WriteError("OnConnected error:{0}", ex);
}
PostReceive(ioEventArgs);
} }
finally
{
PostAccept();  //处理完毕后又重新开始监听
}
     }

  可以看到这个api 做的最重要的事情:1. 将建立连接的socket交给ioSAEA;2. ioSAEA去底层获取消息;3. 继续监听;

  疑问:如果只有1个监听套接字,为什么要做一个 acceptpool? 

  再来看下 ioSAEA 的工作流程:

        private void PostReceive(SocketAsyncEventArgs ioEventArgs)
{
if (ioEventArgs.AcceptSocket == null) return; bool willRaiseEvent = ioEventArgs.AcceptSocket.ReceiveAsync(ioEventArgs); //异步接收io数据 if (!willRaiseEvent) //如果同步获得直接处理,异步获得则由异步回调处理
{
ProcessReceive(ioEventArgs);
}
}

  无论哪种处理方式,都是调用 ProcessReceive,其中比较重要的部分:

bool needPostAnother = requestHandler.TryReceiveMessage(ioEventArgs, out messages, out hasHandshaked);

  在 SocketListener 启动的时候我们注意到:

requestHandler.Bind(this);

  监听套接字管理器自带 requestHandle,这是个什么东西?

    public class RequestHandler
{
public RequestHandler(BaseMessageProcessor messageProcessor)
{
MessageProcessor = messageProcessor;
} internal virtual void Bind(ISocket appServer)
{
AppServer = appServer;
} public ISocket AppServer { get; private set; }
... ...
}
        protected GameSocketHost()
: this(new RequestHandler(new MessageHandler()))
{
} protected GameWebSocketHost(bool isSecurity = false)
: this(new WebSocketRequestHandler(isSecurity))
{
}

  从 Scut 的以上代码应该可以得知:什么类型的套接字宿主应该绑定相应类型的消息处理API

  进一步观察,websocket 与 socket 的“消息发送API”是一致的,而“消息读取”API则完全不同,这里涉及到更加具体的协议规则,有空再回来研究这块内容。

  继续回到 ProcessReceive:

                        switch (message.OpCode)
{
case OpCode.Close:
var statusCode = requestHandler.MessageProcessor != null
? requestHandler.MessageProcessor.GetCloseStatus(message.Data)
: OpCode.Empty;
if (statusCode != OpCode.Empty)
{
DoClosedStatus(exSocket, statusCode);
}
Closing(ioEventArgs, OpCode.Empty);
needPostAnother = false;
break;
case OpCode.Ping:
DoPing(new ConnectionEventArgs { Socket = exSocket, Meaage = message });
break;
case OpCode.Pong:
DoPong(new ConnectionEventArgs { Socket = exSocket, Meaage = message });
break;
default:
OnDataReceived(new ConnectionEventArgs { Socket = exSocket, Meaage = message });
break;
}

  如果是常规数据,则调用 OnDataReceived,这是更上一层注册的逻辑消息处理API,正常来说,到了进入“应用消息分发器”-IActionDispatcher 的节奏了。

  继续往上查,果然不出意料。

  

Scut:SocketListener 的解析的更多相关文章

  1. Scut:GameWebSocketHost 解析

    想使用 Scut 做的是一个短连接项目,所以先直接看 GameWebSocketHost 了. 先来看下 GameWebSocketHost 的成员: protected bool EnableHtt ...

  2. Scut:账号服务器问题修正

    姑且记录一下,以防未来出现bug回来看看今天改了哪些. 原 Scut 账服是应用于 渠道频道 的账号服务器,每天会发放大量的游客账号,它有一个"自动将已经被注册了一段时间的游客账号再重新推送 ...

  3. Scut:从PackageReader分析客户端协议规则

    看第一个解析API: private void ParseData(byte[] data) { var paramBytes = SplitBuffer(data); RawParam = _enc ...

  4. Scut游戏server引擎Unity3d访问

    Scut提供Unity3d Sdk包.便利的高速发展和Scut游戏server对接: 看Unity3d示为以下的比率: 启动Unity3d项目 打开Scutc.svn\SDK\Unity3d\Asse ...

  5. Scut游戏服务器引擎之Unity3d接入

    Scut提供Unity3d Sdk包,方便开发人员快速与Scut游戏服务器对接: 先看Unity3d示例如下: 启动Unity3d项目 打开Scutc.svn\SDK\Unity3d\Assets目录 ...

  6. 如何部署和运行Scut服务器及游戏:Windows篇

    概述 Scut游戏引擎是一个永久免费的全脚本游戏服务器框架,采用MVC框架设计,简化数据库设计和编码工作:降低对开发人员的开发难度:同时提供了丰富的类库和API接口. 一.    安装环境 必须安装的 ...

  7. JSON介绍及Android最全面解析方法(Gson、AS自带org.son、Jackson解析)

    前言 今天,我们来介绍一下现今主流的数据交换格式-JSON! 相同作为主流为数据交换格式-XML,假设有兴趣能够阅读我写的XML及其DOM.SAX.PULL解析方法和对照 文件夹 定义 JavaScr ...

  8. 【原】Android热更新开源项目Tinker源码解析系列之三:so热更新

    本系列将从以下三个方面对Tinker进行源码解析: Android热更新开源项目Tinker源码解析系列之一:Dex热更新 Android热更新开源项目Tinker源码解析系列之二:资源文件热更新 A ...

  9. .NET Core中的认证管理解析

    .NET Core中的认证管理解析 0x00 问题来源 在新建.NET Core的Web项目时选择“使用个人用户账户”就可以创建一个带有用户和权限管理的项目,已经准备好了用户注册.登录等很多页面,也可 ...

随机推荐

  1. 【bzoj2333】 SCOI2011—棘手的操作

    http://www.lydsy.com/JudgeOnline/problem.php?id=2333 (题目链接) 题意 N个节点维护一些操作.. Solution 我们用可并大根堆进行维护. 对 ...

  2. Thread Join()的用法

    Java Thread类有个 join() 方法,先前一直不知道是怎么用的,直到看到这篇文章.http://auguslee.iteye.com/blog/1292203 Java Thread中, ...

  3. 滑动到底部或顶部响应的ScrollView实现

    关于使用可见:滚动到底部或顶部响应的ScrollView使用 示例APK可从这些地址下载:Google Play,  360手机助手,  百度手机助手,  小米应用商店,  豌豆荚 两种实现方式的主要 ...

  4. evernote出现"Invalid username and/or password"的情况

    evernote出现"Invalid username and/or password"的情况 evernote挺好用的,可是这几年用下来也遇到过狗血情况,几乎每次都是更新后出状况 ...

  5. Qt 学习之路 :访问网络(4)

    前面几章我们了解了如何使用QNetworkAccessManager 访问网络.在此基础上,我们已经实现了一个简单的查看天气的程序.在这个程序中,我们使用QNetworkAccessManager进行 ...

  6. cocos2d-x项目过程记录(ios和android设备的适配)

    (原创作品,欢迎转载,注明出处,谢谢:http://www.cnblogs.com/binxindoudou/admin/EditPosts.aspx?postid=3213645) 1.原理分析的博 ...

  7. 关于Xcode的Other Linker Flags

    背景 在ios开发过程中,有时候会用到第三方的静态库(.a文件),然后导入后发现编译正常但运行时会出现selector not recognized的错误,从而导致app闪退.接着仔细阅读库文件的说明 ...

  8. 一些Linux优化方法

    1. 利用栈做备胎,减少分配空间的几率,IO自己有一份缓存,如果超了就使用stack空间 2. 分散IO:代表readv,可以通过一次系统调用,将内容读到分散的缓存中,可以减少系统的系统调用

  9. Java基础知识强化之集合框架笔记09:Collection集合迭代器使用的问题探讨

    1.Collection集合迭代器使用的问题探讨: (1)问题1:能用while循环写这个程序,我能不能用for循环呢?                  可以使用for循环替代. (2)问题2:不要 ...

  10. yii phpexcel <转>

    原文详情参见 这里 1.下载phpexcel,将压缩包中的classes复制到protected/extensions下并修改为PHPExcel. 2.修改YII配置文件config/main.php ...