SpringBoot整合Netty实现socket通讯简单demo
依赖
<dependency> <groupId>io.netty</groupId> <artifactId>netty-all</artifactId> <version>4.1.42.Final</version> </dependency>
还用到了
<dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.20</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.75</version> </dependency>
ChatDto.java
import lombok.Data; import lombok.experimental.Accessors; /** * 传输实体类 */ @Data @Accessors(chain = true) public class ChatDto { /** * 客户端ID 唯一 */ private String clientId; /** * 发送的消息 */ private String msg; }
NettyChannelMap.java
import io.netty.channel.Channel; import io.netty.channel.socket.SocketChannel; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; /** * 存放连接的channel对象 */ public class NettyChannelMap { private static Map<String, SocketChannel> map = new ConcurrentHashMap<String, SocketChannel>(); public static void add(String clientId, SocketChannel socketChannel) { map.put(clientId, socketChannel); } public static Channel get(String clientId) { return map.get(clientId); } public static void remove(SocketChannel socketChannel) { for (Map.Entry entry : map.entrySet()) { if (entry.getValue() == socketChannel) { map.remove(entry.getKey()); } } } }
NettyTcpServerBootstrap.java
import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.*; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioServerSocketChannel; import io.netty.handler.codec.serialization.ClassResolvers; import io.netty.handler.codec.serialization.ObjectDecoder; import io.netty.handler.codec.serialization.ObjectEncoder; import io.netty.handler.logging.LogLevel; import io.netty.handler.logging.LoggingHandler; /** * 服务端启动类 */ public class NettyTcpServerBootstrap { private int port; private SocketChannel socketChannel; public NettyTcpServerBootstrap(int port) throws InterruptedException { this.port = port; } public void start() throws InterruptedException { /** * 创建两个线程组 bossGroup 和 workerGroup * bossGroup 只是处理连接请求,真正的和客户端业务处理,会交给 workerGroup 完成 * 两个都是无线循环 */ EventLoopGroup bossGroup = new NioEventLoopGroup(1); EventLoopGroup workerGroup = new NioEventLoopGroup(); try { //创建服务器端的启动对象,配置参数 ServerBootstrap bootstrap = new ServerBootstrap(); //设置两个线程组 bootstrap.group(bossGroup, workerGroup) //使用NioServerSocketChannel 作为服务器的通道实现 .channel(NioServerSocketChannel.class) //设置线程队列得到连接个数 .option(ChannelOption.SO_BACKLOG, 128) //设置保持活动连接状态 .childOption(ChannelOption.SO_KEEPALIVE, true) //通过NoDelay禁用Nagle,使消息立即发出去,不用等待到一定的数据量才发出去 .childOption(ChannelOption.TCP_NODELAY, true) //可以给 bossGroup 加个日志处理器 .handler(new LoggingHandler(LogLevel.INFO)) //给workerGroup 的 EventLoop 对应的管道设置处理器 .childHandler(new ChannelInitializer<SocketChannel>() { //给pipeline 设置处理器 @Override protected void initChannel(SocketChannel socketChannel) throws Exception { ChannelPipeline p = socketChannel.pipeline(); p.addLast(new ObjectEncoder()); p.addLast(new ObjectDecoder(ClassResolvers.cacheDisabled(null))); p.addLast(new NettyServerHandler()); } }); //启动服务器并绑定一个端口并且同步生成一个 ChannelFuture 对象 ChannelFuture cf = bootstrap.bind(port).sync(); if (cf.isSuccess()) { System.out.println("socket server start---------------"); } //对关闭通道进行监听 cf.channel().closeFuture().sync(); } finally { //发送异常关闭 bossGroup.shutdownGracefully(); workerGroup.shutdownGracefully(); } } }
NettyServerHandler.java
import com.alibaba.fastjson.JSON; import com.example.netty.dto.ChatDto; import io.netty.channel.Channel; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.SimpleChannelInboundHandler; import io.netty.channel.group.ChannelGroup; import io.netty.channel.group.DefaultChannelGroup; import io.netty.channel.socket.SocketChannel; import io.netty.util.ReferenceCountUtil; import io.netty.util.concurrent.GlobalEventExecutor; import lombok.extern.slf4j.Slf4j; @Slf4j public class NettyServerHandler extends SimpleChannelInboundHandler<Object> { /** * 定义一个channel组管理所有channel * GlobalEventExecutor.INSTANCE 是一个全局事件执行器 是一个单例 */ private static ChannelGroup channelGroup=new DefaultChannelGroup(GlobalEventExecutor.INSTANCE); /** * 标识 channel处于活动状态 * @param ctx * @throws Exception */ @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { } /** * 表示连接建立 第一个被执行 * @param ctx * @throws Exception */ @Override public void handlerAdded(ChannelHandlerContext ctx) throws Exception { channelGroup.add(ctx.channel()); /** * 该方法会将 channelGroup 中所有的channel 遍历一遍然后发送消息 不用我们自己遍历 * 这里只是做个说明 不用 */ // channelGroup.writeAndFlush("发送所有给所有channel"); } /** * 断开连接 * @param ctx * @throws Exception */ @Override public void handlerRemoved(ChannelHandlerContext ctx) throws Exception { } /** * 标识channel处于非活动状态 * @param ctx * @throws Exception */ @Override public void channelInactive(ChannelHandlerContext ctx) throws Exception { NettyChannelMap.remove((SocketChannel) ctx.channel()); } /** * 服务端 接收到 客户端 发的数据 * @param context * @param obj * @throws Exception */ @Override protected void channelRead0(ChannelHandlerContext context, Object obj) throws Exception { log.info(">>>>>>>>>>>服务端接收到客户端的消息:{}",obj); SocketChannel socketChannel = (SocketChannel) context.channel(); ChatDto dto = JSON.parseObject(obj.toString(), ChatDto.class); /** * 客户端ID */ String clientId = dto.getClientId(); if (clientId == null) { /** * 心跳包处理 */ ChatDto pingDto=new ChatDto(); pingDto.setMsg("服务端收到心跳包,返回响应"); socketChannel.writeAndFlush(JSON.toJSONString(pingDto)); return; } Channel channel = NettyChannelMap.get(clientId); if (channel==null){ /** * 存放所有连接客户端 */ NettyChannelMap.add(clientId, socketChannel); channel=socketChannel; } /** * 服务器返回客户端消息 */ ChatDto returnDto=new ChatDto(); returnDto.setClientId(clientId).setMsg("我是服务端,收到你的消息了"); channel.writeAndFlush(JSON.toJSONString(returnDto)); /** * 在这里可以设置异步执行 提交任务到该channel的taskQueue 中 */ context.channel().eventLoop().execute(new Runnable() { @Override public void run() { try { Thread.sleep(10*1000); log.info(">>>>>>>>>休眠十秒"); } catch (InterruptedException e) { e.printStackTrace(); } } }); /** * 可以设置多个异步任务 * 但是这个会在上面异步任务执行完之后才执行 */ context.channel().eventLoop().execute(new Runnable() { @Override public void run() { try { Thread.sleep(10*1000); log.info(">>>>>>>>>休眠二十秒"); } catch (InterruptedException e) { e.printStackTrace(); } } }); ReferenceCountUtil.release(obj); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { super.exceptionCaught(ctx, cause); ctx.close(); } }
客户端
NettyClientBootstrap.java
import io.netty.bootstrap.Bootstrap; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelInitializer; 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 io.netty.handler.codec.serialization.ClassResolvers; import io.netty.handler.codec.serialization.ObjectDecoder; import io.netty.handler.codec.serialization.ObjectEncoder; import io.netty.handler.timeout.IdleStateHandler; import io.netty.util.concurrent.DefaultEventExecutorGroup; import io.netty.util.concurrent.EventExecutorGroup; /** * 客户端启动类 */ public class NettyClientBootstrap { private int port; private String host; public SocketChannel socketChannel; private static final EventExecutorGroup group = new DefaultEventExecutorGroup(20); public NettyClientBootstrap(int port, String host) throws InterruptedException { this.port = port; this.host = host; start(); } private void start() throws InterruptedException { EventLoopGroup eventLoopGroup = new NioEventLoopGroup(); Bootstrap bootstrap = new Bootstrap(); try { bootstrap.group(eventLoopGroup) .channel(NioSocketChannel.class) .option(ChannelOption.SO_KEEPALIVE, true) .remoteAddress(host, port) .handler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel socketChannel) throws Exception { /** * 0 表示禁用 * readerIdleTime读空闲超时时间设定,如果channelRead()方法超过readerIdleTime时间未被调用则会触发超时事件调用userEventTrigger()方法; * * writerIdleTime写空闲超时时间设定,如果write()方法超过writerIdleTime时间未被调用则会触发超时事件调用userEventTrigger()方法; * * allIdleTime所有类型的空闲超时时间设定,包括读空闲和写空闲; */ socketChannel.pipeline().addLast(new IdleStateHandler(20, 10, 0)); socketChannel.pipeline().addLast(new ObjectEncoder()); socketChannel.pipeline().addLast(new ObjectDecoder(ClassResolvers.cacheDisabled(null))); socketChannel.pipeline().addLast(new NettyClientHandler()); } }); ChannelFuture future = bootstrap.connect(host, port).sync(); if (future.isSuccess()) { socketChannel = (SocketChannel) future.channel(); System.out.println("connect server 成功---------"); } //给关闭通道进行监听 future.channel().closeFuture().sync(); } finally { eventLoopGroup.shutdownGracefully(); } } }
NettyClientHandler.java
import com.alibaba.fastjson.JSON; import com.example.netty.dto.ChatDto; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.SimpleChannelInboundHandler; import io.netty.handler.timeout.IdleStateEvent; import io.netty.util.ReferenceCountUtil; import lombok.extern.slf4j.Slf4j; @Slf4j public class NettyClientHandler extends SimpleChannelInboundHandler<Object> { @Override public void handlerAdded(ChannelHandlerContext ctx) throws Exception { log.info(">>>>>>>>连接"); } @Override public void handlerRemoved(ChannelHandlerContext ctx) throws Exception { log.info(">>>>>>>>退出"); } @Override public void userEventTriggered(ChannelHandlerContext ctx, Object evt) { if (evt instanceof IdleStateEvent) { IdleStateEvent e = (IdleStateEvent) evt; switch (e.state()) { case WRITER_IDLE: /** * 利用写空闲发送心跳检测消息 */ ChatDto pingDto=new ChatDto(); pingDto.setMsg("我是心跳包"); ctx.writeAndFlush(JSON.toJSONString(pingDto)); log.info("send ping to server----------"); break; default: break; } } } /** * 客户端接收到服务端发的数据 * @param channelHandlerContext * @param obj * @throws Exception */ @Override protected void channelRead0(ChannelHandlerContext channelHandlerContext, Object obj) { log.info(">>>>>>>>>>>>>客户端接收到消息:{}", obj); ReferenceCountUtil.release(obj); } /** * socket通道处于活动状态 * @param ctx * @throws Exception */ @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { log.info(">>>>>>>>>>socket建立了"); super.channelActive(ctx); } /** * socket通道不活动了 * @param ctx * @throws Exception */ @Override public void channelInactive(ChannelHandlerContext ctx) throws Exception { log.info(">>>>>>>>>>socket关闭了"); super.channelInactive(ctx); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { super.exceptionCaught(ctx, cause); ctx.close(); } }
SprigBoot启动类添加服务端启动代码
@SpringBootApplication public class NettyApplication { public static void main(String[] args) { SpringApplication.run(NettyApplication.class, args); try { NettyTcpServerBootstrap bootstrap = new NettyTcpServerBootstrap(9999); bootstrap.start(); } catch (Exception e) { e.printStackTrace(); System.out.println("server socket 启动失败"); } } }
ChatController.java
import com.alibaba.fastjson.JSON; import com.example.netty.dto.ChatDto; import com.example.netty.socket.NettyClientBootstrap; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RestController; import java.util.UUID; /** * 客户端消息发送控制器 */ @RestController @Slf4j public class ChatController { private static String clientId=UUID.randomUUID().toString(); public static NettyClientBootstrap bootstrap; /** * 发送消息demo * @param msg */ @PostMapping(value = "/send") public void send(String msg) { if (bootstrap == null) { try { /** * 连接 输入服务器的端口和ip */ bootstrap = new NettyClientBootstrap(9999, "localhost"); } catch (InterruptedException e) { e.printStackTrace(); log.error(">>>>>>>>> server socket 连接失败"); } } /** * 发送消息 */ ChatDto dto=new ChatDto(); dto.setClientId(clientId).setMsg(msg); /** * json字符串发送 */ bootstrap.socketChannel.writeAndFlush(JSON.toJSONString(dto)); } }
访问
-----------------------有任何问题可以在评论区评论,也可以私信我,我看到的话会进行回复,欢迎大家指教------------------------
(蓝奏云官网有些地址失效了,需要把请求地址lanzous改成lanzoux才可以)