C#中的TCP异步通信以及常用方法解释

本文记录了本人在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);

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值