UN2L8——TCP异步通信常用方法
UN2L8——TCP异步通信常用方法
本章代码关键字
1 | //Begin开头的API:内部开多线程,通过回调形式返回结果,需要和End相关方法配合使用 |
异步方法和同步方法的区别
- 同步方法:方法中逻辑执行完毕后,再继续执行后面的方法
- 异步方法:方法中逻辑可能还没有执行完毕,就继续执行后面的内容
异步方法的本质,往往异步方法当中都会使用多线程执行某部分逻辑
这样我们就不需要等待方法中逻辑执行完毕就可以继续执行下面的逻辑了
注意:
Unity中的协同程序中的某些异步方法,有的使用的是多线程、有的使用的是迭代器分步执行
关于协同程序可以回顾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
27void 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("开始倒计时");
}
-
async
和await
会等待线程执行完毕,继续执行后面的逻辑,相对第一种方式,它可以让函数分步执行1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22void 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("倒计时结束");
}
SocketTCP通信中的异步方法
C#中网络通信异步方法中,主要提供了两种方案
-
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
22void 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
16Socket 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
24void 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
17void 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);
}
-
-
-
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
4if (args.SocketError == SocketError.Success)
print("连接成功");
else
print("连接服务器失败:" + args.SocketError); -
AcceptSocket
:监听连入Async
结尾异步方法执行完毕后,通过该方法获取连接客户端的Socket1
Socket clientSocket = args.AcceptSocket;
-
SetBuffer()
:设置发送/接收消息的字节数组,同时设置发送/接收数据的起始位置,以及字节数组最大发送/接收的字节数如果已经设置了发送/接收消息的字节数组,那么可以只设置发送/接收数据的起始位置,以及字节数组最大发送/接收的字节数
其中设置发送/接收数据的起始位置,以及字节数组最大发送/接收的字节数在接收消息时可以用来处理分包黏包1
2
3
4SocketAsyncEventArgs 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
13SocketAsyncEventArgs 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
14SocketAsyncEventArgs 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
14SocketAsyncEventArgs 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
15SocketAsyncEventArgs 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
18SocketAsyncEventArgs 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);
-
-