Netty 案例之简单C/S通讯

15,477 阅读9分钟

失败往往是黎明前的黑暗,继之而出现的就是成功的朝霞

# Netty 案例之简单C/S通讯

    Netty 往通俗了讲就是封装的NIO的一个框架,将NIO 编程变得更加方便和快捷,而且减少了NIO编程的难度,降低了门槛的“贼牛逼”的框架

背景

    我们在做网络编程的时候,从原始的BIO【性能低下,不支持高并发,浪费线程资源等等】,之后发展成了NIO 【开发上手难度大,容易出现BUG等等】,最后Netty 横空出世,将上面的问题给解决了,成为了网络开发中的一大利器,各种开源的中间件中的网络通讯也用了Netty技术,比如 Dubbo、RocketMQ 等等
    我为什么要研究这个框架呢,我们公司的项目需要用消息推送这块,服务端发送消息将该消息推送到客户端操作,所以就需要深入的了解这个框架,【当然了这个框架上手确实快但是想要真正的掌握它并不容易】,最起码你需要了解NIO方面的知识,还要了解一些Reactor模型、网络协议等等

特点

使用方面

  1. 使用简单,实现特定的接口和方法就可以工作
  2. 功能强大,实现各种协议、比如Webscoket协议、Http协议等
  3. 性能好,支持大并发,充分利用系统的资源
  4. 扩展性强,可以通过 ChannelHandler 进行各种自定义扩展
  5. 可以自定义传输协议,粘包\拆包等
  6. 社区活跃

高性能方面

  1. Reactor 线程模型,同步非阻塞,利用更少的资源做更多的事情
  2. 内存池设计,申请的内存可以重复使用
  3. 零拷贝技术,减少不必要的内存拷贝实现了高效传输
  4. 支持高性能序列化传输协议,比如google的protobuf传输协议,当然也可以自定义传输协议

应用场景

  1. 游戏领域可以使用
  2. IM聊天领域
  3. 消息推送领域
  4. 自定义服务器【HTTP服务器、FTP服务器等】

Netty 组件简要说明

  1. Channel

    通道,服务器端与客户端中间建立的通道,代表一个连接,每个客户端代表一个Channel

  1. ChannelPipeline

    责任链,每个channel都有且仅有一个ChannelPipeline与之对应,而且是双向链表的那种里面都是ChannelHandler

  1. ChannelHandler

    用于出处理出入站消息以及我们自定义的业务逻辑都是这个组件

  1. ChannelHandlerContext

    每一个ChannelHandlerContext都对应一个ChannelHandler,它保存了几乎所有的上下文信息,通过它可以获取到channel通道,channelPipline等信息

  1. ChannelInitializer

    Channel 通道的初始化器

  1. ChannelFuture

    代表I/O操作的执行结果,因为是异步的机制所以需要通过事件机制获取执行结果,通过添加监听器,通过返回的结果执行我们想要的操作

  1. ByteBuf

    字节缓存区通过ByteBuf操作基础的字节数组和缓冲区

  1. EventLoopGroup

    线程池,负责处理Channel对应的I/O事件 ,里面都是通过 EventExecutor 进行任务执行的

  1. ServerBootstrap

    服务端启动器

  1. Bootstrap

    客户端启动器

案例

需求

    我们通过利用上面的组件开发一个客户端和服务器端的简单通讯,先启动服务端,之后监听端口8080,再启动客户端,连接成功后给服务端发消息,服务端收到消息后给客户端回应消息

代码实现

服务端实现

  1. 先创建一个NettyServerHandler 继承 ChannelInboundHandlerAdapter这个类,主要是在这个输入Handler 中实现自己的业务逻辑,我们这块的逻辑就是获取客户端发来的信息并给与客户端进行消息回应
package com.zxp.netty.base;

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;

@ChannelHandler.Sharable
public class NettyServerHandler extends ChannelInboundHandlerAdapter {
    /**
        当客户端发送过来信息的时候会走该方法
    */
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        ByteBuf byteBuf = (ByteBuf) msg;
        System.out.println("客户端输出内容是" + byteBuf.toString(CharsetUtil.UTF_8));
        ctx.channel().writeAndFlush(Unpooled.copiedBuffer("你好客户端我已经收到你的消息了", CharsetUtil.UTF_8));
    }
    /**
        当客户端与服务器端进行连接成功的时候走该方法,他是先于channelActive 方法执行的
    */
    @Override
    public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
        System.out.println(ctx.channel().remoteAddress() + "channelRegistered 已经上线......");
    }
    /**
        当客户端与服务器端进行连接成功的时候走该方法
    */
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        System.out.println(ctx.channel().remoteAddress() + "channelActive 已经上线......");
    }
    /**
        当服务器端读取完客户端信息后执行该方法
    */
    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        System.out.println("客户端消息已经读取完成......");
    }
    /**
        客户端下线走该方法,此方法优先于 channelUnregistered 方法执行
    */
    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        System.out.println(ctx.channel().remoteAddress() + "channelInactive 已经下线......");
    }
    /**
        客户端下线走该方法
    */
    @Override
    public void channelUnregistered(ChannelHandlerContext ctx) throws Exception {
        System.out.println(ctx.channel().remoteAddress() + "channelUnregistered 已经下线......");
    }
    /**
        捕获异常信息的方法
    */
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        System.out.println("出现异常");
    }
}
  1. 创建一个NettyServerChannelInitializer 继承channel初始化工具类 ChannelInitializer 为了将我们自己定义的ChannelHandler加入到ChannelPipeline上面
package com.zxp.netty.base;

import io.netty.channel.ChannelInitializer;
import io.netty.channel.socket.SocketChannel;

public class NettyServerChannelInitializer extends ChannelInitializer<SocketChannel> {
    @Override
    protected void initChannel(SocketChannel ch) throws Exception {
        ch.pipeline().addLast(new NettyServerHandler());
    }
}
  1. 定义服务端启动主类 NettyServer
package com.zxp.netty.base;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;

import java.net.InetSocketAddress;

public class NettyServer {
    public static void main(String[] args) {
        // 创建主线程池,主要是监听客户端的连接请求操作
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        // 创建从线程池,这个才是真正干活的线程池,用来监听客户端的一些事件请求
        EventLoopGroup workGroup = new NioEventLoopGroup(4);
        // 创建服务端启动类
        ServerBootstrap bootstrap = new ServerBootstrap();
        bootstrap.group(bossGroup, workGroup).childHandler(new NettyServerChannelInitializer());
        try {
            ChannelFuture channelFuture = bootstrap
                    // 设置的是非阻塞IO,这个其实和NIO的服务端一样的
                    .channel(NioServerSocketChannel.class)
                    .childOption(ChannelOption.SO_BACKLOG, 1024)
                    // 设置监听端口
                    .bind(new InetSocketAddress("127.0.0.1", 8080)).sync();
            channelFuture.addListener((ChannelFutureListener) future -> {
                if (future.isSuccess()) {
                    System.out.println("服务端启动成功监听端口是 8080");
                }
            });
            // 阻塞操作,closeFuture() 开启了一个channel的监听器【这期间channel在进行各项工作】直到链路断开
            channelFuture.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            try {
                // 关闭EventLoopGroup并释放所有资源,包括所有创建的线程
                bossGroup.shutdownGracefully().sync();
                workGroup.shutdownGracefully().sync();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

服务端启动

图片.png
    此时服务端启动了并且监听的端口是8080

客户端实现

  1. 创建一个客户端的Handler,NettyClientHandler 继承 SimpleChannelInboundHandler 主要的业务逻辑就是监听服务端发送过来的消息以及与服务端建立成功后做一个消息提示
package com.zxp.netty.base;

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.util.CharsetUtil;

/**
 * @ChannelHandler.Sharable 这个注解是为了线程安全,如果你不在乎是否线程安全,不加也可以
 * SimpleChannelInboundHandler:这里的类型可以是ByteBuf,也可以是String,还可以是对象,根据实际情况来
 * ChannelHandlerContext:通道上下文,这里面能获取到很多信息比如channel、pipline等信息
 */
@ChannelHandler.Sharable
public class NettyClientHandler extends SimpleChannelInboundHandler<ByteBuf> {
    /**
        当服务器端发送过来消息的时候会走该方法
    */
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, ByteBuf byteBuf) throws Exception {
        System.out.println("服务端发来消息:" + byteBuf.toString(CharsetUtil.UTF_8));
    }
    /**
        长连接通道建立成功后执行该方法
    */
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        System.out.println("客户端与服务器端已经建立成功");
    }
    /**
    出现异常信息的时候会走该方法
    */
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        System.out.println("发生异常信息");
    }
}
  1. 创建一个 NettyClientChannelInitializer 继承channel初始化工具类 ChannelInitializer 为了将我们自己定义的ChannelHandler加入到ChannelPipeline上面
package com.zxp.netty.base;

import io.netty.channel.Channel;
import io.netty.channel.ChannelInitializer;

/**
 * ChannelInitializer:通道Channel的初始化工作,如加入多个handler,都在这里进行
 */
public class NettyClientChannelInitializer extends ChannelInitializer {
    @Override
    protected void initChannel(Channel channel) throws Exception {
        // 添加我们自定义的Handler
        // 连接建立后,都会自动创建一个管道 pipeline,这个管道也被称为责任链,保证顺序执行,同时又可以灵活的配置各类Handler
        channel.pipeline().addLast(new NettyClientHandler());
    }
}
  1. 定义客户端启动主类 NettyClient
package com.zxp.netty.base;

import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.util.CharsetUtil;

import java.net.InetSocketAddress;

public class NettyClient {
    public static void main(String[] args) {
        EventLoopGroup group = new NioEventLoopGroup();
        Bootstrap bootstrap = new Bootstrap();
        try {
            ChannelFuture future = bootstrap.group(group)
                // 注意这里是 NioSocketChannel
                .channel(NioSocketChannel.class)
                // 进行通道初始化配置
                .handler(new NettyClientChannelInitializer())
                // 同步建立连接的方法,只有连接成功了才会进行后面的操作,才会往下执行
                .connect(new InetSocketAddress("127.0.0.1", 8080)).sync();
            future.addListener((ChannelFutureListener) channelFuture -> {
                // 当客户端与服务器端进行连接成功的时候触发下面的回调
                if (channelFuture.isSuccess()) {
                    System.out.println("与服务器端建立连接成功.....");
                    channelFuture.channel().writeAndFlush(Unpooled.copiedBuffer("你好服务端我是客户端" + channelFuture.channel().localAddress(), CharsetUtil.UTF_8));
                }
            });
            // 阻塞操作,closeFuture()开启了一个channel的监听器【这期间channel在进行各项工作】直到链路断开
            future.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            try {
                group.shutdownGracefully().sync();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

客户端启动

图片.png
    此时你能看到客户端与服务端已经成功的建立了连接并给服务端端发送了消息,而且发送消息的顺序和我们说的一样
图片.png
    服务端已经监听到客户端上线了并且读到了客户端发送的消息并给与了消息返回,而且看控制台输出的日志和我们说的一样,方法的先后执行顺序都是一样的

客户端下线

    当我们客户端进行下线关闭的时候看看服务端是如何执行的
图片.png
    此时服务端已经监听到了客户端下线并且执行了channelInactive和channelUnregistered方法,而且确实向我们上面说的那样,channelInactive在channelUnregistered方法之前执行

开发步骤

服务端

1. 创建服务端Handler
  1. 创建服务端的ChannelHandler业务逻辑类并继承 ChannelInboundHandlerAdapter
  2. 添加 @ChannelHandler.Sharable 注解
  3. 重写一些相关的方法比如channelRead等方法
2. 创建Channel初始化类
  1. 创建 channel初始化类继承 ChannelInitializer
  2. 将自定义的ChannelHandler加入到 channelPipeline中
3. 创建服务端启动类
  1. 创建线程池实例
  2. 创建启动类 ServerBootstrap
  3. 设置相关信息比如NIO通道
  4. 绑定端口
  5. 释放资源

客户端

1. 创建客户端Handler
  1. 创建客户端的ChannelHandler业务逻辑类并继承 SimpleChannelInboundHandler
  2. 添加 @ChannelHandler.Sharable 注解
  3. 重写一些相关的方法比如channelRead0等方法
2. 创建Channel初始化类
  1. 创建 channel初始化类继承 ChannelInitializer
  2. 将自定义的ChannelHandler加入到 channelPipeline中
3. 创建客户端启动类
  1. 创建线程池实例
  2. 创建启动类 Bootstrap
  3. 设置相关信息比如NIO通道
  4. 连接服务端
  5. 释放资源

总结

    到这里客户端与服务端的简单通信也就完成了,并实现了我们想要的效果了,相比于之前NIO的通讯简直要方便太多了,但是这只是简单的开发流程,如果复杂一点的就是涉及到编解码器、自定义传输协议、粘包\拆包等等后续我们都会进行涉及到的,逐一揭开Netty的面纱

我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿