02、Windows Phone 套接字(Socket)实战之服务器端设计
这里主要写 PC 服务器端的逻辑,UI 使用的是 WPF,因为 WPF 比普通的 WinForm 的流式布局
更容易控制,而且比 WinForm 美观一些,显示截图:
一、页面 UI
MainWindow.xaml 文件中布局的 XAML:
<Grid ShowGridLines="True"> <Grid.Resources> <Style TargetType="Button"> <Setter Property="Width" Value="110"/> <Setter Property="Height" Value="30"/> <Setter Property="Margin" Value="10,0,10,0"/> </Style> </Grid.Resources> <Grid.ColumnDefinitions> <ColumnDefinition Width="80*"/> <ColumnDefinition Width="20*"/> </Grid.ColumnDefinitions> <Grid ShowGridLines="True"> <Grid.RowDefinitions> <RowDefinition Height="1*"/> <RowDefinition Height="7*"/> <RowDefinition Height="2*"/> </Grid.RowDefinitions> <!--本地 IP--> <StackPanel Orientation="Horizontal"> <StackPanel.Resources> <Style TargetType="TextBlock"> <Setter Property="Width" Value="120"/> <Setter Property="VerticalAlignment" Value="Center"/> <Setter Property="HorizontalAlignment" Value="Center"/> </Style> </StackPanel.Resources> <!--服务器端的 IP 和端口号--> <TextBlock x:Name="txtIP" Margin="4,14,0,14" Width="144"/> <TextBlock x:Name="txtPort" Text="5000" Margin="10, 0, 0, 0"/> <Button x:Name="btnBeginListening" Content="开始侦听" HorizontalAlignment="Right" Click="btnBeginListening_Click"/> <!--<Button x:Name="btnStop" Content="停止" HorizontalAlignment="Right" Click="btnStop_Click"/>--> </StackPanel> <!--消息窗口和消息发送窗口--> <Grid Grid.Row="1"> <Grid.RowDefinitions> <RowDefinition Height="8*"/> <RowDefinition Height="2*"/> </Grid.RowDefinitions> <ScrollViewer x:Name="scroll"> <!--显示连接状态、聊天消息等--> <TextBlock x:Name="txtResult" TextWrapping="Wrap"/> </ScrollViewer> <!--聊天文字输入框--> <TextBox x:Name="txtInput" Grid.Row="1" KeyDown="txtInput_KeyDown" TextWrapping="Wrap"/> </Grid> <!--操作按钮--> <Grid Grid.Row="2"> <Grid.ColumnDefinitions> <ColumnDefinition Width="5*"/> <ColumnDefinition Width="5*"/> </Grid.ColumnDefinitions> <StackPanel Orientation="Horizontal"> <!--用户选择的文件的路径--> <TextBox x:Name="txtFilePath" Width="150" Height="40"/> <Button Content="选择文件" x:Name="btnChooseFile" Click="btnChooseFile_Click"/> </StackPanel> <StackPanel HorizontalAlignment="Right" Orientation="Horizontal" Grid.Column="1"> <!--发送文件或者发送文字消息--> <Button Content="发送文件" x:Name="btnSendFile" Click="btnSendFile_Click"/> <Button Content="发送消息" x:Name="btnSendMsg" Click="btnSendMsg_Click"/> </StackPanel> </Grid> </Grid> <!--显示请求连接到服务器端的 WP 端的 IP 信息--> <ListBox Grid.Column="1" x:Name="listbox"> </ListBox> </Grid>
二、定义一个服务器端的 SocketClient 类,用来封装与 WP 端通讯所使用的 Socket
虽然 WP 和 PC 端都是使用 C# 语言开发的,但是 WP 端和 PC 端的 Socket 类在使用方法上有一些差异,
并且 WP 端明显是经过精简过的,这里就不贴出来了。直接贴出 SocketClient 类:
namespace DesktopSocketServerDemo { /// <summary> /// 与客户端的 连接通信类(包含了一个 与客户端 通信的 套接字,和线程) /// </summary> public class SocketClient { Socket sokMsg; Thread threadMsg; // 通信操作完成时调用,用来触发在操作完成时,在宿主页面显示消息提示 public event EventHandler<string> Completed; public SocketClient(Socket sokMsg) { this.sokMsg = sokMsg; this.threadMsg = new Thread(ReceiveMsg); this.threadMsg.IsBackground = true; this.threadMsg.Start(); } bool isRec = true; // 负责监听客户端发送来的消息 void ReceiveMsg() { while (isRec) { try { byte[] byteSrc = new byte[CommonHelper.FileLength]; // 从绑定的 Socket 套接字接收数据,将数据存入接收缓冲区。 int length = sokMsg.Receive(byteSrc); DataType dataType; byte[] bytesFile; string strMsg; // 转换源 byte[] 数据内容,获取其中的 head 和 body 的实际内容 CommonHelper.ConvertByteToData(byteSrc, out dataType, out bytesFile, out strMsg); if (dataType.IsFile == true) { // 如果 body 的类型文件,则直接将文件存储到 PC 端的 D: 盘根路径下 using (FileStream file = new FileStream("d:\\" + dataType.FileName + dataType.Exten, FileMode.OpenOrCreate)) { file.Write(bytesFile, 0, bytesFile.Length); } // 显示回调消息 OnCompleted("客户端发送的文件:" + dataType.FileName + dataType.Exten); OnCompleted("该文件保存在 D: 盘的根目录下"); } else { OnCompleted(">>客户端:" + strMsg); } } catch (Exception ex) { // 如果抛异常,则释放该 Socket 对象所占用的资源 CloseConnection(); OnCompleted("SocketClient.ReceiveMsg() :" + ex.Message); } } } /// <summary> /// 向客户端发送消息 /// </summary> /// <param name="strMsg"></param> public void Send(string strMsg) { byte[] arrMsgFinal = CommonHelper.ConvertDataToByte(new DataType { IsFile = false }, null, strMsg); sokMsg.Send(arrMsgFinal); } // 向客户端发送文件数据 public void SendFile(string strPath) { try { byte[] arrFileFina = CommonHelper.ConvertDataToByte(new DataType { IsFile = true }, strPath, null); //发送文件数据 sokMsg.Send(arrFileFina);//, 0, length + 1, SocketFlags.None); OnCompleted("发送文件完成"); } catch (Exception ex) { OnCompleted(ex.Message); } } // 关闭与客户端连接 public void CloseConnection() { isRec = false; sokMsg.Close(); sokMsg.Dispose(); } void OnCompleted(string strMsg) { if (Completed != null) { Completed(null, strMsg); } } } }
三、在 MainWindow.xaml 文件中,定义侦听客户端连接的 Socket
MainWindow.xaml 文件相应的 Codebehind 代码:
namespace DesktopSocketServerDemo { /// <summary> /// PC 端服务器,用来接收连接 PC 的 Socket 连接,并创建 SocketClient 类与客户端通信 /// </summary> public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); //MultiCast cast = new MultiCast(); //cast.Startup(); txtIP.Text += "本地 IP:" + strIP; txtPort.Text = " 端口:" + Port; } int Port = 5000; // 服务器端的 IP 地址 string strIP = CommonHelper.GetIPAddress(); //负责监听 客户端 的连接请求 Socket socketWatch = null; // 执行 socketWatch 对象监听请求的线程 Thread threadWatch = null; // 开启监听 private void btnBeginListening_Click(object sender, RoutedEventArgs e) { if (socketWatch == null) { //实例化 套接字 (ip4寻址协议,流式传输,TCP协议) socketWatch = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); IPEndPoint endpoint = new IPEndPoint(IPAddress.Parse(strIP), Port); //将 监听套接字 绑定到 对应的IP和端口 socketWatch.Bind(endpoint); //设置 监听队列 长度为10(同时能够处理 10个连接请求) socketWatch.Listen(10); threadWatch = new Thread(StartWatch); threadWatch.IsBackground = true; threadWatch.Start(); ShowMsg("启动服务器成功......\r\n"); } else { ShowMsg("服务器已经启动"); } } // 循环侦听客户端的连接请求 bool isWatch = true; // 负责与客户端通信的对象 SocketClient socketClient; /// <summary> /// 被线程调用 监听连接端口 /// </summary> void StartWatch() { while (isWatch) { try { //监听 客户端 连接请求,但是,Accept会阻断当前线程 //监听到请求,立即创建负责与该客户端套接字通信的套接字 Socket socket = socketWatch.Accept(); if (socketClient != null) socketClient.CloseConnection(); socketClient = new SocketClient(socket); socketClient.Completed += connection_Completed; this.Dispatcher.BeginInvoke(new Action(delegate { if (socket != null && socket.RemoteEndPoint != null) //将 通信套接字的 客户端IP端口保存在下拉框里 listbox.Items.Add(new TextBlock { Text = socket.RemoteEndPoint.ToString() }); ShowMsg("接收一个客户端连接......"); }), null); } catch (Exception ex) { ShowMsg(ex.Message); socketWatch.Close(); socketWatch.Dispose(); isWatch = false; } } } //发送消息到已经连接到 PC 的客户端 private void btnSendMsg_Click(object sender, EventArgs e) { if (string.IsNullOrEmpty(txtInput.Text)) { ShowMsg("输入内容不能为空"); return; } if (socketClient != null) { ShowMsg(txtInput.Text.Trim()); // 发送文字内容 socketClient.Send(txtInput.Text.Trim()); txtInput.Text = ""; } else { MessageBox.Show("还没有建立连接"); } } //选择要发送的文件 private void btnChooseFile_Click(object sender, EventArgs e) { Microsoft.Win32.OpenFileDialog openFileDialog = new Microsoft.Win32.OpenFileDialog(); if (openFileDialog.ShowDialog() == true) { txtFilePath.Text = openFileDialog.FileName; } } //发送文件 private void btnSendFile_Click(object sender, EventArgs e) { if (string.IsNullOrWhiteSpace(txtFilePath.Text)) { MessageBox.Show("请先选择一个文件"); return; } //connection = new SocketClient(); if (socketClient != null) { // 发送文件内容 socketClient.SendFile(txtFilePath.Text.Trim()); } else { MessageBox.Show("还没有建立连接"); } } // 操作完成 void connection_Completed(object sender, string e) { ShowMsg(e); } // 向窗口追加消息 void ShowMsg(string strMsg) { this.Dispatcher.BeginInvoke(new Action(delegate { txtResult.Text += Environment.NewLine + strMsg + Environment.NewLine; scroll.ScrollToBottom(); }), null); } // 当服务器关闭时触发 protected override void OnClosed(EventArgs e) { if (socketWatch != null) { socketWatch.Close(); socketWatch.Dispose(); } base.OnClosed(e); } // 按“回车键”发送消息 private void txtInput_KeyDown(object sender, KeyEventArgs e) { if (e.Key == Key.Enter) { btnSendMsg_Click(null, null); } } // 释放连接资源 //private void btnStop_Click(object sender, RoutedEventArgs e) //{ // if (socketClient != null) // { // socketClient.CloseConnection(); // } // if (sokWatch != null) // { // sokWatch.Close(); // sokWatch.Dispose(); // } //} } }