NIO 简单实例

前言

在 Java 中,数据传输 IO 模型大概分为三类:BIO(同步阻塞)、NIO(同步非阻塞)、AIO(异步非阻塞)。

在 BIO 中,服务器会针对每一个连接都去开一个新的线程进行处理,这样实现非常简单快速,但是对于资源消耗巨大,于是提出了 NIO。

在看本博客之前建议先了解一下 NIO 的基本用法。

一、NIO简介

NIO 是一种基于事件驱动的 IO 模型,面向缓冲区编程,NIO有三大核心部分:Channel(通道)、Buffer(缓冲区)、Selector(选择器)。通俗理解,NIO 的一个线程管理一个SelectorSelector中管理多个客户端Channel,也就是说一个线程可以处理多个操作。

下面是三大组件的关系图:
NIO 简单实例_第1张图片

二、聊天室实例

我们通过 NIO ,来实现一个简单的群聊系统,进而学习 NIO 在服务端与客户端中所用到的API。

1.服务端介绍

①首先我们在服务端定义了 SelectorServerSocketChannel,以及当前服务器暴露的端口号;
在这里插入图片描述
②初始化这些组件信息;
NIO 简单实例_第2张图片

  1. 打开选择器(Channel都会由Selector统一管理);
  2. 打开服务端 Socket 通道,并绑定监听的端口号(类似于 BIO 中的 ServerSocket);
  3. serverSocketChannel.configureBlocking(false);设置当前通道为非阻塞状态;
  4. 将服务器 Socket 通道注册进Selector,由选择器统一管理。

③服务端监听并处理客户端事件

需要清楚下面两个方法的作用。

selector.select():返回已经准备就绪的通道个数(这些通道包含你感兴趣的的事件)。
	比如:你对读就绪的通道感兴趣,那么select()方法就会返回读事件已经就绪的那些通道。
selector.selectedKeys():获取就绪通道的事件列表。

SelectionKey中封装了事件的四种类型:

  • OP_READ:可读事件;值为:1<<0
  • OP_WRITE:可写事件;值为:1<<2
  • OP_CONNECT:客户端连接服务端的事件(tcp连接),一般为创建SocketChannel客户端channel;值为:1<<3
  • OP_ACCEPT:服务端接收客户端连接的事件,一般为创建ServerSocketChannel服务端channel;值为:1<<4

服务端不断监听是否有事件发生,根据不同的事件类型做不同的处理。

public void listen() {
        try {
            while (true) {
                int count = selector.select(2000);
                if (count > 0) {
                    Set<SelectionKey> selectionKeys = selector.selectedKeys();

                    Iterator<SelectionKey> iterator = selectionKeys.iterator();
                    while (iterator.hasNext()) {
                        SelectionKey selectionKey = iterator.next();
                        if (selectionKey.isAcceptable()) {
                            SocketChannel socketChannel = serverSocketChannel.accept();
                            socketChannel.configureBlocking(false);
                            socketChannel.register(selector, SelectionKey.OP_READ);
                            System.out.println(socketChannel.getRemoteAddress() + " 上线...");
                        }
                        if (selectionKey.isReadable()) {
                            readClientData(selectionKey);
                        }

                        iterator.remove();
                    }
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

在处理完这个事件之后一定要进行移除,防止多线程重复操作。

服务器 读取客户端数据、转发客户端数据 方法可参考后面总代码。

2.客户端介绍

①通过ip和端口连接服务器端并且注册到 Selector
NIO 简单实例_第3张图片
②开启一个线程不断的获取事件进行处理
NIO 简单实例_第4张图片
这样就算完成了服务端和用户端的实现,下面是所有的代码:

服务端 →

package com.kiger.nio.groupchat;

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

/**
 * @author zk_kiger
 * @date 2020/5/14
 */

public class GroupChatServer {

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

    public GroupChatServer() {
        init();
    }

    public static void main(String[] args) {
        GroupChatServer chatServer = new GroupChatServer();
        chatServer.listen();
    }

    /**
     * 监听
     */
    public void listen() {
        try {
            while (true) {
                int count = selector.select(2000);
                if (count > 0) {
                    Set<SelectionKey> selectionKeys = selector.selectedKeys();

                    Iterator<SelectionKey> iterator = selectionKeys.iterator();
                    while (iterator.hasNext()) {
                        SelectionKey selectionKey = iterator.next();
                        if (selectionKey.isAcceptable()) {
                            SocketChannel socketChannel = serverSocketChannel.accept();
                            System.out.println("监听到的 socketChannel:" + socketChannel.hashCode());
                            socketChannel.configureBlocking(false);
                            socketChannel.register(selector, SelectionKey.OP_READ);
                            System.out.println(socketChannel.getRemoteAddress() + " 上线...");
                        }
                        if (selectionKey.isReadable()) {
                            readClientData(selectionKey);
                        }

                        iterator.remove();
                    }
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 读取客户端发送来数据
     */
    private void readClientData(SelectionKey selectionKey) {
        SocketChannel channel = null;
        try {
            channel = (SocketChannel) selectionKey.channel();
            System.out.println("接收到client SocketChannel:" + channel.hashCode());
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            int count = channel.read(buffer);
            if (count > 0) {
                String msg = new String(buffer.array());
                System.out.println(msg + " from " + channel.getRemoteAddress());
                // 服务器向其他客户端转发消息
                forwardOtherClient(msg, channel);
            }
        } catch (IOException e) {
            try {
                System.out.println(channel.getRemoteAddress() + " 下线..");
                // 取消注册,并关闭通道
                selectionKey.cancel();
                channel.close();
            } catch (IOException e1) {
                e1.printStackTrace();
            }
        }
    }

    /**
     * 向其他客户端发送消息并排除自己
     */
    private void forwardOtherClient(String message, SocketChannel self) {
        try {
            for (SelectionKey key : selector.keys()) {
                Channel channel = key.channel();
                if (channel instanceof SocketChannel && channel != self) {
                    SocketChannel dest = (SocketChannel) channel;
                    dest.write(ByteBuffer.wrap(message.getBytes()));
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

    /**
     * 初始化
     */
    private void init() {
        try {
            selector = Selector.open();
            serverSocketChannel = ServerSocketChannel.open();
            serverSocketChannel.socket().bind(new InetSocketAddress(PORT));
            serverSocketChannel.configureBlocking(false);
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

客户端 →

package com.kiger.nio.groupchat;

import java.io.IOException;
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;

/**
 * @author zk_kiger
 * @date 2020/5/15
 */

public class GroupChatClient {

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

    public GroupChatClient() {
        init();
    }

    public static void main(String[] args) {
        GroupChatClient chatClient = new GroupChatClient();
        // 开启线程不断读取数据
        new Thread(() -> {
            chatClient.readInfo();
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();

        // 发送数据
        Scanner input = new Scanner(System.in);
        while (input.hasNextLine()) {
            chatClient.sendInfo(input.nextLine());
        }
    }

    /**
     * 读取服务器发送的消息
     */
    public void readInfo() {
        try {
            int count = selector.select();
            if (count > 0) {
                Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
                while (iterator.hasNext()) {
                    SelectionKey selectionKey = iterator.next();
                    if (selectionKey.isReadable()) {
                        SocketChannel channel = (SocketChannel) selectionKey.channel();
                        ByteBuffer buffer = ByteBuffer.allocate(1024);
                        channel.read(buffer);
                        String msg = new String(buffer.array()).trim();
                        System.out.println(msg);
                    }
                    iterator.remove();
                }
            }
        } 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();
        }
    }

    /**
     * 初始化
     */
    private void init() {
        try {
            selector = Selector.open();
            socketChannel = SocketChannel.open(new InetSocketAddress(HOST, PORT));
            socketChannel.configureBlocking(false);
            socketChannel.register(selector, SelectionKey.OP_READ);
            userName = socketChannel.getLocalAddress().toString();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

三、文件传输

在网络传输中,文件的传输也非常重要,这里介绍 NIO 中使用零拷贝对文件进行传输。

①服务端实现
NIO 简单实例_第5张图片
②客户端使用零拷贝方法进行文件传输
NIO 简单实例_第6张图片
关键代码只有一句:fileChannel.transferTo(transferCount, maxSize, socketChannel);
将文件通道关联的文件传输给指定的通道。

需要注意:在 Linux 下这个方法对文件大小是没有限制的,但是在 Windows下每次只能传输 8M 大小,所以我做了上面的处理。

你可能感兴趣的:(Java,网络)