Unity8 - 网络基础

Unity8 - 网络基础

目录

正文

基础理论

网络基本概念

  • 网络
  • 局域网LAN(Local Area Network): 一个小区域内的多台设备相互连接形成的计算机组
  • 以太网: 一种计算机局域网技术, 目前应用最普遍的局域网技术, 该技术规定了网络连接的一些规则/协议
    • 以太网网络拓扑结构: 设备相互连接起来的物理布局构成的几何形状
      • 树形,网状,总线型,环形,星型
  • 城域网MAN(Metropolitan Area Network): 城市范围的网络
  • 广域网WAN(Wide Area Network): 超长距离专线连接的网络, 网状结构
  • 互联网(因特网)
  • 万维网: 存储在连接到因特网的计算机上的网页的集合

IP, 端口, Mac地址

作用: 互联设备中的设备地址

  • IP地址(Internet Protocol Address)/网际协议地址: 按协议规定的设备在网络中的具体地址, 用于定位
    • 按协议分
      • IPv4:从0.0.0.0255.255.255.255
      • IPv6:从0:0:0:0:0:0:0:065535:65535:65535:65535:65535:65535:65535:65535
    • 按使用范围分
      • 公网IP: 想要连接外网, 和远程设备通信时使用的IP
      • 私网IP/局域网IP: 只能在局域网内通信
  • 端口: 区分一个设备上的不同应用程序, 从065535, 需要自定义不能与其他程序相同的端口号, 一般1024以上
  • Mac地址Media Access Control Address: 在网络中标识一个网卡的地址, 通常为12个16进制数, 由网卡制造商写入

数据通信模型

  • 数据通信模型
    • 分散式Decentralized: 每一个计算机之间没有信息共享
    • 集中式Centralized: 一个中心计算机保存所有数据, 其他计算机访问中心计算机获得数据
    • 分布式Distributed: 两者的混合
  • C/S模型: 客户端Client和服务端Server模式
  • B/S模型: 基于Web的通信模型(Browse/Server), 使用HTTP传送信息, 是一种特殊的C/S模型, 特殊之处在于客户端是浏览器, 不用自己开发
  • P2P模型: 对等互联(Peer-to-Peer), 每一个设备同时运行ClientServer部分, 游戏一般不用

网络协议

  • 计算机之间交换信息时约定的规则
  • OSI模型是网络通信的基本规则, TCP/IP协议是基于OSI模型的工业实现

OSI模型

  • Open System Interconnection Reference Model是一种概念模型, 所有计算机都遵守这个规则就可以互相通信, 包含七个层级: 应用层,表示层, 会话层, 传输层, 网络层, 数据链路层, 物理层
  • 物理层: 真正的二进制数据
  • 数据链路层: 确定Mac地址head信息, 分离head和data
  • 网络层: 确定IP地址, 路由等head信息
  • 传输层: 提供端口, 版本, 协议等head信息
  • 应用层: 提供原始数据data和协议(FTP,HTTP,SMTP等)head信息
  • 表示层: 数据格式转化, 能与各系统兼容的格式和统一通用格式之间互换
  • 会话层: 通信管理, 判断是否发送完毕, 是否收到, 管理断开连接等功能

TCP/IP协议

  • Transmission Control Protocol/Internet Protocol: 传输控制/网络协议, 网络通讯协议, 包含FTP, SMTP, TCP, UDP等协议簇, TCP和IP最具代表性
  • OSI模型只是一个描述性概念, 描述了应该如何实现, TCP/IP协议是实际实现, 包含4层
    • 应用层: 应用层, 表示层, 会话层
    • 传输层: 传输层
    • 网络层: 网络层
    • 网络接口层: 数据链路层, 物理层
  • 重要协议
    • 应用层
      • HTTP: 超文本传输协议
      • HTTPS: 加密的超文本传输协议
      • FTP: 文件传输
      • DNS: 域名系统
      • SMTP: 简易邮件传输协议
    • 传输层
      • TCP: 传输控制协议
      • UDP: 用户数据报协议
    • 网络层
      • IP协议
  • TCP协议:
    • 特点
      • 必需建立连接
      • 只能一对一
      • 消息发送失败会重新发送, 不允许丢包
      • 有序
    • 三次握手建立连接
      • C -> S 发送连接请求, 监听返回消息
      • S -> C 监听请求, 收到请求, 发送同意回执
      • C -> S 发送确认状态
      • 之后就可以互相通信
    • 四次挥手
      • C -> S 发送断开连接请求, 监听返回消息
      • S -> C 发送收到, 发送剩余需要发送的数据
      • S -> C 发送同意断连信息
      • C -> S 发送等待信息并开始倒计时, 倒计时时间内没有回复就正式断开连接
    • 应用于保证信息准确性的场景: 文件传输, 远程登录等
  • UDP协议:
    • 特点
      • 无需连接
      • 可靠性低, 可能丢失, 丢失后不会重发
      • 传输效率高, 性能消耗小, 处理速度快
      • n对n
    • 应用于要求实时性的场景: 直播, 语音视频通话等
  • TCP与UDP的对比
    TCP UDP
    连接方面 必需先建立连接才能通信 不必连接就可以通讯
    安全方面 无差错, 不丢失, 不重复, 按序到达 只会提交, 不保证到达
    传输效率 相对较低 相对较高
    连接对象 一对一 一对一, 一对多, 多对一, 多对多

网络通信

通信前

  • IPAddress
    1
    2
    3
    4
    //初始化
    IPAddress ip1 = new IPAddress(new byte[]{222,208,105,1}); //byte[]数组
    IPAddress ip2 = new IPAddress(0x79666F01); //16进制long变量
    IPAddress ip3 = IPAddress.Parse("222.208.105.1"); //string, 推荐
  • IPEndPoint
    1
    2
    //初始化
    IPEndPoint ipPoint = new IPEndPoint(IPAddress.Parse("222.208.105.1"),8080);
  • 域名解析
    • DNS: 一个将域名与IP地址相互映射的分布式数据库
    • IPHostEntry
      • 实例化没有意义, 作为某些方法的返回值使用
      • AddressList: 关联IP
      • HostName: DNS名称
    • Dns静态类
      • GetHostName(): 本机名
      • Dns.GetHostEntry[Async]("www.baidu.com")
        • 使用Task声明异步时, 使用task.Result获取结果
  • 网络数据的序列化与反序列化: BitConverter类和Encoding
    • 常见的字符编码
      • ASCII(美国), GB2312(中国), Shift_JIS(日本), 世界通用的Unicode及基于此形成的UTF-8,UTF-16,UTF-32
      • ASCII使用后7位规定了128个字符, 其他字符基本都是对ASCII的扩充
      • Unicode包含了世界上所有符号, 但只规定了符号和二进制的对应关系, 并没有规定如何存储, UTF-X才是具体的编码方案, UTF-8使用1/2/3/4个字节存储, UTF-16使用2/4个字节存储, UTF-32固定使用4个字节存储
    • BitConverter: 除string的其他类型与byte[]互转
      • 静态方法GetBytes(除string的所有类型)
    • Encoding: stringbyte[]
      • Encoding.string编码类型.GetBytes(string)
    • 序列化与反序列化
      • 序列化: 计算对象字节数, 创建相应容量的字节数组, CopyTo进去. 普通变量类型的字节长度用sizeof(变量类型)获取, 字符串用Encoding.UTF8.GetBytes(stringName).Length获取
      • 反序列化: 非string类型用BitConverter.To类型(byte数组变量,起始index), string类型用Encoding.UTF8.GetString(byte数组,起始index,要转的部分的长度int), 按照Writing()中的顺序依次从获得的byte[]中取出来
      • 序列化数据基类
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        25
        26
        27
        28
        29
        30
        31
        32
        33
        34
        35
        36
        37
        38
        39
        40
        41
        42
        43
        44
        45
        46
        47
        48
        49
        50
        51
        52
        53
        54
        55
        56
        57
        58
        59
        60
        61
        62
        63
        64
        65
        66
        67
        68
        69
        70
        71
        72
        73
        74
        75
        76
        77
        78
        79
        80
        81
        82
        83
        84
        85
        86
        87
        88
        89
        90
        91
        92
        93
        94
        95
        96
        97
        98
        99
        100
        101
        102
        //序列化数据基类
        public abstract class BaseData
        {
        public abstract int GetBytesNum();//返回成员变量的size之和
        public abstract byte[] Writing();
        //在Writing中声明index和byte[GetBytesNum()], 调用Write函数返回byte[]
        public abstract int Reading(byte[] bytes, int beginIndex = 0);
        //在Reading中begin处读取bytes, 返回这个对象的byte[].Length
        protected void WriteInt(byte[] bytes,int value,ref int index)
        {
        BitConverter.GetBytes(value).CopyTo(bytes,index);
        index += 4; //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)
        {
        BitConverter.GetBytes(value).CopyTo(bytes,index);
        index++;
        }
        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)
        {
        byte[] valueBytes = Encoding.UTF8.GetBytes(value);
        WriteInt(bytes,valueBytes.Length,ref infex);
        valueBytes.CopyTo(bytes,index);
        index += valueBytes.Length;
        }
        protected void WriteData(byte[] bytes,BaseData value,ref int index)
        {
        value.Writing().CopyTo(bytes,index);
        index += value.GetBytesNum();
        }
        //反序列化
        protected int ReadInt(byte[] bytes,ref int index)
        {
        int value = BitConverter.ToInt32(bytes,index);
        index += 4;
        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 = BitConverter.ToInt32(bytes,index);
        index += 4;
        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 += t.Reading(bytes,index);
        return value;
        }
        }
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        25
        26
        27
        28
        29
        30
        31
        32
        33
        34
        35
        //数据类: 根据成员变量填充基类的抽象方法
        public class Data:BaseData
        {
        public int age;
        public string name;
        public bool sex;
        //List就先传List的int长度

        public override int GetBytesNum(){
        return 4 + /*sizeof(int)*/
        4 +/*sizeof(int)*/
        Encoding.UTF8.GetBytes(name).Length +
        sizeof(bool);
        }
        public override byte[] Writing(){
        int index = 0;
        byte[] bytes = new byte[GetBytesNum()];
        WriteInt(bytes,age,ref index);
        //每一个string前都要对应一个int存length,用于反序列化时提供string的长度
        WrityeInt(bytes,Encoding.UTF8.GetBytes(name).Length,ref index);
        WriteString(bytes,name,ref index);
        WriteBool(bytes,ref index);
        return bytes;
        }
        public override int Reading(byte[] bytes, int beginIndex = 0)
        {
        int index = beginIndex;
        //按在Writing()中定义的byte[]顺序取出
        age = ReadInt(bytes,index);
        name = ReadString(bytes,index);
        sex = ReadBool(bytes,index);
        return index - beginIndex;
        //返回这个对象的byte[].Length, 用于在ReadData()中用于index的自增
        }
        }
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        //外部使用
        //序列化为byte[]
        Data data = new Data();
        data.age = 8;
        data.name = "John";
        data.sex = true;
        byte[] dataBytes = data.Writing();
        //完成序列化,获得dataBytes
        //反序列化
        Data d = new Data();
        d.Reading(dataBytes);

网络游戏通信方案

  • 弱联网和强联网

    弱联网: 不频繁通信, 每次只处理一次请求, 之后就断开连接

    e.g. 开心消消乐等休闲游戏

    强联网: 频繁通信, 一直保持连接状态

    e.g. MMORPG, MOBA, ACT游戏

  • 长连接和短连接

    短连接: 需要传输数据时连接, 然后断开

    通信方式: HTTP, HTTPS

    长连接: 无论是否需要传输数据, 一直处于连接状态

    通信方式: TCP, UDP

  • Socket,HTTP,FTP

    Socket: 应用层通信字段, 主要用于长连接

    HTTP(S): 简单的请求-响应协议, 通常运行于TCP协议之上, 主要用于短连接和资源下载

    FTP: 用于文件传输, 基于TCP, 用于资源下载和上传

套接字Socket

常用API

  • 三种类型: 流套接字(TCP用), 数据包套接字(UDP用), 原始套接字(直接访问低层数据, 用于侦听和分析数据包, 不常用)
  • 实例化参数
    • AddressFamily网络寻址枚举
      • InterNetwork: IPv4
      • InterNetwork6: IPv6
    • SocketType0套接字类型枚举
      • Dgram: 数据报, UDP
      • Stream: 字节流, TCP
    • ProtocolType协议类型枚举
      • Tcp
      • Udp
  • 成员属性
    • 连接状态(bool): Connected
    • 准备读取的数据的字节数量(int): Available
    • 本机EndPoint对象(as IPEndedPoint): LocalEndedPoint
    • 远程EndPoint对象(as IPEndedPoint): RemoteEndedPoint
  • 成员方法

    服务端

    • 绑定IP和端口号: Bind(IPEndedPoint p)
    • 最大连接客户端数量: Listen(int number)
    • 等待客户端连接: Accept()

      客户端

    • 连接服务器: Connect()

      Both

    • 同步和异步发送和接收数据: Send[To]()Receive()
    • 关闭: Shutdown(SocketShutdown type)
    • 关闭连接: Close()

通信流程

  • TCP
    stateDiagram
        state Server {
            s1:创建Socket
            s2:用Bind()绑定本地地址
            s3:用Listen()监听
            s4:用Accept()等待用户连接
            s5:建立连接,Accept()返回新Socket
            s6:用Send()和Receive()收发数据
            s7:用Shutdown()释放连接
            s8:关闭Socket
            [*] --> s1
            s1 --> s2
            s2 --> s3
            s3 --> s4
            s4 --> s5
            s5 --> s6
            s6 --> s7
            s7 --> s8
            s8 --> [*]
        }
        state Client {
            c1 : 创建Socket
            c2 : 用Connect()连接服务器
            c3 : 用Send()和Receive()收发数据
            c4 : 用Shutdown()释放连接
            c5 : 关闭Socket
            [*]-->c1
            c1 --> c2
            c2 --> c3
            c3 --> c4
            c4 --> c5
            c5-->[*]
        }
    
  • UDP
    stateDiagram
        state Server {
            s1:创建Socket
            s2:用Bind()绑定本地地址
            s3:用SendTo()和ReceiveFrom()收发数据
            s4:用Shutdown()释放连接
            s5:关闭Socket
            [*] --> s1
            s1 --> s2
            s2 --> s3
            s3 --> s4
            s4 --> s5
            s5 --> [*]
        }
        state Client {
            c1 : 创建Socket
            c2 : 用Bind()绑定本地地址
            c3 : 用SendTo()和ReceiveFrom()收发数据
            c4 : 用Shutdown()释放连接
            c5 : 关闭Socket
            [*]-->c1
            c1 --> c2
            c2 --> c3
            c3 --> c4
            c4 --> c5
            c5-->[*]
        }
    

TCP同步

  • 服务端: 实例化, 绑定监听等待, 收发消息, 断开关闭
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    static List<Socket> clientSockets = new List<Socket>();
    static bool isClose = false;
    //s1:创建Socket
    static Socket socketTCP;
    socketTCP = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocaolType.Tcp);
    //s2:用Bind()绑定本地地址
    try
    {
    IPEndPoint ipPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 8080);
    socketTCP.Bind(ipPoint);
    }
    catch (Exception e)
    {
    Console.WriteLine("绑定错误: " + e.Message);
    return;
    }
    //s3:用Listen()监听
    socketTCP.Listen(200); //最大客户端数量
    Console.WriteLine("绑定监听结束, 等待客户端连接")
    //s4:用Accept()等待用户连接 & s5:建立连接,Accept()返回新Socket
    /*
    Socket socketClient = socketTCP.Accept(); //阻塞式代码, 有连入后才能继续往下执行
    Console.WriteLine("有客户端连入");
    */
    Thread acceptThread = new Thread(AcceptClient);
    acceptThread.Start();
    /*在外部创建一个静态函数, 用于放在线程中循环接听*/
    static void AcceptClient()
    {
    while(!isClose){
    Socket clientSocket = socketTCP.Accept();
    clientSockets.Add(clientSocket);
    Console.WriteLine("有客户端连入");
    //s6:用Send()发数据
    socketClients.Send(Encoding.UTF8.GetBytes("欢迎连入"));
    }
    }
    //s6:用Receive()收数据
    Thread rcvMsgThread = new Thread(RcvMsg);
    rcvMsgThread.Start();
    /*外部静态函数*/
    static void RcvMsg()
    {
    byte[] res = new byte[1024 * 1024]; //接收byte[]缓存空间
    Socket clientSocket;
    int i; //循环i放外部, 减少循环压力
    while(!isClose)
    {
    for(i=0;i<clientSockets.Count;i++){
    clientSocket = clientSockets[i];
    if(clientSocket.Available > 0) {
    int length = socketClient.Receive(res);
    /*res存放,length获取真正的长度*/
    //使用线程池取线程处理得到的数据, 使用(,)元组传入参数
    ThreadPool.QueueUserWorkItem(HandleMsg, (clientSocket,Encoding.UTF8.GetString(res,0,length)));
    }
    }
    }
    }
    /*处理数据逻辑*/
    static void HandleMsg(object obj)
    {
    //接收元组参数
    (Socket skt, string str) info = ((Socket skt,string str))obj;
    //处理info.s和info.str
    }
    //s7:用Shutdown()释放连接 & s8:关闭Socket
    while(true)
    {
    string input = Console.ReadLine();
    if(input == "quit"){
    isClose = true;
    for(int i=0;i<clientSockets.Count;i++){
    socketClients[i].Shutdown(SocketShutdown.Both);
    socketClients[i].Close();
    }
    clientSockets.Clear();
    break;
    } else if(input.Substring(0,2)=="B:"){/*B:发送广播消息*/
    for(int i=0;i<clientSockets.Count;i++){
    socketClients[i].Send(Encoding.UTF8.GetBytes(input.Substring(2)));
    }
    }
    }
    • 封装
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      87
      88
      89
      90
      91
      92
      93
      94
      95
      96
      97
      98
      99
      100
      101
      102
      103
      104
      105
      106
      107
      108
      109
      110
      111
      112
      113
      114
      115
      116
      117
      118
      119
      120
      121
      122
      123
      124
      125
      126
      127
      128
      129
      130
      131
      132
      133
      134
      135
      136
      137
      138
      139
      140
      141
      142
      143
      144
      145
      146
      147
      148
      149
      150
      151
      152
      153
      154
      155
      156
      157
      158
      159
      160
      161
      162
      163
      164
      165
      166
      167
      168
      169
      170
      171
      172
      173
      174
      175
      176
      177
      178
      179
      180
      181
      182
      183
      184
      185
      186
      187
      188
      189
      190
      191
      192
      193
      194
      195
      196
      197
      198
      199
      200
      201
      202
      203
      204
      205
      206
      207
      208
      209
      210
      211
      212
      213
      214
      215
      216
      217
      218
      219
      220
      221
      222
      223
      224
      225
      public class ServerSocket
      {
      public bool isClose;
      public Socket socket; /*成员Socket*/
      public Dictionary<int,ClientSocket> clientDic = /*所有clients*/
      new Dictionary<int,ClientSocket>();
      //待移除, 避免直接移除导致foreach时出问题
      private List<ClientSocket> delList = new List<ClientSocket>();

      //开启
      public void Start(string ip, int port, int max){
      socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
      IPEndPoint ipPoint = new IPEndPoint(IPAddress.Parse(ip),port);
      socket.Bind(ipPoint);
      socket.Listen(max);
      ThreadPool.QueueUserWorkItem(Accept);
      ThreadPool.QueueUserWorkItem(Receive);
      isClose = false;
      }
      //接收连接逻辑
      private void Accept(object obj){
      while(!isClose)
      {
      try
      {
      Socket clientSocket = socket.Accept();
      ClientSocket client = new ClientSocket(clientSocket);
      lock(clientDic){
      clientDic.Add(client.clientID,client);
      }
      }
      catch (Exception e)
      {
      Console.WriteLine(e.Message);
      }
      }
      }
      //接收数据逻辑
      private void Receive(object obj){
      while(!isClose)
      {
      if(clientDic.Count > 0){
      lock(clientDic){
      foreach (ClientSocket c in clientDic.Values)
      {
      c.Receive();
      }
      }
      }
      }
      }

      //关闭
      public void Close(){
      isClose = true;
      foreach (ClientSocket c in clientDic.Values)
      {
      c.Close();
      }
      clientDic.Clear();
      socket = null;
      }

      //广播
      public void Broadcast(BaseMsg s){
      lock(clientDic){
      foreach (ClientSocket c in clientDic.Values){
      /* 广播的处理逻辑, 待完善
      c.Send(s);
      */
      }
      }
      }

      //客户端关闭事件
      public void CloseClientSocket(ClientSocket c){
      lock(clientDic){
      c.Close();
      if(clientDic.ContainsKey(c.clientID)){
      clientDic.Remove(c.clientID);
      }
      }
      }

      //准备移除ClientSocket
      public void AddDelSocket(ClientSocket cs){
      if(!delList.Contains(cs)){
      delList.Add(cs);
      }
      }
      }

      public class ClientSocket
      {
      private static int CLIENT_ID = 1;/*静态ID*/
      public int clientID; /*成员ID*/
      public Socket socket; /*成员Socket*/
      private byte[] cacheBytes = new byte[1024 * 1024];
      private int cacheLength = 0;

      //构造函数
      public ClientSocket(Socket s){
      this.clientID = CLIENT_ID;
      this.socket = s;
      CLIENT_ID++;
      }

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

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

      //发送
      public void Send(BaseMsg msg){
      if(!IsConnected){return;}
      try
      {
      socket?.Send(msg.Writing());
      }
      catch(Exception e)
      {
      Console.WriteLine(e.message);
      Close();
      }
      }

      //接收
      public void Receive(){
      if(!IsConnected){return;}
      try
      {
      if(socket.Available > 0){
      byte[] res = new byte[1024 * 5];
      int length = socketClient.Receive(res);
      HandleRcvMsg(res,length);
      /*
      int msgID = BitConverter.ToInt32(res,0);
      BaseMsg msg = null;
      switch(msgID)
      {
      case 1001:
      msg = new PlayerInfo();
      msg.Reading(res,4);
      break;
      }
      if(msg==nul){ return;}
      ThreadPool.QueueUserWorkItem(HandleMsg,msg);
      */
      }
      }
      catch (Exception e)
      {
      Console.WriteLine(e.Message);
      Close();
      }
      }
      //接收数据后处理逻辑
      private static void HandleMsg(object obj)
      {
      BaseMsg m = obj as BaseMsg;
      if(m is PlayerInfo){
      PlayerInfo pl = m as PlayerInfo;
      /*接收到PlayerInfo数据的逻辑*/
      }
      }
      private void HandleRcvMsg(byte[] bytes, int len)
      {
      int msgID = 0;
      int msgLength = 0;
      int nowIndex = 0;

      //拼接
      bytes.CopyTo(cacheBytes,cacheLength);
      cacheLength += len;

      while(true)
      {
      msgLength = -1;
      if(cacheLength - nowIndex >= 8){
      //解析ID
      msgID = BitConverter.ToInt32(cacheBytes, nowIndex);
      nowIndex += 4;
      //解析长度
      msgLength = BitConverter.ToInt32(cacheBytes, nowIndex);
      nowIndex += 4;
      }

      if(cacheLength - nowIndex >= msgLength && msgLength != -1){
      /*保证进去时上一段代码已经执行, 避免上一次的数据影响下次*/
      //解析数据
      BaseMsg baseMsg = null;
      switch(msgID)
      {
      case 1001:
      PlayerInfo pl = new PlayerInfo();
      pl.Reading(cacheBytes,nowIndex);
      baseMsg = pl;
      break;
      }
      if(baseMsg!=null){
      ThreadPool.QueueUserWorkItem(HandleMsg,msg);
      }
      nowIndex += len;
      if(nowIndex==cacheLength){
      cacheLength = 0;
      break;
      }
      } else{
      if(len != -1){
      nowIndex -= 8;
      Array.Copy(cacheBytes, nowIndex, cacheBytes, 0, len - nowIndex);
      cacheLength -= nowIndex;
      }
      break;
      }
      }
      }
      }
    • 外部使用
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      ServerSocket s = new ServerSocket();
      s.Start("127.0.0.1",8080,5);
      Console.WriteLine("服务器开启成功");
      while(!s.isClose){
      string in = Console.ReadLine();
      if(in == "quit"){
      s.Close();
      } else if(in.Substring(0,2)=="B:"){
      /* 广播处理逻辑, 待完善, 可以声明一个继承BaseMsg的广播消息类发送
      s.Broadcast(in.Substring(2));
      */
      }
      }
  • 客户端: 实例化, 连接, 收发消息, 断开关闭
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    //c1 : 创建Socket
    Socket socketTCP = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocaolType.Tcp);
    //c2 : 用Connect()连接服务器
    IPEndPoint ipPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 8080 );
    try
    {
    socketTCP.Connect(ipPoint);
    }
    catch (SocketException e)
    {
    if(e.ErrorCode == 10061){
    Console.WriteLine("服务器拒绝连接");
    } else{
    Console.WriteLine("连接失败: " + e.ErrorCode);
    }
    return;
    }
    //c3 : 用Send()和Receive()收发数据
    byte[] res = new byte[1024];
    int length = socketTCP.Receive(res);
    Console.WriteLine("服务器消息: " + Encoding.UTF8.GetString(res,0,length));
    socketTCP.Send(Encoding.UTF8.GetBytes("这里是客户端"));
    //c4 : 用Shutdown()释放连接
    socketTCP.Shutdown(SocketShutdown.Both);
    //c5 : 关闭Socket
    socketTCP.Close();
    • Mono单例模式: 客户端网络通信管理器NetMgr
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      87
      88
      89
      90
      91
      92
      93
      94
      95
      96
      97
      98
      99
      100
      101
      102
      103
      104
      105
      106
      107
      108
      109
      110
      111
      112
      113
      114
      115
      116
      117
      118
      119
      120
      121
      122
      123
      124
      125
      126
      127
      128
      129
      130
      131
      132
      133
      134
      135
      136
      137
      138
      139
      140
      141
      142
      143
      144
      145
      146
      147
      148
      149
      150
      151
      public class NetMgr : MonoBehavior
      {
      private static NetMgr instance;
      public static NetMgr Instance => instance;
      private Socket socket;
      private Queue<BaseMsg> sendMsgQueue = new Queue<BaseMsg>();
      /*用于发送消息的容器队列 主线程存 发送线程取*/
      private Queue<BaseMsg> rcvMsgQueue = new Queue<BaseMsg>();
      private byte[] cacheBytes = new byte[1024 * 1024]; //分包暂存用
      private int cacheLength = 0;
      private bool isConnected = false;

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

      void Update()
      {
      if(rcvMsgQueue.Count > 0){
      BaseMsg msg = rcvMsgQueue.Dequeue();
      if(msg is PlayerInfo){
      PlayerInfo pl = msg as PlayerInfo;
      /*pl逻辑*/
      }
      }
      }

      void OnDestroy()
      {
      Close();
      }

      //连接
      public void Connect(string ip, int port)
      {
      if(isConnected){ return;}
      if(socket == null){
      socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocaolType.Tcp);
      }
      IPEndPoint ipPoint = new IPEndPoint(IPAddress.Parse(ip),port);
      try
      {
      socket.Connect(ipPoint);
      ThreadPool.QueueUserWorkItem(SendMsg);
      ThreadPool.QueueUserWorkItem(ReceiveMsg);
      isConnected = true;
      }
      catch (SocketException e)
      {
      isConnected = false;
      if(e.ErrorCode == 10061){
      Console.WriteLine("服务器拒绝连接");
      } else{
      Console.WriteLine("连接失败: " + e.ErrorCode);
      }
      }
      }

      //发送数据
      public void Send(BaseMsg m)
      {
      sendMsgQueue.Enqueue(m);
      }
      private void SendMsg(object obj)
      {
      while(isConnected)
      {
      if(sendMsgQueue.Count > 0){
      socket?.Send(sendMsgQueue.Dequeue().Writing());
      }
      }
      }

      //接收数据
      /*这里接收的数据在rcvMsgQueue中, 在Update中监听收取*/
      private void ReceiveMsg(object obj)
      {
      while(isConnected)
      {
      if(socket.Available > 0){
      byte[] rcvBytes = new byte[1024 * 1024];
      int rcvLength = socket.Receive(rcvBytes);
      HandleRcvMsg(rcvBytes,rcvLength); //分包黏包判断
      }
      }
      }
      private void HandleRcvMsg(byte[] bytes, int len) //分包黏包判断
      {
      int msgID = 0;
      int msgLength = 0;
      int nowIndex = 0;

      //拼接
      bytes.CopyTo(cacheBytes,cacheLength);
      cacheLength += len;

      while(true)
      {
      msgLength = -1;
      if(cacheLength - nowIndex >= 8){
      //解析ID
      msgID = BitConverter.ToInt32(cacheBytes, nowIndex);
      nowIndex += 4;
      //解析长度
      msgLength = BitConverter.ToInt32(cacheBytes, nowIndex);
      nowIndex += 4;
      }

      if(cacheLength - nowIndex >= msgLength && msgLength != -1){
      /*保证进去时上一段代码已经执行, 避免上一次的数据影响下次*/
      //解析数据
      BaseMsg baseMsg = null;
      switch(msgID)
      {
      case 1001:
      PlayerInfo pl = new PlayerInfo();
      pl.Reading(cacheBytes,nowIndex);
      baseMsg = pl;
      break;
      }
      if(baseMsg!=null){ rcvQueue.Enqueue(baseMsg);}
      nowIndex += len;
      if(nowIndex==cacheLength){
      cacheLength = 0;
      break;
      }
      } else{ //分包
      //如果解析了头部而不解析数据, 则nowIndex - 8并重新存
      if(len != -1){
      nowIndex -= 8;
      Array.Copy(cacheBytes, nowIndex, cacheBytes, 0, len - nowIndex);
      cacheLength -= nowIndex;
      }
      break;
      }
      }
      }

      //关闭
      public void Close()
      {
      isConnected = false;
      socket?.Shutdown(SocketShutdowm.Both);
      socket?.Disconnect(false);
      socket?.Close();
      socket = null;
      isConnected = false;
      }
      }
    • 外部使用: 在游戏Main方法的Start()中为其专门创建一个GO使用
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      void Start()
      {
      //为NetMgr初始化一个GO
      if(NetMgr.Instance == null){
      GameObject netmgr = new GameObject("netmgr");
      netmgr.AddComponent<NetMgr>();
      }

      //然后可以连接和发送消息
      NetMgr.Instance.Connect("127.0.0.1",8080);
      }
  • 区分消息类型

    获取到的字节数组如何区分是什么类 > 在消息头部添加消息ID用于识别

    • 创建继承BaseData的消息基类BaseMsg
      1
      2
      3
      4
      5
      6
      7
      public class BaseMsg:BaseData
      {
      /*三个override*/

      //子类重写
      public virtual int GetID(){ return 0;}
      }
    • 消息数据类
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      public class PlayerInfo:BaseMsg
      {
      string name;
      int age;

      //四个override
      public override int GetBytesNum(){
      return 4 + /*消息ID的int*/
      4 + /*用于存储消息长度, 以检验分包和黏包*/
      4 + /*string前的int*/
      Encoding.UTF8.GetBytes(name).Length +
      sizeof(bool) /*age的int*/;
      }
      public override byte[] Writing(){
      int index = 0,bytesnum = GetBytesNum();
      byte[] bytes = new byte[bytesnum];
      WriteInt(bytes,GetID(),ref index); //消息ID的位置
      WriteInt(bytes,bytesnum - 8, ref index); //消息的长度
      WrityeInt(bytes,Encoding.UTF8.GetBytes(name).Length,ref index);
      WriteString(bytes,name,ref index);
      WriteInt(bytes,age,ref index);
      return bytes;
      }
      public override int Reading(byte[] bytes, int beginIndex = 0)
      {
      int index = beginIndex;
      //识别消息ID应在Reading之前, Reading专门处理数据问题
      name = ReadString(bytes,index);
      age = ReadInt(bytes,index);
      return index - beginIndex;
      }
      public override int GetID()
      {
      return 1001; //自定义标识ID, 也可以返回short,long
      }
      }
    • Socket接收消息前
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      byte[] bytes = new byte[1024]; //临时byte[]
      int rcvLength = socket.Receive(bytes); //真实长度
      int msgID = BitConverter.ToInt32(bytes,0); //获得消息ID
      switch(msgID) //消息ID识别分支
      {
      case 1001:
      PlayerInfo pi = new PlayerInfo();
      pi.Reading(bytes,4);
      break;
      }
  • 分包和黏包

    分包: 一个消息被分成了多个消息进行发送;
    黏包: 一个消息和另一个消息黏在了一起
    ( 以上两者可能同时发生 )
    解决思路: 加一个长度头部, 根据长度判断是否完整/分包/黏包

  • 心跳消息: 长连接游戏中, 客户端按时不断发消息, 服务端一直检测消息
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    //消息类
    public class HeartMsg:BaseMsg
    {
    public override int GetBytesNum()
    {
    return 8; //不需要内容, 只需要头部ID和Length
    }
    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 Reading(byte[] bytes, int beginIndex = 0)
    {
    return 0;
    }
    public override int GetID()
    {
    return 999;
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    //NetMgr类
    private int HEART_MSG_TIME = 2;
    private HeartMsg heartMsg = new HeartMsg();

    void Awake()
    {
    InvokeRepeating("SendHeartMsg",0,HEART_MSG_TIME);
    }

    private void SendHeartMsg()
    {
    if(isConnected){
    Send(heartMsg);
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    //ClientSocket
    private long crtTime = -1;
    private static TIME_OUT_TIME = 10;
    //收到消息的switch
    case 999: break;
    //Handle消息
    crtTime = DataTime.Now.Ticks / TimeSpan.TicksPerSecond;
    /*更新时间*/
    //检查是否超时的函数
    private void CheckTimeOut(object obj)
    {
    if(crtTime != -1 &&
    DataTime.Now.Ticks / TimeSpan.TicksPerSecond >= TIME_OUT_TIME)
    {
    this.AddDelList(this);
    }
    }
    //在Receive函数中添加该函数

TCP异步

  • 异步原理
    • 线程回调
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      //Async
      public void CountAsync(int second, Action callback)
      {
      Thread t = new t(() = {
      while(second>0){
      Console.WriteLine(second);
      Thread.Sleep(1000);
      second--;
      }
      callback?.Invoke();
      });
      t.Start();
      }

      //调用
      CountAsync(3,() = {
      Console.WriteLine("结束");
      });

      /*
      3
      2
      1
      结束
      */
    • 分步执行, await时返回出去, 执行完毕后继续await后面的
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      //async
      public async void CountAsync(int second)
      {
      await Task.Run(() => {
      while(second>0){
      Console.WriteLine(second);
      Thread.Sleep(1000);
      second--;
      }
      });
      Console.WriteLine("结束");
      }

      //调用
      CountAsync(3);
      Console.WriteLine("a");

      /*
      3
      a
      2
      1
      结束
      */
  • Socket TCP中的异步方法(BeginEnd)
    • 函数参数
      1
      2
      3
      //回调函数参数IAsyncResult
      //AsyncState调用异步方法时传入的参数
      //AsyncWaitHandle同步等待
    • 服务器
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      //1. 接收连接
      Socket socketTCP = new Socket(..,..,..);
      socketTCP.BeginAccpet(BeginBeginAccept(res),socketTCP);
      /*res -> IAsyncResult*/

      private BeginBeginAccept(IAsyncResult res)
      {
      try
      {
      Socket s = res.AsyncState as Socket;
      /*res.AsyncState -> BeginAccept的第二个参数*/
      Socket clientSocket = s.EndAccpet(res);
      /*EndSocket返回值为客户端Socket*/
      s.BeginAccept(res,s);
      /*这里不算递归*/
      }
      catch (SocketException e)
      {
      print(e.SocketErrorCode);
      }
      }

      //2. 接收消息
      BeginReceive(存储数组, index, 数组长度, 标识枚举, 回调函数, AsyncState)
    • 客户端
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      //1. 开始连接
      IPEndPoint ipPoint = new (IPAddress.Parse("localhost"),8080);
      Socket socketTCP = new Socket(..,..,..);
      socketTCP.BeginConnect(ipPoint,(res)=>{
      Socket s = res.AsyncState as Socket;
      try
      {
      s.EndConnect(res);
      //连接成功
      }
      catch (SocketException e)
      {
      //连接失败
      }
      },socketTCP);

      //2. 发送消息
      BeginSend(数组, 标识, 回调, AsyncState)
    • 封装
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      87
      88
      89
      90
      91
      92
      93
      //服务端
      public class ClientSocket
      {
      public Socket socket;
      public int clientID;
      private static int CLIENT_BEGIN_ID = 1;
      private byte[] cacheBytes = new byte[1024];
      private int cacheNum = 0;

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

      this.socket.BeginReceive(cacheBytes, cacheNum,
      cacheBytes.Length, SocketFlags.None,
      ReceiveCallback, null);
      }

      private void ReceiveCallback(IAsyncResult res)
      {
      try
      {
      cacheNum = this.socket.EndReceive(res);
      /*处理逻辑*/
      cacheNum = 0;
      if(this.socket.Connected){
      this.socket.BeginReceive(cacheBytes, cacheNum,
      cacheBytes.Length, SocketFlags.None,
      ReceiveCallback, null);
      }
      }
      catch (SocketException e)
      {
      Debug.log(e.SocketErrorCode + e.Message);
      }
      }

      public void Send(string s)
      {
      if(this.socket.Connected){
      byte[] bytes = Encoding.UTF8.GetBytes(s);
      this.socket.BeginSend(bytes, 0, bytes.Length, SocketFlags.None, SendCallback, null);
      }
      }

      private void SendCallback(IAsyncResult res)
      {
      if(this.socket.Connected){
      try
      {
      this.socket.EndSend(res);
      }
      catch (SocketException e)
      {
      Debug.log(e.SocketErrorCode + e.Message);
      }
      }
      }
      }

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

      public void Start(string ip, int port, int num){
      socket = new (..,..,..);
      IPPoint ipPoint = new (IPAddress.Parse("217.0.0.1"),8080);
      try
      {
      socket.Bind(ipPoint);
      socket.Listen(num);
      socket.BeginAccept(AcceptCallback,null);
      }
      catch (Exception e)
      {
      Debug.log(e.Message);
      }
      }
      private void AcceptCallback(IAsyncResult res)
      {
      try
      {
      ClientSocket client = new (socket.EndAccept(res));
      clientDic.Add(client.ID,client);
      socket.BeginAccept(AcceptCallback,null);
      }
      catch (SocketException e)
      {
      Debug.log(e.SocketErrorCode + e.Message);
      }
      }
      }
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      //客户端
      public class NetAsyncMgr : MonoBehaviour
      {
      private static NetAsyncMgr instance;
      public static Instance => instance;
      private Socket socket;
      private byte[] cacheBytes = new byte[1024*1024];
      private int byteNum = 0;

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

      public void Connect(string ip, int port)
      {
      if(socket != null && socket.Connected){ return;}
      IPPoint ipPoint = new (IPAddress.Parse(ip),port);
      socket = new Socket(..,..,..);
      SocketAsyncEventArgs args = new ();
      args.RemoteEndPoint = ipPoint;
      args.Completed += ((socket,args) => {
      if(args.SocketError = SocketError.Success){
      //连接成功
      SocketAsyncEventArgs rcvArgs = new ();
      rcvArgs.SetBuffer(cacheBytes,0,cacheNum);
      rcvArgs.Completed += RcvCallback;
      this.socket.ReceiveAsync(rcvArgs);
      }else{
      Debug.log(args.SocketError);
      }
      });
      socket.ConnectAsync(args);
      }
      private void RcvCallback(Object obj, SocketAsyncEventArgs args)
      {
      if(args.SocketError = SocketError.Success){
      //连接成功
      //args.Buffer
      //args.BytesTransferred
      rcvArgs.SetBuffer(0,args.Buffer.Length);
      this.socket.ReceiveAsync(args);
      }else{
      Debug.log(args.SocketError);
      }
      }

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

      public void Send(string s)
      {
      byte[] bytes = Encoding.UTF8.GetBytes(s);
      SocketAsyncEventArgs rcvArgs = new ();
      rcvArgs.SetBuffer(cacheBytes,0,cacheNum);
      }
      }
  • Socket TCP中的异步方法(Async)(.Net 3.5)
    • 传入参数SocketAsyncEventArgs
    • 服务端
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      Socket socketTCP = new (..,..,..0);
      SocketAsyncEventArgs e = new SocketAsyncEventArgs();
      e.Completed += (socket, args) => {
      if(args.SocketError = SocketError.Success){
      Socket clientSocket = args.Success;
      (socket as Socket).AcceptAsync(args);
      }
      else
      {
      print(args.SocketError);
      }
      };
      socketTCP.AcceptAsync(e);
    • 客户端
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      Socket socketTCP = new (..,..,..0);
      SocketAsyncEventArgs e = new SocketAsyncEventArgs();
      e.Completed += (socket, args) => {
      if(args.SocketError = SocketError.Success){
      //连接成功
      }
      else
      {
      print(args.SocketError);
      }
      };
      socketTCP.ConnectAsync(e);
    • 两端
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      //发送消息
      SocketAsyncEventArgs e2 = new ();
      byte[] bytes = Encoding.UTF8.GetBytes("aaa");
      e2.SetBuffer(bytes,0,bytes.Length);
      e.Completed += (socket, args) => { //监听是否发送成功
      if(args.SocketError = SocketError.Success){
      //成功
      }
      else
      {
      print(args.SocketError);
      }
      };
      socketTCP.SendAsync(e2);

      //接收消息
      SocketAsyncEventArgs e3 = new ();
      e3.SetBuffer(new byte[1024*1024],0,1024*1024);
      e3.Completed += (socket, args) => { //监听是否接收成功
      if(args.SocketError = SocketError.Success){
      //成功处理逻辑
      Encoding.UTF8.GetString(args.Buffer,0,args.BytesTransferred);
      /*
      args.Buffer 收到的字节
      args.BytesTransferred 收到字节的长度
      */
      args.SetBuffer(0,args.Buffer.Length);
      (socket as Socket).ReceiveAsync(args);
      }
      else
      {
      print(args.SocketError);
      }
      };
      socketTCP.ReceiveAsync(e3);

UDP同步

  • UDP不存在黏包问题, 分包问题(因为丢包/乱序)更严重, 因此需要控制大小在最大传输单元(Max Transmission Unit)内, 一般控制大小: 局域网环境1472字节内, 互联网环境内548字节内
    • 消息过小时可以手动黏包, 过大时手动分包, 分包使用序号,长度,ID等信息确定同一个包
  • C/S代码
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    //1. 声明
    Socket socket = new (AddressFamily.InterWorknet, SocketType.Dgram, ProtocolType.Udp);
    //2. 绑定
    IPEndPoint ipPoint = new (IPAdrress.Parse("127.0.0.1"),8080);
    socket.Bind(ipPoint);
    //3. 发送消息
    IPEndPoint remoteIpPoint = new (IPAddress.Parse("1.0.0.1"),8081);
    socket.SendTo(Encoding.UTF8.GetBytes("a"),remoteIpPoint);
    //4. 接收消息
    byte[] bytes = new byte[512];
    EndPoint remoteClientPoint = new IPEndPoint(IPAddress.Any,0);
    int length = socket.ReceiveFrom(bytes, ref remoteClientPoint);
    /*收到的byte[]存入bytes中, 收到的EndPoint存入remoteClientPoint中*/
    //5. 释放关闭
    socket.Shutdown(SocketShutdown.Both);
    socket.Close();
  • 封装
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    //存储连接过的客户端
    public class Client
    {
    private IPEndPoint clientIP;
    public string clientID;
    public long lastTime = -1;

    public Client(string ip, int port)
    {
    clientIP = new IPEndPoint(IPAddress.Parse(ip), port);
    clientID = ip + ":" + port;
    }
    public void RcvBytes(byte[] bytes)
    {
    byte[] cacheBytes = new byte[1024];
    lastTime = DataTime.Now.Ticks / TimeSpan.TicksPerSecond;
    bytes.CopyTo(cacheBytes, 0);
    ThreadPool.QueueUserWorkItem(RcvHandle,cacheBytes);
    }
    private void RcvHandle(object obj)
    {
    byte[] bytes = obj as byte[];
    int nowIndex = 0;
    int msgID = BitConverter.ToInt32(bytes, nowIndex);
    nowIndex += 4;
    int msgLength = BitConverter.ToInt32(bytes, nowIndex);
    nowIndex += 4;
    switch (msgID)
    {
    case 1001:
    break;
    }
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    //服务端
    public class ServerSocket
    {
    Socket sokcet;
    private bool isClose;
    private Dictionary<string,Client> clientDic = new ();

    public void Start(string ip, int port)
    {
    if(!isClose){return;}
    IPEndPoint ipPoint = new (IPAddress.Parse(ip), port);
    socket = new Socket(..,..,..);
    try
    {
    socket.Bind(ipPoint);
    isClose = false;
    ThreadPool.QueueUserWorkItem(RcvMsg);
    ThreadPool.QueueUserWorkItem(CheckTimeOut);
    }
    catch (Exception e)
    {
    Debug.log(e.Message);
    }
    }
    private void RcvMsg(object obj)
    {
    byte[] bytes = new byte[512];
    EndPoint ipPoint = new IPEndPoint(IPAddress.Any, 0);
    string dicID = "";
    string ip;
    int port;
    while(!isClose)
    {
    if(socket.Available > 1){
    lock(socket){
    socket.ReceiveFrom(bytes, ref ipPoint);
    }
    //处理逻辑
    ip = (ipPoint as IPEndPoint).Address.ToString();
    port = (ipPoint as IPEndPoint).Port;
    dicID = ip + ":" + port;
    if(!clientDic.ContainsKey(dicID)){
    clientDic.Add(dicID,new Client(ip,port));
    }
    clientDic[dicID].RcvBytes(bytes);
    }
    }
    }

    public void SendTo(BaseMsg msgIPEndPoint ipPoint)
    {
    try
    {
    lock(socket)
    {
    socket.SendTo(msg.Writing(),ipPoint);
    }
    }
    catch (SocketException e)
    {
    Debug.log(e.SocketErrorCode + e.Message);
    }
    catch (Exception e)
    {
    Debug.log(e.Message);
    }
    }

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

    public void Remove(string clientID)
    {
    if(clientDic.ContainsKey(clientID)){
    clientDic.Remove(clientID);
    }
    }

    private void CheckTimeOut(object obj)
    {
    long nowTime = 0;
    List<string> delList = new ();
    int fori = 0;
    while(!isClose)
    {
    Thread.Sleep(30000);
    nowTime = DataTime.Now.Ticks / TimeSpan.TicksPerSecond;
    foreach(Client c in clientDic.Values){
    if(nowTime - c.lastTime >= 10){
    delList.Add(c.clientID);
    }
    }
    for(fori = 0; fori< delList.Count;fori++){
    Remove(delList[fori]);
    }
    fori = 0;
    delList.Clear();
    }
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    //客户端
    public class UdpNetMgr:MonoBehaviour
    {
    private static UdpNetMgr instance;
    public static UdpNetMgr Instance => instance;
    private EndPoint ipPoint;
    private Socket socket;
    private bool isClose;
    private Queue<BaseMsg> sendQueue = new ();
    private Queue<BaseMsg> receiveQueue = new ();
    private byte[] cacheBytes = new byte[512];

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

    void Update()
    {
    if(receiveQueue.Count > 0){
    BaseMsg baseMsg = receiveQueue.Dequeue();
    switch(baseMsg)
    {
    case PlayerInfo msg:
    print(msg.playerID);
    break;
    }
    }
    }

    void OnDestroy()
    {
    Close();
    }

    public void StartClient(string ip, int port)
    {
    if(!isClose){ return ;}
    ipPoint = new IPEndPoint(IPAddress.Parse(ip),port);
    IPEndPoint cliengIpPoint = new IPEndPoint(IPAddress.Parse("localhost"),8080);
    try
    {
    socket = new Socket(..,..,..);
    socket.Bind(cliengIpPoint);
    isClose = false;
    ThreadPool.QueueUserWorkItem(ReceiveMsg);
    ThreadPool.QueueUserWorkItem(SendMsg);
    }
    catch (Exception e)
    {
    Debug.log(e.Message);
    }
    }

    private void ReceiveMsg(object obj)
    {
    EndPoint tempIpPoint = new IPEndPoint(IPAddress.Any, 0);
    int nowIndex, msgID, msgLength;
    BaseMsg msg = null;
    while(!isClose)
    {
    if(socket != null && socket.Available > 0){
    try
    {
    socket.ReceiveFrom(cacheBytes, ref tempIpPoint);
    //避免骚扰
    if(!tempIpPoint.Equals(ipPoint)){
    continue;
    }
    //处理消息
    nowIndex = 0;
    msgID = BitConverter.ToInt32(cacheBytes, nowIndex);
    nowIndex +=4;
    msgLength = BitConverter.ToInt32(cacheBytes, nowIndex);
    nowIndex +=4;
    switch(msgID)
    {
    case 1001:
    msg = new PlayerInfo();
    msg.Reading(cacheBytes, nowIndex);
    break;
    }
    if(msg!=nul){
    receiveQueue.Enqueue(msg);
    }
    }
    catch (SocketException e)
    {
    Debug.Lod(e.SocketErrorCode + e.Message);
    }
    catch (Exception e)
    {
    Debug.Log(e.Message);
    }
    }
    }
    }

    private void SendMsg(object obj)
    {
    while(!isClose){
    if(socket != null && sendQueue.Count > 0){
    try
    {
    socket.SendTo(sendQueue.Dequeue().Writing(),ipPoint);
    }
    catch (Exception e)
    {
    Debug.Log(e.SocketErrorCode + e.Message);
    }
    }
    }
    }

    public void Send(BaseMsg msg)
    {
    sendQueue.Enqueue(msg);
    }

    public void Close()
    {
    if(socket != null){
    isClose = true;
    /*发一个退出消息*/
    socket.Shutdown(SocketShutdown.Both);
    socket.Close();
    socket = null;
    }
    }
    }

UDP异步

  • Begin方法
    1
    2
    3
    4
    5
    6
    7
    8
    9
    IAsyncResult socket.BeginSendTo(byte[] buffer, //bytes
    int offset, //偏移量
    int size, //字节长度
    SocketFlags flag, //使用None就可以
    EndPoint remoteEP, //远程IP
    AsyncCallback callback,
    object state /*额外的参数*/)
    //在回调委托中使用EndSendTo()
    //BeginReceiveFrom()同
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    socket.BeginSendTo(bytes,
    0,
    bytes.Length,
    SocketFlags.None,
    ipPoint,
    SendToCallback,
    socket);

    private void SendToCallback(IAsyncResult res)
    {
    try
    {
    Socket s = res.AsyncState as Socket;
    s.EndSendTo(res);
    }
    catch (SocketException e)
    {
    Debug.Log(e.SocketErrorCode + e.Message);
    }
    }
  • Async方法
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    SocketEventAsyncArgs args = new ();
    args.SetBuffer(bytes,0, bytes.Length);
    args.Completed += SendToAsyncCallback;
    socket.SendToAsync(args);
    private void SendToAsyncCallback(object s,SocketEventAsyncArgs args)
    {
    //回调事件
    if(args.SocketErrorCode == SocketError.Success){
    //成功
    }else{
    //失败
    }
    }
    //ReceiveFromAsync同理
    //接收后继续接收

文件传输FTP

  • File Transfer Protocol: 上传下载文件的一套规则

    本质是两个TCP连接, 一个控制传输, 一个传输数据

    两种传输模式: 1. 主动(Port)模式, 传输数据请求由服务器发起, 受到客户端防火墙影响用的较少; 2. 被动(Passive)模式, 传输数据请求由客户端发起

    两种传输方式: 1. ASCII, 适用于仅包含英文命令, 参数英文文本文件; 2. 二进制方式(推荐), 可以指定编码方式, 传输非英文文本

  • 相关类: FtpWebRequest, FtpWebResponse, NetworkCredential
  • 解决的问题: 1. 搭建FTP服务器; 2. 上传; 3. 下载

搭建FTP服务器

  • 三种方式
    1. 使用别人写好的FTP软件搭建(推荐)(Serv-U)
    2. 根据原理写FTP(一般后端, 后端一般也不写)
  • NetworkCredential: 设置账号密码
    1
    NetworkCredential n = new (string username, string password);
  • FtpWebRequest: 客户端操作类, 发送上传下载删除文件的命令
    1
    2
    3
    4
    5
    6
    7
    8
    9
    //方法
    //1. 绑定服务器对象Create()
    FtpWebRequest r = FtpWebRequest.Create("ftp://127.0.0.1/a.txt") as FtpWebRequest;
    //2. 正在进行文件传输时中止传输Abort()
    r.Abort();
    //3. 获取上传流GetRequestStream()
    Stream s = r.GetRequestStream();
    //4. 服务器响应WebResponse GetResponse()
    FtpWebResponse res = r.GetResponse() as FtpWebResponse;
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    //成员变量
    //1. Credentials通信凭证
    r.Credentials = n;
    //2. KeepAlive完成传输后是否继续保持连接状态, 默认true
    r.KeepAlive = false;
    //3. 操作命令, 使用WebResquestMethods.Ftp静态类的静态常量实现的枚举string
    // 删除文件,下载文件,文件列表,详细列表,创建目录,删除目录,上传文件
    r.Method = WebResquestMethods.Ftp.DownloadFile;
    //4. 是否使用二进制传输UseBinary
    r.UseBinary = true;
    //5. 重命名RenameTo(string s)
    r.RenameTo("b.txt");
  • FtpWebResponse类: 服务器对请求的响应, 使用成员方法GetResponse()获取, 使用完毕后要Close()
    1
    2
    3
    4
    5
    6
    FtpWebResponse res = r.GetResponse() as FtpWebResponse;
    /*r属性设置完毕后, 调用GetResponse()方法才会发送r并获得响应*/
    //1. 关闭Close()
    res.Close();
    //2. 获取下载数据流
    Stream s = res.GetResponseStream();
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    //1. 接收数据长度ContentLength
    int l = res.ContentLength;
    //2. 接收数据类型ContentType
    string typeString = res.ContentType;
    //3. 服务器发送的最新状态码StatusCode枚举
    print(res.StatusCode);
    //4. 服务器发送的最新状态文本StatusDescription
    print(res.StatusDescription);
    //5. 建立连接时/会话结束时服务器发送的消息BannerMessage/ExitMessage
    print(res.BannerMessage);
    //6. 文件上次修改日期
    print(res.LastModified);

上传

  • 上传流程
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    try{
    FtpWebRequest r = FtpWebRequest.Create("ftp://127.0.0.1/a.txt") as FtpWebRequest;
    NetworkCredential n = new ("admin", "admin");
    r.Credentials = n;
    r.Proxy = null; //避免使用ftp的同时启动http服务
    r.Method = WebResquestMethods.Ftp.UploadFile;
    r.KeepAlive = false;
    r.UseBinary = true;
    Stream s = r.GetRequestStream();
    using(FileStream file = File.OpenRead("文件目录.txt"))
    {
    byte[] bytes = new byte[1024];
    int l = file.Read(bytes,0,bytes.Length);
    while(l != 0){
    s.Write(bytes,0,l);
    l = file.Read(bytes,0,bytes.Length);
    }
    file.Close();
    s.Close();
    /*上传完毕*/
    }
    }
    catch (Exception e)
    {
    Debug.Log(e.Message);
    }
  • 上传单例类FtpMgr
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    public partial class FtpMgr
    {
    private static FtpMgr instance = new FtpMgr();
    public static FtpMgr Instance => instance;
    private string FTP_PATH = "ftp://127.0.0.1/";
    private NetworkCredential n = new ("admin","adminpwd");

    public async void UploadFile(string fileName, string localPath, UnityAction action = null)
    {
    await Task.Run(() => {
    try{
    FtpWebRequest r = FtpWebRequest.Create(FTP_PATH + fileName) as FtpWebRequest;
    r.Credentials = n;
    r.Proxy = null; //避免使用ftp的同时启动http服务
    r.Method = WebResquestMethods.Ftp.UploadFile;
    r.KeepAlive = false;
    r.UseBinary = true;
    Stream s = r.GetRequestStream();
    using(FileStream file = File.OpenRead(localPath))
    {
    byte[] bytes = new byte[1024];
    int l = file.Read(bytes,0,bytes.Length);
    while(l != 0){
    s.Write(bytes,0,l);
    l = file.Read(bytes,0,bytes.Length);
    }
    file.Close();
    s.Close();
    /*上传完毕*/
    }
    }
    catch (Exception e)
    {
    Debug.Log(e.Message);
    }
    });
    action?.Invoke();
    }
    }

下载

  • 下载流程
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    try
    {
    FtpWebRequest r = FtpWebRequest.Create("ftp://127.0.0.1/a.txt") as FtpWebRequest;
    //这个文件必须是服务器上有的
    r.Credentials = new NetworkCredential("admin", "admin");
    r.Method = WebResquestMethods.Ftp.DownloadFile;
    r.Proxy = null;
    r.KeepAlive = false;
    r.UseBinary = true;
    FtpWebResponse res = r.GetResponse() as FtpWebResponse; /*下载请求*/
    Stream dlStream = res.GetResponseStream(); /*下载流*/
    using(FileStream file = File.Create("下载地址加文件名.txt")){
    byte[] bytes = new byte[1024];
    int l = dlStream.Read(bytes,0,bytes.Length);
    while(l != 0){
    file.Write(bytes,0,l);
    l = dlStream.Read(bytes,0,bytes.Length);
    }
    dlStream.Close();
    file.Close();
    }
    }
    catch (Exception e)
    {
    Debug.Log(e.Message);
    }
  • 下载单例类
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    public partial class FtpMgr
    {
    public async void DownloadFile(string fileName, string localPath, UnityAction action = null)
    {
    await Task.Run(() => {
    try
    {
    FtpWebRequest r = FtpWebRequest.Create(FTP_PATH + fileName) as FtpWebRequest;
    r.Credentials = n;
    r.Method = WebResquestMethods.Ftp.DownloadFile;
    r.Proxy = null;
    r.KeepAlive = false;
    r.UseBinary = true;
    FtpWebResponse res = r.GetResponse() as FtpWebResponse;
    Stream dlStream = res.GetResponseStream();
    using(FileStream file = File.Create(localPath)){
    byte[] bytes = new byte[1024];
    int l = dlStream.Read(bytes,0,bytes.Length);
    while(l != 0){
    file.Write(bytes,0,l);
    l = dlStream.Read(bytes,0,bytes.Length);
    }
    dlStream.Close();
    file.Close();
    }
    res.Close();
    }
    catch (Exception e)
    {
    Debug.Log(e.Message);
    }
    });
    action?.Invoke();
    }
    }

FTP其他操作

  • 删除文件
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    public partial class FtpMgr
    {
    public async void DeleteFile(string fileName, UnityAction<bool> action = null)
    {
    await Task.Run(() => {
    try
    {
    FtpWebRequest r = FtpWebRequest.Create(FTP_PATH + fileName) as FtpWebRequest;
    r.Credentials = n;
    r.Method = WebResquestMethods.Ftp.DeleteFile;
    r.Proxy = null;
    r.KeepAlive = false;
    r.UseBinary = true;
    FtpWebResponse res = r.GetResponse() as FtpWebResponse;
    Stream dlStream = res.GetResponseStream();
    res.Close();
    action?.Invoke(true);
    }
    catch (Exception e)
    {
    Debug.Log(e.Message);
    action?.Invoke(false);
    }
    });
    }
    }
  • 获取文件大小
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    public partial class FtpMgr
    {
    public async void GetFileSize(string fileName, UnityAction<long> action = null)
    {
    await Task.Run(() => {
    try
    {
    FtpWebRequest r = FtpWebRequest.Create(FTP_PATH + fileName) as FtpWebRequest;
    r.Credentials = n;
    r.Method = WebResquestMethods.Ftp.GetFileSize;
    r.Proxy = null;
    r.KeepAlive = false;
    r.UseBinary = true;
    FtpWebResponse res = r.GetResponse() as FtpWebResponse;
    Stream dlStream = res.GetResponseStream();
    res.Close();
    action?.Invoke(res.ContentLength);
    }
    catch (Exception e)
    {
    Debug.Log(e.Message);
    action?.Invoke(0);
    }
    });
    }
    }
  • 创建文件夹
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    public partial class FtpMgr
    {
    public async void CreateDirectory(string dirName, UnityAction<bool> action = null)
    {
    await Task.Run(() => {
    try
    {
    FtpWebRequest r = FtpWebRequest.Create(FTP_PATH + dirName) as FtpWebRequest;
    r.Credentials = n;
    r.Method = WebResquestMethods.Ftp.MakeDirectory;
    r.Proxy = null;
    r.KeepAlive = false;
    r.UseBinary = true;
    FtpWebResponse res = r.GetResponse() as FtpWebResponse;
    res.Close();
    action?.Invoke(true);
    }
    catch (Exception e)
    {
    Debug.Log(e.Message);
    action?.Invoke(false);
    }
    });
    }
    }
  • 获取文件列表
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    public partial class FtpMgr
    {
    public async void GetFileList(string dirName, UnityAction<List<string>> action = null)
    {
    await Task.Run(() => {
    try
    {
    FtpWebRequest r = FtpWebRequest.Create(FTP_PATH + dirName) as FtpWebRequest;
    r.Credentials = n;
    r.Method = WebResquestMethods.Ftp.ListDirectory;
    r.Proxy = null;
    r.KeepAlive = false;
    r.UseBinary = true;
    FtpWebResponse res = r.GetResponse() as FtpWebResponse;
    StreamReader sr = new (res.GetResponseStream());
    List<string> l = new ();
    string line = sr.ReadLine();
    while(line != null){
    l.Add(line);
    line = sr.ReadLine();
    }
    res.Close();
    action?.Invoke(l);
    }
    catch (Exception e)
    {
    Debug.Log(e.Message);
    action?.Invoke(null);
    }
    });
    }
    }

超文本传输HTTP

原理

  • HTTP: HyperText Transfer Protocol, 主要传输超文本的网络协议. 本质是TCP通信
  • http协议规定了在数据前添加元信息(metainformation)标头(header)解释数据, 包含数据类型/编码方式等
  • 请求类型(HTTP/1.1): GET,POST,HEAD,PUT,DELETE,OPTIONS,TRACE,CONNECT
    请求类型
    GET 请求获取特定资源, 如一个web页面或资源
    POST 提交数据, 如上传文件
    HEAD 请求获取特定资源, 但不返回具体内容, 可以判断有没有特定文件
  • 响应状态码: 1**, 2**, 3**, 4**, 5**
    编号 状态码
    200 OK 找到资源, 一切正常
    304 NOT MODIFIED 资源在上次请求后没有更改(缓存机制中用)
    401 UNAUTHORIZED 客户端无权访问, 通常需要账号和密码
    403 FORBIDDON 客户端未授权, 如错误的密码
    404 NOT FOUND 资源不存在
    405 METHOD NOT ALLOWED 请求方法不支持
    501 NOT IMPLEMENTED 服务器不能识别请求或未能实现请求
  • C#相关类: WebRequest,HttpWebRequest,HttpWebResponse
  • Unity相关类: UnityWebRequest,WWW,WWWFrom

搭建http服务器

  • 使用别人做好的软件搭建(推荐)(hfs)
  • 默认80端口号, 不是则显示:端口号

C#相关类

  • HttpWebRequest

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    //重要方法
    //1. Create
    HttpWebRequest r = HttpWebRequest.Create("http://baidu.com") as HttpWebRequest;
    //2. Abort 中止
    r.Abort();
    //3. GetRequestStream 获取请求流, 支持Begin/End异步
    Stream s = r.GetRequestStream();
    //4. GetResponse 返回服务器响应, 支持Begin/End异步
    HttpWebResponse res = r.GetResponse() as HttpWebResponse;

    //重要成员变量
    //1. Credentials 账号密码凭证
    //2. PreAuthenticate 是否需要身份验证
    //3. Headers 标头键值对
    //4. ContentLength 发送字节长度, Request时需要设置
    //5. ContentType POST时需要对内容设置Type
    //6. Method 操作命令: WehRequestMethods.Http.
    // Get
    // Post
    // Head
  • HttpWebResponse

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    //成员方法
    //1. Close()
    //2. GetResponseStream() 获取下载流

    //成员变量
    //1. ContentLength 接收到的数据的长度
    //2. ContentType 接收到的数据类型
    //3. StatusCode 服务器下发的最新状态码
    //4. StatusDescription 服务器下发的状态文本
    //5. BannerMessage
    //6. ExitMessage
    //7. LastModified
  • 下载数据流程

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    //检测资源可用性
    try
    {
    HttpWebRequest r = HttpWebRequest.Create("http://baidu.com/logo.png") as HttpWebRequest;
    r.Method = WebRequestMethods.Http.Head;
    r.Timeout = 2000;
    HttpWebResponse res = r.GetResponse() as HttpWebResponse;
    if(res.StatusCode == HttpStatusCode.OK){
    //资源可用
    }else{
    print(r.StatusCode + r.StatusDescription);
    }
    }
    catch (WebException e)
    {
    Debug.Log(e.Status + e.Message);
    }

    //下载资源
    try
    {
    HttpWebRequest r = HttpWebRequest.Create("http://baidu.com/logo.png") as HttpWebRequest;
    r.Method = WebRequestMethods.Http.Get;
    r.Timeout = 2000;
    HttpWebResponse res = r.GetResponse() as HttpWebResponse;
    if(res.StatusCode == HttpStatusCode.OK){
    using(FileStream fs = File.Create("Path路径.png")){
    Stream s = res.GetResponseStream();
    byte[] bytes = new byte[2048];
    int l = s.Read(bytes,0,bytes.Length);
    while(l!=0){
    s.Write(bytes,0,bytes.Length);
    l = s.Read(bytes,0,bytes.Length);
    }
    fs.Close();
    s.Close();
    }
    }else{
    print(r.StatusCode + r.StatusDescription);
    }
    }
    catch (WebException e)
    {
    Debug.Log(e.Status + e.Message);
    }

    //Get携带额外信息: 链接加?
  • 下载数据单例管理

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    public partial class HttpMgr
    {
    private static HttpMgr instance = new HttpMgr();
    public static HttpMgr Instance => instance;
    private string HTTP_PATH = "http://127.0.0.1:8080/httpserver/";

    public async void DownloadFile(string fileName, string localPath, UnityAction<HttpStatusCode> action)
    {
    HttpStatusCode result = HttpStatusCode.OK;
    await Task.Run(() => {
    try
    {
    HttpWebRequest r = HttpWebRequest.Create(HTTP_PATH + fileName) as HttpWebRequest;
    r.Method = WebRequestMethods.Http.Head;
    r.Timeout = 2000;
    HttpWebResponse res = r.GetResponse() as HttpWebResponse;
    if(res.StatusCode == HttpStatusCode.OK){
    res.Close();
    //资源可用
    r.Method = WebRequestMethods.Http.Get;
    res = r.GetResponse() as HttpWebResponse;
    if(res.StatusCode == HttpStatusCode.OK){
    using(FileStream fs = File.Create(localPath)){
    Stream s = res.GetResponseStream();
    byte[] bytes = new byte[2048];
    int l = s.Read(bytes,0,bytes.Length);
    while(l!=0){
    s.Write(bytes,0,bytes.Length);
    l = s.Read(bytes,0,bytes.Length);
    }
    fs.Close();
    s.Close();
    }
    result = HttpStatusCode.OK;
    }else{
    result = res.StatusCode;
    }
    }else{
    result = res.StatusCode;
    }
    res.Close();
    }
    catch (WebException e)
    {
    result = HttpStatusCode.InternalServerError;
    Debug.Log(e.Status + e.Message);
    }
    });
    action?.Invoke(result);
    }
    }
  • PostVSGet

    • 相同: 都可以携带参数发送数据, 并接收数据
    • 不同: 1. Post参数不可见, 更安全; 2. Get需要拼接链接参数, 但url有长度限制; 3. Get内容可被浏览器缓存, Post不会被缓存; 4. Get所有请求一次性发送, Post可能分多次发送
    • Post携带参数: 设置HttpWebRequest对象的ContentType"application/x-www-form-urlencoded"
    • ContentType类型

      文本型: text/plain, text/html, text/css, text/javascript

      图片型: image/ + gif/png/jpeg/bm/webp

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

      视频型: video/ + webm/ogg

      二进制类型: application/ + octet-stream(没有其他类型)/x-www-form-urlencoded(键值对)/xml/pdf

      复合类型: multipart/ + form-data/byteranges

  • 上传数据流程

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    //1. 实例化并设置参数
    HttpWebRequest rq = HttpWebRequest.Create("无文件名目录") as HttpWebRequest;
    rq.Method = WebRequestMethods.Http.Post;
    rq.Timeout = 30000; //根据文件大小设置
    string boundry = DataTime.Now;
    rq.ContentType = "application/form-data;boundry=" + boundry;
    rq.PreAuthenticate = true; /*是否先验证身份再上传*/
    rq.Credentials = new NetworkCredential("admin","pwd");

    //2. 设置头部和尾部信息
    //2.1 头部格式
    // --boundry字符
    // Content-Disposition:form-data;name="文件数据名";filename="上传后名"
    // Content-Type:自定义文件类型对应的Type
    // 空行
    string head = $"--{boundry}\r\n" +
    "Content-Disposition:form-data;name=\"file\";filename=\"a.exe\"\r\n" +
    "Content-Type:application/octet-stream\r\n\r\n";
    byte[] headBytes = Encoding.UTF8.GetBytes(head);
    //2.2 尾部格式
    // --boundry字符--
    byte[] endBytes = Encoding.UTF8.GetBytes($"\r\n--{boundry}--\r\n");

    //3. 文件
    using(FileStream fs = File.OpenRead("本地文件.exe"))
    {
    rq.ContentLength = headBytes.Length + fs.Length + endBytes.Length;
    Stream uploadStream = rq.GetRequestStream(); /*上传流*/
    uploadStream.Write(headBytes,0,headBytes.Length); /*头部字节*/
    byte[] fileBytes = new byte[2048]; /*文件字节*/
    int l = fs.Read(fileBytes,0,fileBytes.Length);
    while(l!=0){
    uploadStream.Write(fileBytes,0,fileBytes.Length);
    l = fs.Read(fileBytes,0,fileBytes.Length);
    }
    uploadStream.Write(endBytes,0,endBytes.Length);
    uploadStream.Close();
    fs.Close();
    }

    //4. 上传, 获得响应
    HttpWebResponse res = rq.GetResponse() as HttpWebResponse;
    if(res.StatusCode == HttpStatusCode.OK){
    //成功
    } else{
    //失败
    }
  • 上传数据单例管理

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    public partial class HttpMgr
    {
    private HttpStatusCode result = HttpStatusCode.BadRequest;
    public async void UploadFile(string fileName, string localPath, UnityAction<HttpStatusCode> action)
    {
    await Task.Run(() => {
    HttpWebRequest rq = HttpWebRequest.Create(HTTP_PATH) as HttpWebRequest;
    rq.Method = WebRequestMethods.Http.Post;
    rq.Timeout = 30000; //根据文件大小设置
    string boundry = DataTime.Now;
    rq.ContentType = "application/form-data;boundry=" + boundry;
    rq.PreAuthenticate = true;
    rq.Credentials = n;
    string head = $"--{boundry}\r\n" +
    $"Content-Disposition:form-data;name=\"file\";filename=\"{fileName}\"\r\n" +
    "Content-Type:application/octet-stream\r\n\r\n";
    byte[] headBytes = Encoding.UTF8.GetBytes(head);
    byte[] endBytes = Encoding.UTF8.GetBytes($"\r\n--{boundry}--\r\n");
    try
    {
    using(FileStream fs = File.OpenRead("本地文件.exe"))
    {
    rq.ContentLength = headBytes.Length + fs.Length + endBytes.Length;
    Stream uploadStream = rq.GetRequestStream();
    uploadStream.Write(headBytes,0,headBytes.Length);
    byte[] fileBytes = new byte[2048];
    int l = fs.Read(fileBytes,0,fileBytes.Length);
    while(l!=0){
    uploadStream.Write(fileBytes,0,fileBytes.Length);
    l = fs.Read(fileBytes,0,fileBytes.Length);
    }
    uploadStream.Write(endBytes,0,endBytes.Length);
    uploadStream.Close();
    fs.Close();
    }
    HttpWebResponse res = rq.GetResponse() as HttpWebResponse;
    result = HttpStatusCode.OK;
    res.Close();
    }
    catch (Exception e)
    {
    Debug.Log(e.Message);
    }
    });
    action?.Invoke(result);
    }
    }

Unity相关类

  • WWW

    支持协议: http(s), ftp匿名下载, file三端本地文件异步加载

    一般配合协程使用
    ; 不支持携带账户密码等信息
    过时类, 整合进UnityWebRequest中, 但仍可以使用

    • 方法
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      //构造
      WWW www = new WWW("文件地址.jpeg");

      //将下载数据转换为AudioClip/MovieTexture
      www.GetAudioClip();
      www.GetMovieTexture();

      //转换为Texture2D
      Texture2D t = new (96,96);
      www.LoadImageIntoTexture(t);
    • 变量
      • assetBundle: 直接转换为AB包资源
      • text: 直接转换为文本读取
      • bytes: 以byte[]形式加载数据
      • bytesDownloaded: 下载中时已下载的字节数
      • error: 下载中出错时返回错误信息, 如果www.error!=null则出错了
      • isDone: 下载是否已完成
      • progress: 下载进度(0-1)
    • 异步下载流程
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      StartCoroutine(Download());
      IEnumerator Download()
      {
      WWW w = new ("http/ftp/file下载直链");
      yield return w; //先return, 同时等待w加载结束再加载后面的代码
      /*
      //返回进度
      while(!w.isDOne){
      print(w.bytesDownloaded);
      print(w.progress);
      yield return null;
      }
      */
      if(w.error == null){
      //加载成功
      //直接使用w.GetXXX()或w.xxx获取对应资源
      }else{
      print(w.error);
      }
      }
    • 异步下载单例管理NetWWWMgr
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      //同样动态添加一个空GO并挂载
      public partial class NetWWWMgr:MonoBehaviour
      {
      private static NetWWWMgr instance;
      public static NetWWWMgr Instance => instance;

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

      public void LoadRes<T>(string path, UnityAction<T> action)
      where T:class
      {
      StartCoroutine(Download<T>(path,action));
      }
      IEnumerator Download<T>(string path, UnityAction<T> action)
      where T:class
      {
      WWW w = new (path);
      yield return w;
      if(w.error == null){
      switch(typeof(T))
      {
      case typeof(AssetBundle):
      action?.Invoke(w.assetBundle as T);
      break;
      case typeof(Texture):
      action?.Invoke(w.texture as T);
      break;
      case typeof(AudioClip):
      action?.Invoke(w.GetAudioClip() as T);
      break;
      case typeof(string):
      action?.Invoke(w.text as T);
      break;
      case typeof(byte[]):
      action?.Invoke(w.bytes as T);
      break;
      //其他自定义类型
      }
      }else{
      print(w.error);
      }
      }
      }
  • WWWForm

    功能: 使用http post上传数据, 需要配合后端处理获得的数据

    • 方法
      • 构造函数WWWform data = new ()
      • 添加二进制数据data.AddBinaryData(字段名,byte[],文件名,文件类型)
      • 添加字段data.AddField()
    • 上传流程
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      StartCoroutine(UpdateData());
      IEnumerator UpdateData()
      {
      WWWForm wf = new ();
      wf.AddField("Name","John",Encoding.UTF8);
      wf.AddField("Age",18);
      wf.AddBinaryData("file",FIle.ReadAllBytes("本地文件名.exe"));
      wf.AddBinaryData("file2",/*otherFIle*/,"/a.exe",
      "application/octet-stream");
      WWW w = new ("上传地址",wf);
      yield return w;
      if(w.error==null){
      //成功
      }else{
      Debug.Log(w.error);
      }
      }
    • 上传BaseMsg单例
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      public partial class NetWWWMgr:MonoBehaviour
      {
      public void SendMsg<T>(BaseMsg m, UnityAction<T> action) where T:BaseMsg
      {
      StartCoroutine(SendMsgAsync<T>(m,action));
      }

      IEnumerator SendMsgAsync<T>(BaseMsg m, UnityAction<T> action) where T:BaseMsg
      {
      WWWForm wf = new ();
      wf.AddBinaryData("Msg",m.Writing());
      WWW w = new ("上传地址",wf);
      yield return w;
      if(w.error==null){
      //成功, 解析返回的BaseMsg
      //Invoke解析的信息
      }else{
      Debug.Log(w.error);
      }
      }
      }
  • UnityWebRequest

    集成了WWW相关功能, 同样使用协程, 支持http/ftp/file, 支持下载上传

    常用操作: Get文本, Get二进制, Get纹理, Get AB包, Post数据

    • Get数据流程
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      IEnumerator LoadText()
      {
      UnityWenRequest rq = UnityWebRequest.Get("s.txt");
      yield return rq.SendWebRequest();
      if(rq.result == UnityWebRequest.Result.Success){
      print(rq.dowmloadHandler.text); //文本
      byte[] bytes = rq.dowmloadHandler.data; //二进制数据
      }else{
      Debug.Log(rq.result + rq.error + rq.responseCode);
      }
      }

      IEnumerator LoadTexture()
      {
      UnityWenRequest rq = UnityWebRequestTexture.GetTexture("纹理地址");
      yield return rq.SendWebRequest();
      if(rq.result == UnityWebRequest.Result.Success){
      go.Texture = DownloadHandlerTexture.GetContent(rq);
      //或者
      //(rq.downloadHandler as DownloadHandlerTexture).texture;
      }else{
      Debug.Log(rq.result + rq.error + rq.responseCode);
      }
      }

      IEnumerator LoadAB()
      {
      UnityWenRequest rq = UnityWebRequestAssetBundle.GetAssetBundle("地址");
      yield return rq.SendWebRequest();
      if(rq.result == UnityWebRequest.Result.Success){
      AssetBundle ab = DownloadHandlerAssetBundle.GetContent(rq);
      //或者
      //(rq.downloadHandler as DownloadHandlerAssetBundle).assetBundle;
      }else{
      Debug.Log(rq.result + rq.error + rq.responseCode);
      }
      }
    • Post数据流程
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      //所有数据都继承了IMultipartFormSection接口, 使用该接口装数据
      List<IMultipartFormSection> list = new ();
      //使用IMultipartFormDataSectino装键值对, 值可以是任何内容
      list.Add(new IMultipartFormDataSection("Name","John"));
      //使用IMultipartFormFileSectino传文件
      // 文件名,字节数组
      list.Add(new IMultipartFormFileSection("a.exe", File.ReadAllBytes("文件地址")));
      // 字符串, 文件名
      list.Add(new IMultipartFormFileSectino("hello","服务器地址.txt"));
      // 字符串, 编码格式, 文件名
      // 表单名, 字节数组, 文件名, 文件类型
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      IEnumerator Upload()
      {
      List<IMultipartFormSection> list = new ();
      //list.Add() 添加键值对
      UnityWenRequest rq = UnityWebRequest.Post("地址",list);
      rq.SendWebRequest();
      while(!rq.isDone){
      print(rq.uploadProgress);
      yield return null;
      }
      if(rq.result == UnityWebRequest.Result.Success){
      print("成功");
      }else{
      Debug.Log(rq.result + rq.error + rq.responseCode);
      }
      }
    • Post上传单例
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      public partial class NetWWWMgr:MonoBehaviour
      {
      public void UploadFile(string filename, string localPath, UnityAction action)
      {
      StartCoroutine(UploadFileAsync(filename,localPath,action));
      }
      IEnumerator UploadFileAsync(string filename, string localPath, UnityAction action)
      {
      List<IMultipartFormSection> list = new ();
      list.Add(new IMultipartFormDataSection(filename, File.ReadAllBytes(localPath));
      UnityWenRequest rq = UnityWebRequest.Post("地址",list);
      rq.SendWebRequest();
      while(!rq.isDone){
      print(rq.uploadProgress);
      yield return null;
      }
      action?.Invoke(rq.result);
      if(rq.result == UnityWebRequest.Result.Success){
      print("成功");
      }else{
      Debug.Log(rq.result + rq.error + rq.responseCode);
      }
      }
      }
    • Get自定义类型数据
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      //关键类:
      //1. DownloadHandlerBuffer 简单二进制数据
      //2. DownloadHandlerFile 下载保存文件(占用小)
      //3. DownloadHandlerTexture/AssetBundle/AudioClip 下载图片/AB包/音频
      //4. DownloadHandlerScript 可继承的类, 用于自定义数据
      IEnumerator DownloadBuffer()
      {
      UnityWebRequest r = new ("地址",UnityWebRequest.kHttpVerbGET);
      r.downloadHandler = new DownloadHandlerBuffer();/*替换不同内容*/
      yield return r.SendWebRequest();
      if(r.result == UnityWebRequest.Result.Success){
      //数据: r.downloadHandlerBuffer.data
      }else{
      Debug.Log(r.result + r.error + r.responseCode);
      }
      }

      //自定义数据类
      public class CustomDOwnloadHandler:DOwnloadHandlerScript
      {
      private string savePath;
      private byte[] cacheBytes;
      private int index = 0;

      public CustomDOwnloadHandler():base(){}
      public CustomDOwnloadHandler(byte[] bytes):base(bytes){}
      public CustomDOwnloadHandler(string path):base(){
      this.savePPth = path;
      }

      //获取数据
      protected override byte[] GetData()
      {
      return cacheBytes;
      }

      //收到数据后每帧自动调用
      protected override bool ReceiveData(byte[] data, int dataLength)
      {
      data.CopyTo(cacheBytes, index);
      index += data.Length;
      return true;
      }

      //消息收完自动调用
      protected override void CompleteContent()
      {
      //保存到本地
      File.WriteAllBytes(savePath, cacheBytes);
      //或者其他解析操作
      }

      //收到ContentLength标头时自动调用
      protected override void ReceiveContentLengthHeader(ulong contentLength)
      {
      cacheBytes = new byte[contentLength];
      }
      }
    • Post自定义数据类型
      1
      2
      3
      4
      //关键类
      //1. UploadHandlerRaw 字节数组
      //2. UploadHandlerFile 文件
      //用法同上, 实际使用较少

消息处理

分包与黏包

自定义协议工具

  • 协议(消息)生成工具

    每一种消息对应一种消息类, 在不同语言中的处理方式不同, 因而使用xml(或json)语言来定义消息类, 然后使用工具根据xml消息类统一生成不同语言对应的消息类代码

    • 编辑xml文件
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      <?xml version="1.0" encoding="UTF-8"?>
      <message>
      <!--枚举配置-->
      <enum name="E_PLAYER_TYPE" namespace="PLAYER">
      <field name="P1">2</field>
      <field name="P2"/>
      </enum>
      <!--数据结构类配置-->
      <data name="PlayerInfo" namespace="Player">
      <field type="int" name="id"/>
      <field type="string" name="name"/>
      <field type="List" T="int" name="list"/>
      </data>
      <!--消息类配置-->
      <msg id="1001" name="PlayerMsg" namespace="Player">
      <field type="PlayerInfo" name="data"/>
      </msg>
      </message>
    • 读取xml信息
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      XmlDocument xml = new ();
      xml.Load("地址文件.xml");
      //选择唯一节点
      XmlNode root = xml.SelectSingleNode("message");
      //选择一个节点下的所有节点
      XmlNodeList enumList = root.SelectNodes("enum");
      foreach(XmlNode x in enumList){
      //输出每个枚举的属性
      print(x.Attribute["name"].Value);
      print(x.Attribute["namespace"].Value);
      //输出枚举下的所有值
      XmlNodeList fields = x.SelectNodes("field");
      foreach(XmlNode f in x){
      print(f.Attribute["type"].Value + " " + f.Attribute["name"].Value);
      }
      }
    • 创建Unity菜单
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      public class ProtocolTool
      {
      [MenuItem("ProtocolTool/创建C#脚本")]
      private static void GenerateCSharp()
      {
      //内部拼接字符串
      }

      [MenuItem("ProtocolTool/创建Java脚本")]
      private static void GenerateJava()
      {}

      [MenuItem("ProtocolTool/创建C++脚本")]
      private static void GenerateCpp()
      {}
      }

第三方协议工具Protobuf

谷歌开发的生成工具

Protobuf

  • 下载开发平台的dll, 下载运行平台的编译器, 使用.proto扩展名
  • 导入dll文件
  • proto文件配置规则
    • 注释: 同C#
    • 版本号: 默认"proto2", 必须放在第一行, syntax="proto3";
    • 命名空间: package 名;
    • 消息类: message 类名{/*声明*/}
    • 成员类型和唯一编号
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      //浮点数: float double
      float f = 1;
      //整数
      // 变长: int32 int64 uint32 uint64 sint32 sint64
      int32 i = 2;
      uint32 ui = 3; //正数
      sint32 si = 4; //负数
      // 固定: fixed32 fixed64 sfixed32 sfixed64
      fixed32 fx = 5; //始终是4个字节
      //其他: bool(默认false) string bytes(字符串字节数组, 少用)
      //每个变量必须有一个唯一编号
    • 变量修饰符
      1
      2
      3
      4
      5
      //数组: repeated
      repeated int32 listInt = 6; //类似List<int>
      //字段是否必须赋值: required(不支持proto3) optional
      //字典: map
      map<int32,string> m = 7; //类似Dictionary<int,string>
    • 枚举
      1
      2
      3
      4
      5
      TestEnum testEnum = 8;
      enum TestEnum{
      First = 0; //第一个必须是0
      Second = 5;
      }
    • 自定义类对象, 同样需要编号, 默认为null
    • 更新删除变量时, 注释变量, 并保留唯一编号不准使用: reserved 2, 15 to 19; 保留变量名不准使用: reserved "testEnum"; 两者选其一就可以, 也可以同时使用
    • 导入其他proto: import "另一个路径.proto";使用时需包含命名空间
  • 生成代码: 传参运行对应平台的protoc.exe
    • 参数(不能有中文): -I=proto文件路径 --csharp_out=输出文件路径 原文件名
  • 加入Unity菜单
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    public class ProtobufTool
    {
    private static string PROTO_PATH = "proto文件目录";
    private static string CS_PATH = "proto文件输出CS目录";
    private static string CPP_PATH = "proto文件输出C++目录";
    private static string JAVA_PATH = "proto文件输出Java目录";
    private static string PROTOC_PATH = "编译器路径";

    [MenuItem("ProtobufTool/生成C#代码")]
    private static void GenerateCS()
    {
    Generate("cssharp_out",CS_PATH);
    }

    [MenuItem("ProtobufTool/生成C++代码")]
    private static void GenerateCPP()
    {
    Generate("cpp_out",CPP_PATH);
    }

    [MenuItem("ProtobufTool/生成Java代码")]
    private static void GenerateJAVA()
    {
    Generate("java_out",JAVA_PATH);
    }

    private static void Generate(string outCmd, string outPath)
    {
    DirectoryInfo di = Directory.CreateDirectory(PROTO_PATH);
    FileInfo[] files = di.GetFiles();
    foreach(FileInfo f in files){
    if(f.Extension==".proto"){
    Process cmd = new ();
    cmd.StartInfo.FileName = PROTO_PATH;
    cmd.StartInfo.Arguments =
    $"-I={PROTO_PATH} --{outCmd}={outPath} {f}";
    cmd.Start();
    Debug.Log(f + " Completed.");
    }
    }
    Debug.Log("All Completed.");
    }
    }
  • 序列化与反序列化
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    //引入Protobuf命名空间
    //以message Msg{}为例
    Msg msg = new ();
    /*填充成员变量*/

    //序列化为本地文件: Protobuf提供基类成员方法WriteTo
    using(FileStream fs = File.Create(Application.persistentDataPath + "/msg.Msg")){
    msg.WriteTo(fs);
    }

    //序列化为字节数组: WriteTo到MemoryStream中再ToArray()
    byte[] bytes = null;
    using(MemoryStream ms = new ()){
    msg.WriteTo(ms);
    bytes = ms.ToArray();
    }

    //反序列化到内存中: Protobuf提供基类方法Parser.ParseFrom(字节数组或流)
    Msg rcvMsg = null;
    using(FileStream fs = File.Read(Application.persistentDataPath + "/msg.Msg")){
    rcvMsg = Msg.Parser.ParseFrom(fs);
    }

    //从字节数组反序列化
    Msg rcvMsg2 = null;
    using(MemoryStream ms = new (bytes)){
    rcvMsg2 = Msg.Parser.ParseFrom(ms);
    }
    • 静态工具类
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      public static class NetTool
      {
      public static byte[] GetProtoBytes(IMessage msg)
      /*工具将所有写的类继承了IMessage*/
      {
      return msg.ToByteArray();
      }

      public static T GetProtoMsg<T>(byte[] bytes) where T:class, IMessage
      {
      //反射
      Type type = typeof(T);
      PropertyInfo pi = type.GetProperty("Parser");
      object parserobj = pi.GetValue(null,null);
      Type parserType = parserobj.GetType();
      MethodInfo mi = parserType.GetMethod("ParseFrom",new Type[] {typeof(byte[])});
      object msg = mi.Invoke(parserobj,new object[] {bytes});
      return msg as T;
      }
      }

      //外部使用
      Msg msg2;
      // 把一个msg2序列化
      byte[] bytes2 = NetTool.GetProtoBytes(msg2);
      // 把一个byte[]反序列化为Msg
      Msg msg3 = NetTool.GetProtoMsg<Msg>(bytes2);

Protobuf-Net

早期的Protobuf不支持C#, 第三方Protobuf-Net添加了对C#的支持. Protobuf不支持.Net3.5及以下版本, 对于较旧的Unity只能使用Protobuf-Net, 新版的Unity可以使用Protobuf

大小端模式/大小端字节序

  • 概念
    • 大端模式: 数据的高字节保存在低地址中, 符合人的阅读习惯
    • 小端模式: 数据的高字节保存在高地址中
  • 产生背景: 计算机系统处理都是小端模式, 人创造大端模式便于阅读
  • 影响: 不同系统不同平台不同语言采用的模式可能不同, 一个数在另一个不同模式的环境中数据不同, 在接收处理数据时必须考虑不同环境的处理
    • C#C++为小端模式, Java/Erlang/AS3是大端模式, 两方之间通信需要进行大小端转换
  • 大小端模式转换
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    //short/int/long
    // 1. 判断是否小端模式
    print(BitConverter.IsLittleEndian); //C#和Unity中显示为True

    // 2. 转网络字节序(大端模式)
    int i = 99;
    byte[] bytes = BitConverter.GetBytes(i); //小端模式的99的byte[]
    byte[] bytes2 = BitConverter.GetBytes(IPAddress.HostToNetworkOrder(i));
    /*大端模式的99的byte[]*/

    // 3. 网络字节序转本机字节序(大→小)
    int i1 = BitConverter.ToInt32(bytes,0); //直接转
    int i2 = IPAddress.NetworkToHostOrder(BitConverter.ToInt32(bytes2,0));
    /*大转小*/

    //通用转换: 反转位置
    if(BitConverter.IsLittleEndian){Array.Reverse(bytes);}
    /*或*/
    if(!BitConverter.IsLittleEndian){Array.Reverse(bytes);}

消息加密与解密

  • 单向加密(不可逆): 将数据计算为另一种固定长度的值
    • 案例: MD5, SHA1, SHA256
    • 用途: 网络传输不用, 一般用于为密码加密传输
  • 对称加密: 密钥加密明文, 解密密文
    • 案例: DES, 3DES, IDEA, AES
    • 优点: 计算量小, 加密速度快
    • 缺点: 知道了密钥和算法可以破解
    • 用途: 消息传输, 密钥由服务器生成下发, 每次建立通讯都变化
  • 非对称加密/公开密钥加密: 分为公钥和私钥, 两者之间不能计算出另一个, 其中一个加密, 只能用另一个解密
    • 案例: RSA, DSA
    • 优点: 安全性高
    • 缺点: 算法复杂, 加密速度慢
    • 用途: 安全性要求较高, 接收速度慢的场景, 如支付SDK