网络异步编程
异步接收客户端连接BeginAcceptTcpClient
像之前的listener.AcceptTcpClient();的方法,这个是同步方法,意思是当没有客户端连接的时候,这个方法就一直等待。
那么异步方法就是listener.BeginAcceptTcpClient()。这个方法不管有没有客户端连接。都继续执行下去,也就是
不会阻塞当前的线程。
使用异步方法接收客户连接的话,需要定义一个AsyncCallback委托方法,因为异步方法是采用通知的方式接收客户端连接
。
也就是当有一个客户端连接的时候,AsyncCallback委托的方法就会被执行。当然我这里同步异步只是举个例子,
不光是接收客户端连接有异步同步方法。客户连接也有异步BeginConnect方法。但是好像同步方法Connect不会等待啊,像之前的例子。
其实那也只是服务端没开启罢了。如果由于网络速度的原因,连接服务端比较慢的话。那么就可以看得出Connect它是在等待了。
再来看看AsyncCallback委托的定义:
public delegate void AsyncCallback(IAsyncResult ar);
使用异步连接话,那么我们就得定义一个上面那样的方法,IAsyncResult表示异步操作的状态,储存着一些信息。
如 ar.AsyncState储存着一个object,它通过BeginAcceptTcpClient方法的第二个参数state传进来的。
AcceptTcpClient和BeginAcceptTcpClient的使命都是一样的,获得一个客户端连接,如果有一个客户端连接了,那么这个方法也就结束了。AcceptTcpClient这个方法就直接返回客户端对象TcpClient,那么BeginAcceptTcpClient是通过调用EndAcceptTcpClient方法来获取TcpClient的。这个表明一个异步调用结束了。
异步调用结束是在一个客户端连接的时候。因为只有当有客户端连接的时候,回调函数才会被执行。也才会执行EndAcceptTcpClient
所以在回调函数中,调用EndAcceptTcpClient结束异步调用后,如果想再等待下一个客户端连接,必须再次调用BeginAcceptTcpClient方法。就像同步的一样,一个AcceptTcpClient对应着一个客户端,如果想连接第二个客户端,就得再次调用AcceptTcpClient。
来看一个例子吧,把上面的理论用例子来证明。
服务端代码:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net;
using System.Net.Sockets;
namespace ServerConls
{
public class Server
{
//这个方法被执行,就表明有一个客户端要连接了。
public static void AcpClientCallback(IAsyncResult ar)
{
TcpListener listener = (TcpListener)ar.AsyncState;
//调用对应的方法EndAcceptTcpClient,获得连接的客户端TcpClient
TcpClient client=listener.EndAcceptTcpClient(ar);
//输出客户端信息
Console.WriteLine("一个客户端连接上了,客户端信息:{0}", client.Client.RemoteEndPoint);
//再接收一个客户端连接
AsyncCallback callback = new AsyncCallback(AcpClientCallback);
listener.BeginAcceptTcpClient(callback, listener);
}
public static void Main()
{
//绑定IP,监听端口
IPAddress ip = new IPAddress(new byte[] { 127, 0, 0, 1 });
TcpListener listener = new TcpListener(ip, 9372);
listener.Start();
//委托方法
AsyncCallback callback = new AsyncCallback(AcpClientCallback);
//接收客户端连接,异步
listener.BeginAcceptTcpClient(callback, listener);
//循环输出一些信息
int i = 0;
while (true)
{
i++;
Console.WriteLine("输出一些信息:{0}",i);
//睡眠1.5秒
System.Threading.Thread.Sleep(1500);
}
}
}
}
客户端代码:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net;
using System.Net.Sockets;
namespace ClientConls
{
class Program
{
static void Main(string[] args)
{
TcpClient client;
//三个客户端连接
for (int i = 0; i < 3; i++)
{
client = new TcpClient();
client.Connect("localhost", 9372);
//睡眠3秒
System.Threading.Thread.Sleep(3000);
}
}
}
}
这就是异步的好处,如果是同步的话,等待连接的时候,是不能再做其它事情的,也就不能一边输出信息,一边等待连接了。
异步读取数据
套路跟上面的差不多,定义一个委托方法,在里面调用对应的EndRead方法结束异步调用。直接看例子吧
服务端代码:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net;
using System.Net.Sockets;
namespace ServerConls
{
public class Server
{
public static byte[] buffer = new byte[800];
public static TcpClient client;
// 客户端有数据发送过来,就会调用这个方法
public static void ReadCallback(IAsyncResult ar)
{
NetworkStream Stream = (NetworkStream)ar.AsyncState;
//结束异步读取方法,返回值是读取了多少字节
int bytesRead = Stream.EndRead(ar);
String msg = Encoding.Unicode.GetString(buffer, 0, bytesRead);
Console.WriteLine("\n从{0}上来发来信息:{1}", client.Client.RemoteEndPoint, msg);
//这次读取BeginRead结束,继续下一次读取
Stream.BeginRead(buffer, 0, 800, ReadCallback, Stream);
}
public static void Main()
{
//绑定IP,监听端口
IPAddress ip = new IPAddress(new byte[] { 127, 0, 0, 1 });
TcpListener listener = new TcpListener(ip, 9372);
listener.Start();
//等待一个客户端连接
client = listener.AcceptTcpClient();
Console.WriteLine("已经连接到一个客户端!");
NetworkStream Stream = client.GetStream();
//异步读取数据
Stream.BeginRead(buffer, 0, 800, ReadCallback, Stream);
byte[] sendBuffer;
//发送数据
while (true)
{
String msg = Console.ReadLine();
sendBuffer = Encoding.Unicode.GetBytes(msg);
Stream.Write(sendBuffer, 0, sendBuffer.Length);
}
}
}
客户端代码:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net;
using System.Net.Sockets;
namespace ClientConls
{
class Program
{
public static byte[] buffer = new byte[800];
public static TcpClient client;
//服务端有数据发送过来,就会执行这个方法。
public static void ReadCallback(IAsyncResult ar)
{
NetworkStream Stream = (NetworkStream)ar.AsyncState;
//结束异步读取方法,返回值是读取了多少字节
int bytesRead = Stream.EndRead(ar);
String msg = Encoding.Unicode.GetString(buffer, 0, bytesRead);
Console.WriteLine("\n从{0}上来发来信息:{1}", client.Client.RemoteEndPoint, msg);
//这次读取BeginRead结束,继续下一次读取
Stream.BeginRead(buffer, 0, 800, ReadCallback, Stream);
}
static void Main(string[] args)
{
//连接服务端
client = new TcpClient();
client.Connect("localhost", 9372);
NetworkStream Stream = client.GetStream();
//异步读取数据
Stream.BeginRead(buffer, 0, 800, ReadCallback, Stream);
byte[] sendBuffer;
//发送数据
while (true)
{
String msg = Console.ReadLine();
sendBuffer = Encoding.Unicode.GetBytes(msg);
Stream.Write(sendBuffer, 0, sendBuffer.Length);
}
}
}
}
客户端有服务端都是采用异步读取数据的,这样双方发送数据,就可以不按一定的顺序来,可以随时发送。如果是同步的话,那就得按固定的发送接收步骤来了。
异步读取数据的方式,很符合聊天软件的需要。
网络异常处理
网络编程异步处理是很有必要的,比如客户端连接服务端,如果服务端没启动的话,就会产生异常,程序就会非正常结束。如果用异常处理的话,可以规定客户端按自己方式来处理,比如服务端没有开启的话,给用户一个选择,是否重新连接,或者做其它的事,这样也不致于让程序就结束了。
我们先来看一个简单的例子,这个例子实现了当客户端关闭的时候,服务端会给出一个提示,提示客户端已关闭。
客户端:
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net;
using System.Net.Sockets;
namespace ClientConls
{
class Program
{
static void Main(string[] args)
{
TcpClient client = new TcpClient();
//连接服务端
client.Connect("localhost", 9372);
Console.ReadLine();
client.Close();
}
}
}
服务端:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net;
using System.Net.Sockets;
namespace ServerConls
{
class Program
{
static void Main(string[] args)
{
//绑定IP,监听端口
IPAddress ip = new IPAddress(new byte[] { 127, 0, 0, 1 });
TcpListener listener = new TcpListener(ip, 9372);
listener.Start();
TcpClient remoteClient = listener.AcceptTcpClient();
Console.WriteLine("一个客户端连接了:{0}", remoteClient.Client.RemoteEndPoint);
//获取数据流
NetworkStream Stream = remoteClient.GetStream();
byte[] buffer = new byte[800];
while (true)
{
if (Stream.Read(buffer, 0, 800) == 0)
{
Console.WriteLine("与客户端失去连接:{0}", remoteClient.Client.RemoteEndPoint);
break;
}
}
Console.ReadLine();
}
}
}
上面的例子是在理想的状态下,先开启服务端,然后运行客户端。最后在客户端的控制台下窗口输入几个字符,执行client.Close();语句,那么服务端的Stream.Read就会返回0,从而给出与客户端失去连接的提示。
但如果直接关闭了客户端控制台窗口,服务端的Stream.Read就会产生异常,程序崩溃了。避免这个问题,当然得用异常处理try catch了。
看修改后的例子:
处理服务端异常,检查客户端连接状态
客户端代码同上,不变。
服务端代码:
class Program
{
static void Main(string[] args)
{
//绑定IP,监听端口
IPAddress ip = new IPAddress(new byte[] { 127, 0, 0, 1 });
TcpListener listener = new TcpListener(ip, 9372);
listener.Start();
TcpClient remoteClient = listener.AcceptTcpClient();
Console.WriteLine("一个客户端连接了:{0}", remoteClient.Client.RemoteEndPoint);
//获取数据流
NetworkStream Stream = remoteClient.GetStream();
byte[] buffer = new byte[800];
while (true)
{
try
{
if (Stream.Read(buffer, 0, 800) == 0)
{
Console.WriteLine("与客户端失去连接:{0}", remoteClient.Client.RemoteEndPoint);
break;
}
}
catch(Exception e)
{
//Connected连接状态为假
if (remoteClient.Connected == false)
Console.WriteLine("异常中:与客户端失去连接:{0}", remoteClient.Client.RemoteEndPoint);
else
Console.WriteLine(e.Message);
}
finally
{
//释放资源
Stream.Dispose();
break;
}
}
Console.ReadLine();
}
}
那么用try catch解决服务端没开启的问题也是一样,捕捉client.Connect("localhost", 9372);这个语句的异常,然后处理。
另外关于等待客户端连接,读取数据什么的也可以用多线程来实现。这样灵活性就增加了许多。这里就不具体举例了,自己可以去琢磨。