如何基于Netty手写简单的Tomcat?

时间:2024-11-20 08:46:35

如何基于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服务器,相信不是什么难事。