• Netty入门系列(1) --使用Netty搭建服务端和客户端


    引言

    前面我们介绍了网络一些基本的概念,虽然说这些很难吧,但是至少要做到理解吧。有了之前的基础,我们来正式揭开Netty这神秘的面纱就会简单很多。

    服务端

    public class PrintServer {
    
        public void bind(int port) throws Exception {
            EventLoopGroup bossGroup = new NioEventLoopGroup();						//1
            EventLoopGroup workerGroup = new NioEventLoopGroup();					//2
            try {
                ServerBootstrap b = new ServerBootstrap();							//3
                b.group(bossGroup, workerGroup)										//4											
                        .channel(NioServerSocketChannel.class)						//5
                        .option(ChannelOption.SO_BACKLOG, 1024)						//6
                        .childHandler(new ChannelInitializer<SocketChannel>() {		//7
                            @Override
                            protected void initChannel(SocketChannel ch) throws Exception {
                                ch.pipeline().addLast(new PrintServerHandler());
                            }
                        });
    
                ChannelFuture f = b.bind(port).sync();				//8
                
                f.channel().closeFuture().sync();					//9
            } finally {
                // 优雅退出,释放线程池资源
                bossGroup.shutdownGracefully();
                workerGroup.shutdownGracefully();
            }
        }
    
    
        /**
         * @param args
         * @throws Exception
         */
        public static void main(String[] args) throws Exception {
            int port = 8080;
            new TimeServer().bind(port);
        }
    }
    

    我们来分析一下上面的这段代码(下面的每一点对应上面的注释)

    1~2:首先我们创建了两个NioEventLoopGroup实例,它是一个由Netty封装好的包含NIO的线程组。为什么创建两个?我想经过前面的学习大家应该都清楚了。对,因为Netty的底层是IO多路复用,bossGroup 是用于接收客户端的连接,原理就是一个实现的Selector的Reactor线程。而workerGroup用于进行SocketChannel的网络读写。

    3:创建一个ServerBootstrap对象,可以把它想象成Netty的入口,通过这类来启动Netty,将所需要的参数传递到该类当中,大大降低了的开发难度。

    4:将两个NioEventLoopGroup实例绑定到ServerBootstrap对象中。

    5:创建Channel(典型的channel有NioSocketChannel,NioServerSocketChannel,OioSocketChannel,OioServerSocketChannel,EpollSocketChannel,EpollServerSocketChannel),这里创建的是NIOserverSocketChannel,它的功能可以理解为当接受到客户端的连接请求的时候,完成TCP三次握手,TCP物理链路建立成功。并将该“通道”与workerGroup线程组的某个线程相关联。

    6:设置参数,这里设置的SO_BACKLOG,意思是客户端连接等待队列的长度为1024.

    7:建立连接后的具体Handler。就是我们接受数据后的具体操作,例如:记录日志,对信息解码编码等。

    8:绑定端口,同步等待成功

    9:等待服务端监听端口关闭

    绑定该服务端的Handler

    public class PrintServerHandler extends ChannelHandlerAdapter {
    
        @Override
        public void channelRead(ChannelHandlerContext ctx, Object msg)
    	    throws Exception {
    	ByteBuf buf = (ByteBuf) msg;										//1
    	byte[] req = new byte[buf.readableBytes()];	
    	buf.readBytes(req); //将缓存区的字节数组复制到新建的req数组中
    	String body = new String(req, "UTF-8");
    	System.out.println(body);
    	String response= "打印成功";
    	ByteBuf resp = Unpooled.copiedBuffer(response.getBytes());						
    	ctx.write(resp);													//2
        }	
    
        @Override
        public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
    	ctx.flush();														//3
        }
    
    
        @Override
        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
    	ctx.close();
        }
    }
    

    PrintServerHandler 继承 ChannelHandlerAdapter ,在这里它的功能为 打印客户端发来的数据并且返回客户端打印成功。

    我们只需要实现channelRead,exceptionCaught,前一个为接受消息具体逻辑的实现,后一个为发生异常后的具体逻辑实现。

    1:我们可以看到,接受的消息被封装为了Object ,我们将其转换为ByteBuf ,前一章的讲解中也说明了该类的作用。我们需要读取的数据就在该缓存类中。

    2~3:我们将写好的数据封装到ByteBuf中,然后通过write方法写回到客户端,这里的3调用flush方法的作用为,防止频繁的发送数据,write方法并不直接将数据写入SocketChannel中,而是把待发送的数据放到发送缓存数组中,再调用flush方法发送数据。

    客户端

    public class PrintClient {
    
        public void connect(int port, String host) throws Exception {
    	EventLoopGroup group = new NioEventLoopGroup();                 //1
    	try {
    	    Bootstrap b = new Bootstrap();                              //2
    	     b.group(group)                                             //3
    	        .channel(NioSocketChannel.class)                        //4
    		    .option(ChannelOption.TCP_NODELAY, true)                //5
    		    .handler(new ChannelInitializer<SocketChannel>() {      //6
    			@Override
    			public void initChannel(SocketChannel ch)               
    				throws Exception {
    			    ch.pipeline().addLast(new PrintClientHandler());
    			}
    		    });
    
    	    ChannelFuture f = b.connect(host, port).sync();             //7
    	    f.channel().closeFuture().sync();                           //8
    	} finally {
    	    // 优雅退出,释放NIO线程组
    	    group.shutdownGracefully();
    	}
        }
    
        /**
         * @param args
         * @throws Exception
         */
        public static void main(String[] args) throws Exception {
    	int port = 8080;
    	new TimeClient().connect(port, "127.0.0.1");
        }
    }
    

    我们继续来分析一下上面的这段代码(下面的每一点对应上面的注释)

    1:区别于服务端,我们在客户端只创建了一个NioEventLoopGroup实例,因为客户端你并不需要使用I/O多路复用模型,需要有一个Reactor来接受请求。只需要单纯的读写数据即可

    2:区别于服务端,我们在客户端只需要创建一个Bootstrap对象,它是客户端辅助启动类,功能类似于ServerBootstrap。

    3:将NioEventLoopGroup实例绑定到Bootstrap对象中。

    4:创建Channel(典型的channel有NioSocketChannel,NioServerSocketChannel,OioSocketChannel,OioServerSocketChannel,EpollSocketChannel,EpollServerSocketChannel),区别与服务端,这里创建的是NIOSocketChannel.

    5:设置参数,这里设置的TCP_NODELAY为true,意思是关闭延迟发送,一有消息就立即发送,默认为false。

    6:建立连接后的具体Handler。注意这里区别与服务端,使用的是handler()而不是childHandler()。handler和childHandler的区别在于,handler是接受或发送之前的执行器;childHandler为建立连接之后的执行器。

    7:发起异步连接操作

    8:当代客户端链路关闭

    绑定该客户端的Handler

    public class PrintClientHandler extends ChannelHandlerAdapter {
    
        private static final Logger logger = Logger
    	    .getLogger(TimeClientHandler.class.getName());
    
        private final ByteBuf firstMessage;
    
        /**
         * Creates a client-side handler.
         */
        public TimeClientHandler() {
    	byte[] req = "你好服务端".getBytes();
    	firstMessage = Unpooled.buffer(req.length);                                 //1
    	firstMessage.writeBytes(req);
    
        }
    
        @Override
        public void channelActive(ChannelHandlerContext ctx) {
    	ctx.writeAndFlush(firstMessage);                                            //2             
        }
    
        @Override
        public void channelRead(ChannelHandlerContext ctx, Object msg)              //3
    	    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("服务端回应消息 : " + body);
        }
    
        @Override
        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {   //4
    	// 释放资源
    	System.out.println("Unexpected exception from downstream : "
    		+ cause.getMessage());
    	ctx.close();
        }
    }
    

    PrintClientHandler 继承 ChannelHandlerAdapter ,在这里它的功能为 发送数据并打印服务端发来的数据。

    我们只需要实现channelActive,channelRead,exceptionCaught,第一个为建立连接后立即执行,后两个与一个为接受消息具体逻辑的实现,另一个为发生异常后的具体逻辑实现。

    1:将发送的信息封装到ByteBuf中。

    2:发送消息。

    3:接受客户端的消息并打印

    4:发生异常时,打印异常信息,释放客户端资源

    总结

    这是一个入门程序,对应前面所讲的I/O多路复用模型以及NIO的特性,能很有效的理解该模式的编程方式。如果这几段代码看着很费劲,那么可以看看之前博主的Netty基础系列。

    如果博主哪里说得有问题,希望大家提出来,一起进步~

  • 相关阅读:
    Linux内核中的信号机制--一个简单的例子【转】
    国际C语言混乱代码大赛代码赏析(一)【转】
    宏内核与微内核【转】
    Linux内核USB驱动【转】
    USB驱动开发大全【转】
    Linux驱动程序学习【转】
    GPIO口及中断API函数【转】
    Linux的fasync驱动异步通知详解【转】
    request_irq() | 注册中断服务函数【转】
    混杂设备动态次设备号分析【转】
  • 原文地址:https://www.cnblogs.com/zhxiansheng/p/10830828.html
Copyright © 2020-2023  润新知