开发环境
OS:Win10(需要开启telnet服务,或使用第三方远程工具)
Java版本:8
BIO
概念
BIO(Block IO),即同步阻塞IO,特点为当客户端发起请求后,在服务端未处理完该请求之前,客户端将一直等待服务端的响应。而服务端在此时也专注于该请求的处理,无法处理其它客户端的请求。
示例
package com.mlyzr.bio;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.Calendar;
/**
* @author 勿忘初心
* @since 2023-08-07-23:59
* 同步阻塞IO示例
*/
public class BioServer {
public static void main(String[] args) throws IOException {
// 监听本地8096端口
ServerSocket serverSocket = new ServerSocket(8096);
while(true){
System.out.println("等待客户端连接...");
// 接收客户端请求,阻塞
Socket clientSocket = serverSocket.accept();
System.out.println("监听到客户端连接");
// 使用NIO处理请求信息
handler(clientSocket);
}
}
public static void handler(Socket clientSocket) throws IOException {
byte[] bytes = new byte[1024];
System.out.println("准备读取");
int read = clientSocket.getInputStream().read(bytes);
System.out.println("读取完毕");
if(read != -1){
System.out.println("接收到来自客户端的数据 "+new String(bytes,0,read,StandardCharsets.UTF_8));
}
}
}
在IDEA运行上述代码后,将在控制台看如下输出
此时先打开第一个客户端,这里使用telnet连接,具体操作为使用快捷键 Win+R 打开命令行,输入cmd,输入telnet localhost 8096(代码中绑定的端口)后回车,按下 ctrl+] (ctrl+ 右括号)即可进入telnet的交互模式,可向服务端发送信息。(telnet命令报错请使用自行搜索开启telnet服务,或使用其它工具如mobxterm等)
注意:cmd工具的telnet模式下无法发送中文字符,会出现乱码现象,如需发送中文字符请使用MobaXterm等第三方工具进行测试。
客户端
当客户端成功连接后,服务端控制台输出将由等待状态变为读取状态,等待当前客户端发送消息。
此时再新建一个客户端连接,服务端控制台输出依旧不会发生任何改变, 因为第一个客户端的资源处理还没有结束。
在第一个客户端中使用send命令向服务端发送消息(不熟悉telnet命令的话可以输入help查看)可以看到服务端已经接收到发送的 client1 字符串后输出,并且之前的第二个客户端的连接目前可以被处理,同样进入准备读取的状态。
使用第二个客户端发送消息,此时服务端正常输出。
优化
不难发现上述操作中存在的问题,服务端一次只能处理一个客户端的请求,其余的客户端只能等待,当某一个客户端请求的资源处理耗时较长时,对于其它的客户端使用是非常糟糕的,这里可以通过使用多线程方式来进行优化。
package com.mlyzr.bio;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
/**
* @author 勿忘初心
* @since 2023-08-07-23:59
* 同步阻塞IO示例
*/
public class BioServer {
public static void main(String[] args) throws IOException {
// 监听本地8096端口
ServerSocket serverSocket = new ServerSocket(8096);
while(true){
System.out.println("等待客户端连接...");
// 接收客户端请求,阻塞
Socket clientSocket = serverSocket.accept();
System.out.println("监听到客户端连接");
// 使用多线程来解决系统处理资源的能力
new Thread(new Runnable(){
@Override
public void run() {
try {
// NIO处理请求资源
handler(clientSocket);
}catch (Exception e){
e.printStackTrace();
}
}
}
).start();
}
}
public static void handler(Socket clientSocket) throws IOException {
byte[] bytes = new byte[1024];
System.out.println("准备读取");
int read = clientSocket.getInputStream().read(bytes);
System.out.println("读取完毕");
if(read != -1){
System.out.println("接收到来自客户端的数据 "+new String(bytes,0,read,StandardCharsets.UTF_8));
}
}
}
使用多线程后,可以看到服务端能够同时监听多个客户端的请求。
总结
使用改进后的代码通过测试可以发现服务端具备了同时处理多个客户端的能力,但同时仍然存在一些问题:
1.当请求非常大时,服务端将因为线程太多无法处理而导致崩溃。
2.即使使用了线程池,当线程池占满后服务将于单线程处理无异。
3.客户端只连接不发送数据,将一直占用服务端资源造成资源浪费。
NIO
概念
同步非阻塞IO,特点为当客户端发起请求后,在此期间客户端可以做其它的操作,但需要主动轮询服务端的处理结果,而且服务端也无需专注于当前请求的处理,也可以处理其他请求。
示例
package com.mlyzr.nio;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
/**
* @author 勿忘初心
* @since 2023-08-08-15:28
* 同步非阻塞IO示例代码
*/
public class NioServer {
public static void main(String[] args) throws IOException {
// 保存客户端的Channel集合
List<SocketChannel> channelList = new ArrayList<>();
// 创建NIO的ServerSocketChannel,与BIO的ServerSocket类似
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.socket().bind(new InetSocketAddress(8096));
// 设置channel为非阻塞
serverChannel.configureBlocking(false);
System.out.println("等待客户端连接");
while(true){
// 非阻塞模式的accept方法不会阻塞
SocketChannel socketChannel = serverChannel.accept();
// 客户端是否连接
if(socketChannel != null){
System.out.println("监听到客户端连接");
socketChannel.configureBlocking(false);
// 将客户端连接保存到list中
channelList.add(socketChannel);
}
Iterator<SocketChannel> iterator = channelList.iterator();
while(iterator.hasNext()){
SocketChannel sc = iterator.next();
ByteBuffer byteBuffer = ByteBuffer.allocate(128);
int read = sc.read(byteBuffer);
// 若存在数据,则将数据打印出来
if(read > 0){
System.out.println("接收到客户端数据"+new String(byteBuffer.array(),0,read,StandardCharsets.UTF_8));
}
// 无数据则说明客户端已断开
if(read == -1){
iterator.remove();
System.out.println("客户端已断开连接");
}
}
}
}
}
客户端
运行实例代码后,可以同时打开多个客户端,客户端连接将被加入集合遍历处理,无需等待,服务端此时可以处理多个请求,此时服务端使用的是单线程。
优化
虽然上述代码解决了BIO中遗留的阻塞问题,但多个请求连接而不发送数据占用资源的情况仍然存在,如当请求数据量巨大时,需要通过遍历集合的方式寻找存在需要处理数据的客户端时,时间的损耗仍然非常大。因此使用多路复用的方式来解决无效遍历的问题。
package com.mlyzr.nio;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;
import java.util.*;
/**
* @author 勿忘初心
* @since 2023-08-08-15:28
* 同步非阻塞IO示例代码
*/
public class NioServer {
public static void main(String[] args) throws IOException {
// 创建NIO的ServerSocketChannel,与BIO的ServerSocket类似
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.socket().bind(new InetSocketAddress(8096));
// 设置channel为非阻塞
serverChannel.configureBlocking(false);
// 引入多路复用,提高对Channel的处理能力, 即epoll
Selector selector = Selector.open();
// 将ServerSocketChannel注册到selector上,即服务端接收到连接请求时,将连接事件进行注册
SelectionKey selectionKey = serverChannel.register(selector,SelectionKey.OP_ACCEPT);
System.out.println("等待客户端连接");
while(true){
// 判断是否有注册的事件,无事件将阻塞
selector.select();
// 获取所有已经注册的事件实例
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = selectionKeys.iterator();
while(keyIterator.hasNext()){
SelectionKey selectKey = keyIterator.next();
// 若为连接事件,则获取该实例
if(selectKey.isAcceptable()){
ServerSocketChannel serverSocketChannel = (ServerSocketChannel) selectKey.channel();
SocketChannel socketChannel = serverSocketChannel.accept();
// 设置为非阻塞
socketChannel.configureBlocking(false);
// 注册读事件,若需要发送消息给客户端可以注册写事件
SelectionKey key = socketChannel.register(selector,SelectionKey.OP_READ);
System.out.println("监听到客户端连接");
}
if (selectKey.isReadable()){
SocketChannel socketChannel = (SocketChannel) selectKey.channel();
ByteBuffer byteBuffer = ByteBuffer.allocate(128);
int read = socketChannel.read(byteBuffer);
// 若存在数据,则将数据打印出来
if(read > 0){
System.out.println("接收到客户端数据"+new String(byteBuffer.array(),0,read,StandardCharsets.UTF_8));
}
// 无数据则说明客户端已断开
if(read == -1){
System.out.println("客户端已断开连接");
socketChannel.close();
}
}
// 从事件集合中删除本次处理的key,防止重复处理
keyIterator.remove();
}
}
}
}
总结
当使用多路复用后,会监听到客户端的连接事件,并为当前的连接注册读事件,当客户端发送数据时,会被再次监听,此时会进入读操作的事件处理中,将打印接收到的数据,同时每一次的事件使用后将会被移除,防止事件重复,从而解决了无效遍历的问题。