一、前言
本文内容是参考《netty权威指南 第二版》写的。之前我一直看不懂netty的方法调用,很懵逼。看了这本书理解多了。在这里记录一下学习过程。
二、导入依赖
注意:这个版本是比较旧的版本。不同版本方法的调用可能有较大差入,但是核心的内容都差不多。
建议你也用这个版本的netty依赖,不然有些方法可能无法调用(被废弃或者迁移到其他类中了)
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>5.0.0.Alpha2</version>
<!--注意 《netty权威指南 第二版》 用的是5.0.0.Alpha1.如果出现异常就换成1吧-->
</dependency>
三、服务端
提示1:netty所有IO都是异步的。
netty中所有的IO都是异步IO,也就是说所有的IO都是立即返回的,返回的时候,IO可能还没有结束,所以需要返回一个ChannelFuture,当IO有结果之后,会去通知ChannelFuture,这样就可以取出结果了。提示2:netty底层使用了Reactor(响应式框架,也就是非阻塞)
在响应式编程中,主线程执行的是建立通道的代码,主线程很快执行完,通道就建好了。此时只是一个空的通道,根本就没有数据。 在数据到来时,由工作线程执行每个节点的逻辑代码来处理数据,然后把数据传入下一个节点,如此反复直至结束。 所以,在写响应式代码的时候,心里一定要默念着,我所做的事情就是建立一条数据通道,在通道上指定的位置插入适合的逻辑处理代码。同时还要切记,主线程执行完时,只是建立了通道,并没有数据。
推荐阅读:https://www.cnblogs.com/lixinjie/p/step-into-reactive-programing-in-an-hour.html
package Netty01;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoop;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.LineBasedFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;
import javafx.event.EventTarget;
import java.util.Date;
/**
* @author hs
* @date 2021/11/29 17:11
* 参考:https://blog.csdn.net/m0_45406092/article/details/103995125?spm=1001.2101.3001.6661.1&utm_medium=distribute.pc_relevant_t0.none-task-blog-2%7Edefault%7ECTRLIST%7Edefault-1.no_search_link&depth_1-utm_source=distribute.pc_relevant_t0.none-task-blog-2%7Edefault%7ECTRLIST%7Edefault-1.no_search_link
* https://blog.csdn.net/m0_45406092/article/details/103985092
*
* 时间服务端(功能:客户端发送消息体内容为:"QUERY TIME ORDER"的请求时,服务端响应返回现在的时间)
*/
public class TimeServer {
public void bind(int port) throws Exception {
//配置服务端的nio线程组,它包含了一组NIO 线程,专门用于网络事件的处理,事实上他们就是Reactor线程组。
// 这里创建两个的原因是一个用于服务端接受客户端的连接,另外一个用于进行SocketChannel的网络读写。
//因为bossGroup仅接收客户端连接,不做复杂的逻辑处理,为了尽可能减少资源的占用,取值越小越好
EventLoopGroup bossGroup = new NioEventLoopGroup();
NioEventLoopGroup workerGroup = new NioEventLoopGroup(1);
try{
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup,workerGroup)//传入两个线程组
.channel(NioServerSocketChannel.class)//设置channel为NioServerSocketChannel
.option(ChannelOption.SO_BACKLOG,1024)//配置NioServerSocketChannel的TCP参数
.childHandler(new ChildChannelHandler());//最后绑定I/O事件处理类ChildChannelHandler,主要用于处理网络IO事件,例如记录日志、对消息进行解码等
//绑定端口,同步等待成功
ChannelFuture f = b.bind(port).sync();
//等待服务端监听端口关闭
f.channel().closeFuture().sync();
}catch (Exception e){
System.out.println(e.getMessage());
}finally {
System.out.println("123");
//退出,释放线程池资源
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
private class ChildChannelHandler extends ChannelInitializer<SocketChannel>{
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
//增加这两个handler 解决TCP的粘包和拆包
// 搭配下面这两个解码器(管道处理器),pipeline作为netty的数据输送管道,该管道中存放了众多的handler
socketChannel.pipeline().addLast(new LineBasedFrameDecoder(1024));
socketChannel.pipeline().addLast(new StringDecoder());
//自定义管道处理器
socketChannel.pipeline().addLast(new TimeServerHandler());
}
}
public static void main(String[] args) throws Exception {
int port = 8080;
if (args!=null && args.length>0){
try{
port = Integer.valueOf(args[0]);
}catch (Exception e){
System.out.println(e.getMessage());
}
}
new TimeServer().bind(port);
}
}
class TimeServerHandler extends ChannelHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buf = (ByteBuf)msg;
byte[] req = new byte[buf.readableBytes()];
buf.readBytes(req);
String body = new String(req, "UTF-8");
System.out.println("当前时间是:"+body);
String currentTime = "QUERY TIME ORDER".equalsIgnoreCase(body) ? new Date(System.currentTimeMillis()).toString():"BAD ORDER";
ByteBuf resp = Unpooled.copiedBuffer(currentTime.getBytes());
ctx.write(resp);
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
ctx.flush();
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
}
}
四、客户端
package Netty01;
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.LineBasedFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;
import java.util.logging.Logger;
/**
* @author hs
* @date 2021/11/30 10:11
* 客户端 从服务端获取现在的时间
*/
public class TimeClient {
public void connect(int port , String host) throws Exception {
//配置客户端NIO线程组,它包含了一组NIO 线程,专门用于网络事件的处理,事实上他们就是Reactor线程组。
NioEventLoopGroup group = new NioEventLoopGroup();
try{
//客户端辅助启动器,目的是为了降低开发复杂性
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(group)//传入一个线程组
.channel(NioSocketChannel.class)
.option(ChannelOption.TCP_NODELAY,true)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
//增加这两个handler 解决TCP的粘包和拆包
// 搭配下面这两个解码器(管道处理器),pipeline作为netty的数据输送管道,该管道中存放了众多的handler
ch.pipeline().addLast(new LineBasedFrameDecoder(1024));
ch.pipeline().addLast(new StringDecoder());
//自定义管道处理器
ch.pipeline().addLast(new TimeClientHandler());
}
});
//发起异步连接操作
ChannelFuture f = bootstrap.connect(host,port).sync();
//等待客户端链路关闭
f.channel().closeFuture().sync();
} catch (Exception e){
System.out.println(e.getMessage());
}finally {
System.out.println("clent 123");
group.shutdownGracefully();
}
}
public static void main(String[] args) {
int port = 8080;
if (args!=null&&args.length>0){
try {
port = Integer.valueOf(args[0]);
}catch (Exception e){
System.out.println(e.getMessage());
}
}
System.out.println("启动");
try {
new TimeClient().connect(port,"127.0.0.1");
} catch (Exception e) {
e.printStackTrace();
}
}
}
class TimeClientHandler extends ChannelHandlerAdapter{
private static final Logger logger = Logger.getLogger(TimeClientHandler.class.getName());
private final ByteBuf firstMessage;
public TimeClientHandler() {
byte[] req = "QUERY TIME ORDER".getBytes();
firstMessage = Unpooled.buffer(req.length);
firstMessage.writeBytes(req);
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ctx.writeAndFlush(firstMessage);
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buf = (ByteBuf)msg;
byte[] req = new byte[buf.readableBytes()];
buf.readBytes(req);
String body = new String(req, "UTF-8");
System.out.println("Now is" + body);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
logger.warning("Unexpected exception from downstream:"+cause.getMessage());
ctx.close();
}
}
五、核心代码
六、handler与childHandler
区别:
- ServerBootstrap有handler()和childHandler()这两个方法,Bootstrap只有handler()。原因ServerBootstrap有两个NIO线程组NioEventLoopGroup,一个负责接受客户端的连接,另外一个用于进行SocketChannel的网络读写 。
- handler在初始化时就会执行,而childHandler会在客户端成功connect后才执行。
七、如何解决TCP的粘包和拆包
LineBasedFrameDecoder 的工作原理是它依次遍历 ByteBuf中的可读字节,判断看是否有“n”或者“\rln”,如果有,就以此位置为结束位置,从可读索引到结束位置区间的字节就组成了一行。它是以换行符为结束标志的解码器,支持携带结束符或者不携带结束符两种解码方式,同时支持配置单行的最大长度。如果连续读取到最大长度后仍然没有发现换行符,就会抛出异常,同时忽略掉之前读到的异常码流。
StringDecoder的功能非常简单,就是将接收到的对象转换成字符串,然后继续调用后面的Handler。LineBasedFrameDecoder + StringDecoder组合就是按行切换的文本解码器,它被设计用来支持TCP的粘包和拆包
。
程序的运行结果完全符合预期,说明通过使用LineBasedFrameDecoder和 StringDecoder成功解决了TCP粘包导致的读半包问题。对于使用者来说,只要将支持半包解码的Handler添加到ChannelPipeline中即可,不需要写额外的代码,用户使用起来非常简单
。
服务端和客户端都需要添加,注意handler的顺序。
还要一些其他的解码器。可以使用时可以百度一下。
- DelimiterBasedFrameDecoder
- FixedLengthFrameDecoder