本文记录了本人在C# TCP异步通信的学习中遇到的难以理解的代码API和参数等,例如:BeginAccept、IAsyncResult 、ConnectAsync、SocketAsyncEventArgs等
本文将从对异步方法、回调的说明出发,对C# TCP通信的基本逻辑和常用API进行详细解释
本文中的服务器代码写在Unity脚本中,因此会有部分特别的用法,例如部分函数/方法不需要添加static也能直接调用
同步方法和异步方法的区别:
同步方法:调用后会阻塞当前线程,直到操作完成,程序才会继续执行后续代码。适用于简单任务,但在网络通信中可能导致程序卡顿。
异步方法:调用后立即返回,操作在后台进行,完成后通过回调函数或事件通知结果。适用于需要高并发或保持界面流畅的场景。
在上述对异步方法的解释中,提到了"回调函数或事件"这一机制,下面将对这一机制进行解释
public void CountDownAsync(int num,Action Callback)
{
print("倒计时开始"); //该代码和t.Start()同步执行
Thread t = new Thread(() =>
{
while (num >= 0)
{
print(num);
num--;
//休眠1s
Thread.Sleep(1000);
}
//当循环结束时调用委托
Callback?.Invoke();
});
t.Start();
}
通过对上述的代码分析可以观察到,当在主线程中执行CountDownAsync方法时,立即执行打印"倒计时开始",同时开启t线程,而在倒计时结束后(num<0),才会执行callBack委托,这便是异步方法中回调事件或函数的原理。
C# TCP通信中常用的异步方法:
一、以Begin开头的异步方法
Begin开头的异步方法都是和End方法配合使用的,例如BeginAccept和EndAccept、BeginConnect和EndConnect等等
其中引入了两个关键参数AsyncCallback和IAsyncResult,将在后面结合例子进行解释
服务器相关方法:
BeginAccpet和EndAccept方法:
代码示例以及参数详解:
//创建服务器Socket
Socket serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
//Bind和Listen在主线程中进行
//异步线程只负责接受连接
//启动异步非阻塞式监听 该lambda表达式只会接受一个客户端
serverSocket.BeginAccept((result) => //result 是异步操作完成时系统自动传入的 IAsyncResult 对象,封装了异步状态的信息,不需要我们手动传入参数
{
//获取调用 BeginAccept 时传入的服务器 Socket 对象
Socket serverSocket = result.AsyncState as Socket; //result.AsyncState 是 BeginAccept 调用时传入的对象(该方法的第二个参数 此处为 serverSocket)
//获取建立了连接的Socket
Socket connectedSocket = serverSocket.EndAccept(result);
},serverSocket); //将 serverSocket作为result.AsyncState传入,以便在回调中使用
// BeginAccept 参数详解
// BeginAccept(AsyncCallback callback, IAsyncResult result);
// 参数1:当有客户端连接时自动调用这个回调方法
// 参数2:传递一个对象到回调方法中,常用于传递服务端 socket 对象等上下文信息
// 注意:BeginAccept传入的第二个参数serverSocket本质上是被当作IAsyncResult.AsyncState传入的,并不是IAsyncResult本身
上述代码中在BeginAccpet方法中使用了lambda表达式来表示回调函数逻辑,可以将该回调函数封装起来重复使用,以用来不停接受多个客户端的连接
serverSocket.BeginAccept(AcceptCallback, serverSocket);//不断接受新的客户端连接
private void AcceptCallback(IAsyncResult result)
{
Socket serverSocket = result.AsyncState as Socket;//获取BeginAccept方法的第二个参数并将其转换为Socket
//连入一个客户端
Socket connectedSocket = serverSocket.EndAccept(result);
//这里执行对connectedSocket的处理逻辑
//继续监听其他客户端 注意:不是递归,而是当前异步方法执行完后,继续执行新的异步方法(接受新的客户端连接)
serverSocket.BeginAccept(AcceptCallback, serverSocket);
}
客户端相关方法:
BeginConnect和EndConnect方法
代码示例以及参数详解:
//创建客户端Socket
Socket clientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
//创建IPEndPoint记录需要连接的IP和端口号
IPEndPoint ipEndPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 8080);
//建立连接
clientSocket.BeginConnect(ipEndPoint, (result) => //此处的result的参数类型也是IAsyncResult,由系统执行后自动填充,不需要手动传入
{
clientSocket.EndConnect(result); //这里需要传入获取到的result,系统才知道到底是哪个Socket建立了连接,我将在下方做出解释
print("连接成功");
},clientSocket);
// clientSocket 是通过 stateObject 参数传入的,会被保存在 IAsyncResult.AsyncState 中;
// 而 result 是一个异步操作的句柄(IAsyncResult 实例),里面包含了此次连接过程的所有上下文信息;
// EndConnect(result) 就是利用这个异步上下文来结束这次连接操作。
形象点的解释就像:
你(clientSocket)去图书馆借书(BeginConnect),图书馆给了你一张借书凭证(IAsyncResult result),你把自己的名字写在凭证背后(AsyncState = clientSocket)。当你来还书时(EndConnect),图书馆看借书凭证(result)来确认是哪本书、由谁借走的。
服务器和客户端通用方法:
BeginReceive和EndReceive方法
因为本文中之前的内容已经对代码中的关键参数进行了解释,所以接下来的代码会减少注释
代码示例以及参数详解:
//创建一个socket作为客户端或服务器的socket示例
Socket tcpSocket = new Socket(AddressFamily.InterNetwork,SocketType.Stream,ProtocolType.Tcp);
//接收/发送消息使用的字节数组
byte[] buffer = new byte[1024];
//字节数组中存储位置的偏移量
int offset = 0;
tcpSocket.BeginReceive(buffer, offset, buffer.Length, SocketFlags.None, (result) =>
{
//返回值:实际上收到多少个字节
int num = tcpSocket.EndReceive(result);
//这里对消息进行逻辑处理
}, tcpSocket);
//参数详解:
//参数1:接收消息使用的字节数组(将消息接收到哪个数组中)
//参数2:从字节数组的哪个位置开始存储数据(常用为 0)
//参数3:接收多少字节(通常为数组的长度)
//参数4:SocketFlags,通常填 None 表示不使用特殊标志
//参数5:回调函数,接收完成后系统会调用此方法处理结果
//参数6:用于传递状态对象,可在回调中通过 result.AsyncState 获取,一般传当前Socket
上述的回调函数也能够封装为方法,用来不停接收消息
//不断接收消息
tcpSocket.BeginReceive(buffer, offset, buffer.Length, SocketFlags.None,ReceiveCallBack,tcpSocket);
public void ReceiveCallBack(IAsyncResult result)
{
Socket tcpSocket = result.AsyncState as Socket; //是哪个socket得到信息
int num = tcpSocket.EndReceive(result);
//再次调用BeginReceive,开启新的接收消息异步方法
tcpSocket.BeginReceive(buffer, offset, buffer.Length, SocketFlags.None,ReceiveCallBack,tcpSocket);
}
BeginSend和EndSend方法
代码示例以及参数详解:
tcpSocket.BeginSend(buffer,offset,bytes.Length,SocketFlags.None, (result) =>
{
tcpSocket.EndSend(result);
print("发送成功");
},tcpSocket);
//参数详解:
//参数1:要发送的数据的字节数组(消息内容)
//参数2:从字节数组的哪个位置开始发送(常为 0)
//参数3:要发送的数据长度
//参数4:SocketFlags,通常填 None 表示不使用特殊标志
//参数5:发送完成后回调的函数,常用于确认是否发送成功
//参数6:传递的状态对象,可在回调中通过 result.AsyncState 获取,一般传当前 Socket
二、以Async结尾的异步方法
以Async结尾的异步方法与对应的Begin开头的方法实现效果相同,在开发过程中二选其一进行使用即可
其中引入了一个关键参数SocketAsyncEventArgs,将在后面结合例子进行解释
服务器相关方法:
AcceptAsync方法
//用于异步 Socket 操作的参数对象,类似于封装请求上下文的容器
SocketAsyncEventArgs eventArgs = new SocketAsyncEventArgs();
//为当前的 eventArgs 绑定一个回调事件,当 AcceptAsync 异步完成后会调用这个回调。
//注意:回调事件可以不设置,只是无法观察到逻辑是否正确运行等
//这里lambda表达式中传入的socket对应tcpSocket,args对应eventArgs
eventArgs.Completed += (socket,args) =>
{
//args中存储了异步方法的执行信息
//这里是使用args查看socket的连接信息
if (args.SocketError == SocketError.Success) //成功连接
{
//获取成功连接的Socket
Socket connectedSocket = args.AcceptSocket;
//这里执行对已连接的Socket的处理逻辑
//继续接受新的客户端连接
(socket as Socket).AcceptAsync(args);
}
else
{
print("连入客户端失败");
}
};
//参数详解:
//参数1:事件触发的对象 这里为Socket对象
//参数2:带有连接信息的SocketAsyncEventArgs
//它们的值由事件机制自动填充 是系统传递回来的参数
//这里才是实际调用AccpetAsync方法
tcpSocket.AcceptAsync(eventArgs);
对上述代码中的逻辑进行形象地解释就像:
一家餐厅(tcpSocket)安排了一个服务员(eventArgs)专门接待顾客(客户端连接)。每当有顾客上门,服务员就接待他(Completed),然后立刻准备接待下一个人(再次调用AcceptAsync)。整个过程异步进行,不阻塞主线程,服务员很勤快,餐厅效率也高。
客户端相关方法:
ConnectAsync
//声明一个新的SocketAsyncEventArgs
SocketAsyncEventArgs eventArgs2 = new SocketAsyncEventArgs();
//为eventArgs2添加回调事件
eventArgs2.Completed += (socket,args) =>
{
if (args.SocketError == SocketError.Success) //成功连接
{
//处理逻辑
}
else
{
print("连接失败");
}
};
//实际执行ConnectAsync
tcpSocket.ConnectAsync(eventArgs2);
服务器和客户端通用方法
SendAsync方法
//发送消息
SocketAsyncEventArgs eventArgs3 = new SocketAsyncEventArgs();
//设置回调函数
eventArgs3.Completed += (socket, args) =>
{
if(args.SocketError == SocketError.Success)
print("成功发送");
};
//申明一个字节数组,用于存储需要发送的消息
byte[] buffer = new byte[1024]; //设置为1kb
//设置需要发送的字节数组
eventArgs3.SetBuffer(buffer,0,buffer.Length);
//实际执行SendAsync方法
tcpSocket.SendAsync(eventArgs3);
ReceiveAsync方法
//接收消息
SocketAsyncEventArgs eventArgs4 = new SocketAsyncEventArgs();
//设置回调函数
eventArgs4.Completed += (socket, args) =>
{
if (args.SocketError == SocketError.Success)
{
//转换为字符串
string getString = Encoding.UTF8.GetString(args.Buffer, 0, args.BytesTransferred);
//变量解释:
//args.Buffer:接收消息的容器
//args.BytesTransferred:获取了多少个字节
//这里执行对获取到的字符串的处理逻辑
//复用buffer(已有容器)
args.SetBuffer(0,1024);
//接收完消息 准备再接收下一条
(socket as Socket).ReceiveAsync(args);
}
};
//设置需要发送的字节数组
eventArgs4.SetBuffer(new byte[1024],0,1024);
//实际调用ReceiveAsync方法
tcpSocket.ReceiveAsync(eventArgs4);
上述代码示例中没有提到的内容:
在实际开发中需要为SocketAsyncEventArgs设置远程地址
//用于连接的SocketAsyncEventArgs
private SocketAsyncEventArgs connectionArgs = new SocketAsyncEventArgs();
//为connectionArgs设置ip和端口
connectionArgs.RemoteEndPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 8080);