了解了spring mvc web容器中一个http请求的全过程,能给我们提升多少武力值

继上一篇文章什么,这年头还有人不知道404 - 掘金 (juejin.cn)后,有些同学发现,学了之后有啥用,有什么实际场景可以用到吗?程序员就是这样,不习惯于纸上谈兵,给一个场景show me code才是最实在的,好了,不扯淡了,回归正文吧!

一、场景

有这么一个场景,大家看看怎么来实现,在咱们使用sentinel(熔断限流器)alibaba/Sentinel: A powerful flow control component enabling reliability, resilience and monitoring for microservices. (面向云原生微服务的高可用流控防护组件) (github.com)时,需要在dashboard展示和编辑各种各样的数据,比如展示某个应用下集群机器列表、展示实时监控数据、规则展示、规则编辑等等。

dashboard展示图如下:

​编辑

二、需求拆解

看到这个场景后,我们能想到的就是这些数据从哪里来?又流向哪里?清楚这个后,才能制定具体的事实施方案。

  1. 这些需要展示的数据从哪里来?

    客户端

  2. 在dashboard上编辑规则后,这些数据流向哪里?

    客户端

三、需求实现

那么在清楚需求之后,总结起来就是一句话,客户端有数据需要传到dashborad,同样dashborad也有数据需要传到客户端。那么如何实现呢?

  1. dashboard 如何知道某个app下某个接口的通讯 ip + port
  2. dashboard 如何接受客户端的请求
  3. 同样,客户端如何接受dashboard的请求(这是本文讲解的重点)

sentinel 的实现逻辑如下:

​编辑

根据上图,如果换做我们,那估计就是分别在客户端和dashboard上开几个接口就ok了,那么sentinel 是这么做的吗?是,也不是。我们拿dashboard从客户端读/写数据为例,在早期的sentinel版本中,并没有在客户端使用web容器开启http接口,因为它觉得使用web容器的方式太重了。不信,你看sentinel官方给出的解释

​编辑

使用web容器太过于重要级我理解有两层含义,第一就是web框架本身就比较重,其次就是有些客户端并不是使用的spring或者spring mvc 框架,为了减小依赖,sentinel提供了比较原生的实现方式。从图中可以看出,sentinel 专门写了一个transport模块用来通信,早期的transport中包含sentinel-transport-simple-httpsentinel-transport-netty-http两个模块,sentinel-transport-simple-http 使用的是jdk原生的socket 而sentinel-transport-netty-http采用的netty来实现http server。那么怎么实现的呢?可以简单看看,以 sentinel-transport-simple-http 模块为例,其大概得执行过程是:

​编辑

可以简单看看代码:

java 复制代码
// HttpEventTask 类
        public void run() {
        if (socket == null) {
            return;
        }

        PrintWriter printWriter = null;
        InputStream inputStream = null;
        try {
            long start = System.currentTimeMillis();
            inputStream = new BufferedInputStream(socket.getInputStream());
            OutputStream outputStream = socket.getOutputStream();

            printWriter = new PrintWriter(
                new OutputStreamWriter(outputStream, Charset.forName(SentinelConfig.charset())));

            String firstLine = readLine(inputStream);
            CommandCenterLog.info("[SimpleHttpCommandCenter] Socket income: " + firstLine
                + ", addr: " + socket.getInetAddress());
            CommandRequest request = processQueryString(firstLine);

            if (firstLine.length() > 4 && StringUtil.equalsIgnoreCase("POST", firstLine.substring(0, 4))) {
                // Deal with post method
                processPostRequest(inputStream, request);
            }

            // Validate the target command.
            String commandName = HttpCommandUtils.getTarget(request);
            if (StringUtil.isBlank(commandName)) {
                writeResponse(printWriter, StatusCode.BAD_REQUEST, INVALID_COMMAND_MESSAGE);
                return;
            }

            // Find the matching command handler.
            CommandHandler<?> commandHandler = SimpleHttpCommandCenter.getHandler(commandName);
            if (commandHandler != null) {
                CommandResponse<?> response = commandHandler.handle(request);
                handleResponse(response, printWriter);
            } else {
                // No matching command handler.
                writeResponse(printWriter, StatusCode.BAD_REQUEST, "Unknown command `" + commandName + '`');
            }

            long cost = System.currentTimeMillis() - start;
            CommandCenterLog.info("[SimpleHttpCommandCenter] Deal a socket task: " + firstLine
                + ", address: " + socket.getInetAddress() + ", time cost: " + cost + " ms");
        } catch (RequestException e) {
            writeResponse(printWriter, e.getStatusCode(), e.getMessage());
        } catch (Throwable e) {
            CommandCenterLog.warn("[SimpleHttpCommandCenter] CommandCenter error", e);
            try {
                if (printWriter != null) {
                    String errorMessage = SERVER_ERROR_MESSAGE;
                    e.printStackTrace();
                    if (!writtenHead) {
                        writeResponse(printWriter, StatusCode.INTERNAL_SERVER_ERROR, errorMessage);
                    } else {
                        printWriter.println(errorMessage);
                    }
                    printWriter.flush();
                }
            } catch (Exception e1) {
                CommandCenterLog.warn("Failed to write error response", e1);
            }
        } finally {
            closeResource(inputStream);
            closeResource(printWriter);
            closeResource(socket);
        }
    }

CommandHandler<?> commandHandler = SimpleHttpCommandCenter.getHandler(commandName); 这行代码就是根据commandName 获取 CommandHandler,CommandHandler 是一个顶层接口,其实现类上定义了一个@CommandMapping,该注解中有个name字段,用来定义command路径,这里有点类似 @RequestMapping的味道,具体代码如下:

java 复制代码
@CommandMapping(name = "tree", desc = "get metrics in tree mode, use id to specify detailed tree root")
public class FetchTreeCommandHandler implements CommandHandler<String> {

    @Override
    public CommandResponse<String> handle(CommandRequest request) {
        String id = request.getParam("id");

        StringBuilder sb = new StringBuilder();

        DefaultNode start = Constants.ROOT;

        if (id == null) {
            visitTree(0, start, sb);
        } else {
            boolean exactly = false;
            for (Node n : start.getChildList()) {
                DefaultNode dn = (DefaultNode)n;
                if (dn.getId().getName().equals(id)) {
                    visitTree(0, dn, sb);
                    exactly = true;
                    break;
                }
            }

            if (!exactly) {
                for (Node n : start.getChildList()) {
                    DefaultNode dn = (DefaultNode)n;
                    if (dn.getId().getName().contains(id)) {
                        visitTree(0, dn, sb);
                    }
                }
            }
        }
        sb.append("\r\n\r\n");
        sb.append(
            "t:threadNum  pq:passQps  bq:blockQps  tq:totalQps  rt:averageRt  prq: passRequestQps 1mp:1m-pass "
                + "1mb:1m-block 1mt:1m-total").append("\r\n");
        return CommandResponse.ofSuccess(sb.toString());
    }

    private void visitTree(int level, DefaultNode node, /*@NonNull*/ StringBuilder sb) {
        for (int i = 0; i < level; ++i) {
            sb.append("-");
        }
        if (!(node instanceof EntranceNode)) {
            sb.append(String.format("%s(t:%s pq:%s bq:%s tq:%s rt:%s prq:%s 1mp:%s 1mb:%s 1mt:%s)",
                node.getId().getShowName(), node.curThreadNum(), node.passQps(),
                node.blockQps(), node.totalQps(), node.avgRt(), node.successQps(),
                node.totalRequest() - node.blockRequest(), node.blockRequest(),
                node.totalRequest())).append("\n");
        } else {
            sb.append(String.format("EntranceNode: %s(t:%s pq:%s bq:%s tq:%s rt:%s prq:%s 1mp:%s 1mb:%s 1mt:%s)",
                node.getId().getShowName(), node.curThreadNum(), node.passQps(),
                node.blockQps(), node.totalQps(), node.avgRt(), node.successQps(),
                node.totalRequest() - node.blockRequest(), node.blockRequest(),
                node.totalRequest())).append("\n");
        }
        for (Node n : node.getChildList()) {
            DefaultNode dn = (DefaultNode)n;
            visitTree(level + 1, dn, sb);
        }
    }
}

这样我们请求接口http://localhost:10000/tree?type=root时,其返回结果如下:

​编辑

同样,sentinel-transport-netty-http 也是类似的逻辑!这样看来一切安好。

四、spring-mvc 模式通信兼容

直到有一天有人提出以下问题:

​编辑

总的来看就是现在和dashboard交互的端口需要和sprinboot web 应用共用一个端口。那现在有个难题。由于已经存在 sentinel-transport-simple-httpsentinel-transport-netty-http 模块,底层设计采用的是 CommandHandler 来适配各类请求,那么如果是以web容器来执行mvc模式的请求该如何兼容呢?

​编辑

江湖中不缺好手,时隔半年后,有人提出,在不改变底层设计的情况下,只需要实现HandlerAdapter 和 HandlerMapping 即可,看到这里是不是觉得很熟悉,HandlerAdapter 和 HandlerMapping不就是大名鼎鼎的处理器适配器和处理器映射器吗?咱们回顾下,用大白话说HandlerMapping的作用就是根据url路径找handler, HandlerAdapter就是对handler进行装饰,忽略底层细节,对上层提供统一的调用方法来进行handler处理。那么sentinel是怎么做的呢?我们看看

java 复制代码
public class SentinelApiHandlerAdapter implements HandlerAdapter, Ordered {

    private int order = Ordered.LOWEST_PRECEDENCE;

    public void setOrder(int order) {
        this.order = order;
    }

    @Override
    public int getOrder() {
        return order;
    }

    @Override
    public boolean supports(Object handler) {
        return handler instanceof SentinelApiHandler;
    }

    @Override
    public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        SentinelApiHandler sentinelApiHandler = (SentinelApiHandler) handler;
        // 调用底层的CommandHandler接口
        sentinelApiHandler.handle(request, response);
        return null;
    }

    @Override
    public long getLastModified(HttpServletRequest request, Object handler) {
        return -1;
    }
}
java 复制代码
public class SentinelApiHandlerMapping extends AbstractHandlerMapping implements ApplicationListener {

    private static final String SPRING_BOOT_WEB_SERVER_INITIALIZED_EVENT_CLASS = "org.springframework.boot.web.context.WebServerInitializedEvent";
    private static Class webServerInitializedEventClass;

    static {
        try {
            webServerInitializedEventClass = ClassUtils.forName(SPRING_BOOT_WEB_SERVER_INITIALIZED_EVENT_CLASS, null);
            RecordLog.info("[SentinelApiHandlerMapping] class {} is present, this is a spring-boot app, we can auto detect port", SPRING_BOOT_WEB_SERVER_INITIALIZED_EVENT_CLASS);
        } catch (ClassNotFoundException e) {
            RecordLog.info("[SentinelApiHandlerMapping] class {} is not present, this is not a spring-boot app, we can not auto detect port", SPRING_BOOT_WEB_SERVER_INITIALIZED_EVENT_CLASS);
        }
    }

    final static Map<String, CommandHandler> handlerMap = new ConcurrentHashMap<>();

    private boolean ignoreInterceptor = true;

    public SentinelApiHandlerMapping() {
        setOrder(Ordered.LOWEST_PRECEDENCE - 10);
    }

    @Override
    protected Object getHandlerInternal(HttpServletRequest request) throws Exception {
        String commandName = request.getRequestURI();
        if (commandName.startsWith("/")) {
            commandName = commandName.substring(1);
        }
        // 获取底层CommandHandler
        CommandHandler commandHandler = handlerMap.get(commandName);
        return commandHandler != null ? new SentinelApiHandler(commandHandler) : null;
    }

    @Override
    protected HandlerExecutionChain getHandlerExecutionChain(Object handler, HttpServletRequest request) {
        return ignoreInterceptor ? new HandlerExecutionChain(handler) : super.getHandlerExecutionChain(handler, request);
    }

    public void setIgnoreInterceptor(boolean ignoreInterceptor) {
        this.ignoreInterceptor = ignoreInterceptor;
    }

    public static void registerCommand(String commandName, CommandHandler handler) {
        if (StringUtil.isEmpty(commandName) || handler == null) {
            return;
        }

        if (handlerMap.containsKey(commandName)) {
            CommandCenterLog.warn("[SentinelApiHandlerMapping] Register failed (duplicate command): " + commandName);
            return;
        }

        handlerMap.put(commandName, handler);
    }

    public static void registerCommands(Map<String, CommandHandler> handlerMap) {
        if (handlerMap != null) {
            for (Map.Entry<String, CommandHandler> e : handlerMap.entrySet()) {
                registerCommand(e.getKey(), e.getValue());
            }
        }
    }

    @Override
    public void onApplicationEvent(ApplicationEvent applicationEvent) {
        if (webServerInitializedEventClass != null && webServerInitializedEventClass.isAssignableFrom(applicationEvent.getClass())) {
            Integer port = null;
            try {
                BeanWrapper beanWrapper = new BeanWrapperImpl(applicationEvent);
                port = (Integer) beanWrapper.getPropertyValue("webServer.port");
            } catch (Exception e) {
                RecordLog.warn("[SentinelApiHandlerMapping] resolve port from event " + applicationEvent + " fail", e);
            }
            if (port != null && TransportConfig.getPort() == null) {
                RecordLog.info("[SentinelApiHandlerMapping] resolve port {} from event {}", port, applicationEvent);
                TransportConfig.setRuntimePort(port);
            }
        }
    }
}

后来sentinel官方也采用了这种方法做了升级,sentinel 1.8.2 升级说明如下:

​编辑

好了,看到这里,你是否对spring mvc web容器下的http请求过程有了更深的理解呢?

相关推荐
Asthenia04121 分钟前
为什么MySQL关联查询要“小表驱动大表”?深入解析与模拟面试复盘
后端
南雨北斗4 分钟前
分布式系统中如何保证数据一致性
后端
Asthenia04128 分钟前
Feign结构与请求链路详解及面试重点解析
后端
左灯右行的爱情11 分钟前
缓存并发更新的挑战
jvm·数据库·redis·后端·缓存
brzhang15 分钟前
告别『上线裸奔』!一文带你配齐生产级 Web 应用的 10 大核心组件
前端·后端·架构
shepherd11116 分钟前
Kafka生产环境实战经验深度总结,让你少走弯路
后端·面试·kafka
袋鱼不重29 分钟前
Cursor 最简易上手体验:谷歌浏览器插件开发3s搞定!
前端·后端·cursor
嘻嘻哈哈开森31 分钟前
Agent 系统技术分享
后端
用户40993225021232 分钟前
异步IO与Tortoise-ORM的数据库
后端·ai编程·trae
会有猫37 分钟前
LabelStudio使用阿里云OSS教程
后端