☕ 第十三章 网络编程
📄 一句话概括

服务端开门等人、客户端主动伸手,接通之后两边各掏一条 IO 流——你写我读、我写你读,网络通信就变成了"读写流"。

上一节 13.1 网络基础 把快递规则(IP、端口、协议)讲清楚了,还预告了 Socket 就是"插座"。这节正式插电:TCP 通信 = 两个程序通过 Socket 互相读写流。学完你会发现,网络编程的地基居然是第十章的 IO——内功完全复用。


一、两个主角:ServerSocket 和 Socket

TCP 通信里两边不是平等的,分工一开始就定好了:

服务端 客户端
类 ServerSocket Socket
角色 开门等客的店家 上门消费的客人
启动顺序 必须先启动 后启动
核心动作 accept() 堵在门口等连接 new Socket(...) 主动拨号
得到什么 accept 返回一条 Socket(专线) 自己就是一条 Socket

记忆口诀:Server 管开门,Client 管敲门。

服务端三步

// ① 占好 8888 房间,开门营业
ServerSocket server = new ServerSocket(8888);
System.out.println("服务端已启动,等待连接...");

// ② 堵在门口等客 —— 没人连就永远卡在这一行(阻塞)
Socket socket = server.accept();
System.out.println("有客人上门了:" + socket.getRemoteSocketAddress());

// ③ 掏 IO 流,开始对讲

客户端两步

// ① 拨号 + 三次握手,全在这一行完成(连不上直接抛异常)
Socket socket = new Socket("127.0.0.1", 8888);
System.out.println("连上了服务端!");

// ② 掏 IO 流,开始对讲
❗ accept 的"阻塞"是什么意思

程序执行到 accept() 会停在这里不动,直到有客户端连上来才继续往下走——就像店家堵在门口盯着街面,客人不来就不回柜台。这个"卡住等事"的行为叫阻塞,是网络编程的常态,不是 bug。

同理,read() 也是阻塞的——管子里没数据时它会一直等,等到有数据(返回个数)或对方关闭输出(返回 -1,10.2 字节流 的哨兵又来了)才返回。


二、流的交叉对位:你写的就是我读的

两边接通后,各有一个入口和出口,但它们是交叉对着的:

客户端 ⇄ 服务端
getOutputStream()(出口) ——写进同一根管——→ getInputStream()(入口)
getInputStream()(入口) ←——同一根管写出来—— getOutputStream()(出口)

记住这一句就懂了大半:客户端的"写",就是服务端的"读";同一根管子,两头各插一个口。

所以"给对方发消息"的代码,长得和10.2里写文件一模一样——只是流不再指向文件,而指向网线:

// 客户端:发一句话
OutputStream os = socket.getOutputStream();
os.write("服务端你好,我是客户端".getBytes());
socket.shutdownOutput();  // 发完了,喊一声"我说完了"
// 服务端:收这句话
InputStream is = socket.getInputStream();
byte[] cart = new byte[1024];
int len = is.read(cart);
System.out.println("收到:" + new String(cart, 0, len));
💡 字符流套餐更好用

传文字时,用10.3的姿势包装一下更顺手: - 发:PrintStream ps = new PrintStream(socket.getOutputStream(), true); → ps.println("你好");(true 表示自动 flush,规矩二直接免疫) - 收:BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream())); → br.readLine();(读到一行完整文本)

这叫装饰叠加——IO 全家是套娃,缺什么能力包一层,第十章收官时说过的话在这儿兑现了。


三、三条保命规矩

规矩一:先服务端,后客户端

客户端 new Socket(...) 拨号时,如果对应端口没人开门,立刻抛:

ConnectException: Connection refused: connect
⚠️ 这不是代码错,是启动顺序错

看到这句话先别改代码——检查服务端跑起来没有。IDEA 里两个 main 方法各点一次绿色箭头就行,先跑服务端那个类,再跑客户端那个类。

规矩二:写完记得 flush

流自带缓冲(10.3 的账),写完不冲一把,数据可能还在半路上,对方一直干等。用 PrintStream(…, true) 自动冲,或写完手动 flush()。

规矩三:只关闭输出方向用 shutdownOutput()

这句最值钱。想象对话场景:

// 客户端
os.write("服务端你好".getBytes());
socket.shutdownOutput();   // ✅ 只关"说"的方向:我把话说完了,但我还在听
// socket.close();         // ❌ 这是整个挂断电话——我还想听服务端回话呢!

一边说还想一边听,就永远别 close。


四、动手实验:Demo23 一问一答

建包 com.bozai.network,两个类各带 main:

服务端 Demo23Server:

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;

public class Demo23Server {
    public static void main(String[] args) throws IOException {
        ServerSocket server = new ServerSocket(8888);
        System.out.println("【服务端】已启动,等待连接...");
        Socket socket = server.accept();            // 等客(阻塞)
        System.out.println("【服务端】客人到:" + socket.getRemoteSocketAddress());

        // 收
        BufferedReader br = new BufferedReader(
                new InputStreamReader(socket.getInputStream()));
        String msg = br.readLine();
        System.out.println("【服务端】收到:" + msg);

        // 回
        PrintStream ps = new PrintStream(socket.getOutputStream(), true);
        ps.println(msg + " 收到,服务端在的!");

        socket.close();
        server.close();
    }
}

客户端 Demo23Client:

import java.io.*;
import java.net.Socket;

public class Demo23Client {
    public static void main(String[] args) throws IOException {
        Socket socket = new Socket("127.0.0.1", 8888);
        System.out.println("【客户端】已连上服务端");

        // 发
        PrintStream ps = new PrintStream(socket.getOutputStream(), true);
        ps.println("服务端你好,我是客户端");

        // 听(服务端的回话走同一根管的反向)
        BufferedReader br = new BufferedReader(
                new InputStreamReader(socket.getInputStream()));
        System.out.println("【客户端】服务端说:" + br.readLine());

        socket.close();
    }
}

保姆式跑法(IDEA):

  1. 在 Demo23Server 里右键 → Run 'Demo23Server.main()',控制台看到 已启动,等待连接... 算成功
  2. 再在 Demo23Client 里右键 → Run,两个控制台各自打印问候,算成功
  3. 想再跑一轮?两个都重跑一遍即可(TCP 一条线聊完就挂,想常驻要 while(true),见误区三)

三个 30 秒实验

  1. 顺序倒着来:只跑客户端不跑服务端 → 当场看 Connection refused,体会规矩一
  2. 去掉 shutdownOutput():服务端会卡在 readLine() 不动(阻塞等"说完了"信号),体会规矩三和 -1 哨兵
  3. 服务端不关,连两个客户端:第二个客户端连上后服务端毫无反应——accept() 只接了一回,想接第二回要 while(true) 循环 + 12.2 的线程(一客户端一线程),这是 13.3 聊天室的地基

五、常见误区

⚠️ 误区一:`new Socket(...)` 只是"造对象",不联网

错。文件流 new FileOutputStream 不碰磁盘(10.1 的账),但 new Socket(ip, 端口) 这一行当场联网——三次握手就在构造方法里完成,连不上直接抛异常。Socket 是 IO 家族里唯一的"急性子"。

⚠️ 误区二:服务端和客户端共用一个类

不行也不该。服务端要 ServerSocket + accept(),客户端只要 new Socket(),职责完全不同,老老实实写两个类。尤其注意 accept() 返回的 Socket 才是"对客专线",ServerSocket 本身只管开门,别拿它读写流。

⚠️ 误区三:以为程序跑完一轮就"一直在线"

Demo23 聊完一轮两边就 close 了。真实服务端都是 while(true){ Socket s = server.accept(); ... } 常驻接客——不然网页刷新一次服务器就下班了。

⚠️ 误区四:忘了端口号两边要一致

客户端拨 8888,服务端就得开门在 8888。端口对不上 = 敲错门,照样 Connection refused。顺手检查:端口号取 1024 以上(13.1 的规矩)。

⚠️ 误区五:读写都用字节流不转字符,中文变乱码

管子里跑的是字节(10.2 单字节读中文乱码的账),传文字时套 InputStreamReader + BufferedReader 转字符流,翻译官跟上就没事。


六、折叠自测

❓ Q1:`ServerSocket` 和 `Socket` 各管什么?哪个必须先启动?

ServerSocket 服务端专用,只管占端口开门 + accept 等客;Socket 是通信专线本身,客户端直接 new 它拨号。服务端必须先启动,否则客户端拨号 Connection refused。accept 返回的那条 Socket 才是用来收发的。

❓ Q2:客户端 `getOutputStream().write(...)` 写的数据,服务端从哪读?

从服务端 Socket 的 getInputStream() 读。客户端的出口直通服务端的入口,是同一根管子的两头——你写我读,交叉对位。

❓ Q3:`read()` 读到 -1 意味着什么?怎么触发它?

对方关闭了输出方向(shutdownOutput() 或 close()),管子那头不会再有数据了,-1 是"说完了"的信号——10.2 文件读的 -1 哨兵在网络流里换了个身份。

❓ Q4:客户端发完消息想等服务端回复,该调 `close()` 还是 `shutdownOutput()`?

shutdownOutput()。close 是整个挂断(还想听呢就挂电话);shutdownOutput 只关"说"的方向,保留"听"的方向。一句话:只关嘴,别关耳朵。

❓ Q5(毕业题):写一个"回声服务器"——客户端发什么,服务端原样返回什么,客户端打印收到的回声。

参考骨架(就是 Demo23 换个回法):

// 服务端收一行后:
ps.println(br.readLine());          // 原样弹回
// 客户端发完后:
System.out.println(br.readLine());  // 打印回声

自检三点:①先服务端后客户端;②客户端发完有 shutdownOutput()(用 readLine 就必须喊"说完了");③两边都 close 干净。跑通它,你就打通了 Java 与网络之间的任督二脉。


下一步学什么


相关笔记