WebSocket简介
为什么使用WebSocket
随着互联网技术的迅速发展,Web应用的多样化趋势日益显著,其中不少应用场景,例如直播间聊天室、实时弹幕等,都需要服务器具备实时推送数据的能力。传统的实现方式是通过轮询技术,即客户端浏览器在固定的时间间隔(例如每1秒)向服务器发起HTTP请求,服务器随后将最新数据返回给客户端。然而,这种模式存在显著的不足,客户端需要频繁地发起请求,而HTTP请求的头部信息通常较长,有效数据却相对较少,这不仅增加了服务器的负担,也造成了带宽资源的极大浪费。
为了解决这些问题,HTML5引入了WebSocket协议,它为服务器与客户端之间的通信提供了更高效的解决方案。WebSocket协议支持全双工通信,这意味着服务器和客户端可以同时进行数据的发送和接收,从而允许服务器在有新数据时主动推送给客户端,无需客户端不断轮询。这种双向实时通信机制显著提高了数据传输的效率,减少不必要的网络请求,有效节省服务器资源和带宽,同时为用户带来更流畅和实时的交互体验。
WebSocket协议的特性
WebSocket通信前,首先客户端与服务器要进行TCP三次握手连接,然后进行一次叫做“握手”的特殊HTTP请求进行协议升级,完成协议升级后原始的HTTP连接被升级到WebSocket连接。在协议升级后,客户端和服务器之间的通信将使用WebSocket协议进行而不再是HTTP,可以在同一个WebSocket连接上进行双向通信。
WebSocket连接一旦经过握手协商成功建立,便能维持活跃状态,使得双方能进行连续不断的双向数据传输,而不必为每个通信回合重新发起连接或等待确认。通过WebSocket,客户端和服务器之间得以建立一种持久、低延迟的连接,极大地提升了数据交换效率。

WebSocket通过数据帧进行通信,它有自己的帧协议格式,头信息更简洁,数据可以作为文本或二进制传输。这种方式减少了持久连接上额外的协议开销,允许更高效的网络交互,能够在节省服务器资源和带宽的同时,提供更优质的实时互动体验。
关于WebSocket协议的更多信息,可参考官方文档The WebSocket Protocol。
WebSocket应用场景
WebSocket主要适用于需要快速、实时的双向通信的应用场景,例如AI应用、在线聊天室、实时通知系统、多人在线游戏、实时市场信息推送等。
场景示例
某公司需要在阿里云上部署Web在线聊天应用,用户可以通过访问域名来接入后端服务进行实时交流。该应用由于其即时通讯的特性,要求用户之间的信息传递必须具备低延迟、高效率和双向实时的特点。
该公司的网站服务面临的挑战是高并发与长连接管理。随着用户数量的增长,传统的HTTP模式无法满足大量用户同时在线并保持实时通信的需求,因为每次通信都需要重新建立连接,这会导致服务器压力剧增且性能低下。
在这个场景下,选用CLB结合WebSocket协议,能够有效解决高并发下的长连接管理问题。通过后端服务器组多服务器部署WebSocket应用程序,并使用Redis进行消息同步,确保服务的高可用性,从而为在线聊天室应用提供了一个可靠、高效的实时信息推送解决方案。

注意事项
CLB的HTTP监听默认支持WebSocket协议。CLB默认支持热更新,即配置变更时不会影响已有长连接。
使用时需要注意如下事项:
操作步骤
步骤一:部署服务
您需要在您的ECS03服务器中部署Redis,在ECS01、ECS02服务器中部署WebSocket应用程序。
本文以CentOS 7.9为示例,演示使用Python快速部署一个简易的在线聊天室测试服务。示例仅供参考,实际使用过程中以您自己开发的程序和服务为准。
在ECS03部署Redis服务
登录ECS03服务器后台。
复制粘贴如下命令,并执行,完成Redis部署与配置。
# 安装 EPEL (Extra Packages for Enterprise Linux)
sudo yum install epel-release -y
# 安装 Redis
sudo yum install redis -y
# 启动并启用 Redis 服务
sudo systemctl start redis
sudo systemctl enable redis
# 检查并编辑 Redis 配置文件,允许远程连接
sudo sed -i 's/^bind 127.0.0.1$/bind 0.0.0.0/' /etc/redis.conf
sudo sed -i 's/^protected-mode yes/protected-mode no/' /etc/redis.conf
# 重启 Redis 服务以使更改生效
sudo systemctl restart redis
# 检查 Redis 是否运行
sudo systemctl status redis
命令执行无报错,并且命令运行完成后,返回如下信息并显示Redis服务为active(running)状态,表示部署与配置成功。
在ECS01中部署WebSocket应用程序
登录ECS01服务器后台。
执行sudo pip3 install flask flask-socketio flask-cors redis
,安装依赖库。
执行vi ECS01_ws.py
,按i
键进入编辑模式。
复制并粘贴如下代码:
说明
注意第13行的redis_url中的IP地址,需要修改为Redis服务器的IP地址,即ECS03的IP地址。
import os
import redis
from flask import Flask, render_template, request
from flask_cors import CORS
from flask_socketio import SocketIO, emit, disconnect
app = Flask(__name__)
app.config['SECRET_KEY'] = 'secret!'
CORS(app)
redis_url = "redis://192.168.*.*:6379/0"
redis_client = redis.StrictRedis.from_url(redis_url)
socketio = SocketIO(app, message_queue=redis_url, manage_session=True, logger=True, engineio_logger=True, cors_allowed_origins="*")
SESSION_PREFIX = "session:"
def set_session_data(session_id, key, value):
redis_client.hset(f"{SESSION_PREFIX}{session_id}", key, value)
def get_session_data(session_id, key):
return redis_client.hget(f"{SESSION_PREFIX}{session_id}", key)
def delete_session_data(session_id):
redis_client.delete(f"{SESSION_PREFIX}{session_id}")
@app.route('/')
def index():
return render_template('index.html')
@socketio.on('connect')
def handle_connect():
try:
session_id = request.sid
print(f"Session {session_id} connected.")
welcome_message = "您已进入聊天室!"
emit('message', welcome_message)
set_session_data(session_id, "username", '')
except Exception as e:
print(f"Error during connection: {str(e)}")
@socketio.on('disconnect')
def handle_disconnect():
try:
session_id = request.sid
username = get_session_data(session_id, "username")
if username:
username = username.decode()
leave_message = f"{username} 已离开聊天室。"
emit('message', leave_message, broadcast=True)
print(leave_message)
delete_session_data(session_id)
print(f"Session {session_id} disconnected.")
except Exception as e:
print(f"Error during disconnection: {str(e)}")
@socketio.on('set_username')
def handle_set_username(username):
session_id = request.sid
set_session_data(session_id, "username", username)
print(f"客户端 {session_id} 的用户名设置为 {username}")
emit('message', f"您的用户名已设置为:{username}")
@socketio.on('message')
def handle_message(msg):
session_id = request.sid
username = get_session_data(session_id, "username")
if username:
username = username.decode()
formatted_message = f"{username}:{msg}"
emit('message', formatted_message, broadcast=True)
print(formatted_message)
else:
warning_message = "发送消息失败:请先设置用户名。"
emit('message', warning_message)
print(warning_message)
if __name__ == '__main__':
if not os.path.exists('templates'):
os.makedirs('templates')
html_code = '''<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>聊天室</title>
<style>
body {
font-family: Arial, sans-serif;
display: flex;
flex-direction: column;
align-items: center;
margin: 0;
padding: 0;
background-color: #f0f0f0;
}
h1 {
color: #333;
}
.chat-container {
width: 90%;
max-width: 600px;
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
.user-container, .message-container {
display: flex;
margin-bottom: 10px;
}
.user-container input, .message-container input {
flex: 1;
padding: 10px;
margin-right: 10px;
border: 1px solid #ccc;
border-radius: 4px;
}
.message-container {
margin-top: 10px;
}
button {
padding: 10px;
background-color: #0056b3;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
button:hover {
background-color: #004099;
}
#messages {
border: 1px solid #ccc;
padding: 10px;
height: 300px;
overflow-y: scroll;
margin-bottom: 10px;
border-radius: 4px;
background-color: #f9f9f9;
}
</style>
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.1/socket.io.js"></script>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
</head>
<body>
<h1>在线聊天室</h1>
<div class="chat-container">
<div class="user-container">
<input type="text" id="username" autocomplete="off" placeholder="请输入用户名">
<button onclick="setUsername()">设置用户名</button>
</div>
<div id="messages"></div>
<div class="message-container">
<input type="text" id="myMessage" autocomplete="off" placeholder="请输入消息...">
<button onclick="sendMessage()">发送</button>
</div>
</div>
<script>
var socket = io({ transports: ['websocket', 'polling', 'flashsocket'] });
var usernameSet = false;
socket.on('connect', function() {
console.log("Connected to the server!");
socket.on('message', function(msg){
$('#messages').append($('<div>').text(msg));
$('#messages').scrollTop($('#messages')[0].scrollHeight);
});
});
function setUsername() {
var username = $('#username').val();
if (username) {
socket.emit('set_username', username);
usernameSet = true; // 设置用户名标识
} else {
alert("用户名不能为空!");
}
}
function sendMessage() {
if (usernameSet) {
var message = $('#myMessage').val();
if (message) {
socket.send(message);
$('#myMessage').val('');
} else {
alert("消息不能为空!");
}
} else {
alert("请先设置用户名!");
}
}
</script>
</body>
</html>
'''
with open('templates/index.html', 'w') as file:
file.write(html_code)
socketio.run(app, host='0.0.0.0', port=5000)
按Esc
键,输入:wq
保存修改。
执行sudo python3 ECS01_ws.py
命令,运行脚本。
当最后显示如下执行结果时,表示WebSocket应用程序已启动,端口为5000。
Server initialized for threading.
* Serving Flask app 'ECS01_ws' (lazy loading)
* Environment: production
WARNING: This is a development server. Do not use it in a production deployment.
Use a production WSGI server instead.
* Debug mode: off
* Running on all addresses.
WARNING: This is a development server. Do not use it in a production deployment.
* Running on http://192.168.*.*:5000/ (Press CTRL+C to quit)
如果出现启动失败,需要排查下端口是否已被占用,或者命令及代码是否复制粘贴错误。
在ECS02中部署WebSocket应用程序
登录ECS02服务器后台。
执行sudo pip3 install flask flask-socketio flask-cors redis
,安装依赖库。
执行vi ECS02_ws.py
,按i
键进入编辑模式。
复制并粘贴如下代码:
说明
注意第13行的redis_url中的IP地址,需要修改为Redis服务器的IP地址,即ECS03的IP地址。
import os
import redis
from flask import Flask, render_template, request
from flask_cors import CORS
from flask_socketio import SocketIO, emit, disconnect
app = Flask(__name__)
app.config['SECRET_KEY'] = 'secret!'
CORS(app)
redis_url = "redis://192.168.*.*:6379/0"
redis_client = redis.StrictRedis.from_url(redis_url)
socketio = SocketIO(app, message_queue=redis_url, manage_session=True, logger=True, engineio_logger=True, cors_allowed_origins="*")
SESSION_PREFIX = "session:"
def set_session_data(session_id, key, value):
redis_client.hset(f"{SESSION_PREFIX}{session_id}", key, value)
def get_session_data(session_id, key):
return redis_client.hget(f"{SESSION_PREFIX}{session_id}", key)
def delete_session_data(session_id):
redis_client.delete(f"{SESSION_PREFIX}{session_id}")
@app.route('/')
def index():
return render_template('index.html')
@socketio.on('connect')
def handle_connect():
try:
session_id = request.sid
print(f"Session {session_id} connected.")
welcome_message = "您已进入聊天室!"
emit('message', welcome_message)
set_session_data(session_id, "username", '')
except Exception as e:
print(f"Error during connection: {str(e)}")
@socketio.on('disconnect')
def handle_disconnect():
try:
session_id = request.sid
username = get_session_data(session_id, "username")
if username:
username = username.decode()
leave_message = f"{username} 已离开聊天室。"
emit('message', leave_message, broadcast=True)
print(leave_message)
delete_session_data(session_id)
print(f"Session {session_id} disconnected.")
except Exception as e:
print(f"Error during disconnection: {str(e)}")
@socketio.on('set_username')
def handle_set_username(username):
session_id = request.sid
set_session_data(session_id, "username", username)
print(f"客户端 {session_id} 的用户名设置为 {username}")
emit('message', f"您的用户名已设置为:{username}")
@socketio.on('message')
def handle_message(msg):
session_id = request.sid
username = get_session_data(session_id, "username")
if username:
username = username.decode()
formatted_message = f"{username}:{msg}"
emit('message', formatted_message, broadcast=True)
print(formatted_message)
else:
warning_message = "发送消息失败:请先设置用户名。"
emit('message', warning_message)
print(warning_message)
if __name__ == '__main__':
if not os.path.exists('templates'):
os.makedirs('templates')
html_code = '''<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>聊天室</title>
<style>
body {
font-family: Arial, sans-serif;
display: flex;
flex-direction: column;
align-items: center;
margin: 0;
padding: 0;
background-color: #f0f0f0;
}
h1 {
color: #333;
}
.chat-container {
width: 90%;
max-width: 600px;
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
.user-container, .message-container {
display: flex;
margin-bottom: 10px;
}
.user-container input, .message-container input {
flex: 1;
padding: 10px;
margin-right: 10px;
border: 1px solid #ccc;
border-radius: 4px;
}
.message-container {
margin-top: 10px;
}
button {
padding: 10px;
background-color: #0056b3;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
button:hover {
background-color: #004099;
}
#messages {
border: 1px solid #ccc;
padding: 10px;
height: 300px;
overflow-y: scroll;
margin-bottom: 10px;
border-radius: 4px;
background-color: #f9f9f9;
}
</style>
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.1/socket.io.js"></script>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
</head>
<body>
<h1>在线聊天室</h1>
<div class="chat-container">
<div class="user-container">
<input type="text" id="username" autocomplete="off" placeholder="请输入用户名">
<button onclick="setUsername()">设置用户名</button>
</div>
<div id="messages"></div>
<div class="message-container">
<input type="text" id="myMessage" autocomplete="off" placeholder="请输入消息...">
<button onclick="sendMessage()">发送</button>
</div>
</div>
<script>
var socket = io({ transports: ['websocket', 'polling', 'flashsocket'] });
var usernameSet = false;
socket.on('connect', function() {
console.log("Connected to the server!");
socket.on('message', function(msg){
$('#messages').append($('<div>').text(msg));
$('#messages').scrollTop($('#messages')[0].scrollHeight);
});
});
function setUsername() {
var username = $('#username').val();
if (username) {
socket.emit('set_username', username);
usernameSet = true; // 设置用户名标识
} else {
alert("用户名不能为空!");
}
}
function sendMessage() {
if (usernameSet) {
var message = $('#myMessage').val();
if (message) {
socket.send(message);
$('#myMessage').val('');
} else {
alert("消息不能为空!");
}
} else {
alert("请先设置用户名!");
}
}
</script>
</body>
</html>
'''
with open('templates/index.html', 'w') as file:
file.write(html_code)
socketio.run(app, host='0.0.0.0', port=5000)
按Esc
键,输入:wq
保存修改。
执行sudo python3 ECS02_ws.py
命令,运行脚本。
当最后显示如下执行结果时,表示WebSocket应用程序已启动,端口为5000。
Server initialized for threading.
* Serving Flask app 'ECS02_ws' (lazy loading)
* Environment: production
WARNING: This is a development server. Do not use it in a production deployment.
Use a production WSGI server instead.
* Debug mode: off
* Running on all addresses.
WARNING: This is a development server. Do not use it in a production deployment.
* Running on http://192.168.*.*:5000/ (Press CTRL+C to quit)
如果出现启动失败,需要排查下端口是否已被占用,或者命令及代码是否复制粘贴错误。
步骤二:配置服务器组
登录传统型负载均衡CLB控制台。
在顶部菜单栏,选择实例所属的地域。
在左侧导航栏,选择实例管理,并在实例管理页面找到目标实例,单击实例ID。
在虚拟服务器组页签单击创建虚拟服务器组。并在创建虚拟服务器组页面配置以下信息,其他参数可保持默认值或根据实际情况修改。完成后单击创建并根据控制台指导完成操作。
配置 | 说明 |
虚拟服务器组名称 | 输入虚拟服务器组名称RS1。 |
在虚拟服务器组页签中,找到已创建的目标虚拟服务器组的操作列单击编辑。
在编辑虚拟服务器组页面,单击添加,并在我的服务器页面根据控制台指导添加后端服务器ECS01与ECS02,注意端口需要配置为WebSocket应用程序端口。本文代码示例中WebSocket应用程序端口为5000。

在编辑虚拟服务器组页面选中已添加的服务器,单击保存。
步骤三:配置HTTP监听
登录传统型负载均衡CLB控制台。
在顶部菜单栏,选择实例所属的地域。
在左侧导航栏,选择实例管理。
在实例管理页面,找到目标实例,在操作列单击监听配置向导。
在协议&监听页面配置以下信息,其他参数可保持默认值或根据实际情况修改。完成后单击下一步。
配置 | 说明 |
选择监听协议 | 选择HTTP。 |
监听端口 | 本文配置端口5000。 |
在后端服务器页面配置以下信息,其他参数可保持默认值或根据实际情况修改。完成后单击下一步。
配置 | 说明 |
选择服务器组 | 选择此前已创建好的虚拟服务器组。 |
在健康检查页面,单击下一步。参数可保持默认值或根据实际情况修改。
在配置审核页面,检查配置参数是否有误,无误的话单击提交,等待监听创建完成。
步骤四:配置域名解析
在左侧导航栏,选择。
在实例管理页面,选择目标实例,并复制目标实例的服务地址。
执行以下步骤添加A解析记录。
登录域名解析控制台。
在权威域名解析页面,找到目标域名,在操作列单击解析设置。
在解析设置页面,单击添加记录。
在添加记录面板配置以下信息,其他参数可保持默认值或根据实际情况修改,完成后单击确定。
配置 | 说明 |
记录类型 | 在下拉列表中选择A。 |
主机记录 | 您的域名的前缀。 |
记录值 | 输入域名对应的A地址,即您复制的CLB实例的服务地址。 |
步骤五:结果验证
准备2个不同IP地址的能够访问公网的终端电脑,通过在浏览器输入聊天消息并查看效果,验证CLB使用WebSocket协议实现信息实时推送。
在浏览器中输入http://域名:5000
,访问在线聊天室应用。
页面访问成功示例:

如果您打开了浏览器开发者工具,您可以在网络或Network页签看到,浏览器已经在使用WebSocket协议进行通信。

输入用户名用于后续聊天交互,完成后单击设置用户名。
在不同终端电脑中,分别输入多条聊天消息并单击发送,进行测试。
所有浏览器中,均可以实时收到消息。

如上验证过程表明,通过CLB使用WebSocket协议实现了信息实时推送,并且实现了高可用。
常见问题
如何使用WebSocket Secure协议?
WebSocket Secure是WebSocket协议的加密版本。
HTTPS监听默认支持WebSocket Secure协议。您在配置监听时,选择使用HTTPS监听,即可使用WebSocket Secure协议。
使用WebSocket收费吗?
WebSocket和WebSocket Secure协议不额外收取费用。
哪些地域支持WebSocket?
CLB支持的地域,都已支持WebSocket和WebSocket Secure。