文章首发于:clawhub.club
在分析Tomcat的请求处理过程之前,得先复习一下Java NIO的一些知识:
概念
NIO最大的特点是面向缓冲区,有三大核心:Channel、Buffer、Selector。
Channel
Channel是双向的,既可以读数据,也可以写数据。这里只关注TCP(Server和Client)相关的ServerSocketChannel和SocketChannel。
Buffer
数据总是从通道读取到缓冲区,或者从缓冲区写入通道中。
Selector
用于监听多个通道的事件,比如连接打开、数据到达。
TCP客户端服务端例子
ServerHandler与其实现
import java.io.IOException;
import java.nio.channels.SelectionKey;
/**
* 消息处理
*/
public interface ServerHandler {
/**
* 连接请求
*
* @param selectionKey selectionKey
* @throws IOException IOException
*/
void handleAccept(SelectionKey selectionKey) throws IOException;
/**
* 读请求
*
* @param selectionKey selectionKey
* @return String
* @throws IOException IOException
*/
String handleRead(SelectionKey selectionKey) throws IOException;
}
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;
/**
* The type Server handler.
*/
public class ServerHandlerImpl implements ServerHandler {
/**
* The Buffer size.
*/
private int bufferSize = 1024;
@Override
public void handleAccept(SelectionKey selectionKey) throws IOException {
//获取channel
SocketChannel socketChannel = ((ServerSocketChannel) selectionKey.channel()).accept();
//非阻塞
socketChannel.configureBlocking(false);
//注册selector,操作—用于读取操作的操作集位。
socketChannel.register(selectionKey.selector(), SelectionKey.OP_READ, ByteBuffer.allocate(bufferSize));
System.out.println("请求连接......");
}
@Override
public String handleRead(SelectionKey selectionKey) throws IOException {
//获取channel
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
//检索当前附件
ByteBuffer buffer = (ByteBuffer) selectionKey.attachment();
String receivedStr = "";
if (socketChannel.read(buffer) == -1) {
//无数据
socketChannel.shutdownOutput();
socketChannel.shutdownInput();
socketChannel.close();
System.out.println("连接断开......");
} else {
//翻转这个缓冲区。
buffer.flip();
//按照编码读取缓冲区中数据
receivedStr = StandardCharsets.UTF_8.newDecoder().decode(buffer).toString();
//Clears this buffer.
buffer.clear();
//返回数据给客户端
buffer = buffer.put(("服务端接收到的消息为: " + receivedStr).getBytes(StandardCharsets.UTF_8));
//翻转这个缓冲区。
buffer.flip();
//缓冲区数据写入到通道
socketChannel.write(buffer);
//注册selector 继续读取数据
socketChannel.register(selectionKey.selector(), SelectionKey.OP_READ, ByteBuffer.allocate(bufferSize));
}
return receivedStr;
}
}
NioSocketServer服务端及启动
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.util.Iterator;
/**
* 服务端
*/
public class NioSocketServer {
/**
* The Open.
*/
private volatile boolean open = true;
/**
* Is open boolean.
*
* @return the boolean
*/
public boolean isOpen() {
return open;
}
/**
* Sets open.
*
* @param open the open
*/
public void setOpen(boolean open) {
this.open = open;
}
/**
* Start.
*/
public void start() {
//打开服务器套接字通道。
try (ServerSocketChannel serverSocketChannel = ServerSocketChannel.open()) {
//将ServerSocket绑定到8080端口
serverSocketChannel.socket().bind(new InetSocketAddress(8080));
//设置为非阻塞模式
serverSocketChannel.configureBlocking(false);
//打开一个选择器。
Selector selector = Selector.open();
//为serverChannel注册selector,套接字接受操作的操作集位。
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("服务端启动......");
//创建消息处理器
ServerHandler handler = new ServerHandlerImpl();
//只要服务是打开的,就一直循环
while (isOpen()) {
//选择一组键,其对应的通道已准备好进行I/O操作。
selector.select();
System.out.println("开始处理请求.......");
//获取selectionKeys并处理
Iterator keyIterator = selector.selectedKeys().iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
try {
//连接请求
if (key.isAcceptable()) {
handler.handleAccept(key);
}
//读请求
if (key.isReadable()) {
System.out.println(handler.handleRead(key));
}
} catch (IOException e) {
e.printStackTrace();
}
//处理完后移除当前使用的key
keyIterator.remove();
}
System.out.println("完成请求处理。");
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* The type Server main.
*/
public class ServerMain {
/**
* The entry point of application.
*
* @param args the input arguments
*/
public static void main(String[] args) {
NioSocketServer server = new NioSocketServer();
//1分钟后关闭服务
new Thread(() -> {
try {
Thread.sleep(60 * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//服务关闭
server.setOpen(false);
}
}).start();
//开启服务
server.start();
}
}
NioSocketClient客户端及启动
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;
/**
* 客户端
*/
public class NioSocketClient {
/**
* Start.
*/
public void start() {
//Opens a socket channel.
try (SocketChannel socketChannel = SocketChannel.open()) {
SocketAddress socketAddress = new InetSocketAddress("localhost", 8080);
//连接服务端socket
socketChannel.connect(socketAddress);
int sendCount = 0;
ByteBuffer buffer = ByteBuffer.allocate(1024);
while (sendCount < 5) {
buffer.clear();
//向服务端发送消息
buffer.put(("当前时间: " + System.currentTimeMillis()).getBytes());
buffer.flip();
socketChannel.write(buffer);
buffer.clear();
//从服务端读取消息
int readLength = socketChannel.read(buffer);
buffer.flip();
byte[] bytes = new byte[readLength];
buffer.get(bytes);
System.out.println(new String(bytes, StandardCharsets.UTF_8));
buffer.clear();
sendCount++;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* The type Client main.
*/
public class ClientMain {
/**
* The entry point of application.
*
* @param args the input arguments
*/
public static void main(String[] args) {
new NioSocketClient().start();
}
}
结果:
服务端启动......
开始处理请求.......
请求连接......
完成请求处理。
开始处理请求.......
当前时间: 1567409176760
完成请求处理。
开始处理请求.......
当前时间: 1567409177761
完成请求处理。
开始处理请求.......
当前时间: 1567409178762
完成请求处理。
开始处理请求.......
当前时间: 1567409179763
完成请求处理。
开始处理请求.......
当前时间: 1567409180763
完成请求处理。
开始处理请求.......
连接断开......
完成请求处理。
简单的了解NIO 服务端的原理后,再来看Tomcat的请求处理过程,应该会更清晰一些,从前几篇文章可知,在Tomcat启动时就就有如下两个步骤:
- 打开服务器套接字通道ServerSocketChannel.open()
- 将ServerSocket绑定到指定端口serverSocketChannel.socket().bind(new InetSocketAddress(8888))
这体现在NioEndpoint类中的initServerSocket()方法中:
/**
* Separated out to make it easier for folks that extend NioEndpoint to implement custom [server]sockets
*/
protected void initServerSocket() throws Exception {
if (!getUseInheritedChannel()) {
serverSock = ServerSocketChannel.open();
socketProperties.setProperties(serverSock.socket());
InetSocketAddress addr = (getAddress() != null ? new InetSocketAddress(getAddress(), getPort()) : new InetSocketAddress(getPort()));
serverSock.socket().bind(addr, getAcceptCount());
} else {
// Retrieve the channel provided by the OS
Channel ic = System.inheritedChannel();
if (ic instanceof ServerSocketChannel) {
serverSock = (ServerSocketChannel) ic;
}
if (serverSock == null) {
throw new IllegalArgumentException(sm.getString("endpoint.init.bind.inherited"));
}
}
//mimic APR behavior
serverSock.configureBlocking(true);
}
我们又知道Poller维护着Selector,在NioEndpoint的内部类Poller中可以看到selector的一些操作。
我的例子中是没有用到serverSock.accept()方法来监听客户请求的,Tomcat中用Acceptor来监听请求,获取SocketChannel,并封装成NioChannel注册到Poller中。
下面分几篇文档来分析接收到请求,封装的请求的过程。