- Socket 客户端:主动连接服务器,发送和接收数据。
- Socket 服务端:监听端口,等待客户端连接,并处理客户端的请求。
第一部分:Socket 客户端
客户端是主动发起连接的一方,它的基本流程是:

- 创建一个
Socket对象,指定服务器的 IP 地址和端口号。 - 如果连接成功,通过
Socket对象获取输出流(OutputStream),向服务器发送数据。 - 通过
Socket对象获取输入流(InputStream),接收服务器返回的数据。 - 通信结束后,关闭所有流和
Socket连接。
完整的客户端代码示例
这是一个简单的客户端,它会连接到本地服务端(localhost),发送一条消息,并打印服务器的回复。
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;
import java.net.UnknownHostException;
public class SocketClient {
public static void main(String[] args) {
// 服务器地址和端口
String host = "localhost"; // 或者服务器的IP地址,如 "192.168.1.100"
int port = 8080;
// try-with-resources 语句,可以自动关闭资源
try (
// 1. 创建一个Socket对象并尝试连接到服务器
Socket socket = new Socket(host, port);
// 2. 获取输出流,用于向服务器发送数据
OutputStream outputStream = socket.getOutputStream();
PrintWriter writer = new PrintWriter(outputStream, true); // true表示自动刷新缓冲区
// 3. 获取输入流,用于接收服务器返回的数据
InputStream inputStream = socket.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))
) {
System.out.println("成功连接到服务器 " + host + ":" + port);
// 4. 向服务器发送数据
String messageToSend = "你好,服务器!我是客户端。";
writer.println(messageToSend);
System.out.println("已发送消息: " + messageToSend);
// 5. 接收服务器的回复
String serverResponse = reader.readLine();
System.out.println("收到服务器回复: " + serverResponse);
} catch (UnknownHostException e) {
System.err.println("找不到主机: " + host);
e.printStackTrace();
} catch (IOException e) {
System.err.println("I/O错误: " + e.getMessage());
e.printStackTrace();
}
System.out.println("客户端连接已关闭。");
}
}
代码解释
new Socket(host, port):这是创建客户端连接的核心,它会尝试与指定host和port的服务器建立 TCP 连接,如果连接失败(例如服务器未启动或地址错误),会抛出IOException。OutputStream和PrintWriter:Socket.getOutputStream()获取一个字节输出流,我们通常将其包装成PrintWriter,因为它提供了方便的println()、print()等方法,可以直接写入字符串和数字,并处理字符编码。InputStream和BufferedReader:Socket.getInputStream()获取一个字节输入流,我们将其包装成InputStreamReader(将字节流转换为字符流),再包装成BufferedReader,以便使用readLine()方法方便地按行读取服务器发来的数据。try-with-resources:这是一个非常推荐的做法,它会自动在try块结束时关闭Socket以及所有从它创建的流(writer,reader),避免了资源泄漏,代码也更简洁。- 网络流与阻塞:
writer.println(...)会将数据发送到服务器的 TCP 缓冲区。reader.readLine()是一个阻塞方法,它会一直等待,直到从输入流中读取到一行完整的数据(以\n或\r\n或者流被关闭,如果服务器没有发送数据,客户端就会在这里“卡住”。
第二部分:Socket 服务端
服务端是被动等待连接的一方,它的基本流程是:
- 创建一个
ServerSocket对象,并绑定一个端口号,开始监听该端口。 - 调用
accept()方法,这是一个阻塞方法,它会一直等待,直到有客户端连接上来。 - 当
accept()返回时,它会返回一个新的Socket对象,这个Socket代表了与那个已连接客户端的通信通道。 - 通过这个新的
Socket对象,像客户端一样获取输入输出流,与单个客户端进行通信。 - 通信结束后,关闭与该客户端的
Socket连接。ServerSocket通常会一直运行,可以接受更多的客户端连接。
完整的服务端代码示例
这个服务端会监听 8080 端口,等待客户端连接,处理一个客户端的请求后,会继续等待下一个客户端。
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
public class SocketServer {
public static void main(String[] args) {
int port = 8080; // 服务端监听的端口
try (
// 1. 创建一个ServerSocket,并绑定到指定的端口
ServerSocket serverSocket = new ServerSocket(port);
) {
System.out.println("服务器已启动,正在监听端口 " + port + "...");
// 2. 调用accept()方法,等待客户端连接
// 这是一个阻塞方法,会一直阻塞直到有客户端连接
Socket clientSocket = serverSocket.accept();
System.out.println("客户端 " + clientSocket.getInetAddress().getHostAddress() + " 已连接。");
// try-with-resources 只管理 clientSocket 的流,serverSocket 保持开启
try (
// 3. 获取客户端的输入流,用于接收客户端发来的数据
InputStream inputStream = clientSocket.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
// 4. 获取客户端的输出流,用于向客户端发送数据
OutputStream outputStream = clientSocket.getOutputStream();
PrintWriter writer = new PrintWriter(outputStream, true);
) {
// 5. 接收客户端的消息
String clientMessage = reader.readLine();
System.out.println("收到客户端消息: " + clientMessage);
// 6. 处理消息并发送回复
String responseMessage = "你好,客户端!我已经收到你的消息: [" + clientMessage + "]";
writer.println(responseMessage);
System.out.println("已向客户端发送回复: " + responseMessage);
} // 自动关闭 clientSocket 的流
finally {
// 7. 关闭与该客户端的连接
clientSocket.close();
System.out.println("与客户端 " + clientSocket.getInetAddress().getHostAddress() + " 的连接已关闭。");
}
} catch (IOException e) {
System.err.println("服务器发生I/O错误: " + e.getMessage());
e.printStackTrace();
}
}
}
代码解释
new ServerSocket(port):创建一个服务端套接字,并绑定到指定的端口,一个端口在同一时间只能被一个程序绑定。serverSocket.accept():这是服务端的核心,它会阻塞程序的执行,直到一个新的网络连接被建立,一旦有客户端连接,accept()方法就会返回一个Socket对象,后续的通信都通过这个Socket对象进行。- 处理多个客户端:上面的示例代码一次只能处理一个客户端,当第一个客户端连接、通信、断开后,
accept()才会返回,等待下一个客户端,要同时处理多个客户端,你需要使用多线程或线程池,基本思路是:在accept()返回后,为这个新的Socket创建一个新线程,让这个线程去处理通信逻辑,而主线程则立即回到accept(),继续等待下一个客户端。
如何运行和测试
-
先运行服务端:
(图片来源网络,侵删)java SocketServer
你会看到控制台输出:
服务器已启动,正在监听端口 8080... -
再运行客户端:
java SocketClient
客户端控制台会输出:
成功连接到服务器 localhost:8080 已发送消息: 你好,服务器!我是客户端。 收到服务器回复: 你好,客户端!我已经收到你的消息: [你好,服务器!我是客户端,] 客户端连接已关闭。 -
观察服务端控制台:
(图片来源网络,侵删)服务器已启动,正在监听端口 8080... 客户端 127.0.0.1 已连接。 收到客户端消息: 你好,服务器!我是客户端。 已向客户端发送回复: 你好,客户端!我已经收到你的消息: [你好,服务器!我是客户端,] 与客户端 127.0.0.1 的连接已关闭。
总结与注意事项
- 阻塞模型:基本的 Socket 编程是阻塞的。
accept(),read(),readLine()等方法在没有数据或连接时会一直等待,对于高并发的场景,需要升级到 NIO (New I/O) 模型。 - 资源管理:务必关闭所有打开的流和
Socket,使用try-with-resources是最佳实践。 - 异常处理:网络编程充满了不确定性,如连接中断、数据丢失等,必须妥善处理
IOException及其子类异常。 - 字符编码:当传输文本数据时,
InputStreamReader和OutputStreamWriter可以指定字符编码(如UTF-8),以确保在不同系统上不会出现乱码。// 在读写时指定编码 BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, "UTF-8")); PrintWriter writer = new PrintWriter(new OutputStreamWriter(outputStream, "UTF-8"), true);
- 协议设计:简单的
readLine()只能处理以换行符结尾的文本,在实际应用中,你可能需要设计自己的应用层协议,- 固定长度:每条消息固定为 N 个字节。
- 特殊分隔符:用特殊字符(如
\0)作为消息的结束标志。 - 长度前缀:在每个消息的头部加上消息体的长度,接收方先读取长度,再读取对应长度的数据,这是最常用和最健壮的方式。
