Unity网络模块开发

网络的作用

        网络是由若干设备和连接这些设备的链路构成,各种设备直接或间接通过介质相连

        设备之间想要进行信息传递时,将想要传递的数据编码为2进制数值便可以被有效的传输;这些数据是以电脉冲的形式进行传输的。

        线缆中的电压是在高低状态之间进行变化的,因而二进制中的1通过产生一个正电压来传输的,二进制中的0是通过产生一个负电压来传输的

       局域网

                局域网(LocalArea Network,LAN)是指在某一区域内由多台计算机互联成的计算机组。一般是房源几千米以内。局域网可以实现文件管理、引用软件共享、打印机共享、工作组内的日程安排、电子邮件和传真通信服务等服务功能。局域网是封闭型的,可以由办公室内的两台计算机组成,也可以由一个公司内的上千台计算机组成

        以太网

                以太网(Ethernet)是一种广泛使用的局域网(Local Area Network, LAN)技术。它是连接计算机和各种设备(如打印机、服务器、网络存储设备等)的主流方式之一,允许它们在有限的地理区域内共享资源和相互通信。以太网的核心规范由IEEE(电气和电子工程师协会)的802.3标准定义。

        以太网 网络拓扑结构

                用传输媒体把计算机等各种设备互相连接而成的物理布局,是指设备互联过程中构成的几何形状

        城域网

                是在一个城市范围内所建立的网络。通常覆盖一个城市,从几十公里到一百公里不等,可能会有多种介质,用户的数量也比局域网更多

        广域网(公网  外网)

                广域网(Wide Area Network,简称WAN)是一种地理覆盖范围广泛的计算机网络,其覆盖区域可以从几十公里延伸到几千公里,甚至全球范围。广域网的主要目的是将不同区域的局域网(Local Area Network, LAN)或计算机系统互连起来,使得分布在不同地点的用户和系统能够进行通信和资源共享。

                广域网可以跨越城市、国家乃至洲际,常用于连接企业的各个分支机构、数据中心,以及为远程工作人员提供对总部网络资源的访问。它利用多种通信技术来实现长距离的数据传输,包括但不限于公用分组交换网、卫星通信、无线分组交换和光纤网络。

        互联网(因特网)

                多个网络还可以通过路由器互连起来,这样就构成了一个覆盖范围更大的计算机网络。这样的网络称为互连网(internetwork或internet)

                因特网是世界上最大的互连网络

        万维网

                万维网 (World Wide Web,WWW)是存储在Internet计算机中、数量巨大的文档的集合。这些文档称为页面,它是一种超文本(Hypertext)信息,可以用于描述超媒体。文本、图形、视频、音频等多媒体,也称为超媒体。

                万维网利用网页之间的链接将不同网站的网页链接成一张逻辑上的信息网,从而用户可以方便的从internet上的一个站点去访问另一个站点 

  • 1.网络:由若干设备和连接这些设备的链路构成,设备间可以相互通信
  • 2.局域网:指某一个小区域内由多台设备互联成的计算机组
  • 3.以太网:网络连接的一种规则,定义了连接传输规范
  • 4.城域网:是在一个城市范围内所建立的网络,几十到一百公里。
  • 5.广域网:是连接不同地区、城市、国家的远程网络,几十到几千公里
  • 6.互联网(因特网):是目前国际上最大的互联网,定义了通信规则等
  • 7.万维网:是基于因特网的网站和网页的统称

网络协议概述

        网络的本质是让我们可以通过物理设备传递2进制数据,让设备和设备之间可以通信        

协议的字面意思:

                经过谈判、协商而制定的共同承认、共同遵守的文件

        网络协议的基本概念:

                网络协议是计算机网络中进行数据交换而建立的规则、标准或约定的集合

                指的是计算机网络中互相通信的对等实体之间交换信息时所必须遵守的规则的集合

OSI模型是网络通信的基本规则

        OSI模型是国际组织定义的一套理论基础,主要用于网络通信的规则

TCP/IP协议是基于OSI模型的工业实现

        TCP/IP协议是基于这套理论基础真正实现的通信规则

OSI模型的规则

        OSI模型将复杂的互联网实现分成了七层;每一层都有自己的功能,就像建筑物一样,从上到下有很多层构成,每一层都要靠下一层的支持;用户接触到的都只是最上面的一层,感受不到下面层级的复杂性

        1.应用层

                应用层为最上层,和用户直接打交道的可以联网的应用程序,比如浏览器游戏等等

                功能:为应用程序提供服务,我们可以根据自己要传输的信息决定使用哪一种协议(规则)来处理数据,进行传输

                它会在原始数据的基础上添加标头

                标头包含信息:协议信息等等

                常用的协议(规则):FTP(文件传输协议),HTTP(超文本传输协议),SMTP(邮件传输协议)等等

        2.表示层

                不同操作系统的应用层代码和数据可能规范都不一样,表示层做的事情就是为了让信息可以在各操作系统和设备中通用

                功能:数据格式转换、代码转换、数据加密

                为了让不同设备之间能够有统一的规则,表示层会把数据转换为能与各系统格式兼容并且适合传输的格式

                表示层就像个翻译,会把数据相关的信息翻译成国际通用的规则

        3.会话层

                在信息传递时我们需要对其进行管理,比如消息是否发送完毕,对方是否收到,是否断开连接等等,会话层的主要工作就是完成这些内容

                功能:建立、管理和维护会话

                他主要负责数据传输中设置和维护网络中两台设备之间的通信连接;他不参与具体的传输,主要提供包括访问验证和会话管理在内的建立和维护应用之间通信的机制。

        4.传输层

                功能:建立、管理和维护端到端的连接。

                传输层也可称为运输层,运输层负责主机中两个进程之间的通信,功能是为端到端连接提供可靠的传输服务。它会在上一层的数据基础上添加标头。

                标头包含信息:发送方接收方的端口信息、协议信息等等

        5.网络层

                功能:IP选址、路由选择

                在网络环境中,两台设备之间可能会经过很多个数据链路,也可能还要经过很多通信子网;网络层的主要作用就是选择一条合适的路径用于通信。它会在上一层的数据基础上添加标头

                标头包含信息:IP地址、版本、长度等等信息。

        6.数据链路层

                它在物理层的上方确定0和1的分组方式,并且明确信息是发送给哪台设备的网卡(Mac地址)

                功能:将想要发送的信息构成一个数据包,叫做“帧”;每一帧分为两个部分:标头Head+数据Data。标头包含数据包的一些说明项,比如发送方和接收方的Mac地址、数据类型等。

        7.物理层

                他就是把设备连接起来的物理手段,主要规定了网络的一些电气特性,作用是负责传输0和1的电信号

                物理层将2进制数据利用电脉冲在物理媒介上实现比特流的传输

                功能:定义传输模式、定义传输速率、比特同步、比特编码等等

           OSI七层职能模型图

TCP/IP协议

        TCP/IP(Transmission Control Protocol/Internet Protocol,传输控制协议/网际协议)体系结构是指能够在多个不同网络间实现的协议簇;定义了消息在网络间进行传输的规则

​         TCP/IP 是一种体系结构,也代表了一系列的协议簇,同时,TCP 和 IP 又分别是建立在传输层和网络层的协议。而传输层协议还包含常见的面向无连接的 UDP 协议等。

        TCP/IP网络结构体系实际上是基于OSI七层模型设计出来的; OSI七层模型只是一个概念模型,他主要用于描述、讨论和理解单个网络功能,而TCP/IP协议是为了解决一组特定的问题而设计的,它是基于互联网开发的标准协议(规则)

        TCP/IP四层模型

        应用层

                应用层头:
                        协议头(FTP、HTTP等协议),一般决定传输信息的类型

                功能:为应用提供服务

  •                 根据需求选择传输协议
  •                 格式化数据,加密解密
  •                 建立管理和维护会话
        传输层

                传输层头:
                        协议端口头(TCP或UDP协议),一般决定传输信息的规则以及端口

                功能:建立管理和维护端到端的连接

        网络层

                网路层头:
                        IP头,决定传输路线,网络接口层头尾:产生帧(消息分段),决定最终路线

                功能:IP选址和路由选择

        网络接口层

                网络接口层头尾:
                        产生帧(消息分段),决定最终路线

                功能:1.提供一条准确无误的传输线路;2.传输数据的物理媒介

TCP/IP协议中的重要协议

        应用层协议:
                HTTP协议:超文本传输协议
                HTTPS协议:加密的超文本传输协议
                FTP协议:文件传输协议
                DNS:域名系统
        传输层协议:
                TCP协议:传输控制协议
                UDP协议:用户数据报协议

        网络层协议:
                IP协议

TCP协议

        TCP(Transmission Control Protocol,传输控制协议),是面向连接的协议,也就是说,在收发数据前,必须和对方建立可靠的连接;并且在消息传送过程中是有顺序的,并且是不会丢包(丢弃消息)的;如果某一条消息在传送过程中失败了,会重新发送消息,直到成功。
        它的特点是:

  •                 1.面向连接        两者之间必须建立可靠的连接
  •                 2.一对一         只能是1对1的建立连接
  •                 3.可靠性高        消息传送失败会重新发送,不允许丢包
  •                 4.有序的        是按照顺序进行消息发送的
三次握手、四次挥手


        三次握手建立连接
                第一次握手(C->S)
TCP连接请求,告诉服务器我要和你建立连接
                第二次握手(S->C)
TCP授予连接,告诉客户端准了,来吧
                第三次握手(C->S)
TCP确认连接,告诉服务器,来就来

        四次挥手断开连接
                第一次挥手(C->S)
告诉服务器我数据发完了,你如果还有消息就快发完
                第二次挥手(S->C)
告诉客户端我知道了,请继续等待我的消息
                第三次挥手(S->C)
告诉客户端消息发完了,你可以正式断开连接了
                第四次挥手(C->S)
告诉服务器我等一会如果没有收到你回复我就断开了

        TCP有了这三次握手,四次挥手的规则,可以提供可靠的服务,通过TCP连接传送的数据,可以做到无差错、不丢失、不重复、且按顺序到达,它让服务器和客户端之间的数据传递变得更加的可靠

        UDP协议

        UDP(User Datagram Protocol,用户数据报协议),是一种无需建立连接就可以发送封装的IP数据包的方法,提供面向事务的简单不可靠信息传送服务
        它的特点是:

  •         1.无连接 -- 两者之间无需建立连接
  •         2.可靠性低 --消息可能在传送过程中丢失,丢失后不会重发
  •         3.传输效率高 -- 由于它的可靠性低并且也无需建立连接,所以传输效率上更高一些
  •         4.n对n -- TCP只能1对1连接进行消息传递,而UDP由于无连接所以可以n对n
TCP和UDP的区别

网络通信方案概述

        网络游戏是以C/S模型为基础进行开发的,由客户端和服务端组成    

        通信方案大体分为两种:长连接(强联网)游戏和短连接(弱联网)游戏    

        弱联网和强联网游戏

        弱联网游戏:

                这种游戏不会频繁的进行数据通信,客户端和服务端之间每次连接只处理一次请求,服务端处理完客户端的请求后返回数据后就断开连接了

        弱联网游戏代表:        

                一般的三消类休闲游戏、卡牌游戏等都会是弱联网游戏,这些游戏的核心玩法都由客户端完成,客户端处理完成后只是告诉服务端一个结果,服务端验证结果即可,不需要随时通信比如:开心消消乐、刀塔传奇、我叫MT等等
        强联网游戏:

                这种游戏会频繁的和服务端进行通信,会一直和服务器保持连接,不停的和服务器之间交换数据
        强联网游戏代表:
                一般的MMORPG(角色扮演)、MOBA(多人在线竞技游戏)、ACT(动作游戏)等等都会是强联网游戏,这些游戏的部分核心逻辑是由客户端和服务端之间不停的在同步信息,比如:王者荣耀、守望先锋、和平精英等等!

        长连接和短连接游戏

        长连接和短连接游戏是按照网络游戏通信特点来划分的
        我们甚至可以认为
                弱联网游戏------>短连接游戏
                强联网游戏------>长连接游戏

        短连接游戏:
                需要传输数据时,建立连接,传输数据,获得响应,断开连接

                通信特点:需要通信时再连接,通信完毕断开连接

                通信方式:Http超文本传输协议、HTTPS安全的超文本传输协议(他们本质上是TCP协议)

        长连接游戏:

                不管是否需要传输数据,客户端与服务器一直处于连接状态,除非一端主动断开,或者出现意外情况(客户端关闭或服务端崩溃等)

                通信特点:连接一直建立,可以实时的传输数据

                通信方式:TCP传输控制协议 或 UDP用户数据报协议

        Socket、HTTP、FTP

                Socket:网络套接字,是对网络中不同主机上的应用进程之间进行双向通信的端点的抽象,一个套接字就是网络上进程通信的一段,提供了应用层进程利用网络协议交换数据的机制

                我们之后主要要学习的就是Socket网络套接字当中的各种API来进行网络通信

                主要用于制作长连接游戏《强联网游戏)

                Http/Https:(安全的)超文本传输协议,是一个简单的请求-响应协议,它通常运行在TCP协议之上,它指定了客户端可能发送给服务端什么样的信息以及得到什么样的响应。

                主要用于制作短连接游戏(弱联网游戏),也可以用来进行资源下载
                FTP:文件传输协议,是用于在网络上进行文件传输的一套标准协议,可以利用它来进行网络上资源的下载和上传。它也是基于TCP的传输,是面向连接的。

        通信基础       

        c#中IP地址类,端口类和域名解析类

                我们知道想要进行网络通信,得先进行网络连接,所以我们需要找到对应设备,IP和端口号是定位网络中设备必不可少的关键元素

        IPAddress类  

                初始化IP信息的方式

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

//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类

        IPEndPoint类将网络端点表示为IP地址和端口号,表现为IP地址和端口号的组合

        命名空间:System.Net;        类名:IPEndPoint

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

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

        域名解析

        域名解析也叫域名指向、服务器设置、域名配置以及反向IP登记等等
        说得简单点就是将好记的域名解析成IP
        IP地址是网络上标识站点的数字地址,但是IP地址相对来说记忆困难
        所以为了方便记忆,采用域名来代替IP地址标识站点地址。
        比如 我们要登录一个网页 www.baidu.com 这个就是域名 我们可以通过记忆域名来记忆一个远端服务器的地址,而不是记录一个复杂的IP地址

        域名解析就是域名到IP地址的转换过程。域名的解析工作由DNS服务器完成

        域名系统(英文:Domain Name System,缩写:DNS)是互联网的一项服务
        它作为将域名和IP地址相互映射的一个分布式数据库,能够使人更方便地访问互联网
        是因特网上解决网上机器命名的一种系统,因为IP地址记忆不方便,就采用了域名系统来管理名字和IP的对应关系

        IPHostEntry类

        主要作用:域名解析后的返回值 可以通过该对象获取IP地址、主机名等等信息

        命名空间:System.Net;        类名:IPHostEntry

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

        Dns类

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

         命名空间:System.Net;        类名:Dns


void Start(){
    //常用方法
    //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);
}

        序列化和反序列化

        可以先看  Unity的四种数据持久化方式  里面的序列化与反序列化

         序列化

                非字符串类型转字节数组

                        主要作用:除字符串的其它常用类型和字节数组相互转换

                        命名空间:System;        类名:BitConverter

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

                字符串类型转字节数组

                        主要作用:将字符串类型和字节数组相互转换,并且决定转换时使用的字符编码类型,网络通信时建议大家使用UTF-8类型

                        命名空间:System.Text;        类名:Encoding

byte[] byte2 = Encoding.UTF8.GetBytes("的卡萨福利卡决胜巅峰卡视角的副驾驶的");

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

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

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

     //单纯的转换一个变量为字节数组非常的简单
     //但是我们如何将一个类对象携带的所有信息放入到一个字节数组中呢
     //我们需要做以下几步
     //1.明确字节数组的容量(注意:在确定字符串字节长度时要考虑解析时如何处理)
     PlayerInfo info = new PlayerInfo();
     info.lev = 10;
     info.name = "Danny";
     info.atk = 88;
     info.sex = false;
     //得到的 这个Info数据 如果转换成 字节数组 那么字节数组容器需要的容量
     int indexNum = sizeof(int) + //lev int类型  4
                    sizeof(int) + //代表 name字符串转换成字节数组后 数组的长度 4
                    Encoding.UTF8.GetBytes(info.name).Length + //字符串具体字节数组的长度
                    sizeof(short) + //atk short类型 2
                    sizeof(bool); //sex bool类型 1

     //2.申明一个装载信息的字节数组容器
     byte[] playerBytes = new byte[indexNum];

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

     //等级
     BitConverter.GetBytes(info.lev).CopyTo(playerBytes, index);
     index += sizeof(int);

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

     //攻击力
     BitConverter.GetBytes(info.atk).CopyTo(playerBytes, index);
     index += sizeof(short);
     //性别
     BitConverter.GetBytes(info.sex).CopyTo(playerBytes, index);
     index += sizeof(bool);
 }
        反序列化

                字节数组转非字符串类型

                        主要作用:除字符串的其它常用类型和字节数组相互转换

                        命名空间:System;        类名:BitConverter

byte[] bytes = BitConverter.GetBytes(99);
int i = BitConverter.ToInt32(bytes, 0);
print(i);

        字节数组转字符串类型

                        主要作用:将字符串类型和字节数组相互转换,并且决定转换时使用的字符编码类型,网络通信时建议大家使用UTF-8类型

                        命名空间:System.Text;        类名:Encoding

byte[] bytes2 = Encoding.UTF8.GetBytes("123123空间大撒了房间阿斯利康放大镜");
string str = Encoding.UTF8.GetString(bytes2, 0, bytes2.Length);
print(str);

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

//1.获取到对应的字节数组
PlayerInfo info = new PlayerInfo();
info.lev = 10;
info.name = "Danny";
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);
封装序列化反序列化基类BaseData
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;
    }
}

Socket

        Socket套接字的作用:

        它是C#提供给我们用于网络通信的一个类(在其它语言当中也有对应的Socket类);Socket套接字是支持TCP/IP网络通信的基本操作单位

        命名空间:System.Net.Sockets;        类名:Socket
        一个套接字对象包含以下关键信息

  •         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控制消息协议

参数的常用搭配:
        SocketType.Dgram  +  ProtocolType.Udp  = UDP协议通信(常用)

Socket socketUdp = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);

        SocketType.Stream  +  ProtocolType.Tcp  = TCP协议通信(常用)

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

           SocketType.Raw  +  ProtocolType.Icmp  = Internet控制报文协议(了解)
           SocketType.Raw  +  ProtocolType.Raw  = 简单的IP包通信(了解)

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同步

实现客户端基本逻辑
//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);

//首先解析消息的ID
//使用字节数组中的前四个字节 得到ID
int msgID = BitConverter.ToInt32(receiveBytes, 0);
switch (msgID)
{
    case 1001:
        PlayerMsg msg = new PlayerMsg();
        msg.Reading(receiveBytes, 4);
        print(msg.playerID);
        print(msg.playerData.name);
        print(msg.playerData.atk);
        print(msg.playerData.lev);
        break;
}

print("收到服务端发来的消息:" + Encoding.UTF8.GetString(receiveBytes, 0, receiveNum));

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

//4.用Shutdown方法释放连接
socket.Shutdown(SocketShutdown.Both);
//5.关闭套接字
socket.Close();
实现服务端基本逻辑
//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相关方法收发数据
//发送
PlayerMsg msg = new PlayerMsg();
msg.playerID = 666;
msg.playerData = new PlayerData();
msg.playerData.name = "我是Danny的服务端";
msg.playerData.atk = 99;
msg.playerData.lev = 50;

socketClient.Send(msg.Writing());
//接受
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();
区分消息类型

        为发送的信息添加标识,比如添加消息ID

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

        举例说明:

        消息构成:如果选用int类型作为消息ID的类型,前4个字节为消息ID,后面的字节为数据类的内容,这样每次收到消息时,先把前4个字节取出来解析为消息ID,再根据ID进行消息反序列化即可

        创建消息基类,基类继承BaseData,基类添加获取消息ID的方法或者属性,让想要被发送的消息继承该类,实现序列化反序列化方法,修改客户端和服务端收发消息的逻辑

        客户端
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

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

        PlayerMsg

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

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;
    }
}
        服务端

                PlayerMsg

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;
    }
}
        分包粘包

                分包、黏包指在网络通信中由于各种因素(网络环境、API规则等)造成的消息与消息之间出现的两种状态

  • 分包:一个消息分成了多个消息进行发送
  • 黏包:一个消息和另一个消息黏在了一起
  • 注意:分包和黏包可能同时发生

                我们收到的消息都是以字节数组的形式在程序中体现;前4个字节是消息ID,后面的字节数组全部用来反序列化;如果出现分包、黏包会导致我们反序列化报错

        解决办法

  • 1、发送端给每个数据包添加包首部,首部中应该至少包含数据包的长度,这样接收端在接收到数据后,通过读取包首部的长度字段,便知道每一个数据包的实际长度了。
  • 2、发送端将每个数据包封装为固定长度(不够的可以通过补0填充),这样接收端每次从接收缓冲区中读取固定长度的数据就自然而然的把每个数据包拆分开来。
  • 3、可以在数据包之间设置边界,如添加特殊符号,这样,接收端通过这个边界就可以将不同的数据包拆分开。
心跳消息

        所谓心跳消息,就是在长连接中,客户端和服务端之间定期发送的一种特殊的数据包,用于通知对方自己还在线,以确保长连接的有效性。

        由于其发送的时间间隔往往是固定的持续的,就像是心跳一样一直存在,所以我们称之为心跳消息

        目的

  1.         避免非正常关闭客户端时,服务器无法正常收到关闭连接消息,通过心跳消息我们可以自定义超时判断,如果超时没有收到客户端消息,证明客户端已经断开连接
  2.         避免客户端长期不发送消息,防火墙或者路由器会断开连接,我们可以通过心跳消息一直保持活跃状态

        HeartMsg

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

        NetMgr

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异步

        异步方法和同步方法的区别

                同步方法:
                        方法中逻辑执行完毕后,再继续执行后面的方法
                异步方法:
                        方法中逻辑可能还没有执行完毕,就继续执行后面的内容

                异步方法的本质
                        往往异步方法当中都会使用多线程执行某部分逻辑,因为我们不需要等待方法中逻辑执行完毕就可以继续执行下面的逻辑了

注意:Unity中的协同程序中的某些异步方法,有的使用的是多线程有的使用的是迭代器分步执行
      Socket TCP通信中的异步方法(Begin开头方法)

        服务器相关
                BeginAccept
                EndAccept

        客户端相关
                BeginConnect
                EndConnect

        接收发送消息 服务端客户端通用

                接收消息
                        BeginReceive
                        EndReceive

                发送消息
                        BeginSend
                        EndSend

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

public class Lesson12 : MonoBehaviour
{
    private byte[] resultBytes = new byte[1024];
    // Start is called before the first frame update
    void Start()
    {
        //我们以一个异步倒计时方法举例
        //1.线程回调
        //CountDownAsync(5, ()=> {
        //    print("倒计时结束");
        //});
        //print("异步执行后的逻辑");

        //2.async和await 会等待线程执行完毕 继续执行后面的逻辑
        //相对第一种方式 可以让函数分步执行
        CountDownAsync(5);
        print("异步执行后的逻辑2");

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

        Socket socketTcp = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        //服务器相关
        //BeginAccept
        //EndAccept
        socketTcp.BeginAccept(AcceptCallBack, socketTcp);

        //客户端相关
        //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);

        //发送消息
        //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);


        //关键变量类型
        //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);
    }

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

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

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

    public void CountDownAsync(int second, UnityAction callBack)
    {
        Thread t = new Thread(() =>
        {
            while (true)
            {
                print(second);
                Thread.Sleep(1000);
                --second;
                if (second == 0)
                    break;
            }
            callBack?.Invoke();
        });
        t.Start();

        print("开始倒计时");
    }

    public async void CountDownAsync(int second)
    {
        print("倒计时开始");

        await Task.Run(() =>
        {
            while (true)
            {
                print(second);
                Thread.Sleep(1000);
                --second;
                if (second == 0)
                    break;
            }
        });

        print("倒计时结束");
    }
}

UDP

        客户端和服务端

  •         1.创建套接字Socket
  •         2.用Bind方法将套接字与本地地址进行绑定
  •         3.用ReceiveFrom和SendTo方法在套接字上收发消息
  •         4.用Shutdown方法释放连接
  •         5.关闭套接字

        黏包问题

                UDP本身作为无连接的不可靠的传输协议(适合频繁发送较小的数据包),他不会对数据包进行合并,一端发送什么数据,直接就发出去了,他不会对数据合并因此在UDP当中不会出现黏包问题(除非你手动进行黏包)

        分包问题

                由于UDP是不可靠的连接,消息传递过程中可能出现无序、丢包等情况,所以如果允许UDP进行分包,那后果将会是灾难性的。比如分包的后半段丢包或者比上半段先发来,我们在处理消息时将会非常困难,因此为了避免其分包,我们建议在发送UDP消息时控制消息的大小在MTU(最大传输单元)范围内

                MTU(Maximum Transmission Unit)最大传输单元,用来通知对方所能接受数据服务单元的最大尺寸。不同操作系统会提供用户一个默认值,以太网和802.3对数据帧的长度限制,其最大值分别是1500字节和1492字节。由于UDP包本身带有一些信息,因此建议:

                1.局域网环境下:1472字节以内(1500减去UDP头部28为1472)

                2.互联网环境下:548字节以内(老的ISP拨号网络标准值为576减去UDP头部28为548)

                只要遵守这个规则,就不会出现自动分包的情况

                如果想要发送的消息确实比较大,要大于548字节或1472字节这个限制呢?比如我们要发一个5000字节的数据,他是一条完整消息,我们可以进行手动分包,将5000拆分成多个消息,每个消息不超过限制,但是手动分包的前提是要解决UDP的丢包和无序问题
                我们可以将不可靠的UDP通信实现为可靠的UDP通信比如:在消息中加入序号、消息总包数、自己的包ID、长度等等信息并且实现消息确认、消息重发等功能

       简单UDP代码

                客户端

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

public class udpClient : 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"), 8081);
        socket.Bind(ipPoint);

        //3.发送到指定目标
        IPEndPoint remoteIpPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 8080);
        //指定要发送的字节数 和 远程计算机的 IP和端口
        socket.SendTo(Encoding.UTF8.GetBytes("Danny发来了UDP信息"), 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()
    {
        
    }
}

        服务端

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

public class udpServer : 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"), 8081);
        socket.Bind(ipPoint);

        //3.接受消息
        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));

        //4.发送到指定目标
        //指定要发送的字节数 和 远程计算机的 IP和端口
        socket.SendTo(Encoding.UTF8.GetBytes("Danny服务端发来了UDP信息"), remoteIpPoint2 );


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

    // Update is called once per frame
    void Update()
    {
        
    }
}
服务端同步的UDP代码

        ServerSocket

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

        Client

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);
            }
            
        }
    }
}
 UDP同步

        ServerSocket

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

        Client

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

        UdpNetMgr        

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通信中的异步方法

        BeginSendTo

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

        BeginReceiveFrom

socket.BeginReceiveFrom(cacheBytes, 0, cacheBytes.Length, SocketFlags.None, ref ipPoint, ReceiveFromOver, (socket, ipPoint));
       

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




rivate 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("接收失败");
        }
    }

FTP

        工作原理

                FTP (File Transfer Protocol)文件传输协议,是支持Internet文件传输的各种规则所组成的集合,这些规则使Internet用户可以把文件从一台主机拷贝到另一台主机上除此之外,FTP还提供登录、目录查询以及其他会话控制等功能

也就是说:FTP文件传输协议就是一个在网络中上传下载文件的一套规则

                本质:FTP的本质是TCP通信通过FTP传输文件,双方至少需要建立两个TCP连接

  •                 一个称为控制连接,用于传输FTP命令
  •                 一个称为数据连接,用于传输文件数据

                FTP的数据连接和控制连接方向一般是相反的,举例说明:
                        用户使用FTP客户端连接FTP服务区请求下载文件

                控制连接方向:
                        客户端主动连接服务器告知其下载命令

                数据连接方向
                        服务端主动连接客户端下发数据

                当客户端和FTP服务器建立控制连接后需要告诉服务器采用那种传输模式
                        1.主动模式(Port模式)
                                服务器主动连接客户端,然后传输文件

                        2.被动模式(Passive模式)

                                客户端主动连接服务器即控制连接和数据连接都由客户端发起

                在使用FTP进行数据传输时有两种数据传输方式

                        1.ASCI传输方式
                                以ASCII编码方式传输数据,适用于传输仅包含英文的命令和参数或者英文文本文件

                        2.二进制传输方式(建议使用该方式)

                                可以指定采用哪种编码传输命令和文件数据,如果传输的文件不是英文文件则应该采用该方式

                一般情况下,使用FTP传输文件时客户端必须先登录服务器,获得相应权限后才能上传或下载文件,服务器也可以允许用户匿名登录FTP,不需要都拥有一个合法账号

        FTP关键类
                NetworkCredential

        命名空间:System.Net           类名:NetworkCredential

        NetworkCredential通信凭证类         用于在Ftp文件传输时,设置账号密码

NetworkCredential n = new NetworkCredential("Danny", "Danny123");
                FtpWebRequest

        命名空间:System.Net           类名:FtpWebRequest

Ftp文件传输协议客户端操作类        主要用于:上传、下载、删除服务器上的文件

        重要方法
1.Create 创建新的WebRequest,用于进行Ftp相关操作

FtpWebRequest req = FtpWebRequest.Create(new Uri("ftp://127.0.0.1/Test.txt")) as FtpWebRequest;   

2.Abort  如果正在进行文件传输,用此方法可以终止传输

req.Abort();

3.GetRequestStream  获取用于上传的流

Stream s = req.GetRequestStream();

4.GetResponse  返回FTP服务器响应

FtpWebResponse res = req.GetResponse() as FtpWebResponse;

        重要成员

1.Credentials 通信凭证,设置为NetworkCredential对象

req.Credentials = n;

2.KeepAlive bool值,当完成请求时是否关闭到FTP服务器的控制连接(默认为true,不关闭)

req.KeepAlive = false;

3.Method  操作命令设置

//  WebRequestMethods.Ftp类中的操作命令属性
//  DeleteFile  删除文件
//  DownloadFile    下载文件    
//  ListDirectory   获取文件简短列表
//  ListDirectoryDetails    获取文件详细列表
//  MakeDirectory   创建目录
//  RemoveDirectory 删除目录
//  UploadFile  上传文件
req.Method = WebRequestMethods.Ftp.DownloadFile;

4.UseBinary 是否使用2进制传输

req.UseBinary = true;

5.RenameTo    重命名

req.RenameTo = "myTest.txt";
        FtpWebResponse

        命名空间:System.Net              类名:FtpWebResponse

        它是用于封装FTP服务器对请求的响应,它提供操作状态以及从服务器下载数据,我们可以通过FtpWebRequest对象中的GetResponse()方法获取,当使用完毕时,要使用Close释放

//通过它来真正的从服务器获取内容
FtpWebResponse res = req.GetResponse() as FtpWebResponse;

        重要方法:
1.Close:释放所有资源

res.Close();

  2.GetResponseStream:返回从FTP服务器下载数据的流

Stream stream = res.GetResponseStream();

        重要成员:

1.ContentLength:接受到数据的长度

 print(res.ContentLength);

2.ContentType:接受数据的类型

 print(res.ContentType);

3.StatusCode:FTP服务器下发的最新状态码

print(res.StatusCode);

4.StatusDescription:FTP服务器下发的状态代码的文本

print(res.StatusDescription);

5.BannerMessage:登录前建立连接时FTP服务器发送的消息

print(res.BannerMessage);

6.ExitMessage:FTP会话结束时服务器发送的消息

print(res.ExitMessage);

7.LastModified:FTP服务器上的文件的上次修改日期和时间

 print(res.LastModified);
FTP上传
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Net;
using UnityEngine;

public class Lesson20 : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        #region 知识点一 使用FTP上传文件关键点
        //1.通信凭证
        //  进行Ftp连接操作时需要的账号密码
        //2.操作命令 WebRequestMethods.Ftp
        //  设置你想要进行的Ftp操作
        //3.文件流相关 FileStream 和 Stream
        //  上传和下载时都会使用的文件流
        //4.保证FTP服务器已经开启
        //  并且能够正常访问
        #endregion

        #region 知识点二 FTP上传
        try
        {
            //1.创建一个Ftp连接
            FtpWebRequest req = FtpWebRequest.Create(new Uri("ftp://192.168.0.1/pic.png")) as FtpWebRequest;
            //2.设置通信凭证(如果不支持匿名 就必须设置这一步)
            //将代理相关信息置空 避免 服务器同时有http相关服务 造成冲突
            req.Proxy = null;
            NetworkCredential n = new NetworkCredential("Danny", "Danny123");
            req.Credentials = n;
            //请求完毕后 是否关闭控制连接,如果想要关闭,可以设置为false
            req.KeepAlive = false;
            //3.设置操作命令
            req.Method = WebRequestMethods.Ftp.UploadFile;//设置命令操作为 上传文件
            //4.指定传输类型
            req.UseBinary = true;
            //5.得到用于上传的流对象
            Stream upLoadStream = req.GetRequestStream();

            //6.开始上传
            using (FileStream file = File.OpenRead(Application.streamingAssetsPath + "/test.png"))
            {
                //我们可以一点一点的把这个文件中的字节数组读取出来 然后存入到 上传流中
                byte[] bytes = new byte[1024];

                //返回值 是真正从文件中读了多少个字节
                int contentLength = file.Read(bytes, 0, bytes.Length);
                //不停的去读取文件中的字节 除非读取完毕了 不然一直读 并且写入到上传流中
                while (contentLength != 0)
                {
                    //写入上传流中
                    upLoadStream.Write(bytes, 0, contentLength);
                    //写完了继续读
                    contentLength = file.Read(bytes, 0, bytes.Length);
                }
                //除了循环就证明 写完了 
                file.Close();
                upLoadStream.Close();
                //上传完毕
                print("上传结束");
            }
        }
        catch (Exception e)
        {
            print("上传出错 失败" + e.Message);
        }
        #endregion

        #region 总结
        //C#已经把Ftp相关操作封装的很好了
        //我们只需要熟悉API,直接使用他们进行FTP上传即可
        //我们主要做的操作是
        //把本地文件流读出字节数据写入到要上传的FTP流中

        //FTP上传相关API也有异步方法
        //使用上和以前的TCP相关类似
        //这里不赘述
        #endregion
    }

    // Update is called once per frame
    void Update()
    {
        
    }
}
FTP下载
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Net;
using UnityEngine;

public class Lesson21 : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        #region 知识点一 使用FTP下载文件关键点
        //1.通信凭证
        //  进行Ftp连接操作时需要的账号密码
        //2.操作命令 WebRequestMethods.Ftp
        //  设置你想要进行的Ftp操作
        //3.文件流相关 FileStream 和 Stream
        //  上传和下载时都会使用的文件流
        //  下载文件流使用FtpWebResponse类获取
        //4.保证FTP服务器已经开启
        //  并且能够正常访问
        #endregion

        #region 知识点二 FTP下载
        try
        {
            //1.创建一个Ftp连接
            //这里和上传不同,上传的文件名 是自己定义的  下载的文件名 一定是资源服务器上有的
            FtpWebRequest req = FtpWebRequest.Create(new Uri("ftp://192.168.50.49/text.jpg")) as FtpWebRequest;
            //2.设置通信凭证(如果不支持匿名 就必须设置这一步)
            req.Credentials = new NetworkCredential("Danny", "Danny123");
            //请求完毕后 是否关闭控制连接,如果要进行多次操作 可以设置为false
            req.KeepAlive = false;
            //3.设置操作命令
            req.Method = WebRequestMethods.Ftp.DownloadFile;
            //4.指定传输类型
            req.UseBinary = true;
            //代理设置为空
            req.Proxy = null;
            //5.得到用于下载的流对象
            //相当于把请求发送给FTP服务器 返回值 就会携带我们想要的信息
            FtpWebResponse res = req.GetResponse() as FtpWebResponse;
            //这就是下载的流
            Stream downLoadStream = res.GetResponseStream();

            //6.开始下载
            print(Application.persistentDataPath);
            using (FileStream fileStream = File.Create(Application.persistentDataPath + "/downLoadText.jpg"))
            {
                byte[] bytes = new byte[1024];
                //读取下载下来的流数据
                int contentLength = downLoadStream.Read(bytes, 0, bytes.Length);
                //一点一点的 下载到本地流中
                while (contentLength != 0)
                {
                    //把读取出来的字节数组 写入到本地文件流中
                    fileStream.Write(bytes, 0, contentLength);
                    //那我们继续读
                    contentLength = downLoadStream.Read(bytes, 0, bytes.Length);
                }
                //下载结束 关闭流
                downLoadStream.Close();
                fileStream.Close();
            }
            print("下载结束");
        }
        catch (Exception e)
        {
            print("下载出错" + e.Message);
        }
        #endregion

        #region 总结
        //C#已经把Ftp相关操作封装的很好了
        //我们只需要熟悉API,直接使用他们进行FTP下载即可
        //我们主要做的操作是
        //把下载文件的FTP流读出字节数据写入到本地文件流中
        #endregion
    }

    // Update is called once per frame
    void Update()
    {
        
    }
}
封装FTP管理类
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.Events;

public class FtpMgr
{
    private static FtpMgr instance = new FtpMgr();
    public static FtpMgr Instance => instance;

    //远端FTP服务器的地址
    private string FTP_PATH = "ftp://127.0.0.1/";
    //用户名和密码
    private string USER_NAME = "Danny";
    private string PASSWORD = "Danny123";

    /// <summary>
    /// 上传文件到Ftp服务器(异步)
    /// </summary>
    /// <param name="fileName">FTP上的文件名</param>
    /// <param name="localPath">本地文件路径</param>
    /// <param name="action">上传完毕后想要做什么的委托函数</param>
    public async void UpLoadFile(string fileName, string localPath, UnityAction action = null)
    {
        await Task.Run(() =>
        {
            try
            {
                //通过一个线程执行这里面的逻辑 那么就不会影响主线程了
                //1.创建一个Ftp连接
                FtpWebRequest req = FtpWebRequest.Create(new Uri(FTP_PATH + fileName)) as FtpWebRequest;
                //2.进行一些设置
                //凭证
                req.Credentials = new NetworkCredential(USER_NAME, PASSWORD);
                //是否操作结束后 关闭 控制连接
                req.KeepAlive = false;
                //传输类型
                req.UseBinary = true;
                //操作类型
                req.Method = WebRequestMethods.Ftp.UploadFile;
                //代理设置为空
                req.Proxy = null;
                //3.上传
                Stream upLoadStream = req.GetRequestStream();
                //开始上传
                using (FileStream fileStream = File.OpenRead(localPath))
                {
                    byte[] bytes = new byte[1024];
                    //返回值 为具体读取了多少个字节
                    int contentLength = fileStream.Read(bytes, 0, bytes.Length);
                    //有数据就上传
                    while (contentLength != 0)
                    {
                        //读了多少就写(上传)多少
                        upLoadStream.Write(bytes, 0, contentLength);
                        //继续从本地文件中读取数据
                        contentLength = fileStream.Read(bytes, 0, bytes.Length);
                    }
                    //上传结束
                    fileStream.Close();
                    upLoadStream.Close();
                }
                Debug.Log("上传成功");
            }
            catch (Exception e)
            {
                Debug.Log("上传文件出错" + e.Message);
            }
        });
        //上传结束后 你想在外部做的事情
        action?.Invoke();
    }

    /// <summary>
    /// 下载文件从Ftp服务器当中(异步)
    /// </summary>
    /// <param name="fileName">FTP上想要下载的文件名</param>
    /// <param name="localPath">存储的本地文件路径</param>
    /// <param name="action">下载完毕后想要做什么的委托函数</param>
    public async void DownLoadFile(string fileName, string localPath, UnityAction action = null)
    {
        await Task.Run(()=> {
            try
            {
                //1.创建一个Ftp连接
                FtpWebRequest req = FtpWebRequest.Create(new Uri(FTP_PATH + fileName)) as FtpWebRequest;
                //2.进行一些设置
                //凭证
                req.Credentials = new NetworkCredential(USER_NAME, PASSWORD);
                //是否操作结束后 关闭 控制连接
                req.KeepAlive = false;
                //传输类型
                req.UseBinary = true;
                //操作类型
                req.Method = WebRequestMethods.Ftp.DownloadFile;
                //代理设置为空
                req.Proxy = null;
                //3.下载
                FtpWebResponse res = req.GetResponse() as FtpWebResponse;
                Stream downLoadStream = res.GetResponseStream();
                //写入到本地文件中
                using (FileStream fileStream = File.Create(localPath))
                {
                    byte[] bytes = new byte[1024];
                    //读取数据
                    int contentLength = downLoadStream.Read(bytes, 0, bytes.Length);
                    //一点一点的写入
                    while (contentLength != 0)
                    {
                        //读多少 写多少
                        fileStream.Write(bytes, 0, contentLength);
                        //继续读
                        contentLength = downLoadStream.Read(bytes, 0, bytes.Length);
                    }
                    fileStream.Close();
                    downLoadStream.Close();
                }
                res.Close();

                Debug.Log("下载成功");
            }
            catch (Exception e)
            {
                Debug.Log("下载失败" + e.Message);
            }
        });

        //如果下载结束有想做的事情 在这里调用外部传入的委托函数
        action?.Invoke();
    }


    /// <summary>
    /// 移除指定的文件
    /// </summary>
    /// <param name="fileName">文件名</param>
    /// <param name="action">移除过后想做什么的委托函数</param>
    public async void DeleteFile(string fileName, UnityAction<bool> action = null)
    {
        await Task.Run(()=> {
            try
            {
                //通过一个线程执行这里面的逻辑 那么就不会影响主线程了
                //1.创建一个Ftp连接
                FtpWebRequest req = FtpWebRequest.Create(new Uri(FTP_PATH + fileName)) as FtpWebRequest;
                //2.进行一些设置
                //凭证
                req.Credentials = new NetworkCredential(USER_NAME, PASSWORD);
                //是否操作结束后 关闭 控制连接
                req.KeepAlive = false;
                //传输类型
                req.UseBinary = true;
                //操作类型
                req.Method = WebRequestMethods.Ftp.DeleteFile;
                //代理设置为空
                req.Proxy = null;
                //3.真正的删除
                FtpWebResponse res = req.GetResponse() as FtpWebResponse;
                res.Close();

                action?.Invoke(true);
            }
            catch (Exception e)
            {
                Debug.Log("移除失败" + e.Message);
                action?.Invoke(false);
            }        
        });
    }


    /// <summary>
    /// 获取FTP服务器上某个文件的大小 (单位 是 字节)
    /// </summary>
    /// <param name="fileName">文件名</param>
    /// <param name="action">获取成功后传递给外部 具体的大小</param>
    public async void GetFileSize(string fileName, UnityAction<long> action = null)
    {
        await Task.Run(() => {
            try
            {
                //通过一个线程执行这里面的逻辑 那么就不会影响主线程了
                //1.创建一个Ftp连接
                FtpWebRequest req = FtpWebRequest.Create(new Uri(FTP_PATH + fileName)) as FtpWebRequest;
                //2.进行一些设置
                //凭证
                req.Credentials = new NetworkCredential(USER_NAME, PASSWORD);
                //是否操作结束后 关闭 控制连接
                req.KeepAlive = false;
                //传输类型
                req.UseBinary = true;
                //操作类型
                req.Method = WebRequestMethods.Ftp.GetFileSize;
                //代理设置为空
                req.Proxy = null;
                //3.真正的获取
                FtpWebResponse res = req.GetResponse() as FtpWebResponse;
                //把大小传递给外部
                action?.Invoke(res.ContentLength);

                res.Close();
            }
            catch (Exception e)
            {
                Debug.Log("获取大小失败" + e.Message);
                action?.Invoke(0);
            }
        });
    }


    /// <summary>
    /// 创建一个文件夹 在FTP服务器上
    /// </summary>
    /// <param name="directoryName">文件夹名字</param>
    /// <param name="action">创建完成后的回调</param>
    public async void CreateDirectory(string directoryName, UnityAction<bool> action = null)
    {
        await Task.Run(() => {
            try
            {
                //通过一个线程执行这里面的逻辑 那么就不会影响主线程了
                //1.创建一个Ftp连接
                FtpWebRequest req = FtpWebRequest.Create(new Uri(FTP_PATH + directoryName)) as FtpWebRequest;
                //2.进行一些设置
                //凭证
                req.Credentials = new NetworkCredential(USER_NAME, PASSWORD);
                //是否操作结束后 关闭 控制连接
                req.KeepAlive = false;
                //传输类型
                req.UseBinary = true;
                //操作类型
                req.Method = WebRequestMethods.Ftp.MakeDirectory;
                //代理设置为空
                req.Proxy = null;
                //3.真正的创建
                FtpWebResponse res = req.GetResponse() as FtpWebResponse;
                res.Close();

                action?.Invoke(true);
            }
            catch (Exception e)
            {
                Debug.Log("创建文件夹失败" + e.Message);
                action?.Invoke(false);
            }
        });
    }

    /// <summary>
    /// 过去所有文件名
    /// </summary>
    /// <param name="directoryName">文件夹路径</param>
    /// <param name="action">返回给外部使用的 文件名列表</param>
    public async void GetFileList(string directoryName, UnityAction<List<string>> action = null)
    {
        await Task.Run(() => {
            try
            {
                //通过一个线程执行这里面的逻辑 那么就不会影响主线程了
                //1.创建一个Ftp连接
                FtpWebRequest req = FtpWebRequest.Create(new Uri(FTP_PATH + directoryName)) as FtpWebRequest;
                //2.进行一些设置
                //凭证
                req.Credentials = new NetworkCredential(USER_NAME, PASSWORD);
                //是否操作结束后 关闭 控制连接
                req.KeepAlive = false;
                //传输类型
                req.UseBinary = true;
                //操作类型
                req.Method = WebRequestMethods.Ftp.ListDirectory;
                //代理设置为空
                req.Proxy = null;
                //3.真正的创建
                FtpWebResponse res = req.GetResponse() as FtpWebResponse;
                //把下载的信息流 转换成StreamReader对象 方便我们一行一行的读取信息
                StreamReader streamReader = new StreamReader(res.GetResponseStream());

                //用于存储文件名的列表
                List<string> nameStrs = new List<string>();
                //一行行的读取
                string line = streamReader.ReadLine();
                while (line != null)
                {
                    nameStrs.Add(line);
                    line = streamReader.ReadLine();
                }
                res.Close();

                action?.Invoke(nameStrs);
            }
            catch (Exception e)
            {
                Debug.Log("获取文件列表失败" + e.Message);
                action?.Invoke(null);
            }
        });
    }
}

HTTP

        HTTP是什么

                 HTTP(HyperText Transfer Protocol)超文本传输协议,是因特网上应用最为广泛的一种网络传输协议。最初设计HTTP的目的是为了提供一种发布和接收由文本文件组成的HTML页面的方法,后来发展到除了文本数据外,还可以传输图片、音频、视频、压缩文件以及各种程序文件等;HTTP主要用于超文本传输,因此相对FTP显得更简单一些,目前常见的HTTP标准是HTTP/1.1.
                简而言之:HTTP超文本传输协议就是一个在网络中上传下载文件的一套规则

        HTTP工作原理

                本质:HTTP的本质也是TCP通信;HTTP定义了Web客户端(一般指浏览器)如何从Web服务器请求Web页面,以及服务器如何把Web页面传送给客户端。HTTP客户端首先与服务器建立TCP连接,然后客户端通过套接字发送HTTP请求,并通过套接字接收HTTP响应;由于HTTP采用TCP传输数据,因此不会丢包、不会乱序。

        特点

        1.HTTP是以TCP方式工作

                在HTTP/1.0中,客户端和服务器建立TCP连接后,发送一个请求到服务器,服务器发送一个应答给客户端,然后立即断开TCP连接,他们的主要步骤为:

                1.客户端与服务端建立TCP连接

                2.客户端向服务端发出请求

                3.若服务端接受请求,则回送响应码和所需的信息

                4.客户端与服务端断开TCP连接

                需要注意,HTTP/1.1 支持持久连接,即客户端和服务端建立连接后,可以发送请求和接收应答,然后迅速地发送另一个请求和接收另一个应答。持久连接也使得在得到上一个请求的应答之前能够发送多个请求,这就是HTTP/1.1与HTTP/1.0的明显不同之处,除此之外,HTTP/1.1可以发送的请求类型也比HTTP/1.0多
                目前市面上的Web服务器软件和浏览器软件基本都是支持HTTP/1.1版本的,目前使用的基本上都是HTTP/1.1版本

                连接->请求->响应->断开(目前的HTTP/1.1支持长久连接)

        2.HTTP是无状态的

        无状态指:
                客户端发送一次请求后,服务端并没有存储关于该客户端的任何状态信息,即使客户端再次请求同一个对象,服务端仍会重新发送这个对象,不会在意之前是否已经向客户端发送过这个对象
                简而言之:HTTP通信就是客户端要什么来什么,想要多少来多少,因为你要过了而不给你,不会记录你要过的状态

        3.HTTP使用元信息作为标头

                HTTP通过添加标头(header)的方式向服务端提供本次HTTP请求的相关信息,即在主要数据前添加一部分额外信息,称为元信息(metainformation),元信息里主要包含:传送的对象属于哪种类型,采用的是哪种编码等等
                简而言之:HTTP的元信息标头,类似之前Socket通信时用于区分消息类型处理分包黏包时,在消息体前方加的自定义信息。它也定义了类似的规则,在头部包含了一些额外信息在HTTP协议中

        HTTP协议的请求类型和响应状态码

                请求类型
                        HTTP/1.0中:
                        GET、POST、HEAD

                        HTTP/1.1中:
                        GET、POST、HEAD、PUT、DELETE、OPTIONS、TRACE、CONNECT
                响应状态码
                        1xx、2xx、3xx、4xx、5xx

        HTTP协议的请求类型
                每一种请求方法,其实就是在HTTP请求的头部信息包含的内容不同而已内容发送的格式为:

        HTTP协议的响应状态码
                客户端向服务端发送请求后,服务端会返回HTTP响应HTTP响应的一般格式为:

                相对HTTP请求的格式,只有第一部分不同,由请求行变成了状态行

        状态行中主要内容有:
                1.HTTP版本号
                2.3位数字组成的状态码

  •                 1xx消息:请求已被服务端接收,继续处理
  •                 2xx成功:请求已成功被服务端理解并接收
  •                 3xx重定向:需要后续操作才能完成这一请求
  •                 4xx请求错误:请求含有语法错误或者无法被执行
  •                 5xx服务器错误:服务端在处理某个正确请求时发牛错误

        HTTP常用状态码

        关键类
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Net;
using UnityEngine;

public class httpClass : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        #region 知识点一 HttpWebRequest类
        //命名空间:System.Net
        //HttpWebRequest是主要用于发送客户端请求的类
        //主要用于:发送HTTP客户端请求给服务器,可以进行消息通信、上传、下载等等操作

        //重要方法
        //1.Create 创建新的WebRequest,用于进行HTTP相关操作
        HttpWebRequest req = HttpWebRequest.Create(new Uri("http://192.168.50.109:8000/Http_Server/")) as HttpWebRequest;
        //2.Abort  如果正在进行文件传输,用此方法可以终止传输 
        //req.Abort();
        //3.GetRequestStream  获取用于上传的流
        Stream s = req.GetRequestStream();
        //4.GetResponse  返回HTTP服务器响应
        HttpWebResponse res = req.GetResponse() as HttpWebResponse;
        //5.Begin/EndGetRequestStream 异步获取用于上传的流
        //req.BeginGetRequestStream()
        //6.Begin/EndGetResponse 异步获取返回的HTTP服务器响应
        //req.BeginGetResponse()

        //重要成员
        //1.Credentials 通信凭证,设置为NetworkCredential对象
        req.Credentials = new NetworkCredential("", "");
        //2.PreAuthenticate 是否随请求发送一个身份验证标头,一般需要进行身份验证时需要将其设置为true
        req.PreAuthenticate = true;

        //3.Headers 构成标头的名称/值对的集合
        //req.Headers
        //4.ContentLength 发送信息的字节数 上传信息时需要先设置该内容长度
        req.ContentLength = 100;
        //5.ContentType 在进行POST请求时,需要对发送的内容进行内容类型的设置
        req.ContentType = "";
        //6.Method  操作命令设置
        //  WebRequestMethods.Http类中的操作命令属性
        //  Get     获取请求,一般用于获取数据
        //  Post    提交请求,一般用于上传数据,同时可以获取
        //  Head    获取和Get一致的内容,只是只会返回消息头,不会返回具体内容
        //  Put     向指定位置上传最新内容
        //  Connect 表示与代理一起使用的 HTTP CONNECT 协议方法,该代理可以动态切换到隧道
        //  MkCol   请求在请求 URI(统一资源标识符)指定的位置新建集合
        req.Method = WebRequestMethods.Http.Get;

        //了解该类的更多信息
        //https://docs.microsoft.com/zh-cn/dotnet/api/system.net.httpwebrequest?view=net-6.0
        #endregion

        #region 知识点二 HttpWebResponse类
        //命名空间:System.Net
        //它主要用于获取服务器反馈信息的类
        //我们可以通过HttpWebRequest对象中的GetResponse()方法获取
        //当使用完毕时,要使用Close释放

        //重要方法:
        //1.Close:释放所有资源
        //res.Close();
        //2.GetResponseStream:返回从FTP服务器下载数据的流
        //res.GetResponseStream()

        //重要成员:
        //1.ContentLength:接受到数据的长度
        //res.ContentLength
        //2.ContentType:接受数据的类型
        //res.ContentType
        //3.StatusCode:HTTP服务器下发的最新状态码
        //res.StatusCode
        //4.StatusDescription:HTTP服务器下发的状态代码的文本
        //res.StatusDescription
        //5.BannerMessage:登录前建立连接时HTTP服务器发送的消息
        //6.ExitMessage:HTTP会话结束时服务器发送的消息
        //7.LastModified:HTTP服务器上的文件的上次修改日期和时间

        //了解该类的更多信息
        //https://docs.microsoft.com/zh-cn/dotnet/api/system.net.httpwebresponse?view=net-6.0
        #endregion

        #region 知识点三 NetworkCredential、Uri、Stream、FileStream类
        //这些类我们在之前Ftp时已经使用过了
        //在HTTP通讯时使用方式不变
        #endregion

    }

    // Update is called once per frame
    void Update()
    {
        
    }
}
HTTP  GET获取数据
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Net;
using UnityEngine;

public class getTest : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        #region 知识点一 检测资源可用性
        try
        {
            //利用Head请求类型,获取信息
            //1.创建HTTP通讯用连接对象HttpWebRequest对象
            HttpWebRequest req = HttpWebRequest.Create(new Uri("http://192.168.50.49:8000/Http_Server/实战就业路线.jpg")) as HttpWebRequest;
            //2.设置请求类型 或 其它相关参数
            req.Method = WebRequestMethods.Http.Head;
            req.Timeout = 2000;
            //3.发送请求,获取响应结果HttpWebResponse对象
            HttpWebResponse res = req.GetResponse() as HttpWebResponse;

            if (res.StatusCode == HttpStatusCode.OK)
            {
                print("文件存在且可用");
                print(res.ContentLength);
                print(res.ContentType);

                res.Close();
            }
            else
                print("文件不能用" + res.StatusCode);
        }
        catch (WebException w)
        {
            print("获取出错" + w.Message + w.Status);
        }
        #endregion

        #region 知识点二 下载资源
        //利用Get请求类型,下载资源
        try
        {
            //1.创建HTTP通讯用连接对象HttpWebRequest对象
            HttpWebRequest req = HttpWebRequest.Create(new Uri("http://192.168.50.49:8000/Http_Server/实战就业路线.jpg")) as HttpWebRequest;
            //2.设置请求类型 或 其它相关参数
            req.Method = WebRequestMethods.Http.Get;
            req.Timeout = 3000;
            //3.发送请求,获取响应结果HttpWebResponse对象
            HttpWebResponse res = req.GetResponse() as HttpWebResponse;
            //4.获取响应数据流,写入本地路径
            if (res.StatusCode == HttpStatusCode.OK)
            {
                print(Application.persistentDataPath);
                using (FileStream fileStream = File.Create(Application.persistentDataPath + "/httpDownLoad.jpg"))
                {
                    Stream downLoadStream = res.GetResponseStream();
                    byte[] bytes = new byte[2048];
                    //读取数据
                    int contentLength = downLoadStream.Read(bytes, 0, bytes.Length);
                    //一点一点的写入本地
                    while (contentLength != 0)
                    {
                        fileStream.Write(bytes, 0, contentLength);
                        contentLength = downLoadStream.Read(bytes, 0, bytes.Length);
                    }
                    fileStream.Close();
                    downLoadStream.Close();
                    res.Close();
                }
                print("下载成功");
            }
            else
                print("下载失败" + res.StatusCode);
        }
        catch (WebException w)
        {
            print("下载出错" + w.Status + w.Message);
        }
        #endregion

        #region 知识点三 Get请求类型携带额外信息
        //我们在进行HTTP通信时,可以在地址后面加一些额外参数传递给服务端
        //一般在和短连接游戏服务器通讯时,需要携带额外信息
        //举例:
        //http://www.aspxfans.com:8080/news/child/index.asp?boardID=5&ID=24618&page=1
        //这个链接可以分成几部分
        //1.协议部分:取决于服务器端使用的哪种协议
        //http://  —— 普通的http超文本传输协议
        //https:// —— 加密的超文本传输协议

        //2.域名部分:
        //www.aspxfans.com
        //也可以填写服务器的公网IP地址

        //3.端口部分:
        //8080
        //可以不写,如果不写默认为80

        //4.虚拟目录部分:
        //news/child/
        //域名后的/开始,到最后一个/之前的部分

        //5.文件名部分:
        //index.asp
        //?之前的最后一个/后的部分

        //6.参数部分:
        //boardID=5&ID=24618&page=1
        //?之后的部分就是参数部分,多个参数一&分隔开
        //这里有三个参数
        //boardID = 5
        //ID = 24618
        //page = 1

        //我们在和服务端进行通信时,只要按照这种规则格式进行通信,就可以传递参数给对象
        //主要可用于:
        //1.web网站服务器
        //2.游戏短连接服务器
        //等
        #endregion

        #region 总结
        //1.Head请求类型
        //主要用于获取文件的一些基础信息 可以用于确定文件是否存在
        
        //2.Get请求类型 主要用于传递信息给服务器,用于获取具体信息
        //  服务器返回的信息,可以通过Response中的流来获取
        //  用Get请求时,可以在连接中携带一些额外参数(在链接后面加上 ?参数名=参数值&参数名=参数值&参数名=参数值&。。。。)
        //  正常的http服务器应用程序,都会去解析Get请求时连接中的参数进行逻辑处理(后端程序的工作)
        //  我们主要要掌握的知识点:
        //  1.额外参数按格式书写
        //  2.通过response对象中的流来获取返回的数据(数据的类型多种多样,可以是文件、自定义消息等等,我们按照规则解析即可)

        //3.在和http服务器通信时,我们经常会使用额外参数的形式传递信息,特别是以后和一些运营平台对接时

        //4.文件下载功能和Ftp非常类似,只是其中使用的类、协议、请求类型不同而已
        #endregion
    }

    // Update is called once per frame
    void Update()
    {
        
    }
}
POST知识点及与GET的区别
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Text;
using UnityEngine;

public class poseTest : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        #region 知识点一 Get和Post的区别
        //这两个请求类型他们的主要区别是什么呢?

        //1.主要用途
        //  Get — 一般从指定的资源请求数据,主要用于获取数据
        //  Post — 一般向指定的资源提交想要被处理的数据,主要用于上传数据

        //2.相同点
        //  Get和Post都可以传递一些额外的参数数据给服务端

        //3.不同点
        //  3-1:在传递参数时,Post相对Get更加的安全,因为Post看不到参数
        //      Get传递的参数都包含在连接中(URL资源定位地址),是暴露式的 ?参数名=参数值&参数名=参数值
        //      Post传递的参数放在请求数据中,不会出现在URL中,是隐藏式的
        //
        //  3-2:Get在传递数据时有大小的限制,因为它主要是在连接中拼接参数,而URL的长度是有限制的(最大长度一般为2048个字符)
        //      Post在传递数据时没有限制
        //
        //  3-3:在浏览器中Get请求能被缓存,Post不能缓存

        //  3-4:传输次数可能不同
        //      Get:  建立连接——>请求行、请求头、请求数据一次传输——>获取响应——>断开连接
        //      Post: 建立连接——>传输可能分两次——>请求行,请求头第一次传输——>请求数据第二次传输——>获取响应——>断开

        //对于前端来说,其实Get和Post都是能够获取和传递数据的,后端只要处理对应逻辑返回响应信息即可
        //但是由于他们的这些特点
        //我们在实际使用时建议Get用于获取,Post用于上传
        //如果想要传递一些不想暴露在外部的参数信息,建议使用Post,它更加的安全
        #endregion

        #region 知识点二 Post如何携带额外参数
        //关键点:将Content-Type设置为 application/x-www-form-urlencoded 键值对类型
        HttpWebRequest req = HttpWebRequest.Create("http://192.168.50.109:8000/Http_Server/") as HttpWebRequest;
        req.Method = WebRequestMethods.Http.Post;
        req.Timeout = 2000;
        //设置上传的内容的类型
        req.ContentType = "application/x-www-form-urlencoded";

        //我们要上传的数据
        string str = "Name=MrTang&ID=2";
        byte[] bytes = Encoding.UTF8.GetBytes(str);
        //我们在上传之前一定要设置内容的长度
        req.ContentLength = bytes.Length;
        //上传数据
        Stream stream = req.GetRequestStream();
        stream.Write(bytes, 0, bytes.Length);
        stream.Close();
        //发送数据 得到响应结果
        HttpWebResponse res = req.GetResponse() as HttpWebResponse;
        print(res.StatusCode);

        #endregion

        #region 知识点三 ContentType的常用类型
        //ContentType的构成:
        //内容类型;charset=编码格式;boundary=边界字符串
        //text/html;charset=utf-8;boundary=自定义字符串
        req.ContentType = "变化的;charset=变化的";


        //其中内容类型有:
        //文本类型text:
        //text/plain 没有特定子类型就是它(重要)
        //text/html
        //text/css
        //text/javascript

        //图片类型image:
        //image/gif
        //image/png
        //image/jpeg
        //image/bm
        //image/webp
        //image/x-icon
        //image/vnd.microsoft.icon

        //音频类型audio:
        //audio/midi
        //audio/mpeg
        //audio/webm
        //audio/ogg
        //audio/wav

        //视频类型video:
        //video/webm
        //video/ogg

        //二进制类型application:
        //application/octet-stream 没有特定子类型就是它(重要)
        //application/x-www-form-urlencoded 传递参数时使用键值对形式(重要)
        //application/pkcs12
        //application/xhtml+xml
        //application/xml
        //application/pdf
        //application/vnd.mspowerpoint

        //复合内容multipart:
        //multipart/form-data  复合内容,有多种内容组合(重要)
        //multipart/byteranges  特殊的复合文件


        //关于ContentType更多内容可以前往
        //https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Content-Type
        //关于媒体类型可以前往
        //https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Basics_of_HTTP/MIME_types
        #endregion

        #region 知识点四 ContentType中对于我们来说重要的类型
        //1.通用2进制类型
        //application/octet-stream
        //2.通用文本类型
        //text/plain 
        //3.键值对参数
        //application/x-www-form-urlencoded
        //4.复合类型(传递的信息有多种类型组成,比如有键值对参数,有文件信息等等,上传资源服务器时需要用该类型)
        //multipart/form-data
        #endregion

        #region 总结
        //这节课的重点知识点是
        //1.Get和Post的区别
        //2.ContentType的重要类型

        //注意:
        //HTTP通讯中
        //客户端发送给服务端的Get和Post请求都需要服务端和客户端约定一些规则进行处理
        //比如传递的参数的含义,数据如何处理等等,都是需要前后端程序制定对应规则来进行处理的
        //只是我们目前没有后端开发的HTTP服务器,所以我们传递过去的参数和数据没有得到对应处理
        //我们目前只针对HTTP资源服务器上传下载数据进行学习
        //他们的通讯原理是一致的,都是通过HTTP通讯交换数据
        #endregion
    }

    // Update is called once per frame
    void Update()
    {
        
    }
}
HTTP上传数据
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Text;
using UnityEngine;

public class PostTest: MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        #region 知识点一 上传文件到HTTP资源服务器需要遵守的规则
        //上传文件时内容的必备规则
        //  1:ContentType = "multipart/form-data; boundary=边界字符串";

        //  2:上传的数据必须按照格式写入流中
        //  --边界字符串
        //  Content-Disposition: form-data; name="字段名字,之后写入的文件2进制数据和该字段名对应";filename="传到服务器上使用的文件名"
        //  Content-Type:application/octet-stream(由于我们传2进制文件 所以这里使用2进制)
        //  空一行
        //  (这里直接写入传入的内容)
        //  --边界字符串--

        //  3:保证服务器允许上传
        //  4:写入流前需要先设置ContentLength内容长度
        #endregion

        #region 知识点二 上传文件
        //1.创建HttpWebRequest对象
        HttpWebRequest req = HttpWebRequest.Create("http://192.168.50.109:8000/Http_Server/") as HttpWebRequest;
        //2.相关设置(请求类型,内容类型,超时,身份验证等)
        req.Method = WebRequestMethods.Http.Post;
        req.ContentType = "multipart/form-data;boundary=Danny";
        req.Timeout = 500000;
        req.Credentials = new NetworkCredential("Danny3", "123123");
        req.PreAuthenticate = true;//先验证身份 再上传数据


        //3.按格式拼接字符串并且转为字节数组之后用于上传
        //3-1.文件数据前的头部信息
        //  --边界字符串
        //  Content-Disposition: form-data; name="字段名字,之后写入的文件2进制数据和该字段名对应";filename="传到服务器上使用的文件名"
        //  Content-Type:application/octet-stream(由于我们传2进制文件 所以这里使用2进制)
        //  空一行
        string head = "--Danny\r\n" +
            "Content-Disposition:form-data;name=\"file\";filename=\"http上传的文件.jpg\"\r\n" +
            "Content-Type:application/octet-stream\r\n\r\n";
        //头部拼接字符串规则信息的字节数组
        byte[] headBytes = Encoding.UTF8.GetBytes(head);

        //3-2.结束的边界信息
        //  --边界字符串--
        byte[] endBytes = Encoding.UTF8.GetBytes("\r\n--Danny--\r\n");

        //4.写入上传流
        using (FileStream localFileStream = File.OpenRead(Application.streamingAssetsPath + "/test.png"))
        {
            //4-1.设置上传长度
            //总长度 是前部分字符串 + 文件本身有多大 + 后部分边界字符串
            req.ContentLength = headBytes.Length + localFileStream.Length + endBytes.Length;
            //用于上传的流
            Stream upLoadStream = req.GetRequestStream();
            //4-2.先写入前部分头部信息
            upLoadStream.Write(headBytes, 0, headBytes.Length);
            //4-3.再写入文件数据
            byte[] bytes = new byte[2048];
            int contentLength = localFileStream.Read(bytes, 0, bytes.Length);
            while (contentLength != 0)
            {
                upLoadStream.Write(bytes, 0, contentLength);
                contentLength = localFileStream.Read(bytes, 0, bytes.Length);
            }
            //4-4.在写入结束的边界信息
            upLoadStream.Write(endBytes, 0, endBytes.Length);

            upLoadStream.Close();
            localFileStream.Close();
        }

        //5.上传数据,获取响应
        HttpWebResponse res = req.GetResponse() as HttpWebResponse;
        if (res.StatusCode == HttpStatusCode.OK)
            print("上传通信成功");
        else
            print("上传失败" + res.StatusCode);
        #endregion

        #region 总结
        //HTTP上传文件相对比较麻烦
        //需要按照指定的规则进行内容拼接达到上传文件的目的
        //其中相对重要的知识点是 
        //上传文件时的规则
        //  --边界字符串
        //  Content-Disposition: form-data; name="file";filename="传到服务器上使用的文件名"
        //  Content-Type:application/octet-stream(由于我们传2进制文件 所以这里使用2进制)
        //  空行
        //  (这里直接写入传入的内容)
        //  --边界字符串--

        //关于其更多的规则,可以前往官网查看详细说明
        //关于ContentType更多内容可以前往
        //https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Content-Type
        //关于媒体类型可以前往
        //https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Basics_of_HTTP/MIME_types
        //关于Content-Disposition更多内容可以前往
        //https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Content-Disposition
        #endregion
    }

    // Update is called once per frame
    void Update()
    {
        
    }
}
封装HTTP的GET和POST
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Text;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.Events;

public class HttpMgr
{
    private static HttpMgr instance = new HttpMgr();

    public static HttpMgr Instance => instance;


    private string HTTP_PATH = "http://192.168.50.109:8000/Http_Server/";

    private string USER_NAME = "Danny3";
    private string PASS_WORD = "123123";

    /// <summary>
    /// 下载指定文件到本地指定路径中
    /// </summary>
    /// <param name="fileName">远程文件名</param>
    /// <param name="loacFilePath">本地路径</param>
    /// <param name="action">下载结束后的回调函数</param>
    public async void DownLoadFile(string fileName, string loacFilePath, UnityAction<HttpStatusCode> action)
    {
        HttpStatusCode result = HttpStatusCode.OK;
        await Task.Run(() =>
        {
            try
            {
                //判断文件是否存在 Head 
                //1.创建HTTP连接对象
                HttpWebRequest req = HttpWebRequest.Create(HTTP_PATH + fileName) as HttpWebRequest;
                //2.设置请求类型 和 其它相关参数
                req.Method = WebRequestMethods.Http.Head;
                req.Timeout = 2000;
                //3.发送请求
                HttpWebResponse res = req.GetResponse() as HttpWebResponse;

                //存在才下载
                if(res.StatusCode == HttpStatusCode.OK)
                {
                    res.Close();
                    //下载
                    //1.创建HTTP连接对象
                    req = HttpWebRequest.Create(HTTP_PATH + fileName) as HttpWebRequest;
                    //2.设置请求类型 和 其它相关参数
                    req.Method = WebRequestMethods.Http.Get;
                    req.Timeout = 2000;
                    //3.发送请求
                    res = req.GetResponse() as HttpWebResponse;
                    //4.存储数据到本地
                    if(res.StatusCode == HttpStatusCode.OK)
                    {
                        //存储数据
                        using (FileStream fileStream = File.Create(loacFilePath))
                        {
                            Stream stream = res.GetResponseStream();
                            byte[] bytes = new byte[4096];
                            int contentLength = stream.Read(bytes, 0, bytes.Length);

                            while (contentLength != 0)
                            {
                                fileStream.Write(bytes, 0, contentLength);
                                contentLength = stream.Read(bytes, 0, bytes.Length);
                            }

                            fileStream.Close();
                            stream.Close();
                        }
                        result = HttpStatusCode.OK;
                    }
                    else
                    {
                        result = res.StatusCode;
                    }
                }
                else
                {
                    result = res.StatusCode;
                }

                res.Close();
            }
            catch (WebException w)
            {
                result = HttpStatusCode.InternalServerError;
                Debug.Log("下载出错" + w.Message + w.Status);
            }
        });

        action?.Invoke(result);
    }


    /// <summary>
    /// 上传文件
    /// </summary>
    /// <param name="fileName">传到远端服务器上的文件名</param>
    /// <param name="loacalFilePath">本地的文件路径</param>
    /// <param name="action">上传结束后的回调函数</param>
    public async void UpLoadFile(string fileName, string loacalFilePath, UnityAction<HttpStatusCode> action)
    {
        HttpStatusCode result = HttpStatusCode.BadRequest;
        await Task.Run(() =>
        {
            try
            {
                HttpWebRequest req = HttpWebRequest.Create(HTTP_PATH) as HttpWebRequest;
                req.Method = WebRequestMethods.Http.Post;
                req.ContentType = "multipart/form-data;boundary=Danny";
                req.Timeout = 500000;
                req.Credentials = new NetworkCredential(USER_NAME, PASS_WORD);
                req.PreAuthenticate = true;

                //拼接字符串 头部
                string head = "--Danny\r\n" +
                "Content-Disposition:form-data;name=\"file\";filename=\"{0}\"\r\n" +
                "Content-Type:application/octet-stream\r\n\r\n";
                //替换文件名
                head = string.Format(head, fileName);
                byte[] headBytes = Encoding.UTF8.GetBytes(head);

                //尾部的边界字符串
                byte[] endBytes = Encoding.UTF8.GetBytes("\r\n--Danny--\r\n");

                using (FileStream localStream = File.OpenRead(loacalFilePath))
                {
                    //设置长度
                    req.ContentLength = headBytes.Length + localStream.Length + endBytes.Length;
                    //写入流
                    Stream upLoadStream = req.GetRequestStream();
                    //写入头部
                    upLoadStream.Write(headBytes, 0, headBytes.Length);
                    //写入上传文件
                    byte[] bytes = new byte[4096];
                    int contentLenght = localStream.Read(bytes, 0, bytes.Length);
                    while (contentLenght != 0)
                    {
                        upLoadStream.Write(bytes, 0, contentLenght);
                        contentLenght = localStream.Read(bytes, 0, bytes.Length);
                    }
                    //写入尾部
                    upLoadStream.Write(endBytes, 0, endBytes.Length);

                    upLoadStream.Close();
                    loacalFilePath.Clone();
                }

                HttpWebResponse res = req.GetResponse() as HttpWebResponse;
                //让外部去处理结果 
                result = res.StatusCode;
                res.Close();
            }
            catch (WebException w)
            {
                Debug.Log("上传出错" + w.Status + w.Message);
            }
        });
        action?.Invoke(result);
    }
}

Unity相关类   

        WWW类
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class Lesson28 : MonoBehaviour
{
    public RawImage image;

    // Start is called before the first frame update
    void Start()
    {
        #region 知识点一 WWW类的作用
        //WWW是Unity提供给我们简单的访问网页的类
        //我们可以通过该类下载和上传一些数据
        //在使用http协议时,默认的请求类型是Get,如果想要Post上传,需要配合下节课学习的WWWFrom类使用
        //它主要支持的协议
        //1.http://和https:// 超文本传输协议
        //2.ftp:// 文件传输协议(但仅限于匿名下载)
        //3.file:// 本地文件传输协议,可以使用该协议异步加载本地文件(PC、IOS、Android都支持)

        //注意:
        //1.该类一般配合协同程序使用
        //2.该类在较新Unity版本中会提示过时,但是仍可以使用,新版本将其功能整合进了UnityWebRequest类(之后讲解)
        #endregion

        #region 知识点二 WWW类的常用方法和变量
        #region 常用方法
        //1.WWW:构造函数,用于创建一个WWW请求
        WWW www = new WWW("http://192.168.50.109:8000/Http_Server/test.jpg");
        //2.GetAudioClip:从下载数据返回一个音效切片AudioClip对象
        //www.GetAudioClip()
        //3.LoadImageIntoTexture:用下载数据中的图像来替换现有的一个Texture2D对象
        //Texture2D tex = new Texture2D(100, 100);
        //www.LoadImageIntoTexture(tex);
        //4.LoadFromCacheOrDownload:从缓存加载AB包对象,如果该包不在缓存则自动下载存储到缓存中,以便以后直接从本地缓存中加载
        //WWW.LoadFromCacheOrDownload("http://192.168.50.109:8000/Http_Server/test.assetbundle", 1);
        #endregion

        #region 常用变量
        //1.assetBundle:如果加载的数据是AB包,可以通过该变量直接获取加载结果
        //www.assetBundle
        //2.audioClip:如果加载的数据是音效切片文件,可以通过该变量直接获取加载结果
        //www.GetAudioClip
        //3.bytes:以字节数组的形式获取加载到的内容
        //www.bytes
        //4.bytesDownloaded:过去已下载的字节数
        //www.bytesDownloaded
        //5.error:返回一个错误消息,如果下载期间出现错误,可以通过它获取错误信息
        //www.error != null
        //6.isDone:判断下载是否已经完成
        //www.isDone
        //7.movie:如果下载的视频,可以获取一个MovieTexture类型结果
        //www.GetMovieTexture()
        //8.progress:下载进度
        //www.progress
        //9.text:如果下载的数据是字符串,以字符串的形式返回内容
        //www.text
        //10.texture:如果下载的数据是图片,以Texture2D的形式返回加载结果
        //www.texture
        #endregion
        #endregion

        #region 知识点三 利用WWW类来异步下载或加载文件
        #region 1.下载HTTP服务器上的内容
        //StartCoroutine(DownLoadHttp());
        #endregion

        #region 2.下载FTP服务器上的内容(FTP服务器一定要支持匿名账户)
        //StartCoroutine(DownLoadFtp());
        #endregion

        #region 3.本地内容加载(一般移动平台加载数据都会使用该方式)
        StartCoroutine(DownLoadLocal());
        #endregion
        #endregion

        #region 总结
        //Unity中的WWW类比使用C#中的Http、FTP相关类更加的方便
        //建议大家使用Unity当中为我们封装好的类来处理下载、加载相关逻辑
        #endregion
    }

    IEnumerator DownLoadHttp()
    {
        //1.创建WWW对象
        WWW www = new WWW("https://i-blog.csdnimg.cn/direct/0002f5896650439db63a5a3be9064f2b.png");

        //2.就是等待加载结束
        while (!www.isDone)
        {
            print(www.bytesDownloaded);
            print(www.progress);
            yield return null;
        }

        print(www.bytesDownloaded);
        print(www.progress);

        //3.使用加载结束后的资源
        if (www.error == null)
        {
            image.texture = www.texture;
        }
        else
            print(www.error);
    }

    IEnumerator DownLoadFtp()
    {
        //1.创建WWW对象
        WWW www = new WWW("ftp://127.0.0.1/text.jpg");

        //2.就是等待加载结束
        while (!www.isDone)
        {
            print(www.bytesDownloaded);
            print(www.progress);
            yield return null;
        }

        print(www.bytesDownloaded);
        print(www.progress);

        //3.使用加载结束后的资源
        if (www.error == null)
        {
            image.texture = www.texture;
        }
        else
            print(www.error);
    }

    IEnumerator DownLoadLocal()
    {
        //1.创建WWW对象
        WWW www = new WWW("file://" + Application.streamingAssetsPath + "/test.png");

        //2.就是等待加载结束
        while (!www.isDone)
        {
            print(www.bytesDownloaded);
            print(www.progress);
            yield return null;
        }

        print(www.bytesDownloaded);
        print(www.progress);

        //3.使用加载结束后的资源
        if (www.error == null)
        {
            image.texture = www.texture;
        }
        else
            print(www.error);
    }

    // Update is called once per frame
    void Update()
    {
        
    }
}
WWWFrom类
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Text;
using UnityEngine;

public class WWWFromText : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        #region 知识点一 WWWFrom类的作用
        //如果想要使用WWW上传数据时,就需要配合WWWFrom类进行使用了
        //而WWWFrom主要就是用于集成数据的,我们可以设置上传的参数或者2进制数据
        //当结合WWWFrom上传数据时
        //它主要用到的请求类型是Post
        //它使用Http协议进行上传处理

        //注意:
        //使用WWW结合WWWFrom上传数据一般需要配合后端程序制定上传规则
        #endregion

        #region 知识点二 WWWFrom类的常用方法和变量
        //该类当中我们主要就使用方法,相关变量很少使用,我们主要就着重讲解方法
        //1.WWWForm:构造函数
        WWWForm data = new WWWForm();
        //2.AddBinaryData:添加二进制数据
        //data.AddBinaryData()
        //3.AddField:添加字段
        //data.AddField()
        #endregion

        #region 知识点三 WWW结合WWWFrom对象来异步上传数据
        StartCoroutine(UpLoadData());
        #endregion

        #region 总结
        //WWW结合WWWFrom上传数据
        //需要配合后端服务器来指定上传规则
        //也就是说我们上传的数据,后端需要知道收到数据后应该如何处理
        //通过这种方式我们没办法像C#类当中完成文件的上传
        //但是该方式非常适合用于制作短连接游戏的前端网络层
        //我们可以对WWW进行二次封装,专门用于上传自定义消息给对应的Web服务器
        #endregion
    }

    IEnumerator UpLoadData()
    {
        WWWForm data = new WWWForm();
        //上传的数据 对应的后端程序 必须要有处理的规则 才能生效
        data.AddField("Name", "MrTang", Encoding.UTF8);
        data.AddField("Age", 99);
        data.AddBinaryData("file", File.ReadAllBytes(Application.streamingAssetsPath + "/test.png"), "testtest.png", "application/octet-stream");

        WWW www = new WWW("http://192.168.50.109:8000/Http_Server/", data);

        yield return www;

        if (www.error == null)
        {
            print("上传成功");
            //www.bytes
        }
        else
            print("上传失败" + www.error);
    }


    // Update is called once per frame
    void Update()
    {
        
    }
}
UnityWebRequest类
        获取GET
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Networking;
using UnityEngine.UI;

public class UnityWebRequest_GET: MonoBehaviour
{
    public RawImage image;
    // Start is called before the first frame update
    void Start()
    {
        #region 知识点一 UnityWebRequest是什么?
        //UnityWebRequest是一个Unity提供的一个模块化的系统类
        //用于构成HTTP请求和处理HTTP响应
        //它主要目标是让Unity游戏和Web服务端进行交互
        //它将之前WWW的相关功能都集成在了其中
        //所以新版本中都建议使用UnityWebRequest类来代替WWW类

        //它在使用上和WWW很类似
        //主要的区别就是UnityWebRequest把下载下来的数据处理单独提取出来了
        //我们可以根据自己的需求选择对应的数据处理对象来获取数据

        //注意:
        //1.UnityWebRequest和WWW一样,需要配合协同程序使用
        //2.UnityWebRequest和WWW一样,支持http、ftp、file协议下载或加载资源
        //3.UnityWebRequest能够上传文件到HTTP资源服务器
        #endregion

        #region 知识点二 UnityWebRequest类的常用操作
        //1.使用Get请求获取文本或二进制数据
        //2.使用Get请求获取纹理数据
        //3.使用Get请求获取AB包数据
        //4.使用Post请求发送数据
        //5.使用Put请求上传数据
        #endregion

        #region 知识点三 Get获取操作
        //1.获取文本或2进制
        StartCoroutine(LoadText());
        //2.获取纹理
        StartCoroutine(LoadTexture());
        //3.获取AB包
        StartCoroutine(LoadAB());
        #endregion

        #region 总结 
        //UnityWebRequest使用上和WWW类很类似
        //我们需要注意的是
        //1.获取文本或二进制数据时
        //  使用UnityWebRequest.Get
        //2.获取纹理图片数据时
        //  使用UnityWebRequestTexture.GetTexture
        //  以及DownloadHandlerTexture.GetContent
        //3.获取AB包数据时
        //  使用UnityWebRequestAssetBundle.GetAssetBundle
        //  以及DownloadHandlerAssetBundle.GetContent
        #endregion
    }

    IEnumerator LoadText()
    {
        UnityWebRequest req = UnityWebRequest.Get("http://192.168.50.109:8000/Http_Server/test.txt");
        //就会等待 服务器端响应后 断开连接后 再继续执行后面的内容
        yield return req.SendWebRequest();

        //如果处理成功 结果就是成功枚举
        if(req.result == UnityWebRequest.Result.Success)
        {
            //文本 字符串
            print(req.downloadHandler.text);
            //字节数组
            byte[] bytes = req.downloadHandler.data;
            print("字节数组长度" + bytes.Length);
        }
        else
        {
            print("获取失败:" + req.result + req.error + req.responseCode);
        }
    }

    IEnumerator LoadTexture()
    {
        //UnityWebRequest req = UnityWebRequestTexture.GetTexture("http://192.168.50.109:8000/Http_Server/test.jpg");

        //UnityWebRequest req = UnityWebRequestTexture.GetTexture("ftp://127.0.0.1/test.jpg");

        UnityWebRequest req = UnityWebRequestTexture.GetTexture("file://" + Application.streamingAssetsPath + "/test.png");

        yield return req.SendWebRequest();

        if (req.result == UnityWebRequest.Result.Success)
        {
            //(req.downloadHandler as DownloadHandlerTexture).texture
            //DownloadHandlerTexture.GetContent(req)
            //image.texture = (req.downloadHandler as DownloadHandlerTexture).texture;
            image.texture = DownloadHandlerTexture.GetContent(req);
        }
        else
            print("获取失败" + req.error + req.result + req.responseCode);
    }

    IEnumerator LoadAB()
    {
        UnityWebRequest req = UnityWebRequestAssetBundle.GetAssetBundle("http://192.168.50.109:8000/Http_Server/lua");

        req.SendWebRequest();

        while (!req.isDone)
        {
            print(req.downloadProgress);
            print(req.downloadedBytes);
            yield return null;
        }
        //yield return req.SendWebRequest();

        print(req.downloadProgress);
        print(req.downloadedBytes);

        if (req.result == UnityWebRequest.Result.Success)
        {
            //AssetBundle ab = (req.downloadHandler as DownloadHandlerAssetBundle).assetBundle;
            AssetBundle ab = DownloadHandlerAssetBundle.GetContent(req);
            print(ab.name);
        }
        else
            print("获取失败" + req.error + req.result + req.responseCode);
    }

    // Update is called once per frame
    void Update()
    {
        
    }
}
        上传POST PUT
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Text;
using UnityEngine;
using UnityEngine.Networking;

public class Lesson31 : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        #region 知识点一 上传相关数据类
        //父接口
        //IMultipartFormSection
        //数据相关类都继承该接口
        //我们可以用父类装子类
        List<IMultipartFormSection> dataList = new List<IMultipartFormSection>();

        //子类数据
        //MultipartFormDataSection
        //1.二进制字节数组
        dataList.Add(new MultipartFormDataSection(Encoding.UTF8.GetBytes("123123123123123")));
        //2.字符串
        dataList.Add(new MultipartFormDataSection("12312312312312312dsfasdf"));
        //3.参数名,参数值(字节数组,字符串),编码类型,资源类型(常用)
        dataList.Add(new MultipartFormDataSection("Name", "Danny", Encoding.UTF8, "application/...."));
        dataList.Add(new MultipartFormDataSection("Msg", new byte[1024], "appl....."));

        //MultipartFormFileSection
        //1.字节数组
        dataList.Add(new MultipartFormFileSection(File.ReadAllBytes(Application.streamingAssetsPath + "/test.png")));
        
        //2.文件名,字节数组(常用)
        dataList.Add(new MultipartFormFileSection("上传的文件.png", File.ReadAllBytes(Application.streamingAssetsPath + "/test.png")));
        //3.字符串数据,文件名(常用)
        dataList.Add(new MultipartFormFileSection("12312313212312", "test.txt"));
        //4.字符串数据,编码格式,文件名(常用)
        dataList.Add(new MultipartFormFileSection("12312313212312", Encoding.UTF8, "test.txt"));
        
        //5.表单名,字节数组,文件名,文件类型
        dataList.Add(new MultipartFormFileSection("file", new byte[1024], "test.txt", ""));
        //6.表单名,字符串数据,编码格式,文件名
        dataList.Add(new MultipartFormFileSection("file", "123123123", Encoding.UTF8, "test.txt"));
        #endregion

        #region 知识点二 Post发送相关
        StartCoroutine(Upload());
        #endregion

        #region 知识点三 Put上传相关
        //注意:Put请求类型不是所有的web服务器都认,必须要服务器处理该请求类型那么才能有相应
        #endregion

        #region 总结
        //我们可以利用Post上传数据或上传文件
        //Put主要用于上传文件,但是必须资源服务器支持Put请求类型
        //为了通用性,我们可以统一使用Post请求类型进行数据和资源的上传
        //它的使用和之前的WWW类似,只要前后端制定好规则就可以相互通信了
        #endregion
    }

    IEnumerator Upload()
    {
        //准备上传的数据 
        List<IMultipartFormSection> data = new List<IMultipartFormSection>();
        //键值对相关的 信息 字段数据
        data.Add(new MultipartFormDataSection("Name", "Danny"));
        //PlayerMsg msg = new PlayerMsg();
        //data.Add(new MultipartFormDataSection("Msg", msg.Writing()));
        //添加一些文件上传文件
        //传2进制文件
        data.Add(new MultipartFormFileSection("TestTest123.png", File.ReadAllBytes(Application.streamingAssetsPath + "/test.png")));
        //传文本文件
        data.Add(new MultipartFormFileSection("123123123123123", "Test123.txt"));

        UnityWebRequest req = UnityWebRequest.Post("http://192.168.50.109:8000/Http_Server/", data);

        req.SendWebRequest();

        while (!req.isDone)
        {
            print(req.uploadProgress);
            print(req.uploadedBytes);
            yield return null;
        }

        print(req.uploadProgress);
        print(req.uploadedBytes);

        if (req.result == UnityWebRequest.Result.Success)
        {
            print("上传成功");
            //req.downloadHandler.data
        }
        else
            print("上传失败" + req.error + req.responseCode + req.result);
    }


    IEnumerator UpLoadPut()
    {
        UnityWebRequest req = UnityWebRequest.Put("http://192.168.50.109:8000/Http_Server/", File.ReadAllBytes(Application.streamingAssetsPath + "/test.png"));

        yield return req.SendWebRequest();

        if (req.result == UnityWebRequest.Result.Success)
        {
            print("Put 上传成功");
        }
        else
        {

        }
    }

    // Update is called once per frame
    void Update()
    {
        
    }
}
UnityWebRequest高级操作
        获取
using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
using UnityEngine.Networking;
using UnityEngine.UI;

public class DownLoad: MonoBehaviour
{
    public RawImage image;
    // Start is called before the first frame update
    void Start()
    {
        #region 知识点一 高级操作指什么?
        //在常用操作中我们使用的是Unity为我们封装好的一些方法
        //我们可以方便的进行一些指定类型的数据获取

        //比如
        //下载数据时:
        //1.文本和2进制
        //2.图片
        //3.AB包
        //如果我们想要获取其它类型的数据应该如何处理呢?

        //上传数据时:
        //1.可以指定参数和值
        //2.可以上传文件
        //如果想要上传一些基于HTTP规则的其它数据应该如何处理呢?

        //高级操作就是用来处理 常用操作不能完成的需求的
        //它的核心思想就是:UnityWebRequest中可以将数据处理分离开
        //比如常规操作中我们用到的
        //DownloadHandlerTexture 和 DownloadHandlerAssetBundle两个类
        //就是用来将2进制字节数组转换成对应类型进行处理的

        //所以高级操作时指 让你按照规则来实现更多的数据获取、上传等功能
        #endregion

        #region 知识点二 UnityWebRequest类的更多内容
        //目前已学的内容
        //UnityWebRequest req = UnityWebRequest.Get("");
        //UnityWebRequest req = UnityWebRequestTexture.GetTexture("");
        //UnityWebRequest req = UnityWebRequestAssetBundle.GetAssetBundle("");
        //UnityWebRequest req = UnityWebRequest.Put()
        //UnityWebRequest req = UnityWebRequest.Post

        //req.isDone
        //req.downloadProgress;
        //req.downloadedBytes;
        //req.uploadProgress;
        //req.uploadedBytes

        //req.SendWebRequest()

        //更多内容
        //1.构造函数
        //UnityWebRequest req = new UnityWebRequest();

        //2.请求地址
        //req.url = "服务器地址";

        //3.请求类型
        //req.method = UnityWebRequest.kHttpVerbPOST;

        //4.进度
        //req.downloadProgress
        //req.uploadProgress

        //5.超时设置
        //req.timeout = 2000;

        //6.上传、下载的字节数
        //req.downloadedBytes
        //req.uploadedBytes

        //7.重定向次数 设置为0表示不进行重定向 可以设置次数
        //req.redirectLimit = 10;

        //8.状态码、结果、错误内容
        //req.result
        //req.error
        //req.responseCode

        //9.下载、上传处理对象
        //req.downloadHandler
        //req.uploadHandler

        //更多内容
        //https://docs.unity.cn/cn/2020.3/ScriptReference/Networking.UnityWebRequest.html
        #endregion

        #region 知识点三 自定义获取数据DownloadHandler相关类
        //关键类:
        //1.DownloadHandlerBuffer 用于简单的数据存储,得到对应的2进制数据。
        //2.DownloadHandlerFile 用于下载文件并将文件保存到磁盘(内存占用少)。
        //3.DownloadHandlerTexture 用于下载图像。
        //4.DownloadHandlerAssetBundle 用于提取 AssetBundle。
        //5.DownloadHandlerAudioClip 用于下载音频文件。

        StartCoroutine(DownLoadTex());

        StartCoroutine(DownLoadAB());

        //以上的这些类,其实就是Unity帮助我们实现好的,用于解析下载下来的数据的类
        //使用对应的类处理下载数据,他们就会在内部将下载的数据处理为对应的类型,方便我们使用

        //DownloadHandlerScript 是一个特殊类。就其本身而言,不会执行任何操作。
        //但是,此类可由用户定义的类继承。此类接收来自 UnityWebRequest 系统的回调,
        //然后可以使用这些回调在数据从网络到达时执行完全自定义的数据处理。

        StartCoroutine(DownLoadCustomHandler());
        #endregion

        #region 总结
        //我们可以自己设置UnityWebRequest当中的下载处理对象
        //当设置后,下载数据后它会使用该对象中对应的函数处理数据
        //让我们更方便的获取我们想要的数据
        //方便我们对数据下载或获取进行拓展
        #endregion
    }

    IEnumerator DownLoadTex()
    {
        UnityWebRequest req = new UnityWebRequest("http://192.168.50.109:8000/Http_Server/text.jpg", 
                                                   UnityWebRequest.kHttpVerbGET);
        //req.method = UnityWebRequest.kHttpVerbGET;
        //1.DownloadHandlerBuffer
        //DownloadHandlerBuffer bufferHandler = new DownloadHandlerBuffer();
        //req.downloadHandler = bufferHandler;

        //2.DownloadHandlerFile
        //print(Application.persistentDataPath);
        //req.downloadHandler = new DownloadHandlerFile(Application.persistentDataPath + "/downloadFile.jpg");

        //3.DownloadHandlerTexture
        DownloadHandlerTexture textureHandler = new DownloadHandlerTexture();
        req.downloadHandler = textureHandler;

        yield return req.SendWebRequest();

        if(req.result == UnityWebRequest.Result.Success)
        {
            //获取字节数组
            //bufferHandler.data

            //textureHandler.texture
            image.texture = textureHandler.texture;
        }
        else
        {
            print("获取数据失败" + req.result + req.error + req.responseCode);
        }
    }

    IEnumerator DownLoadAB()
    {
        UnityWebRequest req = new UnityWebRequest("http://192.168.50.109:8000/Http_Server/lua", UnityWebRequest.kHttpVerbGET);
        //第二个参数 需要已知校检码 才能进行比较 检查完整性 如果不知道的话 只能传0 不进行完整性的检查
        //所以一般 只有进行AB包热更新时 服务器发送了 对应的 文件列表中 包含了 验证码 才能进行检查
        DownloadHandlerAssetBundle handler = new DownloadHandlerAssetBundle(req.url, 0);
        req.downloadHandler = handler;

        yield return req.SendWebRequest();

        if (req.result == UnityWebRequest.Result.Success)
        {
            AssetBundle ab = handler.assetBundle;
            
            print(ab.name);
        }
        else
        {
            print("获取数据失败" + req.result + req.error + req.responseCode);
        }
    }

    IEnumerator DownLoadAudioClip()
    {
        UnityWebRequest req = UnityWebRequestMultimedia.GetAudioClip("http://192.168.50.109:8000/Http_Server/音效名.mp3", 
                                                                      AudioType.MPEG);
        yield return req.SendWebRequest();
        
        if (req.result == UnityWebRequest.Result.Success)
        {
            AudioClip a = DownloadHandlerAudioClip.GetContent(req);
        }
        else
        {
            print("获取数据失败" + req.result + req.error + req.responseCode);
        }
    }

    IEnumerator DownLoadCustomHandler()
    {
        UnityWebRequest req = new UnityWebRequest("http://192.168.50.109:8000/Http_Server/21.服务端.mp4", UnityWebRequest.kHttpVerbGET);

        //使用自定义的下载处理对象 来处理获取到的 2进制字节数组
        print(Application.persistentDataPath);
        req.downloadHandler = new CustomDownLoadFileHandler(Application.persistentDataPath + "/CustomHandler.mp4");

        yield return req.SendWebRequest();

        if(req.result == UnityWebRequest.Result.Success)
        {
            print("存储本地成功");
        }
        else
        {
            print("获取数据失败" + req.result + req.error + req.responseCode);
        }
    }

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

public class CustomDownLoadFileHandler:DownloadHandlerScript
{
    //用于保存 本地存储时的路径
    private string savePath;

    //用于缓存收到的数据的容器
    private byte[] cacheBytes;
    //这是当前已收到的数据长度
    private int index = 0;

    public CustomDownLoadFileHandler():base()
    {

    }

    public CustomDownLoadFileHandler(byte[] bytes) :base(bytes)
    {

    }

    public CustomDownLoadFileHandler(string path) : base()
    {
        savePath = path;
    }

    protected override byte[] GetData()
    {
        //返回字节数组
        return cacheBytes;
    }

    /// <summary>
    /// 从网络收到数据后 每帧会调用的方法  会自动调用的方法
    /// </summary>
    /// <param name="data"></param>
    /// <param name="dataLength"></param>
    /// <returns></returns>
    protected override bool ReceiveData(byte[] data, int dataLength)
    {
        Debug.Log("收到数据长度:" + data.Length);
        Debug.Log("收到数据长度dataLength:" + dataLength);
        data.CopyTo(cacheBytes, index);
        index += dataLength;
        return true;
    }

    /// <summary>
    /// 从服务器收到 COntent-Length标头时  会自动调用的方法
    /// </summary>
    /// <param name="contentLength"></param>
    protected override void ReceiveContentLengthHeader(ulong contentLength)
    {
        //base.ReceiveContentLengthHeader(contentLength);
        Debug.Log("收到数据长度:" + contentLength);
        //根据收到的标头 决定字节数组容器的大小
        cacheBytes = new byte[contentLength];
    }

    /// <summary>
    /// 当消息收完了 会自动调用的方法
    /// </summary>
    protected override void CompleteContent()
    {
        Debug.Log("消息收完");
        //把收到的字节数组 进行自定义处理 我们在这 处理成 存储到本地
        File.WriteAllBytes(savePath, cacheBytes);
    }

}
        上传
using System.Collections;
using System.Collections.Generic;
using System.Text;
using UnityEngine;
using UnityEngine.Networking;

public class Lesson33 : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        #region 知识点一 回顾高级操作中的获取数据
        //主要做法:将2进制字节数组处理,独立到下载处理对象中进行处理
        //         主要就是设置UnityWebRequest对象中 downloadHandler 变量
        //Unity写好的类有
        //1.DownloadHandlerBuffer 用于简单的数据存储,得到对应的2进制数据。
        //2.DownloadHandlerFile 用于下载文件并将文件保存到磁盘(内存占用少)。
        //3.DownloadHandlerTexture 用于下载图像。
        //4.DownloadHandlerAssetBundle 用于提取 AssetBundle。
        //5.DownloadHandlerAudioClip 用于下载音频文件。

        //自己拓展处理方式
        //继承 DownloadHandlerScript
        //并重写其中的固定方法,自己处理字节数组
        #endregion

        #region 知识点二 自定义上传数据UploadHandler相关类
        //注意:
        //由于UnityWebRequest类的常用操作中
        //上传数据相关内容已经封装的很好了
        //我们可以很方便的上传参数和文件
        //我们使用常用操作已经能够满足常用需求了
        //所以以下内容主要做了解

        //UploadHandler相关类
        //1.UploadHandlerRaw  用于上传字节数组
        StartCoroutine(UpLoad());
        //2.UploadHandlerFile 用于上传文件

        //其中比较重要的变量是
        //contentType 内容类型,如果不设置,模式是 application/octet-stream 2进制流的形式
        #endregion

        #region 总结
        //由于上传数据相关 UnityWebRequest原本已经提供了较为完善的
        //参数上传、文件上传相关功能
        //所以高级操作中的 上传数据相关内容拓展较少,使用也较少
        //我们使用常用操作的上传数据相关功能就足够了
        //高级操作的上传数据知识点主要做了解
        #endregion
    }

    IEnumerator UpLoad()
    {
        UnityWebRequest req = new UnityWebRequest("http://192.168.50.109:8000/Http_Server/", UnityWebRequest.kHttpVerbPOST);

        //1.UploadHandlerRaw  用于上传字节数组
        //byte[] bytes = Encoding.UTF8.GetBytes("123123123123123");
        //req.uploadHandler = new UploadHandlerRaw(bytes);
        //req.uploadHandler.contentType = "类型/细分类型";

        //2.UploadHandlerFile 用于上传文件
        req.uploadHandler = new UploadHandlerFile(Application.streamingAssetsPath + "/test.png");

        yield return req.SendWebRequest();

        print(req.result);
    }

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

封装Unity相关类

using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.Networking;

public class NetWWWMgr : MonoBehaviour
{
    private static NetWWWMgr instance;

    public static NetWWWMgr Instance => instance;

    private string HTTP_SERVER_PATH = "http://192.168.50.109:8000/Http_Server/";

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

    /// <summary>
    /// 提供给外部加载资源用的方法
    /// </summary>
    /// <typeparam name="T">资源的类型</typeparam>
    /// <param name="path">资源的路径 http ftp file都支持</param>
    /// <param name="action">加载结束后的回调函数 因为WWW是通过结合协同程序异步加载的 所以不能马上获取结果 需要回调获取</param>
    public void LoadRes<T>(string path, UnityAction<T> action) where T : class
    {
        StartCoroutine(LoadResAsync<T>(path, action));
    }

    private IEnumerator LoadResAsync<T>(string path, UnityAction<T> action) where T : class
    {
        //声明www对象 用于下载或加载
        WWW www = new WWW(path);
        //等待下载或者加载结束(异步)
        yield return www;
        //如果没有错误 证明加载成功
        if (www.error == null)
        {
            //根据T泛型的类型  决定使用哪种类型的资源 传递给外部
            if(typeof(T) == typeof(AssetBundle))
            {
                action?.Invoke(www.assetBundle as T);
            }
            else if (typeof(T) == typeof(Texture))
            {
                action?.Invoke(www.texture as T);
            }
            else if (typeof(T) == typeof(AudioClip))
            {
                action?.Invoke(www.GetAudioClip() as T);
            }
            else if (typeof(T) == typeof(string))
            {
                action?.Invoke(www.text as T);
            }
            else if (typeof(T) == typeof(byte[]))
            {
                action?.Invoke(www.bytes as T);
            }
            //自定义一些类型 可能需要将bytes 转换成对应的类型来使用
        }
        //如果错误 就提示别人
        else
        {
            Debug.LogError("www加载资源出错" + www.error);
        }
    }

    /// <summary>
    /// 通过UnityWebRequest去获取数据
    /// </summary>
    /// <typeparam name="T">byte[]、Texture、AssetBundle、AudioClip、object(自定义的 如果是object证明要保存到本地)</typeparam>
    /// <param name="path">远端或者本地数据路径 http ftp file</param>
    /// <param name="action">获取成功后的回调函数</param>
    /// <param name="localPath">如果是下载到本地 需要传第3个参数</param>
    /// <param name="type">如果是下载 音效切片文件 需要穿音效类型</param>
    public void UnityWebRequestLoad<T>(string path, UnityAction<T> action, string localPath = "", AudioType type = AudioType.MPEG) where T : class
    {
        StartCoroutine(UnityWebRequestLoadAsync<T>(path, action, localPath, type));
    }

    private IEnumerator UnityWebRequestLoadAsync<T>(string path, UnityAction<T> action, string localPath = "", AudioType type = AudioType.MPEG) where T:class
    {
        UnityWebRequest req = new UnityWebRequest(path, UnityWebRequest.kHttpVerbGET);

        if (typeof(T) == typeof(byte[]))
            req.downloadHandler = new DownloadHandlerBuffer();
        else if (typeof(T) == typeof(Texture))
            req.downloadHandler = new DownloadHandlerTexture();
        else if (typeof(T) == typeof(AssetBundle))
            req.downloadHandler = new DownloadHandlerAssetBundle(req.url, 0);
        else if (typeof(T) == typeof(object))
            req.downloadHandler = new DownloadHandlerFile(localPath);
        else if (typeof(T) == typeof(AudioClip))
            req = UnityWebRequestMultimedia.GetAudioClip(path, type);
        else//如果出现没有的类型  就不用继续往下执行了
        {
            Debug.LogWarning("未知类型" + typeof(T));
            yield break;
        }

        yield return req.SendWebRequest();

        if(req.result == UnityWebRequest.Result.Success)
        {
            if (typeof(T) == typeof(byte[]))
                action?.Invoke(req.downloadHandler.data as T);
            else if (typeof(T) == typeof(Texture))
                //action?.Invoke((req.downloadHandler as DownloadHandlerTexture).texture as T);
                action?.Invoke(DownloadHandlerTexture.GetContent(req) as T);
            else if (typeof(T) == typeof(AssetBundle))
                action?.Invoke((req.downloadHandler as DownloadHandlerAssetBundle).assetBundle as T);
            else if (typeof(T) == typeof(object))
                action?.Invoke(null);
            else if (typeof(T) == typeof(AudioClip))
                action?.Invoke(DownloadHandlerAudioClip.GetContent(req) as T);
        }
        else
        {
            Debug.LogWarning("获取数据失败" + req.result + req.error + req.responseCode);
        }
    }


    /// <summary>
    /// 发送继承BaseMsg的消息给web服务器
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="msg"></param>
    /// <param name="action"></param>
    public void SendMsg<T>(BaseMsg msg, UnityAction<T> action) where T:BaseMsg
    {
        StartCoroutine(SendMsgAsync<T>(msg, action));
    }

    private IEnumerator SendMsgAsync<T>(BaseMsg msg, UnityAction<T> action) where T : BaseMsg
    {
        //消息发送
        WWWForm data = new WWWForm();
        //准备要发送的消息数据
        data.AddBinaryData("Msg", msg.Writing());

        WWW www = new WWW(HTTP_SERVER_PATH, data);
        //我们也可以直接传递 2进制字节数组 只要和后端定好规则 怎么传都是可以的
        //WWW www = new WWW("HTTP_SERVER_PATH", msg.Writing());

        //异步等待 发送结束 才会继续执行后面的代码
        yield return www;

        //发送完毕过后 收到响应 
        //认为 后端发回来的内容 也是一个继承自BaseMsg类的一个字节数组对象
        if (www.error == null)
        {
            //先解析 ID和消息长度
            int index = 0;
            int msgID = BitConverter.ToInt32(www.bytes, index);
            index += 4;
            int msgLength = BitConverter.ToInt32(www.bytes, index);
            index += 4;
            //反序列化 BaseMsg
            BaseMsg baseMsg = null;
            switch (msgID)
            {
                case 1001:
                    baseMsg = new PlayerMsg();
                    baseMsg.Reading(www.bytes, index);
                    break;
            }
            if (baseMsg != null)
                action?.Invoke(baseMsg as T);
        }
        else
            Debug.LogError("发消息出问题" + www.error);
    }

    /// <summary>
    /// 上传文件的方法
    /// </summary>
    /// <param name="fileName">上传上去的文件名</param>
    /// <param name="localPath">本地想要上传文件的路径</param>
    /// <param name="action">上传完成后的回调函数</param>
    public void UploadFile(string fileName, string localPath, UnityAction<UnityWebRequest.Result> action)
    {
        StartCoroutine(UploadFileAsync(fileName, localPath, action));
    }

    private IEnumerator UploadFileAsync(string fileName, string localPath, UnityAction<UnityWebRequest.Result> action)
    {
        //添加要上传文件的数据
        List<IMultipartFormSection> dataList = new List<IMultipartFormSection>();
        dataList.Add(new MultipartFormFileSection(fileName, File.ReadAllBytes(localPath)));

        UnityWebRequest req = UnityWebRequest.Post(HTTP_SERVER_PATH, dataList);

        yield return req.SendWebRequest();

        action?.Invoke(req.result);
        //如果不成功
        if (req.result != UnityWebRequest.Result.Success)
        {
            Debug.LogWarning("上传出现问题" + req.error + req.responseCode);
        }
    }

}

网络协议封装

        点击下载协议框架

结尾

以上是本人在项目中及平时积累及参考一些文章的知识归纳总结!!!

如果喜欢我的文章,欢迎关注、点赞、转发、评论,大家的支持是我最大的动力

如有疑问或BUG,请加入QQ群一起交流探讨!!!

技术交流QQ群:1011399921!!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值