與眾不同 windows phone (30) - Communication(通信)之基於 Socket TCP 開發一個多人聊天室
作者:webabcd
介紹
與眾不同 windows phone 7.5 (sdk 7.1) 之通信
- 實例 - 基於 Socket TCP 開發一個多人聊天室
示例
1、服務端
ClientSocketPacket.cs
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace SocketServerTcp { /// <summary> /// 對客戶端 Socket 及其他相關信息做一個封裝 /// </summary> public class ClientSocketPacket { /// <summary> /// 客戶端 Socket /// </summary> public System.Net.Sockets.Socket Socket { get; set; } private byte[] _buffer; /// <summary> /// 為該客戶端 Socket 開辟的緩沖區 /// </summary> public byte[] Buffer { get { if (_buffer == null) _buffer = new byte[64]; return _buffer; } } private List<byte> _receivedByte; /// <summary> /// 客戶端 Socket 發過來的信息的字節集合 /// </summary> public List<byte> ReceivedByte { get { if (_receivedByte == null) _receivedByte = new List<byte>(); return _receivedByte; } } } }
Main.cs
/* * Socket TCP 聊天室的服務端 */ using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Windows.Forms; using System.Net.Sockets; using System.Net; using System.Threading; using System.IO; namespace SocketServerTcp { public partial class Main : Form { SynchronizationContext _syncContext; System.Timers.Timer _timer; // 信息結束符,用於判斷是否完整地讀取了客戶端發過來的信息,要與客戶端的信息結束符相對應(本例只用於演示,實際項目中請用自定義協議) private string _endMarker = "^"; // 服務端監聽的 socket private Socket _listener; // 實例化 ManualResetEvent,設置其初始狀態為無信號 private ManualResetEvent _signal = new ManualResetEvent(false); // 客戶端 Socket 列表 private List<ClientSocketPacket> _clientList = new List<ClientSocketPacket>(); public Main() { InitializeComponent(); // UI 線程 _syncContext = SynchronizationContext.Current; // 啟動后台線程去運行 Socket 服務 Thread thread = new Thread(new ThreadStart(LaunchSocketServer)); thread.IsBackground = true; thread.Start(); } private void LaunchSocketServer() { // 每 10 秒運行一次計時器所指定的方法,群發信息 _timer = new System.Timers.Timer(); _timer.Interval = 10000d; _timer.Elapsed += new System.Timers.ElapsedEventHandler(_timer_Elapsed); _timer.Start(); // TCP 方式監聽 3366 端口 _listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); _listener.Bind(new IPEndPoint(IPAddress.Any, 3366)); // 指定等待連接隊列中允許的最大數 _listener.Listen(10); while (true) { // 設置為無信號 _signal.Reset(); // 開始接受客戶端傳入的連接 _listener.BeginAccept(new AsyncCallback(OnClientConnect), null); // 阻塞當前線程,直至有信號為止 _signal.WaitOne(); } } private void _timer_Elapsed(object sender, System.Timers.ElapsedEventArgs e) { // 每 10 秒給所有連入的客戶端發送一次消息 SendData(string.Format("webabcd 對所有人說:大家好! 【信息來自服務端 {0}】", DateTime.Now.ToString("hh:mm:ss"))); } private void OnClientConnect(IAsyncResult async) { ClientSocketPacket client = new ClientSocketPacket(); // 完成接受客戶端傳入的連接的這個異步操作,並返回客戶端連入的 socket client.Socket = _listener.EndAccept(async); // 將客戶端連入的 Socket 放進客戶端 Socket 列表 _clientList.Add(client); OutputMessage(((IPEndPoint)client.Socket.LocalEndPoint).Address + " 連入了服務器"); SendData("一個新的客戶端已經成功連入服務器。。。 【信息來自服務端】"); try { // 開始接收客戶端傳入的數據 client.Socket.BeginReceive(client.Buffer, 0, client.Buffer.Length, SocketFlags.None, new AsyncCallback(OnDataReceived), client); } catch (SocketException ex) { // 處理異常 HandleException(client, ex); } // 設置為有信號 _signal.Set(); } private void OnDataReceived(IAsyncResult async) { ClientSocketPacket client = async.AsyncState as ClientSocketPacket; int count = 0; try { // 完成接收數據的這個異步操作,並返回接收的字節數 if (client.Socket.Connected) count = client.Socket.EndReceive(async); } catch (SocketException ex) { HandleException(client, ex); } // 把接收到的數據添加進收到的字節集合內 // 本例采用 UTF8 編碼,中文占用 3 字節,英文等字符與 ASCII 相同 foreach (byte b in client.Buffer.Take(count)) { if (b == 0) continue; // 如果是空字節則不做處理('\0') client.ReceivedByte.Add(b); } // 把當前接收到的數據轉換為字符串。用於判斷是否包含自定義的結束符 string receivedString = UTF8Encoding.UTF8.GetString(client.Buffer, 0, count); // 如果該 Socket 在網絡緩沖區中沒有排隊的數據 並且 接收到的數據中有自定義的結束符時 if (client.Socket.Connected && client.Socket.Available == 0 && receivedString.Contains(_endMarker)) { // 把收到的字節集合轉換成字符串(去掉自定義結束符) // 然后清除掉字節集合中的內容,以准備接收用戶發送的下一條信息 string content = UTF8Encoding.UTF8.GetString(client.ReceivedByte.ToArray()); content = content.Replace(_endMarker, ""); client.ReceivedByte.Clear(); // 發送數據到所有連入的客戶端,並在服務端做記錄 SendData(content); OutputMessage(content); } try { // 繼續開始接收客戶端傳入的數據 if (client.Socket.Connected) client.Socket.BeginReceive(client.Buffer, 0, client.Buffer.Length, 0, new AsyncCallback(OnDataReceived), client); } catch (SocketException ex) { HandleException(client, ex); } } /// <summary> /// 發送數據到所有連入的客戶端 /// </summary> /// <param name="data">需要發送的數據</param> private void SendData(string data) { byte[] byteData = UTF8Encoding.UTF8.GetBytes(data); foreach (ClientSocketPacket client in _clientList) { if (client.Socket.Connected) { try { // 如果某客戶端 Socket 是連接狀態,則向其發送數據 client.Socket.BeginSend(byteData, 0, byteData.Length, SocketFlags.None, new AsyncCallback(OnDataSent), client); } catch (SocketException ex) { HandleException(client, ex); } } else { // 某 Socket 斷開了連接的話則將其關閉,並將其清除出客戶端 Socket 列表 // 也就是說每次向所有客戶端發送消息的時候,都會從客戶端 Socket 集合中清除掉已經關閉了連接的 Socket client.Socket.Close(); _clientList.Remove(client); } } } private void OnDataSent(IAsyncResult async) { ClientSocketPacket client = async.AsyncState as ClientSocketPacket; try { // 完成將信息發送到客戶端的這個異步操作 int sentBytesCount = client.Socket.EndSend(async); } catch (SocketException ex) { HandleException(client, ex); } } /// <summary> /// 處理 SocketException 異常 /// </summary> /// <param name="client">導致異常的 ClientSocketPacket</param> /// <param name="ex">SocketException</param> private void HandleException(ClientSocketPacket client, SocketException ex) { // 在服務端記錄異常信息,關閉導致異常的 Socket,並將其清除出客戶端 Socket 列表 OutputMessage(client.Socket.RemoteEndPoint.ToString() + " - " + ex.Message); client.Socket.Close(); _clientList.Remove(client); } // 在 UI 上輸出指定信息 private void OutputMessage(string data) { _syncContext.Post((p) => { txtMsg.Text += p.ToString() + "\r\n"; }, data); } } }
2、客戶端
TcpDemo.xaml
<phone:PhoneApplicationPage x:Class="Demo.Communication.SocketClient.TcpDemo" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:phone="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone" xmlns:shell="clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" FontFamily="{StaticResource PhoneFontFamilyNormal}" FontSize="{StaticResource PhoneFontSizeNormal}" Foreground="{StaticResource PhoneForegroundBrush}" SupportedOrientations="Portrait" Orientation="Portrait" mc:Ignorable="d" d:DesignHeight="768" d:DesignWidth="480" shell:SystemTray.IsVisible="True"> <Grid x:Name="LayoutRoot" Background="Transparent"> <StackPanel HorizontalAlignment="Left"> <ScrollViewer x:Name="svChat" Height="400"> <TextBlock x:Name="txtChat" TextWrapping="Wrap" /> </ScrollViewer> <TextBox x:Name="txtName" /> <TextBox x:Name="txtInput" KeyDown="txtInput_KeyDown" /> <Button x:Name="btnSend" Content="發送" Click="btnSend_Click" /> </StackPanel> </Grid> </phone:PhoneApplicationPage>
TcpDemo.xaml.cs
/* * Socket TCP 聊天室的客戶端 */ 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 Microsoft.Phone.Controls; using System.Net.Sockets; using System.Text; // 此命名空間下有 Socket 的擴展方法 GetCurrentNetworkInterface() using Microsoft.Phone.Net.NetworkInformation; namespace Demo.Communication.SocketClient { public partial class TcpDemo : PhoneApplicationPage { // 信息結束符,用於判斷是否完整地讀取了服務端發過來的信息,要與服務端的信息結束符相對應(本例只用於演示,實際項目中請用自定義協議) private string _endMarker = "^"; // 客戶端 Socket private Socket _socket; // 用於發送數據到服務端的 Socket 異步操作對象 private SocketAsyncEventArgs _socketAsyncSend; // 用於接收數據的 Socket 異步操作對象 private SocketAsyncEventArgs _socketAsyncReceive; public TcpDemo() { InitializeComponent(); this.Loaded += new RoutedEventHandler(TcpDemo_Loaded); } void TcpDemo_Loaded(object sender, RoutedEventArgs e) { // 初始化姓名和需要發送的默認文字 txtName.Text = "匿名用戶" + new Random().Next(0, 9999).ToString().PadLeft(4, '0'); txtInput.Text = "hi"; // 實例化 Socket _socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); // 設置 Socket 連接的首選網絡類型 NetworkSelectionCharacteristics.Cellular 或 NetworkSelectionCharacteristics.NonCellular _socket.SetNetworkPreference(NetworkSelectionCharacteristics.NonCellular); // 強制 Socket 連接的網絡類型 NetworkSelectionCharacteristics.Cellular 或 NetworkSelectionCharacteristics.NonCellular(不指定的話則均可) // 如果無法使用強制要求的網絡類型,則在 OnSocketConnectCompleted 會收到 SocketError.NetworkDown _socket.SetNetworkRequirement(NetworkSelectionCharacteristics.NonCellular); // 實例化 SocketAsyncEventArgs ,用於對 Socket 做異步操作,很方便 _socketAsyncReceive = new SocketAsyncEventArgs(); // 服務器的 EndPoint _socketAsyncReceive.RemoteEndPoint = new DnsEndPoint("192.168.8.217", 3366); // 異步操作完成后執行的事件 _socketAsyncReceive.Completed += new EventHandler<SocketAsyncEventArgs>(OnSocketConnectCompleted); // 異步連接服務端 _socket.ConnectAsync(_socketAsyncReceive); } private void OnSocketConnectCompleted(object sender, SocketAsyncEventArgs e) { if (e.SocketError != SocketError.Success) { OutputMessage("Socket 連接錯誤:" + e.SocketError.ToString()); return; } // 設置數據緩沖區 byte[] response = new byte[1024]; e.SetBuffer(response, 0, response.Length); // 修改 SocketAsyncEventArgs 對象的異步操作完成后需要執行的事件 e.Completed -= new EventHandler<SocketAsyncEventArgs>(OnSocketConnectCompleted); e.Completed += new EventHandler<SocketAsyncEventArgs>(OnSocketReceiveCompleted); // 異步地從服務端 Socket 接收數據 _socket.ReceiveAsync(e); // 構造一個 SocketAsyncEventArgs 對象,用於用戶向服務端發送消息 _socketAsyncSend = new SocketAsyncEventArgs(); _socketAsyncSend.RemoteEndPoint = e.RemoteEndPoint; if (_socket.Connected) { OutputMessage("成功地連接上了服務器。。。"); // Socket 有一個擴展方法 GetCurrentNetworkInterface(),需要引用命名空間 Microsoft.Phone.Net.NetworkInformation // GetCurrentNetworkInterface() 會返回當前 Socket 連接的 NetworkInterfaceInfo 對象(NetworkInterfaceInfo 的詳細說明參見:Device/Status/NetworkStatus.xaml.cs) NetworkInterfaceInfo nii = _socket.GetCurrentNetworkInterface(); OutputMessage("網絡接口的類型:" + nii.InterfaceType.ToString()); } else { OutputMessage("無法連接到服務器。。。請刷新后再試。。。"); } } private void OnSocketReceiveCompleted(object sender, SocketAsyncEventArgs e) { try { // 將接收到的數據轉換為字符串 string data = UTF8Encoding.UTF8.GetString(e.Buffer, e.Offset, e.BytesTransferred); OutputMessage(data); } catch (Exception ex) { OutputMessage(ex.ToString()); } // 繼續異步地從服務端接收數據 _socket.ReceiveAsync(e); } private void OutputMessage(string data) { // 在聊天文本框中輸出指定的信息,並將滾動條滾到底部 this.Dispatcher.BeginInvoke( delegate { txtChat.Text += data + "\r\n"; svChat.ScrollToVerticalOffset(txtChat.ActualHeight - svChat.Height); } ); } private void SendData() { if (_socket.Connected) { // 設置需要發送的數據的緩沖區 _socketAsyncSend.BufferList = new List<ArraySegment<byte>>() { new ArraySegment<byte>(UTF8Encoding.UTF8.GetBytes(txtName.Text + ":" + txtInput.Text + _endMarker)) }; // 異步地向服務端發送消息 _socket.SendAsync(_socketAsyncSend); } else { txtChat.Text += "無法連接到服務器。。。請刷新后再試。。。\r\n"; _socket.Close(); } txtInput.Focus(); txtInput.Text = ""; } private void btnSend_Click(object sender, RoutedEventArgs e) { SendData(); } private void txtInput_KeyDown(object sender, KeyEventArgs e) { if (e.Key == Key.Enter) { SendData(); this.Focus(); } } } }
OK
[源碼下載]