Netty 拆包粘包和服务启动流程分析

通过本章学习,笔者希望你能掌握EventLoopGroup的工作流程,ServerBootstrap的启动流程,ChannelPipeline是如何操作管理Channel。只有清楚这些,才能更好的了解和使用Netty。还在等什么,快来学习吧!

知识结构图:

技术:Netty,拆包粘包,服务启动流程

说明:若你对NIO有一定的了解,对于本章知识来说有很大的帮助!NIO教程

源码:https://github.com/ITDragonBlog/daydayup/tree/master/Netty/netty-stu

Netty 重要组件

这里让你清楚了解 ChannelPipeline,ChannelHandlerContext,ChannelHandler,Channel 四者之间的关系。

这里让你清楚了解 NioEventLoopGroup,NioEventLoop,Channel 三者之间的关系。

这里让你清楚了解 ServerBootstrap,Channel 两者之间的关系。

看懂了这块的理论知识,后面Netty拆包粘包的代码就非常的简单。

Channel

Channel : Netty最核心的接口。NIO通讯模式中通过Channel进行Socket套接字的读,写和同时读写操作。

ChannelHandler : 因为直接使用Channel会比较麻烦,所以在Netty编程中通过ChannelHandler间接操作Channel,从而简化开发。

ChannelPipeline : 可以理解为一个管理ChandlerHandler的链表。对Channel进行操作时,Pipeline负责从尾部依次调用每一个Handler进行处理。每个Channel都有一个属于自己的ChannelPipeline。

ChannelHandlerContext : ChannelPipeline通过ChannelHandlerContext间接管理每个ChannelHandler。

如下图所示,结合代码,在服务器初始化和客户端创建连接的过程中加了四个Handler,分别是日志事务,字符串分割解码器,接受参数转字符串解码器,处理任务的Handler。

NioEventLoopGroup

EventLoopGroup : 本质是个线程池,继承了ScheduledExecutorService 定时任务线程池。

NioEventLoopGroup : 是用来处理NIO通信模式的线程池。每个线程池有N个NioEventLoop来处理Channel事件,每一个NioEventLoop负责处理N个Channel。

NioEventLoop : 负责不停地轮询IO事件,处理IO事件和执行任务,类比多路复用器,细化分三件事。

1 轮询注册到Selector上所有的Channel的IO事件

2 处理产生网络IO事件的Channel

3 处理队列中的任务

ServerBootstrap

本章重点,Netty是如何通过NIO辅助启动类来初始化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());
}
}
ChannelPipeline p = channel.pipeline();
final EventLoopGroup currentChildGroup = childGroup;
final ChannelHandler currentChildHandler = childHandler;
final Entry<ChannelOption<?>, Object>[] currentChildOptions;
final Entry<AttributeKey<?>, Object>[] currentChildAttrs;
synchronized (childOptions) {
currentChildOptions = childOptions.entrySet().toArray(newOptionArray(childOptions.size()));
}
synchronized (childAttrs) {
currentChildAttrs = childAttrs.entrySet().toArray(newAttrArray(childAttrs.size()));
}
p.addLast(new ChannelInitializer<Channel>() {
@Override
public void initChannel(final Channel ch) throws Exception {
final ChannelPipeline pipeline = ch.pipeline();
ChannelHandler handler = config.handler();
if (handler != null) {
pipeline.addLast(handler);
}
ch.eventLoop().execute(new Runnable() {
@Override
public void run() {
pipeline.addLast(new ServerBootstrapAcceptor(
ch, currentChildGroup, currentChildHandler, currentChildOptions, currentChildAttrs));
}
});
}
});
}

服务器启动和连接过程:

第一步:是给Channel设置options和attrs,

第二步:复制childGroup,childHandler,childOptions和childAttrs等待服务器和客户端连接,

第三步:实例化一个ChannelInitializer,添加到Pipeline的末尾。

第四步:当Channel注册到NioEventLoop时,ChannelInitializer触发initChannel方法,pipeline装入自定义的Handler,给Channel设置一下child配置。

小结:

1 group,options,attrs,handler,是在服务器端初始化时配置,是AbstractBootstrap的方法。

2 childGroup,childOption,childAttr,childHandler,是在服务器与客户端建立Channel后配置,是ServerBootstrap的方法。

3 Bootstrap 和 ServerBootstrap 都继承了AbstractBootstrap类。

4 若不设置childGroup,则默认取group值。

5 Bootstrap 和 ServerBootstrap 启动服务时,都会执行验证方法,判断必填参数是否都有配置。

Netty 拆包粘包

这里通过介绍Netty拆包粘包问题来对Netty进行入门学习。

在基于流的传输中,即便客户端发送独立的数据包,操作系统也会将其转换成一串字节队列,而服务端一次读取到的字节数又不确定。再加上网络传输的快慢。服务端很难完整的接收到数据。

常见的拆包粘包方法有三种

1 服务端设置一次接收字节的长度。若服务端接收的字节长度不满足要求则一直处于等待。客户端为满足传输的字节长度合格,可以考虑使用空格填充。

2 服务端设置特殊分隔符。客户端通过特殊分隔符粘包,服务端通过特殊分隔符拆包。

3 自定义协议。数据传输一般分消息头和消息体,消息头中包含了数据的长度。服务端先接收到消息头,得知需要接收N个数据,然后服务端接收直到数据为N个为止。

本章采用第二种,用特殊分隔符的方式。

创建服务端代码流程

第一步:准备两个线程池。一个用于接收事件的boss线程池,另一个用于处理事件的worker线程池。

第二步:服务端实例化ServerBootstrap NIO服务辅助启动类。用于简化提高开发效率。

第三步:配置服务器启动参数。比如channel的类型,接收channel的EventLoop,初始化的日志打印事件,建立连接后的事件(拆包,对象转字符串,自定义事件),初始化的配置和建立连接后的配置。

第四步:绑定端口,启动服务。Netty会根据第三步配置的参数启动服务。

第五步:关闭资源。

package com.itdragon.delimiter;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
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;
import io.netty.handler.codec.DelimiterBasedFrameDecoder;
import io.netty.handler.codec.FixedLengthFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
public class ITDragonServer { private static final Integer PORT = 8888; // 被监听端口号
private static final String DELIMITER = "_$"; // 拆包分隔符 public static void main(String[] args) {
EventLoopGroup bossGroup = new NioEventLoopGroup(); // 用于接收进来的连接
EventLoopGroup workerGroup = new NioEventLoopGroup(); // 用于处理进来的连接
try {
ServerBootstrap serverbootstrap = new ServerBootstrap(); // 启动NIO服务的辅助启动类
serverbootstrap.group(bossGroup, workerGroup) // 分别设置bossGroup, workerGroup 顺序不能反
.channel(NioServerSocketChannel.class) // Channel的创建工厂,启动服务时会通过反射的方式来创建一个NioServerSocketChannel对象
.handler(new LoggingHandler(LogLevel.INFO)) // handler在初始化时就会执行,可以设置打印日志级别
.childHandler(new ChannelInitializer<SocketChannel>() { // childHandler会在客户端成功connect后才执行,这里实例化ChannelInitializer
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception { // initChannel方法执行后删除实例ChannelInitializer,添加以下内容
ByteBuf delimiter = Unpooled.copiedBuffer(DELIMITER.getBytes()); // 获取特殊分隔符的ByteBuffer
socketChannel.pipeline().addLast(new DelimiterBasedFrameDecoder(128, delimiter)); // 设置特殊分隔符用于拆包
// socketChannel.pipeline().addLast(new FixedLengthFrameDecoder(8)); 设置指定长度分割
socketChannel.pipeline().addLast(new StringDecoder()); // 设置字符串形式的解码
socketChannel.pipeline().addLast(new ITDragonServerHandler()); // 自定义的服务器处理类,负责处理事件
}
})
.option(ChannelOption.SO_BACKLOG, 128) // option在初始化时就会执行,设置tcp缓冲区
.childOption(ChannelOption.SO_KEEPALIVE, true); // childOption会在客户端成功connect后才执行,设置保持连接
ChannelFuture future = serverbootstrap.bind(PORT).sync(); // 绑定端口, 阻塞等待服务器启动完成,调用sync()方法会一直阻塞等待channel的停止
future.channel().closeFuture().sync(); // 等待关闭 ,等待服务器套接字关闭
} catch (Exception e) {
e.printStackTrace();
} finally {
workerGroup.shutdownGracefully(); // 关闭线程组,先打开的后关闭
bossGroup.shutdownGracefully();
}
}
}

核心参数说明

NioEventLoopGroup : 是用来处理I/O操作的多线程事件循环器。 Netty提供了许多不同的EventLoopGroup的实现用来处理不同传输协议。

ServerBootstrap : 启动NIO服务的辅助启动类。先配置Netty服务端启动参数,执行bind(PORT)方法才算真正启动服务。

group : 注册EventLoopGroup

channel : channelFactory,用于配置通道的类型。

handler : 服务器始化时就会执行的事件。

childHandler : 服务器在和客户端成功连接后会执行的事件。

initChannel : channelRegistered事件触发后执行,删除ChannelInitializer实例,添加该方法体中的handler。

option : 服务器始化的配置。

childOption : 服务器在和客户端成功连接后的配置。

SocketChannel : 继承了Channel,通过Channel可以对Socket进行各种操作。

ChannelHandler : 通过ChannelHandler来间接操纵Channel,简化了开发。

ChannelPipeline : 可以看成是一个ChandlerHandler的链表。

ChannelHandlerContext : ChannelPipeline通过ChannelHandlerContext来间接管理ChannelHandler。

自定义服务器处理类

第一步:继承 ChannelInboundHandlerAdapter,其父类已经实现了ChannelHandler接口,简化了开发。

第二步:覆盖 chanelRead()事件处理方法 ,每当服务器从客户端收到新的数据时,该方法会在收到消息时被调用。

第三步:释放 ByteBuffer,ByteBuf是一个引用计数对象,这个对象必须显示地调用release()方法来释放。

第四步:异常处理,即当Netty由于IO错误或者处理器在处理事件时抛出的异常时触发。在大部分情况下,捕获的异常应该被记录下来并且把关联的channel给关闭掉。

package com.itdragon.delimiter;
import com.itdragon.utils.ITDragonUtil;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.util.ReferenceCountUtil; public class ITDragonServerHandler extends ChannelInboundHandlerAdapter{
private static final String DELIMITER = "_$"; // 拆包分隔符
@Override
public void channelRead(ChannelHandlerContext chc, Object msg) {
try {
// 普通读写数据
/* 设置字符串形式的解码 new StringDecoder() 后可以直接使用
ByteBuf buf = (ByteBuf) msg;
byte[] req = new byte[buf.readableBytes()];
buf.readBytes(req);
String body = new String(req, "utf-8");
*/
System.out.println("Netty Server : " + msg.toString());
// 分隔符拆包
String response = ITDragonUtil.cal(msg.toString())+ DELIMITER;
chc.channel().writeAndFlush(Unpooled.copiedBuffer(response.getBytes()));
} catch (Exception e) {
e.printStackTrace();
} finally {
ReferenceCountUtil.release(msg); // 写入方法writeAndFlush ,Netty已经释放了
}
}
// 当出现Throwable对象才会被调用
@Override
public void exceptionCaught(ChannelHandlerContext chc, Throwable cause) {
// 这个方法的处理方式会在遇到不同异常的情况下有不同的实现,比如你可能想在关闭连接之前发送一个错误码的响应消息。
cause.printStackTrace();
chc.close();
}
}

客户端启动流程

第一步:创建一个用于发送请求的线程池。

第二步:客户端实例化Bootstrap NIO服务启动辅助类,简化开发。

第三步:配置参数,粘包,发送请求。

第四步:关闭资源。

值得注意的是,和ServerBootstrap不同,它并没有childHandler和childOption方法。

package com.itdragon.delimiter;
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.DelimiterBasedFrameDecoder;
import io.netty.handler.codec.FixedLengthFrameDecoder;
import io.netty.handler.codec.string.StringDecoder; public class ITDragonClient { private static final Integer PORT = 8888;
private static final String HOST = "127.0.0.1";
private static final String DELIMITER = "_$"; // 拆包分隔符 public static void main(String[] args) {
NioEventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(group)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
ByteBuf delimiter = Unpooled.copiedBuffer(DELIMITER.getBytes());
// 设置特殊分隔符
socketChannel.pipeline().addLast(new DelimiterBasedFrameDecoder(128, delimiter));
// 设置指定长度分割 不推荐,两者选其一
// socketChannel.pipeline().addLast(new FixedLengthFrameDecoder(8));
socketChannel.pipeline().addLast(new StringDecoder());
socketChannel.pipeline().addLast(new ITDragonClientHandler());
}
})
.option(ChannelOption.SO_KEEPALIVE, true); ChannelFuture future = bootstrap.connect(HOST, PORT).sync(); // 建立连接
future.channel().writeAndFlush(Unpooled.copiedBuffer(("1+1"+DELIMITER).getBytes()));
future.channel().writeAndFlush(Unpooled.copiedBuffer(("6+1"+DELIMITER).getBytes()));
future.channel().closeFuture().sync();
} catch (Exception e) {
e.printStackTrace();
} finally {
group.shutdownGracefully();
}
}
}

客户端请求接收类

和服务器处理类一样,这里只负责打印数据。

package com.itdragon.delimiter;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.util.ReferenceCountUtil;
public class ITDragonClientHandler extends ChannelInboundHandlerAdapter{ @Override
public void channelRead(ChannelHandlerContext chc, Object msg) {
try {
/* 设置字符串形式的解码 new StringDecoder() 后可以直接使用
ByteBuf buf = (ByteBuf) msg;
byte[] req = new byte[buf.readableBytes()];
buf.readBytes(req);
String body = new String(req, "utf-8");
*/
System.out.println("Netty Client :" + msg);
} catch (Exception e) {
e.printStackTrace();
} finally {
ReferenceCountUtil.release(msg);
}
}
public void exceptionCaught(ChannelHandlerContext chc, Throwable cause) {
cause.printStackTrace();
chc.close();
}
}

打印结果

一月 29, 2018 11:31:10 上午 io.netty.handler.logging.LoggingHandler channelRegistered
信息: [id: 0xcf3a3ac1] REGISTERED
一月 29, 2018 11:31:11 上午 io.netty.handler.logging.LoggingHandler bind
信息: [id: 0xcf3a3ac1] BIND: 0.0.0.0/0.0.0.0:8888
一月 29, 2018 11:31:11 上午 io.netty.handler.logging.LoggingHandler channelActive
信息: [id: 0xcf3a3ac1, L:/0:0:0:0:0:0:0:0:8888] ACTIVE
一月 29, 2018 11:31:18 上午 io.netty.handler.logging.LoggingHandler channelRead
信息: [id: 0xcf3a3ac1, L:/0:0:0:0:0:0:0:0:8888] READ: [id: 0xf1b8096b, L:/127.0.0.1:8888 - R:/127.0.0.1:4777]
一月 29, 2018 11:31:18 上午 io.netty.handler.logging.LoggingHandler channelReadComplete
信息: [id: 0xcf3a3ac1, L:/0:0:0:0:0:0:0:0:8888] READ COMPLETE
Netty Server : 1+1
Netty Server : 6+1 Netty Client :2
Netty Client :7

从日志中可以看出Channel的状态从REGISTERED ---> ACTIVE ---> READ ---> READ COMPLETE。服务端也是按照特殊分割符拆包。

总结

看完本章,你必须要掌握的三个知识点:NioEventLoopGroup,ServerBootstrap,ChannelHandlerAdapter

1 NioEventLoopGroup 本质就是一个线程池,管理多个NioEventLoop,一个NioEventLoop管理多个Channel。

2 NioEventLoop 负责不停地轮询IO事件,处理IO事件和执行任务。

3 ServerBootstrap 是NIO服务的辅助启动类,先配置服务参数,后执行bind方法启动服务。

4 Bootstrap 是NIO客户端的辅助启动类,用法和ServerBootstrap类似。

5 Netty 使用FixedLengthFrameDecoder 固定长度拆包,DelimiterBasedFrameDecoder 分隔符拆包。

到这里,Netty的拆包粘包,以及Netty的重要组件,服务器启动流程到这里就结束了,如果觉得不错可以点一个** "推荐" ** ,也可以** "关注" **我哦。

优质文章

http://blog.csdn.net/spiderdog/article/category/1800249

https://www.jianshu.com/p/c5068caab217

Netty 拆包粘包和服务启动流程分析的更多相关文章

  1. 【转】Netty 拆包粘包和服务启动流程分析

    原文:https://www.cnblogs.com/itdragon/archive/2018/01/29/8365694.html Netty 拆包粘包和服务启动流程分析 通过本章学习,笔者希望你 ...

  2. netty拆包粘包

    客户端 tcp udp socket网络编程接口 http/webservice mqtt/xmpp 自定义RPC (dubbo) 应用层 服务端 ServerSocket ss = new serv ...

  3. Cinder Volume 服务启动流程分析和周期性任务分析

    1.cinder-volume服务的程序入口 #!/usr/bin/python2 # PBR Generated from u'console_scripts' import sys from ci ...

  4. Netty入门教程:Netty拆包粘包技术讲解

    Netty编解码技术是什么意思呢?所谓的编解码技术,说白了就是java序列化技术.序列化有两个目的: 1.进行网络传输2.对象持久化 虽然我们可以使用java进行序列化,Netty去传输.但是java ...

  5. 服务端NETTY 客户端非NETTY处理粘包和拆包的问题

    之前为了调式和方便一直没有处理粘包的问题,今天专门花了时间来搞NETTY的粘包处理,要知道在高并发下,不处理粘包是不可能的,数据流的混乱会造成业务的崩溃什么的我就不说了.所以这个问题 在我心里一直是个 ...

  6. Netty中粘包和拆包的解决方案

    粘包和拆包是TCP网络编程中不可避免的,无论是服务端还是客户端,当我们读取或者发送消息的时候,都需要考虑TCP底层的粘包/拆包机制. TCP粘包和拆包 TCP是个“流”协议,所谓流,就是没有界限的一串 ...

  7. Netty入门系列(2) --使用Netty解决粘包和拆包问题

    前言 上一篇我们介绍了如果使用Netty来开发一个简单的服务端和客户端,接下来我们来讨论如何使用解码器来解决TCP的粘包和拆包问题 TCP为什么会粘包/拆包 我们知道,TCP是以一种流的方式来进行网络 ...

  8. 使用Netty如何解决拆包粘包的问题

    首先,我们通过一个DEMO来模拟TCP的拆包粘包的情况:客户端连续向服务端发送100个相同消息.服务端的代码如下: AtomicLong count = new AtomicLong(0); NioE ...

  9. cassandra 服务启动流程

    cassandra 服务启动流程 1.  setup 1)   CassandraDaemon ->main publicstaticvoidmain(String[]args) { insta ...

随机推荐

  1. rsyslogd以及日志轮替logrotate的梳理

    rsyslog 1)日志类型 auth :(authpriv) 主要与认证有关的机制,例如 login, ssh, su 等需要帐号/密码的咚咚: cron: 就是例行性工作排程 cron/at 等产 ...

  2. 为什么树莓派不会受到 Spectre 和 Meltdown 攻击

    最近爆出来的 Intel CPU 的底层漏洞可谓是影响巨大,过去20年的电脑都可能会受影响.前几天 Raspberry Pi 的官方 Twitter(@Raspberry_Pi) 转推了这篇文章,通过 ...

  3. timeline自适应时间轴

    近期项目一直有类似QQ空间那样的时间轴,来展示公司新闻动态,或者流程之类的设计UI. 每每出现,不以为然,这次总结了下,精简下 ================= ================== ...

  4. ES6小点心之通用弹窗

    小点心,顾名思义,开箱即食,拿来即用. 前端业务逻辑主要分为[交互效果]和[数据展示]两方面.数据展示可使用 MVVM 框架来实现.前端的交互效果常用的也就那么几种,比如弹窗,楼层定位,倒计时,下拉刷 ...

  5. BIOS 品牌快捷键

    主板品牌 启动按键 笔记本品牌 启动按键 台式机品牌 启动按键 华硕主板 F8 联想笔记本 F12 联想台式机 F12 技嘉主板 F12 宏基笔记本 F12 惠普台式机 F12 微星主板 F11 华硕 ...

  6. Redis 部署主从哨兵 C#使用,实现自动获取redis缓存 实例2

    资料查找https://www.cnblogs.com/tdws/p/5836122.html https://www.cnblogs.com/lori/p/5794454.html private ...

  7. (5编译使用最新opencv)从零开始的嵌入式图像图像处理(PI+QT+OpenCV)实战演练

    从零开始的嵌入式图像图像处理(PI+QT+OpenCV)实战演练 1综述http://www.cnblogs.com/jsxyhelu/p/7907241.html 2环境架设http://www.c ...

  8. 从Unity中的Attribute到AOP(五)

    今天主要来讲一下Unity中带Menu的Attribute. 首先是AddComponentMenu.这是UnityEngine命名空间下的一个Attribute. 按照官方文档的说法,会在Compo ...

  9. asp.net core 配置

    ASP.NET Core的配置系统已经和之前版本的ASP.NET有所不同了,之前是依赖于System.Configuration和XML配置文件web.config,现在支持各种格式的配置,比以前灵活 ...

  10. SPATRA的使用

    SPATRA是kali里的集成工具,可以自动化渗透测试 在命令行里键入:sparta即可进入 进去后单机左边空白处 输入你要进行渗透测试的IP 正在扫描中 点击bute可以进入hydra