Socket(套接字)是网络编程的基础,它是一组 API,允许程序通过网络进行数据交换,你可以把它想象成网络通信的“电话插座”,一个程序通过它“拨号”连接到另一个程序,然后双方就可以“通话”(发送和接收数据)。

Java 将 Socket 通信抽象为两个核心概念:
- 服务器端:被动等待客户端连接,并为其提供服务。
- 客户端:主动连接服务器,并向其发送请求。
核心概念与模型
在开始编码之前,理解几个核心概念至关重要:
- IP 地址 (Internet Protocol Address):网络中设备的唯一标识,就像你家的门牌号。
0.0.1是本机地址。 - 端口号 (Port Number):设备上应用程序的唯一标识,就像你家楼里的房号,一个 IP 地址可以有多个端口,每个端口对应一个服务,范围是 0-65535,0-1023 是知名端口,通常被系统服务占用(如 HTTP 服务的 80 端口)。
- Socket:IP 地址 + 端口号 的组合,构成了网络中一个唯一的通信端点,通过 Socket,程序可以明确地知道要和哪个设备的哪个应用程序通信。
- TCP (Transmission Control Protocol):一种面向连接的、可靠的、基于字节流的传输协议,Socket 编程默认使用的就是 TCP。
- 特点:连接前需要“三次握手”,数据传输可靠(有确认、重传、排序机制),传输效率相对较低。
- 适用场景:要求高可靠性的场景,如文件传输、网页浏览、邮件发送等。
- UDP (User Datagram Protocol):一种无连接的、不可靠的、基于数据报的传输协议。
- 特点:无需建立连接,直接发送数据包,速度快但不保证顺序和可靠性。
- 适用场景:对实时性要求高但能容忍少量丢包的场景,如视频会议、在线游戏、DNS 查询等。
本教程主要讲解最常用的 TCP Socket 编程。
TCP Socket 编程流程
TCP 通信是一个典型的“请求-响应”模型,流程如下:

服务器端流程:
- 创建 ServerSocket:在指定端口上创建一个服务器套接字,开始监听客户端的连接请求。
- 等待并接受连接:调用
accept()方法,该方法会阻塞(程序暂停执行),直到有一个客户端连接上来。 - 通信:为每个客户端连接创建一个新的 Socket,通过这个 Socket 的输入流读取客户端数据,通过输出流向客户端发送数据。
- 关闭连接:通信结束后,关闭与客户端的 Socket 连接。
- 关闭 ServerSocket:服务器程序结束时,关闭 ServerSocket。
客户端流程:
- 创建 Socket:创建一个 Socket 对象,指定要连接的服务器的 IP 地址和端口号,此时会向服务器发起连接请求。
- 通信:连接成功后,通过 Socket 的输出流向服务器发送数据,通过输入流读取服务器返回的数据。
- 关闭连接:通信结束后,关闭 Socket。
实战案例:简单的“Echo”服务器
“Echo”服务器是一个非常经典的入门案例:客户端发送什么消息,服务器就原样返回什么消息。
1 服务器端代码 (EchoServer.java)
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
public class EchoServer {
public static void main(String[] args) {
// 定义服务器要监听的端口号
int port = 8888;
// try-with-resources 语句,可以自动关闭资源
try (ServerSocket serverSocket = new ServerSocket(port)) {
System.out.println("服务器已启动,正在监听端口 " + port + "...");
// 2. 等待并接受客户端连接
// accept() 方法会阻塞,直到有客户端连接
Socket clientSocket = serverSocket.accept();
System.out.println("客户端已连接: " + clientSocket.getInetAddress().getHostAddress());
// 3. 获取输入流和输出流
// 使用 try-with-resources 确保流被关闭
try (
BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true);
) {
String inputLine;
// 4. 读取客户端发送的数据
while ((inputLine = in.readLine()) != null) {
System.out.println("收到客户端消息: " + inputLine);
// 5. 将消息回写给客户端
out.println("服务器回响: " + inputLine);
// 如果客户端发送 "bye",则退出循环
if ("bye".equalsIgnoreCase(inputLine)) {
break;
}
}
} catch (IOException e) {
System.err.println("与客户端通信时发生错误: " + e.getMessage());
} finally {
// 6. 关闭与客户端的连接
System.out.println("客户端 " + clientSocket.getInetAddress().getHostAddress() + " 已断开连接。");
clientSocket.close();
}
} catch (IOException e) {
System.err.println("服务器启动或运行时发生错误: " + e.getMessage());
e.printStackTrace();
}
}
}
2 客户端代码 (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;
import java.util.Scanner;
public class EchoClient {
public static void main(String[] args) {
String hostname = "127.0.0.1"; // 服务器地址,本机测试用
int port = 8888; // 服务器端口
// try-with-resources 语句
try (
Socket socket = new Socket(hostname, port);
// 从控制台读取用户输入
Scanner scanner = new Scanner(System.in);
// 获取输出流和输入流
PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
) {
System.out.println("已连接到服务器。");
System.out.println("请输入要发送的消息 (输入 'bye' 退出):");
String userInput;
// 循环读取用户输入并发送
while (true) {
System.out.print("客户端> ");
userInput = scanner.nextLine();
// 1. 发送消息到服务器
out.println(userInput);
// 2. 读取服务器返回的回响
String response = in.readLine();
System.out.println("服务器回响: " + response);
// 如果用户输入 "bye",则退出循环
if ("bye".equalsIgnoreCase(userInput)) {
break;
}
}
} catch (UnknownHostException e) {
System.err.println("无法找到主机: " + hostname);
e.printStackTrace();
} catch (IOException e) {
System.err.println("无法连接到服务器 " + hostname + " 在端口 " + port);
e.printStackTrace();
}
}
}
3 如何运行
- 编译:将两个
.java文件放在同一个目录下,执行javac EchoServer.java EchoClient.java。 - 启动服务器:先运行
java EchoServer,你会看到控制台打印出“服务器已启动...”。 - 启动客户端:再运行
java EchoClient,客户端会连接到服务器。 - 测试:在客户端的控制台输入任何文本,按回车,你会在客户端和服务器端都看到相应的消息,输入
bye即可退出程序。
进阶主题与最佳实践
上面的例子是阻塞式的,一次只能处理一个客户端,在实际应用中,这显然是不够的。
1 多线程处理并发连接
服务器需要为每个客户端连接创建一个新的线程来处理,这样主线程(监听线程)才能继续接受新的连接。
改进版服务器 (MultiThreadEchoServer.java)
import java.io.*;
import java.net.*;
public class MultiThreadEchoServer {
public static void main(String[] args) throws IOException {
int port = 8888;
try (ServerSocket serverSocket = new ServerSocket(port)) {
System.out.println("多线程服务器已启动,监听端口 " + port);
while (true) { // 循环接受所有客户端连接
Socket clientSocket = serverSocket.accept();
System.out.println("新客户端连接: " + clientSocket.getInetAddress().getHostAddress());
// 为每个客户端创建一个新的线程来处理
ClientHandler handler = new ClientHandler(clientSocket);
new Thread(handler).start();
}
}
}
}
// 客户端处理任务
class ClientHandler implements Runnable {
private final Socket clientSocket;
public ClientHandler(Socket socket) {
this.clientSocket = socket;
}
@Override
public void run() {
System.out.println("处理线程 " + Thread.currentThread().getName() + " 正在处理客户端 " + clientSocket.getInetAddress().getHostAddress());
try (
BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true);
) {
String inputLine;
while ((inputLine = in.readLine()) != null) {
System.out.println("[" + clientSocket.getInetAddress().getHostAddress() + "] 收到: " + inputLine);
out.println("服务器回响: " + inputLine);
if ("bye".equalsIgnoreCase(inputLine)) {
break;
}
}
} catch (IOException e) {
System.err.println("处理客户端时出错: " + e.getMessage());
} finally {
try {
clientSocket.close();
System.out.println("客户端 " + clientSocket.getInetAddress().getHostAddress() + " 已断开连接。");
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
2 使用线程池 (ExecutorService)
创建和销毁线程是有开销的,为了避免频繁创建销毁线程,可以使用线程池来管理线程。
// 在 MultiThreadEchoServer 的 main 方法中替换掉 new Thread(handler).start(); // 在类开头添加 // import java.util.concurrent.ExecutorService; // import java.util.concurrent.Executors; // ... // 创建一个固定大小的线程池,10 个线程 ExecutorService threadPool = Executors.newFixedThreadPool(10); // ... // 在 while 循环中 threadPool.execute(handler); // 将任务提交给线程池执行 // ... // 当服务器关闭时,需要关闭线程池 // threadPool.shutdown();
3 NIO (New I/O) 与非阻塞式 I/O
传统的 Socket I/O 是阻塞式的,线程在等待数据时会一直挂起,非常浪费资源。
Java NIO (New I/O) 提供了非阻塞式 I/O 的能力,它使用一个或几个专门的线程来管理所有连接的 I/O 操作,极大地提高了服务器的并发处理能力。
NIO 的核心组件:
- Channel (通道):类似流,但双向的,可以同时进行读写。
- Buffer (缓冲区):数据读写都必须通过缓冲区。
- Selector (选择器):单线程可以监控多个 Channel 的状态(如连接、读就绪、写就绪),当某个 Channel 准备好时,Selector 会通知它。
对于需要处理成千上万个连接的高性能服务器,NIO 是更好的选择,但对于初学者和大多数中小型应用,基于线程池的阻塞式 I/O 已经足够。
| 特性 | 阻塞式 I/O (BIO) | 非阻塞式 I/O (NIO) |
|---|---|---|
| 模型 | 一个连接一个线程 | 一个或多个线程管理多个连接 |
| 连接数 | 受限于线程数 | 理论上不受限制 |
| 编程复杂度 | 简单,易于理解和实现 | 复杂,需要理解 Channel, Buffer, Selector |
| 适用场景 | 连接数较少(<1000),业务逻辑简单 | 连接数多(C10K 问题),对性能要求高 |
学习路径建议:
- 掌握基础:深刻理解 TCP/IP 协议和 Socket 的基本概念。
- 熟练 BIO:能够独立编写单线程和多线程(线程池)的 Socket 服务器。
- 了解 NIO:理解 NIO 的核心思想和工作原理,知道它比 BIO 好在哪里。
- 学习 Netty:在实际项目中,通常不会直接使用原生 NIO API,因为其编程复杂且容易出错,业界广泛使用的是基于 NIO 的高性能网络框架,如 Netty、Mina,Netty 封装了 NIO 的复杂性,提供了简单易用的 API 和强大的功能(如编解码、线程模型等),是进行 Java 高性能网络开发的必备技能。
希望这份详细的指南能帮助你从零开始掌握 Java Socket 编程!
