☰
Windows端口工具sokit:TCP/UDP联调、HEX收发与半包排查
2026/10/28 16:57:07 网站建设 项目流程

简介:Sokit 1.3 中文版是一款面向 Windows 32 位系统的轻量级端口管理小工具,适合网络管理员、开发者和需要排查本机网络连接问题的普通用户使用。zip 压缩包共 6 个文件,约 3.91MB,包含可执行主程序、htm/txt 使用说明、GPL3 许可文件,以及 lan、log 等配置与日志相关文件,解压后即可按说明运行。它围绕端口扫描、端口监听、连通性测试和端口占用排查等常见场景提供辅助,可帮助用户了解指定端口的开放与连接状态,并结合说明文档与变更日志快速上手。对于需要确认某端口被哪个程序占用、服务是否正常监听,或测试远程端口是否可达的用户,可作为便携式辅助工具,无需复杂安装。已有 4225 人学习下载,适合作为 Windows 端口检查与网络诊断的入门工具包。

1. 先别急着改代码,先把「端口对面」这一端立起来

联调最难受的不是报错,是没有任何反馈——服务端说已经起来了,客户端说连不上,中间那段到底是没监听、被防火墙拦了、还是发出去的字节跟你以为的不一样,全靠猜。Windows 端口工具就是用来消灭这段猜测的,sokit 属于其中比较轻的一类:绿色免安装,解压双击就开,一个界面里能同时当 TCP 服务端、TCP 客户端和 UDP 收发端,文本视图和十六进制视图随时切换,收发的原始字节能直接落盘。

它不负责压测,也不做协议解析,但把「端口通不通、字节对不对」这两件事钉死,已经省掉大量来回。做上位机、写后端接口、调串口转网口模块的人,手里备一个,比每次现开编辑器写二十行脚本快得多。

2. 三种工作模式怎么选:TCP Server、TCP Client 与 UDP 的绑定参数

打开主界面会看到几个模式入口,最容易犯的错是模式选对了、参数填错了。三种模式的本质差别只有一句话:谁先 bind,谁先 connect。TCP Server 是先 bind 本地端口再 listen,等别人连进来;TCP Client 是先 connect 到对端,本地端口由系统分配;UDP 不建连接,bind 本地端口之后,收发都靠数据报的目标地址决定。

选型上,我一般按「我要验证的是谁」来定:要验证自己写的客户端能不能扛住断线、能不能正确组帧,就用 TCP Server 假扮服务端;要验证自己写的服务端有没有正确监听、日志有没有按预期打,就用 TCP Client 连过去;只有设备明确走 UDP,或者要观察广播报文,才动 UDP 模式。别图省事拿 UDP 去测 TCP 服务端,那必然是「发出去了但对面一片空白」,白白浪费半小时。

2.1 TCP Server:监听地址填 0.0.0.0 还是 127.0.0.1

这一栏是最典型的翻车点。监听地址填 127.0.0.1,只有本机能连,同一局域网的另一台机器、机柜里的设备都连不进来;填 0.0.0.0,才是把所有网卡都挂上去监听。我见过不止一次「本机自测通过、联调必挂」的现场,最后追到就是这里。

端口选择也有讲究。1024 以下是系统保留段,虽然 Windows 不像某些平台那样严格限制,但里面一堆端口已经被系统服务占了(比如打印、发现类服务常驻的那几个),撞上就是「监听失败」四个字。日常调试我一般从 8000、9000 这类五位数端口起,冲突概率低,记起来也方便。

还有一个多数人不知道的坑:机器上装了虚拟化组件或者容器相关功能之后,系统会预留一整段端口,落在里面的端口你绑不上,报错却只说绑定失败。用命令查一下预留段,能省掉很多玄学排查时间。

# 1) 查目标端口当前被谁占着(TCP),没有输出说明空闲 Get-NetTCPConnection -LocalPort 9000 -ErrorAction SilentlyContinue | Select-Object LocalAddress, LocalPort, State, OwningProcess # 2) 拿到 PID 反查是哪个进程 $p = (Get-NetTCPConnection -LocalPort 9000 -ErrorAction SilentlyContinue).OwningProcess if ($p) { Get-Process -Id $p | Select-Object Id, ProcessName, Path } # 3) 查系统预留的端口段,落在区间内怎么绑都失败 netsh int ipv4 show excludedportrange protocol=tcp

第一段命令的-LocalPort换成你要用的端口,State为Listen说明有人已经在监听,为Established说明是别人正在用的连接;第二段把 PID 翻成进程名,确认是不是自己上一次没关干净的实例;第三段输出的是一段段区间,看到你的端口落在里面,直接换端口,别挣扎。习惯上我会把这三条串成一个检查动作,开监听之前先跑一遍。

2.2 TCP Client:目标地址、端口与「自动重连」的真实语义

客户端模式要填两样:目标 IP 和目标端口。这里最常见的误解是把「自动重连」当成保活。它做的其实是:连接断了之后隔一段时间重新发起一次 connect,而不是在同一条连接上做心跳。对端如果是正常 close,工具端立刻能感知并重连;但如果是网线被拔、对端掉电这种没有 FIN 的场景,工具端可能长时间认为连接还在——这就是半开连接。

要判断是不是半开,最简单的办法是在工具端手动发一包:能发出去没报错,但对端日志里啥也没有,基本就是半开。真正的解法是应用层心跳,或者让两端都开启 TCP keepalive。工具本身不替你做这件事,它只是个观察窗口。

端口填错的表现也很典型:连内网地址填成外网端口,或者把 UDP 端口填到 TCP 客户端里,界面上给的反馈通常只有「连接失败」或者一直停在连接中。我一般会先用Test-NetConnection确认端口是不是开着,再去工具里填。

# 确认对端 TCP 端口是否可达,先排除网络与防火墙因素 Test-NetConnection -ComputerName 192.168.1.50 -Port 9000 -InformationLevel Detailed # 只看结果不打印一堆中间信息 (Test-NetConnection -ComputerName 192.168.1.50 -Port 9000).TcpTestSucceeded

-ComputerName填对端 IP 或主机名,-Port填端口;返回里的TcpTestSucceeded为 True 才说明三次握手通了,False 就不要再在工具里反复试点,先去看对端有没有监听、防火墙有没有放行。这条命令最大的价值是把「工具连不上」和「网络不通」这两件事分开。

2.3 UDP:本地端口绑定与「发出去收不到」的两种典型

UDP 模式要填的是本地绑定端口和目标地址端口。它的坑和 TCP 完全不同,因为无连接,发送成功只代表数据报交给了系统,不代表任何人收到。

第一种典型:本地端口没绑。工具让系统随机分配一个临时端口发出去,对端如果按「回包发回源端口」的方式回应,理论上没问题;但很多设备是把回包固定发到配置里的某个端口,这时候临时端口就收不到任何东西。所以调 UDP 时,本地端口一定要显式填成设备配置里的那个。

第二种典型:防火墙只放行了 TCP。Windows 防火墙的入站规则是按协议分开的,很多人加规则时顺手选了 TCP,UDP 报文进不来,接收区一片空白。判断方法很简单,本机自测(工具发、工具收,两个实例都填 127.0.0.1)如果通、跨机不通,基本就是防火墙。

# 查 UDP 端口占用,注意 UDP 的 State 列通常为空,看 PID 即可 netstat -ano -p UDP | findstr :9000 # 查 TCP 端口占用,-a 全部连接、-n 数字形式、-o 显示 PID netstat -ano -p TCP | findstr :9000

netstat在 Windows 上依然好用,因为参数短、随手能敲。-p UDP只列 UDP,避免输出太长;findstr :9000里的冒号别省,否则会把包含 9000 的其它数字一起匹配出来。拿到最后一列的 PID,去任务管理器详细信息页按 PID 排序就能定位进程。

2.4 模式组合的实用套路

调试一个双向协议,最省事的做法是开两个实例:一个当服务端监听 9000,另一个当客户端连 127.0.0.1:9000。两边都能看到自己发了什么、收到什么,中间不需要任何真实业务代码参与。等协议帧确认无误,再把其中一端换成真实程序,问题范围立刻就缩小到一端了。

这套「先两端都是工具、再换掉一端」的二分法,是我处理陌生协议时最常用的一招。它不依赖任何日志埋点,也不需要对端配合,代价只是多开一个窗口。

3. 报文格式别猜:HEX/ASCII 视图切换、定时发送与日志落盘

字节对不对,光看界面上的字符是看不出来的。工具提供了文本和十六进制两种呈现方式,理解它们的转换关系,比记住按钮位置重要得多。

3.1 文本模式与十六进制模式:一条中文就能让字节数对不上

文本模式下,你敲进去的字符会按当前选择的字符编码转成字节再发出去。同样是「测试」两个字,UTF-8 下是 6 个字节,GBK 下是 4 个字节,如果对端按固定 4 字节去解析,UTF-8 发过去就是错位。这就是为什么调二进制协议我一律用 HEX 模式:所见即所发,不存在编码转换。

HEX 模式的输入规则各家工具略有差异,但共同点是必须两位一组、以空白分隔、不能带0x前缀。最容易翻车的是全角空格——从文档里复制一串十六进制,中间混了个中文输入法打出来的空格,界面上看着和正常的一模一样,发出去却报格式错误或者直接少发几个字节。血泪经验是:粘贴完先数一遍字节数,跟预期对不上就别点发送。

# 在脚本里先把你打算粘进界面的 HEX 串过一遍,确认字节数 hex_str = "01 03 00 00 00 0A C5 CD" frame = bytes.fromhex(hex_str) print("字节数:", len(frame)) print("标准形式:", frame.hex(" ").upper()) # 半角空格能过,全角空格直接抛异常,这就是「看着一样却发不出去」的根源 for bad in ["01 03\u300000 00", "0x01 0x03", "0103000"]: try: bytes.fromhex(bad) print("ok:", bad) except ValueError as e: print("bad:", repr(bad), "->", e)

bytes.fromhex会忽略半角空白、按两位一组解析,遇到全角空格、0x前缀或者奇数位就会抛ValueError。这段脚本的用处不是替代工具,而是在你把串粘进去之前做一次体检,几秒钟就能定位到底是格式问题还是协议问题。参数上唯一要注意的是:hex(" ")里的分隔符只影响打印,不影响字节内容。

字节数对不上的时候,还有一个隐蔽原因:CRC 校验值的字节序。同一帧数据,大端和小端写出来的 HEX 串后半段是反的,工具照发不误,对端直接判定校验失败。判断办法是把两种字节序都发一遍,看哪种能被接受——这比翻协议文档快。

3.2 定时发送的间隔、次数与「发太快界面卡住」

定时发送是用来模拟周期上报的,参数通常只有两个:间隔和次数(或者是否循环)。看着简单,但间隔设小了会立刻出问题:界面线程忙着刷新接收区,日志文本疯长,几百毫秒内内存就上去了,严重的时候窗口直接无响应。

我的经验值:协议联调阶段间隔不低于 200ms,做吞吐观察时不低于 50ms,次数先填 10 次看行为,确认没问题再放开循环。如果需要更密的发送节奏,那说明这个场景已经超出工具的设计范围了,应该上脚本。

另外一个细节:定时发送和手动发送共用同一个连接,两者混用时接收区里分不清哪条回包对应哪次请求。要么先停掉定时,要么在报文体里加个自增序号,靠序号对齐。

3.3 接收日志保存与第三方抓包对照

工具能把收发内容保存到文件,这个功能在长时间跑的时候很有价值,因为界面上的接收区是有上限的,滚过去就没了。保存时注意两点:一是选对编码,纯文本日志用 UTF-8,二进制的建议直接存十六进制文本,避免不可见字符把文件搞乱;二是先停发送再存盘,运行中写文件容易和界面刷新抢同一份数据。

日志和抓包工具对照,是判断「真没收到」还是「收到了但显示不对」的唯一可靠办法。抓包里能看到完整的 TCP 段和字节内容,工具里能看到解析后的视图。两边一比,问题立刻分类:抓包有、工具没有,是工具端解析或过滤的问题;抓包也没有,就是网络或对端根本没发。

4. 联调实战:拿 sokit 当假服务端、假设备和协议回归器

真正让这个工具值回票价的,是把它当成协议的第一道闸。下面几套做法都是围绕「先把未知变已知」展开的。

4.1 先用 Python 起一个回显服务,把工具端行为固定下来

在动真实业务代码之前,我一般先用十几行脚本起一个原样回显的服务端。好处是它的行为完全确定:收到什么就回什么,不解析、不校验、不落库。这样工具端的收发、编码、显示是否正确,就能独立验证。

import socket import threading HOST, PORT = "0.0.0.0", 9000 # 0.0.0.0 表示监听所有网卡 def handle(conn, addr): print("connected:", addr) try: while True: data = conn.recv(4096) # 单次最多读 4096 字节 if not data: # 对端 close,读到空字节串 break print("recv:", data.hex(" ").upper()) conn.sendall(data) # 原样回显,便于在工具端逐字节比对 finally: conn.close() print("closed:", addr) srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM) srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # 重启时不被 TIME_WAIT 卡住 srv.bind((HOST, PORT)) srv.listen(5) # backlog 5,够调试用 print("listening on", PORT) while True: c, a = srv.accept() threading.Thread(target=handle, args=(c, a), daemon=True).start()

SO_REUSEADDR是关键参数,没有它,脚本停掉后端口会在一段时间内处于 TIME_WAIT,重新启动就报地址已被占用,很容易被误判成「工具没关干净」。listen(5)的 backlog 只影响排队等待 accept 的连接数,调试场景给 5 就够,调大没有意义。recv(4096)的大小决定了单次读取的字节上限,小于这个数的包会一次性返回,大于的会分多次——这也正是后面观察半包的基础。

工具端连上去之后,发一串 HEX,看回显是否逐字节一致。一致说明工具、网络、脚本三方都没问题,接下来任何异常都可以锁定在业务代码里。

4.2 构造一帧带帧头帧尾的私有协议,验证解析边界

私有协议通常长这样:固定帧头 + 长度字段 + 负载 + 校验/帧尾。用工具手发的时候,最容易错的是长度字段的字节序和校验范围。先在脚本里组好帧,打印成 HEX,再粘进工具,能少走很多弯路。

import struct def build(payload: bytes) -> bytes: head = b"\xAA\x55" # 两字节帧头 length = struct.pack(">H", len(payload)) # 大端 2 字节长度,> 表示大端 return head + length + payload + b"\x0D\x0A" print(build(b"HELLO").hex(" ").upper()) # 输出:AA 55 00 05 48 45 4C 4C 4F 0D 0A

struct.pack(">H", ...)里的>是大端,换成<就是小端——这一处符号写错,对端要么丢帧要么报长度越界。长度字段只算负载不算帧头,也是常见约定,跟对端对不上时优先怀疑这里。帧尾我用0D 0A,纯粹因为它可打印、肉眼容易核对,实际协议里换成 CRC 也一样,只是要在脚本里把校验算完再拼。

拿到这串十六进制,粘进工具的发送框,接收端如果原样回显,说明链路和字节完全对齐。这一步过了,再谈业务逻辑。

4.3 半包与粘包:故意分两次发,看接收区怎么显示

TCP 是字节流,没有消息边界。工具里看到的「一条一条」,只是它按读取时机做的显示切分,不代表对端也是这么切的。这个认知不建立起来,后面解析一定出问题。

要亲眼看到半包,最简单的办法是用脚本故意拆开发。

import socket import time s = socket.create_connection(("127.0.0.1", 9000)) s.sendall(b"\xAA\x55\x00\x10") # 先发帧头和长度 time.sleep(0.2) # 人为停顿,制造半包 s.sendall(b"A" * 16) # 再发 16 字节负载 time.sleep(0.2) s.sendall(b"\x0D\x0A") # 最后补帧尾 s.close()

time.sleep(0.2)是故意拖出来的时间窗,让工具端至少读到两次;如果把 sleep 去掉,两段数据大概率会被拼成一个包,那就是粘包。两种现象看到的显示效果不同,但对端解析器必须都能处理——这才是重点。用工具观察几次之后,写解析代码时自然会去写缓冲区累积逻辑,而不是假设一次 recv 就是一帧。

4.4 换成设备侧联调:串口转网口模块的常见配置

真实设备大多不是裸 socket,而是通过串口转网口模块接入。这类模块的参数就那么几个,但对不上就完全不通。下面这张表是我联调时必核对的项:

模块参数常见取值与工具侧的对应关系
工作模式TCP Server / TCP Client / UDP决定工具该选哪种模式对测
本地端口9000 之类五位数工具客户端模式填这个端口
目标 IP / 端口上位机 IP 与监听端口模块作为客户端时必填
打包长度64~512 字节决定多久触发一次发送
打包间隔20~200 ms太小会把一帧拆散,太大则延迟明显
心跳 / 注册包可选,十六进制或字符串工具里要能看到并按格式回应

最常出问题的是打包长度和打包间隔:模块在串口侧收到几个字节就立刻打包发走,导致一帧业务数据被拆成好几包,上位机按定长解析就崩。把打包长度设成大一点、间隔设成几十毫秒,让模块攒够再发,现象会立刻改善。这不是工具的功劳,但没有工具在旁边看字节,你根本不知道是模块拆的包。

5. 排错清单:端口工具用错方向的五个典型现场

下面几条都是我在联调现场真实撞过的,按「现象 → 原因 → 解决」记,对照着看能省时间。

5.1 现象:本机 127.0.0.1 能连,同事的机器连不上

原因基本只有一个:监听地址绑在了回环上。工具默认或手填成 127.0.0.1 时,socket 只挂在回环网卡,外部流量根本到不了这个 socket。防火墙在这一步甚至还没参与。

解决分两步走:把监听地址改成 0.0.0.0 重新监听;然后用 2.2 里那条Test-NetConnection从对端机器验证端口可达。两步都过还连不上,再去看对端机器的入站规则有没有放行该端口和对应协议。顺手提一句,机器的 IP 变了(换网络、DHCP 重新分配)也会导致对端连到旧地址,先在命令行ipconfig确认一下当前地址。

5.2 现象:HEX 发送的字节数比预期多,对端报格式错

原因通常是三种之一:串里混了全角空格、每字节写成了0x前缀、或者中途混进了一个不可见字符(从网页复制十六进制最容易带上)。界面上的预览文本未必能看出差别。

解决:先把串丢进 3.1 那段脚本里过一遍,字节数对不上就别发。另外养成一个习惯,发送前把 HEX 串按两位一组数一遍,和协议文档里的字段长度逐个对齐,比事后抓包找问题快得多。

5.3 现象:定时发送开着,接收区滚到看不见,窗口开始卡

原因是发送间隔设得太小,界面刷新频率跟不上数据量,文本累积把内存推高。这不是工具写得差,是任何带界面的调试工具在每秒上千条消息时都会这样。

解决:间隔先调到 200ms 起步,确认协议逻辑正确之后再逐级往下压到 50ms;需要更高频率就直接写脚本,把结果输出到文件而不是界面。另外接收区数据多的时候及时清屏,长时间跑记得开日志落盘,别指望界面留住历史。

5.4 现象:接收区中文乱码,或者日志文件打开是乱码

原因是两端字符编码不一致。发送端按 UTF-8 编码,接收端按 GBK 解码,就出方块和问号;日志文件的乱码则多半是保存时选了 ANSI,用别的编辑器打开就错。

解决:协议里凡是涉及中文的字段,先明确编码并在两端统一,能不用中文就不用;调二进制协议时切到 HEX 视图,从根上绕开编码问题。日志保存统一选 UTF-8,二次打开时确认编辑器也是 UTF-8。

还有一个更隐蔽的:日志文件被占用。上一轮开着的编辑器没关,工具写盘失败但不一定弹提示,你以为在记录,其实一片空白。写日志前先关掉所有打开该文件的程序。

5.5 现象:客户端模式一直重连,服务端日志刷屏

原因是自动重连间隔太短,或者对端在接受连接后立刻关闭(比如对端有 IP 白名单、有首包校验)。表现出来的现象是连接建立又断开,循环往复,服务端日志被刷满。

解决:先把自动重连关掉,手动连一次,观察连接能维持多久。如果一秒内就被对端断开,去查对端的接入控制逻辑;如果能维持,只是偶尔断,再看是不是心跳超时。定位过程中,把两端的收发日志都打开对照,比盯界面有用。

6. 进阶:把 sokit 当协议回归的第一道闸

协议改一版就要回归一次,靠人肉点界面很容易漏。我的做法是把「工具验证」这一步固化成一张判定表,每次发版前照着走一遍,最快的三五分钟就能过完。

用例发送内容(HEX)期望接收判定标准
最小帧AA 55 00 00 0D 0A原样或带 ACK 回包字节完全一致
正常负载AA 55 00 05 48 45 4C 4C 4F 0D 0A长度字段回填 5长度与负载一致
超长负载长度字段填 0100,实际发 256 字节正常接收或明确报错不崩溃、不粘半帧
分片发送先发 4 字节,200ms 后发剩余完整一帧被拼出解析器有缓冲逻辑
断线重连中途断开工具端连接服务端清理会话无残留连接、无重复会话

表里的「分片发送」用 4.3 那段脚本改一下就能造,不需要额外工具。真正值得花时间的是最后一行:很多服务端在客户端异常断开时不清理会话,跑一晚上积累几百个僵尸连接,第二天端口就撑不住了。用工具反复连断十几次,是最省事的验证方式。

想再往前一步,可以用脚本加压,观察工具端和服务端的表现:

import socket import threading import time TARGET = ("127.0.0.1", 9000) N, DURATION = 20, 5 # 20 条并发连接,每条持续 5 秒 lock = threading.Lock() stat = {"send": 0, "recv": 0, "err": 0} frame = bytes.fromhex("AA 55 00 05 48 45 4C 4C 4F 0D 0A") def worker(): try: s = socket.create_connection(TARGET, timeout=3) s.settimeout(3) # 避免 recv 永久阻塞 end = time.time() + DURATION while time.time() < end: s.sendall(frame) data = s.recv(1024) with lock: stat["send"] += 1 stat["recv"] += 1 if data else 0 s.close() except Exception: with lock: stat["err"] += 1 threads = [threading.Thread(target=worker) for _ in range(N)] for t in threads: t.start() for t in threads: t.join() print(stat)

N控制并发连接数,DURATION控制每条连接跑多久,settimeout(3)是防止对端不回包时线程卡死。这段脚本跑起来的时候,工具端会看到多条连接同时收发,正好用来观察服务端的会话管理和工具的显示是否跟得上。数值不要求精确,它是个粗略探针,不是压测工具——真要测吞吐量级,该上专门的负载工具。

从那以后我接手任何新协议,都强制先走一遍这条路:脚本起回显服务 → 工具手发最小帧 → 故意拆包看半包 → 对着判定表跑五条用例。前面多花的二十分钟,基本都能在后面省回来,尤其是那种「本地好好的、上了设备就不对」的问题,八成在第一轮就能暴露。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询