Netty实现服务器和客户端
服务器
话不多说我们直接来实现一个简单的TimerServer(该服务器的提供的服务是接收客户端的指令返回服务器的系统时间)。
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
/**
* @ Author :liuyanzhi
* @ Date :Created in 16:45 2020-12-17
* @ Description:netty Server
* @ Modified By:
* @Version: 1.0$
*/
public class TimerServer {
public void bind(int port){
//配置服务端的nio线程组
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workGroup = new NioEventLoopGroup();
try{
//初始化netty启动NIO服务器的辅助启动类
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 1024)
.childHandler(new ChildChannelHandler());
//绑定端口,同步等待成功
ChannelFuture future = b.bind(port).sync();
System.out.println("TimerServer服务器已经启动...");
//等待服务器监听端口关闭
future.channel().closeFuture().sync();
}catch (Exception e){
}finally {
bossGroup.shutdownGracefully();
workGroup.shutdownGracefully();
}
}
}
流程描述
我们从bind
函数开始:
1、首先创建了两个NioEventLoopGroup
,这是初始化了两个线程组,这两个线程组根据命名可以知道一个是bossGroup
,是服务器用来接收客户端的请求的,另一个是workGroup
是用来处理NioServerSocketChannel
的网络读写的。这两个线程组实际承担的是一个Reactor
的角色。
2、第二步就是初始化一个ServerBootstrap
,这是Netty用于启动NIO服务端的辅助启动类,目的是降低服务器开发的复杂度。ServerBootstrap
的初始化首先是调用group方法,将两个线程组作为参数传到ServerBootstrap
中。接着创建channel
为NioServerSocketChannel
。然后调用option
配置channel
的TCP参数,其中SO_BACKLOG
设置为1024的意思是服务器监听队列的大小是1024,最后设置处理IO事件的处理器ChildChannelHandler
,它的作用类似于Rector
模式中的handler用来处理IO事件。
3、启动类初始化完成后调用ServerBootstrap
的bind
方法绑定端口,监听请求,在调用sync
等待绑定完成。完成之后返回一个ChannelFuture
作用是用于异步操作的通知回调。
4、调用 future.channel().closeFuture().sync
进行阻塞,等待服务端链路关闭main函数退出。
5、调用EventLoopGroup
的shutdownGracefully
方法,释放相关的资源。
Netty的线程模型
netty是一个高可用的基于事件驱动的异步的NIO框架,核心的线程模型是Reactor,Reactor线程模型有单线程Reactor,多线程Reactor和多线程主从复制的Reactor,这里我们就用最简单的单线程Reactor来分析下Netty的线程模型,进而来看Netty的相关核心类在此模型下扮演的角色和作用。
在网络交互中,无论你是建立连接进行通信还是数据的读写都是继续网络通信协议的,底层就是TCP/IP协议栈,协议栈是操作系统层的,我们无法修改他的协议栈,但是操作系统给了我们可以操作和使用协议栈的接口,比如我们经常使用的bind、accept、connect等,而这写操作的本身,我们都是对socket
的操作,所以我们可以任务一个socket
,就是用户跟TCP/IP协议栈进行交互的门户。
传统的IO
一请求一应答,这是BIO。每次读写都需要生成新的线程,请求多的时候占用大量的线程资源。高并发的情况下很可能造成巨大的线上故障。
NIO Reactor模型
socket注册到Selector中,由Selector决策哪个事件可以执行。
Netty的线程模型
服务器的线程模型为例:
抽象出NioEventLoop来表示一个不断循环执行处理任务的线程,每个NioEventLoop有一个selector,用于监听绑定在其上的socket链路。服务端维护了两个EventLoopGroup,一个bossGroup,一个workGroup,bossGroup用来处理客户端的连接请求,然后打开Channel,把这个Channel交给workGroup中的一个EventLoop被注册来处理该Channel上的所有请求。一个Channel只会被一个workGroup处理,一个workGroup可以同时被多个Channel使用。
简单了解了Netty的一个线程模型,我们来看看netty编程中那些核心类的作用。
Netty架构
核心类分析
EventLoop
EventLoop是Netty中极其重要的组件,翻译为时间循环,一个EventLoop被分配给Channel来负责这个Cahnnel上的所有事件。EventLoop你可以理解为一个线程,EventLoopGroup是一个线程池,EventLoopGroup继承ScheduledExecutorService,也就是调度线程池,理论上EventLoopGroup应该具有ScheduledExecutorService的所有的功能,之所以叫做事件循环,我猜想它必然有一个循环把Cahnnel上面准备就绪的事件提交给一个Thread来处理。这个实现在SingleThreadEventLoop的一个继承类EpollEventLoop中的run方法实现,源码如下:
@Override
protected void run() {
for (;;) {
try {
int strategy = selectStrategy.calculateStrategy(selectNowSupplier, hasTasks());
switch (strategy) {
case SelectStrategy.CONTINUE:
continue;
case SelectStrategy.SELECT:
strategy = epollWait(WAKEN_UP_UPDATER.getAndSet(this, 0) == 1);
// (OK - no wake-up required).
if (wakenUp == 1) {
Native.eventFdWrite(eventFd.intValue(), 1L);
}
default:
// fallthrough
}
final int ioRatio = this.ioRatio;
if (ioRatio == 100) {
try {
if (strategy > 0) {
processReady(events, strategy);
}
} finally {
// Ensure we always run tasks.
runAllTasks();
}
} else {
final long ioStartTime = System.nanoTime();
try {
if (strategy > 0) {
processReady(events, strategy);
}
} finally {
// Ensure we always run tasks.
final long ioTime = System.nanoTime() - ioStartTime;
runAllTasks(ioTime * (100 - ioRatio) / ioRatio);
}
}
if (allowGrowing && strategy == events.length()) {
//increase the size of the array as we needed the whole space for the events
events.increase();
}
} catch (Throwable t) {
handleLoopException(t);
}
// Always handle shutdown even if the loop processing threw an exception.
try {
if (isShuttingDown()) {
closeAll();
if (confirmShutdown()) {
break;
}
}
} catch (Throwable t) {
handleLoopException(t);
}
}
}
这是一个基于Epoll实现的Selector。每个Channel都绑定了一个Selector。
private static final SelectorProvider DEFAULT_SELECTOR_PROVIDER = SelectorProvider.provider();
/**
* Create a new instance
*/
public NioServerSocketChannel() {
this(newSocket(DEFAULT_SELECTOR_PROVIDER));
}
Channel
Channel翻译为通道,在Netty中,你可以认为一个Channel绑定一个socket,与socket绑定的Channel处理这个socket上的所有事件。NioServerSocketChannel中默认构造方法中创建socket:
private static final SelectorProvider DEFAULT_SELECTOR_PROVIDER = SelectorProvider.provider();
/**
* Create a new instance
*/
public NioServerSocketChannel() {
this(newSocket(DEFAULT_SELECTOR_PROVIDER));
}
1、Channel是socket交互的顶层接口,它包含了socket的所有操作集:bind
, close
,config
, connect
, isActive
, isOpen
, isWritable
, read
, write
等等。 这些包括 AbstractChannel
,AbstractNioByteChannel
,AbstractNioChannel
,EmbeddedChannel
, LocalServerChannel
,NioSocketChannel
等等。
2、Channel的生命周期:register→active→inactive→unregister。
3、Channel创建时,也会同时创建ChannelPipeline
。
源码:
protected AbstractChannel(Channel parent) {
this.parent = parent;
id = newId();
unsafe = newUnsafe();
pipeline = newChannelPipeline();
}
protected DefaultChannelPipeline(Channel channel) {
this.channel = ObjectUtil.checkNotNull(channel, "channel");
succeededFuture = new SucceededChannelFuture(channel, null);
voidPromise = new VoidChannelPromise(channel, true);
tail = new TailContext(this);
head = new HeadContext(this);
head.next = tail;
tail.prev = head;
}
pipeline是一个双向链表,ChannelHandler会按照顺序调用链表中context,一左一右。
ChannelOption
ChannelOption翻译为通道设置,顾名思义是设置Channel的属性,ChannelOption中维护了用户可选择性设置的Channel的各项属性,在初始化Channel的时候设置属性:
@Override
void init(Channel channel) throws Exception {
final Map<ChannelOption<?>, Object> options = options0();
synchronized (options) {
setChannelOptions(channel, options, logger);
}
final Map<AttributeKey<?>, Object> attrs = attrs0();
synchronized (attrs) {
for (Entry<AttributeKey<?>, Object> e: attrs.entrySet()) {
@SuppressWarnings("unchecked")
AttributeKey<Object> key = (AttributeKey<Object>) e.getKey();
channel.attr(key).set(e.getValue());
}
}
...
}
static void setChannelOptions(
Channel channel, Map<ChannelOption<?>, Object> options, InternalLogger logger) {
for (Map.Entry<ChannelOption<?>, Object> e: options.entrySet()) {
setChannelOption(channel, e.getKey(), e.getValue(), logger);
}
}
ChannelHander
ChannelHander通道处理器,对Channel上的输入输出事件进行处理,ChannelHandler 支持很多协议,并且提供用于数据处理的容器。我们已经知道 ChannelHandler 由特定事件触发。 ChannelHandler 可专用于几乎所有的动作,包括将一个对象转为字节(或相反),执行过程中抛出的异常处理,数据粘包处理等各种处理器。
因为IO事件分为输入和输出,因此ChannelHandler又具体的分为ChannelInboundHandler
和ChannelOutboundHandler
,分别用于某个阶段输入输出事件的处理。
常用的一个接口是 ChannelInboundHandler,这个类型接收到入站事件(包括接收到的数据)可以处理应用程序逻辑。当你需要提供响应时,你也可以从 ChannelInboundHandler 冲刷数据。一句话,业务逻辑经常存活于一个或者多个 ChannelInboundHandler。
ChannelPipeline
Netty中通过ChannelPipeline来保证ChannelHandler之间的处理顺序。每一个Channel对象创建的时候,都会自动创建一个关联的ChannelPipeline对象,我们可以通过io.netty.channel.Channel
对象的pipeline()
方法获取这个对象实例。
ChannelHandlerContext
前面提到可以通过ChannelPipeline的添加方法,按照顺序添加ChannelHandler,并在之后按照顺序进行调用。事实上,每个ChannelHandler会被先封装成ChannelHandlerContext
。之后再封装进ChannelPipeline中。
以DefaultChannelPipeline的addLast
方法为例,最终会定位到以下方法:
@Override
public final ChannelPipeline addLast(EventExecutorGroup group, String name, ChannelHandler handler) {
final AbstractChannelHandlerContext newCtx;
synchronized (this) {
checkMultiplicity(handler);
newCtx = newContext(group, filterName(name, handler), handler);
addLast0(newCtx);
// If the registered is false it means that the channel was not registered on an eventloop yet.
// In this case we add the context to the pipeline and add a task that will call
// ChannelHandler.handlerAdded(...) once the channel is registered.
if (!registered) {
newCtx.setAddPending();
callHandlerCallbackLater(newCtx, true);
return this;
}
EventExecutor executor = newCtx.executor();
if (!executor.inEventLoop()) {
newCtx.setAddPending();
executor.execute(new Runnable() {
@Override
public void run() {
callHandlerAdded0(newCtx);
}
});
return this;
}
}
callHandlerAdded0(newCtx);
return this;
}
private AbstractChannelHandlerContext newContext(EventExecutorGroup group, String name, ChannelHandler handler) {
return new DefaultChannelHandlerContext(this, childExecutor(group), name, handler);
}
DefaultChannelPipeline#addLast(EventExecutorGroup, String,ChannelHandler)
,都会添加到addLast
内部。内部通过两个哨兵节点HeadContext
和TailContext
作为链表的开始和结束。
客户端
客户端Netty实现:
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBuf;
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.NioSocketChannel;
/**
* @ Author :liuyanzhi
* @ Date :Created in 18:06 2020-12-17
* @ Description:time client
* @ Modified By:
* @Version: 1.0$
*/
public class TimerClient {
public void connect(int port, String host){
//设置NIO客户端线程组
EventLoopGroup clientGroup = new NioEventLoopGroup();
try{
//初始化netty启动器
Bootstrap b = new Bootstrap();
b.group(clientGroup)
.channel(NioSocketChannel.class)
//Nagle算法,适用于数据量很小的交互,false开启,适用于网络io较大,减少网络io的次数
.option(ChannelOption.TCP_NODELAY, true)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new TimeClientHandler());
}
});
ChannelFuture future = b.connect(host, port).sync();
future.channel().closeFuture().sync();
}catch (Exception e){
}finally {
//优雅退出,释放线程组
clientGroup.shutdownGracefully();
}
}
}
流程描述
我们来看connect
函数:
首先创建一个NioEventLoopGroup
,一个用来处理IO事件的线程组。然后初始化客户端的Netty启动辅助类Bootstrap
,与服务器不同的是channel
设置为NioSocketChannel
,TCP参数设置TCP_NODELAY
为true,意味着禁用Nagle算法
,则允许小包(TCP报文长度小于MSS)的交互。这里我为了省事,直接创建了匿名内部类,实现initChannel
方法,作用是将TimeClientHandler
放到pipeline
中,用于处理IO事件。
关于客户端核心类的定义和功能可以参考上文提到的服务器的相关类的介绍,思考他们的区别和联系。
小结
-
NioEventLoop实际上就是工作线程,可以直接理解为一个线程。NioEventLoopGroup是一个线程池,线程池中的线程就是NioEventLoop。
-
实际上bossGroup中有多个NioEventLoop线程,每个NioEventLoop绑定一个端口,也就是说,如果程序只需要监听1个端口的话,bossGroup里面只需要有一个NioEventLoop线程就行了。
-
每个NioEventLoop都绑定了一个Selector,所以在Netty的线程模型中,是由多个Selecotr在监听IO就绪事件。而Channel注册到Selector。
-
一个Channel绑定一个NioEventLoop,相当于一个连接绑定一个线程,这个连接所有的ChannelHandler都是在一个线程中执行的,避免了多线程干扰。更重要的是ChannelPipline链表必须严格按照顺序执行的。单线程的设计能够保证ChannelHandler的顺序执行。
-
一个NioEventLoop的selector可以被多个Channel注册,也就是说多个Channel共享一个EventLoop。EventLoop的Selecctor对这些Channel进行检查。