Springboot+Netty搭建TCP服务端,ConcurrentHashMap管理所有连接通道

Springboot+Netty搭建TCP服务端

  • https://blog.csdn.net/myyhtw/article/details/90742121

Netty相关2:

  • https://blog.csdn.net/smltq/article/details/103534704

极简netty

  • https://blog.csdn.net/IRpickstars/article/details/134571373

Netty是业界最流行的nio框架之一,它具有功能强大、性能优异、可定制性和可扩展性的优点

Netty的优点:

1.API使用简单,开发入门门槛低。

2.功能十分强大,预置多种编码解码功能,支持多种主流协议。

3.可定制、可扩展能力强,可以通过其提供的ChannelHandler进行灵活的扩展。

4.性能优异,特别在综合性能上的优异性。

5.成熟,稳定,适用范围广。

6.可用于智能GSM/GPRS模块的通讯服务端开发,使用它进行MQTT协议的开发。

Netty结合Springboot快速开发框架搭建服务端程序:

SpringBoot+Netty实现TCP服务端客户端的源码Demo

pom

新建Springboot的maven项目,pom.xml文件导入依赖包

<...>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.5.RELEASE</version>
        <version>2.1.9.RELEASE</version>
        <relativePath/>
    </parent>

    <groupId>boot.base.tcp.server</groupId>
    <artifactId>boot-example-base-tcp-server-2.0.5</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>boot-example-base-tcp-server-2.0.5</name>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>io.netty</groupId>
            <artifactId>netty-all</artifactId> 默认为:2.0.5boot默认:4.1.29.Final,
            也可以手动引入4.1.43.Final
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>2.9.2</version>
        </dependency>
        <dependency>
            <groupId>com.github.xiaoymin</groupId>
            <artifactId>swagger-bootstrap-ui</artifactId>
            <version>1.9.2</version>
        </dependency>
        
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <scope>compile</scope>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <!-- 打包成一个可执行jar -->
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <executions>
                    <execution>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

Springboot启动类,Netty启动

package boot.example.tcp.server;

import boot.example.tcp.server.netty.BootNettyServer;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.EnableAsync;

/**
 * 蚂蚁舞
 */
@SpringBootApplication //boot主类
@EnableAsync //开启异步
public class BootNettyServerApplication implements CommandLineRunner {//实现CommandLineRunner

    //main方法
    public static void main(String[] args) {
        //new
        SpringApplication app = new SpringApplication(BootNettyServerApplication.class);
        //运行
        app.run(args);
        //运行完毕打印
        System.out.println("Hello World!");
    }

    //异步方法
    @Async
    @Override
    public void run(String... args) throws Exception {
        /**
         * 使用异步注解方式启动netty服务端服务
         */
        new BootNettyServer().bind(6655);
    }
}

1. Netty的server类

package boot.example.tcp.server.netty;


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.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;

/**
 * 蚂蚁舞
 */
public class BootNettyServer {

    public void bind(int port) throws Exception {

        /**
         * 	配置服务端的 NIO线程组
         * 	NioEventLoopGroup 是用来处理I/O操作的Reactor线程组
         * 	*
         * 	bossGroup:用来接收进来的连接,workerGroup:用来处理已经被接收的连接,进行socketChannel的网络读写,
         * 	*
         * 	bossGroup接收到连接后就会把连接信息注册到workerGroup
         * 	*
         * 	workerGroup的 EventLoopGroup默认的线程数是 CPU核数的二倍
         */
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workerGroup = new NioEventLoopGroup();

        try {
            /**
             * 1.ServerBootstrap 是一个启动NIO服务的辅助启动类
             */
            ServerBootstrap sb = new ServerBootstrap();
            /**
             * 	2.设置group,将bossGroup, workerGroup线程组传递到 ServerBootstrap
             */
            sb = sb.group(bossGroup, workerGroup);
            /**
             * 3.ServerSocketChannel是以NIO的selector为基础进行实现的,用来接收新的连接,
             * * 这里告诉Channel通过NioServerSocketChannel获取新的连接
             */
            sb = sb.channel(NioServerSocketChannel.class);

            //  option是设置 bossGroup,childOption是设置workerGroup

            /**
             * 4.服务端接受连接的队列长度,如果队列已满,客户端连接将被拒绝
             * * (队列被接收后,拒绝的客户端下次连接上来只要队列有空余就能连上)
             */
            sb = sb.option(ChannelOption.SO_BACKLOG, 128);
            /**
             * 5.立即发送数据,默认值为Ture(Netty默认为True而操作系统默认为False)。
             * 该值设置Nagle算法的启用,改算法将小的碎片数据连接成更大的报文来最小化所发送的报文的数量,
             * * 如果需要发送一些较小的报文,则需要禁用该算法。
             * Netty默认禁用该算法,从而最小化报文传输延时。
             * 不延迟,就是立刻发送*
             */
            sb = sb.childOption(ChannelOption.TCP_NODELAY, true);
            /**
             * 6.连接保活,默认值为False。启用该功能时,TCP会主动探测空闲连接的有效性。
             * 可以将此功能视为TCP的心跳机制,默认的心跳间隔是7200s即2小时, Netty默认关闭该功能。
             */
            sb = sb.childOption(ChannelOption.SO_KEEPALIVE, true);

            /**
             * 7.设置 I/O处理类,主要用于网络I/O事件,记录日志,编码、解码消息
             */
            sb = sb.childHandler(new BootNettyChannelInitializer<SocketChannel>());

            /**
             * 8.绑定端口,同步等待成功
             */
            ChannelFuture f = sb.bind(port).sync();
            if (f.isSuccess()) {
                System.out.println("netty server start success!");
                /**
                 * 9.等待服务器监听端口关闭
                 */
                f.channel().closeFuture().sync();
            }
        } catch (InterruptedException e) {
            System.out.println(e.toString());
        } finally {
            /**
             * 退出,释放线程池资源
             */
            bossGroup.shutdownGracefully().sync();
            workerGroup.shutdownGracefully().sync();
        }//final

    }//方法结束
}//类结束

2. 通道初始化

package boot.example.tcp.server.netty;


import io.netty.channel.Channel;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelInitializer;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.handler.timeout.IdleStateHandler;
import io.netty.util.CharsetUtil;

import java.util.concurrent.TimeUnit;

/**
 * 通道初始化
 * 蚂蚁舞
 */
@ChannelHandler.Sharable//共享的
public class BootNettyChannelInitializer<SocketChannel> extends ChannelInitializer<Channel> {

    //注意超时时间,这里配置,最好设置长点,要不代码 会自动关连接。这里是60秒
    public static long READ_TIME_OUT = 60;

    public static long WRITE_TIME_OUT = 60;

    public static long ALL_TIME_OUT = 60;

    @Override
    protected void initChannel(Channel ch) throws Exception {

        ch.pipeline().addLast(new IdleStateHandler(READ_TIME_OUT, WRITE_TIME_OUT, ALL_TIME_OUT, TimeUnit.SECONDS));

        // 带编码
        ch.pipeline().addLast("encoder", new StringEncoder(CharsetUtil.UTF_8));
        ch.pipeline().addLast("decoder", new StringDecoder(CharsetUtil.UTF_8));

//		// ChannelOutboundHandler,依照逆序执行
//        ch.pipeline().addLast("encoder", new StringEncoder());
//
//        // 属于ChannelInboundHandler,依照顺序执行
//        ch.pipeline().addLast("decoder", new StringDecoder());

        //自定义ChannelInboundHandlerAdapter
        ch.pipeline().addLast(new BootNettyChannelInboundHandlerAdapter());

    }

}

3. I/O数据读写处理类

package boot.example.tcp.server.netty;

import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;

import java.io.IOException;
import java.net.InetSocketAddress;

/**
 * I/O数据读写处理类
 * 蚂蚁舞
 */
@ChannelHandler.Sharable
public class BootNettyChannelInboundHandlerAdapter extends ChannelInboundHandlerAdapter {

    /**
     * 注册时执行
     */
    @Override
    public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
        //注册通道
        super.channelRegistered(ctx);
        System.out.println("--channelRegistered--" + ctx.channel().id().toString());
    }

    /**
     * 离线时执行
     */
    @Override
    public void channelUnregistered(ChannelHandlerContext ctx) throws Exception {
        //通道离线
        super.channelUnregistered(ctx);
        System.out.println("--channelUnregistered--" + ctx.channel().id().toString());
    }

    /**
     * 从客户端收到新的数据时,这个方法会在收到消息时被调用
     */
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        try {
            if (msg == null) {
                return;
            }
            //转成String
            String data = (String) msg;
            //替换掉 换行 和 空格
            data = data.replaceAll("\r|\n", "");
            //获取通道ID 并打印
            String channelId = ctx.channel().id().toString();
            System.out.println("channelId=" + channelId + "data=" + data);


            // 这里我将通道id作为code来使用,实际是需要msg里来摘取的客户端数据里的唯一值的

            // 如果没有则创建 如果有,更新data值
            BootNettyChannel b = BootNettyChannelCache.get("server:" + channelId);
            if (b == null) {
                //创建通道
                BootNettyChannel bnc = new BootNettyChannel();
                bnc.setChannel(ctx.channel());
                //设置通道编码
                bnc.setCode("server:" + channelId);
                bnc.setReport_last_data(data);
                //保存通道
                BootNettyChannelCache.save("server:" + channelId, bnc);
            } else {
                //更新data值什么意思呢?
                b.setReport_last_data(data);
            }
            //回写给客户端
            ctx.writeAndFlush(Unpooled.buffer().writeBytes(("server:" + channelId).getBytes()));
            // netty的编码已经指定,因此可以不需要再次确认编码
            // ctx.writeAndFlush(Unpooled.buffer().writeBytes(channelId.getBytes(CharsetUtil.UTF_8)));

            //直接这样写入也可以
            //ctx.write("我是服务端,我收到你的消息了!");
            //ctx.flush();

        } catch (Exception e) {
            System.out.println("channelRead--" + e.toString());
        }
    }

    /**
     * 从客户端收到新的数据、读取完成时调用
     */
    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws IOException {
        System.out.println("channelReadComplete");
        ctx.flush();
    }

    /**
     * 当出现 Throwable 对象才会被调用,即当 Netty 由于 IO 错误或者处理器在处理事件时抛出的异常时
     */
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws IOException {
        System.out.println("exceptionCaught");
        //打印错误内容
        cause.printStackTrace();
        //获取到这个通道
        BootNettyChannel bnc = BootNettyChannelCache.get("server:" + ctx.channel().id().toString());
        if (bnc != null) {
            //移除掉
            BootNettyChannelCache.remove("server:" + ctx.channel().id().toString());
        }
        ctx.close();//抛出异常,断开与客户端的连接
    }

    /**
     * 客户端与服务端第一次建立连接时 执行
     */
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception, IOException {
        //调用原始方法
        super.channelActive(ctx);
        ctx.channel().read();
        //获取到IP地址
        InetSocketAddress inSocket = (InetSocketAddress) ctx.channel().remoteAddress();
        String clientIp = inSocket.getAddress().getHostAddress();
        //此处不能使用ctx.close(),否则客户端始终无法与服务端建立连接
        System.out.println("channelActive:" + clientIp + ctx.name());
    }

    /**
     * 客户端与服务端 断连时 执行
     */
    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception, IOException {
        super.channelInactive(ctx);
        //获取到IP
        InetSocketAddress inSocket = (InetSocketAddress) ctx.channel().remoteAddress();
        String clientIp = inSocket.getAddress().getHostAddress();
        System.out.println("channelInactive:" + clientIp);

        //获取到通道
        BootNettyChannel bnc = BootNettyChannelCache.get("server:" + ctx.channel().id().toString());
        if (bnc != null) {
            //移除
            BootNettyChannelCache.remove("server:" + ctx.channel().id().toString());
        }
        ctx.close(); //断开连接时,必须关闭,否则造成资源浪费,并发量很大情况下可能造成宕机
    }

    /**
     * 服务端当read超时, 会调用这个方法
     */
    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception, IOException {
        super.userEventTriggered(ctx, evt);
        //获取到通道
        InetSocketAddress inSocket = (InetSocketAddress) ctx.channel().remoteAddress();
        //获取到IP
        String clientIp = inSocket.getAddress().getHostAddress();

        ctx.close();//超时时断开连接
        System.out.println("userEventTriggered:" + clientIp);
    }

}
BootNettyChannelCache
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 蚂蚁舞
 */
public class BootNettyChannelCache {
    //保证可先性。adj.易变的,动荡不定的,反复无常的;(情绪)易变的,易怒的,突然发作的;
    public static volatile Map<String, BootNettyChannel> channelMapCache = new ConcurrentHashMap<String, BootNettyChannel>();

    //增加
    public static void add(String code, BootNettyChannel channel) {
        channelMapCache.put(code, channel);
    }

    //获取
    public static BootNettyChannel get(String code) {
        return channelMapCache.get(code);
    }

    //移除
    public static void remove(String code) {
        channelMapCache.remove(code);
    }

    //保存:如果不存在,才保存
    public static void save(String code, BootNettyChannel channel) {
        if (channelMapCache.get(code) == null) {
            add(code, channel);
        }
    }
}
BootNettyChannel
  • 客户端最新发送的消息内容 和 通道保存
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
import io.netty.channel.Channel;
import lombok.Data;

/**
 * 蚂蚁舞
 */
@Data
public class BootNettyChannel {

    //	连接客户端唯一的code
    private String code;
    //	客户端最新发送的消息内容
    private String report_last_data;

    //通道
    private transient volatile Channel channel;

    /*//code的get 和 set
    public String getCode() {
        return code;
    }

    public void setCode(String code) {
        this.code = code;
    }

    //report_last_data的get和set
    public String getReport_last_data() {
        return report_last_data;
    }

    public void setReport_last_data(String report_last_data) {
        this.report_last_data = report_last_data;
    }

    //通道的get和set
    public Channel getChannel() {
        return channel;
    }

    public void setChannel(Channel channel) {
        this.channel = channel;
    }*/
}

BootNettyController

import boot.example.tcp.server.netty.cache.BootNettyChannel;
import boot.example.tcp.server.netty.cache.BootNettyChannelCache;
import io.netty.buffer.Unpooled;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * 蚂蚁舞
 */
@RestController
public class BootNettyController {

    @GetMapping(value = {"", "/"})
    public String index() {
        return "netty springBoot tcp demo";
    }

    @GetMapping("/clientList")
    public List<Map<String, String>> clientList() {
        List<Map<String, String>> list = new ArrayList<>();
        //遍历缓存中的 通道
        for (Map.Entry<String, BootNettyChannel> entry : BootNettyChannelCache.channelMapCache.entrySet()) {
            //获取通道的 code
            Map<String, String> map = new HashMap<String, String>();
            map.put("code", entry.getKey());
            //map.put("code", entry.getValue().getCode());
            //获取通道最后一条消息的值
            map.put("report_last_data", entry.getValue().getReport_last_data());
            list.add(map);
        }
        return list;
    }

    @PostMapping("/downDataToAllClient")
    public String downDataToAllClient(@RequestParam(name = "content", required = true) String content) {
        //获取Map中的 entry
        for (Map.Entry<String, BootNettyChannel> entry : BootNettyChannelCache.channelMapCache.entrySet()) {
            //获取到通道
            BootNettyChannel bootNettyChannel = entry.getValue();
            //如果通道是 打开的
            if (bootNettyChannel != null && bootNettyChannel.getChannel().isOpen()) {
                //把内容 写入到 通道
                bootNettyChannel.getChannel().writeAndFlush(Unpooled.buffer().writeBytes(content.getBytes()));
                // netty的编码已经指定,因此可以不需要再次确认编码
                // bootNettyChannel.getChannel().writeAndFlush(Unpooled.buffer().writeBytes(content.getBytes(CharsetUtil.UTF_8)));
            }
        }
        return "ok";
    }

    @PostMapping("/downDataToClient")
    public String downDataToClient(@RequestParam(name = "code", required = true) String code, @RequestParam(name = "content", required = true) String content) {
        //获取到具体的通道
        BootNettyChannel bootNettyChannel = BootNettyChannelCache.get(code);
        //如果通道是打开的
        if (bootNettyChannel != null && bootNettyChannel.getChannel().isOpen()) {
            //内容写入
            bootNettyChannel.getChannel().writeAndFlush(Unpooled.buffer().writeBytes(content.getBytes()));
            // netty的编码已经指定,因此可以不需要再次确认编码
            // bootNettyChannel.getChannel().writeAndFlush(Unpooled.buffer().writeBytes(content.getBytes(CharsetUtil.UTF_8)));
            return "success";
        }
        return "fail";
    }
}

1. 客户端参考写法 Bootstrap


import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import java.util.concurrent.TimeUnit;

@Component
@Slf4j
public class NettyClient {
    //发送线程组?
    private EventLoopGroup group = new NioEventLoopGroup();

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

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

    //socket通道
    private SocketChannel socketChannel;

    /**
     * 发送消息
     */
    public void sendMsg(String msg) {
        socketChannel.writeAndFlush(msg);
    }

    @PostConstruct
    public void start() {
        //1.引导程序
        Bootstrap bootstrap = new Bootstrap();
        //2.分组 和 其他配置
        bootstrap.group(group)
                .channel(NioSocketChannel.class)//通道固定
                .remoteAddress(host, port)//地址和端口
                .option(ChannelOption.SO_KEEPALIVE, true)//长连接
                .option(ChannelOption.TCP_NODELAY, true)//NO DELAY 无延迟
                .handler(new NettyClientInitializer());//处理类
        //3.进行连接
        ChannelFuture future = bootstrap.connect();

        //4.客户端断线重连逻辑
        future.addListener((ChannelFutureListener) future1 -> {
            if (future1.isSuccess()) {
                log.info("连接Netty服务端成功");
            } else {
                log.info("连接失败,进行断线重连");
                //20秒后重连
                future1.channel().eventLoop().schedule(() -> start(), 20, TimeUnit.SECONDS);
            }
        });
        //5.通道赋值
        socketChannel = (SocketChannel) future.channel();
    }
}
ChannelInitializer
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;

public class NettyClientInitializer extends ChannelInitializer<SocketChannel> {
    @Override
    protected void initChannel(SocketChannel socketChannel) throws Exception {
        socketChannel.pipeline().addLast("decoder", new StringDecoder());
        socketChannel.pipeline().addLast("encoder", new StringEncoder());
        socketChannel.pipeline().addLast(new NettyClientHandler());
    }
}
ChannelInboundHandlerAdapter
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import lombok.extern.slf4j.Slf4j;

@Slf4j
public class NettyClientHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        log.info("客户端Active .....");
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        log.info("客户端收到消息: {}", msg.toString());
    }

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

SwaggerConfig

/**
 * 蚂蚁舞
 */
@Configuration
@EnableSwagger2
public class SwaggerConfig {

    @Bean
    public Docket createRestApi() {
        //2版本,apiInfo,选择 所有的
        return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo()).select()
                .apis(RequestHandlerSelectors.any()).paths(PathSelectors.any())
                .paths(Predicates.not(PathSelectors.regex("/error.*")))
                .paths(PathSelectors.regex("/.*"))
                .build().apiInfo(apiInfo());
    }

    private ApiInfo apiInfo() {
        //标题 说明 版本 构建
        return new ApiInfoBuilder()
                .title("netty tcp 服务端demo")
                .description("netty tcp 服务端接口测试demo")
                .version("0.01")
                .build();
    }

    /**
     * http://localhost:6654/doc.html  地址和端口根据实际项目查看
     */
    
}
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>2.9.2</version>
        </dependency>
        <dependency>
            <groupId>com.github.xiaoymin</groupId>
            <artifactId>swagger-bootstrap-ui</artifactId>
            <version>1.9.2</version>
        </dependency>

目录结构

├─boot-example-base-tcp-server-2.0.5
│  │  pom.xml
│  │  
│  ├─src
│  │  ├─main
│  │  │  ├─java
│  │  │  │  └─boot
│  │  │  │      └─example
│  │  │  │          └─tcp
│  │  │  │              └─server
│  │  │  │                  │  BootNettyServerApplication.java
│  │  │  │                  │  SwaggerConfig.java
│  │  │  │                  │  
│  │  │  │                  ├─controller
│  │  │  │                  │      BootNettyController.java
│  │  │  │                  │      
│  │  │  │                  └─netty
│  │  │  │                          BootNettyChannel.java
│  │  │  │                          BootNettyChannelCache.java
│  │  │  │                          BootNettyChannelInboundHandlerAdapter.java
│  │  │  │                          BootNettyChannelInitializer.java
│  │  │  │                          BootNettyServer.java
│  │  │  │                          
│  │  │  └─resources
│  │  │          application.properties
│  │  │          
│  │  └─test
│  │      └─java
│  │          └─boot
│  │              └─example
│  │                  └─tcp
│  │                      └─server
│  │                              BootNettyServerApplicationTest.java
│  │                              

很简单的几个类加swagger,启动Springboot应用的同时也就启动了Netty

Netty Server端口:6655

SpringBoot Web端口: 6654

访问

http://localhost:6654/doc.html
使用常见的tcp客户端工具发送字母或数字(客户端工具发送中文可能出现乱码的,虽然程序已经处理了中文乱码,但依旧容易出现,处理办法是用netty写一个客户端来测试)
img

img

可以看到客户端发送消息,服务端能收到消息,并且在服务端做了保活,服务端也可以根据客户端的信息向客户端发送消息

Springboot整合Netty的服务端demo开发测试完成。

注意:如果乱码的话需要统一编码,最简单的方式

 // 带编码
    ch.pipeline().addLast("encoder", new StringEncoder(CharsetUtil.UTF_8));
    ch.pipeline().addLast("decoder", new StringDecoder(CharsetUtil.UTF_8));

文章知识点与官方知识档案匹配,可进一步学习相关知识
————————————————

                        版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。

原文链接:https://blog.csdn.net/myyhtw/article/details/90742121

2. 服务端保存 ServerBootstrap EventLoopGroup

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.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.net.InetSocketAddress;

@Component
@Slf4j
public class NettyServer {
    /**
     * boss 线程组用于处理 连接工作
     */
    private EventLoopGroup boss = new NioEventLoopGroup();
    /**
     * work 线程组用于 数据处理
     */
    private EventLoopGroup work = new NioEventLoopGroup();

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

    /**
     * 启动Netty Server
     *
     * @throws InterruptedException
     */
    //@PostConstruct
    public void start() throws InterruptedException {
        //1.创建 引导程序,辅助程序;自展 启动(电脑)
        ServerBootstrap bootstrap = new ServerBootstrap();
        //2.设置连接线程,处理线程组
        bootstrap.group(boss, work)
                //3.指定Channel。固定为官方的:NioServerSocketChannel
                .channel(NioServerSocketChannel.class)
                //4.使用指定的端口设置套接字地址
                .localAddress(new InetSocketAddress(port))

                //5.服务端可连接队列数,对应TCP/IP协议listen函数中backlog参数
                .option(ChannelOption.SO_BACKLOG, 1024)

                //6.设置TCP长连接,一般如果两个小时内没有数据的通信时,TCP会自动发送一个活动探测数据报文
                .childOption(ChannelOption.SO_KEEPALIVE, true)

                //7.将小的数据包包装成更大的帧进行传送,提高网络的负载
                .childOption(ChannelOption.TCP_NODELAY, true)

                //8.孩子 处理者。自己实现,处理编码 和 接受消息的类
                .childHandler(new ServerChannelInitializer());

        //9.引导程序:绑定 异步
        ChannelFuture future = bootstrap.bind().sync();
        if (future.isSuccess()) {
            log.info("启动 Netty Server");
        }
    }

    //destroy 销毁
    //在对象销毁之前执行清理或资源释放操作。
    @PreDestroy
    public void destory() throws InterruptedException {
        //关闭连接线程
        boss.shutdownGracefully().sync();
        //关闭工作线程
        work.shutdownGracefully().sync();
        log.info("关闭Netty");
    }
}
ServerChannelInitializer
                //8.孩子 处理者。自己实现,处理编码 和 接受消息的类
                .childHandler(new ServerChannelInitializer());
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;

public class ServerChannelInitializer extends ChannelInitializer<SocketChannel> {
    @Override
    protected void initChannel(SocketChannel socketChannel) throws Exception {
        //添加编解码
        socketChannel.pipeline().addLast("decoder", new StringDecoder(CharsetUtil.UTF_8));
        //传输和解析 都用 UTF8?
        socketChannel.pipeline().addLast("encoder", new StringEncoder(CharsetUtil.UTF_8));
        socketChannel.pipeline().addLast(new NettyServerHandler());
    }
}

//socketChannel.pipeline().addLast 源码
//addLast(ChannelHandler... var1)
ChannelInboundHandlerAdapter
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import lombok.extern.slf4j.Slf4j;

@Slf4j
public class NettyServerHandler extends ChannelInboundHandlerAdapter {
    /**
     * 客户端连接会触发
     */
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        log.info("Channel active......");
    }

    /**
     * 客户端发消息会触发
     */
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        log.info("服务器收到消息: {}", msg.toString());
        ctx.write("我是服务端,我收到你的消息了!");
        ctx.flush();
    }

    /**
     * 发生异常触发
     */
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        ctx.close();
    }
}

Netty 构建 NIO 通信服务 方案

使用原生网络应用API问题
  • 使用JDK原生网络应用程序API,会存在的问题

  • NIO的类库和API繁杂,使用麻烦,你需要熟练掌握

    • Selector、
    • ServerSocketChannel、
    • SocketChannel、
    • ByteBuffer等
  • 需要具备其它的额外技能做铺垫,例如熟悉Java多线程编程,因为NIO编程涉及到Reactor模式,你必须对多线程和网路编程非常熟悉,才能编写出高质量的NIO程序

  • 可靠性能力补齐,开发工作量和难度都非常大。例如客户端面临断连重连、网络闪断、半包读写、失败缓存、网络拥塞和异常码流的处理等等,NIO编程的特点是功能开发相对容易,但是可靠性能力补齐工作量和难度都非常大

反应器模式和23种模式

Reactor反应器模式,也叫做 分发者模式 或 通知者模式,

  • 是一种将就绪事件 派发给 对应服务 处理程序的事件设计模式。

建工抽元单

适桥组装享外代

责命 中解迭备 观状策模访

  • 观察者模式
    • 主题接口:有 注册 移除 观察者,和 通知所有观察者的接口
    • 主题实现类: 维护观察者集合。
Netty主要特点

Netty对JDK自带的NIO的API进行封装,解决上述问题,主要特点有

  • 高并发

Netty是一款基于NIO(Nonblocking I/O,非阻塞IO)开发的网络通信框架,对比于BIO(Blocking I/O,阻塞IO),他的并发性能得到了很大提高 。

  • 传输快

Netty的传输快其实也是依赖了NIO的一个特性——零拷贝。

  • 封装好

Netty封装了NIO操作的很多细节,提供易于使用的API。

Netty框架的优势
  • API使用简单,开发门槛低;

  • 功能强大,预置了多种编解码功能,支持多种主流协议;

  • 定制能力强,可以通过ChannelHandler对通信框架进行灵活地扩展;

  • 性能高,通过与其他业界主流的NIO框架对比,Netty的综合性能最优

  • 成熟、稳定,Netty修复了已经发现的所有JDK NIO BUG,业务开发人员不需要再为NIO的BUG而烦恼;

  • 社区活跃,版本迭代周期短,发现的BUG可以被及时修复,同时,更多的新功能会加入;

  • 经历了大规模的商业应用考验,质量得到验证。在互联网、大数据、网络游戏、企业应用、电信软件等众多行业得到成功商用,证明了它已经完全能够满足不同行业的商业应用了。

运行示例

打开浏览器,地址栏输入:http://localhost:8091/send?msg=你好,观察服务端和客户端控制台

服务端控制台输出

18:01:37.901   [           main] com.easy.nettyServer.NettyServer         : 启动 Netty Server

18:01:45.834   [ntLoopGroup-3-1] com.easy.nettyServer.NettyServerHandler  : Channel active......

18:02:07.858   [ntLoopGroup-3-1] com.easy.nettyServer.NettyServerHandler  : 服务器收到消息: 你好

客户端控制台输出

18:01:45.822  [ntLoopGroup-2-1] com.easy.nettyClient.NettyClient         : 连接Netty服务端成功

18:01:45.822  [ntLoopGroup-2-1] com.easy.nettyClient.NettyClientHandler  : 客户端Active .....

18:02:08.005  [ntLoopGroup-2-1] com.easy.nettyClient.NettyClientHandler  : 客户端收到消息: 我是服务端,我收到你的消息了!

表示使用Netty实现了我们的NIO通信了

配置yml 和 测试
netty:
  port: 10002
  host: 0.0.0.0
@Controller
public class TestController {

    @Resource
    private NettyClient nettyClient;

    @GetMapping("/hello")
    @ResponseBody
    public String hello() {
        nettyClient.sendMsg("我的测试消息1");

        return "ok";
    }
}

Netty 模块组件

Bootstrap、ServerBootstrap

一个Netty应用通常由一个Bootstrap开始,主要作用是配置整个Netty程序,串联各个组件,

  • Netty中Bootstrap类是客户端程序的启动引导类,

  • ServerBootstrap是服务端启动引导类。

Future、ChannelFuture

在Netty中所有的IO操作都是异步的,不能立刻得知消息是否被正确处理,但是可以过一会等它执行完成或者直接注册一个监听,具体的实现就是通过Future和ChannelFuture,他们可以注册一个监听,当操作执行成功或失败时监听会自动触发注册的监听事件。

        //8.孩子 处理者。自己实现,处理编码 和 接受消息的类
        .childHandler(new ServerChannelInitializer());

        //9.引导程序:绑定 异步
        ChannelFuture future = bootstrap.bind().sync();
        if (future.isSuccess()) {
            log.info("启动 Netty Server");
        }

//public class ServerChannelInitializer extends ChannelInitializer<SocketChannel> 
//public class NettyServerHandler extends ChannelInboundHandlerAdapter {
  • 客户端如下
        //3.进行连接
        ChannelFuture future = bootstrap.connect();

        //4.客户端断线重连逻辑
        future.addListener((ChannelFutureListener) future1 -> {
            ...
        });
        //5.通道赋值
        socketChannel = (SocketChannel) future.channel();
Channel

Netty网络通信组件,能够用于执行网络I/O操作。Channel为用户提供:

  • 当前网络连接的通道的状态(例如是否打开?是否已连接?

  • 网络连接的配置参数 (例如接收缓冲区大小

  • 提供异步的网络I/O操作(如建立连接,读写,绑定端口),异步调用意味着任何I/O调用都将立即返回,并且不保证在调用结束时所请求的I/O操作已完成。调用立即返回一个ChannelFuture实例,通过注册监听器到ChannelFuture上,可以I/O操作成功、失败或取消时回调通知调用方。

  • 支持关联I/O操作与对应的处理程序

不同协议、不同阻塞类型的连接都有不同的 Channel 类型与之对应,下面是一些常用的 Channel 类型

  • NioSocketChannel,异步的客户端 TCP Socket 连接
    NioServerSocketChannel,异步的服务器端 TCP Socket 连接
    NioDatagramChannel,异步的 UDP 连接
    NioSctpChannel,异步的客户端 Sctp 连接
    NioSctpServerChannel,异步的 Sctp 服务器端连接

  • 服务端 和 客户端同样

public class ServerChannelInitializer extends ChannelInitializer<SocketChannel> {
    @Override
    protected void initChannel(SocketChannel socketChannel) throws Exception {
}

Selector
Netty基于Selector对象实现I/O多路复用,通过 Selector, 一个线程可以监听多个连接的Channel事件, 当向一个Selector中注册Channel 后,Selector 内部的机制就可以自动不断地查询(select) 这些注册的Channel是否有已就绪的I/O事件(例如可读, 可写, 网络连接完成等),这样程序就可以很简单地使用一个线程高效地管理多个 Channel

NioEventLoop
  • NioEventLoop中维护了一个线程和任务队列,支持异步提交执行任务,线程启动时会调用NioEventLoop的run方法,执行I/O任务和非I/O任务:

  • I/O任务 即selectionKey中ready的事件,如accept、connect、read、write等,由processSelectedKeys方法触发。

  • 非IO任务 添加到taskQueue中的任务,如register0、bind0等任务,由runAllTasks方法触发。
    两种任务的执行时间比由变量ioRatio控制,默认为50,则表示允许非IO任务执行的时间与IO任务的执行时间相等。

NioEventLoopGroup
    /**
     * boss 线程组用于处理 连接工作
     */
    private EventLoopGroup boss = new NioEventLoopGroup();

NioEventLoopGroup,主要管理eventLoop的生命周期,可以理解为一个线程池,内部维护了一组线程,每个线程(NioEventLoop)负责处理多个Channel上的事件,而一个Channel只对应于一个线程。

ChannelHandler

ChannelHandler是一个接口,处理I/O事件或拦截I/O操作,并将其转发到其ChannelPipeline(业务处理链)中的下一个处理程序。

ChannelHandlerContext

保存Channel相关的所有上下文信息,同时关联一个ChannelHandler对象

@Slf4j
public class NettyServerHandler extends ChannelInboundHandlerAdapter {

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        log.info("服务器收到消息: {}", msg.toString());
        ctx.write("我是服务端,我收到你的消息了!");
        ctx.flush();
    }
}
ChannelPipline

保存ChannelHandler的List,用于处理或拦截Channel的入站事件和出站操作。 ChannelPipeline实现了一种高级形式的拦截过滤器模式,使用户可以完全控制事件的处理方式,以及Channel中各个的ChannelHandler如何相互交互。

SpringBoot快速搭建TCP服务端和客户端- 极简netty

  • https://blog.csdn.net/IRpickstars/article/details/134571373

由于工作需要,研究了SpringBoot搭建TCP通信的过程,对于工程需要的小伙伴,只是想快速搭建一个可用的服务.

其他的教程看了许多,感觉讲得太复杂,很容易弄乱,这里我只讲效率,展示快速搭建过程

TCPServer

引入依赖

由于TCP协议是Netty实现的,所以引入Netty的依赖

<dependency>
  <groupId>io.netty</groupId>
  <artifactId>netty-all</artifactId>
  <version>4.1.25.Final</version>
</dependency>

配置TCPServer

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.core.annotation.Order;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;

@Component
@Order(1)
@Slf4j
@Data
@ConfigurationProperties(prefix = "tcp.server")
public class TCPServer implements CommandLineRunner {
    //读取tcp.server.port
    private Integer port;

    @Async
    @Override
    public void run(String... args) throws Exception {
        //连接程组
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        //工作线程组
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap bootstrap = new ServerBootstrap();
            bootstrap.group(bossGroup, workerGroup)
                    //通道固定
                    .channel(NioServerSocketChannel.class)
                    .childHandler(new ChannelInitializer<Channel>() {
                        //处理通道固定
                        @Override
                        protected void initChannel(Channel channel) throws Exception {
                            ChannelPipeline pipeline = channel.pipeline();
                            pipeline.addLast(new StringEncoder());
                            pipeline.addLast(new StringDecoder());
                            //继承:extends SimpleChannelInboundHandler<String>
                            //继承:extends ChannelInboundHandlerAdapter
                            pipeline.addLast(new TCPServerHandler());
                        }
                    })
                    //BACK LOG 服务端可连接队列数,对应TCP/IP协议listen函数中backlog参数
                    .option(ChannelOption.SO_BACKLOG, 128)
                    .childOption(ChannelOption.SO_KEEPALIVE, true);

            //异步绑定。
            //sync阻塞。synchronized。synchronize的过去分词,同步的;同步化的。
            ChannelFuture future = bootstrap.bind(port).sync();
            log.info("TCP server started and listening on port " + port);

            future.channel().closeFuture().sync();
            //等待某个 Channel 关闭的异步操作完成,然后才继续执行后续的代码。
            //Future 代表一个异步计算的结果
            //Channel: 通道,通常指的是网络通信中的数据传输通道
            //这个方法返回一个 Future,代表了当 Channel 关闭时的异步操作结果。
            //sync 这个方法会阻塞当前线程,直到对应的 Future 完成
        } finally {
            //关闭
            //gracefully adv.优雅地;温文地
            //grace 优美,优雅;文雅,高雅;风度,体面;恩宠,恩典;饭前感恩祷告;宽限期,延缓期
            workerGroup.shutdownGracefully();
            bossGroup.shutdownGracefully();
        }
    }//fun方法
}

SimpleChannelInboundHandler

配置TCPServerHandler

import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

@Slf4j
@Component
public class TCPServerHandler extends SimpleChannelInboundHandler<String> {

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, String msg) {
        log.info("收到客户端消息:/n" + msg);
        //Object parse = JSONUtils.parse(msg);
        //System.out.println("parse = " + parse);
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        log.error("TCPServer出现异常", cause);
        ctx.close();
    }

}

TCPClient

客户端的配置大同小异

配置TCPClient

@Component
@Order(2)
@Slf4j
@Data
@ConfigurationProperties(prefix = "tcp.client")
public class TCPClient implements CommandLineRunner {
    private String host;
    private Integer port;

    //socket通道
    private SocketChannel socketChannel;

    @Override
    public void run(String... args) {//throws Exception
        //线程组
        EventLoopGroup group = new NioEventLoopGroup();
        try {
            Bootstrap bootstrap = new Bootstrap()
                    .group(group)
                    .channel(NioSocketChannel.class)
                    //固定写法:TCPClientHandler extends SimpleChannelInboundHandler<String>
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            ChannelPipeline pipeline = ch.pipeline();
                            pipeline.addLast(new StringEncoder());
                            pipeline.addLast(new StringDecoder());
                            pipeline.addLast(new TCPClientHandler());
                        }
                    });
            //异步连接
            ChannelFuture future = bootstrap.connect(host, port);//.sync();

            log.info("TCPClient Start , Connect host:" + host + ":" + port);

            //4.客户端断线重连逻辑。
            future.addListener((ChannelFutureListener) future1 -> {
                if (future1.isSuccess()) {
                    log.info("连接Netty服务端成功");
                } else {
                    log.info("连接失败,进行断线重连");
                    //20秒后重连
                    future1.channel().eventLoop().schedule(() -> run(), 5, TimeUnit.SECONDS);
                }
            });

            //5.通道赋值
            socketChannel = (SocketChannel) future.channel();
            socketChannel.writeAndFlush("我的测试哈哈哈");

            //注释掉:关闭Future的逻辑
            future.channel().closeFuture().sync();
        } catch (Exception e) {
            log.error("TCPClient Error", e);
        } finally {
            //这里一定不能关闭,否则重连有问题
            //group.shutdownGracefully();
        }
    }//run方法

    public void sendMsg(String msg) {
        socketChannel.writeAndFlush(msg);
    }
}

TCPServerHandler

import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

@Slf4j
@Component
public class TCPClientHandler extends SimpleChannelInboundHandler<String> {
 
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, String msg) {
        log.info("Receive TCPServer Message:\n"+ msg);
    }
 
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        log.error("TCPClient Error", cause);
        ctx.close();
    }
}

客户端和服务端application.yml

tcp:
  server:
    port: 8888 #服务器端口
  client:
    port: 8888 #连接的服务器端口
    host: 127.0.0.1 #连接的服务器域名

这样就完成了整个搭建过程,重要的就是服务端的端口和客户端连接服务端的URL和接受消息的处理方式,其他的细节可以自己慢慢探索.

开启异步和测试Control

  • 开启异步
@EnableAsync
@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}
@Controller
public class TestController {

    @Resource
    private TCPClient tCPClient;

    @GetMapping("/hello")
    @ResponseBody
    public String hello() {
        tCPClient.sendMsg("我的测试消息1");

        return "ok";
    }
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值