阻塞和非阻塞的概念
-
传统的 IO 流都是阻塞式的。也就是说,当一个线程调用 read() 或 write()时,该线程被阻塞,直到有一些数据被读取或写入,该线程在此期间不能执行其他任务。因此,在完成网络通信进行 IO 操作时,由于线程会阻塞,所以服务器端必须为每个客户端都提供一个独立的线程进行处理,当服务器端需要处理大量客户端时,性能急剧下降。
-
Java NIO 是非阻塞模式的。当线程从某通道进行读写数据时,若没有数据可用时,该线程可以进行其他任务。线程通常将非阻塞 IO 的空闲时间用于在其他通道上执行 IO 操作,所以单独的线程可以管理多个输入和输出通道。因此,NIO 可以让服务器端使用一个或有限几个线程来同时处理连接到服务器端的所有客户端。
阻塞
- 使用NIO完成网络通信的三个核心
①,通道(Channel):负责连接
java.nio.channels.Channel接口
SelectableChannel
SocketChannel
ServerSocketChannel
DatagramChannel
Pipe.SinkChannel
Pipe.SourceChannel
②,缓冲区(Buffer):负责数据的存取
③, 选择器(Selector):是SelectableChannel的多路复用器。用于监控SelectableChannel的IO状况
- 选择器(Selector)的应用:
当调用 register(Selector sel, int ops) 将通道注册选择器时,选择器对通道的监听事件,需要通过第二个参数 ops 指定。
可以监听的事件类型(用 可使用 SelectionKey 的四个常量 表示):
读 : SelectionKey.OP_READ
写 : SelectionKey.OP_WRITE
连接 : SelectionKey.OP_CONNECT
接收 : SelectionKey.OP_ACCEPT
若注册时不止监听一个事件,则可以使用“位或”操作符连接。
例:
阻塞式代码如下:
//客户端
@Test
public void client() throws Exception{
//1,获取通道
SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9898));
FileChannel inChannel = FileChannel.open(Paths.get("1.jpg"),StandardOpenOption.READ);
//2,分配指定大小的缓冲区
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
//3,读取本地文件,并发送到服务端
while (inChannel.read(byteBuffer) != -1){
byteBuffer.flip();
socketChannel.write(byteBuffer);
byteBuffer.clear();
}
socketChannel.shutdownOutput();
//接收服务端的反馈
int len = 0;
while ((len = socketChannel.read(byteBuffer)) != -1){
byteBuffer.flip();
System.out.println(new String(byteBuffer.array(),0,len));
byteBuffer.clear();
}
//4,关闭通道
inChannel.close();
socketChannel.close();
}
//服务端
@Test
public void server() throws Exception{
//1,获取通道
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
FileChannel outChannel = FileChannel.open(Paths.get("4.jpg"),StandardOpenOption.WRITE,StandardOpenOption.CREATE);
//2,绑定连接
serverSocketChannel.bind(new InetSocketAddress(9898));
//3,获取客户端连接的通道
SocketChannel accept = serverSocketChannel.accept();
//4,分配指定大小的缓冲区
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
//5,接收客户端的数据,并保存到本地
while (accept.read(byteBuffer) != -1){
byteBuffer.flip();
outChannel.write(byteBuffer);
byteBuffer.clear();
}
//发送反映给客户端
byteBuffer.put("图片已收到,Over! Over!".getBytes());
byteBuffer.flip();
accept.write(byteBuffer);
//6,关闭通道
outChannel.close();
accept.close();
serverSocketChannel.close();
}
非阻塞
代码如下:
//客户端
@Test
public void client() throws Exception {
//1,获取通道
SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 8888));
//2,切换非阻塞式模式
socketChannel.configureBlocking(false);
//3,范围内配指定大小的缓冲区
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
//4,发送数据给服务端
int num = 5;
while (num > 0) {
byteBuffer.put((LocalDateTime.now().toString() + ":" + num).getBytes());
byteBuffer.flip();
socketChannel.write(byteBuffer);
byteBuffer.clear();
--num;
}
//5,关闭通道
socketChannel.close();
}
//服务端
@Test
public void server() throws Exception {
//1,获取通道
ServerSocketChannel ssChannel = ServerSocketChannel.open();
//2,切换非阻塞模式
ssChannel.configureBlocking(false);
//3,绑定连接
ssChannel.bind(new InetSocketAddress(8888));
//4,获取选择器
Selector selector = Selector.open();
//5,将通道注册到选择器上,并指定“监听接收事件”
ssChannel.register(selector, SelectionKey.OP_ACCEPT);
//6,轮询式的获取选择器上已经“准备就绪”的事件
while (selector.select() > 0) {
//7,获取当前选择器中所有注册的“选择器(已就绪的监听事件)”
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
//8,获取准备“就绪”的事件
SelectionKey sk = iterator.next();
//9,判断具体是什么事件准备就绪
if (sk.isAcceptable()) {
//10,若“接收就绪”,获取客户端连接
SocketChannel sChannel = ssChannel.accept();
//11,切换非阻塞模式
sChannel.configureBlocking(false);
//12,将该通道注册到选择器上
sChannel.register(selector, SelectionKey.OP_READ);
} else if (sk.isReadable()) {
//13,获取当前选择器上“读就绪”状态的通道
SocketChannel sChannel = (SocketChannel) sk.channel();
//14,读取数据
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
int len = 0;
while ((len = sChannel.read(byteBuffer)) > 0) {
byteBuffer.flip();
System.out.println(new String(byteBuffer.array(),0, len));
byteBuffer.clear();
}
}
iterator.remove();
}
}
}
DatagramChannel
-
Java NIO中的DatagramChannel是一个能收发UDP包的通道。
-
操作步骤:
①,打开 DatagramChannel
②,接收/发送数据。代码如下:
@Test
public void send() throws Exception{
DatagramChannel dc = DatagramChannel.open();
dc.configureBlocking(false);
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
int i = 5;
while (i > 0){
byteBuffer.put(("Tommey周"+i+"号").getBytes());
byteBuffer.flip();
dc.send(byteBuffer, new InetSocketAddress("127.0.0.1",9999));
byteBuffer.clear();
i--;
}
dc.close();
}
@Test
public void receive() throws Exception{
DatagramChannel dc = DatagramChannel.open();
dc.configureBlocking(false);
dc.bind(new InetSocketAddress(9999));
Selector selector = Selector.open();
dc.register(selector,SelectionKey.OP_READ);
while (selector.select() > 0){
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()){
SelectionKey sk = iterator.next();
if (sk.isReadable()){
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
dc.receive(byteBuffer);
byteBuffer.flip();
System.out.println(new String(byteBuffer.array(),0,byteBuffer.limit()));
byteBuffer.clear();
}
}
iterator.remove();
}
}
Pipe管道
- Java NIO 管道是2个线程之间的单向数据连接。Pipe有一个source通道和一个sink通道。数据会被写到sink通道,从source通道读取。代码如下:
@Test
public void testPiPe() throws Exception{
//1,获取管道
Pipe pipe = Pipe.open();
//2,将缓冲区中的数据写入管道
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
Pipe.SinkChannel sink = pipe.sink();
byteBuffer.put("通过单向管道发送数据".getBytes());
byteBuffer.flip();
sink.write(byteBuffer);
//3,读取缓冲区中的数据
Pipe.SourceChannel source = pipe.source();
byteBuffer.flip();
int len = source.read(byteBuffer);
System.out.println(new String(byteBuffer.array(),0, len));
source.close();
sink.close();
}