第一次让两个进程通过网络说话,是学编程以来最接近「魔法」的时刻:两个完全独立的 JVM,隔着 localhost,互相收到了对方的消息。本章把这份魔法拆开:网络课(第 9 章)讲过的 Socket 在 Java 里的样子、UDP 和 TCP 两套 API、以及「一线程一连接」这个经典服务端模型的写法与局限。计网课第 3、4 章的协议知识在这里全部落地成代码。
InetAddress.getByName("localhost")(域名解析,计网课第 2 章 DNS 在这里被调用)。UDP 没有连接概念,发的是「数据报」(一封写好地址的信),收发双方对等:
// 发送端
try (DatagramSocket s = new DatagramSocket()) {
byte[] data = "你好,UDP".getBytes();
DatagramPacket p = new DatagramPacket(data, data.length,
InetAddress.getByName("127.0.0.1"), 9999);
s.send(p); // 扔出去就完事,对方在不在不关我事
}
// 接收端(先启动)
try (DatagramSocket s = new DatagramSocket(9999)) {
byte[] buf = new byte[1024];
DatagramPacket p = new DatagramPacket(buf, buf.length);
s.receive(p); // 阻塞等信
System.out.println(new String(p.getData(), 0, p.getLength()));
}
先跑接收端再跑发送端,消息直达。UDP 的特性全在 API 的「无状态」里:没有 accept、没有流、一个包一个包地寄。它的「不可靠」用个实验感受:内网 localhost 基本不丢,丢包要在真实网络或限速工具下才看得见,这也是「UDP 快」的真相(省了握手和确认,不是传输更快)。
TCP 是面向连接的字节流(计网课第 3 章),Java 把它包成两个类:ServerSocket(守门)和 Socket(通道)。经典入门项目 Echo 服务器:收到什么回什么。
// 服务端
import java.io.*;
import java.net.*;
public class EchoServer {
public static void main(String[] args) throws IOException {
try (ServerSocket server = new ServerSocket(9999)) {
System.out.println("服务端就绪,9999 端口守候中...");
while (true) { // 服务端永不退休
Socket client = server.accept(); // 阻塞:没有连接就停在这
System.out.println("客户端上线:" + client.getRemoteSocketAddress());
try (BufferedReader in = new BufferedReader(
new InputStreamReader(client.getInputStream(), "UTF-8"));
PrintWriter out = new PrintWriter(
new OutputStreamWriter(client.getOutputStream(), "UTF-8"), true)) {
String line;
while ((line = in.readLine()) != null) { // null = 对方关连接
out.println("[echo] " + line); // 自动 flush
}
}
}
}
}
}
// 客户端
try (Socket s = new Socket("127.0.0.1", 9999);
BufferedReader fromServer = new BufferedReader(
new InputStreamReader(s.getInputStream(), "UTF-8"));
BufferedReader keyboard = new BufferedReader(new InputStreamReader(System.in));
PrintWriter toServer = new PrintWriter(s.getOutputStream(), true)) {
String msg;
while (!(msg = keyboard.readLine()).equals("bye")) {
toServer.println(msg);
System.out.println(fromServer.readLine());
}
}
两个观察。其一,第 12 章的 IO 流体系在网络上是原样复用的:getInputStream 拿到的流和文件流同一个接口,读文件和读网络是同一套代码,这是流抽象的胜利。其二,new Socket(host, port) 内部就是 connect(三次握手发生在这一行的阻塞里),readLine() 阻塞在等数据(计网课第 4 章的「对方还没发」),阻塞是默认行为,理解它才能理解 13.4 的问题。
测试工具:服务端跑起来后,终端 nc localhost 9999 也能直接连(和你的 Java 客户端等价),先 nc 后写客户端是排错利器。
上面的服务器同一时刻只能陪一个客户端聊天(accept 之后的 while 在伺候它,别人连不进来)。经典解法:每个连接开一个线程(操作系统课第 2 章的线程终于派上用场):
while (true) {
Socket client = server.accept(); // 主线程只管接客
new Thread(() -> handle(client)).start(); // 服务交给新线程
}
static void handle(Socket client) {
try (client; ...) { /* 和上面的 echo 逻辑相同 */ }
}
这个模型直观、够用到几十上百连接;上限在千级连接(每线程 1MB 栈 + 上下文切换开销,操作系统课第 3 章的账),万级并发要换 IO 多路复用(Java NIO 的 Selector,计网课第 9 章 IO 多路复用的 Java 版),那是进阶课程的内容,知道方向即可。先跑通 thread-per-connection,再理解 NIO 解决什么问题,是学网络编程的正确顺序。
jstack(JDK 自带)看线程数,数一数你的连接都对应哪些线程。