Unity3D C# Socket通信详解之连接池

2020年8月16日 0 作者 老王

上一篇文章Unity3D C# Socket通信详解之基础介绍中,我们介绍了Socket通信的基本知识,文中给的demo有几个需要优化的地方,第一个就是需要使用连接池来处理客户端的连接。这里就来填这个坑。

1 原理

连接池的原理很简单,就是程序刚开始运行的时候,先实例化一个连接的数组,然后有客户端连接上来的时候就把这个连接分配给客户端。这样就避免了每次有客户端连接都需要去重新new一个连接对象(例子中的Conn类),因为new一个对象需要去申请和分配内存,比较耗时(尽管是很短很短的时间)。
连接池依然是用"空间换时间",使用这种思想的地方有很多很多,比如游戏中的对象池,Unity3D中的LOD、MipMap等等,如果我们的程序需要频繁实例化相同的类,不妨考虑使用“池”的思想来优化代码。

2 例子

原理这么简单,那就废话少说,直接上码了。
Conn类,每次有新客户端连接就从连接池中取出一个Conn对象,里面存放了客户端对应的Socket和读取数据的缓存数组。

class Conn
   {
       public const int BufferSize = 1024;
       private Socket m_Socket;
       private bool m_IsUse;
       private byte[] m_ReadBuff;
       private int m_BuffCount = 0;

       public Socket Socket => m_Socket;
       public bool IsUse => m_IsUse;
       public byte[] ReadBuff => m_ReadBuff;
       public Conn()
       {
           m_ReadBuff = new byte[BufferSize];
       }

       public void Init(Socket socket)
       {
           m_Socket = socket;
           m_IsUse = true;
       }

       public string GetAddress()
       {
           if (!m_IsUse)
           {
               return "无法获取地址";
           }

           return m_Socket.RemoteEndPoint.ToString();
       }

       public void Close()
       {
           if (!m_IsUse) return;
           Console.WriteLine($"[ 断开连接 ] {GetAddress()}");
           m_Socket.Close();
           m_IsUse = false;
       }
   }

服务端,这里是收到什么就向客户端回复什么,如果有什么不清楚可参考Unity3D C# Socket通信详解之基础介绍

class Serv
    {
        private const int MaxConn = 50;

        private Socket m_ListenSocket;
        private Conn[] m_Conns;

        public int NewIndex()
        {
            if (m_Conns == null)
            {
                return -1;
            }

            for (int i = 0; i < m_Conns.Length; i++)
            {
                if (m_Conns[i] == null)
                {
                    m_Conns[i] = new Conn();
                    return i;
                }

                if (!m_Conns[i].IsUse)
                {
                    return i;
                }
            }

            return -1;
        }

        public void Start(string host, int port)
        {
            m_Conns = new Conn[MaxConn];
            for (int i = 0; i < MaxConn; i++)
            {
                m_Conns[i] = new Conn();
            }

            m_ListenSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            IPAddress ipAddress = IPAddress.Parse(host);
            IPEndPoint ipEndPoint = new IPEndPoint(ipAddress, port);
            m_ListenSocket.Bind(ipEndPoint);
            m_ListenSocket.Listen(MaxConn);
            m_ListenSocket.BeginAccept(AcceptCb, m_ListenSocket);
            Console.WriteLine("服务器启动...");
        }

        private void AcceptCb(IAsyncResult ar)
        {
            try
            {
                Socket socket = m_ListenSocket.EndAccept(ar);
                int index = NewIndex();
                if (index < 0)
                {
                    socket.Close();
                    Console.WriteLine("[ 警告 ] 连接已满");
                }
                else
                {
                    Conn conn = m_Conns[index];
                    conn.Init(socket);
                    string addr = conn.GetAddress();
                    Console.WriteLine($"客户端连接 [ {addr} ]");
                    // 开始接收数据
                    conn.Socket.BeginReceive(conn.ReadBuff, 0, Conn.BufferSize, SocketFlags.None, ReceiveCb, conn);
                }

                // 继续等待其他连接
                m_ListenSocket.BeginAccept(AcceptCb, m_ListenSocket);
            }
            catch (Exception e)
            {
                Console.WriteLine($"AcceptCb 失败: {e.Message}");
            }
        }

        private void ReceiveCb(IAsyncResult ar)
        {
            Conn conn = (Conn)ar.AsyncState;

            try
            {
                int cnt = conn.Socket.EndReceive(ar);
                if (cnt <= 0)
                {
                    Console.WriteLine($"收到 [ {conn.GetAddress()} ] 断开连接");
                    conn.Close();
                    return;
                }

                string msg = Encoding.Default.GetString(conn.ReadBuff, 0, cnt);
                Console.WriteLine($"收到 [ {conn.GetAddress()} ] 数据: {msg}");

                // 收到什么回复什么
                Send(conn, msg);

                // 继续接收
                conn.Socket.BeginReceive(conn.ReadBuff, 0, Conn.BufferSize, SocketFlags.None, ReceiveCb, conn);
            }
            catch (Exception e)
            {
                Console.WriteLine($"收到 [ {conn.GetAddress()} ] 断开连接");
                conn.Socket.Close();
            }
        }

        private void Send(Conn conn, string msg)
        {
            Socket socket = conn.Socket;
            byte[] msgBytes = Encoding.Default.GetBytes(msg);
            socket.BeginSend(msgBytes, 0, msgBytes.Length, SocketFlags.None, SendCb, conn);
        }

        private void SendCb(IAsyncResult ar)
        {
            Conn conn = (Conn) ar.AsyncState;
            Socket socket = conn.Socket;
            try
            {
                socket.EndSend(ar);
            }
            catch (Exception e)
            {
                Console.WriteLine($"收到 [ {conn.GetAddress()} ] 断开连接");
                conn.Close();
            }
        }
    }

项目工程放到这儿了。提取码:dtry
ps:项目中的vs是vs2017,低版本的vs有些语法不支持会报错。
连接池项目