当前位置: 首页 > news >正文

开源 C# 快速开发(九)通讯--Tcp客户端

         文章的目的为了记录使用C# 开发学习的经历。开发流程和要点有些记忆模糊,赶紧记录,防止忘记。

 相关链接:

开源 C# 快速开发(一)基础知识

开源 C# 快速开发(二)基础控件

开源 C# 快速开发(三)复杂控件

开源 C# 快速开发(四)自定义控件--波形图

开源 C# 快速开发(五)自定义控件--仪表盘

开源 C# 快速开发(六)自定义控件--圆环

开源 C# 快速开发(七)通讯--串口

开源 C# 快速开发(八)通讯--Tcp服务器端

开源 C# 快速开发(九)通讯--Tcp客户端

开源 C# 快速开发(十)通讯--http客户端

推荐链接:

开源 C# .net mvc 开发(一)WEB搭建_c#部署web程序-CSDN博客

开源 C# .net mvc 开发(二)网站快速搭建_c#网站开发-CSDN博客

开源 C# .net mvc 开发(三)WEB内外网访问-CSDN博客

开源 C# .net mvc 开发(四)工程结构、页面提交以及显示-CSDN博客

开源 C# .net mvc 开发(五)常用代码快速开发_c# mvc开发-CSDN博客

开源 C# .net mvc 开发(六)发送邮件、定时以及CMD编程-CSDN博客

开源 C# .net mvc 开发(七)动态图片、动态表格和json数据生成-CSDN博客

开源 C# .net mvc 开发(八)IIS Express轻量化Web服务器的配置和使用-CSDN博客

开源 C# .net mvc 开发(九)websocket--服务器与客户端的实时通信-CSDN博客

本章节主要内容是:TCP客户端应用程序,基于Windows Forms实现。这个TCP客户端实现了完整的连接管理、数据收发和错误处理机制,是一个功能完备的网络通信客户端。

目录:

1.源码分析

2.所有源码

3.效果演示

一、源码分析

1. 构造函数和初始化 Form1()

功能:窗体初始化时自动获取并显示本机IP地址。

public Form1()
{InitializeComponent();UpdateLocalIPInfo(); // 初始化时获取本地IP信息
}


2. 本地IP信息获取 UpdateLocalIPInfo()

技术要点:

使用Dns.GetHostEntry()获取本机所有网络接口的IP地址

过滤出IPv4地址(AddressFamily.InterNetwork)

异常处理确保程序稳定性

private void UpdateLocalIPInfo()
{try{string localIP = GetLocalIPAddress(); // 获取本地IPv4地址txtClientIP.Text = localIP; // 显示在文本框}catch (Exception ex){txtClientIP.Text = "获取失败";AddLog($"获取本地IP失败: {ex.Message}");}
}private string GetLocalIPAddress()
{var host = Dns.GetHostEntry(Dns.GetHostName());foreach (var ip in host.AddressList){if (ip.AddressFamily == AddressFamily.InterNetwork) // 只获取IPv4地址{return ip.ToString();}}throw new Exception("未找到IPv4地址");
}


3. 连接服务器 ConnectToServer()

关键特性:

异步连接:使用BeginConnect避免UI线程阻塞

输入验证:严格检查IP和端口格式

UI状态管理:及时更新按钮和状态显示

private void ConnectToServer()
{try{// 输入验证if (!IPAddress.TryParse(txtServerIP.Text, out IPAddress ipAddress)){MessageBox.Show("请输入有效的服务器IP地址");return;}if (!int.TryParse(txtServerPort.Text, out int port) || port < 1 || port > 65535){MessageBox.Show("请输入有效的端口号(1-65535)");return;}// 异步连接服务器tcpClient = new TcpClient();tcpClient.BeginConnect(ipAddress, port, new AsyncCallback(ConnectCallback), tcpClient);lblStatus.Text = "连接中...";btnConnect.Enabled = false; // 防止重复点击}catch (Exception ex){MessageBox.Show($"连接失败: {ex.Message}");lblStatus.Text = "连接失败";btnConnect.Enabled = true;}
}

4. 连接回调 ConnectCallback(IAsyncResult ar)
重要功能:

连接完成:通过EndConnect完成异步连接操作

本地端口获取:显示客户端使用的本地端口号

自动重连机制:在连接失败时自动尝试重新连接

private void ConnectCallback(IAsyncResult ar)
{try{TcpClient client = (TcpClient)ar.AsyncState;client.EndConnect(ar); // 完成异步连接// 连接成功处理isConnected = true;clientStream = client.GetStream();reconnectAttempts = 0; // 重置重连计数器// 获取本地端口号string localPort = ((IPEndPoint)client.Client.LocalEndPoint).Port.ToString();Invoke(new Action(() =>{txtClientPort.Text = localPort;lblStatus.Text = $"已连接到 {txtServerIP.Text}:{txtServerPort.Text}";btnConnect.Enabled = false;btnDisconnect.Enabled = true;btnSend.Enabled = true;AddLog($"连接成功 - 本地端口: {localPort}");}));// 启动数据接收线程receiveThread = new Thread(new ThreadStart(ReceiveData));receiveThread.IsBackground = true;receiveThread.Start();}catch (Exception ex){Invoke(new Action(() =>{lblStatus.Text = "连接失败";btnConnect.Enabled = true;btnDisconnect.Enabled = false;AddLog($"连接失败: {ex.Message}");// 自动重连逻辑if (autoReconnect && reconnectAttempts < MAX_RECONNECT_ATTEMPTS){reconnectAttempts++;AddLog($"尝试自动重连 ({reconnectAttempts}/{MAX_RECONNECT_ATTEMPTS})...");Thread.Sleep(3000); // 等待3秒ConnectToServer(); // 重新连接}}));}
}

5. 数据接收 ReceiveData()

异常处理策略:

IOException:网络连接问题

ObjectDisposedException:资源已被释放

通用异常:其他未知错误

private void ReceiveData()
{byte[] buffer = new byte[4096]; // 4KB缓冲区int bytesRead;while (isConnected && tcpClient != null && tcpClient.Connected){try{// 阻塞读取数据bytesRead = clientStream.Read(buffer, 0, buffer.Length);if (bytesRead == 0) // 服务器主动关闭连接{HandleDisconnection("服务器关闭了连接");break;}string receivedData = Encoding.UTF8.GetString(buffer, 0, bytesRead);Invoke(new Action(() =>{AddLog($"接收: {receivedData}");}));}catch (IOException){// 网络连接异常HandleDisconnection("连接被中断");break;}catch (ObjectDisposedException){// 流已被关闭break;}catch (Exception ex){if (isConnected){Invoke(new Action(() => AddLog($"接收数据时出错: {ex.Message}")));}break;}}
}


6. 连接断开处理 HandleDisconnection(string reason)

自动重连机制:

最大重试次数:5次(MAX_RECONNECT_ATTEMPTS)

重连间隔:3秒

条件控制:通过autoReconnect复选框启用/禁用

private void HandleDisconnection(string reason)
{if (!isConnected) return;isConnected = false;Invoke(new Action(() =>{lblStatus.Text = "连接已断开";btnConnect.Enabled = true;btnDisconnect.Enabled = false;btnSend.Enabled = false;AddLog($"连接断开: {reason}");// 自动重连逻辑if (autoReconnect && reconnectAttempts < MAX_RECONNECT_ATTEMPTS){reconnectAttempts++;AddLog($"尝试自动重连 ({reconnectAttempts}/{MAX_RECONNECT_ATTEMPTS})...");Thread.Sleep(3000);ConnectToServer();}}));// 资源清理try{clientStream?.Close();tcpClient?.Close();}catch { }
}

7. 主动断开连接 Disconnect()

资源管理:

显式关闭网络流和客户端对象

终止后台接收线程

禁用自动重连功能

private void Disconnect()
{try{isConnected = false;autoReconnect = false; // 禁用自动重连// 终止接收线程receiveThread?.Abort();receiveThread = null;// 关闭网络资源clientStream?.Close();tcpClient?.Close();Invoke(new Action(() =>{lblStatus.Text = "已断开连接";btnConnect.Enabled = true;btnDisconnect.Enabled = false;btnSend.Enabled = false;txtClientPort.Text = ""; // 清空端口显示AddLog("主动断开连接");}));}catch (Exception ex){Invoke(new Action(() => AddLog($"断开连接时出错: {ex.Message}")));}
}

8. 数据发送 SendData()
发送特性:

连接状态实时验证

UTF-8编码支持中文

发送后自动清空输入框

private void SendData()
{// 连接状态检查if (!isConnected || tcpClient == null || !tcpClient.Connected){MessageBox.Show("未连接到服务器");return;}if (string.IsNullOrWhiteSpace(txtSend.Text)){MessageBox.Show("请输入要发送的数据");return;}try{byte[] data = Encoding.UTF8.GetBytes(txtSend.Text);clientStream.Write(data, 0, data.Length);clientStream.Flush(); // 立即发送Invoke(new Action(() =>{AddLog($"发送: {txtSend.Text}");txtSend.Clear(); // 清空发送框}));}catch (Exception ex){Invoke(new Action(() =>{AddLog($"发送失败: {ex.Message}");HandleDisconnection("发送数据时出错");}));}
}

9. 键盘快捷键 txtSend_KeyPress()

用户体验优化:

支持Ctrl+Enter多行发送

支持Enter键快速发送

private void txtSend_KeyPress(object sender, KeyPressEventArgs e)
{// Ctrl+Enter 发送if (e.KeyChar == '\r' && Control.ModifierKeys == Keys.Control){e.Handled = true;SendData();}// Enter 发送(单行模式)else if (e.KeyChar == '\r' && !txtSend.Multiline){e.Handled = true;SendData();}
}

10. 日志记录 AddLog(string message)
线程安全:使用Invoke确保跨线程UI操作安全。

private void AddLog(string message)
{if (txtReceive.InvokeRequired){txtReceive.Invoke(new Action<string>(AddLog), message);}else{txtReceive.AppendText($"[{DateTime.Now:HH:mm:ss}] {message}\r\n");txtReceive.ScrollToCaret(); // 自动滚动到底部}
}

二、所有源码

Form1.Designer.cs界面代码


using System;
using System.Drawing;
using System.Windows.Forms;namespace ClientForm
{partial class Form1{/*/// <summary>/// 必需的设计器变量。/// </summary>private System.ComponentModel.IContainer components = null;/// <summary>/// 清理所有正在使用的资源。/// </summary>/// <param name="disposing">如果应释放托管资源,为 true;否则为 false。</param>protected override void Dispose(bool disposing){if (disposing && (components != null)){components.Dispose();}base.Dispose(disposing);}#region Windows 窗体设计器生成的代码/// <summary>/// 设计器支持所需的方法 - 不要修改/// 使用代码编辑器修改此方法的内容。/// </summary>private void InitializeComponent(){this.components = new System.ComponentModel.Container();this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;this.ClientSize = new System.Drawing.Size(800, 450);this.Text = "Form1";}#endregion*/private System.ComponentModel.IContainer components = null;private TextBox txtServerIP;private TextBox txtServerPort;private Button btnConnect;private Button btnDisconnect;private TextBox txtSend;private Button btnSend;private TextBox txtReceive;private Label lblStatus;private Label label1;private Label label2;private Label label3;private Label label4;private CheckBox chkAutoReconnect;private Button btnClear;private TextBox txtClientIP;private TextBox txtClientPort;private Label label5;private Label label6;protected override void Dispose(bool disposing){if (disposing && (components != null)){components.Dispose();}base.Dispose(disposing);}private void InitializeComponent(){this.components = new System.ComponentModel.Container();this.txtServerIP = new TextBox();this.txtServerPort = new TextBox();this.btnConnect = new Button();this.btnDisconnect = new Button();this.txtSend = new TextBox();this.btnSend = new Button();this.txtReceive = new TextBox();this.lblStatus = new Label();this.label1 = new Label();this.label2 = new Label();this.label3 = new Label();this.label4 = new Label();this.chkAutoReconnect = new CheckBox();this.btnClear = new Button();this.txtClientIP = new TextBox();this.txtClientPort = new TextBox();this.label5 = new Label();this.label6 = new Label();// 窗体设置this.Text = "TCP客户端";this.Size = new Size(700, 600);this.StartPosition = FormStartPosition.CenterScreen;// 服务器IP标签和文本框this.label1.Location = new Point(20, 20);this.label1.Size = new Size(80, 20);this.label1.Text = "服务器IP:";this.txtServerIP.Location = new Point(100, 20);this.txtServerIP.Size = new Size(150, 20);this.txtServerIP.Text = "127.0.0.1";// 服务器端口标签和文本框this.label2.Location = new Point(270, 20);this.label2.Size = new Size(80, 20);this.label2.Text = "服务器端口:";this.txtServerPort.Location = new Point(350, 20);this.txtServerPort.Size = new Size(80, 20);this.txtServerPort.Text = "8080";// 客户端IP标签和文本框(只读,显示本地信息)this.label5.Location = new Point(20, 50);this.label5.Size = new Size(80, 20);this.label5.Text = "本地IP:";this.txtClientIP.Location = new Point(100, 50);this.txtClientIP.Size = new Size(150, 20);this.txtClientIP.ReadOnly = true;// 客户端端口标签和文本框(只读,显示本地信息)this.label6.Location = new Point(270, 50);this.label6.Size = new Size(80, 20);this.label6.Text = "本地端口:";this.txtClientPort.Location = new Point(350, 50);this.txtClientPort.Size = new Size(80, 20);this.txtClientPort.ReadOnly = true;// 连接按钮this.btnConnect.Location = new Point(450, 18);this.btnConnect.Size = new Size(80, 25);this.btnConnect.Text = "连接";this.btnConnect.Click += new EventHandler(this.btnConnect_Click);// 断开按钮this.btnDisconnect.Location = new Point(540, 18);this.btnDisconnect.Size = new Size(80, 25);this.btnDisconnect.Text = "断开";this.btnDisconnect.Enabled = false;this.btnDisconnect.Click += new EventHandler(this.btnDisconnect_Click);// 自动重连复选框this.chkAutoReconnect.Location = new Point(450, 50);this.chkAutoReconnect.Size = new Size(120, 20);this.chkAutoReconnect.Text = "自动重连";// 状态标签this.lblStatus.Location = new Point(20, 85);this.lblStatus.Size = new Size(600, 20);this.lblStatus.Text = "未连接";// 接收数据标签和文本框this.label3.Location = new Point(20, 120);this.label3.Size = new Size(100, 20);this.label3.Text = "接收数据:";this.txtReceive.Location = new Point(20, 140);this.txtReceive.Multiline = true;this.txtReceive.ScrollBars = ScrollBars.Vertical;this.txtReceive.Size = new Size(650, 250);this.txtReceive.ReadOnly = true;// 清空按钮this.btnClear.Location = new Point(580, 115);this.btnClear.Size = new Size(90, 25);this.btnClear.Text = "清空接收区";this.btnClear.Click += new EventHandler(this.btnClear_Click);// 发送数据标签和文本框this.label4.Location = new Point(20, 410);this.label4.Size = new Size(100, 20);this.label4.Text = "发送数据:";this.txtSend.Location = new Point(20, 430);this.txtSend.Multiline = true;this.txtSend.ScrollBars = ScrollBars.Vertical;this.txtSend.Size = new Size(560, 80);// 发送按钮this.btnSend.Location = new Point(590, 430);this.btnSend.Size = new Size(80, 80);this.btnSend.Text = "发送";this.btnSend.Enabled = false;this.btnSend.Click += new EventHandler(this.btnSend_Click);// 添加控件到窗体this.Controls.AddRange(new Control[] {this.txtServerIP, this.txtServerPort, this.btnConnect, this.btnDisconnect,this.txtSend, this.btnSend, this.txtReceive, this.lblStatus,this.label1, this.label2, this.label3, this.label4,this.chkAutoReconnect, this.btnClear, this.txtClientIP,this.txtClientPort, this.label5, this.label6});}}
}

Form1.cs代码

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;
using System.Net.Sockets;using System.Threading;
using System.IO;namespace ClientForm
{public partial class Form1 : Form{/*public Form1(){InitializeComponent();}*/private TcpClient tcpClient;private NetworkStream clientStream;private Thread receiveThread;private bool isConnected = false;private bool autoReconnect = false;private int reconnectAttempts = 0;private const int MAX_RECONNECT_ATTEMPTS = 5;public Form1(){InitializeComponent();UpdateLocalIPInfo();}private void UpdateLocalIPInfo(){try{// 获取本地IP地址string localIP = GetLocalIPAddress();txtClientIP.Text = localIP;}catch (Exception ex){txtClientIP.Text = "获取失败";AddLog($"获取本地IP失败: {ex.Message}");}}private string GetLocalIPAddress(){var host = Dns.GetHostEntry(Dns.GetHostName());foreach (var ip in host.AddressList){if (ip.AddressFamily == AddressFamily.InterNetwork){return ip.ToString();}}throw new Exception("未找到IPv4地址");}private void btnConnect_Click(object sender, EventArgs e){ConnectToServer();}private void ConnectToServer(){try{// 验证输入if (!IPAddress.TryParse(txtServerIP.Text, out IPAddress ipAddress)){MessageBox.Show("请输入有效的服务器IP地址");return;}if (!int.TryParse(txtServerPort.Text, out int port) || port < 1 || port > 65535){MessageBox.Show("请输入有效的端口号(1-65535)");return;}// 创建TCP客户端并连接tcpClient = new TcpClient();tcpClient.BeginConnect(ipAddress, port, new AsyncCallback(ConnectCallback), tcpClient);lblStatus.Text = "连接中...";btnConnect.Enabled = false;}catch (Exception ex){MessageBox.Show($"连接失败: {ex.Message}");lblStatus.Text = "连接失败";btnConnect.Enabled = true;}}private void ConnectCallback(IAsyncResult ar){try{TcpClient client = (TcpClient)ar.AsyncState;client.EndConnect(ar);// 连接成功isConnected = true;clientStream = client.GetStream();reconnectAttempts = 0;// 更新本地端口信息string localPort = ((IPEndPoint)client.Client.LocalEndPoint).Port.ToString();Invoke(new Action(() =>{txtClientPort.Text = localPort;lblStatus.Text = $"已连接到 {txtServerIP.Text}:{txtServerPort.Text}";btnConnect.Enabled = false;btnDisconnect.Enabled = true;btnSend.Enabled = true;AddLog($"连接成功 - 本地端口: {localPort}");}));// 启动接收线程receiveThread = new Thread(new ThreadStart(ReceiveData));receiveThread.IsBackground = true;receiveThread.Start();}catch (Exception ex){Invoke(new Action(() =>{lblStatus.Text = "连接失败";btnConnect.Enabled = true;btnDisconnect.Enabled = false;AddLog($"连接失败: {ex.Message}");// 自动重连逻辑if (autoReconnect && reconnectAttempts < MAX_RECONNECT_ATTEMPTS){reconnectAttempts++;AddLog($"尝试自动重连 ({reconnectAttempts}/{MAX_RECONNECT_ATTEMPTS})...");Thread.Sleep(3000); // 等待3秒后重连ConnectToServer();}}));}}private void ReceiveData(){byte[] buffer = new byte[4096];int bytesRead;while (isConnected && tcpClient != null && tcpClient.Connected){try{bytesRead = clientStream.Read(buffer, 0, buffer.Length);if (bytesRead == 0){// 连接已关闭HandleDisconnection("服务器关闭了连接");break;}string receivedData = Encoding.UTF8.GetString(buffer, 0, bytesRead);Invoke(new Action(() =>{AddLog($"接收: {receivedData}");}));}catch (IOException){// 连接被关闭HandleDisconnection("连接被中断");break;}catch (ObjectDisposedException){// 对象已被释放break;}catch (Exception ex){if (isConnected){Invoke(new Action(() => AddLog($"接收数据时出错: {ex.Message}")));}break;}}}private void HandleDisconnection(string reason){if (!isConnected) return;isConnected = false;Invoke(new Action(() =>{lblStatus.Text = "连接已断开";btnConnect.Enabled = true;btnDisconnect.Enabled = false;btnSend.Enabled = false;AddLog($"连接断开: {reason}");// 自动重连逻辑if (autoReconnect && reconnectAttempts < MAX_RECONNECT_ATTEMPTS){reconnectAttempts++;AddLog($"尝试自动重连 ({reconnectAttempts}/{MAX_RECONNECT_ATTEMPTS})...");Thread.Sleep(3000);ConnectToServer();}}));// 清理资源try{clientStream?.Close();tcpClient?.Close();}catch { }}private void btnDisconnect_Click(object sender, EventArgs e){Disconnect();}private void Disconnect(){try{isConnected = false;autoReconnect = false;// 停止接收线程receiveThread?.Abort();receiveThread = null;// 关闭连接clientStream?.Close();tcpClient?.Close();Invoke(new Action(() =>{lblStatus.Text = "已断开连接";btnConnect.Enabled = true;btnDisconnect.Enabled = false;btnSend.Enabled = false;txtClientPort.Text = "";AddLog("主动断开连接");}));}catch (Exception ex){Invoke(new Action(() => AddLog($"断开连接时出错: {ex.Message}")));}}private void btnSend_Click(object sender, EventArgs e){SendData();}private void SendData(){if (!isConnected || tcpClient == null || !tcpClient.Connected){MessageBox.Show("未连接到服务器");return;}if (string.IsNullOrWhiteSpace(txtSend.Text)){MessageBox.Show("请输入要发送的数据");return;}try{byte[] data = Encoding.UTF8.GetBytes(txtSend.Text);clientStream.Write(data, 0, data.Length);clientStream.Flush();Invoke(new Action(() =>{AddLog($"发送: {txtSend.Text}");txtSend.Clear();}));}catch (Exception ex){Invoke(new Action(() =>{AddLog($"发送失败: {ex.Message}");HandleDisconnection("发送数据时出错");}));}}private void btnClear_Click(object sender, EventArgs e){txtReceive.Clear();}private void chkAutoReconnect_CheckedChanged(object sender, EventArgs e){autoReconnect = chkAutoReconnect.Checked;}private void AddLog(string message){if (txtReceive.InvokeRequired){txtReceive.Invoke(new Action<string>(AddLog), message);}else{txtReceive.AppendText($"[{DateTime.Now:HH:mm:ss}] {message}\r\n");txtReceive.ScrollToCaret();}}private void txtSend_KeyPress(object sender, KeyPressEventArgs e){// 支持Ctrl+Enter发送if (e.KeyChar == '\r' && Control.ModifierKeys == Keys.Control){e.Handled = true;SendData();}// 支持Enter发送(需要设置Multiline为true)else if (e.KeyChar == '\r' && !txtSend.Multiline){e.Handled = true;SendData();}}protected override void OnFormClosing(FormClosingEventArgs e){Disconnect();base.OnFormClosing(e);}}
}

三、效果演示

使用网络调试助手验证,设置好ip和端口号,发送和接收数据,验证通过

http://www.dtcms.com/a/418205.html

相关文章:

  • 大黄蜂云课堂vep格式加密视频录屏截图翻录转换为mp4
  • 【Python办公】批量图片转PDF工具
  • Python爬虫实战:获取北京市交管局最新车检信息与数据分析
  • ubuntu24.04 实现DLNA音频推送
  • 企业网站的建设规划网站建站前期准备工作
  • Docker搭建ESPIDF环境,程序下载
  • MQTT-物联网轻量级通信
  • eclipse复制项目后原项目名依然伴随值所复制的项目名
  • 微服务架构:从单机到分布式的革命性升级
  • 【ROS2学习笔记】话题通信篇:python话题订阅与发布
  • 【碎片化学习】SpringBoot服务的启动过程
  • 儿童网站模板 html做百度快照要先有网站吗
  • Games101 第六章 Shading(着色)
  • 电子电气架构 --- 智能座舱域环境感知和人机交互系统
  • 数字营销网站主页优化制作网页动画的软件
  • CSS详篇
  • Memblock-3
  • 大数据毕业设计选题推荐-基于大数据的全国饮品门店数据可视化分析系统-Hadoop-Spark-数据可视化-BigData
  • 【后端开发】golang部分中间件介绍(任务调度/服务治理/数据库/缓存/服务通信/流量治理)
  • 建设一个自己的网站需要多少钱站长统计官方网站
  • 烟台装修公司网站建设注册公司流程和费用时间
  • java设计模式:工厂方法 + 建造者模式
  • 3、Lombok进阶功能实战:Builder模式、异常处理与资源管理高级用法
  • Linux 内核开发 的核心知识点
  • 【Linux系列】并发世界的基石:透彻理解 Linux 进程 — 理解操作系统
  • 小米17对比iPhone 17:2025年深度对比
  • 借助Aspose.Email,使用 Python 将 EML 转换为 MHTML
  • 免费做名片的网站专业网站开发设计
  • 微服务项目->在线oj系统(Java-Spring)-后台管理(1)
  • 怎么防止网站攻击做网站价格表