模拟TCP客户端和服务器最快的办法是全用回环地址:一端socket.bind()到127.0.0.1的某个高位端口并listen(),另一端connect()上去,两条进程就能在本机完成完整的握手、收发和断开;不想写代码就直接用nc -l 9000和nc 127.0.0.1 9000,一分钟验证完通不通。
TCP通信到底在模拟什么
很多人第一次看到socket这个单词就卡住了,其实它模拟的东西很朴素:一条能双向读写的管道。
一条连接由四元组唯一确定
源IP、源端口、目的IP、目的端口,这四个值凑在一起,内核就能唯一认出某一条TCP连接,你在本机同时开十个客户端连同一个服务端,靠的就是源端口不同(客户端调用connect()时端口由系统随机分配)。
握手过程按RFC 9293的定义走三次交互,客户端先发SYN,服务端回SYN+ACK,客户端再回ACK,这段过程你不需要手写,connect()返回0的那一瞬间,握手已经替你完成了。
服务端和客户端的代码不对称
不对称的地方只有一处:服务端要先bind()再listen(),然后循环accept();客户端只需要connect(),因为服务端必须绑定一个固定端口,别人才能找到它;客户端反过来,谁先发起谁不定端口,由内核分配更省事。
记住这个对称关系,后面所有代码都只是在这两个骨架里填内容。
用Python模拟TCP客户端和服务器(代码可直接复制)
Python的socket模块是最薄的一层封装,几乎和C的Berkeley socket一一对应,拿来理解原理最合适。
服务端:bind、listen、accept三步
import socket
srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
srv.bind(("127.0.0.1", 9000))
srv.listen(5)
print("listening on 127.0.0.1:9000")
while True:
conn, addr = srv.accept() # 阻塞,直到有人连上来
data = conn.recv(1024)
conn.sendall(b"echo: " + data)
conn.close()
几个细节值得停一下:
SOCK_STREAM就是TCP,SOCK_DGRAM是UDP,写错这一个参数,行为完全不同。SO_REUSEADDR几乎必加,进程重启时旧连接还停留在TIME_WAIT状态,不加这一行很容易报Address already in use。listen(5)里的5是未完成握手队列的半连接上限,不是并发连接数上限,很多人把它当成并发量来理解,这是误读。conn是新的套接字,srv本身不负责收发数据。
客户端:先connect,再sendall
import socket
cli = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
cli.connect(("127.0.0.1", 9000))
cli.sendall(b"hello tcp")
print(cli.recv(1024))
cli.close()
想模拟多个客户端并发,把上面这段包进threading.Thread起20个线程即可,服务端那边用accept()拿到新conn后也丢进线程处理,这就是一个最小可用的并发模型。
不想写代码?nc命令和Python脚本模拟TCP哪个好
临时验证端口通不通,没必要开编辑器。
nc的三种典型用法
nc -l 9000:起一个最简陋的服务端,敲回车就发出去。nc 127.0.0.1 9000:当客户端连上去。nc -zv 127.0.0.1 9000:只测端口通不通,-z表示握手后立刻断开,-v打日志。
macOS自带的nc是BSD版本,-l后面不能直接跟端口,得写成nc -l 9000或nc -l -p 9000视版本而定,Linux的GNU netcat则是另一种写法,跨平台时容易踩这个坑,用socat TCP-LISTEN:9000,reuseaddr -更稳。
什么情况下必须回到Python
下面这些需求,nc撑不住:
- 需要构造特定字节序列或固定长度包头(比如模拟一个私有协议)。
- 需要模拟1000个并发连接做压测。
- 需要在收到数据后做业务逻辑判断再回包。
- 需要统计往返时延。
几种方式横向对比
| 方式 | 适合场景 | 依赖 | 启动成本 |
|---|---|---|---|
| Python socket | 自定义协议、并发压测、教学演示 | 需装Python | 写十几行 |
| nc / netcat | 临时端口连通性验证 | 系统自带 | 一条命令 |
| socat | 跨平台、端口转发、加SSL | 需安装 | 一条命令 |
| telnet | 纯文本协议手工调试 | 系统自带 | 一条命令 |
行业共识认为,日常排查八成以上的”连接不通”问题,用nc -zv就能定位到是网络层还是应用层的问题。
本地电脑怎么模拟TCP服务端测试:三个高频场景
验证防火墙或安全组是否放行
服务端绑到0.0.0:9000而不是0.0.1:9000,从另一台机器执行nc -zv 目标IP 9000。
Connection refused说明端口没人监听,包已经到达了目标主机;Connection timed out说明包在中途被丢了,多半是安全组或防火墙,这两个报错含义完全不同,别混着看。
0.0.0表示监听本机所有网卡,0.0.1只接受本机回环访问,这是局域网里连不上的头号原因。
模拟弱网和高延迟
本机回环延迟通常在微秒级,测不出真实体验,用tc给回环网卡加延迟:
tc qdisc add dev lo root netem delay 100ms loss 2%
意思是每个包延迟100毫秒、随机丢2%,跑完记得用tc qdisc del dev lo root清掉,否则后面所有本地调试都会变慢,找半天找不出原因。
压测并发上限
单机跑客户端线程,把连接数从10一路加到几千,观察服务端accept()是否还有响应,文件描述符上限(ulimit -n)和listen的backlog会先撞墙,而这两者恰好是最容易被忽略的两处。
抓包看一眼,比猜十次都准
代码跑通了但数据对不上,别改代码,先抓包。
tcpdump -i lo port 9000 -w tcp.pcap
然后用Wireshark打开tcp.pcap,你能直接看到SYN、SYN+ACK、ACK三行,以及每次sendall对应的数据段。
判断有没有粘包,就看两次逻辑上的”消息”是不是落在同一个TCP段里,TCP是字节流,本身没有消息边界这个概念,所谓”粘包”其实是应用层没做长度前缀或分隔符导致的解析错位,协议层从来没有这个毛病。
业内专家指出,TCP的重传超时计算遵循RFC 6298的算法,抓包里出现重复ACK和快速重传时,不要急着怀疑业务代码,先看是不是链路真的丢了包。
几种新手反复踩的坑
- recv返回空字节:
b""不是”没数据”,而是对端已经关闭了连接,很多循环写成了if not data: continue,结果死循环。 - 忘了处理阻塞:
recv()默认阻塞,对方不发数据就一直卡着,要设超时用cli.settimeout(5)。 - send不等于sendall:
send()可能只发出去一部分,返回实际字节数;除非你手动循环,否则一律用sendall()。 - 端口选太低:1024以下的端口在Linux上需要root权限,调试时随手用
80会直接报权限错误。 - 服务端只accept一次:
accept()只处理一条连接,忘写while True就变成”只能连一次”的服务端。
模拟TCP客户端和服务器的收费与选型
主流的模拟手段全部免费:Python解释器开源,nc和socat属于系统软件包,Wireshark是开源项目,只有部分商业压测平台按并发数或时长收费,但那是给大规模线上压测用的,本地做功能验证根本用不上。
如果只是学协议、调接口、验证端口,别花钱,一条命令或者二十行Python就够。
TCP模拟高频问题
模拟TCP客户端和服务器需要额外装什么软件吗
Linux和macOS自带nc,Windows可以用PowerShell的Test-NetConnection -ComputerName 127.0.0.1 -Port 9000替代,Python需要单独安装但多数开发机已有,除Python外,抓包工具Wireshark按需安装即可。
客户端连不上服务端,按什么顺序排查
第一步ss -lntp | grep 9000确认端口确实在监听;第二步看服务端绑的是0.0.1还是0.0.0;第三步用nc -zv从对端探测;第四步抓包看SYN有没有到达,四步走完,问题基本就落到某一层了。
同一台机器上能同时跑多个客户端吗
能,理论上限受文件描述符数量约束,客户端不指定源端口时由内核从临时端口池分配,同时建立几千条连接没什么问题,超过这个量级就要调ulimit -n和内核的端口范围参数了。
模拟TCP客户端和服务器从来不是难在代码,而是难在把”连接不通”拆成可验证的几步,先把回环跑通,再把0.0.0和防火墙理顺,剩下的问题抓个包就都清楚了。
首发原创文章,作者:王坚,如若转载,请注明出处:https://idctop.com/article/796949.html


