同步模式下,服務器使用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呢。有感興趣者,自己可以實現這些功能。