JAVA-NIO(2)

本系列博客转载自:海纳的知乎专栏
https://www.zhihu.com/people/hinus/activities
JAVA NIO系列,本人补充一些“作业”
nio(2):channel
https://zhuanlan.zhihu.com/p/27365009



本节课是小密圈《进击的Java》新人第十六周第二课,这一节课,我们讲一下Java NIO中另外第二个重要的结构,这就是channel。
NIO中通过channel封装了对数据源的操作,通过 channel 我们可以操作数据源,但又不必关心数据源的具体物理结构。

这个数据源可能是多种的。比如,可以是文件,也可以是网络socket。在大多数应用中,channel 与文件描述符或者 socket 是一一对应的。

在Java IO中,基本上可以分为文件类和Stream类两大类。Channel 也相应地分为了FileChannel 和 Socket Channel,其中 socket channel 又分为三大类,一个是用于监听端口的ServerSocketChannel,第二类是用于TCP通信的SocketChannel,第三类是用于UDP通信的DatagramChannel。

Channel的作用

channel 最主要的作用还是用于非阻塞式读写。这一条我们以后会讲,今天,主要看一下如何使用Channel进行编程。

Channel可以使用 ByteBuffer 进行读写,这是它的一个方便之处。我们先看一个例子,这个例子中,创建了一个ServerSocketChannel 并且在本机的8000端口上进行监听。可以看到,bind, listen这些操作与我们之前讲的这一节课:Java网络编程(二):套接字 - 知乎专栏,是完全对应的,可以猜想到SocketChannel的具体实现,本质上也是对socket的一种封装而已。
public class WebServer {
    public static void main(String args[]) {
        try {
            ServerSocketChannel ssc = ServerSocketChannel.open();
            ssc.socket().bind(new InetSocketAddress("127.0.0.1", 8000));
            SocketChannel socketChannel = ssc.accept();

            ByteBuffer readBuffer = ByteBuffer.allocate(128);
            socketChannel.read(readBuffer);

            readBuffer.flip();
            while (readBuffer.hasRemaining()) {
                System.out.println((char)readBuffer.get());
            }

            socketChannel.close();
            ssc.close();
        }
        catch (IOException e) {
            e.printStackTrace();
        }
    }
}

用静态的 open( )工厂方法创建一个新的 ServerSocketChannel 对象,将会返回同一个未绑定的 java.net.ServerSocket 关联的通道。这个相关联的 ServerSocket 可以通过在 ServerSocketChannel 上调用 socket( )方法来获取。我们在这个例子中,使用了socket().bind来实现socket的绑定。

客户端代码:
public class WebClient {
    public static void main(String[] args) {
        SocketChannel socketChannel = null;
        try {
            socketChannel = SocketChannel.open();
            socketChannel.connect(new InetSocketAddress("127.0.0.1", 8000));

            ByteBuffer writeBuffer = ByteBuffer.allocate(128);
            writeBuffer.put("hello world".getBytes());

            writeBuffer.flip();
            socketChannel.write(writeBuffer);
            socketChannel.close();
        } catch (IOException e) {
        }
    }
}

新创建的 SocketChannel 虽已打开却是未连接的。在一个未连接的 SocketChannel 对象上尝试一 个 I/O 操作会导致 NotYetConnectedException 异常。我们可以通过在通道上直接调用 connect() 方法或在通道关联的 Socket 对象上调用 connect()来实现socket的连接。一旦一个 socket 通道被连接,它将保持连接状态直到被关闭。我们可以通过调用 isConnected()方法来测试某个 SocketChannel 是否已连接。

本节课程所使用的ByteBuffer上一课也有讲解,如果对flip,get, put 等操作还是感到不理解的,可以去查看上一节课程。

先运行服务端,再运行客户端,所得到的结果如下:


就是说,我们把客户端发送过来的字符串逐字符地打印出来了。

其实,熟悉Java IO的读者都知道,我们同样可以使用InputStream和OutputStream进行字节流的读写,而且看起来,ByteBuffer似乎还没有直接使用byte[] 进行读写来得直观。实际上,channel 最大的作用并不仅限于此,它的最大作用是封装了异步操作,后面我会在 selector 的地方详细解释。

Scatter / Gather

Channel 提供了一种被称为 Scatter/Gather 的新功能,也称为本地矢量 I/O。Scatter/Gather 是指在多个缓冲区上实现一个简单的 I/O 操作。对于一个 write 操作而言,数据是从几个缓冲区(通常就是一个缓冲区数组)按顺序抽取(称为 gather)并使用 channel 发送出去。缓冲区本身并不需要具备这种 gather 的能力。gather 过程等效于全部缓冲区的内容被连结起来,并在发送数据前存放到一个大的缓冲区中。对于 read 操作而言,从 通道读取的数据会按顺序被散布(称为 scatter)到多个缓冲区,将每个缓冲区填满直至通道中的数据或者缓冲区的最大空间被消耗完。

大多数现代操作系统都支持本地矢量 I/O(native vectored I/O)。我们在一个通道上发起一个 Scatter/Gather 操作时,该请求会被翻译为适当的本地调用来直接填充或抽取缓冲区。这是一个很大的进步,因为减少或避免了缓冲区拷贝和系统调用。Scatter/Gather 应该使用直接的 ByteBuffers 以从本地 I/O 获取最大性能优势。

例如,我们可以把客户端改写成这个样子:
public class WebClient {
    public static void main(String[] args) {
        SocketChannel socketChannel = null;
        try {
            socketChannel = SocketChannel.open();
            socketChannel.connect(new InetSocketAddress("127.0.0.1", 8000));

            ByteBuffer writeBuffer = ByteBuffer.allocate(128);
            ByteBuffer buffer2 = ByteBuffer.allocate(16);
            writeBuffer.put("hello ".getBytes());
            buffer2.put("world".getBytes());

            writeBuffer.flip();
            buffer2.flip();
            ByteBuffer[] bufferArray = {writeBuffer, buffer2};
            socketChannel.write(bufferArray);
            socketChannel.close();
        } catch (IOException e) {
        }
    }
}

这样就实现了一个Gather IO。Gather IO 在现在还看不出来有什么作用。我们后面会看到,在并发场景下,这是一个非常好用的特性。

今天就讲解到这里了。作业:

1. 把服务端改造成Scatter IO

2. 想一想聊天室背后的网络设计是怎么样的?

===========================================================================
完成作业现在:
1、
public class WebServer {
    public static void main(String args[]) {
        try {
            ServerSocketChannel ssc = ServerSocketChannel.open();
            ssc.socket().bind(new InetSocketAddress("127.0.0.1", 8000));
            SocketChannel socketChannel = ssc.accept();

            ByteBuffer readBuffer1 = ByteBuffer.allocate(5);
            ByteBuffer readBuffer2 = ByteBuffer.allocate(5);
            ByteBuffer[] bufferArray = { readBuffer1, readBuffer2 };

            socketChannel.read(bufferArray);

            readBuffer1.flip();
            readBuffer2.flip();
            while (readBuffer1.hasRemaining()) {
                System.out.println((char)readBuffer1.get());
            }
             System.out.print("second buffer");
            while (readBuffer2.hasRemaining()) {
                System.out.println((char)readBuffer2.get());
            }

            socketChannel.close();
            ssc.close();
        }
        catch (IOException e) {
            e.printStackTrace();
        }
    }
}


上面代码主要看分配之快。
转一段 http://ifeve.com/java-nio-scattergather/
注意buffer首先被插入到数组,然后再将数组作为channel.read() 的输入参数。read()方法按照buffer在数组中的顺序将从channel中读取的数据写入到buffer,当一个buffer被写满后,channel紧接着向另一个buffer中写。

Scattering Reads在移动下一个buffer前,必须填满当前的buffer,这也意味着它不适用于动态消息(译者注:消息大小不固定)。换句话说,如果存在消息头和消息体,消息头必须完成填充(例如 128byte),Scattering Reads才能正常工作

如果,我把第一个缓冲区分配到11,那么结果将是
h
e
l
l
o

w
o
r
l
d
second buffer
也就是第二个缓冲区是空的。验证了Scattering Reads先填满一个缓冲区再填满第二个缓冲区的逻辑。

2、聊天室背后的网络设计。
最简单的想法就是多个客户端对应一个服务端。
可以是阻塞IO和非阻塞IO
服务端启动,不断监控端口,等待连接,当有连接就绪之后,开始准备读写数据。
客户端,同样不断监控输入,然后通过socket想服务端发送数据,服务端将数据显示到界面。

阻塞IO可以用多线程或者线程池实现多客户端同时连接。
NIO可以用单线程或多线程实现多客户端同时连接。


上一篇: http://zxp209.iteye.com/admin/blogs/2396769

猜你喜欢

转载自zxp209.iteye.com/blog/2396809