BIO
BIO,同步阻塞IO,实现模式是一个连接一个线程。
例如:网络编程socket,每次客户端发起一个连接请求就启动一个线程处理数据传递操作,如果第一个连接未做任何操作,则一直占用线程导致其他客户端数据无法传递到服务端。
示例代码演示说明,启动服务:
public class SocketServer {
public static void main(String[] args) {
try (ServerSocket serverSocket = new ServerSocket(SocketConstant.SOCKET_SERVER_PORT)) {
while (true) {
System.out.println(">>>>>>建立socket,监听客户端。");
Socket socket = serverSocket.accept();
InputStream inputStream = socket.getInputStream();
System.out.println(">>>>>>接收socket输入流。");
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
String message = null;
if ((message = bufferedReader.readLine()) != null) {
System.out.println(">>>>>>服务端打印消息:[" + message + "]");
}
socket.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
服务端控制台输出:
>>>>>>建立socket,监听客户端。
此时socket服务已启动,并处理阻塞状态,监听来自指定端口的客户端请求。
然后,执行客户端请求并输入传递数据。
public class SocketClient {
public static void main(String[] args) {
try (Socket socket = new Socket(SocketConstant.SOCKET_SERVER_IP, SocketConstant.SOCKET_SERVER_PORT);
OutputStream outputStream = socket.getOutputStream();) {
Scanner scanner = new Scanner(System.in);
System.out.println("客户端输入消息:");
String message = scanner.nextLine();
outputStream.write(message.getBytes(SocketConstant.SOCKET_CODE_MA));
} catch (IOException e) {
e.printStackTrace();
}
}
}
客户端控制台输入:
客户端输入消息:
socket演示BIO同步阻塞IO示例
客户端输入数据后回车发送至服务端。
服务端控制台输出效果:
>>>>>>建立socket,监听客户端。
>>>>>>接收socket输入流。
>>>>>>服务端打印消息:[socket演示BIO同步阻塞IO示例]
>>>>>>建立socket,监听客户端。
最后,服务端正常接收到客户端传递数据后,又重新阻塞,建立监听等待下一次客户端请求。
这次我们连续执行两次客户端请求,第一次请求后不输入传递数据,第二次请求后输入传递数据,结果服务端执行了接收socket客户端输入流操作,但是没有接收到任何数据。
服务端控制台输出效果:
>>>>>>建立socket,监听客户端。
>>>>>>接收socket输入流。
>>>>>>服务端打印消息:[socket演示BIO同步阻塞IO示例]
>>>>>>建立socket,监听客户端。
>>>>>>接收socket输入流。
为什么第二次客户端请求输入的数据没有在服务端输出呢?
因为客户端第一次请求后,服务端监听到了但是由于客户端没有传递数据所以线程阻塞在数据接收这里,而第二次客户端请求虽然输入了数据但是因为第一次还未执行完成,线程被占用处于阻塞中,所以只能等待第一次执行完成。
那么我们在第一次客户端请求中输入数据执行,就可以看到服务端不仅接收到了第一次的数据传递也接收到了第二次的数据传递。
服务端控制台输出效果:
>>>>>>建立socket,监听客户端。
>>>>>>接收socket输入流。
>>>>>>服务端打印消息:[socket演示BIO同步阻塞IO示例]
>>>>>>建立socket,监听客户端。
>>>>>>接收socket输入流。
>>>>>>服务端打印消息:[第一次客户端数据内容。]
>>>>>>建立socket,监听客户端。
>>>>>>接收socket输入流。
>>>>>>服务端打印消息:[第二次客户端请求。]
>>>>>>建立socket,监听客户端。
上述示例,阻塞出现在了服务监听和数据接收。我们可以使用多线程优化,保证客户端的提交不会因为线程被占用而阻塞,修改服务端代码如下:
public class SocketThreadServer {
public static void main(String[] args) {
try (ServerSocket serverSocket = new ServerSocket(SocketConstant.SOCKET_SERVER_PORT)) {
while (true) {
System.out.println(">>>>>>建立socket,监听客户端。");
Socket socket = serverSocket.accept();
new Thread(new SocketThreadRun(socket)).start();
}
} catch (IOException e) {
e.printStackTrace();
}
}
//开启线程接收客户端消息
static class SocketThreadRun implements Runnable{
Socket socket;
public SocketThreadRun(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
InputStream inputStream = null;
try {
inputStream = socket.getInputStream();
System.out.println(">>>>>>接收socket输入流。");
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
String message = null;
if ((message = bufferedReader.readLine()) != null) {
System.out.println(">>>>>>服务端打印消息:[" + message + "]");
}
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
启动新的多线程服务端,再启动多个客户端输入数据进行传递,就不会互相影响了。
服务端控制台输出效果:
>>>>>>建立socket,监听客户端。
>>>>>>建立socket,监听客户端。
>>>>>>接收socket输入流。
>>>>>>建立socket,监听客户端。
>>>>>>接收socket输入流。
>>>>>>服务端打印消息:[第二个客户端请求的内容。]
注意,如果并发量过高,需要开启大量线程消耗过多内存,并且系统过度频繁的调用也会降低性能。为了降低频繁的创建和销毁对象,因此提出了线程池的概念,提前创建好一个线程池,包含若干空闲线程,需要时直接调用。
public class SocketThreadPoolServer {
public static void main(String[] args) {
//首先创建线程池,指定线程数量为SocketConstant.SOCKET_THREAD_POOLS。
ExecutorService executorService = Executors.newFixedThreadPool(SocketConstant.SOCKET_THREAD_POOLS);
try (ServerSocket serverSocket = new ServerSocket(SocketConstant.SOCKET_SERVER_PORT)) {
while (true) {
System.out.println(">>>>>>建立socket,监听客户端。");
Socket socket = serverSocket.accept();
executorService.execute(new SocketThreadRun(socket));
}
} catch (IOException e) {
e.printStackTrace();
}
executorService.shutdown();
}
//开启线程接收客户端消息
static class SocketThreadRun implements Runnable{
Socket socket;
public SocketThreadRun(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
InputStream inputStream = null;
try {
inputStream = socket.getInputStream();
System.out.println(">>>>>>接收socket输入流。");
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
String message = null;
if ((message = bufferedReader.readLine()) != null) {
System.out.println(">>>>>>服务端打印消息:[" + message + "]");
}
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
我们虽然做了一层层优化,但是并没有消除阻塞,因为即使是线程池也是有边界的,比如线程池中初始化100个空闲线程,但是并发量超过100,而且正在执行的100个线程如果一直未执行数据传递,导致线程池被占满,那么其他请求还是只能等待阻塞处理。这种情况还浪费了很多系统资源,因为只是启动了100个阻塞线程并不一定会占用多少内存和CPU计算资源,那么我们是否可以让发生阻塞的线程重新空闲出来给排队的其他请求使用呢?>。< 想得美!
NIO
NIO,New IO, 同步非阻塞IO,实现模式是一个请求一个线程。java1.4版本开始推出的。
例如:Socket发起一个连接请求,先注册在选择器上,只有检测到该连接发生了I/O请求后才会启动或占用一个线程。
NIO核心思想是事件驱动,它包含三个重要的组件Selector选择器,Channel通道和Buffer缓存区。如上图所示NIO的执行流程示意图,第一步开启一个选择器线程循环执行,它会监听通道的事件是否就绪(如数据是否输入);第二步将Channel都注册在选择器上;第三步一旦Channel上的事件就绪了(如数据已经输入)就会交给线程去执行,这里可以是单线程也可以是线程池,NIO完全可以实现单线程执行多请求,如果使用线程池也只需要开启CPU核心数量就足够了,因为这里执行是不会发生阻塞的,不需要消耗多余资源与性能。
Buffer缓存区则可以理解为是数据的中转站,数据的读写都是通过Buffer完成;Channel则是数据的通道,指向数据的源头或者目的地,执行对Buffer对象的读写操作。
NIO服务端代码:
public class NIOServer {
public static void main(String[] args) {
try {
//1.创建Selector
Selector selector = Selector.open();
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(SocketConstant.SOCKET_SERVER_PORT));
//2.注册到selector
serverSocketChannel.configureBlocking(false);//设置为非阻塞状态
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);//注册监听'连接'状态
ExecutorService executorService = Executors.newFixedThreadPool(SocketConstant.CPU_THREAD_POOLS);//开启线程池
//3.监听事件
int taskcount = 0;//计数器
while (true) {
//阻塞等待事件就绪,但是阻塞可能会释放返回0,增加判断保持阻塞。
int op_channel_count = selector.select();
if (op_channel_count == 0) {
continue;
}
//获得selector监听到就绪事件集合
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> selectionKeyIterator = selectionKeys.iterator();
while (selectionKeyIterator.hasNext()) {
SelectionKey selectionKey = selectionKeyIterator.next();
if (selectionKey.isAcceptable()) {//监听到连接状态(OP_ACCEPT),仅ServerSocketChannel支持
serverSocketChannel = (ServerSocketChannel) selectionKey.channel();
SocketChannel channel = serverSocketChannel.accept();
channel.configureBlocking(false);
channel.register(selector, SelectionKey.OP_READ, ++taskcount);
} else if (selectionKey.isReadable()) {//监听到读取状态(OP_READ)
executorService.execute(new SocketThreadPool(selectionKey));//将事件交给线程池处理具体业务
selectionKey.cancel();//*取消掉selector注册,避免线程池处理不及时重复选择。
} else if (selectionKey.isConnectable()) {//监听到连接状态(OP_CONNECT),客户端可支持。
} else if (selectionKey.isWritable()) {//监听到写入状态(OP_WRITE)
}
//任务处理完成后从集合中移除
selectionKeyIterator.remove();
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
//开启线程处理任务
static class SocketThreadPool implements Runnable {
SelectionKey selectionKey;
public SocketThreadPool(SelectionKey selectionKey) {
this.selectionKey = selectionKey;
}
@Override
public void run() {
try {
System.out.println("[" + selectionKey.attachment() + "]连接数据:" + getChannelData());
selectionKey.channel().close();
} catch (IOException e) {
e.printStackTrace();
}
}
//Plus01.解析channel读取Buffer的数据
private String getChannelData() throws IOException {
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
//Buffer必须指定一个大小
ByteBuffer byteBuffer = ByteBuffer.allocate(SocketConstant.NIO_BUFFER_SIZE);
//为了接收全部的内容后再转换,需要定义一个更大的Buffer
ByteBuffer resultByteBuffer = null;
int bufferCount = 0;
//读取socketChannel,返回-1表示读取完毕。
while ((socketChannel.read(byteBuffer) != -1)) {
bufferCount++;
ByteBuffer tempByteBuffer = ByteBuffer.allocateDirect(SocketConstant.NIO_BUFFER_SIZE * (bufferCount + 1));
if (resultByteBuffer != null) {
resultByteBuffer.flip();//设置为读模式
tempByteBuffer.put(resultByteBuffer);
}
resultByteBuffer = tempByteBuffer;
byteBuffer.flip();
resultByteBuffer.put(byteBuffer);
byteBuffer.clear();
}
if (resultByteBuffer != null) {
resultByteBuffer.flip();
Charset charset = Charset.forName(SocketConstant.SOCKET_CODE_MA);
CharsetDecoder charsetDecoder = charset.newDecoder();
return charsetDecoder.decode(resultByteBuffer).toString();
}
return "";
}
}
}
NIO客户端:
public class NIOClient {
public static void main(String[] args) {
try(SocketChannel socketChannel = SocketChannel.open()){
socketChannel.connect(new InetSocketAddress(SocketConstant.SOCKET_SERVER_IP,SocketConstant.SOCKET_SERVER_PORT));
Scanner scanner = new Scanner(System.in);
System.out.println("NIO客户端输入消息:");
String message = scanner.nextLine();
ByteBuffer byteBuffer = ByteBuffer.wrap(message.getBytes(SocketConstant.SOCKET_CODE_MA));
while (byteBuffer.hasRemaining()){
socketChannel.write(byteBuffer);
}
}catch (IOException e){
e.printStackTrace();
}
}
}
通过代码示例,可以直观感受到,当客户端连接时服务端会执行哪个监听动作,当客户端发送数据时服务端又会执行哪个监听动作,这就是事件驱动的好处,不会因为未完成某个事件而阻塞整个线程,更高的利用了系统资源。