1. Socket基础:理解TCP/IP协议栈、Socket是什么、Socket类型与工作流程

说实话,很多人学网络编程一上来就撸代码,结果连数据怎么从A到B的都没搞明白。我当年也是这样,写了个聊天程序,局域网跑得挺好,一上公网就各种掉线、乱序。后来老老实实把TCP/IP协议栈啃了一遍,才明白问题出在哪。

这一节,咱们就把地基打牢。你想想看,Socket说白了就是操作系统给应用层开的一扇门,数据从这扇门进去,经过协议栈层层打包,最后通过网络发出去。反过来也一样。

1.1 TCP/IP协议栈:数据是怎么旅行的

TCP/IP协议栈不是单个协议,而是一组协议的集合。我习惯把它理解成「快递分拣系统」——每一层只干自己那点事,干完就交给下一层。

核心认知:TCP/IP协议栈分为四层,从上到下依次是:

  • 应用层 — HTTP、FTP、SMTP,你写的程序就在这层
  • 传输层 — TCP、UDP,负责端到端的可靠或不可靠传输
  • 网络层 — IP协议,负责寻址和路由
  • 网络接口层 — 以太网、Wi-Fi,负责物理介质上的数据收发

每一层都会给数据「加个信封」。应用层的数据到了传输层,会被加上TCP头部(源端口、目的端口、序号等);到了网络层,再加上IP头部(源IP、目的IP);到了网络接口层,再加上MAC头部。这个过程叫封装。

接收端反过来,一层层拆信封,直到拿到原始数据。这叫解封装。

我在项目中遇到过一个问题:客户端发了个大文件,服务端收不全。查了半天,发现是应用层没处理TCP的粘包和拆包。说白了,TCP是流式协议,它不关心你发了几次write,只关心字节流。你得自己在应用层定义消息边界。

TCP/IP 协议栈封装与解封装流程 发送端 应用层数据 (Data) TCP头部 + Data IP头部 + TCP + Data MAC头部 + IP + TCP + Data 网络传输 接收端 MAC头部 + IP + TCP + Data IP头部 + TCP + Data TCP头部 + Data 应用层数据 (Data) 说明: 发送端逐层封装头部信息,接收端逐层解封装。每一层只处理自己关心的头部,不修改上层数据。

1.2 Socket到底是什么

Socket,中文叫「套接字」。名字挺拗口,但概念很简单——它是应用层与传输层之间的编程接口。

我个人的理解:Socket就是IP地址 + 端口号的组合。IP地址找到哪台机器,端口号找到机器上的哪个进程。两者一组合,就能唯一定位网络上的一个通信端点。

我的习惯:把Socket想象成「电话插座」。IP地址是区号+电话号码,端口号是分机号。你拨通总机(IP),再转分机(端口),就能找到具体的人(进程)。

在Python里,创建一个Socket只需要一行代码:

import socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)  # TCP Socket

AF_INET 表示使用IPv4地址族,SOCK_STREAM 表示使用TCP协议。就这么简单。

1.3 Socket类型:TCP vs UDP

这是面试常考题,也是实际开发中第一个要做的选择。我直接给你对比表:

特性 TCP (SOCK_STREAM) UDP (SOCK_DGRAM)
连接方式 面向连接(先建立连接再通信) 无连接(直接发数据)
可靠性 可靠传输(有确认、重传机制) 不可靠(发出去就不管了)
数据顺序 保证顺序 不保证顺序
传输模式 流式(无边界) 报文式(有边界)
速度 相对较慢 快
典型应用 HTTP、FTP、SMTP DNS、视频直播、在线游戏

选哪个?我有个简单的判断标准:

  • 如果数据必须完整、有序到达(比如网页、文件传输),选TCP。
  • 如果实时性优先,丢几个包无所谓(比如视频通话、游戏位置同步),选UDP。

我曾经踩过的坑:做一个实时数据采集系统,用了UDP。结果局域网测试一切正常,部署到公网后,数据包丢失率高达15%。后来加了应用层确认重传机制,才勉强能用。所以,别迷信UDP「快」——在不可靠网络上,快但丢包等于没用。

1.4 Socket工作流程:从创建到关闭

TCP Socket的工作流程,我习惯用「打电话」来类比:

  1. 创建Socket — 相当于装一部电话
  2. 绑定地址和端口 (bind) — 相当于给电话分配一个号码
  3. 监听 (listen) — 相当于把电话设为待机状态,等别人打进来
  4. 接受连接 (accept) — 相当于有人打电话进来,你接起来
  5. 收发数据 (send/recv) — 相当于通话
  6. 关闭连接 (close) — 相当于挂电话

客户端那边更简单:

  1. 创建Socket
  2. 连接服务器 (connect) — 相当于拨号
  3. 收发数据 (send/recv)
  4. 关闭连接 (close)

看个最简单的例子,服务端:

import socket

# 1. 创建Socket
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# 2. 绑定地址和端口
server.bind(('0.0.0.0', 8080))

# 3. 监听,最大等待队列为5
server.listen(5)
print('服务器启动,等待连接...')

# 4. 接受连接
conn, addr = server.accept()
print(f'客户端 {addr} 已连接')

# 5. 收发数据
data = conn.recv(1024)
print(f'收到: {data.decode()}')
conn.send(b'Hello, Client!')

# 6. 关闭
conn.close()
server.close()

客户端:

import socket

# 1. 创建Socket
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# 2. 连接服务器
client.connect(('127.0.0.1', 8080))

# 3. 收发数据
client.send(b'Hello, Server!')
data = client.recv(1024)
print(f'收到: {data.decode()}')

# 4. 关闭
client.close()

注意:上面的代码是同步阻塞模式。recv() 会一直等,直到收到数据。在实际的HTTP服务器中,我们会用多线程或异步IO来处理多个客户端。这个后面会详细讲。

UDP的工作流程更简单,不需要连接:

# UDP服务端
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.bind(('0.0.0.0', 8080))
data, addr = s.recvfrom(1024)  # 接收数据,同时获取客户端地址
s.sendto(b'OK', addr)          # 直接发,不需要连接

# UDP客户端
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.sendto(b'Hello', ('127.0.0.1', 8080))
data, addr = s.recvfrom(1024)

看到区别了吗?UDP没有connect、listen、accept这些步骤。发数据时直接指定目标地址,收数据时recvfrom会同时返回数据和对端地址。说白了,UDP就是「发出去就不管了」。

1.5 本章小结

这一节我们搞清楚了四件事:

  • TCP/IP协议栈是分层封装的,每层各司其职
  • Socket是IP+端口的组合,是应用层与传输层的接口
  • TCP可靠但慢,UDP快但不可靠,选哪个看场景
  • TCP Socket工作流程:创建→绑定→监听→接受→收发→关闭