TCP、Socket 与 ServerSocket | JavaSE
TCP、Socket 与 ServerSocket
一、学习目标
学完本章,你应该能够:
- 能够解释 TCP 与 UDP 在通信模型上的核心区别。
- 能够解释 TCP“面向连接、可靠、有序、字节流”的含义。
- 能够理解 Java 中
Socket与ServerSocket的不同职责。 - 能够使用
Socket(host, port)请求建立 TCP 连接。 - 能够使用
ServerSocket(port)监听服务端端口。 - 能够理解
accept()的阻塞行为及其返回的Socket对象。 - 能够通过
Socket的输入流和输出流完成最基本的一发一收。 - 能够区分“TCP 字节流”和“应用层消息边界”。
二、核心知识
2.1 从 UDP 切换到 TCP
前面我们使用 UDP:
Client
│
│ Datagram
▼
Server
UDP 的特点:
无连接
面向数据报
每个数据报独立
不提供可靠交付保证
现在换成 TCP:
Client
│
│ 建立连接
▼
Server
│
│
├────可靠字节流────┐
│ │
└──────────────────┘
TCP:
Transmission Control Protocol
传输控制协议
是一个:
面向连接的、可靠的、有序字节流传输协议。
2.2 TCP 的核心特点
可以先掌握四个关键词:
连接
可靠
有序
字节流
2.3 面向连接
UDP 发送时:
知道目标 IP + Port
↓
直接 send
TCP 不一样。
客户端真正传输应用数据前,需要:
与服务端建立 TCP 连接
Java 中最典型的代码:
Socket socket =
new Socket(
"127.0.0.1",
9999
);
这句话并不是简单:
创建一个普通 Java 对象
它还会尝试:
连接 127.0.0.1:9999
服务端必须有:
ServerSocket
监听对应端口。
2.4 TCP 建立连接
计算机网络课程通常会把 TCP 连接建立概括为:
三次握手
可以用最简模型理解:
Client Server
───── SYN ───────→
←── SYN + ACK ────
───── ACK ───────→
最终双方确认:
对方存在
双方通信能力基本就绪
连接状态建立
JavaSE 阶段重点不是手写 TCP 报文,而是理解:
Socket成功建立以后,应用程序才获得了一条可以进行双向字节流通信的 TCP 连接。
2.5 TCP 的可靠性
TCP 协议提供一系列机制来提高数据可靠交付能力,例如:
序号
确认
重传
校验
流量控制
拥塞控制
所以应用层看到的是:
一个可靠、有序的字节流。
如果发送:
A B C D E
正常连接状态下,接收程序不会把它作为:
E C A B D
这样的乱序字节流交给应用。
2.6 “TCP 可靠”不等于“业务绝不会失败”
必须避免一种误解:
用了 TCP
=
消息百分之百业务成功
不是。
例如:
连接中途断开
程序崩溃
服务器宕机
读取逻辑错误
协议设计错误
仍然可能导致业务失败。
TCP 所说的可靠,主要是:
传输层为已建立连接提供可靠、有序的字节流传输语义。
至于:
用户订单是否写入数据库
支付是否真正成功
聊天消息是否真正落库
仍然需要应用层自己保证。
2.7 TCP 是字节流
这是后面非常重要的概念。
UDP:
Packet A
Packet B
Packet C
有天然的数据报边界。
TCP:
byte byte byte byte byte byte byte ...
应用程序看到的是:
连续字节流。
例如客户端连续写:
Hello
Java
TCP
TCP 并不自动保存:
第一条消息
第二条消息
第三条消息
这种业务意义上的边界。
它只保证:
一串按顺序到达的字节
因此以后进行 TCP 多发多收时必须考虑:
消息边界 / 应用层协议。
2.8 Java TCP 核心 API
Java TCP 编程主要使用:
Socket
与:
ServerSocket
二者都位于:
java.net
包。
2.9 Socket 是什么
Java 中的:
Socket
可以理解为:
TCP 通信连接在应用程序中的对象表示。
客户端通常通过:
new Socket(host, port)
连接服务端。
例如:
Socket socket =
new Socket(
"127.0.0.1",
9999
);
此时如果连接成功:
Client Socket
│
│ TCP Connection
│
Server Socket
双方就拥有一条 TCP 通信通道。
2.10 ServerSocket 是什么
服务端不能使用普通:
Socket
去“等待所有客户端连接”。
Java 提供:
ServerSocket
它的核心职责:
监听某个本地 TCP 端口,等待新的连接请求。
创建:
ServerSocket serverSocket =
new ServerSocket(9999);
表示:
在当前主机
监听 TCP 9999
模型:
TCP 9999
▲
│
ServerSocket
│
等待客户端连接
2.11 ServerSocket 和 Socket 的区别
这是本章必须彻底分清的地方。
ServerSocket
负责:
监听
等待新连接
接受新连接
Socket
负责:
代表一条已经建立的 TCP 通信连接
读取数据
写出数据
模型:
Server
ServerSocket
│
│ accept()
▼
Socket
▲
│ TCP
│
Socket
Client
所以:
ServerSocket
≠
真正与客户端交换业务数据的那条 Socket
它主要负责:
接客
而 accept() 返回的:
Socket
才负责后续:
通信
2.12 accept()
核心方法:
Socket accept()
例如:
Socket socket =
serverSocket.accept();
含义:
等待客户端连接
↓
有客户端连接到达
↓
接受该连接
↓
返回一个 Socket
如果当前没有客户端连接:
serverSocket.accept();
通常会:
阻塞等待。
与 UDP 的:
receive()
很像:
UDP:
receive() 等数据
TCP:
accept() 等连接
2.13 为什么 accept() 返回 Socket
假设服务端监听:
TCP 9999
客户端:
127.0.0.1:53120
建立连接。
服务端的:
ServerSocket
依旧负责监听:
9999
而:
accept()
返回:
Socket socket
表示:
服务端与这个客户端之间
已经建立的具体连接
因此服务器会同时存在:
ServerSocket
+
Socket
它们职责完全不同。
2.14 TCP Socket 如何传输数据
TCP 不使用:
DatagramPacket
而是使用:
IO 流
Socket 提供:
getInputStream()
获得:
InputStream
用于:
读取对方发送的数据
以及:
getOutputStream()
获得:
OutputStream
用于:
向对方发送数据
所以前面学习的:
File / IO
正式和网络编程合流。
2.15 TCP 通信管道模型
客户端:
Socket
├── InputStream
└── OutputStream
服务端接受连接后:
Socket
├── InputStream
└── OutputStream
于是:
Client OutputStream
│
▼
TCP 网络
│
▼
Server InputStream
反方向:
Server OutputStream
│
▼
TCP 网络
│
▼
Client InputStream
因此 TCP Socket 天然支持:
双向通信。
三、使用方法
3.1 TCP 客户端核心 API
客户端:
Socket socket =
new Socket(
"127.0.0.1",
9999
);
连接成功后:
OutputStream outputStream =
socket.getOutputStream();
向服务端发送。
以及:
InputStream inputStream =
socket.getInputStream();
读取服务端发送的数据。
3.2 TCP 服务端核心 API
首先:
ServerSocket serverSocket =
new ServerSocket(9999);
然后:
Socket socket =
serverSocket.accept();
连接建立:
Client Socket
│
│ TCP
│
Server Socket
之后服务端也可以:
socket.getInputStream();
和:
socket.getOutputStream();
3.3 TCP 一发一收客户端
为了与原课程中的 IO 知识自然组合,本章使用:
DataOutputStream
发送:
消息类型
+
字符串消息
代码:
import java.io.DataOutputStream;
import java.net.Socket;
public class TCPClient {
public static void main(String[] args) throws Exception {
System.out.println("=== TCP 客户端启动 ===");
try (
Socket socket =
new Socket(
"127.0.0.1",
9999
);
DataOutputStream output =
new DataOutputStream(
socket.getOutputStream()
)
) {
output.writeInt(1);
output.writeUTF(
"你好,我是 TCP 客户端"
);
output.flush();
System.out.println("消息发送完成");
}
}
}
3.4 客户端发生了什么
第一步:
new Socket(
"127.0.0.1",
9999
);
尝试建立 TCP 连接。
如果连接成功:
Socket 创建成功
第二步:
socket.getOutputStream()
获得 Socket 字节输出流。
第三步:
new DataOutputStream(...)
进行包装。
第四步:
output.writeInt(1);
写入:
一个 int
第五步:
output.writeUTF(...)
写入:
一个 DataOutputStream 格式的 UTF 字符串
3.5 TCP 一发一收服务端
import java.io.DataInputStream;
import java.net.ServerSocket;
import java.net.Socket;
public class TCPServer {
public static void main(String[] args) throws Exception {
System.out.println("=== TCP 服务端启动 ===");
try (
ServerSocket serverSocket =
new ServerSocket(9999)
) {
System.out.println(
"等待客户端连接..."
);
try (
Socket socket =
serverSocket.accept();
DataInputStream input =
new DataInputStream(
socket.getInputStream()
)
) {
System.out.println(
"客户端连接成功:"
+ socket.getInetAddress()
.getHostAddress()
+ ":"
+ socket.getPort()
);
int type =
input.readInt();
String message =
input.readUTF();
System.out.println(
"type = " + type
);
System.out.println(
"message = " + message
);
}
}
}
}
3.6 服务端发生了什么
第一步:
ServerSocket serverSocket =
new ServerSocket(9999);
服务器监听:
TCP 9999
第二步:
Socket socket =
serverSocket.accept();
程序停在这里:
等待客户端
客户端运行:
new Socket(
"127.0.0.1",
9999
);
双方完成 TCP 连接建立。
于是:
accept()
返回。
3.7 accept 返回的 Socket 是谁?
假设客户端:
127.0.0.1:53180
连接服务端:
127.0.0.1:9999
服务端:
Socket socket =
serverSocket.accept();
这个 socket 可以理解为:
服务端这一侧
与 127.0.0.1:53180 客户端
对应的 TCP 通信 Socket
因此可以获取:
socket.getInetAddress()
得到:
远端客户端 IP
以及:
socket.getPort()
得到:
远端客户端端口
3.8 为什么客户端和服务端读取顺序必须一致
客户端写:
output.writeInt(1);
output.writeUTF("Hello");
那么服务端必须按相同协议读取:
input.readInt();
input.readUTF();
如果服务端错误写成:
input.readUTF();
input.readInt();
双方对于字节含义的理解不同:
客户端:
[ int ][ UTF ]
服务端:
[ UTF ][ int ]
就会出现:
数据解析错误
阻塞
异常
这已经是:
应用层协议设计
最简单的形式。
3.9 TCP 通信完整流程
TCP Server
new ServerSocket(9999)
│
▼
accept()
│
阻塞等待
│
│
│
TCP Client │
│
new Socket(host,port)
│ │
└────连接───┘
│
▼
accept 返回
│
▼
Server Socket
Client:
getOutputStream()
│
│ bytes
▼
════════ TCP ════════
│
▼
Server:
getInputStream()
四、原理与进阶
4.1 Socket 到底是什么
Socket 可以翻译为:
套接字
它可以理解为:
应用程序访问操作系统网络通信能力的一个抽象端点。
网络程序不是 Java 自己直接:
操控网卡
而是大致:
Java 程序
↓
Socket API
↓
操作系统网络协议栈
↓
TCP / IP
↓
网卡
因此 Socket 是应用程序与操作系统网络栈之间非常重要的接口抽象。
4.2 一条 TCP 连接如何区分
一条 TCP 连接通常可以由四个核心信息区分:
源 IP
源 Port
目标 IP
目标 Port
例如:
Client
192.168.1.10:53101
→
Server
192.168.1.100:9999
可以表示成:
192.168.1.10:53101
↓
192.168.1.100:9999
另一客户端:
192.168.1.11:53101
↓
192.168.1.100:9999
仍然是另一条连接。
这也解释了后面一个重要现象:
一个服务端的 9999 端口可以同时服务很多 TCP 客户端连接。
因为连接不仅仅由:
9999
一个数字决定。
4.3 ServerSocket 为什么不是“只能接一个客户端”
代码:
Socket socket =
serverSocket.accept();
这一章暂时只执行一次,因此只能接受一个连接。
但 ServerSocket 本身并不是:
只能服务一个客户端
如果以后写:
while (true) {
Socket socket =
serverSocket.accept();
}
就可以持续接受新连接。
问题在于:
接受连接以后,怎么同时处理多个 Socket?
这正是后续:
09-08 多客户端服务端与多线程通信
解决的问题。
4.4 TCP 没有应用层消息边界
非常重要。
例如客户端:
output.write(
"AAA".getBytes(...)
);
output.write(
"BBB".getBytes(...)
);
程序员认为发送:
消息 1:AAA
消息 2:BBB
但 TCP 只看到:
AAABBB
这样的连续字节流。
服务端某次:
input.read(buffer);
可能读到:
AAABBB
也可能:
AA
下一次:
ABBB
应用程序不能把:
一次 write
机械等价成:
对方一次 read
所以 TCP 程序需要设计:
消息长度
分隔符
固定长度
结构化协议
等消息边界。
4.5 那为什么 writeUTF / readUTF 可以读一条字符串?
因为:
DataOutputStream.writeUTF()
本身采用了一套确定的数据编码格式。
对应:
DataInputStream.readUTF()
能够按照相同规则读取。
所以:
writeUTF()
和:
readUTF()
解决的是:
在 TCP 字节流之上人为建立一个数据边界协议。
不是 TCP 自己突然变成:
面向消息协议
必须区分:
TCP
提供字节流
和:
DataInputStream / DataOutputStream
在字节流之上定义读取格式
4.6 accept() 和 receive() 的阻塞有什么不同
UDP:
DatagramSocket.receive()
等待:
数据报
TCP:
ServerSocket.accept()
等待:
新的 TCP 连接
TCP 连接建立以后:
input.read(...)
又可能等待:
连接中的数据
所以网络编程中会大量遇到:
阻塞 IO
4.7 close() 会发生什么
客户端:
socket.close();
意味着应用程序关闭 Socket 资源。
对于 TCP:
连接终止流程
会由操作系统 TCP 协议栈处理。
计算机网络课程常将正常 TCP 连接关闭概括为:
四次挥手
Java 程序员通常不需要手工发送这些 TCP 控制报文。
我们的职责主要是:
socket.close();
操作系统负责:
TCP 协议层连接关闭
五、实践应用
5.1 为什么聊天程序通常更适合 TCP
如果业务要求:
A 发送:
“今天晚上 8 点见”
通常希望:
完整收到
顺序正确
不希望中间丢几个字节
TCP 的可靠字节流模型很适合这类需求。
5.2 文件传输
文件:
1 MB
100 MB
10 GB
本质上都可以:
FileInputStream
↓
Socket OutputStream
↓
TCP
↓
Socket InputStream
↓
FileOutputStream
于是之前学习的:
File
IO
网络编程
真正串起来。
5.3 Web 开发的基础
浏览器访问网站时:
Browser
↓
TCP Connection
↓
HTTP
↓
Server
现代 HTTP 还涉及 TLS、HTTP/2、HTTP/3 等更复杂机制,但理解经典:
Socket
ServerSocket
TCP
InputStream
OutputStream
有助于理解后面的 Java Web 网络基础。
六、常见问题
6.1 Socket 和 ServerSocket 有什么区别?
最重要的记忆:
ServerSocket
=
监听 / 接受连接
Socket
=
真正的数据通信连接
6.2 为什么服务端需要两个对象?
服务端首先:
ServerSocket serverSocket =
new ServerSocket(9999);
负责监听。
然后:
Socket socket =
serverSocket.accept();
负责与某个具体客户端通信。
6.3 accept() 为什么程序像卡死一样?
因为:
accept()
默认会阻塞等待客户端连接。
如果没有客户端:
继续等待
这是正常现象。
6.4 TCP 发送数据为什么没有 DatagramPacket?
因为 TCP 是:
字节流协议
Java 通过:
InputStream
OutputStream
暴露连接中的字节流。
UDP 才主要使用:
DatagramPacket
6.5 Socket 的 getInputStream 是读取谁?
读取:
对方通过这条 TCP 连接发送过来的数据。
客户端:
客户端 InputStream
读取的是:
服务端发送的数据
服务端:
服务端 Socket InputStream
读取的是:
客户端发送的数据
6.6 getOutputStream 是写到本机文件吗?
不是。
它是:
Socket 的网络输出流
向:
TCP 连接另一端
写数据。
6.7 客户端 new Socket() 为什么可能失败?
例如:
new Socket(
"127.0.0.1",
9999
);
但:
服务端没启动
或者:
端口错误
网络不通
防火墙阻止
目标不可达
就可能连接失败并抛出网络相关异常。
这与 UDP:
send()
没有建立连接的模型不同。
6.8 TCP 可靠,为什么还会抛异常?
“可靠”不是:
物理网络永远不会断
而是:
TCP 协议在可工作的连接中
提供可靠有序字节流机制
如果:
网线断了
程序崩了
机器关机了
连接被重置
Java 仍然会收到异常或 EOF。
6.9 一次 write() 是否一定对应对方一次 read()?
不是。
这一点必须记住:
write()
≠
对方一定一次 read() 完整得到
因为:
TCP = 字节流
应用层需要自己定义消息边界。
6.10 为什么 DataOutputStream 和 DataInputStream 必须配套?
例如:
writeInt()
writeUTF()
接收端必须:
readInt()
readUTF()
因为双方必须遵守相同:
数据格式
+
数据顺序
否则就是:
通信协议不一致
七、练习与验收
7.1 知识问答
- TCP 的全称是什么?
- TCP 有哪些核心特点?
- UDP 与 TCP 最大的通信模型区别是什么?
- Java TCP 客户端核心类是什么?
- Java TCP 服务端监听核心类是什么?
ServerSocket与Socket的职责分别是什么?accept()为什么会阻塞?accept()返回的是什么?- TCP 建立连接以后如何发送和接收数据?
getInputStream()与getOutputStream()分别解决什么问题?- TCP 为什么叫字节流协议?
- 为什么一次
write()不等于对方一次read()? writeUTF()/readUTF()为什么能够形成消息边界?- 为什么“TCP 可靠”不代表业务绝不会失败?
7.2 代码阅读
阅读:
ServerSocket serverSocket =
new ServerSocket(9999);
Socket socket =
serverSocket.accept();
InputStream input =
socket.getInputStream();
回答:
serverSocket的职责是什么?9999是什么端口?- 没有客户端时
accept()会怎样? socket表示什么?input读取的数据来自哪里?
再阅读:
Socket socket =
new Socket(
"127.0.0.1",
9999
);
OutputStream output =
socket.getOutputStream();
回答:
new Socket(...)会尝试做什么?127.0.0.1是谁?9999是谁的端口?output.write(...)最终把字节发给谁?
7.3 手写代码
关闭 AI 自动补全。
TCP 客户端
从零实现:
TCPClient
要求:
连接 127.0.0.1:9999
发送:
type = 1001
message =
"Hello TCP"
要求使用:
DataOutputStream
TCP 服务端
从零实现:
TCPServer
要求:
- 监听 TCP 9999。
- 调用
accept()。 - 获取客户端 IP。
- 获取客户端端口。
- 使用
DataInputStream。 - 按顺序读取
int + UTF。 - 输出收到的信息。
7.4 Debug
客户端:
output.writeUTF("Hello");
output.writeInt(100);
服务端:
int id =
input.readInt();
String message =
input.readUTF();
判断:
- 双方协议是否一致?
- 为什么可能产生错误?
- 应如何修改?
再分析:
ServerSocket serverSocket =
new ServerSocket(9999);
InputStream input =
serverSocket.getInputStream();
有什么问题?
要求解释:
ServerSocket
为什么不能直接通过这种方式读取具体客户端业务数据。
7.5 综合训练
画出:
Server
ServerSocket
:9999
│
accept
│
▼
Socket
▲
│ TCP
│
Socket
Client
再补充:
Client OutputStream
↓
Server InputStream
Server OutputStream
↓
Client InputStream
并解释每个对象和流的职责。
7.6 本章验收
如果关闭资料后,可以从零写出:
new Socket(
host,
port
);
socket.getInputStream();
socket.getOutputStream();
new ServerSocket(
port
);
serverSocket.accept();
并能完整回答:
ServerSocket、服务端Socket、客户端Socket三个对象究竟分别是什么?
以及:
为什么 TCP 使用 IO 流而不是
DatagramPacket?
并且能够解释:
TCP
=
连接
+
可靠
+
有序
+
字节流
那么 TCP 基础已经建立。
最终知识模型:
TCP
┌────────────┴────────────┐
│ │
Client Server
│ │
Socket ServerSocket
│ │
│ accept()
│ │
│ Socket
│ │
└──────── TCP ────────────┘
│
Input / Output
│
IO Stream