这里主要写 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();
// }
//}
}
}