引言
在前面的文章中,我们理解了网络层(IP协议)如何实现主机到主机的通信。但这里有个问题:
一台主机上运行着几十个应用程序(浏览器、微信、游戏、音乐播放器…),当数据包到达这台主机时,网络层该把它交给哪个应用?
这就是传输层要解决的核心问题:端到端通信(进程到进程通信)。
今天我们来理解:
- ✅ 传输层为什么必须存在?
- ✅ 端口号如何解决"进程寻址"问题?
- ✅ Socket是什么?为什么它是网络编程的核心?
- ✅ TCP和UDP的本质区别是什么?
第一性原理:为什么需要传输层?
问题1:网络层只能送到主机,不能送到进程
想象一个场景:
你的电脑(IP: 192.168.1.100)同时在:
- 浏览器访问淘宝(Chrome进程)
- 微信聊天(WeChat进程)
- 后台下载文件(迅雷进程)
- 听音乐(网易云进程)
当一个数据包到达 192.168.1.100 时,网络层只知道这是给这台主机的,但不知道该交给哪个应用程序。
传输层的第一个使命:在网络层"主机到主机"的基础上,实现"进程到进程"的通信。
问题2:不同应用对通信质量有不同要求
- HTTP下载文件:要求数据完整,可以慢一点
- 视频通话:要求实时性,丢几个数据包问题不大
- 游戏:要求低延迟,偶尔丢包可以接受
- 文件传输:要求100%可靠,不能有任何错误
传输层的第二个使命:根据应用的不同需求,提供不同的传输服务(可靠的TCP vs 快速的UDP)。
问题3:网络是不可靠的
网络层(IP协议)只负责"尽力而为"地传输数据包,它不保证:
- ❌ 数据包一定能到达
- ❌ 数据包按序到达
- ❌ 数据包不重复
- ❌ 数据包不损坏
对于需要可靠通信的应用,必须有一层协议来保证这些特性。
传输层的第三个使命:为需要可靠通信的应用提供可靠传输服务(TCP的职责)。
端口号:进程的"身份证"
端口号的设计哲学
核心思想:用一个16位整数(0-65535)唯一标识一台主机上的一个应用程序(准确说是一个进程的通信端点)。
IP地址 → 定位主机(哪台电脑)
端口号 → 定位进程(哪个应用)
完整的通信地址(五元组):
源IP + 源端口 + 目标IP + 目标端口 + 协议类型(TCP/UDP)
端口号的分类
| 端口范围 | 名称 | 用途 | 示例 |
|---|---|---|---|
| 0-1023 | 知名端口(Well-Known Ports) | 系统服务和常见应用 | HTTP:80, HTTPS:443, SSH:22, MySQL:3306 |
| 1024-49151 | 注册端口(Registered Ports) | 用户应用程序 | Tomcat:8080, Redis:6379, Nacos:8848 |
| 49152-65535 | 动态端口(Dynamic Ports) | 客户端临时使用 | 客户端发起连接时随机分配 |
实战案例:查看端口使用情况
场景1:查看当前监听的端口
# macOS/Linux
netstat -an | grep LISTEN
# 输出示例
tcp4 0 0 *.8080 *.* LISTEN # Tomcat
tcp4 0 0 *.3306 *.* LISTEN # MySQL
tcp4 0 0 *.6379 *.* LISTEN # Redis
tcp4 0 0 127.0.0.1.8848 *.* LISTEN # Nacos
解读:
*.8080:监听所有网卡的8080端口(0.0.0.0:8080)127.0.0.1.8848:只监听本地回环地址的8848端口
场景2:查看某个端口被哪个进程占用
# macOS
lsof -i :8080
# Linux
netstat -tunlp | grep 8080
# 输出示例
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
java 1234 user 50u IPv6 0x1234 0t0 TCP *:8080 (LISTEN)
场景3:微服务场景 - 端口冲突排查
# 问题:启动Spring Boot应用时报错
# Port 8080 was already in use
# 解决步骤1:找到占用8080的进程
lsof -i :8080
# 解决步骤2:杀掉该进程(谨慎操作!)
kill -9 1234
# 或者:修改应用端口
server.port=8081
Socket:应用与网络的桥梁
Socket是什么?
Socket(套接字) 是操作系统提供给应用程序的网络编程接口,是应用程序访问网络的"桥梁"。
形象类比:
- 电话:Socket就像电话机
- 拨号:
connect()就像拨电话号码 - 通话:
send()/recv()就像说话和听 - 挂机:
close()就像挂断电话
Socket的组成
一个Socket由五元组唯一标识:
Socket = (协议, 源IP, 源端口, 目标IP, 目标端口)
示例:
(TCP, 192.168.1.100, 54321, 180.97.33.108, 443)
↑客户端地址↑ ↑百度HTTPS服务器↑
Socket编程的基本流程
TCP服务器端:
// 1. 创建Socket,监听8080端口
ServerSocket serverSocket = new ServerSocket(8080);
// 2. 等待客户端连接(阻塞)
Socket clientSocket = serverSocket.accept();
// 3. 获取输入输出流
InputStream in = clientSocket.getInputStream();
OutputStream out = clientSocket.getOutputStream();
// 4. 读写数据
byte[] buffer = new byte[1024];
int len = in.read(buffer);
out.write("HTTP/1.1 200 OK\r\n\r\nHello".getBytes());
// 5. 关闭连接
clientSocket.close();
serverSocket.close();
TCP客户端:
// 1. 创建Socket,连接到服务器
Socket socket = new Socket("192.168.1.100", 8080);
// 2. 获取输入输出流
OutputStream out = socket.getOutputStream();
InputStream in = socket.getInputStream();
// 3. 发送请求
out.write("GET / HTTP/1.1\r\n\r\n".getBytes());
// 4. 接收响应
byte[] buffer = new byte[1024];
int len = in.read(buffer);
System.out.println(new String(buffer, 0, len));
// 5. 关闭连接
socket.close();
微服务场景:RestTemplate底层就是Socket
当你用Spring Boot的RestTemplate调用其他服务时:
@Autowired
private RestTemplate restTemplate;
// 调用用户服务
String result = restTemplate.getForObject(
"http://user-service:8080/api/users/123",
String.class
);
底层发生了什么?
- RestTemplate内部使用
HttpClient - HttpClient创建一个TCP Socket
- 连接到
user-service:8080 - 发送HTTP请求(通过Socket的OutputStream)
- 接收HTTP响应(通过Socket的InputStream)
- 关闭或放回连接池
这就是为什么需要配置连接超时和读超时:
# application.yml
spring:
http:
client:
connect-timeout: 5000 # Socket连接超时5秒
read-timeout: 30000 # Socket读取超时30秒
TCP vs UDP:可靠性与效率的权衡
本质区别
| 特性 | TCP(传输控制协议) | UDP(用户数据报协议) |
|---|---|---|
| 连接 | 面向连接(打电话) | 无连接(发短信) |
| 可靠性 | 可靠传输(保证到达、有序、不重复) | 不可靠(尽力而为) |
| 流量控制 | 有(滑动窗口) | 无 |
| 拥塞控制 | 有(慢启动、拥塞避免) | 无 |
| 传输效率 | 较慢(有握手、确认、重传) | 快速(直接发送) |
| 适用场景 | 文件传输、网页浏览、邮件 | 视频直播、游戏、DNS |
形象类比
TCP像快递:
- ✅ 必须先下单(建立连接)
- ✅ 有物流追踪(确认机制)
- ✅ 丢件会重发(重传机制)
- ✅ 保证送达(可靠性)
- ❌ 慢(需要各种确认)
UDP像喊话:
- ✅ 不需要建立连接,直接喊
- ✅ 快速(没有握手和确认)
- ❌ 不保证对方听到(不可靠)
- ❌ 不保证按顺序听到
- ❌ 可能重复听到
实战对比:下载文件 vs 视频直播
场景1:下载10GB的电影文件
必须用TCP:
- 文件不能有任何损坏(一个字节错了都不行)
- 丢包必须重传
- 必须按顺序接收(不能第10块在第1块之前)
虽然TCP慢一点,但保证了数据完整性
场景2:观看抖音直播
适合用UDP:
- 实时性最重要(延迟1秒都影响体验)
- 偶尔丢几帧画面不影响观看
- 不需要保证顺序(反正是实时流)
- 快速发送更重要
虽然UDP不可靠,但保证了低延迟
微服务场景的协议选择
| 场景 | 协议 | 原因 |
|---|---|---|
| HTTP RESTful API | TCP | 需要可靠传输,状态码、响应体不能丢 |
| gRPC | TCP (HTTP/2) | 需要可靠传输和双向流 |
| Dubbo RPC | TCP | 默认TCP,保证调用结果可靠 |
| 服务注册心跳 | UDP | Eureka、Consul可选UDP,追求低延迟 |
| 日志采集 | UDP | Fluentd、Logstash可选UDP,允许丢失 |
| 监控指标上报 | UDP | Prometheus Pushgateway可选UDP |
| 实时消息推送 | TCP (WebSocket) | 需要保证消息到达 |
实战案例:抓包看TCP vs UDP
案例1:用tcpdump观察TCP三次握手
# 启动抓包(监听8080端口的TCP流量)
sudo tcpdump -i any port 8080 -n
# 另一个终端:用curl访问
curl http://localhost:8080
# 抓包输出(简化)
IP 127.0.0.1.54321 > 127.0.0.1.8080: Flags [S], seq 100 # 第一次握手:SYN
IP 127.0.0.1.8080 > 127.0.0.1.54321: Flags [S.], seq 200, ack 101 # 第二次握手:SYN-ACK
IP 127.0.0.1.54321 > 127.0.0.1.8080: Flags [.], ack 201 # 第三次握手:ACK
IP 127.0.0.1.54321 > 127.0.0.1.8080: Flags [P.], seq 101:200 # 发送HTTP请求
IP 127.0.0.1.8080 > 127.0.0.1.54321: Flags [.], ack 200 # 确认收到请求
IP 127.0.0.1.8080 > 127.0.0.1.54321: Flags [P.], seq 201:500 # 发送HTTP响应
IP 127.0.0.1.54321 > 127.0.0.1.8080: Flags [.], ack 500 # 确认收到响应
IP 127.0.0.1.54321 > 127.0.0.1.8080: Flags [F.], seq 200 # 第一次挥手:FIN
IP 127.0.0.1.8080 > 127.0.0.1.54321: Flags [.], ack 201 # 第二次挥手:ACK
IP 127.0.0.1.8080 > 127.0.0.1.54321: Flags [F.], seq 500 # 第三次挥手:FIN
IP 127.0.0.1.54321 > 127.0.0.1.8080: Flags [.], ack 501 # 第四次挥手:ACK
关键观察:
[S]= SYN(同步)[.]= ACK(确认)[P]= PSH(推送数据)[F]= FIN(结束连接)
每个数据包都有确认,这就是TCP的可靠性保证。
案例2:用tcpdump观察UDP通信
# 启动抓包(监听53端口的UDP流量,DNS使用UDP)
sudo tcpdump -i any port 53 -n
# 另一个终端:进行DNS查询
nslookup baidu.com
# 抓包输出(简化)
IP 192.168.1.100.54321 > 8.8.8.8.53: UDP, length 29 # 发送DNS查询
IP 8.8.8.8.53 > 192.168.1.100.54321: UDP, length 45 # 收到DNS响应
关键观察:
- 只有两个数据包:请求和响应
- 没有握手、没有确认、没有挥手
- 直接发送,直接接收
- 快速但不保证可靠
传输层在微服务架构中的地位
微服务通信的三个层次
应用层 → HTTP/gRPC/Dubbo协议(业务逻辑)
↓
传输层 → TCP/UDP(可靠性保证)
↓
网络层 → IP协议(路由寻址)
传输层是应用层的基础:
- Spring Cloud Feign → 基于TCP的HTTP/1.1
- gRPC → 基于TCP的HTTP/2
- Dubbo → 基于TCP的自定义协议
- Kafka → 基于TCP的自定义协议
传输层配置影响微服务性能
常见配置项:
# Spring Boot应用
server:
port: 8080 # 监听端口
tomcat:
max-connections: 10000 # 最大连接数(Socket数量)
accept-count: 100 # 连接队列长度(backlog)
# Feign调用超时
feign:
client:
config:
default:
connectTimeout: 5000 # TCP连接超时
readTimeout: 10000 # TCP读取超时
为什么需要超时配置?
// 没有超时配置的危险:
Socket socket = new Socket("192.168.1.100", 8080);
// 如果服务器挂了,这行代码会永远阻塞!
// 正确做法:设置超时
Socket socket = new Socket();
socket.connect(new InetSocketAddress("192.168.1.100", 8080), 5000); // 5秒超时
socket.setSoTimeout(10000); // 读取超时10秒
总结
核心要点
传输层的三大使命:
- ✅ 端到端通信(进程到进程)
- ✅ 根据应用需求提供不同服务(TCP可靠 vs UDP快速)
- ✅ 为需要可靠传输的应用提供保证
端口号:
- 16位整数,标识主机上的进程
- 知名端口(0-1023)、注册端口(1024-49151)、动态端口(49152-65535)
Socket:
- 应用程序访问网络的编程接口
- 由五元组唯一标识:(协议, 源IP, 源端口, 目标IP, 目标端口)
TCP vs UDP:
- TCP:可靠、有序、面向连接,适合文件传输、网页浏览
- UDP:快速、无连接、不可靠,适合直播、游戏、DNS
与下一篇的关联
本文介绍了传输层的核心概念和TCP/UDP的区别。下一篇我们将深入TCP协议的第一个重要机制:TCP三次握手详解,理解:
- 为什么是三次而不是两次或四次?
- SYN/ACK标志位的作用
- 半连接队列与全连接队列
- SYN Flood攻击原理与防护
思考题
- 为什么HTTP/HTTPS使用TCP而不是UDP?
- 如果你要设计一个实时游戏,会选择TCP还是UDP?为什么?
- 为什么需要端口号?只用IP地址不行吗?
- Socket和TCP/UDP的关系是什么?
下一篇预告:《TCP三次握手详解:为什么是三次而不是两次?》