异步模式下,服务器可以使用BeginAccept 和EndAccept 方法完成连接到客户端的任务。
1.BeginAccept 的函数原型: BeginAccept(AsyncCallback asynCallback, Ojbect state) //asynCallback代表回调函数;state表示状态信息,必须保证state中包含socket的句柄
调用BeginAccept后,程序将继续执行而不是阻塞在该语句上。等客户端连接上后,回调函数asynCallback将被执行。
在回调函数中,开发者可以使用EndAccept获取新客户端的套接字,还可以通过AsyncState获取state参数传入的数据。
使用方法如下:
listenfd.BeginAccept(AcceptCb, null);
private void AcceptCb(IAsyncResult ar)
{
Socket socket = listenfd.EndAccept(ar);
//接收消息
}
2.BeginReceive实现的是异步数据接收
public IAsyncResult BeginReceive( byte[] buffer, int offset, int size, SocketFlags socketFlags, AsyncCallback callback, object state );
3.服务端要处理多个客户端消息,需要用一个数组来维护所有客户端的连接。每个客户端都有自己的Socket和缓冲区。编写Conn类来表示客户端连接,类的成员说明如下
4.编写服务端主体结构Serv类,它包含一个Conn类型的对象池(数组),用于维护客户端连接。
NewIndex 方法找出对象池中尚未使用的元素下标。
在start方法中,服务器经历Socket、Bind、Listen,然后调用BeginAccept 开始异步处理客户端的连接。
5.在MainClass.Main中创建Serv类的实例并开启服务器。
====服务端代码如下
using System;
using System.Net;
using System.Net.Sockets;
using System.Collections;
using System.Collections.Generic; //包含定义泛型集合的接口和类,设置动态数组
namespace test2 //自己随意取的文件名
{
public class Conn //服务端要处理多个客户端消息,需要一个数组来维护所有客户端的连接。Conn类来表示客户端连接
{
//常量
public const int BUFFER_SIZE = 1024; //缓冲区大小
//Socket
public Socket socket;
//是否使用
public bool isUse = false; //指明该对象是否被使用
//Buff
public byte[] readBuff = new byte[BUFFER_SIZE]; //读缓冲区
public int buffCount = 0; //当前缓冲区的长度
//构造函数
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()
{
if (!isUse)
return"无法获取地址";
return socket.RemoteEndPoint.ToString(); //调用socket.RemoteEndPoint获取客户端的IP地址和端口
}
//关闭
public void Close()
{
if (!isUse) //对象未被使用,则直接返回
return;
Console.WriteLine ("[断开连接]" + GetAdress ());
socket.Close (); //关闭连接
isUse = false;
}
}
public class Serv //主体结构,包含一个Conn类型的对象池(数组),用于维护客户端连接
{
//监听嵌套字
public Socket listenfd;
//客户端连接
public Conn[] conns; //数组,代表客户端连接列表(对象池)
//最大连接数
public int maxConn = 50;
//获取连接池索引,返回负数表示获取失败
public int NewIndex() //遍历数组,找出对象池中尚未使用的元素下标
{
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; //最后找不到可用的,则返回-1
}
//开启服务器
public void Start(string host,int port)
{
//连接池,核心思想是连接复用,
conns = new Conn[maxConn]; //初始化连接池
for (int i=0;i < maxConn ; i++)
{
conns[i] = new Conn();
}
//开启Socket连接流程:1.Socket
listenfd = new Socket(AddressFamily.InterNetwork,SocketType.Stream,ProtocolType.Tcp);
//2.Bind
IPAddress ipAdr = IPAddress.Parse(host);
IPEndPoint ipEp = new IPEndPoint(ipAdr,port);
listenfd.Bind(ipEp);
//3.Listen
listenfd.Listen(maxConn);
//4.BeginAccept
listenfd.BeginAccept(AcceptCb,null); //AcceptCb是BeginAccept的回调函数
Console.WriteLine("[服务器]启动成功");
}
//AcceptCb 回调,做三件事情:1.给新的连接分配conn;2.异步接收客户端数据;3.再次调用BeginAccept实现循环。
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); //再次调用BeginAccept实现循环
}
catch(Exception e) //异常发生,则抛出错误
{
Console.WriteLine ("AcceptCb 失败:" + e.Message);
}
}
//ReceiveCb 回调,做三件事情:1.接收并处理消息,因为是多人聊天,服务端接收到消息后,要把它转发给所有人;2.如果收到客户端关闭连接的信号(count=0),则断开连接;3.继续调用BeginReceive接收下一个数据。
private void ReceiveCb(IAsyncResult ar)
{
Conn conn = (Conn)ar.AsyncState; //获取BeginReceive传入的Conn对象
try
{
int count = conn.socket.EndReceive(ar); //获取接收的字节数
//关闭信号
if(count <= 0)
{
Console.WriteLine("收到[" + conn.GetAdress() +"]断开连接");
conn.Close();
return;
}
//数据处理
string str = System.Text.Encoding.Default.GetString(conn.readBuff,0,count);
Console.WriteLine("收到 [" + conn.GetAdress() + "]数据:" +str);
str = conn.GetAdress() + ":" + str;
byte[] bytes = System.Text.Encoding.Default.GetBytes(str);
//广播
for(int i=0;i<conns.Length;i++) //将消息发送给所有正在使用的连接
{
if (conns[i] == null)
continue;
if(!conns[i].isUse)
continue;
Console.WriteLine("将消息转播给" + conns[i].GetAdress());
conns[i].socket.Send(bytes);
//继续接收
conn.socket.BeginReceive(conn.readBuff,conn.buffCount,conn.BuffRemain(),SocketFlags.None,ReceiveCb,conn); //继续接收,实现循环
}
}
catch(Exception e)
{
Console.WriteLine("收到[" + conn.GetAdress()+"]断开连接");
conn.Close();
}
}
}
class MainClass
{
public static void Main (string[] args)
{
Console.WriteLine ("Hello World!");
Serv serv = new Serv();
serv.Start ("127.0.0.1", 1234); //开启服务器
while (true)
{
string str = Console.ReadLine ();
switch (str)
{
case "quit":
return;
}
}
}
}
}
备注:
回调函数:如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。