0%

在内网搭建ws服务器出现的一些问题

WebSocket服务器搭建时的一些问题

在搭建自己的一个内网网站时,将这个网站通过Sakura Frp穿了出去,因为Sakura_Frp开启隧道时打开了自动https(必须开不然访问不了网站),所以直接访问网站是https的,只不过会显示不安全(不受信任),网页还是可以正常访问的。

那么问题出在哪呢,昨天尝试在内网使用php写了一个websocket的聊天室demo,那么浏览器访问这个聊天室的时候先用https发送一个upgrade的请求,然后尝试websocket,但是通过frp的这个公网发送的请求,到了websocket会变成wss而不是ws,网页会返回状态码426,websocket尝试连接wss又不能强制转换成ws,使用frp访问网站也不能使用http访问,必须要https,所以应该需要搞一个CA证书。无法连接的报错如下:

Mixed Content: The page at ‘https://frp-xxx.xxx:port/demoPage/‘ was loaded over HTTPS, but attempted to connect to the insecure WebSocket endpoint ‘ws://xxx.xxx.xxx.xxx:port/chat’. This request has been blocked; this endpoint must be available over WSS. Uncaught DOMException: Failed to construct ‘WebSocket’: An insecure WebSocket connection may not be initiated from a page loaded over HTTPS.

已有配置:使用php和Apache搭建的网站(80端口),本地的80端口通过frp-xxx.xxx:port暴露在公网上,本地开启了一个简单的websocket服务器,ws服务器监听本地8081端口。

第一次的思路

想使用Apache反向代理,因为网站是在frp-xxx.xxx:port上的,所以使用Apache代理一个frp-xxx.xxx:port/chat/,将访问这个网址时的流量转发到本地的127.0.0.1:8081。

所以第一次的代码是这样写的:

index.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
36
37
38
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WebSocket Chat Room</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 20px;
}
#messages {
width: 400px;
height: 300px;
border: 1px solid #ccc;
overflow-y: scroll;
padding: 10px;
}
#messageInput {
width: 300px;
padding: 10px;
margin-right: 10px;
}
#sendButton {
padding: 10px 20px;
}
</style>
</head>
<body>
<h1>WebSocket Chat Room</h1>
<div id="messages"></div>
<input type="text" id="messageInput" placeholder="Input Messages...">
<button id="sendButton">Send</button>

<script src="chat.js"></script>
</body>
</html>

websocket-server.php:(放在这里也可以抄一下)

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
<?php
require dirname(__DIR__) . '/composerApp/vendor/autoload.php';

use Ratchet\Server\IoServer;
use Ratchet\Http\HttpServer;
use Ratchet\WebSocket\WsServer;
use Ratchet\ConnectionInterface;
use Ratchet\MessageComponentInterface;

class Chat implements MessageComponentInterface {
protected $clients;

public function __construct() {
$this->clients = new \SplObjectStorage;
}

public function onOpen(ConnectionInterface $conn) {
$this->clients->attach($conn);
echo "新连接! ({$conn->resourceId})\n";
}

public function onMessage(ConnectionInterface $from, $msg) {
foreach ($this->clients as $client) {
if ($from !== $client) {
$client->send($msg);
}
}
}

public function onClose(ConnectionInterface $conn) {
$this->clients->detach($conn);
echo "连接 {$conn->resourceId} 已断开\n";
}

public function onError(ConnectionInterface $conn, \Exception $e) {
echo "发生错误: {$e->getMessage()}\n";
$conn->close();
}
}

$server = IoServer::factory(
new HttpServer(
new WsServer(
new Chat
)
),
8081
);

$server->run();

PS:如何建立上面这样一个服务器?

首先安装Composer,看完官方文档还是不知道咋安装就问问AI

安装完后,composer -v 有输出就说明安好了

新建一个项目文件夹,然后在文件夹里 composer init,init的时候可以直接无脑回车,因为这里的conf配置也没啥用, 然后安装 Ratchet

1
composer require cboden/ratchet

安装好就行了,注意websocket-server.php的第一行,如果报错说什么文件找不到,就是第一行的文件夹路径错了

chat.js:

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
// document.addEventListener('DOMContentLoaded', function () {
// const messages = document.getElementById('messages');
// const messageInput = document.getElementById('messageInput');
// const sendButton = document.getElementById('sendButton');

// // 创建WebSocket连接
// //const wsUri = "wss://60.215.128.110:62588/chat";
// //const wsUri = "wss://192.168.22.39:8081/";
// const wsUri = "wss://frp-art.top:54456/";
// const websocket = new WebSocket(wsUri);

// websocket.onopen = function (evt) {
// console.log("连接已打开");
// appendMessage("Connected Server successfully!");
// };

// websocket.onclose = function (evt) {
// console.log("连接已关闭");
// appendMessage("Link from server closed.");
// };

// websocket.onmessage = function (evt) {
// console.log("收到消息: " + evt.data);
// appendMessage("Sombody: " + evt.data);
// };

// websocket.onerror = function (evt) {
// console.error("发生错误: " + evt.data);
// appendMessage("something error...please try to refresh page");
// };

// var input = document.getElementById('messageInput');
// input.addEventListener('keydown', function (event) {
// // 检查按下的键是否是回车键
// if (event.key === 'Enter' || event.keyCode === 13) {
// // 如果Shift键也被按下,则不阻止默认行为(允许换行)
// if (event.shiftKey) {
// return; // 不执行任何操作,允许默认的换行行为
// } else {
// // 否则,阻止默认行为(阻止表单提交)并发送消息
// event.preventDefault(); // 阻止表单提交
// const message = input.value.trim();
// if (message) {
// websocket.send(message);
// appendMessage("Me: " + message);
// input.value = '';
// }
// }
// }
// });

// // 发送消息
// sendButton.onclick = function () {
// const message = messageInput.value.trim();
// if (message) {
// websocket.send(message);
// appendMessage("Me: " + message);
// messageInput.value = '';
// }
// };

// // 在消息框中追加消息
// function appendMessage(message) {
// const messageElement = document.createElement('div');
// messageElement.textContent = message;
// messages.appendChild(messageElement);
// messages.scrollTop = messages.scrollHeight;
// }
// });

/etc/apache2/sites-available/000-default.conf:Apache配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
<VirtualHost *:80>
ServerAdmin webmaster@localhost
DocumentRoot /xxx/html # 根据自己的设定

ErrorLog ${APACHE_LOG_DIR}/error.log
CustomLog ${APACHE_LOG_DIR}/access.log combined

<Location /chat>
ProxyPass ws://127.0.0.1:8081/
ProxyPassReverse ws://127.0.0.1:8081/
</Location>
</VirtualHost>

以上全都配置好之后,能够访问成功这个index.html页面,但是连接ws:127.0.0.1:8081是不成功的。

WHY?

使用的内网穿透工具是Sakura Frp,用frpc穿透之后,访问网站是必须要使用https的,那么,使用https连接到网站之后,Websocket是通过HTTPS运行的,意味着WebSocket连接是使用 SSL/TLS加密的,所以,对于wss的访问需要有认证的证书,但是我妹有证书啊QAQ,但是我这里也有一个问题,既然是用https访问的网站,那我用这个https网站在apache配置反向代理为啥不行呢

又看了一下为什么连接不上呢,因为这个js是相当于被浏览器加载到了本地,所以,我使用我的Windows电脑访问127.0.0.1:8081相当于访问的是我自己Windows电脑上的8081端口,那这当然是访问不到的,所以这个wsUri应该填的是公网上的一个ws服务器,所以又想了个办法,用frpc再建一条隧道,把ws服务器再穿出去。

这个方法后来试了试,当然是行得通了的。

于是代码变成了这样:

image-20241220164533172

a63e07e755ab0376578dafc8d320c715

但这也有问题,安全性问题,ws服务器的地址很容易被获取到,被其他人利用,所以后期会做一些js隐藏、混淆之类的



















附一个群组聊天的websocket.php

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
<?php
require __DIR__ . '/vendor/autoload.php';

use Ratchet\MessageComponentInterface;
use Ratchet\ConnectionInterface;
use Ratchet\Http\HttpServer;
use Ratchet\WebSocket\WsServer;
use Ratchet\Server\IoServer;

class Chat implements MessageComponentInterface {
protected $clients;
protected $groups;

public function __construct() {
$this->clients = new \SplObjectStorage;
$this->groups = [];
}

public function onOpen(ConnectionInterface $conn) {
// 存储新连接的客户端
$this->clients->attach($conn);
echo "New connection! ({$conn->resourceId})\n";
}

public function onMessage(ConnectionInterface $from, $msg) {
// 解析消息,消息格式为 "key:message"
$parts = explode(':', $msg, 2);
if (count($parts) == 2) {
list($key, $message) = $parts;

// 如果还没有这个key的群组,则创建一个新的群组
if (!isset($this->groups[$key])) {
$this->groups[$key] = new \SplObjectStorage();
}

// 将客户端加入到对应key的群组中
if (!$this->groups[$key]->contains($from)) {
$this->groups[$key]->attach($from);
}

// 向所有使用相同key连接的客户端转发消息
foreach ($this->groups[$key] as $client) {
if ($from !== $client) {
$client->send($message);
}
}
}
}

public function onClose(ConnectionInterface $conn) {
// 移除关闭连接的客户端
$this->clients->detach($conn);
foreach ($this->groups as $key => $group) {
if ($group->contains($conn)) {
$group->detach($conn);
}
}
echo "Connection {$conn->resourceId} has disconnected\n";
}

public function onError(ConnectionInterface $conn, \Exception $e) {
echo "An error has occurred: {$e->getMessage()}\n";
$conn->close();
}
}

$server = IoServer::factory(
new HttpServer(
new WsServer(
new Chat()
)
),
8080
);

$server->run();

对应的js:

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
var key = 'aaa群组'; // 根据这个值进行组连接
var value = '张三'; // 根据这个值进行消息转发(类似于UUID)
var conn = new WebSocket('ws://localhost:8080');

conn.onopen = function (e) {
console.log("Connection established!");
// 在连接成功后发送消息
sendMessage('Hello, this is a message to specific group!');
};

conn.onmessage = function (e) {
console.log(e.data);
};

conn.onerror = function (e) {
console.log('Error: ', e);
};

conn.onclose = function (e) {
console.log('Connection closed');
};

function sendMessage(message) {
conn.send(key + ':' + value + ':' + message);
}