Netty 1-1 入门实例
NettyServer AND ServerChannelHandler
创建Netty服务端
创建服务端的都是模板代码,
1.设置group,需要设置两个EventLoopGroup。bossGroup用于监听客户端Channel连接的线程组,Selector作用。workGroup用于处理网络IO,可以自定义线程数。
2.设置服务端的ServerSocketChannel类型,这里使用了NioServerSocketChannel。需要注意的是,客户端设置要对应为NioSocketChannel
3.设置option参数,主要是TCP协议的一些参数
4.设置childHandler。指的是 NioServerSocketChannel产生的子Channel,对设置初始化的Handler处理器。
5.绑定端口,默认是异步的。调用sync方法进行阻塞
6.调用closeFuture,等待关闭(服务端永远不会关闭)
7.回收EventLoopGroup资源
接受请求的能力增加,处理能力不一定增加。
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;
/**
* 对于ChannelOption.SO_BACKLOG的解释:tcp连接缓存区
* 服务器端TCP内核模块维护有两个队列。我们称之为A和B。
* 客户端想服务端connect的时候,会发送带有SYN标志的包(第一次握手)
* 服务器收到客户端的SYN时,向客户端发送SYN ACK确认(第二次握手)。TCP内核将完成两次握手的连接加入到队列A,等待客户端发来ACK。
* 收到客户端ACK(第三次握手)。TCP内核模块将连接从A队列转移到B队列,连接完成,应用程序的accept就会返回。
* 也就是说accept方法从队列B中取出完成三次握手的连接。
* A队列和B队列的长度之和就是backlog。当队列长度之和大于backlog时,新连接会被TCP内核拒绝。
* 所以backlog的值过小,会出现accept的速度跟不上新连接加入的速度,AB队列满了,新的客户端无法连接
* 注意:backlog对程序支持的线程数并无影响,只会影响没有被accept取出的连接数
*
*/
public class NettyServer {
public static void main(String[] args) {
try {
EventLoopGroup bossGroup = new NioEventLoopGroup(); //用于监听客户端Channel连接的线程组,Selector作用,默认一个线程
EventLoopGroup workGroup = new NioEventLoopGroup(5); //进行网络IO读写的线程组,可自定义线程数
ServerBootstrap b = new ServerBootstrap(); //服务端引导类,整合Selector线程、IO工作线程、Channel、ChaneelPipeline
b.group(bossGroup,workGroup) //绑定线程组
.channel(NioServerSocketChannel.class) //确定服务端的ServerSocketChannel
.option(ChannelOption.SO_BACKLOG,2) //设置tcp缓冲区
.option(ChannelOption.SO_SNDBUF,8*1024) //设置发送缓冲区大小
.option(ChannelOption.SO_RCVBUF,8*1024) //设置接收缓冲区大小
.option(ChannelOption.SO_KEEPALIVE,true) //保持连接,默认为true
.childHandler(new ChannelInitializer<SocketChannel>() { //定义Channel创建 Handler处理器
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new StringToByteEncode());
ch.pipeline().addLast(new ServerHelloInboundHandler()); //添加入站handler
}
});
ChannelFuture cf = b.bind(12345).sync(); //异步的绑定端口,调用sync方法阻塞等待绑定完成。
ChannelFuture cf2 = b.bind(12346).sync(); //可以开放多个端口
System.out.println("Netty Server start Success");
cf.channel().closeFuture().sync(); //异步等待channel关闭,调用sync方法阻塞。也就是服务端关闭。
cf2.channel().closeFuture().sync();
bossGroup.shutdownGracefully(); //释放线程组资源
workGroup.shutdownGracefully();
} catch (Exception e){
e.printStackTrace();
}
}
}
创建服务端Handler处理器。
1.继承ChannelInboundHandlerAdapter 父类,作为Handler处理的入口。重写channelRead方法。
2.接受Netty默认的ByteBuf。
因为作为服务端处理的第一个Handler,之前没有解码器Handler。所以传入的Object msg就是ByteBuf。
因为代码是基于TCP协议的,TCP使用字节流进行传输数据,所以客户端发出的数据和服务端接受的数据 必须是ByteBuf。 如果想对Channel写入对象,就需要添加对应的编码器,将Java对象转为字节数组。(序列化)
3.对获取到的ByteBuf进行处理。
这里因为客户端传来String(转为ByteBuf后的String)。所以就直接打印了。
这是因为Handler处理器没有区分长连接中的不同次的请求,两次长连接flush数据,可能会同时冲刷到同一个ByteBuf中,也就是服务端只会处理一次。需要专门的解码器来实现 同一个长连接,不同请求的ByteBuf数据切割。
专业说法:因为TCP是基于流的传输,基于流的传输并不是一个数据包队列,而是一个字节队列。即使你发送了2个独立的数据包,操作系统也不会作为2个消息处理而仅仅是作为一连串的字节而言。因此这是不能保证你远程写入的数据就会准确地读取。这是后就需要涉及TCP 粘包拆包。
4.Channel写入数据,冲刷数据到客户端。。
ChannelHandlerContext,向CHannel写入数据。WriteAndFlush方法冲刷数据到客户端Channel。这里需要注意,需要冲刷的数据必须是ByteBuf,否则无法传输。 如果需要传输String,则需要添加自定义的 StringToByteEncode 编码器,将String在发送之前编码写入到ByteBuf。
5.添加ChannelFutureListner,关闭Channel。实现长连接和短连接。
Netty的读写操作都是异步的,调用WriteAndFlush会返回一个ChannelFuture,可以添加监听器,实现关闭Channel,这边关闭的Channel是客户端和服务端通信的CHannel。添加了监听器后,客户端的cf.channel().closeFuture().sync(); 同步阻塞结束,客户端关闭。(服务端关闭Channel)
客户端想实现Channel,就必须在Handler中添加ChannelFutureListener.CLOSE。(客户端确定本次请求完毕,主动关闭连接)。
可以通过是否添加ChannelFutureListener.CLOSE,来实现长连接和短连接。
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.util.ReferenceCountUtil;
public class ServerHelloInboundHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
try {
//Handler接受到的Object 如果没有进行解码的话,默认是ByteBuf。
if (msg instanceof ByteBuf){
ByteBuf byteBuf = (ByteBuf) msg;
byte[] buf = new byte[byteBuf.readableBytes()];
byteBuf.readBytes(buf);
String body = new String (buf,"utf-8");
System.out.println("get request Message:" +body);
String response = new String("hello "+body);
ctx.writeAndFlush(Unpooled.copiedBuffer(response.getBytes()));
ctx.writeAndFlush("return a string");
//添加监听器,关闭Channel。客户端的cf.channel().closeFuture().sync()就会往下执行,关闭客户端连接。
//通过添加ChannelFutureListener.CLOSE 实现长连接和短连接
//关闭Channel 可以在服务端可以在客户端。 //ctx.writeAndFlush(Unpooled.copiedBuffer("".getBytes())).addListener(ChannelFutureListener.CLOSE);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
ReferenceCountUtil.release(msg);
super.channelRead(ctx, msg);
}
}
}
NettyClient AND ClientChannelHandler
创建NettyClient
1.创建过程 和Server端类似
2.添加客户端超时时间 ChannelOption.CONNECT_TIMEOUT_MILLIS
3.启动客户端连接。分为两种同步、异步两种方式。使用异步方式,需要使用CountDownLatch。
4 .通过write和flush方法冲刷数据,这边因为连续冲刷了mdq 和 mdq second。可能导致服务端接受到一次ByteBuf。(TCP粘包拆包问题)
这边一个骚操作就是:把第一次(channelActive方法中)和第二次之间 sleep了3s。 这样从时间上实现了拆包。本质是因为,Netty服务已经处理完数据,将ByteBuf 字节流重置刷新为空了。
注意:这不是正常操作。这不是正常操作。这不是正常操作。
5.closeFuture().sync()阻塞主线程。通过ClientChannelHandler完成 Channel的读写操作。
import io.netty.bootstrap.Bootstrap;
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 java.util.concurrent.CountDownLatch;
public class NettyClient {
public static void main(String[] args) {
EventLoopGroup group = new NioEventLoopGroup();
Bootstrap b = new Bootstrap();
try {
b.group(group)
.channel(NioSocketChannel.class)
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS,3000) //设置连接超时时间
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new StringToByteEncode());
ch.pipeline().addLast(new ClientHelloInboundHandler());
}
});
final CountDownLatch connectedLatch = new CountDownLatch(1);
//因为使用了的异步连接方式,需要CountDownLatch进行阻塞,确保连接完成。如果使用了sync同步,则不需要使用CountDownLatch
//如果设置了sync同步方法,出现连接错误时,需要在finally代码块中释放EventLoopGroup的资源。不然无法释放线程。
ChannelFuture cf = b.connect("127.0.0.1",12345).sync();
//监听Channel是否建立成功
cf.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
if (future.isSuccess()) {
//若Channel建立成功,保存建立成功的标记
System.out.println("Netty client connection Success");
} else {
//若Channel建立失败,保存建立失败的标记
System.out.println("Netty client connection Failed");
future.cause().printStackTrace();
}
connectedLatch.countDown();
}
});
connectedLatch.await();
Channel channel = cf.channel();
Thread.sleep(3000);
channel.write("mdq");
channel.flush();
channel.writeAndFlush(Unpooled.copiedBuffer("mdq second".getBytes()));
cf.channel().closeFuture().sync();
} catch (Exception e){
e.printStackTrace();
} finally {
group.shutdownGracefully();
}
}
}
ClientChannelHandler
1.接受到服务的数据,默认也是ByteBuf
2.对ByteBuf 进行处理,客户端可会出现和服务端出现一样的情况。 服务端多次冲刷的数据,在客户端只接收到一个ByteBuf。
3.可以通过ctx.writeAndFlush 继续想服务端写入数据。
4.可以添加监听器,客户端主动关闭连接
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.util.ReferenceCountUtil;
public class ClientHelloInboundHandler extends ChannelInboundHandlerAdapter {
//连接创建的时候执行
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ctx.writeAndFlush(Unpooled.copiedBuffer("channelActive".getBytes()));
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
try {
if (msg instanceof ByteBuf){
ByteBuf byteBuf = (ByteBuf) msg;
byte[] buf = new byte[byteBuf.readableBytes()];
byteBuf.readBytes(buf);
String response = new String (buf,"utf-8");
System.out.println("get response Message:" +response);
//客户端确认请求结束,主动关闭连接。
//ctx.writeAndFlush(Unpooled.copiedBuffer("".getBytes())).addListener(ChannelFutureListener.CLOSE);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
ReferenceCountUtil.release(msg);
super.channelRead(ctx, msg);
}
}
}
TCP
在服务端和客户端保持长连接的过程中,客户端可以不断向服务端冲刷数据。,服务端可以不断的接收数据并向客户端返回数据。
Netty中TCP传输数据的对象必须是ByteBuf,如果需要传输其他对象需要添加编码器和解码器。也就是说客户端最后发出的必须是ByteBuf,而服务端首先接受的到也必定是ByteBuf。
在正常应用中:需要客户端和服务端中设置编码器和解码器(MessageToByteEncoder ByteToMessageDecoder)。 编码器的实现比较简单,只需要将Java对象转化为byte数组(序列化、String.getBytes),并将byte数组到ByteBuf out中即可。 解码器的功能就比较重要了,需要判断当前的ByteBuf中的数据是否能反序列化成一个完整的对象。如果ByteBuf中数据不够怎么处理、如果ByteBuf中包含了下一个长连接请求的数据该如何处理(这个还是粘包和解包的问题)。
创建编码器 MessageToByteEncoder
因为 Netty TCP需要传输 ByteBuf进行传输数据,这边添加简单的自定义编码器,将String对象 编码为ByteBuf,然后进行传输。
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToByteEncoder;
public class StringToByteEncode extends MessageToByteEncoder {
@Override
protected void encode(ChannelHandlerContext ctx, Object msg, ByteBuf out) throws Exception {
if (msg instanceof String){
String s = (String) msg;
out.writeBytes(s.getBytes());
} else if (msg instanceof ByteBuf){
ByteBuf buf = (ByteBuf) msg;
byte[] b = new byte[buf.readableBytes()];
buf.readBytes(b) ;
out.writeBytes(b);
}
}
}
参考资料 http://ifeve.com/netty5-user-guide/