概述

初步了解了NIO核心组件的API,也大致知道了如何启动一个网络IO服务和客户端后。本篇在此基础上做一些补充,把一些必须要理解的

正文

ServerSocketChannel的accept方法和Selecor的select

在ServerSocketChannel的API中我们可以通过accept方法监听传入的连接,有传入连接的时候返回一个SocketChannel。也可以注册ACCEPT事件到Selector上,然后通过Selector的select方法监听传入的连接。

需要特别注意的是,accept方法返回的是一个连接,并不是连接集合,而通过Selector的select方法返回的是符合事件要求数量,然后通过selectedKeys获得SelectoinKey集合。

进行SelectionKey集合遍历处理的时候,需要删除元素

注意到通过selectedKeys获得SelectoinKey集合后,操作完会进行remove,或者遍历操作后一次性进行clear操作。因为Selector维护着selectedKeys,所以在操作完后如果不进行删除,那么下次轮询还会存在,而出现混乱,那么Selector为什么不自己删除呢,这就不对了,毕竟人家是告诉你现在的事件集合让你处理,你处理是否结束并不清楚,所以由真正处理方来决定删除最合理了。

selector.select()一直返回大于0问题

在前面的例子的Server端代码中,当client发起连接然后发送消息后断开连接,服务端通过read方法获取数据并且执行selectionKeys.remove()方法,但是在下一次while循环中selector.select()结果还是大于0,debug代码可以发现此时selectionKey任然是isReadable的,只是read方法返回并不大于0,所以不会进入第二个while循环中,虽然如此,这个时候是一直死循环在执行第一个while循环的,并不是我们预期的阻塞在selector.select()上等待就绪事件的触发。

客户端关闭连接的时候,read会返回-1,所以我们可以在第二次循环的时候直接关闭Channel。

ByteBuffer的复用

可以看到在服务端read数据的时候使用ByteBuffer承接,获得的数据是不会超过ByteBuffer的长度,在使用完后就进行clear操作切换到读模式进行复用,否则每次读事件都需要为ByteBuffer申请新的内存,非常浪费。当再次需要切换到写模式的时候使用flip方法切换。

Selector.wakeup()

有意思的是Selector采用自己和自己建的TCP连接(Windows)或pipe(Linux)来实现wakeup,因为只要向这个连接或pipe里发点数据,就可以唤醒select了。

以下两个文章透析了这部分的知识:



SelectionKey.attach(Object ob)

SelectionKey关联一个对象,这个对象可以通过attachment方法取出,这点在前篇已经提过,这里再说明一下是要强调一遍通过这个方法,可以比较方便的围绕selectorKey来进行处理IO流程的拆分工作。比如,我们设计一个接口,所有的绑定对象都实现这个接口,接口中定义一个执行方法,在Accept的SelectorKey上绑定专门处理Accept时间的对象,在READ事件绑定专门处理的对象,如此代码可以是一致的都是从SelectorKey中拿到对象调用执行方法。

关于拆包粘包

前置说明一下这个概念,首先需要理解的是TCP是流式协议,传输内容像流水一样是没有明确段落的概念的,而实际使用时我们会把通信的消息进行定义,也就会把一段完整内容定义成有业务含义的消息,那么天然是需要解决这个矛盾的。这也是接触Netty时很快会了解解决拆包粘包问题的办法,这里只是提一下,了解是有这个问题的即可。

代码

在前面的代码基础上,进行了改造,实现一个C/S模型的文件传输功能,客户端将本地文件通过Channel传输给服务端,服务端将文件写到自己本地。

Client代码:

public static void start() throws IOException {
        InetSocketAddress inetSocketAddress = new InetSocketAddress(20023);
        SocketChannel socketChannel = SocketChannel.open();
        socketChannel.connect(inetSocketAddress);
        ByteBuffer byteBuffer = ByteBuffer.allocate(204800);
        FileChannel fileChannel =  new FileInputStream("/Users/dongchao/test").getChannel();
        if (socketChannel.finishConnect()) {
            while (fileChannel.read(byteBuffer) > 0) {
                byteBuffer.flip();
                socketChannel.write(byteBuffer);
                byteBuffer.clear();
            }
        }
        fileChannel.close();
    }

    public static void main(String[] args) throws IOException {
        start();
    }

Server端代码:

public class Server {

    public static void start() throws IOException {
        // 开启Selector
        Selector selector = Selector.open();
        // 开启一个Server Socket Channel 用于监听连接Accept事件
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        // 设置成非阻塞模式
        serverSocketChannel.configureBlocking(false);
        // 监听端口
        serverSocketChannel.bind(new InetSocketAddress(20023));
        // 设置Selector 多路复用监听Accept事件
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        // 产生一个文件名
        String localFile = getFileName();
        // 获取一个File Channel
        FileOutputStream fos = new FileOutputStream(localFile);
        FileChannel outFileChannel = fos.getChannel();
        // 记录文件字节量
        long outLength = 0;
        ByteBuffer byteBuffer = ByteBuffer.allocate(204800);

        // 阻塞选择准备好进行I/O操作的键
        while(selector.select() > 0) {
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = selectionKeys.iterator();
            while (iterator.hasNext()) {
                SelectionKey selectionKey = iterator.next();
                if (selectionKey.isAcceptable()) {
                    ServerSocketChannel serverSocketChannel1 = (ServerSocketChannel) selectionKey.channel();
                    SocketChannel socketChannel = serverSocketChannel1.accept();
                    socketChannel.configureBlocking(false);
                    socketChannel.register(selector, SelectionKey.OP_READ);
                } else if (selectionKey.isReadable()) {
                    SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
                    int length;
                    // 对于Socket Channel来说是从Channel上读取数据,写入到ByteBuffer
                    while ((length = socketChannel.read(byteBuffer)) > 0) {
                        // 切换成读模式
                        byteBuffer.flip();
                        // 对于File Channel来说是读取ByteBuffer数据,写入到Channel
                        outFileChannel.write(byteBuffer);
                        outLength = outLength + length;
                        // 切换成写模式
                        byteBuffer.clear();
                        outFileChannel.force(true);
                    }
                    // read 返回-1 客户端关闭连接
                    if (length < 0) {
                        socketChannel.close();
                    }
                }
                iterator.remove();
            }
            System.out.print("file length : " + outLength);
        }
        fos.close();
        outFileChannel.close();
    }

    private static String getFileName() {
        long currentTimeMillis = System.currentTimeMillis();
        return "/Users/dongchao/" + currentTimeMillis;
    }