写点什么

Serverless 实战:利用云函数 + API 网关实现 Websocket 聊天工具

  • 2020-06-16
  • 本文字数:9259 字

    阅读完需:约 30 分钟

Serverless实战:利用云函数 + API网关实现Websocket聊天工具

如果是传统技术栈想要实现 Websocket 会比较容易,但是函数计算由于不支持长连接操作,由事件驱动,所以实现起来会有难度。本文将结合函数计算与 API 网关,尝试由 Websocket 实现一个聊天工具。

API 网关触发器实现 Websocket

WebSocket 协议是基于 TCP 的一种新的网络协议。它实现了浏览器与服务器全双工(full-duplex)通信,即允许服务器主动发送信息给客户端。WebSocket 在服务端有数据推送需求时,可以主动发送数据至客户端。而原有 HTTP 协议的服务端对于需推送的数据,仅能通过轮询或 long poll 的方式来让客户端获得。


由于云函数是无状态且以触发式运行,即在有事件到来时才会被触发,因此,为了实现 WebSocket,需要云函数与 API 网关相结合,通过 API 网关承接、保持与客户端的连接,可以认为 API 网关与 SCF 一起实现了服务端。当客户端有消息发出时,会先传递给 API 网关,再由 API 网关触发云函数执行。当服务端云函数要向客户端发送消息时,会先由云函数将消息 POST 到 API 网关的反向推送链接,再由 API 网关向客户端完成消息的推送。具体的实现架构如下:



对于 WebSocket 的整个生命周期,主要由以下几个事件组成:


  • 连接建立:客户端向服务端请求建立连接并完成连接建立。

  • 数据上行:客户端通过已经建立的连接向服务端发送数据。

  • 数据下行:服务端通过已经建立的连接向客户端发送数据。

  • 客户端断开:客户端要求断开已经建立的连接。

  • 服务端断开:服务端要求断开已经建立的连接。


对于 WebSocket 整个生命周期的事件,云函数和 API 网关的处理过程如下:


  • 连接建立:客户端与 API 网关建立 WebSocket 连接,API 网关将连接建立事件发送给 SCF。

  • 数据上行:客户端通过 WebSocket 发送数据,API 网关将数据转发送给 SCF。

  • 数据下行:SCF 通过向 API 网关指定的推送地址发送请求,API 网关收到后会将数据通过 WebSocket 发送给客户端。

  • 客户端断开:客户端请求断开连接,API 网关将连接断开事件发送给 SCF。

  • 服务端断开:SCF 通过向 API 网关指定的推送地址发送断开请求,API 网关收到后断开 WebSocket 连接。


API 网关与 SCF 之间的交互需要由 3 类云函数来承载:


  • 注册函数:在客户端发起和 API 网关之间建立 WebSocket 连接时触发该函数,通知 SCF WebSocket 连接的 secConnectionID。通常会在该函数记录 secConnectionID 到持久存储中,用于后续数据的反向推送。

  • 清理函数:在客户端主动发起 WebSocket 连接中断请求时触发该函数,通知 SCF 准备断开连接的 secConnectionID。通常会在该函数清理持久存储中记录的该 secConnectionID。

  • 传输函数:在客户端通过 WebSocket 连接发送数据时触发该函数,告知 SCF 连接的 secConnectionID 以及发送的数据。通常会在该函数处理业务数据。例如,是否将数据推送给持久存储中的其他 secConnectionID。

Websocket 功能实现

下图是腾讯云官网提供的整体架构图:



我们可以使用 COS(对象存储)作为持久化的方案,当用户建立链接存储 ConnectionId 到 COS 中,用户断开连接时删除该链接 Id。


注册函数:



# -*- coding: utf8 -*-
import os
from qcloud_cos_v5 import CosConfig
from qcloud_cos_v5 import CosS3Client
bucket = os.environ.get('bucket')
region = os.environ.get('region')
secret_id = os.environ.get('secret_id')
secret_key = os.environ.get('secret_key')
cosClient = CosS3Client(CosConfig(Region=region, SecretId=secret_id, SecretKey=secret_key))

def main_handler(event, context):
print("event is %s" % event)
connectionID = event['websocket']['secConnectionID']
retmsg = {}
retmsg['errNo'] = 0
retmsg['errMsg'] = "ok"
retmsg['websocket'] = {
"action": "connecting",
"secConnectionID": connectionID
}
cosClient.put_object(
Bucket=bucket,
Body='websocket'.encode("utf-8"),
Key=str(connectionID),
EnableMD5=False
)
return retmsg

复制代码


传输函数:



# -*- coding: utf8 -*-
import os
import json
import requests
from qcloud_cos_v5 import CosConfig
from qcloud_cos_v5 import CosS3Client
bucket = os.environ.get('bucket')
region = os.environ.get('region')
secret_id = os.environ.get('secret_id')
secret_key = os.environ.get('secret_key')
cosClient = CosS3Client(CosConfig(Region=region, SecretId=secret_id, SecretKey=secret_key))
sendbackHost = os.environ.get("url")

def Get_ConnectionID_List():
response = cosClient.list_objects(
Bucket=bucket,
)
return [eve['Key'] for eve in response['Contents']]

def send(connectionID, data):
retmsg = {}
retmsg['websocket'] = {}
retmsg['websocket']['action'] = "data send"
retmsg['websocket']['secConnectionID'] = connectionID
retmsg['websocket']['dataType'] = 'text'
retmsg['websocket']['data'] = data
requests.post(sendbackHost, json=retmsg)

def main_handler(event, context):
print("event is %s" % event)
connectionID_List = Get_ConnectionID_List()
connectionID = event['websocket']['secConnectionID']
count = len(connectionID_List)
data = event['websocket']['data'] + "(===Online people:" + str(count) + "===)"
for ID in connectionID_List:
if ID != connectionID:
send(ID, data)
return "send success"

复制代码


清理函数:



# -*- coding: utf8 -*-
import os
import requests
from qcloud_cos_v5 import CosConfig
from qcloud_cos_v5 import CosS3Client
bucket = os.environ.get('bucket')
region = os.environ.get('region')
secret_id = os.environ.get('secret_id')
secret_key = os.environ.get('secret_key')
cosClient = CosS3Client(CosConfig(Region=region, SecretId=secret_id, SecretKey=secret_key))
sendbackHost = os.environ.get("url")

def main_handler(event, context):
print("event is %s" % event)
connectionID = event['websocket']['secConnectionID']
retmsg = {}
retmsg['websocket'] = {}
retmsg['websocket']['action'] = "closing"
retmsg['websocket']['secConnectionID'] = connectionID
requests.post(sendbackHost, json=retmsg)
cosClient.delete_object(
Bucket=bucket,
Key=str(connectionID),
)
return event

复制代码


Yaml 格式如下所示:



Conf:
component: "serverless-global"
inputs:
region: ap-guangzhou
bucket: chat-cos-1256773370
secret_id:
secret_key:
myBucket:
component: '@serverless/tencent-cos'
inputs:
bucket: ${Conf.bucket}
region: ${Conf.region}
restApi:
component: '@serverless/tencent-apigateway'
inputs:
region: ${Conf.region}
protocols:
- http
- https
serviceName: ChatDemo
environment: release
endpoints:
- path: /
method: GET
protocol: WEBSOCKET
serviceTimeout: 800
function:
transportFunctionName: ChatTrans
registerFunctionName: ChatReg
cleanupFunctionName: ChatClean

ChatReg:
component: "@serverless/tencent-scf"
inputs:
name: ChatReg
codeUri: ./code
handler: reg.main_handler
runtime: Python3.6
region: ${Conf.region}
environment:
variables:
region: ${Conf.region}
bucket: ${Conf.bucket}
secret_id: ${Conf.secret_id}
secret_key: ${Conf.secret_key}
url: [](http://set-gwm9thyc.cb-guangzhou.apigateway.tencentyun.com/api-etj7lhtw)
ChatTrans:
component: "@serverless/tencent-scf"
inputs:
name: ChatTrans
codeUri: ./code
handler: trans.main_handler
runtime: Python3.6
region: ${Conf.region}
environment:
variables:
region: ${Conf.region}
bucket: ${Conf.bucket}
secret_id: ${Conf.secret_id}
secret_key: ${Conf.secret_key}
url: [](http://set-gwm9thyc.cb-guangzhou.apigateway.tencentyun.com/api-etj7lhtw)
ChatClean:
component: "@serverless/tencent-scf"
inputs:
name: ChatClean
codeUri: ./code
handler: clean.main_handler
runtime: Python3.6
region: ${Conf.region}
environment:
variables:
region: ${Conf.region}
bucket: ${Conf.bucket}
secret_id: ${Conf.secret_id}
secret_key: ${Conf.secret_key}
url: [](http://set-gwm9thyc.cb-guangzhou.apigateway.tencentyun.com/api-etj7lhtw)

复制代码


需要注意的是,我们要先部署 API 网关,完成之后获得回推地址,将回推地址以 url 的形式写入到对应函数的环境变量中:



从理论上来讲,这里的设计不是很合理,按道理我们是可以通过${restApi.url[0].internalDomain}自动获得 url,但是我并没有成功获得到 url,所以只能先部署 API 网关,获得地址之后,再重新部署。


部署完成之后,我们可以编写 HTML 代码实现可视化的 Websocket Client,其核心的 JavaScript 代码为:



window.onload = function () {
var conn;
var msg = document.getElementById("msg");
var log = document.getElementById("log");
function appendLog(item) {
var doScroll = log.scrollTop === log.scrollHeight - log.clientHeight;
log.appendChild(item);
if (doScroll) {
log.scrollTop = log.scrollHeight - log.clientHeight;
}
}
document.getElementById("form").onsubmit = function () {
if (!conn) {
return false;
}
if (!msg.value) {
return false;
}
conn.send(msg.value);
//msg.value = "";

var item = document.createElement("div");
item.innerText = "发送↑:";
appendLog(item);

var item = document.createElement("div");
item.innerText = msg.value;
appendLog(item);

return false;
};
if (window["WebSocket"]) {
//替换为websocket连接地址
conn = new WebSocket("ws://service-01era6ni-1256773370.gz.apigw.tencentcs.com/release/");
conn.onclose = function (evt) {
var item = document.createElement("div");
item.innerHTML = "<b>Connection closed.</b>";
appendLog(item);
};
conn.onmessage = function (evt) {
var item = document.createElement("div");
item.innerText = "接收↓:";
appendLog(item);

var messages = evt.data.split('\n');
for (var i = 0; i < messages.length; i++) {
var item = document.createElement("div");
item.innerText = messages[i];
appendLog(item);
}
};
} else {
var item = document.createElement("div");
item.innerHTML = "<b>Your browser does not support WebSockets.</b>";
appendLog(item);
}
};

复制代码


完成之后,我们打开两个页面进行测试:


总结

通过云函数 + API 网关进行 Websocket 的实践,绝对不仅仅只是一个聊天工具,它可以实现很多功能,例如通过 Websocket 进行实时日志系统的制作等。


单独的函数计算仅仅是一个计算平台,只有和周边的 BaaS 结合才能展示出 Serverless 架构的价值和真正的能力、意义。这也是为什么很多人说 Serverless=FaaS+BaaS 的一个原因。


2020-06-16 15:455423

评论

发布
暂无评论
发现更多内容

阿里巴巴官方上线!号称国内Java八股文天花板(终极版)首次开源

Java你猿哥

Java 微服务 算法 JVM 多线程

SPFA 算法:实现原理及其应用

繁依Fanyi

算法 SPFA

分享:集群吞吐量以1抵5,车企MySQL八大痛点的解决方案

OceanBase 数据库

数据库 oceanbase

Github高赞!Alibaba最新亿级并发系统架构(2023 版全彩小册)

Java你猿哥

Java 架构 分布式 高并发 架构设计

从0到1:可自定义数据列的成绩查询小程序开发笔记

CC同学

多种文件清理:Disk Cleanup Pro 激活版

真大的脸盆

Mac Mac 软件 磁盘清理 清理工具

如何保证 RabbitMQ 的消息可靠性

小小怪下士

Java 程序员 RabbitMQ 消息中间件

基于 Rainbond 的混合云管理解决方案

北京好雨科技有限公司

Kubernetes 云原生 rainbond 混合云架构

未来市场主流的五大LED显示屏

Dylan

技术 方案 LED显示屏

【OpenAI】私有框架代码生成实践 | 京东云技术团队

京东科技开发者

openai ChatGPT ChatGPT4 企业号 5 月 PK 榜 私有框架

架构师必备!阿里P8耗时6个月手码架构师进阶笔记真的香

Java你猿哥

架构 前端架构 架构设计 架构师 后端架构

Wallys AP controllers devices/PQ4019 and IPQ4029 chipsets support 20 km remote transmission

Cindy-wallys

IPQ4019 ipq4029

你想要的【微前端】都在这里了! | 京东云技术团队

京东科技开发者

前端 微前端 微前端框架 企业号 5 月 PK 榜 mirco

面向万物智联的应用框架的思考和探索(上)

HarmonyOS开发者

HarmonyOS

从0开始:活动打卡小程序开发笔记

CC同学

数说热点 | 跟着《长月烬明》起飞,今年各地文旅主打的就是一个听劝

MobTech袤博科技

CH32V307V-EVT-R1 简单上手入门

繁依Fanyi

嵌入式

景区共享电动车与校内共享电单车是否可行

共享电单车厂家

共享电动车厂家 景区共享电单车 校内共享电单车 共享电动车投放

字节首次公开!23年Java后端面试上岸手册 ,竟含全套后端面试考点

Java你猿哥

Java 算法 JVM 多线程 java面试

主流框架都用SPI机制,看一下他们的区别和原理

Java你猿哥

ssm 框架 JavaSPI Spring SPI Dubbo SPI

MySQL 并行复制方案演进历史及原理分析

Java你猿哥

Java MySQL ssm 并行复制 主从延迟

阿里P8撰写1500页程序性能调优笔记:GitHub标星79k

程序知音

Java 性能优化 JVM java架构 Java进阶

利用Python分析快手APP全国大学生用户数据(2022 年初赛第四题 )

繁依Fanyi

大数据

一条SQL如何被MySQL架构中的各个组件操作执行的

华为云开发者联盟

sql 开发 华为云 华为云开发者联盟 企业号 5 月 PK 榜

Apache Pulsar 在火山引擎 EMR 的集成与场景

字节跳动数据平台

大数据 开源 云原生 解决方案 企业号 5 月 PK 榜

工业互联网:加速从“中国制造”迈向“中国智造”

华为云开发者联盟

云计算 工业互联网 华为云 华为云开发者联盟 企业号 5 月 PK 榜

Python网络爬虫原理及实践 | 京东云技术团队

京东科技开发者

Python 爬虫 python 爬虫 爬虫入门 企业号 5 月 PK 榜

神秘的IP地址8.8.8.8地址到底是什么?为什么会被用作DNS服务器地址呢?

wljslmz

DNS 三周年连更

Zero-ETL、大模型和数据工程的未来

Baihai IDP

人工智能 大模型 数据工程 企业号 5 月 PK 榜 LLMs

刘强:作业帮给OceanBase提了九条意见

OceanBase 数据库

数据库 oceanbase

Serverless实战:利用云函数 + API网关实现Websocket聊天工具_服务革新_刘宇_InfoQ精选文章