目录

1、什么是socket?

1.1 认识

1.2 定义

1.3 Socket的基本概念

1.4 Socket的工作原理

1.5 Socket的类型

1.6 关于TCP和UDP协议:

1.7 socket在OSI 七层模型的体现​编辑

1.8 底层解读:

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

3、引入子线程

3.1 子线程优化背景

3.2 问题分析

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

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

3.5 基础版客户端代码:

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

3.7 运行效果

4、线程池

4.1 子线程方案的局限性

4.2 解决方案:线程池技术

线程池核心参数(Java ThreadPoolExecutor)

4.2.1 线程池:

4.2.2 参数的含义:

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

5、工业级优化补充

5.1 资源释放优化

5.2 超时设置

5.3 日志替代打印

5.4 端口占用处理

5.5 配置化管理

6、总结与扩展

扩展方向

1、什么是socket?

1.1 认识

在计算机网络的进程通信中,Socket(套接字)是当之无愧的核心技术 —— 它像一座封装了底层协议复杂性的桥梁,让开发者无需关注 TCP 三次握手、数据分片等细节,仅通过简单 API 就能实现跨设备的数据传输。小到即时通讯工具,大到分布式系统,Socket 都是底层通信的基石

1.2 定义

Socket(套接字)是计算机网络中进行通信的一种基本技术,它允许不同计算机上的程序通过网络进行数据交换。Socket本质上是一个通信端点,为网络通信提供了编程接口。

1.3 Socket的基本概念

Socket可以理解为网络通信的"插座",它为应用程序提供了与网络协议栈交互的编程接口。在网络编程中有以下特性:

通信端点:每个Socket代表一个通信的端点,包含IP地址和端口号

数据传输通道:在建立连接后,Socket提供了双向的数据传输通道

协议抽象层:Socket抽象了底层网络协议的复杂性,为开发者提供统一接口

具体解释为:

  • IP 地址:定位目标设备(如 127.0.0.1 是本地设备);
  • 端口号:定位设备上的目标进程(如 4477 端口对应 Socket 服务器进程);
  • 传输协议:定义数据传输规则(TCP 可靠传输,UDP 快速传输)。

形象类比:Socket 就像「快递配送系统」——IP 是收货地址,端口是具体的房间号,协议是配送规则(如顺丰次日达 / TCP 可靠,普通快递 / UDP 快速),三者结合才能让数据精准、有序地送达目标进程。

1.4 Socket的工作原理

Socket通信通常遵循客户端-服务器模型:

  1. 服务器端

    • 创建Socket并绑定到特定IP地址和端口
    • 监听连接请求
    • 接受客户端连接
    • 进行数据交换
  2. 客户端

    • 创建Socket
    • 连接到服务器指定的IP和端口
    • 进行数据交换

Socket 通信需遵循固定流程,缺一不可

  1. 服务器端:创建 Socket → 绑定端口(bind)→ 监听连接(listen)→ 接受连接(accept)→ 读写数据(read/write)→ 关闭连接(close);
  2. 客户端:创建 Socket → 连接服务器(connect)→ 读写数据(read/write)→ 关闭连接(close)。

关键区别:服务器端需「绑定端口 + 监听」,客户端直接「连接目标地址」,这也是「先启动服务器,再启动客户端」的核心原因。

1.5 Socket的类型

主要有两种类型的Socket:

     1.流式Socket(SOCK_STREAM):

        一种面向连接的可靠通信,使用TCP协议,保证数据按序到达且不丢失,常用于Web浏览、文件传输等场景
      2.数据报Socket(SOCK_DGRAM):

        一种无连接的不可靠通信,使用UDP协议,不保证数据到达顺序或是否到达,常用于视频流、在线游戏等实时性要求高的场景

1.6 关于TCP和UDP协议:

UDP协议:

  1. 基本特性:

    • 无连接:通信前不需要建立连接,直接发送数据
    • 不可靠:不保证数据包顺序和完整性,可能丢失或重复
    • 简单高效:头部开销小(仅8字节),传输效率高
  2. 应用场景:

    • 实时音视频传输(如Zoom、Skype)
    • DNS查询
    • 在线游戏
    • SNMP网络管理
    • DHCP动态主机配置
  3. 优缺点:

    • 优点:延迟低、开销小、适合实时应用
    • 缺点:不保证可靠性、可能丢包、无序到达
  4. 相关协议:

    • 基于UDP的上层协议:RTP、QUIC、TFTP等
    • 替代方案:当需要可靠性时可用TCP或QUIC协议

TCP协议:

  1. 基本特性:

    • 面向连接:在数据传输前需要建立连接(三次握手),传输结束后需要释放连接(四次挥手)。
    • 可靠性保证:使用确认应答机制(ACK)、超时重传机制、数据包排序、流量控制和拥塞控制。
    • 全双工通信:允许双方同时发送和接收数据
    • 基于字节流:将数据视为无结构的字节序列
  2. 应用场景:

    • 数据库连接
    • SSH远程登录
    • SMTP/POP3/IMAP电子邮件
    • FTP文件传输

上图为TCP的三次握手和四次挥手的图解。其中在四次挥手中,第三阶段的服务器处理完数据后回向客户端发送确定取消信息!

TCP与UDP的区别:

特性 TCP UDP
连接方式 面向连接 无连接
可靠性 可靠 不可靠
传输顺序 保证顺序 不保证顺序
流量控制
传输效率 相对较低 相对较高
头部大小 20-60字节 8字节
适用场景 可靠传输需求 实时性要求高

1.7 socket在OSI 七层模型的体现

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

层级 所属范畴 核心功能 与 Socket 的关系
应用层 应用程序当中 产生数据,提供 HTTP/DNS 等协议(如浏览器请求网页) 调用 Socket 接口传递数据
表示层 应用程序当中 数据加密、压缩、格式转换(如 SSL 加密、ZIP 压缩) 对 Socket 传输的数据预处理
会话层 应用程序当中 建立、管理、断开通信会话(如视频通话连接) Socket 依赖会话层维护连接状态
传输层 操作系统 端到端可靠传输(TCP 有序重传、UDP 无连接快速传输) Socket 直接封装传输层协议(TCP/UDP)
网络层 操作系统 通过 IP 地址路由数据(如 IPv4 寻址) Socket 无需关注,由操作系统处理
数据链路层 操作系统 局域网内设备通信(如 MAC 地址识别) 底层依赖,Socket 无感知
物理层 物理硬件 电 / 光信号传输(如网线、光纤) 硬件支撑,与 Socket 无关

核心结论:Socket 介于「应用层」与「传输层」之间,向上提供标准化 API(创建、绑定、监听等),向下封装 TCP/UDP 协议细节,是应用程序与网络传输的「衔接枢纽」。

1.8 底层解读:

从操作上来说,无非就是两端之间互相收发数据,对应的就是读数据和写数据,所以从编程的角度来看,可以考虑一个数据结构,而这个数据结构就是sock,由于接收端和发送端可能不止一个,因此引入ip和端口进行区分,ip用来指定是哪台电脑,端口用来指定是哪个进程。另外传输方法可以是可靠的TCP协议,也可以是不可靠的UDP协议,以及还需要支持ICMP协议的ping命令!虽然这些协议各不相同,但是有公共的部分可以提取出来,通过继承的方式复用功能,进而引出sock这个最基础的数据结构,用来维护这些协议都可能会用到的收发数据缓冲区!inet_sock是网络传输功能的sock,在sock的基础上加上了TTL,ip地址,端口这些跟网络传输相关的字段信息。unix domain socket是用于本机进程之间的通信,直接读写文件,不需要经过网络协议栈,而inet_connection_sock是指面向连接的sock,在inet_sock上加上面向连接的协议里相关字段,比如accept队列,数据包分片大小,握手失败,重试次数等,虽然目前所说的面向连接协议就是指TCP,但是从设计上来说Linux需要支持扩展其他面向连接的新协议---比如SCTP协议。tcp_sock就是正儿八经的TCP协议的专用的sock结构,在inet_connection_sock的基础上还加入了TCP特有的滚动窗口、拥塞避免等功能。现在了解了这套数据结构,每个数据结构实现自己职责范围之内的功能,然后再将它们跟硬件网卡对接,就实现了网络传输的功能!考虑到性能和安全,所以将它们放在操作系统内核里面。有句话叫做Linux里一切皆是文件,且读文件和写文件的思想与读数据和写数据一致,就将sock结构封装成文件就好了,在创建sock的同时也创建一个文件,文件有个句柄fd(可以理解为文件系统里面的身份证号码),通过它可以唯一确定是哪个sock。将fd句柄暴露给用户之后,用户操作这个句柄文件,系统就会将操作指向内核sock结构,有了sock fd句柄之后,我们将这部分功能抽象成一个个简单的API接口,让用户更方便的实现 特定的网络编程功能,以后其他人只需要调用这些API接口,就可以驱动我们写好的这一大堆复杂的数据结构去发送数据,这些API我们发现需要send、recv、bind、listen、connect...方法,到这里我们的内核就算设计完成了。所以说socket就是一个代码库或者接口层,它介于内核和应用程序之间,提供了一些高度封装过的接口,让我们使用内核网络的传输功能,所以我们平时写的应用程序的代码里,虽然用了socket实现了收发数据包的功能,但其实真正执行网络通讯功能的不是应用程序,而是Linux内核,相当于应用程序通过socket提供的接口将网络传输的这部分工作外包给了Linux内核。这个socket_fd是int类型的数字,现在回看中文翻译Socket(套接字)可以理解为一套用于连接的数字。

总结一下:在操作系统内核空间里面,实现网络传输功能的结构是sock,基于不同的协议和应用场景有socket会被泛化为各种类型的sock,它们结合硬件共同实现了网络传输功能,为了将这部分功能暴露给用户空间的应用程序使用,于是引入了socket层,同时将sock嵌入文件系统的框架里,sock就变成了一个特殊的文件,用户就可以在用户空间使用句柄,也就是socket fd来操作内核sock的网络传输能力。

那么是怎么具体实现网络传输功能的呢?

以TCP为例,将其分为两个阶段建立连接数据传输,在客户端执行socket提供的connect方法,传入sock_fd和ip端口

connect(句柄fd,IP:port);

内核会通过sock_fd句柄找到对应的文件,再根据文件里面的信息,找到内核的sock结构,通过这个sock结构主动发起3次握手,至此就算连接好了。

接着就可以传输数据了,为了实现发送和接收数据的功能,sock结构体里面是有一个发送缓冲区和一个接收缓冲区,说清楚点就是一个链表,上面挂着一个个准备要发送/接收的数据。

当应用执行send方法发送数据时,同样也会通过sock_fd句柄找到对应的文件,根据文件里面的信息,找到内核的sock结构,再找到这个sock结构里面的发送缓冲区,将数据放到发送缓冲区,

send(句柄fd,消息);

然后结束流程,内核看心情选择时间将这份数据发送出去。接收数据流程也类似,当数据送到Linux内核后,数据不是立马给到应用程序的,而是先放到接收缓冲区中,数据静静躺着,等待应用程序什么时候执行recv方法来带走它。那么问题又来了,服务端listen的时候,那么多数据包发送到一个listen socket上,服务端是如何区分多个客户端的?因为客户端发来的会有源ip和端口以及目的ip和端口,这四个元素构成一个四元组,可以唯一标记一个客户端,服务端会创建一个新的sock,并用四元组生成一个hash key将它放进一个hash表里面。

下次再有消息进来的时候,通过消息自带的四元组生成hash_key,再到这个hash表里面重新取出对应的sock就好了,所以说服务端是通过四元组来区分多个客户端的。那么Linux是基于c语言实现的,c是怎么实现继承关系的呢?可以利用套娃结构体、魔法指针转换(container_of 宏)、函数指针当虚函数。

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

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

2.1 服务器端代码实现


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 客户端代码实现

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、引入子线程

3.1 子线程优化背景

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

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

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

3.2 问题分析

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

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

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

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

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 基础版客户端代码:

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 支持自定义消息 + 双向交互的改造

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

服务器端:

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();//刷新
    }
}

客户端:

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、线程池

4.1 子线程方案的局限性

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

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

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

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

4.2 解决方案:线程池技术

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

线程池核心参数(Java ThreadPoolExecutor)

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

4.2.1 线程池:

对于java线程池ThreadPoolExecutor:

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)创建线程池:

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对象封装成任务

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)服务器

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)客户端

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、工业级优化补充

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

5.1 资源释放优化

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

5.2 超时设置

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

5.3 日志替代打印

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

5.4 端口占用处理

  • 服务器端启动时若端口被占用,会抛出 BindException,需添加异常处理,提示用户更换端口或释放占用
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

6、总结与扩展

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

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

扩展方向

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

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

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐