网络编程 - 4 (Netty案例)

156 阅读9分钟

1.1 Netty案例:客户端与服务器之间通信

目标:使用 Netty 开发一个网络应用程序,实现服务端和客户端之间的数据通信。

image.png

实现步骤:

  1. 导入依赖
  2. 编写Netty服务端程序:配置线程组、配置自定义业务处理类、绑定端口号、启动Server,等待Client连接
  3. 编写服务端-业务处理类Handler:继承ChannelInboundHandlerAdapter,并重写三个方法:读取事件、读取完成事件、异常捕获事件
  4. 编写客户端程序:配置线程组、配置自定义业务处理类,启动Client,连接Server
  5. 编写客户端-业务处理类:继承ChannelInBoundHandlerAdapter,重写2个方法:通道就绪事件、读取时间

导入依赖

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

服务端程序Server

package com.example.netty1.netty;

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

public class NettyServer {
    public static void main(String[] args) throws Exception {
        // 1. 创建一个线程组:接收服务端连接
        NioEventLoopGroup bossGroup = new NioEventLoopGroup();
        // 2. 创建一个线程组:处理网络操作
        NioEventLoopGroup workerGroup = new NioEventLoopGroup();
        // 3. 创建服务端助手,来配置参数
        ServerBootstrap serverBootstrap = new ServerBootstrap();
        serverBootstrap
                // 4. 设置两个线程组
                .group(bossGroup, workerGroup)
                // 5. 使用NioServerSocketChannel作为服务端通道的实现
                .channel(NioServerSocketChannel.class)
                // 6. 设置线程队列中等待连接的个数
                .option(ChannelOption.SO_BACKLOG, 128)
                // 7. 保持活动连接状态
                .childOption(ChannelOption.SO_KEEPALIVE, true)
                // 8. 创建一个通道初始化对象
                .childHandler(new ChannelInitializer<SocketChannel>() {
                    public void initChannel(SocketChannel sc) {
                        // 9. 往pipeline中添加一个自定义handler类
                        sc.pipeline().addLast(new NettyServerHandler());
                    }
                });
        System.out.println(".......服务端启动中 int port:9999......");
        // 10. 绑定端口 bind()方法是异步的 sync方法是同步阻塞的
        ChannelFuture channelFuture = serverBootstrap.bind(9999).sync();
        System.out.println("......服务端启动成功......");

        // 11. 关闭通道,关闭线程组
        channelFuture.channel().closeFuture().sync();
        bossGroup.shutdownGracefully();
        workerGroup.shutdownGracefully();
    }
}

服务端自定义业务处理类

package com.example.netty1.netty;

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;

import java.nio.charset.StandardCharsets;

/**
 * 服务端的业务处理类
 */
public class NettyServerHandler extends ChannelInboundHandlerAdapter {

    /**
     * 读取数据事件,msg为客户端发过来的信息
     */
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        // 用缓冲区接受数据
        ByteBuf buf = (ByteBuf) msg;
        // 转成字符串
        System.out.println("Client msg: " +buf.toString(CharsetUtil.UTF_8));
    }

    /**
     * 数据读取完成事件,读完客户端数据后回复客户端
     * @param ctx 事件处理器上下文
     */
    public void channelReadComplete(ChannelHandlerContext ctx) {
        // Unlooped.copiedBuffer 获取到缓冲区
        ctx.writeAndFlush(Unpooled.copiedBuffer("宝塔镇河妖", StandardCharsets.UTF_8));
    }

    /**
     * 异常发生事件
     */
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable t) {
        // 异常时,关闭ChannelHandlerContext,它是相关信息的汇总,关闭他,其他的也就关闭了
        ctx.close();
    }
}

客户端

package com.example.netty1.netty;

import io.netty.bootstrap.Bootstrap;
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.NioSocketChannel;

public class NettyClient {
    public static void main(String[] args) throws Exception {
        // 1.创建一个线程组
        EventLoopGroup group = new NioEventLoopGroup();
        // 2.创建一个客户端助手,完成相关配置
        Bootstrap bootstrap = new Bootstrap();
        // 3.设置线程组
        bootstrap.group(group)
                // 4.设置客户端通道的实现类
                .channel(NioSocketChannel.class)
                // 5.创建一个通道初始化对象
                .handler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel socketChannel) throws Exception {
                        // 6.往pipeline添加自定义的handler
                        socketChannel.pipeline().addLast(new NettyClientHandler());
                    }
                });
        System.out.println("客户端就绪... msg发射...");
        // 7.启动客户端去连接服务端 connect方法是异步的,sync方法是非阻塞的
        ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 9999).sync();
        // 8.关闭连接(异步非阻塞)
        channelFuture.channel().closeFuture().sync();
    }
}

客户端自定义事件处理类

package com.example.netty1.netty;

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 NettyClientHandler extends ChannelInboundHandlerAdapter {

    /**
     * 通道就绪事件
     */
    public void channelActive(ChannelHandlerContext ctx) {
        ctx.writeAndFlush(Unpooled.copiedBuffer("天王盖地虎", CharsetUtil.UTF_8));
    }

    /**
     * 读取数据事件
     */
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        ByteBuf buf = (ByteBuf) msg;
        System.out.println("Server msg: " + buf.toString(CharsetUtil.UTF_8));
    }
}

1.2 Netty案例:网络聊天室V2.0

image.png

  1. 编写聊天程序服务端:配置线程组,配置编解码器,配置自定义业务处理类,绑定端口号,启动Server,等待Client连接。

  2. 编写服务端-业务处理类:

    (1)当通道就绪,输出上线

    (2)当通道未就绪,输出离线

    (3)当通道发来数据时,读取数据,进行广播

  3. 编写聊天程序客户端:配置线程组,配置编解码器,配置自定义业务处理类,启动Client,连接Server。

    (1)连接服务端成功后,获取客户端与服务端建立的Channel

    (2)获取系统键盘输入,将用户输入信息通过channel发送给客户端

  4. 编写客户端-业务处理类:

    (1)读取事件:监听服务端广播消息

聊天程序服务端ChatServer:

往 Pipeline 链中添加了处理字符串的编码器和解码器,它们加入到 Pipeline 链中后会自动工作,使得服务端读写字符串数据时更加方便,不用人工处理 编解码操作。

package com.example.netty1.netty;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;

public class NettyChatServer {
    //服务端口号
    private int port;

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

    public void run() {
        // 1. 配置线程组
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            // 设置两个线程组
            serverBootstrap.group(bossGroup, workerGroup)
                    // 使用NioServerSocketChannel作为通道实现
                    .channel(NioServerSocketChannel.class)
                    // 设置线程队列中等待连接个数
                    .option(ChannelOption.SO_BACKLOG, 128)
                    // 保持活动连接状态
                    .childOption(ChannelOption.SO_KEEPALIVE, true)
                    .childHandler(new ChannelInitializer<SocketChannel>() {

                        @Override
                        public void initChannel(SocketChannel socketChannel) {
                            ChannelPipeline pipeline = socketChannel.pipeline();
                            pipeline.addLast("decoder", new StringDecoder());
                            pipeline.addLast("encoder", new StringEncoder());
                            pipeline.addLast(new NettyChatServerHandler());
                        }
                    });
            System.out.println("网络聊天室Server启动。。。");
            ChannelFuture channelFuture = serverBootstrap.bind(port).sync();
            channelFuture.channel().closeFuture().sync();
        } catch (Exception e) {
            workerGroup.shutdownGracefully();
            bossGroup.shutdownGracefully();
            System.out.println("网络聊天室Server关闭。。。");
        }
    }

    public static void main(String[] args) throws Exception {
        new NettyChatServer(9999).run();
    }

}

服务端业务处理类ChatServerHandler

上述代码通过继承 SimpleChannelInboundHandler 类自定义了一个服务端业务处理类,并在该类中重写了三个方法。

当通道就绪时,输出上线

当通道未就绪时,输出离线

当通道发来数据时,读取数据,进行广播

package com.example.netty1.netty;

import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.SimpleChannelInboundHandler;

import java.util.ArrayList;
import java.util.List;

public class NettyChatServerHandler extends SimpleChannelInboundHandler<String> {

    public static List<Channel> channelList = new ArrayList<>();

    // 通道就绪
    @Override
    public void channelActive(ChannelHandlerContext ctx) {
        Channel inChannel = ctx.channel();
        channelList.add(inChannel);
        System.out.println("[Server]: " + inChannel.remoteAddress().toString().substring(1) + "上线");
    }

    // 通道未就绪
    @Override
    public void channelInactive(ChannelHandlerContext ctx) {
        Channel channel = ctx.channel();
        channelList.remove(channel);
        System.out.println("[Server]: " + channel.remoteAddress().toString().substring(1) + "离线");
    }

    // 读取数据
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, String s) {
        Channel inChannel = ctx.channel();
        System.out.println("s = " +s);
        for (Channel channel : channelList) {
            if (channel != inChannel) {
                channel.writeAndFlush("[" + channel.remoteAddress().toString().substring(1) + "] 说:" + s + "\n");
            }
        }
    }
}

聊天程序客户端ChatClient

通过 Netty 编写了一个客户端程序。客户端同样需要配置编解码器

package com.example.netty1.netty;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;

public class NettyChatServer {
    //服务端口号
    private int port;

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

    public void run() {
        // 1. 配置线程组
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            // 设置两个线程组
            serverBootstrap.group(bossGroup, workerGroup)
                    // 使用NioServerSocketChannel作为通道实现
                    .channel(NioServerSocketChannel.class)
                    // 设置线程队列中等待连接个数
                    .option(ChannelOption.SO_BACKLOG, 128)
                    // 保持活动连接状态
                    .childOption(ChannelOption.SO_KEEPALIVE, true)
                    .childHandler(new ChannelInitializer<SocketChannel>() {

                        @Override
                        public void initChannel(SocketChannel socketChannel) {
                            ChannelPipeline pipeline = socketChannel.pipeline();
                            pipeline.addLast("decoder", new StringDecoder());
                            pipeline.addLast("encoder", new StringEncoder());
                            pipeline.addLast(new NettyChatServerHandler());
                        }
                    });
            System.out.println("网络聊天室Server启动。。。");
            ChannelFuture channelFuture = serverBootstrap.bind(port).sync();
            channelFuture.channel().closeFuture().sync();
        } catch (Exception e) {
            workerGroup.shutdownGracefully();
            bossGroup.shutdownGracefully();
            System.out.println("网络聊天室Server关闭。。。");
        }
    }

    public static void main(String[] args) throws Exception {
        new NettyChatServer(9999).run();
    }

}

客户端业务处理类ChatClientHandler

上述代码通过继承 SimpleChannelInboundHandler 自定义了一个客户端业务处理类,重写了一个方法用来读取服务端发过来的数据。

package com.example.netty1.netty;

import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;

public class NettyChatClientHandler extends SimpleChannelInboundHandler<String> {


    @Override
    protected void channelRead0(ChannelHandlerContext channelHandlerContext, String s) throws Exception {
        System.out.println(s.trim());
    }
}

1.3 编码和解码

为什么要编码和解码:因为计算机数据传输的是二进制的字节数据。

  • 解码:字节数据 -》 字符串(字符数据)
  • 编码:字符串(字符数据) -》 字节数据

1.3.1 概述

我们在编写网络应用程序的时候需要注意codec(编解码器),因为数据在网络中传输都是二进制字节码数据,而我们拿到的目标数据往往不是字节码数据。因此在发送数据的时候就需要编码,在接收数据的时候就需要解码。

codec的组成部分有2个:decoder(解码器)和encoder(编码器)

decoder负责将字节数据转为业务数据
encoder负责将业务数据转为字节数据

其中Java的序列化技术就可以作为coder使用,但是他的硬伤太多:

  1. 无法跨语言,应该是Java序列化最大的问题
  2. 序列化后的体积太大,是二进制编码的5倍多
  3. 序列化性能太差

Netty自身提供了一些编解码器,如下:

  • StringEncoder对字符串数据进行编码
  • ObjectEncoder对Java对象进行编码

Netty自带的ObjectEncoder和StringEncoder可以用来实现POJO对象或各种业务对象的编码和解码,但其内部使用的还是Java的序列化技术,所以在某些场景下不适用。对于POJO对象或各种业务对象要实现编码和解码,我们需要更高效更强的技术。

1.3.2 Google的Protobuf

Protobuf是Google发布的开源项目,全称Google Protoful Buffers,特点如下:

  • 支持跨平台、多语言(支持目前大部分语言,如Java、Python、C++等)
  • 高性能、高可靠性
  • 使用protoful编码器能自动生成代码,protoful是将类的定义使用.proto文件进行描述,然后通过protoc.exe编辑器根据.proto自动生成Java文件。

在使用Netty开发时,经常会结合Protoful作为codec去使用,具体用法如下:

使用步骤:

  1. 将传递数据的实体类生成(根据构建者模式设计)
  2. 配置编解码器
  3. 传递数据使用生成后的实体类

1.3.3 导入Protoful依赖

<dependency>
    <groupId>com.google.protobuf</groupId>
    <artifactId>protobuf-java</artifactId>
    <version>3.6.1</version>
</dependency>

1.3.4 proto 文件

假设我们要处理的数据是图书信息,那就需要为此编写 proto 文件

image.png

syntax = "proto3";
    option java_outer_classname = "BookMessage";
    message Book{
        int32 id = 1;
        string name = 2;
}

1.3.5 生成Java类

通过 protoc.exe 根据描述文件生成 Java 类

cd C:\protoc-3.6.1-win32\bin

protoc --java_out=. Book.proto

image.png

1.3.6 客户端

在编写客户端程序时,要向 Pipeline 链中添加 ProtobufEncoder 编码器对象。

package com.example.netty1.netty;

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.protobuf.ProtobufEncoder;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;

import java.util.Scanner;

public class NettyEncoderDecoderClient {

    private final String host;
    private final Integer port;

    public NettyEncoderDecoderClient(String host, Integer port) {
        this.host = host;
        this.port = port;
    }

    public void run () {
        EventLoopGroup group = new NioEventLoopGroup();
        try {
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.group(group)
                    .channel(NioSocketChannel.class)
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        public void initChannel(SocketChannel socketChannel) {
                            ChannelPipeline pipeline = socketChannel.pipeline();
                            pipeline.addLast("encoder", new ProtobufEncoder());
                            pipeline.addLast(new NettyEncoderDecoderClientHandler());
                        }
                    });
            ChannelFuture channelFuture = bootstrap.connect(host, port).sync();
            Channel channel = channelFuture.channel();
            System.out.println("-----" + channel.localAddress().toString().substring(1) + "-----");
            Scanner scanner = new Scanner(System.in);
            while (scanner.hasNextLine()) {
                String msg = scanner.nextLine();
                channel.writeAndFlush(msg + "\n");
            }
            channel.closeFuture().sync();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            group.shutdownGracefully();
        }
    }

    public static void main(String[] args) {
        new NettyEncoderDecoderClient("127.0.0.1", 9999).run();
    }

}

1.3.7 客户端业务类

在往服务端发送图书(POJO)时就可以使用生成的 BookMessage 类搞定

package com.example.netty1.netty;

import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.SimpleChannelInboundHandler;

public class NettyEncoderDecoderClientHandler extends ChannelInboundHandlerAdapter {


    @Override
    public void channelActive(ChannelHandlerContext ctx) {
        BookMessage.Book book = BookMessage.Book.newBuilder().setId(1).setName("天王盖地虎").build();
        ctx.writeAndFlush(book);
    }
}

1.3.8 服务端

在编写服务端程序时,要向 Pipeline 链中添加 ProtobufDecoder 解码器对象。

package com.example.netty1.netty;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.protobuf.ProtobufDecoder;
import io.netty.handler.codec.protobuf.ProtobufEncoder;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;

public class NettyEncoderDecoderServer {
    //服务端口号
    private int port;

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

    public void run() {
        // 1. 配置线程组
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            // 设置两个线程组
            serverBootstrap.group(bossGroup, workerGroup)
                    // 使用NioServerSocketChannel作为通道实现
                    .channel(NioServerSocketChannel.class)
                    // 设置线程队列中等待连接个数
                    .option(ChannelOption.SO_BACKLOG, 128)
                    // 保持活动连接状态
                    .childOption(ChannelOption.SO_KEEPALIVE, true)
                    .childHandler(new ChannelInitializer<SocketChannel>() {

                        @Override
                        public void initChannel(SocketChannel socketChannel) {
                            ChannelPipeline pipeline = socketChannel.pipeline();
                            pipeline.addLast("decoder", new ProtobufDecoder(BookMessage.Book.getDefaultInstance()));
                            pipeline.addLast(new NettyEncoderDecoderServerHandler());
                        }
                    });
            System.out.println("网络聊天室Server启动。。。");
            ChannelFuture channelFuture = serverBootstrap.bind(port).sync();
            channelFuture.channel().closeFuture().sync();
        } catch (Exception e) {
            workerGroup.shutdownGracefully();
            bossGroup.shutdownGracefully();
            System.out.println("网络聊天室Server关闭。。。");
        }
    }

    public static void main(String[] args) throws Exception {
        new NettyEncoderDecoderServer(9999).run();
    }

}

1.3.9 服务端业务类

在服务端接收数据时,直接就可以把数据转换成 POJO 使用

package com.example.netty1.netty;

import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;

public class NettyEncoderDecoderServerHandler extends ChannelInboundHandlerAdapter {


    // 读取数据
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        BookMessage.Book book = (BookMessage.Book) msg;
        System.out.println("客户端说:" +book.getName());
    }
}