天道酬勤
A——Algorithm 一道算法题
LeetCode 852. 山脉数组的顶峰索引
-
我们把符合下列属性的数组 A 称作山脉:
- A.length >= 3
- 存在
0 < i < A.length - 1
使得A[0] < A[1] < ... A[i-1] < A[i] > A[i+1] > ... > A[A.length - 1]
-
给定一个确定为山脉的数组,返回任何满足 上述公式的 i 的值。
-
提示:
- 3 <= A.length <= 10000
- 0 <= A[i] <= 10^6
- A 是如上定义的山脉
-
解答:
- 思路:
- 这道题解题思路,就是找到整个数组最大的数字,在无序状态下使用二分法的话,会出现找到的不是真正的山峰的情况,如果需要重新排序,再使用二分法找到最大的元素的话,时间复杂度就多了一个 O(n) 得不偿失。使用一次遍历找到最大的元素是最快的也是符合题目要求的答案
- 思路:
1 | // 示例 1: |
R——Review 阅读一篇英文文章
T——Tips 学习一个新技术点
Websocket
一、Websocket 介绍
- Websocket 是什么:
- Websocket 是一种网络通信协议。RFC6455 定义了它的通信标准。
- Websocket 是 HTML5 开始提供的一种在单个 TCP 连接上进行全双工通讯的协议。
- Websocket 是一个 TCP 接口进行双向通信的技术,PUSH 技术类型。同时 Websocket 仍将基于 W3C 标准,目前为止,Chrome 和 Safari 的最新版本浏览器已经支持 Websocket 了。
- ajax 轮询和 long pull 均可实现实时信息传递。
- ajax 轮询的原理非常简单,让浏览器每隔几秒就发送一次请求,询问服务器是否有新信息。
- long poll 其实原理跟 ajax 轮询差不多,都是采用轮询的方式,不过采取的是阻塞模型(一直打电话,没有收到就不挂电话),也就是说,客户端发起连接后,如果没消息,就一直不反悔 Response 给客户端。直到有消息才返回,返回完之后,客户端再次建立连接,周而复始。
- 场景:
- 最典型的的场景就是聊天室,如果使用 HTTP 协议的话,就只能轮询获取服务端有没有消息,而使用 Websocket 的话,服务端有新消息可以自动推送。
- 特点:
- **较少的控制开销。**在连接创建后,服务器和客户端之间交换数据时,用于协议控制的数据包头部相对较小。在不包含扩展的情况下,对于服务器到客户端的内容,此头部大小只有2至10字节(和数据包长度有关);对于客户端到服务器的内容,此头部还需要加上额外的4字节的掩码。相对于HTTP请求每次都要携带完整的头部,此项开销显著减少了。
- **更强的实时性。**由于协议是全双工的,所以服务器可以随时主动给客户端下发数据。相对于HTTP请求需要等待客户端发起请求服务端才能响应,延迟明显更少;即使是和Comet等类似的长轮询比较,其也能在短时间内更多次地传递数据。
- **保持连接状态。**与HTTP不同的是,Websocket需要先创建连接,这就使得其成为一种有状态的协议,之后通信时可以省略部分状态信息,而HTTP请求可能需要在每个请求都携带状态信息(如身份认证等)。
- **更好的二进制支持。**Websocket定义了二进制帧,相对HTTP,可以更轻松地处理二进制内容。可以支持扩展。Websocket定义了扩展,用户可以扩展协议、实现部分自定义的子协议,如部分浏览器支持压缩等。
- **更好的压缩效果。**相对于HTTP压缩,Websocket在适当的扩展支持下,可以沿用之前内容的上下文,在传递类似的数据时,可以显著地提高压缩率。
二、Stomp 和 Websocket
STOMP
(Simple Text Orientated Messaging Protocol),简单文本定向消息协议,它提供了一个可互操作的连接格式,允许 STOMP 客户端与任意 STO,P 消息代理(Broker)进行交互。STOMP 协议由于设计简单,易于开发客户端,因此在多种语言和多种平台上得到了广泛的应用。- STOMP 协议分为客户端和服务端:
- STOMP 服务端
- STOMP服务端被设计为客户端可以向其发送消息的一组目标地址。STOMP协议并没有规定目标地址的格式,它由使用协议的应用自己来定义。例如,/topic/a、/queue/a、queue-a对于STOMP协议来说都是正确的。应用可以自己规定不同的格式以此来表明不同格式代表的含义。比如应用自己可以定义以/topic打头的为发布订阅模式,消息会被所有消费者客户端收到,以/user开头的为点对点模式,只会被一个消费者客户端收到。
- STOMP 客户端
- 对于STOMP协议来说,客户端会扮演下列两种角色的任意一种:
- 作为生产者,通过 SEND 帧发送消息到指定的地址;
- 作为消费者,通过发送 SUBSCRIBE 帧到已知地址来进行消息订阅,而当生产者发送消息到这个订阅地址后,订阅该地址的其他消费者会受到通过 MESSAGE 帧受到该消息。
- 实际上,WebSocket结合STOMP相当于构建了一个消息分发队列,客户端可以在上述两个角色间转换,订阅机制保证了一个客户端消息可以通过服务器广播到多个其他客户端,作为生产者,又可以通过服务器来发送点对点消息。
- 对于STOMP协议来说,客户端会扮演下列两种角色的任意一种:
- STOMP 帧结构
- 一个 STOMP 帧有三部分组成: 命令、Header(头信息)、Body(消息体)。
- 命令使用UTF-8编码格式,命令有SEND、SUBSCRIBE、MESSAGE、CONNECT、CONNECTED等。
- Header也使用UTF-8编码格式,它类似HTTP的Header,有content-length、content-type等。
- Body可以是二进制也可以是文本,Body与Header间通过一个空行(EOL)来分隔。
- 一个 STOMP 帧有三部分组成: 命令、Header(头信息)、Body(消息体)。
- STOMP 服务端
1 | COMMAND |
1 | SEND |
三、Webcocket 事件
事件 | 事件处理程序 | 描述 |
---|---|---|
open | Socket onopen | 连接建立时触发 |
message | Socket onopen | 客户端接收服务端数据时触发 |
error | Socket onerror | 通讯发生错误时触发 |
close | Socket onclose | 连接关闭时触发 |
- 一个页面使用 Websocket 的示例:
1 | var ws = new WebSocket("ws://localhost:8080"); |
四、Spring Boot 与 Websocket 整合(Spring Boot Websocket 聊天室)
-
聊天室功能
- 支持用户加入聊天室,对应到 Websocket 技术就是建立连接 onopen;
- 支持用户退出聊天室,对应到 Websocket 技术就是关闭连接 onclose;
- 支持用户在聊天室发送消息,对应到 Websocket 技术就是调用 onmessage 发送消息;
- 支持异常时提示,对应到 Websocket 技术 onerror。
-
页面开发
- 前端使用 Bootstrap 渲染页面,页面内容如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
<html lang="en">
<head>
<meta charset="UTF-8">
<title>chat room websocket</title>
<link rel="stylesheet" href="bootstrap.min.css">
<script src="jquery-3.2.1.min.js" ></script>
</head>
<body class="container" style="width: 60%">
<div class="form-group" ></br>
<h5>聊天室</h5>
<textarea id="message_content" class="form-control" readonly="readonly" cols="50" rows="10"></textarea>
</div>
<div class="form-group" >
<label for="in_user_name">用户姓名 </label>
<input id="in_user_name" value="" class="form-control" /></br>
<button id="user_join" class="btn btn-success" >加入聊天室</button>
<button id="user_exit" class="btn btn-warning" >离开聊天室</button>
</div>
<div class="form-group" >
<label for="in_room_msg" >群发消息 </label>
<input id="in_room_msg" value="" class="form-control" /></br>
<button id="user_send_all" class="btn btn-info" >发送消息</button>
</div>
</body>
</html>
<!-- 最上面使用textarea画一个对话框,用来显示聊天室的内容;中间部分添加用户加入聊天室和离开聊天室的按钮,按钮上面是输入用户名的入口;页面最下面添加发送消息的入口, -->
<!-- 页面中添加 Websocket 通讯代码: -->
<script type="text/javascript">
$(document).ready(function(){
var urlPrefix ='ws://localhost:8080/chat-room/';
var ws = null;
$('#user_join').click(function(){
var username = $('#in_user_name').val();
var url = urlPrefix + username;
ws = new WebSocket(url);
ws.onopen = function () {
console.log("建立 websocket 连接...");
};
ws.onmessage = function(event){
//服务端发送的消息
$('#message_content').append(event.data+'\n');
};
ws.onclose = function(){
$('#message_content').append('用户['+username+'] 已经离开聊天室!');
console.log("关闭 websocket 连接...");
}
});
//客户端发送消息到服务器
$('#user_send_all').click(function(){
var msg = $('#in_room_msg').val();
if(ws){
ws.send(msg);
}
});
// 退出聊天室
$('#user_exit').click(function(){
if(ws){
ws.close();
}
});
})
</script>
<!-- 主要是监听三个按钮的点击事件,当用户登录、离开、发送消息是调用对应的WebSocket事件,将信息传送给服务端。同时打开页面时创建了WebSocket对象,页面会监控WebSocket事件,如果后端服务和前端通讯室将对应的信息展示在页面。 --> -
服务端开发
- 添加依赖
1
2
3
4<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>- 启动类
- 启动类需要添加
@EnableWebSocket
开启 WebSocket 功能。
- 启动类需要添加
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class WebSocketApplication
{
public static void main(String[] args)
{
SpringApplication.run(WebSocketApplication.class, args);
}
public ServerEndpointExporter serverEndpointExporter()
{
return new ServerEndpointExporter();
}
}- 请求接收
- 在创建服务端消息接收功能之前,先创建一个WebSocketUtils工具类,用来存储聊天室在线的用户信息,以及发送消息的功能。定义全局变量ONLINE_USER_SESSIONS用来存储在线用户,使用ConcurrentHashMap提升高并发时效率。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
314j
public final class WebSocketUtils {
// 存储 websocket session
public static final Map<String, Session> ONLINE_USER_SESSIONS = new ConcurrentHashMap<>();
public static void sendMessage(Session session, String message)
{
if (session == null)
{
return;
}
final RemoteEndpoint.Basic basic = session.getBasicRemote();
if (basic == null)
{
return;
}
try
{
basic.sendText(message);
}
catch (IOException e)
{
log.error("sendMessage IOException ", e);
}
}
public static void sendMessageAll(String message)
{
ONLINE_USER_SESSIONS.forEach((sessionId, session) -> sendMessage(session, message));
}
}
-
Controller
1 | 4j |
- 接受类上需要添加
@ServerEndpoint("url")
代表监听此地址的 WebSocket 信息。 - @OnOpen注解和前端的onopen事件一致,表示用户建立连接时触发。
- @OnMessage监听发送消息的事件。
- @OnClose监听用户断开连接事件。
- @OnError事件,可以在此方法内记录下错误的异常信息,并关闭用户连接。