
启动一个服务时终端突然出现Address already in use很容易下意识去搜索“如何杀掉端口”。但端口不是进程真正需要查清的是哪个地址上的哪个协议端口正被哪个进程使用它是不是自己刚启动的测试服务停止以后监听和访问结果有没有一起改变这一篇用 Python 自带的 HTTP 测试服务器做一个完整实验检查环境和端口启动服务用curl验证内容用ss找到监听者再启动第二个相同服务制造冲突最后释放端口并重新启动。1. 端口、地址、进程三个概念不要混在一起对于这个 HTTP 服务可以把本地监听目标写成IPv4 TCP 127.0.0.1 8000。只看到一个“8000”信息还不完整。127.0.0.1是 IPv4 回环地址本例从同一 Linux 网络环境访问它。8000是端口号客户端 URL 中通过冒号与地址分隔。TCP 是本例使用的传输协议不能把它与 UDP 的同号端口混为一谈。Python 进程创建监听套接字ss可以显示套接字信息并在权限允许时关联到进程。本文复现的是两个普通 Python 服务尝试在同一网络环境、相同地址和 TCP 端口上监听的冲突。不要把它扩大成“任何情况下一个端口数字都只能对应一个进程”不同协议、地址、网络命名空间以及特定的套接字复用机制会让情况更复杂。本次不配置这些特殊机制。我们通过一个明确、可控的例子练习最常见的排查顺序。2. 检查依赖先确认自己在哪里操作终端 A 使用 Linux BashWSL 用户先进入 Ubuntu。下面的命令不要直接在 Windows PowerShell 中执行。idcommand-vpython3curlsstimeoutpython3--versioncurl--versionss--versionid应显示自己的普通用户如果是uid0(root)先换到普通用户。command -v用于确认命令能够找到。四个命令都应有对应位置若其中某个没有输出或提示找不到先处理依赖不要把后面的失败误判为端口问题。python3提供测试服务器curl发出 HTTP 请求ss查看套接字timeout则为后面的冲突复现提供最长五秒的保护。本机验证环境已具备这些工具没有为了实验临时安装软件或改变系统服务。另开终端 B 时仍进入同一个 WSL 发行版或同一台 Linux 的同一网络环境。本篇不依赖 Windows 到 WSL 的 localhost 转发也不把容器里的回环地址当作宿主机回环地址。先在相同环境中完成实验可以减少不相关的网络变量。3. 启动前先查 8000不接管已有服务在终端 A 执行ss-ltnpsport :8000几个选项分别是写法作用-l只查看监听套接字-t只查看 TCP-n用数字显示地址和端口避免转换成服务名称-p尝试显示使用套接字的进程信息sport :8000按本地端口 8000 过滤这里的引号让过滤表达式作为一个参数传给ss。使用字段过滤比把所有输出交给简单的字符串搜索更准确避免把其他字段中的相似数字也匹配进来。如果只有表头、没有监听数据行说明这次查询没有发现匹配的 TCP 监听。如果已经存在任何 8000 的监听行先不要继续本篇的 8000 实验也不要停止它。那可能是自己其他项目的服务。读者可以另外选择一个已确认空闲的高位端口但必须同步修改服务器、URL 和ss过滤条件。本文实际验证的端口是 8000没有把其他端口写成实测结果。这条查询不是对未来状态的预约。检查之后到启动之前其他进程仍可能抢先绑定端口实际启动结果和后续验收才是最终依据。另外监听查询并不列出所有可能影响绑定的套接字状态所以“没有监听行”也不是任意程序都保证能绑定的证明。4. 建立实验目录把网页与日志分开放确认可以继续后在终端 A 创建目录lab_dir$(mktemp-d$HOME/linux-lab-day5.XXXXXX)cd$lab_dir||exitmkdirsiteprintf%s\nlinux-port-lab-oksite/index.htmlpwdmktemp -d每次生成新的随机目录避免覆盖其他实验。创建失败时请停止cd失败后的exit会结束当前 Shell不能忽略前面的错误。site/index.html是准备通过 HTTP 返回的唯一测试文件。虽然文件名是.html这次故意只写一行普通文本方便用curl精确比较内容。它不是一张假截图也不包含真实业务数据。后面把日志放在实验目录根部把对外提供的文件限制在site子目录。不要在主目录、代码仓库或装有密钥的目录里直接启动文件服务器也不要往这个测试目录放指向其他目录的符号链接。Python 的这种目录服务不是一个通用隔离沙箱。5. 启动第一个服务立即记录它的 PID仍在终端 A把下面整段执行一次python3-u-mhttp.server8000\--bind127.0.0.1\--directory$lab_dir/siteserver.log21server_pid$!printf本次服务 PID%s\n$server_pid每行末尾的反斜杠用于续行它后面不要再加空格。也可以把前三行合成一行参数含义不变。-u让 Python 的标准输出和标准错误不缓冲便于及时查看启动信息。-m http.server运行 Python 的标准库模块。8000指定监听端口。--bind 127.0.0.1明确使用 IPv4 回环地址。省略这一项Python 默认可能绑定所有网络接口不符合本篇实验范围。--directory指定提供文件的根目录避免当前工作目录变化影响理解。 server.log 21把标准输出及标准错误写入同一个日志文件顺序不要颠倒。后台运行server_pid$!紧接着保存这次子进程 PID。这里的会覆盖同名日志因此这一段只在新实验目录执行。不要一边反复点击启动一边覆盖证据后面重启使用另一个日志文件。取得 PID 不代表服务已经启动成功。进程可能因为参数错误、权限或端口占用很快退出。下一步必须核查日志、监听和请求结果。6. 在终端 B 验证“能连上而且内容正确”终端 B 不需要使用 A 的$lab_dir或$server_pid也不用切换到实验目录它只做网络请求和只读查询curl--noproxy*--max-time3-fsShttp://127.0.0.1:8000/printfcurl 状态%s\n$?ss-ltnpsport :8000正常情况下响应正文是linux-port-lab-okcurl状态应为0。各选项值得单独说明选项这里解决的问题--noproxy *此次请求不走代理避免环境代理影响回环地址实验--max-time 3将一次传输的最长等待限制为三秒-f对 HTTP 错误响应返回失败便于排查-s不显示进度条-S与-s配合时仍显示错误信息星号必须加引号避免 Shell 把它展开为当前目录的文件名。这是当前命令的选项没有修改系统代理设置。如果启动后立即请求偶尔连接失败先回到 A 查看cat server.log。服务启动有一个短暂过程确认没有启动错误后再请求一次。本次自动验证使用有次数上限的就绪检查没有把一次失败掩盖为成功也没有无限重试。只看返回码还不够要比较正文标记。如果返回别的页面即使 HTTP 请求成功也可能访问了另一个服务不能继续认定目标正确。6.1 读懂 ss 的监听行本次实测的第一条服务进程 PID 为598。下面是实际监听行的摘录LISTEN 0 5 127.0.0.1:8000 0.0.0.0:* users:((python3,pid598,fd3))你的 PID、文件描述符和列宽可能不同不要复制598去结束进程。Local Address:Port一列中的127.0.0.1:8000才是本地监听地址。后面的0.0.0.0:*位于对端列不表示服务已经绑定到所有本地接口监听套接字尚未绑定到某一个具体客户端因此不能把两个地址列读反。users中的pid应与终端 A 打印的server_pid一致。fd是该进程的文件描述符编号不是另一个端口号。普通用户有时能看到监听却看不到完整进程信息尤其是其他用户的进程。没有users字段不等于端口空闲。本例服务由同一用户启动验证时能够读到 PID其他环境应结合权限与运行位置解释。再回到终端 A 核对进程命令ps-p$server_pid-opid,ppid,user,argscatserver.log应对应本次 Python 模块、8000、回环地址以及本次site路径。把进程、监听和响应正文三条证据连起来比只凭“python3”这个名字判断可靠。7. 制造一次真实的端口冲突保持第一个服务运行。在终端 A 执行第二次启动但这次不加并用timeout限制最长运行时间timeout5python3-u-mhttp.server8000\--bind127.0.0.1\--directory$lab_dir/siteconflict.log21conflict_status$?printf第二次启动状态%s\n$conflict_statuscatconflict.log本次 Linux/Python 环境的实际结果是状态1回溯最后包含OSError: [Errno 98] Address already in use这不是伪造的示例错误。本次测试在第一个服务已确认正常监听后真实启动了第二个相同服务并检查错误文本和退出状态。Python 版本不同时回溯中的文件路径与行号可能变化诊断时重点看最终异常及发生阶段。timeout 5是实验保护假如第一个服务意外提前结束第二次启动可能反而成功。这样它最多运行五秒左右避免你误以为“没有报错所以一直等着就是卡死”。如果观察到124应按超时来解释不能写成已成功复现端口冲突重新检查前一个服务状态。这段存在预期的非零退出状态建议在普通交互 Bash 中逐段运行不要先启用set -e再把整篇当成一个脚本粘贴。conflict_status$?紧接着保存前一条命令状态中间不能插入别的命令。此时再在 B 执行前面的curl仍应得到linux-port-lab-ok。第二个进程绑定失败并没有自动替换或停止第一个服务。本次验证也检查了“冲突后第一个服务仍能返回同样内容”。8. 只停止自己启动的第一个服务回到最初启动服务的终端 A。再核对一次 PID、命令、用户和监听关联ps-p$server_pid-opid,ppid,user,args ss-ltnpsport :8000只有确认仍是本次自己的服务才执行kill-TERM$server_pidifwait$server_pid;thenstop_status0elsestop_status$?fiprintf停止后的 wait 状态%s\n$stop_status如果kill先报不存在或权限错误就停下来检查不要把失败当成成功。这里不能把server_pid换成随便从系统进程列表找来的数字。本例用 SIGTERM 结束测试进程同一 Bash 中的wait获取它的结束状态。本次结果为143是 Linux/Bash 中因 TERM 信号结束时的预期表现不代表整个实验失败。wait需要原来的 Shell不能换到 B 再对 A 的子进程读取结束状态。不使用按名字批量结束 Python 的操作也不把“某端口被占用”当成停止所有相关程序的授权。现实服务还可能有数据库连接、队列消费和未完成请求不能照搬本篇的临时服务停机方式。8.1 停止后做两项验收在 B 执行ss-ltnpsport :8000curl--noproxy*--max-time3-fsShttp://127.0.0.1:8000/printf停止后 curl 状态%s\n$?本次停止后匹配的监听数据行消失curl提示连接被拒绝状态为7。这是我们主动停止服务后的预期结果。连接失败的具体表现与环境有关例如超时和连接被拒绝并不相同。不要把7当作所有网络故障的统一代码更不要只凭一次失败就推断远端服务器已经关机。本篇结合明确的停止动作和本机监听变化做结论。ss -l只查看监听。即使其他查询还能看到相关的历史连接状态也不能把它直接当成“原服务还在监听”的证据要核对状态与本地地址列。9. 在同一个端口重新启动并再次验收确认监听已释放后终端 A 执行python3-u-mhttp.server8000\--bind127.0.0.1\--directory$lab_dir/siterestart.log21restart_pid$!printf重新启动 PID%s\n$restart_pid这次使用新的restart_pid和restart.log保留上一轮记录。PID 可能与以前不同本次实测从598变为623这两个数字只是本次测试证据不是教程要求。终端 B 再执行相同的curl与ss检查响应正文恢复为linux-port-lab-ok监听重新出现显示的 PID 对应 A 中新捕获的值。启动失败时先检查restart.log不要重复启动一串新进程。“能重新启动并返回正确内容”比单独看到停止命令没有报错更完整地证明了这次实验的端口释放与恢复过程。但它仍只说明这个临时服务不是对生产应用的健康检查结论。最后回到 A确认新的目标仍是自己的实验服务后结束它ps-p$restart_pid-opid,ppid,user,argskill-TERM$restart_pidifwait$restart_pid;thenfinal_status0elsefinal_status$?fiprintf最终 wait 状态%s\n$final_statusss-ltnpsport :8000本次最终状态为1438000 上没有留下实验监听。清理重点是先结束本次进程再考虑是否保留文件删除目录本身不会替你可靠地停止服务器。10. 排错时先判断失败发生在哪一层现象优先核查本篇中的区分方法command not found依赖和当前 Shellcommand -v确认已进入 Linux启动时地址已占用本地绑定阶段查看启动日志与ss的监听者有 PID但没有监听进程是否已经退出或尚未就绪查询ps、日志再看实际请求curl连接被拒绝对应地址端口是否有监听核对地址、端口、网络环境能连接但返回别的内容是否访问了另一个服务比对测试正文和 PIDHTTP 404已有 HTTP 响应路径或文件可能不对查 URL、--directory和文件名ss有行但没有 PID进程信息可见权限不把缺少进程字段等同于空闲WSL 可访问Windows 不同跨环境网络行为本篇先在同一 WSL 内验收HTTP 404 与 TCP 连接失败处于不同层面。收到 404 时对端已经返回了 HTTP 响应继续寻找“为什么没有监听”通常不是当前问题。反过来连接都没建立也没有应用层网页内容可供分析。排查实际问题时先保存原始错误信息和命令不要上来就改防火墙、关闭安全软件或重启机器。本篇没有通过修改这些设置让实验看起来成功。11. 一张表复盘全过程阶段监听请求结果本次实测启动前未发现 8000 监听尚未提供实验内容通过前置检查第一次启动后127.0.0.1:8000测试标记文本PID 598第二次启动原监听仍在第一个服务仍能响应第二个服务报 Errno 98状态 1停止第一个后监听消失连接被拒绝curl 状态 7再次启动后新监听出现测试标记恢复PID 623最后收尾无实验监听服务已结束两次 TERM 后 wait 均为 143这里的具体 PID 不可复制状态和结果也必须结合步骤解释。你自己的实验记录可以不同但“谁在监听、请求到了谁、停止的是谁、最后留下了什么”应当能相互对应。12. 留给读者的练习练习一只改客户端端口会怎样如果服务仍监听 8000而 URL 改成另一个没有服务的端口无法因为服务器进程还在就保证请求成功。端口是连接目标的一部分服务端和客户端必须一致。练习二为什么不直接使用 localhost本篇固定 IPv4 字面地址避免名字解析到 IPv6::1等结果时引入额外差异。localhost并不天然等于“本次 Python 已经绑定的那个地址”。练习三删除 index.html 等于停止服务吗不等于。文件决定服务能返回哪些内容监听套接字由运行中的进程管理。不要把文件状态与进程生命周期混为一谈。这些是理解题没有把未运行的变式当成实测结果。完成主实验后保留日志即可如需清理文件只处理自己这次pwd显示的确切目录不做批量删除。下一篇练习tar打包、查看归档、恢复到独立目录再实际比较恢复结果和原文件是否一致。