Netty深入学习(一)——NIO

1、Netty介绍

Netty原是由JBOSS提供的一个Java开源框架,现为Github的独立项目。Netty是一个异步的、基于事件驱动的网络应用框架,用以快速开发高性能、高可靠性的网络IO程序。主要针对在TCP协议下,面向Client端的高并发应用,或Peer-to-Peer场景下的大量数据持续传输应用。本质上Netty是一个NIO框架,深入学习Netty首先需要学习NIO。

2、I/O模型

2.1 I/O模型基本说明

1) 简单理解:就是用什么样的通道进行数据的发送和接收

2)Java共支持3种网络编程模型I/O:BIO、NIO、AIO

Java BIO:同步并阻塞(创痛阻塞型),服务器实现模式为一个连接一个线程,即客户端与服务器端有连接请求时就需要启动一个线程进行处理,如果这个连接不做任何事情就会导致不必要的线程开销

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

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

 2.2 Java BIO

BIO(blocking I/O):同步阻塞,可通过线程池机制改善不必要的线程开销。适用于连接数目较小且固定的架构,这种方式对服务器资源要求较高,并发局限于应用中。

BIO编程简单流程:

1)服务器端启动一个ServerSocket

2)客户端启动Socket对服务器进行通信,默认情况下服务器端需要对每个连接(客户)建立一个线程与之通讯

3)客户端发送请求后,先咨询服务器是否有线程响应,如果没有则会等待,或直接被拒绝

4)如果响应,客户端线程会等待请求结束后,在继续执行

测试实例:

public class BIOServer {
    public static void main(String[] args) throws IOException {
        // 1、创建一个线程池
        ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
        // 2、创建ServerSocket
        ServerSocket serverSocket = new ServerSocket(6666);

        System.out.println("服务启动");

        while (true) {
            // 监听,等待客户端连接
            final Socket socket = serverSocket.accept();
            System.out.println("连接到客户端");
            // 创建线程,与之通讯
            newCachedThreadPool.execute(new Runnable() {
                public void run() {
                    handler(socket);
                }
            });
        }
    }
    // 写一个handler方法,和客户端通讯
    public static void handler(Socket socket) {
        byte[] data = new byte[1024];
        // 通过socke,获取输入流
        InputStream inputStream;
        try {
            inputStream = socket.getInputStream();
            int len = -1;
            // 循环读取客户端发送过来的数据
            while ((len = inputStream.read(data)) != -1) {
                System.out.println(new String(data, 0, len));
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            System.out.println("关闭与client 的连接");
            try {
                socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

 使用telnet 127.0.0.1 6666启动客户端测试:

服务端接收的数据:

BIO问题分析:

1)每个请求都需要创建一个独立线程,与对应的客户端进行数据Read、业务处理、数据Write

2)当并发较大时,就需要创建大量的线程去处理,系统资源使用较大

3)连接建立后,如果当前线程暂时没有数据Read,则线程就会阻塞在Read操作上,造成资源浪费

 2.3 Java NIO

2.3.1 NIO基本介绍

全称java non-blocking IO,相关类都在java.nio包及子包下。

NIO有三大核心部分:Channel(通道)、Buffer(缓冲区)、Selector(选择器)

NIO是面向缓冲区(或者面向块)编程的。将数据读取到一个要稍后处理的缓冲区,需要时可在缓存区中前后移动,这样增加了处理过程中的灵活性,使其可以提供非阻塞式的高伸缩性网络。

 NIO,使一个线程从某通道发送请求或读取数据,但是它仅能得到目前可用的数据,如果目前没有数据就什么也不会获取,二不是保持线程阻塞,所以直至数据变得可以读取之前,该线程都可以继续做其他的事情。非阻塞写也是如此,一个线程请求写入一些数据到某通道,但不要等他完全写入,这个线程同时可以去做其他事情。

2.3.2 NIO的Buffer基本使用

public class BasicBuffer {
    public static void main(String[] args) {
        // 举例说明Buffer的使用(简单使用)
        // 创建一个Buffer
        // 创建一个Buffer,大小为5,可以存5个int
        IntBuffer intBuffer = IntBuffer.allocate(5);
        // 向buffer中存放数据
        for (int i = 0; i < intBuffer.capacity(); i++) {
            intBuffer.put(i * 2);
        }
        // 从buffer中读取数据
        // 将buffer转换,读写切换
        intBuffer.flip();
        while (intBuffer.hasRemaining()) {
            System.out.println(intBuffer.get());
        }
    }
}

2.3.3 NIO与BIO的比较

1)BIO以流的方式处理数据,NIO以块的方式,块I/O的效率比流I/O的高很多

2)BIO是阻塞的,NIO是非阻塞的

3)BIO基于字节流和字符流进行操作,而NIO是基于Channel(通道)和Buffer(缓冲区)进行操作,数据总是从通道读取到缓冲区中,或从缓冲区写入到通道中。Selector(选择器)用于监听多个通道的事件(比如:连接请求,数据到达等),因而可使用单个线程就可以监听多个客户单通道。

2.3.4 Buffer的机制与子类 

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

1)常用子类

在NIO中,Buffer是一个顶层父类,它的子类有:ByteBuffer、ShortBuffer、CharBuffer、IntBuffer、LongBuffer、DoubleBuffer、FloatBuffer。其中使用的最多的是ByteBuffer

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

Capacity:容量,可以容纳的最大数据量;在缓冲区创建时被设定并且不能改变。

Limit:表示缓冲区的当前终点,不能对缓冲区超过极限的位置进行读写操作。且极限是可以修改的

Position:位置,下一个要被读写的元素的索引,每次读写缓冲区数据都会改变该值,为下一次读写操作做准备

Mark:标记

Invariants:mark <= position <= limit <= capacity

 2)常用方法

2.3.4 Channel

(1)基本介绍

1)NIO的通道类似于流,但有一些区别如下:

  • 通道可以同时进行读写,而流只能读或写
  • 通道可以实现异步读写数据
  • 通道可以从缓冲读取数据,也可以写数据到缓冲(Channel <==> Buffer)

2)BIO中的stream是单向的,如FileInputStream只能读取数据,而NIO中的通道是双向的

3)Channel在NIO中是一个接口:

  • public interface CHannel extends Closeable {}

4)常用的Channel类有:FileChannel、DatagramChannel、ServerSocketChannel(类似ServerSocket)和SocketChannel(类似Socket)

5)常用Channel类说明:

  • FileChannel:用于文件的数据读写
  • DatagramChannel:用于UDP的数据读写
  • ServerSocketChannel、SocketChannel:用于TCP数据读写

(2)FIleChannel 常用方法说明

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

1)public int read(ByteBuffer dst),从通道读取数据并放到缓冲区

2)public int write(ByteBuffer src),把缓冲区的数据写到通道中

3)public long transferFrom(ReadableByteChannel src, long position, long count),从目标通道中复制数据到当前通道

4)public long transferTo(long position, long count, WritableByteChannel target),把数据从当前通道复制给目标通道

(3)应用实例

将数据通过Channel写入到本地文件:

public class NIOFileChannel01 {
    public static void main(String[] args) throws Exception {
        String str = "hello world!";
        // 创建一个输出流
        FileOutputStream fileOutputStream = new FileOutputStream("G:\\01.txt");
        // 通过输出流获取对应的FileChannel
        FileChannel fileChannel = fileOutputStream.getChannel();
        // 创建一个缓冲区 ByteBuffer
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        // 将str放到ByteBuffer
        byteBuffer.put(str.getBytes());
        // 对ByteBuffer进行flip
        byteBuffer.flip();
        // 将数据从缓冲区写入到通道
        fileChannel.write(byteBuffer);
        fileOutputStream.close();
    }

 将01.txt文件读入到程序:

public class NIOFileChannel02 {
    public static void main(String[] args) throws Exception {
        // 创建文件输入流
        File file = new File("G:\\01.txt");
        FileInputStream fileInputStream = new FileInputStream(file);
        // 通过输入流获取Channel
        FileChannel fileChannel = fileInputStream.getChannel();
        // 创建一个缓冲区
        ByteBuffer byteBuffer = ByteBuffer.allocate((int) file.length());
        // 将数据从通道读取到缓冲区
        fileChannel.read(byteBuffer);
        // 将byteBuffer字节转换为字符串
        System.out.println(new String(byteBuffer.array()));
        fileInputStream.close();
    }
}

使用一个Buffer完成文件读取与写入

public static void main(String[] args) throws Exception {
        // 通过输入流获取Channel
        FileInputStream fileInputStream = new FileInputStream("1.txt");
        FileChannel inputStreamChannel = fileInputStream.getChannel();
        // 通过输出流获取Channel
        FileOutputStream fileOutputStream = new FileOutputStream("2.txt");
        FileChannel outputStreamChannel = fileOutputStream.getChannel();
        // 创建缓冲区
        ByteBuffer byteBuffer = ByteBuffer.allocate(128);
        // 循环读取
        while (true) {
            // buffer复位
            byteBuffer.clear();
            int read = inputStreamChannel.read(byteBuffer);
            // 读取完毕
            if (read == -1) {
                break;
            }
            // 将buffer中的数据写入到Channel
            byteBuffer.flip();
            int write = outputStreamChannel.write(byteBuffer);
        }
        // 关闭流
        outputStreamChannel.close();
        inputStreamChannel.close();
    }

使用transferFrom完成文件拷贝:

public class NIOFileChannel04 {
    public static void main(String[] args) throws Exception {
        // 创建相关流
        FileInputStream fileInputStream = new FileInputStream("0.jpg");
        FileOutputStream fileOutputStream = new FileOutputStream("0_bak.jpg");
        // 通过流获取相关Channel
        FileChannel inputStreamChannel = fileInputStream.getChannel();
        FileChannel outputStreamChannel = fileOutputStream.getChannel();
        // 使用transferForm完成拷贝
        outputStreamChannel.transferFrom(inputStreamChannel, 0, inputStreamChannel.size());
        // 关闭
        outputStreamChannel.close();
        inputStreamChannel.close();
    }

(4)关于Buffer和Channel的注意事项和细节

1)ByteBuffer支持类型化的put和get,put放入的是什么数据类型,get就应该使用响应的数据类型取出来,否则可能有BufferUnderflowException异常

2)可以将一个普通Buffer转换只读Buffer

buffer.asReadOnlyBuffer();

3)NIO还提供了MappedByteBuffer,可以让文件直接在内存(堆外的内存)中进行修改,而如何同步到文件由NIO来完成。

3)上诉例子都是通过一个Buffer完成的,NIO还支持通过多个Buffer(Buffer数组)完成读写操作,即Scattering和Gatering

MappedByteBuffer说明:

/**
 * MappedByteBuffer(堆外内存)修改,操作系统不需要拷贝一次
 * 1、可让文件直接在内存修改
 */
public class MappedByteBufferTest {
    public static void main(String[] args) throws Exception {
        RandomAccessFile randomAccessFile = new RandomAccessFile("1.txt", "rw");
        // 获取对应通道
        FileChannel channel = randomAccessFile.getChannel();
        /**
         * 参数1:FileChannel.MapMode.READ_WRITE   使用读写模式
         * 参数2:0    可以直接修改的起始位置
         * 参数3:5    是映射到内存的大小,即将1.txt的多少个字节映射到内存
         * 可以直接修改的范围就是[0, 5)
         */
        MappedByteBuffer mappedByteBuffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, 5);
        mappedByteBuffer.put(0, (byte) 'H');
        mappedByteBuffer.put(3, (byte) '9');

        randomAccessFile.close();
    }
}

 Buffer的分散与聚集:

/**
 * Scattering:将数据写入到Buffer时,可以采用Buffer数组,依次写入
 * Gathering:从Buffer读取数据时,可以采用Buffer数组,依次读取
 */
public class ScatteringAndGathering {
    public static void main(String[] args) throws IOException {
        // 使用ServerSocketChannel 和 SocketChannel 网络
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        InetSocketAddress inetSocketAddress = new InetSocketAddress(7777);

        // 绑定端口到socket并启动
        serverSocketChannel.socket().bind(inetSocketAddress);
        // 创建buffer数组
        ByteBuffer[] byteBuffers = new ByteBuffer[2];
        byteBuffers[0] = ByteBuffer.allocate(5);
        byteBuffers[1] = ByteBuffer.allocate(3);

        // 等待客户端连接
        SocketChannel socketChannel = serverSocketChannel.accept();
        int msgLen = 8; // 假设从客户端接收8个字节
        // 循环读取
        while (true) {
            int byteRead = 0;
            while (byteRead < msgLen) {
                long read = socketChannel.read(byteBuffers);
                byteRead += read; // 累计读取的字节数
                System.out.println("byteRead = " + byteRead);
                // 使用流打印,查看当前buffer的position和limit
                Arrays.asList(byteBuffers).stream().map(buffer -> "position = " + buffer.position() + "\tlimit = " + buffer.limit()).forEach(System.out::println);
            }
            // 将所有的buffer进行flip
            Arrays.asList(byteBuffers).forEach(buffer -> buffer.flip());
            // 将数据读出并显示到客户端
            long byteWrite = 0;
            while (byteWrite < msgLen) {
                long l =socketChannel.write(byteBuffers);
                byteWrite += l;
            }
            // 将所有的buffer进行clear
            Arrays.asList(byteBuffers).forEach(buffer -> buffer.clear());
            System.out.println("byteRead = " + byteRead + "\tbyteWrite = " + byteWrite + "\tmsgLen = " + msgLen);
        }
    }
}

2.3.5 Selector(选择器)

(1)基本介绍:

1)Java的NIO用非阻塞的IO方式。可用一个线程,处理多个的客户端连接,这时就会用到Selector

2)Selector能够检测多个注册的通道上是否有事件发生(多个Channel以事件的方式可以注册到同一个Selector),如果有事件发生,便获取事件然后针对每个事件进行相应的处理。这样就可以以一个单线程去管理多个通道,也就是管理多个连接和请求。

3)只有在连接真正有读写事件发生时,才会进行读写,降低了系统开销,且不必为每个连接都创建一个线程更不用去维护多个线程

4)避免了多线程之间的上下文切换导致的开销

(2)特点说明:

1)Netty的IO线程NioEventLoop聚合了Selector,可以同时并发处理多个客户端连接。

2)当线程从某个客户端Socket通道进行读写数据时,若没有数据可用时,该线程可以进行其他任务。

3)线程通常将非阻塞IO的空闲时间用于在其他通道上执行IO操作,所以单独的线程可以管理多个输入和输出通道。

4)由于读写操作都是非阻塞的,这就可以充分提升IO线程的运行效率,避免由于频繁I/O阻塞导致的线程挂起。

5)一个I/O线程可以并发处理N个客户端连接和读写操作,这从根本上解决了传统同步阻塞I/O一请求一线程的模式,架构的性能、弹性伸缩能力、可靠性都得到了极大提升。 

(3)Selector的API

Selector是一个抽象类,常用方法:

public abstract class Selector implements Closeable {
    public static Selector open(); // 得到一个选择器对象
    public int select(long timeout); // 监控所有注册的通道,当其中有IO操作可以进行时,将对应的SelectionKey加入到内部集合中并返回,参数用来设置超时时间
    public Set<SelectionKey> selectedKeys(); // 从内部集合中得到所有的SelectionKey
}

(4)NIO非阻塞网络编程原理分析图

1)当客户端连接时,会通过ServerSocketChannel得到SocketChannel

2)将SocketChannel注册到Selector上【register(Selector sel, int ops)】,一个Selector上可注册多个SocketChannel

3)注册后返回一个SelectionKey,并与该Selector关联

4)Selector进行监听select方法,返回有事件发生的通道的个数

5)进一步得到各个SelectionKey(有事件发生的)

6)再通过SelectionKey反向获取SocketChannel

7)通过得到的Channel完成业务逻辑

(5)举例说明

 实现服务端与客户端之间的数据简单通讯(非阻塞),客户端通过控制台输入数据,服务端打印数据。

服务端:

public class NIOServer {
    public static void main(String[] args) throws Exception {
        // 创建ServerSocketChannel,并通过创建ServerSocketChannel获取ServerSocket
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        // 得到一个Selector对象
        Selector selector = Selector.open();
        // 绑定一个端口,在服务端监听
        serverSocketChannel.socket().bind(new InetSocketAddress(7777));
        // 设置为非阻塞
        serverSocketChannel.configureBlocking(false);
        // 把ServerSocketChannel注册到selector关心事件为OP_ACCEPT (连接事件)
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        // 循环等待客户端连接
        while (true) {
            // 等待1秒
            if(selector.select(1000) == 0) { // 没有监听到事件
                System.out.println("服务器等待了1秒,无任何连接");
                continue;
            }
            // 如果返回的 > 0,就取得相关的selectionKey集合
            // 1、如果返回>0,表示已经获取到关注的事件
            // 2、selector.selectedKeys()返回关注事件的集合
            //      通过selectionKeys反向获取通道
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            // 遍历Set<SelectionKey>,使用迭代器遍历
            Iterator<SelectionKey> keyIterator = selectionKeys.iterator();
            while (keyIterator.hasNext()) {
                // 获取到SelectionKey
                SelectionKey key = keyIterator.next();
                // 根据key 对应通道发生的事件做相应的处理
                if (key.isAcceptable()) { // 如果是OP_ACCEPT,有新客户端连接
                    // 该客户端生成一个SocketChannel
                    SocketChannel socketChannel = serverSocketChannel.accept();
                    socketChannel.configureBlocking(false);
                    // 将socketChannel注册到selector,关注事件为OP_READ,同事给SocketChannel关联一个Buffer
                    socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));

                }
                // 如果是OP_READ,
                if (key.isReadable()) {
                    // 通过key反向获取对应的channel
                    SocketChannel channel = (SocketChannel) key.channel();
                    // 获取到该channel关联的buffer
                    ByteBuffer buffer = (ByteBuffer) key.attachment();
                    buffer.clear();
                    while (channel.read(buffer) > 0) {
                        buffer.flip();
                        byte[] bytes = new byte[buffer.limit()];
                        buffer.get(bytes);
                        System.out.println("from client : " + new String(buffer.array()));
                        buffer.clear();
                    }
                }
                // 手动从集合中移除当前的SelectionKey,防止重复操作
                keyIterator.remove();
            }

        }
    }
}

客户端:

public class NIOClient {
    public static void main(String[] args) throws Exception {
        // 得到一个网络通道
        SocketChannel socketChannel = SocketChannel.open();
        // 设置非阻塞
        socketChannel.configureBlocking(false);
        // 提供服务端的ip和端口
        InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1", 7777);
        // 连接服务器
        if (!socketChannel.connect(inetSocketAddress)) {
            while (!socketChannel.finishConnect()) {
                System.out.println("连接需要时间,客户端不会注册,可以做其它工作");
            }
        }
        // 连接成功,发送数据
//        String str = "Hello, World!";
//        ByteBuffer buffer = ByteBuffer.wrap(str.getBytes());
        Scanner scanner = new Scanner(System.in);
        while (true) {
            String str = scanner.nextLine();
            // 发送数据,将buffer中的数据写入到channel中
            socketChannel.write(ByteBuffer.wrap(str.getBytes()));
            //设置标识符退出客户端
            if ("quit".equals(str)) {
                System.out.println("退出聊天...");
                break;
            }
        }
    }
}

2.3.6 群聊系统

服务端:

步骤:

1)服务器启动并监听7777

2)服务器接收客户端信息,并实现转发(处理上线和离线)

public class GroupChatServer {

    // 定义属性
    private Selector selector;
    private ServerSocketChannel listenChannel;
    private static final int PORT = 7777;

    // 构造器
    public GroupChatServer() {
        try {
            // 得到选择器
            selector = Selector.open();
            // ServerSocketChannel
            listenChannel = ServerSocketChannel.open();
            // 绑定端口
            listenChannel.socket().bind(new InetSocketAddress(PORT));
            // 设置非阻塞模式
            listenChannel.configureBlocking(false);
            // 将该listenChannel注册到selector
            listenChannel.register(selector, SelectionKey.OP_ACCEPT);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    // 监听
    public void listen() {
        try {
            // 循环处理
            while (true) {
                int count = selector.select();
                if (count > 0) { // 有事件待处理
                    // 遍历得到selectionKey集合
                    Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
                    while (iterator.hasNext()) {
                        // 取出selectionkey
                        SelectionKey key = iterator.next();
                        // 监听到accept
                        if (key.isAcceptable()) {
                            SocketChannel socketChannel = listenChannel.accept();
                            // 设置socketChannel为非阻塞
                            socketChannel.configureBlocking(false);
                            // 将socketChannel注册到selector
                            socketChannel.register(selector, SelectionKey.OP_READ);
                            // 提示
                            System.out.println(socketChannel.getRemoteAddress() + " 上线");
                        }
                        // 通道发生read事件,即通道是可读状态
                        if (key.isReadable()) {
                            // TODO
                            readData(key);
                        }
                        // 删除当前的selectionkey
                        iterator.remove();
                    }
                } else {
                    System.out.println("等待...");
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

    // 读取客户端消息
    private void readData(SelectionKey key) {
        // 定义一个SocketChannel
        SocketChannel channel = null;
        try {
            // 得到channel
            channel = (SocketChannel) key.channel();
            // 创建buffer
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            buffer.clear();
            int count = channel.read(buffer);
            String msg = new String(buffer.array(), 0, count);
            buffer.flip();
            System.out.println("from " + channel.getRemoteAddress() + " : " + msg);
            // 向其它客户端转发消息
            sendINfoToOtherClients(msg, channel);
        } catch (IOException e) {
            try {
                System.out.println(channel.getRemoteAddress() + " 离线了...");
                // 取消注册
                key.channel();
                // 关闭通道
                channel.close();
            } catch (IOException ioException) {
                ioException.printStackTrace();
            }
        }
    }

    // 转发消息给其它客户(通道)
    private void sendINfoToOtherClients(String msg, SocketChannel self) throws IOException {
//        System.out.println("服务器转发消息...");
        // 遍历所有注册到selector上的SocketChannel,并排除自己(self)
        for (SelectionKey key : selector.keys()) {
            // 可能取出ServerSocketChannel,故获取类型为Channel,转发时排除掉ServerSocketChannel
            Channel targetChannel = key.channel();
            // 排除发送信息的自己
            if (targetChannel instanceof SocketChannel && targetChannel != self) {
                // 转型
                SocketChannel dest = (SocketChannel) targetChannel;
                // 将msg存储到buffer
                ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes());
                // 将buffer的数据写入通道
                dest.write(buffer);
            }

        }
    }

    // 服务器启动
    public static void main(String[] args) {
        // 创建一个服务器对象
        GroupChatServer groupChatServer = new GroupChatServer();
        groupChatServer.listen();
    }
}

客户端:

步骤:

1)连接服务器

2)发送信息

3)接收服务器信息

public class GroupChatClient {

    // 定义相关的属性
    private final String HOST = "127.0.0.1"; // IP
    private final int PORT = 7777;
    private Selector selector;
    private SocketChannel socketChannel;
    private String username;

    // 构造器
    public GroupChatClient() {
        try {
            // 获取selector
            selector = Selector.open();
            // 连接到服务器
            socketChannel = SocketChannel.open(new InetSocketAddress(HOST, PORT));
            // 设置非阻塞
            socketChannel.configureBlocking(false);
            // 将channel注册到selector
            socketChannel.register(selector, SelectionKey.OP_READ);
            // 得到username
            username = socketChannel.getLocalAddress().toString().substring(1);
            System.out.println(username + " is OK...");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    // 向服务器发送消息
    public void sendInfo(String info) {
        info = username + " 说:" + info;
        try {
            socketChannel.write(ByteBuffer.wrap(info.getBytes()));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    // 读取从服务端回复的消息
    public void readInfo() {
        try {
            int readChannels = selector.select();
            if (readChannels > 0) { // 有可用的通道
                Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
                while (iterator.hasNext()) {
                    SelectionKey key = iterator.next();
                    iterator.remove();
                    if (key.isReadable()) {
                        // 得到相关的通道
                        SocketChannel channel = (SocketChannel) key.channel();
                        // 得到一个Buffer
                        ByteBuffer buffer = ByteBuffer.allocate(1024);
                        buffer.clear();
                        // 读取
                        int count = channel.read(buffer);
                        buffer.flip();
                        // 把读取从缓冲区读取的数据转成字符串
                        String msg = new String(buffer.array(),0, count);
                        System.out.println(msg);
                    }
                }
            }
        } catch (IOException e) {
            System.out.println("没有可用的通道");
        }
    }

    public static void main(String[] args) {
        // 启动客户端
        GroupChatClient client = new GroupChatClient();
        // 启动一个线程
        new Thread() {
            @Override
            public void run() {
                while (true) {
                    client.readInfo();
                }
            }
        }.start();
        // 发送数据给服务器端
        Scanner scanner = new Scanner(System.in);
        while (true) {
            String msg = scanner.nextLine();
            client.sendInfo(msg);
        }
    }
}

2.3.7 NIO与零拷贝

(1)基本介绍

1)零拷贝是网络编程的关键,很多性能优化都离不开

2)在Java中,常用的零拷贝有mmap(内存映射)和sendFile。

(2)mmap优化

mmap通过内存映射,将文件映射到内核缓冲区,同时,用户空间可以共享内核空间的数据。这样,在进行网络传输时,就可以减少内核空间到用户空间的拷贝次数。

(3)sendFile优化

Linux2.1版本提供了sendFile函数,其基本原理:数据不经过用户态,直接从内核缓冲区进入到SocketBuffer,同时,由于和用户态完全无关,就减少了一次上下文切换。 

(4) mmap和sendFile的区别

1)mmap适合小数据量读写,sendFile适合大文件传输

2)mmap需要4次上下文切换,3次数据拷贝;sendFile需要3次上下文切换,最少2两次数据拷贝

3)sendFile可以利用DMA(直接内存访问)方式,减少CPU拷贝,mmap则不能(必须从内核拷贝到Socket缓冲区)

(4)零拷贝传递文件

服务器端:

public class NewIOServer {
    public static void main(String[] args) throws IOException {
        InetSocketAddress address = new InetSocketAddress(7001);
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        ServerSocket serverSocket = serverSocketChannel.socket();
        // 绑定端口
        serverSocket.bind(address);
        // 创建buffer
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        while (true) {
            SocketChannel socketChannel = serverSocketChannel.accept();
            int readCount = 0;
            while (-1 != readCount) {
                readCount = socketChannel.read(buffer);
                buffer.rewind();
            }
        }
    }
}

 服务器端:

public class NewIOClient {
    public static void main(String[] args) throws IOException {
        SocketChannel socketChannel = SocketChannel.open();
        socketChannel.connect(new InetSocketAddress("localhost", 7001));
        String filename = "G:\\学习\\jdk-8u162-linux-x64.tar.gz";
        // 得到一个文件channel
        FileChannel fileChannel = new FileInputStream(filename).getChannel();
        // 准备发送文件
        long startTime = System.currentTimeMillis();
        // 在linux下transferTo可以完成传输
        // 在windows下一次调用transferTO只能发送8m,需要分段传输文件,而且需要注意传输时的位置
        int byteWritten = 0; // 已写入的大小
        long byteCount = fileChannel.size();
        while (byteWritten < byteCount) {
            byteWritten += fileChannel.transferTo(byteWritten, byteCount - byteWritten, socketChannel);
        }
        System.out.println("发送的总的字节数:" + byteWritten + " \t耗时" + (System.currentTimeMillis() - startTime));
    }
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值