使用Asyncio开发Python端口扫描器遇文件句柄超限问题求解
解决Asyncio端口扫描器全端口扫描时的“文件过多”问题
问题原因
终端默认的**文件描述符上限(ulimit)**远低于全端口扫描所需的数量(比如默认1024,而你硬编码的并发数是5000),导致瞬间创建大量socket连接时超出系统限制,连接全部失败,因此无开放端口输出。PyCharm启动进程时会自动调整ulimit,所以能正常运行。
具体解决方案
1. 动态适配系统文件描述符上限
不要硬编码并发数,根据系统当前的文件描述符软上限自动计算安全的并发量,预留资源给系统进程:
import resource def get_safe_concurrency_limit(): soft_limit, _ = resource.getrlimit(resource.RLIMIT_NOFILE) # 预留20%的描述符,避免资源耗尽 return int(soft_limit * 0.8)
在main函数中替换硬编码的并发数:
async def main(host): host = host port_range = (0, 65535) safe_limit = get_safe_concurrency_limit() # 动态获取安全并发数 try: open_ports = await scan_ports(host, port_range, safe_limit) # ... 后续代码不变
2. 分批扫描,避免瞬间创建过多任务
一次性创建65536个扫描任务会瞬间占满文件描述符,改成分批处理:
async def scan_ports(host, port_range, limit): open_ports = [] semaphore = asyncio.Semaphore(limit) ports = list(range(*port_range)) # 按批次处理端口 for i in range(0, len(ports), limit): batch_ports = ports[i:i+limit] scan_tasks = [scan_port(semaphore, host, port) for port in batch_ports] for future in asyncio.as_completed(scan_tasks): result = await future if result: open_ports.append(result) return open_ports
3. 修复代码执行与阻塞问题
- 修复main函数执行入口:原代码在终端运行时未启动事件循环,添加完整执行逻辑:
if __name__ == "__main__": import sys if len(sys.argv) != 2: print("使用方式: python port_scanner.py <目标主机IP>") sys.exit(1) target_host = sys.argv[1] asyncio.run(main(target_host))
- 将同步服务探测改为异步:原
guess_service是同步函数,会阻塞事件循环,改成异步版本:
async def guess_service(host, port): try: reader, writer = await asyncio.open_connection(host, port) # 发送测试指令 test_cmd = b"NOOP\r\n" if port == 21 else b"TEST\r\n" writer.write(test_cmd) await writer.drain() # 读取初始响应 try: response_bv = await asyncio.wait_for(reader.read(1024), timeout=1) response_bv = response_bv.decode() print(f"端口 {port} 初始响应: {response_bv}") except asyncio.TimeoutError: print(f"端口 {port} 响应超时") response_bv = "" service_name = socket.getservbyport(port) writer.close() await writer.wait_closed() return service_name, response_bv except Exception as e: print(f"端口 {port} 服务探测失败: {e}") return None, None
在main中异步调用:
for port in open_ports: try: service_name, response = await guess_service(host, port) if service_name: dict[port] = [service_name, response] except Exception as e: print(f"无法探测端口 {port} 的服务,可能被隐藏")
4. 临时调整ulimit(快速测试用)
如果需要临时在终端运行,可先调整文件描述符上限:
# Linux/macOS终端中执行,临时设置软上限为65536 ulimit -n 65536
注意:此设置仅对当前终端会话有效,重启后恢复默认。
总结
通过动态适配系统资源、分批控制并发、修复代码阻塞与执行问题,即可解决全端口扫描时的“文件过多”问题,同时保证扫描效率与系统稳定性。
内容的提问来源于stack exchange,提问作者GabiDev
相关产品推荐
相关产品推荐

