核心概念
- Socket (套接字):网络通信的端点,你可以把它想象成一个电话,通过它可以在网络上建立连接。
- ServerSocket (服务器套接字):服务器端用来监听客户端连接请求的“总机”,它会在一个指定的端口上等待,当有客户端尝试连接时,它会接受连接并返回一个
Socket对象,用于与该客户端进行后续通信。 - InputStream / OutputStream:通过
Socket获取的输入流和输出流,用于在连接上读取(接收)和写入(发送)数据。 - 字节流 vs. 字符流:
- 字节流:
Socket的getInputStream()和getOutputStream()默认提供的是字节流 (InputStream/OutputStream),它们直接处理字节数据,适合传输任何类型的数据(如图片、文件等)。 - 字符流:为了方便处理文本,我们通常会将字节流包装成字符流,如
InputStreamReader(字节流转字符流) 和BufferedReader(为字符流提供缓冲功能,提高效率)。
- 字节流:
第一步:创建一个简单的 Echo 服务器
一个 "Echo" 服务器会将从客户端接收到的任何消息原封不动地发送回去,这是理解 Socket 通信最经典的例子。

服务器端代码 (EchoServer.java)
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
public class EchoServer {
public static void main(String[] args) {
// 服务器监听的端口号
int port = 12345;
try (// 创建一个 ServerSocket,在指定的端口上监听客户端连接
ServerSocket serverSocket = new ServerSocket(port)) {
System.out.println("服务器已启动,正在监听端口 " + port + "...");
// serverSocket.accept() 是一个阻塞方法,它会一直等待直到有客户端连接
// 当有客户端连接时,它会返回一个 Socket 对象,代表与该客户端的连接
Socket clientSocket = serverSocket.accept();
System.out.println("客户端已连接: " + clientSocket.getInetAddress().getHostAddress());
// 获取客户端的输入流,用于读取客户端发送的消息
// 使用 BufferedReader 包装 InputStreamReader,以便按行读取文本
BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
// 读取客户端发送的消息
// readLine() 也是一个阻塞方法,会一直等待直到客户端发送一行数据(以换行符结尾)
String inputLine;
while ((inputLine = in.readLine()) != null) {
// 如果客户端发送 "exit",则关闭连接
if ("exit".equalsIgnoreCase(inputLine)) {
System.out.println("客户端请求关闭连接。");
break;
}
System.out.println("收到客户端消息: " + inputLine);
// Echo 回去
// 注意:为了简单起见,这里我们只打印到服务器控制台。
// 一个完整的 Echo 服务器会把消息写回客户端的输出流。
}
} catch (IOException e) {
System.err.println("服务器发生错误: " + e.getMessage());
e.printStackTrace();
}
}
}
代码解析:
ServerSocket serverSocket = new ServerSocket(12345);: 在 12345 端口上创建一个服务器套接字。serverSocket.accept();: 这是最关键的一步,它会阻塞程序的执行,直到有一个客户端连接到这个端口,一旦连接成功,它会返回一个Socket对象clientSocket。new BufferedReader(new InputStreamReader(clientSocket.getInputStream())): 获取从客户端流向服务器的数据流,我们使用InputStreamReader将字节流转为字符流,再用BufferedReader包装它,这样可以方便地使用readLine()方法按行读取消息。while ((inputLine = in.readLine()) != null): 循环读取客户端发送的每一行数据。readLine()也会阻塞,直到流中有新的数据可读。if ("exit".equalsIgnoreCase(inputLine)): 一个简单的退出机制,当客户端发送 "exit" 时,服务器会退出循环并关闭连接。try-with-resources:ServerSocket和Socket都实现了AutoCloseable接口,使用try-with-resources语句可以确保它们在使用后被自动关闭,防止资源泄露。
第二步:创建客户端来连接服务器并发送消息
客户端代码 (EchoClient.java)
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.net.UnknownHostException;
public class EchoClient {
public static void main(String[] args) {
// 服务器的 IP 地址(本地服务器用 "localhost" 或 "127.0.0.1")
String hostname = "localhost";
// 服务器的端口号,必须与服务器设置的端口一致
int port = 12345;
try (// 创建一个 Socket 连接到指定主机和端口
Socket socket = new Socket(hostname, port);
// 获取输出流,用于向服务器发送消息
PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
// 获取输入流,用于读取服务器返回的消息
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
// 获取控制台输入流,用于从用户键盘读取输入
BufferedReader stdIn = new BufferedReader(new InputStreamReader(System.in))) {
System.out.println("已连接到服务器。");
System.out.println("请输入要发送的消息(输入 'exit' 退出):");
String userInput;
// 循环读取用户从键盘输入的消息
while ((userInput = stdIn.readLine()) != null) {
// 将用户输入的消息发送给服务器
out.println(userInput);
// 读取服务器返回的 Echo 消息
String response = in.readLine();
System.out.println("服务器回复: " + response);
// 检查是否要退出
if ("exit".equalsIgnoreCase(userInput)) {
break;
}
System.out.println("请输入下一条消息:");
}
} catch (UnknownHostException e) {
System.err.println("无法找到主机: " + hostname);
e.printStackTrace();
} catch (IOException e) {
System.err.println("I/O 发生错误: " + e.getMessage());
e.printStackTrace();
}
}
}
代码解析:
new Socket("localhost", 12345);: 创建一个套接字,尝试连接到localhost(本机)的 12345 端口,这同样是一个阻塞操作,直到连接成功或失败。new PrintWriter(socket.getOutputStream(), true): 获取流向服务器的数据流。PrintWriter可以方便地使用println()方法发送字符串,并自动处理换行符。true参数表示自动刷新(每次调用println后都会立即将数据发送出去)。new BufferedReader(new InputStreamReader(socket.getInputStream())): 获取从服务器返回的数据流,用于读取服务器的回复。BufferedReader stdIn = new BufferedReader(new InputStreamReader(System.in)): 获取标准输入流(即键盘输入),以便让用户可以输入消息。while ((userInput = stdIn.readLine()) != null): 循环读取用户在控制台输入的每一行内容。out.println(userInput);: 将用户输入的内容发送给服务器。String response = in.readLine();: 读取服务器返回的回复。
如何运行
-
编译代码:
javac EchoServer.java EchoClient.java
-
启动服务器: 在一个终端窗口中运行服务器:
(图片来源网络,侵删)java EchoServer
你会看到输出:
服务器已启动,正在监听端口 12345... -
启动客户端: 在另一个终端窗口中运行客户端:
java EchoClient
你会看到输出:
已连接到服务器。 -
测试通信:
(图片来源网络,侵删)-
在客户端的终端输入任意消息,然后按回车。
-
输入
Hello Server,按回车。 -
客户端会显示:
服务器回复: Hello Server -
服务器终端会显示:
收到客户端消息: Hello Server -
在客户端输入
exit,按回车。 -
客户端和服务器都会关闭连接并退出程序。
-
重要注意事项和进阶
阻塞操作
serverSocket.accept(), socket.getInputStream().read(), bufferedReader.readLine() 都是阻塞方法,这意味着如果没有数据到达,线程会一直等待,无法执行后续代码,对于需要同时处理多个客户端的服务器,这是一个大问题。
处理多个客户端
一个简单的 while 循环只能处理一个客户端,要处理多个客户端,你需要使用多线程。
改进思路:
当 serverSocket.accept() 返回一个新的 Socket 后,立即创建一个新的线程来处理这个客户端的通信,而主线程则继续返回 accept() 状态,等待下一个客户端。
示例代码框架:
// 在 EchoServer 的 main 方法中
while (true) {
Socket clientSocket = serverSocket.accept();
// 为每个客户端连接创建一个新线程
new Thread(new ClientHandler(clientSocket)).start();
}
// ClientHandler 类实现 Runnable 接口
class ClientHandler implements Runnable {
private Socket clientSocket;
public ClientHandler(Socket socket) {
this.clientSocket = socket;
}
@Override
public void run() {
try (BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()))) {
String inputLine;
while ((inputLine = in.readLine()) != null) {
System.out.println("来自 " + clientSocket.getInetAddress() + " 的消息: " + inputLine);
// ... 处理消息 ...
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
发送和接收复杂数据
上面的例子只适合发送文本行,如果要发送对象、图片等,你需要使用更高级的序列化技术。
-
发送对象: 使用
ObjectOutputStream和ObjectInputStream。// 发送端 ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream()); oos.writeObject(myObject); // myObject 必须实现 Serializable 接口 // 接收端 ObjectInputStream ois = new ObjectInputStream(socket.getInputStream()); MyObject receivedObject = (MyObject) ois.readObject();
-
发送文件: 使用
FileInputStream读取文件,然后用OutputStream发送。
关闭资源
非常重要! 在通信结束后,必须关闭所有打开的资源(Socket, InputStream, OutputStream),以避免资源泄露,推荐使用 try-with-resources 语句,如上面的示例所示,它能确保资源被自动关闭。
异常处理
网络通信非常脆弱,可能会因为各种原因(如网络中断、客户端突然断开)抛出 IOException,代码中必须有健壮的异常处理机制。
