Scut:SocketListener 的解析

  大致浏览了一遍,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 > 0)    //用一个堆栈记录非顺序释放的内存块,优先使用这些内存块作为缓存
            {
                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, 0, 0);
        }
    }

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

2. SocketListener 的初始化

        private void Init()
        {
            this.bufferManager.InitBuffer();

            for (int i = 0; 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 = 0; 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, 600, 60000);       
    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 的节奏了。

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

  

时间: 2024-12-04 01:48:27

Scut:SocketListener 的解析的相关文章

Scut:GameWebSocketHost 解析

想使用 Scut 做的是一个短连接项目,所以先直接看 GameWebSocketHost 了. 先来看下 GameWebSocketHost 的成员: protected bool EnableHttp; public IActionDispatcher ActionDispatcher; private EnvironmentSetting _setting; private SocketListener socketListener; 由之前的分析可知:SocketListener 搞定了监

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

看第一个解析API: private void ParseData(byte[] data) { var paramBytes = SplitBuffer(data); RawParam = _encoding.GetString(paramBytes); ParseParamString(RawParam); } 再看如何分隔数据包: private byte[] SplitBuffer(byte[] data) { int paramIndex = MathUtils.IndexOf(dat

Scut游戏server引擎Unity3d访问

Scut提供Unity3d Sdk包.便利的高速发展和Scut游戏server对接: 看Unity3d示为以下的比率: 启动Unity3d项目 打开Scutc.svn\SDK\Unity3d\Assets文件夹下的TestScene.unity项目文件,选中Main Camera.将TestGUI.cs文件拖动到Inspector窗体的Script,如图: 点击执行.例如以下: 文件夹层次说明 1)       Net层:封装Http与Socket请求操作,以及网络协议的数据解析和请求參数的打包

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

[狗刨学习网] Scut提供Unity3d Sdk包,方便开发人员快速与Scut游戏服务器对接: 先看Unity3d示例如下: 启动Unity3d项目 打开Scutc.svnSDKUnity3dAssets目录下的TestScene.unity项目文件,选中Main Camera,将TestGUI.cs文件拖动到Inspector窗口的Script,如图: 点击运行,如下: 目录层次说明 1)       Net层:封装Http与Socket请求操作,以及网络协议的数据解析和请求参数的打包,其中

Android开发:JSON简介及最全面解析方法(Gson、AS自带org.json、Jackson解析)

前言 今天,我们来介绍现今主流的数据交换格式-JSON! 同样作为主流为数据交换格式-XML,如果有兴趣可以阅读我写的XML及其DOM.SAX.PULL解析方法和对比 目录 JSON简介&解析方法介绍.png 定义 JavaScript Object Notation,JavaScript的对象表示法,是一种轻量级的文本数据交换格式. 作用 用于数据的标记.存储和传输. 特点 轻量级的文本数据交换格式 独立于语言和平台 具有自我描述性 读写速度快,解析简单 语法 JSON值 名称/值 数组 对象

C++工程编译之“error LNK2001: 无法解析的外部符号”

今天一整天都在折腾“error LNK2001: 无法解析的外部符号”,就在头疼不已的时候,总算是找到问题原因了:各个动态链接库的编译方式必须统一才行,要不然很容易对库函数的引用产生冲突.简单来说就是,如果使用的第三方函数库编译方式采用/MD,那么主工程也应该使用/MD.我使用了libevent,而主工程默认采用/MT,所以需要忽略一大堆的函数库,我还纳闷呢,怎么会这么奇怪!!今天总算是解决了长久以来的困惑了. 下面引用一篇文章的描述:[Z]VC运行库版本不同导致链接.LIB静态库时发生重复定义

防止恶意解析——禁止通过IP直接访问网站

一.什么是恶意解析 一般情况下,要使域名能访问到网站需要两步,第一步,将域名解析到网站所在的主机,第二步,在web服务器中将域名与相应的网站绑定.但是,如果通过主机IP能直接访问某网站,那么把域名解析到这个IP也将能访问到该网站,而无需在主机上绑定,也就是说任何人将任何域名解析到这个IP就能访问到这个网站.可能您并不介意通过别人的域名访问到您的网站,但是如果这个域名是未备案域名呢?一旦被查出,封IP.拔线甚至罚款的后果都是需要您来承担的.某些别有用心的人,通过将未备案域名解析到别人的主机上,使其

.NET深入解析LINQ框架(五:IQueryable、IQueryProvider接口详解)

阅读目录: 1.环路执行对象模型.碎片化执行模型(假递归式调用) 2.N层对象执行模型(纵横向对比链式扩展方法) 3.LINQ查询表达式和链式查询方法其实都是空壳子 4.详细的对象结构图(对象的执行原理) 5.IQueryable<T>与IQueryProvider一对一的关系能否改成一对多的关系 6.完整的自定义查询 1]. 环路执行对象模型.碎片化执行模型(假递归式调用) 这个主题扯的可能有点远,但是它关系着整个LINQ框架的设计结构,至少在我还没有搞懂LINQ的本意之前,在我脑海里一直频

.NET深入解析LINQ框架(一:LINQ优雅的前奏)

阅读目录: 1.LINQ简述 2.LINQ优雅前奏的音符 2.1.隐式类型 (由编辑器自动根据表达式推断出对象的最终类型) 2.2.对象初始化器 (简化了对象的创建及初始化的过程) 2.3.Lambda表达式 (对匿名方法的改进,加入了委托签名的类型推断并很好的与表达式树的结合) 2.4.扩展方法 (允许在不修改类型的内部代码的情况下为类型添加独立的行为) 2.5.匿名类型 (由对象初始化器推断得出的类型,该类型在编译后自动创建) 2.6.表达式目录树(用数据结构表示程序逻辑代码) 3.LINQ