一、什么是 Socket?
Socket(套接字)是网络中不同设备间进程通信的端点,可以理解为 "通信的把手"------ 通过它能建立两台设备间的连接,实现数据收发。
它基于 OSI 网络模型的会话层(Socket 层)负责建立链接工作,向下依赖传输层(TCP/UDP)、网络层等完成数据传输,最终通过物理硬件(网卡)实现设备间通信。

二、用 Socket 实现客户端与服务器通信
我们先写最基础的 "单客户端 - 服务器" 通信代码:
1. 服务器端(Server)
监听指定端口,阻塞等待客户端连接,收到连接后读取消息:
java
package qcby;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
public class Server {
//服务器端监听的端口
public static final Integer SERVER_PORT = 4477;
public static void main(String[] args) throws Exception {
ServerSocket serverSocket = new ServerSocket(SERVER_PORT);
System.out.println("服务器启动成功,等待客户端连接");
while (true) {
Socket socket = serverSocket.accept(); //阻塞监听
InputStream inputStream = socket.getInputStream(); //获得输入流
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
String msg = bufferedReader.readLine();
System.out.println("收到客户端消息" + msg);
}
}
}
2. 客户端(Click)
连接服务器,发送消息:
java
package qcby;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.Socket;
/**
* 客户端
*/
public class Click {
public static void main(String[] args) throws IOException {
Socket socket = new Socket("127.0.0.1", 4477);
OutputStream outputStream = socket.getOutputStream();
PrintWriter printWriter = new PrintWriter(outputStream);
printWriter.println("你好服务器,我是客户端");
printWriter.flush();
}
问题:只能处理 1 个客户端
此时服务器是 "单线程阻塞" 的 ------ 处理完一个客户端前,其他客户端会一直等待,无法同时通信。
三、引入子线程,支持多客户端
为了解决 "单客户端限制",我们给每个客户端连接分配一个子线程处理:
优化后的服务器(多线程版)
java
package qcby;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
public class Server {
public static final Integer SERVER_PORT = 4477;
public static void main(String[] args) throws Exception {
ServerSocket serverSocket = new ServerSocket(SERVER_PORT);
System.out.println("服务器启动成功,等待客户端连接");
while (true) {
Socket socket = serverSocket.accept(); // 阻塞监听
// 每个连接创建子线程处理
new Thread(new Runnable(){
public void run() {
try{
handler(socket);
}catch (IOException e){
e.printStackTrace();
}
}
}).start();
}
}
// 封装消息处理逻辑
public static void handler(Socket socket) throws IOException {
InputStream inputStream = socket.getInputStream();
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
String msg = bufferedReader.readLine();
System.out.println("收到客户端消息" + msg);
}
}
再优化:将 Socket 封装为线程任务
把 "处理 Socket" 的逻辑封装成独立线程类,代码更整洁:
java
package qcby;
import java.io.*;
import java.net.Socket;
// 封装Socket处理任务的线程类
public class ServerThreadReader extends Thread {
private Socket socket;
public ServerThreadReader(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try{
// 读客户端消息
InputStream inputStream = socket.getInputStream();
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
String msg = bufferedReader.readLine();
System.out.println("收到客户端消息" + msg);
// 给客户端回消息
OutputStream outputStream = socket.getOutputStream();
PrintWriter printWriter = new PrintWriter(outputStream);
printWriter.println("你好客户端,我是服务器,是你给我发的消息吧!!");
printWriter.flush();
} catch(Exception e){
e.printStackTrace();
}
}
}
子线程的问题
虽然支持了多客户端,但频繁创建 / 销毁线程会消耗大量资源:
- 客户端访问量激增时,线程数会 1:1 跟着涨(比如 1000 个客户端 = 1000 个线程);
- 线程过多会导致 "线程泄漏""内存溢出",最终服务器宕机。

四、线程池,解决线程资源浪费
1.任务提交后,先看 "核心线程池" 是否已满;
2.未满:直接用核心线程执行任务;
3.已满:把任务存到阻塞队列;
4.队列满了:再看 "线程池总容量" 是否已满;
5.未满:创建临时线程执行任务;
6.已满:触发 "拒绝策略"(比如抛异常、丢弃任务等)。
线程池是预先创建一批线程,复用它们处理任务的技术
1.线程池工作流程

2.线程池的工作逻辑
1.主线程通过execute()提交任务;
2.核心线程先处理任务,处理完从 "阻塞队列" 里take()新任务;
3.队列满、线程数达上限时,触发RejectedExecutionHandler(拒绝策略)。

3.线程池的实现过程
3.1封装线程池工具类
java
package qcby;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* 自定义Socket连接处理线程池工具类
*/
public class HandleSocketServerPool {
// 线程池核心对象,负责管理线程和任务
private ExecutorService executorService;
/**
* 线程池构造方法,支持自定义核心参数
* @param corePoolSize 核心线程数(常驻线程,即使空闲也不会销毁)
* @param maxThreadNum 最大线程数(核心+临时线程的总数上限)
* @param queueSize 任务队列大小(核心线程满时,任务先存到队列)
* @param keepAliveTime 临时线程空闲存活时间
* @param unit 时间单位(秒/分等)
*/
public HandleSocketServerPool(Integer corePoolSize, int maxThreadNum, int queueSize, Integer keepAliveTime, TimeUnit unit) {
// 初始化线程池:使用ArrayBlockingQueue作为有界任务队列,避免队列无限膨胀
executorService = new ThreadPoolExecutor(
corePoolSize,
maxThreadNum,
keepAliveTime,
unit,
new ArrayBlockingQueue<>(queueSize), // 有界队列,控制任务积压
new ThreadPoolExecutor.AbortPolicy() // 拒绝策略:队列满+线程数达上限时抛异常
);
}
/**
* 提交Socket处理任务到线程池
* @param task 实现Runnable的任务(封装Socket处理逻辑)
*/
public void excute(Runnable task) {
executorService.execute(task);
}
}
3.2线程池核心参数说明
| 参数 | 作用 | 本次示例配置(5,10,10,120,TimeUnit.SECONDS) |
|---|---|---|
| corePoolSize(核心线程数) | 常驻线程数量,即使空闲也不会销毁 | 5(始终保留 5 个线程处理任务) |
| maxThreadNum(最大线程数) | 核心 + 临时线程的总数上限 | 10(最多创建 10 个线程) |
| queueSize(队列大小) | 核心线程满时,任务先存入队列等待 | 10(队列最多存 10 个待处理任务) |
| keepAliveTime(空闲时间) | 临时线程空闲超过该时间会被销毁 | 120 秒(临时线程空闲 2 分钟销毁) |
| unit(时间单位) | 配合 keepAliveTime 使用 | 秒 |
| 拒绝策略 | 队列满 + 线程数达上限时的处理方式 | AbortPolicy(抛异常,可替换为 Discard/CallerRuns 等) |
3.3 整合线程池的最终版服务器
将线程池工具类整合到服务器,实现高效的多客户端处理:
java
package qcby;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.TimeUnit;
public class Server {
// 服务器监听端口
public static final Integer SERVER_PORT = 4477;
public static void main(String[] args) throws Exception {
ServerSocket serverSocket = new ServerSocket(SERVER_PORT);
System.out.println("服务器启动成功,等待客户端连接");
// 初始化自定义线程池:核心5线程,最大10线程,队列10个任务,临时线程存活2分钟
HandleSocketServerPool socketServerPool = new HandleSocketServerPool(5, 10, 10, 120, TimeUnit.SECONDS);
while (true) {
// 阻塞监听客户端连接
Socket socket = serverSocket.accept();
// 将Socket封装为任务,提交到线程池处理
Runnable task = new ServerThreadReader(socket);
socketServerPool.excute(task);
System.out.println("新客户端连接已提交到线程池处理");
}
}
}
4. 线程池的优势
1.线程复用:核心线程常驻,避免频繁创建 / 销毁线程的性能开销;
2.资源可控:通过核心参数限制线程数和任务队列大小,防止服务器资源耗尽;
3.高并发支持:相比 "一个客户端一个线程",线程池能支撑数倍的客户端连接;
4.可维护性:封装成工具类后,线程池参数可统一调整,便于线上运维。