Java 网络编程:UDP、TCP 与 Socket
前面学的 IO 是程序和文件对话,本篇让程序和网络另一端的程序对话。网络编程是分布式世界的地基,今天的一切微服务、RPC 最终都落在 Socket 上,这一课值得慢慢啃。
本篇目标
读完本篇你应该能够:说清网络通信三要素各自解决什么寻址问题;用 InetAddress 操作 IP;用 DatagramSocket 完成 UDP 收发并解释它”快但不可靠”的原因;用 ServerSocket/Socket 写出 TCP 客户端与服务端;把单连接服务端改造成每连接一线程的并发模型,并说出它的瓶颈在哪。
网络通信三要素
两台机器上的程序要通信,需要回答三个层层递进的问题:
IP 地址——找到哪台机器。 网络中每台设备的唯一标识。IPv4 由 4 个字节组成(约 42 亿个,2011 年已分完),IPv6 用 8 组十六进制数解决地址枯竭。127.0.0.1 是本地回环地址,数据不出网卡,是学习阶段的最佳拍档;ping 命令用来测试两台机器是否连通。
端口——找到机器上哪个程序。 一台机器同时跑着数据库、Web 服务、聊天软件,IP 只能定位到机器,端口(0~65535)才能定位到进程。1024 以下基本被系统占用,自己写程序用 1024 以上的端口;记住几个常用端口有直觉价值:MySQL 3306、HTTP 80、Tomcat 8080。
协议——双方怎么说话。 数据传输的规则约定,本篇的主角是两个:UDP 与 TCP。
Java 中用 InetAddress 封装 IP:
InetAddress local = InetAddress.getLocalHost(); |
UDP:把数据装进信封扔进邮筒
UDP 面向无连接:发送前不建立连接,不管对方在不在、收没收到,发出去就完事。因此它不可靠(可能丢包、乱序、重复),但也快——没有建连开销,没有确认重传,协议头只有 8 个字节。视频通话、直播、DNS 查询这类”丢一帧无所谓、慢一点都不行”的场景都用它。可以把 UDP 想象成寄平信:信封一扔就不管了,邮资便宜、投递快,但寄没寄到只有天知道;TCP 则像打客服电话,先确认对方在线、通话中句句有回应,稳妥但费时间。协议没有优劣,只有场景匹配。
Java 的 API 比喻很形象:DatagramSocket 是码头,DatagramPacket 是集装箱。
发送端:
DatagramSocket socket = new DatagramSocket(); // 随机端口 |
接收端:
DatagramSocket socket = new DatagramSocket(6666); // 必须监听约定端口 |
两个常见错误:一是接收端没绑定发送端约定的端口,包寄到了没人收;二是 new String(buf) 不按 packet.getLength() 截断,把缓冲区的旧数据也读了进去。想让收发同时进行,就把收和发各放一条线程——这正是上一批并发知识的用武之地。
TCP:先握手,再通话
TCP 面向连接,通信前要先三次握手:客户端发 SYN 请求连接,服务端回 SYN+ACK 表示同意,客户端再回 ACK 确认,通道建立。之后数据有序、不丢、不重,代价是建连和维护连接的开销,速度略低于 UDP。文件传输、网页浏览这种”一个字节都不能错”的场景必须用它。
Java 中服务端用 ServerSocket 监听,客户端用 Socket 发起连接,连接建立后双方各自拿到一个 Socket,数据就通过 Socket 上的 IO 流双向传输——上一阶段的流操作在这里原样复用。
Socket(套接字)这个概念值得多停留一秒:IP 加端口构成网络中唯一可寻址的端点,通信两端各持一个,网络通信本质上就是两个 Socket 之间的数据流动。应用程序不需要关心网卡、路由和报文分片,操作系统把这些都封装在 Socket 背后,对上只暴露熟悉的”读流写流”接口——这是分层设计的胜利,传输层的复杂性对应用层完全透明。
服务端:
ServerSocket server = new ServerSocket(9999); |
客户端:
Socket socket = new Socket("127.0.0.1", 9999); // 指定服务端的 IP 和端口 |
注意两端读写顺序要配对:一端读的时候另一端必须在写,否则两边都阻塞在 readLine() 上干等——这是新手联调时最常踩的坑。另一个高频坑是忘记关流:Socket 及其流都是操作系统层面的稀缺资源,用 try-with-resources 或 finally 显式关闭,否则长时间运行的服务端会耗尽文件描述符。此外 accept() 和 read() 都是阻塞调用,程序”卡住不动”往往不是死机,而是在等网络对端。
多线程服务端:每连接一线程
上面的服务端只能服务一个客户端,accept() 返回一次就结束,现实中不可用。改造思路很直接:accept 循环 + 每来一个连接就交给一条新线程处理:
ServerSocket server = new ServerSocket(9999); |
主线程不被某个客户端的长会话拖住,服务端因此能同时接待多个连接。配合一个练手题目就能跑通全流程:客户端键盘录入字符串发给服务端,服务端反转后写回,客户端打印结果。
但这个模型有个明显的天花板:每连接一线程,一万个连接就是一万条线程。回看上一篇的内容——线程创建贵、栈内存默认 1MB、上下文切换随线程数暴涨——连接数一高,服务端资源先耗尽,而不是业务先跑不动。这也正是线程池登场的理由:把”一连接一线程”改成”有限线程处理排队连接”,至少资源消耗有了上限;以及后来 NIO(用一个线程轮询管理成千上万个连接)出现的背景。从”一连接一线程”到线程池再到 NIO,是服务端并发模型演进的三级台阶,本篇先站在第一级上看清楚问题。学网络编程最有价值的收获往往不是 API 本身,而是这条”需求逼出演进”的线索——日后面对 Netty、Tomcat 连接器时,你会知道它们在解决什么。
小结
- 三要素各管一层寻址:IP 找机器,端口找程序,协议定规则。
- UDP 无连接、不可靠但快,DatagramSocket 收发要记住”按约定端口、按实际长度读数据”。
- TCP 三次握手建连后可靠传输,通信本质是两个 Socket 之间的 IO 流读写,两端读写顺序必须配对。
- 多线程服务端 = accept 循环 + 每连接一线程,能并发但受线程成本制约——这个瓶颈正是线程池与 NIO 的存在理由。
下一篇是本系列收官:反射与类加载,看看 Java 程序在运行时如何”认识自己”。

