Socket 与网络边界
学习目标
- 能用
ServerSocket和Socket建立最小 TCP 客户端/服务端。 - 能区分 TCP 字节流、应用层消息、UDP 数据报和 HTTP 客户端。
- 能处理超时、半关闭、资源关闭、部分读写和本地/远程地址边界。
核心知识点
专业术语
- ServerSocket:监听 TCP 端口并接受连接的服务端入口。
- Socket:一条 TCP 连接,两端通过 InputStream/OutputStream 交换字节。
- DatagramSocket/DatagramPacket:UDP 数据报 API,发送和接收保留报文边界。
- 半包/粘包:一次读取少于或多于一条应用消息,来自 TCP 没有消息边界。
白话解释与边界
Socket 只解决“连接和字节传输”,不会定义一条消息什么时候结束。应用协议需要长度前缀、换行分隔或固定长度,并循环读取到完整内容。HTTP 请求优先交给 HttpClient,长连接、自定义协议或学习 TCP 时才直接使用 Socket。所有 Socket、输入流和输出流都要关闭,超时要按连接、读取和业务分别考虑。
常用用法
ServerSocket:监听端口
传入 0 让操作系统分配临时端口,适合测试;生产服务要明确绑定地址、端口占用、backlog 和防火墙边界。
//说明: 说明:ServerSocket:监听端口。
import java.net.ServerSocket;
public class ServerSocketBindDemo {
public static void main(String[] args) throws Exception {
try (ServerSocket server = new ServerSocket(0)) {
// 输入:try (ServerSocket server = new ServerSocket(0)) {;资源变量 server 接收 new ServerSocket(0),try 结束时自动关闭。
// 关键变化:try (ServerSocket server = new ServerSocket(0)) {;创建资源 server,构造表达式为 new ServerSocket(0);try 结束时关闭该资源。
System.out.println(server.getLocalPort() > 0);
// 输出:true
}
}
}接收 TCP 连接:从监听套接字得到通信端点
accept 会阻塞直到有连接;真实服务通常为每个连接提交任务或使用 NIO,且要限制连接数、空闲时间和输入大小。
// 说明:接收 TCP 连接:从监听套接字得到通信端点。
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.Executors;
public class SocketAcceptDemo {
public static void main(String[] args) throws Exception {
try (ServerSocket server = new ServerSocket(0);
// 输入:try (ServerSocket server = new ServerSocket(0);;资源变量 server 接收 new ServerSocket(0,try 结束时自动关闭。
// 关键变化:try (ServerSocket server = new ServerSocket(0);;创建资源 server,构造表达式为 new ServerSocket(0;try 结束时关闭该资源。
var executor = Executors.newSingleThreadExecutor()) {
executor.submit(() -> {
// 关键变化:executor.submit(() -> {;向 executor 提交 () ->,返回 Future;任务进入执行队列。
try (Socket socket = server.accept()) {
// 输入:try (Socket socket = server.accept()) {;资源变量 socket 接收 server.accept(),try 结束时自动关闭。
// 关键变化:try (Socket socket = server.accept()) {;创建资源 socket,构造表达式为 server.accept();try 结束时关闭该资源。
System.out.println(socket.getInetAddress().isLoopbackAddress());
// 输出:true
} catch (Exception e) {
throw new RuntimeException(e);
// 关键变化:throw new RuntimeException(e);;创建并抛出异常实例,异常控制流向上层传播。
}
});
try (Socket client = new Socket("127.0.0.1", server.getLocalPort())) {
// 输入:try (Socket client = new Socket("127.0.0.1", server.getLocalPort())) {;资源变量 client 接收 new Socket("127.0.0.1", server.getLocalPort()),try 结束时自动关闭。
// 关键变化:try (Socket client = new Socket("127.0.0.1", server.getLocalPort())) {;创建资源 client,构造表达式为 new Socket("127.0.0.1", server.getLocalPort());try 结束时关闭该资源。
client.getOutputStream().close();
// 关键变化:client.getOutputStream().close();;关闭 上一个链式结果,释放底层资源;后续读写进入关闭状态。
}
}
}
}交换 TCP 字节:取得双向流端点
示例用换行定义消息边界;真实二进制协议通常使用长度前缀,不能把 readLine 当作通用 TCP 解包器。
// 说明:交换 TCP 字节:取得双向流端点。
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
public class SocketTextDemo {
public static void main(String[] args) throws Exception {
try (ServerSocket server = new ServerSocket(0)) {
// 输入:try (ServerSocket server = new ServerSocket(0)) {;资源变量 server 接收 new ServerSocket(0),try 结束时自动关闭。
// 关键变化:try (ServerSocket server = new ServerSocket(0)) {;创建资源 server,构造表达式为 new ServerSocket(0);try 结束时关闭该资源。
Thread service = new Thread(() -> {
try (Socket socket = server.accept();
// 输入:server.accept();阻塞等待客户端连接,返回 Socket 给资源变量 socket。
// 关键变化:连接成功后 socket 由 try-with-resources 托管,并在处理完客户端后自动关闭。
var reader = new BufferedReader(new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8));
var writer = new PrintWriter(socket.getOutputStream(), true, StandardCharsets.UTF_8)) {
writer.println(reader.readLine().toUpperCase());
// 关键变化:writer.println(reader.readLine().toUpperCase());;向 writer 写入 reader.readLine().toUpperCase() 并换行;输出流新增一行。
} catch (Exception e) {
throw new RuntimeException(e);
// 关键变化:throw new RuntimeException(e);;创建并抛出异常实例,异常控制流向上层传播。
}
}, "socket-text-service");
service.start();
// 关键变化:service.start();;启动 service,线程或服务进入运行状态。
try (Socket client = new Socket("127.0.0.1", server.getLocalPort());
// 输入:try (Socket client = new Socket("127.0.0.1", server.getLocalPort());;资源变量 client 接收 new Socket("127.0.0.1", server.getLocalPort(),try 结束时自动关闭。
// 关键变化:try (Socket client = new Socket("127.0.0.1", server.getLocalPort());;创建资源 client,构造表达式为 new Socket("127.0.0.1", server.getLocalPort();try 结束时关闭该资源。
var reader = new BufferedReader(new InputStreamReader(client.getInputStream(), StandardCharsets.UTF_8));
var writer = new PrintWriter(client.getOutputStream(), true, StandardCharsets.UTF_8)) {
writer.println("java");
// 关键变化:writer.println("java");;向 writer 写入 "java" 并换行;输出流新增一行。
System.out.println(reader.readLine());
// 输出:JAVA
}
service.join();
// 关键变化:service.join();;等待 service 执行结束,当前线程进入等待状态。
}
}
}setSoTimeout:限制读取阻塞
SoTimeout 限制一次阻塞读取,不会自动关闭 Socket,也不等于连接超时;捕获后要决定重试、断开还是继续读取。
// 说明:setSoTimeout:限制读取阻塞。
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketTimeoutException;
public class SocketTimeoutDemo {
public static void main(String[] args) throws Exception {
try (ServerSocket server = new ServerSocket(0)) {
// 输入:try (ServerSocket server = new ServerSocket(0)) {;资源变量 server 接收 new ServerSocket(0),try 结束时自动关闭。
// 关键变化:try (ServerSocket server = new ServerSocket(0)) {;创建资源 server,构造表达式为 new ServerSocket(0);try 结束时关闭该资源。
new Thread(() -> {
try (Socket ignored = server.accept()) { }
catch (Exception ignored) { }
}, "socket-timeout-service").start();
// 关键变化:}, "socket-timeout-service").start();;启动 上一个链式结果,线程或服务进入运行状态。
try (Socket socket = new Socket("127.0.0.1", server.getLocalPort())) {
// 输入:try (Socket socket = new Socket("127.0.0.1", server.getLocalPort())) {;资源变量 socket 接收 new Socket("127.0.0.1", server.getLocalPort()),try 结束时自动关闭。
// 关键变化:try (Socket socket = new Socket("127.0.0.1", server.getLocalPort())) {;创建资源 socket,构造表达式为 new Socket("127.0.0.1", server.getLocalPort());try 结束时关闭该资源。
socket.setSoTimeout(50);
// 关键变化:socket.setSoTimeout(50);;把 socket 的 setSoTimeout 选项设置为 50,连接配置更新。
try {
socket.getInputStream().read();
// 关键变化:socket.getInputStream().read();;从 socket.getInputStream() 读取一个字节,返回 -1 表示 EOF;读取位置推进。
} catch (SocketTimeoutException e) {
System.out.println("timeout");
// 输出:timeout
}
}
}
}
}InetSocketAddress:明确主机与端口
地址对象可以用于绑定和连接;主机名解析可能阻塞或返回多个地址,生产代码要考虑 DNS 超时、IPv4/IPv6 和 SSRF 校验。
// 说明:InetSocketAddress:明确主机与端口。
import java.net.InetSocketAddress;
public class SocketAddressDemo {
public static void main(String[] args) {
var address = new InetSocketAddress("127.0.0.1", 8080);
System.out.println(address.getHostString() + ":" + address.getPort());
// 输出:127.0.0.1:8080
}
}交换 UDP 数据报:发送并接收报文
UDP 一次 receive 对应一个数据报,但过大的数据可能被截断;应用仍需处理丢失、重复、乱序和伪造来源。
// 说明:交换 UDP 数据报:发送并接收报文。
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.nio.charset.StandardCharsets;
public class UdpLoopbackDemo {
public static void main(String[] args) throws Exception {
try (DatagramSocket receiver = new DatagramSocket(0);
// 输入:try (DatagramSocket receiver = new DatagramSocket(0);;资源变量 receiver 接收 new DatagramSocket(0,try 结束时自动关闭。
// 关键变化:try (DatagramSocket receiver = new DatagramSocket(0);;创建资源 receiver,构造表达式为 new DatagramSocket(0;try 结束时关闭该资源。
DatagramSocket sender = new DatagramSocket()) {
byte[] bytes = "ping".getBytes(StandardCharsets.UTF_8);
sender.send(new DatagramPacket(bytes, bytes.length,
InetAddress.getLoopbackAddress(), receiver.getLocalPort()));
// 关键变化:sender.send(new DatagramPacket(bytes, bytes.length, InetAddress.getLoopbackAddress(), receiver.getLocalPort()));;让 sender 向本机 receiver 端口发送 bytes 数据报,send 返回 void。
// 关键变化:InetAddress.getLoopbackAddress() 不接收显式参数,返回本机回环地址,再与 receiver.getLocalPort() 组成目标地址。
byte[] received = new byte[16];
DatagramPacket packet = new DatagramPacket(received, received.length);
receiver.receive(packet);
// 关键变化:receiver.receive(packet);;阻塞接收数据报并把内容写入 packet,receive 返回 void;packet 的 length 更新。
System.out.println(new String(packet.getData(), packet.getOffset(), packet.getLength(), StandardCharsets.UTF_8));
// 输出:ping
}
}
}shutdownOutput:TCP 半关闭
客户端关闭输出后,服务端读到 EOF,但连接的另一方向仍可能可用;只有协议定义了结束方向时才使用半关闭。
// 说明:shutdownOutput:TCP 半关闭。
import java.net.ServerSocket;
import java.net.Socket;
public class SocketHalfCloseDemo {
public static void main(String[] args) throws Exception {
try (ServerSocket server = new ServerSocket(0)) {
// 输入:try (ServerSocket server = new ServerSocket(0)) {;资源变量 server 接收 new ServerSocket(0),try 结束时自动关闭。
// 关键变化:try (ServerSocket server = new ServerSocket(0)) {;创建资源 server,构造表达式为 new ServerSocket(0);try 结束时关闭该资源。
new Thread(() -> {
try (Socket socket = server.accept()) {
// 输入:try (Socket socket = server.accept()) {;资源变量 socket 接收 server.accept(),try 结束时自动关闭。
// 关键变化:try (Socket socket = server.accept()) {;创建资源 socket,构造表达式为 server.accept();try 结束时关闭该资源。
System.out.println(socket.getInputStream().read() == -1);
// 输出:true
} catch (Exception e) {
throw new RuntimeException(e);
// 关键变化:throw new RuntimeException(e);;创建并抛出异常实例,异常控制流向上层传播。
}
}, "socket-half-close-service").start();
// 关键变化:}, "socket-half-close-service").start();;启动 上一个链式结果,线程或服务进入运行状态。
try (Socket client = new Socket("127.0.0.1", server.getLocalPort())) {
// 输入:try (Socket client = new Socket("127.0.0.1", server.getLocalPort())) {;资源变量 client 接收 new Socket("127.0.0.1", server.getLocalPort()),try 结束时自动关闭。
// 关键变化:try (Socket client = new Socket("127.0.0.1", server.getLocalPort())) {;创建资源 client,构造表达式为 new Socket("127.0.0.1", server.getLocalPort());try 结束时关闭该资源。
client.shutdownOutput();
// 关键变化:client.shutdownOutput();;调用 shutdownOutput 关闭 client 的对应方向;连接半关闭状态更新。
}
}
}
}不常用但需要知道
setReuseAddress:端口重用选项
选项要在 bind 前设置才更有机会生效;端口重用不是绕过端口冲突的万能开关,平台语义也可能不同。
import java.net.ServerSocket;
public class SocketOptionDemo {
public static void main(String[] args) throws Exception {
try (ServerSocket server = new ServerSocket()) {
// 输入:try (ServerSocket server = new ServerSocket()) {;资源变量 server 接收 new ServerSocket(),try 结束时自动关闭。
// 关键变化:try (ServerSocket server = new ServerSocket()) {;创建资源 server,构造表达式为 new ServerSocket();try 结束时关闭该资源。
server.setReuseAddress(true);
// 关键变化:server.setReuseAddress(true);;把 server 的 setReuseAddress 选项设置为 true,连接配置更新。
server.bind(new java.net.InetSocketAddress("127.0.0.1", 0));
// 关键变化:server.bind(new java.net.InetSocketAddress("127.0.0.1", 0));;把 server 绑定到回环地址的临时端口 0;系统分配实际监听端口。
System.out.println(server.getReuseAddress());
// 输出:true
}
}
}setTcpNoDelay:禁用 Nagle 合并
低延迟小消息可能需要 TCP_NODELAY,但它会增加包数量和网络开销;必须用真实延迟和吞吐数据验证。
import java.net.Socket;
public class TcpNoDelayDemo {
public static void main(String[] args) throws Exception {
try (Socket socket = new Socket()) {
// 输入:try (Socket socket = new Socket()) {;资源变量 socket 接收 new Socket(),try 结束时自动关闭。
// 关键变化:try (Socket socket = new Socket()) {;创建资源 socket,构造表达式为 new Socket();try 结束时关闭该资源。
socket.setTcpNoDelay(true);
// 关键变化:socket.setTcpNoDelay(true);;把 socket 的 setTcpNoDelay 选项设置为 true,连接配置更新。
System.out.println(socket.getTcpNoDelay());
// 输出:true
}
}
}setKeepAlive:内核级保活
Keep-alive 不能替代应用心跳、请求超时和连接池空闲淘汰;内核探测周期也通常不是业务可控的。
import java.net.Socket;
public class TcpKeepAliveDemo {
public static void main(String[] args) throws Exception {
try (Socket socket = new Socket()) {
// 输入:try (Socket socket = new Socket()) {;资源变量 socket 接收 new Socket(),try 结束时自动关闭。
// 关键变化:try (Socket socket = new Socket()) {;创建资源 socket,构造表达式为 new Socket();try 结束时关闭该资源。
socket.setKeepAlive(true);
// 关键变化:socket.setKeepAlive(true);;把 socket 的 setKeepAlive 选项设置为 true,连接配置更新。
System.out.println(socket.getKeepAlive());
// 输出:true
}
}
}ServerSocket backlog:连接排队边界
backlog 是内核等待队列的建议值,不等于应用能同时处理的连接数;服务端仍需线程池、连接上限和过载策略。
// 说明:0 让内核分配本地端口,32 是尚未 accept 连接的排队建议值,不会创建 32 个处理线程。
import java.net.ServerSocket;
public class BacklogDemo {
public static void main(String[] args) throws Exception {
try (ServerSocket server = new ServerSocket(0, 32)) {
// 输入:try (ServerSocket server = new ServerSocket(0, 32)) {;资源变量 server 接收 new ServerSocket(0, 32),try 结束时自动关闭。
// 关键变化:try (ServerSocket server = new ServerSocket(0, 32)) {;创建资源 server,构造表达式为 new ServerSocket(0, 32);try 结束时关闭该资源。
System.out.println(server.getLocalPort() > 0);
// 输出:true
}
}
}SocketChannel:从阻塞 Socket 迁移到 NIO
非阻塞 Channel 必须配合 Selector 或连接状态机处理 finishConnect、部分读写和 OP_* 事件;不能只把 blocking 改成 false 就得到高性能服务。
import java.nio.channels.SocketChannel;
public class SocketChannelDemo {
public static void main(String[] args) throws Exception {
try (SocketChannel channel = SocketChannel.open()) {
// 输入:try (SocketChannel channel = SocketChannel.open()) {;资源变量 channel 接收 SocketChannel.open(),try 结束时自动关闭。
// 关键变化:try (SocketChannel channel = SocketChannel.open()) {;创建资源 channel,构造表达式为 SocketChannel.open();try 结束时关闭该资源。
channel.configureBlocking(false);
// 关键变化:channel.configureBlocking(false);;把 channel 的阻塞模式设置为 false,通道状态更新。
System.out.println(channel.isBlocking());
// 输出:false
}
}
}表达网络地址:区分结构化标识与旧式访问入口
URI 适合解析和构造地址,URL 搭配 URLConnection 是旧式访问入口;标准 HTTP 请求优先使用 HttpClient。
import java.net.URI;
import java.net.URL;
public class UriUrlDemo {
public static void main(String[] args) throws Exception {
URI uri = URI.create("https://example.com");
// 关键变化:URI uri = URI.create("https://example.com");;按 "https://example.com" 调用 create 创建值,结果写入 uri。
URL url = uri.toURL();
System.out.println(uri.getHost() + " / " + url.getProtocol());
// 输出:example.com / https
}
}简单案例
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
public class SocketDemo {
public static void main(String[] args) throws Exception {
try (ServerSocket server = new ServerSocket(0)) {
// 输入:try (ServerSocket server = new ServerSocket(0)) {;资源变量 server 接收 new ServerSocket(0),try 结束时自动关闭。
// 关键变化:try (ServerSocket server = new ServerSocket(0)) {;创建资源 server,构造表达式为 new ServerSocket(0);try 结束时关闭该资源。
Thread service = new Thread(() -> {
try (Socket socket = server.accept();
// 输入:server.accept();阻塞等待客户端连接,返回 Socket 给资源变量 socket。
// 关键变化:连接成功后 socket 由 try-with-resources 托管,并在处理完客户端后自动关闭。
var in = new BufferedReader(new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8));
var out = new PrintWriter(socket.getOutputStream(), true, StandardCharsets.UTF_8)) {
out.println("echo:" + in.readLine());
// 关键变化:out.println("echo:" + in.readLine());;向 out 写入 "echo:" + in.readLine() 并换行;输出流新增一行。
} catch (Exception e) {
throw new RuntimeException(e);
// 关键变化:throw new RuntimeException(e);;创建并抛出异常实例,异常控制流向上层传播。
}
}, "socket-echo-service");
service.start();
// 关键变化:service.start();;启动 service,线程或服务进入运行状态。
try (Socket client = new Socket("127.0.0.1", server.getLocalPort());
// 输入:try (Socket client = new Socket("127.0.0.1", server.getLocalPort());;资源变量 client 接收 new Socket("127.0.0.1", server.getLocalPort(),try 结束时自动关闭。
// 关键变化:try (Socket client = new Socket("127.0.0.1", server.getLocalPort());;创建资源 client,构造表达式为 new Socket("127.0.0.1", server.getLocalPort();try 结束时关闭该资源。
var in = new BufferedReader(new InputStreamReader(client.getInputStream(), StandardCharsets.UTF_8));
var out = new PrintWriter(client.getOutputStream(), true, StandardCharsets.UTF_8)) {
out.println("java");
// 关键变化:out.println("java");;向 out 写入 "java" 并换行;输出流新增一行。
System.out.println(in.readLine());
// 输出:echo:java
}
service.join();
// 关键变化:service.join();;等待 service 执行结束,当前线程进入等待状态。
}
}
}这里以换行作为应用消息边界并使用 loopback,避免示例依赖外部服务器;生产协议必须定义长度、编码、认证和超时。
易混点
- TCP 的
read不保证返回一条消息,UDP 的receive才保留数据报边界。 setSoTimeout限制读取阻塞,connect超时限制建立连接,两者不等价。shutdownOutput只关闭一个方向,close才是完整释放 Socket。TCP_NODELAY、KeepAlive、backlog 是低层选项,不是应用层重试、心跳和限流的替代品。
课后小问
为什么 TCP 服务端要设计消息长度或分隔符? 答案:TCP 只提供有序字节流,不知道业务消息从哪里开始和结束。 解析:可以用换行、固定长度、长度前缀或更高层协议定义边界,并循环读取完整消息。
UDP 为什么不能简单替换 TCP 来“提升性能”? 答案:UDP 放弃了可靠、有序、拥塞控制等能力,应用必须自己承担丢包和重传。 解析:只有业务能容忍或自行处理这些风险时,才选择 UDP;实时性不是唯一指标。
本节小结
- ServerSocket 监听,Socket 连接,流读写;三者都要做资源和超时管理。
- TCP 是可靠字节流,应用层必须补上消息边界。
- UDP 保留数据报边界但不保证可靠送达。
- 低层 Socket 选项必须结合协议和测量使用,HTTP 场景优先 HttpClient。
快速回顾
accept阻塞等待连接,setSoTimeout控制读取等待。- TCP 文本示例可以用换行分隔,二进制协议通常用长度前缀。
- 半关闭允许单向结束,完全关闭要
close。 - 网络输入永远要限制大小、时间和并发连接数。
阿源的知识库