一、前言
我们平时调用接口使用requests、aiohttp,底层全部都是TCP协议。HTTP只是构建在TCP之上的应用层协议。
很多运维、后端场景需要直接操作TCP裸连接:端口连通性探测、自定义报文通信、服务心跳、Socket长连接。
TCP是面向连接的可靠传输协议,通信分为三步:建立连接 → 收发数据 → 断开连接。
本文使用Python内置socket库,不依赖任何第三方库,从零实现TCP客户端与服务端通信。
二、TCP与UDP核心区别
- TCP需要先三次握手建立连接,UDP直接发包;
- TCP保证数据有序、不丢失;UDP只管发送,不保障送达;
- TCP使用SOCK_STREAM,UDP使用SOCK_DGRAM;
- TCP适合文件传输、接口调用;UDP适合广播、实时数据流。
三、基础TCP客户端:短连接请求
短连接:连接建立,收发一次数据,立刻关闭。
核心方法:
- socket() 创建套接字
- connect() 连接服务端IP端口
- send() 发送字节数据
- recv() 接收返回数据
python
import socket
# 创建TCP套接字 SOCK_STREAM = TCP
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 目标服务端地址
host = "127.0.0.1"
port = 9000
try:
# 发起三次握手,建立TCP连接
client.connect((host, port))
# 发送二进制数据
client.send("Hello TCP Server".encode("utf-8"))
# 阻塞等待接收服务端返回数据,缓冲区1024字节
recv_data = client.recv(1024)
print("收到服务端返回:", recv_data.decode("utf-8"))
except ConnectionRefusedError:
print("端口未开放,TCP连接失败")
finally:
# 关闭连接,释放资源
client.close()
四、TCP服务端:监听端口并处理连接
服务端固定端口监听,等待客户端接入。
关键函数:
- bind() 绑定IP和端口
- listen() 开启监听
- accept() 阻塞等待新客户端接入
python
import socket
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 绑定0.0.0.0,允许局域网所有设备访问
server.bind(("0.0.0.0", 9000))
# 开启监听,最大挂起队列5
server.listen(5)
print("TCP服务端已启动,监听9000端口")
while True:
# 阻塞等待客户端连接
conn, addr = server.accept()
print(f"新客户端接入:{addr}")
# 接收客户端消息
data = conn.recv(1024)
print("收到消息:", data.decode())
# 回复数据
conn.send("消息已收到".encode("utf-8"))
# 关闭本次客户端连接(短连接模式)
conn.close()
运行顺序:先启动服务端,再运行客户端,即可完成一次TCP通信。
五、TCP长连接:不断开持续收发数据
短连接每次通信都要重新握手,开销很大。长连接建立一次连接,反复收发多条消息。
客户端长连接代码
python
import socket
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(("127.0.0.1", 9000))
# 循环发送多条数据,连接不断开
for i in range(3):
msg = f"第{i+1}条消息".encode("utf-8")
client.send(msg)
res = client.recv(1024)
print("接收:", res.decode())
client.close()
服务端改造(持续读取数据)
python
import socket
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(("0.0.0.0", 9000))
server.listen(5)
while True:
conn, addr = server.accept()
print("客户端已连接")
# 持续读取客户端消息
while True:
data = conn.recv(1024)
if not data:
# 客户端关闭连接,recv会拿到空字节
break
print(data.decode())
conn.send("ok".encode())
conn.close()
六、实战场景1:TCP端口存活检测
运维最常用场景:批量探测服务器端口是否开放,本质就是尝试建立TCP连接。
python
import socket
def check_tcp_port(host, port, timeout=2):
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(timeout)
try:
sock.connect((host, port))
return True, "端口开放"
except Exception:
return False, "端口关闭或无法连接"
finally:
sock.close()
# 测试
print(check_tcp_port("www.baidu.com", 80))
print(check_tcp_port("127.0.0.1", 9999))
这个端口探测比UDP探测更加可靠,广泛用于内网资产扫描、节点健康检测。
七、实战场景2:手动发送原始HTTP报文(裸TCP访问网页)
HTTP协议就是文本TCP报文,我们不用requests,直接用TCP套接字访问网页:
python
import socket
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(("www.baidu.com", 80))
# 原始HTTP GET报文
http_msg = b"GET / HTTP/1.1\r\nHost: www.baidu.com\r\nConnection: close\r\n\r\n"
client.send(http_msg)
# 接收网页响应
response = client.recv(4096)
print(response.decode("utf-8", errors="ignore"))
client.close()
运行就能拿到百度的HTTP响应头与网页正文,可以直观理解HTTP是封装在TCP之上的应用层协议。
八、TCP开发常见问题与避坑
-
recv阻塞卡死
recv默认无限等待,可以设置
sock.settimeout(3)超时断开。 -
粘包问题
TCP是流式字节流,没有数据包边界,多条数据会黏在一起。自定义通信协议时,必须加数据长度分隔符。
-
客户端没有正常close,导致端口被占用(TIME_WAIT)
程序一定要正常关闭套接字;服务端可以开启端口复用:
python
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
-
数据必须为bytes字节串
字符串一定要encode,不能直接发送str字符串。
-
单线程服务端只能处理一个客户端
上面的服务端是单线程,一次只能服务一个连接,高并发场景需要搭配多线程/多协程处理每一条TCP连接。
九、异步TCP(asyncio高级用法)
高并发大量TCP探测时,同步socket效率太低,可以使用asyncio创建异步TCP连接,上万条连接轻松调度,占用内存极低,非常适合批量端口巡检。
十、总结
- TCP核心流程:创建套接字 → connect建立连接 → send/recv收发数据 → close断开;
- 短连接一发一关,长连接复用TCP通道减少握手开销;
- 原生Socket TCP可以实现端口探测、自定义报文、裸HTTP请求;
- requests、urllib、爬虫框架底层全部都是对TCP Socket的封装。
原生TCP网络编程是后端、运维、网络爬虫的基本功,掌握裸Socket通信,才能真正理解互联网数据传输的底层逻辑。
如果你想,我可以再续写一版:多线程TCP服务端 + 异步批量TCP端口检测代码。