從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
	