netty-websocket 鉴权token及统一请求和响应头(鉴权控制器)

news2024/11/15 9:11:32

在这里插入图片描述

自己想法和实现,如果有说错的或者有更好的简单的实现方式可以私信交流一下(主要是实现握手时鉴权)

需求实现

  1. 握手鉴权是基于前台请求头 Sec-WebSocket-Protocol的
  2. 本身socket并没有提供自定义请求头,只能自定义 Sec-WebSocket-Protocol的自协议

问题描述

socket握手请求是基于http的,握手成功后会升级为ws

前台传输了 token作为Sec-WebSocket-Protocol的值,后台接收到后总是断开连接,后来网上看了很多博客说的都是大同小异,然后就看了他的源码一步步走的(倔脾气哈哈),终于我看到了端倪,这个问题是因为前后台的Sec-WebSocket-Protocol值不一致,所以会断开,但是我记得websocket好像是不用自己设置请求头的,但是netty我看了源码,好像没有预留设置websocket的response的响应头(这只是我的个人理解)

具体实现

CustomWebSocketProtocolHandler

解释: 自定义替换WebSocketProtocolHandler,复制WebSocketProtocolHandler的内容即可,因为主要是WebSocketServerProtocolHandler自定义会用到

abstract class CustomWebSocketProtocolHandler extends MessageToMessageDecoder<WebSocketFrame> {
    @Override
    protected void decode(ChannelHandlerContext ctx, WebSocketFrame frame, List<Object> out) throws Exception {
        if (frame instanceof PingWebSocketFrame) {
            frame.content().retain();
            ctx.channel().writeAndFlush(new PongWebSocketFrame(frame.content()));
            return;
        }
        if (frame instanceof PongWebSocketFrame) {
            // Pong frames need to get ignored
            return;
        }

        out.add(frame.retain());
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        ctx.fireExceptionCaught(cause);
        ctx.close();
    }
}

CustomWebSocketServerProtocolHandler

解释: 自定义WebSocketServerProtocolHandler,实现上面自定义的WebSocketProtocolHandler,具体内容和WebSocketServerProtocolHandler保持一致,只需要将handlerAdded中的类ProtocolHandler改为自己定义的即可
注意:后面监听读写的自定义业务的handler需要实现相应的方法:异常或者事件监听,因为比如异常,如果抛出异常了,是不会有控制器去管的,因为当前的业务控制器就是最后一层,因为上面已经把默认实现改成了自己的实现(其他的控制器都是基于默认handler实现的,如果改了后,去初始化自己改后的handler那便是最后一层),所以要手动去关闭

ublic class CustomWebSocketServerProtocolHandler extends CustomWebSocketProtocolHandler {

    /**
     * Events that are fired to notify about handshake status
     */
    public enum ServerHandshakeStateEvent {
        /**
         * The Handshake was completed successfully and the channel was upgraded to websockets.
         *
         * @deprecated in favor of {@link WebSocketServerProtocolHandler.HandshakeComplete} class,
         * it provides extra information about the handshake
         */
        @Deprecated
        HANDSHAKE_COMPLETE
    }

    /**
     * The Handshake was completed successfully and the channel was upgraded to websockets.
     */
    public static final class HandshakeComplete {
        private final String requestUri;
        private final HttpHeaders requestHeaders;
        private final String selectedSubprotocol;

       public HandshakeComplete(String requestUri, HttpHeaders requestHeaders, String selectedSubprotocol) {
            this.requestUri = requestUri;
            this.requestHeaders = requestHeaders;
            this.selectedSubprotocol = selectedSubprotocol;
        }

        public String requestUri() {
            return requestUri;
        }

        public HttpHeaders requestHeaders() {
            return requestHeaders;
        }

        public String selectedSubprotocol() {
            return selectedSubprotocol;
        }
    }

    private static final AttributeKey<WebSocketServerHandshaker> HANDSHAKER_ATTR_KEY =
            AttributeKey.valueOf(WebSocketServerHandshaker.class, "HANDSHAKER");

    private final String websocketPath;
    private final String subprotocols;
    private final boolean allowExtensions;
    private final int maxFramePayloadLength;
    private final boolean allowMaskMismatch;
    private final boolean checkStartsWith;

    public CustomWebSocketServerProtocolHandler(String websocketPath) {
        this(websocketPath, null, false);
    }

    public CustomWebSocketServerProtocolHandler(String websocketPath, boolean checkStartsWith) {
        this(websocketPath, null, false, 65536, false, checkStartsWith);
    }

    public CustomWebSocketServerProtocolHandler(String websocketPath, String subprotocols) {
        this(websocketPath, subprotocols, false);
    }

    public CustomWebSocketServerProtocolHandler(String websocketPath, String subprotocols, boolean allowExtensions) {
        this(websocketPath, subprotocols, allowExtensions, 65536);
    }

    public CustomWebSocketServerProtocolHandler(String websocketPath, String subprotocols,
                                          boolean allowExtensions, int maxFrameSize) {
        this(websocketPath, subprotocols, allowExtensions, maxFrameSize, false);
    }

    public CustomWebSocketServerProtocolHandler(String websocketPath, String subprotocols,
                                          boolean allowExtensions, int maxFrameSize, boolean allowMaskMismatch) {
        this(websocketPath, subprotocols, allowExtensions, maxFrameSize, allowMaskMismatch, false);
    }

    public CustomWebSocketServerProtocolHandler(String websocketPath, String subprotocols,
                                          boolean allowExtensions, int maxFrameSize, boolean allowMaskMismatch, boolean checkStartsWith) {
        this.websocketPath = websocketPath;
        this.subprotocols = subprotocols;
        this.allowExtensions = allowExtensions;
        maxFramePayloadLength = maxFrameSize;
        this.allowMaskMismatch = allowMaskMismatch;
        this.checkStartsWith = checkStartsWith;
    }

    @Override
    public void handlerAdded(ChannelHandlerContext ctx) {
        ChannelPipeline cp = ctx.pipeline();
        if (cp.get(CustomWebSocketServerProtocolHandler.class) == null) {
            // Add the WebSocketHandshakeHandler before this one.
            ctx.pipeline().addBefore(ctx.name(), CustomWebSocketServerProtocolHandler.class.getName(),
                    new CustomWebSocketServerProtocolHandler(websocketPath, subprotocols,
                            allowExtensions, maxFramePayloadLength, allowMaskMismatch, checkStartsWith));
        }
        if (cp.get(Utf8FrameValidator.class) == null) {
            // Add the UFT8 checking before this one.
            ctx.pipeline().addBefore(ctx.name(), Utf8FrameValidator.class.getName(),
                    new Utf8FrameValidator());
        }
    }

    @Override
    protected void decode(ChannelHandlerContext ctx, WebSocketFrame frame, List<Object> out) throws Exception {
        if (frame instanceof CloseWebSocketFrame) {
            WebSocketServerHandshaker handshaker = getHandshaker(ctx.channel());
            if (handshaker != null) {
                frame.retain();
                handshaker.close(ctx.channel(), (CloseWebSocketFrame) frame);
            } else {
                ctx.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE);
            }
            return;
        }
        super.decode(ctx, frame, out);
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        if (cause instanceof WebSocketHandshakeException) {
            FullHttpResponse response = new DefaultFullHttpResponse(
                    HTTP_1_1, HttpResponseStatus.BAD_REQUEST, Unpooled.wrappedBuffer(cause.getMessage().getBytes()));
            ctx.channel().writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
        } else {
            ctx.fireExceptionCaught(cause);
            ctx.close();
        }
    }

    static WebSocketServerHandshaker getHandshaker(Channel channel) {
        return channel.attr(HANDSHAKER_ATTR_KEY).get();
    }

    public static void setHandshaker(Channel channel, WebSocketServerHandshaker handshaker) {
        channel.attr(HANDSHAKER_ATTR_KEY).set(handshaker);
    }

    public static ChannelHandler forbiddenHttpRequestResponder() {
        return new ChannelInboundHandlerAdapter() {
            @Override
            public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                if (msg instanceof FullHttpRequest) {
                    ((FullHttpRequest) msg).release();
                    FullHttpResponse response =
                            new DefaultFullHttpResponse(HTTP_1_1, HttpResponseStatus.FORBIDDEN);
                    ctx.channel().writeAndFlush(response);
                } else {
                    ctx.fireChannelRead(msg);
                }
            }
        };
    }
}

SecurityServerHandler

用SecurityServerHandler自定义的入站控制器替换原有默认的控制器WebSocketServerProtocolHandshakeHandler
这一步最关键了,因为在这一步就要将头设置进去,前面两步只是为这一步做铺垫,因为netty包中的类不能外部引用也没有提供修改方法,所以才有了上面的自定义类,此类中需要调整握手逻辑,添加握手响应头,然后将WebSocketServerProtocolHandler改为CustomWebSocketServerProtocolHandler,其他的实现类也是一样的去改

public class SecurityServerHandler extends ChannelInboundHandlerAdapter {

    private final String websocketPath;
    private final String subprotocols;
    private final boolean allowExtensions;
    private final int maxFramePayloadSize;
    private final boolean allowMaskMismatch;
    private final boolean checkStartsWith;
	
	  /**
     * 自定义属性 token头key
     */
    private final String tokenHeader;
	/**
     * 自定义属性 token
     */
    private final boolean hasToken;


    public SecurityServerHandler(String websocketPath, String subprotocols,
                                 boolean allowExtensions, int maxFrameSize, boolean allowMaskMismatch, String tokenHeader, boolean hasToken) {
        this(websocketPath, subprotocols, allowExtensions, maxFrameSize, allowMaskMismatch, false,tokenHeader,hasToken);
    }

    SecurityServerHandler(String websocketPath, String subprotocols,
                                            boolean allowExtensions, int maxFrameSize,
                          boolean allowMaskMismatch,
                          boolean checkStartsWith,
                          String tokenHeader,
                          boolean hasToken) {
        this.websocketPath = websocketPath;
        this.subprotocols = subprotocols;
        this.allowExtensions = allowExtensions;
        maxFramePayloadSize = maxFrameSize;
        this.allowMaskMismatch = allowMaskMismatch;
        this.checkStartsWith = checkStartsWith;
        this.tokenHeader = tokenHeader;
        this.hasToken = hasToken;
    }

    @Override
    public void channelRead(final ChannelHandlerContext ctx, Object msg) throws Exception {
        final FullHttpRequest req = (FullHttpRequest) msg;
        if (isNotWebSocketPath(req)) {
            ctx.fireChannelRead(msg);
            return;
        }
        try {
        	// 具体的鉴权逻辑
            HttpHeaders headers = req.headers();
            String token = Objects.requireNonNull(headers.get(tokenHeader));
            if(hasToken){
                // 开启鉴权 认证
                //extracts device information headers
                LoginUser loginUser = SecurityUtils.getLoginUser(token);
                if(null == loginUser){
                    refuseChannel(ctx);
                    return;
                }
                Long userId = loginUser.getUserId();
                //check ......
                SecurityCheckComplete complete = new SecurityCheckComplete(String.valueOf(userId),tokenHeader,hasToken);
                ctx.channel().attr(SECURITY_CHECK_COMPLETE_ATTRIBUTE_KEY).set(complete);
                ctx.fireUserEventTriggered(complete);
            }else {
                // 不开启鉴权 / 认证
                SecurityCheckComplete complete = new SecurityCheckComplete(null,tokenHeader,hasToken);
                ctx.channel().attr(SECURITY_CHECK_COMPLETE_ATTRIBUTE_KEY).set(complete);
            }
            if (req.method() != GET) {
                sendHttpResponse(ctx, req, new DefaultFullHttpResponse(HTTP_1_1, FORBIDDEN));
                return;
            }
            final WebSocketServerHandshakerFactory wsFactory = new WebSocketServerHandshakerFactory(
                    getWebSocketLocation(ctx.pipeline(), req, websocketPath), subprotocols,
                    allowExtensions, maxFramePayloadSize, allowExtensions);
            final WebSocketServerHandshaker handshaker = wsFactory.newHandshaker(req);
            if (handshaker == null) {
                WebSocketServerHandshakerFactory.sendUnsupportedVersionResponse(ctx.channel());
            } else {
            	// 此处将具体的头加入http中,因为这个头会传递个netty底层设置响应头的方法中,默认实现是传的null
                HttpHeaders httpHeaders = new DefaultHttpHeaders().add(tokenHeader,token);
                // 此处便是构造握手相应头的关键步骤
                final ChannelFuture handshakeFuture = handshaker.handshake(ctx.channel(), req,httpHeaders,ctx.channel().newPromise());
                handshakeFuture.addListener((ChannelFutureListener) future -> {
                    if (!future.isSuccess()) {
                        ctx.fireExceptionCaught(future.cause());
                    } else {
                        // Kept for compatibility
                        ctx.fireUserEventTriggered(
                                CustomWebSocketServerProtocolHandler.ServerHandshakeStateEvent.HANDSHAKE_COMPLETE);
                        ctx.fireUserEventTriggered(
                                new CustomWebSocketServerProtocolHandler.HandshakeComplete(
                                        req.uri(), req.headers(), handshaker.selectedSubprotocol()));
                    }
                });
                CustomWebSocketServerProtocolHandler.setHandshaker(ctx.channel(), handshaker);
                ctx.pipeline().replace(this, "WS403Responder",
                        CustomWebSocketServerProtocolHandler.forbiddenHttpRequestResponder());
            }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            req.release();
        }
    }

    public static final class HandshakeComplete {
        private final String requestUri;
        private final HttpHeaders requestHeaders;
        private final String selectedSubprotocol;

        HandshakeComplete(String requestUri, HttpHeaders requestHeaders, String selectedSubprotocol) {
            this.requestUri = requestUri;
            this.requestHeaders = requestHeaders;
            this.selectedSubprotocol = selectedSubprotocol;
        }

        public String requestUri() {
            return requestUri;
        }

        public HttpHeaders requestHeaders() {
            return requestHeaders;
        }

        public String selectedSubprotocol() {
            return selectedSubprotocol;
        }
    }



    private boolean isNotWebSocketPath(FullHttpRequest req) {
        return checkStartsWith ? !req.uri().startsWith(websocketPath) : !req.uri().equals(websocketPath);
    }


    private static void sendHttpResponse(ChannelHandlerContext ctx, HttpRequest req, HttpResponse res) {
        ChannelFuture f = ctx.channel().writeAndFlush(res);
        if (!isKeepAlive(req) || res.status().code() != 200) {
            f.addListener(ChannelFutureListener.CLOSE);
        }
    }

    private static String getWebSocketLocation(ChannelPipeline cp, HttpRequest req, String path) {
        String protocol = "ws";
        if (cp.get(SslHandler.class) != null) {
            // SSL in use so use Secure WebSockets
            protocol = "wss";
        }
        String host = req.headers().get(HttpHeaderNames.HOST);
        return protocol + "://" + host + path;
    }

    private void refuseChannel(ChannelHandlerContext ctx) {
        ctx.channel().writeAndFlush(new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.UNAUTHORIZED));
        ctx.channel().close();
    }

    private static void send100Continue(ChannelHandlerContext ctx,String tokenHeader,String token) {
        DefaultFullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.CONTINUE);
        response.headers().set(tokenHeader,token);
        ctx.writeAndFlush(response);
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        System.out.println("channel 捕获到异常了,关闭了");
        super.exceptionCaught(ctx, cause);
    }
    @Getter
    @AllArgsConstructor
    public static final class SecurityCheckComplete {

        private String userId;

        private String tokenHeader;

        private Boolean hasToken;

    }
}

initChannel方法去初始化自己的实现类

其他的类需要自己实现或者引用,其他的就是无关紧要的,不用去处理的类


 @Override
    protected void initChannel(SocketChannel ch){
        log.info("有新的连接");
        //获取工人所要做的工程(管道器==管道器对应的便是管道channel)
        ChannelPipeline pipeline = ch.pipeline();
        //为工人的工程按顺序添加工序/材料 (为管道器设置对应的handler也就是控制器)
        //1.设置心跳机制
        pipeline.addLast("idle-state",new IdleStateHandler(
                nettyWebSocketProperties.getReaderIdleTime(),
                0,
                0,
                TimeUnit.SECONDS));
        //2.出入站时的控制器,大部分用于针对心跳机制
        pipeline.addLast("change-duple",new WsChannelDupleHandler(nettyWebSocketProperties.getReaderIdleTime()));
        //3.加解码
        pipeline.addLast("http-codec",new HttpServerCodec());
        //3.打印控制器,为工人提供明显可见的操作结果的样式
        pipeline.addLast("logging", new LoggingHandler(LogLevel.INFO));
        pipeline.addLast("aggregator",new HttpObjectAggregator(8192));
        // 将自己的授权handler替换原有的handler
        pipeline.addLast("auth",new SecurityServerHandler(
        		// 此处我是用的yaml配置的,换成自己的即可
                nettyWebSocketProperties.getWebsocketPath(),
                nettyWebSocketProperties.getSubProtocols(),
                nettyWebSocketProperties.getAllowExtensions(),
                nettyWebSocketProperties.getMaxFrameSize(),
                //todo
                false,
                nettyWebSocketProperties.getTokenHeader(),
                nettyWebSocketProperties.getHasToken()
        ));
        pipeline.addLast("http-chunked",new ChunkedWriteHandler());
        // 将自己的协议控制器替换原有的协议控制器
        pipeline.addLast("websocket",
                new CustomWebSocketServerProtocolHandler(
                nettyWebSocketProperties.getWebsocketPath(),
                nettyWebSocketProperties.getSubProtocols(),
                nettyWebSocketProperties.getAllowExtensions(),
                nettyWebSocketProperties.getMaxFrameSize())
        );
        //7.自定义的handler针对业务
        pipeline.addLast("chat-handler",new ChatHandler());
    }

效果截图

在这里插入图片描述

源码跟踪

SecurityServerHandler 调整

调整为自定义请求头解析,但不去替换其他handler

package com.edu.message.handler.security;

import com.edu.common.utils.SecurityUtils;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.handler.codec.http.FullHttpMessage;
import io.netty.handler.codec.http.HttpHeaders;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;

import java.util.Objects;

import static com.edu.message.handler.attributeKey.AttributeKeyUtils.SECURITY_CHECK_COMPLETE_ATTRIBUTE_KEY;

/**
 * @author Administrator
 */
@Slf4j
public class SecurityServerHandler extends ChannelInboundHandlerAdapter {

    private String tokenHeader;

    private Boolean hasToken;

    public SecurityServerHandler(String tokenHeader,Boolean hasToken){
        this.tokenHeader = tokenHeader;
        this.hasToken = hasToken;
    }

    private SecurityServerHandler(){}

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        if(msg instanceof FullHttpMessage){
            FullHttpMessage httpMessage = (FullHttpMessage) msg;
            HttpHeaders headers = httpMessage.headers();
            String token = Objects.requireNonNull(headers.get(tokenHeader));
            if(hasToken){
                // 开启鉴权 认证
                //extracts device information headers
                Long userId = 12345L;//SecurityUtils.getLoginUser(token).getUserId();
                //check ......
                SecurityCheckComplete complete = new SecurityCheckComplete(String.valueOf(userId),tokenHeader,hasToken);
                ctx.channel().attr(SECURITY_CHECK_COMPLETE_ATTRIBUTE_KEY).set(complete);
                ctx.fireUserEventTriggered(complete);
            }else {
                // 不开启鉴权 / 认证
                SecurityCheckComplete complete = new SecurityCheckComplete(null,tokenHeader,hasToken);
                ctx.channel().attr(SECURITY_CHECK_COMPLETE_ATTRIBUTE_KEY).set(complete);
            }
        }
        //other protocols
        super.channelRead(ctx, msg);
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        System.out.println("channel 捕获到异常了,关闭了");
        super.exceptionCaught(ctx, cause);
    }
    @Getter
    @AllArgsConstructor
    public static final class SecurityCheckComplete {

        private String userId;

        private String tokenHeader;

        private Boolean hasToken;

    }
}

initChannel方法调整

改为使用默认实现

@Override
    protected void initChannel(SocketChannel ch){
        log.info("有新的连接");
        //获取工人所要做的工程(管道器==管道器对应的便是管道channel)
        ChannelPipeline pipeline = ch.pipeline();
        //为工人的工程按顺序添加工序/材料 (为管道器设置对应的handler也就是控制器)
        //1.设置心跳机制
        pipeline.addLast("idle-state",new IdleStateHandler(
                nettyWebSocketProperties.getReaderIdleTime(),
                0,
                0,
                TimeUnit.SECONDS));
        //2.出入站时的控制器,大部分用于针对心跳机制
        pipeline.addLast("change-duple",new WsChannelDupleHandler(nettyWebSocketProperties.getReaderIdleTime()));
        //3.加解码
        pipeline.addLast("http-codec",new HttpServerCodec());
        //3.打印控制器,为工人提供明显可见的操作结果的样式
        pipeline.addLast("logging", new LoggingHandler(LogLevel.INFO));
        pipeline.addLast("aggregator",new HttpObjectAggregator(8192));
        pipeline.addLast("auth",new SecurityServerHandler(
                nettyWebSocketProperties.getTokenHeader(),
                nettyWebSocketProperties.getHasToken()
        ));
        pipeline.addLast("http-chunked",new ChunkedWriteHandler());
//        pipeline.addLast("websocket",
//                new CustomWebSocketServerProtocolHandler(
//                nettyWebSocketProperties.getWebsocketPath(),
//                nettyWebSocketProperties.getSubProtocols(),
//                nettyWebSocketProperties.getAllowExtensions(),
//                nettyWebSocketProperties.getMaxFrameSize())
//        );
        pipeline.addLast("websocket",
                new WebSocketServerProtocolHandler(
                nettyWebSocketProperties.getWebsocketPath(),
                nettyWebSocketProperties.getSubProtocols(),
                nettyWebSocketProperties.getAllowExtensions(),
                nettyWebSocketProperties.getMaxFrameSize())
        );
        //7.自定义的handler针对业务
        pipeline.addLast("chat-handler",new ChatHandler());
    }

启动项目–流程截图

断点截图

在这里插入图片描述

1. SecurityServerHandler

第一步走到了自己定义的鉴权控制器(入站控制器),执行channelRead方法
在这里插入图片描述

2.userEventTriggered

自定义业务handler中的事件方法
在这里插入图片描述

3.WebSocketServerProtocolHandshakeHandler

此处便是走到了默认协议控制器的channelRead方法,需要注意handshaker.handshake(ctx.channel(), req) 这个方法,这是处理握手的方法,打个断点进去
在这里插入图片描述

4.WebSocketServerHandshaker

可以看到handshake 方法传的 HttpHeaders是null这里就是核心的握手逻辑可以看到并没有提供相应的头处理器
在这里插入图片描述

5. WebSocketServerHandshaker

newHandshakeResponse(req, responseHeaders) 就是构建响应结果,可以看到头是null
在这里插入图片描述

6. 最后的封装返回

可以看到有回到了自定义handler的业务控制器 中的时间监听方法
在这里插入图片描述
此时只要放行这一步便会在控制台打印出响应头,可以看出并没有设置我们自己的响应头,还是null
在这里插入图片描述
最后统一返回,连接中断,自协议头不一致所导致
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/399971.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

你几乎不知道的浏览器内置对象/事件/ajax

浏览器内置对象/事件/ajax 浏览器是⼀个 JS 的运⾏时环境&#xff0c;它基于 JS 解析器的同时&#xff0c;增加了许多环境相关的内容。⽤⼀张图表示各个运⾏环境和 JS 解析器的关系如下&#xff1a; 我们把常⻅的&#xff0c;能够⽤ JS 这⻔语⾔控制的内容称为⼀个 JS 的运⾏环…

Leetcode—环形链表

前言&#xff1a;给定一个链表&#xff0c;判断是否为循环链表并找环形链表的入口点 首先我们需要知道什么是双向循环链表&#xff0c;具体如下图所示。 对于链表&#xff0c;我们如何去判断链表是循环链表呢&#xff1f;又寻找入环点呢&#xff1f;我们可以利用快慢指针的方法…

Meta CTO:Quest 2生命周期或比预期更久

前不久&#xff0c;Meta未来4年路线图遭曝光&#xff0c;泄露了该公司正在筹备中的一些AR/VR原型。除此之外&#xff0c;还有消息称Quest Pro或因销量不佳&#xff0c;而不再迭代。毫无疑问&#xff0c;Meta的一举一动持续受到行业关注&#xff0c;而面对最近的爆料&#xff0c…

关于指针运算的一道题

目录 刚看到这道题的时候我也和大多数小白一样感到无从下手&#xff0c;但是在我写这篇博客的前几分钟开始我对这道题有了一点点的理解。所以我就想着趁热打铁&#xff0c;写一篇博客来记录一下我的想法。 题目如下&#xff1a; 画图&#xff1a; 逐一解答&#xff1a; 题一…

笔记:二叉树

学习了二叉树&#xff0c;今天来整理一下笔记吧&#xff01;一&#xff1a;树的理解树的度是不限制的&#xff0c;一个双亲结点可以有很多子节点&#xff0c;但是子节点是不能交叉的&#xff0c;也就是不能有环。树的的最大层次叫做这棵树的深度或者高度&#xff1b;树的代码表…

高三应该怎么复习

高三是学生们备战高考的重要一年&#xff0c;正确有序的复习可以有效地提高复习效率&#xff0c;下面是一些高效复习的方法和建议&#xff1a;1. 制定合理的学习计划和目标高三的学生要制定合理的学习计划和目标&#xff0c;适当的计划和目标可以使学习更有针对性和效率。建议根…

本地部署dynamics

打开虚拟机的dynamics部署管理&#xff0c;点击组织再点击浏览 进入网址&#xff0c;输入账号密码 登录成功 点销售旁边的下箭头&#xff0c;选择设置中的系统作业 成功进入到系统作业 进入这个网址 Dynamics 365 客户参与快速入门&#xff08;本地&#xff09;&#xff08;Dy…

VRRP与BFD联动配置

VRRP与BFD联动配置 1. 实验目的 熟悉VRRP与BFD联动的应用场景掌握VRRP与BFD联动的配置方法2. 实验拓扑 实验拓扑如图14-13所示: 图14-13:VRRP与BFD联动配置 3. 实验步骤 配置IP地址PC1的配置 PC1的配置如图14-14所示:

Python 协程详解,都在这里了

什么是协程 协程&#xff08;co-routine&#xff0c;又称微线程、纤程&#xff09; 是一种多方协同的工作方式。 协程不是进程或线程&#xff0c; 其执行过程类似于 Python 函数调用&#xff0c; Python 的 asyncio 模块实现的异步IO编程框架中&#xff0c; 协程是对使用 asy…

毕设常用模块之舵机介绍以及使用方法

舵机 舵机是一种位置伺服的驱动器&#xff0c;主要是由外壳、电路板、无核心马达、齿轮与位置检测器所构成。其工作原理是由接收机或者单片机发出信号给舵机&#xff0c;其内部有一个基准电路&#xff0c;产生周期为 20ms&#xff0c;宽度为 1.5ms 的基准信号&#xff0c;将获…

易优cms user 登录注册标签

user 登录注册标签 user 登录注册入口标签 [基础用法] 标签&#xff1a;user 描述&#xff1a;动态显示购物车、登录、注册、退出、会员中心的入口&#xff1b; 用法&#xff1a; {eyou:user typeuserinfo} <div id"{$field.htmlid}"> …

如何用项目管理软件,帮助项目经理监控进度?

项目无论规模大小&#xff0c;都要处理许多任务&#xff0c;管理项目文档&#xff0c;监控任务进度等。 有一个方法可以帮助项目经理在制定计划和项目推进时确保一切保持井井有条。 项目管理软件是最有用的工具之一&#xff0c;通常被用于项目计划、时间管理等&#xff0c;能在…

我国近视眼的人数已经超过了六亿,国老花眼人数超过三亿人

眼镜是一种用于矫正视力问题、改善视力、减轻眼睛疲劳的光学器件&#xff0c;在我们的生活中不可忽略的一部分&#xff0c;那么我国眼镜市场发展情况是怎样了&#xff1f;下面小编通过可视化互动平台对我国眼镜市场的状况进行分析。我国是一个近视眼高发的国家&#xff0c;据统…

【MFA】windows环境下,使用Montreal-Forced-Aligner训练并对齐音频

文章目录一、安装MFA1.安装anaconda2.创建并进入虚拟环境3.安装pyTorch二、训练新的声学模型1.确保数据集的格式正确2.训练声音模型-导出模型和对齐文件3.报错处理1.遇到类似&#xff1a; Command ‘[‘createdb’,–host‘ ’, ‘Librispeech’]’ returned non-zero exit sta…

我一个普通程序员,光靠GitHub打赏就年入70万,

一个国外程序员名叫 Caleb Porzio在网上公开了自己用GitHub打赏年入70万的消息和具体做法。 Caleb Porzio 发推庆祝自己靠 GitHub 打赏&#xff08;GitHub Sponsors&#xff09;赚到了 10 万美元。 GitHub Sponsors是 GitHub 2019 年 5 月份推出的一个功能&#xff0c;允许开发…

SpringBatch简介

参考&#xff1a;https://cloud.tencent.com/developer/article/1456757简介SpringBatch主要是一个轻量级的大数据量的并行处理(批处理)的框架。作用和Hadoop很相似&#xff0c;不过Hadoop是基于重量级的分布式环境(处理巨量数据)&#xff0c;而SpringBatch是基于轻量的应用框架…

mac安装vue脚手架失败及解决方法

大家好&#xff0c;这里是 一口八宝周 &#x1f44f;欢迎来到我的博客 ❤️一起交流学习文章中有需要改进的地方请大佬们多多指点 谢谢 &#x1f64f;最近想学前端的心又开始躁动了&#xff0c;于是说干就干&#xff0c;先搞个vue脚手架谁知道上来就失败了说说我的步骤吧&#…

2017年MathorCup数学建模A题流程工业的智能制造解题全过程文档及程序

2017年第七届MathorCup高校数学建模挑战赛 A题 流程工业的智能制造 原题再现&#xff1a; “中国制造 2025”是我国制造业升级的国家大战略。其技术核心是智能制造&#xff0c;智能化程度相当于“德国工业 4.0”水平。“中国制造 2025”的重点领域既包含重大装备的制造业&…

mybatis小demo讲解(详细demo版)

这篇是mybatis的demo演示版噢&#xff0c;如果要了解理论的可以参考这篇哈mybatis从入门到精通好了&#xff0c;我们开始咯 MyBatis小demo1.简单的mybatis小案例1. 创建项目、准备环境2. mybatis的两种实现方式2.1 映射文件Mapper.xml实现1.简单的mybatis小案例 1. 创建项目、…

ESP8266与手机App通信(STM32)

认识模块 ESP8266是一种低成本的Wi-Fi模块&#xff0c;可用于连接物联网设备&#xff0c;控制器和传感器等。它具有小巧、高度集成和低功耗的特点&#xff0c;因此在物联网应用中被广泛使用。ESP8266模块由Espressif Systems开发&#xff0c;具有单芯片的封装和多种功能&#x…