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,正在通信的客户端会突然断连,对方那边会报错。

完整流程总结:

  1. 创建socket → socket()
  2. 绑定地址端口 → bind()
  3. 开始监听 → listen()
  4. 接受连接 → accept()
  5. 收发数据 → recv() / send()
  6. 关闭连接 → 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服务器核心流程

下面这张图把整个流程串起来了,你可以对照着看:

TCP服务器核心流程 socket() 创建Socket bind() 绑定地址端口 listen() 开始监听 accept() 接受连接 recv() / send() 收发数据 close() 关闭连接 注意:accept() 是阻塞调用,程序会停在这里直到有客户端连接

这张图把六个步骤串成了流水线。你仔细看,每一步都依赖上一步的结果。少了任何一步,服务器都跑不起来。

3.9 常见问题与避坑

  • Address already in use:端口被占用了。要么换端口,要么等一会儿再试。我一般用setsockopt()设置SO_REUSEADDR来解决。
  • Connection refused:客户端连的时候服务器还没启动。检查一下服务器是不是真的在监听。
  • recv() 返回空数据:说明客户端已经关闭连接了。这时候你也该关掉对应的socket。

一个小技巧:调试的时候可以用telnet或者nc命令当客户端,不用写代码就能测试服务器。比如telnet 127.0.0.1 8080,连上去之后随便敲点东西,看看服务器能不能收到。

好了,TCP服务器的基本骨架就是这样。代码不多,但每一步都有它的道理。你最好自己动手敲一遍,跑起来,然后用telnet连上去试试。亲手看到「连接成功」那四个字,比看十遍教程都管用。