Unity的TCP同步通信

1.Socket中的重要API

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

public class Lesson5 : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        #region 知识点一 Socket套接字的作用
        //它是C#提供给我们用于网络通信的一个类(在其它语言当中也有对应的Socket类)
        //类名:Socket
        //命名空间:System.Net.Sockets

        //Socket套接字是支持TCP/IP网络通信的基本操作单位
        //一个套接字对象包含以下关键信息
        //1.本机的IP地址和端口
        //2.对方主机的IP地址和端口
        //3.双方通信的协议信息

        //一个Sccket对象表示一个本地或者远程套接字信息
        //它可以被视为一个数据通道
        //这个通道连接与客户端和服务端之间
        //数据的发送和接受均通过这个通道进行

        //一般在制作长连接游戏时,我们会使用Socket套接字作为我们的通信方案
        //我们通过它连接客户端和服务端,通过它来收发消息
        //你可以把它抽象的想象成一根管子,插在客户端和服务端应用程序上,通过这个管子来传递交换信息
        #endregion

        #region 知识点二 Socket的类型
        //Socket套接字有3种不同的类型
        //1.流套接字
        //  主要用于实现TCP通信,提供了面向连接、可靠的、有序的、数据无差错且无重复的数据传输服务
        //2.数据报套接字
        //  主要用于实现UDP通信,提供了无连接的通信服务,数据包的长度不能大于32KB,不提供正确性检查,不保证顺序,可能出现重发、丢失等情况
        //3.原始套接字(不常用,不深入讲解)
        //  主要用于实现IP数据包通信,用于直接访问协议的较低层,常用于侦听和分析数据包

        //通过Socket的构造函数 我们可以申明不同类型的套接字
        //Socket s = new Socket()
        //参数一:AddressFamily 网络寻址 枚举类型,决定寻址方案
        //  常用:
        //  1.InterNetwork  IPv4寻址
        //  2.InterNetwork6 IPv6寻址
        //  做了解:
        //  1.UNIX          UNIX本地到主机地址 
        //  2.ImpLink       ARPANETIMP地址
        //  3.Ipx           IPX或SPX地址
        //  4.Iso           ISO协议的地址
        //  5.Osi           OSI协议的地址
        //  7.NetBios       NetBios地址
        //  9.Atm           本机ATM服务地址

        //参数二:SocketType 套接字枚举类型,决定使用的套接字类型
        //  常用:
        //  1.Dgram         支持数据报,最大长度固定的无连接、不可靠的消息(主要用于UDP通信)
        //  2.Stream        支持可靠、双向、基于连接的字节流(主要用于TCP通信)
        //  做了解:
        //  1.Raw           支持对基础传输协议的访问
        //  2.Rdm           支持无连接、面向消息、以可靠方式发送的消息
        //  3.Seqpacket     提供排序字节流的面向连接且可靠的双向传输

        //参数三:ProtocolType 协议类型枚举类型,决定套接字使用的通信协议
        //  常用:
        //  1.TCP           TCP传输控制协议
        //  2.UDP           UDP用户数据报协议
        //  做了解:
        //  1.IP            IP网际协议
        //  2.Icmp          Icmp网际消息控制协议
        //  3.Igmp          Igmp网际组管理协议
        //  4.Ggp           网关到网关协议
        //  5.IPv4          Internet协议版本4
        //  6.Pup           PARC通用数据包协议
        //  7.Idp           Internet数据报协议
        //  8.Raw           原始IP数据包协议
        //  9.Ipx           Internet数据包交换协议
        //  10.Spx          顺序包交换协议
        //  11.IcmpV6       用于IPv6的Internet控制消息协议

        //2、3参数的常用搭配:
        //       SocketType.Dgram  +  ProtocolType.Udp  = UDP协议通信(常用,主要学习)
        //       SocketType.Stream  +  ProtocolType.Tcp  = TCP协议通信(常用,主要学习)
        //       SocketType.Raw  +  ProtocolType.Icmp  = Internet控制报文协议(了解)
        //       SocketType.Raw  +  ProtocolType.Raw  = 简单的IP包通信(了解)

        //我们必须掌握的
        //TCP流套接字
        Socket socketTcp = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

        //UDP数据报套接字
        Socket socketUdp = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
        #endregion

        #region 知识点三 Socket的常用属性
        //1.套接字的连接状态
        if(socketTcp.Connected)
        {

        }
        //2.获取套接字的类型
        print(socketTcp.SocketType);
        //3.获取套接字的协议类型
        print(socketTcp.ProtocolType);
        //4.获取套接字的寻址方案
        print(socketTcp.AddressFamily);

        //5.从网络中获取准备读取的数据数据量
        print(socketTcp.Available);

        //6.获取本机EndPoint对象(注意 :IPEndPoint继承EndPoint)
        //socketTcp.LocalEndPoint as IPEndPoint

        //7.获取远程EndPoint对象
        //socketTcp.RemoteEndPoint as IPEndPoint
        #endregion

        #region 知识点四 Socket的常用方法
        //1.主要用于服务端
        //  1-1:绑定IP和端口
        IPEndPoint ipPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 8080);
        socketTcp.Bind(ipPoint);
        //  1-2:设置客户端连接的最大数量
        socketTcp.Listen(10);
        //  1-3:等待客户端连入
        socketTcp.Accept();

        //2.主要用于客户端
        //  1-1:连接远程服务端
        socketTcp.Connect(IPAddress.Parse("118.12.123.11"), 8080);

        //3.客户端服务端都会用的
        //  1-1:同步发送和接收数据
        //  1-2:异步发送和接收数据
        //  1-3:释放连接并关闭Socket,先与Close调用
        socketTcp.Shutdown(SocketShutdown.Both);
        //  1-4:关闭连接,释放所有Socket关联资源
        socketTcp.Close();
        #endregion

        #region 总结
        //这节课我们只是对Socket有一个大体的认识
        //主要要建立的概念就是
        //TCP和UDP两种长连接通信方案都是基于Socket套接字的
        //我们之后只需要使用其中的各种方法,就可以进行网络连接和网络通信了
        //这节课必须掌握的内容就是如何声明TCP和UDP的Socket套接字
        #endregion
    }


    // Update is called once per frame
    void Update()
    {
        
    }
}

2.TCP同步通信基本写法

注意:如果需要在两台PC上使用以下代码测试通信需要确保两台PC处于统一局域网(连接同一WIFI),并将服务器与客户端代码中的本机回环IP(127.0.0.1)改为作为服务器PC的IP地址(可以在cmd中输入ipconfig查询)

2.1服务端

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

namespace TeachTcpServer
{
    class Program
    {
        static void Main(string[] args)
        {
            #region 知识点一 回顾服务端需要做的事情
            //1.创建套接字Socket
            //2.用Bind方法将套接字与本地地址绑定
            //3.用Listen方法监听
            //4.用Accept方法等待客户端连接
            //5.建立连接,Accept返回新套接字
            //6.用Send和Receive相关方法收发数据
            //7.用Shutdown方法释放连接
            //8.关闭套接字
            #endregion

            #region 知识点二 实现服务端基本逻辑
            //1.创建套接字Socket(TCP)
            Socket socketTcp = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            //2.用Bind方法将套接字与本地地址绑定
            try
            {
                IPEndPoint ipPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 8080);
                socketTcp.Bind(ipPoint);
            }
            catch (Exception e)
            {
                Console.WriteLine("绑定报错" + e.Message);
                return;
            }
            //3.用Listen方法监听
            socketTcp.Listen(1024);
            Console.WriteLine("服务端绑定监听结束,等待客户端连入");
            //4.用Accept方法等待客户端连接
            //5.建立连接,Accept返回新套接字
            Socket socketClient = socketTcp.Accept();
            Console.WriteLine("有客户端连入了");
            //6.用Send和Receive相关方法收发数据
            //发送
            socketClient.Send(Encoding.UTF8.GetBytes("欢迎连入服务端"));
            //接受
            byte[] result = new byte[1024];
            //返回值为接受到的字节数
            int receiveNum = socketClient.Receive(result);
            Console.WriteLine("接受到了{0}发来的消息:{1}",
                socketClient.RemoteEndPoint.ToString(),
                Encoding.UTF8.GetString(result, 0, receiveNum));

            //7.用Shutdown方法释放连接
            socketClient.Shutdown(SocketShutdown.Both);
            //8.关闭套接字
            socketClient.Close();
            #endregion

            #region 总结
            //1.服务端开启的流程每次都是相同的
            //2.服务端的 Accept、Send、Receive是会阻塞主线程的,要等到执行完毕才会继续执行后面的内容
            //抛出问题:
            //如何让服务端可以服务n个客户端?
            //我们会在之后的综合练习题进行讲解
            #endregion

            Console.WriteLine("按任意键退出");
            Console.ReadKey();
        }
    }
}

2.2客户端

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

public class Lesson6 : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        #region 知识点一 回顾客户端需要做的事情
        //1.创建套接字Socket
        //2.用Connect方法与服务端相连
        //3.用Send和Receive相关方法收发数据
        //4.用Shutdown方法释放连接
        //5.关闭套接字
        #endregion

        #region 知识点二 实现客户端基本逻辑
        //1.创建套接字Socket
        Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        //2.用Connect方法与服务端相连
        //确定服务端的IP和端口
        IPEndPoint ipPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 8080);
        try
        {
            socket.Connect(ipPoint);
        }
        catch (SocketException e)
        {
            if (e.ErrorCode == 10061)
                print("服务器拒绝连接");
            else
                print("连接服务器失败" + e.ErrorCode);
            return;
        }
        //3.用Send和Receive相关方法收发数据

        //接收数据
        byte[] receiveBytes = new byte[1024];
        int receiveNum = socket.Receive(receiveBytes);
        print("收到服务端发来的消息:" + Encoding.UTF8.GetString(receiveBytes, 0, receiveNum));

        //发送数据
        socket.Send(Encoding.UTF8.GetBytes("你好,我是唐老狮的客户端"));

        //4.用Shutdown方法释放连接
        socket.Shutdown(SocketShutdown.Both);
        //5.关闭套接字
        socket.Close();
        #endregion

        #region 总结
        //1.客户端连接的流程每次都是相同的
        //2.客户端的 Connect、Send、Receive是会阻塞主线程的,要等到执行完毕才会继续执行后面的内容
        //抛出问题:
        //如何让客户端的Socket不影响主线程,并且可以随时收发消息?
        //我们会在之后的综合练习题讲解
        #endregion
    }

    // Update is called once per frame
    void Update()
    {
        
    }
}

3.TCP同步通信服务器与多个客户端连接

3.1服务端

3.1.1封装服务端套接字

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

namespace TeachTcpServerExercises2
{
    class ServerSocket
    {
        //服务端Socket
        public Socket socket;
        //客户端连接的所有Socket
        public Dictionary clientDic = new Dictionary();

        private bool isClose;

        //开启服务器端
        public void Start(string ip, int port, int num)
        {
            isClose = false;
            socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            IPEndPoint ipPoint = new IPEndPoint(IPAddress.Parse(ip), port);
            socket.Bind(ipPoint);
            socket.Listen(num);
            ThreadPool.QueueUserWorkItem(Accept);
            ThreadPool.QueueUserWorkItem(Receive);
        }

        //关闭服务器端
        public void Close()
        {
            isClose = true;
            foreach (ClientSocket client in clientDic.Values)
            {
                client.Close();
            }
            clientDic.Clear();

            socket.Shutdown(SocketShutdown.Both);
            socket.Close();
            socket = null;
        }

        //接受客户端连入
        private void Accept(object obj)
        {
            while (!isClose)
            {
                try
                {
                    //连入一个客户端
                    Socket clientSocket = socket.Accept();
                    ClientSocket client = new ClientSocket(clientSocket);
                    client.Send("欢迎连入服务器");
                    clientDic.Add(client.clientID, client);
                }
                catch (Exception e)
                {
                    Console.WriteLine("客户端连入报错" + e.Message);
                }
            }
        }
        //接收客户端消息
        private void Receive(object obj)
        {
            while (!isClose)
            {
                if(clientDic.Count > 0)
                {
                    foreach (ClientSocket client in clientDic.Values)
                    {
                        client.Receive();
                    }
                }
            }
        }

        public void Broadcast(string info)
        {
            foreach (ClientSocket client in clientDic.Values)
            {
                client.Send(info);
            }
        }
    }
}

3.1.2封装连接进来的客户端套接字方便管理

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

namespace TeachTcpServerExercises2
{
    class ClientSocket
    {
        private static int CLIENT_BEGIN_ID = 1;
        public int clientID;
        public Socket socket;

        public ClientSocket(Socket socket)
        {
            this.clientID = CLIENT_BEGIN_ID;
            this.socket = socket;
            ++CLIENT_BEGIN_ID;
        }

        /// 
        /// 是否是连接状态
        /// 
        public bool Connected => this.socket.Connected;

        //我们应该封装一些方法
        //关闭
        public void Close()
        {
            if(socket != null)
            {
                socket.Shutdown(SocketShutdown.Both);
                socket.Close();
                socket = null;
            }
        }
        //发送
        public void Send(string info)
        {
            if(socket != null)
            {
                try
                {
                    socket.Send(Encoding.UTF8.GetBytes(info));
                }
                catch(Exception e)
                {
                    Console.WriteLine("发消息出错" + e.Message);
                    Close();
                }
            }
                
        }
        //接收
        public void Receive()
        {
            if (socket == null)
                return;
            try
            {
                if(socket.Available > 0)
                {
                    byte[] result = new byte[1024 * 5];
                    int receiveNum = socket.Receive(result);
                    ThreadPool.QueueUserWorkItem(MsgHandle, Encoding.UTF8.GetString(result, 0, receiveNum));
                }
            }
            catch (Exception e)
            {
                Console.WriteLine("收消息出错" + e.Message);
                Close();
            }
        }

        private void MsgHandle(object obj)
        {
            string str = obj as string;
            Console.WriteLine("收到客户端{0}发来的消息:{1}", this.socket.RemoteEndPoint, str);
        }

    }
}

3.1.3服务端程序入口

using System;

namespace TeachTcpServerExercises2
{
    class Program
    {
        static void Main(string[] args)
        {
            ServerSocket socket = new ServerSocket();
            socket.Start("127.0.0.1", 8080, 1024);
            Console.WriteLine("服务器开启成功");
            while (true)
            {
                string input = Console.ReadLine();
                if(input == "Quit")
                {
                    socket.Close();
                }
                else if( input.Substring(0,2) == "B:" )
                {
                    socket.Broadcast(input.Substring(2));
                }
            }
        }
    }
}

3.2客户端

3.2.1客户端网络管理类

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

public class NetMgr : MonoBehaviour
{
    private static NetMgr instance;

    public static NetMgr Instance => instance;

    //客户端Socket
    private Socket socket;
    //用于发送消息的队列 公共容器 主线程往里面放 发送线程从里面取
    private Queue sendMsgQueue = new Queue();
    //用于接收消息的对象 公共容器 子线程往里面放 主线程从里面取
    private Queue receiveQueue = new Queue();

    //用于收消息的水桶(容器)
    private byte[] receiveBytes = new byte[1024 * 1024];
    //返回收到的字节数
    private int receiveNum;

    //是否连接
    private bool isConnected = false;

    void Awake()
    {
        instance = this;
        DontDestroyOnLoad(this.gameObject);
    }

    // Update is called once per frame
    void Update()
    {
        if(receiveQueue.Count > 0)
        {
            print(receiveQueue.Dequeue());
        }
    }

    //连接服务端
    public void Connect(string ip, int port)
    {
        //如果是连接状态 直接返回
        if (isConnected)
            return;

        if (socket == null)
            socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        //连接服务端
        IPEndPoint ipPoint = new IPEndPoint(IPAddress.Parse(ip), port);
        try
        {
            socket.Connect(ipPoint);
            isConnected = true;
            //开启发送线程
            ThreadPool.QueueUserWorkItem(SendMsg);
            //开启接收线程
            ThreadPool.QueueUserWorkItem(ReceiveMsg);
        }
        catch (SocketException e)
        {
            if (e.ErrorCode == 10061)
                print("服务器拒绝连接");
            else
                print("连接失败" + e.ErrorCode + e.Message);
        }
    }

    //发送消息
    public void Send(string info)
    {
        sendMsgQueue.Enqueue(info);
    }

    private void SendMsg(object obj)
    {
        while (isConnected)
        {
            if (sendMsgQueue.Count > 0)
            {
                socket.Send(Encoding.UTF8.GetBytes(sendMsgQueue.Dequeue()));
            }
        }
    }

    //不停的接受消息
    private void ReceiveMsg(object obj)
    {
        while (isConnected)
        {
            if(socket.Available > 0)
            {
                receiveNum = socket.Receive(receiveBytes);
                //收到消息 解析消息为字符串 并放入公共容器
                receiveQueue.Enqueue(Encoding.UTF8.GetString(receiveBytes, 0, receiveNum));
            }    
        }
    }

    public void Close()
    {
        if(socket != null)
        {
            socket.Shutdown(SocketShutdown.Both);
            socket.Close();

            isConnected = false;
        }
    }

    private void OnDestroy()
    {
        Close();
    }
}

3.2.2自动在场景中创建NetMgr物体

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

public class Main : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        if(NetMgr.Instance == null)
        {
            GameObject obj = new GameObject("Net");
            obj.AddComponent();
        }

        NetMgr.Instance.Connect("127.0.0.1", 8080);
    }

    // Update is called once per frame
    void Update()
    {
        
    }
}

4.消息分区

4.1.1数据基类

在网络中传输的消息都继承自这个类

提供了序列化,反序列化各个基础数据类型的方法

自定义数据只需要继承该类重写Writing、Reading、GetNum方法,使用提供的基础类型序列化/反序列化方法即可实现自定义数据类型的序列化/反序列化

using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using UnityEngine;

public abstract class BaseData
{
    /// 
    /// 用于子类重写的 获取字节数组容器大小的方法
    /// 
    /// 
    public abstract int GetBytesNum();

    /// 
    /// 把成员变量 序列化为 对应的字节数组
    /// 
    /// 
    public abstract byte[] Writing();

    /// 
    /// 把2进制字节数组 反序列化到 成员变量当中
    /// 
    /// 反序列化使用的字节数组
    /// 从该字节数组的第几个位置开始解析 默认是0
    public abstract int Reading(byte[] bytes, int beginIndex = 0);

    /// 
    /// 存储int类型变量到指定的字节数组当中
    /// 
    /// 指定字节数组
    /// 具体的int值
    /// 每次存储后用于记录当前索引位置的变量
    protected void WriteInt(byte[] bytes, int value, ref int index)
    {
        BitConverter.GetBytes(value).CopyTo(bytes, index);
        index += sizeof(int);
    }
    protected void WriteShort(byte[] bytes, short value, ref int index)
    {
        BitConverter.GetBytes(value).CopyTo(bytes, index);
        index += sizeof(short);
    }
    protected void WriteLong(byte[] bytes, long value, ref int index)
    {
        BitConverter.GetBytes(value).CopyTo(bytes, index);
        index += sizeof(long);
    }
    protected void WriteFloat(byte[] bytes, float value, ref int index)
    {
        BitConverter.GetBytes(value).CopyTo(bytes, index);
        index += sizeof(float);
    }
    protected void WriteByte(byte[] bytes, byte value, ref int index)
    {
        bytes[index] = value;
        index += sizeof(byte);
    }
    protected void WriteBool(byte[] bytes, bool value, ref int index)
    {
        BitConverter.GetBytes(value).CopyTo(bytes, index);
        index += sizeof(bool);
    }
    protected void WriteString(byte[] bytes, string value, ref int index)
    {
        //先存储string字节数组的长度
        byte[] strBytes = Encoding.UTF8.GetBytes(value);
        //BitConverter.GetBytes(strBytes.Length).CopyTo(bytes, index);
        //index += sizeof(int);
        WriteInt(bytes, strBytes.Length, ref index);
        //再存 string字节数组
        strBytes.CopyTo(bytes, index);
        index += strBytes.Length;
    }
    protected void WriteData(byte[] bytes, BaseData data, ref int index)
    {
        data.Writing().CopyTo(bytes, index);
        index += data.GetBytesNum();
    }

    /// 
    /// 根据字节数组 读取整形
    /// 
    /// 字节数组
    /// 开始读取的索引数
    /// 
    protected int ReadInt(byte[] bytes, ref int index)
    {
        int value = BitConverter.ToInt32(bytes, index);
        index += sizeof(int);
        return value;
    }
    protected short ReadShort(byte[] bytes, ref int index)
    {
        short value = BitConverter.ToInt16(bytes, index);
        index += sizeof(short);
        return value;
    }
    protected long ReadLong(byte[] bytes, ref int index)
    {
        long value = BitConverter.ToInt64(bytes, index);
        index += sizeof(long);
        return value;
    }
    protected float ReadFloat(byte[] bytes, ref int index)
    {
        float value = BitConverter.ToSingle(bytes, index);
        index += sizeof(float);
        return value;
    }
    protected byte ReadByte(byte[] bytes, ref int index)
    {
        byte value = bytes[index];
        index += sizeof(byte);
        return value;
    }
    protected bool ReadBool(byte[] bytes, ref int index)
    {
        bool value = BitConverter.ToBoolean(bytes, index);
        index += sizeof(bool);
        return value;
    }
    protected string ReadString(byte[] bytes, ref int index)
    {
        //首先读取长度
        int length = ReadInt(bytes, ref index);
        //再读取string
        string value = Encoding.UTF8.GetString(bytes, index, length);
        index += length;
        return value;
    }
    protected T ReadData(byte[] bytes, ref int index) where T:BaseData,new()
    {
        T value = new T();
        index += value.Reading(bytes, index);
        return value;
    }
}

(更新中)

你可能感兴趣的:(Unity的TCP同步通信)