1、配置启动类(第一种写法,普遍但是阻塞,不太推荐)
@Slf4j
@Component
public class NettyServer {
@Autowired
private NettyServerChannelInitializer nettyServerChannelInitializer;
@PostConstruct
public void start() {
//创建接收请求和处理请求的实例(默认线程数为 CPU 核心数乘以2也可自定义)
NioEventLoopGroup bossGroup = new NioEventLoopGroup(1);
NioEventLoopGroup workerGroup = new NioEventLoopGroup(2);
try {
//创建服务端启动辅助类(boostrap 用来为 Netty 程序的启动组装配置一些必须要组件,例如上面的创建的两个线程组)
ServerBootstrap socketBs = new ServerBootstrap();
//channel 方法用于指定服务器端监听套接字通道
//socket配置
socketBs.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
//ChannelOption.SO_BACKLOG对应的是tcp/ip协议listen函数中的backlog参数,
// 函数listen(int socketfd,int backlog)用来初始化服务端可连接队列,
// 服务端处理客户端连接请求是顺序处理的,所以同一时间只能处理一个客户端连接,
// 多个客户端来的时候,服务端将不能处理的客户端连接请求放在队列中等待处理,backlog参数指定了队列的大小
.option(ChannelOption.SO_BACKLOG, 1024)
//快速复用,防止服务端重启端口被占用的情况发生
.option(ChannelOption.SO_REUSEADDR, true)
.childHandler(nettyServerChannelInitializer)
//如果TCP_NODELAY没有设置为true,那么底层的TCP为了能减少交互次数,会将网络数据积累到一定的数量后,
// 服务器端才发送出去,会造成一定的延迟。在互联网应用中,通常希望服务是低延迟的,建议将TCP_NODELAY设置为true
.childOption(ChannelOption.TCP_NODELAY, true)
//默认的心跳间隔是7200s即2小时。Netty默认关闭该功能。
.childOption(ChannelOption.SO_KEEPALIVE, true);
//默认的心跳间隔是7200s即2小时。Netty默认关闭该功能。
//.childOption(ChannelOption.SO_KEEPALIVE, true);
//这里的sync()意思是异步阻塞,等待绑定成功,当绑定成功的时候会notifyAll唤醒所有线程,代码继续往下执行,此时如果注册出错,会关闭ChannelFuture ->取消socketFuture.channel().closeFuture().sync()线程阻塞
ChannelFuture socketFuture = socketBs.bind(8688).sync();
//这里的sync()意思是异步阻塞,等待通道成功关闭或者注册异常的时候的时候会notifyAll唤醒所有线程,然后执行finally里的代码,优雅的关闭线程组,去掉这里会直接关闭,具体可参考【https://gorden5566.com/post/1066.html】
socketFuture.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
//最终会执行这里关闭线程组
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
为什么说不太推荐,查了很多资料,也百度了很久,基本的争议是socketFuture.channel().closeFuture().sync();这行代码,这里只要不关闭,就会一直阻塞,导致之后的所有代码不会执行,而这一行代码的意义就是为了让netty更加优雅的关闭,但是在实际开发中,作为服务端来说,如果你有把netty单独停掉的需求,你可以用第一种方法,但是即使这样,为什么不写一个优雅停止的方法呢?在手动关闭netty的时候,顺便手动释放一下线程组,同样可以达到优雅关闭的目的。就像这样
try {
ChannelFuture future = nettyServer.future.channel().close().sync();
future.addListener(future1 -> {
if(future1.isSuccess()) {
nettyServer.shutdown();
}
});
} catch (InterruptedException e) {
e.printStackTrace();
}
启动类第二种写法:
@Slf4j
@Component
public class NettyServer {
@Autowired
private NettyServerChannelInitializer nettyServerChannelInitializer;
private NioEventLoopGroup bossGroup;
private NioEventLoopGroup workerGroup;
private ChannelFuture future;
@PostConstruct
public void start() {
//创建接收请求和处理请求的实例(默认线程数为 CPU 核心数乘以2也可自定义)
bossGroup = new NioEventLoopGroup(1);
workerGroup = new NioEventLoopGroup(2);
try {
//创建服务端启动辅助类(boostrap 用来为 Netty 程序的启动组装配置一些必须要组件,例如上面的创建的两个线程组)
ServerBootstrap socketBs = new ServerBootstrap();
//channel 方法用于指定服务器端监听套接字通道
//socket配置
socketBs.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
//ChannelOption.SO_BACKLOG对应的是tcp/ip协议listen函数中的backlog参数,
// 函数listen(int socketfd,int backlog)用来初始化服务端可连接队列,
// 服务端处理客户端连接请求是顺序处理的,所以同一时间只能处理一个客户端连接,
// 多个客户端来的时候,服务端将不能处理的客户端连接请求放在队列中等待处理,backlog参数指定了队列的大小
.option(ChannelOption.SO_BACKLOG, 1024)
//快速复用,防止服务端重启端口被占用的情况发生
.option(ChannelOption.SO_REUSEADDR, true)
.childHandler(nettyServerChannelInitializer)
//如果TCP_NODELAY没有设置为true,那么底层的TCP为了能减少交互次数,会将网络数据积累到一定的数量后,
// 服务器端才发送出去,会造成一定的延迟。在互联网应用中,通常希望服务是低延迟的,建议将TCP_NODELAY设置为true
.childOption(ChannelOption.TCP_NODELAY, true)
//默认的心跳间隔是7200s即2小时。Netty默认关闭该功能。
.childOption(ChannelOption.SO_KEEPALIVE, true);
future = socketBs.bind(8688).sync();
future.addListener(future1 -> log.info("Netty服务端启动成功"));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/**
*这里可以手动关闭,同时在整个springboot应用停止的时候,这里也得以调用【@PreDestroy】
*参考 https://www.cnblogs.com/CreatorKou/p/11606870.html
*/
@PreDestroy
public void shutdown() {
// 优雅关闭两个 EventLoopGroup 对象
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
log.info("[*Netty服务端关闭成功]");
}
}
2、配置channel管道规则
/**
* @author: zhouwenjie
* @description: 配置管道 服务端初始化,客户端与服务器端连接一旦创建,这个类中方法就会被回调,设置出站编码器和入站解码器
* @create: 2020-04-03 14:14
**/
@Component
public class NettyServerChannelInitializer extends ChannelInitializer<SocketChannel> {
@Autowired
private NettyServerHandler nettyServerHandler;
@Autowired
private HeartBeatServerHandler heartBeatServerHandler;
@Override
protected void initChannel(SocketChannel ch) {
ChannelPipeline pipeline = ch.pipeline();
//数据传输规则,防止粘包(客户端和服务端发送的消息结尾必须都要带上@_)
String delimiter = "@_";
//设置心跳机制 0永不超时,三个参数分别代表 读超时,写超时,读写超时
pipeline.addLast(new IdleStateHandler(30,0,0, TimeUnit.SECONDS));
//限制每次数据传输的长度,防止违法大数据攻击,放在编码器上边
pipeline.addLast(new DelimiterBasedFrameDecoder(1024*8, Unpooled.wrappedBuffer(delimiter.getBytes())));
//通过换行符,即\n或者\r\n对数据进行处理 System.getProperty("line.separator")
//pipeline.addLast(new LineBasedFrameDecoder(1024));
pipeline.addLast(new StringDecoder(Charset.forName("UTF-8")));
pipeline.addLast(new StringEncoder(Charset.forName("UTF-8")));
//一定在编码器下边,否则不生效
pipeline.addLast(new DelimiterBasedFrameEncoder(delimiter));
//心跳放在正常handler前边,先走心跳处理器
pipeline.addLast(heartBeatServerHandler);
pipeline.addLast(nettyServerHandler);
}
}
切记:Handler根据需求可配置多个,但是先后位置一定要选择好,否则可能导致不生效,先触发执行的放在前边,被引用的放在引用的前边。
这里是将心跳(heartBeatServerHandler)的逻辑单独出来,实现更好的解耦,nettyServerHandler中可以更好的处理正常逻辑,下边分别是心跳处理器和正常逻辑处理器的代码。
心跳处理器 HeartBeatServerHandler
为什么需要心跳:当客户端异常退出或者路由器断线等特殊情况的时候,连接可能并不会自动关闭。但是对于应用而言,每一个连接都是宝贵的,所以不能让冗余的连接存在来浪费资源。
/**
* @author: zhouwenjie
* @description: 心跳检测处理器
* @create: 2022-03-25 16:12
**/
@Slf4j
@Component
@ChannelHandler.Sharable
public class HeartBeatServerHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
// 接收到心跳请求,处理心跳消息,否则进入下一处理流程,心跳规则(ping,pong)
String s = (String) msg;
if ("ping".equals(s)) {
ctx.writeAndFlush("pong");
} else {
//fireChannelRead 表示传递消息至下一个处理器
ctx.fireChannelRead(msg);
}
}
/**
* 功能描述: 心跳检测
*
* @param ctx 这里的作用主要是解决断网,弱网的情况发生
* @param evt
* @return void
* @author zhouwenjie
* @date 2020/4/3 17:02
*/
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) {
String socketString = ctx.channel().remoteAddress().toString();
if (evt instanceof IdleStateEvent) {
IdleStateEvent event = (IdleStateEvent) evt;
if (event.state() == IdleState.READER_IDLE) {
log.info("Client RSU: " + socketString + " READER_IDLE 读超时");
ctx.disconnect();
}
}
}
/**
* 在处理过程中引发异常时被调用
*
* @param ctx
* @param cause
* @throws Exception
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
log.error("[server] heart response error: {}", cause.getMessage());
ctx.fireExceptionCaught(cause);
}
}
提示:心跳规则通过NettyServerChannelInitializer中的IdleStateHandler处理器进行配置。
正常逻辑处理器 NettyServerHandler
/**
* @author: zhouwenjie
* @description: 服务端业务处理类
* @create: 2020-04-03 14:13
**/
@Slf4j
@Component
@ChannelHandler.Sharable
public class NettyServerHandler extends ChannelInboundHandlerAdapter {
/**
* 用于记录和管理所有客户端的channle,客户端掉线会自动移除,不用手动触发
*/
public static ChannelGroup clients = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
/**
* 功能描述:客户端连接时触发
*/
@Override
public void channelActive(ChannelHandlerContext ctx) {
clients.add(ctx.channel());
System.out.println(ctx.channel().id().asLongText() + "连接成功,web客户端数量:" + clients.size() + "个");
}
/**
* 功能描述:客户端断开时触发
*/
@Override
public void channelInactive(ChannelHandlerContext ctx) {
//不用手动移除,ChannelGroup可以自动从分组里移除ctx
System.out.println(ctx.channel().id().asLongText() + "连接断开,剩余web客户端数量:" + clients.size() + "个");
}
/**
* 功能描述: 有客户端发消息会触发此函数
*
* @param ctx
* @param msg
* @return void
* @author zhouwenjie
* @date 2020/4/3 16:48
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
User user = JSON.parseObject(msg.toString(), User.class);
}
/**
* 功能描述:
*
* @param ctx
* @param cause
* @return void
* @author 发生异常会触发此函数
* @date 2020/4/3 16:49
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
log.error("连接异常,异常信息:" + cause.getMessage());
cause.printStackTrace();
}
}
启动类配置 NettyServer
/**
* @author: zhouwenjie
* @description: netty启动配置类
* @create: 2020-04-03 11:43
**/
@Component
public class NettyServer {
@Value("${netty.webSocket_port}")
private int webSocketPort;
@Autowired
private WebSocketChannelInitializer webSocketChannelInitializer;
private NioEventLoopGroup bossGroup;
private NioEventLoopGroup workerGroup;
public void start() {
//创建接收请求和处理请求的实例(默认线程数为 CPU 核心数乘以2也可自定义)
bossGroup = new NioEventLoopGroup(1);
workerGroup = new NioEventLoopGroup(4);
try {
//创建服务端启动辅助类(boostrap 用来为 Netty 程序的启动组装配置一些必须要组件,例如上面的创建的两个线程组)
ServerBootstrap webSocketBs = new ServerBootstrap();
//webSocket配置
webSocketBs.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(webSocketChannelInitializer)
//ChannelOption.SO_BACKLOG对应的是tcp/ip协议listen函数中的backlog参数,函数listen(int socketfd,int backlog)用来初始化服务端可连接队列,服务端处理客户端连接请求是顺序处理的,所以同一时间只能处理一个客户端连接,多个客户端来的时候,服务端将不能处理的客户端连接请求放在队列中等待处理,backlog参数指定了队列的大小
.option(ChannelOption.SO_BACKLOG, 10);
// .childOption(ChannelOption.SO_KEEPALIVE, true);
ChannelFuture webSocketFuture = webSocketBs.bind(webSocketPort).sync();
webSocketFuture .addListener(future1 -> log.info("Netty服务端启动成功"));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@PreDestroy
public void shutdown() {
// 优雅关闭两个 EventLoopGroup 对象
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
log.info("[*Netty服务端关闭成功]");
}
}
channel管道配置 WebSocketChannelInitializer
/**
* @author: zhouwenjie
* @description: websocket
* @create: 2020-10-29 16:31
**/
@Component
public class WebSocketChannelInitializer extends ChannelInitializer<SocketChannel> {
@Autowired
private TextWebSocketHandler textWebSocketHandler;
@Override
protected void initChannel(SocketChannel ch) {
ChannelPipeline pipeline = ch.pipeline();
// webSocket 基于http协议,所以要有http编解码器
//以下配置放在最下方不生效
//这里没做心跳,需要的话可以参照socket的做法来做心跳,或者直接在下方userEventTriggered中编写心跳逻辑
//pipeline.addLast(new IdleStateHandler(3,0,0, TimeUnit.SECONDS));
pipeline.addLast(new HttpServerCodec());
// 对写大数据流的支持
pipeline.addLast(new ChunkedWriteHandler());
//用于将http数据聚合到一起发送一个请求 fullHttpRequest
pipeline.addLast(new HttpObjectAggregator(8192));
//设置成true,开启前缀【/ws/alarmHost】检查,否则无法获取uri参数,排除无效连接
pipeline.addLast(new WebSocketServerProtocolHandler("/ws/alarmHost",true));
pipeline.addLast(textWebSocketHandler);
}
}
处理器 TextWebSocketHandler
/**
* @author: zhouwenjie
* @description:
* @create: 2020-10-29 16:35
**/
@Component
@ChannelHandler.Sharable
public class TextWebSocketHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
/**
* 用于记录和管理所有客户端的channle
*/
public static ChannelGroup clients = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
@Override
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
System.out.println("连接首先进入 =====================");
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) {
String content = msg.text();
System.out.println("接受到的数据:" + content);
//转发消息注意项,见下方注明
ctx.writeAndFlush(new TextWebSocketFrame(content));
}
/**
* 此方法在连接之后可以第一时间拿到请求参数及请求头信息,所以可以在这里对连接客户端进行保存等处理
* 另外此方法也可用于心跳事件处理
*/
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
System.out.println("连接之后进入 =====================");
//websocket握手成功触发
if (evt instanceof HandshakeComplete) {
clients.add(ctx.channel());
HandshakeComplete handshakeComplete= (HandshakeComplete) evt;
String s = handshakeComplete.requestUri();
System.out.println(ctx.channel().id().asLongText() + "连接ffff成功,web客户端数量:" + clients.size() + "个"+s);
}
super.userEventTriggered(ctx,evt);
}
/**
* 功能描述:客户端断开时触发
*/
@Override
public void handlerRemoved(ChannelHandlerContext ctx) {
//不用手动一处,ChannelGroup可以自动从分组里移除ctx
System.out.println(ctx.channel().id().asLongText() + "连接断开,剩余web客户端数量:" + clients.size() + "个");
}
/**
* 功能描述:客户端异常时触发
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
cause.printStackTrace();
ctx.close();
}
/*
* 这个方法也可以拿到用户传递的参数,但是触发条件是必须在通信时才能获取
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
//通过这个方法可以拿到web客户端传递过来的参数以及请求头(heads)
if (msg != null && msg instanceof DefaultHttpRequest) {
HttpRequest request = (HttpRequest) msg;
String uri = request.uri();
String id = uri.split("/")[2];
idCtxMap.put(id,ctx);
request.setUri("/ws");
}
super.channelRead(ctx,msg);
}
}
websocket收到消息,转发消息注意事项
这里举个例子,加入我们由两个ChannelHandlerContext =>ctx1、ctx2
protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) {
String content = msg.text();
ctx1.writeAndFlush(msg);//不起作用,因为msg已经被上边一行代码读过了,会报引用计数器异常,需要用下变得代码复制一份
TextWebSocketFrame frame = msg.retainedDuplicate();
ctx1.writeAndFlush(frame);//不起作用
ctx2.writeAndFlush(frame);//起作用
ctx1.writeAndFlush(new TextWebSocketFrame(content));//起作用,推荐使用这一种,上边的方法如果群发只有一个客户端能接收到
}
}
启动类 NettyUdpApplication
UDP协议的特点:
1、UDP传送数据前并不与对方建立连接,即UDP是无连接的。
2、UDP接收到的数据报不发送确认信号,发送端不知道数据是否被正确接收。
3、UDP传送数据比TCP快,系统开销也少。
UDP的客户端个服务端基本一样,主要是在handler的逻辑处理上
public void start() {
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(eventLoopGroup)
.channel(NioDatagramChannel.class)
.option(ChannelOption.SO_BROADCAST, true)
.handler(obuUdpClientHandler);
//这里的bind(receivePort)的意思是监听receivePort端口,端口只要有消息就获取 bind(0)这里的bootstrap只作为发送方存在,不接收回复
log.info("[Netty UDP客户端启动成功]");
bootstrap.bind(receivePort).sync();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@PreDestroy
public void shutdown() {
eventLoopGroup.shutdownGracefully();
log.info("[*Netty关闭成功]");
}
处理器 UdpClientHandler
@Component
@Slf4j
@ChannelHandler.Sharable
public class ObuUdpClientHandler extends SimpleChannelInboundHandler<DatagramPacket> {
@Override
public void channelActive(ChannelHandlerContext ctx) {
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, DatagramPacket packet) {
//String msg=packet.content().toString(StandardCharsets.UTF_8)
String msg;
try {
ByteBuf byteBuf = packet.content();
byte[] bytes = new byte[byteBuf.readableBytes()];
byteBuf.readBytes(bytes);
msg = new String(bytes);
JSONObject jsonObject = JSON.parseObject(msg);
} catch (IllegalArgumentException e) {
e.printStackTrace();
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
super.exceptionCaught(ctx, cause);
cause.printStackTrace();
}
}
UDP发送消息
#监听接收地址填写255.255.255.255,因为是局域网。
receiveHost: 255.255.255.255
#监听接收端口
receivePort: 7301
#发送端口
sendHost: 255.255.255.255
#发送端口
sendPort: 7302
ctx.writeAndFlush(new DatagramPacket(Unpooled.copiedBuffer(msg, Charset.forName("utf-8")), new InetSocketAddress(sendHost, sendPort)));
客户端需要做断线重连机制
private NioEventLoopGroup eventLoopGroup = new NioEventLoopGroup();
private Channel channel;
private Bootstrap bootstrap;
public void run() {
bootstrap = new Bootstrap();
bootstrap.group(eventLoopGroup)
.channel(NioSocketChannel.class)
.remoteAddress(host, port)
//如果TCP_NODELAY没有设置为true,那么底层的TCP为了能减少交互次数,会将网络数据积累到一定的数量后,
//服务器端才发送出去,会造成一定的延迟。在互联网应用中,通常希望服务是低延迟的,建议将TCP_NODELAY设置为true
//建议只是客户端设置此参数,服务端设置会有无效警告
.option(ChannelOption.TCP_NODELAY, true)
//快速复用,防止服务端重启端口被占用的情况发生
.option(ChannelOption.SO_KEEPALIVE, true)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) {
//客户端初始化
String delimiter = "@_";
socketChannel.pipeline().addLast(new DelimiterBasedFrameDecoder(1024*8, Unpooled.wrappedBuffer(delimiter.getBytes())));
socketChannel.pipeline().addLast(new StringDecoder(Charset.forName("utf-8")));
socketChannel.pipeline().addLast(new StringEncoder(Charset.forName("utf-8")));
//socketChannel.pipeline().addLast(new LineBasedFrameDecoder(1024));
socketChannel.pipeline().addLast(new DelimiterBasedFrameEncoder(delimiter));
socketChannel.pipeline().addLast(nettyClientHandler);
}
});
//用于一台电脑多网口的情况下指定客户端的IP地址,端口为0,不指定
//bootstrap.localAddress("127.0.0.1",0);
//连接netty服务器
reconnect();
}
/**
* 功能描述: 断线重连,客户端有断线重连机制,就更不能使用异步阻塞了
* @param
* @return void
* @author zhouwenjie
* @date 2021/3/19 14:53
*/
public void reconnect() {
ChannelFuture channelFuture = bootstrap.connect();
//使用最新的ChannelFuture -> 开启最新的监听器
channelFuture.addListener((ChannelFutureListener) future -> {
if (future.cause() != null) {
log.error("连接失败。。。");
future.channel().eventLoop().schedule(() -> reconnect(), 2, TimeUnit.SECONDS);
} else {
log.info("连接成功。。。");
}
});
}
/**
* 关闭 client
*/
@PreDestroy
public void shutdown() {
// 优雅关闭 EventLoopGroup 对象
eventLoopGroup.shutdownGracefully();
log.info("[*Netty客户端关闭]");
}
handler代码
/**
* @author: zhouwenjie
* @description: 客户端处理类
* @create: 2020-04-03 17:45
**/
@Component
@Slf4j
@ChannelHandler.Sharable
public class NettyClientHandler extends SimpleChannelInboundHandler<String> {
/**
* 注入NettyClient
*/
@Autowired
private NettyClient nettyClient;
/**
* 连接成功
*/
@Override
public void channelActive(ChannelHandlerContext ctx) {
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
log.error("[*The netty server suspends service...]");
super.channelInactive(ctx);
ctx.fireChannelInactive();
nettyClient.reconnect();
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
super.exceptionCaught(ctx, cause);
log.error("[* Netty connection exception]:{}", cause.toString());
cause.printStackTrace();
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, String msg) {
}
}
当然,文中如果有理解不对的地方,欢迎指出!!!