ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

C语言与Go实现端口扫描器:多线程与协程并发模型解析

C语言与Go实现端口扫描器:多线程与协程并发模型解析 简介一套适用于网络编程与信息安全课程设计的端口扫描工具资料包完整交付课程设计报告、答辩演示文稿与双语言项目源码。项目采用C语言与Go语言分别实现TCP-connect、SYN、FIN、UDP四种主流扫描方式并通过Go协程生产者消费者模型、C多线程并发减少Socket I/O等待显著提升扫描效率。压缩包共25个文件、约6.35MB除C/Go源码与对应头文件外还包含Word版设计报告、答辩PPT演示稿、Markdown技术报告、Makefile构建脚本及说明文档覆盖从原理分析到编码实现再到汇报展示的全过程。已有1434人学习下载适合需要借鉴完整课程设计方案、理解端口扫描原理及高并发网络编程技巧的本科学生或开发者。1. 基于C语言的端口扫描工具设计与实现不只是一份课设代码说到C语言的端口扫描工具第一反应往往是nmap可课程设计不会让你直接调system(nmap)而是要你把TCP connect、SYN、FIN、UDP这四种扫描方式自己写一遍。这份资源恰好把C和Go两个完整实现都摆出来了C版用多线程Go版用携程加生产者消费者模型还附带课设报告、技术报告和答辩PPT。适合三类人正在做网络安全或网络编程课设的在校生、想搞懂socket原始报文细节的自学者、以及需要一份能改能跑的扫描器源码的从业者。我完整拆了一遍C版的坑全在报文结构和轮询等待上Go版的坑全在并发控制和channel设计上。下面从资源结构开始按实际动手顺序把每一部分说透。2. 资源的真实结构报告、PPT、源码该怎么配合使用2.1 下载包内文件清单与使用时机解压之后第一件事不是看代码而是先认清这个资源里每部分文件的用途。目录里有这些核心内容C语言的多个源文件tcpConScan.c、tcpSynScan.c、tcpFinScan.c、udpIcmpScan.c及对应头文件、mysock.h一个Go语言的portscan.go还有main.c、Makefile、CMakeLists.txt、技术报告.md、设计报告.docx、答辩PPT端口扫描工具设计与实现.pptx以及一个已经编译好的portscan可执行文件。建议的使用顺序是先读技术报告.md理解四种扫描方式的判定原理再看设计报告.docx这是课设文档的模板参考最后打开源码对照着编译。PPT是答辩前临时准备的但里面的流程图和对比表写得很清楚适合用来快速了解整体框架。README.md 里有编译和运行的基本命令后续我会按实际踩坑再补充一遍。文件作用什么时候看技术报告.md原理、数据结构、流程设计动手修改前设计报告.docx课设文档主体写报告时对照答辩PPT项目讲解大纲演示前C源码头文件多线程扫描实现编译、改功能时portscan.goGo携程实现对比学习并发模型Makefile/CMakeLists构建配置编译报错时portscan可执行文件Linux下已编译产物快速验证功能2.2 为什么C版用多线程、Go版用携程选型不是随便拍的很多人会问同一份工具何必用两种语言写两遍课设要求是两名组员分别完成于是C和Go各出一版。但这不是为了凑工作量两种并发模型恰好代表了不同的落地思路。C版的思路是「多线程加轮询」为每个待扫描端口创建线程线程里发探测报文然后统一轮询多个socket的响应。这里的关键是避免阻塞——如果用阻塞式recv一个端口超时就让整个扫描卡死几秒几千个端口扫下来要等到天荒地老。所以C版实际上是把socket设成非阻塞用select或poll去同时等待多个描述符哪个有响应就说明哪个端口有结果。我平时写这类工具也习惯这样线程数量控制好后逻辑简单且容易调试。Go版则彻底换了个路子用携程加生产者消费者。生产者协程负责生成并发探测任务消费者协程负责监听返回。C语言多线程是操作系统级别的线程而Go的goroutine是用户态调度的轻量级协程创建成本低得多可以同时开几百上千个而不至于把系统拖垮。生产者消费者模型还有一个额外的好处探测和接收解耦生产者只管发消费者只管收两端速度不一样也不会互相卡住。这个模型放在端口扫描这个场景下比单纯开N个线程更优雅也更接近生产级扫描器的做法。2.3 快速跑起来从解压到出结果的三步命令先把环境准备好。这份资源面向Linux因为SYN和FIN扫描需要原始套接字Windows上SOCK_RAW受限Windows下会比较难搞。代码依赖的只有标准socket库和pthread库Makefile里已经把链接参数写好了。推荐在Ubuntu或CentOS上直接编译。unzip 基于C语言的端口扫描工具设计与实现.zip -d portscan_project cd portscan_project make clean make sudo ./portscan -h第一行把压缩包解压到portscan_project目录第二行进入项目根目录第三行重新编译。这里的 make clean 比较重要下载包里可能带着旧的编译中间文件不清一次容易遇到「明明改了代码运行还是老行为」的玄学问题。最后用sudo执行是因为SYN扫描要构造原始IP包普通用户没有权限打开raw socket。先看-h帮助确认可执行文件正常。如果你在Windows上用VSCode做C语言开发这套代码不能直接跑需要先装WSL或者用虚拟机。不过C源码可以直接导入VSCode阅读vscode配置好C语言环境后至少能语法高亮和跳转。我建议把WSL当成编译环境比在Windows上折腾WinPcap省心得多。2.4 两种构建方式Makefile和CMakeLists.txt的区别资源里同时提供了Makefile和CMakeLists.txt这在课设包里很少见但很实用。Makefile是给习惯命令行的人用的直接make就完了CMakeLists.txt是给用CLion或VSCode插件的人用的图形化配置更友好。我一般习惯Makefile改一下编译选项就够用CMake适合需要跨平台管理依赖的工程。CMakeLists里通常会有add_executable和target_link_librariespthread库是C多线程必须链接的。如果你在自己的机器上编译时出现undefined reference对pthread_create那就是没链接-lpthread。这两种构建方式二选一即可不必两套都跑。3. C语言多线程扫描connect、SYN、FIN各自卡在哪个细节3.1 tcpConScan.c最简单的connect扫描为什么还要处理超时TCP connect扫描是最朴素的思路调用connect()去连接目标端口连得上说明端口开放连不上说明关闭。可如果目标主机不可达或端口被防火墙丢弃connect会一直等系统默认超时长达一两分钟完全没法扫。所以C版这里用了「非阻塞connectselect」的组合拳。// tcpConScan.c 中扫描单个端口的核心片段整理 int scan_port(const char *ip, int port, int timeout_ms) { int sock socket(AF_INET, SOCK_STREAM, 0); int flags fcntl(sock, F_GETFL, 0); fcntl(sock, F_SETFL, flags | O_NONBLOCK); // 切换为非阻塞 struct sockaddr_in addr; addr.sin_family AF_INET; addr.sin_port htons(port); inet_pton(AF_INET, ip, addr.sin_addr); int rc connect(sock, (struct sockaddr *)addr, sizeof(addr)); if (rc ! 0 errno ! EINPROGRESS) { close(sock); return 0; // 直接失败端口不可达 } fd_set wset; FD_ZERO(wset); FD_SET(sock, wset); struct timeval tv { timeout_ms / 1000, (timeout_ms % 1000) * 1000 }; int sel select(sock 1, NULL, wset, NULL, tv); if (sel 0) { close(sock); return 0; } // 超时无响应 int err 0; socklen_t len sizeof(err); getsockopt(sock, SOL_SOCKET, SO_ERROR, err, len); close(sock); return err 0; // 连接成功说明端口开放 }逻辑说明先把socket设为非阻塞connect调用会立即返回。如果返回0表示瞬间就连上了如果返回-1且errno是EINPROGRESS说明连接正在进行需要交给select去等。select监听这个socket的可写事件——连接成功或失败都会让socket可写此时再用getsockopt取出SO_ERROR值为0就是成功非0就是失败。这里的关键参数是timeout_ms。太长会让整个扫描变慢太短会漏掉高延迟主机。内网扫描我一般设1000到1500毫秒公网主机建议设到3000否则容易把开放端口误判成关闭。这一段代码是四种扫描方式里最容易理解的也是后续写报告时最方便画流程图的。3.2 tcpSynScan.c原始套接字和TCP校验和SYN扫描不是靠connect而是自己构造一个TCP SYN报文发出去。如果收到SYN/ACK端口开放收到RST端口关闭不做回应则被过滤。这就需要用到原始套接字构造IP头和TCP头并且自己计算校验和。这一段是C语言课设里最容易翻车的地方。// tcpSynScan.c 中校验和计算的常用实现 unsigned short checksum(unsigned short *buf, int size) { unsigned long sum 0; while (size 1) { sum *buf; size - 2; } if (size 1) { sum *(unsigned char *)buf; } while (sum 16) { sum (sum 0xffff) (sum 16); } return (unsigned short)~sum; } // 构造TCP伪头部用于校验注意伪头部包含源IP、目的IP、协议号、TCP长度 struct pseudo_header { unsigned int src_ip; unsigned int dst_ip; unsigned char reserved; unsigned char protocol; unsigned short tcp_len; };逻辑说明TCP校验和必须包含伪头部伪头部的源IP和目的IP要和IP头里填写的一致否则收包方校验失败会直接丢包。发送时要把校验和字段先置0计算完再把结果填回去。很多人漏掉伪头部导致抓包看到checksum offset错误但程序本身没报错这就是典型的「黑匣子问题」——socket层不会告诉你包发出去是否合理必须靠tcpdump验证。构造SYN包时还需要注意IP头的ihl、total_length、protocol字段TCP头的源端口随机选一个目的端口就是扫描目标标志位设为SYN。发送用sendto接收用recvfrom接收时需要同时解析IP头和TCP头才能拿到SYN/ACK标志。为了避免线程间互相干扰每个线程最好绑定不同的源端口否则内核有可能把对应响应混淆。3.3 tcpFinScan.c和udpIcmpScan.c两个容易被误判的扫描方式FIN扫描的判定逻辑和SYN恰好相反向目标端口发送FIN报文按照RFC 793如果端口关闭会回应RST如果端口开放则忽略这个FIN。所以收到RST反而说明端口关闭没有响应才可能开放。这个逻辑很多初学者写反。另外Windows系统对FIN的处理和RFC并不完全一致这也是这个资源只在Linux下测试的原因之一。UDP扫描要复杂一点。代码udpIcmpScan.c的做法是向目标UDP端口发送一个UDP报文如果收到ICMP端口不可达差错报文说明端口关闭如果没有回应则可能是开放或过滤。这个过程容易误报因为丢包、防火墙静默丢弃都会导致「无响应」的假象。我一般会结合超时阈值和重发次数来降低误报。这段代码里的socket操作也用了轮询多个socket io的方式创建多个UDP socket后用一个select同时监听哪个socket收到ICMP差错就标记对应端口关闭。注意ICMP差错信息并不会直接通过UDP的recvfrom返回而是需要通过另一个原始socket读取所以实践中往往要单独建一个ICMP的接收通道。资源里的实现是把ICMP socket事件一并加入select的读集合这样逻辑上更紧凑。4. Go语言携程实现生产者消费者模型比多线程快在哪4.1 portscan.go的整体结构Go版只有一个portscan.go文件代码量比C版全套源码加起来还少但功能完全对齐。Go的优势在于语言层面就支持并发不需要像C那样手动管理线程和fd_set。整个程序的骨架可以抽象成三个部分任务生成生产者、任务执行消费者、结果收集。// portscan.go 中的核心并发骨架整理 func scanPorts(host string, ports []int) map[int]bool { results : make(map[int]bool) var resultMutex sync.Mutex jobs : make(chan int, 100) // 任务队列缓冲100个端口 resultsCh : make(chan int) // 结果通道 // 生产者把待扫描端口放入jobs通道 go func() { defer close(jobs) for _, port : range ports { jobs - port } }() // 消费者多个goroutine从jobs取端口并执行扫描 var wg sync.WaitGroup for i : 0; i 50; i { // 控制并发消费者数量 wg.Add(1) go func() { defer wg.Done() for port : range jobs { if isPortOpen(host, port, 2*time.Second) { resultsCh - port } } }() } // 结果收集另起一个goroutine写共享结果 go func() { for port : range resultsCh { resultMutex.Lock() results[port] true resultMutex.Unlock() } }() wg.Wait() close(resultsCh) return results }逻辑说明这里最关键的是jobs通道的缓冲大小。生产者的发送速度和消费者的处理速度不一致如果jobs是无缓冲的生产者每次都要等消费者取走才继续并发效果大打折扣缓冲100个端口可以让生产者一口气把任务丢进队列消费者慢慢取。消费者数量50控制的是并发socket连接数这个数字根据系统可用文件描述符调整太小没速度太大会报too many open files。关于锁的使用这里有一个值得注意的设计resultsCh 是带缓冲的消费者往一个独立的通道写结果再单独用一个goroutine把结果从通道搬进带锁的map。这样做的原因是Go map并发读写不安全直接在一个goroutine里写另一个goroutine读会panic。用通道串一下让写map的动作集中在同一个goroutine里锁的粒度也就一瞬。4.2 一次探测的时间估算为什么Go版扫得比C版快C版多线程的「快」受限于线程数和系统调度开销每个线程都是一个完整的调度单位线程切换要进入内核态。Go的goroutine是协作式调度的用户态线程创建开销约为几KB栈空间切换不需要陷入内核所以可以轻松创建几百上千个。但真正让Go版拉开差距的是生产者消费者模型本身。C版的常见写法是「每端口一线程」然后统一轮询当端口数量到几千时线程块容易撑爆内存线程管理本身就是瓶颈。Go版只需要固定数量的消费者goroutine任务通过channel分发给它们无论扫描一万个还是一百万个端口活跃的socket数量始终等于消费者数量系统负载平稳。这里也可以参考 n 值取舍资源里50个消费者是个比较均衡的经验值。不过要注意goroutine快不等于网络快。网络扫描的瓶颈往往是带宽和对方主机的连接处理能力开太多并发反而会把目标主机或者中间路由器打爆导致丢包率上升。生产级工具nmap默认的并发也有限制不是越多越好。4.3 TCP connect在Go里的超时控制Go的net包内置了DialTimeout这让connect扫描实现得很简洁。func isPortOpen(host string, port int, timeout time.Duration) bool { conn, err : net.DialTimeout(tcp, fmt.Sprintf(%s:%d, host, port), timeout) if err ! nil { return false } conn.Close() return true }这里用2秒超时作为默认值比C版好写的地方在于不用手动设置非阻塞socketnet包内部已经处理好了。但它仍然是标准的TCP连接走的是完整三次握手也就是connect扫描。如果需要做SYN扫描就得额外用golang.org/x/net的raw socket包那又是另一个层级的复杂度。资源在Go版明确实现的是四种扫描方式SYN、FIN和UDP在Go里的实现思路和C是相通的但用syscall包直接操作不必纠结太多重点理解channel配合带来的异步监听效果。5. 编译与运行排查从Makefile到socket的常见问题5.1 现象运行时报 Operation not permitted代码却编译通过原因SYN、FIN以及UDP的ICMP监听都涉及原始套接字SOCK_RAW而创建raw socket需要root权限。普通用户执行时socket函数返回-1perror直接打印Operation not permitted。解决直接用sudo运行可执行文件。注意不要sudo make只需要sudo加在执行扫描命令上。如果你是在受限的实验室机器上也可以临时设置setcap cap_net_rawep给可执行文件但这要求内核支持文件系统扩展属性不如sudo省事。我自己的习惯是编译用普通用户跑扫描一律sudo避免整个项目目录被root创建的中间文件污染。5.2 现象扫描到1024以上端口就部分丢失或程序退出原因C版如果每端口一线程并要求select同时监听fd_set的FD_SETSIZE通常是1024超过这个数select就管不过来。即使你手动分配fd_set单个进程的文件描述符上限也可能不够。解决优先改C版的并发模型把「一次监听所有端口」改为分批扫描每批最多256个端口一个线程处理一批。另一个更彻底的方案是换用poll或epoll它们是动态数组不受1024限制。Go版则要注意ulimit -n如果消费者开到500以上默认1024的fd限制很容易触碰可以先执行ulimit -n 4096调大。我从第一次跑全端口扫描起就把每批端口数固定成256这个边界参数值得记住。5.3 现象SYN扫描收不到任何响应但connect扫描一切正常原因构造的TCP校验和或IP头总长度不对对方收到报文后直接丢弃。这种问题程序本身不报错是最容易陷入玄学的一类。解决打开第二个终端抓包验证。用tcpdump -i any tcp port 目标端口观察发出的包是否正常。重点看checksum这一行有没有出现incorrect字样。如果错误检查伪头部是否包含完整校验和函数是否按16位对齐。抓包时你会看到链接层是否真的把包发出去了也会看到目标是否回RST。这个过程属于「血泪经验」我写SYN扫描时两次栽在校验和上从此养成了先抓包再调逻辑的习惯。5.4 现象UDP扫描结果中几乎所有端口都显示「开放」原因UDP无连接端口是否开放只能靠ICMP差错报文来判断。很多网络环境会限制ICMP报文的传输或者防火墙直接丢弃导致目标主机明明没有回应ICMP不可达你的扫描端却一直在等待超时。超时设得太长所有无响应端口都计入开放误报率暴涨。解决一是缩短UDP超时一般设800到1000毫秒即可比TCP的超时短二是增加探测次数对无响应的端口重发2到3次仍然无响应才记录为open/filtered。资源里把UDP和ICMP放在同一个模块我认为设计意图就是让ICMP的socket和UDP socket统一被select监听减小等待窗口。如果你扫的是内网靶机可以把超时设为500毫秒基本够用。5.5 现象按提示输入扫描范围时回车后程序直接跳过或读取到脏数据原因课程设计里的main.c通常会用一个scanf读取起始端口和结束端口。scanf接收数字后缓冲区里残留一个换行符如果之后紧接着再用gets或另一个scanf读到的就是空串或垃圾值。这是C语言文件缓冲区最常见的坑和端口扫描本身无关。解决统一用fgets读一行再用atoi或sscanf解析避免scanf残留。比如先char line[64]; fgets(line, sizeof(line), stdin); 然后int start atoi(line);。这样输入和解析分开缓冲区不再互相干扰。vscode调试这类交互式程序时建议在终端里运行而不是在“运行和调试”面板里直接输入后者有时会吞掉第一行输入。从那以后我写C语言交互逻辑都强制走一遍「fgetsatoi」的组合再也没翻过车。6. 进阶验证把C版和Go版的扫描结果与nmap对齐6.1 用nmap做基准测试扫一个东西之前先得有正确答案。我会在局域网里启动一台靶机分别开放几个已知端口比如22、80、443、3306然后用这套工具和nmap分别扫同一网段对比结果。# 用本工具扫描单个IP的1-1024端口 sudo ./portscan -h 192.168.1.100 -p 1-1024 # 用nmap做同步验证 nmap -sS -p 1-1024 192.168.1.100 # 抓包确认四次方式的报文差异 sudo tcpdump -i eth0 host 192.168.1.100 and tcp对比时重点看三类差异一是TCP connect扫描结果是否和nmap一致不一致基本是超时设短了二是SYN扫描是否能识别出connect扫不到的过滤端口nmap的-sS输出里有个filtered状态本工具如果没实现这个状态至少要把无响应的端口和明确拒绝的区分开三是UDP扫描的误报率当nmap显示udp端口为closed而本工具显示open时先查ICMP配置。扫描方式开放特征关闭特征过滤/无法判断TCP connectconnect成功连接拒绝超时SYN收到SYN/ACK收到RST无响应FIN无响应收到RST无响应(同开放混淆)UDP无响应收到ICMP不可达无响应(同开放混淆)这张表直接写进了课设报告答辩时老师一眼就看出你理解四种方式的异同强烈建议放到技术报告里。6.2 超时、并发数、批处理组合调优我给这两个工具分别整理了一组经验参数。C版每批256个端口select超时1.2秒线程数等于端口批次数。Go版消费者50个jobs缓冲100DialTimeout设2秒。在这组参数下扫一个C类网段254个主机的常用端口C版大约10分钟Go版能跑到4分钟左右。差别主要来自goroutine的调度开销低以及任务队列在空闲时的利用效率更高。如果扫公网IP超时建议加到3秒并发减半否则大量无响应会让队列长期堆积实际吞吐反而下降。如果扫本机回环地址超时500毫秒就足够不会误判。最后一招是并行分布式把目标IP列表拆成多份分别跑多个扫描进程再合并结果。虽然工具本身没做分布式但它的命令行参数支持指定单一IP配合shell脚本循环即可。这种方法在课程设计的拓展部分提一句属于加分项。我从第一次自己写完端口扫描器到现在每次拿到新工具都强制走一遍「抓包验证基准对比」的流程。先让已知端口过一遍再上nmap对照最后才放大范围扫。这套习惯救了我很多次也希望帮到你。本文还有配套的精品资源点击获取
返回列表