UN2L8——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
//Begin开头的API:内部开多线程,通过回调形式返回结果,需要和End相关方法配合使用
IAsyncResult //所有Begin开头的API都会返回它,也会作为Begin开头的API传入的回调函数的参数
iAsyncResult.AsyncState //获取Begin开头的API传入的 作为回调方法的参数 的值
iAsyncResult.AsyncWaitHandle //设置Begin开头的API返回的IAsyncResult的AsyncWaitHandle属性,可以让执行异步方法的线程卡住一段时间
socket.BeginAccept() //异步监听客户端连入方法,传入回调函数和回调函数的参数
socket.EndAccept() //结束异步监听客户端连入方法,传入回调函数的参数,返回连接客户端的Socket
socket.BeginConnect() //异步连接服务器方法,传入回调函数和回调函数的参数
socket.EndConnect() //结束异步连接服务器方法,传入回调函数的参数
socket.BeginReceive() //异步接收消息方法,传入字节数组,偏移量,最大接收字节数,标识,回调函数和回调函数的参数
socket.EndReceive() //结束异步接收消息方法,传入回调函数的参数,返回接收了多少字节
socket.BeginSend() //异步发送消息方法,传入字节数组,偏移量,最大发送字节数,标识,回调函数和回调函数的参数
socket.EndSend() //结束异步发送消息方法,传入回调函数的参数,返回发送了多少字节
//Async结尾的API:内部开多线程,通过回调形式返回结果,依赖SocketAsyncEventArgs对象配合使用,可以让我们更加方便的进行操作
SocketAsyncEventArgs //传入Async结尾的异步方法的参数类型,以及回调方法内会传入的参数类型
socketAsyncEventArgs.Completed //当异步方法执行完毕时,会执行这里设置的回调方法
socketAsyncEventArgs.SocketError //获取异步方法执行结果,可用于判断是否执行成功
socketAsyncEventArgs.AcceptSocket //当异步监听客户端连入方法执行完毕时,可以通过该方法来获取连接客户端的Socket
socketAsyncEventArgs.SetBuffer() //设置用于接收/发送的字节数组,并设置接收/发送的起始位置和最多接收/发送多少字节
socketAsyncEventArgs.Buffer //获取设置的接收/发送的字节数组
socketAsyncEventArgs.BytesTransferred //获取字节数组内接收/发送了多少字节
socketAsyncEventArgs.RemoteEndPoint //异步连接服务器前,设置IP地址和端口号的属性
socket.AcceptAsync() //异步监听客户端连入方法
socket.ConnectAsync() //异步连接服务器方法
socket.SendAsync() //异步发送消息方法
socket.ReceiveAsync() //异步接收消息方法

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

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

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

注意:
Unity中的协同程序中的某些异步方法,有的使用的是多线程、有的使用的是迭代器分步执行
关于协同程序可以回顾Unity基础当中讲解协同程序原理的知识点

举例说明异步方法原理

我们以一个异步倒计时方法举例

  1. 线程回调

    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
    void Start()
    {
    CountDownAsync(5, () =>
    {
    print("倒计时结束");
    });
    print("异步执行后的逻辑");
    }

    //倒计时方法
    public void CountDownAsync(int second, UnityAction callBack)
    {
    Thread t = new Thread(() =>
    {
    while (true)
    {
    print(second);
    Thread.Sleep(1000);
    --second;
    if (second <= 0)
    break;
    }
    callBack?.Invoke();
    });
    t.Start();
    print("开始倒计时");
    }

    image

  2. async​ 和 await​ 会等待线程执行完毕,继续执行后面的逻辑,相对第一种方式,它可以让函数分步执行

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    void Start()
    {
    CountDownAsync(5);
    print("异步执行后的逻辑2");
    }

    public async void CountDownAsync(int second)
    {
    print("倒计时开始");
    await Task.Run(() =>
    {
    while (true)
    {
    print(second);
    Thread.Sleep(1000);
    --second;
    if (second <= 0)
    break;
    }
    });
    print("倒计时结束");
    }

    image

SocketTCP通信中的异步方法

C#中网络通信异步方法中,主要提供了两种方案

  1. Begin​开头的API

    内部开多线程,通过回调形式返回结果,需要和End​​相关方法配合使用

    • 回调函数参数,以及Begin​开头的API返回值:IAsyncResult

      以下所有Begin​开头的API执行后都会返回该接口参数,传入该Begin​开头的API的回调方法也会传入该接口参数

      • AsyncState​​:获取调用异步方法时传入的参数,需要转换
      • AsyncWaitHandle​:用于同步等待,通过设置Begin​方法返回的iAsyncResult​的AsyncWaitHandle​属性,可以卡住主线程一段时间
    • 服务器端相关 —— 监听连入Begin​开头异步方法

      BeginAccept​​方法有多个重载,一般使用两个参数的重载,
      参数一:带IAsyncResult​​参数的回调方法,参数二:传入到参数一方法的内容,一般会把Socket​​自己传进去

      在异步监听到连入后,就会执行参数一的回调方法,并将参数二的内容传入到回调方法内,
      在回调方法内获取BeginAccept​​参数二的内容,可以通过iAsyncResult.AsyncState​​获取,需要自行as​​成原来的类型
      回调方法内需要执行EndAccept​​来获取连入的客户端Socket​​,需要传入委托方法的参数

      可以在回调方法内再次执行BeginAccept​​进行下一次的监听,
      因为是执行的异步方法,回调方法会继续执行,所以不构成递归,不需要担心爆栈

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      void Start()
      {
      Socket socketTcp = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
      socketTcp.BeginAccept(AcceptCallBack, socketTcp);
      }

      private void AcceptCallBack(IAsyncResult result)
      {
      try
      {
      //获取传入的参数
      Socket s = result.AsyncState as Socket;
      //通过调用EndAccept就可以得到连入的客户端Socket
      Socket clientSocket = s.EndAccept(result);
      //继续下一次的监听,注意,它不是递归,因为在执行了异步方法后,此方法会继续执行
      s.BeginAccept(AcceptCallBack, clientSocket);
      }
      catch (SocketException e)
      {
      print(e.SocketErrorCode);
      }
      }
    • 客户端相关 —— 连接服务器Begin​开头异步方法

      使用方法与服务端的BeginAccept​​,但是多了一个参数,同时一般也不需要连续多次执行

      • 参数一:IP地址和端口号,
      • 参数二:带IAsyncResult​​参数的回调方法
      • 参数三:传入到参数一方法的内容,一般会把Socket​​自己传进去

      在异步监听到连入后,就会执行参数一的回调方法,并将参数二的内容传入到回调方法内,
      在回调方法内获取参数二的内容,可以通过iAsyncResult.AsyncState​​获取,需要自行as​​成原来的类型
      回调方法内需要执行EndConnect​​,需要传入回调方法的参数

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      Socket socketTcp = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
      IPEndPoint ipPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 8080);
      socketTcp.BeginConnect(ipPoint, (result) =>
      {
      Socket s = result.AsyncState as Socket;
      try
      {
      s.EndConnect(result);
      print("连接成功"); //不需要再次连接
      }
      catch (SocketException e)
      {
      print("连接出错:" + e.ErrorCode);
      //还可以在这里进行断线重连的操作
      }
      }, socketTcp);
    • 服务器和客户端通用

      • TCP接收消息Begin​开头异步方法

        开始异步接收消息使用socket.BeginReceive()​​

        • 参数一:用于接收消息的字节数组
        • 参数二:偏移量,相当于从接收消息字节数组的第几位开始接收,处理分包、黏包可以就利用该参数
        • 参数三:接收消息字节数组还能接收多少字节
        • 参数四:SocketFlag​​枚举,也就是标识,一般传入空标识SocketFlag.None​​即可
        • 参数五:回调函数,参数为IAsyncResult​​
        • 参数六:传入到回调函数内的参数,一般传入socket​​自己,在回调函数内通过iAsyncResult.AsyncState​​获取

        在接收到消息后,会执行参数五的回调函数,并将参数六传入进去,可通过iAsyncResult.AsyncState​获取
        需要通过socket.EndReceive()​获取接收到了多少字节,需要传入回调函数的参数,
        在回调函数内就可以处理消息,执行socket.BeginReceive()​开始下一次消息监听

        同样的,因为socket.BeginReceive()​是异步方法,回调方法会继续执行,所以不构成递归,不需要担心爆栈(前提是正常运行)

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        void Start()
        {
        Socket socketTcp = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        //假设这里连接了另外一台设备
        socketTcp.BeginReceive(resultBytes, 0, resultBytes.Length, SocketFlags.None, ReceiveCallBack, socketTcp);
        }

        private void ReceiveCallBack(IAsyncResult result)
        {
        try
        {
        Socket socket = result.AsyncState as Socket;
        //返回值是接收到了多少字节
        int receiveNum = socket.EndReceive(result);
        //进行消息处理
        print(Encoding.UTF8.GetString(resultBytes, 0, receiveNum));
        //如果还要继续接收
        socket.BeginReceive(resultBytes, 0, resultBytes.Length, SocketFlags.None, ReceiveCallBack, socket);
        }
        catch (SocketException e)
        {
        print("接收消息出错:" + e.ErrorCode + e.Message);
        }
        }
      • TCP发送消息Begin​开头异步方法

        开始异步发送消息使用socket.BeginSend()

        • 参数一:要发送的字节数组
        • 参数二:偏移量,从字节数组的第几位开始发送,将一个消息分开发送时可以使用
        • 参数三:发送消息字节数组最多发送出去多少字节,将一个消息分开发送时可以使用
        • 参数四:SocketFlag​枚举,也就是标识,一般传入空标识SocketFlag.None​即可
        • 参数五:回调函数,参数为IAsyncResult
        • 参数六:传入到回调函数内的参数,一般传入socket​自己,在回调函数内通过iAsyncResult.AsyncState​获取

        在发送了消息后,会执行参数五的回调函数,并将参数六传入进去,可通过iAsyncResult.AsyncState​获取
        通过socket.EndSend()​可以获取发送出去了多少字节,需要传入回调函数的参数
        一般不需要获取发送出去了多少字节,除非要将一个消息分批发送

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        void Start()
        {
        Socket socketTcp = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        byte[] bytes = Encoding.UTF8.GetBytes("123123123123123");
        socketTcp.BeginSend(bytes, 0, bytes.Length, SocketFlags.None, (result) =>
        {
        try
        {
        int sendNum = socketTcp.EndSend(result);
        print("发送成功");
        }
        catch (SocketException e)
        {
        print("发送错误:" + e.SocketErrorCode + e.Message);
        }
        }, socketTcp);
        }
  2. Async​结尾的API

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

    • 关键变量类型:SocketAsyncEventArgs

      它会作为Async​异步方法的传入值,我们需要通过它进行一些关键参数的赋值,同时频繁通过它的属性获取需要的内容
      设置好SocketAsyncEventArgs​之后,调用Async​结尾的API就可以直接传入它而无需做其他操作

      • Completed​​:在传入到Async​​异步方法前先添加回调函数,在异步方法执行完毕后就会执行这里的回调函数,

        Async​​异步方法结束后的处理逻辑主要就在回调函数里执行

        该事件使用的是EventHandler<SocketAsyncEventArgs>​​委托,
        因此回调函数的参数列表就是一个object​​类型参数和一个SocketAsyncEventArgs​​类型参数,
        其中object​​就是执行异步方法的Socket​​,SocketAsyncEventArgs​​就执行异步方法时传入的参数、

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        //监听客户端连入部分代码的例子
        SocketAsyncEventArgs AcceptAsyncArgs = new SocketAsyncEventArgs();
        AcceptAsyncArgs.Completed += (socket, args) =>
        {
        if (args.SocketError == SocketError.Success)
        {
        Socket clientSocket = args.AcceptSocket;
        (socket as Socket).AcceptAsync(args);
        }
        else
        {
        print("连入客户端失败:" + args.SocketError);
        }
        };
      • SocketError​​:获取报错号,可用于判断Async​​异步方法是否执行成功,若等于SocketError.Success​​就是执行成功

        1
        2
        3
        4
        if (args.SocketError == SocketError.Success)
        print("连接成功");
        else
        print("连接服务器失败:" + args.SocketError);
      • AcceptSocket​​:监听连入Async​​结尾异步方法执行完毕后,通过该方法获取连接客户端的Socket

        1
        Socket clientSocket = args.AcceptSocket;
      • SetBuffer()​​:设置发送/接收消息的字节数组,同时设置发送/接收数据的起始位置,以及字节数组最大发送/接收的字节数

        如果已经设置了发送/接收消息的字节数组,那么可以只设置发送/接收数据的起始位置,以及字节数组最大发送/接收的字节数
        其中设置发送/接收数据的起始位置,以及字节数组最大发送/接收的字节数在接收消息时可以用来处理分包黏包

        1
        2
        3
        4
        SocketAsyncEventArgs receiveAsyncArgs = new SocketAsyncEventArgs();
        byte[] receiveBytes = new byte[1024 * 1024];
        receiveAsyncArgs.SetBuffer(receiveBytes, 0, receiveBytes.Length);
        receiveAsyncArgs.SetBuffer(0, args.Buffer.Length);
      • Buffer​​:获取发送/接收消息的字节数组的属性

        1
        2
        //反序列化接收到的字节数组
        Encoding.UTF8.GetString(args.Buffer, 0, args.BytesTransferred);
      • BytesTransferred​​:获取字节数组发送/接收了多少字节数

        1
        2
        //反序列化接收到的字节数组
        Encoding.UTF8.GetString(args.Buffer, 0, args.BytesTransferred);
      • RemoteEndPoint​:异步连接服务器前,设置服务器的IP地址和端口号

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        SocketAsyncEventArgs ConnectAsyncArgs = new SocketAsyncEventArgs();
        ConnectAsyncArgs.RemoteEndPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1", 8080)
        ConnectAsyncArgs.Completed += (socket, args) =>
        {
        if (args.SocketError == SocketError.Success)
        {
        print("连接成功");
        }
        else
        {
        print("连接服务器失败:" + args.SocketError);
        }
        };
    • 服务器端相关 —— 监听连入Async​结尾异步方法

      首先实例化一个SocketAsyncEventArgs​​,然后设置完成异步方法后执行的回调函数
      回调函数有两个参数,一个是执行异步方法的Socket​​(object​​类型),一个是传入到异步方法的SocketAsyncEventArgs​​

      在回调函数中,需要通过socketAsyncEventArgs.SocketError​来判断客户端连入是否成功
      如果成功,就可以通过socketAsyncEventArgs.AcceptSocket​来获取连接客户端的Socket

      同样的,可以通过传入的Socket​执行AcceptAsync​来进行下一次的监听方法

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      SocketAsyncEventArgs AcceptAsyncArgs = new SocketAsyncEventArgs();
      AcceptAsyncArgs.Completed += (socket, args) =>
      {
      if (args.SocketError == SocketError.Success)
      {
      Socket clientSocket = args.AcceptSocket;
      (socket as Socket).AcceptAsync(args);
      }
      else
      {
      print("连入客户端失败:" + args.SocketError);
      }
      };
      socketTcp.AcceptAsync(AcceptAsyncArgs);
    • 客户端相关 —— 连接服务器异步Async​结尾方法

      首先实例化一个SocketAsyncEventArgs​,然后设置完成异步方法后执行的回调函数
      回调函数有两个参数,一个是执行异步方法的Socket​(object​类型),一个是传入到异步方法的SocketAsyncEventArgs
      然后,需要通过socketAsyncEventArgs.RemoteEndPoint​来设置要连接的服务器

      在回调函数中,需要通过socketAsyncEventArgs.SocketError​来判断连接服务器是否成功

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      SocketAsyncEventArgs ConnectAsyncArgs = new SocketAsyncEventArgs();
      ConnectAsyncArgs.RemoteEndPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1", 8080)
      ConnectAsyncArgs.Completed += (socket, args) =>
      {
      if (args.SocketError == SocketError.Success)
      {
      print("连接成功");
      }
      else
      {
      print("连接服务器失败:" + args.SocketError);
      }
      };
      socketTcp.ConnectAsync(ConnectAsyncArgs);
    • 服务器和客户端通用

      • TCP发送消息异步Async​结尾方法

        首先实例化一个SocketAsyncEventArgs​,然后设置完成异步方法后执行的回调函数
        回调函数有两个参数,一个是执行异步方法的Socket​(object​类型),一个是传入到异步方法的SocketAsyncEventArgs
        然后,需要通过socketAsyncEventArgs.SetBuffer()​来设置要发送出去的字节数组

        在回调函数中,需要通过socketAsyncEventArgs.SocketError​来判断消息发送是否成功

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        SocketAsyncEventArgs SendAsyncArgs = new SocketAsyncEventArgs();
        byte[] bytes2 = Encoding.UTF8.GetBytes("abcabcabcabc");
        SendAsyncArgs.SetBuffer(bytes2, 0, bytes2.Length);
        SendAsyncArgs.Completed += (socket, args) =>
        {
        if (args.SocketError == SocketError.Success)
        {
        print("发送成功");
        }
        else
        {
        print("发送失败:" + args.SocketError);
        }
        };
        socketTcp.SendAsync(SendAsyncArgs);
      • TCP接收消息异步Async​结尾方法

        首先实例化一个SocketAsyncEventArgs​,然后设置完成异步方法后执行的回调函数
        回调函数有两个参数,一个是执行异步方法的Socket​(object​类型),一个是传入到异步方法的SocketAsyncEventArgs
        然后,需要通过socketAsyncEventArgs.SetBuffer()​来设置要接收消息的字节数组

        在回调函数中,需要通过socketAsyncEventArgs.SocketError​来判消息接收是否成功
        通过socketAsyncEventArgs.Buffer​来获取接收到消息的字节数组
        通过socketAsyncEventArgs.BytesTransferred​来获取接收到了多少字节数据

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        SocketAsyncEventArgs receiveAsyncArgs = new SocketAsyncEventArgs();
        byte[] receiveBytes = new byte[1024 * 1024];
        receiveAsyncArgs.SetBuffer(receiveBytes, 0, receiveBytes.Length);
        receiveAsyncArgs.Completed += (socket, args) =>
        {
        if (args.SocketError == SocketError.Success)
        {
        Encoding.UTF8.GetString(args.Buffer, 0, args.BytesTransferred);
        args.SetBuffer(0, args.Buffer.Length);
        //接受完消息后在接收下一条
        (socket as Socket).ReceiveAsync(args);
        }
        else
        {
        print("接收失败:" + args.SocketError);
        }
        };
        socketTcp.ReceiveAsync(receiveAsyncArgs);