站在5年后的今天再审视这个议题,我们会发现情况在发生变化,据艾瑞咨询《2018年中国智能家居研究报告》[2] 中的调研结果,智能家居从业者眼中最看好的用户入口排名如下:

调研显示的结果极为分散,经过对“入口”概念的漫长争夺,智能家居从业者对入口仍无共识。
王学集表示,自己在创业初期也在寻找入口级设备的机会,直到半年后才逐渐意识到智能家居行业可能并没有一个确定的入口。他认为:
“智能家居是以AI的技术驱动了多设备联动,形成围绕式人机合一体验,目前看来很难有一个非常明确的设备能做到这样的水平。”
站在5年后的今天再审视这个议题,我们会发现情况在发生变化,据艾瑞咨询《2018年中国智能家居研究报告》[2] 中的调研结果,智能家居从业者眼中最看好的用户入口排名如下:

调研显示的结果极为分散,经过对“入口”概念的漫长争夺,智能家居从业者对入口仍无共识。
王学集表示,自己在创业初期也在寻找入口级设备的机会,直到半年后才逐渐意识到智能家居行业可能并没有一个确定的入口。他认为:
“智能家居是以AI的技术驱动了多设备联动,形成围绕式人机合一体验,目前看来很难有一个非常明确的设备能做到这样的水平。”
NTP 是 Network Time Protocol 的简称,也就是网络时间协议。而 NTP 服务器是可以通过网络来同步时间的服务器。Windows 自带的 NTP 服务器都在美国,有时间经常无法访问。再加上有朋友跟我留言,想让我收集一下 NTP 服务器,所以建立了此页面。
本页优先提供国内可以使用的 NTP 服务器地址,然后也收集了一些国外知名的 NTP 服务器地址,提供给大家选择。
个人建议,国内用阿里云、国外用谷歌或者苹果。也有朋友经过测试苹果在国内也是可以使用的,大家可以试试看。
附:Windows 10 修改时间服务器的方法
如果有什么问题或建议可以跟我留言,戳⇒ 联系我
国内知名公共 NTP 服务器地址
Chinese Public NTP Server
[国内 NTP 服务器] · 国际 NTP 服务器
国家授时中心 NTP 服务器
NTSC NTP Server
ntp.ntsc.ac.cn
中国 NTP 快速授时服务
NTP ORG CN
cn.ntp.org.cn
教育网
edu.ntp.org.cn
国际 NTP 快速授时服务
Pool NTP ORG
cn.pool.ntp.org
阿里云公共 NTP 服务器
Aliyun NTP Server
time.pool.aliyun.com
time1.aliyun.com
time2.aliyun.com
time3.aliyun.com
time4.aliyun.com
time5.aliyun.com
time6.aliyun.com
time7.aliyun.com
腾讯云公共 NTP 服务器
Tencent Cloud NTP Server
time1.cloud.tencent.com
time2.cloud.tencent.com
time3.cloud.tencent.com
time4.cloud.tencent.com
time5.cloud.tencent.com
教育网(高校自建)
EDU NTP Server
ntp.sjtu.edu.cn
ntp.neu.edu.cn
ntp.bupt.edu.cn
ntp.shu.edu.cn
国际 NTP 服务器
Global Public NTP Server
[国际 NTP 服务器] · 国内 NTP 服务器
国际 NTP 快速授时服务
Pool NTP ORG
pool.ntp.org
0.pool.ntp.org
1.pool.ntp.org
2.pool.ntp.org
3.pool.ntp.org
asia.pool.ntp.org
谷歌公共 NTP 服务器
Google NTP Server
time1.google.com
time2.google.com
time3.google.com
time4.google.com
苹果公司公共 NTP 服务器
Apple NTP Server
time.apple.com
time1.apple.com
time2.apple.com
time3.apple.com
time4.apple.com
time5.apple.com
time6.apple.com
time7.apple.com
微软 Windows NTP 服务器
Microsoft Windows NTP Server
time.windows.com
美国标准技术研究院 NTP 服务器
NIST NTP Server
time.nist.gov
time-nw.nist.gov
time-a.nist.gov
time-b.nist.gov
香港天文台公共 NTP 服务器
Hong Kong Observator NTP Server
stdtime.gov.hk
Chrome和Firefox为用户提供了在浏览器中管理已注册Service Worker的选项,包括从浏览器中删除Service Worker的选项。
服务工作者是大多数现代浏览器支持的一项新兴功能,使站点和服务无需在浏览器中打开即可与浏览器进行交互。
将它们视为按需流程,可以使用推送通知和数据同步,或者使站点脱机工作。
当前没有将Web浏览器设计为在Service Worker在浏览器中注册时始终提示用户。当前,大多数情况下,这是作为后台进程发生的。

服务工作者可以自动注册,也可以在用户接受提示后注册。Pinterest是在Chrome或Firefox中访问该网站时会自动注册的网站。
用户不清楚这是因为它发生在后台。
Chrome和Firefox没有提供有关如何管理以前添加到浏览器的Service Worker的明确信息。尽管存在功能,但它们在此时或多或少对用户隐藏了,如果需要从浏览器中删除以前注册的工作人员,这将是一个问题。
本指南为您提供了在Firefox和Chrome中管理工作人员的方法。
有用的信息

Firefox用户可以通过以下方式在浏览器中管理所有注册的Service Worker:
在Mozilla Firefox中禁用服务工作者

Firefox用户可以通过以下方式(通过我们广泛的Firefox隐私和安全设置指南列表)禁用浏览器中的Service Worker :
要撤消更改,请重复此过程,但请确保在完成操作后将首选项的值设置为true。

在Google Chrome中禁用服务人员
目前似乎没有办法在Chrome浏览器中禁用该功能。如果您找到了一种方法,请在下面发表评论,我将尽快更新文章。
Redis CPU占用过高会导致所有使用Redis的客户端性能大幅下降,可能的原因中其中一个是大量的请求,尤其是keys命令请求过多,查询流程:
redis-cli -h 192.168.1.2 -a ‘passwd’ info
connected_clients:25
client_longest_output_list:0
client_biggest_input_buf:0
blocked_clients:1
used_memory:24022936
used_memory_human:22.91M
used_memory_rss:27422720
used_memory_rss_human:26.15M
used_memory_peak:76894968
used_memory_peak_human:73.33M
total_system_memory:33566736384
total_system_memory_human:31.26G
used_memory_lua:43008
used_memory_lua_human:42.00K
maxmemory:0
maxmemory_human:0B
maxmemory_policy:noeviction
mem_fragmentation_ratio:1.14
mem_allocator:jemalloc-4.0.3
loading:0
rdb_changes_since_last_save:3502
rdb_bgsave_in_progress:0
rdb_last_save_time:1575614696
rdb_last_bgsave_status:ok
rdb_last_bgsave_time_sec:0
rdb_current_bgsave_time_sec:-1
aof_enabled:0
aof_rewrite_in_progress:0
aof_rewrite_scheduled:0
aof_last_rewrite_time_sec:-1
aof_current_rewrite_time_sec:-1
aof_last_bgrewrite_status:ok
aof_last_write_status:ok
total_connections_received:8119852
total_commands_processed:287772910835
instantaneous_ops_per_sec:23686
total_net_input_bytes:8372810113180
total_net_output_bytes:1471697139442
instantaneous_input_kbps:676.85
instantaneous_output_kbps:129.54
rejected_connections:0
sync_full:0
sync_partial_ok:0
sync_partial_err:0
expired_keys:158935
evicted_keys:0
keyspace_hits:188709502
keyspace_misses:6948953
pubsub_channels:0
pubsub_patterns:0
latest_fork_usec:823
migrate_cached_sockets:0
role:master
connected_slaves:0
master_repl_offset:0
repl_backlog_active:0
repl_backlog_size:1048576
repl_backlog_first_byte_offset:0
repl_backlog_histlen:0
used_cpu_sys:3372080.50
used_cpu_user:404838.31
used_cpu_sys_children:851.85
used_cpu_user_children:5934.84
cluster_enabled:0
db0:keys=14078,expires=1209,avg_ttl=150542
db6:keys=878,expires=682,avg_ttl=42691293
redis-cli -h 192.168.1.2 -a ‘ passwd ‘ monitor
info命令会显示当前的状态,monitor会显示当前的客户端的命令请求;
redis-cli -h 192.168.1.xx -a ‘ passwd ‘ slowlog get (reset替换get清空旧的log)
这个命令会显示出最近一段时间内的耗时较久的查询。
这几个命令综合起来,基本可以找到是哪些命令频繁调用造成系统繁忙。
一般来说,都是大量调用keys命令并使用通配符造成的。
这是一份事后的总结。在经历了调优过程踩的很多坑之后,我们最终完善并实施了初步的性能测试方案,通过真实的测试数据归纳出了 Laravel 开发过程中的一些实践技巧。
最近有同事反馈 Laravel 写的应用程序响应有点慢、20几个并发把 CPU 跑满… 为了解决慢的问题,甚至一部分接口用 nodejs 来写。
而我的第一反应是一个流行的框架怎么可能会有这么不堪?一定是使用上哪里出现了问题。为了一探究竟,于是开启了这次 Laravel 应用性能调优之旅。
这次性能测试方案中用到的优化技巧主要基于 Laravel 框架本身及其提供的工具。
app.debug=falsephp artisan config:cachephp artisan router:cachephp artisan optimizecomposer dumpautoload除了以上优化技巧之外,还有很多编码上的实践可以提升 Laravel 应用性能,在本文中暂时不会做说明。(也可以关注我的后续文章)
打开应用根目录下的 .env 文件,把 debug 设置为 false。
APP_DEBUG=false
php artisan config:cache
运行以上命令可以把 config 文件夹里所有配置信息合并到一个 bootstrap/cache/config.php 文件中,减少运行时载入文件的数量。
php artisan config:clear
运行以上命令可以清除配置信息的缓存,也就是删除 bootstrap/cache/config.php 文件
php artisan route:cache
运行以上命令会生成文件 bootstrap/cache/routes.php。路由缓存可以有效的提高路由器的注册效率,在大型应用程序中效果越加明显。
php artisan route:clear
运行以上命令会清除路由缓存,也就是删除 bootstrap/cache/routes.php 文件。
php artisan optimize --force
运行以上命令能够把常用加载的类合并到一个文件中,通过减少文件的加载来提高运行效率。这个命令会生成 bootstrap/cache/compiled.php 和 bootstrap/cache/services.json 两个文件。
通过修改 config/compile.php 文件可以添加要合并的类。
在生产环境中不需要指定 --force 参数文件也可以自动生成。
php artisan clear-compiled
运行以上命令会清除类映射加载优化,也就是删除 bootstrap/cache/compiled.php 和 bootstrap/cache/services.json 两个文件。
composer dumpautoload -o
Laravel 应用程序是使用 composer 来构建的。这个命令会把 PSR-0 和 PSR-4 转换为一个类映射表来提高类的加载速度。
注意:
php artisan optimize --force命令里已经做了这个操作。
Laravel 应用程序内置了并开启了很多的中间件。每一个 Laravel 的请求都会加载相关的中间件、产生各种数据。在 app/Http/Kernel.php 中注释掉不需要的中间件(如 session 支持)可以极大的提升性能。
HHVM 和 OPcache 都能轻轻松松的让你的应用程序在不用做任何修改的情况下,直接提高 50% 或者更高的性能。
只能说 PHP 7.x 比起之前的版本在性能上有了极大的提升。
嗯,限于你的真实企业环境,这个也许很长时间内改变不了,算我没说。
我们使用简单的 Apache ab 命令仅对应用入口文件进行测试,并记录和分析数据。
ab -t 10 -c 10 {url}。该命令表示对 url 同时发起 10 个请求,并持续 10 秒钟。命令中具体的参数设置需要根据要测试的服务器性能进行选择。服务器环境说明
所有脱离具体环境的测试数据都没有意义,并且只有在相近的条件下才可以进行比较。
app\Http\routes.php 中定义了 85 个路由。以上的数据,大家在自己进行测试时可以参考。
ab -t 10 -c 10 http://myurl.com/index.php基础检查项
APP_DEBUG=truebootstrap/cache/config.phpbootstrap/cache/routes.phpbootstrap/cache/compiled.php 和 bootstrap/cache/services.jsonapp/Http/Kernel.php 中开启了大部分的中间件APP_DEBUG=false。ab -t 10 -c 10 http://myurl.com/index.php。与步骤 1 结果比较发现:关闭应用 debug 之后,每秒处理请求数从 26-34 上升到 33-35,请求响应时间从 大部分 300ms 以上下降到 290ms 左右,效果不太明显,但确实有一定的提升。
注意:这部分与应用中的日志等使用情况有比较大的关联。
php artisan config:cache,确认生成 bootstrap/cache/config.php。ab -t 10 -c 10 http://myurl.com/index.php。与步骤 2 结果比较发现:开启配置信息缓存之后,每秒处理请求数从 33-35 上升到 36-38,请求响应时间从 290ms 左右下降到 260ms 左右,效果不太明显,但确实有一定的提升。
php artisan route:cache,确认生成 bootstrap/cache/routes.php。ab -t 10 -c 10 http://myurl.com/index.php。与步骤 3 结果比较发现:开启路由信息缓存之后,每秒处理请求数从 36-38 上升到 60 左右,请求响应时间从 260ms 下降到 160ms 左右,效果显著,从 TPS 看,提升了 70%。
ab -t 10 -c 10 http://myurl.com/index.php。注意:这次测试中我注释掉了所有的中间件。实际情况中应该尽量只留下必要的中间件。
与步骤 4 结果比较发现:删除了不必要的中间件之后,每秒处理请求数从 60 左右上升到 90 左右,请求响应时间从 160ms 下降到 110ms 左右,效果非常明显,从 TPS 看,提升了 50%。
php artisan optimize --force,确认生成 bootstrap/cache/compiled.php 和 bootstrap/cache/services.json。ab -t 10 -c 10 http://myurl.com/index.php。与步骤 5 结果比较发现:做了类映射加载优化之后,每秒处理请求数从 90 上升到 110,请求响应时间从 110ms 下降到 100ms 以下,效果还是比较明显的。
ab -t 10 -c 10 http://myurl.com/index.php。与步骤 6 结果比较发现:关闭 OPcache 之后,每秒处理请求数从 110 下降到 15,请求响应时间从 100ms 以下上升到 650ms 以上。开启与关闭 OPcache,数据上竟有几倍的差别。
此后,我重新开启了 PHP 的 OPcache,数据恢复到步骤 6 水平。
在运行 php artisan route:cache 命令时报这个错误。
原因:路由文件中处理“/”时使用了闭包的方式。要运行该命令,路由的具体实现不能使用闭包方式。
修改方案:将路由的具体实现放到控制器中来实现。
在运行 php artisan route:cache 命令时报这个错误。
原因:路由文件中定义了重复的路由。
修改方案:排查路由文件中的重复路由并修改。尤其要注意 resource 方法很可能导致与其方法重复。
在运行 php artisan optimize --force 命名时报这个错误。
原因:在加载需要编译的类时没有找到相应的文件。5.2 版本的 vendor/laravel/framework/src/Illuminate/Foundation/Console/Optimize/config.php 中定义了要编译的文件路径,但不知道为什么 /vendor/laravel/framework/src/Illuminate/Database/Eloquent/ActiveRecords.php 没有找到,所以报了这个错误。
修改方案:暂时注释掉了以上 config.php 中的 ../ActiveRecords.php 一行。
在运行 php artisan config:cache 之后,浏览器上访问 Laravel 应用程序欢迎页报这个错误。
原因:Laravel 应用程序服务器是通过 Homestead 在虚拟机上搭建的。而这个命令我是在虚拟机之外运行的,导致生成的 config.php 中的路径是本机路径,而不是虚拟机上的路径。所以无法找到视图文件。
修改方案:ssh 到虚拟机内部运行该命令。
坑也踩了,测试也做过了。这里针对这次经历做个实践技巧的简单总结。
app.debug=falsephp artisan config:cachephp artisan router:cachephp artisan optimize(包含自动加载优化 composer dumpautoload)resouce 方法。以上的调优技巧及编码注意事项主要针对框架本身,在真正的业务逻辑编码中有很多具体的优化技巧,在此没有讨论。
后续的优化重点将会放在具体编码实践上:
网上看到很多框架性能对比的文章与争论,也看到很多简单贴出了数据。这些都不足以窥探真实的情况,所以有了我们这次的实践,并在过程中做了详实的记录。在各位读者实践过程中提供参考、比较、反思之用。对于这次实践有疑问的读者,也欢迎提出问题和意见。
不多说了,要学习更多技术干货,请关注微信公众号:up2048。
– EOF –
越 来越多的家庭和企业转向WiFi网络作为其首选的互联网接入的提供方式。无线网络对于终端用户更为便捷,使每个人都能够充分利用其移动设备的优势。无线网络相比传统的有线网络而言,也免除了线接的烦扰和限制。
从管理员的角度来说,WiFi网络虽然有很多的优势,但同时也伴随有一系列必须予以解决的其他挑战。这不仅仅只是为您的所有用户提供充足的连接的问题。您还需要了解您的网络的其他方面,如覆盖区域和信道重叠。
对于我们部分使用家庭WiFi网络的用户而言,我们就是自己的网络管理员。如果您是这样的情况或只是一位具有好奇心的终端用户,您需要了解的您的网络的其中一大特性就是WiFi信号强度。这个值对于您的网络活动效率是一个决定性的因素。让我们深度了解一下WiFi信号以及它们是如何影响您的无线网络使用的。
WiFi网络使用无线电波来在不同的设备之间建立通信。这些设备可能包括计算机,移动手机,平板电脑或网络路由器。无线网络路由器是互联网或其他以太网络有线连接和无线连接设备之间的接口。
路由器对从WiFi网络用户那里接收来的无线电信号进行解码并将它们传至互联网。反过来,路由器会将从互联网接收的数据由二进位数据转换成无线电波,传送至使用同一网络的设备。
产生WiFi信号的无线电波使用的2.4 GHz和5 GHz的频带。这些频率要高于电视或手机所使用的频率,相对较低频率能够传递更多的数据。
WiFi信号在传输数据时使用的是802.11网络标准。WiFi网络采用了一系列不同协议的变体。您将会了解到的部分最常见的协议包括2.4GHz频带所使用的802.11n以及5GHz频带主要使用的802.11ac。其他协议您可能还会看到802.11b,它与802.11g是最慢标准。
整个网络覆盖区域内的WiFi信号强度直接影响着用户高效处理各种网络活动的能力。在深入探析怎样的信号强度才适合您进行相应的WiFi网络活动之前,让我们讨论一下 WiFi信号强度是如何测量的。
信号强度是以dBm为单位的。它指的是分贝毫瓦,以从0到-100内的负值表示。因此,-40的信号相比-80的信号更强,因为-80距离0更远,因此是一个更小的数字。
dBm是一个对数性的,而不是线性的,这意味着信号强度之间的变化并不是平滑渐进式的。从这个层面来说,3 dBm的不同会使之前的信号强度减半或提升一倍。
能够影响到您的WiFi表现的背景噪音等级也是用dBm表示的。对于噪音等级而言,数值越接近0,就表示噪音等级越高。经测量的-10 dBm的噪音要高于-40 dBm的噪音。
下表揭示了您应当努力实现的最低信号强度,只有达到才能将您的WiFi网络用于各种不同目的。
| 信号强度 | 限定符 | 适合用途 |
|---|---|---|
| -30 dBm | 极好 | 这是可实现的最大信号强度,适用于任何一种使用情景。 |
| -50 dBm | 极好 | 此极好的信号强度适于各种网络使用情形。 |
| -65 dBm | 非常好 | 建议为智能手机和平板电脑提供支持。 |
| -67 dBm | 非常好 | 此信号强度对于网络电话和流媒体视频的使用是足够的。 |
| -70 dBm | 可接受 | 该等级是确保实现稳定的数据包传输的最低信号强度要求,对于网页冲浪和收发邮件的使用是足够的。 |
| -80 dBm | 不良 | 实现基本连接,但数据包传输不稳定。 |
| -90 dBm | 非常差 | 大多都是噪音,抑制大多数功能的实现。 |
| -100 dBm | 最差 | 全是噪音。 |
能够影响到您的网络WiFi信号的有 一系列不同因素。其中部分因素有:
路由器位置的各个不同方面会影响到良好信号的传输表现。这些包括:
将您的路由器的设置改成自动固件升级以保持其处于高效运作状态。阶段性地重启您的路由器也是一个比较好的做法,有利于实现良好的信号覆盖。
需要较强信号的设备,如那些用于玩游戏或观看视频的设备,将它们放在靠近路由器的位置可能会获得更好的信号表现。在一些情况下,您可能需要通过使用像路由器这样的附加设备来 扩展您的WiFi信号范围以增强WiFi信号强度。
UPDATE w_zshop_goods_sku_images SET path = REPLACE(path, ‘http://qq.com’,’https://qq.com’);
UPDATE w_zshop_commodities SET details = REPLACE(details, ‘http://qq.com’,’https://qq.com’);
UPDATE w_zshop_commodities SET params = REPLACE(params, ‘http://qq.com’,’https://qq.com’);
UPDATE w_users SET profile = REPLACE(profile, ‘http://qq.com’,’https://qq.com’);
UPDATE w_zshop_banners SET pic = REPLACE(pic, ‘http://qq.com’,’https://qq.com’);
UPDATE w_zshop_order_coms SET pic = REPLACE(pic, ‘http://qq.com’,’https://qq.com’);
虽然已经用 coturn 来作为 webrtc 的转发服。 但是我们只做了一个最基本的静态key的加密校验而已。但是对于转发的流量和转发的时长,却没法统计。
但是事实上,对于转发的流量和时长对于我们的业务来说是非常必要的。
刚好 coturn 有提供了一个统计的redis配置,redis-statsdb,这个是就是 turnserver 会将一些统计的数据存到 redis 中。 然后我们去取就行了。
所以要在配置文件里面加上这一个配置:
| 1 | redis-statsdb=”ip=59.57.xx.xx dbname=13 port=6379 connect_timeout=30″ |
具体文档:
-O, –redis-statsdb Redis status and statistics database connection string, if used (default – empty, no Redis stats DB used). This database keeps allocations status information, and it can be also used for publishing and delivering traffic and allocation event notifications. The connection string has the same parameters as redis-userdb connection string.https://github.com/coturn/coturn/wiki/turnserver
也就是这个参数是用来做状态统计的。 可以用来发布和传递流量, 还有断开和连接的webhook通知。通过这个统计的redis db,我们可以得到这一次转发的开始时间,结束时间,以及转发的流量。
大概的一个截图就是:

这边还有更详细的说明: schema.stats.redis
状态的key就是:
| 1 | turn/user/<username>/allocation/<id>/status |
举例:
| 1 | turn/realm/pano/user/1531994598:fe909a0a4cde20be0a7bb9fbfdc8d6dc_21_24_18154723/allocation/003000000000002028/status |

他的值有可能是 new lifetime=… 或者是 refreshed lifetime=…
一个用户名下可能有多个状态。
然后是订阅:
如果是订阅所有的配置,那么就是 turn/realm/*/user/*/allocation/*/total_traffic
因为不能过期,所以redis 的配置文件,要配置这两个:
| 1 2 | timeout 0 tcp-keepalive 60 |
假设我们已经在使用webrtc的应用上连接上turn server了,并且已经在转发了。那我们就可以看下这个redis跑的统计数据都是什么样的?
接下来先在redis 先sub一下:
| 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 | 127.0.0.1:6379[13]> psubscribe turn/realm/* Reading messages… (press Ctrl-C to quit) 1) “pmessage” 2) “turn/realm/*” 3) “turn/realm/pano/user/1532417691:26e4cd38fa6fc39fa8fc40fb25fe558c_21_24_18152649/allocation/001000000000003326/status” 4) “new lifetime=600” 1) “pmessage” 2) “turn/realm/*” 3) “turn/realm/pano/user/1532417569:26e4cd38fa6fc39fa8fc40fb25fe558c_21_24_18152649/allocation/001000000000003324/status” 4) “new lifetime=600” 1) “pmessage” 2) “turn/realm/*” 3) “turn/realm/pano/user/1532417569:26e4cd38fa6fc39fa8fc40fb25fe558c_21_24_18152649/allocation/003000000000003700/status” 4) “refreshed lifetime=0” 1) “pmessage” 2) “turn/realm/*” 3) “turn/realm/pano/user/1532417569:26e4cd38fa6fc39fa8fc40fb25fe558c_21_24_18152649/allocation/003000000000003700/status” 4) “deleted” 1) “pmessage” 2) “turn/realm/*” 3) “turn/realm/pano/user/1532417569:26e4cd38fa6fc39fa8fc40fb25fe558c_21_24_18152649/allocation/003000000000003700/total_traffic” 4) “rcvp=0, rcvb=0, sentp=0, sentb=0” 1) “pmessage” 2) “turn/realm/*” 3) “turn/realm/pano/user/1532417569:26e4cd38fa6fc39fa8fc40fb25fe558c_21_24_18152649/allocation/001000000000003324/traffic” 4) “rcvp=1610, rcvb=1607159, sentp=438, sentb=22052” 1) “pmessage” 2) “turn/realm/*” 3) “turn/realm/pano/user/1532417569:26e4cd38fa6fc39fa8fc40fb25fe558c_21_24_18152649/allocation/001000000000003324/traffic” 4) “rcvp=1385, rcvb=1404252, sentp=663, sentb=31187” 1) “pmessage” 2) “turn/realm/*” 3) “turn/realm/pano/user/1532417569:26e4cd38fa6fc39fa8fc40fb25fe558c_21_24_18152649/allocation/001000000000003324/traffic” 4) “rcvp=1407, rcvb=1442093, sentp=641, sentb=30066” 1) “pmessage” 2) “turn/realm/*” 3) “turn/realm/pano/user/1532417569:26e4cd38fa6fc39fa8fc40fb25fe558c_21_24_18152649/allocation/001000000000003324/traffic” 4) “rcvp=1383, rcvb=1408780, sentp=665, sentb=31146” 1) “pmessage” 2) “turn/realm/*” 3) “turn/realm/pano/user/1532417691:26e4cd38fa6fc39fa8fc40fb25fe558c_21_24_18152649/allocation/001000000000003326/status” 4) “refreshed lifetime=0” 1) “pmessage” 2) “turn/realm/*” 3) “turn/realm/pano/user/1532417691:26e4cd38fa6fc39fa8fc40fb25fe558c_21_24_18152649/allocation/001000000000003326/status” 4) “deleted” 1) “pmessage” 2) “turn/realm/*” 3) “turn/realm/pano/user/1532417691:26e4cd38fa6fc39fa8fc40fb25fe558c_21_24_18152649/allocation/001000000000003326/total_traffic” 4) “rcvp=0, rcvb=0, sentp=0, sentb=0” 1) “pmessage” 2) “turn/realm/*” 3) “turn/realm/pano/user/1532417569:26e4cd38fa6fc39fa8fc40fb25fe558c_21_24_18152649/allocation/001000000000003324/status” 4) “refreshed lifetime=0” 1) “pmessage” 2) “turn/realm/*” 3) “turn/realm/pano/user/1532417691:26e4cd38fa6fc39fa8fc40fb25fe558c_21_24_18152649/allocation/001000000000003327/status” 4) “new lifetime=600” 1) “pmessage” 2) “turn/realm/*” 3) “turn/realm/pano/user/1532417691:26e4cd38fa6fc39fa8fc40fb25fe558c_21_24_18152649/allocation/003000000000003705/status” 4) “new lifetime=600” 1) “pmessage” 2) “turn/realm/*” 3) “turn/realm/pano/user/1532417569:26e4cd38fa6fc39fa8fc40fb25fe558c_21_24_18152649/allocation/001000000000003324/status” 4) “deleted” 1) “pmessage” 2) “turn/realm/*” 3) “turn/realm/pano/user/1532417569:26e4cd38fa6fc39fa8fc40fb25fe558c_21_24_18152649/allocation/001000000000003324/total_traffic” 4) “rcvp=5785, rcvb=5862284, sentp=2407, sentb=114451” |

可以看到进行一次连接的话,会分配好几条临时会话。 从下图来看的话,可以看到有 4 条,其实这四条都属于同一个连接。
接下来我们一条一条来分析:
总结一下:
每个 channel 刚开始创建的时候,生命周期只有 600s, 这个可以在 配置文件设置:
| 1 2 3 4 5 | # Uncomment to set the lifetime for the channel. # Default value is 600 secs (10 minutes). # This value MUST not be changed for production purposes. # #channel-lifetime=600 |
而且刚开始创建就会有状态通知,内容就是 new lifetime=600。
虽然每个channel的生命周期都是 600s, 但是并不意味着,600s 之后,这个channel就会被删除,其实不是的,这个是可以被续的,也就是如果这个 channel 被续了,那么就会有这个状态通知:
| 1 2 3 4 | 1) “pmessage” 2) “turn/realm/*” 3) “turn/realm/pano/user/1532403222:26e4cd38fa6fc39fa8fc40fb25fe558c_21_24_18152649/allocation/004000000000003897/status” 4) “refreshed lifetime=600” |
这个说明被续了一个周期了。当然也有不续的,这时候也会有这个状态,不过这时候 lifetime 是 0:
| 1 2 3 4 | 1) “pmessage” 2) “turn/realm/*” 3) “turn/realm/pano/user/1532415366:07a9939e437c9bca97f67b4b74de6a2f_21_24_22819787/allocation/004000000000003905/status” 4) “refreshed lifetime=0” |
一旦 lifetime 为0, 就说明这个channel要被删除了。 这时候就会收到 delete 状态:
| 1 2 3 4 | 1) “pmessage” 2) “turn/realm/*” 3) “turn/realm/pano/user/1532415366:07a9939e437c9bca97f67b4b74de6a2f_21_24_22819787/allocation/000000000000001816/status” 4) “deleted” |
这时候就说明这条channel已经被删除了。
而且每一个channel 的最长生命周期是可以配置的,默认是 3600s:
| 1 2 3 4 5 | # Uncomment if you want to set the maximum allocation # time before it has to be refreshed. # Default is 3600s. # #max-allocate-lifetime=3600 |
所以一个完整的 channel 周期就是 创建-> 续周期(一次以上) -> 删除 (如果续的是 0)
针对这个,我后面试了一下, 在一次长达 一个多小时的webrtc连接, 那个转发流量的channel, 续周期续了7次, 一次 600s, 总的是 7 * 600 = 4200, 比3600 还大了,但是也还没有断开,所以这个也有点奇怪???
每一条channel 在生命周期之内,如果有进行流量转发的话,那么就会每隔一段时间就会有 traffic 这个状态过来,注意,如果是 stun 或者 local 这种不走转发的,那么是没有这个事件的:
| 1 2 3 4 | 1) “pmessage” 2) “turn/realm/*” 3) “turn/realm/pano/user/1532417569:26e4cd38fa6fc39fa8fc40fb25fe558c_21_24_18152649/allocation/001000000000003324/traffic” 4) “rcvp=1383, rcvb=1408780, sentp=665, sentb=31146” |
而且每次的channel 的生命周期结束之后,也就是 delete 状态,也会收到这一条生命周期的所有的转发流量数据。
| 1 2 3 4 | 1) “pmessage” 2) “turn/realm/*” 3) “turn/realm/pano/user/1532417569:26e4cd38fa6fc39fa8fc40fb25fe558c_21_24_18152649/allocation/001000000000003324/total_traffic” 4) “rcvp=5785, rcvb=5862284, sentp=2407, sentb=114451” |
跟 traffic 不一样, traffic 是真的是转发有流量,才会每隔一段时间抛一次,而 total_traffic 是必会有的,不管是穿透还是转发, 只不过如果是穿透的话,那就是都是 0,比如:
| 1 2 3 4 | 1) “pmessage” 2) “turn/realm/*” 3) “turn/realm/pano/user/1532415366:07a9939e437c9bca97f67b4b74de6a2f_21_24_22819787/allocation/004000000000003905/total_traffic” 4) “rcvp=0, rcvb=0, sentp=0, sentb=0” |
每一个 delete 状态,都会跟着一个 total_traffic 事件。 而里面的 这些参数,其实都是 traffic 的所有对应参数的总和。
这四个参数分别是:
| 1 2 3 4 | rcvp: 收到的数据包 rcvb: 收到的字节(byte) sentp: 发送的数据包 sentb: 发送的字节(byte) |
所以我们如果要统计这一次转发的时长的话,那么就要记录这次连接的所有的channel。也就是如果这一次的转发,有5个channel,那么就第一个channel创建的时候,就是开始时间, 当所有的channel 都断开的时候,那么就是结束时间。 一定要所有的channel 都断开,才算转发结束,
而且中间还会有不断的新的channel被创建,这些新的channel也要加入到map里面去。然后每一个channel断开,就从map里面去掉, 一直到最后map为空的话,才算转发结束。算转发的总流量也是一样,每一个channel都会有一段流量。 当所有的channel都断开的时候,加起来就是总的转发流量。
刚开始是这样子写的:
| 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 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 | package main import ( “errors” “fmt” “github.com/garyburd/redigo/redis“ “iGong/util/log” “strconv” “strings” “sync” “time” ) type OnlineTimeBucket struct { Data map[string]*OnlineTimeBucketSub Locker *sync.Mutex } func NewOnlineTimeBucket() *OnlineTimeBucket { return &OnlineTimeBucket{ Data: make(map[string]*OnlineTimeBucketSub), Locker: &sync.Mutex{}, } } type OnlineTimeBucketSub struct { Timer map[string]*OnlineTime } type OnlineTime struct { Start int64 End int64 Delete bool TotalTraffIc bool Rcvp int Rcvb int Sentp int Sentb int } var onlineTimeBucket *OnlineTimeBucket var serverTime = time.Now().Unix() /* turn/realm/pano/user/1531882310:7ed1019263ce5e09f3d40a1ca15ae992_21_24_18152992/allocation/000000000000000495/status new lifetime=600 turn/realm/pano/user/1531881513:7ed1019263ce5e09f3d40a1ca15ae992_21_24_18152992/allocation/001000000000000462/status refreshed lifetime=0 turn/realm/pano/user/1531882310:7ed1019263ce5e09f3d40a1ca15ae992_21_24_18152992/allocation/004000000000000899/status deleted turn/realm/pano/user/1531881513:7ed1019263ce5e09f3d40a1ca15ae992_21_24_18152992/allocation/001000000000000462/total_traffic rcvp=18612, rcvb=894124, sentp=104268, sentb=114206669 turn/realm/pano/user/1531881926:26e4cd38fa6fc39fa8fc40fb25fe558c_21_24_18152649/allocation/004000000000000896/traffic rcvp=1847, rcvb=2052114, sentp=201, sentb=10652 */ func TurnServerStats() { conn, err := RedisTurn.GetConn() sub := redis.PubSubConn{Conn: conn} if err != nil { log.Error(err) } defer conn.Close() onlineTimeBucket = NewOnlineTimeBucket() sub.PSubscribe(“turn/realm/*”) for { switch n := sub.Receive().(type) { //case redis.Message: // fmt.Printf(“Message: %s %s\n”, n.Channel, n.Data) case redis.PMessage: err = forwardStats(n) if err != nil { continue } case error: fmt.Printf(“error: %v\n”, n) return } } } func forwardStats(data redis.PMessage) (err error) { fmt.Printf(“PMessage: %s %s %s\n”, data.Pattern, data.Channel, data.Data) event, deviceId, channel, err := decodeChannel(data.Channel) if err != nil { log.Error(err) return err } log.Info(event, deviceId) switch event { case “status”: //统计时长 和在线状态 onlineStatus := decodeDataWithStatus(data.Data) log.Info(onlineStatus) addOnlineTime(deviceId, channel, onlineStatus) case “total_traffic”: //统计流量 这个事件过来说明转发已经结束 并且只有一个会话是有值的 trafficMap, err := decodeDataWithTraffic(data.Data) if err != nil { return err } //rcvp 接收到的包数量 rcvb 接收到的流量 sentp 发送的包数量 sentb 发送的包流量 log.Info(trafficMap) addFlow(deviceId, channel, trafficMap) } return } func decodeChannel(channel string) (event string, deviceId, channelId string, err error) { args := strings.Split(channel, “/”) if len(args) != 8 { err = errors.New(“channel fail .”) return } event = args[7] deviceId = strings.Split(args[4], “:”)[1] channelId = args[6] return } func decodeDataWithStatus(data []byte) (onlineStatus int) { args := strings.Split(string(data), “=”) switch args[0] { //新建 case “new lifetime”: onlineStatus = 0 //刷新 case “refreshed lifetime”: onlineStatus = 1 //移除 case “deleted”: onlineStatus = 2 default: onlineStatus = 1 } return } func decodeDataWithTraffic(data []byte) (stats map[string]int, err error) { args := strings.Split(string(data), “,”) if len(args) != 4 { err = errors.New(“traffic data fail”) return } stats = make(map[string]int) for _, v := range args { statsInfo := strings.Split(v, “=”) s, _ := strconv.Atoi(statsInfo[1]) stats[strings.TrimLeft(statsInfo[0], ” “)] = s } return } //记录转发时长 func addOnlineTime(deviceId, channelId string, onlineStatus int) { if onlineStatus == 1 { return } onlineTimeBucket.Locker.Lock() timer := time.Now().Unix() if _, ok := onlineTimeBucket.Data[deviceId]; !ok { j := new(OnlineTimeBucketSub) j.Timer = make(map[string]*OnlineTime) onlineTimeBucket.Data[deviceId] = j } switch onlineStatus { case 0: log.Info(“new life_time”) var onlineModel = new(OnlineTime) onlineModel.Start = timer onlineTimeBucket.Data[deviceId].Timer[channelId] = onlineModel case 2: log.Info(“delete life_time”) if _, ok := onlineTimeBucket.Data[deviceId].Timer[channelId]; !ok { var onlineModel = new(OnlineTime) onlineModel.Start = serverTime onlineTimeBucket.Data[deviceId].Timer[channelId] = onlineModel } onlineTimeBucket.Data[deviceId].Timer[channelId].End = timer onlineTimeBucket.Data[deviceId].Timer[channelId].Delete = true } onlineTimeBucket.Locker.Unlock() } //记录转发流量并入库 func addFlow(deviceId, channelId string, trafficMap map[string]int) { onlineTimeBucket.Locker.Lock() if _, ok := onlineTimeBucket.Data[deviceId]; !ok { j := new(OnlineTimeBucketSub) j.Timer = make(map[string]*OnlineTime) onlineTimeBucket.Data[deviceId] = j } onlineTimeBucket.Data[deviceId].Timer[channelId].Rcvb = trafficMap[“rcvb”] onlineTimeBucket.Data[deviceId].Timer[channelId].Rcvp = trafficMap[“rcvp”] onlineTimeBucket.Data[deviceId].Timer[channelId].Sentb = trafficMap[“sentb”] onlineTimeBucket.Data[deviceId].Timer[channelId].Sentp = trafficMap[“sentp”] onlineTimeBucket.Data[deviceId].Timer[channelId].TotalTraffIc = true var isDelete = true for _, v := range onlineTimeBucket.Data[deviceId].Timer { if v.Delete == false || v.TotalTraffIc == false { isDelete = false } } if isDelete { //计算时间,,流量 var rcvb, rcvp, sentb, sentp int var end int64 start := time.Now().Unix() for _, v := range onlineTimeBucket.Data[deviceId].Timer { rcvb += v.Rcvb rcvp += v.Rcvp sentp += v.Sentp sentb += v.Sentb if start > v.Start { start = v.Start } if end < v.End { end = v.End } } accountInfo := strings.Split(deviceId, “_”) if len(accountInfo) < 4 { log.Info(“deviceId len fail “, deviceId) } total_time := onlineTimeBucket.Data[deviceId].Timer[channelId].End – onlineTimeBucket.Data[deviceId].Timer[channelId].Start err := InsertTurnServerLogs(accountInfo[3], accountInfo[0], accountInfo[1], accountInfo[2], start, end, total_time, rcvp, rcvb, sentp, sentb) if err != nil { log.Error(err) } onlineTimeBucket.Data[deviceId].Timer = nil delete(onlineTimeBucket.Data, deviceId) } onlineTimeBucket.Locker.Unlock() } |
按照上面的算法来做的话,当我在用web测试的时候,会出现一个情况:
就是如果 coturn 的连接断开了。但是对于这个username, 他还是会保持一两条临时 channel
| 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 | 1) “pmessage” 2) “turn/realm/*” 3) “turn/realm/pano/user/1532433896:26e4cd38fa6fc39fa8fc40fb25fe558c_21_24_18152649/allocation/000000000000001857/traffic” 4) “rcvp=1466, rcvb=1325047, sentp=582, sentb=29696” 1) “pmessage” 2) “turn/realm/*” 3) “turn/realm/pano/user/1532433896:26e4cd38fa6fc39fa8fc40fb25fe558c_21_24_18152649/allocation/001000000000003357/status” 4) “refreshed lifetime=0” 1) “pmessage” 2) “turn/realm/*” 3) “turn/realm/pano/user/1532433896:26e4cd38fa6fc39fa8fc40fb25fe558c_21_24_18152649/allocation/001000000000003357/status” 4) “deleted” 1) “pmessage” 2) “turn/realm/*” 3) “turn/realm/pano/user/1532433896:26e4cd38fa6fc39fa8fc40fb25fe558c_21_24_18152649/allocation/001000000000003357/total_traffic” 4) “rcvp=0, rcvb=0, sentp=0, sentb=0” 1) “pmessage” 2) “turn/realm/*” 3) “turn/realm/pano/user/1532433896:26e4cd38fa6fc39fa8fc40fb25fe558c_21_24_18152649/allocation/000000000000001857/status” 4) “refreshed lifetime=0” 1) “pmessage” 2) “turn/realm/*” 3) “turn/realm/pano/user/1532433896:26e4cd38fa6fc39fa8fc40fb25fe558c_21_24_18152649/allocation/003000000000003734/status” 4) “new lifetime=600” 1) “pmessage” 2) “turn/realm/*” 3) “turn/realm/pano/user/1532433896:26e4cd38fa6fc39fa8fc40fb25fe558c_21_24_18152649/allocation/001000000000003358/status” 4) “new lifetime=600” 1) “pmessage” 2) “turn/realm/*” 3) “turn/realm/pano/user/1532433896:26e4cd38fa6fc39fa8fc40fb25fe558c_21_24_18152649/allocation/000000000000001857/status” 4) “deleted” 1) “pmessage” 2) “turn/realm/*” 3) “turn/realm/pano/user/1532433896:26e4cd38fa6fc39fa8fc40fb25fe558c_21_24_18152649/allocation/000000000000001857/total_traffic” 4) “rcvp=1466, rcvb=1325047, sentp=582, sentb=29696” |

明明我连接断开了,但是又重新连接了两条。而且这两条根本不会释放
这个就会导致 isDelete 的条件没法成立,因为总有新的链接没有释放。
而且后面还发现,一次转发,虽然有好几条channel存在,但是真正转发流量的channel只有一条,而且过了一个多小时,还是没有断。所以原则上我们只要去判断这一条channel,就可以知道本次转发的时长和流量了。 其他的channel都不要管。但是如果是stun这种穿透的,因为没有转发流量,所以根本不知道那一条的channel,才是起作用的那一条????
后面发现这种情况其实是一个bug, 之所以浏览器刷新还保持两条turn channel 是因为浏览器在刷新的时候,其实没有明确的给手机端发送断开webrtc的信号。导致手机端其实还不知道webrtc断开了。所以手机端其实是对turnserver连接进行重连了, 这也是为啥会有新的两条channel 的问题。
我试了其他端,比如 ios 端,如果是断开的话,是不会有遗留turnserver 的channel 的,最后全部都会被清掉。因此对于web端,在刷新浏览器,或者是退出页面的时候,都要给手机的发送断开webrtc的信号,并且断开本地的webrtc连接。
所以web端的代码得改下:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | // 关掉 webrtc stopWebRtcServer: function () { var self = this; // 给手机端发送一个webrtc 关闭的指令 self.mqttSocket.pub( self.subCommonStr + ‘toTarget’, { “method”: “webrtc.stop”, }, { useAes: true, } ).done(function (data) { console.log(“%c accept stop:” + JSON.stringify(data), “color:red”); }); // 清空状态统计的定时 clearTimeout(self.rtcStateMap.timeId); // 关闭signal长连接 self.mqttSocket.close(); // 关闭webrtc 连接 self.rtcSocket.close(); }, |
其中 rtcSocket 的 close 事件为:
| 1 2 3 4 5 6 7 | close: function(){ // 停止 video 传输 var video = this._pc.getRemoteStreams()[0].getVideoTracks()[0]; video.stop(); // 关掉 pc 对象 this._pc.close(); } |

先停止video传输,再释放掉 pc 对象。这时候本来 webrtc work 的时候,是这样的:
看到的消息就是:
| 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 | 1) “pmessage” 2) “turn/realm/*” 3) “turn/realm/pano/user/1532677507:ac5fa606abf5ae9d02bd4206625b4911_21_24_18152992/allocation/001000000000004071/status” 4) “new lifetime=600” 1) “pmessage” 2) “turn/realm/*” 3) “turn/realm/pano/user/1532677507:ac5fa606abf5ae9d02bd4206625b4911_21_24_18152992/allocation/004000000000004390/status” 4) “new lifetime=600” 1) “pmessage” 2) “turn/realm/*” 3) “turn/realm/pano/user/1532677507:ac5fa606abf5ae9d02bd4206625b4911_21_24_18152992/allocation/000000000000002210/status” 4) “new lifetime=600” 1) “pmessage” 2) “turn/realm/*” 3) “turn/realm/pano/user/1532677507:ac5fa606abf5ae9d02bd4206625b4911_21_24_18152992/allocation/004000000000004390/status” 4) “refreshed lifetime=0” 1) “pmessage” 2) “turn/realm/*” 3) “turn/realm/pano/user/1532677507:ac5fa606abf5ae9d02bd4206625b4911_21_24_18152992/allocation/004000000000004390/status” 4) “deleted” 1) “pmessage” 2) “turn/realm/*” 3) “turn/realm/pano/user/1532677507:ac5fa606abf5ae9d02bd4206625b4911_21_24_18152992/allocation/004000000000004390/total_traffic” 4) “rcvp=0, rcvb=0, sentp=0, sentb=0” 1) “pmessage” 2) “turn/realm/*” 3) “turn/realm/pano/user/1532677507:ac5fa606abf5ae9d02bd4206625b4911_21_24_18152992/allocation/001000000000004071/traffic” 4) “rcvp=1546, rcvb=1442311, sentp=502, sentb=24802” |


就是建了3条 channel ,然后删掉一条 390 ,保留两条, 后面分别是 210 和 071
这时候我点击 停止webrtc 按钮,图片传输就会停止了。这时候webrtc 就断开了。
这时候就收到了断开的消息了, 210 和 071 都断开了:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | 1) “pmessage” 2) “turn/realm/*” 3) “turn/realm/pano/user/1532677507:ac5fa606abf5ae9d02bd4206625b4911_21_24_18152992/allocation/000000000000002210/status” 4) “refreshed lifetime=0” 1) “pmessage” 2) “turn/realm/*” 3) “turn/realm/pano/user/1532677507:ac5fa606abf5ae9d02bd4206625b4911_21_24_18152992/allocation/001000000000004071/status” 4) “refreshed lifetime=0” 1) “pmessage” 2) “turn/realm/*” 3) “turn/realm/pano/user/1532677507:ac5fa606abf5ae9d02bd4206625b4911_21_24_18152992/allocation/000000000000002210/status” 4) “deleted” 1) “pmessage” 2) “turn/realm/*” 3) “turn/realm/pano/user/1532677507:ac5fa606abf5ae9d02bd4206625b4911_21_24_18152992/allocation/000000000000002210/total_traffic” 4) “rcvp=0, rcvb=0, sentp=0, sentb=0” 1) “pmessage” 2) “turn/realm/*” 3) “turn/realm/pano/user/1532677507:ac5fa606abf5ae9d02bd4206625b4911_21_24_18152992/allocation/001000000000004071/status” 4) “deleted” 1) “pmessage” 2) “turn/realm/*” 3) “turn/realm/pano/user/1532677507:ac5fa606abf5ae9d02bd4206625b4911_21_24_18152992/allocation/001000000000004071/total_traffic” 4) “rcvp=4595, rcvb=4367095, sentp=1549, sentb=74756” |

这时候channel 就全部为空了。
所以要这样才可以。 但是问题来了,刚才是我们手动点击 停止 webrtc 按钮的,当然流程是对的。
但是如果是直接刷新浏览器呢,那可是来不及处理的????
事实上也是有这个问题,如果直接刷新浏览器,并且不作处理的时候,就会出现最早之前的那种情况,就是手机端认为还连着,所以 turnserver 又建了两条新的 channel 。
所以我后面加了这个事件 onbeforeunload:
| 1 2 3 4 5 6 | // 这时候要设置一个unload事件,不然如果直接浏览器刷新的话,手机端是不知道webrtc不用了,他还会连接 turnserver, 导致turnserver 的session 一直在,后面没法算时间 // 设置unonload, 防止直接刷新浏览器的时候,没有传 webrtc.stop 指令 window.onbeforeunload = function(){ self.stopWebRtcServer(); return true; }; |

每次用户刷新的时候,这时候就会弹出这个窗口,当用户点击 重新加载的时候, 这时候 stop 操作就已经处理完了。
因此就成功关闭了,缺点就是用户每次刷新都会弹窗,并且要点击重新加载。
所以服务端来说的话,整个连接过程中创建的channel 都要做判断,只有当channel 全部断掉的时候, 才判断这个连接结束,这时候才要算时间。虽然每一次连接都有一条channel 在发送流量。所以如果 traffic 有值的话,那么就是这一条有流量统计。所以如果有收到有流量的 total_traffic 的事件 ,那么应该就是 relay 的连接断开了。这时候就可以计算时长并入库了。
所以修改后的代码如下:
| 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 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 | package main import ( “errors” “fmt” “github.com/garyburd/redigo/redis“ “iGong/util/log” “strconv” “strings” “sync” “time” ) type OnlineTimeBucket struct { Data map[string]*OnlineTimeBucketSub Locker *sync.RWMutex } func NewOnlineTimeBucket() *OnlineTimeBucket { return &OnlineTimeBucket{ Data: make(map[string]*OnlineTimeBucketSub), Locker: &sync.RWMutex{}, } } type OnlineTimeBucketSub struct { Timer map[string]*OnlineTime } //map 存在线时间,结束时间-开始时间,,结束之后再存入表 type OnlineTime struct { Start int64 RefreshedFlow int64 End int64 Delete bool TotalTraffIc bool Rcvp int Rcvb int Sentp int Sentb int } var onlineTimeBucket *OnlineTimeBucket var serverTime = time.Now().Unix() /* turn/realm/pano/user/1531882310:7ed1019263ce5e09f3d40a1ca15ae992_21_24_18152992/allocation/000000000000000495/status new lifetime=600 turn/realm/pano/user/1531881513:7ed1019263ce5e09f3d40a1ca15ae992_21_24_18152992/allocation/001000000000000462/status refreshed lifetime=0 turn/realm/pano/user/1531882310:7ed1019263ce5e09f3d40a1ca15ae992_21_24_18152992/allocation/004000000000000899/status deleted turn/realm/pano/user/1531881513:7ed1019263ce5e09f3d40a1ca15ae992_21_24_18152992/allocation/001000000000000462/total_traffic rcvp=18612, rcvb=894124, sentp=104268, sentb=114206669 turn/realm/pano/user/1531881926:26e4cd38fa6fc39fa8fc40fb25fe558c_21_24_18152649/allocation/004000000000000896/traffic rcvp=1847, rcvb=2052114, sentp=201, sentb=10652 */ func TurnServerStats() { conn, err := RedisTurn.GetConn() sub := redis.PubSubConn{Conn: conn} if err != nil { log.Error(err) } defer conn.Close() onlineTimeBucket = NewOnlineTimeBucket() sub.PSubscribe(“turn/realm/*”) for { switch n := sub.Receive().(type) { //case redis.Message: // fmt.Printf(“Message: %s %s\n”, n.Channel, n.Data) case redis.PMessage: err = forwardStats(n) if err != nil { continue } case error: fmt.Printf(“error: %v\n”, n) return } } } func forwardStats(data redis.PMessage) (err error) { fmt.Printf(“PMessage: %s %s %s\n”, data.Pattern, data.Channel, data.Data) event, deviceId, channel, err := decodeChannel(data.Channel) if err != nil { log.Error(err) return err } log.Info(event, deviceId) switch event { case “status”: //统计时长 和在线状态 onlineStatus := decodeDataWithStatus(data.Data) log.Info(onlineStatus) addOnlineTime(deviceId, channel, onlineStatus) case “traffic”, “total_traffic”: //统计流量 这个事件过来说明转发已经结束 并且只有一个会话是有值的 trafficMap, err := decodeDataWithTraffic(data.Data) if err != nil { return err } //rcvp 接收到的包数量 rcvb 接收到的流量 sentp 发送的包数量 sentb 发送的包流量 log.Info(trafficMap) addFlow(deviceId, channel, trafficMap, event) } return } func decodeChannel(channel string) (event string, deviceId, channelId string, err error) { args := strings.Split(channel, “/”) if len(args) != 8 { err = errors.New(“channel fail .”) return } event = args[7] deviceId = strings.Split(args[4], “:”)[1] channelId = args[6] return } func decodeDataWithStatus(data []byte) (onlineStatus int) { args := strings.Split(string(data), “=”) switch args[0] { //新建 case “new lifetime”: onlineStatus = 0 //刷新 case “refreshed lifetime”: onlineStatus = 1 //移除 case “deleted”: onlineStatus = 2 default: onlineStatus = 1 } return } func decodeDataWithTraffic(data []byte) (stats map[string]int, err error) { args := strings.Split(string(data), “,”) if len(args) != 4 { err = errors.New(“traffic data fail”) return } stats = make(map[string]int) for _, v := range args { statsInfo := strings.Split(v, “=”) s, _ := strconv.Atoi(statsInfo[1]) stats[strings.TrimLeft(statsInfo[0], ” “)] = s } return } //记录转发时长 func addOnlineTime(deviceId, channelId string, onlineStatus int) { if onlineStatus == 1 { return } timer := time.Now().Unix() if _, ok := onlineTimeBucket.Data[deviceId]; !ok { j := new(OnlineTimeBucketSub) j.Timer = make(map[string]*OnlineTime) onlineTimeBucket.Locker.Lock() onlineTimeBucket.Data[deviceId] = j onlineTimeBucket.Locker.Unlock() } onlineTimeBucket.Locker.RLock() switch onlineStatus { case 0: log.Info(“new life_time”) var onlineModel = new(OnlineTime) onlineModel.Start = timer onlineTimeBucket.Data[deviceId].Timer[channelId] = onlineModel case 2: log.Info(“delete life_time”) if _, ok := onlineTimeBucket.Data[deviceId].Timer[channelId]; !ok { var onlineModel = new(OnlineTime) onlineModel.Start = serverTime onlineTimeBucket.Data[deviceId].Timer[channelId] = onlineModel } onlineTimeBucket.Data[deviceId].Timer[channelId].End = timer onlineTimeBucket.Data[deviceId].Timer[channelId].Delete = true } onlineTimeBucket.Locker.RUnlock() } //记录转发流量并入库 func addFlow(deviceId, channelId string, trafficMap map[string]int, event string) { if _, ok := onlineTimeBucket.Data[deviceId]; !ok { j := new(OnlineTimeBucketSub) j.Timer = make(map[string]*OnlineTime) j.Timer[channelId].Start = time.Now().Unix() onlineTimeBucket.Locker.Lock() onlineTimeBucket.Data[deviceId] = j onlineTimeBucket.Locker.Unlock() } else { } onlineTimeBucket.Locker.RLock() if event == “total_traffic” { onlineTimeBucket.Data[deviceId].Timer[channelId].Rcvb = trafficMap[“rcvb”] onlineTimeBucket.Data[deviceId].Timer[channelId].Rcvp = trafficMap[“rcvp”] onlineTimeBucket.Data[deviceId].Timer[channelId].Sentb = trafficMap[“sentb”] onlineTimeBucket.Data[deviceId].Timer[channelId].Sentp = trafficMap[“sentp”] onlineTimeBucket.Data[deviceId].Timer[channelId].TotalTraffIc = true var isDelete = true if trafficMap[“rcvb”] == 0 { //当穿透的时候流量为0,并且不会保存回话,所以需要判断所有会话是否关闭 当转发的时候,流量会大于0 ,但是可能会保留回话,所以转发的时候只需要判断流量过来就可以结束 for _, v := range onlineTimeBucket.Data[deviceId].Timer { if v.Delete == false || v.TotalTraffIc == false { isDelete = false } } } if isDelete { onlineTimeBucket.Locker.RUnlock() insertFlowLogs(deviceId) return } } else { onlineTimeBucket.Data[deviceId].Timer[channelId].RefreshedFlow = time.Now().Unix() } onlineTimeBucket.Locker.RUnlock() } func insertFlowLogs(deviceId string) { log.Info(“start insertFlowLogs=>”,deviceId) var rcvb, rcvp, sentb, sentp int var end int64 start := time.Now().Unix() for _, v := range onlineTimeBucket.Data[deviceId].Timer { rcvb += v.Rcvb rcvp += v.Rcvp sentp += v.Sentp sentb += v.Sentb if start > v.Start { start = v.Start } if end < v.End { end = v.End } } accountInfo := strings.Split(deviceId, “_”) if len(accountInfo) < 4 { log.Info(“deviceId len fail “, deviceId) } err := InsertTurnServerLogs(accountInfo[3], accountInfo[0], accountInfo[1], accountInfo[2], start, end, end-start, rcvp, rcvb, sentp, sentb) if err != nil { log.Error(err) } onlineTimeBucket.Locker.Lock() onlineTimeBucket.Data[deviceId].Timer = nil delete(onlineTimeBucket.Data, deviceId) onlineTimeBucket.Locker.Unlock() } //程序关闭时调用,,记录 使用时长 func forceInsertTurnServerUseLogs() { onlineTimeBucket.Locker.Lock() log.Info(“start forceInsertTurnServerUseLogs”) for k, v := range onlineTimeBucket.Data { //取其中一个的开始时间就可以 var start = time.Now().Unix() var end = time.Now().Unix() var rcvb, rcvp, sentb, sentp int for _, v := range v.Timer { rcvb += v.Rcvb rcvp += v.Rcvp sentp += v.Sentp sentb += v.Sentb if start > v.Start { start = v.Start } } useTime := end – start accountInfo := strings.Split(k, “_”) if len(accountInfo) < 4 { log.Info(“deviceId len fail “, k) } err := InsertTurnServerLogs(accountInfo[3], accountInfo[0], accountInfo[1], accountInfo[2], start, end, useTime, rcvp, rcvb, sentp, sentb) if err != nil { log.Error(err) } } onlineTimeBucket.Locker.Unlock() } |
如果是穿透的话,因为没有流量,所以每次收到 total_traffic的时候,都要判断是不是所有的channel 都断开了。如果是的话,那么就是连接断开了。这时候就统计时长并且入统计。
相信很多人都遇到过Candy的情况:
公司电商平台备份后文件大小多达10个G!海量小文件、图片、js、css等等……;
这造成了一种情况,如果不小心删除了一个文件而无法恢复的情况! 那就只能从备份文件中提取咯! 但是一想到为了提取几KB的一个文件,而unzip整个压缩文件。
请谨记以下命令:
unzip <Your zip file> “*mobile/要解压的文件” -d <要解压的目录>
如:unzip www.dnsdizhi.com.zip “Nginx/* ” Nginx/
* : 可以使用*做通配符,具体使用我就不废话了!
-d : -d 参数后面跟上你要解压文件到哪个目录;
使用ssh客户端(如:putty)连接Linux服务器,可能会等待10-30秒才有提示输入密码。严重影响工作效率。登录很慢,登录上去后速度正常,这种情况主要有两种可能的原因:
OpenSSH在用户登录的时候会验证IP,它根据用户的IP使用反向DNS找到主机名,再使用DNS找到IP地址,最后匹配一下登录的IP是否合法。如果客户机的IP没有域名,或者DNS服务器很慢或不通,那么登录就会很花时间。
解决办法:在目标服务器上修改sshd服务器端配置,并重启sshd
用ssh -v user@server 可以看到登录时有如下信息:
注:ssh -vvv user@server 可以看到更细的debug信息
解决办法:
修改sshd服务器端配置
可以使用ssh -o GSSAPIAuthentication=no user@server登录
GSSAPI ( Generic Security Services Application Programming Interface) 是一套类似Kerberos 5的通用网络安全系统接口。该接口是对各种不同的客户端服务器安全机制的封装,以消除安全接口的不同,降低编程难度。但该接口在目标机器无域名解析时会有问题
使用strace查看后发现,ssh在验证完key之后,进行authentication gssapi-with-mic,此时先去连接DNS服务器,在这之后会进行其他操作
OpenSSH_6.6.1, OpenSSL 1.0.1e-fips 11 Feb 2013
debug1: Reading configuration data /etc/ssh/ssh_config
debug1: /etc/ssh/ssh_config line 56: Applying options for *
debug1: Connecting to kr.worthcloud.cn [107.150.126.57] port 36000.
debug1: Connection established.
debug1: identity file /home/fredzeng/.ssh/id_rsa type -1
debug1: identity file /home/fredzeng/.ssh/id_rsa-cert type -1
debug1: identity file /home/fredzeng/.ssh/id_dsa type -1
debug1: identity file /home/fredzeng/.ssh/id_dsa-cert type -1
debug1: identity file /home/fredzeng/.ssh/id_ecdsa type -1
debug1: identity file /home/fredzeng/.ssh/id_ecdsa-cert type -1
debug1: identity file /home/fredzeng/.ssh/id_ed25519 type -1
debug1: identity file /home/fredzeng/.ssh/id_ed25519-cert type -1
debug1: Enabling compatibility mode for protocol 2.0
debug1: Local version string SSH-2.0-OpenSSH_6.6.1
debug1: Remote protocol version 2.0, remote software version OpenSSH_7.4
debug1: match: OpenSSH_7.4 pat OpenSSH* compat 0x04000000
debug1: SSH2_MSG_KEXINIT sent
debug1: SSH2_MSG_KEXINIT received
debug1: kex: server->client aes128-ctr hmac-sha1-etm@openssh.com none
debug1: kex: client->server aes128-ctr hmac-sha1-etm@openssh.com none
debug1: kex: curve25519-sha256@libssh.org need=20 dh_need=20
debug1: kex: curve25519-sha256@libssh.org need=20 dh_need=20
debug1: sending SSH2_MSG_KEX_ECDH_INIT
debug1: expecting SSH2_MSG_KEX_ECDH_REPLY
debug1: Server host key: ECDSA 66:cc:4d:4a:e0:88:3d:77:5f:a4:fd:5f:ff:05:5c:94
debug1: Host ‘[kr.worthcloud.cn]:36000’ is known and matches the ECDSA host key.
debug1: Found key in /home/fredzeng/.ssh/known_hosts:3
debug1: ssh_ecdsa_verify: signature correct
debug1: SSH2_MSG_NEWKEYS sent
debug1: expecting SSH2_MSG_NEWKEYS
debug1: SSH2_MSG_NEWKEYS received
debug1: Roaming not allowed by server
debug1: SSH2_MSG_SERVICE_REQUEST sent
debug1: SSH2_MSG_SERVICE_ACCEPT received
debug1: Authentications that can continue: publickey,password
debug1: Next authentication method: publickey
debug1: Trying private key: /home/fredzeng/.ssh/id_rsa
debug1: Trying private key: /home/fredzeng/.ssh/id_dsa
debug1: Trying private key: /home/fredzeng/.ssh/id_ecdsa
debug1: Trying private key: /home/fredzeng/.ssh/id_ed25519
debug1: Next authentication method: password