服务端开门等人、客户端主动伸手,接通之后两边各掏一条 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() 会停在这里不动,直到有客户端连上来才继续往下走——就像店家堵在门口盯着街面,客人不来就不回柜台。这个"卡住等事"的行为叫阻塞,是网络编程的常态,不是 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(); // ❌ 这是整个挂断电话——我还想听服务端回话呢!
shutdownOutput():半关,对应管道的 -1 哨兵信号,对方read()返回 -1 知道你讲完了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):
- 在
Demo23Server里右键 → Run 'Demo23Server.main()',控制台看到已启动,等待连接...算成功 - 再在
Demo23Client里右键 → Run,两个控制台各自打印问候,算成功 - 想再跑一轮?两个都重跑一遍即可(TCP 一条线聊完就挂,想常驻要
while(true),见误区三)
三个 30 秒实验
- 顺序倒着来:只跑客户端不跑服务端 → 当场看
Connection refused,体会规矩一 - 去掉
shutdownOutput():服务端会卡在readLine()不动(阻塞等"说完了"信号),体会规矩三和 -1 哨兵 - 服务端不关,连两个客户端:第二个客户端连上后服务端毫无反应——
accept()只接了一回,想接第二回要while(true)循环 + 12.2 的线程(一客户端一线程),这是 13.3 聊天室的地基
五、常见误区
错。文件流 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 转字符流,翻译官跟上就没事。
六、折叠自测
ServerSocket 服务端专用,只管占端口开门 + accept 等客;Socket 是通信专线本身,客户端直接 new 它拨号。服务端必须先启动,否则客户端拨号 Connection refused。accept 返回的那条 Socket 才是用来收发的。
从服务端 Socket 的 getInputStream() 读。客户端的出口直通服务端的入口,是同一根管子的两头——你写我读,交叉对位。
对方关闭了输出方向(shutdownOutput() 或 close()),管子那头不会再有数据了,-1 是"说完了"的信号——10.2 文件读的 -1 哨兵在网络流里换了个身份。
shutdownOutput()。close 是整个挂断(还想听呢就挂电话);shutdownOutput 只关"说"的方向,保留"听"的方向。一句话:只关嘴,别关耳朵。
参考骨架(就是 Demo23 换个回法):
// 服务端收一行后:
ps.println(br.readLine()); // 原样弹回
// 客户端发完后:
System.out.println(br.readLine()); // 打印回声
自检三点:①先服务端后客户端;②客户端发完有 shutdownOutput()(用 readLine 就必须喊"说完了");③两边都 close 干净。跑通它,你就打通了 Java 与网络之间的任督二脉。
下一步学什么
- ☑ 13.3 UDP通信 ✅ —— 点此复习:DatagramSocket 快递员 + DatagramPacket 包裹,不握手扔了就走
相关笔记
- 第十三章 网络编程/13.1 网络基础:IP、端口、协议、Socket 插座概念——本节的地基
- 第十章 I-O/10.2 字节流:read 的 -1 哨兵与 byte[] 小推车,网络流照单全收
- 第十章 I-O/10.3 字符流:BufferedReader / PrintStream 字符流套餐与 flush 规矩
- 第十二章 多线程/12.2 线程的创建:一客户端一线程的预科班,聊天室的门票
- 5.3 异常处理及语法:try-with-resources 关流姿势,网络流同样适用