前言
在上篇文章中对BIO网络编程的相关内容进行了讲解,通过我们一步一步的优化,虽然我们通过多线程解决了并发访问的问题,但是BIO本身的一些特性造成的问题却没有得到解决。
BIO是阻塞IO,我们使用线程来进行IO的调度,我们无法确定io是否就绪,但是每个IO操作都会创建线程,这个时候如果IO未就绪,那么创建的线程也会处于阻塞状态。
在之前讲解NIO基本知识的时候我们提到过NIO通过通道选择器可以实现同时对多个通道的管理,实际上就是通过管理多个IO操作,换句话说是单线程处理多线程并发,有效的防止线程因为IO没有就绪而被挂起。
在使用NIO进行网络编程的时候需要用到的就是通道选择器,所以我们先看一下通道选择器的相关内容。
NIO
ServerSocketChannel
Java NIO中的 ServerSocketChannel 是一个可以监听新进来的TCP连接的通道, 就像标准IO中的ServerSocket一样。
//代开ServerSocketChannel
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
复制代码
通过 ServerSocketChannel.accept() 方法监听新进来的连接。当 accept()方法返回的时候,它返回一个包含新进来的连接的 SocketChannel。 可以设置成非阻塞模式。在非阻塞模式下,accept() 方法会立刻返回,如果还没有新进来的连接,返回的将是null。
SocketChannel
Java NIO中的SocketChannel是一个连接到TCP网络套接字的通道。可以通过以下2种方式创建SocketChannel:
//打开一个SocketChannel并连接到互联网上的某台服务器。
socketChannel.connect(new InetSocketAddress("localhost",8888));
//一个新连接到达ServerSocketChannel时,会创建一个SocketChannel。
SocketChannel socketChannel = SocketChannel.open();
复制代码
通道选择器 Selector
为什么使用通道选择器?
在前言中提到过,NIO通过通道选择器可以实现同时对多个通道的管理,其实就是同时对多个IO操作的管理,也就是实现了单线程处理多线程并发问题。对于操作系统来说,线程之间上下文切换的开销很大,而且每个线程都要占用系统的一些资源(如内存)。因此太多的线程会耗费大量的资源,所以使用通道选择器来对多个通道进行管理。
Selector的使用
1. 创建Selector
//通过调用Selector.open()方法创建一个Selector,如下:
Selector selector = Selector.open();
复制代码
2.将通道注册到通道选择器中
//为了将Channel和Selector配合使用,必须将channel注册到selector上。通过SelectableChannel.register()方法来实现
// 创建ServerSocketChanner
ServerSocketChannel ssc = ServerSocketChannel.open();
// 绑定端口号
ssc.bind(new InetSocketAddress(8888));
// 设置通道非阻塞
ssc.configureBlocking(false);
// 创建通道选择器
Selector selector = Selector.open();
// 将通道注册到通道选择器中 要求:通道都必须是非阻塞的 意味着不能将FileChannel与Selector一起使用,因为FileChannel不能切换到非阻塞模式
// 第二个参数是我们需要通道选择器帮我们管理什么事件类型 注册为接受就绪
ssc.register(selector, SelectionKey.OP_ACCEPT);
复制代码
register()方法的第二个参数代表注册的通道事件类型,一个通道触发一个事件就意味着该事件准备就绪了,总共有四种事件:Connect(连接就绪 ) Accept(接受就绪) Read(有数据可读的通道 读就绪) Write(写就绪)。对于选择器而言,可以针对性的找到(监听)的事件就绪的通道,进行相关的操作。
这四种事件用SelectionKey的四个常量来表示:
- SelectionKey.OP_CONNECT
- SelectionKey.OP_ACCEPT
- SelectionKey.OP_READ
- SelectionKey.OP_WRITE
可以用“位或”操作符将常量连接起来
SelectionKey对象
对象中包含很多的属性,譬如:
- interest集合(事件集合)
- ready集合
- Channel
- Selector
- 附加的对象(可选)
3.选择器的select ()方法
select()方法返回的int值表示有多少通道已经就绪。自上次调用select()方法后有多少通道变成就绪状态。如果调用select()方法,因为有一个通道变成就绪状态,返回了1,若再次调用select()方法,如果另一个通道就绪了,它会再次返回1。如果对第一个就绪的channel没有做任何操作,现在就有两个就绪的通道,但在每次select()方法调用之间,只会有一个通道就绪。
4.选择器的selectedKeys()方法
通过调用selector的selectedKeys()方法,可以得到就绪通道的集合。遍历集合可以找到自己需要的通道进行相关的操作。
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> iterator = keys.iterator();
while (iterator.hasNext()){
SelectionKey key = iterator.next();
//处理nio事件
if(key.isAcceptable()){
// 获取通道
ServerSocketChannel channel = (ServerSocketChannel) key.channel();
SocketChannel socketChannel = channel.accept();
// 注册读事件类型
socketChannel.configureBlocking(false);
socketChannel.register(selector, SelectionKey.OP_READ);
}
if(key.isReadable()){
SocketChannel socketChannel = (SocketChannel) key.channel();
// 读取数据
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
int len = -1;
while (true){
byteBuffer.clear();
len = socketChannel.read(byteBuffer);
if(len == -1){
break;
}
byteBuffer.flip();
while (byteBuffer.hasRemaining()){
bos.write(byteBuffer.get());
}
}
// 打印读取到的数据
System.out.println(bos.toString());
//写数据给客户端 注册事件类型 写事件
socketChannel.register(selector,SelectionKey.OP_WRITE);
}
if(key.isWritable()){
SocketChannel socketChannel = (SocketChannel) key.channel();
String msg = "你好,我是服务器";
ByteBuffer byteBuffer = ByteBuffer.allocate(msg.getBytes().length);
byteBuffer.put(msg.getBytes());
byteBuffer.flip();
socketChannel.write(byteBuffer);
socketChannel.close();
}
iterator.remove();
}
复制代码
NIO网络编程实例
服务器编程
基本步骤
- 打开ServerSocketChanner
- 绑定端口号
- 设置通道非阻塞
- 打开选择器,把通道注册到选择器中
- 使用Selector轮询所有的key
- 获取socketChannel
- 设置非阻塞 注册读事件
- 读取操作
- 注册写事件
- 写操作
代码
import java.io.ByteArrayOutputStream;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Set;
public class NIOServer {
public static void main(String[] args) throws Exception{
// 创建ServerSocketChanner
ServerSocketChannel ssc = ServerSocketChannel.open();
// 绑定端口号
ssc.bind(new InetSocketAddress(8888));
// 设置通道非阻塞
ssc.configureBlocking(false);
// 创建通道选择器
Selector selector = Selector.open();
// 将通道注册到通道选择器中 要求:通道都必须是非阻塞的
// 第二个参数是我们需要通道选择器帮我们管理什么事件类型 注册为接受就绪
ssc.register(selector, SelectionKey.OP_ACCEPT);
// 遍历通道选择器
while (true){
System.out.println("我在8888等你......");
// 返回准备就绪的通道数量
int nums = selector.select();
// 如果数量小于1说明没有通道准备就绪 跳过本次循环
if(nums<1) {continue;}
// 获取所有的keys(通道 事件类型)
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> iterator = keys.iterator();
while (iterator.hasNext()){
SelectionKey key = iterator.next();
//处理nio事件
if(key.isAcceptable()){
// 获取通道
ServerSocketChannel channel = (ServerSocketChannel) key.channel();
SocketChannel socketChannel = channel.accept();
// 注册读事件类型
socketChannel.configureBlocking(false);
socketChannel.register(selector, SelectionKey.OP_READ);
}
if(key.isReadable()){
SocketChannel socketChannel = (SocketChannel) key.channel();
// 读取数据
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
int len = -1;
while (true){
byteBuffer.clear();
len = socketChannel.read(byteBuffer);
if(len == -1){
break;
}
byteBuffer.flip();
while (byteBuffer.hasRemaining()){
bos.write(byteBuffer.get());
}
}
// 打印读取到的数据
System.out.println(bos.toString());
//写数据给客户端 注册事件类型 写事件
socketChannel.register(selector,SelectionKey.OP_WRITE);
}
if(key.isWritable()){
SocketChannel socketChannel = (SocketChannel) key.channel();
String msg = "你好,我是服务器";
ByteBuffer byteBuffer = ByteBuffer.allocate(msg.getBytes().length);
byteBuffer.put(msg.getBytes());
byteBuffer.flip();
socketChannel.write(byteBuffer);
socketChannel.close();
}
iterator.remove();
}
}
}
}
复制代码
客户端编程
基本步骤
- 打开SocketChannel
- 连接服务器
- 写数据给服务器
- 读取数据
- 关闭SocketChannel
代码
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
public class NIOCleint {
public static void main(String[] args) throws IOException {
// 创建sc
SocketChannel socketChannel = SocketChannel.open();
// 连接服务器
socketChannel.connect(new InetSocketAddress("localhost",8888));
// 写数据给服务器
String msg = "你好,我是客户端";
ByteBuffer byteBuffer = ByteBuffer.allocate(msg.getBytes().length);
byteBuffer.put(msg.getBytes());
byteBuffer.flip();
socketChannel.write(byteBuffer);
// 关闭输出流
socketChannel.shutdownOutput();
// 读取数据
ByteBuffer readBuffer = ByteBuffer.allocate(1024);
ByteArrayOutputStream bosread = new ByteArrayOutputStream();
int len = -1;
while (true){
readBuffer.clear();
len = socketChannel.read(readBuffer);
if(len == -1){
break;
}
readBuffer.flip();
while (readBuffer.hasRemaining()){
bosread.write(readBuffer.get());
}
}
System.out.println("我收到:"+bosread.toString());
socketChannel.close();
}
}
复制代码
我不能保证每一个地方都是对的,但是可以保证每一句话,每一行代码都是经过推敲和斟酌的。希望每一篇文章背后都是自己追求纯粹技术人生的态度。
永远相信美好的事情即将发生。