前言
在计算机网络中,进程间的跨设备通信离不开核心技术支撑,而 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 运行说明:
- 先启动
Server类,控制台输出 "服务器启动成功,等待客户端连接..." - 再启动
Click类,客户端发送消息后,服务器端会打印 "收到客户端消息:你好服务器,我是客户端" - 注意:基础版仅支持单向通信(客户端→服务器),且只能处理一个客户端连接,处理完后程序终止。
三、引入子线程
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 请求连接")
- 服务器端会打印所有客户端的消息,并分别响应每个客户端
- 客户端能接收服务器的针对性响应,实现多客户端并发双向通信
四、线程池
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拒绝策略选型(根据业务场景选择)
AbortPolicy:丢弃任务并抛出RejectedExecutionException异常(适用于核心业务,需感知任务丢失)DiscardPolicy:丢弃任务但不抛出异常(适用于非核心业务,允许任务丢失)DiscardOldestPolicy:丢弃队列最前面的任务,重新尝试提交当前任务(适用于任务时效性要求高的场景)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 秒),避免线程因客户端长时间不发送数据而阻塞 - 示例:在
SocketHandlerTask的run()方法中添加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 基础概念出发,逐步实现了 **"单向通信→多客户端并发双向通信→线程池优化"**的完整链路,代码可直接用于实际开发,核心亮点:
- 层层递进:从基础到优化,每个阶段都解决明确的问题,符合学习和开发逻辑
- 代码完整:所有代码可运行,包含异常处理、资源释放等细节
- 工业级标准:线程池优化 + 细节补充,满足高并发、高可用需求
扩展方向
- 实现 TCP 粘包 / 拆包处理(适用于大数据传输场景,可通过 "消息长度 + 消息内容" 格式解决)
- 基于 Socket 实现文件传输功能(通过流读取文件字节,分段发送)
- 结合 NIO 实现非阻塞 Socket 通信,进一步提升并发性能(适用于十万级并发场景)
通过本文的学习,不仅能掌握 Socket 编程的核心技能,还能理解 "问题→解决方案→优化" 的开发思路,为后续分布式系统、即时通讯等高级应用打下基础。