从入门到精通:Java Socket 网络编程实战(含线程池优化)

前言

在计算机网络中,进程间的跨设备通信离不开核心技术支撑,而 Socket(套接字)正是这一过程的关键抽象。它像一座桥梁,封装了网络协议栈的复杂细节,让开发者通过简单的 API 就能实现客户端与服务器端的高效数据传输。本文将从 Socket 基础概念出发,逐步实现通信功能、解决并发问题,并通过线程池优化达到工业级应用标准,全程附完整可运行代码。

一、什么是socket?

在开始之前,我们需要对计算机网络有一定的认识!这是一种端与端之间的联系。

这个图展示的是OSI 七层模型,它是网络通信的分层框架,各层功能对应图里的分类(应用程序、操作系统、物理硬件),具体解释如下:

1. 应用层(属于 "应用程序当中")
  • 功能:直接和用户 / 应用程序交互,产生要传输的数据,同时提供常用的网络协议(比如图里的 HTTP、DNS)。
  • 例子:浏览器用 HTTP 访问网页、手机 APP 用 DNS 解析网址。
2. 表示层(属于 "应用程序当中")
  • 功能:对数据做 "格式处理",比如加密、压缩、格式转换(把程序里的数据转成网络能传的格式)。
  • 例子:传输文件时压缩数据、用 SSL 加密聊天内容。
3. 会话层(属于 "应用程序当中")
  • 功能:建立、管理、断开通信双方的 "会话连接"(可以理解为 "通话链路")。
  • 例子:视频通话时维持双方的连接、断连后重新建立会话。
4. 传输层(属于 "操作系统")
  • 功能:负责端到端的可靠传输,比如把数据分成 "段",控制传输顺序、重传丢失的部分。
  • 核心协议:TCP(可靠传输)、UDP(快速但不可靠)。
5. 网络层(属于 "操作系统")
  • 功能:找 "路径",把数据分成 "包",通过 IP 地址选择从源设备到目标设备的最佳传输路线。
  • 核心协议:IP(比如 IPv4、IPv6)。
6. 数据链路层(属于 "操作系统")
  • 功能:处理物理设备的 "相邻连接",把数据分成 "帧",负责同一网络内设备间的传输(比如局域网内的设备通信)。
  • 例子:用 MAC 地址识别同一网络里的设备。
7. 物理层(属于 "物理硬件")
  • 功能:负责 "物理信号传输",把数据转成电信号、光信号等,通过网线、光纤等硬件传递。
  • 例子:网线传输电信号、光纤传输光信号。

Socket 直译 "套接字",是网络编程中的端点抽象,用于进程间通过网络(或本地)传输数据。它的核心价值在于 "封装"------ 将 TCP/IP 协议栈的底层逻辑(如三次握手、数据分片、重传等)隐藏,对外提供一套标准化操作接口:

  • 核心操作:创建、绑定、监听、连接、读写、关闭
  • 应用场景:跨设备进程通信(如客户端 - 服务器交互、即时通讯等)
  • 分层定位:介于应用层与传输层之间,承接应用数据与网络传输的衔接工作

形象地说,Socket 就像电话的 "听筒 + 话筒",进程通过它 "拨通" 目标地址,实现数据的 "听"(接收)与 "说"(发送),而无需关心通信线路的底层原理。

二、基础实现:客户端与服务器端单向通信

要实现 Socket 通信,需遵循 "先启动服务器,再连接客户端" 的原则,核心是通过输入流(InputStream)和输出流(OutputStream)传递数据。

2.1 服务器端代码实现

java 复制代码
import java.io.*;
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 IOException {
        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.2 客户端代码实现

java 复制代码
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;

public class Click {
    public static void main(String[] args) throws IOException {
        //http://ip:端口号
        Socket socket = new Socket("127.0.0.1",4477);

        OutputStream outputStream = socket.getOutputStream();
        PrintWriter printWriter = new PrintWriter(outputStream);
        printWriter.println("你好服务器,我是客户端");
        printWriter.flush();//刷新
    }
}

2.3 核心注意事项 **:**要先启动服务器端

2.4 运行说明:

  1. 先启动 Server 类,控制台输出 "服务器启动成功,等待客户端连接..."
  2. 再启动 Click 类,客户端发送消息后,服务器端会打印 "收到客户端消息:你好服务器,我是客户端"
  3. 注意:基础版仅支持单向通信(客户端→服务器),且只能处理一个客户端连接,处理完后程序终止。

三、引入子线程

3.1 子线程优化背景

在上面的代码里面我们发现一个问题,在main线程里面是主线程,既要监听,又要处理。

假如有很多很多的客户端,有可能发送信息很快很快,这个时候main线程就没办法监听了,因为有些还没处理完,新的就来了,会导致数据的丢失,进而子线程用于处理。

基础版的核心缺陷是 "单线程瓶颈"------ 主线程既要监听客户端连接,又要处理数据读写,当多个客户端同时接入时,后续客户端会被阻塞,甚至导致数据丢失。

3.2 问题分析

  • 主线程执行 accept() 阻塞监听,接入一个客户端后,会进入数据读取流程
  • 此时若有第二个客户端发起连接,需等待第一个客户端处理完成,才能被 accept() 接收
  • 高并发场景下,会出现 "连接超时""消息丢失" 等问题

3.3 解决方案:子线程异步处理

让主线程仅负责 "监听连接",每接入一个客户端,就创建一个子线程专门处理该客户端的数据读写,实现 "并发处理多客户端"。

3.4 服务器端子线程改造代码

java 复制代码
import java.io.*;
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 IOException {
        ServerSocket serverSocket = new ServerSocket(SERVER_PORT);
        System.out.println("服务器启动成功,等待客户端连接。。。");

        while(true){
            Socket socket = serverSocket.accept();//阻塞监听 --- 等待客户端信息发送
            //创建子线程
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        handler(socket);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }).start();//.start();进入就绪队列,等待cpu调度,人为不能控制
        }
    }

    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);
    }
}

3.5 基础版客户端代码:

java 复制代码
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;

public class Click {
    public static void main(String[] args) throws IOException {
        //http://ip:端口号
        Socket socket = new Socket("127.0.0.1",4477);

        OutputStream outputStream = socket.getOutputStream();
        PrintWriter printWriter = new PrintWriter(outputStream);
        printWriter.println("你好服务器,我是客户端");
        printWriter.flush();//刷新
    }
}

3.6 支持自定义消息 + 双向交互的改造

实际应用中需要 "客户端→服务器""服务器→客户端" 的双向通信,同时支持客户端自定义发送消息,优化客户端代码并增强服务器端响应逻辑:

服务器端:

java 复制代码
import java.io.*;
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 IOException {
        ServerSocket serverSocket = new ServerSocket(SERVER_PORT);
        System.out.println("服务器启动成功,等待客户端连接。。。");

        while(true){
            Socket socket = serverSocket.accept();//阻塞监听 --- 等待客户端信息发送
            //创建子线程
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        handler(socket);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }).start();//.start();进入就绪队列,等待cpu调度,人为不能控制
        }
    }

    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);

        OutputStream outputStream = socket.getOutputStream();
        PrintWriter printWriter = new PrintWriter(outputStream);
        printWriter.println("你好客户端,我是服务器端,是你找我吧!");
        printWriter.flush();//刷新
    }
}

客户端:

java 复制代码
import java.io.*;
import java.net.Socket;
import java.util.Scanner;

public class Click {
    public static void main(String[] args) throws IOException {
        //http://ip:8080
        Socket socket = new Socket("127.0.0.1",4477);
        //通过键盘输入
        Scanner scanner = new Scanner(System.in);
        String sendMessage = scanner.nextLine();

        OutputStream outputStream = socket.getOutputStream();
        PrintWriter printWriter = new PrintWriter(outputStream);
        printWriter.println(sendMessage);
        printWriter.flush();//刷新

        InputStream inputStream = socket.getInputStream();//获取输入流
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
        String msg = bufferedReader.readLine();//转化为字符串
        System.out.println("收到服务器端消息:"+msg);
    }
}

3.7 运行效果

  1. 启动服务器,控制台输出 "服务器启动成功,等待客户端连接..."
  2. 启动多个客户端,每个客户端输入自定义消息(如 "客户端 1 请求连接")
  3. 服务器端会打印所有客户端的消息,并分别响应每个客户端
  4. 客户端能接收服务器的针对性响应,实现多客户端并发双向通信

四、线程池

4.1 子线程方案的局限性

但是开启子线程的问题比较大,会出现以下问题:

1.每次接收到客户端发来的数据,就会创建一个新的线程,线程的创建和销毁都会消耗计算机资源

2.客户端的访问量增多时,服务器和客户端的线程比是1:1,访问量多,则线程增加,线程多了可能会导致线程创建失败......,最终导致服务器宕机!!!

子线程处理输入和输出流。

4.2 解决方案:线程池技术

线程池是 "线程的容器",核心思想是线程复用------ 提前创建一定数量的核心线程,客户端请求到来时直接复用线程,请求结束后线程不销毁,而是放回池中等待下一个任务,从而减少线程创建 / 销毁的开销。

线程池核心参数(Java ThreadPoolExecutor)

Java 提供 ThreadPoolExecutor 类实现线程池,核心参数决定了线程池的运行机制:

4.2.1 线程池:

对于java线程池ThreadPoolExecutor:

java 复制代码
public ThreadPoolExecutor(
            int corePoolSize, 
            int maximumPoolSize,
            long keepAliveTime, 
            TimeUnit unit,
            BlockingQueue<Runnable> workQueue,
            RejectedExecutionHandler handler
    }

4.2.2 参数的含义:

corePoolSize:核心线程数。规定线程池有几个线程(worker)在运行。

maximumPoolSize:最大线程数。当workQueue满了,不能添加任务的时候,这个参数才会生效。规定线程池最多只能有多少个线程(work)在执行。

keepAliveTime:超出corePoolSize大小的那些线程的生存时间,这些线程如果长时间没有执行任务并且超过了keepAliveTime设定的时间,就会消亡。

unit:生产时间的单位。

workQueue:阻塞队列。存放任务的队列。

handler :当workQueue已经满了,并且线程池数已经达到**maximumPoolSize,**将执行拒绝策略。

4.2.3拒绝策略选型(根据业务场景选择)

  1. AbortPolicy:丢弃任务并抛出 RejectedExecutionException 异常(适用于核心业务,需感知任务丢失)
  2. DiscardPolicy:丢弃任务但不抛出异常(适用于非核心业务,允许任务丢失)
  3. DiscardOldestPolicy:丢弃队列最前面的任务,重新尝试提交当前任务(适用于任务时效性要求高的场景)
  4. CallerRunsPolicy:由提交任务的线程(如主线程)自己执行任务(适用于不想丢失任务,且并发量可控的场景)

调整后利用线程池的代码:

(1)创建线程池:

java 复制代码
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * 线程池
 */
public class HandleSocketServletPool {

    private ExecutorService executorService;//线程池对象   jdk提供的线程池对象

    /**
     * 创建线程池
     * @param corePoolSize
     * @param maxThreadNum
     * @param queueSize
     * @param keepAliveTime
     * @param unit
     */
    public HandleSocketServletPool(Integer corePoolSize, int maxThreadNum,int queueSize, Integer keepAliveTime, TimeUnit unit) {
        executorService = new ThreadPoolExecutor(corePoolSize,maxThreadNum,keepAliveTime,unit,
                new ArrayBlockingQueue<Runnable>(queueSize));
    }

    /**
     * 任务提交的方法
     * @param task
     */
    public void execte(Runnable task){
        executorService.execute(task);
    }
}

(2)将socket对象封装成任务

java 复制代码
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 (IOException e) {
            e.printStackTrace();
        }


    }
}

(3)服务器

java 复制代码
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 IOException {
        ServerSocket serverSocket = new ServerSocket(SERVER_PORT);
        System.out.println("服务器启动成功,等待客户端连接。。。");
        //定义线程池
        HandleSocketServletPool socketServletPool = new HandleSocketServletPool(5, 10, 10, 120, TimeUnit.SECONDS);
        while (true) {
            Socket socket = serverSocket.accept();//阻塞监听
            ServerThreadReader task = new ServerThreadReader(socket);//将socket对象封装成任务
            socketServletPool.execte(task);//提交任务,让线程池执行
        }
    }
}

(4)客户端

java 复制代码
import java.io.*;
import java.net.Socket;
import java.util.Scanner;

public class Click {
    public static void main(String[] args) throws IOException {
        //http://ip:8080
        Socket socket = new Socket("127.0.0.1",4477);
        //通过键盘输入
        Scanner scanner = new Scanner(System.in);
        String sendMessage = scanner.nextLine();

        OutputStream outputStream = socket.getOutputStream();
        PrintWriter printWriter = new PrintWriter(outputStream);
        printWriter.println(sendMessage);
        printWriter.flush();//刷新

        InputStream inputStream = socket.getInputStream();//获取输入流
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
        String msg = bufferedReader.readLine();//转化为字符串
        System.out.println("收到服务器端消息:"+msg);
    }
}

五、工业级优化补充

要让代码达到工业级标准,还需补充以下细节优化,解决实际部署中的潜在问题:

5.1 资源释放优化

  • 使用 try-with-resources 语法自动关闭流和 Socket,避免资源泄漏(已在上述代码中实现)
  • 服务器端需处理客户端异常断开场景(如客户端强制关闭时,readLine() 会返回 null,需及时关闭 Socket)

5.2 超时设置

  • socket.setSoTimeout(int timeout) 设置读取超时时间(如 30 秒),避免线程因客户端长时间不发送数据而阻塞
  • 示例:在 SocketHandlerTaskrun() 方法中添加 socket.setSoTimeout(30000);

5.3 日志替代打印

  • SLF4J + Logback 替代 System.out.println,支持日志分级(DEBUG/INFO/ERROR)、日志持久化,便于问题排查
  • 示例:logger.info("收到客户端[{}]消息:{}", socket.getInetAddress(), clientMsg);

5.4 端口占用处理

  • 服务器端启动时若端口被占用,会抛出 BindException,需添加异常处理,提示用户更换端口或释放占用
java 复制代码
try (ServerSocket serverSocket = new ServerSocket(SERVER_PORT)) {
    // 启动逻辑
} catch (BindException e) {
    System.err.println("端口 " + SERVER_PORT + " 已被占用,请释放后重新启动");
    System.exit(1); // 退出程序
}

5.5 配置化管理

  • 将端口号、线程池参数(核心线程数、队列大小等)抽取到配置文件(如 application.properties),避免硬编码,便于动态调整
  • 示例:通过 Properties 类读取配置文件中的 server.port=4477

六、总结与扩展

本文从 Socket 基础概念出发,逐步实现了 **"单向通信→多客户端并发双向通信→线程池优化"**的完整链路,代码可直接用于实际开发,核心亮点:

  1. 层层递进:从基础到优化,每个阶段都解决明确的问题,符合学习和开发逻辑
  2. 代码完整:所有代码可运行,包含异常处理、资源释放等细节
  3. 工业级标准:线程池优化 + 细节补充,满足高并发、高可用需求

扩展方向

  • 实现 TCP 粘包 / 拆包处理(适用于大数据传输场景,可通过 "消息长度 + 消息内容" 格式解决)
  • 基于 Socket 实现文件传输功能(通过流读取文件字节,分段发送)
  • 结合 NIO 实现非阻塞 Socket 通信,进一步提升并发性能(适用于十万级并发场景)

通过本文的学习,不仅能掌握 Socket 编程的核心技能,还能理解 "问题→解决方案→优化" 的开发思路,为后续分布式系统、即时通讯等高级应用打下基础。

相关推荐
BBB努力学习程序设计33 分钟前
Java:理解数据类型和变量
java
亭上秋和景清34 分钟前
数据在内存中的存储
java·开发语言
古城小栈34 分钟前
SpringBoot:声明式事务 和 编程式事务 的擂台霸业
java·spring boot·后端
刘某的Cloud35 分钟前
全局禁用ipv6
linux·运维·网络·系统·ipv6
小二·37 分钟前
Java基础教程之网络编程
java·开发语言·网络
熊文豪38 分钟前
使用Python快速开发一个MCP服务器
服务器·开发语言·python·mcp
herinspace38 分钟前
管家婆软件中如何运用商品副单位
运维·服务器·数据库·windows·电脑
泥嚎泥嚎38 分钟前
【Android】RecyclerView 刷新方式全解析:从 notifyDataSetChanged 到 DiffUtil
android·java
努力学算法的蒟蒻39 分钟前
day23(12.3)——leetcode面试经典150
java