Netty1:初识Netty

前言:用netty跑了那么久,,也没有真正细枝末节地认识过她,借着2020这年行业大环境,尽一份Coder应有的责任(总结),发现一个蛮优秀的coder有个系列netty一二三,和自己实际项目种用到的差无别二,就转过来(偷个小懒码了),也在必要地方加上了自己的示例图, 这学长内容亲试可关,昵称: 五月的仓颉;原文地址http://www.cnblogs.com/xrq730/p/5260294.html,转载请注明出处

------------------------------------------------------------------------------------------------------------------------

为什么使用Netty

Netty是业界最流行的NIO框架之一,它的健壮性、功能、性能、可定制性、可扩展性在同类框架中都是首屈一指的,它已经得到了成百上千的商用项目的证明。对于为什么使用Netty这个话题,我们先看一下使用原生的NIO有什么缺点:

  • NIO的类库和API繁杂,使用麻烦,需要熟练掌握Selector、ServerSocketChannel、SocketChannel、ByteBuffer等,这就像我们会使用Hibernate、MyBatis这些ORM框架而不会直接使用Connection、Statement一样
  • 需要其他额外技能作为铺垫,必须对多线程和网络编程非常熟悉才能写出高质量的NIO程序
  • 可靠性能力补齐,工作量和难度都非常大,例如客户端面临断线重连、网络闪断、半包读写、失败缓存、网络拥塞、异常码流等问题的处理
  • JDK NIO的BUG,例如著名的epoll bug,该问题会导致Selector空轮训,最终导致CPU 100%

也正是因为有种种缺点,因此不建议使用原生的NIO而是建议使用一些比较成熟的NIO框架例如Netty、Mina,这一系列文章讲的是Netty,Netty作为一款高性能NIO框架,其优点总结有:

  • API使用简单、开发门槛低
  • 功能强大,预置了多种编码解码功能,支持多种主流协议
  • 定制能力强,可以通过ChannelHandler对通信框架进行灵活扩展
  • 性能高,与业界其他主流NIO框架对比,Netty性能最优
  • 成熟、稳定,Netty修复了已经发现的所有JDK NIO的BUG,业务开发人员不需要再为NIO的BUG而烦恼
  • 社区活跃、版本迭代周期短,发现的BUG可以被及时修复,同时,更多的新功能会被加入
  • 经历了大规模的商业应用考验,质量得到验证

正因为这些优点,Netty逐渐成为了Java NIO变成的首选框架。

Netty入门Demo

下面演示一下Netty的Demo(注:Demo来自Netty权威指南第三章),本文只写代码与演示结果,不做讲解,对Netty的使用基本讲解放在下一篇文章中,循序渐进,先感性地认识Netty,再理性地认识Netty中的东西。

提一下,本文及之后的文章Netty基于5.0.0.Alpha1这个版本,贴一下我自己的Maven配置(4.0.33.Final)两年开始用的版本,一直没升级过吧:


      4.0.0

      org.xrq.netty
      netty-test
      1.0.0
      jar


      
        UTF-8
      

      
        
              junit
              junit
              4.11
              test
        
        
            io.netty
            netty-all
            5.0.0.Alpha1
        
        
            org.slf4j
            slf4j-api
            1.7.25
        
        
            ch.qos.logback
            logback-classic
            1.2.3
        
      
      

首先从服务端代码开始,定义一个TimeServer: 

public class TimeServer {

    public void bind(int port) throws Exception {
        // NIO线程组
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        
        try {
            ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup, workerGroup)
                .channel(NioServerSocketChannel.class)
                .option(ChannelOption.SO_BACKLOG, 1024)
                .childHandler(new ChildChannelHandler());
            
            // 绑定端口,同步等待成功
            ChannelFuture f = b.bind(port).sync();
            // 等待服务端监听端口关闭
            f.channel().closeFuture().sync();
        } finally {
            // 优雅退出,释放线程池资源
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
    
    private class ChildChannelHandler extends ChannelInitializer {
        @Override
        protected void initChannel(SocketChannel arg0) throws Exception {
            arg0.pipeline().addLast(new TimeServerHandler());
        }
    }
    
}

TimeServerHandler这么定义:

public class TimeServerHandler extends ChannelHandlerAdapter {

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        ByteBuf buf = (ByteBuf)msg;
        byte[] req = new byte[buf.readableBytes()];
        buf.readBytes(req);
        
        String body = new String(req, "UTF-8");
        System.out.println("The time server receive order:" + body);
        String currentTime = "QUERY TIME ORDER".equalsIgnoreCase(body) ? new Date(System.currentTimeMillis()).toString() : "BAD ORDER";
        
        ByteBuf resp = Unpooled.copiedBuffer(currentTime.getBytes());
        ctx.write(resp);
    }
    
    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        ctx.flush();
    }
    
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        ctx.close();
    }
    
}

 即读取来自客户端的数据,如果是"QUERY TIME ORDER",则把当前时间写到Channel中去。至此,Netty服务端代码已经开发完毕。接下来是Netty客户端代码,首先还是TimeClient:

 

public class TimeClient {

    public void connect(int port, String host) throws Exception {
        EventLoopGroup group = new NioEventLoopGroup();
        try {
            Bootstrap b = new Bootstrap();
            
            b.group(group)
                .channel(NioSocketChannel.class)
                .option(ChannelOption.TCP_NODELAY, true)
                .handler(new ChannelInitializer() {
                    protected void initChannel(SocketChannel ch) throws Exception {
                        ch.pipeline().addLast(new TimeClientHandler());
                    };
                });
            
            // 发起异步连接操作
            ChannelFuture f = b.connect(host, port).sync();
            // 等待客户端连接关闭
            f.channel().closeFuture().sync();
        } finally {
            // 优雅退出,释放NIO线程组
            group.shutdownGracefully();
        }
    }
    
}

 同样的,定义一个TimeClientHandler:

 

public class TimeClientHandler extends ChannelHandlerAdapter {

    private static final Logger LOGGER = LoggerFactory.getLogger(TimeClientHandler.class);
    
    private final ByteBuf firstMessage;
    
    public TimeClientHandler() {
        byte[] req = "QUERY TIME ORDER".getBytes();
        firstMessage = Unpooled.buffer(req.length);
        firstMessage.writeBytes(req);
    }
    
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        ctx.writeAndFlush(firstMessage);
    }
    
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        ByteBuf buf = (ByteBuf)msg;
        byte[] req = new byte[buf.readableBytes()];
        buf.readBytes(req);
        
        String body = new String(req, "UTF-8");
        System.out.println("Now is:" + body);
    }
    
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        LOGGER.warn("Unexcepted exception from downstream:" + cause.getMessage());
        ctx.close();
    }
    
}

客户端的操作为打印来自服务端的数据,这样,整个Netty Demo代码就写完了,结构比较清楚,都是一个Server+一个Handler的模式,Handler用于处理读取到的信息。

public class CoreTest {

    @Test
    public void timeServerTest() throws Exception {
        new TimeServer().bind(8080);
    }
    
    @Test
    public void timeClientTest() throws Exception {
        new TimeClient().connect(8080, "127.0.0.1");
    }
    
}

运行Demo

上面写完了Demo,接着写一下测试代码,很简单,分别运行bind方法和connect方法即可:

 

public class CoreTest {

    @Test
    public void timeServerTest() throws Exception {
        new TimeServer().bind(8080);
    }
    
    @Test
    public void timeClientTest() throws Exception {
        new TimeClient().connect(8080, "127.0.0.1");
    }
    
}

先运行timeServerTest让服务端先启动,再运行timeClientServer让客户端后启动,运行结果服务端的打印为:

The time server receive order:QUERY TIME ORDER

结合代码可以看到,服务端读取到了来自客户端的数据,数据内容为"QUERY TIME ORDER",接着服务端取自己的时间,传输给客户端,看一下客户端的打印:

Now is:Thu Apr 05 21:07:39 CST 2018

打印了来自服务端的时间,这样,利用Netty进行服务端+客户端的相互通信的Demo完成,有了这个Demo,对Netty有了感性上的认识,接着我们一点一点深入去学习Netty。

 ==================================================================================

我不能保证写的每个地方都是对的,每一行代码都经过了认真的推敲、仔细的斟酌。每一篇文章的背后,希望都能看到自己对于技术、对于生活的态度。

我相信乔布斯说的,只有那些疯狂到认为自己可以改变世界的人才能真正地改变世界。面对压力,我可以挑灯夜战、不眠不休;面对困难,我愿意迎难而上、永不退缩。

其实我想说的是,我只是一个程序员,这就是我现在纯粹人生的全部。

更多经历移步博客园:五月的仓颉
==================================================================================

 

另外补充两个自己项目种实际使用到的示例;

1:普通的 tcp-server。

2:基于标准的TLS1.2 双向验证的 tcp-server端启动初始化类

1:普通的 tcp-server:

/* *
 * Netty 服务端代码
 * @title SH
 * @project
 * @note  普通的tcp-server 启动初始化类
 * @author alex
 * @Date 2018/7/5
 */
@Component("tcpServer")
public class TcpServer extends Thread {

	Logger logger = LoggerFactory.getLogger(this.getClass());
	private final int MAX_LENGTH = 1024;
	@Autowired
	private VehMessageHandler vehMessageHandler;
	/**
	 * netty服务端启动的入口
	 *
	 * @author alex
	 */
	public void run() {

		// 配置服务端的NIO线程组
		EventLoopGroup bossGroup = new NioEventLoopGroup();
		EventLoopGroup workerGroup = new NioEventLoopGroup();
		try {
			ServerBootstrap b = new ServerBootstrap();
			/**
			 * 最大连接数量设定 BACKLOG用于构造服务端套接字ServerSocket对象,标识当服务器请求处理线程全满时,
			 * 用于临时存放已完成三次握手的请求的队列的最大长度。如果未设置或所设置的值小于1,Java将使用默认值50。
			 */
			b.group(bossGroup, workerGroup)
					.channel(NioServerSocketChannel.class)
					.option(ChannelOption.SO_BACKLOG, 1024)
					.childOption(ChannelOption.SO_KEEPALIVE, true)
					// 通过NoDelay禁用Nagle,使消息立即发出去,不用等待到一定的数据量才发出去
					.childOption(ChannelOption.TCP_NODELAY, true)
					.childHandler(new ChildChannelHandler());
			//绑定端口,等待同步成功
			ChannelFuture f = b.bind(NettyConfig.nettyPort).sync();
			logger.info("服务监听启用成功,监听端口为:" + NettyConfig.nettyPort);
			f.channel().closeFuture().sync();
		} catch (Exception e) {
			logger.error("启动netty服务失败,请排查原因", e);
			System.exit(1);
		} finally {
			bossGroup.shutdownGracefully();
			workerGroup.shutdownGracefully();
		}
	}

	/* *
	 * 一个完整的业务可能会被TCP拆分成多个包进行发送(拆包),也有可能把多个小的包封装成一个大的数据包发送(粘包),
			这个就是TCP的拆包和封包问题。
	 */
	class ChildChannelHandler extends ChannelInitializer {

		@Override
		protected void initChannel(SocketChannel channel) throws Exception {
			ChannelPipeline channelPipeline = channel.pipeline();
			/**
			 * @Deprecated
			 * 一。解决掉粘包、断包问题(GB23960 未把(0x23,0x23)标识符进行“转义处理”(body中),不能简单采用分隔符处理)。另:evready可以采用
			 * 式1:分隔符,按照guobiao消息中自定义的分隔符“0x23,0x23“,将消息报文分别拆开发送;
			 * 方式2:消息定长,报文大小固定长度,不够空格补全,发送和接收方遵循相同的约定, 这样即使粘包了通过接收方编程实现获取定长报文也能区分。
			 */
//			byte[] splitByte = new byte[]{0x23,0x23};
//			ByteBuf delimiterStart = Unpooled.copiedBuffer(splitByte);
//			channel.pipeline().addLast("framedecoder",new DelimiterBasedFrameDecoder(MAX_LENGTH, false,
//					delimiterStart));
			//方式2: 不需要去除分隔符,消息最大只能是1024
			/*channel.pipeline().addLast( new DelimiterBasedFrameDecoder(1024, false, delimiter));*/
			//这里需要加上把ByteBuf转换为Package对象的handler
			// channel.pipeline().addLast(new StringDecoder());
			// readIdle、writeIdle超时忽略,只启用一个AllIdle,这里设置为2min
			/**
			 * 一。GB23960消息处理:采取根据body中数据数据单元长度截取长度 标识“整包数据”
			 */
			channelPipeline.addLast(new StringEncoder(Charset.forName("UTF-8")));
			channelPipeline.addLast(new GBDelimiterMsgDecoder());
			channelPipeline.addLast(new IdleStateHandler(0, 0,NettyConfig.timeOut,TimeUnit.SECONDS));
//			channelPipeline.addLast("readTimeOut",new ReadTimeoutHandler(10));
			channelPipeline.addLast("vehMessageHandler", vehMessageHandler);
		}

	}
}

2:基于标准的TLS1.2 双向验证的 tcp-server端

/* *
 * Netty 服务端代码
 * @title  SH
 * @project
 * @note  基于标准的TLS1.2 双向验证的 tcp-server启动初始化类
 * @author alex
 * @Date 2018/7/5
 */
@Component("tcpTlsServer")
public class TcpTlsServer extends Thread {

	Logger logger = LoggerFactory.getLogger(this.getClass());
	private final int MAX_LENGTH = 1024;
	@Autowired
	private VehMessageHandler vehMessageHandler;
//	@Autowired
//	private SocketSSLHandler socketSSLHandler;
	/**
	 * netty服务端启动的入口
	 *
	 * @author alex
	 */
	public void run() {


		// 配置服务端的NIO线程组
		EventLoopGroup bossGroup = new NioEventLoopGroup();
		EventLoopGroup workerGroup = new NioEventLoopGroup();
		try {
			ServerBootstrap b = new ServerBootstrap();
			/**
			 * 最大连接数量设定 BACKLOG用于构造服务端套接字ServerSocket对象,标识当服务器请求处理线程全满时,
			 * 用于临时存放已完成三次握手的请求的队列的最大长度。如果未设置或所设置的值小于1,Java将使用默认值50。
			 */
			b.group(bossGroup, workerGroup)
					.channel(NioServerSocketChannel.class)
					.option(ChannelOption.SO_BACKLOG, 1024)
					.childOption(ChannelOption.SO_KEEPALIVE, true)
					// 通过NoDelay禁用Nagle,使消息立即发出去,不用等待到一定的数据量才发出去
					.childOption(ChannelOption.TCP_NODELAY, true)
					.childHandler(new ChildChannelHandler());
			//绑定端口,等待同步成功
			ChannelFuture f = b.bind(NettyConfig.nettyPort).sync();
			logger.info("服务监听启用成功,监听端口为:" + NettyConfig.nettyPort);
			f.channel().closeFuture().sync();
		} catch (Exception e) {
			logger.error("启动netty服务失败,请排查原因", e);
			System.exit(1);
		} finally {
			bossGroup.shutdownGracefully();
			workerGroup.shutdownGracefully();
		}
	}

	/* *
	 * 一个完整的业务可能会被TCP拆分成多个包进行发送(拆包),也有可能把多个小的包封装成一个大的数据包发送(粘包),
			这个就是TCP的拆包和封包问题。
	 */
	class ChildChannelHandler extends ChannelInitializer {

		@Override
		protected void initChannel(SocketChannel channel) throws Exception {
			/**
			 * 解决掉粘包、断包问题
			 * 方式1:分隔符,按照guobiao消息中自定义的分隔符“0x23,0x23“,将消息报文分别拆开发送;
			 * 方式2:消息定长,报文大小固定长度,不够空格补全,发送和接收方遵循相同的约定, 这样即使粘包了通过接收方编程实现获取定长报文也能区分。
			 */

			byte[] splitByte = new byte[]{0x23,0x23};
			ByteBuf delimiterStart = Unpooled.copiedBuffer(splitByte);

//			SSLEngine engine = SSLContextFactory.getSSLContext().createSSLEngine();
			SSLEngine engine = SSLContextFactory.getSslContext().newEngine(channel.alloc());
//			SSLEngine engine = SecureChatSslContextFactory.getServerContext().createSSLEngine();//客户端ca读取jks
			engine.setUseClientMode(false);
			engine.setNeedClientAuth(true); //需要客户端认证,默认为false
			channel.pipeline().addLast("ssl",new SslHandler(engine));//不能添加true 否则偶尔出现unknow data未加密(Wireshark 抓包发现)
			channel.pipeline().addLast(new StringEncoder(Charset.forName("UTF-8")));
			channel.pipeline().addLast(new GBDelimiterMsgDecoder());
			/*channel.pipeline().addLast("framedecoder",new DelimiterBasedFrameDecoder(MAX_LENGTH, false,
					delimiterStart));*/
			//方式2: 不需要去除分隔符,消息最大只能是1024
			/*channel.pipeline().addLast( new DelimiterBasedFrameDecoder(1024, false, delimiter));*/

			/**
			 * 2: 这里需要加上把ByteBuf转换为Package对象的handler
			 */
			// channel.pipeline().addLast(new StringDecoder());
			// readIdle、writeIdle超时忽略,只启用一个AllIdle,这里设置为2min
			channel.pipeline().addLast(new IdleStateHandler(0, 0,NettyConfig.timeOut,TimeUnit.SECONDS));
			channel.pipeline().addLast("vehMessageHandler", vehMessageHandler);
		}

	}
}

 

你可能感兴趣的:(网络(socket)编程,netty)