【学习总结】《Unity3D网络游戏》Part 1

以前都是在客户端这边研究一些逻辑和画面效果,乘着大四最后一个寒假学习一些服务端和客户端交互的内容,正好买了这本书,边看边做边写笔记,希望能用到毕设里面

另外多提一句。微软居然把暴雪收购了!!我超,这下压力来到了索尼这边

目录

Socket相关

socket概念

网络上的两个程序通过一个双侠奴工的通信连接实现数据交换,这个连接的一端称为一个Socket。一个Socket包含了进行网络通信必需的五种信息:
连接使用的协议、本地主机的IP地址、本地的协议端口、远程主机的IP地址和远程协议端口
【学习总结】《Unity3D网络游戏》Part 1

Socket通信的流程

  1. 开启一个连接之前,需要创建一个Socket对象(使用API Socket),然后绑定本地使用的端口(使用API Bind)。对服务端而言,绑定的步骤相当于给手机插上SIM卡,确定了“手机号”。对客户端而言,连接时(使用API Connect)会由系统分配端口,可以省去绑定的步骤
  2. 服务端开启监听(使用API Listen),等待客户端接入。相当于电话开机,等待别人呼叫
  3. 客户端连接服务器(使用API Connect),相当于手机拨号
  4. 服务器接收连接(使用API Accept),相当于接听电话并说出“喂”
  5. 客户端和服务端通过Send和Receive等API收发数据,操作系统会自动完成数据的确认、重传等步骤,确保传输的数据准确无误
  6. 某一方关闭连接(使用API Close),操作系统会执行“四次挥手”的步骤,关闭双方连接,相当于挂断电话

【学习总结】《Unity3D网络游戏》Part 1
上图画出了整个通信流程,而后续的代码基本就是根据这个流程图来写的

TCP和UDP协议

TCP是一种面向连接的、可靠的、基于字节流的传输层通信协议,与TCP相对应的UDP协议是无连接的、不可靠的、但传输效率较高的协议

Unity异步Echo程序

客户端部分

首先给使用UGUI搭建一个简单的界面
【学习总结】《Unity3D网络游戏》Part 1
客户端代码如下

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

public class Echo : MonoBehaviour
{
    Socket socket;//定义套接字

    //UGUI 这里声明界面元素
    public InputField inputField;
    public Text text;

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

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

    //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);//这里是一个递归的调用
        }
        catch(SocketException ex){
            Debug.Log("Socket Receive fail" + ex.ToString());
        }
    }

    //点击发送按钮
    public void Send()
    {
        //Send
        string sendStr = inputField.text;//得到发送的消息
        byte[] sendBytes = System.Text.Encoding.Default.GetBytes(sendStr);
        socket.BeginSend(sendBytes,0,sendBytes.Length,0,SendCallback,socket);
        // socket.Send(sendBytes);
    }

    //Send回调
    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());
        }
    }

    private void Update() 
    {
        text.text = recvStr;
    }
}

注释部分

  1. Socket(AddressFamily.InterNetwork,SocketType.Stream,ProtocolType.Tcp);
    用于创建一个Socket对象,它的三个参数分别代表地址族,套接字类型和协议。地址族知名使用的是IPV4还是IPV6,
    InterNetwork代表IPV4,InterNetworkV6代表IPV6。
    SocketType是套接字类型,游戏中最常用的是字节流套接字,即Stream。ProtocolType指明协议

  2. 客户端通过socket.Connect(远程IP地址,远程端口)连接服务端。
    Connect是一个阻塞方法,程序会卡住直到服务端回应(接受、拒绝或者超时)

  3. 客户端通过socket.send发送数据。GetBytes(字符串)把字符串转换成byte这也是一个阻塞方法。该方法接受一个byte[]类型的参数指明要发送的内容。Send的返回值指明发送数据的长度程序用System.Text.Encoding.Default.[]数组,然后发送给服务端

  4. 客户端使用socket.Receive接受服务端数据。
    Receive也是阻塞方法,没有收到服务端的数据时,程序将卡在Receive不会往下执行
    Receive带有一个byte[]类型的参数,它存储接收到的数据
    Receive的返回值指明接收到的数据的长度。之后使用System.Text.Encoding.Default.GetString(readBuff,0,count)将byte[]

  5. 通过socket.close()关闭连接

  6. 通过BeginConnect和EndConnect来让客户端代码变成异步进行,防止程序卡死
    IAsyncResult是.NET提供的一种异步操作,通过名为BeginXXX和EndXXX的两个方法来为实现原本同步方法的异步调用
    BeginXXX方法中包含同步方法中所需的参数,此外还包含两个参数:一个AsyncCallback委托和一个用户定义的状态对象委托用来调用回调方法,状态对象用来向回调方法传递状态信息,且BeginXXX方法返回一个实现IAsyncResult接口的对象,EndXXX方法用于结束异步操作并且返回结果
    EndXXX方法含有一个IAsyncResult参数,用于获取异步操作是否完成的信息,它的返回值与同步方法相同

  7. BeginReceive的参数为(readBuff,0,1024,ReceiveCallback,socket)
    第一个参数readBuff表示接收缓冲区,第二个参数0表示从readBuff第0位开始接收数据,这个参数和TCP粘包问题有关
    第三个参数1024代表每次最多接收1024个字节,假如服务端回应一串长长的数据,那一次也只会收到1024个字节

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

  9. Update和recvStr
    在Unity中,只有主线程可以操作UI组件。由于异步回调是在其他线程执行的,如果在BeginReceive给text.text赋值,Unity会弹出异常信息,所以只能在主线程中

  10. 异步BeginSend参数说明
    buffer Byte类型的数组,包含需要发送的数据
    offset 从Buffer中的offset位置开始发送
    size 将要发送的字节数
    socketFlags SocetFlags值的按位组合,这里设置为0
    callback 回调函数,一个AsyncCallbakc委托
    state 一个用户定义对象,其中包含发送操作的相关信息。当操作完成时,此对象会传递给EndSend委托

服务器部分

服务器这边使用Visual Studio建立一个C#控制台程序,代码如下

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

namespace EchoServer
{
    //该类用于保存客户端的信息
    class ClientState
    {
        public Socket socket;
        public byte[] readBuff = new byte[1024];
    }

    class Program
    {
        //监听Socket
        static Socket listenfd;

        //定义一个字典集合保存所有客户端的信息,这样的结构可以通过clientState = clients[Socket]进行快速访问
        static Dictionary<Socket, ClientState> clients = new Dictionary<Socket, ClientState>();

        static void Main(string[] args)
        {
            Console.WriteLine("Hello world");

            //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("[服务器]启动成功");

            //Accept
            listenfd.BeginAccept(AccpetCallback,listenfd);

            //waiting
            Console.ReadLine();
        }

        //Accept回调
        //处理三件事情:
        //1.给新的连接分配ClientState,并且把它添加到clients列表之中
        //2.异步接收客户端数据
        //3.再次调用BeginAccept实现循环
        public static void AccpetCallback(IAsyncResult ar)
        {
            try
            {
                Console.WriteLine("[服务器]Accept");
                Socket listenfd = (Socket)ar.AsyncState;
                Socket clientfd = listenfd.EndAccept(ar);

                //clients列表,将连接上的客户端Socket加入列表
                ClientState state = new ClientState();
                state.socket = clientfd;
                clients.Add(clientfd,state);

                //接收数据BeginReceive
                clientfd.BeginReceive(state.readBuff,0,1024,0,ReceiveCallback,state);

                //继续接收Accept
                listenfd.BeginAccept(AccpetCallback,listenfd);
            }
            catch(SocketException ex)
            {
                Console.WriteLine("Socket Accept fail" + ex.ToString());
            }
        }

        //ReceiveCallBack是BeginReceive的回调函数,它也处理了三件事情
        //1.服务端收到消息之后,回应客户端
        //2.如果收到客户端关闭连接的信号“if(count==0)”,断开连接
        //3.继续调用BeginReceive接收下一个数据
        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);
            }
            catch(SocketException ex)
            {
                Console.WriteLine("Socket Receive fail" + ex.ToString());
            }
        }
    }
}

//注释部分
//1.绑定Bind
//listenfd.Bind(ipEp)将给listenfd套接字绑定Ip和端口,这里也可以更改为真实的IP地址,一样有效

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

//3.应答Accept
//开启监听后,服务器调用listenfd.Accept()接受客户端连接。Accept()返回了一个新客户端的Socket对象,对于服务器来说,它有一个监听Socket,用来监听和应答客户端的连接
//对每一个客户端还有一个专门的Socket用来处理该客户端的数据

//4.IPAddress和IPEndPoint
//使用IPAddress指定IP地址,使用IPEndPoint指定IP和端口

//5.Receive方法将接收到的字节流保存到readBuff上

测试

【学习总结】《Unity3D网络游戏》Part 1

上一篇:字符串处理


下一篇:通过v-for创建的a-tabs组件,不展示默认第一项操作