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

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

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

他们的函数原型如下:

BeginConnect:

public IAsyncResult BeginConnect(  string host,  int port,  AsyncCallback requestCallback,  object state )

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

EndConnect:

public void EndConnect(  IAsyncResult asyncResult )

BeginReceive:

public IAsyncResult BeginReceive (  byte[] buffer,  int offset,  int size,  SocketFlags socketFlags,  AsyncCallback callback, object state )

EndReceive:

public int EndReceive(  IAsyncResult asyncResult )

BeginSend:

public IAsyncResult BeginSend(  byte[] buffer,  int offset,  int size,  SocketFlags socketFlags,  AsyncCallback callback,  object state )

EndSend:

public int EndSend (  IAsyncResult asyncResult )

BeginAccept:

public IAsyncResult BeginAccept(  AsyncCallback callback,  object state )

EndAccept:

public Socket EndAccept(  IAsyncResult asyncResult )

一、客户端

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

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)

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)

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

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

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)

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:客户端

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放到死循环里,让它一直接收消息,避免退出

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());
        }
    }
}

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-03-24 19:28:02       94 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-03-24 19:28:02       101 阅读
  3. 在Django里面运行非项目文件

    2024-03-24 19:28:02       82 阅读
  4. Python语言-面向对象

    2024-03-24 19:28:02       91 阅读

热门阅读

  1. uniapp离线打包笔记

    2024-03-24 19:28:02       39 阅读
  2. 单元测试框架 Junit

    2024-03-24 19:28:02       44 阅读
  3. 算法刷题记录 Day25

    2024-03-24 19:28:02       43 阅读
  4. 条件约束聚类并显示

    2024-03-24 19:28:02       39 阅读
  5. 若依实现多数据源

    2024-03-24 19:28:02       43 阅读