Nio和Netty

Netty

(Netty基本学习已完结,SpringBoot整合Netty、Sharding jdbc项目netty_taxi源码放置在笔记相关实战中。)

Netty介绍

  • Netty是由JBoss提供的一个java的开源框架,现为GitHub上的独立项目;
  • Netty是一个异步的、基于事件驱动的网络应用框架,用于快速开发高性能、高可靠性的网络IO程序;
  • Netty主要针对于TCP协议下,面向Clients端的高并发应用,或者Peer-to-Peer场景下的大量数据持续传输的应用;
  • Netty本质是一个NIO框架,适用于服务器通讯相关的多种应用场景。

关于异步模型:最简单的案例类似于B/S架构中浏览器客户端的ajax请求,当使用表单同步提交的时候,浏览器中发起请求之后会进入一个等待阻塞状态,待服务端响应之后进行下一步操作。而ajax实现了浏览器客户端的异步请求,客户端程序的运行不依赖于服务端端响应,发起请求之后客户端的程序可以继续运行,并且可以同时发送其他的请求。

Peer-to-Peer:对等的,意指两台计算机之间进行无中间转发的直接连接,并且计算机之间的角色是对等的,彼此既是客户端又是服务端。
(关于客户端和服务端对等的理解:在web开发中接触的是http协议,由客户端去主动请求服务端,服务端来进行响应,服务端是无法主动推送数据到客户端的。而TCP协议则不同,客户端和服务端通过TCP协议建立长链接,客户端可以发送数据到服务端,服务端也可以推送数据到客户段,也就是所谓对等。)

Netty与TCP/IP的关系:java的基础io封装是基于TCP/IP协议来进行的,NIO技术是基于java的io基础,Netty是对NIO的优质的封装实现。

Java IO模型

IO模型的简单理解,就是用什么样的通道进行数据的发送和接收,很大程度上决定了程序通信的性能。

java共支持三种网络编程/IO模型:BIO、NIO和AIO

  • Java BIO:同步并阻塞,服务器的实现模式是一个连接一个线程,即客户端有连接请求的时候服务端就需要启动一个线程进行处理,如果这个连接不做任何事情就会造成不必要的线程开销,并且这个线程是阻塞的;

  • Java NIO:同步非阻塞,服务器实现模式为一个线程处理多个连接,即客户端发送的请求都会注册到多路复用器上,多路复用器轮询到有I/O请求就进行处理;

  • Java AIO:异步非阻塞,AIO引入了异步通道的概念,采用了Proactor模式,简化了程序编写,有效的请求才启动线程,它的特点是先由操作系统完成后才通知服务端程序启动线程去处理,一般适用于连接数较多且连接时间较长的应用。

AIO是jdk1.7之后新加入的IO模型,目前暂未得到广泛应用,在此仅作了解......

BIO实例

接下来使用java本身的网络编程实现BIO的服务端,以体验阻塞IO的特点,并且希望通过使用线程池技术实现多个客户端连接服务端。

package com.xsh.netty.bio;

import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * @author yuanye
 */
public class BIOServer {
    public static void main(String[] args) throws IOException {

        //创建线程池
        ExecutorService pool = Executors.newCachedThreadPool();

        //启动服务端并监听6666端口
        ServerSocket server = new ServerSocket(6666);
        System.out.println("服务端已启动");

        while (true){

            System.out.println("等待客户端连接......");
            //获取一个客户端的连接信息
            final Socket socket = server.accept();

            //当有一个连接进入当时候就启动一个线程去处理他
            pool.execute(() -> {
                handler(socket);
            });
        }

    }

    /**
     * 服务端读取客户端内容的方法
     * @param socket
     */
    public static void handler(Socket socket){

        try {
            //创建byte数组用于存储客户端所传递的信息
            byte[] bytes = new byte[1024];

            InputStream inputStream = socket.getInputStream();
            while (true){

                //打印线程信息
                System.out.println("线程id:" + Thread.currentThread().getId() + ",线程名称:" + Thread.currentThread().getName());

                int read = inputStream.read(bytes);
                if (read == -1) {
                    break;
                }else {
                    System.out.println(new String(bytes, 0 , read));
                }
            }

        }catch (Exception e){
            e.printStackTrace();
        }finally {
            //释放资源
            try {
                socket.close();
            }catch (Exception e){
                e.printStackTrace();
            }
        }

    }

}

以上便是一个java BIO服务端的简单实现,可以通过mac终端使用telnet ip 端口进行连接然后发送消息,开启多个终端可以展示多个客户端连接服务端。

BIO是一个同步的阻塞IO模型,一个客户端连接就回启动一个线程,并且当服务端使用socket.accept()方法等待客户端连接和使用inputStream.read(bytes)读取客户端消息的时候都会进行阻塞,意思是,当服务端启动的时候就会启动一个线程来等待客户端的连接,如果没有客户端连接,则线程会阻塞在socket.accept()的位置;如果有客户端连接但是没有发送消息,则服务端对应的线程会阻塞在inputStream.read(bytes)的位置。

关于案例线程池的使用: Executors.newCachedThreadPool()会创建一个可缓存的、灵活的线程池,会自动回收空闲线程来执行任务,当任务过多无线程可回收利用的时候会创建新的线程。

NIO详解

  • java NIO全称 java non-blocking IO,是指JDK提供的新API。从jdk1.4开始,java提供了一系列改进的输入/输出的新特性,被统称为NIO,是同步的,非阻塞的
  • NIO相关的类都被放在java.nio及子包下,并且对原io包中很多类进行改写;
  • NIO有三大核心部分:Channel(通道),Buffer(缓冲区),Selector(选择器)
  • NIO是面向缓冲区,面向块编程的。将数据读取到一个稍后处理的缓冲区,需要时可以在缓冲区中前后移动,这就增加了处理过程中的灵活性,使用它可以提供非阻塞式的高伸缩性网络。

NIO完整示意图:


Java NIO的非阻塞模式,使客户端与服务端的沟通通过通道和缓冲区来进行,当客户端进行读写操作的时候,如果没有任何数据可以读取,线程也不会进行阻塞,直到有可用数据之前,线程都可以去做其他的事情。通俗来讲,NIO是可以做到一个线程来管理多个通道,处理多个任务的,当有10000个客户端连接的时候,如果是BIO模型则服务端必须使用10000个线程去处理它,而NIO或许只需要50-100个线程就可以管理这些客户端任务。

初识Buffer

Buffer其实就是nio包中的一个类,其有不同数据类型的不同子类实现,可以理解为缓存各种类型数据的容器,接下来就通过一个最基础的案例初步熟悉Buffer的使用。

package com.xsh.netty.nio;

import java.nio.Buffer;
import java.nio.IntBuffer;

public class FirstBuffer {
    public static void main(String[] args) {

        //初始化一个buffer并赋值长度
        IntBuffer buffer = IntBuffer.allocate(5);

        /**
         * buffer.capacity()返回buffer的最大长度
         */
        for (int i = 0; i < buffer.capacity(); i++) {
            buffer.put(i);
        }

        //翻转buffer,buffer是可读可写的,在写数据之后读需要将buffer翻转为读的模式
        buffer.flip();

        /**
         * buffer.hasRemaining()返回buffer是否还存在内容
         * buffer.get()内部会维护索引,并自动逐个读取buffer内容
         */
        while (buffer.hasRemaining()){
            System.out.println(buffer.get());
        }
    }
}

Buffer详解

Buffer本质上是一个可以读写数据的内存块,可以理解成是一个含数组的容器对象,该对象提供了一组方法,可以更轻松的使用内存块。缓冲区内置的机制可以追踪和记录缓冲区的状态变化情况。Channel提供从文件、网络读取数据的渠道,但是读取或写入的数据都必须经过Buffer。

Buffer及其子类

在NIO中,Buffer是一个顶层父类,它是一个抽象类,子类分别是各种数据类型的具体buffer实现,例如:ByteBuffer、ShortBuffer、CharBuffer、IntBuffer、LongBuffer、DoubleBuffer、FloatBuffer......

Buffer类定义了所有的缓冲区都具有的四个属性来提供关于其所包含的数据元素的信息:

  • capacity:标识buffer的最大容量,在缓存区创建的时候设定并且不允许改变;
  • limit:表示缓冲区的当前终点,不能对缓冲区超过极限的位置进行读写操作,并且该极限位置是可变化的,因为buffer是支持读写的;
  • position:表示缓冲区中下一个被读或写的元素的索引,每次读写缓冲区的时候都会改变这个值,为下一次读写做准备;
  • mark:标记

接下来,我们通过debug之前写的“初识Buffer”的代码,来感受这四个参数的变化:

1、初始化Buffer


如上图所示,创建了一个长度为5的IntBuffer,可以看到debug中的buffer的四个参数也进行了初始化,此刻hb表示缓冲区存放数据的目标数组,capacity表示数组的最大长度,limit表示当前缓冲区的终点,因为没有插入数据,所以position标记是位于数组的初始坐标位置。

2、Buffer赋值


可以看到,buffer经过循环写入之后,position跟随写入的步伐变成了5。

3、Buffer翻转

buffer翻转需要通过一个特殊的flip()方法,其源码如下:

    public final Buffer flip() {
        limit = position;
        position = 0;
        mark = -1;
        return this;
    }

buffer翻转的目的是为了将buffer从写入状态切换到读取状态,可以看到,翻转的第一步是将position赋值给limit,意为读数据的时候,当前数组中数据的长度就应当为可读取的最大长度;第二步是将position = 0,这样做是保证了读取数据的时候是按照写入的顺序从数组的开始坐标进行读取。

4、数据读取


可以看到,读取数据的时候buffer会按照position从0开始逐个的读取数据。

Buffer常用方法

  • public final int capacity():返回此缓冲区的容量;
  • public final int position():返回此缓冲区的位置;
  • public final Buffer position(int newPosition):设置此缓冲区的位置;
  • public final int limit():返回此缓冲区的限制;
  • public final Buffer limit(int newLimit):设置此缓冲区的限制;
  • public final Buffer mark():在此缓冲区的位置设置标记;
  • public final Buffer reset():将此缓冲区的位置重置为之前标记的位置;
  • public final Buffer clear():清除缓冲区,将各个标记回复到初始状态,但是并不会清除数据,数据会中重新写入的时候被覆盖;
  • public final Buffer flip():翻转缓冲区,读翻转为写,写翻转为读;
  • public final Buffer rewind():重绕此缓冲区;
  • public final int remaining():返回当前位置与限制位置之间的元素数;
  • public final boolean hasRemaining():告知当前位置和限制位置之间是否有元素,可以用于判断缓冲区内是否还存在数据;
  • public abstract boolean isReadOnly():告知此缓冲区是否为只读缓冲区;
  • public abstract boolean hasArray():告知此缓冲区是否具有可访问的底层实现数组;
  • public abstract Object array():返回缓冲区的底层实现数组;
  • public abstract int arrayOffset():返回此缓冲区的底层实现数组中第一个缓冲区元素的偏移量;
  • public abstract boolean isDirect():告知缓冲区是否为直接缓冲区。
ByteBuffer

网络编程之间数据的传输都是以二进制的形式传输的,所以中Buffer的所有实现中,字节缓冲区ByteBuffer就是最常用的Buffer,其常用方法如下:

  • public static ByteBuffer allocateDirect(int capacity):创建直接缓冲区;
  • public static ByteBuffer allocate(int capacity):设置缓冲区的初始容量;
  • public static ByteBuffer wrap(byte[] array):把一个数组放到缓冲区中使用;
  • public static ByteBuffer wrap(byte[] array,int offset,int length):构造初始化位置offset和上界length的缓冲区;
  • public abstract byte get():从当前position上get,get之后,position会自动加一;
  • public abstract byte get(int index):从指定位置get,position不会发生变化;
  • public abstract ByteBuffer put(byte b):从当前position上put,put之后,position会自动加一;
  • public abstract ByteBuffer put(int index,byte b) :从指定位置put,position不会发生变化。

Channel详解

NIO中的Channel类似于流,但是Channel可以同时进行读写,而流同一时间只能读或者只能写;Channel可以实现异步读写数据,可以从Buffer中读取数据,也可以写入数据到Buffer中。

Channel在NIO中是一个接口:

public interface Channel extends Closeable {}

其常用的实现类有:FileChannel、DatagramChannel、ServerSocketChannel和SocketChannel。

FileChannel用于文件的数据读写,DatagramChannel用于UDP的数据读写,ServerSocketChannel和SocketChannel用于TCP的数据读写。

ServerSocketChannel

ServerSocketChannel是中服务端监听客户端Socket连接的,常用方法如下:

  • public static ServerSocketChannel open():得到一个ServerSocketChannel;
  • public final ServerSocketChannel bind(SocketAddress local):设置服务器端口;
  • public final SelectableChannel configureBlocking(boolean block):设置阻塞或者非阻塞模式,取值false采用非阻塞模式;
  • public abstract SocketChannel accept():接受一个连接,返回代表这个连接的通道对象;
  • public final SelectionKey register(Selector sel, int ops):将通道注册到选择器上并设置监听的事件类型。
SocketChannel

SocketChannel是一个网络IO通道,具体进行服务器端和客户端之间的读写操作。将缓冲区中的数据写入通道,或者将通道中的数据读取到缓冲区,其主要方法如下:

  • public static ServerSocketChannel open():得到一个SocketChannel;
  • public final SelectableChannel configureBlocking(boolean block):设置阻塞或者非阻塞模式,取值false采用非阻塞模式;
  • public abstract boolean connect(SocketAddress remote):连接到服务器端;
  • public abstract boolean finishConnect():当SocketChannel处于非阻塞状态时,先使用connect()进行连接,如果不能立即连接成功则会返回false,此时就要使用finishConnect()来完成连接。客户端对服务端端连接要从connect()开始,到finishConnect()结束,如果connect()连接成功,则finishConnect()会立即返回true。当在阻塞模式中,如果连接失败会进入阻塞,直到连接成功;
  • public abstract int read(ByteBuffer dst):将通道中的数据读取到Buffer中;
  • public abstract int write(ByteBuffer src):将Buffer中的数据写入到通道中;
  • public final SelectionKey register(Selector sel, int ops):将通道注册到选择器上并设置监听的事件类型。
FileChannel

FileChannel主要用来对本地文件进行IO操作,常见的方法有:

  • public int read(ByteBuffer buffer):从通道读取数据并放到缓冲区中;
  • public int write(ByteBuffer buffer):把缓存区的数据写到通道中;
  • public long transferFrom(ReadableByteChannel src,long position,long count):从目标通道中复制数据到当前通道;
  • public long transferTo(long position,long count,WritableByteChannel target):把数据从当前通道复制给目标通道。

综合案例:

1、通过FileChannel和ByteBuffer实现将数据写入到磁盘文件中。

需要明确的是,NIO的Channel仍然是中jdk io流的基础上的封装,在这个案例中最后仍然是使用jdk io的技术来实现数据的写入。

    public static void main(String[] args) throws Exception {

        String str = "first netty channel";

        //创建io流并且获取channel
        FileOutputStream fileOutputStream = new FileOutputStream("/Users/yuanye/Documents/fileChannel.txt");
        FileChannel channel = fileOutputStream.getChannel();

        //创建buffer将文件数据str写入buffer
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        buffer.put(str.getBytes());

        //翻转buff,由写变读
        buffer.flip();

        //将buffer数据读取出来并写入到channel中,最后由channel中的io流完成写入操作
        channel.write(buffer);
        fileOutputStream.close();

    }

2、通过FileChannel和一个ByteBuffer实现文件的复制

    public static void main(String[] args) throws Exception {
        FileInputStream inputStream = new FileInputStream("/Users/yuanye/Documents/fileChannel.txt");
        FileChannel channel = inputStream.getChannel();

        FileOutputStream outputStream = new FileOutputStream("/Users/yuanye/Desktop/fileChannel.txt");
        FileChannel outChannel = outputStream.getChannel();

        ByteBuffer buffer = ByteBuffer.allocate(5);

        while (true){
            buffer.clear();
            int read = channel.read(buffer);

            if(read == -1){
                break;
            }

            buffer.flip();
            outChannel.write(buffer);
//            buffer.flip();
        }

        inputStream.close();
        outputStream.close();
    }

循环读写的必要性:当缓存区过小,文件内容过大的时候,一次read是无法将所有数据都读取完毕的,所以需要循环读写;

buffer.clear()的必要性,buffer读取之后如果不进行clear,则相当于buffer处于一个满负载状态,read()方法无法对其中写入数据,所以channel的读取永远读不完,循环的退出条件不会成立;

buffer.clear()本质上是buffer的标识位复位的操作,也可以通过buffer.flip()方法来实现,总的来讲就是需要将写完数据的缓冲区标识位回归到初识的位置,让下一个read()写入数据。

3、使用transferFrom()复制通道完成文件复制

    public static void main(String[] args) throws Exception {
        FileInputStream inputStream = new FileInputStream("/Users/yuanye/Documents/fileChannel.txt");
        FileChannel channel = inputStream.getChannel();

        FileOutputStream outputStream = new FileOutputStream("/Users/yuanye/Desktop/fileChannel.txt");
        FileChannel outChannel = outputStream.getChannel();

        outChannel.transferFrom(channel,0,channel.size());
        inputStream.close();
        outputStream.close();
    }

Buffer和Channel使用拓展

1、ByteBuffer支持类型化的put和get操作,放入的是什么数据类型,取出的时候就要用对应的git方法取出,否则会出现数据错误或者导致BufferUnderflowException异常;

2、可以将一个普通Buffer转换为只读Buffer,通过asReadOnlyBuffer()方法可以获取一个只读属性的Buffer;

3、Buffer的分散(Scattering)和聚集(Gathering):在将数据写入Buffer的时候,可以使用多个Buffer组成的数组的形式依次写入,从Buffer中读取数据的时候也一样,称之为Buffer的分散和聚集;

Selector详解

  • Java的NIO用非阻塞的IO方式,可以用一个线程,处理多个客户端连接,其中就会用到选择器(Selector);
  • Selector能够检测多个注册的通道上是否有事件发生(多个Channel可以以事件的方式注册到同一个Selector),如果有事件发生,便获取事件并且对每个事件进行相应的处理,这样就可以使用一个单线程去处理多个通道,也就是管理多个连接和请求;
  • 只有在连接有真正的读写事件发生时,才会进行读写,就大大的减少了系统的开销,并且不必为每个连接都创建一个线程,不用去维护多个线程,同时也避免了多个线程之间上下文切换导致的开销。

Selector是一个抽象类:

public abstract class Selector implements Closeable {}

其核心常用方法如下:

  • public static Selector open():得到一个选择器对象;
  • public abstract int select():获取选择器监听的有事件发生的通道个数,该方法是阻塞的,如果选择器监听的通道没有发生事件则会阻塞,直到监听的通道有事件发生为止;
  • public abstract int select(long timeout):获取选择器监听的有事件发生的通道个数,如果选择器监听的通道都没有事件发生,则等待参数大小时间之后结束方法;
  • public abstract int selectNow():获取选择器监听的有事件发生的通道个数,该方法是非阻塞的,不管选择器监听的通道是否有事件发生都会直接结束;
  • public abstract Set<SelectionKey> selectedKeys():获取选择器监听的发生事件的通道的SelectionKey。
    • SelectionKey是通道对应的标识id,选择器和线程通过SelectionKey来获取通道信息

NIO非阻塞网络编程结构原理分析

  • 当客户端连接服务端端时候,会通过ServerSocketChannel获得一个SocketChannel。SocketChannel会注册到Selector上,一个Selector可以注册多个SocketChannel;
    • SocketChannel通过public final SelectionKey register(Selector sel, int ops)方法注册到Selector上,register方法有两个参数,第一个参数是指要注册到目标Selector,第二个参数是告知Selector要监听的事件类型,一共有OP_READ读、OP_WRITE写、OP_CONNECT连接、OP_ACCEPT初次连接四种类型;
  • 注册之后Channel和Selector通过SelectionKey关联,Selector可以通过select()获取有事件发生的通道的个数,并且可以通过SelectionKey的public abstract SelectableChannel channel()得到Channel完成业务处理。

NIO实例

1、使用NIO实现服务端和客户端的数据简单通讯。

server实例:

    public static void main(String[] args) throws Exception {

        //获取ServerSocketChannel
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        //绑定端口
        serverSocketChannel.bind(new InetSocketAddress(6666));
        //指定为非阻塞模式
        serverSocketChannel.configureBlocking(false);

        //获取Selector
        Selector selector = Selector.open();
        //将ServerSocketChannel注册到Selector,并且设置其关注初次连接的事件
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

        while (true){

            if(selector.select(3000) == 0){
                System.out.println("server 已监听超过3s,无client事件......");
                continue;
            }

            //获取监听到到client事件
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            Iterator<SelectionKey> keyIterator = selectionKeys.iterator();

            while (keyIterator.hasNext()){
                SelectionKey selectionKey = keyIterator.next();

                if(selectionKey.isAcceptable()){
                    //如果事件是初次连接则分配对应到SocketChannel
                    SocketChannel socketChannel = serverSocketChannel.accept();
                    socketChannel.configureBlocking(false);
                    socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
                    System.out.println("客户端连接成功,生成了一个SocketChannel " + socketChannel.hashCode());
                }
                if(selectionKey.isReadable()){
                    //如果事件是读的事件则从SocketChannel中读取数据
                   SocketChannel socketChannel = (SocketChannel)selectionKey.channel();
                   ByteBuffer buffer = (ByteBuffer) selectionKey.attachment();
                   socketChannel.read(buffer);

                    System.out.println("form client " + new String(buffer.array()));
                }

                //处理完client事件后要删除SelectionKey,因为实际上NIO迭代循环是一个多线程的操作,要避免重复处理
                keyIterator.remove();

            }


        }
    }

client实例

    public static void main(String[] args) throws Exception{

        //获取socketChannel
        SocketChannel socketChannel = SocketChannel.open();
        //指定连接为非阻塞模式
        socketChannel.configureBlocking(false);

        //连接服务端
        if(!socketChannel.connect(new InetSocketAddress("127.0.0.1",6666))){
            //连接失败
            while (!socketChannel.finishConnect()){
                System.out.println("连接服务端失败......");
            }
        }

        //连接成功,发送数据
        String str = "hello,NIO";
        //ByteBuffer.wrap()根据参数的byte数组长度创建相同长度的buffer
        ByteBuffer buffer = ByteBuffer.wrap(str.getBytes());
        socketChannel.write(buffer);

        System.out.println("数据发送完毕......");
//        System.in.read();

    }

2、实现简易版群聊系统

server实例:

package com.xsh.netty.nio.groupchat;

import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Set;

public class GroupChatServer {

    private Selector selector;
    private ServerSocketChannel serverSocketChannel;
    private static final int PORT = 8080;

    /**
     * 属性初始化
     */
    public GroupChatServer() {
        try {
            selector = Selector.open();
            serverSocketChannel = ServerSocketChannel.open();
            serverSocketChannel.configureBlocking(false);
            serverSocketChannel.bind(new InetSocketAddress(PORT));
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private void monitor() {
        try {
            while (true) {
                if (selector.select(3000) == 0) {
                    continue;
                }

                Iterator<SelectionKey> keyIterator = selector.selectedKeys().iterator();
                while (keyIterator.hasNext()) {
                    SelectionKey key = keyIterator.next();

                    if (key.isAcceptable()) {
                        SocketChannel socketChannel = serverSocketChannel.accept();
                        socketChannel.configureBlocking(false);
                        socketChannel.register(selector, SelectionKey.OP_READ);
                        System.out.println("客户端主机 " + socketChannel.getRemoteAddress() + "已连接......");
                    }

                    if (key.isReadable()) {
                        readMes(key);
                    }

                    keyIterator.remove();
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private void readMes(SelectionKey key) {
        SocketChannel socketChannel = null;
        try {
            socketChannel = (SocketChannel) key.channel();
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            int read = socketChannel.read(buffer);

            if (read > 0) {
                String msg = new String(buffer.array());
                System.out.println("客户端发送消息:" + msg);

                //转发消息到其他客户端
                transferMsg(msg, socketChannel);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private void transferMsg(String msg, SocketChannel self) throws Exception {
        Set<SelectionKey> keys = selector.keys();
        for (SelectionKey key : keys) {
            SelectableChannel channel = key.channel();
            if (channel instanceof SocketChannel && channel != self) {
                SocketChannel socketChannel = (SocketChannel) channel;
                socketChannel.write(ByteBuffer.wrap(msg.getBytes()));
            }
        }

    }

    public static void main(String[] args) {
        //启动服务器端
        GroupChatServer server = new GroupChatServer();
        new Thread(() -> {
            server.monitor();
        }).start();

    }

}

client实例:

package com.xsh.netty.nio.groupchat;

import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Scanner;

public class GroupChatClient {

    private Selector selector;
    private SocketChannel socketChannel;
    private static final String HOST = "127.0.0.1";
    private static final int PORT = 8080;

    /**
     * 初始化元素
     */
    public GroupChatClient() {
        try {
            selector = Selector.open();
            socketChannel = SocketChannel.open(new InetSocketAddress(HOST, PORT));
            socketChannel.configureBlocking(false);
            socketChannel.register(selector, SelectionKey.OP_READ);

            System.out.println(socketChannel.getLocalAddress() + " is ok");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 发送数据
     */
    public void send(String msg){
        try {
            socketChannel.write(ByteBuffer.wrap(msg.getBytes()));
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    /**
     * 接受数据
     */
    public void read(){
        try {
            while (true){
                if(selector.select(3000) == 0){
                    continue;
                }

                Iterator<SelectionKey> keyIterator = selector.selectedKeys().iterator();
                while (keyIterator.hasNext()){
                    SelectionKey key = keyIterator.next();
                    if(key.isReadable()){
                        SocketChannel socketChannel = (SocketChannel) key.channel();
                        ByteBuffer buffer = ByteBuffer.allocate(1024);
                        int count = socketChannel.read(buffer);

                        if(count > 0){
                            System.out.println("client read msg : " + new String(buffer.array()));
                        }
                    }

                    keyIterator.remove();
                }
            }
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        //启动客户端
        GroupChatClient client = new GroupChatClient();

        //读取数据
        new Thread(() -> {
            client.read();
        }).start();

        //扫描控制台输入,发送数据
        Scanner scanner = new Scanner(System.in);
        while (scanner.hasNextLine()){
            client.send(scanner.nextLine());
        }
    }

}

NIO与零拷贝

零拷贝的基本介绍

  • 零拷贝是网络编程的关键,很多的性能优化都是依赖于零拷贝;
  • 在java程序中,常用的零拷贝技术分为mmap(内存映射)和sendFile;
    • mmap通过内存映射,将文件映射到内存缓冲区,同时,用户空间可以共享内核空间的数据。这样,在进行网络传输时,就可以减少内核空间到用户控件的拷贝次数;
    • Linux2.1版本提供了sendFile函数,其基本原理是数据根本不经过用户态,直接从内核缓冲区进入到socketBuffer,同时由于和用户态完全无关,就减少了一次上下文切换;
    • Linux在2.4版本中对于零拷贝做了一些修改,避免了从内核缓冲区拷贝到socketBuffer的操作,直接拷贝到协议栈,从而再一次减少了数据拷贝的次数;
  • 零拷贝技术在NIO中的具体体现在transferTo函数,transferTo函数与操作系统相关,在windows环境中传输文件最多只能传输的大小为8M,需要进行分段传输处理.....

零拷贝是针对操作系统角度的概念,并不是说绝对性的解决了数据在内存中的复制操作,只是针对传统的IO操作有巨大的提升。对于操作系统而言,从磁盘中拷贝数据到内存的操作是不可避免的。

对于零拷贝和IO的深刻认识在此讲述的比较片面,如果想要比较全面学习需要额外寻找资料......

零拷贝详解: https://zhuanlan.zhihu.com/p/258513662

Netty概述

原生NIO存在的问题

  • NIO的类库和API繁杂,需要熟练掌握Selector、ServerSocketChannel、SocketChannel、ByteBuffer等;
  • 需要具备其他额外技能:要熟悉java多线程网络编程,因为NIO涉及到Reactor模式(反应器模式),需要对多线程和网络编程非常熟悉,才能写出高质量的NIO代码;
  • 开发工作量和难度非常大,例如客户端面临断连重连、网络闪断、半包读写、失败缓存、网络拥塞和异常流的处理等等;
  • JDK NIO的BUG:例如臭名昭著的Epoll Bug,它会导致Selector空轮询,最终导致CPU100%,直到JDK1.7版本问题依旧存在,没有被根本解决。

线程模型

不同的线程模型,对程序的性能影响很大,为了理解Netty线程模型的优势,我们需要现了解现存的各种线程模型之间的差别:

目前存在的线程模型有:

  • 传统阻塞I/O服务模型
  • Reactor(反应器)模式

在Reactor模式中,根据Reactor的数量和处理资源的线程池数量的不同,有3种典型的实现:

  • 单Reactor单线程
  • 单Reactor多线程
  • 主从Reactor多线程(存在多个Reactor)

Netty的线程模型主要是基于主从Reactor多线程模型进行了一定的改进。

传统阻塞I/O服务模型


模型特点:

采用阻塞I/O的形式来获取输入数据,每个连接都需要独立的线程来完成数据的输入、业务处理、数据返回等操作。

存在的问题:

  • 当服务端面临大量连接并发的时候,就会创造大量的线程来处理连接,占用大量系统资源;
  • 当客户端连接服务端之后就会被绑定线程来监听客户端的操作,但是当客户端没有IO操作的时候,服务端线程仍然会处于阻塞状态一支监听,阻塞在read(),造成线程资源的浪费。

Reactor模型

针对传统阻塞I/O存在的问题,Reactor线程模型给出了如下的解决方案:

  • 基于I/O复用模型:多个连接公用一个阻塞对象,应用程序只需要在一个阻塞对象等待,无需阻塞所有连接。当某个连接有新的数据需要处理的时候,操作系统通知应用程序,线程从阻塞状态返回,开始处理数据;
  • 基于线程池服用线程资源:不必再为每个连接创建独立的线程,将连接完成后的业务处理任务分配给线程池中的线程进行处理,一个线程可以管理多个连接的业务。

在此对于I/O复用模型中的阻塞对象应该如何理解?


Reactor模式的设计理念是通过服务处理器(ServiceHandler)来管理多个Client的连接,基于Client的事件来驱动请求的处理,然后将需要处理的请求分发给线程池中的空闲线程进行处理。

对于上方Reactor示意图的理解:

  • Servicehandler就相当于Reactor,它在一个独立的线程中运行,负责监听和分发事件;
  • EventHandler是实际处理事件的应用程序,执行应用程序的线程如何接受到事件由Reactor分发来决定。
单Reactor单线程


之前我们通过NIO实现的群聊案例就属于单Reactor单线程的一个实例,我们在服务端可以监听到多个客户端的请求,并且执行注册连接和群发消息两个功能。但是服务端进行请求执行的时候都是在同一线程中进行的,如果面临高并发的情况的话势必会造成请求的阻塞。

方案优缺点分析:

优点:

  • 模型简单,没有多线程之间线程通信、竞争的问题,全部都在一个线程中进行;

缺点:

  • 只有一个线程处理业务,无法发挥多核CPU的性能。Handler在处理某个连接上的业务时,整个进程无法处理其他连接事件,很容易造成性能瓶颈;
  • 可靠性问题,线程意外终止,或者进入死循环,会导致整个系统通信模块不可用,不能接受和处理外部消息,造成节点故障

使用场景:

  • 客户端的数量有限,业务处理非常迅速的情况。
单Reactor多线程


原理图解:

  • Reactor对象通过select监控客户端请求事件,收到请求后通过dispatch进行分发;
  • 如果是建立连接请求,则由Acceptor通过accept完成连接操作,然后创建一个Handler对象处理完成连接后的各种事件;
  • 如果不是连接请求,则由Reactor分发调用连接对应的Handler来处理;
  • Handler只负责响应事件,不做具体的业务处理,通过read读取数据之后,会分发给后面的Worker线程池的某个线程处理任务;
  • Worker线程池会分配独立线程完成真正的业务,并将结果返回给Handler;
  • Handler收到响应后,通过send发送给对应的client。方案优缺点分析:

优点:

  • 业务处理采用多线程的形式,可以充分使用多核CPU的能力;

缺点:

  • 多线程数据共享和访问处理比较复杂;
  • Reactor对Client事件的监听和分发其实是单线程进行的,在高并发的情况下会出现性能瓶颈。
主从Reactor多线程


原理解析:

  • 主从Reactor模式中,MainReactor负责监听Client的连接事件进行处理,并且将IO事件分配给SubReactor;
  • SubReactor并不是一个单独的线程,也是由一个线程池组成的多线程模式,在此优化了并发瓶颈问题。

方案优缺点分析:

优点:

  • 父线程与子线程的数据交互简单,职责明确,父线程只需要接受新连接,子线程完成后续的业务处理;
  • 父线程与子线程之间的数据交互简单,Reactor主线程只需要把新连接传递给子线程,子线程不用返回数据,可以直接将业务处理结果send给client;

缺点:

  • 编程复杂度较高。

Netty线程模型

Netty线程模型是在主从Reactor线程模型的基础上进行优化拓展。


原理解析:

  • Netty为了处理客户端连接和业务抽象出两组线程池:BossGroup和WorkerGroup,其中BossGroup负责处理Client的连接,Worker Group负责业务处理事件的分发;
    • BossGroup和WorkerGroup具体的实现类型都是NIOEventLoopGroup,NIOEventLoopGroup可以解释为事件循环组,事件循环组中包含多个事件循环(NIOEventLoop);
    • NIOEventLoop表示一个不断循环的处理任务队列的线程,每一个NIOEventLoop中都有一个selector,用于监听绑定在其上的socket网络通讯;
  • BossGroup中NIOEventLoop执行步骤如下:
    • 从任务队列中轮询Accept事件;
    • 处理Accept事件,与Client建立连接,生成NIOSocketChannel,并将其注册到WorkerGroup的某个NIOEventLoop的selector中;
    • 继续处理任务队列中的任务,即runAllTasks;
  • WorkerGroup中NIOEventLoop执行步骤如下:
    • 从任务队列中轮询Read/Write事件;
    • 处理Read/Write事件,将对应的NIOSocketChannel分发给Pipeline中的Handler处理具体业务;
    • 继续处理任务队列中的任务,即runAllTasks;
  • Pipeline中维护的是处理具体业务的Handler

Netty快速入门-TCP

利用Netty实现TCP客户端和服务端通信。

1、引入Netty依赖

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

2、编写服务端代码

package com.xsh.netty.netty.simple;

import com.xsh.netty.netty.simple.handler.SimpleServerHandler;
import io.netty.bootstrap.ServerBootstrap;
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.NioServerSocketChannel;

public class SimpleServer {

    public static void main(String[] args) throws InterruptedException {
        //创建BossGroup和WorkerGroup
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            //创建服务启动对象并设置参数
            ServerBootstrap bootStrap = new ServerBootstrap();
            bootStrap
                    .group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel socketChannel) throws Exception {
                            socketChannel
                                    .pipeline()
                                    .addLast(new SimpleServerHandler());
                        }
                    });

            System.out.println("server is ready......");
            //绑定监听端口,启动server
            ChannelFuture channelFuture = bootStrap.bind(6668).sync();

            //异步监听client的close channel事件
            channelFuture.channel().closeFuture().sync();
        }finally {
            //释放资源
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}

2、编写服务端读取数据处理业务的Handler

package com.xsh.netty.netty.simple.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;

public class SimpleServerHandler extends ChannelInboundHandlerAdapter {

    /**
     * 在读取到客户端消息到时候被调用
     * @param ctx 上下文对象,其中包含pipeline、channel以及channel的地址等信息
     * @param msg client发送的消息
     * @throws Exception
     */
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        ByteBuf byteBuf = (ByteBuf) msg;
        System.out.println("client发送的消息是: " + byteBuf.toString(CharsetUtil.UTF_8));
    }

    /**
     * 在读取完客户端消息之后被调用
     * @param ctx
     * @throws Exception
     */
    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        ctx.channel().writeAndFlush(Unpooled.copiedBuffer("hello client~", CharsetUtil.UTF_8));
    }

    /**
     * 发生异常的时候被调用
     * @param ctx
     * @param cause
     * @throws Exception
     */
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        //打印异常并关闭发生异常的channel
        cause.printStackTrace();
        ctx.channel().closeFuture();
    }
}

4、编写客户端的代码

package com.xsh.netty.netty.simple;

import com.xsh.netty.netty.simple.handler.SimpleClientHandler;
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 SimpleClient {

    public static void main(String[] args) throws InterruptedException {

        //创建clientGroup
        EventLoopGroup clientGroup = new NioEventLoopGroup();
        try {
            //创建初始化对象
            Bootstrap bootstrap = new Bootstrap();
            //配置client初始化对象参数
            bootstrap
                    .group(clientGroup)
                    .channel(NioSocketChannel.class)
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel socketChannel) throws Exception {
                            socketChannel
                                    .pipeline()
                                    .addLast(new SimpleClientHandler());
                        }
                    });
            //连接客户端
            ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 6668).sync();
            //监听channel关闭事件
            channelFuture.channel().closeFuture().sync();
        } finally {
            //释放资源
            clientGroup.shutdownGracefully();
        }

    }

}

5、编写客户端发送数据的Handler

package com.xsh.netty.netty.simple.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;

public class SimpleClientHandler extends ChannelInboundHandlerAdapter {

    /**
     * client连接到server的时候触发
     * @param ctx
     * @throws Exception
     */
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        String msg = "hello server...";
        ctx.writeAndFlush(Unpooled.copiedBuffer(msg, CharsetUtil.UTF_8));
    }

    /**
     * 接收到server发送的消息的时候触发
     * @param ctx
     * @param msg
     * @throws Exception
     */
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        ByteBuf byteBuf = (ByteBuf) msg;
        System.out.println("server发送的消息是:" + byteBuf.toString(CharsetUtil.UTF_8));
    }

    /**
     * 发生异常的时候触发
     * @param ctx
     * @param cause
     * @throws Exception
     */
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        ctx.channel().closeFuture();
    }
}

关于入门案例API的源码解析:

1、BossGroup和WorkerGroup线程组中的子线程数量是多少?

​ 默认情况下,BossGroup和WorkerGroup线程组中的子线程(NioEventLoop)数量为服务器设备的CPU核心数乘以二,也可以在创建线程组的时候制定内部子线程的数量,如下:

        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workerGroup = new NioEventLoopGroup(4);

2、对于ChannelHandlerContext上下文对象的理解:

​ 在具体执行业务的Handler实现中,我们通过实现方法的入参可以轻松获取ChannelHandlerContext对象,ChannelHandlerContext的意思是上下文对象,从上下文对象中我们可以获取到客户端、服务端端地址信息,对应的channel和pipeline信息,以及对应的WorkerGroup的NioEventLoop的信息。

3、关于Handler中channel和pipeline的理解:

​ pipeline可以理解为管道,其中包含着具体执行业务的handler,每个channel和pipeline是互相包含一一对应的。

TaskQueue

BossGroup和WorkerGroup具体的实例对象为NioEventLoopGroup,其中的子线程的具体实例对象为NioEventLoop,每一个NioEventLoop都有自己独立的selector用来注册事件,同时还有独立的TaskQueue。

TaskQueue可以理解为任务队列的意思,其作用为可以用来执行异步任务,比如说业务中执行时间过长会阻塞Handler的业务可以考虑放在TaskQueue中执行,还可以用来执行定时任务等。

1、自定义TaskQueue任务

首先我们自定义一个任务将其放在eventLoop的任务队列中,模拟耗时较长的任务采用线程休眠的方式:

    private void synTack(ChannelHandlerContext ctx, long time){
        ctx.channel().eventLoop().execute(() -> {
            try {
                Thread.sleep(time);
                ctx.channel().writeAndFlush(Unpooled.copiedBuffer("task message " + time, CharsetUtil.UTF_8));
            }catch (Exception e){
                e.printStackTrace();
            }
        });
    }

然后我们在读取客户端发送消息的方法中调用自定义任务,使其进入taskQueue,并且仍然采用channelReadComplete()在channelRead()执行完毕之后给客户端发送消息:

/**
     * 在读取到客户端消息到时候被调用
     * @param ctx 上下文对象,其中包含pipeline、channel以及channel的地址等信息
     * @param msg client发送的消息
     * @throws Exception
     */
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        //第一个异步任务
        synTack(ctx,10 * 1000);
        //第二个异步任务
        synTack(ctx, 20 * 1000);
    }

    /**
     * 在读取完客户端消息之后被调用
     * @param ctx
     * @throws Exception
     */
    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        ctx.channel().writeAndFlush(Unpooled.copiedBuffer("server read end~", CharsetUtil.UTF_8));
    }

如图可以看出,第一个和第二个异步任务会分别被阻塞10s和20s,由于任务是异步的,所以channelRead()不会被阻塞,反而是channelReadComplete()中的返回结果会先发送给客户端:

要注意的是,TaskQueue虽然是一个异步的队列,但是队列中的任务仍然是由单一线程来维护的,所以任务队列中的任务是挨个执行的,例如上面的案例,在task message 10000的任务执行完毕之后,需要等待20s之后才会看到task message 20000消息执行完毕,而不是10s

2、自定义延时的TaskQueue任务

延时的TaskQueue任务指的是,我们可以将任务设定指定的延迟时间然后写进任务队列中异步执行,需要知道的是,延迟任务的队列和普通任务的队列不是同一个,实现如下:

    private void delayedSynTack(ChannelHandlerContext ctx, long time, int delayed){
        ctx.channel().eventLoop().schedule(() -> {
            try {
                Thread.sleep(time);
                ctx.channel().writeAndFlush(Unpooled.copiedBuffer("delayed task message " + time, CharsetUtil.UTF_8));
            }catch (Exception e){
                e.printStackTrace();
            }
        }, delayed, TimeUnit.SECONDS);
    }
    /**
     * 在读取到客户端消息到时候被调用
     * @param ctx 上下文对象,其中包含pipeline、channel以及channel的地址等信息
     * @param msg client发送的消息
     * @throws Exception
     */
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        //第一个异步任务
        synTack(ctx,10 * 1000);
        //第二个异步任务
        synTack(ctx, 20 * 1000);
        //延时的异步任务
        delayedSynTack(ctx,10 * 1000, 5);
    }

Netty异步模型

  • 异步的概念和同步相对。当一个异步过程调用发出后,调用者不能立即获取结果,实际处理的结果会在这个调用的组件执行完成之后,通过状态、通知和回调来通知调用者;
  • Netty中的IO操作是异步的,包括Bind、Write、Connent等操作会简单返回一个ChannelFuture;
  • 调用者不能立即获得处理结果,而是通过Future-Listener机制,调用者可以通过主动获取或者通知机制来获取IO操作结果;
  • Netty的异步模型是建立在Future和Callback之上的,Callback就是回调。
    • Future的设计思想是将有可能需要等待的IO任务提交给异步任务处理,异步任务会直接返回一个Future,后续可以通过Future来监控查看异步任务的处理过程(即Future-Listener机制)。

Future-Listener

当异步任务提交,Future对象刚刚创建的时候,出于非完成状态。调用者可以通过返回的ChannelFuture来获取操作执行的状态,或者注册监听函数来执行完成之后的操作。

常见的有如下操作:

  • 通过isDone方法来判断当前操作是否已经完成;
  • 通过isSuccess方法来判断已完成的当前操作是否执行成功;
  • 通过getCause方法来获取已完成的当前操作失败的原因;
  • 通过isCancelled方法来判断已完成的当前操作是否被取消;
  • 通过addListener方法来注册监听器,当操作已完成(isDone方法返回已完成),将会通知指定的监听器。

下方案例可以体现通过Future-Listener机制获取Client连接Server是否成功:

            //连接客户端
            ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 6668).sync();

            //指定Listener监听关心的事件
            channelFuture.addListener(new ChannelFutureListener() {
                @Override
                public void operationComplete(ChannelFuture channelFuture) throws Exception {
                    if(channelFuture.isSuccess()){
                        System.out.println("连接server成功");
                    }
                }
            });

Netty快速入门-HTTP

使用Netty创建一个Http服务端,实现与浏览器请求交互。

1、服务端实现

package com.xsh.netty.netty.http;

import com.xsh.netty.netty.http.handler.HttpHandler;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.http.HttpServerCodec;

public class HttpServer {

    public static void main(String[] args) throws Exception {

        NioEventLoopGroup bossGroup = new NioEventLoopGroup();
        NioEventLoopGroup workerGroup = new NioEventLoopGroup();

        try {
            ServerBootstrap bootstrap = new ServerBootstrap();
            bootstrap
                    .group(bossGroup,workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel socketChannel) throws Exception {
                            socketChannel
                                    .pipeline()
                                    //添加http协议的编解码器
                                    .addLast(new HttpServerCodec())
                                    .addLast(new HttpHandler());
                        }
                    });
            System.out.println("server is ready......");
            ChannelFuture channelFuture = bootstrap.bind(8888).sync();
            channelFuture.channel().closeFuture().sync();
        }finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }

    }

}

2、Handler实现

package com.xsh.netty.netty.http.handler;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.*;
import io.netty.util.CharsetUtil;

/**
 * SimpleChannelInboundHandler是ChannelInboundHandlerAdapter的子类实现
 */
public class HttpHandler extends SimpleChannelInboundHandler<HttpObject> {
    @Override
    protected void channelRead0(ChannelHandlerContext channelHandlerContext, HttpObject msg) throws Exception {
        if (msg instanceof HttpRequest) {
            //相应数据给浏览器
            ByteBuf byteBuf = Unpooled.copiedBuffer("hello, 我是服务器", CharsetUtil.UTF_8);
            //构建response响应
            DefaultFullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, byteBuf);
            response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain");
            response.headers().set(HttpHeaderNames.CONTENT_LENGTH, byteBuf.readableBytes());

            //返回response
            channelHandlerContext.writeAndFlush(response);
        }

    }
}

此案例中需要注意的点有两个,第一个是使用Netty搭建http服务器需要使用Netty提供的http编解码器Handler,第二点是了解

SimpleChannelInboundHandler。

Netty核心组件API详解

Bootstrap、ServerBootstrap

Bootstrap的意思是引导,一个Netty应用通常应该由一个Bootstrap开始,主要作用是配置整个Netty程序,串联各个组件。Netty中的Bootstrap类是客户端程序的启动引导类,ServerBootstrap是服务端启动引导类。

常见的方法如下:

  • public ServerBootstrap group(EventLoopGroup parentGroup, EventLoopGroup childGroup):该方法用于服务器端,用于设置BossGroup和WorkerGroup线程组;
  • public B group(EventLoopGroup group):该方法用于客户端,用于设置BossGroup线程组;
  • public B channel(Class<? extends C> channelClass):该方法用于设置一个通道的实现;
  • public <T> B option(ChannelOption<T> option, T value):该方法用于给ServerChannel添加配置;
  • public <T> ServerBootstrap childOption(ChannelOption<T> childOption, T value):该方法用于给接收到的通道添加配置;
  • public ServerBootstrap childHandler(ChannelHandler childHandler):该方法用于设置处理业务的Handler;
  • public ChannelFuture bind(int inetPort):该方法用于服务端,用来设置服务端监听的端口号;
  • public ChannelFuture connect(String inetHost, int inetPort):该方法用于客户端,用来连接服务端;

Future、ChannelFuture

Netty中所有的IO操作都是异步的,不能立刻得知消息是否被成功处理,但是可以通过Future或者ChannelFuture监控IO操作的进程,可以设置自定义的监听事件来监听IO操作的各种结果。

常见的方法如下:

  • Channel channel():返回当前正在执行操作的channel;
  • ChannelFuture sync():等待异步操作执行完毕。

Channel

  • Channel是Netty网络通信的组件,能够用于执行网络IO操作;
  • 通过Channel可获得当前网络连接的通道的状态;
  • 通过Channel可以获得网络连接的配置参数(例如接受缓冲区的大小);
  • Channel提供异步的网络IO操作,如建立连接、读写、绑定端口等。异步调用意味着任何IO调用都将立即返回,但是不保证在调用结束的时候所请求的IO操作已完成;
  • 支持关联IO操作与对应的处理程序;
  • 不同协议、不同的阻塞类型的连接都有不同的Channel实现与之对应,常见的Channel类型如下:
    • NioSocketChannel:异步的客户端TCP Socket连接;
    • NioServerSocketChannel:异步的服务端TCP Socket连接;
    • NioDatagramChannel:异步的UDP连接;
    • NioSctpChannel:异步的客户端Sctp连接
    • NioSctpServerChannel:异步的Sctp服务器端连接。

Selector

Netty实现了基于Selector对象的IO多路复用,通过Selector一个线程可以监控多个Channel的IO事件。

当像一个Selector中注册Channel之后,Selector内部的机制就可以自动不断地查询(通过select方法)这些注册的Channel是否有已经就绪的IO事件(例如可读、可写、网络连接)等,这些程序就可以很简单的使用一个线程高效地管理多个Channel。

ChannelHandler及其实现类

ChannelHandler是一个接口,处理IO事件或拦截IO操作,并将其转发到其ChannelPipeline(业务处理链)中的下一个处理程序。

ChannelHandler类本身没有提供更多的方法,这个接口有许多的方法需要实现,在实际使用中,可以使用其实现好的子类。

Pipeline和ChannelPipeline

ChannelPipeline是一个Handler的集合,它负责处理和拦截Inbound或者Outbound的事件和操作,相当于一个贯穿Netty的链。也可以将ChannelPipeline理解为是保存ChannelHandler的List,用于处理或者拦截Channel的入站事件和出站事件。

ChannelPipeline实现了一种高级形式的拦截过滤器模式,使用户可以完全控制事件处理的方式,以及Channel中各个ChannelHandler如何相互交互。

Netty中每一个Channel都有一个唯一的ChannelPipeline与之互相对应,其包含关系以及ChannelPipeline中Handler的流程关系如下:

  • Channel和ChannelPipeline是相互保护的,可以在Channel中获取到对应的Pipeline信息,相反,在PipeLine中也可以获取到对应的Channel信息;
  • ChannelPipeline中维护了一个由ChannelHandlerContext组成的双向链表,并且每一个ChannelHandlerContext都有一个关联的ChannelHandler;
  • 入站和出站的事件都发生在一个双向链表中,入站事件会从链表head往后传递到最后一个入站的Handler,出站事件会从链表tail往前传递到最前一个出站的Handler,两种类型的Handler互不干扰

ChannelPipeline常用方法:

  • ChannelPipeline addFirst(ChannelHandler... var1):将一个Handler添加到链的第一个位置;
  • ChannelPipeline addLast(ChannelHandler... var1):将一个Handler添加到链的最后一个位置。

ChannelHandlerContext

ChannelHandlerContext保存了Channel相关的所有上下文信息,同时关联一个ChannelHandler对象;

即ChannelHandlerContext中包含一个具体的事件处理器ChannelHandler,同时ChannelHandlerContext中也绑定了对应的Pipeline和Channel信息,方便对ChannelHandler进行调用。

常用方法:

  • ChannelFuture close():关闭通道;
  • ChannelHandlerContext flush():刷新通道;
  • ChannelFuture writeAndFlush(Object var1):将数据写入到ChannelPipeline中当前ChannelHandler的下一个ChannelHandler处理。

ChannelOption

在创建Netty Server实例的时候,我们可以通过option()和childOption()对ServerChannel进行参数配置,也可以采用Netty的默认配置。

option()和childOption()的区别:

  • option()是针对ServerChannel的配置,childOption()是针对ServierChannel的子Channel的配置。也可以理解为option()是针对Boss线程组的配置,childOption()是针对Worker线程组的配置;
  • 同理,因为在client端只有一个NioEventLoopGroup线程组,所以只有BootStrap对象只有option()方法来设置对应参数。

option()和childOption()设置参数的主要内容就体现在ChannelOption类中。

常见的ChannelOption取值如下:

  • ChannelOption.SO_BACKLOG:对应TCP/IP协议listen函数中的backlog参数,用来初始化服务器可连接队列的大小。服务器处理客户端的连接请求是顺序处理的,所以同一时间只能处理同一client的连接请求,当有多个客户端的连接请求进来时,服务端将不能处理的客户端连接请求放在队列中等待处理,backlog参数用于指定队列的大小;
  • ChannelOption.SO_KEEPALIVE:保持连接活性,默认值为false。启用该功能时,TCP会主动探测空闲连接的有效性,可以将此功能视为TCP的心跳机制,需要注意的是:默认的心跳间隔是7200s即2小时。Netty默认关闭该功能。

EventLoopGroup和NioEventLoopGroup

EventLoopGroup是一组EventLoop的抽象,Netty为了更好的利用多核CPU资源,一般会有多个EventLoop同时工作,每个EventLoop维护着一个Selector实例;

EventLoopGroup提供一个next接口,可以从组中按照一定规则获取其中一个EventLoop来处理任务。在Neety的服务端编程中,我们一般提供两个EventLoopGroup,即BossEventLoopGroup和WorkerEventLoopGroup。

常见方法:

  • Future<?> shutdownGracefully():断开连接,关闭线程。

Unpooled和ByteBuf

Unpooled是Netty提供的专门用来操作数据缓冲区的工具类;

常见方法如下:

  • public static ByteBuf copiedBuffer(CharSequence string, Charset charset):通过给定的数据和字符编码返回一个ByteBuf对象;
  • public static ByteBuf buffer():返回一个ByteBuf对象,其中包含一个byte[]缓冲区,也可以传入指定参数创建指定长度的数据缓冲区;

ByteBuf类似于NIO中的ByteBuffer,但有所不同。

ByteBuf在进行数据写入和读取的时候不用进行buffer的反转来重置指针,它内部维护了readerIndex和writerIndex两个索引, readerIndex表示下一个读取的数据索引,writerIndex表示下一个写入的数据索引。

常见方法如下:

  • public abstract int capacity():返回buffer缓冲区的大小;
  • public abstract ByteBuf writeByte(int var1):往缓冲区中写入数据;
  • public abstract byte readByte():从缓冲区中读取数据;
  • public abstract byte getByte(int var1):获取缓冲区指定索引位置数据;
  • public abstract int readableBytes():返回缓冲区可读取的数据长度;
  • public abstract CharSequence getCharSequence(int var1, int var2, Charset var3):区间读取缓冲区数据,第一个参数是读取数据的起点,第二个是想要读取的长度,第三个是以什么样的字符编码读取返回。

Netty实战:群聊系统

使用Netty实现群聊系统,实现客户端和服务端之间的通讯以及监测客户端上线、离线状态并给出提示。

1、服务端实现

package com.xsh.netty.netty.groupchat;

import com.xsh.netty.netty.groupchat.handler.GroupChatServerHandler;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
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 GroupChatServer {

    private int prot;

    public GroupChatServer(int prot){
        this.prot = prot;
    }

    public void run() throws InterruptedException {

        NioEventLoopGroup bossGroup = new NioEventLoopGroup();
        NioEventLoopGroup workerGroup = new NioEventLoopGroup();

        try {
            ServerBootstrap bootstrap = new ServerBootstrap();
            bootstrap
                    .group(bossGroup,workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel socketChannel) throws Exception {

                            socketChannel
                                    .pipeline()
                                    //将接收到的消息转换为String
                                    .addLast(new StringDecoder())
                                    .addLast(new StringEncoder())
                                    .addLast(new GroupChatServerHandler());
                        }
                    });

            System.out.println("GroupChat server is ready......");

            ChannelFuture channelFuture = bootstrap.bind(prot).sync();
            channelFuture.channel().closeFuture().sync();
        }finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }

    }

    public static void main(String[] args) throws InterruptedException {
        new GroupChatServer(8888).run();
    }

}

2、服务端Handler实现

package com.xsh.netty.netty.groupchat.handler;

import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.group.ChannelGroup;
import io.netty.channel.group.DefaultChannelGroup;
import io.netty.util.concurrent.GlobalEventExecutor;

public class GroupChatServerHandler extends SimpleChannelInboundHandler<String> {

    /**
     * channels是Netty提供的一个可以用来管理channel的容器
     * 因为是多个线程公用的资源,所以是static的
     */
    private static final ChannelGroup channels = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);

    /**
     * handlerAdded()会在有客户端建立连接的时候第一个被执行,在此借用它实现客户端上线的功能以及channel管理
     */
    @Override
    public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
        channels.add(ctx.channel());
        /*
         * channels.writeAndFlush()会将消息转发给其管理的所有channel,不用遍历
         * 在此实现当有客户端上线通知其他的所有客户端
         */
        channels.writeAndFlush("[客户端]" + ctx.channel().remoteAddress() + "加入聊天\n");
    }

    /**
     * handlerRemoved()会在客户端断开连接之前最后一个被执行,在此借用它实现客户端离线的功能
     * 该方法被调用之后channels容器会自动将断开连接的channel从容器中剔除,不用手动管理
     */
    @Override
    public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
        channels.writeAndFlush("[客户端]" + ctx.channel().remoteAddress() + "离开了\n");
        System.out.println("channels 的长度为: " + channels.size());
    }

    /**
     * 当channel处于活跃状态的时候激活该方法
     */
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        System.out.println("[客户端]" + ctx.channel().remoteAddress() + "上线");
    }

    /**
     * 当channel处于非活动状态当时候激活该方法
     */
    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        System.out.println("[客户端]" + ctx.channel().remoteAddress() + "离线");
    }

    @Override
    protected void channelRead0(ChannelHandlerContext channelHandlerContext, String msg) throws Exception {

        channels.forEach(channel -> {
            if(channel != channelHandlerContext.channel()){
                channel.writeAndFlush("用户" + channelHandlerContext.channel().remoteAddress() + "说:" + msg + "\n");
            }else{
                channel.writeAndFlush("我说:" + msg + "\n");
            }
        });

    }

    /**
     * 发生异常时被调用
     * 当发生异常的时候打印异常并关闭通道
     */
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        ctx.close();
    }

}

3、客户端实现

package com.xsh.netty.netty.groupchat;

import com.xsh.netty.netty.groupchat.handler.GroupChatClientHandler;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;

import java.util.Scanner;

public class GroupChatClient {

    private String host;
    private int port;

    public GroupChatClient(String host, int port) {
        this.host = host;
        this.port = port;
    }

    public void run() throws InterruptedException {
        NioEventLoopGroup clientGroup = new NioEventLoopGroup();
        try {
            Bootstrap bootstrap = new Bootstrap();
            bootstrap
                    .group(clientGroup)
                    .channel(NioSocketChannel.class)
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel socketChannel) throws Exception {
                            socketChannel
                                    .pipeline()
                                    .addLast(new StringDecoder())
                                    .addLast(new StringEncoder())
                                    .addLast(new GroupChatClientHandler());
                        }
                    });
            ChannelFuture channelFuture = bootstrap.connect(host, port).sync();

            System.out.println("----"+ channelFuture.channel().remoteAddress() +"欢迎加入聊天室----");
            Scanner scanner = new Scanner(System.in);
            while (scanner.hasNextLine()){
                channelFuture.channel().writeAndFlush(scanner.nextLine());
            }
        } finally {
            clientGroup.shutdownGracefully();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        new GroupChatClient("127.0.0.1", 8888).run();
    }

}

4、客户端Handler实现

package com.xsh.netty.netty.groupchat.handler;

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

public class GroupChatClientHandler extends SimpleChannelInboundHandler<String> {
    @Override
    protected void channelRead0(ChannelHandlerContext channelHandlerContext, String s) throws Exception {
        System.out.println(s);
    }
}

Netty心跳检测机制

1、server实现

package com.xsh.netty.netty.heartbeat;

import com.xsh.netty.netty.heartbeat.handler.HeartbeatServerHandler;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import io.netty.handler.timeout.IdleStateHandler;

import java.util.concurrent.TimeUnit;

public class HeartbeatServer {

    public static void main(String[] args) throws Exception {

        NioEventLoopGroup bossGroup = new NioEventLoopGroup();
        NioEventLoopGroup workerGroup = new NioEventLoopGroup();

        try {
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            serverBootstrap
                    .group(bossGroup,workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .handler(new LoggingHandler(LogLevel.INFO))
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel socketChannel) throws Exception {
                            socketChannel
                                    .pipeline()
                                    .addLast(new IdleStateHandler(3, 5, 7, TimeUnit.SECONDS))
                                    .addLast(new HeartbeatServerHandler());
                        }
                    });
            System.out.println("------server is ready------");

            ChannelFuture channelFuture = serverBootstrap.bind(8888).sync();
            channelFuture.channel().closeFuture().sync();
        }finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}

2、server端handler实现

package com.xsh.netty.netty.heartbeat.handler;

import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.handler.timeout.IdleState;
import io.netty.handler.timeout.IdleStateEvent;

import static io.netty.handler.timeout.IdleState.*;

public class HeartbeatServerHandler extends ChannelInboundHandlerAdapter {

    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {

        if(evt instanceof IdleStateEvent){
            IdleStateEvent event = (IdleStateEvent) evt;

            IdleState state = event.state();
            if(READER_IDLE.equals(state)){
                System.out.println("server对client" + ctx.channel().remoteAddress() + "发生读空闲");
            }
            if(WRITER_IDLE.equals(state)){
                System.out.println("server对client" + ctx.channel().remoteAddress() + "发生写空闲");
            }
            if(ALL_IDLE.equals(state)){
                System.out.println("server对client" + ctx.channel().remoteAddress() + "发生读写空闲");
            }
        }


    }
}

对于IdleStateHandler的说明:

​ IdleStateHandler是Netty提供的用于心跳检测的Handler,其实例化方法如下:

    public IdleStateHandler(long readerIdleTime, long writerIdleTime, long allIdleTime, TimeUnit unit) {
        this(false, readerIdleTime, writerIdleTime, allIdleTime, unit);
    }

构造方法入参解析:

  • readerIdleTime:当server对某一个channel出现readerIdleTime时间的读取数据空闲后激发事件;
  • writerIdleTime:当server对某一个channel出现writerIdleTime时间的写入数据空闲后激发事件;
  • allIdleTime:当server对某一个channel出现allIdleTime时间的读、写数据空闲后激发事件;

IdleStateHandler激发的事件会按照pipeline的顺序传递给后面一个handler的userEventTriggered(),用户可以在userEventTriggered()中对不同的空闲事件做不同的业务反馈。

编解码器

实际上在网络通信程序中,数据在网络中都是以二进制字节码的形式传输,在发送数据的时候需要进行编码,在接受数据的时候需要进行解码。

codec的组成部分有两个:decoder(解码器)和encoder(编码器),encoder负责将业务数据转换为字节码数据,decoder负责将字节码数据转换为业务数据。

Netty本身提供的常用编解码器:

  • StringEncoder、StringDecoder:对字符串数据进行编解码;
  • ObjectEncoder、ObjectDecoder:对Java对象进行编解码;

ObjectEncoder和ObjectDecoder可以用来实现Java各种对象的编码和解码,底层使用的仍然是Java序列化技术。而Java序列化本身效率就不高,存在无法跨语言、序列化后报文体积太大,是二进制编码的5倍多、序列化性能差等原因,可以采用Protobuf实现。

Protobuf

Protobuf是Google发布的开源项目,全称Google Protocol Buffers,是一种轻便的、高效的结构化数据存储格式,可以用于结构化数据串行化,或者说序列化。它很适合做数据存储或RPC(远程过程调用)数据交换格式。

Protobuf是以message的方式来管理数据的。

支持跨平台、跨语言,即客户端和服务端可以是不同的语言开发的,支持目前绝大多数语言,例如C++、C#、Java、Python等。

快速入门案例

在客户端和服务端使用Protobuf传递一个java对象信息。

1、引入Protobuf依赖

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

2、创建Student.proto文件

在idea中想要编辑.proto文件实现语法高亮和提示需要安装插件,插件名称为:Protobuf Support

安装插件之后创建的.proto文件仍然没有语法提示以及文件图标仍然没发生改变,解决方案:https://blog.csdn.net/qq_43901693/article/details/98364979

Student.proto:

syntax = "proto3";  //指定版本
option java_outer_classname = "StudentPOJO";  //指定生成的外部类类名

//proto使用message管理对象内容
message Student{
  /**
  Student会成为StudentPOJO类的一个内部类,是真正用来数据传递的对象
  int32是proto语法中的数据类型,对应java中的int,该数据类型可以对应多种编程语言
  int32 id = 1,其中id = 1并不是赋值操作,表示的是这个属性在对象中的位置
   */
  int32 id = 1;
  string name = 2;
}

3、将.proto文件转换为java对象

.proto对象想要转换为java对象需要一个工具的支持,在此,mac版本的工具需要FQ才能获取,暂时搁置。工具可以将.proto文件转换为一个java文件,该java文件不可编辑,提供了方法获取Student对象以及给属性赋值取值等操作。

4、在client添加ProtobufEncoder

            bootstrap
                    .group(clientGroup)
                    .channel(NioSocketChannel.class)
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel socketChannel) throws Exception {
                            socketChannel
                                    .pipeline()
                                    .addLast(new ProtobufEncoder())
                                    .addLast(new SimpleClientHandler());
                        }
                    });

5、在server添加ProtobufDecoder

            bootStrap
                    .group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel socketChannel) throws Exception {
                            socketChannel
                                    .pipeline()
                                    .addLast(new ProtobufDecoder(StudentPOJO.Student.getDefaultInstce()))
                                    .addLast(new SimpleServerHandler());
                        }
                    });

创建ProtobufDecoder解码器的时候,需要指定解码参数,即针对哪个proto转换的java对象进行解码。

如上进行编解码器的配置之后,只需要对StudentPOJO进行操作、传输即可,在server端对接收到的数据进行强转唯对应的StudentPOJO就可以对接收到的数据进行操作。

但是上面的写法局限性很大,我们只传输了一个Student对象,在创建ProtobufDecoder的时候指定死了解码类型,那如果需要传递多个复杂的java对象的时候就非常的局限,并且多个java对象的.proto文件又该如何编写呢?

6、多对象管理的.proto文件写法

syntax = "proto3"; //指定版本
option java_outer_classname = "ProtoDataInfo"; //指定生成的java文件名称

/**
ObjectManage用于管理.proto文件中的用于数据传递的message,在此表示Student以及Worker
 */
message ObjectManage{

  /**
  DataType是一个枚举,server接收到数据之后通过枚举值可以判断接收到的数据是哪个对象
   */
  enum DataType{
    StudentType = 0;
    WorkerType = 1;
  }

  //表示ObjectManage的第一个元素为DataType枚举,用于server区分接下来的实际数据属于什么对象
  DataType data_type = 1;

  /**
  Student student = 2;以及Worker worker = 3;表示ObjectManage的第二个第三个元素分别为Student和Worker对象
  这一部分使用oneof包裹起来,表示该部分内容只会出现其中一个,意思是Student和Worker对象在数据传输中只会出现一个,
  不支持两个对象一次并行传输的情况
   */
  oneof data_body{
    Student student = 2;
    Worker worker = 3;
  }

}

/**
定义两个message,相当于会在ProtoDataInfo.java中生成两个内部类
 */
message Student{
  int32 id = 1;
  string name = 2;
}

message Worker{
  int32 id = 1;
  string name = 2;
  int32 age = 3;
}

如上所示的.proto文件就可以发送多个不同的java对象,因此,在server解码的时候,也需要进行修改,由解析指定数据对象修改为解析管理指定对象的ObjectManage。

            bootStrap
                    .group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel socketChannel) throws Exception {
                            socketChannel
                                    .pipeline()
                                    .addLast(new ProtobufDecoder(StudentPOJO.ObjectManage.getDefaultInstce()))
                                    .addLast(new SimpleServerHandler());
                        }
                    });

粘包和拆包

TCP是面向连接、面向流的,提供高可靠性服务。收发两端(客户端和服务器端)都要有一一成对的socket,因此,发送端位了将多个发送给接收端的包更有效的发送给对方,使用了优化算法(Nagle)。将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包传输。

这样做虽然提高了效率,但是接收端就很难分辨出完整的数据包了,因为面向流的通信是无消息保护边界的。

所以,数据的接收端就要进行拆包来处理消息边界的问题,数据拆包就要使用netty提供的编解码器来实现。


解决粘包和拆包问题可以提供如下两种解决方案:

  • 按照数据长度拆包:在传递数据的时候,需要传递每一条数据的总字节长度,然后在接受数据的时候在解码器中根据字节长度来持续读取数据。
  • 按照自定义字符串拆包:使用netty提供的DelimiterBasedFrameDecoder可以自定义字符来读取数据。

(具体采用的方式要根据实际业务采用的协议来挑选,注意灵活的使用Bytebuf来达到业务需求。)

posted @ 2021-11-16 15:39  原野上找一面墙  阅读(143)  评论(1编辑  收藏  举报