3. 构建TCP服务器:从Socket到完整通信
好,咱们正式开始动手写代码了。这一章要干的事,说白了就是让一台机器老老实实地蹲在某个端口上,等着别人来连它,然后收发数据。这是整个HTTP服务器的地基,地基不稳,上面盖什么都白搭。
我记得刚入行那会儿,第一次写TCP服务器,照着教程敲完代码,一运行——啥反应没有。后来才发现是忘了调用listen()。嗯,这种低级错误,谁还没犯过几次呢?
3.1 创建服务器Socket
Socket是什么?你可以把它想象成一根网线的插头。操作系统提供了这个插头,我们只需要告诉它:我要用IPv4,我要用TCP。就这么简单。
核心API:socket.socket(family, type)
- family:地址族,IPv4用
AF_INET,IPv6用AF_INET6 - type:套接字类型,TCP用
SOCK_STREAM,UDP用SOCK_DGRAM
import socket
# 创建TCP Socket
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
这里有个小细节——SOCK_STREAM代表面向连接的流式套接字,说白了就是TCP。如果你用SOCK_DGRAM,那就是UDP,发出去就不管了,咱们做HTTP服务器必须用TCP,因为HTTP依赖可靠的连接。
个人习惯:我每次创建socket后,都会顺手检查一下返回值。虽然Python里很少出问题,但在C/C++里这一步经常翻车。养成好习惯总没错。
3.2 绑定地址和端口
Socket创建好了,但它还是个「空插头」,没插到任何地方。绑定操作就是告诉操作系统:我要用这个IP和这个端口,谁也别跟我抢。
# 绑定地址和端口
host = '0.0.0.0' # 监听所有网络接口
port = 8080 # 端口号
server_socket.bind((host, port))
bind()接收一个元组,第一个元素是IP地址,第二个是端口号。这里我用了0.0.0.0,意思是监听本机所有可用的网络接口。如果你只想让本机访问,可以用127.0.0.1。
| 绑定地址 | 含义 | 适用场景 |
|---|---|---|
0.0.0.0 |
监听所有网络接口 | 生产环境、局域网服务 |
127.0.0.1 |
仅本机可访问 | 本地开发、调试 |
192.168.x.x |
监听特定网卡 | 多网卡服务器、安全隔离 |
我曾经踩过的坑:端口号小于1024需要root权限。有次我在生产服务器上用了80端口,结果普通用户启动直接报错Permission denied。后来我改用8080端口,再用nginx做反向代理。记住,开发阶段用1024以上的端口,省心。
3.3 监听连接
绑定完地址,服务器还不能接收连接。你得先告诉操作系统:我准备好了,有人来找我你就通知我。这就是listen()干的事。
# 开始监听,最大等待队列长度为5
server_socket.listen(5)
print(f"服务器已启动,监听 {host}:{port}")
listen()的参数是backlog,也就是等待连接队列的最大长度。什么意思呢?当服务器正在处理一个客户端时,其他客户端发来的连接请求会排着队等。这个参数就是队列能排多长。
关于backlog的取值:
- 太小:高并发时容易丢连接
- 太大:浪费系统资源,队列里的连接可能超时
- 我个人习惯用5或10,够用就行
3.4 接受客户端连接
服务器现在蹲在端口上等着了。当有客户端来敲门,accept()就会返回一个新的socket——专门用来和这个客户端通信。
# 接受客户端连接
client_socket, client_address = server_socket.accept()
print(f"收到来自 {client_address} 的连接")
accept()是阻塞的。什么意思?就是程序会停在这一行,直到有客户端连上来。你想想看,如果没人连,服务器就一直等着,啥也不干。这在实际生产环境里肯定不行,但咱们现在先理解这个基本模型。
accept()返回两个东西:
- client_socket:新的socket对象,专门和这个客户端通信
- client_address:客户端的IP和端口,是个元组
注意:原来的server_socket还在继续监听,它不负责收发数据。收发数据是client_socket的事。这个设计很巧妙——一个socket负责「接客」,其他socket负责「聊天」。
3.5 收发数据
连接建立好了,接下来就是收发数据。TCP是流式协议,数据像水流一样过来,你得用recv()去接,用send()去发。
# 接收数据
data = client_socket.recv(1024)
print(f"收到数据: {data.decode('utf-8')}")
# 发送响应
response = "Hello, client! I'm your TCP server."
client_socket.send(response.encode('utf-8'))
recv(1024)里的1024是缓冲区大小,单位是字节。意思是每次最多读1024个字节。如果数据超过1024字节,你得循环读取。
| 方法 | 作用 | 注意事项 |
|---|---|---|
recv(buffer_size) |
接收数据 | 可能只收到部分数据,需要循环 |
send(data) |
发送数据 | 可能只发送部分数据,需要检查返回值 |
sendall(data) |
确保全部发送 | 底层循环调用send,直到发完 |
我曾经犯过的错:直接用send()发大文件,结果只发了一半。后来才知道send()不保证一次发完所有数据。如果你要确保全部发送,用sendall(),或者自己写循环检查返回值。
3.6 关闭连接
数据收发完了,记得关掉连接。不关的话,系统资源会被慢慢耗尽。这就像用完水龙头不关,迟早淹了厨房。
# 关闭客户端连接
client_socket.close()
# 关闭服务器Socket(通常在程序退出时)
server_socket.close()
关闭顺序有讲究:先关客户端socket,再关服务器socket。如果先关服务器socket,正在通信的客户端会突然断连,对方那边会报错。
完整流程总结:
- 创建socket →
socket() - 绑定地址端口 →
bind() - 开始监听 →
listen() - 接受连接 →
accept() - 收发数据 →
recv()/send() - 关闭连接 →
close()
3.7 完整代码示例
把上面所有步骤串起来,就是一个最简单的TCP服务器:
import socket
def run_tcp_server():
# 1. 创建Socket
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 2. 绑定地址和端口
server_socket.bind(('0.0.0.0', 8080))
# 3. 开始监听
server_socket.listen(5)
print("TCP服务器已启动,等待连接...")
# 4. 接受连接
client_socket, client_addr = server_socket.accept()
print(f"客户端 {client_addr} 已连接")
# 5. 收发数据
data = client_socket.recv(1024)
print(f"收到: {data.decode('utf-8')}")
response = "Hello from TCP server!"
client_socket.send(response.encode('utf-8'))
# 6. 关闭连接
client_socket.close()
server_socket.close()
if __name__ == '__main__':
run_tcp_server()
这个服务器只能处理一个客户端,处理完就退出了。但没关系,咱们先把基础打牢。后面章节会讲怎么让它持续运行、同时处理多个客户端。
3.8 TCP服务器核心流程
下面这张图把整个流程串起来了,你可以对照着看:
这张图把六个步骤串成了流水线。你仔细看,每一步都依赖上一步的结果。少了任何一步,服务器都跑不起来。
3.9 常见问题与避坑
- Address already in use:端口被占用了。要么换端口,要么等一会儿再试。我一般用
setsockopt()设置SO_REUSEADDR来解决。 - Connection refused:客户端连的时候服务器还没启动。检查一下服务器是不是真的在监听。
- recv() 返回空数据:说明客户端已经关闭连接了。这时候你也该关掉对应的socket。
一个小技巧:调试的时候可以用telnet或者nc命令当客户端,不用写代码就能测试服务器。比如telnet 127.0.0.1 8080,连上去之后随便敲点东西,看看服务器能不能收到。
好了,TCP服务器的基本骨架就是这样。代码不多,但每一步都有它的道理。你最好自己动手敲一遍,跑起来,然后用telnet连上去试试。亲手看到「连接成功」那四个字,比看十遍教程都管用。