理解并实现 Socket 通信基础
网络应用程序通常包含客户端和服务端。客户端向服务器发送请求并接收响应,服务器则提供数据支持。在网络编程中,选择 TCP 还是 UDP 取决于连接建立的需求。TCP 需要先建立连接,而 UDP 直接发送数据。服务器端需在特定端口监听,等待客户端连接,并为其分配资源。客户端则主动发起连接请求,并通过建立的通路与服务器通信。
Socket(套接字)是操作系统提供的一种进程间通信机制,可用于本地或远程进程通信。应用程序通过 Socket API 使用套接字进行数据交换。TCP 和 UDP 是传输层协议,而 Socket 是实现连接和数据传输的途径。TCP 的三次握手是基于 TCP 协议创建网络通路的过程,最终由 Socket 完成。
Socket 连接实现
建立网络连接至少需要一对套接字:服务器端套接字和客户端套接字。
服务器端准备过程
- 创建套接字:这里我们使用 TCP 实现。
- 绑定监听地址:将套接字与特定的 IP 地址和端口号关联。为了适应不同的部署环境,IP 地址通常使用通配符或指定具体网卡的 IP。如果不指定端口号,操作系统会自动分配一个空闲端口。
- 开启监听模式:调用
listen函数使套接字进入被动监听状态,等待客户端连接请求。 - 接受连接:调用
accept函数(阻塞式)等待客户端连接。当客户端成功连接后,accept返回一个新的套接字用于与该客户端通信。
以下是一个简化的服务器端实现:
package com.example.network;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class SimpleServerSocket {
private static final int PORT = 8088;
private static final ExecutorService threadPool = Executors.newFixedThreadPool(10);
private static final java.util.logging.Logger logger = java.util.logging.Logger.getLogger(SimpleServerSocket.class.getName());
public static void main(String[] args) {
try {
new SimpleServerSocket().startServer();
} catch (IOException e) {
logger.severe("Server startup failed: " + e.getMessage());
}
}
private void startServer() throws IOException {
// 创建 ServerSocket,绑定端口并开始监听
try (ServerSocket serverSocket = new ServerSocket(PORT)) {
logger.info("Server started, listening on port " + PORT);
while (true) {
// 阻塞等待客户端连接
Socket clientSocket = serverSocket.accept();
logger.info("Client connected: " + clientSocket.getInetAddress());
// 为每个客户端连接创建一个新线程处理
threadPool.execute(new ClientHandler(clientSocket));
}
}
}
private static class ClientHandler implements Runnable {
private final Socket clientSocket;
public ClientHandler(Socket socket) {
this.clientSocket = socket;
}
@Override
public void run() {
try (var in = clientSocket.getInputStream();
var out = clientSocket.getOutputStream();
var reader = new BufferedReader(new InputStreamReader(in, java.nio.charset.StandardCharsets.UTF_8));
var writer = new BufferedWriter(new OutputStreamWriter(out, java.nio.charset.StandardCharsets.UTF_8))) {
String message;
// 循环读取客户端发送的消息,直到客户端断开连接
while ((message = reader.readLine()) != null) {
logger.info("Received from client: " + message);
// 将收到的消息回显给客户端
writer.write("Echo: " + message);
writer.newLine();
writer.flush();
}
} catch (IOException e) {
logger.warning("Client handling error: " + e.getMessage());
} finally {
try {
clientSocket.close();
logger.info("Client disconnected: " + clientSocket.getInetAddress());
} catch (IOException e) {
logger.warning("Error closing client socket: " + e.getMessage());
}
}
}
}
}
客户端发起连接过程
- 创建套接字:客户端创建一个 Socket 对象。
- 发起连接:使用
connect方法(或直接在 Socket 构造函数中指定服务器地址和端口)向服务器发起连接请求。此过程会触发 TCP 的三次握手。客户端在发起连接前不必显式调用bind,操作系统会自动分配一个临时端口。
注意:读取操作(read)通常也是阻塞的,直到接收到数据或对方明确表示数据发送完毕。在实际应用中,常通过约定数据长度或使用特定分隔符(如换行符 \n)来标识数据结束。
以下是一个简化的客户端实现:
package com.example.network;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class SimpleSocketClient {
private static final String SERVER_HOST = "127.0.0.1";
private static final int SERVER_PORT = 8088;
private static final ExecutorService threadPool = Executors.newSingleThreadExecutor();
private static final java.util.logging.Logger logger = java.util.logging.Logger.getLogger(SimpleSocketClient.class.getName());
public static void main(String[] args) {
try {
// 创建 Socket 并连接到服务器
Socket socket = new Socket(SERVER_HOST, SERVER_PORT);
logger.info("Connected to server: " + SERVER_HOST + ":" + SERVER_PORT);
// 启动一个线程用于接收服务器的响应
threadPool.execute(new ServerResponseHandler(socket));
// 从标准输入读取用户输入并发送给服务器
try (var reader = new BufferedReader(new InputStreamReader(System.in, java.nio.charset.StandardCharsets.UTF_8));
var writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream(), java.nio.charset.StandardCharsets.UTF_8))) {
String messageToSend;
while ((messageToSend = reader.readLine()) != null) {
writer.write(messageToSend);
writer.newLine(); // 发送换行符作为消息结束标识
writer.flush();
}
} finally {
socket.close();
logger.info("Connection closed.");
}
} catch (IOException e) {
logger.severe("Client connection failed: " + e.getMessage());
} finally {
threadPool.shutdown();
}
}
private static class ServerResponseHandler implements Runnable {
private final Socket clientSocket;
public ServerResponseHandler(Socket socket) {
this.clientSocket = socket;
}
@Override
public void run() {
try (var in = clientSocket.getInputStream();
var reader = new BufferedReader(new InputStreamReader(in, java.nio.charset.StandardCharsets.UTF_8))) {
String response;
// 循环读取服务器的响应
while ((response = reader.readLine()) != null) {
logger.info("Received from server: " + response);
}
} catch (IOException e) {
// 忽略连接中断的异常,这通常意味着服务器关闭了连接
if (!clientSocket.isClosed()) {
logger.warning("Error receiving from server: " + e.getMessage());
}
}
}
}
}