NIO系列Channel相关

  接上篇buffer整理,在写nio的时候,我经常会搞不清各种channel 和 socket的关系。对服务器端的一套理解差不多以后,看客户端的nio又产生的尴尬的问题:这是啥? 和刚刚那个不一样么? 所以今天花了一点时间整理了一下。

SocketChannel

  SocketChannel可以读写TCP Socket,数据的读写需要依靠Bytebuffer对象,每个SocketChannel都和一个Socket有关联,这个socket可以用来配置channel。

连接

  SocketChannel类没有公共的构造函数,使用两个静态open方法来创建新的SocketChannel对象:

1
2
public static SocketChannel open(SocketAddress remoteAddress) throws IOException
public static SocketChannel open() throws IOException

  第一个方法会建立连接。这个方法会阻塞,在成功建立连接或者抛出异常之前,这方法不会返回:

1
2
SocketAddress address = new InetSocketAddress("www.xxx.com",80);
SocketChannel channel = SocketChannel.open(address);

  后一个没有参数的版本不立即连接,他创建一个初始未连接的Socket,之后需要用connect()方法来连接:

1
2
3
SocketChannel channel = SocketChannel.open();
SocketAddress address = new InetSocketAddress("www.xxx.com",80);
channel.connect(address);

  为了在连接前配置通道或socket,那貌似只能用这样的比较。。。麻烦的方法。很典型的,在需要使用selector时,需要将channel设置为非阻塞,那就必须像上面那样创建channel,在channel之前设置非阻塞:

1
2
3
...
channel.configureBlocking(false);
channel.connect(address);

读取

  在开头说过,channel的读写需要依靠bytebuffer,具体的使用感觉有点像io流(废话,nio 就是 non-blocking IO),只不过没有了像inputStream 和 outputStream 那样的方向之分。

1
public abstract int read(ByteBuffer dst) throws IOException

  channel会用尽可能多的数据填充缓冲区,然后返回放入的字节数。如果遇到流的末尾,通道会有所有剩余的字节填充缓冲区,而且在下一次调用read()时返回-1。如果通道是阻塞的,这个方法将至少读取一个字节,或者返回-1,也可能抛出一个异常。但如果是非阻塞的,那这个方法可能会返回0。
  因为数据将会存储在缓冲区的当前位置,而这个位置会随着数据的读取自动更新,所以可以一直像read()方法传入同一个缓冲区,一直到这个缓冲区被填满为止。比如下面的循环会一直读取数据,直到缓冲区被填满或者检测到流末尾为止:

1
while(buffer.hasRemaining() && channel.read(buffer) != -1) ;

  从一个channel填充多个buffer,这个被叫做Scatter(散布)。这个概念和写入数据的聚集是相对的。(我为什么老想不出这个的具体使用场景呢?)

1
2
public final long read(ByteBuffer[] dsts) throws IOException
public final long read(ByteBuffer[] dsts , int offset , int length) throws IOException

  第一个方法填充所有的缓冲区,第二个方法则从位于offset的缓冲开始,填充length个缓冲。要填充buffer数组,只要在列表中最后一个buffer还有剩余空间,那就可以继续循环填充,如下:

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

写入

  写入和读取差不多,一般在写入一个bytebuffer之前,需要调用改对象的flip()方法,将这个buffer“切换到写入模式”,flip具体的操作看上一篇buffer相关。

1
public abstract int write(ByteBuffer src) throws IOEXception

  和读取差不多,如果通道是非阻塞的,这个方法不能保证会把缓冲区的所有内容写入,不过因为buffer的机制(position),可以用循环调用,直到buffer完全排空:

1
while(buffer.hasRemaining() && channel.write(buffer)!= -1);

  上面提到过的,Gather(聚集)。这个倒是在网上看到一个使用的场景:比如你可能希望在一个buffer中存储HTTP首部,而在另一个buffer中存储HTTP主体。

1
2
public final long write(ByteBuffer[] dsts) throws IOException
public final long write(ByteBuffer[] dsts, int offset, int length) throws IOException

关闭

  和Socket一样,在channel用完以后也要关闭,释放channel可能使用的端口和其他资源。

1
public void close() throws IOExceptioin

  如果通道已经关闭,在关闭就没有任何效果。但是试图继续 读/写 关闭的通道,则会抛出异常。可以用isOpen()来检查。但是为什么不用神奇的try-with-recources呢?

ServerSocketChannel

  首先,要在一开始就说明。ServerSocketChannel类只有一个目的!那就是接受入站连接,你没有办法去读写或者连接ServerSocketChannel。他支持的唯一的操作就是接受一个新的入站连接。这个类本身只有4个方法,其中accept()最重要。他还从超类继承了几个方法,主要和selector注册来得到入站连接有关。最后,和所有的channel一样,有一个close方法,用来关闭服务器socket。

创建服务器socket通道

  通过静态工厂方法ServerSocketChannel.open()创建一个新的ServerSocketChannel对象。不过,这个方法的名字open有点误导,他实际上并不打开一个新的ServerSocketChannel,仅仅只是创建这个对象。在使用之前,需要调用socket()方法来获得关联的ServerSocket。然后可以用这个ServerSocket的各种设置方法随意配置服务器选项,比如接受换冲的大小、超时的时间等等。然后再将ServerSocket连接到一个SocketAddress(要监听的本地端口)。

1
2
3
4
5
6
7
8
try(){
ServerSocketChannel serverChannel = ServerSocketChannel.open;
ServerSocket socket = serverChannel.socket();
SocketAddress address = new InetSocketAddress(80);//监听80端口
socket.bind(address);
}catch(IOException e){
serr("绑定端口失败 "+ e.getMessage());
}

  上面这段代码是在80端口上打开一个ServerSocketChannel,我在看书的时候看的就是这一段,不过可能我看的有点资料有点老把,在java 7之后更方便一些了,ServerSocketChannel本身就有一个bind方法了,可以像下面这么写:

1
2
3
4
5
6
7
try(){
ServerSocketChannel serverChannel = ServerSocketChannel.open;
SocketAddress address = new InetSocketAddress(80);//监听80端口
serverChannel.bind(address);
}catch(IOException e){
serr("绑定端口失败 "+ e.getMessage());
}

接受连接

   一但打开并绑定了ServerSocketChannel,那就可以用accept()来监听入站连接了:

1
public abstract SocketChannel accpept() throws IOException

   accept()可以在阻塞或者非阻塞模式下操作。在阻塞模式下,accept方法等待入站连接。然后接受一个连接,并返回一个连接到远程客户端SocketChannel对象。在建立连接之前,线程无法进行任何操作。在默认情况下为阻塞模式。在非阻塞模式下工作时,如果没有入站连接,accept会返回null。一般非阻塞模式会与selector一起使用。
  在使用accept时,如果发生错误,则会抛出IOException异常,不过他的几个子类以及几个runtime异常能更详细的说明问题:

ClosedChannelException

  关闭后无法重新打开一个ServerSocketChannel

AsychronousCloseException

  执行accept的时候,另一个线程关闭了这个ServerSocketChannel

ClosedByInterruptException

  一个阻塞ServerSocketChannel在等待的时候,另一个线程中断了这个线程

NotYetBoundException

  首先这是一个运行时的异常,而不是io异常,调用了open,但是在调用accept之前没有将ServerSocketChannel对应的ServerSocket和一个SocketAddress绑定

SecurityException

  安全管理器拒绝绑定,一般是权限不够,在linux 和 macos 上,低于1024的端口号需要管理员权限(sudo),高于1024的则无所谓

Channels

   这是一个工具类,可以把传统的基于I/O的流,reader,writer包装在channel里面,当然也可以反向的将channel转换为前面的那些东西。

1
2
3
4
5
6
7
8
9
10
public static InputStream newInputStream(ReadableBtyeChannel ch)
public static OutputStream newOutputStream(WritableBtyeChannel ch)

public static ReadableBtyeChannel newChannel(InputStream in)
public static WritableBtyeChannel newChannel(OutputStream out)

public static Reader newReader(ReadableBtyeChannel channel,CharsetDecoder decoder,int minimumBufferCapacity)
public static Reader newReader(ReadableBtyeChannel ch,String encoding)
public static Writer newWriter(WritableBtyeChannel ch,String encoding)