说说Silverlight里的Socket
作为.Net Framework中的“古董级”类,Socket类无论在1.0,还是最新的3.5 sp1的网络编程里,都占据着极其重要的作用。优化网络架构,一直是开发人员追求的问题。在2.0时代,我们使用异步Socket(Begin、End等方法)来增强性能,不过,这类方法往往要初始化一个IAsyncResult的对象,这给.Net的GC带来了额外的负担。因此,从2.0 sp1起,.Net Framework的Socket类里又多了一种新的异步方式(Async),Silverlight从2.0起,支持通过Async Socket的方式和服务器通信。
Async Socket
首先让我们来看看使用Async方法和以往的使用Begin、End的方法有什么不同吧:
如果是Begin、End的方法,我们采用以下方法来Accept一个客户端:
Server.BeginAccept(new AsyncCallback(DoAcceptTcpClientCallBack), Server);
以及CallBack函数:
protected void DoAcceptTcpClientCallBack(IAsyncResult result) { //获得当前Socket Socket listener = result.AsyncState as Socket; //远程Socket对象 Socket client = null; try { //停止同步监听端口 client = listener.EndAccept(result); } catch { return; } if (client == null) return; StateObject state = new StateObject(BufferSize); //开始异步接收远程Socket发来的数据 client.BeginReceive(state.Buffer, 0, state.Buffer.Length, SocketFlags.None, new AsyncCallback(DoBeginReceiveCallback), state); }
如果采用Async方式,则相对简单不少,先初始化一个SocketAsyncEventArgs对象:
SocketAsyncEventArgs AcceptEventArgs = new SocketAsyncEventArgs(); AcceptEventArgs.Completed += OnAcceptCompleted; Server.AcceptAsync(AcceptEventArgs);
然后处理Completed事件:
protected void OnAcceptCompleted(object sender, SocketAsyncEventArgs e) { if (e.SocketError == SocketError.Success) { Socket client = e.AcceptSocket; SocketAsyncEventArgs args = new SocketAsyncEventArgs(); args.SetBuffer(new Byte[10240], 0, 10240); args.Completed += OnClientReceive; client.ReceiveAsync(args); Server.AcceptAsync(AcceptEventArgs); } }
SocketAsyncEventArgs
Async Socket的核心部分就是使用SocketAsyncEventArgs类,该类专为需要高性能的网络服务器应用程序而设计。应用程序可以完全使用增强的异步模式,也可以仅仅在目标热点区域(例如,在接收大量数据时)使用此模式。
这些增强功能的主要特点是可以避免在异步套接字 I/O 量非常大时发生重复的对象分配和同步。当前由Socket 类实现的开始/结束设计模式要求为每个异步套接字操作分配一个 IAsyncResult 对象。
SocketAsyncEventArgs类有一个Completed事件,一切和Async Socket有关的操作结素后,都将调用该事件的处理函数。例如在上面的例子中,我们使用OnAcceptCompleted方法来处理Accept后的结果。通常我们使用此类执行异步套接字操作的模式包含以下步骤:
- 分配一个新的 SocketAsyncEventArgs 上下文对象,或者从应用程序池中获取一个空闲的此类对象。
- 将该上下文对象的属性设置为要执行的操作(例如,完成回调方法、数据缓冲区、缓冲区偏移量以及要传输的最大数据量)。
- 调用适当的套接字方法 (xxxAsync) 以启动异步操作。
- 如果异步套接字方法 (xxxAsync) 返回 true,则在回调中查询上下文属性来获取完成状态。
- 如果异步套接字方法 (xxxAsync) 返回 false,则说明操作是同步完成的。可以查询上下文属性来获取操作结果。
- 将该上下文重用于另一个操作,将它放回到应用程序池中,或者将它丢弃。
可以使用同一个SocketAsyncEventArgs来处理Accept(服务器)、Connect(客户端)、Send、Receive的结果,SocketAsyncEventArgs有一个LastOperation的属性来标注该Completed是哪种操作的结果:
if (e.LastOperation == SocketAsyncOperation.Connect) { //………… } else if (e.LastOperation == SocketAsyncOperation.Receive) { //………… } else if (e.LastOperation == SocketAsyncOperation.Send) { //………… }
当然,也可以为不同的操作使用不同的SocketAsyncEventArgs对象。
可以在事件处理函数中通过观察SocketError属性来判断调用结果,当
e.SocketError == SocketError.Success
表明调用成功。
通过调用SocketAsyncEventArgs的SetBuffer方法,来设置需要发送的数据,或指定在接收数据时,的缓存数组。例如在上面例子里,我们为args设置了一个10240字节的数组,来存放接收到的数据:
args.SetBuffer(new Byte[10240], 0, 10240);
在Silverlight里使用Async Socket
在Silverlight里使用Async Socket,除了某些方法不支持外,大体上和完整版的.Net类似。不过,也有一些小小的限制:
- 网络应用程序可以连接到的端口范围必须在 4502-4534 范围内。这些是使用Socket从Silverlight应用程序进行连接所唯一允许使用的端口。如果连接的目标端口不在此端口范围内,则尝试连接时将会失败。
- Silverlight 运行时中的安全策略系统要求必须先从网络资源下载一个策略文件,之后才允许网络连接访问该资源。源站点和跨域网络访问都因此而受到影响。
对于上述第二点,我们使用以下形式的策略文件:
<?xml version="1.0" encoding ="utf-8"?> <access-policy> <cross-domain-access> <policy> <allow-from> <domain uri="file:///" /> </allow-from> <grant-to> <socket-resource port="4502-4506" protocol="tcp" /> </grant-to> </policy> </cross-domain-access> </access-policy>
可以在服务器上架设一个Server服务,监听943接口。Silverlight通过Socket访问服务器资源时,首先连接到服务器地址的943端口,然后下载该策略文件。
封装好的AsyncClient类
以下是我写的一个可在Silverlight中使用的Async Socket类,用于虚拟实验室里的Socket操作。和普通的Socket不同,该类在发送数据包时,在数据包头上添加4字节,作为数据包的长度;接收数据包时,同样的先读取前4字节,作为本次接收的长度。:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Net.Sockets; using System.IO; using System.Threading; using System.Net; using System.ComponentModel; namespace Newinfosoft.Net.Sockets { public class AsyncClient : INotifyPropertyChanged { #region Events /// <summary> /// 当接收到数据时 /// </summary> public event AsyncConnectionEventHandler DataRecieved; /// <summary> /// 当数据发送完毕时 /// </summary> public event AsyncConnectionEventHandler DataSend; /// <summary> /// 当连接服务器成功时 /// </summary> public event AsyncConnectionEventHandler Connected; /// <summary> /// 当属性改变时(例如是否连接属性) /// </summary> public event PropertyChangedEventHandler PropertyChanged; #endregion /// <summary> /// 用于存放接收时临时数据的内部类 /// 假设服务器发送来的数据格式为: /// |数据长度n|数据本生 /// 4字节 n字节 /// </summary> protected class StateObject { public byte[] Buffer; /// <summary> /// 还有多少字节的数据没有接收 /// </summary> public int RemainSize = 0; public MemoryStream Stream = null; public StateObject(int bufferSize) { Buffer = new byte[bufferSize]; } ~StateObject() { if (Stream != null) { Stream.Close(); Stream.Dispose(); } } } /// <summary> /// 客户端Socket对象 /// </summary> public Socket Client { get; private set; } #region 异步SocketAsyncEventArgs public SocketAsyncEventArgs SendEventArgs { get; private set; } public SocketAsyncEventArgs ReceiveEventArgs { get; private set; } public SocketAsyncEventArgs ConnectEventArgs { get; private set; } #endregion /// <summary> /// 读取或设置接收时的StateObject /// </summary> protected StateObject State { get; set; } /// <summary> /// 发送锁,只有当前一个包中的数据全部发送完时,才允许发送下一个包 /// </summary> protected object m_lockobject = new object(); protected ManualResetEvent SendResetEvent = new ManualResetEvent(false); #region IsConnecting protected bool m_IsConnecting = false; /// <summary> /// 读取或设置是否连接到远程服务器,可用于绑定操作 /// </summary> public bool IsConnecting { get { return m_IsConnecting; } set { if (m_IsConnecting != value) { m_IsConnecting = value; OnPropertyChanged("IsConnecting"); } } } #endregion /// <summary> /// 通过指定的IPAddress和Port创建AsyncClient,需要调用Connect方法连接 /// </summary> /// <param name="bufferSize">接收缓存大小</param> public AsyncClient(int bufferSize) { State = new StateObject(bufferSize); SendEventArgs = new SocketAsyncEventArgs(); ReceiveEventArgs = new SocketAsyncEventArgs(); ReceiveEventArgs.Completed += OnReceiveCompleted; SendEventArgs.Completed += OnSendCompleted; IsConnecting = false; } /// <summary> /// 将已有的Socket包装为AsyncClient对象, /// 如果Socket没有连接,则需要调用Connect方法 /// </summary> /// <param name="socket">Socket对象</param> /// <param name="bufferSize">接收缓存的大小</param> public AsyncClient(Socket socket, int bufferSize) { State = new StateObject(bufferSize); SendEventArgs = new SocketAsyncEventArgs(); ReceiveEventArgs = new SocketAsyncEventArgs(); ReceiveEventArgs.Completed += OnReceiveCompleted; SendEventArgs.Completed += OnSendCompleted; this.Client = socket; if (socket != null && socket.Connected) { ReceiveEventArgs.SetBuffer(State.Buffer, 0, State.Buffer.Length); Client.ReceiveAsync(ReceiveEventArgs); IsConnecting = true; } else { IsConnecting = false; } } /// <summary> /// 连接 /// </summary> /// <param name="address">IP地址</param> /// <param name="port">端口号</param> public void Connect(String address, int port) { if (Client == null) { Client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); } if (!Client.Connected) { ConnectEventArgs = new SocketAsyncEventArgs(); ConnectEventArgs.Completed += OnConnectComplete; EndPoint remoteEndPoint = new IPEndPoint(IPAddress.Parse(address), port); ConnectEventArgs.RemoteEndPoint = remoteEndPoint; Client.ConnectAsync(ConnectEventArgs); } } /// <summary> /// 发送数据 /// </summary> /// <param name="data">需要发送的数据</param> public void Send(Byte[] data) { Send(data, 0, data.Length); } /// <summary> /// 发送数据,按照以下格式发送数据: /// |数据长度n|需要发送的数据 /// 4字节 n字节 /// </summary> /// <param name="data">需要发送的数据</param> /// <param name="offset">需要发送数据的偏移量</param> /// <param name="size">需要发送数据的长度</param> public void Send(Byte[] data, int offset, int size) { if (!IsConnecting) { throw new Exception("没有连接,无法发送数据!"); } lock (m_lockobject) { if (data == null || data.Length == 0) return; //计算数据的长度,并转换成字节数组,作为本次发送的头部 byte[] length = BitConverter.GetBytes(size); Byte[] buffer = new Byte[size + length.Length]; Array.Copy(length, 0, buffer, 0, length.Length); Array.Copy(data, offset, buffer, length.Length, size); //设置发送Buffer SendEventArgs.SetBuffer(buffer, 0, buffer.Length); SendResetEvent.Reset(); Client.SendAsync(SendEventArgs); //等待发送成功的信息,只有收到该信息,才退出lock锁, //这样,确保只有当前面得数据发送完后,才发送下一段数据 SendResetEvent.WaitOne(); } } protected void OnPropertyChanged(string propertyName) { if (PropertyChanged != null) { PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); } } protected void OnConnectComplete(object sender, SocketAsyncEventArgs e) { if (e.SocketError == SocketError.Success) { IsConnecting = true; ReceiveEventArgs.SetBuffer(State.Buffer, 0, State.Buffer.Length); Client.ReceiveAsync(ReceiveEventArgs); if (Connected != null) { Connected(this, new AsyncConnectionEventArgs(null, this)); } } else { IsConnecting = false; } } void OnSendCompleted(object sender, SocketAsyncEventArgs e) { //如果传输的数据量为0,则表示链接已经断开 if (e.BytesTransferred == 0) { Client.Close(); } else { if (DataSend != null) { AsyncConnectionEventArgs se = new AsyncConnectionEventArgs(e.Buffer, this); DataSend(this, se); } //通知数据发送完毕 SendResetEvent.Set(); } } /// <summary> /// 接收数据处理函数 /// 1、将收到的数据包中的前4字节转换成Int32类型,作为本次数据包的长度。 /// 2、将这个值设置成StateObject的RemainSize。 /// 3、将数据包中剩下的数据写入StateObject的MemoryStream,并减少相应的RemainSize值。 /// 4、直到RemainSize=0时,表示这一段数据已经接收完毕,从而重复1,开始下一段数据包的接收 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> void OnReceiveCompleted(object sender, SocketAsyncEventArgs e) { //如果传输的数据量为0,则表示链接已经断开 if (e.BytesTransferred == 0) { Client.Close(); } else { int position = 0; while (position < e.BytesTransferred) { if (State.RemainSize > 0) { int bytesToRead = State.RemainSize > e.BytesTransferred - position ? e.BytesTransferred - position : State.RemainSize; State.RemainSize -= bytesToRead; State.Stream.Write(State.Buffer, position, bytesToRead); position += bytesToRead; if (State.RemainSize == 0) { if (DataRecieved != null) { AsyncConnectionEventArgs ce = new AsyncConnectionEventArgs(State.Stream.ToArray(), this); DataRecieved(this, ce); } State.Stream.Dispose(); } } else { State.RemainSize = BitConverter.ToInt32(State.Buffer, position); State.Stream = new MemoryStream(State.RemainSize); position += 4; } } //重新设置数据缓存区 e.SetBuffer(State.Buffer, 0, State.Buffer.Length); Client.ReceiveAsync(e); } } } }
使用方式如下:
AsyncClient client= new AsyncClient(10240); client.Connected += OnClientConnected; client.DataRecieved += OnClientDataRecieved; client.Connect("127.0.0.1", 4503);
小节
本文介绍了在Silverlight中,如何使用Async Socket来连接服务器,以及如何编写跨域访问的配置文件。最后,给出了一个适用于Silverlight的类AsyncClient,封装了Async Socket方法。注意,该类在每次发送数据时,需要在数据的前面发送该段数据的长度,因此,只能适用于采用类似方式编写的Server对象:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Net.Sockets; using System.Net; using System.Threading; using System.IO; namespace Newinfosoft.Net.Sockets { /// <summary> /// ConnectionServer在.Net 2.0 sp1上的实现 /// </summary> public class AsyncServer { #region events /// <summary> /// 接收到数据 /// </summary> public event AsyncConnectionEventHandler DataRecieved; public event AsyncConnectionEventHandler DataSend; /// <summary> /// 远程Socket连接 /// </summary> public event AsyncConnectionEventHandler RemoteSocketAccept; /// <summary> /// 开始监听端口 /// </summary> public event EventHandler Started; /// <summary> /// 停止监听端口 /// </summary> public event EventHandler Stoped; #endregion protected class StateObject { public Socket ClientSocket = null; public byte[] Buffer; public int RemainSize = 0; public MemoryStream Stream = null; public StateObject(int bufferSize) { Buffer = new byte[bufferSize]; } ~StateObject() { Stream.Close(); Stream.Dispose(); } } /// <summary> /// 读取TCP监听服务 /// </summary> public Socket Server { get; private set; } /// <summary> /// 读取当前服务的端口号 /// </summary> public int Port { get; private set; } /// <summary> /// 读取或设置是否正在监听端口 /// </summary> public bool IsListening { get; set; } /// <summary> /// 读取或设置缓存大小 /// </summary> public int BufferSize { get; set; } public SocketAsyncEventArgs AcceptEventArgs { get; set; } #region 构造函数 /// <summary> /// 构造函数 /// </summary> /// <param name="port">监听端口</param> public AsyncServer(int port) : this(port, 20480) { } /// <summary> /// 构造函数 /// </summary> /// <param name="port">监听端口</param> /// <param name="bufferSize">缓存大小</param> public AsyncServer(int port, int bufferSize) { Port = port; BufferSize = bufferSize; } #endregion public void Start() { try { if (Server == null) { IPEndPoint localEP = new IPEndPoint(IPAddress.Any, Port); Server = new Socket(localEP.AddressFamily, SocketType.Stream, ProtocolType.Tcp); Server.Bind(localEP); } Server.Listen(int.MaxValue); IsListening = true; if (Started != null) { Started(this, new EventArgs()); } AcceptEventArgs = new SocketAsyncEventArgs(); AcceptEventArgs.Completed += OnAcceptCompleted; Server.AcceptAsync(AcceptEventArgs); } catch (SocketException ex) { throw ex; } } public void Stop() { IsListening = false; if (Server != null) { Server.Close(); Server = null; if (Stoped != null) { Stoped(this, new EventArgs()); } } } #region Accept protected void OnAcceptCompleted(object sender, SocketAsyncEventArgs e) { if (e.AcceptSocket.RemoteEndPoint != null) { AsyncClient client = new AsyncClient(e.AcceptSocket, BufferSize); client.DataRecieved += (s2, e2) => { if (DataRecieved != null) { DataRecieved(this, e2); } }; client.DataSend += (s2, e2) => { if (DataSend != null) { DataSend(this,e2); } }; //抛出远程Socket连接事件 if (RemoteSocketAccept != null) { RemoteSocketAccept(this, new AsyncConnectionEventArgs(null, client)); } e.AcceptSocket = null; Server.AcceptAsync(AcceptEventArgs); } } #endregion } }