我将从最基础的 TCP Socket 开始,因为它是最常用、最可靠的,然后会介绍 UDP Socket,并附上完整的代码示例和最佳实践。

核心概念
在开始编码前,先理解几个核心概念:
- Socket (套接字):网络通信的端点,可以把它想象成一个“插座”,你的程序通过这个插座来发送和接收数据。
- IP 地址:网络中设备的唯一标识,
168.1.100或www.google.com。 - 端口号:设备上应用程序的唯一标识,一个 IP 地址可以同时运行多个网络服务(如 Web 服务器、邮件服务器),端口号用来区分它们,范围是 0-65535,0-1023 是知名端口,通常被系统服务占用。
- TCP (传输控制协议):
- 面向连接:在发送数据前,必须先建立一个可靠的连接(三次握手)。
- 可靠传输:通过确认、重传、排序等机制确保数据无差错、不丢失、不重复地到达。
- 字节流:发送的数据像水流一样,没有明确的边界,接收方需要自己定义如何从流中分割出完整的数据包。
- 适用于:对可靠性要求高的场景,如文件传输、网页浏览、邮件发送。
- UDP (用户数据报协议):
- 无连接:发送数据前无需建立连接,直接发送数据包(Datagram)。
- 不可靠传输:不保证数据包的顺序、是否到达或是否重复。
- 数据报:数据以独立的包形式发送,每个包都有明确的边界。
- 适用于:对实时性要求高、能容忍少量丢包的场景,如视频会议、在线游戏、DNS 查询。
基于 TCP 的 Socket 编程(最常用)
TCP 通信是客户端/服务器 模型。
流程图
+--------+ 创建 ServerSocket +--------+
| Client | -------------------------> | Server |
+--------+ +--------+
| | (监听端口)
| 1. 连接请求 |
| -------------------------> | (accept() 阻塞,等待连接)
| |
| 2. 获取 Socket |
| <------------------------- |
| | 3. 获取输入/输出流
| 3. 获取输入/输出流 |
| <------------------------- |
| |
| 4. 通过 OutputStream 发送数据 |
| -------------------------> |
| | 5. 通过 InputStream 接收数据
| 5. 通过 InputStream 接收数据 |
| <------------------------- |
| |
... (数据交换) ...
| |
| 6. 关闭 Socket 和流 |
| <------------------------- |
| | 7. 关闭 Socket 和流
| |
+--------+ +--------+
| Client | <------------------------- | Server |
+--------+ +--------+
代码示例
服务器端
服务器需要先启动,并在一个指定的端口上等待客户端的连接。
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
public class TCPServer {
public static void main(String[] args) {
// 1. 创建一个 ServerSocket,并绑定到指定的端口 ( 8888)
// 服务器在这个端口上监听客户端的连接请求
try (ServerSocket serverSocket = new ServerSocket(8888)) {
System.out.println("服务器已启动,等待客户端连接...");
// 2. 调用 accept() 方法,它会阻塞当前线程,直到有客户端连接
// accept() 返回一个 Socket 对象,代表与客户端建立的连接
Socket clientSocket = serverSocket.accept();
System.out.println("客户端已连接: " + clientSocket.getInetAddress().getHostAddress());
// 3. 从 Socket 中获取输入流,用于接收客户端发送的数据
InputStream inputStream = clientSocket.getInputStream();
// 4. 从 Socket 中获取输出流,用于向客户端发送数据
OutputStream outputStream = clientSocket.getOutputStream();
// 5. 接收数据
byte[] buffer = new byte[1024];
int len;
// read() 也会阻塞,直到有数据可读
while ((len = inputStream.read(buffer)) != -1) {
// 将接收到的字节数据转换为字符串并打印
String message = new String(buffer, 0, len, "UTF-8");
System.out.println("收到客户端消息: " + message);
// 6. (可选) 向客户端发送响应
String responseMessage = "服务器已收到你的消息: " + message;
outputStream.write(responseMessage.getBytes("UTF-8"));
outputStream.flush(); // 确保数据被立即发送
}
System.out.println("客户端已断开连接。");
// 7. 关闭资源 (使用 try-with-resources 会自动关闭)
// inputStream.close();
// outputStream.close();
// clientSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
客户端
客户端需要知道服务器的 IP 地址和端口号,然后主动发起连接。

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
public class TCPClient {
public static void main(String[] args) {
// 服务器的 IP 地址和端口号
String serverHost = "localhost"; // 如果在同一台机器上,用 "localhost"
int serverPort = 8888;
try (Socket socket = new Socket(serverHost, serverPort)) {
System.out.println("已连接到服务器: " + socket.getRemoteSocketAddress());
// 1. 从 Socket 中获取输出流,用于向服务器发送数据
OutputStream outputStream = socket.getOutputStream();
// 2. 从 Socket 中获取输入流,用于接收服务器的响应
InputStream inputStream = socket.getInputStream();
// 3. 发送数据
String message = "你好,TCP 服务器!";
System.out.println("向服务器发送消息: " + message);
outputStream.write(message.getBytes("UTF-8"));
outputStream.flush(); // 确保数据被立即发送
// 4. 接收服务器的响应
byte[] buffer = new byte[1024];
int len;
while ((len = inputStream.read(buffer)) != -1) {
String response = new String(buffer, 0, len, "UTF-8");
System.out.println("收到服务器响应: " + response);
break; // 读取一次后退出
}
// 5. 关闭资源 (使用 try-with-resources 会自动关闭)
// outputStream.close();
// inputStream.close();
// socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
如何运行
- 先运行
TCPServer.java,你会看到控制台打印 "服务器已启动,等待客户端连接..."。 - 然后运行
TCPClient.java。 - 你会看到客户端成功连接,发送消息,并收到服务器的响应,服务器端也会打印出收到的消息。
基于 UDP 的 Socket 编程
UDP 是无连接的,通信双方地位对等,没有明确的客户端和服务器之分,但通常我们还是会有一方作为“接收方”,一方作为“发送方”。
流程图
+-----------+ +-----------+
| Sender | | Receiver |
+-----------+ +-----------+
| |
| 1. 创建 DatagramSocket | 1. 创建 DatagramSocket
| | 2. 创建 DatagramPacket (用于接收)
| 2. 创建 DatagramPacket (用于发送) | 3. 调用 receive() 阻塞等待
| 3. 调用 send() 发送数据 |
| -------------------------> |
| | 4. 从 DatagramPacket 中获取数据
| |
| | 5. (可选) 创建新的 DatagramPacket 并回复
| | 6. 调用 send() 回复
| <------------------------- |
| |
... (数据交换) ...
| |
代码示例
接收方
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
public class UDPReceiver {
public static void main(String[] args) {
int receivePort = 9999;
try (DatagramSocket socket = new DatagramSocket(receivePort)) {
System.out.println("UDP 接收方已启动,监听端口: " + receivePort);
byte[] buffer = new byte[1024];
// 创建一个 DatagramPacket 对象,用于接收数据
// 它需要指定接收数据的字节数组和长度
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
// receive() 方法会阻塞,直到收到一个数据包
socket.receive(packet);
// 从 packet 中获取实际接收到的数据长度
String receivedMessage = new String(packet.getData(), 0, packet.getLength(), "UTF-8");
System.out.println("收到消息: " + receivedMessage);
System.out.println("来自: " + packet.getAddress().getHostAddress() + ":" + packet.getPort());
} catch (SocketException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
发送方
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.UnknownHostException;
public class UDPSender {
public static void main(String[] args) {
String message = "你好,UDP 接收方!";
String receiverHost = "localhost";
int receiverPort = 9999;
try (
// 创建一个 DatagramSocket,系统会分配一个可用端口
DatagramSocket socket = new DatagramSocket()
) {
// 将要发送的消息转换为字节数组
byte[] data = message.getBytes("UTF-8");
// 接收方的 IP 地址
InetAddress address = InetAddress.getByName(receiverHost);
// 创建一个 DatagramPacket 对象,包含要发送的数据、长度、目标 IP 和端口
DatagramPacket packet = new DatagramPacket(data, data.length, address, receiverPort);
// 通过 send() 方法发送数据包
socket.send(packet);
System.out.println("消息已发送至 " + receiverHost + ":" + receiverPort);
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
如何运行
- 先运行
UDPReceiver.java,它会开始监听 9999 端口。 - 然后运行
UDPSender.java。 - 接收方会收到消息并打印出来。
重要注意事项与最佳实践
字符编码
在网络传输中,字符串必须被转换为字节数组。强烈建议始终指定字符编码,如 UTF-8,以避免在不同系统上出现乱码问题。
// 正确
String msg = "你好";
byte[] bytes = msg.getBytes("UTF-8");
String msg2 = new String(bytes, "UTF-8");
// 错误 (使用平台默认编码,可能导致跨平台问题)
byte[] bytes = msg.getBytes();
String msg2 = new String(bytes);
资源管理
Socket 和相关的输入/输出流都是需要系统资源的,使用完毕后必须关闭,推荐使用 try-with-resources 语句,它可以自动关闭实现了 AutoCloseable 接口的对象。
// 推荐写法
try (Socket socket = new Socket(...);
OutputStream os = socket.getOutputStream();
InputStream is = socket.getInputStream()) {
// ... 使用 socket, os, is 进行操作 ...
} catch (IOException e) {
// 处理异常
} // try-with-resources 会自动调用 close() 方法
TCP 的流式数据问题
TCP 是字节流,没有消息边界,如果客户端连续发送两条消息,服务器端可能会在一次 read() 中读到两条消息粘在一起,或者一条消息被分成多次 read()。
解决方案:自定义协议
在发送的实际数据前,加上数据的长度。
[数据长度(4字节)][实际数据]
客户端发送时:
String msg1 = "Hello";
String msg2 = "World";
// 发送第一条消息
byte[] data1 = msg1.getBytes("UTF-8");
byte[] lenBytes1 = ByteBuffer.allocate(4).putInt(data1.length).array();
outputStream.write(lenBytes1);
outputStream.write(data1);
// 发送第二条消息
byte[] data2 = msg2.getBytes("UTF-8");
byte[] lenBytes2 = ByteBuffer.allocate(4).putInt(data2.length).array();
outputStream.write(lenBytes2);
outputStream.write(data2);
服务器接收时:
byte[] lenBytes = new byte[4];
inputStream.read(lenBytes); // 先读长度
int length = ByteBuffer.wrap(lenBytes).getInt();
byte[] dataBytes = new byte[length];
int totalRead = 0;
while (totalRead < length) {
int read = inputStream.read(dataBytes, totalRead, length - totalRead);
totalRead += read;
}
String message = new String(dataBytes, "UTF-8");
处理阻塞
SocketInputStream.read() 和 ServerSocket.accept() 等方法都是阻塞的,在实际应用中,通常会用多线程(一个连接一个线程)或更高级的 I/O 模型(如 NIO,即 java.nio.channels 包)来处理并发连接,以避免主线程被阻塞。
异常处理
网络编程充满了不确定性,如连接中断、网络延迟、目标主机不可达等,所有网络操作都应该被 try-catch 块包围,并妥善处理 IOException 及其子类。
| 特性 | TCP (Socket/ServerSocket) | UDP (DatagramSocket/DatagramPacket) |
|---|---|---|
| 连接性 | 面向连接 | 无连接 |
| 可靠性 | 高(保证顺序、不丢失) | 低(不保证顺序、可能丢失) |
| 速度 | 较慢 | 较快 |
| 模型 | 客户端/服务器 | 对等(但通常有接收/发送方之分) |
| 数据形式 | 字节流 | 数据报 |
| 适用场景 | 文件传输、网页浏览、邮件 | 视频会议、在线游戏、DNS |
| Java API | Socket, ServerSocket, InputStream, OutputStream |
DatagramSocket, DatagramPacket |
希望这份详细的指南能帮助你掌握 Java Socket 编程!从 TCP 开始,理解其阻塞特性和流式数据的处理是关键。
