The author also established his own official account. , I usually share some programming knowledge , Welcome everyone to support ~
Scan code or wechat search north wind IT The way Focus on
Official account address : Handwritten one based on NIO The mini Tomcat
I saw an article a long time ago Write a mini version Tomcat, I think it’s still very interesting , So he also followed his hand again , Sure enough, we got what we wanted hello world, But this is based on BIO Of , I happened to read a book about concurrent programming recently , So I tried to change the big guy’s code , So there’s this based on NIO Mini Tomcat.
Source code updated to my Github:https://github.com/tzfun/MyTomcat
BIO and NIO
BIO It’s synchronous blocking IO, In the actual scenario, most of the time is spent on IO Waiting , Compare the consumption of resources , So this kind of IO The way is gradually replaced , The specific introduction will not be repeated here . In its place NIO(New IO), It’s a synchronous, non blocking IO, It passes through Selector Spin or callback to process ready data Channel,Channel Channel , amount to BIO Flow in , It doesn’t exist. It takes a lot of time to IO wait for , This greatly improves throughput .Tomcat The old version is also based on BIO Of , All subsequent updates have been replaced with NIO.
Project structure
The structure of the project is almost the same as that of the original author , It’s just that the code implementation is different , The specific structure is shown in the figure below :
Request and Response
Tomcat Mainly Http service , So the deal is Http agreement , that Http The header information of the 、 Request header 、 Blank line 、 Composition of request data , You just need to split the data to process http request , I’ve just dealt with it briefly , See notes for other details .
Request
package mytomcat;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.SocketChannel;
import java.util.HashMap;
/**
* @author beifengtz
* <a href='http://www.beifengtz.com'>www.beifengtz.com</a>
* <p>location: mytomcat.javase_learning</p>
* Created in 14:45 2019/4/21
*/
public class MyRequest {
private String url;
private String method;
private HashMap<String,String> param = new HashMap<>();
public MyRequest(SelectionKey selectionKey) throws IOException{
// Access from contracts
SocketChannel channel = (SocketChannel) selectionKey.channel();
String httpRequest = "";
ByteBuffer bb = ByteBuffer.allocate(16*1024); // Get memory from heap memory
int length = 0; // Read byte Length of array
length = channel.read(bb); // Read data from the channel to ByteBuffer In the container
if (length < 0){
selectionKey.cancel(); // Cancel the contract
}else {
httpRequest = new String(bb.array()).trim(); // take ByteBuffer To String
String httpHead = httpRequest.split("\n")[0]; // Get request header
url = httpHead.split("\\s")[1].split("\\?")[0]; // Get the request path
String path = httpHead.split("\\s")[1]; // Request full path , contain get Parameter data of
method = httpHead.split("\\s")[0];
// Here's the split get Requested parameter data
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 +
'}';
}
}
Response
package mytomcat;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;
/**
* @author beifengtz
* <a href='http://www.beifengtz.com'>www.beifengtz.com</a>
* <p>location: mytomcat.javase_learning</p>
* Created in 14:49 2019/4/21
*/
public class MyResponse {
private SelectionKey selectionKey;
public MyResponse(SelectionKey selectionKey){
this.selectionKey = selectionKey;
}
public void write(String content) throws IOException{
// Splicing corresponding packets
StringBuffer httpResponse = new StringBuffer();
httpResponse.append("HTTP/1.1 200 OK\n")
.append("Content-type:text/html\n")
.append("\r\n")
.append("<html><body>")
.append(content)
.append("</body></html>");
// Convert to ByteBuffer
ByteBuffer bb = ByteBuffer.wrap(httpResponse.toString().getBytes(StandardCharsets.UTF_8));
SocketChannel channel = (SocketChannel) selectionKey.channel(); // Access from contracts
long len = channel.write(bb); // Write data to the channel
if (len == -1){
selectionKey.cancel();
}
bb.flip();
channel.close();
selectionKey.cancel();
}
}
Servlet and Mapping Mapping class and its configuration class
stay Java web In development, you will encounter Servlet and Mapping Configuration of , These are essential elements ,Servlet Responsible for defining methods for handling requests and responses , It’s an abstract class .
Servlet
package mytomcat;
/**
* @author beifengtz
* <a href='http://www.beifengtz.com'>www.beifengtz.com</a>
* <p>location: mytomcat.javase_learning</p>
* Created in 14:53 2019/4/21
*/
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);
}
}
}
Mapping Mapping is responsible for distributing some request paths to their respective processing classes for processing , Then you need a configuration class , Here are ServletMapping Class definition and Config class .
ServletMapping
package mytomcat;
/**
* @author beifengtz
* <a href='http://www.beifengtz.com'>www.beifengtz.com</a>
* <p>location: mytomcat.javase_learning</p>
* Created in 14:59 2019/4/21
*/
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 mytomcat;
import java.util.ArrayList;
import java.util.List;
/**
* @author beifengtz
* <a href='http://www.beifengtz.com'>www.beifengtz.com</a>
* <p>location: mytomcat.javase_learning</p>
* Created in 15:01 2019/4/21
*/
public class ServletMappingConfig {
public static List<ServletMapping> servletMappingList = new ArrayList<>();
static {
servletMappingList.add(new ServletMapping("helloWorld","/world","mytomcat.HelloWorldServlet"));
}
}
Of course, the general configuration is through xml File to configure .
Here’s how to deal with /world Request processing class
package mytomcat;
import java.io.IOException;
/**
* @author beifengtz
* <a href='http://www.beifengtz.com'>www.beifengtz.com</a>
* <p>location: mytomcat.javase_learning</p>
* Created in 14:57 2019/4/21
*/
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();
}
}
}
Tomcat Core startup class
Tomcat The startup class of is its core , It contains initialization Mapping、 Listening port 、 Processing requests and responses, etc , The main difference between me and the original author is in this part , use NIO To replace the BIO Receiving data mode of , Using thread pool to process data .
MyTomcat
package mytomcat;
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.*;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* @author beifengtz
* <a href='http://www.beifengtz.com'>www.beifengtz.com</a>
* <p>location: mytomcat.javase_learning</p>
* Created in 15:03 2019/4/21
*/
public class MyTomcat {
private int port = 8080;
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 {
// Initialize mapping relationship
initServletMapping();
// start-up Selector
selector = SelectorProvider.provider().openSelector();
// start-up Channel
ServerSocketChannel ssc = ServerSocketChannel.open();
// Configure non blocking selection
ssc.configureBlocking(false);
// Listening port
InetSocketAddress isa = new InetSocketAddress(port);
ssc.socket().bind(isa);
// take Channel Bound to the Selector On , And select the preparation mode as Accept, It may fail here , It can be opened again later
SelectionKey acceptKey = ssc.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("MyTomcat is started...");
ConcurrentLinkedQueue<MyRequest> requestList = new ConcurrentLinkedQueue<>();
ConcurrentLinkedQueue<MyResponse> responseList = new ConcurrentLinkedQueue<>();
while (true) {
selector.select(); // wait for Channel Prepare the data
Set readyKeys = selector.selectedKeys();
Iterator i = readyKeys.iterator();
while (i.hasNext()) {
SelectionKey sk = (SelectionKey) i.next();
i.remove(); // Remove from collection , Prevent duplicate processing
if (sk.isAcceptable()) {
// If the receiving state of the key is not turned on properly , Try opening again
doAccept(sk);
} else if (sk.isValid() && sk.isReadable()) {
// Can be read
requestList.add(getRequest(sk));
// Switch readiness
sk.interestOps(SelectionKey.OP_WRITE);
} else if (sk.isValid() && sk.isWritable()) {
// Can write
responseList.add(getResponse(sk));
// Switch readiness
sk.interestOps(SelectionKey.OP_READ);
}
// Wait for a pair of requests and responses to be ready
if (!requestList.isEmpty() && !responseList.isEmpty()) {
dispatch(requestList.poll(), responseList.poll());
}
}
}
}
/**
* If the receiving mode is not turned on normally
* Try to turn on receive mode
* @param selectionKey
*/
private void doAccept(SelectionKey selectionKey) {
ServerSocketChannel server = (ServerSocketChannel) selectionKey.channel();
SocketChannel clientChannel;
try {
clientChannel = server.accept();
clientChannel.configureBlocking(false);
SelectionKey clientKey = clientChannel.register(selector, SelectionKey.OP_READ);
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* Get the request from the channel and wrap it
*
* @param selectionKey
* @return
* @throws IOException
*/
private MyRequest getRequest(SelectionKey selectionKey) throws IOException {
return new MyRequest(selectionKey); // packing request
}
/**
* Get the response from the channel and wrap it
*
* @param selectionKey
* @return
*/
private MyResponse getResponse(SelectionKey selectionKey) {
return new MyResponse(selectionKey); // packing response
}
/**
* initialization Servlet Mapping object for
*/
private void initServletMapping() {
for (ServletMapping servletMapping : ServletMappingConfig.servletMappingList) {
urlServletMap.put(servletMapping.getUrl(), servletMapping.getClazz());
}
}
/**
* Request scheduling
*
* @param myRequest
* @param myResponse
*/
private void dispatch(MyRequest myRequest, MyResponse myResponse) {
if (myRequest == null) return;
if (myResponse == null) return;
String clazz = urlServletMap.get(myRequest.getUrl());
try {
if (clazz == null) {
myResponse.write("404");
return;
}
es.execute(new Runnable() {
@Override
public void run() {
try {
Class<MyServlet> myServletClass = (Class<MyServlet>) Class.forName(clazz);
MyServlet myServlet = myServletClass.newInstance();
myServlet.service(myRequest, myResponse);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
}
}
});
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws IOException {
new MyTomcat().start();
}
}
test
When we type http://localhost:8080/world?data=yes when , The success achieved the expected results
Console output :