最近开始学习netty,自己写了个服务端的demo,包含从接收到客户端的数据流到完成业务逻辑并回发数据给客户端这一整个过程,下面开始正文。
先看一下工程目录
添加netty包
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.14.Final</version>
<scope>compile</scope>
</dependency>
netty的一些初始化设置
public class NettyServer {
private int port;
public NettyServer(int port) {
this.port = port;
}
public void run() throws Exception {
EventLoopGroup bossGroup = new NioEventLoopGroup();// 用于处理I/O操作的多线程事件循环
EventLoopGroup workerGroup = new NioEventLoopGroup();// 线程池的数量和线程池都可以自己配置,默认的线程池数量为CPU数量 *2
try {
ServerBootstrap bootstrap = new ServerBootstrap();// netty的一个帮助类,用来设置服务器
bootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline channelPipeline = socketChannel.pipeline();
channelPipeline.addLast("decoder", new PacketFrameDecoder());
channelPipeline.addLast("encoder", new PacketFrameEncoder());
channelPipeline.addLast(new NettyServerHandler());
}
})
.option(ChannelOption.SO_BACKLOG, 128)
.childOption(ChannelOption.SO_KEEPALIVE, true);
// 绑定端口并开启服务
ChannelFuture channelFuture = bootstrap.bind(port).sync();
channelFuture.channel().closeFuture().sync();
} finally {
workerGroup.shutdownGracefully();
bossGroup.shutdownGracefully();
}
}
public static void main(String[] args) throws Exception {
System.out.println("server executed");
int port;
if (args.length > 0) {
port = Integer.parseInt(args[0]);
} else {
port = ConfigInfo.SERVER_PORT;
}
new NettyServer(port).run();
}
}
EventLoopGroup可以理解为线程池,这里初始化了两个EventLoopGroup,一个专门用于处理客户端连入事件,另一个用于处理读写事件。
channelPipeline.addLast("decoder", new PacketFrameDecoder());
channelPipeline.addLast("encoder", new PacketFrameEncoder());
channelPipeline.addLast(new NettyServerHandler());
这里配置了编解码器和一个处理通道事件的类,因为netty是基于数据流的形式和客户端交互的,因此需要对数据进行编解码,在接收到数据时将其转换为string,int等,在回发时要将数据转换为byte[]
这三个类的交互顺序是PacketFrameDecoder--->NettyServerHandler--->PacketFrameEncoder
public class PacketFrameDecoder extends LengthFieldBasedFrameDecoder {
private static final int MAX_PACKET_LENGTH = 8192 * 2;
private static final int LENGTH_FIELD_OFFSET = 0;
private static final int LENGTH_FIELD_LENGTH = 4;// 前几位代表报文长度,这里只能是1,2,3,4,8
private static final int LENGTH_ADJUSTMENT = -4;
private static final int INITIAL_BYTES_TO_STRIP = 0;
private Logger logger = Logger.getLogger(PacketFrameDecoder.class);
public PacketFrameDecoder() {
super(MAX_PACKET_LENGTH, LENGTH_FIELD_OFFSET, LENGTH_FIELD_LENGTH);
}
@Override
protected Object decode(ChannelHandlerContext ctx, ByteBuf in) throws Exception {
super.decode(ctx, in);
in.resetReaderIndex();// 重置索引
MsgHeader msgHeader = new MsgHeader();
byte[] bytes = new byte[in.readableBytes()];
in.readBytes(bytes);// 这里要用读的方式获取数据流,不能直接用getBytes方法,不然会出现一直可读而导致死循环的状态
msgHeader.fromBytes(bytes);
logger.info(msgHeader.toString());
return msgHeader;
}
}
MsgHeader是自定义的一个消息类,包含消息长度,消息类型,消息正文。这里的消息正文就是我们通常理解的pojo,只不过是以byte[]的形式存在。
public class NettyServerHandler extends ChannelInboundHandlerAdapter {
private Logger logger = Logger.getLogger(NettyServerHandler.class);
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ServiceDispatcher serviceDispatcher = new ServiceDispatcher();
serviceDispatcher.dispatch(ctx, (MsgHeader) msg);
ctx.close();
}
/**
* 这个方法是在客户端接入时会调用
*
* @param ctx
* @throws Exception
*/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
logger.info("一个客户端连入");
super.channelActive(ctx);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
// 当引发异常时关闭连接
cause.printStackTrace();
ctx.close();
}
}
这个类主要看channelRead方法,在这里将之前解析出来的MsgHeader分发给service,这里要传入上下文,之后还要通过它来发送消息给客户端
public class ServiceDispatcher {
private Logger logger = Logger.getLogger(ServiceDispatcher.class);
public void dispatch(ChannelHandlerContext ctx, MsgHeader msgHeader) {
IService service = null;
switch (msgHeader.getType()) {
case MsgType.REQ_LOGIN:
logger.info("接收到了客户端登录请求");
service = new LoginServiceImpl();
break;
default:
break;
}
if (service != null) {
service.handle(ctx, msgHeader);
}
}
}
在这里根据报文类型将数据分发给对应的service
接下来就是我们熟悉的业务逻辑啦,这里省略数据库操作
public class LoginServiceImpl implements LoginService {
private Logger logger = Logger.getLogger(LoginServiceImpl.class);
private Req_Login req_login;
private Res_Login res_login;
public void handle(ChannelHandlerContext ctx, MsgHeader msgHeader) {
fromBytes(msgHeader.getContent());
// 在这里执行数据库操作,比对账号密码
if (req_login.getPassword().equals("123456") && req_login.getUsername().equals("Mike")) {
logger.info("登录成功");
res_login = new Res_Login(MsgType.SUCCESS);
} else {
logger.info("登录失败");
res_login = new Res_Login(MsgType.FAILURE);
}
MsgHeader resMsg = new MsgHeader();
byte[] resContent = toBytes();
resMsg.setContent(resContent);
resMsg.setType(MsgType.RES_LOGIN);
resMsg.setLength(resContent.length + 8);
// 响应报文组装完成,接下来发送
logger.info("即将发送的东西是:" + msgHeader.toString() + "-->实体类:" + res_login.toString());
final ChannelFuture channelFuture = ctx.writeAndFlush(resMsg);
channelFuture.addListener(new ChannelFutureListener() {
public void operationComplete(ChannelFuture channelFuture) throws Exception {
logger.info("响应报文发送成功");
channelFuture.channel().close();
}
});
}
public byte[] toBytes() {
if (res_login == null) {
throw new NullPointerException("response entity is null");
}
return BigEndian.putInt(res_login.getErrCode());
}
public void fromBytes(byte[] src) {
req_login = new Req_Login();
int offset = 0;
int usernameLength = BigEndian.getInt(src, offset);
offset += 4;
req_login.setUsername(BigEndian.getString(src, offset, usernameLength));
offset += usernameLength;
int passwordLength = BigEndian.getInt(src, offset);
offset += 4;
req_login.setPassword(BigEndian.getString(src, offset, passwordLength));
}
}
还记MsgHeader的消息正文吗?它是数据流,因此需要把它转换为pojo我们才能操作,具体看fromBytes方法,逻辑简单这里就不赘述了。
判断完登录数据后我们该发响应报文给客户端了,通过MsgHeader进行组装报文,其中报文正文则是通过toBytes方法转换为数据流。
接着调用上下文的writeAndFlush方法将数据写入
之后数据被传到编码器的类,在这里MsgHeader自身的toBytes方法把所有数据都转换成流的形式,写入ByteBuf中,之后客户端将收到消息
public class PacketFrameEncoder extends MessageToByteEncoder<MsgHeader> {
@Override
protected void encode(ChannelHandlerContext channelHandlerContext, MsgHeader msgHeader, ByteBuf byteBuf) throws Exception {
byteBuf.writeBytes(msgHeader.toBytes());
}
}
客户端同理,导入netty包,其他操作和服务端类似
客户端可以通过ChannelFuture的引用向服务端发送消息
public class Net {
public static void main(String[] args) throws Exception {
String host = args[0];
int port = Integer.parseInt(args[1]);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
Bootstrap b = new Bootstrap(); // (1)
b.group(workerGroup); // (2)
b.channel(NioSocketChannel.class); // (3)
b.option(ChannelOption.SO_KEEPALIVE, true); // (4)
b.handler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline channelPipeline = ch.pipeline();
channelPipeline.addLast("decoder", new PacketFrameDecoder());
channelPipeline.addLast("encoder", new PacketFrameEncoder());
channelPipeline.addLast(new NetHandler());
}
});
// Start the client.
ChannelFuture f = b.connect(host, port).sync(); // (5)
System.out.println("请输入账号和密码");
Scanner scanner = new Scanner(System.in);
String username = scanner.nextLine();
String password = scanner.nextLine();
AccountEntity entity = new AccountEntity(username, password);
MsgHeader msgHeader = new MsgHeader();
msgHeader.setContent(entity.toBytes());
msgHeader.setType(MsgType.REQ_LOGIN);
msgHeader.setLength(msgHeader.getContent().length + 8);
f.channel().writeAndFlush(msgHeader);
// Wait until the connection is closed.
f.channel().closeFuture().sync();
} finally {
workerGroup.shutdownGracefully();
}
}
}