Netty入门

最近由于项目需要使用一款高性能高并发的java网络编程框架,所以选择了netty正好借此机会来学习netty并记录一下

一、Netty介绍

  1.   Netty是由JBOSS提供的一个java开源框架,   Netty提供异步的、事件驱动的网络应用程序框架和工具,用以快速开发高性能、高可靠性的网络服务器和客户端程序。
  2. Netty是一个NIO客户端服务端框架,它使得快速而简单的开发像服务端客户端协议的网络应用成为了可能。它极大的简化并流 线化了如TCP和UDP套接字服务器开发的网络编程。“快速且简便”不意味着目标应用将容忍维护性和性能上的问题。Netty在吸取了大量协议实现(如FTP,SMTP,HTTP以及各种二进制,基于文本的传统协议)的经验上进行了精心的设计。由此,Netty成功找到了一个无需折衷妥协而让开发、性能、稳定性和灵活性相互协调的方法。

二、介绍几个概念

Netty入门_第1张图片

  1. ChannelEvent:Netty是基于事件驱动的,就是我们上文提到的,发生什么事,就通知"有关部门"。所以,不难理解,
      我们自己的业务代码中,一定有跟这些事件相关的处理。
  2. ChannelPipeline:用于保存处理过程需要用到的ChannelHandler和ChannelHandlerContext。Pipeline,翻译成中文的意思是:管道,传输途径。也就是说,在这里他是控制ChannelEvent事件分发和传递的。 事件在管道中流转,第一站到哪,第二站到哪,到哪是终点,就是用这个ChannelPipeline 处理的。比如:开发事件。 先给A设计,然后给B开发。一个流转图,希望能给你更直观的感觉。
  3. ChannelHandler:刚说Pipeline负责把事件分发到相应的站点,那个这个站点在Netty里,就是指ChannelHandler。 事件到了ChannelHandler这里,就要被具体的进行处理了,我们的样例代码里,实现的就是这样一个处理事件的“站点”,也就是说,你自己的业务逻辑一般都是从这里开始的。
  4. Channel:有了个部门的协调处理,我们还需要一个从整体把握形势的,所谓“大局观”的部门channel。channel,能够告诉你当前通道的状态,是连同还是关闭。获取通道相关的配置信息。得到Pipeline等。是一些全局的信息。Channel自然是由ChannelFactory产生的。Channel的实现类型,决定了你这个通道是同步的还是异步的(nio)。例如,我们样例里用的是NioServerSocketChannel
  5. ChannelHandlerContext,用于传输业务数据它起到了一个桥梁的作用,它可以方便的为ChannelHandler传递数据到位于同一ChannelPipeline前面或者后面的ChannelHander,同时它也可以为当前ChannelHandler存储有状态的信息。

三、Netty的jar包下载

netty版本大致版本分为 netty3.x 和 netty4.x、netty5.x。5.x版本在后来被官方取消什么原因这里不多说总之用已成熟的版本绝对没问题。这里我们使用netty4.x版本。需要下载相应的jar包导入项目。jar包可以从Netty官网:http://netty.io/downloads.html 下载

Netty入门_第2张图片

Netty入门_第3张图片

 

 下载之后解压缩。大家可以看到这样一个目录结构。非常的清晰。

  第一个文件夹jar是jar包的文件夹。第二个javadoc是API文档。第三个license文件夹是开源的授权文件(可以直接无视)。

  javadoc文件夹下面是一个jar包。可以直接解压缩出来。解压缩之后的文件夹就是api文档(以网页的形势展现)。

  jar文件夹里面有很多的jar包和一个all-in-one文件夹。都是Netty框架的组成部分。all-in-one里面有两个文件一个是jar包,另一个是对应的source源代码包。这样做的目的是为了给程序员有选择的添加自己所需要的包。

  假如读者是初学者的话。推荐直接套用all-in-one里面的jar包。假如你熟悉Netty的话可以根据自己的项目需求添加不同的jar包。

四、Netty服务端及客户端的实现

Server.java:

package com.li.nettyServer;

import com.li.nettyBusiness.DiscardServerHandler;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.ChannelPipeline;
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.Delimiters;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;

/**
 * Netty_4.x版本
 * 服务类
 * netty内部是通过线程处理各种数据,EventLoopGroup就是用来管理他们的,注册channel管理他们的生命周期
 * 
 */
public class Server extends Thread{
	private int port;
	
	public Server(int port) {
		super();
		this.port = port;
	}
	
	@Override
	public void run() {
		// NioEventLoopGroup 是用来处理I/O操作的多线程事件循环器,是在4.x版本中提出来的一个新概念用于管理Channel连接的。
		//服务端需要两个。和3.x版本一样,一个是boss线程一个是worker线程。
		//因为bossGroup仅接收客户端连接,不做复杂的逻辑处理,为了尽可能减少资源的占用,取值越小越好
		NioEventLoopGroup bossGroup = new NioEventLoopGroup(1);
		//处理bossGroup接收的连接的流量和将接收的连接注册进入这个worker
		NioEventLoopGroup workerGroup = new NioEventLoopGroup();
		try {
			//ServerBootstrap负责建立服务端,ServerBootstrap是一个启动Nio服务的辅助启动类
			//可以在这个服务中直接使用channel,但是大多数情况下你无需做这种乏味的事情
			ServerBootstrap strap = new ServerBootstrap();
			//设置Group否则会报java.lang.IllegalStateException: group not set异常
			strap.group(bossGroup,workerGroup)
			//设置niosocket工厂,ServerSocketChannel以NIO的selector为基础进行实现的,
			//指定使用NioServerSocketChannel产生一个channel用来接收连接
			.channel(NioServerSocketChannel.class)
			/* 设置管道的工厂,用于添加相关的Handler
			 * channelInitializer用于配置一个新的channel,
			 * 用于向你的channel当中添加ChannelInboundHandler的实现
			 */
			.childHandler(new ChannelInitializer(){
				@Override
				protected void initChannel(SocketChannel paramC) throws Exception {
					// channelPipeline用于存放管理channelHandler
					//channelHandler用于请求响应的业务逻辑相关代码
					ChannelPipeline pipeline = paramC.pipeline();
					/**
					 * 拆包粘包:
					 * TCP是一个“流”协议,所谓流,就是没有界限的一长串二进制数据。TCP作为传输层协议并不不了解上层业务数据的具体含义,
					 * 它会根据TCP缓冲区的实际情况进行数据包的划分,所以在业务上认为是一个完整的包,可能会被TCP拆分成多个包进行发送,
					 * 也有可能把多个小的包封装成一个大的数据包发送,这就是所谓的TCP粘包和拆包问题
					 * 解决策略:
					 * 由于底层的TCP无法理解上层的业务数据,所以在底层是无法保证数据包不被拆分和重组的,这个问题只能通过上层的应用协议栈设计来解决。业界的主流协议的解决方案,可以归纳如下: 
					 *	1. 消息定长,报文大小固定长度,例如每个报文的长度固定为200字节,如果不够空位补空格; 
					 *	2. 包尾添加特殊分隔符,例如每条报文结束都添加回车换行符(例如FTP协议)或者指定特殊字符作为报文分隔符,接收方通过特殊分隔符切分报文区分; 
					 *	3. 将消息分为消息头和消息体,消息头中包含表示信息的总长度(或者消息体长度)的字段; 
					 *	4. 更复杂的自定义应用层协议。
					 * Netty提供了多个解码器,可以进行分包的操作,分别是: 
					 * LineBasedFrameDecoder (回车换行解码器)
					 * 			  p.addLast(new LineBasedFrameDecoder(1024));
					 * DelimiterBasedFrameDecoder(分隔符解码器,用户可以指定消息结束的分隔符,它可以自动完成以分隔符作为码流结束标识的消息的解码) 
					 * FixedLengthFrameDecoder(固定长度解码器,它能够按照指定的长度对消息进行自动解码,开发者不需要考虑TCP的粘包/拆包等问题) 
								  ch.pipeline().addLast(new FixedLengthFrameDecoder(1<<5));
					 * LengthFieldBasedFrameDecoder( 将消息分为消息头和消息体,自动屏蔽TCP底层的拆包和粘包问题,只需要传入正确的参数,即可轻松解决“读半包“问题)
					 */
					// 以("\n")为结尾分割的 解码器(添加特殊分隔符报文来分包---拆包粘包)
					pipeline.addLast("framer",new DelimiterBasedFrameDecoder(8192, Delimiters.lineDelimiter()));
					//编码解码
					pipeline.addLast("decode",new StringDecoder());
					pipeline.addLast("encode",new StringEncoder());
					//业务逻辑处理
					pipeline.addLast("handler",new DiscardServerHandler());
				};
				
			})
			/*
			 *对channel进行一些配置,以下是socket的标准参数
			 *BACKLOG用于构造服务端套接字serverSocket对象,标识当前服务器请求处理线程全满时,
			 *用于临时存放已完成三次握手的请求的队列的最大长度,如果未设置或者设置的值小于1,Java使用默认值50 
			 *Option是为了NioServerSocketChannel设置的,用于接收传入连接
			 */
			.option(ChannelOption.SO_BACKLOG, 128)
			/*
			 * 是否启动心跳保活机制,在双方TCP套接字建立后(即都进入ESTABLISHED状态)
			 * 并且在两个小时左右上层没有任何数据传输的情况下,这套机制才会被激活
			 * childOption是用来给父级ServerChannel之下的channels设置的,用于接收到连接
			 */
			.childOption(ChannelOption.SO_KEEPALIVE, true);
			//绑定端口并启动去接收进来的连接
			try {
				ChannelFuture future = strap.bind(port).sync();
				/*
				 * sync()会同步等待连接操作结果,用户将会再此wait(),直到连接操作完成之后,线程被notify(),用户代码继续执行
				 * closeFuture()当Channel关闭时返回一个channelFuture,用于链路检测
				 */
				future.channel().closeFuture().sync();
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			
		}finally {
			//优雅释放资源
			bossGroup.shutdownGracefully();
			workerGroup.shutdownGracefully();
		}
	
	}
}

ServerHandler:

package com.li.nettyBusiness;

import java.lang.ref.Reference;
import java.net.InetAddress;
import java.nio.charset.Charset;

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.util.CharsetUtil;
import io.netty.util.ReferenceCountUtil;

/*
 * 服务端处理通道
 * ChannelInboundHandlerAdapter实现自ChannelInboundHandler
 * ChannelInboundHandler提供了不同事件的处理方法(可自己重写)
 */
public class HelloServerHandler extends ChannelInboundHandlerAdapter   {
	/**
	 * 每当客户端收到新的数据时,这个方法会在收到消息是被调用
	 * 收到消息的类型是ByteBuf
	 * @param ctx 通道处理的上下文信息
	 * @param msg 接收的信息
	 */
	@Override
	public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
		// TODO Auto-generated method stub
		try {
			/*
			 * ChannelHandlerContext提供各种不同的操作用于触发不同的I/O时间和操作
			 * 调用write方法来逐字返回接收到的信息
	         * 这里我们不需要在DISCARD例子当中那样调用释放,因为Netty会在写的时候自动释放
	         *只调用write是不会释放的,它会缓存,直到调用flush
			 */
			//打印客户端传过来的信息
			//ByteBuf in = (ByteBuf)msg;   //在pipeline中已经进行String类型的编解码操作,所以此时不用ByteBuf接收	
			//System.out.println("客户端:"+in.toString(CharsetUtil.UTF_8));
			System.out.println("客户端:"+msg.toString());
			
			// 注意:字符串最后面的"\n"是必须的。因为我们在前面的解码器DelimiterBasedFrameDecoder是一个
			//根据字符串结尾为“\n”来结尾的。假如没有这个字符的话。解码会出现问题。
			ctx.writeAndFlush("hello Netty\r\n");
			/*另一种写法
			 *  ctx.write(msg);
        	 *	ctx.flush();
			 */
			//ctx.channel().write("1");
		}finally{
			/**
			 * ByteBuf是一个引用计数对象,他是在有对象引用的时候计数+1,无的时候计数-1,当为0对象释放内存,
			 * 这个对象必须显示的调用release()方法来释放
			 * 
			 * 处理器的职责是释放所有传递到处理器的引用计数对象
			 */
			//抛弃接收到的数据
			ReferenceCountUtil.release(msg);
		}
	
	}
	/**
	 * 这个方法会在发生异常时调用
	 */
	@Override
	public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
		// TODO Auto-generated method stub
		/**
		 * exceptionCaught()事件处理方法是当出现Throwable才会被调用,即当netty出现IO错误或者处理器在处理事件时
		 * 抛出的异常时。在大部分情况下,扑捉的异常应该被记录下来并且把关联的channel给关闭掉,然而这个方法的处理方式在遇到
		 * 不同的异常时会有不同的实现,比如你想在关闭连接之前发送一个错误码的响应消息
		 */
		//出现异常就关闭
		cause.printStackTrace();
		ctx.close();
		
	}
	/**
	 * 通道连接事件。
	 * 当Channel变成活跃状态时被调用
	 */
	@Override
	public void channelActive(ChannelHandlerContext ctx) throws Exception {
		// TODO Auto-generated method stub
	    System.out.println("RamoteAddress : "+ ctx.channel().remoteAddress()+ " active !");
		   
	    ctx.writeAndFlush("welcome to "+ InetAddress.getLocalHost().getHostName() + " server.\n");
		super.channelActive(ctx);
	}
	/**
	 * 连接中断时,调用它
	 */
	@Override
	public void channelInactive(ChannelHandlerContext ctx) throws Exception {
		// TODO Auto-generated method stub
		super.channelInactive(ctx);
	}
	/**
	 * 一旦更改了{连接信道}的可写状态,就调用。您可以检查状态
	 */
	@Override
	public void channelWritabilityChanged(ChannelHandlerContext ctx) throws Exception {
		// TODO Auto-generated method stub
		super.channelWritabilityChanged(ctx);
	}
	/**
	 * 如果触发了用户事件,将调用它
	 */
	@Override
	public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
		// TODO Auto-generated method stub
		super.userEventTriggered(ctx, evt);
	}
	
}

上面的是服务端的代码里面的注释解释的都都清楚了这里就不在解释了接着我们来编写客户端程序:

Client.java:

package com.li.nettyServer;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

import com.li.nettyBusiness.HelloClientInitializer;

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;

public class Client extends Thread{

	private static String host="127.0.0.1";
	private static int port=8888;
	@Override
	public void run() {
		// TODO Auto-generated method stub
		//接收的连接的流量和将接收的连接注册进入这个worker
		EventLoopGroup workerGroup = new NioEventLoopGroup();
		try {
			Bootstrap bt = new Bootstrap(); //建立客户端
			bt.group(workerGroup);//设置group
			bt.channel(NioSocketChannel.class); //设置niosocket工厂
			bt.handler(new HelloClientInitializer()); //设置管道的工厂,用于添加相关的Handler处理类
			
			//连接服务端
			Channel channel = bt.connect(host,port).sync().channel();
			//控制台输出
			BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
			for(;;) {  //这是一个死循环
				String line = in.readLine();
				if(line == null) {
					continue;
				}
				// 注意:字符串最后面的"\n"是必须的。因为我们在前面的解码器DelimiterBasedFrameDecoder是一个
				//根据字符串结尾为“\n”来结尾的。假如没有这个字符的话。解码会出现问题。
				channel.writeAndFlush(line+"\r\n");
			}
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}finally {
			//优雅释放资源
			workerGroup.shutdownGracefully();
		}
	}
	
}

ClientInitializer.java

这里从客户端的Client类中分离出来了,也可以写在一块。

package com.li.nettyBusiness;

import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.DelimiterBasedFrameDecoder;
import io.netty.handler.codec.Delimiters;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;

public class HelloClientInitializer extends ChannelInitializer{

	@Override
	protected void initChannel(SocketChannel sc) throws Exception {
		// TODO Auto-generated method stub
		//这个地方的 必须和服务端对应上。否则无法正常解码和编码
		ChannelPipeline pipeline = sc.pipeline();
		// 以("\n")为结尾分割的 解码器
		pipeline.addLast("framer",new DelimiterBasedFrameDecoder(8192, Delimiters.lineDelimiter()));
		//String编码解码
		pipeline.addLast("decoder",new StringDecoder());
		pipeline.addLast("encoder",new StringEncoder());
		//客户端逻辑
		pipeline.addLast("handler",new HelloClientHandler());
	}

}

ClientHandler.java

package com.li.nettyBusiness;

import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.SimpleChannelInboundHandler;

public class HelloClientHandler extends SimpleChannelInboundHandler {

	
	@Override
	protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
		// TODO Auto-generated method stub
		System.out.println("服务器:"+msg.toString());
	}

	@Override
	public void channelActive(ChannelHandlerContext ctx) throws Exception {
		// TODO Auto-generated method stub
		System.out.println("Client active ");
		super.channelActive(ctx);
	}

	@Override
	public void channelInactive(ChannelHandlerContext ctx) throws Exception {
		// TODO Auto-generated method stub
		System.out.println("Client closes");
		super.channelInactive(ctx);
	}

	
}

main启动类:

package com.li.main;

import com.li.nettyServer.Client;
import com.li.nettyServer.Server;

public class main {
	public static void main(String[] args) {
		int port;
		if(args.length>0) {
			port=Integer.parseInt(args[0]);
			System.out.println("args[0]:"+args[0]);
			
		}else {
			port=8888;
		}
		
		
		new Server(port).start();
		new Client().start();
	}
}

好了,上面就是Netty服务端及客户端的全部代码了。上面的解释也很清楚如果不是特别懂的还需找度娘啦哈哈哈

下来看看运行结果

Netty入门_第4张图片

Netty入门_第5张图片

ok没问题。 下来就要了解netty的各个组件更加深入的学习。

你可能感兴趣的:(Netty)