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

2023年 10月 11日 31.6k 0

 继上一篇文章什么,这年头还有人不知道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展示图如下:

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

二、需求拆解

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

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

    客户端

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

    客户端

  • 三、需求实现

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

  • dashboard 如何知道某个app下某个接口的通讯 ip + port
  • dashboard 如何接受客户端的请求
  • 同样,客户端如何接受dashboard的请求(这是本文讲解的重点)
  • sentinel 的实现逻辑如下:

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

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

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

    使用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 模块为例,其大概得执行过程是:

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

    可以简单看看代码:

    // 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);
            }
        }
    

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

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

    @CommandMapping(name = "tree", desc = "get metrics in tree mode, use id to specify detailed tree root")
    public class FetchTreeCommandHandler implements CommandHandler {
    
        @Override
        public CommandResponse 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("rnrn");
            sb.append(
                "t:threadNum  pq:passQps  bq:blockQps  tq:totalQps  rt:averageRt  prq: passRequestQps 1mp:1m-pass "
                    + "1mb:1m-block 1mt:1m-total").append("rn");
            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);
            }
        }
    }
    

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

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

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

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

    四、spring-mvc 模式通信兼容

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

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

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

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

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

    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;
        }
    }
    

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

    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 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 handlerMap) {
            if (handlerMap != null) {
                for (Map.Entry 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);
                }
            }
        }
    }
    

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

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

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

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

    相关文章

    JavaScript2024新功能:Object.groupBy、正则表达式v标志
    PHP trim 函数对多字节字符的使用和限制
    新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
    使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
    为React 19做准备:WordPress 6.6用户指南
    如何删除WordPress中的所有评论

    发布评论