免责声明:本文仅供学习交流,请勿用于非法扫描他人设备。你扫你自己的电脑,我鼓掌;你扫隔壁老王的服务器,警察叔叔鼓掌 👮♂️。
一、故事从一个"好奇"开始
某天深夜,我盯着路由器发呆:"嘿,我家这台小铁盒子,到底开了哪些端口?会不会有神秘服务在偷偷运行?"
于是------我决定写个工具,批量敲门:
"你好,请问 1 号端口在家吗?"
"2 号呢?"
"......65535 号?你睡了吗?"
结果发现:
22 号(SSH)说:"密码不对,走开!" 80 号(HTTP)热情招呼:"欢迎访问我的 404 页面!" 而 1433 号(MSSQL)......根本没人应门 😢 今天,我就带你用 Go 语言,100 行代码实现一个高并发 TCP 端口扫描器,轻松探查目标主机哪些"门"是开着的!
二、核心思路:生产者 + 消费者模型
我们的程序就像一家快递公司:
老板(main):把 65535 个包裹(端口任务)放进仓库(channel)。 快递员(goroutine):100 个(或你指定的数量)工人同时取件、送货(尝试连接)。 成功送达? → 记录日志:"XX 端口有人签收!" 没人收? → 默默扔掉,继续下一件。 整个过程又快又稳,全靠 Go 的并发神器:goroutine + channel!
三、关键 Go 知识点解析
1. goroutine:轻量级线程
go
scanWorker() // 启动一个后台任务
- 不是操作系统线程,而是 Go 调度器管理的"绿色线程"。
- 启动成本极低,100 个并发轻轻松松。
2. channel:安全通信管道
go
taskCh := make(chan string, 1000)
- 带缓冲的 channel,作为任务队列。
- 多个 goroutine 安全地从中读取任务,无需加锁!
3. sync.WaitGroup:等待所有任务完成
go
wg.Add(1)
defer wg.Done()
wg.Wait()
- 主 goroutine 会卡在 Wait(),直到所有 worker 执行完毕。
- 避免程序提前退出。
4. net.DialTimeout:带超时的 TCP 连接
go
net.DialTimeout("tcp", "192.168.1.1:80", 1*time.Second)
- 如果 1 秒内连不上,就放弃,避免卡死。
- 是端口扫描的核心 API!
5. 命令行参数解析
go
ip := os.Args[1]
threads := os.Args[2]
- 简单直接,适合小型工具。
- (复杂场景可用 flag 或 cobra 库)
6. 错误处理 & 输入校验
- 用 net.ParseIP 验证 IP 格式。
- 用 strconv.Atoi 转换线程数,并检查范围。
- 用户输错?友好提示,优雅退出。
四、运行效果预览
bash
$ ./portscanner 127.0.0.1 50
2025-11-08 22:19:16:330076[main.go:75][INFO]开放的服务:192.168.1.1 [192.168.1.1:25]
2025-11-08 22:19:16:334967[main.go:75][INFO]开放的服务:192.168.1.1 [192.168.1.1:80]
2025-11-08 22:19:17:328821[main.go:75][INFO]开放的服务:192.168.1.1 [192.168.1.1:110]
2025-11-08 22:19:20:333560[main.go:75][INFO]开放的服务:192.168.1.1 [192.168.1.1:443]
...
扫描完成。
同时,日志还会自动保存到 runlog/ 目录,方便后续分析!
五、完整源码奉上!
依赖:github.com/Jjmgx/mgxlog(一个简单的日志库)
安装命令:go get github.com/Jjmgx/mgxlog
go
package main
import (
"fmt"
"net"
"os"
"strconv"
"sync"
"time"
"github.com/Jjmgx/mgxlog"
)
var wg sync.WaitGroup
var taskCh = make(chan string, 1000) // 任务通道:ip:port
var logger, _ = mgxlog.NewMgxLog("runlog/", 10*1024*1024, 100, 3, 1000)
func main() {
if len(os.Args) != 3 {
fmt.Fprintf(os.Stderr, "用法: %s <IP地址> <线程数>\n", os.Args[0])
os.Exit(1)
}
ip := os.Args[1]
threadStr := os.Args[2]
// 验证 IP 格式
if net.ParseIP(ip) == nil {
fmt.Fprintf(os.Stderr, "错误: 无效的 IP 地址 '%s'\n", ip)
os.Exit(1)
}
// 解析线程数
threads, err := strconv.Atoi(threadStr)
if err != nil || threads <= 0 || threads > 10000 {
fmt.Fprintf(os.Stderr, "错误: 线程数必须是 1~10000 之间的整数\n")
os.Exit(1)
}
// 启动 worker goroutines
for i := 0; i < threads; i++ {
wg.Add(1)
go scanWorker()
}
// 生产者:生成所有端口任务(1~65535)
go func() {
defer close(taskCh)
for port := 1; port <= 65535; port++ {
taskCh <- fmt.Sprintf("%s:%d", ip, port)
}
}()
wg.Wait()
fmt.Println("扫描完成。")
}
func scanWorker() {
defer wg.Done()
for ipPort := range taskCh {
scanPort(ipPort)
}
}
func scanPort(ipPort string) {
conn, err := net.DialTimeout("tcp", ipPort, 1*time.Second)
if err != nil {
return // 连不上?下一个!
}
defer conn.Close()
addr := conn.RemoteAddr().(*net.TCPAddr)
msg := fmt.Sprintf("开放的服务:%s [%s:%d]", addr.IP, addr.IP, addr.Port)
logger.Info(msg)
fmt.Println(msg) // 控制台也输出,看得爽!
}
六、结语:技术无罪,滥用有责
这个小工具展示了 Go 在网络编程和并发处理上的强大能力。你可以在此基础上扩展:
- 支持端口范围(如 80,443,8000-9000)
- 识别服务类型(发送 HTTP/TDS/Redis 协议探测包)
- 输出 JSON / CSV 报告
但请记住:好奇心要有,边界感更要有。只扫描你拥有权限的设备,做一名负责任的"数字侦探"🕵️♂️!
往期部分文章列表
- 从"双击打不开"到"管理员都服了":用 Go 打造你的专属 .mgx 编辑器
- 震惊!Go语言居然可以这样玩Windows窗口,告别臃肿GUI库
- 剪贴板监控记:用 Go 写一个 Windows 剪贴板监控器
- 一文讲透 Go 的 defer:你的"善后管家",别让他变成"背锅侠"!
- 你知道程序怎样优雅退出吗?------ Go 开发中的"体面告别"全指南
- 用golang解救PDF文件中的图片只要200行代码!
- 200KB 的烦恼,Go 语言 20 分钟搞定!------ 一个程序员的图片压缩自救指南
- 从"CPU 烧开水"到优雅暂停:Go 里 sync.Cond 的正确打开方式
- 时移世易,篡改天机:吾以 Go 语令 Windows 文件"返老还童"记
- golang圆阵列图记:天灵灵地灵灵图标排圆形
- golang解图记
- 从 4.8 秒到 0.25 秒:我是如何把 Go 正则匹配提速 19 倍的?
- 用 Go 手搓一个内网 DNS 服务器:从此告别 IP 地址,用域名畅游家庭网络!
- 我用Go写了个华容道游戏,曹操终于不用再求关羽了!
- 用 Go 接口把 Excel 变成数据库:一个疯狂但可行的想法
- 穿墙术大揭秘:用 Go 手搓一个"内网穿透"神器!
- 布隆过滤器(go):一个可能犯错但从不撒谎的内存大师
- 自由通讯的魔法:Go从零实现UDP/P2P 聊天工具
- Go语言实现的简易远程传屏工具:让你的屏幕「飞」起来
- 当你的程序学会了"诈尸":Go 实现 Windows 进程守护术
- 验证码识别API:告别收费接口,迎接免费午餐
- 用 Go 给 Windows 装个"顺风耳":两分钟写个录音小工具
- 无奈!我用go写了个MySQL服务
- 使用 Go + govcl 实现 Windows 资源管理器快捷方式管理器
- 用 Go 手搓一个 NTP 服务:从"时间混乱"到"精准同步"的奇幻之旅