手写tomcat(1):Socket

一、什么是 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.可维护性:封装成工具类后,线程池参数可统一调整,便于线上运维。

相关推荐
PPPPickup1 小时前
easychat---创建,获取,获取详细,退群,解散,添加与移除群组
java·开发语言·后端·maven
luod1 小时前
SpringBoot自动初始化数据
java·spring boot·spring
牛顿没有错1 小时前
lombok中@Data、@AllArgsConstructor、@NoArgsConstructor不生效
java·spring boot·spring·intellij-idea
南部余额1 小时前
深入理解 Spring Boot:自动化配置类与 FactoryBean 的异同与协作
java·spring boot·自动化
摇滚侠2 小时前
2025最新 SpringCloud 教程,熔断规则-熔断策略-异常数,笔记46
java·笔记·spring cloud
Home2 小时前
23 种设计模式--桥接(Bridge)模式(结构型模式二)
java·后端
摇滚侠2 小时前
2025最新 SpringCloud 教程,熔断规则-熔断策略-慢调用比例,笔记44
java·笔记·spring cloud
s***11702 小时前
使用rustDesk搭建私有远程桌面
java
编程修仙2 小时前
第九篇 Spring中的代理思想
java·后端·spring