從Windows Phone 8 開始,Socket 的支援擴大到允許開發者撰寫Socket Server,也就是說,在不透過中介服務的情況下,開發者可以撰寫一個Windows Phone Server Application來允許其它裝置連入,
這在開發對戰連線遊戲時是個非常有用的機制。
文/黃忠成
Socket Support in Windows Phone 8
從Windows Phone 7.1開始便支援了Socket Programming,開發者可以直接使用Socket來開發需要快速通訊的網路應用程式,不過那時Socket僅支援到Client-Side,簡單的說就是你無法在Windows Phone上建立一個Socket Server
來接入其它的Windows Phone App連線,必須透過一個由其它平台所提供的中介服務(像是Windows Client、網站等等),這大大的限制了Windows Phone Apps Socket的應用範圍。
從Windows Phone 8 開始,Socket 的支援擴大到允許開發者撰寫Socket Server,也就是說,在不透過中介服務的情況下,開發者可以撰寫一個Windows Phone Server Application來允許其它裝置連入,
這在開發對戰連線遊戲時是個非常有用的機制。
A Simple Socket Listener
用法很簡單,你只需要利用StreamSocketListener來建立一個Server Socket,就可以接入來自其它裝置的連線,如以下程式。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Navigation;
using Microsoft.Phone.Controls;
using Microsoft.Phone.Shell;
using SocketServer.Resources;
using Windows.Networking.Sockets;
using Windows.Storage.Streams;
using Windows.Networking;
using Windows.Networking.Connectivity;
using System.Text;
namespace SocketServer
{
public class ListenInformation
{
public HostName Host { get; set; }
public string DisplayName { get; set; }
}
public partial class MainPage : PhoneApplicationPage
{
private StreamSocketListener _listener = new StreamSocketListener();
private List _ips = new List();
// Constructor
public MainPage()
{
InitializeComponent();
var hostNames = NetworkInformation.GetHostNames();
foreach (HostName hn in hostNames)
{
ListenInformation info = new ListenInformation() { Host = hn };
if (hn.IPInformation.NetworkAdapter.IanaInterfaceType == 71) //Wi-Fi
info.DisplayName = hn.CanonicalName + "(Wi-Fi)";
else if (hn.IPInformation.NetworkAdapter.IanaInterfaceType == 244) //cellular
info.DisplayName = hn.CanonicalName + "(cellular)";
else
info.DisplayName = hn.CanonicalName;
_ips.Add(info);
}
lstIPs.DisplayMemberPath = "DisplayName";
lstIPs.ItemsSource = _ips;
}
async private void WaitForData(StreamSocket socket)
{
var dr = new DataReader(socket.InputStream);
var dataReaded = await dr.LoadAsync(sizeof(uint));
if (dataReaded == 0)
return;
uint strLen = dr.ReadUInt32();
await dr.LoadAsync(strLen);
string msg = dr.ReadString(strLen);
if (msg.ToLower().Equals("bye"))
{
socket.Dispose();
return;
}
Dispatcher.BeginInvoke(() =>
{
lstOutput.Items.Add("recv:" + msg);
});
WaitForData(socket);
}
void listener_ConnectionReceived(StreamSocketListener sender, StreamSocketListenerConnectionReceivedEventArgs args)
{
WaitForData(args.Socket);
}
private async void Button_Click(object sender, RoutedEventArgs e)
{
if (lstIPs.SelectedItem != null)
{
((Button)sender).IsEnabled = false;
_listener.ConnectionReceived += listener_ConnectionReceived;
await _listener.BindEndpointAsync(((ListenInformation)lstIPs.SelectedItem).Host, "1067");
appTitle.Text = "listen on " + _listener.Information.LocalPort;
}
else
MessageBox.Show("please select a ip to listen.");
}
}
}
讓我稍微解釋一下這個程式幾個關鍵片段。
public MainPage()
{
InitializeComponent();
var hostNames = NetworkInformation.GetHostNames();
foreach (HostName hn in hostNames)
{
ListenInformation info = new ListenInformation() { Host = hn };
if (hn.IPInformation.NetworkAdapter.IanaInterfaceType == 71) //Wi-Fi
info.DisplayName = hn.CanonicalName + "(Wi-Fi)";
else if (hn.IPInformation.NetworkAdapter.IanaInterfaceType == 244) //cellular
info.DisplayName = hn.CanonicalName + "(cellular)";
else
info.DisplayName = hn.CanonicalName;
_ips.Add(info);
}
lstIPs.DisplayMemberPath = "DisplayName";
lstIPs.ItemsSource = _ips;
}
理論上,每個Windows Phone 8裝置應該都會有一個以上的網路位址,除非該裝置沒有數據連線及Wi-Fi,因此要起始一個Socket Server,得先決定要繫結至哪個網路位址,這裡透過GetHostNames()來取得所有可用的網路位址,
然後透過IanaInterfaceType來標示該網路位址的型態,一般來說, 71是Wi-Fi、244是數據連線。
private async void Button_Click(object sender, RoutedEventArgs e)
{
if (lstIPs.SelectedItem != null)
{
((Button)sender).IsEnabled = false;
_listener.ConnectionReceived += listener_ConnectionReceived;
await _listener.BindEndpointAsync(((ListenInformation)lstIPs.SelectedItem).Host, "1067");
appTitle.Text = "listen on " + _listener.Information.LocalPort;
}
else
MessageBox.Show("please select a ip to listen.");
}
當使用者選擇了要繫結的網路位址後,這裡透過BindEndpointAsync來啟用Socket Server,第一個參數是HostName,第二個則是要聆聽的Port資訊。
當連線接入時,ConnectionReceived事件會觸發,在此處透過DataReader來讀入由Client端傳入的資料。
async private void WaitForData(StreamSocket socket)
{
var dr = new DataReader(socket.InputStream);
var dataReaded = await dr.LoadAsync(sizeof(uint));
if (dataReaded == 0)
return;
uint strLen = dr.ReadUInt32();
await dr.LoadAsync(strLen);
string msg = dr.ReadString(strLen);
if (msg.ToLower().Equals("bye"))
{
socket.Dispose();
return;
}
Dispatcher.BeginInvoke(() =>
{
lstOutput.Items.Add("recv:" + msg);
});
WaitForData(socket);
}
void listener_ConnectionReceived(StreamSocketListener sender, StreamSocketListenerConnectionReceivedEventArgs args)
{
WaitForData(args.Socket);
}
Client: Windows Phone 8
完成Server端後,Client端相對簡單多了。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Navigation;
using Microsoft.Phone.Controls;
using Microsoft.Phone.Shell;
using ClientSocketDemo.Resources;
using Windows.Networking.Sockets;
using Windows.Networking;
using Windows.Storage.Streams;
namespace ClientSocketDemo
{
public partial class MainPage : PhoneApplicationPage
{
private StreamSocket _socket = null;
// Constructor
public MainPage()
{
InitializeComponent();
// Sample code to localize the ApplicationBar
//BuildLocalizedApplicationBar();
}
private async void Button_Click(object sender, RoutedEventArgs e)
{
_socket = new StreamSocket();
await _socket.ConnectAsync(new HostName(txtIP.Text), "1067");
((Button)sender).IsEnabled = false;
txtIP.IsEnabled = false;
btnSend.IsEnabled = true;
txtSendData.IsEnabled = true;
}
private async void btnSend_Click(object sender, RoutedEventArgs e)
{
DataWriter dr = new DataWriter(_socket.OutputStream);
string data = txtSendData.Text;
dr.WriteUInt32((uint)data.Length);
await dr.StoreAsync();
dr.WriteString(data);
await dr.StoreAsync();
dr.DetachStream();
}
}
}
透過ConnectAsync可以連接至Socket Server,第一個參數是HostName(IP),第二個是Port。
圖1
Client: Windows 8 App
同理,你也可以撰寫Windows Store App的客戶端來連入Windows Phone。
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Windows.Foundation;
using Windows.Foundation.Collections;
using Windows.Networking;
using Windows.Networking.Sockets;
using Windows.Storage.Streams;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Controls.Primitives;
using Windows.UI.Xaml.Data;
using Windows.UI.Xaml.Input;
using Windows.UI.Xaml.Media;
using Windows.UI.Xaml.Navigation;
namespace WinSocketClient
{
public sealed partial class MainPage : Page
{
private StreamSocket _socket = null;
public MainPage()
{
this.InitializeComponent();
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
}
private async void Button_Click(object sender, RoutedEventArgs e)
{
_socket = new StreamSocket();
await _socket.ConnectAsync(new HostName(txtIP.Text), "1067");
((Button)sender).IsEnabled = false;
txtIP.IsEnabled = false;
btnSend.IsEnabled = true;
txtSendData.IsEnabled = true;
}
private async void btnSend_Click(object sender, RoutedEventArgs e)
{
DataWriter dr = new DataWriter(_socket.OutputStream);
string data = txtSendData.Text;
dr.WriteUInt32((uint)data.Length);
await dr.StoreAsync();
dr.WriteString(data);
await dr.StoreAsync();
dr.DetachStream();
}
}
}
圖2