最近,我的朋友决定踏上了独立开发者的道路,做了一个临时邮箱网站:tempmail100.com。作为一个旁观者,我亲眼见证了他从零开始,一步步摸索,最终取得了广告日入$10的成绩。今天,我想分享一下他的经历,希望能给那些想要踏入独立开发者行业的朋友一些启发。 1. 从想法到落地 朋友的初衷很简单:做一个实用的临时邮箱工具,帮助用户在不暴露真实邮箱的情况下,快速注册一些临时服务。他选择了临时邮箱这个方向,主要是因为市场需求明确,且技术门槛相对较低。 他花了几周时间,从设计到开发,最终上线了tempmail100.com。网站的页面设计非常简洁,用户体验也很流畅,尤其是那个“一键生成邮箱”的功能,简直不要太方便。 2. 技术栈与SEO优化 作为一个独立开发者,朋友在技术选型上非常务实。他选择了轻量级的技术栈,确保网站能够快速响应,并且易于维护。同时,为了提升网站的SEO表现,他做了以下几件事: 外链建设:他主动在一些技术论坛(如V2EX、X等)上分享了自己的项目,吸引了不少开发者点击和讨论。这些外链不仅带来了流量,还提升了网站的权重。 关键词优化:他在网站的标题、描述和内容中巧妙地融入了“临时邮箱”、“免费邮箱生成”等关键词,确保搜索引擎能够更好地抓取和索引。 社交媒体推广:他还在Twitter(X)上分享了一些开发心得和使用技巧,吸引了不少用户前来试用。 3. 支付与变现 为了让网站能够持续运营,朋友决定引入Stripe作为收款工具。为了接入Stripe,他甚至注册了一家美国公司!虽然这个过程有些复杂,但最终他成功接入了Stripe,用户可以通过付费解锁一些高级功能,比如更长的邮箱有效期、自定义邮箱前缀等。虽然目前付费用户还不多,但这为他提供了一个稳定的收入来源。 此外,他还通过Google AdSense在网站上投放广告。随着流量的增加,广告收入也逐渐提升,最近甚至达到了日入$10的成绩。虽然这个数字看起来不大,但对于一个刚刚起步的独立开发者来说,已经是一个不错的开始了。 4. 挑战与未来 当然,独立开发者的道路并不总是一帆风顺。朋友也遇到了一些挑战,比如如何持续吸引新用户、如何应对竞争对手等。但他始终保持乐观,不断优化产品,提升用户体验。 未来,他计划继续完善网站功能,比如增加多语言支持、提升邮箱的稳定性等。同时,他也希望通过更多的SEO优化和社交媒体推广,进一步提升网站的知名度和流量。 5. 给新人的建议 如果你也想要踏入独立开发者的行业,朋友的建议是: 从小项目开始:不要一开始就想着做大而全的产品,选择一个细分市场,解决一个具体的问题。 注重SEO和推广:酒香也怕巷子深,好的产品需要让更多人知道。SEO和社交媒体推广是非常有效的手段。 保持耐心:独立开发是一个长期的过程,不要急于求成。只要坚持,总会有收获。 结语 朋友的经历让我深刻体会到,独立开发者虽然充满挑战,但也充满了可能性。如果你也有类似的想法,不妨大胆尝试,或许下一个成功的独立开发者就是你! 如果你对tempmail100.com感兴趣,或者有任何关于独立开发的问题,欢迎在评论区交流。我们一起探讨,一起进步! 本文链接:https://deepzz.com/post/indie-maker-tempmail100.html,参与评论 »
2025年啦,大家过得都可还好? 时间转瞬即逝,2024年就过完了,不得不感叹一年时间还是蛮短暂的! 回顾2024: 2024骑着第一辆公路车参加了2023的跨年骑行,千人规模很是壮观,以后是再也不会有了。 2024组装了第一台真正意义上的NAS,五盘位,8T,结合Apple TV该享受的还是要享受。 2024学习了swift与swiftui,与朋友空闲时间开发了一款临时邮箱应用,突破了独开零收入门槛。 2024读了6本书,远未达到最初设定的10本书的目标,看来执行力还是差了些。 2024为小朋友组装了一个大玩具:水管攀爬架,挺爱玩,大运动好了不少。 2024总共骑行了400来公里,都没别人跑步跑得多,看来还得找个骑行搭子。 2024开始关注保险,为家人安排了重疾险,寿险。 2024房贷利率降低了不少,还贷压力也有部分减轻,但依旧是个重担。 2024不少地方发生战争,如乌克兰俄罗斯,还有各种事故,只能说珍惜当下,天灾还是人祸不知道哪一个先来。 2024特朗普竞选总统成功,币圈高涨,可惜没能抓住,错失成为首富的机会。 2024陪着我的小家伙又成长了一年,每次看到他的笑容,喊着爸爸,心里都是感动。 2024家里第一次开了地暖,室内外温差十几二十度,家里真的是舒服。 2024奶奶九十大寿,真的是儿孙满堂,实实在在的印证了家有一老,如有一宝。 2024工作平稳,但有个盼头,来年努力为公司输出,一同成长。 2025愿世界和平,愿家人安康,愿陌生的你快乐幸福,愿风调雨顺! 本文链接:https://deepzz.com/post/2024-year-end.html,参与评论 »
该篇总结是我第一次严格意义上的年终总结,也到了必须要进行总结的时候。总结经验,反思过往! 相较于以往,每年的这个时候都会看到很多人在对自己进行总结。对于自己,是一个性格比较随意的一个人,总结不总结的也无所谓,只要内心对自己有要求,有思考就行。但对于很多事和物,没有纸笔的记录很快就会忘却,2023年对于我来说是挺焦虑的一年,希望记录下对这些事情,以作反省。 先说说自己,性格是一个比较独立的一个人,不想去处理那些复杂的人际关系,不喜欢,也是浪费时间(当然这肯定是不对的,大家不要学我)。简简单单的就挺好,没有利益,没有冲突,有兴趣的就一块玩,没有的我们下次找机会。平时个人想法也挺多的,就是执行力需要提高。 从标题可以反映出我2023焦虑的一年,可以从工作上和生活中来进行总结。 工作上 我目前在一家安全相关的公司从事产品研发的工作,公司不大但老板有技术背景,对技术比较看重。当时过去主要工作内容是数字身份产品的研发(以创业的心态),包括IDaaS平台(OAuth2/OIDC,SAML,LDAP等)、无密码身份认证(WebAuthn/FIDO2)、协同签名和S/MIME相关的产品。 由于各种原因吧,如产品方向和研发方案,经过3年的产品研发,最终的产出很少或者说没有应用场景。个人挺苦恼的,毕竟作为研发研发出没有产生价值的产品真的很没有成就感,产品没价值就代表研发没价值。 2023最主要是将我们的产品通过国密L2认证,这也是痛苦的根源,目前也仅通过一半。起初从已通过小伙伴处得知过认证还是比较容易,检测人员有提供部分指导。自从我们从接手项目开始,整个文档或者说产品的方案就不满足检测要求,才知道有多难。整个过程下来文档修改过不下百遍,在一遍遍修改和沟通下才艰难完成部分项目的认证,期间还收到整改通知。 总结下来: 对检测要求和产品设计的不了解(过检产品由他人研发),沟通和功能修改过程也挺难的。 不能汲取公司内部已过产品的经验,该过检产品非标品,有很大不确定性。仅能部分参考。 检测机构为北京国密检测局且今年格外严格。 我认为这个过程对我来说是焦虑的,首先它不是的我强项(产品研发),其次它的认证过程是我不能够把控的。当一件事情不在我的控制范围内的时候,充满了不确定性,就像一个快断线风筝,指不定什么时候就失败了,它就会让我变得焦虑。我的性格不容许有这样的事情发生。 同时,2023年较年初定下的目标完成了不到50%,未能达成年初计划,其中有研发方向的变化,有人员的变化。 不过今年公司产研的变化让我看到希望,从最初无产品经理到目前三名产品经理。现在产品的立项也较之前更严谨,不再是一股脑拍板觉得这个可以做,这个方向我看到了机会,今天行明天不行。我相信在产品经理的介入下,通过调研用数据说话,更多人的参与立项讨论,专业的事交给专业的人,以后会更加规范,做的产品会更有价值。 从根本上来说,利用每个人的长处去做事情一定是事半功倍的,有时候静下心来好好思考,方向比忙得不可开交更加重要,观察与发现比我觉得更合适。2023年工作上的变化就是没有变化,现在看来,除了埋头干活或许有意提升自身的价值更加重要。 生活中 今年生活上最大的变化是我们家里的天使宝宝降临,给我们整个家庭带来了很多变化。 宝宝出生前,家里就开始囤各种婴儿用品,什么婴儿床、婴儿车、小玩具、纸尿裤、抱被什么的,各种各样,以前我们小时候咋就没这么多玩意。同时两边的父母也过来帮着带带孩子煮个饭照顾我们的饮食,不然两个人确实转不过来。 孩子出生后本以为大人可以轻松一点,谁知孩他妈还堵奶,最严重的是最后还发炎了,痛的恼火得很。最后找通乳师通了五六次才消下去(新生孩母亲一定要注意这个问题)。孩子每几个小时就需要喂一次奶,有时候还一直哭闹,这让你睡眠和精力严重不足。但这就是人生需要经历的一件事,孩子有时候一个笑容就会把你治愈。加油吧,少年! 今年给自己找了一个锻炼的理由,一狠心给自己添置了一台公路车,国庆节第一次就去骑行了95公里的绿道,那感觉很酸爽。身边的朋友也一起购置了,以后跟这大神一起拉练。 2024计划 个人比较向往独立开发者,有经验的朋友的可以带带新人。 装了一年多的房子打算2024年初入住。由于房子不是在工作附近,每次装修或安装什么东西就得安排时间,一来二去整个房子很多细节都没有弄好,不过有什么办法只有妥协,下一次不就有经验了嘛。 年前和朋友聊了许久,感觉每个人都有每个人的苦恼,都过得不容易,比如为了下一代怎么怎么的。要我说,有很多苦恼都是自己给自己的压力。只有先把自己安排好,才能安排别人。一辈子很短,快乐向上的活下去! 2024年,给自己立一些Flag: 产品突破0的销售额 - 100% 读10本书,无论什么书 - 60% SwiftUI编程思想 - 100% Swift进阶 - 100% SwiftUI by Tutorials - 100% macOS by Tutorials - 100% 把时间当作朋友 - 100% 三体(全集)- 100% 骑行1000公里。 - 40% 开始运营小红书和Twitter,粉丝数突破5000。 - 10% 2024让自己发生点变化。 2024年目标没有达成,执行力还是差了!! 本文链接:https://deepzz.com/post/2023-year-end.html,参与评论 »
接上篇文章,继续折腾家庭网络:如何玩转智能家居 - 网络如何智能openwrt+v2ray。 更新说明 2023.02.24:R2S不再作为主路由,采用旁路由接入 本篇文章解决问题是:如何通过配置wireguard实现从世界任何地方优雅的访问家庭网络。我们在家庭网络使用场景汇总一定会遇到以下情况,人在外: 想要访问家庭网络中的NAS服务,获取一些资料。 想要下载一部资源,添加下载任务,一回家就可使用 全屋智能家居,某一个设备出现故障,想要直接接入家庭网络查看原因修复 等等… 这些场景的基本需求是:安全性一定要有保障,能够在外网连接到家庭网络(不是简单的内网穿透,而是组成大局域网)。 答案不言而喻,组建vpn。那么问题来了,用什么vpn,如何连接到家庭网络: 没有公网IP,现在IPv4已经耗尽,想要从运营商那获得公网IP还是很困难的,而且IPv6目前还不是很普及。 家庭内部网络安全性脆弱,需要防火墙来保护,一般的不应该将家庭网络中的服务暴露在公网中,这样会有极大的安全隐患。 本篇文章的目的是:通过vpn的形式使得在外网如同访问家庭内部局域网一样,让外部设备与家庭网络中的设备形成一个局域网。 那为什么选择wireguard: 开源:https://github.com/WireGuard。 简单:代码简单,仅4000余行,原理简单,部署简单。 安全:支持最新加密技术,Curve25519、ChaCha20等。 性能:WireGuard 虚拟网由于是100%内核处理,省去了用户进程和内核交互的开销,因此性能优越,具体表现为吞吐量高。 网络架构: 博主目前设计的网络架构如上图,不过本篇文章只讲 光猫ACR2S 这样的链路: 光猫:192.168.1.1 AC(主路由):192.168.2.1 R2S(旁路由):192.168.2.100 这里的R2S我们作为旁路由来使用,什么是旁路由请自行搜索资料。简而言之,家庭内大多数网络均走AC主路由,部分走旁路由(翻墙),旁路由的(折腾)挂掉不会导致整个家里网络中断。 由于博主家庭网络没有公网IP,动态IP也没有,因此这里找了一台云服务器作为中转(非UDP打洞,中转方式会消耗云服务器流量,请悉知)。当然也可以用基于wireguard现成的方案,如tailscale,免费应该够用(有设备限制)。 提前准备 这里假设你已经准备好已经准备好openwrt路由器。我们还需要准备: 一个公网IP:或是家里自带公网IP+DDNS,或是购买一台公网服务器VPS FRP 因为博主家里没有公网IP,那么只能通过这种内网穿透的方式实现,将wireguard的udp端口通过代理的方式暴露到外网,如果有公网IP跳至下一步。 1、在vps上安装frps服务端,请到 frp/releases 下载对应架构的最新版本即可,解压到服务器。 服务端配置: [common] bind_port = 7000 token = 768f72bc664ad2ad7c9edccf65523fd7 其中 token 是用来鉴权客户端的,请重新生成。bind_port 指定 frps 监听的端口,云服务器需要放开该端口的访问。 2、在openwrt安装frpc客户端,请到 System->Software 搜索 frpc 安装。 完成后进行配置:假设这里我们选择端口 51820/udp 作为wireguard的通信端口,IP 10.0.0.1作为wireguard的网关IP。 我这边的 /etc/config/frpc 配置如下,你可对应着在网页上进行修改: config init option stdout '1' option stderr '1' option respawn '1' option user 'root' option group 'root' config conf 'common' option server_port '7000' option tls_enable 'false' option server_addr '' option token '768f72bc664ad2ad7c9edccf65523fd7' config conf 'ssh' option name 'wireguard' option type 'udp' option use_encryption 'false' option use_compression 'false' option local_ip '10.0.0.1' option local_port '51820' option remote_port '51820' 记得将 server_ip 替换成frps的地址,如果不出意外的,openwrt中的frpc已经跑起来了。 WireGuard WireGuard是点对点的网络,每个节点既可以做服务器,又可以做客户端。我们这里将部署在openwrt中的wireguard节点称为服务端,在手机或电脑端部署的wireguard节点称为客户端。更多安装方式:https://www.wireguard.com/install/。 整体步骤如下: LAN口设置(旁路由) 安装wireguard LAN口设置 1、编辑LAN口,设置上游网关信息: 2、配置DNS: 3、关闭SYN-flood保护,开启动态IP伪装 安装wireguard 1、在openwrt中安装wireguard建议通过System->Software进行安装: 安装完成,重启路由器。同时你也会看到 Status->WireGuard: 配置服务端 需要了解细节的朋友可以参考官方 Quick Start,我们这里通过openwrt进行wireguard服务端配置。 1、点击 Network->Interface 中的 Add new interface 创建接口: 2、接口名称建议 wg0,协议选择 WireGuard VPN 进行创建: 3、一般设置: Private Key 是WireGuard节点的私钥,可通过 Generate Key Pair 生成,也可手动生成: # 通过ssh登录到openwrt,执行 $ wg genkey | tee privatekey | wg pubkey > publickey $ cat privatekey publickey Listen Port 监听的端口,这里设置为 51820 与 frpc 配置对应即可。 IP Addresses 是WireGuard接口的私有IP网段,这里设置为 10.0.0.1/24 与 frpc 配置对应,不要与其它内网IP段冲突。 4、防火墙设置,有 vpn 选 vpn,无则选 lan: 最后 Save,保存即可,后续再步骤添加 Peers。 客户端配置 首先,通过这里的安装方式进行客户端的安装:https://www.wireguard.com/install/,我这里以iOS举例。 1、点击 创建隧道->手动创建: 2、填写基本信息,主要填写下面三项,其它默认: 名称,这里随便填 公私钥,这里点击 生成密钥对 随机生成即可。 局域网IP地址,即客户端节点IP,这个地址要设置为符合 10.0.0.1/24。 3、通过 添加节点 添加服务器信息: 公钥,填写服务端的公钥。 对端,服务端地址+端口,由于家里没有公网,这里走的frp,所以填的是公网vps代理地址。 路由的IP地址(段),这里填写需要走wireguard网络的IP段,如:192.168.100.0/24,192.168.2.0/0,10.0.0.0/0。这样当开启wireguard vpn之后访问这些IP就会走vpn网络。 到了这一步,客户端已经完成配置了。但是服务端还没有,因为wireguard本质上是一个P2P通信的软件,我们还要将客户端公钥信息配置到服务端才行。 配置Peer 在openwrt配置wireguard peer信息。 填写刚才配置的客户端信息,Save 保存即可: 现在在手机上开启 vpn,尝试访问一下路由器地址 192.168.100.1 是否可行。 FAQ 1、客户端如何访问openwrt的下一级路由的服务,如这里如何访问 192.168.2.x 服务: 因为路由器作为openwrt的下一级路由,openwrt是不知道如何到达 192.168.2.x 这个网段的。因此需要添加一条静态路由: 2、如何所有流量都走wireguard,并且可以通过之前设置的v2ray进行科学上网: 请参考:https://www.wireguard.com/netns/#routing-all-your-traffic。 3、不用开启端口吗,很多教程上面需要开启路由器端口? 开启端口,一般是指我们路由器有公网IP,通过ddns方式对外暴露端口的时候需要开启。 相关地址 [1] https://github.com/WireGuard [2] https://www.wireguardconfig.com/ 本文链接:https://deepzz.com/post/openwrt-and-wireguard-connect-homenet-anywhere.html,参与评论 »
接上篇:如何玩转智能家居 - 家庭组网方案选择。 当我们已经选择好家庭组网方案之后:AC(PoE)+AP。那么问题来了:实际该如何操作,如何布线,需要考虑什么? 就我来说,可能考虑这些因素: 弱电箱位置,要不要挪动,什么位置才是最佳 强弱电的走向,如何才不会被影响 我们的AC应该放在哪里,AC为什么需要POE 我们的AP应该放在哪里,要放几个 到底需不需要弄一个类似家庭机房的东西,放哪,有什么用 网线怎么弄,什么地方要放几根,用几类网线 普通家庭上全屋光纤有用吗 确定需求 确定需求前,我们确定下当下的环境: 电信宽带目前只能达到千兆 大多数家庭以WiFi上网为主,台式机上网较少 不考虑全屋光纤,目前成本较高(土豪请忽略),因为直接支持光纤设备极少,需要光电转换 全屋必须网络全覆盖,必须实现网络无缝漫游 以上,我们得出:全屋采用超六类网线即可满足未来需求,超六类网线可以达到万兆网络,往后看还是能够满足的。全屋主要是以WiFi为主,那么部署AP时,如果不需要网线则不牵网线,节省成本。 我们还有有哪些需求需要解决呢,确定以下信息: 是否需要IPTV:需要,至少客厅需要一台电视 哪些设备需要网线:电视,台式电脑,Xbox,AP 是否需要科学上网:需要,长期会查询外文资料 是否使用NAS:需要,NAS可以帮助存储照片,视频,音乐等,能够称为智能家居的后端存储 是否需要万兆网络:需要,内网一定要快,至少保证访问NAS的速度,剪辑视频需要 是否需要内网穿透:需要,我需要在外控制家里设备 预算有多少,4k+ 以上,我们得出: 客厅至少需要三根网线:电视或电视盒子,Xbox游戏机,AP 书房至少需要三根网线:台式电脑,NAS,AP 每个卧室至少需要一根网线:AP或作备用 需要一个万兆交换机,实现内网有线万兆 软路由来一套,自由上网用 向电信要IPv4或外网服务器来一套 那么综合以上信息,我确定我是需要一个机柜的:将所有的网络硬件都放在一个位置,把NAS也放过去,最后将该机柜放到定制柜里完美隐藏。 设计方案 接下来是设计布网方案,这需要实际根据户型图来操作。大家可以参考我的这张设计图: 基本思路就是有一个总的网络机柜,网络从弱电箱出来直接到网络机柜,后续的网络全部走机柜出去。由于博主要实现内网穿透,故画了阿里云上去,如果你们不需要可不参考(也可以走wireguard/tailscale打洞方案)。 以上图仅是设计参考用,实际在实施过程中有些许变化。然后就有了这张图: 这张图AP酌情减少了一个,具体根据实际情况来定。 当你确定好方案之后,一定要第一时间与电工师傅进行核对,确认方案的可行性。博主就是这样,想当然的将弱电箱挪到鞋柜处,所有网络设备都放到鞋柜,谁知强电与弱电在一个垂直面上,不行。所以改到了餐柜处,当时也是打得我措手不及,方案是调整了又调整。 选定设备 基于以上种种结论,可以开始选择设备了。 机柜 首先,选机柜。tb上的机柜也有现成的,而且非常结实,有兴趣的朋友可以看这里:简易开放式机柜 但是,自购机柜的话需要考虑机柜放置位置。比如放鞋柜,那么尺寸一定要合适。还有就是机柜出最好叫木工师傅帮助打一个散热孔。 现成的对于我来说不够折腾,本着极客的原则,博主开始学习网友的购买零件自己组装机柜。具体可参考这里: 从知乎上看到第一眼就爱上这个配置,和我的计划也非常符合。于是我也采购了一套,不过散热还是比较贵的。等房子装修好了晒图。 选AC+AP 由于是面板AP,选择面就比较少了。最开始相中了华为的H6,不过这玩意是贴上去的,不够美观。于是叫朋友推荐了一套TP-Link的套装,省事也省心。 TP-LINK TL-R4010GP-AC PoE ACTP TP-LINK TL-XAP3002GI-PoE 这两款设备,我是在某鱼上购买的全新,应该不会翻车,有需求的朋友可以去某鱼上搜索下。 这款AC是集PoE交换机一体的: 双WAN口接入 9个千兆网口,2WAN+8LAN 内置无线控制器,可统一管理TP-LINK AP产品 所有LAN口支持标准PoE供电,整机输出功率高达120W IPSec/PPTP/L2TP VPN,远程通信更安全 接入认证(Web认证、短信认证、PPPoE服务器) 上网行为管理(移动APP管控/桌面应用管控/网站过滤/网页安全) 这款AP是支持Wi-Fi6: 新一代Wi-Fi 6 (802.11ax)技术 11AX 2.4G/5G 双频并发,无线速率高达2976Mbps 国标86壳体,外观优雅,出墙厚度仅9.4mm 802.3af/at标准PoE网线供电,无需外接电源 支持TP-LINK易展功能,简单按下按键,即可供TP-LINK易展设备无线接入 频谱导航,引导双频用户优先连接5GHz频段,使2.4GHz 和5GHz 两个频段负载均衡,保障网络性能 FIT模式支持自动射频调优 FIT模式支持智能漫游 支持弱信号设备剔除、禁止弱信号设备接入功能 胖瘦一体,可以根据不同应用环境选择不同模式 支持TP-LINK 商用网络云平台集中管理 支持TP-LINK 商云APP远程查看/管理 整机尺寸(mm):88*86*45.4,出墙厚度9.4mm 看中这款AP的原因也是它的出墙厚度仅9.4mm,与普通插座面板差不多的高度。实现了完美的隐藏。 参考 https://www.zhihu.com/question/26596786/answer/279309998 本文链接:https://deepzz.com/post/smart-home-networking-design.html,参与评论 »
为什么会有家庭组网这个说法?其根本原因是想要实现全屋网络覆盖。结合时下流行的智能家居,相辅相成。 目前,大多数家庭的网络仅靠那么一个路由器支撑,但路由器的网络覆盖范围往往都会有局限,就比如信号穿堵墙就会有很大的衰减,家里的角落甚至有时搜索不到信号。那么一般我们的解决方案有哪些呢? 换一个更好更强的路由器,天线更多,窗墙能力更强,4根的,6根的,越多越强 使用WiFi放大器,将信号进行增强。一个不够用两个,两个不够用多个 路由器桥接模式,当前路由器信号不好的时候切换成信号好的 家庭组网方案:电力猫、有线/无线Mesh、AC(POE)+AP 前面三种有各种各样的缺点,比如别墅这种大空间用一个再NB的路由器也是覆盖不全的;WiFi放大器这种东西网速会有损耗,稳定性得不到保证;路由器桥接,由于SSID不同,设备不会自动切换路由器,只能手动切换,不方便。因此,前三种方案均不推荐。 我们这里着重说家庭组网方案。这几种方案都能解决全屋网络覆盖的问题,具体选择哪种需根据情况来决定。 电力猫 电力猫用的传输技术是正交频分复用(OFDM),简单点说就是把互联网信号和电力信号叠加到一起。 我们平时用的电都是交流电,频率是50HZ,电力猫就是在交流电的基础上叠加上互联网的信号。因为互联网的信号频率都很高(10MHz甚至更高)所以如果采用合适的手段,是不会对电力传输产生影响的。 不过要注意,电力猫有以下缺点: 必须在同一电表下,不在同一电表下将不会进行数据传输 如果电力线采用三相供电设计,电力猫的作用就会受到严重限制,速度会降低 品质低下的电力猫存在散热和噪音问题 电力猫的稳定性容易受到滤波产品、充电器、大功率电器的干扰 一般的,如果家里现有的网络布线中没有预埋网线且不能够增加网线的情况下建议采用这种方式组网。 无线/有线Mesh 什么Mesh组网?又称网格网络,即家里的网络通过节点组建的方式实现网格化,网络中所有的节点都互相连接,并且每一个节点至少连接其他两个节点,所有的节点之间形成一个整体的网络。 整个Mesh网络中有一个主节点,主节点用来进行节点信息的同步。 组网后,会生成一种网状网络,不同接入点可以以星状、树状、串联和总线方式,混合组网。在这个网络中,SSID统一,无线设备还可以自由寻找信号最好的节点去连接传输数据,用户手持设备在不同节点间,穿梭时无线网络是无缝切换的,实现较好的漫游效果,漫游过程中,数据丢包,延时,抖动越低,网络质量越好。 Mesh组网分无线组网和有线组网(有线/无线是指Mesh网络中节点的连接方式): 1、无线组网 优点是可以不受空间的限制,可以在需要增加节点的时候随意增加,实现家庭的网络全覆盖。 缺点也是有的,就比如说所有节点均需要的一个供电插座,摆放位置,对于强迫症患者来说是致命的。 2、有线组网 优点是可以通过主路由POE供电方式(POE供电是通过网线为节点供电)和面板子路由形式进行隐藏式安装,美观,比如华为H6。 缺点是后期不能够随意增加节点,因为网口的位置和个数都是固定,如果前期没有提前规划好就会比较痛苦。 AC(POE)+AP AC是指无线控制器(Wireless AccessPoint Controller),是一种网络设备,用来集中化控制局域网内可控的无线AP,是一个无线网络的核心,负责管理无线网络中的所有无线AP,对AP管理包括:下发配置、修改相关配置参数、射频智能管理、接入安全控制等。 AP即无线访问接入点(Wireless AccessPoint),传统有线网络中的HUB。AP相当于一个连接有线网和无线网的桥梁,其主要作用是将各个无线网络客户端连接到一起,然后将无线网络接入以太网,从而达到网络无线覆盖的目的。 目前该组网方式最常用于企业等大型对网络要求较高的商业场所,该组网方式稳定,简单。目前也用于对网络要求较高的家庭组网中。我们这里聊一聊AC+AP组网的特点。 由于AC+AP组网方式中,一般为有线组网,我们这里也只讨论该组网形式。 1、对于AC 常接触到关键词是POE(Power Over Ethernet),一般还是建议采用POE为AP供电的方式,这样更加方便。 2、对于AP 分为面板AP和吸顶AP。面板AP是一种隐藏式安装形式,这种必须要使用POE供电方式才行,非常美观,但覆盖范围相较于吸顶AP会稍微逊色,100平建议2-3个。吸顶AP安装在吊顶处,覆盖范围较广,100平安装1-2个就好。 由于目前我家是新装修,所以博主采用:AC(POE)+AP: TP-LINK TL-R4010GP-AC PoE ACTP TP-LINK TL-XAP3002GI-PoE 啊,之前买的 479GPE 不能放进弱电箱。重新买了个4010GP,它有磁铁可以直接粘在弱电箱上。 FTTR FTTR(Fiber To The Room)即光纤到室的说法。FTTR组网方案采用了一个主光猫,多个从光猫的方式进行网络组网,光猫与光猫之间通过光纤进行连接,由于光纤比较细小,即使显示布线也不会太影响美观: 有兴趣的朋友可以去了解下。我认为FTTR是未来十年发展的一种趋势,但现其高昂的布设成本劝退了我,待后期方案成熟,将网线替换成光纤也是可行的。 下文将介绍博主具体组网方案和思考。 参考文档 [1] https://wenku.baidu.com/view/b001ec4f33687e21af45a953.html https://www.zhihu.com/question/410166038/answer/1989571138 https://zhuanlan.zhihu.com/p/296788149 本文链接:https://deepzz.com/post/home-networking.html,参与评论 »
作为玩转智能家居的第一篇,今天我们了解一下智能家居中的灯光控制系统。 最近准备装修房子,作为互联网从业者一直对科技产品有浓厚兴趣,喜欢折腾。所以新家最好有一个专用家庭机柜,软路由、万兆交换机、AC+CP,全屋光纤,都给它搞上。最重要的还是要有套全屋智能家居,进行自动化场景控制。家庭组网及全屋智能我会在这个系列一一实践,总结经验为大家踩坑,哈哈。 目前来说想要实现全屋智能,两种方式: 全屋智能方案提供商,你只需要提要求,方案和实施均交给他们(省心但价格较贵)。 自己折腾,动手设计,这需要有一定的折腾能力和专业知识(心累)。 开始全屋智能系列之初,你首先要确定的是选择一个全屋智能生态,选生态涉及到你能够搭配智能设备的丰富程度。比如你选了苹果生态,但是家里的灯光是不支持 HomeKit 控制的,这就陷入两难,就很烦(当然这也是有解决办法的,后续文章说到)。 米家生态,有完善的智能家居生态链,配套完整,且设备性价比高(推荐) 华为生态,HarmonyOS支持,目前主要由官方提出的解决方案为主,其它配套较少 苹果生态,HomeKit支持,结合 iPhone 体验超棒,但国内支持产品较少(备选) Matter生态,由全球大厂组成的IoT物联网联盟,实现厂厂互联,不过目前还在起步阶段 其它生态,老牌家电厂商也在推自己的智能家居系统与玩法,不过感觉是和自己玩,个人认为智能家居最终还是会实现统一或互连。我这里选择米家生态作为全屋智能解决方案,自己折腾。后续结合 HomeAssistant 上苹果生态,体验应该是非常棒。 本篇文章作为装修系列中的灯光控制篇。 基础知识 先来普及下电相关的知识。电线分(GB/T6995-2008): 地线(E),黄绿色(双色线),用于有金属外壳电器,防止触电,直接接地,三孔插座会用到。 零线(N),蓝色或绿色、黑色 火线(L),红色或棕色,火线与地线,零线电压差220V。开关接在火线上。L1灯连接线。 我们一般就会接触到零线与火线。 开关 在智能设备出现之前,就一种开关:机械开关。智能设备出现之后,为了让原来不智能的设备变智能,于是智能开关出现了。这里普及一下开关的种类。 开关又分几开几控,几开即该开关面板上有几个开关控制按钮,几控即该开关上有几个接触点,如下图: 开关往上拨会接通一根线,往下拨又会接通一更线。常见的应用就是,一个灯能够被多个开关控制。如一般卧室就是双控。 理解了开关的说法,我们来讲一讲典型的开关案例: 1、单火开关,即普通开关,这是最常见的开关。开关里只有两根线,一根进,一根出。 2、零火开关,即智能开关(忽略普通开关),智能开关是需要一直通电的,因此需要三根线,两进,一出。 3、凌动开关,也是普通机械开关,开关通过弹簧实现回弹(单向按键),使得灯一直通电。因此它需要搭配凌动灯来使用(关键还是在灯),该开关通过对开和关的电脉冲算法实现灯的亮灭。 目前市面上还有一种单火智能开关,同样是智能开关那么必须一直通电在线。它是如何实现的呢?它是通过调节“可变电阻”实现“灯泡”的开启和关闭的或者内置电池(总之需要使得开关处于在线状态),我认为这只是一种妥协的做法。还有就是无线智能开关,原理上均类似。 以上说的开关均是单键开关,多控开关线路会相应增加。 灯 1、普通灯,不做过多描述 2、智能灯,可以通过手机等智能设备和开关控制的灯。 3、凌动灯,支持凌动功能的灯 好了,现在已经基本了解了灯控相关的知识啦,我们来说一说实际的方案。 方案 总的来说,智能灯控就是开关、灯的组合: 智能灯+智能开关,不推荐。容易出现冲突,智能开关控制开,智能灯控制关,到底开还是关傻傻分不清楚。 智能灯+普通开关,不推荐。开关得一直在开启状态,通过智能手机控制,容易出现开关断掉设备不在线状态。 普通灯+智能开关,次选 普通灯(改凌动)+凌动开关,次选 凌动灯+凌动开关,首选 可以看到,博主中意的还是凌动方案。凌动方案既可以满足现房智能改造,也可以满足新房装修线路造价问题,同时还满足对智能不感冒的老年人使用习惯。如果家里没有老人,推荐直接不设计开关,绝对给力。 普通灯+智能开关 由于是智能开关,因此开关盒里有必要再加一根零线。如果你的家已经装修好请确认是否预留零线。 当然如果你基于很多因素考虑,这种方案最适合你的家庭,在没有零线预留的情况你可以采用单火智能开关也是一种不错的选择。不过这种方案可能对灯有一定的需求,比如日光灯就不可以。 不过目前这种方案有两个缺点: 实现双控有难度,即在门口开灯,在床头关灯。 布线成本增加(需要增加零线) 普通灯(改凌动)+凌动开关 结论,这种方案可以和凌动灯方案实现全屋凌动统一方案。这种方案推荐不想过多增加成本,现有灯具不换情况下的用户使用。 这里涉及两个地方的改造。 1、改造灯 凌动开关只是一个普通机械开关,重点还是在控制灯的组件上。该方案需要一个叫灵动通断器的东西,这个不是凌动官方出的,是民间根据凌动方案适配的。所以对此介意的同学可以绕道了。 它的原理就是,将凌动灯拆分成两块:普通灯+灵动通断器。一些灵动通断器的功能还是很多的,如蓝牙,蓝牙Mesh,还有开关随意贴可用。 还有个选择就是,将现有灯换成凌动灯。据我观察,现在支持凌动功能的灯还是蛮多的,如客餐厅吸顶灯,卧室吸顶灯,吊灯等。 2、普通开关改凌动开关 如果家里开关是普通开关,可以通过添加一个弹簧的形式改成凌动开关。普通开关改凌动开关相关可以到某宝搜索,不过推荐购买原装凌动开关更安全稳定。 凌动灯+凌动开关 原生支持凌动,如果家里有条件完全可以采用这样方案。上面所有方案的缺点,都完美的解决了。不管你是正在装修还是已经装修好,完全不需要做任何特殊线路改动(原普通灯光线路),换上凌动开关和凌动灯就可以了。 博主目前采用: 普通灯(改凌动)+凌动开关 凌动灯+凌动开关 两种方案结合的形式,毕竟不是所有灯都支持凌动功能,你喜欢的灯不一定都支持凌动功能。 最后,其实所有方案都不是固定的,当你对智能灯控了解到一定程度之后,你可以有自己的方案,组合和场景是多种多样的。 本文链接:https://deepzz.com/post/smart-home-lighting-control-scheme.html,参与评论 »
接上篇文章:如何玩转HomeAssistant - HA介绍。 如何安装 HA,上篇文章说到了一些,本篇文章实战安装 HA,并介绍 HA 内部概念。通过两种方式安装: 镜像安装 Docker安装 如果大家有更多需求,可以到官方站点查看:Installation。 镜像运行 镜像安装,相当于安装一个操作系统,我们需要提前准备好虚拟机或者一个小型的设备(如树莓派或NUC等等)。 我这里准备的是 VirtualBox Linux 环境来进行安装 HassOS,镜像下载地址在 Github 上:home-assistant/operating-system。 1. Create a new virtual machine 2. Select “Other Linux (64Bit) 3. Select “Use an existing virtual hard disk file”, select the VDI file from above 4. Edit the “Settings” of the VM and go “System” then Motherboard and Enable EFI 5. Then “Network” “Adapter 1” Bridged and your adapter. Docker运行 容器运行对于了解过的人应该是比较喜欢的一种方式。我们这是使用 docker 做演示。 首先,我们得有一个安装 docker 环境的机器:docker安装,选择一个合适的 HA 镜像版本。运行方式很简单: docker run --init -d \ --name homeassistant \ --restart=unless-stopped \ -e TZ=Asia/Shanghai \ -v /PATH_TO_YOUR_CONFIG:/config \ --network=host \ homeassistant/home-assistant:stable 替换 PATH_TO_YOUR_CONFIG 为本地配置路径,容器运行起来之后会监听 :8123 端口,启动过程可能会耗点时间。通过访问 http://:8123 即可。 HA 内在 HA 初次接触是一个新鲜事物,它的内部有很多概念,如集成、设备、服务和实体等。提前理解这些概念有助于我们快速上手,玩转 HA。 设备和服务 集成是 HA 中重要的概念,串联着整个系统。那么什么是集成? 集成可以说集成设备,集成服务,或者说设备和服务是集成的一种抽象体现。而实体(Entity)是通过集成(如 light,switch 等)进行标准化的(如小米灯通过 light集成 集成了 Entity)。标准化实体附加了用于控制的服务。 实体将 HA 的内部工作抽象化。作为集成商,不必担心服务或状态机的工作方式。相反,可以扩展实体类并为要集成的设备类型实现必要的属性和方法。 Device Integration(ie. hue)将使用此配置来建立与设备/服务的连接。它将转发 Config Entry(传统使用发现助手)以在其各自的集成(light,switch)中设置实体。device Integration 还可以为未标准化的事物注册自己的 Services。这些服务在集成的域下发布 hue.activate_scene。 Entity Integration(i.e. light)负责定义抽象实体类和服务来控制实体。 Entity Component 帮助程序负责将配置分发到平台,转发发现并收集用于服务调用的实体以。 Entity Platform 帮助程序管理该平台的所有实体,并在必要时轮询它们以获取更新。添加实体时,entity platform 负责将实体注册到设备和实体注册表中。 Integration Platform(i.e. hue.light)使用配置来查询外部设备/服务,并创建要添加的实体。integration platform 还可以注册实体服务。这些服务将在设备集成的所有实体上进行实体集成(即所有 Hue light 实体)。这些服务在设备集成域下发布。 1、实体与 HA Core 交互 从实体基类集成的集成实体类负责获取数据并处理服务调用。如果禁用了轮询,则它还负责告知 HA 数据何时可用。 实体基类(由实体集成定义)负责格式化数据并将其写入状态机。 实体注册表将为 unavailable 当前未由实体对象支持的任何注册实体写入状态。 2、实体数据层次 删除,禁用或重新启用任何对象,下面的所有对象都将进行相应调整。 什么是实体 什么是实体,实体注册表(Entity Registry)?我认为是 HA 中智能设备所能划分的最小单元,也可以理解为控制单元,如空气净化器中的温度传感器上报视作一个实体。 每个实体均有 Unique ID,该ID不能被用户更改,否则造成数据不一致情况。如果一个设备只有一个ID,但提供多个实体,我们可以这样标识 {unique_id}-{sensor_type}。 切记 Unique ID 必须全局唯一,且不可变,一般用 MAC 地址。 什么是设备 什么是设备,设备注册表(Device Registry)?HA 中的设备代表具有自己的控制单元的物理设备,它位于一个特定的地理区域,通常由一个或多个实体表示。举个例子,一台空气净化器是一台设备,它所包含的温度、湿度和PM2.5传感器(控制单元)所暴露的我们可以认为是实体。 但是一个实体(如温度传感器)如果拆解出来,也可以是一个独立的设备,这里更多的其实就是一个从属关系的划分(设备可以视作实体,实体可以视作设备),具体的实体或设备的划分自行考虑。配置实体、实体、设备之间的关系如下图: Config Entry 配置了指定的 Entry,该 Entry 可能关联着某个 Device。一个设备通常有如下属性: 属性 描述 id HA 生成的唯一ID name 设备的名称 connections connetion_type, connection_identifier的集合 identifiers 标识符集合,外界的设备识别号,如序列号 manufacturer 设备制造商 model 设备模型 suggested_area 建议设备区域 config_entries 联接该设备的实体 sw_version 设备防火墙版本 via_device 设备与 HA 之间路由消息的设备标识符 area_id 区域ID entry_type 实体类型,None 或者 “service” 通过设备注册表来进行管理。 什么是区域 什么是区域,区域注册表(Area Registry)?区域应该是最好理解的,它用来定义区域,如客厅,卧室A,厨房等,代表了一个具体的物理位置,它可以帮助我们归集和标记设备的具体区域。 通过区域注册表来进行管理。 什么是Blueprint 蓝图,是可重复使用的自动化,可以轻松共享。您可以从 Github 和社区论坛导入其他用户的 Blueprint。 什么是自动化 其描述很清晰,为智能家居指定自动化规则。即在什么情况下想要使得智能家居做出什么样的反应。 什么是场景 定格一组设备的状态,日后即可一键恢复。也就是说在实际使用过程中,你可能有一个固定的场景或者模式,比如家庭影院。 什么是脚本 执行一系列动作,相当于指定流水线。可以自行考虑考虑 什么是lovelace 相当于是一个主题,你可以在这个主题上做自己的定制。 HA 初识 当 HA 运行起来之后,你可以通过 :8123 进行访问,默认会根据浏览器进行对应语言的显示(如中文),通过一些简单的配置就可以开启我们的智能家居之旅了。 首先,需要我们初始化一个账户,也就是管理员账户,《创建账户》。 然后,设置 HA 的名称,选择时区和单位等,至于定位可能不准需要自己选择,《下一步》。 然后,HA 会自动发现你网路中的相关设备和服务,如果现在不是很清楚可以直接跳过,《完成》。 现在,我们进入 HA 首页,你的折腾开始了: 概览,也就是仪表盘,我们后续会将我们的智能设备通过卡片的形式添加到这里。可以在这里直接控制设备和查看设备状态。 地图,能够显示相关位置信息,如人员、家的定位等 日志,记录 HA 中发生相关事件 历史,查看历史的事件信息 媒体浏览器,查看一些硬件设备上的媒体 开发者工具,可以帮助我们做一些调试动作 配置,我们使用最多的,如集成、自动化等,和配置文件关系密切 通知,一些通知,告警等 人员,个人设置,如密码修改,主题等 配置文件 HA 的运行离不开配置,现在有两种配置方式,一是通过 HA 网页 UI 进行配置(需要相应的集成适配),二是通过 HA 配置文件进行配置。这里我们直接介绍配置文件: ├── automations.yaml # 自动化 ├── blueprints # 蓝图 │ └── automation │ └── homeassistant │ ├── motion_light.yaml │ └── notify_leaving_zone.yaml ├── configuration.yaml # 主配置文件 ├── deps # 相关依赖 ├── groups.yaml # 分组 ├── home-assistant_v2.db # sqlite3数据库 ├── scenes.yaml # 场景 ├── scripts.yaml # 脚本 ├── secrets.yaml # 密钥 └── tts # 文字转语音记录 默认情况下 HA 会自动创建上面的配置文件。具体的使用方式在后续的实践过程我们一一熟悉。 但想要玩转 HA 我们还会接触到更多的配置,如用来保存第三方开发组件的 custom_components 文件夹等(HA 会默认加载)。当然为了方便管理,我们也可以自己组织文件夹的组成。然后在 configuration.yaml 进行启用: # Configure a default setup of Home Assistant (frontend, api, etc) default_config: # Text to speech tts: - platform: google_translate group: !include groups.yaml automation: !include automations.yaml script: !include scripts.yaml scene: !include scenes.yaml !include 用来指定该字段内容的外部文件,而 !include_dir_merge_named 则用来指定文件夹。 之后的文章,我们通过一个个实战来熟悉 HA 的配置和使用,玩转 HA。 本文链接:https://deepzz.com/post/homeassistant-concept.html,参与评论 »
HomeAssistant是什么 HomeAssistant,简称 HA,是一款基于 Python 的智能家居开源系统,支持众多品牌的智能家居设备,可以轻松实现设备的语音控制、自动化等。 初次接触 HA 是很早之前,当时被那种万物互联所震撼,智能之家的想法吸引。随着自己的成长,有空间去做这些事。也是时候进入 HA 的世界了,为后面的物联网之家打下基础。 推荐学习站点: Home Assistant,HA 的官方网站 瀚思彼岸,国内 HA 最热门的论坛 HA 的安装 初次接触 HA,卧槽,根本不知道如何下手。那我们还是从相关概念开始。 HA 的安装方式有很多,大家可以参考这里:installation。简单说 HA 的安装方式有4种: HA Operating System,又称 HassOS,提供了一个支持多平台的最小化的 HA 操作系统,底层基于 Docker 容器引擎,通过容器化部署的 Supervisor 容器化控制 HA Core 及相关应用。由于是 OS,因此需要独立设备或者虚拟机进行安装。官方推荐 HA Container,基于容器技术独立安装 HA Core,如 Docker。免去了各种环境的不统一困扰。需要在支持容器化技术的系统上使用。 HA Supervised,前身 Hass.io,是一个基于容器化的系统,通过容器化部署的 Supervisor 容器化控制 HA Core 及相关应用。HA 提供了 手动安装 脚本。较于 HA Operating System 最大的区别就是可以安装在已有的操作系统上,其它都一样。 HA Core,手动安装在 Python 虚拟环境中,纯手动,可能需要一定的折腾能力。 官方建议使用专用干净系统来运行 HA。即用一台独立的硬件来装 HassOS,如果不知道怎么选择,可以使用 Raspberry Pi 和 HassOS。如果有现有系统且支持 Docker,用 HA Container 也不错,如 NAS。 HA 的衍生 围绕着 HA,衍生了很多插件、系统,各种名词,眼花缭乱。新人接触,很容易被整懵,不适合新人学习。这里我把我所接触到的关于 HA 的衍生介绍给大家,希望大家尽快入门。 0、Hass 是什么? HA 机制。 1、Hassbian 是什么? 具体的系统可以到这里:操作系统。 2、HassIO (hass.io)是什么? 操作系统)。 架构是这样的: HassIO 通过 docker 将 HA 部署起来,管理着 HA,并通过 HA 的 API 进行有效交互。同时提供了一套插件标准 Add-ons,该标准规范了 HassIO 的容器编写规范。 3、HomeBridge 是什么? HA 与 HB 是两个完全独立的系统,各自可以独立运行。HB 的主要目的是桥接不支持 HomeKit 的智能设备,桥接完成后我们可以通过 iOS 家庭 进行管理。或者接入 HA 自动化管理。 目前 HA 已经内置了 Bridge,通过 配置 -> 集成 -> 添加集成 -> 搜索HomeKit 即可。不过据说,HB 可以支持更多的智能设备,且它的 UI 做得比较棒。当然两者都可以自行定制 UI,不过得自我折腾。 4、HACS(Home Assistant Community Store) 是什么? 5、Node-RED 是什么? 它提供了一个基于浏览器的编辑器,可以轻松地使用面板中广泛的节点连接流,这些节点可以在一次单击中部署到运行时。 总之想要将这些东西玩起来还是需要费一定功夫的,如果没有人指导个人觉得比较难入门。 HA 的机制 HA 是运行在 Python3 环境中的,核心进程是:Core,目前版本通过日期形式定义:最新版本。 如果按照分层概念来看的话,可以分为三层:操作系统、运行环境、Hass。 而 HA 又主要分为三大块:HA Core、 Integration 和配置文件 configuration.yml,它们组成了完整的 HA。 1、HA Core 是整个系统的大脑,相当于操作系统的内核,用来决策和调度相关资源。它有四个核心部分: 事件总线(Event Bug),HA 的心脏,负责监听和触发事件 状态机(State Machine),跟踪事物的状态,并 state_changed 在状态更改后触发事件 服务注册表(Service Registry),在事件总线上侦听 call_service 事件,并允许其他代码注册服务 计时器(Timer),time_changed 每1秒在事件总线上发送一个事件 2、Integration 集成,Core 作为大脑,总得有可利用的手脚吧,集成就是如此。 HA Core 通过集成进行扩展。每个集成都负责 HA 中的特定域。集成可以侦听或触发事件,提供服务并维护状态。集成由组件(基本逻辑)和平台(与其它集成集成的位)组成。集成是用 Python 编写的,可以实现 Python 能够提供的所有优势。开箱即用的 HA 提供了许多 内置集成。 HA 中定义了4种集成类型: 定义物联网域,该集成定义了物联网设备的 特定类别,如灯 light,它定义了什么数据和格式在 HA 可以使用。另外已定义的 域列表。如果要建立一个新的域,还可以在 这里讨论。 定义与外部设备和服务互动,该集成定义了在 HA 中如何通过已定义的《物联网域》与外部设备和服务进行交互,如灯 light,一个例子是 Philips 灯可以作为灯的实体在 HA 进行交互,更多信息:实体体系结构 表示虚拟/计算数据点,该集成表示基于虚拟数据的实体。如 input_booleanIntegration,即虚拟交换机。或者通过 HA 中其它数据做处理输出,如 templateIntegration 和 utility_meterIntegration。 由用户或响应事件触发的动作,该集成提供了家庭自动化逻辑的小片段,可在您的房屋中执行常见任务。最受欢迎的一种是 automation集成,它允许用户通过配置格式创建自动化。如 fluxIntegration,它可以根据太阳落山来控制灯光。 3、configuration.yml 配置文件是用来帮助我们完成上述功能的,当然也可以通过 HA front 网页 UI 进行操作。 具体配置详细,请参考下篇文章。 本文链接:https://deepzz.com/post/homeassistant-introduce.html,参与评论 »
在我们已经确定要异地换工作的情况下,我们在当地缴纳的社保和公积金应该怎么办?如何转移?转移的政策如何?如何确定转移成功? 是的,这个问题一定是需要考虑的。当我们在同一城市换工作,你个人的社保和公积金账户是不会变的,新公司这边可以直接为你接续缴纳。但如果是异地,新公司会确定你是否在该地有账户,无则重新开户,所有年限从零开始。 这里以博主自身经历:上海徐汇 -> 成都高新为例。 知识普及 首先想要普及一些常识,公司一般为员工缴纳五险一金:工伤保险、失业保险、生育保险、医疗保险、养老保险和住房公积金。其中工伤保险、失业保险、生育保险属于“当期交纳当期享受”的险种,养老保险、医疗保险和住房公积金属于累计缴纳,当缴纳到一定年限,相当可观。 因此,我们知道只有养老保险、医疗保险和住房公积金可以进行异地转移。 下面介绍下这几个保险,缴纳比例(上海为例)。具体的缴纳基数可以咨询所在公司的HR。 工伤保险 全额由公司缴纳,0.16-1.52%。该险种是指在工作中或在规定的特殊情况下,遭受意外伤害或患职业病导致暂时或永久丧失劳动能力以及死亡时,劳动者或其遗属从国家和社会获得物质帮助的一种社会保险制度。 就比如说职业病,上下班途中事故都是在该保险所保的范畴。 失业保险 公司缴纳0.5%,个人缴纳0.5%。该险种是指在非个人意愿情况下失业,且未再就业。从而获得补助的一种保险制度。一般每个城市要求员工连续缴纳一年以上才能享受该制度。 如上海政策: 缴费1-5年,可领取最长12个月 缴费5-10年,可领取最长18个月 缴费10年以上,可领取最长24个月 如果失业且暂时未能找到合适工作,可以通过申请失业保险缓解经济压力,同时失业保险基金会为你缴纳医疗保险保障你可以继续享受医保待遇(医保断缴会享受不到医保待遇)。 医疗保险+生育保险 公司缴纳10%,个人缴纳2%。医保是最有用的保险,也是大家比较常用到的,如门诊支付,医保报销等,具体的医保政策可以查看当地档案。医保有两个账户:个人账户和统筹账户。个人缴纳全部划入个人账户,公司缴纳的一般20%划入个人账户。 医保有个连续缴费的说法,连续缴费15年可终身享受医保待遇。停缴次月不再享受医保待遇,停保超3月将重新计算连续缴费周期(不要断缴,影响报销额度)。另外,医保账户的钱是不会清零的哦。 而我们能转移的只有个人账户。 养老保险 公司缴纳16%,个人缴纳8%。简单说就是退休后我们领的退休工资,多缴多得,长缴多得。 有个公式: 每月到手的养老金 = 基础养老金 + 个人账户养老金; 基础养老金 = 退休时上年度在岗职工月平均工资 × (1+本人平均缴费指数(比例)) ÷ 2 × 缴费年限 × 1%; 个人账户养老金 = 个人账户累计储蓄额(含利息) ÷ 计发月数。 退休时能领多少养老金与个人缴费年限、缴费基数、个人账户余额以及养老金领取地的职工平均工资等密切相关。 同样的条件下,一线城市的社会平均工资要高于其它地方,那么在一线城市领取的养老金要比其他地方高得多。 所以说,并不是换了城市工作就一定要转移社保。如果你在大城市缴纳了十几年的社保,已满足在当地退休的条件,因为某些原因需要回老家工作,此时的社保可以考虑不转移。 住房公积金 公司和个人缴纳相同比例,5%-7%之间。住房公积金缴纳比例由公司决定,基数有些公司是工资全额,有些是定额。找工作可以特意看看,一般来说住房公积金缴得越多越好。 用于购房,住房公积金贷款能够贷得比较低的利率,一般是三点几,商业贷款现在已经上六了。如果不贷款还可以用于支付房屋首付款。 用于租房,一般城市租房可通过住房公积金支付租金。 用于建房,翻建或大修住房,公积金缴费人有建造自有住房,或翻修、大修自有住房需求时,可以申请一次性提取住房公积金,由于支付翻建、建房、大修住房费用 重大疾病,公积金缴费人的家庭成员如果不幸患重大疾病,那么可以申请提取公积金用于支付医疗费用,帮助减轻负担 退休、离休、出国定居等,可以申请一次性提取公积金。对于退休人员来说,一次性提取的公积金是一笔可观的养老金。 养老保险转移 首先你得在新就业地办理参保手续,然后才能申请转移。你可以登录 上海一网通办: 1、搜索:基本养老保险,找到《基本养老保险转往外省市缴费凭证》 2、填写申请信息,等待3日(一般很快) 3、前往我的主页,下载缴费凭证: 然后交给公司的HR让她帮你办理社保转移即可。 另一种方式,如果你已经办理过电子社保卡,可直接在开通过电子社保卡的APP上进行申请,如各个银行APP(包括掌上12333): 上述两种,在社保转移申请成功之后,可以在开通电子社保卡的APP(包括掌上12333)上查看转移进度: 这里我遇到的情况是,公司HR也是通过电子发函方式帮我申请的。我也能在电子社保卡的APP查看到申请记录,但是该进度迟迟未能更新。于是我通过转移接续电话咨询得知,上海那边由于系统还在更换当中,电子发函没有提示,所以一直没有处理我的申请。 医疗保险转移 我这边了解到,社保能够通过公司帮忙进行转移,但医保必须得自己到社保局申请。目前成都这边已经将这些工作下发到各个街道办事处了,可以提前了解你所处的街道办事处。 首先你得在新就业地办理参保手续,然后才能申请转移。你可以登录 上海一网通办: 1、搜索:基本医疗保障,找到《基本医疗保障参保(合)凭证(发函用)》 2、填写申请信息,等待3日(一般很快) 3、同上,前往我的主页,下载缴费凭证 4、将凭证打印,到街道办事处进行办理转移接续 这里我遇到的情况是,基本社保和医保转移是同时申请的。但医保很无赖,没有进度,也不好查询。每每前往街道办事处,得知的都是一些还在进行中的说话,没有一个人告诉我需要等到社保转移成功之后才行。 后续社保转移成功之后,医保转移也是迟迟没有成功。多次打街道办电话也不知道具体的原因,我也是很无赖,我甚至从上海那边拿到了挂号信的单号。那就等呗,将近一个月时间总算是成功了。所以医保这块转移还是只有等。 公积金转移 公积金转移的规则是,必须要在新参保地缴纳六个月以上才能够进行转移。 转移需要自行到公积金服务大厅申请。申请的过程很简单,需要一些资料: 新参保地的公司名称 新参保地的公积金账号 愿参保地的公积金账号 愿参保地的单位名称(填写封存的账户名称) 公积金申请接续是体验最好的,填写一个转移接续单就好,转移过程大概2个工作日,且转移成功会有短信提醒。目前成都公积金管理中心需要先预约才能前往办理业务,切记。 参考文档 [1] https://zhuanlan.zhihu.com/p/152012584 https://zhuanlan.zhihu.com/p/106685043 本文链接:https://deepzz.com/post/interprovincial-job-change.html,参与评论 »
2月16日,Go1.16版本发布了。对于我们普通开发者来说,本次版本发布了一些有趣的特性,这里列举了重要的几点: 新增了embed包,在编译时通过使用 //go:embed 指令可以进行嵌入文件的访问,即将文件嵌入到二进制包中。 增加了对 macOS ARM64 的支持(Apple silicon)。 默认开启 Go modules。 修复了一些bug和改进一些问题,如构建速度提升25%,内存使用量降低15%。 io/util 包被弃用,所有方法被移至 io 和 os 包。 具体详细的发布日志移步:go1.16,本篇文章关注的是如何使用://go:embed。 embed功能说明 embed能帮我们做什么?一句话概括就是将静态资源文件嵌入到编译的二进制文件中。 这样做有什么优势?个人认为比较重要是保证一个应用的完整性。比如: 比如一个Web应用,包含了很多image和html,一般情况下我们需要将所有的文件和编译好的二进制文件拷贝到同一机器。如果是分布式应用还会带来更多的拷贝过程。当然如果使用如docker容器镜像方式,是会简化拷贝过程,但也会增加一些负担,如:打包过程。 比如一个App应用,本身会携带很多如音频、图片小文件。一般情况下在安装过程中我们需要将许许多多的小文件进行拷贝,我们知道磁盘I/O瓶颈比较大的,安装时间长会给用户带来不好的体验。 比如一个游戏应用。 比如一个WebAssembly应用等等。 当然上面举的例子只是从一些方面来考虑,具体的打包部署方式需要综合考虑多个因素,如当前公司的自动化运维体系。 embed能帮我们保证一个应用的整体性和完整性,我觉得对于强迫症的开发者来说一定是个福利,哈哈。下面来看看embed的使用方法。 embed基础用法 通过 官方文档 我们知道embed嵌入的三种方式:string、bytes和FS(File Systems)。 //go:embed 基本用法是: package main import "embed" //go:embed hello.txt var s string //go:embed hello.txt var b []byte //go:embed hello.txt //go:embed assets var f embed.FS func main() { print(s) print(string(b)) data, _ := f.ReadFile("hello.txt") print(string(data)) } 1、导入 embed 包,如果没有使用 embed.FS 需要显示的导入: import _ "embed" 2、匹配文件 //go:embed ...,匹配模式符合 path.Match 方式。 (1)匹配模式是相对位置,如: ├── assets │ ├── .gitkeep │ ├── _home.html │ └── index.html ├── hello.txt └── main.go 匹配 index.html 则使用 //go:embed assets/index.html 即可,不能使用 . 和 ..(如./hello.txt)。 //go:embed hello.txt assets/index.html。也可以重复,避免匹配长度过长: //go:embed hello.txt //go:embed assets/index.html var f embed.FS (3)[]byte 和 string 只能匹配单个文件。如果文件名称有空格可使用双引号 " 或者反引号 ``。 (4)如果//go:embed assets匹配的是一个目录,那么该目录中所有文件都将递归的嵌入,除了以.或开头的文件。 (5)匹配目录中的所有内容,使用统配*,包括以.和` 开头的文件。 embed进阶用法 Go1.16 为了对 embed 的支持也添加了一个新包 io/fs。两者结合起来可以像之前操作普通文件一样。 常规文件操作 如通过 embed 进行常规的文件目录读取,文件递归遍历等: //go:embed hello.txt //go:embed hello.txt assets/* var f embed.FS ... entries, err := f.ReadDir(".") if err != nil { panic(err) } for _, entry := range entries { info, err := entry.Info() if err != nil { panic(err) } fmt.Println(info.Name(), info.Size(), info.IsDir()) } Web文件系统 通过原生go http服务,我们将静态资源文件嵌入到二进制中,做静态文件服务器: package main import ( "embed" "net/http" ) //go:embed hello.txt assets/* var f embed.FS func main() { http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.FS(f)))) http.ListenAndServe(":8080", nil) } 通过常见的Web服务框架,提供文件的访问: package main import ( "embed" "net/http" "github.com/gin-gonic/gin" ) //go:embed hello.txt assets/* var f embed.FS func main() { e := gin.Default() e.StaticFS("/static/", http.FS(f)) e.Run(":8080") } 其它web框架各自可以试试。 模版操作 通过 embed 方式嵌入模版,渲染模版: ├── main.go └── tmpl ├── en.tmpl └── zh.tmpl package main import ( "embed" "fmt" "html/template" "net/http" ) //go:embed tmpl/*.tmpl var f embed.FS func main() { t, err := template.ParseFS(f, "tmpl/*.tmpl") if err != nil { panic(err) } // /hello?lang=xx.tmpl http.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) { r.ParseForm() t.ExecuteTemplate(w, r.FormValue("lang"), nil) }) http.ListenAndServe(":8080", nil) } 版本嵌入 通常我们需要将我们的版本打包到二进制文件中,以便确定我们的版本信息。embed 之前我们可以采取通过 -ldflags 的方法将版本动态的赋值到变量。现在,我们可以通过 embed 方式赋值啦。 # version_dev.go // +build !prod package main var version string = "dev" # version_prod.go // +build prod package main import ( _ "embed" ) //go:embed version.txt var version string 执行命令: $ go run . Version "dev" $ go run -tags prod . Version "0.0.1" 参考文档 [1] https://pkg.go.dev/embed How to Use //go:embed 本文链接:https://deepzz.com/post/how-to-use-go-embed.html,参与评论 »
什么是 OIDC OIDC是一个OAuth2上层的简单身份层协议。它允许客户端验证用户的身份并获取基本的用户配置信息。OIDC使用JSON Web Token(JWT)作为信息返回,通过符合OAuth2的流程来获取,更多详细 10 分钟理解什么是 OAuth 2.0 协议。 OAuth2与资源访问和共享有关,而OIDC与用户身份验证有关。 其目的是为您提供多个站点的登录名。每次需要使用OIDC登录网站时,都会被重定向到登录的OpenID网站,然后再回到该网站。例如,如果选择使用Google帐户登录Auth0,这就使用了OIDC。成功通过Google身份验证并授权Auth0访问您的信息后,Google会将有关用户和执行的身份验证的信息发送回Auth0。此信息在JWT中返回,包含ID Token或者Access Token。 JWT包含Claims,它们是有关实体(通常是用户)的Claims(例如名称或电子邮件地址)和其他元数据。OIDC规范定义了一组标准的权利要求。这组标准声明包括姓名,电子邮件,性别,出生日期等。但是,如果要获取有关用户的信息,并且当前没有最能反映此信息的标准声明,则可以创建自定义声明并将其添加到令牌中。 较OAuth2,OIDC有一些不同的概念: OpenID Provider(OP),实现OIDC的OAuth2授权服务器 Relying Party(RP),使用OIDC的OAuth2客户端 End-User(EU),用户 ID Token,JWT格式的授权Claims UserInfo Endpoint,用户信息接口,通过ID Token访问时返回用户信息,此端点必须为HTTPS 协议流程 从理论上来讲,OIDC协议遵循以下步骤: RP发送认证请求到OP OP验证End-User并颁发授权 OP用ID Token(通常是Access Token)进行响应 RP携带Access Token发送请求到UserInfo Endpoint UserInfo Endpoint返回End-User的Claims +--------+ +--------+ | | | | | |---------(1) AuthN Request-------->| | | | | | | | +--------+ | | | | | | | | | | | End- || | | | | User | | | | RP | | | | OP | | | +--------+ | | | | | | | || | | | | | | |<--------(5) UserInfo Response-----| | | | | | +--------+ +--------+ ID Token 这里预先解释ID Token的含义,OIDC对OAuth2进行的主要扩展(用户用户身份验证)就是ID Token,为JWT格式。其中包含授权服务器对用户验证的Claims和其它请求的Claims。 在ID Token中,以下Clams适用于使用OIDC的所有OAuth2: iss,必须,发行机构Issuer,大小写敏感的URL,不能包含query参数 sub,必须,用户身份Subject,Issuer为End-User分配的唯一标识符,大小写敏感不超过255 ASCII自符 aud,必须,特别的身份Audience,必须包含OAuth2的client_id,大小写敏感的字符串/数组 exp,必须,iat到期时间Expire,参数要求当前时间在该时间之前,通常可以时钟偏差几分钟,unix时间戳 iat,必须,JWT颁发时间Issuer at time,unix时间戳 auth_time,End-User验证时间,unix时间戳。当发出max_age或auth_time Claims时,必须。 nonce,用于将Client session和ID Token关联,减轻重放攻击,大小写敏感字符串 acr,可选,Authentication Context Class Reference,0 End-User不符合ISO/IEC 28115 level 1,不应该授权对任何货币价值的资源访问。大小写敏感的字符串。 amr,可选,Authentication Methods References,JSON字符串数组,身份验证的表示符,如可能使用了密码和OTP身份验证方式 azp,可选,Authorized party,被授权方。如果存在必须包含OAuth2的Client ID,仅当ID Token有单个Audience且与授权方不同时,才需要此Claim ID Token可能包含其它Claims,任何未知的Claims都必须忽略。ID Token必须使用JWS进行签名,并分别使用JWS和JWE进行可选的签名和加密,从而提供身份验证、完整性、不可抵赖性和可选的机密性。如果对ID Token进行了加密,则必须先对其签名,结果是一个嵌套的JWT。ID Token不能使用nonce作为alg值,除非所使用的响应类型没有从Authorization Endpoint返回任何ID Token(如Authorization Code Flow),并且客户端在注册时显示请求使用nonce。 授权 身份验证遵循以下三种方式;授权码方式(response_type=code)、隐式方式(response_type=id_token token或response_type=id_token)、混合方式。 下表是三种方式的特征: 属性 授权码 隐式 混合 Token从authorization端点返回 no yes no Token从token端点返回 yes no no Token未显示给浏览器 yes no no 能够验证客户端 yes no yes 可以刷新Token yes no yes 一次交流 no yes no 服务器到服务器 yes no no response_type对应的身份验证方式: response_type 方式 code 授权码 id_token 隐式 id_token token 隐式 code id_token 混合 code token 混合 code id_token token 混合 除了由OAuth2定义的“response_type”之外,所有code均在 OAuth2多种响应类型编码实践。 注意OAuth2为隐式类型定义token的响应类型,但OIDC不会使用此响应类型,因为不会返回ID Token。 授权码方式 使用授权码方式时,所有Token从Token端点返回。授权码将授权code返回给客户端,然后客户端可以将其直接交换为ID Token和Access Token。这样的好处是不会向User-Agent及可能访问User-Agent的其它恶意应用公开任何Token。授权服务器还可以在交换Access Token的授权code之前对客户端进行身份验证。授权code适用于可以安全的维护其自身和授权服务器之间的客户端机密的客户端。 执行以下步骤: 客户端(RP)准备一个包含所需请求参数的身份验证请求 客户端将(RP)请求发送到授权服务器(OP) 授权服务器(OP)对用户(EU)进行身份验证 授权服务器(OP)获得用户同意/或授权 授权服务器(OP)使用授权码将用户发送回客户端(RP) 客户端(RP)使用Token Endpoint的授权码来请求响应 客户端(RP)收到响应,该响应Body中包含ID Token和Access Token 客户端(RP)验证ID Token并检索用户的标识符 授权请求 授权服务器(OP)的authorization端点需要支持GET和POST方法,GET采用Query String序列化,POST采用Form序列化。OIDC采用OAuth2的授权码流程参数: scope,必须,OIDC必须包含openid的scope参数 response_type,必须,同OAuth2 client_id,必须,同OAuth2 redirect_uri,必须,同OAuth2 state,可选,同OAuth2 如: HTTP/1.1 302 Found Location: https://openid.c2id.com/login? response_type=code &scope=openid &client_id=s6BhdRkqt3 &state=af0ifjsldkj &redirect_uri=https%3A%2F%2Fclient.example.org%2Fcb 授权响应 OP收到验证请求后,需要对请求参数做严格的验证: 验证OAuth2的相关参数 验证scope是否有openid参数,如果没有则为OAuth2请求 验证所有必须的参数是否都存在 如果sub是被要求了,必须尽在由子值标识的最终用户与活动session通过身份验证的情况下积极响应。不得使用不用用户的ID Token或Access Token响应,即使这些用户与授权服务器由活动session。如果支持claims,则可以使用id_token_hint发出请求。 验证通过后引导EU进行身份认证并同意授权。完成后,会重定向到RP指定的回调地址,并携带code和state相关参数: HTTP/1.1 302 Found Location: https://client.example.org/cb? code=SplxlOBeZQQYbYS6WxSbIA &state=af0ifjsldkj 获取Token RP使用上一步获得的code请求token端点,然后就可以获得响应Token,其中除了OAuth2规定的数据外,还会附加一个id_token的字段,如: POST /token HTTP/1.1 Host: openid.c2id.com Content-Type: application/x-www-form-urlencoded Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW grant_type=authorization_code &code=SplxlOBeZQQYbYS6WxSbIA &redirect_uri=https%3A%2F%2Fclient.example.org%2Fcb 成功后,OP会返回带有ID Token的JSON数据: HTTP/1.1 200 OK Content-Type: application/json Cache-Control: no-store Pragma: no-cache { "access_token": "SlAV32hkKG", "token_type": "Bearer", "refresh_token": "8xLOxBtZp8", "expires_in": 3600, "id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IjFlOWdkazcifQ.ewogImlzc yI6ICJodHRwOi8vc2VydmVyLmV4YW1wbGUuY29tIiwKICJzdWIiOiAiMjQ4Mjg5 NzYxMDAxIiwKICJhdWQiOiAiczZCaGRSa3F0MyIsCiAibm9uY2UiOiAibi0wUzZ fV3pBMk1qIiwKICJleHAiOiAxMzExMjgxOTcwLAogImlhdCI6IDEzMTEyODA5Nz AKfQ.ggW8hZ1EuVLuxNuuIJKX_V8a_OMXzR0EHR9R6jgdqrOOF4daGU96Sr_P6q Jp6IcmD3HP99Obi1PRs-cwh3LO-p146waJ8IhehcwL7F09JdijmBqkvPeB2T9CJ NqeGpe-gccMg4vfKjkM8FcGvnzZUN4_KSP0aAp1tOJ1zZwgjxqGByKHiOtX7Tpd QyHE5lcMiKPXfEIQILVq0pc_E2DzL7emopWoaoZTF_m0_N0YzFC6g6EJbOEoRoS K5hoDalrcvRYLSrQAZZKflyuVCyixEoV9GfNQC3_osjzw2PAithfubEEBLuVVk4 XUVrWOLrLl0nx7RkKU8NXNHq-rvKMzqg" } 在拿到这些信息后,需要对id_token及access_token进行验证。验证成功就可以通过UserInfo端点获取用户信息了。 验证Token 授权服务器必须验证Token的有效性: 根据RFC6749 验证ID Token规则 验证Access Token规则 UserInfo 获取用户信息,客户端(RP)可以通过GET或POST请求通过UserInfo Endpoint获取用户信息。 GET /userinfo HTTP/1.1 Host: openid.c2id.com Authorization: Bearer SlAV32hkKG 请求成功: { "sub" : "alice", "email" : "alice@wonderland.net", "email_verified" : true, "name" : "Alice Adams", "given_name" : "Alice", "family_name" : "Adams", "phone_number" : "+359 (99) 100200305", "profile" : "https://c2id.com/users/alice", "https://c2id.com/groups" : [ "audit", "admin" ] } 隐式授权 隐式授权,所有Token都从授权端点返回。主要由浏览器中使用脚本语言实现的客户机使用。访问Token和ID Token直接返回给客户端,授权服务器不执行客户端身份验证。 客户端(RP)携带认证参数发送请求到授权服务器(OP) 授权服务器(OP)验证用户并得到用户批准 授权服务器(OP)携带用户相关信息+ID Token/Access Token返回到客户端(RP) 客户端(RP)验证ID Token和检索用户标识符 授权请求 response_type,必须,’id_token token’或’id_token’。无Access Token使用’id_token’ redirect_uri,必须,OP处登记的重定向地址 nonce,必须,隐式授权必须 授权响应 access_token,如果response_type是id_token可以不反回 token_type,固定为Bearer, id_token,必须,ID Token state expires_in,可选,Access Token到期时间(s) 之后就可以拿着ID Token 混合授权 是上面两种模式的混合。可选response_type有:code id_token,code token,code id_token token。 参考资料 [1] https://openid.net/specs/openid-connect-core-1_0.html https://www.jianshu.com/p/be7cc032a4e9 https://demo.c2id.com/oidc-client/ 本文链接:https://deepzz.com/post/what-is-oidc-protocol.html,参与评论 »
是时候离开上海了,脑袋里生出这个念头的时候已是一年前。 上海,中国最发达的城市之一,是很多人实现梦想的地方,这里有很多之最,吸引这无数年轻人在这里奋斗。这里为外来的我们提供了非常多的便利,基本政策非常好。可,成功毕竟是少数人的。 还记得,最初:无知小伙,怀揣着见一见大城市的心情来到了上海,见一见曾经的上海滩,看一看儿时记忆里的东方之珠。几年过去的今天,小伙已然成为00后口中的大叔,成为了前浪。仿佛应该让出位置给更多的年轻人机会,哈哈。 四年时间,让我学到了很多,成长了很多,也让我对自己有一个清楚的认识,更加确定未来的方向。从来不敢想象能够在上海安家,毕竟以现在的工资比较渺茫,离开是必然。和在一起工作了四年的小伙伴已经磨合的非常好,一个眼神都知道是不是有需要交流的地方。决定离开的那天,不是很舒服,离别是一件让人伤感的事情,况且是一个奋斗过四年的地方。 因为个人种种原因,不得不回成都。决定了,那就洒洒脱脱的离开。非常感谢公司和领导对我的支持与帮助,感谢我们能够相遇,共事。感谢我们有机会能够成为朋友,相知。今天的分别不舍,希望我们依然可以相遇,莫要断了这份情。 离开前,躺在床上脑袋时常回想这些年在上海的际遇。画面一张张闪过,是上海呀:一座现代化的大都市。这里有著名的外滩,有享誉世界的陆家嘴,有中国内地唯一的迪士尼。有经常去的电影院,有熟悉的话剧院,有好玩的密室逃脱,各种沉浸式的表演。更多的是我那朝夕相处了四年的小伙伴呐。 途径地铁,看着匆匆走过的人群,和我曾经一样,每一位都是为着自己能够在这个城市立足而努力着,奋斗着,不甘平庸的年轻人。加油,年轻人!我先撤。 是的,今天我已经回到成都,一个生我养我的城市。这里没有了熟悉的公司,熟悉的同事,熟悉的周边商场。同时,面对的是未知,些许担忧:不知道即将面对什么工作,与什么样的同事相处,公司前景如何。不过,信心总是有的,这几年不是白练的。 收获 上海之行,收获颇丰,无论工作还是生活。 工作中,有幸结识了一群优秀的小伙伴,四年的相处中,我们的默契可以说达到了灵魂阶段,非常融洽。每个人都有各自的特点,优点,从他们身上学到很多:坚持、乐观、专业、刻苦。相信我们还会再见,来成都,我请客! 生活中,从一名单身狗,到有了女朋友,到最后成婚,有了家庭。相信会越来越好!同时,也收获了一身负债,哈哈。 成长 自我感觉最深的是:学会了如何思考。几年前,是一个比较冲动的人,遇事沉不住气,意气风发。现在稍微好多了,或许是岁月磨平了棱角。 更多的是一个角色的转变,从而自己的心态也跟着转变。如处理生产事故,需要在极短的时间内找出问题,并且修复它,这需要你有扎实的技术基础和心态。如你带新人,你需要细致耐心的去引导他如何更好的处理问题,解决问题,以致最后学会如何思考问题。等等,你需要的更快成长。 等你技术到了一个瓶颈的时候,思考的问题也发生了转变。是继续在技术上深耕,还是转向人员的管理。技术上如何精进?更多的时候是自己的迷茫,就像是你成为了一个新人需要人带。所以,换个环境,感受一下不同?人员的管理,你会不会有内心的惶恐,前一步还安安心心打酱油,后一步就得学着如何规划:事与人,学着如何去处理部门与部门之间的沟通,人与人之间的沟通,人与事的合理安排。 很庆幸,在老东家学到了不少(虽只是皮毛),这对我自己来说已是一次不可多得的历练。相信,学以致用,学有所成。 现在 现在,已然成为一名新人,一个新的环境。 很多时候,新环境能够激发人类的一些潜质,一些新的东西会使你迸发出新的思维。你能够学习的东西更多,遇到的难题也会更多,需要掌握技术的深度与广度也更高。 同时,挑战意味着机遇,学着如何在挑战中成长才是我们需要关注的地方。加油,兄die! 所以,让自己偶尔处于一个不太舒适的环境或许遇见不一样的你。 未来 未来已来!大多数时候能够从现在的你窥见未来的你。一句老话说得好:努力不一定成功,但不努力一定不会成功。所有成功都是来之不易,点滴积累,最要坚持。 如果此时此刻的你还不知道坚持什么,可以和我交流。 如果此时此刻的你还不知道为什么坚持,可以和我交流。 如果此时此刻的你想要放弃,卧槽,那你一定不会看到这句。 致: 感谢我们已经经历过的,享受我们正在经历的,期待我们即将经历的! 上海 爱上海,爱生活! 本文链接:https://deepzz.com/post/my-shanghai-life.html,参与评论 »
本篇文章主要介绍 nginx server 虚拟服务器如何配置。其中包括的一些例子我会已保存到 nginx 。尽可能的想要整理一份比较完整的配置说明,避免找寻资料的麻烦。博主也尽可能的保证本篇文章的准确性,如有失误,请告知。 通过 $ nginx -V,你可以看到 nginx 的编译配置信息: $ nginx -V nginx version: nginx/1.17.3 built by gcc 8.3.0 (Debian 8.3.0-6) built with OpenSSL 1.1.1c 28 May 2019 TLS SNI support enabled configure arguments: --prefix=/etc/nginx --sbin-path=/usr/sbin/nginx --modules-path=/usr/lib/nginx/modules --conf-path=/etc/nginx/nginx.conf --error-log-path=/var/log/nginx/error.log --http-log-path=/var/log/nginx/access.log --pid-path=/var/run/nginx.pid --lock-path=/var/run/nginx.lock --http-client-body-temp-path=/var/cache/nginx/client_temp --http-proxy-temp-path=/var/cache/nginx/proxy_temp --http-fastcgi-temp-path=/var/cache/nginx/fastcgi_temp --http-uwsgi-temp-path=/var/cache/nginx/uwsgi_temp --http-scgi-temp-path=/var/cache/nginx/scgi_temp --user=nginx --group=nginx --with-compat --with-file-aio --with-threads ... 其中可以看到 --prefix=/etc/nginx,nginx 安装时会把相关数据文件写入到该目录,如我们的配置文件 --conf-path。 每次更改 nginx 的配置文件,你需要执行一下操作: # 验证配置文件的正确性 $ nginx -T # 重新加载配置文件 $ nginx -s reload 基础概念 这里推荐 nginx 的官方文档地址:http://nginx.org/en/docs/。 文档中涵盖了各个模块的配置用法,以及默认值,可以填写的上下文位置。 目前 nginx 支持多种服务类型: http mail stream google perftools 我们 着重介绍 http 服务。其它服务基本知识点都能涵盖到。 安装完 nginx ,我们先来看一看 nginx 的默认配置 /etc/nginx/nginx.conf,当然可能与你的默认配置不同,不过大同小异: # worker以什么身份运行 user nginx; // default nobody # worker进程个数,一般为 CPU 个数,也可选 auto worker_processes 1; # default 1 # 每个worker可打开的描述符限制 worker_rlimit_nofile 8192; # 错误日志保存路径和级别 error_log /var/log/nginx/error.log warn; # 进程pid保存路径 pid /var/run/nginx.pid; # 指定dns服务器 resolver 10.0.0.1; events { # 每个worker最大连接数 worker_connections 1024; # default 1024 } # http 服务定义 http { # 加载 mime 类型 include /etc/nginx/mime.types; # 定义默认数据类型 default_type application/octet-stream; # 日志格式 log_format main '$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent" "$http_x_forwarded_for"'; # 访问日志 access_log /var/log/nginx/access.log main; # 是否调用sendfile函数(zero copy 方式)来输出文件,如果磁盘IO重负载应用,可设置为off sendfile on; # 此选项允许或禁止使用socke的TCP_CORK的选项,此选项仅在使用sendfile的时候使用 #tcp_nopush on; keepalive_timeout 65; # 代理相关设置 # proxy_connect_timeout 90; # proxy_read_timeout 180; # proxy_send_timeout 180; # proxy_buffer_size 256k; # proxy_buffers 4 256k; # proxy_busy_buffers_size 256k; # proxy_temp_file_write_size 256k; # tcp_nodelay on; # gzip 压缩 #gzip on; # 加载其它配置,这样我们在 conf.d 下写的文件才会生效 include /etc/nginx/conf.d/*.conf; } 加载配置 /etc/nginx/conf.d,才能让我们的配置生效: # 加载其它配置 include /etc/nginx/conf.d/*.conf; 一般的,如果是小站点不用去修改默认配置。当流量到达一定程度,需要进行适当优化。 内置变量 内置变量,nginx 各个模块都将请求的一些参数进行变量化,通过 $ + 变量名 即可使用。每个模块或多或少都有自己的变量。着重介绍下核心模块的 内置变量: # 通过arg_的方式可取出相关参数,若请求 /foo?name=Tony&age=2,则 arg_name=tony arg_age=2 $arg_name $args # 客户端IP地址二进制 $binary_remote_addr # 发送到客户端的字节数,不包括响应头 $body_bytes_sent # 发送给客户端字节数 $bytes_sent # 连接序列号 $connection # 当前已经连接的请求书 $connection_requests # Content-Length 请求头 $content_length # Content-Type 请求头 $content_type # cookie 名称 $cookie_name # 当前请求的 root 或 alias 的值 $document_root # 与 $uri 相同 $document_uri # 优先级:请求行中的 host name,请求头中的 Host,请求匹配的 server name $host # host name $hostname # 任意请求头字段。变量名的最后一部分是转换为小写的字段名,用下划线替换破折号 $http_name # 如果连接在 SSL 模式下运行,则为 on,否则为空字符串 $https # ? 后如果请求行有参数,或者空字符串 $is_args # 设置此变量可以限制响应速度 $limit_rate # 当前时间(秒),分辨率为毫秒 $msec # nginx 版本号 $nginx_version # 当前 worker 进程号 $pid # 如果是 pipelined 则为 p,否则为 . $pipe # 代理协议头中的客户端地址,否则为空字符串,代理协议之前必须通过在listen指令中设置 proxy_protocol 参数来启用 $proxy_protocol_addr # 来自代理协议头的客户端端口,否则为空字符串,代理协议之前必须通过在listen指令中设置 proxy_protocol 参数来启用 $proxy_protocol_port # 与 $args 相同 $query_string # 与当前请求的 root 或 alias 指令值对应的绝对路径名,所有符号链接都解析为实际路径 $realpath_root # 客户端地址 $remote_addr # 客户端端口 $remote_port # 使用 Basic auth 的用户名 $remote_user # 完整的请求行 $request # 请求体,当将请求体读入内存缓冲区时,proxy_pass、fastcgi_pass、uwsgi_pass和scgi_pass指令处理的位置可以使用变量的值 $request_body # 具有请求主体的临时文件的名称 $request_body_file # 如果请求完成则为 OK,否则为空 $request_completion # 当前请求的文件路径,基于 root 或 alias 和请求 URI $request_filename # 由16个随机字节生成的惟一请求标识符,以十六进制表示 $request_id # 请求长度(包括请求行、头和请求体) $request_length # 请求方法,如 GET 或 POST $request_method # 请求处理时间,从客户端读取第一个字节以来的时间 $request_time # 若请求 /foo?a=1&b=2,则 request_uri=/foo?a=1&b=2 $request_uri # 如 http 或 https $scheme # 任意响应报头字段,变量名的最后一部分是转换为小写的字段名,用下划线替换破折号 $sent_http_name # 响应结束时发送的任意字段,变量名的最后一部分是转换为小写的字段名,用下划线替换破折号 $sent_trailer_name # 接受请求的服务器的地址 $server_addr # 接受请求的 server 名称 $server_name # 接受请求的 server 端口 $server_port # 请求协议,如 HTTP/1.0 或 HTTP/1.1 或 HTTP/2.0 $server_protocol # 响应状态 $status $tcpinfo_rtt,$tcpinfo_rttvar,$tcpinfo_snd_cwnd,$tcpinfo_rcv_space # 本地时间ISO 8601标准格式 $time_iso8601 # 通用日志格式的本地时间 $time_local # 若请求 /foo?a=1&b=2,则 uri=/foo $uri # 用户代理 $http_user_agent # cookie $http_cookie 你还可以通过自定义变量指令 set 进行变量的定义。 server定义 server 即虚拟服务,它用来描述我们站点一些访问规则。需要填写在 http 标签中,可定义多个,如: http { server { ... } server { ... } ... } 一个常见的 server 的定义: resolver 10.0.0.1; # 负载均衡 upstream dynamic { zone upstream_dynamic 64k; server backend1.example.com weight=5; server backend2.example.com:8080 fail_timeout=5s slow_start=30s; server 192.0.2.1 max_fails=3; server backend3.example.com resolve; server backend4.example.com service=http resolve; server backup1.example.com:8080 backup; server backup2.example.com:8080 backup; } # http服务 server { listen 80; server_name example.com www.example.com; location / { rewrite https://$host; # 重定向到https } } # https 服务 server { listen 443 ssl; # 监听端口 server_name example.com www.example.com; # 匹配域名 # ssl证书 ssl_protocols TLSv1 TLSv1.1 TLSv1.2; ssl_ciphers AES128-SHA:AES256-SHA:RC4-SHA:DES-CBC3-SHA:RC4-MD5; ssl_certificate /usr/local/nginx/conf/cert.pem; ssl_certificate_key /usr/local/nginx/conf/cert.key; ssl_session_cache shared:SSL:10m; ssl_session_timeout 10m; # 静态服务 location / { root /usr/share/nginx/html; index index.html index.htm; } # 反向代理 location /api { proxy_pass http://dynamic; health_check; } } 下面就让我们来详细解释下。 http_upstream_module http_upstream_module,upstream 说白了就是做负载均衡,它可以帮助我们定义一组相同服务的别名,如backend,当请求到来的时候可以通过相关策略帮我们选一组服务提供响应。 目前只能被 proxy_pass,fastcgi_pass,uwsgi_pass,scgi_pass,memcached_pass,grpc_pass 使用。 形式如下: upstream { # 命名 server [parameters]; # 服务 server [parameters]; ... } [parameters] 参数可选以下值: weight=number,default 1,设置 server 的权重 max_conns=number,default 0,限制 server 的活跃连接数,0 代表不限制 max_fails=number,default 1,设置在 fail_timeout 时间内失败的最大次数,可由 proxy_next_upstream,fastcgi_next_upstream,uwsgi_next_upstream,scgi_next_upstream,memcached_next_upstream,grpc_next_upstream 指定下组 upstream,0 值代表不启用 fail_timeout=time,default 10s,设置多长时间判定无连接服务器失败 backup,标记 server 为备用 server,当 primary server 不可用时启用 down,标记 server 下线不可用 resolve,用来监视与服务器域名对应IP地址的更改,它会自动更改上游配置,upstream 必须驻留在共享内存中,必须写在 http 标签中。 http { resolver 10.0.0.1; upstream u { zone ...; ... server example.com resolve; } } route=string,设置 server 路由名称 server=name, slow_start=time,慢启动,server 非正常状态恢复到正常需要的时间 drain,设置为 drain 模式 其它负载均衡设置: zone name [size],设置共享内存的名称和大小 state file, hash key [consistent],负载均衡方式,key 可以为文本,变量,或其组合 ip_hash,负载均衡方式,根据IP地址范围分布 server,用 IPv4 前三个8位或整个IPv6 keepalive connections,设置到上游 server 保持最大空闲连接 upstream memcached_backend { server 127.0.0.1:11211; server 10.0.0.2:11211; keepalive 32; } server { ... location /memcached/ { set $memcached_key $uri; memcached_pass memcached_backend; } } keepalive_requests number,设置最大请求连接数 keepalive_timeout timeout,连接超时时间 ntlm,允许使用NTLM身份验证代理请求 upstream http_backend { server 127.0.0.1:8080; ntlm; } server { ... location /http/ { proxy_pass http://http_backend; proxy_http_version 1.1; proxy_set_header Connection ""; ... } } least_conn,负载均衡方式,将请求传给活跃连接数最少的 server least_time header | last_byte [inflight],负载均衡方式,将请求传给平均响应时间和活跃连接数最少的 server queue number [timeout=time],队列缓存,当选择不到 server 处理请求时放入队列,如果队列满,返回502 random [two [method]],负载均衡方式, sticky,会话关联,同一客户端请求将会被传给同一 upstream 的同一 server # cookie upstream backend { server backend1.example.com route=a; server backend2.example.com route=b; sticky cookie srv_id expires=1h domain=.example.com path=/; } # route map $cookie_jsessionid $route_cookie { ~.+\.(?P\w+)$ $route; } map $request_uri $route_uri { ~jsessionid=.+\.(?P\w+)$ $route; } upstream backend { server backend1.example.com route=a; server backend2.example.com route=b; sticky route $route_cookie $route_uri; } # learn upstream backend { server backend1.example.com:8080; server backend2.example.com:8081; sticky learn create=$upstream_cookie_examplecookie lookup=$cookie_examplecookie zone=client_sessions:1m; } listen listen 监听设置,来看一看可选参数: 默认 listen *:80 | *:8000; listen address[:port] [default_server] [ssl] [http2 | spdy] [proxy_protocol] [setfib=number] [fastopen=number] [backlog=number] [rcvbuf=size] [sndbuf=size] [accept_filter=filter] [deferred] [bind] [ipv6only=on|off] [reuseport] [so_keepalive=on|off|[keepidle]:[keepintvl]:[keepcnt]]; listen port [default_server] [ssl] [http2 | spdy] [proxy_protocol] [setfib=number] [fastopen=number] [backlog=number] [rcvbuf=size] [sndbuf=size] [accept_filter=filter] [deferred] [bind] [ipv6only=on|off] [reuseport] [so_keepalive=on|off|[keepidle]:[keepintvl]:[keepcnt]]; listen unix:path [default_server] [ssl] [http2 | spdy] [proxy_protocol] [backlog=number] [rcvbuf=size] [sndbuf=size] [accept_filter=filter] [deferred] [bind] [so_keepalive=on|off|[keepidle]:[keepintvl]:[keepcnt]]; 真的多,可平时也没用几个,举例: listen 127.0.0.1:8000; listen 127.0.0.1; # 如果只指定地址,默认监听 80 listen 8000; listen *:8000; listen localhost:8000; listen 127.0.0.1 default_server accept_filter=dataready backlog=1024; # IPv6 listen [::]:8000; listen [::1]; # unix socket listen unix:/var/run/nginx.sock; 其它参数说明: default_server,如果指定,server 将会成为默认 server ssl,开启 ssl 模式,即 https http2,正常情况开启 http2 都应该开始 ssl,但 nginx 也支持不开启 ssl 下的 http2 协议 spdy,和 http2 一样,建议开启 ssl setfib=number,监听套接字设置关联的路由表FIB (SO_SETFIB选项)。这目前只适用于FreeBSD fastopen=number,为监听套接字启用“TCP Fast Open”(1.5.8),并限制尚未完成三方握手的连接队列的最大长度 backlog=number rcvbuf=size,接受 buffer 的大小(SO_CRCVBUF) sndbuf=size,发送 buffer 的大小(SO_SNDBUF) accept_filter=filter,可选 dataready 和 httpready,在 accept() 前过滤 deferred,指示在Linux上使用deferred accept() (TCP_DEFER_ACCEPT套接字选项) bind,标记指定 address:port 单独的绑定 ipv6only on|off,只接受 IPv6 连接 reuseport so_keepaliv on|off|[keepidle]:[keepintv1]:[keepcnt],”TCP keepalive” 开关 server_name server_name,设置虚拟主机的名称。 形式如下: 默认值 server_name ""; server_name name ...; 例1,穷举域名 server { server_name example.com www.example.com; } 例2,通配符写法 server { server_name example.com *.example.com www.example.*; } 例3,这种写法满足例1 server { server_name .example.com; } 例4,正则表达式,以 ~ 开头 server { server_name www.example.com ~^www\d+\.example\.com$; } 例5,正则表达式捕获 server { server_name ~^(www\.)?(.+)$; location / { root /sites/$2; } } server { server_name _; location / { root /sites/default; } } 例6,正则表达式变量 server { server_name ~^(www\.)?(?.+)$; location / { root /sites/$domain; } } server { server_name _; location / { root /sites/default; } } 例7,与空名称使用 server { server_name www.example.com ""; } 如果当一个名称匹配多个 server 的是时候,匹配优先级如下: 确切的名称 以 * 开头的最长的通配符名称 以 * 结尾的最长通配符名称 第一个匹配的正则表达式 更多匹配规则请查阅:http://nginx.org/en/docs/http/server_names.html location location 是用来干嘛的,它是用来根据 URI 进行配置设置的,如: server { listen 80; server_name example.com; location / { # 普通请求网页 root /usr/share/nginx/html; index index.html index.htm; } location /api { # API请求代理 proxy_pass http://dynamic; health_check; } } 形式如下: location [ = | ~ | ~* | ^~ ] uri { ... } none,如果没有修饰符,则将该位置解释为前缀匹配。这意味着给定的位置将根据请求URI的开头进行匹配,以确定匹配 =,代表精确匹配,完全相等即匹配 ~,区分大小写的正则表达式匹配 ~*,不区分大小写的正则表达式匹配 ^~,普通字符匹配,如果该选项匹配,只匹配该选项 nginx 的匹配过程如下: 精确匹配 =,如果匹配成功,搜索停止 前缀匹配,最长位置匹配,如果该匹配具有 ^~,搜索停止 正则匹配,按配置文件中定义的顺序进行匹配。 如果第3条规则产生匹配的话,结果被使用。否则,使用第2条规则的结果。 让我们通过一个例子来了解下匹配规则: location = / { [ configuration A ] } location / { [ configuration B ] } location /documents/ { [ configuration C ] } location ^~ /images/ { [ configuration D ] } location ~* \.(gif|jpg|jpeg)$ { [ configuration E ] } 请求 / 将会匹配 A,请求 /index.html 将会匹配 B,请求 /documents/document.html 将会匹配 C,请求 /images/1.gif 将会匹配 D,请求 /documents/1.jpg 将会匹配 E。 ssl mode ssl 模式可以让我们站点启用 HTTPS,具体详细请参考 http_ssl_module。 想要开启 ssl 模式,需要在 listen 关键字处添加上 ssl,如: server { listen 443 ssl; server_name example.com; ssl_certificate example.com.rsa.crt; ssl_certificate_key example.com.rsa.key; ssl_certificate example.com.ecdsa.crt; ssl_certificate_key example.com.ecdsa.key; ... } 上面的例子是部署双证书,当某一证书因某种原因失效不至于导致站点不能访问。下面来看看参数解释: ssl_buffer_size size,default 16k,发送数据的缓冲区的大小 ssl_certificate file,PEM 格式证书文件 ssl_certificate_key file,PEM 格式私钥文件 ssl_ciphers ciphers,default HIGH:!aNULL:!MD5,ssl套件 openssl ciphers ssl_client_certificate file,用于验证客户端证书的 CA 文件 ssl_crl file,用于验证客户端证书的吊销文件 ssl_dhparam file,为DHE密码指定具有DH参数的文件 ssl_early_data on|off,default on ssl_ecdh_curve curve,default auto,为ECDHE密码指定一条曲线 ssl_password_file file,私钥密码文件 ssl_prefer_server_ciphers on|off,是否启用服务器套件偏好 ssl_protocols [SSLv2] [SSLv3] [SSLv3] [TLSv1] [TLSv1.1] [TLSv1.2] [TLSv1.3],default TLSv1 TLSv1.1 TLSv1.2,可选的ssl协议 ssl_session_cache off|none|[builtin[:size]] [shared:name:size],default none,设置 session cache 的类型和大小 ssl_session_cache builtin:1000 shared:SSL:10m; ssl_session_ticket_key file,设置一个文件,其中包含用于加密和解密TLS会话票据的密钥 ssl_session_ticket_key current.key; ssl_session_ticket_key previous.key; 随机一个 AES256(80),AES128(40) openssl rand 80 > ticket.key ssl_session_tickets on|off,default on,是否启用 session ticket ssl_session_timeout time,default 5m,超时时间 ssl_stapling on|off,default off,ocsp 装订 ssl_stapling on; resolver 192.0.2.1; ssl_stapling_file file ssl_stapling_responder url ssl_stapling_verify on|off,default off ssl_trusted_certificate file,指定验证客户端证书的 CA 文件 ssl_verify_client on|off|optional|optional_no_ca,default off,是否验证客户端证书 ssl_verify_depth number,default 1,设置客户端证书链的验证深度 相关变量 $ssl_cipher,已建立连接使用的 ciphers $ssl_ciphers,客户端支持的 ciphers $ssl_client_escaped_cert,urlencoded 客户端证书 $ssl_client_fingerprint,SHA1指纹 $ssl_client_i_dn,issuer DN $ssl_client_i_dn_legacy,同上,1.11.6之后使用 $ssl_client_raw_cert,PEM格式客户端证书 $ssl_client_s_dn,subject DN $ssl_client_s_dn_legacy,同上,1.11.6之后使用 $ssl_client_serial,客户端证书序列号 $ssl_client_v_end,客户端证书结束时间 $ssl_client_v_remain,剩余多少天 $ssl_client_v_start,证书开始时间 $ssl_client_verify,客户端证书是否验证成功,"SUCCESS" 或 "FAILED:reason" 或 "NONE" $ssl_curves,客户端支持的曲线 $ssl_early_data $ssl_protocol,连接使用的协议 $ssl_server_name,从 SNI 获取的 server name $ssl_session_id,连接的 session id $ssl_session_reused,session是否重用,"r" 重用,"." 没有 其它模块 其它模块你需要根据文档及编译信息判断该模块是否默认编译在 nginx 中,并且版本是否匹配: ngx_http_access_module ngx_http_addition_module ngx_http_api_module ngx_http_auth_basic_module ngx_http_auth_jwt_module ngx_http_auth_request_module ngx_http_autoindex_module ngx_http_browser_module ngx_http_charset_module ngx_http_dav_module ngx_http_empty_gif_module ngx_http_f4f_module ngx_http_fastcgi_module ngx_http_flv_module ngx_http_geo_module ngx_http_geoip_module ngx_http_grpc_module ngx_http_gunzip_module ngx_http_gzip_module ngx_http_gzip_static_module ngx_http_headers_module ngx_http_hls_module ngx_http_image_filter_module ngx_http_index_module ngx_http_js_module ngx_http_keyval_module ngx_http_limit_conn_module ngx_http_limit_req_module ngx_http_log_module ngx_http_map_module ngx_http_memcached_module ngx_http_mirror_module ngx_http_mp4_module ngx_http_perl_module ngx_http_proxy_module ngx_http_random_index_module ngx_http_realip_module ngx_http_referer_module ngx_http_rewrite_module ngx_http_scgi_module ngx_http_secure_link_module ngx_http_session_log_module ngx_http_slice_module ngx_http_spdy_module ngx_http_split_clients_module ngx_http_ssi_module ngx_http_ssl_module ngx_http_status_module ngx_http_stub_status_module ngx_http_sub_module ngx_http_upstream_module ngx_http_upstream_conf_module ngx_http_upstream_hc_module ngx_http_userid_module ngx_http_uwsgi_module ngx_http_v2_module ngx_http_xslt_module nginx完整配置 一份给新人的 nginx 完整配置:https://github.com/deepzz0/nginx。 参考链接 [1] http://nginx.org/en/docs/ 本文链接:https://deepzz.com/post/how-to-write-nginx-server.html,参与评论 »
前一篇文章:她和他的第一次出国自由行 - 出行模版。 她姓陈,他也姓陈,她和他走到了一起,于是他们的旅程开始了。 她是个活泼开朗的人,喜欢所有美好的东西,向往自由。她做事直接,雷厉风行却也做事细心,知书达理。 他是个典型的程序员,理性睿智,性格沉稳。但同时也有着一颗走出去的心,喜欢有挑战的东西,登山,漂流。 一天某人说想要去巴厘岛旅行,他说我也要去。于是,她以后的计划里都不再是她一个人,她要和她爱的人一同出发啦。说干就干,她开始做攻略了,购买有关巴厘岛旅行相关的书,泡在马蜂窝看各种攻略,安装各种旅行APP。她说这是她第一次做这么详细的攻略,也是第一次出国自由行,她怕她做的攻略不完美。可她怎会知道,他正认真的看着她做攻略的样子,认真,笃定。很感谢她带给他的这种感觉,这是幸福的味道。 经过多日奋战及最后的实践,我们得出了如下时间计划: 旅行预算 愉快的旅行必定少不了精心的预算环节,毕竟大多数人还是需要考虑花多少钱的问题。因为机票和酒店的费用因淡旺季的原因起伏很大,这里就不列举了。 一日游项目 2(人)x¥570x2(天)=¥2280,只计划了前两天的一日游项目。一日游一般包酒店接送,包午餐。 吃喝 2(人)x2(餐)x¥75x6(天)+¥200=¥2050,平均一餐¥150左右,外加¥200的上浮。去除了一日游的午餐,故少一天。 包车 ¥350x4(天)=¥1400,计划有4天自由行(包含小费),想去哪就去哪。 门票 2(人)x¥50x4(天)=¥400,包车自由行的花需要自己开销门票。 根据上面的预算,我们总共大概需要¥6100。因为我们一日游是通过网上订票,因此我们只在国内(中国银行)兑换了$500,兑换美金的原因是在兑换印尼盾的时候可能汇率稍微高些(也没太注意),人民币也可以在当地兑换。总共身上携带现金共$500+¥1000。另外巴厘岛的ATM是支持银联卡的。 而事实上,我们的实际花销其实是和计划差不多的。就是,在吃的问题上吃了一次大餐(被坑),当时是去金巴兰看日落,身上又刚去兑换了印尼盾。身揣巨款不得虚,我们的司机直接就把我们拉到一个海鲜店里。尼玛,乡巴佬没吃过大龙虾啊,尝试一下!事后才感觉坑。。。 行前准备 一定要做如下准备,却保你的旅行畅通无忧,安全前行。 签证,目的地是否需要签证,巴厘岛免签。确保签证下来之后再继续下面的安排。 预定机票,根据你们的计划时间提前预定机票。国际旅行确保是否转机,行李是否直挂。 预定酒店,最好提前预定酒店,免得到时候像无头苍蝇乱撞,麻烦。最好根据旅行行程预定游玩景点附近的酒店,省的奔波,辛苦。 下载APP,提前想好我们在国外可能遇到的问题,做好准备。 导航,谷歌地图。国外貌似谷歌地图才好使 翻译,谷歌翻译,百度翻译。是在不知道怎么表达的时候派上用场。 旅行,携程,飞猪,马蜂窝,可以在上面定一日游,找司机等。 出行,Uber,Grab,Go-Jek,东南亚的滴滴,美团,好用得很。 民宿,Airbnb,Agoda,用来预定特色民宿,很不错。 网络通讯,一定要保持网络通畅,出门在外不容易啊。 国际漫游,如果不嫌贵可以申请开通国际漫游服务。 当地电话卡,去淘宝购买当地电话卡,非常方便。 租赁WIFI,让我们随时都有网可以上。需要先预定,然后在机场借还。 相关资料,可以打印需要的重要资料备份,以防手机丢失或没电。 当然还有我们自己的行李了,巴厘岛属于热带,太阳非常的毒辣,一定要注意防嗮。不然要脱两层皮。每年10月 - 次年3月为雨季,注意防雨。 实际行程 总体来说我们的行程安排的不是很满,有快有慢,这才是节奏。毕竟,旅行的最终意义在于感受与理解。看到了什么,理解了什么,要有所得才好啊。 出发前,前往双流机场T1取了租借的随身Wifi,托运行李。因为是到新加坡转机,行李直挂,所以我们拿到了两程的机票。接下来就是长达4个多小时的飞行时间。到达新加坡之后,衣服要换成夏装。可以逛逛这边的免税店,她说有的卖得比国内便宜几百块。有需要的朋友也可以提前在网上订购,等回程的时候直接去取就可以了。接着就是从新加坡到巴厘岛啦,期间会让你填一张海关申请表: Day1 大概在早上10点,我们到达伍拉赖机场(这个机场有好多名字)。在机场兑换了$200,事实证明机场兑换稍微少那么几十块。一出机场,好家伙,一股热气袭来。哇,从冬天直接到夏天了。 当天下着小雨,不过没有影响到我们的心情,因为有司机直接接我们去酒店。她在微信上联系的,到库塔的海滩馨乐酒店120k。 不过酒店要下午2点才能入住。没办法,我们只好将行李寄放在酒店,出去逛了逛。顺便吃个午餐(路边小店),这里的午餐还不错啊,挺实惠,能吃饱。 整体而言,这个酒店是本次旅行最普通的,但也是我认为是最舒适的。酒店位置靠近库塔沙滩,只间隔一条马路。处在市中心,有大型商场,酒吧等娱乐场所。最重要的是提供早餐,早餐啊!特别棒。 入住之后,稍微整理了下。还是稍微睡个觉,休息一下。准备明天的行程。之后就是出去觅食啦。酒店后面是一条比较传统的老街,有各种手工作品,赏心悦目啊。 不过,对于雨季的担心还是不可避免啊,天呐。 Day2 今天我们报了一个Penida岛深度游(不浮潜)。报一日游的好处就是自己不用找车,酒店专车接送,包午餐,有印尼范的中文导游。不好的地方就是线路一定,时间有限,一个点只能停留一个小时左右。 要到岛上需要先到码头坐40分钟左右的船(Fast Boat),期间有一段时间会有比较大的浪,可能会晕船,最好向导游要个袋子。之后是坐一个小时的车上山。整个过程只能用颠簸来形容,还非常的热,火辣(我们有理由相信天气预报不准)。期间你可以睡一觉哦,但一定要抓紧,弯很大,车很快。 经过一路颠簸,你就会来到达传说中iPhone壁纸拍摄的地方。遗憾的是我们没有足够的时间,更加深一步的了解这个地方。 回到酒店,我们就约莫着可以吃晚饭了。累了一天还是好好犒劳自己,在大众点评上找了一家网红,猫途鹰上的评价也还不错。说走就走,拐来拐去的还是把它找到了,不过要排队,很多人。 Day3 本来今天我们是打算去滑翔的,考虑到天气的问题,万一不能滑翔呢?要不调到后面再去把。这一调直接让我们错过了最想去的滑翔伞项目。 今天我们睡到自然醒,吃完早餐,懒洋洋的呆了会。下午包了半天的辆车,自由行。路线是:情人崖 -> Suluban bench -> 金巴兰日落。 可能因为对情人崖景点不太了解的缘故,我们没走几步,拍了一些照片就没继续了。其实前面还有很长的一段路程。也没见着传说中的猴子。 之后,我们就去了离这里比较近的Suluban bench,真的超级棒。这里的人很少,是个小众景点,但美得很出众。 晚上我们打车去了一个叫Warung Ipang的餐厅,这里真的是性价比超高,推荐。 Day4 今天我们没有计划,有时候没有计划就是最好的计划。睡觉睡到自然醒,然后到传统市场逛逛,转转,感受一下慢生活。傍晚夕阳西下,弥补一下金巴兰没有看到日落的遗憾。尽管这里的沙滩有很多渣子,不过依然有美丽的日落。旁边还有踢沙滩足球的印尼孩童,美哉。 We’re at kuta beach. Day5 今天我们要离开海滩馨乐酒店,去往我们的下一个很有特色的民宿,所以我们不得不带上我们的行李出发,包个车是不错的选择。 今天选择的第一个地方是印尼盾上印的一个 Temple,恰巧碰上了一个什么活动,挺有特色的。 之后,去了她心心念的 Ins 上看到的双子瀑布。 最后,来到了拍照很美的Wanagiri Hidden Hill。这里门票真的贵,不过拍出来的照片真的美。面朝高山湖泊,我心似大海。 Yes, 今天陈同志安排的是一个非常有特色的民宿圆顶屋。看似简单,内在却很丰富,这里包含了厕所,洗浴,以及多人玩耍的地,不过都是开放式的哦。屋子是建在田中间的,很有乡村气息,晚上能听到各种虫鸣,也有比较大胆的动物进来偷吃我们的零食,胆大! 不过这个圆顶屋是真的不好找,我们司机带我们险些走错路。 Day6 今天,我们还得换地方。原本可以定两个晚上,可是后面日期都被预定出去了,罢了,换一个地方。 今天我们去了巴厘岛必打卡的地方Bali Swing。这里的服务员厉害的哦,见哪国人说哪国话,中文说得是溜溜的。一进Bali Swing就齐声高喊Welcome to Bali Swing,很棒。对了,一定要体验那根最长的秋千,爽。 接下来是去了一个瀑布,不过这个瀑布因为前一天晚上下雨的缘故,很浑浊。我们待了一会就转战下个点。 这会,我们来到了德格拉朗梯田,我们没有见到网上那些生长茂密的农作物,只瞧见了小苗。想要说的是,这里的民风有点不好,可能是作为游客的我们打扰到他们了吧。要进入到梯田,首先需要在坡上的小店吃东西,才能进去。等到我们走了一段路,又有个大妈搭了个草棚,堵在那要钱,讲了讲价,最终让我们进去了。没想到的是,在往梯田上走的时候还有个老哥要钱,那必须是没钱啊。一看我们要走,立马说免费还和我们聊了聊天,打听我们是哪里人。中国人是冤大头吗! 到了晚上,我们住的还是一个民宿,类似于圆顶屋,不过是一个独立的院子,有大门,圆顶屋下还有个水池养着鱼。这个民宿挺大的,有好几个独立的院子。院子外面房东弄了个无边的泳池,以及餐厅(对,就是提供早餐)。 晚上,我们又开始了我们的觅食之旅。不走出去不知道,一走出去吓一跳,这里就是民宿窝啊,这一遍全是民宿,连餐厅都能够开到民宿里。如果能生活在这个地方多好,好不悠闲自在。 Day7 今天迎来了我们的终极挑战Campuhan Ridge Walk,乌布坎普罕山脊徒步。说是徒步,那就是晒太阳,太阳太大了,没走几步,汗如雨下啊。期间遇到很多外国友人,还有带娃的,冲啊。 实际上,这条徒步路线非常出名,据说排名世界前三。本来我是下决心要走完几公里的,某人不知道咋想的,看到后面的路景色不美了就不走了。计划三个小时完成的徒步之旅,一个小时就搞定了,这导致我们还有很多时间。我们的司机推荐我们去漂流,好像是提前准备好的,汗。 漂流的话最好是提前预定,这样才好安排人手。我们去的时候因为只有我们俩,最后漂流的时候加上教练也就仨,没有人多那么刺激。不过咱也没虚过谁,以仨人之力挑战其它5~7人皮划艇,玩得不亦乐乎。阿勇河上有很多小瀑布,我们教练就是自嗨型的人,直接下去把我们推到瀑布下,哇,透心凉。 阿勇河很长,大概要漂流2个小时,感觉还是不过瘾啊,哈哈。 晚上是我们在巴厘岛住的最后一晚了,选择了网红酒店阿卡萨别墅,就好好的享受最后一晚把。晚上我们又去了Warang Ipang,回味一下。我们还定了一个漂浮早餐,感受一下。 不过就个人感觉而言,该别墅不推荐。屋里什么都要付费,厕所像是没有清洗过一样,不太干净。最后我们想打个前台电话,翻遍了手册,没找到有用的信息(因为里面全是付费清单),清单还翻译成了中文。最后还是按酒店常规配置按0拨过去的。 Day8 最后一天也贼精彩。 我们享受了我们的漂浮早餐。满满的活力,出发,回家。 司机一大早就到酒店,接上我们就直接去机场了。8天的巴厘岛之行就这样结束了。可事情没完… 抵达新加坡,我们需要去领取她朋友在网上订购的化妆品。因为时间的问题,我们大概只有1个多小时的逗留时间。当时的时间是非常紧张的,再加上网络的问题,非常着急。iShop柜台在哪?怎么又有T1和T2?我们先去T1还是T2?怎么去T1?我们拿着网站给的地图,着急的寻找着。最后还是到柜台咨询才找到方向,然后决定先去T1,到了T1柜台处,一询问:天,原来每个航站楼都有一个柜台,他们会根据你的航班将物品放在指定的iShop柜台。 好家伙,我们再返回T2,赶紧跑到柜台。当时就震惊了,这么多人。没办法,只能抱歉的插个位置了,对服务人员说航班即将起飞,能否帮我们先处理。很nice的小姐姐,帮我们一一核对清单。登上飞往成都的航班。 还掉WIFI,巴厘岛之行圆满落幕! 本文链接:https://deepzz.com/post/we-are-traveled-in-bali.html,参与评论 »
第一次出国自由行,心里难免有些慌张,哈。希望在这里记录下本次出行的经验,做一个模版供下次出行参考。 三要素 和写小说一样,地点,人物、时间是我认为最重要的三个要素。 地点,首先确定地点,有些地方真的值得前去。 安全,安全永远放在第一位。 目的,美景、美食、美人,总之这个地方一定要有吸引你的地方。 交通,包车,租车,还是公共交通。当地交通的便利性决定了我们旅行的欢乐值。 物价,当地物价,基于当地物价及预算需要确定是否前去。 (国际)语言,英语,日语…语言是我们交流的工具,也是我们重要的考虑因素。 人物,其次是人物,有同样目的地的人有时更容易达成目标。 人数,总共多少人,关系着定机票、门票、酒店、包车大小。 关系,人物关系,房间是安排亲子房还是其它,以及本次旅行的尴尬度。 年龄,年龄大小,决定了本次游玩的项目危险程度,疲劳程度,或者安排分支行程。 时间,最后确定时间,时间是开始也是结尾。 旅行天数,旅行时间一定要安排好,不能匆忙,一般5天以上为最佳。 出发日期,确定出发日期便于定机票,酒店及其它日程安排。根据当地天气及人物假期恰当安排。 三者都确定好之后,我们需要根据这些情况做一个大概的预算。然后着手我们的旅行了。 提前准备 哈,旅行之前我们是需要准备好一些东西的。等一切准备就绪,带着人和行李就可以了。 机票,提前定好机票。国际旅行一定确定是否转机,行李是否直挂。 酒店,为了不每天的奔波及找酒店。有一个好的行程安排是轻松旅行的关键。酒店最好定在景点的周边,这样节省时间体力。 APP安装,旅行(飞猪,携程,马蜂窝),地图(谷歌,高德),民宿(airbnb,途家),出行(滴滴,Uber,grab),翻译(谷歌,百度), (国际)资料,护照复印件,旅行线路图,打印酒店订单。 (国际)电话,当地大使馆紧急联络方式(地址,电话,邮件等)。当地紧急电话(报警,救护车,火警等)。 (国际)货币,是到当地兑换还是提前国内兑换(参考汇率)。一般可以先在国内兑换美元,再到当地用美元兑现当地货币。 所有东西都准备好了,就等着人带着你的行李出发啦。 物料准备 当我们的资料准备好了之后,机票、酒店均已定好。那么现在只却人过去了,当然人也得带着自己的随身行李了。 必备证件,切记一定把自己的证件带全,最好放在一个专用的包里。 身份证 学生证 驾驶证 银行卡 (国际)护照 (国际)信用卡 (国际)驾驶证 日常衣物,请根据当地气候天气携带日常衣物。 常用物品: 墨镜 内衣裤(一次性) 拖鞋(一次性) 春夏: T恤 短裤 防嗮衫 牛仔裤 泳装 袜子(船袜,短袜) 帽子(草帽,鸭舌帽) 连衣裙 吊带 鞋(运动鞋,小白鞋等) 秋冬: 羽绒服 毛衣 秋衣秋裤 围巾 手套 雪地靴 帽子 口罩 长袜 洗漱用品,最好携带洗漱用品,很多酒店民宿都不提供。 牙刷牙膏 一次性毛巾 洗发水、护发素、沐浴露 剃须刀 护肤用品,现在皮肤娇贵得很,一定记得携带护肤用品。 水、乳、霜、精华 面膜 防嗮霜(喷雾) 洗面奶、卸妆油 化妆品等 备用药品,出门在外,总有磕磕碰碰,随身携带些药品备用。 创口贴 感冒药 消炎药 晕船药 藿香正气胶囊 驱蚊药 设备器材,携带什么样的设备代表你旅行的目的,嘿嘿。 手机(&防水套) 相机(&三脚架) Kindle iPad 雨伞 充电宝 充电器(手机,相机,电脑等) 电脑 对讲机 (国际)电源转换插头(欧标、美标等) (国际)变压器,国内是220v,不过充电器一般都是有适配范围。 (国际)网络通讯,一定有随时能通信的方式。 (国际)手机漫游,开通手机漫游业务。 (国际)购买当地电话卡,这将是一个非常方便的做法。 (国际)WiFi 租赁,考虑方便性,可以租赁个。 行程安排 提前计划好旅行的行程,拒绝盲目。一般建议定好前两天的行程,然后根据状态选择后面的行程。 如: 2-10,双流T1-樟宜T2 23:20-04:10 2-11,樟宜T2-伍拉赖I 06:55-09:35 库塔海滩馨乐庭酒店 2-12,佩尼达深度游 库塔海滩馨乐庭酒店 2-13,…… 2-14,…… 2-15,伍拉赖l-樟宜 13:00-15:40 樟宜T2-双流T1 17:54-22:20 本文链接:https://deepzz.com/post/our-travel-template.html,参与评论 »
接上篇文章 Go 单元测试,基准测试,http 测试。本篇文章介绍 Go 测试工具 go test,包括各种子命令、参数之类的内容。你可以通过 go test -h 查看帮助信息。 其基本形式是: go test [build/test flags] [packages] [build/test flags & test binary flags] 执行 go test 命令,它会在 *_test.go 中寻找 test 测试、benchmark 基准 和 examples 示例 函数。测试函数必须以 TestXXX 的函数名出现(XXX 为以非小写字母开头),基准函数必须以 BenchmarkXXX 的函数名出现,示例函数必须以 ExampleXXX 的形式。三种函数类似下面的签名形式: // test 测试函数 func TestXXX(t *testing.T) { ... } // benchmark 基准函数 func BenchmarkXXX(b *testing.B) { ... } // examples 示例函数,其相关命名方式可以查看第一篇文章 func ExamplePrintln() { Println("The output of\nthis example.") // Output: The output of // this example. } 或 func ExamplePerm() { for _, value := range Perm(4) { fmt.Println(value) } // Unordered output: 4 // 2 // 1 // 3 // 0 } 更多请查看 go help testfunc。 go test 命令还会忽略 testdata 目录,该目录用来保存测试需要用到的辅助数据。 go test 有两种运行模式: 1、本地目录模式,在没有包参数(例如 go test 或 go test -v)调用时发生。在此模式下,go test 编译当前目录中找到的包和测试,然后运行测试二进制文件。在这种模式下,caching 是禁用的。在包测试完成后,go test 打印一个概要行,显示测试状态、包名和运行时间。 2、包列表模式,在使用显示包参数调用 go test 时发生(例如 go test math,go test ./... 甚至是 go test .)。在此模式下,go 测试编译并测试在命令上列出的每个包。如果一个包测试通过,go test 只打印最终的 ok 总结行。如果一个包测试失败,go test 将输出完整的测试输出。如果使用 -bench 或 -v 标志,则 go test 会输出完整的输出,甚至是通过包测试,以显示所请求的基准测试结果或详细日志记录。 下面详细说明下 go test 的具体用法,flag 的作用及一些相关例子。需要说明的是:一些 flag 支持 go test 命令和编译后的二进制测试文件。它们都能识别加 -test. 前缀的 flag,如 go test -test.v,但编译后的二进制文件必须加前缀 ./sum.test -test.bench=.。 有以下测试文件 sum.go: package sum func Sum(a, b int) int { return a + b } sum_test.go 内容: package sum import ( "flag" "fmt" "testing" "time" ) var print bool func init() { flag.BoolVar(&print, "p", false, "print test log") flag.Parse() } func TestSum(t *testing.T) { val := Sum(1, 2) if print { fmt.Println("sum=", val) } } // -bench 基准测试 func BenchmarkSum(b *testing.B) { for i := 0; i $GOPATH/src/test/sum $ go test -coverpkg test/sum -cpu 1,2,4:指定测试或基准测试的 GOMAXPROCS 值。默认为 GOMAXPROCS 的当前值。 -list regexp:列出与正则表达式匹配的测试、基准测试或 Examples。只列出顶级测试(不列出子测试),不运行测试。 $ go test -list Sum -parallel n:允许并行执行通过调用 t.Parallel 的测试函数的最大次数。默认值为 GOMAXPROCS 的值。-parallel 仅适用于单个二进制测试文件,但go test命令可以通过指定 -p 并行测试不同的包。查看 go help build。 $ go test -run=TestSumParallel -parallel=2 -run regexp:只运行与正则表达式匹配的测试和Examples。我们可以通过 / 来指定测试子函数。go test Foo/A=,会先去匹配并执行 Foo 函数,再查找子函数。 $ go test -v -run TestSumSubTest/1+ -short:缩短长时间运行的测试的测试时间。默认关闭。 $ go test -short -timeout d:如果二进制测试文件执行时间过长,panic。默认10分钟(10m)。 $ go test -run TestSumLongTime -timeout 1s -v:详细输出,运行期间所有测试的日志。 $ go test -v analyze flag 以下测试适用于 go test 和测试二进制文件: -benchmem:打印用于基准的内存分配统计数据。 $ go test -bench=. -benchmem $ ./sum.test -test.bench -test.benchmem -blockprofile block.out:当所有的测试都完成时,在指定的文件中写入一个 goroutine 阻塞概要文件。指定 -c,将写入测试二进制文件。 $ go test -v -cpuprofile=prof.out $ go tool pprof prof.out -blockprofilerate n:goroutine 阻塞时候打点的纳秒数。默认不设置就相当于 -test.blockprofilerate=1,每一纳秒都打点记录一下。 -coverprofile cover.out:在所有测试通过后,将覆盖概要文件写到文件中。设置过 -cover。 -cpuprofile cpu.out:在退出之前,将一个 CPU 概要文件写入指定的文件。 -memprofile mem.out:在所有测试通过后,将内存概要文件写到文件中。 -memprofilerate n:开启更精确的内存配置。如果为 1,将会记录所有内存分配到 profile。 $ go test -memprofile mem.out -memprofilerate 1 $ go tool pprof mem.out -mutexprofile mutex.out:当所有的测试都完成时,在指定的文件中写入一个互斥锁争用概要文件。指定 -c,将写入测试二进制文件。 -mutexprofilefraction n:样本 1 在 n 个堆栈中,goroutines 持有 a,争用互斥锁。 -outputdir directory:在指定的目录中放置输出文件,默认情况下,go test 正在运行的目录。 -trace trace.out:在退出之前,将执行跟踪写入指定文件。 本文链接:https://deepzz.com/post/the-command-flag-of-go-test.html,参与评论 »
对我们程序员来说,如何提高代码质量一定是我们的重中之重。不仅需要你能够写得一手的业务代码,还需要做的是如何保证你的代码质量。测试用例便是一个非常好的用来提高我们代码质量的工具。 通过测试,我们能够及时的发现我们程序的设计逻辑错误,并能够给接手项目的其它程序员同学理解函数有帮助。 本篇文章主要介绍 Go 语言中的 testing 包。它要求我们以 *_test.go 新建文件,并在文件中以 TestXxx 命名函数。然后再通过 go test [flags] [packages] 执行函数。 $ ls db.go db_test.go $ cat db_test.go package db import "testing" func TestGetUser(t *testing.T) { user, err := GetUser("test@example.com") if err != nil { t.Fatal(err) } t.Log(user) } 它也为我们提供了三种类型的函数:测试函数 T、基准测试函数 B、实例函数 Example。 Test 测试 函数测试,其基本签名是: func TestName(t *testing.T){ // ... } 测试函数的名字必须以 Test 开头,可选的后缀名必须不以小写字母开头,一般跟我们测试的函数名。 类型 testing.T 有以下方法: // 打印日志。对于测试,会在失败或指定 -test.v 标志时打印。对与基准测试,总是打印,避免因未指定 -test.v 带来的测试不准确 func (c *T) Log(args ...interface{}) func (c *T) Logf(format string, args ...interface{}) // 标记函数失败,继续执行该函数 func (c *T) Fail() // 标记函数失败,调用 runtime.Goexit 退出该函数。但继续执行其它函数或基准测试。 func (c *T) FailNow() // 返回函数是否失败 func (c *T) Failed() bool // 等同于 t.Log + t.Fail func (c *T) Error(args ...interface{}) // 等同于 t.Logf + t.Fail func (c *T) Errorf(format string, args ...interface{}) // 等同于 t.Log + t.FailNow func (c *T) Fatal(args ...interface{}) // 等同于 t.Logf + t.FailNow func (c *T) Fatalf(format string, args ...interface{}) // 将调用函数标记标记为测试助手函数。 func (c *T) Helper() // 返回正在运行的测试或基准测试的名称 func (c *T) Name() string // 用于表示当前测试只会与其他带有 Parallel 方法的测试并行进行测试。 func (t *T) Parallel() // 执行名字为 name 的子测试 f,并报告 f 在执行过程中是否失败 // Run 会阻塞到 f 的所有并行测试执行完毕。 func (t *T) Run(name string, f func(t *T)) bool // 相当于 t.Log + t. SkipNow func (c *T) Skip(args ...interface{}) // 将测试标记为跳过,并调用 runtime.Goexit 退出该测试。继续执行其它测试或基准测试 func (c *T) SkipNow() // 相当于 t.Logf + t.SkipNow func (c *T) Skipf(format string, args ...interface{}) // 报告该测试是否是忽略 func (c *T) Skipped() bool Benchmark 测试 函数测试,其基本签名是: func BenchmarkName(b *testing.B){ // ... } 测试函数的名字必须以 Benchmark 开头,可选的后缀名必须不以小写字母开头,一般跟我们测试的函数名。 B 类型有一个参数 N,它可以用来只是基准测试的迭代运行的次数。基准测试与测试,基准测试总是会输出日志。 type B struct { N int // contains filtered or unexported fields } 基准测试较测试多了些函数: func (c *B) Log(args ...interface{}) func (c *B) Logf(format string, args ...interface{}) func (c *B) Fail() func (c *B) FailNow() func (c *B) Failed() bool func (c *B) Error(args ...interface{}) func (c *B) Errorf(format string, args ...interface{}) func (c *B) Fatal(args ...interface{}) func (c *B) Fatalf(format string, args ...interface{}) func (c *B) Helper() func (c *B) Name() string func (b *B) Run(name string, f func(b *B)) bool func (c *B) Skip(args ...interface{}) func (c *B) SkipNow() func (c *B) Skipf(format string, args ...interface{}) func (c *B) Skipped() bool // 打开当前基准测试的内存统计功能,与使用 -test.benchmem 设置类似, // 但 ReportAllocs 只影响那些调用了该函数的基准测试。 func (b *B) ReportAllocs() // 对已经逝去的基准测试时间以及内存分配计数器进行清零。对于正在运行中的计时器,这个方法不会产生任何效果。 func (b *B) ResetTimer() 例: func BenchmarkBigLen(b *testing.B) { big := NewBig() b.ResetTimer() for i := 0; i t.Run("A=1", func(t *testing.T) { ... }) t.Run("A=2", func(t *testing.T) { ... }) t.Run("B=1", func(t *testing.T) { ... }) // } 每个子测试可以用一个唯一的名字表示:顶级测试的名称和传递给 Run 的名称序的组合,用 / 分隔。 go test -run '' # 运行所有测试 go test -run Foo # 匹配 Foo 相关的顶级测试,如 TestFooBar go test -run Foo/A= # 匹配 Foo 相关的顶级测试, 并匹配子测试 A= go test -run /A=1 # 匹配所有顶级测试, 并匹配它们的子测试 A=1 子测试也可以用来控制并行性。父级测试只有在完成所有子测试后才能完成。在这个例子中,所有的测试都是相互平行的,并且只与对方一起运行,而不管可能定义的其它顶级测试: func TestGroupedParallel(t *testing.T) { for _, tc := range tests { tc := tc // capture range variable t.Run(tc.Name, func(t *testing.T) { t.Parallel() ... }) } } 运行直到并行子测试完成才会返回,这提供了一种在一组并行测试后进行清理的方法: func TestTeardownParallel(t *testing.T) { // This Run will not return until the parallel tests finish. t.Run("group", func(t *testing.T) { t.Run("Test1", parallelTest1) t.Run("Test2", parallelTest2) t.Run("Test3", parallelTest3) }) // } Main 测试 有时候我们也需要从主函数开始进行测试: func TestMain(m *testing.M) 例: func TestMain(m *testing.M) { // call flag.Parse() here if TestMain uses flags os.Exit(m.Run()) } HTTP 测试 Go 语言目前的 web 开发是比较多的,那么在我们对功能函数有了测试之后,HTTP 的测试又该怎样做呢? Go 的标准库为我们提供了一个 httptest 的库,通过它就能够轻松的完成 HTTP 的测试。 1、测试 Handle 函数 package main import ( "fmt" "io" "io/ioutil" "net/http" "net/http/httptest" ) var HandleHelloWorld = func(w http.ResponseWriter, r *http.Request) { io.WriteString(w, "Hello World!") } func main() { req := httptest.NewRequest("GET", "http://example.com/foo", nil) w := httptest.NewRecorder() HandleHelloWorld(w, req) resp := w.Result() body, _ := ioutil.ReadAll(resp.Body) fmt.Println(resp.StatusCode) fmt.Println(resp.Header.Get("Content-Type")) fmt.Println(string(body)) } 2、TLS 服务器? package main import ( "fmt" "io/ioutil" "log" "net/http" "net/http/httptest" ) func main() { ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, "Hello, client") })) defer ts.Close() client := ts.Client() res, err := client.Get(ts.URL) if err != nil { log.Fatal(err) } greeting, err := ioutil.ReadAll(res.Body) res.Body.Close() if err != nil { log.Fatal(err) } fmt.Printf("%s", greeting) } 3、常用的 HTTP 框架又如何测试? // Package main provides ... package main import ( "fmt" "net/http" "net/http/httptest" "github.com/gin-gonic/gin" ) func main() { engine := gin.Default() engine.GET("/hello", func(c *gin.Context) { c.String(http.StatusOK, "Hello") }) engine.GET("/world", func(c *gin.Context) { c.String(http.StatusOK, "world") }) req := httptest.NewRequest(http.MethodGet, "/hello", nil) w := httptest.NewRecorder() engine.ServeHTTP(w, req) fmt.Println(w.Body.String()) } 本文链接:https://deepzz.com/post/study-golang-test.html,参与评论 »
什么是 OAuth 2.0 OAuth 2.0 是一个行业的标准授权协议。OAuth 2.0 专注于简化客户端开发人员,同时为 Web 应用程序,桌面应用程序,手机和客厅设备提供特定的授权流程。 它的最终目的是为第三方应用颁发一个有时效性的令牌 token。使得第三方应用能够通过该令牌获取相关的资源。常见的场景就是:第三方登录。当你想要登录某个论坛,但没有账号,而这个论坛接入了如 QQ、Facebook 等登录功能,在你使用 QQ 登录的过程中就使用的 OAuth 2.0 协议。 如果你想了解更多,其官方网址为:https://oauth.net/2/。下面我们来了解下 OAuth 协议的基本原理 角色 首先需要介绍的是 OAuth 2.0 协议中的一些角色,整个授权协议的流程都将围绕着这些角色: resource owner,资源所有者,能够允许访问受保护资源的实体。如果是个人,被称为 end-user。 resource server,资源服务器,托管受保护资源的服务器。 client,客户端,使用资源所有者的授权代表资源所有者发起对受保护资源的请求的应用程序。如:web网站,移动应用等。 authorization server,授权服务器,能够向客户端颁发令牌。 user-agent,用户代理,帮助资源所有者与客户端沟通的工具,一般为 web 浏览器,移动 APP 等。 可能有些朋友依然不太理解,这里举例说明:假如我想要在 coding.net 这个网站上用 github.com 的账号登录。那么 coding 相对于 github 就是一个客户端。而我们用什么操作的呢?浏览器,这就是一个用户代理。当从 github 的授权服务器获得 token 后,coding 是需要请求 github 账号信息的,从哪请求?从 github 的资源服务器。 协议流程 上图详细的描述了这四个角色之间的步骤流程: (A) Client 请求 Resource Owner 的授权。授权请求可以直接向 Resource Owner 请求,也可以通过 Authorization Server 间接的进行。 (B) Client 获得授权许可。 © Client 向 Authorization Server 请求访问令牌。 (D) Authorization Server 验证授权许可,如果有效则颁发访问令牌。 (E) Client 通过访问令牌从 Resource Server 请求受保护资源。 (F) Resource Server 验证访问令牌,有效则响应请求。 +--------+ +---------------+ | |--(A)- Authorization Request ->| Resource | | | | Owner | | || Authorization | | Client | | Server | | || Resource | | | | Server | | || | | User- | | Authorization | | Agent -+----(B)-- User authenticates --->| Server | | | | | | -+----(C)-- Authorization Code ------(D)-- Authorization Code ---------' | | Client | & Redirection URI | | | | | || | | | | | | || | | | | | | | | | | || | | | | | | | | | | || | | | | | | || | | User- | | Authorization | | Agent -|----(B)-- User authenticates -->| Server | | | | | | || Web-Hosted | | | without Fragment | Client | | | | Resource | | (F) |--(B)---- Resource Owner ------->| | | | Password Credentials | Authorization | | Client | | Server | | |--(A)- Client Authentication --->| Authorization | | Client | | Server | | |<--(B)---- Access Token ---------<| | | | | | +---------+ +---------------+ Figure 6: Client Credentials Flow POST 请求 客户端凭证流程: https://oauth.example.com/token?grant_type=client_credentials&client_id=CLIENT_ID&client_secret=CLIENT_SECRET 字段 描述 grant_type 必须。必须设置到客户端证书中。 scope 可选。授权的作用域。 返回值 { "access_token" : "...", "token_type" : "...", "expires_in" : "...", } 如果授权服务器验证成功,那么将直接返回令牌 token,改客户端已被授权。 参考网站 [1] https://developers.douban.com/wiki/?title=oauth2 [2] https://www.digitalocean.com/community/tutorials/an-introduction-to-oauth-2 [3] https://tools.ietf.org/html/rfc6749 [4] https://oauth.net/2/ 本文链接:https://deepzz.com/post/what-is-oauth2-protocol.html,参与评论 »
自 2017.04 CAB 论坛通过投票决定:2017.09.08 起,所有 CA 机构颁发 SSL 证书前必须对颁发的域名进行 CAA 检测。CAA 就时不时出现在技术人员的眼前。那么 CAA 记录是什么?又有什么作用呢?如何添加 CAA 记录? 什么是 CAA CAA(Certification Authority Authorization)是一种 DNS 记录,它被定义在 RFC6844,其目的是用来指定域名允许哪个证书颁发机构(CA)为其颁发证书。防止钓鱼攻击者使用该域名申请 SSL 证书。 它们还提供了一种方法来指示通知规则,以防有人从未经授权的 CA 颁发证书。在没有 CAA 记录的情况下,所有 CA 均可为该域名颁发证书。当然如果存在 CAA 记录,CA 必须遵守规则,只能是在记录列表中的 CA 才被允许。 CAA记录可以为整个域或特定主机名设置策略。CAA 记录也被子域继承,因此 CAA 记录集 example.com 也将适用于任何子域,例如 subdomain.example.com(除非被覆盖)。CAA 记录可以控制发行单域名证书,通配符证书或同时。 CAA 记录格式 CAA 记录由以下元素组成: 标签 描述 flag 0-255 之间的无符号整数 tag 用来表示关键标志,RFC 有定义 value 与 tag 关联的值 CAA 记录的规范的表示法是: CAA RFC 目前定义了 3 个可用的 tag: issue:明确授权单个证书颁发机构颁发主机名的证书(任何类型)。 issuewild:明确授权单个证书颁发机构为主机名颁发通配符证书(只有通配符)。 iodef:指定认证机构可以向其报告策略违规的URL或邮箱。 我们先来尝试看看 CAA 到底是什么样的: $ dig dnsimple.com caa ; > DiG 9.11.2 > dnsimple.com caa ;; global options: +cmd ;; Got answer: ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 26969 ;; flags: qr rd ra; QUERY: 1, ANSWER: 5, AUTHORITY: 0, ADDITIONAL: 0 ;; QUESTION SECTION: ;dnsimple.com. IN CAA ;; ANSWER SECTION: dnsimple.com. 3600 IN CAA 0 issue "amazonaws.com" dnsimple.com. 3600 IN CAA 0 issue "comodoca.com" dnsimple.com. 3600 IN CAA 0 iodef "mailto:ops@dnsimple.com" dnsimple.com. 3600 IN CAA 0 issue "letsencrypt.org" dnsimple.com. 3600 IN CAA 0 issuewild "comodoca.com" ;; Query time: 176 msec ;; SERVER: 223.5.5.5#53(223.5.5.5) ;; WHEN: Sun Feb 04 13:18:35 CST 2018 ;; MSG SIZE rcvd: 204 请注意这里的 dig 的版本,较低的版本是不支持 CAA 记录的查询的,请悉知。另外,dig 是不支持继承的查询方式。 查询规则 通过查看 CAB 论坛的帖子来看,他们清晰的定义了 CAA 记录的查询规则,具体如下: 名称 含义 CAA(X) 查询标签 X 返回的结果。 P(X) 是 DNS 层次结构中位于 X 之上的 DNS 标签。 A(X) 在标签 X 处指定的 CNAME 或 DNAME 记录的目标。 如果 CAA(X) 不为空,则 R(X) = CAA(X),否则 如果 A(X) 不为空,并且 CAA( A(X) ) 不为空,那么 R(X) = CAA( A(X) ),否则 如果 X 不是顶级域名,那么 R(X) = R( P(X) ),否则 R(X) 为空 因此,当节点处的搜索返回 CNAME 记录时,CA 将沿着 CNAME 记录链到达其目标。如果目标标签含 CAA 记录,则返回。否则,CA 继续在节点 X 的父节点处进行搜索。 注意,搜索不包括 CNAME 记录的目标的父级(除非 CNAME 指向自己的路径)。 为了防止资源耗尽攻击,CA 应该限制被接受的 CNAME 链的长度。但 CA 必须处理包含 8 个或更少的 CNAME 记录的 CNAME 链。 这里举例说明。例如,如果我们想要限制颁发 example.com 给 Let’s Encrypt 证书颁发机构的 SSL 证书,我们应该添加以下 CAA 记录: example.com. CAA 0 issue "letsencrypt.org" 如果我们想允许 Let’s Encrypt 和 Comodo,我们应该添加 2 个 CAA 记录,每个 CA 记录一个: example.com. CAA 0 issue "comodoca.com" example.com. CAA 0 issue "letsencrypt.org" 如果我们想让 Let’s Encrypt 并且 Comodo 只用于通配符,那么我们可以使用 issuewild: example.com. CAA 0 issue "letsencrypt.org" example.com. CAA 0 issuewild "comodoca.com" 请注意,issuewild 的存在将覆盖 issue。因此,Let’s Encrypt 不允许发出通配符证书(不管他们不支持这种类型的证书)。 最后,要获得违反政策的通知,您可以添加一个带有 iodef 标记的记录,其中包含要通知的电子邮件地址: example.com. CAA 0 iodef "mailto:example@example.com" 如前所述,记录是由子主机名继承的。我们来看一个子域配置的例子: example.com. CAA 0 issue "letsencrypt.org" alpha.example.com. CAA 0 issue "comodoca.com" beta.example.com. CAA 0 issue "letsencrypt.org" beta.example.com. CAA 0 issue "comodoca.com" 在上面的示例中,Let’s Encrypt 是 example.com 域的默认 CA。但是,只有 Comodo 才能颁发证书 alpha.example.com。Comodo 和 Let’s Encrypt 都可以颁发证书 beta.example.com。那么 foo.example.com 呢?因为没有记录存在 foo.example.com,但有一个记录 example.com,在这种情况下,只有 Let’s Encrypt 将被允许发出 foo.example.com。 相关工具 myssl:如果你想查看某个域名是否添加 CAA 记录,你可以到这里检测(实现链式的查询)。 sslmate:CAA 记录自助生成,如果你不知道 CAA 记录是什么样,你可以在这里生成,然后添加到你的 DNS 上。 FAQ 1、为什么我添加的 CAA 记录没有生效? 2、有哪些支持 CAA 记录的 DNS 服务商? CloudXNS AliYun CloudDNS Cloudflare DNSimple 本文链接:https://deepzz.com/post/what-is-caa-record-in-dns.html,参与评论 »
如果你读了docker volume 容器卷的那些事(一),我想应该不会遇到下面这些问题的,毕竟是具有指导意义的。本篇文章的内容依旧是有关 volume 的内容,主要讲诉的是如何解决非 root 用户下的文件映射问题。博主将自己常遇到的一些问题总结如下。 事情要从博主使用 prometheus 说起。当时博主使用的执行脚本类似下面这种: $ docker run --rm \ --name prometheus \ -p 9090:9090 \ -v "$(pwd)"/data:/prometheus \ prom/prometheus:v2.0.0 应该是在其版本 2.0.0 之前,博主使用 prometheus 一切正常。突然有一天冒出这样的错误: level=info ts=2017-12-22T12:40:09.154479277Z caller=main.go:314 msg="Starting TSDB" level=error ts=2017-12-22T12:40:09.154587496Z caller=main.go:323 msg="Opening storage failed" err="open DB in /prometheus: open /prometheus/872424405: permission denied" 什么情况!发生了什么?没有权限?明明没有该执行脚本,不应该的啊。这才想起来咱刚刚更新过 prometheus 镜像的版本(该版本优化很大,故及时跟进)。没办法,看看它的 Dockerfile 更新了什么 #Use user nobody in Dockerfile。在 Dockerfile 中明显的看到: USER nobody 从以前的 root 用户切换到了 nobody 用户(为了安全考虑)。 而我们映射的目录: drwxr-sr-x 2 root root 40 Dec 5 02:41 data/ 看到我们的 data 目录的拥有者依然是 root 用户,权限的问题必然出现了。 那么,如果你依然固执的要这样做(不使用命名容器卷)。这里提供了几种解决的办法,供参考。 在某些情况下,即使使用下面方法也不能达到效果,可能你需要尝试关闭 SELinux:setenforce 0(临时关闭) 更改目录拥有者 是的,非常容易的想到,既然这个映射出来的文件夹所有者不是 nobody,我给它改成 nobody 不就可以了吗? 首先,我们找到 nobody 用户的 id: # 找到它的原始镜像执行命令。 $ docker run --rm quay.io/prometheus/busybox cat /etc/passwd ... nobody:x:65534:65534:nobody:/home:/bin/false 发现,其 id 为 65534(其实这些用户uid是约定的),执行如下命令: $ sudo chown -R 65534 data $ ls -al data drwxr-sr-x 3 65534 root 60 Dec 22 12:59 data/ 可以看到 data 目录的所有者已经改为了 uid 为 65534 的用户。再次执行运行 prometheus 的脚本,成功。 Data Container 是的,你可以使用 Data Container 的方式进行容器卷的共享,这样也能够解决权限的问题。其基本运行方式是: # 声明一个容器卷 /data,并在 /data 目录下新建 a.txt 文件 $ docker run --name data_container -v /data alpine touch /data/a.txt # 挂载容器卷,查看 /data 目录下的内容 $ docker run --volumes-from container_name alpine ls /data a.txt 当执行第二条命令时,你会看到了 a.txt 文件,说明挂载数据容器成功了。 需要说明的是,最好用同一个镜像运行数据容器,这样才能保证两者的 UID 一致,然也会出现权限问题。数据容器应该是执行一条命令就退出。 再把前面 prometheus 的例子拿来实践一下。首先,在 prometheus 的 Dockerfile 中我们看到: # 声明容器卷 VOLUME [ "/prometheus" ] ... # 入口 ENTRYPOINT [ "/bin/prometheus" ] 原来 prom/prometheus 镜像就声明了一个容器卷,那么我们就不必再多次一举了。但我们需要覆盖 ENTRYPOINT 指令。 $ docker run --name data_container --entrypoint="" prom/prometheus:v2.0.0 ls 然后再次执行: $ docker run --rm \ --name prometheus \ -p 9090:9090 \ --volumes-from data_container \ prom/prometheus:v2.0.0 成功。 切换用户 有没有更好的方式去实现呢?有的,这种方式较第一种优点是自动化,不需要手动更改文件权限。具体流程是: 切换为 root 用户。 更改目录权限到当前非 root 用户。 用 gosu 以非 root 用户执行命令。 这里需要自行书写 Dockerfile 构建镜像。具体实现类似下面,新建 Dockerfile: FROM prom/prometheus:v2.0.0 USER root RUN mkdir -p /usr/local/bin \ && wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/1.10/gosu-amd64" \ && chmod +x /usr/local/bin/gosu COPY entrypoint.sh /entrypoint.sh ENTRYPOINT ["/entrypoint.sh"] CMD [ "--config.file=/etc/prometheus/prometheus.yml", \ "--storage.tsdb.path=/prometheus", \ "--web.console.libraries=/usr/share/prometheus/console_libraries", \ "--web.console.templates=/usr/share/prometheus/consoles" ] 其中 entrypoint.sh 的内容如下,它的目的就是将我们的目录的权限改成非 root 用户的权限: #!/bin/sh chown -R nobody /prometheus gosu nobody prometheus "$@" 这里提到了 gosu 工具,用它来替换 sudo 从某种意义上解决了信号传递和 TTY 的问题,更多详情到其项目首页。 然后我们构建镜像,执行最初的运行脚本,成功。我们查看下映射到宿主机上的目录: $ ls -al data drwxr-sr-x 3 nobody root 80 Jan 11 11:09 data # 进入容器查看进程 $ ps PID USER TIME COMMAND 1 root 0:00 {entrypoint.sh} /bin/sh /entrypoint.sh ... 6 nobody 0:00 prometheus --config.file=/etc/prometheus/prometheus.yml ... 注意,standard_init_linux.go:195: exec user process caused "exec format error" 得到这个错误,可能是你没有指定运行 entrypoint.sh 的 shebang。指定如 #!/bin/sh 即可。 参考文章 [1] https://denibertovic.com/posts/handling-permissions-with-docker-volumes/ https://segmentfault.com/a/1190000004527476 本文链接:https://deepzz.com/post/the-docker-volumes-permissions.html,参与评论 »
您可以订阅此RSS以获取更多信息