C#实现简单异步Echo服务端和客户端(实现聊天室)

上篇文章中我们使用Connect,Receive和Send来进行接收数据,会阻塞,它是在单一线程完成的,不具备灵活性

因此可以使用BeginConnect和EndConnect等API完成相同功能完成异步连接和异步的发送接收

他们的函数原型如下:

BeginConnect:

cs 复制代码
public IAsyncResult BeginConnect(  string host,  int port,  AsyncCallback requestCallback,  object state )

对应IP,端口,回调函数,state参数用于传递用户自定义的对象或数据给回调函数

EndConnect:

cs 复制代码
public void EndConnect(  IAsyncResult asyncResult )

BeginReceive:

cs 复制代码
public IAsyncResult BeginReceive (  byte[] buffer,  int offset,  int size,  SocketFlags socketFlags,  AsyncCallback callback, object state )

EndReceive:

cs 复制代码
public int EndReceive(  IAsyncResult asyncResult )

BeginSend:

cs 复制代码
public IAsyncResult BeginSend(  byte[] buffer,  int offset,  int size,  SocketFlags socketFlags,  AsyncCallback callback,  object state )

EndSend:

cs 复制代码
public int EndSend (  IAsyncResult asyncResult )

BeginAccept:

cs 复制代码
public IAsyncResult BeginAccept(  AsyncCallback callback,  object state )

EndAccept:

cs 复制代码
public Socket EndAccept(  IAsyncResult asyncResult )

一、客户端

1:异步Socket创建和连接(BeginConnect/EndConnect)

cs 复制代码
byte[] readBuff = new byte[1024];
string recvStr = ""; 

socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
socket.BeginConnect("127.0.0.1", 8888, ConnectionCallback, socket);    //异步连接,socket作为参数传递给回调函数
public void ConnectCallback(IAsyncResult ar)    //回调函数
{  
 try
   {  
    Socket socket = (Socket)ar.AsyncState;    //ar.AsyncState 属性中获取了传递给 BeginConnect 方法的状态对象,即原始的Socket
    socket.EndConnect(ar);    //用于等待连接操作完成,并返回连接的结果。如果连接成功,则该方法会返回并且不会抛出异常;如果连接失败,则会抛出相应的异常。
    Debug.Log("Socket Connect Succ");
   }
 catch (SocketException ex)
 {  
    Debug.Log("Socket Connect fail" + ex.ToString());
  
  }
}

注:每次调用BeginConnect方法启动异步连接操作时,都应该在适当的地方调用EndConnect方法来结束该操作

2:异步接收消息(基于异步连接)(BeginReceive/EndReceive)

cs 复制代码
byte[] readBuff = new byte[1024];
string recvStr = ""; 

socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
socket.BeginConnect("127.0.0.1", 8888, ConnectionCallback, socket);
public void ConnectCallback(IAsyncResult ar) 
{  
 try
   {  
    Socket socket = (Socket)ar.AsyncState; 
    socket.EndConnect(ar); 
    Debug.Log("Socket Connect Succ");
    socket.BeginReceive( readBuff, 0, 1024, 0,  ReceiveCallback, socket);     //继续开启接收数据的回调
   }
 catch (SocketException ex)
 {  
    Debug.Log("Socket Connect fail" + ex.ToString());
  
  }
}
private void ReveiveCallBack(IAsyncResult ar)    //异步回调
    {
        try
        {
            Socket socket = (Socket)ar.AsyncState;    //从BeginReceive中获取到的socket,就是原始Socket
            int count = socket.EndReceive(ar);    //同理,EndReceive和BeginReceive同时出现,结束异步操作,返回实际字节数
            recvStr = System.Text.Encoding.Default.GetString(readBuff, 0, count);
            socket.BeginReceive(readBuff, 0, 1024, 0, ReveiveCallBack, socket);    //再次调用回调,继续接收下一批数据
        }
        catch(SocketException e)
        {
            Debug.Log("Socket Receive fail" + e.ToString());
        }
    }
    
    private void Update()
    {
        text.text = recvStr;
    }

注:两个地方调用了BeginReceive,一个在Connect的回调函数,一个在结束一次接收后,解析后再次调用

3:异步发送消息(BeginSend/EndSend)

cs 复制代码
public void Send() {
  //Send
  string sendStr = InputFeld.text;
  byte[] sendBytes = System.Text.Encoding.Default.GetBytes(sendStr);
  socket.BeginSend(sendBytes, 0, sendBytes.Length, 0, SendCallback, socket);
  }
public void SendCallback(IAsyncResult ar)
{  
    try
        {
            Socket socket = (Socket) ar.AsyncState;
            int count = socket.EndSend(ar);
            Debug.Log("Socket Send succ" + count);
        }  
     catch (SocketException ex)
     {
         Debug.Log("Socket Send fail" + ex.ToString());
      } 
}

注:一般情况下,EndSend的返回值count与要发送数据的长度相同, 代表数据全部发出

二、服务端

1:多客户端准备

服务端需要处理多个连接,所以需要创建一个存储多个连接的列表,先定义一个ClientState类,保存一个客户端的全部信息,包括Socket,读缓冲区readBuff

然后创建一个能存储多个用户信息的字典

cs 复制代码
class ClientState
{
    public Socket socket;
    public byte[] readBuff = new byte[1024];
}
class MainClass
{
    static Socket listenfd;
    public static Dictionary<Socket, ClientState> clients = new Dictionary<Socket, ClientState>();
}

2:异步接受连接(BeginAccept/EndAccept)

cs 复制代码
class ClientState
{
    public Socket socket;
    public byte[] readBuff = new byte[1024];
}
class MainClass
{
    static Socket listenfd;
    public static Dictionary<Socket, ClientState> clients = new Dictionary<Socket, ClientState>();
    static void Main(string[] args)
    {
        listenfd = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        IPAddress ipAdr = IPAddress.Parse("127.0.0.1");
        IPEndPoint ipEp = new IPEndPoint(ipAdr, 8888);
        listenfd.Bind(ipEp);
        listenfd.Listen(0);
        Console.WriteLine("[服务器]启动成功");
        listenfd.BeginAccept (AcceptCallback, listenfd); 
    }
    public static void AcceptCallback(IAsyncResult ar)
    {
    try{
        Console.WriteLine("[服务器]Accept");
        Socket listenfd = (Socket) ar.AsyncState;    //获取原始的Socket连接
        Socket clientfd = listenfd.EndAccept(ar);    //从原始Socket中读取到客户端Socket
        
        //把读取到的客户端添加到字典
        ClientState state = new ClientState();
        state.socket = clientfd;
        clients.Add(clientfd, state);
        
        //接收数据BeginReceive
        clientfd.BeginReceive(state.readBuff, 0, 1024, 0,  ReceiveCallback, state);
        //继续Accept
        listenfd.BeginAccept (AcceptCallback, listenfd);
        }
    }
    catch(SocketException e)
    {
        Console.WriteLine("Socket Accept fail" + e.ToStrin());
    }  
}

三、聊天室

1:客户端

cs 复制代码
using System;
using System.Net.Sockets;
using UnityEngine;
using UnityEngine.UI;

public class ChatClient : MonoBehaviour
{
    public InputField inputField;
    public Text text;
    byte[] readBuff = new byte[1024];
    string recvStr = "";

    Socket socket;
    private void Start()
    {
        socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        socket.BeginConnect("127.0.0.1", 8888, ConnectCallback, socket);
    }

    private void ConnectCallback(IAsyncResult ar)
    {
        try
        {
            Socket socket = (Socket)ar.AsyncState;
            socket.EndConnect(ar);
            Debug.Log("Socket Connect Succ");
            socket.BeginReceive(readBuff, 0, 1024, 0,ReceiveCallback, socket);
        }
        catch(Exception e)
        {
            Debug.Log("Socket connect fail" + e.ToString());
        }
    }

    private void ReceiveCallback(IAsyncResult ar)
    {
        try
        {
            Socket socket = (Socket)ar.AsyncState;
            int count = socket.EndReceive(ar);
            string s = System.Text.Encoding.Default.GetString(readBuff, 0, count);
            recvStr = s + "\n" + recvStr;//差异在于会显示历史数据
            socket.BeginReceive(readBuff, 0, 1024, 0, ReceiveCallback, socket);
        }
        catch(Exception e)
        {
            Debug.Log("Socket Receive fail" + e.ToString());
        }
    }
    public void Send()
    {
        string sendStr = inputField.text;
        byte[] sendBytes = System.Text.Encoding.Default.GetBytes(sendStr);
        socket.BeginSend(sendBytes, 0, sendBytes.Length, 0, SendCallback, socket);
    }

    private void SendCallback(IAsyncResult ar)
    {
        try
        {
            Socket socket = (Socket)ar.AsyncState;
            int count = socket.EndSend(ar);
            Debug.Log("Socket Send succ" + count);
        }
        catch (SocketException ex)
        {
            Debug.Log("Socket Send fail" + ex.ToString());
        }
    }

    private void Update()
    {
        text.text = recvStr;
    }
}

2:服务端

聊天室与Echo程序的不同之处在于服务端对消息的处理

同样需要把BeginAccept放到死循环里,让它一直接收消息,避免退出

cs 复制代码
using System.Net;
using System.Net.Sockets;

class ClientState{
    public Socket? socket;
    public byte[] readBuff = new byte[1024];
}
class MainClass
{
    static Socket listenfd;
    public static Dictionary<Socket,ClientState> clients = new Dictionary<Socket, ClientState> ();
    static void Main(string[] args)
    {
        listenfd = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        IPAddress iP = IPAddress.Parse ("127.0.0.1");
        IPEndPoint iPEp = new IPEndPoint(iP, 8888);
        listenfd.Bind (iPEp);
        listenfd.Listen(0);
        Console.WriteLine("[服务器]启动成功");
        while (true)
        {
            listenfd.BeginAccept(AcceptCallback, listenfd);
        }
    }

    private static void AcceptCallback(IAsyncResult ar)
    {
        try
        {
            Console.WriteLine("[服务器]Accept");
            Socket listenfd = (Socket) ar.AsyncState;
            Socket clientfd = listenfd.EndAccept(ar);

            ClientState state = new ClientState();
            state.socket = clientfd;
            clients.Add(clientfd, state);

            clientfd.BeginReceive(state.readBuff, 0, 1024, 0, ReceiveCallback, state);
            listenfd.BeginAccept(AcceptCallback, listenfd);
        }
        catch (Exception ex)
        {
            Console.WriteLine("Socket Accept fail" + ex.ToString());
        }
    }

    private static void ReceiveCallback(IAsyncResult ar)
    {
        try
        {
            ClientState state = (ClientState)ar.AsyncState;
            Socket clientfd = state.socket;
            int count = clientfd.EndReceive(ar);
            if(count == 0)
            {
                clientfd.Close();
                clients.Remove(clientfd);
                Console.WriteLine("Socket Close");
                return;
            }
            string recvStr = System.Text.Encoding.Default.GetString(state.readBuff, 0, count);
            byte[] sendBytes = System.Text.Encoding.Default.GetBytes("echo" + recvStr);
            foreach(ClientState s in clients.Values)
            {
                s.socket.Send(sendBytes);
            }
            clientfd.BeginReceive(state.readBuff, 0, 1024, 0, ReceiveCallback, state);
        }
        catch (Exception ex)
        {
            Console.WriteLine("Socket Receive fail" + ex.ToString());
        }
    }
}
相关推荐
Victor3567 小时前
Netty(20)如何实现基于Netty的WebSocket服务器?
后端
缘不易7 小时前
Springboot 整合JustAuth实现gitee授权登录
spring boot·后端·gitee
Kiri霧7 小时前
Range循环和切片
前端·后端·学习·golang
WizLC7 小时前
【Java】各种IO流知识详解
java·开发语言·后端·spring·intellij idea
Victor3567 小时前
Netty(19)Netty的性能优化手段有哪些?
后端
爬山算法7 小时前
Netty(15)Netty的线程模型是什么?它有哪些线程池类型?
java·后端
白宇横流学长8 小时前
基于SpringBoot实现的冬奥会科普平台设计与实现【源码+文档】
java·spring boot·后端
小猪快跑爱摄影8 小时前
【AutoCad 2025】【C#】零基础教程(四)——MText 常见属性
c#·autocad
Python编程学习圈8 小时前
Asciinema - 终端日志记录神器,开发者的福音
后端
bing.shao8 小时前
Golang 高并发秒杀系统踩坑
开发语言·后端·golang