Unity中使用Socket进行Tcp连接-多路复用、解决粘包(三)

上一篇文章将网络连接改为异步方式,但仍有不足之处。

异步实际上属于多线程的操作,这可能会造成线程问题,Select多路复用的方式可以同时监听多个客户端Socket列表,如果有可读的socket则返回,否则阻塞,这样不会造成线程问题,且不会造成CPU占用过高。

服务器多路复用Select:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net.Sockets;
using System.Net;
using System.Reflection;
using UnityEngine;
namespace Server
{
    class ClientState
    {
        public Socket socket;
        public Byte[] readBuff = new Byte[1024];
        public int buffCount;
    }
    public static class ServerManager
    {        
        static Socket listenfd;
        static Dictionary clients = new Dictionary();

        public static void InitServer(string ip,string port)
        {
            listenfd = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

            IPEndPoint ipEp;
            if (!string.IsNullOrEmpty(ip))
            {
                IPAddress ipAdr = IPAddress.Parse(ip);
                ipEp =  new IPEndPoint(ipAdr, int.Parse(port));
            } else
            {
                ipEp = new IPEndPoint(IPAddress.Any, int.Parse(port));
            }
           
            listenfd.Bind(ipEp);
            listenfd.Listen(0);
            Debug.Log("Server Ready");

            List checkRead = new List();
            while (true)
            {
                checkRead.Clear();
                checkRead.Add(listenfd);
                foreach (ClientState cs in clients.Values)
                {
                    checkRead.Add(cs.socket);
                }
                Socket.Select(checkRead, null, null, 1000);//多路复用,检测是否有可读或可写
                foreach (Socket s in checkRead)
                {
                    if (s == listenfd)
                    {
                        ReadListenfd(s);
                    }
                    else
                    {
                        ReadClientfd(s);
                    }
                }
            }
        }
        static void ReadListenfd(Socket listenfd)
        {
            Debug.Log("accept client");
            Socket clientfd = listenfd.Accept();
            ClientState state = new ClientState();
            state.socket = clientfd;
            clients.Add(clientfd, state);
        }
        static bool ReadClientfd(Socket client)
        {
            ClientState state = clients[client];
            int count;
            try
            {
                count = client.Receive(state.readBuff, state.buffCount, 1024 - state.buffCount, 0);
                state.buffCount += count;
            }
            catch (SocketException ex)
            {              
                client.Close();
                clients.Remove(client);
                Debug.Log("client receive fail");
                return false;
            }
            if (count == 0)
            {
                client.Close();
                clients.Remove(client);
                Debug.Log("client receive fail");
                return false;
            }
            OnReceiveData(state);
            return true;
        }
        static void OnReceiveData(ClientState state)
        {
            Socket client = state.socket;
            int buffCount = state.buffCount;
            if (buffCount < 2)
            {
                return;
            }
            int bodyLength = BitConverter.ToInt16(state.readBuff, 0);//从第一位开始,取前两个
            if (buffCount < 2 + bodyLength)
            {
                return;
            }
            string recStr = Encoding.Default.GetString(state.readBuff, 2, bodyLength);
            Debug.Log("收到消息:"+ recStr);           
            foreach (ClientState s in clients.Values)
            {
                byte[] sendBytes = Encoding.Default.GetBytes(recStr);
                s.socket.Send(sendBytes);
                Debug.Log("向客户端发送" + recStr);
            }
            //继续处理剩余字节
            int start = 2 + bodyLength;
            int count = buffCount - start;
            Array.Copy(state.readBuff, start, state.readBuff, 0, count);
            state.buffCount -= start;
            OnReceiveData(state);
        }
    }
}

Socket.Select(checkRead, null, null, 1000);//多路复用,检测是否有可读或可写

Select方式,对socket列表进行监听,没有可读的socket时会阻塞。

ReadListenfd函数负责监听新连接进来的客户端,ReadClientfd负责接收客户端发来的消息。

在上述代码中可以看到对ClientState类新增了buffCount字段,该字段记录了收到客户端传来消息的有效长度,这是为了解决粘包问题的改进。在接收数据时可能产生粘包问题,常规的解决办法有固定长度法,即每次发送协议内容长度固定,还有在发送的信息前标明长度,本文主要介绍在发送的协议头部增加主体消息长度的方式。

在原始的客户端发送协议前拼接该协议长度,   消息长度+消息,以此方式组装成新的协议发送,服务器收到消息后解析消息长度,若收到的消息长度不够,则等待接收到新的消息后在解析。

 count = client.Receive(state.readBuff, state.buffCount, 1024 - state.buffCount, 0);
 state.buffCount += count;

上述代码记录收到消息的长度。

static void OnReceiveData(ClientState state)
        {
            Socket client = state.socket;
            int buffCount = state.buffCount;
            if (buffCount < 2)//消息长度占两个位,若收到消息小于2,则没有收到完整消息
            {
                return;
            }
            int bodyLength = BitConverter.ToInt16(state.readBuff, 0);//从[0]位去两位(ToInt16),获得主体消息长度
            if (buffCount < 2 + bodyLength)
            {
                return;
            }
            string recStr = Encoding.Default.GetString(state.readBuff, 2, bodyLength);
            Debug.Log("收到消息:"+ recStr);           
            foreach (ClientState s in clients.Values)
            {
                byte[] sendBytes = Encoding.Default.GetBytes(recStr);
                s.socket.Send(sendBytes);
                Debug.Log("向客户端发送" + recStr);
            }
            //继续处理剩余字节
            int start = 2 + bodyLength;
            int count = buffCount - start;
            Array.Copy(state.readBuff, start, state.readBuff, 0, count);//将后面的数据前移
            state.buffCount -= start;
            OnReceiveData(state);
        }

上述代码为解析收到的消息,若收到的数据长度小于2,或小于消息长度,则收到消息不完整,等待下次处理。

本文介绍了多路复用以及收到消息粘包问题的解决,但除了收到消息的粘包问题,发送消息也会有不完成的情况发生,下一篇将会介绍。

你可能感兴趣的:(游戏网络通信Unity)