手写 Tomcat
确切地说,这里要手写的是一个 Web 容器,一个类似于 Tomcat 的容器,用于处理 HTTP请求。该 Web 容器没有实现 JavaEE 的 Servlet 规范,不是一个 Servlet 容器。但其是类比着Tomcat 来写的,这里定义了自己的请求、响应及 Servlet,分别命名为了 NettyRequest,NettyResponse 与 Servnet。
Tomcat底层是用的NIO,不是Netty
1. Tomcat 具体需求
我们这里要定义一个 Tomcat,这个 Web 容器提供给用户后,用户只需要按照使用步骤就可以将其自定义的 Servnet 发布到该 Tomcat 中。我们现在给出用户对于该 Tomcat 的使用步骤:
- 用户只需将自定义的 Servnet 放入到指定的包中。例如,com.abc.webapp 包中。
- 用户在访问时,需要将自定义的 Servnet 的简单类名全小写后的字符串作为该 Servnet的 Name 进行访问。
- 若没有指定的 Servnet,则访问默认的 Servnet。
2. 创建工程 09-tomcat
(1) 创建工程
创建一个普通的 Maven 的 Java 工程:09-tomcat
(2) 导入依赖
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
<dependencies>
<!-- netty-all 依赖 -->
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.36.Final</version>
</dependency>
<!--lombok 依赖-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.6</version>
<scope>provided</scope>
</dependency>
</dependencies>
3. 定义 Servnet 规范
(1) 定义请求接口 NettyRequest
/**
* Servnet规范之请求规范
*/
public interface NettyRequest {
// 获取URI,包含请求参数,即?后的内容
String getUri();
// 获取请求路径,其不包含请求参数
String getPath();
// 获取请求方法(GET、POST等)
String getMethod();
// 获取所有请求参数
Map<String, List<String>> getParameters();
// 获取指定名称的请求参数
List<String> getParameters(String name);
// 获取指定名称的请求参数的第一个值
String getParameter(String name);
}
(2) 定义响应接口 NettyResponse
/**
* ServNet规范之响应规范
*/
public interface NettyResponse {
// 将响应写入到Channel
void write(String content) throws Exception;
}
(3) 定义 Servnet 规范
/**
* 定义Servnet规范
*/
public abstract class Servnet {
public abstract void doGet(NettyRequest request, NettyResponse response) throws Exception;
public abstract void doPost(NettyRequest request, NettyResponse response) throws Exception;
}
4. 定义 Tomcat 服务器
(1) 定义 DefaultNettyRequest 类
注意:
- 我们用netty的io.netty.handler.codec.http.HttpRequest实现
/**
* Tomcat中对Servnet规范的默认实现
*/
public class DefaultNettyRequest implements NettyRequest {
//我们用io.netty.handler.codec.http.HttpRequest实现Servnet规范
private HttpRequest request;
//通过构造传进来
public DefaultNettyRequest(HttpRequest request) {
this.request = request;
}
@Override
public String getUri() {
return request.uri();
}
@Override
public String getPath() {
//io.netty.handler.codec.http.QueryStringDecoder
QueryStringDecoder decoder = new QueryStringDecoder(request.uri());
return decoder.path();
}
@Override
public String getMethod() {
return request.method().name();
}
@Override
public Map<String, List<String>> getParameters() {
QueryStringDecoder decoder = new QueryStringDecoder(request.uri());
return decoder.parameters();
}
@Override
public List<String> getParameters(String name) {
return getParameters().get(name);
}
@Override
public String getParameter(String name) {
List<String> parameters = getParameters(name);
if (parameters == null || parameters.size() == 0) {
return null;
}
return parameters.get(0);
}
}
(2) 定义 DefaultNettyResponse 类
/**
* Tomcat中对Servnet规范的默认实现
*/
public class DefaultNettyResponse implements NettyResponse {
private HttpRequest request;
private ChannelHandlerContext context;
//需要获取到request判断是否是长连接
//需要ChannelHandlerContext将响应写回去
public DefaultNettyResponse(HttpRequest request, ChannelHandlerContext context) {
this.request = request;
this.context = context;
}
@Override
public void write(String content) throws Exception {
// 处理content为空的情况
if (StringUtil.isNullOrEmpty(content)) {
return;
}
// 创建响应对象
FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1,
HttpResponseStatus.OK,
// 根据响应体内容大小为response对象分配存储空间
Unpooled.wrappedBuffer(content.getBytes("UTF-8")));
// 获取响应头
HttpHeaders headers = response.headers();
// 设置响应体类型
headers.set(HttpHeaderNames.CONTENT_TYPE, "text/json");
// 设置响应体长度
headers.set(HttpHeaderNames.CONTENT_LENGTH, response.content().readableBytes());
// 设置缓存过期时间
headers.set(HttpHeaderNames.EXPIRES, 0);
// 若HTTP请求是长连接,则响应也使用长连接
if (HttpUtil.isKeepAlive(request)) {
headers.set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);
}
// 将响应写入到Channel
context.writeAndFlush(response);
}
}
(3) 定义 DefaultServnet 类
/**
* Tomcat中对Servnet规范的默认实现
*/
public class DefaultServnet extends Servnet {
@Override
public void doGet(NettyRequest request, NettyResponse response) throws Exception {
// http://localhost:8888/someservnet/xxx/ooo?name=zs
// uri:/someservnet/xxx/ooo?name=zs
// path:/someservnet/xxx/ooo
String sernetName = request.getUri().split("/")[1];
response.write("404 - no this servnet : " + sernetName);
}
@Override
public void doPost(NettyRequest request, NettyResponse response) throws Exception {
doGet(request, response);
}
}
(4) 定义服务器类 TomcatServer
注意:
- Server端需要维护两个map,key都是servlet的名称,value第一个map放servlet的实例,第二个map放servlet的全路径类名
其中放servlet的全路径类名的map,是没有线程安全问题,因为它在Server启动时会进行初始化,初始化完成后就不会变了
放servlet实例的map,是有线程安全问题的,多个线程同时访问同一个servlet,如果servlet还没有初始化,这些线程会都进行初始化。
public class TomcatServer {
// key为servnet的简单类名,value为对应servnet实例
private Map<String, Servnet> nameToServnetMap = new ConcurrentHashMap<>();
// key为servnet的简单类名,value为对应servnet类的全限定性类名
private Map<String, String> nameToClassNameMap = new HashMap<>();
}
根据我们定义的需求,TomcatServer启动时需要一个发布的包路径,在启动的时候把包路径下servet注册到Server中的map
/**
* Tomcat功能的实现
*/
public class TomcatServer {
// key为servnet的简单类名,value为对应servnet实例
private Map<String, Servnet> nameToServnetMap = new ConcurrentHashMap<>();
// key为servnet的简单类名,value为对应servnet类的全限定性类名
private Map<String, String> nameToClassNameMap = new HashMap<>();
private String basePackage;
public TomcatServer(String basePackage) {
this.basePackage = basePackage;
}
// 启动tomcat
public void start() throws Exception {
// 加载指定包中的所有Servnet的类名(尚未实现)
cacheClassName(basePackage);
// 启动server服务(尚未实现)
runServer();
}
}
实现cacheClassName方法,加载指定包中的所有Servnet的类名:
private void cacheClassName(String basePackage) {
// 获取指定包中的资源
URL resource = this.getClass().getClassLoader()
// com.abc.webapp => com/abc/webapp
.getResource(basePackage.replaceAll("\\.", "/"));
// 若目录中没有任何资源,则直接结束
if (resource == null) {
return;
}
// 将URL资源转换为File资源
File dir = new File(resource.getFile());
// 遍历指定包及其子孙包中的所有文件,查找所有.class文件
// 遍历有两种情况,要么是目录,要么是文件
// 如果是目录,就递归
// 如果是文件,并且是class文件则将类全路径名注册到map
for (File file : dir.listFiles()) {
if (file.isDirectory()) {
// 若当前遍历的file为目录,则递归调用当前方法
cacheClassName(basePackage + "." + file.getName());
} else if (file.getName().endsWith(".class")) {
String simpleClassName = file.getName().replace(".class", "").trim();
// key为简单类名,value为全限定性类名
nameToClassNameMap.put(simpleClassName.toLowerCase(), basePackage + "." + simpleClassName);
}
}
// System.out.println(nameToClassNameMap);
}
实现runServer方法,启动Server服务:
private void runServer() throws Exception {
EventLoopGroup parent = new NioEventLoopGroup();
EventLoopGroup child = new NioEventLoopGroup();
try {
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(parent, child)
// 指定存放请求的队列的长度
.option(ChannelOption.SO_BACKLOG, 1024)
// 指定是否启用心跳机制来检测长连接的存活性,即客户端的存活性
.childOption(ChannelOption.SO_KEEPALIVE, true)
//指定channel类型
.channel(NioServerSocketChannel.class)
//初始化channel管道
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new HttpServerCodec());
pipeline.addLast(new TomcatHandler(nameToServnetMap, nameToClassNameMap));
}
});
//监听端口8888启动Server
ChannelFuture future = bootstrap.bind(8888).sync();
System.out.println("Tomcat启动成功:监听端口号为8888");
future.channel().closeFuture().sync();
} finally {
parent.shutdownGracefully();
child.shutdownGracefully();
}
}
注意:
option(ChannelOption.SO_BACKLOG,1024)
指定存放连接请求的队列长度,这个是Socket的标准参数,TCP的参数
option是处理parent EventLoopGroup的childOption(ChannelOption.SO_KEEPALIVE,true)
指定是否启用心跳机制来检测长连接的存活性,即客户端的存活性(这个心跳机制是操作系统层实现的!也是TCP的标准参数
)
childOption是处理child EventLoopGroup的添加HttpServerCodec处理器等于同时添加了HttpRequestDecoder和HttpResponseEncoder,用来处理http请求
/** * A combination of {@link HttpRequestDecoder} and {@link HttpResponseEncoder} * which enables easier server side HTTP implementation. * * HttpServerCodec是HttpRequestDecoder解码器和HttpResponseEncoder编码器的结合 */ public final class HttpServerCodec extends CombinedChannelDuplexHandler<HttpRequestDecoder, HttpResponseEncoder> implements HttpServerUpgradeHandler.SourceCodec { ... }
(5) 定义服务器端处理器
注意:
- 处理器最好继承ChannelInboundHandlerAdapter
- 处理器的处理逻辑:
- 1)从用户请求URI中解析出要访问的Servnet名称
- 2)从nameToServnetMap中查找会否存在该名称的key,若存在则直接使用该实例,否则执行第三步
- 3)从nameToClassnameMap中查找是否存在该名称的key,若存在,则获取到其对应的全限定性类名使用反射机制创建相应的Servnet实例,并写入到nameToServnetMap,若不存在则直接访问默认Servnet
/**
* Tomcat服务端处理器
*
* 1)从用户请求URI中解析出要访问的Servnet名称
* 2)从nameToServnetMap中查找是否存在该名称的key。若存在,则直接使用该实例,否则执行第3)步
* 3)从nameToClassNameMap中查找是否存在该名称的key,若存在,则获取到其对应的全限定性类名,
* 使用反射机制创建相应的sernet实例,并写入到nameToServnetMap中,若不存在,则直接访问默认Servnet
*
*/
public class TomcatHandler extends ChannelInboundHandlerAdapter {
private Map<String, Servnet> nameToServnetMap;
private Map<String, String> nameToClassNameMap;
public TomcatHandler(Map<String, Servnet> nameToServnetMap, Map<String, String> nameToClassNameMap) {
this.nameToServnetMap = nameToServnetMap;
this.nameToClassNameMap = nameToClassNameMap;
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
if (msg instanceof HttpRequest) {
HttpRequest request = (HttpRequest) msg;
// 从请求中解析出要访问的Servnet名称
String servnetName = request.uri().split("/")[1];
// 直接给Servnet赋值一个默认的
Servnet servnet = new DefaultServnet();
if (nameToServnetMap.containsKey(servnetName)) {
servnet = nameToServnetMap.get(servnetName);
} else if (nameToClassNameMap.containsKey(servnetName)) {
// double-check,双重检测锁
// 当读到消息的时候根据请求获取对应的Servnet
// 如果不存在创建的时候用双重检查锁,避免线程安全问题
if (nameToServnetMap.get(servnetName) == null) {
synchronized (this) {
if (nameToServnetMap.get(servnetName) == null) {
// 获取当前Servnet的全限定性类名
String className = nameToClassNameMap.get(servnetName);
// 使用反射机制创建Servnet实例
servnet = (Servnet) Class.forName(className).newInstance();
// 将Servnet实例写入到nameToServnetMap
nameToServnetMap.put(servnetName, servnet);
}
}
}
} // end-else if
// 代码走到这里,servnet肯定不空
NettyRequest req = new DefaultNettyRequest(request);
NettyResponse res = new DefaultNettyResponse(request, ctx);
// 根据不同的请求类型,调用servnet实例的不同方法
if (request.method().name().equalsIgnoreCase("GET")) {
servnet.doGet(req, res);
} else if(request.method().name().equalsIgnoreCase("POST")) {
servnet.doPost(req, res);
}
ctx.close();
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
(6) 定义启动类 TomcatStarter
public class TomcatStarter {
public static void main(String[] args) throws Exception {
TomcatServer server = new TomcatServer("com.abc.webapp");
server.start();
}
}
5. 定义业务 Servnet
/**
* 业务Servnet
*/
public class OneServnet extends Servnet {
@Override
public void doGet(NettyRequest request, NettyResponse response) throws Exception {
String uri = request.getUri();
String path = request.getPath();
String method = request.getMethod();
String name = request.getParameter("name");
String content = "uri = " + uri + "\n" +
"path = " + path + "\n" +
"method = " + method + "\n" +
"param = " + name;
response.write(content);
}
@Override
public void doPost(NettyRequest request, NettyResponse response) throws Exception {
doGet(request, response);
}
}
6. 启动测试
没有回复内容