一、netty流程(抽象)
- 定义核心处理类
- 定义Initializer
- 定义服务启动类
二、netty server流程
- 构造netty服务端处理器 -> NettyServerHandler
@Slf4j
public class NettyServerHandler extends ChannelInboundHandlerAdapter {
/**
*@Author wuxubiao
*@Description 客户端连接触发
*@Date 2021/7/23 11:17
*@Param [ctx]
*@return void
**/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
log.info("channel connect active...");
}
/**
*@Author wuxubiao
*@Description 客户端发消息触发
*@Date 2021/7/23 11:17
*@Param [ctx, msg]
*@return void
**/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
log.info("server receive message is :{}", msg.toString());
//回应消息
ctx.write("hello client!");
ctx.flush();
}
/**
*@Author wuxubiao
*@Description 发生异常触发
* 当出现 Throwable 对象才会被调用,即当 Netty 由于 IO 错误或者处理器在处理事件时抛出的异常时
* 在大部分情况下,捕获的异常应该被记录下来并且把关联的 channel 给关闭掉。然而这个方法的处理方式会在遇到不同异常的情况下有不同的实现,比如你可能想在关闭连接之前发送一个错误码的响应消息。
*@Date 2021/7/23 11:17
*@Param [ctx, cause]
*@return void
**/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
NettyServerHandler继承自 ChannelInboundHandlerAdapter,这个类实现了 ChannelInboundHandler接口,ChannelInboundHandler 提供了许多事件处理的接口方法,然后你可以覆盖这些方法。
常用:
channelActive() 事件处理方法:每当新的客户端连接时该方法被调用
chanelRead() 事件处理方法:每当从客户端收到新的数据时,这个方法会在收到消息时被调用
exceptionCaught() 事件处理方法:当出现 Throwable 对象才会被调用,即当 Netty 由于 IO 错误或者处理器在处理事件时抛出的异常时。在大部分情况下,捕获的异常应该被记录下来并且把关联的 channel 给关闭掉。然而这个方法的处理方式会在遇到不同异常的情况下有不同的实现,比如你可能想在关闭连接之前发送一个错误码的响应消息。
ChannelHandlerContext:ChannelHandlerContext 对象提供了许多操作,使你能够触发各种各样的 I/O 事件和操作。这里我们调用了 write(Object) 方法来逐字地把接受到的消息写入。
ctx.write(Object) 方法不会使消息写入到通道上,他被缓冲在了内部,你需要调用 ctx.flush() 方法来把缓冲区中数据强行输出。或者你可以用更简洁的 cxt.writeAndFlush(msg) 以达到同样的目的。
此处有一个特殊说明的地方:
1.处理器的职责是释放所有传递到处理器的引用计数对象
2.msg即接收到的消息-接收到的消息的类型为ByteBuf,ByteBuf 是一个引用计数对象
3.则这个对象必须显示地调用 release() 方法来释放
4.我们调用write的时候并没有释放接受到的消息,这是因为当写入的时候 Netty 已经帮我们释放了
5.write()方法内部也会调用release(),参考源码:AbstractChannelHandlerContext类下
- 构造netty服务初始化器 -> ServerChannelInitializer
/**
* @Description: netty服务初始化器
* @Author wuxubiao
* @Date 2021/7/23 11:30
* @Version V-1.0
*/
public class ServerChannelInitializer extends ChannelInitializer {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
//添加编解码
socketChannel.pipeline().addLast("decoder", new StringDecoder(CharsetUtil.UTF_8));
socketChannel.pipeline().addLast("encoder", new StringEncoder(CharsetUtil.UTF_8));
//添加 netty 服务端处理器逻辑
socketChannel.pipeline().addLast(new NettyServerHandler());
}
}
ChannelInitializer 是一个特殊的处理类,他的目的是帮助使用者配置一个新的 Channel
也许你想通过增加一些处理类比如NettyServerHandler 来配置一个新的 Channel 或者其对应的ChannelPipeline 来实现你的网络程序。当你的程序变的复杂时,可能你会增加更多的处理类到 pipline 上,然后提取这些匿名类到最顶层的类上。
- 构造netty服务启动监听器 -> NettyServer
/**
* @Description: 服务启动监听器
* @Author wuxubiao
* @Date 2021/7/23 11:35
* @Version V-1.0
*/
@Slf4j
public class NettyServer {
/**
*@Author wuxubiao
*@Description 启动
*@Date 2021/7/23 11:37
*@Param [socketAddress-socket地址包装类]
*@return void
**/
public void start(InetSocketAddress socketAddress){
// boss线程组(初始化一个线程)
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
// 工作线程组(初始化200线程)
EventLoopGroup workGroup = new NioEventLoopGroup(200);
//构造引导
ServerBootstrap bootstrap = new ServerBootstrap()
.group(bossGroup,workGroup)
//根据场景不同选择合适得通信channel,继承自 Channel
.channel(NioServerSocketChannel.class)
.childHandler(new ServerChannelInitializer())
.localAddress(socketAddress)
// option() 是提供给NioServerSocketChannel 用来接收进来的连接
//设置队列大小
// ChannelOption.SO_BACKLOG对应的是tcp/ip协议listen函数中的backlog参数,函数listen(int socketfd,int backlog)用来初始化服务端可连接队列,服务端处理客户端连接请求是顺序处理的,
//所以同一时间只能处理一个客户端连接,多个客户端来的时候,服务端将不能处理的客户端连接请求放在队列中等待处理,backlog参数指定了队列的大小
.option(ChannelOption.SO_BACKLOG,1024)
// childOption() 是提供给由父管道 ServerChannel 接收到的连接,在这个例子中也是 NioServerSocketChannel
// 两小时内没有数据的通信时,TCP会自动发送一个活动探测数据报文
.childOption(ChannelOption.SO_KEEPALIVE, true);
try {
// 绑定端口,开始接收进来的连接
ChannelFuture future = bootstrap.bind(socketAddress).sync();
log.info("server start begin to listen in port: {}",socketAddress.getPort());
// 同步等待服务器 socket 关闭
// 在这个例子中,这不会发生,但你可以优雅地关闭你的服务器。
future.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
// 关闭主线程组
bossGroup.shutdownGracefully();
// 关闭工作线程组
workGroup.shutdownGracefully();
}
}
}
NioEventLoopGroup:用来处理I/O操作的多线程事件循环器,Netty 提供了许多不同的 EventLoopGroup 的实现用来处理不同的传输。在这个例子中我们实现了一个服务端的应用,因此会有2个 NioEventLoopGroup 会被使用。第一个经常被叫做‘boss’,用来接收进来的连接。第二个经常被叫做‘worker’,用来处理已经被接收的连接,一旦‘boss’接收到连接,就会把连接信息注册到‘worker’上。如何知道多少个线程已经被使用,如何映射到已经创建的 Channel上都需要依赖于 EventLoopGroup 的实现,并且可以通过构造函数来配置他们的关系。
ServerBootstrap:是一个启动 NIO 服务的辅助启动类。你可以在这个服务中直接使用 Channel,但是这会是一个复杂的处理过程,在很多情况下你并不需要这样做。
option 和 childOption配置:你可以设置这里指定的 Channel 实现的配置参数。我们正在写一个TCP/IP 的服务端,因此我们被允许设置 socket 的参数选项比如tcpNoDelay 和 keepAlive。请参考 ChannelOption 和详细的 ChannelConfig 实现的接口文档以此可以对ChannelOption 的有一个大概的认识
option() 是提供给NioServerSocketChannel 用来接收进来的连接
childOption() 是提供给由父管道 ServerChannel 接收到的连接,在这个例子中也是 NioServerSocketChannel
三、netty client流程
- 构造netty客户端处理器 -> NettyClientHandler
- 构造netty客户初始化器 -> NettyClientInitializer
- 构造netty客户启动监听器 -> NettyClient
在 Netty 中,编写服务端和客户端最大的并且唯一不同的使用了不同的BootStrap 和 Channel的实现
/**
* @Description: 客户端处理器
* @Author wuxubiao
* @Date 2021/7/23 14:01
* @Version V-1.0
*/
@Slf4j
public class NettyClientHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
log.info("客户端Active .....");
super.channelActive(ctx);
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
log.info("客户端收到消息: {}", msg.toString());
// 在TCP/IP中,Netty 会把读到的数据放到 ByteBuf 的数据结构中
ByteBuf m = (ByteBuf) msg;
try {
long currentTimeMillis = (m.readUnsignedInt() - 2208988800L) * 1000L;
System.out.println(new Date(currentTimeMillis));
ctx.close();
} finally {
m.release();
}
super.channelRead(ctx, msg);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
super.exceptionCaught(ctx, cause);
}
}
在TCP/IP中,Netty 会把读到的数据放到 ByteBuf 的数据结构中
@Slf4j
public class NettyClient {
public void start(){
// 如果你只指定了一个 EventLoopGroup,那他就会即作为一个 boss group ,也会作为一个 workder group,尽管客户端不需要使用到 boss worker
EventLoopGroup group = new NioEventLoopGroup();
// BootStrap 和 ServerBootstrap 类似,不过他是对非服务端的 channel 而言,比如客户端或者无连接传输模式的 channel
Bootstrap bootstrap = new Bootstrap()
.group(group)
// 代替NioServerSocketChannel的是NioSocketChannel,这个类在客户端channel 被创建时使用
.channel(NioSocketChannel.class)
// 该参数的作用就是禁止使用Nagle算法,使用于小数据即时传输(不进行数据包组装为更大的帧然后进行发送)
// 不像在使用 ServerBootstrap 时需要用 childOption() 方法,因为客户端的 SocketChannel 没有父亲
.option(ChannelOption.TCP_NODELAY, true)
.handler(new NettyClientInitializer());
try {
ChannelFuture future = bootstrap.connect("127.0.0.1", 8090).sync();
log.info("client connect successful...");
//发送消息
future.channel().writeAndFlush("send message!");
// 同步等待连接被关闭
// 主线程执行到这里就 wait 子线程结束,子线程才是真正监听和接受请求的,closeFuture()是开启了一个channel的监听器,负责监听channel是否关闭的状态,如果监听到channel关闭了,子线程才会释放,syncUninterruptibly()让主线程同步等待子线程结果
future.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
group.shutdownGracefully();
}
}
}
区别于服务端程序
1.BootStrap 和 ServerBootstrap 类似,不过他是对非服务端的 channel 而言,比如客户端或者无连接传输模式的 channel
2.如果你只指定了一个 EventLoopGroup,那他就会即作为一个 boss group ,也会作为一个 workder group,尽管客户端不需要使用到 boss worker
3.代替NioServerSocketChannel的是NioSocketChannel,这个类在客户端channel 被创建时使用
4.不像在使用 ServerBootstrap 时需要用 childOption() 方法,因为客户端的 SocketChannel 没有父亲
5.我们用 connect() 方法代替了 bind() 方法
四、ChannelOption参数详解
1、ChannelOption.SO_BACKLOG
ChannelOption.SO_BACKLOG对应的是tcp/ip协议listen函数中的backlog参数,函数listen(int socketfd,int backlog)用来初始化服务端可连接队列,服务端处理客户端连接请求是顺序处理的,所以同一时间只能处理一个客户端连接,多个客户端来的时候,服务端将不能处理的客户端连接请求放在队列中等待处理,backlog参数指定了队列的大小
2、ChannelOption.SO_REUSEADDR
ChanneOption.SO_REUSEADDR对应于套接字选项中的SO_REUSEADDR,这个参数表示允许重复使用本地地址和端口,比如,某个服务器进程占用了TCP的80端口进行监听,此时再次监听该端口就会返回错误,使用该参数就可以解决问题,该参数允许共用该端口,这个在服务器程序中比较常使用,比如某个进程非正常退出,该程序占用的端口可能要被占用一段时间才能允许其他进程使用,而且程序死掉以后,内核一需要一定的时间才能够释放此端口,不设置SO_REUSEADDR就无法正常使用该端口。
3、ChannelOption.SO_KEEPALIVE
Channeloption.SO_KEEPALIVE参数对应于套接字选项中的SO_KEEPALIVE,该参数用于设置TCP连接,当设置该选项以后,连接会测试链接的状态,这个选项用于可能长时间没有数据交流的连接。当设置该选项以后,如果在两小时内没有数据的通信时,TCP会自动发送一个活动探测数据报文
4、ChannelOption.SO_SNDBUF和ChannelOption.SO_RCVBUF
ChannelOption.SO_SNDBUF参数对应于套接字选项中的SO_SNDBUF,ChannelOption.SO_RCVBUF参数对应于套接字选项中的SO_RCVBUF这两个参数用于操作接收缓冲区和发送缓冲区的大小,接收缓冲区用于保存网络协议站内收到的数据,直到应用程序读取成功,发送缓冲区用于保存发送数据,直到发送成功。
5、ChannelOption.SO_LINGER
ChannelOption.SO_LINGER参数对应于套接字选项中的SO_LINGER,Linux内核默认的处理方式是当用户调用close()方法的时候,函数返回,在可能的情况下,尽量发送数据,不一定保证会发生剩余的数据,造成了数据的不确定性,使用SO_LINGER可以阻塞close()的调用时间,直到数据完全发送
6、ChannelOption.TCP_NODELAY
ChannelOption.TCP_NODELAY参数对应于套接字选项中的TCP_NODELAY,该参数的使用与Nagle算法有关,Nagle算法是将小的数据包组装为更大的帧然后进行发送,而不是输入一次发送一次,因此在数据包不足的时候会等待其他数据的到了,组装成大的数据包进行发送,虽然该方式有效提高网络的有效负载,但是却造成了延时,而该参数的作用就是禁止使用Nagle算法,使用于小数据即时传输,于TCP_NODELAY相对应的是TCP_CORK,该选项是需要等到发送的数据量最大的时候,一次性发送数据,适用于文件传输。
7、IP_TOS
IP参数,设置IP头部的Type-of-Service字段,用于描述IP包的优先级和QoS选项。
8、ALLOW_HALF_CLOSURE
Netty参数,一个连接的远端关闭时本地端是否关闭,默认值为False。值为False时,连接自动关闭;为True时,触发ChannelInboundHandler的userEventTriggered()方法,事件为ChannelInputShutdownEvent。
谢谢大家关注,点个赞呗~
如需转载请标明出处,谢谢~~