整合SpringBoot + Netty 进行一个小白入门的第一个Java项目,可以作为毕设和学习项目的敲门砖。
项目的开头,创建项目以及添加相关的的依赖。接下来就把手交到我手里吧,跟着我一起走进Netty 。
1、创建 Maven 依赖
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.2.RELEASE</version>
<relativePath /> <!-- lookup parent from repository -->
</parent>
<dependencies>
<!-- Netty依赖 -->
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.32.Final</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- protubuf -->
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
<version>3.5.1</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.google.protobuf/protobuf-java-util -->
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java-util</artifactId>
<version>3.5.1</version>
</dependency>
<!-- https://mvnrepository.com/artifact/io.grpc/grpc-all -->
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-all</artifactId>
<version>1.11.0</version>
</dependency>
</dependencies>
2、创建Netty server 服务
why:在创建服务端,要明白为什么EventLoopGroup需要两个?
because:因为netty线程模式,可以去网上找找资料,这类的资料百度。
他用到两个线程组worKerGroup、bossGroup
主线程主要用于接收client连接的接收,创建相关资源。
从线程主要用于处理通道的读写、解码、编码。
可以看下ServerBootstrap的group的方法,入参有两个方法,从方法名就可以看出父和子,如果线程数不够,在定义的时候就定好就,线程数不一定要很多,最好不要超过本身CPU线程,可以通过测试找出来,最合适的线程数。
why:Channel通道是拿来干啥?
because:处理相应的逻辑,netty有个流水线,他把所有的事件放到pip中,他就会顺序执行,而Channel通道也有相应的生命周期比如:
handlerAdded: handler被添加到channel的pipeline
channelRegistered: channel注册到NioEventLoop
channelActive: channel准备好了,可以进行
channelRead: channel有可以读取的数据
还有一些注销的事件
public ServerBootstrap group(EventLoopGroup parentGroup, EventLoopGroup childGroup) {
super.group(parentGroup);
if (childGroup == null) {
throw new NullPointerException("childGroup");
}
if (this.childGroup != null) {
throw new IllegalStateException("childGroup set already");
}
this.childGroup = childGroup;
return this;
}
nettSerever的实现,配置相应的端口、通道设置、流水线等,进行一个创建
package com.binglian.netty;
import com.binglian.handler.HelloServerHandler;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.PooledByteBufAllocator;
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.NioServerSocketChannel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.net.InetSocketAddress;
@Slf4j
@Component
public class NettyServer {
// 通过nio方式来接收链接何处理链接
// 这两定义两个线程组
// bossGroup 是主线程组,只是接收客户的连接
// worKerGroup是从线程组,用于处理通道的
private EventLoopGroup bossGroup;
private EventLoopGroup worKerGroup;
// 启动类
private ServerBootstrap bootstrap = new ServerBootstrap();
public void run() {
// 链接监听线程组
bossGroup = new NioEventLoopGroup(1);
// 传输处理线程组
worKerGroup = new NioEventLoopGroup();
try {
// 1设置reactor 线程
bootstrap.group(bossGroup, worKerGroup)
.channel(NioServerSocketChannel.class) // 2、设置nio类型的channel
.localAddress(new InetSocketAddress(8080)) //3、设置监听端口
.option(ChannelOption.ALLOCATOR,
PooledByteBufAllocator.DEFAULT) // 4、设置通道选项
// 5、装配流水线
.childHandler(new ChannelInitializer<SocketChannel>() {
// 有链接到达时会创建一个channel
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new HelloServerHandler());
//
}
});
// 6、开始绑定server
// 通过调用sync同步方法阻塞直到绑定成功
ChannelFuture channelFuture = bootstrap.bind().sync();
log.info("服务启动,端口" + channelFuture.channel().localAddress());
ChannelFuture closeFuture =
channelFuture.channel().closeFuture();
closeFuture.sync();
} catch (Exception e) {
e.printStackTrace();
}finally {
// 8 优雅关闭EventLoopGroup,
// 释放掉所有资源包括创建的线程
worKerGroup.shutdownGracefully();
bossGroup.shutdownGracefully();
}
}
public static void main(String[] args) {
NettyServer nettyServer = new NettyServer();
nettyServer.run();
}
}
3、创建Serever Channel事件
他就是读取客户端发送的信息,以及发送信息到客户端,Netty 必须是字节才能接收到信息,这里就把msg转换成了,ByteBuf,进行一个日志输出。
// 传来的消息必须转换成字节缓冲区,才能接收到。netty必须是字节传输
ByteBuf byteBuf = (ByteBuf) msg;
log.info("客户端发来的消息:" + byteBuf.toString(CharsetUtil.UTF_8));
log.info("msg");
package com.binglian.handler;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.util.CharsetUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.concurrent.atomic.AtomicInteger;
@Slf4j
@Component
@ChannelHandler.Sharable
public class HelloServerHandler extends ChannelInboundHandlerAdapter {
//连接数
private AtomicInteger receivedConnecitons = new AtomicInteger();
@Override
public void channelActive(ChannelHandlerContext ctx) {
log.info("channelActive");
ctx.writeAndFlush(Unpooled.copiedBuffer("你好啊,客户端", CharsetUtil.UTF_8));
}
@Override
public void channelInactive(ChannelHandlerContext ctx) {
log.info("channelInactive");
}
/**
* 收到消息
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg)
throws Exception {
// 传来的消息必须转换成字节缓冲区,才能接收到。netty必须是字节传输
ByteBuf byteBuf = (ByteBuf) msg;
log.info("客户端发来的消息:" + byteBuf.toString(CharsetUtil.UTF_8));
log.info("msg");
}
}
4、创建Netty client 服务
package com.binglian.client;
import com.binglian.handler.HelloClientHandler;
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.PooledByteBufAllocator;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@Service
@Data
@Slf4j
public class NettyClient {
private String host;
private int port;
private Bootstrap bootstrap;
private EventLoopGroup group;
public NettyClient() {
/**
* 客户端的是Bootstrap,服务端的则是 ServerBootstrap。
* 都是AbstractBootstrap的子类。
**/
/**
* 通过nio方式来接收连接和处理连接
*/
group = new NioEventLoopGroup();
}
/**
* 连接
*/
public void doConnect() {
try {
bootstrap = new Bootstrap();
bootstrap.group(group)
.channel(NioSocketChannel.class)
.option(ChannelOption.SO_KEEPALIVE, true)
.option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT)
.remoteAddress("127.0.0.1", 8080)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new HelloClientHandler());
// ch.pipeline().addLast(chatMsgHandler);
// ch.pipeline().addLast(exceptionHandler);
}
});
log.info("客户端开始链接");
// netty都是异步 所以需要addListener
ChannelFuture future = bootstrap.connect().sync();
//关闭连接(异步非阻塞)
future.channel().closeFuture().sync();
} catch (Exception e) {
log.info("客户端连接失败!" + e.getMessage());
}
}
public static void main(String[] args) {
NettyClient nettyClient = new NettyClient();
nettyClient.doConnect();
}
}
5、创建Client Channel事件
代码里面很多Channel的事件,可以看下他是怎么样运行的顺序是怎么样,自己动手试一试。重点代码就在这发送。
当 client 准备就绪,就将”你好“转成utf8发送到服务器
ctx.writeAndFlush(Unpooled.copiedBuffer("你好啊!", CharsetUtil.UTF_8));
package com.binglian.handler;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.util.CharsetUtil;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class HelloClientHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRegistered(ChannelHandlerContext ctx)
throws Exception {
log.info("channelRegistered: channel注册到NioEventLoop");
super.channelRegistered(ctx);
}
@Override
public void channelUnregistered(ChannelHandlerContext ctx)
throws Exception {
log.info("channelUnregistered: channel取消和NioEventLoop的绑定");
super.channelUnregistered(ctx);
}
@Override
public void channelActive(ChannelHandlerContext ctx)
throws Exception {
log.info("channelActive: channel准备就绪");
ctx.writeAndFlush(Unpooled.copiedBuffer("你好啊!", CharsetUtil.UTF_8));
super.channelActive(ctx);
}
@Override
public void channelInactive(ChannelHandlerContext ctx)
throws Exception {
log.info("channelInactive: channel被关闭");
super.channelInactive(ctx);
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg)
throws Exception {
log.info("channelRead: channel中有可读的数据" );
// 传来的消息必须转换成字节缓冲区,才能接收到。netty必须是字节传输
ByteBuf byteBuf = (ByteBuf) msg;
System.out.println("服务器发来的消息:" + byteBuf.toString(CharsetUtil.UTF_8));
super.channelRead(ctx, msg);
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx)
throws Exception {
log.info("channelReadComplete: channel读数据完成");
super.channelReadComplete(ctx);
}
@Override
public void handlerAdded(ChannelHandlerContext ctx)
throws Exception {
log.info("handlerAdded: handler被添加到channel的pipeline");
super.handlerAdded(ctx);
}
@Override
public void handlerRemoved(ChannelHandlerContext ctx)
throws Exception {
log.info("handlerRemoved: handler从channel的pipeline中移除");
super.handlerRemoved(ctx);
}
}
最后我们运行一下代码,这里没有和Springboot整合,我们就用两个main方法执行一下(main方法就在上面的 server 服务和 client 服务),看能不能接收到信息、发送信息。
server日志:
可以服务器启动,当client链接的时候,就接受到客户端发送过来的信息,已经一些生命周期。
client日志:
我们看到了顺序:handleradded —> channelRegistered —> channelActive —> channelRead —>channelReadComplete
第一步我们就完成了,接下来就是Netty 的登录和session实现,快跟着我一步步实现通讯系统吧。