文章目录

  • ​​Netty框架入门​​
  • ​​客户端代码​​
  • ​​客户端业务handler​​
  • ​​服务端代码​​
  • ​​服务端业务handler​​
  • ​​关于bytebuf对象的释放问题​​

Netty框架入门

首先声明在学习的过程中参考了netty5的user guide。在此非常感谢netty的贡献者社区。在本篇博客中,将会基于netty5实现最简单的读取和发送数据的功能。代码中有一些自己的理解,以注释形式给出。
参考手册地址:​​​http://ifeve.com/netty5-user-guide/​

客户端代码

package netty;

import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
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 java.io.UnsupportedEncodingException;

/**
* @Auther: ;李泽
* @Date: 2019/3/4 22:05
* @Description:
*/
public class NettyClient {
public static void main(String[] args) throws InterruptedException, UnsupportedEncodingException {
EventLoopGroup eventLoopGroup = new NioEventLoopGroup();
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(eventLoopGroup)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
//这里面的是接受服务器端反馈时,才会触发的handler
socketChannel.pipeline().addLast(new ClientHandler());
}
});
ChannelFuture channelFuture1 = bootstrap.connect("127.0.0.1",8080).sync();
//客户端发送数据,借用netty的buffer转化工具
channelFuture1.channel().writeAndFlush(Unpooled.copiedBuffer("李泽".getBytes("utf-8")));

//可以理解为阻塞在这
channelFuture1.channel().closeFuture().sync();
eventLoopGroup.shutdownGracefully();
}
}

客户端业务handler

package netty;

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerAdapter;
import io.netty.channel.ChannelHandlerContext;
import io.netty.util.ReferenceCountUtil;

/**
* @Auther: ;李泽
* @Date: 2019/3/4 22:05
* @Description:
*/
public class ClientHandler extends ChannelHandlerAdapter {
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
super.exceptionCaught(ctx, cause);
cause.printStackTrace();
}

@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
//super.channelRead(ctx, msg);
try {
//获得反馈过来的信息,转化为bytebuffer,这个对象是netty封装的,不是原生的。
ByteBuf buffer = (ByteBuf)msg;
//创建一个空间,从buffer中拿出数据
byte[] bytes = new byte[buffer.readableBytes()];
buffer.readBytes(bytes);
//打印输出
String resp = new String(bytes,"utf-8");
System.out.println("resp = " + resp);
//buffer.release();
} finally {
ReferenceCountUtil.release(msg);
}
}
}

服务端代码

package netty;

import io.netty.bootstrap.ServerBootstrap;
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;

/**
* @Auther: ;李泽
* @Date: 2019/3/4 22:04
* @Description:
*/
public class NettyServer {
private int port;

public NettyServer(int port) {
this.port = port;
}

public void run() throws Exception {
/*
NioEventLoopGroup 是用来处理I/O操作的多线程事件循环器,Netty提供了许多不同的EventLoopGroup的实现用来处理不同传
输协议。在这个例子中我们实现了一个服务端的应用,因此会有2个NioEventLoopGroup会被使用。第一个经常被叫做‘boss’,
用来接收进来的连接。第二个经常被叫做‘worker’,用来处理已经被接收的连接,一旦‘boss’接收到连接,就会把连接信
息注册到‘worker’上。如何知道多少个线程已经被使用,如何映射到已经创建的Channels上都需要依赖于EventLoopGroup的
实现,并且可以通过构造函数来配置他们的关系。
*/
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
/*
ServerBootstrap 是一个启动NIO服务的辅助启动类。你可以在这个服务中直接使用Channel,但是这会是一个复杂的处
理过程,在很多情况下你并不需要这样做。
*/
ServerBootstrap b = new ServerBootstrap();
//给启动器配置两个线程组
b.group(bossGroup, workerGroup)
//这里我们指定使用NioServerSocketChannel类来举例说明一个新的Channel如何接收进来的连接。
.channel(NioServerSocketChannel.class)
/*
这里的事件处理类经常会被用来处理一个最近的已经接收的Channel。ChannelInitializer是一个特殊的处理类,
他的目的是帮助使用者配置一个新的Channel。也许你想通过增加一些处理类比如ServerHandler来配置一
个新的Channel或者其对应的ChannelPipeline来实现你的网络程序。当你的程序变的复杂时,可能你会增加更多
的处理类到pipline上,然后提取这些匿名类到最顶层的类上。
*/
.childHandler(new ChannelInitializer <SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new ServerHandler());
}
})
/*
你可以设置这里指定的通道实现的配置参数。我们正在写一个TCP/IP的服务端,因此我们被允许设置socket的参
数选项比如tcpNoDelay和keepAlive。
*/
/*
SO_BACKLOG参数解释:服务器端的内核模块维护有两个队列,我们称之为A,B。
客户端向服务器端connect的时候,会发送带有SYN标志的包,称之为第一次握手。
服务器收到客户端发来的SYN时,向客户端发送SYN_ACk确认,称之为第二次握手。
此时TCP内核模块把客户端连接加入到A队列中,然后服务器收到客户端发来的ACK时,称之为第三次握手
TCP内核把客户端连接从A队列移动到B队列,连接完成,应用程序的accept被触发,产生返回值。
也就是说accept从B队列中取出完成三次握手的连接。
A,B的队列的长度之和是BACKLOG,若AB队列之和大于BACKLOG时,新连接会被tcp内核拒绝。
所以,如果BACKLOG过小,可能会出现accept速度跟不上,AB满了,导致新连接无法接入。
需要注意的是:BACKLOG对支持的连接数并无影响,影响的只是还没被accept取出的连接。
*/
.option(ChannelOption.SO_BACKLOG, 128)
/*
你关注过option()和childOption()吗?option()是提供给NioServerSocketChannel用来接收进来的连接。childOption()
是提供给由父管道ServerChannel接收到的连接,在这个例子中也是NioServerSocketChannel。
*/
/*
保持连接不断开
*/
.option(ChannelOption.SO_KEEPALIVE, true);

// 绑定并且启动服务来接受到来的请求
ChannelFuture f = b.bind(port).sync(); // (7)

// 在服务关闭之前都会等待
// 在这个例子中这是不会发生的, 但是你也可以写上这个来做到完美
// 关闭你的服务.
f.channel().closeFuture().sync();
} finally {
workerGroup.shutdownGracefully();
bossGroup.shutdownGracefully();
}
}

public static void main(String[] args) throws Exception {
int port;
if (args.length > 0) {
port = Integer.parseInt(args[0]);
} else {
port = 8080;
}
new NettyServer(port).run();
}
}

服务端业务handler

package netty;

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerAdapter;
import io.netty.channel.ChannelHandlerContext;
import io.netty.util.ReferenceCountUtil;


/**
* @Auther: ;李泽
* @Date: 2019/3/4 22:05
* @Description: 继承自ChannelHandlerAdapter,这个类实现了ChannelHandler接口,ChannelHandler提供了许多事件处理
* 的接口方法,然后你可以覆盖这些方法。现在仅仅只需要继承ChannelHandlerAdapter类而不是你自己去实
* 现接口方法。
*/
public class ServerHandler extends ChannelHandlerAdapter {
/**
* 功能描述: exceptionCaught()事件处理方法是当出现Throwable对象才会被调用,即当Netty由于IO错误或者处理器在处理事件
* 时抛出的异常时。在大部分情况下,捕获的异常应该被记录下来并且把关联的channel给关闭掉。然而这个方法的处
* 理方式会在遇到不同异常的情况下有不同的实现,比如你可能想在关闭连接之前发送一个错误码的响应消息。
*
* @auther: 李泽
* @date: 2019/3/4 22:24
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
super.exceptionCaught(ctx, cause);
}
/**
* 功能描述: 这里我们覆盖了chanelRead()事件处理方法。每当从客户端收到新的数据时,这个方法会在收到消息时被调用,
* 这个例子中,收到的消息的类型是ByteBuf
*
* @auther: 李泽
* @date: 2019/3/4 22:23
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
//super.channelRead(ctx, msg); 不注释掉会报错
/*
介绍一个读取事件的最佳实践:
ByteBuf in = (ByteBuf) msg;
try {
// Do something with msg
如果单纯用网络助手测试的话 发16进制,可以在这打印一下:
while (in.isReadable()) { // (1)
System.out.print((char) in.readByte());
System.out.flush();
}
等价于:System.out.println(in.toString(io.netty.util.CharsetUtil.US_ASCII))
} finally {
ReferenceCountUtil.release(msg);
}
*/
try {

//获得发送过来的信息,转化为bytebuffer,这个对象是netty封装的,不是原生的。
ByteBuf bufferRead = (ByteBuf)msg;
//复制一份,用来响应数据
ByteBuf bufferWrite = bufferRead.copy();
//创建一个空间,从buffer中拿出数据
byte[] bytes = new byte[bufferRead.readableBytes()];
bufferRead.readBytes(bytes);
//打印输出
String req = new String(bytes,"utf-8");
System.out.println("req = " + req);
//下面的finally来释放
//bufferRead.release();
//给个反馈,遵循echo响应协议,发啥反馈啥,
ctx.writeAndFlush(bufferWrite);
//不需要释放,因为冲刷之后,自动释放了
//bufferWrite.release();
} finally {
//如果上面的业务出现了反馈的代码,就不需要释放空间,冲刷了之后,就自动释放了
ReferenceCountUtil.release(msg);
}
}
}

关于bytebuf对象的释放问题

  1. 对于单纯读的逻辑,读完了之后要最终使用 ReferenceCountUtil.release(msg)来释放。
  2. 对于写的逻辑,用到的bytebuf对象,netty会自动处理掉,不用手动release(),相关方法:ctx.writeAndFlush(bufferWrite)
  3. 对一个msg对象又要读取,又要写出,最好是复制一份。