非专业人员,因项目需要,要写一个TCP服务端,一个TCP客户端。但在网上搜了好多都没有看见TCP服务端正常关闭的代码,所以把最后写完的代码放上来留作日后备用。如果有更好的实现方式烦请各位赐教或指路。
XML页面:
<Fluent:RibbonWindow x:Class="MyProject.TCP_server"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:Fluent="urn:fluent-ribbon"
xmlns:AduSkin="clr-namespace:AduSkin.Controls.Metro;assembly=AduSkin"
xmlns:hc="https://handyorg.github.io/handycontrol"
mc:Ignorable="d"
x:Name="tcp_server"
IsIconVisible="False"
Height="600"
Width="900"
WindowStartupLocation="CenterScreen"
ResizeMode="CanMinimize"
Background="#091125"
Title="TCP_server"
TitleForeground="#091125"
Loaded="tcp_server_Loaded"
Closing="tcp_server_Closing">
<Grid>
<Image Source="Assets/BackGround.png" Stretch="Fill"></Image>
<Image Source="Assets/TopBottom.png" Stretch="Fill"></Image>
<TextBlock Foreground="White" FontSize="25" Margin="387,10,386,0" TextWrapping="Wrap" Text="TCP服务端" TextAlignment="Center" Width="130" Height="32" VerticalAlignment="Top" HorizontalAlignment="Center"/>
<TextBlock x:Name="IPtextBlock" Text="本地ip地址:" Height="20" Width="95" TextWrapping="Wrap" Foreground="White" Background="Transparent" FontSize="16" FontFamily="微软雅黑" FontStretch="Normal" Margin="32,75,767,476" MouseLeftButtonDown="IPtextBlock_MouseLeftButtonDown"/>
<TextBlock Text="本地端口号:" Height="20" Width="100" TextWrapping="Wrap" Foreground="White" Background="Transparent" FontSize="16" FontFamily="微软雅黑" FontStretch="Normal" Margin="287,75,507,476"/>
<ComboBox x:Name="IPtextbox" Margin="0,0,500,400" Height="35" Width="150" ItemsSource="{Binding}" Style="{StaticResource ComboBoxStyle}" Foreground="White" FontSize="16" FontFamily="微软雅黑" FontStretch="Normal"/>
<!--<TextBox x:Name="IPtextbox" Text="0.0.0.0" Margin="0,0,500,400" Height="35" Width="150" CaretBrush="White" Foreground="White" Background="Transparent" BorderBrush="#00c0ff" BorderThickness="1" FontSize="16" FontFamily="微软雅黑" FontStretch="Normal"/>-->
<TextBox x:Name="PORTtextbox" Text="20000" Margin="379,68,435,468" Height="35" Width="80" CaretBrush="White" Foreground="White" Background="Transparent" BorderBrush="#00c0ff" BorderThickness="1" FontSize="16" FontFamily="微软雅黑" FontStretch="Normal"/>
<Button x:Name="MonitorButton" Content="开始监听" Style="{StaticResource Button_Menu}" Width="80" Click="MonitorButton_Click"
Margin="471,68,343,468" Height="35" Foreground="White" Background="Transparent" FontSize="16" FontFamily="微软雅黑" FontStretch="Normal" />
<Button x:Name="ClearReceive" Content="清除显示" Style="{StaticResource Button_Menu}" Width="80" Height="33" Click="ClearReceive_Click"
Margin="792,284,22,254" Foreground="White" Background="Transparent" FontSize="16" FontFamily="微软雅黑" FontStretch="Normal"/>
<Button x:Name="SEND" Content="发送" Style="{StaticResource Button_Menu}" Width="80" Height="33" Click="SEND_Click"
Margin="791,458,23,80" Foreground="White" Background="Transparent" FontSize="16" FontFamily="微软雅黑" FontStretch="Normal" />
<Button x:Name="ClearSend" Content="清除输入" Style="{StaticResource Button_Menu}" Width="80" Height="33" Click="ClearSend_Click"
Margin="791,502,23,36" Foreground="White" Background="Transparent"
FontSize="16" FontFamily="微软雅黑" FontStretch="Normal" />
<ComboBox x:Name="clientCombo" Margin="444,0,0,400" Height="35" Width="205" Style="{StaticResource ComboBoxStyle}" Foreground="White" FontSize="16" FontFamily="微软雅黑" FontStretch="Normal"/>
<!--ItemsSource="{Binding PortList,Mode = TwoWay,UpdateSourceTrigger = PropertyChanged}"
DisplayMemberPath="TargetPort"
SelectedValuePath="TargetPort"
SelectedItem="{Binding PortList}"-->
<TextBox x:Name="ReceiveBox" Margin="32,118,122,253" Width="740" Height="200" VerticalAlignment="Center" VerticalContentAlignment="Top" CaretBrush="White" TextWrapping="Wrap" Foreground="White" Background="Transparent" BorderBrush="#00c0ff" BorderThickness="2" FontSize="16" FontFamily="微软雅黑" FontStretch="Normal" VerticalScrollBarVisibility="Auto"/>
<TextBox x:Name="InputBox" Margin="32,336,122,35" Width="740" Height="200" VerticalAlignment="Center" VerticalContentAlignment="Top" CaretBrush="White" Foreground="White" TextWrapping="Wrap" Background="Transparent" BorderBrush="#00c0ff" BorderThickness="2" FontSize="16" FontFamily="微软雅黑" FontStretch="Normal" VerticalScrollBarVisibility="Auto"/>
<RadioButton x:Name="sendHEX" Width="50" Height="50" Content="HEX" Style="{StaticResource GroupSelectRadioButton_Left}" Margin="784,378,60,143" IsChecked="True"/>
<RadioButton x:Name="sendASCII" Width="50" Height="50" Content="ASCII" Style="{StaticResource GroupSelectRadioButton_Right}" Margin="834,378,10,143"/>
</Grid>
</Fluent:RibbonWindow>
后台代码:
using Fluent;
using System;
using System.Collections.Generic;
using System.Data;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
namespace MyProject
{
/// <summary>
/// TCP_server.xaml 的交互逻辑
/// </summary>
public partial class TCP_server : Fluent.RibbonWindow
{
public static TCP_server TCP_server_;
public TCP_server()
{
TCP_server_ = this;
InitializeComponent();
}
/// <summary>
/// 负责通信的socket
/// </summary>
Socket socketSend;
/// <summary>
/// 负责监听Socket
/// </summary>
Socket socket;
/// <summary>
/// 存放连接的socket
/// </summary>
Dictionary<string, Socket> dictionary = new Dictionary<string, Socket>();
/// <summary>
/// 用于开机自启选择监听的标志位
/// </summary>
private bool SelfStart = false;
/// <summary>
/// 线程集合
/// </summary>
Dictionary<string, Thread> dictThread = new Dictionary<string, Thread>();
private void tcp_server_Loaded(object sender, RoutedEventArgs e)
{
IPAddress[] myip = GetLocalIPv4Address();//加载完成自动获取本机IP
IPtextbox.ItemsSource = myip;//将本机IP填入下拉框
ReceiveBox.UndoLimit = 0;//文本框默认可以撤销,但会占用内存,禁用接收文本框的撤销队列,节省内存
SelfStart = true;//启动时默认选择0.0.0.0监听
MonitorButton_Click(sender, e);
}
/// <summary>
/// 判断是否开始监听的标志位
/// </summary>
private bool isListening = false;
/// <summary>
/// 开始监听按钮
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void MonitorButton_Click(object sender, RoutedEventArgs e)
{
try
{
if (!isListening) // 如果未监听,则开始监听
{
//创建监听的socket,
//SocketType.Stream 流式对应tcp协议
//Dgram,数据报对应UDP协议
socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
//创建IP地址
IPAddress ip;
if (SelfStart)
{
ip = IPAddress.Parse("0.0.0.0"); // 开机自启监控固定IP
IPtextbox.Text = ip.ToString();
//ip = IPAddress.Any; // 开机默认选择0.0.0.0监听
}
else
{
ip = IPAddress.Parse(IPtextbox.Text); // IPAddress.Any;
}
//创建端口号
int port = Convert.ToInt32(PORTtextbox.Text);
IPEndPoint iPEndPoint = new IPEndPoint(ip, port);
//让负责监听的Socket绑定ip和端口号
socket.Bind(iPEndPoint);//若是已经监听成功,再次点击“开始监听”,此句会报错,加了try防止报错
ShowLog(DateTime.Now + " 监听成功!" + iPEndPoint);//打印日志
//设置监听队列
socket.Listen(16);//一段时间内可以连接到的服务器的最大数量
MonitorButton.Content = "监听中...";
isListening = true;
Thread thread = new Thread(listen);
thread.IsBackground = true;
thread.Start(socket);
}
else // 如果已经在监听,则关闭监听
{
// 关闭监听线程
isListening = false;
if (socketSend != null) // 先关闭通信套接字
{
// 报错“集合已修改;可能无法执行枚举操作。”的解决:1、可在遍历的时候将字典列表.ToArray()转化为数组
// 2、可使用另一个List对这两个集合字典进行操作
foreach (KeyValuePair<string, Socket> item in dictionary.ToArray()) // 遍历字典里的所有远程终结点,向它们全部发出关闭请求
{
try
{
item.Value.Shutdown(SocketShutdown.Both); // 不知道是否能改进:发出关闭请求的之前,应该先终止线程,可能就不会出现“一个封锁操作被对 WSACancelBlockingCall 的调用中断”的异常了?
// 从 通信套接字 集合中删除被中断连接的通信套接字;
dictionary.Remove(item.Key);
// 从通信线程集合中删除被中断连接的通信线程对象;
dictThread.Remove(item.Key);
//ShowLog(DateTime.Now + "—" + item.Key + "已下线。");
//从下拉框集合中删除被中断连接的端口;
if (clientCombo.Items.Contains(item.Key))
{
this.Dispatcher.Invoke(new Action(delegate
{
clientCombo.Items.Remove(item.Key);
}));
}
}
catch (Exception err)
{
Console.WriteLine("TCP关闭监听时遍历字典异常:" + err.Message);
}
item.Value.Close();
}
this.Dispatcher.Invoke(new Action(delegate
{
//将下拉框显示的文本置空;
clientCombo.Text = null;
}));
socketSend = null;
ShowLog(DateTime.Now + "—" + "服务器已关闭。");
}
if (socket != null) // 后关闭监听套接字,否则会报错“无法访问已释放的对象”,原因可能是关闭监听后,receive函数会收到来自客户端长度为0的“关闭请求”,导致判断下线要移除终结点时,其已经被释放
{
socket.Close();
socket = null; // 执行后控制台报错:引发的异常:“System.Net.Sockets.SocketException”(位于 System.dll 中)。但目前不影响使用,原因未知。
}
// 修改按钮状态
MonitorButton.Content = "开始监听";
}
SelfStart = false;
}
catch (Exception ex)
{
Console.WriteLine("开始监听点击函数异常:" + ex.Message);
SelfStart = false;
}
}
/// <summary>
/// 使用线程来接收数据
/// </summary>
/// <param name="o"></param>
private void listen(object o)
{
Socket socket = o as Socket;
while (isListening)
{
try
{
try
{
socketSend = socket.Accept(); // 监听程序运行到这里处于阻塞状态,直到有客户端连接进来,才会继续往下运行,然后再次运行到此行阻塞等待下一个客户端连接
}
catch (SocketException ex)
{
if (ex.SocketErrorCode == SocketError.Interrupted) // 有客户端连接过服务器后,监听程序阻塞在上面,当关闭监听时会继续运行抛出一个中断的异常,在这里接收并处理,用break结束while循环
{
// 监听线程被关闭
Console.WriteLine("Listen函数catch处理if:" + ex.Message);
break;
}
else
{
Console.WriteLine("Listen函数catch处理else:" + ex.Message);
}
}
//socketSend = socket.Accept(); // 监听程序运行到这里处于阻塞状态,直到有客户端连接进来,才会继续往下运行,然后再次运行到此行阻塞等待下一个客户端连接
if (!clientCombo.Items.Contains(socketSend.RemoteEndPoint.ToString()))
{
this.Dispatcher.Invoke(new Action(delegate
{
clientCombo.Items.Add(socketSend.RemoteEndPoint.ToString()); // 在下拉框中添加ip地址
}));
}
//负责监听的socket是用来接收客户端连接
//创建负责通信的socket
//socketSend = socket.Accept();
if (!(dictionary.ContainsKey(socketSend.RemoteEndPoint.ToString()) && dictThread.ContainsKey(socketSend.RemoteEndPoint.ToString())))
{
//创建一个通信线程,通信线程可以解决“客户端异常断开,重连后无法由服务端再向客户端发送数据”的问题
ParameterizedThreadStart pts = new ParameterizedThreadStart(receive);
//开启新线程,接收客户端发来的信息
Thread th = new Thread(pts);
th.IsBackground = true;
th.Start(socketSend);
dictThread.Add(socketSend.RemoteEndPoint.ToString(), th); // 将新建的线程 添加 到线程的集合中去。
dictionary.Add(socketSend.RemoteEndPoint.ToString(), socketSend); // 将ip地址添加到字典集合中
}
ShowLog(DateTime.Now + "—" + socketSend.RemoteEndPoint.ToString() + "已连接");
}
catch (Exception error)
{
Console.WriteLine("Listen函数其他异常:" + error.Message);
}
}
}
/// <summary>
/// 服务器接收客户端传来的消息
/// </summary>
private void receive(object o)
{
Socket socketsend = o as Socket;
while (isListening)
{
try
{
//客户端连接成功后,服务器接收客户端发来的消息
byte[] buffer = new byte[1024 * 50]; // 50K大小的缓存区。是否隐患?是否每次while循环都会创建一个50K大小的缓冲区?可以释放内存改进吗?
//接收到的有效字节数
int length = socketsend.Receive(buffer);
if (length == 0) // TCP客户端发起“关闭连接”的请求后,挥手两次,receive会一直接收0
{
try
{
// 从 通信套接字 集合中删除被中断连接的通信套接字;
dictionary.Remove(socketsend.RemoteEndPoint.ToString());
// 从通信线程集合中删除被中断连接的通信线程对象;
dictThread.Remove(socketsend.RemoteEndPoint.ToString());
ShowLog(DateTime.Now + "—" + socketsend.RemoteEndPoint.ToString() + "下线了。");
//从下拉框集合中删除被中断连接的端口;
if (clientCombo.Items.Contains(socketsend.RemoteEndPoint.ToString()))
{
this.Dispatcher.Invoke(new Action(delegate
{
clientCombo.Items.Remove(socketsend.RemoteEndPoint.ToString());
}));
}
this.Dispatcher.Invoke(new Action(delegate
{
//将下拉框显示的文本置空;
clientCombo.Text = null;
}));
socketsend.Close(); // 这句话代表服务端也关闭连接,对客户端进行后两次的挥手
break; // break结束while循环,本次TCP连接正常关闭
}
catch (Exception err)
{
Console.WriteLine("TCP接收到length为0的数据catch异常:" + err);
break;
}
}
byte[] newBuffer = new byte[length];
Array.ConstrainedCopy(buffer, 0, newBuffer, 0, length);
SaveTxt(byteToHexStr(newBuffer)); // 将接收到数据的原始十六进制数存入TXT文件
//string str = Encoding.UTF8.GetString(buffer, 0, length); // 以UTF8码接收显示
//string str3 = Encoding.Default.GetString(buffer, 0, length); // 以.NET默认编码接收显示
string str = byteToHexStr(newBuffer); // 以十六进制接收显示
string str2 = Encoding.ASCII.GetString(buffer, 0, length); // 以ASCII码接收显示
ShowLog($"\r\n时间:{DateTime.Now},来自端口{socketsend.RemoteEndPoint} ;\r\n以ASCII码显示:{str2}"); // 以ASCII码显示
ShowLog($"\r\n时间:{DateTime.Now},来自端口{socketsend.RemoteEndPoint} ;\r\n以十六进制显示:{str}"); // 以十六进制显示
}
catch (System.Net.Sockets.SocketException ex)
{
Console.WriteLine("receive函数异常:" + ex.Message);
break;
}
}
}
/// <summary>
/// 显示在文本框,若文本长度超出限制,则清空文本框
/// </summary>
/// <param name="str"></param>
private void ShowLog(string str)
{
this.Dispatcher.Invoke(new Action(delegate
{
if (ReceiveBox.Text.Length > 1024 * 80) // 如果文本框内容大于80K,大于85000字节的对象被.NET认为是“大对象”,大对象的内存分配和管理方式与一般对象不同,它们通常是在托管堆以外的位置进行内存分配,并且不受垃圾回收的控制。
{
ReceiveBox.Clear(); // 先清除文本框
}
ReceiveBox.AppendText(str + "\r\n");
}));
}
/// <summary>
/// 发送按钮
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void SEND_Click(object sender, RoutedEventArgs e)
{
try
{
if (clientCombo.Text != null && clientCombo.Text != "")
{
string txt = InputBox.Text;
if (sendHEX.IsChecked == true)
{
byte[] buffer = strToToHexByte(txt); // 默认以十六进制发送
string ip = clientCombo.SelectedItem.ToString(); // 获取选中的ip地址
Socket send_socket = dictionary[ip];
send_socket.Send(buffer);
}
else if (sendASCII.IsChecked == true)
{
byte[] buffer = Encoding.ASCII.GetBytes(txt); // 以ASCII码发送
string ip = clientCombo.SelectedItem.ToString(); // 获取选中的ip地址
Socket send_socket = dictionary[ip];
send_socket.Send(buffer);
}
}
else
{
HandyControl.Controls.MessageBox.Show("未选择发送目标端口", "提示", MessageBoxButton.OK, MessageBoxImage.Information);
}
}
catch (Exception ex)
{
Console.WriteLine("发送按钮点击函数异常:" + ex.Message);
}
}
/// <summary>
/// 发送函数
/// </summary>
public bool SendMsg(string deviceID, string comID, byte[] msg)
{
try
{
SQL se = new SQL();
string sql = "自己的一句SQL语句";
DataTable data = se.ExecuteDatable(sql);
string b = data.Rows[0][0].ToString();
//将控件的选项置为数据库中绑定过的端口号,此句执行成功的前提是:下拉框中已经有这一项,否则执行无效,将仍然保持执行前的状态
clientCombo.SelectedItem = data.Rows[0][0].ToString();
if (clientCombo.SelectedItem != null) // 判断是否选择了客户端,如果是,执行下一步操作
{
string ip = clientCombo.SelectedItem.ToString(); // 获取选中的ip地址
Socket socketsend = dictionary[ip];
socketsend.Send(msg);
return true;
}
else
{
HandyControl.Controls.MessageBox.Show("未选择目标客户端", "提示", MessageBoxButton.OK, MessageBoxImage.Information);
return false;
}
}
catch (Exception ex)
{
Console.WriteLine("SendMsg函数异常:" + ex.Message);
return false;
}
}
/// <summary>
/// 字符串转16进制格式,不够自动前面补零
/// </summary>
private static byte[] strToToHexByte(String hexString)
{
int i;
hexString = hexString.Replace(" ", "");//清除空格
if ((hexString.Length % 2) != 0)//奇数个
{
byte[] returnBytes = new byte[(hexString.Length + 1) / 2];
try
{
for (i = 0; i < (hexString.Length - 1) / 2; i++)
{
returnBytes[i] = Convert.ToByte(hexString.Substring(i * 2, 2), 16);
}
returnBytes[returnBytes.Length - 1] = Convert.ToByte(hexString.Substring(hexString.Length - 1, 1).PadLeft(2, '0'), 16);
}
catch
{
HandyControl.Controls.MessageBox.Show("含有非16进制字符", "提示", MessageBoxButton.OK, MessageBoxImage.Information);
return null;
}
return returnBytes;
}
else
{
byte[] returnBytes = new byte[(hexString.Length) / 2];
try
{
for (i = 0; i < returnBytes.Length; i++)
{
returnBytes[i] = Convert.ToByte(hexString.Substring(i * 2, 2), 16);
}
}
catch
{
HandyControl.Controls.MessageBox.Show("含有非16进制字符", "提示", MessageBoxButton.OK, MessageBoxImage.Information);
return null;
}
return returnBytes;
}
}
/// <summary>
/// 字节数组转16进制字符串
/// </summary>
public static string byteToHexStr(byte[] bytes)
{
string returnStr = "";
try
{
if (bytes != null)
{
for (int i = 0; i < bytes.Length; i++)
{
returnStr += bytes[i].ToString("X2");
returnStr += " ";//两个16进制用空格隔开,方便看数据
}
}
return returnStr;
}
catch (Exception)
{
return returnStr;
}
}
/// <summary>
/// 清除接收显示
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void ClearReceive_Click(object sender, RoutedEventArgs e)
{
ReceiveBox.Clear();
}
/// <summary>
/// 清除发送输入
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void ClearSend_Click(object sender, RoutedEventArgs e)
{
InputBox.Clear();
}
/// <summary>
/// 关闭时执行
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void tcp_server_Closing(object sender, System.ComponentModel.CancelEventArgs e)
{
e.Cancel = true; // 禁止关闭TCP服务端
}
/// <summary>
/// IPtextBlock单击事件,将本机IP填入文本框中
/// </summary>
private void IPtextBlock_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
IPAddress[] ip = GetLocalIPv4Address();
IPtextbox.ItemsSource = ip;
}
/// <summary>
/// 获取本地IPv4地址
/// </summary>
/// <returns></returns>
IPAddress[] GetLocalIPv4Address()
{
IPAddress[] localIpv4 = new IPAddress[Dns.GetHostAddresses(Dns.GetHostName()).Length + 2];
//获取本机所有的IP地址列表
IPAddress[] IpList = Dns.GetHostAddresses(Dns.GetHostName());
int i = 0;
//循环遍历所有IP地址
foreach (IPAddress IP in IpList)
{
//判断是否是IPv4地址
if (IP.AddressFamily == AddressFamily.InterNetwork)
{
localIpv4[i] = IP;
i++;
}
else
{
continue;
}
}
localIpv4[localIpv4.Length - 1] = IPAddress.Parse("0.0.0.0");
localIpv4[localIpv4.Length - 2] = IPAddress.Parse("127.0.0.1");
//将数组中的null值去除
localIpv4 = localIpv4.Where(x => !string.IsNullOrEmpty(Convert.ToString(x))).ToArray();
//localIpv4 = localIpv4.Where(x => !string.IsNullOrWhiteSpace(Convert.ToString(x))).ToArray();
return localIpv4;
}
/// <summary>
/// 将原始数据保存为TXT文件
/// </summary>
private void SaveTxt(string strLog)
{
string sFilePath = System.AppDomain.CurrentDomain.BaseDirectory + "/TCP原始数据/"; // 找到Debug文件夹中的“TCP原始数据”文件夹
string sFileName = DateTime.Now.ToString("yyyyMMdd") + ".txt"; // 在文件夹中创建以当前系统时间命名的.txt文件
sFileName = sFilePath + "\\" + sFileName; // 文件的绝对路径
if (!Directory.Exists(sFilePath)) // 验证路径是否存在
{
Directory.CreateDirectory(sFilePath); // 不存在则创建
}
FileStream fs;
StreamWriter sw;
if (File.Exists(sFileName))
//验证文件是否存在,有则追加,无则创建
{
fs = new FileStream(sFileName, FileMode.Append, FileAccess.Write, FileShare.ReadWrite);
}
else
{
fs = new FileStream(sFileName, FileMode.Create, FileAccess.Write, FileShare.ReadWrite);
}
sw = new StreamWriter(fs);
sw.WriteLine("【" + DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss") + "】" + "\r\n" + strLog);//写入日志的内容
sw.Close();
fs.Close();
}
}
}
后台代码的一些注释中提出了疑问,不知是否可以作为改进的点?或者是否可以有更好的代码逻辑实现?希望看见的大佬不吝赐教!