刚接触Silverlight的时候,除了其异步应用WCF、流媒体、动画效果等方面外,Socket是最另我兴奋的功能。
在Web上实现Socket虽然不是什么新鲜事了,Activex,flash等都可以实现这样的效果,但是Silverlight这样方便的运用Socket让服务器与客户端通信确是我之前没有体验过的。
用它可以做什么?可以连线式的让服务器与客户端交互,而且,是在Web上,那么Web开发游戏,语音,视频聊天等都可以基于Socket功能实现,另外,服务器端是独立出来的,不依赖IIS进程,这样让数据之间的交互更自由。
废话不说,下面来看看如何实现
首先,在进行数据交换之前,我们必须明白Silverlight Socket的一些规矩和原则。
Silverlight客户端的Socket都是异步的,这点很容易明白,另外就是,考虑到Silverlight是应用到Web上的,而Silverlight的Socket自然就有一些安全限制。
每一个请求到服务器端的新的Socket连接会话Silverlight都会先悄悄的用另一个Socket去请求策略文件,这是很多刚接触 Silverlight Socket的人感到郁闷的地方,请求策略时,Silverlight会自己发送一个字符串<policy-file-request/>到 服务器的943端口,然后你必须在服务器程序里接收该请求,分析是否是策略请求后,发送一个策略文件的字符串给客户端,客户端接收到策略文件后自己分析完 后再发送程序员自己写的数据请求。
客户端的策略请求是自动发送的,策略文件的接收和分析也是自动的,是Silverlight自发工作的,不需要程序员手工写代码进行发送接收和分析。
但是,服务器端接收策略请求需要手工完成,程序员必须创建一个Socket监听943端口(该端口是固定的,客户端策略请求固定发送到该端口),然后分析请求过来的数据是否是策略请求,如果是的,那么就读取策略文件,再将该策略文件发送到客户端就可以了。
另外一个限制,Silverlight Socket 数据交换端口必须在4502-4534范围,也就是说,整个Socket将用到两个端口,一个是943用于策略请求,另一个是4502-4534范围的你指定的数据交换端口。
不管你的Socket代码是如何工作,第一次在连接之前,Silverlight都会发送策略请求,只有成功接收到服务器返回的策略文件后,你的 Socket代码才能进行工作,所以在第一次连接的时候,实际上Silverlight是进行了两次Socket,第一次请求策略,成功才进行你的 Socket,因此,服务器端必要监听两个端口,但是两个监听可以分开在两个线程上工作(两个线程,不是两个进程)。每个会话请求一次策略后,之后的请求 就不会再请求策略了,所以他们不能是线性的工作,而是两个独立的监听,否则会阻塞。
我的服务器端的策略监听和数据监听是用的两个子线程运行,而MS的示例是用的异步方法,都是为了不相互阻塞,用MS的方式也许更有效率些,而我是为了让代码更容易理解。
客户端实现了将文本框的内容发送到服务器端,然后服务器收到后显示出来,然后发回一句字符串,关闭连接,客户端收到服务器端的信息后也关闭连接。就这么简单
好后,具体看看示例,说明很详细。
客户端
建立一个Silverlight项目
XAML
< UserControl x : Class ="SilverlightTest.Socket1"
xmlns ="http://schemas.microsoft.com/client/2007 "
xmlns : x ="http://schemas.microsoft.com/winfx/2006/xaml "
Width ="400" Height ="300">
< Grid x : Name ="LayoutRoot" Background ="White" ShowGridLines ="True">
< Grid.RowDefinitions >
< RowDefinition />
< RowDefinition />
</ Grid.RowDefinitions >
< TextBox x : Name ="txtToSend" Grid.Row ="0"/>
< Button Grid.Row ="1" Click ="OnSend" Content ="Send" Margin ="20" />
</ Grid >
</ UserControl >
代码:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using System.Net.Sockets;
using System.Threading;
using System.Text;
namespace SilverlightTest
{
public partial class Socket1 : UserControl
{
public Socket1()
{
InitializeComponent();
}
// 定义一个可在全局使用的Socket
System.Net.Sockets.Socket socket;
// 定义一个同步上下文类,用来将子线程的操作调度到主线程上以可控制UI 属性。
SynchronizationContext syn;
// 发送信息按钮的单击事件
void OnSend(object sender, EventArgs args)
{
// 定义一个字节数组,并将文本框的的类容转换为字节数组后存入
byte [] bytes = Encoding .UTF8.GetBytes(txtToSend.Text);
// 显示信息,可不要。
txtToSend.Text += "\r\nDnsSafeHost:" +Application .Current.Host.Source.DnsSafeHost;
// 将同步上下文设置在当前上下文(线程,主线程,可控制UI 的)
syn = SynchronizationContext .Current;
// 为socket 创建示例,并设置相关属性。
socket = new System.Net.Sockets.Socket (AddressFamily .InterNetwork, SocketType .Stream,ProtocolType .Tcp);
// 定义并实例一个Socket 参数
SocketAsyncEventArgs socketArgs = new SocketAsyncEventArgs ();
// 设置到远程终节点属性(4502 端口,为什么是4502 ,MS 的SL 通信安全上有)
socketArgs.RemoteEndPoint = new DnsEndPoint (Application .Current.Host.Source.DnsSafeHost, 4502);
// 设置好当Socket 任何一个动作完成时的回调函数。
socketArgs.Completed += new EventHandler <SocketAsyncEventArgs >(socketArgs_Completed);
//Socket 参数的用户标识,实际上就是一个可以传递的OBJECT 参数。
socketArgs.UserToken = bytes;
// 执行连接。
socket.ConnectAsync(socketArgs);
}
void socketArgs_Completed(object sender, SocketAsyncEventArgs e)
{
// 当任何一个Socket 动作完成,都回调该函数,然后对LastOperation 进行判断后继续执行相应的部分
switch (e.LastOperation)
{
case SocketAsyncOperation .Connect:
ProcessConnect(e);
break ;
case SocketAsyncOperation .Receive:
ProcessReceive(e);
break ;
case SocketAsyncOperation .Send:
ProcessSend(e);
break ;
}
}
// 将数据放入buffer 并进行异步发送
void ProcessConnect(SocketAsyncEventArgs e)
{
// 当连接成功后,获取Socket 参数 e 传递过来的用户标识(也就是本示例中用户输入的字符串转换的Byte 字节数组)
byte [] bytes = (byte [])e.UserToken;
// 设置Socket 参数的缓冲区参数,将我们的字节数组设置为Socket 的缓冲区。
e.SetBuffer(bytes, 0, bytes.Length);
// 同步一下上下文,显示一下当前的状态信息。
syn.Post(GetText,"States:" +e.SocketError.ToString()+"," +e.LastOperation.ToString());
// 发送数据
socket.SendAsync(e);
}
// 发送完成后,执行等待接收服务器发回的数据
void ProcessSend(SocketAsyncEventArgs e)
{
// 定义个空的字节数组,设置好其大小
byte [] bytes = new byte [1024];
// 将前面定义字节数组设置成缓冲区
e.SetBuffer(bytes, 0, bytes.Length);
// 执行异步接收
socket.ReceiveAsync(e);
}
// 当接收完成后
void ProcessReceive(SocketAsyncEventArgs e)
{
// 在执行好接收后,本地SOCKET 的缓冲区就会被服务器发送的数据填充。
// 显示下信息,当然也是用同步上下文的方式,在显示信息的时候,就直接将缓冲区的字节数组转换成字符串。
syn.Post(GetText, Encoding .UTF8.GetString(e.Buffer, 0,e.Buffer.Length)+" and Received" );
// 关闭Socket 连接
socket.Close();
// 最后显示下,Socket 关闭。
syn.Post(GetText,"Socket Closed" );
}
// 同步上下文调用的方法。
void GetText(object str)
{
txtToSend.Text +="\r\n" + str.ToString();
}
}
}
服务器端,创建一个控制台项目
代码:
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 ConsoleApp
{
class Program
{
static void Main(string [] args)
{
Console .WriteLine("================Socket 服务开启======================" );
// 建立一个子线程,用于创建Socket 来监听策略请求和发送。
ThreadStart pcts = new ThreadStart (PolicyThread);
Thread policythread = new Thread (pcts);
policythread.Start();
// 建立一个子线程,用于创建Socket 来监听信息请求和发送。
ThreadStart infots = new ThreadStart (InfoThread);
Thread infothread = new Thread (infots);
infothread.Start();
}
// 监听策略请求和发送策略请求方法
static void PolicyThread()
{
// 创建一个Socket 用来监听943 (固定的)端口的策略请求
Socket policy = new Socket (AddressFamily .InterNetwork, SocketType .Stream, ProtocolType .Tcp);
policy.Bind(new IPEndPoint (IPAddress .Any, 943));
policy.Listen(10);
// 无限循环监听
while (true )
{
if (policy.Blocking)// 如果Socket 是阻止模式的(这个东西实际上可以用不)
{
// 创建Socket ,用来获取监听Socket 的第一个Socket 链接
Socket _policy = policy.Accept();
// 定义一个字符串,该字符串与Silverlight 发送过来的请求字符串一样。
string policyRequestString = "<policy-file-request/>" ;
// 定义一个字节数组
byte [] b = new byte [policyRequestString.Length];
// 将客户端发送过来,服务器接收到的字节数组存入b 中
_policy.Receive(b);
// 将接收到的字节数组转换成字符串
string requeststring = System.Text.Encoding .UTF8.GetString(b, 0, b.Length);
// 显示客户端发送的字符串
Console .WriteLine(requeststring);
// 比对客户端发送过来的字符串是否和之前定义的额定好的策略请求字符串相同,如果相同,说明该请求是一个策略请求。
if (requeststring == policyRequestString)
{
// 如果客户端发送的是一个策略请求,服务器发送策略文件到客户端
SendPolicy(_policy);
Console .WriteLine("Policy File have sended" );
// 关闭当前连接Socket
_policy.Close();
}
else // 否则,显示错误
{
Console .WriteLine("not a sure request string!" );
}
}
}
}
// 监听信息请求和发送信息方法
static void InfoThread()
{
// 创建一个Socket 用于监听4502 端口,获取接收客户端发送过来的信息
Socket socket = new Socket (AddressFamily .InterNetwork, SocketType .Stream, ProtocolType .Tcp);
socket.Bind(new IPEndPoint (IPAddress .Any, 4502));
socket.Listen(10);
// 无线循环监听
while (true )
{
// 创建Socket ,用来获取监听Socket 的第一个Socket 链接
Socket _socket = socket.Accept();
// 创建一个空字节数组
byte [] b2 = new byte [1024];
// 将接受到的字节数组存入到之前定义的b2 字节数组中。
_socket.Receive(b2);
// 显示接收到的信息
Console .WriteLine(Encoding .UTF8.GetString(b2));
// 发回一个信息给客户端,该信息是字节数组,所以我们将信息字符串转换成字节数组
_socket.Send(Encoding .UTF8.GetBytes("This Send Over!!" ));
// 关闭当前Socket 连接
_socket.Close();
}
}
// 发送策略文件的方法
// 参数是传递进来的Socket 连接
static void SendPolicy(Socket socket)
{
// 创建一个文件流,该文件留指定代开一个策略文件,至于策略文件的格式,MS 的Silverlight 有详细说明和配置方法
FileStream fs = new FileStream (@"D:\WebSites\S