SpringBoot 集成 WebSocket

场景 & 需求

  1. 客户端发送请求后,服务端进行处理后可以对所有的客户端进行 广播
  2. 服务端可以在任何时候主动对所有客户端进行 广播
  3. 客户端发送请求后,服务端进行处理后可以对指定客户端进行 点对点推送
  4. 服务端可以在任何时候主动对指定客户端进行 点对点推送
  5. 服务端可以在任何时候主动对指定某些客户端进行 广播
  6. 服务端可以识别客户端(状态),并以此进行 点对点推送

前置要求

本文假设你已经了解或知道以下技能,尤其而且是勾选的内容。

  • Java
  • Maven
  • SpringBoot

引入依赖

创建一个 SpringBoot 项目,并添加 spring-boot-starter-websocket 依赖

1
2
3
4
5
<!--spring boot web socket-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

配置 SpringBoot WebSocket 支持

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
/**
* 配置 SpringBoot WebSocket 支持
*
* @author rxliuli
*/
@Configuration
@EnableWebSocketMessageBroker
public class SpringWebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
/**
* 注册一个 Socket 端点
*
* @param stompEndpointRegistry stomp 端点注册表
*/
@Override
public void registerStompEndpoints(StompEndpointRegistry stompEndpointRegistry) {
stompEndpointRegistry.addEndpoint("/endpoint")
//设置允许所有源请求(跨域)
.setAllowedOrigins("*")
.withSockJS();
}

/**
* 注册一些广播消息代理
*
* @param registry 消息代理注册对象
*/
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
//注册简单代理(里面是前缀)
//注:默认 topic 是主题(广播),user 则是用户(点对点)
registry.enableSimpleBroker("/topic", "/user");
}
}

双向广播服务端

客户端发送请求后,服务端进行处理后可以对所有的客户端进行 广播

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* 双向广播控制器
*
* @author rxliuli
*/
@Controller
public class BilateralBroadcastingSocket {
/**
* 广播推送
* 注解 @Payload 是为了绑定消息到参数 text 上
*
* @param text 简单的文本信息
* @param sessionId 当前请求 socket 会话 id
* @return 会话 id 和消息内容
*/
@MessageMapping(value = "/talk")
@SendTo("/topic/broadcasting/bilateral/allClient")
public String talk(@Payload String text, @Header("simpSessionId") String sessionId) throws InterruptedException {
//模拟处理其他事情
Thread.sleep(1000L);
return "[ " + sessionId + "] 说: [" + text + "]";
}
}

双向广播客户端

向服务端发送消息,并监听服务端的广播。客户端发送消息与监听是分离的,也可以只向服务端发送消息而不监听广播,或者只接收广播不发送消息。

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
66
67
68
69
70
71
72
73
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Document</title>
</head>

<body>
<script
type="application/javascript"
src="https://cdn.bootcss.com/sockjs-client/1.1.4/sockjs.min.js"
></script>
<script
type="application/javascript"
src="https://cdn.bootcss.com/stomp.js/2.3.3/stomp.min.js"
></script>
<script>
let socket = new SockJS('http://127.0.0.1:8080/endpoint')
stompClient = Stomp.over(socket)
stompClient.connect(
{},
// 连接成功回调函数
frame => {
console.log('服务端 Socket 连接建立')

// 获取 websocket 连接的 sessionId
const sessionId = /\/([^\/]+)\/websocket/.exec(
socket._transport.url,
)[1]
console.log('connected, session id: ' + sessionId)

// 订阅广播消息(双向通信)
// 这里是关键(订阅了服务端的 topic)
stompClient.subscribe(
'/topic/broadcasting/bilateral/allClient',
res => {
console.log(`[广播(双向通信)]: ${res.body}`)
},
)

// 发送请求
send()
},
error => {
console.log('Socket 连接失败')
},
)

function send() {
// 发送一个消息到服务端
// 发送消息到服务端
var headers = {}
var body = {
message: '消息内容',
}
stompClient.send('/talk', headers, JSON.stringify(body))
}

/**
* 监听窗口关闭事件,窗口关闭前,主动关闭连接,防止连接还没断开就关闭窗口,server 端会抛异常
*/
window.onbeforeunload = function() {
if (stompClient !== null) {
stompClient.disconnect()
socket.close()
}
console.log('断开连接')
}
</script>
</body>
</html>

单向广播服务端

从服务端推送消息到所有客户端,是单向推送到客户端的,不接受从客户端的输入。

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
/**
* 单向广播控制器
*
* @author rxliuli
*/
@Controller
public class UnidirectionalBroadcastingSocket {
/**
* 从服务端推送消息到所有客户端
* 这是单向推送到客户端的,不接受从客户端的输入
*/
@SendTo("/topic/broadcasting/unidirectional/allClient")
public Object broadcasting() {
return null;
}
}

/**
* 使用 Scheduled 不停的推送信息
*
* @author rxliuli
*/
@Component
public class ScheduledRefreshJob {
@Autowired
private SimpMessagingTemplate simpMessagingTemplate;

/**
* 不停地推送消息到客户端
*/
@Scheduled(fixedDelay = 10 * 1000)
public void scheduledBroadcasting() {
simpMessagingTemplate.convertAndSend("/topic/broadcasting/unidirectional/allClient", new Person(1L, "rx", false));
}
}

单向广播客户端

客户端只需要添加一个监听器就好了,不需要也不能向服务端发送消息。

1
2
3
4
5
6
7
// 订阅广播消息(服务端单向推送)
const subscription_broadcast = stompClient.subscribe(
'/topic/broadcasting/unidirectional/allClient',
response => {
console.log(`[广播(服务端单向推送)]: ${response.body}`)
},
)

点对点推送服务端

服务端使用 @SendToUser(path) 向单个客户端推送消息,这里的 @Header("simpSessionId") 指的是从客户端的请求头中的 simpSessionId 参数赋值给 sessionIdWebSocket 会话 ID,和用户 Session 不同,是每一个 WebSocket 唯一的 #即和用户不是一一对应)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* 双向点对点推送控制器
*
* @author rxliuli
*/
@Controller
public class BilateralPushSocket {
/**
* 点对点推送(双向通信)
*
* @param text 消息
* @param sessionId 会话 id
* @return 推送到当前会话的消息
*/
@MessageMapping("/speak")
@SendToUser("/push/bilateral/thisClient")
public String speak(@Payload String text, @Header("simpSessionId") String sessionId) throws InterruptedException {
//模拟处理其他事情
Thread.sleep(1000L);
return "[ " + sessionId + "] send: [" + text + "]";
}
}

点对点推送客户端

客户端请求的路径需要注意一下,是以 /user/${sessionId} 开头,后面才是 @SendToUser(path) 中设置的 path

1
2
3
4
// 订阅私人消息(双向通信)
stompClient.subscribe(`/user/${sessionId}/push/bilateral/thisClient`, res => {
console.log(`[点对点推送(双向通信)]: ${res.body}`)
})

单向点对点推送服务端

其实和上面双向的点对点推送没什么太大的差别,就是只用 @SendToUser(path) 而不用 @MessageMapping(path) 了而已

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
/**
* 单向点对点推送服务端
*
* @author rxliuli
*/
@Controller
public class UnidirectionalPushSocket {
/**
* 从服务端推送消息到所有客户端
* 这是单向推送到客户端的,不接受从客户端的输入
*/
@SendToUser("/push/unidirectional/thisClient")
public Object push() {
return null;
}
}

/**
* 使用 Scheduled 不停的推送信息
*
* @author rxliuli
*/
@Component
public class ScheduledRefreshJob {
@Autowired
private SimpMessagingTemplate simpMessagingTemplate;

/**
* 不停推送消息到某个指定的客户端
*/
@Scheduled(fixedDelay = 10 * 1000)
public void scheduledPush() {
simpMessagingTemplate.convertAndSendToUser("r2qspi4s", "/push/unidirectional/thisClient", new Person(2L, "琉璃", false));
}
}

单向点对点推送客户端

客户端和上面的双向点对点推送基本一致(完全一样好么?!)

1
2
3
4
5
6
7
// 订阅私人消息(单向通信)
stompClient.subscribe(
`/user/${sessionId}/push/unidirectional/thisClient`,
res => {
console.log(`[点对点推送(单向通信)]:${res.body}`)
},
)

记录 user -> Socket 会话对应的映射表

上面的点对点推送客户端几乎是没什么用处的(尤其而且是 单向点对点推送),因为每次创建的 Socket 连接都会变化,而没有与用户建立对应关系的话怎无法知道哪个用户对应的哪个人,也就不能发送消息给指定的用户(非 Socket Session Id)了

  1. 首先需要一个记录用户 Socket Session Id 的类,并注册为 SpringBoot 的组件。
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
66
67
68
69
70
71
72
73
/**
* 用户 session 记录类
*
* @author rxliuli
*/
@Component
public class SocketSessionRegistry {
/**
* 未登录的用户默认存储的 user id
*/
public static final String DIRECT_TOURIST = "DIRECT_TOURIST";
/**
* 这个集合存储 用户 id -> session 列表
* 单个用户可能打开多个页面,就会出现多个 Socket 会话
*/
private final ConcurrentMap<String, Set<String>> userSessionIds = new ConcurrentHashMap<>();
private final Object lock = new Object();

/**
* 根据 user id 获取 sessionId
*
* @param user 用户 id
* @return 用户关联的 sessionId
*/
public Set<String> getSessionIds(String user) {
Set<String> set = this.userSessionIds.get(user);
return set != null ? set : Collections.emptySet();
}

/**
* 获取所有 session
*
* @return 所有的 用户 id -> session 列表
*/
public ConcurrentMap<String, Set<String>> getAllSessionIds() {
return this.userSessionIds;
}

/**
* 根据用户 id 注册一个 session
*
* @param user 用户 id
* @param sessionId Socket 会话 id
*/
public void registerSessionId(String user, String sessionId) {
Assert.notNull(user, "User must not be null");
Assert.notNull(sessionId, "Session ID must not be null");
synchronized (this.lock) {
Set<String> set = this.userSessionIds.get(user);
if (set == null) {
this.userSessionIds.put(user, new CopyOnWriteArraySet<>());
}
set.add(sessionId);
}
}

/**
* 根据用户 id 删除一个 session
*
* @param user 用户 id
* @param sessionId Socket 会话 id
*/
public void unregisterSessionId(String user, String sessionId) {
Assert.notNull(user, "User Name must not be null");
Assert.notNull(sessionId, "Session ID must not be null");
synchronized (this.lock) {
Set set = this.userSessionIds.get(user);
if (set != null && set.remove(sessionId) && set.isEmpty()) {
this.userSessionIds.remove(user);
}
}
}
}
  1. 监听 WebSocket 连接建立和关闭事件
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
66
67
68
69
/**
* 会话事件监听基类
*
* @author rxliuli
*/
public abstract class BaseSessionEventListener<Event extends AbstractSubProtocolEvent> implements ApplicationListener<Event> {
protected final Logger log = LoggerFactory.getLogger(getClass());
@Autowired
protected SocketSessionRegistry webAgentSessionRegistry;

/**
* 计算出 user id 和 session id 并传入到自定义的函数中
*
* @param event 事件
* @param biConsumer 自定义的操作
*/
protected void using(Event event, BiConsumer<String, String> biConsumer) {
StompHeaderAccessor sha = StompHeaderAccessor.wrap(event.getMessage());
//login get from browser
List<String> shaNativeHeader = sha.getNativeHeader("Authorization");
String user;
if (shaNativeHeader == null || shaNativeHeader.isEmpty()) {
user = null;
} else {
user = shaNativeHeader.get(0);
}
//如果当前用户没有登录(没有认证信息),就添加到游客里面
if (user == null || "".equals(user) || "undefined".equals(user) || "null".equals(user)) {
user = SocketSessionRegistry.DIRECT_TOURIST;
}
String sessionId = sha.getSessionId();
biConsumer.accept(user, sessionId);
}
}

/**
* Socket 连接建立监听
* 用于 session 注册 以及 key 值获取
*
* @author rxliuli
*/
@Component
public class SessionConnectEventListener extends BaseSessionEventListener<SessionConnectEvent> {
@Override
public void onApplicationEvent(SessionConnectEvent event) {
using(event, (user, sessionId) -> webAgentSessionRegistry.registerSessionId(user, sessionId));
}
}

/**
* Socket 会话断开监听
*
* @author rxliuli
*/
@Component
public class SessionDisconnectEventListener extends BaseSessionEventListener<SessionDisconnectEvent> {
@Override
public void onApplicationEvent(SessionDisconnectEvent event) {
//这里先根据 session id 查询出 user,然后删除对应的会话 id
//前端无法传递 token 到这里却是只能出此下策了
using(event, (user, sessionId) -> webAgentSessionRegistry.getAllSessionIds().entrySet().stream()
.filter(sse -> sse.getValue().contains(sessionId))
.findFirst()
.ifPresent(sse -> {
webAgentSessionRegistry.unregisterSessionId(sse.getKey(), sessionId);
log.info("Socket 连接断开,用户:{},会话:{}", sse.getKey(), sessionId);
}));
}
}
  1. 客户端在打开和关闭连接的时候需要发送 user 给服务端

这里使用 headers 存放用户认证信息(唯一标识),所以在连接和关闭时要带上请求头

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
stompClient.connect(getHeaders(), function(){
console.log('打开 Socket 连接')
})
// TODO 这里还有一些问题,无法带上 headers 到后端
stompClient.disconnect(function () {
console.log('断开连接');
}, getHeaders());

/**
* 获取一个认证的 headers 信息
* @return {{"X-Requested-With": string, Authorization: any}} 含有认证信息的 headers 对象
*/
function getHeaders() {
return {
'X-Requested-With': 'X-Requested-With',
'Authorization': localStorage.token
}
}
  1. 使用记录的 user -> session id 发送消息给指定的用户

下面是获取到所有已经登录的用户的 WebSocket 连接并发送一条消息

1
2
3
socketSessionRegistry.getAllSessionIds().entrySet().stream()
.filter(kv -> !SocketSessionRegistry.DIRECT_TOURIST.equals(kv.getKey()))
.forEach(kv -> kv.getValue().forEach(sessionId -> simpMessagingTemplate.convertAndSendToUser(sessionId, "/push/unidirectional/thisClient", new Person(2L, "琉璃", false))));

接受/返回复杂类型的消息(服务端)

其实看起来和刚才是没什么区别的,但 SpringBoot WebSocket 原本就对消息进行了解析/封装,所以我们不需要再去手动转换了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 接受和发送复杂类型的消息
*
* @author rxliuli
*/
@Controller
public class ComplexMessageSocket {
/**
* 接收/返回复杂类型 Person 的对象
*
* @param person Person 类对象
* @return Person 类对象
*/
@MessageMapping("/complexMessage")
@SendTo("/topic/complexMessage/allClient")
public Person complexMessage(Person person) {
return new Person().setName("Mr. " + person.getName());
}
}

发送/订阅复杂类型的消息(客户端)

客户端和之前的也差不多,需要注意的就是无论是发送/接受都需要将复杂类型的对象序列化为字符串(JavaScript 原生支持)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 订阅返回复杂类型的消息
stompClient.subscribe('/topic/complexMessage/allClient', res => {
console.log('订阅复杂类型类型的返回消息:{}', JSON.parse(res.body))
})

// 发送一个复杂类型的消息
stompClient.send(
'/complexMessage',
headers,
JSON.stringify({
id: 17,
name: 'rxliuli',
sex: false,
}),
)

WebSocket 客户端封装

每次这么一大堆的代码可以封装一下,吾辈也封装了一个 StopmClient 的客户端工具类,如果有什么不好的地方欢迎提出!

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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
/**
* websocket 连接的工具类
* 该工具类依赖于 sockjs-client 与 webstomp-client 两个类库
* 使用方法:
* 1. 设定 endpoint 属性
* 2. 添加连接成功 / 失败的回调函数进行连接
* 3. 订阅 / 发送消息
* 4. 断开连接
*/
const socketUtil = {
//最大重连次数
maxLen: 10,
//当前重连次数
currentLen: 0,
// 每次连接的时间间隔
timeInterval: 3000,
// 连接的 Socket 节点
endpoint: undefined,
// Socket 连接信息
stompClient: undefined,
socket: undefined,
/**
* Socket 连接的方法
*/
connectWebSocket(successFn, errorFn) {
this.socket = new SockJS(this.endpoint)
this.stompClient = Stomp.over(this.socket)
this.stompClient.connect(this.getHeaders(), successFn, error => {
if (this.currentLen++ < this.maxLen) {
console.log(`Socket 连接失败,将在 ${this.timeInterval / 1000}s 后重试`)
setTimeout(() => this.connectWebSocket(), 3000)
} else {
console.log('Socket 连接失败次数过多,将不再重试')
}
errorFn(error)
})
},
/**
* 断开连接的方法
*/
disconnectWebSocket() {
if (this.stompClient) {
this.stompClient.disconnect(function() {
console.log('断开连接')
}, this.getHeaders())
this.socket.close()
}
},
/**
* 获取当前 Socket 连接的 session id
*/
getSessionId() {
return /\/([^\/]+)\/websocket/.exec(this.socket._transport.url)[1]
},
/**
* 获取一个认证的 headers 信息
* 该方法可以被覆盖
* @return {{"X-Requested-With": string, Authorization: any}} 含有认证信息的 headers 对象
*/
getHeaders() {
return {
'X-Requested-With': 'X-Requested-With',
Authorization: localStorage.token,
}
},
/**
* 发送简单文本类型的消息
*/
sendText(url, body, headers = {}) {
return this.stompClient.send(url, headers, body)
},
/**
* 发送 json 类型的消息
*/
sendJSON(url, body, headers = {}) {
return this.stompClient.send(url, headers, JSON.stringify(body))
},
/**
* 订阅简单文本类型的消息
*/
subscribeText(url, successFn) {
return this.stompClient.subscribe(url, res => successFn(res))
},
/**
* 订阅 json 类型的消息
*/
subscribeJSON(url, successFn) {
return this.stompClient.subscribe(url, res =>
successFn(JSON.parse(res.body)),
)
},
/**
* 取消订阅
* @param obj 订阅对象
*/
unsubscribe(obj) {
if (obj && obj.unsubscribe) {
obj.unsubscribe()
}
},
}

V2Ray 使用教程

介绍

官网, GitHub, 非官方指南

Project V 提供了单一的内核和多种界面操作方式。内核(V2Ray)用于实际的网络交互、路由等针对网络数据的处理,而外围的用户界面程序提供了方便直接的操作流程。

V2Ray 的主要作用是根据用户的配置,对于传入的网络连接进行一定处理,然后发往指定的服务器。它是一个命令行程序,可以接受一个 JSON 格式的配置文件。

服务端

首先,你要有一台可以正常访问的国外服务器,例如 Vultr,最便宜的大概 $3.5/month。

看到官网教程中那么庞大的文档,一般人都会表示瞬间不想玩了吧!

然而现在我们也可以使用 V2Ray 的傻瓜式一键部署脚本了,下面是 GitHub 的项目地址

GitHub

基本上如 GitHub 所述,是为了简化 V2Ray 的部署

  1. 安装

    1
    bash -c "$(curl -fsSL https://git.io/vpOeN)"

    如果脚本链接失效了请复制以下命令到 .sh 文件并执行

    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
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    #!/bin/bash
    export PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin

    # 检查是否为Root
    [ $(id -u) != "0" ] && { echo "Error: You must be root to run this script"; exit 1; }

    # 检查系统信息
    if [ -f /etc/redhat-release ];then
    OS='CentOS'
    elif [ ! -z "`cat /etc/issue | grep bian`" ];then
    OS='Debian'
    elif [ ! -z "`cat /etc/issue | grep Ubuntu`" ];then
    OS='Ubuntu'
    else
    echo "Not support OS, Please reinstall OS and retry!"
    exit 1
    fi

    # 禁用SELinux
    if [ -s /etc/selinux/config ] && grep 'SELINUX=enforcing' /etc/selinux/config; then
    sed -i 's/SELINUX=enforcing/SELINUX=disabled/g' /etc/selinux/config
    setenforce 0
    fi

    # 安装依赖
    if [[ ${OS} == 'CentOS' ]];then
    curl --silent --location https://rpm.nodesource.com/setup_8.x | bash -
    yum install curl wget unzip git ntp ntpdate lrzsz python socat nodejs -y
    npm install -g qrcode
    else
    curl -sL https://deb.nodesource.com/setup_8.x | bash -
    apt-get update
    apt-get install curl unzip git ntp wget ntpdate python socat lrzsz nodejs -y
    npm install -g qrcode
    fi

    # 安装 acme.sh 以自动获取SSL证书
    curl https://get.acme.sh | sh

    # 克隆V2ray.fun项目
    cd /usr/local/
    rm -R v2ray.fun
    git clone https://github.com/tracyone/v2ray.fun

    # 安装V2ray主程序
    bash <(curl -L -s https://install.direct/go.sh)

    # 配置V2ray初始环境
    ln -sf /usr/local/v2ray.fun/v2ray /usr/local/bin
    chmod +x /usr/bin/v2ray
    chmod +x /usr/local/bin/v2ray
    rm -rf /etc/v2ray/config.json
    cp /usr/local/v2ray.fun/json_template/server.json /etc/v2ray/config.json
    let PORT=$RANDOM+10000
    UUID=$(cat /proc/sys/kernel/random/uuid)
    sed -i "s/cc4f8d5b-967b-4557-a4b6-bde92965bc27/${UUID}/g" /etc/v2ray/config.json
    sed -i "s/12345/${PORT}/g" "/etc/v2ray/config.json"
    python /usr/local/v2ray.fun/genclient.py
    python /usr/local/v2ray.fun/openport.py
    service v2ray restart

    # auto open port after start
    # append a new line
    cat /etc/rc.local | grep openport.py
    if [[ $? -ne 0 ]]; then
    cat>>/etc/rc.local<<EOF
    python /usr/local/v2ray.fun/openport.py
    EOF
    chmod a+x /etc/rc.local
    fi

    clear

    echo "V2ray.fun 安装成功!"
    echo "输入 v2ray 回车即可使用"
  2. 使用

    1
    v2ray

    输出如下选项

    1
    2
    3
    4
    5
    6
    7
    8
    欢迎使用 V2ray.fun 管理程序

    1.服务管理
    2.更改配置
    3.查看服务端信息
    4.下载客户端配置文件
    5.更新v2ray和v2ray.fun
    请输入数字选择功能(按回车键退出):
  3. 修改一下端口

    注:此步骤可跳过,但最好修改端口为 80/443(HTTP/HTTPS 默认端口)

    选择 2.更改配置 > 2.更改主端口,输入新的端口,然后在 1.服务管理 中重启服务即可

  4. 下载客户端配置文件

    选择 4.下载客户端配置文件,应该会提示 **保存成功!(/root/config.json)**。下载这个文件到本地,一会还有用。

客户端

首先去 GitHub 下载合适的客户端,这里以 Windows 平台为例。

  1. 首先解压出来,将上面下载的那个文件替换掉解压文件 config.json,然后双击 v2ray.exe 就启动了

  2. 设置浏览器的本地代理
    代理配置为 socks 127.0.0.1:1080 即可

好了,现在浏览器应该可以正常访问 https://www.google.com/

可视化

虽然能够使用了,但每次都是命令行启动着是麻烦了点。说到底,还是需要一个可视化的客户端呢

Project V 客户端

  1. Windows 客户端
    V2RayW 算是比较方便的了,将程序复制到解压目录下面就能用了
  2. 安卓客户端
    BifrostVv2rayNG

Git Push 提示不支持具有 Socks5 方案的代理

场景

使用 Git Push 提交代码到远程服务器时提示了一个错误

1
2
fatal: NotSupportedException encountered.
ServicePointManager 不支持具有 socks5 方案的代理。

问题

然而之后还是正常提交成功了,实际上问题是:

  1. 配置了本地的 socks5 的代理(Shadowsocks 之类的代理软件)
  2. 配置了远程服务器 Git 服务端的 SSH
  3. 本地提交代码到远程服务器时使用的是 http/https 协议

这三者只要有一个不满足就不会出现这个错误了

解决方案

  1. 取消代理
    使用以下简单命令即可取消代理
1
2
git config --global --unset http.proxy
git config --global --unset https.proxy

注:取消代理会出现另外一个错误,所以并不能解决实际问题

1
2
git config --global --unset http.proxy
git config --global --unset https.proxy
  1. 取消远程的 SSH
    在下面的页面中删除你的 SSH Keys 即可

  2. 提交内容到远程 Git 服务器时选择 SSH 协议
    设置远程仓库为 SSH 协议,例如 GitHubSSH 链接就是 <git@github.com:rxliuli/rxliuli.github.io.git>

好了,关于 Git 提示错误 Git Push 提示不支持具有 Socks5 方案的代理 就到这里啦

Xmind 激活

转自 原链,对文法部分进行了复核。本文仅供参考,如有能力请支持正版!

Xmind 不激活也是可以使用的,那么,你可能会问为什么还要激活?这是因为未激活的 Xmind 少了很多实用的功能。好了,废话不多说了,下面讲如何激活 Xmind。

使用激活补丁

  1. 下载 Xmind 激活补丁 XMindCrack.jar

  2. 打开 Xmind 取消检查更新选项 (下面两步可以一起设置)

    -> 编辑 -> 首选项 -> 常规 -> 启动 -> 启动时检查更新和消息:取消勾选 (最后确定)

    取消发送用户数据选项

    -> 编辑 -> 首选项 -> 常规 -> 启动 ->发送用户数据: 取消勾选 (最后确定)
    最后关闭 Xmind

  3. 将 XMindCrack.jar 复制到 Xmind 的安装目录下,默认安装路径为 C:\Program Files (x86)\XMind(如果你是自定义安装的请找到自己的安装目录)

  4. 在 Xmind 的安装目录下找到 XMind.ini 这个文件(部分人隐藏后缀名了,所以显示的是 xmind 这个名字,只要是在 xmind 图标后面的那个就是的)用记事本打开这个文件并在最后添加一行:

    1
    2
    -javaagent:./XMindCrack.jar
    ; 注意此处-javaagent后面的地址应为补丁文件 XMindCrack.jar 的地址,因为我们把该文件放到了Xmind 的安装目录下,Xmind.ini 和 XMindCrack.jar 在同一目录下,因此这里我们可以使用相对路径,如果这两个文件不在同一个目录下,注意填写正确的路径
  5. 断开网络,或者使用防火墙阻止 XMind 联网,或者在 hosts 文件中添加一行127.0.0.1 www.xmind.net(建议采用断网或者增加 hosts 记录法)。其中 hosts 文件一般在:C:\Windows\System32\drivers\etc 目录下

  6. 再次启动 Xmind,进行软件激活 -> 帮助 -> 序列号 -> 输入任意邮箱地址及以下序列号:

    1
    XAka34A2rVRYJ4XBIU35UZMUEEF64CMMIYZCK2FZZUQNODEKUHGJLFMSLIQMQUCUBXRENLK6NZL37JXP4PZXQFILMQ2RG5R7G4QNDO3PSOEUBOCDRYSSXZGRARV6MGA33TN2AMUBHEL4FXMWYTTJDEINJXUAV4BAYKBDCZQWVF3LWYXSDCXY546U3NBGOI3ZPAP2SO3CSQFNB7VVIY123456789012345
  7. 所有步骤完成之后,即可重新联网。

Linux Centos 安装 MongoDB

创建一个 mongodbyum 仓库

1
vim /etc/yum.repos.d/mongodb-org-3.6.repo

内容是

1
2
3
4
5
6
[mongodb-org-3.6]
name=MongoDB Repository
baseurl=https://repo.mongodb.org/yum/amazon/2013.03/mongodb-org/3.6/x86_64/
gpgcheck=1
enabled=1
gpgkey=https://www.mongodb.org/static/pgp/server-3.6.asc

注:此处复制的时候不要漏掉什么内容就好。。。

安装一下

1
yum install -y mongodb-org

创建 mongodb 的数据文件目录

1
mkdir -p /data/db

启动服务测试一下

启动 mongod 服务

1
service mongod start

连接 mongo shell

1
mongo

应该会得到下面的输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
MongoDB shell version v3.6.7
connecting to: mongodb://127.0.0.1:27017
MongoDB server version: 3.6.7
Server has startup warnings:
2018-08-23T08:57:46.048+0800 I STORAGE [initandlisten]
2018-08-23T08:57:46.048+0800 I STORAGE [initandlisten] ** WARNING: Using the XFS filesystem is strongly recommended with the WiredTiger storage engine
2018-08-23T08:57:46.048+0800 I STORAGE [initandlisten] ** See http://dochub.mongodb.org/core/prodnotes-filesystem
2018-08-23T08:57:47.190+0800 I CONTROL [initandlisten]
2018-08-23T08:57:47.190+0800 I CONTROL [initandlisten] ** WARNING: Access control is not enabled for the database.
2018-08-23T08:57:47.190+0800 I CONTROL [initandlisten] ** Read and write access to data and configuration is unrestricted.
2018-08-23T08:57:47.190+0800 I CONTROL [initandlisten]
2018-08-23T08:57:47.190+0800 I CONTROL [initandlisten]
2018-08-23T08:57:47.190+0800 I CONTROL [initandlisten] ** WARNING: /sys/kernel/mm/transparent_hugepage/enabled is 'always'.
2018-08-23T08:57:47.190+0800 I CONTROL [initandlisten] ** We suggest setting it to 'never'
2018-08-23T08:57:47.190+0800 I CONTROL [initandlisten]
2018-08-23T08:57:47.190+0800 I CONTROL [initandlisten] ** WARNING: /sys/kernel/mm/transparent_hugepage/defrag is 'always'.
2018-08-23T08:57:47.190+0800 I CONTROL [initandlisten] ** We suggest setting it to 'never'
2018-08-23T08:57:47.190+0800 I CONTROL [initandlisten]
2018-08-23T08:57:47.190+0800 I CONTROL [initandlisten] ** WARNING: soft rlimits too low. rlimits set to 4096 processes, 65535 files. Number of processes should be at least 32767.5 : 0.5 times number of files.
2018-08-23T08:57:47.190+0800 I CONTROL [initandlisten]

SpringBoot 使用阿里云的短信服务出现问题

异常

1
2
3
4
5
Exception in thread "main" java.lang.NoSuchMethodError: org.json.JSONArray.iterator()Ljava/util/Iterator;
at com.aliyuncs.regions.LocalEndpointResolver.<init>(LocalEndpointResolver.java:39)
at com.aliyuncs.profile.DefaultProfile.<init>(DefaultProfile.java:72)
at com.aliyuncs.profile.DefaultProfile.getProfile(DefaultProfile.java:209)
at com.rx.f3d.common.util.SmsSendUtil.main(SmsSendUtil.java:28)

解决方案

NoSuchMethodError 不能找到方法,吾辈第一感觉就是包冲突了。去网络找了一圈,大概有下面这几种说法

对于吾辈而言,只有最后一种方法是有效的。当然,吾辈修改的版本是 3.3.1 才行的呢

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-core</artifactId>
<version>3.3.1</version>
</dependency>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-dysmsapi</artifactId>
<version>1.1.0</version>
</dependency>

修改完后记得清空缓存并重启 IDE ,然后删除 maven 本地仓库下的 aliyun-java-sdk-core 包以避免缓存问题。

注:吾辈使用的 SpringBoot 版本是 1.5.12.RELEASE,可能和这个也有关系呢

Linux Centos 常用命令

进程

nohup:后台运行命令

例如想要运行一个程序的时候不会因为 SSH 退出而退出,就需要使用这个命令了。在需要执行的命令前面加上 nohup,之后就算用 Ctrl+C 停止了命令行的输出也不会影响刚才运行的命令本身。

setsid:同样是后台运行命令

虽然也是也个后台运行命令,但吾辈最近使用 nohup 总是失败,这个相比之下就安全多了

pkill:根据名字杀死进程

不需要在先使用 ps ef|grep name 查看进程的 pid 再使用 kill -9 pid 去杀死进程了,直接使用 pkill name 就可以杀死进程了呢

whereis:根据名字查看软件的安装位置

安装软件后不知道默认安装位置,使用 whereis 就可以知道啦

1
2
3
4
# 使用 whereis java 查看 java 的安装位置
whereis java
# 结果
# java: /usr/bin/java /usr/lib/java /etc/java /usr/share/java /opt/java/jdk1.8.0_171/bin/java /opt/java/jdk1.8.0_171/jre/bin/java /usr/share/man/man1/java.1.gz

systemctl:系统服务管理

命令格式

1
systemctl option serverName.service

option 有以下常用可选项

  • start:启动一个服务
  • stop: 关闭一个服务
  • restart:重启服务
  • status:查看服务的状态

例如下面的命令就是用于启动 mongod 服务

1
systemctl start mongod.service

service:系统服务管理

和上面的 systemctl:系统服务管理 几乎完全一样的效果,但命令更为简洁/直观

还是以启动 mongod 服务为例

1
2
3
service mongod start
# 系统会直接提示重定向到 systemctl 命令
# Starting mongod (via systemctl):

远程连接

ssh:远程连接到 Linux 服务器

使用 ssh username@ip 就可以连接远程的开启了 SSH 服务端的服务器(Linux 系统默认就有)。
使用 ssh username@ip "ls /" 甚至可以远程发送一些命令到 Linux 服务器执行,对于脚本而言还是挺好的。

scpLinux 下的文件传输工具

使用 scp 命令可以轻易地在本地与服务器之间传输文件,一个基本的示例是:

1
2
# 将本地的 ssh 公钥上传到 Linux 服务器
scp ~/.ssh/id_rsa.pub username@ip:~/.ssh/

具体可以参考:[使用 SCP 上传和下载服务器的文件](./2018-08-10-使用 SSH 连接 Linux 服务器.md)

文件管理

查找文件

命令:find

基本示例

1
2
# 查找所有 .iml 后缀名的文件
find . -name *.iml

删除文件

命令:rm

示例

  • 删除文件: rm file.iml
  • 递归删除目录: rm -rf .git

删除找到的文件

1
2
# 删除当前目录下所有以 .iml 结尾的文件
find . -name *.iml -exec rm {} \;

该页面持续更新中

Linux SSH 远程连接出现 [Host key verification failed]

今早设置了服务器的 SSH 后尝试连接结果出现了这个错误

1
2
3
4
5
6
7
8
9
10
IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY!
Someone could be eavesdropping on you right now (man-in-the-middle attack)!
It is also possible that a host key has just been changed.
The fingerprint for the ECDSA key sent by the remote host is
SHA256:LlGBRJTgQmY2rDmEi/PH6Ql0UF1zX/nbQXHwORhTK1Q.
Please contact your system administrator.
Add correct host key in ~/.ssh/known_hosts to get rid of this message.
Offending ECDSA key in ~/.ssh/known_hosts:14
ECDSA host key for 119.32.78.141 has changed and you have requested strict checking.
Host key verification failed.

关键是最后一句话:主机密钥验证失败

想了一下好像是之前也有设置过一次 SSH,所以这次才会不认了呀,删除掉 ~/.ssh/known_hosts 文件就好啦

在线代理

hidemyass

一个可以访问受限制的网站的在线代理网站,虽然自建了服务器之后基本上不需要这些了,不过作为应急之需还是极好的

Google 镜像网站

目前吾辈不推荐自建的行为,因为学习、使用和维护成本较高,使用一个合适的机场一年下来花个几百块省点心还是值得的。当然,一切都取决于你 – 事物的价值取决于被需要的程度。
具体大佬的评测可以参考 浅谈部分机场(SS/SSR 提供商)的使用感受,这里吾辈仅推荐 BosLife

购买服务器

首先需要需要一台在国外并且能够正常访问的服务器,可以使用 vultr 这个服务提供商,也可以选择其他的。

安装 SSR

先在服务器上安装一下 SSR 的服务端,嗯,其实也就是复制一下命令的事情(具体的操作由脚本帮忙执行了)

1
2
yum -y install wget
wget -N --no-check-certificate https://raw.githubusercontent.com/ToyoDAdoubi/doubi/master/ssr.sh && chmod +x ssr.sh && bash ssr.sh

如果上面的链接失效,请将以下 bash 命令复制到 .sh 文件中执行

注:在你安装完成后显示的信息中不要去查看二维码,因为 SS/SSR 链接在你访问 http://doub.pw 查看二维码的时候被服务器记录,以防万一还是不要去访问比较好。。。

安装 BBR

BBR 是一个单边加速工具,能够保持线路稳定。也安装一下,安装完成重启服务器就好。

1
2
3
4
yum -y install wget
wget --no-check-certificate https://github.com/teddysun/across/raw/master/bbr.sh
chmod +x bbr.sh
./bbr.sh

如果上面的链接失效,请将以下 bash 命令复制到 .sh 文件中执行

安装 SSR 客户端

GitHub

访问上面的 SSR 的 GitHub 地址,在上面找到你所需要的客户端即可(基本上主流平台都有了)