Socket(套接字)是网络通信的基石,它就像计算机之间的"电话插座"。想象一下,当你想和朋友通话时,你需要先拨通对方的号码,建立连接后才能交谈。Socket 在网络通信中扮演着类似的角色,它提供了应用程序与网络协议栈之间的标准接口。
在 Java 中,Socket 编程主要分为两种模式:
提示:TCP 适合需要可靠传输的场景(如文件传输、网页浏览),UDP 适合实时性要求高的场景(如视频会议、在线游戏)
TCP 协议采用"三次握手"建立连接:
这个机制确保了通信双方都做好了收发数据的准备,就像打电话时的"喂,能听到吗?""能听到,你呢?""我也能听到"这样的确认过程。
一个完整的 TCP 服务端需要以下步骤:
java复制public class TCPServer {
public static void main(String[] args) throws IOException {
// 1. 创建ServerSocket并绑定端口
ServerSocket serverSocket = new ServerSocket(8888);
System.out.println("服务器启动,监听端口:8888...");
// 2. 循环接受客户端连接
while (true) {
// 3. 等待客户端连接(阻塞方法)
Socket clientSocket = serverSocket.accept();
System.out.println("客户端连接:" + clientSocket.getInetAddress());
// 4. 为每个客户端创建独立线程
new Thread(() -> {
try {
// 5. 获取输入流读取数据
BufferedReader reader = new BufferedReader(
new InputStreamReader(clientSocket.getInputStream())
);
String message = reader.readLine();
System.out.println("收到消息:" + message);
// 6. 获取输出流发送响应
PrintWriter writer = new PrintWriter(
clientSocket.getOutputStream(), true
);
writer.println("服务器响应:" + message);
// 7. 关闭资源
reader.close();
writer.close();
clientSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}).start();
}
}
}
注意:实际项目中应该使用线程池管理客户端连接线程,避免频繁创建销毁线程的开销
TCP 客户端的实现相对简单:
java复制public class TCPClient {
public static void main(String[] args) {
try {
// 1. 创建Socket连接服务器
Socket socket = new Socket("localhost", 8888);
System.out.println("已连接服务器");
// 2. 获取输出流发送数据
PrintWriter writer = new PrintWriter(
socket.getOutputStream(), true
);
writer.println("Hello Server!");
// 3. 获取输入流接收响应
BufferedReader reader = new BufferedReader(
new InputStreamReader(socket.getInputStream())
);
String response = reader.readLine();
System.out.println("服务器响应:" + response);
// 4. 关闭资源
writer.close();
reader.close();
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
TCP Socket 通信的标准流程可以用以下步骤描述:
服务端准备:
客户端连接:
数据传输:
连接关闭:
单线程服务端只能同时处理一个客户端连接,这显然不能满足实际需求。我们可以通过以下两种方式改进:
方案一:每个连接独立线程
java复制while (true) {
Socket clientSocket = serverSocket.accept();
new Thread(() -> handleClient(clientSocket)).start();
}
方案二:使用线程池
java复制ExecutorService pool = Executors.newFixedThreadPool(10);
while (true) {
Socket clientSocket = serverSocket.accept();
pool.execute(() -> handleClient(clientSocket));
}
经验:线程池大小应根据服务器硬件和预期负载调整,通常设置为 CPU 核心数的 2-3 倍
正确的资源管理可以避免内存泄漏和连接泄漏:
java复制// 使用try-with-resources确保资源自动关闭
try (Socket socket = new Socket("localhost", 8888);
PrintWriter writer = new PrintWriter(socket.getOutputStream(), true);
BufferedReader reader = new BufferedReader(
new InputStreamReader(socket.getInputStream())
)) {
// 通信逻辑...
} catch (IOException e) {
e.printStackTrace();
}
合理设置缓冲区大小可以提升传输效率:
java复制// 设置发送缓冲区大小(单位:字节)
socket.setSendBufferSize(8192);
// 设置接收缓冲区大小
socket.setReceiveBufferSize(8192);
提示:缓冲区大小应根据网络状况和传输数据量调整,通常设置为 MTU(1500字节)的整数倍
现象:客户端连接服务端时长时间无响应
可能原因:
解决方案:
java复制// 设置连接超时时间(毫秒)
socket.connect(new InetSocketAddress("localhost", 8888), 5000);
现象:多条消息被合并接收
原因:TCP是流式协议,不维护消息边界
解决方案:
java复制// 使用BufferedReader的readLine()方法按行读取
String message = reader.readLine();
// 或者自定义协议
DataInputStream dis = new DataInputStream(socket.getInputStream());
int length = dis.readInt(); // 先读取长度
byte[] data = new byte[length];
dis.readFully(data); // 再读取指定长度的数据
让我们通过一个完整案例巩固所学知识:
java复制public class ChatServer {
private static final int PORT = 8888;
private static List<Socket> clients = new ArrayList<>();
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(PORT);
System.out.println("聊天室服务器已启动,端口:" + PORT);
while (true) {
Socket clientSocket = serverSocket.accept();
synchronized (clients) {
clients.add(clientSocket);
}
System.out.println("新用户加入:" + clientSocket.getInetAddress());
new Thread(() -> handleClient(clientSocket)).start();
}
}
private static void handleClient(Socket clientSocket) {
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(clientSocket.getInputStream()))) {
String message;
while ((message = reader.readLine()) != null) {
System.out.println("收到消息:" + message);
broadcast(message, clientSocket);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
synchronized (clients) {
clients.remove(clientSocket);
}
System.out.println("用户退出:" + clientSocket.getInetAddress());
}
}
private static void broadcast(String message, Socket sender) {
synchronized (clients) {
for (Socket client : clients) {
if (client != sender) {
try {
PrintWriter writer = new PrintWriter(
client.getOutputStream(), true
);
writer.println(message);
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
}
java复制public class ChatClient {
public static void main(String[] args) {
try (Socket socket = new Socket("localhost", 8888);
BufferedReader reader = new BufferedReader(
new InputStreamReader(socket.getInputStream())
);
PrintWriter writer = new PrintWriter(
socket.getOutputStream(), true
);
Scanner scanner = new Scanner(System.in)) {
// 启动消息接收线程
new Thread(() -> {
try {
String message;
while ((message = reader.readLine()) != null) {
System.out.println("收到:" + message);
}
} catch (IOException e) {
e.printStackTrace();
}
}).start();
// 主线程处理用户输入
System.out.println("请输入消息(输入exit退出):");
while (true) {
String input = scanner.nextLine();
if ("exit".equalsIgnoreCase(input)) {
break;
}
writer.println(input);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
java复制// 设置读取超时(毫秒)
socket.setSoTimeout(30000);
在实际项目中,我经常遇到客户端异常断开导致连接泄漏的情况。我的经验是:一定要在finally块中确保所有资源都被正确关闭,同时使用心跳机制检测连接状态。对于高并发场景,建议使用Netty等成熟框架而非原生Socket API。