之前我写的Tomcat,都是通过获取socket对象,然后通过socket对象的输入输出流完成的.引入Netty之后,由于需要通过ByteBuf进行相关的操作,本次有较大的改动,将core包中的核心类都进行了更改.
GitHub地址:https://github.com/shenshaoming/tomcat
首先是主程序HttpServer,更改为Netty后代码有所减少:
package com.tomcat.core;
import com.tomcat.annotations.Servlet;
import com.tomcat.baseservlet.AbstractServlet;
import com.tomcat.exceptions.RequestMappingException;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.ServerSocketChannel;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import java.io.File;
import java.io.IOException;
import java.net.*;
import java.util.*;
import java.util.concurrent.*;
/**
* Netty版本的tomcat
* 监听请求,调用request和response对请求作出反应
* @author 申劭明
* @date 2019/9/16 17:21
* @version 5.3
*/
public class HttpServer {
/**
* 线程池中核心线程数的最大值(不是操作系统的最大值)
*/
private int corePoolSize = 10;
/**
* 最大队列空间
*/
private int maximumPoolSize = 50;
/**
* 空闲线程的最大存活时间
*/
private long keepAliveTime = 100L;
/**
* keepAliveTime的时间单位设置
*/
private TimeUnit unit = TimeUnit.SECONDS;
/**
* 监听端口
*/
private static int port = 8080;
/**
* 关闭服务器的请求URI
*/
static final String CLOSE_URI = "/shutdown";
/**
* Key值为Servlet的别名(uri),value为该Servlet对象
* default权限
*/
static HashMap map;
static {
//包名,可以通过application.properties设置
getServlets("com.tomcat.servlet");
}
/**
* @Description : nio监听数据请求
* @author : 申劭明
* @date : 2019/9/17 10:29
*/
public void acceptWait() {
//监听请求
EventLoopGroup listenGroup = new NioEventLoopGroup();
//请求处理
EventLoopGroup workerGroup = new NioEventLoopGroup();
ServerBootstrap bootstrap = new ServerBootstrap();
//绑定监听请求和处理请求的group
bootstrap.group(listenGroup,workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline().addLast(new RequestHandler());
}
});
ChannelFuture future = null;
try {
future = bootstrap.bind(port).sync();
future.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
listenGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
/**
* @param packageName 包名,如com.tomcat.servlet
* @return : void
* @Description : 扫描packageName包下的所有带有@Servlet注解的类文件
* @author : 申劭明
* @date : 2019/9/18 10:36
*/
private static void getServlets(String packageName) {
//class类的集合
Set> classes = new LinkedHashSet<>();
try {
//com.tomcat.servlet,com/tomcat/servelet
String packageDirName = packageName.replace(".", "/");
Enumeration resources = Thread.currentThread().getContextClassLoader().getResources(packageDirName);
while (resources.hasMoreElements()) {
URL url = resources.nextElement();
System.out.println("****************" + url);
System.out.println();
String protocol = url.getProtocol();
if ("file".equals(protocol)) {
String filePath = URLDecoder.decode(url.getFile(), "UTF-8");
findAndAddClassesInPackageByFile(packageName, filePath,true,classes);
}else if("jar".equals(protocol)){
//扫描JAR包
}
}
//遍历class集合
if (map == null){
map = new HashMap<>(classes.size());
}
for (Class> aClass : classes) {
//如果该class有Servlet注解
if (aClass.isAnnotationPresent(Servlet.class)){
try {
String value = aClass.getAnnotation(Servlet.class).value();
//如果已经包含有该key值,则抛出异常
if (map.containsKey(value)){
//当前正在扫描的Servlet
String now = aClass.getName();
//已经存在的Servlet
String old = map.get(value).getClass().getName();
throw new RequestMappingException(now,old);
}else{
Class> superclass = aClass.getSuperclass();
if (AbstractServlet.class != superclass){
System.err.println("带有Servlet注解的类'" + aClass.getName() + "'没有继承自AbstractServlet");
continue;
}
}
//添加至map集合中
map.put(value, (AbstractServlet) aClass.newInstance());
} catch (InstantiationException | IllegalAccessException e) {
e.printStackTrace();
} catch (RequestMappingException e) {
e.printStackTrace();
System.exit(-1);
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* @Description : 对于file类型获取该类型的所有class
*
* @param packageName 包名,com.tomcat.servlet
* @param packagePath 包路径,com/tomcat/servlet
* @param recursive 是否循环遍历子包内的文件
* @param classes class集合
* @author : 申劭明
* @date : 2019/9/18 16:55
*/
public static void findAndAddClassesInPackageByFile(String packageName,
String packagePath, final boolean recursive, Set> classes) {
// 获取此包的目录 建立一个File
File dir = new File(packagePath);
// 如果不存在或者 也不是目录就直接返回
if (!dir.exists() || !dir.isDirectory()) {
return;
}
// 如果存在 就获取包下的所有文件 包括目录
// 自定义过滤规则 如果可以循环(包含子目录) 或则是以.class结尾的文件(编译好的java类文件)
File[] dirfiles = dir.listFiles(file -> (recursive && file.isDirectory())
|| (file.getName().endsWith(".class")));
// 循环所有文件
for (File file : dirfiles) {
// 如果是目录 则继续扫描
if (file.isDirectory()) {
findAndAddClassesInPackageByFile(packageName + "."
+ file.getName(), file.getAbsolutePath(), recursive,
classes);
} else {
// 如果是java类文件 去掉后面的.class 只留下类名
String className = file.getName().substring(0,
file.getName().length() - 6);
try {
// 添加到集合中去
// classes.add(Class.forName(packageName + '.' +
// className));
// 经过回复同学的提醒,这里用forName有一些不好,会触发static方法,没有使用classLoader的load干净
classes.add(Thread.currentThread().getContextClassLoader()
.loadClass(packageName + '.' + className));
} catch (ClassNotFoundException e) {
// log.error("添加用户自定义视图类错误 找不到此类的.class文件");
e.printStackTrace();
}
}
}
}
}
紧接着就是处理请求的Handler,RequestHandler.java,由原先的实现Runnable接口改为继承SimpleChannelInboundHandler.
package com.tomcat.core;
import com.tomcat.baseservlet.AbstractServlet;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
/**
* @Author: 申劭明
* @Date: 2019/9/17 17:45
*/
public class RequestHandler extends SimpleChannelInboundHandler {
public RequestHandler(){
}
@Override
protected void messageReceived(ChannelHandlerContext channelHandlerContext, Object o) {
//接收数据
Request request = new Request((ByteBuf) o);
//写入数据
Response response = new Response(channelHandlerContext);
//将请求对象放入响应对象中
response.setRequest(request);
AbstractServlet abstractServlet = HttpServer.map.get(request.getUri());
try{
if (abstractServlet != null){
abstractServlet.service(request,response);
}else{
//找不到对应的Servlet则直接访问文件
response.sendStaticResource();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
channelHandlerContext.close();
}
}
}
接下来就是对Http请求报文的处理,Request.java,该类中的属性由原先的输入流改为ByteBuf属性:
package com.tomcat.core;
import io.netty.buffer.ByteBuf;
import io.netty.util.CharsetUtil;
/**
* @Author: 申劭明
* @Date: 2019/9/16 17:24
*/
public class Request {
/**
* 通过Netty读取到的数据
*/
private ByteBuf byteBuf;
/**
* 请求路径,如:/test.txt
*/
private String uri;
/**
* 请求类型,GET或POST等
*/
private String method;
public String getMethod() {
return method;
}
public Request(){
}
@Override
public String toString() {
return "Request{" +
"uri='" + uri + '\'' +
", method='" + method + '\'' +
'}';
}
public Request(ByteBuf byteBuf) {
this.byteBuf = byteBuf;
parse();
}
/**
* @Description : 获取http请求中的相关参数
*
* @author : 申劭明
* @date : 2019/9/17 10:26
*/
private void parse() {
String requestStr = byteBuf.toString(CharsetUtil.UTF_8);
//获取请求uri
uri = parseUri(requestStr);
//获取请求类型
method = parseMethod(requestStr);
}
/**
* @Description : 获取请求路径,如http://localhost:8080/test.txt,截取/test.txt(URI)
*
* @param request 请求头
* @return : 请求路径
* @author : 申劭明
* @date : 2019/9/17 9:33
*/
private String parseUri(String request) {
int index1,index2;
//查看socket获取的请求头是否有值
index1 = request.indexOf(' ');
if (index1 != -1){
index2 = request.indexOf(' ', index1 + 1);
if (index2 > index1){
return request.substring(index1 + 1,index2);
}
}
return null;
}
/**
* @Description : 获取请求类型
*
* @param request 请求报文
* @return : GET,POST...
* @author : 申劭明
* @date : 2019/9/18 9:51
*/
private String parseMethod(String request){
int index = request.indexOf(' ');
if (index != -1){
return request.substring(0,index);
}
return null;
}
public String getUri() {
return uri;
}
}
接下来就是Reponse.java,作为响应类,修改主要是讲响应的内容放入ByteBuf中然后由ChannelHandlerContext返回给客户端.
package com.tomcat.core;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import java.io.*;
/**
* @Author: 申劭明
* @Date: 2019/9/16 17:28
*/
public class Response {
/**
* 传输数组的最大字节数
*/
public static final int BUFFER_SIZE = 2048;
/**
* 访问的文件的路径,即tomcat中部署项目的目录
*/
private static final String WEB_ROOT = "D:";
/**
* 响应头信息
*/
private static final String RESPONSE_HEADER = "HTTP/1.1 200 Read File Success\r\n" +
"Content-Type: text/html\r\n" + "\r\n";
/**
* 请求
*/
private Request request;
/**
* 返回页面的数据
*/
private ChannelHandlerContext output;
public Response(ChannelHandlerContext outputStream) {
this.output = outputStream;
}
public void setRequest(Request request) {
this.request = request;
}
/**
* @Description : 向页面返回静态数据
*
* @author : 申劭明
* @date : 2019/9/17 10:27
*/
public void sendStaticResource() throws IOException {
//读取文件时的字节流
byte[] bytes = new byte[BUFFER_SIZE];
FileInputStream fis = null;
if (request == null){
return;
}
File file = new File(WEB_ROOT, request.getUri());
String returnMessage = null;
try {
//如果文件存在,且不是个目录
if (file.exists() && !file.isDirectory()) {
fis = new FileInputStream(file);
//读文件
int ch ;
StringBuilder sb = new StringBuilder(BUFFER_SIZE);
//写文件
while ((ch = fis.read(bytes,0,bytes.length)) != -1) {
sb.append(new String(bytes,0,ch));
}
returnMessage = RESPONSE_HEADER + sb;
}else {
//文件不存在,返回给浏览器响应提示,这里可以拼接HTML任何元素
String retMessage = "" + file.getName() + " file or directory not exists
";
returnMessage = "HTTP/1.1 404 File Not Fount\r\n" +
"Content-Type: text/html\r\n" +
"Content-Length: " + retMessage.length() + "\r\n" +
"\r\n" +
retMessage;
}
ByteBuf buf = Unpooled.buffer();
//用输出流返回数据给页面
if (checkImage(request.getUri())){
buf = buf.writeBytes(returnMessage.replaceAll("text/html","image/jpeg;charset=UTF-8").getBytes());
}else{
buf = buf.writeBytes(returnMessage.getBytes());
}
output.writeAndFlush(buf);
} catch (FileNotFoundException e) {
e.printStackTrace();
} finally {
if (fis != null){
fis.close();
}
if (output != null){
//清空缓存区,调用close方法时会有flush操作
output.close();
}
}
}
/**
* @Description : 判断请求是否为图片类型
*
* @param uri 请求的uri
* @return : 是/否
* @author : 申劭明
* @date : 2019/9/18 14:21
*/
private boolean checkImage(String uri) {
boolean flag = uri.endsWith(".jpg") || uri.endsWith(".png") || uri.endsWith(".jpeg");
return flag;
}
/**
* @Description : 设置返回数据
*
* @param message 返回给页面的数据
* @author : 申劭明
* @date : 2019/9/18 10:19
*/
public void setResponseContent(StringBuilder message){
ByteBuf buf = Unpooled.buffer();
buf = buf.writeBytes(new StringBuilder(RESPONSE_HEADER).append(message).toString().getBytes());
output.writeAndFlush(buf);
}
public void setResponseContent(String message){
setResponseContent(new StringBuilder(message));
}
}
附:手写tomcat我放在了我的github上,之前的bio版本和nio版本也都留下了没有删,可以拿去学习- -当然了,希望对大家有所帮助.另外,aio模型的tomcat我不打算去写了,毕竟Netty本身也没有支持aio,主要是因为linux系统没有对aio的相关支持,而且nio转aio也相对较为简单,所以鄙人就不做研究了.