🌈个人主页:一条泥憨鱼(欢迎各位大佬莅临)

❄️《数据结构》 ❄️《AI与Agent那些事》
❄️《从0开始学计算机网络》 ❄️《后端开发》

前言:
这篇文章整理了 TCP Socket 建立连接的完整流程,从服务端的 bind、listen 到客户端的 connect,再到服务端的 accept。这些动作串起来就是一次网络通信从"准备"到"接通"的全过程。
先搞懂 Socket 是什么——一扇"门"
假设你想开一家客服热线。要能接到客户的电话,你得先做几件事:装一部电话机、申请一个对外公布的号码、把线路开通、然后坐在那儿等电话响。
TCP 网络编程里的服务端,做的事情几乎一样。只不过它的"电话机"叫 Socket。
Socket 这个词直译是"插座",也有人叫它"套接字"。你可以把它理解成程序和网络之间的一扇门:程序在门里面,网络在外面。程序想把数据发出去,就从这扇门递出去;网络有数据进来,也从这扇门送进来。没有这扇门,程序根本碰不到网络。

那数据要送到哪台机器、哪个程序呢?靠两个东西定位:
- IP 地址:相当于门牌号,标识网络上的哪一台机器。比如 `192.168.1.10`。
- 端口:相当于房间号,标识这台机器上的哪一个程序。比如 `8080`。
一台机器上可以同时跑很多个网络程序——浏览器、微信、你的服务端代码。它们共用同一个 IP,但各自占着不同的端口。数据包到了这台机器,操作系统看端口号,就知道该把数据交给哪个程序。所以端口是区分不同程序的关键。
还有一点要分清角色:
- 服务端 Socket:被动等待,别人来找我。
- 客户端 Socket:主动出击,我去找别人。
下面所有的函数,都是围绕这两种角色展开的。
服务端的准备动作——bind 和 listen
服务端要能接客,得先做两步准备。
bind:给热线申请一个固定号码
创建好一个 Socket 之后,它还是个"没号码的电话机"。`bind` 的作用就是把这个 Socket 绑定到一个具体的 IP 和端口上。
为什么要固定端口?因为客户端得知道往哪儿打。你的服务对外说"我在 8080 端口",那所有客户端都会往这个端口发连接。如果端口每次都变,客户端根本找不到你。
import socket
# 创建一个 TCP Socket
# AF_INET 表示用 IPv4 地址,SOCK_STREAM 表示用 TCP 协议
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 绑定到本机所有网卡的 8080 端口
# '0.0.0.0' 表示监听本机所有 IP,不限定某一块网卡
# 8080 是我们选定的端口号
server_socket.bind(('0.0.0.0', 8080))
`bind` 最常见的失败原因是端口被占用。报错一般长这样:`Address already in use`。意思是这个端口已经有别的程序在用了。
这里有坑:程序刚退出,端口可能还处于 `TIME_WAIT` 状态(TCP 正常关闭连接后会保留一段时间),短时间内重新 bind 会失败。开发时可以在 bind 之前加一句设置:
# 允许端口在 TIME_WAIT 状态下被重用,方便开发调试时快速重启
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
端口号的范围是 0 到 65535。其中 1024 以下一般是系统保留给已知服务的(比如 80 是 HTTP、443 是 HTTPS),普通程序要用得用 1024 以上。
listen:告诉操作系统"我开始接客了"
bind 只是给了号码,电话线路还没开通。`listen` 才是把这个 Socket 切换成"监听状态",相当于告诉操作系统:从现在起,有客户端来连这个端口,你就接待,别拒绝。
# 开始监听,backlog 设为 5
server_socket.listen(5)
这里的 `backlog` 参数值得说一下。它表示等待被处理的连接队列的最大长度。
用排队叫号来理解:客服热线开通后,同一时间可能有很多人打进来。操作系统会先把这些连接放进队列里排着,你的程序一个个按顺序处理。这个队列的长度就是 backlog。
严格来说,TCP 的连接队列分两个:
- 半连接队列:收到了客户端的连接请求,但握手还没完成,连接还没真正建立。
- 全连接队列:握手已经完成,连接建好了,就等你的程序调用 `accept` 把它取走。
backlog 主要影响的是全连接队列的长度。如果队列满了,新的连接请求可能会被丢弃或拒绝。这也是为什么高并发服务要合理设置 backlog,还要尽快 accept 把连接取走。

客户端发起连接——connect 与三次握手
服务端准备好了,客户端这边就一个动作:连接。
connect:拨号
客户端创建 Socket 后,调用 `connect` 主动发起连接,指定要连的服务端 IP 和端口。
import socket
# 创建 TCP Socket
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 连接服务端:IP 是 127.0.0.1,端口是 8080
# 127.0.0.1 是本机回环地址,用来在本机测试
client_socket.connect(('127.0.0.1', 8080))
这一步就相当于"拨号"。但拨号不是拨出去就完事了,中间要经过 TCP 三次握手,确认双方都能收发数据。
connect 默认是阻塞的。也就是说,调用 connect 之后,程序会停在这一行,一直等到三次握手完成、连接真正建立,才会继续往下走。如果服务端没开、或者网络不通,connect 会卡住直到超时然后报错。
服务端接收连接——accept 返回的是"新电话"
现在客户端连上来了,服务端得把它"接起来"。这个动作就是 `accept`。
accept 返回一个全新的 Socket
accept 返回的不是原来那个监听 Socket,而是一个全新的 Socket,专门用来和这个客户端通信。
# accept 会阻塞,直到有客户端连接进来
# 返回值:
# conn —— 全新的 Socket,专门和这个客户端通信
# addr —— 客户端的地址信息(IP 和端口)
conn, addr = server_socket.accept()
print('新连接来自:', addr)
# 用 conn 收发数据,而不是 server_socket
data = conn.recv(1024)
conn.send(b'hello client')
# 通信结束,关闭的是这个连接 Socket
conn.close()
```
用前台接线员来理解:监听 Socket(server_socket)就是那个接线员,它只负责转接,不负责聊天。有客户打进来,接线员把线路转给一个客服,然后继续守着电话等下一个。真正和客户聊天的是那个客服,也就是 accept 返回的新 Socket。
- `server_socket` 只用来 `accept`,它一直守着 8080 端口。
- `conn` 用来收发数据,连接关掉它就行,监听 Socket 不受影响。

为什么要循环 accept
因为接线员接待完一个客户,还得接下一个。服务端通常写成循环:
while True:
# 每来一个新连接,就取出一个新的 Socket
conn, addr = server_socket.accept()
# 处理这个客户端的请求
handle_client(conn)
但这样写有个问题:`handle_client` 如果处理得慢,后面的客户端就得一直等着,接线员被占住了。这是单线程 accept 的局限。
要同时服务多个客户端,常见的方向有:每个连接开一个线程或进程,或者用 IO 多路复用(比如 `select`、`epoll`)在一个线程里管理大量连接
收束
装电话(创建 Socket)、申请号码(bind)、开通线路(listen)、守着等来电(accept),客户那边拨号(connect)。整套动作走完,一条 TCP 连接才算真正建立。
一个可迁移的判断方法:看任何一段 TCP 服务端代码,先找"监听 Socket"和"连接 Socket"是不是两个不同的变量。如果只有一个 Socket 变量贯穿始终,或者把 accept 的返回值又赋值回监听 Socket,那这段代码八成有问题。
两个易错:一是忘了循环 accept,服务端接完一个连接就退出了;二是把监听 Socket 关掉,导致后续再也接不到新连接——该关的永远是 accept 返回的那个连接 Socket。
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/2503_94545876/article/details/167398929




