Netty入门介绍及HelloWorld实例

一切的一切要从网络IO模型说起!

IO模型:BIO/NIO/Netty

BIO(Blocking IO):阻塞IO

早期的Java API(java.net)提供了由本地系统套接字库提供的所谓的阻塞函数,样例代码如下:

ServerSocket serverSocket = new ServerSocket(portNumber);
Socket clientSocket = serverSocket.accept();
BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
PrintWriter out =new PrintWriter(clientSocket.getOutputStream(), true);
String request, response;
while ((request = in.readLine()) != null) {
    if ("Done".equals(request)) {
        break;
}
response = processRequest(request);
out.println(response);
}

这段代码片段将只能同时处理一个连接,要管理多个并发客户端,需要为每个新的客户端

Socket 创建一个新的 Thread,线程模型如下图所示:

 

该种模型存在以下两个问题:

1. 在任何时候都可能有大量的线程处于休眠状态,只是等待输入或者输出数据就绪,这可能算是一种资源浪费

2. 需要为每个线程的调用栈都分配内存

3. 即使 Java 虚拟机(JVM) 在物理上可以支持非常大数量的线程, 但是远在到达该极限之前, 上下文切换所带来的开销就会带来麻烦

NIO(Non Blocking IO):非阻塞IO

Java的NIO特性在JDK 1.4中引入,其结构如下:

从该图可以看出Selector 是Java 的非阻塞 I/O 实现的关键。它使用了事件通知 API

以确定在一组非阻塞套接字中有哪些已经就绪能够进行 I/O 相关的操作。因为可以在任何的时间检查任意的读操作或者写操作的完成状态。该种模型下,一个单一的线程便可以处理多个并发的连接。

与BIO相比,该模型有以下特点:

1. 使用较少的线程便可以处理许多连接,因此也减少了内存管理和上下文切换所带来开销

2. 当没有 I/O 操作需要处理的时候,线程也可以被用于其他任务

 

虽然Java 的NIO在性能上比BIO已经相当的优秀,但是要做到如此正确和安全并

不容易。特别是,在高负载下可靠和高效地处理和调度 I/O 操作是一项繁琐而且容易出错的任务,此时就时Netty上场的时间了。

NIO的通信步骤:

①创建ServerSocketChannel,为其配置非阻塞模式。

②绑定监听,配置TCP参数,录入backlog大小等。

③创建一个独立的IO线程,用于轮询多路复用器Selector。

④创建Selector,将之前创建的ServerSocketChannel注册到Selector上,并设置监听标识位SelectionKey.OP_ACCEPT。

⑤启动IO线程,在循环体中执行Selector.select()方法,轮询就绪的通道。

⑥当轮询到处于就绪状态的通道时,需要进行操作位判断,如果是ACCEPT状态,说明是新的客户端接入,则调用accept方法接收新的客户端。

⑦设置新接入客户端的一些参数,如非阻塞,并将其继续注册到Selector上,设置监听标识位等。

⑧如果轮询的通道标识位是READ,则进行读取,构造Buffer对象等。

⑨更细节的问题还有数据没发送完成继续发送的问题......

Netty框架

Netty是最流行的NIO框架。高并发--用netty

Netty对NIO的API进行了封装,通过以下手段让性能又得到了一定程度的提升

1. 使用多路复用技术,提高处理连接的并发性

2. 零拷贝:

1. Netty的接收和发送数据采用DIRECT BUFFERS,使用堆外直接内存进行Socket读写,不需要进行字节缓冲区的二次拷贝

2. Netty提供了组合Buffer对象,可以聚合多个ByteBuffer对象进行一次操作

3. Netty的文件传输采用了transferTo方法,它可以直接将文件缓冲区的数据发送到目标Channel,避免了传统通过循环write方式导致的内存拷贝问题

3. 内存池:为了减少堆外直接内存的分配和回收产生的资源损耗问题,Netty提供了基于内存池的缓冲区重用机制

4. 使用主从Reactor多线程模型,提高并发性

5. 采用了串行无锁化设计,在IO线程内部进行串行操作,避免多线程竞争导致的性能下降

6. 默认使用Protobuf的序列化框架

7. 灵活的TCP参数配置

阿里的RPC框架Dubbo 底层用的就是netty

Netty架构组成:

 

Netty通信的步骤:

①创建两个NIO线程组,一个专门用于网络事件处理(接受客户端的连接),另一个则进行网络通信的读写。

②创建一个ServerBootstrap对象,配置Netty的一系列参数,例如接受传出数据的缓存大小等。

③创建一个用于实际处理数据的类ChannelInitializer,进行初始化的准备工作,比如设置接受传出数据的字符集、格式以及实际处理数据的接口。

④绑定端口,执行同步阻塞方法等待服务器端启动即可。

服务器端:

public class Server {
 
    private int port;
 
    public Server(int port) {
        this.port = port;
    }
 
    public void run() {
        EventLoopGroup bossGroup = new NioEventLoopGroup(); //用于处理服务器端接收客户端连接
        EventLoopGroup workerGroup = new NioEventLoopGroup(); //进行网络通信(读写)
        try {
            ServerBootstrap bootstrap = new ServerBootstrap(); //辅助工具类,用于服务器通道的一系列配置
            bootstrap.group(bossGroup, workerGroup) //绑定两个线程组
                    .channel(NioServerSocketChannel.class) //指定NIO的模式
                    .childHandler(new ChannelInitializer<SocketChannel>() { //配置具体的数据处理方式
                        @Override
                        protected void initChannel(SocketChannel socketChannel) throws Exception {
                            socketChannel.pipeline().addLast(new ServerHandler());
                        }
                    })
                    /**
                     * 对于ChannelOption.SO_BACKLOG的解释:
                     * 服务器端TCP内核维护有两个队列,我们称之为A、B队列。客户端向服务器端connect时,会发送带有SYN标志的包(第一次握手),服务器端
                     * 接收到客户端发送的SYN时,向客户端发送SYN ACK确认(第二次握手),此时TCP内核模块把客户端连接加入到A队列中,然后服务器接收到
                     * 客户端发送的ACK时(第三次握手),TCP内核模块把客户端连接从A队列移动到B队列,连接完成,应用程序的accept会返回。也就是说accept
                     * 从B队列中取出完成了三次握手的连接。
                     * A队列和B队列的长度之和就是backlog。当A、B队列的长度之和大于ChannelOption.SO_BACKLOG时,新的连接将会被TCP内核拒绝。
                     * 所以,如果backlog过小,可能会出现accept速度跟不上,A、B队列满了,导致新的客户端无法连接。要注意的是,backlog对程序支持的
                     * 连接数并无影响,backlog影响的只是还没有被accept取出的连接
                     */
                    .option(ChannelOption.SO_BACKLOG, 128) //设置TCP缓冲区
                    .option(ChannelOption.SO_SNDBUF, 32 * 1024) //设置发送数据缓冲大小
                    .option(ChannelOption.SO_RCVBUF, 32 * 1024) //设置接受数据缓冲大小
                    .childOption(ChannelOption.SO_KEEPALIVE, true); //保持连接
            ChannelFuture future = bootstrap.bind(port).sync();
            future.channel().closeFuture().sync();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            workerGroup.shutdownGracefully();
            bossGroup.shutdownGracefully();
        }
    }
 
    public static void main(String[] args) {
        new Server(8379).run();
    }
}

ServerHandler类:

public class ServerHandler  extends ChannelHandlerAdapter {
 
	@Override
	public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
	
			//do something msg
			ByteBuf buf = (ByteBuf)msg;
			byte[] data = new byte[buf.readableBytes()];
			buf.readBytes(data);
			String request = new String(data, "utf-8");
			System.out.println("Server: " + request);
			//写给客户端
			String response = "我是反馈的信息";
			ctx.writeAndFlush(Unpooled.copiedBuffer("888".getBytes()));
			//.addListener(ChannelFutureListener.CLOSE);
			
 
	}
 
	@Override
	public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
		cause.printStackTrace();
		ctx.close();
	}
 
}

客户端:

public class Client {
 
    public static void main(String[] args) throws InterruptedException {
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        Bootstrap bootstrap = new Bootstrap();
        bootstrap.group(workerGroup)
                .channel(NioSocketChannel.class)
                .handler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel socketChannel) throws Exception {
                        socketChannel.pipeline().addLast(new ClientHandler());
                    }
                });
        ChannelFuture future = bootstrap.connect("127.0.0.1", 8379).sync();
        future.channel().writeAndFlush(Unpooled.copiedBuffer("777".getBytes()));
        future.channel().closeFuture().sync();
        workerGroup.shutdownGracefully();
    }
 
}

ClientHandler类:

public class ClientHandler extends ChannelHandlerAdapter {
 
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        try {
            ByteBuf buf = (ByteBuf) msg;
            byte[] data = new byte[buf.readableBytes()];
            buf.readBytes(data);
            System.out.println("Client:" + new String(data).trim());
        } finally {
            ReferenceCountUtil.release(msg);
        }
    }
 
 
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        ctx.close();
    }
 
}

TCP粘包、拆包问题

TCP是一个“流”协议,所谓流就是没有界限的遗传数据。大家可以想象一下,如果河水就好比数据,他们是连成一片的,没有分界线,TCP底层并不了解上层业务数据的具体含义,它会根据TCP缓冲区的具体情况进行包的划分,也就是说,在业务上一个完整的包可能会被TCP分成多个包进行发送,也可能把多个小包封装成一个大的数据包发送出去,这就是所谓的粘包/拆包问题。

解决方案:

①消息定长,例如每个报文的大小固定为200个字节,如果不够,空位补空格。

②在包尾部增加特殊字符进行分割,例如加回车等。

③将消息分为消息头和消息体,在消息头中包含表示消息总长度的字段,然后进行业务逻辑的处理。

Netty中解决TCP粘包/拆包的方法:

①分隔符类:DelimiterBasedFrameDecoder(自定义分隔符)

②定长:FixedLengthFrameDecoder

总结

网络通信有两种类型:阻塞IO和非阻塞IO,Netty对NIO进行了封装改进。

RPC通信涉及两个技术:1.对象转字节;2.socket传输。

Netty常常被用作高性能网络通讯,在微服务架构中的RPC调用被应用。

猜你喜欢

转载自blog.csdn.net/x18094/article/details/114745014