目录

webscoket方案调研及实践

一、使用场景

1、考试管理端需要给特定考试用户单独暂停考试、继续考试、加时、减时的操作,当管理端执行了上述的某个操作,需要实时的通知到正在考试的用户那里。

2、社交聊天、弹幕、多玩家游戏、协同编辑、股票基金实时报价、体育实况更新、视频会议/聊天、智能家居等需要高实时的场景

二、方案调研

1、Ajax短轮询

短轮询:客户端定时向服务器发送Ajax请求,服务器接到请求后马上返回响应信息并关闭连接。

优点:后端程序编写比较容易。

缺点:请求中有大半是无用,浪费带宽和服务器资源。

2、long-polling长轮询

长轮询:客户端向服务器发送Ajax请求,服务器接到请求后hold住连接,直到有新消息才返回响应信息并关闭连接,客户端处理完响应信息后再向服务器发送新的请求。

优点:在无消息的情况下不会频繁的请求,耗费资源小。

缺点:服务器hold连接会消耗资源,返回数据顺序无保证,难于管理维护。

3、iframe长连接

长连接:在页面里嵌入一个隐蔵iframe,将这个隐蔵iframe的src属性设为对一个长连接的请求或是采用xhr请求,服务器端就能源源不断地往客户端输入数据。

优点:消息即时到达,不发无用请求;管理起来也相对方便。

缺点:服务器维护一个长连接会增加开销,不同浏览器会有加载问题。

4、XHR-streaming

XHR流:服务端使用分块传输编码(Chunked transfer encoding)的HTTP传输机制进行响应,并且服务器端不终止HTTP响应流,让HTTP始终处于持久连接状态,当有数据需要发送给客户端时再进行写入数据。

优点:通过XHR-Streaming,可以允许服务端连续地发送消息,无需每次响应后再去建立一个连接。

缺点:XHR-streaming连接的时间越长,浏览器会占用过多内存,sockjs默认只允许每个xhr-streaming连接输出128kb数据,超过这个大小时会关闭输出流,让浏览器重新发起请求。

5、Websocket

websocket:Webscoket是Web浏览器和服务器之间的一种全双工通信协议.一旦Web客户端与服务器建立起连接,之后的全部数据通信都通过这个连接进行。通信过程中,可互相发送JSON、XML、HTML或图片等任意格式的数据。

优点:复用长连接,全双工通信,支持服务器推送消息

缺点:服务器维护一个长连接会增加开销,不同浏览器会支持程度不一

5.1 实现原理

Websocket是应用层第七层上的一个应用层协议,它必须依赖 HTTP 协议进行一次握手 ,握手成功后,数据就直接从 TCP 通道传输,与 HTTP 无关了。

WebSocket 交互以 HTTP 请求开始,该请求使用 HTTP“Upgrade”header头升级或在本例中切换到 WebSocket 协议

GET /spring-websocket-portfolio/portfolio HTTP/1.1
Host: localhost:8080
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: Uc9l9TMkWGbHFD2qnFHltg==
Sec-WebSocket-Protocol: v10.stomp, v11.stomp
Sec-WebSocket-Version: 13
Origin: http://localhost:8080

http升级成websocket请求返回的是状态码101,而不是200

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: 1qVdfYHU9hPOl4JYYNXF623Gzn0=
Sec-WebSocket-Protocol: v10.stomp

成功握手后,HTTP 升级请求下的 TCP 套接字保持打开状态,以便客户端和服务器继续发送和接收消息。

参考

https://segmentfault.com/a/1190000019697463

三、实现方案(Websocket)

​ 在java层面实现Webocket主要有spring、netty两个方向,由于我们系统使用的是spring系列,所以采用spring的Websocket实现方案。

​ 在spring的Websocket实现中,又可以细分为3种:

1、基于java原生注解:

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency> @Configuration
@EnableWebSocket // 开启websocket
public class WebSocketConfig { @Bean
public ServerEndpointExporter serverEndpoint() {
return new ServerEndpointExporter();
}
} @ServerEndpoint("/myWs") // 声明websocket端点
@Component
public class WsServerEndpoint { private static Map<String, Session> onlineUserCache = new HashMap<>();
/**
* 连接成功
*
* @param session
*/
@OnOpen
public void onOpen(Session session) {
System.out.println("连接成功");
}
/**
* 连接关闭
*
* @param session
*/
@OnClose
public void onClose(Session session) {
System.out.println("连接关闭");
}
/**
* 接收到消息
*
* @param text
*/
@OnMessage
public String onMsg(String text) throws IOException {
return "servet 发送:" + text;
}
}

​ WsServerEndpoint类下的几个注解需要注意一下,首先是他们的包都在 javax.websocket 下。并不是 spring 提供的,而 jdk 自带的,下面是他们的具体作用。

  1. @ServerEndpoint通过这个 spring boot 就可以知道你暴露出去的 ws 应用的路径,有点类似我们经常用的@RequestMapping。比如你的启动端口是8080,而这个注解的值是ws,那我们就可以通过 ws://127.0.0.1:8080/ws 来连接你的应用
  2. @OnOpen当 websocket 建立连接成功后会触发这个注解修饰的方法,注意它有一个 Session 参数
  3. @OnClose当 websocket 建立的连接断开后会触发这个注解修饰的方法,注意它有一个 Session 参数
  4. @OnMessage当客户端发送消息到服务端时,会触发这个注解修改的方法,它有一个 String 入参表明客户端传入的值
  5. @OnError当 websocket 建立连接时出现异常会触发这个注解修饰的方法,注意它有一个 Session 参数

​ 另外一点就是服务端如何发送消息给客户端,服务端发送消息必须通过上面说的 Session 类,通常是在@OnOpen 方法中,当连接成功后把 session 存入 Map 的 value,key 是与 session 对应的用户标识,当要发送的时候通过 key 获得 session 再发送,这里可以通过 session.getBasicRemote().sendText()来对客户端发送消息

2、spring提供的WebSocket API

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency> public class MyHandler extends TextWebSocketHandler {
// 建立连接成功事件
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
Object token = session.getAttributes().get("token");
if (token != null) {
// 用户连接成功,放入在线用户缓存
WsSessionManager.add(token.toString(), session);
} else {
throw new RuntimeException("用户登录已经失效!");
}
} // 接收消息事件
@Override
public void handleTextMessage(WebSocketSession session, TextMessage message) {
// ...
} // 断开连接时
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
Object token = session.getAttributes().get("token");
if (token != null) {
// 用户退出,移除缓存
WsSessionManager.remove(token.toString());
}
}
}

MyHandler通过继承 TextWebSocketHandler 类并覆盖相应方法,可以对 websocket 的事件进行处理,这里可以同原生注解的那几个注解连起来看.

  1. afterConnectionEstablished 方法是在 socket 连接成功后被触发,同原生注解里的 @OnOpen 功能
  2. afterConnectionClosed 方法是在 socket 连接关闭后被触发,同原生注解里的 @OnClose 功能
  3. handleTextMessage 方法是在客户端发送信息时触发,同原生注解里的 @OnMessage 功能、
public class WsSessionManager {
/**
* 保存连接 session 的地方
*/
private static ConcurrentHashMap<String, List<WebSocketSession>> SESSION_POOL = new ConcurrentHashMap<>(); /**
* 添加 session
*
* @param key
*/
public static void add(String key, WebSocketSession session) {
// 添加 session
SESSION_POOL.put(key, session);
} /**
* 删除 session,会返回删除的 session
*
* @param key
* @return
*/
public static WebSocketSession remove(String key) {
// 删除 session
return SESSION_POOL.remove(key);
} /**
* 删除并同步关闭连接
*
* @param key
*/
public static void removeAndClose(String key) {
WebSocketSession session = remove(key);
if (session != null) {
try {
// 关闭连接
session.close();
} catch (IOException e) {
// todo: 关闭出现异常处理
e.printStackTrace();
}
}
} /**
* 获得 session
*
* @param key
* @return
*/
public static WebSocketSession get(String key) {
// 获得 session
return SESSION_POOL.get(key);
}
}

​ 这里简单通过 ConcurrentHashMap 来实现了一个 session 池,用来保存已经登录的 web socket 的 session。前面提过,服务端发送消息给客户端必须要通过这个 session。

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer { @Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(myHandler(), "/myHandler").setAllowedOrigins("http://mydomain.com");
} @Bean
public WebSocketHandler myHandler() {
return new MyHandler();
}
}

​ 通过实现 WebSocketConfigurer 类并覆盖相应的方法进行 websocket 的配置。我们主要覆盖 registerWebSocketHandlers 这个方法。通过向 WebSocketHandlerRegistry 设置不同参数来进行配置。其中 addHandler 方法添加我们上面的写的 ws 的 handler 处理类,第二个参数是你暴露出的 ws 路径。setAllowedOrigins("*") 这个是关闭跨域校验,方便本地调试,线上推荐打开。

3、基于STOMP消息协议实现

​ 首先需要对SockJS、StompJS以及跟WebSocket三者做简要的说明。

3.1、SockJS

​ SockJS是一个JavaScript库,为了应对许多浏览器不支持WebSocket协议的问题,设计了备选SockJS 。SockJS 是 WebSocket 技术的一种模拟。SockJS会尽可能对应 WebSocket API,但如果WebSocket 技术不可用的话,会自动降为轮询的方式。还提供了心跳检测的机制。

3.2、StompJS

​ STOMP—— Simple Text Oriented Message Protocol——面向消息的简单文本协议。

SockJS 为 WebSocket 提供了 备选方案。 STOMP协议,采用消息订阅的机制,为浏览器 和 server 间的 通信增加适当的消息语义。

3.3、WebSocket、SockJs、STOMP三者关系

​ WebSocket 是底层协议,SockJS 是WebSocket 的备选方案,是一种兼容实现,而 STOMP 是基于 WebSocket(SockJS)的上层协议。

3.4、代码实现

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency> import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry; @Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { @Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/portfolio").withSockJS(); // 1
} @Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.setApplicationDestinationPrefixes("/app"); // 2
config.enableSimpleBroker("/topic", "/queue"); // 3
}
}
  1. "/portfolio"是 WebSocket(或 SockJS)客户端需要连接到 WebSocket 握手的端点的 HTTP URL
  2. 以“/app”开头的STOMP消息将被路由到@Controller 类中的@MessageMapping 方法中
  3. 使用内置的消息代理进行订阅和广播;将以“/topic”或“/queue”开头的消息路由到代理
@Controller
public class WSController { @Autowired
private SimpMessagingTemplate simpMessagingTemplate; @MessageMapping("/hello")
@SendTo("/topic/hello")
public ResponseMessage hello(RequestMessage requestMessage) {
System.out.println("接收消息:" + requestMessage);
return new ResponseMessage("服务端接收到你发的:" + requestMessage);
} @GetMapping("/sendMsgByUser")
public @ResponseBody
Object sendMsgByUser(String token, String msg) {
simpMessagingTemplate.convertAndSendToUser(token, "/msg", msg);
return "success";
}
}

​ 通过 @MessageMapping 来暴露节点路径,有点类似 @RequestMapping。注意这里虽然写的是 hello ,但是我们客户端调用的真正地址是 /app/hello。 因为我们在上面的 config 里配置了registry.setApplicationDestinationPrefixes("/app")

@SendTo这个注解会把返回值的内容发送给订阅了 /topic/hello 的客户端,与之类似的还有一个@SendToUser 只不过他是发送给用户端一对一通信的。这两个注解一般是应答时响应的,如果服务端主动发送消息可以通过 simpMessagingTemplate类的convertAndSend方法。

3.5、 消息流向

​ 对于客户端来说,既可以发送指定的消息请求"/app/a",又可以订阅某一个消息主题"/topic/a"。消息订阅的会被路由到消息代理SimpleBroker中,指定的消息请求会被@MessageMapper路由到指定的方法中,之后再根据特定的主题订阅到消息代理SimpleBroker中。最后再通过消息代码返回消息到对应的客户端。

4、问题

4.1 Websocket连接鉴权问题

​ 前2种可以通过添加握手过程的拦截器,在进行握手前,通过获取url传参,进行鉴权;STOMP实现方案可以通过获取header中的参数来进行鉴权。

4.2 分布式问题

​ 前2种方案,跟客户端的交互都需要通过Session进行,并且需要在各个JVM中维护自己的Session池,在分布式环境中,跟消息服务A连接的用户没办法发送消息给跟消息服务B连接的用户。

​ 而Stomp可以通过外部消息中间件MessageBroker的接入解决分布式问题。

4.3 浏览器Websocket协议兼容问题

​ 前2种方案只能通过Websocket协议进行握手,当客户端所在浏览器不支持WebSocket协议时,需要再实现一套轮询的方案来实现客户端与服务端的交互问题。

​ 而SockJS可以实现当浏览器不支持WebSocket协议时,会自动降为轮询的方式进行交互。

5、消息推送负载均衡方案

​ 上述不管哪种实现方案,都避不开分布式问题。解决分布式问题一般引入第三方中间件,在这里我们可以引入rocketMq、redis、rabbitMq等。消息推送服务都订阅到中间件,业务系统通过发布到中间件,进而让各个连接到消息推送服务的客户端能够收到消息。

四、方案实践

1、方案选型

​ 这里我们选基于STOMP消息协议实现的Websocket方案,原因有如下几点:

1、前后端采用消息订阅消费机制,无须在各个JVM中维护各个SESSION池;

2、采用STOMP协议通信,更容易与中间件结合,解决分布式连接问题(浏览器A连接服务A,浏览器B连接服务B,A没法跟B通信)

3、前端实现采用的是SockJS,能够检测各个浏览器能否支持Websocket协议,不支持的话,会自己降级成XHR- streaming、iframe的实现方式

4、通过SockJS发起的Websocket连接,可以在header中添加参数,来实现鉴权,不然只能通过跟在url后的参数进行鉴权

2、具体实现

2.1 引入websocket依赖

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

2.2 添加Websocket消息代理配置

package com.learnfuture.elearning.exam.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.ChannelRegistration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; import javax.annotation.Resource; /**
* @author huangyizeng
* @description Websocket 消息代理配置
* @date 2021/8/22
**/
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { @Resource
private SocketChanelInterceptor socketChanelInterceptor; @Override
public void registerStompEndpoints(StompEndpointRegistry registry){
//客户端连接端点
registry.addEndpoint("/exam/websocket")
.setAllowedOrigins("*")
.withSockJS();
} @Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/topic","/queue/", "/exchange/");
} @Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.interceptors(socketChanelInterceptor);
} } package com.learnfuture.elearning.exam.config; import lombok.extern.slf4j.Slf4j;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.simp.stomp.StompCommand;
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
import org.springframework.messaging.support.ChannelInterceptor;
import org.springframework.messaging.support.MessageHeaderAccessor;
import org.springframework.stereotype.Component; import java.util.List; /**
* @author huangyizeng
* @description
* @date 2021/8/22
**/
@Slf4j
@Component
public class SocketChanelInterceptor implements ChannelInterceptor { /**
* 实际消息发送到频道之前调用
*/
@Override
public Message<?> preSend(Message<?> message, MessageChannel channel) { StompHeaderAccessor accessor =
MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
//1、判断是否首次连接
if (accessor != null && StompCommand.CONNECT.equals(accessor.getCommand())) {
List<String> nativeHeader = accessor.getNativeHeader("Authorization");
System.out.println(nativeHeader);
}
String jwtToken = accessor.getFirstNativeHeader("token");
return message;
}
}

WebSocketConfig主要是配置Websocket的开放端点,使能SockJS连接端点,并且配置Spring自带的消息代理的各个过滤器前缀/topic、/queue、/exchange 等。这里的消息代理是作为浏览器的消息代理,是浏览器跟websocket服务端的订阅及消费关系。

​ 接着配置Socket的通道拦截器SocketChanelInterceptor,与Websocket端点建立连接以及后续通过该通道接收消息,都会经过该拦截器。在连接/发送消息的时候,可以通过添加header头,来实现websocket的鉴权。

2.3 编写Websocket的业务消费者

/**
* @author huangyizeng
* @description 考试用户消息消费者
* @date 2021/8/26
**/
@Slf4j
@Component
public class SendToExamUserConsumer{ @Resource
private SimpMessagingTemplate template; @Value("${rocketmq.consumer.group.message.sendToExamUser}")
private String consumerGroup; @Value("${rocketmq.name-server}")
private String nameServer; @Value("${rocketmq.topic.message}")
private String topic; @Value("${rocketmq.topic.message.tag.sendToExamUser}")
private String selectorExpression; @PostConstruct
public void init() {
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(consumerGroup); consumer.setNamesrvAddr(nameServer);
consumer.setMessageModel(MessageModel.BROADCASTING);
consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
try {
consumer.subscribe(topic, selectorExpression);
} catch (MQClientException e) {
e.printStackTrace();
} //设置一个Listener,主要进行消息的逻辑处理
consumer.registerMessageListener(new MessageListenerConcurrently() { @Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs,
ConsumeConcurrentlyContext context) {
try {
for (MessageExt messageExt : msgs) {
String messageBody = new String(messageExt.getBody());
JSONObject jsonObject = JSONObject.parseObject(messageBody);
WebsocketMsgResp websocketMsgResp = jsonObject.toJavaObject(WebsocketMsgResp.class); String destination = "/queue/examUserDetailId_" + websocketMsgResp.getExamUserDetailId();
template.convertAndSend(destination, websocketMsgResp); log.info("考试用户消息消费成功, message={}", messageBody);
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
} catch (Exception e) {
log.error("考试用户消息消费失败, message={}", msgs, e);
throw new ServiceException(ApiCode.FAILURE, "考试用户消息消费失败", e);
}
}
}); // 调用start()方法启动consumer
try {
consumer.start();
} catch (MQClientException e) {
log.error("SendToExamUserConsumer 启动失败", e);
}
}
}

​ 这里主要是定义了一个业务的RocketMQ消费者,消息模式设为广播模式,这样当websocket服务集群化部署时,只要有一条消息生产过来,那每一个websocket服务都能够消费到。避免了分布式下Websocket的消息推送问题。

2.4 js代码实现

<script src="/js/websocket.js"></script>
<script src="/js/jquery.min.js"></script>
<script src="/js/sockjs.min.js"></script>
<script src="/js/stomp.min.js"></script> function connect(url) {
var host = window.location.host; // 带有端口号
userId = GetQueryString("userId");
var socket = new SockJS("http://localhost:9000/api/exam/websocket?access_token=4c819e0f-a0a0-448a-8f79-9b9a538f5837", null, {timeout : 10000});
stompClient = Stomp.over(socket);
stompClient.connect({"Authorization" : "Bearer 5807a5eb-dbf1-4ac7-8c40-8aeaa25ccf65"}, function (frame) {
writeToScreen("connected: " + frame);
stompClient.subscribe("/queue/examUserDetailId_" + userId, function (response) {
writeToScreen(response.body);
}); }, function (error) {
}
)
}

​ 引入相关js包后,新建SockJS对象后,调用stompClient.connect发起websocket服务连接,通过stompClient.subscribe发起跟websocket服务的消息订阅,后续websocket服务发送到具体topic下的消息,都会发送到对应的浏览器那里。

3、技术难点

3.1 网关鉴权

​ 现在系统架构中,所有需要鉴权的接口都会在gateway中进行鉴权,并且通过在header中,添加对应的Authrization属性来传参通过OAuth2的鉴权,而websocket的info接口(比如/api/exam/websocket 是端点,那么在发起websocket连接之前,会先发送一个/api/exam/websocket/info接口查看当前websocket服务是否存在)是没办法将token放在header进行调用的,只能通过/info?token=xxx 的形式来发起。

​ 而OAuth默认是先从Header中获取token,接着再从url中的参数获取token。此时我们只能通过url传递token过去,但是OAuth默认是不允许从url进行获取的,所以需要手动设置成允许。

​ 优先从header中获取,其次是从参数中获取。

OAuth2AuthenticationProcessingFilter.doFilter

​ 设置成允许从参数中获取token

// token转换器
ServerBearerTokenAuthenticationConverter converter = new ServerBearerTokenAuthenticationConverter();
converter.setAllowUriQueryParameter(true); // 设置成允许从参数中获取token

3.2 前端联调问题

3.2.1 WebSocket is closed before the connection is established.

​ 主要是由于前端本地webpack的代理没有开启ws 协议的支持。

3.2.2 Error during WebSocket handshake: Unexpected response code: 400

​ 前端经过nginx代理,而nginx 不支持http请求的升级,而Websocket是首先发送一个http请求,然后将该请求升级成Websocket请求,所以需要添加支持配置:

proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";

4、存在的问题

4.1 websocket 的高并发性能还未进行测试

websocket方案调研及实践的更多相关文章

  1. 大文件拆分方案的java实践(附源码)

    引子 大文件拆分问题涉及到io处理.并发编程.生产者/消费者模式的理解,是一个很好的综合应用场景,为此,花点时间做一些实践,对相关的知识做一次梳理和集成,总结一些共性的处理方案和思路,以供后续工作中借 ...

  2. Hadoop HA方案调研

    原文成文于去年(2012.7.30),已然过去了一年,很多信息也许已经过时,不保证正确,与Hadoop学习笔记系列一样仅为留做提醒. ----- 针对现有的所有Hadoop HA方案进行调研,以时间为 ...

  3. EasyDarwin云存储方案调研:海康萤石云采用的是MPEG-PS打包的方式进行的存储

    EasyDarwin开源流媒体服务器项目在直播功能稳定和完善之后,开始涉及服务器端存储与回放功能的调研与开发,当然,这里就要研究一下行业标杆萤石云是怎么来做的,我们通过非常复杂的流程将萤石存储的录像文 ...

  4. Session共享实现方案调研

    1.背景 随 着互联网的日益壮大,网站的pv和uv成线性或者指数倍的增加.单服务器单数据库早已经不能满足实际需求.目前大多数大型网站的服务器都采用了分布式服务 集群的部署方式,所谓集群,就是让一组计算 ...

  5. 基于 Istio 的全链路灰度方案探索和实践

    作者|曾宇星(宇曾) 审核&校对:曾宇星(宇曾) 编辑&排版:雯燕 背景 微服务软件架构下,业务新功能上线前搭建完整的一套测试系统进行验证是相当费人费时的事,随着所拆分出微服务数量的不 ...

  6. 基于nginx的频率控制方案思考和实践

    基于nginx的频率控制方案思考 标签: 频率控制 nginx 背景 nginx其实有自带的limit_req和limit_conn模块,不过它们需要在配置文件中进行配置才能发挥作用,每次有频控策略的 ...

  7. 石墨文档Websocket百万长连接技术实践

    引言 在石墨文档的部分业务中,例如文档分享.评论.幻灯片演示和文档表格跟随等场景,涉及到多客户端数据同步和服务端批量数据推送的需求,一般的 HTTP 协议无法满足服务端主动 Push 数据的场景,因此 ...

  8. 大型网站系统架构实践(五)深入探讨web应用高可用方案

    从上篇文章到这篇文章,中间用了一段时间准备,主要是想把东西讲透,同时希望大家给与一些批评和建议,这样我才能有所进步,也希望喜欢我文章的朋友,给个赞,这样我才能更有激情,呵呵. 由于本篇要写的内容有点多 ...

  9. 基于angularJs的单页面应用seo优化及可抓取方案原理分析

    公司使用angularJs(以下都是指ng1)框架做了互联网应用,之前没接触过seo,突然一天运营那边传来任务:要给网站做搜索引擎优化,需要研发支持.搜了下发现单页面应用做seo比较费劲,国内相关实践 ...

随机推荐

  1. 虚拟机VMWare开机黑屏 无法进入系统

    参考了: https://blog.csdn.net/x534119219/article/details/79497264 可能方案一: 关闭VMware Workstation加速3D图形设置 可 ...

  2. cmd编译java时常见错误

    中文乱码 在执行javac时出现如图所示问题, 解决方法: 改用 javac -encoding UTF-8执行 找到路径:控制面板--系统和安全--系统--高级系统设置--环境变量--系统变量. 新 ...

  3. ssl.SSLCertVerificationError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: self signed certificate in certificate chain (_ssl.c:1122)

    只需执行 /Applications/Python\ 3.9/Install\ Certificates.command

  4. Linux常用命令 - less命令详解

    21篇测试必备的Linux常用命令,每天敲一篇,每次敲三遍,每月一循环,全都可记住!! https://www.cnblogs.com/poloyy/category/1672457.html 查看文 ...

  5. ubuntu 20.04 发邮件配置

    安装sendmail后,发邮件一直没有成功,因此卸载sendmail后,安装heirloom-mailx. # unbuntu 18.04和20.04移除了heirloom-mailx,需要另外配置软 ...

  6. Redis集群的搭建及与SpringBoot的整合

    1.概述 之前聊了Redis的哨兵模式,哨兵模式解决了读的并发问题,也解决了Master节点单点的问题. 但随着系统越来越庞大,缓存的数据越来越多,服务器的内存容量又成了问题,需要水平扩容,此时哨兵模 ...

  7. ClickOnce手动更新

    if (ApplicationDeployment.IsNetworkDeployed == true)             {                 ApplicationDeploy ...

  8. 利用GetInvalidFileNameChars()得到有效的文件名

    public static string GetValidName(string fileName) {     foreach (char c in System.IO.Path.GetInvali ...

  9. node实战小例子

    第一章 2020-2-6 留言小本子 思路(由于本章没有数据库,客户提交的数据放在全局变量,接收请求用的是bodyParser, padyParser使用方法 app.use(bodyParser.u ...

  10. 基于Tensorflow + Opencv 实现CNN自定义图像分类

    摘要:本篇文章主要通过Tensorflow+Opencv实现CNN自定义图像分类案例,它能解决我们现实论文或实践中的图像分类问题,并与机器学习的图像分类算法进行对比实验. 本文分享自华为云社区< ...