原文:http://blog.csdn.net/banmuhuangci/archive/2009/05/16/4192031.aspx
刚接触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的方式也许更有效率些,而我是为了让代码更容易理解。
客户端实现了将文本框的内容发送到服务器端,然后服务器收到后显示出来,然后发回一句字符串,关闭连接,客户端收到服务器端的信息后也关闭连接。就这么简单
好后,具体看看示例,说明很详细。(yjmyzz注:周飞原文中的代码有些问题,主要是用户发送的内容长度超过指定缓冲区时,服务端/客户端接收均不完整,我这里做了些改进)
Silverlight客户端:
Xaml部分:
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="640" d:DesignHeight="480">
<Grid x:Name="LayoutRoot" Background="White" ShowGridLines="False">
<Grid.RowDefinitions >
<RowDefinition Height="*" />
<RowDefinition Height="25" />
<RowDefinition Height="200" />
</Grid.RowDefinitions>
<ScrollViewer Margin="5" Grid.Row="0">
<TextBox x:Name="txtToSend" TextWrapping="Wrap" />
</ScrollViewer>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Grid.Row="1">
<Button Click="OnSend" Content="Send" Width="80" Height="20"/>
<Button Width="80" Height="20" Margin="5,0,0,0" Click="ClearResult" Content="清除结果区"></Button>
</StackPanel>
<ScrollViewer Margin="5" Grid.Row="2">
<TextBlock x:Name="txtResult" Text="就绪" TextWrapping="Wrap"/>
</ScrollViewer>
</Grid>
</UserControl>
CS部分:
using System.Net;
using System.Net.Sockets;
using System.Collections.Generic;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Browser;
namespace SocketDemo
{
//socket通讯 silverlight客户端示例(最终修改:菩提树下的杨过 2009-11-28 http://yjmyzz.cnblogs.com/)
public partial class MainPage : UserControl
{
//定义一个可在全局使用的Socket
Socket socket;
string splitChar = "^";
List<byte> _listReceive = new List<byte>(1024);
public MainPage()
{
InitializeComponent();
//事先生成一些字符串,省得打字 :) 可去掉
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10; i++)
{
for (int j = 0; j < 1024; j++)
{
sb.Append(i);
}
}
txtToSend.Text = sb.ToString();
}
//发送信息按钮的单击事件
void OnSend(object sender, EventArgs eventArgs)
{
if (txtToSend.Text.Trim().Length == 0)
{
HtmlPage.Window.Alert("请输入发送内容!");
txtToSend.Focus();
return;
}
//定义一个字节数组,并将文本框的的类容转换为字节数组后存入
byte[] bytes = Encoding.UTF8.GetBytes(splitChar + txtToSend.Text.Trim().Replace(splitChar,"") + splitChar);
//为socket创建示例,并设置相关属性。
socket = new System.Net.Sockets.Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
//定义并实例一个Socket参数
SocketAsyncEventArgs args = new SocketAsyncEventArgs();
//设置到远程终节点属性(实际使用中,请将下面的Ip地址换成scoket服务器所在的IP)
args.RemoteEndPoint = new DnsEndPoint("127.0.0.1", 4502);
//设置好当Socket任何一个动作完成时的回调函数。
args.Completed += new EventHandler<SocketAsyncEventArgs>(ConnectComplete);
//Socket参数的用户标识,实际上就是一个可以传递的OBJECT参数。
args.UserToken = bytes;
//执行连接。
socket.ConnectAsync(args);
}
/// <summary>
/// 连接完成的回调函数
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
void ConnectComplete(object sender, SocketAsyncEventArgs e)
{
//当连接成功后,获取Socket参数 e传递过来的用户标识(也就是本示例中用户输入的字符串转换的Byte字节数组)
byte[] bytes = (byte[])e.UserToken;
//同步一下上下文,显示一下当前的状态信息。
GetText("连接状态:" + e.SocketError.ToString() + ",操作:" + e.LastOperation.ToString());
if (e.SocketError != SocketError.Success)
{
return;
}
SocketAsyncEventArgs args = new SocketAsyncEventArgs();
args.RemoteEndPoint = e.RemoteEndPoint;
//设置Socket参数的缓冲区参数,将我们的字节数组设置为Socket的缓冲区。
args.SetBuffer(bytes, 0, bytes.Length);
args.Completed += new EventHandler<SocketAsyncEventArgs>(SendComplete);
//发送数据
socket.SendAsync(args);
}
/// <summary>
/// 发送完成的回调函数
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
void SendComplete(object sender, SocketAsyncEventArgs e)
{
GetText("发送状态:" + e.SocketError.ToString() + ",操作:" + e.LastOperation.ToString());
//执行异步接收
SocketAsyncEventArgs args = new SocketAsyncEventArgs();
args.RemoteEndPoint = e.RemoteEndPoint;
byte[] buffer = new byte[1024];
args.SetBuffer(buffer, 0, buffer.Length);
args.Completed += new EventHandler<SocketAsyncEventArgs>(ReceiveComplate);
socket.ReceiveAsync(args);
}
/// <summary>
/// 接收完成的回调函数
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
void ReceiveComplate(object sender, SocketAsyncEventArgs e)
{
GetText("接收状态:" + e.SocketError.ToString() + ",操作:" + e.LastOperation.ToString());
char _splitChar = splitChar.ToCharArray()[0];
bool _IsReceiveEnd = false;
if (e.Buffer[e.Offset] == _splitChar && e.BytesTransferred > 0) //验证开头是否以分隔符开始
{
for (int i = e.Offset + 1; i < e.BytesTransferred; i++)
{
if (e.Buffer[i] == _splitChar) //遇到结束符号
{
_IsReceiveEnd = true;
break;
}
_listReceive.Add(e.Buffer[i]);
}
}
else
{
_IsReceiveEnd = true;
}
if (_IsReceiveEnd)
{
string _Content = UTF8Encoding.UTF8.GetString(_listReceive.ToArray(),0,_listReceive.Count);
_listReceive.Clear();
_listReceive = new List<byte>(1024);
GetText(_Content);
socket.Close();
socket = null;
GetText("本次socket通讯完成,socket对象已关闭!");
this.Dispatcher.BeginInvoke(() => { this.txtToSend.Text = ""; });
}
else
{
//继续接收
SocketAsyncEventArgs args = new SocketAsyncEventArgs();
args.RemoteEndPoint = e.RemoteEndPoint;
byte[] buffer = new byte[1024];
args.SetBuffer(buffer, 0, buffer.Length);
args.Completed += new EventHandler<SocketAsyncEventArgs>(ReceiveNext);
socket.ReceiveAsync(args);
}
}
/// <summary>
/// 继续接受
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
void ReceiveNext(object sender, SocketAsyncEventArgs e)
{
GetText("接收状态:" + e.SocketError.ToString() + ",操作:" + e.LastOperation.ToString());
char _splitChar = splitChar.ToCharArray()[0];
bool _IsReceiveEnd = false;
for (int i = e.Offset ; i < e.BytesTransferred; i++)
{
if (e.Buffer[i] == _splitChar) //遇到结束符号
{
_IsReceiveEnd = true;
break;
}
_listReceive.Add(e.Buffer[i]);
}
if (_IsReceiveEnd)
{
string _Content = UTF8Encoding.UTF8.GetString(_listReceive.ToArray(), 0, _listReceive.Count);
_listReceive.Clear();
_listReceive = new List<byte>(1024);
GetText(_Content);
socket.Close();
socket = null;
GetText("本次socket通讯完成,socket对象已关闭!");
this.Dispatcher.BeginInvoke(() => { this.txtToSend.Text = ""; });
}
else
{
//继续接收
SocketAsyncEventArgs args = new SocketAsyncEventArgs();
args.RemoteEndPoint = e.RemoteEndPoint;
byte[] buffer = new byte[1024];
args.SetBuffer(buffer, 0, buffer.Length);
args.Completed += new EventHandler<SocketAsyncEventArgs>(ReceiveNext);
socket.ReceiveAsync(args);
}
}
//同步上下文调用的方法。
void GetText(object str)
{
//异步操作中,无法直接用txtResult.Text="xxx"来赋值,会报异常
this.Dispatcher.BeginInvoke(() => { this.txtResult.Text = str.ToString() + "\r\n" + this.txtResult.Text; });
}
/// <summary>
/// 清空结果区
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void ClearResult(object sender, RoutedEventArgs e)
{
this.Dispatcher.BeginInvoke(() => { this.txtResult.Text = ""; });
}
}
}
Console服务端:
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Windows.Forms;
namespace SocketServer
{
/// <summary>
/// socket服务端示例(最终修改:菩提树下的杨过 2009-11-28 http://yjmyzz.cnblogs.com/)
/// </summary>
class Program
{
static char splitChar = '^';
static void Main(string[] args)
{
Console.WriteLine("================Socket服务开启======================");
Thread tPolicy = new Thread(PolicyListen);//943策略监听线程
tPolicy.Start();
Thread tMsg = new Thread(MessageListen);
tMsg.Start();
}
//监听策略请求和发送策略请求方法
static void PolicyListen()
{
//创建一个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 MessageListen()
{
//创建一个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 _client = socket.Accept();
#region 接受数据
List<byte> listReceive = new List<byte>(_client.ReceiveBufferSize);
byte[] b1 = new byte[32];
_client.Receive(b1);
if (b1[0] == splitChar)//如果是以分隔符开头
{
for (int i = 1; i < b1.Length; i++)
{
if (b1[i] == splitChar) //遇到结束符
{
goto ok;
}
else
{
listReceive.Add(b1[i]);
}
}
while (_client.Available > 0)
{
byte[] b = new byte[32];
_client.Receive(b);
for (int j = 0; j < b.Length; j++)
{
if (b[j] == splitChar) //遇到结束符号则退出
{
goto ok;
}
else
{
listReceive.Add(b[j]);
}
}
}
}
#endregion
ok: string _Content = UTF32Encoding.UTF8.GetString(listReceive.ToArray(), 0, listReceive.Count);
Console.WriteLine("收到" + _client.RemoteEndPoint + ":" + listReceive.Count + " 字节 " + DateTime.Now.ToString() + "\n" + _Content);
//发回一个信息给客户端,该信息是字节数组,所以我们将信息字符串转换成字节数组
byte[] _sendData = UTF32Encoding.UTF8.GetBytes(splitChar.ToString() + "本次Scoket通讯完成,共收到" + _client.RemoteEndPoint + "发来" + listReceive.Count + "字节的内容!(来自服务端)\r\n" + _Content.Replace(splitChar.ToString(),"") + splitChar.ToString());
_client.Send(_sendData);
//关闭当前Socket连接
_client.Close();
System.Threading.Thread.Sleep(5);
}
}
//发送策略文件的方法
//参数是传递进来的Socket连接
static void SendPolicy(Socket socket)
{
//创建一个文件流,该文件留指定代开一个策略文件,至于策略文件的格式,MS的Silverlight有详细说明和配置方法
string _policyFilePath = Application.StartupPath + "\\PolicyFile.xml";
FileStream fs = new FileStream(_policyFilePath, FileMode.Open);
int length = (int)fs.Length;
byte[] bytes = new byte[length];
//将策略文件流读到上面定义的字节数组中
fs.Read(bytes, 0, length);
//关闭文件流
fs.Close();
//其策略文件的字节数组发送给客户端
socket.Send(bytes, length, SocketFlags.None);
}
}
}
服务端策略文件:
<access-policy>
<cross-domain-access>
<policy>
<allow-from>
<domain uri="*" />
</allow-from>
<grant-to>
<socket-resource port="4502-4506" protocol="tcp" />
</grant-to>
</policy>
</cross-domain-access>
</access-policy>
源代码下载: