https://www.w3cschool.cn/essential_netty_in_action/essential_netty_in_action-x7mn28bx.html
netty server创建
package com.cn.server;
import io.netty.bootstrap.ServerBootstrap;
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;
import org.springframework.stereotype.Component;
import com.cn.common.core.codc.RequestDecoder;
import com.cn.common.core.codc.ResponseEncoder;
/**
* netty服务端入门
*
* @author -琴兽-
*
*/
@Component
public class Server {
/**
* 启动
*/
public void start() {
// 服务类
ServerBootstrap b = new ServerBootstrap();
// 创建boss和worker
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
// 设置循环线程组事例
b.group(bossGroup, workerGroup);
// 设置channel工厂
b.channel(NioServerSocketChannel.class);
// 设置管道
b.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new RequestDecoder());
ch.pipeline().addLast(new ResponseEncoder());
ch.pipeline().addLast(new ServerHandler());
ch.pipeline().addLast(new ServerHandler2());
}
});
b.option(ChannelOption.SO_BACKLOG, 2048);// 链接缓冲池队列大小
// 绑定端口
b.bind(10102).sync();
System.out.println("start!!!");
} catch (Exception e) {
e.printStackTrace();
}
}
}
Server 的启动方法涉及到了一些组件的调用,例如 EventLoopGroup,Channel。这些会在后面详细讲解。
这里有个大致的印象就好:
- 创建 EventLoopGroup。
- 创建 ServerBootstrap。
- 指定所使用的 NIO 传输 Channel。
- 使用指定的端口设置套接字地址。
- 添加一个 ServerHandler 到 Channel 的 ChannelPipeline。
- 异步地绑定服务器;调用 sync 方法阻塞等待直到绑定完成。
- 获取 Channel 的 CloseFuture,并且阻塞当前线程直到它完成。
- 关闭 EventLoopGroup,释放所有的资源。
NettyServer 启动以后会监听某个端口的请求,当接受到了请求就需要处理了。在 Netty 中客户端请求服务端,被称为“入站”操作。
客户端代码
客户端和服务端的代码基本相似,在初始化时需要输入服务端的 IP 和 Port。
package com.cn.client;
import io.netty.bootstrap.Bootstrap;
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.net.InetSocketAddress;
import javax.annotation.PostConstruct;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import com.cn.client.swing.Swingclient;
import com.cn.common.core.codc.RequestEncoder;
import com.cn.common.core.codc.ResponseDecoder;
import com.cn.common.core.model.Request;
/**
* netty客户端入门
*
* @author -琴兽-
*
*/
@Component
public class Client {
/**
* 界面
*/
@Autowired
private Swingclient swingclient;
/**
* 服务类
*/
Bootstrap bootstrap = new Bootstrap();
/**
* 会话
*/
private Channel channel;
/**
* 线程池
*/
private EventLoopGroup workerGroup = new NioEventLoopGroup();
/**
* 初始化
*/
@PostConstruct
public void init() {
// 设置循环线程组事例
bootstrap.group(workerGroup);
// 设置channel工厂
bootstrap.channel(NioSocketChannel.class);
// 设置管道
bootstrap.handler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new ResponseDecoder());
ch.pipeline().addLast(new RequestEncoder());
ch.pipeline().addLast(new ClientHandler(swingclient));
}
});
}
/**
* 连接
*
* @param ip
* @param port
* @throws InterruptedException
*/
public void connect() throws InterruptedException {
// 连接服务端
ChannelFuture connect = bootstrap.connect(new InetSocketAddress("127.0.0.1", 10102));
connect.sync();
channel = connect.channel();
}
/**
* 关闭
*/
public void shutdown() {
workerGroup.shutdownGracefully();
}
/**
* 获取会话
*
* @return
*/
public Channel getChannel() {
return channel;
}
/**
* 发送消息
* @param request
* @throws InterruptedException
*/
public void sendRequest(Request request) throws InterruptedException{
if(channel == null || !channel.isActive()){
connect();
}
channel.writeAndFlush(request);
}
}
客户端启动程序的顺序:
- 创建 Bootstrap。
- 指定 EventLoopGroup 用来监听事件。
- 定义 Channel 的传输模式为 NIO(Non-BlockingInputOutput)。
- 设置服务器的 InetSocketAddress。
- 在创建 Channel 时,向 ChannelPipeline 中添加一个 EchoClientHandler 实例。
- 连接到远程节点,阻塞等待直到连接完成。
- 阻塞,直到 Channel 关闭。
- 关闭线程池并且释放所有的资源。
客户端在完成以上操作以后,会与服务端建立连接从而传输数据。同样在接受到 Channel 中触发的事件时,客户端会触发对应事件的操作。
Netty 核心组件
通过上面的简单例子,发现有些 Netty 组件在服务初始化以及通讯时被用到,下面就来介绍一下这些组件的用途和关系。
①Channel
通过上面例子可以看出,当客户端和服务端连接的时候会建立一个 Channel。
这个 Channel 我们可以理解为 Socket 连接,它负责基本的 IO 操作,例如:bind,connect,read,write 等等。
简单的说,Channel 就是代表连接,实体之间的连接,程序之间的连接,文件之间的连接,设备之间的连接。同时它也是数据入站和出站的载体。
②EventLoop 和 EventLoopGroup
既然有了 Channel 连接服务,让信息之间可以流动。如果服务发出的消息称作“出站”消息,服务接受的消息称作“入站”消息。那么消息的“出站”/“入站”就会产生事件(Event)。
例如:连接已激活;数据读取;用户事件;异常事件;打开链接;关闭链接等等。
顺着这个思路往下想,有了数据,数据的流动产生事件,那么就有一个机制去监控和协调事件。
这个机制(组件)就是 EventLoop。在 Netty 中每个 Channel 都会被分配到一个 EventLoop。一个 EventLoop 可以服务于多个 Channel。
每个 EventLoop 会占用一个 Thread,同时这个 Thread 会处理 EventLoop 上面发生的所有 IO 操作和事件(Netty 4.0)。
EventLoop 与 Channel 关系
理解了 EventLoop,再来说 EventLoopGroup 就容易了,EventLoopGroup 是用来生成 EventLoop 的,还记得例子代码中第一行就 new 了 EventLoopGroup 对象。
一个 EventLoopGroup 中包含了多个 EventLoop 对象。
创建 EventLoopGroup
EventLoopGroup 要做的就是创建一个新的 Channel,并且给它分配一个 EventLoop。
EventLoopGroup,EventLoop 和 Channel 的关系
在异步传输的情况下,一个 EventLoop 是可以处理多个 Channel 中产生的事件的,它主要的工作就是事件的发现以及通知。
相对于以前一个 Channel 就占用一个 Thread 的情况。Netty 的方式就要合理多了。
客户端发送消息到服务端,EventLoop 发现以后会告诉服务端:“你去获取消息”,同时客户端进行其他的工作。
当 EventLoop 检测到服务端返回的消息,也会通知客户端:“消息返回了,你去取吧“。客户端再去获取消息。整个过程 EventLoop 就是监视器+传声筒。
③ChannelHandler,ChannelPipeline 和 ChannelHandlerContext
如果说 EventLoop 是事件的通知者,那么 ChannelHandler 就是事件的处理者。
在 ChannelHandler 中可以添加一些业务代码,例如数据转换,逻辑运算等等。
正如上面例子中展示的,Server 和 Client 分别都有一个 ChannelHandler 来处理,读取信息,网络可用,网络异常之类的信息。
并且,针对出站和入站的事件,有不同的 ChannelHandler,分别是:
- ChannelInBoundHandler(入站事件处理器)
- ChannelOutBoundHandler(出站事件处理器)
假设每次请求都会触发事件,而由 ChannelHandler 来处理这些事件,这个事件的处理顺序是由 ChannelPipeline 来决定的。
ChannelHanlder 处理,出站/入站的事件
ChannelPipeline 为 ChannelHandler 链提供了容器。到 Channel 被创建的时候,会被 Netty 框架自动分配到 ChannelPipeline 上。
ChannelPipeline 保证 ChannelHandler 按照一定顺序处理事件,当事件触发以后,会将数据通过 ChannelPipeline 按照一定的顺序通过 ChannelHandler。
说白了,ChannelPipeline 是负责“排队”的。这里的“排队”是处理事件的顺序。
同时,ChannelPipeline 也可以添加或者删除 ChannelHandler,管理整个队列。
如上图,ChannelPipeline 使 ChannelHandler 按照先后顺序排列,信息按照箭头所示方向流动并且被 ChannelHandler 处理。
说完了 ChannelPipeline 和 ChannelHandler,前者管理后者的排列顺序。那么它们之间的关联就由 ChannelHandlerContext 来表示了。
每当有 ChannelHandler 添加到 ChannelPipeline 时,同时会创建 ChannelHandlerContext 。
ChannelHandlerContext 的主要功能是管理 ChannelHandler 和 ChannelPipeline 的交互。
ChannelHandlerContext 参数贯穿 ChannelPipeline,将信息传递给每个 ChannelHandler,是个合格的“通讯员”。
ChannelHandlerContext 负责传递消息
把上面提到的几个核心组件归纳一下,用下图表示方便记忆他们之间的关系。
Netty 核心组件关系图
Netty 的数据容器
前面介绍了 Netty 的几个核心组件,服务器在数据传输的时候,产生事件,并且对事件进行监控和处理。
接下来看看数据是如何存放以及是如何读写的。Netty 将 ByteBuf 作为数据容器,来存放数据。
ByteBuf 工作原理
从结构上来说,ByteBuf 由一串字节数组构成。数组中每个字节用来存放信息。
ByteBuf 提供了两个索引,一个用于读取数据,一个用于写入数据。这两个索引通过在字节数组中移动,来定位需要读或者写信息的位置。
当从 ByteBuf 读取时,它的 readerIndex(读索引)将会根据读取的字节数递增。
同样,当写 ByteBuf 时,它的 writerIndex 也会根据写入的字节数进行递增。
ByteBuf 读写索引图例
需要注意的是极限的情况是 readerIndex 刚好读到了 writerIndex 写入的地方。
如果 readerIndex 超过了 writerIndex 的时候,Netty 会抛出 IndexOutOf-BoundsException 异常。