Netty理论与实践(一) netty入门之创建echo服务器

目录

    • 一、理论知识
      • 1. 网络协议TCP/UDP
      • 2. netty简介
      • 3. 依赖
      • 4. netty核心类介绍
    • 二、开发实战
      • 1. 服务端
      • 2. 客户端
    • demo源码
    • 参考


一、理论知识

1. 网络协议TCP/UDP

TCP、UDP协议属于七层协议中传输层的协议,这两种主流协议的差异:

  1. TCP是一个面向连接的、可靠的、基于字节流(socket)的传输层通信协议;
  2. UDP是一个面向数据包(datagram)的、简单的、不可靠的通信协议。
  3. TCP进行有序无错的传输;
  4. UDP则可能出现数据包丢失、错误或重复。
  5. UDP不需要额外的保证,结构简单,只管发送数据,通常UDP的传输速度比TCP更快。
  6. UPD支持广播,TCP则不支持,TCP需要先建立端到端的连接才能发送数据。

基于以上特性,TCP、UDP的使用场景也不同:

  • TCP: HTTPS、HTTP、SMTP、FTP等等。
  • UDP:视频流、语音流、即时通信、广播等等。

在netty服务端中使用哪种协议取决于你在启动类AbstractBootstrap.channel(Class channelClass)中使用了哪种Channel接口实现类(这里的TCP/IP并非指TCP/IP协议簇,其表示的是传输层使用TCP传输,网络层使用IP来提供路由和寻址):

  • ServerSocketChannel:TCP/IP协议,主要实现类NioServerSocketChannelEpollServerSocketChannel
  • DatagramChannel:UDP/IP协议,主要实现类NioDatagramChannelEpollDatagramChannel
  • 其它协议……

如官网Echo源码中使用NioServerSocketChannel进行网络通信:

ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
 .channel(NioServerSocketChannel.class)
 .option(ChannelOption.SO_BACKLOG, 100)
……



2. netty简介

Nett是一个NIO的通信框架,在基于netty的核心框架上,netty实现了各种协议支持,很多功能都是开箱即用的netty-github。

下图分为三个部分:

  • 核心包:零拷贝、高扩展的事件模型、多协议统一的API
  • 传输服务:包括字节流&数据包传输(TCP&UDP)、HTTP隧道、虚拟管道等服务
  • 协议支持:这一部分提供了HTTP&websocket协议、SSL/TLS安全协议、zlib/gzip等压缩技术、大文件传输等等支持
    Netty理论与实践(一) netty入门之创建echo服务器_第1张图片



3. 依赖

<dependency>
    <groupId>io.nettygroupId>
    <artifactId>netty-allartifactId>
    <version>4.1.91.Finalversion>
    <scope>compilescope>
dependency>



4. netty核心类介绍

简单介绍一下netty框架中主要类的作用:

  1. AbstractBootstrap:抽象的引导类。通过该引导类可以配置整个端的生命周期。
  2. ServerBootstrap:AbstractBootstrap子类,用于创建方便创建服务端引导类。
  3. Bootstrap:AbstractBootstrap子类,用于创建方便创建客户端引导类。
  4. EventLoopGroup:事件循环执行组,用于循环执行某项Channel任务。每一个组中包含至少一个EventLoop,每个EventLoop负责执行一个个实际的Channel任务。
  5. Channel:netty的核心组件,它代表一个到实体(硬件设备、文件、网络套接字、或能执行一个或多个不同的I/O操作的程序组件)的开放连接,如进行读操作和写操作。每个与实体的连接都会封装为一个Channel,随后被注册到EventLoop中,然后由EventLoop负责调度处理。
  6. ChannelHandler:Channel的回调处理器,ChannelHandler的每个方法都是Channel的一个回调方法,当Channel触发了某个事件时,就会调用一系列ChannelHandler回调处理器中的某个回调方法,回调方法会对该事件进行各种处理。
  7. ChannelInboundHandler:实现ChannelHandler,是入站(入境,即是由外部传入的操作)事件的回调处理器。譬如远程建立连接或该连接失活数据读取用户事件错误事件都会触发一个入站事件。
  8. ChannelOutboundHandler: 实现ChannelHandler,是出站(出境,即是由内部传出的操作)事件的回调处理器。譬如主动创建或关闭对外连接将数据写到或冲刷到套接字
  9. ChannelHandlerAdapter:实现ChannelHandler,如果将ChannelInboundHandler、ChannelOutboundHandler看作是一种出入站的标记接口的话,那么ChannelHandlerAdapter就是实现各种出入站事件的核心骨架。你可以在各种出入站处理器中看见它的身影。
  10. ChannelInitializer:特殊的入站处理器,它将在一个Channel被注册到EventLoop时去初始化Channel,常用来设置Channel的ChannelPipeline。
  11. ChannelPipeline:一个Channel内部的出入站列表,包含了一系列的ChannelHandler。





二、开发实战

1. 服务端

  1. 服务端两个EventLoopGroup,bossGroup用于监听连接,有连接接入后转发到workerGroup;workerGroup用于处理与Channel的操作事件。
  2. bossGroup注册事件处理器:
    • LoggingHandler:日志处理器,DEBUG模式
  3. workerGroup注册事件处理器:
    • LoggingHandler:日志处理器,DEBUG模式
    • StringDecoder:字符串解码,将字节流转为字符
    • StringEncoder:字符串编码,将字符串转为字节流
    • ServerStringHandler:自定义处理器,将接收的字符原封不动的写回

服务器可以直接设置port,也可以像netty官方示例代码一样使用System.getProperty()来获取java启动命令的键值-Dkey=value

public class EchoServerRunner {
    private final int port;
    
    public EchoServerRunner(Integer port) {
        this.port = port;
    }
    public static void main(String[] args) throws Exception {
        EchoServerRunner server = new EchoServerRunner(8001);
        server.start();
    }
    public void start() throws Exception {
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup, workerGroup)
                    // 新的Channel 如何进行数据传输
                    .channel(NioServerSocketChannel.class)
                    .option(ChannelOption.SO_BACKLOG, 100)
                    // 打印bossGroup处理日志
                    .handler(new LoggingHandler(LogLevel.DEBUG))
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        public void initChannel(SocketChannel ch) throws Exception {
                            // 设置出入消息的处理链
                            ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG), new StringDecoder(), new StringEncoder(), new ServerStringHandler());
                        }
                    });
            // 绑定监听服务端口,并开始接收进来的连接
            ChannelFuture f = b.bind(port).sync();
            f.channel().closeFuture().sync();
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}

ServerStringHandler,自定义处理器代码:

public class ServerStringHandler extends SimpleChannelInboundHandler<String> {
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
        System.out.println("服务端接收到消息:" + msg);
        ctx.writeAndFlush(msg);
    }
}

ok,一个echo服务器的功能就写好了,我们可以通过cmd的telnet命令来测试连接

telnet localhost 8001

连接成功后,服务端显示日志:
在这里插入图片描述

这时我们就可以开始传输数据了,先后输入1 c,控制台打印1cc(前面的一个c是本地输入回显)。
Netty理论与实践(一) netty入门之创建echo服务器_第2张图片

服务端的日志打印中,服务器接收(READ)到两次1byte(1B)的数据:31 63,解码后的原始数据为1 c

Netty理论与实践(一) netty入门之创建echo服务器_第3张图片


解释一下下面的一段内容:

         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 31                                              |1               |
+--------+-------------------------------------------------+----------------+
  • 0 1 2 3 4 5 6 7 8 9 a b c d e f表示的是一个包中的第几个字节流(8bit = 1byte),其最大值也表示了一个包的最大容量。
  • 00000000表示的是包的序号,32位,TCP为了不丢包,就会给每一个包设置一个序号,服务端会按着序号顺序逐步接收,接收成功一个包即返回ack,这也保证了数据接收的有序性。
  • 31表示的是传输的数据,一行中的一列占8字节,这里用的十六进制ASCII表示。
  • 1表示的是该包转码后的数据。

ASCII码的对照表:
ascii-code
ascii

ok,服务端看起来没什么问题了,我们接下来就用netty来模拟一个用户给echo服务器发送数据。



2. 客户端

  1. 客户端只有一个EventLoopGroup,因为我们只需要处理消息收发,并不需要监听某个端口的连接。
  2. workerGroup注册事件处理器:
    • LoggingHandler:日志处理器,DEBUG模式
    • StringDecoder:字符串解码,将字节流转为字符
    • StringEncoder:字符串编码,将字符串转为字节流
    • ClientStringHandler:自定义处理器,用来处理服务器回复
public class EchoClientRunner {
    private String host;
    private Integer port;

    public EchoClientRunner(String host, Integer port) {
        this.host = host;
        this.port = port;
    }
    public static void main(String[] args) throws Exception {
        EchoClientRunner client = new EchoClientRunner("127.0.0.1", 8001);
        client.start();
    }
    public void start() throws Exception {
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            Bootstrap b = new Bootstrap();
            b.group(workerGroup)
                    // 新的Channel 如何接收进来的连接
                    .channel(NioSocketChannel.class)
                    .option(ChannelOption.TCP_NODELAY, true)
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        public void initChannel(SocketChannel ch) throws Exception {
                            // 设置出入消息的处理链
                            ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG), new StringDecoder(), new StringEncoder(), new ClientStringHandler());
                        }
                    });
            // 创建一个连接
            ChannelFuture f = b.connect(host, port).sync();
            // 创建连接后手动发送一个请求
            f.channel().writeAndFlush("Hello!");
            f.channel().closeFuture().sync();
        } finally {
            workerGroup.shutdownGracefully();
        }
    }
}

先启动echo服务器,再启动客户端
服务器日志如下:
Netty理论与实践(一) netty入门之创建echo服务器_第4张图片

客户端日志如下:
Netty理论与实践(一) netty入门之创建echo服务器_第5张图片

echo服务端 客户端交互成功。


不过也行你会对上面的ChannelPipeline中的执行器顺序感到疑惑。这里就不得不提一下入境(ChannelInboundHandler)、出境(ChannelOutboundHandler)以及同时有出入境(ChannelDuplexHandler)处理器的执行顺序的区别。

// 服务端
ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG), new StringDecoder(), new StringEncoder(), new ServerStringHandler());
// 客户端
ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG), new StringDecoder(), new StringEncoder(), new ClientStringHandler());

以服务端举例,当建立连接,客户端发送过来数据时,这时就会进入入境的处理链,执行顺序是ChannelInboundHandler注入的先后顺序:

  1. LoggingHandler(ChannelDuplexHandler)
  2. StringDecoder(ChannelInboundHandler)
  3. ServerStringHandler(ChannelInboundHandler)

当入境执行链处理完毕后,入境也就执行完成了。但是在ServerStringHandler中,我们又向客户端回复了信息,这时进入出境的处理链,执行顺序为ChannelOutboundHandler注入的相反方向:

  1. StringEncoder(ChannelOutboundHandler)
  2. LoggingHandler(ChannelDuplexHandler)

到这里一个完整的echo服务器就开发完成了。





demo源码

netty-demo





参考

netty实战
User guide for 5.x
java/io/netty/example

你可能感兴趣的:(Netty网络编程实战训练,服务器,java,网络)