接上篇buffer整理,在写nio的时候,我经常会搞不清各种channel 和 socket的关系。对服务器端的一套理解差不多以后,看客户端的nio又产生的尴尬的问题:这是啥? 和刚刚那个不一样么? 所以今天花了一点时间整理了一下。
SocketChannel
SocketChannel可以读写TCP Socket,数据的读写需要依靠Bytebuffer对象,每个SocketChannel都和一个Socket有关联,这个socket可以用来配置channel。
连接
SocketChannel类没有公共的构造函数,使用两个静态open方法来创建新的SocketChannel对象:
1 | public static SocketChannel open(SocketAddress remoteAddress) throws IOException |
第一个方法会建立连接。这个方法会阻塞,在成功建立连接或者抛出异常之前,这方法不会返回:
1 | SocketAddress address = new InetSocketAddress("www.xxx.com",80); |
后一个没有参数的版本不立即连接,他创建一个初始未连接的Socket,之后需要用connect()方法来连接:
1 | SocketChannel channel = SocketChannel.open(); |
为了在连接前配置通道或socket,那貌似只能用这样的比较。。。麻烦的方法。很典型的,在需要使用selector时,需要将channel设置为非阻塞,那就必须像上面那样创建channel,在channel之前设置非阻塞:
1 | ... |
读取
在开头说过,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 | public final long read(ByteBuffer[] dsts) throws IOException |
第一个方法填充所有的缓冲区,第二个方法则从位于offset的缓冲开始,填充length个缓冲。要填充buffer数组,只要在列表中最后一个buffer还有剩余空间,那就可以继续循环填充,如下:
1 | ByteBuffer[] buffers = new ByteBuffer[2]; |
写入
写入和读取差不多,一般在写入一个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 | public final long write(ByteBuffer[] dsts) 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 | try(){ |
上面这段代码是在80端口上打开一个ServerSocketChannel,我在看书的时候看的就是这一段,不过可能我看的有点资料有点老把,在java 7之后更方便一些了,ServerSocketChannel本身就有一个bind方法了,可以像下面这么写:
1 | try(){ |
接受连接
一但打开并绑定了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 | public static InputStream newInputStream(ReadableBtyeChannel ch) |