[begin] 在 [/begin]第一章的学习内容中我们已经完成了基本的 Socket 通信,但是在第一章中我们只能处理一个客户端的消息。
在实际游戏中,我们服务器需要对多个客户端进行监听和处理。

Begin XXX

每一个同步 API 对应着两个异步 API,分别是在原名称前面加上 Begin 和 End(如 BeginConnect 和 EndConnect)。使用异步程序可以防止程序卡住 。

例如:BenginConnect

参数说明
host远程主机ip地址
port端口号
requestCallback异步回调函数,其函数必须包含一个实现 IAsynResult的对象
state一个自定义对象,此对象会被传递给回调函数

EndConnect

参数说明
asyncResult接收一个异步结果对象

其他的都大致相同
[toc]

客户端

客户端使用 begin 和 end,修改为:

using System;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using UnityEngine;
using UnityEngine.UI;
public class Echo : MonoBehaviour
{
    public Button linkBtn;
    public Button sendBtn;
    public InputField inputField;
    public Text infoText;
    //套接字
    private Socket _socket;
    //接收缓冲区
    private byte[] _readBuff=new byte[1024];
    private string _recvStr=string.Empty;



    private void Start()
    {
        linkBtn.onClick.AddListener(Connection);
        sendBtn.onClick.AddListener(SendMessage);
        //Timer线程计时器
        var timer = new Timer((state) => { print("时间到"); },null,5000,0);
    }
    
    private void Update()
    {
        infoText.text = "接收到来自服务器的数据:"+_recvStr;
    }
    
    /// 
    /// 连接
    /// 
    void Connection()
    {
        _socket=new Socket(SocketType.Stream,ProtocolType.Tcp);
        //异步进行连接
        _socket.BeginConnect("127.0.0.1", 8888, ConnectCallback, _socket);
        Debug.Log("开始连接...");
        // _socket.Connect("127.0.0.1",8888);
    }
    /// 
    /// 连接回调
    /// 
    /// 
    void ConnectCallback(IAsyncResult ar)
    {
        try
        {
            var socket = ar.AsyncState as Socket;
            socket?.EndConnect(ar);    //连接完毕
            Debug.Log("连接成功!");
            //开始监听服务器端回复
            socket?.BeginReceive(_readBuff, 0, _readBuff.Length, 0, ReceiveCallback, socket);
        }
        catch (SocketException e)
        {
            Debug.Log("Socket连接错误:"+e);
            throw;
        }
    }


    /// 
    /// 接收回调
    /// 
    /// 
    void ReceiveCallback(IAsyncResult ar)
    {
        //ps:作者在这里提到,假如在send缓冲区的数据量太多,并且删除服务端receive相关的内容,使这些数据不能得到及时的释放,这时候客户端就会卡住。
        //所以我们也需要对send进行异步处理,防止堵塞。
        try
        {
            var socket = ar.AsyncState as Socket;
            var count = socket.EndReceive(ar);
            //注意在unity中,UI对象只能在主线程中更新,由于异步回调是由其他线程更新的,所以我只在这儿对_recvStr进行记录
            //并在主线程update中对UI对象进行更新。
            _recvStr = Encoding.UTF8.GetString(_readBuff, 0, count);
    
            socket.BeginReceive(_readBuff, 0, _readBuff.Length, 0, ReceiveCallback, socket);
        }
        catch (SocketException e)
        {
            Debug.Log("Socket错误:"+e);
        }
    }
    
    /// 
    /// 发送消息
    /// 
    void SendMessage()
    {
        var sendStr = inputField.text;
        var sendBytes = System.Text.Encoding.UTF8.GetBytes(sendStr);
        // _socket.Send(sendBytes);
        /*
         * 值得注意的是,send方法的过程只是将数据写入到缓冲区,由操作系统进行重传和确认。
         * send成功只表示,数据成功吸入缓冲区,并不代表对方已经收到数据。
         */
        _socket.BeginSend(sendBytes, 0, sendBytes.Length, 0, SendCallback, _socket);
    
    }
    
    /// 
    /// Send回调
    /// 
    /// 
    void SendCallback(IAsyncResult ar)
    {
        try
        {
            var socket = ar.AsyncState as Socket;
            var count = socket?.EndSend(ar);
            /*
             * 在这里作者提到,一般来说收发的数据长度应当保持一致,但是当我们的数据大小超过缓冲区的大小时,我们需要再次调用相关的send或reivce方法
             * 发送或接收剩余的数据。(在后面的章节再介绍)
             */
            Debug.Log("socket 发送成功 发送字节数:"+count);
        }
        catch (SocketException e)
        {
           Debug.Log("Socket Send 错误:"+e);
        }
    }
}

服务器

在上一章中,我们的服务器使用单线程的阻塞,也就是说每次只能处理一个客户端的请求,现在我们也将服务器改为多线程模式,让他可以同时处理多个线程。(多个客户端请求)
其代码实现如下:

创建一个clientState类存储客户端状态

using System.Net.Sockets;

namespace netWork_Server
{
    /// 
    /// 连接状态
    /// 
    public class ClientState
    {
        public Socket Socket { get; set; }
        public byte[] ReadBuff { get; set; }

        public ClientState()
        {
            ReadBuff=new byte[1024];
        }
    }
}

主代码

using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Text;

namespace netWork_Server
{


    internal class Program
    {
        private static Socket socket;
        static  Dictionary _clientStates=new Dictionary();
    
        public static void Main(string[] args)
        {


            Console.WriteLine("Hi");
    
            socket=new Socket(AddressFamily.InterNetwork,SocketType.Stream,ProtocolType.Tcp);
            IPAddress ipAddress = IPAddress.Parse("127.0.0.1");
            IPEndPoint ipEndPoint=new IPEndPoint(ipAddress,8888);
            socket.Bind(ipEndPoint);
            //listen
            socket.Listen(0);
            Console.WriteLine("[服务器] 启动成功");
            //Accept
            //当客户端连接上服务器时,调用AcceptCallback
            socket.BeginAccept(AcceptCallback,socket);
            Console.ReadLine();
        }
    
        /// 
        /// Accept回调
        /// 
        /// 
        private static void AcceptCallback(IAsyncResult ar)
        {
            try
            {
                Console.WriteLine("[服务器] 连接到新客户端");
                Socket listenfd=ar.AsyncState as Socket;
                Socket clientfd = listenfd.EndAccept(ar);
                ClientState state=new ClientState();
                state.Socket = clientfd;
                _clientStates.Add(clientfd,state);
                //进行Receive
                clientfd.BeginReceive(state.ReadBuff, 0, 1024, 0,ReceiveCallback, state);
                //继续Accept
                listenfd.BeginAccept(AcceptCallback, listenfd);
            }
            catch (SocketException e)
            {
                Console.WriteLine("Socket Accept错误:"+e);
                throw;
            }
    
        }
    
        /// 
        /// Receive回调
        /// 
        /// 
        static void ReceiveCallback(IAsyncResult ar)
        {
            try
            {
                var state = ar.AsyncState as ClientState;
                var clientfd = state.Socket;
                int count = clientfd.EndReceive(ar);
                //关闭客户端
                if (count==0)
                {
                    clientfd.Close();
                    _clientStates.Remove(clientfd);
                    Console.WriteLine("Socket Close!");
                    return;
                }
    
                string recvStr = Encoding.UTF8.GetString(state.ReadBuff,0,count);
                Console.WriteLine("[客户端]"+recvStr);
                byte[] sendBytes = Encoding.UTF8.GetBytes("echo" + recvStr);
    
                clientfd.Send(sendBytes);
                //继续获取数据
                clientfd.BeginReceive(state.ReadBuff, 0, 1024, 0, ReceiveCallback, state);
    
            }
            catch (SocketException e)
            {
                Console.WriteLine("Socket Receive 错误:"+e);
            }
        }
    }
}

进行会话

我们将客户端生成一份,让生成的客户端和编辑器客户端同时与服务器连接并进行会话操作。
其效果如图所示:
!{服务器端}(https://i.loli.net/2020/01/14/kVrQIWpejXBRman.png)
!{客户端}(https://i.loli.net/2020/01/14/JdbaRLpt5k1hogs.png)

结语

这是这一章的前半部分,完成了在 socket 下的多线程模式,其中在字符编码部分,原书使用了 Default,这导致可能没法解析中文字符,所以我将其改为了固定的 UTF8 做解析。
感谢您读完这篇文章,在本章的下半部分将实现一个聊天室。