本文主要内容:

  • 设置开发环境

  • 编写Echo服务器和客户端

  • 构建并测试应用程序

1.设置开发环境

准备好JDK和Maven

2.Netty客户端/服务器概览

在这里插入图片描述

在客户端建立一个连接之后,它会向服务器发送一个或多个消息,反过来,服务器又会将每个消息回送给客户端。

3.编写Echo服务器

首先明确的一点是,所有的Netty服务器都需要以下两部分:

  • 至少一个ChannelHandler:用户实现服务器对从客户端接收的数据的处理,即业务逻辑
  • 引导:配置服务器的启动代码,如将服务器绑定到它需要监听连接请求的端口上。

3.1ChannelHandler和业务逻辑

ChannelHandler是一个父接口,它的实现负责接收并响应事件通知

这里我们的Echo服务器需要响应传入的消息,所以需要实现ChannelInboundHandler接口,用来定义响应入站事件的方法,这里我们继承ChannelInboundHandlerAdapter类。

主要有如下方法可以调用:

  • channelRead(): 对于每个传入的消息都要调用

  • channelReadComplete():表明了本次从 Socket 读了数据,但是否是完整的数据它其实并不知道

  • exceptionCaught():在读取操作期间 ,有异常抛出时会调用

/** Sharable标识一个ChannelHandler可以被多个Channel安全地共享 **/
@ChannelHandler.Sharable
public class EchoServerHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        ByteBuf in = (ByteBuf)msg;
        System.out.println("Server received: " + in.toString(CharsetUtil.UTF_8));
        //将接收到的消息写给发送者
        ctx.write(in);
    }

    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        //将消息冲刷到客户端,并且关闭该Channel
        ctx.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE);
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        //打印异常栈跟踪
        cause.printStackTrace();
        //关闭该Channel
        ctx.close();
    }
}

3.2 引导服务器

主要内容如下:

  • 绑定服务器将在其上监听并接收传入连接请求的端口;
  • 配置Channel,以将有关的入站消息通知给EchoServerHandler实例

编写EchoServer类:

public class EchoServer {
    private final int port;

    public EchoServer(int port) {
        this.port = port;
    }

    public static void main(String[] args) throws InterruptedException {
        //设置端口值不正确
        if(args.length != 1){
            System.err.print("Usage: " + EchoServer.class.getSimpleName() + "<port>");
        }
        int port = Integer.parseInt(args[0]);
        //调用服务器的start()方法
        new EchoServer(port).start();
    }

    private void start() throws InterruptedException {
        final EchoServerHandler serverHandler = new EchoServerHandler();
        //创建EventLoopGroup,因为我们使用的是NIO传输,所以要指定NioEventLoopGroup来接收和处理新的连接
        NioEventLoopGroup group = new NioEventLoopGroup();
        try {
            //创建ServerBootstrap
            ServerBootstrap b = new ServerBootstrap();
            b.group(group)
                    //指定所使用的NIO传输Channel
                    .channel(NioServerSocketChannel.class)
                    //使用指定的端口设置套接字地址,服务器将绑定到这个地址以监听新的连接请求
                    .localAddress(new InetSocketAddress(port))
                    //当一个新的连接被接收时,一个新的子Channel将会被创建,ChannelInitializer会把EchoServerHandler的实例添加到ChannelChannelPipeline中,这个ChannelHandler会接收入站消息的通知。
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel socketChannel) throws Exception {
                            //由于EchoServerHandler被标注位@Shareable,所以我们使用的是同一个EchoServerHandler
                            socketChannel.pipeline().addLast(serverHandler);
                        }
                    });
            //异步的绑定服务器,调用sync()方法阻塞等待直到绑定完成
            ChannelFuture f = b.bind().sync();
            //获取Channel的CloseFuture,并且阻塞当前线程直到它完成
            f.channel().closeFuture().sync();
        }finally {
            //关闭EventLoopGroup,并且释放所有的资源
            group.shutdownGracefully().sync();
        }
    }
}

主要步骤:

  • EchoServerHandler实现业务逻辑;
  • main()方法引导了服务器

在引导过程中的步骤:

  • 使用一个EchoServerHandler实例来初始化每一个新的Channel

  • 创建并分配一个NioEventLoopGroup实例以进行事件的处理,如接收新连接以及读/写数据;

  • 创建一个ServerBootstrap的实例以引导和绑定服务器;

  • 指定服务器绑定的本地InetSocketAddress

  • 调用ServerBootstrap.bind()方法来绑定服务器

4. 编写Echo客户端

Echo客户端主要任务:

  • 连接到服务端
  • 发送一个或多个消息;
  • 对于每个消息,等待并接收从服务器发回的消息;
  • 关闭连接

客户端所涉及到的两个主要代码部分也是业务逻辑和引导

4.1 通过ChannelHandler实现客户端逻辑

客户端也拥有一个用来处理数据的ChannelInboundHandler,这里我们扩展SimpleChannelInboundHandler类来处理所有必须的任务,重写以下方法:

  • channelActive():在到服务器的连接已经建立之后将被调用

  • channelRead0():当从服务器接收到一条消息时被调用

  • exceptionCaught():在处理过程中引发异常时被调用

代码如下:

@ChannelHandler.Sharable
public class EchoClientHandler extends SimpleChannelInboundHandler<ByteBuf> {

    /** 连接服务器后调用该方法 **/
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        ctx.writeAndFlush(Unpooled.copiedBuffer("Netty rocks!", CharsetUtil.UTF_8));
    }

    /** 从服务器接收到消息后调用该方法 **/
    @Override
    protected void channelRead0(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf) throws Exception {
        System.out.println("Client received: "+ byteBuf.toString(CharsetUtil.UTF_8));
    }

    /** 发生异常时,记录错误并关闭Channel **/
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        ctx.close();
    }
}

注意: 从服务器发送的消息可能会被分块接收,channelRead0()方法因此有可能被调用多次。

还有一点值得注意的是Echo 服务端使用的ChannelHandler是 ChannelInboundHandlerAdapter,而 Echo 客户端使用的却是 SimpleChannelInboundHandler,其实它们是继承关系。

在这里插入图片描述

既然是继承关系,也就是说,”你有的我也有,你没有的我还有。” 那么 SimpleChannelInboundHandler 里面肯定重写或者新增了 ChannelInboundHandlerAdapter 里面的方法功能 - channelRead0 和 channelRead()。

protected abstract void channelRead0(ChannelHandlerContext ctx, I msg) throws Exception;

至于为什么会这样设计,原因是在客户端,当 channelRead0() 方法完成时,你已经有了传入消息,并且已经处理完它了。当该方法返回时,SimpleChannelInboundHandler负责释放指向保存该消息的ByteBuf的内存引用。而在服务端,你仍然需要将传入消息回送给发送者,而 write() 操作是异步的,直到 channelRead() 方法返回后可能仍然没有完成。为此,EchoServerHandler扩展了 ChannelInboundHandlerAdapter ,其在这个时间点上不会释放消息。

4.2 引导客户端

客户端使用主机和端口号来连接远程地址

代码如下:

public class EchoClient {
    private final String host;
    private final int port;

    public EchoClient(String host, int port) {
        this.host = host;
        this.port = port;
    }

    public static void main(String[] args) throws InterruptedException {
        if(args.length != 2){
            System.err.println("Usage: "+ EchoClient.class.getSimpleName()+"<host> <port>");
            return;
        }
        String host = args[0];
        int port = Integer.parseInt(args[1]);
        new EchoClient(host,port).start();
    }

    private void start() throws InterruptedException {
        NioEventLoopGroup group = new NioEventLoopGroup();
        try {
            Bootstrap b = new Bootstrap();
            b.group(group)
                    .channel(NioSocketChannel.class)
                    .remoteAddress(new InetSocketAddress(host,port))
                    //在创建Channel时向ChannelPipeline中添加一个EchoClientHandler实例
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel socketChannel) throws Exception {
                            socketChannel.pipeline().addLast(new EchoClientHandler());
                        }
                    });
            ChannelFuture f = b.connect().sync();
            f.channel().closeFuture().sync();
        }finally {
            group.shutdownGracefully().sync();
        }
    }
}

5. 构建和运行Echo服务器和客户端

目录如下:

在这里插入图片描述

注意要在pom.xml文件中引入netty依赖以及编译maven模板需要的插件。

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


  <build>
      <plugins>
          <plugin>
              <groupId>org.codehaus.mojo</groupId>
              <artifactId>exec-maven-plugin</artifactId>
              <version>1.2.1</version>
              <executions>
                  <execution>
                      <goals>
                          <goal>java</goal>
                      </goals>
                  </execution>
              </executions>
              <configuration>
                  <!--指定main文件,不指定会报错,如果是client就要变成EchoClient-->
                  <mainClass>EchoServer</mainClass>
              </configuration>
          </plugin>
      </plugins>
  </build>

先执行mvn clean package来清除指定的包,然后cd server,执行exec:java -Dexec.args="1",接下来点击右下角的加号新建一个终端,然后cd client,执行exec:java -Dexec.args="0 1",即可看到效果。一定一定要先开启服务器然后再开客户端,否则会报错。

效果:

在这里插入图片描述

补充一个遇到的坑:

在这里插入图片描述

如果在执行mvn exec:java -Dexec.args="0 1"的时候出现上面错误,首先去自己的maven的目录下查看配置文件setting.xml

<localRepository>D:\Software\apache-maven-3.6.1\repository</localRepository>

看路径是否正确,其次检查IDEA中的MAVEN配置。

在这里插入图片描述