网络编程(进阶)
网络编程(进阶)
目标:理解两台机器之间如何通信;掌握
Socket/ServerSocket与输入/输出流收发数据的基本模型;会用 Java 写出最简的 client / server,理解 TCP 三次握手与「序列化对象」在网络里的落地。本模块是进阶补充(原 32 课次无独立网络课),覆盖复习提纲「六、网络编程基础:socket / client / server」。贯穿示例用网络版 ERASE(
素材/Erase-JAVA-NET/)——单机版俄罗斯方块的对战版,客户端与服务端各自起线程并发收发。
一、为什么要学网络编程:两台机器如何通信
- 单机程序的局限:前面所有程序都跑在一台机器的一个进程里;数据要「跨机器」传递(聊天、对战、网页请求),就需要网络编程;
- 最抽象的模型是 client / server:一台机器扮演服务端(server)——开一个「门牌号」(端口)等着人来;另一台扮演客户端(client)——拿着门牌号找上门去。数据就在这条通道上双向流动;
- Java 用
java.net包的 Socket 来做这件事:ServerSocket(服务端监听)、Socket(客户端连接)、InputStream/OutputStream(收发字节)。
一句话:服务端「守株待兔」,客户端「主动上门」,握手成功后双向传数据。
二、TCP/IP 与 Socket 基础
传输层两大协议:
- TCP(面向连接的可靠传输):先建连(三次握手)、再传数据;数据按序、不丢、可重传。聊天、对战、网页这类「要保序保真」的场景用它——本模块全程围绕 TCP;
- UDP(无连接的不保证传输):不建连、直接发;快但不保证到达与顺序,适合实时音视频等「丢一帧没关系」的场景。(两者详细对比见后续
TCPvsUDPCompare实验。)
Socket 编程四件套(本模块的核心 API):
| 角色 | 关键类 / 方法 | 作用 |
|---|---|---|
| 服务端·监听 | new ServerSocket(port) | 绑定端口并进入监听,等待客户端接入 |
| 服务端·接客 | serverSocket.accept() | 阻塞等待;每来一个客户端返回一个专属 Socket |
| 客户端·连接 | new Socket(host, port) | 向服务端发起 TCP 连接(背后是三次握手) |
| 双方·收发 | getInputStream() / getOutputStream() | 输入流收数据、输出流发数据(配合序列化) |
三、Socket 握手与数据帧
一次完整的 Socket 通信遵循固定五步:① 监听 → ② 连接 → ③ 通道 → ④ 收发 → ⑤ 关闭。下面按时序逐步高亮每条消息,看 TCP 三次握手如何建立连接、序列化数据帧如何在两端之间双向流动:
交互实验:Socket 客户端 / 服务端通信(SocketClientServerSim)
网络 · TCP两台机器要通信,靠的是 Socket:服务端 new ServerSocket(port) 监听端口、accept() 接客;客户端 new Socket(host, port) 发起连接(背后是 TCP 三次握手);双方再用输入/输出流收发序列化对象。下面按 「监听 → 连接 → 通道 → 收发 → 关闭」逐步高亮每条消息:
客户端 → 服务端:请求建立连接,携带随机初始序号 x。
服务端 → 客户端:同意建连,给出自己的序号 y,并确认收到 x(回 x+1)。
客户端 → 服务端:确认收到 y(回 y+1)。三次握手完成,连接正式建立。
客户端每帧把 PlayInfo(玩家名 + 分数/等级/速度 + 棋盘状态)用 writeObject() 序列化成字节发出去,对端 readObject() 再还原——这就是模块6 学的序列化在网络里的落地。
| 字段 | 值(示例) |
|---|---|
| name | zm23 |
| score | 1280 |
| level | 5 |
| speed | 400 |
| board[41×22] | 棋盘状态… |
最小可运行 client / server(改编自网络版 ERASE)
// 服务端:监听端口,为每个客户端起一个线程收发数据
ServerSocket ss = new ServerSocket(9001); // 绑定端口并进入监听
while (true) {
Socket cs = ss.accept(); // 阻塞等待客户端接入
new Thread(() -> { // 每客户端一个线程(模块7)
ObjectInputStream is = new ObjectInputStream(cs.getInputStream());
ObjectOutputStream os = new ObjectOutputStream(cs.getOutputStream());
while (true) {
PlayInfo info = (PlayInfo) is.readObject(); // 读数据帧
os.writeObject(handle(info)); // 回发数据帧
os.flush();
}
}).start();
}// 客户端:连接服务端,收发序列化数据
Socket cs = new Socket("localhost", 9001); // TCP 三次握手建立连接
ObjectOutputStream os = new ObjectOutputStream(cs.getOutputStream());
ObjectInputStream is = new ObjectInputStream(cs.getInputStream());
PlayInfo me = buildMyInfo(); // 组装自己的操作/状态
os.writeObject(me); // 发数据帧(序列化)
os.flush();
PlayInfo fellow = (PlayInfo) is.readObject(); // 读回发来的数据帧
cs.close();Socket / 网络要点
- Socket 通信三件套:Server 端 `new ServerSocket(port)` 监听 + `accept()` 拿专属 Socket;Client 端 `new Socket(host, port)` 发起连接;双方用 `getInputStream()` / `getOutputStream()` 收发字节。
- TCP 是面向连接的可靠传输:`new Socket(...)` 背后是三次握手(SYN → SYN-ACK → ACK),建连后数据按序、不丢、可重传——适合聊天/对战这类要保序保真的场景(UDP 则相反,见 C2 对比)。
- Java 里收发的是「序列化对象」:`writeObject()` 把对象转成字节流发出,`readObject()` 再还原成对象(Erase-NET 每帧发的就是 `PlayInfo`)——模块6 的序列化在网络里的落地。
- 每个客户端对应服务端一个线程(`ServThreadPerClient`):`accept()` 返回后立刻起线程处理该客户端的收发——多线程 + 网络结合,正是模块7 的知识落到真实项目。
TCP 三次握手的每一步。 上面 new Socket(host, port) 背后正是 TCP 三次握手:客户端发 SYN、服务端回 SYN-ACK、客户端再发 ACK——三步走完,两端才同时进入 ESTABLISHED;若目标端口没有服务在监听,服务端回 RST(复位),连接被拒绝。下面逐步走查三个报文如何在 client/server 间往返、两端状态机如何同步迁移(含 RST 异常分支):
交互实验:TCP 三次握手动画(ThreeWayHandshake)
new Socket(host, port) 背后是 TCP 三次握手:客户端发 SYN、服务端回 SYN-ACK、 客户端再发 ACK,三步走完两端才进入 ESTABLISHED;若服务端拒绝则回 RST 复位。点「下一步」看报文往返与状态机同步迁移。
客户端(client)
服务端(server)
初始
客户端尚未发起连接;服务端已 bind + listen,处于监听态等待接入。
四、TCP vs UDP:可靠有序 vs 快而无序
上面实验全程用的是 TCP。但传输层其实有两位选手——TCP 和 UDP,它们不是「谁更好」的关系,而是「各管一类场景」:
- TCP:先建连(三次握手)、再传数据;确认 + 重传保证必达,序号保证有序——代价是头部大、状态多、有延迟;
- UDP:不建连直接发,头部仅 8 字节——快,但不保证到达与顺序,丢了不重传。
七维度并排对比 + 场景选型练习(点场景看推荐协议)+ 两套 Java API 代码对照:
交互实验:TCP vs UDP 并排对比(TCPvsUDPCompare)
同一条传输层,两种性格——点击任意维度行,看两者的具体表现与一句话点评:
← 点击任意维度行查看点评
← 点一个场景,看推荐协议与理由
// TCP:先建连,可靠有序
ServerSocket server = new ServerSocket(9999);
Socket socket = server.accept(); // 三次握手完成
OutputStream out = socket.getOutputStream();
out.write("Hello".getBytes()); // 按序到达、保证送达// UDP:不建连,直接发
DatagramSocket socket = new DatagramSocket(9999);
byte[] buf = "Hi".getBytes();
socket.send(new DatagramPacket(buf, buf.length,
InetAddress.getByName("localhost"), 8888)); // 发了就不管
byte[] rbuf = new byte[1024];
DatagramPacket rp = new DatagramPacket(rbuf, rbuf.length);
socket.receive(rp); // 一次 receive() = 一个完整数据报- •TCP = 面向连接、可靠、有序、字节流;UDP = 无连接、不保证、无序、数据报。
- •选型原则:保序保真(网页/聊天/对战)→ TCP;速度实时、可容忍丢包(音视频/直播/DNS)→ UDP。
- •Java API 两套完全分开:TCP 用 ServerSocket / Socket + 输入输出流;UDP 用 DatagramSocket + DatagramPacket 的 send() / receive()。
- •真实项目里两者可以并存:例如对战用 TCP 同步状态(可靠),同时用 UDP 同步低延迟的位置信息。
一句话选型:要保序保真(网页、聊天、对战)→ TCP;要速度实时、丢一帧没关系(音视频、直播、DNS)→ UDP。
五、把两个程序跑起来(贯穿示例:网络版 ERASE)
最简的 client / server 只要几十行——上面实验里已给出可直接运行的 Server.java / Client.java。真实项目网络版 ERASE(素材/Erase-JAVA-NET/src/com/zmofun/neterase/)把它们放大成完整对战:
Server.java:new ServerSocket(port)监听 →while(true) accept()接客 → 每个客户端起一个ServThreadPerClient线程,循环is.readObject()读、os.writeObject()转发给队友;Client.java:new Socket(server, port)连接 → 起ReadThread收、主逻辑sendMe()发(每帧把一个PlayInfo——玩家名 + 分数/等级/速度 + 棋盘状态——序列化发出);- 两端配合模块6 的序列化(
writeObject/readObject)与模块7 的多线程(每客户端一条线程),把前面学的知识串成真实可运行的对战程序。
思政落点:网络编程让「个人借助平台提升能力」——每个客户端是独立个体,服务端是共享平台;遵守协议(TCP/序列化格式)才能协作无间。
六、URL 与 WebService 概览
- URL:统一资源定位符,形如
http://host:port/path——它把「用哪个协议、找哪台机器、访问哪个资源」写在一行里;浏览器地址栏、new URL(...)都是它。理解 URL 是理解 HTTP/网页请求的基础; - WebService:让不同程序跨网络以标准方式(如 REST / SOAP)交换数据的统称。本模块只作概览——掌握 Socket 这个「最底层」的收发模型后,上层协议都是在它之上层层封装出来的。
七、动手实验:在浏览器里编译运行 Java(CodePlayground)
前面都是「看」网络,这一节反过来——直接在本页写一段 Java,让后端真实地 javac 编译 + java 运行,再把输出送回你的浏览器。这正是第一节 client / server 模型的落地版:
- 前端(本页面) = 客户端:点「运行」后,浏览器开一条 WebSocket 连到后端
ws://<host>:8080/ws; - 后端(Spring Boot 服务,端口 8080) = 服务端:按 STOMP 协议收到目的地为
/app/run的源码,在独立沙箱里编译运行,把结果推回你这条会话专属的队列/queue/result-<你的id>——多个标签页同时实验也互不串台; - 为什么用 WebSocket + STOMP 而不是普通 HTTP? 编译+运行要好几秒:普通 HTTP 请求要么傻等、要么轮询「好了没」;WebSocket 是全双工通道,后端一跑完就能主动把结果推过来。STOMP 骑在 WebSocket 上,给消息加上「目的地」(像寄信写地址),前端订阅哪个队列、后端往哪个队列回,一目了然。
交互实验:浏览器里编译运行 Java(CodePlayground)
未连接左边写 Java,点「运行」——源码经 WebSocket+STOMP 发给后端(端口 8080),真实javac编译 + java 运行,输出推回你的浏览器。先选一个预置程序试试:
← 点「运行」看编译/运行结果
💡 最小可运行程序:UTF-8 中文输出 + 表达式求值。
ws://<host>:8080/ws)→ 后端 STOMP /app/run → 沙箱编译运行 → 结果推回你的会话专属队列 /queue/result-<你的id>。每个浏览器标签页只收自己的结果,互不串台。沙箱安全说明(C4):每次运行都放在独立临时目录(用完即删)、子进程内存上限 256 MB、编译/运行各自 10 秒超时后强杀整棵进程树——所以「死循环」预置不会把服务端拖死,只会收到
timeout。
八、总结
- client / server 模型:服务端
ServerSocket监听 +accept()接客,客户端Socket主动连接; - TCP 三次握手(SYN → SYN-ACK → ACK)建立可靠、有序的连接;收发用
getInputStream()/getOutputStream(); - Java 收发的是序列化对象:
writeObject()/readObject()把对象与字节流互转——模块6 的序列化在网络里落地;每客户端一个线程(模块7)并发处理。
思考题
- 为什么说 TCP 是「面向连接」的?三次握手的三步分别由谁发起、目的是什么?
- 服务端为什么通常要为每个客户端起一个线程?如果只开一个线程串行处理所有客户端,会出现什么问题?
- Java 通过 Socket 发送一个对象时为什么能直接
writeObject(obj)而不是先手动转成字符串?(提示:序列化)
学习小测验
学习小测验
在 Java Socket 编程中,负责监听端口、等待客户端接入的是哪个类?
服务端调用 serverSocket.accept() 的作用是( )
TCP 建立连接时的三次握手顺序是( )
Java 里通过 Socket 发送一个对象时,应使用( )
网络版 ERASE 里,服务端为每个接入的客户端各起一个线程(ServThreadPerClient)处理收发,这主要体现了哪个模块的知识?
客户端要向服务端发送数据,应使用( )
开发实时视频通话应用时,传输层协议更适合选( )
TCP 能保证数据按发送顺序到达接收方,靠的是( )
CodePlayground 前端把 Java 源码提交给后端编译运行、并接收回传的结果,采用的传输方式是( )
在 CodePlayground 里提交一段含 for(;;){} 死循环的程序,沙箱最可能返回的状态是( )
TCP 三次握手中,第三步由客户端发送的报文是( )
客户端向一个没有服务在监听的端口发起连接,服务端最可能回( )