Java网络通信基础系列-Netty入门

一.基于各种模型开发的难点

  之所以在整个的开发之中去使用系统实现的程序类,核心的意义在于:它可以帮助开发者隐藏协议的实现细节。

  但是在开发中依然会发现有如下几点:

  1.程序的实现方式上的差异,因为代码的执行会有底层的实现琐碎问题;

  2.在现在给定的通讯里面并没有处理长连接的问题,也就是说按照当前编写的网络通讯,一旦要发送的是稍微大一些的文件,则很大可能是无法传送完成;

  3.在数据量较大的时候需要考虑粘包与拆包问题;

  4.实现的协议细节操作不好控制;

  5.在很多项目开发中(RCP底层实现)需要提供有大量的IO通讯的问题,如果直接使用原始的程序类,开发难度过高;

 

二.Netty框架

  Netty的产生也是符合时代的要求,可以简化大量的繁琐的程序代码,官网:https://netty.io/

  Dubbo使用了Netty作为底层的通讯实现,Netty是基于NIO实现的,也采用了线程池的概念。Netty的最新版本为"4.1.31",

  一开始为了进一步提高Netty处理性能,所以研发了5.0版本,但是研发之后(修改了一些类名称和方法名称、通讯算法)发现性能没有比4.x提升多少。

  所以Netty 5.x是暂时不会被更新的版本了,而现在主要更新的就是Netty 4.x。

  在AIO的模型里面,会发现都采用了回调的处理形式,所以在Netty里面是基于状态的处理形式,例如:连接成功、信息的读取、失败等操作都会由一系列的状态方法来进行定义的。

1.使用Netty实现了一个Echo数据交互处理操作

EchoServerMain.java

package com.bijian.netty.server.main;

import com.bijian.netty.server.EchoServer;

public class EchoServerMain {
    public static void main(String[] args) throws Exception {
        new EchoServer().run();
    }
}

EchoServer.java

package com.bijian.netty.server;

import com.bijian.netty.info.HostInfo;
import com.bijian.netty.server.handler.EchoServerHandler;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
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 EchoServer {
    public void run() throws Exception {    // 进行服务器端的启动处理
        // 线程池是提升服务器性能的重要技术手段,利用定长的线程池可以保证核心线程的有效数量
        // 在Netty之中线程池的实现分为两类:主线程池(接收客户端连接)、工作线程池(处理客户端连接)
        EventLoopGroup bossGroup = new NioEventLoopGroup(10); // 创建接收线程池
        EventLoopGroup workerGroup = new NioEventLoopGroup(20); // 创建工作线程池
        System.out.println("服务器启动成功,监听端口为:" + HostInfo.PORT);
        try {
            // 创建一个服务器端的程序类进行NIO启动,同时可以设置Channel
            ServerBootstrap serverBootstrap = new ServerBootstrap();   // 服务器端
            // 设置要使用的线程池以及当前的Channel类型
            serverBootstrap.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class);
            // 接收到信息之后需要进行处理,于是定义子处理器
            serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
                @Override
                protected void initChannel(SocketChannel socketChannel) throws Exception {
                    socketChannel.pipeline().addLast(new EchoServerHandler()); // 追加了处理器
                }
            });
            // 可以直接利用常量进行TCP协议的相关配置
            serverBootstrap.option(ChannelOption.SO_BACKLOG, 128);
            serverBootstrap.childOption(ChannelOption.SO_KEEPALIVE, true);
            // ChannelFuture描述的是异步回调的处理操作
            ChannelFuture future = serverBootstrap.bind(HostInfo.PORT).sync();
            future.channel().closeFuture().sync();// 等待Socket被关闭
        } finally {
            workerGroup.shutdownGracefully() ;
            bossGroup.shutdownGracefully() ;
        }
    }
}

EchoServerHandler.java

package com.bijian.netty.server.handler;

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 io.netty.util.ReferenceCountUtil;

/**
 * 处理Echo的操作方式,其中ChannelInboundHandlerAdapter是针对于数据输入的处理
 * Netty是基于NIO的一种开发框架的封装,这里面和AIO是没有任何关系的。
 */
public class EchoServerHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        // 当客户端连接成功之后会进行此方法的调用,明确可以给客户端发送一些信息
        byte data [] = "【服务器激活信息】连接通道已经创建,服务器开始进行响应交互。".getBytes() ;
        // NIO是基于缓存的操作,所以Netty也提供有一系列的缓存类(封装了NIO中的Buffer)
        ByteBuf buf = Unpooled.buffer(data.length) ;    // Netty自己定义的缓存类
        buf.writeBytes(data) ; // 将数据写入到缓存之中
        ctx.writeAndFlush(buf) ; // 强制性发送所有的数据
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        try {
            // 表示要进行数据信息的读取操作,对于读取操作完成后也可以直接回应
            // 对于客户端发送来的数据信息,由于没有进行指定的数据类型,所以都统一按照Object进行接收
            ByteBuf buf = (ByteBuf) msg;       // 默认情况下的类型就是ByteBuf类型
            // 在进行数据类型转换的过程之中还可以进行编码指定(NIO的封装)
            String inputData = buf.toString(CharsetUtil.UTF_8);    // 将字节缓冲区的内容转为字符串
            String echoData = "【ECHO】" + inputData; // 数据的回应处理
            // exit是客户端发送来的内容,可以理解为客户端的编码,而quit描述的是一个客户端的结束
            if ("exit".equalsIgnoreCase(inputData)) {    // 进行沟通的端开
                echoData = "quit"; // 结束当前交互
            }
            byte[] data = echoData.getBytes(); // 将字符串变为字节数组
            ByteBuf echoBuf = Unpooled.buffer(data.length);
            echoBuf.writeBytes(data);// 将内容保存在缓存之中
            ctx.writeAndFlush(echoBuf); // 回应的输出操作
        } finally {
            ReferenceCountUtil.release(msg) ; // 释放缓存
        }
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        ctx.close() ;
    }
}

EchoClientMain.java

package com.bijian.netty.client.main;

import com.bijian.netty.client.EchoClient;

public class EchoClientMain {
    public static void main(String[] args) throws Exception {
        new EchoClient().run();
    }
}

EchoClient.java

package com.bijian.netty.client;

import com.bijian.netty.info.HostInfo;
import com.bijian.netty.client.handler.EchoClientHandler;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
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 EchoClient {
    public void run() throws Exception {
        // 1、如果现在客户端不同,那么也可以不使用多线程模式来处理;
        // 在Netty中考虑到代码的统一性,也允许你在客户端设置线程池
        EventLoopGroup group = new NioEventLoopGroup(); // 创建一个线程池
        try {
            Bootstrap client = new Bootstrap(); // 创建客户端处理程序
            client.group(group).channel(NioSocketChannel.class)
                    .option(ChannelOption.TCP_NODELAY, true) // 允许接收大块的返回数据
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel socketChannel) throws Exception {
                            socketChannel.pipeline().addLast(new EchoClientHandler()); // 追加了处理器
                        }
                    });
            ChannelFuture channelFuture = client.connect(HostInfo.HOST_NAME, HostInfo.PORT).sync();
            channelFuture.channel().closeFuture().sync() ; // 关闭连接
        } finally {
            group.shutdownGracefully();
        }
    }
}

EchoClientHandler.java

package com.bijian.netty.client.handler;

import com.bijian.netty.util.InputUtil;
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 io.netty.util.ReferenceCountUtil;

/**
 * 需要进行数据的读取操作,服务器端处理完成的数据信息会进行读取
 */
public class EchoClientHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        // 只要服务器端发送完成信息之后,都会执行此方法进行内容的输出操作
        try {
            ByteBuf readBuf = (ByteBuf) msg ;
            String readData = readBuf.toString(CharsetUtil.UTF_8).trim() ; // 接收返回数据内容
            if("quit".equalsIgnoreCase(readData)) { // 结束操作
                System.out.println("【EXIT】拜拜,您已经结束了本次网络传输,再见!");
                ctx.close() ; // 关闭通道
            } else {
                System.out.println(readData); // 输出服务器端的响应内容
                String inputData = InputUtil.getString("请输入要发送的消息:") ;
                byte [] data = inputData.getBytes() ; // 将输入数据变为字节数组的形式
                ByteBuf sendBuf = Unpooled.buffer(data.length) ;
                sendBuf.writeBytes(data) ; // 将数据保存在缓存之中
                ctx.writeAndFlush(sendBuf) ; // 数据发送
            }
        } finally {
            ReferenceCountUtil.release(msg); // 释放缓存
        }
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        ctx.close() ;
    }
}

  先启动服务端,再启动客户端,测试如下:

2.实现服务器连接状态监听

  修改EchoClient.java如下:

  EchoClient.java

package com.bijian.netty.client;

import com.bijian.netty.info.HostInfo;
import com.bijian.netty.client.handler.EchoClientHandler;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.util.concurrent.Future;
import io.netty.util.concurrent.GenericFutureListener;

public class EchoClient {
    public void run() throws Exception {
        // 1、如果现在客户端不同,那么也可以不使用多线程模式来处理;
        // 在Netty中考虑到代码的统一性,也允许你在客户端设置线程池
        EventLoopGroup group = new NioEventLoopGroup(); // 创建一个线程池
        try {
            Bootstrap client = new Bootstrap(); // 创建客户端处理程序
            client.group(group).channel(NioSocketChannel.class)
                    .option(ChannelOption.TCP_NODELAY, true) // 允许接收大块的返回数据
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel socketChannel) throws Exception {
                            socketChannel.pipeline().addLast(new EchoClientHandler()); // 追加了处理器
                        }
                    });
            ChannelFuture channelFuture = client.connect(HostInfo.HOST_NAME, HostInfo.PORT).sync();
            channelFuture.addListener(new GenericFutureListener() {
                @Override
                public void operationComplete(Future future) throws Exception {
                    if(future.isSuccess()) {
                        System.out.println("服务器连接已经完成,可以确保进行消息的准确传输。");
                    }
                }
            }) ;
            channelFuture.channel().closeFuture().sync() ; // 关闭连接
        } finally {
            group.shutdownGracefully();
        }
    }
}

  先启动服务端,再启客户端,运行效果如下:

posted on 2019-06-15 21:34  bijian1013  阅读(155)  评论(0)    收藏  举报

导航