Java网络编程——第十章 非阻塞I/O

使用非阻塞IO方式实现chargen收发协议

客户端
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.SocketChannel;
import java.nio.channels.WritableByteChannel;

public class ChargenClient {
    public static final int DEFAULT_PORT = 19;
    public static final String HOST= "localhost";
    public static void main(String[] args) {
        try {
            SocketAddress address = new InetSocketAddress(HOST, DEFAULT_PORT);

            SocketChannel client = SocketChannel.open(address);
            // 注意client的通道是阻塞模式,修改为非阻塞模式,则通过
            //client.configureBlocking(false);

            ByteBuffer buffer = ByteBuffer.allocate(74);
            // 使用通道的方式将缓冲区的数据写入标准输出
            // Channels.newChannel是构建一个写入目标流的通道
            WritableByteChannel out = Channels.newChannel(System.out);

            while (client.read(buffer) != -1) {
                // 回绕缓冲区,该方法将缓冲区准备为数据传出状态,输出通道从数据开头而不是末尾开始
                // 回绕缓冲区保持缓冲区数据不变,只是准备写入而不是读取
                buffer.flip();
                out.write(buffer);
                // 要重用现有缓冲区,再次读取之前需要清空缓冲区;该方法不会改变缓冲区数据
                // 只是简单重置索引
                buffer.clear();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}


服务端
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;

public class ChargenServer {
    public static final int DEFAULT_PORT = 19;

    // 服务器端单线程处理多客户端连接
    public static void main(String[] args) {
        int port = DEFAULT_PORT;
        System.out.println("listening for connections on port: " + port);

        // 可打印ASCII共95个,由于每打印一行需要左移一个字符,这里生成92*2个只是为了打印方便
        byte[] rotation = new byte[95 * 2];
        for (byte i = ' '; i <= '~'; i++) {
            rotation[i - ' '] = i;
            rotation[i + 95 - ' '] = i;
        }

        ServerSocketChannel serverChannel;
        // Selector只接受非阻塞通道
        Selector selector;
        try {
            serverChannel = ServerSocketChannel.open();
            // 要绑定端口,需要先用socket方法获取ServerSocket的对等端peer
            // 然后使用bind绑定端口
            ServerSocket ss = serverChannel.socket();
            InetSocketAddress address = new InetSocketAddress(DEFAULT_PORT);
            ss.bind(address);
            // 配置serverChannel为非阻塞模式,如果没有连接则accpet方法立即返回null, 如果不设置,accept方法将一直阻塞直到有连接进入
            serverChannel.configureBlocking(false);
            // 使用选择器迭代处理准备好的连接
            selector = Selector.open();
            // 向选择器注册对此通道的监视,对于chargen,只关心服务器Socket是否准备好接收一个新连接
            serverChannel.register(selector, SelectionKey.OP_ACCEPT);
        } catch (IOException e) {
            e.printStackTrace();
            return;
        }

        while (true) {
            try {
                // 检查是否有可操作的通道准备好接受IO操作
                selector.select();
            } catch (IOException e) {
                e.printStackTrace();
                break;
            }

            // 获取就绪通道的的SelectionKey的集合
            Set readyKey = selector.selectedKeys();
            // 迭代处理所有的key
            Iterator iterator = readyKey.iterator();
            while (iterator.hasNext()) {
                SelectionKey key = iterator.next();
                // 已经处理的key需要从集合中删除,标记该键已经处理
                // 若下次该key还再次准备好,还将继续出现在Set中
                iterator.remove();

                try {
                    // 测试该key是否已经准备好接收一个新的socket连接,即此时就绪的是服务器通道,接收一个新的Socket通道,将其添加到选择器
                    if (key.isAcceptable()) {
                        // 获取该key创建的channel
                        ServerSocketChannel server = (ServerSocketChannel) key.channel();
                        // 接受一个到此server通道的连接
                        SocketChannel client = server.accept();
                        System.out.println("accepted from " + client);
                        // 配置client的通道为非阻塞模式
                        client.configureBlocking(false);
                        SelectionKey key2 = client.register(selector, SelectionKey.OP_WRITE);

                        ByteBuffer buffer = ByteBuffer.allocate(74);
                        buffer.put(rotation, 0, 72);
                        buffer.put((byte) '\r');
                        buffer.put((byte) '\n');
                        // 回绕缓冲区
                        buffer.flip();
                        key2.attach(buffer);
                    } else if (key.isWritable()) {// 测试该key是否准备好写操作,即此时就绪的是Socket通道,向缓冲区写入数据
                        SocketChannel client = (SocketChannel) key.channel();
                        ByteBuffer buffer = (ByteBuffer) key.attachment();
                        if (!buffer.hasRemaining()) {
                            // 用下一行重新填充缓冲区
                            buffer.rewind();
                            // 得到上一次首字符
                            int first = buffer.get();
                            // 准备改变缓冲区数据
                            buffer.rewind();
                            // 寻找rotation中新的首字符位置
                            int position = first - ' ' + 1;
                            // 将数据从rotation复制到缓冲区
                            buffer.put(rotation, position, 72);
                            // 在缓冲区末尾存放一个行分隔符
                            buffer.put((byte) '\r');
                            buffer.put((byte) '\n');
                            // 回绕缓冲区,准备写入
                            buffer.flip();
                        }
                        client.write(buffer);
                    }
                    // 没有就绪通道,选择器等待就绪通道
                } catch (IOException e) {
                    key.cancel();
                    try {
                        key.channel().close();
                    } catch (IOException ex) {

                    }
                }
            }
        }
    }
}

缓冲区
在NIO中,所有IO都必须缓冲;同时新模型中不再直接操作输入流、输出流写,而是从缓冲区读写数据,注意缓冲区底层可能是字节数组,也可能是其他实现

流与通道的区别
1、流基于字节,按顺序一个字节一个字节的传送,处于性能考虑也可以传送字节数组;通道基于块,传送缓冲区中的数据块,在读写通道字节之前,数据必须已经存储在缓冲区中,且一次读写一个缓冲区的数据
2、通道和缓冲区支持同一对象的读写

缓冲区类型
Java基本数据类型中,除了boolean外,所有基本类型都有对应的缓冲区,如float对应FloatBuffer,其超类是Buffer,Buffer中的方法为通用方法

缓冲区4个关键参数,无论缓冲区类型,都有相同的方法来获取和设置这些值
1、位置position,即缓冲区中将读取或者写入的下一个位置,该值从0开始,最大值为缓冲区大小
     public final int position()
     public final Buffer position(int newPosition)
2、容量capacity,缓冲区保存数据最大数目,容量在创建时设置,且设置后不可改变
     public final int capacity()
3、限度limit,缓冲区中可访问数据的末尾位置,只要不改变限度,就无法读写超过这个位置的数据,即使缓冲区容量更大
     public final int limit()
     public final Buffer limit(int newLimit)
4、标记mark,缓冲区中客户端指定的标记,使得缓冲区能够记住一个位置并在之后将其返回,缓冲区标记在mark()调用之前是未定义的,调用时则设置mark=position,reset()方法一样,设置mark=position,如果标记未定义时调用reset(),则抛出InvalidMaekException,一些缓冲区函数会抛弃已经设定的标记,如rewind()、clear()、flip()总是抛弃标记,如果设置的值比当前的标记小,则抛弃标记
     public final Buffer mark()
     public final Buffer reset()
注意: 0<= mark<=position<=limit<= capacity

公共buffer中的其他方法
     public final Bufferclear(),位置设置为0,限度设置为容量,“清空”缓冲区,注意clear()并没有删除数据,只是重置了位置参数,这些数据依然可以使用绝对的get方法或者再改变限度和位置进行读取
     public final Buffer rewind(),位置设置为0,但不改变限度,即允许重新读取缓冲区
     public final Buffer flip(),将限度设置为当前位置,位置设置为0,在希望排空刚刚填充的缓冲区时可以使用这个方法
     public final int remaining() 返回position和limit之间的元素数目
     public fianl boolean hasRemaining(),如果remaining()返回的数大于0,则返回true

创建缓冲区
缓冲区类的层次是基于继承的,而非基于多态,在使用缓冲区前,首先需要确定的是需要的缓冲区类型,然后使用对应的工程方法创建特定的实现类;空的缓冲区使用allocate创建,常用于输入;预填充缓冲区由wrap创建,常用于输出
allocate方法只用于创建固定容量的空缓冲区,底层基于数组实现,可以通过array()和arrayOffset()访问该底层数组(后备数组),但需要注意的是,这实际上暴露了buffer的私有数据,对该数组的任何更改都将反映到缓冲区中,因此在获取数组后,就不应该在写缓冲区
allocateDirect方法( 仅用于ByteBuffer),底层使用直接内存在网卡、核心内存等位置建立缓冲区,底层不是数组,且使用array()将抛出UnSupportedOperationException;需要注意的是,直接分配应用于持续时间较短的缓冲区,其实现细节依赖JVM,不是必须不应使用
wrap方法,用于包装已有的数组形成缓冲区,此时缓冲区包含了数组(充当了后备数组)的引用,类似的对此后备数组的改变会反映到缓冲区,因此在数组操作完成前,不应使用wrap

填充put与排空get
对于使用相对的put和get,都会影响position,put会使position增大,超出容量抛出BufferOverflowException,get会使position减小,可能抛出BufferUnderflowException
对于使用绝对的put和get,即使用index,此时不会影响position,超出范围则抛出IndexOutOfBoundsException
批量方法,即以数据块的方式填充或排空缓冲区,该方法会影响position,对应的put和get都是从当前position填充或排空数据,put在缓冲区没有足够空间容纳数组内容则抛出BufferOverflowException,get在缓冲区没有足够数据填充数组则抛出BufferUnderflowException
批量方法(一下为ByteBuffer的bulk method,对于IntBuffer,则参数类型为int[]等)
     public ByteBuffer get(byte[] det, int offset, int length)
     public ByteBuffer get(byte[] det)
     public ByteBuffer put(byte[] det, int offset, int length)
     public ByteBuffer put(byte[] det)

数据转换( 仅用于ByteBuffer),提供了将简单类型参数对应的字节填充到字节缓冲区的put方法和通过将适量的字节转换为简单类型的get方法;该方法可以指定字节顺序是Big-endian还是little-endian
检查和设置缓冲区字节顺序
if (buffer.order().equals(ByteOrder.BIG_ENDIAN)) {
     buffer.order(ByteOrder.LITTLE_ENDIAN)
}

视图缓冲区
如果SocketChannel的ByteBuffer中只包含一种特定的基本类型元素,此时可以创建视图缓冲区view buffer,他可以从底层ByteBuffer中提取数据,需要注意的是,对视图缓冲区的修改会反映到底层缓冲区,反之亦然,并且每个缓冲区都有自己独立的position、mark、limit、capacity
如果以非阻塞模式,需要注意在读写上层缓冲区之前,将底层ByteBuffer的数据排空,同时非阻塞模式不能保证在排空数据后仍能以int等类型的边界对其,即向非阻塞通道中写入半个字节是可能出现的,因此在使用非阻塞IO时,在想视图缓冲区中放入更多数据前,需要确保检查这个问题
     public abstract ShortBuffer asShortBuffer()
     public abstract CharBuffer asCharBuffer()
     public abstract IntBuffer asIntBuffer()
     public abstract LongBuffer asLongBuffer()
     public abstract FloatBuffer asFloatBuffer()
     public abstract DoubleBuffer asDoubleBuffer()

整型数字服务器InetgenServer
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.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;

public class IntgenServer {
     public static final int PORT = 1919;

     public static void main(String[] args) {
           System.out.println("Listening for connections on port " + PORT);

           ServerSocketChannel serverChannel;
           Selector selector;
           try {
                serverChannel = ServerSocketChannel.open();
                serverChannel.bind(new InetSocketAddress(PORT));
                serverChannel.configureBlocking(false);
                selector = Selector.open();
                serverChannel.register(selector, SelectionKey.OP_ACCEPT);
           } catch (IOException e) {
                e.printStackTrace();
                return;
           }

           while (true) {
                try {
                     selector.select();
                } catch (IOException e) {
                     e.printStackTrace();
                     break;
                }

                Set readyKeys = selector.selectedKeys();
                Iterator iterator = readyKeys.iterator();
                while (iterator.hasNext()) {
                     SelectionKey key = iterator.next();
                     iterator.remove();

                     try {
                           if (key.isAcceptable()) {
                                ServerSocketChannel server = (ServerSocketChannel) key.channel();
                                SocketChannel client = server.accept();
                                System.out.println("Accepted connection from " + client);
                                client.configureBlocking(false);
                                SelectionKey key2 = client.register(selector, SelectionKey.OP_WRITE);
                                ByteBuffer output = ByteBuffer.allocate(4);
                                output.putInt(0);
                                output.flip();
                                key2.attach(output);
                           } else if (key.isWritable()) {
                                SocketChannel client = (SocketChannel) key.channel();
                                ByteBuffer output = (ByteBuffer) key.attachment();
                                if (!output.hasRemaining()) {
                                     output.rewind();
                                     int value = output.getInt();
                                     output.clear();
                                     output.putInt(value + 1);
                                     output.flip();
                                }
                                client.write(output);
                           }
                     } catch (IOException e) {
                           key.cancel();
                           try {
                                key.channel().close();
                           } catch (IOException ex) {

                           }
                     }
                }
           }
     }
}

整型数字客户端IntgenClient
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.IntBuffer;
import java.nio.channels.SocketChannel;

public class IntgenClient {
     public static final int PORT = 1919;
     public static final String host = "localhost";
     public static void main(String[] args) {
           try {
                SocketAddress address = new InetSocketAddress(host, PORT);
                SocketChannel client = SocketChannel.open(address);
                ByteBuffer buffer = ByteBuffer.allocate(4);
                IntBuffer view = buffer.asIntBuffer();

                for (int expected = 0; ; expected++) {
                     client.read(buffer);
                     int actual = view.get();
                     // SocketChannel 只能读写ByteBuffer,无法读写其他类型缓冲区
                     // 因此每次循环需要清空ByteBuffer,否则缓冲区慢程序将终止(这里使用的阻塞模式)
                     buffer.clear();
                     view.rewind();

                     if (actual != expected) {
                           System.out.println("Expected " + expected + "; was " + actual);
                           break;
                     }
                     System.out.println(actual);
                }
           } catch (IOException e) {
                e.printStackTrace();
           }
     }
}

压缩缓冲区,将缓冲区的剩余数据移到缓冲区开头,position设置到数据末尾,进而释放空间从而可以写入更多数据;压缩主要用在非阻塞IO的复制(读取一个通道,再把数据写入另一个通道)过程中,先读入,在写出,然后压缩,这样就可以接收更多的数据,利用一个缓冲区就能完成比较随机的交替读写
     public abstract ShortBuffer compact()
     public abstract CharBuffer compact()
     public abstract IntBuffer compact()
     public abstract LongBuffer compact()
     public abstract ByteBuffer compact()
     public abstract DoubleBuffer compact()
     public abstract FloatBuffer compact()
Echo服务器EchoServer
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.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;

public class EchoServer {
     public static final int PORT = 7;

     public static void main(String[] args) {
           System.out.println("Listening for connections on port " + PORT);

           ServerSocketChannel serverChannel;
           Selector selector;
           try {
                serverChannel = ServerSocketChannel.open();
                serverChannel.bind(new InetSocketAddress(PORT));
                serverChannel.configureBlocking(false);
                selector = Selector.open();
                serverChannel.register(selector, SelectionKey.OP_ACCEPT);
           } catch (IOException e) {
                e.printStackTrace();
                return;
           }

           while (true) {
                try {
                     selector.select();
                } catch (IOException e) {
                     e.printStackTrace();
                     break;
                }

                Set ready = selector.selectedKeys();
                Iterator iterator = ready.iterator();
                while (iterator.hasNext()) {
                     SelectionKey key = iterator.next();
                     iterator.remove();
                     try {
                           if (key.isAcceptable()) {
                                ServerSocketChannel server = (ServerSocketChannel) key.channel();
                                SocketChannel client = server.accept();
                                System.out.println("Accepted connection from " + client);
                                client.configureBlocking(false);
                                SelectionKey clientKey = client.register(selector, SelectionKey.OP_WRITE | SelectionKey.OP_READ);
                                ByteBuffer buffer = ByteBuffer.allocate(100);
                                clientKey.attach(buffer);
                           }
                           if (key.isReadable()) {
                                SocketChannel client = (SocketChannel) key.channel();
                                ByteBuffer output = (ByteBuffer) key.attachment();
                                client.read(output);
                           }
                           if (key.isWritable()) {
                                SocketChannel client = (SocketChannel) key.channel();
                                ByteBuffer output = (ByteBuffer) key.attachment();
                                // 注意回绕缓冲区!!
                                output.flip();
                                client.write(output);
                                output.compact();
                           }
                     } catch (IOException e) {
                           key.cancel();
                           try {
                                key.channel().close();
                           } catch (IOException ex) {

                           }
                     }
                }
           }
     }
}

复制缓冲区,建立缓冲区副本,从而将相同的信息发送到多个通道;复制的缓冲区不是克隆,他们共享相同的数据;每个缓冲区有独立的position、mark、capacity、limit;对于一个间接缓冲区,他们共享相同的后备数组,对一个缓冲区的修改会影响另一个缓冲区,因此复制缓冲区主要用于读取缓冲区,如通过多通道大致并行的传输相同的数据;
     public abstract ShortBuffer duplicate()
     public abstract CharBuffer duplicate()
     public abstract IntBuffer duplicate()
     public abstract LongBuffer duplicate()
     public abstract ByteBuffer duplicate()
     public abstract DoubleBuffer duplicate()
     public abstract FloatBuffer duplicate()
// 服务端接收一个请求,服务端下发一个文件
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.URLConnection;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Iterator;
import java.util.Set;

public class NoBlockingSingleFileHTTPServer {
     private ByteBuffer contentBuffer;
     private int port = 80;

     public NoBlockingSingleFileHTTPServer(ByteBuffer data, String encoding, String MIMEType, int port) {
           this.port = port;
           String header = "HTTP/1.0 200 OK\r\n"
                     + "Server: NoBlockingSingleFileHTTPServer\r\n"
                     + "Content-Length " + data.limit() + "\r\n"
                     + "Content-type " + MIMEType + "\r\n"
                     + "\r\n";
           byte[] headerData = header.getBytes(Charset.forName("UTF-8"));

           ByteBuffer buffer = ByteBuffer.allocate(data.limit() + headerData.length);
           buffer.put(headerData);
           buffer.put(data);
           buffer.flip();
           this.contentBuffer = buffer;
     }

     public void run() throws IOException {
           ServerSocketChannel serverChannel = ServerSocketChannel.open();

           serverChannel.bind(new InetSocketAddress(port));
           // 下述三行代码和上一行代码效果相同
//         ServerSocket ss = serverChannel.socket();
//         SocketAddress add = new InetSocketAddress(port);
//         ss.bind(add);

           serverChannel.configureBlocking(false);
           Selector selector = Selector.open();
           serverChannel.register(selector, SelectionKey.OP_ACCEPT);

           while (true) {
                selector.select();
                Set keys = selector.selectedKeys();
                Iterator iterator = keys.iterator();
                while (iterator.hasNext()) {
                     SelectionKey key = iterator.next();
                     iterator.remove();

                     try {
                           if (key.isAcceptable()) {
                                ServerSocketChannel server = (ServerSocketChannel) key.channel();
                                SocketChannel client = server.accept();
                                client.configureBlocking(false);
                                client.register(selector, SelectionKey.OP_READ);
                           } else if (key.isWritable()) {
                                SocketChannel client = (SocketChannel) key.channel();
                                ByteBuffer buffer = (ByteBuffer) key.attachment();
                                if (buffer.hasRemaining()) {
                                     client.write(buffer);
                                } else {
                                     // 结束工作
                                     client.close();
                                }
                           } else if (key.isReadable()) {
                                // 只需要读取,不需要解析,故下面三行代码无实际作用
                                SocketChannel client = (SocketChannel) key.channel();
                                ByteBuffer buffer = ByteBuffer.allocate(4096);
                                client.read(buffer);
                                // 通道切换回只写模式
                                key.interestOps(SelectionKey.OP_WRITE);
                                // 每个通道都有自己的缓冲区,所有通道共享相同内容
                                key.attach(contentBuffer.duplicate());
                           }
                     } catch (IOException e) {
                           key.cancel();
                           try {
                                key.channel().close();
                           } catch (IOException ex) {

                           }
                     }
                }
           }
     }

     public static void main(String[] args) {
           String file = "C:\\Users\\Administrator\\Desktop\\程序学习\\新建文本文档.txt";
           String encoding = "UTF-8";
           int port = 80;

           try {
                String contentType = URLConnection.getFileNameMap().getContentTypeFor(file);
                System.out.println(contentType);
                Path filePath = FileSystems.getDefault().getPath(file);
                System.out.println(filePath);
                byte[] data = Files.readAllBytes(filePath);
                ByteBuffer input = ByteBuffer.wrap(data);

                NoBlockingSingleFileHTTPServer server = new NoBlockingSingleFileHTTPServer(input, encoding, contentType, port);
                server.run();
           } catch (IOException e) {
                e.printStackTrace();
           }

     }
}

分片缓冲区,复制缓冲区的变形,分片缓冲区也会创建一个新的缓冲区,与原缓冲区共享数据,不同之处在于分片的起始位置position=0的位置是原缓冲区的position,分片缓冲区的capacity不大于原缓冲区的limit;倒回分片只能回到原缓冲区的position处;分片缓冲区对于协议数据缓冲区很有用,可以读出受,然后使用分片缓冲区,然后将对应的新缓冲区传递给对应的方法或类
     public abstract ShortBuffer slice()
     public abstract CharBuffer slice()
     public abstract IntBuffer slice()
     public abstract LongBuffer slice()
     public abstract ByteBuffer slice()
     public abstract DoubleBuffer slice()
     public abstract FloatBuffer slice()

标记与重置,用于重读之前的数据
     public final Buffer mark() // 设置标记,如果position < 标记位置,则忽略标记
     public final Buffer reset() // 如果之前没有设置mark,则抛出 InvalidMarkException

缓冲区类的Object方法
     equals(),两个缓冲区数据类型相同、缓冲区 剩余数据个数相同、相同 相对位置的数据相同
     hashCode(),hashCode的方法是根据equals设计的,即向缓冲区的添加、删除元素均会改变hashCode,即hashCode是易变的
     compareTo(),buffer类实现了Comparable方法,该比较 按剩余数据进行比较,对应元素相等则缓冲区相等;否则按第一个不相等数据返回结果;如果某个缓冲区没有元素而另一个还有数据,则认为较小的缓冲区小 
     toString(),除了CharBuffer会返回剩余的数据,其余则返回position、limit、capacity

SocketChannel,用于读写TCP Socket,数据 必须编码到ByteBuffer,每个SocketChannel都与一个对等端peer相关联

连接
SocketChannel没有公共构造方法
     public static SocketChannel open(SocketAddress address) throws IOExcsption,该方法为立即建立连接,为阻塞方法,在连接建立或者抛出异常前不会返回
     public static SocketChannel open() throws IOException,该方法不立即建立连接,创建一个未连接Socket,必须使用 connect() 方法建立连接
// 阻塞模式,connect方法会等待连接建立
SocketChannel channel = SocketChannel.open();
SocketAddress address = new InetSocketAddress(host, port);
channel.connect(address);

// 非阻塞模式,connect方法会立即返回
SocketChannel channel = SocketChannel.open();
SocketAddress address = new InetSocketAddress(host, port);
channel.configureBlocking(fase);
channel.connect(address);

注意非阻塞模式,在使用连接前必须带调用
public abstract boolean finishConnect() throws IOException
对于阻塞通道,该方法立即返回 true;连接可用则返回 true;连接不可用则返回 false,连接无法建立则抛出异常,可以通过调用下列方法测试连接建立情况
public abstract boolean isConnect()   连接打开则返回true
public abstract boolean isConnectPending()  连接在建立过程中则返回true

读取
读取SokcetChannel,首选需要创建ByteBuffer,然后使用 public abstract int read(ByteBuffer dst) throws IOException
1、通道会用尽可能多的数据填充缓冲区,然后返回直接
2、遇到流末尾,使用剩余字节填充缓冲区,且在下一次调用read()返回-1
3、若通道是阻塞的,这个方法至少读回一个字节、或者返回-1、或者抛出异常
4、如果通道是非阻塞的,该方法可能返回0
// 读取缓冲区,直到缓冲区满或者遇到流结尾
while (buffer.hasRemaining() && channel.read(buffer) != -1);

// 散布scatter,从一个源填充多个缓冲区,只要缓冲区数组的最后一个缓冲区还有空间,就可以继续读取
public final long read(ByteBuffer[] dsts) throws IOException
public final long read(ByteBuffer[] dsts, int offset, int length) throws IOException

ByteBuffer[] buffers = new ByteBuffer[2];
buffer[0] = ByteBuffer.allocate(1000);
buffer[1] = ByteBuffer.allocate(1000);
while (buffer[1].hasRemaining() && channel.read(buffers) != -1);

写入
public abstract int write(ByteBuffer src) throws IOException,如果通道是非阻塞的,则write方法不保证写入缓冲区的所有内容,但由于缓冲区是基于游标的,通过反复调用该方法,即可完全写入数据;聚集Gather,和Scatter类似,Gather是将多个缓冲区的数据写入一个Socket
     public final long write(ByteBuffer[] srcs) throws IOException
     public final long write(ByteBuffer[] srcs, int offset, int length) throws IOException

关闭缓冲区
     1、public void close() throws IOException,关闭通道并释放资源;已关闭的通道再次关闭没有任何效果;试图读写一个已关闭的通道将抛出异常
     2、public boolean isOpen(),检查通道是否关闭
     3、SokcetChannel实现了AutoCloseable

ServerSocketChannel
该类的唯一作用:接收一个连接,无法对ServerSocketChannel进行直接读写

ServerSocketChannel.open(),创建一个新的ServerSocketChannel对象,该方法并不是打开一个Socket,仅仅是创建一个对象,需要再使用Socket()方法获得对等端ServerSocket,进而对其进行配置;注意这里使用的工厂方法,不同虚拟机有着不同的实现,且是用户不可配置的,相同虚拟机中open()总是返回相同类的实例


接受连接
public abstract SocketChannel accept() throwws IOException
accept()方法可以工作在阻塞模式和非阻塞模式
阻塞模式,accept()等待入站连接,在建立连接前线程无法执行任何操作,该策略适用于需要立即响应每个请求的简单服务器,默认阻塞模式
非阻塞模式,没有入站连接时则返回null,非阻塞模式适用于需要为每个连接完成大量工作的服务器,非阻塞模式一般结合Selector
accept方法抛出的IOException的几个子类异常,用于指示更详细的问题
ClosedChannelException,关闭后无法重新打开一个ServerSocketChannel
AsynchronousCloseException,执行accept()时,另一个线程关闭了ServerSocketChannel
ClosedByInterruptException,一个阻塞ServerSocketChannel在等待时被另一个线程中断
NotYetBoundException,调用了accept()方法之前没有将ServerSocketChannel的对等端SocketChannel与地址绑定,是运行时异常,不是IOException
SecurityException,安全管理器拒绝应用程序请求的端口

Channels类,用于完成通道和基于IO的流、读写器类之间的相互转换,SocketChannel实现了下述方法,ServerSocketChannel没有实现
public static InputStream newInputStream(ReadableByteChannel ch)
public static OutputStream newOutputStream(WritableByteChannel ch)
public static ReadableByteChannel newChannel(InputStream in)
public static WritableByteChannel newChannel(OutputStream out)
public static Reader newReader(ReadableByteChannel ch, CharsetDeocder decoder, int minimumBufferCapacity)
public static Reader newReader(ReadableByteChannel ch, String encoding)
public static Writer newWriter(WritableByteChannel ch, String encoding)

// 服务器处理SOAP请求,通过通道读取http请求主题,通道转换为流,在使用SAX解析XML,
SocketChannel channel = server.accept();
processHTTPHeader(channel)
XMLReader parser = XMLReaderFactory.creatXMLReader()
parse.setContentHandler(someContnetHandlerObject)
InputStream in = Channels.newInputStream(channel);
parser.parse(in);

Java 7 异步通道
AschronousSocketChannel和AschronousServerSocketChannel,有着和SocketChannel与ServerSocketChannel类似的方法,但不是他们的子类,不同之处在于,对异步通道的读写会立即返回,数据由Future或者CompletionHandler进一步处理,connect()和accept()也是异步的,并返回Future
1、future,适用于以一种特定顺序获取结果
// 网络连接并行执行
SocketAddress address = new InetSocketAddress(host, port);
AsynchronousSocketChannel client = AsynchronousSocketChannel.open();
Future connected = client.connect(address);
ByteBuffer buffer = ByteBuffer.allocate(capacity);
// 等待连接完成
connected.get();
// 从连接处读取
Future future = client.read(buffer);
// 做其他工作
// 等待读取完成
future.get();
// 回绕并排空缓冲区
buffer.flip();
WritableByteChannel out = Channels.newChannel(System.out);
out.write(buffer);
2、CompletionHandler,适用于独立处理每个网络连接,例如网络爬虫;CompletionHandler接口声明两个方法,
成功调用public void completed(Object exc, Object attachment)
失败调用public void failed(Throwable exc, Object attachment)
class LineHandler implements CompletionHandler {
     @Override
     public void completed(Integer result, ByteBuffer buffer) {
           // TODO Auto-generated method stub
           buffer.flip();
           WritableByteChannel out = Channels.newChannel(System.out);
           try {
                out.write(buffer);
           } catch (IOException e){
                e.printStackTrace();
           }

     }

     @Override
     public void failed(Throwable exc, ByteBuffer buffer) {
           // TODO Auto-generated method stub
           System.out.println(exc.getMessage());
     }
}
AsynchronousSocketChannle和AsynchronousServerSocketChannel是线程安全的,但是读写操作时,相同的操作只能有一个线程进行,但是读写可以同时进行,多线程同时读写时将抛出ReadPendingException或者WritePendingException

Java 7 Socket选项
SocketChannel、ServerSocketChannel、AsynchronousSocketChannel、AsynChronousServerSocektChannel、DatagramChannel实现了NetworkChannel接口,主要用于支持各种TCP选项,主要通过以下三个方法设置、获取支持的选项
      T getOption(SocektOption name) throws IOException
      NetworkChannel setOption(SoketOption name, T value) throws IOException
     Set> supportedOptions()
其中SocketOption类时泛型类,类型参数T的选项为 Integer、Boolean、NetworkInterface,其选项对应的常量在StandardSocketOptions类中定义
import java.io.IOException;
import java.net.SocketOption;
import java.nio.channels.AsynchronousServerSocketChannel;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.DatagramChannel;
import java.nio.channels.NetworkChannel;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;

public class OptionSupport {
     public static void main(String[] args) throws IOException {
           printOptions(SocketChannel.open());
           //printOptions(ServerSocketChannel.open());
           printOptions(AsynchronousSocketChannel.open());
           printOptions(AsynchronousServerSocketChannel.open());
           printOptions(DatagramChannel.open());
     }

     public static void printOptions(NetworkChannel channel) throws IOException {
           System.out.println(channel.getClass().getSimpleName() + " supports:");
           for (SocketOption option : channel.supportedOptions()) {
                System.out.println(option.name() + " : " + channel.getOption(option));
           }
           System.out.println();
           channel.close();
     }
}

就绪选择
将不同的通道注册到一个Selector对象,每个通道分配一个SelectionKey,程序通过询问这个Selector对象,哪些通道已经准备就绪可以无阻塞的完成操作,可以通过请求Selector对象返回相应的键集合

Selector类
1、首选调用静态工厂方法创建选择器
     public static Selector open() thrwos IOException
2、向选择器增加SelectableChannel通道对象,ops是SelectionKey的命名常量,如果一个通道需要在同一个选择器中关注对个操作,使用 | 符号;att 是键的附件,该对象用于存储连接的状态;一个通道可以注册到多个选择器
     public final SelectionKey register(Selector sel, int ops)
     public final SelectionKey register(Selector sel, int ops, Object att)
3、查询选择器,选择就绪的通道
     public abstract int selectNow() throws IOException   // 非阻塞选择的方法,若没有准备好的连接,则立即返回
     public abstract int select() throws IOException   // 返回前等待,直到至少有一个注册的通道准备好进行处理
     public abstract int select(long timeout) throws IOException  // 在返回 0 前至多等到 time out 毫秒,没有通道准备好则不做任何操作
4、使用SelectedKeys()获取就绪的通道,然后迭代处理每个准备好的键,并将该键remove()掉
     public abstract Set selectedKeys()
5、不再需要选择器时,需要将其关闭,释放与选择器相关的所有资源,取消向选择器注册的所有的键,中断被该选择器的某个方法中断的线程
     public abstract void close() throws IOException

Selectionk类,通道指针,并保存一个存储通道连接状态的附件
1、迭代处理SelectionKey时,首先测试这些键能进行操作,共四种可能,对象通道关注的四个操作
     public final boolean isAcceptable()
     public final boolean isConnectable()
     public final boolean isReadable()
     public final boolean isWritable()
2、使用channel()获得这个通道
     public abstract SelectableChannel channel()
3、若在保存状态信息的SelectionKey存储了一个对象,使用attachment()方法获取该对象
     public final Object attachment()
4、结束使用连接,则应该撤销该SelectionKey对象的注册
     public abstract void cancel()



你可能感兴趣的:(java网络编程)