Java TCP 网络编程

一、TCP 协议基础与 Java 核心类

1.1 TCP 协议核心特性

TCP 协议之所以成为可靠通信的首选,源于其三大核心机制:

面向连接

  • 通信前需通过"三次握手"建立可靠连接:
    1. 客户端发送SYN报文(同步序列号)发起连接请求
    2. 服务器回复SYN+ACK报文(确认客户端的SYN并提供自己的序列号)
    3. 客户端回复ACK报文确认服务器的SYN
  • 三次握手确保双方通信通道通畅,防止历史连接请求导致错误
  • 断开连接时采用"四次挥手"机制确保数据完整传输

可靠传输

  • 序列号机制:每个字节都有唯一序列号,接收方可以按序重组数据
  • 确认应答(ACK):接收方必须对收到的数据发送确认
  • 重传机制:发送方启动定时器,超时未收到ACK则重发数据包
  • 流量控制:通过滑动窗口动态调整发送速率,避免接收方缓冲区溢出
  • 拥塞控制:采用慢启动、拥塞避免、快速重传等算法适应网络状况
  • 通过这些机制确保数据不丢失、不重复、按序到达

字节流服务

  • TCP将数据视为连续的字节流,不保留应用层消息边界
  • 这是"粘包"问题的根源:多个应用层消息可能被合并为一个TCP包传输
  • 解决方案:应用层需要定义消息分割规则,常见方法包括:
    • 固定长度分隔
    • 特殊字符分隔(如换行符)
    • 消息头+消息体(头部包含消息长度)

1.2 Java TCP 编程核心类

Java通过java.net包提供TCP编程核心API,核心类仅有两个,职责明确:

Socket类(客户端)

  • 代表客户端TCP连接端点
  • 关键方法和功能:
    • Socket(String host, int port):创建并连接到指定主机和端口
    • getInputStream():获取输入流读取服务器数据
    • getOutputStream():获取输出流向服务器发送数据
    • setSoTimeout(int timeout):设置读取超时时间(毫秒)
    • close():关闭连接释放资源
  • 典型使用场景:
// 客户端示例
try (Socket socket = new Socket("127.0.0.1", 8080)) {
    OutputStream out = socket.getOutputStream();
    InputStream in = socket.getInputStream();
    // 读写数据...
} catch (IOException e) {
    e.printStackTrace();
}

ServerSocket类(服务器端)

  • 代表服务器端监听套接字
  • 关键方法和功能:
    • ServerSocket(int port):绑定到指定端口开始监听
    • accept():阻塞等待客户端连接,返回Socket对象
    • setSoTimeout(int timeout):设置accept超时时间
    • close():停止监听释放资源
  • 典型使用场景:
// 服务器端示例
try (ServerSocket server = new ServerSocket(8080)) {
    while (true) {
        Socket client = server.accept();  // 等待客户端连接
        new Thread(() -> {
            try (InputStream in = client.getInputStream();
                 OutputStream out = client.getOutputStream()) {
                // 处理客户端请求...
            } catch (IOException e) {
                e.printStackTrace();
            }
        }).start();
    }
} catch (IOException e) {
    e.printStackTrace();
}

重要说明

  1. Socket是"双向通道":既可以通过输出流发送数据,也可以通过输入流接收数据
  2. ServerSocket仅负责"开门":不直接处理数据,真正的通信由accept()返回的Socket完成
  3. 资源管理:必须使用try-with-resources或finally块确保关闭连接
  4. 多线程处理:服务器通常需要为每个客户端连接创建新线程处理,避免阻塞其他连接

二、Java TCP 编程核心知识点

Java TCP 编程分为客户端服务器端两大模块,两者流程不同但相互依赖

2.1 客户端流程

客户端的核心目标是实现 "连接服务器 → 交换数据 → 关闭连接" 的标准TCP通信流程。该流程适用于大多数客户端开发场景,如即时通讯、文件传输、远程控制等。以下是详细步骤说明:


步骤 1:创建 Socket 并建立连接

  • 实现方式:通过 Socket 类构造方法 Socket(String host, int port) 指定服务器IP和端口
  • 连接成功:TCP三次握手完成,进入可通信状态
  • 连接失败:可能由于以下原因抛出 IOException
    • 服务器未启动(Connection refused
    • 网络不可达(No route to host
    • 端口错误(Port unreachable
    • 防火墙拦截
  • 调试建议:先用 telnetping 测试网络连通性

步骤 2:获取 IO 流并交换数据

  • 底层机制:TCP基于字节流传输,需处理以下关键点:
    • 流的获取
      InputStream in = socket.getInputStream();  // 接收数据流
      OutputStream out = socket.getOutputStream(); // 发送数据流
      

    • 流包装(推荐方案):
      • 字符流包装(需显式指定编码,如UTF-8):
        BufferedReader reader = new BufferedReader(
            new InputStreamReader(in, "UTF-8"));
        PrintWriter writer = new PrintWriter(
            new OutputStreamWriter(out, "UTF-8"), true); // autoFlush
        

      • 二进制数据包装(文件传输场景):
        BufferedInputStream bin = new BufferedInputStream(in);
        BufferedOutputStream bout = new BufferedOutputStream(out);
        

  • 通信模式
    • 短连接:单次请求-响应后立即关闭(如HTTP)
    • 长连接:保持连接持续通信(如WebSocket)

步骤 3:关闭资源(关键!)

  • 关闭顺序:必须遵循 "先打开的后关闭" 原则:
    1. 关闭所有IO流
    2. 最后关闭Socket
  • 推荐方案
    • try-with-resources(Java 7+):
      try (Socket socket = new Socket(ip, port);
           InputStream in = socket.getInputStream();
           OutputStream out = socket.getOutputStream()) {
          // 通信代码...
      } // 自动调用close()
      

    • 传统方式(需finally块):
      try {
          // 通信代码...
      } finally {
          if (out != null) out.close();
          if (in != null) in.close();
          if (socket != null) socket.close();
      }
      

  • 资源泄漏风险
    • 未关闭Socket会导致端口占用(TIME_WAIT状态)
    • 未关闭流可能导致内存泄漏

客户端示例代码(增强版)

import java.io.*;
import java.net.Socket;
import java.nio.charset.StandardCharsets;

/**
 * 增强版TCP客户端示例:
 * 1. 使用try-with-resources确保资源释放
 * 2. 支持控制台交互式通信
 * 3. 添加心跳检测机制(每5秒发送PING)
 */
public class EnhancedTcpClient {
    private static final int HEARTBEAT_INTERVAL = 5000; // 心跳间隔5秒

    public static void main(String[] args) {
        final String serverIp = "127.0.0.1"; // 本地回环地址
        final int serverPort = 8888;         // 建议使用1024-49151的注册端口

        try (Socket socket = new Socket(serverIp, serverPort);
             PrintWriter out = new PrintWriter(
                 new OutputStreamWriter(socket.getOutputStream(), 
                     StandardCharsets.UTF_8), true);
             BufferedReader in = new BufferedReader(
                 new InputStreamReader(socket.getInputStream(),
                     StandardCharsets.UTF_8));
             BufferedReader consoleIn = new BufferedReader(
                 new InputStreamReader(System.in,
                     StandardCharsets.UTF_8))) {

            System.out.println("已连接服务器 " + socket.getRemoteSocketAddress());
            
            // 启动心跳线程
            Thread heartbeatThread = new Thread(() -> {
                try {
                    while (!socket.isClosed()) {
                        out.println("PING");
                        Thread.sleep(HEARTBEAT_INTERVAL);
                    }
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            });
            heartbeatThread.setDaemon(true);
            heartbeatThread.start();

            // 主通信循环
            System.out.println("输入消息发送(输入exit退出):");
            String userInput;
            while ((userInput = consoleIn.readLine()) != null) {
                if ("exit".equalsIgnoreCase(userInput.trim())) {
                    out.println("CLIENT_EXIT"); // 发送退出指令
                    break;
                }
                
                // 发送数据并获取响应
                out.println(userInput);
                String response = in.readLine();
                System.out.println("[" + System.currentTimeMillis() + "] 服务器响应: " + response);
            }
        } catch (IOException e) {
            System.err.println("通信异常: " + e.getClass().getSimpleName() + 
                ": " + e.getMessage());
            e.printStackTrace();
        } finally {
            System.out.println("客户端已终止");
        }
    }
}


关键注意事项

  1. 编码问题:必须统一客户端和服务端的字符编码(推荐UTF-8)
  2. 异常处理:需要捕获 SocketTimeoutException(连接超时)、UnknownHostException(域名解析失败)等特定异常
  3. 性能优化
    • 使用缓冲流(BufferedReader/PrintWriter)减少IO操作次数
    • 大数据传输时采用分块(chunked)方式
  4. 网络安全:生产环境应使用SSL/TLS加密(如SSLSocket)

2.2 服务器端流程

服务器端程序的核心目标是实现"监听端口 → 接收客户端连接 → 处理通信 → 关闭资源"的完整流程。其中"处理通信"环节需要结合多线程技术,否则单线程服务器只能串行处理一个客户端,无法满足实际应用需求。

详细步骤说明

步骤 1:创建 ServerSocket 并绑定端口

  1. 使用 ServerSocket 类的构造方法绑定指定端口
    • 示例:ServerSocket serverSocket = new ServerSocket(8888);
    • 端口号范围应在 1024-65535 之间(0-1023 为系统保留端口)
  2. 端口冲突处理
    • 如果端口已被占用(如 8888 端口被其他程序使用),会抛出 BindException
    • 解决方案:更换端口号或终止占用端口的程序

步骤 2:阻塞监听客户端连接

  1. 调用 ServerSocket.accept() 方法
    • 该方法会阻塞当前线程,直到有客户端连接请求到达
    • 返回一个与该客户端对应的 Socket 对象
  2. 连接特性
    • 每个客户端连接都会创建一个独立的 Socket 对象
    • 服务器端通过该 Socket 与特定客户端进行数据交换

步骤 3:处理客户端通信(多线程实现关键)

  1. 单线程模式的局限性
    • 服务器在 accept() 后直接处理当前客户端的通信
    • 在此期间无法接收其他客户端的连接请求
  2. 多线程解决方案
    • 每接收一个客户端连接,就创建一个新线程处理该客户端的 IO 操作
    • 主线程继续监听新的客户端连接
  3. 线程池优化
    • 直接创建线程会导致性能开销
    • 推荐使用线程池管理客户端处理线程
    • 优点:控制并发数、复用线程资源、避免频繁创建销毁线程

步骤 4:关闭资源

  1. 关闭顺序
    • 先关闭所有客户端 Socket 连接
    • 最后关闭 ServerSocket
  2. 资源泄漏风险
    • 必须确保所有 SocketServerSocket 都被正确关闭
    • 推荐使用 try-with-resources 语句自动关闭

服务器端基础版(单线程演示)

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;

/**
 * 单线程服务器演示
 * 特点:只能处理一个客户端,处理完后才能接收下一个
 * 实际应用中不应使用此模式
 */
public class TcpSingleThreadServer {
    public static void main(String[] args) {
        int port = 8888; // 服务器监听端口
        
        // try-with-resources自动关闭ServerSocket
        try (ServerSocket serverSocket = new ServerSocket(port)) {
            System.out.println("服务器已启动,监听端口:" + port + "(单线程,仅支持1个客户端)");
            
            // 步骤2:阻塞等待客户端连接
            Socket clientSocket = serverSocket.accept();
            System.out.println("新客户端连接:" + clientSocket.getInetAddress() + ":" + clientSocket.getPort());
            
            // 步骤3:处理客户端通信(单线程阻塞在此处)
            handleClientCommunication(clientSocket);
        } catch (IOException e) {
            System.err.println("服务器启动/通信异常:" + e.getMessage());
            e.printStackTrace();
        }
    }
    
    /**
     * 处理单个客户端的通信逻辑
     * @param clientSocket 客户端Socket对象
     */
    private static void handleClientCommunication(Socket clientSocket) {
        // try-with-resources自动关闭客户端Socket、输入流、输出流
        try (
            BufferedReader in = new BufferedReader(
                new InputStreamReader(clientSocket.getInputStream(), "UTF-8")
            );
            PrintWriter out = new PrintWriter(
                new OutputStreamWriter(clientSocket.getOutputStream(), "UTF-8"),
                true
            )
        ) {
            String clientMsg;
            // 循环读取客户端消息(客户端关闭则in.readLine()返回null)
            while ((clientMsg = in.readLine()) != null) {
                System.out.println("客户端[" + clientSocket.getPort() + "]发送:" + clientMsg);
                if ("exit".equals(clientMsg)) {
                    out.println("已收到退出请求,断开连接");
                    break; // 退出循环,关闭连接
                }
                // 向客户端发送响应
                out.println("已收到:" + clientMsg);
            }
            System.out.println("客户端[" + clientSocket.getPort() + "]断开连接");
        } catch (IOException e) {
            System.err.println("客户端[" + clientSocket.getPort() + "]通信异常:" + e.getMessage());
            e.printStackTrace();
        }
    }
}

服务器端进阶版(多线程+线程池,生产级实现)

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * 多线程服务器(线程池实现)
 * 特点:支持并发处理多个客户端连接
 * 适用于实际生产环境
 */
public class TcpThreadPoolServer {
    // 线程池配置:固定大小线程池,最大并发处理5个客户端
    private static final ExecutorService THREAD_POOL = Executors.newFixedThreadPool(5);
    
    public static void main(String[] args) {
        int port = 8888;
        
        try (ServerSocket serverSocket = new ServerSocket(port)) {
            System.out.println("多线程服务器已启动,监听端口:" + port + "(支持5个并发客户端)");
            
            // 主线程循环监听客户端连接(无限循环,除非手动关闭服务器)
            while (true) {
                Socket clientSocket = serverSocket.accept(); // 阻塞等待连接
                System.out.println("新客户端连接:" + clientSocket.getInetAddress() + ":" + clientSocket.getPort());
                
                // 将客户端通信任务提交给线程池处理
                THREAD_POOL.submit(() -> handleClientCommunication(clientSocket));
            }
        } catch (IOException e) {
            System.err.println("服务器异常:" + e.getMessage());
            e.printStackTrace();
        } finally {
            // 关闭服务器时关闭线程池(不再接收新任务,等待已提交任务完成)
            THREAD_POOL.shutdown();
            System.out.println("服务器线程池已关闭");
        }
    }
    
    /**
     * 客户端通信处理方法(与单线程版一致)
     * @param clientSocket 客户端Socket对象
     */
    private static void handleClientCommunication(Socket clientSocket) {
        try (
            BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream(), "UTF-8"));
            PrintWriter out = new PrintWriter(new OutputStreamWriter(clientSocket.getOutputStream(), "UTF-8"), true)
        ) {
            String clientMsg;
            while ((clientMsg = in.readLine()) != null) {
                System.out.println("客户端[" + clientSocket.getPort() + "]:" + clientMsg);
                if ("exit".equals(clientMsg)) {
                    out.println("已断开连接");
                    break;
                }
                out.println("服务器响应:" + clientMsg);
            }
            System.out.println("客户端[" + clientSocket.getPort() + "]断开");
        } catch (IOException e) {
            System.err.println("客户端[" + clientSocket.getPort() + "]异常:" + e.getMessage());
        }
    }
}

生产环境扩展建议

  1. 线程池优化

    • 根据服务器硬件配置调整线程池大小
    • 考虑使用 ThreadPoolExecutor 自定义线程池参数
  2. 异常处理

    • 添加更完善的异常处理机制
    • 记录详细的错误日志
  3. 性能监控

    • 添加连接数统计功能
    • 实现服务器负载监控
  4. 安全考虑

    • 添加客户端认证机制
    • 实现通信数据加密
  5. 资源管理

    • 设置连接超时时间
    • 实现空闲连接自动回收

2.3 关键扩展知识点

2.3.1 数据粘包问题与解决方案

问题详解

TCP协议作为面向字节流的传输层协议,其核心特性是不保证数据边界。在网络传输过程中,发送方的多个数据包可能被合并传输,接收方也可能分批接收。例如:

  • 客户端连续发送两条消息:"Hello"和"World"
  • 服务器可能一次性收到"HelloWorld"(粘包)
  • 也可能分两次收到"He"和"lloWorld"(半包)
产生原因深度分析
  1. 发送方机制

    • Nagle算法优化:默认启用,会缓冲小于MSS(最大报文段长度)的小数据包,等待更多数据或超时(200ms)再发送
    • 内核缓冲区合并:当应用层快速连续调用write()时,内核可能合并多个小数据包
  2. 接收方机制

    • 滑动窗口机制:接收缓冲区未及时读取时,多个数据包会在缓冲区堆积
    • 网络延迟抖动:不同数据包可能通过不同路由到达,导致接收顺序异常
解决方案对比
方案优点缺点适用场景
定长消息实现简单空间浪费固定格式协议(如金融报文)
分隔符灵活可变长需转义处理文本协议(如Redis)
消息头+体高效精准复杂度略高主流二进制协议
消息头+消息体实现进阶
  1. 协议设计优化

    // 协议格式:4字节魔数(0xCAFEBABE) + 4字节长度 + N字节消息体
    dos.writeInt(0xCAFEBABE); // 协议标识
    dos.writeInt(msgBytes.length);
    dos.write(msgBytes);
    

  2. 异常处理增强

    try {
        int magic = dis.readInt();
        if(magic != 0xCAFEBABE) throw new ProtocolException();
        int length = dis.readInt();
        if(length > MAX_LENGTH) throw new OverflowException();
        // ...读取消息体
    } catch(EOFException e) {
        // 处理连接中断
    }
    

2.3.2 Java NIO与Selector(高并发优化)

性能对比数据
模型线程数内存占用QPS(测试值)
BIO1:11MB/线程2,000
NIO1:N50KB/连接50,000
核心组件详解
  1. Channel类型

    • ServerSocketChannel:监听TCP连接
    • SocketChannel:客户端连接通道
    • DatagramChannel:UDP通道
    • FileChannel:文件IO通道
  2. Buffer工作机制

    ByteBuffer buf = ByteBuffer.allocateDirect(1024); // 堆外内存
    buf.put(data);  // position移动
    buf.flip();     // limit=position, position=0
    channel.write(buf);
    buf.clear();    // position=0, limit=capacity
    

  3. Selector事件类型

    • OP_ACCEPT:服务端接收连接
    • OP_CONNECT:客户端连接完成
    • OP_READ:数据可读
    • OP_WRITE:通道可写
生产级实现建议
  1. 多Reactor模式

    // 主Reactor处理accept
    selector1.register(serverChannel, SelectionKey.OP_ACCEPT);
    
    // 子Reactor线程池处理IO
    Selector[] workers = new Selector[Runtime.getRuntime().availableProcessors()];
    

  2. 内存管理优化

    // 使用对象池复用ByteBuffer
    BufferPool pool = new BufferPool(1024, 100);
    ByteBuffer buf = pool.borrowBuffer();
    try {
        // ...使用buffer
    } finally {
        pool.returnBuffer(buf);
    }
    

  3. Netty对比优势

    • 零拷贝支持(FileRegion)
    • 内存泄漏检测
    • 完善的编解码框架
    • 流量整形(TrafficShaping)
完整示例:NIO文件传输
FileChannel fileChannel = FileChannel.open(Paths.get("data.txt"));
SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 8080));

// 零拷贝传输
long transferSize = fileChannel.transferTo(0, fileChannel.size(), socketChannel);
System.out.println("传输字节数:" + transferSize);

三、Java TCP 编程关键注意事项

3.1 端口相关注意事项

端口范围与权限

  • 端口号范围:0~65535(16位整型)

    • 0~1023:知名端口(Well-Known Ports),由IANA分配给系统服务,普通用户程序无法直接绑定(需root或管理员权限)
      • 常见示例:80(HTTP)、443(HTTPS)、22(SSH)、21(FTP)、53(DNS)
    • 1024~49151:注册端口(Registered Ports),可自由使用但建议避免与已注册服务冲突
    • 49152~65535:动态/私有端口(Ephemeral Ports),临时连接使用
  • 最佳实践

    • 开发测试建议使用1024~49151范围内端口
    • 生产环境端口应记录在《服务部署手册》中
    • 通过Spring的@Value或配置文件灵活配置端口:
      # application.properties
      server.port=8080
      

端口被占用问题

  • 检测命令

    # Windows
    netstat -ano | findstr :8080
    taskkill /PID <进程ID> /F
    
    # Linux/Mac
    lsof -i :8080
    kill -9 <进程ID>
    

  • 编程解决方案

    • 自动端口递增算法(示例):
      public int findAvailablePort(int startPort) {
          while (startPort < 65535) {
              try (ServerSocket ignored = new ServerSocket(startPort)) {
                  return startPort;
              } catch (IOException e) {
                  startPort++;
              }
          }
          throw new RuntimeException("No available port found");
      }
      

3.2 异常处理与资源释放

必须捕获的异常类型

异常类型触发场景处理建议
BindException端口被占用记录日志并尝试备用端口
ConnectException连接拒绝检查服务状态和网络配置
SocketTimeoutException超时设置生效增加超时阈值或优化网络
IOException流读写异常关闭连接并重建会话

资源释放最佳实践

// 传统方式(易遗漏)
try {
    Socket socket = new Socket(...);
    // 业务代码
    socket.close(); // 可能被异常跳过
} catch (...) {...}

// 推荐方式(Java 7+)
try (ServerSocket server = new ServerSocket(port);
     Socket client = server.accept();
     InputStream in = client.getInputStream();
     OutputStream out = client.getOutputStream()) {
    // 业务逻辑
} // 自动调用close()

3.3 超时设置(避免无限阻塞)

连接超时设置

Socket socket = new Socket();
// 设置DNS解析超时(Java 8+)
socket.setSoTimeout(5000); 

// 设置TCP握手超时
socket.connect(
    new InetSocketAddress("example.com", 80), 
    3000 // 3秒超时
);

读写超时优化

// 设置SO_TIMEOUT(影响所有IO操作)
socket.setSoTimeout(10000); // 10秒

// 针对不同操作的超时控制
BufferedReader reader = new BufferedReader(...);
long start = System.currentTimeMillis();
while ((line = reader.readLine()) != null) {
    if (System.currentTimeMillis() - start > 5000) {
        throw new SocketTimeoutException("Read timeout");
    }
    // 处理数据
}

3.4 半关闭状态与shutdown方法

半关闭工作流程

  1. 客户端发送完数据后:

    socket.shutdownOutput(); // 发送FIN包
    // 仍可接收服务器响应
    

  2. 服务器检测到EOF:

    while ((bytesRead = input.read(buffer)) != -1) {
        // 处理数据
    }
    // 收到FIN后发送响应
    

  3. 完全关闭连接:

    socket.close(); // 发送RST包
    

应用场景对比

场景关闭方式优点
HTTP请求全关闭简单高效
文件传输半关闭确认接收结果
视频流保持连接减少握手开销

3.5 编码一致性(避免乱码)

编码处理规范

// 错误示范(使用平台默认编码)
new InputStreamReader(socket.getInputStream());

// 正确做法(显式指定UTF-8)
new InputStreamReader(
    socket.getInputStream(), 
    StandardCharsets.UTF_8
);

// 写入时同样处理
new OutputStreamWriter(
    socket.getOutputStream(),
    StandardCharsets.UTF_8
);

BOM头处理

// 检测UTF-8 BOM头
PushbackInputStream pis = new PushbackInputStream(in, 3);
byte[] bom = new byte[3];
if (pis.read(bom) == 3 
    && bom[0] == (byte)0xEF 
    && bom[1] == (byte)0xBB 
    && bom[2] == (byte)0xBF) {
    // 是BOM头
} else {
    pis.unread(bom); // 非BOM头回退
}

3.6 多线程安全与线程池参数

线程安全实践

// 不安全示例
public class Counter {
    private int count;
    public void increment() { count++; } // 竞态条件
}

// 安全解决方案
public class SafeCounter {
    private final AtomicInteger count = new AtomicInteger();
    public void increment() {
        count.incrementAndGet();
    }
}

线程池配置公式

int corePoolSize = Runtime.getRuntime().availableProcessors();
int maxPoolSize = corePoolSize * 2;
long keepAliveTime = 60L;
TimeUnit unit = TimeUnit.SECONDS;
BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(100);

ThreadPoolExecutor executor = new ThreadPoolExecutor(
    corePoolSize,
    maxPoolSize,
    keepAliveTime,
    unit,
    workQueue,
    new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
);

监控指标

// 获取线程池状态
executor.getActiveCount(); // 活动线程数
executor.getQueue().size(); // 队列积压
executor.getCompletedTaskCount(); // 已完成任务

四、TCP 文件传输(客户端→服务器)

4.1 客户端实现(文件发送端)

import java.io.*;
import java.net.InetSocketAddress;
import java.net.Socket;

/**
 * 文件传输客户端实现
 * 功能:向指定服务器发送文件并接收响应
 */
public class FileClient {
    public static void main(String[] args) {
        // 服务器配置
        String serverIp = "127.0.0.1";  // 服务器IP地址
        int serverPort = 9999;          // 服务器端口
        
        // 待传输文件配置
        String filePath = "D:/test.jpg"; // 待发送文件路径(可替换为实际文件路径)
        
        try (
            // 自动关闭资源:Socket连接、文件输入流、数据输出流
            Socket socket = new Socket();
            FileInputStream fis = new FileInputStream(filePath);
            DataOutputStream dos = new DataOutputStream(socket.getOutputStream())
        ) {
            // 连接设置
            socket.connect(new InetSocketAddress(serverIp, serverPort), 5000); // 5秒连接超时
            socket.setSoTimeout(5000); // 5秒读超时(接收服务器响应)
            
            // 1. 发送文件名(消息头:4字节长度 + 消息体:文件名字节)
            File file = new File(filePath);
            String fileName = file.getName();
            byte[] fileNameBytes = fileName.getBytes("UTF-8");
            dos.writeInt(fileNameBytes.length);  // 写入文件名长度
            dos.write(fileNameBytes);            // 写入文件名内容
            
            // 2. 发送文件大小(8字节long)
            long fileSize = file.length();
            dos.writeLong(fileSize);
            
            // 3. 发送文件内容(分块传输)
            byte[] buffer = new byte[1024 * 8]; // 8KB缓冲区(可根据网络情况调整)
            int readLen;
            long sentSize = 0;
            
            System.out.println("开始发送文件:" + fileName + "(大小:" + fileSize + "字节)");
            
            // 分块读取文件并发送
            while ((readLen = fis.read(buffer)) != -1) {
                dos.write(buffer, 0, readLen);
                sentSize += readLen;
                
                // 打印传输进度(每发送一个块更新一次)
                System.out.printf("发送进度:%.2f%%\r", (sentSize * 100.0) / fileSize);
            }
            
            dos.flush(); // 确保所有数据已发送
            System.out.println("\n文件发送完成");
            
            // 4. 接收服务器响应
            DataInputStream dis = new DataInputStream(socket.getInputStream());
            String response = dis.readUTF();
            System.out.println("服务器回复:" + response);
            
        } catch (IOException e) {
            System.err.println("文件传输异常:" + e.getMessage());
            e.printStackTrace();
        }
    }
}

4.2 服务器实现(文件接收端)

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * 文件传输服务器实现
 * 功能:接收客户端发送的文件并保存到指定目录
 */
public class FileServer {
    // 线程池配置(支持并发处理多个客户端连接)
    private static final ExecutorService THREAD_POOL = Executors.newFixedThreadPool(3);
    
    // 文件保存目录配置
    private static final String SAVE_DIR = "D:/tcp-file-receive/"; // 文件保存目录

    public static void main(String[] args) {
        // 创建保存目录(不存在则创建)
        File saveDir = new File(SAVE_DIR);
        if (!saveDir.exists()) {
            boolean created = saveDir.mkdirs();
            if (!created) {
                System.err.println("无法创建保存目录:" + SAVE_DIR);
                return;
            }
        }

        int port = 9999; // 监听端口
        
        try (ServerSocket serverSocket = new ServerSocket(port)) {
            System.out.println("文件服务器已启动,监听端口:" + port + ",保存目录:" + SAVE_DIR);
            
            // 持续监听客户端连接
            while (true) {
                Socket clientSocket = serverSocket.accept();
                System.out.println("客户端连接:" + clientSocket.getInetAddress());
                
                // 使用线程池处理文件接收(支持并发)
                THREAD_POOL.submit(() -> receiveFile(clientSocket));
            }
        } catch (IOException e) {
            System.err.println("服务器异常:" + e.getMessage());
        } finally {
            THREAD_POOL.shutdown(); // 优雅关闭线程池
        }
    }

    /**
     * 文件接收处理逻辑
     * @param clientSocket 客户端Socket连接
     */
    private static void receiveFile(Socket clientSocket) {
        try (
            DataInputStream dis = new DataInputStream(clientSocket.getInputStream());
            DataOutputStream dos = new DataOutputStream(clientSocket.getOutputStream())
        ) {
            // 设置30秒操作超时(适应大文件传输)
            clientSocket.setSoTimeout(30000);
            
            // 1. 接收文件名
            int fileNameLen = dis.readInt();
            byte[] fileNameBytes = new byte[fileNameLen];
            dis.readFully(fileNameBytes);
            String fileName = new String(fileNameBytes, "UTF-8");
            String savePath = SAVE_DIR + fileName;
            
            // 2. 接收文件大小
            long fileSize = dis.readLong();
            System.out.println("开始接收文件:" + fileName + "(大小:" + fileSize + "字节)");
            
            // 3. 接收文件内容
            try (FileOutputStream fos = new FileOutputStream(savePath)) {
                byte[] buffer = new byte[1024 * 8]; // 8KB接收缓冲区
                int readLen;
                long receivedSize = 0;
                
                while ((readLen = dis.read(buffer)) != -1) {
                    fos.write(buffer, 0, readLen);
                    receivedSize += readLen;
                    
                    // 打印接收进度(每接收一个块更新一次)
                    System.out.printf("接收进度:%.2f%%\r", (receivedSize * 100.0) / fileSize);
                    
                    // 检查是否接收完成
                    if (receivedSize == fileSize) {
                        break;
                    }
                }
                
                fos.flush();
                System.out.println("\n文件接收完成,保存路径:" + savePath);
                
                // 4. 向客户端发送成功响应
                dos.writeUTF("文件接收成功!保存路径:" + savePath);
            }
        } catch (IOException e) {
            System.err.println("文件接收异常:" + e.getMessage());
        } finally {
            try {
                clientSocket.close(); // 确保连接关闭
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

实现特点说明

  1. 可靠传输机制

    • 采用固定格式的消息头(文件名长度+文件名+文件大小)
    • 实现进度监控和完整性校验
    • 设置合理的超时机制
  2. 性能优化

    • 使用8KB缓冲区减少I/O操作次数
    • 支持大文件传输(分块处理)
    • 服务器使用线程池处理并发连接
  3. 健壮性设计

    • 自动创建保存目录
    • 使用try-with-resources确保资源释放
    • 详细的异常处理和日志记录

五、扩展

Netty 框架详解

Netty 是一个基于 Java NIO 的异步事件驱动网络应用框架,用于快速开发高性能、高可靠性的网络服务器和客户端程序。其核心优势包括:

  1. TCP/UDP 编程简化:通过 Channel、EventLoop 等抽象概念,简化了底层网络编程复杂度
  2. 粘包/拆包解决方案
    • 内置多种编解码器(如 LengthFieldBasedFrameDecoder)
    • 支持自定义协议编解码(如 ProtobufCodec)
  3. 高可用机制
    • 自动断线重连(通过 ChannelFutureListener 监听连接状态)
    • 心跳检测(IdleStateHandler 实现读写空闲检测)
  4. 应用案例
    • Dubbo 的底层通信框架
    • Elasticsearch 的节点间通信
    • RocketMQ 的 Broker 与 NameServer 通信

SSL/TLS 加密实现

  1. 核心类
    • SSLContext:安全套接字协议实现
    • SSLSocketFactory:创建安全套接字
    • SSLSocket:加密通信套接字
  2. 典型配置流程
    // 1. 获取SSLContext实例
    SSLContext sslContext = SSLContext.getInstance("TLSv1.2");
    // 2. 初始化信任管理器
    sslContext.init(null, trustManager, new SecureRandom());
    // 3. 创建SSLSocket
    SSLSocket socket = (SSLSocket)sslContext.getSocketFactory()
                           .createSocket(host, port);
    // 4. 启用加密套件
    socket.setEnabledCipherSuites(new String[]{"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"});
    

  3. 适用场景
    • 金融支付交易
    • 用户认证登录
    • 医疗数据传输
    • 政府机密通信

TCP 性能优化方案

  1. 缓冲区调优

    • 接收缓冲区:socket.setReceiveBufferSize(64 * 1024)(建议64KB起)
    • 发送缓冲区:socket.setSendBufferSize(64 * 1024)
    • 需与操作系统参数net.core.rmem_max协调
  2. Nagle算法禁用

    // 适用于实时性要求高的场景
    socket.setTcpNoDelay(true);
    

    • 效果:减少小数据包延迟(约40ms)
    • 适用:在线游戏、实时交易系统
  3. 保活机制

    // 操作系统层心跳检测
    socket.setKeepAlive(true);
    // 配合应用层心跳(如每30秒发送心跳包)
    

  4. 其他优化参数

    • socket.setReuseAddress(true):端口快速重用
    • socket.setSoTimeout(3000):设置读取超时
    • socket.setPerformancePreferences(1, 2, 0):偏好延迟优化
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值