springboot集成netty服务端(TCP socket服务端)

netty依赖:

	<!-- https://mvnrepository.com/artifact/io.netty/netty-all -->
	<dependency>
		<groupId>io.netty</groupId>
		<artifactId>netty-all</artifactId>
		<version>4.1.28.Final</version>
	</dependency>

代码:

server启动类

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import java.net.InetSocketAddress;

/**
 * @description: netty服务启动类
 **/

@Slf4j
@Component
public class NettyServer {

    public void start(InetSocketAddress address) {
        //配置服务端的NIO线程组
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workerGroup = new NioEventLoopGroup();

        try {
            ServerBootstrap bootstrap = new ServerBootstrap()
                    .group(bossGroup, workerGroup)  // 绑定线程池
                    .channel(NioServerSocketChannel.class)
                    .localAddress(address)
                    .childHandler(new NettyServerChannelInitializer())//编码解码
                    .option(ChannelOption.SO_BACKLOG, 128)  //服务端接受连接的队列长度,如果队列已满,客户端连接将被拒绝
                    .childOption(ChannelOption.SO_KEEPALIVE, true);  //保持长连接,2小时无数据激活心跳机制

            // 绑定端口,开始接收进来的连接
            ChannelFuture future = bootstrap.bind(address).sync();
            log.info("netty服务器开始监听端口:" + address.getPort());
            //关闭channel和块,直到它被关闭
            future.channel().closeFuture().sync();
        } catch (Exception e) {
            e.printStackTrace();
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }

}

server初始化类

import io.netty.channel.ChannelInitializer;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.util.CharsetUtil;

/**
 * @description: 服务端初始化,客户端与服务器端连接一旦创建,这个类中方法就会被回调,设置出站编码器和入站解码器
 **/

public class NettyServerChannelInitializer extends ChannelInitializer<SocketChannel> {

    @Override
    protected void initChannel(SocketChannel channel) throws Exception {

        channel.pipeline().addLast("decoder",new StringDecoder(CharsetUtil.UTF_8));
        channel.pipeline().addLast("encoder",new StringEncoder(CharsetUtil.UTF_8));
        channel.pipeline().addLast(new NettyServerHandler());
    }
}

server处理类

import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelId;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.handler.timeout.IdleState;
import io.netty.handler.timeout.IdleStateEvent;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import java.net.InetSocketAddress;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @description: netty服务端处理类
 **/

@Slf4j
@Component
public class NettyServerHandler extends ChannelInboundHandlerAdapter {

    /**
     * 管理一个全局map,保存连接进服务端的通道数量
     */
    private static final ConcurrentHashMap<ChannelId, ChannelHandlerContext> CHANNEL_MAP = new ConcurrentHashMap<>();

   

    /**
     * @param ctx
     * @DESCRIPTION: 有客户端连接服务器会触发此函数
     * @return: void
     */
    @Override
    public void channelActive(ChannelHandlerContext ctx) {

        InetSocketAddress insocket = (InetSocketAddress) ctx.channel().remoteAddress();

        String clientIp = insocket.getAddress().getHostAddress();
        int clientPort = insocket.getPort();

        //获取连接通道唯一标识
        ChannelId channelId = ctx.channel().id();

        System.out.println();
        //如果map中不包含此连接,就保存连接
        if (CHANNEL_MAP.containsKey(channelId)) {
            log.info("客户端【" + channelId + "】是连接状态,连接通道数量: " + CHANNEL_MAP.size());
        } else {
            //保存连接
            CHANNEL_MAP.put(channelId, ctx);

            log.info("客户端【" + channelId + "】连接netty服务器[IP:" + clientIp + "--->PORT:" + clientPort + "]");
            log.info("连接通道数量: " + CHANNEL_MAP.size());
        }
    }

    /**
     * @param ctx
     * @DESCRIPTION: 有客户端终止连接服务器会触发此函数
     * @return: void
     */
    @Override
    public void channelInactive(ChannelHandlerContext ctx) {

        InetSocketAddress insocket = (InetSocketAddress) ctx.channel().remoteAddress();

        String clientIp = insocket.getAddress().getHostAddress();

        ChannelId channelId = ctx.channel().id();

        //包含此客户端才去删除
        if (CHANNEL_MAP.containsKey(channelId)) {
            //删除连接
            CHANNEL_MAP.remove(channelId);
            System.out.println();
            log.info("客户端【" + channelId + "】退出netty服务器[IP:" + clientIp + "--->PORT:" + insocket.getPort() + "]");
            log.info("连接通道数量: " + CHANNEL_MAP.size());
        }
    }

    /**
     * @param ctx
     * @DESCRIPTION: 有客户端发消息会触发此函数
     * @return: void
     */
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {

        String msgStr = String.valueOf(msg);

        System.out.println();
        log.info("加载客户端报文......");
        log.info("【" + ctx.channel().id() + "】" + " :" + msg);

   
        /**
         *  下面可以解析数据,保存数据,生成返回报文,将需要返回报文写入write函数
         *
         */

        //响应客户端
        log.info("服务端端返回报文......");
        log.info("【" + ctx.channel().id() + "】" + " :" + msg);
        this.channelWrite(ctx.channel().id(), msg);
    }

    /**
     * @param msg        需要发送的消息内容
     * @param channelId 连接通道唯一id
     * @DESCRIPTION: 服务端给客户端发送消息
     * @return: void
     */
    public void channelWrite(ChannelId channelId, Object msg) throws Exception {

        ChannelHandlerContext ctx = CHANNEL_MAP.get(channelId);

        if (ctx == null) {
            log.info("通道【" + channelId + "】不存在");
            return;
        }

        if (msg == null || msg == "") {
            log.info("服务端响应空的消息");
            return;
        }

        //将客户端的信息直接返回写入ctx
        ctx.write(msg);
        //刷新缓存区
        ctx.flush();
    }

    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {

        String socketString = ctx.channel().remoteAddress().toString();

        if (evt instanceof IdleStateEvent) {
            IdleStateEvent event = (IdleStateEvent) evt;
            if (event.state() == IdleState.READER_IDLE) {
                log.info("Client: " + socketString + " READER_IDLE 读超时");
                ctx.disconnect();
            } else if (event.state() == IdleState.WRITER_IDLE) {
                log.info("Client: " + socketString + " WRITER_IDLE 写超时");
                ctx.disconnect();
            } else if (event.state() == IdleState.ALL_IDLE) {
                log.info("Client: " + socketString + " ALL_IDLE 总超时");
                ctx.disconnect();
            }
        }
    }

    /**
     * @param ctx
     * @DESCRIPTION: 发生异常会触发此函数
     * @return: void
     */
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {

        System.out.println();
        ctx.close();
        log.info(ctx.channel().id() + " 发生了错误,此连接被关闭" + "此时连通数量: " + CHANNEL_MAP.size());
        //cause.printStackTrace();
    }
}

启动类

在springboot启动类中加入服务端的启动代码,让netty服务跟着项目一起启动。

@Slf4j
@SpringBootApplication
@EnableAutoConfiguration(exclude={MongoAutoConfiguration.class})
public class JeecgSystemApplication extends SpringBootServletInitializer implements CommandLineRunner{

    @Value("${netty.port}")
    private Integer port;
    @Value("${netty.host}")
    private String host;

    @Autowired
    private NettyServer nettyServer;

    @Override
    protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
        return application.sources(JeecgSystemApplication.class);
    }

    public static void main(String[] args) throws UnknownHostException {
        ConfigurableApplicationContext application = SpringApplication.run(JeecgSystemApplication.class, args);
        Environment env = application.getEnvironment();
        String ip = InetAddress.getLocalHost().getHostAddress();
        String port = env.getProperty("server.port");
        String path = oConvertUtils.getString(env.getProperty("server.servlet.context-path"));
    }

    /**
     * netty服务启动
     * @param args
     * @throws Exception
     */
    @Override
    public void run(String... args) throws Exception {
        InetSocketAddress address = new InetSocketAddress(host,port);
        log.info("neety服务器启动地址: "+host+":"+ port);
        nettyServer.start(address);
    }
}

结束!

### Spring Boot 整合 Netty 实现 Socket 通信 #### 创建 Maven 或 Gradle 工程并引入依赖项 为了使Spring Boot项目能够使用Netty进行Socket通信,首先需要创建一个新的Maven或Gradle工程,并在`pom.xml`文件中加入必要的依赖库。对于Maven而言,应添加如下所示的依赖配置: ```xml <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-netty</artifactId> </dependency> <!-- 如果上述starter不存在,则需单独引入 --> <dependency> <groupId>io.netty</groupId> <artifactId>netty-all</artifactId> <version>${netty.version}</version> </dependency> ``` 需要注意的是,当前并没有官方提供的`spring-boot-starter-netty`组件,因此可能需要直接引用具体的Netty包来完成集成工作。 #### 编写自定义Netty Server类 接下来编写一个继承自`ChannelInboundHandlerAdapter`的处理器类用于处理入站消息。该类负责接收来自客户端的消息以及向其发送响应数据。下面是一个简单的例子[^3]: ```java public class MyServerHandler extends ChannelInboundHandlerAdapter { @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { ByteBuf in = (ByteBuf) msg; try { System.out.println(in.toString(CharsetUtil.UTF_8)); ctx.write(in); } finally { ReferenceCountUtil.release(msg); } } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { cause.printStackTrace(); ctx.close(); } } ``` 在此基础上还需要构建启动器类,通过指定端口号和其他参数初始化Netty服务器实例。这里给出一段简化版的服务端代码片段作为参考[^1]: ```java @SpringBootApplication public class Application { private static final int PORT = 9090; public static void main(String[] args) throws InterruptedException { SpringApplication.run(Application.class, args); EventLoopGroup bossGroup = new NioEventLoopGroup(); // Boss线程组只关注accept事件的发生 EventLoopGroup workerGroup = new NioEventLoopGroup(); // Worker线程组则会真正执行I/O读写操作 try { ServerBootstrap b = new ServerBootstrap() .group(bossGroup, workerGroup) .channel(NioServerSocketChannel.class) .childHandler(new ChannelInitializer<SocketChannel>() { // 给pipeline设置处理器链路 protected void initChannel(SocketChannel ch) throws Exception { ch.pipeline().addLast(new MyServerHandler()); } }) .option(ChannelOption.SO_BACKLOG, 128) // 设置tcp缓冲区大小 .childOption(ChannelOption.SO_KEEPALIVE, true); // 开启心跳检测机制 ChannelFuture f = b.bind(PORT).sync(); System.out.println("Server started on port " + PORT); f.channel().closeFuture().sync(); } catch(Exception e){ throw new RuntimeException(e.getMessage(), e.getCause()); }finally{ workerGroup.shutdownGracefully(); bossGroup.shutdownGracefully(); } } } ``` 这段代码展示了如何在一个标准的Spring Boot应用程序上下文中嵌入Netty服务端逻辑。当接收到新的连接请求时,它将触发相应的通道初始化流程并将新建立起来的链接分配给专门的工作线程池去管理;与此同时,每当有可读就绪状态发生时就会激活我们之前定义好的业务处理器对象来进行具体的数据交换活动。 #### 配置客户端部分 除了搭建好监听外部访问的服务端之外,通常还会涉及到发起主动连接动作的一方——即所谓的“客户端”。针对这种情况下的场景需求同样可以借助于相同的技术栈轻松达成目标。以下是有关怎样构造出具备基本功能特性的TCP Client端程序样例说明[^2]: ```java @PostConstruct private void startClient() throws InterruptedException { String host = "localhost"; int port = 9090; EventLoopGroup group = new NioEventLoopGroup(); Bootstrap bootstrap = new Bootstrap() .group(group) .channel(NioSocketChannel.class) .handler(new ChannelInitializer<NioSocketChannel>() { @Override protected void initChannel(NioSocketChannel ch) throws Exception { ch.pipeline().addLast(new SimpleChannelInboundHandler<String>() { @Override protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception { System.out.println("Received message from server: " + msg); } }); } }); ChannelFuture future = bootstrap.connect(host, port).sync(); BufferedReader reader = new BufferedReader(new InputStreamReader(System.in)); while(true){ String line = reader.readLine(); if(line.equals("exit")){ break; } Channel channel = future.channel(); if(channel != null && channel.isActive()){ channel.writeAndFlush(line+"\r\n"); } } future.channel().closeFuture().sync(); group.shutdownGracefully(); } ``` 此段脚本里边包含了几个重要的组成部分:首先是关于网络IO多路复用的支持单元(`NioEventLoopGroup`)的选择;其次是明确了所使用的传输层协议类型(此处采用的是面向字节流式的套接口形式),最后则是指定了用来解析/编码应用层负载信息的具体编解码策略集锦。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

微微一笑满城空

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值