-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathsearch.json
1 lines (1 loc) · 794 KB
/
search.json
1
[{"title":"使用Hexo+Next搭建个人博客","url":"/2022/03/17/093375c998745d381f00dcaac184ef81/","content":"分享一下个人博客搭建,本人是有一个个人的私有仓库,然后呢私有仓库内有些感觉写的可以文章会分享到自己的博客上,但是用Hexo+Next主题吧,发现哇使用起来比较麻烦,还需要copy来copy去的,所以自己写了一些脚本方便使用和操作!主要是为了傻瓜式的使用!同时帮助换个工作电脑可能就跑不起来了,所以写一篇文章记录下!同时本文也会分享一些Hexo的插件,比如支持流程图和UML,以及一些优化,比如使用gulp压缩代码!\n\n\n1. 环境\n环境: Linux or Mac (后期会增加Windows环境,主要是不会写windows的脚本!)\n创建一个账号:GitHub 个人账号,例如我的个人账号是 Anthony-Dong\n创建一个仓库:GitHub仓库,例如我的仓库名是 anthony-dong.github.io,格式就是{个人账号名称}.github.io,注意都要小写,仓库地址 https://github.com/Anthony-Dong/anthony-dong.github.io\n下载我的博客模版到本地,项目地址: https://github.com/Anthony-Dong/blog_template\n\nwget https://github.com/Anthony-Dong/blog_template/archive/refs/heads/master.zipunzip master.zip\n\n\n启动项目\n\n\n配置参数( 如果你本地有docker且已经启动起来了,直接 make init run 起来了,下面可以不看了!)\n\n\n如果你的环境依赖本地都有,只需要把 EXEC_TYPE := docker 改成 EXEC_TYPE := 即可\n\n注意: 如果你用环境依赖指的是 node+hexo环境,需要注意的是你需要安装 node.js 16.3 版本 + hexo 4.3.0 版本,可以参考下面的安装方式!\n\n# 安装 node.js,如果你是mac环境完全可以下面这样安装brew install node@16# 其他环境,需要下载 https://nodejs.org/dist/v16.3.0/ 源码进行安装!# 安装 hexo, 这里配置taobao的源,比较快!npm config set registry https://registry.npm.taobao.orgnpm install hexo-cli@4.3.0 -g# 安装gulp 压缩html/js/cssnpm install gulp-cli -g # cli-version: 2.3.0, local-version: 4.0.2\n\n\n初始化环境: 执行 make init run 即可!\n具体帮助命令\n\n➜ note-master git:(master) ✗ make push push项目到远程 create 创建博客文件的头部信息 init 初始化整个项目[第一次执行会比较慢] build 重新构建网站 run 启动网站 deploy 发布到个人网站 help 帮助\n\n2. 快速修改配置\n修改配置文件hexo-home/_config.yml , 只需要修改我下面标注的!\n\n# Sitetitle: 技术小白 # 网站标题subtitle: '技术小白的技术博客' # 网站介绍keywords: # 网站关键词 - Hexo - Node.js - Flinkdescription: '每天进步一点点!' # 个性签名author: xiao-bai # 作者名称# URL## Set your site url here. For example, if you use GitHub Page, set url as 'https://username.github.io/project'url: https://xiao-bai.github.io/ # 你的博客地址,一般你部署在github的话## Docs: https://hexo.io/docs/one-command-deploymentdeploy: type: git repo: git@github.com:xiao-bai/xiao-bai.github.io.git # 你仓库的地址 branch: master\n\n\n修改配置文件 hexo-home/themes/next/_config.yml , 简单使用只需要替换以下的配置文件,高级使用请看官方文档: https://github.com/iissnan/hexo-theme-next\n\n# 下面的联系地址可以改成你的!你也可以根据配置文件添加social: GitHub: https://github.com/anthony-dong || fab fa-github E-Mail: mailto:fanhaodong516@gmail.com || fa fa-envelope 掘金: https://juejin.cn/user/4248168663101320 || fas fa-book 国内邮箱: mailto:fanhaodong516@163.com || fa fa-envelope # 这个替换成的话术就行了,不需要的话可以 enable: false 关闭reward_settings: # If true, reward will be displayed in every article by default. enable: true animation: false comment: 本人坚持原创技术分享,如果你觉得文章对您有用,请随意打赏! 如果有需要咨询的请发送到我的邮箱! # 你的收款码,不需要的话可以注释掉!reward: wechatpay: /images/wechatpay.png alipay: /images/alipay.png# 这个替换成你的github就行了,不需要的话可以 enable: false 关闭github_banner: enable: true permalink: https://github.com/Anthony-Dong title: Follow me on GitHub\n\n\n修改以下路径的图片,替换成你的图片\n\nhexo-home/themes/next/source/images/alipay.png 你的支付宝收款码hexo-home/themes/next/source/images/wechatpay.png 你的微信收款码hexo-home/themes/next/source/images/avatar.png 你的头像hexo-home/themes/next/source/images/favicon.ico 你网站的icon\n\n\n修改个人简介,可以修改此文件: hexo-home/source/about/index.md 即可!\n如果开启百度统计,只需要修改hexo-home/themes/next/_config.yml此文件即可! appid是hm.src = "https://hm.baidu.com/hm.js?{app_id}"; 最后那个ID!\n\n# Baidu Analyticsbaidu_analytics: # <app_id>\n\n3. 添加评论插件\n首先前往链接: Register a new OAuth application 创建一个 Auth APP , 申请页面如下,可以根据我的注释进行填写\n\n\n\n申请完成后可以在页面上查看 client-id 和 secret-id\n\n\n\n然后修改hexo-home/themes/next/_config.yml添加 gitalk 评论系统\n\n# Gitalk# For more information: https://gitalk.github.io, https://github.com/gitalk/gitalkgitalk: enable: true # 是否开启 github_id: xiao-bai # 你的GitHub用户名 repo: xiao-bai.github.io # 你仓库的地址 client_id: xxxx # 上面申请拿到的 client-id client_secret: xxxxxx # 上面申请拿到的 secret-id admin_user: xiao-bai # 你的GitHub名称,注意大小写 distraction_free_mode: true # 设置为true language: #不用动!\n\n同时注意一下看一下是否存在hexo-home/themes/next/layout/_custom/sidebar.swig以下内容,不存在可以复制一下\n{% if page.comments and config.gitalk.enable %}{% endif %}\n\n\n搞完以后记得重新build下 make build 下,然后 make run 启动就行了\n打开页面会遇到这种情况 Related Issues not found , 这时候你其实已经成功了,make deploy发布就行了!\n\n\n这时候需要你登陆你的GitHub账号,然后他就会自动帮你初始化issues! \n注意: \n\n你每发布一篇文章都需要你打开页面初始化下一Issues!\n你本地http://localhost:4000 访问无法创建Issues的!\n\n\n打开你的仓库的Issues页面就可以看到创建 issues 了!\n\n\n4. 支持mermain(流程图)\n在目录 hexo-home 执行 npm install hexo-filter-mermaid-diagrams --save , 我安装的是1.0.5^版本\n修改配置文件hexo-home/themes/next/_config.yml \n\n# Mermaid tagmermaid: enable: true # Available themes: default | dark | forest | neutral theme: forest\n\n注意更多配置可以参考: mermaid-js 文档 , 需要配合一起修改 hexo-home/themes/next/layout/_third-party/tags/mermaid.swig 文件!\n\n修改css样式,在文件 hexo-home/themes/next/source/css/_colors.styl 末尾添加\n\n.mermaid { background: transparent; text-align: center;}\n\n\n切记一定要关闭 pjax, 需要修改配置文件hexo-home/themes/next/_config.yml,原因是切换页面需要重新渲染流程图,如果开启切换页面的时候不会帮你渲染!!\n\n# Easily enable fast Ajax navigation on your website.# Dependencies: https://github.com/theme-next/theme-next-pjaxpjax: false\n\n5. 支持flowchart (流程图)\n在目录hexo-home 执行 npm install --save hexo-filter-flowchart ,我安装的版本是1.0.4!\n修改配置文件 hexo-home/_config.yml , 默认的sdk版本过低,可能支持不太好,所以需要更换一下!\n\n# flowchart使用 hexo-filter-flowchart https://github.com/bubkoo/hexo-filter-flowchartflowchart: raphael: "https://cdnjs.cloudflare.com/ajax/libs/raphael/2.3.0/raphael.min.js" # optional, the source url of raphael.js flowchart: "https://cdnjs.cloudflare.com/ajax/libs/flowchart/1.17.1/flowchart.min.js" # optional, the source url of flowchart.js options: # options used for `drawSVG` scale: 1 line-width: 2 line-length: 50 text-margin: 10 font-size: 12\n\n\n具体更多配置可以参考文档: hexo-filter-flowchart\n\n6. 压缩HTML、JS、CSS目前hexo生成的代码,全部都是未压缩,目前前端主流的方式都是使用webpack和gulp进行压缩,但是webpack这块本人也未搜索到相关资料,所以用的gulp进行压缩!\n\n安装 gulp,执行 npm install --global gulp-cli\n安装其他插件\n\n\ngulp: 核心框架\ngulp-clean-css: 压缩css代码\ngulp-uglify: 压缩js代码,这个不支持es6\n**gulp-uglify-es**:压缩js代码,支持es6\ngulp-htmlclean:压缩html代码,清除空格和换行符\ngulp-htmlmin:压缩html的js/css代码等,但是支持度不是特别好,比如js代码无法压缩成行!\ngulp-html-minifier-terser: 压缩html的js/css代码,支持压缩js(兼容es6)\n\n\n安装依赖\n\nnpm install gulp gulp-clean-css gulp-uglify-es gulp-html-minifier-terser\n\n\n配置文件\n\nvar gulp = require('gulp')var minifycss = require('gulp-clean-css');const uglify = require('gulp-uglify-es').default;const htmlmin = require('gulp-html-minifier-terser');gulp.task('compress_css', function () { return gulp.src('./public/**/*.css') .pipe(minifycss({ compatibility: 'ie8' })) // 兼容到IE8 .pipe(gulp.dest('./public'));});gulp.task('compress_js', function () { return gulp.src('./public/**/*.js') .pipe(uglify()) .pipe(gulp.dest('./public'))})// 注意报错可以排出目录!防止生成失败!// 可接受参数的文档:https://github.com/terser/html-minifier-terser#options-quick-referencegulp.task('compress_html', function () { return gulp.src(['./public/**/*.html', '!./public/2022/03/27/583186f06a088dc9967a483e3876b2a2/index.html']) .pipe(htmlmin( { removeComments: true, // 移除注释 removeEmptyAttributes: true, // 移除值为空的参数 removeRedundantAttributes: true, // 移除值跟默认值匹配的属性 collapseBooleanAttributes: true, // 省略布尔属性的值 collapseWhitespace: true, // 移除空格和空行 minifyJS: true, // 压缩HTML中的JS minifyCSS: true, // 压缩HTML中的CSS minifyURLs: true, // 压缩HTML中的链接 } )) .pipe(gulp.dest('./public'))})// 默认任务,不带任务名运行gulp时执行的任务gulp.task('default', gulp.parallel( 'compress_css', 'compress_js', 'compress_html'));\n\n\n参考文章\n\nhttps://blog.inkuang.com/2021/405/\n7. 其他BUG修复1. 图片设置 style="zoom: 50%;" 导致图片放大展示失败背景:目前typora其实可以缩放图片,导致使用 medium-zoom 可能出现bug,参考 issues#164 我大概改了一版本!\n\n修改文件: hexo-home/themes/next/source/js/next-boot.js\n\n// 修改此行代码// CONFIG.mediumzoom && window.mediumZoom('.post-body :not(a) > img, .post-body > img');// 为这个CONFIG.mediumzoom && NexT.utils.mediumZoomFunc();\n\n\n修改文件: hexo-home/themes/next/source/js/utils.js , 添加mediumZoomFunc 方法!\n\nmediumZoomFunc: function () { const zoom = mediumZoom('.post-body :not(a) > img, .post-body > img'); // 开启的时候取消掉 style.zoom zoom.on('open', event => { var curentNodeAlt = event.target.alt; document.querySelectorAll('.medium-zoom-image').forEach(elem => { if (elem.style.zoom == '') { return; } if (elem.alt != curentNodeAlt) { return; } elem.alt = elem.alt + '|' + elem.style.zoom elem.style.zoom = '' }); }) // 关闭的时候 重新set style.zoom zoom.on('closed', event => { var curentNodeAlt = event.target.alt; document.querySelectorAll('.medium-zoom-image').forEach(elem => { if (elem.alt != curentNodeAlt) { return; } var elemAltLastIndex = elem.alt.lastIndexOf('|') if (elemAltLastIndex == -1) { return; } elem.style.zoom = elem.alt.substring(elemAltLastIndex + 1) elem.alt = elem.alt.substring(0, elemAltLastIndex) }); })}\n\n8. 配置 Tool 一起使用1. 创建一篇文章 or 标注一篇文章\n执行下面命令make create ,会生成一个页眉,你只需要把这个东西 copy 到你的文章中去!\n\n\n\n找到你的文章,写一些描述信息,例如我这篇文章就是这么写的!\n\n\n2. 发布到网站上\n本地构建一下make run,看看详情信息\n\n➜ note-master git:(master) ✗ make runbin/go-tool hexo --dir ./ --target_dir ./hexo-home/source/_posts2022/03/17 21:53:00.668245 api.go:63: [INFO] [hexo] command load config:....13:53:40.106 DEBUG Processed: layout/_third-party/search/localsearch.swig13:53:40.366 DEBUG Generator: page13:53:40.367 DEBUG Generator: post13:53:40.367 DEBUG Generator: category13:53:40.367 DEBUG Generator: archive13:53:40.367 DEBUG Generator: json13:53:40.368 DEBUG Generator: index13:53:40.368 DEBUG Generator: tag13:53:40.371 DEBUG Generator: asset13:53:40.403 INFO Hexo is running at http://localhost:4000 . Press Ctrl+C to stop.13:53:40.425 DEBUG Database saved13:53:59.402 DEBUG Rendering HTML index: index.html\n\n\n然后访问 http://localhost:4000 即可!看到网页\n\n\n\n最后没问题,执行make deploy 即可发布到远程网站了!\n\nmake deploy\n\n3. 高级功能1. 敏感关键字过滤这个我们都知道,公司会有安全团队扫描开源仓库,假如你涉及到公司敏感字眼也比较恶心,比如把你个人信息暴露了!但是要知道不能发布公司内部的文章上传出去,或者公司内部的代码,这个是任何公司的红线!切记,这个插件主要就是过滤一些公司的名字而已!\n配置文件在: 你只需要列出敏感词即可!在KeyWord地方!\nHexo: KeyWord: - "敏感词" - "敏感词2" Ignore: - hexo-home\n\n2. 图片上传目前我使用的是我自己写的工具上传图片,主要是用的阿里云的OSS,基本上一年花个不到几块钱就可以搞定!\n具体可以参考 Upload 插件\n你在你本地的 .config/.go-tool.yaml 文件,配置一下配置即可!\nUpload: Bucket: default: AccessKeyId: xxxx AccessKeySecret: ihDP2HkiTQGYwMY1udCtq8cBQNKP5N Endpoint: oss-accelerate.aliyuncs.com UrlEndpoint: xxx.oss-accelerate.aliyuncs.com Bucket: xxxx PathPrefix: image pdf: AccessKeyId: xxxxx AccessKeySecret: xxxxx Endpoint: oss-accelerate.aliyuncs.com UrlEndpoint: xxxx.oss-accelerate.aliyuncs.com Bucket: xxxx PathPrefix: pdf\n\n然后Typora配置下: \n\n3. 修改个人主页修改本地文件hexo-home/source/about/index.md 即可\n","categories":["工具"],"tags":["工具","Hexo","Next","Mermaid","Gulp"]},{"title":"Docker资源限制和如何监控","url":"/2021/01/28/1041c14319d758f789b79049f9abedad/","content":" 容器比较强大的地方就是使用方便,强大的隔离性,但是生产上往往需要做到保护,比如a容器不能对于b容器造成任何影响,比如a容器资源占用太高,导致b容器无法响应,获取a容器down机影响宿主机或其他容器等等,都是不允许的,所以容器隔离技术就解决了这些问题!\n\n\n1、cgroup简介cgroup是Control Groups的缩写,是Linux 内核提供的一种可以限制、记录、隔离进程组所使用的物理资源(如 cpu、memory、磁盘IO等等) 的机制,被LXC、docker等很多项目用于实现进程资源控制。cgroup将任意进程进行分组化管理的 Linux 内核功能。cgroup本身是提供将进程进行分组化管理的功能和接口的基础结构,I/O 或内存的分配控制等具体的资源管理功能是通过这个功能来实现的。这些具体的资源管理功能称为cgroup子系统,有以下几大子系统实现:\n\nblkio:设置限制每个块设备的输入输出控制。例如:磁盘,光盘以及usb等等。\ncpu:使用调度程序为cgroup任务提供cpu的访问。\ncpuacct:产生cgroup任务的cpu资源报告。\ncpuset:如果是多核心的cpu,这个子系统会为cgroup任务分配单独的cpu和内存。\ndevices:允许或拒绝cgroup任务对设备的访问。\nfreezer:暂停和恢复cgroup任务。\nmemory:设置每个cgroup的内存限制以及产生内存资源报告。\nnet_cls:标记每个网络包以供cgroup方便使用。\nns:命名空间子系统。\nperf_event:增加了对每group的监测跟踪的能力,即可以监测属于某个特定的group的所有线程以及运行在特定CPU上的线程。\n\n目前docker只是用了其中一部分子系统,实现对资源配额和使用的控制。\n2、docker如何限制的使用的是stress镜像压测 :https://hub.docker.com/r/jfusterm/stress\ndocker pull jfusterm/stress\n\n1、内存限制\n\n\n选项\n描述\n\n\n\n-m,--memory\n内存限制,格式是数字加单位,单位可以为 b,k,m,g。最小为 4M\n\n\n--memory-swap\n内存+交换分区大小总限制。格式同上。必须必-m设置的大\n\n\n--memory-reservation\n内存的软性限制。格式同上\n\n\n--oom-kill-disable\n是否阻止 OOM killer 杀死容器,默认没设置\n\n\n--oom-score-adj\n容器被 OOM killer 杀死的优先级,范围是[-1000, 1000],默认为 0\n\n\n--memory-swappiness\n用于设置容器的虚拟内存控制行为。值为 0~100 之间的整数\n\n\n--kernel-memory\n核心内存限制。格式同上,最小为 4M\n\n\n为了压测我们选择一个容器\n1、只设置 -m 参数,可以发现当限制内存在 128m时,我们还是可以分配128m的,所以-m并不是全部的限制\n➜ ~ docker run -it --rm -m 128m jfusterm/stress --vm 1 --vm-bytes 128m -t 5sstress: info: [1] dispatching hogs: 0 cpu, 0 io, 1 vm, 0 hddstress: info: [1] successful run completed in 5s\n\n当我们继续增加到原来的两倍,发现运行失败\n➜ ~ docker run -it --rm -m 128m jfusterm/stress --vm 1 --vm-bytes 256m -t 5sstress: info: [1] dispatching hogs: 0 cpu, 0 io, 1 vm, 0 hddstress: FAIL: [1] (415) <-- worker 6 got signal 9stress: WARN: [1] (417) now reaping child worker processesstress: FAIL: [1] (421) kill error: No such processstress: FAIL: [1] (451) failed run completed in 1s\n\n当我们设置为 250m,发现运行成功\n➜ ~ docker run -it --rm -m 128m jfusterm/stress --vm 1 --vm-bytes 250m -t 5sstress: info: [1] dispatching hogs: 0 cpu, 0 io, 1 vm, 0 hddstress: info: [1] successful run completed in 5s\n\n2、设置--memory 和--memory-swap 参数\n设置 128 & 128 ,分配 127时OK的,\n➜ ~ docker run -it --rm --memory 128m --memory-swap 128m jfusterm/stress --vm 1 --vm-bytes 127m -t 5sstress: info: [1] dispatching hogs: 0 cpu, 0 io, 1 vm, 0 hddstress: info: [1] successful run completed in 5s\n\n设置128 & 128 ,分配128 失败\n➜ ~ docker run -it --rm --memory 128m --memory-swap 128m jfusterm/stress --vm 1 --vm-bytes 128m -t 5sstress: info: [1] dispatching hogs: 0 cpu, 0 io, 1 vm, 0 hddstress: FAIL: [1] (415) <-- worker 7 got signal 9stress: WARN: [1] (417) now reaping child worker processesstress: FAIL: [1] (421) kill error: No such processstress: FAIL: [1] (451) failed run completed in 1s\n\n2、如何查看容器真实内存[root@centos-linux ~]# docker run --rm -it -m 128m alpine /bin/sh/ # free -m total used free shared buff/cache availableMem: 1980 266 1143 21 569 1559Swap: 0 0 0\n\n经常遇到这种问题,容器内的内存和真实内存其实不一致的,原因是啥? \n这是由于docker产生的容器的隔离性不足造成的。docker创建容器时,会为容器提供一些必要的目录和文件,比如/proc下的若干文件。其中/proc/meminfo文件docker并没有直接提供其生成,而是将宿主机的/proc/meminfo挂载给了容器。因此容器看到的/proc/meminfo与宿主机的/proc/meminfo的内容是一样的。而free命令也不过是查看该文件的信息而已。如果想增强其隔离性,可以使用lxcfs的方式。\n那如何解决了?\ndocker其实本身使用 cgroup进行隔离,其实它监控也只能监控cgroup,本机是 CentOS Linux release 7.9.2009 (Core)\n[root@centos-linux ~]# ls /sys/fs/cgroup/memory/docker/9fc278a2cab2bde9f2aa10fb8eb58732f4d1f3ce0f988ed64cac4b4616a585f1/cgroup.clone_children memory.kmem.tcp.max_usage_in_bytes memory.oom_controlcgroup.event_control memory.kmem.tcp.usage_in_bytes memory.pressure_levelcgroup.procs memory.kmem.usage_in_bytes memory.soft_limit_in_bytesmemory.failcnt memory.limit_in_bytes memory.statmemory.force_empty memory.max_usage_in_bytes memory.swappinessmemory.kmem.failcnt memory.memsw.failcnt memory.usage_in_bytesmemory.kmem.limit_in_bytes memory.memsw.limit_in_bytes memory.use_hierarchymemory.kmem.max_usage_in_bytes memory.memsw.max_usage_in_bytes notify_on_releasememory.kmem.slabinfo memory.memsw.usage_in_bytes tasksmemory.kmem.tcp.failcnt memory.move_charge_at_immigratememory.kmem.tcp.limit_in_bytes memory.numa_stat\n\n\n\n\n文件名称\n含义\n\n\n\nmemory.usage_in_bytes\n已使用的内存量(包含cache和buffer)(字节),相当于linux的used_meme\n\n\nmemory.limit_in_bytes\n限制的内存总量(字节),相当于linux的total_mem\n\n\nmemory.failcnt\n申请内存失败次数计数\n\n\nmemory.memsw.usage_in_bytes\n已使用的内存和swap(字节)\n\n\nmemory.memsw.limit_in_bytes\n限制的内存和swap容量(字节)\n\n\nmemory.memsw.failcnt\n申请内存和swap失败次数计数\n\n\nmemory.stat\n内存相关状态\n\n\n查看\n[root@centos-linux ~]# docker stats 9fc278a2cab2bde9f2aa10fb8eb58732f4d1f3ce0f988ed64cac4b4616a585f1 --no-streamCONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS9fc278a2cab2 elated_panini 0.00% 560KiB / 128MiB 0.43% 1.01kB / 0B 0B / 0B 1[root@centos-linux 9fc278a2cab2bde9f2aa10fb8eb58732f4d1f3ce0f988ed64cac4b4616a585f1]# cat memory.usage_in_bytes573440\n\n所以可以看到俩数据基本一致,具体逻辑:https://github.com/opencontainers/runc/blob/v0.1.1/libcontainer/cgroups/fs/memory.go#L148\n参考:Docker容器内存监控\n","categories":["云原生"],"tags":["Docker","监控"]},{"title":"聊一聊HTTP协议","url":"/2022/05/19/14ae618744ce995daa153156bee2d2e6/","content":" 如今互联网已经与我们密不可分了,购物、金融、社交、娱乐等都依赖于互联网,其中主要依赖的几项技术就包含HTTP(HyperText Transfer Protocol, 超文本传输协议)。本文核心就是介绍HTTP发展历史以及HTTP/2协议!\n\n\nHTTP发展历史\nHTTP/0.9HTTP协议的第一个规范是1991年发布的0.9版本,此规范文档不足700个单词,其中规范中指出了通过TCP/IP (或类似的面向连接的服务)与服务器和端口建立连接!规范申明错误类型以可读文本显示HTML语法,以及请求是幂等的!\n\n目前已经没有网站和浏览器支持 http/0.9 协议了!\n\n请求格式:只有GET方法,报文如下:\nGET /doc/index.heml\n\n响应内容:只有HTML文本,所以无法传输其他类型文件!\n<HTML>这是一个非常简单的HTML页面</HTML>\n\nHTTP/1.0在1991-1995年,人们由于不满足HTTP/0.9,于是搞了一些新扩展,但是这些新拓展并没有被引入到标准中。直到1996年11月,为了解决这些问题,一份新文档(RFC 1945)被发表出来,用以描述如何操作实践这些新扩展功能。\n主要新增内容如下:\n\n引入了 POST、HEAD方法\n请求与响应都引入了 HTTP 头/首部(Header),为此也支持了其他传输类型(图片等)\n引入响应状态码\n\n➜ ~ curl --http1.0 www.baidu.com -v* Trying 220.181.38.149:80...* Connected to www.baidu.com (220.181.38.149) port 80 (#0)> GET / HTTP/1.0> Host: www.baidu.com> User-Agent: curl/7.77.0> Accept: */*>* Mark bundle as not supporting multiuse* HTTP 1.0, assume close after body< HTTP/1.0 200 OK< Accept-Ranges: bytes< Cache-Control: private, no-cache, no-store, proxy-revalidate, no-transform< Content-Length: 2381< Content-Type: text/html# .... 省略\n\nHTTP/1.1HTTP/1.0 多种不同的实现方式在实际运用中显得有些混乱,自1995年开始,即HTTP/1.0文档发布的下一年,就开始修订HTTP的第一个标准化版本。于是在1997年初,HTTP1.1 标准发布。\n主要新增内容如下:(下列内容部分也在HTTP/1.0中有支持,所以和HTTP/1.1统称为HTTP/1.X)\n\n连接复用 (引入 Keep-Alive Header)\n增加管线化(管道化)技术,允许在第一个应答被完全发送之前就发送第二个请求,以降低通信延迟。\n支持响应分块(chunked编码)\n引入额外的缓存控制机制\n引入内容协商机制,包括语言,编码,类型等,并允许客户端和服务器之间约定以最合适的内容进行交换\n凭借Host头,能够使不同域名配置在同一个IP地址的服务器上,并且强制要求客户端请求必须携带Host。例如Nginx这种反向代理工具,可能根据Host进行反向代理,也就是一台服务器承载多个域名!\n支持包含GET、POST、PUT、PATCH、DELETE、HEAD、CONNECT、OPTIONS、TRACE 请求方法\n\n➜ ~ curl --http1.1 www.baidu.com -v* Trying 220.181.38.149:80...* Connected to www.baidu.com (220.181.38.149) port 80 (#0)> GET / HTTP/1.1> Host: www.baidu.com> User-Agent: curl/7.77.0> Accept: */*>* Mark bundle as not supporting multiuse< HTTP/1.1 200 OK< Accept-Ranges: bytes< Cache-Control: private, no-cache, no-store, proxy-revalidate, no-transform< Connection: keep-alive< Content-Length: 2381< Content-Type: text/html< Date: Mon, 23 May 2022 09:57:28 GMT< Etag: "588604c8-94d"< Last-Modified: Mon, 23 Jan 2017 13:27:36 GMT< Pragma: no-cache< Server: bfe/1.0.8.18< Set-Cookie: BDORZ=27315; max-age=86400; domain=.baidu.com; path=/\n\n由于HTTP协议的可扩展性 – 创建新的头部和方法是很容易的 – 即使 HTTP/1.1 协议进行过两次修订,RFC 2616 发布于 1999 年 6 月,而另外两个文档 RFC 7230-RFC 7235 发布于 2014 年 6 月(在 HTTP/2 发布之前)。HTTP/1.1 协议已经稳定使用超过 15 年了。\n1. HTTP/1.1 修订记录HTTP/1.1 协议发布于1997年的1月份,后面经过三次修订!最后一次修订时间是2014年6月份!具体修订内容可以点击下面链接进行查看!HTTP1.1的岁数和我差不多!\n\n首发 1997年 rfc2068\n第一次修订 1999年 rfc2616\n第二次修订 2014年 rfc7230 (想要详细了解HTTP协议编码可以看这个)\n第三次修订 2014年 rfc7235\n\n2. 服务端优化手段1. keep-alive引入keep-alive 可以有效的降低了TCP建立连接的开销(包含TCP握手和TCP慢启动,其实TCP一开始窗口挺大的,默认14KB),其次就是有效的降低了系统开销,如TCP连接数等!\n在HTTP/1.0中默认是关闭开启 keep-alive 的,必须在请求头部添加Connection: keep-alive才可以;但是在HTTP/1.1中默认是开启keep-alive,需要在响应头加入Connection: close才可以关闭!下图是两者的差异:\n\n但是也会存在很多问题,原来是请求->响应直接关闭连接很容易拆包,但是出现了连接复用,那么如何拆包呢?HTTP/1.x中如果我们开启了keep-alive必须在请求头和响应头中加入content-length来标识请求体/响应体的大小!(chunked传输编码除外)\n\n 测试\n\n# HTTP/1.1,下图为HTTP/1.1的抓包图curl --url http://localhost:8888/api/test/req1 --url http://localhost:8888/api/test/req2 --data-raw 'hello world' -v# HTTP/1.0curl --url http://localhost:8888/api/test/req1 --url http://localhost:8888/api/test/req2 --data-raw 'hello world' -H 'Connection: keep-alive' --http1.0 -v\n\n\n\n关于如何实现HTTP的 keep-alive,一般server端设置读超时即可,client端维护就比较麻烦,可以阅读一些源码看一下即可,其次就是用的连接池!\n目前字节内部的TLB做代理的时候维护单个请求连接的时间过长,导致连接不会被销毁,也就是经常会出现后端服务(up stream)出现大量的 TCP ESTABLISHED ,其主要原因也是因为client侧不断开连接,server侧一般也不会直接断开,这也就是为什么后面HTTP/2做了自己的keep-alive机制了!\nkeep-alive 另外还是额外的两个配置,一个是 timeout 一个是max,例如Keep-Alive: timeout=5, max=1000 , 具体可以看: https://tools.ietf.org/id/draft-thomson-hybi-http-timeout-01.html !可以参考Nginx的实现!\n如果响应头里申明 Connection=close ,则会关闭连接!\n\n2. 请求体压缩一般压缩是根据 Content-Encoding 进行区分Body体使用哪种压缩方式,请求的时候会携带Accept-Encoding来标识支持的压缩方式! 常见就是 gzip、tr、deflate 压缩,字节内部也有一些压缩率比较高的压缩算法,比如 ttzip 基于zstd优化后的!\nGET /api/test HTTP/1.1Host: localhost:8888Connection: keep-aliveAccept: */*Accept-Encoding: gzip, deflate, brHTTP/1.1 200 OKContent-Encoding: gzipDate: Wed, 11 May 2022 14:39:27 GMTContent-Length: 343...............!...Y..@.....[Z...........b......zP.1}.JjFKw.......g....oO.Z......./.\tOXwJ..S..%.......7...F3.pF.|B.*....E.-o..ObpT2..6J..5.S....0.u.R.'Ce....,.......>C..(|.8D=.:_...X..$.=....l..1..m.P..s.%.....s....)T.wiyN9_^p.|la@.D/.t.@.>...._/$..=Sz}k.O.......Al..m...j......M..,. ..:g...:.6Q.......T..c..z..jU...u...jWn.J..N.?............\n\n3. Chunked 编码(分块传输)分块编码是HTTP/1.1新增的传输报文格式,响应会携带Transfer-Encoding: chunked,其次响应体是不会有content-length的,因为chunked编码长度部分记录在一个 16进制编码单独的文本行上,然后再写响应内容,如果仍然有内容继续重复操作即可!终止符是 0\\r\\n\\r\\n!下图是chunked编码的抓包图,其中12其实表示0x12也就是为十进制的18!\n\n使用场景: 其中分块编码可以节约用户首屏加载时间,先加载一部分渲染!实际业务中可以做一些 实时进度条展示、日志展示、流式拉取、大文件传输等!比如抖音的首屏预测方案就是基于chunked编码实现的,具体可以看文章尾部的参考文章!\n注意1: 假如你使用Go语言默认HTTP Server,当响应体大于2KB的时候就会默认升级为 chunked 编码!你也可以手动添加响应header Transfer-Encoding: chunked!\n注意2:对于L7 Proxy来说,chunked编码处理不得当很可能造成业务的OOM,所以可以参考一些chunked包转发和抓取的优化手段,尽可能的降低内存拷贝次数和维护的buffer大小!\n上面第二、第三节讲完了,可以发现会存在两个Header, Content-Encoding 和 Transfer-Encoding, 这里可以根据句面意思,可以知道 Content-Encoding 是表示内容编码格式,Transfer-Encoding 表示传输格式! 两者是可以同时使用的!\n3. 前端(浏览器)优化手段1. 合并资源合并资源这个我理解大部分都或多多少的了解过,因为请求3个资源的耗时一般都会大于合并成一个资源的请求耗时!例如前端也有很多静态资源(js/css)的打包工具,比如webpack、gulp 等资源合并工具!其次一些小的Image可以使用精灵图!\n这个东西有利也有弊端,比如我原来20个文件,合并成了一个,我改动一个,那么全部资源都是需要重新reload,浏览器缓存的效果就丢失了,不过这个目前也有解决方案!\n\n2. 域名分片浏览器其实有同域名请求的最大并发数限制,例如主流的浏览器Chrome 其实会对于同域名下最大并发数限制在6个!\n\n域名分片就是讲一个域名划分为多个,比如 www.google.com, www.gstatic.com, 这样就可以很好的解决浏览器的限制!\n3. 管道化下图是使用管道化和未使用的差别,可以发现他可以解决http/1.x 请求阻塞,但是并不能解决响应阻塞的问题!这个主要还是由于服务端HTTP请求处理是串行的!注意只有支持幂等的请求且开启keep-alive才会支持管道化!其次这个技术主要在浏览器侧实现!但是由于keep-alive持久连接实现上并不可靠,所以管道化也需要支持重试等,所以这也就是为什么只允许幂等请求了!\n\n4. 长连接 这个放在这里,感觉也不合理,主要是目前也存在这种场景,比如在线编辑、在线聊天等一些在线软件对于实时性要求比较高,轮训的话也不太好,因为HTTP建连开销比较大,其次对于服务器压力也比较大(比如无效的请求,类似于空转),所以在这条路上诞生了很多的技术,这里我们这里就简单介绍下WebSocket!\n WebSocket是HTML5开始提供的一种在单个TCP连接上进行全双工通讯的协议,位于OSI模型的应用层。WebSocket协议在2008年诞生,于2011年由IETF标准化为 RFC 6455 ,后由 RFC 7936 补充规范。\n注意:WebSocket 和 HTTP同为应用层协议!WebSocket是利用HTTP协议进握手的!\n\n4. 注意点1. 网关方向上诉我们讲的可能是偏向于业务方面一些技巧,对于网关/LB来说,往往在HTTP链路中担任着一个代理角色,那么代理就会涉及到一个问题,我要代理哪些东西,哪些东西不可代理!\n\nHop-by-hop headers (点到点)\n\n具体可以见 https://www.rfc-editor.org/rfc/rfc7230.txt, 大概描述了逐级跳的时候一个 header 是不能传递的!\nConnectionProxy-ConnectionKeep-AliveProxy-AuthenticateProxy-AuthorizationTeTrailerTransfer-EncodingUpgrade\n\n上诉这些header 主要是和协议有关,其中不少网关直接不支持上诉功能代理,\n\n比如 Transfer-Encoding=chunked,很多业务使用chunked编码进行分段传输,来实现单向长链接,但是可能经过网关后会remove掉这个传输协议,因此使用 chunked很可能在网络传输某个节点丢失了frc-4.1.1 . 所以很多业务会自定义 chunked编码,向字节内部就开发了一个Next-Chunked编码解决传输过程中 chunked 分段编码丢失问题.(本质上就是在payload中进行分段)\nConnection、Keep-Alive 等都是一些连接复用优化策略,本质上上下游并不可信,对于连接. \n其次还有一些Header,比如Upgrade用作协议切换,也是不可以进行透传的。\n\n\nEnd-to-End (端到端)\n\n就是很常见的业务header\n\n正向代理\n\n主要是代理客户端(Client)请求,例如VPN、抓包工具、匿名访问、提高访问速度 等\n\n是”代理服务器”代理了”客户端”,去和”目标服务器”进行交互,目标服务器是不知道真正的客户端是谁的\n\n\n反向代理\n\n主要是代理服务端(Server)端请求,例如LB\n\n是”代理服务器”代理了”目标服务器”,去和”客户端”进行交互,客户端是不知道真正的目标服务器是谁的\n\n参考: https://oxylabs.cn/blog/reverse-proxy-vs-forward-proxy \n2. form-data 编码 (body篡改)这里我申明一个名词(透明代理),在实际业务中往往会存在一个问题,就是客户端+服务端传输的时候会进行一个 body/header加密+checksum来防止被篡改(checksum往往会使用加密防止中途被篡改),这种case非常场景.\n那么这里就有一个问题了,form-data 在很多应用中会很容易出现丢失header信息和乱序.具体关于 form-data编码可以看 rfc2056\n所以透明代理往往是不会修改整体的http请求报文,例如正向代理中的抓包,就充当了一个透明代理,它并不会修改你的流量,其实就是不会修改应用层流量(这里主要指的是代理抓包,仅作为4层代理,其实也不是病不能称为4层)\nPOST /api/v1/test/codec?ce=gzip&te=chunked HTTP/1.1Host: xxx-xx.xxx.netUser-Agent: curl/7.77.0Accept: */*Content-Length: 413Content-Type: multipart/form-data; boundary=------------------------8ca4ceae80dc1105--------------------------8ca4ceae80dc1105Content-Disposition: form-data; name="file"; filename="test2.log"Content-Type: application/octet-streamhello world1hello world2--------------------------8ca4ceae80dc1105Content-Disposition: form-data; name="k1"v1--------------------------8ca4ceae80dc1105Content-Disposition: form-data; name="k2"v2--------------------------8ca4ceae80dc1105--\n\n乱序后 可能会出现: \nPOST /api/v1/test/codec?ce=gzip&te=chunked HTTP/1.1User-Agent: curl/7.77.0Host: xxx-xx.xxx.netContent-Type: multipart/form-data; boundary=------------------------8ca4ceae80dc1105Content-Length: 413Accept: */*--------------------------8ca4ceae80dc1105Content-Disposition: form-data; name="k1"v1--------------------------8ca4ceae80dc1105Content-Disposition: form-data; name="k2"v2--------------------------8ca4ceae80dc1105Content-Disposition: form-data; name="file"; filename="test2.log"Content-Type: application/octet-streamhello world1hello world2--------------------------8ca4ceae80dc1105--\n\n3. 流式传输\n有些应用使用 长轮训 ,代理层会读超时\n有些应用使用 分段传输,代理层会读取全部数据后再传输 (为了安全限制读取的最大size,除非支持转发chunked编码)\n\n4. 总结根据上诉描述,所以我们发现做一款优秀的HTTP代理工具,并不是简简单单的一件事情,需要很深入的了解HTTP协议的细节,以及尽可能的满足业务需求,所以优秀的HTTP代理工具,比如apache,nginx等都是不断的迭代和满足业务现状!所以现在一般都是 通用型代理工具 + 偏向于特定业务场景的代理工具!\nHTTPS这个并不是HTTP协议,只是在HTTP发展路上遇到的问题,随着互联网发展,越来越多的人注意到数据安全,比如用户敏感信息需要加密,一些恶意软件的攻击,以及恶意拦截等!所以HTTPS就诞生了!这里我们只是引入而已,不会深入讲解!HTTPS(Hypertext Transfer Protocol Secure) 是 HTTP 协议外面包裹了一层 TLS/SSL!\nSSL(Secure Sockets Layer,安全套接字层),它是由网景公司(Netscape)设计的主要用于Web的安全传输协议,目的是为网络通信提供机密性、认证性及数据完整性保障。如今,SSL已经成为互联网保密通信的工业标准。SSL最初的几个版本(SSL 1.0、SSL2.0、SSL 3.0)由网景公司设计和维护,从3.1版本开始,SSL协议由因特网工程任务小组(IETF)正式接管,并更名为TLS(Transport Layer Security),发展至今已有TLS 1.0、TLS1.1、TLS1.2、TLS1.3 这几个版本。\n如TLS名字所说,SSL/TLS协议仅保障传输层安全。同时,由于协议自身特性(数字证书机制),SSL/TLS不能被用于保护多跳(multi-hop)端到端通信,而只能保护点到点通信。\nSSL/TLS发展历程如下图:\n\n\n\n\n协议\n使用情况\n\n\n\nSSLv3.0以下\n有安全问题,且已被废弃,不建议使用\n\n\nTLSv1.0/v1.1\n过渡版本,不建议使用\n\n\nTLSv1.2\n目前绝大多数都在使用\n\n\nTLSv1.3\n最新的更快更安全的协议(变更最大的一次协议,可以实现1RTT)\n\n\n其次TLS加解密会非常的消耗CPU,其次加密也会额外消耗带宽,不过主要开销都在TLS握手这块,因为现在主流CPU都会对常见加密算法做硬件加速,所以传输过程中数据包这块开销不大!目前一般内网都会卸载掉TLS!其次TLS握手过程也会很长,主要包含证书认证和确认加密算法!其中TLS发展过程中也经历了不断的迭代,发展方向主要是为了更加安全、效率更高!下图是现在主流TLSv1.2的握手流程,相比于裸TCP,会多两倍的时间开销!\n\n注: TLS握手中很多流程是可选的,上图并不完全正确,其次就是还会分为全完握手和简化握手!\n对于TLS来说也是一个协议规范,具体我们在使用的过程中需要在应用程序中设置或者需要程序来支持!常见开源实现主要有 OpenSSL(主流)、JSSE(Java版实现)、NSS(浏览器中广泛使用),下图是些常见软件以及它们所使用的SSL/TLS开源实现的情况!对于安全(TLS/SSL)来说也是不断在进步中,同时也有漏洞不断挖出!\n\nHTTP/2 & HTTP/3随着近20年来互联网的高速发展,页面愈加复杂,有些甚至演变成了独立的应用,一个页面会加载大量的资源,增进交互的脚本大小也增加了许多,更多的数据通过HTTP请求被传输。HTTP/1.1链接需要请求以正确的顺序发送,理论上可以用一些并行的链接,带来的成本和复杂性堪忧。比如,HTTP管线化(pipelining)就成为了Web开发的负担。\n在2009年到2015年,谷歌通过实践了一个实验性的SPDY协议(2011年Google的全部服务就添加了SPDY),证明了一个在客户端和服务器端交换数据的另类方式。其收集了浏览器和服务器端的开发者的焦点问题。明确了响应数量的增加和解决复杂的数据传输,SPDY成为了HTTP/2协议的基础。在2015年5月HTTP/2正式标准化后,取得了极大的成功!目前来看SPDY已经完全被HTTP/2所取代!然后Google其实早起已经对于TCP做了优化,叫做QUIC,所以就考虑把HTTP运行在QUIC上,于是诞生了后面的HTTP/3!\nHTTP/2 相比HTTP/1.1 比较最大的特点就是多路复用,有点类似于我们web server由HTTP服务升级为RPC服务,带来的提升!从浏览器视角看的话,下图是169张图渲染一张图浏览器加载的耗时,结果是HTTP/2 为1.53s, HTTP1.1为 2.47s!\n\nHTTP/2 介绍主要特性\n\n二进制协议 (frame帧)\n多路复用 (stream 流)\n流量控制\n数据流优先级\n首部压缩(HPACK)\n服务端推送 (Push)\n\n虽然有这么多特性,对于HTTP的语义来说,其实并没有太大的变更!\n理解流对于HTTP/1.x协议来说一个连接上的请求、响应是串行的处理,但是HTTP/2引入流的概念,把请求、响应的过程抽象成流,一个连接上可以有多个流,把数据包抽象成帧,帧在建立的连接上传输!\n一个流的生命周期只是一次请求、响应,不存在复用(即另外一个请求不能复用之前的流,关闭即销毁),具体可以看流的生成周期!\n\n流的生命周期\n下图是一个状态机,对于一个普通的请求、响应来说,一般经历有四个阶段\n\n\n空闲(idle): 这里可以理解为初始化一个stream后的状态,这个状态可以理解为很短!\n开启(open) : 当客户端开始写header的时候(包含服务端读header),会流转到这个状态! 这里也就是\n半关闭状态(half closed): 客户端写完数据,发送END_STREAM flags时(写完请求时)就会变成这个状态,此时客户端的流不能再写数据!对于服务端而言就是收到客户端发送的END_STREAM flags,就处于此状态!\n关闭(closed): 对于客户端而言,收到服务端响应发来的END_STREAM 会变为此状态;对于服务器而言发送END_STREAM 也会变为此状态!\n\n\n只要客户端、服务端发送或者接收到 RST_STREAM 帧时就会直接变为 closed 状态!\n对于服务端推送来说我们后文会讲到!\nGRPC双向流(stream msg)实现原理其实也就是一个普通的请求、响应模型,根据上面四个阶段也能大概了解它是如何实现的!即客户端发完请求并没有直接发送 END_STREAM,而是一直处于open状态,且服务端也是!只有客户端/服务端主动关闭才可以关闭stream,或者双方通过自定义协议约定才可以!\n\n +--------+ send PP | | recv PP ,--------| idle |--------. / | | \\ v +--------+ v +----------+ | +----------+ | | | send H / | |,------| reserved | | recv H | reserved |------.| | (local) | | | (remote) | || +----------+ v +----------+ || | +--------+ | || | recv ES | | send ES | || send H | ,-------| open |-------. | recv H || | / | | \\ | || v v +--------+ v v || +----------+ | +----------+ || | half | | | half | || | closed | | send R / | closed | || | (remote) | | recv R | (local) | || +----------+ | +----------+ || | | | || | send ES / | recv ES / | || | send R / v send R / | || | recv R +--------+ recv R | || send R / `----------->| |<-----------' send R / || recv R | closed | recv R |`----------------------->| |<----------------------' +--------+ send: endpoint sends this frame recv: endpoint receives this frame H: HEADERS frame (with implied CONTINUATIONs) PP: PUSH_PROMISE frame (with implied CONTINUATIONs) ES: END_STREAM flag R: RST_STREAM frame\n\n如何建立HTTP/2连接首先建立连接需要客户端和服务端都支持HTTP/2协议,才可以使用HTTP/2!\n使用HTTPS协商 (h2)例如下面这个curl 请求其实是模拟了一个使用HTTPS发送HTTP/2 协议的请求\n➜ ~ curl --http2 https://www.toutiao.com/ -v -o s/dev/null* Trying 240e:b1:9801:407:3::3f6:443...* Connected to www.toutiao.com (240e:b1:9801:407:3::3f6) port 443 (#0)* ALPN, offering h2* ALPN, offering http/1.1* successfully set certificate verify locations:* CAfile: /etc/ssl/cert.pem* CApath: none* TLSv1.2 (OUT), TLS handshake, Client hello (1):* TLSv1.2 (IN), TLS handshake, Server hello (2):* TLSv1.2 (IN), TLS handshake, Certificate (11):* TLSv1.2 (IN), TLS handshake, Server key exchange (12):* TLSv1.2 (IN), TLS handshake, Server finished (14):* TLSv1.2 (OUT), TLS handshake, Client key exchange (16):* TLSv1.2 (OUT), TLS change cipher, Change cipher spec (1):* TLSv1.2 (OUT), TLS handshake, Finished (20):* TLSv1.2 (IN), TLS change cipher, Change cipher spec (1):* TLSv1.2 (IN), TLS handshake, Finished (20):* SSL connection using TLSv1.2 / ECDHE-RSA-AES128-GCM-SHA256* ALPN, server accepted to use h2* Server certificate:* subject: CN=*.toutiao.com* start date: Jul 23 00:00:00 2021 GMT* expire date: Aug 23 23:59:59 2022 GMT* subjectAltName: host "www.toutiao.com" matched cert's "*.toutiao.com"* issuer: C=US; O=DigiCert Inc; CN=RapidSSL TLS DV RSA Mixed SHA256 2020 CA-1* SSL certificate verify ok.* Using HTTP2, server supports multi-use* Connection state changed (HTTP/2 confirmed)* Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0* Using Stream ID: 1 (easy handle 0x7f7d21011e00)> GET / HTTP/2> Host: www.toutiao.com> user-agent: curl/7.77.0> accept: */*>* Connection state changed (MAX_CONCURRENT_STREAMS == 128)!< HTTP/2 200< server: Tengine< content-type: text/html< content-length: 72772< date: Sun, 15 May 2022 10:30:06 GMT.... \n\n上面我们只需要核心关注\n## ALPN客户端支持列表* ALPN, offering h2* ALPN, offering http/1.1## 服务端选择* ALPN, server accepted to use h2\n\n\n\n这里其实利用了TLS的 ALPN (Application-Layer Protocol Negotiation 应用层协议协商) 扩展字段以及识别 HTTP/2 over TLS !其中HTTP/2 就是 ALPN 最佳实践!\n其中原理就是ALNP给Client hello 和 Server hello 消息加了个拓展功能,客户端可以来申明我支持的应用层协议(嗨,我支持h2和http/1,你用哪个都行),服务端可以用它来确认在HTTPS协商后的应用层协议(好吧,我们用h2吧)!\n更多ALPN可以参考文章: https://datatracker.ietf.org/doc/html/rfc7301#section-3 !\n另外其实在 ALPN 协议之前,有一个NPN(Next Protocol Negotiation 下一代协议协商)其实作用和ALPN一样,但是呢过程不一样,其中我这里直接拍照吧,就是它分为三步,选择权利在客户端,其中选择的协议是加密的!所以这么一看其实NPN更加安全可靠!但是想一想增加这几步有必要吗?其实没有必要,所以ALPN成为了规范!\n\n本图片引用自 <HTTP/2 in Action>, 有兴趣可以看一下!\n注意这里有一种情况就是假如服务器不支持HTTP2但是客户端支持,那么根据上面流程选择HTTP/1.1就可以了,例如下面这个图,最后选择的是HTTP/1.1。 \n\n但是还有一种情况就是如果 server hello中不返回ALNP,比如举个例子服务端TLS版本过低不支持ANLP,所以此时目前大部分浏览器做法就是默认降级成HTTP/1.1!具体可以看:https://stackoverflow.com/questions/47758705/http-2-h2-with-no-alpn-support-in-server!\n注意: 上面讲诉的过程是基于TLS/1.2的,其中1.3会有部分差异!其次就是h2建立要求最低TLS/v1.2版本!\n使用 h2c 协商我们知道HTTP协议是可以做协议协商的,那么h2c的建立流程和这个大同小异!目前h2c的主要使用场景就是后端服务希望HTTP/2来带来性能提升,而且大部分场景也不需要tls加密,所以h2c就诞生了!\n注意h2c 有两种实现,一种是基于HTTP/1.1协议升级,一种就是h2的流程但是移除了tls!\n1. 方式一(HTTP/1.1升级)\n 目前主流浏览器都不支持h2c,所以这个基本上没办法测试,如果想要测试可以看一下我写的测试用例: h2c_example\n\n\n\n浏览器发起请求\n\nGET / HTTP/1.1Host: server.example.comConnection: Upgrade, HTTP2-SettingsUpgrade: h2cHTTP2-Settings: <base64url encoding of HTTP/2 SETTINGS payload>\n\n\n如果服务端支持则会响应如下报文,如果不支持正常返回即可!其中下面可以看到会携带 HTTP/2首帧!这里后面会讲解!\n\nHTTP/1.1 101 Switching ProtocolsConnection: UpgradeUpgrade: h2c[ HTTP/2 connection ...\n\n\n关于 HTTP2-Settings 需要了解HTTP/2涉及到的基本概念,一个核心的就是 SETTTINGS 帧, 必须在建连的时候发送,这个header也就是 SETTTINGS 帧 的内容,理解这个就很简单了,它本质上就是把 SETTTINGS 帧 base64编码了一下!用于发送客户端建立的初始化设置!\n[ HTTP/2 connection ... 后续流程是客户端会先发送连接前奏、但是不需要发送SETTINGS帧了,其他就和h2流程差不多!\n这里我们不能抓包了,所以直接看我写的测试用例输出吧\n\n➜ h2c_client git:(master) ✗ go run main.goHTTP/1.1 101 Switching ProtocolsConnection: UpgradeUpgrade: h2c[FrameHeader SETTINGS len=24][FrameHeader WINDOW_UPDATE len=4][FrameHeader HEADERS flags=END_HEADERS stream=1 len=49][FrameHeader DATA flags=END_STREAM stream=1 len=12][FrameHeader RST_STREAM stream=1 len=4]\n\n2. 方式二(h2c)这种是grpc(without tls)采用的方式,就是我们已经知道对面是HTTP/2服务器了,所以不需要使用h2c那种通过 http/1.1升级为http/2这个流程!它的过程和h2的过程一模一样! 具体可以自行抓包查看!\n帧介绍下图是一个HTTP2 over https(h2) 的整个流程,可以看到HTTP2 主要包含有几个特殊标识\n\nMagic\nSettings\nWindows_Update\nHeaders\nData\n\n那么我们下面会依次介绍!我们把这些都成为帧,这些帧都有着不同的作用!\n\n如果你用 nghttp2, 可以执行以下命令: nghttp -vo https://www.toutiao.com | more\n连接前奏根据上图可以看到在 h2 或者 h2c 升级完成,下面紧接着就跟着一个 连接前奏(客户端向服务端发送的),这个内容是固定的,主要是有24个字节组成,16进制标识法如下,展示成字符串就是 PRI * HTTP/2.0\\r\\n\\r\\nSM\\r\\n\\r\\n ! \n0000 50 52 49 20 2a 20 48 54 54 50 2f 32 2e 30 0d 0a PRI * HTTP/2.0..0010 0d 0a 53 4d 0d 0a 0d 0a ..SM....\n\n注意: 具体这么设计的原因也是有的,比如向一个不支持HTTP2的服务器(HTTP/1.x)接收到这个报文,是可以正常解析为HTTP请求报文的,如果不支持就直接返回异常不支持了!\n帧格式介绍连接前奏紧跟着就是 SETTINGS 帧了, 下图就是一个HTTP SETTINGS 帧的报文。在这里我们先补充体下帧的报文格式,也方便后续的理解!这也就是为什么HTTP/2是二进制协议了,同时也能避免HTTP/1.x协议通过纯文本等进行协议分帧的尴尬了!\n\n帧报文如下:\n# 帧格式, 头部固定9字节, 所以 9+ bytes:+-----------------------------------------------+| Length (24) |+---------------+---------------+---------------+| Type (8) | Flags (8) |+-+-------------+---------------+-------------------------------+|R| Stream Identifier (31) |+=+=============================================================+| Frame Payload (0...) ...+---------------------------------------------------------------+\n\n\nLength: 固定为3字节,所以帧最大长度为 1 << 24 -1 , 注意这个长度=len(frame_content)\nType: 即帧的类型,主要分为以下几种,例如 SETTINGS帧就是 0x04,其中这个类型目前仍然在不断拓展中!常见的就是这9种!\n\n+---------------+------+--------------+| Frame Type | Code | Section |+---------------+------+--------------+| DATA | 0x0 | Section 6.1 || HEADERS | 0x1 | Section 6.2 || PRIORITY | 0x2 | Section 6.3 || RST_STREAM | 0x3 | Section 6.4 || SETTINGS | 0x4 | Section 6.5 || PUSH_PROMISE | 0x5 | Section 6.6 || PING | 0x6 | Section 6.7 || GOAWAY | 0x7 | Section 6.8 || WINDOW_UPDATE | 0x8 | Section 6.9 || CONTINUATION | 0x9 | Section 6.10 |+---------------+------+--------------+\n\n\nFlags: 即标志位,这个不太方便理解,你可以理解为 帧的特殊标记!例如 SETTINGS帧 会有一个 ACK Falgs! 多个falgs通过 bitmap 进行设置!\n\nR: 这个是保留位(reserved bit),占用1bit,必须是0\n\nStream Identifier: 即流ID,占用31bit,为无符号整数!顾名思义和RPC的seq id 很像!这里注意客户端发起的stream_id必须为奇数,服务端发起的为偶数!例如上面那个case,客户端发起的stream_id=13! \n\nFrame Payload 即 payload,允许为空!\n\n\nSETTINGS 帧在建立请求过程中,Magic帧后面就会立马跟一个SETTINGS 帧!如下图,这个帧主要作用就是为了初始化连接的配置信息!\n\n其中SETTINGS 帧就是一对对KV组成,如下图,多个KV的话,顺序写即可!\n+-------------------------------+| Identifier (16) |+-------------------------------+-------------------------------+| Value (32) |+---------------------------------------------------------------+\n\n\nIdentifier:标识符,主要分为以下几类,可以理解为key \n\nSETTINGS_HEADER_TABLE_SIZE (0x1): 设置HPACK中动态表的大小,默认是4096个字节SETTINGS_ENABLE_PUSH (0x2): 是否允许服务端推送,默认为0表示关闭,1表示开启SETTINGS_MAX_CONCURRENT_STREAMS (0x3): 表示一个连接上最大的并发数量,无默认值,例如头条主站就是设置的128SETTINGS_INITIAL_WINDOW_SIZE (0x4): 初始化窗口的大小,用于流量控制,默认值是65535, 最大为 2<<31 - 1SETTINGS_MAX_FRAME_SIZE (0x5): 最大的帧大小,默认值是 16384,最大为2<<24 - 1SETTINGS_MAX_HEADER_LIST_SIZE (0x6): 这里表示请求header的最大长度\n\n\nValue: 值,四个字节,都是int值\n\n注意:\n\n如果一个端需要发起设置,那么它需要标记 falgs Ack=0, 然后携带上配置,另外一端接收后会响应一个falgs Ack=1,且 payload 为空! 例如下面流程:\n\n## 收到settings帧[ 0.083] recv SETTINGS frame <length=18, flags=0x00, stream_id=0> (niv=3) [SETTINGS_MAX_CONCURRENT_STREAMS(0x03):128] [SETTINGS_INITIAL_WINDOW_SIZE(0x04):65536] [SETTINGS_MAX_FRAME_SIZE(0x05):16777215]## 发送ack帧 [ 0.083] send SETTINGS frame <length=0, flags=0x01, stream_id=0> ; ACK (niv=0)\n\n\nSETTINGS 帧的 stream_id 固定为0 !\n\nWINDOW_UPDATE 帧 (流量控制)WINDOW_UPDATE 帧 主要是用于流量控制(Flow Control),这个名词在TCP也有!不过 HTTP/1.x 并未包含流控机制,依靠 TCP 的流控也工作得很好,那为什么 HTTP/2 需要添加呢?原因很明显,因为 HTTP/2 引入了 stream(流) 和 multiplexing(多路复用) ,想让众多 stream 协同工作,就需要一种控制机制,防止某个流阻塞其他流!\n具体原理如下:sender发送数据会降低窗口大小,需要receiver方通知sender来恢复窗口,这就需要WINDOW_UPDATE 帧 了! 其中流控算法官方并没有提出实现规范!注意 sender 指的数据发送方(可以是server、也可以是client)。 整体总结就是:接收者来提供控制!\n\n 这里理解比较抽象,比如现在假如一个连接有多个流,流A和流B…xxx,假如此时客户端压力比较大(比如处理不过来消息或者机器负载过高),客户端检测到了,那么可以通过流量控制去实现!\n\n\n具体HTTP/2如何实现流控的,主要是由于HTTP/2 的流控分为了两类,stream flow-control window(sfw) 和 connection flow-control window (cfw), 其中每个stream会维护自己的sfw,所有的stream共用一个cfw !发送多少包窗口就减少多少,cfw和sfw都会减少!那么增加呢是单独增加!要么是cfw、要么是sfw!其次cfw、sfw只针对于DATA帧的内容!\n例如:GRPC中在当接收的包的总大小(自己做的receive flow-control)大于cfw/sfw大小的1/4的时候,就会发送window_update帧,然后重置receive size,或者发送PING包的时候也会携带发送window_update 帧!具体逻辑可以看: sender逻辑 、 server 端receiver逻辑 、 client receiver逻辑,有兴趣的可以看一下!目前HTTP/2应用比较广泛的应该是GRPC、Nginx(L7)、浏览器!\nWINDOW_UPDATE帧 协议包比较简单,主要就是传输Window Size Increment,这个值是增加cfw or sfw的大小! 如果调整 sfw则需要传递 stream_id !\n+-+-------------------------------------------------------------+|R| Window Size Increment (31) |+-+-------------------------------------------------------------+\n\n说到这里,可能还得知道,初始化窗口: 当一个连接初始化的时候,sfw和cfw都是65535,如果需要调整sfw 的初始化窗口大小则需要需要发送Setting帧 (这个是针对所有stream),如果需要调整cfw大小(所以初始化时候cfw一定是65535)需要发送window_update帧!\nHEADERS 帧一个HTTP/2 请求从 HEADERS 帧 开始发送的!例如这个是一个HTTP/2 Headers 帧的抓包图:\n\n其中header的编码比较复杂,后续我们会讲解HPACK,所以这里只要知道有这个header帧主要包含哪些内容即可!\n+---------------+|Pad Length? (8)|+-+-------------+-----------------------------------------------+|E| Stream Dependency? (31) |+-+-------------+-----------------------------------------------+| Weight? (8) |+-+-------------+-----------------------------------------------+| Header Block Fragment (*) ...+---------------------------------------------------------------+| Padding (*) ...+---------------------------------------------------------------+\n\n\nPad Length: 表示尾部Padding的长度,为可选字段(注意标记?的为可选字段)\nE:1bit,表示当前流是否排他的 (与 PRIORITY 帧有关)\nStream Dependency: 表示依赖于哪个流 (与 PRIORITY 帧有关)\nWeight:流的权重 (与 PRIORITY 帧有关)\nHeader Block Fragment: 请求的首部,使用HPACK编码\nPadding:补齐内容,用0填充\n\n例如报文如下:\n0000 00 00 1f 01 05 00 00 00 01 82 84 87 41 8b f1 e3 ............A...0010 c2 f3 1c f3 50 55 c8 7a 7f 7a 88 25 b6 50 c3 ab ....PU.z.z.%.P..0020 ba ea e0 53 03 2a 2f 2a ...S.*/*\n\n\n00 00 1f 01 05 00 00 00 01: 前9个字段为Frame的头部,payload长度为31,为Headers帧,flag为5,stream_id=1\n其中后面31个字节就是头部的header内容了, 这里采用了 HPACK 编码!所以需要了解下HPACK编码,这里也能看出来下文是Header的内容,一共是100个字节,但是使用HPACK编码后只占用了31个字节! 整整节约了70%的编码!\n\n:method: GET:path: /:scheme: https:authority: www.google.comuser-agent: curl/7.77.0accept: */*\n\n这里我们要解码者31个字节需要直接跳转到 HPACK那一节吧!\n注意:\n\nHTTP/2 和 HTTP/1.x 协议有些header是不兼容的,比如Keep-Alive 和 Connection 等在HTTP/2中直接没有!\nHTTP的header是支持 header: v1;v2表示多个 ,同时也支持 header: k1,header:k2 ,在HTTP/2中在编码的时候会使用后者!同时header name推荐全部小写(看完HPACK就会理解)!\n\nDATA 帧data帧对应着 HTTP的Body,如果没有响应体的话是不需要传输的!\n+---------------+|Pad Length? (8)|+---------------+-----------------------------------------------+| Data (*) ...+---------------------------------------------------------------+| Padding (*) ...+---------------------------------------------------------------+\n\n\nPad Length: 表示padding长度, 只有存在 PADDED flags才有这个值!\nData: 真正的数据\nPadding : 表示padding数据,并没有强制的padding算法,也就是你发出的数据可以不padding!\n\n这里 DATA 帧会有两个标识,一个是 END_STREAM 、一个是 PADDED\nPRIORITY帧 (请求优先级)这个帧可以定义请求(stream)的优先级,但是这个优先级只是客户端发来的提议,服务端可以不去支持!下面就是报文格式:\n+-+-------------------------------------------------------------+|E| Stream Dependency (31) |+-+-------------+-----------------------------------------------+| Weight (8) |+-+-------------+\n\n\nE:是否独占(排他),独占依赖的流(形象点就是我独占它,原来依赖它的人全部得依赖我)\nStream Dependency:流依赖\nWeight:流权重,这个应该是最好理解的,权重越大,优先级越高!\n\n这里吐槽一句:使用HTTP/1.x,浏览器可以完全控制资源加载顺序!HTTP/2让这些事情变得更好也更复杂了!但是么还是值得学习一下的!\n具体实现就设计到算法了,因为既要要求性能高,同时也要求准确性!关于算法可以看 RFC7540-5.3,实现的话可以看下NGINX或者熟悉语言、框架的SDK!下面我就就单介绍一下!\n\n简单的依赖,比图 B和C依赖于A,此时插入个D也依赖于A那么就是下面这个效果!注意D的位置可以任意!\n\n A A / \\ ==> /|\\B C B D C\n\n\n独占情况,还是上面的关系,假如D独占A,那么就会变为下面这个图\n\n A A | / \\ ==> DB C / \\ B C\n\n\n权重,主要是解决具有相同父流的流应该根据权重比例分配资源!比如这个图假如A加载完成后,那么B应该优先处理,才会处理C!\n\n A / \\ / \\ B(12) C(4) \n\n\n动态调整,例如下图初始化是图一,假如此时A依赖于D,那么D直接替换A的位置(这里看着操作是D放到了和A一样的父亲下面,变成了图intermediate),然后A的子依赖关系不变,挪动一下,就变成了图non-exclusive !假如A独占D了,就变成了 图exclusive !\n\n x x x x | / \\ | | A D A D D / \\ / / \\ / \\ | B C ==> F B C ==> F A OR A / \\ | / \\ /|\\ D E E B C B C F | | | F E E(图一) (intermediate) (non-exclusive) (exclusive)\n\n其他帧\nPING帧 ,主要是用来维持一个空闲连接的,以及可以计算一次RTT时间!\n\nGOAWAY帧 ,主要是用来关闭连接的,同时允许携带发送一个错误码!\n\nRST_STREAM 帧,主要是用来关闭流的,同时允许携带发送一个错误码!\n\nCONTINUATION 帧,主要是解决当 HEADER帧 超出了限制MAX_FRAME_SIZE的大小来获取完整的HTTP首部的问题,所以需要发送 CONTINUATION 帧,同时CONTINUATION帧也需要在MAX_FRAME_SIZE大小之内,所以可能跟随多个 CONTINUATION 帧! 它有个END_HEADERS 标记来标记是否结束!所以假如出现这个case那么 Header帧就需要标记END_HEADERS=False了! 其实PUSH_PROMISE帧 不够大(这个后面会讲到),后面也得用 CONTINUATION 帧!\n\n\nHPACK 编码我们知道HTTP/1.x其实可以对Body部分进行压缩,但是头部部分是无法压缩的!所以HTTP/2中引入了HPACK 主要是用来压缩HTTP的头部的,主要是采用静态表+动态表+压缩算法组成!然后就是HTTP/2中把请求行/响应行部分移到了全部转移到了Header部分!对照关系是:\nmethod -> :methodpath -> :pathstatus -> :status\n\n静态表静态表(Static Table),一共61对,应该很好理解,比如 method=GET,那么会把它转换成一个数字,例如:authority=www.google.com 会转换一个数字 + 字符串(不一定)!\n+-------+-----------------------------+---------------+| Index | Header Name | Header Value |+-------+-----------------------------+---------------+| 1 | :authority | || 2 | :method | GET || 3 | :method | POST || 4 | :path | / || 5 | :path | /index.html || 6 | :scheme | http || 7 | :scheme | https || 8 | :status | 200 || 9 | :status | 204 || 10 | :status | 206 || 11 | :status | 304 || 12 | :status | 400 || 13 | :status | 404 || 14 | :status | 500 || 15 | accept-charset | || 16 | accept-encoding | gzip, deflate || 17 | accept-language | |............| 58 | user-agent | || 59 | vary | || 60 | via | || 61 | www-authenticate | |+-------+-----------------------------+---------------+\n\n注意:这里虽然没有:method 且 Header Value 为空的这种情况,其实实际处理的时候:method=Delete 这种实际上 :method会编码为3,如果没有则取key相同最大的index!\n动态表动态表(Header Table),顾名思义就是动态生成表,然后最终形式上和上面静态表差不多!它的生命周期是一个连接上(注意一个连接可以有多个请求/响应)!\n由于动态表时动态生成的,那么对于单机维护的连接数过多,此时动态表会很占内存,假如此时我们不进行内存限制,很容易被攻击导致内存OOM,那么所以动态表会有一个大小限制,可以通过 SETTINGS 帧下发HEADER_TABLE_SIZE=4096, 单位是字节,来实现动态表内存限制!那么一个Header的Key和Value的大小是多少了?计算公式是 len(key)+len(value)+32 , 为什么加32了,是因为大部分语言,存储string会额外消耗16字节用来存储数组指针和数组长度!\n那么假如动态表在我们写header的时候,发现大小超出了 HEADER_TABLE_SIZE 怎么办了,具体算法就是个FIFO模型,队列大小确定,所以大概流程就是下面这个:\n// 请求1// set max_size=32 + 4 + 32 + 4 + 32 + 4 + 32// 计算index(key,value)公式 = arr_index(key,value) + 61 + 1addHeader("k1", "v1") // write k1-v1, arr=[k1-v1]addHeader("k2", "v2") // write k2-v2, arr=[k2-v2, k1-v1]addHeader("k3", "v3") // write k3-v3, arr=[k3-v3, k2-v2, k1-v1]addHeader("k4", "v4") // write k4-v4, arr=[k4-v4, k3-v3, k2-v2]// 请求2addHeader("k3", "v3") // write index=63addHeader("k1", "v1") // write k1-v1, arr=[k1-v1, k4-v4, k3-v3]addHeader("k3", "v3") // write index=64addHeader("k4", "v4") // write index=63addHeader("k1", "v1") // write index=62\n\n编码在HTTP/2中编码采用的算法主要是 varint编码 和 霍夫曼编码 (Huffman Coding 也叫做哈夫曼编码)\n概念:\n\nvarint 编码是数字压缩算法中常见的一种,主要是采用变长来存储数字,比如虽然值定义的是8字节,但是比如数字1可以用1个字节进行编码,主要原理就是利用 msb (the Most Significant Bit 最高有效位)!\n霍夫曼编码是文本压缩算法中常见的一种,是基于字符出现的次数为权重(墒编码)进行的构建字典,进而构建霍夫曼树,然后根据霍夫曼树来确定字符的二进制编码。但是如果字符不重复,且字符还很多,导致霍夫曼树很深,那么霍夫曼编码的意义就不大了!\n\n注意:\n\nHTTP/2 中采用的 varint 编码并不是标准的,因为他需要根据低位第一个字节标记falg, 所以解决思路就是减去第一个字节定义的最大值(区间是 1 到 1<<7-1),剩余的值用的varint编码,具体可以参考: https://datatracker.ietf.org/doc/html/rfc7541#section-5.1, 伪代码如下: \n\n# 伪代码if num < max{\t return num}[Max,varint(num-max) ...]# case: # 例如max=1<<7-1, num=126(0b01111110), 那么输出 [0x7e]# 例如max=1<<7-1, num=127(0b01111111),那么输出 [0x7f, 0x00]# 例如max=1<<7-1, num=127+128(0b10000000), 那么输出 [0x7f, 0x80, 0x01]# 下文中叫 (7+)varint 表示max=1<<7-1 也就是上面这个case, 例如(6+)varint 表示max=1<<6-1\n\n\nHTTP/2 中采用的 霍夫曼编码也不是按照标准的,它采用的是静态表,也就是它省去了动态生成霍夫曼树的过程(省略计算过程和Map过程),这个树就是个静态的!具体可以参考: https://datatracker.ietf.org/doc/html/rfc7541#section-5.2 !\n\n1. Indexed Header Field Representation采用这边编码情况是: header 的 key和value 都在中,然后可以拿到index(动态表index=静态表index+偏移量),比如method: GET\n+---+---+---+---+---+---+---+---+| 1 | Index (7+) |+---+---------------------------+\n\n\n1:标识符号,varint编码后第一个字节的高位8bit标识符是1\nindex (7+): 表示index值,采用 (7+)varint 算法\n\n2.1 Literal Header Field with Incremental Indexing - indexed Name表示: name 在表中,value 不在表中,且需要添加到表中,比如 Host: www.google.com\n 0 1 2 3 4 5 6 7+---+---+---+---+---+---+---+---+| 0 | 1 | Index (6+) |+---+---+-----------------------+| H | Value Length (7+) |+---+---------------------------+| Value String (Length octets) |+-------------------------------+\n\n\n01+ Index (6+) ,为index编码,主要是为了区分上面Indexed Header Field Representation 的case\nH: 为Value String 是否采用霍夫曼编码编码,H=1 表示采用霍夫曼编码\nValue Length(7+): 表示value string的长度,具体算法就是 (7+)varint 算法\nValue String:霍夫曼编码 or 原值\n\n2.2 Literal Header Field with Incremental Indexing - New Name表示: name 不在表中,value也不在表中,但是需要添加到表中!例如自定义header X-Host: www.google.com,且我们允许 name 和 value 添加到表中!\n 0 1 2 3 4 5 6 7+---+---+---+---+---+---+---+---+| 0 | 1 | 0 |+---+---+-----------------------+| H | Name Length (7+) |+---+---------------------------+| Name String (Length octets) |+---+---------------------------+| H | Value Length (7+) |+---+---------------------------+| Value String (Length octets) |+-------------------------------+\n\n这里我就不过多讲解了,通过上面的两个讲解,应该大家都有所了解!这个也就是第一个字节一定是 0x40\n3.1 Literal Header Field without (Never) Indexing - Indexed Name和上面的Literal Header Field with Incremental Indexing - indexed Name 的区别在于,这个不需要被添加到表中!\n其次就是 Literal Header Field without Indexing 和 Literal Header Field Never Indexing 主要区别在于首字节,前者是 0000xxxx,后者是0001xxxx!\n 0 1 2 3 4 5 6 7+---+---+---+---+---+---+---+---+| 0 | 0 | 0 | 0 | Index (4+) |+---+---+-----------------------+| H | Value Length (7+) |+---+---------------------------+| Value String (Length octets) |+-------------------------------+\n\n3.2 Literal Header Field without(Never) Indexing - New Name和上面的Literal Header Field with Incremental Indexing - indexed Name 的区别在于,这个不需要被添加到表中!\n\t0 1 2 3 4 5 6 7+---+---+---+---+---+---+---+---+| 0 | 0 | 0 | 0 | 0 |+---+---+-----------------------+| H | Name Length (7+) |+---+---------------------------+| Name String (Length octets) |+---+---------------------------+| H | Value Length (7+) |+---+---------------------------+| Value String (Length octets) |+-------------------------------+\n\n4. Dynamic Table Size Update我们知道 SETTINGS帧的 SETTINGS_HEADER_TABLE_SIZE 也是设置 tables size大小的,那么这个作用是什么?它的含义就是动态变更 size,但是这个max size 必须小于或者等于 SETTINGS_HEADER_TABLE_SIZE!\n0 1 2 3 4 5 6 7+---+---+---+---+---+---+---+---+| 0 | 0 | 1 | Max size (5+) |+---+---------------------------+\n\n5. 总结注意关于 Literal Header Field without Indexing 和 Literal Header Field Never Indexing 的区别在于哪里了?官方的解释差别就一句话 Intermediaries MUST use the same representation for encoding this header field., 意思就是假如我们请求中间有个HTTP/2代理(例如 nginx),那么如果是Never Indexed 那么代理也必须原封不动的转发 !\n但是目前看Nginx并不支持后端服务(up stream)是HTTP/2协议,只支持GRPC(注意GRPC处理模块可以处理一些通用的HTTP/2协议的请求,但是支持度并不好)!具体可以参考 https://trac.nginx.org/nginx/ticket/923 !\n服务器推送 PUSH_PROMISE 帧首先要明白,HTTP/2为什么会有服务端推送,比如我们一个普通的加载网页的流程,会返回网页,然后再起请求一些静态资源,所以浏览器的整个流程是下面这个流程如下图左侧!但是如果有服务端推送的话后续加载n次资源就会减少n次RTT的时间,如下图右侧!那么HTTP/2是如何实现服务端推送的呢?\n\n首先在讲下面我们需要先说明一个问题:如果实现上面的功能,我们需要告诉浏览器要推送哪些资源,不然我们先返回html浏览器就渲染html直接发起请求加载静态文件了,那么我们推送就白做了,所以这里需要知道是哪个流发起的加载html的请求,然后通过这个流告诉浏览器我们要下发给你哪些资源在返回HTML之前!\n# 服务器推送从源服务器的 Link 标头的 rel=preload 参数提取 URI 引用,然后将这些额外 URI 提供给客户端Link: </images/image.png>;rel=preload;Link: </css/main.css>;rel=preload;\n\nPUSH_PROMISE 帧是服务器发起的请求,所以这里的stream_id是偶数!\n+---------------+|Pad Length? (8)|+-+-------------+-----------------------------------------------+|R| Promised Stream ID (31) |+-+-----------------------------+-------------------------------+| Header Block Fragment (*) ...+---------------------------------------------------------------+| Padding (*) ...+---------------------------------------------------------------+\n\n\nR: 保留位\nPromised Stream ID: 这个就是上面讲到的,其实就是Push stream id(承诺要发送的stream ID)\nHeader Block Fragment: 请求header,其实就是把推送的请求体发送过去了!\n\n注意:由于推送会涉及到幂等、安全等问题,所以一般就是推送静态资源!\n服务器推送的整体流程还是很简单,就是响应HTML之前发送 PUSH_PROMISE 帧,这个帧会携带push资源的请求内容!然后返回HTML,然后再用Promised Stream ID 写响应关闭流即可!\n服务端推送相比于HTTP/2不推送,前端页面整体的提升率大约在8%左右,具体可以看相关测评 ,同时开启后也会存在一定浪费带宽的问题,那就是假如浏览器有缓存不就不用返回了!目前主流的架构都是将静态资源基本放在CDN上,所以如果需要使用服务端推送,需要确定CDN厂商是否支持!\n下图来自于又拍云关于DNS 服务端推送相关的页面,具体可以点击链接查看:\n\nHTTP/3 介绍 通过学习上面我们发现HTTP/2其实做了很多的优化,在整个传输层,不仅降低了数据包的大小,同时流和多路复用降低了连接上的开销,提高了页面的加载速度!但是这种优化也是有弊端的!现在移动互联网比较普及,大部分流量都来自于移动设备,而移动设备最大的特点就是网络问题!而HTTP/2依赖于TCP,在网络不稳定的情况下,TCP的流控会导致HTTP/2变慢,如果发生丢包,就会导致HoL (Head of Line Blocking 头部阻塞), 导致一个连接上所有的流都需要等待!\n 因此作为HTTP/2前身SPDY的发明者Google,毕竟走的路比较长,所以也提出了比较好的解决思路,那就是把TCP替换掉,随后就诞生了QUIC!QUIC ( Quick UDP Internet Connection) 是 Google 研发的一种基于 UDP 协议的低时延互联网传输协议。在2018年IETF会议中(主要原因还是TLS v1.3同年正式发布),HTTP-over-QUIC协议被重命名为HTTP/3,并成为 HTTP 协议的第三个正式版本,大概的关系就是: 运行在 QUIC 之上的 HTTP 协议被称为 HTTP/3!\n**QUIC 是传输层协议(4层协议)**,其中有两个版本一个是gQUIC(Google QUIC)和iQUIC(IETF QUIC),注意两者差别!有兴趣的可以看下QUIC 介绍,QUIC[RFC 8999-9002],以及 TLS1.3[RFC 8446],下图是一个大概的一个模型图:\n\n由于本文前面篇幅有点长了,而且从HTTP发展历史也可以看得出来会越来越复杂,所以如果有兴趣的想深入了解的可以查阅相关资料,上面已经提供了很多了,下面就列出来了QUIC的几大特点:\n\n降低了建连时间(注意a为TLS完全握手,b为TLS简化握手,可以实现0RTT 握手)\n\n\n\n根据第三方数据使用QUIC 0RTT率大约在55%左右,具体可以点击文章查看详情!\n\n\n优化了流量控制算法\n优化了拥塞控制算法(其实和TCP用的是一样的都是Cubic算法)\n用户态协议,不需要升级or修改内核,升级迭代容易,虽然TCP有许多新特性,但是抵挡不住不敢随意升级内核哇!\n\n目前外部对于QUIC的实现也比较多,比如字节的TTQUIC,快手的KQUIC, 以及微软的 msquic 。但是目前来看QUIC也有很多问题,主要问题还是UDP带来的问题,因为不同运营商会对UDP进行了一定的限制,其次就是多一层用户态协议的开销也是有一定的性能损耗(CPU、Mem的开销),最后就是目前人们对于TCP优化做的太多了!不过相信这些问题都是可以用时间去解决的,相信未来QUIC发展的更好!\n参考文章\nSSL/TLS发展历史和SSLv3.0协议详解\nHTTPS的性能消耗\n深入解读SSL/TLS的实现\nRFC7540\nHTTP2流量控制介绍\n技术干货:HTTP/2 之服务器推送 (Server Push) 最佳实践\n揭秘QUIC的五大特性及外网表现\nTCP流量控制、拥塞控制\nHTTP/3 原理实战\nQUIC with TLS1.3 简介\n\n","categories":["HTTP"],"tags":["HTTP"]},{"title":"如何调试 envoy","url":"/2023/08/20/1f843b2e9cdc2e9eb8812867e097b659/","content":"envoy 是目前高性能数据平面的基本标准了,虽然有些看着笨重,但是社区牛逼,其次性能也很高,目前大部分公司的mesh基本都是通过envoy构建的,所以学习是很重要的,本人主要是介绍如何debug envoy,本人也是踩坑了大半天!\n\n\n背景envoy 是目前高性能数据平面的基本标准了,虽然有些看着笨重,但是社区牛逼,其次性能也很高,目前大部分公司的mesh基本都是通过envoy构建的,所以学习是很重要的,本人主要是介绍如何debug envoy,本人也是踩坑了大半天!\nC++的代码最难的就是主流的IDE工具对代码阅读、调试难度比较大,这几本就挡住了大部分人的学习动力,其中clion对于cmake支持度很好,vscode是比较方便和灵活,可以远程直接debug,一般debug c++的流程就是先要编译成一个二进制产物,通过 gdb/lldb 进行debug。envoy 默认在linux上构建是用clang编译的,debug的话最好用lldb!\n环境\n本地开发环境是 Mac\n远程开发环境是 Linux(Debian10, 8C+16G, 公司有32c+64g的啥时候我申请个,真的是不好意思😬)【如果你本地机器性能高可以不用远程,直接本地起个docker就行了】\n编译环境是远程的Docker (vscode remote 也在这里运行)\nenvoy - v1.26.0 (构建环境是 clang/lldb 14.0.0,clang比gcc构建要快很多….)\n本地 vscode\n\n\n注意:\n\nenvoy这种大型项目它对于环境依赖太重了,所以千万别用本地环境去构建,基本行不通!\n\nc++ debug对于环境一致性要求很高\n\n我本来想试试 lldbserver/gdbserver 用 clion 进行 remote debug,发现人家产物1.4个G,下载elf太墨迹了,所以还是直接在docker容器内调试算了\n\n\n\n调试流程修改脚本\nrun_envoy_docker.sh \n\n\n配置 http代理,不然拉代码太墨迹了,自己找http/https代理\n配置docker run 添加参数 --cap-add=SYS_PTRACE --security-opt seccomp=unconfined 支持调试\ndocker 最好限制下 cpu,防止编译把电脑cpu打满了,直接挂了!\n修改bazel cache的挂载目录: DEFAULT_ENVOY_DOCKER_BUILD_DIR=${HOME}/.cache/bazel_docker (默认是在/temp 目录下重启电脑就没了)\n\n\nbuild_setup.sh 脚本注释掉 (防止把构建产物给删了)\n\n#cleanup#trap cleanup EXIT\n\n3.do_ci.sh 注释掉 bazel_binary_build 的一些内容\n\n进入容器./ci/run_envoy_docker.sh '/bin/bash'\n\n构建执行./ci/do_ci.sh bazel.debug.server_only 即可, 首次构建基本上得两个小时左右,后面增量构建基本上2分钟左右!\n# 这里可以看到 --config=libc++ 实际上就是clang/usr/local/bin/bazel --output_user_root=/build/tmp/output build --verbose_failures --experimental_generate_json_trace_profile --test_output=errors --repository_cache=/build/repository_cache --experimental_repository_cache_hardlinks --action_env=CLANG_FORMAT --test_tmpdir=/build/tmp --config=libc++ --remote_download_toplevel -c dbg //source/exe:envoy-static\n\n\n最后自己把 cache 文件压缩一下,备份一份,防止手残给删了。。。。\nenvoybuild@a66c8f90a687:/source$ ls -al bazel-bin/source/exe/total 1381592drwxrwxrwx 4 envoybuild envoybuild 4096 Aug 19 15:05 .drwxrwxrwx 6 envoybuild envoybuild 4096 Aug 19 13:18 ..-r-xr-xr-x 1 envoybuild envoybuild 56844 Aug 19 13:18 envoy_common_lib.cppmap-r-xr-xr-x 1 envoybuild envoybuild 2096 Aug 19 13:18 envoy_main_common_lib.cppmap-r-xr-xr-x 1 envoybuild envoybuild 1954 Aug 19 13:18 envoy_main_entry_lib.cppmap-r-xr-xr-x 1 envoybuild envoybuild 1413995968 Aug 19 15:05 envoy-static-r-xr-xr-x 1 envoybuild envoybuild 632368 Aug 19 10:40 envoy-static-2.paramsdrwxrwxr-x 3 envoybuild envoybuild 4096 Aug 19 15:03 envoy-static.runfiles-r-xr-xr-x 1 envoybuild envoybuild 141 Aug 19 10:41 envoy-static.runfiles_manifest-r-xr-xr-x 1 envoybuild envoybuild 3546 Aug 19 13:18 main_common_lib.cppmapdrwxrwxr-x 8 envoybuild envoybuild 4096 Aug 19 15:04 _objs-r-xr-xr-x 1 envoybuild envoybuild 1501 Aug 19 14:36 platform_header_lib.cppmap-r-xr-xr-x 1 envoybuild envoybuild 1613 Aug 19 13:18 platform_impl_lib.cppmap-r-xr-xr-x 1 envoybuild envoybuild 1803 Aug 19 14:36 platform_impl_lib_linux.cppmap-r-xr-xr-x 1 envoybuild envoybuild 2275 Aug 19 13:18 process_wide_lib.cppmap-r-xr-xr-x 1 envoybuild envoybuild 1264 Aug 19 13:18 scm_impl_lib.cppmap-r-xr-xr-x 1 envoybuild envoybuild 1863 Aug 19 13:18 terminate_handler_lib.cppmap\n\n调试\nenvoy 核心用的libevent,所以大量回掉代码,阅读调试比较困难!\n\n\n支持调试本地代码\n\n\n\n支持调试依赖代码\n\n\n\n本地下载 vscode 插件\n\n\nRemote - SSH 可以连接远程虚拟机\nDev Containers 连接连接服务器内的docker容器\n\n\n远程连接后下载vscode插件 CodeLLDB\n\n配置 /.vscode/launch.json 文件中配置\n\n\n{ "version": "0.2.0", "configurations": [ { "type": "lldb", "request": "launch", "name": "Launch", "program": "/source/bazel-bin/source/exe/envoy-static", "args": [], "cwd": "/source", "sourceMap":{ "/proc/self/cwd": "/source", "/proc/self/cwd/external": "/build/tmp/output/b570b5ccd0454dc9af9f65ab1833764d/execroot/envoy/external", } } ]}\n\n\n挂载点是,自行查看自己的挂载点 bazel-source 目录就是bazel的构建目录\n\nenvoybuild@82990b41dce5:/source$ ls -altotal 464drwxr-xr-x 29 envoybuild envoybuild 4096 Aug 19 10:08 .drwxr-xr-x 1 root root 4096 Aug 19 10:23 ..drwxr-xr-x 9 envoybuild envoybuild 4096 Aug 19 05:23 api-rw-r--r-- 1 envoybuild envoybuild 6 Aug 19 05:23 API_VERSION.txtdrwxr-xr-x 3 envoybuild envoybuild 4096 Aug 19 05:23 .azure-pipelines-rw-r--r-- 1 envoybuild envoybuild 2298 Aug 19 05:23 BACKPORTS.mddrwxr-xr-x 8 envoybuild envoybuild 4096 Aug 19 05:23 bazellrwxrwxrwx 1 envoybuild envoybuild 86 Aug 19 06:08 bazel-bin -> /build/tmp/output/b570b5ccd0454dc9af9f65ab1833764d/execroot/envoy/bazel-out/k8-dbg/bindrwxr-xr-x 2 envoybuild envoybuild 4096 Aug 19 05:23 .bazelci-rw-r--r-- 1 envoybuild envoybuild 152 Aug 19 05:23 .bazelignorelrwxrwxrwx 1 envoybuild envoybuild 75 Aug 19 06:08 bazel-out -> /build/tmp/output/b570b5ccd0454dc9af9f65ab1833764d/execroot/envoy/bazel-out-rw-r--r-- 1 envoybuild envoybuild 19921 Aug 19 05:23 .bazelrclrwxrwxrwx 1 envoybuild envoybuild 65 Aug 19 06:08 bazel-source -> /build/tmp/output/b570b5ccd0454dc9af9f65ab1833764d/execroot/envoylrwxrwxrwx 1 envoybuild envoybuild 91 Aug 19 06:08 bazel-testlogs -> /build/tmp/output/b570b5ccd0454dc9af9f65ab1833764d/execroot/envoy/bazel-out/k8-dbg/testlogs-rw-r--r-- 1 envoybuild envoybuild 6 Aug 19 05:23 .bazelversion-rw-r--r-- 1 envoybuild envoybuild 1454 Aug 19 05:23 BUILD\n\n\n调试官方demo https://www.envoyproxy.io/docs/envoy/latest/start/quick-start/run-envoy\n\n\n下载配置\n\nwget https://www.envoyproxy.io/docs/envoy/latest/_downloads/92dcb9714fb6bc288d042029b34c0de4/envoy-demo.yaml\n\n\n配置启动参数\n\n{ "version": "0.2.0", "configurations": [ { "type": "lldb", "request": "launch", "name": "Launch", "program": "/source/bazel-bin/source/exe/envoy-static", "args": [ "-c", "/source/envoy-demo.yaml" ], "cwd": "/source", "sourceMap":{ "/proc/self/cwd": "/source", // cwd可以理解为编译的根路径(死的), 需要改成你的项目路径 "/proc/self/cwd/external": "/build/tmp/output/b570b5ccd0454dc9af9f65ab1833764d/execroot/envoy/external", // bazel下载的一些依赖挂载点 } } ]}\n\n\n调试, 浏览器可以打开 http://localhost:10000/ (vscode yyds 支持转发端口)\n\nbazel 基本用法\n这里我用的clang,不太喜欢用gcc, bazel的话直接下载 https://github.com/bazelbuild/bazelisk ,他可以自动下载对应的bazel版本,根据项目里的.bazelversion\n\n~ which clang++/usr/lib/llvm-13/bin/clang++~ which bazel # https://github.com/bazelbuild/bazelisk/releases /home/fanhaodong.516/go/bin/bazel\n\n\n脚本\n\n.PHONY: fastbuild optbuild debug cleanall: fastbuildfastbuild: # 快速构建\tbazel build --config clang //:mainoptbuild: # 优化构建\tbazel build --config clang -c opt //:main\tbazel build --config clang -c opt //:main.dwp\tls -al bazel-bin/clean: # 清理bazel此项目的构建缓存\tbazel cleandebug: # debug构建, --subcommands 可以查看构建参数,非常有用排查问题。 make debug 2>&1 | tee out.log\trm -rf bazel-bin\tbazel build --config clang -c dbg //:main --subcommands\tbazel build --config clang -c dbg //:main.dwp --subcommands\tls -al bazel-bin/\n\n\n.bazelrc 文件配置构建参数\n\nbuild --cxxopt=-std=c++14 --host_cxxopt=-std=c++14build:linux --features=per_object_debug_infobuild:linux --fission=dbg,optbuild:clang --config=linuxbuild:clang --action_env=CC=clang --action_env=CXX=clang++\n\n\n.bazelversion 配置bazel版本\n\n6.2.1\n\n\nCodeLLDB 配置 lldb脚本 .vscode/launch.json 文件\n\n{ "version": "0.2.0", "configurations": [ { "type": "lldb", // 下同 "request": "custom", "name": "Custom launch", "targetCreateCommands": [ "target create /home/fanhaodong.516/go/src/github.com/anthony-dong/bazel_simple/bazel-bin/main", "setting append target.source-map /proc/self/cwd /home/fanhaodong.516/go/src/github.com/anthony-dong/bazel_simple", "setting append target.source-map /proc/self/cwd/external /home/fanhaodong.516/.cache/bazel/_bazel_fanhaodong.516/a3fbbe1bc80076bc1aaa5a9834db2f5d/execroot/bazel_simple/external" ], "processCreateCommands": [ "settings set target.run-args 1 '2 3' '4 5'", "process launch" ] }, { "type": "lldb", "request": "launch", "name": "Launch", "program": "/home/fanhaodong.516/go/src/github.com/anthony-dong/bazel_simple/bazel-bin/main", // 执行文件路径 "args": [ // 配置执行文件启动参数 "ls", "-al" ], "cwd": "/home/fanhaodong.516/go/src/github.com/anthony-dong/bazel_simple", // 代码路径 "sourceMap": { "/proc/self/cwd": "/home/fanhaodong.516/go/src/github.com/anthony-dong/bazel_simple", // 文件map,第一个必须(bazel在linux下构建 PWD=/proc/self/cwd,强制的) "/proc/self/cwd/external": "/home/fanhaodong.516/.cache/bazel/_bazel_fanhaodong.516/a3fbbe1bc80076bc1aaa5a9834db2f5d/execroot/bazel_simple/external", // 次要配置,看看你需不需要debug依赖,可以配置多个 } } ]}\n\nbazel - fission模式\n这个比较坑,需要深入了解下,不然debug的话会是噩梦,可以自己写个小项目试试!\n\n\nbazel 构建模式:https://bazel.build/docs/user-manual#compilation-mode , 默认是fastbuild\nbazel 调试模式:https://bazel.build/docs/user-manual#strip \n\n# 还可以手动再精简 。。。strip bazel-bin/main -o ./main\n\n\nfission 模式 https://bazel.build/docs/user-manual#fission\n\n# --features=per_object_debug_info 可选可不选了现在 (我的版本是 6.x)# fission 支持 dbg,opt,fastbuild,yes(all),no(none)# 只有开启了 fission 才能够支持 bazel build //:xxx.dwpbuild:linux --features=per_object_debug_infobuild:linux --fission=dbg,opt\n\n\n构建文件\n\nbazel build --config clang -c dbg //:main# 没开启 fission 这个执行了也没啥用... //:xxx.dwp 只在开启fission下生效bazel build --config clang -c dbg //:main.dwp\n\n\n未开启 fission (dbg) 构建\n\ndrwxr-xr-x 5 fanhaodong.516 fanhaodong.516 4096 Aug 19 22:56 .drwxr-xr-x 4 fanhaodong.516 fanhaodong.516 4096 Aug 19 22:56 ..drwxrwxrwx 4 fanhaodong.516 fanhaodong.516 4096 Aug 19 22:56 external-r-xr-xr-x 1 fanhaodong.516 fanhaodong.516 1274544 Aug 19 22:56 main-r-xr-xr-x 1 fanhaodong.516 fanhaodong.516 3513 Aug 19 22:56 main-2.params-r-xr-xr-x 1 fanhaodong.516 fanhaodong.516 674 Aug 19 22:56 main.cppmap-r-xr-xr-x 1 fanhaodong.516 fanhaodong.516 0 Aug 19 22:56 main.dwpdrwxr-xr-x 3 fanhaodong.516 fanhaodong.516 4096 Aug 19 22:56 main.runfiles-r-xr-xr-x 1 fanhaodong.516 fanhaodong.516 162 Aug 19 22:56 main.runfiles_manifestdrwxr-xr-x 3 fanhaodong.516 fanhaodong.516 4096 Aug 19 22:56 _objs\n\n\n开启 fission (opt) 明显能看出来可执行文件大小降低了不少 1274544 -> 396626\n\nls -al bazel-bin/total 1676drwxr-xr-x 5 fanhaodong.516 fanhaodong.516 4096 Aug 19 23:00 .drwxr-xr-x 4 fanhaodong.516 fanhaodong.516 4096 Aug 19 23:00 ..drwxr-xr-x 4 fanhaodong.516 fanhaodong.516 4096 Aug 19 23:00 external-r-xr-xr-x 1 fanhaodong.516 fanhaodong.516 396626 Aug 19 23:00 main-r-xr-xr-x 1 fanhaodong.516 fanhaodong.516 3411 Aug 19 23:00 main-2.params-r-xr-xr-x 1 fanhaodong.516 fanhaodong.516 674 Aug 19 23:00 main.cppmap-r-xr-xr-x 1 fanhaodong.516 fanhaodong.516 1285592 Aug 19 23:00 main.dwpdrwxr-xr-x 3 fanhaodong.516 fanhaodong.516 4096 Aug 19 23:00 main.runfiles-r-xr-xr-x 1 fanhaodong.516 fanhaodong.516 162 Aug 19 23:00 main.runfiles_manifestdrwxr-xr-x 3 fanhaodong.516 fanhaodong.516 4096 Aug 19 23:00 _objs\n\n\n开启fission (dbg) 1274544 -> 834808\n\ntotal 1808drwxr-xr-x 5 fanhaodong.516 fanhaodong.516 4096 Aug 19 23:00 .drwxr-xr-x 4 fanhaodong.516 fanhaodong.516 4096 Aug 19 23:00 ..drwxr-xr-x 4 fanhaodong.516 fanhaodong.516 4096 Aug 19 23:00 external-r-xr-xr-x 1 fanhaodong.516 fanhaodong.516 834808 Aug 19 23:00 main-r-xr-xr-x 1 fanhaodong.516 fanhaodong.516 3529 Aug 19 23:00 main-2.params-r-xr-xr-x 1 fanhaodong.516 fanhaodong.516 674 Aug 19 23:00 main.cppmap-r-xr-xr-x 1 fanhaodong.516 fanhaodong.516 980664 Aug 19 23:00 main.dwpdrwxr-xr-x 3 fanhaodong.516 fanhaodong.516 4096 Aug 19 23:00 main.runfiles-r-xr-xr-x 1 fanhaodong.516 fanhaodong.516 162 Aug 19 23:00 main.runfiles_manifestdrwxr-xr-x 3 fanhaodong.516 fanhaodong.516 4096 Aug 19 23:00 _objs\n\n\n总结 最好别开,支持度不太好,所以看到 --fission 参数 或者 bazelrc 文件中配置了,请立马注释掉关闭了!!!\n\nbazel - .bazelrc\n一旦修改这个文件就得重新构建\n\n# 配置c++版本build --cxxopt=-std=c++14 --host_cxxopt=-std=c++14# 配置 build 的构建参数(当config为linux时)# 可以看上面解释build:linux --features=per_object_debug_info# build:linux --fission=dbg,optbuild:linux --fission=no# --config=linux 继承配置linux的,可以配置多个 --config 。build:clang --config=linux# config=clang时 cc/gcc 为 clang / clang++# --action_env 主要clang 构建时传递的env参数build:clang --action_env=CC=clang --action_env=CXX=clang++# 可配可不配吧,假如clang路径下有ld.lld既不需要配置# build:clang --linkopt=-fuse-ld=lld# config=gcc的构建build:gcc --action_env=CC=gcc --action_env=CXX=g++\n\n参考\nbazel 命令行介绍 https://bazel.build/reference/command-line-reference\nbazel 目录介绍 https://bazel.build/remote/output-directories\nbazelrc 介绍 https://bazel.build/run/bazelrc\nenvoy: https://www.envoyproxy.io/\nenvoy devp: https://github.com/envoyproxy/envoy/tree/v1.26.0/ci\n\n","categories":["Linux","C++"],"tags":["Linux","C++","Envoy"]},{"title":"Thrift 协议讲解","url":"/2022/03/20/1fbc1901406195cf47c58e7436468f2e/","content":"本文主要是介绍 Thrift协议,Thrift 协议发展历史和发展背后的故事!再其次就是介绍如何编解码Thrift协议!如果你也想了解PB,可以看我写的这篇文章: PB协议讲解.\n\n\n整体介绍\nThrift 协议整体设计比较复杂,但是呢语法支持度也比较高,能满足大部分业务需求,完全可以通过编写IDL把所有接口定义能做的事情都做了!这个是区别于PB的,但是PB规范性要好于Thrift 以及整体的生态支持、社区上都是优与Thrift!\n\nThrift 包含了RPC的协议层、序列化层!PB只提供了序列化/编码层,需要GRPC/其他RPC框架协议进行传输!\n\n架构图就不聊了,网上一堆,就一个rpc通信框架其实架构图都一样!下文主要介绍: 消息协议(协议层) + 传输协议(传输层),有兴趣可以看下 Kitex 、GRPC、Dubbo .\n\nThrift 是 facebook 开源的一个rpc协议,诞生时间比较早,应该是10年之前了,所以国内互联网公司基本上都用的thrift协议,原因就是出来的比较早的成熟的RPC框架!但是也存在一个问题就是,古老的协议往往不满足现在的服务架构,所以后期也做了进一步的升级,但是老的业务还在跑,升级比较麻烦,也就导致很多公司Thrift并没有用到新特性!\n\n大家可以看下GO的实现https://github.com/apache/thrift/tree/master/lib/go !\n\nThrift 语法丰富,详细可以看文档:https://thrift.apache.org/docs/ !\n\n\ntypedef string Birthdayconst Birthday NationalDay='1949-10-01'// 其他类型 i64 i32 i16 byte double bool binarystruct TestRequest { 1: string Field_name = 'default value' (api.tag='xxxx'); 2: required string F_string_required; 3: optional string F_string_optional; 4: list<string> F_list_default; 5: map<string,string> F_map_default; 6: set<string> F_set_default; 7: optional Numberz F_enum = Numberz.Unknown,}enum Numberz{ Unknown = 0 ONE = 1 TWO}struct TestResponse { }service ThriftTest{\tTestResponse Test (1: TestRequest req),}\n\n消息(编码)协议主要是详细介绍了消息传输的时候协议的主要编解码方式,注意thrift采用的是大端编码!\n1. TBinaryProtocol消息协议介绍,有两种协议格式!主要是由于历史原因,导致有两种协议并存!下面介绍基参考自官方文档: thrift-binary-protocol\n1. 消息协议一(新编码,严格模式)Binary protocol Message, strict encoding, 12+ bytes:+--------+--------+--------+--------+--------+--------+--------+--------+--------+...+--------+--------+--------+--------+--------+|1vvvvvvv|vvvvvvvv|unused |00000mmm| name length | name | seq id |+--------+--------+--------+--------+--------+--------+--------+--------+--------+...+--------+--------+--------+--------+--------+\n\n\n前两个字节表示版本号:**高位第一个bit固定为1(因为为了区分下面协议二)**,其他17个bit(vvvvvvvvvvvvvvv)表示版本。\n第三个字节为无用字节:unused是一个被忽略的字节。\n第四个字节为消息类型: mmm是消息类型,一个无符号的 3 位整数,同时高位5bit必须为0(有些SDK会校验)。\n\n// 取头部四个字节: size = readInt32()// 取消息类型: size & (1<<8)-1// 取版本号: siez & 0xffff0000, 版本基本就是1,直接硬编码比较就行了// 消息类型占用3bitCall: 1Reply: 2Exception: 3Oneway: 4\n\n\nname length:消息名长度,占用4字节!大端!(注意大小端只是针对于number类型,而且大小端转换基本无开销)\nname: 消息名,utf8编码\nseq id: 占用四字节,大端!(一般rpc多路复用都有,因为要并发发送请求么,不能同一个连接 发送接收完 A 再发送接收 B请求, 像Http1.1就是PingPong协议,HTTP2也是有一个seq id ,这东西做的简单点就全局自增一个id即可!)\n\n\n这里补充下位运算,位运算中 &一般作用就是取值, |一般作用就是Set值!\n\n2. 消息协议二 (旧编码,非严格模式)\n这个假如客户端/server端开启了严格模式,则不能兼容次协议\n\nBinary protocol Message, old encoding, 9+ bytes:+--------+--------+--------+--------+--------+...+--------+--------+--------+--------+--------+--------+| name length | name |00000mmm| seq id |+--------+--------+--------+--------+--------+...+--------+--------+--------+--------+--------+--------+\n\n\nname length(四字节): 这里为了兼容上面协议一,所以高位第一个bit必须为0!也就是name length必须要有符号的正数!\nname:消息名称\nmmm: 消息类型,同上\nseq id: 消息ID\n\n3. 类型编码协议这个协议比较简单,就是个基本的编码协议!!!其实就是一个最简单的 tlv 编码!\n1. structBinary protocol field header and field value:+--------+--------+--------+--------+...+--------+|tttttttt| field id | field value |+--------+--------+--------+--------+...+--------+Binary protocol stop field:+--------+|00000000|+--------+\n\n\ntttttttt字段类型,带符号的 8 位整数。一个字节!\nfield idfield-id,一个大端序的有符号 16 位整数。两个字节!\nfield-value编码的字段值!\n\n# 字段类型BOOL, encoded as 2I8, encoded as 3DOUBLE, encoded as 4I16, encoded as 6I32, encoded as 8I64, encoded as 10BINARY, used for binary and string fields, encoded as 11STRUCT, used for structs and union fields, encoded as 12MAP, encoded as 13SET, encoded as 14LIST, encoded as 15\n\n2. list / setBinary protocol list (5+ bytes) and elements:+--------+--------+--------+--------+--------+--------+...+--------+|tttttttt| size | elements |+--------+--------+--------+--------+--------+--------+...+--------+\n\n\ntttttttt表示元素类型,编码为 int8\nsize 表示全部元素个数,编码为 int32,仅正值\nelements 全部元素,顺序排列\n\n3. mapBinary protocol map (6+ bytes) and key value pairs:+--------+--------+--------+--------+--------+--------+--------+...+--------+|kkkkkkkk|vvvvvvvv| size | key value pairs |+--------+--------+--------+--------+--------+--------+--------+...+--------+\n\n\nkkkkkkkk是关键元素类型,编码为 int8\nvvvvvvvv是值元素类型,编码为 int8\nsize是 map的size,编码为 int32,仅正值\nkey value pairs是编码的键和值,意思就是先读key,再读value,再读key,再读value\n\n4. string/binaryBinary protocol, binary data, 4+ bytes:+--------+--------+--------+--------+--------+...+--------+| byte length | bytes |+--------+--------+--------+--------+--------+...+--------+\n\n\nbyte length是字节数组的长度\nbytes是字节数组的字节\n\nstring 类型就是utf-8 编码为字节流,然后传输的时候用 binary 类型即可! \n5. 其他类型基本类型就是占用固定字节!比如boolean一个字节,i64是8个字节之类的!! 枚举等同于i32!\n2. TCompactProtocol (改进协议,和PB基本类似!)名字显而易见,就是用来压缩的,压缩算法和pb很像,就是 zigzag(整数压缩算法)+ varint 算法, 这俩东西可以看我讲的PB协议的文章 ,不过也做了很多取巧的地方,下面内容基本来自官方文档: thrift-compact-protocol\n1. 消息协议Compact protocol Message (4+ bytes):+--------+--------+--------+...+--------+--------+...+--------+--------+...+--------+|pppppppp|mmmvvvvv| seq id | name length | name |+--------+--------+--------+...+--------+--------+...+--------+--------+...+--------+\n\n这个协议也很简单,就是更紧凑\n\n第一个字节: 协议ID,TCompactProtocol ID为 130,二进制编码为 10000010\n第二个字节: version + type, 其中version是低5bit,type是高3bit , 其中 COMPACT_VERSION = 1\nseq id 4字节,var int 编码\nname len 为4字节,也是var int 编码\nname: 消息名称\n\n// 消息类型Call: 1Reply: 2Exception: 3Oneway: 4\n\n2. 类型编码协议1. structCompact protocol field header (short form) and field value:+--------+--------+...+--------+|ddddtttt| field value |+--------+--------+...+--------+Compact protocol field header (1 to 3 bytes, long form) and field value:+--------+--------+...+--------+--------+...+--------+|0000tttt| field id | field value |+--------+--------+...+--------+--------+...+--------+Compact protocol stop field:+--------+|00000000|+--------+\n\n\ndddd是字段 delta,一个无符号的 4 位整数,严格正数。\ntttt是字段类型 id,一个无符号的 4 位整数。\nfield id字段 id,一个有符号的 16 位整数,编码为 zigzag int!\nfield-value编码的字段值。\n\n# 字段的类型,其中下面的 map/list 类型,也很简单就是可以把BOOLEAN_FALSE当作BOOL类型即可,就不重复写了!BOOLEAN_TRUE, encoded as 1BOOLEAN_FALSE, encoded as 2I8, encoded as 3I16, encoded as 4I32, encoded as 5I64, encoded as 6DOUBLE, encoded as 7BINARY, used for binary and string fields, encoded as 8LIST, encoded as 9SET, encoded as 10MAP, encoded as 11STRUCT, used for both structs and union fields, encoded as 12\n\n这里比较特殊的就是:bool类型是不占用field value 字节的,其次就是有两种编码协议,它的原理确实比pb 还要取巧,哈哈哈!\n首先我们在编码的时候,field_id 一般都是顺序自增的!也就是基本上是1,2,3…n , 这时候由于type占用4bit,此时可以用剩余的4bit存储field_id的增量即可!这里增量处理说实话比PB编码还要巧妙!\n// start last_field_id=0// write fieldif field_id > last_field_id & field_id-last_field_id<=15 { // use delta}else{ // use normal}last_field_id=field_id\n\n例如下面这个例子一个结构体定义的field_id为1,2,3,4,5,30,31,32\n+--------+--------+--------+--------+|0000tttt| field id| field value |+--------+--------+--------+--------+0000tttt, 1 # 1 (懒得用二进制编码表示了)0001tttt, # 2 (这里用0001是因为字段ID增量是1)0001tttt, # 30001tttt, # 40001tttt, # 50000tttt, 30 # 30(这里重制是因为30-5>15了,所以重置了)0001tttt, # 310001tttt, # 32\n\n这里还有个细节点就是 bool 类型!bool 类型是不占用 field_value 的!\n2. list/setCompact protocol list header (1 byte, short form) and elements:+--------+--------+...+--------+|sssstttt| elements |+--------+--------+...+--------+Compact protocol list header (2+ bytes, long form) and elements:+--------+--------+...+--------+--------+...+--------+|1111tttt| size | elements |+--------+--------+...+--------+--------+...+--------+\n\n\nssss是大小,4 位无符号整数,值0-14\ntttt是元素类型,一个 4 位无符号整数\nsize是大小,var int (int32),正值15或更高\nelements是编码元素\n\n这个其实我就不用说了,假如size<=15的话那么可以使用第一种了!\n3. mapCompact protocol map header (1 byte, empty map):+--------+|00000000|+--------+Compact protocol map header (2+ bytes, non empty map) and key value pairs:+--------+...+--------+--------+--------+...+--------+| size |kkkkvvvv| key value pairs |+--------+...+--------+--------+--------+...+--------+\n\n\nsize是大小,一个 var int (int32),严格的正值\nkkkk是关键元素类型,一个 4 位无符号整数\nvvvv是值元素类型,一个 4 位无符号整数\nkey value pairs是编码的键和值\n\n其实这个更不用说了,就是当size等于0时,就写入第一种协议!\n4. binary、stringBinary protocol, binary data, 1+ bytes:+--------+...+--------+--------+...+--------+| byte length | bytes |+--------+...+--------+--------+...+--------+\n\n\nbyte length 采用varint, 四字节编码\nbytes body\n\nstring类型传输的时候是utf8编码的binary类型!\n5. 其他类型占用固定字节,采用varint编码\n消息(传输)协议其实上面部分讲到的消息协议已经是一个完整的协议,你直接基于tcp流发送请求响应协议即可!但是为啥这块还跑出个传输协议!对的上面协议其实是有局限性的!因此下列列出了几个传输协议!下面这块内容基本来自于官方文档: rpc 协议介绍\nFramed 协议 & Bufferd(Unframed) 协议 (传统RPC协议)Bufferd协议就是我们需要不断的读取socket,然后进行协议拆解!\n现在问题就是异步比较流行,所以需要再发送数据的时候采用Framed编码,先写size,再写payload!当server端处理的时候可以根据frame_size来获取payload,然后交给 processs处理!官方的解释是为了支持 async 编程!其实我觉得就是为了更加高效罢了,不需要把整个包解析出来,也就是传输层可以节省很多性能开销!\n\n其实这个协议是有问题的,就是假如消息体过大的话!那么内存开销就比较大,因为需要先写到内存里,然后再头部加四个字节!\nTHeader 协议随着技术/需求/业务不断发展,传统的架构不能满足业务快速发展,普通的传输协议不满足于现状,因此当出现service mesh的新一代微服务架构的时候,需要通过Mesh去做流量治理(而不是框架),我们需要在协议里注入一些服务信息进行流量治理等等,因此后来Facebook推出了THeaderProtocol。其实原来的老协议也能做,那就是可以在 message中定义一些 公共字段来注入流量信息,但是存在问题就是需要把整个包解出来,浪费性能,而且比较麻烦!注意这里讲到的THeaderProtocol是有区别于字节内部的TTHeader 协议的以及Mesh协议等!\n协议如下: \n 0 1 2 3 4 5 6 7 8 9 a b c d e f 0 1 2 3 4 5 6 7 8 9 a b c d e f+----------------------------------------------------------------+| 0| LENGTH |+----------------------------------------------------------------+| 0| HEADER MAGIC | FLAGS |+----------------------------------------------------------------+| SEQUENCE NUMBER |+----------------------------------------------------------------+| 0| Header Size(/32) | ...+--------------------------------- Header is of variable size: (and starts at offset 14)+----------------------------------------------------------------+| PROTOCOL ID (varint) | NUM TRANSFORMS (varint) |+----------------------------------------------------------------+| TRANSFORM 0 ID (varint) | TRANSFORM 0 DATA ...+----------------------------------------------------------------+| ... ... |+----------------------------------------------------------------+| INFO 0 ID (varint) | INFO 0 DATA ...+----------------------------------------------------------------+| ... ... |+----------------------------------------------------------------+| || PAYLOAD || |+----------------------------------------------------------------+\n\n具体细节我就不讲了,其实就是可以携带一些header信息,在传输的时候可以携带上,然后头部有头部编码的协议!\nheader信息可以做些什么呢,它包含一些 trace、acl、env、服务优先级之类的!\n其次就是mesh其实只需要关注于这些信息,payload部分它不关心,所以他不需要解pyload 部分!\n如何解码上面介绍完了,我们发现Thrift 编码协议+传输协议有很多,那么问题是我一个client/server需要全部支持,怎么解码呢,一个消息来了如何解码是麻烦事? 这里我简单介绍一下我的大体实现逻辑!仅供参考:源码\n// buffered readertype reader interface { io.Reader Peek(int) ([]byte, error)}// Unframed: Buffered协议// Framed: Framed协议// Header: 开源的THeader协议// TTHeader: 字节的TTHeader协议// MeshHeader: 字节的MeshHeader协议func GetProtocol(ctx context.Context, reader reader) (Protocol, error) { if IsUnframedHeader(reader, 0) { return UnframedHeader, nil } if IsUnframedHeader(reader, FrameHeaderSize) { return FramedHeader, nil } if IsUnframedBinary(reader, 0) { return UnframedBinary, nil } if IsUnframedBinary(reader, FrameHeaderSize) { return FramedBinary, nil } if IsUnframedUnStrictBinary(reader, 0) { return UnframedUnStrictBinary, nil } if IsUnframedUnStrictBinary(reader, FrameHeaderSize) { return FramedUnStrictBinary, nil } if IsUnframedCompact(reader, 0) { return UnframedCompact, nil } if IsUnframedCompact(reader, FrameHeaderSize) { return FramedCompact, nil } if kitex.IsTTHeader(reader) { meatInfo := GetMateInfo(ctx) size, err := kitex.ReadTTHeader(reader, meatInfo) if err != nil { return UnknownProto, err } if IsUnframedBinary(reader, size) { _ = commons.SkipReader(reader, size) return UnframedBinaryTTHeader, nil } if IsFramedBinary(reader, size) { _ = commons.SkipReader(reader, size) return FramedBinaryTTHeader, nil } } if kitex.IsMeshHeader(reader) { meatInfo := GetMateInfo(ctx) size, err := kitex.ReadMeshHeader(reader, meatInfo) if err != nil { return UnknownProto, err } if IsUnframedBinary(reader, size) { _ = commons.SkipReader(reader, size) return UnframedBinaryMeshHeader, nil } if IsFramedBinary(reader, size) { _ = commons.SkipReader(reader, size) return FramedBinaryMeshHeader, nil } } return UnknownProto, thrift.NewTProtocolExceptionWithType( thrift.UNKNOWN_PROTOCOL_EXCEPTION, errors.New("unknown protocol"), )}\n\n\n参考\nthrift doc \ngo thrift\nthrift\npb 协议讲解\n\n","categories":["RPC"],"tags":["Thrift","GRPC","API"]},{"title":"Cmake学习","url":"/2023/04/23/291f1489dc6255cedb0b626e74c04f9d/","content":"C/C++ 的构建/编译工具有很多,CMake应该属于第三代构建工具,其次个人觉得应该是C++生态领域中最广的,一些新一代的虽好但是生态不行!Cmake-Demo地址: https://github.com/Anthony-Dong/cpp\n\n\n介绍C/C++ 的构建/编译工具有很多,CMake应该属于第三代构建工具,其次个人觉得应该是C++生态领域中最广的,一些新一代的虽好但是生态不行!\n第一代:g++/gcc/clang,最原始了,构建便大点的项目非常痛苦\n第二代:Makefile,通过Makefile语法规则简化了命令\n第三代:Autotools 和 CMake 其实就是省略了自己写Makefile的过程!\n第四代:Bazel 、Blade 集大成者支持任何语言的构建,对于构建缓存支持也好!\n注意:Bazel生态不太行,但是语法比较现代化,不过可以通过 repo_rule 的方式引入cmake项目,但是缺陷就是需要将cmake项目编译成静态库再依赖!\n快速开始\nmain.cpp 文件\n\n#include <iostream>#include <fmt/core.h>int main() { std::cout << "hello world!" << std::endl; fmt::print("hello {}!\\n", "world");}\n\n\nCMakeLists.txt 文件\n\n# 设置CMake最低版本cmake_minimum_required(VERSION 3.8.0)# 设置项目名称project(cmake_demo)## 设置C++版本 和 编译器set(CMAKE_CXX_STANDARD 11)set(CMAKE_CXX_COMPILER g++)## 设置编译选项## add_compile_options(-Wall -Wextra -pedantic -Werror)## 设置全局动态链接库目录link_directories(/usr/local/lib)## 设置target 为一个可执行文件add_executable(${PROJECT_NAME} main.cpp)## 引用的头文件地址target_include_directories(${PROJECT_NAME} PRIVATE /usr/local/include)## 引用的链接库target_link_libraries(${PROJECT_NAME} PUBLIC fmt)\n\n\n执行 cmake . 即可编译\n备注: fmt库可以前往 fmt 官网自行下载构建 https://fmt.dev/latest/usage.html\n\n关键概念其他注意事项可以看: https://github.com/anthony-dong/cpp/blob/master/doc/cmake_notes.md\ntargetCmake的所有操作都是围绕着 target 走了, target 可以是 可执行文件(executable)、静态链接库(static)、动态链接库(shared) !了解了这些你会很方便的理解Camke的编写方式!\n# 定义可执行文件: targe为demoadd_executable(demo make.cpp)# 定义动态链接库: targe为lib_utilsadd_library(lib_utils SHARED utils.cpp)# 定义静态链接库: targe为lib_randomadd_library(${PROJECT_NAME} STATIC random.cpp)\n\ntarget_xxx_directories我建议申明为 PRIVATE,遵循使用再引用的原则\n# 头文件引用地址target_include_directories(${PROJECT_NAME} PRIVATE /usr/local/include)# 动态链接库lookup的地址target_link_directories(${PROJECT_NAME} PRIVATE /usr/local/lib)\n\ntarget_link_libraries这里表示我引用的链接库,这里我建议设置为 PUBLIC ,不然别人引用你的库会发现找不到某个函数的定义,那就尴尬了,报错!\n如果有比较好的规范的话我觉得无所谓!\ntarget_link_directories(${PROJECT_NAME} PRIVATE /usr/local/lib)# 引用的链接库target_link_libraries(${PROJECT_NAME} PUBLIC fmt)\n\n多模块管理 - 本地链接一般情况下,如果你是自己本地学习,本地链接是最方便的因为啥了不需要重复编译哇,一次编译处处使用!\n这里我大概就自己写了个demo,大家自行参考,地址:https://github.com/Anthony-Dong/cmake_demo\n\n学会用cmake自己安装链接库,这里的例子是protoc\n\n 这里其实很简单,首先这些库一般对于cmake都支持,其次就是找到介绍文件,看看它的CamkeLists.txt 文件,最后自己看一下哪些需要哪些不需要,最后自己编译即可!具体可以看: https://github.com/Anthony-Dong/cmake_demo \n\n使用 protoc 库\n\nproject(lib_idl)add_library(${PROJECT_NAME} SHARED model.pb.cc common.pb.cc utils.cpp)## link protobuftarget_include_directories(${PROJECT_NAME} PRIVATE /usr/local/include)target_link_directories(${PROJECT_NAME} PRIVATE /usr/local/lib)target_link_libraries(${PROJECT_NAME} PUBLIC protobuf)\n\n\n定义pb文件,使用protobuf可以省去了自己写序列化函数的时间,执行命令 protoc -I . --cpp_out=. common.proto model.proto\n\n\n注意: protoc版本必须要和链接库版本一致,不然项目编译报错!\n\n// model.protosyntax = "proto2";package model.idl;import "common.proto";enum Gendor { Female = 1; Male = 2;}message People { optional int64 ID = 1; optional string Name = 2; optional Gendor Gendor = 3; repeated string ExtralList = 4; map<string, string> Extra = 5; optional ExtraInfo ExtraInfo = 6; optional Status status = 7;}message ExtraInfo {optional string name = 1;}// common.protosyntax = "proto2";package model.idl;enum Status { On = 0; Off = 1;}\n\n\n主函数\n\n#include <fmt/core.h>#include <idl/model.pb.h>#include <idl/utils.h>#include <db/Class.h++>#include <iostream>int main() { std::cout << "hello world!" << std::endl; fmt::print("hello {}!\\n", "world"); DB::Model::Class a(1, "1314班"); fmt::print("id: {}, name: {}\\n", a.getId(), a.getName()); model::idl::People people{}; people.set_name("tom"); people.set_id(1); fmt::print("people: {}\\n", model::idl::toJson(people)); fmt::print("people: {}\\n", *model::idl::toJsonPtr(people));}\n\n多模块管理 - FetchContentFetchContent 比较现代化吧,比较推荐管理外部模块!\n\nCMakeLists.txt\n\ninclude (FetchContent)if (CMAKE_VERSION VERSION_GREATER_EQUAL "3.24.0") cmake_policy (SET CMP0135 NEW)endif ()FetchContent_Declare ( spdlog URL https://github.com/gabime/spdlog/archive/refs/tags/v1.12.0.tar.gz)FetchContent_MakeAvailable (spdlog)add_library(main main.cpp)target_link_libraries(main PUBLIC spdlog)\n\n\nmain.cpp\n\n#include "spdlog/spdlog.h"int main() { spdlog::info("hello {}", "cmake"); SPDLOG_INFO("hello {}", "cmake");}\n\n多模块管理 - sub_modulesub_module 比较现代化,比较推荐管理子模块,很多cmake项目是全局一个cmakelists.txt 文件维护构建信息,导致庞大。sub_module 可以添加子模块的方式去解决!具体可以参考这个项目: https://github.com/Anthony-Dong/cpp\nadd_subdirectory 仅需要模块目录定义 CMakeLists.txt 文件即可!\n\nCMakeLists.txt\n\ncmake_minimum_required (VERSION 3.11)include (cmake/cc_library.cmake)include (cmake/cc_binary.cmake)include (cmake/cc_test.cmake)add_subdirectory (cpp/io)add_subdirectory (cpp/utils)add_subdirectory (cpp/network)add_subdirectory (cpp/log)add_subdirectory (example)add_subdirectory (test)\n\n\ncpp/io/CMakeLists.txt\n\ncc_library ( NAME cpp_io ALIAS cpp::io SRCS io.cpp HDRS io.h DEPS cpp::utils)cc_test ( NAME cpp_io_test SRCS io_test.cpp DEPS cpp::io)\n\n远程调试远程调试的能力依赖于gdbserver,其次 cmake构建的时候需要指定 -DCMAKE_BUILD_TYPE=Debug \n\nDebug:用于在没有优化的情况下,使用带有调试符号构建库或者可执行文件\n\n~/go/src/github.com/anthony-dong/cmake_demo gdbserver :10086 output/cmake_demoProcess output/cmake_demo created; pid = 149849Listening on port 10086Remote debugging from host 10.78.117.128now 2023-08-18 17:28:22hello world!\n\n2. 效果\n\ncmake 构建命令Makefile 中定义了 build 和 test指令,也是cmake中场景的命令\n.PHONY: all init build test cleanDIR_ROOT := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))CC := clangCXX := clang++CXX_STANDARD := 17C_FLAGS :=CXX_FLAGS := -pthreadBUILD_TYPE := DebugBUILD_DIRECTORY := outputall: buildinit:\tmkdir -p "$(BUILD_DIRECTORY)"\tmkdir -p "$(BUILD_DIRECTORY)/install"\tcmake --log-level=DEBUG -G "Unix Makefiles" -DCMAKE_BUILD_TYPE="$(BUILD_TYPE)" \\\t\t-DCMAKE_INSTALL_PREFIX="$(DIR_ROOT)$(BUILD_DIRECTORY)/install" \\\t\t-DCMAKE_C_COMPILER="$(CC)" \\\t\t-DCMAKE_CXX_COMPILER="$(CXX)" \\\t\t-DCMAKE_C_FLAGS="$(C_FLAGS)" \\\t\t-DCMAKE_CXX_FLAGS="$(CXX_FLAGS)" \\\t\t-DCMAKE_CXX_STANDARD="$(CXX_STANDARD)" \\\t\t-DABSL_PROPAGATE_CXX_STD=ON \\\t\t-S . \\\t\t-B "$(BUILD_DIRECTORY)"build: init\tcmake --build "$(BUILD_DIRECTORY)" --config "$(BUILD_TYPE)" -j8test:\tcd "$(BUILD_DIRECTORY)" && ctest --config "$(BUILD_TYPE)" --tests-regex '_test' -j8clean:\trm -rf "$(BUILD_DIRECTORY)"\n\nBazel代码示例: https://github.com/Anthony-Dong/bazel_demo\n最佳实践:https://anthony-dong.github.io/2023/08/20/1f843b2e9cdc2e9eb8812867e097b659/\nBazel Rule 的话我后期后有问题去写,暂时还没写完!\n","categories":["C++"],"tags":["C++","Cmake"]},{"title":"使用strace抓取uds数据包","url":"/2024/04/18/31d85698cf34ca96a6213eebf903ec0a/","content":"目前很多公司的服务架构都采用了mesh作为服务治理,那么就会引入一个问题,就需要所有的流量都多经历一跳,大部分为了优化性能都会采用UDS作为通信,那么如何抓取uds数据包呢?tcpdump没办法使用,专业点就是使用 bcc ,但是依赖太重了,所以采用 strace 抓取系统调用, 进行实现抓包,就可以完美解决了!\n\n\nuds 服务端代码\nuds 是 unix domain socket,其实就是不实用tcp/ip进行传输,降低一些性能开销,是比较广泛的进程间通信的解决方案!\n\n\n代码\n\npackage mainimport (\t"flag"\t"fmt"\t"net"\t"net/http"\t"os")func main() {\tsocket := flag.String("socket", "", "unix domain socket")\tflag.Parse()\tif socket == nil || *socket == "" {\t\tpanic("not found socket")\t}\t_ = os.Remove(*socket) // 防止文件存在,启动失败\tfmt.Println("pid: ", os.Getpid())\tfmt.Println("listen addr: ", *socket)\tlisten, err := net.Listen("unix", *socket)\tif err != nil {\t\tpanic(err)\t}\tif err := http.Serve(listen, http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {\t\tif _, err := writer.Write([]byte(`hello world`)); err != nil {\t\t\tpanic(err)\t\t}\t})); err != nil {\t\tpanic(err)\t}}\n\n\n启动服务\n\ngo run example/uds/server.go -socket /tmp/tmp.9dd2RLqRnc/uds.socket\n\ncurl 测试请求\n--unix-socket <socket> 用于指定socket地址\nhttp:/xx.xx.xxx/api/v1 格式 <scheme>:/<host><path> , 例如这里 scheme是http, host是xx.xx.xx , path是/api/v1\n\n~ curl --unix-socket /tmp/tmp.9dd2RLqRnc/uds.socket -X GET http:/xx.xx.xxx/api/v1 -vNote: Unnecessary use of -X or --request, GET is already inferred.* Trying /tmp/tmp.9dd2RLqRnc/uds.socket...* Connected to xx.xx.xxx (/tmp/tmp.9dd2RLqRnc/uds.socket) port 80 (#0)> GET /api/v1 HTTP/1.1> Host: xx.xx.xxx> User-Agent: curl/7.64.0> Accept: */*>< HTTP/1.1 200 OK< Date: Thu, 18 Apr 2024 05:28:58 GMT< Content-Length: 11< Content-Type: text/plain; charset=utf-8<* Connection #0 to host xx.xx.xxx left intacthello world\n\n使用 strace 抓包\n抓取系统调用 sudo strace -p 3604685 -tt -f -y -s 65535 \n\n\n-f 参数主要是适用于多线程/多进程\n-tt 会附加时间\n-s <size> 是打印字符串的最大长度,默认很短貌似16个?,这里我设置的是64k\n-p <pid> 标识抓取的进程ID\n-y 表示打印出相关文件的路径,对于 open, connect 等函数,它将显示打开或连接的文件或 socket 的路径\n-o <output> 输出到文件\n-x: print non-ascii strings in hex\n-xx:print all strings in hex,比较适用于抓取二进制数据包,后续做处理!\n\nstrace: Process 3604685 attached with 6 threads[pid 3608084] 14:09:45.676154 epoll_pwait(5<anon_inode:[eventpoll]>, <unfinished ...>[pid 3604689] 14:09:45.676221 futex(0xc000080148, FUTEX_WAIT_PRIVATE, 0, NULL <unfinished ...>[pid 3604687] 14:09:45.676266 futex(0xc000050548, FUTEX_WAIT_PRIVATE, 0, NULL <unfinished ...>[pid 3604686] 14:09:45.676286 restart_syscall(<... resuming interrupted futex ...> <unfinished ...>[pid 3604685] 14:09:45.676320 futex(0x85c368, FUTEX_WAIT_PRIVATE, 0, NULL <unfinished ...>[pid 3604688] 14:09:45.676340 futex(0xc000050948, FUTEX_WAIT_PRIVATE, 0, NULL <unfinished ...>[pid 3608084] 14:09:47.595066 <... epoll_pwait resumed> [{EPOLLIN, {u32=1924628344, u64=139897599393656}}], 128, -1, NULL, 0) = 1[pid 3608084] 14:09:47.595186 futex(0x85c720, FUTEX_WAKE_PRIVATE, 1) = 1[pid 3604686] 14:09:47.595242 <... restart_syscall resumed> ) = 0[pid 3608084] 14:09:47.595255 accept4(3<socket:[1187928144]>, <unfinished ...>[pid 3604686] 14:09:47.595330 epoll_pwait(5<anon_inode:[eventpoll]>, <unfinished ...>[pid 3608084] 14:09:47.595371 <... accept4 resumed> {sa_family=AF_UNIX}, [112->2], SOCK_CLOEXEC|SOCK_NONBLOCK) = 4<socket:[1188331398]>[pid 3604686] 14:09:47.595399 <... epoll_pwait resumed> [], 128, 0, NULL, 0) = 0[pid 3608084] 14:09:47.595462 epoll_ctl(5<anon_inode:[eventpoll]>, EPOLL_CTL_ADD, 4<socket:[1188331398]>, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1924628104, u64=139897599393416}} <unfinished ...>[pid 3604686] 14:09:47.595597 nanosleep({tv_sec=0, tv_nsec=20000}, <unfinished ...>[pid 3608084] 14:09:47.595621 <... epoll_ctl resumed> ) = 0[pid 3608084] 14:09:47.595655 getsockname(4<socket:[1188331398]>, {sa_family=AF_UNIX, sun_path="/tmp/tmp.9dd2RLqRnc/uds.socket"}, [112->33]) = 0[pid 3604686] 14:09:47.595700 <... nanosleep resumed> NULL) = 0[pid 3604686] 14:09:47.595721 nanosleep({tv_sec=0, tv_nsec=20000}, <unfinished ...>[pid 3608084] 14:09:47.595773 futex(0x85c368, FUTEX_WAKE_PRIVATE, 1) = 1[pid 3604685] 14:09:47.595863 <... futex resumed> ) = 0[pid 3608084] 14:09:47.595876 accept4(3<socket:[1187928144]>, <unfinished ...>[pid 3604686] 14:09:47.595901 <... nanosleep resumed> NULL) = 0[pid 3604685] 14:09:47.595914 epoll_pwait(5<anon_inode:[eventpoll]>, <unfinished ...>[pid 3608084] 14:09:47.595939 <... accept4 resumed> 0xc000104b28, [112], SOCK_CLOEXEC|SOCK_NONBLOCK) = -1 EAGAIN (Resource temporarily unavailable)[pid 3604686] 14:09:47.596022 nanosleep({tv_sec=0, tv_nsec=20000}, <unfinished ...>[pid 3604685] 14:09:47.596041 <... epoll_pwait resumed> [{EPOLLIN|EPOLLOUT, {u32=1924628104, u64=139897599393416}}], 128, 0, NULL, 0) = 1[pid 3604685] 14:09:47.596071 epoll_pwait(5<anon_inode:[eventpoll]>, <unfinished ...>[pid 3608084] 14:09:47.596109 read(4<socket:[1188331398]>, <unfinished ...>[pid 3604686] 14:09:47.596135 <... nanosleep resumed> NULL) = 0[pid 3608084] 14:09:47.596155 <... read resumed> "GET /api/v1 HTTP/1.1\\r\\nHost: xx.xx.xxx\\r\\nUser-Agent: curl/7.64.0\\r\\nAccept: */*\\r\\n\\r\\n", 4096) = 79[pid 3604686] 14:09:47.596179 nanosleep({tv_sec=0, tv_nsec=20000}, <unfinished ...>[pid 3608084] 14:09:47.596258 futex(0xc000080148, FUTEX_WAKE_PRIVATE, 1 <unfinished ...>[pid 3604686] 14:09:47.596284 <... nanosleep resumed> NULL) = 0[pid 3608084] 14:09:47.596297 <... futex resumed> ) = 1[pid 3604689] 14:09:47.596309 <... futex resumed> ) = 0[pid 3604686] 14:09:47.596320 nanosleep({tv_sec=0, tv_nsec=20000}, <unfinished ...>[pid 3608084] 14:09:47.596406 write(4<socket:[1188331398]>, "HTTP/1.1 200 OK\\r\\nDate: Thu, 18 Apr 2024 06:09:47 GMT\\r\\nContent-Length: 11\\r\\nContent-Type: text/plain; charset=utf-8\\r\\n\\r\\nhello world", 128 <unfinished ...>[pid 3604689] 14:09:47.596487 nanosleep({tv_sec=0, tv_nsec=3000}, <unfinished ...>[pid 3608084] 14:09:47.596567 <... write resumed> ) = 128[pid 3604686] 14:09:47.596582 <... nanosleep resumed> NULL) = 0[pid 3604685] 14:09:47.596600 <... epoll_pwait resumed> [{EPOLLOUT, {u32=1924628104, u64=139897599393416}}], 128, -1, NULL, 0) = 1[pid 3608084] 14:09:47.596620 read(4<socket:[1188331398]>, <unfinished ...>[pid 3604689] 14:09:47.596634 <... nanosleep resumed> NULL) = 0[pid 3604686] 14:09:47.596647 nanosleep({tv_sec=0, tv_nsec=20000}, <unfinished ...>[pid 3608084] 14:09:47.596659 <... read resumed> 0xc0001b0000, 4096) = -1 EAGAIN (Resource temporarily unavailable)[pid 3604689] 14:09:47.596678 epoll_pwait(5<anon_inode:[eventpoll]>, <unfinished ...>[pid 3608084] 14:09:47.596740 futex(0xc000080548, FUTEX_WAIT_PRIVATE, 0, NULL <unfinished ...>[pid 3604689] 14:09:47.596774 <... epoll_pwait resumed> [{EPOLLIN|EPOLLOUT|EPOLLHUP|EPOLLRDHUP, {u32=1924628104, u64=139897599393416}}], 128, -1, NULL, 0) = 1[pid 3604686] 14:09:47.596807 <... nanosleep resumed> NULL) = 0[pid 3604685] 14:09:47.596816 epoll_pwait(5<anon_inode:[eventpoll]>, <unfinished ...>[pid 3604689] 14:09:47.596907 read(4<socket:[1188331398]>, <unfinished ...>[pid 3604686] 14:09:47.596975 nanosleep({tv_sec=0, tv_nsec=20000}, <unfinished ...>[pid 3604689] 14:09:47.597008 <... read resumed> "", 4096) = 0[pid 3604685] 14:09:47.597022 <... epoll_pwait resumed> [], 128, 0, NULL, 0) = 0[pid 3604689] 14:09:47.597063 epoll_ctl(5<anon_inode:[eventpoll]>, EPOLL_CTL_DEL, 4<socket:[1188331398]>, 0xc00010595c <unfinished ...>[pid 3604686] 14:09:47.597126 <... nanosleep resumed> NULL) = 0[pid 3604689] 14:09:47.597152 <... epoll_ctl resumed> ) = 0[pid 3604686] 14:09:47.597171 nanosleep({tv_sec=0, tv_nsec=20000}, <unfinished ...>[pid 3604689] 14:09:47.597190 close(4<socket:[1188331398]> <unfinished ...>[pid 3604685] 14:09:47.597211 epoll_pwait(5<anon_inode:[eventpoll]>, <unfinished ...>[pid 3604689] 14:09:47.597245 <... close resumed> ) = 0[pid 3604689] 14:09:47.597284 futex(0xc000080148, FUTEX_WAIT_PRIVATE, 0, NULL <unfinished ...>[pid 3604686] 14:09:47.597309 <... nanosleep resumed> NULL) = 0[pid 3604686] 14:09:47.597335 futex(0x85c720, FUTEX_WAIT_PRIVATE, 0, {tv_sec=60, tv_nsec=0}\n\n这里我们就可以看到请求/响应的内容了!\n\n内容太杂了,我们可能仅关注我们需要关注的系统调用,比如 read 、write 、close 等系统调用,可以通过参数\n\n-e read,write,close:这个选项告诉 strace 只跟踪指定的系统调用。在这个例子中,只有 read、write 和 close 这三个系统调用被跟踪。\n我们使用 sudo strace -p 3604685 -tt -f -y -s 65535 -e read,write,close 再抓取一次包呢?\nstrace: Process 3604685 attached with 6 threads[pid 3604685] 14:16:15.263803 read(4<socket:[1188372268]>, "GET /api/v1 HTTP/1.1\\r\\nHost: xx.xx.xxx\\r\\nUser-Agent: curl/7.64.0\\r\\nAccept: */*\\r\\n\\r\\n", 4096) = 79[pid 3604685] 14:16:15.264236 write(4<socket:[1188372268]>, "HTTP/1.1 200 OK\\r\\nDate: Thu, 18 Apr 2024 06:16:15 GMT\\r\\nContent-Length: 11\\r\\nContent-Type: text/plain; charset=utf-8\\r\\n\\r\\nhello world", 128) = 128[pid 3608084] 14:16:15.264523 read(4<socket:[1188372268]>, "", 4096) = 0[pid 3608084] 14:16:15.264702 close(4<socket:[1188372268]>) = 0\n\n这么就清晰许多了!解释一下含义\n# 内容read(4<socket:[1188372268]>, "GET /api/v1 HTTP/1.1\\r\\nHost: xx.xx.xxx\\r\\nUser-Agent: curl/7.64.0\\r\\nAccept: */*\\r\\n\\r\\n", 4096) = 79# read 系统调用,头文件 #include <unistd.h># extern ssize_t read (int __fd, void *__buf, size_t __nbytes) __wur;# 4<socket:[1188372268]> 表示 fd=4# "GET..." 表示__buf内容# 4096 表示 __nbytes# 79 表示读取了多少字节\n\n备注对于 hex 的解析,这里有个简单例子,大家可以看一下!逻辑很简单!\npackage mainimport (\t"encoding/hex"\t"fmt"\t"regexp"\t"strings")var input = `[pid 3604687] 13:44:54.848366 write(4<\\x73\\x6f\\x63\\x6b\\x65\\x74\\x3a\\x5b\\x31\\x31\\x38\\x38\\x30\\x38\\x37\\x30\\x33\\x38\\x5d>, 4<\\x6f\\x6f\\x63\\x6b\\x65\\x74\\x3a\\x5b\\x31\\x31\\x38\\x38\\x30\\x38\\x37\\x30\\x33\\x38\\x5d>, "\\x48\\x54\\x54\\x50\\x2f\\x31\\x2e\\x31\\x20\\x32\\x30\\x30\\x20\\x4f\\x4b\\x0d\\x0a\\x44\\x61\\x74\\x65\\x3a\\x20\\x54\\x68\\x75\\x2c\\x20\\x31\\x38\\x20\\x41\\x70\\x72\\x20\\x32\\x30\\x32\\x34\\x20\\x30\\x35\\x3a\\x34\\x34\\x3a\\x35\\x34\\x20\\x47\\x4d\\x54\\x0d\\x0a\\x43\\x6f\\x6e\\x74\\x65\\x6e\\x74\\x2d\\x4c\\x65\\x6e\\x67\\x74\\x68\\x3a\\x20\\x31\\x31\\x0d\\x0a\\x43\\x6f\\x6e\\x74\\x65\\x6e\\x74\\x2d\\x54\\x79\\x70\\x65\\x3a\\x20\\x74\\x65\\x78\\x74\\x2f\\x70\\x6c\\x61\\x69\\x6e\\x3b\\x20\\x63\\x68\\x61\\x72\\x73\\x65\\x74\\x3d\\x75\\x74\\x66\\x2d\\x38\\x0d\\x0a\\x0d\\x0a\\x68\\x65\\x6c\\x6c\\x6f\\x20\\x77\\x6f\\x72\\x6c\\x64", 128 <unfinished ...>`func hexToString(input string) (string, error) {\tinput = strings.ReplaceAll(input, "\\\\x", "")\tdecodeString, err := hex.DecodeString(input)\tif err != nil {\t\treturn "", err\t}\treturn string(decodeString), nil}func handleSubMatch(input string, subs [][]string) string {\tfor _, sub := range subs {\t\tif len(sub) != 2 {\t\t\tcontinue\t\t}\t\toriginStr := sub[1]\t\tif toStr, err := hexToString(originStr); err == nil {\t\t\tinput = strings.ReplaceAll(input, originStr, toStr)\t\t}\t}\treturn input}func main() {\tmatch1 := regexp.MustCompile(`"([\\\\x0-9a-f]+)"`)\tmatch2 := regexp.MustCompile(`\\d<([\\\\x0-9a-f]+)>`)\tinput = handleSubMatch(input, match1.FindAllStringSubmatch(input, -1))\tinput = handleSubMatch(input, match2.FindAllStringSubmatch(input, -1))\tfmt.Println(input)}","categories":["Linux","抓包","网络"],"tags":["Linux","抓包","网络"]},{"title":"关于容器化的思考","url":"/2020/10/06/33ea9e6f1ad28eb266559efefc7ac6a1/","content":" 我最近半个月内一直在看docker,但是看完后发现,发现它对于我来说只是个cli的工具,cli提供了build,push,pull,run等功能,包含了构建镜像,打包发布,拉取,运行。其实不考虑这些,对于公司级别的cicd工具来说,也是这几个流程,比如说我一个git仓库地址,再通过Jenkins等ci工具构建,构建完成后发布到发布机器上,等我们去发布的时候,就是拉取这个zip包/或者镜像,解压/运行,程序去启动后不在考虑范围内,这个过程是最简单最常见的。\n 所以docker只是提供了一个工具进行这个流程。换了一种承载方式。换句话说它确定了软件究竟应该通过什么样的方式进行交付。docker的创新就是将交付转变为容器/镜像,解决了开发人员的痛点。\n 本篇不讨论,定义和管理容器技术的OpenStack & kubernetes & Docker Swarm & Containerd等!\n ps:学习这些只是看看自己适不适合学习容器化方向的技术,每一个技术背后的技术都很多,如果只是使用,那么了解即可。为啥要学习容器化技术呢,虽然作为一个后端开发,不需要掌握容器化技术,但是了解只是为了思考和成长!\n\n\n1、什么是容器化容器化是应用程序级别的虚拟化,允许单个内核上有多个独立的用户空间实例(它是一个进程,但是进程内部确实一个完整的运行环境)。这些实例称为容器。\n那么虚拟化是什么?虚拟化是指硬件虚拟化,也就是在操作系统(OS)中创建虚拟机。\n虚拟化:虚拟机监视器(Hypervisor)是安装在物理硬件上的软件层,可以将物理机通过虚拟化分成许多虚拟机。这样多个操作系统可以在一个物理硬件上同时运行。安装在虚拟机上的操作系统称为虚拟操作系统,也称为实例。有虚拟机监视器运行的硬件称为主机。虚拟机管理控制台(也称为虚拟机管理员(VMM))是一种计算机软件,可以轻松管理虚拟机。关于虚拟机的分类:https://www.alibabacloud.com/zh/knowledge/what-is-hypervisor\n关于虚拟机的实现,相关讨论:https://www.zhihu.com/question/20848931 \n\n上图是虚拟机与容器的区别!,详细可以看:https://www.alibabacloud.com/zh/knowledge/difference-between-container-and-virtual-machine , 可以发现每个虚拟机都有单独的操作系统,而容器是不需要的。但是容器真的不需要单独的操作系统吗,你还记得dockerfile中每一个镜像都需要制定一个运行环境。那么上面这个图是错误的吗???\n其实很多人应该注意到,这个链接有解释:https://stackoverflow.com/questions/32841982/how-can-docker-run-distros-with-different-kernels\n\nThere’s no kernel inside a container. Even if you install a kernel, it won’t be loaded when the container starts. The very purpose of a container is to isolate processes without the need to run a new kernel.\n\n容器内没有内核。即使您安装了内核,在容器启动时也不会加载该内核。容器的真正目的是在不运行新内核的情况下隔离进程。\n那么容器实现的技术,这里也不详细展开了,主要是依赖于linux系统本身自带的隔离功能!\n\n\n主要核心就是资源隔离(做到容器互不影响很关键),使用技术就是 cgroup 对于cpu、memory的限制,以及namespace等,其实这些技术都是操作系统提供的,对于docker的作者也是基于这些进行实现的,所以docker的成功是可能就是机遇吧,而不是docker的技术!!docker公司前身就是个云服务提供商!\n有兴趣可以看看这篇文章:容器发展简史 以及 容器的隔离与限制, Cgroup介绍\n做云原生开发工程师,需要掌握Kubernetes,Docker,Service Mesh等领域相关的知识,并且有实践,交付经验,道阻且长,本人也是兴趣。\n2、容器化发展历史\n 这一篇主要是介绍,容器化走过这么多年,难道真的是因为docker出生才火的吗,还是时代的趋势!虽然 docker 把容器技术推向了巅峰,但容器技术却不是从 docker 诞生的。\n\n1、容器化发展历史\n1、Chroot Jail就是我们常见的 chroot 命令的用法。它在 1979 年的时候就出现了,被认为是最早的容器化技术之一。它可以把一个进程的文件系统隔离起来。\n2、The FreeBSD JailFreebsd Jail 实现了操作系统级别的虚拟化,它是操作系统级别虚拟化技术的先驱之一。\n3、Linux VServer使用添加到 Linux 内核的系统级别的虚拟化功能实现的专用虚拟服务器。\n4、Solaris Containers它也是操作系统级别的虚拟化技术,专为 X86 和 SPARC 系统设计。Solaris 容器是系统资源控制和通过 “区域” 提供边界隔离的组合。\n5、OpenVZOpenVZ 是一种 Linux 中操作系统级别的虚拟化技术。 它允许创建多个安全隔离的 Linux 容器,即 VPS。\n6、Process ContainersProcess 容器由 Google 的工程师开发,一般被称为 cgroups。\n7、LXC2008年,通过将 Cgroups 的资源管理能力和 Linux Namespace 的视图隔离能力组合在一起,LXC(Linux Container)这样的完整的容器技术出现在了 Linux 内核当中。(0.9一下的低版本的docker就是利用的这个技术!!)\n8、Warden在最初阶段,Warden 使用 LXC 作为容器运行时。 如今已被 CloudFoundy ( VMware 公司于 2011 年宣布了这个项目的开源,第一次对 PaaS 的概念完成了清晰而完整的定义,PaaS 项目通过对应用的直接管理、编排和调度让开发者专注于业务逻辑而非基础设施)取代。\n9、LMCTFYLMCTY 是 Let me contain that for you 的缩写。它是 Google 的容器技术栈的开源版本。Google 的工程师一直在与 docker 的 libertainer 团队合作,并将 libertainer 的核心概念进行抽象并移植到此项目中。该项目的进展不明,估计会被 libcontainer 取代。(0.9以上的高版本的docker就是利用的libcontainer,其实就是对于lxc的封装,用go写的,开发起来比较方便)\n10、Docker\nDocker 是一个可以将应用程序及其依赖打包到几乎可以在任何服务器上运行的容器的工具。(dotCloud公司开源的自己的容器化技术,最后公司直接改名字叫为docker了)\n11、Docker-Swarm\nDocker公司在2014年12月的DockerCon上发布Swarm的举动,你可以很轻松的一个命令,就可以将容器调度在任意一台Swarm集群的机器上。docker的衰败也是因为在容器编排技术之争中跌落神坛的。\n12、Fig\ndocker的大紫大红,后来收购了Fig项目,他可以解决容器之间依赖的问题,也就是当前的docker-compose项目,前身就是Fig。\ndocker成功后,收购了很多好的项目,专门负责处理容器网络的SocketPlane项目,专门负责处理容器存储的Flocker项目,专门给Docker集群做图形化管理界面和对外提供云服务的Tutum项目。\n13、RKTRKT 是 Rocket 的缩写,它是一个专注于安全和开放标准的应用程序容器引擎。(原来docker的合作伙伴CoreOS公司,CoreOS公司自己研发的容器化工具)\n13、终结者Kubernetes\n 2017年, 基础设施领域的翘楚Google公司突然发力,正式宣告了一个名叫Kubernetes项目的诞生。这个项目,不仅挽救了当时的CoreOS和RedHat,还如同当年Docker项目的横空出世一样,再一次改变了整个容器市场的格局。并将 CNCF 这个以“云原生”为关键词的组织和生态推向了巅峰。\n2、容器化的一些名词其实看到这里,我们发现容器化技术发展了这么多年,技术变更这么快的年代,最可怕的是,我学了一门技术,立马被淘汰了,所以docker也是,为了保障docker的标准化,做出了一系列的努力。\n1、Docker & LXCDocker 的第一个执行环境是 LXC,但从版本 0.9 开始 LXC 被 libcontainer 取代。\n2、Docker & libcontainerLibcontainer 为 docker 封装了 Linux 提供的基础功能,如 cgroups,namespaces,netlink 和 netfilter 等\n3、2015 - Docker & runC\n2015 年,docker 发布了 runC,一个轻量级的跨平台的容器运行时。 这基本上就是一个命令行小工具,可以直接利用 libcontainer 运行容器,而无需通过 docker engine。runC 的目标是使标准容器在任何地方都可用。\n4、Docker & The Open Containers Initiative(OCI)OCI 是一个轻量级的开放式管理架构,由 docker,CoreOS 和容器行业的其他领导厂商于 2015 年建立。它维护一些项目,如 runC ,还有容器运行时规范和镜像规范。OCI 的目的是围绕容器行业制定标准,比如使用 docker 创建的容器可以在任何其他容器引擎上运行。\n5、2016 - Docker & containerd\n2016年,Docker 分拆了 containerd ,并将其捐赠给了社区。将这个组件分解为一个单独的项目,使得 docker 将容器的管理功能移出 docker 的核心引擎并移入一个单独的守护进程(即 containerd)。这个组件的剥离很好的实现了开发者可以以编程的方式管理容器!\n6、Docker Components分拆完 containerd 后,docker 各组件的关系如下图所示:\n\n7、Docker 如何运行一个容器?\n\nDocker 引擎创建容器镜像(oci规范)\n将容器映像传递给 containerd\ncontainerd 调用 containerd-shim\ncontainerd-shim 使用 runC 来运行容器\ncontainerd-shim 允许运行时(本例中为 runC)在启动容器后退出\n\n该模型带来的最大好处是在升级 docker 引擎时不会中断容器的运行。\n8、2017 - 容器成为主流\n2017 年是容器成为主流技术的一年,这就是为什么 docker 在 Linux 之外支持众多平台的原因(Docker for Mac,Docker for Windows,Docker for AWS,GCP 等)。\n当容器技术被大众接受后,Docker 公司意识到需要新的生产模型,这就是为什么它开始 Moby 项目。最为go语言开源项目的top3的项目,一开始我也不知道moby项目是做啥了。。。\n其实可以发现,在这个百花齐放的操作系统平台上,如何不进行case by case,重复造轮子,就是拆分,通用组件,现成的直接使用就行了。moby就是这个\n3、容器化做的一些转变主要还是开源出moby项目,https://github.com/moby/moby\n(1)ContainerdContainerd 是 docker 基于行业标准创建的核心容器运行时。它可以用作 Linux 和 Windows 的守护进程,并管理整个容器生命周期。\n(2)LinuxkitLinuxkit 是 Moby 项目中的另一个组件,它是为容器构建安全、跨平台、精简系统的工具。目前已经支持的本地 hypervisor 有 hyper-v 和 vmware。支持的云平台有 AWS、Azure 等。\n(3)InfrakitInfrakit 也是 Moby 项目的一部分。它是创建和管理声明式、不可变和自我修复基础架构的工具包。Infrakit 旨在自动化基础架构的设置和管理,以支持分布式系统和更高级别的容器编排系统。Infrakit 对于像 Docker Swarm 和 Kubernetes 这样的编排工具或跨越 AWS 等公共云创建自动缩放群集的用例很有用。\n(4)LibnetworkLibnetwork 是用 Go 语言实现的容器网络管理项目。它的目标是定义一个容器网络模型(CNM),并为应用程序提供一致的编程接口以及网络抽象。这样就可以满足容器网络的 “可组合” 需求。\n(5)Docker & Docker SwarmDocker Swarm 是一个在 docker 引擎中构建的编排工具。从 docker 1.12 开始它就作为一个独立的工具被原生包含在 docker engine 中。我们可以使用 docker cli 通过 docker swarm 创建群集,并部署和管理应用程序和服务。下图描述了 docker swarm 在 docker 体系中的作用(此图来自互联网):\n(6)Docker&Kubernetes在 docker swarm 与 kubernetes 的竞争中,显然是 kubernetes 占据了优势。所以 docker 紧急掉头,开始原生的支持与 kubernetes 的集成。这可是 2017 年容器界的一大新闻啊!至此,docker 用户和开发人员可以自由地选择使用 kubernetes 或是 swarm 执行容器的编排工作。我们可以认为 docker 与 kubernetes 联姻了。\n\n文章参考: https://www.cnblogs.com/along21/p/9183609.html\n茫茫的容器化技术,不是一篇文章能说清楚的,理解其本质也不知是说,我了解namespcae和cgropus就能说了解的。\n3、容器化技术解决了什么我想很多公司都在使用云服务商提供的虚拟机,其中资源浪费简直是不能看,基本资源利用率在80-90%之间,所以急需一个轻量级的运行时。容器提供了将应用程序的代码、运行时、系统工具、系统库和配置打包到一个实例中的标准方法。容器共享一个内核(操作系统),它安装在硬件上。\n好处:(我觉得很多人都知道)\n轻便容器占用的服务器空间比虚拟机少,通常只需几秒钟即可启动。\n\n弹性容器具有高弹性,不需要分配给定数量的资源。这意味着容器能够更有效地动态使用服务器中的资源。当一个容器上的需求减少时,释放额外的资源供其他容器使用。\n\n密度密度是指一次可以运行单个物理服务器的对象数。容器化允许创建密集的环境,其中主机服务器的资源被充分利用但不被过度利用。与传统虚拟化相比,容器化允许更密集的环境容器不需要托管自己的操作系统。\n\n性能当资源压力很大时,应用程序的性能远远高于使用虚拟机管理程序的容器。因为使用传统的虚拟化,客户操作系统还必须满足其自身的内存需求,从主机上获取宝贵的RAM。\n\n维护效率只有一个操作系统内核,操作系统级别的更新或补丁只需要执行一次,以使更改在所有容器中生效。这使得服务器的操作和维护更加高效。\n\n方便应用程序管理\n容器受益者,主要还是环境隔离,比如一个应用程序,1、需要supervise启动应用程序(守护程序),2、需要logstash/Filebeat收集日志,3、需要nginx/其他Sadicar进行一些服务治理的功能,服务发现/限流/熔断等。4、还需要很多运行环境。所以对于快速发展的公司来说,sre如果还是手动的创建虚拟机,那么和容器带来的时间不是一个量级的。5、解决了开发人员不需要登陆到机子上查看物理机信息了。\n\n省钱,省机器\n虚拟机资源利用率太低了,但是配合k8s编排工具,很好的解决了资源的浪费。但是k8s技术对于公司也是一个挑战,玩不好服务不稳定,带来的损失还是不如走物理机/虚拟机。\n\n\n4、容器化/云原生的技术1、OCI(Open Container Initiative) 标准,隔离了容器镜像与runtine的关系,提供的规范标准,\n2、CNCF全称Cloud Native Computing Foundation(云原生计算基金会),成立于 2015 年7月21日,组织内技术栈:https://www.cncf.io/projects/,致力于:\n\n容器化包装。\n通过中心编排系统的动态资源管理。\n面向微服务。(核心还是切入到真实业务开发中)\n\n3、Linux资源隔离技术了解\n4、Kubernetes熟悉,有Operator扩展或相关产品研发经验优先\n5、 Docker的相关的网络和存储技术,有生产环境的实践\n6、云原生技术栈,Prometheus,Envoy等,更多的还是CNCF的毕业的项目的。\n7、更多的还是 Iass,Pass系统开发者经验,空谈技术不务实,就是最扯淡的。\n5、容器化技术面临的挑战 作为一种轻量级的虚拟化技术,容器使用方便、操作便捷,大大提高开发人员的工作效率,并得到业内的广泛使用。但与此同时,容器安全事故频发,包括不安全的镜像源、容器入侵事件、运行环境的安全问题等等。\n1. 不安全的镜像源 开发者通常会在 Docker 官方的 Docker Hub 仓库下载镜像,这些镜像一部分来源于开发镜像内相应软件的官方组织,还有大量镜像来自第三方组织甚至个人。从这些镜像仓库中获取镜像的同时,也带来潜在的安全风险。例如,下载镜像内软件本身是否就包含漏洞,下载的镜像是否被恶意植入后门,镜像在传输过程中是否被篡改。其次就是容器的构建全过程是对于开发者是可以看到的!\n2. 容器入侵事件 由 docker 本身的架构与机制可能产生的问题,这一攻击场景主要产生在黑客已经控制了宿主机上的一些容器(或者通过在公有云上建立容器的方式获得这个条件),然后对宿主机或其他容器发起攻击来产生影响,这个也就是为什么外网服务不允许使用容器!\n3. 运行环境的安全 除 docker 本身存在的问题外,docker 运行环境存在的问题同样给 docker 的使用带来风险。\n 由于容器是介于基础设施和平台之间的虚拟化技术,因此面向基础设施虚拟化的传统云安全解决方案无法完全解决前述安全问题。如以容器为支撑技术构建 DevOps 环境,就需要设计涵盖从容器镜像的创建到投产上线的整个生命周期的容器安全方案。\n4、性能 现在容器的宿主机往往都是 128G+64C 的组合还有更高的,但是单机的瓶颈还是有的,比如如何解决网络/磁盘IO的性能问题,合理的网络技术实现跨宿主机访问,实现可靠的cicd平台!\n6、总结 最后记住,这些所有的一切,只是为了提高研发效率,保证产研质量!\n可以看看这个PPT:https://docs.qq.com/pdf/DU290V1dSU2NMTHlw\n参考云计算图志\n40 年回顾,一文读懂容器发展史\n为什么说 2019,是属于容器技术的时代?\nCNCF chinal\n命令式和声明式区别\n","categories":["云原生"],"tags":["Docker","容器化"]},{"title":"Golang的GC的回收","url":"/2021/04/01/3a1ab3f1f398e2c69489c00767a5a560/","content":" Golang Gc相关!\n\n\n程序启动\n Go程序启动,首先绝对是初始化一堆资源,关于如何启动需要看Go的转汇编代码了 !\n\n// The bootstrap sequence is:////\tcall osinit//\tcall schedinit//\tmake & queue new G//\tcall runtime·mstart//// The new G calls runtime·main.func schedinit() {\t// raceinit must be the first call to race detector.\t// In particular, it must be done before mallocinit below calls racemapshadow.//...\tgcinit()//...}\n\n首先在一个Go程序启动的时候会调用 https://golang.org/src/runtime/proc.go ,大致启动逻辑就和这个注释上一样,会先启动os,然后schedle,然后再启动\ngc初始化func gcinit() {\tif unsafe.Sizeof(workbuf{}) != _WorkbufSize {\t\tthrow("size of Workbuf is suboptimal")\t}\t// No sweep on the first cycle.\tmheap_.sweepdone = 1\t// Set a reasonable initial GC trigger. 核心关注与这个!就是触发GC回收的阈值,默认是0.875\tmemstats.triggerRatio = 7 / 8.0\t// Fake a heap_marked value so it looks like a trigger at\t// heapminimum is the appropriate growth from heap_marked.\t// This will go into computing the initial GC goal.\tmemstats.heap_marked = uint64(float64(heapminimum) / (1 + memstats.triggerRatio))\t// Set gcpercent from the environment. This will also compute\t// and set the GC trigger and goal.\t_ = setGCPercent(readgogc())\twork.startSema = 1\twork.markDoneSema = 1}\n\n1、triggerRatio Set a reasonable initial GC trigger. 核心关注与这个!就是触发GC回收的阈值,默认是0.875,含义是这次堆中存活的对象是上一次的 1+(7/0.8)值要大的时候就回收\n假如上次完成后堆内存是 100M 现在是 200M,此时 (200M-100M)/100M>7/0.8的,所以需要进行回收!\n具体这个值可以根据GOGC/100 进行设置,根据具体业务来,GOGC = off 将完全禁用垃圾收集\n2、heapminimumheapminimum是触发GC的最小堆大小。 \n在初始化期间,此设置为4MB * GOGC / 100\nGC执行分类gogc 执行会分配下面大致几类 \n\ngcTriggerHeap\ngcTriggerTime\ngcTriggerCycle\n\n\nfunc (t gcTrigger) test() bool {\tif !memstats.enablegc || panicking != 0 || gcphase != _GCoff {\t\treturn false\t}\tswitch t.kind {\tcase gcTriggerHeap:\t\t// Non-atomic access to heap_live for performance. If\t\t// we are going to trigger on this, this thread just\t\t// atomically wrote heap_live anyway and we'll see our\t\t// own write. return memstats.heap_live >= memstats.gc_trigger // heap中存活的对象大于gc需要触发的阈值(这个阈值时上一次gc设置的)\tcase gcTriggerTime:\t\tif gcpercent < 0 {\t\t\treturn false\t\t}\t\tlastgc := int64(atomic.Load64(&memstats.last_gc_nanotime))\t\treturn lastgc != 0 && t.now-lastgc > forcegcperiod // 当前时间与上次gc时间相差2分钟\tcase gcTriggerCycle:\t\t// t.n > work.cycles, but accounting for wraparound.\t\treturn int32(t.n-work.cycles) > 0\t}\treturn true}\n\n1、周期性GCfunc forcegchelper() {\tforcegc.g = getg()\tfor {\t\tlock(&forcegc.lock)\t\tif forcegc.idle != 0 {\t\t\tthrow("forcegc: phase error")\t\t}\t\tatomic.Store(&forcegc.idle, 1)\t\tgoparkunlock(&forcegc.lock, waitReasonForceGGIdle, traceEvGoBlock, 1)\t\t// this goroutine is explicitly resumed by sysmon\t\tif debug.gctrace > 0 {\t\t\tprintln("GC forced")\t\t}\t\t// Time-triggered, fully concurrent.\t\tgcStart(gcTrigger{kind: gcTriggerTime, now: nanotime()})\t}}\n\n可以看到当调用的时候自己给自己加了把锁,然后把自己挂起等待别人唤醒去执行gc,然后看一下 sysmon函数,这个值一般不会改变!\n// forcegcperiod is the maximum time in nanoseconds between garbage// collections. If we go this long without a garbage collection, one// is forced to run.//// This is a variable for testing purposes. It normally doesn't change.var forcegcperiod int64 = 2 * 60 * 1e9// Always runs without a P, so write barriers are not allowed.////go:nowritebarrierrecfunc sysmon() { // 。。。。。。。。\t\t// check if we need to force a GC // 要求第一符合gc周期,第二 forcegc.idle 不为0\t\tif t := (gcTrigger{kind: gcTriggerTime, now: now}); t.test() && atomic.Load(&forcegc.idle) != 0 {\t\t\tlock(&forcegc.lock)\t\t\tforcegc.idle = 0\t\t\tvar list gList\t\t\tlist.push(forcegc.g)\t\t\tinjectglist(&list)\t\t\tunlock(&forcegc.lock)\t\t} ////。。。。。。。。}\n\n2、malloc gc// Allocate an object of size bytes.// Small objects are allocated from the per-P cache's free lists.// Large objects (> 32 kB) are allocated straight from the heap.func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {\t// ........ // 是否需要gc\tshouldhelpgc := false\tif size <= maxSmallSize { // 小于32k\t\tif noscan && size < maxTinySize { //小于16字节\t\t} else {\t\t} else {\t\tvar s *mspan\t\tshouldhelpgc = true\t\tsystemstack(func() {\t\t\ts = largeAlloc(size, needzero, noscan)\t\t})\t\ts.freeindex = 1\t\ts.allocCount = 1\t\tx = unsafe.Pointer(s.base())\t\tsize = s.elemsize\t}\t// 。。。。。。。\tif shouldhelpgc {\t\tif t := (gcTrigger{kind: gcTriggerHeap}); t.test() {\t\t\tgcStart(t)\t\t}\t}\treturn x}\n\n这里可以看到需要判断是否需要执行gc,大致如果分配大于32k的对象时都会去check一下gc\n3、强制GC这个行为一般不推荐用户自己去执行,首先他会强制的阻塞程序!所以不推荐,第二个就是go的GC并不会回收实际分配的物理内存,所以依旧是依赖于系统去强制回收!\n// GC runs a garbage collection and blocks the caller until the// garbage collection is complete. It may also block the entire// program.func GC() {\t// We consider a cycle to be: sweep termination, mark, mark\t// termination, and sweep. This function shouldn't return\t// until a full cycle has been completed, from beginning to\t// end. Hence, we always want to finish up the current cycle\t// and start a new one. That means:\t//\t// 1. In sweep termination, mark, or mark termination of cycle\t// N, wait until mark termination N completes and transitions\t// to sweep N.\t//\t// 2. In sweep N, help with sweep N.\t//\t// At this point we can begin a full cycle N+1.\t//\t// 3. Trigger cycle N+1 by starting sweep termination N+1.\t//\t// 4. Wait for mark termination N+1 to complete.\t//\t// 5. Help with sweep N+1 until it's done.\t//\t// This all has to be written to deal with the fact that the\t// GC may move ahead on its own. For example, when we block\t// until mark termination N, we may wake up in cycle N+2.\t// Wait until the current sweep termination, mark, and mark\t// termination complete.\tn := atomic.Load(&work.cycles)\tgcWaitOnMark(n)\t// We're now in sweep N or later. Trigger GC cycle N+1, which\t// will first finish sweep N if necessary and then enter sweep\t// termination N+1.\tgcStart(gcTrigger{kind: gcTriggerCycle, n: n + 1})\t// Wait for mark termination N+1 to complete.\tgcWaitOnMark(n + 1)// .......}\n\nGC测试1、内存扩容GC测试这里来测试一下 malloc 的方法\npackage mainimport (\t"fmt"\t"time")var (\tappender = make([][]byte, 0, 100))// GODEBUG=gctrace=1func main() {\tticker := time.NewTicker(time.Second * 1)\tcount := 0\talloc := 4 << 20\tfor {\t\t<-ticker.C\t\tappender = append(appender, make([]byte, alloc))\t\tcount++\t\tfmt.Printf("第%d次分配空间: %dm\\n", count, alloc>>20)\t}}\n\n1、执行,配置 GOGC=100 GODEBUG=gctrace=1, 更多关于GODEBUG的配置是 https://golang.org/src/runtime/extern.go\n➜ gc git:(master) ✗ GOGC=100 GODEBUG=gctrace=1 bin/app第1次分配空间: 4mgc 1 @1.001s 0%: 0.010+0.19+0.023 ms clock, 0.12+0.11/0.060/0.11+0.28 ms cpu, 4->4->4 MB, 5 MB goal, 12 P第2次分配空间: 4mgc 2 @2.003s 0%: 0.003+0.082+0.024 ms clock, 0.040+0.060/0.019/0.059+0.29 ms cpu, 8->8->8 MB, 9 MB goal, 12 P第3次分配空间: 4m第4次分配空间: 4mgc 3 @4.005s 0%: 0.021+0.20+0.009 ms clock, 0.25+0.10/0.099/0.12+0.11 ms cpu, 16->16->16 MB, 17 MB goal, 12 P第5次分配空间: 4m第6次分配空间: 4m第7次分配空间: 4m第8次分配空间: 4mgc 4 @8.005s 0%: 0.005+0.19+0.008 ms clock, 0.061+0.10/0.074/0.086+0.10 ms cpu, 32->32->32 MB, 33 MB goal, 12 P第9次分配空间: 4m第10次分配空间: 4m第11次分配空间: 4m第12次分配空间: 4m第13次分配空间: 4m第14次分配空间: 4m第15次分配空间: 4m第16次分配空间: 4mgc 5 @16.005s 0%: 0.015+0.21+0.011 ms clock, 0.18+0.14/0.062/0.15+0.13 ms cpu, 64->64->64 MB, 65 MB goal, 12 P\n\n2、这里可以看到第一次分配就进行了GC,完全符合默认的设置,当内存第二次分配的时候,由于8/4>1进行了gc,那么下一次触发的阈值就会是 16,假如我们将 GOGC=50,继续执行\n➜ gc git:(master) ✗ GOGC=50 GODEBUG=gctrace=1 bin/appgc 第1次分配空间: 4m1 @1.000s 0%: 0.011+0.18+0.016 ms clock, 0.13+0.10/0.024/0.16+0.19 ms cpu, 4->4->4 MB, 5 MB goal, 12 P第2次分配空间: 4mgc 2 @2.002s 0%: 0.006+0.27+0.010 ms clock, 0.078+0.12/0.12/0.12+0.12 ms cpu, 8->8->8 MB, 9 MB goal, 12 P第3次分配空间: 4mgc 3 @3.001s 0%: 0.006+0.21+0.010 ms clock, 0.083+0.10/0.12/0.11+0.12 ms cpu, 12->12->12 MB, 13 MB goal, 12 P第4次分配空间: 4m第5次分配空间: 4mgc 4 @5.005s 0%: 0.006+0.24+0.012 ms clock, 0.080+0.10/0.055/0.22+0.14 ms cpu, 20->20->20 MB, 21 MB goal, 12 P\n\n可以看到第3次gc的内存是12m,原因是上次gc后堆内存为8m,那么下一次就是 8m*1.5=12m,所以完全符合!\n3、假如关闭垃圾回收\n➜ gc git:(master) ✗ GOGC=off GODEBUG=gctrace=1 bin/app第1次分配空间: 4m第2次分配空间: 4m第3次分配空间: 4m第4次分配空间: 4m第5次分配空间: 4m\n\n关于gc日志学习gctrace: setting gctrace=1 causes the garbage collector to emit a single line to standarderror at each collection, summarizing the amount of memory collected and thelength of the pause. The format of this line is subject to change.Currently, it is: gc # @#s #%: #+#+# ms clock, #+#/#/#+# ms cpu, #->#-># MB, # MB goal, # Pwhere the fields are as follows: gc # the GC number, incremented at each GC @#s time in seconds since program start,距离程序的启动时间,单位s #% percentage of time spent in GC since program start,花费时间的百分比 #+...+# wall-clock/CPU times for the phases of the GC, cpu花费的时间 #->#-># MB heap size at GC start, at GC end, and live heap,gc开始-gc结束-存活的对象 (堆内存) # MB goal goal heap size (全局堆内存大小) # P number of processors used p的数量\n\n2、强制gcpackage mainimport (\t"runtime"\t"time")// GOGC=100 GODEBUG=gctrace=1func main() {\t_ = make([]byte, 0, 3<<20)\truntime.GC()\ttime.Sleep(time.Second * 100)}\n\n执行\n➜ gc git:(master) ✗ GOGC=100 GODEBUG=gctrace=1 bin/appgc 1 @0.000s 2%: 0.003+0.11+0.006 ms clock, 0.037+0/0.089/0.10+0.080 ms cpu, 3->3->0 MB, 4 MB goal, 12 P (forced)^C\n\n可以看到结果是堆中最后存活的对象是 0M,可以发现我们申明的那个3m对象被回收,也没有触发系统mem回收!\n3、周期清理这个其实不好测试,因为那个周期值无法做配置!\npackage mainimport (\t"time")// GOGC=100 GODEBUG=gctrace=1func main() {\t_ = make([]byte, 0, 3<<20)\ttime.Sleep(time.Second * 130)}\n\n等待120s的到来!……….. 结果没有\n获取Runtime信息\n 使用prometheus 获取 proc信息,必须是linux/windows,所以mac不能使用\n\n1、mem信息package mainimport (\t"fmt"\t"github.com/prometheus/procfs"\t"os"\t"runtime")// GOGC=100 GODEBUG=gctrace=1func main() {\tmemInfo()\t_ = make([]byte, 0, 3<<20)\tmemInfo()\truntime.GC()\tmemInfo()}func memInfo() {\tstats := runtime.MemStats{}\truntime.ReadMemStats(&stats)\tfmt.Printf("%+v\\n", stats)\tprocMem()}func procMem() {\tp, err := procfs.NewProc(os.Getpid())\tif err != nil {\t\tpanic(err)\t}\tprocStat, err := p.Stat()\tif err != nil {\t\tpanic(err)\t}\tprocStat.ResidentMemory() // 进程所占用的RES\tprocStat.VirtualMemory() // 进程所占用的VIRT\tfmt.Printf("res: %dM, virt: %dM\\n", procStat.ResidentMemory()>>20, procStat.VirtualMemory()>>20)}\n\n执行上面函数以下结果\n➜ gc git:(master) ✗ GOGC=100 GODEBUG=gctrace=1 bin/app{Alloc:158760 TotalAlloc:158760 Sys:69928960 Lookups:0 Mallocs:173 Frees:3 HeapAlloc:158760 HeapSys:66879488 HeapIdle:66535424 HeapInuse:344064 HeapReleased:66469888 HeapObjects:170 StackInuse:229376 StackSys:229376 MSpanInuse:5168 MSpanSys:16384 MCacheInuse:20832 MCacheSys:32768 BuckHashSys:2203 GCSys:2240512 OtherSys:528229 NextGC:4473924 LastGC:0 PauseTotalNs:0 PauseNs:[0 0...] PauseEnd:[0 0 .....] NumGC:0 NumForcedGC:0 GCCPUFraction:0 EnableGC:true DebugGC:false BySize:[{Size:0 Mallocs:0 Frees:0} {Size:8 Mallocs:5 Frees:0} {Size:16 Mallocs:42 Frees:0} ......]}res: 3M, virt: 211M{Alloc:3328688 TotalAlloc:3328688 Sys:69928960 Lookups:0 Mallocs:406 Frees:111 HeapAlloc:3328688 HeapSys:66879488 HeapIdle:63250432 HeapInuse:3629056 HeapReleased:63250432 HeapObjects:295 StackInuse:229376 StackSys:229376 MSpanInuse:6528 MSpanSys:16384 MCacheInuse:20832 MCacheSys:32768 BuckHashSys:2203 GCSys:2240512 OtherSys:528229 NextGC:4473924 LastGC:0 PauseTotalNs:0 PauseNs:[0 0 ......] PauseEnd:[0 0 .......] NumGC:0 NumForcedGC:0 GCCPUFraction:0 EnableGC:true DebugGC:false BySize:[{Size:0 Mallocs:0 Frees:0} {Size:8 Mallocs:6 Frees:0} {Size:16 Mallocs:151 Frees:0} .....]}res: 3M, virt: 211Mgc 1 @0.002s 0%: 0.006+0.17+0.004 ms clock, 0.075+0/0.098/0.20+0.052 ms cpu, 3->3->0 MB, 4 MB goal, 12 P (forced){Alloc:158248 TotalAlloc:3345520 Sys:70256640 Lookups:0 Mallocs:656 Frees:484 HeapAlloc:158248 HeapSys:66781184 HeapIdle:66,396,160 HeapInuse:385,024 HeapReleased:62,996,480 HeapObjects:172 StackInuse:327680 StackSys:327680 MSpanInuse:7072 MSpanSys:16384 MCacheInuse:20832 MCacheSys:32768 BuckHashSys:2203 GCSys:2312192 OtherSys:784229 NextGC:4194304 LastGC:1617196841921944000 PauseTotalNs:10675 PauseNs:[10675 0 0 ......] PauseEnd:[1617196841921944000 0 0 0......] NumGC:1 NumForcedGC:1 GCCPUFraction:0.007580984200182396 EnableGC:true DebugGC:false BySize:[{Size:0 Mallocs:0 Frees:0} {Size:8 Mallocs:6 Frees:1} ........]}res: 3M, virt: 283M\n\n可以看到gc前内存是3m,gc后内存是0m\n\nAlloc 158,760->3,328,688->158,248 (和 HeapAlloc 一样)\nTotalAlloc 158,760-> 3,328,688-> 3,345,520(一共分配的内存)\nSys 69,928,960->69,928,960->70,256,640(系统分配的内存,它表示占用操作系统的全部内存!)\nHeapAlloc 158,760->3,328,688->158,248 (堆分配的内存)\nHeapSys 66,879,488->66,879,488->66,781,184\nHeapIdle 66,535,424->63,250,432->66,396,160(未被使用的span字节树,其实就是未被分配的堆内存,当内存被回收时这个数量会增加回收的内存)\nHeapInuse 344,064->3,629,056->385,024 (正在使用的字节数)\nHeapReleased 66,469,888->63,250,432->62,996,480 (返还给操作系统的内存,它统计了从idle span中返还给操作系统,没有被重新获取的内存大小.)\nPauseNs 表示GC停顿时常, PauseNs[NumGC%256] 表示第多少次GC的时长,记录最近256次的GC!\nNextGC 4,473,924-> 4,473,924 -> 4,194,304, 表示下次GC触发的阈值\nGCSys 229,376->2,240,512->2,312,192\n\n其他指标讲解请看 https://golang.org/src/runtime/mstats.go\n2、监控进程的指标这里需要使用 github.com/prometheus/procfs 包,很好的解决了跨平台问题!\npackage mainimport (\t"github.com/prometheus/procfs"\t"os")func main() {\tp, err := procfs.NewProc(os.Getpid())\tif err != nil {\t\tpanic(err)\t}\tprocStat, err := p.Stat()\tif err != nil {\t\tpanic(err)\t}\tprocStat.ResidentMemory() // 进程所占用的RES\tprocStat.VirtualMemory() // 进程所占用的VIRT}\n\n3、使用prometheus 监控下面是一个线上服务的metrics\n# HELP go_gc_duration_seconds A summary of the pause duration of garbage collection cycles.# TYPE go_gc_duration_seconds summarygo_gc_duration_seconds{quantile="0"} 5.6827e-05go_gc_duration_seconds{quantile="0.25"} 8.1842e-05go_gc_duration_seconds{quantile="0.5"} 9.8818e-05go_gc_duration_seconds{quantile="0.75"} 0.000125499go_gc_duration_seconds{quantile="1"} 0.000555719go_gc_duration_seconds_sum 0.247680951go_gc_duration_seconds_count 2366# HELP go_goroutines Number of goroutines that currently exist.# TYPE go_goroutines gaugego_goroutines 50# HELP go_info Information about the Go environment.# TYPE go_info gaugego_info{version="go1.13.5"} 1# HELP go_memstats_alloc_bytes Number of bytes allocated and still in use.# TYPE go_memstats_alloc_bytes gaugego_memstats_alloc_bytes 8.338104e+06# HELP go_memstats_alloc_bytes_total Total number of bytes allocated, even if freed.# TYPE go_memstats_alloc_bytes_total countergo_memstats_alloc_bytes_total 1.3874634688e+10# HELP go_memstats_buck_hash_sys_bytes Number of bytes used by the profiling bucket hash table.# TYPE go_memstats_buck_hash_sys_bytes gaugego_memstats_buck_hash_sys_bytes 1.922436e+06# HELP go_memstats_frees_total Total number of frees.# TYPE go_memstats_frees_total countergo_memstats_frees_total 8.9915565e+07# HELP go_memstats_gc_cpu_fraction The fraction of this program's available CPU time used by the GC since the program started.# TYPE go_memstats_gc_cpu_fraction gaugego_memstats_gc_cpu_fraction 5.2633836319412915e-06# HELP go_memstats_gc_sys_bytes Number of bytes used for garbage collection system metadata.# TYPE go_memstats_gc_sys_bytes gaugego_memstats_gc_sys_bytes 2.398208e+06# HELP go_memstats_heap_alloc_bytes Number of heap bytes allocated and still in use.# TYPE go_memstats_heap_alloc_bytes gaugego_memstats_heap_alloc_bytes 8.338104e+06# HELP go_memstats_heap_idle_bytes Number of heap bytes waiting to be used.# TYPE go_memstats_heap_idle_bytes gaugego_memstats_heap_idle_bytes 5.1625984e+07# HELP go_memstats_heap_inuse_bytes Number of heap bytes that are in use.# TYPE go_memstats_heap_inuse_bytes gaugego_memstats_heap_inuse_bytes 1.0829824e+07# HELP go_memstats_heap_objects Number of allocated objects.# TYPE go_memstats_heap_objects gaugego_memstats_heap_objects 42405# HELP go_memstats_heap_released_bytes Number of heap bytes released to OS.# TYPE go_memstats_heap_released_bytes gaugego_memstats_heap_released_bytes 4.9709056e+07# HELP go_memstats_heap_sys_bytes Number of heap bytes obtained from system.# TYPE go_memstats_heap_sys_bytes gaugego_memstats_heap_sys_bytes 6.2455808e+07# HELP go_memstats_last_gc_time_seconds Number of seconds since 1970 of last garbage collection.# TYPE go_memstats_last_gc_time_seconds gaugego_memstats_last_gc_time_seconds 1.6172457774344466e+09# HELP go_memstats_lookups_total Total number of pointer lookups.# TYPE go_memstats_lookups_total countergo_memstats_lookups_total 0# HELP go_memstats_mallocs_total Total number of mallocs.# TYPE go_memstats_mallocs_total countergo_memstats_mallocs_total 8.995797e+07# HELP go_memstats_mcache_inuse_bytes Number of bytes in use by mcache structures.# TYPE go_memstats_mcache_inuse_bytes gaugego_memstats_mcache_inuse_bytes 83328# HELP go_memstats_mcache_sys_bytes Number of bytes used for mcache structures obtained from system.# TYPE go_memstats_mcache_sys_bytes gaugego_memstats_mcache_sys_bytes 98304# HELP go_memstats_mspan_inuse_bytes Number of bytes in use by mspan structures.# TYPE go_memstats_mspan_inuse_bytes gaugego_memstats_mspan_inuse_bytes 142528# HELP go_memstats_mspan_sys_bytes Number of bytes used for mspan structures obtained from system.# TYPE go_memstats_mspan_sys_bytes gaugego_memstats_mspan_sys_bytes 196608# HELP go_memstats_next_gc_bytes Number of heap bytes when next garbage collection will take place.# TYPE go_memstats_next_gc_bytes gaugego_memstats_next_gc_bytes 1.0362992e+07# HELP go_memstats_other_sys_bytes Number of bytes used for other system allocations.# TYPE go_memstats_other_sys_bytes gaugego_memstats_other_sys_bytes 5.542772e+06# HELP go_memstats_stack_inuse_bytes Number of bytes in use by the stack allocator.# TYPE go_memstats_stack_inuse_bytes gaugego_memstats_stack_inuse_bytes 4.653056e+06# HELP go_memstats_stack_sys_bytes Number of bytes obtained from system for stack allocator.# TYPE go_memstats_stack_sys_bytes gaugego_memstats_stack_sys_bytes 4.653056e+06# HELP go_memstats_sys_bytes Number of bytes obtained from system.# TYPE go_memstats_sys_bytes gaugego_memstats_sys_bytes 7.7267192e+07# HELP go_threads Number of OS threads created.# TYPE go_threads gaugego_threads 48# HELP process_cpu_seconds_total Total user and system CPU time spent in seconds.# TYPE process_cpu_seconds_total counterprocess_cpu_seconds_total 3875.24# HELP process_max_fds Maximum number of open file descriptors.# TYPE process_max_fds gaugeprocess_max_fds 1.048576e+06# HELP process_open_fds Number of open file descriptors.# TYPE process_open_fds gaugeprocess_open_fds 29# HELP process_resident_memory_bytes Resident memory size in bytes.# TYPE process_resident_memory_bytes gaugeprocess_resident_memory_bytes 7.5575296e+07# HELP process_start_time_seconds Start time of the process since unix epoch in seconds.# TYPE process_start_time_seconds gaugeprocess_start_time_seconds 1.61709350436e+09# HELP process_virtual_memory_bytes Virtual memory size in bytes.# TYPE process_virtual_memory_bytes gaugeprocess_virtual_memory_bytes 2.018103296e+09# HELP process_virtual_memory_max_bytes Maximum amount of virtual memory available in bytes.# TYPE process_virtual_memory_max_bytes gaugeprocess_virtual_memory_max_bytes -1# HELP promhttp_metric_handler_requests_in_flight Current number of scrapes being served.# TYPE promhttp_metric_handler_requests_in_flight gaugepromhttp_metric_handler_requests_in_flight 1# HELP promhttp_metric_handler_requests_total Total number of scrapes by HTTP status code.# TYPE promhttp_metric_handler_requests_total counterpromhttp_metric_handler_requests_total{code="200"} 25373promhttp_metric_handler_requests_total{code="500"} 0promhttp_metric_handler_requests_total{code="503"} 0\n\n核心关注的指标: \n\n 7.5575296e+07 含义是 7.5575296*10^7 ,所以转换为 M,快速计算只需要 / 10^6, 所以就 -6即可,也就是7.5575296e+01 所以就是 75M\n\n\nprocess_resident_memory_bytes - RES 进程所占用的物理内存 (75M)\nprocess_virtual_memory_bytes - VIRT 进程所占用的虚拟内存 (Go的虚拟内存往往很大,2G)\ngo_memstats_heap_alloc_bytes - HeapAlloc 堆内存大小(当前堆实际使用的大小 , 8M)\ngo_memstats_next_gc_bytes - NextGC 表示下次触发GC的阈值 (10M)\ngo_memstats_heap_idle_bytes - HeapIdle 表示堆中空闲的内存( 59M)\ngo_memstats_heap_inuse_bytes - HeapInuse表示正在使用的堆内存 (10M,可能包含有碎片,这个就是实际占用的内存,可以参考的,因为空闲内存可能被回收/未被分配的也是可能实际没有分配)\nprocess_open_fds 打开的文件 (29)\ngo_goroutines 表示 go的goroutine 个数 (50)\ngo_memstats_buck_hash_sys_bytes 表示hash表中的数据 (8M)\n\n下面是我们公司对于Go服务的监控\n这个容器里放的是一个站内信服务,服务部署在容器中4c_4g(宿主机是128G_64C),可以看到24小时内,服务的内存还是相对来说很稳定的,堆内存基本维持在10m-15m左右,gc基本在100us内!\n\n4、使用 net/http/pprof只需要引入 _ "net/http/pprof" \n然后加一行\ngo func() { // 未占用的端口\thttp.ListenAndServe(":8080", nil)}()\n\n1、mem最后执行一下下面的,也就是当前的 runtime.MemStats, 不用说核心关注 HeapAlloc 和 HeapInuse 以及 NextGC , PauseNs , NumGC\n➜ ~ curl http://localhost:8080/debug/pprof/allocs\\?debug\\=1 -v# runtime.MemStats# Alloc = 457531928# TotalAlloc = 672404416# Sys = 556939512# Lookups = 0# Mallocs = 24449# Frees = 23362# HeapAlloc = 457531928# HeapSys = 536248320# HeapIdle = 77873152# HeapInuse = 458375168# HeapReleased = 44040192# HeapObjects = 1087# Stack = 622592 / 622592# MSpan = 29512 / 32768# MCache = 20832 / 32768# BuckHashSys = 1443701# GCSys = 17530880# OtherSys = 1028483# NextGC = 915023280# LastGC = 1617268469532778000# PauseNs = [35105 36373 34839 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]# PauseEnd = [1617268337687730000 1617268338545459000 1617268469532778000 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]# NumGC = 3# NumForcedGC = 0# GCCPUFraction = 1.2248226722456959e-06# DebugGC = false* Connection #0 to host localhost left intact* Closing connection 0\n\n2、goroutine / thread➜ ~ curl http://localhost:8080/debug/pprof/goroutine\\?debug\\=1 -vgoroutine profile: total 6➜ ~ curl http://localhost:8080/debug/pprof/threadcreate\\?debug\\=1 -vthreadcreate profile: total 14\n\n3、配合go tool pprof 命令\n 主要需要加参数 seconds,默认收集30s,下面例子是10s\n\n➜ ~ go tool pprof -http ":8888" http://localhost:62316/debug/pprof/allocs\\?seconds\\=10Fetching profile over HTTP from http://localhost:62316/debug/pprof/allocs?seconds=10Saved profile in /Users/fanhaodong/pprof/pprof.alloc_objects.alloc_space.inuse_objects.inuse_space.011.pb.gzServing web UI on http://localhost:8888\n\n可以看到采集10s最后效果\n\n1、比如查找goroutine 使用率较高!我们首先要查询 个数\n➜ ~ curl http://localhost:62316/debug/pprof/goroutine\\?debug\\=1goroutine profile: total 100\n\n为啥这么多来\n➜ ~ go tool pprof -http ":8888" http://localhost:62316/debug/pprof/goroutine\\?seconds\\=10Fetching profile over HTTP from http://localhost:62316/debug/pprof/goroutine?seconds=10Saved profile in /Users/fanhaodong/pprof/pprof.goroutine.001.pb.gzServing web UI on http://localhost:8888\n\n可以查看csv图,前提是你是在本地,首先如果在线上服务器,显然是不可能的!线上需要执行\n➜ ~ go tool pprof http://localhost:62316/debug/pprof/goroutine\\?seconds\\=\n\n然后去查看\n\n查看trace\n➜ ~ go tool pprof http://localhost:62316/debug/pprof/goroutine\\?seconds\\=5Fetching profile over HTTP from http://localhost:62316/debug/pprof/goroutine?seconds=5(pprof) treeShowing nodes accounting for 102, 98.08% of 104 totalShowing top 80 nodes out of 129----------------------------------------------------------+------------- flat flat% sum% cum cum% calls calls% + context----------------------------------------------------------+------------- 53 51.96% | runtime.selectgo 26 25.49% | runtime.goparkunlock 23 22.55% | runtime.netpollblock 102 98.08% 98.08% 102 98.08% | runtime.gopark----------------------------------------------------------+------------- 0 0% 98.08% 54 51.92% | github.com/apache/rocketmq-client-go/v2/primitive.WithRecover 9 16.67% | github.com/apache/rocketmq-client-go/v2/internal/remote.(*remotingClient).connect.func1 5 9.26% | github.com/apache/rocketmq-client-go/v2/consumer.(*pushConsumer).pullMessage.func1 5 9.26% | github.com/apache/rocketmq-client-go/v2/consumer.(*statsItemSet).init.func1 5 9.26% | github.com/apache/rocketmq-client-go/v2/consumer.(*statsItemSet).init.func2 5 9.26% | github.com/apache/rocketmq-client-go/v2/consumer.(*statsItemSet).init.func3 5 9.26% | github.com/apache/rocketmq-client-go/v2/consumer.(*statsItemSet).init.func4 5 9.26% | github.com/apache/rocketmq-client-go/v2/consumer.(*statsItemSet).init.func5 5 9.26% | github.com/apache/rocketmq-client-go/v2/consumer.(*statsItemSet).init.func6 2 3.70% | github.com/apache/rocketmq-client-go/v2/internal.(*rmqClient).Start.func1.2 2 3.70% | github.com/apache/rocketmq-client-go/v2/internal.(*rmqClient).Start.func1.3 2 3.70% | github.com/apache/rocketmq-client-go/v2/internal.(*rmqClient).Start.func1.4 2 3.70% | github.com/apache/rocketmq-client-go/v2/internal.(*rmqClient).Start.func1.5 2 3.70% | github.com/apache/rocketmq-client-go/v2/internal.(*traceDispatcher).Start.func1----------------------------------------------------------+------------- 5 9.43% | github.com/apache/rocketmq-client-go/v2/consumer.(*statsItemSet).init.func1 5 9.43% | github.com/apache/rocketmq-client-go/v2/consumer.(*statsItemSet).init.func2 5 9.43% | github.com/apache/rocketmq-client-go/v2/consumer.(*statsItemSet).init.func3 5 9.43% | github.com/apache/rocketmq-client-go/v2/consumer.(*statsItemSet).init.func4 5 9.43% | github.com/apache/rocketmq-client-go/v2/internal/remote.(*ResponseFuture).waitResponse 5 9.43% | net/http.(*persistConn).writeLoop 2 3.77% | database/sql.(*DB).connectionOpener 2 3.77% | database/sql.(*DB).connectionResetter 2 3.77% | github.com/apache/rocketmq-client-go/v2/internal.(*rmqClient).Start.func1.2 2 3.77% | github.com/apache/rocketmq-client-go/v2/internal.(*rmqClient).Start.func1.3 2 3.77% | github.com/apache/rocketmq-client-go/v2/internal.(*rmqClient).Start.func1.4 2 3.77% | github.com/apache/rocketmq-client-go/v2/internal.(*rmqClient).Start.func1.5 2 3.77% | github.com/apache/rocketmq-client-go/v2/internal.(*traceDispatcher).process 2 3.77% | github.com/go-sql-driver/mysql.(*mysqlConn).startWatcher.func1 1 1.89% | github.com/SkyAPM/go2sky/reporter/grpc/management.(*managementServiceClient).KeepAlive 1 1.89% | github.com/apache/rocketmq-client-go/v2/consumer.(*pushConsumer).Start.func1.1 1 1.89% | github.com/nacos-group/nacos-sdk-go/common/http_agent.post 0 0% 98.08% 53 50.96% | runtime.selectgo 53 100% | runtime.gopark\n\n\n\n总结Go的内存模型和GC策略总结一下,堆属于Go最大的空间,堆大小基本用不完,对于GC来说(因为分配的是虚拟内存,同时会定期释放内存),Go采用的是能复用即复用,也就是说如果我开辟了3M然后回收了3M下次就复用这空间,其次就是Go的堆内存并不会回收,也就是说如果我某一时刻堆空间开辟了很大的空间,其实对于程序来说,内存并不会回收!\nGo的GC时间主要是根据堆的大小有关,我们线上来说,Go的堆大小基本很小,不到100M,所以GC时长也不会很大!\nGo的GC优化就是能回收的在程序/请求运行结束就立马回收,如果开辟大量内存介意用sync.Pool!\n其次减少GC频率可以在程序初始化的时候先开辟一个和程序稳定运行时大小的一个空间,那么对于Go来说,会减少GC回收的次数,但是GC回收的时间就会增加!\nGo的GC回收时间主要是受机器的CPU限制,cpu越牛逼的机器回收越快!\n备注mem 信息字段// A MemStats records statistics about the memory allocator.type MemStats struct {\t// 常规统计。\t// Alloc 是已分配的堆内存对象占用的内存量(bytes)。\t//\t// 这个值和 基本和HeapAlloc 一致(看下面)。\tAlloc uint64\t// TotalAlloc 是累积的堆内存对象分配的内存量(bytes)。\t//\t// TotalAlloc 会随着堆内存对象分配慢慢增长,但不像 Alloc 和 HeapAlloc,\t// 这个值不会随着对象被释放而缩小。\tTotalAlloc uint64\t// Sys 是从 OS 获得的内存总量(bytes)。\t//\t// Sys 是下面列出的 XSys 字段的综合。Sys 维护着为 Go 运行时预留的虚拟内存空间地址,\t// 里面包含了:堆、栈,以及其他内部数据结构。\tSys uint64\t// Lookups 是 runtime 执行的指针查询的数量。\t//\t// 这主要在针对 runtime 内部进行 debugging 的时候比较有用。\tLookups uint64\t// Mallocs 是累积被分配的堆内存对象数量。\t// 存活堆内存对象数量是 Mallocs - Frees。\tMallocs uint64\t// Frees 是累积被释放掉的堆内存对象数量。\tFrees uint64\t// 堆内存统计。\t//\t// 理解堆内存统计需要一些 Go 是如何管理内存的知识。Go 将堆内存虚拟内存空间以 "spans" 为单位进行分割。\t// spans 是 8K(或更大)的连续内存空间。一个 span 可能会在以下三种状态之一:\t//\t// 一个 "空闲 idle" 的 span 内部不含任何对象或其他数据。\t// 占用物理内存空间的空闲状态 span 可以被释放回 OS(但虚拟内存空间不会),\t// 或者也可以被转化成为 "使用中 in use" 或 "堆栈 stack" 状态。\t//\t// 一个 "使用中 in use" span 包含了至少一个堆内存对象且可能还有富余的空间可以分配更多的堆内存对象。\t//\t// 一个 "堆栈 stack" span 是被用作 goroutine stack 的 内存空间。\t// 堆栈状态的 span 不被视作是堆内存的一部分。一个 span 可以在堆内存和栈内存之间切换;\t// 但不可能同时作为两者。\t// HeapAlloc 是已分配的堆内存对象占用的内存量(bytes)。\t//\t// "已分配"的堆内存对象包含了所有可达的对象,以及所有垃圾回收器已知但仍未回收的不可达对象。\t// 确切的说,HeapAlloc 随着堆内存对象分配而增长,并随着内存清理、不可达对象的释放而缩小。\t// 清理会随着 GC 循环渐进发生,所有增长和缩小这两个情况是同时存在的,\t// 作为结果 HeapAlloc 的变动趋势是平滑的(与传统的 stop-the-world 型垃圾回收器的锯齿状趋势成对比)。\tHeapAlloc uint64\t// HeapSys 是堆内存从 OS 获得的内存总量(bytes)。\t//\t// HeapSys 维护着为堆内存而保留的虚拟内存空间。这包括被保留但尚未使用的虚拟内存空间,\t// 这部分是不占用实际物理内存的,但趋向于缩小,\t// 和那些占用物理内存但后续因不再使用而释放回 OS 的虚拟内存空间一样。(查看 HeapReleased 作为校对)\t//\t// HeapSys 用来评估堆内存曾经到过的最大尺寸。\tHeapSys uint64\t// HeapIdle 是处于"空闲状态(未使用)"的 spans 占用的内存总量(bytes)。\t//\t// 空闲状态的 spans 内部不含对象。这些 spans 可以(并可能已经被)释放回 OS,\t// 或者它们可以在堆内存分配中重新被利用起来,或者也可以被重新作为栈内存利用起来。\t//\t// HeapIdle 减去 HeapReleased 用来评估可以被释放回 OS 的内存总量,\t// 但因为这些内存已经被 runtime 占用了(已经从 OS 申请下来了)所以堆内存可以重新使用这些内存,\t// 就不用再向 OS 申请更多内存了。如果这个差值显著大于堆内存尺寸,这意味着近期堆内存存活对象数量存在一个短时峰值。\tHeapIdle uint64\t// HeapInuse 是处于"使用中"状态的 spans 占用的内存总量(bytes)。\t//\t// 使用中的 spans 内部存在至少一个对象。这些 spans 仅可以被用来存储其他尺寸接近的对象。\t//\t// HeapInuse 减去 HeapAlloc 用来评估被用来存储特定尺寸对象的内存空间的总量,\t// 但目前并没有被使用。这是内存碎片的上界,但通常来说这些内存会被高效重用。\tHeapInuse uint64\t// HeapReleased 是被释放回 OS 的物理内存总量(bytes)。\t//\t// 这个值计算为已经被释放回 OS 的空闲状态的 spans 堆内存空间,且尚未重新被堆内存分配。\tHeapReleased uint64\t// HeapObjects 是堆内存中的对象总量。\t//\t// 和 HeapAlloc 一样,这个值随着对象分配而上涨,随着堆内存清理不可达对象而缩小。\tHeapObjects uint64\t// 栈内存统计。\t//\t// 栈内存不被认为是堆内存的一部分,但 runtime 会将一个堆内存中的 span 用作为栈内存,反之亦然。\t// StackInuse 是栈内存使用的 spans 占用的内存总量(bytes)。\t//\t// 使用中状态的栈内存 spans 其中至少有一个栈内存。这些 spans 只能被用来存储其他尺寸接近的栈内存。\t//\t// 并不存在 StackIdle,因为未使用的栈内存 spans 会被释放回堆内存(因此被计入 HeapIdle)。\tStackInuse uint64\t// StackSys 是栈内存从 OS 获得的内存总量(bytes)。\t//\t// StackSys 是 StackInuse 加上一些为了 OS 线程栈而直接从 OS 获取的内存(应该很小)。\tStackSys uint64\t// 堆外(off-heap)内存统计。\t//\t// 下列的统计信息描述了并不会从堆内存进行分配的运行时内部(runtime-internal)结构体(通常因为它们是堆内存实现的一部分)。\t// 不像堆内存或栈内存,任何这些结构体的内存分配仅只是为这些结构服务。\t//\t// 这些统计信息对 debugging runtime 内存额外开销非常有用。\t// MSpanInuse 是 mspan 结构体分配的内存量(bytes)。\tMSpanInuse uint64\t// MSpanSys 是为 mspan 结构体从 OS 申请过来的内存量(bytes)。\tMSpanSys uint64\t// MCacheInuse 是 mcache 结构体分配的内存量(bytes)。\tMCacheInuse uint64\t// MCacheSys 是为 mcache 结构体从 OS 申请过来的内存量(bytes)。\tMCacheSys uint64\t// BuckHashSys 是用来 profiling bucket hash tables 的内存量(bytes)。\tBuckHashSys uint64\t// GCSys 是在垃圾回收中使用的 metadata 的内存量(bytes)。 \tGCSys uint64\t// OtherSys 是各种各样的 runtime 分配的堆外内存量(bytes)。\tOtherSys uint64\t// 垃圾回收统计。\t// NextGC 是下一次 GC 循环的目标堆内存尺寸。\t//\t// 垃圾回收器的目标是保持 HeapAlloc ≤ NextGC。\t// 在每一轮 GC 循环末尾,下一次循环的目标值会基于当前可达对象数据量以及 GOGC 的值来进行计算。\tNextGC uint64\t// LastGC 是上一次垃圾回收完成的时间,其值为自 1970 年纸巾的 nanoseconds(UNIX epoch)。\tLastGC uint64\t// PauseTotalNs 是自程序启动开始,在 GC stop-the-world 中暂停的累积时长,以 nanoseconds 计数。\t//\t// 在一次 stop-the-world 暂停期间,所有的 goroutines 都会被暂停,仅垃圾回收器在运行。\tPauseTotalNs uint64\t// PauseNs 是最近的 GC stop-the-world 暂停耗时的环形缓冲区(以 nanoseconds 计数)。\t//\t// 最近一次的暂停耗时在 PauseNs[(NumGC+255)%256] 这个位置。\t// 通常来说,PauseNs[N%256] 记录着最近第 N%256th 次 GC 循环的暂停耗时。\t// 在每次 GC 循环中可能会有多次暂停;这是在一次循环中的所有暂停时长的总合。\tPauseNs [256]uint64\t// PauseEnd 是最近的 GC 暂停结束时间的环形缓冲区,其值为自 1970 年纸巾的 nanoseconds(UNIX epoch)。\t//\t// 这个缓冲区的填充方式和 PauseNs 是一致的。\t// 每次 GC 循环可能有多次暂停;这个缓冲区记录的是每个循环的最后一次暂停的结束时间。\tPauseEnd [256]uint64\t// NumGC 是完成过的 GC 循环的数量。\tNumGC uint32\t// NumForcedGC 是应用程序经由调用 GC 函数来强制发起的 GC 循环的数量。\tNumForcedGC uint32\t// GCCPUFraction 是自程序启动以来,应用程序的可用 CPU 时间被 GC 消耗的时长部分。\t//\t// GCCPUFraction 是一个 0 和 1 之间的数字,0 代表 GC 并没有消耗该应用程序的任何 CPU。\t// 一个应用程序的可用 CPU 时间定义为:自应用程序启动以来 GOMAXPROCS 的积分。\t// 举例来说,如果 GOMAXPROCS 是 2 且应用程序已经运行了 10 秒,那么"可用 CPU 时长"就是 20 秒。\t// GCCPUFraction 并未包含写屏障行为消耗的 CPU 时长。\t//\t// 该值和经由 GODEBUG=gctrace=1 报告出来的 CPU 时长是一致的。 \tGCCPUFraction float64\t// EnableGC 显示 GC 是否被启用了。该值永远为真,即便 GOGC=off 被启用。\tEnableGC bool\t// DebugGC 目前并未被使用。\tDebugGC bool\t// BySize 汇报了按大小划分的 span 级别内存分配统计信息。\t//\t// BySize[N] 给出了尺寸 S 对象的内存分配统计信息,尺寸大小是:\t// BySize[N-1].Size < S ≤ BySize[N].Size。\t//\t// 这个结构里的数据并未汇报尺寸大于 BySize[60].Size 的内存分配数据。\tBySize [61]struct {\t\t// Size 是当前尺寸级别可容纳的最大对象的 byte 大小。\t\tSize uint32\t\t// Mallocs 是分配到这个尺寸级别的堆内存对象的累积数量。\t\t// 累积分配的内存容量(bytes)可用:Size*Mallocs 进行计算。\t\t// 当前尺寸级别内存活的对象数量可以用 Mallocs - Frees 进行计算。\t\tMallocs uint64\t\t// Frees 是当前尺寸级别累积释放的堆内存对象的数量。\t\tFrees uint64\t}}\n\n使用初始化内存,减少gc周期测试代码\n 可以看到这个代码很自由初始化240m内存的时候触发了GC,所以后续分配内存没有发生一次GC\n\nvar (\tbuffer = makeArr(240 << 20) // 240m ,意思就是堆2*240M是不会触发gc的)var (\tappender = make([][]byte, 0, 100))// GODEBUG=gctrace=1func main() {\tcount := 0\talloc := 4 << 20\tfor x := 0; x < 100; x++ {\t\tappender = append(appender, makeArr(alloc))\t\tcount++\t\ttime.Sleep(time.Millisecond * 10)\t\t// 到50就回收历史的数据,那么内存达到 480m的时候就会触发gc,所以这个程序结束后内存使用一般是在480m左右\t\tif x == 50 {\t\t\tfor index := range appender {\t\t\t\tappender[index] = nil\t\t\t}\t\t\tprintln(len(appender))\t\t}\t}}func makeArr(len int) []byte {\tfmt.Printf("分配堆内存: %dM\\n", len>>20)\tbytes := make([]byte, len)\tfor index, _ := range bytes {\t\tbytes[index] = 'x'\t}\treturn bytes}\n\n对比一下使用buffer和不使用buffer的gc总时间\n分配buffer的gc次数两次\ngc 1 @0.009s 0%: 0.016+191+0.034 ms clock, 0.064+0.007/0.13/191+0.13 ms cpu, 240->240->240 MB, 241 MB goal, 4 Pgc 2 @1.251s 0%: 0.030+8.8+0.025 ms clock, 0.12+0/0.13/8.6+0.10 ms cpu, 468->468->264 MB, 480 MB goal, 4 P# gc时间283700 302700 =586400 ns\n\n未分配buffer的gc\n# 工9次gc122800 81400 55000 78900 103200 39400 88400 66800 33900 = 669800 ns# 最后一次gcgc 9 @1.830s 0%: 0.016+2.2+0.017 ms clock, 0.066+0/0/2.2+0.069 ms cpu, 188->188->188 MB, 192 MB goal, 4 Pres: 263M, virt: 355M\n\n所以整体来说,gc时间基本一致,但是降低gc次数也是一个不错的选择\n参考https://xenojoshua.com/2019/03/golang-memory/\n","categories":["Golang"],"tags":["Golang","GC"]},{"title":"python学习笔记(基本语法+脚本)","url":"/2021/11/21/3f563fcaf31a49a8ecd1b956a4aa695a/","content":" 个人学习python的笔记!基于 python3 ! 相关文档可以参考: https://docs.python.org/zh-cn/3/library/index.html , 本地可以执行python3 -m pydoc -p 1234 打开文档,python作为世界上最好的语言和万能的胶水语言,只能说太过于完美了!\n\n\n基本语法1. 简单控制语句\n字符串推荐用 '' 单引号引用\n\nlist: List[int] = [1, 2, 3]for elem in list: if elem > 1: print(f'data {elem} > 1') # 这里是format语句,属于语法糖 else: print(f'data {elem} < 1')'''data 1 < 1data 2 > 1data 3 > 1'''\n\n2. 异常x = -1try: if x < 0: raise Exception("Sorry, no numbers below zero")except Exception as err: print("find err: %s" % err)'''find err: Sorry, no numbers below zero''' \n\n3. 推导式(YYDS)\n参考: https://www.cnblogs.com/desireyang/p/12160332.html\n\n\n推导式好处: 效率更高,有点像Java的stream-api,羡慕的要死\n\nlist = [-1,-2,-3,4,5,6]print([elem if elem>0 else -elem for elem in list if elem %2==0])# output [2, 4, 6]\n\n1. 列表推导式一共两种形式:(参考: https://zhuanlan.zhihu.com/p/139621170) , 它主要是输出是列表(list)\n\n[x for x in data if condition] 这里的含义是data只有满足if条件中的情况才保留 (if)\n\n[exp1 if condition else exp2 for x in data] , 这里的含义是data满足if条件时执行exp1 否则 exp2 (if - else)\n\n\nimport re"""获取所有的数字"""list = ["1", "2", "3", "4", "5", "a", "b", "c"]print([elem for elem in list if re.match("\\\\d", elem)])'''['1', '2', '3', '4', '5']'''"""获取所有的字母"""print([elem for elem in list if re.match("[a-z]", elem)])'''['a', 'b', 'c']'''"""如果元素是数字则存储,否则则upper"""print([elem if re.match("\\\\d", elem) else elem.upper() for elem in list])'''['1', '2', '3', '4', '5', 'A', 'B', 'C']'''\n\n最佳实践: 参考(https://github.com/httpie/httpie/blob/master/httpie/core.py#L235)\ndef decode_raw_args( args: List[Union[str, bytes]], stdin_encoding: str) -> List[str]: """ Convert all bytes args to str by decoding them using stdin encoding. """ return [ arg.decode(stdin_encoding) if type(arg) is bytes else arg for arg in args ]def decode_raw_args_parse( args: List[Union[str, bytes]], stdin_encoding: str) -> List[str]: """ Convert all bytes args to str by decoding them using stdin encoding. 不使用推导式 """ result: List[str] = [] for arg in args: if type(arg) is bytes: result.append(arg.decode(stdin_encoding)) else: result.append(arg) return result# arg.decode(stdin_encoding) if type(arg) is bytes else arg for arg in argsprint(decode_raw_args(args=[b'111', b'222'], stdin_encoding="utf-8"))print(decode_raw_args(args=["111", "222"], stdin_encoding=""))'''['111', '222']['111', '222']'''print(decode_raw_args_parse(args=[b'111', b'222'], stdin_encoding="utf-8"))print(decode_raw_args_parse(args=["111", "222"], stdin_encoding=""))'''['111', '222']['111', '222']'''\n\n2. 字典推导式{ key_expr: value_expr for value in collection if condition } ,输出是 dict\n"""{ key_expr: value_expr for value in collection if condition }反转key value,且获取 value 为在set {'a', 'b', 'c'}中的元素"""dict_old = {'a': 'A', 'b': 'B', 'c': 'C', 'd': 'D'}print({dict_old[value]: value for value in dict_old if value in {'a', 'b', 'c'}})'''{'A': 'a', 'B': 'b', 'C': 'c'}'''print({key: value for value, key in dict_old.items() if value in {'a', 'b', 'c'}})'''{'A': 'a', 'B': 'b', 'C': 'c'}'''\n\n3. 集合推导式表达式:\n\n{ expr for value in collection if condition } \n{exp1 if condition else exp2 for x in data}\n\n 输出是 set\n其实就是上面列表推导式 [] 换成 {} ,输出由 list 变成了 set\n4. for 循环 迭代器import osfrom collections.abc import Iterablewith open("text.log", "wt") as file: file.truncate() file.writelines("line 1" + os.linesep) file.writelines("line 2" + os.linesep) file.writelines("line 3" + os.linesep) passwith open("text.log", "rt") as file: for line in file: print("type: {type}, isinstance: {isinstance}, line: {line}".format(type=type(file), isinstance=isinstance(file, Iterable), line=line)) pass'''type: <class '_io.TextIOWrapper'>, isinstance: True, line: line 1type: <class '_io.TextIOWrapper'>, isinstance: True, line: line 2type: <class '_io.TextIOWrapper'>, isinstance: True, line: line 3'''\n\n这里面 _io.TextIOWrapper 实现了 __next__() 方法\n比如我们自己实现一个可迭代的对象\n\n下面可以看到我使用了类型申明 List[str] 其实这个python运行时并不会检测,需要工具进行检测!\n变量默认都是 Any 类型 ,具体可以看 https://docs.python.org/zh-cn/3/library/typing.html\n\nfrom typing import Listclass Items(object): def __init__(self, list: List[str]): self.list = list self.index = 0 def __next__(self, *args, **kwargs): """ next,没有抛出StopIteration """ if self.index >= len(self.list): raise StopIteration result = self.list[self.index] self.index = self.index + 1 return result def __iter__(self, *args, **kwargs): """ 返回一个迭代器 """ return selfdata = Items(["1", "2", "3"])for x in data: print(x)'''123'''\n\n5. 包管理from ..a import foo # 上级目录from .a import foo_a # 当前目录import sys # 引用源码或者libfrom copy import deepcopy # 引用源码或者libfrom pygments.formatters.terminal import TerminalFormatter # 引用 lib.lib.fileimport demo.utils.adef c_foo(): demo.utils.a.foo_a() TerminalFormatter() deepcopy() print(sys.api_version)def b_foo(): foo()\n\n基本数据类型1. 定义方式\nmylist: list[str] = ["apple", "banana", "cherry"]\nmylist=["apple", "banana", "cherry"]\n\n\n\n\nText Type:\nstr\n\n\n\nNumeric Types:\nint, float, complex\n\n\nSequence Types:\nlist, tuple, range\n\n\nMapping Type:\ndict\n\n\nSet Types:\nset, frozenset\n\n\nBoolean Type:\nbool\n\n\nBinary Types:\nbytes, bytearray, memoryview\n\n\n2. 数字基本类型x = 1 # inty = 1.1 # floatz = 1j # 复数(complex)a = complex(1, 2) # 复数(complex)print(type(x))print(type(y))print(type(z))print(z.imag, z.real)print(type(a))print(a.imag, a.real)'''<class 'int'><class 'float'><class 'complex'>1.0 0.0<class 'complex'>2.0 1.0'''\n\n3. 字符串str = "hello"print(str)print(str[0:])print(str[:5])print(str[:-1])print(str[0:5])print(str[0:5:1])print(str[0:5:2])'''hellohellohellohellhellohellohlo'''# formatprint("My name is {} and age is {}".format("tom", 18))'''My name is tom and age is 18'''quantity = 3itemno = 567price = 49.95myorder = "I want to pay {2} dollars for {0} pieces of item {1}."print(myorder.format(quantity, itemno, price))'''I want to pay 49.95 dollars for 3 pieces of item 567.'''# funcstr = "hello world! "print(str.upper())print(str.lower())print(str.strip())print(str + " ...")'''HELLO WORLD! hello world! hello world!hello world! ...'''# formatmyorder = "I have a {carname}, it is a {model}."print(myorder.format(carname="Ford", model="Mustang"))'''I have a Ford, it is a Mustang.'''\n\n4. lambda其实就是一个func\ndef add(num): return lambda x: x + numprint(add(10)(10))'''20'''\n\nlanbda 例子2\nimport jsonclass Obj: def __init__(self): self.name = "tom" self.age = 1print(json.dumps(Obj(), default=lambda obj: obj.__dict__))'''{"name": "tom", "age": 1}'''\n\n集合list, tuple, range, dict, set, frozenset\n\nlist , 例如: mylist = ["apple", "banana", "cherry"]\ntuple 是特殊的数组,就是不能改变, 例如 mytuple = ("apple", "banana", "cherry")\nrange 可以理解是个迭代器, 例如: \ndict 就是个map, 例如: thisdict = {"brand": "Ford", "model": "Mustang", "year": 1964}\nset 就是个去重复的list , 例如: myset = {"apple", "banana", "cherry"}\n\n1. listmylist = ["apple", "banana", "cherry"]# 切片print(mylist[0])print(mylist[2])print(mylist[-1])print(mylist[0:3:2])'''applecherrycherry['apple', 'cherry']'''# 基本操作mylist.append("orange")print(mylist)'''['apple', 'banana', 'cherry', 'orange']'''mylist.insert(0, "mango")print(mylist)'''['mango', 'apple', 'banana', 'cherry', 'orange']'''# 循环for x in mylist: print(x)'''applebananacherryorange'''for index in range(len(mylist)): print("index: %d" % index)'''index: 0index: 1index: 2index: 3index: 4'''if "apple" in mylist: print("success!")'''success!'''# [执行表达式(也就是for循环中的,如果有if则是if中执行的), for item in list 条件表达式]new_list = [elem.upper() for elem in mylist if "a" in elem] # contains 'a' char elem strprint(new_list)'''['MANGO', 'APPLE', 'BANANA', 'ORANGE']'''newList = []for elem in mylist: if 'a' in elem: newList.append(elem.upper())print(newList)'''['MANGO', 'APPLE', 'BANANA', 'ORANGE']'''\n\n2. mapthisdict = {"brand": "Ford", "model": "Mustang", "year": 1964}for key, value in thisdict.items(): print("key: {}, value: {}".format(key, value))'''key: brand, value: Fordkey: model, value: Mustangkey: year, value: 1964'''for key in thisdict: print("key: {}, value: {}".format(key, thisdict[key]))'''key: brand, value: Fordkey: model, value: Mustangkey: year, value: 1964'''\n\n3. range# range 会生成一个迭代器,(start,end,sep) , 左闭右开for x in range(6): # [0,1,2,3,4,5] print("x is %d" % x)'''x is 0x is 1x is 2x is 3x is 4x is 5'''for x in range(2, 6): print("x is %d" % x)'''x is 2x is 3x is 4x is 5'''for x in range(1, 6, 2): print("x is %d" % x)'''x is 1x is 3x is 5'''\n\n函数1. 定义一个函数def func_1(): pass # 空方法必须申明passfunc_1()\n\n2. 参数# name 为必须添的参数,不然为空会报错# age 为默认参数# agrs 为可变参数# kwargs 为 k v 参数def func_1(name, age=1, *args, **kwargs): print("name: %s" % name) print("age: %d" % age) print("len(args): {}, type: {}".format(len(args), type(args))) for value in args: print("args value: {}".format(value)) print("len(kwargs): {}, type: {}".format(len(kwargs), type(kwargs))) for key, value in kwargs.items(): print("kwargs key: {}, value: {}".format(key, value))func_1(name="tom", age=10, args="1", kwargs="2")'''name: tomage: 10len(args): 0len(kwargs): 0, type: <class 'tuple'>len(kwargs): 2, type: <class 'dict'>kwargs key: args, value: 1kwargs key: kwargs, value: 2'''# 这里注意由于dict所以不能申明kvfunc_1("tom", 10, "1", "2", args="1", kwargs="2")'''name: tomage: 10len(args): 2, type: <class 'tuple'>args value: 1args value: 2len(kwargs): 2, type: <class 'dict'>kwargs key: args, value: 1kwargs key: kwargs, value: 2'''\n\n3. 类型\n申明输入输出类型\n\nfrom typing import List, Uniondef decode_raw_args( args: List[Union[str, bytes]], stdin_encoding: str) -> List[str]: """ Convert all bytes args to str by decoding them using stdin encoding. """ return [ arg.decode(stdin_encoding) if type(arg) is bytes else arg for arg in args ]\n\n类1. 定义类和方法# 如果没有父类继承,这里选择 object,比较规范class Person(object): # gender none, male or female gender = "none" # 构造器 def __init__(self, name, age): self.name = name self.age = age def my_name(self): return self.namep = Person(name="tome", age=1)print(p.my_name())\n\n2. 类型的继承import jsonclass Person(object): # gender none, male or female gender = "none" # 构造器 def __init__(self, name, age): self.name = name self.age = age def my_name(self): return self.namep = Person(name="tome", age=1)print(p.my_name())class Mail(Person): def __init__(self, name, age): super(Mail, self).__init__(name, age) self.gender = "mail" def my_name(self): return self.name + "_mail"p = Mail(name="tome", age=1)print(json.dumps(p, default=lambda obj: obj.__dict__))print(p.my_name())\n\n3. 类 __new__ 函数\n主要是__init__ 执行前会调用\n\n#!/usr/bin/pythonimport jsonclass Person(object): def __new__(cls, *args, **kwargs): instance = object.__new__(cls) instance.job = "it" return instance # construct def __init__(self, name, age): self.name = name self.age = age def to_json(self): return json.dumps(self, default=lambda obj: obj.__dict__)p = Person(name="tome", age=1)print(p.to_json()) # {"age": 1, "job": "it", "name": "tome"}\n\n其他用法技巧1. 类型断言if type(1) is int: print("args is int") ... # 等效 pass'''args is int'''\n\n2. 测试可以参考文件: https://segmentfault.com/q/1010000010389542 , 属于doctest\ndef humanize_bytes(n, precision=2): # Author: Doug Latornell # Licence: MIT # URL: https://code.activestate.com/recipes/577081/ """Return a humanized string representation of a number of bytes. >>> humanize_bytes(1) '1 B' >>> humanize_bytes(1024, precision=1) '1.0 kB' >>> humanize_bytes(1024 * 123, precision=1) '123.0 kB' >>> humanize_bytes(1024 * 12342, precision=1) '12.1 MB' >>> humanize_bytes(1024 * 12342, precision=2) '12.05 MB' >>> humanize_bytes(1024 * 1234, precision=2) '1.21 MB' >>> humanize_bytes(1024 * 1234 * 1111, precision=2) '1.31 GB' >>> humanize_bytes(1024 * 1234 * 1111, precision=1) '1.3 GB' """ abbrevs = [ (1 << 50, 'PB'), (1 << 40, 'TB'), (1 << 30, 'GB'), (1 << 20, 'MB'), (1 << 10, 'kB'), (1, 'B') ] if n == 1: return '1 B' for factor, suffix in abbrevs: if n >= factor: break # noinspection PyUnboundLocalVariable return f'{n / factor:.{precision}f} {suffix}'\n\n3. yield\n参考: https://zhuanlan.zhihu.com/p/268605982\n\n其实类似于程序的断电,比如程序运行到那里其实是返回一个生成器,然后当你下一步是才会执行,比较节省内存\nfrom typing import Listdef new(size: int = 1024 * 1024): yield new_data(size)def new_data(size: int) -> List[int]: return [0] * sizedata = new()print(type(data))print(len(next(data))) # 只能执行一次 next不然报错'''<class 'generator'>1048576'''\n\n常用的工具包base64echo "aGVsbG8gcHl0aG9uCg==" | python -c "import sys,base64; print(sys.stdin.read())"->echo "aGVsbG8gcHl0aG9uCg==" | python -c "import sys,base64; print(base64.b64decode(sys.stdin.read()))"-> stdout:b'hello python\\n'\n\n文件操作\nr , w, x ,a四种类型(a: append, w=truncate+create, x=truncate+create if not exit)\nb,t 文件类型\n\n第一列可以和第二列文件类型组合,第一列不允许并存\nimport oswith open("file.log", "w") as file: for x in range(0, 100): file.write("hello world"+os.linesep)with open("file.log","r") as file: for line in file.readlines(): print(line)\n\njsonimport jsonprint(json.dumps({"k1": "v1", "k2": [1, 2, 3]}))print(json.loads('{"k1": "v1", "k2": [1, 2, 3]}'))\n\n如果是class,需要继承 JSONEncoder和JSONDecoder实现子类 ,或者\nimport json, datetimeclass Demo(object): def __init__(self, name: str, age: int, birthday: datetime.date): self.name = name self.agw = age self.birthday = birthday def to_json(self, _): return {"name": self.name, "age": self.agw, "birthday": self.birthday.strftime("%Y-%m-%d")}data = Demo("tom", 18, datetime.date(2001, 1, 1))print(json.dumps(data, default=data.to_json))\n\ntyping (类型限制)官方文档: https://docs.python.org/zh-cn/3/library/typing.html\n可以参考这篇文章: https://sikasjc.github.io/2018/07/14/type-hint-in-python/\n对于喜欢静态类型的语言,我觉得是非常nice的\nfrom typing import Dict, Listdef test(data: Dict[str, str]) -> List[str]: return [x for x in data]print(test({"k1": "v1", "k2": "v2"}))\n\n可变参数 (args&kwargs)可以使用 *args (arguments) 和 **kwargs (keyword arguments) 来定义可变参数\n注意:可变参数解绑需要使用 * 或者 ** 解出来,不然的话就有问题了,所以在参数转发中称为万能转发\nclass People: def __init__(self, name='', age=0, **kwargs) -> None: self.name = name self.age = age self.hobby = '' if 'hobby' not in kwargs else kwargs['hobby'] def __str__(self) -> str: return f'name: {self.name}, age: {self.age}, hobby: {self.hobby}'def NewPeople(*args, **kwargs): # 这个参数模版可以理解为万能转发, 可以转发任何参数 print(f'NewPeople args: {args}, kwargs:{kwargs} ') return People(*args, **kwargs) # 转发参数需要使用*/**反解出来print(People('tom', 18, hobby='swimming'))print(People(name='tom', age=18, hobby='swimming'))print(NewPeople('tom', 18, hobby='swimming'))print(NewPeople(name='tom', age=18, hobby='swimming'))\n\n输出\nname: tom, age: 18, hobby: swimmingname: tom, age: 18, hobby: swimmingNewPeople args: ('tom', 18), kwargs:{'hobby': 'swimming'} name: tom, age: 18, hobby: swimmingNewPeople args: (), kwargs:{'name': 'tom', 'age': 18, 'hobby': 'swimming'} name: tom, age: 18, hobby: swimming\n\nsubprocess (执行命令)使用python执行命令是非常常见的事情,这里我们使用python去处理\n# ignore_security_alert_file RCEimport osimport subprocessdef c_system(): status = os.system("ls -al /data") if status != 0: raise Exception(f"exit: {status}")def test_popen(): a = os.popen("ls -al /data") # proccess opens print(a.read()) # combine stderr & stdout# 推荐使用 subprocessdef test_subprocess(): # https://docs.python.org/zh-cn/3/library/subprocess.html cmd = subprocess.run( ["ls", "-l", "/"], capture_output=True) # 本质上就是封装了subprocess.Popen print(str(cmd.stdout, 'utf-8')) print(str(cmd.stderr, 'utf-8')) print(cmd.returncode)test_subprocess()\n\nargparse 工具python实际上自带了参数解析器,说实话非常的nice,实际上完全够用了,但是写法不太优雅,有兴趣的同学可以看 https://github.com/pallets/click/ 这个库\nimport argparseimport sys# https://docs.python.org/zh-cn/3/library/argparse.htmlif __name__ == '__main__': parser = argparse.ArgumentParser(description='The test command') """ 子命令 """ subparsers = parser.add_subparsers(help='sub-command help') run_parser = subparsers.add_parser('run', description='run cpp files') run_parser.add_argument('--cpp', dest='cpp', required=True, type=str, help='the cpp file') """ 参数 """ parser.add_argument('-c', '--config', dest='config', metavar='config', type=str, required=False, help='The config file') parser.add_argument('-v', '--version', dest='version', action='store_true') parser.add_argument('--num', dest='num', action='append', type=str) # nargs 默认为1 # + 表示最少一个 # ? 表示0或者1 # * 表示无所谓 parser.add_argument('--int', dest='int', nargs='+', type=int) """ 参数组 """ run_parser = parser.add_argument_group( 'test', description='build && run cpp file') run_parser = run_parser.add_mutually_exclusive_group(required=True) run_parser.add_argument('--foo', help='foo help') run_parser.add_argument('--bar', help='bar help') run_parser.add_argument('--cpp', '-cpp', dest='cpp', type=str, help='the cpp file') args = parser.parse_args() print(args) if args.version: print("version: 1.0.0") exit(0)\n\n如何使用了\n➜ vscode git:(master) ✗ python parse_args.py --helpusage: parse_args.py [-h] [-c config] [-v] [--num NUM] [--int INT [INT ...]] [--foo FOO | --bar BAR] {run,build} ...The test commandpositional arguments: {run,build} sub-command helpoptional arguments: -h, --help show this help message and exit -c config, --config config The config file -v, --version --num NUM --int INT [INT ...]group params: test group params --foo FOO foo help --bar BAR bar help ➜ vscode git:(master) ✗ python parse_args.py run --cpp main.cpp test.cppNamespace(config=None, version=False, num=None, int=None, foo=None, bar=None, cpp=['main.cpp', 'test.cpp'])➜ vscode git:(master) ✗ python parse_args.py --version --int 1 2 3 --num 2 --num 3 Namespace(config=None, version=True, num=['2', '3'], int=[1, 2, 3], foo=None, bar=None)version: 1.0.0\n\n\n\n包装器(decorator)decorator 体现了aop的思想,非常的方便,简直是yyds!\n\n打印日志\n\n\nbytedance/byteapi/utils.py\n\nimport timeimport logging# 固定写法 wrapperdef print_run_time_simple(func): def wrapper(*args, **kwargs): start = time.time() ret = func(*args, **kwargs) run_time = time.time() - start if run_time > 0.5: logging.info(f'当前函数 {func.__name__} 耗时: {run_time:.2f}s') return ret return wrapper\n\n\ntests/test_utils.py\n\nfrom bytedance.byteapi import utilsimport logging@utils.print_run_time_simpledef test_print(): logging.info("hello world")\n\n\n输出\n\n(venv) ➜ pytest tests/test_utils.py========================================================== test session starts ==========================================================platform darwin -- Python 3.9.18, pytest-4.6.11, py-1.11.0, pluggy-0.13.1rootdir: /Users/bytedance/python, inifile: pytest.inicollected 1 item tests/test_utils.py::test_print ------------------------------------------------------------- live log call -------------------------------------------------------------2023-12-04 23:41:24 test_utils.py:8 [INFO]: hello world2023-12-04 23:41:24 utils.py:10 [INFO]: 当前函数 test_print 耗时: 0.00sPASSED [100%]\n\n\ndecorator 写法,可以传递参数\n\n\nbytedance/byteapi/utils.py\n\nimport timeimport logging# 固定写法 decorator+wrapperdef print_run_time(cost): def decorator(func): def wrapper(*args, **kwargs): start = time.time() ret = func(*args, **kwargs) run_time = time.time() - start if run_time > cost: logging.info(f'当前函数 {func.__name__} 耗时: {run_time:.2f}s') return ret return wrapper return decorator\n\n\ntests/test_utils.py\n\nfrom bytedance.byteapi import utilsimport loggingimport time@utils.print_run_time(cost=1.5)def test_sleep_1s(): logging.info(f"start sleep: 1s") time.sleep(1) logging.info(f"end sleep: 1s")@utils.print_run_time(cost=1.5)def test_sleep_2s(): logging.info(f"start sleep: 2s") time.sleep(2) logging.info(f"end sleep: 2s")\n\n\n输出,我们发现sleep2s的函数执行的时候命中了耗时打印日志\n\n(venv) ➜ pytest tests/test_utils.py::test_sleep_2s tests/test_utils.py::test_sleep_1s========================================================== test session starts ==========================================================platform darwin -- Python 3.9.18, pytest-4.6.11, py-1.11.0, pluggy-0.13.1rootdir: /Users/bytedance/python, inifile: pytest.inicollected 2 items tests/test_utils.py::test_sleep_2s ------------------------------------------------------------- live log call -------------------------------------------------------------2023-12-04 23:49:42 test_utils.py:23 [INFO]: start sleep: 2s2023-12-04 23:49:44 test_utils.py:25 [INFO]: end sleep: 2s2023-12-04 23:49:44 utils.py:23 [INFO]: 当前函数 test_sleep_2s 耗时: 2.01sPASSED [ 50%]tests/test_utils.py::test_sleep_1s ------------------------------------------------------------- live log call -------------------------------------------------------------2023-12-04 23:49:44 test_utils.py:15 [INFO]: start sleep: 1s2023-12-04 23:49:45 test_utils.py:17 [INFO]: end sleep: 1sPASSED [100%]\n\n__name__ 变量如果在一个包下有多个立即执行代码,切记一定要用这个判断下\nif __name__ == "__main__": main()","categories":["Python"],"tags":["Python"]},{"title":"Java-ThreadLocal和InheritableThreadLocal的局限性以及如何在线程池模型中传递上下文","url":"/2021/03/10/4d9d26e79dde722bc37676b188b2f254/","content":"跨线程使用ThreadLocal 如何做???\n\n\nThreadLocal1、使用@Testpublic void testThreadLocal() { final ThreadLocal<Object> threadLocal = new ThreadLocal<>(); new Thread( () -> { threadLocal.set(Thread.currentThread().getName()); System.out.println("current thread: " + threadLocal.get()); // current thread: thread-1 threadLocal.remove(); } , "thread-1") .start(); new Thread( () -> { threadLocal.set(Thread.currentThread().getName()); System.out.println("current thread: " + threadLocal.get()); // current thread: thread-2 threadLocal.remove(); } , "thread-2") .start();}\n\n执行\ncurrent thread: thread-1current thread: thread-2\n\n可以看到线程是可以拿到 threadlocal中的变量\n2、threadlocal是否跨线程?@Testpublic void testThreadLocalWithChild() { final ThreadLocal<Object> threadLocal = new ThreadLocal<>(); new Thread( () -> { threadLocal.set(Thread.currentThread().getName()); System.out.println("current thread: " + threadLocal.get()); // current thread: thread-1 new Thread(() -> { System.out.println("child thread current thread: " + threadLocal.get()); // child thread current thread: null }, "thread-child-1").start(); threadLocal.remove(); } , "thread-1") .start();}\n\n输出\ncurrent thread: thread-1current thread: thread-2\n\n可以看到跨线程后,我们无法拿到父亲线程的变量,所以thread无法解决跨线程\n3、问题\n使用简单,模型简单,如果普通业务完全满足,对于没有异步操作的业务都满足\n局限性就是多线程异步操作!\n\nInheritableThreadLocal1、简单实用\n 它可以传递父线程的变量\n\n@Testpublic void testInheritableThreadLocal() { final InheritableThreadLocal<Object> threadLocal = new InheritableThreadLocal<>(); threadLocal.set(Thread.currentThread().getName()); Thread thread = new Thread(() -> { System.out.println("current thread: " + threadLocal.get());//current thread: main }); thread.start();}\n\n输出\ncurrent thread: main\n\n可以看到是可以拿到结果的,是如何实现的呢?\n2、InheritableThreadLocal 原理1、OBJECT_INHERITABLE_THREAD_LOCAL.set(Thread.currentThread().getName()); 到底执行了什么\njava.lang.ThreadLocal#set 方法:\npublic void set(T value) {// 获取当前线程为main Thread t = Thread.currentThread();// 去获取当前线程的map -> t.threadLocals 显然是没有,因为没有threadlocal去绑定到main线程里 ThreadLocalMap map = getMap(t); if (map != null) // 有的话,就设置 thradlocal=>value map.set(this, value); else // 去创建一个map createMap(t, value);}\n\njava.lang.InheritableThreadLocal#createMap 重写了 java.lang.ThreadLocal#createMap \n# java.lang.ThreadLocal#createMapvoid createMap(Thread t, T firstValue) { // t.threadLocals 进行赋值,显然是线程捆绑对象罢了 t.threadLocals = new ThreadLocalMap(this, firstValue);}void createMap(Thread t, T firstValue) { t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);}\n\n结论就是,inheritableThreadLocal进行set的时候会像thread线程绑定一个 k-v 对象,k就是 inheritableThreadLocal , v就是我们要的对象,然后如果线程的 inheritableThreadLocals 为空就初始化一下。注意ThreadLocalMap里有趣的WeakReference,值得测试一哈,如果真的内存满了,会回收线程的变量么?\n2、new Thread()\npublic Thread(Runnable target) { init(null, target, "Thread-" + nextThreadNum(), 0);}\n\n继续\nprivate void init(ThreadGroup g, Runnable target, String name, long stackSize) { init(g, target, name, stackSize, null, true);}\n\n继续\nprivate void init(ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc, boolean inheritThreadLocals) { if (name == null) { throw new NullPointerException("name cannot be null"); } this.name = name; Thread parent = currentThread(); \t\t/// ................省略 \t // 如果支持inheritThreadLocals && 父线程的inheritableThreadLocals不为空,显然调用过java.lang.InheritableThreadLocal#set是会进行初始化的 if (inheritThreadLocals && parent.inheritableThreadLocals != null) // 设置当前线程的inheritableThreadLocals为父类的,下面其实就是一个map拷贝 this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals); /* Stash the specified stack size in case the VM cares */ this.stackSize = stackSize; /* Set thread ID */ tid = nextThreadID();}\n\n3、局限性测试1、测试是否支持线程传递\n@Testpublic void testInheritableThreadLocalQuestion() throws InterruptedException { final InheritableThreadLocal<Object> threadLocal = new InheritableThreadLocal<>(); ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(2, 10, 1000000, TimeUnit.SECONDS, new SynchronousQueue<>()); threadPoolExecutor.execute(() -> { // 父线程set一个变量 threadLocal.set("parent"); threadPoolExecutor.execute(() -> { // 子线程去拿,最后移除 System.out.println("child get thread name: " + threadLocal.get()); threadLocal.remove(); }); // 最后父线程执行完毕去删除当前线程变量 threadLocal.remove(); }); if (!threadPoolExecutor.awaitTermination(5, TimeUnit.SECONDS)) { threadPoolExecutor.shutdown(); }}\n\n输出: child get thread name: parent\n这个现象显然是支持线程传递的,因为在传递过程中出现了创建线程,执行第二个execute时出现了线程池没有线程,然后去创建一个,就可以传递了,真实中线程池是基本保活的\n2、再次模拟\n@Testpublic void testInheritableThreadLocalQuestion2() throws InterruptedException { final InheritableThreadLocal<Object> threadLocal = new InheritableThreadLocal<>(); ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(2, 2, 1000000, TimeUnit.SECONDS, new SynchronousQueue<>()); // 初始化线程池中的线程 threadPoolExecutor.execute(() -> { try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } }); threadPoolExecutor.execute(() -> { try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } }); TimeUnit.SECONDS.sleep(2); // 测试 threadPoolExecutor.execute(() -> { threadLocal.set("parent"); threadPoolExecutor.execute(() -> { System.out.println("child get thread name: " + threadLocal.get()); threadLocal.remove(); }); threadLocal.remove(); }); if (!threadPoolExecutor.awaitTermination(5, TimeUnit.SECONDS)) { threadPoolExecutor.shutdown(); }}\n\n输出:\nchild get thread name: null\n\n可以看到输出结果为null,是因为在执行测试任务的第二个execute的时候没有创建线程,也就是没有发生传递\n4、问题\n虽然InheritableThreadLocal可以解决跨线程传递的问题,但是它的局限性就是在于跨线程是必须创建新的线程\n业务中通常使用线程池模型(这里就不做解释了),大多数实现的线程池无法满足调用时线程重复创建,所以无法\n\n线程池如何跨线程传递?参考Golang中的context.Context的实现,我们可以想到如果依赖ThreadLocal 和 InheritableThreadLocal 是不解决问题的,除非我们修改了 ThreadPoolExecutor 的源码,在每次调用的时候,将线程变量传递进去,其实也只能通过 Runnable函数进行传递了,因为对于线程池还是线程来说他们都需要传递 Runnable函数,所以考虑这个通用性最好!\n1、代码实现\n 局限性就是依赖参数传递ThreadLocal,所以后期可以考虑加入多个ThreadLocal 或者 业务中通常依赖于Spring进行管理,所以可以对ThreadLocal进行bean的注入,全局使用spring的bean进行初始化ThreadLocalRunnable,那么参数传递其实只需要一个ApplicationContext\n\nprivate static class ThreadLocalRunnable<T> implements Runnable { private ThreadLocal<T> local; private T args; private Runnable runnable; public ThreadLocalRunnable(ThreadLocal<T> local, Runnable runnable) { if (local == null || runnable == null) { throw new RuntimeException("new ThreadLocalRunnable find args has null arg"); } this.local = local; // 初始化的时候获取绑定的线程变量 this.args = local.get(); this.runnable = runnable; } /** * 被new的线程/线程池中调度的线程调用 */ @Override public void run() { try { // 设置绑定的线程变量 local.set(args); runnable.run(); } finally { // 移除绑定的线程变量 local.remove(); } }}\n\n2、测试代码@Testpublic void testThreadLocalRunnable() throws InterruptedException { final ThreadLocal<Object> threadLocal = new ThreadLocal<>(); ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(2, 2, 1000000, TimeUnit.SECONDS, new SynchronousQueue<>()); // 初始化线程池中的线程 threadPoolExecutor.execute(() -> { try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } }); threadPoolExecutor.execute(() -> { try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } }); TimeUnit.SECONDS.sleep(2); threadLocal.set("my name is main"); // 测试 threadPoolExecutor.execute(new ThreadLocalRunnable<>(threadLocal, () -> { System.out.println("parent get thread name: " + threadLocal.get()); threadLocal.set("my name is parent"); threadPoolExecutor.execute(new ThreadLocalRunnable<>(threadLocal, () -> { System.out.println("child get thread name: " + threadLocal.get()); threadLocal.remove(); })); threadLocal.remove(); })); if (!threadPoolExecutor.awaitTermination(5, TimeUnit.SECONDS)) { threadPoolExecutor.shutdown(); }}\n\n输出\nparent get thread name: my name is mainchild get thread name: my name is parent\n\n3、参考 spring的 ThreadPoolTaskExecutor其实就是一个包装模式的使用,给你一个Runnable,然后你去包装一个Runnable,依靠闭包去实现!\n@Configurationpublic class Config { @Bean public Executor executor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setThreadNamePrefix(ASYNC_EXECUTOR_NAME); executor.setCorePoolSize(5); executor.setMaxPoolSize(5); executor.setQueueCapacity(-1); // 这里在每次异步调用的时候, 会包装一下. executor.setTaskDecorator(runnable -> { // 这个时候还是同步状态 RequestAttributes requestAttributes = RequestContextHolder.currentRequestAttributes(); // 返回的这个 runnable对象 才是去调用线程池. return () -> { try { // 我们set 进去 ,其实是一个ThreadLocal维护的. RequestContextHolder.setRequestAttributes(requestAttributes); runnable.run(); } finally { // 最后记得释放内存 RequestContextHolder.resetRequestAttributes(); } }; }); return executor; }}","categories":["Java"],"tags":["ThreadLocal","InheritableThreadLocal","线程池"]},{"title":"Cgo学习","url":"/2023/05/04/563272b471b5af82d27c85596b1e6342/","content":"Cgo 的诞生是为了继承C/C++积累了半个世纪的软件财富,这样的话我们可以方便的在Go项目中使用这些财富!具体信息可以看官方文档 ,本文会介绍如何使用Cgo,如何将C++项目集成到Go中,有兴趣可以直接看我自己用Cgo写的一个项目,成熟度还可以: https://github.com/anthony-dong/protobuf\n\n\nCgo 真的完美吗Cgo 顾名思义,是C与GO的一个桥梁,但是C与GO的调度模型、内存模型不太一样,就会导致这个桥梁会有一些性能、内存损耗,例如Go的用户代码都跑在goroutine(有栈协程)中,但是C跑在原生的线程中,就会导致要进行一次线程的切换,由 goroutine -> Native-Thread -> goroutine ,所以应该尽量避免使用一些耗时比较长的c程序!\nTODO:后续补充JNI的性能开销!!\n下面我们可以对比一下简单的Go和Cgo差异\npackage test/*int sum_c (int x, int y){\treturn x + y ;}*/import "C"func sum(x, y int) int {\treturn x + y}func sum_c(x, y int) int {\treturn int(C.sum_c(C.int(x), C.int(y)))}\n\n来看下benchmark的结果,\ngoos: linuxgoarch: amd64pkg: github.com/anthony-dong/protobuf/internal/pb_gencpu: Intel(R) Xeon(R) Platinum 8260 CPU @ 2.40GHzBenchmarkSUMBenchmarkSUM-8 \t1000000000\t 0.3557 ns/op\t 0 B/op\t 0 allocs/opBenchmarkSUM-8 \t1000000000\t 0.3567 ns/op\t 0 B/op\t 0 allocs/opBenchmarkSUM-8 \t1000000000\t 0.3626 ns/op\t 0 B/op\t 0 allocs/opBenchmarkSUM-8 \t1000000000\t 0.3588 ns/op\t 0 B/op\t 0 allocs/opBenchmarkSUM-8 \t1000000000\t 0.3540 ns/op\t 0 B/op\t 0 allocs/opBenchmarkSUM_CBenchmarkSUM_C-8 \t14440388\t 79.73 ns/op\t 0 B/op\t 0 allocs/opBenchmarkSUM_C-8 \t15093638\t 85.74 ns/op\t 0 B/op\t 0 allocs/opBenchmarkSUM_C-8 \t14932076\t 85.33 ns/op\t 0 B/op\t 0 allocs/opBenchmarkSUM_C-8 \t14808447\t 79.42 ns/op\t 0 B/op\t 0 allocs/opBenchmarkSUM_C-8 \t13486689\t 78.92 ns/op\t 0 B/op\t 0 allocs/opPASSok \tgithub.com/anthony-dong/protobuf/internal/pb_gen\t8.531s\n\n结论就是 \n\n大概调度上是3个数量级的损耗,差距近千倍!所以CGO不适合做那种简单的业务逻辑处理,如果代码可以很简单的通过Go程序实现,那么原则上不要用CGO去做,除非C性能要远高于GO或者GO去实现太过于麻烦!\nCGO使用原生的Native线程,如果你的C程序耗时比较严重,且并发较高,对于GO程序的影响也会很大!\n注意GO里面可以通过 debug.SetMaxThreads(10) 来设置最大的线程数,但是假如CGO调度的线程不够了,那么会直接程序挂掉,所以不要使用 限制最大线程数的函数,可以通过channel等工具来限制最大并发数量 !\n\n// main.go 文件/*#include <unistd.h>int sum_c (int x, int y){\tsleep(60);\treturn x + y ;}*/import "C"func sum_c(x, y int) int {\treturn int(C.sum_c(C.int(x), C.int(y)))}// main_test.gofunc TestCThread(t *testing.T) {\tt.Log("pid: ", os.Getpid())\tcurrentLock := make(chan bool, 10)\twg := sync.WaitGroup{}\twg.Add(100)\tfor x := 0; x < 100; x++ {\t\tcloneX := x\t\tgo func() {\t\t\tcurrentLock <- true\t\t\tdefer func() {\t\t\t\t<-currentLock\t\t\t\tdefer wg.Done()\t\t\t}()\t\t\tt.Log("sum-start: ", cloneX)\t\t\ts := sum_c(cloneX, cloneX+1)\t\t\tt.Log("sum-done: ", cloneX, s)\t\t}()\t}\twg.Wait()}// ps -mq ${pid} | wc -l\n\n熟悉Cgo基本写法例子代码分为两部分,一部分是C代码(注意: 必须是C,不能是C++),一部分是Go代码,其次一定要 import "C"\npackage main/*#include <stdlib.h>#include <stdio.h>#include <string.h>void c_print_str(const char* str){\tprintf("c_print_str: %s\\n",str);}void c_print_str_size(const char* str,int len){\tprintf("c_print_str_size: ");\tfor (int x=0; x<len; x++){\t\tprintf("%c",*str);\t\tstr=str+1;\t}\tprintf("\\n");}int c_str_len(const char* str){\treturn strlen(str);}const char* c_new_str() { char* str = (char*)malloc(5 * sizeof(char)); str[0] = 'a'; str[1] = '\\0'; str[2] = 'b'; str[3] = '\\0'; str[4] = 'c'; return str;}typedef struct __CStruct { char* name; int age;} CStruct;void print_CStruct(CStruct* ss) { printf("name: %s, age: %d\\n", ss->name, ss->age);}*/import "C"import (\t"fmt"\t"unsafe")func main() {\t{\t\t// C语言中认为'\\0'是一个字符串的结尾符,也就是说字符串会额外多一个size(char)来存储'\\0',但是Go语言不是!\t\tgstr := "hello world\\u00001111"\t\t// 创建一个C的 char* 字符串,这里会涉及到一次内存的拷贝,原因是为了安全,同时你也需要free掉!\t\tcstr := C.CString(gstr)\t\tdefer C.free(unsafe.Pointer(cstr))\t\t// 调用C函数\t\tC.c_print_str(cstr)\t\tC.c_print_str_size(cstr, C.int(len(gstr)))\t\t// 注意: C中基本类型转换Go直接强转即可,最好转成对应类型\t\tfmt.Println("int(C.c_str_len(cstr)): ", int(C.c_str_len(cstr)))\t\tfmt.Println("len(gstr): ", len(gstr))\t}\t{\t\t// 获取C的字符串\t\tcstr := C.c_new_str()\t\tdefer C.free(unsafe.Pointer(cstr))\t\t// 默认GoString遵循的C的实现,也就是遇到'\\0'就截断了,所以输出了 a\t\tprintStr("C.GoString(cstr)", C.GoString(cstr))\t\t// 就是由于上诉的原因,因此人家开发了一个C.GoStringN函数,就是你需要显示告诉我C中char*的长度!\t\tprintStr("C.GoStringN(cstr, 5)", C.GoStringN(cstr, 5))\t\t// char* -> []byte 转换\t\tvar data []byte = C.GoBytes(unsafe.Pointer(cstr), C.int(5))\t\tfor _, elem := range data {\t\t\tfmt.Printf("char: %U\\n", elem)\t\t}\t\t// 它是一个切片!\t\t// 注意:切片也可以转换成char数组\t\tvar data2 = data[:1]\t\tprintStr("data2", string(data2))\t}\t{\t\tvar ss C.CStruct\t\tss.name = C.CString("tom")\t\tss.age = C.int(1)\t\tC.print_CStruct(&ss)\t}}func printStr(name, value string) {\tfmt.Printf(`%s: "%s", len: %d`+"\\n", name, value, len(value))}\n\n执行 CGO_ENABLED=1 go run -v main.go ,输出:\nc_print_str: hello worldc_print_str_size: hello world1111int(C.c_str_len(cstr)): 11len(gstr): 16C.GoString(cstr): "a", len: 1C.GoStringN(cstr, 5): "abc", len: 5char: U+0061char: U+0000char: U+0062char: U+0000char: U+0063data2: "a", len: 1name: tom, age: 1\n\n总结\nfunc C.CString(string) *C.char 这个函数转换成C的字符串的时候,没有考虑 \\0 结尾符号的问题,所以这点一定要注意!\nfunc C.GoString(*C.char) string 的实现考虑了\\0结尾符号的问题,因此它实际上就是拷贝了 strlen 长度的C字符串到Go的字符串\nfunc C.GoStringN(*C.char, C.int) string 解决了\\0结尾符号的问题,需要显示指定 C语言中字符串的长度!\nfunc C.GoBytes(unsafe.Pointer, C.int) []byte 和 func C.CBytes([]byte) unsafe.Pointer 可以实现数组的转换\n其他基础类型都支持转换,具体看文档:官方文档\n\n如何降低开销使用原生的API,go->c 和 c->go 都需要涉及到数据的拷贝!\nimport "C"const cStrEnd = string('\\u0000')// unsafe string GO -> C func unsafeCString(str string) *C.char {\t// C语言的字符串是以\\u0000 结尾的,所以这里注意了. 需要手动加一个结尾符号\tif index := strings.IndexByte(str, '\\u0000'); index == -1 {\t\tstr = str + cStrEnd\t}\theader := (*reflect.StringHeader)(unsafe.Pointer(&str))\treturn (*C.char)(unsafe.Pointer(header.Data))}// unsafe []byte GO -> C// 注意 []byte 长度大于0func unsafeCBytes(str []byte) *C.char {\treturn (*C.char)(unsafe.Pointer(&str[0]))}// unsafeGoBytes []byte C->GOfunc unsafeGoBytes(arr *C.char, arrSize C.int) []byte {\theader := reflect.SliceHeader{\t\tData: uintptr(unsafe.Pointer(arr)),\t\tLen: int(arrSize),\t\tCap: int(arrSize),\t}\treturn *(*[]byte)(unsafe.Pointer(&header))}\n\n调试工具可以使用 go tool cgo main.go 查看cgo生成的文件, 其实我们用注释写C代码,编译器并不会识别,而是GO编译期间有个预处理的阶段 生成了 go tool cgo 的产物!\n大概会生成一份 C -> GO 转换的代码,具体可以自己调试一下!\n如何集成C++C++ 与 C的关系我们知道C++ 实际上是完全兼容 C的,其次C++与C是可以相互调用的,那么建立这些前提的就是 要明确告诉 c/c++ 编译器,我这个代码是C语言的,因此需要 extern "C" 来告诉 C++ 我这个代码是C语言的,编译器就会按照C语言的规范去链接!\n返过来,C语言他没有 extern "C" 这个关键词,那么C++引用了C函数的代码,因此需要 extern "C" 可以修饰 #include ${c的头文件},也就是说告诉编译器,这些申明用C语言的规范去链接!\n其实上面非常的绕,需要大家亲自体会一下!其次C与C++语法不完全一样,有些时候在做这种集成开发的时候容易混了!\n下面这里有个例子,就是集成 libprotobuf 实现解析 protobuf 文件,目前应该Go开源社区里面没有做集成的!\n前置准备\n下载 protobuf, 具体如何本地构建protobuf的链接库,可以直接看我们的这个项目\n学会用CMake等工具构建代码\n掌握C/C++/Go的基本语法\n项目地址: https://github.com/anthony-dong/protobuf\n\n项目结构├── CMakeLists.txt├── README.md├── cgo.go # cgo go语言实现├── cgo.h # cgo c头文件├── deps # 依赖│ ├── README.md│ ├── darwin_x86_64│ ├── include # 引用的第三方头文件│ └── linux_x86_64 # 静态依赖│ ├── libprotobuf.a│ └── vendor.go # 解决go vendor 问题├── errors.go├── go.mod├── go.sum├── option.go├── pb_include.h├── pb_parser.cpp # 核心业务逻辑├── pb_parser.go # 对外接口├── pb_parser.h # 核心业务逻辑├── utils.go └── vendor.go # 解决go vendor 问题\n\n大概就是C++写业务逻辑,然后 C++ 的接口 转成 C接口, C->GO的翻译!\n实现功能package mainimport (\t"fmt"\t"github.com/anthony-dong/protobuf")func main() {\ttree, err := protobuf.NewProtobufDiskSourceTree("internal/test/idl_example")\tif err != nil {\t\tpanic(err)\t}\tidlConfig := new(protobuf.IDLConfig)\tidlConfig.IDLs = tree\tidlConfig.Main = "service/im.proto"\tidlConfig.IncludePath = []string{"desc", "."}\tdesc, err := protobuf.ParsePBMultiFileDesc(idlConfig,\t\tprotobuf.WithJsonTag(),\t\tprotobuf.WithSourceCodeInfo(),\t\tprotobuf.WithGoogleProtobuf(),\t\tprotobuf.WithRequireSyntaxIdentifier(),\t)\tif err != nil {\t\tpanic(err)\t}\tfmt.Println(protobuf.MessageToJson(desc, true))}// 运行: CGO_ENABLED=1 go run main.go\n\n注意点\n尽可能的使用 unsafe 操作避免内存拷贝,尤其是数据大的情况,效果优秀\n简单函数尽可能的用GO实现\n注意内存管理和回收,避免直接暴露给使用者\nC++的技巧可以参考我的这篇文章: https://anthony-dong.github.io/2023/04/06/fd8e40efcdb71f2be44fb720dc582d67/\nC 语言实际上没有太多要学习的,是最简单的语言了,没啥难度,无非注意内存分配!\nC++ 翻译 C 会存在有些类对象转换不来或者拷贝代价太高,尽可能的使用void指针避免拷贝!\n\n参考\nCgo 官方文档: https://pkg.go.dev/cmd/cgo@go1.17\nGo语言高级编程:https://chai2010.cn/advanced-go-programming-book/ch2-cgo/index.html\n\n","categories":["Golang"],"tags":["C++","Golang","Cgo"]},{"title":"用markdown画流程图和时序图","url":"/2022/03/27/583186f06a088dc9967a483e3876b2a2/","content":" markdown 目前已经成为轻量级编辑器的代表,依靠markdown可以解决日常生活中写文档的基本需求,并且对于一些日常使用的流程图和序列图也有一定的支持!目前本人使用的是 Typora 写个人文章,在公司内用的是飞书文档,基本都是markdown语法!而使用目前比较好用的ProcessOn(网页版)、Visio/PowerPoint(microsoft) 、Draw.io(开源)使用下来的体验就是文章和流程图分离!也就是导致使用体验大打折扣,目前飞书文档支持的PlantUML很不错!我个人一般是PlantUML和Mermaid!\n\n\n1. UML1. 基本介绍 UML 是统一建模语言的简称,它是一种由一整套图表组成的标准化建模语言。UML用于帮助系统开发人员阐明,展示,构建和记录软件系统的产出。UML代表了一系列在大型而复杂系统建模中被证明是成功的做法,是开发面向对象软件和软件开发过程中非常重要的一部分。UML主要使用图形符号来表示软件项目的设计,使用UML可以帮助项目团队沟通、探索潜在的设计和验证软件的架构设计。以下我们将向您详细介绍什么是UML、UML的历史以及每个UML图类型的描述,辅之以UML示例。\n2. 作用\n为用户提供现成的、有表现力的可视化建模语言,主要就是可视化,直观!!\n逻辑思维的整理吧,如果你可以画图清晰的表述出逻辑,那么一定方案可执行会很好!\n画图也是一门学问和技巧!\n\n3. 分类大概上有13种,下面我列出我们常用的!\n\n1. 结构图1. 类图 (Class Diagram)\n\n\n2. 行为性图1. 活动图 (Activity Diagram)\n 这里主要多了一个概念叫做泳道,泳道可以很好的隔离开流程!\n\n\n\n2. 状态机图 (State Machine Diagram)\n其实这个就和流程图!\n\n\n\n\n3. 序列图 (Sequence Diagram)\n\n2. 流程图 (Flow Chart)markdown 支持流程图(Flow Chart) 是用的开源的 flowchart 语法进行的支持! 流程图一般是用来清晰的表达出流程逻辑的一个可视化工具,比如你要跟同事沟通一个技术方案,你文档文字写的满满的,但是导致有些专业性的隔阂或者你表述不清晰,导致同事很难理解你在说什么,这时候可能就需要流程图进行弥补下!\n1. 简单语法介绍流程图一般有几个关键的节点,也一般是开始和结束两个节点!其次是有多个不同类型的节点组成!具体可以看官方文档: flowchart\n节点定义语法如 : 节点名称=>节点类型: 节点显示的内容[:> 超链接URL] 是空格敏感的!\n1. 节点类型节点类型主要有: 常用的其实就前5个\n\nstart (开始)\n\nend (结束)\n\noperation (操作节点,表示你要执行的步骤,比如平时顺序结构 步骤A->步骤B->步骤C)\n\ncondition (条件节点,表示判断条件)\n\ninputoutput (输入输出节点,类似于前端的input框,输入个账户名称和密码之类的!)\n\nsubroutine(子流程,即表示一个流程,但是流程比较复杂,直接通过自流程代替,表示这块逻辑只是没有展开而已,其实很复杂!)\n\nparallel (并行流,允许多个流程同时发生)\n\n\n2. 流程控制类型说明符:\n\n基本操作流: startVar(<direction>)->nextNode\n\ndirection 表示方位,有left、right、top、bottom \n\npreviousNode->endVar\n\noperationVar(<direction>)->nextNode\n\ninputoutputVar(<direction>)->nextNode\n\nsubroutineVar(<direction>)->nextNode\n\nconditionalVar(yes, <direction>)->nextNode1\n\nconditionalVar(no, <direction>)->nextNode2\n\nparallel 控制流介绍\n\n\nparallelVar(path1, <direction>)->nextNode1parallelVar(path2, <direction>)->nextNode2parallelVar(path3, <direction>)->nextNode3\n\n\ncondition 控制流介绍\n\n# @ 表示覆写描述,比如正在这个line上描述的是true,@可以覆写描述为 正确cond(true@正确)->io->e\n\n3. 官方例子介绍下图为官方介绍的流程图:\nst=>start: Start:>http://www.google.com[blank]e=>end:>http://www.google.comop1=>operation: My Operationsub1=>subroutine: My Subroutinecond=>condition: Yesor No?:>http://www.google.comio=>inputoutput: catch something...para=>parallel: parallel tasksst->op1->condcond(yes)->io->econd(no)->parapara(path1, bottom)->sub1(right)->op1para(path2, top)->op1\n\n展示效果如下: \n\n\n2. 简单例子介绍例如一个网关发布(分级发布)的前置流程,主要就是用户选择发布版本,然后进行判断,提交发布信息的流程,流程图如下:\n我觉得flowchart好处就是可以像写代码一样,清晰的描述流程图,要比 mermaid实用的多!\nstart=>start: 开始流程end=>end: 结束select_publish_version=>inputoutput: 输入/选择发布版本op_start_publish=>operation: 点击开始发布cond_publish_version=>condition: 判断版本是否已经发布测试环境op_throw_error=>operation: 抛出异常(当前版本x.x.x未经过测试环境验证,不允许发布)op_load_publish_list=>operation: 加载测试环境发布成功的工单select_tlb_path=>inputoutput: 选择TLB发布集群以及分流策略select_agw_env=>inputoutput: 选择AGW发布环境op_submit=>operation: 提交发布信息start->select_publish_version->op_start_publish->cond_publish_versioncond_publish_version(no@否)->op_throw_error->endcond_publish_version(yes@是)->op_load_publish_list->select_tlb_path->select_agw_env->op_submit->end\n\n\n\n\n\n\n\n3. PlantUML(UML)markdown 支持时序图基本上用的都是 PlantUML ! 目前飞书文档支持使用 PlantUML ,但是我个人使用的typora不支持,而且我的博客也不支持,所以目前主流的解决方案就是通过网站:https://g.gravizo.com/ 画 svg 实现!\n不过可以配合 vscode进行使用!不过我觉得PlantUML渲染比较棒!\n@startuml;actor User;participant "First Class" as A;participant "Second Class" as B;participant "Last Class" as C;User -> A: DoWork;activate A;A -> B: Create Request;activate B;B -> C: DoWork;activate C;C --> B: WorkDone;destroy C;B --> A: Request Created;deactivate B;A --> User: Done;deactivate A;@enduml\n\n\n\n4. Mermaid (UML)1. 流程图(flowchart)\n头部需要定义 graph|flowchart [direction] , 其中direction 主要由 LR 和 TB 组合上下左右!\n内容部分只需要定义流程即可\na --> b --> c 表示 顺序引用\na --> |注释|b 表示 a -> b 中间需要加注释\na(show_name) 表示 a需要通过 show_name 进行展示,也就是节点内容\n\n\n单向箭头主要有 -->实线, -.-> 虚线,==> 加粗线; 双向箭头就是 <==>和…, 无箭头是 == ,很形象!\n节点可以通过 {condition name} 和 [operation name] 和 (start|end name) 和 ((multi condition name)) 进行渲染形状!\n和上面我们讲的 flowchart区别就是他不需要再去定义节点了!但是这个流程图定义的并不严格!\n\n1. 普通流程图例子1:\ngraph LR\ta-->|注释|b(开始结束类型)-->c[operation类型] -->d{condition类型} -.->|虚线|e((圆型节点))==>|加粗线|f <--> |双向箭头|g(end)\n\ngraph LR\n a-->|注释|b(开始结束类型)-->c[operation类型] -->d{condition类型} -.->|虚线|e((圆型节点))==>|加粗线|f |双向箭头|g(end)\n\n例子2:\ngraph LR A{A} --> |a->b普通线|B(B) B --> C --> F((F)) --> |输入|G>G] --> D C --> D A ==> |a->d block|D(D) A --> |a->e 注释|E[e注释] E -.-> |e-d 虚线|D\n\ngraph LR\n A{A} --> |a->b普通线|B(B)\n B --> C --> F((F)) --> |输入|G>G] --> D \n C --> D\n A ==> |a->d block|D(D)\n A --> |a->e 注释|E[e注释]\n E -.-> |e-d 虚线|D\n\n例子3:\nflowchart LRA[Hard] <-->|Text| B(Round)B --> C{Decision}C -->|One| D[Result 1]C -->|Two| E[Result 2]\n\nflowchart LR\nA[Hard] |Text| B(Round)\nB --> C{Decision}\nC -->|One| D[Result 1]\nC -->|Two| E[Result 2]\n\n例子4: 可以通过关键字 & 来表示 a->c , b->d, c->d\nflowchart LR a --> b & c--> d\n\nflowchart LR\n a --> b & c--> d\n\n2. 子图表可以通过 subgraph 名称+end关键字来构建子图标,你可以理解为就是加了个框框!可以区分流程图\ngraph LR\t\ta(a) --> b\t\tsubgraph one模块\t\tb --> c\t\tend\t\tc --> d\t\tsubgraph two模块\t\td --> e\t\tend\t\te --> f(f)\n\ngraph LR\n a(a) --> b\n subgraph one模块\n b --> c\n end\n c --> d\n subgraph two模块\n d --> e\n end\n e --> f(f)\n\n\n\n\n\n2. 序列图 (Sequence Diagram)时序图由简称UML图,\n\n首先需要定义参与者: participant [name] as [show_name] 或者直接 participant name , 或者可以直接不定义participant\n连接线: ->> 实线,-->> 虚线,一般请求用实线,return 用虚线,\n + 和 -表示加入会话[activate]和结束会话[deactivate],例如 a -> + b 表示b开启会话(会话是俩人都可以开启的,这个a->>+b只是表示b开启,如果a也要开启,需要 activate a),例如 activate a表示a开启会话!\n添加node: Note 位置描述 参与者: 标注文字 \n\n# over a,b,c 多个人# left of a 单个人# right of a 单个人Note over John,Alice: note: 嘻嘻嘻 😊!Note left of John: Text in noteNote right of John: Text in note\n\n1. 普通序列图例子一:\nsequenceDiagram\tparticipant Alice as Alice # 定义参与者: participant [name] as [show_name]\tparticipant John # 定义参与者: participant [name]\tAlice->> + John: Hello John, how are you? # 表示和john进行会话,同时a和j开始会话\tJohn-->> Alice: Great! \tNote over John,Alice: note: 嘻嘻嘻,over 😊! # 表示添加注释\tNote left of John: note: 嘻嘻嘻, left 😊!\tNote right of John: note: 嘻嘻嘻, wright 😊!\t\tAlice->> John: Hi John, I can hear you!\tJohn-->> - Alice: I feel great! # 表示john断开了会话\n\n\n\nsequenceDiagram\n participant Alice as Alice # 定义参与者: participant [name] as [show_name]\n participant John # 定义参与者: participant [name]\n Alice->> + John: Hello John, how are you? # 表示和john进行会话,同时a和j开始会话\n John-->> Alice: Great! \n Note over John,Alice: note: 嘻嘻嘻,over 😊! # 表示添加注释\n Note left of John: note: 嘻嘻嘻, left 😊!\n Note right of John: note: 嘻嘻嘻, wright 😊! \n Alice->> John: Hi John, I can hear you!\n John-->> - Alice: I feel great! # 表示john断开了会话\n\n\n\n2. 序列图例子例子2: \nsequenceDiagram\tparticipant fe as 小程序\tparticipant service as 服务器\tparticipant wx_service as 微信服务器\tloop 失败重试\t\tfe ->> fe: wx.login()获取code\tend\tfe ->> service: wx.request() 发送code\tactivate service\tservice ->> wx_service: 发送code+appid+签名\tactivate wx_service\twx_service -->> service: 返回code id\tdeactivate wx_service\tservice ->> service: 生成token\tservice -->> fe: 返回token\tdeactivate service\n\nsequenceDiagram\n participant fe as 小程序\n participant service as 服务器\n participant wx_service as 微信服务器\n loop 失败重试\n fe ->> fe: wx.login()获取code\n end\n fe ->> service: wx.request() 发送code\n activate service\n service ->> wx_service: 发送code+appid+签名\n activate wx_service\n wx_service -->> service: 返回code id\n deactivate wx_service\n service ->> service: 生成token\n service -->> fe: 返回token\n deactivate service\n\n5. 参考\nUML整体概述\nUML时序图(Sequence Diagram)学习笔记st=>start: Start:> http://www.google.com[blank]\ne=>end: End :>http://www.google.com\nop1=>operation: My Operation\nsub1=>subroutine: My Subroutine\ncond=>condition: Yes\nor No?:>http://www.google.com\nio=>inputoutput: catch something...\npara=>parallel: parallel tasks\n\nst->op1->cond\ncond(yes)->io->e\ncond(no)->para\npara(path1, bottom)->sub1(right)->op1\npara(path2, top)->op1{\"scale\":1,\"line-width\":2,\"line-length\":50,\"text-margin\":10,\"font-size\":12} var code = document.getElementById(\"flowchart-0-code\").value; var options = JSON.parse(decodeURIComponent(document.getElementById(\"flowchart-0-options\").value)); var diagram = flowchart.parse(code); diagram.drawSVG(\"flowchart-0\", options);start=>start: 开始流程\nend=>end: 结束\nselect_publish_version=>inputoutput: 输入/选择发布版本\nop_start_publish=>operation: 点击开始发布\ncond_publish_version=>condition: 判断版本是否\n已经发布测试环境\nop_throw_error=>operation: 抛出异常\n(当前版本x.x.x未经过测试环境验证,不允许发布)\nop_load_publish_list=>operation: 加载测试环境发布成功的工单\nselect_tlb_path=>inputoutput: 选择TLB发布集群以及分流策略\nselect_agw_env=>inputoutput: 选择AGW发布环境\nop_submit=>operation: 提交发布信息\n\nstart->select_publish_version->op_start_publish->cond_publish_version\ncond_publish_version(no@否)->op_throw_error->end\ncond_publish_version(yes@是)->op_load_publish_list->select_tlb_path->select_agw_env->op_submit->end{\"scale\":1,\"line-width\":2,\"line-length\":50,\"text-margin\":10,\"font-size\":12} var code = document.getElementById(\"flowchart-1-code\").value; var options = JSON.parse(decodeURIComponent(document.getElementById(\"flowchart-1-options\").value)); var diagram = flowchart.parse(code); diagram.drawSVG(\"flowchart-1\", options);\n\n","categories":["工具"],"tags":["工具","Mermaid","UML","流程图"]},{"title":"Docker学习","url":"/2020/09/26/58eb7a52b940359a191f97578179026e/","content":" 学习docker的基本组件、dockerfile、docker命令等!\n\n\n官方文章:https://docs.docker.com/\ndocker 官方镜像地址: https://hub.docker.com/\n推荐阅读丛书 : Docker实战(博文视点出品) , 第一本Docker书 修订版\ndocker 快速安装: curl -fsSL https://get.docker.com | bash -s docker --mirror Aliyun , 记得用户设置在docker用户组!,推荐在非mac os/windows上玩。\n1、docker 的组成\n1、runcrunc实质上是一个轻量级的、针对Libcontainer进行了包装的命令行交互工具( Libcontainer取代了早期Docker架构中的LXC )。\n使用很简单,它是运行一个容器最基本的工具,所以我们需要创建容器。\ndocker中创建容器是,docker create docker-image-name ,但是需要导出到文件系统中\n# create the top most bundle directorymkdir /mycontainercd /mycontainer# create the rootfs directorymkdir rootfs# export busybox via Docker into the rootfs directorydocker export $(docker create busybox) | tar -C rootfs -xvf -runc spec# run as rootcd /mycontainerrunc run mycontainerid\n\n2、containerd对于docker进行拆分后,容器执行逻辑被重构到一个新的名为containerd (发音为container-dee) 的工具中。它的主要任务是容器的生命周期管理———— start | stop | pause | rm….\nDocker引擎技术栈中,containerd位于daemon和runc所在的OCI层之间。随着时间的推移,它被赋予了更多的功能,如镜像管理。虽然名叫containerd, 但是它并不负责创建容器,而是指挥runc去做。containerd将Docker镜像转换为OCI bundle,并让runc基于此创建一个新的容器。然后,runc与操作系统内核接口进行通信,基于所有必要的工具( Namespace、CGroup 等)来创建容器。容器进程作为runc的子进程启动,启动完毕后,runc 将会退出。\n\n 将所有的用于启动、管理容器的逻辑和代码从daemon中移除,意味着容器运行时与Docker daemon是解耦的,有时称之为“无守护进程的容器(daemonless container)”,如此,对Docker daemon的维护和升级工作不会影响到运行中的容器。\n3、shimshim是实现无daemon的容器(用于将运行中的容器与daemon解耦,以便进行daemon升级等操作)不可或缺的工具。containerd 指挥runc来创建新容器。事实上,每次创建容器时它都会fork一个新的runc实例。不过,一旦容器创建完毕,对应的runc进程就会退出。因此,即使运行上百个容器,也无须保持上百个运行中的runc实例。一旦容器进程的父进程runc退出,相关联的containerd-shim 进程就会成为容器的父进程。\n作为容器的父进程,shim 的部分职责如下。\n\n保持所有STDIN和STDOUT流是开启状态,从而当daemon重启的时候,容器不会因为管道( pipe)的关闭而终止。\n将容器的退出状态反馈给daemon。\n\n4、daemondaemon的主要功能包括镜像管理、镜像构建、REST API、身份验证、安全、核心网络以及编排。\n2、Dockerfile 学习\n 官方文档: https://docs.docker.com/engine/reference/builder/\n\n1、dockerfile 文件,基本玩法FROM alpine:latestMAINTAINER anthony-dong "fanhaodong516@gamil.com"RUN mkdir -p /opt/projectWORKDIR /opt/projectADD project.tar.gz .COPY run.sh .EXPOSE 8080VOLUME [ "/opt/project" ]ENV PROJECT_HOME /opt/projectENV PATH $PATH:$PROJECT_HOMECMD ["/bin/sh","-c","run.sh"]\n\n执行:\ndocker build -t test .docker run --rm -it test\n\n2、CMD 和 ENTRYPOINT 的区别\nCMD 和 ENTRYPOINT 的区别,这里其实区别很简单, 可以理解为 在没有启动命令是 ENTRYPOINT+ CMD (启动docker run时运行的默认命令)\n当我们 docker run image_name [cmd1] [cmd2] 时,其实已经把 Dokerfile中配置的CMD命令替换掉了,其实真正执行的是 ENTRYPOINT+ [cmd1]+ [cmd2] , 但是这俩CMD 和 ENTRYPOINT 都可以为空\n根据上面可以发现 CMD可以通过 docker run可以替换,那么 ENTRYPOINT 也是可以替换的,可以通过 -entrypoint 指定\n\nFROM alpine:latestENTRYPOINT [ "ls"]CMD ["-al"]\n\n比如上面找个,我们如果默认执行的话,会是:\n➜ kubernetes docker run --rm demo total 64drwxr-xr-x 1 root root 4096 Jan 26 12:50 .// ...drwxr-xr-x 2 root root 4096 Jan 14 11:49 optdr-xr-xr-x 226 root root 0 Jan 26 12:50 proc\n\n但是如果我们添加了参数:\n➜ kubernetes docker run --rm demo -a...dockerenvbin//.... home\n\n修改 ENTRYPOINT\n➜ kubernetes docker run --rm --entrypoint env demoPATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/binHOSTNAME=4791b485ab53HOME=/root\n\n3、AND 和 COPY的区别AND 可以是多种source源,支持url/tar/unzip/文件包,所以看需求选择,比如使用tar包会自动解压\nCOPY 只能copy文件,所以一般推荐copy\n4、build 失败如何继续,如何调试!!FROM centos:centos7USER admin:adminCMD [ "/bin/bash" ]\n\n这个build是可以通过的!!!\n➜ docker-file-test docker build -t test-1 .Sending build context to Docker daemon 25.09kBStep 1/3 : FROM centos:centos7 ---> 7e6257c9f8d8Step 2/3 : USER admin:admin ---> Running in f8ed46bdda32Removing intermediate container f8ed46bdda32 ---> 9611a153742eStep 3/3 : CMD [ "/bin/bash" ] ---> Running in abc32bd8e245Removing intermediate container abc32bd8e245 ---> 0f0f0aeeec29Successfully built 0f0f0aeeec29Successfully tagged test-1:latest\n\n然后运行\n➜ docker-file-test docker run --rm -it test-1docker: Error response from daemon: linux spec user: unable to find user admin: no matching entries in passwd file.\n\n发现这个,我们需要从Step 1/3开始!!!\n➜ docker-file-test docker run --rm -it 7e6257c9f8d8[root@532c8a693273 /]# useradd admin -p admin -d /home/admin --create-home -s /bin/bash[root@532c8a693273 /]# su admin[admin@532c8a693273 /]$ exitexit\n\n然后我们需要继续改dockerfile\nFROM centos:centos7RUN useradd admin -p admin -d /home/admin --create-home -s /bin/bashUSER admin:adminCMD [ "/bin/bash" ]\n\n继续运行\n➜ docker-file-test docker build -t test-1 .Sending build context to Docker daemon 25.09kBStep 1/4 : FROM centos:centos7 ---> 7e6257c9f8d8Step 2/4 : RUN useradd admin -p admin -d /home/admin --create-home -s /bin/bash ---> Running in f36f10b603bdRemoving intermediate container f36f10b603bd ---> 843554e160f7Step 3/4 : USER admin:admin ---> Running in 796341a1be27Removing intermediate container 796341a1be27 ---> ab964ed78d8fStep 4/4 : CMD [ "/bin/bash" ] ---> Running in 2c67de0242eaRemoving intermediate container 2c67de0242ea ---> 17a4bc8d7206Successfully built 17a4bc8d7206Successfully tagged test-1:latest➜ docker-file-test docker run --rm -it 17a4bc8d7206[admin@55f84f22acf6 /]$\n\n这个就是一个简单的过程!!!!!!,这个好处是类似于调试的过程\n其实还可以通过 docker run -u 来制定用户,切记一点,别记忆命令!!,要记忆如何学习!!\n➜ docker-file-test docker run -it --rm --user root test-1[root@6dbeb01f5329 /]#\n\n5、安装一个Go的环境FROM centos:centos7MAINTAINER anthony-dong "fanhaodong516@gamil.com"# 添加文件地址ADD go1.13.15.linux-amd64.tar.gz /opt# 项目地址文件,安装工具RUN mkdir /opt/project \\ && yum install -y vim \\ && yum install -y curl \\ && yum install -y git \\ && yum install -y wget# 环境变量 ENV GO_HOME "/opt/go"ENV PATH $PATH:$GO_HOME/bin# export端口EXPOSE 8080 EXPOSE 10010 # 直接启动bashCMD [ "/bin/bash"]\n\n然后执行, --tag 可以写成-t ,比如--tag go:1.13 意思就是镜像名称是go,版本是1.13,大致就是这个样子! \ndocker build --file ./Dockerfile -p --tag goenv1.13 .\n\n最后启动需要指定-t ,意思就是 --rm是容器被停止则被删除,-d是deamon启动, -it就是hold住类似于开启一个终端(i是输出,t是终端,然后程序就被hold住了), -P暴漏端口随机到宿主机上, 最后指定使用的镜像\ndocker run --rm -d -it -P goenv1.13\n\n然后查看一下\n➜ test docker psCONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMESf67a0f2bcb0e goenv1.13 "/bin/bash" 21 seconds ago Up 19 seconds 0.0.0.0:32769->8080/tcp, 0.0.0.0:32768->10010/tcp heuristic_lumiere\n\n6、多阶段构建\n Docker v17.05 开始支持多阶段构建 (multistage builds), 参考:https://yeasy.gitbook.io/docker_practice/image/multistage-builds , 主要命令就是 COPY --from=builder /data/apps/project/bin/app bin/\n但是业务中不推荐,很多时候要去容器里操作!\n\n还是一个Go项目,假如以一个Http-Server 为例子\n➜ pck tree -L 2.├── Dockerfile├── cmd│ └── main.go├── go.mod└── go.sum\n\n项目文件:\npackage mainimport (\t"github.com/gin-gonic/gin"\t"log"\t"net/http")func main() {\trouter := gin.Default()\trouter.GET("/echo", func(context *gin.Context) {\t\tcontext.JSON(http.StatusOK, gin.H{\t\t\t"code": 0,\t\t\t"data": "hello world",\t\t\t"message": "success",\t\t})\t})\tlog.Fatal(router.Run(":8080"))}\n\nDockerfile\n# 1.11+ 默认自动支持Go mod,切记builder的编译和运行的编译内核一致FROM golang:1.13.15-alpine3.12 as builder# 全局变量,项目名称ARG GOPROXY=https://goproxy.cn,directENV GOPROXY=${GOPROXY}WORKDIR /dataCOPY . .RUN go build -v -ldflags "-s -w" -o bin/app cmd/main.goFROM alpine:3.12 as runing# 切记环境变量不能共享ARG PROJECT_NAME=projectARG PROJECT_PORT=8080WORKDIR /data/${PROJECT_NAME}COPY . .# 含义是 copy上一个镜像的 /data/${PROJECT_NAME}/bin/app 文件到当前目录的binCOPY --from=builder /data/bin/app bin/# COPY --from=0 /opt/bin/app .EXPOSE ${PROJECT_PORT}CMD [ "bin/app" ]\n\n编译 :(注意 alpine是不支持 race 的,会编译报错)\ndocker build -t test .\n\n启动:\n➜ my-docker docker run --rm -p 8080:8080 test [GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production. - using env: export GIN_MODE=release - using code: gin.SetMode(gin.ReleaseMode)[GIN-debug] GET /echo --> main.main.func1 (3 handlers)[GIN-debug] Listening and serving HTTP on :8080\n\n查看大小:\n➜ my-docker docker imagesREPOSITORY TAG IMAGE ID CREATED SIZEtest latest fdbca700806f 8 minutes ago 17.8MBalpine latest 7731472c3f2a 6 days ago 5.61MB\n\n其实真实的Go的构建不是这种,一般都有打包机器,无须我们去找机器编译和运行\n7、构建多种系统架构支持的 Docker 镜像https://yeasy.gitbook.io/docker_practice/image/manifest\n3、Docker 限制资源压测程序,不准确,因为数组扩容,是十分消耗内存的,如果内存满了,无法申请内存,那么就不会打印消息!可以使用stress工具来测试CPU和内存。这里我也懒得下载!!\npackage mainimport (\t"fmt"\t"os"\t"strconv"\t"time")var (\tref []byte)func main() {\tfmt.Println(os.Getpid())\tmem, _ := strconv.ParseInt(os.Args[1], 10, 64)\tfor {\t\tif mem == 0 {\t\t\tmem = 1\t\t}\t\tsize := 1024 * 1024 * mem\t\tnewSlice(size)\t\ttime.Sleep(time.Second)\t}}func newSlice(size int64) {\tadd := make([]byte, size)\tref = append(ref, add...) // 分配size\tfmt.Println(fmt.Sprintf("%s %v\\n", time.Now().Format("15:04:05"), os.Getpid())) // 打印消息}\n\n下面表示:表示在现在的内存是200M,cpu限制是1\n➜ go-demo docker run -it --rm --memory 200M --cpuset-cpus="1" --oom-kill-disable -v /Users/dong/go/version/go-1.13.5:/opt/project ce2534430fc2 /bin/bash\n\n[root@17d356297d5f project]# go build -o bin/main study/slice/main2.go // .. 可以发现在45s的卡壳了,也就是内存被限制了08:52:44 10308:52:45 103top - 08:52:45 up 5:24, 0 users, load average: 0.96, 1.45, 1.12Tasks: 4 total, 1 running, 3 sleeping, 0 stopped, 0 zombie%Cpu(s): 0.3 us, 3.0 sy, 0.0 ni, 93.3 id, 1.0 wa, 0.0 hi, 2.4 si, 0.0 stKiB Mem : 2046748 total, 1592784 free, 324656 used, 129308 buff/cache// 这些信息是假的,不能看KiB Swap: 1048572 total, 703508 free, 345064 used. 1582140 avail Mem PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 103 root 20 0 645568 200120 68 S 18.9 9.8 0:01.31 main // 200 m 1 root 20 0 11836 2400 2400 S 0.0 0.1 0:00.15 bash 51 root 20 0 11836 2332 2332 S 0.0 0.1 0:00.05 bash 71 root 20 0 56188 2016 1924 R 0.0 0.1 0:00.03 top\n\n查看docker容器真实的内存,可以\n➜ ~ docker stats 17d356297d5fCONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS17d356297d5f cocky_shaw 0.00% 199.4MiB / 200MiB 99.69% 1.18kB / 0B 357MB / 927MB 7\n\n1、关于内存设置这几个参数的关系\n\n\n选项\n描述\n\n\n\n-m,--memory\n内存限制,格式是数字加单位,单位可以为 b,k,m,g。最小为 4M\n\n\n--memory-swap\n内存+交换分区大小总限制。格式同上。必须必-m设置的大\n\n\n--memory-reservation\n内存的软性限制。格式同上\n\n\n--oom-kill-disable\n是否阻止 OOM killer 杀死容器,默认没设置\n\n\n--oom-score-adj\n容器被 OOM killer 杀死的优先级,范围是[-1000, 1000],默认为 0\n\n\n--memory-swappiness\n用于设置容器的虚拟内存控制行为。值为 0~100 之间的整数\n\n\n--kernel-memory\n核心内存限制。格式同上,最小为 4M\n\n\n –memory-swap 值必须比**–memory** 值大,因为:–memory-swap不是交换分区,而是内存加交换分区的总大小\n1. 不设置如果不设置-m,–memory和–memory-swap,容器默认可以用完宿舍机的所有内存和 swap 分区。不过注意,如果容器占用宿主机的所有内存和 swap 分区超过一段时间后,会被宿主机系统杀死(如果没有设置–00m-kill-disable=true的话)。\n2. 设置-m,–memory,不设置–memory-swap给-m或–memory设置一个不小于 4M 的值,假设为 a,不设置–memory-swap,或将–memory-swap设置为 0。这种情况下,容器能使用的内存大小为 a,能使用的交换分区大小也为 a。因为 Docker 默认容器交换分区的大小和内存相同。\n如果在容器中运行一个一直不停申请内存的程序,你会观察到该程序最终能占用的内存大小为 2a。\n比如$ docker run -m 1G ubuntu:16.04,该容器能使用的内存大小为 1G,能使用的 swap 分区大小也为 1G。容器内的进程能申请到的总内存大小为 2G。\n3. 设置-m,–memory=a,–memory-swap=b,且b > a给-m设置一个参数 a,给–memory-swap设置一个参数 b。a 时容器能使用的内存大小,b是容器能使用的 内存大小 + swap 分区大小。所以 b 必须大于 a。b -a 即为容器能使用的 swap 分区大小。\n比如$ docker run -m 1G –memory-swap 3G ubuntu:16.04,该容器能使用的内存大小为 1G,能使用的 swap 分区大小为 2G。容器内的进程能申请到的总内存大小为 3G。\n4. 设置-m,–memory=a,–memory-swap=-1给-m参数设置一个正常值,而给–memory-swap设置成 -1。这种情况表示限制容器能使用的内存大小为 a,而不限制容器能使用的 swap 分区大小。\n这时候,容器内进程能申请到的内存大小为 a + 宿主机的 swap 大小。\n2、cpu限制➜ ~ docker run --helpUsage:\tdocker run [OPTIONS] IMAGE [COMMAND] [ARG...]Run a command in a new containerOptions: --add-host list Add a custom host-to-IP mapping (host:ip) -c, --cpu-shares int CPU shares (relative weight) --cpus decimal Number of CPUs --cpuset-cpus string CPUs in which to allow execution (0-3, 0,1) --cpuset-mems string MEMs in which to allow execution (0-3, 0,1)\n\n\n\n\n选项\n描述\n\n\n\n--cpuset-cpus=""\n允许使用的 CPU 集,值可以为 0-3,0,1\n\n\n-c,--cpu-shares=0\nCPU 共享权值(相对权重)\n\n\ncpu-period=0\n限制 CPU CFS 的周期,范围从 100ms~1s,即[1000, 1000000]\n\n\n--cpu-quota=0\n限制 CPU CFS 配额,必须不小于1ms,即 >= 1000\n\n\n--cpuset-mems=""\n允许在上执行的内存节点(MEMs),只对 NUMA 系统有效\n\n\n关于CFS的概念: https://www.jianshu.com/p/1da5cfd5cee4 \n后端基本不需要关注cpu,因为本身不是cpu密集型业务,基本都是io密集型/内存密集型。\n4、docker push 命令\n1、docker hub,类似于git一样,比如很多私人仓库,默认是 hub.docker.com, 比如覆盖则 docker login aliyun.docker.com 等等\n\ndocker login\n\n输入姓名,输入密码\n\n2、 查看推送的镜像\n\ndocker images go1.13.15 latest ce2534430fc2 26 minutes ago 769MB\n\n\n3、第一步需要打tag\n\n一般是 : docker tag <镜像名称> <用户名>/<镜像名称>:<镜像版本号>\ndocker tag go1.13.15 fanhaodong/go1.13.15:v1.0\n\n\n4、push\n\n命令: docker push <用户名>/<镜像名称>:<镜像版本号>\ndocker push fanhaodong/go1.13.15:v1.0\n\n5、docker 其他命令1、docker builddocker build --build-arg arg=value --file Dockerfile_path --tag name:version build_path\nFROM alpine:latest# 变量 = 默认值ARG IMG_V=1.0 ENV IMG_V=${IMG_V}CMD [ "sh","-c","env" ]\n\n执行:\n➜ docker-file-test docker build --tag test:v1 --file ./Dockerfile --build-arg IMG_V=2.0 .Sending build context to Docker daemon 20.99kBStep 1/4 : FROM alpine:latest ---> a24bb4013296Step 2/4 : ARG IMG_V=1.0 ---> Running in bd247961f4c5Removing intermediate container bd247961f4c5 ---> 48608560ae13Step 3/4 : ENV IMG_V=${IMG_V} ---> Running in 0ccb1c5aef84Removing intermediate container 0ccb1c5aef84 ---> 45d309fc4a52Step 4/4 : CMD [ "sh","-c","env" ] ---> Running in 6d266d8d8301Removing intermediate container 6d266d8d8301 ---> d4ccf528c0edSuccessfully built d4ccf528c0edSuccessfully tagged test:v1➜ docker-file-test docker run --rm test:v1HOSTNAME=59991997b9beSHLVL=1HOME=/rootIMG_V=2.0PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/binPWD=/\n\n可以看到成功 执行了环境变量!\n2、docker run传递envFROM alpine:latestaCMD [ "sh","-c","env" ]\n\n[admin@centos-linux docker-file-test]$ docker build -t test-1 .Sending build context to Docker daemon 23.04kBStep 1/2 : FROM alpine:latest ---> a24bb4013296Step 2/2 : CMD [ "sh","-c","env" ] ---> Running in 65c81ab9dc1fRemoving intermediate container 65c81ab9dc1f ---> 027268ad86eeSuccessfully built 027268ad86eeSuccessfully tagged test-1:latest[admin@centos-linux docker-file-test]$ docker run --env demo=1 test-1HOSTNAME=d4504e8ef3beSHLVL=1HOME=/rootPATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bindemo=1PWD=/\n\n传递环境变量,是可以到运行时!\n3、docker start\n\nshell脚本启动#!/bin/bash# 获取cidCID=$(docker create --rm test-1)# 根据cid启动docker start $CID# flagecho "start success!!!!"\n\n运行\n[admin@centos-linux docker-file-test]$ bash new.shdd9159e2d3da614f6dc7f816300d75ad58c6acf673eb9bea1e8a3f3af60a4137start success!!!!\n\ngood 启动了 !!!\n5、挂载docker run --rm -v /opt test-3\n如果容器销毁,宿主机文件也会被销毁!!!!\n"Mounts": [ { "Type": "volume", "Name": "79c49a14a84ce8f6ba852e11d91a109ac1c02a6649e83f68b316990404035148", "Source": "/var/lib/docker/volumes/79c49a14a84ce8f6ba852e11d91a109ac1c02a6649e83f68b316990404035148/_data",// 宿主机目录 "Destination": "/opt", // 容器目录 "Driver": "local", "Mode": "", "RW": true, "Propagation": "" }],"Volumes": { "/home/admin/dong/docker/docker-file-test": {} },\n\ndocker run --rm -v /home/admin/dong/docker/docker-file-te/:/opt test-3\n"Mounts": [ { "Type": "bind", "Source": "/home/admin/dong/docker/docker-file-test", "Destination": "/opt", "Mode": "", "RW": true, "Propagation": "rprivate" }],\n\n6、docker commit\n 这个比较适合不会写docker file的人,这里我举个例子,假如现在我们有一个centos:7\n\ndocker pull centos:7\n\n其次,我们要安装一个curl命令\n➜ /data docker run --rm -it 7e6257c9f8d8 /bin/bash[root@bcdaea8354a6 /]# yum install -y curl[root@bcdaea8354a6 /]# curl www.baidu.com<!DOCTYPE html> // good\n\n此时我们只需要进行\n➜ ~ docker ps -a -lCONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMESbcdaea8354a6 7e6257c9f8d8 "/bin/bash" 2 minutes ago Up 2 minutes happy_liskov➜ ~ docker commit bcdaea8354a6 demo-2sha256:350fbf288cb1a47a29e3d8e441e2814726de6faf54d044a585fb80b7a1f38f83\n\n这个镜像就OK了,就可以使用 demo-2的镜像了\n","categories":["云原生"],"tags":["Docker"]},{"title":"Protobuf-SourceCodeInfo 介绍","url":"/2023/01/31/5c4904cd995fb585e97a439065161818/","content":"PB 感觉工具非常成熟,这里主要指的是protoc 提供了很多核心的功能,我们直接拿来即用即可,protoc解析后的 FileDescriptorSet 足够我们去实现 代码生成、生成API文档、协议转换 等全部功能了,这里我核心讲解一下 SourceCodeInfo 的设计,其他部分感觉很好理解!\n\n\nSourceCodeInfo 介绍message SourceCodeInfo {\t // 从作用域大到小+从前到后的顺序 进行排序 repeated Location location = 1; message Location { // 这个表示具体的路径,表示接口定义,实际上也不难理解 // 例如 [5,0,2,1] 表示的是 enum_type[0].value[1] 它所在的位置,也就是对应的是 `Tow = 2;` 对应的区域 // // 5: 表示字段5,字段名称: enum_type,字段类型: list<DescriptorProto> // 0: 表示索引为0,原因是字段5类型是数组 // 2: 表示字段2,字段名称: value, 字段类: list<EnumValueDescriptorProto> // 1: 表示索引为1,原因是字段2类型是数组 // // 所以我感觉最有意思的实际上还是path这个设计,类似于flat-json这种json-path,但是它的设定是紧凑、巧妙、准确的 repeated int32 path = 1 [packed = true]; // 这里表示某个 file/message/enum/field/method 所在的位置,用坐标围起来 // 四坐标[0,0,48,1] : 起始行: 0, 起始列: 0, 结束行: 48, 结束列: 1 // 三坐标[2,0,42]: 起始行: 2, 起始列: 0, 结束行: 2, 结束列: 42 repeated int32 span = 2 [packed = true]; // 这里表示头部的comment optional string leading_comments = 3; // 这里表示尾部的comment optional string trailing_comments = 4; // 这里表示 不属于/定义模糊/脱离 的comment (一般不会取这部分作为这个字段/类型的注释) repeated string leading_detached_comments = 6; }}message FileDescriptorSet { repeated FileDescriptorProto file = 1;}// Describes a complete .proto file.message FileDescriptorProto { optional string name = 1; optional string package = 2; repeated string dependency = 3; repeated int32 public_dependency = 10; repeated int32 weak_dependency = 11; repeated DescriptorProto message_type = 4; repeated EnumDescriptorProto enum_type = 5; repeated ServiceDescriptorProto service = 6; repeated FieldDescriptorProto extension = 7; optional FileOptions options = 8; optional SourceCodeInfo source_code_info = 9; optional string syntax = 12;}// Describes an enum type.message EnumDescriptorProto { optional string name = 1; repeated EnumValueDescriptorProto value = 2; optional EnumOptions options = 3; message EnumReservedRange { optional int32 start = 1; // Inclusive. optional int32 end = 2; // Inclusive. } repeated EnumReservedRange reserved_range = 4; repeated string reserved_name = 5;}\n\n如何获取 sourcecode info\n定义api.proto文件内容如下:\n\nsyntax = "proto2";option java_package = "com.byted.xxx.xxx";enum Num { // 注释 One = 1; // 注释 // 注释 Tow = 2; // Three = 3; // 1111 // 222 // 333 Four = 4; // 注释}// 111message Request { // 1 // 2 optional string name = 1; /** * 123 */ repeated int32 nums = 3 [packed = true]; /* title hello world */ map<string, int64> kv = 4; required Num num= 5; optional InlineRequest req = 6; message InlineRequest { }}message Response {}service Demo { // 33 // 22 rpc MethodName (Request) returns (Response); // 11 // 44}\n\n\n获取 source-info\n\nprotoc -I . --descriptor_set_out=out.pb --include_source_info api.proto\n\n\n解析 out.pb 文件,这里任何语言都可以,我使用的是Go\n\npackage codecimport (\t"google.golang.org/protobuf/encoding/protojson"\t"google.golang.org/protobuf/proto"\t"google.golang.org/protobuf/types/descriptorpb"\t"io/ioutil"\t"testing")func TestProto(t *testing.T) {\tset := descriptorpb.FileDescriptorSet{}\tfile, _ := ioutil.ReadFile("/Users/bytedance/data/pb_file/out.pb")\tif err := proto.Unmarshal(file, &set); err != nil {\t\tt.Fatal(err)\t}\tt.Log(protojson.Format(&set))}\n\n\n获取 source info,我这里输出的是JSON\n\n{ "file": [ { "name": "api.proto", "messageType": [ { "name": "Request", "field": [ { "name": "name", "number": 1, "label": "LABEL_OPTIONAL", "type": "TYPE_STRING", "jsonName": "name" }, { "name": "nums", "number": 3, "label": "LABEL_REPEATED", "type": "TYPE_INT32", "jsonName": "nums", "options": { "packed": true } }, { "name": "kv", "number": 4, "label": "LABEL_REPEATED", "type": "TYPE_MESSAGE", "typeName": ".Request.KvEntry", "jsonName": "kv" }, { "name": "num", "number": 5, "label": "LABEL_REQUIRED", "type": "TYPE_ENUM", "typeName": ".Num", "jsonName": "num" }, { "name": "req", "number": 6, "label": "LABEL_OPTIONAL", "type": "TYPE_MESSAGE", "typeName": ".Request.InlineRequest", "jsonName": "req" } ], "nestedType": [ { "name": "KvEntry", "field": [ { "name": "key", "number": 1, "label": "LABEL_OPTIONAL", "type": "TYPE_STRING", "jsonName": "key" }, { "name": "value", "number": 2, "label": "LABEL_OPTIONAL", "type": "TYPE_INT64", "jsonName": "value" } ], "options": { "mapEntry": true } }, { "name": "InlineRequest" } ] }, { "name": "Response" } ], "enumType": [ { "name": "Num", "value": [ { "name": "One", "number": 1 }, { "name": "Tow", "number": 2 }, { "name": "Four", "number": 4 } ] } ], "service": [ { "name": "Demo", "method": [ { "name": "MethodName", "inputType": ".Request", "outputType": ".Response" } ] } ], "options": { "javaPackage": "com.byted.xxx.xxx" }, "sourceCodeInfo": { "location": [ { "span": [ //表示整个文件 0, 0, 48, 1 ] }, { "path": [ 12 ], "span": [ // 第1行,定义了syntax 0, 0, 18 ] }, { "path": [ 8 ], "span": [ // 第3行,定义了 FileOptions 2, 0, 42 ] }, { "path": [ // 第3行,定义了 FileOptions.java_package 8, 1 ], "span": [ 2, 0, 42 ] }, { "path": [ // 第5行-17行,定义了第一个枚举 5, 0 ], "span": [ 4, 0, 16, 1 ] }, { "path": [ // 第5行,定义了第一个枚举的名称 5, 0, 1 ], "span": [ 4, 5, 8 ] }, { "path": [ // 第7行,定义了第一个枚举的第一个枚举值 5, 0, 2, 0 ], "span": [ 6, 4, 12 ], "leadingComments": " 注释\\n", "trailingComments": " 注释\\n" }, { "path": [ 5, 0, 2, 0, 1 ], "span": [ 6, 4, 7 ] }, { "path": [ 5, 0, 2, 0, 2 ], "span": [ 6, 10, 11 ] }, { "path": [ 5, 0, 2, 1 ], "span": [ 8, 4, 12 ], "leadingComments": " 注释\\n" }, { "path": [ 5, 0, 2, 1, 1 ], "span": [ 8, 4, 7 ] }, { "path": [ 5, 0, 2, 1, 2 ], "span": [ 8, 10, 11 ] }, { "path": [ 5, 0, 2, 2 ], "span": [ 15, 4, 13 ], "leadingComments": " 333\\n", "trailingComments": " 注释\\n", "leadingDetachedComments": [ " Three = 3;\\n 1111\\n 222\\n" ] }, { "path": [ 5, 0, 2, 2, 1 ], "span": [ 15, 4, 8 ] }, { "path": [ 5, 0, 2, 2, 2 ], "span": [ 15, 11, 12 ] }, { "path": [ // 第20行-39行 定义了 第一个结构体 4, 0 ], "span": [ 19, 0, 38, 1 ], "leadingComments": " 111\\n" }, { "path": [ 4, 0, 1 ], "span": [ 19, 8, 15 ] }, { "path": [ 4, 0, 2, 0 ], "span": [ 22, 4, 29 ], "leadingComments": " 1\\n 2\\n" }, { "path": [ 4, 0, 2, 0, 4 ], "span": [ 22, 4, 12 ] }, { "path": [ 4, 0, 2, 0, 5 ], "span": [ 22, 13, 19 ] }, { "path": [ 4, 0, 2, 0, 1 ], "span": [ 22, 20, 24 ] }, { "path": [ 4, 0, 2, 0, 3 ], "span": [ 22, 27, 28 ] }, { "path": [ 4, 0, 2, 1 ], "span": [ 26, 4, 44 ], "leadingComments": "*\\n 123\\n" }, { "path": [ 4, 0, 2, 1, 4 ], "span": [ 26, 4, 12 ] }, { "path": [ 4, 0, 2, 1, 5 ], "span": [ 26, 13, 18 ] }, { "path": [ 4, 0, 2, 1, 1 ], "span": [ 26, 19, 23 ] }, { "path": [ 4, 0, 2, 1, 3 ], "span": [ 26, 26, 27 ] }, { "path": [ 4, 0, 2, 1, 8 ], "span": [ 26, 28, 43 ] }, { "path": [ 4, 0, 2, 1, 8, 2 ], "span": [ 26, 29, 42 ] }, { "path": [ 4, 0, 2, 2 ], "span": [ 30, 4, 30 ], "leadingComments": " title\\nhello world\\n" }, { "path": [ 4, 0, 2, 2, 6 ], "span": [ 30, 4, 22 ] }, { "path": [ 4, 0, 2, 2, 1 ], "span": [ 30, 23, 25 ] }, { "path": [ 4, 0, 2, 2, 3 ], "span": [ 30, 28, 29 ] }, { "path": [ 4, 0, 2, 3 ], "span": [ 32, 4, 24 ] }, { "path": [ 4, 0, 2, 3, 4 ], "span": [ 32, 4, 12 ] }, { "path": [ 4, 0, 2, 3, 6 ], "span": [ 32, 13, 16 ] }, { "path": [ 4, 0, 2, 3, 1 ], "span": [ 32, 17, 20 ] }, { "path": [ 4, 0, 2, 3, 3 ], "span": [ 32, 22, 23 ] }, { "path": [ 4, 0, 2, 4 ], "span": [ 34, 4, 36 ] }, { "path": [ 4, 0, 2, 4, 4 ], "span": [ 34, 4, 12 ] }, { "path": [ 4, 0, 2, 4, 6 ], "span": [ 34, 14, 27 ] }, { "path": [ 4, 0, 2, 4, 1 ], "span": [ 34, 28, 31 ] }, { "path": [ 4, 0, 2, 4, 3 ], "span": [ 34, 34, 35 ] }, { "path": [ 4, 0, 3, 1 ], "span": [ 35, 4, 37, 5 ] }, { "path": [ 4, 0, 3, 1, 1 ], "span": [ 35, 12, 25 ] }, { "path": [ 4, 1 ], "span": [ 40, 0, 19 ] }, { "path": [ 4, 1, 1 ], "span": [ 40, 8, 16 ] }, { "path": [ 6, 0 ], "span": [ 42, 0, 48, 1 ], "trailingComments": " 33\\n" }, { "path": [ 6, 0, 1 ], "span": [ 42, 8, 12 ] }, { "path": [ 6, 0, 2, 0 ], "span": [ 46, 4, 48 ], "leadingComments": " 22\\n", "trailingComments": " 11\\n" }, { "path": [ 6, 0, 2, 0, 1 ], "span": [ 46, 8, 18 ] }, { "path": [ 6, 0, 2, 0, 2 ], "span": [ 46, 20, 27 ] }, { "path": [ 6, 0, 2, 0, 3 ], "span": [ 46, 38, 46 ] } ] } } ]}\n\n如何解析/使用 SourceCodeInfo那么问题来了,我们需要SourceCodeInfo 做啥了,那当然是做 代码生成、接口文档展示、IDE了!\n具体做法大概有两种做法,懒加载/预加载,这两种方法都可取,看实际需求即可!\n针对于场景一个人比较推荐预加载,因为懒加载需要每次都需要遍历 SourceCodeInfo,而预加载性能会很高!\n懒加载我们知道对于一个字段/类型来说,它的索引是固定的,我们只需要去SourceCodeInfo中查找即可,为此实现会很简单\n例如我们要查找 Num.One 的注释那么根据索引,可以知道是 一个枚举的第一个字段,因此坐标是 [5,0,2,0],那么结果就是\n{ "path": [ 5, 0, 2, 0 ], "span": [ 6, 4, 12 ], "leadingComments": " 注释\\n", "trailingComments": " 注释\\n"}\n\n预加载就是先遍历,实际上如果我们只是处理字段的注释,那么只需要处理以下几个对号入座即可!\n\n[4,x,2,x]: 表示message字段注释\n[5,x,2,x]:表示enum 字段注释\n[6,x,2,x]:表示method 注释\n[4,x]:表示message 注释\n[5,x]:表示enum注释\n[6,x]:表示service注释\n\n参考文章\nBest way to parse SourceCodeInfo Data From Protobuf Files\n\n","categories":["RPC"],"tags":["Protobuf"]},{"title":"grpc","url":"/2021/11/29/5d738324e2189d8e9a1c3974d34f5a95/","content":" 学习grpc文件\n\n\n1、rpc1、wikipedia概念在分布式计算,远程过程调用(英语:Remote Procedure Call,缩写为 RPC)是一个计算机通信协议。该协议允许运行于一台计算机的程序调用另一个地址空间(通常为一个开放网络的一台计算机)的子程序,而程序员就像调用本地程序一样,无需额外地为这个交互作用编程(无需关注细节)。RPC是一种服务器-客户端(Client/Server)模式,经典实现是一个通过发送请求-接受回应进行信息交互的系统。\n如果涉及的软件采用面向对象编程,那么远程过程调用亦可称作远程调用或远程方法调用,例:Java RMI。\nRPC是一种进程间通信的模式,程序分布在不同的地址空间里。如果在同一主机里,RPC可以通过不同的虚拟地址空间(即便使用相同的物理地址)进行通讯,而在不同的主机间,则通过不同的物理地址进行交互。许多技术(常常是不兼容)都是基于这种概念而实现的。\n\n2、http和rpc的区别1、http指的是一个应用层协议,它提供的只是一个传输协议\n2、rpc讲的是一个远程过程调用,它是一个过程,一个rpc架构包含了多个层级,以dubbo的架构来\nrpc其实架构很简单,就是下面一图,但是具体实现上差异还是有点,比如我们所了解的 http + json 只能说是dubbo 只能说是dubbo的最下层实现,所以 rpc相对来说偏向于服务治理这一块\n\n\n3、为什么我们需要rpc框架,http1.1 提供的rest api不行吗1、不支持长连接,keepalive \n2、http1.1 ping-pang client - server http , 建立很多个连接 (http tcp 连接慢,传输/窗口) \n3、rpc 多路复用能力 (http2/3) client - server (io) ,server 包 (二进制) -> go 顺序 http2 奇偶 \n4、并发性\n5、rpc tcp 传输 -> http1.1 纯文本 头部, rcp hello 行头体 \n5、 json rpc \n4、比较出名的rpc框架\nJava: JNI , WebService , Dubbo ,HSF ,spring的Feign那一套,grpc\nGolang: gorpc ,grpc 等\nC++:Svrkit ,grpc 等\n\n大厂用的吧,各大厂都有自己的轮子,比如 thrift,gprc,Tars,brpc ,motan, dubbo 还有很多吧\n其实轮生态的话,绝对是开源项目的生态比较好,所以开源项目中生态比较好的就是 grpc,thrift,dubbo ,使用难度上来看 dubbo是最简单的,如果你们全Java的话它是个不错的选择!\n2、grpc介绍\n gRPC 是一个现代的开源高性能远程过程调用(Remote Procedure Call,RPC)框架,可以在任何环境中运行。它可以高效地连接数据中心内部和跨数据中心的服务,并为负载平衡、跟踪、健康检查和身份验证提供可插拔的支持。它也适用于最后一英里的分布式计算连接设备,移动应用程序和浏览器的后端服务。\n\n1、开源的grpc官方,主要提供的能力\n\n服务调用(提供有stream流,请求响应类型的)\n负载均衡 (提供xsd协议)\n权限(安全方面)\n\n2、grpc主要采用的技术\n\n传输层:http2协议\n序列化层:protobuf \n负载均衡:xsd\n\n3、推荐学习文章,其实grpc是奔着一个规范去走了,所以在开源项目中所使用grpc的项目有很多,云原生中大量的项目使用grpc\n关于序列化和反序列化的思考\n关于HTTP2相关知识\nXDS标准引入GRPC\n关于xsd的学习\n➜ grpc-go git:(master) ✗ tree -L 2.├── api ## pb 项目(不同业务组名称是不一样的,像我们组直接叫项目名字直接叫api)│ ├── Makefile ## 脚本│ ├── bin ## protoc / protoc-gen-go/ protoc-gen-gofast 脚本用来生成pb文件│ ├── dto ## 传输层数据│ ├── go.mod│ ├── go.sum│ └── third ## rpc接口,我们这里叫做third└── service ## 业务项目 ├── client.go ├── common ##common ├── go.mods ├── go.sum ├── lbs-service.go ├── service ## 具体业务层 └── user-service.go\n\n3、protobuf 介绍1、介绍和文档\n Protocol Buffers 是一种语言无关、平台无关、可扩展的序列化结构数据的方法,它可用于(数据)通信协议、数据存储等。Protocol Buffers 是一种灵活,高效,自动化机制的结构数据序列化方法-可类比 XML,但是比 XML 更小(3 ~ 10倍)、更快(20 ~ 100倍)、更为简单。你可以定义数据的结构,然后使用特殊生成的源代码轻松的在各种数据流中使用各种语言进行编写和读取结构数据。你甚至可以更新数据结构,而不破坏由旧数据结构编译的已部署程序。\n\n\nwiki: https://en.wikipedia.org/wiki/Protocol_Buffers\nprotobuf是 Google 的语言中立、平台中立、可扩展的结构化数据序列化机制\n编译器是用 c + + 编写,高性能!\n支持各种开发语言,有效解决了跨平台问题,跨平台开发\n其实就是 pb 核心的一点:高性能、高压缩率、兼容性比较高\n语法简单,学习难度较低\ngithub官方地址:https://github.com/protocolbuffers/protobuf \nprotobuf go support (golang):https://github.com/golang/protobuf\nprotobuf go support (gogo):https://github.com/gogo/protobuf\n官方文档:https://developers.google.com/protocol-buffers/\n相当的火热,在c/cpp/java/go/云原生/Native App 中火热程度很高,使用范围也很广, 学习只有好处没有坏处\n\n2、基础学习1、基本格式syntax = "proto3"; // 默认走的是 proto2,所以需要强制指定,pb3比pb2语法上简单package dto; //当前文件的package,我们项目不喜欢使用前缀类似于 project-name.dto,因为根本不会存在多个项目互相引用!根据你们需求去决定import "dto/demo.proto"; // import 其他文件,相对于--proto_path 路径的相对路径option java_multiple_files = true;option java_package = "com.grpc.api.third"; //java包的路径option go_package="api/dto";// go的包路径(切记一定要写全,和go的import有关系,如果写 dto,那么别人引入你就是 dto了,显然是引入不到的,一定要写全)message SearchRequest {// 每个字段都有四块组成: 字段规则,类型,名称,编号 string query = 1; int32 page_number = 2; int32 result_per_page = 3; repeated string hobby = 4;}\n\n2、类型\n字符串:string ,(默认为””)\n整型:int32, int64, uint32, uint64 (默认为0)\n浮点: double, float (默认为0)\n字节流: bytes (默认为nil)\n布尔类型:bool ,(默认false)\n枚举:enum (默认为0,枚举的第一个值必须是0)\nmap类型: 比如 map<string,string> (默认为nil)\noneof 类型,它是一个结构体,但是它定义你结构体中只能赋值一个字段,就算你有多个字段!(默认为nil)\n\n3、编号 消息定义中的每个字段都有一个唯一的编号。这些字段编号用于以消息二进制格式标识字段,在使用消息类型后不应更改。注意,范围1到15中的字段编号需要一个字节进行编码,包括字段编号和字段类型。范围16到2047的字段编号采用两个字节。因此,应该为经常出现的消息元素保留数字1到15。记住为将来可能添加的频繁出现的元素留出一些空间。\n\n大小限制:1-2^29-1 , 19000 through 19999 不能使用\n\n4、字段规则\nsingular,默认类型就是这个,不需要申明\nrepeated, 类似于数组,go里面默认就转换成了数组,是个list所以不会自动去重\n\n5、注释两种格式,和java/c++/go保持一致\n/** 注释*/// 注释\n\n6、删除/保留字段message SearchRequest { string query = 1; int32 page_number = 2; int32 result_per_page = 3; repeated string hobby = 4; reserved 15, 9 to 11; reserved "foo", "bar";}\t\n\n7、其他1、支持任意的结构体嵌套message Outer { // Level 0 message MiddleAA { // Level 1 message Inner { // Level 2 int64 ival = 1; bool booly = 2; } } message MiddleBB { // Level 1 message Inner { // Level 2 int32 ival = 1; bool booly = 2; } }}\n\n2、任意类型\n 这里代表的不是任意的数据类型,指的是 Message 类型!\n\nsyntax="proto3";package dto;option java_multiple_files = true;option java_package = "com.grpc.api.third";option go_package="dto";import "google/protobuf/any.proto";message City { uint64 id=1; string name=2; // 引用包名称.类型名称 google.protobuf.Any any = 9;}\n\n3、map类型message City { uint64 id=1; string name=2; // 引用包名称.类型名称 google.protobuf.Any any = 9; map<string,uint64> demo=10;}\n\n4、oneof 类型下面为例子,就是你只能选择 v3 或者 v4 !\nmessage BenchMarkModel { string v1=1; uint64 v2=2; oneof test_oneof { string v3 = 3; uint32 v4 = 4; }}\n\n8、import 作用首先 import 是引用 路径的,那个路径是相对于你的 --proto_path 路径的路径\n比如我的项目中,引用就是,项目路径是 /Users/fanhaodong/project/programing/grpc-go/api\nbin/protoc \\ --proto_path /Users/fanhaodong/project/programing/grpc-go/api \\ --proto_path /Users/fanhaodong/go/src/github.com/gogo/protobuf/protobuf \\ --plugin=protoc-gen-go=bin/protoc-gen-go \\ --go_out=. \\ ./dto/*.proto\n\n那么我的import 可以来自于两个路径\nimport "dto/city.proto";import "google/protobuf/any.proto";\n\n首先 dto/city.proto ,绝对路径是 /Users/fanhaodong/project/programing/grpc-go/api/dto/city.proto 我们的编译目录是 /Users/fanhaodong/project/programing/grpc-go/api ,所以去除前缀就是 dto/city.proto\n然后看google/protobuf/any.proto 也是同理\n9、package 作用主要是区分 package 区分 import 可能引用了相同的 message ,所以就需要 package指定,一般package命令为比如我的目录是 api/dto,所以一般命名为 api.dto ,一般来说跨业务组是不允许相互引用,只能引用一些common的结构\n比如下面这种情况,我引用了两个 文件 dto/demo.proto, google/protobuf/any.proto\ndemo.proto 定义了 Any 类型\nmessage Any { string name=1;}\n\n但是我这时候没有告诉我到底用的哪个 Any,此时编译期无法解决\nsyntax="proto3";package dto;option java_multiple_files = true;option java_package = "com.grpc.api.dto";option go_package="dto";import "google/protobuf/any.proto";import "dto/demo.proto";message City { uint64 id=1; string name=2; // 引用包名称.类型名称 Any any = 9; map<string,uint64> demo=10;}\n\n可以看到\ntype City struct {\tstate protoimpl.MessageState\tsizeCache protoimpl.SizeCache\tunknownFields protoimpl.UnknownFields\tId uint64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`\tName string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`\t// 引用包名称.类型名称\tAny *Any `protobuf:"bytes,9,opt,name=any,proto3" json:"any,omitempty"`\tDemo map[string]uint64 `protobuf:"bytes,10,rep,name=demo,proto3" json:"demo,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"varint,2,opt,name=value,proto3"`}\n\n所以存在这种问题,此时就需要 package解决引用的问题\nmessage City { uint64 id=1; string name=2; // 引用包名称.类型名称 google.protobuf.Any any = 9; map<string,uint64> demo=10;}\n\n3、Protobuf的编码算法原理Encoding\n4、编译工具\n官方编译工具,比如我是mac os ,可以直接下载 wget https://github.com/protocolbuffers/protobuf/releases/download/v3.9.0/protoc-3.9.0-osx-x86_64.zip\ngogoprotobuf ,对于需要编码为Golang时,其编码效率贼高而且还省内存, 对立项目是 https://github.com/golang/protobuf\n如何编译了 ? --proto_path指定include 目录, --plugin 指定需要用的plugin, 最后一个参数指的是引用的pb文件\n\n使用测试的pb 文件\nsyntax="proto3";package dto;option java_multiple_files = true;option java_package = "com.grpc.api.dto";option go_package="api/dto";import "google/protobuf/any.proto";import "google/protobuf/duration.proto";import "google/protobuf/timestamp.proto";import "google/protobuf/empty.proto";import "google/protobuf/wrappers.proto";message BenchMarkModel { string v1=1; uint64 v2=2; int64 v3=3; double v4=4; float v5=5; bool v6=6; enum Corpus { UNIVERSAL = 0; WEB = 1; IMAGES = 2; LOCAL = 3; NEWS = 4; PRODUCTS = 5; VIDEO = 6; } Corpus v7=7; map<string,uint32> v8=8; oneof test_oneof { string v9 = 9; uint32 v10 = 10; } bytes v11=11; message Msg { string content=1; } repeated Msg v12=12; google.protobuf.Any v13 = 13; google.protobuf.Duration v14=14; google.protobuf.Empty v15=15; google.protobuf.UInt64Value v16=16; google.protobuf.Timestamp v17=17;}\n\n\n\n2、测试代码\npackage dtoimport (\t"github.com/golang/protobuf/proto"\t"github.com/stretchr/testify/assert"\t"google.golang.org/protobuf/types/known/anypb"\t"google.golang.org/protobuf/types/known/durationpb"\t"google.golang.org/protobuf/types/known/emptypb"\t"google.golang.org/protobuf/types/known/timestamppb"\t"google.golang.org/protobuf/types/known/wrapperspb"\t"math"\t"testing"\t"time")func newModel(b testing.TB) *BenchMarkModel {\tany := &anypb.Any{}\tif err := any.MarshalFrom(wrapperspb.UInt64(math.MaxUint64)); err != nil {\t\tb.Fatal(err)\t}\treturn &BenchMarkModel{\t\tV1: "hello 12345424234234",\t\tV2: math.MaxUint64,\t\tV3: math.MaxInt64,\t\tV4: math.MaxFloat64,\t\tV5: math.MaxFloat32,\t\tV6: true,\t\tV7: BenchMarkModel_PRODUCTS,\t\tV8: map[string]uint32{"1": 1, "2": 2, "3": 3},\t\tTestOneof: &BenchMarkModel_V10{\t\t\tV10: math.MaxUint32,\t\t},\t\tV11: []byte("hello 1234567890"),\t\tV12: []*BenchMarkModel_Msg{\t\t\t{Content: "1"}, {Content: "2"}, {Content: "3"},\t\t},\t\tV13: any,\t\tV14: durationpb.New(time.Hour * 24),\t\tV15: &emptypb.Empty{},\t\tV16: wrapperspb.UInt64(math.MaxUint64),\t\tV17: timestamppb.Now(),\t}}func TestGo_Marshal(t *testing.T) {\tmodel := newModel(t)\tprotoBufBody, err := proto.Marshal(model)\tif err != nil {\t\tt.Fatal(err)\t}\tnewModel := &BenchMarkModel{}\tif err := proto.UnmarshalMerge(protoBufBody, newModel); err != nil {\t\tt.Fatal(err)\t}\tassertModel(t, model, newModel)}func assertModel(t testing.TB, model *BenchMarkModel, newModel *BenchMarkModel) {\tassert.Equal(t, model.V1, newModel.V1)\tassert.Equal(t, model.V2, newModel.V2)\tassert.Equal(t, model.V3, newModel.V3)\tassert.Equal(t, model.V4, newModel.V4)\tassert.Equal(t, model.V5, newModel.V5)\tassert.Equal(t, model.V6, newModel.V6)\tassert.Equal(t, model.V7, newModel.V7)\tassert.Equal(t, model.V8, newModel.V8)\tassert.Equal(t, model.TestOneof, newModel.TestOneof)\tassert.Equal(t, model.V11, newModel.V11)\tfor index, _ := range model.V12 {\t\tassert.Equal(t, model.V12[index].Content, newModel.V12[index].Content)\t} \tassert.Equal(t, model.V13.Value, newModel.V13.Value)\tassert.Equal(t, model.V13.TypeUrl, newModel.V13.TypeUrl)\tassert.Equal(t, model.V14.Nanos, newModel.V14.Nanos)\tassert.Equal(t, model.V14.Seconds, newModel.V14.Seconds)\tassert.Equal(t, model.V15, newModel.V15)\tassert.Equal(t, model.V16.Value, newModel.V16.Value)\tassert.Equal(t, model.V17.Seconds, newModel.V17.Seconds)\tassert.Equal(t, model.V17.Nanos, newModel.V17.Nanos)}\n\n1、使用protoc-gen-go这里需要知道的是 --go_out 需要找到 protoc-gen-go 的位置,如果你的protoc-gen-go放在 PATH目录下就可以直接使用,但是我们一般不会放在PATH目录下,所以需要指定 --plugin=protoc-gen-go=bin/protoc-gen-go,意思就是告诉位置所在\nbin/protoc \\ --proto_path . \\ --proto_path /Users/fanhaodong/go/grpc/include \\ --plugin=protoc-gen-go=bin/protoc-gen-go \\ --go_out=../ \\ ./dto/*.proto\n\n帮助\n--plugin=EXECUTABLE Specifies a plugin executable to use. Normally, protoc searches the PATH for plugins, but you may specify additional executables not in the path using this flag. Additionally, EXECUTABLE may be of the form NAME=PATH, in which case the given plugin name is mapped to the given executable even if the executable's own name differs.\n\n其实规则就是\n--plugin=protoc-gen-go=bin/protoc-gen-go 对应的必须是 --go_out,它走的是拼前缀,比如你执行这个也OK,因为是看前缀说话\nbin/protoc \\ --proto_path . \\ --proto_path /Users/fanhaodong/go/grpc/include \\ --plugin=protoc-gen-go1=bin/protoc-gen-go \\ --go1_out=../ \\ ./dto/*.proto\n\n开始进入话题进行benchmark\nfunc BenchmarkGo_Marshal(b *testing.B) {\tmodel := newModel(b)\tfor i := 0; i < b.N; i++ {\t\tif _, err := proto.Marshal(model); err != nil {\t\t\tb.Fatal(err)\t\t}\t}}func BenchmarkGo_UnMarshal(b *testing.B) {\tmodel := newModel(b)\tresult, err := proto.Marshal(model)\tif err != nil {\t\tb.Fatal(err)\t}\tnewModel := &BenchMarkModel{}\tfor i := 0; i < b.N; i++ {\t\tif err := proto.UnmarshalMerge(result, newModel); err != nil {\t\t\tb.Fatal(err)\t\t}\t}}\n\n测试结果\n➜ dto git:(master) ✗ go test -run=none -bench=BenchmarkGo_ -benchmem -count=4 . goos: darwingoarch: amd64pkg: api/dtoBenchmarkGo_Marshal-12 428138 2624 ns/op 600 B/op 17 allocs/opBenchmarkGo_Marshal-12 431756 2552 ns/op 600 B/op 17 allocs/opBenchmarkGo_Marshal-12 418332 2595 ns/op 600 B/op 17 allocs/opBenchmarkGo_Marshal-12 503637 2520 ns/op 600 B/op 17 allocs/opBenchmarkGo_UnMarshal-12 537661 2824 ns/op 555 B/op 19 allocs/opBenchmarkGo_UnMarshal-12 542142 2398 ns/op 554 B/op 19 allocs/opBenchmarkGo_UnMarshal-12 509076 2420 ns/op 563 B/op 19 allocs/opBenchmarkGo_UnMarshal-12 544599 2063 ns/op 553 B/op 19 allocs/opPASSok api/dto 11.746s\n\n2、使用 protoc-gen-gofast1、首先需要安装\ngo install github.com/gogo/protobuf/protoc-gen-gofast ## protoc-gen-gofast 工具go get github.com/gogo/protobuf ## 代码依赖\n\n2、这里使用了Any类型,所以需要我们做gofast的兼容,这点比较坑,官网也写了如何解决:https://github.com/gogo/protobuf , 因此编译命令需要添加一些参数!\nbin/protoc \\ --proto_path . \\ --proto_path /Users/fanhaodong/go/src/github.com/gogo/protobuf/protobuf \\ --plugin=protoc-gen-gofast=bin/protoc-gen-gofast \\ --gofast_out=\\ Mgoogle/protobuf/any.proto=github.com/gogo/protobuf/types,\\ Mgoogle/protobuf/duration.proto=github.com/gogo/protobuf/types,\\ Mgoogle/protobuf/field_mask.proto=github.com/gogo/protobuf/types,\\ Mgoogle/protobuf/struct.proto=github.com/gogo/protobuf/types,\\ Mgoogle/protobuf/type.proto=github.com/gogo/protobuf/types,\\ Mgoogle/protobuf/api.proto=github.com/gogo/protobuf/types,\\ Mgoogle/protobuf/descriptor.proto=github.com/gogo/protobuf/types,\\ Mgoogle/protobuf/empty.proto=github.com/gogo/protobuf/types,\\ Mgoogle/protobuf/source_context.proto=github.com/gogo/protobuf/types,\\ Mgoogle/protobuf/timestamp.proto=github.com/gogo/protobuf/types,\\ Mgoogle/protobuf/wrappers.proto=github.com/gogo/protobuf/types:../ \\ ./dto/*.proto\n\n3、最坑的来了,也就是它为啥不兼容的问题!\n1)原来定义的any现在不能使用了,也就是说api的使用方式上变了!\nimport (\t"encoding/json"\t"github.com/gogo/protobuf/types"\t"github.com/golang/protobuf/proto"\t"github.com/stretchr/testify/assert"\t"math"\t"testing"\t"time")func newModel(b testing.TB) *BenchMarkModel {\tany, err := types.MarshalAny(&types.UInt64Value{\t\tValue: math.MaxUint64,\t})\tif err != nil {\t\tb.Fatal(err)\t}\treturn &BenchMarkModel{\t\tV1: "hello 12345424234234",\t\tV2: math.MaxUint64,\t\tV3: math.MaxInt64,\t\tV4: math.MaxFloat64,\t\tV5: math.MaxFloat32,\t\tV6: true,\t\tV7: BenchMarkModel_PRODUCTS,\t\tV8: map[string]uint32{"1": 1, "2": 2, "3": 3},\t\tTestOneof: &BenchMarkModel_V10{\t\t\tV10: math.MaxUint32,\t\t},\t\tV11: []byte("hello 1234567890"),\t\tV12: []*BenchMarkModel_Msg{{Content: "1"}, {Content: "2"}, {Content: "3"},},\t\tV13: any,\t\tV14: types.DurationProto(time.Hour * 24),\t\tV15: &types.Empty{},\t\tV16: &types.UInt64Value{\t\t\tValue: math.MaxUint64,\t\t},\t\tV17: types.TimestampNow(),\t}}\n\n3、benchmark \n➜ dto git:(master) ✗ go test -run=none -bench=BenchmarkGoFast_ -benchmem -count=4 .goos: darwingoarch: amd64pkg: api/dtoBenchmarkGoFast_Marshal-12 1579309 748 ns/op 240 B/op 2 allocs/opBenchmarkGoFast_Marshal-12 1487350 840 ns/op 240 B/op 2 allocs/opBenchmarkGoFast_Marshal-12 1389932 765 ns/op 240 B/op 2 allocs/opBenchmarkGoFast_Marshal-12 1532866 784 ns/op 240 B/op 2 allocs/opBenchmarkGoFast_UnMarshal-12 1000000 1173 ns/op 382 B/op 7 allocs/opBenchmarkGoFast_UnMarshal-12 1235286 1001 ns/op 384 B/op 7 allocs/opBenchmarkGoFast_UnMarshal-12 1083085 1191 ns/op 371 B/op 7 allocs/opBenchmarkGoFast_UnMarshal-12 1000000 1144 ns/op 382 B/op 7 allocs/opPASSok api/dto 14.907s\n\n3、总结1、从性能上来看确实提升至少是一倍起步,基本带来了翻倍的收益(官方给的数据是5-10倍的性能提升,它还提供了更快的!1),然后主要是内存分配上可以看到内存优势很大!\n2、但从效率上来说其实对于业务开发其实是不关注太多这些的,开发效率和质量决定一切!\n3、关于选择protoc-gen-gofast 还是选择 protoc-gen-go ,看你们业务已开始用的什么,如果开始就选择 protoc-gen-gofast 那么可以一直用,但是一开始就选择 protoc-gen-go 那就恶心了,基本上无法切换到 protoc-gen-gofast,可以选择使用 protoc-gen-gogo\n4、gRPC1、介绍和文档\n gRPC 是一个现代的开源高性能远程过程调用(Remote Procedure Call,RPC)框架,可以在任何环境中运行。它可以高效地连接数据中心内部和跨数据中心的服务,并为负载平衡、跟踪、健康检查和身份验证提供可插拔的支持。它也适用于最后一英里的分布式计算连接设备,移动应用程序和浏览器的后端服务。\n\n\nc系列(官方系): https://github.com/grpc/grpc\njava 系:https://github.com/grpc/grpc-java\ngo 系:https://github.com/grpc/grpc-go\n官方文档:https://grpc.io/\n如何使用 wireshark 抓取 grpc && http2 包:https://grpc.io/blog/wireshark/\ngrcp 和 http2的关系: https://grpc.io/blog/grpc-on-http2/\n\n2、写rpc接口 (IDL)第一个lbs接口是:\nsyntax="proto3";package third;option java_multiple_files = true;option java_package = "com.grpc.api.third";option go_package="api/third";import "google/protobuf/wrappers.proto";import "dto/city.proto";service LbsService { rpc getCityInfo (google.protobuf.UInt64Value) returns (dto.City);}\n\n第二个是user服务接口:\nsyntax="proto3";package third;option java_multiple_files = true;option java_package = "com.grpc.api.third";option go_package="api/third";import "dto/user.proto";import "google/protobuf/wrappers.proto";service UserService { rpc GetUserInfo (google.protobuf.UInt64Value) returns (dto.User);}\n\n3、gofast编译rcp接口如何编译,我们使用的是 gofast进行编译,所以需要修改一些参数,关于参数如何使用的,这些绝对百度不来,主要是看人家这个gofast项目的文档和example\n这里就是需要告诉一下编译的时候使用 plugin=grpc, 然后还需要改变一下引用,最后就是指定一下输出目录\n格式就是 --{pugin}_out=k1=v1,k2=v2,k3=v3....,kn=vn:{输出目录}\nbin/protoc \\ --proto_path . \\ --proto_path /Users/fanhaodong/go/src/github.com/gogo/protobuf/protobuf \\ --plugin=protoc-gen-gofast=bin/protoc-gen-gofast \\ --gofast_out=plugins=grpc,\\ Mgoogle/protobuf/any.proto=github.com/gogo/protobuf/types,\\ Mgoogle/protobuf/duration.proto=github.com/gogo/protobuf/types,\\ Mgoogle/protobuf/field_mask.proto=github.com/gogo/protobuf/types,\\ Mgoogle/protobuf/struct.proto=github.com/gogo/protobuf/types,\\ Mgoogle/protobuf/type.proto=github.com/gogo/protobuf/types,\\ Mgoogle/protobuf/api.proto=github.com/gogo/protobuf/types,\\ Mgoogle/protobuf/descriptor.proto=github.com/gogo/protobuf/types,\\ Mgoogle/protobuf/empty.proto=github.com/gogo/protobuf/types,\\ Mgoogle/protobuf/source_context.proto=github.com/gogo/protobuf/types,\\ Mgoogle/protobuf/timestamp.proto=github.com/gogo/protobuf/types,\\ Mgoogle/protobuf/wrappers.proto=github.com/gogo/protobuf/types:../ \\ ./third/*.proto\n\n4、go写接口服务调用1、服务端代码\npackage mainimport (\t"api/dto"\t"api/third"\t"context"\t"fmt"\t"github.com/gogo/protobuf/types"\t"google.golang.org/grpc"\t"log"\t"net"\t"service/common"\t"time")type lbsServiceServer struct{}func (lbsServiceServer) GetCityInfo(ctx context.Context, cityId *types.UInt64Value) (*dto.City, error) {\tif cityId.Value == 0 {\t\treturn nil, fmt.Errorf("not found city: %d", cityId.Value)\t}\treturn &dto.City{\t\tId: cityId.Value,\t\tName: fmt.Sprintf("beijing-%d", cityId.Value),\t\tProperties: map[string]string{"time": time.Now().Format(time.RFC3339)},\t\tMsg: &dto.OneofMessage{\t\t\tTestOneof: &dto.OneofMessage_Name{\t\t\t\tName: "demo",\t\t\t},\t\t},\t}, nil}func main() {\t// 创建一个tcp listener\tlis, err := net.Listen("tcp", common.LbsService)\tif err != nil {\t\tlog.Fatalf("failed to listen: %v", err)\t}\t// 创建一个 grpc server\tser := grpc.NewServer()\t// 注册信息\tthird.RegisterLbsServiceServer(ser, lbsServiceServer{})\t// 启动服务\tif err := ser.Serve(lis); err != nil {\t\tlog.Fatalf("failed to serve: %v", err)\t}}\n\n2、客户端代码\npackage mainimport (\t"api/third"\t"context"\t"github.com/gogo/protobuf/types"\t"google.golang.org/grpc"\t"log"\t"service/common")func main() {\tconn, err := grpc.Dial(common.LbsService, grpc.WithInsecure(), grpc.WithBlock())\tif err != nil {\t\tlog.Fatal(err)\t}\tclient := third.NewLbsServiceClient(conn)\tcityInfo, err := client.GetCityInfo(context.Background(), &types.UInt64Value{Value: 1})\tif err != nil {\t\tlog.Fatal(err)\t}\tlog.Printf("%s", cityInfo)}\n\n请求一下可以看到完全可以调通\n2021/04/16 20:10:48 id:1 name:"beijing-1" properties:<key:"time" value:"2021-04-16T20:10:48+08:00" > msg:<name:"demo" > \n\n5、如何抓包1、配置pb位置,比如我的就是在 /Users/fanhaodong/project/programing/grpc-go/api目录下\n\n2、选择抓取网卡\n本地的话一般是就是本地回环网络,我的本地网卡就是 lo0\n\n然后选择过滤的端口,比如我刚刚启动的服务端口是 8001, 然后记得客户端调用一次服务端,就可以看到以下的流量包了\n\n此时可以监控到流量,但是是tcp,所以我们很难看懂,需要需要分析一下,因此需要decode一下包\n\n所以大概你就可以看到所有包的情况了!\n6、http27、grpc conn pool1、如果发现 [] conn\n2、如果发现 conn (http2 http3 ) \n3、\n5、使用go-micro 搭建grpc服务1、官方文档https://github.com/Anthony-Dong/go-micro\n\n微解决了在云中构建服务的关键需求。它利用微服务体系结构模式,并提供一组作为平台构建块的服务。微处理分布式系统的复杂性,并提供更简单的可编程抽象。\n\n其实看起来和那个现在比较火的 dapr 很像,抽象的级别很高,更加傻瓜式,但是你要是研究的话往往会增大学习成本\n2、快速开始的话,你就根据官方提供的就行了\n2、提供的能力1、基本上人家帮代码给你封装好了,直接用\n2、提供api-gateway 方便使用\n3、提供有 一些内部提供的服务治理能力,需要细细学习\n4、有兴趣可以学习一下\n","categories":["RPC"],"tags":["GRPC","Protobuf"]},{"title":"多进程 - daemon进程和优雅重启","url":"/2023/08/14/62cb47bb883c0e290c18a7ebd8ff9b3a/","content":"本文会详细介绍主流的daemon进程的实现方案,以及网络编程中如何实现优雅重启,这些都是多进程的一些编程技巧!\n\n\n如何创建daemon进程\n为什么我们需要daemon进程?\n\n我们平时做服务器开发都是启动一个程序,这个程序是一个前台程序,但是前台程序它一直在那开着,我想让他后台运行,例如mysql的server,那么怎么解决呢,我自己怎么实现一个daemon进程呢?\n\n常见的手段\n\n\nsystemctl 是linux最常见的手段,它需要软件定义一个 .service 文件,来定义和管理 软件的等 https://www.freedesktop.org/software/systemd/man/systemd.unit.html\n可以用 systemctl status 查看所有 systemctl 的进程,但是貌似需要root权限用起来不太方便….\n\n\n~ cat /lib/systemd/system/docker.service[Unit]Description=Docker Application Container EngineDocumentation=https://docs.docker.comAfter=network-online.target docker.socket firewalld.serviceWants=network-online.targetRequires=docker.socket[Service]Type=notify# the default is not to use systemd for cgroups because the delegate issues still# exists and systemd currently does not support the cgroup feature set required# for containers run by dockerExecStart=/usr/bin/dockerd -H fd://ExecReload=/bin/kill -s HUP $MAINPIDLimitNOFILE=1048576# Having non-zero Limit*s causes performance problems due to accounting overhead# in the kernel. We recommend using cgroups to do container-local accounting.LimitNPROC=infinityLimitCORE=infinity# Uncomment TasksMax if your systemd version supports it.# Only systemd 226 and above support this version.TasksMax=infinityTimeoutStartSec=0# set delegate yes so that systemd does not reset the cgroups of docker containersDelegate=yes# kill only the docker process, not all processes in the cgroupKillMode=process# restart the docker process if it exits prematurelyRestart=on-failureStartLimitBurst=3StartLimitInterval=60s[Install]WantedBy=multi-user.target\n\n其实大概描述了,当前软件详情信息,如何启动当前软件等,具体可以参考这个文章 https://segmentfault.com/a/1190000023029058\n\nnohup 就更简单了,只需要 nohup 命令一下即可,其实它所做的就更简单了,就是一个后台运行,并不会涉及到重启等操作\n\nsupervisor 我个人感觉就和 systemctl差不多\n\n\n如何实现一个daemon进程首先需要了解一个进程的机制,例如我们在shell里执行了一个命令(非nohup),那么整体流程是,shell进程启动了我们的进程,那么我们当前进程的父亲进程就是 shell 进程,当我们把shell进程关了,那么我们的进程也没了!\n下图是我写了一个 Go代码,其实就是解释了上面说的!整个进程的关系!\n\n根据上面描述基本无解了,那么怎么办呢,实际上这里就需要用到孤儿进程,孤儿进程的父进程ID是1,他的回收权就转移给了init进程(进程ID为1),那么如何创建一个孤儿进程了!\n其实很简单,就是当父进程退出,子进程还在运行,此时子进程就是孤儿进程了,孤儿进程的父亲进程为init进程(进程ID=1)!\n僵尸进程(zomibe)进程产生的原因是因为子进程退出后,父进程没有退出但是也没有及时清理子进程的资源,当父进程退出了僵尸进程会自动回收,僵尸进程造成的问题就是占用系统资源(pid资源)!\n#include <iostream>#include <unistd.h>#include <cerrno>#include <cstring>// g++ -std=c++14 main.cpp -o mainint main() { std::cout << "main start: " << getpid() << "\\n"; auto child_pid = fork(); if (child_pid < 0) { std::cout << "fork find err: " << strerror(errno) << "\\n"; } if (child_pid == 0) { // 子进程 std::cout << "child pid " << getpid() << " start" << "\\n"; sleep(1); std::cout << "child pid " << getpid() << " end" << "\\n"; return 0; } std::cout << "main start wait child process success: " << child_pid << "\\n"; sleep(20); // 这里子进程1s退出后子进程身份就标记为僵尸进程了.// if (waitpid(child_pid, nullptr, 0) != child_pid) { // 正确做法是分配有限的资源或者及时回收// std::cout << "main wait child process find err: " << strerror(errno) << "\\n";// return 1;// } std::cout << "main wait child process success: " << child_pid << "\\n"; // 当父进程退出僵尸进程就会被清理了.(如果系统创建大量的子进程且子进程退出后没及时回收就会造成系统进程无法分配)}\n\n具体文章可以看: \n\nhttps://blog.csdn.net/a745233700/article/details/120715371\nhttps://segmentfault.com/a/1190000038820321\n\n简单实现一个daemon进程这个例子是实现一个 http 服务的daemon进程,直接运行后会后台启动一个http服务!\npackage mainimport (\t"fmt"\t"net/http"\t"os"\t"path/filepath")func main() {\tfmt.Printf("当前ppid: %v, pid: %v, args: %#v\\n", os.Getppid(), os.Getpid(), os.Args)\tif len(os.Args) > 1 && os.Args[1] == "child_process" { // 子进程\t\tfmt.Println("child process")\t\tif err := http.ListenAndServe(":10099", http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {\t\t\tfmt.Printf("method: %s, url: %s\\n", request.Method, request.URL)\t\t\t_, _ = writer.Write([]byte(`hello world`))\t\t})); err != nil {\t\t\tpanic(err)\t\t}\t\treturn\t}\texecutable, err := os.Executable()\tif err != nil {\t\tpanic(err)\t}\tdir, err := os.Getwd()\tif err != nil {\t\tpanic(err)\t}\tfmt.Printf("dir: %s\\n", dir)\tstdout, err := os.OpenFile(filepath.Join(dir, "child_process.log"), os.O_CREATE|os.O_WRONLY, 0644)\tif err != nil {\t\tpanic(err)\t}\tprocess, err := os.StartProcess(executable, []string{os.Args[0], "child_process"}, &os.ProcAttr{\t\tDir: dir,\t\tFiles: []*os.File{ // 共享fd\t\t\tos.Stdin, // stdin\t\t\tstdout, // stdout\t\t\tstdout, // std error\t\t},\t})\tif err != nil {\t\tpanic(err)\t}\tpidFile, err := os.OpenFile(filepath.Join(dir, "child_process.pid"), os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)\tif err != nil {\t\tpanic(err)\t}\tdefer pidFile.Close()\tif _, err := pidFile.WriteString(fmt.Sprintf("%d", process.Pid)); err != nil {\t\tpanic(err)\t}\tfmt.Printf("create child process %d\\n", process.Pid)\treturn\t// 不执行这个,直接return就是孤儿进程了\tif _, err := process.Wait(); err != nil {\t\tpanic(err)\t}}\n\n\n我们成功实现了一个 孤儿进程, 如何结束孤儿进程了, 直接 kill 孤儿进程的进程ID即可\n➜ test git:(master) ✗ ps -ef | grep './main' 502 48190 1 0 2:42PM ttys052 0:00.01 ./main child_process 502 48312 43352 0 2:42PM ttys052 0:00.00 grep --color=auto --exclude-dir=.bzr --exclude-dir=CVS --exclude-dir=.git --exclude-dir=.hg --exclude-dir=.svn --exclude-dir=.idea --exclude-dir=.tox ./main➜ test git:(master) ✗ kill `cat child_process.pid`\n\n注意:Go的StartProcess底层是fork + exec , fork函数是创建一个子进程,exec函数是加载一个程序覆盖当前程序(替换整个程序). 不懂得可以百度下.. \n代码地址: https://coliru.stacked-crooked.com/a/caed3826f0be8391\n#include <iostream>#include <unistd.h>#include <sys/wait.h>#include <cstring>#include <cerrno>int main() { std::cout << "main start process" << std::endl; int count = 0; pid_t cpid = fork(); if (cpid == 0) { // 子进程从这里开始执行(fork 函数会有很多优化,核心是写时复制,所以fork函数开销不大) \t// copy on write https://wingsxdu.com/posts/linux/concurrency-oriented-programming/fork-and-cow/ count = count + 100; std::cout << "child process start. pid: " << getpid() << " , count: " << count << std::endl; sleep(2); // sleep 2s std::cout << "child process replace current exec process" << count << std::endl; // 这里的意思时将子进程替换成 ls 这个程序 if (execl("/bin/ls", "ls", "-a", NULL) == -1) { // 替换程序失败会报错 std::cout << "child process error, err: " << strerror(errno) << std::endl; return 1; } // 子进程替换成功后下面这些代码都不会执行了(因为已经替换了完整的代码段/数据段之类的,完全和这个程序无关了) } else if (cpid == -1) { std::cout << "fork err: " << strerror(errno) << std::endl; return 1; } else { std::cout << "main process: " << getpid() << ", child process: " << cpid << std::endl; waitpid(cpid, nullptr, 0); // 等待子进程结束 std::cout << "main process done, count: " << count << std::endl; } return 0;}\n\n封装 daemon 进程这里我就不造轮子了,大概可以看一下 https://github.com/sevlyar/go-daemon 这个项目,我大概介绍一些几个方法的核心原理\n\nctx\n\nfunc (s *DaemonService) newCtx() *daemon.Context {\treturn &daemon.Context{\t\tPidFileName: filepath.Join(s.homeDir, PidFile), // pid所在文件,主要是解决如何获取子进程pid的问题\t\tPidFilePerm: 0644,\t\tLogFileName: filepath.Join(s.homeDir, LogFile), // 替换子进程的stdout/stderr\t\tLogFilePerm: 0644,\t\tWorkDir: s.homeDir, // 子进程工作目录\t\tUmask: 027, // 文件权限,有兴趣可以查一下\t\tArgs: os.Args, // 子进程参数\t}}\n\n\nDaemonStart\n\nfunc (s *DaemonService) DaemonStart() error {\tctx := s.newCtx()\t// Search\t// 其实就是读取pid文件,判断进程是否存在\tsearch, err := ctx.Search()\tif err == nil && search != nil {\t\treturn fmt.Errorf(`the background program has started. PID: %d`, search.Pid)\t}\t// Reborn\t// 如果是父进程,则返回 child process\t// 如果是子进程,则返回 空 (判断父子进程逻辑很简单就是根据环境变量 _GO_DAEMON=1,子进程会被注入这个环境变量)\tchildProcess, err := ctx.Reborn()\tif err != nil {\t\treturn fmt.Errorf("unable to run background program, reason: %v", err)\t} // 子进程不为空,说明是父进程,直接退出即可(这里子进程就是孤儿进程了)\tif childProcess != nil {\t\tlogs.Infof("start parent process success. pid: %d, cpid: %d", os.Getpid(), childProcess.Pid)\t\treturn nil\t}\tdefer func() {\t\t_ = ctx.Release() // 释放一些当时创建时分配的资源\t}()\tlogs.Infof("start child process success. ppid: %d, pid: %d", os.Getppid(), os.Getpid())\treturn s.run()}\n\n实现优雅重启tcp服务\n现在已经有了 k8s / 自研的发布平台都支持滚动重启了,滚动重启阶段会新建一个新的服务,然后等待旧服务结束。但是吧他比较消耗资源,因为假如你服务1w台,滚动粒度时10%,那么需要冗余1000台服务器的资源!\n原地重启吧,需要实现优雅重启或者暴力重启了,暴力重启可能会短暂影响sla,所以优雅重启也非常重要!\n优雅重启的大概原理就是:多进程的文件共享,这里共享的是tcp socket的文件,当需要重启时候会创建一个新进程,然后通知旧进程关闭监听socket文件,两个进程共享socket文件,新进程启动后会重新监听共享的socket,那么新的连接会打向新进程,旧进程依然处理旧的连接,最后处理完后旧进程会退出,最终实现优雅重启,它很好的解决了新连接/旧连接的处理!\n\n造个轮子这个例子,我是主进程正常创建和监听TCPListener, 当需要重启的时候此时需要关闭 TCPListener 然后创建子进程继续监听,当再监听到重启时同样的需要主进程关闭子进程!\npackage mainimport (\t"fmt"\t"net"\t"net/http"\t"os"\t"os/exec"\t"os/signal"\t"strings"\t"sync"\t"syscall"\t"time")func main() {\tvar name string\tvar err error\tvar listen net.Listener\t// 如果是子进程的话,listen 获取不太一样\tif os.Getenv("is_slave") == "true" {\t\tfile := os.NewFile(uintptr(3), "")\t\tlisten, err = net.FileListener(file)\t\tname = fmt.Sprintf("slave-%d", os.Getpid())\t} else {\t\tlisten, err = net.Listen("tcp", ":10086")\t\tname = "master"\t}\tif err != nil {\t\tpanic(fmt.Errorf("init (%s) listen err: %v", name, err))\t}\tdebug("[%s] start", name)\tgo func() {\t\tif isSlave(name) {\t\t\treturn\t\t}\t\tvar listenFd *os.File\t\tvar loadFD = sync.Once{}\t\tloadListenFd := func() *os.File {\t\t\tloadFD.Do(func() {\t\t\t\ttl := listen.(*net.TCPListener)\t\t\t\tfds, err := tl.File()\t\t\t\tif err != nil {\t\t\t\t\tpanic(fmt.Errorf("tl.File() find err: %v\\n", err))\t\t\t\t}\t\t\t\tif err := listen.Close(); err != nil { // 只需要关闭一次,所以用sync.once\t\t\t\t\tpanic(err)\t\t\t\t}\t\t\t\tlistenFd = fds\t\t\t})\t\t\treturn listenFd\t\t}\t\t// 父亲进程watch 变更\t\tdebug("[%s] watch file changed", name)\t\tvar command *exec.Cmd\t\tvar done chan bool\t\tvar errch chan error\t\tfor {\t\t\t<-time.After(time.Second * 3) // 监听到需要重启进程 (这里可以换成实际程序的)\t\t\tif command != nil {\t\t\t\t// 通知子进程关闭.\t\t\t\tif err := command.Process.Signal(syscall.SIGQUIT); err != nil {\t\t\t\t\tpanic(err)\t\t\t\t}\t\t\t\tselect {\t\t\t\tcase err := <-errch: // todo watch err.\t\t\t\t\tdebug("[%s] close slave-%d err: %v", name, command.Process.Pid, err)\t\t\t\t\tpanic(err)\t\t\t\tcase <-done:\t\t\t\t\tdebug("[%s] close slave-%d success", name, command.Process.Pid)\t\t\t\t}\t\t\t}\t\t\t// 启动子进程\t\t\tdone = make(chan bool, 0)\t\t\terrch = make(chan error, 0)\t\t\tif subCmd, err := startSlaveProcess(loadListenFd(), done, errch); err != nil {\t\t\t\tpanic(err)\t\t\t} else {\t\t\t\tcommand = subCmd\t\t\t}\t\t\tdebug("[%s] run slave-%d success", name, command.Process.Pid)\t\t}\t}()\t// 子进程如果监听到关闭,则需要关闭连接\tdone := make(chan bool, 0)\tif isSlave(name) {\t\tc := make(chan os.Signal)\t\tsignal.Notify(c, syscall.SIGQUIT)\t\tgo func() {\t\t\tvv := <-c\t\t\tif err := listen.Close(); err != nil { // close 不优雅,优雅的话需要用 wait-group\t\t\t\tpanic(err)\t\t\t}\t\t\tdebug("[%s] close listen success, signal: %v", name, vv.String())\t\t\tclose(done)\t\t}()\t}\t// 启动监听\tif err := http.Serve(listen, newHandlerFunc(name)); err != nil {\t\tif strings.Contains(err.Error(), "use of closed network connection") {\t\t\t<-done\t\t\treturn\t\t}\t\tpanic(err) // 别的异常直接panic\t}}func newHandlerFunc(name string) http.HandlerFunc {\treturn func(writer http.ResponseWriter, request *http.Request) {\t\t_, err := writer.Write([]byte(fmt.Sprintf("name: %s, hello world", name)))\t\tif err != nil {\t\t\tpanic(err)\t\t}\t}}func isMaster(name string) bool {\treturn name == "master"}func isSlave(name string) bool {\treturn strings.HasPrefix(name, "slave")}func debug(format string, v ...interface{}) {\tfmt.Printf(format+"\\n", v...)}func startSlaveProcess(fd *os.File, done chan bool, errch chan error) (*exec.Cmd, error) {\texecutable, err := os.Executable()\tif err != nil {\t\treturn nil, err\t}\tcommand := exec.Command(executable)\tcommand.Stdout = os.Stdout\tcommand.Stdin = os.Stdin\tcommand.Stderr = os.Stderr\tcommand.Env = append(os.Environ(), "is_slave=true")\tcommand.ExtraFiles = append(command.ExtraFiles, fd) // 共享fd\tif err := command.Start(); err != nil {\t\treturn nil, err\t}\tgo func() {\t\tif err := command.Wait(); err != nil {\t\t\terrch <- err\t\t\treturn\t\t}\t\tclose(done)\t}()\treturn command, nil}\n\n开源实现\nhttps://github.com/jpillora/overseer 父进程不负责监听端口(负责创建端口/管理子进程),子进程负责监听端口,当父进程监听到重启的时候会重启 子进程 (区别于我这个例子) 【比较推荐,sdk也比较成熟】\nhttps://github.com/facebookarchive/grace 没怎么细看\nhttps://github.com/fvbock/endless 这个做法更暴力了,相当于当监听到 SIGHUP 信号时,直接启动个孤儿进程(子进程),当前进程因为被close,自己主动退出!不太适用于!\n\n总结\n上面例子的缺陷就是 子进程/父进程 直接调用 close 方法去关闭连接,然后子进程时直接退出进程了,此时会存在部分已经建立连接的请求失败了,需要优雅关闭,但是实际上优雅关闭也会存在问题,就是wait的时间过长,导致后续新建的连接失败(连接超时),所以可以间接过度,也就先 close 关闭新建连接,然后创建子进程继续监听,然后等待前面这个子进程优雅退出即可!\n优雅重启都强依赖于sdk,假如你父进程的sdk有BUG还是得强制升级的!\n\nlinux 小技巧日常中我们也不需要上面那些复杂的东西,比如我就是想后台挂起几个进程是不是,还需要我自己造个轮子,太麻烦了!\n后台运行package main// go build -v -o main main.goimport (\t"fmt"\t"log"\t"net/http"\t"net/http/httputil"\t"os")func main() {\taddr := fmt.Sprintf(":%s", os.Args[1])\tfmt.Printf("ppid: %d, pid: %d, listen: %s\\n", os.Getppid(), os.Getpid(), addr)\tif err := http.ListenAndServe(addr, http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {\t\tdumpRequest, _ := httputil.DumpRequest(request, false)\t\tfmt.Printf(string(dumpRequest))\t\tif _, err := writer.Write([]byte(`hello world`)); err != nil {\t\t\tlog.Printf("ERROR write conn find err: %v", err)\t\t}\t})); err != nil {\t\tlog.Fatal(err)\t}}\n\n\n后台运行 (当终端关闭的时候他也会关闭)\n\n➜ test git:(master) ✗ ./main 10086 &[1] 17195ppid: 12947, pid: 17195, listen: :10086 ➜ test git:(master) ✗ echo $! 17195\n\n\nnohub (not hang up) 当终端关闭它也不会关闭\n\n➜ test git:(master) ✗ nohup ./main 10086 >nohub.log 2>&1 &[1] 18193➜ test git:(master) ✗ echo $! 18193\n\n后台运行多个进程信号:\nHUP 1 终端断线(你把终端关了,就会收到这个)INT 2 中断(同 Ctrl + C)QUIT 3 退出(同 Ctrl + \\)TERM 15 终止KILL 9 强制终止CONT 18 继续(与STOP相反, fg/bg命令)STOP 19 暂停(同 Ctrl + Z)\n\n这里我们运行多个后台进程,当脚本结束的时候杀死后台进程\n#!/usr/bin/env bashset -echild_pids=()function kill_child_process() { echo "kill process ...." for elem in "${child_pids[@]}" ; do echo "kill pid: $elem" kill -9 "$elem" done}# 当脚本退出时执行 kill_child_process (exit类似于defer函数)trap kill_child_process EXIT# 当收到INT/QUIT信号时执行 kill_child_process# trap kill_child_process INT QUIT# 创建子进程1./main 10086 &child_pids+=("$!")# 创建子进程2./main 10010 &child_pids+=("$!")echo "创建子进程: ${child_pids[*]}"# 等待子进程结束wait\n\n总结方案没有绝对的好与坏,取决于具体场景,掌握了各种方案的底层实现,会方便我们针对于各个场景做出支持!\n","categories":["Linux"],"tags":["Linux"]},{"title":"docker网络","url":"/2021/01/26/62dbd7c2e00a1add795e54f9c9d4ee75/","content":" 容器网络通信是一块很大的内容,docker原生的网络模式其实已经很强大了,可以看一下官方文档!本文只是阐述业务中常用的几种模式!\n\n\n\n\n1、bridge模式\n 在该模式中,Docker 守护进程创建了一个虚拟以太网桥 docker0,新建的容器会自动桥接到这个接口,附加在其上的任何网卡之间都能自动转发数据包。\n[root@centos-linux ~]# ip addr show docker03: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default link/ether 02:42:dd:54:2a:d4 brd ff:ff:ff:ff:ff:ff inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0 valid_lft forever preferred_lft forever inet6 fe80::42:ddff:fe54:2ad4/64 scope link valid_lft forever preferred_lft forever\n\n默认情况下,守护进程会创建一一对等虚拟设备接口 veth pair,将其中一个接口设置为容器的 eth0 接口(容器的网卡),另一个接口放置在宿主机的命名空间中,以类似 vethxxx 这样的名字命名,从而将宿主机上的所有容器都连接到这个内部网络上。\n首先启动一个容器,可以看到它的网卡,eth0\n[fanhaodong@centos-linux ~]$ docker run --rm -it busybox /bin/sh/ # ip addr1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1000 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 inet 127.0.0.1/8 scope host lo valid_lft forever preferred_lft forever6: eth0@if7: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0 valid_lft forever preferred_lft forever \n\n然后再看宿主机,由于mac的网络模型和linux发行版有区别,所以使用的centos7的CentOS Linux release 7.9.2009\n[root@centos-linux ~]# ip addr1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 inet 127.0.0.1/8 scope host lo valid_lft forever preferred_lft forever inet6 ::1/128 scope host valid_lft forever preferred_lft forever2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000 link/ether 00:1c:42:b8:a6:b2 brd ff:ff:ff:ff:ff:ff inet 192.168.56.3/24 brd 192.168.56.255 scope global noprefixroute dynamic eth0 valid_lft 1769sec preferred_lft 1769sec inet6 fdb2:2c26:f4e4:0:21c:42ff:feb8:a6b2/64 scope global noprefixroute dynamic valid_lft 2591754sec preferred_lft 604554sec inet6 fe80::21c:42ff:feb8:a6b2/64 scope link noprefixroute valid_lft forever preferred_lft forever3: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default link/ether 02:42:dd:54:2a:d4 brd ff:ff:ff:ff:ff:ff inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0 valid_lft forever preferred_lft forever inet6 fe80::42:ddff:fe54:2ad4/64 scope link valid_lft forever preferred_lft forever7: veth132f35c@if6: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker0 state UP group default link/ether 0e:5e:61:09:28:7c brd ff:ff:ff:ff:ff:ff link-netnsid 0 inet6 fe80::c5e:61ff:fe09:287c/64 scope link valid_lft forever preferred_lft forever\n\n\n可以看到本地网络多了一个网卡7: veth132f35c@if6,\n通过以上的比较可以发现,证实了之前所说的:守护进程会创建一对对等虚拟设备接口 veth pair,将其中一个接口设置为容器的 eth0 接口(容器的网卡),另一个接口放置在宿主机的命名空间中,以类似 vethxxx 这样的名字命名。\n同时,守护进程还会从网桥 docker0 的私有地址空间中分配一个 IP 地址和子网给该容器,并设置 docker0 的 IP 地址为容器的默认网关。也可以安装 yum install -y bridge-utils 以后,通过 brctl show 命令查看网桥信息。\n\n[fanhaodong@centos-linux ~]$ brctl showbridge name\tbridge id\t\tSTP enabled\tinterfacesdocker0\t\t8000.0242dd542ad4\tno\t\tveth132f35c\n\n可以看到 interfaces有一个叫做 veth132f35c\n可以通过 docker network inspect bridge 查看模式\n[root@centos-linux ~]# docker network inspect bridge[ { "Name": "bridge", "Id": "50e703932ca8d619016f1e08df21269ee09b3f9799c81f47e5df6e477ee3c341", "Created": "2021-02-03T11:22:58.527865244+08:00", "Scope": "local", "Driver": "bridge", "EnableIPv6": false, "IPAM": { "Driver": "default", "Options": null, "Config": [ { "Subnet": "172.17.0.0/16", "Gateway": "172.17.0.1" } ] }, "Internal": false, "Attachable": false, "Ingress": false, "ConfigFrom": { "Network": "" }, "ConfigOnly": false, "Containers": { "66949d48a6c0d1629240ab01c2601fd1c885e66b8e75b254b2e5537e7414914e": { "Name": "modest_tesla", "EndpointID": "1012aaac090fa8dac5fababa3467338a255f867b581a97539abb67477da3036a", "MacAddress": "02:42:ac:11:00:02", "IPv4Address": "172.17.0.2/16", "IPv6Address": "" } }, "Options": { "com.docker.network.bridge.default_bridge": "true", "com.docker.network.bridge.enable_icc": "true", "com.docker.network.bridge.enable_ip_masquerade": "true", "com.docker.network.bridge.host_binding_ipv4": "0.0.0.0", "com.docker.network.bridge.name": "docker0", "com.docker.network.driver.mtu": "1500" }, "Labels": {} }]\n\n可以看到 容器名称一样,\n[root@centos-linux ~]# docker psCONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES66949d48a6c0 busybox "/bin/sh" 2 minutes ago Up 2 minutes modest_tesla\n\n查看 network, docker inspect 66949d48a6c0 -f '{{json .NetworkSettings.Networks.bridge}}'\n[root@centos-linux ~]# docker inspect 66949d48a6c0 -f '{{json .NetworkSettings.Networks.bridge}}'{ "IPAMConfig": null, "Links": null, "Aliases": null, "NetworkID": "50e703932ca8d619016f1e08df21269ee09b3f9799c81f47e5df6e477ee3c341", "EndpointID": "1012aaac090fa8dac5fababa3467338a255f867b581a97539abb67477da3036a", "Gateway": "172.17.0.1", "IPAddress": "172.17.0.2", "IPPrefixLen": 16, "IPv6Gateway": "", "GlobalIPv6Address": "", "GlobalIPv6PrefixLen": 0, "MacAddress": "02:42:ac:11:00:02", "DriverOpts": null}\n\n注意:\n网桥模式下,各个容器是可以互相ping通的,可以通过主机IP相互PING通(默认的不支持主机名相互ping通)\n2、host 网络模式\n 这个比较适合用于本地软件安装,单软件,类似于启动一个程序,但是需要环境依赖,可以使用这个\n\n\nhost 网络模式需要在创建容器时通过参数 --net host 或者 --network host 指定;\n采用 host 网络模式的 Docker Container,可以直接使用宿主机的 IP 地址与外界进行通信,若宿主机的 eth0 是一个公有 IP,那么容器也拥有这个公有 IP。同时容器内服务的端口也可以使用宿主机的端口,无需额外进行 NAT 转换;\nhost 网络模式可以让容器共享宿主机网络栈,这样的好处是外部主机与容器直接通信,但是容器的网络缺少隔离性。\nhost 模式不能访问外部网络,比如ping www.baidu.com是行不通的\n\n[root@centos-linux .ssh]# docker run --rm -it --network host busybox ip addr1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1000 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 inet 127.0.0.1/8 scope host lo valid_lft forever preferred_lft forever inet6 ::1/128 scope host valid_lft forever preferred_lft forever2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast qlen 1000 link/ether 00:1c:42:b8:a6:b2 brd ff:ff:ff:ff:ff:ff inet 192.168.56.3/24 brd 192.168.56.255 scope global dynamic eth0 valid_lft 1128sec preferred_lft 1128sec inet6 fdb2:2c26:f4e4:0:21c:42ff:feb8:a6b2/64 scope global dynamic valid_lft 2591996sec preferred_lft 604796sec inet6 fe80::21c:42ff:feb8:a6b2/64 scope link valid_lft forever preferred_lft forever3: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue link/ether 02:42:dd:54:2a:d4 brd ff:ff:ff:ff:ff:ff inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0 valid_lft forever preferred_lft forever inet6 fe80::42:ddff:fe54:2ad4/64 scope link valid_lft forever preferred_lft forever\n\n3、none 网络模式\nnone 网络模式是指禁用网络功能,只有 lo 接口 local 的简写,代表 127.0.0.1,即 localhost 本地环回接口。在创建容器时通过参数 --net none 或者 --network none 指定;\nnone 网络模式即不为 Docker Container 创建任何的网络环境,容器内部就只能使用 loopback 网络设备,不会再有其他的网络资源。可以说 none 模式为 Docke Container 做了极少的网络设定,但是俗话说得好“少即是多”,在没有网络配置的情况下,作为 Docker 开发者,才能在这基础做其他无限多可能的网络定制开发。这也恰巧体现了 Docker 设计理念的开放。\n\n[root@centos-linux .ssh]# docker run --rm -it --network none busybox ip addr1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1000 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 inet 127.0.0.1/8 scope host lo valid_lft forever preferred_lft forever\n\n4、container 网络模式\nContainer 网络模式是 Docker 中一种较为特别的网络的模式。在创建容器时通过参数 --net container:已运行的容器名称/ID 或者 --network container:已运行的容器名称/ID 指定;\n处于这个模式下的 Docker 容器会共享一个网络栈,这样两个容器之间可以使用 localhost 高效快速通信。\n\n[root@centos-linux .ssh]# docker run --rm -it --name b1 busybox /bin/sh/ # ip addr1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1000 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 inet 127.0.0.1/8 scope host lo valid_lft forever preferred_lft forever28: eth0@if29: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0 valid_lft forever preferred_lft forever\n\n主机2\n[root@centos-linux ~]# docker run --rm -it --name b2 --network container:b1 busybox ip addr1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1000 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 inet 127.0.0.1/8 scope host lo valid_lft forever preferred_lft forever28: eth0@if29: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0 valid_lft forever preferred_lft forever\n\n假如此时 主机b1死机了,其实它的网卡也只剩下lo网卡\nb2 主机当 b1 挂掉后会如图所示,当 b1重启,b2也需要重启才能看到网卡信息\n/ # ip addr1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1000 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 inet 127.0.0.1/8 scope host lo valid_lft forever preferred_lft forever/ #\n\n5、自定义网络1、自定义创建一个网络,默认是bridge模式!\n[root@centos-linux /]# docker network create custom_network94a35aa31bdee048a0e5d8b178e4246b429d4525a4036d3d9287650933357c6e\n\n2、查看网络\n[root@centos-linux /]# docker network inspect custom_network[ { "Name": "custom_network", "Id": "94a35aa31bdee048a0e5d8b178e4246b429d4525a4036d3d9287650933357c6e", "Created": "2021-03-26T14:35:27.907496817+08:00", "Scope": "local", "Driver": "bridge", "EnableIPv6": false, "IPAM": { "Driver": "default", "Options": {}, "Config": [ { "Subnet": "172.18.0.0/16", "Gateway": "172.18.0.1" } ] }, "Internal": false, "Attachable": false, "Ingress": false, "ConfigFrom": { "Network": "" }, "ConfigOnly": false, "Containers": {}, "Options": {}, "Labels": {} }]\n\n3、使用 custom网络\n[root@centos-linux /]# docker run --rm -d --name demo-1 --network custom_network alpine top93e04b37b6a7418131a1ff2ef21d45c807757746841a601a1438ffa5c6fa1061[root@centos-linux /]# docker run --rm -d --name demo-2 --network custom_network alpine topbe07cf3872c42a46dc564fa8f20dee696301e8cc24dc653ffa71cb52bd4c0de3\n\n4、是否可以相互ping通\n\n容器间可以相互Ping通\n\n[root@centos-linux /]# docker exec -it demo-1 ping -c 2 demo-2PING demo-2 (172.18.0.3): 56 data bytes64 bytes from 172.18.0.3: seq=0 ttl=64 time=0.034 ms64 bytes from 172.18.0.3: seq=1 ttl=64 time=0.143 ms--- demo-2 ping statistics ---2 packets transmitted, 2 packets received, 0% packet lossround-trip min/avg/max = 0.034/0.088/0.143 ms[root@centos-linux /]# docker exec -it demo-2 ping -c 2 demo-1PING demo-1 (172.18.0.2): 56 data bytes64 bytes from 172.18.0.2: seq=0 ttl=64 time=0.041 ms64 bytes from 172.18.0.2: seq=1 ttl=64 time=0.091 ms--- demo-1 ping statistics ---2 packets transmitted, 2 packets received, 0% packet lossround-trip min/avg/max = 0.041/0.066/0.091 ms\n\n\nping 宿主机 pass\n\n[root@centos-linux /]# docker exec -it demo-2 ping -c 2 192.168.56.3PING 192.168.56.3 (192.168.56.3): 56 data bytes64 bytes from 192.168.56.3: seq=0 ttl=64 time=0.082 ms64 bytes from 192.168.56.3: seq=1 ttl=64 time=0.121 ms--- 192.168.56.3 ping statistics ---2 packets transmitted, 2 packets received, 0% packet lossround-trip min/avg/max = 0.082/0.101/0.121 ms\n\n6、跨宿主机访问 官方文档: 某些应用程序,尤其是旧版应用程序或监视网络流量的应用程序,期望直接连接到物理网络。在这种情况下,可以使用macvlan网络驱动程序为每个容器的虚拟网络接口分配MAC地址,使其看起来像是直接连接到物理网络的物理网络接口。\n 对于SDN来说,一般都是基于overlay模式,尤其是在使用docker中会出现跨主机容器无法通信的问题,docker提供ipvlan技术完全可以解决,但是需要linux内核版本> 4.2,唯一的局限性就是需要自己实现宿主机与容器间通信的问题!\n 像我们公司就是基于ipvlan实现的跨宿主机通信的问题\n1、macvlan 模式介绍 macvlan 是在docker1.2版本之后推出的,主要是解决:是旧版应用程序或监视网络流量的应用程序,期望直接连接到物理网络(意思就是直接连接到物理网络,可以相互ping通)\n 在这种情况下,可以使用macvlan网络驱动程序为每个容器的虚拟网络接口分配MAC地址,使其看起来像是直接连接到物理网络的物理网络接口。在这种情况下,您需要在Docker主机上指定用于的物理接口macvlan,以及的子网和网关macvlan。您甚至可以macvlan使用不同的物理网络接口隔离网络。请记住以下几点:\n\n由于IP地址耗尽或“ VLAN传播”,很容易无意间损坏您的网络,在这种情况下,您的网络中有大量不正确的唯一MAC地址。\n您的网络设备需要能够处理“混杂模式”,在该模式下,可以为一个物理接口分配多个MAC地址。\n如果您的应用程序可以使用网桥(在单个Docker主机上)或覆盖(跨多个Docker主机进行通信)工作,那么从长远来看,这些解决方案可能会更好。\n\n使用1、我的主机ip\n[root@centos-linux ~]# ip addr show eth02: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000 link/ether 00:1c:42:b8:a6:b2 brd ff:ff:ff:ff:ff:ff inet 192.168.56.3/24 brd 192.168.56.255 scope global noprefixroute dynamic eth0 valid_lft 1098sec preferred_lft 1098sec inet6 fdb2:2c26:f4e4:0:21c:42ff:feb8:a6b2/64 scope global noprefixroute dynamic valid_lft 2591546sec preferred_lft 604346sec inet6 fe80::21c:42ff:feb8:a6b2/64 scope link noprefixroute valid_lft forever preferred_lft forever\n\n2、根据主机ip去创建 macvlan\n[root@centos-linux ~]# docker network create -d macvlan \\--subnet=192.168.56.0/24 \\--gateway=192.168.56.1 \\-o parent=eth0 macvlan_net3adcc89a20a0a40b55b026ffcb9164990ca8c347d23445f367ed1a88f83ddd57\n\n3、查看docker网络信息\n所有网络\n[root@centos-linux ~]# docker network listNETWORK ID NAME DRIVER SCOPE3505ceea1e1b bridge bridge local892b017dd40d host host local3adcc89a20a0 macvlan_net macvlan local69e3ce2179b5 none null local\n\n信息\n[root@centos-linux ~]# docker network inspect macvlan_net[ { "Name": "macvlan_net", "Id": "71e776d96002b0d6977a65b6e370b3d8b71283304239e16144cdf1298f7500cc", "Created": "2021-03-05T19:59:54.375164139+08:00", "Scope": "local", "Driver": "macvlan", "EnableIPv6": false, "IPAM": { "Driver": "default", "Options": {}, "Config": [ { "Subnet": "192.168.56.0/24", "Gateway": "192.168.56.1" } ] }, "Internal": false, "Attachable": false, "Ingress": false, "ConfigFrom": { "Network": "" }, "ConfigOnly": false, "Containers": { "2a2285139cd7fb92a93b437ad6bd00b9b2d80f1918b448f22cadc03a32cc5d12": { "Name": "gallant_nightingale", "EndpointID": "7399fc4d01163d87b0ef078772c0abd3e1dd857eb6bcc692eb06d5cc9767008b", "MacAddress": "02:42:c0:a8:38:20", "IPv4Address": "192.168.56.32/24", "IPv6Address": "" }, "61e4e496c22ab5ca0f4bd7a44fd3c9b996f6ff81a33c6853ac94270608696974": { "Name": "nervous_mendeleev", "EndpointID": "e238ca83dc06eff26cc6f862d431ffac8750815870bfb2eb7fc5a498c9a81cec", "MacAddress": "02:42:c0:a8:38:1f", "IPv4Address": "192.168.56.31/24", "IPv6Address": "" } }, "Options": { "parent": "eth0" }, "Labels": {} }]\n\n4、创建容器\n1)直接启动\n可以看到IP是 192.168.56.2 ,属于 192.168.56.3/24 子网下面,由于是第一个,所以第一个容器是顺序创建的,ip就是2 (存在的问题就是可能和宿主机ip相同,这就尴尬了)\n容器1\n[root@centos-linux ~]# docker run --rm -it --net=macvlan_net --ip 192.168.56.31 alpine /bin/sh/ # ip addr1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN qlen 1000 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 inet 127.0.0.1/8 scope host lo valid_lft forever preferred_lft forever8: eth0@if2: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue state UP link/ether 02:42:c0:a8:38:1f brd ff:ff:ff:ff:ff:ff inet 192.168.56.31/24 brd 192.168.56.255 scope global eth0 valid_lft forever preferred_lft forever\n\n容器2\n[root@centos-linux ~]# docker run --rm -it --net=macvlan_net --ip 192.168.56.32 alpine /bin/sh/ # ip addr1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN qlen 1000 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 inet 127.0.0.1/8 scope host lo valid_lft forever preferred_lft forever9: eth0@if2: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue state UP link/ether 02:42:c0:a8:38:20 brd ff:ff:ff:ff:ff:ff inet 192.168.56.32/24 brd 192.168.56.255 scope global eth0 valid_lft forever preferred_lft forever\n\n2)ping实验\n1、同一宿主机无法PING通容器:\n[root@centos-linux ~]# ping 192.168.56.31 -c 2PING 192.168.56.31 (192.168.56.31) 56(84) bytes of data.From 192.168.56.3 icmp_seq=1 Destination Host UnreachableFrom 192.168.56.3 icmp_seq=2 Destination Host Unreachable--- 192.168.56.31 ping statistics ---2 packets transmitted, 0 received, +2 errors, 100% packet loss, time 1002mspipe 2[root@centos-linux ~]# ping 192.168.56.32 -c 2PING 192.168.56.32 (192.168.56.32) 56(84) bytes of data.^[From 192.168.56.3 icmp_seq=1 Destination Host UnreachableFrom 192.168.56.3 icmp_seq=2 Destination Host Unreachable--- 192.168.56.32 ping statistics ---2 packets transmitted, 0 received, +2 errors, 100% packet loss, time 1007mspipe 2\n\n关于: Destination Host Unreachable ,可以看一下这篇文章:https://www.eefocus.com/communication/426853 ,问题就是 局域网中无法找到对应 IP 的 MAC 地址,无法完成封装,所以可以看到docker的实现其实就是做了一层mac地址的转换\n[root@centos-linux ~]# ip route get 192.168.56.32192.168.56.32 dev eth0 src 192.168.56.3 uid 0 cache# 主要走的是通过 eth0 \n\n2、同一个宿主机那容器无法ping通宿主机:\n/ # ping -c 2 192.168.56.3PING 192.168.56.3 (192.168.56.3): 56 data bytes^C--- 192.168.56.3 ping statistics ---2 packets transmitted, 0 packets received, 100% packet loss/ # ping -c 2 192.168.56.3PING 192.168.56.3 (192.168.56.3): 56 data bytes--- 192.168.56.3 ping statistics ---2 packets transmitted, 0 packets received, 100% packet loss\n\n原因是什么:\n3、同一个宿主机那容器间通信\n可以看到同一个宿主机内的容器可以相互通信\n/ # ping 192.168.56.32 -c 2PING 192.168.56.32 (192.168.56.32): 56 data bytes64 bytes from 192.168.56.32: seq=0 ttl=64 time=0.054 ms64 bytes from 192.168.56.32: seq=1 ttl=64 time=0.116 ms--- 192.168.56.32 ping statistics ---2 packets transmitted, 2 packets received, 0% packet lossround-trip min/avg/max = 0.054/0.085/0.116 ms/ # ping 192.168.56.31 -c 2PING 192.168.56.31 (192.168.56.31): 56 data bytes64 bytes from 192.168.56.31: seq=0 ttl=64 time=0.055 ms64 bytes from 192.168.56.31: seq=1 ttl=64 time=0.114 ms--- 192.168.56.31 ping statistics ---2 packets transmitted, 2 packets received, 0% packet lossround-trip min/avg/max = 0.055/0.084/0.114 ms\n\n4、创建宿主机2\n[root@centos-4-5 ~]# ip addr show eth02: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000 link/ether 00:1c:42:79:65:9d brd ff:ff:ff:ff:ff:ff inet 192.168.56.7/24 brd 192.168.56.255 scope global noprefixroute dynamic eth0 valid_lft 1475sec preferred_lft 1475sec inet6 fdb2:2c26:f4e4:0:21c:42ff:fe79:659d/64 scope global noprefixroute dynamic valid_lft 2591565sec preferred_lft 604365sec inet6 fe80::21c:42ff:fe79:659d/64 scope link noprefixroute valid_lft forever preferred_lft forever\n\n5、测试主机1与主机2通信 ,完全OK\n[root@centos-4-5 ~]# ping 192.168.56.3 -c 2PING 192.168.56.3 (192.168.56.3) 56(84) bytes of data.64 bytes from 192.168.56.3: icmp_seq=1 ttl=64 time=0.585 ms64 bytes from 192.168.56.3: icmp_seq=2 ttl=64 time=0.521 ms--- 192.168.56.3 ping statistics ---2 packets transmitted, 2 received, 0% packet loss, time 1043msrtt min/avg/max/mdev = 0.521/0.553/0.585/0.032 ms\n\n6、测试主机2与主机1的容器通信,不可用\n[root@centos-4-5 ~]# ping 192.168.56.31 -c 2PING 192.168.56.31 (192.168.56.31) 56(84) bytes of data.--- 192.168.56.31 ping statistics ---2 packets transmitted, 0 received, 100% packet loss, time 1051ms\n\n总结macvlan 模式的局限性较大,只能宿主机内容器之间相互可以访问,但是不能实现跨主机容器的通信\n2、IpVlan 模式ipvlan就比较强大了,可以支持跨宿主机通信,不需要任何额外的配置!\n使用[root@centos-linux ~]# docker network create -d ipvlan \\--subnet=192.168.56.0/24 \\--gateway=192.168.56.1 \\-o parent=eth0 ipvlan_net\n\n详情\n[root@centos-linux ~]# docker network inspect ipvlan_net[ { "Name": "ipvlan_net", "Id": "22a8b5faf9ddc2b8a7a81d67467649b331c8ccaf2a7bc7aaf25dd7344d1fed26", "Created": "2021-03-06T16:03:26.113213178+08:00", "Scope": "local", "Driver": "ipvlan", "EnableIPv6": false, "IPAM": { "Driver": "default", "Options": {}, "Config": [ { "Subnet": "192.168.56.0/24", "Gateway": "192.168.56.1" } ] }, "Internal": false, "Attachable": false, "Ingress": false, "ConfigFrom": { "Network": "" }, "ConfigOnly": false, "Containers": { "80c8bb73c329f1c0c370514c72cd25979a1b5d8aa112b49c24385d558d2d4e53": { "Name": "recursing_leakey", "EndpointID": "8055b00e4981d35b08f39565bde348142785565b3e43055fb719d9f7cef7fae5", "MacAddress": "", "IPv4Address": "192.168.56.32/24", "IPv6Address": "" }, "e1f96d16c0e5f9b04cc49b63cc991c89834ec6f0f599a64853bb6dddd26b3f51": { "Name": "confident_hodgkin", "EndpointID": "d1347ff2d6f1d8c2070d1dccad5aa29f3e3c34bc982ab026a352f071f6626dd7", "MacAddress": "", "IPv4Address": "192.168.56.31/24", "IPv6Address": "" } }, "Options": { "parent": "eth0" }, "Labels": {} }]\n\n1、主机1-> 容器1-2(不通) / 容器1-2 -> 主机1 (不通)\n[root@centos-linux ~]# ping 192.168.56.32 -c 2PING 192.168.56.32 (192.168.56.32) 56(84) bytes of data.From 192.168.56.3 icmp_seq=1 Destination Host UnreachableFrom 192.168.56.3 icmp_seq=2 Destination Host Unreachable--- 192.168.56.32 ping statistics ---2 packets transmitted, 0 received, +2 errors, 100% packet loss, time 1072mspipe 2/ # ping 192.168.56.3 -c 2PING 192.168.56.3 (192.168.56.3): 56 data bytes--- 192.168.56.3 ping statistics ---2 packets transmitted, 0 packets received, 100% packet loss\n\n2、容器1-2->容器1-1 (通)\n/ # ping 192.168.56.31 -c 2PING 192.168.56.31 (192.168.56.31): 56 data bytes64 bytes from 192.168.56.31: seq=0 ttl=64 time=0.099 ms64 bytes from 192.168.56.31: seq=1 ttl=64 time=0.188 ms--- 192.168.56.31 ping statistics ---2 packets transmitted, 2 packets received, 0% packet lossround-trip min/avg/max = 0.099/0.143/0.188 ms\n\n3、主机2->主机1 (通)\n[root@centos-4-5 ~]# ping 192.168.56.3 -c 2PING 192.168.56.3 (192.168.56.3) 56(84) bytes of data.64 bytes from 192.168.56.3: icmp_seq=1 ttl=64 time=0.478 ms64 bytes from 192.168.56.3: icmp_seq=2 ttl=64 time=0.551 ms--- 192.168.56.3 ping statistics ---2 packets transmitted, 2 received, 0% packet loss, time 1064msrtt min/avg/max/mdev = 0.478/0.514/0.551/0.042 ms\n\n4、主机2容器2-> 主机1 (通)\n/ # ping 192.168.56.3 -c 2PING 192.168.56.3 (192.168.56.3): 56 data bytes64 bytes from 192.168.56.3: seq=0 ttl=64 time=0.469 ms64 bytes from 192.168.56.3: seq=1 ttl=64 time=0.605 ms--- 192.168.56.3 ping statistics ---2 packets transmitted, 2 packets received, 0% packet lossround-trip min/avg/max = 0.469/0.537/0.605 ms\n\n5、主机2容器2-2->主机1容器1-2 (通)\n/ # ping 192.168.56.32 -c 2PING 192.168.56.32 (192.168.56.32): 56 data bytes64 bytes from 192.168.56.32: seq=0 ttl=64 time=0.569 ms64 bytes from 192.168.56.32: seq=1 ttl=64 time=0.687 ms--- 192.168.56.32 ping statistics ---2 packets transmitted, 2 packets received, 0% packet lossround-trip min/avg/max = 0.569/0.628/0.687 ms\n\n6、主机2 -> 主机1容器1-2 (通)\n[root@centos-4-5 ~]# ping 192.168.56.32 -c 2PING 192.168.56.32 (192.168.56.32) 56(84) bytes of data.64 bytes from 192.168.56.32: icmp_seq=1 ttl=64 time=0.321 ms64 bytes from 192.168.56.32: icmp_seq=2 ttl=64 time=0.609 ms--- 192.168.56.32 ping statistics ---2 packets transmitted, 2 received, 0% packet loss, time 1022msrtt min/avg/max/mdev = 0.321/0.465/0.609/0.144 ms\n\n总结\n支持跨主机间容器通信(依赖于宿主机之间可以相互通信,其实只要在一个子网下即可)\n不支持单机内的宿主机容器间的通信\n\n3、参考文章linux 网络虚拟化: macvlan\nDocker 跨主机容器间网络通信(一)\nDocker系列(十三):Docker 跨主机容器间网络通信(二)\nDocker系列(十四):Docker Swarm集群\n 容器网络:盘点,解释与分析\nMacvlan与ipvlan\n","categories":["云原生"],"tags":["Docker","网络"]},{"title":"使用PEG实现一个高性能的过滤器","url":"/2024/03/02/7ade909060c47e5d0a12977ec2bd7a1b/","content":"说实话大家如果开发过一些研发工具的话或多或少会遇到定制过滤器(Filter)的能力,主要场景比如动态的匹配一些流量,动态匹配一些规则等,为此通常平台方会简单提供一些过滤能力比如仅支持and 操作,不支持复杂的逻辑判断,或者引入lua 、 python、JavaScript 这种动态语言来解决此问题,但是这些性能都很差,为此可以自己实现一个过滤表达式,性能也可以做到最优解!\n\n\n介绍这里我们要实现一个逻辑表达式,大概大家懂了我的想法了吧,我这里实现了一个抓取流量的工具,但是需要实现一个过滤的表达式去实现请求过滤!\nhost='www.douyin.com' and http_code in (400, 500)\n\n或者\nhost='www.douyin.com' and (http_code = 400 or http_code = 500)\n\n这里我实现的比较复杂,支持类似于\na='1' and b='2' or (c=3 and (d='4' or f='5') and (x='6' or y='7' and z in (1,2)))\n\n定义数据结构(AST)其实实现parser的第一步就是需要定义数据结构,需要想到怎么描述这个AST,简单的规则说实话直接搞个数组就行了,但是对于()分组规则呢?对于and 和 or 混合使用呢? 所以它是一个单链表!\ntype Conditions struct {\tGroup *Conditions `json:"group,omitempty"` // 当前节点可能是一个group\tValue *Condition `json:"value,omitempty"`\tLogical string `json:"logical,omitempty"` // 逻辑符号 and / or\tNext *Conditions `json:"next,omitempty"` // 下一个节点}type Condition struct {\tKey string `json:"key,omitempty"`\tOperator string `json:"operator,omitempty"` // 运算符号 > / < / = / in / !=\tValue string `json:"value,omitempty"`\tValues []string `json:"values,omitempty"` // operator=in 使用此字段}\n\n定义 PEG这里我使用的PEG库是 https://github.com/mna/pigeon 这个\n{package parsertype Conditions struct {\tGroup *Conditions `json:"group,omitempty"`\tValue *Condition `json:"value,omitempty"`\tLogical string `json:"logical,omitempty"` // 逻辑符号 and / or\tNext *Conditions `json:"next,omitempty"`}type Condition struct {\tKey string `json:"key,omitempty"`\tOperator string `json:"operator,omitempty"` // 运算符号 > / < / = / in / !=\tValue string `json:"value,omitempty"`\tValues []string `json:"values,omitempty"`}}Grammar <- values:Conditions+ EOF {\treturn values.([]interface{})[0].(*Conditions), nil} / SyntaxErrorSyntaxError <- . {\treturn nil, errors.New("parser: syntax error")}ConditionGroup <- '(' __ values: Conditions* __ ')' { if len(values.([]interface{})) == 0 { return nil, nil } return values.([]interface{})[0].(*Conditions), nil}Conditions <- values:( ( Condition / ConditionGroup ) __ LogicalOperator? __ )+ {\thead := &Conditions{}\tcur := head\tlastIndex := len(values.([]interface{})) - 1\tfor index, value := range values.([]interface{}) {\t\targs := value.([]interface{})\t\tswitch arg0 := args[0].(type) {\t\tcase *Condition:\t\t\tcur.Value = arg0\t\tcase *Conditions:\t\t\tcur.Group = arg0\t\t}\t\tcur.Logical, _ = args[2].(string)\t\tif index == lastIndex {\t\t\tbreak\t\t}\t\tcur.Next = &Conditions{}\t\tcur = cur.Next\t}\treturn head, nil}LogicalOperator <- ( "and" / "or" ) { return string(c.text), nil}Condition <- key:Identifier __ op:Operator __ value:( Double / Integer / String / List) {\tret := &Condition{\t\tKey: key.(string),\t\tOperator: op.(string),\t}\tif vs, isOK := value.([]string); isOK {\t\tret.Values = vs\t}\tif v, isOK := value.(string); isOK {\t\tret.Value = v\t}\treturn ret, nil}Integer <- '-'? Digit+ {\tif _, err := strconv.ParseInt(string(c.text), 10, 64); err != nil {\t\treturn nil, err\t}\treturn string(c.text), nil}Double ← [+-]? Digit+ '.' Digit+ {\tif _, err := strconv.ParseFloat(string(c.text), 64); err != nil {\t\treturn nil, err\t}\treturn string(c.text), nil}String <- LiteralList <- '(' __ values:(Literal __ ListSeparator? __)* __ ')' {\tresult := make([]string, 0)\tfor _, value := range values.([]interface{}) {\t\targs, _ := value.([]interface{})\t\tresult = append(result, args[0].(string))\t}\treturn result, nil}Literal <- (('"' (`\\"` / [^"])* '"') / ('\\'' (`\\'` / [^'])* '\\'')) {\tif len(c.text) == 0 {\t\treturn "", nil\t} switch c.text[0] { case '\\'': return strings.Replace(string(c.text[1:len(c.text)-1]), `\\'`, `'`, -1), nil case '"': return strings.Replace(string(c.text[1:len(c.text)-1]), `\\"`, `"`, -1), nil } return string(c.text) ,nil}Operator <- ( "in" / ">=" / "<=" / "!=" / "=" / ">" / "<" ) { return string(c.text), nil}Identifier <- (Letter / '_')+ (Letter / Digit / '.' / '_' / '[' / ']' )* {\treturn string(c.text), nil}ListSeparator <- [,]Letter <- [A-Za-z]Digit <- [0-9]__ <- (_)*_ <- [ \\n\\t\\r]+EOF <- !.\n\n编译pigeon -o condition.peg.go ./condition.peg\n\n注意很多时候匹配失败可能是规则顺序的问题!例如 Double / Integer 如果改成 Integer / Double 就会报错对于 a=1.1 这种case!因为啥呢? \nInteger <- '-'? Digit+ {\tif _, err := strconv.ParseInt(string(c.text), 10, 64); err != nil {\t\treturn nil, err\t}\treturn string(c.text), nil}Double ← [+-]? Digit+ '.' Digit+ {\tif _, err := strconv.ParseFloat(string(c.text), 64); err != nil {\t\treturn nil, err\t}\treturn string(c.text), nil}\n\n我们发现这俩规则基本上一样,如果是Integer/Double 那么匹配是惰性匹配,它不会贪心的去搜索,直接找到1 就去处理了,然后成功,但是剩余了.1 导致解析失败了, 他不会走 Double 这个分支解析,这也就是为啥会失败了,我们应该注意优先顺序!\n获取ASTpackage parserimport (\t"encoding/json"\t"testing")func TestParse(t *testing.T) {\tinput := `a='1' and b='2' or (c=3 and (d='4' or f='5') and (x='6' or y='7' and z in (1,2)))`\tparse, err := Parse("", []byte(input))\tif err != nil {\t\tt.Fatal(err)\t}\tmarshal, _ := json.MarshalIndent(parse, "", "\\t")\tt.Log(string(marshal))}\n\n输出\n{ "value": { "key": "a", "operator": "=", "value": "1" }, "logical": "and", "next": { "value": { "key": "b", "operator": "=", "value": "2" }, "logical": "or", "next": { "group": { "value": { "key": "c", "operator": "=", "value": "3" }, "logical": "and", "next": { "group": { "value": { "key": "d", "operator": "=", "value": "4" }, "logical": "or", "next": { "value": { "key": "f", "operator": "=", "value": "5" } } }, "logical": "and", "next": { "group": { "value": { "key": "x", "operator": "=", "value": "6" }, "logical": "or", "next": { "value": { "key": "y", "operator": "=", "value": "7" }, "logical": "and", "next": { "value": { "key": "z", "operator": "in", "values": [ "1", "2" ] } } } } } } } } }}\n\n封装过滤器封装逻辑type MapInput map[string]stringfunc (m MapInput) Get(key string) string {\treturn m[key]}// 抽象一个Input接口type Input interface {\tGet(key string) string}// 逻辑判断func (c *Conditions) Result(intput Input) bool {\tvar result bool\tif c.Value != nil {\t\tresult = c.Value.Result(intput)\t} else if c.Group != nil {\t\tresult = c.Group.Result(intput)\t} else { result = true // 什么也没定义,类似于 () 这种\t}\tif c.Next == nil {\t\treturn result\t}\tif c.Logical == "and" {\t\treturn result && c.Next.Result(intput)\t}\treturn result || c.Next.Result(intput)}// 逻辑判断func (c *Condition) Result(input Input) bool {\tvalue := input.Get(c.Key)\tswitch c.Operator {\tcase "in":\t\treturn utils.Contains(c.Values, value)\tcase "=":\t\treturn value == c.Value\tcase "!=":\t\treturn value != c.Value\tcase ">":\t\treturn value > c.Value\tcase "<":\t\treturn value < c.Value\tcase ">=":\t\treturn value >= c.Value\tcase "<=":\t\treturn value <= c.Value\tdefault:\t\tpanic(fmt.Sprintf(`invalid operator (%s)`, c.Operator))\t}}\n\n测试func TestRule(t *testing.T) {\tintput := MapInput{\t\t"a": "1",\t\t"b": "2",\t\t"c": "3",\t\t"d": "4",\t\t"e": "5.5",\t\t"f": "xiaoming",\t}\trule, err := ParseRule(`a = 1 and b = 2 and ( c = 4 or ( d = 4 and e in ( 5.5 , 6.6 ) ) ) and f = "xiaoming"`)\tif err != nil {\t\tt.Fatal(err)\t}\tresult := rule.Result(intput)\tt.Log(result)}func BenchmarkRule(b *testing.B) {\tb.StopTimer()\tintput := MapInput{\t\t"a": "1",\t\t"b": "2",\t\t"c": "3",\t\t"d": "4",\t\t"e": "5.5",\t\t"f": "xiaoming",\t}\tparseRule, err := ParseRule(`a = 1 and b = 2 and ( c = 4 or ( d = 4 and e in ( 5.5 , 6.6 ) ) ) and f = "xiaoming"`)\tif err != nil {\t\tb.Fatal(err)\t}\tb.StartTimer()\tfor i := 0; i < b.N; i++ {\t\tif !parseRule.Result(intput) {\t\t\tb.Fatal("must ture")\t\t}\t}}\n\nbenchmark~/go/src/github.com/anthony-dong/golang go test -v -run=none -bench=BenchmarkRule -count=5 -benchmem ./pkg/filter/...goos: linuxgoarch: amd64pkg: github.com/anthony-dong/golang/pkg/filtercpu: Intel(R) Xeon(R) Platinum 8260 CPU @ 2.40GHzBenchmarkRuleBenchmarkRule-8 \t 5362532\t 224.3 ns/op\t 0 B/op\t 0 allocs/opBenchmarkRule-8 \t 5361560\t 219.5 ns/op\t 0 B/op\t 0 allocs/opBenchmarkRule-8 \t 5391529\t 224.4 ns/op\t 0 B/op\t 0 allocs/opBenchmarkRule-8 \t 5425616\t 221.1 ns/op\t 0 B/op\t 0 allocs/opBenchmarkRule-8 \t 5365962\t 222.1 ns/op\t 0 B/op\t 0 allocs/opPASSok \tgithub.com/anthony-dong/golang/pkg/filter\t7.138s\n\n可以看到性能是非常的高,也基本没啥开销!\n总结说实话我实现的这个过滤表达式其实并不难,但是难的是实际上是PEG 、BNF 、ANTLR4、Lex/YACC 这种词法生成器!有兴趣的同学可以尝试实现一个词法生成器,可以深入理解编译原理!\n相关代码实现可以看这里:https://github.com/Anthony-Dong/golang/tree/master/pkg/filter\n","categories":["编译原理"],"tags":["编译原理"]},{"title":"C++ 继承的底层设计与原理","url":"/2023/09/16/91c61c1043b15978e6cca833236c4694/","content":"C++ 作为面向对象语言,其次面向对象语言的重要特性封装、继承、多态,所以理解继承底层设计对于我们学习C++是非常重要的,其次他是C++的灵魂所在,本人也是走了些弯路所以打算深度学习一下!\n\n\n环境\nGCC 8.3.0(本人的运行环境) \n\n\ngcc开发选项: https://gcc.gnu.org/onlinedocs/gcc/Developer-Options.html\n\n# 查看class的结构信息(Dump class hierarchy information)g++ -std=c++17 -O0 -fdump-lang-class main.cpp# 编译 g++ -std=c++17 -O0 main.cpp -o main\n\n\nClang 13.0.0\n\n# 查看class的结构信息clang++ -std=c++17 -O0 -c main.cpp -Xclang -fdump-vtable-layouts\n\n例子#include <iostream>// 子类struct Base { virtual void test(int x) { std::cout << "Base int x=" << x << std::endl; } void test(double x) { std::cout << "Base double x=" << x << std::endl; }};// 派生类struct Derived : Base { void test(int x) { std::cout << "Derived int x=" << x << std::endl; } void test(double x) { std::cout << "Derived double x=" << x << std::endl; }};void testRef(Base& b) { b.test(1); b.test(1.1);}void test(Base b) { b.test(1); b.test(1.1);}/**基类:Base派生类:Derived虚函数:允许在派生类中被重新定义,支持所谓的运行时多态(late binding)。只有当函数被声明为 virtual,在派生类中覆盖同名函数时才构成真正的多态。这时,如果通过基类指针或引用调用该函数,将调用实际对象类型的那个版本。非虚函数:如果派生类中重新定义了非虚的基类函数(即使函数名和参数列表与基类相同),实际上是隐藏了基类版本的函数。这种场合下,如果派生类对象被当作基类类型处理(比如通过基类指针或引用调用),将调用基类的函数;而如果直接以派生类类型来调用,则使用派生类的函数。*/int main() { Base* b = new Derived(); b->test(1); // Derived int x=1 (虚函数) b->test(1.1); // Base double x=1.1 (非虚函数) testRef(*b); // Derived int x=1 // Base double x=1.1 test(*b); // Base int x=1 // Base double x=1.1 return 0;}\n\n\n\n虚表C++的虚函数底层实现上采用的都是虚表(virtual table),虚表中会把虚函数真实的函数地址记录下来,方便函数调用直接使用,因此虚函数的性能会略差一下因为多了一次寻址! 注意: 这个只是大部分编译器的实现,例如GCC!\n例如下面例子TestA定义了虚函数foo1,那么TestA会生成一个虚函数表,其中定义了 foo1 指向 TestA::foo1 , 其中TestB继承自A它会继承A的虚函数表,如果重写会覆盖重写的函数,下面这个例子B的虚函数表中定义了一个 foo1 指向 TestA::foo1 ,TestC继承了TestB,重写了foo1方法,因此TestC的虚表中变成了TestC::foo1 。\n#include <iostream>struct TestA { virtual void foo1() { std::cout << "TestA.foo1\\n"; }};struct TestB : virtual TestA {};struct TestC : virtual TestB { void foo1() { std::cout << "TestC.foo1\\n"; }};int main() { TestA *ab = new TestB(); ab->foo1(); // 会查找TestB虚表中foo1函数地址(代码段地址),发现foo1为TestA::foo1,最终执行输出TestA.foo1 TestA *ac = new TestC(); ac->foo1(); // 会查找TestC虚表中foo1函数地址(代码段地址),发现foo1为TestC::foo1,最终执行输出TestC.foo1}\n\n虚表是C++元信息的体现,它记录了虚函数的函数地址,但是C++本质上并不会为runtime阶段提供类型的元信息,例如类型的字段、函数信息等,所以C++不支持反射。但是c++是一个直接面向内存的语言,只要拿到了内存什么语法限制(private?const?)都不存在了。\n虚函数虚函数是继承的核心,派生类允许重写父类的虚函数,进而实现多态!\n代码示例: https://godbolt.org/z/TEEsYra7f 或者 https://coliru.stacked-crooked.com/a/9895523dd69257b6\n单继承#include <iostream>struct TestA { virtual void foo1() { std::cout << "TestA.foo1\\n"; } virtual void foo2() { std::cout << "TestA.foo2\\n"; } void foo3() { std::cout << "TestA.foo3\\n"; } long arr[3];};struct TestB : TestA { virtual void foo1() { std::cout << "TestB.foo1\\n"; } virtual void foo3() { std::cout << "TestB.foo3\\n"; } long arr[3];};struct TestC : TestB { virtual void foo4() { std::cout << "TestC.foo4\\n"; }};// 理解这个需要理解 虚函数继承的实现方式和类对象的内存结构struct TestAMemory { struct TestATable { void (*foo1)(); // 指向 TestA.foo1 函数 (offset=16) void (*foo2)(); // 指向 TestA.foo2 函数 }; TestATable *vptr; long arr[3];};// 普通继承的内存结构// 优先定义基类,再定义派生类struct TestBMemory { struct TestBTable { void (*foo1)(); // 指向 TestB.foo1 函数 (offset=16) void (*foo2)(); // 指向 TestA.foo2 函数 (这里可以看到虚表会拷贝基类全部的虚函数,不管是否重写) void (*foo3)(); // 指向 TestB.foo3 函数 (添加自己的虚函数) }; TestBTable *vptr; // TestA.arr long arra[3]; // TestB.arr long arrb[3];};int main() { TestA *a = new TestB(); a->foo1(); // TestB.foo1 a->foo2(); // TestA.foo2 a->foo3(); // TestA.foo3 ((TestB *)a)->foo1(); // TestB.foo1 ((TestB *)a)->foo2(); // TestA.foo2 ((TestB *)a)->foo3(); // TestB.foo3 // TestB的内存结构,这个参考就行了 ... (实际上TestB成员函数地址拿不到的,因为他是编译信息.) TestBMemory *bb = (TestBMemory *)(a); bb->vptr->foo1(); // TestB.foo1 bb->vptr->foo2(); // TestA.foo2 bb->vptr->foo3(); // TestB.foo3 // 修改成员变量 bb->arra[0] = 111; bb->arrb[0] = 222; // 当基类和派生类定义类相同的字段/函数,那么主要是看当前指针的类型是什么,类型是基类那么就是基类的函数,否则派生类的函数 std::cout << "(TestA)arr[0]: " << a->arr[0] << "\\n"; // (TestA)arr[0]: 111 std::cout << "(TestB)arr[0]: " << ((TestB *)a)->arr[0] << "\\n"; // (TestB)arr[0]: 222 // 同上 TestAMemory *aa = (TestAMemory *)(new TestA()); aa->vptr->foo1(); // TestA.foo1 aa->vptr->foo2(); // TestA.foo2}\n\n那么具体是如何实现的呢? 可以通过 g++ -O0 -fdump-lang-class main.cpp dump 类信息\nVtable for TestATestA::_ZTV5TestA: 4 entries0 (int (*)(...))08 (int (*)(...))(& _ZTI5TestA)16 (int (*)(...))TestA::foo124 (int (*)(...))TestA::foo2Class TestA size=32 align=8 // TestA size = 32 = 8(vptr:TestA) + 24 (arr) base size=32 base align=8TestA (0x0x7fdde31d2960) 0 vptr=((& TestA::_ZTV5TestA) + 16) // 表示vtpr的指向,指向 (TestA::_ZTV5TestA addr +16), 即TestA::foo1函数地址. Vtable for TestBTestB::_ZTV5TestB: 5 entries0 (int (*)(...))08 (int (*)(...))(& _ZTI5TestB)16 (int (*)(...))TestB::foo1 // 重写基类虚函数24 (int (*)(...))TestA::foo2 // 这里注意下. 会把未重写基类的copy过来32 (int (*)(...))TestB::foo3 // 添加自己定义的虚函数Class TestB size=56 align=8 base size=56 base align=8 // size = 56 = 8(vptr:TestB) + 24(a.arr) + 24(b.arr) TestB (0x0x7fdde322d068) 0 vptr=((& TestB::_ZTV5TestB) + 16) // 同上 TestA (0x0x7fdde31d2de0) 0 primary-for TestB (0x0x7fdde322d068) // primary-for TestB 表示继承关系,多继承需要看这个字段Vtable for TestCTestC::_ZTV5TestC: 6 entries0 (int (*)(...))08 (int (*)(...))(& _ZTI5TestC)16 (int (*)(...))TestB::foo124 (int (*)(...))TestA::foo232 (int (*)(...))TestB::foo340 (int (*)(...))TestC::foo4 // 可以看到直接copy的基类的全部虚函数+自己定义的虚函数Class TestC // c -> b -> a size=56 align=8 // size = 8(vptr:TestC) + 24(a.arr) + 24(b.arr) base size=56 base align=8TestC (0x0x7f639bfee680) 0 vptr=((& TestC::_ZTV5TestC) + 16) TestB (0x0x7f639bfee6e8) 0 primary-for TestC (0x0x7f639bfee680) TestA (0x0x7f639bfd7780) 0 primary-for TestB (0x0x7f639bfee6e8)\n\n总结:\n\n可以发现当定义了虚函数那么此时会生成一个虚函数表,虚函表记录了虚函数的函数地址,例如 TestA 内部会定义一个 vptr 指向 Vtable for TestA + 16 ,即 (int (*)(...))TestA::foo1 函数开始\n\nTestB 继承了 TestA,TestB内部也定义了一个 vptr 指向 Vtable for TestB , 定义了其申明的虚函数\n\nTestC 继承 TestB ,此时也只会有一份vptr指向 vtable testc \n\n单继承仅会有一个 vptr ,指向其自己的 vtable\n\n\n多继承C++是支持多继承的,这个也是与Java等语言的区别(C++不支持接口Interface),其次多继承会比较复杂,比如涉及到交叉/菱形继承的问题,我们可以看下C++是符合实现多继承的!\n继续上面那个例子,我们新增一个结构体, D 继承自A/B/C\nstruct TestD : TestA, TestB, TestC { void foo1() { std::cout << "TestD.foo1\\n"; }};// 继承关系/多继承/菱形继承// D -> A// -> B -> A// -> C -> B -> A\n\n此时虚表为如下:\nVtable for TestDTestD::_ZTV5TestD: 15 entries0 (int (*)(...))08 (int (*)(...))(& _ZTI5TestD)16 (int (*)(...))TestD::foo1 // vptr(TestD/TestA)24 (int (*)(...))TestA::foo232 (int (*)(...))-3240 (int (*)(...))(& _ZTI5TestD)48 (int (*)(...))TestD::_ZThn32_N5TestD4foo1Ev // vptr(TestB) TestD::foo156 (int (*)(...))TestA::foo264 (int (*)(...))TestB::foo372 (int (*)(...))-8880 (int (*)(...))(& _ZTI5TestD)88 (int (*)(...))TestD::_ZThn88_N5TestD4foo1Ev //vptr(TestC) TestD::foo196 (int (*)(...))TestA::foo2104 (int (*)(...))TestB::foo3112 (int (*)(...))TestC::foo4Class TestD size=144 align=8 // size = 144 = vptr(TestD) + a.arr + vptr(TestB) + (a.arr,b.arr) + vptr(TestC) + (a.arr, b.arr) base size=144 base align=8TestD (0x0x7efe8ac38d98) 0 // vptr(TestD)偏移量为0 vptr=((& TestD::_ZTV5TestD) + 16) TestA (0x0x7efe8acbe840) 0 // d::a内存 primary-for TestD (0x0x7efe8ac38d98) TestB (0x0x7efe8acd5750) 32 // vptr(TestB)偏移量32 vptr=((& TestD::_ZTV5TestD) + 48) TestA (0x0x7efe8acbe8a0) 32 // b::a 内存 primary-for TestB (0x0x7efe8acd5750) TestC (0x0x7efe8acd57b8) 88 // vptr(TestC)偏移量88 vptr=((& TestD::_ZTV5TestD) + 88) TestB (0x0x7efe8acd5820) 88 // c::b 内存 primary-for TestC (0x0x7efe8acd57b8) TestA (0x0x7efe8acbe900) 88 // c::a 内存 primary-for TestB (0x0x7efe8acd5820)\n\n\nTestD 交叉继承造成结构的大小升级到了 144 ,导致 A冗余了2份,B 冗余了1份 ,是不是发现问题了,这么继承的话遇到重复继承基类导致内存会成倍的增加,怎么解决呢,下文会介绍到!\n多继承会为每个基类分配一个 vptr 指针!\nvptr(TestD) 偏移量 0\nvptr(TestB) 偏移量 32\nvptr(TestC) 偏移量 88\n\n\n多继承当涉及到类型转换的时候(向上/向下)类型转换的时候会涉及到指针的移动(下文会降到),具体的移动偏移量可以参考上面的class dump,向下转型需要使用 dynamic_cast ! 但是上面的例子多类型转换的时候会存在二义性,例如D向上换成A,会发现A内存中有3份到低是哪个,所以编译器不会让你转换,但是我们可以通过内存进行非安全转换!!\n可以结合下面这个代码看下\n\ntypedef void (*VoidFunc)();VoidFunc GetVoidFunc(void *ptr, int offset) { long *pptr = (long *)ptr; long *table = (long *)(*pptr); long *func = table + offset; return (VoidFunc)(*func);}// struct TestD : TestA, TestB, TestC {}// TestD = vptr(TestA) + a.arr + vptr(TestB) + (a.arr,b.arr) + vptr(TestC) + (a.arr, b.arr) = 144int main() { // 多继承vptr会有偏移量 TestD d; // vptr偏移量0 (TestD) GetVoidFunc(&d, 0)(); // TestD::foo1 GetVoidFunc(&d, 1)(); // TestA::foo2 // vptr偏移量32(基类为TestB) GetVoidFunc(((long *)&d) + 4, 0)(); // TestD.foo1 GetVoidFunc(((long *)&d) + 4, 1)(); // TestA.foo2 GetVoidFunc(((long *)&d) + 4, 2)(); // TestB.foo3 // vptr偏移量88(基类为TestC) GetVoidFunc(((long *)&d) + 11, 0)(); // TestD.foo1 GetVoidFunc(((long *)&d) + 11, 1)(); // TestA.foo2 GetVoidFunc(((long *)&d) + 11, 2)(); // TestB.foo3 GetVoidFunc(((long *)&d) + 11, 3)(); // TestC.foo4}\n\n虚继承代码示例: https://godbolt.org/z/rxeza5EEa 或者 https://coliru.stacked-crooked.com/a/44776e393808d238\n#include <iostream>struct TestA { virtual void foo1() { std::cout << "TestA.foo1\\n"; } virtual void foo2() { std::cout << "TestA.foo2\\n"; } void foo3() { std::cout << "TestA.foo3\\n"; } long arr[3];};struct TestB : virtual TestA { virtual void foo1() { std::cout << "TestB.foo1\\n"; } void foo2() { std::cout << "TestB.foo2\\n"; } virtual void foo3() { std::cout << "TestB.foo3\\n"; } long arr[3];};struct TestC : virtual TestB { virtual void foo4() { std::cout << "TestC.foo4\\n"; }};struct TestD : virtual TestB { virtual void foo4() { std::cout << "TestC.foo4\\n"; }};// TestE 继承关系// -> C// -> -> B// E -> A// -> -> B// -> Dstruct TestE : virtual TestC, virtual TestD { void foo1() { std::cout << "TestD.foo1\\n"; }};typedef void (*VoidFunc)();VoidFunc GetVoidFunc(void *ptr, int offset) { long *pptr = (long *)ptr; long *table = (long *)(*pptr); long *func = table + offset; return (VoidFunc)(*func);}VoidFunc GetVoidFunc(void *ptr, int size, int offset) { long *pptr = ((long *)ptr) + size; // 指针偏移 long *table = (long *)(*pptr); long *func = table + offset; // vtable 偏移 return (VoidFunc)(*func);}// https://stackoverflow.com/questions/6258559/what-is-the-vtt-for-a-class// 72int main() { // (TestC)vptr + (TestB)vptr + 24 + (TestA)vptr + 24 TestC *c = new TestC(); GetVoidFunc(c, 0, 0)(); // TestC.foo4 GetVoidFunc(c, 1, 0)(); // TestB::foo1 GetVoidFunc(c, 1, 1)(); // TestB::foo2 GetVoidFunc(c, 1, 2)(); // TestB::foo3 GetVoidFunc(c, 5, 0)(); // TestB.foo1 GetVoidFunc(c, 5, 1)(); // TestB.foo2 // 堆是从低地址到高地址 // 强制类型转换会移动函数指针 TestB *b = c; // 偏移量为8: 1个字节 = vptr(TestC) std::cout << ((long)b - (long)c) << std::endl; TestA *a = c; // 偏移量为40: 5个字节 = vptr(TestC) + vptr(TestB) + 24 std::cout << ((long)a - (long)c) << std::endl; TestE *d = new TestE(); // vptr(TestC) + vptr(TestB) + 24 + vptr(TestA) + 24 + vptr(TestD) GetVoidFunc(d, 9, 0)(); // TestD::foo4 std::cout << "size: " << sizeof(TestA) << "\\n"; // 32 = vptr(TestA) + 24 std::cout << "size: " << sizeof(TestB) << "\\n"; // 64 = vptr(TestB) + 24 + vptr(TestA) + 24 std::cout << "size: " << sizeof(TestC) << "\\n"; // 72 = vptr(TestC) + vptr(TestB) + 24 + vptr(TestA) + 24 std::cout << "size: " << sizeof(TestD) << "\\n"; // 72 = vptr(TestD) + vptr(TestB) + 24 + vptr(TestA) + 24 std::cout << "size: " << sizeof(TestE) << "\\n"; // 80 = vptr(TestE) + vptr(TestB) + 24 + vptr(TestA) + 24 + vptr(TestD)}\n\n\n虚继承后内存仅需 72,只需要维护基类的vptr 和 基类分配的内存即可,所以虚继承可以极大的降低内存开销!\n虚继承后内存中有且仅有一份基类的内存(包含多层引用),具体的内存逻辑图可以通过 dump class查看\n多继承当进行强制类型转换时会通过移动指针实现,具体可以看下面例子,但是其实还有一些case,比如TestE中 TestE会和TestC的地址一样,原因很简单就是两者在virtual table中函数申明都一样,所以没必要再分配一份内存了(这个属于GCC的优化吧)!\n\nint main(){ TestE *ee = new TestE(); // vptr(TestE/TestC) + vptr(TestB) + 24 + vptr(TestA) + 24 + vptr(TestD) TestC *cc = ee; TestD *dd = ee; TestB *bb = ee; TestA *aa = ee; std::cout << std::hex << ee << "\\n"; // 0x600002ef80f0 (offset=0) std::cout << std::hex << cc << "\\n"; // 0x600002ef80f0 (offset=0) std::cout << std::hex << bb << "\\n"; // 0x600002ef80f8 (offset=8) std::cout << std::hex << aa << "\\n"; // 0x600002ef8118 (offset=40) std::cout << std::hex << dd << "\\n"; // 0x600002ef8138 (offset=72)}\n\n\n虚继承表如下图所示, 这里以 TestC 为例子\n\n# TestB Vtable for TestBTestB::_ZTV5TestB: 12 entries0 328 (int (*)(...))016 (int (*)(...))(& _ZTI5TestB)24 (int (*)(...))TestB::foo132 (int (*)(...))TestB::foo240 (int (*)(...))TestB::foo348 1844674407370955158456 1844674407370955158464 (int (*)(...))-3272 (int (*)(...))(& _ZTI5TestB)80 (int (*)(...))TestB::_ZTv0_n24_N5TestB4foo1Ev88 (int (*)(...))TestB::_ZTv0_n32_N5TestB4foo2EvVTT for TestBTestB::_ZTT5TestB: 2 entries0 ((& TestB::_ZTV5TestB) + 24)8 ((& TestB::_ZTV5TestB) + 80)Class TestB size=64 align=8 base size=32 base align=8TestB (0x0x7f779a7cc618) 0 vptridx=0 vptr=((& TestB::_ZTV5TestB) + 24) TestA (0x0x7f779a7b5660) 32 virtual vptridx=8 vbaseoffset=-24 vptr=((& TestB::_ZTV5TestB) + 80)## TestCVtable for TestCTestC::_ZTV5TestC: 20 entries0 408 816 (int (*)(...))024 (int (*)(...))(& _ZTI5TestC)32 (int (*)(...))TestC::foo440 048 056 064 3272 (int (*)(...))-880 (int (*)(...))(& _ZTI5TestC)88 (int (*)(...))TestB::foo196 (int (*)(...))TestB::foo2104 (int (*)(...))TestB::foo3112 18446744073709551584120 18446744073709551584128 (int (*)(...))-40136 (int (*)(...))(& _ZTI5TestC)144 (int (*)(...))TestB::_ZTv0_n24_N5TestB4foo1Ev152 (int (*)(...))TestB::_ZTv0_n32_N5TestB4foo2EvConstruction vtable for TestB in TestCTestC::_ZTC5TestC8_5TestB: 12 entries0 328 (int (*)(...))016 (int (*)(...))(& _ZTI5TestB)24 (int (*)(...))TestB::foo132 (int (*)(...))TestB::foo240 (int (*)(...))TestB::foo348 1844674407370955158456 1844674407370955158464 (int (*)(...))-3272 (int (*)(...))(& _ZTI5TestB)80 (int (*)(...))TestB::_ZTv0_n24_N5TestB4foo1Ev88 (int (*)(...))TestB::_ZTv0_n32_N5TestB4foo2EvVTT for TestCTestC::_ZTT5TestC: 5 entries0 ((& TestC::_ZTV5TestC) + 32)8 ((& TestC::_ZTV5TestC) + 88)16 ((& TestC::_ZTV5TestC) + 144)24 ((& TestC::_ZTC5TestC8_5TestB) + 24)32 ((& TestC::_ZTC5TestC8_5TestB) + 80)Class TestC size=72 align=8 // vptr(TestC) + vptr(TestB) + b.arr + vptr(TestA) + a.arr base size=8 base align=8TestC (0x0x7f779a7cc750) 0 nearly-empty ## offset=0 (TestC vptr) -> 32 vptridx=0 vptr=((& TestC::_ZTV5TestC) + 32) TestB (0x0x7f779a7cc7b8) 8 virtual ## offset=8 (TestB vptr) -> 88 subvttidx=24 vptridx=8 vbaseoffset=-24 vptr=((& TestC::_ZTV5TestC) + 88) TestA (0x0x7f779a7b57e0) 40 virtual ## offset=40 (TestA vptr) -> 144 vptridx=16 vbaseoffset=-32 vptr=((& TestC::_ZTV5TestC) + 144)# TestEClass TestE size=80 align=8 # 80 = vptr(TestE/TestC) + vptr(TestB) + 24 + vptr(TestA) + 24 + vptr(TestD) base size=8 base align=8TestE (0x0x7f9b2d91d1c0) 0 nearly-empty vptridx=0 vptr=((& TestE::_ZTV5TestE) + 56) TestC (0x0x7f9b2d91c9c0) 0 nearly-empty virtual primary-for TestE (0x0x7f9b2d91d1c0) subvttidx=40 vptridx=8 vbaseoffset=-48 TestB (0x0x7f9b2d91ca28) 8 virtual subvttidx=64 vptridx=16 vbaseoffset=-24 vptr=((& TestE::_ZTV5TestE) + 120) TestA (0x0x7f9b2d905960) 40 virtual vptridx=24 vbaseoffset=-32 vptr=((& TestE::_ZTV5TestE) + 176) TestD (0x0x7f9b2d91ca90) 72 nearly-empty virtual subvttidx=80 vptridx=32 vbaseoffset=-56 vptr=((& TestE::_ZTV5TestE) + 232) TestB (0x0x7f9b2d91ca28) alternative-path\n\n析构函数上面聊到了继承,但是没有聊到内存回收,我们知道不论是虚继承、普通继承他的内存分配机制大家上面应该是有所了解了,但是对于内存回收没谈到,C++作为一个非GC语言需要手动回收。析构函数使用虚函数完美的解决了内存回收,那么具体怎么使用呢?\n注意:抽象类一定要为把析构函数定义为虚函数,否则系统不会回收!\n#include <iostream>#include <memory>struct TestA { virtual ~TestA() { std::cout << "~TestA()\\n"; }};struct TestB : virtual TestA { ~TestB() override { std::cout << "~TestB()\\n"; }};struct TestC : virtual TestA { ~TestC() override { std::cout << "~TestC()\\n"; }};struct TestD : virtual TestB, virtual TestC { ~TestD() override { std::cout << "~TestD()\\n"; }};int main() { // 手动基类删除,这时候如果析构函数不是虚函数就会存在问题 TestA *a = new TestD(); delete a; // 这种case不需要析构函数是虚函数,回收d的时候会依次回收继承的父类 TestD *td = new TestD(); delete td; // 通 TestD *td 这种case,无需定义虚函数 std::shared_ptr<TestA> share_a = std::make_shared<TestD>();}// ~TestD()// ~TestC()// ~TestB()// ~TestA()\n\n还有一个case大家有兴趣可以看下,就是多继承类型转换会涉及到指针移动,因此如果没有虚继承很可能会出现 pointer being freed was not allocated: https://zhuanlan.zhihu.com/p/26392392 。 我相信通过本文的学习对于这个问题应该大家也能知道为啥会报错!\n或者还有一种就是把构造(包含析构)函数设置为protected防止向上转型后调用析构函数,这么的话由派生类去析构就不会出现问题,代码例子: https://godbolt.org/z/d15KdT3PM 。\n坑类型转换 和 void指针的坑我在使用http_parser库封装http库的时候发现,http_parser库是c语言写的,对接C++的话暴露了一堆void指针,就导致很容易写出BUG代码!下面是我写了一个demo,为了复用我抽出了 headers_parser 、body_parser ,导致需要void指针向上转型\n#include <iostream>#include <map>using headers_t = std::map<std::string, std::string>;struct headers_parser { static void on_header(void* ptr, const std::string& key, const std::string& value) { auto parser = static_cast<headers_parser*>(ptr); parser->headers_[key] = value; } headers_t headers_{};};struct body_parser { static void on_data(void* ptr, const std::string& data) { auto parser = static_cast<body_parser*>(ptr); parser->body_ = data; } std::string body_{}; headers_t extendion_headers_{};};struct response_parser : headers_parser, body_parser { std::string status_{}; std::string version_{};};int main() { response_parser parser{}; response_parser::on_data(&parser, "data"); response_parser::on_header(&parser, "k1", "v1");}\n\n运行一下???我去内存访问异常了,出现了段错误!\nProcess finished with exit code 139 (interrupted by signal 11:SIGSEGV)\n\n怎么定位呢?估计是类型转换的问题,我debug调试了下发现 static_cast 在转换void指针的时候是非常的暴力!!就是内存填充一下就行了!你可以看一下下面这个例子!\nint main() { response_parser parser{}; std::cout << &parser << "\\n"; void* ptr = &parser; auto header = static_cast<headers_parser*>(ptr); std::cout << header << "\\n"; auto body = static_cast<body_parser*>(ptr); std::cout << body << "\\n";}\n\n内存结果就是如图所示!\n\n但是实际上内存是啥了?\n\n所以那里出问题了呢?问题就是 void* 导致编译器丢失了原类型,导致向上转型失败了! \n那么很多时候我们和一些 c 语言的库进行打交道的时候,确实存在一堆 void* 指针,这时候如果我们直接 static_cast 转换的话会直接出BUG!怎么解决这个问题?\n\n如果不涉及到 void* 指针, 下面代码没问题\n\nint main() { response_parser parser{}; auto header = static_cast<headers_parser*>(&parser); std::cout << header << "\\n"; auto body = static_cast<body_parser*>(&parser); // dynamic_cast 也可以,这里面static_cast性能更高 std::cout << body << "\\n";}\n\n\n如果涉及到void 指针,只能将 void* 强制转换为 response_parser 然后再用类型转换!\n\nint main() { response_parser parser{}; void* ptr = &parser; auto header = static_cast<headers_parser*>(static_cast<response_parser*>(ptr)); std::cout << header << "\\n"; auto body = static_cast<body_parser*>(static_cast<response_parser*>(ptr)); std::cout << body << "\\n";}\n\n那么怎么解决我们的问题?是的只能用C++模版了,最终完美解决!\n#include <iostream>#include <map>using headers_t = std::map<std::string, std::string>;template <typename T>struct headers_parser { static void on_header(void* ptr, const std::string& key, const std::string& value) { auto parser = static_cast<headers_parser*>(static_cast<T*>(ptr)); parser->headers_[key] = value; } headers_t headers_{};};template <typename T>struct body_parser { static void on_data(void* ptr, const std::string& data) { auto parser = static_cast<body_parser*>(static_cast<T*>(ptr)); parser->body_ = data; } std::string body_{}; headers_t extendion_headers_{};};struct response_parser : headers_parser<response_parser>, body_parser<response_parser> { std::string status_{}; std::string version_{};};int main() { response_parser parser{}; response_parser::on_data(&parser,"data"); response_parser::on_header(&parser,"k1","v1");}\n\n怎么评价 staic_cast 和 dynamic_cast 呢,在不清楚上下文的情况下用 dynamic_cast,否者用 staic_cast ,后者性能高!\nobject slicing#include "spdlog/spdlog.h"#include "gtest/gtest.h"struct reader { reader() = default; virtual ~reader() = default; virtual size_t read_some(char* buffer, size_t buffer_size) { return 0; };};struct buffer_reader final : reader { explicit buffer_reader(std::string& buffer) : buffer_(buffer) { } size_t read_some(char* buffer, size_t buffer_size) override { // auto size = buffer_size > buffer_.size() ? buffer_.size() : buffer_size; // std::copy_n(buffer_.begin(), size, buffer); return buffer_size; }private: std::string& buffer_;};void test(reader& r) { SPDLOG_INFO(r.read_some(nullptr, 100));}TEST(Struct, oo) { std::string buffer = "hello world"; // 直接值赋值就会出现 object slicing ! reader r = buffer_reader(buffer); SPDLOG_INFO(r.read_some(nullptr, 10)); // 输出0 buffer_reader br = buffer_reader(buffer); test(br); // 输出100 // 解决:引用/指针,就不会出现值赋值}\n\n这个其实很坑的,大家注意奥!\n总结\n只要基类定义了虚函数,就会添加到基类的虚函数表中,子类重写后是否标记为虚函数(virtual修饰)都会添加到子类的虚函数表中\n虚继承,可以降低内存开销,在一些交叉继承中有效果\n子类继承的时候最好用 override 修饰一下函数重写,方便代码阅读\n子类不涉及到重写,没必要设置一个虚函数,因为会额外分配内存\n抽象类一定要把析构函数设置为虚函数,否则会存在内存泄漏或者内存回收出现空引用问题\n如果遇到特别不理解的,看一下 dump class 看看或者查看下mem\n虚表的设计或多或少有些冗余,因为在派生类中记录下来全部的虚函数的函数地址(是否重写都会记录),这个过程在编译期就决定了,可以通过查看汇编代码发现数据段中定义有virtual table.\n多阅读源码、优秀的开源项目可以掌握不少技巧,多记录多尝试\n\n其他小点\n我们可以使用 traits 的 std::is_convertible 来判断是否可以进行类型转换, 实践可以参考 std::enable_shared_from_this \n例子:https://godbolt.org/z/sTzz4o9he\n\n\n\n// -std=c++17 is_convertible_v 是c17提供的,c11可能写法麻烦点#include <iostream>#include <type_traits>struct TestB {};struct TestC : private TestB {};struct TestD : TestB {};template <typename T>typename std::enable_if<std::is_convertible_v<T, TestB *>>::type DoPrint(T t) { std::cout << "impl TestB*" << std::endl;}template <typename T>typename std::enable_if<std::is_convertible_v<T, TestB>>::type DoPrint(T t) { std::cout << "impl TestB" << std::endl;}void DoPrint(...) { std::cout << "not impl TestB" << std::endl;}int main() { DoPrint(new TestD()); // impl TestB* DoPrint(TestD{}); // impl TestB DoPrint(TestC{}); // not impl TestB}\n\n\n继承是可以限定作用域的,继承是无法使用基类private的成员的,除非基类给你开启friend,struct的话默认是public继承,class默认是private继承. protect继承会使用继承的public属性的成员变成protect,private会使用继承的成员变成(public&protect -> private)。 基类申明为final表示禁止继承,override可以显示申明重写!\n\n继承构造函数,当我们想直接服用父类的构造函数需要手动再申明一次!! \n\n\n// -std=c++11#include <iostream>struct TestB { TestB(int f1, int f2) : f1_(f1), f2_(f2) { } virtual int sum() { return f1_ + f2_; }private: int f1_; int f2_;};struct TestC : TestB { using TestB::TestB; // 继承构造函数};struct TestD : TestB { // 需要手动申明,使用构造函数委托 TestD(int f1, int f2) : TestB(f1, f2) { }};int main() { TestC c = TestC(1, 2); std::cout << c.sum() << std::endl; TestD d = TestD(1, 2); std::cout << c.sum() << std::endl;}\n\n参考文章\nC++对象模型: https://www.miaoerduo.com/2023/01/19/cpp-object-model/\n\nCPP-Virtual-Table:https://leimao.github.io/blog/CPP-Virtual-Table-Table/\n\nC++中的deleting destructor:https://zhuanlan.zhihu.com/p/26392392\n\n\n","categories":["C++"],"tags":["C++","面向对象"]},{"title":"tcpdump","url":"/2021/03/29/a22db6b9984d48fcdf4adcc9ff16c659/","content":" 使用tcpdump + wireshark 可以很好的帮助我们解决线上抓包问题!这里同时也推荐一下我写的一个工具,tcpdump decoder, 因为线上我们并没有wireshark的可视化工具,往往需要tcpdump -w tcpdump.pcap 然后本地通过wireshark调试,是的太过于麻烦和冗余,因此我个人开了一个工具,可以在线解析 HTTP/Thrift流量!\n\n\ntcpdump 命令介绍\n一般来说 tcpdump 命令有三部分组成, tcpdump -i [interface] '[pacp expression]' [options]\n\n\ninterface: 标识网卡,指的你的网卡名称,可以通过 ip addr or ifconfig 查看\npacp expression: pcap表达式,可以理解为过滤表达式\noptions: 一些额外的参数,\n -n 标识不解析host \n -X hexdump打印payload\n-v 解析payload(一般就只支持一些纯文本协议)\n-t 隐藏时间搓\n-w 保存到文件\n-r 读取文件\n\n\n相关文档\npcap filter: https://www.tcpdump.org/manpages/pcap-filter.7.html\ntcpdump: https://www.tcpdump.org/manpages/tcpdump.1.html\n\n\n\n\n获取网卡. 一般你需要确定你要抓取的ip,然后再去确认网卡, 例如我要抓取10.248.166.215 则需要使用 eth0的网卡!\n\nfanhaodong.516:~/ $ ip addr [14:23:51]1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 inet 127.0.0.1/8 scope host lo valid_lft forever preferred_lft forever inet6 ::1/128 scope host valid_lft forever preferred_lft forever2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000 link/ether fa:16:3e:34:9d:82 brd ff:ff:ff:ff:ff:ff inet 10.248.166.215/22 brd 10.248.167.255 scope global eth0 valid_lft forever preferred_lft forever inet6 fdbd:dc03:ff:1:1:248:166:215/128 scope global valid_lft forever preferred_lft forever inet6 fe80::f816:3eff:fe34:9d82/64 scope link valid_lft forever preferred_lft forever\n\n\n抓包\n\n➜ tcpdump -i lo0 'port 8080 and host ::1' -l -n -vtcpdump: listening on lo0, link-type NULL (BSD loopback), capture size 262144 bytes14:11:38.539268 IP6 (flowlabel 0x20900, hlim 64, next-header TCP (6) payload length: 212) ::1.8080 > ::1.59678: Flags [P.], cksum 0x00dc (incorrect -> 0x72c5), seq 633202846:633203026, ack 3120721099, win 6164, options [nop,nop,TS val 1356080951 ecr 3287837093], length 180: HTTP, length: 180\tHTTP/1.1 200 OK\tContent-Encoding: gzip\tDate: Wed, 31 Aug 2022 06:11:38 GMT\tTransfer-Encoding: chunked\t3d\n\n\n高级过滤\n\n# 区分 src 和 dst tcpdump -i lo0 'src port 8080 and dst host ::1' -X# 抓取sync包,也就是握手包➜ tcpdump -i lo0 'tcp[13]=0x02' -l -ntcpdump: verbose output suppressed, use -v or -vv for full protocol decodelistening on lo0, link-type NULL (BSD loopback), capture size 262144 bytes14:20:24.679509 IP 127.0.0.1.63788 > 127.0.0.1.9229: Flags [S], seq 220694465, win 65535, options [mss 16344,nop,wscale 6,nop,nop,TS val 3788980622 ecr 0,sackOK,eol], length 014:20:24.680149 IP 127.0.0.1.63790 > 127.0.0.1.9229: Flags [S], seq 3821191416, win 65535, options [mss 16344,nop,wscale 6,nop,nop,TS val 278963318 ecr 0,sackOK,eol], length 014:20:25.682374 IP 127.0.0.1.63792 > 127.0.0.1.9229: Flags [S], seq 1726308526, win 65535, options [mss 16344,nop,wscale 6,nop,nop,TS val 818205733 ecr 0,sackOK,eol], length 014:20:25.683262 IP 127.0.0.1.63794 > 127.0.0.1.9229: Flags [S], seq 3871219645, win 65535, options [mss 16344,nop,wscale 6,nop,nop,TS val 1976538711 ecr 0,sackOK,eol], length 014:20:26.685361 IP 127.0.0.1.63797 > 127.0.0.1.9229: Flags [S], seq 2836663358, win 65535, options [mss 16344,nop,wscale 6,nop,nop,TS val 2399073465 ecr 0,sackOK,eol], length 014:20:26.686232 IP 127.0.0.1.63799 > 127.0.0.1.9229: Flags [S], seq 1158021919, win 65535, options [mss 16344,nop,wscale 6,nop,nop,TS val 837560897 ecr 0,sackOK,eol], length 0\n\n\n特殊case,我们很多时候请求的是 域名,那么怎么解决呢?可以通过下面两种方式获取\n\n#方式1. 获取a记录➜ test git:(fix/bug/1_0_4) ✗ dig www.baidu.com## ....www.baidu.com.\t\t134\tIN\tCNAME\twww.a.shifen.com.www.a.shifen.com.\t134\tIN\tA\t220.181.38.149www.a.shifen.com.\t134\tIN\tA\t220.181.38.150#方式2. curl 获取ip ➜ curl www.baidu.com -v* Trying 220.181.38.150:80...* Connected to www.baidu.com (220.181.38.150) port 80 (#0)#方式3. 直接过滤hosttcpdump -i en0 'host www.baidu.com' -l -n -v\n\n抓取HTTP服务流量\n我们需要创建一个HTTP Server, 这个接口有点复杂,是Chunked编码+压缩的!\n\npackage mainimport (\t"fmt"\t"log"\t"github.com/anthony-dong/go-sdk/commons"\t"github.com/anthony-dong/go-sdk/commons/codec/http_codec"\t"github.com/gin-gonic/gin")func main() {\trouter := gin.Default()\trouter.GET("/api/v1/:compress", func(context *gin.Context) {\t\tresp := map[string]interface{}{\t\t\t"data": commons.NewString('A', 1024*8),\t\t}\t\tdata := commons.ToJsonString(resp)\t\tcontext.Writer.Header().Set("Transfer-Encoding", "chunked")\t\tcompress := context.Param("compress")\t\tif !http_codec.CheckAcceptEncoding(context.Request.Header, compress) {\t\t\tcompress = ""\t\t}\t\tif err := http_codec.EncodeHttpBody(context.Writer, context.Writer.Header(), []byte(data), compress); err != nil {\t\t\tcontext.JSON(500, map[string]interface{}{\t\t\t\t"error": err.Error(),\t\t\t})\t\t\treturn\t\t}\t})\tfmt.Println("Listen: http://localhost:8080\\nTest: http://localhost:8080/api/v1/gzip")\tif err := router.Run(":8080"); err != nil {\t\tlog.Fatal(err)\t}}\n\n\n启动并且抓取流量\n\n➜ test git:(fix/bug/1_0_4) ✗ tcpdump -i lo0 'port 8080' -l -v -ntcpdump: listening on lo0, link-type NULL (BSD loopback), capture size 262144 bytes14:31:22.066978 IP6 (flowlabel 0x80f00, hlim 64, next-header TCP (6) payload length: 814) ::1.50327 > ::1.8080: Flags [P.], cksum 0x0336 (incorrect -> 0x2a59), seq 3927208909:3927209691, ack 3528028485, win 6369, options [nop,nop,TS val 4277916016 ecr 1002452734], length 782: HTTP, length: 782\tGET /api/v1/gzip HTTP/1.1\tHost: localhost:8080\tConnection: keep-alive\tCache-Control: max-age=0\tsec-ch-ua: "Chromium";v="104", " Not A;Brand";v="99", "Google Chrome";v="104"\tsec-ch-ua-mobile: ?0\tsec-ch-ua-platform: "macOS"\tUpgrade-Insecure-Requests: 1\tUser-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36\tAccept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9\tSec-Fetch-Site: none\tSec-Fetch-Mode: navigate\tSec-Fetch-User: ?1\tSec-Fetch-Dest: document\tAccept-Encoding: gzip, deflate, br\tAccept-Language: zh-CN,zh;q=0.9\tCookie: Hm_lvt_899b2a5c34078209c5f30853eaaa7846=1650731778,1652082863,165294802914:31:22.067020 IP6 (flowlabel 0x10e00, hlim 64, next-header TCP (6) payload length: 32) ::1.8080 > ::1.50327: Flags [.], cksum 0x0028 (incorrect -> 0x68da), ack 782, win 6347, options [nop,nop,TS val 1002463298 ecr 4277916016], length 014:31:22.067407 IP6 (flowlabel 0x10e00, hlim 64, next-header TCP (6) payload length: 189) ::1.8080 > ::1.50327: Flags [P.], cksum 0x00c5 (incorrect -> 0xdce5), seq 1:158, ack 782, win 6347, options [nop,nop,TS val 1002463298 ecr 4277916016], length 157: HTTP, length: 157\tHTTP/1.1 200 OK\tContent-Encoding: gzip\tDate: Wed, 31 Aug 2022 06:31:22 GMT\tTransfer-Encoding: chunked\t2614:31:22.067429 IP6 (flowlabel 0x80f00, hlim 64, next-header TCP (6) payload length: 32) ::1.50327 > ::1.8080: Flags [.], cksum 0x0028 (incorrect -> 0x682a), ack 158, win 6366, options [nop,nop,TS val 4277916016 ecr 1002463298], length 0\n\n我们可以发现并不能拿到chunked编码的内容,这里只展示了第一行 26,如何解决呢? 通过 gtool tcpdump\n➜ test git:(fix/bug/1_0_4) ✗ tcpdump -i lo0 'port 8080' -l -X -n | gtool tcpdumptcpdump: verbose output suppressed, use -v or -vv for full protocol decodelistening on lo0, link-type NULL (BSD loopback), capture size 262144 bytes14:32:08.549007 IP6 ::1.8080 > ::1.50327: Flags [.], ack 3927210473, win 6335, length 014:32:08.549037 IP6 ::1.50327 > ::1.8080: Flags [.], ack 1, win 6364, options [nop,nop,TS val 4277962497 ecr 1002494741], length 014:32:08.570433 IP6 ::1.50327 > ::1.8080: Flags [P.], seq 1:783, ack 1, win 6364, options [nop,nop,TS val 4277962519 ecr 1002494741], length 782: HTTP: GET /api/v1/gzip HTTP/1.1GET /api/v1/gzip HTTP/1.1Host: localhost:8080Connection: keep-aliveCache-Control: max-age=0sec-ch-ua: "Chromium";v="104", " Not A;Brand";v="99", "Google Chrome";v="104"sec-ch-ua-mobile: ?0sec-ch-ua-platform: "macOS"Upgrade-Insecure-Requests: 1User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9Sec-Fetch-Site: noneSec-Fetch-Mode: navigateSec-Fetch-User: ?1Sec-Fetch-Dest: documentAccept-Encoding: gzip, deflate, brAccept-Language: zh-CN,zh;q=0.9Cookie: Hm_lvt_899b2a5c34078209c5f30853eaaa7846=1650731778,1652082863,165294802914:32:08.570456 IP6 ::1.8080 > ::1.50327: Flags [.], ack 783, win 6323, options [nop,nop,TS val 1002509801 ecr 4277962519], length 014:32:08.571189 IP6 ::1.8080 > ::1.50327: Flags [P.], seq 1:158, ack 783, win 6323, options [nop,nop,TS val 1002509801 ecr 4277962519], length 157: HTTP: HTTP/1.1 200 OKHTTP/1.1 200 OKTransfer-Encoding: chunkedContent-Encoding: gzipDate: Wed, 31 Aug 2022 06:32:08 GMT{"data":"AAAAAAAAAAAAAAAAAAAA"}14:32:08.571208 IP6 ::1.50327 > ::1.8080: Flags [.], ack 158, win 6362, options [nop,nop,TS val 4277962519 ecr 1002509801], length 0\n\n配合wireshark使用\n介绍\n\nwireshark 是一个完全开源的项目,出身也比较早,九十年代的产物,核心思想就是利用pcap去解析数据包,并支持强大的过滤语法,源码: https://gitlab.com/wireshark/wireshark, 文档: https://www.wireshark.org/docs/man-pages/wireshark-filter.html\n\n其实wireshark 提供了很多cli,帮助dump数据,具体可以看: https://www.wireshark.org/docs/man-pages/\n\n只需要tcpdump -w [filename] 就OK了,是的还可以 tcpdump -r [filename] 也可以读的!\n\n\n➜ tcpdump -i lo0 'port 8080' -w ~/data/tcpdump.pcaptcpdump: listening on lo0, link-type NULL (BSD loopback), capture size 262144 bytes\n\n\n读取文件(一般抓包文件的文件后缀是 .pcap) , 或者直接监听网卡 \n\n5. 使用 flow 可以更好的查看连接上的请求!\n\n使用 gtool tcpdump\n介绍: https://github.com/Anthony-Dong/go-sdk/blob/master/gtool/tcpdump\n\n\n本项目只是为了了解 pcap+流量解析,虽然wireshark是一个非常好的GUI工具,但是线上并不能直接使用,其次就是使用 tcpdump大部分可以满足能力!所以我工具并没有造轮子,更多的是在线解析业务流量,关注于解析应用层流量!\n\n\n支持解析TCP的包\n支持解析HTTP包(HTTP/1.1 & HTTP/1.0),支持自动根据content-encoding类型进行解析!\n支持解析Thrift包,支持多种协议,包含Kitex的TTHeader 和 Thrift 官方协议(Framed、THeader、Unframed)!\n支持解析大包(粘包问题)\n支持tcpdump在线/离线流量解析\n\n➜ gtool tcpdump -hName: decode tcpdump file, help doc: https://github.com/Anthony-Dong/go-sdk/tree/master/gtool/tcpdumpUsage: gtool tcpdump [-r file] [-v] [-X] [--max dump size] [flags]Examples:\t1. step1: tcpdump 'port 8080' -w ~/data/tcpdump.pcap\t step2: gtool tcpdump -r ~/data/tcpdump.pcap\t2. tcpdump 'port 8080' -X -l -n | gtool tcpdumpOptions: -X, --dump Enable Display payload details with hexdump. -r, --file string The packets file, eg: tcpdump_xxx_file.pcap. -h, --help help for tcpdump --max int The hexdump max size -v, --verbose Enable Display decoded details.Global Options: --config-file string set the config file (default "/Users/bytedance/.gtool.yaml") --log-level string set the log level in "fatal|error|warn|info|debug" (default "debug")To get more help with gtool, check out our guides at https://github.com/Anthony-Dong/go-sdk\n\n\n使用 (注意管道符读取,需要-X输出,因为其他会丢失原数据)\n\nfanhaodong.516:go-sdk/ $ sudo tcpdump -i eth0 'port 8888' -l -n -X | bin/gtool tcpdump [14:08:10]tcpdump: verbose output suppressed, use -v or -vv for full protocol decodelistening on eth0, link-type EN10MB (Ethernet), capture size 262144 bytes14:08:51.426622 IP 10.225.xx.196.28284 > 10.248.xx.215.8888: Flags [S], seq 174152772, win 28200, options [mss 1410,sackOK,TS val 445695352 ecr 0,nop,wscale 10], length 014:08:51.426726 IP 10.248.xx.215.8888 > 10.225.xx.196.28284: Flags [S.], seq 2533803252, ack 174152773, win 28960, options [mss 1460,sackOK,TS val 2106570914 ecr 445695352,nop,wscale 10], length 014:08:51.430699 IP 10.225.xx.196.28284 > 10.248.xxx.215.8888: Flags [.], ack 1, win 28, options [nop,nop,TS val 445695357 ecr 2106570914], length 014:08:51.430790 IP 10.225.xx.196.28284 > 10.248.xxx.215.8888: Flags [P.], seq 1:987, ack 1, win 28, options [nop,nop,TS val 445695357 ecr 2106570914], length 986{ "method": "SimpleTestRPC", "seq_id": 324, "protocol": "UnframedBinary", "message_type": "call", "payload": { "1_STRUCT": { "255_STRUCT": { "1_STRING": "111", "2_STRING": "xxxx.xxx.xx", "3_STRING": "10.xxx.xxx.xxx", "4_STRING": "", "6_MAP": { "cluster": "test", "env": "prod", "idc": "xxx", "tracestate": "_sr=1", "user_extra": "" } }, "1_STRING": "hello world" } }, "meta_info": {}}14:08:51.430803 IP 10.248.xxx.215.8888 > 10.225.xxx.196.28284: Flags [.], ack 987, win 32, options [nop,nop,TS val 2106570918 ecr 445695357], length 014:08:51.432384 IP 10.248.xxx.215.8888 > 10.225.xxx.196.28284: Flags [P.], seq 1:890, ack 987, win 32, options [nop,nop,TS val 2106570919 ecr 445695357], length 889{ "method": "SimpleTestRPC", "seq_id": 324, "protocol": "UnframedBinary", "message_type": "reply", "payload": { "0_STRUCT": { "1_STRING": "hello world", "2_STRING": "hello, world!", "255_STRUCT": { "2_I32": 0, "1_STRING": "", "3_MAP": { "_CUSTOM_CLUSTER": "default", "_CUSTOM_ENV": "prod", "_CUSTOM_IDC": "xxx", "_CUSTOM_IP": "10.xxx.xx.xx", "_CUSTOM_IP_V4": "xxx.xxx.xxx.xxx", "_CUSTOM_IP_V6": "xxxx", } } } }, "meta_info": {}}14:08:51.436438 IP 10.225.xx.196.28284 > 10.248.xxx.215.8888: Flags [.], ack 890, win 31, options [nop,nop,TS val 445695362 ecr 2106570919], length 014:08:51.637336 IP 10.225.xx.196.28284 > 10.248.xx.215.8888: Flags [F.], seq 987, ack 890, win 31, options [nop,nop,TS val 445695563 ecr 2106570919], length 014:08:51.637552 IP 10.248.xx.215.8888 > 10.225.xx.196.28284: Flags [F.], seq 890, ack 988, win 32, options [nop,nop,TS val 2106571125 ecr 445695563], length 014:08:51.641670 IP 10.225.xx.196.28284 > 10.248.xxx.215.8888: Flags [.], ack 891, win 31, options [nop,nop,TS val 445695568 ecr 2106571125], length 0","categories":["Linux"],"tags":["Linux命令"]},{"title":"Mac 个人开发环境搭建","url":"/2021/08/24/a5ccc7a0e58afc7d2f946516f3e32dd0/","content":" 个人的Mac环境,主要是解决一些更换电脑时需要重新搞一些东西,以及分享一下个人mac的配置,以及一些命令的推荐!\n\n\n安装常用命令brew和 on-my-zsh# 安装 brew (仅mac)sh -c "$(curl -fsSL https://anthony-wangpan.oss-accelerate.aliyuncs.com/software/2021/6-17/brew.sh)"# 安装zsh, 可以先检查下有没有 zsh --version ,mac默认是自带的zsh --version# https://github.com/ohmyzsh/ohmyzshsodu apt-get install zsh# 安装on-myzshsh -c "$(curl -fsSL https://anthony-wangpan.oss-accelerate.aliyuncs.com/software/2021/6-17/zsh.sh)"# 设置默认chsh -s $(which zsh)\n\n解决brew Updating 慢的问题我们是国内用户,在没有好的VPN的情况下,访问外网那真的叫做一个糟心!幸好有阿里巴巴这种公司做镜像!\n参考自: https://blog.csdn.net/weixin_43318367/article/details/111411462\n# 1. 替换 brew.git 仓库地址cd "$(brew --repo)"git remote set-url origin https://mirrors.aliyun.com/homebrew/brew.git# 2. 替换 homebrew-core.git 仓库地址cd "$(brew --repo)/Library/Taps/homebrew/homebrew-core"git remote set-url origin https://mirrors.aliyun.com/homebrew/homebrew-core.git# 3. 替换 homebrew-bottles 访问地址echo 'export HOMEBREW_BOTTLE_DOMAIN=https://mirrors.aliyun.com/homebrew/homebrew-bottles' >> ~/.zshrcsource ~/.zshrc\n\non-my-zsh 插件\n zsh-autosuggestions 命令记录/推荐\n\n\n# 1. 下载插件git clone --depth 1 https://github.com/zsh-users/zsh-autosuggestions.git $ZSH_CUSTOM/plugins/zsh-autosuggestions# 2. 配置插件 vim ~/.zshrcplugins=(\tgit\tzsh-autosuggestions)\n\n\nzsh-syntax-highlighting 高亮\n\ngit clone --depth 1 https://github.com/zsh-users/zsh-syntax-highlighting.git $ZSH_CUSTOM/plugins/zsh-syntax-highlighting\n\n\n\nhttps://github.com/zsh-users/zsh-autosuggestions/issues/238 解析 ctrl c+v 墨迹的问题,放到 ~/.zshrc 里面就行\n\n# This speeds up pasting w/ autosuggest# https://github.com/zsh-users/zsh-autosuggestions/issues/238pasteinit() { OLD_SELF_INSERT=${${(s.:.)widgets[self-insert]}[2,3]} zle -N self-insert url-quote-magic # I wonder if you'd need `.url-quote-magic`?}pastefinish() { zle -N self-insert $OLD_SELF_INSERT}zstyle :bracketed-paste-magic paste-init pasteinitzstyle :bracketed-paste-magic paste-finish pastefinish\n\n\n关于 agnoster 乱码问题: 自行百度解决,解决方案都不太好,建议别用,默认挺好!\n\n安装GNU命令行 (仅Mac)主要是解决,我们Linux用户的痛点,发现很多命令macos使用方式不一样!\n# 备份下cp ~/.zshrc ~/.zshrc_copy# install grep sed awk gtarbrew install grepbrew install gnu-sedbrew install gawkbrew install gnu-tar# install coreutils, eg: ls,cd,nc, ... brew install coreutils# install find utils, eg: find, xargs ..brew install findutils# install mysql-clientbrew install mysql-client\n\n环境变量配置:\n# aliasalias awk=gawk# export gnu binexport PATH="/usr/local/opt/gnu-sed/libexec/gnubin:$PATH"export PATH="/usr/local/opt/gnu-tar/libexec/gnubin:$PATH"export PATH="/usr/local/opt/grep/libexec/gnubin:$PATH"export PATH="/usr/local/opt/coreutils/libexec/gnubin:$PATH"export PATH="/usr/local/opt/findutils/libexec/gnubin:$PATH"# export softwareexport PATH="/usr/local/opt/mysql-client/bin:$PATH"\n\n系统工具安装\nbrew install htop\n\n安装语言环境\n安装GO语言,文档: gvm\n\n## 安装gvmbash < <(curl -s -S -L https://raw.githubusercontent.com/moovweb/gvm/master/binscripts/gvm-installer)## 注意: 配置一下这个, gvm会重写GOPATH.source ${HOME}/.gvm/scripts/gvmexport GOPATH=$HOME/go## 下载和使用gvm install go1.17## 默认使用 go1.17gvm use go1.17 --default\n\n\n安装openjdk\n\nbrew install openjdk@8\n\n\n安装其他语言\n\n# 配置python 使用 python3alias python=python3\n\n\n安装vscode\n\n\n一定要配置这个,可以做到 anywhere code xxxx !!!\n\n\nls 高亮alias 下 alias ls='ls -F --show-control-chars --color=auto'\n个人高频脚本数据同步文件脚本\n这个脚本主要是本地和远程双向同步,远程开发的话直接用 remote-ssh 或者 cloud-ide 即可!\n\n\n记得在 ${SYNC_HOME}/.fileignore 创建文件,主要是申明一些不需要同步的文件目录和文件\n\n*.swp*.pyc*.pyo.git.DS_Store.idea.vscode-upload.jsonoutput*.class*.log*mvnw*target*.iml.m2binnode_modulescmake-build-debugcmake-build-debug-event-trace\n\n\n在${SYNC_HOME}/sync.sh 创建文件,并且copy下面文件\n\n#!/bin/bashset -e########################### 配置开始# 远程服务器的用户名称readonly DEV_USER="xxx.xxx"# 远程服务器的地址readonly DEV_IP="10.xx.xx.xx"# 远程同步目录,推荐用户的根目录,例如可以执行 echo "$HOME" 查看用户根目录readonly REMOTE_HOME="/home/xxx.xxx"# 本地脚本目录readonly SYNC_HOME="/Users/xxx/go/bin/sync-devbox"# 本地同步白名单readonly WHITE_LIST=("/Users/xxx/go/src" "/Users/xxx/data" "/Users/xxx/note")########################### 配置结束LOCAL_HOME="$HOME"function usage() { echo "Usage: $(basename "$0") [dir]" echo "" echo " Support reverse synchronization, support forward synchronization"}# 获取远程文件的文件类型function get_remote_file_type () { local FILE="$1" FILE_TYPE="" if ssh ${DEV_USER}@${DEV_IP} [ -d "$FILE" ] ; then FILE_TYPE="dir" elif ssh ${DEV_USER}@${DEV_IP} [ -f "$FILE" ] ; then FILE_TYPE="file" else echo "> [ERROR] the remote file or directory does not exist: $FILE" >&2 return 1 fi echo "$FILE_TYPE"}# 获取本地文件的文件类型function get_file_type() { local FILE="$1" FILE_TYPE="" if [ -d "${FILE}" ]; then FILE_TYPE="dir" elif [ -f "${FILE}" ]; then FILE_TYPE="file" else echo "> [ERROR] the file or directory does not exist: ${FILE}" >&2 return 1 fi echo "${FILE_TYPE}"}# 检测是否允许传输function is_white_list_file() { local FILE="$1" NOT_PASS_DIR=() for elem in "${WHITE_LIST[@]}"; do if [[ "${FILE}" == ${elem}/* ]]; then break fi NOT_PASS_DIR+=("$elem") done if [ ${#WHITE_LIST[@]} -eq ${#NOT_PASS_DIR[@]} ]; then echo "> [ERROR] the current directory ${FILE} is not under the whitelist: ${NOT_PASS_DIR[*]}" >&2 return 1 fi}# 正向同步 local->remotefunction file_rsync() { FILE=$(realpath "$1") FILE_TYPE=$(get_file_type "$FILE") echo "> file: $FILE" echo "> file type: ${FILE_TYPE}" # 目录同步整个目录,文件同步单个文件 is_white_list_file "$FILE" REMOTE_FILE=${FILE/${LOCAL_HOME}/${REMOTE_HOME}} ssh ${DEV_USER}@${DEV_IP} mkdir -p "$(dirname "$REMOTE_FILE")" echo "> mkdir -p $(dirname "$REMOTE_FILE")" # 这里选择下文件目录 IGNORE_FILE=${SYNC_HOME}/.fileignore if [ "$FILE_TYPE" = "dir" ] && [ -f "${FILE}/.fileignore" ]; then IGNORE_FILE="${FILE}/.fileignore" fi # 如果同步的是文件,则忽略排除文件 rsync_opt=() if [ "$FILE_TYPE" = "dir" ] && [ -f "${IGNORE_FILE}" ]; then rsync_opt+=(--exclude-from="${IGNORE_FILE}") fi # 目录需要特殊处理下 if [ "$FILE_TYPE" = "dir" ]; then FILE="$FILE/" REMOTE_FILE="$REMOTE_FILE/" fi # 用法参考: https://www.ruanyifeng.com/blog/2020/08/rsync.html set -x rsync -avz \\ --delete \\ --progress \\ --log-file="${SYNC_HOME}/sync-devbox.log" \\ --log-file-format="%t %f %b" \\ "${rsync_opt[@]}" \\ "${FILE}" "${DEV_USER}@${DEV_IP}:${REMOTE_FILE}" set +x echo "> rsync ${FILE} -> ${REMOTE_FILE} success!"}# 反向同步 remote->localfunction file_reverse_rsync() { FILE="$1" echo "> remote file: $FILE" LOCAL_FILE=${FILE/${REMOTE_HOME}/${LOCAL_HOME}} echo "> local file: $LOCAL_FILE" is_white_list_file "$LOCAL_FILE" FILE_TYPE=$(get_remote_file_type "$FILE") echo "> remote file type: $FILE_TYPE" # 如果允许反向同步目录,那么注释掉下面这行代码 if [ "$FILE_TYPE" = "dir" ]; then echo "> [ERROR] reverse synchronization only supports synchronization of file types" >&2 ; return 1 ; fi rsync_opt=() if [ -f "${SYNC_HOME}/.fileignore" ]; then rsync_opt+=(--exclude-from="${SYNC_HOME}/.fileignore") fi mkdir -p "$(dirname "$LOCAL_FILE")" set -x rsync -avz \\ --delete \\ --progress \\ --log-file="${SYNC_HOME}/sync-devbox.log" \\ --log-file-format="%t %f %b" \\ "${rsync_opt[@]}" \\ "${DEV_USER}@${DEV_IP}:${FILE}" "${LOCAL_FILE}" set +x echo "> rsync ${FILE} -> ${LOCAL_FILE} success!"}FILE="$1"if [ -z "$FILE" ]; then FILE=$(pwd)fi# usageif [ "$1" = '--help' ] || [ "$1" = '-help' ] || [ "$1" = 'help' ] || [ "$1" = '-h' ] ; then usage ; exit 1; fi# 初始化sync脚本路径if [ ! -d "$SYNC_HOME" ]; then mkdir -p "$SYNC_HOME" ; fi# 如果在远程目录下那么就表示反向同步if [[ $FILE == ${REMOTE_HOME}/* ]]; then REVERSE_RSYNC="true"; echo "> reverse_rsync: $REVERSE_RSYNC" ; fi# 执行if [ "$REVERSE_RSYNC" = "true" ]; then file_reverse_rsync "$FILE"else file_rsync "$FILE"fi\n\n\n在远程服务器,配置本地服务的公钥,进行免密登陆!\n执行 sync.sh 或者 sync.sh [dir]\n\nmac系统配置\n这个不能过快,不然双击无法选中东西!\nvim 配置本人参考的配置是: https://github.com/amix/vimrc 直接根据文档操作即可 ,个人fork了一个版本: https://github.com/Anthony-Dong/vimrc/tree/dev \n\n基本操作\n\n\ncontrol + f 向下翻一页 forward\ncontrol + b 向上翻一页 back forward\ncontrol + d 向下翻半页 down\ncontrol + u 向下翻半页 up\nctrl + e: 向下滚动一行\nctrl + y: 向上滚动一行\nline number + gg: 掉转到指定行,从1开始\n/{xxx}: 查找文件内容, shift+n 查找下一个\n\n\n操作,基本就是视图模式、基本模式、编辑模式、命令模式,大概4个模式,其中视图模式需要按v进入\n\n其他基本操作 d删除(Delete),c剪切(Cut),y复制(Yank),p粘贴(Paste),但是一般也兼容 (command+c/command+v)\n\nvim的快捷键\n\n\nmap (递归map, nmap表示非递归)# leader是一个全局的快捷键,例如 \\\\# <leader>nn 含义就是 \\\\nn那么它等价于 :NERDTreeToggle<cr>map <leader>nn :NERDTreeToggle<cr>\n\n\nNERDTree 文件树插件\n\n\n个人喜欢 ctrl + t 打开 NERDTreeToggle \nctrl + w + w: 其实就是来回切换 目录树/编辑区 的光标\no: 打开目录 (或者 enter 也可以)\nx: 关闭目录\nr: 刷新目录\n\n\n\n\nCtrl-P 搜索文件插件\n\n\n个人喜欢 ctrl+p 打开 CtrlP\nctrl+c 取消搜索\n\n\n\n\nGit-Fugitive Git插件\n\n\nGvdiffsplit 可以快速看到diff\n\n\n\npostman 低版本下载\npostman新版本太慢了,而且ui太复杂,所以我个人比较喜欢旧的版本,我个人使用软件的原则是能不升级则不升级!\n\nRelease Notes: https://www.postman.com/downloads/release-notes/\n例如我要下载 v7.36.7 版本的postman ,平台是macos,可以打开 https://dl.pstmn.io/download/version/7.36.7/osx 进行下载\n\n注意:{version} 需要把 v7.36.7 中的 v去除掉\n\n\n\n\n平台\n下载地址\n\n\n\nWindows64位\nhttps://dl.pstmn.io/download/version/{version}/win64\n\n\nWindows32位\nhttps://dl.pstmn.io/download/version/{version}/win32\n\n\nMac\nhttps://dl.pstmn.io/download/version/{version}/osx\n\n\nLinux\nhttps://dl.pstmn.io/download/version/{version}/linux\n\n\n 不过最新版本,mac-os由于有arm芯片所以:\n\narm : https://dl.pstmn.io/download/latest/osx_arm64\nintel : https://dl.pstmn.io/download/latest/osx_64\n\nidea / goland / clion 配置优化索引缓存\n找到安装位置 打开\n\n\n\n\n修改 GoLand.app/Contents/bin/idea.properties 文件,主要作用就是防止电脑重启缓存失效\n\n#---------------------------------------------------------------------# Uncomment this option if you want to customize a path to the caches directory.#---------------------------------------------------------------------idea.system.path=/Users/bytedance/.GoLand/system\n\n\n修改GoLand.app/Contents/bin/goland.vmoptions 配置idea的内存,配置最大内存为8G\n\n-Xms1024m-Xmx8192m\n\nclion/idea 都是同理!\n","categories":["Linux"],"tags":["mac"]},{"title":"Makefile学习","url":"/2021/03/20/a9ec632af8ce6689c201007d3b97b0df/","content":" Makefile 在开源项目中还是相当的常见的,熟悉他的基本语法,还是很有必要的,其次是Makefile相对于shell脚本的优点就是他的关联性,和前置条件等都很好的解决的构建链条的问题。有些学c/cpp的同学可能比较熟悉,我们这个核心不关注于这个,主要是使用在日常中\n\n\nmake 一些cli参数\n-n 参数:\n\n 使用 -n 参数,让 make 命令输出将要执行的操作步骤,而不是真正执行这些操作;\n➜ makefile git:(master) ✗ touch Makefile2 ➜ makefile git:(master) ✗ make -n rm -f Makefile1 Makefile2 Makefile3➜ makefile git:(master) ✗ ls Makefile Makefile1 Makefile2\n\n\n-f 参数:\n\n 使用 -f 参数,后面可以接一个文件名,用于指定一个文件作为 makefile 文件。如果没有使用 -f 选项,则 make 命令会在当前目录下查找名为 makefile 的文件,如果该文件不存在,则查找名为 Makefile 的文件。 \n\n-C 参数\n 一般当我们调用其他目录的makefile,可以直接 make -C <dir> 执行完退回当前make命令,类似于shell\n\ninclude 可以引用用其他的makefile,类似于其他编程语言的import,和环境变量 MAKEFILES 等效\n\n\nMakefile文件\ninclude a.make b.makeall: echoa echob\t@echo hello\n\na.make 文件\nechoa:\t@echo hello a\t\n\nb.make 文件\nechob:\t@echo hello b\n\n执行\n1) 可以发现include却是是把它完完全全的copy到了头部\n➜ makefile git:(master) ✗ makehello a\n\n2)继续,完全符合\n➜ makefile git:(master) ✗ make allhello ahello bhello\n\nmakefile一些环境变量MAKE\n 其实就是你的make环境变量的,which make 即可\n\n.PHONY: allall:\t@echo "make路径: $(MAKE)"\n\n输出\n➜ makefile git:(master) ✗ makemake路径: /Library/Developer/CommandLineTools/usr/bin/make\n\nRM\n 这个主要是当作 rm -f 参数\n\n.PHONY: allclean:\t$(RM) Makefile1 Makefile2 Makefile3\n\n输出:\n➜ makefile git:(master) ✗ makerm -f Makefile1 Makefile2 Makefile3\n\nMAKEFILE_LIST\n MAKEFILE_LIST的变量, 它是个列表变量, 在每次make读入一个make文件时, 都把它添加到最后一项,gnu make 有效。\n\n\nMakefile 文件\n\nall:\t@echo "当前makefile: $(MAKEFILE_LIST)"\t@$(MAKE) -f Makefile2\n\n\nMakefile 文件2\n\nall:\t@echo "当前makefile: $(MAKEFILE_LIST)"\n\n输出\n➜ makefile git:(master) ✗ make当前makefile: Makefile当前makefile: Makefile2\n\n所以依靠这个可以获取当前路径,但是目前没有模拟出 MAKEFILE_LIST 多个列表\n.PHONY:first:\t@echo $(MAKEFILE_LIST)second:\t@echo $(lastword $(MAKEFILE_LIST))third:\t@echo $(realpath $(lastword $(MAKEFILE_LIST)))latest: first second third\t@echo $(shell dirname $(realpath $(lastword $(MAKEFILE_LIST))))\n\n执行\n➜ go-source git:(master) ✗ make latest MakefileMakefile/Users/fanhaodong/go/code/go-source/Makefile/Users/fanhaodong/go/code/go-source\n\nmakefile 文件书写规则 makefile 文件由一组依赖关系和规则构成。每个依赖关系都由一个目标(即将要创建的文件)和一个该目标所依赖的源文件组成;规则描述了如何通过这些依赖文件创建目标。简单的来说,makefile 文件的写法如下:\ntarget: prerequisites command1 command2 ...\n\n 其中,target 是即将要创建的目标(通常是一个可执行文件),target 后面紧跟一个冒号,prerequisite 是生成该目标所需要的源文件(依赖),一个目标所依赖的文件可以有多个,依赖文件与目标之间以及各依赖文件之间用空格或制表符 Tab 隔开,这些元素组成了一个依赖关系。随后的命令 command 就是规则,也就是 make 需要执行的命令,它可以是任意的 shell 命令。另外,makefile 文件中,注释以 # 号开头,一直延续到该行的结束。\n比如下面这个,target就是hello, prerequisite是 hello.c的文件\nhello: hello.c\t$(CC) -o hello.s -S hello.c\t$(CC) -o hello.o -c hello.s\t$(CC) -o hello hello.o\n\n\n构建c项目all: test test: test.o anotherTest.o gcc -Wall test.o anotherTest.o -o testtest.o: test.c gcc -c -Wall test.c anotherTest.o: anotherTest.c gcc -c -Wall anotherTest.c clean: rm -rf *.o test\n\nGNU的make工作时的执行步骤如下:\n\n读入所有的Makefile。\n读入被include的其它Makefile。\n初始化文件中的变量。\n推导隐晦规则,并分析所有规则。\n为所有的目标文件创建依赖关系链。\n根据依赖关系,决定哪些目标要重新生成。\n执行生成命令。\n\n1-5步为第一个阶段,6-7为第二个阶段。第一个阶段中,如果定义的变量被使用了,那么,make会把其展开在使用的位置。但make并不会完全马上展开,make使用的是拖延战术,如果变量出现在依赖关系的规则中,那么仅当这条依赖被决定要使用了,变量才会在其内部展开。\n当然,这个工作方式你不一定要清楚,但是知道这个方式你也会对make更为熟悉。有了这个基础,后续部分也就容易看懂了。\n申明变量\n= 类似宏一样,他会对变量进行引用,在执行时扩展,允许递归扩展\n:= 如果变量申明符合先来后到,和 =含义一样,但是如果 申明a引用了b但是b还没有申明,此时认为b为空\n\na = $(b) + 1b = 2c := $(d) + 1d = 2all:\t@echo $(a)\t@echo $(c)\n\n输出\n➜ makefile git:(master) ✗ make2 + 1+ 1\n奇怪的现象: 可以发现我们申明a变量后,但是输出的时候却是 100 ,可以发现cli传递的优先级最高,不可以被覆盖\n➜ makefile git:(master) ✗ make a=100 100+ 1\n\n\n\n?= 如果a变量前面已经申明过了,那么后面 a ?= xxx 则因为前面已经申明了a,所以不进行赋值,也就是 a?=xxxx无效,如果前面没有申明则有效\n\nA = helloA ?= hello worldall:\t@echo $(A)\n\n输出:hello\n\n+= 这个类似于 a+=1 , 意思就是在原来的基础上 += ,很方便,下面提供demo\n\nbuild_args := -raceifeq ($(vendor),true)\tbuild_args += -mod=vendorendifall:\t@echo $(build_args)\n\n输出:\n➜ makefile git:(master) ✗ make vendor=true-race -mod=vendor\n\n命令行参数echo:\t@echo $(arg)\n\n执行:\n➜ makefile git:(master) ✗ make arg=ruoyuruoyu\n\n执行函数1、call + define 宏定义类似于C语言的宏定义\n# 编译生成到bin目录下define build sh ./build.sh $(1) ./bin/$(strip $(2))endef# 脚手架脚本go-build: pre\t$(call build, cmd/go-build/main.go, go-build)\n\n2、自带函数\n 格式 $(<命令> <参数>)\n\nall:\t@echo $(lastword 1 2 3)\n\n输出\n➜ makefile git:(master) ✗ make3\n\n3、调用shell函数all:\t@echo $(shell dirname /data/test)\n\n执行\n➜ makefile git:(master) ✗ make/data\n\nMakefile文件的语法<target> : <prerequisites> [tab] <commands>\n\n\ntarget: 目标,支持模式匹配\nprerequisites:前置条件,可以有多个,支持模式匹配\ncommands: 前面必须有 tab ,是shell命令/makefile函数命令\n\n1、注释 注释一般使用 # 开头表示,但是如果注释在目标的命令包含\n# 一般all定义了全部all:\t#hello\n\n执行\n➜ makefile git:(master) ✗ make#hello\n\n2、关闭回声这个其实很简单,就是在执行shell命令的时候,往往会打印日志,所以这里提供了很好的解决方式,使用 @ 符号\nall:\techo "hello world"\n\n执行后会发现,每次执行的时候都会打印回声\n➜ makefile git:(master) ✗ makeecho "hello world"hello world\n\n所以可以将makefile文件改成以下\nall:\t@echo "hello world"\n\n输出\n➜ makefile git:(master) ✗ makehello world\n\n3、通配符 和bash一样,主要有 * 等通配符,主要是在 shell脚本中使用\nnew:\tfor x in {1,2,3,4};do touch $$x.test ;doneclean:\t$(RM) *.test\n\n执行\n➜ makefile git:(master) ✗ make new for x in {1,2,3,4};do touch $x.test ;done➜ makefile git:(master) ✗ ls | grep test1.test2.test3.test4.test➜ makefile git:(master) ✗ make clean rm -f *.test➜ makefile git:(master) ✗ ls | grep test\n\n4、模式匹配主要是对文件名的支持!主要是在 目标和依赖中使用, 使用匹配符%,可以将大量同类型的文件,只用一条规则就完成构建。\n%.o: %.c\n\n等同于\nf1.o: f1.cf2.o: f2.c\n\n不懂的可以看一下这篇文章,对比一下 模式匹配和通配符的区别 : https://blog.csdn.net/BobYuan888/article/details/88640923\n理解模式匹配必须了解下面这四个\n$@:目标的名字\n$^:构造所需文件列表所有所有文件的名字\n$<:构造所需文件列表的第一个文件的名字\n$?:构造所需文件列表中更新过的文件\n大致原理:\n\n我要找f1.o的构造规则,看看Makefile中那个规则符合。\n然后找到了%.o:%.c\n来套一下来套一下\n %.o 和我要找的 f1.o 匹配\n套上了,得到%=f1。\n所以在后面的%.c就表示f1.c了。\nOK进行构造\n\n1、例子一(编译c文件)\n%.o: %.c %.h\t@echo "目标的名字: $@, 依赖的第一个文件: $< , 依赖的全部文件: $^, 所更新的文件: $?"\t$(CC) -o $@ -c $<all: utils.o\t@echo "编译。。。"\tclean:\t$(RM) *.i *.s *.o main\n\n执行,可以看到完全符合我们的例子\n目标的名字: utils.o, 依赖的第一个文件: utils.c , 依赖的全部文件: utils.c utils.h, 所更新的文件: utils.c utils.hcc -o utils.o -c utils.c编译。。。\n\nfor循环1、makefile: foreach循环语法: $(foreach <var>, $(g_var), <command1>;<command2>) , 这里需要变量引用需要使用 $()\nlist := $(shell ls)all:\t@$(foreach item,$(list),\\\t\techo $(item);\\\t\techo $(realpath $(item));\\\t\techo "====================";\\\t)\n\n输出:\n➜ makefile git:(master) ✗ makeMakefile/Users/fanhaodong/note/note/demo/makefile/Makefile====================Makefile1/Users/fanhaodong/note/note/demo/makefile/Makefile1====================Makefile2/Users/fanhaodong/note/note/demo/makefile/Makefile2====================\n\n3、shell:for 循环list := $(shell ls)all:\t@for x in $(list); do\\\t\techo $$x;\\\tdone\n\n记住一点就好, $ 符号转移需要使用 $$\n执行\n➜ makefile git:(master) ✗ make mfor MakefileMakefile1Makefile2a.makeb.make\n\nif 函数1、makefile: if 函数命令格式: $(if <condition>, <yes do1>;<yes do2>, <no do1>;<no do2>)\nall:\t@$(if $(shell command -v $(arg)),echo command $(arg) is exist,echo command $(arg) is not exist)\n\n执行\n➜ makefile git:(master) ✗ make arg=gocommand go is exist➜ makefile git:(master) ✗ make arg=go1command go1 is not exist\n\n2、shell: if 函数all:\t@if [ `command -v $(arg)` ];then\\\t\techo "command [$(arg)] is exist";\\\telse \\\t\techo "command [$(arg)] is not exist";\\\tfi\n\n执行\n➜ makefile git:(master) ✗ make arg=gocommand [go] is exist➜ makefile git:(master) ✗ make arg=go1command [go1] is not exist\n\n执行多个命令echo:\t@echo hello worldecho2:\t@echo hello world 2\t\n\n执行:\n➜ makefile git:(master) ✗ make echo echo2hello worldhello world 2\n\n宏定义define echo\techo "hello, $(1)!"endefARG := ifdef arg\tARG := $(arg)else\tARG := NULLendif\tall: print\t@$(call echo,"world")\t@echo $(ARG)print:\t@echo "arg: $(arg)"\n\n执行\n➜ makefile git:(master) ✗ make arg=worldarg: worldhello, world!world\n\n系统环境变量申明推荐: export <变量名称> , 获取使用 ${<变量名称>}\nGOPROXY := https://goproxy.cn,directexport GOPROXYall:\t@echo ${GOPROXY}\n\n编译C项目c项目往往很复杂,设计到 预编译,编译,汇编,链接 的过程\n\n1、文件 (头文件、main文件)1、utils.h\n#ifndef _ADD_H_#define _ADD_H_int add (int a,int b);#endif\n\n2、utils.c\nint add(int x ,int y){ return x+y;}\n\n3、main.c\n注意:头文件的寻找方式\n\n先搜索当前目录\n然后搜索-I指定的目录,例如 -I ./head\n再搜索gcc的环境变量CPLUS_INCLUDE_PATH(C程序使用的是C_INCLUDE_PATH)\n最后搜索gcc的内定目录\n\n#include <stdio.h>#include "utils.h"int main(int argc, char const *argv[]){ printf("1+2 = %d\\n",add(1,2)); return 0;}\n\n假如 .h 文件放在 head 目录\n➜ cpp git:(master) ✗ ls head utils.h# 可以发现编译异常,异常时 .h文件未找到➜ cpp git:(master) ✗ gcc -c main.c -o main.omain.c:2:10: fatal error: 'utils.h' file not found#include "utils.h" ^~~~~~~~~1 error generated.# 修改 -I 参数可以发现通过➜ cpp git:(master) ✗ gcc -I ./head -c main.c -o main.o➜ cpp git:(master) ✗ ls | grep main.omain.o\n\n2、预编译 -E-E:预编译,这一步主要是将头文件,宏定义展开到文件,是文本形式\n➜ cpp git:(master) ✗ gcc -E main.c -o main.i➜ cpp git:(master) ✗ tail -f 10 main.i tail: 10: No such file or directory==> main.i <==### 可以看到这里是把 utils.h 的头文件信息 copy 过来了int add (int a,int b);# 3 "main.c" 2int main(int argc, char const *argv[]){ printf("1+2 = %d\\n",add(1,2)); return 0;}\n\n3、编译 -S\n编译为汇编代码,是文本形式\n\n➜ cpp git:(master) ✗ gcc -S main.i -o main.s\n\n\n4、汇编 -c\n 就是编译成二进制的汇编文件,是可重定位目标程序,属于二进制文件\n\n➜ cpp git:(master) ✗ gcc -c main.s -o main.o➜ cpp git:(master) ✗ hexdump -C main.o00000000 cf fa ed fe 07 00 00 01 03 00 00 00 01 00 00 00 |................|00000010 04 00 00 00 08 02 00 00 00 20 00 00 00 00 00 00 |......... ......|00000020 19 00 00 00 88 01 00 00 00 00 00 00 00 00 00 00 |................|00000030 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|00000040 b0 00 00 00 00 00 00 00 28 02 00 00 00 00 00 00 |........(.......|00000050 b0 00 00 00 00 00 00 00 07 00 00 00 07 00 00 00 |................|00000060 04 00 00 00 00 00 00 00 5f 5f 74 65 78 74 00 00 |........__text..|00000070 00 00 00 00 00 00 00 00 5f 5f 54 45 58 54 00 00 |........__TEXT..|00000080 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|00000090 42 00 00 00 00 00 00 00 28 02 00 00 04 00 00 00 |B.......(.......|000000a0 d8 02 00 00 03 00 00 00 00 04 00 80 00 00 00 00 |................|➜ cpp git:(master) ✗ objdump -d main.omain.o: file format Mach-O 64-bit x86-64Disassembly of section __TEXT,__text:0000000000000000 _main: 0: 55 pushq %rbp 1: 48 89 e5 movq %rsp, %rbp 4: 48 83 ec 20 subq $32, %rsp 8: c7 45 fc 00 00 00 00 movl $0, -4(%rbp) f: 89 7d f8 movl %edi, -8(%rbp) 12: 48 89 75 f0 movq %rsi, -16(%rbp) 16: bf 01 00 00 00 movl $1, %edi 1b: be 02 00 00 00 movl $2, %esi 20: e8 00 00 00 00 callq 0 <_main+0x25> 25: 48 8d 3d 16 00 00 00 leaq 22(%rip), %rdi 2c: 89 c6 movl %eax, %esi 2e: b0 00 movb $0, %al 30: e8 00 00 00 00 callq 0 <_main+0x35> 35: 31 c9 xorl %ecx, %ecx 37: 89 45 ec movl %eax, -20(%rbp) 3a: 89 c8 movl %ecx, %eax 3c: 48 83 c4 20 addq $32, %rsp 40: 5d popq %rbp 41: c3 retq\n\n5、链接对于c/cpp语言来说,最难的就是链接了!这里也设计到隐晦规则了,首先 .o 是符合 main.o, utils.o的,所以会执行 两次 cc,最终链接成功\n# 伪目标,这里定义的目标不会去文件系统里寻找.PHONY: all clean# CC 属于makefile的全局变量,已经定义好了,但是我们使用gcc需要指定CC := gcc# $@ 目前的目标项目名称 也就是 %.o# $< 目前的依赖项目%.o: %.c\t$(CC) -c $< -o $@all: install run clean# 当依赖符合模式匹配时候,会执行上面的 %.o: %.cinstall: utils.o main.o\tgcc -o main utils.o main.orun:\t./mainclean:\t$(RM) *.i *.s *.o main\n\n执行\n➜ cpp git:(master) ✗ makegcc -c utils.c -o utils.ogcc -c main.c -o main.ogcc -o main utils.o main.o./main1+2 = 3rm -f *.i *.s *.o main\n\n帮助如果你想写help,可以使用下面那个表达式\n.PHONY: helpecho: ## 打印echo\t@echo "hello"all: ## 打印echo1help: ## 帮助\t@awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {sub("\\\\\\\\n",sprintf("\\n%22c"," "), $$2);printf " \\033[36m%-20s\\033[0m %s\\n", $$1, $$2}' $(MAKEFILE_LIST)\n\n其实很简单,了解 awk 语法的话,知道 awk '条件 动作' 文件名 所谓条件就是正则表达式,分隔符是:.*?## ,然后匹配的条件是以 字母开头的\n[root@19096dee708b data]# cat demo.txt11 2211122 33\n\n匹配一下·\n[root@19096dee708b data]# awk '{printf "$1=%s $2=%s\\n",$1,$2}' demo.txt$1=11 $2=22$1=111 $2=$1=22 $2=33\n\n我们要拿到我们的结果!所以需要匹配有空格的,匹配空格就是 \\s\n[root@19096dee708b data]# awk '/\\s/ {printf "$1=%s $2=%s\\n",$1,$2}' demo.txt$1=11 $2=22$1=22 $2=33","categories":["Linux"],"tags":["Makefile"]},{"title":"C++20协程原理和Asio的使用介绍","url":"/2024/04/12/b903df19afba47750da320da14459b29/","content":"协程和异步的关系是什么?C++20协程的原理是什么?如何使用Asio开发高性能网络框架!\n\n\n开发环境\nllvm14\nc++20\n\n协程和异步网络开发中异步的性能往往会远远高于同步,异步的好处在于当事件真正触发的时候才会去执行对应逻辑,因此可以在消耗较少的系统资源下就可以完成非常高的并发!主要原因还是网络开发中大部分开销都在网络IO上!\ncoroutine(协程) 主要解决的就是异步的问题,异步代码最大的问题就是回掉,当你逻辑复杂点,简直的回掉地狱(如果你代码抽象的不好的话),例如envoy就是采用的异步(libevent)去实现的!coroutine为啥能解决异步的问题呢?coroutine本质上是一个可以暂停(suspend)和恢复(resumed)的函数,异步本质上也是因为当我们创建一个异步任务的时候那么实际上就是一个函数暂停的操作,当真正事件触发的时候才会恢复执行你的回掉函数!\ncoroutine 是在语言层面支持了函数的暂停和恢复,要实现只能对应的语言、编译器去支持,例如 c++20、javascript、c# 、rust 这几种就是在语言层面支持了函数的暂停和恢复,具体怎么实现的大家应该都能理解,就是将函数在暂停点进行拆分,从而一个函数被拆分成为多个函数,当事件触发的时候执行暂停点之后的函数即可!所以它本质上和异步并无差异,只不过可读性会高很多,你可以理解为是语法糖,所以我们在实现coroutine的时候也需要实现一个调度器!在C++中调度器比较出名的就 libevent 、asio吧(跨平台能力会比较好),他们都支持计时器、网络IO等事件调度!\n不清楚大家用过Go有没有,Go语言是我觉得对于协程设计最完美的语言!它太简单了,简单到任何人都可以快速的实现一个高并发服务器!这就是协程的魅力,在多线程时代你要实现高并发服务器,太难了,多线程高并发基于都是基于异步去做的!很多人认为Go的协程是Fiber(纤程),其实概念这个东西无可厚非,程序员都比较务实只要快就行了!\n基于 coroutine 自己实现一个 timer 调度器官方文档: https://en.cppreference.com/w/cpp/language/coroutines\n在C++ coroutine 设计中,一个函数是协程要求需要返回一个coroutine对象,coroutine对象要求内部有一个promise_type类型,promise_type需要实现几个钩子函数来控制coroutine对象的,下面例子的async实际上就是coroutine对象。我们在协程中可以调用 co_await一个 awaitable 对象,awaitable对象可以操作coroutine_handle进而实现恢复操作,暂停操作是通过勾子函数实现的!其次main函数不允许返回coroutine对象,因此main函数不能是一个coroutine,也就不能调用co_await等关键词了!\n下面这个例子你可以更好的了解c++ coroutine的设计原理!\n#include "spdlog/spdlog.h"#include <coroutine>#include <iostream>#include <list>// 事件template <typename T>struct event { event() = default; virtual ~event() = default; [[nodiscard]] virtual bool ready() const = 0; [[nodiscard]] virtual T return_value() const = 0;};// 任务,一个可以被恢复执行的任务struct task { task() = default; virtual ~task() = default; [[nodiscard]] virtual bool ready() = 0; virtual void resume() = 0;};// 执行器,执行任务!struct executor { executor() = default; ~executor() = default; void push(task* task) { tasks_.push_back(task); } void run() { std::vector<task*> rm_tasks; while (true) { if (tasks_.empty()) { return; } for (auto const& this_task : tasks_) { if (this_task->ready()) { this_task->resume(); rm_tasks.push_back(this_task); } } for (auto const& rm_task : rm_tasks) { tasks_.remove(rm_task); delete rm_task; } rm_tasks.clear(); std::this_thread::yield(); // 让出cpu调度防止空转 } }private: std::list<task*> tasks_{};};// timer_eventstruct timer_event final : virtual event<std::chrono::time_point<std::chrono::system_clock>> { [[nodiscard]] bool ready() const override { return std::chrono::system_clock::now() >= this->point_; } [[nodiscard]] std::chrono::time_point<std::chrono::system_clock> return_value() const override { return this->point_; } template <typename Rep, typename Period> void reset(const std::chrono::duration<Rep, Period>& sleep) { this->point_ = std::chrono::system_clock::now() + sleep; } template <typename Rep, typename Period> explicit timer_event(const std::chrono::duration<Rep, Period>& sleep) : point_(std::chrono::system_clock::now() + sleep) { }private: std::chrono::time_point<std::chrono::system_clock> point_;};struct simple_task final : task { bool ready() override { return this->is_ready_(); } void resume() override { this->resume_(); } simple_task(const std::function<bool()>& is_ready, const std::function<void()>& resume) : is_ready_(is_ready), resume_(resume) { }private: std::function<bool()> is_ready_; std::function<void()> resume_;};// coroutine 对象struct async { struct promise_type; using coroutine_handle = std::coroutine_handle<promise_type>; struct promise_type { // 1. coroutine 创建的时候因为它没办法直接调用 coroutine 的构造函数,因为开放了一个 coroutine::promise_type::get_return_object 函数来创建coroutine对象!因此 promise_type 的构造函数必须是无参的! async get_return_object() { SPDLOG_INFO("async::promise_type::get_return_object"); return async{coroutine_handle::from_promise(*this)}; } // 2. get_return_object 后会立即调用 initial_suspend 需要返回一个 awaitables 对象 // 2.1 如果返回 std::suspend_never,表示继续执行,即进入coroutine函数体执行 std::suspend_never initial_suspend() { SPDLOG_INFO("async::promise_type::initial_suspend"); return {}; } \t// coroutine 结束 std::suspend_never final_suspend() noexcept { SPDLOG_INFO("async::promise_type::final_suspend"); return {}; } // co_return void 结束 void return_void() { SPDLOG_INFO("async::promise_type::return_void"); } // coroutine 抛出异常 void unhandled_exception() { SPDLOG_INFO("async::promise_type::unhandled_exception"); } // 这个是我们自己实现的函数! void resume() { SPDLOG_INFO("async::promise_type::resume"); const auto handle = coroutine_handle::from_promise(*this); if (!handle) { return; } handle(); } }; template <typename T> struct event_awaiter { [[nodiscard]] auto await_ready() const noexcept { SPDLOG_INFO("async::event_awaiter::await_ready()"); // 在async函数中 // 1. 事件是否准备好 // 2. 没有准备好执行 await_suspend return this->event_.ready(); } [[nodiscard]] auto await_suspend(std::coroutine_handle<promise_type> handler) noexcept { SPDLOG_INFO("async::event_awaiter::await_suspend()"); // 3. 没有准备好,放入 executor_ 的队列中,并且让出当前执行函数,等待调度器恢复 this->executor_.push(new simple_task{[this]() -> bool { return this->event_.ready(); }, [handler]() -> void { handler.promise().resume(); }}); } [[nodiscard]] auto await_resume() const noexcept { SPDLOG_INFO("async::event_awaiter::await_resume()"); // 执行成功,获取返回值 return this->event_.return_value(); } event_awaiter(event<T>& event, executor& executor) : event_(event), executor_(executor) { } private: event<T>& event_; executor& executor_; }; explicit async(const coroutine_handle& handle) : handle_(handle) { }private: coroutine_handle handle_{};};template <typename Rep, typename Period>async sleep(executor& executor, const std::string& name, std::chrono::duration<Rep, Period> sleep_time) { timer_event event(sleep_time); SPDLOG_INFO("[{}] start event", name); co_await async::event_awaiter(event, executor); SPDLOG_INFO("[{}] done event.", name);}int main() { using namespace std::chrono_literals; executor executor{}; sleep(executor, "coroutine-1", 1s); // 会创建一个 coroutine ! sleep(executor, "coroutine-2", 2s); executor.run();}\n\nasio - 异步 写法#include "asio.hpp"#include "spdlog/spdlog.h"int main() { asio::io_context ctx; using namespace std::chrono_literals; // 注册时间 asio::steady_timer timer(ctx, 1s); SPDLOG_INFO("steady_timer start"); timer.async_wait([](auto) { SPDLOG_INFO("steady_timer tigger"); // 当事件触发回回掉此函数 });\t // 启动调度器 ctx.run();}\n\nasio - coroutine 写法#include "asio.hpp"#include "spdlog/spdlog.h"asio::awaitable<void> sleep() { using namespace std::chrono_literals; SPDLOG_INFO("steady_timer start"); asio::steady_timer timer(co_await asio::this_coro::executor, 1s); co_await timer.async_wait(asio::use_awaitable); SPDLOG_INFO("steady_timer tigger");}int main() { asio::io_context ctx; asio::co_spawn(ctx, sleep(), asio::detached); // 把 sleep这个coroutine(任务) 交给 ctx 去调度 ctx.run();}\n\n总结我们可以看到不论是异步还是coroutine,只要封装得当,其实没有啥差异,不过coroutine这种同步写法更适合我们去编程!所以c++26都快出来了,还不用c++20的coroutine吗?\n本文没有降到 coroutine - generator 模型,对于网络开发来说基本上用不到也!\nasio 介绍这个是 asio 官方的示例代码 https://think-async.com/Asio/asio-1.28.0/doc/asio/examples/cpp20_examples.html 可以看下\nasio 提供了丰富的coroutine原语,我们可以基于其实现更上层的业务代码!可以说 asio 就是一门coroutine语言!\ntcp服务asio 整体设计的非常的简单,整体上来看就是创建一个io_context,然后将异步任务(coroutine)绑定到io_context中,然后运行io_context\n#include "asio.hpp"#include "spdlog/spdlog.h"#include "fmt/chrono.h"template <class Rep, class Period>asio::awaitable<void> do_print(const std::string& task_name, std::chrono::duration<Rep, Period> sleep) { // 获取当前coroutine的执行器 auto executor = co_await asio::this_coro::executor; for (;;) { // 创建一个timer auto steady_timer = asio::steady_timer(executor, sleep); // 等待这个timer触发 co_await steady_timer.async_wait(asio::use_awaitable); SPDLOG_INFO("[{}] sleep {}", task_name, sleep); }}int main() { using namespace std::chrono_literals; // 1. 初始化io_context, 你可以理解为是一个调度器 asio::io_context ctx{}; std::string task1="task1"; std::string task2="task2"; // 2. 创建异步任务 绑定 到 io_context asio::co_spawn(ctx, do_print(task1, 1s), asio::detached); asio::co_spawn(ctx, do_print(task2, 2s), asio::detached); // 3. 运行io_context ctx.run();}\n\nco_returnco_return 本质上会调用 promise_type::return_value 函数! 对于一些有返回值的 coroutine 可能需要特殊处理下!通常做法就是coroutine也是一个awaitable对象!\nasio::detached 实际上会创建并开始协程,所以这里我们是没办法直接获取 read_data 数据,需要使用 run 函数包装下!\n#include "asio.hpp"#include "asio/experimental/coro.hpp"#include "spdlog/spdlog.h"asio::awaitable<std::string> read_data() { const auto executor = co_await asio::this_coro::executor; asio::system_timer timer(executor, std::chrono::seconds(1)); co_await timer.async_wait(asio::use_awaitable); co_return "hello world";}asio::awaitable<void> run() { SPDLOG_INFO("run start"); auto result = co_await read_data(); SPDLOG_INFO("run end {}", result);}int main(int argc, char* argv[]) { asio::io_context ctx; asio::co_spawn(ctx, run(), asio::detached); ctx.run();}\n\nchannel#include "asio.hpp"#include "asio/experimental/channel.hpp"#include "spdlog/spdlog.h"// 注意 channel 必需第一个参数是 asio::error_code,其他是自己需要传递的参数,可以多个!// channel是绑定了io_context不能跨io_context调度!using int_channel = asio::experimental::channel<void(asio::error_code, int)>;asio::awaitable<void> produce(int_channel& channel) { for (int x = 0; x < 10; x++) { co_await channel.async_send(asio::error_code{}, x + 1, asio::use_awaitable); } // 用完后记得close channel.close();}asio::awaitable<void> consume(int_channel& channel) { for (;;) { auto [errcode, num] = co_await channel.async_receive(asio::as_tuple(asio::use_awaitable)); if (errcode) { if (errcode == asio::experimental::channel_errc::channel_closed) { SPDLOG_INFO("channel_closed"); co_return; } SPDLOG_INFO("system error code: {}, message: {}", errcode.value(), errcode.message()); co_return; } SPDLOG_INFO("receive: {}", num); }}asio::awaitable<void> consume_try_catch(int_channel& channel) { asio::steady_timer timer(channel.get_executor(), std::chrono::seconds(1)); co_await timer.async_wait(asio::use_awaitable); SPDLOG_INFO("start receive"); for (;;) { try { auto num = co_await channel.async_receive(asio::use_awaitable); SPDLOG_INFO("receive: {}", num); } catch (const std::exception& err) { // 也可以 try catch抓 SPDLOG_INFO("error: {}", err.what()); co_return; } }}int main(int argc, char* argv[]) { asio::io_context ctx; int_channel channel(ctx); asio::co_spawn(ctx, produce(channel), asio::detached); asio::co_spawn(ctx, consume(channel), asio::detached); ctx.run();}\n\ndeferred 和 use_awaitable 区别总结一下就是 deferred 会创建一个 deferred_async_operation (awaitable对象) ,然后需要我们需要执行的时候再使用 co_await!可以通过下面例子体验下!\n但是有个坑爹的地方在于 deferred_async_operation 貌似没有实现move!\n#include "asio.hpp"#include "spdlog/spdlog.h"using namespace std::chrono_literals;asio::awaitable<void> do_sleep(asio::io_context& context) { asio::steady_timer timer(context, std::chrono::seconds(1)); // 创建一个 deferred_async_operation SPDLOG_INFO("do_sleep start"); const auto timer_async_op = timer.async_wait(asio::deferred); // 中间可以做一些别的事情! SPDLOG_INFO("do_sleep end"); // 需要执行的时候再执行 deferred_async_operation co_await timer_async_op(asio::use_awaitable); SPDLOG_INFO("do_sleep end");}asio::awaitable<void> do_sleep_use_awaitable(asio::io_context& context) { asio::steady_timer timer(context, std::chrono::seconds(1)); // 如果你需要直接 await 那么你只需要 use_awaitable,不推荐用 deferred SPDLOG_INFO("do_sleep_use_awaitable start"); co_await timer.async_wait(asio::use_awaitable); SPDLOG_INFO("do_sleep_use_awaitable end");}int main() { asio::io_context context{}; asio::co_spawn(context, do_sleep(context), asio::detached); context.run();}\n\nasio::make_work_guardwork_guard 非常重要,通过上面的例子其实我没发现当事件全部结束那么 io_context.run()将会执行结束退出!那么问题是啥呢?有些时候我们可能需要在一些后置流程中给 io_context 添加任务,那么此时 work_guard 就非常有用了! 下面这个 master+worker模型中就用到了!\nasio 异常设计c++20的异常设计是coroutine如果不抓异常,那么直接忽略了,asio为了高性能,提供了两种方案,一种是手动抓取返回值 err_code ,一种是抓取异常,推荐前者,上面讲到的 channel 例子中就有提到!\n多个 coroutine awaitable_operators在Go里面实际上是支持channel select + waitgroup的,但是在asio中实际上实现这俩能力需要借助 awaitable_operators !\noror 对标的就是channel select,我们可以选择一个等待的事件去处理!\n#include "asio.hpp"#include "asio/experimental/awaitable_operators.hpp"#include "fmt/chrono.h"#include "spdlog/spdlog.h"template <class Rep, class Period>asio::awaitable<std::string> create_task(std::chrono::duration<Rep, Period> spend, std::string task_name) { const auto executor = co_await asio::this_coro::executor; asio::steady_timer timer(executor, spend); SPDLOG_INFO("create_io_task - {} start. spend {}", task_name, spend); co_await timer.async_wait(asio::use_awaitable); SPDLOG_INFO("create_io_task - {} end.", task_name); co_return task_name;}asio::awaitable<void> select_one_task() { using namespace asio::experimental::awaitable_operators; using namespace std::chrono_literals; const auto result = co_await (create_task(1s, "task1") || create_task(2s, "task2")); if (const auto name = std::get_if<0>(&result); name != nullptr) { SPDLOG_INFO("select task0 - {}", *name); } if (const auto name = std::get_if<1>(&result); name != nullptr) { SPDLOG_INFO("select task1 - {}", *name); }}asio::awaitable<void> wait_group() { using namespace asio::experimental::awaitable_operators; using namespace std::chrono_literals; const auto& [task1, task2] = co_await (create_task(1s, "task1") && create_task(2s, "task2")); SPDLOG_INFO("result: {} {}", task1, task2);}int main(int argc, char* argv[]) { asio::io_context context{}; asio::co_spawn(context, select_one_task(), asio::detached); context.run();}// 输出// [2024-04-12 14:34:07.631] [info] [awaitable_operators_asio.cpp:10] create_io_task - task1 start. spend 1s// [2024-04-12 14:34:07.632] [info] [awaitable_operators_asio.cpp:10] create_io_task - task2 start. spend 2s// [2024-04-12 14:34:08.631] [info] [awaitable_operators_asio.cpp:12] create_io_task - task1 end.// [2024-04-12 14:34:08.632] [info] [awaitable_operators_asio.cpp:21] select task0 - task1\n\nandand 对标的就是 waitgroup,我们可以等待多个事件结束后再处理!\nasio::awaitable<void> wait_group() { using namespace asio::experimental::awaitable_operators; using namespace std::chrono_literals; const auto& [task1, task2] = co_await (create_io_task(1s, "task1") && create_io_task(2s, "task2")); SPDLOG_INFO("result: {} {}", task1, task2);}int main(int argc, char* argv[]) { asio::io_context context{}; asio::co_spawn(context, wait_group(), asio::detached); context.run();}//输出://[2024-04-12 14:34:57.945] [info] [awaitable_operators_asio.cpp:10] create_io_task - task1 start. spend 1s//[2024-04-12 14:34:57.946] [info] [awaitable_operators_asio.cpp:10] create_io_task - task2 start. spend 2s//[2024-04-12 14:34:58.945] [info] [awaitable_operators_asio.cpp:12] create_io_task - task1 end.//[2024-04-12 14:34:59.947] [info] [awaitable_operators_asio.cpp:12] create_io_task - task2 end.//[2024-04-12 14:34:59.947] [info] [awaitable_operators_asio.cpp:32] result: task1 task2\n\n备注这里我举的是 二元操作,实际上是支持下面这个操作!具体返回值是 std::variant<string, string, string> 的!\nconst auto result = co_await (create_task(1s, "task1") || create_task(2s, "task2") || create_task(3s, "task3"));\n\n网络编程其实在asio中我们不难发现,aiso整体设计都是围绕着io_context去走,很显然上面例子都是单线程的,但是实际业务不可能用单线程,因此通常有几种模型\n\n单io_context + 单线程,性能瓶颈较大!\n单io_context + 多线程,用法简单,但是坏处是不清楚当前coroutine(代码)执行在哪个线程上,天然的需要考虑多线程/数据竞争/多线程数据一致性等问题!\nmaster + worker 模型,master io_context 负责一些框架,网络请求的连接/读/写/关闭事件,真正的业务处理交给 worker io_context,这种好处就是同一个请求会走一个io_context 天然的没有多线程问题!\n\n[1] 单io_context + 单线程#include "asio.hpp"#include "spdlog/spdlog.h"using namespace asio::ip;asio::awaitable<void> echo(tcp::socket socket) { try { char data[1024]; for (;;) { const auto size = co_await socket.async_read_some(asio::buffer(data), asio::use_awaitable); // SPDLOG_INFO("{} {} - read", thread_id(), socket.remote_endpoint()); co_await async_write(socket, asio::buffer(data, size), asio::use_awaitable); // SPDLOG_INFO("{} {} - write", thread_id(), socket.remote_endpoint()); } } catch (std::exception& e) { SPDLOG_INFO("{} {} - echo Exception: {}", thread_id(), socket.remote_endpoint(), e.what()); socket.close(); }}asio::awaitable<void> handler_listener(tcp::acceptor& listener) { for (;;) { auto conn = co_await listener.async_accept(asio::use_awaitable); SPDLOG_INFO("{} - receive conn {} -> {}", thread_id(), conn.remote_endpoint(), conn.local_endpoint()); asio::co_spawn(listener.get_executor(), echo(std::move(conn)), asio::detached); }}int main() { asio::io_context ctx; std::allocator<void> alloc; tcp::acceptor listener(ctx, tcp::endpoint(tcp::v4(), 8080)); asio::co_spawn(ctx, handler_listener(listener), asio::detached); ctx.run();}\n\n[2] 单io_context + 多线程#include "asio.hpp"#include "spdlog/spdlog.h"using namespace asio::ip;struct multi_thread { explicit multi_thread(const size_t size) : threads_(std::vector<std::thread>(size)) { } ~multi_thread() { for (auto& thread : threads_) { if (thread.joinable()) { thread.join(); } } } void run(const std::function<void()>& foo) { for (auto& thread : threads_) { thread = std::move(std::thread(foo)); } }private: std::vector<std::thread> threads_;};int main() { asio::io_context ctx; tcp::acceptor listener(ctx, tcp::endpoint(tcp::v4(), 8080)); asio::co_spawn(ctx, handler_listener(listener), asio::detached); multi_thread multi_threads(4); multi_threads.run([&] { ctx.run(); });}\n\n[3] master + worker 模型#include "asio.hpp"#include "spdlog/spdlog.h"struct worker_context { worker_context() = default; void run() { thread_ = std::move(std::thread([this] { io_ctx.run(); })); } ~worker_context() { io_ctx.stop(); if (this->thread_.joinable()) { thread_.join(); } } asio::awaitable<void> echo(asio::ip::tcp::socket socket) { SPDLOG_INFO("{} - handle conn {}", thread_id(), socket.remote_endpoint()); try { char data[1024]; for (;;) { const auto size = co_await socket.async_read_some(asio::buffer(data), asio::use_awaitable); // SPDLOG_INFO("{} {} - read", thread_id(), socket.remote_endpoint()); co_await async_write(socket, asio::buffer(data, size), asio::use_awaitable); // SPDLOG_INFO("{} {} - write", thread_id(), socket.remote_endpoint()); } } catch (std::exception& e) { SPDLOG_INFO("{} {} - echo Exception: {}", thread_id(), socket.remote_endpoint(), e.what()); socket.close(); } } asio::io_context& get_context() { return io_ctx; }private: asio::io_context io_ctx{}; std::thread thread_{};};struct main_context { using work_guard_type = decltype(asio::make_work_guard(std::declval<worker_context>().get_context())); explicit main_context(size_t worker_size) : worker_contexts_(std::vector<worker_context>(worker_size)){}; void run() { asio::ip::tcp::acceptor listener(io_ctx_, asio::ip::tcp::endpoint(asio::ip::tcp::v4(), 8080)); asio::co_spawn(io_ctx_, handler_listener(listener), asio::detached); std::vector<work_guard_type> work_guards{}; for (auto& worker_context : worker_contexts_) { work_guards.push_back(asio::make_work_guard(worker_context.get_context())); worker_context.run(); } io_ctx_.run(); } ~main_context() { io_ctx_.stop(); } asio::awaitable<void> handler_listener(asio::ip::tcp::acceptor& listener) { for (;;) { auto conn = co_await listener.async_accept(asio::use_awaitable); conn_counter = conn_counter + 1; SPDLOG_INFO("{} - receive conn[{}] {} -> {}", thread_id(), conn_counter, conn.remote_endpoint(), conn.local_endpoint()); auto& worker_context = worker_contexts_[conn_counter % worker_contexts_.size()]; asio::co_spawn(worker_context.get_context(), worker_context.echo(std::move(conn)), asio::detached); } }private: asio::io_context io_ctx_{}; std::vector<worker_context> worker_contexts_{}; size_t conn_counter{};};int main() { main_context main_context(4); main_context.run();}\n\n性能对比\n单 io_context + 单线程\n\n~ devtool tcp benchmark_echo_service --conn 100 --count 10000[INFO] 00:21:21.818 addr=localhost:8080, max_conn=100, max_req=10000, data_size=64, run_time=10s, interval=0s[INFO] 00:21:31.819 latency avg(req): 870.994µs[INFO] 00:21:31.819 throughput avg(s): 114507[INFO] 00:21:31.819 total success request: 1145075[INFO] 00:21:31.819 total error request: 0\n\n\n单 io_context + 多线程\n\n~ devtool tcp benchmark_echo_service --conn 100 --count 10000[INFO] 00:15:45.176 addr=localhost:8080, max_conn=100, max_req=10000, data_size=64, run_time=10s, interval=0s[INFO] 00:15:55.179 latency avg(req): 371.247µs[INFO] 00:15:55.179 throughput avg(s): 268017[INFO] 00:15:55.179 total success request: 2680170[INFO] 00:15:55.179 total error request: 0\n\n\nmaster + worker 模型\n\n~ devtool tcp benchmark_echo_service --conn 100 --count 10000[INFO] 00:16:43.651 addr=localhost:8080, max_conn=100, max_req=10000, data_size=64, run_time=10s, interval=0s[INFO] 00:16:53.653 latency avg(req): 512.29µs[INFO] 00:16:53.653 throughput avg(s): 194573[INFO] 00:16:53.653 total success request: 1945732[INFO] 00:16:53.653 total error request: 0\n\n总结单 io_context + 单线程在echo这种简单的模型下都显得最弱,确实如此因为利用不了系统资源!\n单 io_context + 多线程 模型依赖于asio的强大的调度器来看确实性能会优秀一些,但是会出现一个问题,导致同一个请求读写事件回掉会分配到不同的线程执行,会导致线程切换开销较大\nmaster + worker 模型用的不太对,因为 master io_context仅处理接收连接(在这个例子中它仅处理了100个事件),worker io_context仅处理连接的读写事件,好处在于 同一个请求的全部任务都在一个线程上执行,开销小!\n待优化:master io_context 处理所有请求的连接/读/写事件,worker io_context 仅去执行具体的业务逻辑,但是我这个例子里体现不出来,因为我是一个echo!后续我可以实现一个简单的HTTP服务器试试!\n为什么 master + worker 模型 会比较好呢?因为实际业务中我们业务逻辑的开销往往占大头!而IO和业务线程混合在一起会导致资源分配不均匀,比如io事件延时较高抖动等!\nasio 确实是一个非常强力的网络框架,提供了非常丰富的 coroutine 原语,例如 channle、join 等,目前在开源的现状看确实asio的功能最为丰富!\n其他对于网络编程来说,超时控制也非常的重要,asio/更底层的网络库(epoll/io_uring)等并不会直接提供读、写、连接等超时选项,所以通常需要业务自己控制请求!通常的做法就是与 timer 一起维护!上文我们也讲了,可以使用 awaitable_operators 进行处理!\n备注\n使用到两个库: spdlog & fmt!\n\n#include "fmt/core.h"#include "fmt/ostream.h"#include "spdlog/spdlog.h"template <>struct fmt::formatter<decltype(std::this_thread::get_id())> : fmt::ostream_formatter {};std::string thread_id() { return fmt::to_string(std::this_thread::get_id());}template <typename T>struct fmt::formatter<asio::ip::basic_endpoint<T>> : fmt::formatter<std::string_view> { auto format(const asio::ip::basic_endpoint<T>& endpoint, fmt::format_context& ctx) { return fmt::format_to(ctx.out(), "[{}]:{}", endpoint.address().to_string(), endpoint.port()); }};\n\n\n本文的代码都在这个项目里:https://github.com/Anthony-Dong/cpp\n\n","categories":["C++","Asio"],"tags":["C++","Asio","coroutine"]},{"title":"shell技巧介绍","url":"/2021/03/01/bfb83a0f198dea6e73567268963f4e9b/","content":"Shell 脚本在我们日常开发和学习都有举足轻重的地位,比如看一些开源项目,比如项目中的各式各样的脚本,对于促进生产力工具有很大帮助!而且shell最大的好处就是非常的方便!bash是一个非常成熟的脚本语言,基本上具有现代脚本语言的所有特性,所以你可以通过bash实现各种各样的脚本!\n我们日常开发中使用mac作为开发机的话需要考虑和linux命令兼容的问题,所以需要在mac上替换成gun相关的命令!\n\n\n1、命令小技巧1、-x 命令进行跟踪调试执行注意set -x是开启trace,set +x是关闭trace\n#!/bin/shnum1=10num2=20if (($num1 <= $num2)); then echo num1 lesser equal num2else echo num1 greater num2fi\n\n执行:\n➜ note git:(master) ✗ sh -x /Users/fanhaodong/Desktop/project/test.sh+ num1=10+ num2=20+ (( 10 <= 20 ))+ echo num1 lesser equal num2num1 lesser equal num2\n\n2、-c 命令 (执行命令参数)➜ note git:(master) ✗ sh -c << EOF "dquote> echo hello worlddquote> echo hello world2dquote> "heredoc> EOFhello worldhello world2\n\n\n这种经常会在网上下载一个脚本然后直接执行,可以 sh -c "curl xxxx.sh"\n\n3、使用set变量一般就是 set -ex ,或者执行的时候 bash -ex\n#!/bin/sh# -v Print shell input lines as they are read.# -x Print commands and their arguments as they are executed.# -e Exit immediately if a command exits with a non-zero status.set -execho "hello world"exit 1\n\n执行\n➜ makefile git:(master) ✗ sh ./main.sh+ echo 'hello world'hello world+ exit 1➜ makefile git:(master) ✗ echo $? 1\n\n帮助可以看: sh -c "help set"\n2、语法小技巧1、引用变量一般推荐正确用法是,变量使用 "" 双引号引用,其次就是变量使用 ${}进行引用!very good!\n# 使用 ${} 引用变量,拒绝歧义➜ ~ data="hello" ;echo $dataa➜ ~ data="hello" ;echo ${data}ahelloa# 字符串用 "" 引用➜ ~ data="hello " ;echo "${data}a"hello a➜ ~ data=hello ;echo "${data}a"helloa# 单引号不会进行变量赋值 (注意)➜ ~ data=hello ;echo '${data}a'${data}a\n\n2、 $( cmd ) 和 `cmd` 执行命令➜ ~ echo $(uname)Darwin➜ ~ echo `uname`Darwin\n\n3、 cat [>>|>] [file] << EOF .... EOF 写入文件\n如果重定向的操作符是<<-,那么分界符(EOF)所在行的开头部分的制表符(Tab)都将被去除。这可以解决由于脚本中的自然缩进产生的制表符。\n\n➜ test cat > glide.yaml << EOFheredoc> name: tomheredoc> age: 10heredoc> hobby:heredoc> - footballheredoc> EOF➜ test cat glide.yamlname: tomage: 10hobby:- football\n\n4、管道符 和 xargs1. 管道符管道符作用就是把上一个命令的标准输出作为下一个命令个标准输入\n➜ echo "hello wrold" | python -c 'import sys; print(sys.stdin.read())'hello wrold\n\n2. xargs1 .xargs的作用就是把上个命令的标准输出 作为 下一个命令的参数 \n➜ echo "hello wrold" | xargs python -c 'import sys; print(sys.argv)'['-c', 'hello', 'wrold']\n\n\n如果想替换分隔符号需要输入参数 -d, 同时你想打印所执行的命令 -t\n\n➜ ~ echo "hello:wrold" | xargs -t -d ':' python -c 'import sys; print(sys.argv)'python -c 'import sys; print(sys.argv)' hello 'wrold'$'\\n'['-c', 'hello', 'wrold\\n']# 上面带\\n的原因是echo默认换行,需要➜ ~ echo -e "hello:wrold\\c" | xargs -d ':' python -c 'import sys; print(sys.argv)'['-c', 'hello', 'wrold']\n\n\nxargs 是并行执行的,可以通过如下测试**, -P等于0表示不限制进程数**,默认是1, -n等于1表示参数按每一个进行拆分\n\n\n 这个命令相当棒,就是可以并行执行!\n\n➜ echo "hello world" | xargs -P0 -n1 sh -c 'echo "start $$"; sleep 5s; echo "end $$"'start 21377start 21378end 21377end 21378\n\n\n-I,可以替换命令行参数\n\n➜ ~ echo "hello wrold" | xargs echohello wrold➜ ~ echo "hello wrold" | xargs -I {} echo {}hello wrold\n\n5、特殊变量\n$0: 当前脚本的文件名\n$[n] : 传递给脚本或函数的参数。n 是一个数字,表示第几个参数。例如,第一个参数是$1,第二个参数是$2。\n$#: 传递给脚本或函数的参数个数。\n$*: 传递给脚本或函数的所有参数\n$@: **传递给脚本或函数的所有参数(推荐使用这个)**,当使用 "" 双引号引用是 $*会变成字符串而不是数组\n$?: 上个命令的退出状态,或函数的返回值。一般情况下,大部分命令执行成功会返回 0,失败返回 1。\n$$: 当前Shell进程ID。对于 Shell 脚本,就是这些脚本所在的进程ID。\n$! 子进程ID\n\n6、[[]] 和 [] 标准 以及基本语法规范name="xiao_li"# 比较 = / !=if [ "$name" = "xiao_li" ]; then echo "1(bash运算符). name is xiao_li"; fiif [[ "$name" = "xiao_li" ]]; then echo "1(bash运算符). name is xiao_li"; fiage=10if [ "$name" = "xiao_li" ] && [ "$age" -eq 10 ]; then echo "3(sh逻辑逻辑). name is xiao_li and age is 10"; fiif [[ "$name" = "xiao_li" && "$age" -eq 10 ]]; then echo "4(bash逻辑逻辑). name is xiao_li and age is 10"; fiif [[ "$name" == xiao_* ]]; then echo "5(bash通配符). xiao_li has prefix xiao_"; fiif [[ "$name" =~ ^xiao_[a-z]+* ]]; then echo "6(bash正则). xiao_li reg match ^xiao_[a-z]+* "; fi\n\n总结: 大部分情况下 [] 都可以满足需求,如果使用到通配符、正则等需求再使用[[]],其次关于逻辑运算推荐使用 [] && [] 这种方式,除非有复杂的逻辑运算需求!\n7、/bin/sh 与 /bin/bash 的区别/bin/sh 与 /bin/bash 的区别\n3、获取命令结果 $(cmd)有两种写法,一种是 $()这个并不是所有的shell都支持,但是比较直观, 另外一种是 "``" (它可是适用更多的平台)\n#!/bin/shecho `ls -a /Users/fanhaodong/note`echo $(ls -a /Users/fanhaodong/note)\n\n输出:\n. .. .DS_Store 1714.jpg docker-rocketmq-cluster gridea-home hexo-home note pdf vuepress-starter. .. .DS_Store 1714.jpg docker-rocketmq-cluster gridea-home hexo-home note pdf vuepress-starter\n\n4、输入输出重定向 2>&1使用程序中经常有,标准输出,但是还有错误输出,因此需要合并到一个流中\n其实Go的程序中正执行脚本的时候可以指定,标准输出和错误输出\ncommand := exec.Command(shell, "-c", cmd)command.Stdout = os.Stdoutcommand.Stderr = os.Stderr\n\n使用的时候:\n\n默认为标准输出重定向,与 1> 相同\n2>&1 意思是把 标准错误输出 重定向到 标准输出.\n&>file 意思是把标准输出和标准错误输出 都重定向到文件file中\n\n例如:\ncommand >out.file 2>&1 &\ncommand >out.file是将command的标准输出重定向到out.file文件,即输出内容不打印到屏幕上,而是输出到out.file文件中。2>&1 是将标准出错重定向到标准输出,这里的标准输出已经重定向到了out.file文件,即将标准出错也输出到out.file文件中。最后一个& ,是让该命令在后台执行。\n参考https://www.cnblogs.com/caolisong/archive/2007/04/25/726896.html\n5、If语句\n if 其实就是test 命令\n\n1、格式\n换行写\n\nif [ condition ]; then # bodyelif [ condition ]; then # bodyelse # bodyfi\n\n2)非换行写\nif [ -f "/Users/fanhaodong/note/note/Makefile1" ]; then echo 111 ; echo 222 ;elif [ -f "/Users/fanhaodong/note/note/README.md" ]; then echo 333 ; echo 4444 ; else echo 555 ; echo 666 ; fi\n\n2、结果获取/判断结果输出0 ,表示为真,可以通过$? 来获取结果\n3、例如调试条件➜ note git:(master) ✗ test "abc"!="def"➜ note git:(master) ✗ echo $?0\n\n4、测试文件是否存在\n如果你要判断一个文件是否存在,只需要 -e 即可,输出0 表示文件存在 (不在判断类型的时候推荐使用这个)\n如果你要判断一个文件是否为文件夹,并且判断是否存在,只需要 -d 即可\n如果你要判断一个文件是否为常规文件 ,并且判断是否存在,只需要-f 即可\n-L filename 如果 filename为符号链接,则为真\n\n[root@019066c0cd63 ~]# ls -allrwxrwxrwx 1 root root 5 Mar 1 09:49 c.txt -> a.txt[root@019066c0cd63 ~]# [ -L "./c.txt" ][root@019066c0cd63 ~]# echo $?0\n\n\n-r filename 如果 filename可读,则为真 \n-w filename 如果 filename可写,则为真 \n-x filename 如果 filename可执行,则为真\n-s filename 如果文件长度不为0,则为真\n-h filename 如果文件是软链接,则为真\n\n➜ note git:(master) ✗ [ -f "/Users/fanhaodong/note/note/Makefile" ]➜ note git:(master) ✗ echo $?0➜ note git:(master) ✗ [ -f "/Users/fanhaodong/note/note/Makefile1" ]➜ note git:(master) ✗ echo $?1\n\n5、字符串操作\n 字符串推荐加 "" 进行定义\n\n\n判断字符串是否为空 -z (zero)么\n\n#!/bin/shstr=""if [ -z "${str}" ]; then echo str is emptyfi# str is empty\n\n2)判断两个字符串是否相同\n#!/bin/shstr1="str"str2="str2"if [ "$str1" = "$str2" ]; then echo str1 is equal str2else echo str1 is not equal str2fi# str1 is not equal str2\n\n4、测试一个命令是否存在 command -v $#\n#!/bin/shcmd=goif [ `command -v $cmd` ]; then echo $cmd command is exists else echo $cmd command not exists fi# go command is exists\n\n5、获取字符串长度 ${#var}\n#!/bin/shstr="hello " str1=helloecho str 的长度是 ${#str}echo str1 的长度是 ${#str1}#str 的长度是 8#str1 的长度是 5\n\n6、数字比较\n-eq 等于\n-ne 不等于\n-gt 大于\n-ge 大于等于\n-lt 小于\n-le 小于等于\n\n#!/bin/shnum1=10num2=20if (($num1 <= $num2)); then echo num1 lesser equal num2fiif [ $num1 -le $num2 ]; then echo num1 lesser equal num2fi# num1 lesser equal num2# num1 lesser equal num2\n\n7、shell脚本中if判断’-a’ - ‘-z’含义https://blog.csdn.net/tootsy_you/article/details/95597376\n\n\n\n6、数组和循环#!/bin/bash#### 数组# 1. 定义数组arr=("00" "11" "22" "33" "44")# 2. 遍历数组for elem in "${arr[@]}"; do echo "elem: ${elem}"done# 数组在shell中定义和map差不多,map也可以使用数组进行表达,key=index, value=elemfor index in "${!arr[@]}"; do echo "elem: ${arr[index]}"done# 标准for循环for ((index = 0; index < ${#arr[@]}; index++)); do echo "elem: ${arr[index]}"done# 3. 添加元素arr+=("55" "66")echo "len(arr) = ${#arr[@]}"# 4. 元素切片echo "arr[2:4] = ${arr[*]:2:2}"# 5. 判断元素是否存在# $1=want# $2...=arr# 注意shell函数参数没有数组的概念,只能说你把它展开成多个参数# 注意 $@ 为参数, $# 为参数长度function contains() { # $0 为执行文件 # $1 ... 为参数 want="$1" for elem in "${@:2}"; do if [ "$elem" == "$want" ]; then return 0; fi done return 1}if contains "111" "${arr[@]}"; then echo "contains 111"else echo "not contains 111"fiif contains "11" "${arr[@]}"; then echo "contains 11"else echo "not contains 11"fi\n\n7、switch#!/bin/bashname="$1"case "$name" in "11 222"| "222 333") echo "num is: 11 222| 222 3333" ;; 44) echo "name is: 44" ;; *) echo "num is(*): $name" ;;esac\n\n输出:\n➜ script git:(master) ✗ bash -e string.sh "11 222" num is: 11 222| 222 3333➜ script git:(master) ✗ bash -e string.sh "11 222 3333"num is(*): 11 222 3333➜ script git:(master) ✗ bash -e string.sh "44" name is: 44\n\n字符串操作#!/bin/bashname="xiao_min"# 移除前缀suffix=${name#"xiao_"}echo "suffix: $suffix" # suffix: min# 移除后缀prefix=${name%"$suffix"}echo "prefix: $prefix" # prefix: xiao_# 字符串替换name2=${name/"min"/"li"}echo "name2: $name2" # name2: xiao_li\n\ngetopt实际中我们写一些复杂的脚本(命令行工具)都需要解析参数,shell提供了getopts 和 getopt 命令可以帮助实现命令行解析\n~ type getoptsgetopts is a shell builtin~ type getoptgetopt is /usr/bin/getopt\n\n\n-o 或者 --option 表示短选项\n\n-l 或者--longoptions 表示长选项,多个参数用 , 进行分割\n\n选项会有 必须、可选、Flag 三种状态,是通过申明 :来表示\n\nrequired 必须:参数定义后面需要跟: , 必须的意思就是我定义 required 的参数那么这个参数后面一定得跟东西,不是必须传递这个参数,例如\n\n➜ ~ getopt -o 'a:' -- -agetopt: option requires an argument -- a -- ➜ ~ getopt -o 'a:' -- -a 1 -a '1' -- ➜ ~ getopt -o 'a:' -- 111 222 -- '111' '222'\n\n\noptional 可选:参数定义后面跟:: , 解释同上面,optional 参数就是你后面可以不跟东西,但是参数值需要附加在值\n\n➜ ~ getopt -o 'a::' -- -a 1 -a '' -- '1'➜ ~ getopt -o 'a::' -- -a1 -a '1' --➜ ~ getopt -o 'a::' -- -a -a '' -- ➜ ~ getopt -o '' -l 'name::' -- --name --name '' --➜ ~ getopt -o '' -l 'name::' -- --name='111' --name '111' --➜ ~ getopt -o '' -l 'name::' -- --name --name '' --\n\n\nflag : 参数后面啥也不跟\n\n例如 -o 'abc:d::e' 表示 a、b不接收参数,c必须要跟一个参数,d为可选参数,e不接收参数\n\n\n\n注意:optioanl 参数值赋值有点奇葩,这个也是必然的不然无法区分出来optioanl 和 required,例如 短选项需要直接附加在值后面,长选项需要用=来附加值\n\n\ngetoptsgetopts 是shell的内置方法,但是不能处理长命令,用起来会比较方便和简单,所以经常用于处理一些简单场景!\n#!/bin/bash# getopts ":ab:c:" opt # 其中最前面的:表示忽略错误# OPTIND: 选项索引 option index, 下一个要处理的元素位置, 初始化值是1, 大部分场景也不会用到# OPTARG: 选项参数 option argecho "args: $*"echo "OPTIND=[$OPTIND]"while getopts ":ab:c:" opt; do case $opt in a) echo "this is -a option. OPTARG=[$OPTARG] OPTIND=[$OPTIND]" ;; b) echo "this is -b option. OPTARG=[$OPTARG] OPTIND=[$OPTIND]" ;; c) echo "this is -c option. OPTARG=[$OPTARG] OPTIND=[$OPTIND]" ;; *) echo "there is unrecognized parameter." exit 1 ;; esacdone\n\n执行:\n➜ vscode git:(master) ✗ bash run.sh -a -b1 -c 1 -c2 args: -a -b1 -c 1 -c2OPTIND=[1]this is -a option. OPTARG=[] OPTIND=[2]this is -b option. OPTARG=[1] OPTIND=[3]this is -c option. OPTARG=[1] OPTIND=[5]this is -c option. OPTARG=[2] OPTIND=[6]\n\ngetopt 命令mac的话需要: brew install gnu-getopt\n帮助文档:https://linux.die.net/man/1/getopt\ngetopt既然是命令,说白了,我们可以怎么处理呢?很简单\n# a为flag# b为required 如果需要带`-d` 参数那么后面一定要附加值# c为optional# help 为flag# size为optionl# name为required➜ ~ getopt -o 'ab:c::d' -l 'help,size::,name:' -- -a --name '1' -b '1111' -c1111 --size='1111' -a --name '1' -b '1111' -c '1111' --size '1111' --\n\n现代编程语言 - bash shellbash 和 sh的区别bash shell 是一门现代的编程语言\nsh 仅支持一些简单的逻辑判断、循环等,并不支持一些丰富的内置能力,比如数组、数学计算、条件判断支持正则/通配符等一些高级特性!\n控制逻辑条件控制name="xiao_li"# 比较 = / !=if [ "$name" = "xiao_li" ]; then echo "1(bash运算符). name is xiao_li"; fiif [[ "$name" = "xiao_li" ]]; then echo "1(bash运算符). name is xiao_li"; fiage=10if [ "$name" = "xiao_li" ] && [ "$age" -eq 10 ]; then echo "3(sh逻辑逻辑). name is xiao_li and age is 10"; fiif [[ "$name" = "xiao_li" && "$age" -eq 10 ]]; then echo "4(bash逻辑逻辑). name is xiao_li and age is 10"; fiif [[ "$name" == xiao_* ]]; then echo "5(bash通配符). xiao_li has prefix xiao_"; fiif [[ "$name" =~ ^xiao_[a-z]+* ]]; then echo "6(bash正则). xiao_li reg match ^xiao_[a-z]+* "; fi\n\n循环语句#!/bin/bash# 以下全部代码仅支持在bash中使用!# {1..5}迭代器生成[1-5]的数组for elem in {1..5}; do echo "[for-range] elem: $elem"donearr=(1 2 3)arr+=(4) # appendarr[0]=11 # setecho "arr[0] = ${arr[0]}" # get# arr - sizelength=${#arr[@]}echo "arr length = ${length}"# foreachfor elem in "${arr[@]}"; do echo "[foreach] elem: $elem"; done# for index (需要bash>=4.0, 支持 associative arrays)for index in "${!arr[@]}"; do echo "[for-index] index: $index, value: ${arr[$index]}"; done# forfor ((x = 0; x < length; x++)); do echo "[for] index: $x, value: ${arr[$x]}"done# whileindex=0while [ $index -lt "$length" ]; do echo "[while] index: $index, value: ${arr[$index]}" index=$((index + 1))done# breakfor elem in "${arr[@]}"; do if [ "$elem" -gt 10 ]; then echo "[break] $elem > 10" break fidone\n\n数据结构","categories":["Linux"],"tags":["shell"]},{"title":"grep、awk、sed、正则表达式 开发必备分享","url":"/2022/03/12/c9cb8912930fdc26498e1bf5b085fabf/","content":"grep、awk命令主要是用于我们日常开发中日志检索,问题就是有同学可能会咨询不是有elk、企业内部日志收集过滤系统,那么我为啥要学这些东西!日志收集在系统不稳定的情况下是很容易丢失日志的或者你做一些高精度的过滤日志也不符合,比如我要查看一下latency>10s的接口,你日志咋搜!!所以还是有学习必要性的!\n正则表达式如果你业务中处理文本的需求比较多的话,正则表达式的作用不容小觑,而且正则表达式庞大的知识体系也需要经常练习才可以!\n\n\n1、grep1. 基础概念grep全名叫Global regular expression print, wiki: https://zh.wikipedia.org/zh-tw/Grep,其实就是一个全局的正则表达式过滤然后打印!\n2. 日常使用grep 是我们日常最常见和使用的命令了,主要是做正则匹配做日志过滤!\n这里主要介绍一下常用的命令和技巧吧\ngrep [-abcdDEFGHhIiJLlMmnOopqRSsUVvwXxZz] [-A num] [-B num] [-C[num]] [-e pattern] [-f file] [--binary-files=value] [--color[=when]] [--colour[=when]] [--context[=num]] [--label] [--line-buffered] [--null] [pattern] [file ...]\n\n简单匹配就是 cat biz.log | grep 'Error' 过滤所有包含Error的行\n\n-i 可以忽略大小写,比如上面的grep -i error 它可以匹配 error和Error 等!\n-v 取反,比如说grep -v 'error',那么他就可以取info、debug、warn的日志了\n--colour=auto|always|never,一般设置为 --colour=always 就可以高亮展示了!\n-E 是高级正则,比如一些高级的正则 . 或则\\w ,\\d 等,就需要了\n-F为fgrep 其实就是--fixed-strings 本质上就是对于正则表达式不进行转义,比如\n\n\n3. 使用案例查找目录下关键词# 查看当前目录下存在`QuoteType` 关键词的文件~ grep -r 'QuoteType' ././diversity.proto: optional QuoteType quoteType = 1; // 引用类型# 仅展示文件~ grep -rl 'QuoteType' ././diversity.proto\n\n4. 总结所以一般你是精确字符匹配的话,推荐用 grep -F 或者fgrep , 如果你用的是正则匹配的话推荐用egrep和grep -E , 具体差异可以下我下面正则表达式那个章节!\n4. 参考文章\nGNU grep 介绍\n\n2、awk\nawk 是三个大佬写的一门语言,说是一门语言其实不足为过!因为确实贼厉害!\n\n1. 基础概念介绍awk 整体格式就是有一组的 pattern-action组成,不过也有可能只有action,但是必须要知道这一点很重要!!\n备注: 下列例子可能用到 out.log文件,文件内容如下: \nDan 3.75 0Kathy 4.00 10Mark 5.00 20Mary 5.50 22Susie 4.25 18\n\n\n简单过滤模式 (pattern-action)\n\nawk '$3>0 {print}' # pattern 就是 $3>0 , {print} 表示行为, 如果不指定 pattern 那么表示无过滤条件,# 换成其他语言就是for x:=0; x<len; x++{\t\tif (pattern) {\t\t\t\taction()\t\t}}\n\n\n(action1) (action2) 模式\n\nawk '{print} {print}' # 转换后for x:=0; x< len; x++{\t\taction1()\t\taction2()}\n\n\n内置pattern模式, pattern=BEGIN / END\n\n# pattern=END,表示结束awk 'END {print}' # 换成其他语言就是for x:=0; x<len; x++{}action()# pattern=BEGIN,表示开始awk 'BEGIN {print}' # 换成其他语言就是action()for x:=0; x<len; x++{}\n\n\n输出函数\n\nawk '{print 1, $1; print 2, $1}'awk '{print 1, $1} {print 2,$1}' awk '{printf "1 %s\\n", $1; printf "2 %s\\n", $1}'...# 上诉例子输出其实是一样的!\n\n\nif / else \n\n\n前面虽然说了 pattern可以支持条件过滤,但是太过于简单!不支持else语句\n\n➜ yulili cat out.log | awk '{if ($3>0) printf "%s-%d gt 0\\n", $1, $3; else printf "%s-%d eq 0\\n", $1,$3}'Beth-0 eq 0Dan-0 eq 0Kathy-10 gt 0Mark-20 gt 0Mary-22 gt 0Susie-18 gt 0\n\n\nfor 循环 ,一般会在END语句中执行!\n\n➜ yulili cat out.log | awk 'END { for (x=1; x<NR; x++) printf "row %d\\n", x}'row 1row 2row 3row 4row 5\n\n\n数组/map, 可以通过 var[index]=value或者var[key]=value 进行定义!同时可以通过 in进行判断是否存在\n\n# 简单的便利循环➜ yulili cat out.log | awk '{line[NR]=$0} END { for (x=1; x<NR; x++) printf "row: %s\\n", line[x]}'row: Beth 4.00 0row: Dan 3.75 0row: Kathy 4.00 10row: Mark 5.00 20row: Mary 5.50 22# 判断是否存在➜ yulili cat out.log | awk '{line[NR]=$0} END {if (1 in line) print "1 in line"} END {if (0 in line) print "0 in line"}'1 in line\n\n\n正则匹配\n\n# 例如输出M开头的用户信息➜ yulili cat out.log| awk '$1 ~ "^M" {print}'Mark 5.00 20Mary 5.50 22# 例如输出非M开头的用户信息➜ yulili cat out.log| awk '$1 !~ "^M" {print}'Beth 4.00 0Dan 3.75 0Kathy 4.00 10Susie 4.25 18\n\n\n内置变量\n\n\n\n\n内置变量\n含义\n\n\n\nNR(numeric row)\n表示总行数,已经阅读的行数, BEGIN=0,END=total\n\n\nNF(number field)\n表示没行的列数,BEGIN=0, END=pre_row_column\n\n\nFS (field separator)\n输入的分隔符,默认应该是 " "\n\n\nOFS(out field separator)\n输出的分隔符,默认是" "\n\n\n$0…n\n变量,$0表示整个列,$1表示第一列,注意变量是可以被修改!\n\n\n\n\n\n\n\n例如我修改FS和OFS, FS为空格,输出的OFS是,\n\n➜ yulili cat out.log | awk 'BEGIN { FS=" "; OFS=","} {print $1 ,$2 ,$3}'Beth,4.00,0Dan,3.75,0Kathy,4.00,10Mark,5.00,20Mary,5.50,22Susie,4.25,18\n\n\n内置函数\n\n\n\n\n内置函数\n含义\n\n\n\nprint\n换行输出, eg: awk '{print $1, $2, $3; print $1, $2}'\n\n\nprintf\nformat输出, eg: awk '{printf "第一列: %d", $1}'\n\n\nlength(s)\ns为字符串,输出字符串长度\n\n\nsubstr(s,p)\ns为字符串,p为index,输出从index后截取的字符串\n\n\ngsub(r,s,t)\n将字符串t中的 r 替换为s, 输出字符串\n\n\nstrtonum(s)\ns为字符串,输出为numeric\n\n\nrand()\n输出一个随机数\n\n\nint(x)\n输出x的整数部分\n\n\n\n例如我们使用gsub函数替换,这里可能注意可以正则替换\n\n➜ yulili cat out.log| awk '{gsub("\\\\.","0",$2)}{print $2}'400030754000500050504025\n\n2. 简单例子介绍\n查询工时大于0的记录\n\n➜ yulili cat out.log | awk '$3>0 {print}'Kathy 4.00 10Mark 5.00 20Mary 5.50 22Susie 4.25 18\n\n\n查询工时大于0的人员数量 , 业务中比较适合过滤 latency > xxxms的数量!\n\n➜ yulili cat out.log | awk '$3>0 {emp=emp+1} END {printf "total emp: %d\\n", emp}'total emp: 4\n\n\n查询平均工资 (工时*工时费用/ 人员) , 其实吧业务中比较适合求avg-latency\n\n➜ yulili cat out.log | awk '{ salary = salary + $2*$3 } END { printf "avg salary: %d\\n", salary / NR }'avg salary: 56\n\n3. 使用技巧技巧\n比如经常出现我们要检查 latency > xxx 的access_log ,怎么解决了,由于我们日志中 latency是这么记录的cost=251045,那么我们在过滤的时候需要字符串操作,这时候需要substr(str, index) 函数取出来lantency,然后取出来实际上是字符串类型,那么此时需要通过+0来转换为int!! 或者通过函数 strtonum进行转换, 下面例子是取出大于200ms的日志的logid\n\n# cat access.log | awk 'strtonum(substr($11,6)) > 200000 {print $6 ,$11, substr($11,6)}'# cat access.log | awk 'substr($11,6)+0 > 200000 {print $6 ,$11, substr($11,6)}'20220312000212010212043169032F1158 cost=358144 35814420220312000442010150139043227E02C8 cost=251045 25104520220312002645010150132075097C7676 cost=532952 5329522022031200293601021009615805605CF5 cost=256238 25623820220312002943010211182012276F60AD cost=298612 2986122022031200295801021009615805606647 cost=213975 21397520220312003410010150135045168607FA cost=366926 366926202203120037310101501390291691A893 cost=4882332 4882332202203120039100101501322000F82B0D0 cost=276756 276756\n\n\n文本处理,例如以下文本\n\na,b,cd1,c1,a1d2\n\n需求是需要按, 分割,且要拼接字符串!如 a,b,c 需要输出 t1.a=t2.a and t1.b=t2.b and t1.c=t2.c , 可以下面这么写!\ncat text.txt| awk 'BEGIN { FS="," }$0 ~ /\\S/ { sql="" for (x=1;x<=NF;x++) { if ( x != NF ){ sql= sprintf ("%s a.%s=b.%s and",sql, $x, $x) }else{ sql= sprintf ("%s a.%s=b.%s", sql, $x, $x) } } sqls[NR]=sql}END { for (x=1;x<=NR;x++){ if ( x in sqls) { printf "sql: %s\\n", sqls[x] # todo 拼接sql } }}'\n\n4. 参考文章\nawk 简单介绍\n推荐: awk中文文档\n\n3、sed1. 基础学习sed全名叫stream editor,流编辑器,用程序的方式来编辑文本。sed基本上就是玩正则模式匹配,所以,玩sed的人,正则表达式一般都比较强。其次就是gun的sed函数和mac的sed是有些不同的,mac上玩sed推荐用gsed!\n这里我就介绍一些简单的用法,比如我们经常进行的批量替换比如把代码中某个变量名替换为另一个变量名,或者配置文件之类的,或者进行简单的文本操作!\n\n替换,用法就是 sed 's/a/b/g' file , 意思就是把a替换为b,全局替换\n\n\ns: substitute\n\n➜ docs cat test.logaaaaaabbbbbbaaaaaacccccc# 把a替换成b,每行第一个➜ docs cat test.log| sed 's/a/b/'baaaaabbbbbbbaaaaacccccc# 最后加一个g表示global的意思,表示每行全局替换!➜ docs cat test.log| sed 's/a/b/g'bbbbbbbbbbbbbbbbbbcccccc# 表示从第三个字符开始替换➜ docs cat test.log| sed 's/a/b/3g'aabbbbbbbbbbaabbbbcccccc# s 可以通过[start,end]来修饰行数,比如 1-3行可以 1,3s, 比如1行可以1s,末尾行可以 $s, 比如2-最后一行可以用2,$表示➜ docs cat test.log| sed '1,3s/a/b/g'bbbbbbbbbbbbbbbbbbcccccc# 只替换第一行➜ docs cat test.log| sed '1s/a/b/g'bbbbbbbbbbbbaaaaaacccccc# 只替换最后一行➜ docs cat test.log| sed '$s/c/a/g'aaaaaabbbbbbaaaaaaaaaaaa\n\n注意:如果你要使用单引号,那么你没办法通过\\这样来转义,就有双引号就可以了,在双引号内可以用\\”来转义。\n\n正则替换,但是一般推荐使用正则直接携带 -E参数,注意会有一些转义字符,需要通过\\来处理\n\n➜ docs cat test.log| sed -E '2,$s/\\w/a/g'aaaaaaaaaaaaaaaaaaaaaaaa\n\n\n添加/插入/删除/替换文本, sed 'a1 文本'也就是在第一行后面添加文本,sed 'i1 文本'含义就是在第一行插入文本\n\n\na: append\ni: insert\nd: delete\n\n# 在第一行后面添加zzzzzz➜ docs cat test.log | sed '1a zzzzzz'aaaaaazzzzzzbbbbbbaaaaaacccccc# 在最后一行添加`zzzzzz`➜ docs cat test.log | sed '$a zzzzzz'aaaaaabbbbbbaaaaaacccccczzzzzz# 第一行插入 `zzzzzz`➜ docs cat test.log | sed '1i zzzzzz'zzzzzzaaaaaabbbbbbaaaaaacccccc# 删除第一行和最后一行!➜ docs cat test.log | sed '1d;$d'bbbbbbaaaaaa\n\n\n截取文本 \n\n# 选择第2-3行文本,注意这里必须加-n参数,否则会重复打印➜ docs cat test.log| sed -n '2,3p'bbbbbbaaaaaa# 打印➜ docs cat test.log| sed -n '/b/p'bbbbbb# 打印b或者c的文本➜ docs cat test.log| sed -n '/\\(\\(b\\|c\\)\\)/p'bbbbbbcccccc\n\n2. 总结\n命令通用格式 [addr]/[regexp]/[flags] 或者 [addr]/[regexp]/[replace content]/[false] \n多个命令可以通过 ;进行分割\n 2addr表示可以通过 1,2进行选择行操作\n1addr只支持1或者$或者 ….\nsed -i 是直接替换原文本!所以不推荐这么使用,可以用重定向符号进行操作!\nsed -E 表示使用拓展正则!如果你使用正则则推荐使用这个!\nmac 上推荐使用gsed命令!\n\n\n\n\n命令\n备注\n\n\n\n[2addr]s/regular expression/replacement/flags\n把regular expression替换成replacement\n\n\nw file\n写入到file中\n\n\nr file\n读取文件\n\n\n[2addr]x\n清空某行 1x表示清空第一行, Swap the contents of the pattern and hold spaces.\n\n\n[1addr]a text\napped text\n\n\n[1addr]i text\ninsert text\n\n\n[2addr] d\ndelete 指定行\n\n\n/regexp/d\n删除匹配行\n\n\n-n '[2addr]p'\nprint 打印指定行\n\n\n-n /regexp/p\n打印匹配行\n\n\n3. 参考文章\nGNU sed 文档\nsed 简明教程\n\n4、Dash如果你用的是mac作为你的开发环境,我推荐你使用dash进行命令或者代码库检索!比较好用,因为平时比如我们经常遇到写代码忘记api的,可以通过dash进行搜索!\n不过Linux的GNU命令都会携带帮助的!看个人喜好吧!\n\n5、 正则表达式其实不难发现,只要文本处理就离不开 正则表达式 (Regular Expression) ,可能有些同学会说 通配符(wildcard),确实通配符可以解决一部分问题,但是还是有些时候还是正则更加强大和灵活,其实通配符是正则表达式的前身,出生更早!\n1. 通配符\n* 匹配零个或多个字符 (有点像数据库的通配符*)\n\n? 匹配任意一个字符(有点像数据库的通配符_)\n\n[char_list] 匹配char_list中任意单个字符\n\n[^char_list] or [!char_list] 排除 char_list中任意一个字符\n\n\n➜ test_file ls *.filea.file ab.file c.file cd.file➜ test_file ls -a./ ../ a.file ab.file c.file cd.file➜ test_file ls *.filea.file ab.file c.file cd.file➜ test_file ls [a].filea.file➜ test_file ls [a]?.fileab.file➜ test_file ls [a]*.filea.file ab.file➜ test_file ls [ac]*.filea.file ab.file c.file cd.file➜ test_file ls [^a].filec.file➜ test_file ls [^a]*.filec.file cd.file\n\n\n{xxx1,xxx2,xxx3} 只能是 xxx1 or xxx2 or xxx3 , 模式, 这个比较常见主要用作创建文件ls文件\n\n➜ test_file ls {a,ab}.filea.file ab.file➜ test_file ls {a,c}*.filea.file ab.file c.file cd.file➜ test_file touch d{a,b,c,d}.file➜ test_file ls d{a,b,c,d}.fileda.file db.file dc.file dd.file\n\n\n {start..end} 表示匹配 start->end 中的字符, 比较适合for循环!!其中支持数字和字母\n\n➜ test_file for x in {1..10}; do echo "num: ${x}"; donenum: 1num: 2num: 3num: 4num: 5num: 6num: 7num: 8num: 9num: 10\n\n最后介绍下 通配符执行原理,通配符顾名思义就是执行的实现先进行解释,也就是比如 ls *.file 先解析成ls a.file 再执行 ls a.file, 假如没有匹配会导致报错或者程序不符合预期\n2. 正则表达式一 (基础)基础感觉不用说,可以看下 https://github.com/Anthony-Dong/learn-regex-zh , 想必看这篇文章的人正则基本都还是可以的! 这里推荐一个正则表达式的可视化网站 https://regexper.com/\n\n元字符,这类字符会有特殊含义,所以如果你正则中想要不表示特殊含义就需要转义了! 以下列表 POSIX Extended 和 Perl 全部支持 !POSIX 部分不支持!\n\n\n\n\n元字符\n描述\n\n\n\n.\n匹配除换行符以外的任意字符。\n\n\n[ ]\n字符类,匹配方括号中包含的任意字符。\n\n\n[^ ]\n否定字符类。匹配方括号中不包含的任意字符\n\n\n*\n匹配前面的子表达式零次或多次\n\n\n+\n匹配前面的子表达式一次或多次\n\n\n?\n匹配前面的子表达式零次或一次,或指明一个非贪婪限定符。\n\n\n{n,m}\n花括号,匹配前面字符至少 n 次,但是不超过 m 次。\n\n\n(xyz)\n字符组,按照确切的顺序匹配字符 xyz。\n\n\n|\n分支结构,匹配符号之前的字符或后面的字符。\n\n\n\\\n转义符,它可以还原元字符原来的含义,允许你匹配保留字符 `[ ] ( ) { } . * + ? ^ $ \\\n\n\n^\n匹配行的开始\n\n\n$\n匹配行的结束\n\n\n\n简写字符集,这个比较常用,因为确实这类简写字符集很方便,省代码! 以下只要 Perl 全部支持,POSIX 支持不友好!\n\n\n\n\n简写\n描述\n\n\n\n.\n匹配除换行符以外的任意字符\n\n\n\\w\n匹配所有字母和数字的字符:[a-zA-Z0-9_]\n\n\n\\W\n匹配非字母和数字的字符:[^\\w]\n\n\n\\d\n匹配数字:[0-9]\n\n\n\\D\n匹配非数字:[^\\d]\n\n\n\\s\n匹配空格符:[\\t\\n\\f\\r\\p{Z}]\n\n\n\\S\n匹配非空格符:[^\\s]\n\n\n3. 正则表达式二 (分组)说实话,业务中我遇到分组的情况也不少,但是大多数人也基本不会用,也就是写个基础正则罢了!分组作用就是将正则分为多个组,然后我们可以取每个组内部的东西!举个例子,比如我要匹配一个文本 2020年 01月 02日 ,我要第一匹配,第二取出来年月日!\nimport (\t"regexp"\t"testing"\t"github.com/anthony-dong/go-sdk/commons"\t"github.com/stretchr/testify/assert")func TestMatch(t *testing.T) {\tre := regexp.MustCompile(`^\\d+年\\s*\\d{1,2}月\\s*\\d{1,2}日$`)\tassert.Equal(t, re.MatchString("2020年 01月 02日"), true)\tassert.Equal(t, re.MatchString("2020年01月02日"), true)\tassert.Equal(t, re.MatchString("2020年\t01月\t02日"), true)\tassert.Equal(t, re.MatchString("01月02日"), false)}func TestGroup(t *testing.T) {\tre := regexp.MustCompile(`^(\\d+)年\\s*(\\d{1,2})月\\s*(\\d{1,2})日$`)\tresult := re.FindStringSubmatch("2020年 01月 02日")\tt.Logf("%#v\\n", result)\tassert.Equal(t, len(result), 4)\tt.Logf("年: %s, 月 %s, 日: %s\\n", result[1], result[2], result[3])}// output:// regexp_test.go:22: []string{"2020年 01月 02日", "2020", "01", "02"}// regexp_test.go:24: 年: 2020, 月 01, 日: 02func TestNameGroup(t *testing.T) {\tre := regexp.MustCompile(`^(?P<year>\\d+)年\\s*(?P<month>\\d{1,2})月\\s*(?P<day>\\d{1,2})日$`)\tresult := re.FindStringSubmatch("2020年 01月 02日")\tnames := re.SubexpNames()\tfor _, elem := range names {\t\tt.Logf("sub exp name: %s\\n", elem)\t}\tmapData := make(map[string]string)\tfor index, elem := range names {\t\tif index == 0 {\t\t\tcontinue\t\t}\t\tmapData[elem] = result[index]\t}\tt.Logf("%s\\n", commons.ToJsonString(mapData))}// output:// regexp_test.go:35: sub exp name: // regexp_test.go:35: sub exp name: year// regexp_test.go:35: sub exp name: month// regexp_test.go:35: sub exp name: day// regexp_test.go:44: {"day":"02","month":"01","year":"2020"}\n\n不过Go语言也提供了一些高阶用法,这里根据需求进行使用!\n# re 表示正则表达式(re) 最简单的分组使用法,通过index获取(?P<name>re) 可以对分组进行命名(?:re) \t不会对当前分组进行捕获(?flags)\t设置当前所在分组的标志,不捕获也不匹配\n\n4. 正则表达式标准分类正则表达式也是经历了不断的发展,目前日趋完善,目前主流计算机高级语言都是使用的perl标准,未来perl也会成为正则表达式的标准(不过目前就是)!但是说是标准但是毕竟还是有些工具仍然使用POSIX! 所以我们讲一下区别!\n\nPOSIX or BRE( Basic Regular Expression)\nPOSIX Extended or ERE (Extended Regular Express)\nPerl or PCRE (Perl Compatible Regular Expression)\n\n\n目前常见的grep 就是用的 POSIX 标准,而 grep -E 或者 egrep 就是用的 POSIX Extended 标准了!像linux命令目前基本上都是走的POSIX 标准,有些可能会拓展POSIX Extended !\n\n\n\n主要区别就是转义字符的区别了, 像POSIX 的转义字符有 .、\\、[、^、$、* , 但是 POSIX Extended 多了7个需要转义的字符 (、)、{、}、+、?、| 其实说白了就是不支持 字符组和花括号还有 +和? !\n\n\n\n\n\nPerl 和 POSIX主要区别也很简单, 就是不支持简写字符集!但是Go语言好像是POSIX完全不允许简写字符集!所以假如我们使用POSIX还是不用简写字符集吧!\n\nfunc TestPOSIX(t *testing.T) {\t// 这里不允许使用 perl 的 `\\d` 之类的....\tassert.Equal(t, regexp.MustCompilePOSIX(`^[0-9]+`).MatchString("123abc"), true)\t// panic\tassert.Equal(t, assert.Panics(t, func() {\t\tregexp.MustCompilePOSIX(`^[\\d]+`).MatchString("123abc")\t}), true)}func TestPerl(t *testing.T) {\tassert.Equal(t, regexp.MustCompile(`^[\\d]+`).MatchString("123abc"), true)}\n\n5. 总结 总结一些,假如我们现在要写一个正则,那么需要确定是否使用Perl,如果不是那么我们需要确认是否支持 POSIX Extended !然后就是我们别用简写字符集就行了!\n还有断言我没有讲到,所以这里就偷懒了!后续补充!日常中确实没用到断言!\n6. 参考文章\n正则表达式学习\n\nBRE和ERE区别\n\nGo语言标准库 regexp 学习\n\n\n7. bashbash 和sh的区别在于,sh遵守POSIX标准,这里我们一般都使用bash,其次大部分命令的话推荐使用 posix (mac上很多命令并不遵守posix)\n","categories":["Linux"],"tags":["Linux","grep","awk","sed","正则表达式"]},{"title":"Elasticsearch 基础、概念、原理学习","url":"/2021/03/17/cacf73d580b314c8a4c95660b46a7178/","content":" es作为 db、搜索、alap以及成熟的社区,已经越来越成为后端比较成熟的技术栈了,业务中由于需要大量聚合操作,来弥补传统关系型数据(My-SQL)的性能不足,行数据库的劣势,往往会以es作为辅助的存储工具,因此深入学习es基本概念,原理对于日常开发有很大的帮助!对于SQL-Body来说,es支持SQL语法,还是相当给力的!\n 由于我们公司es集群基本使用的是 6.8.8版本,所以全部学习资料基于这个版本学习!\n\n\n1、官方文档6.8版本的官方文档,推荐大家学习的时候详细阅读一下!! , 中文版可能只有2.x版本的!\n2、docker安装ES环境\n 这里不去做集群节点,本文只是做练习,所以不去搭建那么多节点!(docker是个好工具,对于本地学习软件)\n\n# 创建bridge网络docker network create elasticsearch# 创建es(signle-node)docker run -d --rm --name elasticsearch -p 9200:9200 -p 9300:9300 --network elasticsearch -e "discovery.type=single-node" elasticsearch:6.8.8# 创建kibana(web-console)docker run -d --rm --name kibana -p 5601:5601 --network elasticsearch kibana:6.8.8\n\n3、相关概念1、索引\n elastic-search 的基本概念: 索引(index) -> type(类型) -> document(文档) -> field (字段) 和关系型数据库的关系如下:\n\n\nRelational DB -> Databases -> Tables -> Rows -> Columns\nElasticsearch -> Indices -> Types -> Documents -> Fields\n\n但是其实开发上实际上不允许一个索引创建多个类型的,所以也就是为什么后期es废弃了type,最终到8.X版本废弃掉了!原因其实根据es底层有关,影响检索效率,这个和es存储于Lucene 的关系了\n\n在6.x版本只支持一个索引一个type,在7.x版本移除了 type ,8.x彻底废弃,主要原因还是因为lucene的底层设置问题,可以看一下官方文档:https://www.elastic.co/guide/en/elasticsearch/reference/6.8/removal-of-types.html\n\n如何创建一个索引呢 首先先要pass掉那种直接插入数据进行创建索引的,对于线上业务来说不允许开发者去以这种方式去创建索引的,es可以做控制!其次就是创建索引6.x版本后只能创建一个type,推荐type设置为 _doc,其次就是指定属性了\n 这个例子是创建一个my_index索引,然后创建一个_doc 类型,字段是 full_name,类型为 text\nPUT my_index{ "mappings": { "_doc": { "properties": {# 属性 "full_name": { # 字段名称 "type": "text" # 字段属性控制,具体根据官方配置走 } } } }}\n\n关于更多索引的配置可以参考:mapping字段类型 和 mapping 的字段参数\n核心关注的几个点吧,1、字段的类型 type,2、字段是否可以被索引(默认true)由index控制,3、字段的分词器 analyzer,4、fields 属性(text类型特有的)\n索引的类型使用就不介绍了,这个根据经验有关,主要有基本类型,数组,对象,geo,\n以日志收集来说\n{ "filebeat-xxxxxxx-2021.03.11": { "mappings": { "doc": { "properties": { "@timestamp": { "type": "date" }, "agent": { "type": "object" }, "ecs": { "type": "object" }, "fields": { "properties": { "log_type": { "type": "text", "fields": { "keyword": { "type": "keyword", "ignore_above": 256 } } } } }, "host": { "properties": { "name": { "type": "text", "fields": { "keyword": { "type": "keyword", "ignore_above": 256 } } } } }, "input": { "type": "object" }, "log": { "properties": { "file": { "type": "object" } } }, "message": { "type": "text", "fields": { "keyword": { "type": "keyword", "ignore_above": 256 } } } } } } }}\n\n数据:\n\n2、字段业务中通常关注的是字段设置,因为字段关系到你的数据结构设计,掌握好es的数据结构很重要,下面这个例子我会大概展示如何设置一个结构体\n复杂对象如何存储/检索1、存储主要是采用json的扁平化\nPUT my_index/_doc/1{ "region": "US", "manager": { "age": 30, "name": { "first": "John", "last": "Smith" } }}\n\n=> 存储到es中由于Lucene没有对象检索这种概念,所以会进行扁平化存储\n{ "region": "US", "manager.age": 30, "manager.name.first": "John", "manager.name.last": "Smith"}\n\n2、检索的话和普通字段基本就没有差异了,只要准寻扁平化字段进行检索\n参考:https://www.elastic.co/guide/en/elasticsearch/reference/6.8/object.html\n对象数组如何存储/检索es里面叫做 nested ,中文名称叫做嵌套\n类似于下面的数组,对于es来说,到底是怎么存储的呢???\nPUT my_index/_doc/1{ "group" : "fans", "user" : [ { "first" : "John", "last" : "Smith" }, { "first" : "Alice", "last" : "White" } ]}\n\nes会将其存储为\n{ "group" : "fans", "user.first" : [ "alice", "john" ], "user.last" : [ "smith", "white" ]}\n\n因为这里就会有个问题了,那么我查询咋查哇,如何确定唯一,比如查询Alice-Smith,但是其实查询出内容\nGET my_index/_search{ "query": { "bool": { "must": [ { "match": { "user.first": "Alice" }}, { "match": { "user.last": "Smith" }} ] } }}\n\nfields 字段的作用\n 是我基于官方文档对于这个概念的理解,文档: https://www.elastic.co/guide/en/elasticsearch/reference/6.8/multi-fields.html\n\n功能一:聚合1、加入要做keyword了,比如说你的日志可能分类型记录,比如请求超时,请求无权限,请求参数错误,对于这种简单的参数进行聚合统计,但是由于日志需要做全文检索,所以不能设置为 keyword,这里就使用 fields !\nDELETE /my_indexPUT /my_index{ "settings": { "number_of_replicas": 1, "number_of_shards": 5 }, "mappings":{ "_doc":{ "properties":{ "message":{ "type":"text", "fields":{ "keyword": { "type": "keyword", "ignore_above": 10 } } } } } }}POST /my_index/_doc{ "message":"请求超时"}POST /my_index/_doc{ "message":"业务日志:name: xiaoli, id: 111"}POST /my_index/_doc{ "message":"请求无权限"}POST /my_index/_doc{ "message":"请求参数错误"}\n\n2、进行检索:\n2.1、使用kibana会自动告诉你keywork,进行聚合统计\n\n2.2、我还可以通过日志进行全文检索\n\n3、但是对于es来说,如果你没有指定mapping去创建一个索引,\nPOST /test_index/_doc{ "message":"hello"}GET /test_index/_mapping{ "test_index" : { "mappings" : { "_doc" : { "properties" : { "message" : { "type" : "text", "fields" : { "keyword" : { "type" : "keyword", "ignore_above" : 256 } } } } } } }}\n\n可以看到对于 text类型,默认会支持 256个字符的keyword,聚合检索\n功能二:分词\n 业务上一个字段可能使用多种分词,这里就支持分词属性\n\nPUT my_index{ "mappings": { "_doc": { "properties": { "text": { "type": "text", "fields": { "english": { "type": "text", "analyzer": "english" } } } } } }}\n\n3、相关操作1、插入\n\n megacorp 表示index,employee 表示类型,3表示id-document\n\nPUT \t/megacorp/employee/3{ "first_name": "Douglas", "last_name": "Fir", "age": 35, "about": "I like to build cabinets", "interests": [ "forestry" ]}\n\n2、查询\nGET \t/megacorp/employee/_search{ "query": { "match": { "first_name": "Anthony" } }}\n\n3、过滤查询\n\n过滤查询已被弃用,并在ES 5.0中删除。现在应该使用bool / must / filter查询。\n\n4、更新\nPOST /megacorp/employee/3/_update{ "doc":{ "age":22, "deatil":"my name is ...." }}\n\n加入添加一个字段,但是这个索引的mapping不变\n健康状态:Elasticsearch 集群和索引健康状态及常见错误说明\n","categories":["存储"],"tags":["Elasticsearch"]},{"title":"Protocol Buffers协议讲解","url":"/2022/01/16/cc45d69abc6417303d451f43acf099d9/","content":" Protobuf 主要是以数据编码小为著名,主要是用于数据交互和数据存储等,降低带宽、磁盘、移动端网络环境较差减少报文大小等场景,关于序列化速度主要是取决于你用的sdk,所以本文不会关心序列化速度!本文将以proto3语法进行介绍!并且也介绍了如何使用pb规范的定义接口,以及对比了pb2/pb3差别!如果你还对Thrift感兴趣,可以看我这边文章: Thrift协议讲解!\n\n\n1. 协议讲解\npb3 与 pb2差别:\n\n\npb3 对于基本类型已经约定了默认值,会把 0/ ""/false/枚举为0的值 在序列化的时候不进行编码,也就是无法区分这个值是否设置了!\npb3 后期支持了 optional,但是需要在编译的时候指定--experimental_allow_proto3_optional !\npb3 不支持 required 关键字,不推荐业务定义required! \npb3 不支持默认值设置,pb3中默认值都是约定好的,以及不支持group message!\npb3 的枚举类型的第一个字段必须为 0!\npb3 和 pb2 是可以混合使用的!pb3和pb2基本上压缩率和性能上无差别!\n\n\nlabels\npb2\npb3\n备注\n\n\n\nrequired\n支持\n不支持\n\n\n\noptional\n支持\n支持\n\n\n\nsingular (类似于thrift default)\n不支持\n支持\n\n\n\nrepeated\n支持\n支持\n\n\n\noneof\n支持\n支持\n\n\n\nmap\n支持\n支持\n\n\n\nextend\n支持\n不支持\n\n\n\n\n\n选择上来说就是看你是否需要 null和默认值!如果需要那就pb2,不行就pb3!\n\npb3基本上语法如下,具体可以看官方文档: https://developers.google.com/protocol-buffers/docs/proto3 , 例如下面的test.proto 文件\n\nsyntax = "proto3";message TestData { enum EnumType { UnknownType = 0; // 必须以0开始! Test1Type = 1; Test2Type = 2; } message TestObj { int64 t_int64 = 1; } string t_string = 1; int64 t_int64 = 2; bool t_bool = 3; fixed64 t_fix64 = 4; repeated int64 t_list_i64 = 5; map<int64, string> t_map = 6; EnumType t_enum = 7; TestObj t_obj = 8 ; repeated TestObj t_list_obj = 9 ; map<string, TestData> t_map_obj = 10; repeated string t_list_string = 11;}\n\n\n如何编译了? 如果是Go的话可以下面这种方式编译!记住提前下载好 protoc-gen-go 和 protoc-gen-go-grpc , 源码地址: protobuf-go\n\n# install protoc & protoc-gen-go & protoc-gen-go-grpcwget https://github.com/protocolbuffers/protobuf/releases/download/v3.17.3/protoc-3.17.3-osx-x86_64.zipgo get -v google.golang.org/protobuf/cmd/protoc-gen-gogo get -v google.golang.org/grpc/cmd/protoc-gen-go-grpc# 编译上面的'test.proto'文件protoc \\--experimental_allow_proto3_optional \\--proto_path=. \\--plugin=protoc-gen-go=${HOME}/go/bin/protoc-gen-go \\--go_opt=Mtest.proto=github.com/anthony-dong/go-tool/internal/example/protobuf/test \\--go_out=${HOME}/go/src \\--plugin=protoc-gen-go-grpc=${HOME}/go/bin/protoc-gen-go-grpc \\--go-grpc_opt=Mtest.proto=github.com/anthony-dong/go-tool/internal/example/protobuf/test \\--go-grpc_out=${HOME}/go/src \\test.proto\n\n\npb 序列化核心用到的思想就是varint + zigzap, 具体可以看官方文章: https://developers.google.com/protocol-buffers/docs/encoding\n本文的目标是可以做到简单的序列化 message 和 反序列化message!\n目前Go主要有两个PB库,一个是V1版本的: https://github.com/golang/protobuf,一个是V2版本的: https://github.com/protocolbuffers/protobuf-go , 都属于官方实现!\n\n2. 编码+解码关于消息各个类型的编码逻辑: https://github.com/protocolbuffers/protobuf-go/tree/master/internal/impl\n核心思想: \n\nvarint,根据数字的大小进行动态编码,可以减少字节数的占用,采用 msb(the Most Significant Bit) 最高有效位,整个过程可以理解为大端->小端转换,具体可以看后面讲述!\nzigzag 由于负数的最高高位永远是1,导致-1占用8字节,比较浪费,所以zigzag做了一个映射,负数可以映射成正数,正数还是正数,具体可以看后面讲述!\n\n1. 简单例子(学习目标)下面是一个测试用例,可以看到一个是通过PB自带的库编码,一个是我们自己实现的编码,我们这篇文章目标是可以自己实现编码!\n// 使用pb 序列化func Test_Marshal_Data(t *testing.T) {\tvar request = test.TestData{\t\tTString: "hello", // 1:string\t\tTInt64: 520, //8:int64\t\tTObj: &test.TestData_TestObj{ //8:message\t\t\tTInt64: 520, // 1:int64\t\t},\t}\tmarshal, err := proto.Marshal(&request)\tif err != nil {\t\tt.Fatal(err)\t}\tt.Log(hex.Dump(marshal))\t// 00000000 0a 05 68 65 6c 6c 6f 10 88 04 42 03 08 88 04 |..hello...B....|}// 自己编码完成!func TestMarshal_Data_Custom_Test(t *testing.T) {\t// 注释语法\t// field_id:field_type:wire_type\t// size(field_value)\t// field_type=field_value\tbuffer := bytes.NewBuffer(make([]byte, 0, 1024))\tbinary.Write(buffer, binary.BigEndian, uint8(0x0a)) // 1:string:WireBytes, 0000 1010 = 0x0a\tbinary.Write(buffer, binary.BigEndian, uint8(0x05)) // size(string) = 5\tbinary.Write(buffer, binary.BigEndian, []byte("hello")) // string='hello', 68 65 6c 6c 6f\tbinary.Write(buffer, binary.BigEndian, uint8(0x10)) // 2:int64:WireVarint, 0001 0000 = 0x10\tbinary.Write(buffer, binary.BigEndian, uint16(0x8804)) // int64=520, 0000 0010 0000 1000 => 1000 1000 0000 0100 = 0x8804\tbinary.Write(buffer, binary.BigEndian, uint8(0x42)) // 8:message:WireBytes, 0100 0010 = 0x42\tbinary.Write(buffer, binary.BigEndian, uint8(0x03)) // size(message) = 3\tbinary.Write(buffer, binary.BigEndian, uint8(0x08)) // 1:int64:WireVarint, 0000 1000=0x08\tbinary.Write(buffer, binary.BigEndian, uint16(0x8804)) // int64=520, 0000 0010 0000 1000 => 1000 1000 0000 0100 = 0x8804\tt.Log(hex.Dump(buffer.Bytes()))\t// 00000000 0a 05 68 65 6c 6c 6f 10 88 04 42 03 08 88 04 |..hello...B....|}\n\n2. Message 编码介绍1. 介绍消息是由 field_id 和 field_value组成,但是pb支持的类型比较多,考虑到编码的时候很多类型其实有相似的逻辑,因此pb对于类型进行了二次归类,叫做wire type,也就是 field_id和wire_type 组合成一个字段用varint 进行编码!\nfield id, wire_type used varint encode+--------+...+--------+--------+--------+...+--------+| field id |dddddttt| field value |+--------+...+--------+--------+--------+...+--------+\n\n\nfield id + dddddttt 一共是 1-4个字节,用的是 varint 编码!具体Go的代码实现如下\n\n// EncodeTagAndWireType encodes the given field tag and wire type to the// buffer. This combines the two values and then writes them as a varint.func (b *Buffer) EncodeTagAndWireType(fieldId int32, wireType int8) error {\tv := uint64((int64(fieldId) << 3) | int64(wireType))\treturn b.EncodeVarint(v)}// DecodeTagAndWireType decodes a field tag and wire type from input.// This reads a varint and then extracts the two fields from the varint// value read.func (cb *Buffer) DecodeTagAndWireType() (tag int32, wireType int8, err error) {\tvar v uint64\tv, err = cb.DecodeVarint()\tif err != nil {\t\treturn\t}\t// low 7 bits is wire type\twireType = int8(v & 7)\t// rest is int32 tag number\tv = v >> 3\tif v > math.MaxInt32 {\t\terr = fmt.Errorf("tag number out of range: %d", v)\t\treturn\t}\ttag = int32(v)\treturn}\n\n\nttt 为3bit表示wire_type,也就是最多表示1<<3 -17种类型,包含000 ,也就是8种类型,具体可以看官方文档: wire types 介绍!\n\n\nconst (\tWireVarint = 0\tWireFixed32 = 5\tWireFixed64 = 1\tWireBytes = 2\tWireStartGroup = 3\tWireEndGroup = 4)// 映射关系,就是 字段真实类型 -> 序列化类型func MustWireType(t descriptor.FieldDescriptorProto_Type) int8 {\twireType, err := GetWireType(t)\tif err != nil {\t\tpanic(err)\t}\treturn wireType}func GetWireType(t descriptor.FieldDescriptorProto_Type) (int8, error) {\tswitch t {\tcase descriptor.FieldDescriptorProto_TYPE_ENUM,\t\tdescriptor.FieldDescriptorProto_TYPE_BOOL,\t\tdescriptor.FieldDescriptorProto_TYPE_INT32,\t\tdescriptor.FieldDescriptorProto_TYPE_SINT32,\t\tdescriptor.FieldDescriptorProto_TYPE_UINT32,\t\tdescriptor.FieldDescriptorProto_TYPE_INT64,\t\tdescriptor.FieldDescriptorProto_TYPE_SINT64,\t\tdescriptor.FieldDescriptorProto_TYPE_UINT64:\t\treturn proto.WireVarint, nil\tcase descriptor.FieldDescriptorProto_TYPE_FIXED32,\t\tdescriptor.FieldDescriptorProto_TYPE_SFIXED32,\t\tdescriptor.FieldDescriptorProto_TYPE_FLOAT:\t\treturn proto.WireFixed32, nil\tcase descriptor.FieldDescriptorProto_TYPE_FIXED64,\t\tdescriptor.FieldDescriptorProto_TYPE_SFIXED64,\t\tdescriptor.FieldDescriptorProto_TYPE_DOUBLE:\t\treturn proto.WireFixed64, nil\tcase descriptor.FieldDescriptorProto_TYPE_BYTES,\t\tdescriptor.FieldDescriptorProto_TYPE_STRING,\t\tdescriptor.FieldDescriptorProto_TYPE_MESSAGE:\t\treturn proto.WireBytes, nil\tcase descriptor.FieldDescriptorProto_TYPE_GROUP:\t\treturn proto.WireStartGroup, nil\tdefault:\t\treturn 0, fmt.Errorf("not support pb type: %d", t)\t}}\n\n\nfield value 就是字段内容了,下面会详细介绍每一种对应的!\n\nWireVarint 写的时候采用varint 编码,可变1-10字节WireFixed32 写的时候会进行小端转换,固定4字节WireFixed64 写的时候会进行小端转换,固定8字节WireBytes 写的时候正常写出字节流即可!WireStartGroup / WireEndGroup 不进行介绍了!\n\n2. 总结\n这里谈个小技巧,其实看协议编码这种源码的时候,很多位运算,其实一般来说 |表示set bit操作, &表示get bit操作!\n这里再补充下为啥最大字段是2^29-1,是因为nuber最大是ui32编码,然后有3bit用作msb,就剩余29位了,所以就是 2^29-1了!\n这就是为什么pb中1-15字段可以使用一个字节存储,是因为 var int只有7字段存储数据,但是3bit存储wire_type ,所以剩余的4bit存储字段ID,也就是 1<<4 -1 = 15 个字段了!\n\n3. varint 编码介绍wiki介绍 https://en.wikipedia.org/wiki/Variable-length_quantity ,整体概述一下就是对于无符号整数来说,很多时候都是浪费字节,比如uint64 占用 8字节,值为1是占用8字节,值为1<<64 - 1也是一样,那么varint就是解决这个问题了,可以用1-10个字节进行表示!核心思想就是使用低位的7bit表示数据,高位1bit表示msb(The Most Significant Bit, 最高有效位),最小1个字节,最大10个字节表示 int64 !\npb中类型为如下类型都会采用varint 编码 , 枚举等同于int32!\nvarint := int32 | int64 | uint32 | uint64 | bool | enum, encoded as varints\n\n1. 例子1比如: data=15 -> 0000 1111, \n编码逻辑:\nvarint 表示为 0000 1111,是因为他能够用7字节表示!所以不需要设置 msb!\n解析逻辑:\n我们拿到 0000 1111 取出msb 发现1 ,这里拿到msb有多种方式,可以比较大小,也能通过位运算进行取,比如 0000 1111 & 1<<7 == 0 就可以说明没有设置msb,然后取出低7位即是真实数据,这里由于8位也是0其实可以忽略这个操作!\n2. 例子2比如 data=520 -> 0000 0010 0000 1000 (大端表示法,低位在高地址)\n编码逻辑:\n首先确定520是7个bit放不下,所以先取出 前7个字节( data & (1<<7) - 1) = 000 1000,然后设置msb 1000 1000, 这个是第一轮;\n第二轮剩余字节 0000 0010 0= 4 , 发现4可以用7个字节放下,所以是 0000 0100\n所以最终结果是 1000 1000 0000 0100 ,也就是 [136,4],这个过程可以理解为是大端 -> 小端的一个过程!\n解析逻辑:\n首先varint 其实输出的是一个小端表示法,因此我们需要从低位开始!\n首先是取出第一个字节1000 1000 ,发现msb,然后得到结果是 000 1000 = 8 \n然后是取出第二个字节0000 0100,发现不是msb,然后得到结果 000 0100,我们需要将它放到 000 1000后面去!怎么做了,其实跟简单 000 0100 << 7 | 000 1000 即可得到结果是 000 0100 000 1000 = 0000 0010 0000 1000 。 这个逻辑可以理解为是小端->大端的一个过程\n3. 代码实现func (p *pbCodec) EncodeVarInt(data int64) error {\t// 1. 取出低7位(如果7个字节不可以放下!)\t// 2. 然后设置高8位标识符号\t// 3. 然后右移\tfor data > (1<<7 - 1) {\t\tp.buffer = append(p.buffer, byte(data&(1<<7-1)|(1<<7)))\t\tdata >>= 7\t}\tp.buffer = append(p.buffer, byte(data))\treturn nil}func (p *pbCodec) DecodeVarInt() (int64, error) {\tvar (\t\tx int64\t\tn = 0\t)\tdefer func() {\t\tp.buffer = p.buffer[n:]\t}()\tfor shift := uint(0); shift < 64; shift += 7 { // 偏移量从0开始,每次+7\t\tif n >= len(p.buffer) {\t\t\treturn 0, fmt.Errorf("not enough buffer")\t\t}\t\t// 1. 取出第一个自己\t\t// 2. 然后取出低7位\t\t// 3. 然后由于数据是小端,所以取出的数据需要移动偏移量\t\t// 4. 然后设置进去原来的数据中!\t\tb := int64(p.buffer[n])\t\tn++\t\tx |= (b & 0x7F) << shift\t\tif (b & 0x80) == 0 {\t\t\treturn x, nil\t\t}\t}\treturn 0, fmt.Errorf("proto integer overflow")}\n\n4. 非 varint 编码类型1. fixed 64/32 类型 (小端)其实就是用小端进行传输!例如fixed64 = 520 小端编码后如下,为此为了和varint进行区分,所以定了两个wire type=WireFixed32|WireFixed64\n# fix64 520 占用 8 字节00 00 00 00 00 00 02 08# 编码后08 02 00 00 00 00 00 00\n\n例如Go代码的具体实现, 这里以 64位为例子\nimport "encoding/binary"// 写的时候可以通过如下binary.Write(bf, binary.LittleEndian, uint64(520))// 读的时候可以通过如下实现var data uint64binary.Read(bf, binary.LittleEndian, &data)\n\n2. double / float 类型同上面的fixed 64/32 ,double需要转换为 fixed64 , float需要转换为fixed32, 具体 float -> uint Go的转换代码实现:\nimport "math"math.Float32bits(v)math.Float64bits(v)\n\n3. string / bytes / message / packed 类型string 和 bytes 都是变长,所以需要先写长度(var int)编码,再写payload,如果是string的话需要utf-8编码!\nmessage 类型也是采用的如下编码,所以在PB里无法通过二进制报文直接解析!\ndelimited := size (message | string | bytes | packed), size encoded as varintmessage := valid protobuf sub-messagestring := valid UTF-8 string (often simply ASCII); max 2GB of bytesbytes := any sequence of 8-bit bytes; max 2GB\n\ndelimited := size (message | string | bytes | packed), size encoded as varint# size bytes+--------+...+--------+--------+...+--------+| byte length | bytes |+--------+...+--------+--------+...+--------+\n\n5. zigzag 编码 ( sint32 / sint64)前面讲的varint并不是万能的,因为数据往存在负数,而负数二进制最高位都是1,所以导致varint编码后数据都很大,所以需要zigzag编码,它可以帮负数转换成正数,正数转换成正数!而且基于位运算效率很高,所以pb提出了sint32、sint64编码,解决这个问题,核心其实就是使用了 zigzag 编码!\n例如: int64 类型,值为 -1, varint 编码是:ff ff ff ff ff ff ff ff ff 01 满满的占用了10个字节! 但是假如是 sint64 类型,值为 -1, zigzag 编码后值为01,然后varint编码后是 01, 此时就节省了9个字节!\nzigzag 编码其实很简单就是类似于做了层映射!用无符号的一半表示正数一半表示负数!\n\n具体算法用Go写大改如下:\n// EncodeZigZag64 does zig-zag encoding to convert the given// signed 64-bit integer into a form that can be expressed// efficiently as a varint, even for negative values.func EncodeZigZag64(v int64) uint64 {\treturn (uint64(v) << 1) ^ uint64(v>>63)}// EncodeZigZag32 does zig-zag encoding to convert the given// signed 32-bit integer into a form that can be expressed// efficiently as a varint, even for negative values.func EncodeZigZag32(v int32) uint64 {\treturn uint64((uint32(v) << 1) ^ uint32((v >> 31)))}// DecodeZigZag32 decodes a signed 32-bit integer from the given// zig-zag encoded value.func DecodeZigZag32(v uint64) int32 {\treturn int32((uint32(v) >> 1) ^ uint32((int32(v&1)<<31)>>31))}// DecodeZigZag64 decodes a signed 64-bit integer from the given// zig-zag encoded value.func DecodeZigZag64(v uint64) int64 {\treturn int64((v >> 1) ^ uint64((int64(v&1)<<63)>>63))}\n\n\n异或:相同为0,相异为1\n\n例如下面例子,将-1 和 1 进行zigzag 编码后:\n# -11111 1111 1111 1111 1111 1111 1111 1111# d1=uint32(n) << 11111 1111 1111 1111 1111 1111 1111 1110# d2=uint32(n >> 31) (负数左移添加1)1111 1111 1111 1111 1111 1111 1111 1111# d1 ^ d20000 0000 0000 0000 0000 0000 0000 0001# 10000 0000 0000 0000 0000 0000 0000 0001#n<<10000 0000 0000 0000 0000 0000 0000 0010#n>>310000 0000 0000 0000 0000 0000 0000 0000# 输出0000 0000 0000 0000 0000 0000 0000 0010\n\n6. repeated (list)上文都没有讲解到 集合类型,protbuf 提供了 repeated关键字来提供list类型!关于 repeated 具体编码实现有两种:\n\npacked ( pb3默认会根据字段类型选择packed, pb2 v2.1.0 引入的,具体可以参考官方文档: packed介绍 )\n\nunpacked \n\n\n目前pb中支持 wire_type=WireVarint|WireFixed32|WireFixed64进行 packed编码!\n其实可以思考一下为啥!首先假如是WireBytes 类型,那么我数据量很大,比如一个bytes数据最大可以写2G,那么我写出的时候假如用packed编码,会存在一个问题就是我写3条数据,需要内存中积压6G数据,然后算出总大小,再写出去,占用内存很大,而且解码的时候也是!PB考虑的可真细致!\n1. packed 编码可以根据官网提供的demo为例子:\nmessage Test4 { repeated int32 d = 4 [packed=true];}\n\n假如d= [3, 270,86942] ,编码d字段的时候,会进行如下操作,先写 field_number 和 wire_type 然后再去写整个payload 大小,最后再写每一个元素!\n22 // key (field number = 4, wire type = 2 WireBytes)06 // payload size (6 bytes)03 // first element (varint 3)8E 02 // second element (varint 270)9E A7 05 // third element (varint 86942)\n\n2. unpacked 编码message Test5 { repeated int32 d = 4 [packed = false];}\n\n还是以字段d= [3, 270,86942] 进行编码,可以看到是会把每个元素作为单独的整体进行写出,比如元素一会写field_type and wire_type,然后再写 field_value,依次!!\n00000000 20 03 20 8e 02 20 9e a7 05 | . .. ...|20 // key (field number 4, wire type=proto.WireVarint)03 // (varint 3)20 // key (field number 4, wire type=proto.WireVarint)8e 02 // (varint 270)20 // key (field number 4, wire type=proto.WireVarint)9e a7 05 // (varint 86942)\n\n7. map其实在PB中map实际上就是 repeated kv message,可以通过FieldDescriptor可以看到!\n{ "name": "TestData", "field": [ { "name": "t_map", "number": 6, "label": 3, "type": 11, "type_name": ".TestData.TMapEntry", "json_name": "tMap" }, ], "nested_type": [ { "name": "TMapEntry", "field": [ { "name": "key", "number": 1, "label": 1, "type": 3, "json_name": "key" }, { "name": "value", "number": 2, "label": 1, "type": 9, "json_name": "value" } ], "options": { "map_entry": true } } ], "enum_type": []}\n\n所以编码的时候也很简单,例如\nmessage TestMapData1 {// 这里无法定义 TMapEntry,会报错! map<int64, string> t_map = 6;}==> 实际上是生成了这个代码!message TestMapData2 { message TMapEntry { int64 key = 1; string value = 2; } repeated TMapEntry t_map = 6;}\n\n所以编码过程是一个repeated k v message的序列化方式!例如下面\nt_map= {1:"1",2:"2"}=>32 05 08 01 12 01 31 32 05 08 02 12 01 32=> 32 // field_number=6 and wire_type=proto.WireBytes05 // entry data length=508 // entry data key field_number=1 and wire_type=proto.WireVarint01 // entry data key_value=varint(1)12 // entry data value field_number=2 and wire_type=proto.WireBytes01 // entry data value len= varint(1)31 // entry data value="1"32 // field_number=6 and wire_type=proto.WireBytes05 // entry data length=508 02 12 01 32 // 同上!\n\n8. field orderpb编码不在意字段的编码顺序,也就是encode的字段顺序不一样会导致输出的数据不一样!但是解析出来的数据是一样的!\n还有就是map的key顺序也会影响!\n所以一般api都会指定是否支持 deterministic,如果设置为true,结果一般都会保证一样,否则可能不一样!\n但是你懂得,实际上效果吧,就是开启之后一定比不开启慢,因为需要进行order!\n3. pb 协议整体概括下面这个是一个类似于bnf范式的东西,具体可以参考: PB Encode 算法\nmessage := (tag value)* You can think of this as “key value”tag := (field << 3) BIT_OR wire_type, encoded as varintvalue := (varint|zigzag) for wire_type==0 | fixed32bit for wire_type==5 | fixed64bit for wire_type==1 | delimited for wire_type==2 | group_start for wire_type==3 | This is like “open parenthesis” group_end for wire_type==4 This is like “close parenthesis”varint := int32 | int64 | uint32 | uint64 | bool | enum, encoded as varintszigzag := sint32 | sint64, encoded as zig-zag varintsfixed32bit := sfixed32 | fixed32 | float, encoded as 4-byte little-endian; memcpy of the equivalent C types (u?int32_t, float)fixed64bit := sfixed64 | fixed64 | double, encoded as 8-byte little-endian; memcpy of the equivalent C types (u?int64_t, double)delimited := size (message | string | bytes | packed), size encoded as varintmessage := valid protobuf sub-messagestring := valid UTF-8 string (often simply ASCII); max 2GB of bytesbytes := any sequence of 8-bit bytes; max 2GBpacked := varint* | fixed32bit* | fixed64bit*, consecutive values of the type described in the protocol definitionvarint encoding: sets MSB of 8-bit byte to indicate “no more bytes”zigzag encoding: sint32 and sint64 types use zigzag encoding.\n\n4. protoc 命令讲解这里讲解一下protoc 的架构图,生成架构图\n\n第一个节点protoc其实是 c++写的, https://github.com/protocolbuffers/protobuf\n第二个节点是 protoc 输出的二进制报文(PB编码) CodeGeneratorRequest ,具体可以看 CodeGeneratorRequest IDL定义,其中 SourceCode 比较难理解,可以看我的这篇文章: SourceCodeInfo介绍 !\n第三个节点是 plugin 生成逻辑,可以通过标准输入获取CodeGeneratorRequest,通过标准输出写入CodeGeneratorResponse\n第四个节点输出 CodeGeneratorResponse,具体可以看 CodeGeneratorResponse IDL定义\n目前虽然有很多项目在解析 protoc 的 ast时,都是自己实现的词法解析和语法解析,所以假如可以把protoc封装个lib库就好了,上层可以用cgo 、 java/python native 去掉用会好很多,这里我自己用cgo封装的libprotobuf, 具体可以看我自己写的项目: https://github.com/Anthony-Dong/protobuf!\n1. 自定义plugin\n自己用Go语言写一个 print CodeGeneratorRequest 的生成器\n\npackage main//build: go build -o $(go env GOPATH)/bin/protoc-gen-print -v .import (\t"io/ioutil"\t"os"\t"strings"\t"google.golang.org/protobuf/encoding/protojson"\t"google.golang.org/protobuf/proto"\t"google.golang.org/protobuf/types/pluginpb")func main() {\treq := pluginpb.CodeGeneratorRequest{}\tresp := pluginpb.CodeGeneratorResponse{}\tstdIn, err := ioutil.ReadAll(os.Stdin)\tif err != nil {\t\tpanic(err)\t}\tif err := proto.Unmarshal(stdIn, &req); err != nil {\t\tpanic(err)\t}\toptions := toKVOption(&req)\tswitch options["type"] {\tcase "pb":\t\tmarshal, err := proto.Marshal(&req)\t\tif err != nil {\t\t\tpanic(err)\t\t}\t\tresp.File = []*pluginpb.CodeGeneratorResponse_File{\t\t\t{\t\t\t\tName: proto.String("desc.binary"),\t\t\t\tContent: proto.String(string(marshal)),\t\t\t},\t\t}\tdefault:\t\tresp.File = []*pluginpb.CodeGeneratorResponse_File{\t\t\t{\t\t\t\tName: proto.String("desc.json"),\t\t\t\tContent: proto.String(MessageToJson(&req, true)),\t\t\t},\t\t}\t}\trespBinary, err := proto.Marshal(&resp)\tif err != nil {\t\tpanic(err)\t}\tif _, err := os.Stdout.Write(respBinary); err != nil {\t\tpanic(err)\t}}func toKVOption(req *pluginpb.CodeGeneratorRequest) map[string]string {\tkvs := strings.Split(req.GetParameter(), ",")\tresult := make(map[string]string, len(kvs))\tfor _, kv := range kvs {\t\tkvv := strings.Split(kv, "=")\t\tif len(kvv) == 2 {\t\t\tresult[kvv[0]] = kvv[1]\t\t} else {\t\t\tresult[kvv[0]] = ""\t\t}\t}\treturn result}func MessageToJson(v proto.Message, pretty ...bool) string {\tops := protojson.MarshalOptions{Multiline: len(pretty) > 0 && pretty[0]}\treturn ops.Format(v)}\n\n\n其中plugin一般遵守命名规则: protoc-gen-{plugin name} , 使用 --plugin 指定自己插件的路径,使用 --{pulgin}_out 指定输出路径,使用 --{pulgin}_opt 指定parameter, 多个请参数用, 分割 \n\nprotoc --proto_path desc --proto_path . \\--plugin=protoc-gen-print=${HOME}/go/bin/protoc-gen-print \\--print_out=${HOME}/data/print \\--print_opt=type=json,k1=v1 \\--print_opt=k2=v2 `find . -name '*.proto'`\n\n2. 解析options (Extensions)\n业务中经常拓展 protobuf.FieldOptions 定义一些注解(元信息),在代码生成中/运行时的时候特殊处理,例如如下我定义的 api.proto 文件\n\nsyntax = "proto2";package api;option go_package = "github.com/anthony-dong/protobuf/internal/pb_gen/api";import "google/protobuf/descriptor.proto";extend google.protobuf.FileOptions{ optional string android_package = 1001;}extend google.protobuf.FieldOptions { optional HttpSourceType source = 50101; // 来自于http 请求的哪个部位 optional string key = 50102; // http 请求的header 还是哪 optional bool unbox = 50103; // 是否平铺开结构体,除body外,默认处理第一层}enum HttpSourceType { Query = 1; Body = 2; Header = 3;}extend google.protobuf.MethodOptions { optional HttpMethodType method = 50201; // http method optional string path = 50202; // http path}enum HttpMethodType{ GET = 1; POST = 2; PUT = 3;}\n\n\n那么我们如何拿到这些信息了?\n\n\n使用protoc生成对应的api.proto 文件产物,对应语言的\n\n读取 options 的实现,具体可以看 GetProtobufOptions 的实现\n\n\npackage mainimport (\t"errors"\t"fmt"\t"reflect"\t"google.golang.org/protobuf/types/pluginpb"\t"github.com/anthony-dong/protobuf/internal/printer/api" // 这个是api.proto的编译产物\t"google.golang.org/protobuf/proto"\t"google.golang.org/protobuf/runtime/protoimpl")func readProtobufOptions(req *pluginpb.CodeGeneratorRequest) {\tfor _, file := range req.GetProtoFile() {\t\tfor _, message := range file.GetMessageType() {\t\t\tif message.GetName() != "ImMessageRequest" {\t\t\t\tcontinue\t\t\t}\t\t\t/**\t\t\timport "api.proto";\t\t\tmessage ImMessageRequest {\t\t\t optional int64 Cursor = 1 [default = 2, (api.source) = Query, (api.key) = '测试注解解析'];\t\t\t optional im.commons.ImCommons ImCommons = 255 [(api.unbox) = true];\t\t\t}\t\t\t*/\t\t\tfor _, field := range message.GetField() {\t\t\t\toutput, err := GetProtobufOptions(field.GetOptions())\t\t\t\tif err != nil {\t\t\t\t\tpanic(err)\t\t\t\t}\t\t\t\tfmt.Printf("%s: %#v\\n", field.GetName(), output)\t\t\t}\t\t}\t}}func GetProtobufOptions(options proto.Message) (map[string]interface{}, error) {\toptionKV := make(map[string]interface{}, 0)\tvar source api.HttpSourceType\tif isOk, err := marshalExtension(options, api.E_Source, &source); err != nil {\t\treturn nil, err\t} else if isOk {\t\toptionKV["api.source"] = source\t}\tvar key string\tif isOk, err := marshalExtension(options, api.E_Key, &key); err != nil {\t\treturn nil, err\t} else if isOk {\t\toptionKV["api.key"] = key\t}\tvar unbox bool\tif isOk, err := marshalExtension(options, api.E_Unbox, &unbox); err != nil {\t\treturn nil, err\t} else if isOk {\t\toptionKV["api.unbox"] = unbox\t}\treturn optionKV, nil}// 参考: https://github.com/lyft/protoc-gen-star/blob/master/extension.go 实现func marshalExtension(opts proto.Message, e *protoimpl.ExtensionInfo, out interface{}) (bool, error) {\tif opts == nil || reflect.ValueOf(opts).IsNil() {\t\treturn false, nil\t}\tif e == nil {\t\treturn false, errors.New("nil *protoimpl.ExtensionInfo parameter provided")\t}\tif out == nil {\t\treturn false, errors.New("nil extension output parameter provided")\t}\to := reflect.ValueOf(out)\tif o.Kind() != reflect.Ptr {\t\treturn false, errors.New("out parameter must be a pointer type")\t}\tif !proto.HasExtension(opts, e) {\t\treturn false, nil\t}\tval := proto.GetExtension(opts, e)\tif val == nil {\t\treturn false, errors.New("extracted extension value is nil")\t}\tv := reflect.ValueOf(val)\tfor v.Kind() == reflect.Ptr || v.Kind() == reflect.Interface {\t\tv = v.Elem()\t}\tfor o.Kind() == reflect.Ptr || o.Kind() == reflect.Interface {\t\tif o.Kind() == reflect.Ptr && o.IsNil() {\t\t\to.Set(reflect.New(o.Type().Elem()))\t\t}\t\to = o.Elem()\t}\tif v.Type().AssignableTo(o.Type()) {\t\to.Set(v)\t\treturn true, nil\t}\treturn true, fmt.Errorf("cannot assign extension type %q to output type %q",\t\tv.Type().String(),\t\to.Type().String())}\n\n\n输出\n\nCursor: map[string]interface {}{"api.key":"cursor", "api.source":1}ImCommons: map[string]interface {}{"api.unbox":true}\n\n\n具体为啥非要解析 api.proto文件才能获取ExtensionInfo 呢,原因很简单,就是desc中并不会有详细的类型信息,只有 id:value 信息,可以看我解析后的信息,这里可以使用 cat /Users/bytedance/data/print/desc.binary | gtool codec pb 进行解析, gtool 可以从这里下载: https://github.com/anthony-dong/go-sdk \n\n[ { "1": "Cursor", "3": 1, "4": 1, "5": 3, "7": "2", "8": { "50101": 1, "50102": "cursor" }, "10": "Cursor" }, { "1": { "9": 1.0640046222664341e+248 }, "3": 255, "4": 1, "5": 11, "6": ".im.commons.ImCommons", "8": { "50103": 1 }, "10": { "9": 1.0640046222664341e+248 } }]\n\n5. pb 其他细节讲解\npackage(包),一个包下不能存在相同定义的message和枚举以及枚举字段!\ninclude(import),基于include_path 作为根目录的relative pata\noption,可以理解为是一些注解,可以对于字段、消息、枚举、method进行标记!\n\n\n这里不推荐在idl里定义 option go_package=xxx java php py 之类的,因为pb在编译期可以指定!如果你们有自己的pb gen plugin可以拓展 descriptor!\n\nsyntax = "proto2";package api;option go_package = "github.com/anthony-dong/go-tool/internal/example/protobuf/idl_example/pb_gen/api";import "google/protobuf/descriptor.proto";extend google.protobuf.FieldOptions { optional HttpSourceType source = 50101; // 来自于http 请求的哪个部位 optional string key = 50102; // http 请求的header 还是哪 optional bool unbox = 50103; // 是否平铺开结构体,除body外,默认处理第一层}enum HttpSourceType { Query = 1; Body = 2; Header = 3;}extend google.protobuf.MethodOptions { optional HttpMethodType method = 50201; // http method optional string path = 50202; // http path}enum HttpMethodType{ GET = 1; POST = 2; PUT = 3;}// extend google.protobuf.EnumValueOptions {// }// extend google.protobuf.EnumOptions {// }// extend google.protobuf.MessageOptions {// }// extend google.protobuf.ServiceOptions {// }\n\n具体可以看我的写的例子:protobuf\n6. proto any 类型例如pb文件\nimport "google/protobuf/any.proto";message TestAnyType { optional google.protobuf.Any any = 1;}message Type1 { string value = 1;}message Type2 { int64 value = 1;}message Type3 { float value = 1;}\n\n此时Value可能是很多类型,比如 Type1 或者Type2或者Type3,所以此时需要一个any类型\n首先我们写代码创建一个 TestAnyType 经过pb编码\nfunc encodeData(t *testing.T) []byte {\t// import "google.golang.org/protobuf/types/known/anypb"\tdata := test.TestAnyType{\t\tAny: &anypb.Any{},\t}\tif err := data.Any.MarshalFrom(&test.Type1{\t\tValue: "11111",\t}); err != nil {\t\tt.Fatal(err)\t}\t// import "google.golang.org/protobuf/proto"\tif result, err := proto.Marshal(&data); err != nil {\t\tt.Fatal(err)\t\treturn nil\t} else {\t\treturn result\t}}\n\n如何使用了?\nfunc TestAnyType(t *testing.T) {\tdata := encodeData(t)\tresult := test.TestAnyType{}\tif err := proto.Unmarshal(data, &result); err != nil {\t\tt.Fatal(err)\t}\tt.Log(result.String())\tany := result.Any\t// 通过 any.MessageIs 判断类型!记住可以使用 ”伪空对象“ 即可!\tif any.MessageIs((*test.Type1)(nil)) {\t\ttype1 := test.Type1{}\t\tif err := any.UnmarshalTo(&type1); err != nil {\t\t\tt.Fatal(err)\t\t}\t\t// handler type1 func\t\tt.Logf("type1: %s\\n", type1.String())\t}\tif any.MessageIs((*test.Type2)(nil)) {\t\ttype2 := test.Type2{}\t\tif err := any.UnmarshalTo(&type2); err != nil {\t\t\tt.Fatal(err)\t\t}\t\t// handler type2 func\t\tt.Logf("type2: %s\\n", type2.String())\t}\tif any.MessageIs((*test.Type3)(nil)) {\t\ttype2 := test.Type3{}\t\tif err := any.UnmarshalTo(&type2); err != nil {\t\t\tt.Fatal(err)\t\t}\t\t// handler type3 func\t\tt.Logf("type3: %s\\n", type2.String())\t}}\n\n7. 参考文章\nprotobuf 编码介绍\nprotobuf编码之varint/zigzag\n微软 API设计规范\nGoogle API设计规范\nrestful api设计指南\n\n","categories":["RPC"],"tags":["GRPC","API","Protobuf"]},{"title":"C++模版","url":"/2023/12/18/d35c07b4c6e5ba2745977d866bfc0e0f/","content":"C++模板是一种强大的工具,主要被用于实现泛型编程。泛型编程允许你编写能够处理任何类型的代码,同时保持类型安全和性能。\n\n\n介绍C++模板是一种强大的工具,主要被用于实现泛型编程。泛型编程允许你编写能够处理任何类型的代码,同时保持类型安全和性能。以下是模板在日常开发中的一些常见应用场景:\n\n容器类:C++标准库中的许多工具,如std::vector,std::map,std::array等,都使用模板来处理任意类型的数据。\n算法:标准库中的算法如std::sort,std::find等,都使用模板来处理容器中的任意类型的数据。\n智能指针:如std::unique_ptr、std::shared_ptr等,都用模板实现,可以用来处理任何类型的数据。\n元编程:模板元编程(TMP)是种利用模板机制执行在编译期的计算的技术。TMP可以用来生成在编译期确定的数据结构和算法,有助于优化程序性能。\n类型萃取:通过模板,我们可以编写只在某些满足特定条件的类型上起作用的代码。例如,你可以创建一个模板,它接受一个参数,并有一个静态断言来检查这个类型是否具有某个特定的成员函数。\n策略模式:在运行时根据策略的改变而改变对象的行为。通过模板,你可以在编译期决定使用哪种策略,这样可以避免在运行期的性能损失。\n\n这些都是模板在日常 C++ 编程中的一些应用。注意,模板是一个深奔蓝似海的主题,你可以通过学习和实践来掌握它更多的使用方法和技巧。\n\nfrom chatgpt\n\n注意:不同语言对于泛型的底层实现是不一样的,C++是基于模版去做的,模版就比如vue/react的模版、GO的template、别的一些模版库他们都是模版,只不过C++模版主要是用来处理泛型的,然后编译器在预处理阶段将模版代码实例化(帮你代码生成)!最后 C++模版是图灵完备语言,所以c++模版只有你想不到的,没有它做不到的,它非常的强大!\n容器类数据结构(容器)这块应该是任何语言都应该提供的!常见的数据结构就是 list/map, 然后他们引出了很多分类,C++提供了一些常见的数据结构,可以看下面的示例代码:https://godbolt.org/z/YP7WeEGPs,我个人比较喜欢Go/Python的设计仅支持list/map别的自行实现,好处在于不需要记忆大量的的API!\n#include <algorithm>#include <vector>#include <list>#include <queue>#include <stack>#include <map>#include <iostream>#include <unordered_map>template <typename T>void print_c(const T& t) { std::cout << "["; std::for_each(t.begin(), t.end(), [](const auto& elem) { std::cout << elem << " ,"; }); std::cout << "]\\n";}int main() { // vector 底层是数组,读写性能会高很多 std::vector<int> arrary_list = {1, 2, 3}; print_c(arrary_list); // list 底层是链表,适合随机插入删除等操作 std::list<int> linked_list = {3, 4, 5}; print_c(linked_list); // https://en.cppreference.com/w/cpp/container/queue // 底层默认是双端队列(dqueue), queue: 先进先出/后进后出 std::queue<int> double_end_queue; double_end_queue.push(1); double_end_queue.push(2); double_end_queue.push(3); std::cout << "double_end_queue [ " << double_end_queue.front() << ", " << double_end_queue.back() << "]" << "\\n"; double_end_queue.pop(); std::cout << "double_end_queue [ " << double_end_queue.front() << ", " << double_end_queue.back() << "]" << "\\n"; // https://en.cppreference.com/w/cpp/container/stack // 底层默认是双端队列(dqueue), stack: 先进后出/后进先出 std::stack<int> double_end_stack; double_end_stack.push(1); double_end_stack.push(2); double_end_stack.push(3); std::cout << "double_end_stack-top [ " << double_end_stack.top() << "]" << "\\n"; double_end_stack.pop(); std::cout << "double_end_stack-top [ " << double_end_stack.top() << "]" << "\\n"; // https://en.cppreference.com/w/cpp/container/unordered_map // unordered_map 就是Java中的HashMap, 底层是红黑树,读写性能会高一些 std::unordered_map<std::string, int> hash_map = {{"1", 1}, {"2", 2}, {"3", 3}}; for (const auto& kv : hash_map) { std::cout << "hash_map: [" << kv.first << ": " << kv.second << "]" << std::endl; } // https://en.cppreference.com/w/cpp/container/map // map(ordered_map) 就是Java中的TreeMap, 底层是平衡二叉树,读写性能会低一些 std::map<std::string, int> tree_map = {{"1", 1}, {"2", 2}, {"3", 3}}; for (const auto& kv : tree_map) { std::cout << "tree_map: [" << kv.first << ": " << kv.second << "]" << std::endl; }}// output:// [ 1 ,2 ,3 ,]// [3 ,4 ,5 ,]// double_end_queue [ 1, 3]// double_end_queue [ 2, 3]// double_end_stack-top [ 3]// double_end_stack-top [ 2]// hash_map: [2: 2]// hash_map: [3: 3]// hash_map: [1: 1]// tree_map: [1: 1]// tree_map: [2: 2]// tree_map: [3: 3]\n\n算法算法本质上就是业务逻辑的抽象,在日常大部分需求中主要就是排序,剩余的就是一些去重过滤、类型转换等操作了,这部分核心体现的是抽象,说白了就是些工具包!\nC++里用 absl 这种工具包就行了或者别的吧,这部分存粹是造轮子所以大家不需要太关心,底层算法自己了解一下就行了!\n#include <algorithm>#include <vector>#include <iostream>template <typename T>void sort_desc(std::vector<T>& arr) { std::sort(arr.begin(), arr.end(), [](auto i, auto j) { return i > j; });}int main() { std::vector<int> arr = {1, 2, 3, 4}; sort_desc(arr); for (const auto& item : arr) { std::cout << item << ", "; }}\n\n这里我以最简单的快速排序为例子,临时写的,代码写的比较垃圾见谅\n#include <vector>#include <iostream>void swap(std::vector<int>& arr, int i, int j) { int tmp = arr[j]; arr[j] = arr[i]; arr[i] = tmp;}// [5] 3 4 2 6 1 7// 1 7// 3 4 2 6// 3 4 2 1 6 7// 1// 1 3 4 2 5 6 7int quick_sort_helper(std::vector<int>& arr, int left, int right) { if (arr.size() <= 1) { return left; } int flag = arr[left]; int flag_index = left; left = left + 1; while (right != left) { while (arr[right] > flag && right > left) { right--; } while (arr[left] < flag && left < right) { left++; } if (left != right) { swap(arr, left, right); } } if (flag < arr[left]) { return flag_index; } swap(arr, flag_index, left); return left;}// quick_sort_helper_gpt 写的... 代码不容易理解int quick_sort_helper_gpt(std::vector<int>& array, int low, int high) { int pivot = array[low]; while (low < high) { while (low < high && array[high] >= pivot) --high; array[low] = array[high]; while (low < high && array[low] <= pivot) ++low; array[high] = array[low]; } array[low] = pivot; return low;}int main() { std::vector<int> arr = {5, 3, 4, 2, 6, 1, 7}; std::cout << quick_sort_helper(arr, 0, int(arr.size() - 1)) << std::endl; print(arr);}\n\n那么这个例子有个问题,仅支持 std::vector 和 int 类型,怎么解决呢?此时就需要使用模版了\ntemplate <typename T, template <typename...> class Array>void swap(Array<T>& arr, typename Array<T>::size_type i, typename Array<T>::size_type j) { int tmp = arr[j]; arr[j] = arr[i]; arr[i] = tmp;}template <typename T, template <typename...> class Array>int quick_sort_helper(Array<T>& arr, typename Array<T>::size_type left, typename Array<T>::size_type right);template <typename T, template <typename...> class Array>void quick_sort(Array<T>& arr, typename Array<T>::size_type left, typename Array<T>::size_type right) { if (left >= right) { return; } auto index = quick_sort_helper(arr, left, right); quick_sort(arr, left, index - 1); quick_sort(arr, index + 1, right);}template <typename T, template <typename...> class Array>int quick_sort_helper(Array<T>& arr, typename Array<T>::size_type left, typename Array<T>::size_type right) { if (arr.size() <= 1) { return left; } int flag = arr[left]; int flag_index = left; left = left + 1; while (right != left) { while (arr[right] > flag && right > left) { right--; } while (arr[left] < flag && left < right) { left++; } if (left != right) { swap(arr, left, right); } } if (flag < arr[left]) { return flag_index; } swap(arr, flag_index, left); return left;}\n\n模版元编程 (TMP)这个太难了,不建议学习,直接pass掉吧,下面这种简单例子,照葫芦画瓢还行,复杂一点的直接就废了看不懂,当你阅读C++模版+模版动态参数的时候那么久该放弃了,虽然C++17提供了一些模版动态参数表达式,有兴趣可以看下这个文章 C++17 constexpr \n// factorial 阶乘template <unsigned int n>struct factorial { enum : unsigned int { value = n * factorial<n - 1>::value };};template <>struct factorial<0> { enum : unsigned int { value = 1 };};int main() { constexpr unsigned int fact_5 = factorial<5>::value; // 会在编译时计算为 120 std::cout << "5! = " << fact_5 << '\\n';}\n\n实际上我们可以使用 constexpr 也可以实现类似的效果\n#include <iostream>constexpr int factorial(int n) { return n < 1 ? 1 : (n * factorial(n - 1));}int main() { std::cout << factorial(5) << std::endl;}\n\n最后说实话对于这种存粹的数值计算,C++的编译器已经给你做了,不信我们把 constexpr 去了开启 -O3 优化,代码链接: https://godbolt.org/z/e5Thh7GYG,所以大部分情况下我们没有编译器聪明,所以这种编译计算的技巧说实话不如交给编译器去做,我们呢只负责写代码就行了!!\n\n类型萃取 (trait)对于大部分C++开发者来说,这个我觉得是C++模版的核心了,因此我们需要根据trait需要编写一些复杂的模版代码!在C++ 中type_traits头文件已经提供了大量的trait函数,实际上我们一些简单的trait直接使用它提供的就行了!\ntrait是C++类型处理的核心实现,因为C++这个语言不会在提供runtime阶段提供类型判断,即大部分涉及到类型判断的逻辑都需要在编译阶段解决,这个原因也是为了代码的性能,即模版不会带到运行时!\ntrait 类型下面是简单实现了一个 is_integral<T> 的trait,本质上是用了C++类模版的重载实现的,相对来说比较简单和易读!\n代码地址: https://godbolt.org/z/hGvMrPrbW\n#include <type_traits>#include <iostream>template <typename T>struct is_integral : std::false_type {};template <>struct is_integral<char> : std::true_type {};template <>struct is_integral<short> : std::true_type {};template <>struct is_integral<int> : std::true_type {};template <>struct is_integral<long> : std::true_type {};template <typename T>constexpr bool is_integral_v = is_integral<T>::value;int main() { std::cout << is_integral_v<std::string> << std::endl; std::cout << is_integral_v<int> << std::endl;}\n\n上面例子显然是最简单的,日常需求大部分trait类型基本都可以参考上面这种写法,不过像std::is_convertible 和 std::is_same 这种实现可能会和上诉这个类型判断有差异的,感兴趣的人可以详细了解下!\ntrait 方法例如我要判断一个类型是否有 std::string to_string() 方法? 通常我们都是定义一个类去判断,这种比较方便,定义方法的比较少!\n下面代码本质上就是利用了 SFINAE (Substitution Failure Is Not An Error) 这个也是C++20之前模版编程的核心了,其本质是一种模版匹配机制,即我会在所有的模版定义中去匹配,哪个匹配成功用哪个,其次它匹配不会去匹配你的代码逻辑只会匹配你的模版申明,注意这个!\n代码地址: https://godbolt.org/z/adG7nMGWf,这个例子核心就是了解下 std::declval 的用法,我个人感觉这个属于编译器的一个test行为和decltype本质上应该是一样的!\n#include <type_traits>#include <string>#include <iostream>/**1. 继承 std::false_type*/template <typename T, typename = void>struct has_to_string_func : std::false_type {};/**偏特化 has_to_string_func 方法,继承 std::true_type1. std::is_same_v<T1,T2> 这个主要是进行判断类型是否相等2. std::enable_if_t<bool,void> 上面相等判断完成后需要进行bool表达式判断,因此 std::enable_if_t 进行判断3. std::declval<T>() 可以创建一个类型T的对象,注意这个实际上不会创建,只是编译器的一种模版匹配机制4. decltype 这个就很简单了,就是提取类型*/template <typename T>struct has_to_string_func<T, std::enable_if_t<std::is_same_v<decltype(std::declval<T>().to_string()), std::string>>> : std::true_type {};struct Test { std::string to_string();};struct Test2 { std::string to_string2();};struct Test3 { void to_string();};int main() { std::cout << has_to_string_func<Test>::value << std::endl; std::cout << has_to_string_func<Test2>::value << std::endl; std::cout << has_to_string_func<Test3>::value << std::endl;}// output:// 1// 0// 0\n\n实现to_string方法上面这个例子实际上我们会发现有个缺点即需要用户在类里面申明to_string() 方法,这个缺点太坑了侵入型比较大,而且可能不同框架对于 to_string() 方法的函数签名要求不一样,所以通常做法都是通过模版类特化的方式实现 to_string() 方法!\n我们知道Go支持interface, rust支持trait,实际上两者对于此场景提供了很好的支持,这里我觉得比较方便的是rust的trait,因此我们可以参考rust的trait的实现来实现一个可拓展的 Stringer trait,说实话我是参考的libfmt! (注意: rust大部分思想应该来源于c++,只不过取其精华罢了)\n\n定义模版类Stringer\n\n#include <string>template <typename T>struct Stringer { Stringer() = delete; std::string to_string(T t) = delete;};\n\n\n实现一个 Stringer 类型的trait,代码地址: https://godbolt.org/z/YxjfMa6qd\n\n#include <string>#include <type_traits>#include <iostream>template <typename T>struct Stringer { Stringer() = delete; std::string to_string(T t) = delete;};template <typename T, typename = void>struct is_stringer : std::false_type {};template <typename T>struct is_stringer<T, std::enable_if_t<std::is_same_v<decltype(std::declval<Stringer<T>>().to_string(std::declval<T>())), std::string>>> : std::true_type {};struct Test {};template <>struct Stringer<Test> { std::string to_string(const Test& t) { return "hello world"; }};struct Test2 {};template <>struct Stringer<Test2> {};struct Test3 {};int main() { std::cout << is_stringer<Test>::value << std::endl; std::cout << is_stringer<Test2>::value << std::endl; std::cout << is_stringer<Test3>::value << std::endl;}// output:// 1// 0// 0\n\n\n至此呢我们就可以实现一个可拓展比较高的 to_string 方法,代码地址: https://godbolt.org/z/Tq99aq3MP\n\n#include <iostream>#include <type_traits>#include <string>template <typename T>struct Stringer { Stringer() = delete; std::string to_string(T t) = delete;};template <typename T, typename = void>struct is_stringer : std::false_type {};template <typename T>struct is_stringer<T, std::enable_if_t<std::is_same_v<decltype(std::declval<Stringer<T>>().to_string(std::declval<T>())), std::string>>> : std::true_type {};template <typename T, typename = void>struct has_to_string_func : std::false_type {};template <typename T>struct has_to_string_func<T, std::enable_if_t<std::is_same_v<decltype(std::declval<T>().to_string()), std::string>>> : std::true_type {};template <typename T>inline std::enable_if_t<has_to_string_func<T>::value, std::string> to_string(T t) { return t.to_string();}template <typename T>inline std::enable_if_t<is_stringer<T>::value, std::string> to_string(T t) { return (Stringer<T>{}).to_string(t);}template <typename T>inline std::enable_if_t<std::is_integral_v<T> || std::is_floating_point_v<T>, std::string> to_string(T t) { return std::to_string(t);}template <typename T>inline std::enable_if_t<std::is_same_v<decltype(std::string(std::declval<T>())), std::string>, std::string> to_string(T t) { return t;}inline std::string to_string(bool b) { return b ? "true" : "false";}struct Test1 { std::string to_string() { return "Test1(" + std::string("name=") + name + ", f2=" + ::to_string(age) + ")"; }; std::string name; int age;};struct Test2 { std::string f1; int f2;};template <>struct Stringer<Test2> { std::string to_string(const Test2& t) { return "Test2(" + std::string("f1=") + t.f1 + ", f2=" + ::to_string(t.f2) + ")"; };};int main() { std::cout << to_string(Test1{.name = "tom", .age = 18}) << std::endl; std::cout << to_string(Test2{.f1 = "F1", .f2 = 2}) << std::endl; std::cout << to_string(false) << std::endl; std::cout << to_string(true) << std::endl; std::cout << to_string(1.11) << std::endl; std::cout << to_string(1111) << std::endl; std::cout << to_string("1111") << std::endl; std::cout << to_string("1111") << std::endl;}// output:// Test1(name=tom, f2=18)// Test2(f1=F1, f2=2)// false// true// 1.110000// 1111// 1111\n\n备注rust-trait上诉我讲到了rust的trait,这里我们可以简单写一个Stringer trait,说实话我发现rust的写法真像python+typehints\n// src/lib.rspub struct Tweet { pub username: String, pub content: String, pub reply: bool, pub retweet: bool,}// src/main.rsuse hello_cargo::Tweet;pub trait Stringer { fn to_string(&self) -> String;}impl Stringer for Tweet { fn to_string(&self) -> String { format!("Tweet username: {}, content: {}", self.username, self.content) }}pub fn test(item: &impl Stringer) { println!("test func: {}", item.to_string());}fn main() { let tweet = Tweet { username: String::from("xiaoming"), content: String::from("from chinese"), reply: false, retweet: false, }; println!("Breaking news {}", tweet.to_string()); test(&tweet);}\n\n\n执行\n\n➜ bazel_simple git:(master) ✗ cargo run Compiling hello_cargo v0.1.0 (/Users/bytedance/go/src/github.com/anthony-dong/bazel_simple) Finished dev [unoptimized + debuginfo] target(s) in 0.23s Running `target/debug/hello_cargo`Breaking news Tweet username: xiaoming, content: from chinesetest func: Tweet username: xiaoming, content: from chinese\n\nlibfmt我个人觉得你如果能写出来 libfmt 这种框架,那么你对于C++的模版的理解已经超越了99% 的人了!\n下面我们可以简单使用下libfmt实现一个logger库,自己编译的话选择cmake自己构建下就行了!\n\ncpp/log/logger.h 文件\n\n#include <iostream>#include "fmt/core.h"#include <cmath>#include <string>#include <chrono>#include "cpp/utils/time.h"#include <unordered_map>#include "absl/time/time.h"#include "absl/time/clock.h"namespace cpp::log {enum class Level { Debug, Info, Warn, Error,};#define LOG_NAMESPACE cpp::log#define INFO(...) LOG_NAMESPACE::Log(LOG_NAMESPACE::Level::Info, __FILE__, __LINE__, __FUNCTION__, __VA_ARGS__)template <typename... Args>inline void Log(Level level, const char* filename, int line, const char* func, fmt::format_string<Args...> format, Args&&... args) { auto file_base = [](const char* name) -> std::string { const std::string_view file(name); const size_t index = file.find_last_of('/'); if (index == -1) { return name; } return {file.begin() + index + 1, file.end()}; }; using clock = std::chrono::system_clock; static std::unordered_map<Level, std::string> level_string_map = { {Level::Debug, "DEBUG"}, {Level::Info, "INFO"}, {Level::Warn, "WARN"}, {Level::Error, "ERROR"}, }; auto level_name = level_string_map[level]; if (level_name.empty()) { level_name = "-"; } fmt::println("[{}] {} {}:{} {}", level_name, absl::FormatTime("%Y-%m-%d %H:%M:%S", absl::Now(), absl::LocalTimeZone()), file_base(filename), line, fmt::format(format, std::forward<Args>(args)...));}} // namespace cpp::log\n\n\ntest/log/logger_test.cpp 文件\n\n#include "gtest/gtest.h"#include "cpp/log/logger.h"#include <source_location>struct MyTest { int age; std::string name;};struct Nop {};template <>struct fmt::formatter<Nop> { template <typename Context> constexpr auto parse(Context& ctx) { return ctx.begin(); }};template <> // 特化模版 fmt::formatter<T>struct fmt::formatter<MyTest> : fmt::formatter<Nop> { template <typename Context> auto format(const MyTest& data, Context& ctx) { return fmt::format_to(ctx.out(), "name: {}. age: {}", data.name, data.age); }};TEST(LogTest, Print) { INFO("hello {}", "world"); MyTest data{.age = 1, .name = "hello"}; INFO("MyTest {}", data); cpp::log::Log(cpp::log::Level::Debug, "logger_test.cpp", 7, "", "hello {}", "1111"); // 注意c++20支持sourcelocation,更加的easy https://en.cppreference.com/w/cpp/utility/source_location std::source_location location = std::source_location::current(); INFO("source_location: filename: {}, line: {}", location.file_name(), location.line()); // logger_test.cpp, 37}:\n\n3.output\n[INFO] 2023-12-18 16:13:22 logger_test.cpp:29 hello world[INFO] 2023-12-18 16:13:22 logger_test.cpp:32 MyTest name: hello. age: 1[DEBUG] 2023-12-18 16:13:22 logger_test.cpp:7 hello 1111[INFO] 2023-12-18 16:13:22 logger_test.cpp:38 source_location: filename: /Users/bytedance/go/src/github.com/anthony-dong/cpp/test/log/logger_test.cpp, line: 37\n\n模版匹配机制\n我们首先写一个 to_string 函数\n\n#include <string>#include <iostream>#include <type_traits>template <typename T>std::string to_string(T t) { return std::to_string(t);}int main() { std::cout << to_string(1) << "\\n"; // pass std::cout << to_string(true) << "\\n"; // pass}\n\n\n我们发现这个函数不满足我们需求,比如参数是一个 string 类型上面代码走不通,因此我改成了,此时问题来了,编译器直接爆错redefinition of 'to_string',这个报错我理解大家都懂重复定义了函数!\n\ntemplate <typename T>std::string to_string(T t) { return std::to_string(t);}template <typename T>std::string to_string(T t) { return t;}\n\n\n我们继续改,解决这个问题,那么此时函数签名不一致了,但是我们执行报错说 call to 'to_string' is ambiguous \n\n#include <string>#include <iostream>#include <type_traits>template <typename T>std::string to_string(T t) { // #1 return std::to_string(t);}template <typename T>std::enable_if_t<std::is_convertible_v<T, std::string>, std::string> to_string(T t) { // #2 return t;}int main() { std::cout << to_string(1) << "\\n"; // pass std::cout << to_string(true) << "\\n"; // pass std::cout << to_string("1111") << "\\n"; // error}\n\n\n继续改,我们需要解决二义性问题,即一个类型不能匹配成功两个函数,比如上面这个例子就是输入参数 t="1111"它既可以匹配#1的模版,也可以匹配 #2的模版 ,这个就是二义性问题,即不能同时匹配多个模版函数!\n\n#include <string>#include <iostream>#include <type_traits>template <typename T>std::enable_if_t<std::is_same_v<decltype(std::to_string(std::declval<T>())), std::string>, std::string> to_string(T t) { return std::to_string(t);}template <typename T>std::enable_if_t<std::is_convertible_v<T, std::string>, std::string> to_string(T t) { return t;}int main() { std::cout << to_string(1) << "\\n"; // pass std::cout << to_string(true) << "\\n"; // pass std::cout << to_string("1111") << "\\n"; // pass}\n\nconcept上面例子我们发现写法非常的丑陋,大量使用std::enable_if, 本质上concept就是替换这种写法的,功能是一模一样的!我们可以使用 concept重写上面的代码,注意concept是c++20提供的!其次concept的报错上会优于模版!\n类型约束类型约束,其实就是要替代上面讲到的trait类型,写法也比较简单,这里不多赘述了,具体可以看下面的代码: https://godbolt.org/z/xze71f49W\n#include <concepts>#include <type_traits>#include <iostream>template <typename T>concept is_numeric = std::same_as<T, int> || std::same_as<T, float>;// 本质上就是下面这个代码,我们发现concept是不是非常简单呢!!template <typename T, typename = void>struct is_numeric_c : std::false_type {};template <typename T>struct is_numeric_c<T, std::enable_if_t<std::is_same_v<T, int> || std::is_same_v<T, float>>> : std::true_type {};int main() { std::cout << is_numeric<int> << std::endl; std::cout << is_numeric<float> << std::endl; std::cout << is_numeric<bool> << std::endl; std::cout << is_numeric_c<int>::value << std::endl; std::cout << is_numeric_c<float>::value << std::endl; std::cout << is_numeric_c<bool>::value << std::endl;}\n\n复杂类型约束复杂类型约束, 这里我们可以实现类似于 Go的 Writer/Reader 接口,具体可以看下面这个例子:https://godbolt.org/z/PfrrrfjGG\n#include <concepts>#include <type_traits>#include <iostream>#include <cassert>// go 里面我们定义了一个 ReadWriter// type Reader interface {//\tRead(p []byte) (n int, err error)// }// type Writer interface {//\tWrite(p []byte) (n int, err error)// }// ReadWriter is the interface that groups the basic Read and Write methods.// type ReadWriter interface {//\tReader//\tWriter//}// 在C++中我们可以定义一个 concept来实现, 是不是非常牛逼哇!!!template <typename T>concept Reader = requires(T t) { requires requires(std::string& buffer) { { t.Read(buffer) } -> std::same_as<size_t>; };};template <typename T>concept Writer = requires(T t) { requires requires(std::string& buffer) { { t.Write(buffer) } -> std::same_as<size_t>; };};template <typename T>concept ReadWriter = Reader<T> && Writer<T>;// 简单实现一个StringBufferstruct StringBuffer { explicit StringBuffer(std::string&& buffer) : buffer_(std::move(buffer)) { w = buffer_.size(); r = 0; }; size_t Read(std::string& buffer) { auto size = buffer.size(); if (w == r) { return 0; } if (w - r < size) { size = w - r; } std::copy(buffer_.begin() + long(r), buffer_.begin() + long(r) + long(size), buffer.begin()); r = r + size; return size; } size_t Write(std::string& buffer) { buffer_.append(buffer, 0, buffer.size()); w = w + buffer.size(); return buffer.size(); } size_t Write(std::string&& buffer) { return Write(buffer); }private: size_t r, w; std::string buffer_;};// 实现ioutil.ReadAll(reader) 方法template <ReadWriter Rw>size_t ReadAll(Rw& reader, std::string& buffer) { buffer.clear(); std::string bw{}; bw.resize(16); while (true) { if (auto size = reader.Read(bw); size >= 0) { if (size == 0) { break; } buffer.append(bw, 0, size); } } return buffer.size();}int main() { StringBuffer sb("C++是世界上最好的语言"); sb.Write("!!!!!"); std::string buffer{}; ReadAll(sb, buffer); std::cout << buffer << std::endl;}// output:// C++是世界上最好的语言!!!!!\n\n实现to_string方法我们还是以上面trait里讲到的 to_string 方法进行重写,代码地址: https://godbolt.org/z/vqxfqEY66\n#include <iostream>#include <type_traits>#include <string>#include <concepts>template <typename T>struct Stringer { Stringer() = delete; std::string to_string(T t) = delete;};template <typename T>concept has_to_string_func = requires(T t) { // {expression} noexcept(optional) -> type-constraint; // 本质上就是,std::is_same_v<decltype(std::declval<Stringer<T>>().to_string(std::declval<T>())), std::string> { t.to_string() } -> std::same_as<std::string>;};template <typename T>concept is_stringer = requires(T t) { // {expression} noexcept(optional) -> type-constraint; requires requires(Stringer<T> s) { { s.to_string(t) } -> std::same_as<std::string>; }; // 也可以这么写,不过推荐上面这种写法 // { std::declval<Stringer<T>>().to_string(t) } -> std::same_as<std::string>;};template <has_to_string_func T>inline std::string to_string(T t) { return t.to_string();}template <is_stringer T>inline std::string to_string(T t) { return (Stringer<T>{}).to_string(t);}template <typename T> requires std::is_integral_v<T> || std::is_floating_point_v<T>inline std::string to_string(T t) { return std::to_string(t);}template <typename T> requires std::convertible_to<T, std::string>inline std::string to_string(T t) { return t;}inline std::string to_string(bool b) { return b ? "true" : "false";}struct Test1 { std::string to_string() { return "Test1(" + std::string("name=") + name + ", f2=" + ::to_string(age) + ")"; }; std::string name; int age;};struct Test2 { std::string f1; int f2;};template <>struct Stringer<Test2> { std::string to_string(const Test2& t) { return "Test2(" + std::string("f1=") + t.f1 + ", f2=" + ::to_string(t.f2) + ")"; };};int main() { std::cout << to_string(Test1{.name = "tom", .age = 18}) << std::endl; std::cout << to_string(Test2{.f1 = "F1", .f2 = 2}) << std::endl; std::cout << to_string(false) << std::endl; std::cout << to_string(true) << std::endl; std::cout << to_string(1.11) << std::endl; std::cout << to_string(1111) << std::endl; std::cout << to_string("1111") << std::endl; std::cout << to_string("1111") << std::endl;}// output:// Test1(name=tom, f2=18)// Test2(f1=F1, f2=2)// false// true// 1.110000// 1111// 1111\n\n其他模版与左右值的关系总结C++的模版可以说是C++的灵魂所在,例如上面的例子我们通过类模版的特化就很轻松的实现了一个 rust 的trait 类型哇,我们用 concept/template 就实现了类似于Go的Interface呢,所以C++的技巧太多了,平时可以多阅读一些优秀的代码,自己也会变得优秀!\n我理解日常需求中我们只要学会了本文讲到的实际上已经基本够用了!\n其次模版相关的文章太少了,大家有不懂的代码和语法直接问GPT就行了,说实话我也是问人家的,然后自己多动手!\n","categories":["C++"],"tags":["C++"]},{"title":"HTTPS抓包的原理和实现","url":"/2024/02/28/d60ca162f1a2911c884053ddc7786384/","content":"实际上生产环境中我们存在大量的业务请求都走的HTTPS,那么如何抓取调用下游的请求成了问题!如果是HTTP还行,你这HTTPS呢,能用tcpdump抓但是我们没有证书的私钥哇也解析不了哇。目前主流的HTTPS抓包工具都是通过MITM (Man-In-The-Middle 中间人攻击)实现的, 具体原理和实现本文也会讲到, 所以这里我们就不造轮子了直接用开源的mitmproxy,如果你是本地抓包的话完全可以用Wireshark和 Charles 工具!\n\n\n使用mitmproxy抓取HTTPS流量注意\n如果你第一次操作建议自己起一个容器进行测试,别直接上来在自己机器上搞!可以安装个Debian/Ubuntu的镜像先试试!\n请勿在线上生产环境使用,如果使用请明确影响面后再使用!\n\n安装 mitmproxy# 通过pip3源码安装(推荐,能第一时间安装到最新的)pip3 install mitmproxy# 根据Linux发行版的内置版本下载sudo apt-get install -y mitmproxy\n\n安装完成后可以查看下版本\n~ mitmproxy --versionMitmproxy: 4.0.4Python: 3.7.3OpenSSL: OpenSSL 1.1.1n 15 Mar 2022Platform: Linux-5.4.143.bsk.8-amd64-x86_64-with-debian-10.12\n\n注意: 有些服务器的Linux发行版本比较低,实际上 mitmproxy 就处于不可用的状态,此时可以使用我自己开发的一个 https 抓包工具 devtool!\n启动 mitmproxy直接执行 mitmproxy 即可,默认监听的是本地的8080 端口\nmitmproxy\n\n下载安装证书\n下载证书\n\nwget -e http_proxy=localhost:8080 http://mitm.it/cert/pem -O mitmproxy-ca-cert.pem\n\n\n安装证书\n\n# 1. 安装到系统的根证书中mv mitmproxy-ca-cert.pem /usr/local/share/ca-certificates/mitmproxy.crt# 2. 更新系统根证书, 其实就是写入到了 /etc/ssl/certs/ca-certificates.crt 这里!sudo update-ca-certificates\n\n\n如何卸载证书,其实就是删除掉mitmproxy.crt, 然后sudo update-ca-certificates!\n\n抓取HTTPS流量\n配置HTTP代理\n\nexport http_proxy=http://localhost:8080export https_proxy=http://localhost:8080# 关于 no_proxy 环境变量,这篇文章介绍的比较好 https://about.gitlab.com/blog/2021/01/27/we-need-to-talk-no-proxy/ # export no_proxy=localhost# 如何删除/取消环境变量?# unset http_proxy# unset https_proxy# unset no_proxy\n\n\ncurl发起请求测试\n\ncurl https://www.douyin.com/\n\n\n查看数据包(这个页面是一个交互式的页面)\n\n\n\n查看数据包详情\n\n\n\n应用程序开启抓包,通常情况下大部分开发语言的框架/应用程序的都是有适配 http_proxy 的(不明确的可以自行百度),因此开启应用程序抓包只需要给程序配置 https_proxy 和 http_proxy 相关环境变量然后重启下应用程序即可!\n\nHTTPS抓包原理1. https_proxy 的工作原理HTTPS代理服务器的工作原理基于HTTP的CONNECT方法。与普通的HTTP代理(根据客户端的GET,POST等请求,代理服务器会直接进行处理并将结果返回给客户端)有所不同,HTTPS代理主要用于建立一个TCP的隧道,用于客户端和目标服务器的相互通信。\n第一步:客户端向代理服务器发送一个CONNECT请求,请求中包含目标服务器的地址和端口号!\n~/.mitmproxy curl 'https://www.douyin.com' -v* Uses proxy env variable https_proxy == 'http://localhost:8080'* Trying ::1...* TCP_NODELAY set* Connected to localhost (::1) port 8080 (#0)* allocate connect buffer!* Establish HTTP proxy tunnel to www.douyin.com:443> CONNECT www.douyin.com:443 HTTP/1.1> Host: www.douyin.com:443> User-Agent: curl/7.64.0> Proxy-Connection: Keep-Alive>< HTTP/1.0 200 Connection Established<* Proxy replied 200 to CONNECT request\n\n上面这个日志我们可以看到请求Host是www.douyin.com:443 \n第二步:如果代理服务器允许此连接,它会与目标服务器建立TCP连接,并向客户端发送一个状态行,比如 “HTTP/1.0 200 Connection Established”,告知客户端可以开始发送请求到目标服务器了!\n第三步:代理服务器主要扮演数据转发的角色,客户端与目标服务器之间可以通过这个隧道来发送任何类型的数据,包括但不限于HTTP请求,也可以能是websocket等!\n2. https_proxy 抓包原理MITM上面我们介绍了https proxy的原理,第三步的时候实际上我们做了一个tcp代理,此时就能下文章了!官方术语叫做MITM (Man-In-The-Middle)中间人攻击,其实就是伪造服务端的证书进行实现的,然后骗取客户端提前信任 MITM 服务的根证书!\n\n根证书如何伪造服务端的证书?首先要熟悉TLS认证(握手 handshake)流程,其中一步是客户端获取证书,此时代理服务器(mitm)会伪造一份证书(公钥)发给客户端,客户端拿到证书后会进行证书的认证,这个是一个证书链认证的过程,其中最核心的就是根证书!如果客户端信任的了根证书,那么基于根证书分发的证书都会被认证!\n\n证书链,实际上我们通过浏览器就能获得,例如 https://excalidraw.com/ 这个网站的根证书是ISRG Root X1 签发的,然后我们打开 mac 的钥匙串,既可以看到确实已经信任了根证书ISRG Root X1 \n\n\n备注:如何生成一个根证书\nopenssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 3650 -nodes\n\n伪造证书所以MITM的思路就是我自己签发一个根证书,然后让客户端所在的操作系统信任我的根证书,当客户端发起请求的时候,用我的根证书去给客户端访问的域名或者地址签发一个证书基于根证书的私钥,那么客户端就会认证成功!MITM也能成功劫持(Hijack)请求!\nsequenceDiagram\n客户端->>MITM: 1. 发起 CONNECT www.douyin.com:443 请求\nMITM-->>客户端: 响应 HTTP/1.0 200 Connection Established\n客户端 ->> MITM: 2. 发起 https 的握手请求\nMITM ->> MITM: 3. 基于根证书为 www.douyin.com 生成一份证书\nMITM -->> 客户端: 响应证书\n客户端 ->> 客户端: 4. 认证证书成功, 握手成功\n客户端 ->> MITM: 5. 发起HTTPS请求 https://www.douyin.com/\nMITM ->> MITM: MITM这里是可以篡改、抓取请求/响应的\nMITM ->> 服务端: 6. 发起请求\n服务端 -->> MITM: 返回响应 \nMITM -->> 客户端: 返回响应\n\n\n\n\n其中第四步,由于已经提前信任了根证书,为此客户端一定会认证成功!关于伪造证书的生成逻辑下文代码会有!\n\n3. 代码实现\nhttp代理逻辑\n证书相关逻辑\n\n其他服务间调用的流量大部分都是RPC流量,Thrift/HTTP占据大部分,那么我能不能直接在机器上抓和解析Thrift包呢?是的可以,我本人开发了一个工具可以读取tcpdump的输出,来实时解析thrift/http报文,有兴趣的同学可以看一下 https://github.com/Anthony-Dong/golang/tree/master/command/tcpdump 这个,使用起来会非常的简单,目前我在公司内部也经常使用和处理线上问题!\n","categories":["Linux"],"tags":["Linux","抓包"]},{"title":"git 日常用法","url":"/2020/03/08/e29180bc3d76b959abec41c7fc18c142/","content":" 日常开发中经常使用git作为版本控制,常用的话就是git命令,也可以使用第三方的 sourcetree 工具,对于常用命令来说,其实git命令基本够用了,但是当你大量阅读代码的时候,依托于ide,速度太慢了!\n\n\n1、常用命令git pull <远程主机名> <远程分支名>:<本地分支名>\ngit push <远程主机名> <本地分支名>:<远程分支名> , 和git pull 都是 src:dest\n git fetch <远程主机名> <分支名> 其实就是拉去远程分支到本地版本库,然后再使用 git merge\ngit merge <branch> 合并分支,将<branch> 合并到当前所在的分支, 比如git merge master ,将本地master与自己的分支合并,比如git merge origin/branch ,就是将远程的origin/branch与本地的分支合并 , 比如不自动commit,可以git merge --no-commit <branch> 。 其实git merge 可以一次合并多个分支,比如 git merge <branch> origin/<branch> 其实就是管你远程和本地了全部合到我的分支上。\ngit diff <branch1> <branch2> 在两个分支之间比较\ngit diff --cached 比较 git add . 与 工作区之间的比较, 也就是本地暂存区与工作区之间的比较\n\ngit diff\t 工作区 vs 暂存区git diff head\t 工作区 vs 版本库git diff –cached 暂存区 vs 版本库\n\ngit reset --mixed 将暂存区的分支直接回到到工作区, 也就是比如git add . 你想回退到本地,直接 `git resrt –mixed\ngit reset --mixed HEAD~1 回退一个版本,比如你本地commit/远程已经push了,那么你本地和远程是一样的,如果你想回退一个版本,此时需要reset操作,主要是解决回退的操作,上面就是commit一次后需要回退一个版本,所以是HEAD~1, 比如commit了两次就是HEAD~2 , 此时有4种选项,一般分为--hard 和 --mixed ,--soft , --hard 硬reset,回退回来你的这次修改全部没有了,也就是直接回到了上一个版本,而--mixed 回退到你没有git add . 操作的时候,--soft 是回退到你git add . 操作完后的时候。 看需求吧。一般只用--hard 和 --mixed .\ngit commit --amend 主要是处理 你 git commit -m '' 想要修改 commit的desc/comment了。\ngit rm -r --cached . 比如修改了 gitignore ,但是其实你的版本库/暂存区是没有 ignore的,所以需要直接删除 暂存区的缓存。\ngit rm file 也就是删除本地开发的一个文件,硬删除,直接删没了,回退也需要硬回退。\ngit checkout . 清空本地所有修改的代码\ngit checkout -b <branch> 将本地的这个分支,checkout 出一个新的分支,名字为<branch>git checkout -b <branch> origin/<branch> ,将远程的<branch> 分支上 checkout一个新的本地分支名字为`\n有些场景可能需要打tag,所以一般是 git pull ,拉去远程的所有代码(记得切换到master上),然后 git tag 查看tag 历史,创建一个新的tag ,比如git tag v1.1.0,然后直接推送到远程git push origin v1.1.0 就好了(一般是别人给你合master了,上线可能需要打新的tag)\n文件权限发生变更需要配置:git diff old mode 100644 new mode 100755 的问题 :需要设置 git config –add core.filemode false\n git log -p README.md 查看文件的变更详细历史, git log <file> 查看文件的变更历史,另外有可能查看某一行的变更,这个是指你没有ide的情况下,所以需要指定\n➜ ebike-**** git:(master) ✗ cat -n cmd/main.go 1 package main 2 3 import ( ## ........18 )19 20 func main() {# ......24 // 启动RocketMQ25 rq.RocketmqInit()## ......53 r.Run(fmt.Sprintf(":%s", host))54 }55 ## .............83 }\n\n然后\n➜ ebike-op-helios git:(master) ✗ git log -L 25:cmd/main.go\n\n\ngit shortlog -sn 查看提交者\ngit submodel add remote_addr local_dir 子模块\n# 将远程项目https://github.com/maonx/vimwiki-assets.git克隆到本地assets文件夹。git submodule add https://github.com/maonx/vimwiki-assets.git assets\t\n\n2、子模块\n 有种情况我们经常会遇到:某个工作中的项目需要包含并使用另一个项目。 也许是第三方库,或者你独立开发的,用于多个父项目的库。 现在问题来了:你想要把它们当做两个独立的项目,同时又想在一个项目中使用另一个。\n\n文档:Git-工具-子模块\n1、在父项目新建子模块\ngit submodule add https://github.com/chaconinc/DbConnector ./submodule/DbConnector\n\n2、提交子模块\n\n如果在父项目(父项目无任何变更)中提交子模块,会出现:\n\n➜ test-dir git:(master) gitpush masterOn branch masterYour branch is up to date with 'origin/master'.Changes not staged for commit:\tmodified: submodule-01 (modified content)no changes added to commitEverything up-to-date\n\n\n父项目更更提交 (可以提交变更,但是子项目并没有提交)\n\n➜ test-dir git:(master) ✗ gitpush master[master f00fa83] fanhaodong 提交与2021-03-08 15:54:09 1 file changed, 1 insertion(+)## ......To gitee.com:Anthony-Dong/parent-report.git 53487e2..f00fa83 master -> master\n\n\n提交子项目(需要切换到子项目目录,然后切换到父项目提交)\n\n➜ submodule-01 git:(master) ✗ gitpush master[master 969f9ea] fanhaodong 提交与2021-03-08 15:52:08## ....To gitee.com:Anthony-Dong/submodule-01.git 6ac8e7d..969f9ea master -> master ➜ test-dir git:(master) ✗ gitpush master[master 53487e2] fanhaodong 提交与2021-03-08 15:52:12 1 file changed, 1 insertion(+), 1 deletion(-)## ....To gitee.com:Anthony-Dong/parent-report.git 14e259d..53487e2 master -> maste \n\n问题就是,提交流程过于复杂!!\n参考\nProGit, 最好的Git指南\nAdvanced Git\nGit and GitHub Secrets\nGIT子模块\n\n","categories":["Linux"],"tags":["git"]},{"title":"Golang的调度模型","url":"/2021/05/27/ed44e381cbc098d95c5091eb11450b7e/","content":"Go有四大核心模块,基本全部体现在runtime,有调度系统、GC、goroutine、channel,那么深入理解其中的精髓可以帮助我们理解Go这一门语言!\n\n\n1、Go调度模型发展历史\n单线程调度器 (0.x 版本)\n只包含 40 多行代码;\n程序中只能存在一个活跃线程,由 G-M 模型组成;\n\n\n多线程调度器 ·(1.0版本)\n允许运行多线程的程序;\n全局锁导致竞争严重;\n\n\n任务窃取调度器 · (1.1版本)\n引入了处理器 P,构成了目前的 G-M-P 模型;\n在处理器 P 的基础上实现了基于工作窃取的调度器;\n在某些情况下,Goroutine 不会让出线程,进而造成饥饿问题;(单个p+空转)\n时间过长的垃圾回收(Stop-the-world,STW)会导致程序长时间无法工作;\n\n\n抢占式调度器 · (1.2版本~ 至今)\n基于协作的抢占式调度器 - 1.2 ~ 1.13\n通过编译器在函数调用时插入抢占检查指令,在函数调用时检查当前 Goroutine 是否发起了抢占请求,实现基于协作的抢占式调度;\nGoroutine 可能会因为垃圾回收和循环长时间占用资源导致程序暂停;\n\n\n基于信号的抢占式调度器 - 1.14 ~ 至今\n实现基于信号的真抢占式调度;\n垃圾回收在扫描栈时会触发抢占调度;\n抢占的时间点不够多,还不能覆盖全部的边缘情况;\n\n\n\n\n非均匀存储访问调度器 · 提案\n对运行时的各种资源进行分区;\n实现非常复杂,到今天还没有提上日程;\n\n\n\n上面说到的1.3版本以前历史,其实都是go的非发行版本,所以我们关注与的是go的发行版本,也就是go的gpm模型!\n\nG: Goroutine,即我们在 Go 程序中使用 go 关键字创建的执行体;\nM: Machine,或 worker thread,即传统意义上进程的线程;\nP: Processor,即一种人为抽象的、用于执行 Go 代码被要求局部资源。只有当 M 与一个 P 关联后才能执行 Go 代码。除非 M 发生阻塞或在进行系统调用时间过长时,没有与之关联的 P。\n\n参考: 调度系统设计精要\n2、GM模型\n P的作用不光光是队列这种抽象,如果理解为队列,那么它的地位只是M的一个子集,GM模型,我们核心关注的是G和M,这也是一般线程池的模型!目标就是高效的调度G在M上!\n\n下面是我用Go语言简单写的一个调度器,大家可以看看设计思路,以及存在的问题!\npackage mainimport (\t"fmt"\t"go.uber.org/atomic"\t"os"\t"os/signal"\t"time")func main() {\tsig := make(chan os.Signal, 0) // 监听程序的信号\tsignal.Notify(sig, os.Interrupt, os.Kill)\tdown := make(chan struct{}, 0) // 程序down机信号\tthreadNum := 2 // 运行的线程\ttaskQueue := make(chan func(), 1<<20) // 任务队列大小限制\taddTask := func(foo func()) {\t\tselect {\t\tcase <-down:\t\tcase taskQueue <- foo:\t\t}\t}\tschedule := func() {\t\tfor x := 0; x < threadNum; x++ {\t\t\tgo func() {\t\t\t\tfor {\t\t\t\t\tselect {\t\t\t\t\tcase <-down:\t\t\t\t\t\treturn\t\t\t\t\tcase foo := <-taskQueue:\t\t\t\t\t\tfoo()\t\t\t\t\t}\t\t\t\t}\t\t\t}()\t\t}\t}\tschedule() // 启动调度器\tcount := &atomic.Int64{}\tfor x := 0; x < 1; x++ {\t\taddTask(func() { // 添加一个任务,循环的塞入任务\t\t\tfor {\t\t\t\taddTask(func() {\t\t\t\t\tcount.Add(1)\t\t\t\t})\t\t\t}\t\t})\t}\t// 等待程序结束\tselect {\tcase <-sig:\t\tclose(down)\t\tfmt.Println("程序退出, exec ctrl+c")\tcase <-time.After(time.Second):\t\tclose(down)\t\tfmt.Printf("调用量: %v\\n", count.Load())\t\treturn\t}}\n\n1、现象1、测试条件,调度器只启动两个线程,然后一个线程主要是负责循环的添加任务,一个线程循环的去执行任务\n➜ go-tool git:(master) ✗ bin/app 调用量: 5078714➜ go-tool git:(master) ✗ bin/app调用量: 5043506\n\n2、测试条件,调度器启动三个线程,然后两个线程去执行任务,一个添加任务\n➜ go-tool git:(master) ✗ bin/app 调用量: 4333959➜ go-tool git:(master) ✗ bin/app调用量: 4359804\n\n3、继续测试,启动十个线程,一个添加任务,九个执行任务\n➜ go-tool git:(master) ✗ bin/app 调用量: 1663691➜ go-tool git:(master) ✗ bin/app调用量: 1692096\n\n4、我们添加一些阻塞的任务\naddTask(func() { count.Inc() time.Sleep(time.Second * 2)})\n\n执行可以看到完全不可用\n➜ go-tool git:(master) ✗ bin/app 调用量: 9\n\n2、问题 1、 可以看到随着M的不断的增加,可以发现执行任务的数量也不断的减少,原因是什么呢?有兴趣的同学可以加一个pprof可以看看,其实大量的在等待锁的过程! \n 2、如果我的M运行了类似于Sleep操作的方法如何解决了,我的调度器还能支撑这个量级的调度吗?\n关于pprof如何使用:在代码头部加一个这个代码:\nfile, err := os.OpenFile("/Users/fanhaodong/go/code/go-tool/main/prof.pporf", os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644)if err != nil { panic(err)}if err := pprof.StartCPUProfile(file); err != nil { panic(err)}defer pprof.StopCPUProfile()\n\n我们查看一下 go tool pprof main/prof.pporf\nShowing top 10 nodes out of 36 flat flat% sum% cum cum% 2.45s 63.80% 63.80% 2.45s 63.80% runtime.usleep 0.40s 10.42% 74.22% 0.40s 10.42% runtime.pthread_cond_wait 0.28s 7.29% 81.51% 0.28s 7.29% runtime.(*waitq).dequeueSudoG 0.25s 6.51% 88.02% 0.25s 6.51% runtime.pthread_cond_signal 0.17s 4.43% 92.45% 0.94s 24.48% main.main.func2.1 0.10s 2.60% 95.05% 0.10s 2.60% runtime.procyield 0.07s 1.82% 96.88% 0.07s 1.82% runtime.(*waitq).dequeue 0.03s 0.78% 97.66% 0.03s 0.78% runtime.madvise 0.02s 0.52% 98.18% 0.99s 25.78% main.main.func3 0.02s 0.52% 98.70% 1.72s 44.79% runtime.selectgo\n\n可以看到真正执行代码的时间只有 0.17s + 0.02s 其他时间都被阻塞掉了!\n3、GPM模型1、GM模型问题1、GM模型中的所有G都是放入到一个queue,那么导致所有的M取执行任务时都会去竞争锁,我们插入G也会去竞争锁,所以解决这种问题一般就是减少对单一资源的竞争,那就是桶化,其实就是每个线程都分配一个队列\n2、GM模型中没有任务状态,只有runnable,假如任务遇到阻塞,完全可以把任务挂起再唤醒\n\n运行队列去存放所有的可以运行的任务,runnable\n所有线程执行运行中的任务,running \n运行中的任务被阻塞的任务,需要放弃运行权利,挂起到等待队列,blocking\n\n2、GPM如何优化GM的(核心)1、引入P 这里其实会遇到一个问题,假如要分配很多个线程,那么此时随着线程的增加,也会造成队列的增加,其实也会造成调度器的压力,因为它需要遍历全部线程的队列去分配任务以及后续会讲到的窃取任务!\n 因为我们知道CPU的最大并行度其实取决于CPU的核数,也就是我们没必要为每个线程都去分配一个队列,因为就算是给他们分配了,他们自己去那执行调度,其实也会出现大量阻塞,原因就是CPU调度不过来这些线程!\n Go里面是只分配了CPU个数的队列,这里就是P这个概念,你可以理解为P其实是真正的资源分配器,M很轻只是执行程序,所有的资源内存都维护在P上!M只有绑定P才能执行任务(强制的)!\n\n这样做的好处:\n\n如果线程很轻,那么销毁和创建会变得很简单,也就是后面会讲到的内核阻塞调用创建线程\n如果全部资源分配在固定数量的P上,那么可以充分利用CPU并行度\n\n2、GPM调度器1、首先调度程序其实就是调度不同状态的任务,go里面为Go标记了不同的状态,其实大概就是分为:runnable,running,block等,所以如何充分调度不同状态的G成了问题,那么关于阻塞的G如何解决,其实可以很好的解决G调度的问题!\n\n在channel上发送和接收\n网络I/O操作\n阻塞的系统调用\n使用定时器或者Sleep\n使用互斥锁 sync.Mutex\n\n上面这些情况其实就分为:\n\n用户态阻塞\n内核态阻塞但是不会挂起当前线程\n内核态阻塞会挂起当前线程\n\n2、用户态阻塞,一般Go里面依靠gopark 函数去实现,大体的代码逻辑基本上和go的调度绑定死了\n源码在:https://golang.org/src/runtime/proc.go\nfunc gopark(unlockf func(*g, unsafe.Pointer) bool, lock unsafe.Pointer, reason waitReason, traceEv byte, traceskip int) {\tif reason != waitReasonSleep {\t\tcheckTimeouts() // timeouts may expire while two goroutines keep the scheduler busy\t}\tmp := acquirem()\tgp := mp.curg\tstatus := readgstatus(gp)\tif status != _Grunning && status != _Gscanrunning {\t\tthrow("gopark: bad g status")\t}\tmp.waitlock = lock\tmp.waitunlockf = unlockf\tgp.waitreason = reason\tmp.waittraceev = traceEv\tmp.waittraceskip = traceskip\treleasem(mp)\t// can't do anything that might move the G between Ms here.\tmcall(park_m)}// park continuation on g0.func park_m(gp *g) {\t_g_ := getg()\tif trace.enabled {\t\ttraceGoPark(_g_.m.waittraceev, _g_.m.waittraceskip)\t}\tcasgstatus(gp, _Grunning, _Gwaiting)\tdropg()\tif fn := _g_.m.waitunlockf; fn != nil {\t\tok := fn(gp, _g_.m.waitlock)\t\t_g_.m.waitunlockf = nil\t\t_g_.m.waitlock = nil\t\tif !ok {\t\t\tif trace.enabled {\t\t\t\ttraceGoUnpark(gp, 2)\t\t\t}\t\t\tcasgstatus(gp, _Gwaiting, _Grunnable)\t\t\texecute(gp, true) // Schedule it back, never returns.\t\t}\t}\tschedule() // 调度程序}\n\n\n他会标记当前的g的状态从 running -> waiting 状态\n然后开始执行调度: schedule() 方法\n首先就是看看有没有其他特殊情况:比如GC或者trace\n其次就是可能先去获取全局队列的(代码里写的偶尔,根据随机性去执行的)\n获取当前g绑定的p队列里获取g\n从其他地方获取可以运行的g (优先级是:从本地队列->全局队列->网络轮巡器->窃取其他P的G,具体可以看 findrunnable 方法)\n最后执行那个唤醒的G\n\n\n最后就是unpark,就是将当前goroutine置于等待状态并解锁锁。 可以通过调用goready方法使goroutine再次运行。\n\n3、其实对于netpool 这种nio模型,其实内核调用是非阻塞的,所以go开辟了一个网络轮训器队列,来存放这些被阻塞的g,等待内核被唤醒!那么什么时候会被唤醒了,其实就是需要等待调度器去调度了!\nn, err := syscall.Read(fd.Sysfd, p)if err != nil { n = 0 if err == syscall.EAGAIN && fd.pd.pollable() { if err = fd.pd.waitRead(fd.isFile); err == nil { // 这里会等待读,其实就是挂起了当前g,也就是主动让出了m continue } } // 。。。。。。。}\n\n4、如果是内核态阻塞了(内核态阻塞一般都会将线程挂起,线程需要等待被唤醒),我们此时P只能放弃此线程的权利,然后再找一个新的线程去运行P!\n关于着新线程:找有没有idle的线程,没有就会创建一个新的线程!\n关于当内核被唤醒后的操作:因为GPM模型所以需要找到个P绑定,所以G会去尝试找一个可用的P,如果没有可用的P,G会标记为runnable放到全局队列中!\n\n 关于内核唤醒后G如何执行的代码我没有找到,不好意思,逻辑没有看太清晰!其实疑问点:如何找到可用的P,所以固定数量的P的好处就是查询时间比较可控!为何不找到上一次绑定的P呢?为何切换上下文了!\n\n5、其实了解上面大致其实就了解了Go的基本调度模型\n\nG运行的优先级是啥\nP和M啥时候会接触绑定\nP啥时候会窃取G\n\n答案文章里慢慢品味!\n3、如何防止G长时间占用P如果某个 G 执行时间过长,其他的 G 如何才能被正常的调度? 这便涉及到有关调度的两个理念:协作式调度与抢占式调度。协作式和抢占式这两个理念解释起来很简单: 协作式调度依靠被调度方主动弃权;抢占式调度则依靠调度器强制将被调度方被动中断。\n1、空转代码例如下面的代码,我本地的版本是go1.13.5\npackage mainimport (\t"fmt"\t"runtime")func main() {\tgo func() {\t\tfor {\t\t\t{\t\t\t}\t\t}\t}()\truntime.Gosched() // 表示主线程让出当前线程,可以理解为先让go执行\tfmt.Println("close")}\n\n2、存在的问题执行: GOMAXPROCS=1 配置全局只能有一个P\n➜ go-tool git:(master) ✗ GODEBUG=schedtrace=1 GOMAXPROCS=1 bin/appSCHED 0ms: gomaxprocs=1 idleprocs=0 threads=3 spinningthreads=0 idlethreads=1 runqueue=0 [2]SCHED 1ms: gomaxprocs=1 idleprocs=0 threads=3 spinningthreads=0 idlethreads=1 runqueue=1 [1]SCHED 2ms: gomaxprocs=1 idleprocs=0 threads=3 spinningthreads=0 idlethreads=1 runqueue=1 [1]SCHED 4ms: gomaxprocs=1 idleprocs=0 threads=3 spinningthreads=0 idlethreads=1 runqueue=1 [1]SCHED 7ms: gomaxprocs=1 idleprocs=0 threads=3 spinningthreads=0 idlethreads=1 runqueue=1 [1]SCHED 13ms: gomaxprocs=1 idleprocs=0 threads=3 spinningthreads=0 idlethreads=1 runqueue=1 [1]\n\n可以看到main函数无法执行!也就是那个go 空转抢占了整个程序\n备注:\n\nGODEBUG=schedtrace=1 表示1ms打印一次\n\nSCHED:调试信息输出标志字符串,代表本行是goroutine scheduler的输出;1ms:即从程序启动到输出这行日志的时间;gomaxprocs: P的数量;idleprocs: 处于idle状态的P的数量;通过gomaxprocs和idleprocs的差值,我们就可知道执行go代码的P的数量;threads: os threads的数量,包含scheduler使用的m数量,加上runtime自用的类似sysmon这样的thread的数量;spinningthreads: 处于自旋状态的os thread数量;idlethread: 处于idle状态的os thread的数量;runqueue=1: go scheduler全局队列中G的数量;[3 4 0 10]: 分别为4个P的local queue中的G的数量。\n\n3、升级1.14+版本解决但是假如我换为用 1.14+版本执行,有兴趣的话可以使用我的docker镜像,直接可以拉取: fanhaodong/golang:1.15.11 和 fanhaodong/golang:1.13.5\n[root@647af84b3319 code]# GODEBUG=schedtrace=1 GOMAXPROCS=1 bin/appSCHED 0ms: gomaxprocs=1 idleprocs=0 threads=2 spinningthreads=0 idlethreads=0 runqueue=0 [0]SCHED 1ms: gomaxprocs=1 idleprocs=0 threads=3 spinningthreads=0 idlethreads=1 runqueue=0 [0]SCHED 2ms: gomaxprocs=1 idleprocs=0 threads=3 spinningthreads=0 idlethreads=1 runqueue=0 [0]SCHED 3ms: gomaxprocs=1 idleprocs=0 threads=3 spinningthreads=0 idlethreads=1 runqueue=1 [1]SCHED 4ms: gomaxprocs=1 idleprocs=0 threads=3 spinningthreads=0 idlethreads=1 runqueue=1 [1]SCHED 5ms: gomaxprocs=1 idleprocs=0 threads=3 spinningthreads=0 idlethreads=1 runqueue=1 [1]SCHED 6ms: gomaxprocs=1 idleprocs=0 threads=3 spinningthreads=0 idlethreads=1 runqueue=1 [1]SCHED 7ms: gomaxprocs=1 idleprocs=0 threads=3 spinningthreads=0 idlethreads=1 runqueue=1 [1]SCHED 8ms: gomaxprocs=1 idleprocs=0 threads=3 spinningthreads=0 idlethreads=1 runqueue=1 [1]SCHED 10ms: gomaxprocs=1 idleprocs=0 threads=3 spinningthreads=0 idlethreads=1 runqueue=1 [1]SCHED 13ms: gomaxprocs=1 idleprocs=0 threads=3 spinningthreads=0 idlethreads=1 runqueue=1 [1]close\n\n4、关于G上内存分配的问题首先我们知道G/M/P,G可能和M也可能和P解除绑定,那么关于数据变量放在哪哇!其实这个就是逃逸分析!\n1、什么情况下会逃逸package maintype Demo struct {\tMain string}func main() {\tgo func() {\t\tdemo := Demo{}\t\tgo func() {\t\t\ttest(demo)\t\t}()\t}()}func test(demo Demo) {}\n\n输出可以看到其实没有发生逃逸,那是因为 demo被拷贝它自己的栈空间内\n[root@647af84b3319 code]# go build -gcflags "-N -l -m" -o bin/app deno/main2.go# command-line-argumentsdeno/main2.go:13:11: demo does not escapedeno/main2.go:6:5: func literal escapes to heapdeno/main2.go:8:6: func literal escapes to heap\n\n备注:\n -gcflags "-N -l -m" 其中 -N 禁用优化 -l禁止内联优化,-m打印逃逸信息\n那么继续改成这个\npackage maintype Demo struct {\tMain string}func main() {\tgo func() {\t\tdemo := Demo{}\t\tgo func() {\t\t\ttest(&demo)\t\t}()\t}()}func test(demo *Demo) {}\n\n可以看到发现 demo对象其实被逃逸到了堆上!这就是不会出现类似于G如果被别的M执行,其实不会出现内存分配位置的问题!\n[root@647af84b3319 code]# go build -gcflags "-N -l -m" -o bin/app deno/main2.go# command-line-argumentsdeno/main2.go:13:11: demo does not escapedeno/main2.go:7:3: moved to heap: demodeno/main2.go:6:5: func literal escapes to heapdeno/main2.go:8:6: func literal escapes to heap\n\n2、for循环中申明g内存引用问题所以可以看到demo其实是copy到了堆上!这就是g逃逸的问题,和for循环一样的\nfunc main() {\tfor x := 0; x < 10; x++ {\t\tgo func() {\t\t\tfmt.Println(x)\t\t}()\t}}\n\n执行可以发现,其实x已经逃逸到了堆上,所以你所有的g都引用的一个对象,如何解决了\n➜ go-tool git:(master) ✗ go build -gcflags "-l -m" -o bin/app deno/main2.go# command-line-argumentsdeno/main2.go:10:6: moved to heap: xdeno/main2.go:11:6: func literal escapes to heapdeno/main2.go:12:15: main.func1 ... argument does not escapedeno/main2.go:12:15: x escapes to heapdeno/main2.go:16:11: test demo does not escape\n\n如何解决了,其实很简单\n\n直接copy一份数据\n\nfor x := 0; x < 10; x++ { x := x // clone go func() { fmt.Println(x) }()}\n\n\n通过参数传递(推荐)\n\nfor x := 0; x < 10; x++ {go func(x int) { fmt.Println(x)}(x)}\n\n参考文章也谈goroutine调度器\n图解Go运行时调度器\nGo语言回顾:从Go 1.0到Go 1.13\nGo语言原本\n调度系统设计精要\nScalable Go Scheduler Design Doc\n","categories":["Golang"],"tags":["Golang","调度器"]},{"title":"召回率和精确率","url":"/2024/03/11/e747ef7b44a7a88ef9e3c7cc5d8473cf/","content":"召回率和精确率是一些文档中常用的词语,怎么理解了?本文会简单、明了的介绍一下!\n\n\n概念参考:https://en.wikipedia.org/wiki/Precision_and_recall\nPrecision (also called positive predictive value) is the fraction of relevant instances among the retrieved instances. Written as a formula:\nPrecision=Relevant retrieved instancesAll retrieved instances\n\nRecall (also known as sensitivity) is the fraction of relevant instances that were retrieved. Written as a formula:\nRecall=Relevant retrieved instancesAll relevant instances\n\n\n\n例子参考: https://www.zhihu.com/question/19645541\n公园里有50只皮卡丘和10只臭臭泥,有正常审美的人都会想要用精灵球把尽可能多的皮卡丘抓回来,同时尽可能少地抓住臭臭泥!最终我们的精灵球成功抓55只回来了,其中45只是皮卡丘和10是只臭臭泥!\n我们就可以说50只皮卡丘中有45只被召唤 (call) 回来 (re) 了,所以 recall = 45 / 50,但同时,这台机器还误把5只臭臭泥识别为皮卡丘,在它抓回来的所有55只神奇宝贝中,精灵球对皮卡丘判断的精确率 (precision) = 45 / 55 !\n\nTP = 45 (真正,抓对的)\nFP = 55 - 45 = 10(假正,抓错了[误报],例如我把10只臭臭泥当成了皮卡丘)\nFN = 50 - 45 = 5(假负,将现实的True判断为Negative,没抓住[漏报],例如这里我有5只皮卡丘没抓住)\nTN = 10 - 10 = 0 (真负,全部的负集-误报,这里我们把错误的全抓了,所以这里为0)\n\nrecall(召回率) = TP/(TP+FN) = 45/50 抓回来多少,召回率越高需要降低精确率保证全都得抓住,可能抓错的就回多!precision(精确率) = TP/(TP+FP) = 45/55 抓对了多少,提高精确率需要保证你抓错的尽可能的少,可能抓回来的比较少!\n实际应用例如有一个监控报警的服务,通常来说对于一个流经LB的服务需要统计SLA,那么如果我们把报警的条件设计成5xx才报警的话,会造成漏报,导致一些对于4xx敏感的业务报警没报警出来,此时如果我们把条件变成4xx/5xx就会导致误报很多!\n所以实际需求中需要根据业务实际情况来评估召回率/精确率,通常报警系统需要保证召回率尽可能的高需要保证90%以上(避免漏报),精确率只要保证20%左右就行了!\n总结避免漏报,就需要提高召回率,对应到代码里就是放宽条件限制!\n避免误报,就需要提高精确率,对应到代码里就是多加一些条件判断!\n综合来看需要一个权衡,不同业务场景要求的召回率和精确率是不一样的!\n中文中有些不严谨,例如 精确率、准确率 这俩词其实一个意思,但是 precision、accuracy 是俩意思,注意很多文章拿精确率、准确率来做区分,本文所讲的主要是 precision!\n","categories":["统计学"],"tags":["统计学"]},{"title":"singleflight和backoff包介绍和组合使用","url":"/2022/02/28/efabb79e15af38ef629520bf36df0adf/","content":" 业务中我们经常遇到一些重复使用的轮子代码,本篇介绍了 singleflight 和 backoff 以及本地缓存!来提高我们平时业务开发的效率和代码的精简度!\n\n\nsingleflight介绍\n源码位置: https://github.com/golang/groupcache/tree/master/singleflight 或者 golang.org/x/sync/singleflight\n\n主要是google 开源的group cache 封装的sdk,目的是为了解决 cache 回源的时候,容易出现并发加载一个或者多个key,导致缓存击穿\n\n其中 golang.org/x/sync/singleflight 它提供了更多方法,比如异步加载等!但是代码量增加了很多,比如很多异步的bug之类的!\n\n\n简单使用\n简单模拟100个并发请求去加载k1\n\npackage mainimport (\t"fmt"\t"sync"\t"github.com/golang/groupcache/singleflight")var (\tcache sync.Map\tsf = singleflight.Group{})func main() {\tkey := "k1" // 假如现在有100个并发请求访问 k1\twg := sync.WaitGroup{}\twg.Add(100)\tfor x := 0; x < 100; x++ {\t\tgo func() {\t\t\tdefer wg.Done()\t\t\tloadKey(key)\t\t}()\t}\twg.Wait()\tfmt.Printf("result key: %s\\n", loadKey(key))}func loadKey(key string) (v string) {\tif data, ok := cache.Load(key); ok {\t\treturn data.(string)\t}\tdata, err := sf.Do(key, func() (interface{}, error) {\t\tdata := "data" + "|" + key\t\tfmt.Printf("load and set success, data: %s\\n", data)\t\tcache.Store(key, data)\t\treturn data, nil\t})\tif err != nil {\t\t// todo handler\t\tpanic(err)\t}\treturn data.(string)}// output//load and set success, data: data|k1//load and set success, data: data|k1//result key: data|k1\n\n可以看到输出中,其中有2次去 loadKeyFromRemote 去加载,并没有做到完全防止得到的作用\n\n如何解决上诉问题了,问题出在哪了?我们进行简单的源码分析\n\n源码分析\n数据结构\n\n// call is an in-flight or completed Do calltype call struct {\twg sync.WaitGroup\tval interface{}\terr error}// Group represents a class of work and forms a namespace in which// units of work can be executed with duplicate suppression.type Group struct {\tmu sync.Mutex // protects m\tm map[string]*call // lazily initialized // 懒加载}\n\n\n主逻辑\n\nfunc (g *Group) Do(key string, fn func() (interface{}, error)) (interface{}, error) {\tg.mu.Lock() // lock\tif g.m == nil { // 懒加载\t\tg.m = make(map[string]*call)\t} // 如果key存在,则wait\tif c, ok := g.m[key]; ok {\t\tg.mu.Unlock()\t\tc.wg.Wait()\t\treturn c.val, c.err\t} // new caller + wg add + set\tc := new(call)\tc.wg.Add(1)\tg.m[key] = c\tg.mu.Unlock()\t // 调用方法\tc.val, c.err = fn() // notify\tc.wg.Done()\t // 删除key,防止内存泄漏\tg.mu.Lock()\tdelete(g.m, key)\tg.mu.Unlock()\treturn c.val, c.err}\n\n\n(1) 首先会去初始化一个 caller,然后waitgroup ++ ,然后set [锁]\n(2) 然后调用方法,再done [无锁]\n(3) 最后删除 key [锁]\n(4) 其他同一个key并发请求,会发现key存在,则直接wait了!\n\n假如现在并发请求,那么此时假如都加载同一个key,那么只有一个key先经过,但是计算机执行的很快,在第(2)和(3)步执行的很快,导致key已经删除,但是还有请求未开始 Do 方法或者到了 g.m[key] 这一步,都是会再次重新走一遍\n\n问题? 能不能使用读写锁优化加锁了?\n\n假如读取key加的读锁,那么此时最长流程变为: 读锁 + 写锁 + 写锁, 最短流程变为: 读锁, 当特别高的并发才会有较为大的提升!\n优化后用法package mainimport (\t"fmt"\t"sync"\t"github.com/golang/groupcache/singleflight")var (\tcache sync.Map\tsf = singleflight.Group{})func main() {\tkey := "k1" // 假如现在有100个并发请求访问 k1\twg := sync.WaitGroup{}\twg.Add(100)\tfor x := 0; x < 100; x++ {\t\tgo func() {\t\t\tdefer wg.Done()\t\t\tloadKey(key)\t\t}()\t}\twg.Wait()\tfmt.Printf("result key: %s\\n", loadKey(key))}func loadKey(key string) (v string) {\tif data, ok := cache.Load(key); ok {\t\treturn data.(string)\t}\tdata, err := sf.Do(key, func() (interface{}, error) {\t\tif data, ok := cache.Load(key); ok { // 双重检测\t\t\treturn data.(string), nil\t\t}\t\tdata := "data" + "|" + key\t\tfmt.Printf("load and set success, data: %s\\n", data)\t\tcache.Store(key, data)\t\treturn data, nil\t})\tif err != nil {\t\t// todo handler\t\tpanic(err)\t}\treturn data.(string)}// output//load and set success, data: data|k1//result key: data|k1\n\nbackoff介绍\n源码地址: github.com/cenkalti/backoff\n\n主要是解决补偿的操作,当业务/方法遇到异常的情况,通常会有补偿的操作,一般就是业务继续重试\n\n我经常使用这个包做重试,感觉比较好用!不用自己写for循环了\n\n\n简单使用\n模拟一个异常,去加载一个data数据,当遇到偶数的时候就爆异常!\n\npackage mainimport (\t"fmt"\t"math/rand"\t"time"\t"github.com/cenkalti/backoff")func main() {\tvar (\t\tdata interface{}\t)\tif err := backoff.Retry(func() error {\t\tif rand.Int()%2 == 0 { // 模拟异常\t\t\terr := fmt.Errorf("find data mod 2 is zero")\t\t\tfmt.Printf("find err, err: %s\\n", err)\t\t\treturn err\t\t}\t\tdata = "load success"\t\treturn nil\t}, backoff.WithMaxRetries(backoff.NewConstantBackOff(time.Millisecond*1), 3)); err != nil {\t\tpanic(err)\t}\tfmt.Printf("data: %s\\n", data)}//output//find err, err: find data mod 2 is zero//data: load success\n\n结果可以看到很好的解决了重试的问题!代码很优雅!\n\n关于为啥业务中重试都喜欢等待一下,其实比较佛学!\n\nsdk介绍\nback off\n\ntype BackOff interface {\t// NextBackOff returns the duration to wait before retrying the operation,\t// or backoff. Stop to indicate that no more retries should be made.\t// 是否下一次,以及下一次需要等待的时间!\tNextBackOff() time.Duration\t// Reset to initial state.\tReset()}\n\n\n封装了四个基本的Backoff\n\n// 不需要等待,继续重试type ZeroBackOff struct{}func (b *ZeroBackOff) Reset() {}func (b *ZeroBackOff) NextBackOff() time.Duration { return 0 }// 不允许重试type StopBackOff struct{}func (b *StopBackOff) Reset() {}func (b *StopBackOff) NextBackOff() time.Duration { return Stop }// 每次重试等待相同的时间type ConstantBackOff struct {\tInterval time.Duration}func (b *ConstantBackOff) Reset() {}func (b *ConstantBackOff) NextBackOff() time.Duration { return b.Interval }func NewConstantBackOff(d time.Duration) *ConstantBackOff {\treturn &ConstantBackOff{Interval: d}}// 重试back off,主要是计数重试的次数,以及基于委托代理模型,实现比较好的拓展// max=0 会无限重试下去func WithMaxRetries(b BackOff, max uint64) BackOff {\treturn &backOffTries{delegate: b, maxTries: max}}type backOffTries struct {\tdelegate BackOff\tmaxTries uint64\tnumTries uint64}func (b *backOffTries) NextBackOff() time.Duration {\tif b.maxTries > 0 {\t\tif b.maxTries <= b.numTries {\t\t\treturn Stop\t\t}\t\tb.numTries++\t}\treturn b.delegate.NextBackOff()}func (b *backOffTries) Reset() {\tb.numTries = 0\tb.delegate.Reset()}\n\n\n自适应backoff\n\n\n整个时间 < 15min,重试时间从500ms开始增长,每次增长1.5倍,直到60s每次!\n\n// NewExponentialBackOff creates an instance of ExponentialBackOff using default values.func NewExponentialBackOff() *ExponentialBackOff {\tb := &ExponentialBackOff{\t\tInitialInterval: DefaultInitialInterval,\t\tRandomizationFactor: DefaultRandomizationFactor,\t\tMultiplier: DefaultMultiplier,\t\tMaxInterval: DefaultMaxInterval,\t\tMaxElapsedTime: DefaultMaxElapsedTime,\t\tClock: SystemClock,\t}\tb.Reset()\treturn b}\n\n组合使用,构建一个本地缓存!这个应该是日常开发中经常用到的,本地缓存可以有效解决高频数据但是数据整体占用并不是特别的大,但是每次加载都需要额外的开销,所以基于本地缓存去构建一个可用性比较高的缓存框架!\n\n核心代码\n\npackage mainimport (\t"context"\t"fmt"\t"time"\t"github.com/cenkalti/backoff"\t"golang.org/x/sync/singleflight")var (\tlocalCacheCallbackIsNil = fmt.Errorf("cache callback func is nil"))type CacheOption interface {}type Cache interface {\tGet(key string) (value interface{}, isExist bool)\tSet(key string, value interface{}, opts ...CacheOption)}type WrapperCache interface {\tGetData(ctx context.Context, key string, callback func(ctx context.Context) (interface{}, error)) (v interface{}, err error)}type wrapperCache struct {\tname string\tcache Cache\tsingleflight singleflight.Group\tretrySleepTime time.Duration\tretryNum uint64}func NewWrapperCache(name string, cache Cache) WrapperCache {\treturn &wrapperCache{\t\tname: name,\t\tcache: cache,\t\tretryNum: 3,\t\tretrySleepTime: time.Millisecond * 10,\t}}// emitHitCachedMetric 计算缓存命中率func (c *wrapperCache) emitHitCachedMetric(hit bool) {}func (c *wrapperCache) GetData(ctx context.Context, key string, callback func(ctx context.Context) (interface{}, error)) (v interface{}, err error) {\tif result, isExist := c.cache.Get(key); isExist {\t\tc.emitHitCachedMetric(true)\t\treturn result, nil\t}\tif callback == nil {\t\treturn nil, localCacheCallbackIsNil\t}\tc.emitHitCachedMetric(false)\tresult, err, _ := c.singleflight.Do(key, func() (interface{}, error) {\t\t// 双重检测,防止singleflight 锁的key失效\t\tif result, isExist := c.cache.Get(key); isExist {\t\t\treturn result, nil\t\t}\t\tvar callBackData interface{}\t\tif err := backoff.Retry(func() error {\t\t\tif data, err := callback(ctx); err != nil {\t\t\t\treturn err\t\t\t} else {\t\t\t\tcallBackData = data\t\t\t\treturn nil\t\t\t}\t\t}, backoff.WithMaxRetries(backoff.NewConstantBackOff(c.retrySleepTime), c.retryNum)); err != nil {\t\t\t// todo add log\t\t\treturn nil, err\t\t}\t\tc.cache.Set(key, callBackData)\t\treturn callBackData, nil\t})\tif err != nil {\t\treturn nil, err\t}\treturn result, nil}\n\n\ncache 实现\n\n这里介绍一下sync.Map为一个无过期的本地缓存和 go-cache有ttl的缓存框架!或者你自己去实现一个也可以!\nimport ( "sync"\t"github.com/patrickmn/go-cache")type localCache struct {\tsync.Map}func (l *localCache) Get(key string) (value interface{}, isExist bool) {\treturn l.Load(key)}func (l *localCache) Set(key string, value interface{}, opts ...CacheOption) {\tl.Store(key, value)}type goCache struct {\t*cache.Cache}func (l goCache) Set(key string, value interface{}, opts ...CacheOption) {\tl.SetDefault(key, value)}\n\n\n测试用例\n\nimport (\t"context"\t"strconv"\t"sync"\t"sync/atomic"\t"testing"\t"time"\t"github.com/patrickmn/go-cache"\t"github.com/stretchr/testify/assert")func TestNewCached(t *testing.T) {\tcached := NewWrapperCache("test", goCache{\t\tCache: cache.New(time.Second*10, time.Second*30),\t})\t//cached := NewWrapperCache("test", &localCache{})\tctx := context.Background()\twg := sync.WaitGroup{}\tvar (\t\tloadTime uint64 = 0\t\tcurrG = 20\t)\twg.Add(currG)\tfor x := 0; x < currG; x++ {\t\tgo func(x int) {\t\t\tdefer wg.Done()\t\t\tfor y := 0; y < 200000; y++ {\t\t\t\tkey := y % 10\t\t\t\tresult, err := cached.GetData(ctx, strconv.Itoa(key), func(ctx context.Context) (interface{}, error) {\t\t\t\t\tatomic.AddUint64(&loadTime, 1)\t\t\t\t\tt.Logf("load key: %s, num: %d, g_id: %d\\n", strconv.Itoa(key), y, x)\t\t\t\t\treturn int(key), nil\t\t\t\t})\t\t\t\tif err != nil {\t\t\t\t\tt.Fatal(err)\t\t\t\t}\t\t\t\tif result.(int) != key {\t\t\t\t\tt.Fatal("data is not eq err")\t\t\t\t}\t\t\t}\t\t}(x)\t}\twg.Wait()\tfor x := 0; x < 10; x++ {\t\tresult, _ := cached.GetData(ctx, strconv.Itoa(x), nil)\t\tt.Log(result)\t\tassert.Equal(t, result.(int), int(x))\t}\tassert.Equal(t, int(loadTime), int(10))}","categories":["Golang"],"tags":["singleflight","backoff"]},{"title":"Go的runtime.SetFinalizer函数介绍","url":"/2022/02/28/f2436dad6e7955374f33b91fcf1ddca0/","content":" 业务中我们经常遇到需要进行手动回收的操作,虽然Go提供了defer操作可以用来手动回收,但是有些时候确实会出现一些case用户忘记手动回收,并且大量内存泄漏或者goroutine泄口的问题,而且只能通过线上工具进行事后定位!本文介绍一下 runtime.SetFinalizer 来解决对象回收释放资源的问题!本文只是根据简单的例子进行阐述,例子选择不一定的好!\n\n\n介绍\nruntime.SetFinalizer 是Go提供对象被GC回收时的一个注册函数,可以在对象被回收的时候回掉函数\n此方法类似于JAVA的finalize 方法和C++的析构函数!\n当存在多层引用时,类似于A->B->C 这种关系的时候,是如何解决呢?\nGo函数内部原理介绍\n\nfunc SetFinalizer(obj interface{}, finalizer interface{}) { .... // 对象的低5位是对象类型,这里检测一下是否是指针类型\tif etyp.kind&kindMask != kindPtr {\t\tthrow("runtime.SetFinalizer: first argument is " + etyp.string() + ", not pointer")\t} // 第二个参数必须是函数\tif ftyp.kind&kindMask != kindFunc {\t\tthrow("runtime.SetFinalizer: second argument is " + ftyp.string() + ", not a function")\t} //\t// make sure we have a finalizer goroutine\tcreatefing() // finally add finalizer\tsystemstack(func() {\t\tif !addfinalizer(e.data, (*funcval)(f.data), nret, fint, ot) {\t\t\tthrow("runtime.SetFinalizer: finalizer already set")\t\t}\t})}// 最后启动调度池子,也就是只有一个G去回收整个应用程序的finalize函数!func createfing() {\t// start the finalizer goroutine exactly once\tif fingCreate == 0 && atomic.Cas(&fingCreate, 0, 1) {\t\tgo runfinq()\t}}// 1. addfinalizer 就是给对象的指针指向的内存加了个特殊标记!此标记此对象是finalizer对象,内部实现就是拿到对象的span,然后span里面有个链表维护对象的特殊标记!// 2. runfinq 函数,就是遍历一个队列,然后回收队列中的对象,一个死循环罢了!如果没有等待回收的对象,就park住// 3. 每次当GC sweep 阶段,会先标记,然后第二次GC才要被回收(清理)!具体逻辑可以看mspan#sweep\n\n简单使用package mainimport (\t"fmt"\t"runtime"\t"time")type object intfunc (o object) Ptr() *object {\treturn &o}var (\tcacheData = make(map[string]*object, 1024))func deleteData(key string) {\tdelete(cacheData, key)}func setData(key string, v object) {\tdata := v.Ptr()\truntime.SetFinalizer(data, func(data *object) {\t\tfmt.Printf("runtime invoke Finalizer data: %d, time: %s\\n", *data, time.Now().Format("15:04:05.000"))\t\ttime.Sleep(time.Second)\t})\tcacheData[key] = data}func main() {\tsetData("key1", 1)\tsetData("key2", 2)\tsetData("key3", 3)\tdeleteData("key1")\tdeleteData("key2")\tdeleteData("key3")\tfor x := 0; x < 5; x++ {\t\tfmt.Println("invoke runtime.GC()")\t\truntime.GC()\t\ttime.Sleep(time.Second)\t}}// output://invoke runtime.GC()//runtime invoke Finalizer data: 1, time: 23:44:49.013//invoke runtime.GC()//runtime invoke Finalizer data: 3, time: 23:44:50.019//invoke runtime.GC()//runtime invoke Finalizer data: 2, time: 23:44:51.020//invoke runtime.GC()//invoke runtime.GC()\n\n\n并没有看出第二次才会GC掉,可能是系统在delele过程中触发过一次GC\n可以看到GC后调用Finalizer 函数是串行执行的!\n\n日常使用注意点\nGC注册的Finalizer函数执行时间不适合过长!\nFinalizer 函数返回结果是系统会忽略,所以你返回error也无所谓,但是切记不可以panic,程序是无法recover的!\n如果对象在Finalizer函数再次被引用,是不会被再次回收调用Finalizer函数的!\n当存在 A->B->C 的引用时,回收顺序是引用顺序,当回收A后,然后再回收B,然后再回收C,应该没啥问题吧\n如果你对象被goroutine 引用而分配到堆上,goroutine 又没办法关闭,导致你需要包装一层对象进行回收!例如下列例子,导致业务函数结束后忘记了Close,导致G泄漏,虽然注册了Finalizer函数,但是没有被回收!我就犯过这样的错误,不过在写测试用例的时候就发现了!!\n\ntype cacheData struct {\tname string\tdataLock sync.RWMutex\tdata map[string]interface{}\treporter func(data *cacheData)\tcloseOnce sync.Once\tdone chan struct{}}func NewCacheData(name string) *cacheData {\tdata := &cacheData{\t\tname: name,\t\tdata: map[string]interface{}{},\t\treporter: func(data *cacheData) {\t\t\tlog.Println("reporter")\t\t},\t\tdone: make(chan struct{}, 0),\t}\tdata.init()\truntime.SetFinalizer(data, (*cacheData).Close)\treturn data}// init 注册reporter函数,比如上报一些缓存的信息func (c *cacheData) init() {\tgo func() {\t\tc.reporter(c)\t\tt := time.NewTicker(time.Second)\t\tfor {\t\t\tselect {\t\t\tcase <-c.done:\t\t\t\tt.Stop()\t\t\t\treturn\t\t\tcase <-t.C:\t\t\t\tc.reporter(c)\t\t\t}\t\t}\t}()}// Close 函数主要是防止goroutine泄漏func (c *cacheData) Close() {\tc.closeOnce.Do(func() {\t\tclose(c.done)\t})}func BizFunc() {\tcache := NewCacheData("test")\tcache.Set("k1", "v1")\t// biz ....\t// 但是忘记关闭cache了,或者等等的没有close,导致G泄漏}func main() {\tBizFunc()\tfor x := 0; x < 10; x++ {\t\truntime.GC()\t\tlog.Println("runtime.GC")\t\ttime.Sleep(time.Second)\t}}\n\n如何解决了?? 可以看 NewSafeCacheData\npackage mainimport (\t"log"\t"runtime"\t"sync"\t"time")func init() {\tlog.SetFlags(log.Ltime)}func (c *cacheData) Set(key string, v interface{}) {\tc.dataLock.Lock()\tdefer c.dataLock.Unlock()\tc.data[key] = v}type CacheData struct {\t*cacheData}type cacheData struct {\tname string\tdataLock sync.RWMutex\tdata map[string]interface{}\treporter func(data *cacheData)\tcloseOnce sync.Once\tdone chan struct{}}func NewCacheData(name string) *cacheData {\tdata := &cacheData{\t\tname: name,\t\tdata: map[string]interface{}{},\t\treporter: func(data *cacheData) {\t\t\tlog.Println("reporter")\t\t},\t\tdone: make(chan struct{}, 0),\t}\treturn data}// NewSafeCacheData 安全的函数func NewSafeCacheData(name string) *CacheData {\tdata := NewCacheData(name)\tdata.init()\tresult := &CacheData{\t\tcacheData: data,\t}\truntime.SetFinalizer(result, (*CacheData).Close)\treturn result}// init 注册reporter函数,比如上报一些缓存的信息func (c *cacheData) init() {\tgo func() {\t\tc.reporter(c)\t\tt := time.NewTicker(time.Second)\t\tfor {\t\t\tselect {\t\t\tcase <-c.done:\t\t\t\tt.Stop()\t\t\t\treturn\t\t\tcase <-t.C:\t\t\t\tc.reporter(c)\t\t\t}\t\t}\t}()}// Close 函数主要是防止goroutine泄漏func (c *cacheData) Close() {\tc.closeOnce.Do(func() {\t\tclose(c.done)\t})}func BizFunc() {\tcache := NewSafeCacheData("test")\tcache.Set("k1", "v1")\t// biz ....\t// 但是忘记关闭cache了,或者等等的没有close,导致G泄漏}func main() {\tBizFunc()\tfor x := 0; x < 10; x++ {\t\truntime.GC()\t\tlog.Println("runtime.GC")\t\ttime.Sleep(time.Second)\t}}","categories":["Golang"],"tags":["Golang"]},{"title":"接口mock平台","url":"/2021/03/02/f86786a14802b2f17d560d87387e8690/","content":" 接口Mock平台主要实践在项目的开发团队太多,业务需要对接各个下层服务,而下层服务提供的API时间线的偏差,往往需要Mock接口提供给需求方(前端),进行联调,提高交付质量。最后再由我们去对接下层服务来保证交付质量,同时下层服务也需要提供mock来格式化响应参数,但是往往省略这部分工作导致接口文档可用性太低。\n\n\n1、Yapi\n由去哪儿网大前端技术中心 开源的项目\n官方文档:https://hellosean1025.github.io/yapi/documents/index.html\ngithub:https://github.com/ymfe/yapi\n\n1、界面介绍:\n 请求 Mock 数据时,规则匹配优先级:Mock 期望 > 自定义 Mock 脚本 > 项目全局 mock 脚本 > 普通 Mock。如果前面匹配到 Mock 数据,后面 Mock 则不返回。\n\n1、预览界面\n 大概介绍,无其他用处,唯一用处就是接口Mock\n\n\n2、编辑界面功能 (优先级 level-3)1、特点这个是数据Mock,支持基本的语法,这个语法来自于 Mock数据占位符定义规范 DPD , 他支持调用 Mock.Random 的任何方法,比如调用 Random.datetime('yy-MM-dd a HH:mm:ss'), 你只需要写 @data('yy-MM-dd a HH:mm:ss') 即可\n2、适用场景\n比较适用于请求参数的Mock\n响应字段不复杂,没有复杂的数组结构,不需要定制化处理\n\n\n3、运行页面1、特点\n支持多种环境的支持\n\n\n\n支持测试mock接口\n支持调试大部分环境\n\n\n2、适用场景\n接口调试(但是无历史记录,这个比较坑)\n和配置进行比较\n\n4、高级Mock-期望 (优先级最高-level 1)1、特点可以对于数据的响应进行期望值设定,完全支持 Mock.js 的语法,语法文档: http://mockjs.com/examples.html\n2、适用场景\n对于数据mock需要定制化处理的,比如我们需要将list进行指定mock\n但是它不适合对于数据进行脚本处理,比如要对于请求参数进行校验等等\n可以对于响应时长进行mock\n可以对于\n\n例子:\n{ "code": 0, "message": "success", "data": { "list|40-50": [ { "worker_name": "@cname", "id": "@integer(0)", "create_time": "@date('yyyy-MM-dd HH-mm-ss')", "status": "@integer(1,4)", "discover_place": "@city" } ], "nums_list": [ { "status": 1, "num": "@integer(1,1000)" }, { "status": 2, "num": "@integer(1,1000)" }, { "status": 3, "num": "@integer(1,1000)" }, { "status": 3, "num": "@integer(1,1000)" } ], "page_index": 1, "page_size": 10, "total_count": 50, "is_more": 1 }}\n\n\n5、高级Mock - 脚本 (优先级 level-2)1、特点脚本带来的强大功能,可以各种定制化处理,比如说请求参数,比如说请求头等等,比如说各种逻辑判断\n请求\n\nheader 请求的 HTTP 头\nparams 请求参数,包括 Body、Query 中所有参数\ncookie 请求带的 Cookies\n\n响应\n\nmockJson 接口定义的响应数据 Mock 模板\nresHeader 响应的 HTTP 头\nhttpCode 响应的 HTTP 状态码\ndelay Mock 响应延时,单位为 ms\nRandom Mock.Random 方法,可以添加自定义占位符,详细使用方法请查看 Wiki\n\n2、使用场景\n响应结果需要对于请求参数有依赖的\n\ndemo\nlet data=[]for (let x =0 ; x<100;x++){ data[x]={ name:Random.string(), }}mockJson={ list: data}\n\n\n2、语法介绍英文(中文前缀为c,例如cfirst)名字:Random.first()\n英文性:Random.last()\n全名字:Random.name()\n随机的时间搓(ms):Random.datetime(‘T’)\n随机日期:Random.datetime(‘yyyy-MM-dd HH:mm:ss’)\n随机时间:Random.now(‘yyyy-MM-dd HH:mm:ss’)\n随机文本:Random.csentence(length)\n随机的http-url: Random.url(‘http’, ‘nuysoft.com’)\n随机的ip: Random.ip()\n随机的email: Random.email(‘nuysoft.com’) ,Random.email()\n随机的地址: 随机城市:Random.city(true) 随机县城:Random.county(true) 随机省份:Random.province()\nUUID: Random.guid()\n随机身份证:Random.id()\n随机数:Random.range(1, 10, 2)\n随机英文串串:Random.string( 5 )\n随机bool:Random.bool()\n","categories":["API"],"tags":["yapi","mock"]},{"title":"C++ 入门到放弃","url":"/2023/04/06/fd8e40efcdb71f2be44fb720dc582d67/","content":"C++目前在一些领域处于垄断地位,比如数据库内核、高性能网络代理、基础软件设施 等基本都是C/C++的垄断领域,虽然其他语言也有在做,但是生态、性能等都无法企及,其次C/C++有着丰富的生态,很多高级语言也提供了接口可以对接C/C++ (JNI/CGO等) ,这样你可以很方便的将一些底层C/C++库链接到自己的项目中,避免造轮子!本人学习C++目的是为了看懂别人的代码,因为很多优秀的项目都是C++写的,而非我从事C++相关领域开发!\n个人觉得C++本身包含了几门语言:面向对象语言 + 内存管理语言 + 模版语言,其中最臭名昭著的就是模版,大量的SFINAE实现,难以理解的报错信息,让很多人讨厌C++,其次就是C++委员会对于各种语法的支持!\n本篇文章会长期更新和补充,而且篇幅过长,我平时喜欢把学习语言语法相关的文档归类到一起,所以会存在体积较大的问题,方便平时当作工具书使用,可能有点啰嗦了,这个是我从学习一开始记录的文章,不同阶段理解程度是不一样的!\n\n\n学习环境个人觉得如果你是一个新手,一定要选一个利于学习的环境,个人比较推荐新手用 clion(或者vscode+clangd 非常适合非cmake项目或者比较大的项目)!目前C++ 版本应该已经到了C++23了 !编译器的话比较推荐clang,编译工具的话推荐cmake,版本的话目前比较推荐 C++17,不过2023年了更加推荐C++20!camke学习成本并不是太高(bazel复杂度有点高),可以看我写的文章: cmake入门!\n\n11:STL + 智能指针\n14、17 优化了语法和新增部分API,所以如果不用c++20最好的选择就是c++17了!\n20 支持了 coroutine(无栈协程)、concept(新版的SFINAE)、模块(目前还没大量使用)\n\n如果你是c++开发同学最好选择自己公司的编译工具和开发规范!C++规范,按照公司的来即可,如果没有的话可以参考Google的:https://github.com/google/styleguide\n学习文档的话,语法学习仅建议学习官方文档:https://en.cppreference.com/w/cpp/language, 原因就是内容最全面、分类最具体,如果你东看西看可能概念很模糊!技巧学习的话我建议多看看开源项目,其次就是看一下经验的书 Effective c++ 和 C++ Template 第二版,实践才是硬道理。\nC++的语法应该是没有任何一个语言能超越的,复杂恶心,所以死啃开源项目,啃完就好了,最难理解的就是模版元编程!\n从hello world 开始#include <iostream>int main() { std::cout << "Hello" << " " << "World!" << std::endl;}\n\n不清楚大家对于上面代码比较好奇的是哪里了?比如说我好奇的是为啥<< 就可以输出了, 为啥还可以 << 实现 append 输出? 对,这个就是我的疑问!\n思考一下是不是等价于下面这个代码了?是不是很容易理解了就!可以把 operator<< 理解为一个方法名! 具体细节下文会讲解!\n#include <iostream>int main() { std::operator<<(std::cout,"Hello").operator<<(" ").operator<<("World!").operator<<(std::endl);}\n\n内置类型注意C++很多时候都是跨端开发,所以具体基础类型得看你的系统环境,常见的基础类型你可以直接在 https://en.cppreference.com/w/cpp/language/types 这里查看 !\nchar* 和 char[] 和 std::string\n本块内容可以先了解一遍,看完本篇内容再回头看一下会理解一些!\n\n字符串在编程中处于一个必不可少的操作,那么C++中提供的 std::string 和 char* 区别在呢了?\n简单来说const char* xxx= "字面量" 的性能应该是最高的,因为字面量分配在常量区域,更加安全,但是注意奥不可修改的!\nchar[]= "字面量" | new char[]{} 分配在栈上或者堆上非常不安全,这种需求直接用 std::vector 或者 std::array 更好!\nstd::string 在C++11有了移动语意后,性能已经在部分场景优化了很多,进行字符串操作比较多的话介意用这个,别乱用std::string* 。使用 std::string 一般不会涉及到内存安全问题,无非就是多几次拷贝! 如果用指针最好也别用裸指针,别瞎new,可以用智能指针,或者参数[引用]传递!\n下面是一个简单的例子,可以参考学习!\n #include <cstring>#include <iostream>using namespace std;const char* getStackStr() { char arr[] = "hello world"; // 不能这么返回,属于不安全的行为,因为arr分配在栈上,你返回了一个栈上的地址,但是这个函数调用这个栈就消亡了,所以不安全! return arr;}const char* getConstStr() { // 不会有内存安全问题,就是永远指向常量池的一块内存 // 对于这种代码,我们非常推荐用 const char* const char* arr = "hello world"; return arr;}const char* getHeapStr() { // stack 分配在栈上, 将数据拷贝到返回函数 arr上! char stack[] = "hello world"; char* arr = new char[strlen(stack) + 1]{}; strcpy(arr, stack); *arr = 'H'; arr[1] = 'E'; // arr 分配在堆上,我们返回了一个裸指针,用户需要手动释放,不释放有内存安全问题 return arr;}// 这里std::string直接分配在堆上, 它的回收取决于 std::unique_ptr 的消亡, 具体有兴趣可以看下智能指针// 注意: 千万别用函数返回一个裸指针,那么它是非常不安全的,需要手动释放!std::unique_ptr<std::string> getUniquePtrStr() { auto str = std::unique_ptr<std::string>(new std::string("hello world.")); str->append(" i am from heap and used unique_ptr."); return str;}// 注意: 这里返回的str实际上进行了一次拷贝,实现在std::string的拷贝构造函数上!std::string getStdStr() { std::string str = "hello world."; str += " i am from stack and used copy constructor."; return str;}int main() { // a是一个指针,指向常量区, "hello world" 分配在常量区,对于这种申明C++11推荐用 const 标记出来,因为常量区我们程序运行时是无法修改的 char* a = "hello world"; // b是一个指针,指向常量区,"c++" 分配在常量区 // 常量区编译器会优化,也就是说 a 和 b 俩人吧他们的内容都一模一样,那么所以常量只有一份 const char* b = "hello world"; const char* c = "hello world c"; printf("%p\\n", a); printf("%p\\n", b); printf("%p\\n", c); // arr 分配在栈上,当函数调用结束就销毁了! char arr[] = "1111"; // 乱码!!! cout << getStackStr() << endl; // 正常 cout << getConstStr() << endl; // 常量是不会重复分配内存的,所以下面3个输出结果是一样的! auto arr1 = getConstStr(); auto arr2 = getConstStr(); printf("%p\\n", arr1); printf("%p\\n", arr2); printf("%p\\n", b); auto arr3 = getHeapStr(); // 正常打印 cout << arr3 << endl; // 需要手动释放 delete[] arr3; // std::string 是一个类,也就是说它内存开销非常的高,而且对于大的数据会分配在堆上性能以及效率会差一些! // 这里本质上调用的是 str的copy constructor函数,属于隐式类型转换! std::string str = arr1; cout << str << endl; printf("%p\\n", str.data()); // 业务中如何使用 std::string了,最好使用std:unique_ptr,可以减少内存的拷贝! // c++ 中一般不推荐return一个复杂的数据结构(因为涉及到拷贝, // 或者你就用指针,或者C++11引入了移动语意,降低拷贝),而是推荐通过参数把返回变量传递过去,进而减少拷贝! cout << *getUniquePtrStr() << endl; cout << getStdStr() << endl;}\n\n注意点关于 x++ 和 ++x首先学过Java/C的同学都知道,x++ 返回的是x+1之前的值, ++x返回的是x+1后的值! 他俩都可以使x加1,但是他俩的返回值不同罢了!\n#include <iostream>using namespace std;// 实现 x++int xadd(int& x) { int tmp = x; x = x + 1; return tmp;}// 实现 ++xconst int& addx(int& x) { x = x + 1; return x;}int main() { int x = 10; // int tmp = x++; int tmp = xadd(x); cout << "x: " << x << ", tmp: " << tmp << endl; // reset x = 10; // tmp = ++x; tmp = addx(x); cout << "x: " << x << ", tmp: " << tmp << endl; tmp = tmp + 1; cout << "x: " << x << ", tmp: " << tmp << endl; // 输出: // x: 11, tmp: 10 // x: 11, tmp: 11 // x: 11, tmp: 12}\n\n引用 (左值/右值/万能引用)引用本质上就是指针,但是它解决了空指针的问题,我个人觉得他是一个比较完美的解决方案!\n\n下面是一个简单的例子,可以看到引用的效果 (单说引用一般是指的左值引用)\n\nvoid inc(int& a, int inc) { a = a + inc;}using namespace std;int main(int argc, char const* argv[]) { int a = 1; inc(a, 10); cout << a << endl; return 0;}// 输出:// 11\n\n\n其实上面这个例子(inc函数)属于左值引用,为什么叫左值引用,是因为它只能引用 左值(lvalue) , 你可以理解为左值 是一个被定义类型的变量,那么它一定可以被取址(因为左引用很多编译器就是用的指针去实现的), 右值则相反,例如字面量; 右值包含纯右值(prvalue)和将亡值(xvalue)(将亡值我个人理解是如果没有使用那么下一步就被回收了,生命到达终点的那种!)\n\nint x = 10;// x: 是一个变量,其内存分配在栈空间上,为左值,我可以取x的指针,那么x指针指向的就是栈上的某个空间// 10: 是一个字面量,为右值,如果没有x那么它就和谁也没关系,认为是垃圾(注意右值引用就是要用垃圾,让垃圾生命延续)\n\n\n下面是一个左值(lvalue)/右值(rvalue)/万能引用(Universal Reference)在实际开发中的例子\n\n#include <unordered_map>#include <string>#include <iostream>#include <shared_mutex>// 注意: -std=c++17template <typename K, typename V>struct SafeMap {public: // T 为万能引用(注意: 万能引用会涉及到类型推断, 区别于右值引用) // 万能引用一定要和std::forward(万能转发)结合使用, 不然没啥意义 template <class T> auto Get(T &&key) { std::shared_lock lock(mutex); return map[std::forward<T>(key)]; } // K 为右值引用 bool Exist(K &&key) { std::shared_lock lock(mutex); if (const auto &kv = map.find(key); kv == map.end()) { return false; } return true; } void Put(K key, const V &value) { std::unique_lock lock(mutex); map[key] = value; } auto Size() { std::shared_lock lock(mutex); return map.size(); }private: std::unordered_map<K, V> map; std::shared_mutex mutex;};template <typename T>using StringSafeMap = SafeMap<std::string, T>;int main() { std::string key = "1"; StringSafeMap<int> map; map.Put("1", 1); std::cout << map.Get("1") << std::endl; // 右值引用 std::cout << map.Get(key) << std::endl; // 左值引用 // map.Exist(key); // 编译不过去,因为没有定义左值引用函数 if (map.Exist("1")) { std::cout << "exist" << std::endl; } else { std::cout << "not exist" << std::endl; }}\n\n有兴趣的可以看文章:\n\nhttps://paul.pub/cpp-value-category/\nhttps://oi-wiki.org/lang/reference/\nhttps://blog.51cto.com/u_6343747/5464960\n\n总结: \n\n右值引用可以降低内存拷贝,但是需要实现移动语义!\n引用本质上就是指针,所以使用引用一定要注意对象的生命周期,推荐生命周期明确引用传递,不明确值传递(值传递可以通过移动进行优化本质上开销并不大)!\n常量左值引用,可传递右值!\n万能引用可以减少代码量,尤其是参数多的情况下,万能引用需要配合 std::forword 万能转发使用!\n\n类的初始化函数类的基本的成员函数\n这个是C++ 最难的地方,新手做到知道即可,不建议深挖,无底洞一个,显然禁止拷贝和移动才是最佳选择!\n\nC++ 的类,最基本也会有几个部分组成,就算你定义了一个空的类,那么它也会有(前提你使用了这些操作),和Java的有点像!\n\ndefault constructor: 默认构造函数\ncopy constructor: 拷贝构造函数 (注意: 编译器默认生成的拷贝构造函数是浅拷贝!)\ncopy assignment constructor: 拷贝赋值构造函数\ndeconstructor: 析构函数 !\nC++11引入了 move constructor (移动构造函数 ) 、 move assigment constructor(移动赋值构造函数),你不定义是不会生成的。\n\n下面例子我是根据此教程写的,大概可以解释6个函数 https://coliru.stacked-crooked.com/a/ae31c28f852e3220\n// g++ -std=c++17 -O0 -Wall main.cpp -o main && ./main#include <iostream>struct Memory {public: // 构造函数 explicit Memory(size_t size) : size_(size), data_(new char[size]) { std::cout << "Memory constructors" << std::endl; } // 析构函数 ~Memory() { clearMemory(); std::cout << "Memory deconstructors" << std::endl; } // 拷贝构造函数(就是创建一个b,把a拷贝到b) Memory(Memory &from) : size_(from.size_), data_(new char[from.size_]) { // 参数: start,end,dst std::copy(from.data_, from.data_ + from.size_, data_); std::cout << "Memory Copy constructors" << std::endl; } // 拷贝赋值函数(就是做拷贝,把a拷贝到b) Memory &operator=(const Memory &from) { // 很可能自身移动, 这里一般都需要这么处理 if (this == &from) { std::cout << "Memory Copy assignment constructors (=)" << std::endl; return *this; } std::cout << "Memory Copy assignment constructors (!=)" << std::endl; // 1. 清理自己 clearMemory(); // 2. 拷贝 this->size_ = from.size_; this->data_ = new char[from.size_]; std::copy(from.data_, from.data_ + from.size_, data_); return *this; } // 移动构造函数 Memory(Memory &&from) noexcept : size_(0), data_(nullptr) { std::cout << "Memory Move constructors" << std::endl; // https://blog.csdn.net/p942005405/article/details/84644069 // 1. std::move 强制变成了右值 // 2. 调用移动赋值构造函数 *this = std::move(from); } // 移动赋值构造函数, 这些函数需要 noexcept Memory &operator=(Memory &&from) noexcept { if (this == &from) { std::cout << "Memory Move Assignment constructors(=)" << std::endl; return *this; } std::cout << "Memory Move Assignment constructors(!=)" << std::endl; // 先清理自己的内存 delete[] data_; // 移动 data_ = from.data_; size_ = from.size_; // 标记空,防止析构函数失败 from.data_ = nullptr; from.size_ = 0; return *this; } // 这里没记录写偏移量,所以这里就只支持set函数了. void Set(const char *data, size_t size) { if (size > size_) { size = size_; } std::copy(data, data + size, data_); } friend std::ostream &operator<<(std::ostream &out, Memory &from) { if (from.data_ == nullptr) { return out << "null"; } return out << from.data_; }private: void clearMemory() { if (this->data_ == nullptr) { return; } delete[] this->data_; // 正常来说如果不存在移动可以这么写 this->data_ = nullptr; }private: size_t size_{}; char *data_{};};Memory getMemory(bool x) { if (x) { Memory mm(16); mm.Set("true", 4); return mm; } Memory mm(16); mm.Set("false", 5); return mm;}int main() { Memory memory1(20); // constructor memory1.Set("hello world", 11); std::cout << "memory1: " << memory1 << std::endl; Memory memory2 = memory1; // copy constructor (这个属于编译器优化了, 不然你这个代码也执行不通哇,因为我们没有默认构造函数, 所以左值是无法初始化的) std::cout << "memory2: " << memory2 << std::endl; Memory memory3(0); // constructor memory3 = memory2; // copy assignment constructor std::cout << "memory3: " << memory3 << std::endl; Memory memory4 = getMemory(true); // move constructor(如果未定义移动构造函数,则会调用拷贝构造函数,所以移动构造函数是不会默认生成的) std::cout << "memory4: " << memory4 << std::endl; memory3 = getMemory(false); // move constructor + move assignment constructor std::cout << "memory3: " << memory3 << std::endl; Memory memory5 = std::move(memory3); std::cout << "memory3: " << memory3 << std::endl; std::cout << "memory5: " << memory5 << std::endl; return 0;}\n\n总结:拷贝可以避免堆内存随意引用问题,比如我定义了A对象,此时我在A对象上分配了10M空间,此时B对象拷贝自我,那么此时B引用了A的10M内存,此时A/B回收的时候到底要清理A还是B的10M内存了? 第二个就是移动解决的问题,对于一些右值可能会存在冗余拷贝的问题,此时就可以使用移动优化内存拷贝。 本质上这些构造函数都是为了解决一个问题内存分配!!\n初始化列表这里我们要知道一点就是 C++ 类的初始化内置类型(builtin type)是不会自动初始化为0的,但是类类型(非指针类型)的话却会自动调用默认构造函数,具体为啥了,兼容C,不然会很慢,因为假如你要初始化一个类,例如定义了10个内置类型的字段,我需要10次赋值调用才能把10个字段初始化成0,而不初始化只需要开辟固定的内存空间即可,可以大大提高代码运行效率!\n\n大部分情况下都是推荐使用初始化列表的!\n\n#include <iostream>// struct Info{// int id;// long salary;// };using namespace std;class Demo { public: int id; Demo() { cout << "init demo" << endl; }};class Info { public: int id; long salary; Demo wrapper;};int main() { // 未使用初始化列表 Info info; cout << info.id << endl; cout << info.wrapper.id << endl; int x; cout << x << endl; Info* infop; cout << infop << endl; // 使用初始化列表 cout << "======= C++11 初始化列表 " << endl; Info info1{}; cout << info1.id << endl; cout << info1.wrapper.id << endl; int x1{}; cout << x1 << endl; Info* infop1{}; cout << infop1 << endl;}// 输出// init demo// 185313075// 88051808// 32759// 0x10b11c010// ======= C++11 初始化列表// init demo// 0// 0// 0// 0x0\n\n类的初始化列表:\n\n https://www.cnblogs.com/graphics/archive/2010/07/04/1770900.html\nhttps://en.cppreference.com/w/cpp/language/value_initialization\n\n类的初始化写法C++11 就下面这三种写法\n\n( expression-list ) 小括号括起来的表达式列表\n= expression 表达式\n{ initializer-list } 大括号括起来的表达式列表,C++11比较推荐这种写法\n\n然后这三种写法大题分为了几大类,这几大类主要是为了区分吧,我个人觉得就是语法上的归类,主要是cpp历史包袱太重了,其次追求高性能,进而分类了很多初始化写法,具体可以看官方文档: https://en.cppreference.com/w/cpp/language/initialization !\n类的多态\n前期先掌握基本语法吧,实际用到的时候再深入学习,类的继承在C++中特别复杂,因为会涉及到模版、类型转换、虚函数、析构函数,注意事项非常多!\n\n继承c++的继承非常复杂,底层设计以及各种细节,所以我单独写了一篇文章: C++继承的底层设计\n\n基类 base class ,基类需要把析构函数设置为虚函数,派生类 derived class,基类 和 派生类是相对关系\n三种继承方式: \n\n\npublic: 基类的 public 和 protected 成员的访问属性在派生类中保持不变(传递性),但基类的 private 成员不可直接访问\nprotect: 基类的 public 和 protected 成员都以 protected 身份出现在派生类中(传递性),但基类的 private 成员不可直接访问\nprivate: 基类的 public 和 protected 成员都以 private 身份出现在派生类中(传递性),但基类的 private 成员不可直接访问\n总结: \npublic 一劳永逸,protect、private 的话会修改基类的访问属性。业务中一般用public,不想对外暴露基类除外\nstruct 默认继承是public , class 默认继承是private\n多写写代码尝试下,就行了\n\n\n\n\n使用虚继承可以降低内存开销,解决多继承的二义性问题\n具体例子可以看:\n\n\n https://godbolt.org/z/TEEsYra7f \nhttps://godbolt.org/z/rxeza5EEa\n\noverride 、final\noverride(重写) 和 overload(重载) 区别在于 override 是继承引入的概念!\n\n这俩修饰词主要是解决继承中重写的问题!\n\n类被修饰为 final \n\nclass A final { public: void func() { cout << "我不想被继承" << endl; };};class B : A { // 这里会被编译报错,说A无法被继承! };\n\n\n方法被修饰为 final\n\nclass A { public: virtual void func() final { cout << "我不想被继承" << endl; }; // 申明我这个函数无法被继承,注意: final只能修饰virtual函数};class B : A { public: void func(); // 这里编译报错,无法重写父类方法};\n\n\n方法修饰为 override \n\nclass A {};class B : A { void func() override; // 这里编译报错,重写需要父类有定义!};\n\nprotectedpublic 和 private其实没多必要介绍, 但是涉及到继承,仅允许我的子类访问那么就需要protected关键词了,区别于Java的protected.\nfriendfriend (友元)表示外部方法可以访问我的private/protected变量, 正常来说我定义一个一些私有的成员变量,外部函数调用的话,是访问不了的,但是友元函数可以,例如下面这个case:\n#include <iostream>class Data { friend std::ostream& operator<<(std::ostream& os, const Data& c); private: int id{}; std::string name;};std::ostream& operator<<(std::ostream& os, const Data& c) { os << "(Id=" << c.id << ",Name=" << c.name << ")"; return os;}int main() { std::cout << Data{} << std::endl; // 这里会涉及到运算符重载的一些细节,具体可以看本篇文章!}\n\n指针的一些细节注意:别瞎new指针, new了地方要么用智能指针自动回收,要么用delete手动回收! 手动new的一定会分配在堆上,所以性能本身就不高,推荐用智能指针 + raii!\n什么叫指针,你可以理解为就是一个long类型的值,但是呢这个long类型的值是一个内存地址,你可以通过操作这个内存地址进行 获取值(因为指针是有类型的),修改内存等操作!\n在C/C++ 语言中,表示指针很简单,例如 int* ptr 表示ptr是一个int类型的指针 或者 一个int类型的数组!c++ 判断指针为空用 nullptr !\nint main() { // 栈是 高地址->低地址 走了 // x,y都是分配在栈上 int x = 1; int y = 2; // 指针就是一个long类型的值 long yp = (long)(&y); // 由于他在栈上分配,所以不需要转换一次了,直接+-就可以挪动内存了,最后在给他转换成真是的指针类型 int *xp = (int *)((yp) + 4); *xp = 100; std::cout << x << std::endl;}// 输出 100\n\n例子1: 数组与指针C++/C 中数组和指针最奇妙,原因是 数组 和 指针 基本概念等价,因为两者都是指向内存的首地址,区别在于数组名定义了数组的长度,但是指针没有数组长度的概念,因此我们无法通过一个指针获取数组长度!\n类似于下面这个例子, arr 是一个数组,p1、p2是一个数组指针\nint main(int argc, char const* argv[]) { int arr[] = {1, 2, 3, 4, 5}; int* p1 = arr; int* p2 = &arr[0]; cout << "sizeof(arr)=" << sizeof(arr) << ", sizeof(arr[1])=" << sizeof(arr[1]) << ", sizeof(p1)=" << sizeof(p1) << ", sizeof(p2)=" << sizeof(p2) << endl; cout << "arr len=" << sizeof(arr) / sizeof(arr[0]) << endl; cout << "arr=" << arr << ", p1=" << p1 << ", p2=" << p2 << endl; for (int i = 0; i < 5; i++) { cout << "i=" << i << ", (arr+i)=" << arr + i << ", (p1+i)=" << p1 + i << ", arr[i]=" << arr[i] << ", *(p1+i)=" << *(p1 + i) << endl; } return 0;}\n\n输出\nsizeof(arr)=20, sizeof(arr[1])=4, sizeof(p1)=8, sizeof(p2)=8arr len=5arr=0x7ff7bd9999f0, p1=0x7ff7bd9999f0, p2=0x7ff7bd9999f0i=0, (arr+i)=0x7ff7bd9999f0, (p1+i)=0x7ff7bd9999f0, arr[i]=1, *(p1+i)=1i=1, (arr+i)=0x7ff7bd9999f4, (p1+i)=0x7ff7bd9999f4, arr[i]=2, *(p1+i)=2i=2, (arr+i)=0x7ff7bd9999f8, (p1+i)=0x7ff7bd9999f8, arr[i]=3, *(p1+i)=3i=3, (arr+i)=0x7ff7bd9999fc, (p1+i)=0x7ff7bd9999fc, arr[i]=4, *(p1+i)=4i=4, (arr+i)=0x7ff7bd999a00, (p1+i)=0x7ff7bd999a00, arr[i]=5, *(p1+i)=5\n\n结论:\n\n数组、数组指针其实都是 数组的第一个元素对应的内存地址(指针)\n数组+1 和 指针+1 ,其实不是简单的int+1的操作,而是偏移了类型的长度,原因是 指针是有类型的,且指针默认重载了 + 运算符!\n数组是可以获取数组的长度的,但是数组指针不可以!\n\n注意:\n\n数组delete 和 delete[] 需要特别注意,因为 delete[]与new[] 成对出现,以及 delete和new成对出现\n\n例子2: 数组长度通常,我们不可能在main函数里写代码,是不是,我们更多都是函数调用,那么问题来了? 函数调用如何安全的操作呢?\nint* get_array() { int* arr = new int[12]; for (int i = 0; i < 12; i++) { *(arr + i) = i + 1; } return arr;}int main(int argc, char const* argv[]) { int* arr = get_array(); for (int i = 0; i < 12; i++) { // 这里无法获取数组指针 arr 的长度 cout << *(arr + i) << endl; } return 0;}\n\n问题: 如何获取arr的长度的呢? 显然是不可以获取的!\n例子3: 常量指针\n常量指针(Constant Pointer),表示的是指针指向的内存(内容)不可以修改,也就是说 *p 不可以修改,但是 p 可以修改\n\nint const* p; // const 修饰的是 *p, *p不可以变(指向的内容),但是p可以变const int* p; // 写法上没啥区别, 都修饰的是 *p, 我比较推荐这种写法\n\n例子\nint main(int argc, char const* argv[]) { using namespace std; int x = 10; int* p2 = new int; const int* p = &x; // *p = 10; // 不允许改变 指针指向的值 p = p2; // 允许 cout << "p: " << *p << endl; return 0;}// 输出:// p: 0\n\n\n指针常量(pointer to a constant:指向常量的指针),表示 p 不可以修改,但是 *p 可以修改\n\nint* const p\n\n例子\nint main(int argc, char const* argv[]) { using namespace std; int x = 10; int* p2 = new int; int* const p = &x; *p = 20; // 允许改变 指针指向的值 // p = p2; // 不允许 cout << "p: " << *p << endl; return 0;}// 输出// p: 20\n\n\n指向常量的常量指针\n\nconst int* const p; // 它兼容了两者的全部优点!\n\n\n总结\n\n大部分case都是使用常量指针,因为指针传递是不安全的,如果我们的目的是不让指针去操作内存,那么我们就用 常量指针,对与指针本身来说就是一个64位的int它变与不变你不用管! \n补充一些小点\n指针到底写在 类型上好 int* p,还是变量上好 int *p, 没有正确答案,我是写Go的所以习惯写到类型上!具体可以看 https://www.zhihu.com/question/52305847?rf=21136956\n指向成员的指针运算符: (比较难理解,个人感觉实际上就是定义了一个指针 alies )\n .* 和 ->*\n::*\n\n\n\n智能指针在C++11中存在四种智能指针:std::auto_ptr,std::unique_ptr,std::shared_ptr, std::weak_ptr,\nauto_ptr : c++98 中提供了,目前已经不推荐使用了\nunique_ptr: 这个对象没有实现拷贝构造函数,所以我们用的时候只能用 std::move 进行移动赋值 ,经常使用!\nshared_ptr: 其实类似于GC语言的对象,他通过引用计数【循环引用会导致内存泄露】,实现自动回收,经常使用吧!\nweak_ptr: 本质上就是解决 shared_ptr 循环引用的问题,它持有 shared_ptr,但是不会使得shared_ptr引用计数增加,很少使用吧!\nc++14新增了make_unique 的api,这里的原理会涉及到 std::move 和 std::forward 函数相关知识, 有兴趣可以了解下 完美转发和万能引用,以及移动语意!\nstd::unique_ptrclass Test { public: explicit Test(int x_) : x(x_) {} ~Test() { std::cout << "release: " << x << std::endl; } public: int x;};Test* newTestFunc(int x) { return new Test(x);}int main() { std::unique_ptr<Test> test1 = std::unique_ptr<Test>(new Test(1)); // unique_ptr只有移动语意,没有拷贝语义 auto test2 = std::move(test1); std::cout << "test1 is null ptr: " << (test1 == nullptr) << std::endl; std::cout << "test2.x: " << test2->x << std::endl; // reset 会先释放原来指针,然后再赋值 test2.reset(new Test(2)); // 释放引用, 例如理论上 test2 会在main函数结束后会释放,但是我其实想要这个内容,我自己管理,就可以用 release 函数释放指针 Test* test2_ = test2.release(); std::cout << "test2_->x: " << test2_->x << std::endl; delete test2_; // 不推荐这么写,这样裸指针很危险,也容易忘记释放.// Test* test3 = newTestFunc(3); // 推荐使用智能包装一层 auto test = std::unique_ptr<Test>(newTestFunc(3));}\n\nstd::shared_ptrshared_ptr 实际上基本已经对标主流的垃圾回收语言了,它使用引用计数的方式实现了垃圾回收!\nshared_ptr 会存储一个引用计数器+指针,每次拷贝都会使得计数器+1然后再拷贝数据,当调用析构函数(或者 reset函数)的时候会使得计数器-1;当为0的时候会直接会去释放指针!所以原理并不复杂吧!\nstruct AStruct;struct BStruct;struct AStruct { std::shared_ptr<BStruct> bPtr; ~AStruct() { std::cout << "AStruct is deleted!" << std::endl; }};struct BStruct { int Num; ~BStruct() { std::cout << "BStruct is deleted!" << std::endl; }};void setAB(const std::shared_ptr<AStruct> &ap) { std::shared_ptr<BStruct> bp(new BStruct{}); std::cout << "bp->count[0]: " << bp.use_count() << std::endl; // 1 bp->Num = 111; ap->bPtr = bp; std::cout << "bp->count[1]: " << bp.use_count() << std::endl; // 2 std::cout << "bp->count[1.1]: " << ap->bPtr.use_count() << std::endl; // 2 // defer: bp释放 count=1, 未触发回收;}void Test() { std::shared_ptr<AStruct> ap(new AStruct{}); setAB(ap); std::cout << ap->bPtr->Num << std::endl; std::cout << "bp->count[2]: " << ap->bPtr.use_count() << std::endl; // 1 // defer ap 释放 -> bp释放后 bp.count=0 释放bp!}int main() { Test();}\n\n但是这个也注定有一个陷阱,就是循环引用无法解决!\nstruct AStruct;struct BStruct;struct AStruct { std::shared_ptr<BStruct> bPtr; ~AStruct() { std::cout << "AStruct is deleted!" << std::endl; }};struct BStruct { std::shared_ptr<AStruct> aPtr; ~BStruct() { std::cout << "BStruct is deleted!" << std::endl; }};void TestLoopReference() { std::shared_ptr<AStruct> ap(new AStruct{}); std::shared_ptr<BStruct> bp(new BStruct{}); ap->bPtr = bp; bp->aPtr = ap; // 无法释放 ap 和 bp}int main() { TestLoopReference();}\n\nstd::weak_ptrweak_ptr 本质上并不能算的上是一个智能指针,只能说是为了解决 shared_ptr 循环引用的问题 [不能根本解决],weak_ptr相当于拷贝了一份 shared_ptr, 但是引用次数并不会增加,为此假如 shared_ptr 已经被释放了,那么weak_ptr也会指向空指针!\nstruct AStruct;struct BStruct;struct AStruct { std::weak_ptr<BStruct> bPtr; ~AStruct() { std::cout << "AStruct is deleted!" << std::endl; }};struct BStruct { std::weak_ptr<AStruct> aPtr; int Num; ~BStruct() { std::cout << "BStruct is deleted!" << std::endl; }};void TestLoopReference() { std::shared_ptr<AStruct> ap(new AStruct{}); std::shared_ptr<BStruct> bp(new BStruct{}); bp->Num = 1; // weak ptr 本身就是弱引用,此时只是只要ap/bp生命周期(也就是这个函数没执行结束)没结束就一直可以使用! ap->bPtr = bp; bp->aPtr = ap; std::cout << "BStruct.Num: " << ap->bPtr.lock()->Num << std::endl;}int main() { TestLoopReference();}\n\n智能指针和数组\n针对于数组指针, 需要自己定义delete函数 https://coliru.stacked-crooked.com/a/83d4d163afb6cdd8\n针对于数组,无需特殊处理 https://coliru.stacked-crooked.com/a/b3e9c0103382fe3b\n\n#include <memory>int main() { // 数组 std::shared_ptr<Int[]> data{}; data.reset(new Int[10]); // 数组指针 std::shared_ptr<Int> data2{}; data.reset(new Int[10], [](auto p) { delete[] p; // 首地址的前8字节(64位)地址就是数组长度,所以可以删除成功 }); // 发现个很神奇的地方,删除数组是从尾到首部删除...}\n\n内存回收的一些思考\n虽然C++中提供了 raii 和 智能指针,但是内存的频繁分配和频繁销毁,会给cpu造成一些开销(性能慢、延时高等),那么业务中经常遇到那种巨型结构进行序列化反序列化,那么业内也有一些解决方案,就是使用 arena ,具体可以参考\n\n\nhttps://protobuf.dev/reference/cpp/arenas/\nhttps://github.com/protocolbuffers/protobuf/issues/4327\n\n关键词constC++的 const 表达的意思是只读的意思,就是不可变的意思!\n\n这里主要是介绍一个双重指针,其他疑问可以看这个链接: https://www.zhihu.com/question/433076446!\n\n#include <iostream>void foo1() { using namespace std; int* x = new int(10); int* const* p = &x; // 表示*p是常量 cout << **p << endl; **p = 100; // **p允许修改 cout << **p << endl; // *p = x2; // *p不允许修改!}void foo2() { using namespace std; const int* x = new int(10); const int** p = &x; // 表示**p是常量, 因为它也不需要要用常量*x初始化, 不然编译报错! cout << **p << endl; *p = new int(11); // *p可以修改 cout << **p << endl; // **p = 10; // **p不可以修改}int main() { foo1(); foo2();}\n\n\nconst 可以修饰方法的返回值\n\nconst char* getString() { return "hello"; }int main() { auto str = getString(); *(str + 1) = 'a'; // 这里编译报错,只读 str return 0;}\n\n\nconst 修饰方法的参数\n\nvoid printStr(const char* str) { cout << str << endl; } // 这里无法修改strint main() { printStr("1111"); return 0;}\n\n\nconst 修饰方法, 表示此方法是一个只读的函数\n\nclass F { private: int a; public: void foo() const { this->a = 1; } // 编译报错,无法修改 this->a !};\n\nconstexprconstexpr 常量表达式,就是它可以在编译后直接替换为计算所得的值!可以看下面这个例子,直接计算出 fib(6) 的值直接赋值给了esi (参数一) !\n\nconstexpr 目前已经是非常成熟的能力了,但是它会给编译器带来比较大的压力!\nC++11:仅支持简单的常量表达式\nC++14:支持逻辑语句\nC++17:支持Lambda\n参考文章\n\nhttps://zhxilin.github.io/post/tech_stack/1_programming_language/modern_cpp/cpp17/constexpr/\nhttps://en.cppreference.com/w/cpp/language/constexpr\n\n说实话,一堆花里胡哨的东西你很难理解它的实际用途,像上面这种常量表达式计算,人家编译器可能直接给你优化了,完全不需要你申明 consteptr\n主要实用的用途就是:\n\n让编译器提前优化代码(提前的意思表示可能未来编译器就优化了),类似于上面那个纯粹的计算函数\n\n编译时期进行 static_assert,进行一些类型、常量检测 \n\n编译器进行条件判断,减少代码量,但是实际上这个例子可以用 cpp17的 fold expression\n\n\ntemplate<typename T, typename ... Args>constexpr void print(T t, Args ... args) { std::cout << t << " "; if constexpr (sizeof...(args) > 0) { print(args ...); } if constexpr (sizeof...(args) == 0) { std::cout << "\\n"; }}int main(){ print("1", "2", "3");}\n\n\n模版元编程: 太过于强大,此处不建议学习\n\nstaticstatic 主要是内存分配的问题,在程序初始化阶段会有一个静态内存区域专门存储静态变量的,其次静态局部变量可以保证多线程安全(c++11后)!\n\n注意: c++中static定义在头文件中会被初始化多次,不要在头文件中定义全局static变量,别误以为是static作用域失效了!\n\n\n全局 静态变量、静态方法\n\n\n静态成员变量可以初始化,但只能在类体外进行初始化\n\n// main.h#include <fmt/core.h>namespace example {static int NumberX = 100;static int NumberY = 200;static int NumberZ = NumberY + 300;static void print() { fmt::print("x: {}, y: {}, z: {}\\n", NumberX, NumberY, NumberZ);}class Class {public: static void print(); static const int x; static int y; static int z;};} // namespace example// main.cpp#include <fmt/core.h>const int example::Class::x = 1;int example::Class::y = z + 2;int example::Class::z = 2;void example::Class::print() { fmt::print("x: {}, y: {}, z: {}", x, y, z);}int main() { example::print(); example::Class::print();}// output:// x: 100, y: 200, z: 500// x: 1, y: 4, z: 2\n\n\n全局静态方法和静态变量\n\n#include <iostream>int inc() { static int sum = 0; return ++sum;}int main(int argc, char const* argv[]) { std::cout << inc() << std::endl; std::cout << inc() << std::endl; return 0;}// 输出:// 1// 2\n\n\n模版的静态变量\n\nclass A {public: static int num;};int A::num = 100;class B : public A {};class C : public A {};template <class T>class AA {public: static int num;};template <class T>int AA<T>::num = 1;class BB : public AA<BB> {};class CC : public AA<CC> {};int main() { printf("%p\\n", &B::num); printf("%p\\n", &C::num); printf("%p\\n", &BB::num); printf("%p\\n", &CC::num);}// output:// 0x10f2c5030// 0x10f2c5030// 0x10f2c5034// 0x10f2c5038\n\n\n写一个单例对象\n\n#include <absl/base/call_once.h>#include <fmt/core.h>template <class T>class ThreadSafeSingleton {public: static T &get() { absl::call_once(ThreadSafeSingleton<T>::create_once_, &ThreadSafeSingleton<T>::Create); return *ThreadSafeSingleton<T>::instance_; }protected: static void Create() { instance_ = new T(); } static absl::once_flag create_once_; static T *instance_;};template <class T>absl::once_flag ThreadSafeSingleton<T>::create_once_;template <class T>T *ThreadSafeSingleton<T>::instance_ = nullptr;// C++ 11 可以这么写,因为static线程安全template <class T>class ConstSingleton {public: static T &get() { static T *t = new T(); return *t; }};struct ExampleStruct { std::string name;};using ExampleStructSingleton = ThreadSafeSingleton<ExampleStruct>;using ExampleStructConstSingleton = ConstSingleton<ExampleStruct>;int main() { ExampleStructSingleton::get().name = "hello world"; fmt::print("name = {}\\n", ExampleStructSingleton::get().name); ExampleStructConstSingleton::get().name = "hello world"; fmt::print("name = {}\\n", ExampleStructConstSingleton::get().name);}\n\nextern\n extern C 主要是解决C++ -> C 链接方式不得同,以及C与C++函数互相调用的问题\n其他待补充!\n\nauto 和 decltype\n 看这里之前建议先学习模版\n\nauto 实际上是大部分高级语言现在都有的一个功能,就是类型推断,c++11引入auto 原因也是因为模版, 其次更加方便!\ndecltype 本质上也是类型推断,但是它与 auto 是俩场景,解决不同的场景的问题,非常好用,decltype并不会真正的调用函数,只是获取函数的类型,非常好用,尤其是面对复杂模版的时候!\ntemplate <typename T, typename U>auto add(T t, U u) -> decltype(t + u) { // 返回类型的后置写法! using Sum = decltype(t + u); Sum s = t + u; s = s + 1; return s;}int main() { auto num = add(float(1.1), int(1)); cout << num << endl; return 0;}\n\n上面代码,如果没有 decltype 很难去实现,如果仅用模版根本无法推断出到底返回类型是啥,可能是int 也可能是 float !\n注意: \n\ndecltype 最难的地方还是在于它保留了 左值/右值信息,这个就给编程带来了一定的难度!\nc++14 有更精简的语法,具体可以看c++14语法\n\nusing 和 typedef\n看这里之前先学习模版\n\n虽然大部分case两者差距不大,using 这里主要解决了一些case 语法过于复杂的问题!\n例如 typedef 无法解决模版的问题,只能依赖于类模版去实现!\nusing 更加方便!\n#include <iostream>#include <list>template <typename T, template <typename> class MyAlloc = std::allocator>using MyAllocList = std::list<T, MyAlloc<T>>;int main() { auto my_list = MyAllocList<int>{1, 2, 3, 4}; for (auto item : my_list) { cout << item << endl; } return 0;}\n\n如果用typedef 我们只能定义一个 类\n#include <iostream>#include <list>template <typename T, template <typename> class MyAlloc = std::allocator>struct MyAllocList2 { public: typedef std::list<T, MyAlloc<T>> type;};int main() { auto my_list_2 = MyAllocList2<int>::type{1, 2, 3, 4}; return 0;}\n\nswitch & break其实我这里就想说一点,就是switch当匹配到case后,如果case没有执行break,会继续执行下面的case,已经不管case是否匹配了!!\n#include <iostream>int main() { int x = 2; switch (x) { case 1: { std::cout << "1" << std::endl; } case 2: { std::cout << "2" << std::endl; } case 3: { std::cout << "3" << std::endl; } }}// output// 2// 3\n\nbreak用法\n#include <iostream>int main() { int x = 2; switch (x) { case 1: { std::cout << "1" << std::endl; } case 2: { std::cout << "2" << std::endl; } break; case 3: { std::cout << "3" << std::endl; } }}// 输出:// 2\n\ntypenameC++ 为什么要引入一个 typename 关键词,不光光是申明一个 模版参数列表 这么简单,其次更重要的是申明模版依赖(dependency),需要配合 template 关键词使用!在模版元编程中大量使用!\n比较感兴趣的两个话题:\n\nhttps://stackoverflow.com/questions/610245/where-and-why-do-i-have-to-put-the-template-and-typename-keywords/613132#613132\n\nhttps://pages.cs.wisc.edu/~driscoll/typename.html\n\n\ntypename和class有着相同的能力在模版这里,但是typename更多的是为了支持模版!\n#include <spdlog/spdlog.h>struct Test { std::string name;};template <>struct fmt::formatter<Test> : fmt::formatter<std::string> { static auto format(const Test &my, fmt::format_context &ctx) -> decltype(ctx.out()) { return format_to(ctx.out(), "[test name={}]", my.name); }};template <typename T>constexpr auto has_const_formatter(T t) -> decltype(typename fmt::formatter<T>().format(std::declval<const T &>(), std::declval<fmt::format_context &>()), true) { return true;}int main(){ std::cout << has_const_formatter<>(Test{}) << std::endl;}\n\n操作符重载(运算符重载)本质上操作符重载就是可以理解为方法的重载,和普通方法没啥差别!但是C++支持将一些 一元/二元/三元的运算符进行重载!\n实际上运算符重载是支持 类内重载、类外重载的,两者是等价的!但是有些运算符必须要类内重载,例如 = 、[]、()、-> 等运算符必须类内重载!\n这也就是为啥 ostream 的 <<仅仅重载了部分类型,就可以实现输出任意类型了(只要你实现了重载),有别于一些其他语言的实现了,例如Java依赖于Object#ToString继承,Go依赖于接口实现等!运算符重载的好处在于编译器就可以做到检测!\n#include <iostream>using namespace std;class Complex { private: int re, im; public: Complex(int re, int im) : re(re), im(im) { } // 语法就是 type operator<operator-symbol>(parameter-list) Complex operator+(const Complex& other) { return Complex(this->re + other.re, this->im + other.im); } void print() { cout << "re: " << re << ", im: " << im << endl; }};int main(int argc, char const* argv[]) { Complex a = Complex(1, 1); Complex b = Complex(2, 2); Complex c = a + b; c.print(); return 0;}// 输出:// re: 3, im: 3\n\nlambda首先lambda 其实在函数式编程很常见,但实际上我个人还是不理解,如果为了更短的代码,我觉得毫无意义,只不过是一个语法糖罢了,本质上C++的Lambda就是语法糖,编译后会发现实际上是一个匿名的仿函数!\n那么什么才是lambda?我觉得函数式编程,一个很强的概念就是(anywhere define function)任意地方都可以定义函数,例如我现在经常写Go,我定义了一个方法,我需要用到某个方法,但是呢这个作用范围我不想放到外面,因为外面也用不到。因此分为了立即执行函数和变量函数\ntype Demo struct {\tName *string}func foo() {\tnewDemo := func(v string) *Demo { // newDemo变量 是一个函数类型\t\treturn &Demo{\t\t\tName: func(v string) *string {\t\t\t\tif v == "" {\t\t\t\t\treturn nil\t\t\t\t}\t\t\t\treturn &v\t\t\t}(v), // 立即执行函数\t\t}\t}\tdemo1 := newDemo("1")\tdemo2 := newDemo("")\tfmt.Println(demo1.Name)\tfmt.Println(demo2.Name)}\n\n那么换做C++,我怎么写呢? 是的如此强大的C++完全支持, 哈哈哈哈!注意是C++11 !\nstruct Demo { public: const char* name; Demo(const char* name) : name(name) { }};void foo() { auto newDemo = [](const char* name) { return new Demo([&] { if (*name == '\\0') { const char * null; return null; } return name; }()); }; Demo* d1 = newDemo("111"); Demo* d2 = newDemo(""); std::cout << d1->name << std::endl; std::cout << d2->name << std::endl;}\n\n基于上面的例子我们大概知道了如何定义一个 变量的类型是函数 , 其次如何定义一个立即执行函数!\n\n函数类型\n\n/**[=]:通过值捕捉所有变量[&]:通过引用捕捉所有变量[&x]只通过引用捕捉x,不捕捉其他变量。[x]只通过值捕捉x,不捕捉其他变量。[=, &x, &y]默认通过值捕捉,变量x和y例外,这两个变量通过引用捕捉。[&, x]默认通过引用捕捉,变量x例外,这个变量通过引用捕捉。[&x, &y]非法,因为标志符不允许重复。*/int add1(int x, int y) { auto lam = [&]() { // [&] 表示引用传递 x = x + 1; y = y + 1; return x + y; }; return lam();}int add2(int x, int y) { auto lam = [=]() { // [=] 表示值传递,不可以做写操作,类似于const属性 // x = x+1; // 不可以操作 // y = y+1; // 不可以操作 return x + y; }; return lam();}int add3(int x, int y) { // &x表示传递x的引用 // y 表示函数参数 // 类型是: std::function<int(int)> std::function<int(int)> lam = [&x](int y) { x = x + 1; return x + y; }; return lam(y);}\n\n\n立即执行函数\n\nint main(int argc, char const* argv[]) { // lam: 函数类型 std::function<int(int, int)> lam = [](int a, int b) { return a + b; }; std::cout << lam(1, 9) << " " << lam(2, 6) << std::endl; // 立即执行函数 [] { std::cout << "立即执行函数" << std::endl; }(); return 0;}// 输出:// 10 8// 立即执行函数\n\n\n函数作为参数传递\n\nstd::function<void()> print(std::string str) throw(const char*) { if (str == "") { throw "str is empty"; } return [=] { std::cout << "print: " << str << std::endl; };}int main(int argc, char const* argv[]) { try { print("")(); } catch (const char* v) { std::cout << "函数执行失败, 异常信息: " << v << std::endl; } print("abc")(); return 0;}// 输出: // 函数执行失败, 异常信息: str is empty// print: abc\n\n注意点:\n\n区别于仿函数,仿函数是重载了()运算符,仿函数本质上是类,但是C++11引入了 std::function 也就是 lamdba 简化了仿函数,所以C++11 不再推荐仿函数了!\n区别于函数指针\n\n#include <algorithm>#include <iostream>#include <vector>class NumberPrint { public: explicit NumberPrint(int max) : max(max){}; void operator()(int num) const { // 仿函数 if (num < max) { std::cout << "num: " << num << std::endl; } }; private: int max;};void printVector(std::vector<int>&& vector, void (*foo)(int)) { std::for_each(vector.begin(), vector.end(), foo); }void printNum(int num) { std::cout << "num: " << num << std::endl; }int main() { printVector(std::vector<int>{1, 2, 3, 4}, printNum); auto arr = std::vector<int>{1, 2, 3, 4}; std::for_each(arr.begin(), arr.end(), NumberPrint(3));}\n\n\n函数指针的致命缺陷, 就是函数指针不支持捕获参数,所以最好别用函数指针,除非对接C! \n\n\n\n总结\n\n#include <functional>#include <string>#include <iostream>template <typename T>struct Handler { using c_function = std::string (*)(T); std::string operator()(T x) { return "Handler<T>(x)"; };};template <>struct Handler<int> { // 特化版本的模板并不是基模板的子类,而是另一个具有相同模板参数的全新类型 using c_function = std::string (*)(int); std::string operator()(int x) { return "Handler<int>(" + std::to_string(x) + ")"; // 仿函数 };};void print(int num, const std::function<std::string(int)>& handler) { std::cout << "print: " << handler(num) << "\\n";}void print_c(int num, std::string (*handler)(int)) { std::cout << "print_c: " << handler(num) << "\\n";}int main() { std::string name = "lambda"; // std::function 是支持捕获的 print(1, [&](auto x) { return name + "(" + std::to_string(x) + ")"; }); // 仿函数 print(2, Handler<int>{}); // like c 这种传递函数的方式不支持捕获的 // 本质上lambda就是一个仿函数,为啥转换成c function,可以看他处理后的源码 print_c(3, [](int x) -> std::string { return "c_lambda (" + std::to_string(x) + ")"; });}// output:// print: lambda(1)// print: IntHandler(2)// print_c: c lambda (3)\n\n枚举C++的枚举继承了C,也就是支持 enum 和 enum class,两者的区别主要是在于作用范围的不同, 例如下面 Child 和 Student 都定义了 Girl 和 Body,如果不是 enum class 的话则会报错!\n#include <iostream>#include <map>// 允许指定类型enum class Child : char { Girl, // 不指定且位置是第一个就是0 Boy = 1,};const static std::map<Child, std::string> child_map = {{ Child::Girl, "Girl", }, { Child::Boy, "Boy", }};std::ostream& operator<<(std::ostream& out, const Child& child) { // 重载方法 << 方法 auto kv = child_map.find(child); if (kv == child_map.end()) { out << "Unknown[" << int(child) << "]"; return out; } out << kv->second; return out;}enum class Student { Girl, Boy};using namespace std;int main() { Child x = Child::Boy; cout << x << endl; cout << int(x) << endl; cout << Child(100) << endl;}\n\n模版我自己写了篇文章有兴趣的可以读一下:C++模版\n类/函数模版类模版支持全特化和偏特化,函数模版仅支持全特化\n注意:在C++中,特化的模板并不能从其通用模板(也就是基模板)”继承”成员\n#include <iostream>#include <unordered_map>#include <map>#include <vector>// Map 包装了map,方便拓展template <typename K, typename V, template <typename...> class Map_ = std::unordered_map>struct Map { using type = Map_<K, V>; V& operator[](const K& k) { return map_[k]; };private: Map_<K, V> map_{};};// struct/class支持偏特化和特化// 模版模版参数一般都用动态参数模版作为入参,主要是没必要再限制了...// template <typename K, typename V, template <typename k, typename v> class Map = std::unordered_map>// struct KVMap2 {// private:// Map<K, V> map;// };// 函数模版 支持打印 Containertemplate <template <typename...> class Container, typename T>void PrintContainer(const Container<T>& c) { for (const auto& v : c) { std::cout << v << ' '; } std::cout << '\\n';}// 一般情况下是不需要特化的,除非有特殊需要(本质上就是重载,你需要重载实现一个特殊逻辑)template <template <typename...> class Map, typename K, typename V>void PrintMap(const Map<K, V>& c) { for (const auto& v : c) { std::cout << v.first << ':' << v.second << ' '; } std::cout << '\\n';}template <>void PrintMap(const std::map<int, int>& c) { for (const auto& v : c) { std::cout << "i" << v.first << ':' << v.second << ' '; } std::cout << '\\n';}int main() { // 做一个适配器 // 或者别的 Map<std::string, int> map; map["11"] = 1; std::cout << map["11"] << std::endl; Map<std::string, int, std::map>::type ordered_map; PrintContainer(std::vector<int>{1, 2, 3}); PrintMap(std::map<int, int>{{1, 1}, {2, 2}}); PrintMap(std::map<std::string, int>{{"1", 1}, {"2", 2}});}\n\n可变参数模版c++如果不使用模版是不支持可变参数的,因此如果实现可变参数必须要通过模版,区别于别的语言,其实像Go这种语言可变参数仅是一个语法糖,最终还是会实例化成List的!下面是C++模版的例子!\n#include <iostream>// c++11中我们可以通过函数的重载来实现模版代码的递归生成!template <typename T>T sum(T num) { return num;}template <typename T, typename... Ts>T sum(T num, Ts... args) { return num + sum(args...);}// c++17中支持通过constexpr去写一些逻辑语句// 生成结果本质上与上面是等价的!template <typename T, typename... Ts>auto const_expression_sum(T num, Ts... args) { if constexpr (sizeof...(args) == 0) { return num; } else { return num + const_expression_sum(args...); }}int main() { std::cout << const_expression_sum(1, 2, 3) << "\\n"; std::cout << sum(1, 2, 3) << "\\n";}\n\n具体会生成一个类似于下面这个代码,有兴趣的同学可以通过这个网站编译看一下 https://cppinsights.io/\n\nC++17 fold expression (折叠表达式)官方文档: https://en.cppreference.com/w/cpp/language/fold\n上面的可变参数模版我们发现一个问题需要大量生成函数,那么如果我n个可变参数多就会生成n个函数,会导致代码的膨胀,因此C++17提供了折叠表达式完美的解决了此问题!说实话我感觉这玩意特别像Python的列表推到式 !\n#include <iostream>/**右折叠: ( pack op ... )\t(1)\t // (E1 op (... op (EN-1 op EN)))左折叠: ( ... op pack )\t(2)\t // (((E1 op E2) op ...) op EN)有初始化的右折叠: ( pack op ... op init )\t(3)\t // (E1 op (... op (EN−1 op (EN op I))))有初始化的左折叠: ( init op ... op pack )\t(4)\t// ((((I op E1) op E2) op ...) op EN)*/template <typename... Ts>auto sum_left(Ts... args) { return (... + args); // 折叠表达式 ((1+2) + 3) + 4}template <typename... Ts>auto sum_right(Ts... args) { return (args + ...); // 折叠表达式 ((1+2) + 3) + 4}template <typename T, typename... Ts>auto sum_left_init(T init, Ts... args) { return (init + ... + args); // (((10+1 ) + 2 ) + 3 ) + 4}template <typename T, typename... Ts>auto sum_left_inc(T inc, Ts... args) { return (... + (args + inc)); // (((1+5)+(2+5))+(3+5))+(4+5)}template <typename T, typename... Ts>void print_v(T t, Ts... args) { ((std::cout << t) << ... << args) << "\\n"; // (((std::cout << "1") << "2") << "3") << "4"}int main() { std::cout << sum_left(1, 2, 3, 4) << "\\n"; std::cout << sum_right(1, 2, 3, 4) << "\\n"; std::cout << sum_left_init(10, 1, 2, 3, 4) << "\\n"; std::cout << sum_left_inc(5, 1, 2, 3, 4) << "\\n"; print_v("1", "2", "3", "4");}\n\n\n针对于一些复杂case我们可以这么操作,通过lambda或者抽出一个方法来操作!! \n#include <iostream>#include <vector>#include <algorithm>template <typename T, typename... Ts>auto append(std::vector<T>& arr, Ts... elems) { // 简单点用 (..., arr.push_back(elems)); // 复杂点, 就使用lambda封装下,然后... auto append = [&](T elem) { if (elem > 10) { arr.push_back(elem); } }; (append(elems), ...);}int main() { std::vector<int> arr; append(arr, 1, 2, 3, 4, 4, 10, 11); std::for_each(arr.begin(), arr.end(), [](auto elem) { std::cout << elem << ","; });}\n\nSTLSTL:(Standard Template Library)叫做C++标准模版库,其实可以理解为C++最核心的部分,很多人望而却步,其实我感觉还好!\n主要包含:\n\n容器类模板: 基本的数据结构,数组、队列、栈、map、图 等,如果你学习过很多高级语言,那么对于C++这些容器结构我觉得其实不用太投入,只要熟悉几个API就可以了!\n\n\n// 头文件#include <vector>#include <array>#include <deque>#include <list>#include <forward_list>#include <map>#include <set>#include <stack>\n\n\n算法(函数)模板:基本的算法,排序和统计等 , 其实就是一些工具包\n\n// 头文件#include <algorithm>\n\n\n迭代器类模板:我觉得在Java中很常见,因为你要实现 for each 就需要实现 iterator 接口,其实迭代器类模版也就是这个了!\n\n// 头文件#include <iterator>\n\n\n总结\n\n#include <iostream>#include <algorithm> // 算法#include <iterator> // 迭代器#include <vector> // 容器// 找到targetVal位置,并在targetVal前面插入insertVal// 未找到则在尾部插入template <typename C, typename V>void findAndInsert(C& container, const V& targetVal, const V& insertVal) { // 迭代器 using std::begin; using std::end; // 算法 auto it = std::find(begin(container), end(container), targetVal); container.insert(it, insertVal);}int main() { // 定义容器 auto arr = std::vector<int>{1, 2, 3, 4}; findAndInsert(arr, 4, 2); // 算法 std::for_each(arr.begin(), arr.end(), [](decltype(*arr.begin()) elem) { cout << elem << endl; }); return 0;}\n\n\n现在很多高级语言都支持切片,可以说是大大提高了开发效率,但是cpp也有,也很简单,区别在于是c++实现的是拷贝,而非内存复用,所以这种需求还是用迭代器比较好!\n\n#include <iostream>#include <vector>#include <functional>#include <algorithm>using IntVector = std::vector<int>;template <typename T>void print(std::vector<T> &v) { int index = 0; std::cout << "["; for (const auto &item : v) { if (index != 0) { std::cout << ", "; } std::cout << item; ++index; } std::cout << "]" << std::endl;}int main() { IntVector v1{}; v1.reserve(10); for (int x = 0; x < 10; x++) { v1.push_back(x); } print(v1); // [:4] auto v2 = IntVector(v1.begin(), v1.begin() + 4); print(v2); // [1:-1] auto v3 = IntVector(v1.begin() + 1, v1.end() - 1); print(v3); // 更加推荐,传递迭代器 for (auto begin = v1.begin(); begin != v1.begin() + 4; begin++) { std::cout << "range: " << *begin << std::endl; } // c++14支持lambda表达式参数用auto std::for_each(v1.begin() + 1, v1.begin() + 4, [](auto elem) { std::cout << "for_each: " << elem << std::endl; });}\n\n预处理器 - 宏宏本质上就是在预处理阶段把宏替换成对应的代码,属于代码模版[ C++/C 思想真的超前 ],可以省去不少代码工作量,其次就是性能更好,不需要函数调用,直接预处理阶段内联到代码中去了,例如我这里就用了宏 https://github.com/Anthony-Dong/protobuf/blob/master/pb_include.h !\n宏的玩法太高级,很多源码满满的宏,不介意新手去深入了解!只要能看懂就行了,简单实用一下也完全可以的哈!\n简单的例子#include <iostream>#define product(x) x* xusing namespace std;int main() { int x = product((1 + 1)) + 10; // 展开后: (1 + 1)*(1 + 1) + 10 std::cout << "x: " << x << std::endl; int y = product(1 + 1) + 10; // 展开后: 1 + 1*1 + 1 + 10 std::cout << "y " << y << std::endl;#ifdef ENABLE_DEBUG cout << "print debug" << endl;#endif}// 输出:x: 14y: 13\n\nclass+宏+类名的意义\n注意: 这里要是有windows环境的话可以自己体验下!\n\n不清楚大家阅读过c++源码吗,发现开源的代码中基本都有一个 ,那么问题是 PROTOBUF_EXPORT 干啥了?\nclass PROTOBUF_EXPORT CodedInputStream { \t//...}\n\n实际上你自己写代码没啥问题,定不定义这个宏,你要把代码/ddl提供给别人用windows的开发者来说就有问题了,别人引用你的api需要申明一个 __declspec(dllexport) 宏定义,表示导出这个class,具体可以看 https://learn.microsoft.com/en-us/cpp/cpp/using-dllimport-and-dllexport-in-cpp-classes 所以说对于跨端开发来说是非常重要的这点!\n其次这个东西很多时候可以在编译器层面做手脚,表示特殊标识,反正 大概你知道 windows 下需求这个东东就行了!\n#define DllExport __declspec( dllexport )class DllExport C { int i; virtual int func( void ) { return 1; }};\n\nRTTI待补充!\n多线程https://en.cppreference.com/w/cpp/thread\ncpp11 的 thread、mutex、lock_guard、lock_uniq、feature\ncpp14 支持了 shared_lock\ncpp17 支持了 async 、shared_mutex\ncpp20 支持了 jthread 和 coroutine\n#include <mutex>#include <thread>#include <iostream>int main() { using GuardLock = std::lock_guard<std::mutex>; using UniqueLock = std::unique_lock<std::mutex>; std::mutex mutex; int count = 0; { auto test = [&count, &mutex]() { for (int y = 0; y < 100000; y++) { GuardLock lock(mutex); ++count; } std::cout << std::this_thread::get_id() << ": " << count << std::endl; }; std::jthread tt(test); std::jthread t2(test); std::jthread t3(test); } std::cout << "main" << count << std::endl;}\n\n其他new 与 malloc我们知道,我们可以再 C语言里使用 malloc 和 frees 初始化内存,但是C++ 里更加推荐使用 new 和 delete ,那么区别在哪里了!\n首先我们知道C++引入了 构造函数 和 析构函数,因此我们用 c系列的api操作,会丢失这些信息,这就是最主要的区别,也是特别需要注意的!\n例子一: 最常见的乱用行为! \nclass Test { public: explicit Test(int x_) : x(x_) {} ~Test() { std::cout << "release: " << x << std::endl; } public: int x;};int main() { Test* test = new Test(1); // 错误行为// free(test); // 正确,会调用析构函数! delete test;}\n\n例子二: 业务中为了做一些事情,例如有些特殊case需要用 void* 指针进行操作(例如导出C),解决内存拷贝的问题\nstruct CClass { void* point;};int main() { Test* test = new Test(1); CClass c{ .point = test, }; // 正确行为,需要强制转换成 Test*; delete (Test*)c.point;}\n\nnew[] 与 delete[]我们可以简单看下面这个例子,就大概明白了,new与delete的区别\nclass Test { public: \tTest() = default; int x;};void builtin() { auto list = new int[10]{}; for (int x = 0; x < 10; x++) { list[x] = x; } cout << *((unsigned long*)list - 1) << endl; // 输出不确定 delete[] list;}void external() { auto list = new Test[10]{}; for (int x = 0; x < 10; x++) { list[x].x = x; } cout << *((unsigned long*)list - 1) << endl; // 输出10 delete[] list;}\n\n\n内置类型的话,内存中不会存储长度字段\n\n\n\n其他类型,会在首地址-8 的位置存储长度,也就是64位是8字节\n\n\n\n所以对于 new[] 的指针对象,一定要用delete[] 释放,不然的话你会内存泄漏奥!\n\nC++ 位域https://learn.microsoft.com/zh-cn/cpp/cpp/cpp-bit-fields?view=msvc-170 可以节约内存开销\n#include <iostream>using namespace std;struct http_parser { /** PRIVATE **/ unsigned int type : 2; /* enum http_parser_type */ unsigned int flags : 8; /* F_* values from 'flags' enum; semi-public */ unsigned int state : 7; /* enum state from http_parser.c */ unsigned int header_state : 7; /* enum header_state from http_parser.c */ unsigned int index : 5; /* index into current matcher */ unsigned int uses_transfer_encoding : 1; /* Transfer-Encoding header is present */ unsigned int allow_chunked_length : 1; /* Allow headers with both * `Content-Length` and * `Transfer-Encoding: chunked` set */ unsigned int lenient_http_headers : 1; uint32_t nread; /* # bytes read in various scenarios */ uint64_t content_length; /* # bytes in body. `(uint64_t) -1` (all bits one) * if no Content-Length header. */ /** READ-ONLY **/ unsigned short http_major; unsigned short http_minor; unsigned int status_code : 16; /* responses only */ unsigned int method : 8; /* requests only */ unsigned int http_errno : 7; /* 1 = Upgrade header was present and the parser has exited because of that. * 0 = No upgrade header present. * Should be checked when http_parser_execute() returns in addition to * error checking. */ unsigned int upgrade : 1; /** PUBLIC **/ void *data; /* A pointer to get hook to the "connection" or "socket" object */};int main() { cout << sizeof(http_parser) << endl;}// output:// 32\n\nnamespace我们知道c语言是没有namespace的概念的,作用域是全局的,所以导致头文件如果存在公共定义是可能会存在问题的。\nc++ 支持了namespace,解决命名冲突的问题,但是同样的它会造成编译的时候 符号连接会带上namespace,导致c语言无法和c++链接,此时就需要用到 extern "C" 了!\nnamespace Misc {namespace Utils {struct Consts {};} // namespace Utils} // namespace Miscnamespace Misc {namespace Network {struct TcpConnect { using Consts = Utils::Consts; // 他们都存在Misc namespace下,所以可以这么引用,因为必须要全限定namespace(这种日常开发中经常会使用) using FullConsts = Misc::Utils::Consts; // 不推荐};} // namespace Network} // namespace Misc\n\n常用库\nabsl: https://github.com/abseil/abseil-cpp\nprotobuf: https://github.com/protocolbuffers/protobuf\nfmt: https://github.com/fmtlib/fmt\nlibevent: https://libevent.org/\ngoogletest: https://github.com/google/googletest\n\n其他学习资料\nhttps://godbolt.org/ 一个C++ 转 汇编的工具\nhttps://cppinsights.io/ 可以看到编译器编译后的结果\nhttps://coliru.stacked-crooked.com/ 可以运行c++代码\nhttps://gcc.gnu.org/onlinedocs\nC++学习的一些网站资料(直接去Github找)\nhttps://en.cppreference.com/w/cpp/language 无脑推荐的百科全书\nhttps://oi-wiki.org/lang/csl/iterator/\nhttps://cntransgroup.github.io/EffectiveModernCppChinese/\nhttps://learn.microsoft.com/zh-cn/cpp/cpp/operator-overloading\n\n\n\n","categories":["C++"],"tags":["C++"]}]