这是本节的多页打印视图。
点击此处打印 .
返回本页常规视图 .
Blog
个人随笔
Thursday, June 27, 2024
随笔就是随便写写, 简短的, 未经严肃思考推敲的, 临时起意的, 无关紧要的胡思乱想.
一些技术博客要写定义, 写前因后果, 篇幅会比较长, 写的很累, 也很费时间.
简短的技术博客也会放在这里, 但是不会写太多背景, 在随笔写的东西都会比较轻松.
OKR的陷阱与助力
Tuesday, June 27, 2023
OKR 的陷阱与助力
2009 年,哈佛商学院发表了一篇名为《疯狂目标》(Goals Gone Wild)的论文。文章用一系列例子解释了“过度追求目标的破坏性”:福特平托(Pinto)汽车油箱爆炸、西尔斯汽车维修中心的漫天要价、安然公司疯狂膨胀的销售目标,以及 1996 年造成 8 人死亡的珠穆朗玛峰灾难。作者提醒说:目标就像是“一种需要谨慎使用和严密监管的处方药”。作者甚至还提出这样的警告:“由于聚焦过度、出现不道德行为、冒险行为增多,以及合作意愿和工作积极性下降等原因,目标会在组织内部引发系统性问题。”目标设定的坏处可能会抵消其所带来的好处,这就是这篇论文的观点。
读"这就是 OKR"
在前公司实践过 3 年 OKR, 恰逢新公司现在也要转向 OKR, 老板推荐了这本书这就是 OKR .
花了两周时间才断断续续看完, 简单且主观的分享一点未深思的观后感.
OKR, 原文 objectives and key results, 直译是目标和关键性结果 .
按照谷歌的 OKR 模式, 目标可以分为两种, 承诺型目标, 和愿景型目标. 对待两种类型目标会有不同评价方式. 目标的设立需要仔细思考, 可以参考原书最后一章资源 1 谷歌公司的内部 OKR 模板 或者 这个链接 , 对照阅读.
对关键性结果的设立也需要好好思考, 可以把这个词理解为一个里程碑, 每一次前进时, 朝着最近的里程碑前进, 最终达到目标. 这个里程碑是建议能用数字衡量的, 以此判断自己达到目标与否, 分析产生差距原因.
由于 OKR 里的关键性结果仍然建议能用数字衡量, 那么它和 KPI 的区别在哪. KPI 是 key performance indicator, 关键绩效指标. 很明显, KPI 没有明确的包含目标 .
不考虑目标, 盲目的下发数字任务对企业产生伤害有许多的案例, 书中会举很多例子.
除了解释和推销 OKR, 还有一个很重要的工具, 在书的偏后段提出, 那就是持续性绩效管理 , 使用工具是 CFR, 也就是 Conversations, Feedback, Recognition, 即交流,反馈,认可 .
主要介绍的是主管找普通职员进行交流, 得到反馈, 并且认可他们的表现. 这里话虽好听, 实际场景里, 由于对不同人的工作无法完全的了解, 误会和自以为是总会出现, 因此, 书中推荐更多的进行交流. 什么是"更多"并没有明确的指标. 如何避免"交流"变为"施压", “反馈"变为"抱怨”, “认可"变为"PUA”, 需要交流双方都具有一定的沟通技巧.
此书的下篇提到的持续性绩效管理 , 字面上看和绩效管理更像了, 同时书中多次郑重强调, OKR 的完成度绝不能与薪资待遇挂钩, 否则会导致数字失真, 走回 KPI 伤害企业的老路.
那么实践 OKR 后, 什么指标会影响员工的收入呢, 书中没有给出答案. 按我自己的理解, OKR 相较绩效主要多出目标这个维度, 那么有可能这个目标和公司整体的利益越相关, 越有利于个人的升职加薪. 因此个人在设立目标时, 应该考虑公司的利益, 并且把目标设置为最大化利益的目标. 避免设置为个人利益, 而不利于公司的, 比如考取证书, 锻炼身体, work&life balance. 虽然有点滑稽, 但是见过不少走错路的朋友.
粗暴的绩效管理会伤害企业, 本来是一件可以预期的结果, 反而好奇为什么很多企业坚持使用了 KPI 多年, 它们如今的经营状况如何. 有许多决定并不太经得起推敲, 如果几个逻辑优秀的人在一起好好讨论, 沟通交流, 就更有可能做出更正确的决定.
总结
按照我一贯的标准, 举例子目的应是帮助理解, 不能用于证明观点, 只能证反观点.
此书有以下缺陷:
在证明 KPI 失败上举了一些案例, 但不能证明 KPI 一无是处, 也不能证明凡是有 KPI 的地方都可以通过替换为 OKR 来达到成功.
为证明 OKR 有用, 例举了一些成功的企业做出的部分正确的选择, 但是使用了 OKR 仍然失败的企业更是数不胜数, 如果说失败者们是因为"心不诚"才会失败, 那么 OKR 只不过是另一个玄学而已.
企业的成功依赖很多因素, 例如经营状况, 员工的绩效, 客户的满意度, 客户的支持度等等, 没有哪一项为决定性因素.
存在一些断言, 但是不能证明它们是正确的, 孤立的案例无论成功与否都不能说明什么, 因此不是一本较严谨的书.
虽然书不太严谨, 但从阅读此书中我也仍然有收获, 或许本来就是我自己的想法, 那就是合作的人需要更多的交流, 将透明作为企业文化, 促进众人齐心协力, 这样就可以集到一张"人和"卡.
参考资料
武汉小龙虾市场提供加工服务了
Saturday, May 18, 2024
武汉的小龙虾 市场现在提供处理服务了, 买虾后, 虾摊旁提供免费的虾清洗和处理服务, 有三个人一同处理.
第一批提供虾处理服务的商家, 可以立即获得一些利益, 吸引购买者, 毕竟这是典型的"人无我有 “的优质服务.
但是, 该服务门槛较低, 任何商家叫上三个人都可以提供, 而代价却很高, 毕竟虾处理服务占用了三个劳动力. 如果不能抢占足够多的市场, 这个服务迟早成本高于获利.
只要商贩在这个夏天长期的贩卖小龙虾, 最终会发现这个服务的代价远远高于收益, 但是却无法停止, 因为这个服务已经成为了他们的卖点, 他们的客户已经习惯了这个服务, 一旦停止, 客户就会流失. 你可以一开始就不给客户提供这个服务, 但很难在提供免费服务后撤回.
有的人做生意会讲究一个”多给一点 ", 这种策略自然比"少给一点"更受消费者欢迎, 但它无形中增加了商家的成本, 让商家们陷入低意义低门槛的竞争中, 最终大家都挣不到钱而行业枯萎. 所以有些行业到底是因为服务差而没落, 还是因为服务好而没落, 这个问题值得深思.
许多大企业也有类似亏本赚吆喝的行为, 其目的乃是为了垄断 , 直至有一天市场长只有一家打车, 只有一家团购, 这一天才是到了收割的时候. 但我们也可以发现, 它们都不急于收割, 而是通过算法只收割部分人. 一方面赚取定价权带来的超额利润, 另一方面, 使用新产品的低定价对新入场者围追堵截, 防范每一个潜在的竞争对手. 这些大企业已形成事实上的垄断 , 韭菜割不割只是时间问题.
我们在工作中也会遇到不少"卷王", 很难评估他们是否带来了更多价值, 但能他们明确能做到的就是永远比别人晚下班半小时, 一旦俩卷王较上劲了, “多给一点"互锁, 全办公室都得笼罩在其阴影之下. 他们靠着这种低质的服务, 挤压着正常打工者的生存空间. 比的既不是创新, 也不是业绩, 主打的就是"卖苦力”, 却能获得老板的青睐, 这显然是一种不正常的恶行竞争.
最后再说回小龙虾市场, 有人能垄断定价自行定价, 有人能垄断货源专供高端, 而谁能通过垄断打工而自发打工 呢?
走走停停
Thursday, June 27, 2024
观影
Thursday, June 27, 2024
某厂商防止DNS拦截的办法
Friday, December 13, 2024
近日发觉 DNS 公共服务有 IP 有异常访问行为, 每秒数十次重复的请求一个域名, 完全不遵循 DNS 协议, 不理会全局生存时间 (TTL)值.
开始时以为该 IP 是攻击者, 观察流量后发现, 主要是某厂商的 App 在疯狂请求 DNS. 后端设置的TTL=10
表示接收到的 DNS 查询返回值生命周期为 10 秒, 这 10 秒内请求者都应该使用这个返回值, 而不是再次请求 DNS 服务器. 但该 App 每秒数十个相同请求, 说明该 App 没有按照 DNS 协议正确处理 TTL 值. 后台拦截请求统计里, 有 90%以上的请求都是该域名的请求.
可能该厂商知道有 DNS 拦截的手段, 采取了你不让我访问, 我就让用户 App 直接 DoS 攻击你的 DNS 服务器的方式. 由于后端同时设置了每秒只允许 20 次突发请求, 该莽撞行为同时会影响到用户的其它正常 DNS 查询, 影响其它 App 的正常使用.
运维看到这样单 IP 疯狂请求同一域名的行为, 不想放行也得放行了.
Google翻译API的使用教程
Friday, November 15, 2024
如果你需要使用 API 自动化翻译工作, Google 翻译 API 是一个不错的选择. 它相较 DeepL 翻译质量可能略逊一筹, 但具有更好的性价比, 特别是每月有 50w 字符的免费额度.
产品介绍
Google 翻译大家都用过, 这里介绍的是它的 API 服务, 全称叫做 Google Cloud Translation. 通过 API, 可以实现批量翻译, 自定义翻译模型, 翻译文档等功能.
价格
每月 50w 字符免费额度, 超出部分按字符计费.
基本版和高级版的区别
功能
基本版
高级版
免费额度
50w 字符/月
50w 字符/月
每百万字符
20 美元
80 美元
文档翻译
0.08/页
0.25/页
自定义翻译
✘
✔
开始使用
启用 API, 如果没有启用结算功能, 这里会提示增加结算账户, 需要外币信用卡
$cred = gcloud auth print-access -token
$project_id = "example"
$headers = @ { "Authorization" = "Bearer $cred " }
Invoke-WebRequest `
-Method GET `
-Headers $headers `
-Uri "https://cloudresourcemanager.googleapis.com/v3/projects/ ${project_id} " | Select-Object -Expand Content
$cred = gcloud auth print-access -token
$project_id = "example"
$body = @ {
"sourceLanguageCode" = "en"
"targetLanguageCode" = "zh"
"contents" = @ ( "Hello, world!" )
"mimeType" = "text/plain"
}
$body = $body | ConvertTo-Json
$headers = @ {
"Authorization" = "Bearer $cred "
"Content-Type" = "application/json; charset=utf-8"
"x-goog-user-project" = $project_id
}
Invoke-WebRequest `
-Method POST `
-Headers $headers `
-Uri "https://translation.googleapis.com/v3/projects/ ${project_id} :translateText" `
-Body $body | Select-Object -Expand Content
Linux 使用 curl 命令
export CRED = $( gcloud auth print-access-token)
export PROJECT_ID = "example"
export SOURCE_LANGUAGE_CODE = "en"
export TARGET_LANGUAGE_CODE = "zh"
export CONTENTS = "Hello, world!"
export MIME_TYPE = "text/plain"
curl -X POST -H "Authorization: Bearer $CRED " -H "Content-Type: application/json; charset=utf-8" -H "x-goog-user-project: $PROJECT_ID " -d "{
\"sourceLanguageCode\": \" $SOURCE_LANGUAGE_CODE \",
\"targetLanguageCode\": \" $TARGET_LANGUAGE_CODE \",
\"contents\": [\" $CONTENTS \"],
\"mimeType\": \" $MIME_TYPE \"
}" "https://translation.googleapis.com/v3/projects/ $PROJECT_ID :translateText"
至此, 你已经可以使用 Google 翻译 API 进行批量翻译了.
用途参考
翻译网站或应用
训练自定义翻译模型
为视频添加不同语言的字幕
使用不同语言为视频配音
翻译有格式的文档
实时翻译客户互动内容
扩展阅读
后记
Google 翻译的官方文档冗长, 实现同一功能有多种不通方式, 在认证和调用步骤有多种途径实现, 本文只选取普通用户最建议和最简单的使用方式, 以供参考.
认证种类中选择了本地认证 (gcloud CLI)
使用方式中选择了 REST API (Curl/Invoke-WebRequest)
基本版和高级版中选择了高级版
这是原文发布在blog.jqknono.dev 的原创文章, 未经许可不得转载
社区规则分析
Wednesday, November 13, 2024
亚马逊商店社区规则
Monday, November 13, 2023
亚马逊商店社区规则
社区准则
社区准则的目的是为亚马逊社区保持有帮助、相关、有意义和适当的信息
什么是亚马逊社区?
社区是与其他用户分享您的想法和经历(正面和负面)的地方。以下准则解释了社区允许和不允许发布的内容。
使用社区功能,即表示您已同意我们的使用条件 。并将遵守不时修订的社区准则。社区功能包括:
准则适用于什么
您的社区行为,包括:
分享文字、照片、视频、链接
将评论标记为 “有帮助的”
与其他社区成员以及亚马逊的互动
该指南不适用于在亚马逊上销售的商品或服务中的内容(例如:图书内容)。
谁可以参加
如果您有亚马逊账户,则可以:
创建和更新购物清单、愿望清单和礼品清单
更新您的个人资料页面
参加数码和设备论坛
要执行以下任何操作,您需要在过去 12 个月内使用有效的信用卡或借记卡在 Amazon.cn 上花费至少 20 元人民币:
发表评论(包括星级)
回答买家的问题
提交有帮助的投票
创建心愿单
关注其他用户
促销折扣不计入 20 元人民币的最低消费要求。
**关于定价和供货情况的评论**
如果您的评论与商品的价值相关即可进行评论(例如: “仅售 100 人民币,这款搅拌机真的很棒。” 不允许发表与个人体验相关的定价评论。例如,不要比较不同商店中同一商品的价格: “在这里找到这件商品的价格比我在当地的商店便宜 5 人民币。” 这样的评论是不允许的,因为该评论并非与所有用户都相关。
关于产品货存性的一些评论是允许的。例如,你可以讨论尚未发布的产品形式: “我希望这本书也有平装版。” 但是,我们不允许对特定商店的货存情况发表评论。同样,社区的目的是为了帮助您与其他买家分享产品相关信息的反馈。
**以不受支持的语言编写的内容**
为确保内容有帮助性,我们仅允许您所访问的亚马逊网站所提供支持的语言编写内容。例如,我们不允许在 Amazon.cn 上以法语撰写的评论,因为该亚马逊仅支持中文和英语语言选择。某些亚马逊网站支持多种语言,但不允许使用多种语言编写的内容。了解此亚马逊网站支持哪些语言。
**重复的文字、垃圾邮件、用符号创建的图片**
我们不允许任何以下形式的内容:
多次重复的文字
不含有任何意义的文字
仅使用标点符号或其他符号的内容
ASCII 艺术(用符号和字母创建的图片)
**私人信息**
请勿发布侵犯他人隐私或分享您个人信息的内容。这包括:
电话号码
电子邮件地址
邮寄地址
车牌号
数据源名称 (DSN)
订单号
亵渎、骚扰的内容
我们允许对他人的信念和专业知识具有尊重性的质疑。我们不允许:
亵渎、淫秽、骂人。
骚扰、威胁。
关于危害儿童和青少年的人格的内容。
攻击与您意见不同的人。
侮辱、诽谤或煽动性内容。
掩盖他人的意见。请不要从多个帐户或煽动他人发布相同言论。
**仇恨言论**
不允许基于以下特征对他人表达仇恨言论:
种族
民族或地域
国籍
性别
性别认同
性取向
宗教
年龄
残疾
同样也不允许宣传使用以上言论的组织。
**色情内容**
I 我们允许对在亚马逊上销售的色情和情趣商品进行讨论。带有色情内容的商品(书籍、电影)也是如此。但是我们仍然不允许亵渎或淫秽语言。我们也不允许包含裸露内容和露骨色情图片或相关描述的内容。
**链接**
我们允许链接到亚马逊上的其他商品,但不允许链接到外部网站。不要发布指向钓鱼或其他恶意软件网站的链接。 我们不允许使用带有引荐来源标签或附属代码的网址。
**广告以及宣传性内容**
请勿发布任何以宣传公司、网站、作者或特价为主要目的的内容。
**利益冲突**
不允许创建、编辑或发布有关您自己的产品或服务的内容。以下个人或组织提供的产品和服务也是如此:
**邀请**
如果您要求其他人发布有关您的产品或服务的内容,请保持中立。例如,不要询问或以其他方式试图影响他人留下正面评分或评论。
请勿提供,要求 ,或接受 与创建、编辑或发布内容有关的交换报酬请求。交换报酬形式包括提供免费或折扣商品、退款和赔偿。 不要试图操纵对持有"亚马逊验证购买 " 徽章的用户提供特价或相关补偿。
与品牌商、卖家、作者或艺术家有财务关系或密切的个人关系?
可以发布评论、问题和答案以外的内容,但你需要明确表明您的关系。但是,我们不允许品牌商或企业参与任何将亚马逊用户引导至非亚马逊网站、应用程序、服务或渠道的行为。这包括任何以营销或销售为目的的广告、特价或 “号召性用语”。如果您通过品牌商、卖家、作者或艺术家账户发布有关自己产品或服务的内容,则无需额外贴标。
作者和出版商可以在不要求评论或影响评论行为的前提下继续向读者提供免费或打折的图书副本。
**抄袭、侵权、冒名顶替**
我们仅允许您发布自己的内容或您有权在亚马逊上使用的内容。这包括文字、图像和视频。不允许:
发布侵犯他人知识产权(包括版权、商标、专利、商业机密)或其他专有权的内容
以侵犯他人知识产权或所有权的方式与社区成员互动
冒充某人或组织
**非法和危险活动**
不要发布鼓励非法活动的内容,例如:
暴力
非法使用药物
未成年人饮酒
虐待儿童或动物
欺诈
我们不允许宣传或威胁对自己或他人造成人身或经济伤害的内容。这包括恐怖主义。关于造成伤害的笑话或讽刺评论是不允许的。
也不允许提供欺诈性商品、服务、促销或计划(快速赚钱、金字塔)。不要鼓励错误使用商品的危险行为。
违规行为的后果
违反我们的准则会使社区变得不在具备信赖性、安全性及实用性。如果有人违反了准则,我们将:
删除其相关内容
限制他们使用社区功能的权限
移除相关商品
暂停或终止他们的账户
预扣付款
如果我们发现不寻常的评论行为,我们可能会限制发布评论的权限。如果我们拒绝或删除某人的评论,因为它违反了我们的关于评论的推广 ,我们将不再接受他们对同一商品的任何评论。
如果违法相关法律法规的规定,我们可能会采取法律行动,并产生民事和刑事处罚。
如何举报违规行为
使用您要举报的内容旁边的 “报告滥用行为” 链接。如果没有 “报告滥用行为” 链接,请发送电子邮件至 [email protected] 。描述出您所举报内容的位置及您认为它违反了社区准则的原因。
如果有人以提供报酬的形式请求您创建、编辑或发布违规内容,请将该请求发送至 [email protected] 。发送内容需包括:
收到您的举报后,我们将进行调查并采取适当的措施。
疑难杂症
Friday, June 28, 2024
Windows Server 2019长时间运行ipv6断连问题
Wednesday, November 06, 2024
我的Windows Server 2019不怎么关机, 在电信/红米路由下, ipv6每次更新时, 本地ipv6连接都会显示无Internet访问权限, 重启设备或者开闭IPv6功能可以解决, Linux下不会出现这样问题.
考虑自动化操作,用这两条命令解决:
Set-NetIPInterface -AddressFamily IPv6 -ifAlias Ethernet -RouterDiscovery Disabled ;
Set-NetIPInterface -AddressFamily IPv6 -ifAlias Ethernet -RouterDiscovery Enabled ;
你可以看到命令只是让Windows更新了路由, 不知道为什么Windows没有自动更新路由.
如果有人碰到相同问题可以参考, 如果有更好的解决办法, 也欢迎讨论.
openvpn网络不通
Friday, June 28, 2024
openvpn配置
工具脚本
openvpn-install
Windows防火墙配置
New-NetFirewallRule -DisplayName "@openvpn" -Direction Inbound -RemoteAddress 10.8 . 0 . 1 / 24 -Action Allow
New-NetFirewallRule -DisplayName "@openvpn" -Direction Outbound -RemoteAddress 10.8 . 0 . 1 / 24 -Action Allow
Windows桥接时的IPv6问题
Saturday, May 06, 2023
现在很多用作软路由的机器硬件配置较好, 仅安装一个 openwrt 大材小用, 基本都会自己折腾一下去榨干它的价值. Linux 的难点在于命令行, 其实命令行用的多的能感受到这也是 linux 的容易之处.
外网访问需求基本爱折腾的人都会遇到, 考虑到 linux 不太有专业的人维护, 安全补丁更新较慢, 衡量后会有部分人决定使用 Windows Server 系统. 原本 openwrt 上的软件则使用 wsl 加 docker 方式运行, 所有需求都可以同样满足.
在 Windows(Server)桥接多个网络时, 会出现 IPv6 地址无法更新的问题, 但是 IPv4 可以正常访问. 由于 IPv6 的地址是运营商自动分配的, 无法手动修改, 所以需要修改桥接的网络配置.
参考
Generally, bridging is purely layer 2 so no IP address is required, so just like an unmanaged switch should be iPv6 capable.
However, if you can plug the bridge into a switch and more than one client at a time can have internet access through the bridge, then IPv6 will most likely only work with one of the clients because the main router handling IPv6 connections can only see the bridge’s MAC address. I’m not sure how SLAAC decides which client gets the IPv6 but you could test this out with a switch.
DHCP is of course for IPv4. It may be possible to use stateful DHCPv6 to assign DUIDs to each client and make this work but I have no idea how this would be done. Good luck!
解释下, 由于桥接是二层的, 所以不需要 IP 地址, 但是如果桥接的网络连接到交换机, 交换机上的路由器只能看到桥接的 MAC 地址, 无法分辨出桥接的多个设备, 所以只能给其中一个设备分配 IPv6 地址.
一份标准可联网的配置如下:
PS C:\Users\jqkno> netsh interface ipv6 show interface "wi-fi"
Interface Wi-Fi Parameters
----------------------------------------------
IfLuid : wireless_32768
IfIndex : 24
State : connected
Metric : 45
Link MTU : 1480 bytes
Reachable Time : 29000 ms
Base Reachable Time : 30000 ms
Retransmission Interval : 1000 ms
DAD Transmits : 1
Site Prefix Length : 64
Site Id : 1
Forwarding : disabled
Advertising : disabled
Neighbor Discovery : enabled
Neighbor Unreachability Detection : enabled
Router Discovery : enabled
Managed Address Configuration : enabled
Other Stateful Configuration : enabled
Weak Host Sends : disabled
Weak Host Receives : disabled
Use Automatic Metric : enabled
Ignore Default Routes : disabled
Advertised Router Lifetime : 1800 seconds
Advertise Default Route : disabled
Current Hop Limit : 64
Force ARPND Wake up patterns : disabled
Directed MAC Wake up patterns : disabled
ECN capability : application
RA Based DNS Config (RFC 6106) : enabled
DHCP/Static IP coexistence : enabled
修改设置方法: netsh interface ipv6 set interface "Network Bridge" managedaddress=enabled
宝塔docker源加速
Monday, June 03, 2024
宝塔 8.2 及以下版本设置 docker 源加速无效, 并且界面上手动设置配置文件内容无效.
这是由于 docker 配置文件位于/etc/docker/daemon.json
, 该文件及其文件夹默认不存在, 直接修改文件不会保存成功.
只需要执行mkdir /etc/docker
, 然后再在界面上修改加速配置即可生效.
Windows Edge浏览器卡顿的一种解决办法
Tuesday, May 07, 2024
浏览器版本
122.0.2365.80+
卡顿现象
打开个人 profile 时卡顿
打开和搜索存储密码时卡顿
新建和关闭 tab 时卡顿
在新建的 tab 中输入字符时卡顿
目前发现仅中文版 Windows 系统会出现此类型的卡顿.
解决办法
中文浏览器设置路径: 隐私-搜索-服务 -> 地址栏和搜索 -> 搜索建议和筛选器 -> 搜索筛选器
, 关闭 搜索筛选器.
英文浏览器设置路径: Privacy search and services -> Address bar and search -> Search sugesstion and filters -> Search filters
, TURN OFF Search filters.
# Set Key File Variable:
New-Variable -Name Key -Value " $env:UserProfile \.ssh\id_rsa"
# Remove Inheritance:
Icacls $Key / c / t / Inheritance : d
# Set Ownership to Owner:
# Key's within $env:UserProfile:
Icacls $Key / c / t / Grant ${ env : UserName } : F
# Key's outside of $env:UserProfile:
TakeOwn / F $Key
Icacls $Key / c / t / Grant : r ${ env : UserName } : F
# Remove All Users, except for Owner:
Icacls $Key / c / t / Remove : g Administrator "Authenticated Users" BUILTIN \ Administrators BUILTIN Everyone System Users
# Verify:
Icacls $Key
# Remove Variable:
Remove-Variable -Name Key
程序员
Friday, June 28, 2024
网络
Wednesday, November 13, 2024
网络
电信IPv6的一些特征
Friday, June 28, 2024
国内已经全面铺开 ipv6 使用, ipv6 地址池足够大, 个人的每个设备都可以获取到一个 ipv6 地址.
家庭用户使用时需要全栈设备都支持 ipv6 才能最终使用到 ipv6, 由于已经推了很多年, 目前来说 2016 年以后买的设备基本都支持 ipv6 了.
全栈设备包括: 城域设备->小区路由->家庭路由(光猫,路由器)->终端设备(手机,电脑,电视等)
这里不讨论标准的 ipv6 协议, 只讨论电信的 ipv6 的一些特征.
地址分配
首先是地址分配方式, ipv6 有三种分配方式: 静态分配, SLAAC, DHCPv6.
湖北电信使用的是 SLAAC, 也就是说电信的 ipv6 地址是由设备自动分配的, 由于电信的 ipv6 地址池足够大, 所以不会出现地址冲突的问题.
电信 ipv6 地址是随机分配的, 24 小时后重新分配. 如果要从外部访问, 必须使用 DDNS 服务.
防火墙
目前可以发现常见的80
, 139
, 445
等端口已对齐 ipv4 防火前已经都封了, 这非常容易理解, 运营商级的防火墙确实能保护到缺乏网络安全意识的普通用户. 2020 年时电信 ipv6 都是开放的, 现在已经封了一些常用端口.
443
端口在电信网内偶尔开放, 但对移动联通不开放. 开发者应注意这一点. 在开发环境测试好的服务, 甚至电信网路手机也能访问, 但移动手机网络却访问不了.
基于简单的防火墙测试, 建议开发者牢记对运营商防火墙的不信任, 选择一个5 位数 的端口提供服务.
另外, 电信防火墙没有屏蔽22
端口, Windows 的远程桌面服务端口3389
也没有屏蔽.
也就是可以远程登录控制, 这会导致一些风险.
攻击者获取到 IP 或者 DDNS 域名后, 就可以开始展开针对攻击, 利用暴力破解的方式获取到密码, 从而获取到控制权, 域名也会暴露一些个人信息, 例如姓名, 住址等, 也可能利用社会工程学的方式获取到更多信息以加快破解速度.
建议关闭 ssh
的密码登录, 仅使用密钥登录, 或者使用 VPN 的方式进行远程登录, 或者使用跳板机的方式进行远程登录.
为什么不应该把TCP思维套在UDP上
Friday, June 28, 2024
为什么不应该把 TCP 思维套在 UDP 上?
结构差异
TCP 上的概念很多: 建立通路, 资源使用, 数据传输, 可靠传输, 基于重复累计确认的重传, 超时重传, 校验和, 流量控制, 拥塞控制, 最大分段大小, 选择确认, TCP 窗口缩放选项, TCP 时间戳, 强制数据递交, 终结通路.
以上这些能力, UDP 基本上都没有, 它仅比链路层多一点区分应用层目的的能力. UDP 足够简单意味着足够灵活.
如果可能发生,则一定会发生
墨菲定律:
如果有多过一种方式去做某事,而其中一种方式将导致灾难,则必定有人会这样选择。
通常介绍 UDP 适合应用在游戏/语音/视频等场景, 少量的错包不影响业务.
为什么 UDP 适合这些场景? 它能用在这些场景, 不代表它是这些场景的最优方案, 必然是存在 TCP 无法解决的问题, 才让这些服务选择了功能简陋的 UDP 协议. 错包不影响业务扩展开来讲是指 TCP 协议在乎错包, UDP 不在乎错包, 更在乎实时性/连续性. UDP 的特点就是它不在乎 TCP 在乎的因素, 这些因素影响了实时性.
在代码实现上, UDP 只需要创建一个 socket, 绑定到一个端口上, 即可以开始收发. 通常 socket 用完时, 端口也用完了.
因此我可以这样使用 UDP:
往任意 IP 的任意端口发送随机报文, 看看哪个端口有响应
甲通过 A 端口, 将请求报文发送到乙的 B 端口; 乙将响应报文用 C 端口, 发给甲的 D 端口
甲通过 A 端口, 将请求报文发送到乙的 B 端口; 乙委托丙将响应报文用 C 端口, 发给甲的 D 端口
甲通过 A 端口, 将请求报文发送到乙的 B 端口, 但将发送报文的源 IP 修改为了丙的 IP, 乙将会将响应报文发往丙
双方协商各用 10 个 UDP 端口, 同时进行接受和发送
这些方法在 TCP 里自然是行不通的, 但在 UDP 协议中, 只要可以这样做, 就一定会有人这样做. 所以当把 TCP 的一些思维套在 UDP 上是一种理想主义, 真实情况常常不是我们能枚举完的.
UDP 的报文非常简单, 使用也非常灵活, 原本没有连接的概念, 需要自己定义 UDP 连接. 尝试了一些定义方法, 都不能完全准确达到连接方向判断意图, 这时需要接纳一些容错, 毕竟原本就没有 UDP 连接的定义, 当各方对 UDP 连接的定义不一致时, 必然会导致行为与预期不一样.
客户端视角的 UDP
语音/视频等业务常会产生丢包, 但是丢包方式的不同对业务有着不同的影响. 比如 30%的丢包是均匀发生的, 还是全丢在某个时间段, 对体验的影响有明显的区分. 显然, 我们期待的是更均匀的丢包. 可是 UDP 没有流量控制防止方法, 如何丢包则有一些方法. 尽管 UDP 通信常被描述为"尽力而为", 但是不同方式的"尽力"会达到不同的效果.
服务商视角的 UDP
如果是 TCP 攻击, 客户端需要一定的开销, 创建连接, 维护连接, 也就是攻击者需要付出一定的代价. 而在 UDP 攻击中, 攻击者付出的代价小很多, 如果攻击者想消耗的就是服务方的带宽流量, UDP 是一个很好的方式. 比如说服务购买了 100GB 的不限速流量, 处理能力仅 10MB 每秒, 但接受速度 1GB 每秒, 那么 90%的请求流量无效, 但这些流量不是免费的. 服务方应该避免产生这种情况.
运营商的视角的 UDP
完成一次通信需包含多个终端以及通信通道, 受关注的总是服务端和客户端, 其实运营商的视角同样重要. DDoS 攻击中, 我们常关心服务端的资源消耗情况, 实际上运营商的资源也是有限的, 服务端简单不响应请求, 但接收流量却已经消耗了带宽, 只是这个资源一般属于运营商. 我们在压力测试中常用到"丢包率"指标, 这个指标表达的完整通信链条中的丢包, 而不仅仅是服务端的丢包. 运营商也会丢包. 在运营商看, 服务方仅购买了 1MB/s 的带宽, 但客户端以 1GB/s 的速度发送, 双方都不必为浪费的流量付费, 是运营商承担了这部分带宽的代价. 因此, 运营商必然想办法屏蔽这种流量, 也就是 UDP 的 QoS. 在 TCP 中有拥塞控制, 但在 UDP 中, 运营商可以通过丢包来控制流量. 实际情况中, 运营商更加简单粗暴, 直接屏蔽长时间使用的端口的流量, 也就是 UDP 的端口屏蔽. 在微信通话的实际测试中发现, 每一通电话客户端会使用多个端口, 其中有一个 UDP 端口会和同一服务器的 6 个 UDP 端口进行通信, 推测就是为了应对运营商的端口屏蔽.
总结
UDP 的灵活表示在实现一个目标时, 它有着多种实现方式, 并且都是合法的, 只要能最终实现稳定的通信, 不管它实现的如何和 TCP 大相径庭, 都是"存在即合理"的. 因而, 我们不能完全将 TCP 的概念套用在 UDP 上, 即便为了产品设计, 创造了新的 UDP 连接定义, 也应该能预期并允许出错, 毕竟"允许出错"就是 UDP 的核心功能, 这是 UDP 的优势, 不是它的缺点, 是服务主动选择的协议核心能力, 而不是不得不接受的缺点.
更多阅读
linux网络问题定位
Tuesday, May 28, 2024
排障工具
工具
说明
用法
说明
ping
测试网络连通性
ping baidu.com
traceroute
路由跟踪
traceroute ip
route
路由表
route -n
netstat
网络连接
netstat -ano
nslookup
DNS 解析
nslookup baidu.com
ifconfig
网络配置
ifconfig
arp
ARP 缓存
arp -a
nbtstat
NetBIOS
nbtstat -n
netsh
网络配置
netsh
net
网络配置
net
tcpdump
网络抓包
tcpdump
wireshark
网络抓包
wireshark
ip
网络配置
ip addr show
ss
网络连接
ss -tunlp
netstat
查看网络连接状态
netstat -anp
tcpdump
抓包工具
tcpdump -i eth0 -nn -s 0 -c 1000 -w /tmp/tcpdump.pcap
iptables
防火墙
iptables -L -n -v -t nat -t mangle -t filter
ss
netstat 的替代品
ss -anp
ifconfig
查看网卡信息
ifconfig eth0
ip
查看网卡信息
ip addr show eth0
route
查看路由表
route -n
traceroute
查看路由跳数
traceroute www.baidu.com
ping
测试网络连通性
ping www.baidu.com
telnet
测试端口连通性
telnet www.baidu.com 80
nslookup
域名解析
nslookup www.baidu.com
dig
域名解析
dig www.baidu.com
arp
查看 arp 缓存
arp -a
netcat
网络调试工具
nc -l 1234
nmap
端口扫描工具
nmap -sT -p 80 www.baidu.com
mtr
网络连通性测试工具
mtr www.baidu.com
iperf
网络性能测试工具
iperf -s -p 1234
iptraf
网络流量监控工具
iptraf -i eth0
ipcalc
IP 地址计算工具
ipcalc
iftop
网络流量监控工具
iftop -i eth0
iostat
磁盘 IO 监控工具
iostat -x 1 10
vmstat
虚拟内存监控工具
vmstat 1 10
sar
系统性能监控工具
sar -n DEV 1 10
lsof
查看文件打开情况
lsof -i:80
strace
跟踪系统调用
strace -p 1234
tcpflow
抓包工具
tcpflow -i eth0 -c -C -p -o /tmp/tcpflow
tcpick
抓包工具
tcpick -i eth0 -C -p -o /tmp/tcpick
tcptrace
抓包工具
tcptrace -i eth0 -C -p -o /tmp/tcptrace
tcpslice
抓包工具
tcpslice -i eth0 -C -p -o /tmp/tcpslice
tcpstat
抓包工具
tcpstat -i eth0 -C -p -o /tmp/tcpstat
tcpdump
抓包工具
tcpdump -i eth0 -C -p -o /tmp/tcpdump
tshark
抓包工具
tshark -i eth0 -C -p -o /tmp/tshark
wireshark
抓包工具
wireshark -i eth0 -C -p -o /tmp/wireshark
socat
网络调试工具
socat -d -d TCP-LISTEN:1234,fork TCP:www.baidu.com:80
ncat
网络调试工具
ncat -l 1234 -c ’ncat www.baidu.com 80'
netperf
网络性能测试工具
netperf -H www.baidu.com -l 60 -t TCP_STREAM
netcat
网络调试工具
netcat -l 1234
nc
网络调试工具
nc -l 1234
netpipe
网络性能测试工具
netpipe -l 1234
netkit
网络调试工具
netkit -l 1234
bridge
网桥工具
bridge -s
如何提升自建DNS服务下的网络体验
Saturday, May 18, 2024
网络质量和网络体验
什么都不做, 即可以获得最好的网络体验
需要明确, 这里网络质量
和网络体验
是两个不同的概念. 通信是一个过程, 涉及多个设备, 我们可以称单个设备的上下行表现为网络质量
, 而整个端到端的通信表现, 我们可以称为网络体验
.
如何衡量网络质量
衡量网络质量通常涉及多个指标和方法。以下是一些常见的衡量网络质量的方法和指标:
带宽(Bandwidth) :带宽是指网络传输数据的能力,通常以每秒传输的数据量(比特/秒)来衡量。更高的带宽通常表示更好的网络质量。
延迟(Latency) :延迟是指数据从发送端到接收端所需的时间。低延迟表示数据传输速度快,网络响应更快。
丢包率(Packet Loss Rate) :丢包率是指在数据传输过程中丢失的数据包的比例。较低的丢包率通常意味着网络质量较好。
抖动(Jitter) :抖动是指数据包在传输过程中的变化或波动。较小的抖动表示网络稳定性较高。
吞吐量(Throughput) :吞吐量是指网络传输的实际数据量,通常以单位时间内的数据传输量来衡量。更高的吞吐量表示网络质量更好。
网络拓扑(Network Topology) :网络拓扑描述了网络中节点之间的连接方式和结构。合理的网络拓扑设计可以提高网络性能和质量。
服务质量(Quality of Service,QoS) :QoS 是一组技术和机制,用于确保在网络中的数据传输中实现可接受的服务质量。QoS 可以通过各种方式实现,包括流量控制、优先级队列等。
网络协议分析(Protocol Analysis) :通过分析网络协议和数据包,可以了解网络中的性能指标和问题,例如使用 Wireshark 等网络分析工具。
综合利用这些指标和方法,可以全面地评估网络质量,确定网络性能的优势和改进的空间。 但这些是运营商关注的指标, 对于普通用户, 只需要购买价格合适的路由器即可, 现代路由器都有自动调整网络质量的功能.
如何衡量网络体验
首先是可访问性 , 能访问是最重要的基础. 因此, 域名解析服务需要满足基础的能力:
全面, 上级 DNS 服务需要权威, 且能够解析更多的域名
正确, 解析结果需要正确, 不能出现解析错误. 部分 DNS 服务商会对一些域名进行劫持或污染, 解析到广告页面.
及时, ip 地址变更后, 需要及时更新解析结果, 而不是返回旧的 ip 地址
其次是 DNS 解析结果的 IP 所能提供服务的网络质量
.
互联网服务所能提供的网络质量
, 通常强依赖地域 , 服务器和客户端在地域上越接近, 则服务质量越好.
许多付费 DNS 解析服务商都支持按地域解析不同 IP, 例如这是阿里云能提供的一部分服务:
(1)运营商线路:支持按联通、电信、移动、教育网、鹏博士、广电网智能解析,细分到省份;
(2)海外地区线路:支持,细分到大洲、国家;
(3)阿里云线路:支持,细分到各个地区;
(4)自定义线路:支持自定义 IP 地址范围智能解析;
按区域解析不同 IP 的机制, 意味着不同地域的用户访问同一个域名时, 会得到不同的解析结果, 自然而然的, 优先解析到距离用户更近的服务器, 将会有更好的网络体验.
而优化用户网络体验这件事, 一般都是服务提供商根据用户的真实 IP 地址来做优化. 也就是对多数用户来说, 什么都不做, 即可以获得最好的网络体验
.
自建 DNS 服务如何选择上游 DNS 服务
中文互联网你搜索到的所有资料都会推荐你选择权威 DNS 服务商, 例如阿里云, 腾讯云, Cloudflare, 谷歌等. 这些 DNS 可以满足网络服务的的可访问性
, 因为它们全面/正确/及时, 但是, 它们未必会给你解析到最近的服务器 IP.
互联网上大量的资料推荐大企业的 DNS 服务有其历史原因.
曾经我国的 ISP 运营商, 仅靠 DNS 劫持加上 HTTP 的中间人攻击, 就能够实现对用户的流量劫持, 从而实现广告推送. 现如今随着 https 的普及, 这种劫持方式已较为少见, 但部分地区的小区宽带仍然可能存在这种问题. 针对 DNS 劫持问题, 实际上改 DNS IP 无济于事, 因为劫持可以针对 53 端口, 而绝大多数 DNS 请求都是未加密的.
此外, 一些特殊用户希望访问特殊网站, 而部分 DNS 服务商存在 IP 污染问题, 会将特殊网站的域名解析到错误的 IP 地址, 导致无法访问. 而权威 DNS 服务商则较少出现这样问题.
因此, 这里存在三个问题需要考虑:
IP 污染
DNS 劫持
最优服务体验
权威 DNS 服务商可以解决问题 1 , 加密协议(DoT/DoH/QUIC)可以解决问题 2.
想要解决问题 3, 你需要使用回宽带运营商的默认 DNS 服务. , 正如本文开头所说, 什么都不做, 即可以获得最好的网络体验
.
但如果你是一个有追求的人, 或者特殊用户, 下文将介绍如何配置 AdguardHome
及 Clash
两种工具的配置, 以同时解决这三个问题.
权威且智能的 DNS 服务
AdguardHome 配置
AdguardHome , 以下简称ADG
是一个网络广告拦截与隐私保护软件, 也是一个 DNS 服务. 它支持自定义上游 DNS 服务, 以及自定义 DNS 规则.
ADG
默认的向上游请求 DNS 的方式是负载均衡
, 用户可以设置多个上游, ADG
将根据历史 DNS 查询加权权重选择其中 DNS 响应最快的上游. 简单说, ADG
会以更高的概率选择更快的 DNS 上游来解析域名, 以较低的概率选择非最优的 DNS 上游.
我们可以选择第三个选项: 最快的IP地址
.
该选项带来的好处, ADG
自行测试上游 DNS 的 IP 解析结果, 将其中延迟最低的 IP 返回给下游客户端. 以下是bilibili 的常规解析结果.
你可以看到 IP 非常多, 如果ADG
不测试 IP 解析结果, 而将所有 IP 返回给客户端, 那么客户端会做什么?
有的客户端会选择第一个 IP, 有的客户端会选择最后一个 IP, 有的客户端会随机选择一个 IP. 不管是哪种, 都未必是最优的选择.
开启最快的IP地址
选项后, 以下是bilibili 的优选解析结果, 这一步将会带来网络体验
的提升 .
最快的IP地址
为什么不是默认选择? 这个功能这么实用, 为什么不默认开启?
因为它的代价是等待所有上游 DNS 的 IP 解析结果 , 当你的上游同时有多个 DNS 服务商时, 向上游的查询时间以其中最慢的为准. 例如, 你的上游有平均服务时长50ms
的阿里和平均服务时长500ms
谷歌, ADG
的上游查询时间将是500ms+
.
因此用户在配置此选项时, 需要权衡上游 DNS 的服务质量和数量, 不要贪多.
这里我推荐设置两个上游, 一个权威(https://dns.alidns.com/dns-query ) , 加上一个运营商 DNS .
运营商的 DNS IP 各地都不相同, 可以点击这里 查看自己所在地区的运营商 DNS.
或者, 你可以在路由器的管理界面上查看运营商推荐的 DNS :
Clash 配置
特殊需求用户看重 DNS 劫持和 IP 污染问题, 但又不想放弃最优服务体验, 可以使用Clash
的dns
模块.
其中nameserver-policy
可以指定不同的域名使用不同的 DNS 服务商, 以下是一个示例配置:
dns :
default-nameserver :
- tls://223.5.5.5:853
- tls://1.12.12.12:853
nameserver :
- https://dns.alidns.com/dns-query
- https://one.one.one.one/dns-query
- https://dns.google/dns-query
nameserver-policy :
"geosite:cn,private,apple" :
- 202.103.24.68 # 自己所在地的运营商 DNS
- https://dns.alidns.com/dns-query
"geosite:geolocation-!cn" :
- https://one.one.one.one/dns-query
- https://dns.google/dns-query
它的含义是:
default-nameserver: 用于解析配置nameserver
中的 DNS 服务的 IP
nameserver: 用于解析网络请求的域名
nameserver-policy: 根据策略, 指定不同的域名使用不同的 DNS 服务
感谢阅读
如果本文对您有所帮助, 还请点个赞. 也非常欢迎留言讨论.
ChatGPT VPN识别绕过方法
Thursday, May 09, 2024
如何处理 ChatGPT 报错
“Unable to load site”
“Please try again later, if you are using a VPN, try turning it off.”
“Check the status page for information on outages.”
前言
chatgpt 目前仍然是使用体验最好的聊天机器人,但是在国内使用时,由于网络环境的限制,我们需要使用梯子来访问 chatgpt。但是 chatgpt 对梯子的检测较为严格,如果检测到使用了梯子,会直接拒绝访问。这里介绍一种绕过 chatgpt 对梯子检测的方法。
有其他人提到更换 IP 来绕过封锁, 但我们一般使用 IP 的地域已经是可以提供服务的地区, 所以这种方法并不一定是实际的拒绝服务原因.
另外有人提到梯子使用人数较多容易被识别, 劝人购买较贵的使用人数少的梯子, 这也很难成为合理理由, 在 ipv4 短缺的今天, 即便是海外, 也存在大量的社区使用 nat 分配端口, 共用一个 ipv4 的情况. chatgpt 一封就要封一大片, 作为一个被广泛使用的服务, 这样的检测设计肯定是不合理的.
对大众服务来说, 检测源 IP 一致性则更为合理. 付费梯子的特征通常是限制流量或限制网速, 因此多数使用梯子的用户选择按规则绕过. 绕过自己的运营商可直接访问的地址, 以减少流量消耗, 或者获得更快的访问速度, 仅在访问被防火墙拦截的地址时导入流量到代理. 这种访问目标服务的不同方式, 可能会造成源地址不一致. 例如访问 A 服务需要同时和域名 X 和域名 Y 进行通信, 而防火墙仅拦截了域名 X, 那么在 A 服务看到的同一请求的不同阶段的访问来源 IP 不一致.
解决代理策略导致的源 IP 不一致问题, 即可绕过 chatgpt 的梯子识别.
梯子规则中通常会含有域名规则
, IP规则
等.
我们还需要知道域名解析
的 IP 结果是可以根据地域而变化的, 比如我在 A 地区时解析到附近的服务 IP, 在 B 地区时则解析到不同的 IP. 因此, DNS 的选择也非常重要.
DNS 选择
现在 DNS 有很多的协议, UDP:53
已经是非常落后而且极不安全的协议, 我国甚至已将 DNS 服务列入企业经营中的一级条目. 这主要来源于过去几十年我国的各级运行商使用DNS劫持
加HTTP
塞入了大量的跳转广告, 蒙骗不少网络小白, 招致大量投诉. 尽管现在Chrome/Edge
已经标配自动跳转HTTPS
, 标记HTTP
网站为不安全, 但我国还存在许多的地方小区级的网络服务提供商, 以及国内各种老版本的Chromium
封装魔改, 导致 DNS 劫持和 HTTP 劫持仍然存在.
因此, 我们需要选择一个安全的 DNS 服务协议, 以避免 DNS 劫持. 根据个人经验, 阿里云的223.5.5.5
体验足够好. 当然, 当我提223.5.5.5
时, 肯定不是UDP:53
的 alidns, 而是DoH
或DoT
协议. 在配置时, 你需要使用tls://223.5.5.5
, 或者https://dns.alidns.com/dns-query
写入配置.
alidns 服务在绝大多数时候都不会污染, 仅在少数敏感时期会出现污染, 你也可以使用我自建的长期 dns 服务tls://dns.jqknono.com
, 上游来自8.8.8.8
和1.1.1.1
, 通过缓存来加速访问.
域名规则
首先打开的检测网页会包含检测逻辑, 通过向不同域名 发送请求来验证源 IP, 因此这里需要保持域名代理的一致性.
chatgpt 网页访问的域名除了自己的域名openai
外, 还有auth0
, cloudflare
等第三方域名.
可以手动写入以下规则:
# openai
- DOMAIN-SUFFIX,chatgpt.com,PROXY
- DOMAIN-SUFFIX,openai.com,PROXY
- DOMAIN-SUFFIX,openai.org,PROXY
- DOMAIN-SUFFIX,auth0.com,PROXY
- DOMAIN-SUFFIX,cloudflare.com,PROXY
如何试验域名规则
上边列举的域名可能随着 ChatGPT 业务发展而有所变化, 下面说明域名的获取方法.
浏览器打开 InPrivate 页面, 隐私页面可以避免缓存/cookies 等的影响
按F12
打开控制台, 选择Network
/网络
选项卡
访问chat.openai.com
, 或者chatgpt.com
下图展示了这篇文章写成时 ChatGPT 使用的域名
仅添加这几个域名可能仍然不够, 这里分析访问失败的连接具体细节. 看到challenge 的请求的Content-Security-Policy 中含有众多域名, 我们将其一一添加到代理策略.
# openai
- DOMAIN-SUFFIX,chatgpt.com,PROXY
- DOMAIN-SUFFIX,openai.com,PROXY
- DOMAIN-SUFFIX,openai.org,PROXY
- DOMAIN-SUFFIX,auth0.com,PROXY
- DOMAIN-SUFFIX,cloudflare.com,PROXY
# additional
- DOMAIN-SUFFIX,oaistatic.com,PROXY
- DOMAIN-SUFFIX,oaiusercontent.com,PROXY
- DOMAIN-SUFFIX,intercomcdn.com,PROXY
- DOMAIN-SUFFIX,intercom.io,PROXY
- DOMAIN-SUFFIX,mixpanel.com,PROXY
- DOMAIN-SUFFIX,statsigapi.net,PROXY
- DOMAIN-SUFFIX,featuregates.org,PROXY
- DOMAIN-SUFFIX,stripe.com,PROXY
- DOMAIN-SUFFIX,browser-intake-datadoghq.com,PROXY
- DOMAIN-SUFFIX,sentry.io,PROXY
- DOMAIN-SUFFIX,live.net,PROXY
- DOMAIN-SUFFIX,live.com,PROXY
- DOMAIN-SUFFIX,windows.net,PROXY
- DOMAIN-SUFFIX,onedrive.com,PROXY
- DOMAIN-SUFFIX,microsoft.com,PROXY
- DOMAIN-SUFFIX,azure.com,PROXY
- DOMAIN-SUFFIX,sharepoint.com,PROXY
- DOMAIN-SUFFIX,gstatic.com,PROXY
- DOMAIN-SUFFIX,google.com,PROXY
- DOMAIN-SUFFIX,googleapis.com,PROXY
- DOMAIN-SUFFIX,googleusercontent.com,PROXY
IP 规则
如果上述步骤尝试后仍然不能访问chatgpt.com , 则可能还存在基于IP 的检测行为, 以下是我在连接跟踪中尝试出的一些 IP, 你可以自行尝试使用, 需要说明这些 IP 并不一定适用于每个地区, 你或许需要自行尝试.
# openai
- IP-CIDR6,2606:4700:4400::6812:231c/96,PROXY
- IP-CIDR,17.253.84.253/24,PROXY
- IP-CIDR,172.64.152.228/24,PROXY
- IP-CIDR,104.18.35.28/16,PROXY
如何试验 IP 规则
你需要了解自己的梯子客户端工具, 在连接跟踪显示页面, 观察新增的连接, 通过这些连接的 IP 地址来尝试添加规则.
以下是简单的步骤描述:
浏览器打开 InPrivate 页面, 隐私页面可以避免缓存/cookies 等的影响
访问chat.openai.com
, 或者chatgpt.com
梯子客户端中观察新增连接, 将这些连接加入到代理规则
协议规则
QUIC
是基于UDP
的加密协议, chatgpt 大量使用了 QUIC 流量, 因此梯子的服务端/客户端需要支持 UDP 代理, 有许多梯子是不支持 UDP 的, 这也是导致 chatgpt 无法访问的原因之一. 客户端和服务端都支持 UDP, 还需要用户明确配置, 一些客户端会配置默认不代理 UDP 流量. 如果对 UDP 设置不熟悉, 可以设置屏蔽代理客户端的 QUIC 流量, 或者在浏览器设置屏蔽 QUIC. 浏览器发现 QUIC 不通会自动切换到基于 TCP 的 HTTP/2. QUIC 是基于 UDP 的加密协议, 多数时候可以获得更流畅的体验, 有兴趣的可以自行尝试.
最简单配置–白名单模式
配置仅中国 IP 直连, 未匹配到的流量走代理, 这样可以保证 chatgpt 的访问, 也可以保证其他国外服务的访问.
这种方式的缺点就是流量消耗大, 网络流畅度体验依赖梯子的网络质量, 如果您对自己的梯子有信心, 可以尝试这种方式.
当然, 您还得记得开启UDP
代理.
第三方库的陷阱
Friday, June 28, 2024
今天聊到最近出的第三方日志库的一个漏洞, 可以很低门槛的利用以执行远程命令. 一个日志库和远程命令看着毫不相干, 但是画蛇添足的第三方库遍地都是.
读的代码越多越感受到很多开源代码的水平非常差, 无论它有多少 k 的 star, star 代表了需求, 不代表开发水平.
开源的好处是有更多的人来开发, 好处是特性迅速增加, bug 有人来解, 代码有人来审核, 但是水平参差不齐.
如果没有一个强有力的提交约束, 代码的质量很难保证.
代码越多增加的攻击面越多
虽说重复造轮子不好, 但是产品需求就是婴儿车轮子, 一个塑料轮子怎么都用不坏, 装了个飞机轮胎, 徒增攻击面和维护成本. 因此如果只需要婴儿车的轮子, 不需要大材小用.
维护成本高, 第三方库需要专门的流程和人员去维护. 华为一个魔改的测试框架, 直接导致升级编译器就用例失败, 升级测试框架和升级编译器产生冲突, 维护时要花大量时间继续魔改这条路. 作为参与者深刻体会到魔改三方库的困难. 如果魔改的是特性可以合回开源库还好说, 为了自己的需求去侵入式的定制开发, 会导致很难维护.
对待第三方库华为创建了一系列流程, 可以说阻力重重.
门槛收的极紧, 增加的第三方库需要 18 级专家和 20 级部长评审, 基本只有久负盛名的三方库能被使用.
所有第三方库都放在 thirdparty 文件夹下, 全量编译时 CI 和源库对比, 严格禁止侵入式修改.
专门的工具追踪所有第三方库的版本, 这部分请了外包人员来管理, 如果开发申请升级版本需要提申请, 部长审核.
很难找部长去处理这样的事, 当一个流程非常繁琐的时候, 它实际上是在劝你不要这样做.
对待第三方库应该保持不轻信的态度, 相信自己人的开发.
方案设计模板
Friday, June 28, 2024
XXX系统/子系统详细设计
系统名称
XXX系统
作者
XXX
—
—
提交日期
2021-6-30
修改记录
修改后版本号
修改内容
修改日期
修改人
v1.0
XXXXXXX
2021-6-30
XXX
—
—
—
—
技术评审意见
No.
评审人
评审意见(通过/不通过/待定,可附上评论)
评审时间
1
XXX
通过
2022.1.1
设计背景
名词解释
SIP: 会话初始协议 (Session Initiation Protocol)
RTP: 实时传输协议(Real-time Transport P rotocol)
设计目标
功能需求
非功能需求(不可省略)
系统环境
相关软件及硬件(可选)
系统限制
数据规模估计(不可省略)
遍历已有
设计思路及折衷
假设及与其它系统的依赖/联系
系统设计
基本介绍
系统架构图及说明
系统流程图及说明(可选)
与外部系统的接口
全局性数据结构说明
XXX1模块简要说明
XXX1模块的功能
与其它模块的接口
XXX2模块简要说明
XXX2模块的功能
与其它模块的接口
威胁建模
升级影响(不可省略)
风险评估及对其它系统影响(可选)
已知的或可预知的风险
与其它系统/模块可能的影响
创新点挖掘(可选)
附件及参考资料
命令行语法约定
Friday, June 28, 2024
参考
e.g.
Notation
Description
Text without brackets or braces
Items you must type as shown.
<
Text inside angle brackets>
Placeholder for which you must supply a value.
[
Text inside square brackets]
Optional items.
{
Text inside braces}
Set of required items. You must choose one.
Vertical bar ( |
)
Separator for mutually exclusive items. You must choose one.
Ellipsis (…
)
Items that can be repeated and used multiple times.
命令行手册中括号的含义
Friday, June 28, 2024
命令行手册中括号的含义
在命令行帮助中,不同种类的括号通常有以下含义:
尖括号 <>
:
尖括号用于表示必需参数,即在运行命令时必须提供的值。通常用于表示命令的基本语法和参数。
例如:command <filename>
表示你需要提供一个文件名作为必需参数,如 command file.txt
。
方括号 []
:
方括号用于表示可选参数,即在运行命令时可以选择是否提供的值。它们通常用于标记命令的可选参数和选项。
例如:command [option]
表示你可以选择性地提供一个选项,如 command -v
或 command
。
大括号 {}
:
大括号通常用于表示一组选项或值,表示你需要从中选择一个。它们也被称为"选择性参数组"。
例如:command {option1 | option2 | option3}
表示你必须从给定的选项中选择一个,如 command option2
。
圆括号 ()
:
圆括号在命令行帮助中通常用于表示参数的分组,以明确参数的结构和优先级。
例如:command (option1 | option2) filename
表示你需要选择 option1
或 option2
,并提供一个文件名作为参数,如 command option1 file.txt
。
这些括号的使用旨在帮助用户理解命令的语法和参数选择,从而正确地使用命令行工具。在阅读命令行帮助时,仔细注意括号的含义和作用是很重要的,这样可以避免错误的命令输入并获得所需的结果。
华为C++编程规范
Friday, June 28, 2024
C++语言编程规范
目的
规则并不是完美的,通过禁止在特定情况下有用的特性,可能会对代码实现造成影响。但是我们制定规则的目的“为了大多数程序员可以得到更多的好处”, 如果在团队运作中认为某个规则无法遵循,希望可以共同改进该规则。
参考该规范之前,希望您具有相应的C++语言基础能力,而不是通过该文档来学习C++语言。
了解C++语言的ISO标准;
熟知C++语言的基本语言特性,包括C++ 03/11/14/17相关特性;
了解C++语言的标准库;
总体原则
代码需要在保证功能正确的前提下,满足可读、可维护、安全、可靠、可测试、高效、可移植 的特征要求。
重点关注
约定C++语言的编程风格,比如命名,排版等。
C++语言的模块化设计,如何设计头文件,类,接口和函数。
C++语言相关特性的优秀实践,比如常量,类型转换,资源管理,模板等。
现代C++语言的优秀实践,包括C++11/14/17中可以提高代码可维护性,提高代码可靠性的相关约定。
本规范优先适于用C++17版本。
约定
规则 :编程时必须遵守的约定(must)
建议 :编程时应该遵守的约定(should)
本规范适用通用C++标准, 如果没有特定的标准版本,适用所有的版本(C++03/11/14/17)。
例外
无论是’规则’还是’建议’,都必须理解该条目这么规定的原因,并努力遵守。
但是,有些规则和建议可能会有例外。
在不违背总体原则,经过充分考虑,有充足的理由的前提下,可以适当违背规范中约定。
例外破坏了代码的一致性,请尽量避免。‘规则’的例外应该是极少的。
下列情况,应风格一致性原则优先:
修改外部开源代码、第三方代码时,应该遵守开源代码、第三方代码已有规范,保持风格统一。
2 命名
通用命名
驼峰风格(CamelCase)
大小写字母混用,单词连在一起,不同单词间通过单词首字母大写来分开。
按连接后的首字母是否大写,又分: 大驼峰(UpperCamelCase)和小驼峰(lowerCamelCase)
类型
命名风格
类类型,结构体类型,枚举类型,联合体类型等类型定义, 作用域名称
大驼峰
函数(包括全局函数,作用域函数,成员函数)
大驼峰
全局变量(包括全局和命名空间域下的变量,类静态变量),局部变量,函数参数,类、结构体和联合体中的成员变量
小驼峰
宏,常量(const),枚举值,goto 标签
全大写,下划线分割
注意:
上表中__常量__是指全局作用域、namespace域、类的静态成员域下,以 const或constexpr 修饰的基本数据类型、枚举、字符串类型的变量,不包括数组和其他类型变量。
上表中__变量__是指除常量定义以外的其他变量,均使用小驼峰风格。
文件命名
规则2.2.1 C++文件以.cpp结尾,头文件以.h结尾
我们推荐使用.h作为头文件的后缀,这样头文件可以直接兼容C和C++。
我们推荐使用.cpp作为实现文件的后缀,这样可以直接区分C++代码,而不是C代码。
目前业界还有一些其他的后缀的表示方法:
头文件: .hh, .hpp, .hxx
cpp文件:.cc, .cxx, .c
如果当前项目组使用了某种特定的后缀,那么可以继续使用,但是请保持风格统一。
但是对于本文档,我们默认使用.h和.cpp作为后缀。
规则2.2.2 C++文件名和类名保持一致
C++的头文件和cpp文件名和类名保持一致,使用下划线小写风格。
如果有一个类叫DatabaseConnection,那么对应的文件名:
database_connection.h
database_connection.cpp
结构体,命名空间,枚举等定义的文件名类似。
函数命名
函数命名统一使用大驼峰风格,一般采用动词或者动宾结构。
class List {
public :
void AddElement ( const Element & element );
Element GetElement ( const unsigned int index ) const ;
bool IsEmpty () const ;
};
namespace Utils {
void DeleteUser ();
}
类型命名
类型命名采用大驼峰命名风格。
所有类型命名——类、结构体、联合体、类型定义(typedef)、枚举——使用相同约定,例如:
// classes, structs and unions
class UrlTable { ...
class UrlTableTester { ...
struct UrlTableProperties { ...
union Packet { ...
// typedefs
typedef std :: map < std :: string , UrlTableProperties *> PropertiesMap ;
// enums
enum UrlTableErrors { ...
对于命名空间的命名,建议使用大驼峰:
// namespace
namespace OsUtils {
namespace FileUtils {
}
}
建议2.4.1 避免滥用 typedef或者#define 对基本类型起别名
除有明确的必要性,否则不要用 typedef/#define 对基本数据类型进行重定义。
优先使用<cstdint>
头文件中的基本类型:
有符号类型
无符号类型
描述
int8_t
uint8_t
宽度恰为8的有/无符号整数类型
int16_t
uint16_t
宽度恰为16的有/无符号整数类型
int32_t
uint32_t
宽度恰为32的有/无符号整数类型
int64_t
uint64_t
宽度恰为64的有/无符号整数类型
intptr_t
uintptr_t
足以保存指针的有/无符号整数类型
变量命名
通用变量命名采用小驼峰,包括全局变量,函数形参,局部变量,成员变量。
std :: string tableName ; // Good: 推荐此风格
std :: string tablename ; // Bad: 禁止此风格
std :: string path ; // Good: 只有一个单词时,小驼峰为全小写
规则2.5.1 全局变量应增加 ‘g_’ 前缀,静态变量命名不需要加特殊前缀
全局变量是应当尽量少使用的,使用时应特别注意,所以加上前缀用于视觉上的突出,促使开发人员对这些变量的使用更加小心。
全局静态变量命名与全局变量相同。
函数内的静态变量命名与普通局部变量相同。
类的静态成员变量和普通成员变量相同。
int g_activeConnectCount ;
void Func ()
{
static int packetCount = 0 ;
...
}
规则2.5.2 类的成员变量命名以小驼峰加后下划线组成
class Foo {
private :
std :: string fileName_ ; // 添加_后缀,类似于K&R命名风格
};
对于struct/union的成员变量,仍采用小驼峰不加后缀的命名方式,与局部变量命名风格一致。
宏、常量、枚举命名
宏、枚举值采用全大写,下划线连接的格式。
全局作用域内,有名和匿名namespace内的 const 常量,类的静态成员常量,全大写,下划线连接;函数局部 const 常量和类的普通const成员变量,使用小驼峰命名风格。
#define MAX(a, b) (((a) < (b)) ? (b) : (a)) // 仅对宏命名举例,并不推荐用宏实现此类功能
enum TintColor { // 注意,枚举类型名用大驼峰,其下面的取值是全大写,下划线相连
RED ,
DARK_RED ,
GREEN ,
LIGHT_GREEN
};
int Func (...)
{
const unsigned int bufferSize = 100 ; // 函数局部常量
char * p = new char [ bufferSize ];
...
}
namespace Utils {
const unsigned int DEFAULT_FILE_SIZE_KB = 200 ; // 全局常量
}
3 格式
行宽
规则3.1.1 行宽不超过 120 个字符
建议每行字符数不要超过 120 个。如果超过120个字符,请选择合理的方式进行换行。
例外:
如果一行注释包含了超过120 个字符的命令或URL,则可以保持一行,以方便复制、粘贴和通过grep查找;
包含长路径的 #include 语句可以超出120 个字符,但是也需要尽量避免;
编译预处理中的error信息可以超出一行。
预处理的 error 信息在一行便于阅读和理解,即使超过 120 个字符。
#ifndef XXX_YYY_ZZZ
#error Header aaaa/bbbb/cccc/abc.h must only be included after xxxx/yyyy/zzzz/xyz.h, because xxxxxxxxxxxxxxxxxxxxxxxxxxxxx
#endif
缩进
规则3.2.1 使用空格进行缩进,每次缩进4个空格
只允许使用空格(space)进行缩进,每次缩进为 4 个空格。不允许使用Tab符进行缩进。
当前几乎所有的集成开发环境(IDE)都支持配置将Tab符自动扩展为4空格输入;请配置你的IDE支持使用空格进行缩进。
大括号
规则3.3.1 使用 K&R 缩进风格
K&R风格
换行时,函数(不包括lambda表达式)左大括号另起一行放行首,并独占一行;其他左大括号跟随语句放行末。
右大括号独占一行,除非后面跟着同一语句的剩余部分,如 do 语句中的 while,或者 if 语句的 else/else if,或者逗号、分号。
如:
struct MyType { // 跟随语句放行末,前置1空格
...
};
int Foo ( int a )
{ // 函数左大括号独占一行,放行首
if (...) {
...
} else {
...
}
}
推荐这种风格的理由:
代码更紧凑;
相比另起一行,放行末使代码阅读节奏感上更连续;
符合后来语言的习惯,符合业界主流习惯;
现代集成开发环境(IDE)都具有代码缩进对齐显示的辅助功能,大括号放在行尾并不会对缩进和范围产生理解上的影响。
对于空函数体,可以将大括号放在同一行:
class MyClass {
public :
MyClass () : value_ ( 0 ) {}
private :
int value_ ;
};
函数声明和定义
规则3.4.1 函数声明和定义的返回类型和函数名在同一行;函数参数列表超出行宽时要换行并合理对齐
在声明和定义函数的时候,函数的返回值类型应该和函数名在同一行;如果行宽度允许,函数参数也应该放在一行;否则,函数参数应该换行,并进行合理对齐。
参数列表的左圆括号总是和函数名在同一行,不要单独一行;右圆括号总是跟随最后一个参数。
换行举例:
ReturnType FunctionName ( ArgType paramName1 , ArgType paramName2 ) // Good:全在同一行
{
...
}
ReturnType VeryVeryVeryLongFunctionName ( ArgType paramName1 , // 行宽不满足所有参数,进行换行
ArgType paramName2 , // Good:和上一行参数对齐
ArgType paramName3 )
{
...
}
ReturnType LongFunctionName ( ArgType paramName1 , ArgType paramName2 , // 行宽限制,进行换行
ArgType paramName3 , ArgType paramName4 , ArgType paramName5 ) // Good: 换行后 4 空格缩进
{
...
}
ReturnType ReallyReallyReallyReallyLongFunctionName ( // 行宽不满足第1个参数,直接换行
ArgType paramName1 , ArgType paramName2 , ArgType paramName3 ) // Good: 换行后 4 空格缩进
{
...
}
函数调用
规则3.5.1 函数调用入参列表应放在一行,超出行宽换行时,保持参数进行合理对齐
函数调用时,函数参数列表放在一行。参数列表如果超过行宽,需要换行并进行合理的参数对齐。
左圆括号总是跟函数名,右圆括号总是跟最后一个参数。
换行举例:
ReturnType result = FunctionName ( paramName1 , paramName2 ); // Good:函数参数放在一行
ReturnType result = FunctionName ( paramName1 ,
paramName2 , // Good:保持与上方参数对齐
paramName3 );
ReturnType result = FunctionName ( paramName1 , paramName2 ,
paramName3 , paramName4 , paramName5 ); // Good:参数换行,4 空格缩进
ReturnType result = VeryVeryVeryLongFunctionName ( // 行宽不满足第1个参数,直接换行
paramName1 , paramName2 , paramName3 ); // 换行后,4 空格缩进
如果函数调用的参数存在内在关联性,按照可理解性优先于格式排版要求,对参数进行合理分组换行。
// Good:每行的参数代表一组相关性较强的数据结构,放在一行便于理解
int result = DealWithStructureLikeParams ( left . x , left . y , // 表示一组相关参数
right . x , right . y ); // 表示另外一组相关参数
if语句
规则3.6.1 if语句必须要使用大括号
我们要求if语句都需要使用大括号,即便只有一条语句。
理由:
代码逻辑直观,易读;
在已有条件语句代码上增加新代码时不容易出错;
对于在if语句中使用函数式宏时,有大括号保护不易出错(如果宏定义时遗漏了大括号)。
if ( objectIsNotExist ) { // Good:单行条件语句也加大括号
return CreateNewObject ();
}
规则3.6.2 禁止 if/else/else if 写在同一行
条件语句中,若有多个分支,应该写在不同行。
如下是正确的写法:
if ( someConditions ) {
DoSomething ();
...
} else { // Good: else 与 if 在不同行
...
}
下面是不符合规范的案例:
if ( someConditions ) { ... } else { ... } // Bad: else 与 if 在同一行
循环语句
规则3.7.1 循环语句必须使用大括号
和条件表达式类似,我们要求for/while循环语句必须加上大括号,即便循环体是空的,或循环语句只有一条。
for ( int i = 0 ; i < someRange ; i ++ ) { // Good: 使用了大括号
DoSomething ();
}
while ( condition ) { } // Good:循环体是空,使用大括号
while ( condition ) {
continue ; // Good:continue 表示空逻辑,使用大括号
}
坏的例子:
for ( int i = 0 ; i < someRange ; i ++ )
DoSomething (); // Bad: 应该加上括号
while ( condition ); // Bad:使用分号容易让人误解是while语句中的一部分
switch语句
规则3.8.1 switch 语句的 case/default 要缩进一层
switch 语句的缩进风格如下:
switch ( var ) {
case 0 : // Good: 缩进
DoSomething1 (); // Good: 缩进
break ;
case 1 : { // Good: 带大括号格式
DoSomething2 ();
break ;
}
default :
break ;
}
switch ( var ) {
case 0 : // Bad: case 未缩进
DoSomething ();
break ;
default : // Bad: default 未缩进
break ;
}
表达式
建议3.9.1 表达式换行要保持换行的一致性,运算符放行末
较长的表达式,不满足行宽要求的时候,需要在适当的地方换行。一般在较低优先级运算符或连接符后面截断,运算符或连接符放在行末。
运算符、连接符放在行末,表示“未结束,后续还有”。
例:
// 假设下面第一行已经不满足行宽要求
if (( currentValue > threshold ) && // Good:换行后,逻辑操作符放在行尾
someCondition ) {
DoSomething ();
...
}
int result = reallyReallyLongVariableName1 + // Good
reallyReallyLongVariableName2 ;
表达式换行后,注意保持合理对齐,或者4空格缩进。参考下面例子
int sum = longVariableName1 + longVariableName2 + longVariableName3 +
longVariableName4 + longVariableName5 + longVariableName6 ; // Good: 4空格缩进
int sum = longVariableName1 + longVariableName2 + longVariableName3 +
longVariableName4 + longVariableName5 + longVariableName6 ; // Good: 保持对齐
变量赋值
规则3.10.1 多个变量定义和赋值语句不允许写在一行
每行只有一个变量初始化的语句,更容易阅读和理解。
int maxCount = 10 ;
bool isCompleted = false ;
下面是不符合规范的示例:
int maxCount = 10 ; bool isCompleted = false ; // Bad:多个变量初始化需要分开放在多行,每行一个变量初始化
int x , y = 0 ; // Bad:多个变量定义需要分行,每行一个
int pointX ;
int pointY ;
...
pointX = 1 ; pointY = 2 ; // Bad:多个变量赋值语句放同一行
例外:for 循环头、if 初始化语句(C++17)、结构化绑定语句(C++17)中可以声明和初始化多个变量。这些语句中的多个变量声明有较强关联,如果强行分成多行会带来作用域不一致,声明和初始化割裂等问题。
初始化
初始化包括结构体、联合体、及数组的初始化
规则3.11.1 初始化换行时要有缩进,并进行合理对齐
结构体或数组初始化时,如果换行应保持4空格缩进。
从可读性角度出发,选择换行点和对齐位置。
const int rank [] = {
16 , 16 , 16 , 16 , 32 , 32 , 32 , 32 ,
64 , 64 , 64 , 64 , 32 , 32 , 32 , 32
};
指针与引用
建议3.12.1 指针类型"*
“跟随变量名或者类型,不要两边都留有或者都没有空格
指针命名: *
靠左靠右都可以,但是不要两边都有或者都没有空格。
int * p = nullptr ; // Good
int * p = nullptr ; // Good
int * p = nullptr ; // Bad
int * p = nullptr ; // Bad
例外:当变量被 const 修饰时,"*
” 无法跟随变量,此时也不要跟随类型。
const char * const VERSION = "V100" ;
建议3.12.2 引用类型"&
“跟随变量名或者类型,不要两边都留有或者都没有空格
引用命名:&
靠左靠右都可以,但是不要两边都有或者都没有空格。
int i = 8 ;
int & p = i ; // Good
int & p = i ; // Good
int *& rp = pi ; // Good,指针的引用,*& 一起跟随类型
int *& rp = pi ; // Good,指针的引用,*& 一起跟随变量名
int * & rp = pi ; // Good,指针的引用,* 跟随类型,& 跟随变量名
int & p = i ; // Bad
int & p = i ; // Bad
编译预处理
规则3.13.1 编译预处理的”#“统一放在行首,嵌套编译预处理语句时,”#“可以进行缩进
编译预处理的”#“统一放在行首,即使编译预处理的代码是嵌入在函数体中的,”#“也应该放在行首。
规则3.13.2 避免使用宏
宏会忽略作用域,类型系统以及各种规则,容易引发问题。应尽量避免使用宏定义,如果必须使用宏,要保证证宏名的唯一性。
在C++中,有许多方式来避免使用宏:
用const或enum定义易于理解的常量
用namespace避免名字冲突
用inline函数避免函数调用的开销
用template函数来处理多种类型
在文件头保护宏、条件编译、日志记录等必要场景中可以使用宏。
规则3.13.3 禁止使用宏来表示常量
宏是简单的文本替换,在预处理阶段完成,运行报错时直接报相应的值;跟踪调试时也是显示值,而不是宏名; 宏没有类型检查,不安全; 宏没有作用域。
规则3.13.4 禁止使用函数式宏
宏义函数式宏前,应考虑能否用函数替代。对于可替代场景,建议用函数替代宏。
函数式宏的缺点如下:
函数式宏缺乏类型检查,不如函数调用检查严格
宏展开时宏参数不求值,可能会产生非预期结果
宏没有独立的作用域
宏的技巧性太强,例如#的用法和无处不在的括号,影响可读性
在特定场景中必须用编译器对宏的扩展语法,如GCC的statement expression,影响可移植性
宏在预编译阶段展开后,在期后编译、链接和调试时都不可见;而且包含多行的宏会展开为一行。函数式宏难以调试、难以打断点,不利于定位问题
对于包含大量语句的宏,在每个调用点都要展开。如果调用点很多,会造成代码空间的膨胀
函数没有宏的上述缺点。但是,函数相比宏,最大的劣势是执行效率不高(增加函数调用的开销和编译器优化的难度)。
为此,可以在必要时使用内联函数。内联函数跟宏类似,也是在调用点展开。不同之处在于内联函数是在编译时展开。
内联函数兼具函数和宏的优点:
内联函数执行严格的类型检查
内联函数的参数求值只会进行一次
内联函数就地展开,没有函数调用的开销
内联函数比函数优化得更好
对于性能要求高的产品代码,可以考虑用内联函数代替函数。
例外:
在日志记录场景中,需要通过函数式宏保持调用点的文件名(FILE )、行号(LINE )等信息。
空格和空行
规则3.14.1 水平空格应该突出关键字和重要信息,避免不必要的留白
水平空格应该突出关键字和重要信息,每行代码尾部不要加空格。总体规则如下:
if, switch, case, do, while, for等关键字之后加空格;
小括号内部的两侧,不要加空格;
大括号内部两侧有无空格,左右必须保持一致;
一元操作符(& * + ‐ ~ !)之后不要加空格;
二元操作符(= + ‐ < > * / % | & ^ <= >= == != )左右两侧加空格
三目运算符(? :)符号两侧均需要空格
前置和后置的自增、自减(++ –)和变量之间不加空格
结构体成员操作符(. ->)前后不加空格
逗号(,)前面不加空格,后面增加空格
对于模板和类型转换(<>)和类型之间不要添加空格
域操作符(::)前后不要添加空格
冒号(:)前后根据情况来判断是否要添加空格
常规情况:
void Foo ( int b ) { // Good:大括号前应该留空格
int i = 0 ; // Good:变量初始化时,=前后应该有空格,分号前面不要留空格
int buf [ BUF_SIZE ] = { 0 }; // Good:大括号内两侧都无空格
函数定义和函数调用:
int result = Foo ( arg1 , arg2 );
^ // Bad: 逗号后面需要增加空格
int result = Foo ( arg1 , arg2 );
^ ^ // Bad: 函数参数列表的左括号后面不应该有空格,右括号前面不应该有空格
指针和取地址
x = * p ; // Good:*操作符和指针p之间不加空格
p = & x ; // Good:&操作符和变量x之间不加空格
x = r . y ; // Good:通过.访问成员变量时不加空格
x = r -> y ; // Good:通过->访问成员变量时不加空格
操作符:
x = 0 ; // Good:赋值操作的=前后都要加空格
x = - 5 ; // Good:负数的符号和数值之前不要加空格
++ x ; // Good:前置和后置的++/--和变量之间不要加空格
x -- ;
if ( x && ! y ) // Good:布尔操作符前后要加上空格,!操作和变量之间不要空格
v = w * x + y / z ; // Good:二元操作符前后要加空格
v = w * ( x + z ); // Good:括号内的表达式前后不需要加空格
int a = ( x < y ) ? x : y ; // Good: 三目运算符, ?和:前后需要添加空格
循环和条件语句:
if ( condition ) { // Good:if关键字和括号之间加空格,括号内条件语句前后不加空格
...
} else { // Good:else关键字和大括号之间加空格
...
}
while ( condition ) {} // Good:while关键字和括号之间加空格,括号内条件语句前后不加空格
for ( int i = 0 ; i < someRange ; ++ i ) { // Good:for关键字和括号之间加空格,分号之后加空格
...
}
switch ( condition ) { // Good: switch 关键字后面有1空格
case 0 : // Good:case语句条件和冒号之间不加空格
...
break ;
...
default :
...
break ;
}
模板和转换
// 尖括号(< and >) 不与空格紧邻, < 前没有空格, > 和 ( 之间也没有.
vector < string > x ;
y = static_cast < char *> ( x );
// 在类型与指针操作符之间留空格也可以, 但要保持一致.
vector < char *> x ;
域操作符
std :: cout ; // Good: 命名空间访问,不要留空格
int MyClass :: GetValue () const {} // Good: 对于成员函数定义,不要留空格
冒号
// 添加空格的场景
// Good: 类的派生需要留有空格
class Sub : public Base {
};
// 构造函数初始化列表需要留有空格
MyClass :: MyClass ( int var ) : someVar_ ( var )
{
DoSomething ();
}
// 位域表示也留有空格
struct XX {
char a : 4 ;
char b : 5 ;
char c : 4 ;
};
// 不添加空格的场景
// Good: 对于public:, private:这种类访问权限的冒号不用添加空格
class MyClass {
public :
MyClass ( int var );
private :
int someVar_ ;
};
// 对于switch-case的case和default后面的冒号不用添加空格
switch ( value )
{
case 1 :
DoSomething ();
break ;
default :
break ;
}
注意:当前的集成开发环境(IDE)可以设置删除行尾的空格,请正确配置。
建议3.14.1 合理安排空行,保持代码紧凑
减少不必要的空行,可以显示更多的代码,方便代码阅读。下面有一些建议遵守的规则:
根据上下内容的相关程度,合理安排空行;
函数内部、类型定义内部、宏内部、初始化表达式内部,不使用连续空行
不使用连续 3 个空行,或更多
大括号内的代码块行首之前和行尾之后不要加空行,但namespace的大括号内不作要求。
int Foo ()
{
...
}
int Bar () // Bad:最多使用连续2个空行。
{
...
}
if (...) {
// Bad:大括号内的代码块行首不要加入空行
...
// Bad:大括号内的代码块行尾不要加入空行
}
int Foo (...)
{
// Bad:函数体内行首不要加空行
...
}
类
规则3.15.1 类访问控制块的声明依次序是 public:, protected:, private:,缩进和 class 关键字对齐
class MyClass : public BaseClass {
public : // 注意没有缩进
MyClass (); // 标准的4空格缩进
explicit MyClass ( int var );
~ MyClass () {}
void SomeFunction ();
void SomeFunctionThatDoesNothing ()
{
}
void SetVar ( int var ) { someVar_ = var ; }
int GetVar () const { return someVar_ ; }
private :
bool SomeInternalFunction ();
int someVar_ ;
int someOtherVar_ ;
};
在各个部分中,建议将类似的声明放在一起, 并且建议以如下的顺序: 类型 (包括 typedef, using 和嵌套的结构体与类), 常量, 工厂函数, 构造函数, 赋值运算符, 析构函数, 其它成员函数, 数据成员。
规则3.15.2 构造函数初始化列表放在同一行或按四格缩进并排多行
// 如果所有变量能放在同一行:
MyClass :: MyClass ( int var ) : someVar_ ( var )
{
DoSomething ();
}
// 如果不能放在同一行,
// 必须置于冒号后, 并缩进4个空格
MyClass :: MyClass ( int var )
: someVar_ ( var ), someOtherVar_ ( var + 1 ) // Good: 逗号后面留有空格
{
DoSomething ();
}
// 如果初始化列表需要置于多行, 需要逐行对齐
MyClass :: MyClass ( int var )
: someVar_ ( var ), // 缩进4个空格
someOtherVar_ ( var + 1 )
{
DoSomething ();
}
4 注释
一般的,尽量通过清晰的架构逻辑,好的符号命名来提高代码可读性;需要的时候,才辅以注释说明。
注释是为了帮助阅读者快速读懂代码,所以要从读者的角度出发,按需注释 。
注释内容要简洁、明了、无二义性,信息全面且不冗余。
注释跟代码一样重要。
写注释时要换位思考,用注释去表达此时读者真正需要的信息。在代码的功能、意图层次上进行注释,即注释解释代码难以表达的意图,不要重复代码信息。
修改代码时,也要保证其相关注释的一致性。只改代码,不改注释是一种不文明行为,破坏了代码与注释的一致性,让阅读者迷惑、费解,甚至误解。
使用英文进行注释。
注释风格
在 C++ 代码中,使用 /*
*/
和 //
都是可以的。
按注释的目的和位置,注释可分为不同的类型,如文件头注释、函数头注释、代码注释等等;
同一类型的注释应该保持统一的风格。
注意:本文示例代码中,大量使用 ‘//’ 后置注释只是为了更精确的描述问题,并不代表这种注释风格更好。
文件头注释
规则3.1 文件头注释必须包含版权许可
/*
*
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an “AS IS” BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
函数头注释
规则4.3.1 公有(public)函数必须编写函数头注释
公有函数属于类对外提供的接口,调用者需要了解函数的功能、参数的取值范围、返回的结果、注意事项等信息才能正常使用。
特别是参数的取值范围、返回的结果、注意事项等都无法做到自注示,需要编写函数头注释辅助说明。
规则4.3.2 禁止空有格式的函数头注释
并不是所有的函数都需要函数头注释;
函数签名无法表达的信息,加函数头注释辅助说明;
函数头注释统一放在函数声明或定义上方,使用如下风格之一:
使用//
写函数头
// 单行函数头
int Func1 ( void );
// 多行函数头
// 第二行
int Func2 ( void );
使用/* */
写函数头
/* 单行函数头 */
int Func1 ( void );
/*
* 另一种单行函数头
*/
int Func2 ( void );
/*
* 多行函数头
* 第二行
*/
int Func3 ( void );
函数尽量通过函数名自注释,按需写函数头注释。
不要写无用、信息冗余的函数头;不要写空有格式的函数头。
函数头注释内容可选,但不限于:功能说明、返回值,性能约束、用法、内存约定、算法实现、可重入的要求等等。
模块对外头文件中的函数接口声明,其函数头注释,应当将重要、有用的信息表达清楚。
例:
/*
* 返回实际写入的字节数,-1表示写入失败
* 注意,内存 buf 由调用者负责释放
*/
int WriteString ( const char * buf , int len );
坏的例子:
/*
* 函数名:WriteString
* 功能:写入字符串
* 参数:
* 返回值:
*/
int WriteString ( const char * buf , int len );
上面例子中的问题:
参数、返回值,空有格式没内容
函数名信息冗余
关键的 buf 由谁释放没有说清楚
代码注释
规则4.4.1 代码注释放于对应代码的上方或右边
规则4.4.2 注释符与注释内容间要有1空格;右置注释与前面代码至少1空格
代码上方的注释,应该保持对应代码一样的缩进。
选择并统一使用如下风格之一:
使用//
// 这是单行注释
DoSomething ();
// 这是多行注释
// 第二行
DoSomething ();
使用/*' '*/
/* 这是单行注释 */
DoSomething ();
/*
* 另一种方式的多行注释
* 第二行
*/
DoSomething ();
代码右边的注释,与代码之间,至少留1空格,建议不超过4空格。
通常使用扩展后的 TAB 键即可实现 1-4 空格的缩进。
选择并统一使用如下风格之一:
int foo = 100 ; // 放右边的注释
int bar = 200 ; /* 放右边的注释 */
右置格式在适当的时候,上下对齐会更美观。
对齐后的注释,离左边代码最近的那一行,保证1-4空格的间隔。
例:
const int A_CONST = 100 ; /* 相关的同类注释,可以考虑上下对齐 */
const int ANOTHER_CONST = 200 ; /* 上下对齐时,与左侧代码保持间隔 */
当右置的注释超过行宽时,请考虑将注释置于代码上方。
规则4.4.3 不用的代码段直接删除,不要注释掉
被注释掉的代码,无法被正常维护;当企图恢复使用这段代码时,极有可能引入易被忽略的缺陷。
正确的做法是,不需要的代码直接删除掉。若再需要时,考虑移植或重写这段代码。
这里说的注释掉代码,包括用 /* */ 和 //,还包括 #if 0, #ifdef NEVER_DEFINED 等等。
5 头文件
头文件职责
头文件是模块或文件的对外接口,头文件的设计体现了大部分的系统设计。
头文件中适合放置接口的声明,不适合放置实现(内联函数除外)。对于cpp文件中内部才需要使用的函数、宏、枚举、结构定义等不要放在头文件中。
头文件应当职责单一。头文件过于复杂,依赖过于复杂还是导致编译时间过长的主要原因。
建议5.1.1 每一个.cpp文件应有一个对应的.h文件,用于声明需要对外公开的类与接口
通常情况下,每个.cpp文件都有一个相应的.h,用于放置对外提供的函数声明、宏定义、类型定义等。
如果一个.cpp文件不需要对外公布任何接口,则其就不应当存在。
例外:程序的入口(如main函数所在的文件),单元测试代码,动态库代码。
示例:
// Foo.h
#ifndef FOO_H
#define FOO_H
class Foo {
public :
Foo ();
void Fun ();
private :
int value_ ;
};
#endif
// Foo.cpp
#include "Foo.h"
namespace { // Good: 对内函数的声明放在.cpp文件的头部,并声明为匿名namespace或者static限制其作用域
void Bar ()
{
}
}
...
void Foo :: Fun ()
{
Bar ();
}
头文件依赖
规则5.2.1 禁止头文件循环依赖
头文件循环依赖,指 a.h 包含 b.h,b.h 包含 c.h,c.h 包含 a.h, 导致任何一个头文件修改,都导致所有包含了a.h/b.h/c.h的代码全部重新编译一遍。
而如果是单向依赖,如a.h包含b.h,b.h包含c.h,而c.h不包含任何头文件,则修改a.h不会导致包含了b.h/c.h的源代码重新编译。
头文件循环依赖直接体现了架构设计上的不合理,可通过优化架构去避免。
规则5.2.2 头文件必须编写#define
保护,防止重复包含
为防止头文件被重复包含,所有头文件都应当使用 #define 保护;不要使用 #pragma once
定义包含保护符时,应该遵守如下规则:
1)保护符使用唯一名称;
2)不要在受保护部分的前后放置代码或者注释,文件头注释除外。
示例:假定timer模块的timer.h,其目录为timer/include/timer.h,应按如下方式保护:
#ifndef TIMER_INCLUDE_TIMER_H
#define TIMER_INCLUDE_TIMER_H
...
#endif
规则5.2.3 禁止通过声明的方式引用外部函数接口、变量
只能通过包含头文件的方式使用其他模块或文件提供的接口。
通过 extern 声明的方式使用外部函数接口、变量,容易在外部接口改变时可能导致声明和定义不一致。
同时这种隐式依赖,容易导致架构腐化。
不符合规范的案例:
// a.cpp内容
extern int Fun (); // Bad: 通过extern的方式使用外部函数
void Bar ()
{
int i = Fun ();
...
}
// b.cpp内容
int Fun ()
{
// Do something
}
应该改为:
// a.cpp内容
#include "b.h" // Good: 通过包含头文件的方式使用其他.cpp提供的接口
void Bar ()
{
int i = Fun ();
...
}
// b.h内容
// b.cpp内容
int Fun ()
{
// Do something
}
例外,有些场景需要引用其内部函数,但并不想侵入代码时,可以 extern 声明方式引用。
如:
针对某一内部函数进行单元测试时,可以通过 extern 声明来引用被测函数;
当需要对某一函数进行打桩、打补丁处理时,允许 extern 声明该函数。
规则5.2.4 禁止在extern “C"中包含头文件
在 extern “C” 中包含头文件,有可能会导致 extern “C” 嵌套,部分编译器对 extern “C” 嵌套层次有限制,嵌套层次太多会编译错误。
在C,C++混合编程的情况下,在extern “C"中包含头文件,可能会导致被包含头文件的原有意图遭到破坏,比如链接规范被不正确地更改。
示例,存在a.h和b.h两个头文件:
// a.h内容
...
#ifdef __cplusplus
void Foo ( int );
#define A(value) Foo(value)
#else
void A ( int )
#endif
// b.h内容
...
#ifdef __cplusplus
extern "C" {
#endif
#include "a.h"
void B ();
#ifdef __cplusplus
}
#endif
使用C++预处理器展开b.h,将会得到
extern "C" {
void Foo ( int );
void B ();
}
按照 a.h 作者的本意,函数 Foo 是一个 C++ 自由函数,其链接规范为 “C++"。
但在 b.h 中,由于 #include "a.h"
被放到了 extern "C"
的内部,函数 Foo 的链接规范被不正确地更改了。
例外:
如果在 C++ 编译环境中,想引用纯C的头文件,这些C头文件并没有 extern "C"
修饰。非侵入式的做法是,在 extern "C"
中去包含C头文件。
建议5.2.1尽量避免使用前置声明,而是通过#include
来包含头文件
前置声明(forward declaration)通常指类、模板的纯粹声明,没伴随着其定义。
优点:
前置声明能够节省编译时间,多余的 #include 会迫使编译器展开更多的文件,处理更多的输入。
前置声明能够节省不必要的重新编译的时间。 #include 使代码因为头文件中无关的改动而被重新编译多次。
缺点:
前置声明隐藏了依赖关系,头文件改动时,用户的代码会跳过必要的重新编译过程。
前置声明可能会被库的后续更改所破坏。前置声明模板有时会妨碍头文件开发者变动其 API. 例如扩大形参类型,加个自带默认参数的模板形参等等。
前置声明来自命名空间 std::
的 symbol 时,其行为未定义(在C++11标准规范中明确说明)。
前置声明了不少来自头文件的 symbol 时,就会比单单一行的 include 冗长。
仅仅为了能前置声明而重构代码(比如用指针成员代替对象成员)会使代码变得更慢更复杂。
很难判断什么时候该用前置声明,什么时候该用#include
,某些场景下面前置声明和#include
互换以后会导致意想不到的结果。
所以我们尽可能避免使用前置声明,而是使用#include头文件来保证依赖关系。
6 作用域
命名空间
建议6.1.1 对于cpp文件中不需要导出的变量,常量或者函数,请使用匿名namespace封装或者用static修饰
在C++ 2003标准规范中,使用static修饰文件作用域的变量,函数等被标记为deprecated特性,所以更推荐使用匿名namespace。
主要原因如下:
static在C++中已经赋予了太多的含义,静态函数成员变量,静态成员函数,静态全局变量,静态函数局部变量,每一种都有特殊的处理。
static只能保证变量,常量和函数的文件作用域,但是namespace还可以封装类型等。
统一namespace来处理C++的作用域,而不需要同时使用static和namespace来管理。
static修饰的函数不能用来实例化模板,而匿名namespace可以。
但是不要在 .h 中使用中使用匿名namespace或者static。
// Foo.cpp
namespace {
const int MAX_COUNT = 20 ;
void InternalFun () {};
}
void Foo :: Fun ()
{
int i = MAX_COUNT ;
InternalFun ();
}
规则6.1.1 不要在头文件中或者#include之前使用using导入命名空间
说明:使用using导入命名空间会影响后续代码,易造成符号冲突,所以不要在头文件以及源文件中的#include之前使用using导入命名空间。
示例:
// 头文件a.h
namespace NamespaceA {
int Fun ( int );
}
// 头文件b.h
namespace NamespaceB {
int Fun ( int );
}
using namespace NamespaceB ;
void G ()
{
Fun ( 1 );
}
// 源代码a.cpp
#include "a.h"
using namespace NamespaceA ;
#include "b.h"
void main ()
{
G (); // using namespace NamespaceA在#include “b.h”之前,引发歧义:NamespaceA::Fun,NamespaceB::Fun调用不明确
}
对于在头文件中使用using导入单个符号或定义别名,允许在模块自定义名字空间中使用,但禁止在全局名字空间中使用。
// foo.h
#include <fancy/string>
using fancy :: string ; // Bad,禁止向全局名字空间导入符号
namespace Foo {
using fancy :: string ; // Good,可以在模块自定义名字空间中导入符号
using MyVector = fancy :: vector < int > ; // Good,C++11可在自定义名字空间中定义别名
}
全局函数和静态成员函数
建议6.2.1 优先使用命名空间来管理全局函数,如果和某个class有直接关系的,可以使用静态成员函数
说明:非成员函数放在名字空间内可避免污染全局作用域, 也不要用类+静态成员方法来简单管理全局函数。 如果某个全局函数和某个类有紧密联系, 那么可以作为类的静态成员函数。
如果你需要定义一些全局函数,给某个cpp文件使用,那么请使用匿名namespace来管理。
namespace MyNamespace {
int Add ( int a , int b );
}
class File {
public :
static File CreateTempFile ( const std :: string & fileName );
};
全局常量和静态成员常量
建议6.3.1 优先使用命名空间来管理全局常量,如果和某个class有直接关系的,可以使用静态成员常量
说明:全局常量放在命名空间内可避免污染全局作用域, 也不要用类+静态成员常量来简单管理全局常量。 如果某个全局常量和某个类有紧密联系, 那么可以作为类的静态成员常量。
如果你需要定义一些全局常量,只给某个cpp文件使用,那么请使用匿名namespace来管理。
namespace MyNamespace {
const int MAX_SIZE = 100 ;
}
class File {
public :
static const std :: string SEPARATOR ;
};
全局变量
建议6.4.1 尽量避免使用全局变量,考虑使用单例模式
说明:全局变量是可以修改和读取的,那么这样会导致业务代码和这个全局变量产生数据耦合。
int g_counter = 0 ;
// a.cpp
g_counter ++ ;
// b.cpp
g_counter ++ ;
// c.cpp
cout << g_counter << endl ;
使用单实例模式
class Counter {
public :
static Counter & GetInstance ()
{
static Counter counter ;
return counter ;
} // 单实例实现简单举例
void Increase ()
{
value_ ++ ;
}
void Print () const
{
std :: cout << value_ << std :: endl ;
}
private :
Counter () : value_ ( 0 ) {}
private :
int value_ ;
};
// a.cpp
Counter :: GetInstance (). Increase ();
// b.cpp
Counter :: GetInstance (). Increase ();
// c.cpp
Counter :: GetInstance (). Print ();
实现单例模式以后,实现了全局唯一一个实例,和全局变量同样的效果,并且单实例提供了更好的封装性。
例外:有的时候全局变量的作用域仅仅是模块内部,这样进程空间里面就会有多个全局变量实例,每个模块持有一份,这种场景下是无法使用单例模式解决的。
7 类
构造,拷贝构造,赋值和析构函数
构造,拷贝,移动和析构函数提供了对象的生命周期管理方法:
构造函数(constructor): X()
拷贝构造函数(copy constructor):X(const X&)
拷贝赋值操作符(copy assignment):operator=(const X&)
移动构造函数(move constructor):X(X&&)
C++11以后提供
移动赋值操作符(move assignment):operator=(X&&)
C++11以后提供
析构函数(destructor):~X()
规则7.1.1 类的成员变量必须显式初始化
说明:如果类有成员变量,没有定义构造函数,又没有定义默认构造函数,编译器将自动生成一个构造函数,但编译器生成的构造函数并不会对成员变量进行初始化,对象状态处于一种不确定性。
例外:
如果类的成员变量具有默认构造函数,那么可以不需要显式初始化。
示例:如下代码没有构造函数,私有数据成员无法初始化:
class Message {
public :
void ProcessOutMsg ()
{
//…
}
private :
unsigned int msgID_ ;
unsigned int msgLength_ ;
unsigned char * msgBuffer_ ;
std :: string someIdentifier_ ;
};
Message message ; // message成员变量没有初始化
message . ProcessOutMsg (); // 后续使用存在隐患
// 因此,有必要定义默认构造函数,如下:
class Message {
public :
Message () : msgID_ ( 0 ), msgLength_ ( 0 ), msgBuffer_ ( nullptr )
{
}
void ProcessOutMsg ()
{
// …
}
private :
unsigned int msgID_ ;
unsigned int msgLength_ ;
unsigned char * msgBuffer_ ;
std :: string someIdentifier_ ; // 具有默认构造函数,不需要显式初始化
};
建议7.1.1 成员变量优先使用声明时初始化(C++11)和构造函数初始化列表初始化
说明:C++11的声明时初始化可以一目了然的看出成员初始值,应当优先使用。如果成员初始化值和构造函数相关,或者不支持C++11,则应当优先使用构造函数初始化列表来初始化成员。相比起在构造函数体中对成员赋值,初始化列表的代码更简洁,执行性能更好,而且可以对const成员和引用成员初始化。
class Message {
public :
Message () : msgLength_ ( 0 ) // Good,优先使用初始化列表
{
msgBuffer_ = nullptr ; // Bad,不推荐在构造函数中赋值
}
private :
unsigned int msgID_ { 0 }; // Good,C++11中使用
unsigned int msgLength_ ;
unsigned char * msgBuffer_ ;
};
规则7.1.2 为避免隐式转换,将单参数构造函数声明为explicit
说明:单参数构造函数如果没有用explicit声明,则会成为隐式转换函数。
示例:
class Foo {
public :
explicit Foo ( const string & name ) : name_ ( name )
{
}
private :
string name_ ;
};
void ProcessFoo ( const Foo & foo ){}
int main ( void )
{
std :: string test = "test" ;
ProcessFoo ( test ); // 编译不通过
return 0 ;
}
上面的代码编译不通过,因为ProcessFoo
需要的参数是Foo类型,传入的string类型不匹配。
如果将Foo构造函数的explicit关键字移除,那么调用ProcessFoo
传入的string就会触发隐式转换,生成一个临时的Foo对象。往往这种隐式转换是让人迷惑的,并且容易隐藏Bug,得到了一个不期望的类型转换。所以对于单参数的构造函数是要求explicit声明。
规则7.1.3 如果不需要拷贝构造函数、赋值操作符 / 移动构造函数、赋值操作符,请明确禁止
说明:如果用户不定义,编译器默认会生成拷贝构造函数和拷贝赋值操作符, 移动构造和移动赋值操作符(移动语义的函数C++11以后才有)。
如果我们不要使用拷贝构造函数,或者赋值操作符,请明确拒绝:
将拷贝构造函数或者赋值操作符设置为private,并且不实现:
class Foo {
private :
Foo ( const Foo & );
Foo & operator = ( const Foo & );
};
使用C++11提供的delete, 请参见后面现代C++的相关章节。
推荐继承NoCopyable、NoMovable,禁止使用DISALLOW_COPY_AND_MOVE,DISALLOW_COPY,DISALLOW_MOVE等宏。
class Foo : public NoCopyable , public NoMovable {
};
NoCopyable和NoMovable的实现:
class NoCopyable {
public :
NoCopyable () = default ;
NoCopyable ( const NoCopyable & ) = delete ;
NoCopyable & operator = ( NoCopyable & ) = delete ;
};
class NoMovable {
public :
NoMovable () = default ;
NoMovable ( NoMovable && ) noexcept = delete ;
NoMovable & operator = ( NoMovable && ) noexcept = delete ;
};
规则7.1.4 拷贝构造和拷贝赋值操作符应该是成对出现或者禁止
拷贝构造函数和拷贝赋值操作符都是具有拷贝语义的,应该同时出现或者禁止。
// 同时出现
class Foo {
public :
...
Foo ( const Foo & );
Foo & operator = ( const Foo & );
...
};
// 同时default, C++11支持
class Foo {
public :
Foo ( const Foo & ) = default ;
Foo & operator = ( const Foo & ) = default ;
};
// 同时禁止, C++11可以使用delete
class Foo {
private :
Foo ( const Foo & );
Foo & operator = ( const Foo & );
};
规则7.1.5 移动构造和移动赋值操作符应该是成对出现或者禁止
在C++11中增加了move操作,如果需要某个类支持移动操作,那么需要实现移动构造和移动赋值操作符。
移动构造函数和移动赋值操作符都是具有移动语义的,应该同时出现或者禁止。
// 同时出现
class Foo {
public :
...
Foo ( Foo && );
Foo & operator = ( Foo && );
...
};
// 同时default, C++11支持
class Foo {
public :
Foo ( Foo && ) = default ;
Foo & operator = ( Foo && ) = default ;
};
// 同时禁止, 使用C++11的delete
class Foo {
public :
Foo ( Foo && ) = delete ;
Foo & operator = ( Foo && ) = delete ;
};
规则7.1.6 禁止在构造函数和析构函数中调用虚函数
说明:在构造函数和析构函数中调用当前对象的虚函数,会导致未实现多态的行为。
在C++中,一个基类一次只构造一个完整的对象。
示例:类Base是基类,Sub是派生类
class Base {
public :
Base ();
virtual void Log () = 0 ; // 不同的派生类调用不同的日志文件
};
Base :: Base () // 基类构造函数
{
Log (); // 调用虚函数Log
}
class Sub : public Base {
public :
virtual void Log ();
};
当执行如下语句:
Sub sub;
会先执行Sub的构造函数,但首先调用Base的构造函数,由于Base的构造函数调用虚函数Log,此时Log还是基类的版本,只有基类构造完成后,才会完成派生类的构造,从而导致未实现多态的行为。
同样的道理也适用于析构函数。
规则7.1.7 多态基类中的拷贝构造函数、拷贝赋值操作符、移动构造函数、移动赋值操作符必须为非public函数或者为delete函数
如果报一个派生类对象直接赋值给基类对象,会发生切片,只拷贝或者移动了基类部分,损害了多态行为。
【反例】
如下代码中,基类没有定义拷贝构造函数或拷贝赋值操作符,编译器会自动生成这两个特殊成员函数,
如果派生类对象赋值给基类对象时就发生切片。可以将此例中的拷贝构造函数和拷贝赋值操作符声明为delete,编译器可检查出此类赋值行为。
class Base {
public :
Base () = default ;
virtual ~ Base () = default ;
...
virtual void Fun () { std :: cout << "Base" << std :: endl ;}
};
class Derived : public Base {
...
void Fun () override { std :: cout << "Derived" << std :: endl ; }
};
void Foo ( const Base & base )
{
Base other = base ; // 不符合:发生切片
other . Fun (); // 调用的时Base类的Fun函数
}
Derived d ;
Foo ( d ); // 传入的是派生类对象
将拷贝构造函数或者赋值操作符设置为private,并且不实现:
继承
规则7.2.1 基类的析构函数应该声明为virtual,不准备被继承的类需要声明为final
说明:只有基类析构函数是virtual,通过多态调用的时候才能保证派生类的析构函数被调用。
示例:基类的析构函数没有声明为virtual导致了内存泄漏。
class Base {
public :
virtual std :: string getVersion () = 0 ;
~ Base ()
{
std :: cout << "~Base" << std :: endl ;
}
};
class Sub : public Base {
public :
Sub () : numbers_ ( nullptr )
{
}
~ Sub ()
{
delete [] numbers_ ;
std :: cout << "~Sub" << std :: endl ;
}
int Init ()
{
const size_t numberCount = 100 ;
numbers_ = new ( std :: nothrow ) int [ numberCount ];
if ( numbers_ == nullptr ) {
return - 1 ;
}
...
}
std :: string getVersion ()
{
return std :: string ( "hello!" );
}
private :
int * numbers_ ;
};
int main ( int argc , char * args [])
{
Base * b = new Sub ();
delete b ;
return 0 ;
}
由于基类Base的析构函数没有声明为virtual,当对象被销毁时,只会调用基类的析构函数,不会调用派生类Sub的析构函数,导致内存泄漏。
例外:
NoCopyable、NoMovable这种没有任何行为,仅仅用来做标识符的类,可以不定义虚析构也不定义final。
规则7.2.2 禁止虚函数使用缺省参数值
说明:在C++中,虚函数是动态绑定的,但函数的缺省参数却是在编译时就静态绑定的。这意味着你最终执行的函数是一个定义在派生类,但使用了基类中的缺省参数值的虚函数。为了避免虚函数重载时,因参数声明不一致给使用者带来的困惑和由此导致的问题,规定所有虚函数均不允许声明缺省参数值。
示例:虚函数display缺省参数值text是由编译时刻决定的,而非运行时刻,没有达到多态的目的:
class Base {
public :
virtual void Display ( const std :: string & text = "Base!" )
{
std :: cout << text << std :: endl ;
}
virtual ~ Base (){}
};
class Sub : public Base {
public :
virtual void Display ( const std :: string & text = "Sub!" )
{
std :: cout << text << std :: endl ;
}
virtual ~ Sub (){}
};
int main ()
{
Base * base = new Sub ();
Sub * sub = new Sub ();
...
base -> Display (); // 程序输出结果: Base! 而期望输出:Sub!
sub -> Display (); // 程序输出结果: Sub!
delete base ;
delete sub ;
return 0 ;
};
规则7.2.3 禁止重新定义继承而来的非虚函数
说明:因为非虚函数无法实现动态绑定,只有虚函数才能实现动态绑定:只要操作基类的指针,即可获得正确的结果。
示例:
class Base {
public :
void Fun ();
};
class Sub : public Base {
public :
void Fun ();
};
Sub * sub = new Sub ();
Base * base = sub ;
sub -> Fun (); // 调用子类的Fun
base -> Fun (); // 调用父类的Fun
//...
多重继承
在实际开发过程中使用多重继承的场景是比较少的,因为多重继承使用过程中有下面的典型问题:
菱形继承所带来的数据重复,以及名字二义性。因此,C++引入了virtual继承来解决这类问题;
即便不是菱形继承,多个父类之间的名字也可能存在冲突,从而导致的二义性;
如果子类需要扩展或改写多个父类的方法时,造成子类的职责不明,语义混乱;
相对于委托,继承是一种白盒复用,即子类可以访问父类的protected成员, 这会导致更强的耦合。而多重继承,由于耦合了多个父类,相对于单根继承,这会产生更强的耦合关系。
多重继承具有下面的优点:
多重继承提供了一种更简单的组合来实现多种接口或者类的组装与复用。
所以,对于多重继承的只有下面几种情况下面才允许使用多重继承。
建议7.3.1 使用多重继承来实现接口分离与多角色组合
如果某个类需要实现多重接口,可以通过多重继承把多个分离的接口组合起来,类似 scala 语言的 traits 混入。
class Role1 {};
class Role2 {};
class Role3 {};
class Object1 : public Role1 , public Role2 {
// ...
};
class Object2 : public Role2 , public Role3 {
// ...
};
在C++标准库中也有类似的实现样例:
class basic_istream {};
class basic_ostream {};
class basic_iostream : public basic_istream , public basic_ostream {
};
重载
重载操作符要有充分理由,而且不要改变操作符原有语义,例如不要使用 ‘+’ 操作符来做减运算。
操作符重载令代码更加直观,但也有一些不足:
混淆直觉,误以为该操作和内建类型一样是高性能的,忽略了性能降低的可能;
问题定位时不够直观,按函数名查找比按操作符显然更方便。
重载操作符如果行为定义不直观(例如将‘+’ 操作符来做减运算),会让代码产生混淆。
赋值操作符的重载引入的隐式转换会隐藏很深的bug。可以定义类似Equals()、CopyFrom()等函数来替代=,==操作符。
8 函数
函数设计
规则8.1.1 避免函数过长,函数不超过50行(非空非注释)
函数应该可以一屏显示完 (50行以内),只做一件事情,而且把它做好。
过长的函数往往意味着函数功能不单一,过于复杂,或过分呈现细节,未进行进一步抽象。
例外:某些实现算法的函数,由于算法的聚合性与功能的全面性,可能会超过50行。
即使一个长函数现在工作的非常好, 一旦有人对其修改, 有可能出现新的问题, 甚至导致难以发现的bug。
建议将其拆分为更加简短并易于管理的若干函数,以便于他人阅读和修改代码。
内联函数
建议8.2.1 内联函数不超过10行(非空非注释)
说明 :内联函数具有一般函数的特性,它与一般函数不同之处只在于函数调用的处理。一般函数进行调用时,要将程序执行权转到被调用函数中,然后再返回到调用它的函数中;而内联函数在调用时,是将调用表达式用内联函数体来替换。
内联函数只适合于只有 1~10 行的小函数。对一个含有许多语句的大函数,函数调用和返回的开销相对来说微不足道,也没有必要用内联函数实现,一般的编译器会放弃内联方式,而采用普通的方式调用函数。
如果内联函数包含复杂的控制结构,如循环、分支(switch)、try-catch 等语句,一般编译器将该函数视同普通函数。
虚函数、递归函数不能被用来做内联函数 。
函数参数
建议8.3.1 函数参数使用引用取代指针
说明 :引用比指针更安全,因为它一定非空,且一定不会再指向其他目标;引用不需要检查非法的NULL指针。
如果是基于老平台开发的产品,则优先顺从原有平台的处理方式。
选择 const 避免参数被修改,让代码阅读者清晰地知道该参数不被修改,可大大增强代码可读性。
例外:当传入参数为编译期长度未知的数组时,可以使用指针而不是引用。
建议8.3.2 使用强类型参数,避免使用void*
尽管不同的语言对待强类型和弱类型有自己的观点,但是一般认为c/c++是强类型语言,既然我们使用的语言是强类型的,就应该保持这样的风格。
好处是尽量让编译器在编译阶段就检查出类型不匹配的问题。
使用强类型便于编译器帮我们发现错误,如下代码中注意函数 FooListAddNode 的使用:
struct FooNode {
struct List link ;
int foo ;
};
struct BarNode {
struct List link ;
int bar ;
}
void FooListAddNode ( void * node ) // Bad: 这里用 void * 类型传递参数
{
FooNode * foo = ( FooNode * ) node ;
ListAppend ( & g_FooList , & foo -> link );
}
void MakeTheList ()
{
FooNode * foo = nullptr ;
BarNode * bar = nullptr ;
...
FooListAddNode ( bar ); // Wrong: 这里本意是想传递参数 foo,但错传了 bar,却没有报错
}
可以使用模板函数来实现参数类型的变化。
可以使用基类指针来实现多态。
建议8.3.3 函数的参数个数不超过5个
函数的参数过多,会使得该函数易于受外部变化的影响,从而影响维护工作。函数的参数过多同时也会增大测试的工作量。
如果超过可以考虑:
看能否拆分函数
看能否将相关参数合在一起,定义结构体
9 C++其他特性
常量与初始化
不变的值更易于理解、跟踪和分析,所以应该尽可能地使用常量代替变量,定义值的时候,应该把const作为默认的选项。
规则9.1.1 不允许使用宏来表示常量
说明 :宏是简单的文本替换,在预处理阶段时完成,运行报错时直接报相应的值;跟踪调试时也是显示值,而不是宏名;宏没有类型检查,不安全;宏没有作用域。
#define MAX_MSISDN_LEN 20 // 不好
// C++请使用const常量
const int MAX_MSISDN_LEN = 20 ; // 好
// 对于C++11以上版本,可以使用constexpr
constexpr int MAX_MSISDN_LEN = 20 ;
建议9.1.1 一组相关的整型常量应定义为枚举
说明 :枚举比#define
或const int
更安全。编译器会检查参数值是否位于枚举取值范围内,避免错误发生。
// 好的例子:
enum Week {
SUNDAY ,
MONDAY ,
TUESDAY ,
WEDNESDAY ,
THURSDAY ,
FRIDAY ,
SATURDAY
};
enum Color {
RED ,
BLACK ,
BLUE
};
void ColorizeCalendar ( Week today , Color color );
ColorizeCalendar ( BLUE , SUNDAY ); // 编译报错,参数类型错误
// 不好的例子:
const int SUNDAY = 0 ;
const int MONDAY = 1 ;
const int BLACK = 0 ;
const int BLUE = 1 ;
bool ColorizeCalendar ( int today , int color );
ColorizeCalendar ( BLUE , SUNDAY ); // 不会报错
当枚举值需要对应到具体数值时,须在声明时显式赋值。否则不需要显式赋值,以避免重复赋值,降低维护(增加、删除成员)工作量。
// 好的例子:S协议里定义的设备ID值,用于标识设备类型
enum DeviceType {
DEV_UNKNOWN = - 1 ,
DEV_DSMP = 0 ,
DEV_ISMG = 1 ,
DEV_WAPPORTAL = 2
};
程序内部使用,仅用于分类的情况,不应该进行显式的赋值。
// 好的例子:程序中用来标识会话状态的枚举定义
enum SessionState {
INIT ,
CLOSED ,
WAITING_FOR_RESPONSE
};
应当尽量避免枚举值重复,如必须重复也要用已定义的枚举来修饰
enum RTCPType {
RTCP_SR = 200 ,
RTCP_MIN_TYPE = RTCP_SR ,
RTCP_RR = 201 ,
RTCP_SDES = 202 ,
RTCP_BYE = 203 ,
RTCP_APP = 204 ,
RTCP_RTPFB = 205 ,
RTCP_PSFB = 206 ,
RTCP_XR = 207 ,
RTCP_RSI = 208 ,
RTCP_PUBPORTS = 209 ,
RTCP_MAX_TYPE = RTCP_PUBPORTS
};
规则9.1.2 不允许使用魔鬼数字
所谓魔鬼数字即看不懂、难以理解的数字。
魔鬼数字并非一个非黑即白的概念,看不懂也有程度,需要自行判断。
例如数字 12,在不同的上下文中情况是不一样的:
type = 12; 就看不懂,但 monthsCount = yearsCount * 12
; 就能看懂。
数字 0 有时候也是魔鬼数字,比如 status = 0
; 并不能表达是什么状态。
解决途径:
对于局部使用的数字,可以增加注释说明
对于多处使用的数字,必须定义 const 常量,并通过符号命名自注释。
禁止出现下列情况:
没有通过符号来解释数字含义,如 const int ZERO = 0
符号命名限制了其取值,如 const int XX_TIMER_INTERVAL_300MS = 300
,直接使用XX_TIMER_INTERVAL_MS
来表示该常量是定时器的时间间隔。
规则9.1.3 常量应该保证单一职责
说明 :一个常量只用来表示一个特定功能,即一个常量不能有多种用途。
// 好的例子:协议A和协议B,手机号(MSISDN)的长度都是20。
const unsigned int A_MAX_MSISDN_LEN = 20 ;
const unsigned int B_MAX_MSISDN_LEN = 20 ;
// 或者使用不同的名字空间:
namespace Namespace1 {
const unsigned int MAX_MSISDN_LEN = 20 ;
}
namespace Namespace2 {
const unsigned int MAX_MSISDN_LEN = 20 ;
}
规则9.1.4 禁止用memcpy_s、memset_s初始化非POD对象
说明 :POD
全称是Plain Old Data
,是C++ 98标准(ISO/IEC 14882, first edition, 1998-09-01)中引入的一个概念,POD
类型主要包括int
, char
, float
,double
,enumeration
,void
,指针等原始类型以及聚合类型,不能使用封装和面向对象特性(如用户定义的构造/赋值/析构函数、基类、虚函数等)。
由于非POD类型比如非聚合类型的class对象,可能存在虚函数,内存布局不确定,跟编译器有关,滥用内存拷贝可能会导致严重的问题。
即使对聚合类型的class,使用直接的内存拷贝和比较,破坏了信息隐蔽和数据保护的作用,也不提倡memcpy_s
、memset_s
操作。
对于POD类型的详细说明请参见附录。
建议9.1.2 变量使用时才声明并初始化
说明 :变量在使用前未赋初值,是常见的低级编程错误。使用前才声明变量并同时初始化,非常方便地避免了此类低级错误。
在函数开始位置声明所有变量,后面才使用变量,作用域覆盖整个函数实现,容易导致如下问题:
程序难以理解和维护:变量的定义与使用分离。
变量难以合理初始化:在函数开始时,经常没有足够的信息进行变量初始化,往往用某个默认的空值(比如零)来初始化,这通常是一种浪费,如果变量在被赋于有效值以前使用,还会导致错误。
遵循变量作用域最小化原则与就近声明原则, 使得代码更容易阅读,方便了解变量的类型和初始值。特别是,应使用初始化的方式替代声明再赋值。
// 不好的例子:声明与初始化分离
string name ; // 声明时未初始化:调用缺省构造函数
name = "zhangsan" ; // 再次调用赋值操作符函数;声明与定义在不同的地方,理解相对困难
// 好的例子:声明与初始化一体,理解相对容易
string name ( "zhangsan" ); // 调用构造函数
表达式
规则9.2.1 含有变量自增或自减运算的表达式中禁止再次引用该变量
含有变量自增或自减运算的表达式中,如果再引用该变量,其结果在C++标准中未明确定义。各个编译器或者同一个编译器不同版本实现可能会不一致。
为了更好的可移植性,不应该对标准未定义的运算次序做任何假设。
注意,运算次序的问题不能使用括号来解决,因为这不是优先级的问题。
示例:
x = b [ i ] + i ++ ; // Bad: b[i]运算跟 i++,先后顺序并不明确。
正确的写法是将自增或自减运算单独放一行:
x = b [ i ] + i ;
i ++ ; // Good: 单独一行
函数参数
Func ( i ++ , i ); // Bad: 传递第2个参数时,不确定自增运算有没有发生
正确的写法
i ++ ; // Good: 单独一行
x = Func ( i , i );
规则9.2.2 switch语句要有default分支
大部分情况下,switch语句中要有default分支,保证在遗漏case标签处理时能够有一个缺省的处理行为。
特例:
如果switch条件变量是枚举类型,并且 case 分支覆盖了所有取值,则加上default分支处理有些多余。
现代编译器都具备检查是否在switch语句中遗漏了某些枚举值的case分支的能力,会有相应的warning提示。
enum Color {
RED = 0 ,
BLUE
};
// 因为switch条件变量是枚举值,这里可以不用加default处理分支
switch ( color ) {
case RED :
DoRedThing ();
break ;
case BLUE :
DoBlueThing ();
...
break ;
}
建议9.2.1 表达式的比较,应当遵循左侧倾向于变化、右侧倾向于不变的原则
当变量与常量比较时,如果常量放左边,如 if (MAX == v) 不符合阅读习惯,而 if (MAX > v) 更是难于理解。
应当按人的正常阅读、表达习惯,将常量放右边。写成如下方式:
if ( value == MAX ) {
}
if ( value < MAX ) {
}
也有特殊情况,如:if (MIN < value && value < MAX)
用来描述区间时,前半段是常量在左的。
不用担心将 ‘==’ 误写成 ‘=’,因为 if (value = MAX)
会有编译告警,其他静态检查工具也会报错。让工具去解决笔误问题,代码要符合可读性第一。
建议9.2.2 使用括号明确操作符的优先级
使用括号明确操作符的优先级,防止因默认的优先级与设计思想不符而导致程序出错;同时使得代码更为清晰可读,然而过多的括号会分散代码使其降低了可读性。下面是如何使用括号的建议。
二元及以上操作符, 如果涉及多种操作符,则应该使用括号
x = a + b + c ; /* 操作符相同,可以不加括号 */
x = Foo ( a + b , c ); /* 逗号两边的表达式,不需要括号 */
x = 1 << ( 2 + 3 ); /* 操作符不同,需要括号 */
x = a + ( b / 5 ); /* 操作符不同,需要括号 */
x = ( a == b ) ? a : ( a – b ); /* 操作符不同,需要括号 */
类型转换
避免使用类型分支来定制行为:类型分支来定制行为容易出错,是企图用C++编写C代码的明显标志。这是一种很不灵活的技术,要添加新类型时,如果忘记修改所有分支,编译器也不会告知。使用模板和虚函数,让类型自己而不是调用它们的代码来决定行为。
建议避免类型转换,我们在代码的类型设计上应该考虑到每种数据的数据类型是什么,而不是应该过度使用类型转换来解决问题。在设计某个基本类型的时候,请考虑:
是无符号还是有符号的
是适合float还是double
是使用int8,int16,int32还是int64,确定整形的长度
但是我们无法禁止使用类型转换,因为C++语言是一门面向机器编程的语言,涉及到指针地址,并且我们会与各种第三方或者底层API交互,他们的类型设计不一定是合理的,在这个适配的过程中很容易出现类型转换。
例外:在调用某个函数的时候,如果我们不想处理函数结果,首先要考虑这个是否是你的最好的选择。如果确实不想处理函数的返回值,那么可以使用(void)转换来解决。
规则9.3.1 如果确定要使用类型转换,请使用由C++提供的类型转换,而不是C风格的类型转换
说明 :
C++提供的类型转换操作比C风格更有针对性,更易读,也更加安全,C++提供的转换有:
dynamic_cast
:主要用于继承体系下行转换,dynamic_cast
具有类型检查的功能,请做好基类和派生类的设计,避免使用dynamic_cast来进行转换。
static_cast
:和C风格转换相似可做值的强制转换,或上行转换(把派生类的指针或引用转换成基类的指针或引用)。该转换经常用于消除多重继承带来的类型歧义,是相对安全的。如果是纯粹的算数转换,那么请使用后面的大括号转换方式。
reinterpret_cast
:用于转换不相关的类型。reinterpret_cast
强制编译器将某个类型对象的内存重新解释成另一种类型,这是一种不安全的转换,建议尽可能少用reinterpret_cast
。
const_cast
:用于移除对象的const
属性,使对象变得可修改,这样会破坏数据的不变性,建议尽可能少用。
算数转换: (C++11开始支持)
对于那种算数转换,并且类型信息没有丢失的,比如float到double, int32到int64的转换,推荐使用大括号的初始方式。
double d { someFloat };
int64_t i { someInt32 };
建议9.3.1 避免使用dynamic_cast
dynamic_cast
依赖于C++的RTTI, 让程序员在运行时识别C++类对象的类型。
dynamic_cast
的出现一般说明我们的基类和派生类设计出现了问题,派生类破坏了基类的契约,不得不通过dynamic_cast
转换到子类进行特殊处理,这个时候更希望来改善类的设计,而不是通过dynamic_cast
来解决问题。
建议9.3.2 避免使用reinterpret_cast
说明 :reinterpret_cast
用于转换不相关类型。尝试用reinterpret_cast
将一种类型强制转换另一种类型,这破坏了类型的安全性与可靠性,是一种不安全的转换。不同类型之间尽量避免转换。
建议9.3.3 避免使用const_cast
说明 :const_cast
用于移除对象的const
和volatile
性质。
使用const_cast转换后的指针或者引用来修改const对象,行为是未定义的。
// 不好的例子
const int i = 1024 ;
int * p = const_cast < int *> ( & i );
* p = 2048 ; // 未定义行为
// 不好的例子
class Foo {
public :
Foo () : i ( 3 ) {}
void Fun ( int v )
{
i = v ;
}
private :
int i ;
};
int main ( void )
{
const Foo f ;
Foo * p = const_cast < Foo *> ( & f );
p -> Fun ( 8 ); // 未定义行为
}
资源分配和释放
规则9.4.1 单个对象释放使用delete,数组对象释放使用delete []
说明:单个对象删除使用delete, 数组对象删除使用delete [],原因:
调用new所包含的动作:从系统中申请一块内存,并调用此类型的构造函数。
调用new[n]所包含的动作:申请可容纳n个对象的内存,并且对每一个对象调用其构造函数。
调用delete所包含的动作:先调用相应的析构函数,再将内存归还系统。
调用delete[]所包含的动作:对每一个对象调用析构函数,再释放所有内存
如果new和delete的格式不匹配,结果是未知的。对于非class类型, new和delete不会调用构造与析构函数。
错误写法:
const int MAX_ARRAY_SIZE = 100 ;
int * numberArray = new int [ MAX_ARRAY_SIZE ];
...
delete numberArray ;
numberArray = nullptr ;
正确写法:
const int MAX_ARRAY_SIZE = 100 ;
int * numberArray = new int [ MAX_ARRAY_SIZE ];
...
delete [] numberArray ;
numberArray = nullptr ;
建议9.4.1 使用 RAII 特性来帮助追踪动态分配
说明:RAII是“资源获取就是初始化”的缩语(Resource Acquisition Is Initialization),是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。
RAII 的一般做法是这样的:在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。这种做法有两大好处:
我们不需要显式地释放资源。
对象所需的资源在其生命期内始终保持有效。这样,就不必检查资源有效性的问题,可以简化逻辑、提高效率。
示例:使用RAII不需要显式地释放互斥资源。
class LockGuard {
public :
LockGuard ( const LockType & lockType ) : lock_ ( lockType )
{
lock_ . Acquire ();
}
~ LockGuard ()
{
lock_ . Release ();
}
private :
LockType lock_ ;
};
bool Update ()
{
LockGuard lockGuard ( mutex );
if (...) {
return false ;
} else {
// 操作数据
}
return true ;
}
标准库
STL标准模板库在不同产品使用程度不同,这里列出一些基本规则和建议,供各团队参考。
规则9.5.1 不要保存std::string的c_str()返回的指针
说明:在C++标准中并未规定string::c_str()指针持久有效,因此特定STL实现完全可以在调用string::c_str()时返回一个临时存储区并很快释放。所以为了保证程序的可移植性,不要保存string::c_str()的结果,而是在每次需要时直接调用。
示例:
void Fun1 ()
{
std :: string name = "demo" ;
const char * text = name . c_str (); // 表达式结束以后,name的生命周期还在,指针有效
// 如果中间调用了string的非const成员函数,导致string被修改,比如operator[], begin()等
// 可能会导致text的内容不可用,或者不是原来的字符串
name = "test" ;
name [ 1 ] = '2' ;
// 后续使用text指针,其字符串内容不再是"demo"
}
void Fun2 ()
{
std :: string name = "demo" ;
std :: string test = "test" ;
const char * text = ( name + test ). c_str (); // 表达式结束以后,+号产生的临时对象被销毁,指针无效
// 后续使用text指针,其已不再指向合法内存空间
}
例外:在少数对性能要求非常高的代码中,为了适配已有的只接受const char*类型入参的函数,可以临时保存string::c_str()返回的指针。但是必须严格保证string对象的生命周期长于所保存指针的生命周期,并且保证在所保存指针的生命周期内,string对象不会被修改。
建议9.5.1 使用std::string代替char*
说明:使用string代替char*
有很多优势,比如:
不用考虑结尾的’\0’;
可以直接使用+, =, ==等运算符以及其它字符串操作函数;
不需要考虑内存分配操作,避免了显式的new/delete,以及由此导致的错误;
需要注意的是某些stl实现中string是基于写时复制策略的,这会带来2个问题,一是某些版本的写时复制策略没有实现线程安全,在多线程环境下会引起程序崩溃;二是当与动态链接库相互传递基于写时复制策略的string时,由于引用计数在动态链接库被卸载时无法减少可能导致悬挂指针。因此,慎重选择一个可靠的stl实现对于保证程序稳定是很重要的。
例外:
当调用系统或者其它第三方库的API时,针对已经定义好的接口,只能使用char*
。但是在调用接口之前都可以使用string,在调用接口时使用string::c_str()获得字符指针。
当在栈上分配字符数组当作缓冲区使用时,可以直接定义字符数组,不要使用string,也没有必要使用类似vector<char>
等容器。
规则9.5.2 禁止使用auto_ptr
说明:在stl库中的std::auto_ptr具有一个隐式的所有权转移行为,如下代码:
auto_ptr < T > p1 ( new T );
auto_ptr < T > p2 = p1 ;
当执行完第2行语句后,p1已经不再指向第1行中分配的对象,而是变为nullptr。正因为如此,auto_ptr不能被置于各种标准容器中。
转移所有权的行为通常不是期望的结果。对于必须转移所有权的场景,也不应该使用隐式转移的方式。这往往需要程序员对使用auto_ptr的代码保持额外的谨慎,否则出现对空指针的访问。
使用auto_ptr常见的有两种场景,一是作为智能指针传递到产生auto_ptr的函数外部,二是使用auto_ptr作为RAII管理类,在超出auto_ptr的生命周期时自动释放资源。
对于第1种场景,可以使用std::shared_ptr来代替。
对于第2种场景,可以使用C++11标准中的std::unique_ptr来代替。其中std::unique_ptr是std::auto_ptr的代替品,支持显式的所有权转移。
例外:
在C++11标准得到普遍使用之前,在一定需要对所有权进行转移的场景下,可以使用std::auto_ptr,但是建议对std::auto_ptr进行封装,并禁用封装类的拷贝构造函数和赋值运算符,以使该封装类无法用于标准容器。
建议9.5.2 使用新的标准头文件
说明:
使用C++的标准头文件时,请使用<cstdlib>
这样的,而不是<stdlib.h>
这种的。
const的用法
在声明的变量或参数前加上关键字 const 用于指明变量值不可被篡改 (如 const int foo
). 为类中的函数加上 const 限定符表明该函数不会修改类成员变量的状态 (如 class Foo { int Bar(char c) const; };
)。 const 变量, 数据成员, 函数和参数为编译时类型检测增加了一层保障, 便于尽早发现错误。因此, 我们强烈建议在任何可能的情况下使用 const。
有时候,使用C++11的constexpr来定义真正的常量可能更好。
规则9.6.1 对于指针和引用类型的形参,如果是不需要修改的,请使用const
不变的值更易于理解/跟踪和分析,把const作为默认选项,在编译时会对其进行检查,使代码更牢固/更安全。
class Foo ;
void PrintFoo ( const Foo & foo );
规则9.6.2 对于不会修改成员变量的成员函数请使用const修饰
尽可能将成员函数声明为 const。 访问函数应该总是 const。只要不修改数据成员的成员函数,都声明为const。
对于虚函数,应当从设计意图上考虑继承链上的所有类是否需要在此虚函数中修改数据成员,而不是仅关注单个类的实现。
class Foo {
public :
// ...
int PrintValue () const // const修饰成员函数,不会修改成员变量
{
std :: cout << value_ << std :: endl ;
}
int GetValue () const // const修饰成员函数,不会修改成员变量
{
return value_ ;
}
private :
int value_ ;
};
建议9.6.1 初始化后不会再修改的成员变量定义为const
class Foo {
public :
Foo ( int length ) : dataLength_ ( length ) {}
private :
const int dataLength_ ;
};
异常
建议9.7.1 C++11中,如果函数不会抛出异常,声明为noexcept
理由
如果函数不会抛出异常,声明为noexcept
可以让编译器最大程度的优化函数,如减少执行路径,提高错误退出的效率。
vector
等STL容器,为了保证接口的健壮性,如果保存元素的move运算符
没有声明为noexcept
,则在容器扩张搬移元素时不会使用move机制
,而使用copy机制
,带来性能损失的风险。如果一个函数不能抛出异常,或者一个程序并没有截获某个函数所抛出的异常并进行处理,那么这个函数可以用新的noexcept
关键字对其进行修饰,表示这个函数不会抛出异常或者抛出的异常不会被截获并处理。例如:
extern "C" double sqrt ( double ) noexcept ; // 永远不会抛出异常
// 即使可能抛出异常,也可以使用 noexcept
// 这里不准备处理内存耗尽的异常,简单地将函数声明为noexcept
std :: vector < int > MyComputation ( const std :: vector < int >& v ) noexcept
{
std :: vector < int > res = v ; // 可能会抛出异常
// do something
return res ;
}
示例
RetType Function ( Type params ) noexcept ; // 最大的优化
RetType Function ( Type params ); // 更少的优化
// std::vector 的 move 操作需要声明 noexcept
class Foo1 {
public :
Foo1 ( Foo1 && other ); // no noexcept
};
std :: vector < Foo1 > a1 ;
a1 . push_back ( Foo1 ());
a1 . push_back ( Foo1 ()); // 触发容器扩张,搬移已有元素时调用copy constructor
class Foo2 {
public :
Foo2 ( Foo2 && other ) noexcept ;
};
std :: vector < Foo2 > a2 ;
a2 . push_back ( Foo2 ());
a2 . push_back ( Foo2 ()); // 触发容器扩张,搬移已有元素时调用move constructor
注意
默认构造函数、析构函数、swap
函数,move操作符
都不应该抛出异常。
模板与泛型编程
规则9.8.1 禁止在OpenHarmony项目中进行泛型编程
泛型编程和面向对象编程的思想、理念以及技巧完全不同,OpenHarmony项目主流使用面向对象的思想。
C++提供了强大的泛型编程的机制,能够实现非常灵活简洁的类型安全的接口,实现类型不同但是行为相同的代码复用。
但是C++泛型编程存在以下缺点:
对泛型编程不很熟练的人,常常会将面向对象的逻辑写成模板、将不依赖模板参数的成员写在模板中等等导致逻辑混乱代码膨胀诸多问题。
模板编程所使用的技巧对于使用c++不是很熟练的人是比较晦涩难懂的。在复杂的地方使用模板的代码让人更不容易读懂,并且debug 和维护起来都很麻烦。
模板编程经常会导致编译出错的信息非常不友好: 在代码出错的时候, 即使这个接口非常的简单, 模板内部复杂的实现细节也会在出错信息显示. 导致这个编译出错信息看起来非常难以理解。
模板如果使用不当,会导致运行时代码过度膨胀。
模板代码难以修改和重构。模板的代码会在很多上下文里面扩展开来, 所以很难确认重构对所有的这些展开的代码有用。
所以,OpenHarmony大部分部件禁止模板编程,仅有 少数部件 可以使用泛型编程,并且开发的模板要有详细的注释。
例外:
stl适配层可以使用模板
宏
在C++语言中,我们强烈建议尽可能少使用复杂的宏
对于常量定义,请按照前面章节所述,使用const或者枚举;
对于宏函数,尽可能简单,并且遵循下面的原则,并且优先使用内联函数,模板函数等进行替换。
// 不推荐使用宏函数
#define SQUARE(a, b) ((a) * (b))
// 请使用模板函数,内联函数等来替换。
template < typename T > T Square ( T a , T b ) { return a * b ; }
如果需要使用宏,请参考C语言规范的相关章节。
例外 :一些通用且成熟的应用,如:对 new, delete 的封装处理,可以保留对宏的使用。
10 现代C++特性
随着 ISO 在2011年发布 C++11 语言标准,以及2017年3月发布 C++17 ,现代C++(C++11/14/17等)增加了大量提高编程效率、代码质量的新语言特性和标准库。
本章节描述了一些可以帮助团队更有效率的使用现代C++,规避语言陷阱的指导意见。
代码简洁性和安全性提升
建议10.1.1 合理使用auto
理由
auto
可以避免编写冗长、重复的类型名,也可以保证定义变量时初始化。
auto
类型推导规则复杂,需要仔细理解。
如果能够使代码更清晰,继续使用明确的类型,且只在局部变量使用auto
。
示例
// 避免冗长的类型名
std :: map < string , int >:: iterator iter = m . find ( val );
auto iter = m . find ( val );
// 避免重复类型名
class Foo {...};
Foo * p = new Foo ;
auto p = new Foo ;
// 保证初始化
int x ; // 编译正确,没有初始化
auto x ; // 编译失败,必须初始化
auto 的类型推导可能导致困惑:
auto a = 3 ; // int
const auto ca = a ; // const int
const auto & ra = a ; // const int&
auto aa = ca ; // int, 忽略 const 和 reference
auto ila1 = { 10 }; // std::initializer_list<int>
auto ila2 { 10 }; // std::initializer_list<int>
auto && ura1 = x ; // int&
auto && ura2 = ca ; // const int&
auto && ura3 = 10 ; // int&&
const int b [ 10 ];
auto arr1 = b ; // const int*
auto & arr2 = b ; // const int(&)[10]
如果没有注意 auto
类型推导时忽略引用,可能引入难以发现的性能问题:
std :: vector < std :: string > v ;
auto s1 = v [ 0 ]; // auto 推导为 std::string,拷贝 v[0]
如果使用auto
定义接口,如头文件中的常量,可能因为开发人员修改了值,而导致类型发生变化。
规则10.1.1 在重写虚函数时请使用override
或final
关键字
理由
override
和final
关键字都能保证函数是虚函数,且重写了基类的虚函数。如果子类函数与基类函数原型不一致,则产生编译告警。final
还保证虚函数不会再被子类重写。
使用override
或final
关键字后,如果修改了基类虚函数原型,但忘记修改子类重写的虚函数,在编译期就可以发现。也可以避免有多个子类时,重写虚函数的修改遗漏。
示例
class Base {
public :
virtual void Foo ();
virtual void Foo ( int var );
void Bar ();
};
class Derived : public Base {
public :
void Foo () const override ; // 编译失败: Derived::Foo 和 Base::Foo 原型不一致,不是重写
void Foo () override ; // 正确: Derived::Foo 重写 Base::Foo
void Foo ( int var ) final ; // 正确: Derived::Foo(int) 重写 Base::Foo(int),且Derived的派生类不能再重写此函数
void Bar () override ; // 编译失败: Base::Bar 不是虚函数
};
总结
基类首次定义虚函数,使用virtual
关键字
子类重写基类虚函数(包括析构函数),使用override
或final
关键字(但不要两者一起使用),并且不使用virtual
关键字
非虚函数,virtual
、override
和final
都不使用
规则10.1.2 使用delete
关键字删除函数
理由
相比于将类成员函数声明为private
但不实现,delete
关键字更明确,且适用范围更广。
示例
class Foo {
private :
// 只看头文件不知道拷贝构造是否被删除
Foo ( const Foo & );
};
class Foo {
public :
// 明确删除拷贝赋值函数
Foo & operator = ( const Foo & ) = delete ;
};
delete
关键字还支持删除非成员函数
template < typename T >
void Process ( T value );
template <>
void Process < void > ( void ) = delete ;
规则10.1.3 使用nullptr
,而不是NULL
或0
理由
长期以来,C++没有一个代表空指针的关键字,这是一件很尴尬的事:
#define NULL ((void *)0)
char * str = NULL ; // 错误: void* 不能自动转换为 char*
void ( C ::* pmf )() = & C :: Func ;
if ( pmf == NULL ) {} // 错误: void* 不能自动转换为指向成员函数的指针
如果把NULL
被定义为0
或0L
。可以解决上面的问题。
或者在需要空指针的地方直接使用0
。但这引入另一个问题,代码不清晰,特别是使用auto
自动推导:
auto result = Find ( id );
if ( result == 0 ) { // Find() 返回的是 指针 还是 整数?
// do something
}
0
字面上是int
类型(0L
是long
),所以NULL
和0
都不是指针类型。
当重载指针和整数类型的函数时,传递NULL
或0
都调用到整数类型重载的函数:
void F ( int );
void F ( int * );
F ( 0 ); // 调用 F(int),而非 F(int*)
F ( NULL ); // 调用 F(int),而非 F(int*)
另外,sizeof(NULL) == sizeof(void*)
并不一定总是成立的,这也是一个潜在的风险。
总结: 直接使用0
或0L
,代码不清晰,且无法做到类型安全;使用NULL
无法做到类型安全。这些都是潜在的风险。
nullptr
的优势不仅仅是在字面上代表了空指针,使代码清晰,而且它不再是一个整数类型。
nullptr
是std::nullptr_t
类型,而std::nullptr_t
可以隐式的转换为所有的原始指针类型,这使得nullptr
可以表现成指向任意类型的空指针。
void F ( int );
void F ( int * );
F ( nullptr ); // 调用 F(int*)
auto result = Find ( id );
if ( result == nullptr ) { // Find() 返回的是 指针
// do something
}
规则10.1.4 使用using
而非typedef
在C++11
之前,可以通过typedef
定义类型的别名。没人愿意多次重复std::map<uint32_t, std::vector<int>>
这样的代码。
typedef std :: map < uint32_t , std :: vector < int >> SomeType ;
类型的别名实际是对类型的封装。而通过封装,可以让代码更清晰,同时在很大程度上避免类型变化带来的散弹式修改。
在C++11
之后,提供using
,实现声明别名(alias declarations)
:
using SomeType = std :: map < uint32_t , std :: vector < int >> ;
对比两者的格式:
typedef Type Alias ; // Type 在前,还是 Alias 在前
using Alias = Type ; // 符合'赋值'的用法,容易理解,不易出错
如果觉得这点还不足以切换到using
,我们接着看看模板别名(alias template)
:
// 定义模板的别名,一行代码
template < class T >
using MyAllocatorVector = std :: vector < T , MyAllocator < T >> ;
MyAllocatorVector < int > data ; // 使用 using 定义的别名
template < class T >
class MyClass {
private :
MyAllocatorVector < int > data_ ; // 模板类中使用 using 定义的别名
};
而typedef
不支持带模板参数的别名,只能"曲线救国”:
// 通过模板包装 typedef,需要实现一个模板类
template < class T >
struct MyAllocatorVector {
typedef std :: vector < T , MyAllocator < T >> type ;
};
MyAllocatorVector < int >:: type data ; // 使用 typedef 定义的别名,多写 ::type
template < class T >
class MyClass {
private :
typename MyAllocatorVector < int >:: type data_ ; // 模板类中使用,除了 ::type,还需要加上 typename
};
规则10.1.5 禁止使用std::move操作const对象
从字面上看,std::move
的意思是要移动一个对象。而const对象是不允许修改的,自然也无法移动。因此用std::move
操作const对象会给代码阅读者带来困惑。
在实际功能上,std::move
会把对象转换成右值引用类型;对于const对象,会将其转换成const的右值引用。由于极少有类型会定义以const右值引用为参数的移动构造函数和移动赋值操作符,因此代码实际功能往往退化成了对象拷贝而不是对象移动,带来了性能上的损失。
错误示例:
std :: string g_string ;
std :: vector < std :: string > g_stringList ;
void func ()
{
const std :: string myString = "String content" ;
g_string = std :: move ( myString ); // bad:并没有移动myString,而是进行了复制
const std :: string anotherString = "Another string content" ;
g_stringList . push_back ( std :: move ( anotherString )); // bad:并没有移动anotherString,而是进行了复制
}
智能指针
规则10.2.1 单例、类的成员等所有权不会被多方持有的优先使用原始指针而不是智能指针
理由
智能指针会自动释放对象资源避免资源泄露,但会带额外的资源开销。如:智能指针自动生成的类、构造和析构的开销、内存占用多等。
单例、类的成员等对象的所有权不会被多方持有的情况,仅在类析构中释放资源即可。不应该使用智能指针增加额外的开销。
示例
class Foo ;
class Base {
public :
Base () {}
virtual ~ Base ()
{
delete foo_ ;
}
private :
Foo * foo_ = nullptr ;
};
例外
返回创建的对象时,需要指针销毁函数的可以使用智能指针。
class User ;
class Foo {
public :
std :: unique_ptr < User , void ( User * ) > CreateUniqueUser () // 可使用unique_ptr保证对象的创建和释放在同一runtime
{
sptr < User > ipcUser = iface_cast < User > ( remoter );
return std :: unique_ptr < User , void ( User * ) > ( :: new User ( ipcUser ), []( User * user ) {
user -> Close ();
:: delete user ;
});
}
std :: shared_ptr < User > CreateSharedUser () // 可使用shared_ptr保证对象的创建和释放在同一runtime中
{
sptr < User > ipcUser = iface_cast < User > ( remoter );
return std :: shared_ptr < User > ( ipcUser . GetRefPtr (), [ ipcUser ]( User * user ) mutable {
ipcUser = nullptr ;
});
}
};
返回创建的对象且对象需要被多方引用时,可以使用shared_ptr。
规则10.2.2 使用std::make_unique
而不是new
创建unique_ptr
理由
make_unique
提供了更简洁的创建方式
保证了复杂表达式的异常安全
示例
// 不好:两次出现 MyClass,重复导致不一致风险
std :: unique_ptr < MyClass > ptr ( new MyClass ( 0 , 1 ));
// 好:只出现一次 MyClass,不存在不一致的可能
auto ptr = std :: make_unique < MyClass > ( 0 , 1 );
重复出现类型可能导致非常严重的问题,且很难发现:
// 编译正确,但new和delete不配套
std :: unique_ptr < uint8_t > ptr ( new uint8_t [ 10 ]);
std :: unique_ptr < uint8_t [] > ptr ( new uint8_t );
// 非异常安全: 编译器可能按如下顺序计算参数:
// 1. 分配 Foo 的内存,
// 2. 构造 Foo,
// 3. 调用 Bar,
// 4. 构造 unique_ptr<Foo>.
// 如果 Bar 抛出异常, Foo 不会被销毁,产生内存泄露。
F ( unique_ptr < Foo > ( new Foo ()), Bar ());
// 异常安全: 调用函数不会被打断.
F ( make_unique < Foo > (), Bar ());
例外
std::make_unique
不支持自定义deleter
。
在需要自定义deleter
的场景,建议在自己的命名空间实现定制版本的make_unique
。
使用new
创建自定义deleter
的unique_ptr
是最后的选择。
规则10.2.4 使用std::make_shared
而不是new
创建shared_ptr
理由
使用std::make_shared
除了类似std::make_unique
一致性等原因外,还有性能的因素。
std::shared_ptr
管理两个实体:
控制块(存储引用计数,deleter
等)
管理对象
std::make_shared
创建std::shared_ptr
,会一次性在堆上分配足够容纳控制块和管理对象的内存。而使用std::shared_ptr<MyClass>(new MyClass)
创建std::shared_ptr
,除了new MyClass
会触发一次堆分配外,std::shard_ptr
的构造函数还会触发第二次堆分配,产生额外的开销。
例外
类似std::make_unique
,std::make_shared
不支持定制deleter
Lambda
建议10.3.1 当函数不能工作时选择使用lambda
(捕获局部变量,或编写局部函数)
理由
函数无法捕获局部变量或在局部范围内声明;如果需要这些东西,尽可能选择lambda
,而不是手写的functor
。
另一方面,lambda
和functor
不会重载;如果需要重载,则使用函数。
如果lambda
和函数都可以的场景,则优先使用函数;尽可能使用最简单的工具。
示例
// 编写一个只接受 int 或 string 的函数
// -- 重载是自然的选择
void F ( int );
void F ( const string & );
// 需要捕获局部状态,或出现在语句或表达式范围
// -- lambda 是自然的选择
vector < Work > v = LotsOfWork ();
for ( int taskNum = 0 ; taskNum < max ; ++ taskNum ) {
pool . Run ([ = , & v ] {...});
}
pool . Join ();
规则10.3.1 非局部范围使用lambdas
,避免使用按引用捕获
理由
非局部范围使用lambdas
包括返回值,存储在堆上,或者传递给其它线程。局部的指针和引用不应该在它们的范围外存在。lambdas
按引用捕获就是把局部对象的引用存储起来。如果这会导致超过局部变量生命周期的引用存在,则不应该按引用捕获。
示例
// 不好
void Foo ()
{
int local = 42 ;
// 按引用捕获 local.
// 当函数返回后,local 不再存在,
// 因此 Process() 的行为未定义!
threadPool . QueueWork ([ & ]{ Process ( local ); });
}
// 好
void Foo ()
{
int local = 42 ;
// 按值捕获 local。
// 因为拷贝,Process() 调用过程中,local 总是有效的
threadPool . QueueWork ([ = ]{ Process ( local ); });
}
建议10.3.2 如果捕获this
,则显式捕获所有变量
理由
在成员函数中的[=]
看起来是按值捕获。但因为是隐式的按值获取了this
指针,并能够操作所有成员变量,数据成员实际是按引用捕获的,一般情况下建议避免。如果的确需要这样做,明确写出对this
的捕获。
示例
class MyClass {
public :
void Foo ()
{
int i = 0 ;
auto Lambda = [ = ]() { Use ( i , data_ ); }; // 不好: 看起来像是拷贝/按值捕获,成员变量实际上是按引用捕获
data_ = 42 ;
Lambda (); // 调用 use(42);
data_ = 43 ;
Lambda (); // 调用 use(43);
auto Lambda2 = [ i , this ]() { Use ( i , data_ ); }; // 好,显式指定按值捕获,最明确,最少的混淆
}
private :
int data_ = 0 ;
};
建议10.3.3 避免使用默认捕获模式
理由
lambda表达式提供了两种默认捕获模式:按引用(&)和按值(=)。
默认按引用捕获会隐式的捕获所有局部变量的引用,容易导致访问悬空引用。相比之下,显式的写出需要捕获的变量可以更容易的检查对象生命周期,减小犯错可能。
默认按值捕获会隐式的捕获this指针,且难以看出lambda函数所依赖的变量是哪些。如果存在静态变量,还会让阅读者误以为lambda拷贝了一份静态变量。
因此,通常应当明确写出lambda需要捕获的变量,而不是使用默认捕获模式。
错误示例
auto func ()
{
int addend = 5 ;
static int baseValue = 3 ;
return [ = ]() { // 实际上只复制了addend
++ baseValue ; // 修改会影响静态变量的值
return baseValue + addend ;
};
}
正确示例
auto func ()
{
int addend = 5 ;
static int baseValue = 3 ;
return [ addend , baseValue = baseValue ]() mutable { // 使用C++14的捕获初始化拷贝一份变量
++ baseValue ; // 修改自己的拷贝,不会影响静态变量的值
return baseValue + addend ;
};
}
参考:《Effective Modern C++》:Item 31: Avoid default capture modes.
接口
建议10.4.1 不涉及所有权的场景,使用T*
或T&
作为参数,而不是智能指针
理由
只在需要明确所有权机制时,才通过智能指针转移或共享所有权.
通过智能指针传递,限制了函数调用者必须使用智能指针(如调用者希望传递this
)。
传递共享所有权的智能指针存在运行时的开销。
示例
// 接受任何 int*
void F ( int * );
// 只能接受希望转移所有权的 int
void G ( unique_ptr < int > );
// 只能接受希望共享所有权的 int
void G ( shared_ptr < int > );
// 不改变所有权,但需要特定所有权的调用者
void H ( const unique_ptr < int >& );
// 接受任何 int
void H ( int & );
// 不好
void F ( shared_ptr < Widget >& w )
{
// ...
Use ( * w ); // 只使用 w -- 完全不涉及生命周期管理
// ...
};
linux
Friday, June 28, 2024
linux导览
Friday, June 28, 2024
windows
Friday, June 28, 2024
理解Windows网络_WFP
Friday, June 28, 2024
理解 Windows 网络
WFP
名词解释
https://learn.microsoft.com/en-us/windows/win32/fwp/object-model
https://learn.microsoft.com/en-us/windows/win32/fwp/basic-operation
https://learn.microsoft.com/en-us/windows-hardware/drivers/network
callout : A callout provides functionality that extends the capabilities of the Windows Filtering Platform. A callout consists of a set of callout functions and a GUID key that uniquely identifies the callout.
callout driver : A callout driver is a driver that registers callouts with the Windows Filtering Platform. A callout driver is a type of filter driver.
callout function : A callout function is a function that is called by the Windows Filtering Platform to perform a specific task. A callout function is associated with a callout.
filter : A filter is a set of functions that are called by the Windows Filtering Platform to perform filtering operations. A filter consists of a set of filter functions and a GUID key that uniquely identifies the filter.
filter engine : The filter engine is the component of the Windows Filtering Platform that performs filtering operations. The filter engine is responsible for calling the filter functions that are registered with the Windows Filtering Platform.
filter layer : A filter layer is a set of functions that are called by the Windows Filtering Platform to perform filtering operations. A filter layer consists of a set of filter layer functions and a GUID key that uniquely identifies the filter layer.
Dispatcher队列触发回调是尽快触发形式, 不需要等队列满, 因此可以满足实时性.
当用户回调较慢时, 阻塞的报文会尽可能插入下个队列, 队列上限256. 更多的阻塞报文则由系统缓存, 粗略的测试缓存能力是16500, 系统缓存能力可能随机器性能和配置不同存在差异.
用户回调处理报文时, 存在两份报文实体:
内核报文, 在回调处理完队列后一并释放. 因此回调较慢时, 一次回调执行会最多锁定系统256个报文的缓存能力.
回调中的拷贝, 处理完单个报文后立即释放.
在FwppNetEvent1Callback中对报文进行拷贝组装, 不会操作原始报文, 对业务没有影响.
订阅可以使用模板过滤器, 以减少需要处理的报文:
https://learn.microsoft.com/en-us/windows/win32/api/fwpmtypes/ns-fwpmtypes-fwpm_net_event_enum_template0
filterCondition
An array of FWPM_FILTER_CONDITION0 structures that contain distinct filter conditions (duplicated filter conditions will generate an error). All conditions must be true for the action to be performed. In other words, the conditions are AND’ed together. If no conditions are specified, the action is always performed.
不可使用相同的filter
所有过滤器间的关系是"与", 需要全都满足
微软文档显示支持的过滤器有八种, 实际上支持的过滤器会更多.
FWPM_CONDITION_IP_PROTOCOL
The IP protocol number, as specified in RFC 1700.
FWPM_CONDITION_IP_LOCAL_ADDRESS
The local IP address.
FWPM_CONDITION_IP_REMOTE_ADDRESS
The remote IP address.
FWPM_CONDITION_IP_LOCAL_PORT
The local transport protocol port number. For ICMP, the message type.
FWPM_CONDITION_IP_REMOTE_PORT
The remote transport protocol port number. For ICMP, the message code.
FWPM_CONDITION_SCOPE_ID
The interface IPv6 scope identifier. Reserved for internal use.
FWPM_CONDITION_ALE_APP_ID
The full path of the application.
FWPM_CONDITION_ALE_USER_ID
The identification of the local user.
枚举系统已注册的订阅发现已有两个订阅, 查看其sessionKey GUID无法确认由谁注册, 对其进行分析发现两个订阅各自实现了以下功能:
订阅了所有FWPM_NET_EVENT_TYPE_CLASSIFY_DROP的数据包, 统计了所有被丢弃的包.
订阅了所有FWPM_NET_EVENT_TYPE_CLASSIFY_ALLOW的数据包, 可以用来做流量统计
这两个订阅用到的contition filter都是FWPM_CONDITION_NET_EVENT_TYPE(206e9996-490e-40cf-b831-b38641eb6fcb), 说明可以实现过滤的filter不止微软文档中提到的8个.
更多调研发现用户态调用接口仅能捕获drop的事件, 非drop事件需要使用内核模式获取, 因此微隔离不能使用FWPM_CONDITION_NET_EVENT_TYPE获取事件.
理解Windows事件跟踪_ETW
Friday, June 28, 2024
理解 ETW
筛除了一些不必要的信息, 完整文档参阅: https://docs.microsoft.com/en-us/windows/win32/etw/event-tracing-portal
理解基础
https://learn.microsoft.com/en-us/windows/win32/etw/about-event-tracing
Session
存在四种 session
session 种类
使用
限制
特点
Event Tracing Session (Standard ETW)
1. EVENT_TRACE_PROPERTIES 2. StartTrace , 创建 session3. EnableTrace 1. EnableTrace for classic provider 2. EnableTraceEx for manifest-based provider4. ControlTrace 停止 session
- 一个 manifest-based provider 仅支持提供事件到至多 8 个 session- 一个 classic provider, 仅能服务一个 session.- session 抢占 provider 行为是后来居上.
标准 ETW.
SystemTraceProvider Session
1. EVENT_TRACE_PROPERTIES ->EnableFlags 2. StartTrace 3. ControlTrace 停止 session
- **SystemTraceProvider **是一个内核事件 provider, 提供一套预定义的内核事件 .- NT Kernel Logger session 是系统预置 session, 记录一系列系统预定义的内核事件- Win7/WinServer2008R2 仅 NT Kernel Logger session 可使用 SystemTraceProvider - Win8/WinServer2012 的 SystemTraceProvider 可以提供事件给 8 个 logger session , 其中两个固定为 NT Kernel Logger 和 Circular Kernel Context Logger.- Win10 20348 之后, 各 Systerm provider 可以被单独控制.
获取系统内核预定义事件.
AutoLogger session
1. 修改注册表 2. EnableTraceEx 3. ControlTrace 停止 session
- **Global Logger Session **是特殊独立的 session, 记录系统启动时事件.- 普通 AutoLogger 需要自行使能 provider, GlobleLogger 不需要.- AutoLogger 不支持 NT Kernel Logger 事件, 仅 GlobalLogger 支持.- 影响启动时间, 节制使用
记录操作系统启动期间事件
Private Logger Session
-
- User-mode ETW- 仅进程内使用- 不计入 64 session 并行限制.
进程私有
工具
wireguard配置
Friday, June 28, 2024
wireguard 配置
防火墙配置
wireguard / installtunnelservice < wg_conf_path >
wg show
Get-NetConnectionProfile
Get-NetAdapter
Get-NetFirewallProfile
Set-NetFirewallProfile -Profile domain , public , private -DisabledInterfaceAliases < wg_config_name >
Set-NetIPInterface -ifindex < interface index > -Forwarding Enabled
New-NetFirewallRule -DisplayName "@wg1" -Direction Inbound -RemoteAddress 10.66 . 66 . 1 / 24 -Action Allow
New-NetFirewallRule -DisplayName "@wg1" -Direction Outbound -RemoteAddress 10.66 . 66 . 1 / 24 -Action Allow
# 定位拦截原因
auditpol / set / subcategory : "{0CCE9225-69AE-11D9-BED3-505054503030}" / success : disable / failure : enable
wevtutil qe Security / q: "*[System/EventID=5152]" / c: 5 / rd : true / f:text
auditpol / set / subcategory : "{0CCE9225-69AE-11D9-BED3-505054503030}" / success : disable / failure : disable
Windows阻断网络流量获取
Friday, June 28, 2024
Windows 阻断网络流量获取
需要识别出被阻断的流量, 被阻断的流量包括出站入站方向.
阻断的两种形式, 基于链接(connection), 和基于数据包(packet). 数据包的丢弃较为频繁常见, 需要审查丢弃原因, 基于链接的阻断更符合实际需关注的阻断场景.
许多正常处理的报文也会被 drop, 因此需要区分 drop 和 block 行为, 我们主要关注 block 的情况.
搭建测试工程
WFP 主要工作在 usermode, 另一部分在 kernalmode, 能力以驱动形式体现, 搭建测试环境的方法比较复杂. 推荐的方法是测试机使用另一台物理机, 开发机编译好后, 发送至测试机远程调试.
受条件限制, 我们也可以直接在本地进行调试.
编译问题:
其它问题:
通过审计获取 block 事件
默认情况下,禁用对 WFP 的审核。
可以通过组策略对象编辑器 MMC 管理单元、本地安全策略 MMC 管理单元或 auditpol.exe 命令,按类别(category)启用审核。
可以通过 auditpol.exe 命令按子类别(subcategory)启用审核。
应该使用 guid 进行设置, 否则不同语言系统有本地化的问题.
审计使用循环日志, 128KB 不用担心资源消耗
类别https://docs.microsoft.com/en-us/windows/win32/secauthz/auditing-constants
Category/Subcategory
GUID
…
…
Object Access
{6997984A-797A-11D9-BED3-505054503030}
Policy Change
{6997984D-797A-11D9-BED3-505054503030}
…
…
Object Access 子类和对应 GUID https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-gpac/77878370-0712-47cd-997d-b07053429f6d
Object Access Subcategory
Subcategory GUID
Inclusion Setting
…
…
…
Filtering Platform Packet Drop
{0CCE9225-69AE-11D9-BED3-505054503030}
No Auditing
Filtering Platform Connection
{0CCE9226-69AE-11D9-BED3-505054503030}
No Auditing
Other Object Access Events
{0CCE9227-69AE-11D9-BED3-505054503030}
No Auditing
…
…
…
Policy Change 子类和对应 GUID:
Policy Change Subcategory
Subcategory GUID
Audit Policy Change
{0CCE922F-69AE-11D9-BED3-505054503030}
Authentication Policy Change
{0CCE9230-69AE-11D9-BED3-505054503030}
Authorization Policy Change
{0CCE9231-69AE-11D9-BED3-505054503030}
MPSSVC Rule-Level Policy Change
{0CCE9232-69AE-11D9-BED3-505054503030}
Filtering Platform Policy Change
{0CCE9233-69AE-11D9-BED3-505054503030}
Other Policy Change Events
{0CCE9234-69AE-11D9-BED3-505054503030}
# auditpol手册参阅: https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/auditpol
# 本段主要关注 'Object Access' 类别
# 获取可查询的字段
# -v 显示GUID, -r显示csv报告
auditpol / list / category / v
auditpol / list / subcategory : * / v
# 获取某个子类别的审计设置
auditpol / get / category : 'Object Access' / r | ConvertFrom-Csv | Get-Member
# 查询guid
auditpol / get / category : 'Object Access' / r | ConvertFrom-Csv | Format-Table Subcategory , 'Subcategory GUID' , 'Inclusion Setting'
# 查找subcategory
auditpol / list / subcategory : "Object Access" , "Policy Change" -v
# 备份
auditpol / backup / file : d: \ audit . bak
# 还原
auditpol / restore / file : d: \ audit . bak
# 修改Policy
# **Policy Change** | {6997984D-797A-11D9-BED3-505054503030}
auditpol / set / category : "{6997984D-797A-11D9-BED3-505054503030}" / success : disable / failure : disable
# Filtering Platform Policy Change | {0CCE9233-69AE-11D9-BED3-505054503030}
auditpol / set / subcategory : "{0CCE9233-69AE-11D9-BED3-505054503030}" / success : enable / failure : enable
# **Object Access** | {6997984A-797A-11D9-BED3-505054503030}
auditpol / get / category : "{6997984A-797A-11D9-BED3-505054503030}"
auditpol / set / category : "{6997984A-797A-11D9-BED3-505054503030}" / success : disable / failure : disable
# Filtering Platform Packet Drop | {0CCE9225-69AE-11D9-BED3-505054503030}
auditpol / set / subcategory : "{0CCE9225-69AE-11D9-BED3-505054503030}" / success : disable / failure : enable
# Filtering Platform Connection | {0CCE9226-69AE-11D9-BED3-505054503030}
auditpol / set / subcategory : "{0CCE9226-69AE-11D9-BED3-505054503030}" / success : disable / failure : enable
# 读取日志
$Events = Get-WinEvent -LogName 'Security'
foreach ( $event in $Events ) {
ForEach ( $line in $ ( $event . Message -split " `r`n " )) {
Write-host $event . RecordId ':' $Line
break
}
}
事件说明:
Event ID
Explanation
5031(F)
The Windows Firewall Service blocked an application from accepting incoming connections on the network.
5150(-)
The Windows Filtering Platform blocked a packet .
5151(-)
A more restrictive Windows Filtering Platform filter has blocked a packet .
5152(F)
The Windows Filtering Platform blocked a packet .
5153(S)
A more restrictive Windows Filtering Platform filter has blocked a packet .
5154(S)
The Windows Filtering Platform has permitted an application or service to listen on a port for incoming connections.
5155(F)
The Windows Filtering Platform has blocked an application or service from listening on a port for incoming connections.
5156(S)
The Windows Filtering Platform has permitted a connection.
5157(F)
The Windows Filtering Platform has blocked a connection.
5158(S)
The Windows Filtering Platform has permitted a bind to a local port.
5159(F)
The Windows Filtering Platform has blocked a bind to a local port.
关注的事件详细说明:
Audit Filtering Platform Packet Drop
这类事件产生量非常大,建议关注5157 事件, 它记录了几乎相同的信息, 但是 5157 基于链接记录而不是基于数据包.
Failure events volume typically is very high for this subcategory and typically used for troubleshooting. If you need to monitor blocked connections, it is better to use “5157(F): The Windows Filtering Platform has blocked a connection,” because it contains almost the same information and generates per-connection, not per-packet.
5152
5153
Audit Filtering Platform Connection
建议只关注失败事件, 如被阻止的连接, 按需关注允许的链接.
5031
If you don’t have any firewall rules (Allow or Deny) in Windows Firewall for specific applications, you will get this event from Windows Filtering Platform layer, because by default this layer is denying any incoming connections.
5150
5151
5155
5157
5159
获取 provider 信息
# 获取security相关的provider信息
Get-WinEvent -ListProvider "*Security*" | Select-Object providername , id
# Microsoft-Windows-Security-Auditing 54849625-5478-4994-a5ba-3e3b0328c30d
# 获取provider提供的task信息
Get-WinEvent -ListProvider "Microsoft-Windows-Security-Auditing" | Select-Object -ExpandProperty tasks
# SE_ADT_OBJECTACCESS_FIREWALLCONNECTION 12810 Filtering Platform Connection 00000000-0000-0000-0000-000000000000
ProviderName
Id
Security Account Manager
00000000-0000-0000-0000-000000000000
Security
00000000-0000-0000-0000-000000000000
SecurityCenter
00000000-0000-0000-0000-000000000000
Microsoft-Windows-Security-SPP-UX-GenuineCenter-Logging
fb829150-cd7d-44c3-af5b-711a3c31cedc
Microsoft-Windows-Security-Mitigations
fae10392-f0af-4ac0-b8ff-9f4d920c3cdf
Microsoft-Windows-VerifyHardwareSecurity
f3f53c76-b06d-4f15-b412-61164a0d2b73
Microsoft-Windows-SecurityMitigationsBroker
ea8cd8a5-78ff-4418-b292-aadc6a7181df
Microsoft-Windows-Security-Adminless
ea216962-877b-5b73-f7c5-8aef5375959e
Microsoft-Windows-Security-Vault
e6c92fb8-89d7-4d1f-be46-d56e59804783
Microsoft-Windows-Security-Netlogon
e5ba83f6-07d0-46b1-8bc7-7e669a1d31dc
Microsoft-Windows-Security-SPP
e23b33b0-c8c9-472c-a5f9-f2bdfea0f156
Microsoft-Windows-Windows Firewall With Advanced Security
d1bc9aff-2abf-4d71-9146-ecb2a986eb85
Microsoft-Windows-Security-SPP-UX-Notifications
c4efc9bb-2570-4821-8923-1bad317d2d4b
Microsoft-Windows-Security-SPP-UX-GC
bbbdd6a3-f35e-449b-a471-4d830c8eda1f
Microsoft-Windows-Security-Kerberos
98e6cfcb-ee0a-41e0-a57b-622d4e1b30b1
Microsoft-Windows-Security-ExchangeActiveSyncProvisioning
9249d0d0-f034-402f-a29b-92fa8853d9f3
Microsoft-Windows-NetworkSecurity
7b702970-90bc-4584-8b20-c0799086ee5a
Microsoft-Windows-Security-SPP-UX
6bdadc96-673e-468c-9f5b-f382f95b2832
Microsoft-Windows-Security-Auditing
54849625-5478-4994-a5ba-3e3b0328c30d
Microsoft-Windows-Security-LessPrivilegedAppContainer
45eec9e5-4a1b-5446-7ad8-a4ab1313c437
Microsoft-Windows-Security-UserConsentVerifier
40783728-8921-45d0-b231-919037b4b4fd
Microsoft-Windows-Security-IdentityListener
3c6c422b-019b-4f48-b67b-f79a3fa8b4ed
Microsoft-Windows-Security-EnterpriseData-FileRevocationManager
2cd58181-0bb6-463e-828a-056ff837f966
Microsoft-Windows-Security-Audit-Configuration-Client
08466062-aed4-4834-8b04-cddb414504e5
Microsoft-Windows-Security-IdentityStore
00b7e1df-b469-4c69-9c41-53a6576e3dad
构造 block 事件
必须非常注意,在构造 block 事件时, 会影响本地其它软件的运行!
可及时使用.\WFPSampler.exe -clean
来清理过滤器.
操作步骤:
打开 Filtering Platform Connection 的审计开关, auditpol /set /subcategory:"{0CCE9226-69AE-11D9-BED3-505054503030}" /success:enable /failure:enable
打开 Event Viewer, 构造一个 Custom View, 创建过滤器, 我们暂只关注 5155, 5157, 5159 三个事件.
构造一个过滤器, 我们使用WFPSampler.exe 来构造过滤器, 阻止监听本地的80 端口, .\WFPSampler.exe -s BASIC_ACTION_BLOCK -l FWPM_LAYER_ALE_AUTH_LISTEN_V4 -iplp 80
使用一个第三方(非 IIS)的 http server, 这里使用的 nginx, 默认监听 80 端口, 双击启动启动则触发 5155 事件
还原过滤器, .\WFPSampler.exe -clean
还原审计开关, auditpol /set /category:"{0CCE9226-69AE-11D9-BED3-505054503030}" /success:disable /failure:disable
# 5155 blocked an application or service from listening on a port for incoming connections
.\ WFPSampler . exe -s BASIC_ACTION_BLOCK -l FWPM_LAYER_ALE_AUTH_LISTEN_V4
# 5157 blocked a connection
.\ WFPSampler . exe -s BASIC_ACTION_BLOCK -l FWPM_LAYER_ALE_AUTH_RECV_ACCEPT_V4
.\ WFPSampler . exe -s BASIC_ACTION_BLOCK -l FWPM_LAYER_ALE_AUTH_CONNECT_V4
# 5159, blocked a bind to a local port
.\ WFPSampler . exe -s BASIC_ACTION_BLOCK -l FWPM_LAYER_ALE_RESOURCE_ASSIGNMENT_V4
# Other
.\ WFPSampler . exe -s BASIC_ACTION_BLOCK -l FWPM_LAYER_ALE_RESOURCE_ASSIGNMENT_V4_DISCARD
.\ WFPSampler . exe -s BASIC_ACTION_BLOCK -l FWPM_LAYER_ALE_AUTH_RECV_ACCEPT_V4_DISCARD
.\ WFPSampler . exe -s BASIC_ACTION_BLOCK -l FWPM_LAYER_ALE_AUTH_CONNECT_V4_DISCARD
# To find a specific Windows Filtering Platform filter by ID, run the following command:
netsh wfp show filter s
# To find a specific Windows Filtering Platform layer ID, you need to execute the following command:
netsh wfp show state
监控网络事件(NET_EVENT)
网络事件支持枚举查找, 支持订阅.
枚举方式支持定制过滤条件, 获取一段时间内的网络事件.
订阅方式可以注入一个 callback 函数, 实时反馈.
支持的事件种类:
typedef enum FWPM_NET_EVENT_TYPE_ {
FWPM_NET_EVENT_TYPE_IKEEXT_MM_FAILURE = 0 ,
FWPM_NET_EVENT_TYPE_IKEEXT_QM_FAILURE ,
FWPM_NET_EVENT_TYPE_IKEEXT_EM_FAILURE ,
FWPM_NET_EVENT_TYPE_CLASSIFY_DROP ,
FWPM_NET_EVENT_TYPE_IPSEC_KERNEL_DROP ,
FWPM_NET_EVENT_TYPE_IPSEC_DOSP_DROP ,
FWPM_NET_EVENT_TYPE_CLASSIFY_ALLOW ,
FWPM_NET_EVENT_TYPE_CAPABILITY_DROP ,
FWPM_NET_EVENT_TYPE_CAPABILITY_ALLOW ,
FWPM_NET_EVENT_TYPE_CLASSIFY_DROP_MAC ,
FWPM_NET_EVENT_TYPE_LPM_PACKET_ARRIVAL ,
FWPM_NET_EVENT_TYPE_MAX
} FWPM_NET_EVENT_TYPE ;
支持的过滤条件(FWPM_NET_EVENT_ENUM_TEMPLATE):
Value
Meaning
FWPM_CONDITION_IP_PROTOCOL
The IP protocol number, as specified in RFC 1700.
FWPM_CONDITION_IP_LOCAL_ADDRESS
The local IP address.
FWPM_CONDITION_IP_REMOTE_ADDRESS
The remote IP address.
FWPM_CONDITION_IP_LOCAL_PORT
The local transport protocol port number. For ICMP, the message type.
FWPM_CONDITION_IP_REMOTE_PORT
The remote transport protocol port number. For ICMP, the message code.
FWPM_CONDITION_SCOPE_ID
The interface IPv6 scope identifier. Reserved for internal use.
FWPM_CONDITION_ALE_APP_ID
The full path of the application.
FWPM_CONDITION_ALE_USER_ID
The identification of the local user.
非 driver 调用的方式只能获得普通的 drop 事件.
监控网络链接(NetConnection)
相较监控网络事件, 监控链接需要更高权限.
callback 方式
The caller needs FWPM_ACTRL_ENUM access to the connection objects’ containers and FWPM_ACTRL_READ access to the connection objects. See Access Control for more information.
暂未能成功监控网络链接.
查到同样问题, Receiving in/out traffic stats using WFP user-mode API , 和我调研中遇到的现象一样, 订阅函数收不到任何上报, 得不到任何事件, 没有报错. 开审计, 提权都没有成功. 有人提示非内核模式只能得到 drop 事件的上报, 这不能满足获取阻断事件的需求.
添加 security descriptor 示例: https://docs.microsoft.com/en-us/windows/win32/fwp/reserving-ports
Application Layer Enforcement(ALE)介绍
ALE 包含一系列在内核模式下的过滤器, 支持状态过滤.
ALE 层的过滤器可授权链接的创建, 端口分配, 套接字管理, 原始套接字创建, 和混杂模式接收.
ALE 层过滤器的分类基于链接(connection), 或基于套接字(socket), 其它层的过滤器只能基于数据包(packet)进行分类.
ALE 过滤器参考 ale-layers
编码
大多数 WFP 函数都可以从用户模式或内核模式调用。 但是,用户模式函数返回表示 Win32 错误代码的 DWORD 值,而内核模式函数返回表示 NT 状态代码的 NTSTATUS 值。 因此,函数名称和语义在用户模式和内核模式之间是相同的,但函数签名则不同。 这需要函数原型的单独用户模式和内核模式特定标头。 用户模式头文件名以"u"结尾,内核模式头文件名以"k"结尾。
结论
需求仅需要知道事件发生, 不需要即时处理事件, 另外开发驱动会带来更大的风险, 因此决定使用事件审计, 监控日志生成事件的方式来获得阻断事件.
新开一个线程来使用NotifyChangeEventLog 来监控日志记录事件.
附录
WFP 体系结构
WFP(Windows Filter Platform)
数据流
Data flow:
A packet comes into the network stack.
The network stack finds and calls a shim.
The shim invokes the classification process at a particular layer.
During classification, filters are matched and the resultant action is taken. (See Filter Arbitration.)
If any callout filters are matched during the classification process, the corresponding callouts are invoked.
The shim acts on the final filtering decision (for example, drop the packet).
参考链接
Windows防火墙管理-netsh
Friday, June 28, 2024
Windows 防火墙管理-netsh
管理工具
netsh advfirewall
# 导出防火墙规则
netsh advfirewall export advfirewallpolicy . wfw
# 导入防火墙规则
netsh advfirewall import advfirewallpolicy . wfw
# 查看防火墙状态
netsh advfirewall show allprofiles state
# 查看防火墙默认规则
netsh advfirewall show allprofiles firewallpolicy
# netsh advfirewall set allprofiles firewallpolicy blockinbound,allowoutbound
# netsh advfirewall set allprofiles firewallpolicy blockinbound,blockoutbound
# 查看防火墙设置
netsh advfirewall show allprofiles settings
# 启用防火墙
netsh advfirewall set allprofiles state on
# 禁用防火墙
netsh advfirewall set allprofiles state off
# 查看防火墙规则
netsh advfirewall firewall show rule name = all
# 查看防火墙状态
netsh advfirewall monitor show firewall
netsh firewall(deprecated)
# 查看防火墙状态
netsh firewall show state
netsh mbn(Mobile Broadband network)
netsh wfp
# 查看防火墙状态
netsh wfp show state
# 查看防火墙规则
netsh wfp show filter s
Windows相关资源
Friday, June 28, 2024
Windows 资源整理
这里只列举了一些 Windows 上调试,排查问题以及测试的一些常用工具,其他的加壳脱壳,加密解密,文件编辑器以及编程工具不进行整理了。
工具篇
监控&分析
AntiRootkit 工具
PE 工具
逆向&调试
注入工具
网络
压测工具
其他
代码篇
操作系统
内核封装
VT 技术
其他
CTF 资源
渗透相关
专利免费查询
Windows导览
Friday, June 28, 2024
Windows
windows-ipv6管理
Friday, June 28, 2024
windows-ipv6 管理
# 查看ipv6地址, 过滤locallink地址, 过滤Loopback地址
Get-NetIPAddress -AddressFamily IPv6 | Where-Object { $_ . IPAddress -notlike "fe80*" -and $_ . IPAddress -notlike "::1" } | Format-Table -AutoSize
# 查看ipv6路由
Get-NetRoute -AddressFamily IPv6
# 查看ipv6邻居
Get-NetNeighbor -AddressFamily IPv6
# 查看interface
Get-NetAdapter
# 使能临时ipv6地址
Set-NetIPv6Protocol -UseTemporaryAddress Enabled
# 获取interface 信息
Get-NetIPInterface -AddressFamily IPv6 -ifAlias Ethernet | Select-Object -Property *
Get-NetIPv6Protocol
# 设置interface 信息, 解决Windows IPv6地址不更新的问题
Set-NetIPInterface -AddressFamily IPv6 -ifAlias Ethernet -Dhcp Disabled
Set-NetIPInterface -AddressFamily IPv6 -ifAlias Ethernet -AdvertiseDefaultRoute Disabled
Set-NetIPInterface -AddressFamily IPv6 -ifAlias Ethernet -IgnoreDefaultRoutes Enabled
# 手动恢复ipv6访问
# Set-NetIPInterface -AddressFamily IPv6 -ifAlias Ethernet -RouterDiscovery Disabled
# Set-NetIPInterface -AddressFamily IPv6 -ifAlias Ethernet -RouterDiscovery Enabled
Set-NetIPv6Protocol -DhcpMediaSense Disabled
Set-NetIPv6Protocol -RandomizeIdentifiers Disabled
Set-NetIPv6Protocol -UseTemporaryAddresses Disabled
Set-NetIPv6Protocol -MaxTemporaryDesyncTime 0 : 3 : 0
Set-NetIPv6Protocol -MaxTemporaryPreferredLifetime 0 : 10 : 0
Set-NetIPv6Protocol -MaxTemporaryValidLifetime 0 : 30 : 0
Set-NetIPv6Protocol -TemporaryRegenerateTime 0 : 0 : 30
window-message
Friday, June 28, 2024
windows-message
Win-to-go
Friday, June 28, 2024
===
Windows To Go 的优点在于移动便携性, 缺点在于经典 Windows系统的数个功能受到限制.
前言
Windows To Go出现很多年了, 可是百度到的中文文档却如此少, 不禁为国内IT技术的发展而担忧.作者J参加工作时间不长, 能力有限, 但工作中接触大量英文开发文档, 因此仍希望能做一点基础的铺路工作, 方便后来者查阅, 有不当之处也请读者不吝指出. Windows To Go有详尽的官方文档, 有英文阅读能力的可以直接跳转到微软官方文档. 链接如下:
本文主要会介绍 Overview, 和一些常见问题, 大部分内容为翻译, 少量作者的提醒以[J]来标注直至句号结束, 以确保不误导读者.
Windows To Go Overview
Windows To Go 是 Windows 企业版和教育版上的功能, 大多数家庭用户使用的家庭版没有此功能. 它使我们能创建从U盘或硬盘启动的便携Windows系统. Windows To Go 并不是创造出来取代传统工作工具的. 它的主要目的是为了使具有经常切换工作空间需求的人更有效率. 在开始使用 Windows To Go 之前, 使用者必须了解以下注意事项:
Windows To Go 和传统 Windows 安装方式的区别;
使用 Windows To Go 来移动工作;
准备安装 Windows To Go;
硬件要求.
Windows To Go 和传统 Windows 安装方式的区别
Windows To Go 的工作环境和传统 Windows 几乎一样, 只有以下几点不同:
除了使用中的U盘, 机器的其它硬盘默认为离线状态. 即在文件管理器里不可见, 这是为了保护数据的安全. [J]但你仍然有方法可以使其它硬盘出现, 并修改里边的文件.
TPM 信任平台模块不可用. TPM 模块会绑定到特定某台电脑, 以保护商业数据. [J]多数民用电脑没有TPM模块, 但如果你的商用电脑已经加入了公司的域, 最好不要尝试在该电脑上使用 Windows To Go, 否则建议您先准备好下份工作的简历.
Windows To Go的休眠默认被禁用, 但仍可以通过组策略来打开. [J]很多机器在休眠会断开和USB设备的连接, 导致不能从休眠中恢复, 这很好理解, 微软已经替我们考虑到了这点, 所以没必要去修改这个设置.
Windows 的恢复(Restore)功能被禁用. 如果系统出现问题, 只能重装Windows了.
恢复到出厂设置不可用, 重置Windows不可用.
升级不可用. Windows 只能停留在安装时的版本, 不能从Windows 7 升到8, 也不能从 Windows 10 Red Stone 1 升级到 Red Stone 2.
使用 Windows To Go 来移动工作
Windows To Go 可以在多台机器之间切换, 系统会自动决定设备启动需要的驱动程序. 有一些和系统硬件强关联的应用可能无法运行. [J]比如Thinkpad触控板的设置程序, 指纹识别设置程序等.
准备安装 Windows To Go
可以使用 System Center Configuration Manager , 或者 Windows 的标准部署工具, 例如 DiskPart , Deployment Image Servicing and Management (DISM) . 需要注意以下问题:
是否有需要注入到 Windows To Go 镜像的驱动?
在不同机器上移动工作时时, 怎样合适的存储及同步数据?
32位还是64位? [J]新的机器都支持64位, 64位处理器的机器也能运行32位系统, 32位处理器不能运行64位的系统, 64位系统运行时占用更大的硬盘空间和内存空间. 如果你需要迁移使用的机器处理器架构有只支持32位的处理器, 或者机器内存少于4G, 建议你使用32位系统.
从协作网络以外的网络远程连接时的分辨率应该设为多少?
硬件要求
USB 硬盘或 U盘
Windows To Go针对以下列出的设备已做出了特别优化来满足需求, 包括
优化USB设备的高随机读写, 以使日常操作更流畅.
在已认证的设备上可以启动Windows 7及后续系统.
即使运行Windows To Go, USB设备也享受原厂保修支持. [J]没说插U盘的电脑会享受保修.
没有通过认证的 USB 设备, 不支持使用 Windows To Go. [J]能不能使用试试就知道了, 不行也知道是为什么. [J]同时网上有修改 U 盘厂商和型号来达到强制支持的另类方法, 不做赘述.
载体机器(Host computer)
认证支持Windows 7及后续系统.
运行Windows RT系统的电脑不受支持.
苹果Mac电脑不受支持. [J]尽管网络上遍布谈Windows To Go在Mac上运行的体验, 但官方文档明确说了, 不支持Mac的使用场景.
以下列出载体电脑的最低配置.
Item
Requirement
启动方式
可以USB启动
固件
从USB启动的设置打开
处理器架构
必须支持Windows To Go
外置USB Hub
不支持. Windows To Go 设备必须直接接在载体电脑上
处理器
1GHz以上
RAM
2 GB以上
显卡
有WDDM1.2的DirectX 9及以上
USB端口
USB 2.0及以上
检查载体 PC 和 Windows To Go 盘的架构兼容性
Host PC Firmware Type
Host PC Processor Architecture
Compatible Windows To Go Image Architecture
Legacy BIOS
32-bit
32-bit only
Legacy BIOS
64-bit
32-bit and 64-bit
UEFI BIOS
32-bit
32-bit only
UEFI BIOS
64-bit
64-bit only
Windows To Go 的常见问题
Windows To Go: frequently asked questions
environment
Friday, June 28, 2024
虚拟内存磁盘配置
Friday, June 28, 2024
虚拟内存磁盘配置
浏览器缓存到虚拟磁盘
< !-- imdisk -->
rd /q /s "C:\Users\Administrator\AppData\Local\Microsoft\Edge\User Data\Default\Cache"
rd /q /s "C:\Users\Administrator\AppData\Local\Microsoft\Edge\User Data\Default\Code Cache"
md M:\Edge_Cache\
md M:\Edge_CodeCache\
mklink /D "C:\Users\Administrator\AppData\Local\Microsoft\Edge\User Data\Default\Cache" "M:\Edge_Cache\"
mklink /D "C:\Users\Administrator\AppData\Local\Microsoft\Edge\User Data\Default\Code Cache" "M:\Edge_CodeCache\"
wsl
Friday, June 28, 2024
配置 wsl
远程访问 ssh
wsl
sudo apt install openssh-server
sudo nano /etc/ssh/sshd_config
/etc/ssh/sshd_config
...STUFF ABOVE THIS...
Port 2222
#AddressFamily any
ListenAddress 0.0.0.0
#ListenAddress ::
...STUFF BELOW THIS...
windows
service ssh start
netsh interface portproxy add v4tov4 listenaddress = 0.0 . 0 . 0 listenport = 2222 connectaddress = 172.23 . 129 . 80 connectport = 2222
netsh advfirewall firewall add rule name = "Open Port 2222 for WSL2" dir = in action = allow protocol = TCP localport = 2222
netsh interface portproxy show v4tov4
netsh int portproxy reset all
配置 wsl
https://docs.microsoft.com/en-us/windows/wsl/wsl-config#configuration-setting-for-wslconfig
Set-Content -Path " $env:userprofile \\.wslconfig" -Value "
# Settings apply across all Linux distros running on WSL 2
[wsl2]
# Limits VM memory to use no more than 4 GB, this can be set as whole numbers using GB or MB
memory=2GB
# Sets the VM to use two virtual processors
processors=2
# Specify a custom Linux kernel to use with your installed distros. The default kernel used can be found at https://github.com/microsoft/WSL2-Linux-Kernel
# kernel=C:\\temp\\myCustomKernel
# Sets additional kernel parameters, in this case enabling older Linux base images such as Centos 6
# kernelCommandLine = vsyscall=emulate
# Sets amount of swap storage space to 8GB, default is 25% of available RAM
swap=1GB
# Sets swapfile path location, default is %USERPROFILE%\AppData\Local\Temp\swap.vhdx
swapfile=C:\\temp\\wsl-swap.vhdx
# Disable page reporting so WSL retains all allocated memory claimed from Windows and releases none back when free
pageReporting=false
# Turn off default connection to bind WSL 2 localhost to Windows localhost
localhostforwarding=true
# Disables nested virtualization
nestedVirtualization=false
# Turns on output console showing contents of dmesg when opening a WSL 2 distro for debugging
debugConsole=true
"
设计模式究竟有几个原则
Thursday, May 16, 2024
最早总结的设计模式只有 5 个, 即SOLID
:
单一职责原则 (Single Responsibility Principle, SRP)
:一个类应该只有一个引起变化的原因,即一个类应该只有一个责任。
开闭原则 (Open/Closed Principle, OCP)
:软件实体(类、模块、函数等)应该对扩展开放,对修改关闭,即应该通过扩展来实现变化,而不是通过修改已有的代码。
里氏替换原则 (Liskov Substitution Principle, LSP)
:子类型必须能够替换其基类型,即派生类必须能够替换其基类而不影响程序的正确性。
接口隔离原则 (Interface Segregation Principle, ISP)
:不应该强迫客户端依赖于它们不使用的接口。应该将大接口拆分成更小的、更具体的接口,以便客户端只需知道它们需要使用的方法。
依赖倒置原则 (Dependency Inversion Principle, DIP)
:高层模块不应该依赖于低层模块,二者都应该依赖于抽象。抽象不应该依赖于具体实现细节,具体实现细节应该依赖于抽象。
后来增加了两个规则, 这些后加的规则相较来说更具体, 更有指导性. 我们从原则解释中可以看到SOLID
描述应该怎么做
, 后加的规则描述优先/最好怎么做
.
合成/聚合复用原则 (Composition/Aggregation Reuse Principle, CARP)
:应该优先使用对象组合(合成)和聚合,而不是继承来达到代码复用的目的。
迪米特法则 (Law of Demeter, LoD)
:一个对象应该对其他对象有尽可能少的了解,即一个对象应该对其它对象的内部结构和实现细节知道得越少越好。
除了上述提到的常见设计原则外,还有一些其他的设计原则,虽然不如前面提到的那些广为人知,但同样对软件设计和架构有重要的指导作用。
后续提出的这些规则, 有点画蛇添足, 至少我认为它们不反直觉, 不需要深入思考.
最少知识原则 (Principle of Least Knowledge, PoLK)
:也被称为迪米特法则的扩展,主张一个对象应该尽可能少地了解其他对象的信息。这个原则的产生可以追溯到 1987 年由帕特里夏·莱塞尔(Patricia Lago)和科威特·伯克(Koos Visser)提出的“最少通信法则”。
稳定依赖原则 (Stable Dependencies Principle, SDP)
:该原则认为软件设计应该确保稳定的组件不依赖于不稳定的组件,即稳定性较高的组件应该更少地依赖于稳定性较低的组件。这个原则的思想来源于对软件系统中组件之间关系的深入研究。
稳定抽象原则 (Stable Abstraction Principle, SAP)
:与稳定依赖原则相呼应,该原则指导着将抽象性与稳定性相匹配,即稳定的组件应该是抽象的,而不稳定的组件应该是具体的。这个原则有助于确保软件系统的稳定性和灵活性。
多平台内容发布工具--蚁小二体验
Thursday, May 09, 2024
前言
最近想写点东西扩展下收入类别, 四处调研了下创作者平台, 看看是否能靠文字赚点钱, 实在不行赚点金币也是好的.
先不论具体的平台, 写东西多少是要费点脑子的, 文章生产不易, 自然不会只投一个平台, 如果需要发布多个平台, 则会涉及一件比较令人厌烦的事, 那就是同一件事需要重复做多次.
如果各平台都支持外链, 支持 markdown 格式, 简单的复制粘贴倒也不会太令人苦恼. 但实际情况是, 很多平台都不支持 markdown 文件导入, 但好消息是它们都支持 word 导入. 可以 md 转 docx, 再 docx 导入.
另外在多个平台发文, 还需要在各自的发布页面操作, 我希望的是可以批量操作. 搜索的时候找到了蚁小二
这样一个工具, 请放心这不是带货, 如果这东西确实能给我带来很多价值的话, 我自然会审慎而吝啬的决定是否要分享. 既然我分享出来了, 就代表我对它带来的价值存疑.
支持的平台
可以一键发布内容到多个平台, 我所使用的免费版支持添加五个账号, 由于我是文字创作, 五个账号已经足够了. 如果是视频创作者, 这个工具或许可以帮上许多忙.
文字自媒体体验
视频部分由于我完全不会做视频, 就暂且不分享体验了, 下面仅就自媒体平台体验做一些分享.
创作界面可以参考常见的 word 编辑器, 包含段落, 加粗, 引用, 下划线, 删除线, 斜体, 分割线, 缩进, 图片
不支持超链接
不支持表格
没有 markdown, 可以通过在 vscode 的 mardown 预览中拷贝粘贴实现了一些格式的保留
抽象支持多个平台
抽象支持平台多个账号
一键发布 , 不得不说体验还可以, 但是我想看到文章反馈还是得去各平台查看.
平时不太看这些自媒体, 因为质量着实不高, 现在看其实门槛也不高, 未来我也将会在这些平台上发布一些内容, 敬请期待.
这是我第一次使用这个工具, 我并不是专家, 不知道发什么能赚钱, 也不知道收入如何, 如果有大佬, 诚心请教指点一二, 不胜感激.
简书的创作体验
Thursday, May 09, 2024
简书的写文章体验仅强于记事本.
笔记管理简洁
这是文章编辑页面, 仅两层抽象,
层级少有好处也有坏处, 简单的操作可以减低理解成本. 但会在将来增加作者的文章管理成本.
图片上传困难
简书在长达 8 年的时间里仍然没有处理好外链图片上传问题
外链只是有时会失败, 许多图床是允许空 reffer 或任意 reffer 获取的, 简书试都不试下, 还称本地上传是"正确"的图片上传方式, 不知道运营者有没有试过别的平台的创作体验.
很难相信会有作者只深耕一个平台, 平台如果不能让创作者方便的复制粘贴, 只能一直走小众路线.
没有审核
简书似乎不怎么审核, 从未看到过审核状态, 文章发出即可阅. 如果一个平台不怎么审核的话, 或许我们可以这样那样…
随机 IP 属地
简书实际没有实现 IP 属地, IP 地址刷新即随机更新.
微信读书体验分享
Tuesday, December 24, 2024
免费读书方案有很多, 但是微信读书的确是体验做的较好的一个. 希望免费读书的可以看 zlibrary.
这里主要分享微信读书的使用体验, 以及一些辅助工具, 如有任何侵权, 请联系我删除: [email protected]
微信读书自动打卡刷时长
Thursday, December 05, 2024
只为便宜一点买微信读书会员.
微信读书规则
离线阅读计入总时长, 但需要联网上报
网页版, 墨水屏, 小程序, 听书, 有声书收听都计入总时长
对单次自动阅读或收听时长过长的行为, 平台将结合用户行为特征判断, 过长部分不计入总时长
当日阅读超过5 分钟 才算作有效阅读天数
付费 5 元立即获得 2 天会员, 后续 30 日内打卡 29 天, 读书时长超过 30 小时, 可获得 30 天会员和 30 书币
付费 50 元立即获得 30 天会员, 后续 365 日内打卡 360 天, 读书时长超过 300 小时, 可获得 365 天会员和 500 书币
根据实际操作, 还有如下未明确说明的特点:
第 29 日打卡后立即获得读书会员奖励, 并可立即开始下一轮挑战会员打卡, 无需等待第 31 日开始下一轮挑战, 第 29 日的打卡既算上一轮的打卡, 也算下一轮的打卡.
除第一轮需 29 日外, 后续每 28 日即可获得 32 日会员, 1+28*13=365, 一年可完成 13 轮, 花费 65 元, 获得 32*13=416 天会员和 390 书币.
更划算的仍然是年卡挑战会员, 但周期更长, 风险更大.
工具特性
使用有头浏览器
支持本地浏览器和远程浏览器
随机浏览器宽度和高度
支持等待登录
支持登录二维码刷新
支持保存 cookies
支持加载 cookies
支持选择最近阅读的第 X 本书开始阅读
默认随机选择一本书开始阅读
支持自动阅读
支持跳到下一章
支持读完跳回第一章继续阅读
支持选择阅读速度
随机单页阅读时间
随机翻页时间
每分钟截图当前界面
支持日志
支持定时任务
支持设置阅读时间
支持邮件通知
多平台支持: linux | windows | macos
多架构支持: amd64 | arm64
支持浏览器: chrome | MicrosoftEdge | firefox
支持多用户
异常时强制刷新
使用统计
Linux
直接运行
# 安装nodejs
sudo apt install nodejs
# 老旧版本的 nodejs 需要安装 npm
sudo apt install npm
# 创建运行文件夹
mkdir -p $HOME /Documents/weread-challenge
cd $HOME /Documents/weread-challenge
# 安装依赖
npm install selenium-webdriver
# 下载脚本
wget https://storage1.techfetch.dev/weread-challenge/weread-challenge.js -O weread-challenge.js
# 通过环境变量设置运行参数
export WEREAD_BROWSER = "chrome"
# 运行
WEREAD_BROWSER = "chrome" node weread-challenge.js
如需邮件通知, 需安装 nodemailer :
npm install nodemailer
Docker Compose 运行
services :
app :
image : jqknono/weread-challenge:latest
pull_policy : always
environment :
- WEREAD_REMOTE_BROWSER=http://selenium:4444
- WEREAD_DURATION=68
volumes :
- ./data:/app/data
depends_on :
selenium :
condition : service_healthy
selenium :
image : selenium/standalone-chrome:4.26
pull_policy : if_not_present
shm_size : 2gb
volumes :
- /var/run/docker.sock:/var/run/docker.sock
environment :
- SE_ENABLE_TRACING=false
- SE_BIND_HOST=false
- SE_JAVA_DISABLE_HOSTNAME_VERIFICATION=false
healthcheck :
test : [ "CMD" , "curl" , "-f" , "http://localhost:4444/wd/hub/status" ]
interval : 5s
timeout : 60s
retries : 10
保存为 docker-compose.yml
, 运行 docker compose up -d
.
Docker 运行
# run selenium standalone
docker run --restart always -d --name selenium-live \
-v /var/run/docker.sock:/var/run/docker.sock \
--shm-size= "2g" \
-p 4444:4444 \
-p 7900:7900 \
-e SE_ENABLE_TRACING = false \
-e SE_BIND_HOST = false \
-e SE_JAVA_DISABLE_HOSTNAME_VERIFICATION = false \
-e SE_NODE_MAX_INSTANCES = 10 \
-e SE_NODE_MAX_SESSIONS = 10 \
-e SE_NODE_OVERRIDE_MAX_SESSIONS = true \
selenium/standalone-chrome:4.26
# run weread-challenge
docker run --rm --name user-read \
-v $HOME /weread-challenge/user/data:/app/data \
-e WEREAD_REMOTE_BROWSER = http://172.17.0.2:4444 \
-e WEREAD_DURATION = 68 \
weread-challenge:latest
# add another user
docker run --rm --name user2-read \
-v $HOME /weread-challenge/user2/data:/app/data \
-e WEREAD_REMOTE_BROWSER = http://172.17.0.2:4444 \
-e WEREAD_DURATION = 68 \
weread-challenge:latest
创建定时任务
docker-compose 方式
WORKDIR = $HOME /weread-challenge
mkdir -p $WORKDIR
cd $WORKDIR
cat > $WORKDIR /docker-compose.yml <<EOF
services:
app:
image: jqknono/weread-challenge:latest
pull_policy: always
environment:
- WEREAD_REMOTE_BROWSER=http://selenium:4444
- WEREAD_DURATION=68
volumes:
- ./data:/app/data
depends_on:
selenium:
condition: service_healthy
selenium:
image: selenium/standalone-chrome:4.26
pull_policy: if_not_present
shm_size: 2gb
volumes:
- /var/run/docker.sock:/var/run/docker.sock
environment:
- SE_ENABLE_TRACING=false
- SE_BIND_HOST=false
- SE_JAVA_DISABLE_HOSTNAME_VERIFICATION=false
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:4444/wd/hub/status"]
interval: 5s
timeout: 60s
retries: 10
EOF
# 首次启动后, 需微信扫描二维码登录, 二维码保存在 $HOME/weread-challenge/data/login.png
# 每天早上 7 点启动, 阅读68分钟
( crontab -l 2>/dev/null; echo "00 07 * * * cd $WORKDIR && docker compose up -d" ) | crontab -
docker 方式
# 启动浏览器
docker run --restart always -d --name selenium-live \
-v /var/run/docker.sock:/var/run/docker.sock \
--shm-size= "2g" \
-p 4444:4444 \
-p 7900:7900 \
-e SE_ENABLE_TRACING = false \
-e SE_BIND_HOST = false \
-e SE_JAVA_DISABLE_HOSTNAME_VERIFICATION = false \
-e SE_NODE_MAX_INSTANCES = 3 \
-e SE_NODE_MAX_SESSIONS = 3 \
-e SE_NODE_OVERRIDE_MAX_SESSIONS = true \
-e SE_SESSION_REQUEST_TIMEOUT = 10 \
-e SE_SESSION_RETRY_INTERVAL = 3 \
selenium/standalone-chrome:4.26
WEREAD_USER = "user"
mkdir -p $HOME /weread-challenge/$WEREAD_USER /data
# Get container IP
Selenium_IP = $( docker inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' selenium-live)
# 首次启动后, 需微信扫描二维码登录, 二维码保存在 $HOME/weread-challenge/$WEREAD_USER/data/login.png
# 每天早上 7 点启动, 阅读68分钟
( crontab -l 2>/dev/null; echo "00 07 * * * docker run --rm --name ${ WEREAD_USER } -read -v $HOME /weread-challenge/ ${ WEREAD_USER } /data:/app/data -e WEREAD_REMOTE_BROWSER=http:// ${ Selenium_IP } :4444 -e WEREAD_DURATION=68 -e WEREAD_USER= ${ WEREAD_USER } jqknono/weread-challenge:latest" ) | crontab -
Windows
# 安装nodejs
winget install -e - -id Node . js . Node . js
# 创建运行文件夹
mkdir -p $HOME / Documents / weread-challenge
cd $HOME / Documents / weread-challenge
# 安装依赖
npm install selenium-webdriver
# 下载脚本powershell
Invoke-WebRequest -Uri https : // storage1 . techfetch . dev / weread-challenge / weread-challenge . js -OutFile weread-challenge . js
# 通过环境变量设置运行参数
$env:WEREAD_BROWSER = "MicrosoftEdge"
# 运行
node weread-challenge . js
Docker 运行同 Linux.
MacOS
# 安装nodejs
brew install node
# 创建运行文件夹
mkdir -p $HOME /Documents/weread-challenge
cd $HOME /Documents/weread-challenge
# 安装依赖
npm install selenium-webdriver
# 下载脚本
wget https://storage1.techfetch.dev/weread-challenge/weread-challenge.js -O weread-challenge.js
# 通过环境变量设置运行参数
export WEREAD_BROWSER = "chrome"
# 运行
node weread-challenge.js
Docker 运行同 Linux.
多用户支持
# 启动浏览器
docker run --restart always -d --name selenium-live \
-v /var/run/docker.sock:/var/run/docker.sock \
--shm-size= "2g" \
-p 4444:4444 \
-p 7900:7900 \
-e SE_ENABLE_TRACING = false \
-e SE_BIND_HOST = false \
-e SE_JAVA_DISABLE_HOSTNAME_VERIFICATION = false \
-e SE_NODE_MAX_INSTANCES = 10 \
-e SE_NODE_MAX_SESSIONS = 10 \
-e SE_NODE_OVERRIDE_MAX_SESSIONS = true \
selenium/standalone-chrome:4.26
WEREAD_USER1 = "user1"
WEREAD_USER2 = "user2"
mkdir -p $HOME /weread-challenge/$WEREAD_USER1 /data
mkdir -p $HOME /weread-challenge/$WEREAD_USER2 /data
# Get container IP
Selenium_IP = $( docker inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' selenium-live)
# 首次启动后, 需微信扫描二维码登录, 二维码保存在:
# /$HOME/weread-challenge/${WEREAD_USER1}/data/login.png
# /$HOME/weread-challenge/${WEREAD_USER2}/data/login.png
# 每天早上 7 点启动, 阅读68分钟
( crontab -l 2>/dev/null; echo "00 07 * * * docker run --rm --name ${ WEREAD_USER1 } -read -v $HOME /weread-challenge/ ${ WEREAD_USER1 } /data:/app/data -e WEREAD_REMOTE_BROWSER=http:// ${ Selenium_IP } :4444 -e WEREAD_DURATION=68 -e WEREAD_USER= ${ WEREAD_USER1 } jqknono/weread-challenge:latest" ) | crontab -
( crontab -l 2>/dev/null; echo "00 07 * * * docker run --rm --name ${ WEREAD_USER2 } -read -v $HOME /weread-challenge/ ${ WEREAD_USER2 } /data:/app/data -e WEREAD_REMOTE_BROWSER=http:// ${ Selenium_IP } :4444 -e WEREAD_DURATION=68 -e WEREAD_USER= ${ WEREAD_USER2 } jqknono/weread-challenge:latest" ) | crontab -
可配置项
环境变量
默认值
可选值
说明
WEREAD_USER
weread-default
-
用户标识
WEREAD_REMOTE_BROWSER
""
-
远程浏览器地址
WEREAD_DURATION
10
-
阅读时长
WEREAD_SPEED
slow
slow,normal,fast
阅读速度
WEREAD_SELECTION
random
[0-4]
选择阅读的书籍
WEREAD_BROWSER
chrome
chrome,MicrosoftEdge,firefox
浏览器
ENABLE_EMAIL
false
true,false
邮件通知
EMAIL_SMTP
""
-
邮箱 SMTP 服务器
EMAIL_USER
""
-
邮箱用户名
EMAIL_PASS
""
-
邮箱密码
EMAIL_TO
""
-
收件人
WEREAD_AGREE_TERMS
true
true,false
隐私同意条款
容器多架构支持
支持 linux/amd64,linux/arm64, 支持树莓派等开发板.
docker buildx create --name weread-challenge
docker buildx use weread-challenge
docker buildx inspect --bootstrap
docker buildx build --platform linux/amd64,linux/arm64 -t jqknono/weread-challenge:base -f Dockerfile.base --push .
docker buildx build --platform linux/amd64,linux/arm64 -t jqknono/weread-challenge:latest -f Dockerfile.quick --push .
注意事项
28 日刷满 30 小时, 需每日至少 65 分钟, 而不是每日 60 分钟.
微信读书统计可能会漏数分钟, 期望每日获得 65 分钟, 建议调整阅读时长到 68 分钟
网页扫码登录 cookies 有效期为 30 天, 30 天后需重新扫码登录, 适合月挑战会员
邮件通知可能被识别为垃圾邮件, 建议在收件方添加白名单
本项目仅供学习交流使用, 请勿用于商业用途, 请勿用于违法用途
如存在可能的侵权, 请联系 [email protected]
, 本项目会立即删除
隐私政策
隐私获取
本项目搜集使用者的 cookies
部分信息, 以用于使用者统计和展示.
搜集使用者的使用信息, 包含: 用户名称 | 首次使用时间 | 最近使用时间 | 总使用次数 | 浏览器类型 | 操作系统类别 | 阅读时长设置 | 异常退出原因
如不希望被搜集任何信息, 可设置启动参数WEREAD_AGREE_TERMS=false
风险提示
cookies
可用于微信读书网页登录, 登录后可以执行书架操作, 但本工具不会使用搜集的信息进行登录操作 .
腾讯保护机制确保异常登录时, 手机客户端将收到风险提示, 可在手机客户端设置
->登录设备
中确认登录设备.
本工具纯 js 实现, 容易反混淆和扩展, 第三方可以继续开发. 即使信任本工具, 也应在使用自动化工具时, 经常确认登录设备, 避免书架被恶意操作.
参考
关闭独显以省电
Monday, November 18, 2024
这篇文分享给台式机很少关机, 经常远程回家中的台式机上工作的朋友.
我的主力工作机和游戏机是同一台机器, 显示屏是 4K 144Hz, 日常都是开着独显, 普通操作显示都会更顺滑一些, 但是功耗也是明显更大.
以下截图里的功率同时带着一个 J4125 小主机, 日常功耗在 18w 上下, 因此结论可能有不准确的地方
不开游戏, 在桌面快速滑动鼠标的峰值功率可以到192w
关闭独显后, 刷新率降到 60Hz, 峰值功率降到120w 上下.
在外隧道回家工作是使用的腾讯的一个入门主机, 带宽较小, 远端刷新率只有 30hz, 这种情况用独显是没有意义, 可以考虑切换到集显.
多数时候, 我不直接使用远程桌面, 而是使用 vscode 的远程开发, 优势是隐蔽, 占用带宽小, 几乎是本地开发的体验.
普通代码编辑时, 约 72w, 与关闭独显前的 120w 相比, 有一定的节能效果.
使用remote ssh
进行远程开发时, 可以用使用脚本关闭独显.
脚本保存为switch_dedicate_graphic_cards.ps1
, 使用方法为switch_dedicate_graphic_cards.ps1 off
# Usage: switch_dedicate_graphic_cards.ps1 on|off
# Get parameters
$switch = $args [ 0 ]
# exit if no parameter is passed
if ( $switch -eq $null ) {
Write-Host "Usage: switch_dedicate_graphic_cards.ps1 on|off" -ForegroundColor Yellow
exit
}
# Get display devices
$displayDevices = Get-CimInstance -Namespace root \ cimv2 -ClassName Win32_VideoController
# If there is no display device or only one display device, exit
if ( $displayDevices . Count -le 1 ) {
Write-Host "No display device found."
exit
}
# Get dedicated graphic cards
$dedicatedGraphicCards = $displayDevices | Where-Object { $_ . Description -like "*NVIDIA*" }
# If there is no dedicated graphic card, exit
if ( $dedicatedGraphicCards . Count -eq 0 ) {
Write-Host "No dedicated graphic card found."
exit
}
# turn dedicated graphic cards on or off
if ( $switch -eq "on" ) {
$dedicatedGraphicCards | ForEach-Object { pnputil / enable-device $_ . PNPDeviceID }
Write-Host "Dedicated graphic cards are turned on."
} elseif ( $switch -eq "off" ) {
$dedicatedGraphicCards | ForEach-Object { pnputil / disable-device $_ . PNPDeviceID }
Write-Host "Dedicated graphic cards are turned off."
} else {
Write-Host "Invalid parameter."
Write-Host "Usage: switch_dedicate_graphic_cards.ps1 on|off" -ForegroundColor Yellow
}
docker
Friday, June 28, 2024
k8s 和 docker
docker介绍
Friday, June 28, 2024
docker 介绍
docker 是一个应用容器引擎, 可以打包应用及其依赖包到一个可移植的容器中, 然后发布到任何流行的 Linux 或 Windows 机器上, 也可以实现虚拟化.
为什么会有 docker, 因为开发和运维经常遇到一类问题, 那就是应用在开发人员的环境上运行没有任何问题, 但在实际生产环境中却 bug 百出.
程序的运行从硬件架构到操作系统, 再到应用程序, 这些都是不同的层次, 但是开发人员往往只关注应用程序的开发, 而忽略了其他层次的问题.
docker 的出现就是为了解决这个问题, 它将应用程序及其依赖, 打包在一个容器中, 这样就不用担心环境的问题了.
同步开发和生产环境, 使开发人员可以在本地开发, 测试, 部署应用程序, 而不用担心环境的问题. 显著提升了开发和运维的效率, 代价是一点点资源的浪费.
我极力建议所有开发者都学会使用容器进行开发和部署, 它以相对很低的代价, 为你的应用程序提供一个稳定的运行环境, 从而提高开发和运维的效率.
使用一些通俗的语言来描述使用 docker 的一种工作流:
从零创建一个开发的环境, 包含了操作系统, 应用程序, 依赖包, 配置文件等等.
环境可以在任何地方运行, 也可以在任何地方创建.
环境对源码编译的结果稳定且可预测, 行为完全一致.
环境中程序的运行不会产生任何歧义.
最好是可以使用声明式的方式来创建环境(docker-compose), 进一步减少环境的隐藏差异, 环境的一切都已在声明里展示.
创建一个 commit, 创建镜像, 这相当于一个快照, 保存当前的环境, 以便以后使用.
分享镜像给其它开发和运维, 大家基于相同语境同步展开工作.
随着业务的发展需求, 修改镜像, 重新创建 commit, 重新创建镜像, 重新分发.
docker 的基本架构
adguard
Friday, June 28, 2024
利用DNS服务平滑切换网络服务
Wednesday, June 12, 2024
假设服务域名为example.domain
, 原服务器 IP 地址为A
, 由于服务器迁移或 IP 更换, 新服务器 IP 地址为B
, 为了保证用户无感知, 可以通过 DNS 服务平滑切换网络服务.
原服务状态, example.domain
解析到 IP 地址A
过渡状态, example.domain
解析到 IP 地址A
和B
新服务状态, example.domain
解析到 IP 地址B
, 移除 IP 地址A
说明: 当用户获得两个解析地址时, 客户端会选择其中一个地址进行连接, 当连接失败时, 会尝试其它地址, 以此保证服务的可用性.
由于 DNS 解析存在缓存, 为了保证平滑切换, 需要在过渡状态保持一段时间, 以确保所有缓存失效.
我这里需要迁移的是 dns 服务, 可以在过渡状态中设置DNS重写
, 加快迁移过程.
A 服务重写规则:
B 服务重写规则:
原迁移过程拓展为:
原服务状态, example.domain
解析到 IP 地址A
过渡状态, example.domain
在dns A
服务中重写到A
和B
, 在dns B
服务中重写到B
新服务状态, example.domain
解析到 IP 地址B
, 移除 IP 地址A
当用户仍在使用dns A
服务时, 可以获得两个地址, 有一半的概率会选择dns A
服务.
另外一半的概率会切换到dns B
服务, dns B
服务故障时切换回dns A
. dns B
服务未故障时, 将只会获得一个地址, 因而用户会停留在dns B
服务中.
这样我们可以逐步的减少dns A
服务的资源消耗, 而不是直接停止, 实现更平滑的迁移.
测试工具
Friday, June 28, 2024
测试工具
Copilot系列
Friday, June 28, 2024
避免博客泄露个人信息
本文介绍了在博客写作中如何保护个人隐私,避免敏感信息泄露的实用技巧和最佳实践。
Tuesday, November 12, 2024
常用的免费开源平台 GitHub Pages 比较受欢迎,许多博客使用 GitHub Pages 进行发布。
但其免费版要求公开仓库才允许公开访问。而仓库公开后,一些标记为草稿的文章也可以从 Git 仓库访问到。
尽管公开的文章较少包含敏感信息, 但开源博客的源库 可能会泄露个人信息,以下是一些常见的信息泄露关键词,欢迎评论补充。
敏感词
中文关键词
英文关键词
密码
password
账号
account
身份证
id
银行卡
card
支付宝
alipay
微信
wechat
手机号
phone
家庭住址
address
工作单位
company
社保卡
card
驾驶证
driver
护照
passport
信用卡
credit
密钥
key
配置文件
ini
凭证
credential
用户名
username
正则搜索:
( 密码| 账号| 身份证| 银行卡| 支付宝| 微信| 手机号| 家庭住址| 工作单位| 社保卡| 驾驶证| 护照| 信用卡| username| password| passwd| account| key\s *:| \. ini| credential| card| bank| alipay| wechat| passport| id\s *:| phone| address| company)
如果使用 VSCode 作为博客编辑器,可以使用正则搜索快速进行全站搜索,检查可能泄露信息的位置。
Git 历史
Git 历史可能包含信息泄露,通过简单的脚本即可扫描开源博客的历史提交信息。
如果是自己的仓库,可以通过以下方式清除历史。如果需要保留历史信息,则不要清除。
请务必确认理解命令含义,它会清理历史, 请谨慎操作,操作前请备份重要数据。
git reset --soft ${ first -commit }
git push --force
其它扫描仓库方式
https://github.com/trufflesecurity/trufflehog
Find, verify, and analyze leaked credentials
17.2k
stars
1.7k
forks
其它发布博客方式
Github Pro 支持将私有仓库发布到 Pages, Pro 四美元每月
设置为私有仓库, 发布到 Cloudflare Pages
分库, 一个私有库存放正在编辑的文章, 一个公开库存放可发布的文章
如果你的博客使用giscus
这样依赖 github 的评论系统, 那就仍然需要一个公开仓库.
良好的习惯 vs 良好的机制
在讨论开源博客泄露个人信息的问题时, 有许多人认为, 只要注意不将敏感信息上传到仓库, 就不会有问题.
这是一句无用的废话, 如同要求程序员不要写 bug 一样, 正确但是无用. 靠习惯来保护个人信息, 是不可靠的. 别轻易相信一个人的习惯, 他可能随时会忘记.
写作有时会有一些临时的语句, 特别是程序员的技术博客, 简短的脚本可能随手就写了, 未必会时时记住使用环境变量, 因此留下敏感信息的可能性一定存在.
相信多数人能明白好的习惯是什么, 因此这里不讨论良好的习惯, 主要分享如何通过机制来避免泄露个人信息.
首先是分库, 手稿库和发布库分开, 所有发布在 Github Pages 上的文章都是经过审核的, 且不会有 draft 状态的文章泄露.
还可以通过 Github Action , 在每次提交时, 扫描敏感信息, 如果有敏感信息, 则不允许提交, 参阅trufflehog
本文分享的正则搜索, 只是一个简单的示例, 未集成到任何流程中, 你可以根据自己的需求, 做更多的定制化工作, 将其集成到流程中.
参考
snort
Friday, June 28, 2024
Snort
https://www.snort.org/
Protect your network with the world’s most powerful Open Source detection software.
What is Snort?
Snort is the foremost Open Source Intrusion Prevention System (IPS) in the world.
Snort IPS uses a series of rules that help define malicious network activity and
uses those rules to find packets that match against them and generates alerts for users.
Snort can be deployed inline to stop these packets, as well.
Snort has three primary uses: As a packet sniffer like tcpdump, as a packet logger —
which is useful for network traffic debugging, or it can be used as a full-blown network intrusion prevention system.
Snort can be downloaded and configured for personal and business use alike.
snort配置
snort作防护工具使用的配置文件是默认的, 但是可以通过配置文件进行修改.
可信设计
Friday, June 28, 2024
安全架构与设计原则
安全三要素与安全设计原则
完整性 Integrity
可用性 Availability
机密性 Confidentiality
开放设计原则
Open Design
设计不应该是秘密, 开放设计更安全.
安全不依赖保密.
失败-默认安全原则
Fail-safe defaults
访问决策基于"允许", 而不是"拒绝".
默认情况下不允许访问, 保护机制仅用来识别允许访问的情况.
失败安全: 任何一个复杂系统应该有功能失效后的应急安全机制, 另外对错误消息和日志要小心, 防止信息泄露.
默认安全: 系统在初始状态下, 默认配置是安全的, 通过使用最少的系统和服务来提供最大的安全性.
权限分离原则
Separation of Privilege
一种保护机制需要使用两把钥匙来解锁, 比使用一把钥匙要更健壮和更灵活.
权限分离的目的
防止利益冲突, 个别权力滥用
对某一重要权限分解为多个权限, 让需要保护的对象更难被非法获取, 从而也更安全.
分离不同进程的权责
系统可以默认设置 3 个角色, 角色间系统账号权限相互独立, 权责分离:
系统管理员: 负责系统的日常用户管理, 配置管理.
安全管理员: 负责对用户状态, 安全配置的激活, 去激活管理.
安全审计员: 负责对前面二者的操作做日志审计, 并拥有日志导出权限, 保证系统用户所有操作的可追溯性.
最小权限原则
Least Privilege
系统的每一个用户, 每一个程序, 都应该使用最小且必须的权限集来完成工作.
确保应用程序使用最低的权限运行.
对系统中各用户运行各类程序, 如数据库, WEB 服务器登, 要注意最小权限的账户运行或连接, 不能是系统最高权限的账号.
新建账号时, 默认赋给最小权限的角色.
经济使用原则
Economy of Mechanism
保持系统设计和代码尽可能简单, 紧凑.
软件设计越复杂, 代码中出现 bug 的几率越高, 如果设计尽可能精巧, 那么出现安全问题几率越小.
删除不需要的冗余代码和功能模块, 保留该代码只会增加系统的攻击面.
设计可以重复使用的组件减少冗余代码.
经济适用: 简单, 精巧, 组件化.
不要过设计
最小公共化原则
Least Common Mechanism
尽量避免提供多个对象共享同一资源的场景, 对资源访问的共享数量和使用应应尽可能最小化.
共享对象提供了信息流和无意的相互作用的潜在危险通道, 尽量避免提供多个对象共享同一资源的场景.
如果一个或者多个对象不满意共享机制提供的服务. 那他们可以选择根本不用共享机制, 以免被其它对象的 bug 间接攻击.
共享内存最小化
端口绑定最小化
减少连接, 防御 Dos 攻击
完全仲裁原则
Complete Mediation
完全仲裁原则要求, 对于每个对象的每次访问都必须经过安全检查审核.
当主体试图访问客体时, 系统每次都会校验主体是否拥有该权限.
尽可能的由资源所有者来做出访问控制决定, 例如如果是一个 URL, 那么由后台服务器来检查, 不要在前端进行判断.
特别注意缓存的使用和检查, 无法保证每次访问缓存的信息都没有被黑客篡改过. eg. DNS 缓存欺骗.
心理可承受原则
Psychological Acceptability
安全机制可能为用户增加额外的负担, 但这种负担必须是最小的而且是合理的.
安全机制应该尽可能对系统用户友好, 方便他们对系统的使用和理解.
如果配置方法过于复杂繁琐, 系统管理员可能无意配置了一个错误的选项, 反而让系统变得不安全.
该原则一般与人机交互, UCD(User Centered Design)界面相关.
纵深防御原则
Defense in Depth
纵深防御是一个综合性要求很高的防御原则, 一般要求系统架构师综合运用其他的各类安全设计原则, 采用多点, 多重的安全校验机制, 高屋建瓴地的从系统架构层面来关注整个系统级的安全防御机制, 而不能只依赖单一安全机制.
华为可信概念
Friday, June 28, 2024
华为可信概念
安全性(Security):产品有良好的抗攻击能力,保护业务和数据的机密性、完整性和可用性。
韧性(Resilience):系统受攻击时保持有定义的运行状态(包括降级),遭遇攻击后快速恢复并持续演进的能力。
隐私性(Privacy):遵从隐私保护既是法律法规的要求,也是价值观的体现。用户应该能够适当地控制他们的数据的使用方式。信息的使用政策应该是对用户透明的。用户应该根据自己的需要来控制何时接收以及是否接收信息。用户的隐私数据要有完善的保护能力和机制。
安全性(Safety):系统失效导致的危害不存在不可接受的风险,不会伤害自然人生命或危及自然人健康,不管是直接还是通过损害环境或财产间接造成的。
可靠性和可用性(Reliability& Availability):产品能在生命周期内长期保障业务无故障运行,具备快速恢复和自我管理的能力,提供可预期的、一致的服务。
ref:
华为.我们提供什么
华为内网网络安全分析
Friday, June 28, 2024
华为公司内部有很多不错的学习资料,自己也总结了很多知识经验,一直想着如何导入到自己的知识库。我清楚的明白这些通用化的知识是不涉密不敏感的,但信息安全警钟长鸣,让人心痒又不敢越雷池一步。经过一些测试,我发现公司的网络安全保护比较难突破。本文将对研发区黄区作一点粗略解析。绿区属于自由区域,默认无重要信息,一般为外围工作人员的网络。红区为超高级别的网络防护,目前尚未有长时间深入接触,简单接触到的红区位于网络设备实验室,存放各种大型交换机框架,是公司内网的枢纽,攻破红区的话就相当于攻破了区域网络,至少一栋楼的网络是可以瘫痪一段时间的。
路由器防火墙方式
加密 :加密使用公钥,什么是公钥,简单理解为钥匙,这把钥匙可以人手一把,但只能上锁,不可以开锁。以上是极为具现化的表达,下边会稍微抽象一点,公钥是一个数字 A,有一条信息 M,用 A 对 M 进行加密操作$$f(A, M)$$,得到的信息无法轻易反向解密,类似对数字求平方和求开方的难度区别,合并同类项和因式分解的难度区别。反向解密会非常困难且耗时,使用超级计算机也需要数年乃至数十年。
解密 :服务端使用私钥揭秘,四面八方汇聚来的已加密信息可以使用同一把私钥解密。
中间人 :中间人角色类似传话筒,对客户端它是服务端,在服务端看来它是一个普通用户。因为传声筒的角色,双方的信息它都一览无余。简单描述的话,华为自身扮演了一个非常强大的中间人,所有外发的网络流量都会经过其扫描,不使用 80/443 端口的流量会全部拦截。
如何破解 :由于黄区只有特定端口可以走代理服务器进出公网,对其它端口默认全封,那么严格来说网络流量就没有漏洞。我们可以手动生成密钥,在内网手动加密,再在外网手动解密,这样至少中间人看到的信息无法真正解析。加密器如何发送至内网,邮件/welink/网页都可以,但都会留下痕迹,其中通过网页直接秘密发送影响最小,痕迹最不明显。或者直接把密钥抄纸上,公司电脑保存起来,完全无法察觉,除了公司内遍布的摄像头。github 上的 ssh 贴心的支持 ssh over 443,经过测试发现也行不通,毕竟代理作为防火墙可以轻易识别这样的高风险网站。根据自身体验,公司的防火墙是基于白名单,而非黑名单,也就是即便是自建 ssh 服务器,也会被代理拦住。在浏览器中访问未知网站会有跳转页面提示“后果自负”,在终端窗口中直接就显示链接被关了。
华为毕竟是搞网络起家,搞网络的能人异士众多,技术上几乎无法突破,恐怕唯有社会工程能突破了。
本地防火墙方式
Windows 系统会安装安全应用,用户无法随意更改配置,配置由管理员统一下发。应用的网络访问权限可能是黑白名单方式,部分应用无法访问网络。vscode 的新版无法走代理通道。
DoS防范
Friday, June 28, 2024
DDoS 防范
DDoS 定义
两种 DoS 攻击方式:
攻击类型
攻击类型
攻击方式
应对方式
Distributed DoS
多台独立 IP 的机器同时开始攻击
1. 降级服务 2. 黑名单 3. 关闭网络设备
Yo-yo attack 悠悠球攻击
对有自动扩展资源能力的服务, 在资源减少的间隙进行攻击
黑名单
Application layer attacks 应用层攻击
针对特定的功能或特性进行攻击,LAND 攻击属于这种类型
黑名单
LANS
这种攻击方式采用了特别构造的 TCP SYN 数据包(通常用于开启一个新的连接),使目标机器开启一个源地址与目标地址均为自身 IP 地址的空连接,持续地自我应答,消耗系统资源直至崩溃。这种攻击方法与 SYN 洪泛攻击并不相同。
黑名单
Advanced persistent DoS 高级持续性 DoS
反侦察/目标明确/逃避反制/长时间攻击/大算力/多线程攻击
降级服务
HTTP slow POST DoS attack 慢 post 攻击
创造合法连接后以极慢的速度发送大量数据, 导致服务器资源耗尽
降级服务
Challenge Collapsar (CC) attack 挑战 Collapsar (CC) 攻击
将标准合法请求频繁发送,该请求会占用较多资源,比如搜索引擎会占用大量的内存
降级服务,内容识别
ICMP flood Internet 控制消息协议 (ICMP) 洪水
大量 ping/错误 ping 包 /Ping of death(malformed ping packet)
降级服务
永久拒绝服务攻击 Permanent denial-of-service attacks
对硬件进行攻击
内容识别
反射攻击 Reflected attack
向第三方发送请求,通过伪造地址,将回复引导至真正受害者
ddos 范畴
Amplification 放大
利用一些服务作为反射器,将流量放大
ddos 范畴
Mirai botnet 僵尸网络
利用被控制的物联网设备
ddos 范畴
SACK Panic 麻袋恐慌
操作最大段大小和选择性确认,导致重传
内容识别
Shrew attack 泼妇攻击
利用 TCP 重传超时机制的弱点,使用短暂的同步流量突发中断同一链路上的 TCP 连接
超时丢弃
慢读攻击 Slow Read attack
和慢 post 类似,发送合法请求,但读取非常慢, 以耗尽连接池,通过为 TCP Receive Window 大小通告一个非常小的数字来实现
超时断连,降级服务,黑名单
SYN flood SYN 洪水
发送大量 TCP/SYN 数据包, 导致服务器产生半开连接
超时机制
泪珠攻击 Teardrop attacks
向目标机器发送带有重叠、超大有效负载的损坏 IP 片段
内容识别
TTL 过期攻击
当由于 TTL 过期而丢弃数据包时,路由器 CPU 必须生成并发送 ICMP 超时响应。生成许多 这样的响应会使路由器的 CPU 过载
丢弃流量
UPnP 攻击
基于 DNS 放大技术,但攻击机制是一个 UPnP 路由器,它将请求从一个外部源转发到另一个源,而忽略 UPnP 行为规则
降级服务
SSDP 反射攻击
许多设备,包括一些住宅路由器,都在 UPnP 软件中存在漏洞,攻击者可以利用该漏洞从端口号 1900 获取对他们选择的目标地址的回复。
降级服务, 封禁端口
ARP 欺骗
将 MAC 地址与另一台计算机或网关(如路由器)的 IP 地址相关联,导致原本用于原始真实 IP 的流量重新路由到攻击者,导致拒绝服务。
ddos 范畴
防范措施
识别攻击流量
对攻击流量进行处理
丢弃攻击流量
封禁攻击 ip
ipv4 ip 数量有限, 容易构造黑名单
ipv6 数量较多, 不容易构造黑名单. 可以使用 ipv6 的地址段, 但有错封禁的风险
控制访问频率
开源工具
攻击工具
防御工具
流量监控