Bootstrap

UNITY-网络通信(TCP/UDP)

IP和端口

        //我们知道想要进行网络通信,进行网络连接
        //首先我们需要找到对应设备,IP和端口号是定位网络中设备必不可少的关键元素
        //C#中提供了对应的IP和端口相关的类 来声明对应信息

IPAddress类

        //命名空间:System.Net;
        //类名:IPAddress

        //初始化IP信息的方式

        //1.用byte数组进行初始化
        byte[] ipAddress = new byte[] { 118, 102, 111, 11 };
        IPAddress ip1 = new IPAddress(ipAddress);

        //2.用long长整型进行初始化
        //4字节对应的长整型 一般不建议大家使用
        IPAddress ip2 = new IPAddress(0x79666F0B);

        //3.推荐使用的方式 使用字符串转换
        IPAddress ip3 = IPAddress.Parse("118.102.111.11");

        //特殊IP地址
        //127.0.0.1代表本机地址

        //一些静态成员
        //获取可用的IPv6地址
        //IPAddress.IPv6Any

IPEndPoint类

        //命名空间:System.Net;
        //类名:IPEndPoint
        //IPEndPoint类将网络端点表示为IP地址和端口号,表现为IP地址和端口号的组合

        //初始化方式
        IPEndPoint ipPoint = new IPEndPoint(0x79666F0B, 8080);

        IPEndPoint ipPoint2 = new IPEndPoint(IPAddress.Parse("118.102.111.11"), 8080);

域名解析

        //域名解析就是域名到IP地址的转换过程。域名的解析工作由DNS服务器完成
        //我们在进行通信时有时会有需求通过域名获取IP

IPHostEntry类

        //命名空间:System.Net
        //类名:IPHostEntry
        //主要作用:域名解析后的返回值 可以通过该对象获取IP地址、主机名等等信息
        //该类不会自己声明,都是作为某些方法的返回值返回信息,我们主要通过该类对象获取返回的信息

        //获取关联IP       成员变量:AddressList
        //获取主机别名列表  成员变量:Aliases
        //获取DNS名称      成员变量:HostName

Dns类

        //命名空间:System.Net
        //类名:Dns
        //主要作用:Dns是一个静态类,提供了很多静态方法,可以使用它来根据域名获取IP地址

        //常用方法
        //1.获取本地系统的主机名
        print(Dns.GetHostName());

        //2.获取指定域名的IP信息
        //根据域名获取
        //同步获取
        //注意:由于获取远程主机信息是需要进行网路通信,所以可能会阻塞主线程
        //IPHostEntry entry = Dns.GetHostEntry("www.baidu.com");
        //for (int i = 0; i < entry.AddressList.Length; i++)
        //{
        //    print("IP地址:" + entry.AddressList[i]);
        //}
        //for (int i = 0; i < entry.Aliases.Length; i++)
        //{
        //    print("主机别名" + entry.Aliases[i]);
        //}
        //print("DNS服务器名称" + entry.HostName);


        //异步获取
        GetHostEntry();
        private async void GetHostEntry()
        {
            Task<IPHostEntry> task = Dns.GetHostEntryAsync("www.baidu.com");
            await task;
            for (int i = 0; i < task.Result.AddressList.Length; i++)
            {
                print("IP地址:" + task.Result.AddressList[i]);
            }
            for (int i = 0; i < task.Result.Aliases.Length; i++)
            {
                print("主机别名" + task.Result.Aliases[i]);
            }
            print("DNS服务器名称" + task.Result.HostName);
        }

序列化

非字符串类型转字节数组

        //关键类:BitConverter
        //所在命名空间:System
        //主要作用:除字符串的其它常用类型和字节数组相互转换

        byte[] bytes = BitConverter.GetBytes(1);

字符串类型转字节数组

        //关键类:Encoding
        //所在命名空间:System.Text
        //主要作用:将字符串类型和字节数组相互转换,并且决定转换时使用的字符编码类型,网络通信时建议大家使用UTF-8类型
        byte[] byte2 = Encoding.UTF8.GetBytes("的卡萨福利卡决胜巅峰卡视角的副驾驶的");

将一个类对象转换为二进制

        //注意:网络通信中我们不能直接使用BinaryFormatter 2进制格式化类
        //因为客户端和服务器使用的语言可能不一样,BinaryFormatter是C#的序列化规则,和其它语言之间的兼容性不好
        //如果使用它,那么其它语言开发的服务器无法对其进行反序列化
        //我们需要自己来处理将类对象数据序列化为字节数组

        //字符串需要记录字符串的长度

public class PlayerInfo
{
    public int lev;
    public string name;
    public short atk;
    public bool sex;

    public byte[] GetBytes()
    {
        //1.得到的 这个类数据 如果转换成 字节数组 那么字节数组容器需要的容量
        int indexNum = sizeof(int) + //lev int类型  4
                      sizeof(int) + //代表 name字符串转换成字节数组后 数组的长度 4
                      Encoding.UTF8.GetBytes(name).Length + //字符串具体字节数组的长度
                      sizeof(short) + //atk short类型 2
                      sizeof(bool); //sex bool类型 1

         //2.申明一个装载信息的字节数组容器
        byte[] playerBytes = new byte[indexNum];
        int index = 0;//从 playerBytes数组中的第几个位置去存储数据

        //3.将对象中的所有信息转为字节数组并放入该容器当中(可以利用数组中的CopeTo方法转存字节数组)
        //CopyTo方法的第二个参数代表 从容器的第几个位置开始存储
        //等级
        BitConverter.GetBytes(lev).CopyTo(playerBytes, index);
        index += sizeof(int);

        //姓名
        byte[] strBytes = Encoding.UTF8.GetBytes(name);
        int num = strBytes.Length;
        //存储的是姓名转换成字节数组后 字节数组的长度
        BitConverter.GetBytes(num).CopyTo(playerBytes, index);
        index += sizeof(int);
        //存储字符串的字节数组
        strBytes.CopyTo(playerBytes, index);
        index += num;

        //攻击力
        BitConverter.GetBytes(atk).CopyTo(playerBytes, index);
        index += sizeof(short);
        //性别
        BitConverter.GetBytes(sex).CopyTo(playerBytes, index);
        index += sizeof(bool);

        return playerBytes;
    }
}

反序列化

字节数组转非字符串类型

        //关键类:BitConverter
        //所在命名空间:System
        //主要作用:除字符串的其它常用类型和字节数组相互转换
        byte[] bytes = BitConverter.GetBytes(99);
        int i = BitConverter.ToInt32(bytes, 0);
        print(i);

字节数组转字符串类型

        //关键类:Encoding
        //所在命名空间:System.Text
        //主要作用:将字符串类型和字节数组相互转换,并且决定转换时使用的字符编码类型,网络通信时建议大家使用UTF-8类型
        byte[] bytes2 = Encoding.UTF8.GetBytes("123123空间大撒了房间阿斯利康放大镜");
        string str = Encoding.UTF8.GetString(bytes2, 0, bytes2.Length);
        print(str);

将二进制数据转为一个类对象

类对象为序列化当中声明的PlayerInfo类,且封装了一个得到类对象字节数组的方法

        //1.获取到对应的字节数组
        PlayerInfo info = new PlayerInfo();
        info.lev = 10;
        info.name = "唐老狮";
        info.atk = 88;
        info.sex = false;

        byte[] playerBytes = info.GetBytes();

        //2.将字节数组按照序列化时的顺序进行反序列化(将对应字节分组转换为对应类型变量)
        PlayerInfo info2 = new PlayerInfo();
        //等级
        int index = 0;
        info2.lev = BitConverter.ToInt32(playerBytes, index);
        index += 4;
        print(info2.lev);
        //姓名的长度
        int length = BitConverter.ToInt32(playerBytes, index);
        index += 4;
        //姓名字符串
        info2.name = Encoding.UTF8.GetString(playerBytes, index, length);
        index += length;
        print(info2.name);
        //攻击力
        info2.atk = BitConverter.ToInt16(playerBytes, index);
        index += 2;
        print(info2.atk);
        //性别
        info2.sex = BitConverter.ToBoolean(playerBytes, index);
        index += 1;
        print(info2.sex);

Socket

Socket套接字的作用

        //它是C#提供给我们用于网络通信的一个类(在其它语言当中也有对应的Socket类)
        //类名:Socket
        //命名空间:System.Net.Sockets

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

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

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

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);

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

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();

TCP服务端

TCP服务端需要做的事情

            //1.创建套接字Socket
            //2.用Bind方法将套接字与本地地址绑定
            //3.用Listen方法监听
            //4.用Accept方法等待客户端连接
            //5.建立连接,Accept返回新套接字
            //6.用Send和Receive相关方法收发数据
            //7.用Shutdown方法释放连接
            //8.关闭套接字

最基础的实现方式

服务端的 Accept、Send、Receive是会阻塞主线程的,要等到执行完毕才会继续执行后面的内容

            //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();

TCP客户端

TCP客户端需要做的事情

        //1.创建套接字Socket
        //2.用Connect方法与服务端相连
        //3.用Send和Receive相关方法收发数据
        //4.用Shutdown方法释放连接
        //5.关闭套接字

实现客户端基本逻辑

        //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();

TCP常用异步通信方法(补充)

Socket TCP通信中的异步方法(Begin开头方法)

        //回调函数参数IAsyncResult
        //AsyncState 调用异步方法时传入的参数 需要转换
        //AsyncWaitHandle 用于同步等待

        Socket socketTcp = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        //服务器相关
        //BeginAccept
        //EndAccept
        socketTcp.BeginAccept(AcceptCallBack, socketTcp);
        private void AcceptCallBack(IAsyncResult result)
        {
            try
            {
                //获取传入的参数
                Socket s = result.AsyncState as Socket;
                //通过调用EndAccept就可以得到连入的客户端Socket
                Socket clientSocket = s.EndAccept(result);

                s.BeginAccept(AcceptCallBack, s);
            }
            catch (SocketException e)
            {
                print(e.SocketErrorCode);
            }
        }

        //客户端相关
        //BeginConnect
        //EndConnect
        IPEndPoint ipPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 8080);
        socketTcp.BeginConnect(ipPoint, (result) =>
        {
            Socket s = result.AsyncState as Socket;
            try
            {
                s.EndConnect(result);
                print("连接成功");
            }
            catch (SocketException e)
            {
                print("连接出错" + e.SocketErrorCode + e.Message);
            }

        }, socketTcp);


        //服务器客户端通用
        //接收消息
        //BeginReceive
        //EndReceive
        socketTcp.BeginReceive(resultBytes, 0, resultBytes.Length, SocketFlags.None, ReceiveCallBack, socketTcp);
        private void ReceiveCallBack(IAsyncResult result)
        {
            try
            {
                Socket s = result.AsyncState as Socket;
                //这个返回值是你受到了多少个字节
                int num = s.EndReceive(result);
                //进行消息处理
                Encoding.UTF8.GetString(resultBytes, 0, num);

                //我还要继续接受
                s.BeginReceive(resultBytes, 0, resultBytes.Length, SocketFlags.None, ReceiveCallBack, s);
            }
            catch (SocketException e)
            {
                print("接受消息处问题" + e.SocketErrorCode + e.Message);
            }
        }

        //发送消息
        //BeginSend
        //EndSend
        byte[] bytes = Encoding.UTF8.GetBytes("1231231231223123123");
        socketTcp.BeginSend(bytes, 0, bytes.Length, SocketFlags.None, (result) =>
        {
            try
            {
                socketTcp.EndSend(result);
                print("发送成功");
            }
            catch (SocketException e)
            {
                print("发送错误" + e.SocketErrorCode + e.Message);
            }
        }, socketTcp);

Socket TCP通信中的异步方法2(Async结尾方法)

        //关键变量类型
        //SocketAsyncEventArgs
        //它会作为Async异步方法的传入值
        //我们需要通过它进行一些关键参数的赋值

        //服务器端
        //AcceptAsync
        SocketAsyncEventArgs e = new SocketAsyncEventArgs();
        e.Completed += (socket, args) =>
        {
            //首先判断是否成功
            if (args.SocketError == SocketError.Success)
            {
                //获取连入的客户端socket
                Socket clientSocket = args.AcceptSocket;

                (socket as Socket).AcceptAsync(args);
            }
            else
            {
                print("连入客户端失败" + args.SocketError);
            }
        };
        socketTcp.AcceptAsync(e);

        //客户端
        //ConnectAsync
        SocketAsyncEventArgs e2 = new SocketAsyncEventArgs();
        e2.Completed += (socket, args) =>
        {
            if (args.SocketError == SocketError.Success)
            {
                //连接成功
            }
            else
            {
                //连接失败
                print(args.SocketError);
            }
        };
        socketTcp.ConnectAsync(e2);

        //服务端和客户端
        //发送消息
        //SendAsync
        SocketAsyncEventArgs e3 = new SocketAsyncEventArgs();
        byte[] bytes2 = Encoding.UTF8.GetBytes("123123的就是拉法基萨克两地分居");
        e3.SetBuffer(bytes2, 0, bytes2.Length);
        e3.Completed += (socket, args) =>
        {
            if (args.SocketError == SocketError.Success)
            {
                print("发送成功");
            }
            else
            {

            }
        };
        socketTcp.SendAsync(e3);

        //接受消息
        //ReceiveAsync
        SocketAsyncEventArgs e4 = new SocketAsyncEventArgs();
        //设置接受数据的容器,偏移位置,容量
        e4.SetBuffer(new byte[1024 * 1024], 0, 1024 * 1024);
        e4.Completed += (socket, args) =>
        {
            if(args.SocketError == SocketError.Success)
            {
                //收取存储在容器当中的字节
                //Buffer是容器
                //BytesTransferred是收取了多少个字节
                Encoding.UTF8.GetString(args.Buffer, 0, args.BytesTransferred);

                args.SetBuffer(0, args.Buffer.Length);
                //接收完消息 再接收下一条
                (socket as Socket).ReceiveAsync(args);
            }
            else
            {

            }
        };
        socketTcp.ReceiveAsync(e4);

        //C#中网络通信 异步方法中 主要提供了两种方案
        //1.Begin开头的API
        //内部开多线程,通过回调形式返回结果,需要和End相关方法 配合使用

        //2.Async结尾的API
        //内部开多线程,通过回调形式返回结果,依赖SocketAsyncEventArgs对象配合使用
        //可以让我们更加方便的进行操作

TCP客户端与服务端一般实现(同步)

问题

1.如何发送之前的自定义类的2进制信息

自己封装一个方法类,让每一个数据类继承并实现其中的方法

2.如何区分消息

        //为发送的信息添加标识,比如添加消息ID
        //在所有发送的消息的头部加上消息ID(int、short、byte、long都可以,根据实际情况选择)

3.如何处理分包黏包

在标识里面在添加长度信息,得到不同的长度信息之后进行不同的处理

4.客户端socket无论使用ShutDown和Close方法还是Disconnect方法主动断开连接,服务端都无法得知客户端已经断开

自定义退出消息 让服务端收到该消息就知道客户端想断开

5.客户端非正常退出时以及长期不发送消息,防火墙或者路由器会断开连接

在客户端与服务端当中实现心跳消息,固定时间产生联络

TCP服务端的实现(同步)

BaseDate为一个抽象类,封装了常用变量序列化与反序列化的方法,自定义数据类以及对应的数据管理类需继承实现其中的抽象方法,实现自定义数据的传输

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

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

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

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

    /// <summary>
    /// 存储int类型变量到指定的字节数组当中
    /// </summary>
    /// <param name="bytes">指定字节数组</param>
    /// <param name="value">具体的int值</param>
    /// <param name="index">每次存储后用于记录当前索引位置的变量</param>
    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();
    }

    /// <summary>
    /// 根据字节数组 读取整形
    /// </summary>
    /// <param name="bytes">字节数组</param>
    /// <param name="index">开始读取的索引数</param>
    /// <returns></returns>
    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<T>(byte[] bytes, ref int index) where T:BaseData,new()
    {
        T value = new T();
        index += value.Reading(bytes, index);
        return value;
    }
}

BaseMsg管理类 继承BaseDate  主要添加了GetID方法,方便不同处理类型的消息

using System.Collections;
using System.Collections.Generic;
public class BaseMsg : BaseData
{
    public override int GetBytesNum()
    {
        throw new System.NotImplementedException();
    }

    public override int Reading(byte[] bytes, int beginIndex = 0)
    {
        throw new System.NotImplementedException();
    }

    public override byte[] Writing()
    {
        throw new System.NotImplementedException();
    }

    public virtual int GetID()
    {
        return 0;
    }
}

玩家数据类 示范数据

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

/// <summary>
/// 玩家数据类
/// </summary>
public class PlayerData : BaseData
{
    public string name;
    public int atk;
    public int lev;

    public override int GetBytesNum()
    {
        return 4 + 4 + 4 + Encoding.UTF8.GetBytes(name).Length;
    }

    public override int Reading(byte[] bytes, int beginIndex = 0)
    {
        int index = beginIndex;
        name = ReadString(bytes, ref index);
        atk = ReadInt(bytes, ref index);
        lev = ReadInt(bytes, ref index);
        return index - beginIndex;
    }

    public override byte[] Writing()
    {
        int index = 0;
        byte[] bytes = new byte[GetBytesNum()];
        WriteString(bytes, name, ref index);
        WriteInt(bytes, atk, ref index);
        WriteInt(bytes, lev, ref index);
        return bytes;
    }
}

玩家管理类

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

public class PlayerMsg : BaseMsg
{
    public int playerID;
    public PlayerData playerData;
    public override byte[] Writing()
    {
        int index = 0;
        int bytesNum = GetBytesNum();
        byte[] bytes = new byte[bytesNum];
        //先写消息ID
        WriteInt(bytes, GetID(), ref index);
        //写如消息体的长度 我们-8的目的 是只存储 消息体的长度 前面8个字节 是我们自己定的规则 解析时按照这个规则处理就行了
        WriteInt(bytes, bytesNum - 8, ref index);
        //写这个消息的成员变量
        WriteInt(bytes, playerID, ref index);
        WriteData(bytes, playerData, ref index);
        return bytes;
    }

    public override int Reading(byte[] bytes, int beginIndex = 0)
    {
        //反序列化不需要去解析ID 因为在这一步之前 就应该把ID反序列化出来
        //用来判断到底使用哪一个自定义类来反序化
        int index = beginIndex;
        playerID = ReadInt(bytes, ref index);
        playerData = ReadData<PlayerData>(bytes, ref index);
        return index - beginIndex;
    }

    public override int GetBytesNum()
    {
        return 4 + //消息ID的长度
             4 + //消息体的长度
             4 + //playerID的字节数组长度
             playerData.GetBytesNum();//playerData的字节数组长度
    }

    /// <summary>
    /// 自定义的消息ID 主要用于区分是哪一个消息类
    /// </summary>
    /// <returns></returns>
    public override int GetID()
    {
        return 1001;
    }
}

退出消息管理类  无实质信息 当客户端退出时使用

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

public class QuitMsg : BaseMsg
{
    public override int GetBytesNum()
    {
        return 8;
    }

    public override int Reading(byte[] bytes, int beginIndex = 0)
    {
        return 0;
    }

    public override byte[] Writing()
    {
        int index = 0;
        byte[] bytes = new byte[GetBytesNum()];
        WriteInt(bytes, GetID(), ref index);
        WriteInt(bytes, 0, ref index);
        return bytes;
    }

    public override int GetID()
    {
        return 1003;
    }
}

心跳信息管理类 无实质信息 用于客户端与服务端的定时沟通

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

public class HeartMsg : BaseMsg
{
    public override int GetBytesNum()
    {
        return 8;
    }

    public override int Reading(byte[] bytes, int beginIndex = 0)
    {
        return 0;
    }

    public override byte[] Writing()
    {
        int index = 0;
        byte[] bytes = new byte[GetBytesNum()];
        WriteInt(bytes, GetID(), ref index);
        WriteInt(bytes, 0, ref index);
        return bytes;
    }

    public override int GetID()
    {
        return 999;
    }
}

服务端对于客户端Socket一些功能封装类

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;

        //用于处理分包时 缓存的 字节数组 和 字节数组长度
        private byte[] cacheBytes = new byte[1024 * 1024];
        private int cacheNum = 0;

        //上一次收到消息的时间
        private long frontTime = -1;
        //超时时间
        private static int TIME_OUT_TIME = 10;

        public ClientSocket(Socket socket)
        {
            this.clientID = CLIENT_BEGIN_ID;
            this.socket = socket;
            ++CLIENT_BEGIN_ID;
            //我们现在为了方便大家理解 所以开了一个线程专门计时 但是这种方式比较消耗性能 不建议这样使用
            //ThreadPool.QueueUserWorkItem(CheckTimeOut);
        }

        /// <summary>
        /// 间隔一段时间 检测一次超时 如果超时 就会主动断开该客户端的连接
        /// </summary>
        /// <param name="obj"></param>
        private void CheckTimeOut(/*object obj*/)
        {
            //while (Connected)
            //{
                if (frontTime != -1 &&
                DateTime.Now.Ticks / TimeSpan.TicksPerSecond - frontTime >= TIME_OUT_TIME)
                {
                    Program.socket.AddDelSocket(this);
                    //break;
                }
                //Thread.Sleep(5000);
            //}
        }

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


        //我们应该封装一些方法
        //关闭
        public void Close()
        {
            if(socket != null)
            {
                socket.Shutdown(SocketShutdown.Both);
                socket.Close();
                socket = null;
            }
        }
        //发送
        public void Send(BaseMsg info)
        {
            if (Connected)
            {
                try
                {
                    socket.Send(info.Writing());
                }
                catch (Exception e)
                {
                    Console.WriteLine("发消息出错" + e.Message);
                    Program.socket.AddDelSocket(this);
                }
            }
            else
                Program.socket.AddDelSocket(this);
        }
        //接收
        public void Receive()
        {
            if (!Connected)
            {
                Program.socket.AddDelSocket(this);
                return;
            }
            try
            {
                if(socket.Available > 0)
                {
                    byte[] result = new byte[1024 * 5];
                    int receiveNum = socket.Receive(result);
                    HandleReceiveMsg(result, receiveNum);
                    收到数据后 先读取4个字节 转为ID 才知道用哪一个类型去处理反序列化
                    //int msgID = BitConverter.ToInt32(result, 0);
                    //BaseMsg msg = null;
                    //switch (msgID)
                    //{
                    //    case 1001:
                    //        msg = new PlayerMsg();
                    //        msg.Reading(result, 4);
                    //        break;
                    //}
                    //if (msg == null)
                    //    return;
                    //ThreadPool.QueueUserWorkItem(MsgHandle, msg);
                }

                //检测 是否超时 
                CheckTimeOut();
            }
            catch (Exception e)
            {
                Console.WriteLine("收消息出错" + e.Message);
                //解析消息出错 也认为 要把socket断开了
                Program.socket.AddDelSocket(this);
            }
        }

        //处理接受消息 分包、黏包问题的方法
        private void HandleReceiveMsg(byte[] receiveBytes, int receiveNum)
        {
            int msgID = 0;
            int msgLength = 0;
            int nowIndex = 0;

            //收到消息时 应该看看 之前有没有缓存的 如果有的话 我们直接拼接到后面
            receiveBytes.CopyTo(cacheBytes, cacheNum);
            cacheNum += receiveNum;

            while (true)
            {
                //每次将长度设置为-1 是避免上一次解析的数据 影响这一次的判断
                msgLength = -1;
                //处理解析一条消息
                if (cacheNum - nowIndex >= 8)
                {
                    //解析ID
                    msgID = BitConverter.ToInt32(cacheBytes, nowIndex);
                    nowIndex += 4;
                    //解析长度
                    msgLength = BitConverter.ToInt32(cacheBytes, nowIndex);
                    nowIndex += 4;
                }

                if (cacheNum - nowIndex >= msgLength && msgLength != -1)
                {
                    //解析消息体
                    BaseMsg baseMsg = null;
                    switch (msgID)
                    {
                        case 1001:
                            baseMsg = new PlayerMsg();
                            baseMsg.Reading(cacheBytes, nowIndex);
                            break;
                        case 1003:
                            baseMsg = new QuitMsg();
                            //由于该消息没有消息体 所以都不用反序列化
                            break;
                        case 999:
                            baseMsg = new HeartMsg();
                            //由于该消息没有消息体 所以都不用反序列化
                            break;
                    }
                    if (baseMsg != null)
                        ThreadPool.QueueUserWorkItem(MsgHandle, baseMsg);
                    nowIndex += msgLength;
                    if (nowIndex == cacheNum)
                    {
                        cacheNum = 0;
                        break;
                    }
                }
                else
                {
                    //如果不满足 证明有分包 
                    //那么我们需要把当前收到的内容 记录下来
                    //有待下次接受到消息后 再做处理
                    //receiveBytes.CopyTo(cacheBytes, 0);
                    //cacheNum = receiveNum;
                    //如果进行了 id和长度的解析 但是 没有成功解析消息体 那么我们需要减去nowIndex移动的位置
                    if (msgLength != -1)
                        nowIndex -= 8;
                    //就是把剩余没有解析的字节数组内容 移到前面来 用于缓存下次继续解析
                    Array.Copy(cacheBytes, nowIndex, cacheBytes, 0, cacheNum - nowIndex);
                    cacheNum = cacheNum - nowIndex;
                    break;
                }
            }

        }

        private void MsgHandle(object obj)
        {
            BaseMsg msg = obj as BaseMsg;
            if(msg is PlayerMsg)
            {
                PlayerMsg playerMsg = msg as PlayerMsg;
                Console.WriteLine(playerMsg.playerID);
                Console.WriteLine(playerMsg.playerData.name);
                Console.WriteLine(playerMsg.playerData.lev);
                Console.WriteLine(playerMsg.playerData.atk);
            }
            else if(msg is QuitMsg)
            {
                //收到断开连接消息 把自己添加到待移除的列表当中
                Program.socket.AddDelSocket(this);
            }
            else if(msg is HeartMsg)
            {
                //收到心跳消息 记录收到消息的时间
                frontTime = DateTime.Now.Ticks / TimeSpan.TicksPerSecond;
                Console.WriteLine("收到心跳消息");
            }
        }

    }
}

服务端对于服务端Socket功能的一些封装

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<int, ClientSocket> clientDic = new Dictionary<int, ClientSocket>();

        //有待移除的客户端socket 避免 在foreach时直接从字典中移除 出现问题
        private List<ClientSocket> delList = new List<ClientSocket>();

        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);
                    lock(clientDic)
                        clientDic.Add(client.clientID, client);
                }
                catch (Exception e)
                {
                    Console.WriteLine("客户端连入报错" + e.Message);
                }
            }
        }
        //接收客户端消息
        private void Receive(object obj)
        {
            while (!isClose)
            {
                if(clientDic.Count > 0)
                {
                    lock (clientDic)
                    {
                        foreach (ClientSocket client in clientDic.Values)
                        {
                            client.Receive();
                        }

                        CloseDelListSocket();
                    }
                    
                }
            }
        }

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

        //添加待移除的 socket内容
        public void AddDelSocket(ClientSocket socket)
        {
            if (!delList.Contains(socket))
                delList.Add(socket);
        }

        判断有没有 断开连接的 把其 移除
        public void CloseDelListSocket()
        {
            //判断有没有 断开连接的 把其 移除
            for (int i = 0; i < delList.Count; i++)
                CloseClientSocket(delList[i]);
            delList.Clear();
        }

        //关闭客户端连接的 从字典中移除
        public void CloseClientSocket(ClientSocket socket)
        {
            lock (clientDic)
            {
                socket.Close();
                if (clientDic.ContainsKey(socket.clientID))
                {
                    clientDic.Remove(socket.clientID);
                    Console.WriteLine("客户端{0}主动断开连接了", socket.clientID);
                }
            }
        }
    }
}

服务端主程序

using System;

namespace TeachTcpServerExercises2
{
    class Program
    {
        public static ServerSocket socket;
        static void Main(string[] args)
        {
            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:" )
                {
                    if(input.Substring(2) == "1001")
                    {
                        PlayerMsg msg = new PlayerMsg();
                        msg.playerID = 9876;
                        msg.playerData = new PlayerData();
                        msg.playerData.name = "服务器端发来的消息";
                        msg.playerData.lev = 99;
                        msg.playerData.atk = 80;
                        socket.Broadcast(msg);
                    }
                }
            }
        }
    }
}

TCP客户端的实现(同步)

tcp服务端中各种消息类型也都客户端需要这边实现,重复部分省略 

using System;
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<BaseMsg> sendMsgQueue = new Queue<BaseMsg>();
    //用于接收消息的对象 公共容器 子线程往里面放 主线程从里面取
    private Queue<BaseMsg> receiveQueue = new Queue<BaseMsg>();

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

    //用于处理分包时 缓存的 字节数组 和 字节数组长度
    private byte[] cacheBytes = new byte[1024 * 1024];
    private int cacheNum = 0;

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

    //发送心跳消息的间隔时间
    private int SEND_HEART_MSG_TIME = 2;
    private HeartMsg hearMsg = new HeartMsg();

    void Awake()
    {
        instance = this;
        DontDestroyOnLoad(this.gameObject);
        //客户端循环定时给服务端发送心跳消息
        InvokeRepeating("SendHeartMsg", 0, SEND_HEART_MSG_TIME);
    }

    private void SendHeartMsg()
    {
        if (isConnected)
            Send(hearMsg);
    }

    // Update is called once per frame
    void Update()
    {
        if(receiveQueue.Count > 0)
        {
            BaseMsg msg = receiveQueue.Dequeue();
            if(msg is PlayerMsg)
            {
                PlayerMsg playerMsg = (msg as PlayerMsg);
                print(playerMsg.playerID);
                print(playerMsg.playerData.name);
                print(playerMsg.playerData.lev);
                print(playerMsg.playerData.atk);
            }
        }
    }

    //连接服务端
    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(BaseMsg msg)
    {
        sendMsgQueue.Enqueue(msg);
    }

    /// <summary>
    /// 用于测试 直接发字节数组的方法
    /// </summary>
    /// <param name="bytes"></param>
    public void SendTest(byte[] bytes)
    {
        
        socket.Send(bytes);
    }

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

    //不停的接受消息
    private void ReceiveMsg(object obj)
    {
        while (isConnected)
        {
            if(socket.Available > 0)
            {
                byte[] receiveBytes = new byte[1024 * 1024];
                int receiveNum = socket.Receive(receiveBytes);
                HandleReceiveMsg(receiveBytes, receiveNum);
                首先把收到字节数组的前4个字节  读取出来得到ID
                //int msgID = BitConverter.ToInt32(receiveBytes, 0);
                //BaseMsg baseMsg = null;
                //switch (msgID)
                //{
                //    case 1001:
                //        PlayerMsg msg = new PlayerMsg();
                //        msg.Reading(receiveBytes, 4);
                //        baseMsg = msg;
                //        break;
                //}
                如果消息为空 那证明是不知道类型的消息 没有解析
                //if (baseMsg == null)
                //    continue;
                收到消息 解析消息为字符串 并放入公共容器
                //receiveQueue.Enqueue(baseMsg);
            }    
        }
    }

    //处理接受消息 分包、黏包问题的方法
    private void HandleReceiveMsg(byte[] receiveBytes, int receiveNum)
    {
        int msgID = 0;
        int msgLength = 0;
        int nowIndex = 0;

        //收到消息时 应该看看 之前有没有缓存的 如果有的话 我们直接拼接到后面
        receiveBytes.CopyTo(cacheBytes, cacheNum);
        cacheNum += receiveNum;

        while (true)
        {
            //每次将长度设置为-1 是避免上一次解析的数据 影响这一次的判断
            msgLength = -1;
            //处理解析一条消息
            if(cacheNum - nowIndex >= 8)
            {
                //解析ID
                msgID = BitConverter.ToInt32(cacheBytes, nowIndex);
                nowIndex += 4;
                //解析长度
                msgLength = BitConverter.ToInt32(cacheBytes, nowIndex);
                nowIndex += 4;
            }

            if(cacheNum - nowIndex >= msgLength && msgLength != -1)
            {
                //解析消息体
                BaseMsg baseMsg = null;
                switch (msgID)
                {
                    case 1001:
                        PlayerMsg msg = new PlayerMsg();
                        msg.Reading(cacheBytes, nowIndex);
                        baseMsg = msg;
                        break;
                }
                if (baseMsg != null)
                    receiveQueue.Enqueue(baseMsg);
                nowIndex += msgLength;
                if (nowIndex == cacheNum)
                {
                    cacheNum = 0;
                    break;
                }
            }
            else
            {
                //如果不满足 证明有分包 
                //那么我们需要把当前收到的内容 记录下来
                //有待下次接受到消息后 再做处理
                //receiveBytes.CopyTo(cacheBytes, 0);
                //cacheNum = receiveNum;
                //如果进行了 id和长度的解析 但是 没有成功解析消息体 那么我们需要减去nowIndex移动的位置
                if (msgLength != -1)
                    nowIndex -= 8;
                //就是把剩余没有解析的字节数组内容 移到前面来 用于缓存下次继续解析
                Array.Copy(cacheBytes, nowIndex, cacheBytes, 0, cacheNum - nowIndex);
                cacheNum = cacheNum - nowIndex;
                break;
            }
        }
        
    }

    public void Close()
    {
        if(socket != null)
        {
            print("客户端主动断开连接");

            //主动发送一条断开连接的消息给服务端
            //QuitMsg msg = new QuitMsg();
            //socket.Send(msg.Writing());
            //socket.Shutdown(SocketShutdown.Both);
            //socket.Disconnect(false);
            //socket.Close();
            socket = null;

            isConnected = false;
        }
    }

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

TCP客户端与服务端一般实现(异步)

问题与同步相同,仍需考虑分包,粘包,断开连接等问题

仍需需要同步实现中所有的消息类型

只是将同步中实现方法换成异步通信方法

TCP客户端的实现(异步)

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

public class NetAsyncMgr : MonoBehaviour
{
    private static NetAsyncMgr instance;

    public static NetAsyncMgr Instance => instance;

    //和服务器进行连接的 Socket
    private Socket socket;

    //接受消息用的 缓存容器
    private byte[] cacheBytes = new byte[1024 * 1024];
    private int cacheNum = 0;

    private Queue<BaseMsg> receiveQueue = new Queue<BaseMsg>();

    //发送心跳消息的间隔时间
    private int SEND_HEART_MSG_TIME = 2;
    private HeartMsg hearMsg = new HeartMsg();

    // Start is called before the first frame update
    void Awake()
    {
        instance = this;
        //过场景不移除
        DontDestroyOnLoad(this.gameObject);
        //客户端循环定时给服务端发送心跳消息
        InvokeRepeating("SendHeartMsg", 0, SEND_HEART_MSG_TIME);
    }

    private void SendHeartMsg()
    {
        if (socket != null && socket.Connected)
            Send(hearMsg);
    }

    // Update is called once per frame
    void Update()
    {
        if (receiveQueue.Count > 0)
        {
            BaseMsg baseMsg = receiveQueue.Dequeue();
            switch (baseMsg)
            {
                case PlayerMsg msg:
                    print(msg.playerID);
                    print(msg.playerData.name);
                    print(msg.playerData.lev);
                    print(msg.playerData.atk);
                    break;
            }
        }
    }

    //连接服务器的代码
    public void Connect(string ip, int port)
    {
        if (socket != null && socket.Connected)
            return;

        IPEndPoint ipPoint = new IPEndPoint(IPAddress.Parse(ip), port);
        socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

        SocketAsyncEventArgs args = new SocketAsyncEventArgs();
        args.RemoteEndPoint = ipPoint;
        args.Completed += (socket, args) =>
        {
            if(args.SocketError == SocketError.Success)
            {
                print("连接成功");
                //收消息
                SocketAsyncEventArgs receiveArgs = new SocketAsyncEventArgs();
                receiveArgs.SetBuffer(cacheBytes, 0, cacheBytes.Length);
                receiveArgs.Completed += ReceiveCallBack;
                this.socket.ReceiveAsync(receiveArgs);
            }
            else
            {
                print("连接失败" + args.SocketError);
            }
        };
        socket.ConnectAsync(args);
    }

    //收消息完成的回调函数
    private void ReceiveCallBack(object obj, SocketAsyncEventArgs args)
    {
        if(args.SocketError == SocketError.Success)
        {
            HandleReceiveMsg(args.BytesTransferred);
            //继续去收消息
            args.SetBuffer(cacheNum, args.Buffer.Length - cacheNum);
            //继续异步收消息
            if (this.socket != null && this.socket.Connected)
                socket.ReceiveAsync(args);
            else
                Close();
        }
        else
        {
            print("接受消息出错" + args.SocketError);
            //关闭客户端连接
            Close();
        }
    }

    public void Close()
    {
        if(socket != null)
        {
            QuitMsg msg = new QuitMsg();
            socket.Send(msg.Writing());
            socket.Shutdown(SocketShutdown.Both);
            socket.Disconnect(false);
            socket.Close();
            socket = null;
        }
    }

    public void SendTest(byte[] bytes)
    {
        SocketAsyncEventArgs args = new SocketAsyncEventArgs();
        args.SetBuffer(bytes, 0, bytes.Length);
        args.Completed += (socket, args) =>
        {
            if (args.SocketError != SocketError.Success)
            {
                print("发送消息失败" + args.SocketError);
                Close();
            }

        };
        this.socket.SendAsync(args);
    }

    public void Send(BaseMsg msg)
    {
        if(this.socket != null && this.socket.Connected)
        {
            byte[] bytes = msg.Writing();
            SocketAsyncEventArgs args = new SocketAsyncEventArgs();
            args.SetBuffer(bytes, 0, bytes.Length);
            args.Completed += (socket, args) =>
            {
                if (args.SocketError != SocketError.Success)
                {
                    print("发送消息失败" + args.SocketError);
                    Close();
                }
                    
            };
            this.socket.SendAsync(args);
        }
        else
        {
            Close();
        }
    }

    //处理接受消息 分包、黏包问题的方法
    private void HandleReceiveMsg(int receiveNum)
    {
        int msgID = 0;
        int msgLength = 0;
        int nowIndex = 0;

        cacheNum += receiveNum;

        while (true)
        {
            //每次将长度设置为-1 是避免上一次解析的数据 影响这一次的判断
            msgLength = -1;
            //处理解析一条消息
            if (cacheNum - nowIndex >= 8)
            {
                //解析ID
                msgID = BitConverter.ToInt32(cacheBytes, nowIndex);
                nowIndex += 4;
                //解析长度
                msgLength = BitConverter.ToInt32(cacheBytes, nowIndex);
                nowIndex += 4;
            }

            if (cacheNum - nowIndex >= msgLength && msgLength != -1)
            {
                //解析消息体
                BaseMsg baseMsg = null;
                switch (msgID)
                {
                    case 1001:
                        baseMsg = new PlayerMsg();
                        baseMsg.Reading(cacheBytes, nowIndex);
                        break;
                }
                if (baseMsg != null)
                    receiveQueue.Enqueue(baseMsg);
                nowIndex += msgLength;
                if (nowIndex == cacheNum)
                {
                    cacheNum = 0;
                    break;
                }
            }
            else
            {
                if (msgLength != -1)
                    nowIndex -= 8;
                //就是把剩余没有解析的字节数组内容 移到前面来 用于缓存下次继续解析
                Array.Copy(cacheBytes, nowIndex, cacheBytes, 0, cacheNum - nowIndex);
                cacheNum = cacheNum - nowIndex;
                break;
            }
        }

    }

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

TCP服务端的实现(异步)

客户端socket功能的封装

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

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

        private byte[] cacheBytes = new byte[1024];
        private int cacheNum = 0;

        //上一次收到消息的时间
        private long frontTime = -1;
        //超时时间
        private static int TIME_OUT_TIME = 10;

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

            //开始收消息
            this.socket.BeginReceive(cacheBytes, cacheNum, cacheBytes.Length, SocketFlags.None, ReceiveCallBack, null);
            ThreadPool.QueueUserWorkItem(CheckTimeOut);
        }

        /// <summary>
        /// 间隔一段时间 检测一次超时 如果超时 就会主动断开该客户端的连接
        /// </summary>
        /// <param name="obj"></param>
        private void CheckTimeOut(object obj)
        {
            while (this.socket != null && this.socket.Connected)
            {
                if (frontTime != -1 &&
                DateTime.Now.Ticks / TimeSpan.TicksPerSecond - frontTime >= TIME_OUT_TIME)
                {
                    Program.serverSocket.CloseClientSocket(this);
                    break;
                }
                Thread.Sleep(5000);
            }
        }

        private void ReceiveCallBack(IAsyncResult result)
        {
            try
            {
                if (this.socket != null && this.socket.Connected)
                {
                    //消息成功
                    int num = this.socket.EndReceive(result);
                    //处理分包黏包
                    HandleReceiveMsg(num);
                    this.socket.BeginReceive(cacheBytes, cacheNum, cacheBytes.Length - cacheNum, SocketFlags.None, ReceiveCallBack, this.socket);
                }
                else
                {
                    Console.WriteLine("没有连接,不用再收消息了");
                    Program.serverSocket.CloseClientSocket(this);
                }
            }
            catch (SocketException e)
            {
                Console.WriteLine("接受消息错误" + e.SocketErrorCode + e.Message);
                Program.serverSocket.CloseClientSocket(this);
            }
        }

        public void Send(BaseMsg msg)
        {
            if(socket != null && socket.Connected)
            {
                byte[] bytes = msg.Writing();
                socket.BeginSend(bytes, 0, bytes.Length, SocketFlags.None, SendCallBack, null);
            }
            else
            {
                Program.serverSocket.CloseClientSocket(this);
            }
        }

        private void SendCallBack(IAsyncResult result)
        {
            try
            {
                if (socket != null && socket.Connected)
                    this.socket.EndSend(result);
                else
                    Program.serverSocket.CloseClientSocket(this);
            }
            catch (SocketException e)
            {
                Console.WriteLine("发送失败" + e.SocketErrorCode + e.Message);
                Program.serverSocket.CloseClientSocket(this);
            }
        }

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

        //处理分包黏包
        private void HandleReceiveMsg(int receiveNum)
        {
            int msgID = 0;
            int msgLength = 0;
            int nowIndex = 0;

            //由于消息接收后是直接存储在 cacheBytes中的 所以不需要进行什么拷贝操作
            //收到消息的字节数量
            cacheNum += receiveNum;

            while (true)
            {
                //每次将长度设置为-1 是避免上一次解析的数据 影响这一次的判断
                msgLength = -1;
                //处理解析一条消息
                if (cacheNum - nowIndex >= 8)
                {
                    //解析ID
                    msgID = BitConverter.ToInt32(cacheBytes, nowIndex);
                    nowIndex += 4;
                    //解析长度
                    msgLength = BitConverter.ToInt32(cacheBytes, nowIndex);
                    nowIndex += 4;
                }

                if (cacheNum - nowIndex >= msgLength && msgLength != -1)
                {
                    //解析消息体
                    BaseMsg baseMsg = null;
                    switch (msgID)
                    {
                        case 1001:
                            baseMsg = new PlayerMsg();
                            baseMsg.Reading(cacheBytes, nowIndex);
                            break;
                        case 1003:
                            baseMsg = new QuitMsg();
                            //由于该消息没有消息体 所以都不用反序列化
                            break;
                        case 999:
                            baseMsg = new HeartMsg();
                            //由于该消息没有消息体 所以都不用反序列化
                            break;
                    }
                    if (baseMsg != null)
                        ThreadPool.QueueUserWorkItem(MsgHandle, baseMsg);
                    nowIndex += msgLength;
                    if (nowIndex == cacheNum)
                    {
                        cacheNum = 0;
                        break;
                    }
                }
                else
                {
                    //如果不满足 证明有分包 
                    //那么我们需要把当前收到的内容 记录下来
                    //有待下次接受到消息后 再做处理
                    //receiveBytes.CopyTo(cacheBytes, 0);
                    //cacheNum = receiveNum;
                    //如果进行了 id和长度的解析 但是 没有成功解析消息体 那么我们需要减去nowIndex移动的位置
                    if (msgLength != -1)
                        nowIndex -= 8;
                    //就是把剩余没有解析的字节数组内容 移到前面来 用于缓存下次继续解析
                    Array.Copy(cacheBytes, nowIndex, cacheBytes, 0, cacheNum - nowIndex);
                    cacheNum = cacheNum - nowIndex;
                    break;
                }
            }

        }

        //消息处理
        private void MsgHandle(object obj)
        {
            switch (obj)
            {
                case PlayerMsg msg:
                    PlayerMsg playerMsg = msg as PlayerMsg;
                    Console.WriteLine(playerMsg.playerID);
                    Console.WriteLine(playerMsg.playerData.name);
                    Console.WriteLine(playerMsg.playerData.lev);
                    Console.WriteLine(playerMsg.playerData.atk);
                    break;
                case QuitMsg msg:
                    //收到断开连接消息 把自己添加到待移除的列表当中
                    Program.serverSocket.CloseClientSocket(this);
                    break;
                case HeartMsg msg:
                    //收到心跳消息 记录收到消息的时间
                    frontTime = DateTime.Now.Ticks / TimeSpan.TicksPerSecond;
                    Console.WriteLine("收到心跳消息");
                    break;
            }
        }
    }
}

服务端socket功能的封装

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

namespace TeachTcpServerAsync
{
    class ServerSocket
    {
        private Socket socket;
        private Dictionary<int, ClientSocket> clientDic = new Dictionary<int, ClientSocket>();

        public void Start(string ip, int port, int num)
        {
            socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            IPEndPoint ipPoint = new IPEndPoint(IPAddress.Parse(ip), port);
            try
            {
                socket.Bind(ipPoint);
                socket.Listen(num);
                //通过异步接受客户端连入
                socket.BeginAccept(AcceptCallBack, null);
            }
            catch (Exception e)
            {
                Console.WriteLine("启动服务器失败" + e.Message);
            }
        }

        private void AcceptCallBack(IAsyncResult result)
        {
            try
            {
                //获取连入的客户端
                Socket clientSocket = socket.EndAccept(result);
                ClientSocket client = new ClientSocket(clientSocket);
                //记录客户端对象
                clientDic.Add(client.clientID, client);

                //继续去让别的客户端可以连入
                socket.BeginAccept(AcceptCallBack, null);
            }
            catch (Exception e)
            {
                Console.WriteLine("客户端连入失败" + e.Message);
            }
        }

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

        //关闭客户端连接的 从字典中移除
        public void CloseClientSocket(ClientSocket socket)
        {
            lock (clientDic)
            {
                socket.Close();
                if (clientDic.ContainsKey(socket.clientID))
                {
                    clientDic.Remove(socket.clientID);
                    Console.WriteLine("客户端{0}主动断开连接了", socket.clientID);
                }
            }
        }
    }
}

主程序

using System;

namespace TeachTcpServerAsync
{
    class Program
    {
        public static ServerSocket serverSocket;
        static void Main(string[] args)
        {
            serverSocket = new ServerSocket();
            serverSocket.Start("127.0.0.1", 8080, 1024);
            Console.WriteLine("开启服务器成功");

            while (true)
            {
                string input = Console.ReadLine();
                if (input.Substring(2) == "1001")
                {
                    PlayerMsg msg = new PlayerMsg();
                    msg.playerID = 9876;
                    msg.playerData = new PlayerData();
                    msg.playerData.name = "服务器端发来的消息";
                    msg.playerData.lev = 99;
                    msg.playerData.atk = 80;
                    serverSocket.Broadcast(msg);
                }
            }
        }
    }
}

UDP服务端

UDP服务端基本逻辑

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

namespace TeachUdpServer
{
    class Program
    {
        static void Main(string[] args)
        {
            #region 实现UDP服务端通信 收发字符串
            //1.创建套接字
            Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
            //2.绑定本机地址
            IPEndPoint ipPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 8081);
            socket.Bind(ipPoint);
            Console.WriteLine("服务器开启");
            //3.接受消息
            byte[] bytes = new byte[512];
            //这个变量主要是用来记录 谁发的信息给你 传入函数后 在内部 它会帮助我们进行赋值
            EndPoint remoteIpPoint2 = new IPEndPoint(IPAddress.Any, 0);
            int length = socket.ReceiveFrom(bytes, ref remoteIpPoint2);
            Console.WriteLine("IP:" + (remoteIpPoint2 as IPEndPoint).Address.ToString() +
                "port:" + (remoteIpPoint2 as IPEndPoint).Port +
                "发来了" +
                Encoding.UTF8.GetString(bytes, 0, length));

            //4.发送到指定目标
            //由于我们先收 所以 我们已经知道谁发了消息给我 我直接发给它就行了
            socket.SendTo(Encoding.UTF8.GetBytes("欢迎发送消息给服务器"), remoteIpPoint2);

            //5.释放关闭
            socket.Shutdown(SocketShutdown.Both);
            socket.Close();
            #endregion

            Console.ReadKey();
        }
    }
}

UDP客户端

UDP客户端基本逻辑

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

public class Lesson14 : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        #region 实现UDP客户端通信 收发字符串
        //1.创建套接字
        Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
        
        //2.绑定本机地址
        IPEndPoint ipPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 8080);
        socket.Bind(ipPoint);

        //3.发送到指定目标
        IPEndPoint remoteIpPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 8081);
        //指定要发送的字节数 和 远程计算机的 IP和端口
        socket.SendTo(Encoding.UTF8.GetBytes("来了"), remoteIpPoint);

        //4.接受消息
        byte[] bytes = new byte[512];
        //这个变量主要是用来记录 谁发的信息给你 传入函数后 在内部 它会帮助我们进行赋值
        EndPoint remoteIpPoint2 = new IPEndPoint(IPAddress.Any, 0);
        int length = socket.ReceiveFrom(bytes, ref remoteIpPoint2);
        print("IP:" + (remoteIpPoint2 as IPEndPoint).Address.ToString() +
            "port:" + (remoteIpPoint2 as IPEndPoint).Port +
            "发来了" +
            Encoding.UTF8.GetString(bytes, 0, length));

        //5.释放关闭
        socket.Shutdown(SocketShutdown.Both);
        socket.Close();
        #endregion
    }

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

UDP异步通信常用方法

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

public class Lesson16 : MonoBehaviour
{
    private byte[] cacheBytes = new byte[512];
    // Start is called before the first frame update
    void Start()
    {


        #region  UDP通信中Begin相关异步方法
        Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
        //BeginSendTo
        byte[] bytes = Encoding.UTF8.GetBytes("123123lkdsajlfjas");
        EndPoint ipPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 8080);
        socket.BeginSendTo(bytes, 0, bytes.Length, SocketFlags.None, ipPoint, SendToOver, socket);
        private void SendToOver(IAsyncResult result)
        {
            try
            {
                Socket s = result.AsyncState as Socket;
                s.EndSendTo(result);
                print("发送成功");
            }
            catch (SocketException s)
            {
                print("发送失败" + s.SocketErrorCode + s.Message);
            }
        }

        //BeginReceiveFrom
        socket.BeginReceiveFrom(cacheBytes, 0, cacheBytes.Length, SocketFlags.None, ref ipPoint, ReceiveFromOver, (socket, ipPoint));
        private void ReceiveFromOver(IAsyncResult result)
        {
            try
            {
                (Socket s, EndPoint ipPoint) info = ((Socket, EndPoint))result.AsyncState;
                //返回值 就是接收了多少个 字节数
                int num = info.s.EndReceiveFrom(result, ref info.ipPoint);
                //处理消息

                //处理完消息 又继续接受消息
                info.s.BeginReceiveFrom(cacheBytes, 0, cacheBytes.Length, SocketFlags.None, ref info.ipPoint, ReceiveFromOver, info);
            }
            catch (SocketException s)
            {
                print("接受消息出问题" + s.SocketErrorCode + s.Message);
            }
        }
        #endregion

        #region  UDP通信中Async相关异步方法
        //SendToAsync
        SocketAsyncEventArgs args = new SocketAsyncEventArgs();
        //设置要发送的数据 
        args.SetBuffer(bytes, 0, bytes.Length);
        //设置完成事件
        args.Completed += SendToAsync;
        socket.SendToAsync(args);
        private void SendToAsync(object s, SocketAsyncEventArgs args)
        {
            if (args.SocketError == SocketError.Success)
            {
                print("发送成功");
            }
            else
            {
                print("发送失败");
            }
        }

        //ReceiveFromAsync
        SocketAsyncEventArgs args2 = new SocketAsyncEventArgs();
        //这是设置接受消息的容器
        args2.SetBuffer(cacheBytes, 0, cacheBytes.Length);
        args2.Completed += ReceiveFromAsync;
        socket.ReceiveFromAsync(args2);
        private void ReceiveFromAsync(object s, SocketAsyncEventArgs args)
        {
            if (args.SocketError == SocketError.Success)
            {
                print("接收成功");
                //具体收了多少个字节
                //args.BytesTransferred
                //可以通过以下两种方式获取到收到的字节数组内容
                //args.Buffer
                //cacheBytes
                //解析消息

                Socket socket = s as Socket;
                //只需要设置 从第几个位置开始接 能接多少
                args.SetBuffer(0, cacheBytes.Length);
                socket.ReceiveFromAsync(args);
            }
            else
            {
                print("接收失败");
            }
        }
        #endregion

     
    }


}

UDP客户端与服务端一般实现(同步)

问题

TCP需要考虑的问题UDP一般也要考虑,但是UDP不需要考虑粘包问题

其余思路与TCP大致相同,细节部分有所不同

UDP客户端实现(同步)

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

public class UdpNetMgr : MonoBehaviour
{
    private static UdpNetMgr instance;
    public static UdpNetMgr Instance => instance;

    private EndPoint serverIpPoint;

    private Socket socket;

    //客户端socket是否关闭
    private bool isClose = true;

    //两个容器 队列
    //接受和发送消息的队列 在多线程里面可以操作
    private Queue<BaseMsg> sendQueue = new Queue<BaseMsg>();
    private Queue<BaseMsg> receiveQueue = new Queue<BaseMsg>();

    private byte[] cacheBytes = new byte[512];

    // Start is called before the first frame update
    void Awake()
    {
        instance = this;
        DontDestroyOnLoad(this.gameObject);
    }

    // Update is called once per frame
    void Update()
    {
        if(receiveQueue.Count > 0)
        {
            BaseMsg baseMsg = receiveQueue.Dequeue();
            switch (baseMsg)
            {
                case PlayerMsg msg:
                    print(msg.playerID);
                    print(msg.playerData.name);
                    print(msg.playerData.atk);
                    print(msg.playerData.lev);
                    break;
            }
        }
    }

    /// <summary>
    /// 启动客户端socket相关的方法
    /// </summary>
    /// <param name="ip">远端服务器的IP</param>
    /// <param name="port">远端服务器的port</param>
    public void StartClient(string ip, int port)
    {
        //如果当前是开启状态 就不用再开了
        if (!isClose)
            return;

        //先记录服务器地址,一会发消息时会使用 
        serverIpPoint = new IPEndPoint(IPAddress.Parse(ip), port);

        IPEndPoint clientIpPort = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 8081);
        try
        {
            socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
            socket.Bind(clientIpPort);
            isClose = false;
            print("客户端网络启动");
            ThreadPool.QueueUserWorkItem(ReceiveMsg);
            ThreadPool.QueueUserWorkItem(SendMsg);
        }
        catch (System.Exception e)
        {
            print("启动Socket出问题" + e.Message);
        }
    }

    private void ReceiveMsg(object obj)
    {
        EndPoint tempIpPoint = new IPEndPoint(IPAddress.Any, 0);
        int nowIndex;
        int msgID;
        int msgLength;
        while (!isClose)
        {
            if(socket != null && socket.Available > 0)
            {
                try
                {
                    socket.ReceiveFrom(cacheBytes, ref tempIpPoint);
                    //为了避免处理 非服务器发来的 骚扰消息
                    if(!tempIpPoint.Equals(serverIpPoint))
                        continue;//如果发现 发消息给你的 不是服务器 那么证明是骚扰消息 就不用处理

                    //处理服务器发来的消息
                    nowIndex = 0;
                    //解析ID
                    msgID = BitConverter.ToInt32(cacheBytes, nowIndex);
                    nowIndex += 4;
                    //解析长度
                    msgLength = BitConverter.ToInt32(cacheBytes, nowIndex);
                    nowIndex += 4;
                    //解析消息体
                    BaseMsg msg = null;
                    switch (msgID)
                    {
                        case 1001:
                            msg = new PlayerMsg();
                            //反序列化消息体
                            msg.Reading(cacheBytes, nowIndex);
                            break;
                    }
                    if (msg != null)
                        receiveQueue.Enqueue(msg);
                }
                catch (SocketException s)
                {
                    print("接受消息出问题" + s.SocketErrorCode + s.Message);
                }
                catch (Exception e)
                {
                    print("接受消息出问题(非网络问题)" + e.Message);
                }
            }
        }
    }

    private void SendMsg(object obj)
    {
        while (!isClose)
        {
            if (socket != null && sendQueue.Count > 0)
            {
                try
                {
                    socket.SendTo(sendQueue.Dequeue().Writing(), serverIpPoint);
                }
                catch (SocketException s)
                {
                    print("发送消息出错" + s.SocketErrorCode + s.Message);
                }
            }
        }
    }

    //发送消息
    public void Send(BaseMsg msg)
    {
        sendQueue.Enqueue(msg);
    }

    //关闭socket
    public void Close()
    {
        if(socket != null)
        {
            isClose = true;
            QuitMsg msg = new QuitMsg();
            //发送一个退出消息给服务器 让其移除记录
            socket.SendTo(msg.Writing(), serverIpPoint);
            socket.Shutdown(SocketShutdown.Both);
            socket.Close();
            socket = null;
        }
        
    }

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

UDP服务端实现(同步)

客户端socket功能封装

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

namespace TeachUdpServerExercises
{
    //它是用于记录和服务器通信过的客户端的IP和端口 
    class Client
    {
        public IPEndPoint clientIPandPort;
        public string clientStrID;

        //上一次收到消息的时间
        public long frontTime = -1;

        public Client(string ip, int port)
        {
            //规则和外面一样 记录唯一ID 通过 ip + port 拼接的形式
            clientStrID = ip + port;
            //就把客户端的信息记录下来了
            clientIPandPort = new IPEndPoint(IPAddress.Parse(ip), port);
        }

        public void ReceiveMsg(byte[] bytes)
        {
            //为了避免处理消息时 又 接受到了 其它消息 所以我们需要在处理之前 先把信息拷贝出来
            //处理消息和接收消息 用不同的容器 避免出现问题
            byte[] cacheBytes = new byte[512];
            bytes.CopyTo(cacheBytes, 0);
            //记录收到消息的 系统时间 单位为秒
            frontTime = DateTime.Now.Ticks / TimeSpan.TicksPerSecond;
            ThreadPool.QueueUserWorkItem(ReceiveHandle, cacheBytes);
        }

        //多线程处理消息
        private void ReceiveHandle(object obj)
        {
            try
            {
                //取出传进来的字节
                byte[] bytes = obj as byte[];
                int nowIndex = 0;
                //先处理 ID
                int msgID = BitConverter.ToInt32(bytes, nowIndex);
                nowIndex += 4;
                //再处理 长度
                int msgLength = BitConverter.ToInt32(bytes, nowIndex);
                nowIndex += 4;
                //再解析消息体
                switch (msgID)
                {
                    case 1001:
                        PlayerMsg playerMsg = new PlayerMsg();
                        playerMsg.Reading(bytes, nowIndex);
                        Console.WriteLine(playerMsg.playerID);
                        Console.WriteLine(playerMsg.playerData.name);
                        Console.WriteLine(playerMsg.playerData.atk);
                        Console.WriteLine(playerMsg.playerData.lev);
                        break;
                    case 1003:
                        QuitMsg quitMsg = new QuitMsg();
                        //由于它没有消息体 所以不用反序列化
                        //quitMsg.Reading(bytes, nowIndex);
                        //处理退出
                        Program.serverSocket.RemoveClient(clientStrID);
                        break;
                }
            }
            catch (Exception e)
            {
                Console.WriteLine("处理消息时出错" + e.Message);
                //如果出错 就不用记录这个客户端信息
                Program.serverSocket.RemoveClient(clientStrID);
            }
            
        }
    }
}

服务端socket功能封装

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

namespace TeachUdpServerExercises
{
    class ServerSocket
    {
        private Socket socket;

        private bool isClose;

        //我们可以通过记录谁给我发了消息 把它的 ip和端口记下来 这样就认为它是我的客户端了嘛
        private Dictionary<string, Client> clientDic = new Dictionary<string, Client>();

        public void Start(string ip, int port)
        {
            IPEndPoint ipPoint = new IPEndPoint(IPAddress.Parse(ip), port);
            //声明一个用于UDP通信的Socket
            socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
            try
            {
                socket.Bind(ipPoint);
                isClose = false;
                //消息接收的处理 
                ThreadPool.QueueUserWorkItem(ReceiveMsg);
                //定时检测超时线程
                ThreadPool.QueueUserWorkItem(CheckTimeOut);
            }
            catch (Exception e)
            {
                Console.WriteLine("UDP开启出错" + e.Message);
            }
        }

        private void CheckTimeOut(object obj)
        {
            long nowTime = 0;
            List<string> delList = new List<string>();
            while (true)
            {
                //每30s检测一次 是否移除长时间没有接收到消息的客户端信息
                Thread.Sleep(30000);
                //得到当前系统时间
                nowTime = DateTime.Now.Ticks / TimeSpan.TicksPerSecond;
                foreach (Client c in clientDic.Values)
                {
                    //超过10秒没有收到消息的 客户端信息 需要被移除
                    if (nowTime - c.frontTime >= 10)
                        delList.Add(c.clientStrID);
                }
                //从待删除列表中移除 超时的客户端信息
                for (int i = 0; i < delList.Count; i++)
                    RemoveClient(delList[i]);
                delList.Clear();
            }
        }

        private void ReceiveMsg(object obj)
        {
            //接收消息的容器
            byte[] bytes = new byte[512];
            //记录谁发的
            EndPoint ipPoint = new IPEndPoint(IPAddress.Any, 0);
            //用于拼接字符串 位移ID 是由 IP + 端口构成的
            string strID = "";
            string ip;
            int port;
            while (!isClose)
            {
                if(socket.Available > 0)
                {
                    lock(socket)
                        socket.ReceiveFrom(bytes, ref ipPoint);
                    //处理消息 最好不要在这直接处理 而是交给 客户端对象处理
                    //收到消息时 我们可以来判断 是不是记录了这个客户端信息 (ip和端口)
                    //取出发送消息给我的 IP和端口
                    ip = (ipPoint as IPEndPoint).Address.ToString();
                    port = (ipPoint as IPEndPoint).Port;
                    strID = ip + port;//拼接成一个唯一ID 这个是我们自定义的规则
                    //判断有没有记录这个客户端信息 如果有 用它直接处理消息
                    if (clientDic.ContainsKey(strID))
                        clientDic[strID].ReceiveMsg(bytes);
                    else//如果没有 直接添加并且处理消息
                    {
                        clientDic.Add(strID, new Client(ip, port));
                        clientDic[strID].ReceiveMsg(bytes);
                    }
                }
            }
        }

        //指定发送一个消息给某个目标
        public void SendTo(BaseMsg msg, IPEndPoint ipPoint)
        {
            try
            {
                lock (socket)
                    socket.SendTo(msg.Writing(), ipPoint);
            }
            catch (SocketException s)
            {
                Console.WriteLine("发消息出现问题" + s.SocketErrorCode + s.Message);
            }
            catch (Exception e)
            {
                Console.WriteLine("发送消息出问题(可能是序列化问题)" + e.Message);
            }
            
        }

        public void Broadcast(BaseMsg msg)
        {
            //广播消息 给谁广播
            foreach (Client c in clientDic.Values)
            {
                SendTo(msg, c.clientIPandPort);
            }
        }

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

        public void RemoveClient(string clientID)
        {
            if(clientDic.ContainsKey(clientID))
            {
                Console.WriteLine("客户端{0}被移除了" + clientDic[clientID].clientIPandPort);
                clientDic.Remove(clientID);
            }
        }
    }
}

主程序

using System;

namespace TeachUdpServerExercises
{
    class Program
    {
        public static ServerSocket serverSocket;
        static void Main(string[] args)
        {

            serverSocket = new ServerSocket();
            serverSocket.Start("127.0.0.1", 8080);

            Console.WriteLine("UDP服务器启动了");

            while (true)
            {
                string input = Console.ReadLine();
                if(input.Substring(0,2) == "B:")
                {
                    PlayerMsg msg = new PlayerMsg();
                    msg.playerData = new PlayerData();
                    msg.playerID = 1001;
                    msg.playerData.name = "UDP服务器";
                    msg.playerData.atk = 88;
                    msg.playerData.lev = 66;
                    serverSocket.Broadcast(msg);
                }
            }

            #endregion
        }
    }
}

UDP客户端与服务端一般实现(异步)

UDP客户端实现(异步)

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

public class UdpAsyncNetMgr : MonoBehaviour
{
    private static UdpAsyncNetMgr instance;
    public static UdpAsyncNetMgr Instance => instance;

    private EndPoint serverIpPoint;

    private Socket socket;

    //客户端socket是否关闭
    private bool isClose = true;

    //发送消息的队列 在多线程里面可以操作
    private Queue<BaseMsg> receiveQueue = new Queue<BaseMsg>();

    private byte[] cacheBytes = new byte[512];

    // Start is called before the first frame update
    void Awake()
    {
        instance = this;
        DontDestroyOnLoad(this.gameObject);
    }

    // Update is called once per frame
    void Update()
    {
        if (receiveQueue.Count > 0)
        {
            BaseMsg baseMsg = receiveQueue.Dequeue();
            switch (baseMsg)
            {
                case PlayerMsg msg:
                    print(msg.playerID);
                    print(msg.playerData.name);
                    print(msg.playerData.atk);
                    print(msg.playerData.lev);
                    break;
            }
        }
    }

    /// <summary>
    /// 启动客户端socket相关的方法
    /// </summary>
    /// <param name="ip">远端服务器的IP</param>
    /// <param name="port">远端服务器的port</param>
    public void StartClient(string ip, int port)
    {
        //如果当前是开启状态 就不用再开了
        if (!isClose)
            return;

        //先记录服务器地址,一会发消息时会使用 
        serverIpPoint = new IPEndPoint(IPAddress.Parse(ip), port);

        IPEndPoint clientIpPort = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 8081);
        try
        {
            socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
            socket.Bind(clientIpPort);
            isClose = false;
            SocketAsyncEventArgs args = new SocketAsyncEventArgs();
            args.SetBuffer(cacheBytes, 0, cacheBytes.Length);
            args.RemoteEndPoint = new IPEndPoint(IPAddress.Any, 0);
            args.Completed += ReceiveMsg;
            socket.ReceiveFromAsync(args);
            print("客户端网络启动");
        }
        catch (System.Exception e)
        {
            print("启动Socket出问题" + e.Message);
        }
    }

    private void ReceiveMsg(object obj, SocketAsyncEventArgs args)
    {
        int nowIndex;
        int msgID;
        int msgLength;
        if(args.SocketError == SocketError.Success)
        {
            try
            {
                //要是服务器发的才处理
                if (args.RemoteEndPoint.Equals(serverIpPoint))
                {
                    //处理服务器发来的消息
                    nowIndex = 0;
                    //解析ID
                    msgID = BitConverter.ToInt32(args.Buffer, nowIndex);
                    nowIndex += 4;
                    //解析长度
                    msgLength = BitConverter.ToInt32(args.Buffer, nowIndex);
                    nowIndex += 4;
                    //解析消息体
                    BaseMsg msg = null;
                    switch (msgID)
                    {
                        case 1001:
                            msg = new PlayerMsg();
                            //反序列化消息体
                            msg.Reading(args.Buffer, nowIndex);
                            break;
                    }
                    if (msg != null)
                        receiveQueue.Enqueue(msg);
                }
                //再次接收消息
                if (socket != null && !isClose)
                {
                    args.SetBuffer(0, cacheBytes.Length);
                    socket.ReceiveFromAsync(args);
                }
            }
            catch (SocketException s)
            {
                print("接收消息出错" + s.SocketErrorCode + s.Message);
                Close();
            }
            catch (Exception e)
            {
                print("接收消息出错(可能是反序列化问题)" + e.Message);
                Close();
            }
        }
        else
        {
            print("接收消息失败" + args.SocketError);
        }
    }

    //发送消息
    public void Send(BaseMsg msg)
    {
        try
        {
            if(socket != null && !isClose)
            {
                SocketAsyncEventArgs args = new SocketAsyncEventArgs();
                byte[] bytes = msg.Writing();
                args.SetBuffer(bytes, 0, bytes.Length);
                args.Completed += SendToCallBack;
                //设置远端目标
                args.RemoteEndPoint = serverIpPoint;
                socket.SendToAsync(args);
            }
        }
        catch (SocketException s)
        {
            print("发送消息出错" + s.SocketErrorCode + s.Message);
        }
        catch (Exception e)
        {
            print("发送消息出错(可能是序列化问题)" + e.Message);
        }
    }

    private void SendToCallBack(object o, SocketAsyncEventArgs args)
    {
        if (args.SocketError != SocketError.Success)
            print("发送消息失败" + args.SocketError);
    }

    //关闭socket
    public void Close()
    {
        if (socket != null)
        {
            isClose = true;
            QuitMsg msg = new QuitMsg();
            //发送一个退出消息给服务器 让其移除记录
            socket.SendTo(msg.Writing(), serverIpPoint);
            socket.Shutdown(SocketShutdown.Both);
            socket.Close();
            socket = null;
        }

    }

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

UDP服务端实现(异步)

客户端socke功能实现

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

namespace TeachUdpAsyncServerExercises
{
    class Client
    {
        public IPEndPoint clientIPandPort;
        public string clientStrID;

        //上一次收到消息的时间
        public long frontTime = -1;

        public Client(string ip, int port)
        {
            //规则和外面一样 记录唯一ID 通过 ip + port 拼接的形式
            clientStrID = ip + port;
            //就把客户端的信息记录下来了
            clientIPandPort = new IPEndPoint(IPAddress.Parse(ip), port);
        }

        public void ReceiveMsg(byte[] bytes)
        {
            //为了避免处理消息时 又 接受到了 其它消息 所以我们需要在处理之前 先把信息拷贝出来
            //处理消息和接收消息 用不同的容器 避免出现问题
            byte[] cacheBytes = new byte[512];
            bytes.CopyTo(cacheBytes, 0);
            //记录收到消息的 系统时间 单位为秒
            frontTime = DateTime.Now.Ticks / TimeSpan.TicksPerSecond;
            ThreadPool.QueueUserWorkItem(ReceiveHandle, cacheBytes);
        }

        //多线程处理消息
        private void ReceiveHandle(object obj)
        {
            try
            {
                //取出传进来的字节
                byte[] bytes = obj as byte[];
                int nowIndex = 0;
                //先处理 ID
                int msgID = BitConverter.ToInt32(bytes, nowIndex);
                nowIndex += 4;
                //再处理 长度
                int msgLength = BitConverter.ToInt32(bytes, nowIndex);
                nowIndex += 4;
                //再解析消息体
                switch (msgID)
                {
                    case 1001:
                        PlayerMsg playerMsg = new PlayerMsg();
                        playerMsg.Reading(bytes, nowIndex);
                        Console.WriteLine(playerMsg.playerID);
                        Console.WriteLine(playerMsg.playerData.name);
                        Console.WriteLine(playerMsg.playerData.atk);
                        Console.WriteLine(playerMsg.playerData.lev);
                        break;
                    case 1003:
                        QuitMsg quitMsg = new QuitMsg();
                        //由于它没有消息体 所以不用反序列化
                        //quitMsg.Reading(bytes, nowIndex);
                        //处理退出
                        Program.serverSocket.RemoveClient(clientStrID);
                        break;
                }
            }
            catch (Exception e)
            {
                Console.WriteLine("处理消息时出错" + e.Message);
                //如果出错 就不用记录这个客户端信息
                Program.serverSocket.RemoveClient(clientStrID);
            }

        }
    }
}

服务端socket功能实现

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

namespace TeachUdpAsyncServerExercises
{
    class ServerSocket
    {
        private Socket socket;

        private bool isClose;

        //我们可以通过记录谁给我发了消息 把它的 ip和端口记下来 这样就认为它是我的客户端了嘛
        private Dictionary<string, Client> clientDic = new Dictionary<string, Client>();
        //用于接收消息的容器
        private byte[] cacheBytes = new byte[512];

        public void Start(string ip, int port)
        {
            EndPoint ipPoint = new IPEndPoint(IPAddress.Parse(ip), port);
            //声明一个用于UDP通信的Socket
            socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
            try
            {
                socket.Bind(ipPoint);
                isClose = false;
                //消息接收的处理 
                socket.BeginReceiveFrom(cacheBytes, 0, cacheBytes.Length, SocketFlags.None, ref ipPoint, ReceiveMsg, ipPoint);

                //定时检测超时线程
                ThreadPool.QueueUserWorkItem(CheckTimeOut);
            }
            catch (Exception e)
            {
                Console.WriteLine("UDP开启出错" + e.Message);
            }
        }

        private void CheckTimeOut(object obj)
        {
            long nowTime = 0;
            List<string> delList = new List<string>();
            while (true)
            {
                //每30s检测一次 是否移除长时间没有接收到消息的客户端信息
                Thread.Sleep(30000);
                //得到当前系统时间
                nowTime = DateTime.Now.Ticks / TimeSpan.TicksPerSecond;
                foreach (Client c in clientDic.Values)
                {
                    //超过10秒没有收到消息的 客户端信息 需要被移除
                    if (nowTime - c.frontTime >= 10)
                        delList.Add(c.clientStrID);
                }
                //从待删除列表中移除 超时的客户端信息
                for (int i = 0; i < delList.Count; i++)
                    RemoveClient(delList[i]);
                delList.Clear();
            }
        }

        private void ReceiveMsg(IAsyncResult result)
        {
            //接收消息的容器
            //记录谁发的
            //用于拼接字符串 位移ID 是由 IP + 端口构成的
            
            EndPoint ipPoint = result.AsyncState as IPEndPoint;
            
            string ip = (ipPoint as IPEndPoint).Address.ToString();
            int port = (ipPoint as IPEndPoint).Port;
            string strID = ip + port;//拼接成一个唯一ID 这个是我们自定义的规则
            try
            {
                socket.EndReceiveFrom(result, ref ipPoint);
                //判断有没有记录这个客户端信息 如果有 用它直接处理消息
                if (clientDic.ContainsKey(strID))
                    clientDic[strID].ReceiveMsg(cacheBytes);
                else//如果没有 直接添加并且处理消息
                {
                    clientDic.Add(strID, new Client(ip, port));
                    clientDic[strID].ReceiveMsg(cacheBytes);
                }

                //继续接受消息
                socket.BeginReceiveFrom(cacheBytes, 0, cacheBytes.Length, SocketFlags.None, ref ipPoint, ReceiveMsg, ipPoint);
            }
            catch (SocketException s)
            {
                Console.WriteLine("接受消息出错" + s.SocketErrorCode + s.Message);
            }
            catch (Exception e)
            {
                Console.WriteLine("接受消息出错(非Socket错误)" + e.Message);
            }
        }

        //指定发送一个消息给某个目标
        public void SendTo(BaseMsg msg, IPEndPoint ipPoint)
        {
            try
            {
                byte[] bytes = msg.Writing();
                socket.BeginSendTo(bytes, 0, bytes.Length, SocketFlags.None, ipPoint, (result) =>
                {
                    try
                    {
                        socket.EndSendTo(result);
                    }
                    catch (SocketException s)
                    {
                        Console.WriteLine("发消息出现问题" + s.SocketErrorCode + s.Message);
                    }
                    catch (Exception e)
                    {
                        Console.WriteLine("发送消息出问题(可能是序列化问题)" + e.Message);
                    }
                }, null);
            }
            catch (SocketException s)
            {
                Console.WriteLine("发消息出现问题" + s.SocketErrorCode + s.Message);
            }
            catch (Exception e)
            {
                Console.WriteLine("发送消息出问题(可能是序列化问题)" + e.Message);
            }

        }

        public void Broadcast(BaseMsg msg)
        {
            //广播消息 给谁广播
            foreach (Client c in clientDic.Values)
            {
                SendTo(msg, c.clientIPandPort);
            }
        }

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

        public void RemoveClient(string clientID)
        {
            if (clientDic.ContainsKey(clientID))
            {
                Console.WriteLine("客户端{0}被移除了" + clientDic[clientID].clientIPandPort);
                clientDic.Remove(clientID);
            }
        }
    }
}

主程序

using System;

namespace TeachUdpAsyncServerExercises
{
    class Program
    {
        public static ServerSocket serverSocket;
        static void Main(string[] args)
        {
            serverSocket = new ServerSocket();
            serverSocket.Start("127.0.0.1", 8080);

            Console.WriteLine("UDP服务器启动了");

            while (true)
            {
                string input = Console.ReadLine();
                if (input.Substring(0, 2) == "B:")
                {
                    PlayerMsg msg = new PlayerMsg();
                    msg.playerData = new PlayerData();
                    msg.playerID = 1001;
                    msg.playerData.name = "UDP服务器";
                    msg.playerData.atk = 88;
                    msg.playerData.lev = 66;
                    serverSocket.Broadcast(msg);
                }
            }
        }
    }
}

;