Netty4.X学习(一) 入门级Demo--简单C/S端与注意事项及常用方法

这次了解Netty是想要尝试推送服务以及应对长连接的处理,由于Netty处理高并发NIO效率高,故先进行学习;方法是:先进行Demo操作,排除进展中的错误,再对问题进行分析,之后接着参考源码及他人经验去理解~

目录

本篇幅环境:

Customer端

初始化通道类

业务逻辑类

客户端启动类  

Server端  

初始化通道类

业务逻辑类

服务端启动类 

公共处理类(可以不写,这个主要用来说明问题)

内容解析

1、SimpleChannelInboundHandler &  ChannelInboundHandlerAdapter 区别

2、关于启动类中 ServerBootstrap作用的解析

3、引导程序NioServerSocketChannel & NioSocketChannel 的差异

4、ServerBootstrap 中 childHandler 的作用

Boostrap 下option() 、childOption() 参数目录

SocketChannel 参数目录

DatagramChannel参数 (适合并发的信道)

5、ChannelInboundHandlerAdapter和ChannelOutboundHandlerAdapter 差异

ChannelInboundHandler

ChannelOutboundHandler


本篇幅环境:

linux:centos 7.0    IP:192.168.88.132

java: 1.8.0.152

tomcat 7

Eclipse:Mars.2 Release (4.5.2)

简单的C/S聊天(客户端发送,服务端固定回应)

注意 在netty里,进出的都是ByteBuf

Customer端

初始化通道类

ClientChannelInitializer.java    (主要是用于初始化客户端的逻辑与编/解码,通道是无法直接发信息的,ctx.writeAndFlush是不能直接写串类型的,需要编码器)

/**
 * 客户端Channel通道初始化设置
 */
public class ClientChannelInitializer extends ChannelInitializer<SocketChannel> {
    @Override
    protected void initChannel(SocketChannel socketChannel) throws Exception {
        ChannelPipeline pipeline = socketChannel.pipeline();
       //这里引用默认的编码解码,默认是UTF-8的
        pipeline.addLast("decoder", new StringDecoder());
        pipeline.addLast("encoder", new StringEncoder());
        //客户端的逻辑
        pipeline.addLast("handler", new DemoClientHandler());
     
    }
    
}

业务逻辑类

  DemoClientHandler.java

  (这里可以填写你所需的业务逻辑处理返回数据)

/**
 * 客户端业务逻辑
 */
public class DemoClientHandler extends SimpleChannelInboundHandler<Object> {
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
        System.out.println("Server say : " + msg.toString());
    }
    @Override  //处理异常用
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {  
         System.out.println("exception is general");  
     }


 }

 

 

客户端启动类  

DemoClient.java  (用于启动客户端)

/**
 * 客户端启动逻辑
 */
public class DemoClient {
	 public static String host = "192.168.88.132"; //服务器IP地址
	    public static int port = 8000; //服务器端口

	    public static void main(String[] args) throws Exception {
	        EventLoopGroup group = new NioEventLoopGroup();
	        try {
	            Bootstrap b = new Bootstrap();
	            b.group(group)
	                    .channel(NioSocketChannel.class)       
	                    .handler(new ClientChannelInitializer());

	            //连接客户端
	            Channel channel = b.connect(host, port).sync().channel();
	            
	            //控制台输入
	            BufferedReader in = new BufferedReader(new InputStreamReader(System.in));

	            for (;;) {
	                String line = in.readLine();
	                if (line == null) {
	                    continue;
	                }else if(line.equals("close") || line.equals("关闭")){
	                	channel.disconnect();
	                	return;
	                }
	                //向服务端发送数据
	                channel.writeAndFlush(line);
	            }
	        } finally {
	            //优雅退出,释放线程池资源
	            group.shutdownGracefully();
	        }
	    }
	}

Server端  

初始化通道类

ServerChannelInitializer.java

/**
 * 服务器Channel通道初始化设置
 */
public class ServerChannelInitializer extends ChannelInitializer<SocketChannel> {
    @Override
    protected void initChannel(SocketChannel socketChannel) throws Exception {
        ChannelPipeline pipeline = socketChannel.pipeline();
        //字符串解码和编码
        pipeline.addLast("decoder", new StringDecoder());
        pipeline.addLast("encoder", new StringEncoder());
        //服务器的逻辑
        //添加一个Hanlder用来处理各种Channel状态
        pipeline.addLast("handlerIn", new DemoClientHandler());
        //添加一个Handler用来接收监听IO操作的  
        pipeline.addLast("handlerOut", new OutHandler()); //公共处理类说明,可以不写

    }
}


业务逻辑类

 DemoServerHandler.java

/**
 * 服务器业务逻辑
 */
public class DemoServerHandler extends SimpleChannelInboundHandler<Object> {
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
    	System.out.println("Client :"+ctx.channel().remoteAddress() +" say : " + msg.toString());
        String message = "";
        message =  msg.toString();
        //返回客户端消息 - 我已经接收到了你的消息
        if(message.equals("fuck")){
        	message ="请文明用语";
        }
        ctx.writeAndFlush("Received your message : " + message);
//        channelFuture.addListener(ChannelFutureListener.CLOSE);
    }
    
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        System.out.println("RemoteAddress : " + ctx.channel().remoteAddress() + " active !");
        ctx.writeAndFlush("连接成功!");
        super.channelActive(ctx);
    }
    
    @Override  //处理异常用
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {  
         System.out.println("exception is general");  
     }

}

服务端启动类 

DemoServer.java

/**
 * 服务器启动逻辑
 */
public class DemoServer {
    public static void main(String[] args) throws Exception {
        int port = 8000;
        if (args != null && args.length > 0) {
            try {
                port = Integer.valueOf(args[0]);
            } catch (NumberFormatException e) {
                //采用默认值
            }
        }
        new DemoServer().bind(port);
    }

    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)
                    .handler(new OutHandler())  //公共处理类说明用,可以不写
                    .childHandler(new ServerChannelInitializer());

            //绑定端口,同步等待成功
            ChannelFuture f = b.bind(port).sync();

            Channel channel = f.channel();
            
            //等待服务器监听端口关闭
            f.channel().closeFuture().sync();
            
        } finally {
            //优雅退出,释放线程池资源
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}

公共处理类(可以不写,这个主要用来说明问题)

/**
* @ClassName: OutHandler
* @Description: 公共处理类
* @author linge
* @date 2018年1月8日 下午3:48:40
*
*/
public class OutHandler extends ChannelOutboundHandlerAdapter{  
    @Override  
    public void connect(ChannelHandlerContext ctx,  
            SocketAddress remoteAddress, SocketAddress localAddress,  
            ChannelPromise promise) throws Exception {  
        // TODO Auto-generated method stub  
        super.connect(ctx, remoteAddress, localAddress, promise);  
        System.out.println("<<<<<<<<<<<<<<< connect server success >>>>>>>>>>>>>>>>");  
    }  
  
    @Override  
    public void bind(ChannelHandlerContext ctx,  
            SocketAddress localAddress, ChannelPromise promise)  
            throws Exception {  
        // TODO Auto-generated method stub  
        super.bind(ctx, localAddress, promise);  
        System.out.println("<<<<<<<<<<<<<<< server bind success >>>>>>>>>>>>>>>>");  
    }  
} 

如果想进一步主动发信息给客户端,我之后会在文章之后附带github的链接,给你们实例。这个仅仅是一个入门级简例,用于理解基本的C/S通信;
 

内容解析

看完第一个Demo后,有几个注意点

         官网api : 官网API传送门

1、SimpleChannelInboundHandler &  ChannelInboundHandlerAdapter 区别

     一般用netty来发送和接收数据都会继承SimpleChannelInboundHandlerChannelInboundHandlerAdapter这两个抽象类,那么这两个到底有什么区别呢?

      其实用这两个抽象类是有讲究的,在客户端的业务Handler继承的是SimpleChannelInboundHandler,而在服务器端继承的是ChannelInboundHandlerAdapter。

最主要的区别就是SimpleChannelInboundHandler在接收到数据后会自动release掉数据占用的Bytebuffer资源(自动调用Bytebuffer.release())。

而为何服务器端不能用呢,因为我们想让服务器把客户端请求的数据发送回去,而服务器端有可能在channelRead方法返回前还没有写完数据,因此不能让它自动release。

2、关于启动类中 ServerBootstrap作用的解析

   关于启动类中的ServerBootstrap()方法

       观看  .group() 可知,这里如果调用时,如果只传入了一个EventLoopGroup,最后也会调用group(EventLoopGroup parentGroup, EventLoopGroup childGroup)。
这里传入的两个EventLoopGroup分别叫做parentGroup和childGroup。

其实我觉得更加好理解的方式应该叫boss和worker。

boss这个EventLoopGroup作为一个acceptor负责接收来自客户端的请求,然后分发给worker这个EventLoopGroup来处理所有的事件event和channel的IO。

     由父类AbstractBootstrap()重载方法group(EventLoopGroup parentGroup, EventLoopGroup childGroup)的super.(parentGroup)可得

   

   /**
     * The {@link EventLoopGroup} which is used to handle all the events for the to-be-created
     * {@link Channel}
     */
    public B group(EventLoopGroup group) {
        if (group == null) {
            throw new NullPointerException("group");
        }
        if (this.group != null) {
            throw new IllegalStateException("group set already");
        }
        this.group = group;
        return self();
    }


  为单例;  详情移步分析:Netty的引导程序ServerBootstrap

3、引导程序NioServerSocketChannel & NioSocketChannel 的差异

启动类中的引导程序所设置的信道差异

       NioServerSocketChannel是给server用的,程序由始至终只有一个NioServerSocketChannel

       NioSocketChannel是给客户端用的,每个连接生成一个NioSocketChannel 对象

最后NioServerSocketChannel中的doReadMessages()方法重载也会帮我们将每一个套接字请求转化为NioSocketChannel()对象进行处理

   protected int doReadMessages(List<Object> buf) throws Exception {
        SocketChannel ch = SocketUtils.accept(javaChannel());

        try {
            if (ch != null) {
                buf.add(new NioSocketChannel(this, ch));
                return 1;
            }
        } catch (Throwable t) {
            logger.warn("Failed to create a new channel from an accepted socket.", t);

            try {
                ch.close();
            } catch (Throwable t2) {
                logger.warn("Failed to close a socket.", t2);
            }
        }

        return 0;
    }

4、ServerBootstrap 中 childHandler 的作用

Bootstrap的handler和childHandler 方法说明(基于注意点⑤的说明)

     在服务端的ServerBootstrap中增加了一个方法childHandler,它的目的是添加handler,用来监听已经连接的客户端的Channel的动作和状态。

handler在初始化时就会执行,而childHandler会在客户端成功connect后才执行,这是两者的区别。

      客户端需要加入的话也可以 在初始化处理类中加入监听,如上代码

        //添加一个Handler用来接收监听IO操作的  
        pipeline.addLast("handlerOut", new OutHandler()); //公共处理类说明,可以不写

  Tips: pipeline是伴随Channel的存在而存在的,交互信息通过它进行传递,我们可以addLast(或者addFirst)多个handler,第一个参数是名字,无具体要求,如果填写nul,系统会自动命名。

Boostrap 下option() 、childOption() 参数目录

io.netty.bootstrap.Bootstrap

 

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

参数名          备注
CONNECT_TIMEOUT_MILLIS连接超时毫秒数,默认值30000毫秒即30秒

MAX_MESSAGES_PER_READ

一次Loop读取的最大消息数,对于ServerChannel或者NioByteChannel,默认值为16,其他Channel默认值为1。

默认值这样设置,是因为:ServerChannel需要接受足够多的连接,保证大吞吐量,NioByteChannel可以减少不必要的系统调用select

WRITE_SPIN_COUNT

一个Loop写操作执行的最大次数,默认值为16。

对于大数据量的写操作至多进行16次,如果16次仍没有全部写完数据,此时会提交一个新的写任务给EventLoop,任务将在下次调度继续执行。

这样,其他的写请求才能被响应不会因为单个大数据量写请求而耽误

ALLOCATOR

ByteBuf的分配器,默认值为ByteBufAllocator.DEFAULT,4.0版本为UnpooledByteBufAllocator,4.1版本为PooledByteBufAllocator。

该值也可以使用系统参数io.netty.allocator.type配置,可使用字符串值:"unpooled","pooled"

RCVBUF_ALLOCATOR

用于Channel分配接受Buffer的分配器,默认值为AdaptiveRecvByteBufAllocator.DEFAULT,是一个自适应的接受缓冲区分配器,能根据接受到的数据自动调节大小。

可选值为FixedRecvByteBufAllocator,固定大小的接受缓冲区分配器。

AUTO_READ

自动读取,默认值为True。

Netty只在必要的时候才设置关心相应的I/O事件。

对于读操作,需要调用channel.read()设置关心的I/O事件为OP_READ,这样若有数据到达才能读取以供用户处理。

         该值为True时,每次读操作完毕后会自动调用channel.read(),从而有数据到达便能读取;

         否则,需要用户手动调用channel.read()。

         需要注意的是:当调用config.setAutoRead(boolean)方法时,如果状态由false变为true,将会调用channel.read()方法读取数据;由true变为false,将调用config.autoReadCleared()方法终止数据读取。

WRITE_BUFFER_HIGH_WATER_MARK

写高水位标记,默认值64KB。

如果Netty的写缓冲区中的字节超过该值,Channel的isWritable()返回False。

WRITE_BUFFER_LOW_WATER_MARK

写低水位标记,默认值32KB。

当Netty的写缓冲区中的字节超过高水位之后若下降到低水位,则Channel的isWritable()返回True。

写高低水位标记使用户可以控制写入数据速度,从而实现流量控制。推荐做法是:每次调用channl.write(msg)方法首先调用channel.isWritable()判断是否可写。

MESSAGE_SIZE_ESTIMATOR

消息大小估算器,默认为DefaultMessageSizeEstimator.DEFAULT。估算ByteBuf、ByteBufHolder和FileRegion的大小,其中ByteBuf和ByteBufHolder为实际大小,FileRegion估算值为0。

该值估算的字节数在计算水位时使用,FileRegion为0可知FileRegion不影响高低水位。

SINGLE_EVENTEXECUTOR_PER_GROUP

单线程执行ChannelPipeline中的事件,默认值为True。该值控制执行ChannelPipeline中执行ChannelHandler的线程。

       如果为True,整个pipeline由一个线程执行,这样不需要进行线程切换以及线程同步,是Netty4的推荐做法;

       如果为False,ChannelHandler中的处理过程会由Group中的不同线程执行。

SocketChannel 参数目录

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

io.netty.channel.socket.SocketChannel

属性名 备注
SO_RCVBUF

TCP数据接收缓冲区大小。

该缓冲区即TCP接收滑动窗口,linux操作系统可使用命令:cat /proc/sys/net/ipv4/tcp_rmem查询其大小。

一般情况下,该值可由用户在任意时刻设置,但当设置值超过64KB时,需要在连接到远端之前设置

SO_SNDBUF

TCP数据发送缓冲区大小。

该缓冲区即TCP发送滑动窗口,linux操作系统可使用命令:cat /proc/sys/net/ipv4/tcp_smem查询其大小。

TCP_NODELAY

立即发送数据,默认值为Ture(Netty默认为True而操作系统默认为False)

该值设置Nagle算法的启用,改算法将小的碎片数据连接成更大的报文来最小化所发送的报文的数量,如果需要发送一些较小的报文,则需要禁用该算法。Netty默认禁用该算法,从而最小化报文传输延时。

SO_KEEPALIVE

连接保活,默认值为False.

启用该功能时,TCP会主动探测空闲连接的有效性。可以将此功能视为TCP的心跳机制,需要注意的是:默认的心跳间隔是7200s即2小时。Netty默认关闭该功能.

SO_REUSEADDR

地址复用,默认值False。

有四种情况可以使用:

(1).当有一个有相同本地地址和端口的socket1处于TIME_WAIT状态时,而你希望启动的程序的socket2要占用该地址和端口,比如重启服务且保持先前端口。(

2).有多块网卡或用IP Alias技术的机器在同一端口启动多个进程,但每个进程绑定的本地IP地址不能相同。

(3).单个进程绑定相同的端口到多个socket上,但每个socket绑定的ip地址不同。

(4).完全相同的地址和端口的重复绑定。但这只用于UDP的多播,不用于TCP。

SO_LINGER

关闭Socket的延迟时间,默认值为-1,表示禁用该功能。

-1表示socket.close()方法立即返回,但OS底层会将发送缓冲区全部发送到对端。

0表示socket.close()方法立即返回,OS放弃发送缓冲区的数据直接向对端发送RST包,对端收到复位错误。

非0整数值表示调用socket.close()方法的线程被阻塞直到延迟时间到或发送缓冲区中的数据发送完毕,若超时,则对端会收到复位错误。

IP_TOS设置IP头部的Type-of-Service字段,用于描述IP包的优先级和QoS选项。
ALLOW_HALF_CLOSURE

一个连接的远端关闭时本地端是否关闭,默认值为False。

值为False时,连接自动关闭;

为True时,触发ChannelInboundHandler的userEventTriggered()方法,事件为ChannelInputShutdownEvent。

DatagramChannel参数 (适合并发的信道)

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

参数名备注
SO_BROADCAST

设置广播模式,默认true

SO_RCVBUF

TCP数据接收缓冲区大小。

该缓冲区即TCP接收滑动窗口,linux操作系统可使用命令:cat /proc/sys/net/ipv4/tcp_rmem查询其大小。

一般情况下,该值可由用户在任意时刻设置,但当设置值超过64KB时,需要在连接到远端之前设置

SO_SNDBUF

TCP数据发送缓冲区大小。

该缓冲区即TCP发送滑动窗口,linux操作系统可使用命令:cat /proc/sys/net/ipv4/tcp_smem查询其大小。

SO_REUSEADDR

地址复用,默认值False。

有四种情况可以使用:

(1).当有一个有相同本地地址和端口的socket1处于TIME_WAIT状态时,而你希望启动的程序的socket2要占用该地址和端口,比如重启服务且保持先前端口。(

2).有多块网卡或用IP Alias技术的机器在同一端口启动多个进程,但每个进程绑定的本地IP地址不能相同。

(3).单个进程绑定相同的端口到多个socket上,但每个socket绑定的ip地址不同。

(4).完全相同的地址和端口的重复绑定。但这只用于UDP的多播,不用于TCP。

IP_MULTICAST_LOOP_DISABLED

禁用本地回环接口的多播功能,默认为true

设置本地回环接口的多播功能。由于IP_MULTICAST_LOOP返回True表示关闭,所以Netty加上后缀_DISABLED防止歧义

IP_MULTICAST_ADDR 对应IP参数IP_MULTICAST_IF,设置对应地址的网卡为多播模式。
IP_MULTICAST_IF对应IP参数IP_MULTICAST_IF2,同上但支持IPV6。
IP_MULTICAST_TTLIP参数,多播数据报的time-to-live即存活时间数
DATAGRAM_CHANNEL_ACTIVE_ON_REGISTRATIONDatagramChannel注册的EventLoop即表示已激活。

5、ChannelInboundHandlerAdapter和ChannelOutboundHandlerAdapter 差异

根据名字的IN  与 Out可知,是对出入的操作,其实也是根据是否有数据或事件(event)来触发相应方法

          前者  -   处理输入的数据且处理一切状态的改变

          后者  -   处理输出数据,运行拦截一切的操作

这个channel的四种状态,我们从ChannelInboundHandler类中可以知道

ChannelUnregisteredChannel已经创建,但是还没有注册到EcentLoop上
ChannelRegisteredChannel已经注册到EventLoop
ChannelActiveChannel已经激活了(已经连接到远程端),现在它已经准备好接受和发送信息
ChannelInactiveChannel没有连接到远程端

父类

一般常用方法如下:

ChannelInboundHandler

类型描述
channelRegistered当一个Channel被注册到EventLoop上的时候并且能够处理IO的时候调用执行
channelUnregistered当一个Channel从EventLoop中注销的时候且不能再处理I/O的时候调用执行
channelActive当一个Channel被激活是调用执行
channelInactive当一个Channel已经处于非激活的状态且不再连接到远程端的时候被调用执行
channelReadComplete当一个Channel的读操作已经准备好的时候被调用执行
channelRead当数据已经从Channel读取的时候执行
channelWritabilityChanged当一个Channel的可写的状态发生改变的时候执行,用户可以保证写的操作不要太快,这样可以防止OOM,写的太快容易发生OOM,如果当发现Channel变得再次可写之后重新恢复写入的操作,Channel中的isWritable方法可以监控该channel的可写状态,可写状态的阀门直接通过Channel.config().setWriterHighWaterMark()和Channel.config().setWriteLowWaterMark()配置
userEventTriggered当ChannelInboundHandler的fireUserEventTriggered被调用的时候执行,因为一个POJO对象传输通过了ChannelPipeline,实际上userEventTriggered是调用fireUserEventTriggered()方法

ChannelOutboundHandler

类型描述
bind(ChannelHandlerContext,
SocketAddress,ChannelPromise)
绑定到本地的地址的channel的请求被执行
connect(ChannelHandlerContext,
SocketAddress,SocketAddress,ChannelPromise)
连接到远程端的channel的请求被执行
disconnect(ChannelHandlerContext,
ChannelPromise)
当从远程端停止连接的时候执行
close(ChannelHandlerContext,ChannelPromise)请求关闭channel的时候执行
deregister(ChannelHandlerContext,
ChannelPromise)
请求当channel从EventLoop上注销的时候执行
read(ChannelHandlerContext)请求从channel中读取更多信息的时候执行
flush(ChannelHandlerContext)当从channel刷入队列信息到远程端的时候执行
write(ChannelHandlerContext,Object,
ChannelPromise)
当从channel中写数据到远程端的时候执行

当然一般我们都会利用适配器模式的适配类来重载自己需要的方法

我的gitHub地址 :文中例子及其他应用简单案例

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值