引言

在前面的文章中,我们理解了网络层(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
);

底层发生了什么?

  1. RestTemplate内部使用 HttpClient
  2. HttpClient创建一个TCP Socket
  3. 连接到 user-service:8080
  4. 发送HTTP请求(通过Socket的OutputStream)
  5. 接收HTTP响应(通过Socket的InputStream)
  6. 关闭或放回连接池

这就是为什么需要配置连接超时和读超时

# 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 APITCP需要可靠传输,状态码、响应体不能丢
gRPCTCP (HTTP/2)需要可靠传输和双向流
Dubbo RPCTCP默认TCP,保证调用结果可靠
服务注册心跳UDPEureka、Consul可选UDP,追求低延迟
日志采集UDPFluentd、Logstash可选UDP,允许丢失
监控指标上报UDPPrometheus 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秒

总结

核心要点

  1. 传输层的三大使命

    • ✅ 端到端通信(进程到进程)
    • ✅ 根据应用需求提供不同服务(TCP可靠 vs UDP快速)
    • ✅ 为需要可靠传输的应用提供保证
  2. 端口号

    • 16位整数,标识主机上的进程
    • 知名端口(0-1023)、注册端口(1024-49151)、动态端口(49152-65535)
  3. Socket

    • 应用程序访问网络的编程接口
    • 由五元组唯一标识:(协议, 源IP, 源端口, 目标IP, 目标端口)
  4. TCP vs UDP

    • TCP:可靠、有序、面向连接,适合文件传输、网页浏览
    • UDP:快速、无连接、不可靠,适合直播、游戏、DNS

与下一篇的关联

本文介绍了传输层的核心概念和TCP/UDP的区别。下一篇我们将深入TCP协议的第一个重要机制:TCP三次握手详解,理解:

  • 为什么是三次而不是两次或四次?
  • SYN/ACK标志位的作用
  • 半连接队列与全连接队列
  • SYN Flood攻击原理与防护

思考题

  1. 为什么HTTP/HTTPS使用TCP而不是UDP?
  2. 如果你要设计一个实时游戏,会选择TCP还是UDP?为什么?
  3. 为什么需要端口号?只用IP地址不行吗?
  4. Socket和TCP/UDP的关系是什么?

下一篇预告:《TCP三次握手详解:为什么是三次而不是两次?》