Java 中的 Socket 编程是基于 TCP/IP 协议实现网络通信的。Socket 提供了一种双向通信机制,允许不同主机之间通过网络进行数据交换。以下是 Java Socket 连接的底层原理详解:
1. 基本概念
- Socket:用于描述 IP 地址和端口,是网络通信的基本单元。
- ServerSocket:服务器端的套接字,用于监听来自客户端的连接请求。
- TCP(Transmission Control Protocol) :传输控制协议,提供可靠、面向连接的通信。
- IP(Internet Protocol) :互联网协议,定义了在网络中的地址和路径选择。
2. 工作过程
1. 建立连接
-
服务器端:
- 创建
ServerSocket对象,并绑定到指定的端口,等待客户端的连接请求。 - 使用
accept()方法监听并接受客户端的连接。
- 创建
-
客户端:
- 创建
Socket对象,并指定服务器的 IP 地址和端口号,发起连接请求。
- 创建
// 服务器端
ServerSocket serverSocket = new ServerSocket(8080);
Socket clientSocket = serverSocket.accept(); // 阻塞,直到有客户端连接
// 客户端
Socket socket = new Socket("127.0.0.1", 8080); // 连接到服务器
2. 数据传输
一旦建立连接,双方可以通过输入输出流来发送和接收数据。
- 输出流:用来发送数据到对方。
- 输入流:用来接收对方发送的数据。
// 服务器端
InputStream inputStream = clientSocket.getInputStream();
OutputStream outputStream = clientSocket.getOutputStream();
// 客户端
InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream();
3. 关闭连接
当通信完成后,双方都可以关闭连接,以释放资源。
clientSocket.close();
serverSocket.close();
socket.close();
3. 底层原理
1. 网络协议栈
Java 的 Socket 编程依赖于操作系统提供的网络协议栈,其主要涉及以下几层:
- 应用层:Java 的 Socket API 位于应用层,通过调用操作系统的 Socket 接口来发送和接收数据。
- 传输层:主要使用 TCP 协议,提供面向连接的可靠传输服务。
- 网络层:使用 IP 协议,负责将数据包从源地址传递到目的地址。
- 数据链路层:负责在物理介质上传输数据。
2. 三次握手
TCP 使用三次握手(Three-way Handshake)来建立连接:
- 第一次握手:客户端发送一个 SYN(同步)包到服务器,表示请求建立连接。
- 第二次握手:服务器接收到 SYN 包后,回复一个 SYN-ACK(同步-确认)包。
- 第三次握手:客户端收到 SYN-ACK 包后,再次发送一个 ACK(确认)包,连接正式建立。
3. 四次挥手
TCP 使用四次挥手(Four-way Handshake)来关闭连接:
- 第一次挥手:发送方发送一个 FIN(结束)包,表示不再发送数据。
- 第二次挥手:接收方接收到 FIN 包后,回复一个 ACK 包,表示确认接收到结束信号。
- 第三次挥手:接收方发送一个 FIN 包,表示它也不再发送数据。
- 第四次挥手:发送方接收到 FIN 包后,回复一个 ACK 包,表示确认接收到结束信号,连接正式关闭。
4. 示例代码
以下是一个简单的 Java Socket 编程示例:
服务器端:
import java.io.*;
import java.net.*;
public class EchoServer {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(8080);
System.out.println("Server started, waiting for connection...");
while (true) {
Socket clientSocket = serverSocket.accept();
System.out.println("Connection established with client.");
BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true);
String inputLine;
while ((inputLine = in.readLine()) != null) {
String inputLine;
while ((inputLine = in.readLine()) != null) {
System.out.println("Received: " + inputLine);
out.println(inputLine); // Echo back the received message
if ("exit".equalsIgnoreCase(inputLine)) {
break;
}
}
in.close();
out.close();
clientSocket.close();
System.out.println("Connection closed with client.");
}
}
}
客户端:
import java.io.*;
import java.net.*;
public class EchoClient {
public static void main(String[] args) throws IOException {
String serverAddress = "127.0.0.1"; // Localhost
int port = 8080;
Socket socket = new Socket(serverAddress, port);
System.out.println("Connected to the server.");
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
BufferedReader userIn = new BufferedReader(new InputStreamReader(System.in));
String userInput;
while ((userInput = userIn.readLine()) != null) {
out.println(userInput);
System.out.println("Server response: " + in.readLine());
if ("exit".equalsIgnoreCase(userInput)) {
break;
}
}
in.close();
out.close();
userIn.close();
socket.close();
System.out.println("Disconnected from the server.");
}
}
运行示例
-
运行服务器端:先启动
EchoServer类。这时服务器会在指定的端口(例如 8080)上监听客户端连接。 -
运行客户端:然后启动
EchoClient类,客户端将连接到服务器,并且能够发送消息。服务器接收到消息后会回显给客户端。 -
通信过程:
- 在客户端输入消息并按回车,消息会发送到服务器。
- 服务器接收到消息后,会回显同样的消息。
- 输入
exit可以终止连接。
Socket 类源码分析
Socket 类表示客户端或服务器端与网络上的另一台机器之间的一条通信链路。
Socket 类的重要字段:
private boolean created = false;
private boolean bound = false; //指示客户端套接字是否已经绑定到特定的端口和地址
private boolean connected = false;
private boolean closed = false;
private final Object closeLock = new Object();
private boolean shutIn = false;
private boolean shutOut = false;
SocketImpl impl; // Socket 实现类,实际进行通信的底层实现
关键方法:
-
构造方法
public Socket() { setImpl(); } public Socket(InetAddress address, int port) throws IOException { this(address, port, null, 0); } public Socket(String host, int port) throws UnknownHostException, IOException { this(host != null ? new InetSocketAddress(host, port) : null, (SocketAddress) null, true); } -
连接方法
当我们使用 Socket 创建连接时,调用了如下方法:
public void connect(SocketAddress endpoint, int timeout) throws IOException { if (endpoint == null) throw new IllegalArgumentException("connect: The address can't be null"); if (timeout < 0) throw new IllegalArgumentException("connect: timeout can't be negative"); synchronized (this) {//确保在多线程环境下`Socket`对象的状态一致性和线程安全 if (isClosed()) throw new SocketException("Socket is closed"); if (!oldImpl && isConnected()) throw new SocketException("already connected"); if (endpoint instanceof InetSocketAddress) { InetSocketAddress epoint = (InetSocketAddress) endpoint; InetAddress addr = epoint.getAddress(); int port = epoint.getPort(); checkAddress(addr, "connect"); SecurityManager security = System.getSecurityManager(); if (security != null) { if (epoint.isUnresolved()) security.checkConnect(epoint.getHostName(), port); else security.checkConnect(addr.getHostAddress(), port); } } else { throw new IllegalArgumentException("Unsupported address type"); } if (!created) createImpl(true); if (!oldImpl) impl.connect(endpoint, timeout); else if (timeout == 0) { if (epoint.isUnresolved()) impl.connect((String) null, -1); else impl.connect(epoint.getAddress(), epoint.getPort()); } else throw new UnsupportedOperationException("SO_TIMEOUT not supported with old socket impl"); connected = true; bound = true; } }
synchronized同步的重要性
使用synchronized关键字的原因是确保在多线程环境下Socket对象的状态一致性和线程安全。例如:一个分布式系统中的节点可能需要与其他多个节点进行通信,或者一个下载管理器需要同时从多个服务器拉取数据。每个连接到不同服务器的逻辑都被放到了一个独立的线程中,从而实现多连接并行。
- 确保连接过程不会被其他线程打断。
- 防止多个线程同时尝试创建、绑定或连接
Socket对象,从而导致不可预测的行为或竞态条件。
-
数据传输方法
getInputStream()和getOutputStream()方法用于获取与远程主机进行通信的输入输出流。public InputStream getInputStream() throws IOException { if (isClosed()) throw new SocketException("Socket is closed"); if (!isConnected()) throw new SocketException("Socket is not connected"); if (isInputShutdown()) throw new SocketException("Socket input is shutdown"); return impl.getInputStream(); } public OutputStream getOutputStream() throws IOException { if (isClosed()) throw new SocketException("Socket is closed"); if (!isConnected()) throw new SocketException("Socket is not connected"); if (isOutputShutdown()) throw new SocketException("Socket output is shutdown"); return impl.getOutputStream(); }
ServerSocket 类源码分析
ServerSocket 类用于服务器端监听客户端连接请求。
ServerSocket 类的重要字段:
private boolean created = false;
private boolean bound = false;
private boolean closed = false;
private Object closeLock = new Object();
private SocketImpl impl; // ServerSocket 实现类,实际进行通信的底层实现
关键方法:
-
构造方法
public ServerSocket() throws IOException { setImpl(); } public ServerSocket(int port) throws IOException { this(port, 50, null); } public ServerSocket(int port, int backlog, InetAddress bindAddr) throws IOException { setImpl(); if (port < 0 || port > 0xFFFF) throw new IllegalArgumentException("Port value out of range: " + port); if (backlog < 1) backlog = 50; try { bind(new InetSocketAddress(bindAddr, port), backlog); } catch (SecurityException e) { close(); throw e; } catch (IOException e) { close(); throw e; } -
绑定方法
bind()方法用于绑定服务器套接字到指定的端口和地址。public void bind(SocketAddress endpoint, int backlog) throws IOException { if (isClosed()) throw new SocketException("Socket is closed"); if (isBound()) throw new SocketException("Already bound"); if (endpoint == null) endpoint = new InetSocketAddress(0); if (backlog < 1) backlog = 50; impl.bind(endpoint, backlog); bound = true; } -
监听连接
accept()方法用于监听并接受客户端的连接请求。它会阻塞直到一个连接建立。public Socket accept() throws IOException { if (isClosed()) throw new SocketException("Socket is closed"); if (!isBound()) throw new SocketException("Socket is not bound yet"); Socket s = new Socket((SocketImpl) null); implAccept(s); return s; } protected final void implAccept(Socket s) throws IOException { SocketImpl si = null; try { si = s.impl; if (si == null) { s.setImpl(); } s.impl.accept(this.impl); s.postAccept(); } catch (IOException e) { s.close(); throw e; } } -
关闭连接
当不再需要使用
ServerSocket时,可以调用close()方法关闭连接,释放资源。public synchronized void close() throws IOException { synchronized (closeLock) { if (isClosed()) return; if (created) { impl.close(); } closed = true; } }
小结
通过以上对 Socket 和 ServerSocket 的源码分析,我们可以看到它们是如何在 Java 中实现网络通信的。以下是总结:
-
Socket类主要用于创建客户端与服务器之间的连接,并通过输入输出流进行数据传输。- 重要方法:
connect()、getInputStream()、getOutputStream()。 - 底层由
SocketImpl实现实际的网络操作。
- 重要方法:
-
ServerSocket类用于在服务器端监听连接请求,并接受客户端的连接。- 重要方法:
bind()、accept()、close()。 - 同样依赖于
SocketImpl来处理具体的网络操作。
- 重要方法: