springboot使用netty做TCP客户端

1、服务端文档说明

css 复制代码
## 1. 概述

本文档描述了Socket模拟器的通信协议实现细节,包括数据包格式、字节序、编码方式等信息。

## 2. 通信基础

### 2.1 连接方式
- 协议类型:TCP
- 网络层:IPv4 (AddressFamily.InterNetwork)
- 传输方式:流式 (SocketType.Stream)
- 协议:TCP (ProtocolType.Tcp)

### 2.2 字节序
- 使用小端字节序(Little-Endian)
- 使用 `BitConverter` 进行字节序列的转换
- 长度字段采用4字节整数表示

## 3. 数据包格式

### 3.1 基本结构
```
+----------------+------------------+
|  Length (4B)   |   Payload       |
+----------------+------------------+
```

- Length: 4字节整数,表示Payload的长度
- Payload: UTF-8编码的消息内容

### 3.2 字段说明
1. Length字段
   - 大小:4字节
   - 类型:Int32
   - 字节序:小端序
   - 说明:表示后续Payload的字节长度

2. Payload字段
   - 编码:UTF-8
   - 长度:可变,由Length字段指定
   - 内容:实际传输的消息数据

## 4. 消息处理流程

### 4.1 发送流程
1. 将消息字符串转换为UTF-8字节数组
2. 计算消息字节数组长度
3. 将长度转换为4字节数组(小端序)
4. 组合长度字段和消息内容
5. 发送完整数据包

示例代码:
```csharp
byte[] bytes = Encoding.UTF8.GetBytes(sendMsg);   
byte[] xLenAry = BitConverter.GetBytes(bytes.Length);
byte[] sendData = new byte[bytes.Length + 4];
xLenAry.CopyTo(sendData, 0);
bytes.CopyTo(sendData, 4);
socket.Send(sendData);
```

### 4.2 接收流程
1. 接收数据到缓冲区(缓冲区大小1MB)
2. 读取前4字节获取消息长度
3. 根据长度读取后续消息内容
4. 将字节数组转换为UTF-8字符串

示例代码:
```csharp
byte[] arrServerRecMsg = new byte[1024 * 1024];
int length = socket.Receive(arrServerRecMsg);
byte[] lenstr = arrServerRecMsg.Skip(0).Take(4).ToArray();
int len = BitConverter.ToInt32(lenstr, 0);
string strSRecMsg = Encoding.UTF8.GetString(arrServerRecMsg, 4, len);
```

## 5. 错误处理

### 5.1 连接断开检测
- 通过 `Socket.Connected` 属性检查连接状态
- 捕获异常处理连接断开情况
- 在连接断开时清理资源并通知UI

### 5.2 异常处理
- 捕获Socket异常并进行相应处理
- 在连接断开时关闭Socket
- 从连接池中移除断开的连接
- 更新UI显示连接状态

## 6. 缓冲区管理

### 6.1 接收缓冲区
- 大小:1MB (1024 * 1024 字节)
- 类型:字节数组
- 用途:临时存储接收到的数据

### 6.2 发送缓冲区
- 动态分配,根据消息长度创建
- 包含4字节长度头部和消息内容
- 一次性发送完整数据包

## 7. 注意事项

1. 字符编码统一使用UTF-8,支持中文等多语言字符
2. 发送消息时需要先发送长度信息
3. 接收消息时需要先解析长度字段
4. 所有网络操作都需要进行异常处理
5. 在连接断开时要及时清理资源

## 8. 性能考虑

1. 使用后台线程处理接收消息
2. 设置适当的缓冲区大小
3. 及时关闭不使用的连接
4. 避免频繁的字符串转换操作 

2、pom文件

css 复制代码
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.5.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
css 复制代码
        <dependency>
            <groupId>io.netty</groupId>
            <artifactId>netty-all</artifactId>
            <version>4.1.86.Final</version>
        </dependency>

3、配置文件

css 复制代码
tcp:
  client:
    host: 127.0.0.1
    port: 15000
    timeout: 5000
    pool:
      maxTotal: 10
      maxIdle: 5
      minIdle: 2

4、 客户端示例

4.1、配置类

java 复制代码
package com.netty.client.config;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
@Data
@Configuration
@ConfigurationProperties(prefix = "tcp.client")
public class TcpClientConfig {
    private String host;
    private int port;
    private Pool pool;
    private int timeout;

    // Getters and Setters
    @Data
    public static class Pool {
        private int maxTotal;
        private int maxIdle;
        private int minIdle;

        // Getters and Setters
    }
}

4.2、ClientHandler

java 复制代码
package com.netty.client.hander;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import java.util.Arrays;
import java.util.concurrent.*;
@Slf4j
@Component
@ChannelHandler.Sharable
public class ClientHandler extends SimpleChannelInboundHandler<String> {
    private final ConcurrentMap<String, CompletableFuture<String>> pendingRequests = new ConcurrentHashMap<>();
    private static final ScheduledExecutorService TIMEOUT_EXECUTOR = Executors.newScheduledThreadPool(1);

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, String msg) {
        log.info("收到消息 ===> {}", msg);
        try{
            JSONObject jsonObject = JSON.parseObject(msg);
            JSONObject header = jsonObject.getJSONObject("Header");
            String correlationId = header.getString("TransactionID");
            CompletableFuture<String> future = pendingRequests.remove(correlationId);
            if (future != null) {
                future.complete(msg);
            }
        }catch (Exception e){
            log.info("channelRead0 has error ==>{}", Arrays.toString(e.getStackTrace()));
            log.info("channelRead0 message ==>{}",e.getMessage());
        }
    }

    public CompletableFuture<String> prepareResponse(String correlationId) {
        CompletableFuture<String> future = new CompletableFuture<>();
        ScheduledFuture<?> timeout = TIMEOUT_EXECUTOR.schedule(() -> {
            if (future.completeExceptionally(new TimeoutException())) {
                pendingRequests.remove(correlationId);
            }
        }, 5, TimeUnit.SECONDS);

        future.whenComplete((r, t) -> {
            timeout.cancel(true);
            pendingRequests.remove(correlationId);
        });

        pendingRequests.put(correlationId, future);

        return future;
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        ctx.close();
    }

}

4.3、client

java 复制代码
package com.netty.client;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.netty.client.config.TcpClientConfig;
import com.netty.client.hander.ClientHandler;
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;

import io.netty.handler.codec.ByteToMessageDecoder;
import io.netty.handler.codec.MessageToByteEncoder;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.pool2.BasePooledObjectFactory;
import org.apache.commons.pool2.PooledObject;
import org.apache.commons.pool2.impl.DefaultPooledObject;
import org.apache.commons.pool2.impl.GenericObjectPool;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import org.springframework.stereotype.Component;

import javax.annotation.PreDestroy;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
@Slf4j
@Component
public class NettyTcpClient {
    private final GenericObjectPool<Channel> connectionPool;
    private final ClientHandler clientHandler;
    private final TcpClientConfig config;

    public NettyTcpClient(TcpClientConfig config) {
        this.config = config;
        this.clientHandler = new ClientHandler();
        this.connectionPool = new GenericObjectPool<>(
                new ChannelFactory(config, clientHandler),
                buildPoolConfig(config.getPool())
        );
    }

    private GenericObjectPoolConfig<Channel> buildPoolConfig(TcpClientConfig.Pool poolConfig) {
        GenericObjectPoolConfig<Channel> config = new GenericObjectPoolConfig<>();
        config.setMaxTotal(poolConfig.getMaxTotal());
        config.setMaxIdle(poolConfig.getMaxIdle());
        config.setMinIdle(poolConfig.getMinIdle());
        return config;
    }

    public String sendSync(String message) throws Exception {
        Channel channel = null;
        try {
            channel = connectionPool.borrowObject();
            JSONObject jsonObject = JSON.parseObject(message);
            JSONObject header = jsonObject.getJSONObject("header");
            String correlationId = header.getString("transactionID");
            // String correlationId = UUID.randomUUID().toString();
            CompletableFuture<String> future = clientHandler.prepareResponse(correlationId);
            channel.writeAndFlush(message).sync();
            return future.get(config.getTimeout(), TimeUnit.MILLISECONDS);
        }
        finally {
            if (channel != null) {
                connectionPool.returnObject(channel);
            }
        }
    }

    @PreDestroy
    public void shutdown() {
        connectionPool.close();
    }

    private static class ChannelFactory extends BasePooledObjectFactory<Channel> {
        private final Bootstrap bootstrap;
        private final ClientHandler handler;

        public ChannelFactory(TcpClientConfig config, ClientHandler handler) {
            this.handler = handler;
            this.bootstrap = new Bootstrap()
                    .group(new NioEventLoopGroup())
                    .channel(NioSocketChannel.class)
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) {
                            ChannelPipeline pipeline = ch.pipeline();
                            // 解码器
                            pipeline.addLast(new LengthFieldDecoder());
                            // 编码器
                            pipeline.addLast(new LengthFieldEncoder());
                            // 业务处理器
                            pipeline.addLast(handler);
                        }
                    })
                    .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000)
                    .remoteAddress(config.getHost(), config.getPort());
        }

        @Override
        public Channel create() throws Exception {
            return bootstrap.connect().sync().channel();
        }

        @Override
        public PooledObject<Channel> wrap(Channel channel) {
            return new DefaultPooledObject<>(channel);
        }

        @Override
        public boolean validateObject(PooledObject<Channel> p) {
            return p.getObject().isActive();
        }

        @Override
        public void destroyObject(PooledObject<Channel> p) {
            p.getObject().close();
        }
    }
    static class LengthFieldDecoder extends ByteToMessageDecoder {
        @Override
        protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
            if (in.readableBytes() < 4) return;

            in.markReaderIndex();
            int length = in.readIntLE();  // 小端序读取长度字段
            if (in.readableBytes() < length) {
                in.resetReaderIndex();
                return;
            }

            ByteBuf payload = in.readBytes(length);
            out.add(payload.toString(StandardCharsets.UTF_8));
        }
    }

    static class LengthFieldEncoder extends MessageToByteEncoder<String> {
        @Override
        protected void encode(ChannelHandlerContext ctx, String msg, ByteBuf out) {
            byte[] bytes = msg.getBytes(StandardCharsets.UTF_8);
            out.writeIntLE(bytes.length);  // 小端序写入长度字段
            out.writeBytes(bytes);
        }
    }
}
相关推荐
千里码aicood3 小时前
【2025】基于springboot+vue的医院在线问诊系统设计与实现(源码、万字文档、图文修改、调试答疑)
vue.js·spring boot·后端
故事与他6453 小时前
Thinkphp(TP)框架漏洞攻略
android·服务器·网络·中间件·tomcat
yang_love10114 小时前
Spring Boot 中的 @ConditionalOnBean 注解详解
java·spring boot·后端
郑州吴彦祖7724 小时前
【Java】UDP网络编程:无连接通信到Socket实战
java·网络·udp
kfepiza5 小时前
netplan是如何操控systemd-networkd的? 笔记250324
linux·网络·笔记·ubuntu
夏夏不吃糖5 小时前
基于Spring Boot + Vue的银行管理系统设计与实现
java·vue.js·spring boot·maven
九转苍翎6 小时前
Java EE(12)——初始网络
网络·java-ee
Honeysea_707 小时前
网络编程和计算机网络五层模型的关系
网络·计算机网络
佳佳_8 小时前
Spring Boot 优化容器镜像
spring boot·后端·容器
独行soc8 小时前
2025年渗透测试面试题总结- shopee-安全工程师(题目+回答)
java·网络·python·科技·面试·职场和发展·红蓝攻防