Redis CPU使用率过高问题的排查

Redis CPU占用过高会导致所有使用Redis的客户端性能大幅下降,可能的原因中其中一个是大量的请求,尤其是keys命令请求过多,查询流程:

  1. 使用info和monitor命令(这两个命令也可以登录之后使用,不过有可能造成client的crash)

redis-cli -h 192.168.1.2  -a ‘passwd’ info 

Clients

connected_clients:25
client_longest_output_list:0
client_biggest_input_buf:0
blocked_clients:1

Memory

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

Persistence

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

Stats

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

Replication

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

CPU

used_cpu_sys:3372080.50
used_cpu_user:404838.31
used_cpu_sys_children:851.85
used_cpu_user_children:5934.84

Cluster

cluster_enabled:0

Keyspace

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会显示当前的客户端的命令请求;

  1. 使用慢查询 

redis-cli -h 192.168.1.xx  -a ‘ passwd ‘ slowlog get (reset替换get清空旧的log)

这个命令会显示出最近一段时间内的耗时较久的查询。

这几个命令综合起来,基本可以找到是哪些命令频繁调用造成系统繁忙。

一般来说,都是大量调用keys命令并使用通配符造成的。

记一次 Laravel 应用性能调优经历

这是一份事后的总结。在经历了调优过程踩的很多坑之后,我们最终完善并实施了初步的性能测试方案,通过真实的测试数据归纳出了 Laravel 开发过程中的一些实践技巧。

0x00 源起

最近有同事反馈 Laravel 写的应用程序响应有点慢、20几个并发把 CPU 跑满… 为了解决慢的问题,甚至一部分接口用 nodejs 来写。

而我的第一反应是一个流行的框架怎么可能会有这么不堪?一定是使用上哪里出现了问题。为了一探究竟,于是开启了这次 Laravel 应用性能调优之旅。

0x01 调优技巧

这次性能测试方案中用到的优化技巧主要基于 Laravel 框架本身及其提供的工具。

  1. 关闭应用debug app.debug=false
  2. 缓存配置信息 php artisan config:cache
  3. 缓存路由信息 php artisan router:cache
  4. 类映射加载优化 php artisan optimize
  5. 自动加载优化 composer dumpautoload
  6. 根据需要只加载必要的中间件
  7. 使用即时编译器(JIT),如:HHVM、OPcache
  8. 使用 PHP 7.x

除了以上优化技巧之外,还有很多编码上的实践可以提升 Laravel 应用性能,在本文中暂时不会做说明。(也可以关注我的后续文章)

1. 关闭应用 debug

打开应用根目录下的 .env 文件,把 debug 设置为 false。

APP_DEBUG=false

2. 缓存配置信息

php artisan config:cache

运行以上命令可以把 config 文件夹里所有配置信息合并到一个 bootstrap/cache/config.php 文件中,减少运行时载入文件的数量。

php artisan config:clear

运行以上命令可以清除配置信息的缓存,也就是删除 bootstrap/cache/config.php 文件

3. 缓存路由信息

php artisan route:cache

运行以上命令会生成文件 bootstrap/cache/routes.php。路由缓存可以有效的提高路由器的注册效率,在大型应用程序中效果越加明显。

php artisan route:clear

运行以上命令会清除路由缓存,也就是删除 bootstrap/cache/routes.php 文件。

4. 类映射加载优化

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 两个文件。

5. 自动加载优化

composer dumpautoload -o

Laravel 应用程序是使用 composer 来构建的。这个命令会把 PSR-0 和 PSR-4 转换为一个类映射表来提高类的加载速度。

注意:php artisan optimize --force 命令里已经做了这个操作。

6. 根据需要只加载必要的中间件

Laravel 应用程序内置了并开启了很多的中间件。每一个 Laravel 的请求都会加载相关的中间件、产生各种数据。在 app/Http/Kernel.php 中注释掉不需要的中间件(如 session 支持)可以极大的提升性能。

7. 使用即时编译器

HHVM 和 OPcache 都能轻轻松松的让你的应用程序在不用做任何修改的情况下,直接提高 50% 或者更高的性能。

8. 使用 PHP 7.x

只能说 PHP 7.x 比起之前的版本在性能上有了极大的提升。

嗯,限于你的真实企业环境,这个也许很长时间内改变不了,算我没说。

0x02 测试方案

我们使用简单的 Apache ab 命令仅对应用入口文件进行测试,并记录和分析数据。

  1. 仅对应用的入口文件 index.php 进行测试,访问 “/” 或者 “/index.php” 返回框架的欢迎页面。更全面的性能测试需要针对应用的更多接口进行测试。
  2. 使用 Apache ab 命令。ab -t 10 -c 10 {url}。该命令表示对 url 同时发起 10 个请求,并持续 10 秒钟。命令中具体的参数设置需要根据要测试的服务器性能进行选择。
  3. 为了避免机器波动导致的数据错误,每种测试条件会执行多次 ab 命令,并记录命令执行结果,重点关注每秒处理的请求数及请求响应时间,分析并剔除异常值。
  4. 每次对测试条件进行了调整,需要在浏览器上对欢迎页进行访问,确保没有因为测试条件修改而访问出错。如果页面访问出错会导致测试结果错误。

服务器环境说明

所有脱离具体环境的测试数据都没有意义,并且只有在相近的条件下才可以进行比较。

  1. 这套环境运行在 Mac 上,内存 8G,处理器 2.8GHz,SSD 硬盘。
  2. 测试服务器是使用 Homestead 搭建的。虚拟机配置为单核 CPU、2G 内存。
  3. 服务器 PHP 版本为 7.1,未特殊说明,则标识开启了 OPcache。
  4. 测试的 Laravel 应用程序采用 5.2 版本编写。app\Http\routes.php 中定义了 85 个路由。
  5. 测试过程中除了虚拟机、终端及固定的浏览器窗口外,没有会影响机器的程序运行。

以上的数据,大家在自己进行测试时可以参考。

0x03 测试过程及数据

1. 未做任何优化

1.1 操作

  • 按照以下检查项执行相应的操作。
  • 运行 ab -t 10 -c 10 http://myurl.com/index.php

基础检查项

  • .env 文件中 APP_DEBUG=true
  • 不存在 bootstrap/cache/config.php
  • 不存在 bootstrap/cache/routes.php
  • 不存在 bootstrap/cache/compiled.php 和 bootstrap/cache/services.json
  • app/Http/Kernel.php 中开启了大部分的中间件
  • 浏览器访问 Laravel 应用程序欢迎页确保正常访问

1.2 数据记录

2. 关闭应用debug

2.1 操作

  • 在步骤 1 基础上修改 .env 文件中 APP_DEBUG=false
  • 浏览器访问 Laravel 应用程序欢迎页确保正常访问。
  • 运行 ab -t 10 -c 10 http://myurl.com/index.php

2.2 数据记录

2.3 对比结果

与步骤 1 结果比较发现:关闭应用 debug 之后,每秒处理请求数从 26-34 上升到 33-35,请求响应时间从 大部分 300ms 以上下降到 290ms 左右,效果不太明显,但确实有一定的提升。

注意:这部分与应用中的日志等使用情况有比较大的关联。

3. 开启缓存配置信息

3.1 操作

  • 在步骤 2 基础上,运行 php artisan config:cache,确认生成 bootstrap/cache/config.php
  • 浏览器访问 Laravel 应用程序欢迎页确保正常访问。
  • 运行 ab -t 10 -c 10 http://myurl.com/index.php

3.2 数据记录

3.3 对比结果

与步骤 2 结果比较发现:开启配置信息缓存之后,每秒处理请求数从 33-35 上升到 36-38,请求响应时间从 290ms 左右下降到 260ms 左右,效果不太明显,但确实有一定的提升。

4. 开启缓存路由信息

4.1 操作

  • 在步骤 3 基础上,运行 php artisan route:cache,确认生成 bootstrap/cache/routes.php
  • 浏览器访问 Laravel 应用程序欢迎页确保正常访问。
  • 运行 ab -t 10 -c 10 http://myurl.com/index.php

4.2 数据记录

4.3 对比结果

与步骤 3 结果比较发现:开启路由信息缓存之后,每秒处理请求数从 36-38 上升到 60 左右,请求响应时间从 260ms 下降到 160ms 左右,效果显著,从 TPS 看,提升了 70%。

5. 删除不必要的中间件

5.1 操作

  • 在步骤 4 基础上,注释掉不必要的中间件代码。
  • 浏览器访问 Laravel 应用程序欢迎页确保正常访问。
  • 运行 ab -t 10 -c 10 http://myurl.com/index.php

注意:这次测试中我注释掉了所有的中间件。实际情况中应该尽量只留下必要的中间件。

5.2 数据记录

5.3 对比结果

与步骤 4 结果比较发现:删除了不必要的中间件之后,每秒处理请求数从 60 左右上升到 90 左右,请求响应时间从 160ms 下降到 110ms 左右,效果非常明显,从 TPS 看,提升了 50%。

6. 开启类映射加载优化

6.1 操作

  • 在步骤 5 基础上,运行 php artisan optimize --force,确认生成 bootstrap/cache/compiled.php 和 bootstrap/cache/services.json
  • 浏览器访问 Laravel 应用程序欢迎页确保正常访问。
  • 运行 ab -t 10 -c 10 http://myurl.com/index.php

6.2 数据记录

6.3 对比结果

与步骤 5 结果比较发现:做了类映射加载优化之后,每秒处理请求数从 90 上升到 110,请求响应时间从 110ms 下降到 100ms 以下,效果还是比较明显的。

7. 关闭 OPcache

7.1 操作

  • 在步骤 6 基础上,关闭 PHP 的 OPcache,并重启服务器。通过 phpinfo() 的 Zend OPcache 确认 OPcache 已经关闭。
  • 浏览器访问 Laravel 应用程序欢迎页确保正常访问。
  • 运行 ab -t 10 -c 10 http://myurl.com/index.php

7.2 数据记录

7.3 对比结果

与步骤 6 结果比较发现:关闭 OPcache 之后,每秒处理请求数从 110 下降到 15,请求响应时间从 100ms 以下上升到 650ms 以上。开启与关闭 OPcache,数据上竟有几倍的差别。

此后,我重新开启了 PHP 的 OPcache,数据恢复到步骤 6 水平。

0x04 踩过的坑

1. [LogicException] Unable to prepare route [/] for serialization. Uses Closure.

在运行 php artisan route:cache 命令时报这个错误。

原因:路由文件中处理“/”时使用了闭包的方式。要运行该命令,路由的具体实现不能使用闭包方式。

修改方案:将路由的具体实现放到控制器中来实现。

2. [Exception] Serialization of ‘Closure’ is not allowed.

在运行 php artisan route:cache 命令时报这个错误。

原因:路由文件中定义了重复的路由。

修改方案:排查路由文件中的重复路由并修改。尤其要注意 resource 方法很可能导致与其方法重复。

3. [RuntimeException] Invalid filename provided.

在运行 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 一行。

4. InvalidArgumentException in FileViewFinder.php line 137: View [welcome] not found.

在运行 php artisan config:cache 之后,浏览器上访问 Laravel 应用程序欢迎页报这个错误。

原因:Laravel 应用程序服务器是通过 Homestead 在虚拟机上搭建的。而这个命令我是在虚拟机之外运行的,导致生成的 config.php 中的路径是本机路径,而不是虚拟机上的路径。所以无法找到视图文件。

修改方案:ssh 到虚拟机内部运行该命令。

0x05 实践技巧

坑也踩了,测试也做过了。这里针对这次经历做个实践技巧的简单总结。

1. 有效的 Laravel 应用程序优化技巧

  1. 关闭应用debug app.debug=false
  2. 缓存配置信息 php artisan config:cache
  3. 缓存路由信息 php artisan router:cache
  4. 类映射加载优化 php artisan optimize(包含自动加载优化 composer dumpautoload
  5. 根据需要只加载必要的中间件
  6. 使用即时编译器(JIT),如:HHVM、OPcache

2. 编写代码时注意事项

  1. 路由的具体实现放到控制器中。
  2. 不定义重复的路由,尤其注意 resouce 方法。
  3. 弄清各中间件的作用,删除不必要的中间件引用。

0x06 下一步

以上的调优技巧及编码注意事项主要针对框架本身,在真正的业务逻辑编码中有很多具体的优化技巧,在此没有讨论。

后续的优化重点将会放在具体编码实践上:

  1. 使用 Memcached 来存储会话 config/session.php
  2. 使用专业的缓存驱动器
  3. 数据库请求优化
  4. 为数据集书写缓存逻辑
  5. 前端资源合并 Elixir

0x07 写在最后

网上看到很多框架性能对比的文章与争论,也看到很多简单贴出了数据。这些都不足以窥探真实的情况,所以有了我们这次的实践,并在过程中做了详实的记录。在各位读者实践过程中提供参考、比较、反思之用。对于这次实践有疑问的读者,也欢迎提出问题和意见。

不多说了,要学习更多技术干货,请关注微信公众号:up2048。

– EOF –

原文有图参考: https://segmentfault.com/a/1190000011569012

Wi-Fi现场勘测,分析,故障排查

越 来越多的家庭和企业转向WiFi网络作为其首选的互联网接入的提供方式。无线网络对于终端用户更为便捷,使每个人都能够充分利用其移动设备的优势。无线网络相比传统的有线网络而言,也免除了线接的烦扰和限制。

从管理员的角度来说,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网络活动之前,让我们讨论一下 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信号覆盖。将路由器放在家里的角落将会导致WiFi信号泄漏并降低您的信号覆盖区域内的信号强度。

  • 来自其他设备的干扰 – 微波炉和无线电话可能使用的是与您的 WiFi路由器相同的频率,能够产生对信号等级造成影响的背景噪音。

  • 墙壁和地板 – 如果在连接设备的位置可以清楚地看到路由器的位置,那么您将会获得最佳WiFi信号。穿越 墙壁和地板进行传输的信号的强度会大打折扣。

保持您的路由器的更新

将您的路由器的设置改成自动固件升级以保持其处于高效运作状态。阶段性地重启您的路由器也是一个比较好的做法,有利于实现良好的信号覆盖。

距离路由器的距离

需要较强信号的设备,如那些用于玩游戏或观看视频的设备,将它们放在靠近路由器的位置可能会获得更好的信号表现。在一些情况下,您可能需要通过使用像路由器这样的附加设备来 扩展您的WiFi信号范围以增强WiFi信号强度。

mysql文本替换

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 添加redis实现对转发流量和转发时长的统计

前言

虽然已经用 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,我们可以得到这一次转发的开始时间,结束时间,以及转发的流量。
大概的一个截图就是:

1

这边还有更详细的说明: schema.stats.redis
状态的key就是:

1
turn/user/<username>/allocation/<id>/status

举例:

1
turn/realm/pano/user/1531994598:fe909a0a4cde20be0a7bb9fbfdc8d6dc_21_24_18154723/allocation/003000000000002028/status
1

他的值有可能是 new lifetime=… 或者是 refreshed lifetime=…

一个用户名下可能有多个状态。
然后是订阅:

  • 通过订阅: turn/realm/*/user/*/allocation/*/status 这个主题,可以得到一些事件,比如 deleted 事件
  • 通过订阅: turn/realm/*/user/*/allocation/*/traffic 这个主题,可以得到一些信息,比如流量, 当这个配置被删除的时候, 也可以得到通知。

如果是订阅所有的配置,那么就是 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”
1

可以看到进行一次连接的话,会分配好几条临时会话。 从下图来看的话,可以看到有 4 条,其实这四条都属于同一个连接。

接下来我们一条一条来分析:

  • 刚开始建立了一条 3326 的临时channel , 生命周期是 600s,也就是 10分钟
  • 然后又建立了一条 3324 的临时channel
  • 接下来的三条分别是,刷新一条 3700 的临时channel ,lifetime 为0,说明是要删除,所以接下来就收到一条 delete 的状态,说明这个临时topic要删除,然后当删除的时候,就会将这一段时间的流量(total_traffic )也传上来。
  • 接下来的4条,全部都是 3324 这一个topic的流量信息。包括收到的字节和发送的字节
  • 接下来的三条是3326,分别是将lifetime 为0,然后delete,最后关于这条临时channel 的流量过来。
  • 接下来的一条是 3324 这一条的lifetime 为 0, 然后是两条新的临时channel 的建立, 然后就是 3324 收到 delete 的状态,和总流量的通知。

总结一下:

1. 一次的webrtc 的turnserver 转发,其实会分配了好几个临时的 channel。

2. 每个 channel 的生命周期和对应的状态

每个 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 还大了,但是也还没有断开,所以这个也有点奇怪???

3. 流量信息

每一条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”
1

明明我连接断开了,但是又重新连接了两条。而且这两条根本不会释放

这个就会导致 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();
}
1

先停止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”
1
1

就是建了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”
1

这时候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;
};
1

每次用户刷新的时候,这时候就会弹出这个窗口,当用户点击 重新加载的时候, 这时候 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 都断开了。如果是的话,那么就是连接断开了。这时候就统计时长并且入统计。

Linux使用unzip命令解压其中的部分文件到指定文件夹

相信很多人都遇到过Candy的情况:

公司电商平台备份后文件大小多达10个G!海量小文件、图片、js、css等等……;

这造成了一种情况,如果不小心删除了一个文件而无法恢复的情况! 那就只能从备份文件中提取咯! 但是一想到为了提取几KB的一个文件,而unzip整个压缩文件。



请谨记以下命令:

unzip <Your zip file> “*mobile/要解压的文件” -d <要解压的目录>

如:unzip www.dnsdizhi.com.zip “Nginx/* ” Nginx/

* : 可以使用*做通配符,具体使用我就不废话了!
-d : -d 参数后面跟上你要解压文件到哪个目录;

ssh登录很慢解决方法

使用ssh客户端(如:putty)连接Linux服务器,可能会等待10-30秒才有提示输入密码。严重影响工作效率。登录很慢,登录上去后速度正常,这种情况主要有两种可能的原因:

1. DNS反向解析问题

OpenSSH在用户登录的时候会验证IP,它根据用户的IP使用反向DNS找到主机名,再使用DNS找到IP地址,最后匹配一下登录的IP是否合法。如果客户机的IP没有域名,或者DNS服务器很慢或不通,那么登录就会很花时间。

解决办法:在目标服务器上修改sshd服务器端配置,并重启sshd

  1. vi /etc/ssh/sshd_config
  2. UseDNS no

2. 关闭ssh的gssapi认证

用ssh -v user@server 可以看到登录时有如下信息:

  1. debug1: Next authentication method: gssapi-with-mic
  2. debug1: Unspecified GSS failure. Minor code may provide more information

注:ssh -vvv user@server 可以看到更细的debug信息

解决办法:

修改sshd服务器端配置

  1. vi /etc/ssh/ssh_config
  2. GSSAPIAuthentication no

可以使用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

电信、连通要求限制电脑数量联网解决办法

原文且维护更新地址:在厦大宿舍安装路由器

前言

厦大一些宿舍要求用一个客户端联网,所以不能装路由器;17 年暑假我找到了获得真实的 PPPoE 用户名的方法,于是可以绕过绕过这个客户端联网,也可以安装路由器。除此以外,有时用客户端联网会出现一些奇怪的问题(我见过几例,网页打不开或者客户端报错什么的),也可以用这个办法,不用客户端联网直接拨号解决。下面会介绍这个方法。

2018 年七夕前后,学校开始检测时间戳和 UA 来判断一个账号是否共享给了多台设备使用(不论是安装路由器还是在电脑上开热点)。如果检测到就会禁止账号联网半个小时(认证日志显示“检测到代理”);随后我也找到了绕过检查的办法。下面也会介绍这个方法。

如果在阅读的过程中有问题,可以 QQ 联系(897331845)。如果是小白寻求帮助,请在联系我之前确认自己的网线没问题也插对了,确认自己的密码没有弄错,不要因为这些问题浪费大家的时间。如果是来交朋友的,那么非常欢迎。

为了避免被查水表,我还需要多说几句废话:

  1. 对于任何硬、软件的损坏,本人不赔偿,也不保证协助处理,哪怕这样的后果是因教程中的错误造成的;你需要自担风险。出现问题时,我应该会尽力协助,但我不是专业技术人员,没有能力也没有义务保证教程内容万无一失。尽管我还没有遇到过把路由器弄坏的例子,但读者也需要做好损失一台价值几十元或者几百元的路由器的心理准备。另外,本教程中的内容会使你的路由器失去保修。
  2. 对于因为安装路由器而可能造成的事故,我不负责。请设置足够复杂的管理员密码,并妥善保存;如果发生了安全事故(资金被盗、个人信息泄露等),与本人无关。
  3. 厦大宿舍明令禁止安装路由器,相关文件点击这里可以查看。我没有怂恿或建议任何同学安装路由器,安装路由器的行为完全是安装者的个人意志导致的,本人仅仅是作为技术交流的发起者而存在。请仅仅以学习研究为目的鼓捣路由器,并在安装好路由器之后 24 小时内再将之拆除;如果您喜欢上网,请按照学校指明的方式连接到互联网。如果学校根据校规等文件追究责任,与我无关。
  4. 不允许将此技术用于任何商业行为,我本人也保证不会用来赚钱,而会将研究结果无偿地公布出来,只为造福同学的同时顺便装逼
  5. 不要发表不文明的言论。虽然我也觉得学校这样做很不厚道,但说脏话还是不好的。鼓励积极地传播这篇文章,以及义务地帮助身边有需要的同学。

简单地安装路由器(绕过客户端联网)

非常简单:

  1. 确认自己在“厦门大学宽带认证客户端”(就是电脑联网时需要使用的那个软件)中填写的那个学号,以及选择的是电信还是移动。如果忘掉了,先把正确的试出来。

  2. 将学号按照下面的规则转换(实质上是 ASCII 码加四),在前面加上{SRUN2},在后面加上@cmcc(移动)或@ctnet(电信),得到用户名。

转换前 0 1 2 3 4 5 6 7 8 9
转换后 4 5 6 7 8 9 :(英文冒号) ;(英文分号) < =

例如,小明的学号为19720192203456,移动,则用户名为{SRUN2}5=;645=664789:@cmcc

  1. 用户名对应的密码就是“厦门大学宽带认证客户端”中填写的密码。

  2. 如果目标是解决网络问题,在电脑创建 PPPoE 拨号(不会就去百度),把用户名密码填进去,直接拨号就可以了;如果目标是装路由器,将这个用户名和密码按正常的方法填入路由器,按通常方法设置就可以了。有的奇葩路由器用户名不能包含大括号,那就换一台。

我最先找到的办法是在系统日志里找用户名;后来经过别人提醒,才发现用户名有这样的规律。


配置防止共享检测

学校的共享检测并不十分严格,并不总是可以检测到装或者没装路由器;是否被查到看人品,有的人可能几天才会被查到一次。如果要装路由器,建议先不用配置防止共享检测,如果接下来总是被封掉的话,再来配置。

下面的内容中,我假设读者已经熟悉了通常情况下安装路由器的方法。你要是完完全全的小白,那这个不适合你,找你身边会的人或者我帮你做吧。

安装 OpenWrt

这一节的目标是:得到一台安装好 OpenWrt 的路由器。

“OpenWrt”(Linux 的一个发行版)是一个安装在路由器上的操作系统。OpenWrt 对路由器的性能要求较高,所以只有一部分中高端的路由器可以安装。淘宝或者咸鱼上可以很容易找到已经安装好 OpenWrt 的路由器售卖,最便宜大概五十块钱就可以买到;也可以自己买一台路由器,自己动手安装 OpenWrt。

OpenWrt 的官网是 openwrt.org,打开比较慢需要耐心等待。在这里直接搜索你的路由器的型号,一般来说就可以找到一个详细的页面,上面会详细地说明这个型号是否支持安装 OpenWrt 以及在哪里下载、怎样安装等。也可以直接百度或者谷歌,常常也可以找到详尽的中文教程。不同型号的路由器,安装方法不尽相同;任何版本的 OpenWrt 都是可以用来绕开检测的,并且安装方法没有区别;推荐安装官方最新稳定版。当然了,如果无论如何也找不到相关的资料,那说明这个路由器不能安装 OpenWrt。

路由器的型号(Model)在路由器背面就可以看到。有时,同一个型号中还会分几个不同的硬件版本。

有的路由器会注明“基于 OpenWrt 深度定制”,不要买,除非你有办法刷回原版 OpenWrt。有的路由器会注明安装了“明月永在”或其它非官方版本的 OpenWrt,也可以买,但因为无法拿到这个系统的 SDK,所以在接下来的步骤中要么受限制、要么还需要自己再刷回官方版本;具体情况后面会说明。

“LEDE”和 OpenWrt 几乎一样,可以当作 OpenWrt 来用。

在很多情况下,安装 OpenWrt 只需要在管理页面中使用“系统更新”或“固件升级”功能,就可以将 OpenWrt 安装好(使用文件名以factory.xxx结尾的那个文件,勾选“不保留配置”)。

安装好 OpenWrt 后,建议等系统完全启动后,马上再断电重启一遍。

由于这一步没有统一的方法,虽然比较麻烦,但无法更详细地写下去了。

配置上网

现在,你已经有了一台安装好 OpenWrt 的路由器。这一节的目标是,让你的 OpenWrt 可以上网,以及配置好无线。

方法和配置大多路由器差不多,只不过 OpenWrt 没有面向新手的一目了然的指引,而需要在繁多的设置中找出需要修改的那部分。

  1. 进入管理页面。如果是刚刷入的 OpenWrt 没有root账户密码,登入后按照提醒设置一个即可;如果已经有了密码,也可以修改:点击 Go to password configuration 按钮或 System -> Administrator(管理权)。

    如果你不知道路由器的管理页面地址,可以接好路由器后打开命令提示符,输入ipconfig并回车,寻找“默认网关”一项。新安装的 OpenWrt 管理页面地址一般是192.168.1.1

    • Password(密码):填写你想好的管理员密码。
    • Confirmation(确认密码):再填一遍。

    点击 Save & Apply(保存并应用)。

  2. 点击 Network(网络)-> Interfaces(接口),点击“WAN”对应的 Edit 按钮。

    • Protocol(通信协议):改选为PPPoE,然后点击出现的 Switch Protocol(切换协议)按钮。
    • PAP/CHAP username(PAP/CHAP 用户名):填入之前由学号转换得到的用户名。
    • PAP/CHAP password(PAP/CHAP 密码):填入对应的密码。

    点击 Save & Apply(保存并应用)。这时,路由器本身和有线网已经可以上网了。

  3. 点击 Network -> Wireless(无线)。如果提示 Disabled(已禁用)就点击 Enable(启用)。可能只有一个 2.4G 的,也可能有一个 2.4G 的、一个 5G 的。点 2.4G 的“编辑”。

    • ESSID:填 WiFi 的名字。

    点击 Wireless Security(无线安全)。

    • Encryption(加密):改选为WPA2-PSK
    • Key(密码):填 WiFi 密码。

    点击 Save & Apply(保存并应用)。

    如果还有一个 5G 的,就一样地配置,只是 Channel(信道)需要选择147或更高。

    这时,WiFi 就可以正常联网啦。

配置防检测

接下来的步骤必须联网才行,进行之前先确认已经可以使用路由器的网络打开百度。另外,如果你现在是在手机上看我的这篇文章,强烈建议你接下来改用电脑看,可以方便地复制粘贴一些代码;如果你非要手动打代码,注意不要打错,一个空格都不要错。

防检测分为两个部分:同步局域网内电脑的时间,以及修改 HTTP 头的 UA。

同步时间

NTP 就是用来同步两台电脑上的时钟的协议。接下来先启用 OpenWrt 自带的 NTP 服务器,然后将局域网内所有时钟校正的请求都发给路由器上的 NTP 服务器(也就是说,无视局域网内的电脑原本想要和哪台服务器同步时间,而强制将路由器上的时钟作为标准),这样局域网内所有的电脑上的时间都会变得一致。

只需要在管理页面中修改几个设置就可以了。

  • 配置 NTP 服务。点击 System -> System。

    • 勾选 Enable NTP client(启用 NTP 客户端)和 Provide NTP server(作为 NTP 服务器提供服务)。
    • NTP server candidates(候选 NTP 服务器)四个框框分别填写time.xmu.edu.cncn.pool.ntp.orgpool.ntp.orgtime.apple.com

    点击 Save & Apply 按钮。

  • 配置防火墙转发。点击 Network -> Firewall(防火墙),然后点击 Custom Rules 标签,在大框框里另起一行,添加下面的代码:

    iptables -t nat -N ntp_force_local
    iptables -t nat -I PREROUTING -p udp --dport 123 -j ntp_force_local
    iptables -t nat -A ntp_force_local -d 0.0.0.0/8 -j RETURN
    iptables -t nat -A ntp_force_local -d 127.0.0.0/8 -j RETURN
    iptables -t nat -A ntp_force_local -d 192.168.0.0/16 -j RETURN
    iptables -t nat -A ntp_force_local -s 192.168.0.0/16 -j DNAT --to-destination 192.168.1.1 

    第六行最后的192.168.1.1需要修改为你的路由器的管理页面地址。例如,你的管理页面地址是192.168.10.1,那么这一行代码需要修改为:

    iptables -t nat -A ntp_force_local -s 192.168.0.0/16 -j DNAT --to-destination 192.168.10.1 

    然后点击 Restart Firewall(重启防火墙)。

  • 确认效果。在 Windows 电脑上,打开控制面板,在右上角查看方式处选择小图标,然后点击“日期和时间”。点击 Internet 时间 -> 更改设置,点几次“立即更新”,直到提示“时钟在 xxx 与 xxx 同步成功”。这时,暂时地拔掉墙上接口与路由器之间的网线(断开了外网的连接),再点一次“立即更新”,应该仍然提示成功。

修改 HTTP 头的 UA

UA 中包含了操作系统版本等信息,而 HTTP 协议没有对这些信息加密,因此别人可以从这里看到这个数据包发自 Windows 电脑还是安卓手机等等。接下来把所有 HTTP 头中的 UA 都改得一样。

有两种方法来修改 UA,二选一即可。另外,修改 UA 还会导致其它的异常状况,稍等我会说明如何设置例外。

使用 Privoxy 修改 UA

不推荐。这个方法很简单,但是会导致 HTTP 协议的内容传输缓慢(比如 QQ 发送图片会特别慢,有的下载也会特别慢)。但是我还是把它列出来吧,因为确实很容易配置。

接下来,会安装好 Privoxy 并正确配置,然后将所有 HTTP 流量转发给 Privoxy 代理,并在 Privoxy 中替换 UA。

  • 安装 Privoxy。进入路由器管理页面,点击 System -> Software(软件包)。

    • 点击 Update lists(刷新列表)按钮,等待几分钟。如果提示好几条“Signature check passed”那么这一步执行成功;如果卡死了,几分钟后再进入这个页面,看到了很长很长的软件列表,那也是成功了。要是老不成功,可能是晚上网络太差了,白天上网人少的时候试试。
    • 在 Filter(过滤器)中填写luci-app-privoxy,点击 Find package(查找软件包)按钮。点击下方“luci-app-privoxy”对应的 Install(安装)按钮。如果提示好几条“Configuring xxxx”,那么就是执行成功了;如果卡死后再进入管理页面,看到有一个 Services(服务)菜单,菜单里有 Privoxy WEB proxy(Privoxy 网络代理),那也是成功了。
  • 配置 Privoxy 设置。点击 Services -> Privoxy WEB proxy。

    • Files and Directories(文件和目录):Action Files 删除到只剩一个框,填入match-all.action。Filter files 和 Trust files 均留空。
    • Access Control(访问控制):Listen addresses 填写0.0.0.0:8118,Permit access 填写192.168.0.0/16。Enable action file editor 勾选。
    • Miscellaneous(杂项):Accept intercepted requests 勾选。
    • Logging(日志):全部取消勾选。

    点击 Save & Apply。

  • 配置防火墙转发。点击 Network -> Firewall(防火墙),然后点击 Custom Rules 标签,在大框框里另起一行,添加下面的代码:

    iptables -t nat -N http_ua_drop
    iptables -t nat -I PREROUTING -p tcp --dport 80 -j http_ua_drop
    iptables -t nat -A http_ua_drop -d 0.0.0.0/8 -j RETURN
    iptables -t nat -A http_ua_drop -d 127.0.0.0/8 -j RETURN
    iptables -t nat -A http_ua_drop -d 192.168.0.0/16 -j RETURN
    iptables -t nat -A http_ua_drop -p tcp -j REDIRECT --to-port 8118 

点击 Restart Firewall(重启防火墙)按钮。

  • 使用 Privoxy 替换 UA。在路由器的局域网中打开http://config.privoxy.org/edit-actions-list?f=0,点击 Edit 按钮。Action 那一列中,hide-user-agent 改选为 Enable(绿色),在右侧 User Agent string to send 框中填写Privoxy/1.0;其它全部选择为 No Change (紫色)。点击 Submit 按钮。

使用 XMURP-UA 修改 UA

接下来需要下载我写好的代码,在 Linux 下将它编译后上传到路由器,然后安装好。但是大部分人并不熟悉 Linux 或者懒得装 Linux,所以你可以把你的路由器的型号、硬件版本、OpenWrt 版本发给我(QQ:897331845),我编译好软件包后发给你,然后你按照下面的方法安装:

  • 在电脑上安装 WInSCP。在官网下载,使用默认选项安装好就行了。

  • 打开 WinSCP,文件协议选择“SCP”,主机名填写管理页面地址,端口号填写 22(默认应该就是),用户名填写 root,密码填写你的管理员密码,点“登录”连接到路由器。如果出现警告,继续就可以了。

  • WinSCP 窗口右半边展示的就是路由器上的文件了;双击第一行的 .. 是返回上一层目录。把我发给你的文件(kmod-xmurp-ua-xxxx.ipk)复制到路由器的根目录(最上层目录)的 tmp 文件夹下,不要改名。

  • 然后点击靠近左上角的一个黑方块(鼠标指上去会提示“打开终端”),打开“控制台”,输入以下命令并点击“执行”:

    opkg install /tmp/*.ipk 

    执行完就可以关掉 WinSCP 了。

  • 浏览器打开路由器管理页面,选择 System(系统)->Kernel Log(内核日志),在最后几行看到有类似这样的句子:

    [xxxx.xxxx] xmurp-ua: Started.
    [xxxx.xxxx] xmurp-ua: nf_register_hook returnd 0. 

    这说明模块已经被成功加载。

  • 对于较新的 OpenWrt,在 Network -> Firewall(防火墙)中,可能有流量分载(offloading)的开关。如果有,不要勾选。如果确实需要启用流量分载(大概就是 nat 加速,具体我也不熟悉),可以在防火墙的自定义规则中加入:

    iptables -t filter -I FORWARD -p tcp ! --dport 80 -m conntrack --ctstate RELATED,ESTABLISHED -j FLOWOFFLOAD --hw 

如果你执意要自己编译,那么下面是步骤。

  • 在电脑上安装一个 64 位的 Linux 系统。安装成虚拟机或双系统都可以。推荐使用 Win10 的 Ubuntu 子系统(WSL)。

  • 安装编译依赖项。

    sudo apt-get update && sudo apt-get install git-core build-essential libssl-dev libncurses5-dev unzip gawk subversion mercurial ccache tar ssh 
  • 下载 SDK。

    不同路由器、不同系统版本,SDK 都是不同的。官方 OpenWrt 的 SDK 和固件放在同一个目录下;删掉 OpenWrt 固件下载链接最后的文件名,将剩余的部分复制到浏览器地址栏,回车,拉到最下面,就可以看到 SDK 的下载地址了。比如,WNDR4300 的固件下载地址为:

    http://downloads.openwrt.org/releases/18.06.1/targets/ar71xx/nand/openwrt-18.06.1-ar71xx-nand-wndr4300-ubi-factory.img 

    打开网站:

    http://downloads.openwrt.org/releases/18.06.1/targets/ar71xx/nand/ 

    就可以找到对应 SDK 的下载地址。

    下载好 SDK 后,放到用户目录下,解压 SDK 并进入。

    cd ~ && tar xvf *.tar.xz && cd openwrt-sdk* 

    如果使用 WSL,可以将 SDK 放到 C 盘根目录,然后使用命令 cd ~ && cp /mnt/c/*.tar.xz . 将它复制到 WSL 中

  • 下载 xmurp-ua 的源代码,并编译。

    git clone https://github.com/CHN-beta/xmurp-ua.git package/xmurp-ua make package/xmurp-ua/compile V=sc 

    如果弹出来一个菜单,按左右键选择 Exit,回车;然后选择 Save,回车;即可继续。

    如果编译失败(使用 LEDE 的 SDK 时),可能需要在编译时增加一些参数,参考这篇文章;也可能是我的代码有误(没有适配你的路由器内核),请反馈给我。

  • 将编译好的软件包上传到路由器,安装。

    逐行执行:

    scp bin/targets/*/*/packages/*.ipk root@192.168.1.1:/tmp ssh root@192.168.1.1 "opkg install /tmp/*.ipk" 

    其中的 192.168.1.1 换成你自己的管理页面地址。

    如果提示是否继续,输入 yes 即可。还会提示输入密码,输入路由器的管理员密码即可。如果使用 WSL,也可以复制出来到 Windows,再上传到路由器、安装。

确认效果

打开 ua.chn.moe,网页上应该显示 Privoxy/1.0(如果使用 Privoxy 修改 UA)或 XMURP/1.0(如果使用 XMURP-UA 修改 UA)。

对于使用 XMURP-UA 修改的情况,如果没有专门设置例外,并且确实没有修改成功的话,考虑是否是 mark 冲突了;也就是说,别的模块设置了 mark 的第九位,导致我的模块将这些流量当成例外而放行(具体看下文)。如果出现这样的情况,可以在内核日志中找到这样的句子:

[xxxx.xxxx] xmurp-ua: Mark matched. Note that all packages with the mark will be ACCEPT without modify.
[xxxx.xxxx] xmurp-ua: If the mark is not set manually, it maybe a conflict there. Find out which app is using the desired bit and let it use others, or modify and recompile me. 

解决方法也是如上所述。

设置例外

修改 UA 后会导致一些异常,比如 WeGame 和腾讯游戏下载器无法下载,“校友邦”无法登陆,还有人反馈 GTA5(好像是叫这个名字)不能玩。建议给自己局域网内的电脑都设置例外。

对于使用 XMURP-UA 修改的情况,只需要在防火墙自定义规则中,另起一行,加上下面的代码(其中的 01:23:45:67:89:AB 替换为自己电脑的 MAC 地址),点击“重启防火墙”,那么来自这台电脑的 HTTP 头 UA 就不会被修改;将命令删除或者在前面加一个#,点击“重启防火墙”,就可以恢复。

iptables -t mangle -A PREROUTING -p tcp --dport 80 -m mac --mac-source 01:23:45:67:89:AB -j MARK --set-xmark 0x100/0x100 

对于 Windows 系统,在命令提示符中输入 ipconfig -all 并回车,“物理地址”一项就是 MAC 地址。

如果需要临时将所有的流量都设置为例外:

iptables -t mangle -A PREROUTING -p tcp -m tcp --dport 80 -j MARK --set-xmark 0x100/0x100 

如果你稍稍了解 iptables 命令,就可以看明白我在干什么;如果不了解,也可以照葫芦画瓢。

对于使用 privoxy 修改的情况,则是在防火墙自定义规则中增加类似于下面的内容:

iptables -t nat -I http_ua_drop -m mac --mac-source 01:23:45:67:89:AB -j RETURN 

使用 SRUN3 拨号

学校给的客户端其实是使用 SRUN3 拨号而不是 SRUN2,只是 SRUN3 在普通路由器上无法实现,SRUN2 在厦大恰好也可以拨通,就用它了。对于 OpenWrt,稍稍修改拨号脚本后就可以使用 SRUN3 拨号。最近,用 SRUN2 拨号经常会掉线几分钟,SRUN3 就没问题,不确定是学校故意搞的鬼还是巧合。

下面是用 SRUN3 拨号的步骤:

  • 修改拨号脚本。WinSCP 连接到路由器后,打开文件 /lib/netifd/proto/ppp.sh,这个文件就是拨号的脚本了。搜索 username 关键字,可以找到这样一句:

     {username:+user "username" password "password"} \
      ``` 把它修改成: ```bash {username:+user "(echo -e username)" password "$password"} \ 

    保存,关闭 WinSCP。

    (上面的内容排版有问题,是 gitbook 的 bug,你们知道我想表达什么就行了。)

  • 打开管理页面,点击 Network(网络)-> Interfaces(接口),点击“WAN”对应的 Edit 按钮。用户名中的 {SRUN2} 需要修改成 {SRUN3}\r\n。举例来说,小明原来的用户名是:

    {SRUN2}5=;645=664789:@cmcc 

    那么,他需要改成:

    {SRUN3}\r\n5=;645=664789:@cmcc 

    保存并应用,等待几分钟,就可以正常使用了。


使用 WinSCP 快速配置防共享检测

我忽然觉得这部分太沙雕了,就删了。

参考资料

更新记录

  • 2018.9.15:修正修改 UA 中的一些错误。
  • 2018.9.15:补充使用 LEDE 的 SDK 时可能出现的一些问题。
  • 2018.9.16:确认不需要修改 IPID。
  • 2018.9.16:改正关于 HWNAT 的说法。
  • 2018.9.18:确认不需要做 NTP 服务。
  • 2018.9.20:在仅仅修改 UA 的情况下,学校今天中午封掉了我的路由器。赶紧又加载了 NTP 服务和改 IPID/TTL 的内容。看接下来会怎样。
  • 2018.10.3:优化 privoxy 配置文件。
  • 2018.10.4:将近两个星期过去了。这次确认,不改 IPID 和 TTL,只改 HTTP 的 UA 和 NTP 服务已经足够防止封杀。
  • 2018.10.10:整理了一下这篇文章,准备写那篇面向小白的了。
  • 2018.10.19(20454):再次整理了一下文章,完成了给小白看的版本。
  • 2018.10.22(20505):更改了使用 shadowsocks 翻墙的内容(实际上服务器并没有开 UDP 转发支持,而且我错误地配置了 mwan3,才导致的不稳定)。
  • 2018.10.24(20534):改正了教程里的两个小错误。如果之前照着教程走不成功的,可以再试一次。因为自己的疏忽给大家造了麻烦,我深表歉意。另外,学校封路由器,大家生气归生气,但是作为一个高素质的厦大学子,不要说脏话。
  • 2018.11.16(20853):发现一些路由器 sysupgrade 和 factory 固件都是.bin后缀,把这一点改正了。还有的人说升级 OpenWRT 之后路由器就开不了机了,虽然我觉得应该是他操作的问题,但还是加上了不升级的操作方法。另外,应该有人根本不介意路由器偶尔掉线,那这样的话,其实不需要这样复杂地配置。
  • 2018.12.15(20C63):解决了 HTTP 下载卡顿的问题。另外把额外配置的那一个部分删掉了,寒假专门写一篇文章吧。
  • 2018.12.15(20C64):对操作过程中可能出现的正常的卡死作了说明。补充了用 Privoxy 改 UA 可能导致的一些问题。
  • 2018.12.22(20D55):重写了改 UA 时的代码。
  • 2018.12.22(20D61):发现 UA 不是 GET 特有的字段,其它情况貌似基本是 POST 了(剩下的请求类型貌似不常用)。出这样的错误,显然就是专业计算机和非专业爱好者的区别吧。
  • 2018.12.24(20E13):删掉了改 IP 头的部分,改 UA 的代码也放到了 GitHub。不过这样看起来,这篇文章的主要内容反而不占主要的字数了。
  • 2018.12.25(20E24):把整个文章重新整理了一下,把新的方法写了进去。另外,Merry Christmas!
  • 2019.1.8(20G24):代码适配新内核、修复 BUG。
  • 2019.2.6(21233):把两篇文章合并了(放弃了教会所有小白的念头),删掉了很多废话。
  • 2019.2.8(21245):稍稍补充。
  • 2019.3.17(22403):博客搬家的过程中哪里被我搞坏了,就复制粘贴到了这里。这是我新博客第一篇不水的文章。
  • 2019.3.29(22552):发现原来查看 UA 的网站挂掉了,就自己做了个简易的。
  • 2019.4.17(22834):把语言改生动活泼了一些,以及一些其它的小修改。
  • 2019.7.6(23261):确认学校不再检测共享。不知道是彻底放弃了阻止网络共享的计划,还是计划在这个暑假再搞个更厉害的。如果是前者那当然好,如果是后者我奉陪到底。
  • 2019.9.16(30013):学校又开始查了。我感到兴奋。
  • 2019.9.20(30052):发现 xmurp-ua 和 luci-app-shadowsocks 以及 offloading 冲突,所以修改了 mark 位置,增加了关于 offloading 的说明。
  • 2019.10.10(30355):源代码中完善了点。增加了 mark 冲突的检查。增加使用了 SRUN3 拨号的方案。

SSH连接永远发起,卡在“服务:pledge:network”

使用ssh连接到我的一台服务器需要超过20秒的时间才能启动。

这与局域网或广域网条件无关,因为连接到本身需要相同的(ssh localhost)。 连接build立之后,与服务器进行交互是非常快的。

使用-v表示在说出“pledge:network”之后连接被卡住了。 此时,身份validation(这里使用密钥)已经完成,如下所示:

... debug1: Authentication succeeded (publickey). Authenticated to myserver.mydomain.com ([xx.xx.xx.xx]:22). debug1: channel 0: new [client-session] debug2: channel 0: send open debug1: Requesting no-more-sessions@openssh.com debug1: Entering interactive session. debug1: pledge: network 

(…在这里停留15到30秒…)

 debug1: client_input_global_request: rtype hostkeys-00@openssh.com want_reply 0 debug2: callback start debug2: fd 3 setting TCP_NODELAY debug2: client_session2_setup: id 0 ... 

服务器是CentOS 7.2。 它已经发生在我以前与另一台服务器(是CentOS 7.2),nerver发现解决scheme,并在一段时间后消失的问题…

sshd_config是Ubuntu提供的默认configuration。

到目前为止我已经尝试过:

  • 在ssh命令中使用-o GSSAPIAuthentication = no
  • 使用密码而不是密钥
  • 在sshd_config中使用UsePrivilegeSeparation no而不是yes

测试回复正常速度

Installing Janus on Redhat Enterprise Linux

This article discusses the installation of Janus on a Redhat Enterprise Linux 7 server. The installation process is long and complicated due to the dependencies required for Janus and their lack of inclusion in Yum repositories.

The steps below require come code compiling. The process is simple but may be scary for those who’ve never done it. Compiling code has a side effect hindering the use of Yum. Yum (as a system) only knows about packages installed with yum (and rpm) and therefore cannot see what you’ve compiled and installed manually.

The reason i’ve documented this process is because it was difficult. A client requested that we install Janus on their application server which runs Redhat Enterprise Linux 7. In hindsight i would recommend a dedicated Ubuntu server for this role (the media server, not the web sever).

Janus is a WebRTC server described as “Janus is an open source, general purpose, WebRTC server designed and developed by [Meetecho](http://www.meetecho.com). This version of the server is tailored for Linux systems, although it can be compiled for, and installed on, MacOS machines as well. Windows is not supported, but if that’s a requirement, Janus is known to work in the “Windows Subsystem for Linux” on Windows 10.

I strongly recommend taking a backup of the system before following the steps in this article. Even better would be to do this on a test system and then on a production system (still with backups or a snapshot that can be reverted to). I’ve sourced CentOS packages to fill the gaps in what Redhat doesn’t supply.

TIP: This biggest sticking point for me was the “libsrtp, libsrtp-devel” software. See below for the section on installing them. It was a matter of doing things in the right order. If what i’ve done doesn’t work for you, try in different ways with different commands such as “yum” versus “rpm -ihv” versus “rpm -Uhv”.

It helps to have EPEL configured.

yum install epel-release

Install as much as possible using the following Yum command. Several of the packages listed in the command below wont be found.

yum install libmicrohttpd-devel jansson-devel \
openssl-devel libsrtp-devel sofia-sip-devel glib-devel \
opus-devel libogg-devel libcurl-devel lua-devel \
pkgconfig gengetopt libtool autoconf automake coreutils \
glib2-devel openssl098e

Throughout this process i have sourced dependencies from “https://pkgs.org/”.  The versions that i’ve noted below will obviously change over time so consider the date of this article and the versions available.

The documentation at “https://janus.conf.meetecho.com/docs/README.html” gives guidance on the installation process but not specific to Redhat (or CentOS, etc) and includes content not included here. Consider reading that article if you get stuck.

The following sections start with the name of the dependencies being installed (in bold).

Packages: libmicrohttpd, libmicrohttpd-devel wget http://mirror.centos.org/centos/7/os/x86_64/Packages/libmicrohttpd-0.9.33-2.el7.x86_64.rpm
wget http://mirror.centos.org/centos/7/os/x86_64/Packages/libmicrohttpd-devel-0.9.33-2.el7.x86_64.rpm
rpm -ihv libmicrohttpd-0.9.33-2.el7.x86_64.rpm
rpm -ihv libmicrohttpd-devel-0.9.33-2.el7.x86_64.rpm
Package: libnice wget http://mirror.centos.org/centos/7/os/x86_64/Packages/libnice-devel-0.1.3-4.el7.x86_64.rpm
rpm -ihv libnice-devel
Packages: jansson, jansson-devel wget http://mirror.centos.org/centos/7/os/x86_64/Packages/jansson-2.10-1.el7.x86_64.rpm
wget http://mirror.centos.org/centos/7/os/x86_64/Packages/jansson-devel-2.10-1.el7.x86_64.rpm
rpm -Uhv jansson-2.10-1.el7.x86_64.rpm
rpm -ihv jansson-devel-2.10-1.el7.x86_64.rpm
Packages: libsrtp, libsrtp-devel yum remove gstreamer1-plugins-bad-free
wget http://dl.fedoraproject.org/pub/epel/6/x86_64/Packages/l/libsrtp-devel.1.5.4-3.el6.x86_64.rpm
wget http://dl.fedoraproject.org/pub/epel/6/x86_64/Packages/l/libsrtp-1.5.4-3.el6.x86_64.rpm
rpm -Uhv libsrtp-1.5.4-3.el6.x86_64.rpm libsrtp-devel-1.5.4-3.el6.x86_64.rpm
yum install cheese empathy farstream02 telepathy-farstream totem totem-nautilus
Package: sofia-sip-devel wget -O sofia-sip-1.12.11.tar.gz https://downloads.sourceforge.net/project/sofia-sip/sofia-sip/1.12.11/sofia-sip-1.12.11.tar.gz?r=https%3A%2F%2Fsourceforge.net%2Fprojects%2Fsofia-sip%2Ffiles%2Flatest%2Fdownload&ts=1540078563
tar -xzf sofia-sip-1.12.11.tar.gz
cd sofia-sip-1.12.11
./configure; make; make install
Package: opus-devel wget http://mirror.centos.org/centos/7/os/x86_64/Packages/opus-devel-1.0.2-6.el7.x86_64.rpm
rpm -ihv opus-devel-1.0.2-6.el7.x86_64.rpm
Package: libogg-devel wget http://mirror.centos.org/centos/7/os/x86_64/Packages/libogg-devel-1.3.0-7.el7.x86_64.rpm
rpm -ihv libogg-devel-1.3.0-7.el7.x86_64.rpm
Package: lua-devel wget http://mirror.centos.org/centos/7/os/x86_64/Packages/lua-devel-5.1.4-15.el7.x86_64.rpm
rpm -ihv lua-devel-5.1.4-15.el7.x86_64.rpm
Janus: git clone https://github.com/meetecho/janus-gateway.git
cd janus-gateway
sh autogen.sh

The final step is to compile Janus. This step will end either in success of an explanation of anything missing that you’ll need to install manually.

./configure --prefix=/opt/janus
make
make install

Now you should be able to go into “/opt/janus” and run the following command:

janus -V