现在聊天和视频会议应用火遍全球。 Slack、Microsoft Teams、Zoom、Google Meet、Facebook Rooms等应用程序越来越受欢迎。这是因为Covid-19大流行,我们所有人不得不呆在家里,所以掌握在线工作和协作的能力变得非常必要。聊天和视频会议应用解决了我们的困境,并提供了有效的远程团队协作工具,所以其用户和收入有了巨大增长。
虽然大多数这样的应用程序都提供免费选项,但如果你想使用一些高级功能(比如更大的聊天组,更多的会议参与者或更高质量的视频/音频)就要花钱了。
如果我们可以创建自己的Slack应用呢?
我指的不是那种网上一抓一大把的聊天演示应用,而是更加复杂,类似于完全由以下功能组成的应用程序:
- 多达50人的多方通话和视频会议;
- 强大的富文本聊天功能,包含表情符号、字样、文件共享、提示和回复;
- 支持采用多种形式的对话频道,包括私人的频道(房间加密码)、公开的频道(仅需输入房间名称即可加入)、多频道(在一个房间内可支持两人或多人发起私密对话,类似场景可以参考在线教育中的超级小班课场景),并且可支持简单、高效的会议组织形式(比如主持人+观众);
除此之外,还有可以在本地或云中部署的这种功能。
好吧,这正是我们要做的内容。我们把这个项目命名为Roomler。
各位,你可以将其视为“ Slack on Crack”或“ Microsoft Teams on Wheels”项目,以及所有免费和开源的产品。
事不宜迟,让我们开始深入讨论。
行动起来!
俗话说得好,行动胜于雄辩。首先我们看看该项目实操如何。要了解最终结果,请观看这个介绍视频:
构建功能强大的视频会议和团队协作工具
技术栈
我们将使用最先进的开源技术来搭建技术栈:
Janus Gateway:Meetecho优秀的通用WebRTC服务器(SFU);
Coturn :TURN和STUN Server的免费开源项目;
Fastify :用于Node.js的快速、低开销的Web框架;
PM2 :Node.JS的生产过程管理器;
MongoDB :为现代应用程序开发人员和云时代构建的基于文档的分布式数据库;
Redis :内存中的数据结构存储,用作数据库,缓存和消息代理;
VueJS :渐进式JavaScript框架;
NuxtJS:通用应用程序的元框架;
VuetifyJS :材质设计UI组件框架;
Nginx :高性能负载平衡器,Web服务器和有HTTP3 / Quiche和Brtoli支持的反向代理;
Docker:用于构建、部署和管理容器化应用程序的平台。
基于微服务的架构
整个应用程序由以下几个微服务(Docker容器)组成:
- MongoDB :官方图像(386MB);
- Redis:Bitnami图像(105MB);
- Coturn :高山:基于边缘的图像(20MB);
- Janus:Debian:精巧的形象(170MB);
- Roomler:Debian:buster-slim NodeJS安装的图像(622MB);
- Nginx :Debian:有HTTP3 / Quic + Brotli支持的buster-slim映像(107MB)。

组装微服务
先决条件——我们需要两个docker网络:
docker network create frontend # for nginx<->roomler docker network create backend # for roomler<->[mongo, redis]
MongoDB
用于存储以下内容的数据库:
- 用户;
- 房间;
- 信息;
- 连接数量;
- 通话请求等等..
我们将使用MongoDB,通过以下方式启动一个MongoDB容器:
docker run -d --name mongo \ --restart=always \ -p 27017:27017 \ -e MONGO_INITDB_ROOT_USERNAME=SuperAdmin \ -e MONGO_INITDB_ROOT_PASSWORD=SuperAdminSecret \ -v mongo_data:/data/db \ --network=backend \ mongo
启动MongoDB容器后,你可以登录进来:
mongo roomlerdb -u SuperAdmin -p SuperAdminSecret --host your_host --port 27017
并为Roomler应用创建用户:
db.createUser(
{
user: "roomler",
pwd: "super_secret",
roles: [ { role: "readWrite", db: "roomlerdb" } ]
}
)
然后,您的MongoDB连接字符串会作为环境变量传递到roomler中(请参阅下面的Roomler部分),如下所示:
为了简单起见,我们仅启动一个mongo实例。在实际场景中,你可以设置mongo实例集群。关于如何设置mongo集群,详见这篇文章。
下面我们继续单个实例的操作。
Redis
为了使这些容器能够相互通信,Roomler Chat需要有可扩展的功能(即支持多个可复制的容器),所以我们选择使用Redis的PUB / SUB机制。
举个例子,通过容器C1中Web Sockets收到的聊天消息会发布到Redis,以便其他订阅Redis的容器Ci也可以接收这些信息。

接下来,在没有密码的情况下启动Redis。即使我们不建议这样做,但这不会造成多大困扰。因为Internet无法访问Redis,只能通过后端Docker Bridge网络访问。
docker run -d --name redis \
--restart=always \
-e ALLOW_EMPTY_PASSWORD=yes \
--network=backend \
bitnami/redis:latest
Coturn
为了支持NAT身后(即具有专用IP地址)的用户加入视频会议,我们需要一个TURN服务器。建议大家直接在dockers host网络上运行此程序:
docker run -d \
--name="coturn" \
--restart="always" \
--net=host \
instrumentisto/coturn -n \
--lt-cred-mech
--fingerprint \
--no-multicast-peers \
--cli-password=MyTopSecret \
--no-tlsv1 \
--no-tlsv1_1 \
--fingerprint \
--lt-cred-mech \
--verbose \
--user=SuperUser:MyTopSecret \
--server-name=your_domain \
--realm=your_domain \
--listening-ip='$(detect-external-ip)' \
--min-port=10200 \
--max-port=49200
Janus
我们会依靠浏览器的WebRTC连接视频会议。 但由于WebRTC是p2p协议,在视频会议下,默认的MESHED可扩展功能不太行。所以我们选择通过优秀的Janus网关实现SFU(选择性转发单元)拓扑。

像Coturn一样,我们建议大家把janus也连接到dockers主机网络,以避免在加入视频会议时出现网络问题,比如ICE candidate收集失败等。
docker run -d \ --name="janus" \ --restart="always" \ --network="host" \ gjovanov/janus-slim
Roomler
Roomler支持用自己喜欢的帐户(Facebook、Gmail、LinkedIn、Github)或本地注册(使用用户名、电子邮件和密码)进行OAuth身份验证。我们需要连接所有OAuth ID / Secrets、数据库连接字符串、Coturn Auth和Giphy API密钥。
docker run -d --name roomler \
--hostname roomler \
--restart always \
--network=backend \
-e API_URL=https://roomler.live \
-e DB_CONN=YOUR_DB_CONN \
-e WS_SCALEOUT_ENABLED=true \
-e WS_SCALEOUT_HOST=redis \
-e SENDGRID_API_KEY=YOUR_SEND_GRID_KEY \
-e FACEBOOK_ID=YOUR_FACEBOOK_ID \
-e FACEBOOK_SECRET=YOUR_FACEBOOK_SECRET \
-e GOOGLE_ID=YOUR_GOOGLE_ID \
-e GOOGLE_SECRET=YOUR_GOOGLE_SECRET \
-e GITHUB_ID=YOUR_GITHUB_ID \
-e GITHUB_SECRET=YOUR_GITHUB_SECRET \
-e LINKEDIN_ID=YOUR_LINKEDIN_ID \
-e LINKEDIN_SECRET=YOUR_LINKEDIN_SECRET \
-e TURN_URL=YOUR_TURN_URL \
-e TURN_USERNAME=YOUR_TURN_USERNAME \
-e TURN_PASSWORD=YOUR_TURN_PASSWORD \
-e GIPHY_API_KEY=YOUR_GIPHY_KEY \
gjovanov/roomler
由于Roomler容器需要同时在后端和前端docker网络中运行,且默认情况下,在启动容器时我们只能将其附加到一个网络(后端),所以我们需要在启动roomler之后将容器附加到第二个网络:
docker network connect frontend roomler
Nginx
在启动nginx之前,我们需要先启动nginx.conf
user nginx;
pid /var/run/nginx.pid;
##################################################################################
# nginx.conf Performance Tuning: https://github.com/denji/nginx-tuning
##################################################################################
# you must set worker processes based on your CPU cores, nginx does not benefit from setting more than that
worker_processes auto; #some last versions calculate it automatically
# number of file descriptors used for nginx
# the limit for the maximum FDs on the server is usually set by the OS.
# if you don't set FD's then OS settings will be used which is by default 2000
worker_rlimit_nofile 100000;
# only log critical errors
error_log /var/log/nginx/error.log crit;
# provides the configuration file context in which the directives that affect connection processing are specified.
events {
# determines how much clients will be served per worker
# max clients = worker_connections * worker_processes
# max clients is also limited by the number of socket connections available on the system (~64k)
worker_connections 4000;
# optmized to serve many clients with each thread, essential for linux -- for testing environment
use epoll;
# accept as many connections as possible, may flood worker connections if set too low -- for testing environment
multi_accept on;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
# cache informations about FDs, frequently accessed files
# can boost performance, but you need to test those values
open_file_cache max=200000 inactive=20s;
open_file_cache_valid 30s;
open_file_cache_min_uses 2;
open_file_cache_errors on;
# to boost I/O on HDD we can disable access logs
access_log off;
# copies data between one FD and other from within the kernel
# faster then read() + write()
sendfile on;
# send headers in one peace, its better then sending them one by one
tcp_nopush on;
# don't buffer data sent, good for small data bursts in real time
tcp_nodelay on;
# reduce the data that needs to be sent over network -- for testing environment
gzip on;
gzip_min_length 10240;
gzip_proxied expired no-cache no-store private auth;
gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/json application/xml;
gzip_disable msie6;
# allow the server to close connection on non responding client, this will free up memory
reset_timedout_connection on;
# request timed out -- default 60
client_body_timeout 10;
# if client stop responding, free up memory -- default 60
send_timeout 2;
# server will close connection after this time -- default 75
keepalive_timeout 30;
# number of requests client can make over keep-alive -- for testing environment
keepalive_requests 100000;
#########################################
# Just For Security Reason
#########################################
# Security reasons, turn off nginx versions
server_tokens off;
more_clear_headers Server; # Custom module: headers-more-nginx-module (https://github.com/openresty/headers-more-nginx-module)
# #########################################
# # NGINX Simple DDoS Defense
# #########################################
# limit the number of connections per single IP
limit_conn_zone $binary_remote_addr zone=conn_limit_per_ip:10m;
# limit the number of requests for a given session
limit_req_zone $binary_remote_addr zone=req_limit_per_ip:10m rate=5r/s;
# zone which we want to limit by upper values, we want limit whole server
server {
limit_conn conn_limit_per_ip 10;
limit_req zone=req_limit_per_ip burst=10 nodelay;
}
# if the request body size is more than the buffer size, then the entire (or partial)
# request body is written into a temporary file
client_body_buffer_size 128k;
# headerbuffer size for the request header from client -- for testing environment
client_header_buffer_size 3m;
# maximum number and size of buffers for large headers to read from client request
large_client_header_buffers 4 256k;
# read timeout for the request body from client -- for testing environment
# client_body_timeout 3m;
# how long to wait for the client to send a request header -- for testing environment
client_header_timeout 3m;
include /etc/nginx/conf.d/*.conf;
}
以及conf.d / roomler.live.conf:
server {
listen 80;
listen [::]:80;
server_name roomler.live; # replace with your domain
return 301 https://$server_name$request_uri;
}
# HTTPS server
#
server {
# Enable QUIC and HTTP/3.
listen 443 quic reuseport;
# Ensure that HTTP/2 is enabled for the server
listen 443 ssl http2;
server_name roomler.live; # replace with your domain
http2_push_preload on;
client_max_body_size 0;
gzip on;
gzip_http_version 1.1;
gzip_vary on;
gzip_comp_level 6;
gzip_proxied any;
gzip_types text/plain text/css application/json application/javascript application/x-javascript text/javascript;
brotli_static on;
brotli on;
brotli_types text/plain text/css application/json application/javascript application/x-javascript text/javascript;
brotli_comp_level 4;
# Enable TLS versions (TLSv1.3 is required for QUIC).
ssl_protocols TLSv1.2 TLSv1.3;
ssl_certificate /etc/nginx/cert/roomler.live.pem; # replace with your cert
ssl_certificate_key /etc/nginx/cert/roomler.live.key; # replace with your cert key
ssl_session_cache shared:SSL:1m;
ssl_session_timeout 5m;
# Enable TLSv1.3's 0-RTT. Use $ssl_early_data when reverse proxying to
# prevent replay attacks.
#
# @see: http://nginx.org/en/docs/http/ngx_http_ssl_module.html#ssl_early_data
ssl_early_data on;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
# Add Alt-Svc header to negotiate HTTP/3.
add_header alt-svc 'h3-27=":443"; ma=86400';
# Debug 0-RTT.
add_header X-Early-Data $tls1_3_early_data;
add_header x-frame-options "deny";
add_header Strict-Transport-Security "max-age=31536000" always;
location / {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $http_host;
proxy_pass http://roomler2:3000;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 1800;
proxy_connect_timeout 1800;
proxy_send_timeout 1800;
send_timeout 1800;
}
}
map $ssl_early_data $tls1_3_early_data {
"~." $ssl_early_data;
default "";
}
docker run -d --name nginx \
--hostname nginx \
--restart always \
-v /your_path/nginx/nginx.conf:/etc/nginx/nginx.conf \
-v /your_path/nginx/conf.d/:/etc/nginx/conf.d/ \
-v /your_path/nginx/cert/:/etc/nginx/cert/ \
-v /your_path/nginx/logs/:/etc/nginx/logs/ \
-p 80:80 \
-p 443:443/tcp \
-p 443:443/udp \
--net=bridge \
gjovanov/nginx
默认情况下,我们将启动连接到默认bridge网络的nginx容器,并实行端口映射。需要注意的是:在端口443上,我们同时公开了tcp和udp协议映射。因为我们的nginx图像支持实验性的http3,该实验速度很快且依赖于udp和tcp连接。
同Roomler操作相同,我们也把nginx附加到前端网络。
docker network connect frontend nginx
成功!
如果按照上述步骤操作,现在你应该可以启动、运行自己强大的视频会议和团队协作工具S̶l̶a̶c̶kRoomler。
你也可以试试这种方法,详见此视频。
当然,上述所有步骤都可以连接到单个docker-compose.yml文件中。但是那样一来,我们就会缺少亲自动手的乐趣。如果想试试的话,你可以通过docker-compose进行此操作。
同时,你可以在这里找到源代码。
总结
通过组合一些简单的开源,但功能非常强大的微服务,我们设法建立并运行了自己的视频会议和团队协作工具。该工具的功能可以与Slack、Microsoft、Teams、Zoom等大型应用程序的相媲美,甚至超过他们。
只要花上一点钱,你就可以在自己的基础架构(内部部署或在云中)上与团队、客户、家庭和朋友进行协作了。否则你就得为上述大公司的优质服务支付大量费用。
文章地址:https://medium.com/@gjovanov/building-your-own-slack-54874bf5fd7a
原文作者:Goran Jovanov
