罗培羽-Unity3D网络游戏实战(第2版)学习笔记一

目录

第一部分:“扎基础”导读目标:

1、TCP网络游戏开发的必备知识有哪些?

2、TCP异步连接怎么样实现?

3、多路复用的处理是怎么处理?

4、什么是粘包分包以及怎么处理?

5、怎么样发送完整的网络数据?

6、怎么样设置正确的网络参数

第一章节:网络游戏的开端

1、Socket(套接字):

2、Socket通信流程:

3、TCP和UDP协议:

4、TCP/IP协议模型

5、编写客户端程序:

6、编写服务端程序

7、思考

1、上面的实现有什么优缺点?

2、怎么样实现同时处理多个客户端的请求?

第二章:分手有术:异步和多路复用

2.1、Unity中的异步

2.2、异步客户端

2.2.1、BeginConnect 函数原型:

2.2.2、EndConnect函数原型

2.2.3、使用异步改进客户端代码

2.2.4、BeginReceive 函数原型

2.2.5、EndReceive 函数原型

2.2.6、使用异步改进客户端代码

2.2.6.1、BeginReceive的调用位置:

2.2.6.2、Update和ReceiveStr

2.2.7 异步Send,BeginSend函数原型

2.2.8、EndSend函数原型

2.2.9、修改客户端程

2.3、异步服务端

2.3.1、异步Accept,BeginAccept函数原型

2.3.2、EndAccept函数原型

2.3.3、异步服务端改进代码

2.4、做一个聊天室

2.4.1、需要改进的地方

2.4.2、聊天室完整版异步客户端

2.4.3、聊天室完整版异步服务端

2.5、状态检测Poll


 

第一部分:“扎基础”导读目标:

1、TCP网络游戏开发的必备知识有哪些?

2、TCP异步连接怎么样实现?

3、多路复用的处理是怎么处理?

4、什么是粘包分包以及怎么处理?

5、怎么样发送完整的网络数据?

6、怎么样设置正确的网络参数


第一章节:网络游戏的开端

1、Socket(套接字):

网络上的两个程序通过一个双向的通信连接实现数据交换、这个连接的一端成为一个socket。socket一般包含了进行网络通信必需的五大信息:连接使用的协议、本地主机ip地址、本地的协议端口、远程主机的ip地址、远程协议端口

7da6c44ee64841749a828ea3a02ae8c0.png

 

2、Socket通信流程:

1、创建一个Socket对象(使用API Socket),绑定端口(使用API Bind),对于客户端而言,连接时(使用API Connect)会由系统分配端口;

2、服务端开始监听,等待客户端接入

3、客户端连接服务器

4、服务端接受客户端连接,可以开始通信

通过以上4步可以成功建立连接,可以收发数据

5、客户端和服务端收发数据,传输信息

6、某一方关闭连接,通信结束

f5cb6415ee3441bbad04b78b26bdd931.png

 3、TCP和UDP协议:

TCP:是一种面向连接的,可靠安全的,基于字节流的,传输层通信协议

UDP:无连接的,不安全的,传输效率高的,传输层协议

4、TCP/IP协议模型

2eb4f14a13f14536ad10e0690e9a66cc.png

 5、编写客户端程序:

1.5.1、Echo客户端1.0

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

public class EchoClient : MonoBehaviour
{
    Socket socket;
    string sendMsg;
    public GameObject InputGameObject;
    private InputField inputField;
    public GameObject showGameObject;
    public Text ShowText;
    
    private void Awake()
    {        
        inputField = InputGameObject.transform.GetComponent<InputField>();
        ShowText = showGameObject.GetComponent<Text>();
    }
    // Start is called before the first frame update
    void Start()
    {
        
    }
    public void Connet()
    {
        socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        IPAddress iPAddress = IPAddress.Parse("127.0.0.1");
        IPEndPoint endPoint = new IPEndPoint(iPAddress, 8888);
        socket.Connect(endPoint);
        Debug.Log("连接成功");
    }
    public void Send()
    {
        sendMsg = inputField.text;
        byte[] sendBytes = System.Text.Encoding.Default.GetBytes(sendMsg);
        socket.Send(sendBytes);

        byte[] receiveBytes = new byte[1024];
        int count = socket.Receive(receiveBytes, SocketFlags.None);
        string receiveStr = System.Text.Encoding.Default.GetString(receiveBytes, 0, count);
        System.Console.WriteLine("接受的数据为:" + receiveStr);
        ShowText.text = receiveStr;
        socket.Close();

    }

}

6、编写服务端程序

1.6.1、服务端EchoServer1.0

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;

namespace EchoServer
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("HelloWorld!");

            //Socket
            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(10);
            Console.WriteLine("服务器已经启动成功了");
            
            while (true)
            {
                //Accept
                Socket connfd = listenfd.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);

                byte[] sendBytes = System.Text.Encoding.Default.GetBytes(readStr);
                connfd.Send(sendBytes);
            }


        }
    }
}

7、思考

1、上面的实现有什么优缺点?

程序集使用的API都是阻塞API(Connect,Send,Receive等),又称同步程序,会造成程序卡顿的致命缺点,同时服务端每次只能处理一个客户端的请求消息,不具有实用性

改进:使用异步方式和多路复用技术

2、怎么样实现同时处理多个客户端的请求?

使用异步方式和多路复用技术

第二章:分手有术:异步和多路复用

2.1、Unity中的异步

异步的实现依赖于多线程,在unity中的执行生命周期函数的线程是主线程。

2.2、异步客户端

2.2.1、BeginConnect 函数原型:

public IAsyncResult BeginConnect(
    string host,
    int port,
    AsyncCallback requestCallback,
    object state
)
参数说明
host远程主机的名称(IP)
port 远程主机的端口号
requestCallback一个AsyncCallback委托,即回调函数,回函函数必须是这样的形式:void ConnectCallback(IAsyncResult ar)
state一个用户自定义的对象,可包含连接操作的相关信息。此对象会被传递给回调函数

知识点:IAsyncResult是.NET提供的一种异步操作,通过名为BeginXXX和EndXXX的两个方法来实现原同步方法的异步调用。BeginXXX方法包含同步方法中所需要的参数,此外还包含另外两个参数:一个AsyncCallback委托和一个用户自定义的状态对象。委托用来调用回调方法,状态对象用来向回调方法传递状态信息。

2.2.2、EndConnect函数原型

public void EndConnect(IAsyncResult asyncResult)

2.2.3、使用异步改进客户端代码

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

public class EchoClient : MonoBehaviour
{
    Socket socket;
    string sendMsg;
    public GameObject InputGameObject;
    private InputField inputField;
    public GameObject showGameObject;
    public Text ShowText;
    
    private void Awake()
    {        
        inputField = InputGameObject.transform.GetComponent<InputField>();
        ShowText = showGameObject.GetComponent<Text>();
    }
    // Start is called before the first frame update
    void Start()
    {
        
    }
    public void Connet()
    {
        //socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        //IPAddress iPAddress = IPAddress.Parse("127.0.0.1");
        //IPEndPoint endPoint = new IPEndPoint(iPAddress, 8888);
        //socket.Connect(endPoint);
        //Debug.Log("连接成功");

        socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        IPAddress iPAddress = IPAddress.Parse("127.0.0.1");
        IPEndPoint endPoint = new IPEndPoint(iPAddress, 8888);
        socket.BeginConnect(endPoint, AsyncConnetCallback, socket);
    }

    private void AsyncConnetCallback(IAsyncResult ar)
    {
        try
        {
            Socket socket = (Socket)ar.AsyncState;
            socket.EndConnect(ar);
            Debug.Log("connect Succ");
        }
        catch (Exception e)
        {
            Debug.Log("connect Fail"+e.ToString());
            throw;
        }
    }

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

        byte[] receiveBytes = new byte[1024];
        int count = socket.Receive(receiveBytes, SocketFlags.None);
        string receiveStr = System.Text.Encoding.Default.GetString(receiveBytes, 0, count);
        System.Console.WriteLine("接受的数据为:" + receiveStr);
        ShowText.text = receiveStr;
        socket.Close();

    }

}

2.2.4、BeginReceive 函数原型

//用于实现异步接收数据
public IAsyncResult BeginReceive(
    byte[] buffer,
    int offset,
    int size,
    SocketFlags socketFlags,
    AsyncCallback callback,
    object state
)
参数说明
bufferByte类型的数组
offsetbuffer中存储数据的位置,该位置是从0开始计数的
size最多接受的字节数
socketFlags

socketFlags值的按位组合,这里设置为0

callback

回调函数,一个AsyncCallback委托

state一个用户自定义的对象,其中包含接受操作的相关信息。当操作完成时,此对象会被传递给EndReceive委托

2.2.5、EndReceive 函数原型

public int EndReceive(ASyncResult asyncResult)

2.2.6、使用异步改进客户端代码

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

public class EchoClient : MonoBehaviour
{
    Socket socket;
    string sendMsg;
    public GameObject InputGameObject;
    private InputField inputField;
    public GameObject showGameObject;
    public Text ShowText;
    string receiveStr;
    
    private void Awake()
    {        
        inputField = InputGameObject.transform.GetComponent<InputField>();
        ShowText = showGameObject.GetComponent<Text>();
    }
    // Start is called before the first frame update
    void Start()
    {
        
    }
    public void Connet()
    {
        //socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        //IPAddress iPAddress = IPAddress.Parse("127.0.0.1");
        //IPEndPoint endPoint = new IPEndPoint(iPAddress, 8888);
        //socket.Connect(endPoint);
        //Debug.Log("连接成功");

        socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        IPAddress iPAddress = IPAddress.Parse("127.0.0.1");
        IPEndPoint endPoint = new IPEndPoint(iPAddress, 8888);
        socket.BeginConnect(endPoint, AsyncConnetCallback, socket);

        byte[] receiveBytes = new byte[1024];
        socket.BeginReceive(receiveBytes, 0, receiveBytes.Length, SocketFlags.None, AsyncReceiveCallback, socket);
    }

    private void AsyncConnetCallback(IAsyncResult ar)
    {
        try
        {
            Socket socket = (Socket)ar.AsyncState;
            socket.EndConnect(ar);
            Debug.Log("connect Succ");
        }
        catch (Exception e)
        {
            Debug.Log("connect Fail"+e.ToString());
            throw;
        }
    }

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

    private void AsyncReceiveCallback(IAsyncResult ar)
    {
        try
        {
            Socket socket = (Socket)ar.AsyncState;
            int count = socket.EndReceive(ar);
            byte[] receiveBytes = new byte[1024];
            receiveStr = System.Text.Encoding.UTF8.GetString(receiveBytes, 0, count);

            socket.BeginReceive(receiveBytes, 0, receiveBytes.Length, SocketFlags.None, AsyncReceiveCallback, socket);


        }
        catch (Exception e)
        {
            Debug.Log(e.ToString());
            throw;
        }
    }

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

注意:

2.2.6.1、BeginReceive的调用位置:

两次调用BeginReceive,在ConnetCallback,在连接成功后就开始调用接受数据,接受到数据后,回调函数AsyncReceiveCallback被调用,接受玩一串数据后,等待接受下一串数据。

2.2.6.2、Update和ReceiveStr

在unity中只有主线程可以操作ui组件。由于异步回调是在其它的线程中执行的,如果在BeginReceive或者AsyncReceiveCallback中给UnityEngine.UI中的Text.text赋值,此程序会报错

2.2.7 异步Send,BeginSend函数原型

public IAsyncResult BeginSend(
    byte[] buffer,
    int offset,
    int size,
    SocketFlags socketFlags,
    AsyncCallback callback,
    object state
)
参数说明
bufferByte类型的数组
offsetbuffer中存储数据的位置,该位置是从0开始计数的
size最多接受的字节数
socketFlags

socketFlags值的按位组合,这里设置为0

callback

回调函数,一个AsyncCallback委托

state一个用户自定义的对象,其中包含接受操作的相关信息。当操作完成时,此对象会被传递给EndSend委托

2.2.8、EndSend函数原型

public int EndSend(IAsyncResult asyncResult)

2.2.9、修改客户端程

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

public class EchoClient : MonoBehaviour
{
    Socket socket;
    string sendMsg;
    public GameObject InputGameObject;
    private InputField inputField;
    public GameObject showGameObject;
    public Text ShowText;
    string receiveStr;
    byte[] readbuff = new byte[1024];

    private void Awake()
    {        
        inputField = InputGameObject.transform.GetComponent<InputField>();
        ShowText = showGameObject.GetComponent<Text>();
    }
    // Start is called before the first frame update
    void Start()
    {
        
    }
    public void Connet()
    {
        //socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        //IPAddress iPAddress = IPAddress.Parse("127.0.0.1");
        //IPEndPoint endPoint = new IPEndPoint(iPAddress, 8888);
        //socket.Connect(endPoint);
        //Debug.Log("连接成功");

        socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        IPAddress iPAddress = IPAddress.Parse("127.0.0.1");
        IPEndPoint endPoint = new IPEndPoint(iPAddress, 8888);
        socket.BeginConnect(endPoint, AsyncConnetCallback, socket);

    }

    private void AsyncConnetCallback(IAsyncResult ar)
    {
        try
        {
            Socket socket = (Socket)ar.AsyncState;
            socket.EndConnect(ar);
            Debug.Log("connect Succ");
            socket.BeginReceive(readbuff, 0, 1024, 0, AsyncReceiveCallback, socket);
       
        }
        catch (Exception e)
        {
            Debug.Log("connect Fail"+e.ToString());
            throw;
        }
    }

    public void Send()
    {
        sendMsg = inputField.text;
        byte[] sendBytes = System.Text.Encoding.Default.GetBytes(sendMsg);
        //socket.Send(sendBytes);
        socket.BeginSend(sendBytes, 0, sendBytes.Length, SocketFlags.None, AsyncBeginSendCallback, socket);

    }

    private void AsyncBeginSendCallback(IAsyncResult ar)
    {
        try
        {
            Socket socket = (Socket)ar.AsyncState;
            socket.EndSend(ar);
        }
        catch (Exception e)
        {
            Debug.Log(e.ToString());
            throw;
        }
    }

    private void AsyncReceiveCallback(IAsyncResult ar)
    {
        try
        {
            Socket socket = (Socket)ar.AsyncState;
            int count = socket.EndReceive(ar);
            receiveStr = System.Text.Encoding.UTF8.GetString(readbuff, 0, count);

            socket.BeginReceive(readbuff, 0, readbuff.Length, SocketFlags.None, AsyncReceiveCallback, socket);
            

        }
        catch (Exception e)
        {
            Debug.Log(e.ToString());
            throw;
        }
    }

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

2.3、异步服务端

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

2.3.1、异步Accept,BeginAccept函数原型

pubic IAsyncResult BeginAccept(
    AsynncCallback asyncCallback,
    object state
)

2.3.2、EndAccept函数原型

public IAsyncResult EndAccept(AsyncResult asyncResult)

2.3.3、异步服务端改进代码

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;

namespace EchoServer
{
    class Program
    {
        static Socket listenSocket;
        static Dictionary<Socket, ClientState> ClientSocketDic = new Dictionary<Socket, ClientState>();
        static void Main(string[] args)
        {
            Console.WriteLine("HelloWorld!");

            //Socket
            listenSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

            //Bind
            IPAddress ipAdr = IPAddress.Parse("127.0.0.1");
            IPEndPoint ipEp = new IPEndPoint(ipAdr, 8888);
            listenSocket.Bind(ipEp);

            //Listen
            listenSocket.Listen(10);
            Console.WriteLine("服务器已经启动成功了");

            listenSocket.BeginAccept(AsyncBeginAceptCallback, listenSocket);
            Console.ReadLine();
        }

        private static void AsyncBeginAceptCallback(IAsyncResult ar)
        {
            try
            {
                Socket socket = (Socket)ar.AsyncState;
                Socket clientSocket = socket.EndAccept(ar);

                Console.WriteLine(clientSocket.ToString());
                Console.WriteLine(clientSocket.RemoteEndPoint.ToString());

                ClientState state = new ClientState();
                state.clientSocket = clientSocket;
                ClientSocketDic.Add(clientSocket, state);

                clientSocket.BeginReceive(state.readBuff, 0, state.readBuff.Length, SocketFlags.None, AsyReceiveCallback, state);

                //clientSocket.BeginAccept(AsyncBeginAceptCallback, clientSocket);//System.InvalidOperationException: 在执行此操作前必须先调用 Listen 方法。
                socket.BeginAccept(AsyncBeginAceptCallback, socket);
            }
            catch (SocketException ex)
            {
                Console.WriteLine("Socket Aceept fail" + ex.ToString());
            }
        }

        private static void AsyReceiveCallback(IAsyncResult ar)
        {
            try
            {
                ClientState clientState = (ClientState)ar.AsyncState;
                Socket clientSocket = clientState.clientSocket;
                int count = clientSocket.EndReceive(ar);

                if (count == 0)
                {
                    clientSocket.Close();
                    ClientSocketDic.Remove(clientSocket);
                    Console.WriteLine("Socket Close");
                    return;
                }

                string receveStr = System.Text.Encoding.UTF8.GetString(clientState.readBuff, 0, count);

                Console.WriteLine("接受的数据为:"+receveStr);

                byte[] sendBytes = System.Text.Encoding.UTF8.GetBytes("ServerSend:" + receveStr);
                clientSocket.Send(sendBytes);
                clientSocket.BeginReceive(clientState.readBuff, 0, clientState.readBuff.Length, SocketFlags.None, AsyReceiveCallback, clientState);
            }
            catch (Exception e)
            {
                Console.WriteLine("Socket Receive Fail  "+e.ToString());
                throw;
            }
        }
    }

    //新建一个类,包含客户端的socket的信息,
    //因为每个客户端和服务器通信都需要一个自己的readBuff,不然容易造成数据丢失
    public class ClientState
    {
        public Socket clientSocket;
        public byte[] readBuff = new byte[1024];
    }
}

2.4、做一个聊天室

2.4.1、需要改进的地方

在前面2.3.3中异步服务端的代码可以接受多个客户端的连接请求,但是处理消失时给客户端发送的数据是最近一个客户端发来的数据,因此这种在聊天室中是不符合的,需要遍历在线的客户端,然后向每一个客户端推送消息。改进后的AsyncReceiveCallback函数如下:

private static void AsyReceiveCallback(IAsyncResult ar)
        {
            try
            {
                ClientState clientState = (ClientState)ar.AsyncState;
                Socket clientSocket = clientState.clientSocket;
                int count = clientSocket.EndReceive(ar);

                if (count == 0)
                {
                    clientSocket.Close();
                    ClientSocketDic.Remove(clientSocket);
                    Console.WriteLine("Socket Close");
                    return;
                }

                string receveStr = System.Text.Encoding.UTF8.GetString(clientState.readBuff, 0, count);
                Console.WriteLine("接受的数据为:"+receveStr);

                string sendStr = clientSocket.RemoteEndPoint.ToString() + ":" + receveStr;
                byte[] sendBytes = System.Text.Encoding.UTF8.GetBytes(sendStr);
                foreach (var item in ClientSocketDic.Values)
                {
                    item.clientSocket.Send(sendBytes);
                }
                //clientSocket.Send(sendBytes);
                clientSocket.BeginReceive(clientState.readBuff, 0, clientState.readBuff.Length, SocketFlags.None, AsyReceiveCallback, clientState);
            }
            catch (Exception e)
            {
                Console.WriteLine("Socket Receive Fail  "+e.ToString());
                throw;
            }
        }

2.4.2、聊天室完整版异步客户端

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

public class EchoClient : MonoBehaviour
{
    Socket socket;
    string sendMsg;
    public GameObject InputGameObject;
    private InputField inputField;
    public GameObject showGameObject;
    public Text ShowText;
    string receiveStr;
    byte[] readbuff = new byte[1024];

    private void Awake()
    {        
        inputField = InputGameObject.transform.GetComponent<InputField>();
        ShowText = showGameObject.GetComponent<Text>();
    }
    // Start is called before the first frame update
    void Start()
    {
        
    }
    public void Connet()
    {
        //socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        //IPAddress iPAddress = IPAddress.Parse("127.0.0.1");
        //IPEndPoint endPoint = new IPEndPoint(iPAddress, 8888);
        //socket.Connect(endPoint);
        //Debug.Log("连接成功");

        socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        IPAddress iPAddress = IPAddress.Parse("127.0.0.1");
        IPEndPoint endPoint = new IPEndPoint(iPAddress, 8888);
        socket.BeginConnect(endPoint, AsyncConnetCallback, socket);

    }

    private void AsyncConnetCallback(IAsyncResult ar)
    {
        try
        {
            Socket socket = (Socket)ar.AsyncState;
            socket.EndConnect(ar);
            Debug.Log("connect Succ");
            socket.BeginReceive(readbuff, 0, 1024, 0, AsyncReceiveCallback, socket);
       
        }
        catch (Exception e)
        {
            Debug.Log("connect Fail"+e.ToString());
            throw;
        }
    }

    public void Send()
    {
        sendMsg = inputField.text;
        byte[] sendBytes = System.Text.Encoding.Default.GetBytes(sendMsg);
        //socket.Send(sendBytes);
        socket.BeginSend(sendBytes, 0, sendBytes.Length, SocketFlags.None, AsyncBeginSendCallback, socket);

    }

    private void AsyncBeginSendCallback(IAsyncResult ar)
    {
        try
        {
            Socket socket = (Socket)ar.AsyncState;
            socket.EndSend(ar);
        }
        catch (Exception e)
        {
            Debug.Log(e.ToString());
            throw;
        }
    }

    private void AsyncReceiveCallback(IAsyncResult ar)
    {
        try
        {
            Socket socket = (Socket)ar.AsyncState;
            int count = socket.EndReceive(ar);
            string s = System.Text.Encoding.UTF8.GetString(readbuff, 0, count);
            receiveStr = s + "\n" + receiveStr;

            socket.BeginReceive(readbuff, 0, readbuff.Length, SocketFlags.None, AsyncReceiveCallback, socket);
            

        }
        catch (Exception e)
        {
            Debug.Log(e.ToString());
            throw;
        }
    }

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

客户端界面:

72900769060a41d98a9d796f59fed9e6.png

 

2.4.3、聊天室完整版异步服务端

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;

namespace EchoServer
{
    class Program
    {
        static Socket listenSocket;
        static Dictionary<Socket, ClientState> ClientSocketDic = new Dictionary<Socket, ClientState>();
        static void Main(string[] args)
        {
            Console.WriteLine("HelloWorld!");

            //Socket
            listenSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

            //Bind
            IPAddress ipAdr = IPAddress.Parse("127.0.0.1");
            IPEndPoint ipEp = new IPEndPoint(ipAdr, 8888);
            listenSocket.Bind(ipEp);

            //Listen
            listenSocket.Listen(10);
            Console.WriteLine("服务器已经启动成功了");

            listenSocket.BeginAccept(AsyncBeginAceptCallback, listenSocket);
            Console.ReadLine();
        }

        private static void AsyncBeginAceptCallback(IAsyncResult ar)
        {
            try
            {
                Socket socket = (Socket)ar.AsyncState;
                Socket clientSocket = socket.EndAccept(ar);

                Console.WriteLine(clientSocket.ToString());
                Console.WriteLine(clientSocket.RemoteEndPoint.ToString());

                ClientState state = new ClientState();
                state.clientSocket = clientSocket;
                ClientSocketDic.Add(clientSocket, state);

                clientSocket.BeginReceive(state.readBuff, 0, state.readBuff.Length, SocketFlags.None, AsyReceiveCallback, state);

                //clientSocket.BeginAccept(AsyncBeginAceptCallback, clientSocket);//System.InvalidOperationException: 在执行此操作前必须先调用 Listen 方法。
                socket.BeginAccept(AsyncBeginAceptCallback, socket);
            }
            catch (SocketException ex)
            {
                Console.WriteLine("Socket Aceept fail" + ex.ToString());
            }
        }

        private static void AsyReceiveCallback(IAsyncResult ar)
        {
            try
            {
                ClientState clientState = (ClientState)ar.AsyncState;
                Socket clientSocket = clientState.clientSocket;
                int count = clientSocket.EndReceive(ar);

                if (count == 0)
                {
                    clientSocket.Close();
                    ClientSocketDic.Remove(clientSocket);
                    Console.WriteLine("Socket Close");
                    return;
                }

                string receveStr = System.Text.Encoding.UTF8.GetString(clientState.readBuff, 0, count);
                Console.WriteLine("接受的数据为:"+receveStr);

                string sendStr = clientSocket.RemoteEndPoint.ToString() + ":" + receveStr;
                byte[] sendBytes = System.Text.Encoding.UTF8.GetBytes(sendStr);
                foreach (var item in ClientSocketDic.Values)
                {
                    item.clientSocket.Send(sendBytes);
                }
                //clientSocket.Send(sendBytes);
                clientSocket.BeginReceive(clientState.readBuff, 0, clientState.readBuff.Length, SocketFlags.None, AsyReceiveCallback, clientState);
            }
            catch (Exception e)
            {
                Console.WriteLine("Socket Receive Fail  "+e.ToString());
                throw;
            }
        }
    }

    //新建一个类,包含客户端的socket的信息,
    //因为每个客户端和服务器通信都需要一个自己的readBuff,不然容易造成数据丢失
    public class ClientState
    {
        public Socket clientSocket;
        public byte[] readBuff = new byte[1024];
    }
}

聊天室效果图:                      

87781e2ed8ca42f9a59873b078c1b3f5.png

 

2.5、状态检测Poll

2.5.1、什么是Poll?

比起异步程序,同步程序更加的简单明了,而且不会引发线程安全问题,只需要在阻塞方法前加上一层判断,当有数据可读才调用Receive,有数据可写才调用Send,这样就既能够实现功能,又不会卡住程序。于是,微软给Socket类提供了Poll方法。

2.5.2、Poll方法原型

public bool Poll(
    int microSecends,
    SelectMode mode
)
参数

说明

microSecends等待回应的时间,以微秒为单位,如果参数为-1,表示一直等待,如果该参数为0,表示非阻塞
mode

有三种可选的模式,分别如下:

SelectRead:如果Socket可读(可以接收数据),返回true,否则返回false;

SelectWrite:如果Socket可写,返回true,否则返回false;

SelectError:如果连接失败,返回true,否则返回false;

2.5.3、使用Poll方法改写客户端:

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

public class PollClient : MonoBehaviour
{
    Socket PollClientSocket;
    public InputField inputField;
    public Text text;
    byte[] buffer;
    private bool isSend = false;
    void Awake()
    {
        
    }
    // Start is called before the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        if (PollClientSocket == null) return;
        if (PollClientSocket.Poll(0, SelectMode.SelectError)) return;
        else if (PollClientSocket.Poll(0, SelectMode.SelectRead))
        {
            byte[] receBuffers = new byte[1024];
            int count = PollClientSocket.Receive(receBuffers, 0, 1024, 0);
            string recString = System.Text.Encoding.UTF8.GetString(receBuffers, 0, count);
            text.text = text.text +"\n"+ recString;
        }else if(PollClientSocket.Poll(0, SelectMode.SelectWrite)&&isSend){
            buffer = System.Text.Encoding.UTF8.GetBytes(inputField.text);
            PollClientSocket.Send(buffer, 0, buffer.Length, SocketFlags.None);

            isSend = false;
        }
    }

   public void Connect()
    {
        PollClientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        IPAddress ipAdress = IPAddress.Parse("127.0.0.1");
        PollClientSocket.Connect(ipAdress, 8888);
        Debug.Log("连接成功!");
    }

    public void Send()
    {
        //buffer = System.Text.Encoding.UTF8.GetBytes(inputField.text);
        //PollClientSocket.Send(buffer, 0, buffer.Length, SocketFlags.None);
        //Receive();
        isSend = true;
    }

    void Receive()
    {
        //byte[] receBuffers = new byte[1024];
        //int count = PollClientSocket.Receive(receBuffers, 0, 1024, 0);
        //string recString = System.Text.Encoding.UTF8.GetString(receBuffers, 0, count);
        //text.text = text.text + recString;
    }
}

运行效果:

61825280183f4c40a10bf8a843c99906.png

2.5.4、使用Poll方法改写服务端:

在服务端需要不断重复两件事情,第一个,监听客户端Socke是否可读,如果可读意味着客户端连接上来了,就需要Accept客户端连接,然后把连接加入到客户端信息列表里;第二个,遍历客户端的信息列表,判断每一个客户端是否可读,如果可读,就处理消息数据。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;

namespace PollServerTest
{
    class Program
    {
        static Socket ServerSocket;//listenfd
        static Dictionary<Socket, ClientState> ClientSocketDic = new Dictionary<Socket, ClientState>(); 

        static void Main(string[] args)
        {
            ServerSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            IPAddress iPAddress = IPAddress.Parse("127.0.0.1");
            IPEndPoint iPEndPoint = new IPEndPoint(iPAddress, 8888);
            ServerSocket.Bind(iPEndPoint);
            ServerSocket.Listen(10);
            Console.WriteLine("服务器已经启动了");

            while (true)
            {
                if (ServerSocket.Poll(0, SelectMode.SelectRead))
                {
                    ReadListListenfd(ServerSocket);
                }
                foreach (var item in ClientSocketDic.Values)
                {
                    if (item.ClientSocket.Poll(0, SelectMode.SelectRead))
                    {
                        if (!ReadListClientfd(item))
                            break;
                    }
                }

                //防止CPU占用过高
                System.Threading.Thread.Sleep(1);
            }
            
        }

        private static bool ReadListClientfd(ClientState clientState)
        {
            Socket ClientSocket = clientState.ClientSocket;
            byte[] receiiveBuffer = clientState.readBuffer;
            int count = 0;
            try
            {
                count = ClientSocket.Receive(receiiveBuffer, 0, clientState.readBuffer.Length, SocketFlags.None);
            }
            catch (Exception ex)
            {
                ClientSocket.Close();
                ClientSocketDic.Remove(ClientSocket);
                Console.WriteLine("Exception:" + ex.ToString() + "\n");
                return false;
            }

            if (count == 0)
            {
                ClientSocket.Close();
                ClientSocketDic.Remove(ClientSocket);
                Console.WriteLine("ClientSocket Close");
                return false;
            }

            string ReceiveStr = System.Text.Encoding.UTF8.GetString(receiiveBuffer,0,count);
            Console.WriteLine("接受到的数据:" + ReceiveStr);

            byte[] sendBuffer = System.Text.Encoding.UTF8.GetBytes((ClientSocket.RemoteEndPoint.ToString() +":"+ReceiveStr));
            foreach (var item in ClientSocketDic.Values)
            {
                item.ClientSocket.Send(sendBuffer);
            }
            return true;
        }

        private static void ReadListListenfd(Socket serverSocket)
        {
            Socket ClientSocket =  serverSocket.Accept();
            ClientState clientState = new ClientState();
            clientState.ClientSocket =ClientSocket;
            ClientSocketDic.Add(ClientSocket,clientState);

            Console.WriteLine(ClientSocket.RemoteEndPoint.ToString());
        }
    }

    class ClientState
    {
        public Socket ClientSocket;
        public byte[] readBuffer = new byte[1024];
    }
}

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

public class PollClient : MonoBehaviour
{
    Socket PollClientSocket;
    public InputField inputField;
    public Text text;
    byte[] buffer;
    private bool isSend = false;
    void Awake()
    {
        
    }
    // Start is called before the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        if (PollClientSocket == null) return;
        if (PollClientSocket.Poll(0, SelectMode.SelectError)) return;
        else if (PollClientSocket.Poll(0, SelectMode.SelectRead))
        {
            byte[] receBuffers = new byte[1024];
            int count = PollClientSocket.Receive(receBuffers, 0, 1024, 0);
            string recString = System.Text.Encoding.UTF8.GetString(receBuffers, 0, count);
            text.text = text.text +"\n"+ recString;
        }else if(PollClientSocket.Poll(0, SelectMode.SelectWrite)&&isSend){
            buffer = System.Text.Encoding.UTF8.GetBytes(inputField.text);
            PollClientSocket.Send(buffer, 0, buffer.Length, SocketFlags.None);

            isSend = false;
        }
    }

   public void Connect()
    {
        PollClientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        IPAddress ipAdress = IPAddress.Parse("127.0.0.1");
        PollClientSocket.Connect(ipAdress, 8888);
        Debug.Log("连接成功!");
    }

    public void Send()
    {
        //buffer = System.Text.Encoding.UTF8.GetBytes(inputField.text);
        //PollClientSocket.Send(buffer, 0, buffer.Length, SocketFlags.None);
        //Receive();
        isSend = true;
    }

    void Receive()
    {
        //byte[] receBuffers = new byte[1024];
        //int count = PollClientSocket.Receive(receBuffers, 0, 1024, 0);
        //string recString = System.Text.Encoding.UTF8.GetString(receBuffers, 0, count);
        //text.text = text.text + recString;
    }
}

这段代码注意点:第一个:主循环之后调用了System.Threading.Thread.Sleep(1);让程序挂起1秒,这样做的目的是避免死循环,让CPU有个短暂喘息的机会。第二个:ReadClient会返回true或者false,返回false表示客户端断开连接,由于客户端断开后,ReadClientfd会删除ClientSocketDic列表中对应的客户端信息,导致ClientSocketDic列表发生改变,然而ReadClientfd又是在foreach的循环中被调用的,ClientSocketDic列表变化,会导致遍历失败,因此程序在检测到客户端关闭后将退出foreach循环,第三个,是将Poll的超时时间设置为0,程序不会有任何等待。如果设置较长的超时时间,服务端将无法及时处理多个客户端同时连接的情况。

2.6、多路复用

2.6.1、什么是多路复用?

多路复用就是同时处理多路信号,比如同时检测多个Socket的状态。在PollClient中update()每帧判断Socket是否可读可写,服务端也一直在循环,这样会浪费CPU。如果同时检测多个Socket的状态。在设置要监听的Socket列表后,如果有一个(或者多个)Socket可读(或者可写,或发生错误信息),那就返回这些可读的Socket,如果没有可读的,那就阻塞。这样即可优化。

2.6.2、Select方法原型

public static void Select(
    IList checkRead,
    IList checkWrite,
    IList checkError,
    IList microSeconds
)
参数说明
checkRead检测是否有可读的Socket列表
checkWrite检测是否有可写的Socket列表
CheckError检测是否有出错的Socket列表
microSeconds等待回应的时间,以微秒为单位,如果该参数为-1,表示一直等待,如果为0,表示非阻塞。

2.6.3、使用Select改进服务端

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;

namespace SelectServerTest
{
    class ClientState
    {
        public Socket socket;
        public byte[] readBuffer = new byte[1024];
    }
    class Program
    {
        static Socket listenfd;//listenfd
        static Dictionary<Socket, ClientState> clients = new Dictionary<Socket, ClientState>();
        static void Main(string[] args)
        {
            listenfd = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            IPAddress iPAddress = IPAddress.Parse("127.0.0.1");
            IPEndPoint iPEndPoint = new IPEndPoint(iPAddress, 8888);
            listenfd.Bind(iPEndPoint);
            listenfd.Listen(10);
            Console.WriteLine("服务器已经启动了");

            //checkRead
            List<Socket> checkRead = new List<Socket>();
            //主循环
            while (true)
            {
                //填充Socktet列表
                checkRead.Clear();
                checkRead.Add(listenfd);
                foreach (var item in clients.Values)
                {
                    checkRead.Add(item.socket);
                }
                //Select
                Socket.Select(checkRead, null, null, 0);
                foreach (Socket s in checkRead)
                {
                    if (s == listenfd)
                    {
                        ReadListenfd(s);
                    }
                    else
                    {
                        ReadClientfd(s);
                    }
                }
            }
        }
        private static void ReadListenfd(Socket listenfd)
        {
            Console.WriteLine("Accept");
            Socket clientfd = listenfd.Accept();
            ClientState clientState = new ClientState();
            clientState.socket = clientfd;
            clients.Add(clientfd, clientState);

            Console.WriteLine(clientfd.RemoteEndPoint.ToString());
        }
        private static void ReadClientfd(Socket clientfd)
        {
            ClientState state = clients[clientfd];
            //接收
            int count = 0;
            try
            {
                count = clientfd.Receive(state.readBuffer);
            }
            catch (Exception e)
            {
                clientfd.Close();
                clients.Remove(clientfd);
                Console.WriteLine("Receive SocketException:"+e.ToString());
            }
            if(count == 0)
            {
                clientfd.Close();
                clients.Remove(clientfd);
                Console.WriteLine("socket close");
            }

            string ReceiveStr = System.Text.Encoding.UTF8.GetString(state.readBuffer, 0, count);
            Console.WriteLine("接受到的数据:" + ReceiveStr);

            byte[] sendBuffer = System.Text.Encoding.UTF8.GetBytes((state.socket.RemoteEndPoint.ToString() + ":" + ReceiveStr));
            foreach (ClientState cs in clients.Values)
            {
                cs.socket.Send(sendBuffer);
            }

        }

       
    }
}

2.6.4、Select客户端

与Poll客户端相似,不赘述,都需要在Update里面不停的检测数据,性能较差。商业上为了做到性能的极致,大多使用异步的客户端,Select服务端(或者异步服务端,之后的项目展示我们使用Select服务端)

 

 

  • 2
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 《Unity3D网络游戏实战PDF》是一本非常重要的Unity3D开发书籍,它着重介绍了Unity3D游戏开发的网络部分。本书包含了网络游戏的核心概念、原理及实现方法,深入剖析了网络游戏的架构设计模式、游戏性能优化、安全防护等方面内容,并提供了实际的案例分析和设计思路,是网络游戏开发者的必备指南。 本书主要包括了以下内容:网络游戏的基础知识,如网络通信、协议、数据传输、数据整合等;Unity3D网络游戏的基本架构设计,如服务器端和客户端的架构设计、消息通信机制等;Unity3D游戏性能优化技巧,如消息压缩、消息缓存、负载均衡等;网络游戏的安全防护策略,如加密算法、防作弊、账号安全等;最后,作者还介绍了如何进行网络游戏的调试及问题排除。 《Unity3D网络游戏实战PDF》书籍内容深入浅出,适合初学者和中级开发者阅读。它提供了实用的方法和工具,帮助读者掌握网络游戏开发的技能,提高游戏开发的效率和质量。无论你是想开发网络游戏,还是想学习Unity3D游戏开发,本书都是不容错过的开发指南。 ### 回答2: unity3d网络游戏实战pdf是一本讲解使用Unity3D引擎开发网络游戏实战教程,内容涵盖了Unity3D的基础知识、网络编程与多人联机游戏设计。本书从基础开始,逐步讲解如何使用Unity3D引擎搭建网络游戏,包括如何进行多人联机游戏设计、实现网络通信,以及如何利用Unity3D引擎的特性实现游戏的界面设计和多人对战功能。本书不仅具有理论知识,更有大量实例和案例,可以帮助读者深刻了解Unity3D引擎的应用,提高网络游戏开发的技能和水平。这本书适合想要学习和掌握Unity3D引擎开发网络游戏的初、中级程序员、游戏开发者和爱好者阅读使用,可以帮助他们快速地了解并掌握开发网络游戏的流程和技巧。总之,如果你正在寻找一本全面且系统的Unity3D网络游戏开发教程,那么这本《unity3d网络游戏实战pdf》将是你的不二选择。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值