核心概念:Socket 是什么?
想象一下打电话的过程:

- 拨号:你拨对方的电话号码,建立连接。
- 通话:双方通过听筒和麦克风(输入/输出流)进行语音交流。
- 挂断:通话结束,双方挂断电话,释放资源。
Java Socket 的过程与此非常相似:
- Socket (套接字):代表了网络连接中的一个端点,每个 Socket 都有一个 IP 地址和一个端口号,就像一个电话号码,通过
Socket对象,你可以发起连接或接受连接。 - IP 地址:网络中设备的唯一标识,
168.1.100。 - 端口号:应用程序在设备上的唯一标识,范围是 0-65535,Web 服务通常使用 80 端口,HTTPS 使用 443 端口。
- 输入流 / 输出流:一旦连接建立,Socket 就会提供
InputStream和OutputStream,你可以把它们想象成电话的听筒和麦克风,用来在网络上读写数据。
Java 中有两个主要的 Socket 类:
java.net.Socket:客户端使用的类,用于发起连接。java.net.ServerSocket:服务器端使用的类,用于监听并接受客户端的连接。
数据传输的关键:字节流
Socket 传输的是原始的字节,而不是字符串或对象,这意味着:
- 你需要将 Java 的
String、int、Object等类型转换为字节数组(序列化)。 - 接收方需要将收到的字节数组再转换回原来的类型(反序列化)。
常用的转换方式有:

- 字符串:使用
String.getBytes()和new String(byte[])。 - 基本数据类型:可以使用
DataInputStream和DataOutputStream,它们提供了方便的方法如writeInt(),readInt(),writeUTF(),readUTF()。 - 对象:使用
ObjectInputStream和ObjectOutputStream,要求对象必须实现Serializable接口。
完整代码示例:一个简单的 Echo 服务器
下面我们创建一个经典的 "Echo"(回声)服务:客户端发送一条消息,服务器原样返回这条消息。
1 服务器端代码
服务器需要一直运行,等待客户端的连接。
EchoServer.java
import java.io.*;
import java.net.*;
public class EchoServer {
public static void main(String[] args) {
// 定义服务器监听的端口号
int port = 12345;
try ( // 使用 try-with-resources 自动关闭资源
// 创建一个 ServerSocket,在指定端口上监听客户端连接
ServerSocket serverSocket = new ServerSocket(port);
// 等待客户端连接,accept() 是一个阻塞方法,直到有客户端连接才会返回
Socket clientSocket = serverSocket.accept();
// 获取客户端的输入流,用于读取客户端发送的数据
BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
// 获取客户端的输出流,用于向客户端发送数据
PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true)
) {
System.out.println("客户端已连接: " + clientSocket.getInetAddress().getHostAddress());
String inputLine;
// 循环读取客户端发送的数据
// readLine() 也是一个阻塞方法,直到客户端发送一行数据或关闭连接
while ((inputLine = in.readLine()) != null) {
System.out.println("收到客户端消息: " + inputLine);
// 将收到的消息回写给客户端
out.println("服务器回声: " + inputLine);
}
System.out.println("客户端已断开连接。");
} catch (IOException e) {
System.err.println("服务器异常: " + e.getMessage());
e.printStackTrace();
}
}
}
2 客户端代码
客户端主动连接服务器,发送消息并接收响应。

EchoClient.java
import java.io.*;
import java.net.*;
public class EchoClient {
public static void main(String[] args) {
// 服务器的 IP 地址和端口号
String serverAddress = "127.0.0.1"; // "localhost" 代表本机
int port = 12345;
try ( // 使用 try-with-resources 自动关闭资源
// 创建一个 Socket 对象,连接到指定的服务器地址和端口
Socket socket = new Socket(serverAddress, port);
// 获取服务器的输入流,用于读取服务器返回的数据
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
// 获取服务器的输出流,用于向服务器发送数据
PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
// 从控制台读取用户输入
BufferedReader stdIn = new BufferedReader(new InputStreamReader(System.in))
) {
System.out.println("已连接到服务器 " + serverAddress + ":" + port);
System.out.println("请输入要发送的消息 (输入 'exit' 退出):");
String userInput;
// 循环读取用户输入
while ((userInput = stdIn.readLine()) != null) {
if ("exit".equalsIgnoreCase(userInput)) {
break; // 用户输入 exit,则退出循环
}
// 将用户输入的消息发送给服务器
out.println(userInput);
// 读取服务器返回的回声
String response = in.readLine();
System.out.println("服务器响应: " + response);
}
} catch (UnknownHostException e) {
System.err.println("无法找到主机: " + serverAddress);
e.printStackTrace();
} catch (IOException e) {
System.err.println("I/O Error: " + e.getMessage());
e.printStackTrace();
}
System.out.println("客户端已关闭。");
}
}
3 如何运行
- 编译:将两个
.java文件放在同一个目录下,编译它们。javac EchoServer.java EchoClient.java
- 运行服务器:先运行服务器端程序,它会启动并等待连接。
java EchoServer
你会看到控制台输出:
服务器已启动,等待连接在端口 12345... - 运行客户端:在另一个新的终端窗口中,运行客户端程序。
java EchoClient
- 交互:
- 客户端控制台会提示你输入消息。
- 服务器端控制台会显示
客户端已连接...和收到客户端消息: ...。 - 客户端输入
你好,按回车。 - 客户端会立即收到服务器的响应:
服务器响应: 服务器回声: 你好。 - 客户端输入
exit并按回车,客户端程序会退出。 - 服务器端会显示
客户端已断开连接。,并继续等待新的客户端连接。
更复杂的传输:发送和接收对象
上面的例子只能传输字符串,在实际开发中,我们更希望传输自定义的对象。
1 定义可序列化的对象
对象必须实现 java.io.Serializable 接口,它是一个标记接口,不需要实现任何方法。
User.java
import java.io.Serializable;
// 必须实现 Serializable 接口
public class User implements Serializable {
private String username;
private int age;
// 构造函数
public User(String username, int age) {
this.username = username;
this.age = age;
}
// Getters
public String getUsername() { return username; }
public int getAge() { return age; }
@Override
public String toString() {
return "User{username='" + username + "', age=" + age + "}";
}
}
2 修改代码以传输对象
ObjectEchoServer.java
import java.io.*;
import java.net.*;
public class ObjectEchoServer {
public static void main(String[] args) {
int port = 12346;
try (ServerSocket serverSocket = new ServerSocket(port);
Socket clientSocket = serverSocket.accept();
// 使用 ObjectInputStream 读取对象
ObjectInputStream in = new ObjectInputStream(clientSocket.getInputStream());
// 使用 ObjectOutputStream 发送对象
ObjectOutputStream out = new ObjectOutputStream(clientSocket.getOutputStream())
) {
System.out.println("客户端已连接,等待接收 User 对象...");
// 读取客户端发送的 User 对象
User receivedUser = (User) in.readObject();
System.out.println("收到用户对象: " + receivedUser);
// 修改对象并发回
receivedUser.setAge(receivedUser.getAge() + 1);
System.out.println("修改后并发回对象: " + receivedUser);
out.writeObject(receivedUser);
} catch (IOException e) {
System.err.println("服务器 I/O 异常: " + e.getMessage());
} catch (ClassNotFoundException e) {
System.err.println("服务器: 无法找到类 " + e.getMessage());
}
}
}
ObjectEchoClient.java
import java.io.*;
import java.net.*;
public class ObjectEchoClient {
public static void main(String[] args) {
String serverAddress = "127.0.0.1";
int port = 12346;
try (Socket socket = new Socket(serverAddress, port);
// 使用 ObjectOutputStream 发送对象
ObjectOutputStream out = new ObjectOutputStream(socket.getOutputStream());
// 使用 ObjectInputStream 读取对象
ObjectInputStream in = new ObjectInputStream(socket.getInputStream())
) {
// 创建一个 User 对象并发送给服务器
User userToSend = new User("张三", 30);
System.out.println("发送用户对象: " + userToSend);
out.writeObject(userToSend);
// 读取服务器返回的修改后的 User 对象
User modifiedUser = (User) in.readObject();
System.out.println("收到服务器返回的对象: " + modifiedUser);
} catch (IOException e) {
System.err.println("客户端 I/O 异常: " + e.getMessage());
} catch (ClassNotFoundException e) {
System.err.println("客户端: 无法找到类 " + e.getMessage());
}
}
}
重要注意事项和最佳实践
-
阻塞与非阻塞:
ServerSocket.accept()、InputStream.read()等方法都是阻塞的,线程会一直等待,直到有事件发生(如连接到来、数据到达),对于简单的服务,这没问题,对于高并发的服务,需要使用多线程、NIO(New I/O)或 Netty 等框架。
-
资源管理:
- Socket 和相关的 I/O 流都是需要手动关闭的资源。强烈推荐使用
try-with-resources语句,它能确保在代码块执行完毕后,资源被自动关闭,即使发生了异常。
- Socket 和相关的 I/O 流都是需要手动关闭的资源。强烈推荐使用
-
字符编码:
- 当传输文本(如
String)时,务必注意编码问题,最好在创建InputStreamReader或OutputStreamWriter时明确指定编码,new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8),以避免因平台默认编码不同而导致的乱码。
- 当传输文本(如
-
网络异常处理:
- 网络是不可靠的,必须妥善处理各种
IOException,如连接中断、主机不可达、读写超时等。
- 网络是不可靠的,必须妥善处理各种
-
协议设计:
- 对于复杂的应用,仅仅靠
readLine()或readObject()是不够的,你需要设计一个应用层协议。- 长度前缀:在每个消息前加上一个固定长度的整数(4字节),表示消息体的长度,接收方先读取这4个字节,就知道接下来要读多少字节的数据。
- 分隔符:使用特殊字符(如
\r\n或\0)来分隔消息。 - 序列化格式:使用 JSON、XML、Protocol Buffers 等格式来结构化你的数据,这比 Java 原生的序列化更通用、更高效。
- 对于复杂的应用,仅仅靠
| 角色 | 核心类 | 主要步骤 | 关键点 |
|---|---|---|---|
| 服务器 | ServerSocket |
创建 ServerSocket 并绑定端口。accept() 阻塞等待连接。通过 Socket 获取 InputStream 和 OutputStream。读写数据。 关闭资源。 |
accept() 是阻塞的,需要持续运行。 |
| 客户端 | Socket |
创建 Socket 并连接服务器 IP 和端口。通过 Socket 获取 InputStream 和 OutputStream。读写数据。 关闭资源。 |
new Socket() 连接过程是阻塞的。 |
掌握 Java Socket 编程是理解网络编程的基础,从简单的字符串传输开始,逐步过渡到对象和自定义协议,你就能构建出强大的网络应用。
