一、UI 设计
-
用 VS2017 新建一个 Windows 窗体应用(.NET Framework) 的项目
-
点击进入 From.cs[设计],可将界面设计如下:
二、只用单一进程实现
-
核心代码
//用单一进程测试端口扫描 private void main_Thread() { listBox1.Items.Add("开始扫描……(该过程可能会等待一定时间!)"); for (int i = startPort; i <= endPort; i++) { progressBar1.Value = i; label5.Text = i.ToString(); try { TcpClient tcp = new TcpClient(IP, i); listBox1.Items.Add("端口 " + i.ToString() + " 开放!"); } catch { } } }
-
效果如下
由图可见,程序执行十分的卡顿,窗体界面已经不能拖动了。此时,应该用多线程的方式解决。
三、多线程的说明
-
线程: 是程序中的一个执行流,每个线程都有自己的专有寄存器(栈指针、程序计数器等),但代码区是共享的,即不同的线程可以执行同样的函数。
-
多线程: 是指程序中包含多个执行流,即在一个程序中可以同时运行多个不同的线程来执行不同的任务,也就是说允许单个程序创建多个并行执行的线程来完成各自的任务。
-
多线程的好处: 可以提高CPU的利用率。在多线程程序中,一个线程必须等待的时候,CPU可以运行其它的线程而不是等待,这样就大大提高了程序的效率。
四、用多线程方式实现
-
C# 实现多线程有多种方法,这里主要用 Thread类 实现
a.
用 Thread类 创建线程ThreadStart childref = new ThreadStart(CallToChildThread); Thread childThread = new Thread(childref); childThread.Start();
ThreadStart 委托定义了一个返回类型为void的无参数方法(即,CallToChildThread为一个void类型的无参函数)
Thread类 的构造函数重载为接受 ThreadStart 和 ParameterizedThreadStart 类型的委托参数
Start() 方法启动线程
Thread类 的更多属性和方法请参考:C# 多线程
-
实现流程大致如下
-
核心代码
a.
添加 button 按钮 click 事件private void button_Click(object sender, EventArgs e) { //创建线程,并创建 ThreadStart 委托对象 ThreadStart childScan = new ThreadStart(port_Scan); Thread childThread = new Thread(childScan); childThread.Start(); //显示端口扫描范围 progressBar1.Minimum = startPort; progressBar1.Maximum = endPort; //main_Thread(); //显示初始化端口信息 listBox1.Items.Clear(); listBox1.Items.Add("欢迎使用端口扫描器 v1.0!!!"); }
b.
线程 childThread 传入的 ThreadStart 委托对象的返回类型为void的无参数方法 port_Scan()private void port_Scan() { //检查端口合法性 if((startPort>=0 && startPort<=65535) && (endPort>=0 && endPort<=65535) && (startPort <= endPort)) { if(IP!="" && judge_IP()) { listBox1.Items.Clear(); listBox1.Items.Add("开始扫描……(该过程可能会等待一定时间!)"); for (int i=startPort; i <= endPort; i++) { port = i; //创建该端口的扫描线程 ThreadStart childScan = new ThreadStart(Scan); Thread scanThread = new Thread(childScan); scanThread.Start(); //使线程暂停100ms System.Threading.Thread.Sleep(100); progressBar1.Value = i; label5.Text = i.ToString(); } bool flag = true; while (flag) { int i; for(i = startPort; i <= endPort; i++) { if (!done[i]) { break; } } if (i == endPort + 1) { flag = false; } //线程暂停2000ms System.Threading.Thread.Sleep(2000); } listBox1.Items.Add("扫描结束!!!"); } else { MessageBox.Show("IP地址不合法!", "Error", MessageBoxButtons.OKCancel, MessageBoxIcon.Error); } } else { //错误消息反馈 MessageBox.Show("端口不合法!", "Error", MessageBoxButtons.OKCancel,MessageBoxIcon.Error); } }
c.
线程 scanThread传入的 ThreadStart 委托对象的返回类型为void的无参数方法 Scan()private void Scan() { int portnow = port; //记录该端口已访问 done[portnow] = true; try { TcpClient tcp = new TcpClient(IP, portnow); listBox1.Items.Add("端口 " + portnow.ToString() + " 开放!"); } catch { } }
-
异常错误处理
错误原因是因为子线程修改了主线程中的 UI控件的参数
a.
解决方案一:在窗体的 构造函数Form1() 中添加如下代码:
System.Windows.Forms.Control.CheckForIllegalCrossThreadCalls = false;
即:禁止捕获对错误线程的调用,但该方法存在一定的安全性
b.
采用委托的方式(微软官方推荐)this.Invoke((EventHandler)delegate { listBox1.Items.Clear(); button1.Enabled = false; listBox1.Items.Add("开始扫描……(该过程可能会等待一定时间!)"); });
即:子线程应该使用一个委托让UI线程来执行修改UI控件参数的操作
Invoke方法的说明和用法具体可参考:C#中Invoke的用法()
-
最终实现效果
与单一的进程相比,程序的流畅性有了显著的提高
五、参考🔗
- 学习多线程之前需提前了解的小知识
- [深入学习C#]C#实现多线程的方法:线程(Thread类)和线程池(ThreadPool)
- C# 异常:已引发: “线程间操作无效: 从不是创建控件“textBox1”的线程访问它。” (System.InvalidOperationException)
- c#学习笔记之Invoke()作用分析
六、完整源码
链接:https://pan.baidu.com/s/1jr5ro74MwI5gLpja8y4vjQ
提取码:n05v