JDK版本
- jdk8
Channel
-
Channel
概念- 数据的源头或者数据的目的地
- 用于向
buffer
写入数据或者读取buffer
数据 - 异步I/O支持–包含
socket
,file
和pipe
三种管道,都是全双工的通道
-
如果一个
Channel
实现了Interrupted
接口,那么当他被阻塞,并且发生中断的时候,那么该Channel
将会被中断,线程会抛出一个ClosedByInterruptException
异常,如果一个线程的状态是中断,他试图访问一个Channel
,那么Channel
将立即被关闭。 -
Channel
可以以阻塞(blocking)或者非阻塞(nonblocking)模式运行,非阻塞模式的Channel
永远不会让调用的线程休眠,请求的操作要么立即完成,要么返回一个结果表明未进行任何操作,不是所有通道都可以使用非阻塞模式 -
Channel
分为两类:FileChannel
和Socket
通道,包括SocketChannel
、ServerSocketChannel
和DatagramChannel(UDP)
,Socket
通道可以直接通过工厂方法创建,但是一个FileChannel
只能通过一个打开的RandomAccessFile
、FileInputStream
、FileOutputStream
对象上调用getChannel()
方法来获取//Socket通道 SocketChannel sc = SocketChannel.open( ); ServerSocketChannel ssc = ServerSocketChannel.open( ); DatagramChannel dc = DatagramChannel.open( ); //文件通道 RandomAccessFile raf = new RandomAccessFile ("somefile", "r"); FileChannel fc = raf.getChannel( );
Channel读写
- 正常情况下,一个写操作会在全部数据写入后返回,但是在某些特殊的通道(Socket的非阻塞模式)中,因为缓冲区满,数据有可能部分写入或者没有写入。所以从这里来说,通道的写不是线程安全的,因为有可能想写入10B的数据,但是只写入了5B,那下次线程调度的时候,就有可能写乱数据
- 因为读/写本身就是阻塞式的,同时只有一个线程可以操作,但是读/写都不是线程安全的,所以一般一个通道也都只有一个线程读,一个线程写,或者一个线程把读/写都做了
read
和write
方法并不一定会操作和ByteBuffer
容量相等的数据,这两个方法会返回这期间操作的字节数,因此在应用端需要判断,如果仅仅进行部分传输,需要重新进行传输,使用hasReminding()
方法判断是否还有数据
Channel关闭
Channel
不能被重复利用,一个打开的Channel
即代表与一个特定的I/O服务的特定连接,并封装该连接的状态。当Channel
关闭时,那个连接会丢失,然后Channel
将不再连接任何东西。Channel
调用通道的close()
方法时,可能会导致在Channel
关闭底层I/O服务的过程中线程暂时阻塞(即使Channel
处于非阻塞模式)。通道关闭的阻塞行为(如果有的话)是高度取决于操作系统或文件系统的(Socket通道关闭会花费较长时间,具体时耗取决于操作系统的网络实现。在输出内容被提取时,一些网络协议堆栈可能会阻塞通道的关闭,在通道关闭之后,正在进行的read或者write操作会收到一个AsynchronousCloseException
)。在一个通道上多次调用close()
方法是没有坏处的,但是如果第一个线程在close()
方法中阻塞,那么在它完成关闭通道之前,任何其他调用close()
方法都会阻塞,后续在已关闭的Channel
上调close()
方法不会产生任何操作,只会立即返回- 关闭一个已经注册的
SelectableChannel
需要两个步骤-
取消注册的key,这个可以通过
SelectionKey.cancel()
方法,也可以通过SelectableChannel.close()
方法,或者中断阻塞在该channel上的IO操作的线程来做到。 -
后续的
Selector.selectXXX()
方法的调用才真正地关闭本地Socket。 -
因而,如果在取消
SelectionKey
后没有调用到selector
的select()
方法(因为Client一般在取消key后, 我们都会终止调用select的循环,当然,server关闭一个注册的Channel我们是不会终止select循环的),那么本地socket将进入CLOSE-WAIT 状态(等待本地Socket关闭)。简单的解决办法是在SelectableChannel.close()
方法之后调用Selector.selectNow()
方法,类似:Selector sel; SocketChannel sch; // … sch.close(); sel.selectNow(); Netty在超过256连接关闭的时候主动调用一次selectNow
-
SocketChannel
-
Socket
和SocketChannel
类封装点对点、有序的网络连接。每个SocketChannel
对象创建时都是和一个对等的Socket
对象关联的。 -
连接过程
connect
还未被调用,此时抛出NoConnectionPendingException
- 正在连接,但是连接未完成,
finishConnect
会立即返回false - 非阻塞模式下,调用
connect
之后,SocketChannel
可以调用configureBlocking()
来切换回阻塞模式,这时候调用finishConnect()
会阻塞直到连接建立完成 - 如果连接已经建立,那么调用
finishConnect()
方法会返回true,什么也不发生
-
OP_CONNECT事件
,表示连接通道连接就绪或者发生了错误,会被加到ready中.即这个事件发生时不能简单的认为连接成功了,要使用finishConnect()
判断//新创建的Channel都是未连接的 SocketChannel channel = SocketChannel.open(); //设置该SocketChannel以非阻塞方式工作 channel.configureBlocking(false); /** 调用connect进行连接 * 在阻塞模式下,线程在连接建立好或超时之前会保持阻塞 * 非阻塞模式下,没有超时参数,会发起连接请求,并且立即返回,如果是true,则说明连接已经建立(本地环回连接);如果不能连接,立即返回false,并且异步的继续尝试连接,这时候isConnectPending()会返回true(连接等待状态), **/ channel.connect(addr); while(!sc.finishConnect()){ System.out.println("正在尝试连接"); } //注册连接事件,连接就会就绪 channel.register(selector, SelectionKey.OP_CONNECT); //连接成功finishConnect()返回true if (key.isValid() && key.isConnectable()) { SocketChannel ch = (SocketChannel) key.channel(); if (ch.finishConnect()) { // Connect successfully // key.interestOps(SelectionKey.OP_READ); } else { // Connect failed } }
ServerSocketChannel
创建
-
ServerSocketChannel
是一个面向流的监听Socket
的SelectableChannel
。ServerSocketChannel
是一个抽象类,即不能直接实例化,可以通过自身的静态方法open()
创建实例,open()
方法作用是打开服务端Socket通道
,新创建的服务端Socket通道
最初是未绑定的,在接收连接之前,必须通过它的bind()方法将其绑定到具体的地址和端口 -
静态open方法
public static ServerSocketChannel open() throws IOException { return SelectorProvider.provider().openServerSocketChannel(); }
阻塞模式
accept()
方法的作用是接收通道Socket的连接。如果ServerSocketChannel
是非阻塞模式,那么accept()
方法将直接返回null。如果是阻塞模式,在新的连接可用或者发生I/O错误之前会无限地阻塞。@Test public void testBlockServer() throws Exception { ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); int backlog = 50; int port = 8888; //true 默认是阻塞模式 System.out.println(serverSocketChannel.isBlocking()); serverSocketChannel.bind(new InetSocketAddress("localhost", port), backlog); System.out.println("start:" + System.currentTimeMillis()); //阻塞模式下:在新的连接可用或者发生I/O错误之前会无限期地阻塞它 //使用telnet 127.0.0.1 8888 ,此时accept()立即返回 SocketChannel socketChannel = serverSocketChannel.accept(); System.out.println("end:" + System.currentTimeMillis()); socketChannel.close(); serverSocketChannel.close(); }
启动输出: true start:1551490993132 使用telnet 127.0.0.1 8888 后输出: end:1551490999952
@Test public void testNotBlockServer() throws Exception { ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); int backlog = 50; int port = 8888; //true System.out.println(serverSocketChannel.isBlocking()); //设置为非阻塞模式 serverSocketChannel.configureBlocking(false); //false System.out.println(serverSocketChannel.isBlocking()); serverSocketChannel.bind(new InetSocketAddress("localhost", port), backlog); System.out.println("start:" + System.currentTimeMillis()); //非阻塞模式下:在不存在挂起的连接时,accept方法将直接返回 null SocketChannel socketChannel = serverSocketChannel.accept(); System.out.println("end:" + System.currentTimeMillis()); if (socketChannel != null) { socketChannel.close(); } else { // null System.out.println(socketChannel); } serverSocketChannel.close(); }
输出: true false begin:1551452354670 end:1551452354673 null
选项
-
Socket Option
@Test public void testChannelOption() throws Exception { ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); SocketChannel socketChannel = SocketChannel.open(); //获取支持的Socket Option Set<SocketOption<?>> socketOptions = serverSocketChannel.supportedOptions(); //[SO_RCVBUF, IP_TOS, SO_REUSEADDR] System.out.println(socketOptions); //[IP_TOS, TCP_NODELAY, SO_SNDBUF, SO_RCVBUF, SO_OOBINLINE, SO_KEEPALIVE, SO_LINGER, SO_REUSEADDR] socketOptions = socketChannel.supportedOptions(); System.out.println(socketOptions); serverSocketChannel.setOption(StandardSocketOptions.SO_RCVBUF, 65535); socketChannel.setOption(StandardSocketOptions.SO_RCVBUF, 65535); //65535 System.out.println(serverSocketChannel.getOption(StandardSocketOptions.SO_RCVBUF)); //65535 System.out.println(socketChannel.getOption(StandardSocketOptions.SO_RCVBUF)); }