文章目录
项目内容
- 构建一个Servlet容器(仿Tomcat)
- 写一个仿支付的页面,其可以在接收到支付成功通知后自动跳转
- 多人聊天室
建议至少有使用过Netty的经验、了解HTTP协议、了解Servlet用法的读者阅读,否则可能会有一点吃力
文章中的示例demo已上传至本人Github,戳此访问?
其中html、js文件均为网络web上的源码copy而来,如有侵犯版权,请告知作者
1. Servlet容器
1.1 介绍
在tomcat中,启动tomcat应用首先会读取web.xml文件,去读取servlet配置,在最早的时候我们就是在web.xml中配置servlet映射关系,然后在具体的servlet类中编写我们的业务逻辑,但是这样做相当冗余,表现在多一个uri请求我就要加一个servlet类,管理起来十分繁琐且复杂,所以这时候MVC思想就来了,使用仅一个DispatcherServlet接受所有请求,我们就不需要在web.xml配置Servlet映射了,正好Spring中有一个MVC模块,我们只需要将具体业务逻辑变成一个个method,变成Bean交给Spring管理,此时Spring就会让这个DispatcherServlet去根据uri执行它管理的Bean —>执行对应的method。
有点扯远了,但我想说的是,将每个uri映射到某个Servlet上这个过程十分繁琐,但项目中为了还原tomcat的原始度,构建最底层最古老的写法,包括对http协议的构建,io的处理,servlet的映射和业务逻辑处理,关注更多的也更值得我们学习的是如何实现服务端的io高性能处理,如何仿tomcat去接受请求,仿造一个Servlet容器的网络请求做法,和如何构建http协议,所以暂且不要关注它的可用性。
1.2 协议
既然仿造的是tomcat,那么协议一定是HTTP协议,这里客户端指的是浏览器,当在浏览器输入一个url后,将会向服务端发起一个HttpRequest,然后服务端需要根据request的不同,构造不同的response返回出去。
1.3 Servlet映射关系初始化
这里我没有照搬tomcat的web.xml配置,而是使用properties放置配置,怎么方便怎么来,大致意思有了就行
servlet.chatServlet.url=/
servlet.chatServlet.className=com.push.server.servlet.ChatIndexServlet
servlet.RestfulServlet.url=/payOrder
servlet.RestfulServlet.className=com.push.server.servlet.RestfulServlet
servlet.WaitPayServlet.url=/pay
servlet.WaitPayServlet.className=com.push.server.servlet.WaitPayServlet
这里是我demo中的一个配置,你需要按照以上规则去配置url与Servlet的映射关系,这样,我在浏览器访问/pay就会交给WaitPayServlet这个Servlet去处理请求了
具体初始化Servlet的代码如下:
public class ServletUtils {
private static Properties webProperties = new Properties();
// 存放url与Servlet关系的Map
private static Map<String, IServlet> servletMapping = new HashMap<String, IServlet>();
public static final String WEB_INF = "/WEB-INF";
// 初始化url与Servlet关系
public static synchronized void init() throws IOException {
if (AbstractServlet.successInit) {
log.info("已经初始化过一次了,不要重复初始化servlet");
}
InputStream in = null;
try {
// 获取 resources/web.properties 下的文件流
log.info("开启文件流");
in = ServletUtils.class.getResourceAsStream("/web.properties");
log.info("读取文件流");
// 读取properties的内容
webProperties.load(in);
for (Object k : webProperties.keySet()) {
log.info("读取内容");
String key = k.toString();
// 按照我们的规则去解析它
if (key.endsWith(".url")) {
String servletName = key.replaceAll("\\.url$", "");
String url = webProperties.getProperty(key);
String className = webProperties.getProperty(servletName + ".className");
// 单实例,多线程
IServlet obj = (IServlet) Class.forName(className).newInstance();
servletMapping.put(url, obj);
}
}
if (servletMapping.size() == 0) {
log.info("没有读取到servlet映射配置");
throw new RuntimeException("没有读取到servlet映射配置");
} else {
AbstractServlet.successInit = true;
}
} catch (Exception e) {
log.info("读取servlet配置文件失败: [{}]", e.getMessage());
throw new RuntimeException("读取servlet配置文件失败");
} finally {
if (in != null) {
in.close();
}
}
}
主要做的就是读取指定的文件名,然后按照我们的规则去解析映射关系,并且使用反射去创建一个Servlet实例,并保存在Map中,以便后续使用。值得一提的是,这里和tomcat的思想一致,Servlet是单实例,并多线程访问的。
之后我们只需要根据url拿到对应的Servlet去执行service方法即可:
public static IServlet findUriMapping(String uri) {
return servletMapping.get(uri);
}
1.4 HTTP协议的编解码
-
解码器作用:在网络io的过程中,数据的传输一般都是使用字节来传输,同样,在浏览器对服务端发起一个http请求的时候也是发送一串字节,此时我们需要根据字节解析出可以看得懂的HttpRequest对象,这时候我们就需要一个解码器。
-
编码器作用:当我们根据请求构造相应的响应对象HttpResponse时,同样需要将其变成字节,利用网络传输出去给浏览器,这时候我们就需要一个编码器。
编解码过程相对繁琐,但Netty帮我们实现了大部分的公有协议,例如HTTP协议、WebSocket协议等等,所以编解码这块不需要我们关心,如果想自己实现一个轻量的协议,Netty提供了一些基类,所以实现起来也是很方便的,在后面的聊天室服务器的实现中我就自定义了一个通信规则,感兴趣的读者可以在下面了解自定义协议的实现。
1.5 服务端的启动
private static final int DEFAULT_PORT = 8888;
public static void start(int port) {
EventLoopGroup bossEventLoop = new NioEventLoopGroup(1);
EventLoopGroup workerEventLoop = new NioEventLoopGroup();
try {
// 初始化servlet的映射关系
ServletUtils.init();
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossEventLoop, workerEventLoop)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer() {
@Override
protected void initChannel