《Unity3D网络游戏实战》学习与实践

纸上得来终觉浅,绝知此事要躬行~

Echo

网络上的两个程序通过一个双向的通信连接实现数据交换,这个连接的一端称为一个Socket

端口”是英文port的意译,是设备与外界通信交流的出口。每台计算机可以分配0到65535共65536个端口

每一条Socket连接代表着本地Socket→本地端口→网络介质→远程端口→远程Socket的链路

Socket通信的基本流程

  • 开启一个连接之前,需要创建一个Socket对象(使用API Socket)​,然后绑定本地使用的端口(使用API Bind)​。对客户端而言,连接时(使用API Connect)会由系统分配端口,可以省去绑定步骤。
  • 对客户端而言,连接时(使用API Connect)会由系统分配端口,可以省去绑定步骤。
  • 客户端连接服务器(使用API Connect)
  • 服务器接受连接(使用API Accept)
  • 客户端和服务端通过Send和Receive等API收发数据,操作系统会自动完成数据的确认、重传等步骤,确保传输的数据准确无误。
  • 某一方关闭连接(使用API Close)​,操作系统会执行“四次挥手”的步骤,关闭双方连接
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    using System.Net.Sockets;
    using UnityEngine.UI;
    public class Echo : MonoBehaviour {
        //定义套接字
        Socket socket;
        //UGUI
        public InputField InputFeld;
        public Text text;

        //点击连接按钮
        //客户端通过socket.Connect(远程IP地址,远程端口)连接服务端。Connect是一个阻塞方法,程        
        //序会卡住直到服务端回应(接收、拒绝或超时)​。
        public void Connection()
        {
            //Socket
            socket = new Socket(AddressFamily.InterNetwork,
                SocketType.Stream, ProtocolType.Tcp);
            //这一行用于创建一个Socket对象,它的三个参数分别代表地址族、套接字类型和协议。
            //Connect
            socket.Connect("127.0.0.1", 8888);
        }

        //点击发送按钮
        //客户端通过socket.Send发送数据,这也是一个阻塞方法。该方法接受一个byte[​]类型的参数指明    
        //要发送的内容。Send的返回值指明发送数据的长度(例子中没有使用)​。程序用 
        //System.Text.Encoding.Default.GetBytes(字符串)把字符串转换成byte[​]数组,然后发送给服 
        //务端。
        public void Send()
        {
            //Send
            string sendStr = InputFeld.text;
            byte[] sendBytes = System.Text.Encoding.Default.GetBytes(sendStr);
            socket.Send(sendBytes);
            //Recv
            byte[] readBuff = new byte[1024];
            //客户端通过socket.Receive接收服务端数据。Receive也是阻塞方法,没有收到服务端数据 
            //时,程序将卡在Receive不会往下执行。Receive带有一个byte[​]类型的参数,它存储接收到 
            //的数据。Receive的返回值指明接收到数据的长度。之后使用System.Text.Encoding. 
            //Default.GetString(readBuff,0, count)将byte[​]数组转换成字符串显示在屏幕上。
            int count = socket.Receive(readBuff);
            string recvStr = System.Text.Encoding.Default.GetString(readBuff, 0, count);
            text.text = recvStr;
            //Close
            socket.Close();
        }
    }

此时运行游戏点击连接会出现

因为我们还没有启动服务器,所以属于正常现象

创建服务端程序

using System.Net;
using System.Net.Sockets;

internal class Program
{
    private static void Main(string[] args)
    {
        Console.WriteLine("Hello, World!");
        //Socket
        Socket listenfd = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        IPAddress ipAdr = IPAddress.Parse("127.0.0.1");
        IPEndPoint ipEp = new IPEndPoint(ipAdr, 8888);
        listenfd.Bind(ipEp);
        //Listen
        listenfd.Listen(0);
        Console.WriteLine("[服务器]启动成功");
        while (true)
        {
            //Accept
            Socket connfd = listenfd.Accept();
            Console.WriteLine("[服务器]Accept");
            //Receive
            byte[] readBuff = new byte[1024];
            int count = connfd.Receive(readBuff);
            string readStr = System.Text.Encoding.Default.GetString(readBuff, 0, count);
            Console.WriteLine("[服务器接受]" + readStr);
            //Send
            byte[] sendBytes = System.Text.Encoding.Default.GetBytes(readStr);
            connfd.Send(sendBytes);
        }
    }
}

绑定 Bind

listenfd.Bind(ipEp)将给listenfd套接字绑定IP和端口。程序中绑定本地地址“127.0.0.1”和8888号端口。127.0.0.1是回送地址,指本地机,一般用于测试。读者也可以设置成真实的IP地址,然后在两台计算机上分别运行客户端和服务端程序

监听 Listen

服务端通过listenfd.Listen(backlog)开启监听,等待客户端连接。参数backlog指定队列中最多可容纳等待接受的连接数,0表示不限制。

应答 Accept

开启监听后,服务器调用listenfd.Accept()接收客户端连接。本例使用的所有Socket方法都是阻塞方法,也就是说当没有客户端连接时,服务器程序卡在listenfd.Accept()不会往下执行,直到接收了客户端的连接。Accept返回一个新客户端的Socket对象,对于服务器来说,它有一个监听Socket(例子中的listenfd)用来监听(Listen)和应答(Accept)客户端的连接,对每个客户端还有一个专门的Socket(例子中的connfd)用来处理该客户端的数据。

IPAddress 和 IPEndPoint

使用IPAddress指定IP地址,使用IPEndPoint指定IP和端口。

System.Text.Encoding.Default.GetString

Receive方法将接收到的字节流保存到readBuff上,readBuff是byte型数组。GetString方法可以将byte型数组转换成字符串。同理,System.Text.Encoding.Default.GetBytes可以将字符串转换成byte型数组。

测试 :

Socket类的一些常用方法

公网和局域网

把宽带连接到家里的路由器,再由路由器分发到多台计算机(校园网、公司局域网同理)​,在这种情况下,路由器会有公网和局域网两个IP

比如:路由器的公网IP是123.207.111.220,局域网IP为192.168.0.1,连接路由器的计算机只有内网IP,它们分别是192.168.0.10和192.168.0.12。。如果将服务端放到连接路由器的某台计算机上,因为它只有局域网IP,所以只有局域网内的计算机可以连接上。如果拥有路由器的控制权,可以使用一种叫“端口映射”的技术,即设置路由器,将路由器IP地址的一个端口映射到内网中的一台计算机,提供相应的服务。当用户访问该IP的这个端口时,路由器自动将请求映射到对应局域网内部的计算机上

异步和多路复用

上面的程序全部使用阻塞API(Connect、Send、Receive等)​,可称为同步Socket程序

一个简单的异步程序示例:

        using System.Collections;
        using System.Collections.Generic;
        using UnityEngine;
        using System.Threading;

        public class Async : MonoBehaviour {
            // Use this for initialization
            void Start () {
                //创建定时器
                Timer timer = new Timer(TimeOut, null, 5000, 0);
                //其他程序代码
                //……
            }

            //回调函数
            private void TimeOut(System.Object state){
                Debug.Log("铃铃铃");
            }
        }

异步Connect

每一个同步API(如Connect)对应着两个异步API,分别是在原名称前面加上Begin和End(如BeginConnect和EndConnect)

BeginConnect的函数原型如下:

public IAsyncResult BeginConnect( string host, int port, AsyncCallback requestCallback, object state )

修改代码:

        using System;

        //点击连接按钮
        public void Connection()
        {
            //Socket
            socket = new Socket(AddressFamily.InterNetwork,
                SocketType.Stream, ProtocolType.Tcp);
            //Connect
            socket.BeginConnect("127.0.0.1", 8888, ConnectCallback, socket);
        }

        //Connect回调
        public void ConnectCallback(IAsyncResult ar){
            try{
                Socket socket = (Socket) ar.AsyncState;
                socket.EndConnect(ar);
                Debug.Log("Socket Connect Succ");
            }
            catch (SocketException ex){
                Debug.Log("Socket Connect fail" + ex.ToString());
            }
        }

说明:

  • 由BeginConnect最后一个参数传入的socket,可由ar.AsyncState获取到。

异步Receive

public IAsyncResult BeginReceive ( byte[] buffer, int offset, int size, SocketFlags socketFlags, AsyncCallback callback, object state )

public int EndReceive( IAsyncResult asyncResult )  它的返回值代表接收到的字节数

修改客户端代码:

using System;
using System.Collections;
using System.Collections.Generic;
using System.Net.Sockets;
using UnityEngine;
using UnityEngine.UI;

public class Echo : MonoBehaviour
{
    //UGUI
    public InputField inputField;
    public Text text;
    //定义套接字
    Socket socket;

    //接收缓冲区
    byte[] readBuff = new byte[1024];
    string recvStr = "";

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

    //Connect回调
    public void ConnectCallback(IAsyncResult ar)
    {
        try
        {
            Socket socket = (Socket)ar.AsyncState;
            socket.EndConnect(ar);
            Debug.Log("Socket Connect Succ");
            socket.BeginReceive(readBuff,0,1024,0,ReceiveCallback,socket);
        }
        catch (SocketException ex)
        {
            Debug.Log("Socket Connect fail" + ex.ToString());
        }
    }

    public void ReceiveCallback(IAsyncResult ar) {
        try
        {
            Socket socket = (Socket) ar.AsyncState;
            int count = socket.EndReceive(ar);
            recvStr = System.Text.Encoding.Default.GetString(readBuff,0,count);
            socket.BeginReceive(readBuff,0,1024,0,ReceiveCallback,socket);
            Debug.Log("ReceiveCallback" + recvStr);
        }
        catch (SocketException ex)
        {
            Debug.Log("Socket Receive fail" + ex.ToString());
        }
    }

    public void Send()
    {
        string sendStr = inputField.text;
        byte[] sendBytes = System.Text.Encoding.Default.GetBytes(sendStr);
        socket.Send(sendBytes);
    }

    // Start is called before the first frame update
    void Start()
    {

    }

    // Update is called once per frame
    void Update()
    {
        text.text = recvStr;
    }
}

说明:

BeginReceive的参数

上述程序中,BeginReceive的参数为(readBuff, 0, 1024, 0, ReceiveCallback,socket)。第一个参数readBuff表示接收缓冲区;第二个参数0表示从readBuff第0位开始接收数据,这个参数和TCP粘包问题有关,第三个参数1024代表每次最多接收1024个字节的数据

BeginReceive的调用位置

程序在两个地方调用了BeginReceive:一个是ConnectCallback,在连接成功后,就开始接收数据,接收到数据后,回调函数ReceiveCallback被调用。另一个是BeginReceive内部,接收完一串数据后,等待下一串数据的到来

Update和recvStr

在Unity中,只有主线程可以操作UI组件。由于异步回调是在其他线程执行的,如果在BeginReceive给text.text赋值,Unity会弹出“get_isActiveAndEnabled can onlybe called from the main thread”的异常信息,所以程序只给变量recvStr赋值,在主线程执行的Update中再给text.text赋值

异步Send

Socket使用的协议、IP、端口属于用户层面的属性,可以直接修改;操作系统层面拥有“发送”和“接收”两个缓冲区,当调用Send方法时,程序将要发送的字节流写入到发送缓冲区中,再由操作系统完成数据的发送和确认

如果缓冲区满,那么Send就会阻塞,直到缓冲区的数据被确认腾出空间

值得注意的是,Send过程只是把数据写入到发送缓冲区,然后由操作系统负责重传、确认等步骤。Send方法返回只代表成功将数据放到发送缓存区中,对方可能还没有收到数据。

异步Send不会卡住程序,当数据成功写入输入缓冲区(或发生错误)时会调用回调函数。异步Send方法BeginSend的原型如下。

public IAsyncResult BeginSend( byte[] buffer, int offset, int size, SocketFlags socketFlags, AsyncCallback callback, object state )

public int EndSend ( IAsyncResult asyncResult )

修改客户端代码,使用异步发送:

    public void Send()
    {
        string sendStr = inputField.text;
        byte[] sendBytes = System.Text.Encoding.Default.GetBytes(sendStr);
        for(int i = 0;i < 10000;i++) {
        socket.BeginSend(sendBytes,0,sendBytes.Length,0,SendCallback,socket);
        }
    }

    public void SendCallback(IAsyncResult ar) {
        try
        {
            Socket socket = (Socket) ar.AsyncState;
            int count = socket.EndSend(ar);
            Debug.Log("Socket Send succ" + count);
        }
        catch (SocketException ex)
        {
            Debug.Log("Socket Send fail" + ex.ToString());
        }
    }

异步服务端

上面的同步服务端程序同一时间只能处理一个客户端的请求,因为它会一直阻塞,等待某一个客户端的数据,无暇接应其他客户端。使用异步方法,可以让服务端同时处理多个客户端的数据,及时响应

管理客户端

定义一个名为ClientState的类,用于保存一个客户端信息。ClientState包含TCP连接所需Socket,以及用于填充BeginReceive参数的读缓冲区readBuff

        //数据结构
        class ClientState {

            public Socket socket;

            public byte[] readBuff = new byte[1024];

        }
        static Dictionary<Socket, ClientState> clients =
            new Dictionary<Socket, ClientState>();

异步Accept

public IAsyncResult BeginAccept( AsyncCallback callback, object state )

public Socket EndAccept( IAsyncResult asyncResult )

程序结构:

修改代码:

using System.Net;
using System.Net.Sockets;

class ClientState {
    public Socket socket;
    public byte[] readBuff = new byte[1024];
}

internal class Program
{
    //监听Socket
    static Socket listenfd;
    //客户端Socket及状态信息
    static Dictionary<Socket,ClientState> clients = new Dictionary<Socket, ClientState>();

    private static void Main(string[] args)
    {
        Console.WriteLine("Hello, World!");
        //Socket
        listenfd = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        IPAddress ipAdr = IPAddress.Parse("127.0.0.1");
        IPEndPoint ipEp = new IPEndPoint(ipAdr, 8888);
        listenfd.Bind(ipEp);
        //Listen
        listenfd.Listen(0);
        Console.WriteLine("[服务器]启动成功");
        listenfd.BeginAccept(AcceptCallback,listenfd);
        //等待
        Console.ReadLine();
        // while (true)
        // {
        //     //Accept
        //     Socket connfd = listenfd.Accept();
        //     Console.WriteLine("[服务器]Accept");
        //     //Receive
        //     byte[] readBuff = new byte[1024];
        //     int count = connfd.Receive(readBuff);
        //     string readStr = System.Text.Encoding.Default.GetString(readBuff, 0, count);
        //     Console.WriteLine("[服务器接受]" + readStr);
        //     //Send
        //     string sendStr = System.DateTime.Today.ToString();
        //     byte[] sendBytes = System.Text.Encoding.Default.GetBytes(sendStr);
        //     connfd.Send(sendBytes);
        // }
    }

    //Accept回调
    public static void AcceptCallback(IAsyncResult ar) {
        try
        {
            Console.WriteLine("[服务器]Accept");
            Socket listenfd = (Socket) ar.AsyncState;
            Socket clientfd = listenfd.EndAccept(ar);
            //clients列表
            ClientState state = new ClientState();
            state.socket = clientfd;
            clients.Add(clientfd,state);
            //接收数据的BeginReceive
            clientfd.BeginReceive(state.readBuff,0,1024,0,ReceiveCallback,state);
            //继续Accept
            listenfd.BeginAccept(AcceptCallback,listenfd);
        }
        catch (SocketException ex)
        {
            Console.WriteLine("Socket Accept fail" + ex.ToString());
        }
    }

    //Receive回调
    public static void ReceiveCallback(IAsyncResult ar) {
        try
        {
            ClientState state = (ClientState) ar.AsyncState;
            Socket clientfd = state.socket;
            int count = clientfd.EndReceive(ar);
            //客户端关闭
            if(count == 0) {
                clientfd.Close();
                clients.Remove(clientfd);
                Console.WriteLine("Socket Close");
                return;
            }
            string recvStr = System.Text.Encoding.Default.GetString(state.readBuff,0,count);
            byte[] sendBytes = System.Text.Encoding.Default.GetBytes("echo" + recvStr);
            clientfd.Send(sendBytes); //减少代码量,不用异步
            clientfd.BeginReceive(state.readBuff,0,1024,0,ReceiveCallback,state);
            //注意BeginReceive的最后一个参数,这里以ClientState代替了原来的Socket。
        }
        catch (SocketException ex)
        {
            Console.WriteLine("Socket Receive fail" + ex.ToString());
            throw;
        }
    }
}

AcceptCallback是BeginAccept的回调函数,它处理了三件事情:

  • 给新的连接分配ClientState,并把它添加到clients列表中;
  • 异步接收客户端数据;
  • 再次调用BeginAccept实现循环。

ReceiveCallback是BeginReceive的回调函数,它也处理了三件事情:

  • 服务端收到消息后,回应客户端;
  • 如果收到客户端关闭连接的信号“if(count == 0)”​,断开连接;
  • 继续调用BeginReceive接收下一个数据。

当Receive返回值小于等于0时,表示Socket连接断开,可以关闭Socket。

聊天室

在聊天室中,某个客户端发送聊天消息,所有在线的客户端都会收到这条消息。也就是会遍历在线的客户端,然后推送消息

修改服务端代码:

        //Receive回调
        public static void ReceiveCallback(IAsyncResult ar){
            try {
                ……
                  string recvStr = System.Text.Encoding.Default.GetString(state.readBuff,
    0, count);
                string sendStr = clientfd.RemoteEndPoint.ToString() + ":" + recvStr;
                byte[] sendBytes = System.Text.Encoding.Default.GetBytes(sendStr);
                foreach (ClientState s in clients.Values){
                    s.socket.Send(sendBytes);
                }
                clientfd.BeginReceive( state.readBuff, 0, 1024, 0,
                    ReceiveCallback, state);
            }
            catch (SocketException ex){
                ……
            }
        }

服务端整体代码:

using System.Net;
using System.Net.Sockets;

class ClientState {
    public Socket socket;
    public byte[] readBuff = new byte[1024];
}

internal class Program
{
    //监听Socket
    static Socket listenfd;
    //客户端Socket及状态信息
    static Dictionary<Socket,ClientState> clients = new Dictionary<Socket, ClientState>();

    private static void Main(string[] args)
    {
        Console.WriteLine("Hello, World!");
        //Socket
        listenfd = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        IPAddress ipAdr = IPAddress.Parse("127.0.0.1");
        IPEndPoint ipEp = new IPEndPoint(ipAdr, 8888);
        listenfd.Bind(ipEp);
        //Listen
        listenfd.Listen(0);
        Console.WriteLine("[服务器]启动成功");
        listenfd.BeginAccept(AcceptCallback,listenfd);
        //等待
        Console.ReadLine();
    }

    //Accept回调
    public static void AcceptCallback(IAsyncResult ar) {
        try
        {
            Console.WriteLine("[服务器]Accept");
            Socket listenfd = (Socket) ar.AsyncState;
            Socket clientfd = listenfd.EndAccept(ar);
            //clients列表
            ClientState state = new ClientState();
            state.socket = clientfd;
            clients.Add(clientfd,state);
            //接收数据的BeginReceive
            clientfd.BeginReceive(state.readBuff,0,1024,0,ReceiveCallback,state);
            //继续Accept
            listenfd.BeginAccept(AcceptCallback,listenfd);
        }
        catch (SocketException ex)
        {
            Console.WriteLine("Socket Accept fail" + ex.ToString());
        }
    }

    //Receive回调
    public static void ReceiveCallback(IAsyncResult ar) {
        try
        {
            ClientState state = (ClientState) ar.AsyncState;
            Socket clientfd = state.socket;
            int count = clientfd.EndReceive(ar);
            //客户端关闭
            if(count == 0) {
                clientfd.Close();
                clients.Remove(clientfd);
                Console.WriteLine("Socket Close");
                return;
            }
            string recvStr = System.Text.Encoding.Default.GetString(state.readBuff,0,count);
            string sendStr = clientfd.RemoteEndPoint.ToString() + ":" + recvStr;
            byte[] sendBytes = System.Text.Encoding.Default.GetBytes("echo" + sendStr);
            foreach(ClientState s in clients.Values) {
                s.socket.Send(sendBytes);
            }
            clientfd.BeginReceive(state.readBuff,0,1024,0,ReceiveCallback,state);
        }
        catch (SocketException ex)
        {
            Console.WriteLine("Socket Receive fail" + ex.ToString());
            throw;
        }
    }
}

修改客户端代码,显示历史聊天:

        //Receive回调
        public void ReceiveCallback(IAsyncResult ar){
            try {
                Socket socket = (Socket) ar.AsyncState;
                int count = socket.EndReceive(ar);
                string s = System.Text.Encoding.Default.GetString(readBuff, 0, count);
                recvStr = s + "\n" + recvStr;

                socket.BeginReceive( readBuff, 0, 1024, 0,
                    ReceiveCallback, socket);
            }
            catch (SocketException ex){
                Debug.Log("Socket Receive fail" + ex.ToString());
            }
        }

客户端整体代码:

using System;
using System.Collections;
using System.Collections.Generic;
using System.Net.Sockets;
using UnityEngine;
using UnityEngine.UI;

public class Echo : MonoBehaviour
{
    //UGUI
    public InputField inputField;
    public Text text;
    //定义套接字
    Socket socket;

    //接收缓冲区
    byte[] readBuff = new byte[1024];
    string recvStr = "";

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

    //Connect回调
    public void ConnectCallback(IAsyncResult ar)
    {
        try
        {
            Socket socket = (Socket)ar.AsyncState;
            socket.EndConnect(ar);
            Debug.Log("Socket Connect Succ");
            socket.BeginReceive(readBuff,0,1024,0,ReceiveCallback,socket);
        }
        catch (SocketException ex)
        {
            Debug.Log("Socket Connect fail" + ex.ToString());
        }
    }

    public void ReceiveCallback(IAsyncResult ar) {
        try
        {
            Socket socket = (Socket) ar.AsyncState;
            int count = socket.EndReceive(ar);
            string s = System.Text.Encoding.Default.GetString(readBuff,0,count);
            recvStr = s + "\n" + recvStr;
            socket.BeginReceive(readBuff,0,1024,0,ReceiveCallback,socket);
            Debug.Log("ReceiveCallback" + recvStr);
        }
        catch (SocketException ex)
        {
            Debug.Log("Socket Receive fail" + ex.ToString());
        }
    }

    public void Send()
    {
        string sendStr = inputField.text;
        byte[] sendBytes = System.Text.Encoding.Default.GetBytes(sendStr);
        socket.BeginSend(sendBytes,0,sendBytes.Length,0,SendCallback,socket);

    }

    public void SendCallback(IAsyncResult ar) {
        try
        {
            Socket socket = (Socket) ar.AsyncState;
            int count = socket.EndSend(ar);
            Debug.Log("Socket Send succ" + count);
        }
        catch (SocketException ex)
        {
            Debug.Log("Socket Send fail" + ex.ToString());
        }
    }

    // Start is called before the first frame update
    void Start()
    {

    }

    // Update is called once per frame
    void Update()
    {
        text.text = recvStr;
    }
}

效果: 

状态检测Poll

什么是Poll

处理阻塞的代码:

        if(socket有可读数据){
            socket.Receive()
        }

        if(socket缓冲区可写){
            socket.Send()
        }

        if(socket发生程序){
            错误处理
        }

public bool Poll ( int microSeconds, SelectMode mode )

防止单线程卡住程序的Poll方法

Poll方法将会检查Socket的状态。如果指定mode参数为SelectMode.SelectRead,则可确定Socket是否为可读;指定参数为SelectMode.SelectWrite,可确定Socket是否为可写;指定参数为SelectMode.SelectError,可以检测错误条件。Poll将在指定的时段(以微秒为单位)内阻止执行,如果希望无限期地等待响应,可将microSeconds设置为一个负整数;如果希望不阻塞,可将microSeconds设置为0。

        //省略各种using
        public class Echo : MonoBehaviour {

            //定义套接字
            Socket socket;
            //UGUI
            public InputField InputFeld;
            public Text text;

            //点击连接按钮
            public void Connection()
            {
                //Socket
                socket = new Socket(AddressFamily.InterNetwork,
                    SocketType.Stream, ProtocolType.Tcp);
                //Connect
                socket.Connect("127.0.0.1", 8888);
            }

            //点击发送按钮
            public void Send(){……//略}

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

                if(socket.Poll(0, SelectMode.SelectRead)){
                    byte[] readBuff = new byte[1024];
                    int count = socket.Receive(readBuff);
                    string recvStr =
                        System.Text.Encoding.Default.GetString(readBuff, 0, count);
                    text.text = recvStr;
                }
            }
        }

服务端代码:

        class MainClass
        {
            //监听Socket
            static Socket listenfd;
            //客户端Socket及状态信息
            static Dictionary<Socket, ClientState> clients =
                new Dictionary<Socket, ClientState>();
            public static void Main (string[] args)
            {
                //Socket
                listenfd = new Socket(AddressFamily.InterNetwork,
                    SocketType.Stream, ProtocolType.Tcp);
                //Bind
                IPAddress ipAdr = IPAddress.Parse("127.0.0.1");
                IPEndPoint ipEp = new IPEndPoint(ipAdr, 8888);
                listenfd.Bind(ipEp);
                //Listen
                listenfd.Listen(0);
                Console.WriteLine("[服务器]启动成功");
                //主循环
                while(true){
                    //检查listenfd
                    if(listenfd.Poll(0, SelectMode.SelectRead)){
                        ReadListenfd(listenfd);
                    }
                    //检查clientfd
                    foreach (ClientState s in clients.Values){
                        Socket clientfd = s.socket;
                        if(clientfd.Poll(0, SelectMode.SelectRead)){
                            if(! ReadClientfd(clientfd)){
                                break;
                            }
                        }
                    }
                    //防止CPU占用过高
                    System.Threading.Thread.Sleep(1);
                }
            }
        }

        //读取Listenfd
        public static void ReadListenfd(Socket listenfd){
            Console.WriteLine("Accept");
            Socket clientfd = listenfd.Accept();
            ClientState state = new ClientState();
            state.socket = clientfd;
            clients.Add(clientfd, state);
        }

        //读取Clientfd
        public static bool ReadClientfd(Socket clientfd){
            ClientState state = clients[clientfd];
            //接收
            int count = 0;
            try{
                count = clientfd.Receive(state.readBuff);
            }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 =
                System.Text.Encoding.Default.GetString(state.readBuff, 0, count);
            Console.WriteLine("Receive" + recvStr);
            string sendStr = clientfd.RemoteEndPoint.ToString() + ":" + recvStr;
            byte[] sendBytes = System.Text.Encoding.Default.GetBytes(sendStr);
            foreach (ClientState cs in clients.Values){
                cs.socket.Send(sendBytes);
            }
            return true;
        }

多路复用Select(重点)

多路复用,就是同时处理多路信号,比如同时检测多个Socket的状态。

解决Poll服务端中CPU占用率过高的方法,那就是:同时检测多个Socket的状态。在设置要监听的Socket列表后,如果有一个(或多个)Socket可读(或可写,或发生错误信息)​,那就返回这些可读的Socket,如果没有可读的,那就阻塞。

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

如图所示:把包含6个Socket的列表传给Select, Select方法将会阻塞,等到超时或某个(或多个)Socket可读时返回,并且修改checkRead列表,仅保存可读的socket A和socket C。当没有任何可读Socket时,程序将会阻塞,不占用CPU资源。

Select 服务端

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

        class ClientState
        {
            public Socket socket;
            public byte[] readBuff = new byte[1024];
        }

        class MainClass
        {
            //监听Socket
            static Socket listenfd;
            //客户端Socket及状态信息
            static Dictionary<Socket, ClientState> clients =
                new Dictionary<Socket, ClientState>();

            public static void Main (string[] args)
            {
                //Socket
                listenfd = new Socket(AddressFamily.InterNetwork,
                    SocketType.Stream, ProtocolType.Tcp);
                //Bind
                IPAddress ipAdr = IPAddress.Parse("127.0.0.1");
                IPEndPoint ipEp = new IPEndPoint(ipAdr, 8888);
                listenfd.Bind(ipEp);
                //Listen
                listenfd.Listen(0);
                Console.WriteLine("[服务器]启动成功");
                //checkRead
                List<Socket> checkRead = new List<Socket>();
                //主循环
                while(true){
                    //填充checkRead列表
                    checkRead.Clear();
                    checkRead.Add(listenfd);
                    foreach (ClientState s in clients.Values){
                        checkRead.Add(s.socket);
                    }
                    //select
                    Socket.Select(checkRead, null, null, 1000);
                    //检查可读对象
                    foreach (Socket s in checkRead){
                        if(s == listenfd){
                            ReadListenfd(s);
                        }
                        else{
                            ReadClientfd(s);
                        }
                    }
                }
            }
        }
  • 将监听Socket(listenfd)和客户端Socket(遍历clients列表)添加到待检测Socket可读状态的列表checkList中。
  • 调用Select,程序中设置超时时间为1秒,若1秒内没有任何可读信息,Select方法将checkList列表变成空列表,然后返回。
  • 对Select处理后的每个Socket做处理,如果监听Socket(listenfd)可读,说明有客户端连接,需调用Accept。如果客户端Socket可读,说明客户端发送了消息(或关闭)​,将消息广播给所有客户端。

Select客户端

            public void Update(){
                if(socket == null) {
                    return;
                }
                //填充checkRead列表
                checkRead.Clear();
                checkRead.Add(socket);
                //select
                Socket.Select(checkRead, null, null, 0);
                //check
                foreach (Socket s in checkRead){
                    byte[] readBuff = new byte[1024];
                    int count = socket.Receive(readBuff);
                    string recvStr =
                        System.Text.Encoding.Default.GetString(readBuff, 0, count);
                    text.text = recvStr;
                }
            }

        }

为了不卡住客户端,Select的超时时间设置为0,永不阻塞

参考书籍:《Unity3D网络游戏实战(第2版)》 (豆瓣) (douban.com)

  • 9
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值