【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 {
				// 通过请求找到handlerAdapter
				HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
		。。。略
				//用这个handlerAdapter来执行请求处理
				mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
	...

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

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

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


//SockJsHttpRequestHandler代码
	@Override
	public void handleRequest(HttpServletRequest servletRequest, HttpServletResponse servletResponse)
			throws ServletException, IOException {

		ServerHttpRequest request = new ServletServerHttpRequest(servletRequest);
		ServerHttpResponse response = new ServletServerHttpResponse(servletResponse);

		try {
		//DefaultSockJsService.handleRequest
			this.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);
代码如下,主要流程见代码注释

	@Override
	protected void handleTransportRequest(ServerHttpRequest request, ServerHttpResponse response,
			WebSocketHandler handler, String sessionId, String transport) throws SockJsException {
		//这个值是Websocket
		TransportType 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;
		}
		//这里取到的是WebSocketTransportHandler
		TransportHandler 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>() {
				@Override
				public void onMessage(String message, boolean isLast) {
					handleTextMessage(session, message, isLast);
				}
			});
			session.addMessageHandler(new MessageHandler.Partial<ByteBuffer>() {
				@Override
				public void onMessage(ByteBuffer message, boolean isLast) {
					handleBinaryMessage(session, message, isLast);
				}
			});
		}
		else {
			session.addMessageHandler(new MessageHandler.Whole<String>() {
				@Override
				public void onMessage(String message) {
					handleTextMessage(session, message, true);
				}
			});
			session.addMessageHandler(new MessageHandler.Whole<ByteBuffer>() {
				@Override
				public void onMessage(ByteBuffer message) {
					handleBinaryMessage(session, message, true);
				}
			});
		}

		session.addMessageHandler(new MessageHandler.Whole<javax.websocket.PongMessage>() {
			@Override
			public 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看一下处理逻辑,原来是将消息分为三类

  • 文本消息
  • 二进制消息
  • 心跳消息
    这三种消息,分别进行处理
@Override
	public 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);
		}
	}

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

	@Override
	public 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 handler
				delegateConnectionEstablished();
				this.webSocketSession.sendMessage(new TextMessage(SockJsFrame.openFrame().getContent()));

				// Flush any messages cached in the meantime
				while (!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.mfbz.cn/a/574431.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Linux中手工创建一个用户

当我们需要新创建一个用户时&#xff0c;有两种方法 1.使用命令添加用户 2.去配置文件里面添加用户 1&#xff0c;使用useradd命令&#xff1a; [rootlocalhost /]# useradd tmg 然后给它设置一个密码 [rootlocalhost etc]# passwd tmg Changing password for user tmg. N…

linux 系统文件目录颜色及特殊权限对应的颜色

什么决定文件目录的颜色和背景&#xff1f; 颜色 说明 栗子 权限白色表示普通文件 蓝色表示目录 绿色表示可执行文件 浅蓝色链接文件 黄色表示设备文件 红色 表示压缩文件 红色闪烁表示链接的文件有问题 灰色 表示其它文件 可以用字符表示文件的类型&am…

新风口下的必应bing国内广告投放该怎么做?

必应Bing作为全球搜索引擎市场的重要参与者&#xff0c;正逐渐显现出其在国内市场的独特价值和潜力。随着互联网生态的多元化发展&#xff0c;必应Bing凭借其高质量用户群和精准投放能力&#xff0c;成为了企业寻求新增长点的新风口。 一、洞察先机&#xff0c;精准定位市场 …

【Flink入门修炼】2-3 Flink Checkpoint 原理机制

如果让你来做一个有状态流式应用的故障恢复&#xff0c;你会如何来做呢&#xff1f; 单机和多机会遇到什么不同的问题&#xff1f; Flink Checkpoint 是做什么用的&#xff1f;原理是什么&#xff1f; 一、什么是 Checkpoint&#xff1f; Checkpoint 是对当前运行状态的完整记…

YOLOv8+PyQt5输电线路缺陷检测(目前最全面的类别检测,可以从图像、视频和摄像头三种路径检测)

1.效果视频&#xff1a;YOLOv8PyQt5输电线路缺陷检测&#xff08;目前最全面的类别检测&#xff0c;可以从图像、视频和摄像头三种路径检测&#xff09;_哔哩哔哩_bilibili 资源包含可视化的输电线路缺陷检测系统&#xff0c;可识别图片和视频当中出现的五类常见的输电线路缺陷…

web前端学习笔记2

2. 网页穿上美丽外衣 2.1 什么是CSS CSS (Cascading Style Sheets,层叠样式表),是一种用来为结构化文档(如 HTML 文档或 XML 应用)添加样式(字体、间距和颜色等)的计算机语言,CSS 文件扩展名为 .css。 CSS样式包括对字体、颜色、边距、高度、宽度、背景图片、网页定位…

4.8 Python dict字典

Python dict字典详解 Python字典&#xff08;dict&#xff09;是一种无序的、可变的序列&#xff0c;它的元素以“键值对&#xff08;key-value&#xff09;”的形式存储。相对地&#xff0c;列表&#xff08;list&#xff09;和元组&#xff08;tuple&#xff09;都是有序的序…

Python中ArcPy按照分幅条带与成像日期拼接每个8天间隔内的遥感影像

本文介绍基于Python中的ArcPy模块&#xff0c;将大量遥感影像文件按照分幅条带编号与成像时间加以分组&#xff0c;并将同一分幅的遥感影像加以每个8天时间间隔内的镶嵌拼接的方法。 首先&#xff0c;来看一下本文具体的需求。我们现有一个文件夹&#xff0c;其中含有大量的.ti…

配置有效的防爬虫技术保护网站

本文主要介绍了防爬虫的概念、目的以及一些有效的防爬虫手段。防爬虫是指网站采取各种技术手段阻止爬虫程序对其数据进行抓取的过程。为了保护网站的数据和内容的安全性&#xff0c;防止经济损失和恶意竞争&#xff0c;以及减轻服务器负载&#xff0c;网站需要采取防爬虫机制。…

NIKKE胜利女神妮姬1.5周年(PC)怎么注册?账号注册教程一看就懂

游戏的世界观了一些轻科幻、末世和废土背景&#xff0c;剧情中也探讨了一些深刻的主题&#xff0c;比如NIKKE的人权问题。虽然整体剧情表现得连贯&#xff0c;但本质上有一些俗套情节&#xff0c;特别是在序章的玛丽安之死后&#xff0c;剧情逐渐失去了原有的紧张感&#xff0c…

那些你不知道的数据库知识:行式存储和列式存储

前几天听课&#xff0c;听到老师讲数据列式存储。 我&#x1f64b;&#x1f3fb;‍♀️&#xff1a;等等&#xff0c;what&#xff0c;什么列式存储&#xff0c;数据一行一行的展示&#xff0c;然后一行一行的存在数据库里面不就好了&#xff0c;什么叫做列式存储&#xff0c;…

rust中结构体的属性默认是不能修改的,要想修改可以有两种方式

Rust中结构体里面的属性默认是不支持修改的&#xff0c;而且默认不是pub的&#xff0c;要想修改的话&#xff0c;有两种方式&#xff0c;我以为和python里面的类似呢&#xff0c;但是还是需要一点技术含量的。如果想在引到外部修改&#xff0c;需要声明pub&#xff0c;如果想在…

专利视角下的量子竞赛:《2024全球专利格局白皮书》

2024年1月&#xff0c;欧洲量子产业联盟&#xff08;QuIC&#xff09;发布了题为《全球量子技术专利格局描述》的综合白皮书。 该文件以透明的视角展示了当今的知识产权格局&#xff0c;包括知识产权持有人的地理分布。该文件由 QuIC 知识产权&#xff08;IP&#xff09;与贸易…

【C++】图的存储 -- 邻接表

目录 邻接表的创建使用dfs遍历图dfs遍历图的递归思想 邻接表的创建 #include <iostream> using namespace std; #include <vector>struct edge//创建边集类型 {int v;//出边int w;//权值 }; const int N 100010;//顶点数量 vector<edge> e[N];//邻接表int m…

淘宝新店有流量扶持吗

淘宝新店有流量扶持 淘宝新店需要做些推广提高店铺权重 淘宝新店用3an推客做推广比较好 3an推客是给商家提供的营销工具&#xff0c;3an推客CPS推广模式由商家自主设置佣金比例&#xff0c;以及设置商品优惠券&#xff0c;激励推广者去帮助商家推广商品链接&#xff0c;按最…

初步认识泛型

文章目录 泛型的编译擦除机制 泛型的上界泛型方法 泛型的编译 擦除机制 泛型到底是怎么编译的&#xff1f;这个问题&#xff0c;也是曾经的一个面试问题。泛型本质是非常难的语法 通过命令&#xff1a;javap -c 查看字节码文件&#xff0c;所有的T都是Object。 在编译的过程当…

“量子谷”启程!数亿资金注入,加速量子、人工智能创新

2024年4月23日&#xff0c;欧洲委员会启动了新一轮“地平线欧洲”&#xff08;Horizon Europe&#xff09;项目征集&#xff0c;动用资金总额超过1.12亿欧元&#xff08;约8.67亿人民币&#xff09;。此次征集活动主要面向人工智能&#xff08;AI&#xff09;与量子技术领域的前…

面试:JVM垃圾回收

一、三种垃圾回收算法 1、标记清除&#xff08;已废弃&#xff09; 找到根对象&#xff08;局部变量正在引用的对象、静态变量正在引用的对象&#xff09;&#xff1b;沿着根对象的引用链&#xff0c;查看当前的对象是否被根对象所引用&#xff0c;若被引用&#xff0c;则加上…

redis和mysql数据一致性方案

请求 A 更新数据 请求B读数据 在高并发情况下&#xff0c;A、B请求过程步骤相互穿插&#xff0c;就会出现图中的问题。 期望redis 的数据是11&#xff0c;最后变成了10 场景&#xff1a;先删除Redis&#xff0c;再更新 MySQL&#xff0c;不主动更新Redis&#xff0c;访问redi…

软件企业质量保证的基石――QA,QC的良性协作

软件企业质量保证的基石――QA、QC的良性协作 国内的软件产业发展了20多年的时间&#xff0c;已经由个人英雄时代步入到中、小团队协作时代。相信不久的将来&#xff0c;国内一定会出现航母级的软件企业&#xff0c;那时候我们会迎来集团军作战的时代。不同的时代表明软件规模…
最新文章