异步

客户端

public class AsyncClient : MonoBehaviour  
{  
    //UGUI  
    public InputField InputFeld;  
    public Button connect;  
    public Button send;  
  
    //定义套接字  
    private Socket socket;  
    //接收缓冲区  
    private byte[] readBuff = new byte[1024];  
    private string recvStr = "";  
    private string sendStr;  
  
    private void Start()  
    {  
        connect.onClick.AddListener(Connetion);  
        send.onClick.AddListener(Send);  
    }  
  
    private void Connetion()  
    {  
        socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);  
        socket.BeginConnect("127.0.0.1", 8888, ConnectCallback, socket);  
    }  
  
    private void ConnectCallback(IAsyncResult ar)  
    {  
        var client = ar.AsyncState as Socket;  
        client.EndConnect(ar);  
        Debug.Log("Socket Connect Succ ");  
        client.BeginReceive( readBuff, 0, 1024, 0, ReceiveCallback, client);  
    }  
  
    private void ReceiveCallback(IAsyncResult ar)  
    {  
        var client = ar.AsyncState as Socket;  
        int count = client.EndReceive(ar);  
        recvStr = System.Text.Encoding.UTF8.GetString(readBuff, 0, count);  
        Debug.Log(recvStr);  
        client.BeginReceive( readBuff, 0, 1024, 0, ReceiveCallback, client);  
    }  
  
    private void Send()  
    {  
        //Send  
        sendStr = InputFeld.text;  
        byte[] sendBytes = System.Text.Encoding.Default.GetBytes(sendStr);  
        socket.BeginSend(sendBytes, 0, sendBytes.Length, 0, SendCallback, socket);  
    }  
  
    private void SendCallback(IAsyncResult ar)  
    {  
        var client = ar.AsyncState as Socket;  
        client.EndSend(ar);  
    }  
}

Socket的异步方法中,BeginXXXEndXXX总是成对的出现

Socket.BeginConnect ( IP , Port , CallBack , State) 等待连接

  1. void Func(IAsyncResult): CallBack 回调函数
  2. Object: State 一个想要传入的对象数据,会被放入IAsyncResult.State中

Socket.BeginReceive( Buffer , Offset , Size , SocketFlags , CallBack , State) 等待接受数据

  1. byte[]: Buffer 用于存储数据
  2. int: Offset 偏移量,正常就是0
  3. int: Size 最大可接受字节数
  4. SocketFLags: SocketFlags 一些标志,这里设置为0
    • SocketFlags.None

Socket发送缓冲区

每一个Socket内部都有一个发送数据缓冲区,默认为8KB,还没有被服务器确认的数据会留在这里,当缓冲区满时,就会阻塞线程,直到部分数据被确认完之后腾出空间

同样的也有一个接受缓冲区,道理一致

服务端

  1. 等待请求
  2. 处理消息

ClientState

一个简单的数据结构,记录所有通讯的socket

public class Server : MonoBehaviour  
{  
    public class ClientState  
    {  
        public Socket socket;  
        public byte[] buff = new byte[1024];  
    }  
  
    private Dictionary<Socket, ClientState> clients = new Dictionary<Socket, ClientState>();  
}

等待请求

public class Server : MonoBehaviour  
{  
    private Socket socket;  
    private IPAddress ipAdr;  
    private IPEndPoint ipEp;  
  
    private void Start()  
    {  
        socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);  
        ipAdr = IPAddress.Parse("127.0.0.1");  
        ipEp = new IPEndPoint(ipAdr, 8888);  
        socket.Bind(ipEp);  
        socket.Listen(0);  
  
  		// 开始等待Socket请求
        socket.BeginAccept(AcceptCallback, socket);  
    }  
      
    private void AcceptCallback(IAsyncResult ar)  
    {  
        try 
		{  
            var server = (Socket) ar.AsyncState;  
            var handle = server.EndAccept(ar);  
  
            if (clients.TryGetValue(handle, out var state))   
            {  
                state.socket = handle;  
            }  
            else  
            {  
                state = new ClientState();  
                state.socket = handle;  
                clients.Add(handle, state);  
            }  
  
  			// 开始处理消息
            handle.BeginReceive(state.buff, 0, 1024, 0, ReceiveCallback, state);  
			// 等待下一次请求
            server.BeginAccept (AcceptCallback, server);  
        }  
        catch (SocketException ex)  
        {  
            Console.WriteLine("Socket Accept fail" + ex.ToString());  
        }  
    }  
}

每一次连接请求,都会触发Accept(哪怕是同一台电脑),而handle就是客户端的Socket连接

处理消息

public class Server : MonoBehaviour  
{  
    private void ReceiveCallback(IAsyncResult ar)  
    {  
        try 
		{  
            var state = (ClientState) ar.AsyncState;  
            var handle = state.socket;  
            int count = handle.EndReceive(ar);  
            //客户端关闭  
            if(count == 0)  
            {  
                handle.Close();  
                clients.Remove(handle);  
                Console.WriteLine("Socket Close");  
                return;  
            }  
            string recvStr = System.Text.Encoding.Default.GetString(state.buff, 0, count);  
            Debug.Log("客户端发送数据:" + recvStr);  
            byte[] sendBytes = System.Text.Encoding.Default.GetBytes("服务器发送数据:" + recvStr);  
            handle.Send(sendBytes);  
            handle.BeginReceive( state.buff, 0, 1024, 0, ReceiveCallback, state);  
        }  
        catch (SocketException ex)  
        {   
            Console.WriteLine("Socket Receive fail" + ex.ToString());  
        }  
    }  
}

测试一下,左边是服务端,右边是客户端

聊天室

客户端

public class ChatClient : MonoBehaviour
{
  	//定义套接字
    private Socket socket;
    
    //接收缓冲区
    private bool isChange = false;
    private byte[] buffer = new byte[1024];
    private StringBuilder sb = new StringBuilder();

    private void Start()
    {
        connect.onClick.AddListener(Connetion);
        send.onClick.AddListener(Send);
    }

    private void Connetion()
    {
        socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        socket.BeginConnect("127.0.0.1", 8888, ConnectCallback, socket);
    }

    private void ConnectCallback(IAsyncResult ar)
    {
        var client = ar.AsyncState as Socket;
        client.EndConnect(ar);
        SendMsg("客户端1 加入聊天");
        client.BeginReceive( buffer, 0, 1024, 0, ReceiveCallback, client);
    }

    // ============ 接受数据 ============
    private void ReceiveCallback(IAsyncResult ar)
    {
        var client = ar.AsyncState as Socket;
        int count = client.EndReceive(ar);
        AddMsg(Encoding.UTF8.GetString(buffer, 0, count));
        client.BeginReceive( buffer, 0, 1024, 0, ReceiveCallback, client);
    }

    private void AddMsg(string msg)
    {
        sb.Append($"{msg}\n");
        isChange = true;
    }

    private void Update()
    {
        if (isChange)
        {
            text.text = sb.ToString();
            isChange = false;
        }
    }
    
    // ============ 发送数据 ============
    private void Send()
    {
        SendMsg(inputFeld.text);
    }

    private void SendMsg(string msg)
    {
        byte[] sendBytes = System.Text.Encoding.Default.GetBytes(msg);
        socket.BeginSend(sendBytes, 0, sendBytes.Length, 0, SendCallback, socket);
    }

    private void SendCallback(IAsyncResult ar)
    {
        var client = ar.AsyncState as Socket;
        client.EndSend(ar);
    }
}

服务端

服务端的程序同上

测试

客户端1:

image-20220821141619334

客户端2:

image-20220821141628915

服务端:

image-20220821141642920

Poll状态监测

异步程序很好,但可能引发很多问题,比如UI不能再子线程中操作UI

相比之下同步代码就简单安全,问题是同步代码会阻塞线程,我们不可能接受他卡在那里

对于接受数据而言,Receive就是一个阻塞方法,但实际上我们不需要一直等待Reveice,只要socket有数据时再去Receive,就不会一直卡主了

if(socket.有数据)
{
	socket.receive    
}

那么如何检查socket是否有数据呢,微软提供了一个Poll方法

public bool Pool (int microSeconds,SelectMode mode)
  • int : 等待响应时间,以微秒为单位,-1为永远等待,0为不等待
  • SelectMode
    • SelectRead : 判断socket是否可读
    • SelectWrite : 判断socket是否可写
    • SelectError : 判断是否有错误

修改客户端

其他不用修改,只需要更改一下Update

private void Update()
{
    if (socket == null)
    {
        return;
    }

    if (socket.Poll(0, SelectMode.SelectRead))
    {
        byte[] readBuff = new byte[1024];
        int count = socket.Receive(readBuff);
        string recvStr = Encoding.Default.GetString(readBuff, 0, count);
        sb.Append($"{recvStr}\n");
        text.text = sb.ToString();
    }
}

很简单,就在Update里不断判断socket是否有数据,有数据就开始读取

修改服务端

类似客户端,之前我们服务端是写在Unity生命周期里的,但实际上服务器肯定不可能运行一个Unity,我们直接改成C#代码

首先还是一个数据类,用于存储客户端的Socket以及一个存储发送数据的字节数组

namespace Server
{
    public class ClientState
    {
        public Socket socket;
        public byte[] buff = new byte[1024];
    }
}

然后是正式的服务类,首先是Init的方法,我们先创建服务端Socket,只有用一个while循环不断检测是否有连接请求

namespace Server
{
	internal class Server : IDisposable
    {
        // IP地址
        private const string IP = "127.0.0.1";
        // 端口号
        private const int HOST = 8888;

        // 服务器Socket
        private Socket server;
        // 客户端Socket及状态信息
        private Dictionary<Socket, ClientState> clients = new Dictionary<Socket, ClientState>();

        public void Init()
        {
            //Socket
            server = new Socket(AddressFamily.InterNetwork,SocketType.Stream, ProtocolType.Tcp);
            //Bind
            var ipAdr = IPAddress.Parse(IP);
            var ipEp = new IPEndPoint(ipAdr, HOST);
            server.Bind(ipEp);
            //Listen
            server.Listen(0);
            Console.WriteLine("[服务器]启动成功");
            
            //主循环
            while (true)
            {
                //检查listenfd
                if (server.Poll(0, SelectMode.SelectRead))
                {
                    AcceptClient(server);
                }
                //检查clientfd
                foreach (var state in clients.Values)
                {
                    var client = state.socket;
                    if (client.Poll(0, SelectMode.SelectRead))
                    {
                        if (!ReadClientfd(client))
                        {
                            break;
                        }
                    }
                }
                //防止CPU占用过高
                System.Threading.Thread.Sleep(1);
            }
        }

        public void Dispose()
        {
            if (server != null)
            {
                Console.WriteLine("[服务器]关闭");
                server.Dispose();
            }
        }
    }
}

如果有连接请求,那么将连接到的客户端程序放入字典中

namespace Server
{
	internal class Server : IDisposable
    {
        //读取Listenfd
        public void AcceptClient(Socket server)
        {
            Console.WriteLine("Accept");
            var clientfd = server.Accept();
            var state = new ClientState();
            state.socket = clientfd;
            clients.Add(clientfd, state);
        }
    }
}

然后依次判断每个客户端是否有数据发送,最后广播

namespace Server
{
	internal class Server : IDisposable
    {
        //读取Clientfd
        public bool ReadClientfd(Socket clientfd)
        {
            ClientState state = clients[clientfd];
            //接收
            int count = 0;
            try
            {
                count = clientfd.Receive(state.buff);
            }
            catch (SocketException ex)
            {
                clientfd.Close();
                clients.Remove(clientfd);
                Console.WriteLine("Receive SocketException " + ex.ToString());
                return false;
            }
            //客户端关闭
            if (count == 0)
            {
                clientfd.Close();
                clients.Remove(clientfd);
                Console.WriteLine("Socket Close");
                return false;
            }
            //广播
            string recvStr = Encoding.UTF8.GetString(state.buff, 0, count);
            Console.WriteLine("Receive" + recvStr);
            string sendStr = clientfd.RemoteEndPoint.ToString() + ":" + recvStr;
            byte[] sendBytes = Encoding.UTF8.GetBytes(sendStr);
            foreach (ClientState cs in clients.Values)
            {
                cs.socket.Send(sendBytes);
            }
            return true;
        }
    }
}

image-20220822002646707

Select 多路复用

Poll很好的解决了异步编程的一些问题,但把逻辑放在while里,会造成CPU的很大消耗,于是又出现了Select,可以一次性检测多个Socket的状态

public static void Select(
    IList checkRead,
    IList check Write,
    IList checkError,
    int microSeconds
)

很好理解,传入一个socket数组,一次性检测所有符合要求的socket

服务端

服务端需要做出一些修改,一次性检测所有客户端,主要体现在while里

namespace Server
{
    internal class Server : IDisposable
    {
        // 多路复用
        List<Socket> checkRead = new List<Socket>();

        public void Init()

            //主循环
            while (true)
            {
                //填充checkRead列表
                checkRead.Clear();
                checkRead.Add(server);
                foreach (var state in clients.Values)
                {
                    checkRead.Add(state.socket);
                }
                //select
                Socket.Select(checkRead, null, null, 1000);
                //检查可读对象
                foreach (var socket in checkRead)
                {
                    if (socket == server)
                    {
                        AcceptClient(socket);
                    }
                    else
                    {
                        ReadClientfd(socket);
                    }
                }
            }
        }
    }
}

客户端

客户端因为只连接了一个服务器,所以无须修改了

测试

image-20220822003549689