如何基于Netty手写简单的Tomcat?
我们最常用的服务器是tomcat ,我们使用tomcat 也主要作为http服务器 。
http协议是基于TCP 协议,换句话说使用socket 或者 NIO编程,只要能正确的解析http报文,然后将结果按照 http 报文的格式返回。我们就可以实现自己的web服务器,当然再支持websocket 也是可以做到的。
Netty 给我们内置了http 、websocket 处理器,直接拿来使用即可,因此基于Netty 会简化很多。
手写tomcat 核心是什么呢?
服务器就是处理用户请求,并返回结果。使用过curl 命令的应该印象很深刻,http 请求最主要的是url 和参数(包含请求头)
URL 就是告诉我们我要做什么功能(what),而参数就是告诉系统具体怎样做(How)。
HTTP 报文示例
POST /login HTTP/1.1
Host: api.example.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3
Accept: application/json, text/plain, */*
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Accept-Encoding: gzip, deflate, br
Content-Type: application/x-www-form-urlencoded
Content-Length: 27
Connection: keep-alive
username=admin&password=123456
举个不恰当例子
-
前端调用 /user/login,说明用户想登录了。
-
请求参数包含了用户名、密码,该用户选择了用户名、密码进行登录。
-
当然系统也支持短信验证码登录方式, 如果用户选择该方式,请求参数必然包含手机号和短信验证码。
如何根据URL 确定相应的业务处理逻辑
通过上面分析,通请求过来,我们需要根据 url 找到对应的处理器 ,比如下图, 前端调用 /user/login 这个接口,后端如何就能会执行到 登录相关业务代码呢?
使用HashMap 做个映射可否
很容易想到用 HashMap 记录url到对应的处理器不就可以了,直接硬编码肯定是不妥的,作为一个通用的服务器比如tomcat ,你不可能每次实现一个新业务,让tomcat为你修改源码。因此才有了外部配置文件。
配置文件方式
在tomcat 中我们可以在web.xml中配置servlet-mapping,来确认url匹配对应的Servlet, 启动时加载配置文件,注册url 与Servlet映射关系,收到请求,就能根据url 找到 对应Servlet 处理了。
实际上根据url查找对应的Servlet 不一定是简单的 hashMap.get() 就可以了, 因为servlet-mapping 中配置的URL 可能包含一些通配符,tomcat内部url 匹配算法,优先匹配最精确的那个。
Tomcat使用的是XML方式进行动态配置,抛开Tomcat,就实现这种映射关系可以是各种配置,yaml,json等,只要能满足动态配置即可,当然要考虑使用简单。
<servlet-mapping>
<servlet-name>LoginServlet</servlet-name>
<url-pattern>/user/login</url-pattern>
</servlet-mapping>
自定义注解
Spring MVC 处理业务逻辑的DispatcherServlet,我们感知不到它的存在,平常我们只需要写Controller ,这是因为DispatcherServlet拦截了所有的请求。
SpringBoot 提倡零配置,使用注解方式是Spring MVC 的主流。
Spring 容器启动时会扫描 @Controller @RestController 相关类,并且j解析类上、方法上 @RequestMapping 注解。
类上URL作为基础路径和方法上URL拼接作为最终URL ,而对应方法即最终URL的处理器。相关的映射关系也被称为handler mappings 。
状态码
对于Socket本身其实只能绑定端口,当用户URL找不到对应处理器,Tomcat一般会报404 。
当然Http 标准定义了很多状态码,如200 代表请求成功等,处理出错了会报500等
Netty http 示例
服务端pipeline 配置如下,HttpServerHandler 是一个简单的处理器。
-
QueryStringDecoder 可以用来处理GET请求参数解析
-
HttpPostRequestDecoder 用来解析POST请求参数
-
FullHttpResponse 用来响应请求
读者自己加上根据URL 进行不同处理逻辑,就是一个简单的HTTP服务器了,限于篇幅,具体实现省略。
ChannelPipeline p = ch.pipeline();
// 添加 HTTP 请求解码器
p.addLast(new HttpRequestDecoder());
// 添加 HTTP 响应编码器
p.addLast(new HttpResponseEncoder());
// 聚合HTTP消息,将多个部分组合成一个完整的请求
p.addLast(new HttpObjectAggregator(65536));
// 添加自定义处理器
p.addLast(new HttpServerHandler());
public class HttpServerHandler extends SimpleChannelInboundHandler<FullHttpRequest> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest req) throws Exception {
if (req.method().equals(HttpMethod.GET)) {
QueryStringDecoder decoder = new QueryStringDecoder(req.uri());
Map<String, List<String>> paramers = decoder.parameters();
if (paramers != null) {
paramers.forEach((key, value) -> System.out.println(key + " => " + value));
}
} else {
HttpPostRequestDecoder decoder = new HttpPostRequestDecoder(req);
List<InterfaceHttpData> datas = decoder.getBodyHttpDatas();
for (InterfaceHttpData data : datas) {
if (data.getHttpDataType() == InterfaceHttpData.HttpDataType.Attribute) {
MixedAttribute attribute = (MixedAttribute) data;
System.out.println(attribute.getName() + " => " + attribute.getValue());
}
}
}
// 构造响应
FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1,
HttpResponseStatus.OK, Unpooled.wrappedBuffer("Hello, World!".getBytes()));
// 设置响应头
response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain; charset=UTF-8");
response.headers().set(HttpHeaderNames.CONTENT_LENGTH, response.content().readableBytes());
// 发送响应
ctx.writeAndFlush(response);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
cause.printStackTrace();
ctx.close();
}
}
演示结果
webSocket
服务端核心代码
ch.pipeline().addLast(new HttpServerCodec());
ch.pipeline().addLast(new ChunkedWriteHandler());
ch.pipeline().addLast(new HttpObjectAggregator(65536));
ch.pipeline().addLast(new WebSocketServerProtocolHandler("/ws"));
ch.pipeline().addLast(new WebSocketServerHandler());
public class WebSocketServerHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
// 输出接收到的消息
System.out.println("Received message: " + msg.text());
// 回应客户端
String response = "Server received: " + msg.text();
ctx.channel().writeAndFlush(new TextWebSocketFrame(response));
}
//省略其他方法
}
演示效果
演示时可以使用 java websocket client
WebSocketContainer container = ContainerProvider.getWebSocketContainer();
// 连接到WebSocket服务器
String uri = "ws://localhost:8080/ws";
logger.info("Connecting to " + uri);
Session session = container.connectToServer(MyWebSocketClient.class, URI.create(uri));
// 等待一段时间以保持连接
Thread.sleep(10000);
// 关闭连接
session.close();
@ClientEndpoint
public class MyWebSocketClient {
//省略非重要代码
@OnOpen
public void onOpen(Session session, EndpointConfig config) {
logger.info("Connected to server: " + session.getId());
// 发送消息到服务器
session.getAsyncRemote().sendText("Hello, Server!");
}
@OnMessage
public void onMessage(String message, Session session) {
logger.info("Received from server: " + message);
}
}
总结
本文简单梳理了Tomcat 、Spring MVC 通过URL 找对应的处理器的流程,为手写Tomcat 提供了思路。给出了基于Netty 支持Http 和 WebSocket示例代码。
至此手写一个简单的Http服务器,相信不是什么难事。