前阵子由于公司需要,接触到了网络传输框架Netty,Netty是由JBOSS提供的一个java开源框架。Netty提供异步的、事件驱动的网络应用程序框架和工具,用以快速开发高性能、高可靠性的网络服务器和客户端程序。
也就是说,Netty 是一个基于NIO的客户、服务器端编程框架,使用Netty 可以确保你快速和简单的开发出一个网络应用,例如实现了某种协议的客户,服务端应用。Netty相当简化和流线化了网络应用的编程开发过程,例如,TCP和UDP的socket服务开发。
如果是要系统学习Netty的话可以从java 的IO开始学习,到NIO,后面再去接触Netty就会觉得一气呵成了,但是如果是需要快速熟悉,需要大致了解之后就开始编码的话,可以参考下面这篇系列文章:
经过上面的学习之后,再加上你的感悟力就可以开始打码了,这篇文章只讨论服务端,客户端开发请绕行:
下面文章大致分为6个部分:
jar包导入、Netty类和主方法的启动、解码器略讲、粘包拆包、业务处理
1、jar包导入:
<!-- Netty Start -->
<!-- 使用Spring管理netty -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.0.Final</version>
</dependency>
<!-- jboss-marshalling-river -->
<dependency>
<groupId>org.jboss.marshalling</groupId>
<artifactId>jboss-marshalling-river</artifactId>
<version>2.0.0.CR1</version>
</dependency>
<!-- jboss-marshalling -->
<dependency>
<groupId>org.jboss.marshalling</groupId>
<artifactId>jboss-marshalling</artifactId>
<version>2.0.0.CR1</version>
</dependency>
<!-- aurora-api -->
<dependency>
<groupId>org.apache.aurora</groupId>
<artifactId>aurora-api</artifactId>
<version>0.8.0</version>
</dependency>
<!-- Netty End -->
上面是netty会用到的jar包,其中Spring-context包的使用场景是如果你需要使用Spring管理Netty的话会用到,我们暂时系统中用到的是通过加载Spring的配置文件,然后通过getBean("BeanName")的形式get到Bean的,这样也可以通过Spring来管理Netty的事务。
2、Netty类和主方法的启动
这里需要稍微讲一下就是启动Netty我们采用的是通过new Thread的形式去加载,原因很简单,就是虽然Netty中已经帮我们实现了多线程,高并发等等,属于不堵塞的场景。但是现实的运用中,我们往往是启动了Netty之后,我们就不用再次启动了,然后具体的业务方法会帮我们一直在接受来自客户端的数据,这样意味着启动了这个主业务方法之后,Netty就跟吃了炫迈一样,根本停不下来!这会直接导致Tomcat启动失败,因为它一直在启动。
这里需要稍微讲一下就是启动Netty我们采用的是通过new Thread的形式去加载,原因很简单,就是虽然Netty中已经帮我们实现了多线程,高并发等等,属于不堵塞的场景。但是现实的运用中,我们往往是启动了Netty之后,我们就不用再次启动了,然后具体的业务方法会帮我们一直在接受来自客户端的数据,这样意味着启动了这个主业务方法之后,Netty就跟吃了炫迈一样,根本停不下来!这会直接导致Tomcat启动失败,因为它一直在启动。
因为什上面的原因,我采用的方法是通过添加监听事件来实现启动的方式,然后再new Thread来给Netty加载数据,实现步骤如下:
首先编写web.xml文件,添加listener事件(注:<listener></listener>标签要写在<filter></filter>下面,具体原因自己百度)
<listener>
<listener-class>com.John.service.netty.StartNettyFromInit</listener-class>
</listener>
然后需要在那个类那里实现ServletContextListener接口,然后重写两个方法:contextInitialized()、contextDestroyed()这两个方法是在Tomcat启动时加载、Tomcat关闭时加载。这里我们把具体的方法写在启动方法下面;
@Override
public void contextInitialized(ServletContextEvent sce) {
//使用一个新的线程启动netty的连接和其耗时操作,避免主线程堵塞
new Thread(new Runnable() {
@Override
public void run() {
/*NioEventLoopGroup可以理解为一个线程池,内部维护了一组线程,每个线程负责处理多个Channel上的事件,而一个Channel只对应于一个线程,这样可以回避多线程下的数据同步问题。
这里新创建两个线程池,一个负责连接,一个负责处理业务*/
//boss用来接收进来的连接
NioEventLoopGroup bossGroup = new NioEventLoopGroup();
//work用来处理已经被接收的连接;
NioEventLoopGroup workGroup = new NioEventLoopGroup();
//ServerBootstrap负责初始化netty服务器,并且开始监听端口的socket请求。
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup, workGroup);
//channel()方法设置了ServerBootstrap的ChannelFactory,这里传入的参数是NioServerSocketChannel.class,也就是说这个ReflectiveChannelFactory创建的就是NioServerSocketChannel的实例。
bootstrap.channel(NioServerSocketChannel.class);
//设置workGroup的Handler
bootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
//这里接收到的报文可能会出现粘包拆包的问题,解决方法可以为:
/*1、消息定长,报文大小固定长度,不够空格补全,发送和接收方遵循相同的约定,这样即使粘包了通过接收方编程实现获取定长报文也能区分。
2、包尾添加特殊分隔符,例如每条报文结束都添加回车换行符(例如FTP协议)或者指定特殊字符作为报文分隔符,接收方通过特殊分隔符切分报文区分。
3、将消息分为消息头和消息体,消息头中包含表示信息的总长度(或者消息体长度)的字段
注意:选择哪种关键是看客户端是使用什么协议发过来的,*/
/*//1、如果客户端使用定长消息发过来,并且直接是发String可以使用下面这种:
//ch.pipeline().addLast(new StringEncoder());
//ch.pipeline().addLast(new FixedLengthFrameDecoder(5));
//ch.pipeline().addLast(new StringDecoder());*/
/*//2、如果是报尾有特殊分隔符的(例如:一条报文是从十六进制==》字节流==》buf==》字节流):
//将这个分隔符转成byte[]
byte[] a = SysConverUtils.hex2byte("EEEE");
//将byte[]转成buf流
ByteBuf delimiter = Unpooled.copiedBuffer(a);
//核心内置方法,可以通过切割得到切割后的报文,一次最大检测长度设置为int的最大值,考虑到图片大小
ch.pipeline().addLast(new DelimiterBasedFrameDecoder(Integer.MAX_VALUE,delimiter));*/
/*//3、消息分为消息头和消息体,消息头中包含表示信息的总长度
这里如果是自己写,就要同时写编码器和解码器,如果只是服务器端的话就只要写解码器就行了。
由于内容太多了,大概思路是用一个类来暂存数据头信息,解析包长,然后可以用包长来截取适当的长度
具体可以参考这个网址:http://blog.csdn.net/zbw18297786698/article/details/53691915 */
//下面的DealHandler类是用来处理主要的业务逻辑的
ch.pipeline().addLast(new DealHandler());
}
});
//BACKLOG用于构造服务端套接字ServerSocket对象,标识当服务器请求处理线程全满时,用于临时存放已完成三次握手的请求的队列的最大长度。如果未设置或所设置的值小于1,Java将使用默认值50。
bootstrap.option(ChannelOption.SO_BACKLOG, 1024);
//是否启用心跳保活机制。在双方TCP套接字建立连接后(即都进入ESTABLISHED状态)并且在两个小时左右上层没有任何数据传输的情况下,这套机制才会被激活。
bootstrap.childOption(ChannelOption.SO_KEEPALIVE, true);
ChannelFuture future = null;
try {
future = bootstrap.bind(10086).sync();
} catch (Exception e) {
e.printStackTrace();
}
//等待服务器socket关闭
try {
/*ChannelFuture sync():等待,直到异步操作执行完毕,核心思想同await。我们得到Future实例后,可以使用sync()方法来阻塞当前线程,
* 直到异步操作执行完毕。和await的区别为,如果异步操作失败,那么将会重新抛出异常(将上述cause()方法中的异常抛出)。await和sync一样,
* 当异步操作执行完毕后,通过notifyAll()唤醒。*/
future.channel().closeFuture().sync();
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
}
3、编解码:
首先了解什么是编码器,什么是解码器:
解码器:负责将消息从字节或其他序列形式转成指定的消息对象;
编码器:将消息对象转成字节或其他序列形式在网络上传输。
上面用一句话总结了概念,常见的解码器有:ByteToMessageDecoder、ReplayingDecoder、MessageToMessageDecoder、tringEncoder,用更加通俗的语言来说,就是一种变换规则的反方向过程。例如
ch.pipeline().addLast(new StringEncoder());
这里是使用了StringEncoder()解码器,这个解码器的作用就是把从网络、磁盘等读取的字节数组还原成原始对象(通常是原始对象的拷贝),以方便后续的业务逻辑操作。
机智的你,视力很好的你有可能会很疑问,我去,你怎么上面都注释掉了,不用解码器是要闹哪样,是的,我的确没有用解码器,因为我们的业务实在太坑了,绕了N多的弯,我就直接接收Object对象到后面的具体方法处理了,后面第五点的业务处理方面会讲到,请往下看。
4、粘包拆包的处理:
首先看图分析,为啥会出现粘包的情况:
那么粘包之后我会怎么处理呢?当然是使用自带的工具啦,Netty早就想到了会出现这个问题啦,所以它早就准备好了几个工具给你用:
1、消息定长,报文大小固定长度,不够空格补全,发送和接收方遵循相同的约定,这样即使粘包了通过接收方编程实现获取定长报文也能区分。
2、包尾添加特殊分隔符,例如每条报文结束都添加回车换行符(例如FTP协议)或者指定特殊字符作为报文分隔符,接收方通过特殊分隔符切分报文区分。
3、将消息分为消息头和消息体,消息头中包含表示信息的总长度(或者消息体长度)的字段
你现在可以去看上面的代码,上面已经说到这三种解决方案,详细的反正我是不会这这里写的,因为大神们已经写好了很多例子了,我搬下砖:
BazingaLyncc CSDN博客
转载请注明出处,谢谢。
5、业务处理:
在上面的代码那里,你可以看到有那么一行:
ch.pipeline().addLast(new DealHandler()
机智,你已经功能猜到了,这个就是我的主业务方法,就是你收到报文之后你要咋地的地方,下面我把上面那个解码器的也联合起来讲,因为刚才我是拒绝使用解码器的,这里我会将它应得的东西“弥补”回来,下面贴代码:
@Sharable
public class DealHandler extends SimpleChannelInboundHandler<Object> {
public DealHandler() {
}
/**
* @author John
* @date 2017年12月26日 下午2:35:56
* @describe 主方法,接收来自NettyServer的数据,并转成十六进制后处理数据
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
//1、以ByteBuf的形式接收来自上一个方法的流数据
ByteBuf msgBuf = (ByteBuf) msg;
//2、将ByteBuf类型的数据转成Byte
byte[] result = new byte[msgBuf.readableBytes()];
msgBuf.readBytes(result);
// 释放资源,这行很关键
msgBuf.release();
//3、使用十六进制解码数据并做详细的数据操作
String allRec = ByteTransformer.byte2Hex(result);
//去掉注释可以查看收到的全部报文
//System.out.println("全部收到的报文为:===>" + allRec);
}
/**
*@author John
*@date 2017年12月26日
*@describe
*/
protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
}
注意点:这个类头顶有个注解:
@Sharable
这个注解是因为你是通过多线程那边过来的数据,所以你需要线程共享。上面的Object msg 这个就是我们刚才通过拆粘包处理之后的数据,至于是什么数据,这里需要通过一个逆向过程来获得它。具体的看上面的方法,附件是我的珍贵笔记,总的思路逻辑就是逆向推回去。
差不多了,你拿到那个报文之后你就可以为所欲为了。
打的字挺多的了,我很满意了。先这样吧。有什么问题可以互相交流下哈,要转载的话记得注明出处。