01、Netty框架使用之 - 新手起步

前阵子由于公司需要,接触到了网络传输框架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启动失败,因为它一直在启动。
因为什上面的原因,我采用的方法是通过添加监听事件来实现启动的方式,然后再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 这个就是我们刚才通过拆粘包处理之后的数据,至于是什么数据,这里需要通过一个逆向过程来获得它。具体的看上面的方法,附件是我的珍贵笔记,总的思路逻辑就是逆向推回去。


差不多了,你拿到那个报文之后你就可以为所欲为了。
打的字挺多的了,我很满意了。先这样吧。有什么问题可以互相交流下哈,要转载的话记得注明出处。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值