Channel
Channel特点
Channel对象可以异步的读写,每次读写都要依赖Buffer才能操作数据,所以有时候说Java NIO是面向缓冲区的。
Channel分类
FileChannel:面向文件的读写,文件读写和IO流一样是阻塞IO模式
DatagramChannel:面向UDP网络读写数据
SocketChannel:面向TCP网络读写数据
ServerSocketChannel:可以监听TCP连接,类似一个监听器,会为每个进来的连接创建一个SocketChannel,它本身不会传输数据
所有Channel都具备的方法
读写
public int read(ByteBuffer dst) throws IOException:从该通道的当前文件位置开始读取字节到缓冲区,并且用实际读取数更新读取下标指针的位置,如果读取完了返回-1
public final long read(ByteBuffer[] dsts) throws IOException :从该通道的当前文件位置开始读取字节到缓冲区,并且用实际读取数更新读取下标指针的位置,如果读取完了返回-1
public long read(ByteBuffer[] dsts, int offset, int length) throws IOException:将数据读取到指定的多个buffer容器中,将数据分散,可以指定偏移量和读取长度,如果读取完了返回-1
public abstract int write(ByteBuffer src) throws IOException: 将buffer容器的数据读取到通道中
public final long write(ByteBuffer[] srcs) throws IOException: 将多个buffer容器的数据读取到通道中,也叫数据聚合
public abstract long write(ByteBuffer[] srcs, int offset, int length) throws IOException: 将多个buffer容器的数据读取到通道中,可以指定偏移量和写出长度
SocketChannel configureBlocking(boolean block): 设置channel读取数据时是否需要阻塞等待,如果将一个SocketChannel实例设置为非阻塞,那么当这个实例调用read读取数据时将不会阻塞,即使这个socket通道没有数据发生过来。如果设置为阻塞状态,那么SocketChannel实例调用read会进入阻塞等待,直到数据发送过来才会结束等待状态。
非阻塞模式下,如果read方法返回值是0,说明目前没有数据发过来。如果read方法返回值是-1,说明客户端关闭了连接通道。如果read方法返回值大于0,说明有数据,打印出来
ServerSocketChannel常用方法
创建、关闭ServerSocketChannel
public static ServerSocketChannel open():ServerSocketChannel类是抽象的,不能直接new实例化,API中提供open()方法来创建ServerSocketChannel实例。open()作用是打开服务器套接字通道。新通道的套接字最初是未绑定端口的。在接受连接之前必须通过它的bind()方法将其绑定到具体的地址和端口。
public final void close():方法的作用是关闭此通道。如果已关闭该通道,则此方法立即返回。否则,它会将该通道标记为已关闭,然后调用implCloseChannel()方法以完成关闭操作。
获取ServerSocket
public abstract ServerSocket socket():ServerSocketChannel实例调用socket()方法返回ServerSocket类的对象,可以与客户端套接字进行通信。socket()方法的作用是获取与此通道关联的服务器套接字ServerSocket类的对象。如果ServerSocketChannel实例已经绑定了端口,那么返回的ServerSocket就不需要再次绑定了。此方法很少用,因为ServerSocket提供的API又回到了传统BIO模式
监听连接
public abstract SocketChannel accept():监听通道并进入阻塞状态,直到有连接时才返回新连接的SocketChannel,如果是非阻塞模式,调用此方法就会立即返回SocketChannel实例,但若此时没有连接进入,SocketChannel实例是为null的。
设置非阻塞模式
serverSocketChannel configureBlocking(false):监听通道时是否阻塞等待
案例1:用Server和Channel来完成一个单路单线程的NIO模型
package _8nio;
import org.junit.Test;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.*;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.*;
public class ServerSocketChannelTest {
@Test
public void server() {
List<SocketChannel> list= new ArrayList();
ServerSocketChannel serverSocketChannel = null;
try {
//创建Server
serverSocketChannel = ServerSocketChannel.open();
//绑定端口
serverSocketChannel.bind(new InetSocketAddress("127.0.0.1", 9999));
serverSocketChannel.configureBlocking(false);
System.out.println("开始监听");
while (true){//每次循环都查看一下有没有新连接,同时处理已有的连接,看看有没有数据发送
SocketChannel channel = serverSocketChannel.accept();
if(channel!=null){//如果有连接进入就放入队列
channel.configureBlocking(false);//设置channel为非阻塞读取模式,免得没有数据时阻塞循环
list.add(channel);
}
//每次循环都处理已连接进来的channel,查看它们是否发送了数据
Iterator<SocketChannel> iterator = list.iterator();
while (iterator.hasNext()){
SocketChannel next = iterator.next();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int len=next.read(buffer);//如果len=0,说明目前没有数据发过来。如果len=-1,说明客户端关闭了连接通道。如果len>0,说明有数据,打印出来
if(len==-1){
iterator.remove();
}
System.out.println(next.getRemoteAddress()+"发送的数据:" + new String(buffer.array())+"数据大小:"+len);
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (serverSocketChannel != null) {
serverSocketChannel.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
@Test
public void client() {
Socket socket = null;
OutputStream outputStream = null;
InputStream inputStream = null;
try {
//1.创建客户端对象,并指定连接的IP地址或主机名+端口号
socket = new Socket("127.0.0.1", 9999);
//2.获取这个通道的流,输出数据到通道中
outputStream = socket.getOutputStream();
//3.发送数据
outputStream.write("你好".getBytes());
socket.shutdownOutput();//通过shutdownOutput高速服务器已经发送完数据,后续只能接受数据
} catch (Exception e) {
e.printStackTrace();
} finally {
//5.关闭资源
try {
if (inputStream != null) {
inputStream.close();
}
if (outputStream != null) {
outputStream.close();
}
if (socket != null) {
socket.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
总结:此模式只用单线程就可以处理连接进入、读取数据、将已断开的连接剔除的工作。可以支持少量连接
缺点:但是不够完美,每次处理数据都会遍历所有连接,有些连接一直不发数据,如果100个连接90个都没发送数据,那么这次循环的前90次都是在浪费时间。
解决:使用两个队列,队列1放所有连接进来的channel,只要某一个channel发送过一次数据,就视其为真实连接,将其从队列1移到队列2,然后使用一个handler线程专门处理队列2的真实连接,这样可以有效避免那些虚假连接占用资源。handler还要设置为饥饿模式,有连接马上就处理,没有连接时进入阻塞,这样的话还不会占用线程1的时间片。这个解决方案Selector已经完成了。