作者归档:daniel
40块钱解决IPTV内网融合
起因
去年的这个时候,解决了家里组网的问题:《我的家庭AC+AP分体组网高性价比满血方案》。最近,由于家里要看直播的需求开始骚动,于是把目光投向了电信的IPTV。
之所以选择运营商的IPTV,主要是因为互联网三方直播走的是公网,遇到节假日直播,非常容易卡顿。而运营上IPTV具有独立带宽、vlan专网、不占用互联网带宽、有4K信号源等优点,使用感受上非常稳定,尤其是看世界杯直播、晚会直播有相当优势。
基础知识
运营商的IPTV直播一般使用igmp 组播协议。认证上,主要有两种,一种是电信机顶盒上PPPOE拨好,一种是IPoE, 也就是DHCP+r认证:
DHCP+OPTION扩展字段进行认证,又称为IPoE认证方式。用来作为DHCP扩展的OPTION字段主要为OPTION60和OPTION82。其中,OPTION60中带有Vendor和Service Option信息,是由用户终端发起DHCP请求时携带的信息,网络设备只需要透传即可。其在应用中的作用是用来识别用户终端类型,从而识别用户业务类型,DHCP服务器可以依赖于此分配不同的业务IP地址。OPTION82信息是由网络设备插入在终端发出的DHCP报文中,主要用来标识用户终端的接入位置,DHCP OPTION82信息可以由DHCP SNOOP-ING或DHCPRELAY设备进行插入。
成都地区是第二种认证方式,也是当前把IPTV融合到内网最简单的方式。
网络结构及设备
家里设备挺多,但是结构尽量保持简单:光猫桥接,在主路由A进行拨号;电信IPTV直插电信盒子。主路由A下面挂了两个10口千兆交换机,且都不支持vlan, 两个交换机之间通过光纤连接。弱电箱空间足够,且网线预留充足,因此就不考虑vlan单线复用方案了。
从海鲜市场40包邮购入了一个今天主角——新路由mini,并刷入padavan作为路由器B待接入内网。
IPTV内网融合设置方法
- 光猫IPTV的LAN口接入独立的路由器B的WAN口。路由器B只有个作用:接收组播的IPTV数据,然后将它通过udprxy转为http供内网客户端访问。路由器
- 设置路由器B的WAN口。因为IPTV的LAN口是组播,成都地区IPTV虽然采用了IPoE认证,但是因为我们不需要看回放,因此只需要随意设置一个IP即可,注意改IP不要跟主路由A在同一个网络。
- 路由器B的LAN口接入路由器A的LAN口。设置路由器B的LAN地址为主路由A的某地址,这里我设置为
192.168.10.252
。关闭路由器B的DHCP功能,避免跟主路由A的DHCP冲突。 - 开启路由器B的udprxy,设置端口
8848
。 以成都地区为例,在VLC上就可以通过http://192.168.10.252:8848/rtp/239.93.1.12:2224
观看CCTV-5
的直播了。 - 开启主路由器
xUPNPd
, 然后上传网友分享的m3u节目表单,实现在内网任何设备通过DLNA共享节目列表的功能。
使用感受
- 因为使用路由器B进行了udp转http到内网,因此可以完全在内网关闭组播协议,IPTV融合到内网后,对内网没有任何影响。
- 如果想保留电信的IPTV LAN口,可以把光猫上的LAN口多绑定一个到光猫的IPTV配置上(一般为
_OTHER_B_VID_XX
)。 - 便宜购买的新路由mini价格便宜,但是一个百兆路由。然后,实际测试下来,发现1路1080P占用带宽约 10mbps, cpu占用20%,也就是说同时播放5路1080P已经达到CPU极限,但是带宽仅占用到50mbps。一路4K占用带宽 25mbps, cpu占用 60%, 同样也是先达到CPU极限。完全不用纠结百兆路由带宽的问题。
- 家里宽带使用的是电信。4K信号源还是比较少,目前使用下来CCTV的奥运频道4K无论是内容还是信源都是最值得观看的。
Golang http response 解压缩分析
Golang自带的 http 标准库一直是自己的首选http library,最主要的原因是对标准的细节支持非常到位,兼容性优秀。
前几天遇到一个问题一直没时间处理,晚上花了挺多时间排查,落实到代码上其实也就一行的修改量,但是对于有段时间没有读标准库代码的自己来说倒是值得记录一下。
问题
使用标准库进行 http 请求时,如果在请求 header 中手动设置了 Accept-Encoding
, 那么返回的响应内容看起来是乱码(其实是二进制)。
原因
定位的过程其实挺曲折的,分析抓包的时候对比了多个请求数据,最终才确认是header 中的 Accept-Encoding
导致。而这个细节之前在 http.Transport 的 DisableCompression
字段是有文档说明的:
1 2 3 4 5 6 7 8 9 10 |
// DisableCompression, if true, prevents the Transport from // requesting compression with an "Accept-Encoding: gzip" // request header when the Request contains no existing // Accept-Encoding value. If the Transport requests gzip on // its own and gets a gzipped response, it's transparently // decoded in the Response.Body. However, if the user // explicitly requested gzip it is not automatically // uncompressed. DisableCompression bool |
也就是说:
- 如果设置
DisableCompression
为true
并且请求中没有设置Accept-Encoding
的情况下,那么将发送要求服务器返回非压缩的请求;如果DisableCompression
为false
(默认值), 则会发送允许服务器返回压缩内容的请求,并且会在 transport 层自动把 response 内容解压返回到业务层。这一点很容易理解,屏蔽了解压读取压缩内容细节,使用起来也非常方便。这也是我对golang http 标准库的理解,很多东西都自动处理了。我遇到的问题在于下面说的第二点。 - 如果用户手动在请求中设置了
Accept-Encoding
, 那么标准库会认为业务层是想自己接管 response 内容的处理,因此会返回原始的未解压的内容。也就是上面问题里面提到的乱码数据。
这段话看起来比较绕,我们直接看一下 transport.go 相关代码。
发起请求时,DisableCompression
为 false
且业务层没有设置 Accept-Encoding
的情况下,自动设置header:
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 |
// Ask for a compressed version if the caller didn't set their // own value for Accept-Encoding. We only attempt to // uncompress the gzip stream if we were the layer that // requested it. requestedGzip := false if !pc.t.DisableCompression && req.Header.Get("Accept-Encoding") == "" && req.Header.Get("Range") == "" && req.Method != "HEAD" { // Request gzip only, not deflate. Deflate is ambiguous and // not as universally supported anyway. // See: https://zlib.net/zlib_faq.html#faq39 // // Note that we don't request this for HEAD requests, // due to a bug in nginx: // https://trac.nginx.org/nginx/ticket/358 // https://golang.org/issue/5522 // // We don't request gzip if the request is for a range, since // auto-decoding a portion of a gzipped document will just fail // anyway. See https://golang.org/issue/8923 requestedGzip = true req.extraHeaders().Set("Accept-Encoding", "gzip") } |
读取 response 时,解压内容:
1 2 3 4 5 6 7 8 9 |
resp.Body = body if rc.addedGzip && ascii.EqualFold(resp.Header.Get("Content-Encoding"), "gzip") { resp.Body = &gzipReader{body: body} resp.Header.Del("Content-Encoding") resp.Header.Del("Content-Length") resp.ContentLength = -1 resp.Uncompressed = true } |
小结
很多问题其实都是魔鬼在细节中,本质上还是基础文档没有仔细阅读。不过找问题的晚上其实也挺开心的,因为通过配置好的 github Copilot 来写代码,整个过程真的非常愉悦。最开始自己还会写几个关键字,引导 Copilot 给出提示代码,后面索性回车就开始等待 Copilot 帮我写代码,跟自己编写代码匹配度能到 80% 以上。目前已知的不足是在 string 的内容推导上有时候不太准确(事实上,人工编码的时候,这部分也容易出现typo或者其他错误),因此要注意这部分代码的review.