netty轻松入门(二)—— 第一个netty应用

netty 专栏收录该内容
4 篇文章 0 订阅

看了上一篇netty轻松入门(一)——netty线程模型,应用对netty有个大致的了解。

现在我们就用netty实现一个简单的服务

需求很简单:

  1. 服务端起一个服务,监听1010端口
  2. 客户端连接,并发送一个hello server
  3. 服务端收到hello server后,响应一个hello client
  4. 服务端主动关闭连接

环境配置

  1. 首先你要安装JDK,具体步骤就不说了
  2. 安装maven或者gradle(我使用gradle)
  3. 创建工程,引入netty包

gradle:

compile group: 'io.netty', name: 'netty-all', version: '4.0.4.Final'

或maven:

<dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty-all</artifactId>
    <version>4.0.4.Final</version>
</dependency>

服务端

业务处理器handler

handler是业务处理器,是核心,我们先写一个handler,命名为:EchoServerHandler ,继承自 ChannelInboundHandlerAdapter

package com.netty.server;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandler.Sharable;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.util.CharsetUtil;

@Sharable
public class EchoServerHandler extends ChannelInboundHandlerAdapter {

    /**
     * 建立起连接时,该方法将被调用
     *
     * @param ctx
     * @throws Exception
     */
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        System.out.println("channelActive...");
    }

    /**
     * 收到消息时,该方法将被调用
     *
     * @param ctx
     * @param msg
     * @throws Exception
     */
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        ByteBuf byteBuf = (ByteBuf) msg;
        System.out.println(byteBuf.toString(CharsetUtil.UTF_8));
        ctx.write(Unpooled.copiedBuffer("hello client".toCharArray(), CharsetUtil.UTF_8));
    }

    /**
     * 消息接收完毕时,该方法将被调用
     *
     * @param ctx
     * @throws Exception
     */
    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        System.out.println("channelReadComplete");
        // 向客户端写一个空内容,然后关闭通道
        ctx.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE);
    }

    /**
     * 发生异常时,该方法将被调用
     *
     * @param ctx
     * @param cause
     * @throws Exception
     */
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
    }
}

看到上面一长串代码,你肯定有很多疑问,没关系,我们来一一解释。

问题一:handler为什么要继承ChannelInboundHandlerAdapter

实际上这里应该是实现ChannelInboundHandler 接口,该接口定义了响应事件的方法。但如果你直接去实现该接口,有很多个方法需要去实现,麻烦的很。

ChannelInboundHandlerAdapter 类是这个接口的实现类,提供了默认实现,为了方便,我们直接继承该类就可以省去很多麻烦。

然后我们再看上面代码中的注释,每一个方法将在什么时机被调用已经注释的很清楚了。

问题二: channelRead()channelReadComplete() 的区别

当客户端往channel里写了一堆数据,服务端在读取的时候,并不是一次全部读完,而是分多次读,而每一次读都会调用 channelRead() ,当全部读完的时候,将会调用 channelReadComplete() ,因此,值得一提的是, channelRead() 读到的数据可能并不是一个完整的信息。

问题三:write()和flush()的作用?

往channel中写数据时,我们常用 write()flush()writeAndFlush() ,那么它们有什么区别呢?

write() 很简单,就是往channel中写数据,但是此时channel的另一端并不会收到数据,只有当执行了 flush() 后,对方才会真正的接收到数据。writeAndFlush() 就是执行完 write 立马执行 flush

启动服务端

netty给我们提供了一个 ServerBootstrap 类来进行服务端的启动。这里的Bootstrap 并不是那个著名的前端css框架,它的英文本意是启动引导

上代码:

package com.netty.server;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;

public class Server {

    public static void main(String[] args) throws InterruptedException {
        int port = 1010;
        final EchoServerHandler echoHandler = new EchoServerHandler();
        EventLoopGroup boss = new NioEventLoopGroup();
        EventLoopGroup worker = new NioEventLoopGroup();
        try {
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            serverBootstrap.group(boss, worker)
                    .channel(NioServerSocketChannel.class)
                    .localAddress(port)
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) {
                            ch.pipeline().addLast(echoHandler);
                        }
                    });
            ChannelFuture f = serverBootstrap.bind().sync();
            System.out.println("wait channel close...");
            f.channel().closeFuture().sync();
            System.out.println("channel close...");
        } finally {
            boss.shutdownGracefully().sync();
            worker.shutdownGracefully().sync();
        }

    }
}

上面代码主要做了这么几件事:

  1. 创建handler
  2. 创建boss和worker两个EventLoopGroup,这里使用的是NioEventLoopGroup
  3. 创建ServerBootstrap,然后把boss,worker,handler,端口号都配置进来。
  4. 启动

问题一:ChannelInitializer是什么?

上面代码中,添加之前写的handler用的是ChannelInitializer类,然后ch.pipeline().addLast(echoHandler) ,ChannelInitializer是一个帮助我们初始化channel的类,可以看它源码的注释,有这句话:

A special {@link ChannelInboundHandler} which offers an easy way to initialize a {@link Channel} once it was registered to its {@link EventLoop}.

大致意思是:一个特别的ChannelInboundHandler,当它注册到EventLoop时,提供了一个简单的方式进行初始化channel

很明确了,ChannelInitializer就是一个用于初始化channel的类。

问题二:channel().closeFuture().sync()什么作用?

这行代码看起来像是关闭channel,但是其实不是。当你代码运行起来时,你会发现代码阻塞在这一行。

那么closeFuture到底是做什么的呢,我们看看它的注释:

Returns the {@link ChannelFuture} which will be notified when this channel is closed. This method always returns the same future instance.

大致意思是:当channel关闭时,返回一个ChannelFuture。

但是closeFuture()并不会阻塞,它是立即返回一个future,真正让代码阻塞的是sync() ,只要channel不关闭,这行代码就会一直阻塞。

PS:如果你不了解future,建议你先自行百度了解一下再回头来看netty。

然后我们就可以运行启动netty服务端了。

客户端

handler

服务端和客户端双方是可以互相发送消息的,所以客户端也一样有handler,它的机制也和服务端的一样。代码也十分相似:

package com.netty.client;

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

public class EchoClientHandler extends ChannelInboundHandlerAdapter {

    @Override
    public void channelActive(ChannelHandlerContext ctx) {
        ctx.writeAndFlush(Unpooled.copiedBuffer("hello server".toCharArray(), CharsetUtil.UTF_8));
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        System.out.println("receive: " + ((ByteBuf) msg).toString(CharsetUtil.UTF_8));
    }
}


启动客户端

package com.netty.client;

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

import java.net.InetSocketAddress;

public class Client {

    public static void main(String[] args) throws InterruptedException {
        EventLoopGroup group =  new NioEventLoopGroup();
        EchoClientHandler handler = new EchoClientHandler();
        try{
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.group(group)
                    .channel(NioSocketChannel.class)
                    .remoteAddress(new InetSocketAddress("127.0.0.1", 1010))
                    .handler(new ChannelInitializer() {
                        @Override
                        protected void initChannel(Channel ch) throws Exception {
                            ch.pipeline().addLast(handler);
                        }
                    });
            ChannelFuture future = bootstrap.connect().sync();
            future.channel().closeFuture().sync();
        }finally{
            group.shutdownGracefully().sync();
        }
    }
}

客户端启动的代码与服务端十分相似,大家自己仔细对比一下就知道了,这里不再重复说明了。

值得一提的是,在客户端和服务端不同的是,客户端的NioEventLoopGroup只配置了一个,而服务端配置了boss和worker两个。这是因为服务端面向的是众多客户端,因此需要的线程数比较多。而客户端只需面对一个服务端,因此只配置一个NioEventLoopGroup就够了。

甚至可以给NioEventLoopGroup构造方法加上一个参数使其成为单线程:

new NioEventLoopGroup(1);

到这里,第一个netty应用就写完了。先启动服务端,再启动客户端,不出意外的话服务端就会收到客户端收到的hello server了。

  • 1
    点赞
  • 1
    评论
  • 1
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

相关推荐
©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值