1、Java NIO 基本介绍
- Java NIO 全称 java non-blocking IO,是指 JDK 提供的新 API。从 JDK1.4 开始,Java 提供了一系列改进的
输入/输出的新特性,被统称为 NIO(即 New IO),是同步非阻塞的 - NIO 相关类都被放在 java.nio 包及子包下,并且对原 java.io 包中的很多类进行改写。
- NIO 有三大核心部分:Channel( 通道) ,Buffer( 缓冲区), Selector(选择器)。
- NIO 是 是 区 面向缓冲区 ,向 或者面向 块 块 编程的。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动,这就增加了处理过程中的灵活性,使用它可以提供非阻塞式的高伸缩性网络。
- Java NIO 的非阻塞模式,使一个线程从某通道发送请求或者读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取,而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。 非阻塞写也是如此,一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。
- HTTP2.0 使用了多路复用的技术,做到同一个连接并发处理多个请求,而且并发请求的数量比 HTTP1.1 大了好几个数量级。
初学者最困难的地方是 Channel通道 和 Selector选择器 的关系,以及确定当前正在使用的通道是 Selector下管理的哪条通道
//得到当前通道 ip:端口号 socketChannel.getRemoteAddress()
//得到当前进程的 ip:端口号 socketChannel.getLocalAddress()
Selector 、 Channel 和 Buffer 的关系图(简单版)
2、NIO基本实现服务端与客户端消息接收发送
图示
服务端 NioServer
package study.tangxz.nio;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
/**
* @Info:
* @Author: 唐小尊
* @Date: 2020/10/29 04:27
*/
public class NioServer {
private ServerSocketChannel ssc;
private SocketChannel sc;
private Selector selector;
private String host = "127.0.0.1";
private int port = 9999;
NioServer() {
try {
ssc = ServerSocketChannel.open();
//设置为非阻塞
ssc.configureBlocking(false);
//给当前服务器主线程创建一个SocketChannel绑定ip地址和端口号
ssc.socket().bind(new InetSocketAddress(host, port));
//得到一个选择器
selector = Selector.open();
//把服务SocketChannel给注册进selector
ssc.register(selector, SelectionKey.OP_ACCEPT);
} catch (IOException e) {
e.printStackTrace();
}
}
//启动服务端方法
public void start() {
System.out.println("监听线程: " + Thread.currentThread().getName());
while (true) {
try {
//监听所有的连接请求,如果连接成功,则通过下面这条代码。
//如果没有任何的请求连接,则在这里等待。
int select = selector.select();
//得到当前key的迭代器
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
if (select > 0) {
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
//每次迭代之后,移除当前迭代内容,防止重复迭代
iterator.remove();
//如果该key为连接监听事件,则进入下面的方法
if (key.isAcceptable()) {
//接受一个连接,返回代表这个连接的通道对象
//用于监听服务器的读事件,客户端的写事件
SocketChannel socketChannel = ssc.accept();
//设置为非阻塞
socketChannel.configureBlocking(false);
//放入selector选择器中,并让选择器监听该通道读事件
socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024 * 16));
System.out.println(socketChannel.getRemoteAddress() + " 服务器连接成功!注册读事件!");
}
//如果该key为监听到的读事件
if (key.isReadable()) {
//服务端就去读,读完后,发送给所有非发送者的其他客户端
read(key);
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
//SelectionKey相当于是包装了的Channel,
//其可以得到该SelectionKey中对应的Channel和其通道中的数据
public void read(SelectionKey key) {
SocketChannel socketChannel = null;
try {
//得到当前发送消息者SelectionKey对应的Channel
socketChannel = (SocketChannel) key.channel();
//创建一个字节Buffer缓冲区,用来获取通道中的数据
ByteBuffer buffer = ByteBuffer.allocate(1024 * 16);
//把数据写入buffer,并返回数据的长度
int read = socketChannel.read(buffer);
if (read > 0) {//如果长度大于0,说明有数据
System.out.println("【接收到客户端的消息】" + new String(buffer.array(), "UTF-8"));
//当服务端获取到正确的数据时,就调用写方法,把数据返回给除了服务端channel之外的所有channel,细节看方法内部代码
write(socketChannel,new String(buffer.array()));
}
} catch (IOException e) {
try {
//如果连接断开,就会触发异常
System.out.println(socketChannel.getRemoteAddress() + " 离线了..");
//取消注册
key.cancel();
//关闭通道
socketChannel.close();
} catch (IOException e2) {
e2.printStackTrace();
}
e.printStackTrace();
}
}
//服务端发送消息方法
public void write(SocketChannel socketChannel, String msg) {
System.out.println("服务器转发数据给客户端线程: " + Thread.currentThread().getName());
//该 selector 绑定了服务端ServerSocketChannel
//连接该 服务端地址 的所有channel通道,都在这个selector中
//遍历 selector选择器中的所有 SelectionKey
for (SelectionKey key : selector.keys()) {
//得到其中的Channel
Channel channel = key.channel();
//转发不转发给服务端
//channel instanceof SocketChannel 过滤掉 ServerSocketChannel
//channel != socketChannel 过滤掉发送者本身
if (channel instanceof SocketChannel && channel != socketChannel) {
SocketChannel nowChannel = (SocketChannel) channel;
try {
//当前通道 socketChannel.getRemoteAddress()
System.out.println("now:"+socketChannel.getLocalAddress());
nowChannel.write(ByteBuffer.wrap(msg.getBytes()));
} catch (IOException e) {
e.printStackTrace();
}
} else if (channel instanceof SocketChannel) {
String content = "【服务端回复】你好客户端,我已收到你发的消息";
try {
SocketChannel nowChannel = (SocketChannel) channel;
nowChannel.write(ByteBuffer.wrap(content.getBytes()));
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
new NioServer().start();
}
}
客户端 NioClient
package study.tangxz.nio;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Scanner;
/**
* @Info:
* @Author: 唐小尊
* @Date: 2020/10/29 04:27
*/
public class NioClient {
private final String host = "127.0.0.1";
private final int port = 9999;
private SocketChannel socketChannel;
private Selector selector;
private String username;
NioClient(){
try {
//打开一个通道,通道的另一头是这个服务端
//服务端的Selector中配置了服务端的ServerSocketChannel,与该ServerSocketChannel有关联的所有SocketChannel通道发送数据,都会默认把自己(当前通道)给放入Selector中。
socketChannel = SocketChannel.open(new InetSocketAddress(host,port));
//设置通道为非阻塞通道
socketChannel.configureBlocking(false);
//打开一个选择器
selector = Selector.open();
//这个selector选择器监听该通道的读方法,这个通道为接收服务端消息的通道
socketChannel.register(selector, SelectionKey.OP_READ);
//得到username,ip地址:端口号
username = socketChannel.getLocalAddress().toString().substring(1);
System.out.println(username + " is ok...");
} catch (IOException e) {
e.printStackTrace();
}
}
public void read(){
try {
//等待监听的事件发生,register(selector, SelectionKey.OP_READ);
selector.select();
//得到监听到的事件通道
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()){
SelectionKey key = iterator.next();
iterator.remove();
//这个通道为服务端发送数据的通道
SocketChannel channel = (SocketChannel) key.channel();
System.out.println("本地地址:"+channel.getLocalAddress());
ByteBuffer buffer = ByteBuffer.allocate(1024*16);
int read = channel.read(buffer);
if (read>0){
System.out.println(new String(buffer.array()));
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
public void write(String msg){
msg = username + " 说:" + msg;
try {
//这个socketChannel就是当前通道,write直接发送给服务端的ServerSocketChannel,ServerSocketChannel收到通道后,会默认直接连接在服务端的selector中。
socketChannel.write(ByteBuffer.wrap(msg.getBytes()));
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
//启动我们客户端
NioClient nioClient = new NioClient();
new Thread(()->{
while (true){
nioClient.read();
try{
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
Scanner sc = new Scanner(System.in);
while (sc.hasNextLine()){
String msg = sc.nextLine();
nioClient.write(msg);
}
}
}
3、NIO实现Tomcat
MyServlet
package study.tangxz.nio2;
/**
* @Info:
* @Author: 唐小尊
* @Date: 2020/10/29 15:47
*/
public abstract class MyServlet {
public abstract void doGet(MyRequest myRequest,MyResponse myResponse);
public abstract void doPost(MyRequest myRequest,MyResponse myResponse);
public void service(MyRequest myRequest,MyResponse myResponse){
if (myRequest.getMethod().equalsIgnoreCase("POST")){
doPost(myRequest,myResponse);
}else if (myRequest.getMethod().equalsIgnoreCase("GET")){
doGet(myRequest,myResponse);
}
}
}
HelloWorldServlet
package study.tangxz.nio2;
import java.io.IOException;
/**
* @Info:
* @Author: 唐小尊
* @Date: 2020/10/29 15:47
*/
public class HelloWorldServlet extends MyServlet{
@Override
public void doGet(MyRequest myRequest,MyResponse myResponse) {
try {
myResponse.write("get hello world");
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void doPost(MyRequest myRequest, MyResponse myResponse) {
try {
myResponse.write("post hello world");
} catch (IOException e) {
e.printStackTrace();
}
}
}
MyRequest
package study.tangxz.nio2;
import com.sun.xml.internal.stream.events.StartElementEvent;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.SocketChannel;
import java.util.HashMap;
/**
* @Info:
* @Author: 唐小尊
* @Date: 2020/10/29 15:47
*/
public class MyRequest {
private String url;
private String method;
private HashMap<String,String> param = new HashMap<>();
public MyRequest(SelectionKey selectionKey) throws IOException{
//从契约获取通道
SocketChannel channel = (SocketChannel) selectionKey.channel();
String httpRequest = "";
ByteBuffer bb = ByteBuffer.allocate(16*1024);
int length = 0;
length = channel.read(bb);
if (length<0){
selectionKey.channel();
}else {
httpRequest = new String(bb.array()).trim();
String httpHead = httpRequest.split("\n")[0];
url = httpHead.split("\\s")[1].split("\\?")[0];
String path = httpHead.split("\\s")[1];
method = httpHead.split("\\s")[0];
//以下是拆分get请求的参数数据
String[] params = path.indexOf("?")>0?path.split("\\?")[1].split("\\&"):null;
if (params!=null){
try {
for (String tmp:params){
param.put(tmp.split("\\=")[0],tmp.split("\\=")[1]);
}
}catch (NullPointerException e){
e.printStackTrace();
}
}
System.out.println(this);
}
bb.flip();
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public String getMethod() {
return method;
}
public void setMethod(String method) {
this.method = method;
}
@Override
public String toString() {
return "MyRequest{" +
"url='" + url + '\'' +
", method='" + method + '\'' +
", param=" + param +
'}';
}
}
MyResponse
package study.tangxz.nio2;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.SocketChannel;
/**
* @Info:
* @Author: 唐小尊
* @Date: 2020/10/29 15:47
*/
public class MyResponse {
private SelectionKey selectionKey;
public MyResponse(SelectionKey selectionKey){
this.selectionKey = selectionKey;
}
public void write(String content) throws IOException{
//拼接相应数据包
StringBuffer httpResponse = new StringBuffer();
httpResponse.append("HTTP/1.1 200 OK\n")
.append("\r\n")
.append("<html><body>")
.append(content)
.append("</body></html>");
//转换为ByteBuffer
ByteBuffer bb = ByteBuffer.wrap(httpResponse.toString().getBytes());
SocketChannel channel = (SocketChannel) selectionKey.channel();
long len = channel.write(bb);
if (len==-1){
selectionKey.cancel();
}
channel.close();
selectionKey.cancel();
}
}
ServletMapping
package study.tangxz.nio2;
/**
* @Info:
* @Author: 唐小尊
* @Date: 2020/10/29 15:47
*/
public class ServletMapping {
private String servletName;
private String url;
private String clazz;
public ServletMapping(String servletName, String url, String clazz) {
this.servletName = servletName;
this.url = url;
this.clazz = clazz;
}
public String getServletName() {
return servletName;
}
public void setServletName(String servletName) {
this.servletName = servletName;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public String getClazz() {
return clazz;
}
public void setClazz(String clazz) {
this.clazz = clazz;
}
}
ServletMappingConfig
package study.tangxz.nio2;
import java.util.ArrayList;
import java.util.List;
/**
* @Info:
* @Author: 唐小尊
* @Date: 2020/10/29 15:47
*/
public class ServletMappingConfig {
public static List<ServletMapping> servletMappingList = new ArrayList<>();
static {
servletMappingList.add(new ServletMapping("Hello World","/world","study.tangxz.nio2.HelloWorldServlet"));
}
}
MyTomcat
package study.tangxz.nio2;
import study.tangxz.nio.NioTomcat;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.channels.spi.SelectorProvider;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* @Info:
* @Author: 唐小尊
* @Date: 2020/10/29 15:47
*/
public class MyTomcat {
private int port = 9999;
private Map<String, String> urlServletMap = new HashMap<>();
private Selector selector;
private ExecutorService es = Executors.newCachedThreadPool();
public MyTomcat() {
}
public MyTomcat(int port) {
this.port = port;
}
public void start() throws IOException {
initServletMapping();
//SelectorProvider 此类中的所有方法均可安全地被多个并发线程使用
selector = SelectorProvider.provider().openSelector();
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.configureBlocking(false);
InetSocketAddress isa = new InetSocketAddress(9999);
ssc.socket().bind(isa);
ssc.register(selector, SelectionKey.OP_ACCEPT);
ConcurrentLinkedQueue<MyRequest> requestList = new ConcurrentLinkedQueue<>();
ConcurrentLinkedQueue<MyResponse> responseList = new ConcurrentLinkedQueue<>();
while (true) {
selector.select();
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey sk = iterator.next();
iterator.remove();
if (sk.isAcceptable()) {
doAccept(sk);
//isValid() 告知此密钥是否有效。
} else if (sk.isValid() && sk.isReadable()) {
requestList.add(getRequest(sk));
sk.interestOps(SelectionKey.OP_WRITE);
} else if (sk.isValid() && sk.isWritable()) {
responseList.add(getResponse(sk));
sk.interestOps(SelectionKey.OP_READ);
}
if (!requestList.isEmpty() && !responseList.isEmpty()) {
dispatch(requestList.poll(), responseList.poll());
}
}
}
}
private void doAccept(SelectionKey sk) throws IOException {
ServerSocketChannel server = (ServerSocketChannel) sk.channel();
SocketChannel clientChannel;
try {
//接受与ServerSocketChannel建立的连接。创建一条通道,提供给该客户端读写数据。
clientChannel=server.accept();
clientChannel.configureBlocking(false);
clientChannel.register(selector,SelectionKey.OP_READ);
System.out.println("【" + clientChannel.getRemoteAddress() + "】 上线了");
}catch (IOException e){
e.printStackTrace();
}
}
private MyRequest getRequest(SelectionKey sk) throws IOException {
return new MyRequest(sk);
}
private MyResponse getResponse(SelectionKey sk) {
return new MyResponse(sk);
}
private void dispatch(MyRequest myRequest, MyResponse myResponse) {
if (myRequest == null||myResponse==null){
return;
}
String clazz = urlServletMap.get(myRequest.getUrl());
try {
if (clazz==null){
myResponse.write("404");
return;
}
es.execute(()->{
try {
Class<MyServlet> myServletClass = (Class<MyServlet>) Class.forName(clazz);
MyServlet myServlet = myServletClass.newInstance();
myServlet.service(myRequest,myResponse);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
}
});
} catch (IOException e) {
e.printStackTrace();
}
}
private void initServletMapping() {
for (ServletMapping servletMapping : ServletMappingConfig.servletMappingList) {
urlServletMap.put(servletMapping.getUrl(), servletMapping.getClazz());
}
}
public static void main(String[] args) throws IOException {
new MyTomcat(9999).start();
}
}