NIO详解
一、其他Channel
1. Socket通道
所有的 socket 通道类(DatagramChannel、
SocketChannel 和 ServerSocketChannel)都继承了位于 java.nio.channels.spi 包中的 AbstractSelectableChannel。这意味着我们可以用一个 Selector 对象来执行socket 通道的就绪选择
- 非阻塞模式中,有更多的伸缩性和灵活性。使用socket,可以用一个线程或者多个线程就可以管理成百上千个socket,管理功能强大,而且没有性能损失,可以通过 Selector监听多个通道
- DatagramChannel 和 SocketChannel 实现定义读和写功能的接口而ServerSocketChannel 不实现。ServerSocketChannel 负责监听传入的连接和创建新的 SocketChannel 对象,它本身从不传输数据。究其代码是因为它实现的接口比较少
- socket通道可以被重复使用,socket不可以被重复使用
- 设置socket的通道模式为阻塞还是非阻塞,可以通过AbstractSelectableChannel.java 中实现的 configureBlocking()方法。传递参数值为 true 则设为阻塞模式,参数值为 false 值设为非阻塞模式
2. ServerSocketChannel
- 本身不传入数据,主要是为了监听,可以在非阻塞模式下运行没有bind()绑定方法,通过对等的socket来绑定端口并进行监听
- ServerSocketChannel的对象.socket().bind();
- 有accept()方法,返回SocketChannel 类型,对象为空则没有链接,反之则。可以在非阻塞下运行
其它 Socket 的 accept()方法会阻塞返回一个 Socket 对象。如果
ServerSocketChannel 以非阻塞模式被调用,当没有传入连接在等待时,ServerSocketChannel.accept( )会立即返回 null。正是这种检查连接而不阻塞的能力实现了可伸缩性并降低了复杂性。可选择性也因此得到实现。我们可以使用一个选择器实例来注册 ServerSocketChannel 对象以实现新连接到达时自动通知的功能
具体的代码思路步骤为:
- 打开ServerSocketChannel
- 绑定端口号
- 设置非阻塞模式
configureBlocking(false);
- 监听连接,通过accept
- 将其buffer归为0指针
rewind()
,并且写入buffer - 关闭 ServerSocketChannel
具体通过代码演示演示代码代码如下:
public class ServerSocketChannelDemo {
public static void main(String[] args) throws Exception {
//端口号
int port = 8888;
//buffer
ByteBuffer buffer = ByteBuffer.wrap("hello opencoder".getBytes());
//ServerSocketChannel
ServerSocketChannel ssc = ServerSocketChannel.open();
//绑定
ssc.socket().bind(new InetSocketAddress(port));
//设置非阻塞模式
ssc.configureBlocking(false);
//监听有新链接传入
while(true) {
System.out.println("Waiting for connections");
SocketChannel sc = ssc.accept();
if(sc == null) {
//没有链接传入
System.out.println("null");
Thread.sleep(2000);
} else {
System.out.println("Incoming connection from: " + sc.socket().getRemoteSocketAddress());
buffer.rewind(); //指针0
sc.write(buffer);
sc.close();
}
}
}
}
通过浏览器点击127.0.0.1:8888,就会出现监听的提示
3. SocketChannel
- 用于连接到TCP的套接字
- 实现多路复用
- 面向缓冲区
主要的特征有:
(1)对于已经存在的 socket 不能创建 SocketChannel
(2)SocketChannel 中提供的 open 接口创建的 Channel 并没有进行网络级联,需要使用 connect 接口连接到指定地址
(3)未进行连接的 SocketChannle 执行 I/O 操作时,会抛出
NotYetConnectedException
(4)SocketChannel 支持两种 I/O 模式:阻塞式和非阻塞式
(5)SocketChannel 支持异步关闭。如果 SocketChannel 在一个线程上 read 阻塞,另一个线程对该 SocketChannel 调用 shutdownInput,则读阻塞的线程将返回-1 表示没有读取任何数据;如果 SocketChannel 在一个线程上 write 阻塞,另一个线程对该SocketChannel 调用 shutdownWrite,则写阻塞的线程将抛出AsynchronousCloseException
(6)SocketChannel 支持设定参数
SO_SNDBUF 套接字发送缓冲区大小
SO_RCVBUF 套接字接收缓冲区大小
SO_KEEPALIVE 保活连接
O_REUSEADDR 复用地址
SO_LINGER 有数据传输时延缓关闭 Channel (只有在非阻塞模式下有用)
TCP_NODELAY 禁用 Nagle 算法
部分代码解释
创建 SocketChannel
SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("www.baidu.com", 80)); //或者 SocketChannel socketChanne2 = SocketChannel.open(); socketChanne2.connect(new InetSocketAddress("www.baidu.com", 80));
连接校验
socketChannel.isOpen(); // 测试 SocketChannel 是否为 open 状态 socketChannel.isConnected(); //测试 SocketChannel 是否已经被连接 socketChannel.isConnectionPending(); //测试 SocketChannel 是否正在进行 连接 socketChannel.finishConnect(); //校验正在进行套接字连接的 SocketChannel 是否已经完成连接
读写模式(有阻塞和非阻塞)
设置 SocketChannel 的读写模式。false 表示非阻塞,true 表示阻塞
socketChannel.configureBlocking(false);
读写相关代码
//阻塞读 SocketChannel socketChannel = SocketChannel.open( new InetSocketAddress("www.baidu.com", 80)); ByteBuffer byteBuffer = ByteBuffer.allocate(16); socketChannel.read(byteBuffer); socketChannel.close(); System.out.println("read over"); //非阻塞读 SocketChannel socketChannel = SocketChannel.open( new InetSocketAddress("www.baidu.com", 80)); socketChannel.configureBlocking(false); ByteBuffer byteBuffer = ByteBuffer.allocate(16); socketChannel.read(byteBuffer); socketChannel.close(); System.out.println("read over");
完整代码如下:
public class SocketChannelDemo {
public static void main(String[] args) throws Exception {
//创建SocketChannel
SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("www.baidu.com", 80));
// SocketChannel socketChanne2 = SocketChannel.open();
// socketChanne2.connect(new InetSocketAddress("www.baidu.com", 80));
//设置阻塞和非阻塞
socketChannel.configureBlocking(false);
//读操作
ByteBuffer byteBuffer = ByteBuffer.allocate(16);
socketChannel.read(byteBuffer);
socketChannel.close();
System.out.println("read over");
}
}
4. DatagramChannel
- SocketChannel 对应 Socket,ServerSocketChannel 对应ServerSocket
- DatagramChannel 对应的是 DatagramSocket 对象,DatagramChannel (如UDP/IP)是无连接的,可以发送单独的数据报给不同的目的地址。同样,可以接收来自任意地址的数据包。
部分代码解释
打开 DatagramChannel
DatagramChannel server = DatagramChannel.open(); server.socket().bind(new InetSocketAddress(10086));
接收数据
ByteBuffer receiveBuffer = ByteBuffer.allocate(64); receiveBuffer.clear(); SocketAddress receiveAddr = server.receive(receiveBuffer);
发送数据
DatagramChannel server = DatagramChannel.open(); ByteBuffer sendBuffer = ByteBuffer.wrap("client send".getBytes()); server.send(sendBuffer, new InetSocketAddress("127.0.0.1",10086));
连接
read()和 write()只有在 connect()后才能使用,否则会抛异常。
client.connect(new InetSocketAddress("127.0.0.1",10086)); int readSize= client.read(sendBuffer); server.write(sendBuffer);
具体发送的完整代码
//发送的实现 @Test public void sendDatagram() throws Exception { //打开 DatagramChannel DatagramChannel sendChannel = DatagramChannel.open(); InetSocketAddress sendAddress = new InetSocketAddress("127.0.0.1",9999); //发送 while(true) { ByteBuffer buffer = ByteBuffer.wrap("发送数据".getBytes("UTF-8")); sendChannel.send(buffer,sendAddress); System.out.println("已经完成发送"); Thread.sleep(1000); } }
具体接收的完整代码
接收代码的时候其缓冲区一开始要先清除,在将其接收之后缓冲区的数据读写转换在将其输出注意此处的输出语句要用
toString()
//接收的实现 @Test public void receiveDatagram() throws Exception { //打开DatagramChannel DatagramChannel receiveChannel = DatagramChannel.open(); InetSocketAddress receiveAddress = new InetSocketAddress(9999); //绑定 receiveChannel.bind(receiveAddress); //buffer ByteBuffer receiveBuffer = ByteBuffer.allocate(1024); //接收 while(true) { receiveBuffer.clear(); SocketAddress socketAddress = receiveChannel.receive(receiveBuffer); receiveBuffer.flip(); System.out.println(socketAddress.toString()); System.out.println(Charset.forName("UTF-8").decode(receiveBuffer)); } }
5. Scatter/Gather
以下两种方式都是按照顺序读取或者写入:
分散(scatter)从 Channel 中读取是指在读操作时将读取的数据写入多个 buffer中。
ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body = ByteBuffer.allocate(1024);
ByteBuffer[] bufferArray = {
header, body };
channel.read(bufferArray);
聚集(gather)写入 Channel 是指在写操作时将多个 buffer 的数据写入同一个Channel
ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body = ByteBuffer.allocate(1024);
//write data into buffers
ByteBuffer[] bufferArray = {
header, body };
channel.write(bufferArray);
他们的使用方式跟其他的Channel基本都是一样的这里就不在赘述了,大家可以按照之前的使用方式自行学一下这两种方式的使用。
总结
今天主要是为大家详细的介绍了常见的各种Channel以及他们的用法,本文编写了大量的案例还需要大家认真的去实践以后才能真正的掌握住。介绍完Channel那么下一篇文章我们就可以为大家介绍Buffer和Selector的具体使用了。