【stomp 实战】spring websocket源码分析之握手请求的处理

上一节【搭建一套websocket推送平台】我们通过一个项目,实现了一套推送平台。由于spring框架对于websocket的支持和stomp协议的良好封装,我们很容易地就实现了websocket的消息推送功能。虽然搭建这么一套推送系统不难,但是如果不了解其底层原理,当出现问题时,我们就比较痛苦了。这次我们就来分析一下这块的源码。

一、WebSocket 握手过程

1.1 客户端握手请求

客户端发起 WebSocket 握手流程。客户端发送带有如下请求头的标准 HTTP 请求(HTTP 版本必须是 1.1 或更高,并且请求方法必须是 GET):

GET /chat HTTP/1.1
Host: example.com:8000
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13

1.2 服务端握手响应

当服务端收到握手请求时,将发送一个特殊响应,该响应表明协议将从 HTTP 变更为 WebSocket。

该响应头大致如下(记住,每个响应头行以 \r\n 结尾,在最后一行的后面添加额外的 \r\n,以说明响应头结束):

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

二、源码分析

上面就是握手的大概请求和响应报文。通过握手,客户端和服务端就可以建立连接了。
我们来看一下源码中是如何实现的。
整个过程我总结成了一个流程图,对照着这个流程图,我们再来一步步分析代码,避免在源码中迷路

2.1 流程图

在这里插入图片描述

2.2 把请求交给对应的处理器

如果你看过Spring-MVC的代码,你一定对DispatcherServlet有一定印象。这个Servlet是所有http请求的入口,所有的http请求都会经过它。

	protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {// 通过请求找到handlerAdapterHandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());。。。略//用这个handlerAdapter来执行请求处理mv = ha.handle(processedRequest, response, mappedHandler.getHandler());...

省略大量代码后,实际上主要做了两件事

  • 通过请求找到handlerAdapter。握手报文进来后,找到的是HttpRequestHandlerAdapter
  • 这个handlerAdapter来执行请求处理

HttpRequestHandlerAdapter又将处理SockJsHttpRequestHandler处理。最终是DefaultSockJsService来处理请求。


//SockJsHttpRequestHandler代码@Overridepublic void handleRequest(HttpServletRequest servletRequest, HttpServletResponse servletResponse)throws ServletException, IOException {ServerHttpRequest request = new ServletServerHttpRequest(servletRequest);ServerHttpResponse response = new ServletServerHttpResponse(servletResponse);try {//DefaultSockJsService.handleRequestthis.sockJsService.handleRequest(request, response, getSockJsPath(servletRequest), this.webSocketHandler);}catch (Exception ex) {throw new SockJsException("Uncaught failure in SockJS request, uri=" + request.getURI(), ex);}}

进入this.sockJsService.handleRequest,会由(DefaultSockJsService的父类)AbstractSockJsService.handleRequest来处理请求
下面的代码有点长,完全理解有难度。还是抓重点

	public final void handleRequest(ServerHttpRequest request, ServerHttpResponse response,@Nullable String sockJsPath, WebSocketHandler wsHandler) throws SockJsException {if (sockJsPath == null) {if (logger.isWarnEnabled()) {logger.warn(LogFormatUtils.formatValue("Expected SockJS path. Failing request: " + request.getURI(), -1, true));}response.setStatusCode(HttpStatus.NOT_FOUND);return;}try {request.getHeaders();}catch (InvalidMediaTypeException ex) {// As per SockJS protocol content-type can be ignored (it's always json)}String requestInfo = (logger.isDebugEnabled() ? request.getMethod() + " " + request.getURI() : null);try {if (sockJsPath.isEmpty() || sockJsPath.equals("/")) {if (requestInfo != null) {logger.debug("Processing transport request: " + requestInfo);}if ("websocket".equalsIgnoreCase(request.getHeaders().getUpgrade())) {response.setStatusCode(HttpStatus.BAD_REQUEST);return;}response.getHeaders().setContentType(new MediaType("text", "plain", StandardCharsets.UTF_8));response.getBody().write("Welcome to SockJS!\n".getBytes(StandardCharsets.UTF_8));}else if (sockJsPath.equals("/info")) {if (requestInfo != null) {logger.debug("Processing transport request: " + requestInfo);}this.infoHandler.handle(request, response);}else if (sockJsPath.matches("/iframe[0-9-.a-z_]*.html")) {if (!getAllowedOrigins().isEmpty() && !getAllowedOrigins().contains("*") ||!getAllowedOriginPatterns().isEmpty()) {if (requestInfo != null) {logger.debug("Iframe support is disabled when an origin check is required. " +"Ignoring transport request: " + requestInfo);}response.setStatusCode(HttpStatus.NOT_FOUND);return;}if (getAllowedOrigins().isEmpty()) {response.getHeaders().add(XFRAME_OPTIONS_HEADER, "SAMEORIGIN");}if (requestInfo != null) {logger.debug("Processing transport request: " + requestInfo);}this.iframeHandler.handle(request, response);}else if (sockJsPath.equals("/websocket")) {if (isWebSocketEnabled()) {if (requestInfo != null) {logger.debug("Processing transport request: " + requestInfo);}handleRawWebSocketRequest(request, response, wsHandler);}else if (requestInfo != null) {logger.debug("WebSocket disabled. Ignoring transport request: " + requestInfo);}}else {String[] pathSegments = StringUtils.tokenizeToStringArray(sockJsPath.substring(1), "/");if (pathSegments.length != 3) {if (logger.isWarnEnabled()) {logger.warn(LogFormatUtils.formatValue("Invalid SockJS path '" + sockJsPath + "' - " +"required to have 3 path segments", -1, true));}if (requestInfo != null) {logger.debug("Ignoring transport request: " + requestInfo);}response.setStatusCode(HttpStatus.NOT_FOUND);return;}String serverId = pathSegments[0];String sessionId = pathSegments[1];String transport = pathSegments[2];if (!isWebSocketEnabled() && transport.equals("websocket")) {if (requestInfo != null) {logger.debug("WebSocket disabled. Ignoring transport request: " + requestInfo);}response.setStatusCode(HttpStatus.NOT_FOUND);return;}else if (!validateRequest(serverId, sessionId, transport) || !validatePath(request)) {if (requestInfo != null) {logger.debug("Ignoring transport request: " + requestInfo);}response.setStatusCode(HttpStatus.NOT_FOUND);return;}if (requestInfo != null) {logger.debug("Processing transport request: " + requestInfo);}handleTransportRequest(request, response, wsHandler, sessionId, transport);}response.close();}catch (IOException ex) {throw new SockJsException("Failed to write to the response", null, ex);}}

上面的代码很长,阅读起来不容易,可以通过debug的方式看下整个流程。这里就不细讲了,重点的一个方法
TransportHandlingSockJsService.handleTransportRequest(request, response, wsHandler, sessionId, transport);
代码如下,主要流程见代码注释

	@Overrideprotected void handleTransportRequest(ServerHttpRequest request, ServerHttpResponse response,WebSocketHandler handler, String sessionId, String transport) throws SockJsException {//这个值是WebsocketTransportType transportType = TransportType.fromValue(transport);if (transportType == null) {if (logger.isWarnEnabled()) {logger.warn(LogFormatUtils.formatValue("Unknown transport type for " + request.getURI(), -1, true));}response.setStatusCode(HttpStatus.NOT_FOUND);return;}//这里取到的是WebSocketTransportHandlerTransportHandler transportHandler = this.handlers.get(transportType);if (transportHandler == null) {if (logger.isWarnEnabled()) {logger.warn(LogFormatUtils.formatValue("No TransportHandler for " + request.getURI(), -1, true));}response.setStatusCode(HttpStatus.NOT_FOUND);return;}SockJsException failure = null;//构造一个拦截链,我们可以注册自己的拦截器,这样就可以在握手阶段来注入我们自己的业务逻辑,比如报文校验等HandshakeInterceptorChain chain = new HandshakeInterceptorChain(this.interceptors, handler);try {HttpMethod supportedMethod = transportType.getHttpMethod();if (supportedMethod != request.getMethod()) {if (request.getMethod() == HttpMethod.OPTIONS && transportType.supportsCors()) {if (checkOrigin(request, response, HttpMethod.OPTIONS, supportedMethod)) {response.setStatusCode(HttpStatus.NO_CONTENT);addCacheHeaders(response);}}else if (transportType.supportsCors()) {sendMethodNotAllowed(response, supportedMethod, HttpMethod.OPTIONS);}else {sendMethodNotAllowed(response, supportedMethod);}return;}//会话的创建SockJsSession session = this.sessions.get(sessionId);boolean isNewSession = false;if (session == null) {if (transportHandler instanceof SockJsSessionFactory) {Map<String, Object> attributes = new HashMap<>();//拦截链的前置处理if (!chain.applyBeforeHandshake(request, response, attributes)) {return;}SockJsSessionFactory sessionFactory = (SockJsSessionFactory) transportHandler;session = createSockJsSession(sessionId, sessionFactory, handler, attributes);isNewSession = true;}else {response.setStatusCode(HttpStatus.NOT_FOUND);if (logger.isDebugEnabled()) {logger.debug("Session not found, sessionId=" + sessionId +". The session may have been closed " +"(e.g. missed heart-beat) while a message was coming in.");}return;}}else {Principal principal = session.getPrincipal();if (principal != null && !principal.equals(request.getPrincipal())) {logger.debug("The user for the session does not match the user for the request.");response.setStatusCode(HttpStatus.NOT_FOUND);return;}if (!transportHandler.checkSessionType(session)) {logger.debug("Session type does not match the transport type for the request.");response.setStatusCode(HttpStatus.NOT_FOUND);return;}}if (transportType.sendsNoCacheInstruction()) {addNoCacheHeaders(response);}if (transportType.supportsCors() && !checkOrigin(request, response)) {return;}//这里是核心的处理逻辑transportHandler.handleRequest(request, response, handler, session);if (isNewSession && (response instanceof ServletServerHttpResponse)) {int status = ((ServletServerHttpResponse) response).getServletResponse().getStatus();if (HttpStatus.valueOf(status).is4xxClientError()) {this.sessions.remove(sessionId);}}//拦截链的后置处理chain.applyAfterHandshake(request, response, null);}catch (SockJsException ex) {failure = ex;}catch (Exception ex) {failure = new SockJsException("Uncaught failure for request " + request.getURI(), sessionId, ex);}finally {if (failure != null) {chain.applyAfterHandshake(request, response, failure);throw failure;}}}

总结起来有几个过程

  • 先取到一个hander,这里取到的是WebSocketTransportHandler
  • 构造一个拦截链,我们可以注册自己的拦截器,这样就可以在握手阶段来注入我们自己的业务逻辑,比如报文校验等
  • 拦截链的前置处理
  • 创建一个用户会话,握手时,肯定会话是空的,得建一个会话session
  • 处理器hander处理核心逻辑:transportHandler.handleRequest(request, response, handler, session);。这里后面详细写
  • 拦截链的后置处理

transportHandler.handleRequest(request, response, handler, session); 到底做了啥

	public void handleRequest(ServerHttpRequest request, ServerHttpResponse response,WebSocketHandler wsHandler, SockJsSession wsSession) throws SockJsException {WebSocketServerSockJsSession sockJsSession = (WebSocketServerSockJsSession) wsSession;try {wsHandler = new SockJsWebSocketHandler(getServiceConfig(), wsHandler, sockJsSession);//握手处理器握手this.handshakeHandler.doHandshake(request, response, wsHandler, sockJsSession.getAttributes());}catch (Exception ex) {sockJsSession.tryCloseWithSockJsTransportError(ex, CloseStatus.SERVER_ERROR);throw new SockJsTransportFailureException("WebSocket handshake failure", wsSession.getId(), ex);}}

终于看到了一个握手处理器,handshakeHandler。先不看代码,猜测一下,这里的作用应该是,构造一个握手返回报文,然后通过response写回给客户端。然后告知web容器tomcat,当前请求升级为websocket了。这样,浏览器后面就可以发送websockdet消息了。
握手的逻辑代码,这里不是我们主要研究的点,就不再细讲了。
升级成功后,tomcat有个回调方法,然后再进行一系列的初始化动作
在这里插入图片描述
上面的代码是tomcat的代码,可以看到红框中的StandardWebSocketHandlerAdapter。这里进行一第列的初始化动作

2.3 websocket初始化

StandardWebSocketHandlerAdapter.onOpen是入口,由Tomcat回调。

	public void onOpen(final javax.websocket.Session session, EndpointConfig config) {this.wsSession.initializeNativeSession(session);// The following inner classes need to remain since lambdas would not retain their// declared generic types (which need to be seen by the underlying WebSocket engine)if (this.handler.supportsPartialMessages()) {session.addMessageHandler(new MessageHandler.Partial<String>() {@Overridepublic void onMessage(String message, boolean isLast) {handleTextMessage(session, message, isLast);}});session.addMessageHandler(new MessageHandler.Partial<ByteBuffer>() {@Overridepublic void onMessage(ByteBuffer message, boolean isLast) {handleBinaryMessage(session, message, isLast);}});}else {session.addMessageHandler(new MessageHandler.Whole<String>() {@Overridepublic void onMessage(String message) {handleTextMessage(session, message, true);}});session.addMessageHandler(new MessageHandler.Whole<ByteBuffer>() {@Overridepublic void onMessage(ByteBuffer message) {handleBinaryMessage(session, message, true);}});}session.addMessageHandler(new MessageHandler.Whole<javax.websocket.PongMessage>() {@Overridepublic void onMessage(javax.websocket.PongMessage message) {handlePongMessage(session, message.getApplicationData());}});try {this.handler.afterConnectionEstablished(this.wsSession);}catch (Exception ex) {ExceptionWebSocketHandlerDecorator.tryCloseWithError(this.wsSession, ex, logger);}}

代码总结:

  • 这里入参传了一个javax.websocket.Session。这个可以理解为当前Websocket连接。
  • 原来这个Session可以给自己添加messageHandler,那当有消息来的时候,就会经过这些handler来进行处理。
  • 那这个hander就是处理业务消息的重点了
    看一下这个hander是怎么处理消息的
private void handleTextMessage(javax.websocket.Session session, String payload, boolean isLast) {TextMessage textMessage = new TextMessage(payload, isLast);try {this.handler.handleMessage(this.wsSession, textMessage);}catch (Exception ex) {ExceptionWebSocketHandlerDecorator.tryCloseWithError(this.wsSession, ex, logger);}}

这个handler,对应的实现是:SockJsWebSocketHandler
进入handleMessage看一下处理逻辑,原来是将消息分为三类

  • 文本消息
  • 二进制消息
  • 心跳消息
    这三种消息,分别进行处理
@Overridepublic void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception {if (message instanceof TextMessage) {handleTextMessage(session, (TextMessage) message);}else if (message instanceof BinaryMessage) {handleBinaryMessage(session, (BinaryMessage) message);}else if (message instanceof PongMessage) {handlePongMessage(session, (PongMessage) message);}else {throw new IllegalStateException("Unexpected WebSocket message type: " + message);}}

我们一般处理的是文本消息

	@Overridepublic void handleTextMessage(WebSocketSession wsSession, TextMessage message) throws Exception {this.sockJsSession.handleMessage(message, wsSession);}

又交给sockJsSession来处理消息。
消息的处理过程,我们暂且不表。下节再来分析。

this.handler.afterConnectionEstablished(this.wsSession);

	public void initializeDelegateSession(WebSocketSession session) {synchronized (this.initSessionLock) {this.webSocketSession = session;try {// Let "our" handler know before sending the open frame to the remote handlerdelegateConnectionEstablished();this.webSocketSession.sendMessage(new TextMessage(SockJsFrame.openFrame().getContent()));// Flush any messages cached in the meantimewhile (!this.initSessionCache.isEmpty()) {writeFrame(SockJsFrame.messageFrame(getMessageCodec(), this.initSessionCache.poll()));}scheduleHeartbeat();this.openFrameSent = true;}catch (Exception ex) {tryCloseWithSockJsTransportError(ex, CloseStatus.SERVER_ERROR);}}}

这里的逻辑如下

  • delegateConnectionEstablished。让我们的hander知晓,当前Websocket连接已经建立了,这是个回调方法
  • 发送一个websocket open报文给客户端
  • 开启websocket心跳线程

代码就分析完毕了,结合最开始的流程图,可以自己再debug一下加深印象。

三、总结

整个握手过程包含以下关键步骤

  • 通过http请求,找到对应的握手的处理器
  • 握手处理器将websocket握手成功的返回报文发送给客户端
  • web容器回调自身,告知协议升级
  • 注册消息处理器,当有websocket消息来时,就会回调处理器进行消息的逻辑处理
  • 初始化事件,包括发送一个open报文给客户端,开启Websocket心跳线程等

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

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

相关文章

Spring Cloud Alibaba Sentinel 使用

初识Sentinel Sentinel是阿里巴巴开源的一款微服务流量控制组件。官网地址&#xff1a; home | Sentinel 需要了解的概念 簇点链路 在学习 Sentinel 的使用之前&#xff0c;我们有必要首先了解一下簇点链路。当请求进入微服务时&#xff0c;首先会访Controller、Service、Ma…

赋能智慧校园!A3D数字孪生可视化,轻量又高效!

放假之后&#xff0c;学生们会逐步返学&#xff0c;大量人员出入校园&#xff0c;安全更是不容忽视&#xff0c;如何在短时间内对大批人员及设施进行智能监管&#xff1f;数字化转型是关键手段&#xff0c;我们可以融合线上线下数据&#xff0c;搭建3D立体的智慧校园&#xff0…

Unity 按下Play键后,Scene View里面一切正常,但是Game View中什么都没有 -- Camera Clear Flags的设置

问题如下所示。 最先遇到这个问题是我想用Unity开发一个VR 360-degree Image Viewer。在Scene View中可以看到球体&#xff0c;但是Game View什么都看不到。最后找到的原因是&#xff0c;我使用的shader是Skybox/Panorama&#xff0c; 需要把Main Camera的Clear Flags设置成Do…

hanoi塔

hanoi塔问题&#xff1a; 1.规则&#xff1a;一次移动一个盘子&#xff0c;小盘子压大盘子上面&#xff0c;有A、B、C三个柱子&#xff0c;A是起始放盘子的柱子&#xff0c;B是中间可以借助的柱子&#xff0c;C是最后放盘子的位置 2.简单思路&#xff1a; 如果有1个盘子&…

netsh int ipv4 show dynamicport tcp动态端口port设置

netsh int ipv4 show dynamicport tcp netsh int ipv4 set dynamicport tcp start4000 num10000

【Go语言快速上手(四)】面向对象的三大特性引入

&#x1f493;博主CSDN主页:杭电码农-NEO&#x1f493;   ⏩专栏分类:Go语言专栏⏪   &#x1f69a;代码仓库:NEO的学习日记&#x1f69a;   &#x1f339;关注我&#x1faf5;带你学习更多Go语言知识   &#x1f51d;&#x1f51d; GO快速上手 1. 前言2. 初识GO中的结构…

JAVA毕业设计136—基于Java+Springboot+Vue的房屋租赁管理系统(源代码+数据库)

毕设所有选题&#xff1a; https://blog.csdn.net/2303_76227485/article/details/131104075 基于JavaSpringbootVue的房屋租赁管理系统(源代码数据库)136 一、系统介绍 本项目前后端分离&#xff0c;分为管理员、用户、工作人员、房东四种角色 1、用户/房东&#xff1a; …

【网络安全】网络安全协议和防火墙

目录 1、网络层的安全协议&#xff1a;IPsec 协议族 &#xff08;1&#xff09;IP 安全数据报格式 &#xff08;2&#xff09;互联网密钥交换 IKE (Internet Key Exchange) 协议 2、运输层的安全协议&#xff1a;TLS 协议 3、系统安全&#xff1a;防火墙与入侵检测 1、网络…

数据结构篇其二---单链表(C语言+超万字解析)

目录 前言&#xff1a; 一、顺序表的缺点和链表的引入。 二、链表概述 实现一个简单的链表 空链表的概念 三、链表的功能实现 链表的打印 链表节点的创建 链表的头插&#xff08;自上而下看完分析&#xff0c;相信你会有所收获&#xff09; 头插的前置分析 传值调用和…

OSPF的LSA与特殊区域

Area区域概念 *一个区域维护一张LSDB&#xff0c;路由器详细的链路信息只在这个区域内传播 不是每一台路由器都需要了解所有外部目的地的详细信息 *OSPF网络的层次化设计 通过区域ID标识 骨干&#xff08; Backbone &#xff09;区域&#xff0c;必须是area 0(骨干区域…

Dos慢速攻击

这里写自定义目录标题 Dos慢速攻击 Dos慢速攻击 测试结果为“Exit status&#xff1a; No open connections left"&#xff0c;代表无此漏洞。 如果测试结束后connected数量较多&#xff0c;closed数量很少或0&#xff0c;说明之前建立的慢速攻击测试连接没有关闭&#…

书生·浦语 大模型(学习笔记-7)LMDeploy 量化部署 LLM-VLM 实践

目录 一、模型的部署 二、模型部署面临的问题 三、如何解决&#xff08;两种方法&#xff09; 四、LMDeploy相关知识 创建conda环境(漫长的等待) 五、使用LMDeploy与模型对话 六、设置最大KV Cache缓存大小 七、W4A16量化 八、客户端连接API服务器 一、模型的部署 二、…

NLP step by step -- 了解Transformer

Transformer模型 Transformer相关历史 首先我们先看一下有关Transformer模型的发展历史&#xff0c;下面的图是基于Transformer架构的一些关键模型节点&#xff1a; 图片来源于Hugging Face 图片来源于Hugging Face Transformer 架构 于 2017 年 6 月推出。原本研究的重点是…

Java面试八股之Java中为什么没有全局变量

Java中为什么没有全局变量 Java中没有传统意义上的全局变量&#xff0c;这是因为Java语言设计遵循面向对象的原则&#xff0c;强调封装性和模块化&#xff0c;以及避免全局状态带来的副作用。 封装性&#xff1a; 全局变量违反了面向对象编程中的封装原则&#xff0c;即隐藏对…

Spring Boot集成RabbitMQ快速入门Demo

1.什么是RabbitMQ&#xff1f; RabbitMQ是一款使用Erlang语言开发的&#xff0c;基于AMQP协议的消息中间件&#xff0c;作为一款优秀的消息系统&#xff0c;RabbitMQ有高并发、可扩展等优势&#xff0c;并适用于大型系统中各个模块之间的通信。 RabbitMQ的特点为&#xff1a; 持…

【白盒测试】单元测试的理论基础及用例设计技术(6种)详解

目录 &#x1f31e;前言 &#x1f3de;️1. 单元测试的理论基础 &#x1f30a;1.1 单元测试是什么 &#x1f30a;1.2 单元测试的好处 &#x1f30a;1.3 单元测试的要求 &#x1f30a;1.4 测试框架-Junit4的介绍 &#x1f30a;1.5 单元测试为什么要mock &#x1f3de;️…

《前端面试题》- React - 如何区分函数组件和类组件

问题 如何区分函数组件和类组件&#xff1f; 答案 可以使用instanceof 或者Component.prototype.isReactComponent。 示例 函数组件 export default function FunctionComonent() {if(FunctionComonent.prototype.isReactComponent){console.log(FunctionComonent是类组件…

prompt提示词:AI英语词典优化版Pro,让AI教你学英语,通过AI实现一个网易有道英语词典

目录 一、前言二、效果对比三、优化《AI英语词典》提示词四、其他获奖作品链接 一、前言 不可思议&#xff01;我的AI有道英语字典助手竟然与百度千帆AI应用创意挑战赛K12教育主题赛榜首作品差之毫厘 &#xff0c;真的是高手都是惺惺相惜的&#xff0c;哈哈&#xff0c;自恋一…

docker 集群管理实战mesos+zookeeper+marathon(一)

一 实验环境 1.1 系统版本&#xff0c;本实验使用cnetos7.9版本镜像 1.2 准备5台虚拟机&#xff0c;其中3台master&#xff0c;两台slave&#xff0c;使用克隆的方式 1.3 使用远程连接工具登录 1.4 修改主机名 1.5 设置域名映射 每个虚拟机都配置一下&#xff0c;这里就演示一…

SN74LV1T125DBVR SN74LV1T125单电源单缓冲门,带三态输出CMOS逻辑电平转换器

SN74LV1T125DBVR 规格信息&#xff1a; 制造商:Texas Instruments 产品品种:转换 - 电压电平 RoHS:是 电源电压-最大:5.5 V 电源电压-最小:1.6 V 最小作业温度:- 40 C 最大作业温度: 125 C 安装风格:SMD/SMT 封装 / 箱体:SOT-23-5 封装:Cut Tape 封装:MouseReel 封装…