C#實現異步Socket通信(控制檯)

     同步模式下,服務器使用Accept接受連接請求,客戶端使用Connect連接服務器。相對地,在異步模式下,服務器可以使用BeginAccept和EndAccept方法 完成連接到客戶端的任務

1 服務端

BeginAccept(AsyncCallback asyncCallBack,Object state);

參數 說明
asyncCallBack 代表回調函數
state 表示狀態信息,必須保證state中包含socket句柄

Socket EndAccept(IAsyncResult asyncResult)寫在回調函數中,用於獲取客戶端套接字

調用該函數後,程序繼續執行,有客戶端連接上,回調函數會執行。
IAsyncResult BeginReceive(byte[] buffer, int offset, int size, SocketFlags socketFlags, AsyncCallback callback, object state);

參數 說明
buffer Byte類型數組,用於存儲接受到的數據
offset buffer參數中存儲數據的位置,該位置應該從零開始技術
size 最多接受的字節數
socketFlags SocketFlags值的按位組合,這裏設置爲0
callback 回調函數,一個AsynCallback委託
state 一個用戶定義對象,其中包含接受操作的相關信息。遞給EndReceiv委託

int EndReceive(IAsyncResult asyncResult);寫在BeginReceive函數中,用於得到接收數據的大小。
異步服務端如圖:
在這裏插入圖片描述
     服務端需要處理多個客戶端消息,所以需要一個數組維持這些客戶端的連接。每個客戶端都有自己的名字,Socket和緩衝區。所以定義Conn類來描述客戶端連接,然後用Conn數組維持所有客戶端的連接。

using System;
using System.Net;
using System.Net.Sockets;

namespace Server
{
    class Conn
    {
        public const int BUFFER_SIZE = 1024;
        public Socket socket;
        public bool isUse = false;//該客戶端是否在使用中
        public byte[] readBuff = new byte[BUFFER_SIZE];//內容緩衝區
        public int buffCount = 0;//當前緩衝區數據字節量
        public string m_name = "";//客戶端名字
        public Conn()
        {
            readBuff = new byte[BUFFER_SIZE];
        }
        public void Init(Socket socket)
        {
            this.socket = socket;
            isUse = true;
            buffCount = 0;
        }
        public int BuffRemain()//獲取緩衝區剩餘容量
        {
            return BUFFER_SIZE - buffCount;
        }
        public string GetAdress()//獲取客戶端套接字地址  ip:端口
        {
               if (!isUse)
                return "無法獲取地址";
            return socket.RemoteEndPoint.ToString();
        }
        public void close()//關閉客戶端
        {
            if(!isUse)
                return;
            Console.WriteLine("[斷開連接]" + GetAdress());
            socket.Close();
            isUse = false;
        }
    }
    class Serv
    {
        public Socket listenfd;//服務端套接字
        public Conn[] conns;//連接池
        public int maxConn = 50;//最大連接數
        public int NewIndex()//從連接池獲取一個沒在使用的Conn的下標
        {
            if (conns == null)
                return -1;
            for (int i = 0; i < conns.Length; i++)
            {
                if (conns[i] == null)
                {
                    conns[i] = new Conn();
                    return i;
                }
                else if (conns[i].isUse == false)
                {
                    return i;
                }

            }
            return -1;
        }
        public void Start(string host, int port)//初始化連接池,開始監聽端口
        {
            conns = new Conn[maxConn];
            for (int i = 0; i < maxConn; i++)
            {
                conns[i] = new Conn();
            }
            listenfd = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            IPAddress ipAdr = IPAddress.Parse(host);
            IPEndPoint ipEp = new IPEndPoint(ipAdr, port);
            listenfd.Bind(ipEp);
            listenfd.Listen(maxConn);
            listenfd.BeginAccept(AcceptCb, null);
            Console.WriteLine("[服務器]啓動成功");

        }
        private void AcceptCb(IAsyncResult ar)//回調函數,接收到客戶端的連接
        {
            try
            {
                Socket socket = listenfd.EndAccept(ar);
                int index = NewIndex();
                if (index < 0)
                {
                    socket.Close();
                    Console.WriteLine("[警告]連接已滿");
                }
                else
                {
                    Conn conn = conns[index];
                    conn.Init(socket);
                    string adr = conn.GetAdress();
                    Console.WriteLine("客戶端連接[" + adr + "] conn池ID:" + index);
                    conn.socket.BeginReceive(conn.readBuff, conn.buffCount, conn.BuffRemain(), SocketFlags.None, ReceiveCb, conn);//接收客戶端發送的內容
                    listenfd.BeginAccept(AcceptCb, null);//繼續監聽,等待客戶端連接,
                }

            }
            catch (System.Exception ex)
            {
                Console.WriteLine("AcceptCb失敗:" + ex.Message);
            }
        }
        private void ReceiveCb(IAsyncResult ar)//回調函數 接收到客戶端內容進行調用
        {
            Conn conn = (Conn)ar.AsyncState;
            try
            {
                int count = conn.socket.EndReceive(ar);//獲取客戶端內容的字節數
                if (count <= 0)
                {
                    Console.WriteLine("收到 【" + conn.m_name + "】斷開連接 ");
                    conn.close();
                    return;
                }
                string str = System.Text.Encoding.Default.GetString(conn.readBuff, 0, count);
                //將客戶端名字和消息內容分割開  yar:hello  =》yar  hello
                string[] arr = str.Split(':');
                string name = "";
                int t = 0;
                foreach (string s in arr)
                {
                    if (t == 0) name = s ;
                    t = 1;
                    str = s;
                }
                conn.m_name = name;



                Console.WriteLine("收到 【" + conn.m_name + "】數據: " + str);
                byte[] bytes = System.Text.Encoding.Default.GetBytes(conn.m_name+":"+str);
                for (int i = 0; i < conns.Length; i++)//轉播給其他正在使用的客戶端
                {
                    if (conns[i] == null)
                        continue;
                    if (!conns[i].isUse)
                        continue;
                    Console.WriteLine("將消息轉播給" + conns[i].m_name);
                    conns[i].socket.Send(bytes);
                }
                conn.socket.BeginReceive(conn.readBuff, conn.buffCount, conn.BuffRemain(), SocketFlags.None, ReceiveCb, conn);//繼續接收該客戶端的數據
            }
            catch (System.Exception ex)
            {
                Console.WriteLine("收到 【" + conn.m_name + "】斷開連接 ");
                conn.close();
                return;
            }
        }
    }
    class Program
    {

       
        static void Main(string[] args)
        {
            Serv serv = new Serv();
            serv.Start("127.0.0.1", 1234);
            while (true)
            {
                string str = Console.ReadLine();
                switch (str)
                {
                    case "quit":
                        return;
                }
            }


        }


    }

}

2 客戶端

客戶端示意圖:
在這裏插入圖片描述

using System;
using System.Collections;
using System.Net;
using System.Net.Sockets;

namespace Client
{
    class Cli
    {
        string m_name = "無名";
        Socket socket;
        const int BUFFER_SIZE = 1024;
        public byte[] readBuff = new byte[BUFFER_SIZE];
        public Cli(String name)
        {
            m_name = name;
        }
        public void Connection()
        {
            socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            string host = "127.0.0.1";
            int port = 1234;
            socket.Connect(host, port);
           // socket.LocalEndPoint.ToString();
            socket.BeginReceive(readBuff,0,BUFFER_SIZE,SocketFlags.None,ReceiveCb,null);
        }
        private void ReceiveCb(IAsyncResult ar)
        {
            try
            {
                int count = socket.EndReceive(ar);
                string str = System.Text.Encoding.Default.GetString(readBuff, 0, count);
                Console.WriteLine(str);
                socket.BeginReceive(readBuff, 0, BUFFER_SIZE, SocketFlags.None, ReceiveCb, null);

            }
            catch (System.Exception ex)
            {
                socket.Close();
            }
            
        }
        public void Send(string str)
        {
            
            byte[] bytes = System.Text.Encoding.Default.GetBytes(m_name+":"+str);
            try
            {
                socket.Send(bytes);
            }
            catch { }
        }
        public static void Main(string[] args)
        {
            Console.Write("請輸入你的名字:");
            string username = Console.ReadLine();
            Cli cli = new Cli(username);
            cli.Connection();
            while (true)
            {
                string str = Console.ReadLine();
                switch (str)
                {
                    case "quit":
                        return;
                }
                cli.Send(str);
            }


        }
    }
}

3 運行結果展示

服務端啓動:
在這裏插入圖片描述
啓動兩個客戶端:
在這裏插入圖片描述
在這裏插入圖片描述
服務端顯示客戶端已經連接:
在這裏插入圖片描述
客戶端發送信息:
在這裏插入圖片描述

4 總結

       之前寫了一篇同步通信,有諸多弊端。現在寫了一篇異步通信,可以看成簡單的即時羣聊程序其實就是調不一樣的函數而已。這個程序現在可以進行多個客戶端同時連接,一個客戶端發送信息,其他客戶端都能收到信息;但是還有些缺點,比如,沒有圖形化界面,沒有用戶登錄,存在客戶端長時間不發送消息佔用連接池資源等。後續會出用於檢測客戶端在線狀態的心跳機制,連接數據庫實現用戶登錄,聊天記錄查詢,添加圖形化界面。說不住它就是下一個QQ呢。有感興趣者,自己可以實現這些功能。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章