├── .github └── workflows │ └── build.yml ├── .gitignore ├── CNAME ├── README.md ├── _config.fluid.yml ├── _config.yml ├── package-lock.json ├── package.json ├── scripts ├── inject.js └── mail.js └── source ├── _drafts └── ebpf.md ├── _feed.xml ├── _inject └── comment.ejs ├── _posts ├── 2017-08-19-avoid-using-unsigned-numbers.md ├── 2018-02-09-dc-dp-ga.md ├── 2019-08-10-quaternion.md ├── 2019-09-12-jump-game.md ├── 2019-09-12-use-latex-in-jekyll.md ├── 2019-09-13-harmonic-series.md ├── 2019-09-15-lua-decorator.md ├── 2019-09-17-sqrt.md ├── 2019-09-22-pathfinding-graph-search.md ├── 2019-09-23-edit-distance.md ├── 2019-09-28-pathfinding-gen-graph.md ├── 2019-10-07-sudoku-solution.md ├── 2019-10-11-pass-fd-over-domain-socket.md ├── 2019-10-24-mathematics-principle-of-rsa-algorithm.md ├── 2019-11-04-fault-tolerant-and-assert.md ├── 2019-11-07-use-coroutines-to-process-time-consuming-procedures.md ├── 2019-11-18-behavior-tree.md ├── 2019-11-27-passing-passwords-securely.md ├── 2019-11-28-jekyll-step-by-step-tutorial.md ├── 2019-12-21-beginners-guide.md ├── 2020-01-01-2019-annual-summary.md ├── 2020-01-01-dwords.md ├── 2020-01-16-three-maximum-area-problems.md ├── 2020-02-09-promise-and-deferred.md ├── 2020-03-06-dht-and-p2p.md ├── 2020-03-16-fibonacci-sequence.md ├── 2020-03-18-serialize-lua-objects.md ├── 2020-03-20-lua53-environment.md ├── 2020-04-06-vscode-rss.md ├── 2020-04-28-gzip-and-deflate.md ├── 2020-05-08-sync-time-zone.md ├── 2020-05-22-nginx-beginners-guide.md ├── 2020-06-03-cloudflare-free-https.md ├── 2020-06-24-lpeg.md ├── 2020-07-16-character-encoding.md ├── 2020-08-08-y-combinator.md ├── 2020-08-27-rfc1928.md ├── 2020-08-28-single-number.md ├── 2020-09-05-permutation.md ├── 2020-09-13-callback-to-coroutine.md ├── 2020-10-23-lua-next.md ├── 2020-10-29-lua-dst.md ├── 2020-12-01-lua-traceback-with-parameters.md ├── 2020-12-02-subsocks.md ├── 2020-12-09-kcp.md ├── 2021-01-01-2020-annual-summary.md ├── 2021-01-24-reuse-port.md ├── 2021-01-27-regions-cut-by-slashes.md ├── 2021-02-05-hotfix-gen.md ├── 2021-02-20-classic-dp.md ├── 2021-03-31-binary-find.md ├── 2021-05-02-zookeeper.md ├── 2021-06-13-jump-consistent-hash.md ├── 2021-06-27-raspberry-nas.md ├── 2021-07-30-hash-collision.md ├── 2021-08-13-tab-to-search.md ├── 2021-09-05-application-layer-protocol.md ├── 2021-09-21-dwords2.md ├── 2021-11-14-switch-statement.md ├── 2021-12-12-service-migration.md ├── 2021-12-25-kmp.md ├── 2022-01-01-2021-annual-summary.md ├── 2022-01-18-x86-assembly.md ├── 2022-03-03-jekyll-email-protection.md ├── 2022-04-10-gperftools.md ├── 2022-05-06-tree-dp.md ├── 2022-05-10-cpp-const.md ├── 2022-06-25-cpp-memory-order.md ├── 2022-10-30-lock-free-queue.md ├── 2022-12-05-tcpdump.md ├── 2023-01-11-2022-annual-summary.md ├── 2023-01-27-rank-transform-of-a-matrix.md ├── 2023-03-21-nvim.md ├── 2023-06-18-simple-transaction.md ├── 2023-07-09-scheme-lang.md ├── 2024-01-01-2023-annual-review.md ├── 2024-04-19-appimage.md ├── 2024-06-08-republic.md ├── 2024-06-14-c-coroutine.md ├── 2025-02-16-asan.md └── 2025-03-30-clang.md ├── about └── index.md ├── assets ├── images │ ├── 2019-annual-summary_1.jpg │ ├── 2019-annual-summary_2.png │ ├── 2019-annual-summary_3.png │ ├── 2019-annual-summary_4.jpg │ ├── 2020-annual-summary_1.png │ ├── 2020-annual-summary_2.png │ ├── 2020-annual-summary_3.png │ ├── 2020-annual-summary_4.png │ ├── 2020-annual-summary_5.jpg │ ├── 2020-annual-summary_6.jpg │ ├── 2021-annual-summary_1.png │ ├── 2021-annual-summary_2.png │ ├── 2021-annual-summary_3.jpg │ ├── 2021-annual-summary_4.jpg │ ├── 2022-annual-summary_1.png │ ├── 2022-annual-summary_2.png │ ├── 2022-annual-summary_3.jpg │ ├── 2022-annual-summary_4.jpg │ ├── 2022-annual-summary_5.png │ ├── 2022-annual-summary_6.jpg │ ├── 2022-annual-summary_7.png │ ├── 2023-annual-review-1.png │ ├── 2023-annual-review-2.jpg │ ├── 2023-annual-review-3.jpg │ ├── 2023-annual-review-4.jpg │ ├── 2023-annual-review-5.jpg │ ├── 2023-annual-review-6.jpg │ ├── asan_1.png │ ├── asan_2.png │ ├── asan_3.png │ ├── asan_4.png │ ├── beginners-guide_1.png │ ├── c-coroutine_1.svg │ ├── c-coroutine_2.svg │ ├── character-encoding_1.svg │ ├── character-encoding_2.gif │ ├── cloudflare_1.png │ ├── cloudflare_2.png │ ├── cloudflare_3.png │ ├── cloudflare_4.png │ ├── cloudflare_5.svg │ ├── cpp-memory-order_1.svg │ ├── cpp-memory-order_2.svg │ ├── dc-dp-ga_1.png │ ├── dc-dp-ga_2.png │ ├── dc-dp-ga_3.png │ ├── dc-dp-ga_4.png │ ├── dc-dp-ga_5.png │ ├── dht-and-p2p_1.png │ ├── dht-and-p2p_2.png │ ├── dht-and-p2p_3.png │ ├── dht-and-p2p_4.png │ ├── dht-and-p2p_5.png │ ├── dht-and-p2p_6.png │ ├── dht-and-p2p_7.png │ ├── dht-and-p2p_8.png │ ├── dht-and-p2p_9.png │ ├── dwords2_1.png │ ├── dwords2_2.png │ ├── dwords2_3.png │ ├── dwords_1.png │ ├── edit-distance_1.png │ ├── gperftools_1.gif │ ├── gperftools_2.png │ ├── gperftools_3.png │ ├── gzip-and-deflate_1.png │ ├── gzip-and-deflate_2.png │ ├── gzip-and-deflate_3.png │ ├── gzip-and-deflate_4.png │ ├── harmonic-series_1.png │ ├── jump-consistent-hash_1.svg │ ├── jump-consistent-hash_2.svg │ ├── jump-consistent-hash_3.svg │ ├── jump-consistent-hash_4.svg │ ├── kcp_1.svg │ ├── kcp_2.svg │ ├── kcp_3.svg │ ├── kcp_4.svg │ ├── kcp_5.svg │ ├── kcp_6.svg │ ├── kcp_7.svg │ ├── kcp_8.svg │ ├── kcp_9.svg │ ├── kmp_1.svg │ ├── kmp_2.svg │ ├── kmp_3.svg │ ├── kmp_4.svg │ ├── kmp_5.svg │ ├── lock-free-queue_1.svg │ ├── lock-free-queue_2.svg │ ├── lock-free-queue_3.svg │ ├── lock-free-queue_4.png │ ├── nvim_1.png │ ├── nvim_2.png │ ├── nvim_3.png │ ├── pass-fd-over-domain-socket_1.gif │ ├── pathfinding-gen-graph_1.jpeg │ ├── pathfinding-gen-graph_2.png │ ├── pathfinding-gen-graph_3.png │ ├── pathfinding-gen-graph_4.png │ ├── pathfinding-gen-graph_5.png │ ├── pathfinding-gen-graph_6.png │ ├── pathfinding-gen-graph_7.png │ ├── pathfinding-gen-graph_8.png │ ├── pathfinding-gen-graph_9.png │ ├── pathfinding-graph-search_1.png │ ├── pathfinding-graph-search_2.png │ ├── pathfinding-graph-search_3.png │ ├── pathfinding-graph-search_4.png │ ├── pathfinding-graph-search_5.png │ ├── promise-and-deferred_1.png │ ├── promise-and-deferred_2.png │ ├── promise-and-deferred_3.png │ ├── rank-transform-of-a-matrix_1.svg │ ├── rank-transform-of-a-matrix_2.svg │ ├── rank-transform-of-a-matrix_3.svg │ ├── rank-transform-of-a-matrix_4.svg │ ├── rank-transform-of-a-matrix_5.svg │ ├── rank-transform-of-a-matrix_6.svg │ ├── raspberry-nas_1.jpg │ ├── raspberry-nas_2.png │ ├── raspberry-nas_3.jpg │ ├── raspberry-nas_4.png │ ├── regions-cut-by-slashes_1.png │ ├── regions-cut-by-slashes_2.png │ ├── regions-cut-by-slashes_3.png │ ├── regions-cut-by-slashes_4.png │ ├── regions-cut-by-slashes_5.png │ ├── regions-cut-by-slashes_6.svg │ ├── regions-cut-by-slashes_7.svg │ ├── regions-cut-by-slashes_8.svg │ ├── regions-cut-by-slashes_9.svg │ ├── scheme-lang_1.png │ ├── scheme-lang_2.png │ ├── scheme-lang_3.png │ ├── service-migration_1.png │ ├── service-migration_2.svg │ ├── sqrt_1.png │ ├── sqrt_2.png │ ├── subsocks_1.svg │ ├── subsocks_2.svg │ ├── subsocks_3.svg │ ├── sudoku-solution_1.png │ ├── sudoku-solution_2.png │ ├── sudoku-solution_3.png │ ├── sudoku-solution_4.png │ ├── three-maximum-area-problems_1.png │ ├── three-maximum-area-problems_2.png │ ├── three-maximum-area-problems_3.png │ ├── three-maximum-area-problems_4.png │ ├── three-maximum-area-problems_5.png │ ├── three-maximum-area-problems_6.png │ ├── three-maximum-area-problems_7.png │ ├── tree-dp_1.svg │ ├── tree-dp_2.svg │ ├── tree-dp_3.svg │ ├── tree-dp_4.jpeg │ ├── tree-dp_4.svg │ ├── tree-dp_5.svg │ ├── tree-dp_6.svg │ ├── unreliable-tcp-connections_1.svg │ ├── unreliable-tcp-connections_2.svg │ ├── use-coroutines-to-process-time-consuming-procedures_1.png │ ├── vscode-rss_1.gif │ ├── x86-assembly_1.svg │ ├── x86-assembly_2.svg │ ├── x86-assembly_3.svg │ ├── x86-assembly_4.svg │ ├── x86-assembly_5.svg │ ├── x86-assembly_6.svg │ ├── x86-assembly_7.svg │ ├── zookeeper_1.jpg │ ├── zookeeper_2.jpg │ └── zookeeper_3.svg └── videos │ ├── dwords2_1.mp4 │ ├── dwords2_2.mp4 │ ├── dwords2_3.mp4 │ ├── nvim_1.mp4 │ ├── nvim_2.mp4 │ ├── nvim_3.mp4 │ ├── nvim_4.mp4 │ ├── tab-to-search_1.mp4 │ └── tab-to-search_2.mp4 ├── favicon.ico ├── google1c9d1155cc80282c.html ├── img ├── avatar.png ├── default-avatar.png ├── default.jpg └── favicon-32x32.png ├── links └── index.md ├── pretty-feed-v3.xsl └── robots.txt /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - env: 12 | BUILD_PASSWORD: ${{ secrets.BUILD_PASSWORD }} 13 | run: | 14 | code=$(curl --silent --output /dev/stderr --write-out "%{http_code}" -u "github:$BUILD_PASSWORD" -X POST 'https://luyuhuang.tech/build') 15 | test "$code" -eq 200 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | .DS_Store 3 | Thumbs.db 4 | db.json 5 | *.log 6 | node_modules/ 7 | public/ 8 | .deploy*/ 9 | _multiconfig.yml 10 | -------------------------------------------------------------------------------- /CNAME: -------------------------------------------------------------------------------- 1 | luyuhuang.tech -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is my blog: . Powered by [Hexo](https://hexo.io) & [Fluid](https://github.com/fluid-dev/hexo-theme-fluid). 2 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | # Hexo Configuration 2 | ## Docs: https://hexo.io/docs/configuration.html 3 | ## Source: https://github.com/hexojs/hexo/ 4 | 5 | # Site 6 | title: Luyu Huang's Blog 7 | subtitle: Stay hungry. Stay foolish. 8 | description: >- 9 | Sharing something about programming, algorithms, mathematics, etc. Update from time to time | 10 | 分享有关编程, 算法, 数学等方面的信息. 不定期更新 11 | keywords: 12 | author: Luyu Huang 13 | language: en 14 | 15 | # URL 16 | ## Set your site url here. For example, if you use GitHub Page, set url as 'https://username.github.io/project' 17 | url: https://luyuhuang.tech 18 | permalink: :year/:month/:day/:title.html 19 | permalink_defaults: 20 | pretty_urls: 21 | trailing_index: true # Set to false to remove trailing 'index.html' from permalinks 22 | trailing_html: true # Set to false to remove trailing '.html' from permalinks 23 | 24 | # Directory 25 | source_dir: source 26 | public_dir: public 27 | tag_dir: tags 28 | archive_dir: archives 29 | category_dir: categories 30 | code_dir: downloads/code 31 | i18n_dir: :lang 32 | skip_render: 33 | - google1c9d1155cc80282c.html 34 | - robots.txt 35 | 36 | # Writing 37 | new_post_name: :year-:month-:day-:title.md # File name of new posts 38 | default_layout: post 39 | titlecase: false # Transform title into titlecase 40 | external_link: 41 | enable: true # Open external links in new tab 42 | field: site # Apply to the whole site 43 | exclude: '' 44 | filename_case: 0 45 | render_drafts: false 46 | post_asset_folder: false 47 | relative_link: false 48 | future: true 49 | highlight: 50 | enable: true 51 | line_number: true 52 | auto_detect: false 53 | tab_replace: '' 54 | wrap: true 55 | hljs: false 56 | prismjs: 57 | enable: false 58 | preprocess: true 59 | line_number: true 60 | tab_replace: '' 61 | 62 | # Home page setting 63 | # path: Root path for your blogs index page. (default = '') 64 | # per_page: Posts displayed per page. (0 = disable pagination) 65 | # order_by: Posts order. (Order by date descending by default) 66 | index_generator: 67 | path: '' 68 | per_page: 10 69 | order_by: -date 70 | 71 | archive_generator: 72 | per_page: 0 73 | 74 | sitemap: 75 | path: 76 | - sitemap.xml 77 | rel: false 78 | tags: false 79 | categories: false 80 | 81 | # Category & Tag 82 | default_category: uncategorized 83 | category_map: 84 | tag_map: 85 | 86 | # Metadata elements 87 | ## https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta 88 | meta_generator: true 89 | 90 | # Date / Time format 91 | ## Hexo uses Moment.js to parse and display date 92 | ## You can customize the date format as defined in 93 | ## http://momentjs.com/docs/#/displaying/format/ 94 | date_format: LL 95 | time_format: HH:mm:ss 96 | ## updated_option supports 'mtime', 'date', 'empty' 97 | updated_option: 'mtime' 98 | 99 | # Pagination 100 | ## Set per_page to 0 to disable pagination 101 | per_page: 10 102 | pagination_dir: page 103 | 104 | # Include / Exclude file(s) 105 | ## include:/exclude: options only apply to the 'source/' folder 106 | include: 107 | exclude: 108 | ignore: 109 | 110 | # Extensions 111 | ## Plugins: https://hexo.io/plugins/ 112 | ## Themes: https://hexo.io/themes/ 113 | theme: fluid 114 | 115 | # Deployment 116 | ## Docs: https://hexo.io/docs/one-command-deployment 117 | deploy: 118 | type: '' 119 | 120 | feed: 121 | enable: true 122 | autodiscovery: false 123 | type: atom 124 | path: feed.xml 125 | limit: 10 126 | template: 127 | - source/_feed.xml 128 | 129 | pandoc: 130 | extra: 131 | - from: markdown 132 | 133 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hexo-site", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "build": "hexo generate", 7 | "clean": "hexo clean", 8 | "deploy": "hexo deploy", 9 | "server": "hexo server" 10 | }, 11 | "hexo": { 12 | "version": "6.3.0" 13 | }, 14 | "dependencies": { 15 | "hexo": "^6.3.0", 16 | "hexo-clean-css": "^2.0.0", 17 | "hexo-generator-archive": "^2.0.0", 18 | "hexo-generator-feed": "^3.0.0", 19 | "hexo-generator-index": "^3.0.0", 20 | "hexo-generator-sitemap": "^3.0.1", 21 | "hexo-generator-tag": "^2.0.0", 22 | "hexo-html-minifier": "^1.0.0", 23 | "hexo-renderer-ejs": "^2.0.0", 24 | "hexo-renderer-pandoc": "^0.3.1", 25 | "hexo-renderer-stylus": "^2.1.0", 26 | "hexo-server": "^3.0.0", 27 | "hexo-theme-fluid": "git+https://github.com/luyuhuang/hexo-theme-fluid.git#random-slogan", 28 | "hexo-uglify": "^2.0.0" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /scripts/inject.js: -------------------------------------------------------------------------------- 1 | hexo.extend.filter.register('theme_inject', function(injects) { 2 | injects.postComments.file('default', 'source/_inject/comment.ejs'); 3 | injects.linksComments.file('default', 'source/_inject/comment.ejs'); 4 | }); 5 | 6 | hexo.extend.injector.register('head_end', ``) 11 | 12 | hexo.extend.injector.register('head_end', 13 | ``) 14 | hexo.extend.injector.register('head_end', 15 | ``) 16 | 17 | -------------------------------------------------------------------------------- /scripts/mail.js: -------------------------------------------------------------------------------- 1 | const url_for = hexo.extend.helper.get('url_for').bind(hexo); 2 | hexo.extend.helper.register('url_for', function(path) { 3 | if (path && path.startsWith('mailto:')) { 4 | const rand = Math.floor(Math.random() * 0x100); 5 | return '#' + rand.toString(16).padStart(2, '0') + Array.from(new TextEncoder().encode(path)) 6 | .map(c => (c ^ rand).toString(16).padStart(2, '0')) 7 | .join('') 8 | } 9 | return url_for(path); 10 | }); 11 | 12 | const decoder = (function() { 13 | function byte(s, i) { 14 | return parseInt(s.substr(i, 2), 16); 15 | }; 16 | 17 | function decode(s) { 18 | if (!s) return s; 19 | s = s.substr(1); 20 | for (var a = '', t = byte(s, 0), i = 2; i < s.length; i += 2) { 21 | a += String.fromCharCode(byte(s, i) ^ t); 22 | } 23 | return a; 24 | }; 25 | 26 | document.querySelectorAll('a').forEach(function(el) { 27 | var s = decode(el.getAttribute('href')); 28 | if (s && s.startsWith('mailto:')) { 29 | el.setAttribute('href', s); 30 | } 31 | }); 32 | }).toString(); 33 | 34 | hexo.extend.injector.register('body_end', ``) 35 | -------------------------------------------------------------------------------- /source/_feed.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ config.title }} 5 | {% if icon %}{{ icon }}{% endif %} 6 | {% if config.subtitle %}{{ config.subtitle }}{% endif %} 7 | 8 | {% if config.feed.hub %}{% endif %} 9 | 10 | {% if posts.first().updated %}{{ posts.first().updated.toISOString() }}{% else %}{{ posts.first().date.toISOString() }}{% endif %} 11 | {{ url | uriencode }} 12 | {% if config.author %} 13 | 14 | {{ config.author }} 15 | {% if config.email %}{{ config.email }}{% endif %} 16 | 17 | {% endif %} 18 | Hexo 19 | {% for post in posts.toArray() %} 20 | 21 | {{ post.title }} 22 | 23 | {{ post.permalink | uriencode }} 24 | {{ post.date.toISOString() }} 25 | {% if post.updated %}{{ post.updated.toISOString() }}{% else %}{{ post.date.toISOString() }}{% endif %} 26 | {% if config.feed.content and post.content %} 27 | 28 | {% endif %} 29 | {% if post.description %} 30 | {{ post.description }} 31 | {% elif post.intro %} 32 | {{ post.intro }} 33 | {% elif post.excerpt %} 34 | {{ post.excerpt }} 35 | {% elif post.content %} 36 | {% set short_content = post.content.substring(0, config.feed.content_limit) %} 37 | {% if config.feed.content_limit_delim %} 38 | {% set delim_pos = short_content.lastIndexOf(config.feed.content_limit_delim) %} 39 | {% if delim_pos > -1 %} 40 | {{ short_content.substring(0, delim_pos) }} 41 | {% else %} 42 | {{ short_content }} 43 | {% endif %} 44 | {% else %} 45 | {{ short_content }} 46 | {% endif %} 47 | {% endif %} 48 | {% if post.image %} 49 | 50 | {% endif %} 51 | {% for category in post.categories.toArray() %} 52 | 53 | {% endfor %} 54 | {% for tag in post.tags.toArray() %} 55 | 56 | {% endfor %} 57 | 58 | {% endfor %} 59 | 60 | -------------------------------------------------------------------------------- /source/_inject/comment.ejs: -------------------------------------------------------------------------------- 1 | <% if(theme.gitalk.clientID && theme.gitalk.clientSecret && theme.gitalk.repo){ %> 2 |
3 | 18 | 19 | <% } %> 20 | -------------------------------------------------------------------------------- /source/_posts/2017-08-19-avoid-using-unsigned-numbers.md: -------------------------------------------------------------------------------- 1 | --- 2 | key: 1 3 | title: 避免使用无符号数 4 | tag: experience 5 | --- 6 | 考察这样一段代码: 7 | ```c 8 | int a = -1; 9 | unsigned int b = 1; 10 | 11 | if (a < b) 12 | printf("a < b\n"); 13 | else 14 | printf("a > b\n"); 15 | ``` 16 | a是有符号整数,b是无符号整数。C语言在比较他们的大小时会进行隐式类型转换。如果执行的是 17 | `if ((unsigned int)a < b)` 18 | 则-1被转换成4294967295,结果是 **a > b**;如果执行的是 19 | `if (a < (int)b)` 20 | 则结果是 **a < b**。采取哪种方式**依赖于编译器**。 21 | 22 | 在g++中,输出的结果是a > b。当然,也会打出警告:warning: comparison between signed and unsigned integer expressions 23 | 24 | 为了避免这个问题,我们通常的做法是 25 | `if (a < (int)b)` 26 | 但是,如果b大与32有符号整数的最大值2147483647,就会发生数据溢出,(int)b将会是一个负值。 27 | 28 | 因此,**避免在程序中使用无符号数!!!** 29 | -------------------------------------------------------------------------------- /source/_posts/2019-08-10-quaternion.md: -------------------------------------------------------------------------------- 1 | --- 2 | key: 3 3 | title: 四元数描述旋转 4 | math: true 5 | tag: math 6 | --- 7 | **先看结论:** 8 | 9 | 对于任意坐标 $(a,b,c)$ , 我们希望绕旋转轴 $(x,y,z)$ 旋转 $\theta$ 度, 其中 x, y, z 的平方和为1. 那么: 10 | 11 | 令四元数 12 | 13 | $$q=\cos\frac{\theta}{2}+\sin\frac{\theta}{2}(x\mathrm{i}+y\mathrm{j}+z\mathrm{k})$$ 14 | 15 | $$p=a\mathrm{i}+b\mathrm{j}+c\mathrm{k}$$ 16 | 17 | 得到 18 | 19 | $$p'=qpq^{-1}$$ 20 | 21 | 其中 $q^{-1}$ 是 $q$ 的逆, $q^{-1}=\cos\frac{\theta}{2}-\sin\frac{\theta}{2}(x\mathrm{i}+y\mathrm{j}+z\mathrm{k})$. 这时 $p'$ 是形如 $a'\mathrm{i}+b'\mathrm{j}+c'\mathrm{k}$ 的四元数, 实数部分必然为 0 . 坐标 $(a',b',c')$ 即是旋转后的坐标. 22 | 23 | 将来(有空的话)我会补上详解. 强烈推荐去看参考资料列出的视频, 可以说讲得非常直观形象, 可能一遍看不懂, 多看几便就好了. 24 | 25 | **参考资料:** 26 | - [四元数的可视化](https://www.bilibili.com/video/av33385105) 27 | - [四元数和三维转动,可互动的探索式视频(请看链接)](https://www.bilibili.com/video/av35804287) 28 | -------------------------------------------------------------------------------- /source/_posts/2019-09-12-jump-game.md: -------------------------------------------------------------------------------- 1 | --- 2 | key: 4 3 | title: 跳跃游戏 4 | math: true 5 | tag: 6 | - algorithms 7 | - leetcode 8 | --- 9 | 题目源自Leetcode: [跳跃游戏II-leetcode](https://leetcode-cn.com/problems/jump-game-ii/) 10 | 11 | 给定一个非负整数数组, 你最初位于数组的第一个位置. 数组中的每个元素代表你在该位置可以跳跃的最大长度. 你的目标是使用最少的跳跃次数到达数组的最后一个位置. 12 | 13 | 示例: 14 | 15 | > 输入: [2,3,1,1,4] 16 | > 17 | > 输出: 2 18 | > 19 | > 解释: 跳到最后一个位置的最小跳跃数是 2. 20 | >  从下标为 0 跳到下标为 1 的位置, 跳 1 步, 然后跳 3 步到达数组的最后一个位置. 21 | 22 | ### 思路1: 动态规划 23 | 我们令 dp[i] 为 从第 i 个位置到最后一个位置所需的跳跃次数. 显然, 若数组长度为 n , dp[n] = 0. 对于其他的位置 i, 假设 j 是任意一个 i 能跳到的位置, dp[i] 应为 所有 dp[j] 的最小值再加1. 即: 24 | 25 | $$ 26 | dp[i]=\left\{\begin{matrix} 27 | 0 & i = n \\ 28 | \underset{j\in \{all\}}{\min}\{dp[j]\}+1 & i < n 29 | \end{matrix}\right. 30 | $$ 31 | 32 | 这里的 {all} 表示 所有在位置 i 能跳到的位置. 33 | 34 | 有了这个递归式我们很快能写出一个动态规划算法: 35 | 36 | ```python 37 | class Solution(object): 38 | def jump(self, nums): 39 | """ 40 | :type nums: List[int] 41 | :rtype: int 42 | """ 43 | dp = [float('inf')] * len(nums) 44 | dp[-1] = 0 45 | for i in xrange(len(nums) - 2, -1, -1): 46 | to = min(i + nums[i], len(nums) - 1) 47 | dp[i] = min(dp[j] for j in xrange(i, to + 1)) + 1 48 | 49 | return dp[0] 50 | ``` 51 | 52 | 这个算法本身是正确的, 但是无法通过 Leetcode 提交, 因为会超出时间限制 53 | 54 | ### 思路2: 贪心算法 55 | 我们可以选用这样一种策略: 对于每个位置 i, 都能跳到若干个位置; 总是在这若干个位置中**选择能跳得最远的位置**进行跳跃. 56 | 57 | 以 [2,3,1,1,4] 为例: 对于位置 i = 0, 能跳到 1 和 2 这连个位置; 如果选择跳到位置 1, 那么最远可以跳到 5; 如果选择跳到位置2, 那么最远可以跳到 4. 因此我们选择跳到位置 1. 接下来对于 i = 1, 再作同样的操作即可. 根据这个思路, 我们可以写出这样一个贪心算法: 58 | 59 | ```python 60 | class Solution(object): 61 | def jump(self, nums): 62 | """ 63 | :type nums: List[int] 64 | :rtype: int 65 | """ 66 | steps = 0 67 | end = 0 68 | maxPos = 0 69 | for i in xrange(len(nums) - 1): 70 | maxPos = max(nums[i] + i, maxPos) 71 | 72 | if i == end: 73 | end = maxPos 74 | steps += 1 75 | 76 | return steps 77 | ``` 78 | -------------------------------------------------------------------------------- /source/_posts/2019-09-12-use-latex-in-jekyll.md: -------------------------------------------------------------------------------- 1 | --- 2 | key: 5 3 | title: 在Jekyll中使用LaTeX 4 | math: true 5 | tag: tools 6 | --- 7 | 我准备用 Jekyll + Github page 搭建自己的技术博客. 但是有个问题, 技术文章中不可避免地需要使用到数学公式, Jekyll 原生的 Markdown 解释器总是不能很好地使用 Latex. 通过查阅资料, 我最终解决了这个问题. 下面是我的做法: 8 | 9 | 1. 禁用 Kramdown 自带的公式解释器: 10 | 11 | 在 `_config.yml` 中加入: 12 | ```yml 13 | kramdown: 14 | math_engine: null 15 | ``` 16 | 2. 导入 mathjax 的 javascript 代码: 17 | 在 `_includes` 下新建文件 `latex.html`, 粘贴上以下内容: 18 | ```html 19 | 33 | 35 | ``` 36 | 3. 把 mathjax include 到 html 的 `` 标签中: 37 | 这一步根据你使用的主题的不同, 修改 _layouts 文件 或是 _includes 文件. 总之就是找到 `` 标签定义的地方然后加入 include 代码. 我使用的主题是 minima, minima 的 `` 标签定义在 `_includes/head.html` 中. 因此我在自己的博客目录下新建文件 `_includes/head.html` 来覆盖主题默认的文件, 粘贴上以下内容: 38 | ```html 39 | 40 | 41 | ... 42 | 43 | {% unless page.no_latex %} 44 | {% include latex.html %} 45 | {% endunless %} 46 | 47 | ``` 48 | 49 | 大功告成! 在 \\$ \\$ 之间的 LaTex 会变成行内公式就像这样: `$\mathrm{e}^{\pi\mathrm{i}}+1=0$` 转换成 $\mathrm{e}^{\pi\mathrm{i}}+1=0$ ; 新起一段并且在 \\$\\$ \\$\\$ 之间的 LaTeX 会变成段落公式就像这样: 50 | ``` 51 | 52 | $$ 53 | H_n=1+\frac{1}{2}+\frac{1}{3}+...+\frac{1}{n}=\sum_{i=1}^{n}\frac{1}{i}=O(\log n) 54 | $$ 55 | ``` 56 | 转换成 57 | 58 | $$ 59 | H_n=1+\frac{1}{2}+\frac{1}{3}+...+\frac{1}{n}=\sum_{i=1}^{n}\frac{1}{i}=O(\log n) 60 | $$ 61 | 62 | 参考资料: [How to use MathJax in Jekyll generated Github pages 63 | ](https://haixing-hu.github.io/programming/2013/09/20/how-to-use-mathjax-in-jekyll-generated-github-pages/) 64 | -------------------------------------------------------------------------------- /source/_posts/2019-09-13-harmonic-series.md: -------------------------------------------------------------------------------- 1 | --- 2 | key: 6 3 | title: 调和级数的渐进表示 4 | math: true 5 | tag: math 6 | --- 7 | > 令 $H_n$ 为第 n 项调和数 8 | > 9 | $$ 10 | H_n=1+\frac{1}{2}+\frac{1}{3}+...+\frac{1}{n}=\sum_{i=1}^{n}\frac{1}{i} 11 | $$ 12 | > 13 | > 证明 $H_n$ 是 $O(\log n)$ 的 14 | 15 | **证明** 如下图所示: 16 | 17 | ![img](/assets/images/harmonic-series_1.png) 18 | 19 | $\sum_{i=1}^{n}\frac{1}{i}$可以看作图中蓝色阴影的面积; 而橙色部分的面积则可以看作函数 $y=\frac{1}{x}$ 的积分 $\int_{0}^{n}\frac{1}{x}\mathrm{d}x$. 因此有 20 | 21 | $$ 22 | \sum_{i=2}^{n}\frac{1}{i}\lt \int_{0}^{n}\frac{1}{x}\mathrm{d}x=\ln n 23 | $$ 24 | 25 | $$ 26 | \sum_{i=1}^{n}\frac{1}{i}\lt \ln n + 1 = O(\log n) 27 | $$ 28 | 29 | 证毕. 30 | -------------------------------------------------------------------------------- /source/_posts/2019-09-15-lua-decorator.md: -------------------------------------------------------------------------------- 1 | --- 2 | key: 7 3 | title: 在Lua中使用装饰器 4 | tag: lua 5 | --- 6 | ## 引言 7 | 使用过 Python 的同学都会喜欢上 Python 的装饰器. 它提供一种语法, 对函数进行"声明": 8 | 9 | ```python 10 | def decorator(f): 11 | def wrapper(x): 12 | print 'call %s' % f.__name__ 13 | return "The 2nd power of {0} is {1}".format(x, f(x)) 14 | return wrapper 15 | 16 | @decorator 17 | def foo(x): 18 | return x ** 2 19 | ``` 20 | 21 | 装饰器本质上只是一种语法糖: 把目标函数作为参数传入装饰器. 巧妙地使用装饰器, 可以让程序变得简洁优雅. 比如说, 声明一个函数是事件, 声明一个函数是远程调用接口, 等等. 22 | 23 | ## 在 Lua 中实现 "装饰器" 24 | 笔者过去常常使用 Python, 后来使用了 Lua 后, 一直很怀念 Python 的装饰器. 于是就运用了一些奇技淫巧, 用曲线救国的方式在 Lua 中实现了类似装饰器的用法. 具体的做法是用元表监听每一次函数定义, 然后在每次函数定义的时候检测有没有调用过装饰器函数, 如果有, 就执行执行相关逻辑. 25 | 26 | ```lua 27 | local decorated = false 28 | local m = setmetatable({}, {__newindex = function(t, k, v) 29 | if 'function' == type(v) and decorated then 30 | local function wrapper(x) 31 | print(string.format('call %s', k)) 32 | return string.format("The 2nd power of %s is %s", x, v(x)) 33 | end 34 | decorated = false 35 | rawset(t, k, wrapper) 36 | else 37 | rawset(t, k, v) 38 | end 39 | end}) 40 | 41 | local function decorator() 42 | decorated = true 43 | end 44 | 45 | decorator() 46 | function m.foo(x) 47 | return x ^ 2 48 | end 49 | ``` 50 | 51 | 这种实现还是有点麻烦, 局限性也比较强. 笔者在项目中把它应用在了 远程调用接口 的声明上. 被声明过的函数可被客户端调用, 否则只是一个内部函数. 主要是为了让业务逻辑层用着爽, 底层麻烦点也就无所谓了. 52 | 53 | *** 54 | **2019-11-14 更新:** 55 | 56 | 上面那种做法太过特殊处理了, 而且不支持多重装饰器. 后来我又想到一种改进方案: 57 | 58 | ```lua 59 | local decorators = {} 60 | local m = setmetatable({}, {__newindex = function(t, k, v) 61 | if 'function' == type(v) then 62 | for i = #decorators, 1, -1 do 63 | v = decorators[i](k, v) 64 | decorators[i] = nil 65 | end 66 | end 67 | rawset(t, k, v) 68 | end}) 69 | 70 | local function AT(f) 71 | table.insert(decorators, f) 72 | end 73 | 74 | local function decorator() 75 | AT(function(fname, f) 76 | return function(x) 77 | print(string.format('call %s', fname)) 78 | return string.format("The 2nd power of %s is %s", x, f(x)) 79 | end 80 | end) 81 | end 82 | 83 | decorator() 84 | function m.foo(x) 85 | return x ^ 2 86 | end 87 | ``` 88 | 89 | 这样灵活性和可维护性就强多了, 而且支持多重装饰器. 90 | -------------------------------------------------------------------------------- /source/_posts/2019-09-17-sqrt.md: -------------------------------------------------------------------------------- 1 | --- 2 | key: 8 3 | title: 牛顿迭代法求平方根 4 | math: true 5 | tag: math 6 | --- 7 | ## 1.先说结论 8 | $\sqrt{a}$ 可这样求得: 令 $x_0$ 为任意实数, 执行以下迭代式: 9 | 10 | $$ 11 | x_i = \frac{x_{i-1}+\frac{a}{x_{i-1}}}{2} \tag{1} 12 | $$ 13 | 14 | 迭代若干次, 当 $\|x_i-x_{i-1}\|$ 小于想要的精度时便可停止迭代. 最终的 $x_i$ 便可视为 $\sqrt{a}$. 根据 (1) 式我们可以很快写出求平方根的代码: 15 | 16 | ```python 17 | def sqrt(a): 18 | x = 1.0 19 | while True: 20 | pre = x 21 | x = (x + a / x) / 2 22 | if abs(x - pre) < 1e-6: 23 | break 24 | 25 | return x 26 | ``` 27 | 28 | ## 2.详解 29 | 牛顿迭代法是一种近似求多项式方程根的一种方法. 30 | 31 | ![image](/assets/images/sqrt_1.png) 32 | 33 | 如图所示, 对于方程 $f(x) = 0$ , 我们任取一个实数 $x_0$, 过点 $(x_0, f(x_0))$ 作 $f(x)$ 的切线 $l$ 交 x 轴于 $x_1$ . 我们有: 34 | 35 | $$ 36 | f'(x_0) = \frac{\mathrm{d}f(x_0)}{\mathrm{d}x_0} = \frac{f(x_0)}{x_0-x_1} 37 | $$ 38 | 39 | $$ 40 | x_1 = x_0 - \frac{f(x_0)}{f'(x_0)} 41 | $$ 42 | 43 | 重复以上操作, 分别计算出 $x_2, x_3, ...$ 44 | 45 | ![image](/assets/images/sqrt_2.png) 46 | 47 | 最终 $x_n$ 会逼近 $f(x) = 0$ 的根. 也就是不断执行这个迭代式: 48 | 49 | $$ 50 | x_i = x_{i-1} - \frac{f(x_{i-1})}{f'(x_{i-1})} \tag{2} 51 | $$ 52 | 53 | \(2) 式被称为**牛顿迭代公式** 54 | 55 | 56 | 用牛顿迭代法求 $\sqrt{a}$ 实际上就是求方程 $x^2-a=0$ 的根. 带入牛顿迭代公式, 得: 57 | 58 | $$ 59 | x_i = x_{i-1} - \frac{x_{i-1}^2 - a}{2x_{i-1}} = \frac{2x_{i-1}}{2} - \frac{x_{i-1}-\frac{a}{x_{i-1}}}{2} = \frac{x_{i-1}+\frac{a}{x_{i-1}}}{2} 60 | $$ 61 | 62 | 也就得到了文章开头所列出的 (1) 式. 63 | -------------------------------------------------------------------------------- /source/_posts/2019-09-23-edit-distance.md: -------------------------------------------------------------------------------- 1 | --- 2 | key: 10 3 | title: 编辑距离 4 | math: true 5 | tag: 6 | - algorithms 7 | - leetcode 8 | --- 9 | 题目源自Leetcode: [编辑距离-leetcode](https://leetcode-cn.com/problems/edit-distance/) 10 | 11 | 给定两个单词 word1 和 word2,计算出将 word1 转换成 word2 所使用的最少操作数 。 12 | 13 | 你可以对一个单词进行如下三种操作: 14 | 15 | - 插入一个字符 16 | - 删除一个字符 17 | - 替换一个字符 18 | 19 | 示例: 20 | 21 | > 输入: word1 = "horse", word2 = "ros"
22 | > 输出: 3
23 | > 解释:
24 | > horse -> rorse (将 'h' 替换为 'r')
25 | > rorse -> rose (删除 'r')
26 | > rose -> ros (删除 'e') 27 | 28 | ### 解法: 动态规划 29 | 这是一道很漂亮的动态规划问题. 我们这样想: 越长的单词求解就越困难, 越短的单词求解就越简单: 长度为1的单词只需比较字母是否相等, 相等则编辑距离为1否则为0. 因此, 我们要设法把大问题变成小问题. 这里我们用 `dp[i][j]` 表示 `word1[:i]` 到 `word2[:j]` 的编辑距离. 例如, `word1 = "horse"` `word2 = "ros"`, 那么 `dp[1][1] = 1`, 因为 `"h"` 到 `"r"` 只需要执行一次替换, 编辑距离为1. 特别地, `dp[0][n] = n`, 它表示空字符串到任意长度为n的字符串的编辑距离: 做n次插入即可. 同理, `dp[n][0] = n`. 30 | 31 | 接下来我们想办法构造出递推式. 我们通过观察可以发现, 把 `word1[:i]` 编辑到 `word2[:j]` 可以看作: 32 | 33 | 1. 先把 `word1[:i]` 最后一个字母删掉, 得到 `word1[:i-1]`, 再把 `word1[:i-1]` 编辑到 `word2[:j]`. 这个时候的编辑次数等于 `dp[i-1][j] + 1`; 34 | 2. 先把 `word1[:i]` 编辑到 `word2[:j-1]`, 再在 `word2[:j-1]` 末尾作一次插入. 这个时候编辑次数等于 `dp[i][j-1] + 1`; 35 | 3. 先把 `word1[:i-1]` 编辑到 `word2[:j-1]`, 然后再看: 36 | - 如果最后一个字母相等, 那么就什么都不用做, 编辑距离等于 `dp[i-1][j-1]`; 37 | - 如果最后一个字母不相等, 那么就需要作一次修改, 编辑距离等于 `dp[i-1][j-1] + 1`. 38 | 39 | 由此, 我们可得递推式: 40 | 41 | $$ 42 | dp[i][j]=\left\{\begin{matrix} 43 | \min(dp[i-1][j]+1,dp[i][j-1]+1,dp[i-1][j-1]) & word_1[i-1]=word_2[j-1] \\ 44 | \min(dp[i-1][j]+1,dp[i][j-1]+1,dp[i-1][j-1] + 1) & word_1[i-1]\ne word_2[j-1] 45 | \end{matrix}\right. 46 | $$ 47 | 48 | OK, 有了递推式, 我们可以开始写代码了. 49 | 50 | **第一步:初始化**. 因为我们用 `dp[i][j]` 表示 `word1[:i]` 到 `word2[:j]` 的编辑距离, 所以数组 `dp` 两个维度的长度分别要比 `len(word1)` 和 `len(word2)` 多1. 所以有: 51 | 52 | ```python 53 | m, n = len(word1), len(word2) 54 | dp = [[None] * (n + 1) for _ in xrange(m + 1)] 55 | ``` 56 | 57 | 上文提到了 `dp[0][n] = n` 和 `dp[n][0] = n`, 所以有: 58 | 59 | ```python 60 | dp[0][0] = 0 61 | for i in xrange(m + 1): 62 | dp[i][0] = i 63 | for j in xrange(n + 1): 64 | dp[0][j] = j 65 | ``` 66 | 67 | **第二步:安排迭代顺序**. 我们发现, `dp[i][j]` 依赖于 `dp[i-1][j]`, `dp[i][j-1]` 和 `dp[i-1][j-1]`. 如图所示: 68 | 69 | ![iter](/assets/images/edit-distance_1.png){width="300"} 70 | 71 | 图中红色箭头代表依赖. 所以, 很简单, 逐行遍历即可. 所以代码是这样的: 72 | 73 | ```python 74 | for i in xrange(1, m + 1): 75 | for j in xrange(1, n + 1): 76 | if word1[i - 1] == word2[j - 1]: 77 | dp[i][j] = min(dp[i-1][j] + 1, dp[i][j-1] + 1, dp[i-1][j-1]) 78 | else: 79 | dp[i][j] = min(dp[i-1][j] + 1, dp[i][j-1] + 1, dp[i-1][j-1] + 1) 80 | ``` 81 | 82 | 最后 `return dp[-1][-1]` 即可. 83 | -------------------------------------------------------------------------------- /source/_posts/2019-09-28-pathfinding-gen-graph.md: -------------------------------------------------------------------------------- 1 | --- 2 | key: 11 3 | title: 详解寻路算法(2)-生成图 4 | math: true 5 | tag: 6 | - algorithms 7 | - featured 8 | --- 9 | ## 1. 引言 10 | [上篇文章](/2019/09/22/pathfinding-graph-search.html) 中主要讲解了 A* 算法. 然而 A* 算法只是一个图搜索算法, 我们在游戏中的地图通常是用一些不规则图形定义的一片行走区域, A* 算法并不能识别这样的地图. 11 | 12 | ![lol](/assets/images/pathfinding-gen-graph_1.jpeg){width="400"} 13 | 14 | 因此我们要做的工作就是把一张这样的地图抽象成可用**邻接矩阵**或**邻接链表**表示的数学上的图 $G=(V,E)$. 本文介绍两种方法, **可视图(visibility graph)** 法和 **导航网络(Navigation Meshes)**法. 15 | 16 | ## 2. 可视图(visibility graph) 17 | 对于大多数地图来说, 我们可以看成由一个无限大的行走区域和若干个障碍物组成; 为了简化问题, 障碍物通常都可以看做多边形. 如下图所示: 18 | 19 | ![obstacle](/assets/images/pathfinding-gen-graph_2.png) 20 | 21 | 想象我们处于起始点, 要绕过障碍物到达目标点. 当我们绕过障碍物时, 最短的方式应该是贴着障碍物的边缘走: 22 | 23 | ![bypass obstacles](/assets/images/pathfinding-gen-graph_3.png) 24 | 25 | 所以我们选择多边形的各个顶点, 我们称之为**导航点(navigation points)**; 同时, 因为起始点和终止点也是寻路中要用到的关键点, 所以它们也属于导航点. 26 | 27 | 现在我们尝试通过导航点构造出图. 具体的做法是, 对于任意一对导航点 `u, v`, 如果互相能够 "看见" 对方, 就连接这两个两个点. 所谓的 "看见" 就是, 连接两点, 连线不与任何障碍物相交. 如下图所示: 28 | 29 | ![bypass obstacles](/assets/images/pathfinding-gen-graph_4.png) 30 | 31 | 这样, 我们就构造出了一张**可视图(visibility graph)**. 我们把地图抽象成了数学上的图 $G=(V,E)$, V 便是所有的导航点集合, E 便是图中所有的连线集合; 每条边的权重就是这条边实际的长度, 同时每个点保留实际坐标信息, 供 A* 算法的启发式函数使用. 现在, 我们就可以用 A* 算法或 Dijkstra 算法搜索这张图, 就能得到最短路径. 32 | 33 | 可视图虽然可以把地图抽象成图, 但也有一定的问题. 最大的问题是, 当多边形过于复杂, 顶点过多时, 可视图算法会生成大量的边. 假设有 n 个导航点, 那么边的数量将会是 $O(n^2)$, 如图所示: 34 | 35 | ![bypass obstacles](/assets/images/pathfinding-gen-graph_5.png) 36 | > 图片源自 [Map representations](https://theory.stanford.edu/~amitp/GameProgramming/MapRepresentations.html) 37 | 38 | 另外一个问题是, 由于起始点和目标点都是导航点, 所以在每次寻路开始时都需要把起始点和终止点加入图中, 并且构造出必要的边; 在寻路结束时又要把他们删除掉. 这都会在地图过大或过复杂时导致算法运行缓慢. 39 | 40 | ## 3. 导航网络(Navigation Meshes) 41 | 与考虑障碍物的可视图不同, 导航网络考虑的是可行走区域. 对于一个地图, 我们把它看做由若干个多边形相接组成的可行走区域. 如下图所示: 42 | 43 | ![bypass obstacles](/assets/images/pathfinding-gen-graph_6.png) 44 | 45 | 同样地, 绕过障碍物时, 最短的方式一定是贴着障碍物的边缘走. 因此在导航网络里, 我们同样选择各个多边形的顶点作为导航点. 当然, 起始点和终止点也同样是导航点, 我们需要把起始点或终止点和其所在的多边形的各个顶点连接起来, 除此之外, 不需要增加其他的边. 如图所示: 46 | 47 | ![bypass obstacles](/assets/images/pathfinding-gen-graph_7.png) 48 | 49 | 这样我们就把地图抽象成了数学上的图. 可以看到, 导航网络生成的图的边比可视图少了很多, 把这张图应用 A* 算法试试: 50 | 51 | ![bypass obstacles](/assets/images/pathfinding-gen-graph_8.png) 52 | 53 | 呃, 似乎不太对, 最短路径可不应该是这样的! 不用担心, 我们可以用一个很简单的操作优化它. 对于路径中的第 i 个顶点, 如果它能够看见第 i + 2 个顶点, 就移除第 i + 1 个顶点. 这里的 "看见" 同样是两点连线不与任何障碍物相交. 我们对路径中的每个点都执行这样的操作. 这个操作被称为**路径平滑(Path smoothing)**. 执行完平滑操作后的路径就是这样的: 54 | 55 | ![bypass obstacles](/assets/images/pathfinding-gen-graph_9.png) 56 | 57 | 使用导航网络生成的图的边更少, 起始点和终止点加入图时创建的边也更少, 算法的速度会更快. 然而导航网络也有它的问题: 它是一个 "爬墙怪", 总是寻找沿墙走最短的路径然后进行平滑. 这在某些特殊的情况下不能保证最短路径. 这种情况我们通常可以通过增加导航点, 或者细分多边形来避免. 但是这样一来生成的图的边又会更多, 算法会更慢. 58 | 59 | ## 4. 总结 60 | 本文介绍了两种生成图算法. 把一张地图生成图, 在使用图搜索算法对其进行搜索, 就可以完成寻路. 生成图的算法相对较为复杂, 本文只是讲解其思路, 并未给出实现. 寻路算法实际是一个复杂的算法. 笔者的这两篇文章介绍的算法只能适用于小型地图的寻路, 针对开放性大地图的游戏, 寻路算法还会采用更加高级的策略, 后续的文章中笔者也会作补充. 笔者也建议大家去看参考资料中的教程. 61 | 62 | **参考资料:** 63 | - [Map representations](https://theory.stanford.edu/~amitp/GameProgramming/MapRepresentations.html) 64 | -------------------------------------------------------------------------------- /source/_posts/2019-10-11-pass-fd-over-domain-socket.md: -------------------------------------------------------------------------------- 1 | --- 2 | key: 13 3 | title: 通过 UNIX domain socket 在进程间传递文件描述符 4 | tag: linux 5 | --- 6 | Linux 提供了一系列系统调用使我们能在进程间传递文件描述符. 这里的 "传递文件描述符" 不是简单地传递文件描述符这个32位整数, 而是真正地把这个文件句柄传递给目标进程, 使目标进程可以对文件执行读写操作. 现在假设 进程B 要给 进程A 发送文件描述符, 我们来看具体做法. 7 | 8 | ### 1. UNIX domain socket 9 | 要想传递文件描述符, 首先需要建立进程间通信. 这里我们需要用到 UNIX domain socket. UNIX domain socket 是一种进程间通信的方式, 它和普通的 socket 类似, 不同的是不需要用到 ip 地址, 而是使用一个 socket 文件. 我们要利用它发送控制信息, 从而传递文件描述符. 首先我们让进程A先创建一个 socket: 10 | 11 | ```c 12 | /*code of process A*/ 13 | int socket_fd = socket(AF_UNIX, SOCK_DGRAM, 0); 14 | ``` 15 | 注意 `socket` 函数的参数. `AF_UNIX` 便是指定协议族为 UNIX domain socket. 这里我们使用数据报套接字 `SOCK_DGRAM`, 这与 UDP 类似, 不需要建立链接, 直接通过地址发送. 当然也可以使用 `SOCK_STREAM`, 这与就与 TCP 类似. 这里就不列举了. 16 | 17 | 接下来我们绑定地址: 18 | 19 | ```c 20 | /*code of process A*/ 21 | struct sockaddr_un un; 22 | un.sun_family = AF_UNIX; 23 | unlink("process_a"); 24 | strcpy(un.sun_path, "process_a"); 25 | 26 | if (bind(socket_fd, (struct sockaddr*)&un, sizeof(un)) < 0) { 27 | printf("bind failed\n"); 28 | return 1; 29 | } 30 | ``` 31 | 32 | 注意这里的地址就不再是一个 ip 地址了, 而是一个文件路径. 在这里我们指定地址为 `process_a`, 然后调用 `bind` 绑定地址, 这时就会创建一个名为 `process_a` 的 socket 文件. 33 | 34 | 这样一来, 其他的进程就可以通过 `process_a` 这样一个特殊的地址跟这个进程发送消息了. 就跟发送 UDP 消息一样, 不同的是使用 `struct sockaddr_un` 来定义地址, 而不是 `struct sockaddr_in` 或 `struct sockaddr_in6`. 除此之外, 重要的是, 其他进程还可以通过发送控制信息向这个进程传递文件描述符. 35 | 36 | ### 2. sendmsg 37 | Linux 提供了一对系统调用: `sendmsg` 和 `recvmsg`. 与我们平时用的 `send` 和 `recv` 不同, 它们除了可以发送或接收常规数据之外, 还可以用来发送或接收控制信息, 这是传递文件描述符的关键; 此外它们还可以用来发送或接收一段不连续的数据. 38 | 39 | 我们来看这两个系统调用的声明 40 | 41 | ```c 42 | #include 43 | #include 44 | 45 | ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags); 46 | ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags); 47 | ``` 48 | 49 | `sendmsg` 和 `recvmsg` 使用一个结构体 `struct msghdr` 来描述发送的数据. 结构体定义如下: 50 | 51 | ```c 52 | struct iovec { /* Scatter/gather array items */ 53 | void *iov_base; /* Starting address */ 54 | size_t iov_len; /* Number of bytes to transfer */ 55 | }; 56 | 57 | struct msghdr { 58 | void *msg_name; /* optional address */ 59 | socklen_t msg_namelen; /* size of address */ 60 | struct iovec *msg_iov; /* scatter/gather array */ 61 | size_t msg_iovlen; /* # elements in msg_iov */ 62 | void *msg_control; /* ancillary data, see below */ 63 | size_t msg_controllen; /* ancillary data buffer len */ 64 | int msg_flags; /* flags on received message */ 65 | }; 66 | ``` 67 | 68 | - `msg_name` 目标地址. 这个是可选的, 如果协议是面向连接的, 就不需要指定地址; 否则就需要指定地址. 这就类似于 `send()` 和 `sendto()`. 69 | - `msg_iov` 要发送的数据. 这是一个数组, 数组的长度由 `msg_iovlen` 指定, 数组的元素是一个 `struct iovec` 结构体, 这个结构体指定一段连续数据的起始地址(`iov_base`)和长度(`iov_len`). 也就是说, 它可以发送多段连续数据; 或者说, 可以发送一段不连续的数据. 70 | - `msg_control` 控制信息. 这就是我们今天的主角. 我们不能直接设置它, 必须使用一系列的宏来设置它. 71 | 72 | `msg_control` 指向一个由 `struct cmsghdr` 结构体及其附加数据构成的序列. `struct cmsghdr` 的定义如下: 73 | 74 | ```c 75 | struct cmsghdr { 76 | socklen_t cmsg_len; /* data byte count, including header */ 77 | int cmsg_level; /* originating protocol */ 78 | int cmsg_type; /* protocol-specific type */ 79 | /* followed by 80 | unsigned char cmsg_data[]; */ 81 | }; 82 | ``` 83 | 84 | `struct cmsghdr` 实际上定义的是数据的头部, 后面应该紧跟着一个 `unsigned char` 数组, 存放控制信息的实际数据. 也就是大家常说的 "变长结构体". `msg_control` 便是指向一个由这样的变长结构体构成的序列. 内存结构如下图所示: 85 | 86 | ![control data](/assets/images/pass-fd-over-domain-socket_1.gif) 87 | 88 | 我们需要用到以下几个宏: 89 | 90 | ```c 91 | #include 92 | 93 | struct cmsghdr *CMSG_FIRSTHDR(struct msghdr *msgh); 94 | size_t CMSG_SPACE(size_t length); 95 | size_t CMSG_LEN(size_t length); 96 | unsigned char *CMSG_DATA(struct cmsghdr *cmsg); 97 | ``` 98 | 99 | - `CMSG_FIRSTHDR()` 返回 `msg_control` 指向的序列的第一个元素 100 | - `CMSG_SPACE()` 传入控制信息的实际数据的长度, 返回变长结构体需要占用的空间 101 | - `CMSG_LEN()` 传入控制信息的实际数据的长度, 返回变长结构体的长度 102 | - `CMSG_DATA()` 返回存放控制信息的实际数据的首地址. 103 | 104 | > 需要注意 `CMSG_SPACE()` 和 `CMSG_LEN()` 的区别: 前者包含 padding 的长度, 是实际占用的空间; 后者则不包含 padding 的长度, 用于赋值给 `cmsg_len`. 105 | 106 | 接下来我们来让进程B传递文件描述符给进程A. 首先设置进程A的地址, 也就是 "process_a": 107 | 108 | ```c 109 | /*code of process B*/ 110 | struct sockaddr_un ad; 111 | ad.sun_family = AF_UNIX; 112 | strcpy(ad.sun_path, "process_a"); 113 | ``` 114 | 115 | 我们只需要发送控制信息, 不需要发送常规数据, 所以把常规数据置空: 116 | 117 | ```c 118 | /*code of process B*/ 119 | struct iovec e = {NULL, 0}; 120 | ``` 121 | 122 | 接下来为控制数据分配空间, 因为我们只传递一个文件描述符, 所以长度是 `sizeof(int)`: 123 | 124 | ```c 125 | /*code of process B*/ 126 | char cmsg[CMSG_SPACE(sizeof(int))]; 127 | ``` 128 | 129 | 然后就可以设置 `struct msghdr` 结构体: 130 | 131 | ```c 132 | /*code of process B*/ 133 | struct msghdr m = {(void*)&ad, sizeof(ad), &e, 1, cmsg, sizeof(cmsg), 0}; 134 | ``` 135 | 136 | 接下来获取我们获取 `struct cmsghdr` 并设置它: 137 | 138 | ```c 139 | /*code of process B*/ 140 | struct cmsghdr *c = CMSG_FIRSTHDR(&m); 141 | c->cmsg_level = SOL_SOCKET; 142 | c->cmsg_type = SCM_RIGHTS; 143 | c->cmsg_len = CMSG_LEN(sizeof(int)); 144 | *(int*)CMSG_DATA(c) = cfd; // set file descriptor 145 | ``` 146 | 147 | 最后发送出去即可: 148 | 149 | ```c 150 | /*code of process B*/ 151 | sendmsg(mfd, &m, 0) 152 | ``` 153 | 154 | ### 3. recvmsg 155 | 现在我们让进程A接收传递过来的文件描述符. 这里我们调用 `recvmsg`. 156 | 157 | ```c 158 | /*code of process A*/ 159 | char buf[512]; 160 | struct iovec e = {buf, 512}; 161 | char cmsg[CMSG_SPACE(sizeof(int))]; 162 | struct msghdr m = {NULL, 0, &e, 1, cmsg, sizeof(cmsg), 0}; 163 | 164 | int n = recvmsg(socket_fd, &m, 0); 165 | printf("Receive: %d\n", n); 166 | 167 | struct cmsghdr *c = CMSG_FIRSTHDR(&m); 168 | int cfd = *(int*)CMSG_DATA(c); // receive file descriptor 169 | ``` 170 | 171 | 这样, 进程A 就收到了进程B传递过来的文件描述符, 并且进程B打开着这个文件, 可以对其执行读写操作. 172 | 173 | *** 174 | **参考资料**: 175 | - [cmsg(3) - Linux man page](https://linux.die.net/man/3/cmsg) 176 | - [Ancillary Data](http://www.masterraghu.com/subjects/np/introduction/unix_network_programming_v1.3/ch14lev1sec6.html) 177 | -------------------------------------------------------------------------------- /source/_posts/2019-11-04-fault-tolerant-and-assert.md: -------------------------------------------------------------------------------- 1 | --- 2 | key: 15 3 | title: 关于容错和断言的一些思考 4 | tag: experience 5 | --- 6 | 实际项目中的代码总是多多少少会有一些问题的. 面对一些问题, 我们有两种做法: 一种是容错, 把错误自行消化掉, 让代码能够继续往下运行; 另一种是加断言, 让错误发生时抛出异常, 把错误暴露出来, 并且能中断当前过程, 避免后续行为未定义. 这两种策略中, 笔者更偏爱后者. 因为无脑的容错最后会导致问题的根源不被解决, 使问题堆积, 导致代码腐败. 毕竟解决问题的第一步是面对问题, 而不是回避它. 7 | 8 | 然而事情并没有这么简单. 无论是容错, 还是加断言, 都是要根据不同的场景进行思考. 无脑地容错和无脑地加断言都是不可取的. 我们来看两个例子: 9 | 10 | #### 1. 排行榜结算 11 | 游戏服务器中一个常见的需求就是结算排行榜, 给排行榜中的玩家发奖. 这样的代码通常是这样的: 12 | 13 | ```lua 14 | function settlement(ranking_list) 15 | for ranking, player_id in ipairs(ranking_list) do 16 | award(player_id, ranking) 17 | end 18 | clean_ranking_list() 19 | end 20 | 21 | function award(player_id, ranking) 22 | local reward = config.ranking_reward[ranking] 23 | assert(reward ~= nil, "No configuration of ranking " .. ranking) 24 | send_mail_with_reward(player_id, reward) 25 | end 26 | ``` 27 | 28 | 在 `award` 函数中, 需要读取配置, 获取各个名次所对应的奖励. 然而有可能策划粗心大意没有配置某一名的奖励, 导致发奖无法进行. 根据 "不要隐藏问题" 的原则, 我在第二行加上了断言, 把策划的错误暴露了出来. 这个思路本身没错的, 但是在这里是不正确的. 因为这个异常会中断 `settlement` 函数, 导致之后的玩家都无法收到奖励. 也就是说, 你**把问题扩大化了**. 在这个情况下, 我们应该在 `settlement` 函数中捕获 `award` 函数的异常, 并打印错误日志, 使其不影响接下来的代码. 然而很多情况下, 大家都不会把函数包在 `try cache` 语句中, 特别是 Lua 这种连 `try cache` 语句都没有的语言. 因此, 作为一个公共函数, 应该自己处理异常, 不让上层调用者操心. 这里更好的做法是 `award` 函数发现没有配置, 打印错误日志, 取消发奖. 29 | 30 | ```lua 31 | function award(player_id, ranking) 32 | local reward = config.ranking_reward[ranking] 33 | if not reward then 34 | error_log("No configuration of ranking " .. ranking) 35 | return false 36 | end 37 | send_mail_with_reward(player_id, reward) 38 | return true 39 | end 40 | ``` 41 | 42 | > 这里我不得不说 Java 在这一点上做得非常好: 如果一个声明了不会抛出异常的函数调用了一个可能抛出异常的函数却没有捕获其异常的话, 编译会报错. 这就直接解决了这一问题, 公共函数可以放心地抛出异常, 基本不用担心会把问题扩大化. 43 | 44 | #### 2. 自增函数 45 | 我们通常会对数据库作一层封装, 避免直接写 SQL 语句. 假设我们把 `player` 表封装了一个 `Player` 类, 这个类里面有一个 `Player:add(field, num)` 方法, 给 `field` 字段自增 `num`. 现在 `player` 类中有两个字段, `level` 和 `exp` 分别表示玩家的的等级和当前经验. 再假设 `config.upgrade[i]` 表示从 `i - 1` 级升到 `i` 级所需要的经验. 需求是玩家会在某些时刻获取经验, 经验达到升级条件自动升级. 很典型很常见的需求对不对? 那么获取经验升级的代码通常是这样的: 46 | 47 | ```lua 48 | function get_experiences(player, num) 49 | player:add("exp", num) 50 | while player.level < MAX_LEVEL and player.exp >= config.upgrade[player.level + 1] do 51 | player:add("exp", -config.upgrade[player.level + 1]) 52 | player:add("level", 1) 53 | end 54 | end 55 | 56 | function Player:add(field, num) 57 | self[field] = self[field] + num 58 | DB.run("update player set " .. field .. " = " .. self[field]) 59 | end 60 | ``` 61 | 62 | 这样看上去没什么问题对不对? 然而等你提交完代码, 测试完毕, 产品上线了, 随后发现生产环境有时候数据库连接不稳定, 导致 `DB.run` 运行报错. 为了解决这个报错, **不要让问题扩大化**, 团队中有一位程序员就加上了这样一行代码: 63 | 64 | ```lua 65 | function Player:add(field, num) 66 | if not DB.connected then return end 67 | self[field] = self[field] + num 68 | DB.run("update player set " .. field .. " = " .. self[field]) 69 | end 70 | ``` 71 | 72 | 这就直接导致了灾难性的后果: 一旦运行到 `add` 函数时 `DB.connected` 为假, 直接导致程序死循环. 我想没有比这更严重的问题了, 这便是无脑容错的代价. 你容错可以, 但至少要让上层调用者知道, 比如说连接断开返回 `false`, 成功自增返回 `true`. 也就是说, **容错绝对不是隐瞒问题**, 一定要用某种方式将错误暴露出来. 但我认为这里应该用一种更强的方式把问题暴露出来--那就是抛出异常. 73 | 74 | 所以我总结出两条原则: 75 | - **不能隐瞒问题, 一定要把问题暴露出来;** 76 | - **抛出异常是最强的暴露问题的方式, 一定要考虑抛出的异常会不会扩大问题(特别是对某些语言而言).** 77 | 78 | 总而言之, 既不能无脑容错, 也不能无脑断言. 一定要根据不同的情况采取不同的策略, 特别是对某些语言而言. 顺便多说一句, 与 Java, C# 相比, Lua, Python 这样的脚本语言反而对程序员要求更高, 特别是在大项目中. 它们灵活, 开发效率高, 但是没有完善的流程和代码规范, 也很容易出问题. 79 | -------------------------------------------------------------------------------- /source/_posts/2019-11-07-use-coroutines-to-process-time-consuming-procedures.md: -------------------------------------------------------------------------------- 1 | --- 2 | key: 16 3 | title: 使用协程处理耗时过程 4 | tag: lua 5 | --- 6 | 游戏服务器常常有一些耗时的操作, 比如说给全服玩家发放奖励. 如果直接写一个循环, 遍历全服玩家, 给每个玩家发放奖励, 那么整个过程可能持续几分钟, 十几分钟甚至几十分钟, 整个进程都阻塞在这个过程中了. 解决这个问题的一种做法是使用定时器, 比如说每处理完 50 个玩家, 就停 1 秒, 1 秒后继续处理, 就像这样: 7 | 8 | ```lua 9 | function deal(list) 10 | local p = 0 11 | local function foo() 12 | local from, to = p + 1, math.min(p + 50, #list) 13 | for i = from, to do 14 | local id = list[i] 15 | dosth(id) 16 | end 17 | p = to 18 | if p < #list then 19 | timer:start_once(1, foo) 20 | end 21 | end 22 | foo() 23 | end 24 | ``` 25 | 26 | 但是这种做法太过麻烦. 特别是, 有的时候这个耗时过程是二重循环或者别的什么奇怪的控制流, 那就无法使用这种方法了. 更好的做法是使用**协程(coroutine)**. 27 | 28 | 对于一个常规的过程, 一旦返回, 就丢失了全部栈里的信息, 下次调用时就得重新来过. 然而协程不同, 它允许过程在某些时刻切出, 进入挂起状态, 却又保存其全部的栈信息; 然后可以在将来的某些时刻将其唤醒. 唤醒之后的协程会在上次切出的地方继续执行, 就像它从来没有切出过一样. 除此之外, 还能在切入切出的时候传递参数. 举个简单的例子: 29 | 30 | ![result](/assets/images/use-coroutines-to-process-time-consuming-procedures_1.png) 31 | 32 | 我们可以用协程处理耗时过程. 具体的思路就是把耗时过程包在一个协程里, 每执行一定的量就调用 `coroutine.yield` 切出协程, 然后利用定时器延时一段时间再唤醒协程, 直到协程执行完毕. 我们可以简单封装一下, 让使用者不感知协程的存在. 以下是个简单示例: 33 | 34 | ```lua 35 | counts = {} 36 | max_counts = {} 37 | function try_yield() 38 | local co = coroutine.running() 39 | assert(co ~= nil) 40 | counts[co] = counts[co] + 1 41 | if counts[co] >= max_counts[co] then 42 | counts[co] = 0 43 | coroutine.yield() 44 | end 45 | end 46 | 47 | function with_coroutine(f, n, t) 48 | return function(...) 49 | local co = coroutine.create(f) 50 | counts[co] = 0 51 | max_counts[co] = n 52 | 53 | local function foo(...) 54 | coroutine.resume(co, ...) 55 | if coroutine.status(co) == 'dead' then 56 | counts[co] = nil 57 | max_counts[co] = nil 58 | return 59 | end 60 | timer:start_once(t, foo) 61 | end 62 | foo(...) 63 | end 64 | end 65 | ``` 66 | 67 | 这样的话使用起来就很简单了, 现在你就可以真的 "直接写一个循环, 遍历全服玩家, 给每个玩家发放奖励" 了, 只要记得调用 `try_yield`: 68 | 69 | ```lua 70 | deal = with_coroutine(function(list) 71 | for i, id in ipairs(list) do 72 | dosth(id) 73 | try_yield() 74 | end 75 | end, 50, 1) 76 | ``` 77 | 78 | 你还可以选择使用[这篇文章](/2019/09/15/lua-decorator.html)中介绍的装饰器, 让代码更加优雅. 79 | 80 | 处理耗时过程应该是协程的一个比较常规的操作. 其实对于处理耗时过程, 很多人第一想到的是开辟一条线程去处理. 然而线程会有并发的问题, 况且线程数太多会给CPU带来额外的负担. 这里我们可以利用 Lua 的优势, 使用协程解决这个问题. 81 | -------------------------------------------------------------------------------- /source/_posts/2019-11-27-passing-passwords-securely.md: -------------------------------------------------------------------------------- 1 | --- 2 | key: 18 3 | title: 安全地传递密码 4 | tag: experience 5 | --- 6 | 前端要想安全地将密码传递给后端, 应该按照以下几步操作: 7 | 8 | **前端** 9 | 1. 将明文密码作 md5 哈希; 10 | 2. 将哈希过的密码拼上一段随即生成的, 固定长度的盐; 11 | 3. 将上一步的结果使用后端的公钥加密并传递给后端. 12 | 13 | **后端** 14 | 1. 将前端传递过来的密码用私钥解密; 15 | 2. 去盐. 因为盐是固定长度的, 所以直接裁剪即可; 16 | 3. 再次随机生成一段盐; 17 | 4. 将第2步去盐后的密码拼上第3步生成的盐, 并作 md5 哈希; 18 | 5. 将第4步的结果和第3步生成的盐存入数据库. 19 | 20 | 验证密码的时候, 后端只需将第2步去盐后的密码拼上从数据库中取得的盐, 作 md5 哈希后与数据库中的密码相比较即可. 21 | 22 | ### 这么做的原因 23 | 24 | 1. **为什么前端要将明文密码作 md5 哈希并拼上盐再加密?** 25 | 26 | 因为公钥是公开的, 而明文秘密是简短并有一定意义的. 如果直接拿明文密码用公钥加密, 黑客就可以截获这个数据包, 然后不断尝试密码, 用公钥加密并和截获的密码相比较, 从而破解密码. 如果密码安全系数低(如生日), 黑客就可以很快破解. 将明文密码 md5 哈希后拼上盐, 密码就变成无意义的了, 破解难度大大提升. 27 | 28 | 2. **为什么后端要将 md5 后的密码拼上盐再作二次 md5 哈希?** 29 | 30 | 因为前端 md5 时没有拼上盐. 如果直接存储这个密码, 一旦数据库被泄露, 黑客就可以通过不断尝试密码, md5 哈希并与数据库中的密码相比较, 从而破解密码. 所以我们再次拼上盐并作二次哈希, 破解难度大大提升. 31 | -------------------------------------------------------------------------------- /source/_posts/2019-12-21-beginners-guide.md: -------------------------------------------------------------------------------- 1 | --- 2 | key: 20 3 | title: 如何优雅地实现一个新手引导系统 4 | tag: design 5 | --- 6 | 笔者最近要在项目中实现一个新手引导系统. 新手引导其实上是一个比较复杂的系统, 与许多具体的功能紧密相关, 其中涉及到的特殊处理也比较多. 这篇文章我想谈谈新手引导的设计思路, 尽量不涉及具体的引擎框架和实现. 7 | 8 | ### 事件驱动 9 | 10 | 整个新手引导的流程应是事件驱动的. 比如说当宠物功能开启时在宠物功能按钮上显示引导提示, 当点击 A 按钮时把引导提示移动到 B 按钮上. 对于特殊的事件, 我们可以在必要的地方单独处理; 但是对于一些非常通用的事件, 比如说 点击按钮, 打开一个界面, 切换场景等, 我们就应该充分利用引擎和框架, 提供通用的事件, 而不是为每个按钮, 每个界面单独作处理. 抛出事件应该带上必要的数据. 这里举几个笔者项目中的例子: 11 | 12 | - 对于按钮点击事件, 根据 UI 组织结构, 给每个按钮定义一个唯一的名字. 这有点像 jQuery 选择器: 比如说宠物界面右边面板上的激活按钮, 它的名字就是 `dialog_pet.right_panel.btn_activate`. 然后, 在点击每一个按钮时都抛出一个 `click_button` 事件, 并带上按钮的名字. 13 | - 对于打开界面的事件, 由于笔者的项目中每个界面都有唯一的名字, 所有只需在打开界面时抛出 `pop_dialog` 事件并带上界面的名字即可. 14 | - 对于一些特殊的事件, 就在必要的地方单独作处理: 比如说通关某一关卡, 就在结算时抛出 `mission_finished` 事件, 并且带上通关关卡 ID, 通关成绩等数据. 15 | 16 | ### 不要跟具体的功能相耦合 17 | 18 | 新手引导需要引导玩家点击各种按钮, 这与许多具体的功能紧密相关. 那么很重要的一点就是不要把新手引导跟它们耦合起来. 上面提到的**事件驱动**也是为了避免耦合. 19 | 20 | 新手引导中用的最多的表现就是在某个按钮上显示引导提示, 比如说手, 箭头等. 最糟糕的做法是修改具体的界面, 把引导提示摆在适当的位置, 并控制其显隐. 这样改动的东西太多, 不便于维护. 比较好的做法把新手引导相关的代码提取出来, 做一个引导管理器; 然后再做一个类似于 jQuery 选择器的东西, 可以通过名字选择一个具体的 UI 组件, 然后由引导管理器负责把引导提示动态地加载到这个 UI 组件上. 21 | 22 | 这里我的做法跟上面提到的按钮点击事件类似: 给每个 UI 组件定义一个唯一的名字, 通过这个名字查找到对应的 UI, 然后在这个位置加载引导提示. 同时保证引导提示的显隐和层级跟这个 UI 组件一致. 引导提示的完全由引导管理器控制, 不用修改具体功能的代码. 23 | 24 | ### 使用状态机抽象引导逻辑 25 | 26 | 引导逻辑实际上是最头疼的. 举个例子, 一个完整的引导流程通常是这样的: 27 | 28 | 1. 点击右侧按钮 -> 展开功能面板 29 | 2. 点击宠物按钮 -> 打开宠物界面 30 | 3. 点击选择第一个宠物 -> 第一个宠物被选中 31 | 4. 点击激活按钮 -> 宠物被激活 32 | 5. 引导结束 33 | 34 | 在这个流程中, 每当某一步完成, 就应该推进到下一步: 比如说第三步中点击选中了第一个宠物, 引导就推进到第四步. 但是现实总是事与愿违, 玩家有可能不按照引导进行操作: 玩家有可能在第三步点击关闭按钮退出了宠物界面, 或者点击选择第二个宠物, 又或者点击了宠物预览按钮. 另外在第一步的时候, 有可能在引导第一步开始时功能面板已经展开, 这个时候就不应该引导玩家点击右侧按钮, 而应该直接引导玩家点击宠物按钮. 这里就存在很多特殊处理. 如何描述这些特殊的逻辑使其一般化呢? 答案是使用**有穷状态机**. 35 | 36 | 使用有穷状态机可以完美地抽象引导逻辑. 引导流程不是线性的, 而是一个有向图. 对于上面的例子, 用状态机描述就是这样的: 37 | 38 | ![fsm](/assets/images/beginners-guide_1.png) 39 | 40 | 每一个状态都是引导的的一个步骤, 箭头上的都是事件. 41 | 42 | 上文中我们实现的事件都是比较抽象并且带数据的, 而状态中状态转移的事件都是非常具体的. i.e. 打开界面的事件为 `pop_dialog` 附带数据为界面的 ID, 而状态机中事件都是 "宠物界面打开" 这样具体的, 不带数据的事件. 因此我做了一个事件分发器, 把抽象的, 带数据的事件分发成具体的, 不带数据的事件, 通过对其携带的数据作判断. 例如: 43 | 44 | ```lua 45 | pop_dialog = { 46 | pop_pet_dialog = function(id) 47 | return id == "dialog_pet" 48 | end, 49 | ... 50 | } 51 | ... 52 | ``` 53 | 54 | 每触发一个事件都会经过事件分发器, 转换成具体的子事件. 这样 `pop_dialog` 事件就被转换成 `pop_pet_dialog` 事件. 55 | 56 | ### Put Them Together 57 | 58 | 做个总结就是: 59 | 60 | - 使用一个引导管理类管理引导相关的逻辑, 包括监听事件, 事件分发, 用事件控制引导的开启与关闭, 管理各个引导的状态机等; 61 | - 使用状态机抽象引导逻辑, 在状态机的各个状态中实现具体的引导表现; 62 | - 为每个 UI 组件定义一个唯一的名字, 使用这样的名字来抛出事件, 寻找组件; 63 | 64 | 以上便是我实现新手引导系统的的思路. 如果你有其他的想法, 欢迎与我讨论. 65 | -------------------------------------------------------------------------------- /source/_posts/2020-01-01-2019-annual-summary.md: -------------------------------------------------------------------------------- 1 | --- 2 | key: 21 3 | title: 2019 Annual Summary 4 | tag: [essays, english] 5 | banner_img: /assets/images/2019-annual-summary_4.jpg 6 | --- 7 | On September 15, 2017, the first day I joined the company, I told myself that I joined a good company which has a good treatment but I was not feeling happy at all. I had plenty of worry and confusion at that moment because I didn't know my direction. 8 | 9 | All I can do is working hard and learning as much as possible. 10 | 11 | At the beginning of 2019, I told myself that 2019 is crucial for me. I must do something that will allow me to seize the opportunity that gives me a clear direction. 12 | 13 | Time flies and many events happened in 2019. Maybe it didn't meet my expectations, but fortunately I didn't feel regret. 14 | 15 | Today is January 1, 2020, it's time to make a summary. 16 | 17 | ## Annual Targets 18 | 19 | ### Unfinished Targets 20 | 21 | - Read Lua source code 22 | 23 | I didn't finish reading the code, I only read part of it. But it's a start, I have started to understand some of these principles. 24 | 25 | - Finish [OpenGL tutorial](http://www.opengl-tutorial.org/) 26 | 27 | I didn't have time to learn it since I didn't finish reading Lua source code. 28 | 29 | - Read 10 books 30 | 31 | Only 6. 32 | 33 | ### Finished Targets 34 | 35 | - Memorize words every day 36 | 37 | I did it. 38 | 39 | - Finish reading *CLRS*, and then read *TCP/IP Illustrated* 40 | 41 | I did it. But the translation of *TCP/IP Illustrated* is too bad, So I read *Discrete Mathematics and Its Applications* instead of it. 42 | 43 | ## English 44 | 45 | I kept remembering words every day and completed daily plan every day except June 22 due to too busy that day to finish the plan before 00:00. 46 | 47 | ![baicizhan](/assets/images/2019-annual-summary_1.jpg){width="400"} 48 | 49 | I have been remembering words for 605 days. I'm not good at language learning and I used to hate English. Nevertheless, for programmers, English is essential and it's a basic skill. I have overcome many difficulties and now I can read English articles more easily. In the future I hope I can hold on and improve writing and speaking skills. 50 | 51 | ## Technology 52 | 53 | - I finished reading *CLRS* except the last two chapter due to I didn't have enough mathematical knowledge to understand it. 54 | - I read about 1/3 of *Discrete Mathematics and Its Applications*. 55 | - I have been doing algorithmic exercises on [LeetCode](https://leetcode-cn.com/) since June. It's difficult for me, but this is the beginning. 56 | 57 | ![leetcode](/assets/images/2019-annual-summary_2.png) 58 | 59 | - I built [my tech blog](https://luyuhuang.github.io) and wrote 17 posts. 60 | - I have been contributing on GitHub since September, it taught me a lot. 61 | 62 | ![github](/assets/images/2019-annual-summary_3.png) 63 | 64 | I hope it'll be full of green spots in 2020. 65 | 66 | - I read part of Lua source code following [this guide](https://github.com/lichuang/Lua-Source-Internal). I have understood some of these principles. 67 | 68 | ## Reading 69 | 70 | I read 6 books in 2019. 71 | 72 | - *The Tale of Genji* 73 | - *A Tale of Two Cities* 74 | - *Notre-Dame de Paris* 75 | - *No Longer Human* 76 | - *The Romance of the Three Kingdoms* 77 | - *Journey Under the Midnight Sun* 78 | 79 | The two books that impressed me most were *Notre-Dame de Paris* and *Journey Under the Midnight Sun*. 80 | 81 | ## Something Happy 82 | 83 | On November 9 I attended the *LisAni! LIVE Beijing* concert and met Nitta Emi. This is my happiest day in years. 84 | 85 | ![concert](/assets/images/2019-annual-summary_4.jpg) 86 | 87 | ## Finally 88 | 89 | The most comforting is that I still love programming despite I graduated from college over 2 years. I feel so happy: what I work every day is what I love. I hope to keep it like this in the future. 90 | 91 | I hope to make some changes in the future, I hope I won't feel confused in the future. 92 | 93 | Hello 2020. It must be a new beginning. 94 | -------------------------------------------------------------------------------- /source/_posts/2020-01-01-dwords.md: -------------------------------------------------------------------------------- 1 | --- 2 | key: 22 3 | title: 通过弹幕背单词 4 | tag: tools 5 | --- 6 | 众所周知, 对于程序员来说, 最重要的语言不是 C, 不是 C++, 不是 Java, 而是英语. 十分不幸的是, 鄙人极其不擅长英语, 尤其是背单词: 背完即忘. 我需要的是高频重复. 所以我决定采取点措施, 然后就做了这个: [DWords](https://github.com/luyuhuang/DWords). 它可以单词变成屏幕上的弹幕, 用起来就像这样: 7 | 8 | ![screenshot](/assets/images/dwords_1.png) 9 | 10 | 这样在工作写代码的时候就会时不时地飘过来一个弹幕, 不断地强化重复记忆. 我认为这个工具非常适合程序员. 11 | 12 | 除此之外, 它还支持在多个客户端之间同步单词. 为了做到这一点, 我让用户设置一个电子邮箱, 然后通过发送和接受电子邮件来同步数据. 这么做的原因是我不想维护服务器, 也没有必要, 这只是一个小工具. 使用电子邮件的一个好处是可以通过手机添加单词了 -- 所有的智能手机都有邮件客户端, 发送电子邮件即可. 13 | 14 | DWords 使用 Python + PyQt5 开发, 支持跨平台(Windows, Mac OS, Linux). 开发的过程中也是踩了不少坑, 以后(有时间的话)也许会写一篇文章总结一下. 15 | 16 | 我给这个工具的定位是一个难词本, 适用于强化记忆那些老记不住的单词, 并不是记单词的主力工具, 所以也没有词库或是学习计划等一些高级功能; 单词本身也依靠手动录入(录入单词本身也是一种记忆嘛). 我认为背单词还是需要花集中一定的时间去读去背. 17 | 18 | 详细的安装, 使用方式见 [项目主页](https://github.com/luyuhuang/DWords). 如果你有任何问题, 或者有任何建议, 欢迎提 [issue](https://github.com/luyuhuang/DWords/issues). 如果你会 Python, 也欢迎提 pull request. 19 | -------------------------------------------------------------------------------- /source/_posts/2020-03-18-serialize-lua-objects.md: -------------------------------------------------------------------------------- 1 | --- 2 | key: 27 3 | title: 序列化 Lua 对象 4 | tag: lua 5 | --- 6 | 在项目中由于种种需求经常需要将对象序列化成一个字符串. 由于 Lua 唯一的复合结构是 table, 所以实现起来还是比较简单的. 之前我们的做法是用 Lua 写一个递归函数遍历 table 的键值然后转换成字符串并拼接起来. 然而 Lua 在字符串拼接的过程中会不断地构造字符串对象, 因此这样的实现方式性能较差, 并且会浪费内存, 特别是数据比较大的时候. 一种优化方式是将键值转换的小字符串存到一个 table 里, 最后使用 `table.concat` 拼接. 不过我想跟进一步, 使用 C 实现它. 7 | 8 | C 实现的序列化函数与 Lua 实现的类似, 都是遍历 table, 遇到简单类型就直接转换成字符串, 遇到 table 就递归. 这里我们就可以把键值转换的小字符串存到一个缓冲区里, 然后将缓冲区转换成字符串 push 进栈返回即可. 这比使用 `..` 和 `table.concat` 要快不少. 一开始我直接使用 `luaL_Buffer` 作缓冲区, 后来发现这玩意儿有坑, 它在扩容的时候往栈里 push 东西, 导致数据不正确. 为此我就自己写一个 buffer 代替 `luaL_Buffer`. 这里我借鉴了 `luaL_Buffer` 的设计: 当数据比较小时, 数据直接存在栈里, 不需要向操作系统申请内存; 只有当数据长度超过某个值时才申请内存. 由于项目中大部分序列化操作的数据都比较小, 这个做法可以带来不少优化. 此外我实现的 buffer 实际上是个链表, 扩容的时候只需要追加链表节点即可, 不需要复制数据. 在最后, 只有扩容过的 buffer 才需要将数据复制出来, 否则只需要将头节点数据的指针取出即可. 9 | 10 | 有时我们对序列化对象的结果没有可读性需求, 这个时候序列化成二进制数据会比较快, 序列化的结果也比较小. 这里我参照了云风大神的项目 [cloudwu/lua-serialize](https://github.com/cloudwu/lua-serialize). 由于我们的项目没有序列化成 userdata 的需求, 这里我就简化, 并且使用我自己写的 buffer 代替他的 `struct write_block`. 反序列化时由于不用考虑链表的问题, 也可以简化. 此外我们还需要将序列化的数据在网络中传递, 这就需要考虑整数字节序的问题了. 因此我还加上了字节序转换. 11 | 12 | 最后粗略测试了下性能, 运行效率大概是 Lua 实现的四倍, 二进制序列化又比文本序列化快一倍以上. 感觉还有优化空间. 完整代码见 [luyuhuang/cseri](https://github.com/luyuhuang/cseri). 13 | -------------------------------------------------------------------------------- /source/_posts/2020-04-06-vscode-rss.md: -------------------------------------------------------------------------------- 1 | --- 2 | key: 29 3 | title: 给 VSCode 做了个 RSS 阅读器插件 4 | tag: tools 5 | --- 6 | 一直比较喜欢使用 RSS 订阅一些新闻和技术博客, 但总觉得 Windows 上除了雷鸟外没有什么好用的阅读器. 后来突然想到既然平时 VSCode 用得这么多, 为什么不给它写个 RSS 阅读插件呢, 而且 VSCode 扩展性这么强, 又天生支持 HTML 渲染. 于是清明节这几天就搞出了这个: [luyuhuang/vscode-rss](https://github.com/luyuhuang/vscode-rss). 在 VSCode 扩展商店中搜 "RSS" 就能找到它. 它用起来就像这样: 7 | 8 | ![demonstrate](/assets/images/vscode-rss_1.gif) 9 | 10 | 嗯, 这样以后写代码写累了就可以~~摸鱼~~看看 RSS 订阅了. 11 | 12 | 目前已经实现了一个 RSS 阅读器所需的基本功能了, 自动刷新, 已读未读标记, 识别相对 URL 等都是有的. 它的配置非常简单, 直接在 `settings.json` 中加一个 RSS 源列表即可: 13 | 14 | ```json 15 | { 16 | "rss.feeds": [ 17 | "https://realpython.com/atom.xml", 18 | "https://luyuhuang.github.io/feed.xml", 19 | "https://www.ruanyifeng.com/blog/atom.xml" 20 | ] 21 | } 22 | ``` 23 | 24 | BTW, VSCode 的扩展十分强大, 有了它你就可以把一切你觉得不爽的地方都变爽. 可以说 VSCode 是一个可高度定制的工具, 这一点对于程序员来说十分重要. 我认为这也是 Vim 之所以仍有什么多人喜欢的原因. 25 | -------------------------------------------------------------------------------- /source/_posts/2020-05-08-sync-time-zone.md: -------------------------------------------------------------------------------- 1 | --- 2 | key: 31 3 | title: Synchronize time and time zone between client and server 4 | tag: [lua, experience, english] 5 | --- 6 | The time in online games is generally based on the server time, which include the time used by the client for calculation and display, since the time in the client may be incorrect. In addition, my game project'll be released globally, so it's important to synchronize time zone between client and server. I have to deal with the time zone problems manually since the game is written in Lua and there are no related functions in Lua. 7 | 8 | ### Synchronize time 9 | 10 | Since the function `os.time()` always returns a UTC timestamp, as long as your clock is accurate, the function `os.time()` always returns a same value at the same moment regardless of the time zone. So, when the client calls `os.time()`, it only needs to consider the accuracy of the clock, not the time zone. 11 | 12 | The server returns the server's timestamp `server_timestamp` to the client when the client logs in, and client calculates the difference between server's time and client's time `time_diff = os.time() - server_timestamp`. Next, the server's timestamp will be periodically sent by the server to the client and the client will calibrate the time difference continuously. 13 | 14 | If the client wants to get current accurate timestamp, it should execute `os.time() - time_diff`. Since the client should always use server time, we rewrite `os.time()` as: 15 | 16 | ```lua 17 | local os_time = os.time 18 | local time_diff = 0 19 | 20 | function calibrate_time(server_timestamp) 21 | time_diff = os_time() - server_timestamp 22 | end 23 | 24 | function os.time() 25 | return os_time() - time_diff 26 | end 27 | ``` 28 | 29 | ### Synchronize time zone 30 | 31 | Synchronization of the time zone is more complicated compared with synchronization of time. If the client uses timestamps only, we don't need to care about time zone, since the timestamp is independent of time zone. However, a readable time, i.e. consists of year, month, day, hour, minute, second, is time zone related. If we discuss about converting a timestamp to a readable time or converting a readable time to a timestamp, the premise is that in a certain time zone. 32 | 33 | In Lua, call `os.time` and pass in a argument describing the readable time to convert the readable time to a timestamp; call `os.date` and pass the format string and timestamp to convert the timestamp to a readable time. However, Lua will use the local time zone(i.e. the machine time zone) for these conversions, which is not what we want. Therefore, we must calculate the time zone conversion. 34 | 35 | To converting a specific readable time such as `yyyy-MM-dd HH:mm:ss` to a timestamp, first, we can calculate how many days have passed since January 1, 1970, according to the leap year rule; then count the number of seconds, and get a "timestamp". However, it is incorrect, because the readable time `yyyy-MM-dd HH:mm:ss` includes time zone. The correct way is to subtract the time zone after counting the number of seconds to eliminate the time zone effect. 36 | 37 | Therefore, to converting time `yyyy-MM-dd HH:mm:ss` to a timestamp in server time zone, we should: 38 | 39 | ```lua 40 | count_number_of_seconds_since_1970(yyyy-MM-dd HH:mm:ss) - SERVER_TIMEZONE 41 | ``` 42 | 43 | However, when we call `os.time` and pass the time, the result is: 44 | 45 | ```lua 46 | os.time(yyyy-MM-dd HH:mm:ss) = count_number_of_seconds_since_1970(yyyy-MM-dd HH:mm:ss) - CLIENT_TIMEZONE 47 | ``` 48 | 49 | So, to get the timestamp in server time zone, we should: 50 | 51 | ```lua 52 | os.time(yyyy-MM-dd HH:mm:ss) + CLIENT_TIMEZONE - SERVER_TIMEZONE 53 | ``` 54 | 55 | Similarly, to converting a specific timestamp `n` to a readable time, we can calculate the year, month, day and time after `n` seconds have passed since January 1, 1970. This result is incorrect either since it's the conversion in UTC time zone. To get the correct time, we should: 56 | 57 | ```lua 58 | calculate_the_datetime_since_1970(n + SERVER_TIMEZONE) 59 | ``` 60 | 61 | However, when we call `os.date` and pass the format string and `n`, the result is: 62 | 63 | ```lua 64 | os.date("%Y-%m-%d %H:%M:%S", n) = calculate_the_datetime_since_1970(n + CLIENT_TIMEZONE) 65 | ``` 66 | 67 | So, to get the readable time in server time zone, we should: 68 | 69 | ```lua 70 | os.date("%Y-%m-%d %H:%M:%S", n - CLIENT_TIMEZONE + SERVER_TIMEZONE) 71 | ``` 72 | 73 | ### Put Them Together 74 | 75 | After the client logs in, server tells the client its time and timezone. Server will also tell clients its time periodically. We rewrite `os.time` and `os.date`: 76 | 77 | ```lua 78 | local os_time = os.time 79 | local os_date = os.date 80 | local time_diff = 0 81 | local now = os_time() 82 | local CLIENT_TIMEZONE = math.floor(os.difftime(now, os_time(os_date("!*t", now)))) 83 | local SERVER_TIMEZONE = CLIENT_TIMEZONE 84 | 85 | -- call it when the client loged in 86 | function init_time(server_timezone, server_timestamp) 87 | SERVER_TIMEZONE = server_timezone 88 | time_diff = os_time() - server_timestamp 89 | end 90 | 91 | -- call it periodically 92 | function calibrate_time(server_timestamp) 93 | time_diff = os_time() - server_timestamp 94 | end 95 | 96 | function os.time(date) 97 | if date != nil then 98 | return os_time(date) + CLIENT_TIMEZONE - SERVER_TIMEZONE 99 | else 100 | return os_time() - time_diff 101 | end 102 | end 103 | 104 | function os.date(format, time) 105 | if time == nil then 106 | time = os.time() 107 | end 108 | return os_date(format, time - CLIENT_TIMEZONE + SERVER_TIMEZONE) 109 | end 110 | ``` 111 | 112 | *** 113 | 114 | **Updated on October 29, 2020:** 115 | 116 | `local CLIENT_TIMEZONE = math.floor(os.difftime(now, os_time(os_date("!*t", now))))` is not a correct way to calculate the client timezone. see [Lua 夏令时时区问题](/2020/10/29/lua-dst.html). 117 | -------------------------------------------------------------------------------- /source/_posts/2020-07-16-character-encoding.md: -------------------------------------------------------------------------------- 1 | --- 2 | key: 35 3 | title: 搞清楚令人头疼的乱码问题 4 | tag: problems 5 | --- 6 | 相信我们每个人都被乱码的问题困扰过. 乱码常常令人十分头疼, 这主要是因为没有搞清楚字符编码的问题: 何为 encode 何为 decode, UTF-8 和 Unicode 是什么关系, 它与 UTF-16 和 UTF-32 的区别又是什么, BOM 头又是什么东西等等. 这里我们彻底地捋一遍字符编码问题. 首先搞清楚几个概念: 7 | 8 | ### 基本概念 9 | 10 | - **字符** 11 | 12 | 字符指语言中的书写原子, 是不可再分的最小单元, 例如字母 `a` `é`, 表达符号 `;` `。`, 汉字 `阿`, 假名 `あ`, emoji `😂` 等等. 总之是书写的最小单位, 它们都应该看作一个字符. 13 | 14 | - **字符库** 15 | 16 | 人类的字符太多了, 我们一次用不了这么多. 所以我们根据不同的情况, 取全体字符的一个子集, 形成一个字符库. 17 | 18 | - **编码后的字符** 19 | 20 | 人类写字, 在纸上 "画出" 相应的字形即可. 但计算机不可能处理和存储字形图案, 所以人们给每个字符编一个序号(编码), 计算机就只需操作这些序号即可. 例如字母 `a` 在 ASCII 中的编号为 `97`, 汉字 `阿` 在 Unicode 中的编号为 `38463`, 在 GBK 中的编号为 `45218`. 21 | 22 | - **编码后的字符集(coded character set)** 23 | 24 | 为字符库中的每个字符分配一个唯一的编码, 并把编码唯一映射到字符. 也就是说**编码后的字符集**是一个编码到字符的映射. Unicode, GBK, iso-8859-1 等都是编码后的字符集 25 | 26 | - **字符编码方案(character encoding scheme)** 27 | 28 | 把一个字符串的每个字符都编上号了, 怎么存储这些编号呢? 目前 Unicode 编码的最大值达 0x10FFFF, 需要 21 位二进制数; 然而每个字符的编码大小不一, 如果简单地将一个字符分配三字节的话, 未免太浪费空间了. 这个时候我们就需要**字符编码方案**, 将这些字符编码转换成二进制字节流. UTF-8 就是一种字符编码方案, 它采用变长编码, 能够有效节省空间. 29 | 30 | 实际上, 我们平时常说的 "这个文件使用 UTF-8 编码", 准确地说应该是 "这个文件使用 Unicode 字符集和 UTF-8 编码方案". 31 | 32 | ### 工作流程 33 | 34 | 我们来看看计算机是如何使用编码后的字符集和字符编码方案写入和读取文本文件的. 35 | 36 | 例如, 对于字符串 `Hello 世界`, 使用 Unicode 字符集编码, 可得到编码序列 `[72, 101, 108, 108, 111, 32, 19990, 30028]`; 然后使用 UTF-8 字符编码方案, 可得到二进制字节码 `48 65 6c 6c 6f 20 e4 b8 96 e7 95 8c`, 长度为 12 字节. 把这 12 字节写入文件, 我们就得到了一个使用 UTF-8 编码的文件了. 37 | 38 | 新建一个文件 `hello.txt`, 输入 `Hello 世界` 然后以无 BOM 头 UTF-8 保存, 然后用二进制文件查看器打开, 就能看到它的二进制字节码了. 39 | 40 | ![hexdump](/assets/images/character-encoding_2.gif) 41 | 42 | 读取文本文件其打印或显示出来的过程与之相反. 先使用字符编码方案将二进制字节码解码为字符代码, 然后使用编码后的字符集将字符代码映射成字符; 要想打印或显示这些字符, 就需要加载字体, 将字符转换成相应的成字形. 整个流程如下所示: 43 | 44 | ![flow](/assets/images/character-encoding_1.svg) 45 | 46 | ### 不同语言中的字符串和编码 47 | 48 | 对于不支持 Unicode 的语言, 如 C 或 Lua, 它们的字符串都可以看作单字节 (8 位) 整数序列. 它们读入文件时, 会把文件的每一个字节原样复制在内存中, 因此实际上它们是不区分文本文件和二进制文件的. (C 语言调用 `fopen` 可以在 `mode` 参数中包含 `b` 表示以二进制打开, 这是为了兼容某些奇怪的系统 (没错, 说的就是 Windows), 不是为了编码.) 可以说, 这样的语言其实是 "不认识" 字符的. 例如, 用 C 语言读入上述的 `hello.txt` 文件, 得到的字符串长度就是 12, 而这个字符串实际的字符数应该是 8. 49 | 50 | 而对于视字符串为字符序列的语言, 如 Python 或 NodeJS, 情况就不一样了. 例如, Python 在读取文本文件的时候必须要知道它们的编码 (字符编码方案和编码后的字符集), 然后将文件的内容 -- -- 二进制字节流转换成该 Python 使用的字符集 (也就是 Unicode) 所编码的字符序列(字符串). 51 | 52 | 在 Python 中使用 `open` 函数打开一个文件, 它的原型是这样的: 53 | 54 | ```py 55 | open( 56 | file, 57 | mode='r', 58 | buffering=-1, 59 | encoding=None, 60 | errors=None, 61 | newline=None, 62 | closefd=True, 63 | opener=None, 64 | ) 65 | ``` 66 | 67 | 其中 `encoding` 参数指令打开文件的编码, 若不指定, 则使用当前平台的默认编码. 如果我们调用 `open("hello.txt", encoding="UTF-8").read()`, 就可得到长度为 8 的字符串, 这表明 Python 准确地识别了每一个字符. 68 | 69 | 我们还可以在 `mode` 参数中标识该文件是二进制文件, 这样 Python 就不会对文件解码, 而是向 C 语言一样将文件内容原样读取到内存里. 不过 Python 不认为二进制字节流是字符串 (str), 而是字节序列 (bytes). 调用 `open("hello.txt", "rb").read()` 会得到一个字节序列, 它的长度是 12. 我们可以也调用 `bytes.decode` 方法将字节序列解码成字符串. 70 | 71 | ### UTF 和 BOM 头 72 | 73 | UTF-8, UTF-16 和 UTF-32 都是 Unicode 字符集的不同编码方案. 它们之间的区别就是编码单元的长度: UTF-8 的编码单元为 8 位, UTF-16 为 16 位, UTF-32 为 32 位. 因为目前 Unicode 的单个字符最多需要三个字节存储, 因此 UTF-8 和 UTF-16 都是变长编码. 如何实现变长编码呢? 以 UTF-8 为例, 它的一个字符会占用一至四字节. 它会在第一字节中标记这个字符占用的字节数: `0` 开头表示占用 1 字节, `110` 开头表示占用 2 字节, `1110` 开头表示占用 3 字节等; 第二至四字节始终以 `10` 开头. 如下表所示: 74 | 75 | | 1st byte | 2nd byte | 3rd byte | 4th byte | 76 | |:---------|:---------|:---------|:---------| 77 | | `0xxxxxxx` | | | | 78 | | `110xxxxx` | `10xxxxxx` | | | 79 | | `1110xxxx` | `10xxxxxx` | `10xxxxxx` | | 80 | | `11110xxx` | `10xxxxxx` | `10xxxxxx` | `10xxxxxx` | 81 | 82 | 这样 1 字节的 UTF-8 可表示 7 位, 与 ASCII 码兼容; 2 字节的 UTF-8 可表示 11 位, 3 字节可表示 16 位, 4 字节可表示 21 位. 这样, UTF-8 可以为全体 Unicode 码编码. 例如前面提到的汉字 `阿` 的 Unicode 码为 `38463`, 二进制就是 `0b1001011000111111` 共 16 位, 需要使用 3 字节的 UTF-8 码, 编码得到: 1110**1001** 10**011000** 10**111111**. 打开 Python 试试: 83 | 84 | ```py 85 | In [1]: bytes([0b11101001, 0b10011000, 0b10111111]).decode("UTF-8") 86 | Out[1]: '阿' 87 | ``` 88 | 89 | 对于 UTF-16 和 UTF-32 这样的编码方案, 因为使用了多个字节表示一个字符, 这就涉及到字节序的问题了. 因此 UTF-16 和 UTF-32 都有大端序和小端序两种. 如何知道一个文件使用的 UTF 码是大端序还是小端序呢? 这就要用到 BOM 头了. 在文件头部加入若干字节的特殊字符, 称之为 "BOM 头", 来标识这个文件的编码和字节序. 如下表所示: 90 | 91 | | UTF | Byte Order | BOM | 92 | | UTF-8 | - | `ef bb bf` | 93 | | UTF-16 | big endian | `fe ff` | 94 | | UTF-16 | little endian | `ff fe` | 95 | | UTF-32 | big endian | `00 00 fe ff` | 96 | | UTF-32 | little endian | `ff fe 00 00` | 97 | 98 | UTF-8 的编码单元只有 1 位, 不存在字节序的问题, 因此在 UTF-8 的 BOM 头就有些多余了. 我们常常不会在 UTF-8 文件的开头加上三字节 BOM 头, 这样的文件就是无 BOM 头的 UTF-8 文件. 99 | 100 | *** 101 | 102 | **参考资料:** 103 | 104 | - HTTP 权威指南, 人民邮电出版社 105 | -------------------------------------------------------------------------------- /source/_posts/2020-08-28-single-number.md: -------------------------------------------------------------------------------- 1 | --- 2 | key: 38 3 | title: 只出现一次的数字 4 | tag: [algorithms, leetcode] 5 | --- 6 | 这里分享三道寻找数组中只出现一次的数字的问题. 这些题使用哈希表都很好做, 但这里我们使用位运算, 可以很巧妙地在常数空间复杂度内解决问题. 7 | 8 | ### 第一题 9 | 10 | 题目源自 [Leetcode 136 题](https://leetcode-cn.com/problems/single-number/) 11 | 12 | > 给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。 13 | > 14 | > 说明: 15 | > 16 | > 你的算法应该具有线性时间复杂度。 你可以不使用额外空间来实现吗? 17 | > 18 | > 示例 1: 19 | > 20 | > ``` 21 | > 输入: [2,2,1] 22 | > 输出: 1 23 | > ``` 24 | > 25 | > 示例 2: 26 | > 27 | > ``` 28 | > 输入: [4,1,2,1,2] 29 | > 输出: 4 30 | > ``` 31 | 32 | 异或运算的特性是, 异为真同为假. 即 `1 ^ 1 = 0`, `0 ^ 0 = 0`, `1 ^ 0 = 1`, `0 ^ 1 = 1`. 因此, 两个相同的数异或的结果为 0, 任何数与 0 异或的结果都为它自身. 此外, 异或运算还满足交换律. 如果我们将数组里的元素全部异或会得到什么结果呢? 以数组 `[4,1,2,1,2]` 为例: 33 | 34 | ``` 35 | 4 ^ 1 ^ 2 ^ 1 ^ 2 36 | = 4 ^ 1 ^ 1 ^ 2 ^ 2 37 | = 4 ^ 0 ^ 0 38 | = 4 39 | ``` 40 | 41 | 因为异或运算满足交换律, 因此我们可以将出现两次的数字移动到一起, 它们异或的结果为 0; 结果就成了只出现一次的数字与 0 异或, 最后结果等于只出现一次的数字. 也就是说, 解这道题我们只需将数组里的元素全部异或就可以了. 代码非常简洁: 42 | 43 | ```py 44 | def singleNumber(nums): 45 | return reduce(lambda a, b: a ^ b, nums) 46 | ``` 47 | 48 | ### 第二题 49 | 50 | 题目源自 [Leetcode 260 题](https://leetcode-cn.com/problems/single-number-iii/) 51 | 52 | > 给定一个整数数组 nums,其中恰好有两个元素只出现一次,其余所有元素均出现两次。 找出只出现一次的那两个元素。 53 | > 54 | > 示例 : 55 | > 56 | > ``` 57 | > 输入: [1,2,1,3,2,5] 58 | > 输出: [3,5] 59 | > ``` 60 | > 61 | > 注意: 62 | > 63 | > 1. 结果输出的顺序并不重要,对于上面的例子, [5, 3] 也是正确答案。 64 | > 2. 你的算法应该具有线性时间复杂度。你能否仅使用常数空间复杂度来实现? 65 | 66 | 这道题变成了有两个数字只出现一次. 怎么解呢? 如果我们将数组里的元素全部异或, 得到的结果应该是这两个只出现一次的数字的异或. 可是怎么从异或的结果还原出这两个数字呢? 67 | 68 | 别忘了异或运算的特性: 异为真同为假. 首先, 这两个只出现一次的数字必然是不相等的, 因此异或的结果至少有一个二进制位为 1. 反过来, 如果已知异或结果的第 i 位为 1, 那么这两个数中必有一个第 i 位也为 1. 如果我们遍历数组, 将所有第 i 位为 1 的数字全部异或会发生什么呢? 注意第 i 位为 1 的数要么是这两个只出现一次的数中的一个, 要么就是其它出现了两次的数. 出现两次的数异或结果必为 0. 因此, 将所有第 i 位为 1 的数字全部异或就能得到这两个只出现一次的数中的一个. 69 | 70 | 异或的逆运算还是异或, 即如果 `a ^ b = c` 则 `a ^ c = b`. 因此只需再作一次异或就能得到另一个数了. 71 | 72 | 至于取异或结果某一个为 1 的二进制位, 我们可以这样做. 一个数与自己的相反数作按位与运算, 即 `x & -x`, 会保留这个数最右边的 1, 其余位都置为 0. 因为 `-x` 为 `x` 的补码, 等于按位取反再加一, 这样最右边的 1 先取反变为 0; 而它右边所有的 0 (如果有的话) 会先取反变为 1, 然后加一全部进位变为 0, 最右边的 1 的位置 (刚刚取反变成 0 了) 进位又变回了 1. 这样 `-x` 再与 `x` 按位与, 就只剩下最右边的 1 了. 举个例子: 73 | 74 | ``` 75 | x = 101000 76 | -x = ~x + 1 77 | = 010111 + 1 # 取反, 最右边的 1 变为 0, 且它右边的 0 全部变为 1 78 | = 011000 # 加一, 右边 0 取反得到的 1 全部进位 79 | 80 | x & -x = 101000 & 011000 81 | = 001000 # 保留最右边的 1, 其余位都置为 0 82 | ``` 83 | 84 | 最终的代码如下: 85 | 86 | ```py 87 | def singleNumber(nums): 88 | xor = reduce(lambda a, b: a ^ b, nums) 89 | mask = xor & -xor 90 | k = 0 91 | for n in nums: 92 | if n & mask: 93 | k ^= n 94 | 95 | return [k, k ^ xor] 96 | ``` 97 | 98 | ### 第三题 99 | 100 | 题目源自 [Leetcode 137 题](https://leetcode-cn.com/problems/single-number-ii) 101 | 102 | > 给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现了三次。找出那个只出现了一次的元素。 103 | > 104 | > 说明: 105 | > 106 | > 你的算法应该具有线性时间复杂度。 你可以不使用额外空间来实现吗? 107 | > 108 | > 示例 1: 109 | > 110 | > ``` 111 | > 输入: [2,2,3,2] 112 | > 输出: 3 113 | > ``` 114 | > 115 | > 示例 2: 116 | > 117 | > ``` 118 | > 输入: [0,1,0,1,0,1,99] 119 | > 输出: 99 120 | > ``` 121 | 122 | 这道题中, 数组中的数字可能出现一次或三次, 这就没法像前两道题一样使用异或了. 因为一个数与自身异或三次得到的还是它本身, 这就无法与只出现一次的数区分开了. 123 | 124 | 既然无法使用异或, 我们不妨想想什么样的运算可以满足我们的需求. 假设有一种逻辑运算 `@`, 有 125 | 126 | - `0 @ 0 = 0` 127 | - `1 @ 0 = 1` 128 | - `0 @ 1 = 1` 129 | - `1 @ 1 @ 1 = 0` 130 | 131 | 至于 `1 @ 1` 的结果, 我们不关心, 它可以是任意值. 这样的运算就能帮助我们解这个问题了. 但遗憾的是这个运算是不存在的 -- -- `1 @ 1` 的值该为多少呢? 如果为 1, 那么 `1 @ 1 @ 1 = 1 @ 1 = 1`, 不满足需求; 如果为 0, 那么 `1 @ 1 @ 1 = 0 @ 1 = 1` 也不满足需求. 实际上, 要想 `1 @ 1 @ 1 = 0`, 这个运算就必须设法对 1 出现的次数计数, 只有 0 和 1 是不够用的. 132 | 133 | 既然只有 0 和 1 不够用, 那我们就用两个变量 a 和 b, 来对 1 出现的次数计数. 我们可以规定: 134 | 135 | | 1 出现的次数 | a | b | 136 | |:------------|:--|:--| 137 | | 0 | 0 | 0 | 138 | | 1 | 0 | 1 | 139 | | 2 | 1 | 0 | 140 | 141 | 我们可以定义 `@` 运算接受元组 `(a, b)` 和位元 `n`. 它的结果是一个新的元组 `(a', b')`. 那么就有: 142 | 143 | - `(0, 0) @ 0 = (0, 0)` 144 | - `(0, 0) @ 1 = (0, 1)` 145 | - `(0, 0) @ 1 @ 1 @ 1 = (0, 0)` 146 | 147 | 我们可以列出 `@` 运算的真值表: 148 | 149 | | a | b | n | a' | b' | 150 | |:----|:----|:----|:-----|:-----| 151 | | 0 | 0 | 0 | 0 | 0 | 152 | | 0 | 1 | 0 | 0 | 1 | 153 | | 1 | 0 | 0 | 1 | 0 | 154 | | 0 | 0 | 1 | 0 | 1 | 155 | | 0 | 1 | 1 | 1 | 0 | 156 | | 1 | 0 | 1 | 0 | 0 | 157 | 158 | 类似于 "按位与", "按位异或", 我们把整数的每一位都执行 `@` 逻辑运算, 就是 "按位 `@`". 根据真值表我们很快就能写出按位 `@` 运算的代码了: 159 | 160 | ```py 161 | a_ = a & ~b & ~n | ~a & b & n 162 | b_ = ~a & b & ~n | ~a & ~b & n 163 | ``` 164 | 165 | 接下来的做法就跟第一题一样了. 我们只需将数组里的元素全部执行按位 `@`. 因为当且仅当 `b = 1` 时 1 出现的次数为 1. 因此我们最后返回 `b` 即可. 166 | 167 | ```py 168 | def singleNumber(nums): 169 | a = b = 0 170 | for n in nums: 171 | a_ = a & ~b & ~n | ~a & b & n 172 | b_ = ~a & b & ~n | ~a & ~b & n 173 | a, b = a_, b_ 174 | return b 175 | ``` 176 | 177 | *** 178 | 179 | **参考资料:** 180 | 181 | - [逻辑电路角度详细分析该题思路,可推广至通解 182 | ](https://leetcode-cn.com/problems/single-number-ii/solution/luo-ji-dian-lu-jiao-du-xiang-xi-fen-xi-gai-ti-si-l/) 183 | -------------------------------------------------------------------------------- /source/_posts/2020-09-13-callback-to-coroutine.md: -------------------------------------------------------------------------------- 1 | --- 2 | key: 40 3 | title: A simple way to turn callback pattern to coroutine pattern in Lua 4 | tag: [lua, english] 5 | aside: false 6 | --- 7 | My game project is written by Lua. However, its framework does not provide a coroutine pattern, it uses a simple callback pattern instead. For example, to send an http request and receive the response, you must write like this: 8 | 9 | ```lua 10 | http_get("https://luyuhuang.tech/sitemap.xml", function(code, content) 11 | if code ~= 200 then 12 | print("an error occured:", content) 13 | return 14 | end 15 | print("data received:", content) 16 | end) 17 | ``` 18 | 19 | For simple requests, it' OK. However, in some scenarios, maybe there are several successive requests in one procedure. For example, you must request A and then request B, and then request C. It makes you fall in the **callback hell**. Worse yet, sometimes you should call A again if request C fails. As we all know, coroutines can resolve this problem easily, but how can we use coroutines without changing the framework? Inspired by Javascript's Promise, I found an easy way to turn callback pattern to coroutine pattern. Review how JS's Promise is made: 20 | 21 | ```js 22 | async function get_sitemap(url) { 23 | const [code, content] = await new Promise((resolve) => { 24 | http_get(url, resolve); 25 | }); 26 | if (code !== 200) { 27 | console.log('an error occured:', content); 28 | return; 29 | } 30 | console.log('data received:', content); 31 | } 32 | ``` 33 | 34 | JS can await for a Promise object and then suspend the coroutine. After calling `resolve`, the coroutine resumes. That's a good idea but I don't need a Promise object since I dont't need so many methods like `then` and `catch`. So I decided to implement a simple mechanism. 35 | 36 | The key is we should suspend the coroutine and resume it after calling the callback `resolve`. Therefore, instead of yield an object, we can just yield a function whose parameter is the callback function `resolve`. We pass the `resolve` function to the yielded function after the coroutine suspended. In the `resolve` function, we resume the coroutine and pass its parameters to the coroutine, so the coroutine will be resumed after calling the `resolve` function. The code is as follows: 37 | 38 | ```lua 39 | function coroutinize(f, ...) 40 | local co = coroutine.create(f) 41 | local function exec(...) 42 | local ok, data = coroutine.resume(co, ...) 43 | if not ok then 44 | error(debug.traceback(co, data)) 45 | end 46 | if coroutine.status(co) ~= "dead" then 47 | data(exec) 48 | end 49 | end 50 | exec(...) 51 | end 52 | ``` 53 | 54 | Well, it's a very simple implementation. No such sophisticated mechanism as JS's Promise, but it works well! You can use it like this: 55 | 56 | ```lua 57 | function get_sitemap(url) 58 | local code, content = coroutine.yield(function(resolve) 59 | http_get(url, resolve) 60 | end) 61 | if code ~= 200 then 62 | print("an error occured:", content) 63 | return 64 | end 65 | print("data received:", content) 66 | end 67 | 68 | coroutinize(get_sitemap, "https://luyuhuang.tech/sitemap.xml") 69 | ``` 70 | 71 | It's easy for several successive requests too. You can also encapsulate a function suitable for coroutines. Here is a complex example that is hard to write in callback pattern: 72 | 73 | ```lua 74 | function http_get_co(url) 75 | return coroutine.yield(function(resolve) 76 | http_get(url, resolve) 77 | end) 78 | end 79 | 80 | function successive_requests(cb) 81 | local code, arg1 = http_get_co(URL_A) 82 | assert(code == 200) 83 | 84 | local code, arg2 = http_get_co(URL_B) 85 | assert(code == 200) 86 | 87 | local code, res = http_get_co(URL_C .. "?arg1=" .. arg1 .. "&arg2=" .. arg2) 88 | if code ~= 200 then 89 | -- try again 90 | return successive_requests(cb) 91 | end 92 | 93 | cb(res) 94 | end 95 | 96 | coroutinize(successive_requests, function(res) 97 | print("the result is", res) 98 | end) 99 | ``` 100 | -------------------------------------------------------------------------------- /source/_posts/2020-10-29-lua-dst.md: -------------------------------------------------------------------------------- 1 | --- 2 | key: 42 3 | title: Lua 夏令时时区问题 4 | tag: [lua, experience] 5 | --- 6 | 我之前的[一篇文章](/2020/05/08/sync-time-zone.html)介绍了怎样在服务器和客户端之间同步时间和时区. 同步时间相对简单些, 本质就是一个时间差; 而时区相对复杂些. 那篇文章介绍的方法有一个问题: 在客户端的时区启用了夏令时的时候, 客户端得到的本地时间会比实际快一个小时. 原因是求客户端时区的方法不对. 例如, 太平洋时区本为 UTC-0800, 而当客户端处于太平洋时区的 2020 年 10 月 29 日, 此时太平洋时区启用夏令时, 时区应为 UTC-0700. 如果使用这样的方法 7 | 8 | ```lua 9 | local now = os_time() 10 | local CLIENT_TIMEZONE = math.floor(os.difftime(now, os_time(os_date("!*t", now)))) 11 | ``` 12 | 13 | 求得的时区 `CLIENT_TIMEZONE` 的值为 `-28800`, 也就是负八小时, 比实际时区少了一小时, 导致求本地时间时 14 | 15 | ```lua 16 | function os.date(format, time) 17 | if time == nil then 18 | time = os.time() 19 | end 20 | return os_date(format, time - CLIENT_TIMEZONE + SERVER_TIMEZONE) 21 | end 22 | ``` 23 | 24 | 得到的时间比实际多一个小时. 为什么会有这个问题呢? 25 | 26 | 首先, 这种求时区的思路是, 先调用 `os_date("!*t", now)` 获取当前 UTC 时区的本地时间, 然后再调用 `os_time(os_date("!*t", now))` 将 UTC 时区的本地时间视为本时区的本地时间, 再转换回时间戳. 这样一来, 这个时间戳与当前时间戳的差值 `os.difftime(now, os_time(os_date("!*t", now)))` 就是当前时区了. 27 | 28 | 问题就出在 `os_time(os_date("!*t", now))` 这里. 首先我们知道调用 `os.date` 传入 `"*t"` 会得到一个表示本地时间的 table, 表示年月日时分秒等. 其中有一个不显眼的字段 `isdst`, 它的含义是当前是否启用夏令时. 而调用 `os_date("!*t", now)` 得到 UTC 时区的本地时间, 注意 UTC 时间是永远没有夏令时的, `isdst` 一定是 `false`. 而当我们把 UTC 时区的本地时间视为本时区的本地时间, 调用 `os_time(os_date("!*t", now))` 将其转换回时间戳时, 由于当前时区启用了夏令时, 这会导致其结果多出一个小时 (将非夏令时时间转换成夏令时时间, 需要加上一小时). 因此最后求的的时区就会比实际小一个小时. 29 | 30 | 解决办法也很简单. 既然减数 `os_time(os_date("!*t", now))` 会多出一个小时, 那么我们让被减数也多出一个小时就好了. 31 | 32 | ```lua 33 | local now = os_time() 34 | local utcdate = os_date("!*t", now) 35 | local localdate = os_date("*t", now) 36 | localdate.isdst = false 37 | local CLIENT_TIMEZONE = math.floor(os.difftime(os_time(localdate), os_time(utcdate))) 38 | ``` 39 | 40 | 无论当前是否是夏令时, 我们都将 `localdate.isdst` 置为 `false`. 如果当前是夏令时, `os_time(localdate)` 和 `os_time(utcdate)` 都会多出一个小时; 如果当前不是夏令时, 它们都是准确的. 这样最终得出的时区就是准确的. 41 | 42 | *** 43 | 44 | **参考:** [lua-users wiki: Time Zone](http://lua-users.org/wiki/TimeZone) 45 | -------------------------------------------------------------------------------- /source/_posts/2020-12-01-lua-traceback-with-parameters.md: -------------------------------------------------------------------------------- 1 | --- 2 | key: 43 3 | title: Printing parameters in Lua traceback 4 | tag: [lua, english] 5 | aside: false 6 | --- 7 | When an error occurs, Lua will print a traceback of the call stack, it helps us to find bugs. In many cases, however, a call stack traceback is not enough for us to find out the problem. We need more information, such as the environment, parameters of each function call, even all local variables of the stack. 8 | 9 | I decide to modify Lua to improve traceback printing. Printing parameters of each function call does not yield too much output and can help us find bugs much better. 10 | 11 | Just modify `luaL_traceback`. `lua_Debug.nparams` holds the number of parameters of the function, `lua_getlocal` returns the local variable of the given function and index. Not difficult to do this. 12 | 13 | ```c 14 | LUALIB_API void luaL_traceback (lua_State *L, lua_State *L1, 15 | const char *msg, int level) { 16 | lua_Debug ar; 17 | int top = lua_gettop(L); 18 | int last = lastlevel(L1); 19 | int n1 = (last - level > LEVELS1 + LEVELS2) ? LEVELS1 : -1; 20 | if (msg) 21 | lua_pushfstring(L, "%s\n", msg); 22 | luaL_checkstack(L, 10, NULL); 23 | lua_pushliteral(L, "stack traceback:"); 24 | while (lua_getstack(L1, level++, &ar)) { 25 | if (n1-- == 0) { /* too many levels? */ 26 | lua_pushliteral(L, "\n\t..."); /* add a '...' */ 27 | level = last - LEVELS2 + 1; /* and skip to last ones */ 28 | } 29 | else { 30 | lua_getinfo(L1, "Slntu", &ar); 31 | lua_pushfstring(L, "\n\t%s:", ar.short_src); 32 | if (ar.currentline > 0) 33 | lua_pushfstring(L, "%d:", ar.currentline); 34 | lua_pushliteral(L, " in "); 35 | pushfuncname(L, &ar); 36 | 37 | if (ar.nparams > 0) { 38 | lua_pushliteral(L, ", params:"); 39 | } 40 | for (int i = 1; i <= ar.nparams; ++i) { 41 | const char *name = lua_getlocal(L1, &ar, i); 42 | if (name) { 43 | lua_xmove(L1, L, 1); // -3 44 | const char *val = luaL_tolstring(L, -1, NULL); // -2 45 | lua_pushfstring(L, " %s = %s;", name, val); // -1 46 | lua_insert(L, -3); 47 | lua_pop(L, 2); 48 | } 49 | } 50 | 51 | if (ar.istailcall) 52 | lua_pushliteral(L, "\n\t(...tail calls...)"); 53 | lua_concat(L, lua_gettop(L) - top); 54 | } 55 | } 56 | lua_concat(L, lua_gettop(L) - top); 57 | } 58 | ``` 59 | 60 | Now, when an error occurs, we get the following output: 61 | 62 | ``` 63 | src/lua: t.lua:2: attempt to perform arithmetic on a string value (local 's') 64 | stack traceback: 65 | t.lua:2: in upvalue 'foo', params: n = 22.46; s = I'm a string; 66 | t.lua:6: in local 'bar', params: n = 11.23; s = I'm a string; 67 | t.lua:9: in main chunk 68 | [C]: in ? 69 | ``` 70 | -------------------------------------------------------------------------------- /source/_posts/2021-01-24-reuse-port.md: -------------------------------------------------------------------------------- 1 | --- 2 | key: 47 3 | title: Go 设置 socket 端口复用 4 | tag: [go, network, linux] 5 | --- 6 | 我们知道, 一般来说, TCP/UDP 的端口只能绑定在一个套接字上. 当我们尝试监听一个已经被其他进程监听的端口时, `bind` 调用就会失败, `errno` 置为 98 EADDRINUSE. 也就是所谓的端口占用. 7 | 8 | ```c 9 | int fd1 = socket(AF_INET, SOCK_DGRAM, 0); 10 | int fd2 = socket(AF_INET, SOCK_DGRAM, 0); 11 | 12 | struct sockaddr_in addr = {0}; 13 | addr.sin_family = AF_INET; 14 | addr.sin_port = htons(1234); 15 | addr.sin_addr.s_addr = inet_addr("127.0.0.1"); 16 | 17 | bind(fd1, (struct sockaddr*)&addr, sizeof(addr)); // 0 18 | bind(fd2, (struct sockaddr*)&addr, sizeof(addr)); // -1, errno = 98 19 | ``` 20 | 21 | 但是一个端口只能被一个进程监听吗? 显然不是的. 比如说我们可以先 bind 一个套接字再 fork, 这样两个子进程就监听了同一个端口. Nginx 就是这样做的, 它的所有 worker 进程都监听着同一个端口. 我们还可以[使用 UNIX domain socket 传递文件](/2019/10/11/pass-fd-over-domain-socket.html), 将一个 fd "发送" 到另一个进程中, 实现同样的效果. 22 | 23 | 事实上, 根据 TCP/IP 的标准, 端口本身就是允许复用的. 绑定端口的本质就是当系统收到一个 TCP 报文段或 UDP 数据报时, 可以根据其头部的端口字段找到对应的进程, 并将数据传递给对应的进程. 另外对于广播和组播, 端口复用是必须的, 因为它们本身就是多重交付的. 24 | 25 | ### setsockopt 26 | 27 | 在 Linux 中, 我们可以调用 `setsockopt` 将 `SO_REUSEADDR` 和 `SO_REUSEPORT` 选项置为 1 以启用地址和端口复用. 28 | 29 | ```c 30 | int val = 1; 31 | setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &val, sizeof(val)); 32 | setsockopt(fd, SOL_SOCKET, SO_REUSEPORT, &val, sizeof(val)); 33 | ``` 34 | 35 | 对于 Go 来说稍微麻烦点, 因为 Go 会在调用 `net.Listen` 的时候将 `socket()`, `bind()`, `listen()` 这几步一次性做完. 所以我们只能使用 `net.ListenConfig` 设置回调函数以控制中间过程. 在回调函数中拿到原始的文件描述符后, 我们可以调用 `syscall.SetsockoptInt` 设置 socket 选项, 这与原始的 `setsockopt` 系统调用类似. 36 | 37 | ```go 38 | cfg := net.ListenConfig{ 39 | Control: func(network, address string, c syscall.RawConn) error { 40 | return c.Control(func(fd uintptr) { 41 | syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, unix.SO_REUSEADDR, 1) 42 | syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, unix.SO_REUSEPORT, 1) 43 | }) 44 | }, 45 | } 46 | tcp, err := cfg.Listen(context.Background(), "tcp", "127.0.0.1:1234") 47 | ``` 48 | 49 | ### 作用 50 | 51 | 端口复用有什么作用呢? 根据 TCP/IP 标准, 对于单播数据报, 如果存在端口复用, 只能将其交付给其中一个进程 (组播和广播相反, 会交付给所有的进程). 我们可以让多个进程监听同一个端口, 让它们抢占式地接收处理数据, 提高服务器效率. Nginx 就是这么做的. 52 | 53 | ```go 54 | func main() { 55 | cfg := net.ListenConfig{ 56 | Control: func(network, address string, c syscall.RawConn) error { 57 | return c.Control(func(fd uintptr) { 58 | syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, unix.SO_REUSEADDR, 1) 59 | syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, unix.SO_REUSEPORT, 1) 60 | }) 61 | }, 62 | } 63 | tcp, err := cfg.Listen(context.Background(), "tcp", "127.0.0.1:1234") 64 | if err != nil { 65 | fmt.Println("listen failed", err) 66 | return 67 | } 68 | 69 | buf := make([]byte, 1024) 70 | for { 71 | conn, err := tcp.Accept() 72 | if err != nil { 73 | fmt.Println("accept failed", err) 74 | continue 75 | } 76 | for { 77 | n, err := conn.Read(buf) 78 | if err != nil { 79 | fmt.Println("read failed", err) 80 | break 81 | } 82 | 83 | fmt.Println(string(buf[:n])) 84 | } 85 | } 86 | } 87 | ``` 88 | 89 | 上面是一个迭代式 TCP 服务器, 同时只能处理一条连接. 但是我们启用了端口复用, 如果我们同时启 N 个这样的服务器, 它们就可以同时处理 N 条连接, 这个过程是抢占式的. 90 | 91 | 除此之外, 我们还可以同时监听同样的端口, 不同的 IP 地址, 以处理不同的数据报. 例如我们可以创建两个 goroutine, 其中一个监听 127.0.0.1:1234, 而另一个监听 0.0.0.0:1234, 针对不同的来源作不同的处理. 92 | 93 | ```go 94 | func serve(addr string) { 95 | cfg := net.ListenConfig{ 96 | Control: func(network, address string, c syscall.RawConn) error { 97 | return c.Control(func(fd uintptr) { 98 | syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, unix.SO_REUSEADDR, 1) 99 | }) 100 | }, 101 | } 102 | udp, err := cfg.ListenPacket(context.Background(), "udp", addr) 103 | 104 | if err != nil { 105 | fmt.Println("listen failed", err) 106 | return 107 | } 108 | 109 | buf := make([]byte, 1024) 110 | for { 111 | n, caddr, err := udp.ReadFrom(buf) 112 | if err != nil { 113 | fmt.Println("read failed", err) 114 | continue 115 | } 116 | 117 | fmt.Println(addr, caddr, string(buf[:n])) 118 | } 119 | } 120 | 121 | func main() { 122 | go serve("127.0.0.1:1234") 123 | go serve("0.0.0.0:1234") 124 | select {} 125 | } 126 | ``` 127 | 128 | 在上面的例子中就可以根据不同的目的地 IP 地址分发到不同的 goroutine 了. 129 | 130 | *** 131 | 132 | **参考资料:** TCP/IP 详解 卷1: 协议, 机械工程出版社 -------------------------------------------------------------------------------- /source/_posts/2021-01-27-regions-cut-by-slashes.md: -------------------------------------------------------------------------------- 1 | --- 2 | key: 48 3 | title: 由斜杠划分的区域 4 | math: true 5 | tag: [algorithms, leetcode] 6 | aside: false 7 | --- 8 | 一月份 Leetcode 的每日一题几乎都是并查集. 不过个人认为与状态转移方程千变万化的动态规划相比, 并查集还是相对比较简单的. 这道题是我觉得最有趣的两道之一 (另一道是[打砖块](https://leetcode-cn.com/problems/bricks-falling-when-hit/), 以后有时间的话也写一篇它的题解). 9 | 10 | 题目源自 [Leetcode 959 题](https://leetcode-cn.com/problems/regions-cut-by-slashes) 11 | 12 | > 在由 1 x 1 方格组成的 N x N 网格 grid 中,每个 1 x 1 方块由 /、\\ 或空格构成。这些字符会将方块划分为一些共边的区域。 13 | > 14 | > (请注意,反斜杠字符是转义的,因此 \\ 用 "\\\\" 表示。) 15 | > 16 | > 返回区域的数目。 17 | > 18 | > **示例 1:** 19 | > 20 | > > **输入:**
21 | > > [
22 | > >   " /",
23 | > >   "/ "
24 | > > ]
25 | > > **输出:**2
26 | > > **解释:**2x2 网格如下:
27 | > > ![](/assets/images/regions-cut-by-slashes_1.png) 28 | > 29 | > **示例 2:** 30 | > 31 | > > **输入:**
32 | > > [
33 | > >   " /",
34 | > >   " "
35 | > > ]
36 | > > **输出:**1
37 | > > **解释:**2x2 网格如下:
38 | > > ![](/assets/images/regions-cut-by-slashes_2.png) 39 | > 40 | > **示例 3:** 41 | > 42 | > > **输入:**
43 | > > [
44 | > >   "\\\\/",
45 | > >   "/\\\\"
46 | > > ]
47 | > > **输出:**4
48 | > > **解释:**(回想一下,因为 \\ 字符是转义的,所以 "\\\\/" 表示 \\/,而 "/\\\\" 表示 /\\。)
49 | > > 2x2 网格如下:
50 | > > ![](/assets/images/regions-cut-by-slashes_3.png) 51 | > 52 | > **示例 4:** 53 | > 54 | > > **输入:**
55 | > > [
56 | > >   "/\\\\",
57 | > >   "\\\\/"
58 | > > ]
59 | > > **输出:**5
60 | > > **解释:**(回想一下,因为 \\ 字符是转义的,所以 "/\\\\" 表示 /\\,而 "\\\\/" 表示 \\/。)
61 | > > 2x2 网格如下:
62 | > > ![](/assets/images/regions-cut-by-slashes_4.png) 63 | > 64 | > **示例 5:** 65 | > 66 | > > **输入:**
67 | > > [
68 | > >   "//",
69 | > >   "/ "
70 | > > ]
71 | > > **输出:**3
72 | > > **解释:**2x2 网格如下:
73 | > > ![](/assets/images/regions-cut-by-slashes_5.png) 74 | > 75 | > **提示:** 76 | > 77 | > 1. 1 <= grid.length == grid[0].length <= 30 78 | > 2. grid[i][j] 是 '/'、'\\'、或 ' '。 79 | 80 | 容易想到, 这是一个求图的连通分量的个数问题. 因此思路分两步: 81 | 82 | 1. 将斜杠 `/` 反斜杠 `\` 和空格表示的网格抽象成图; 83 | 2. 求图的连通分量的个数. 84 | 85 | ![abstract](/assets/images/regions-cut-by-slashes_6.svg) 86 | 87 | 如上图所示, 如果我们将上图左边的网格转换成右边的图, 我们就能很快地使用一些图算法求出图的连通分量的数量, 这也就是网格中区域的数量. 88 | 89 | ### 并查集 90 | 91 | 并查集, *算法导论* 中称为**不相交集合的数据结构(Disjoint-set data structure)**, 在第 21 章中有介绍. 也可以看[这篇文章](https://zhuanlan.zhihu.com/p/93647900/), 讲解地很清楚. 这里我 (因为懒) 就不做过多的介绍. 简单地来说就是遍历图的每条边, 依次合并每条边连接的两个节点; 最终若节点 `i` 与 节点 `j` 连通, 必然有 `find(i) == find(j)`. 92 | 93 | 这里我们使用的路径压缩的并查集算法. 我们使用数组 `pi` ($\pi$, 谐音 parent) 存储每个节点的父节点. 94 | 95 | ```py 96 | pi = list(range(n)) # 初始化 n 个节点的并查集, pi[i] = i 97 | 98 | def find(k): 99 | if pi[k] != k: 100 | pi[k] = find(pi[k]) # 路径压缩 101 | return pi[k] 102 | 103 | def merge(i, j): 104 | pi[find(i)] = find(j) 105 | ``` 106 | 107 | ### 将网格抽象成图 108 | 109 | 每个格子要么是 `/`, 要么是 `\`, 要么是空格. 我们可以认为每个格子都是由两个节点组成, 因此可以给每个格子分配两个节点编号. 对于空格来说, 这两个节点是相连的; 对于 `/` 和 `\`, 它们的节点分布如下图所示: 110 | 111 | ![abstract](/assets/images/regions-cut-by-slashes_7.svg) 112 | 113 | 我们规定, 若靠左的节点 (即上图中的 0 号节点) 编号为 $k$, 则靠右的节点 (上图中的 1 号节点) 编号为 $k + 1$. 对于一个 $N\times N$ 的网格中 $i$ 行 $j$ 列的格子的两个节点编号分别是 $2(iN + j)$ 和 $2(iN + j) + 1$. 114 | 115 | 使用并查集, 我们需要依次遍历一个图的所有边, 依次 merge 每条边连通的两个节点. 我们可以遍历网格中的每个格子, 然后考虑这个格子的节点和与其相邻的格子的节点之间的连通性, 依次 merge 即可. 因为是无向图, 节点 a 连通 b 也意味着 b 连通 a, 因此每个格子都只需要考虑上方和左边的格子. 对于左边的格子, 如下图所示, 无论如何都是 0 号节点与左边格子的 1 号节点相连: 116 | 117 | ![abstract](/assets/images/regions-cut-by-slashes_8.svg) 118 | 119 | 对于上方的格子, 就有四种情况. 我们可根据当前格子和上方格子是 `/` 还是 `\` 判断应该 merge 哪两个节点. 120 | 121 | ![abstract](/assets/images/regions-cut-by-slashes_9.svg) 122 | 123 | 当然, 如果当前格子是空格, 还要 merge 它的两个节点. 124 | 125 | 最终代码如下: 126 | 127 | ```py 128 | def regionsBySlashes(grid): 129 | N = len(grid) 130 | pi = list(range(N * N * 2)) # 初始化 N * N * 2 个节点的并查集 131 | def find(k): 132 | if pi[k] != k: 133 | pi[k] = find(pi[k]) 134 | return pi[k] 135 | def merge(i, j): 136 | pi[find(i)] = find(j) 137 | 138 | for i in range(N): 139 | for j in range(N): 140 | c = grid[i][j] 141 | k = 2*(i*N + j) 142 | if c == ' ': # 空格, merge 它的两个节点 143 | merge(k, k + 1) 144 | 145 | if i > 0: # merge 上方格子的节点 146 | C = grid[i-1][j] 147 | K = 2*((i-1)*N + j) 148 | m = k if c == '/' else k + 1 149 | n = K if C == '\\' else K + 1 150 | merge(m, n) 151 | 152 | if j > 0: # merge 左边格子的节点 153 | K = 2*(i*N + j-1) 154 | merge(k, K + 1) 155 | 156 | ans = 0 157 | for i in range(N * N * 2): 158 | if find(i) == i: 159 | ans += 1 160 | 161 | return ans 162 | ``` 163 | -------------------------------------------------------------------------------- /source/_posts/2021-02-05-hotfix-gen.md: -------------------------------------------------------------------------------- 1 | --- 2 | key: 49 3 | title: 自动生成 Lua 热更新代码 4 | tag: lua 5 | aside: false 6 | --- 7 | 游戏服务器使用 Lua 的一个重要原因是 Lua 便于热更. 即使服务器正在运行, 只需让它执行一段代码, 即可重写其中的某些函数, 达到热更新的目的. 例如模块 `app` 有一个函数 `foo` 8 | 9 | ```lua 10 | local M = {} 11 | 12 | function M.foo(a, b) 13 | return a + b 14 | end 15 | 16 | return M 17 | ``` 18 | 19 | 如果我们要将 `foo` 热更成将 `a` 和 `b` 相乘, 只需要让服务器加载运行如下代码即可: 20 | 21 | ```lua 22 | local M = require("app") 23 | function M.foo(a, b) 24 | return a * b 25 | end 26 | ``` 27 | 28 | 不过很多时候, 函数并不是这么单纯. 函数常常会依赖许多上值 (upvalue), 举一个复杂点的例子: 29 | 30 | ```lua 31 | local database = require('database') 32 | local M = {} 33 | M.n = 0 34 | 35 | local function bar(n) 36 | return n * 2 37 | end 38 | 39 | function M.foo(a, b) 40 | M.n = M.n + 1 41 | return database.query(bar(a + b)) 42 | end 43 | 44 | return M 45 | ``` 46 | 47 | 这个例子中, 我们写热更代码时就得注意了, `foo` 有上值 `M`, `database` 和 `bar`. 有人说直接执行整个文件不就好了? 那可不行, Lua 很灵活, 执行整个文件很有可能出别的问题. 在这个例子中会导致 `M.n` 被重置 (虽然我个人不推荐在模块空间中存状态, 但是总是会有人这么做). 在一些复杂的情况下, 函数可能会有多重依赖, 比如 `foo` 的上值中有 `bar`, `bar` 还有它的上值等等. 这就会给热更代来很多困难. 48 | 49 | ### hotfix-gen 50 | 51 | 为了解决这个问题, 我写了一个工具 [hotfix-gen](https://github.com/luyuhuang/hotfix-gen), 它能够分析代码, 提取出函数的相关依赖, 生成热更代码. 我们使用 `luarocks` 就能安装它: 52 | 53 | ```bash 54 | luarocks install hotfix-gen 55 | ``` 56 | 57 | 我们要热更 `app` 模块的 `foo` 函数, 执行 `hotfix app foo` 即可: 58 | 59 | ```bash 60 | $ hotfix app foo 61 | local database = require('database') 62 | 63 | local M = require("app") 64 | local function bar(n) 65 | return n * 2 66 | end 67 | 68 | function M.foo(a, b) 69 | M.n = M.n + 1 70 | return database.query(bar(a + b)) 71 | end 72 | ``` 73 | 74 | 这样它就能自动生成热更代码. 它会假设函数依赖的上值本身 (而非引用) 是不可变的, 例如如下的代码: 75 | 76 | ```lua 77 | local n = 1 78 | 79 | function M.foo() 80 | print(n) 81 | end 82 | 83 | n = 2 84 | ``` 85 | 86 | 提取的时候就会有问题. 因此生成的代码还是需要 review 和测试的. 不过只要代码符合一定的规范, 生成的结果就没问题; 而且比起人工编写要快捷准确的多. 87 | 88 | ### 实现原理 89 | 90 | hotfix-gen 的实现用的是笨办法, 也就是读取代码, 编译成语法树, 然后分析语法树. 虽然有 `debug.getupvalue` 可以用, 但是这必须将代码运行起来. 此外对于 `local a = b * 2` 这样的语句我们还需要知道 `a` 依赖于 `b`. 不过好消息是分析代码并没有那么复杂, 我们有现成的库可以用: [lua-parser](https://github.com/andremm/lua-parser). lua-parser 会利用 [lpeg](/2020/06/24/lpeg.html), 将 Lua 源码解析成语法树. 我们只需要分析语法树即可. 91 | 92 | 主要工作就是识别变量的定义和引用, 这需要考虑作用域. 例如下面代码中, `foo` 依赖于 `a` 但不依赖于 `b`. 但如果 `print(b)` 在 `for` 语句块外, `foo` 就又依赖于 `b` 了. 93 | 94 | ```lua 95 | local a, b 96 | local function foo() 97 | for b = a, 10 do 98 | local b = 1 99 | print(b) 100 | end 101 | -- print(b) 102 | end 103 | ``` 104 | 105 | 此外还必须考虑一些微妙的语法. 例如 `local function f()` 和 `local f = function()` 是不一样的. 下面的例子中, `foo` 依赖于定义在它之上的 `local foo = 1`, 但 `bar` 不会, 函数 `bar` 中的 `bar` 就是它自己. 106 | 107 | ```lua 108 | local foo = 1 109 | local foo = function() 110 | print(foo) -- foo is 1 111 | end 112 | 113 | local bar = 1 114 | local function bar() 115 | bar() -- bar is itself 116 | end 117 | ``` 118 | 119 | 实现主要有以下几步: 120 | 121 | - 扫描文件 block 作用域下的每个局部变量, 并且分析它们的依赖. 122 | - 如果遇到目标函数, 也分析目标函数的依赖. 123 | - 从目标函数开始遍历依赖关系网, 得到所有需要提取的语句. 语句的顺序保持不变. 124 | - 生成目标代码. 125 | 126 | 最终的代码只有三百行左右, 并不很复杂. 经过测试, 能够准确处理各种情况. 127 | -------------------------------------------------------------------------------- /source/_posts/2021-03-31-binary-find.md: -------------------------------------------------------------------------------- 1 | --- 2 | key: 51 3 | title: 并不简单的二分查找 4 | math: true 5 | tag: algorithms 6 | aside: false 7 | --- 8 | 二分查找是一个很经典的入门算法, 我们每个人都学过. 然而它往往没有我们没有想象的那么简单, 它有很多容易出错的细节: 用 `<` 还是 `<=` ? 是 `right = mid` 还是 `right = mid - 1` ? 是用 `mid = (right + left) / 2` 还是 `mid = left + (right - left) / 2` ? 如何使用二分查找找出左边界和右边界? 等等等等. 这篇文章我们就来搞清楚这些问题. 9 | 10 | ### 基本思路 11 | 12 | 二分查找的基本思路很简单: 在一个升序排列的数组中找到目标值, 首先检查数组正中间的数, 如果它比目标数大, 那么目标数一定位于数组的前半部分, 否则位于数组的后半部分; 然后在前半部分或者后半部分中执行同样的查找操作, 直到找到目标数. 13 | 14 | 为了实现这一算法, 我们需要维护一段区间, 为当前的查找区间, 初始为整个数组. 为此我们维护两个变量 `left` 和 `right` 分别表示查找区间的左边界和右边界. 接着我们会找出数组的中点位置 `mid`, 将目标数于中点位置的数比较, 然后收缩区间. 15 | 16 | 算法何时终止? 显然是找到了就可以终止了. 但若目标数不在数组中呢? 那应当是当查找区间为空时算法终止. 因为查找区间为空意味着没有数可找了, 就可以认为目标数不在数组之中了. 17 | 18 | ### 前闭后闭区间 19 | 20 | 如果数组的长度为 `len`, 则它的最大下标为 `len - 1`. 因此很自然地想到将 `left` 初始化为 0, `right` 初始化为 `len - 1`. 这样, `left` 和 `right` 表示的便是一个前闭后闭的区间. 很自然地想到中点位置 `mid = (left + right) / 2`, 即二者的平均数. 21 | 22 | ```c++ 23 | int binfind(const std::vector &array, int target) { 24 | int left = 0, right = array.size() - 1; 25 | while (left <= right) { 26 | int mid = (left + right) / 2; 27 | if (array[mid] < target) { 28 | left = mid + 1; 29 | } else if (array[mid] > target) { 30 | right = mid - 1; 31 | } else { 32 | return mid; 33 | } 34 | } 35 | return -1; 36 | } 37 | ``` 38 | 39 | 由于区间是前闭后闭的, 因此只要 `left <= right`, 区间便不为空. 同时注意到在收缩区间时, `mid` 已经检查过了, 就不必包含在新区间中了, 因此有 `left = mid + 1` 和 `right = mid - 1`. 40 | 41 | ### 前闭后开区间 42 | 43 | 我们还可以初始令 `left = 0` 和 `right = length`, 让 `left` 和 `right` 表示成一个前闭后开的区间. 相应的代码就要改改了: 44 | 45 | ```c++ 46 | int binfind(const std::vector &array, int target) { 47 | int left = 0, right = array.size(); 48 | while (left < right) { 49 | int mid = left + (right - left) / 2; 50 | if (array[mid] < target) { 51 | left = mid + 1; 52 | } else if (array[mid] > target) { 53 | right = mid; 54 | } else { 55 | return mid; 56 | } 57 | } 58 | return -1; 59 | } 60 | ``` 61 | 62 | 由于是前闭后开的区间, 因此当 `left < right` 时区间不为空. 并且由于 `right` 位置上的数并不包含在区间中, 因此收缩区间时设置 `right = mid` 即可. 63 | 64 | 这里我们使用了另外一种求中点的方式: `mid = left + (right - left) / 2`. 前闭后开的区间 `[left, right)` 的长度为 `right - left`, 我们让 `left` 向右偏移区间长度的一半, 即为中点的位置. 65 | 66 | ### 求中点位置 67 | 68 | 两种求中点位置的方式其实是一样的: 69 | 70 | $$ 71 | \begin{align} 72 | m &= l + \frac{r - l}{2} \\ 73 | &= \frac{2l}{2} + \frac{r - l}{2} \\ 74 | &= \frac{l + r}{2} 75 | \end{align} 76 | $$ 77 | 78 | 不过不同的是, `left + right` 的值有可能过大而导致整数溢出. 因此推荐使用 `mid = left + (right - left) / 2` 这种方式. 79 | 80 | 如果认为中点位置应该是 `left` 加上区间长度的一半, 则对于前闭后闭的区间, 中点位置应该是 `mid = left + (right - left + 1) / 2`. 若区间长度为奇数 (即 `right - left` 为偶数), 则两种方式求得的结果是一样的; 若区间长度为偶数, 求得的结果则会比 `(left + right) / 2` 大 1. 不过这一差别并不会对算法造成实质影响. 81 | 82 | ### 找到边界 83 | 84 | 提问: 给定一个升序排序数组和一个目标数, 找出目标数在数组中的开始位置和位置. 例如目标数 3 在数组 `[1, 2, 3, 3, 3, 5]` 中的开始位置为 2, 结束位置为 4. 85 | 86 | 我们仍然可以使用二分查找解决这个问题. 在前面看到的二分查找中, 可以看到: 87 | 88 | - 当中点数小于目标数时, 区间右移; 89 | - 当中点数大于目标数时, 区间左移; 90 | - 当中点数等于目标数时, 查找结束. 91 | 92 | 而此时我们要找到目标数第一个出现当位置 (左边界), 意味着中点数等于目标数时查找不能结束, 而应该让区间左移. 只有这样区间才会不断逼近左边界. 93 | 94 | ```c++ 95 | int leftbound(const std::vector &array, int target) { 96 | int left = 0, right = array.size(); 97 | while (left < right) { 98 | int mid = left + (right - left) / 2; 99 | if (array[mid] < target) { 100 | left = mid + 1; // move right 101 | } else { // array[mid] >= target 102 | right = mid; // move left 103 | } 104 | } 105 | return left; 106 | } 107 | ``` 108 | 109 | 这个过程中, `left` 只有在中间数小于目标数 (`array[mid] < target`) 时才会向右移动. 因为有 `array[mid] <= array[mid+1]` 且 `array[mid] < target`, 如果 `target` 在 `array` 中, 则 `mid + 1` 必然不会超过 `target` 的左边界. 因此 `left` 不会超过 `target` 的左边界. 随着区间不断收缩, 循环结束时必然有 `left == right`. 最后 `left` 和 `right` 都会处于 `target` 的左边界的位置. 110 | 111 | 找右边界的道理是一样的. 不过注意我们使用的时前闭后开的区间, 得到的右边界不属于区间的一部分. 因此最后的结果要减一. 112 | 113 | ```c++ 114 | int rightbound(const std::vector &array, int target) { 115 | int left = 0, right = array.size(); 116 | while (left < right) { 117 | int mid = left + (right - left) / 2; 118 | if (array[mid] > target) { 119 | right = mid; // move left 120 | } else { // array[mid] <= target 121 | left = mid + 1; // move right 122 | } 123 | } 124 | return left - 1; 125 | } 126 | ``` 127 | 128 | 能不能使用前闭后闭的方式解这个问题呢? 其实也是可以的. 不过这样的话循环结束时会有 `left == right + 1`, 理解起来没这么自然. 129 | 130 | ```c++ 131 | int leftbound(const std::vector &array, int target) { 132 | int left = 0, right = array.size() - 1; 133 | while (left <= right) { 134 | int mid = left + (right - left) / 2; 135 | if (array[mid] < target) { 136 | left = mid + 1; 137 | } else { 138 | right = mid - 1; 139 | } 140 | } 141 | return left; 142 | } 143 | 144 | int rightbound(const std::vector &array, int target) { 145 | int left = 0, right = array.size() - 1; 146 | while (left <= right) { 147 | int mid = left + (right - left) / 2; 148 | if (array[mid] > target) { 149 | right = mid - 1; 150 | } else { 151 | left = mid + 1; 152 | } 153 | } 154 | return right; 155 | } 156 | ``` 157 | 158 | ### 总结 159 | 160 | 这么看下来, 二分查找的细节还是挺多的, 如果不搞清楚这些细节, 就很容易出错. 总的来说, 如果使用前闭后闭区间, 则: 161 | 162 | - 循环条件为 `left <= right` 163 | - 左移区间 `right = mid - 1`, 右移区间 `left = mid + 1` 164 | - 循环结束时有 `left == right + 1` 165 | 166 | 如果使用前闭后开区间, 则: 167 | 168 | - 循环条件为 `left < right` 169 | - 左移区间 `right = mid`, 右移区间 `left = mid + 1` 170 | - 循环结束时有 `left == right` 171 | 172 | 为了防止整数溢出, 应该使用 `mid = left + (right - left) / 2` 的方式求中点. 如果要找到左边界, 则当中间数等于目标数时区间左移; 如果要找右边界, 则当中间数等于目标数时区间右移. 综合看来, 使用前闭后开的区间理解起来容易些, 我个人也比较喜欢这种方式. 173 | 174 | -------------------------------------------------------------------------------- /source/_posts/2021-06-13-jump-consistent-hash.md: -------------------------------------------------------------------------------- 1 | --- 2 | key: 53 3 | title: Jump Consistent Hash 算法 4 | math: true 5 | tag: algorithms 6 | --- 7 | 8 | 这篇文章我们讨论 Jump Consistent Hash 算法, 一个极简且高效的一致性哈希算法. 9 | 10 | ### 哈希与一致性哈希 11 | 12 | 哈希函数, 或者说散列函数, 能将一个较大定义域中的元素映射到一个较小的有限值域中. 值域中的元素有时也称为**桶 (bucket)**, 值域的大小亦称为桶的数量. 13 | 14 | MD5 就是一种常用的哈希函数, 它能将任意大小的数据 (较大的定义域) 映射成一个 16 字节的哈希值 (较小的有限值域). 最简单的哈希函数就是取模, 它能将全体整数 (较大的定义域) 映射到一个整数区间 (较小的有限值域) 中. 15 | 16 | 假设我们的服务器有 3 个工作进程, 同时为用户提供服务. 这些工作进程的功能是相同的, 并且会保存用户的状态数据. 那么如何决定一个用户由哪个工作进程提供服务呢? 最简单的办法是将用户 ID 对 3 取模, 结果是几就分配到哪个进程上. 17 | 18 | ![hash](/assets/images/jump-consistent-hash_1.svg) 19 | 20 | 这样看似工作得很完美. 随着用户量不断增长, 3 个工作进程压力过大, 需要对服务器进行扩容. 我们需要增加一个工作进程, 由 4 个工作进程同时为用户提供服务. 这个时候问题便出现了. 21 | 22 | ![rehash](/assets/images/jump-consistent-hash_2.svg) 23 | 24 | 假设扩容前服务器有 12 名用户在线, ID 为 1 至 12, 那么它们在 3 个进程中的分布如上图 (1) 所示. 现在我们增加一个工作进程, 这就需要将用户分配到 4 个进程中, 哈希函数应该改为对 4 求余, 这会导致客户端的分布转为上图 (2) 所示的情况. 前面提到, 服务器的各个进程会保存用户的状态数据; 而这次扩容会导致几乎所有用户的进程发生变化, 这就必须执行大量的数据迁移操作. 如果服务器有大量用户在线, 扩容操作的成本会变得难以接受. 25 | 26 | 为了解决这个问题, 人们提出了**一致性哈希 (consistent hash)**. 一致性哈希是一类特殊的哈希函数, 它的特点是, 当桶的数量从 $N-1$ 增加至 $N$ 时, 平均只有 $\frac{1}{N}$ 的映射结果发生改变. 观察上面的例子, 扩容时我们只需要在 p0, p1 和 p2 中各取一个用户迁移到 p4 即可, 也就是只改变 $\frac{12}{4} = 3$ 个 ID 的映射结果. 27 | 28 | 一致性哈希算法有很多种. 最早的一致性哈希算法由 Karger 等人提出, 它将桶关联在一个顺时针排列的环中, [Chord 算法](/2020/03/06/dht-and-p2p.html#3-chord-%E7%AE%97%E6%B3%95)中就用到了它. 本文介绍的是 2014 年 John Lamping 等人提出的 Jump Consistent Hash 算法. 它极其简洁, 仅有 7 行代码: 29 | 30 | ```c++ 31 | int32_t JumpConsistentHash(uint64_t key, int32_t num_buckets) { 32 | int64_t b = 1, j = 0; 33 | while (j < num_buckets) { 34 | b = j; 35 | key = key * 2862933555777941757ULL + 1; 36 | j = (b + 1) * (double(1LL << 31) / double((key >> 33) + 1)); 37 | } 38 | return b; 39 | } 40 | ``` 41 | 42 | 初看可能会一头雾水: `2862933555777941757ULL` 是什么东西? 左移右移转浮点再相除是在做什么? 这么简单几行真的能实现只改变 $\frac{1}{N}$ 的映射吗? 接下来我们便来逐步探究这个神奇的算法. 43 | 44 | ### 基于概率的哈希算法 45 | 46 | 前面提到, 当桶的数量从 $N-1$ 增加至 $N$ 时, 有 $\frac{1}{N}$ 的映射结果发生改变. 也就是说, 假设哈希函数将 $m$ 个元素映射到 1 个桶中, 此时所有元素都映射在 0 号桶中, 即哈希值都为 0; 若桶数变为 2, 则有 $\frac{m}{2}$ 个元素从 0 号桶移动到 1 号桶, 即哈希值变为 1. 同理, 当桶数变为 3, 则有 $\frac{m}{3}$ 个元素从前两个桶移动到 2 号桶中; 桶数变为 4, 则有 $\frac{m}{4}$ 个元素从前三个桶移动到 3 号桶中, 以此类推. 47 | 48 | ![buckets](/assets/images/jump-consistent-hash_3.svg) 49 | 50 | 从另一个角度考虑这个问题: 假设有一个元素, 当有 1 个桶时映射在 0 号桶中. 当桶数变为 2 时, 它有 $\frac{1}{2}$ 当概率移动到 1 号桶中; 当桶数变为 3 时, 它有 $\frac{1}{3}$ 当概率移动到 2 号桶中, 以此类推. 也就是说, 无论这个元素当前在哪个桶中, 只要当桶当数量从 $N-1$ 变为 $N$, 它都有 $\frac{1}{N}$ 的概率移动到 $N-1$ 号桶中. 这样, 我们就把这个问题转换成一个概率问题. 51 | 52 | 根据这个思路, 我们就能实现一个一致性哈希算法了. 由于算法是基于随机概率的, 为了让每次哈希结果一致, 我们将元素的值做随机种子. 53 | 54 | ```c++ 55 | int consistent_hash(int key, int num_buckets) { 56 | srand(key); 57 | int b = 0; 58 | for (int n = 2; n <= num_buckets; ++n) { 59 | if ((double)rand() / RAND_MAX < 1.0 / n) 60 | b = n - 1; 61 | } 62 | return b; 63 | } 64 | ``` 65 | 66 | 这个算法模拟了桶数量增长的情况. 当桶数量为 1 时, 元素必定在 0 号桶中, 因此有初始 `b = 0`. 之后桶依次增加, 每次增加到 `n` 时, 该元素都有 `1 / n` 的概率移动到 `n - 1` 号桶中, 也就是刚新增的桶中. 67 | 68 | ### 改进后的算法 69 | 70 | 这个算法虽然能实现一致性哈希, 但它有些慢, 时间复杂度为 $\mathrm{O}(n)$. 有没有更快但方式呢? 我们注意到, 当桶增加时, 元素只有较小的概率移动到新增加的桶中, 大概率会原地不动. 如果我们能够只在元素移动时计算, 算法就会快很多. 71 | 72 | 注意到元素只会在桶增加时移动, 且每次移动都必然移动到最新的桶中. 即, 如果一个元素移动到 $b$ 号桶中, 则必然是桶增加到 $b+1$ 个导致了这次移动. 假设元素刚刚在桶增加到 $b + 1$ 个时移动到了 $b$ 号桶中, 那么我们能不能求出这个元素下次移动时的目标呢? 73 | 74 | ![jump-probability](/assets/images/jump-consistent-hash_4.svg) 75 | 76 | 假设元素下次移动时的目标为 $j$ 号桶, 意味着元素在桶增加到 $j + 1$ 个时才会移动, 换句话说, 元素在桶增加到 $b+2, b+3, \cdots, j$ 个时都不移动. 如果我们求出了最大的 $j$ 使得元素在桶增加到 $b+2, b+3, \cdots, j$ 个时都不移动, 我们就求出了下次移动的目标 $j$. 77 | 78 | 而从概率上考虑, 元素下次移动的目标至少为 $j$ 的概率 $P_j$ 等于桶增加到 $b+2, b+3, \cdots, j$ 个时不移动的概率. 我们知道当桶增加到 $N$ 个时元素移动的概率为 $\frac{1}{N}$, 所以不移动的概率为 $\frac{N-1}{N}$. 因此有: 79 | 80 | $$ 81 | \begin{align} 82 | P_j &= \frac{b+1}{b+2} \frac{b+2}{b+3} \frac{b+3}{b+4} \cdots \frac{j-1}{j} \\ 83 | &= \frac{b+1}{j} 84 | \end{align} 85 | $$ 86 | 87 | 现在我们把思路逆转过来. 现在元素在桶增加到 $b+1$ 个时移动到了 $b$, 我们要确定这个元素下次移动的位置 $j$. 我们完全可以生成一个 0 至 1 之间的随机数 $r$, 令 $P_j = r$. 然后我们就可以决定 $j$ 了: 88 | 89 | $$ 90 | j = \left\lfloor \frac{b + 1}{r} \right\rfloor \tag{1} 91 | $$ 92 | 93 | 改进后的算法如下: 94 | 95 | ```c++ 96 | int consistent_hash(int key, int num_buckets) { 97 | srand(key); 98 | int b = 1, j = 0; 99 | while (j < num_buckets) { 100 | b = j; 101 | r = (double)rand() / RAND_MAX; 102 | j = floor((b + 1) / r); 103 | } 104 | return b; 105 | } 106 | ``` 107 | 108 | 上面的算法中, 每次迭代都用 (1) 式求出元素下一个移动的位置 `j`, 直到 `j >= num_buckets`. 109 | 110 | ### 线性同余发生器 111 | 112 | 进一步改进算法, 我们可以使用自己实现的随机函数, 而不是依赖于系统的. 这里我们使用**线性同余发生器 (Linear congruential generator)**. 这是一个古老的随机数生成算法, 很容易实现. 它的随机数是迭代生成的, 迭代式如下: 113 | 114 | $$ 115 | X_{n+1} = (aX_n + c) \mod m 116 | $$ 117 | 118 | $a, c, m$ 都为常数. $m$ 是一个正整数, 称为模数; $a$ 称为乘数, $0 \lt a \lt m$; $c$ 称为增量, $0 \le c \lt m$. 算法每次迭代根据一个旧的随机数 $X_n$ , 生成一个新的随机数 $X_{n+1}$. 迭代的初始值 $X_0$ 称为种子, $0 \le X_0 \lt m$. 119 | 120 | Jump consistent hash 算法中, 每次迭代都是这样的: 121 | 122 | ```c++ 123 | key = key * 2862933555777941757ULL + 1; 124 | j = (b + 1) * (double(1LL << 31) / double((key >> 33) + 1)); 125 | ``` 126 | 127 | 随机算法的乘数为 `2862933555777941757`, 增量为 `1`, 模数为 `0x10000000000000000`. 不需要手动取模, 让整数自然溢出即可. 因此每次迭代都生成一个随机数, 随机数的范围是 `0` 至 `0xffffffffffffffff`. 这里 `(key >> 33) + 1` 取随机数的高 31 位再加一, 范围是 `1` 至 `0x80000000`. 再让 `0x80000000` 与之相除, 得到概率的倒数. 最后乘以 `b + 1` 便是元素下次移动的位置 `j`. 128 | 129 | ### 总结 130 | 131 | 所以说小代码里有大智慧. 这其中还一些内容本文还没深处展开, 比如说概率公式的严格证明; 比如说为什么乘数是 `2862933555777941757`, 增量为什么是 `1`. 随机数生成算法是一个不小的课题, 本文只能点到为止. 比起 Karger 的算法, Jump Consistent Hash 算法简洁, 快速, 不需要额外内存. 当然它也有缺点: 它的值域只能是小于 N 的非负整数集. 这意味着不能简单地把中间的桶去掉, 这会导致值域不连续. 132 | 133 | *** 134 | 135 | **参考资料:** 136 | - [A Fast, Minimal Memory, Consistent Hash Algorithm](https://arxiv.org/abs/1406.2294) 137 | - [Linear congruential generator](https://en.wikipedia.org/wiki/Linear_congruential_generator) 138 | -------------------------------------------------------------------------------- /source/_posts/2021-08-13-tab-to-search.md: -------------------------------------------------------------------------------- 1 | --- 2 | key: 56 3 | title: 在自己的博客上启用 Tab 键搜索 4 | tag: practice 5 | aside: false 6 | --- 7 | Chrome 浏览器有一个功能, 在地址栏输入域名后按下 Tab 键, 就可进入搜索状态. 这让我们很方便地搜索一个网站的内容. 8 | 9 | 10 | 11 | 这个功能称作 Tab to Search. 当然不是每个网站都支持, 毕竟不同网站的搜索接口都不一样. 不过, 这个功能并不是 Chrome 专门为一些知名网站做的适配, 而是通过一个开放性标准 [OpenSearch description format](https://developer.mozilla.org/en-US/docs/Web/OpenSearch) 实现的. 只要你的网站符合这个规范就能支持 Tab to Search. 12 | 13 | 实现起来非常简单. 首先在网站主页的 `` 标签里加一个链接, 指向一个 OpenSearch description. 14 | 15 | ```html 16 | 17 | 18 | 19 | ``` 20 | 21 | 接着创建一个 OpenSearch description. 在这个例子中我们需要在网站根目录创建一个 `opensearch.xml`. 22 | 23 | ```xml 24 | 25 | 26 | Luyu's Blog 27 | 28 | 29 | ``` 30 | 31 | 其中 `` 标签指定搜索链接的模版, `{searchTerms}` 表示搜索的关键词. OpenSearch description 还支持更多配置字段, 详细格式参考它的[文档](https://developer.mozilla.org/en-US/docs/Web/OpenSearch). 不过对于 Tab to Search, 这样就足够了. 32 | 33 | 我的博客是静态网站, 搜索也是纯前端行为. 所以我还要做一些处理, 读取 URL 的 search 字段. 34 | 35 | ```js 36 | if (window.location.pathname === '/' && window.location.search.startsWith('?search=')) { 37 | var val = decodeURIComponent(window.location.search.replace(/^\?search=/, '')); 38 | search.onInputNotEmpty(val); 39 | } 40 | ``` 41 | 42 | 大功告成! 效果就像这样: 43 | 44 | 45 | 46 | 虽然我的博客一共没多少文章, 搜索的作用比较有限, 不过效果还是不错的. 47 | 48 | *** 49 | 50 | **参考资料:** [Tab to Search](https://www.chromium.org/tab-to-search) 51 | -------------------------------------------------------------------------------- /source/_posts/2021-09-05-application-layer-protocol.md: -------------------------------------------------------------------------------- 1 | --- 2 | key: 57 3 | title: 基于 TCP 的应用层协议设计 4 | tag: [experience, network] 5 | --- 6 | 一直以来, 包括我在内的很多人都认为, 基于 TCP 的应用层协议很简单, 只需要加个包头就行了. 因为 TCP 是可靠的协议, 能保证数据有序无误地送达对端; 只是它面向流, 不保留消息边界, 因此我们只需要定义协议包头, 能区分各个数据报即可. 然而这个看法是错误的: 传输层协议所做的工作是有限的, 应用层协议的工作远不止封装一个包头. 我们来看个例子. 7 | 8 | ### TCP 连接并不是那么可靠 9 | 10 | 我们用 C 写一个简单的客户端. 它用 TCP 连接上服务器, 然后 sleep 一段时间, 然后调用 `send(2)` 发送一段数据. 11 | 12 | ```c 13 | int main() { 14 | int fd = socket(PF_INET, SOCK_STREAM, 0); 15 | 16 | struct sockaddr_in addr = {0}; 17 | addr.sin_family = AF_INET; 18 | addr.sin_port = htons(8000); 19 | inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr); 20 | 21 | if (connect(fd, (struct sockaddr*)&addr, sizeof(addr)) < 0) { 22 | printf("connect error: %d\n", errno); 23 | return 1; 24 | } 25 | 26 | printf("connected\n"); 27 | 28 | sleep(5); // pull the network cable 29 | 30 | printf("try send\n"); 31 | 32 | const char *data = "Hello"; 33 | if (send(fd, data, strlen(data), 0) < 0) { 34 | printf("send error: %d\n", errno); 35 | return 1; 36 | } 37 | printf("sent\n"); 38 | 39 | return 0; 40 | } 41 | ``` 42 | 43 | 现在我们运行这个客户端. 在它连接上服务器后, 趁它在 sleep 时, 把网线拔了. 结果是, 它会正常发送, 然后正常退出. 服务器却什么都没收到. 44 | 45 | ``` 46 | $ ./cli 47 | connected 48 | try send 49 | sent 50 | ``` 51 | 52 | 为什么会出现这种情况? 我们知道, 在 TCP 中, 数据发送给对端后, 必须收到相应的 ACK 才能保证数据送达对端. 53 | 54 | ![psh-ack](/assets/images/unreliable-tcp-connections_1.svg) 55 | 56 | 然而当 `send(2)` 返回时, 并不能保证发送的数据被确认, 甚至不能保证数据发出去了. 它所做的只是将数据放入内核缓冲区, 只要这个操作成功了, 它就会返回成功. send 完成后, 并没有正常关闭连接, 而是直接退出. 由于程序没有关闭连接, 在程序退出后, 操作系统仍然会维持这个连接状态. 在重传多次无果后, 连接会被放弃, 且程序感知不到异常. 57 | 58 | 这个问题相当严重, 尤其是在移动网络中. 移动设备的无线网络并不稳定, "拔网线" 这样的事情经常发生. 有可能发生在 `send` 调用前; 有可能发生在 `send` 调用后, 但数据尚未发送时; 有可能发生在数据已送达, 等待接收 ACK 时. 如何解决这个问题? 59 | 60 | ### 正常关闭连接 61 | 62 | 上面程序的一个问题是, 在程序结束前没有关闭连接. 回忆一下, 关闭 TCP 连接会向对端发送一个 FIN, 告诉对端: "小于 FIN 序列号的报文段都已发送完毕, 如果你已确认接收包括 FIN 在内的所有报文段, 请给我回复一个 ACK". 因此如果连接能够正常关闭, 就能够保证所有数据送达对端. 63 | 64 | ![close-connection](/assets/images/unreliable-tcp-connections_2.svg) 65 | 66 | 现在修改我们的代码, 在程序退出前调用 `close(2)`, 看看效果怎样. 67 | 68 | ```c 69 | int main() { 70 | ... 71 | 72 | if (close(fd) < 0) { 73 | printf("close error: %d\n", errno); 74 | return 1; 75 | } 76 | printf("closed\n"); 77 | 78 | return 0; 79 | } 80 | ``` 81 | 82 | 我们再次运行程序, 同样趁它 sleep 的时候拔掉网线. 然而结果却是数据成功发送, 连接成功关闭, 程序正常退出. 83 | 84 | ``` 85 | $ ./cli 86 | connected 87 | try send 88 | sent 89 | closed 90 | ``` 91 | 92 | 原因是 `close(2)` 和 `send(2)` 一样, 并不会等待 ACK, 它只是将 FIN 放入发送队列. 问题是, 就算我们能够确认连接正常关闭 (实际上确实有办法), 也是无济于事的, 因为对于长连接应用, 不可能通过关闭连接来确保数据送达. 93 | 94 | ### 获取发送队列大小 95 | 96 | 那么有没有办法确定数据对应的 ACK 已收到呢? 实际上是有的. Linux 支持 `SIOCOUTQ` 请求, 可以获取发送队列大小. 这也就是 TCP 尚未确认送达的数据大小. 我们在发送数据后 sleep 2 秒, 然后调用 `ioctl(2)` 传入 `SIOCOUTQ` 获取发送队列大小. 97 | 98 | ```c 99 | ... 100 | 101 | sleep(2); 102 | 103 | int qsize; 104 | ioctl(fd, SIOCOUTQ, &qsize); 105 | printf("queue size: %d\n", qsize); 106 | ``` 107 | 108 | 再次执行同样的操作: 109 | 110 | ``` 111 | $ ./cli 112 | connected 113 | try send 114 | sent 115 | queue size: 5 116 | closed 117 | ``` 118 | 119 | 可以看到, 即使等待了 2 秒, 仍然有 5 字节数据没有确认送达. 120 | 121 | 这种做法虽然能检测数据是否送达, 但我们不能采用这个做法. 它有以下几个问题: 122 | 123 | - 首先, 不是所有的系统都支持获取发送队列大小. `SIOCOUTQ` 只有 Linux 支持. 124 | - 其次, 未收到 ACK 并不代表数据未送达, 有可能数据已送达, 但在接收 ACK 时网络断开. 这些未被确认的数据本应由 TCP 重传, 对端在收到这些重传数据时, 会因为重复的序列号而忽略它们. 然而现在网络断开, TCP 无法重传, 我们能在之后建立的新连接中手动重传它们吗? 如果这些数据表示提交一笔订单会怎样? 这就导致请求重复发送了. 125 | - 最后, 这种做法违反了协议分层原则. TCP 的自动重传对于上层协议来说应该是透明的, 应用层应该将 TCP 看成一个面向流的双工通道, 而不应该关心 TCP 有没有收到 ACK. 况且应用层数据报与 TCP 的发送队列在概念上完全不同: 队列中未确认的数据有可能属于多个不同的数据报. 126 | 127 | 因此, 这个问题需要在应用层协议中解决. 128 | 129 | ### HTTP 的解决方案 130 | 131 | 我们来看看成熟的应用层协议是怎么做的. HTTP 的做法非常简单, 它规定一个请求必然要对应一个响应, 且不允许服务器主动推送数据. 132 | 133 | HTTP 的每个请求都期待一个响应, 如果一个请求迟迟收不到响应, 就会认为这次请求失败. 这样的话, 如果在发送过程中网络断开, 客户端就能够感知到异常, 从而重试. 然而这会带来另一个问题, 客户端不知道服务器是否收到请求, 因为网络有可能是在服务器应答时断开的. 如果请求涉及到状态, 就会出问题. 比如, 假设这个请求是提交订单, 重试就有可能导致提交多笔订单. 134 | 135 | 对此 HTTP 引入了方法这个概念. 对于幂等的请求, 也就是说, 不会改变服务器状态的请求, 使用 GET 方法. 重试这类请求没有任何问题. 对于会改变服务器状态的请求, 使用 POST 方法. 这类请求不能随意重试, 而应该先使用 GET 方法获取当前状态, 确认无误再重新发送 POST. 例如, 如果提交订单失败, 重试时浏览器会给出警告 "确认重新提交表单". 这时应当刷新订单列表, 确认订单并未成功提交, 再作重试. 136 | 137 | HTTP 不允许服务器主动推送数据. 因为客户端通常不会 (也不应当) 响应服务器的推送, 如果服务器随意推送数据, 那么在推送的过程中网络就有可能会断开, 造成数据丢失, 并且双方都感知不到. 即使客户端在稍后请求时感知到网络断开, 双方也不知道丢失了哪些数据. 138 | 139 | ### WebSocket 的解决方案 140 | 141 | WebSocket 的通信双方都可以自由地向对方发送数据, 也不要求请求一定有响应. 但是它有心跳机制. WebSocket 基于 TCP, 一旦网络断开, 必然导致下一次心跳超时, 从而检测出连接断开. 此外 WebSocket 会管理连接的关闭, 关闭一条 WebSocket 连接需向对端发送 Close 消息, 对方收到后会回复 Close 消息. 这样应用层就能清楚地知道连接是否正常关闭. 142 | 143 | ### 应用层协议设计 144 | 145 | 考虑到可能的网络断开, 即使使用 TCP 协议, 直接向对端发送一段数据并断言它能送达是错误的. 应用层协议要有保活机制. 我们可以像 HTTP 一样要求请求必有响应, 这样能最及时地检测出异常; 也可以使用心跳, 这可以用于一些要求较低的场景. 146 | 147 | 如果检测出网络故障, 我们可能需要断开当前连接并尝试重新建立连接, 然后重新发送请求. 然而并不是所有的请求都能这样做. 对于非幂等的请求, 即会改变服务器状态的请求, 这可能会因重复请求而导致意料之外的结果. 这类请求往往依赖双端状态, 一旦连接异常关闭, 双端状态就有可能不一致, 重连后应当重新同步状态. 148 | 149 | 举个例子, 假设客户端要请求服务器提交订单. 如果出现响应超时, 那么重连后客户端应当重新请求订单列表. 如果订单列表中已经有这个订单, 我们就不应该重新下单, 也就是说 "提交订单" 这个请求依赖 "订单列表" 这个状态. 更进一步, 我们可以为每个订单分配一个唯一的 ID, 这样客户端在重连成功后就能放心地重传订单, 服务器会忽略重复的订单. 150 | 151 | 再举个例子, 假设服务器要向全体客户端广播消息. 一般来说客户端不会对服务器推送的消息作回复, 且回复广播消息会造成集中请求, 给服务器造成负担. 这种情况下如何保证客户端都能收到这个消息呢? 我们可以采用以下做法: 152 | 153 | - 服务器广播消息的同时将它们缓存起来, 缓存时间取决于消息的有效期. 这便是广播消息的 "状态". 154 | - 客户端有心跳机制. 如果在接收广播消息时出现网络断开, 稍后能够检测出来. 155 | - 客户端重连成功后, 会请求所有缓存的广播消息. 这样客户端就能获取所有的消息了. 156 | 157 | 这种做法是面向状态的通信, 或者说通过同步状态来通信. 它将消息或者消息造成的结果作为状态. 再比如在游戏服务器中, 要给玩家加 50 金币. 直接给客户端推送消息 "加 50 金币" 是不靠谱的, 正确的做法是服务器将其记录的玩家金币数增加 50, 然后向客户端推送玩家当前金币数. 如果出现网络断开, 重连后客户端会再次获取当前金币数. 这也是面向状态通信的例子, 这次是将消息造成的结果作为状态. 158 | 159 | ### 总结 160 | 161 | 网络协议并不仅仅定义交换数据的结构, 还要定义数据的交换方式, 传送步骤等一系列内容. 由于应用层不能 (也不应该) 获取传输层的详细状态, 应用层协议需要做一些工作保证数据的可靠性和完整性. 为了防止意外的网络断开, 应用层需要保活机制, 这可能依赖响应或者心跳. 当网络断开, 重连成功后, 不可轻易重传请求, 应当先同步状态. 对于没有应答的请求, 使用面向状态的通信是一个好办法. 162 | 163 | *** 164 | 165 | **参考资料:** 166 | 167 | - [The ultimate SO_LINGER page, or: why is my tcp not reliable](https://blog.netherlabs.nl/articles/2009/01/18/the-ultimate-so_linger-page-or-why-is-my-tcp-not-reliable) 168 | - TCP/IP 详解 卷 1: 协议 (原书第 2 版), 机械工程出版社 169 | -------------------------------------------------------------------------------- /source/_posts/2021-09-21-dwords2.md: -------------------------------------------------------------------------------- 1 | --- 2 | key: 58 3 | title: "DWords2: 全新版本的弹幕背单词" 4 | tag: tools 5 | aside: false 6 | --- 7 | 我觉得通过弹幕背单词是个好主意, 不过一年多前我用 Python 写的那个软件有点太简陋了, 局限性比较强, 而且放到现在还有各种兼容性问题. 因此这次我重写了 DWords, 全新版本使用 Electron 开发, 界面 (相对一代的烂界面) 要好看得多, 且新增了一些功能. 新版本使用 WebDAV 同步, 用坚果云就可以很好地同步了, 比一代的邮件同步强很多. 8 | 9 | [项目主页](https://github.com/luyuhuang/DWords2) \| [下载地址](https://github.com/luyuhuang/DWords2/releases) 10 | 11 | 12 | 13 | ### 主要功能 14 | 15 | 弹幕除了显示释义外, 还有音标, 发音按钮, 以及外部词典链接. 外部词典可以自定义. 弹幕大小, 移动速度, 颜色和透明度都可设置, UI 相对一代精细很多. 16 | 17 | ![danmaku](/assets/images/dwords2_1.png){width="300"} 18 | 19 | 支持学习计划, 可以从词库创建学习计划. 支持常用的词库如四六级, 考研, 雅思等. 当然也支持自定义计划, 学习计划还能导入导出. DWords 维护一定数量的 "当前单词" 列表, 随机播放当前列表中的单词; 标记为已记住的单词会从当前列表中移除, 并依次从计划中补充新的单词. 20 | 21 | 22 | 23 | 支持查词功能, 可以当词典使用. 如果单词不在计划中, 可以快速添加到计划; 如果已经在计划中, 还可以编辑它. 24 | 25 | 26 | 27 | 支持使用 WebDAV 同步. 这样我们就可以使用任何支持 WebDAV 的云盘同步. 以[坚果云](https://www.jianguoyun.com/)为例, 我们先在根目录创建一个文件夹 `DWords`, 然后进入安全选项设置页面, 创建一个 WebDAV 授权. 28 | 29 | ![jianguoyun](/assets/images/dwords2_2.png) 30 | 31 | 这里的服务器地址表示云盘的根目录, 因此 `https://dav.jianguoyun.com/dav/DWords` 便表示 `DWords` 目录. 我们进入 DWords 的设置页面设置好相应的 WebDAV URL, 用户名和密码即可使用同步功能了. 32 | 33 | ![sync](/assets/images/dwords2_3.png){width="650"} 34 | 35 | DWords 会定时自动同步, 也可以点击主界面上的 "Sync" 按钮手动同步. 36 | 37 | ### 实现 38 | 39 | DWords2 使用 [Electron][electron] 构建. 前端使用 [Vue][vue] 框架, 布局样式则使用 [Bootstrap][bootstrap]. 我作为一个后端开发确实不太懂这些东西, 开发过程也感受到前端开发并不容易. 语言用的是 js, 代码基本上是面向过程的, 状态-更新状态的模式. 数据存储使用的是 SQLite. 40 | 41 | [electron]: https://www.electronjs.org/ 42 | [vue]: https://vuejs.org/ 43 | [bootstrap]: https://getbootstrap.com/ 44 | 45 | 为了通过云盘同步, 我的做法是将单词列表序列化成 CSV, 相比 JSON, CSV 占用空间小很多. 同步使用的是全量 + 增量的方式. 单词的添加和修改会先以增量的方式上传到云盘, 拉取增量数据时会从上次同步的位置开始取, 这样同步速度会比较快. 如果增量文件过多, 就会将所有增量文件整合成一个全量文件. 这种方式支持同步大量单词. 46 | 47 | 内置词典使用的是 [skywind3000/ECDICT](https://github.com/skywind3000/ECDICT), 语言库使用的是 [Lingoes](http://www.lingoes.cn/index.html) 的语音库. 在此向 Linwei 和 Lingoes 表示感谢. 48 | 49 | ### 后续工作 50 | 51 | 后续可做的东西还是有很多的. 现在弹幕是随机播放的, 且标记为已记住的单词不会再出现. 后续可能会做一个根据遗忘曲线复习的功能. 还可以做一些统计图表, 记录每天记住的单词数量, 每个单词查看释义的次数等. 52 | 53 | 最后希望这个工具能对英语学习者有帮助, 也祝大家都能学好英语. 54 | -------------------------------------------------------------------------------- /source/_posts/2021-11-14-switch-statement.md: -------------------------------------------------------------------------------- 1 | --- 2 | key: 59 3 | title: Switch 语句的语义 4 | math: true 5 | tag: c/c++ 6 | aside: false 7 | --- 8 | `switch` 语句一般用于多重分支选择. 不过 `switch` 的语义与 `if ... else if` 完全不同, 它更像是 `goto` 语句. `switch` 只接一个语句块, 语句块中可以包含一些 `case` 跳转标签, `switch` 先对表达式求值, 然后跳转至对应的标签. `break` 可以跳出 `switch` 语句. 9 | 10 | ```c++ 11 | switch (state) { 12 | case 1: 13 | foo(); 14 | break; 15 | case 2: 16 | bar(); 17 | break; 18 | } 19 | ``` 20 | 21 | 因为 `switch` 语句是简单粗暴的跳转, 所以它并没有什么结构性可言. 所以我们可以写出一些奇奇怪怪的 `switch` 语句: 22 | 23 | ```c++ 24 | int n, i = 0; 25 | switch (std::cin >> n, n) { 26 | default: 27 | for (i = n; i < 10; ++i) { 28 | case 0: 29 | std::cout << i << std::endl; 30 | } 31 | } 32 | ``` 33 | 34 | 上面的代码完全合法, 类似 `goto`, 跳转是无所谓层级结构的, 因此当 `n` 等于 0 时, 跳转至循环体内部也是完全合法的. 也许是因为这种用法比较 hack, 很少有教材和教程会告诉你 `switch` 语句还能这么用. 这一特性的一个比较著名的应用就是 [Duff's device](https://en.wikipedia.org/wiki/Duff%27s_device): 35 | 36 | ```c 37 | send(to, from, count) 38 | register short *to, *from; 39 | register count; 40 | { 41 | register n = (count + 7) / 8; 42 | switch (count % 8) { 43 | case 0: do { *to = *from++; 44 | case 7: *to = *from++; 45 | case 6: *to = *from++; 46 | case 5: *to = *from++; 47 | case 4: *to = *from++; 48 | case 3: *to = *from++; 49 | case 2: *to = *from++; 50 | case 1: *to = *from++; 51 | } while (--n > 0); 52 | } 53 | } 54 | ``` 55 | 56 | 这是风格非常古老的 C 代码. 它所做的是将 `from` 指向的数据复制到 `to`, 中, 一共有 `count` 个. 如果使用常规的写法, `while` 循环要执行 `count` 次, 表达式 `--n > 0` 也需求值 `count` 次. Duff's device 的做法是让循环执行 $\lceil \frac{\mathrm{count}}{8} \rceil$ 次, 如果 `count` 不能被 8 整除, 则会在第一次循环时用 `switch` 语句跳转至循环体的相应位置, 让第一次循环时 `*to = *from++` 执行 `count % 8` 次; 再加上接下来的 $\lfloor \frac{\mathrm{count}}{8} \rfloor$ 次循环, 保证赋值语句始终执行 `count` 次. 这样 `while` 的条件判断次数减少为常规做法的 1/8. 57 | 58 | 正是因为这种可随意跳转的特性, C/C++ 就必须对其做一些限制. 我们不能在 `switch` 语句块中随意初始化变量: 59 | 60 | ```c++ 61 | switch (state) { 62 | case 1: 63 | int a = foo(); // error: jump bypasses variable initialization 64 | break; 65 | case 2: 66 | bar(); 67 | break; 68 | } 69 | ``` 70 | 71 | 这是因为整个 `switch` 的语句块是同一个作用域, 因此变量 `a` 的作用域包括 `case 2:`. 如果 `state` 为 2, `case 2:` 中就能访问到一个未初始化都变量 (虽然它没这么做). 这个规则与 `goto` 语句一致: 72 | 73 | ```c++ 74 | goto next 75 | int a = foo(); // error: jump bypasses variable initialization 76 | next: 77 | bar(); 78 | ``` 79 | 80 | 解决办法是手动限制变量的作用域, 让变量只存在于特定 case 中: 81 | 82 | ```c++ 83 | switch (state) { 84 | case 1: { 85 | int a = foo(); 86 | break; 87 | } 88 | case 2: { 89 | bar(); 90 | break; 91 | } 92 | } 93 | ``` 94 | 95 | 如果只是声明变量而没有初始化它, 则也是合法的. 这与 `goto` 语句一样: 96 | 97 | ```c++ 98 | switch (state) { 99 | int a, b; 100 | case 1: 101 | a = 1; 102 | break; 103 | case 2: 104 | b = 2; 105 | break; 106 | } 107 | 108 | goto next 109 | int c; 110 | next: 111 | foo(c = 1); 112 | ``` 113 | 114 | 因此与其称 `switch` 为 "多分支选择语句", 不如称其为 "条件跳转语句" -- 与之对应的 `goto` 是无条件跳转语句. 正是因为这种语义比较 hack, 所以后来新出的一些语言虽然有 `switch` 语句, 但是语义完全不同. 例如 C# 的 `switch` 语句要求 `case` 必须对应一个 `break`, 除非这个 `case` 为空; Go 的 `switch` 语句的每个 `case` 都是一个明确的分支. 这样的 `switch` 语句才能光明正大地称为 "多分支选择语句". 115 | 116 | Python 中的 `if ... elif` 也可以认为是多分支选择语句: 117 | 118 | ```python 119 | if a == 1: 120 | foo() 121 | elif a == 2: 122 | bar() 123 | elif a == 3: 124 | baz() 125 | ``` 126 | 127 | 其中各个 `if` 和 `elif` 的地位是等同的, 它的语法树是这样的: 128 | 129 | ``` 130 | if 131 | ├── case 1 132 | │ ├── condition: a == 1 133 | │ └── statements... 134 | ├── case 2 135 | │ ├── condition: a == 2 136 | │ └── statements... 137 | └── case 3 138 | ├── condition: a == 3 139 | └── statements... 140 | ``` 141 | 142 | 而 C/C++ 中 `if ... else if` 这样的用法则不能认为是多分支选择语句, 因为它的语义实际上是嵌套 if-else. 它的语法树是这样的: 143 | 144 | ``` 145 | if: a == 1 146 | ├── statements... 147 | └── if: a == 2 148 | ├── statements... 149 | └── if: a == 3 150 | └── statements... 151 | ``` 152 | -------------------------------------------------------------------------------- /source/_posts/2021-12-25-kmp.md: -------------------------------------------------------------------------------- 1 | --- 2 | key: 61 3 | title: 深入理解 KMP 算法 4 | tag: algorithms 5 | aside: false 6 | --- 7 | 字符串匹配是非常常用的算法. 它要做的是, 对于模式串 `t`, 找到它在目标串 `s` 中第一个出现的位置. 例如 `t = "abcab", s = "ababcabd"`, 算法就应该返回 2. 说起字符串匹配, 不得不提到 KMP 算法. KMP 是一个很厉害的算法, 它非常巧妙, 能在线性时间复杂度内完成字符串匹配. 本文我们来了解它. 8 | 9 | 本文会用到 Python 的切片语法表示子串. 如果你不熟悉 Python, 只需要记住: `s[:k]` 表示 `s` 长度为 `k` 的前缀, `s[-k:]` 表示 `s` 长度为 `k` 的后缀. 此外字符串的下标从 0 开始, 因此 `s[:k]` 的最后一个字符为 `s[k-1]`. 10 | 11 | ### 匹配算法 12 | 13 | 粗略地说, KMP 算法利用了字符串的一个性质: 字符串的尾部有可能与自己的首部相匹配. 比如说字符串 `abcab`, 它的尾部有两个字符可以与自己的首部相匹配: 14 | 15 | ``` 16 | abcab 17 | || 18 | abcab 19 | ``` 20 | 21 | 用形式化的语言描述: 对于一个字符串 `t`, 存在一个最大的数 `0 <= m < len(t)`, 使得 `t[:m] == t[-m:]` 成立. 我们称 `m` 为**最大首尾匹配长度**. 注意 `m` 必须小于 `t` 的长度, 不然没有意义 -- 任何字符串总是与自身相等. 22 | 23 | KMP 首先会为模式串的所有前缀求得最大首尾匹配长度, 存储在数组 `pi` 中. 前缀 `t[:i]` 的最大首尾匹配长度为 `pi[i-1]`. 例如字符串 `p = "abcab"` 的 `pi` 数组为 `[0, 0, 0, 1, 2]`, 因为: 24 | 25 | - `p[:1] = "a"`, 最大首尾匹配长度为 0, `pi[0] = 0` 26 | - `p[:2] = "ab"`, 最大首尾匹配长度为 0, `pi[1] = 0` 27 | - `p[:3] = "abc"`, 最大首尾匹配长度为 0, `pi[2] = 0` 28 | - `p[:4] = "abca"`, 最大首尾匹配长度为 1, `pi[0] = 1`, 因为有 29 | ``` 30 | abca 31 | | 32 | abca 33 | ``` 34 | - `p[:5] = "abcab"`, 最大首尾匹配长度为 2, `pi[0] = 2`, 因为 35 | ``` 36 | abcab 37 | || 38 | abcab 39 | ``` 40 | 41 | 我们先不管 KMP 是怎样求 `pi` 数组的, 先看看 KMP 是怎样利用 `pi` 数组做匹配的. 42 | 43 | 假设字符串 `s` 与模式串 `t` 匹配. 我们从第 0 个字符开始匹配, 接着是第 1 个, 第 2 个, 一直到第 k 个字符, 都匹配成功了; 然而第 k + 1 个字符匹配失败了. 44 | 45 | ![kmp_1](/assets/images/kmp_1.svg) 46 | 47 | 匹配失败了, 那怎么办呢? 重点来了. 第 0 至 k 个字符匹配成功了, 这段子串等于 `t[:k+1]`. `pi` 数组告诉我们, 这段子串的后 `pi[k]` 个字符正好等于它的前 `pi[k]` 个字符. 48 | 49 | ![kmp_2](/assets/images/kmp_2.svg) 50 | 51 | 这样下一步我们就让 `s[k+1]` 与 `t[pi[k]]` 相比较, 如果相等, 就继续匹配后面的字符; 如果不相等 -- 没关系, `t[:pi[k]]` 已经匹配成功, 我们再去查询 `pi` 数组得到这段子串的最大首尾匹配长度, 再按照同样的方式比较相应的字符即可. 52 | 53 | 有了这个思路, 我们就不难写出 KMP 算法的代码了: 54 | 55 | ```py 56 | def kmp(s, t): 57 | pi = calc_pi(t) # 先不管如何计算 pi 58 | j = 0 59 | for i, c in enumerate(s): 60 | while j > 0 and t[j] != c: # t[j] 匹配失败, 但是 t[:j] 匹配成功 61 | j = pi[j-1] # t[:j] 的后 pi[j-1] 个字符与前 pi[j-1] 个字符相等, 下一步匹配 t[pi[j-1]] 62 | if t[j] == c: 63 | j += 1 64 | if j == len(t): 65 | return i - j + 1 # 返回起始位置 66 | 67 | return -1 68 | ``` 69 | 70 | ### 求 pi 数组 71 | 72 | 既然 `pi` 数组这么好用, 怎么求它呢? 首先很容易想到 `pi[0] = 0`, 因为最大首尾匹配长度需小于字符串长度. 如果我们能够用 `pi[0], pi[1], ..., pi[k]` 推出 `pi[k+1]`, 我们就能求出整个 `pi` 数组了. 73 | 74 | 假设我们求出了 `pi[k]`, 也就是 `t[:k+1]` 的最大首尾匹配长度. 75 | 76 | ![kmp_3](/assets/images/kmp_3.svg) 77 | 78 | 那么 `pi[k+1]` 也就是 `t[:k+2]` 的最大首位匹配长度是多少呢? 这需要分两种情况讨论. 假设 `t[k+1] == t[pi[k]]`, 这种情况很简单, `pi[k+1] = pi[k] + 1`. 79 | 80 | ![kmp_4](/assets/images/kmp_4.svg) 81 | 82 | 要是不相等呢? 没关系, 前面那几个字符, 也就是 `t[:pi[k]]`, 不是匹配上了么? 根据前面刚求出来的 `pi` 数组, 我们知道对于 `t[:pi[k]]` 这个子串, 后 `pi[pi[k]-1]` 个字符与前 `pi[pi[k]-1]` 个字符相等! 这就回到前面匹配算法的情况了. 83 | 84 | ![kmp_5](/assets/images/kmp_5.svg) 85 | 86 | 接下来我们只需要让 `t[k+1]` 与 `t[pi[pi[k]-1]]` 相比较. 如果相等, 那么 `pi[k+1] = pi[pi[k]-1] + 1`; 如果不相等, 那就再重复这个操作: 查询 `pi` 数组, 获得前面相等部分的最大首尾匹配长度, 然后再比较相应的字符即可. 87 | 88 | 这样, 计算 `pi` 数组的代码也不难理解了. 89 | 90 | ```py 91 | def calc_pi(t): 92 | pi = [0] * len(t) # pi[0] = 0 93 | j = 0 # j 等于 pi[-1] 94 | for i in range(1, len(t)): 95 | while j > 0 and t[i] != t[j]: # t[j] 匹配失败, 但是 t[:j] 匹配成功. 注意这里 j = pi[i-1] 96 | j = pi[j-1] # t[:j] 的后 pi[j-1] 个字符与前 pi[j-1] 个字符相等, 下一步匹配 t[pi[j-1]] 97 | if t[i] == t[j]: 98 | j += 1 99 | pi[i] = j 100 | 101 | return pi 102 | ``` 103 | 104 | 计算 `pi` 的代码与 KMP 匹配算法非常像. 匹配算法是将模式串与目标串匹配, 而计算 `pi` 则是将模式串与模式串自己匹配. 在与自己匹配的过程中只会依赖之前已经计算好的 `pi` 值, 所以说这是一个动态规划算法. 105 | 106 | ### 总结 107 | 108 | 下面的代码将算法的两部分写在了一起. 概括一下, KMP 利用匹配算法, 逐渐推导出 `pi` 数组, 然后再使用同样的匹配算法, 利用前面求出的 `pi` 匹配模式串与目标串. 所以说 KMP 算法是一个非常厉害的算法. 109 | 110 | ```py 111 | def kmp(s, t): 112 | if not t: return 0 113 | pi = [0] * len(t) 114 | j = 0 115 | for i in range(1, len(t)): 116 | while j > 0 and t[i] != t[j]: 117 | j = pi[j-1] 118 | if t[i] == t[j]: 119 | j += 1 120 | pi[i] = j 121 | 122 | j = 0 123 | for i, c in enumerate(s): 124 | while j > 0 and t[j] != c: 125 | j = pi[j-1] 126 | if t[j] == c: 127 | j += 1 128 | if j == len(t): 129 | return i - j + 1 130 | 131 | return -1 132 | ``` 133 | -------------------------------------------------------------------------------- /source/_posts/2022-01-01-2021-annual-summary.md: -------------------------------------------------------------------------------- 1 | --- 2 | key: 62 3 | title: 2021 Annual Summary 4 | tag: [essays, english] 5 | banner_img: /assets/images/2021-annual-summary_4.jpg 6 | --- 7 | I didn't really do so many things in 2021 compared to last year, but at least I kept some good habits like learning English and doing LeetCode. The most important thing this year was I joined a big company, which was a major goal in the past few years. That means I left Beijing, a city I lived in for nearly 4 years since I graduated. 8 | 9 | As I mentioned in the past annual summary, I have to accept that I am just an ordinary programmer, who regarded joining a big company as a major goal in the past years, while many excellent undergraduates joined a big company when they graduated, with even better salary. Instead of comparing with others, comparing with myself in the past is more practicable. So it's important to go over the year and make a summary. 10 | 11 | ### Goals 12 | 13 | Go over the 2021 new year resolutions, I just finished some of those: 14 | 15 | - [x] Keep learning English every day, including memorizing and reviewing words, reading articles, and practicing the listening skill. 16 | - [ ] Write weekly in English every week. *(gave up since April)* 17 | - [x] Finish reading TCP/IP Illustrated. 18 | - [x] Keep everyday Leetcode exercises. 19 | - [x] Keep writing blogs, at least one post a month. 20 | - [ ] Learn at least one programming language (e.g. Rust). 21 | - [ ] Practice social skills. *(I feel lonely even more)* 22 | - [ ] Keeping exercises(running, push-up). *(not running, just push-ups every day)* 23 | 24 | ### Learning 25 | 26 | I read two books this year, about half of each. It's much less than last year. 27 | 28 | - Finished reading *TCP/IP Illustrated, Volume 1*. Last year I read half of it. 29 | - Read near half of *C++ Primer* 30 | 31 | ### Creating 32 | 33 | - Wrote [DWords2](https://github.com/luyuhuang/DWords2), an upgraded version of DWords, and got 54 stars. Instead of a simple tool, it's a useful software with some practical features. 34 | - Wrote 16 post on my blog. 35 | 36 | In this year I have 243 contributions on Github. 37 | 38 | ![github](/assets/images/2021-annual-summary_1.png) 39 | 40 | It's not easy to insist on creating outside of work. I do it because I love it. Maybe not too much, but I think I can hold on. 41 | 42 | ### LeetCode 43 | 44 | I did many LeetCode this year. I insist on doing daily exercise almost every day. 45 | 46 | ![leetcode](/assets/images/2021-annual-summary_2.png) 47 | 48 | Someone asked me why did you do LeetCode every day, are you preparing for an interview? Well, you see those deep green dots in February, those days I was really preparing for interviews. But after that, I still kept doing it. I hope that will make me "smarter", I mean, be more sensitive to code. Also, I hope that will enable me to always seize the opportunity. 49 | 50 | ### English 51 | 52 | I insist on memorizing words every day this year. Actually, I didn't learn any new words, I was just reviewing old words on and on. It's about three rounds of CET-6 words and four rounds of IELTS words. 53 | 54 | ![leetcode](/assets/images/2021-annual-summary_3.jpg){width="500"} 55 | 56 | I practiced listening and speaking skills on OpenLanguage. I also joined several times of English corner. I found that oral practice helps a lot and I'll spend more time on it. 57 | 58 | ### Something Happy 59 | 60 | I have more leisure time this year. I visited Qinhuangdao in April. The weather was cold and clear and few visitors here; the coastal scenery was so beautiful. 61 | 62 | ![qinhuangdao](/assets/images/2021-annual-summary_4.jpg){width="600"} 63 | 64 | I'd love to go there again if I have any opportunity. 65 | 66 | ### Finally 67 | 68 | Despite vaccinations, the pandemic has not gone. It's not easy for us: we can't commute freely, we have to face strict control policies and we must pay much attention to protect ourselves from the virus. This year I couldn't even go home to celebrate the Spring Festival with my family. I hope the pandemic would go in the near future. 69 | 70 | I hope I can keep learning and keep growing, and becoming better. I hope I'll always be a kid who loves his life and work and is curious about knowledge. 71 | 72 | Finally, I'd like to thank my family, coworkers, and friends for staying with me and giving me lots of help in the past year. 73 | 74 | 2022 is here. I wish you a very happy New Year. 75 | -------------------------------------------------------------------------------- /source/_posts/2022-03-03-jekyll-email-protection.md: -------------------------------------------------------------------------------- 1 | --- 2 | key: 64 3 | title: Jekyll Email Protection 4 | tag: [english, practice] 5 | aside: false 6 | --- 7 | A few months ago, I [migrated](/2021/12/12/service-migration.html) my blog from Cloudflare to my cloud server. Since then, I've received more spam than before. I found the reason is that Cloudflare has a feature that protects email from crawlers. Cloudflare scans your HTML pages and replaces all `mailto` links with encoded URLs, then insert a Javascript that will decode them when the browser loads that page. For example, suppose we have such an email link: 8 | 9 | ```html 10 | send me an email! 11 | ``` 12 | 13 | Then Cloudflare replaces it to: 14 | 15 | ```html 16 | send me an email! 17 | 18 | ``` 19 | 20 | So it looks like a normal link. That string `523e272b27123a27333c357c313d3f` is the encoded email. If a crawler gets that, it's hard to know that's an email link, except run the page, like a true browser. In that case, the following Javascript would be run and the link would be decoded. It's a large overhead for crawlers to parse a page like browsers. 21 | 22 | The encoding algorithm is pretty simple. We choose a random one-byte number and xor it with each character of the email string. Convert each xor result to hex and join them to get the encoded string. The random number also be converted to hex and inserted into the beginning of that encoded string. 23 | 24 | To decode it to get the original email, we regard every two characters as a one-byte hex number. Just xor the remaining numbers with the first number and convert them to characters. 25 | 26 | I'd like to do the same thing on my self-hosted blog. I want Jekyll to encode email links when building the site, then add a piece of Javascript to decode them when the browser loads the page. Liquid, the template language used by Jekyll, has limited functions, it can't handle string and characters. Lucky, Jekyll provided a convenient way to extend Liquid. In that case, we add a Liquid filter `email_encode` to encode email. Just add a Ruby script to the directory `_plugins`: 27 | 28 | ```ruby 29 | # _plugins/email_encode.rb 30 | 31 | module Jekyll::CustomFilters 32 | def email_encode(email) 33 | @token = rand(1..0xff) 34 | '#%02x%s' % [@token, email.each_byte.map{|n| '%02x' % (n ^ @token)}.join('')] 35 | end 36 | end 37 | 38 | Liquid::Template.register_filter(Jekyll::CustomFilters) 39 | ``` 40 | 41 | It uses the same way as Cloudflare to encode the email and place a '#' at the beginning the make it an anchor link. So we can use it like the following: 42 | 43 | ```html 44 | send me an email! 45 | ``` 46 | 47 | And Jekyll would generate such results: 48 | 49 | ```html 50 | send me an email! 51 | ``` 52 | 53 | The last thing is adding a piece of Javascript to decode that link: 54 | 55 | ```js 56 | (function () { 57 | function byte(s, i) { 58 | return parseInt(s.substr(i, 2), 16); 59 | }; 60 | 61 | function decode(s) { 62 | s = s.substr(1); 63 | for (var a = '', t = byte(s, 0), i = 2; i < s.length; i += 2) { 64 | a += String.fromCharCode(byte(s, i) ^ t); 65 | } 66 | return a; 67 | }; 68 | 69 | document.querySelectorAll('a.encoded-email').forEach(function(el) { 70 | el.setAttribute('href', 'mailto:' + decode(el.getAttribute('href'))); 71 | }); 72 | })(); 73 | ``` 74 | 75 | **Reference:** 76 | -------------------------------------------------------------------------------- /source/_posts/2022-05-06-tree-dp.md: -------------------------------------------------------------------------------- 1 | --- 2 | key: 66 3 | title: "树形动态规划: 如何处理子节点依赖父节点的问题" 4 | math: true 5 | tag: [algorithms, leetcode] 6 | aside: false 7 | --- 8 | 动态规划规划是一种很常见的算法. 它的思路基本上是将大问题转化成小问题, 大问题依赖小问题的结果. 常见的动态规划有一维动态规划, x = N 的问题可能依赖 x = N - 1 的问题 9 | 10 | ![1d](/assets/images/tree-dp_1.svg) 11 | 12 | 这样只要我们知道 x = 0 的问题的解, 就能逐步推出 x = N 的问题的解. 或者有二维动态规划, x = N, y = M 的问题可能依赖 x = N - 1, y = M 和 x = N, y = M - 1 的问题. 13 | 14 | ![2d](/assets/images/tree-dp_2.svg) 15 | 16 | 这样我们也可以从 x = 0, y = 0 的问题的解逐步推出 x = N, Y = M 的问题的解. 但有一类特殊的动态规划, 子问题之间的依赖关系是网状的 17 | 18 | ![tree](/assets/images/tree-dp_3.svg) 19 | 20 | 如果把子问题看作节点, 依赖关系看作边, 整个问题就可以看作一个无向图. 如果这个图没有环路, 那么它也可以看作一颗树. 如何解决树形动态规划问题? 本文我们来探讨它. 21 | 22 | ### 最小高度树 23 | 24 | 问题来自 [LeetCode 310 题](https://leetcode-cn.com/problems/minimum-height-trees/). 给定一个无向无环图, 将其中的一个节点视为根节点, 那么它也可以看作一棵树. 求选取哪个节点作为根节点能让这棵树高度最小. 需要返回所有的解. 例如下图所示的树, 选择 3 号或者 4 号节点能让树的高度最小, 因此返回 `[3, 4]`. 25 | 26 | ![problem](/assets/images/tree-dp_4.jpeg){width="500"} 27 | 28 | 将一个节点视为根节点时树的高度, 也就是它能到达的最远节点的距离加一. 如果我们对每个节点求出了它们最远节点的距离, 我们就能知道哪几个节点作为根节点能让树的高度最小. 我们不妨令距节点 $i$ 最远的节点的距离为 $D(i)$; 为了方便, 如果没有任何节点与 $i$ 相邻, 则 $D(i) = 0$. 29 | 30 | 最笨的办法就是, 对于每个节点 $i$, 都使用 BFS 或者 DFS 计算出距它最远的节点的距离 $D(i)$; 最后返回结果最小的节点. 如果图中有 $N$ 个节点, 这种做法的时间复杂度就是 $\mathrm{O}(N^2)$. 有没有更好的方法呢? 31 | 32 | 假设与节点 $i$ 相邻的节点有 $i_1, i_2, ..., i_k$, 不难发现节点 $D(i)$ 取决于与其相邻的节点. 也就是 33 | 34 | $$ 35 | D(i) = \max(D(i_1), D(i_2), ..., D(i_k)) + 1 \tag 1 36 | $$ 37 | 38 | 但是问题是, 当一个节点依赖它的相邻节点时, 相邻节点也在依赖它. 如何解决这个互相依赖的问题呢? 39 | 40 | ![depends](/assets/images/tree-dp_4.svg) 41 | 42 | 图的结构比较混乱, 我们不妨取其中任意一个节点作为根节点, 将图视为一棵树. 43 | 44 | ![ci](/assets/images/tree-dp_5.svg) 45 | 46 | 对于节点 $i$, 有一个父节点 $i_p$ 和若干个子节点 $i_{c1}, i_{c2}, ..., i_{ck}$. 我们知道, $D(i)$ 等于节点 $i$ 距其最远节点的距离. 那么这个最远的节点就有两种情况: 47 | 48 | - 距 $i$ 最远的节点可以经由 $i$ 的子节点访问到 49 | - 距 $i$ 最远的节点可以经由 $i$ 的父节点访问到 50 | 51 | 我们记节点 $i$ 经由子节点访问到的最远节点的距离为 $C(i)$, 经由父节点能访问到的最远节点的距离为 $P(i)$. 那么显然 52 | 53 | $$ 54 | D(i) = \max(C(i), P(i)) \tag 2 55 | $$ 56 | 57 | $C(i)$ 的求解非常简单, 直接递归即可. 我们用邻接列表表示图, `G[i]` 为节点 `i` 的邻接节点列表. 我们将结果保存在数组 `C` 中. 58 | 59 | ```c++ 60 | int children(const vector> &G, int i, int pi, vector &C) { 61 | int ans = 0; 62 | for (int j : G[i]) { 63 | if (j == pi) continue; 64 | ans = max(ans, children(G, j, i, C) + 1); 65 | } 66 | return C[i] = ans; 67 | } 68 | ``` 69 | 70 | 上面的代码中, 对于叶子节点, 由于没有子节点, `ans` 在 for 循环后仍然为 0. 对于其他节点, `C[i]` 等于其结果最大的子节点加 1. 71 | 72 | $P(i)$ 的求解则相对复杂些. 如下图所示, 节点 $i$ 经由父节点 $i_p$ 到达最远的节点可能有这几条路径: 经由父节点的父节点, 或者经由父节点的子节点. 73 | 74 | ![pi](/assets/images/tree-dp_6.svg) 75 | 76 | 因此可知, 节点 $i$ 经由父节点能到达的最远节点的距离应该是 $P(i) = \max(C(i_p), P(i_p)) + 1$. 77 | 78 | ... 真的是这样的吗? 注意节点之间是相互依赖的. 父节点 $i_p$ 经由子节点到达最远节点时, 这个子节点有可能正是 $i$. 如上图所示, 我们考虑 $i$ 的父节点经由其子节点到达的最远节点时, 要排除掉节点 $i$. 79 | 80 | 记节点 $i$ 经由子节点访问到的第二远节点的距离为 $C'(i)$. 如果 $i$ 的子节点少于两个, 则 $C'(i) = 0$. 于是有 81 | 82 | $$ 83 | P(i) = \left\{\begin{matrix} 84 | \max(C(i_p), P(i_p)) + 1 & 当 C(i_p) 不经由 i \\ 85 | \max(C'(i_p), P(i_p)) + 1 & 当 C(i_p) 经由 i 86 | \end{matrix}\right. \tag 3 87 | $$ 88 | 89 | 我们可以在递归求出 $C(i)$ 的同时求出 $C'(i)$, 并记录节点 $i$ 经由哪个子节点到达最远的节点. 然后再根据 (3) 式递归地求出 $P(i)$. 接着根据 (2) 式便可得到所有节点的 $D(i)$, 最后返回 $D(i)$ 最小的节点即可. 整个算法需要遍历 2 遍图 (树), 如果图中有 $N$ 个节点, 则时间复杂度为 $\mathrm{O}(N)$. 90 | 91 | 在 [LeetCode 的原题](https://leetcode-cn.com/problems/minimum-height-trees/)中, 所有节点被编号为 `0` 至 `n - 1`. 给定数字 `n` 和一个有 `n - 1` 条无向边的 `edges` 列表 (每一个边都是一对标签), 其中 `edges[i] = [ai, bi]` 表示树中节点 `ai` 和 `bi` 之间存在一条无向边. 完整的代码如下: 92 | 93 | ```c++ 94 | int children(const vector> &G, int i, int pi, 95 | vector &C, vector &C1, vector &mc) { 96 | int m = 0, n = 0, c = -1; // m 为最远节点的距离, n 为第二远节点的距离. 97 | for (int j : G[i]) { 98 | if (j == pi) continue; 99 | int l = children(G, j, i, C, C1, mc) + 1; 100 | if (l >= m) { 101 | n = m, m = l; 102 | c = j; 103 | } else if (l > n) { 104 | n = l; 105 | } 106 | } 107 | C[i] = m, C1[i] = n; 108 | mc[i] = c; 109 | return m; 110 | } 111 | 112 | void parents(const vector> &G, int i, int pi, 113 | vector &C, vector &C1, vector &mc, vector &P) { 114 | if (pi >= 0) { 115 | P[i] = max(mc[pi] == i ? C1[pi] : C[pi], P[pi]) + 1; // (3) 式 116 | } 117 | for (int j : G[i]) { 118 | if (j != pi) parents(G, j, i, C, C1, mc, P); 119 | } 120 | } 121 | 122 | vector findMinHeightTrees(int n, vector>& edges) { 123 | vector> G(n); 124 | for (const auto &edge : edges) { 125 | G[edge[0]].push_back(edge[1]); 126 | G[edge[1]].push_back(edge[0]); 127 | } 128 | 129 | vector C(n), C1(n), mc(n), P(n), ans; // C1 即 C'; mc 记录经由那个子节点到达最远的节点. 130 | children(G, 0, -1, C, C1, mc); // 第一次遍历, 求出 C(i) 和 C'(i) 131 | parents(G, 0, -1, C, C1, mc, P); // 第二次遍历, 求出 P(i) 132 | 133 | int d = n; 134 | for (int i = 0; i < n; ++i) 135 | d = min(d, max(C[i], P[i])); 136 | for (int i = 0; i < n; ++i) 137 | if (d == max(C[i], P[i])) ans.push_back(i); 138 | return ans; 139 | } 140 | ``` 141 | -------------------------------------------------------------------------------- /source/_posts/2022-05-10-cpp-const.md: -------------------------------------------------------------------------------- 1 | --- 2 | key: 67 3 | title: 谈谈 C++ 中的 const 4 | tag: c/c++ 5 | aside: false 6 | --- 7 | C++ 用关键字 `const` 标识一个类型不可变. 这其实很容易理解. 不过, 对于 C++ 而言, 简单的概念也有很多可以讨论的. 我们来看一个问题. 8 | 9 | ### 问题 10 | 11 | 我们知道 `const` 可以用于修饰成员函数, 标识这个函数不能修改这个类的数据. 假设一个类有一个指针类型的成员 `T *p`, 我们希望通过 `get()` 方法获取 `p` 所指向的对象的引用. 如果 `get()` 被 `const` 修饰, 它应该返回什么类型, 是 `T&` 还是 `const T&` 呢? 12 | 13 | ```c++ 14 | class C { 15 | public: 16 | ??? get() const { return *p; } 17 | private: 18 | T *p; 19 | }; 20 | ``` 21 | 22 | 可能很多同学很自然地认为应该返回 `const T&`, 因为 `get()` 不应该改变数据. 的确, 很多类就是这样处理的. 例如标准库的顺序容器都有 `front` 方法, 返回容器中第一个元素的引用. 如 `vector::front()` 23 | 24 | ```c++ 25 | vector v = {1, 2, 3}; 26 | v.front() = 10; // int & 27 | 28 | const vector cv = {1, 2, 3}; 29 | v.front() = 10; // error: assignment of read-only location. const int & 30 | ``` 31 | 32 | 可以看到非 const 版本返回的是 `int&`, 而 const 版本返回的是 `const int&`. 33 | 34 | 我们看另一个例子. 标准库的迭代器, 例如 `vector::iterator`, 会重载解引用运算符 `operator*()`. 那么它的返回类型是什么呢? 35 | 36 | ```c++ 37 | vector v = {1, 2, 3}; 38 | const auto i = v.begin(); 39 | *i = 10; // int & 40 | ``` 41 | 42 | 它返回了 `int&` 而不是 `const int&`, 即使这个 `operator*()` 是 const 版本的. 43 | 44 | ### 引用类型, 顶层 const 和底层 const 45 | 46 | 首先我们知道, C++ 的类型分为**值类型**和**引用类型**. 对于引用类型而言, 例如指针, 它有两层 const: **顶层 (top-level) const** 和**底层 (low-level) const**. 顶层 const 表示这个变量本身不可变. 47 | 48 | ```c++ 49 | int a, b; 50 | int *const p = &a; 51 | p = &b; // error: assignment of read-only variable 52 | *p = 10; // ok 53 | ``` 54 | 55 | 而底层 const 表示这个变量引用的值不可变. 56 | 57 | ```c++ 58 | int a, b; 59 | const int *p = &a; 60 | p = &b; // ok 61 | *p = 10; // error: assignment of read-only location 62 | ``` 63 | 64 | 对变量赋值或初始化时, 顶层 const 可以隐式加上或去除, 底层 const 可以隐式加上, 却不能去除. 65 | 66 | ```c++ 67 | int *p; 68 | int *const q = p; // int* -> int *const 69 | p = q; // int *const -> int* 70 | 71 | const int *cp; 72 | cp = p; // int* -> const int* 73 | p = cp; // error error: invalid conversion from ‘const int*’ to ‘int*’ 74 | ``` 75 | 76 | 如果一个类的成员函数被 `const` 修饰, 则这个函数的 `this` 指针是底层 const 的, 也就是 `const T *this`. 那么通过 `this` 指针访问到的所有成员, 也就是这个函数能访问到的所有成员, 都是顶层 const 的. 77 | 78 | 以本文开头的例子, `get()` 被 `const` 修饰, `get()` 中访问到的 `p` 的类型应该是 `T *const p`. 编译器并不阻止我们在 const 成员函数里修改指针成员指向的值, 那为什么有些类要禁止修改, 而有些类允许修改呢? 79 | 80 | ### 引用类型还是值类型 81 | 82 | 如果一个类有一个指针类型的成员 `T *p`, 那么我们在拷贝这个类的对象时, 是复制这个指针本身还是复制指针指向的值呢? 83 | 84 | ```c++ 85 | class C { 86 | public: 87 | C(const C &c) : p(c.p) { } // or 88 | C(const C &c) : p(new T(*c.p)) {} 89 | 90 | C &operator=(const C &c) { 91 | if (&c == this) return *this; 92 | p = c.p; 93 | return *this; 94 | } // or 95 | C &operator=(const C &c) { 96 | if (&c == this) return *this; 97 | delete p; 98 | p = new T(*c.p); 99 | return *this; 100 | } 101 | 102 | private: 103 | T *p; 104 | }; 105 | ``` 106 | 107 | C++ 允许开发者控制对象拷贝时的行为. 我们可以仅拷贝指针, 让拷贝前后指向同一个对象; 也可以拷贝指针指向的值, 向用户隐藏这个类存在引用成员这一事实. 108 | 109 | 当我们拷贝指针指向的值时, 这个类看起来就是个**值类型**. 例如 `std::vector`, 它的内存是动态分配的, `vector` 对象本身只记录指向分配内存的指针. 但是我们在拷贝 `vector` 时, 会复制其包含的所有对象. 因此对于用户来说它就是个值类型. 110 | 111 | 既然是值类型, 就只有一层 const, 也就是顶层 const. 因此当一个 `vector` 是 const 的时候, `vector::front()` 也应该返回 const 的引用. 类需要负责将顶层的 const 传递到底层. 112 | 113 | 当我们仅拷贝指针本身时, 这个类看起来就是个**引用类型**. 例如 `vector::iterator`, 它包含一个指向 `vector` 中元素的指针. 当拷贝迭代器时, 仅会拷贝指针本身, 拷贝前后的迭代器指向同一个元素. 因此对于用户来说它就是个引用类型. 114 | 115 | 既然是引用类型, 就应该区分底层 const 和底层 const. 因此即使迭代器本身是 const 的, `operator*()` 也不会返回 const 的引用, 因为顶层 const 不会传递到底层. 怎样设置迭代器的底层 const? `vector` 提供了两个类, `vector::iterator` 和 `vector::const_iterator`. 后者无论迭代器本身是否是 const, `operator*()` 始终返回 const 的引用, 因为它是底层 const 的. 116 | 117 | ### C++ 很强大 118 | 119 | 回到本文开头的问题. 标准答案是, 返回 `const T&` 还是 `T&` 取决于我们如何定义这个类. 如果 `class C` 的拷贝控制函数拷贝 (或移动) 了 `p` 指向的值, 则应当返回 `const T&`; 如果只是拷贝指针本身, 则应当返回 `T&`. 120 | 121 | 更一般地总结一下, 对于包含引用类型成员 (如指针, 智能指针) 的类来说, 如果要将其视为值类型, 则 122 | 123 | - 拷贝控制函数需要拷贝引用类型成员所引用的数据 124 | - 对于访问所引用数据的方法, 应当提供 const 和非 const 两个版本 125 | 126 | ```c++ 127 | class C { 128 | public: 129 | C(const T &t) : p(new T(t)) {} 130 | C(const C &c) : p(new T(*c.p)) {} 131 | ~C() { delete p; } 132 | C &operator=(const C &c) { 133 | if (&c == this) return *this; 134 | delete p; 135 | p = new T(*c.p); 136 | return *this; 137 | } 138 | 139 | T &get() { return *p; } 140 | const T &get() const { return *p; } 141 | private: 142 | T *p; 143 | }; 144 | ``` 145 | 146 | 反之, 如果将其视为引用类型, 则 147 | 148 | - 拷贝控制函数拷贝引用类型成员本身 149 | - 应当通过一些方式 (如模版) 设置底层 const 150 | - 对于访问所引用数据的方法, 只需要 const 版本. 如果是底层非 const, 则允许修改所引用数据. 151 | 152 | ```c++ 153 | template class C { 154 | public: 155 | C(T *p) : p(p) {} 156 | C(const C &c) : p(c.p) {} 157 | C &operator=(const C &c) { 158 | if (&c == this) return *this; 159 | p = c.p; 160 | return *this; 161 | } 162 | 163 | T &get() const { return *p; } 164 | private: 165 | T *p; 166 | }; 167 | ``` 168 | 169 | 当然, 如果这个类是诸如某某管理器之类的单例类或不可拷贝的类, 就不需要考虑这么多了, 根据需求处理即可. 170 | 171 | 与其他语言 (Java, Go, Python) 不同, C++ 的类既可以是值类型, 又可以是引用类型, 这取决于开发者怎样设计. C++ 希望开发者可以像用内置类型一样使用自定义类型, 因此它提供了运算符重载, 拷贝控制等一系列的机制, 这让 C++ 的类很强大, 同时也比较复杂. 这就要求我们能够理解这些概念, 而不是只是记住 `const` 有哪几种用法. 172 | -------------------------------------------------------------------------------- /source/_posts/2023-01-11-2022-annual-summary.md: -------------------------------------------------------------------------------- 1 | --- 2 | key: 71 3 | title: 2022 Annual Summary 4 | tag: [essays, english] 5 | banner_img: /assets/images/2022-annual-summary_6.jpg 6 | --- 7 | 8 | 9 | 10 | 2022 is a bit tough for many of us. The pandemic disturbed many people's life. 11 | The Chinese internet industry is not that prosperous in the year: many companies 12 | layoffs and many people are unemployed, many game projects were aborted due to 13 | suspended issued publishing licenses, many companies' share prices fell, and so on. 14 | I was infected with COVID-19 at the end of 2022 and suffered from fever, body 15 | aches, and fatigue for a week. 16 | 17 | Anyway, It's worth celebrating at the beginning of 2023 as I'm still alive, I still 18 | have a job, and I've done some meaningful work in the year. We still hope that the 19 | world will be better and that all wishes come true. 20 | 21 | ### Goals 22 | 23 | I finished some of the new year's resolutions for 2022. 24 | 25 | - [x] Keep up learning English. 26 | - [x] Finish reading *C++ Primer* 27 | - [x] Read *Understand the Linux Kernel* *(4 chapters)* 28 | - [x] Keep up daily LeetCode exercises 29 | - [ ] Keep up writing blogs, at least one post a month *(only 9 posts)* 30 | - [ ] Keep up exercises (running, push-up) *(did not run, just push-ups every day)* 31 | 32 | ### Learning 33 | 34 | On weekends, I usually spend time reading tech books. 35 | 36 | - Finished reading *C++ Primer*. Last year I read half of it. 37 | - Read 4 chapters of *Understand the Linux Kernel*. 38 | 39 | ### LeetCode 40 | 41 | This year I spent lots of time doing LeetCode, basically every day. 42 | 43 | ![leetcode](/assets/images/2022-annual-summary_1.png) 44 | 45 | Now I've done 912 exercises, I think it might be a small achievement that's not 46 | very easy to achieve. Doing LeetCode has somewhat improved my coding skill and 47 | logical mind. it's a good habit that's worth keeping up. 48 | 49 | ![leetcode](/assets/images/2022-annual-summary_2.png) 50 | 51 | ### Language 52 | 53 | English learning is not easy. I've spent plenty of time learning English, including 54 | memorizing words and practicing listening and speaking. I insist on memorizing 55 | words every day this year and until now, I've used Baicizhan to insist on memorizing 56 | words for 1709 days. 57 | 58 | ![English](/assets/images/2022-annual-summary_3.jpg){width="650"} 59 | 60 | I also spent lots of time on Open Language to practice listening and speaking. It 61 | helped a lot at the beginning, but now I feel it doesn't help much. I think I've hit 62 | the English learning plateau. 63 | 64 | In addition to English learning, I've been learning Japanese on Duolingo. I didn't 65 | spend much time on that, basically two units a day. It's just for fun. 66 | 67 | ![duolingo](/assets/images/2022-annual-summary_4.jpg){width="400"} 68 | 69 | ### Creating 70 | 71 | - (only) Wrote 9 post on my blog. 72 | 73 | This year I didn't create much work in my spare time. I intended to use C++ to write 74 | a VPN software, but I only completed the most basic feature and have not released an 75 | initial version. I just had some sporadic commit records on Github. 76 | 77 | ![github](/assets/images/2022-annual-summary_5.png) 78 | 79 | ### Something Happy 80 | 81 | The happiest thing is that I've had a good time with the people I love. Thanks for her 82 | company to make me no longer feel lonely. 83 | 84 | ![flowers](/assets/images/2022-annual-summary_6.jpg){width="600"} 85 | 86 | ### Finally 87 | 88 | Not long ago I revisited *My Life as McDull (麦兜故事)*, a famous Hongkong film I 89 | watched when I was a kid. But I didn't understand its profound central idea at that 90 | age. I love what it said at the ending: 91 | 92 | > Roasted chicken is easy to cook. The material is a chicken; the method is to take 93 | > a chicken and roast it, and then it's done. If you want it to be delicious, the secret 94 | > is to cook it better. 95 | 96 | ![mcdull](/assets/images/2022-annual-summary_7.png){width="700"} 97 | 98 | Well, life might be a simple process from birth to death. It's simple, but it also can be 99 | complicated if you want. If you want it to be beautiful, the secret is to be positive and 100 | try to make it better. 101 | 102 | Although we might be going to face challenges in 2023, we still believe that good things 103 | are about to happen. Happy New Year to us. 104 | -------------------------------------------------------------------------------- /source/_posts/2023-06-18-simple-transaction.md: -------------------------------------------------------------------------------- 1 | --- 2 | key: 74 3 | title: 一种简单的事务实现 4 | tag: [design, lua] 5 | --- 6 | 在服务器编程中,事务往往是非常重要的,它的一个很重要的作用就是保证一系列操作的完整性。例如服务器处理某个请求要先后执行 a, b 两个修改操作,它们都有可能失败;如果 a 成功了但 b 失败了,事务会负责回滚 a 的修改。试想如果 a 操作是扣除余额,b 操作是发货,如果发货失败,钱就得退回去。如果服务器使用了支持事务的数据库系统,如 MySQL,事情就很好办。否则的话,实现类似的逻辑会比较棘手,也很容易犯错。 7 | 8 | 我希望有一种简单的事务系统,实现这样的效果:例如在下面的代码中,`handler` 函数处理业务逻辑。只要 `handler` 函数的任意位置抛出异常,那么 `handler` 中所有修改,无论是 `_G.DB.last_update_time`、`data.order` 还是 `data.money`,都将回滚。 9 | 10 | ```lua 11 | function handler(data) 12 | _G.DB.last_update_time = os.time() 13 | data.order_id = get_order_id() 14 | check_order(data) 15 | data.money = data.money - 10 16 | deliver(data) 17 | end 18 | ``` 19 | 20 | 因为我们的程序是单线程的,因此不用考虑事务隔离性之类的问题。所以这个所谓的“事务系统”只是一种自动回滚机制。 21 | 22 | 其实我在以前见过类似的事务实现。它的做法是将需要修改的数据(如上面的 `data`)存储两份,一份是正式数据,一份是暂存数据。业务代码修改暂存数据,如果没有抛出异常,则让暂存数据覆盖正式数据 (commit);否则让正式数据覆盖暂存数据 (rollback)。暂存数据只是正式数据的浅拷贝,即使是这样,内存开销仍然非常大。而且由于是浅拷贝,这种机制对引用类型(如 table)的字段无效。我认为这种做法并不够好。 23 | 24 | 最近我受到 *SICP* 4.3 节 Nondeterministic Computing 的启发,想到其实回滚数据很简单——再改回去就好了。我们在修改数据的时候记录下数据在修改之前的值,如果捕获到异常,就把对应的数据改回修改之前的值。我们从 `pcall` 开始动手: 25 | 26 | ```lua 27 | local original_pcall = pcall 28 | function pcall(f, ...) 29 | begin() 30 | local ok, res = original_pcall(f, ...) 31 | if ok then 32 | commit() 33 | else 34 | rollback() 35 | end 36 | return ok, res 37 | end 38 | ``` 39 | 40 | 由于 `pcall` 可以嵌套,i.e. `pcall(function() pcall(function() end) end)`,我们使用栈保存事务的上下文,在 `begin` 中压栈,`commit` 和 `rollback` 时弹出栈。因此栈顶就是当前事务的上下文。调用 `set` 执行修改操作,它会将数据的原始值保存在上下文中。 41 | 42 | ```lua 43 | local stack = {} 44 | 45 | local function begin() 46 | table.insert(stack, {}) 47 | end 48 | 49 | function set(tab, key, val) 50 | local top = stack[#stack] 51 | if top then 52 | table.insert(top, {tab, key, tab[key]}) 53 | end 54 | tab[key] = val 55 | end 56 | ``` 57 | 58 | Commit 时,当前事务的赋值操作全部生效,当前事务造成的副作用亦是上层事务的副作用,需要将当前事务记录的数据原始值移动到上层事务(如果有的话)的上下文中。回滚时,从后往前依次取出每次 `set` 操作的原始值,将数据设置成修改前的值,完成回滚操作。 59 | 60 | ```lua 61 | local function commit() 62 | local top = table.remove(stack) 63 | local pi = stack[#stack] 64 | if pi then 65 | for _, assign in ipairs(top) do 66 | table.insert(pi, assign) 67 | end 68 | end 69 | end 70 | 71 | local function rollback() 72 | local top = table.remove(stack) 73 | for i = #top, 1, -1 do 74 | local tab, key, val = table.unpack(top[i], 1, 3) 75 | tab[key] = val 76 | end 77 | end 78 | ``` 79 | 80 | 使用的时候不能直接赋值,需要调用 `set`。当然也可以做成元表,不过我不是很喜欢这样。 81 | 82 | ```lua 83 | val = {} 84 | function test() 85 | local old = val 86 | set(_G, 'val', 42) 87 | set(old, 1, 1) 88 | error() 89 | end 90 | 91 | pcall(test) -- false nil 92 | next(val) -- nil 93 | ``` 94 | 95 | 整个实现可以说是非常简单且行之有效,开销也并不大。代码是我随手写的,它还有优化空间:`stack` 中的旧数据存储可以使用更紧凑的数据结构;代码可以用 C 实现提高性能等。 -------------------------------------------------------------------------------- /source/_posts/2024-01-01-2023-annual-review.md: -------------------------------------------------------------------------------- 1 | --- 2 | key: 76 3 | title: 2023 Annual Review 4 | tag: [essays, english] 5 | banner_img: /assets/images/2023-annual-review-6.jpg 6 | --- 7 | 8 | In 2023 I spent most of my time on work, learning and dating. Compared with the last year, devoted less time on this blog and community. It might be a pretext but to be honest, writing a blog post always consumes a lot of my energy, especially when there was a lot of overtime this year. Anyway, I should write more posts in the new year, and I'll try to write some non-technical posts (well, partially because they're easy to write (and read)). 9 | 10 | Let's talk a little about professional skills over here. In the past, I focused most of my efforts on coding skill, rather than engineering skills, or let's say, the business skills. This is because I love the hacker spirit, and am fascinated by intricate program structures and algorithms. But your boss just need one who solve problems, them don't care about computer science. To solve a problem, you have to consider many things other than the computer - namely, the people around you. And that's exactly what software engineering does - to use some engineering methods to prevent or eliminate mistakes made by humans. So when I focus on hacking, I must keep an open mind on other skills, especially those methodologies for problem-solving. 11 | 12 | ## Goals 13 | 14 | - [x] Keep up learning English 15 | - [ ] Read *Understanding the Linux Kernel* (I'm not sure if I can finish it) 16 | - [x] Keep up daily LeetCode exercises 17 | - [ ] Keep up writing blogs, basically one post a month 18 | - [x] Read some non-technical books 19 | - [x] Keep up exercises 20 | 21 | ## Learning 22 | 23 | I finished learning *Structure and Interpretation of Computer Programs (SICP)*, an amazing book. Its content comprised of functional programming, layering of program and data structure, OOP, infinite streams, the metacircular evaluator, lazy evaluation, compilation principle, etc. I have to say, *SICP* opened the gate of computer science for me. Before then, I don't really comprehend the essential of computer science. 24 | 25 | ![sicp](/assets/images/2023-annual-review-2.jpg){width="350"} 26 | 27 | ## LeetCode 28 | 29 | I kept on doing LeetCode like the past few years. The grid looks not bad. 30 | 31 | ![leetcode](/assets/images/2023-annual-review-1.png) 32 | 33 | Now I've solved 1182 problems. Last year it's 912, so I solved 270 problems in 2023. 34 | 35 | ## Language 36 | 37 | I kept learning English in 2023, as I did in the past few years. After continuously learning vocabulary on the APP *baicizhan* for 2024 days, I found it might not be a very effective way to memorize new words for me at the moment. Therefore, In November, I started learning *Merriam-Webster's Vocabulary Builder*. This book organizes words by their roots, besides telling you how to use a word, its history, and related knowledge. 38 | 39 | ![webster](/assets/images/2023-annual-review-3.jpg){width="250"} 40 | 41 | I also read another amazing vocabulary builder, *Word Power Made Easy*. To me, this book like an introduction to the etymology and at the same time teaches you how to memorize over 3,000 words and continue building your vocabulary. 42 | 43 | ![webster](/assets/images/2023-annual-review-4.jpg){width="250"} 44 | 45 | In addition, I kept leaning Japanese on Duolingo as I did in 2022. 46 | 47 | ![webster](/assets/images/2023-annual-review-5.jpg){width="400"} 48 | 49 | ## Creating 50 | 51 | I only wrote 6 posts on the blog. 52 | 53 | I created a repo [luyuhuang/nvim](https://github.com/luyuhuang/nvim), but it's just for personal configuration, should not be considered as a contribution. I submitted some pull requests and issues to the community and some of have been accepted. 54 | 55 | ## Something Happy 56 | 57 | Hiking and seeing the skyline of the city at the summit is quite happy, especially with the one you love. 58 | 59 | ![webster](/assets/images/2023-annual-review-6.jpg){width="700"} 60 | 61 | 62 | ## Finally 63 | 64 | At the end of a year, I always feel time flies by quickly. But after I wrote the annual review, I found that the time of a year is quite long, because you can literally do many things in a year and grow quite a bit in certain aspects (say, I found my English writing skill is much better than the last year). So in the new year, keep learning and growing, and I believe we'll get a better result. Happy New Year to everybody. -------------------------------------------------------------------------------- /source/_posts/2024-06-08-republic.md: -------------------------------------------------------------------------------- 1 | --- 2 | key: 78 3 | title: 理想国 4 | math: true 5 | tag: [reading, philosophy] 6 | --- 7 | 我最近读完了《理想国》。这本书为古希腊哲学先贤柏拉图所著,是哲学经典之作、奠基之作,内容晦涩深奥,比较难懂。本人才疏学浅,以我的贫乏的哲学素养,难以完全领会其中深邃的思想。然而读完之后,仍然对我的价值观造成了不小的冲击。这里斗胆分享两点我的体会。 8 | 9 | ## 什么是正义 10 | 11 | 中国有句古话,叫做成王败寇。胜者为正义,败者为邪恶。蒙古铁蹄南下,击败南宋,占据中原,成为正统王朝,自然是正义。元末朱元璋起兵驱逐鞑虏,恢复中华,自然也是正义。但如果朱元璋失败了会怎样?自然是和其它千千万万失败农民起义一样,被列为乱臣贼子,成为邪恶的化身。正义与邪恶似乎是相对的概念,胜利者定义的价值即为正义。“邪不压正”不是规律,而是结果:正是因为甲战胜了乙,甲便是“正”;如果“邪”压住了“正”,“邪”就变成了正。 12 | 13 | 我曾经一度相信这种观点。然而柏拉图全然否定了这个观点。因为如果把胜利者定义为正义,反对胜利者定义为邪恶,我们可以作出这样的推导: 14 | 15 | 胜利者是正义的,所以胜利者制定的法律也是正义的。因此遵纪守法是正义的,违法犯罪是不正义的。 16 | 17 | 那么按照现行法律,谋杀是不正义的、抢劫是不正义的、盗窃是不正义的、贪污是不正义的、诈骗是不正义的。 18 | 19 | 与这些行为相关的品质也是不正义的。所以伤害他人是不正义的、损人利己是不正义的、贪婪是不正义的、奸诈狡猾是不正义的。 20 | 21 | 我们说一个贪婪、损人利己、奸诈狡猾的人是坏人,他会做坏事。个人的力量有限,他能做的坏事相对较小。我们试图让一群这样的坏人聚在一起,看看他们能不能做一件更大的坏事,甚至是最大的坏事:把正义——当前的胜利者——推翻? 22 | 23 | 然而他们会让我们失望的:一群贪婪、损人利己、奸诈狡猾的人,无论如何不能团结起来。越是不正义的个人,越不能团结成不正义的群体。因此不正义的人终究无法完成任何伟业。书中原话是这么说的: 24 | 25 | > 我们看到正义的人的确更聪明能干更好,而不正义的人根本不能合作。当我们说不正义者可以有坚强一致的行动,我们实在说得有点不对头。因为他们要是绝对违反正义,结果非内讧不可。他们残害敌人,而不至于自相残杀,还是因为他们之间多少还有点正义。就凭这么一点儿正义,才使他们做事好歹有点成果;而他们之间的不正义对他们的作恶也有相当的妨碍。因为绝对不正义的真正坏人,也就绝对做不出任何事情来。 26 | 27 | 因此我们发现,即使是黑帮,也要讲究道义,要求遵守纪律。他们能在一定程度上团结在一起,正是因为他们还有一些正义。蒙古能灭南宋,因为蒙古有一定的正义;而南宋,却冤杀岳飞,昏庸腐败,致使生灵涂炭,怎能说它没有不正义。而元朝末年亦是“人心离叛,天下兵起,使我中国之民,死者肝脑涂地,生者骨肉不相保”,也是它的不正义导致了灭亡。 28 | 29 | 所以无论怎么改朝换代,无论谁是胜利者,法律永远禁止谋杀、抢劫、盗窃、贪污和诈骗。所以邪不压正,是因为邪本来就不压正,这是规律,不是结果。 30 | 31 | 社会上流行这样的观点:好人和坏人是相对的,只是立场不同;小孩子才讲对错,大人只看利弊。但这种善恶相对的观点很危险。如果认为正义与邪恶是相对的概念,就不会相信真正的良善是存在的。人就可能会走邪路,成为自私自利、损人利己、为达目的不择手段的人。 32 | 33 | 为了探寻什么是正义,柏拉图构想了一个理想的城邦,一个“理想国”。在这个城邦里,不同的人分工合作,每个人在国家里执行一种最适合他天性的职务。工匠制作工具,农民种田,皮匠做鞋,以及“爱智慧的人[^1]”担任领导者。因为柏拉图认为只有智慧和理性才能让这个城邦在各种情况下做出最正确的决策。在智慧和理性的领导下,这个城邦训练勇敢的护卫者,用音乐和体操教化人民。这样的城邦是智慧的、理性的、勇敢的、节制的。一个这样的城邦便是正义的城邦。 34 | 35 | 柏拉图将人与城邦类比。一个城邦有形形色色的人,一个人内心也有不同的部分。一个人内心有受**理性**控制的部分,也有受**欲望**控制的非理性的部分,也有受**激情**控制的部分。柏拉图认为在这三部分中,理性的部分应该担任“领导者”,就像理性的人在应当在城邦担任领导者一样。 36 | 37 | > 理智既然是智慧的,是为整个心灵的利益而谋划的,还不应该由它起领导作用吗?激情不应该服从它和协助它吗? 38 | 39 | > 正义的人不许可自己灵魂里的各个部分相互干涉,起别的部分的作用。他应当安排好真正自己的事情,首先达到自己主宰自己,自身内秩序井然,对自己友善。 40 | 41 | > 不正义应该就是三种部分之间的争斗不和、相互间管闲事和相互干涉,灵魂的一个部分起而反对整个灵魂,企图在内部取得领导地位——它天生就不应该领导的而是应该像奴隶一样为统治部分服务的,——不是吗?我觉得我们要说的正是这种东西。 42 | 43 | 我们常说“做自己的主人”,柏拉图说人怎么才能做自己的主人呢?因为如果说一个人是自己的主人,那他同时也是自己的奴隶。他认为这句话的意思是,一个人内心理性的部分要做非理性部分的主人。理性会让人追求智慧,会在必要时抑制欲望与激情;在他需要战斗时,又会释放激情。这样,这个人便是智慧的、理性的、勇敢的、节制的。这样的人便是正义的人。 44 | 45 | ## 什么是快乐 46 | 47 | 我曾经认为,人的快乐不取决于人拥有多少物质,而取决于拥有的物质的变化。例如,假设你在路上捡到一千块钱,你会快乐,但仅限于得到这些钱的这一刻。你不会因为资产增加了一千元而一直开心。再比如年收入只有 10 万的时候会想,要是我一年能赚 20 万就好了。然而当收入真的变为 20 万时,他会发现快乐只存在于收入变化的这一小段时间,之后便开始追求更高的收入了。痛苦便与之相反:当人失去物质时,会感受到痛苦,但痛苦也仅存在于失去的这一刻。我甚至提出了一个“快乐公式”:函数 $f(t)$ 表示 $t$ 时刻人拥有的物质,那么 $t$ 时刻的快乐便是函数 $f$ 的导数 $f'(t)$。如果 $f'(t) > 0$ 则人是快乐的,反之则是痛苦的。也就是说快乐和痛苦是对比出来的。 48 | 49 | 然而柏拉图全然否定了这个观点。柏拉图认为世界上有快乐,也有痛苦;还有一种介于快乐和痛苦之间的平静的状态。把快乐、平静、痛苦比喻为上、中、下三级。但人在受到痛苦时会把摆脱痛苦称为快乐,然而摆脱痛苦实际是中间的平静的状态,并不是什么享受。例如生病的人会说,没有什么比健康更快乐的了。然而他们在生病之前并不曾觉得那是最大的快乐。同样,当一个人正在享乐,让他突然停止享乐,进入平静的状态,也是痛苦的。然而平静怎么可以既是快乐又是痛苦呢?这便推导出矛盾了。因此柏拉图认为这种对比出来的快乐不是真的快乐,只是快乐的影像。 50 | 51 | > 和痛苦对比的快乐以及和快乐对比的痛苦都是平静,不是真实的快乐和痛苦,而只是似乎快乐或痛苦。这些快乐的影像和真正的快乐毫无关系,都只是一种欺骗。 52 | 53 | 柏拉图认为,通过肉体上的享受得到的快乐,大多数属于“快乐的影像”,是某种意义上的脱离痛苦。例如人饥饿时吃食物会觉得无比的美味,这种快乐实际上是脱离痛苦。更进一步地,因**欲望**的满足而得到的快乐,大多属于“快乐的影像”。人对某事物有了欲望,求而不得,感到痛苦。欲望越强,越求而不得,就越痛苦,得到它时就越“快乐”。然而这种“快乐”某种意义上是痛苦的脱离,只是“快乐的影像”,不是真正的快乐。 54 | 55 | 柏拉图认为有一种真正的快乐,得到它之前不会感到痛苦,出现的时候能感受到强烈的快乐;停止之后也不留下痛苦。例如当你坚持学习,头脑变得越来越充实的时候;领悟了某个真理时那种”朝闻道,夕死可矣”的满足感。我还记得当时理解了 Y-Combinator,学会了用 CPS 变换实现 `call/cc` 的时候的那种兴奋感,真的能让人快乐好几天。柏拉图认为,用**理性**的部分追求智慧、美德得到的是**实在的东西**,而肉体上的享受是**不实在的东西**。让实在的东西填充内心才能得到可靠的真实的快乐。 56 | 57 | > 那么,没有经验过真实的人,他们对快乐、痛苦及这两者之中间状态的看法应该是不正确的,正如他们对许多别的事物的看法不正确那样。因此,当他们遭遇到痛苦时,他们就会认为自己处于痛苦之中,认为他们的痛苦是真实的。他们会深信,从痛苦转变到中间状态就能带来满足和快乐。而事实上,由于没有经验过真正的快乐,他们是错误地对比了痛苦和无痛苦。正如一个从未见过白色的人把灰色和黑色相比那样。 58 | 59 | > 因此,那些没有智慧和美德经验的人,只知聚在一起寻欢作乐,终身往返于我们所比喻的中下两级之间,从未再向上攀登看见和到达真正的最高一级境界,或为任何实在所满足,或体验到过任何可靠的纯粹的快乐。他们头向下眼睛看着宴席,就像牲畜俯首牧场只知吃草,雌雄交配一样。须知,**他们想用这些不实在的东西满足心灵的那个不实在的无法满足的部分是完全徒劳的**。由于不能满足,他们还像牲畜用犄角和蹄爪互相踢打顶撞一样地用铁的武器互相残杀。 60 | 61 | 柏拉图称肉体上的享受为**不实在的东西**,认为内心的欲望是**不实在的无法满足的部分**。现代科学在一定程度上支持柏拉图的观点。肉体上的享受得到的快乐来源于多巴胺。多巴胺能给人带来快乐,然而代价是当多巴胺消退时,人会感觉到空虚和痛苦,需要更大剂量的多巴胺才能弥补这些痛苦。于是人对多巴胺的追求永远不会满足,这个过程最终会变成一种折磨。而人在节制、自律的时候会分泌内啡肽,它给人带来的快乐是缓慢持续的;当它消退时也不会感到痛苦。自律和节制能给人带来更高级的快乐。这也是罗翔所说的,低级的快乐来自放纵,高级的快乐来自克制。 62 | 63 | 我过去提出的那个“快乐公式”只适用于通过多巴胺获取的快乐。也就是柏拉图所说的,往返于中下两级之间,从平静和痛苦之间感受快乐的影像。 64 | 65 | 最后柏拉图认为正义的人是真正快乐的,因为他们是**理性**的、节制的。他们会追求智慧,追求真理,这其中得到的快乐要胜与满足欲望得到的快乐。一个极端不正义的人**欲望**会无限膨胀,理性成为欲望的奴隶,他永远无法满足,给他再多的物质也得不到快乐。 66 | 67 | [^1]: 哲学家 philosopher 字面意思为“爱智慧的人”。philo- 爱,sophia 智慧。 -------------------------------------------------------------------------------- /source/about/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: About 3 | layout: about 4 | lazyload: false 5 | --- 6 | Hi there, my name is Luyu Huang. I'm a back-end programmer for game development. I have been working in the Chinese game industry since I graduated from my university in 2017. I worked at Tencent IEG and Perfect World. I mainly use C++ and Lua, but I also use many other languages such as Go, Python, JavaScript, etc. I like video games, anime, playing the piano, and drawing. Moreover, I like programming, even though it's a part of my job. 7 | 8 | Here's my blog, mainly about programming technology. I'll write posts about programming, algorithms, mathematics, etc as a summary of my learning and work experience. If you found they're helpful, feel free to let me know and I'll be happy; if you find a mistake in my posts, I would appreciate it if you point it out. 9 | 10 | ### Works 11 | 12 | I wrote some open source software in spare time. 13 | 14 | - [VSCode-RSS](https://github.com/luyuhuang/vscode-rss) ![vscode-rss](https://img.shields.io/github/stars/luyuhuang/vscode-rss?style=social) An RSS reader embedded in Visual Studio Code 15 | - [DWords2](https://github.com/luyuhuang/DWords2) ![DWords2](https://img.shields.io/github/stars/luyuhuang/DWords2?style=social) Show words as Danmaku on the screen to help you memorize them 16 | - [Subsocks](https://github.com/luyuhuang/subsocks) ![subsocks](https://img.shields.io/github/stars/luyuhuang/subsocks?style=social) A Socks5 proxy that encapsulates Socks5 in other security protocols 17 | 18 | -------------------------------------------------------------------------------- /source/assets/images/2019-annual-summary_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/images/2019-annual-summary_1.jpg -------------------------------------------------------------------------------- /source/assets/images/2019-annual-summary_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/images/2019-annual-summary_2.png -------------------------------------------------------------------------------- /source/assets/images/2019-annual-summary_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/images/2019-annual-summary_3.png -------------------------------------------------------------------------------- /source/assets/images/2019-annual-summary_4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/images/2019-annual-summary_4.jpg -------------------------------------------------------------------------------- /source/assets/images/2020-annual-summary_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/images/2020-annual-summary_1.png -------------------------------------------------------------------------------- /source/assets/images/2020-annual-summary_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/images/2020-annual-summary_2.png -------------------------------------------------------------------------------- /source/assets/images/2020-annual-summary_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/images/2020-annual-summary_3.png -------------------------------------------------------------------------------- /source/assets/images/2020-annual-summary_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/images/2020-annual-summary_4.png -------------------------------------------------------------------------------- /source/assets/images/2020-annual-summary_5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/images/2020-annual-summary_5.jpg -------------------------------------------------------------------------------- /source/assets/images/2020-annual-summary_6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/images/2020-annual-summary_6.jpg -------------------------------------------------------------------------------- /source/assets/images/2021-annual-summary_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/images/2021-annual-summary_1.png -------------------------------------------------------------------------------- /source/assets/images/2021-annual-summary_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/images/2021-annual-summary_2.png -------------------------------------------------------------------------------- /source/assets/images/2021-annual-summary_3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/images/2021-annual-summary_3.jpg -------------------------------------------------------------------------------- /source/assets/images/2021-annual-summary_4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/images/2021-annual-summary_4.jpg -------------------------------------------------------------------------------- /source/assets/images/2022-annual-summary_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/images/2022-annual-summary_1.png -------------------------------------------------------------------------------- /source/assets/images/2022-annual-summary_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/images/2022-annual-summary_2.png -------------------------------------------------------------------------------- /source/assets/images/2022-annual-summary_3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/images/2022-annual-summary_3.jpg -------------------------------------------------------------------------------- /source/assets/images/2022-annual-summary_4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/images/2022-annual-summary_4.jpg -------------------------------------------------------------------------------- /source/assets/images/2022-annual-summary_5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/images/2022-annual-summary_5.png -------------------------------------------------------------------------------- /source/assets/images/2022-annual-summary_6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/images/2022-annual-summary_6.jpg -------------------------------------------------------------------------------- /source/assets/images/2022-annual-summary_7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/images/2022-annual-summary_7.png -------------------------------------------------------------------------------- /source/assets/images/2023-annual-review-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/images/2023-annual-review-1.png -------------------------------------------------------------------------------- /source/assets/images/2023-annual-review-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/images/2023-annual-review-2.jpg -------------------------------------------------------------------------------- /source/assets/images/2023-annual-review-3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/images/2023-annual-review-3.jpg -------------------------------------------------------------------------------- /source/assets/images/2023-annual-review-4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/images/2023-annual-review-4.jpg -------------------------------------------------------------------------------- /source/assets/images/2023-annual-review-5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/images/2023-annual-review-5.jpg -------------------------------------------------------------------------------- /source/assets/images/2023-annual-review-6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/images/2023-annual-review-6.jpg -------------------------------------------------------------------------------- /source/assets/images/asan_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/images/asan_1.png -------------------------------------------------------------------------------- /source/assets/images/asan_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/images/asan_2.png -------------------------------------------------------------------------------- /source/assets/images/asan_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/images/asan_3.png -------------------------------------------------------------------------------- /source/assets/images/asan_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/images/asan_4.png -------------------------------------------------------------------------------- /source/assets/images/beginners-guide_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/images/beginners-guide_1.png -------------------------------------------------------------------------------- /source/assets/images/character-encoding_2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/images/character-encoding_2.gif -------------------------------------------------------------------------------- /source/assets/images/cloudflare_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/images/cloudflare_1.png -------------------------------------------------------------------------------- /source/assets/images/cloudflare_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/images/cloudflare_2.png -------------------------------------------------------------------------------- /source/assets/images/cloudflare_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/images/cloudflare_3.png -------------------------------------------------------------------------------- /source/assets/images/cloudflare_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/images/cloudflare_4.png -------------------------------------------------------------------------------- /source/assets/images/dc-dp-ga_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/images/dc-dp-ga_1.png -------------------------------------------------------------------------------- /source/assets/images/dc-dp-ga_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/images/dc-dp-ga_2.png -------------------------------------------------------------------------------- /source/assets/images/dc-dp-ga_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/images/dc-dp-ga_3.png -------------------------------------------------------------------------------- /source/assets/images/dc-dp-ga_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/images/dc-dp-ga_4.png -------------------------------------------------------------------------------- /source/assets/images/dc-dp-ga_5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/images/dc-dp-ga_5.png -------------------------------------------------------------------------------- /source/assets/images/dht-and-p2p_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/images/dht-and-p2p_1.png -------------------------------------------------------------------------------- /source/assets/images/dht-and-p2p_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/images/dht-and-p2p_2.png -------------------------------------------------------------------------------- /source/assets/images/dht-and-p2p_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/images/dht-and-p2p_3.png -------------------------------------------------------------------------------- /source/assets/images/dht-and-p2p_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/images/dht-and-p2p_4.png -------------------------------------------------------------------------------- /source/assets/images/dht-and-p2p_5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/images/dht-and-p2p_5.png -------------------------------------------------------------------------------- /source/assets/images/dht-and-p2p_6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/images/dht-and-p2p_6.png -------------------------------------------------------------------------------- /source/assets/images/dht-and-p2p_7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/images/dht-and-p2p_7.png -------------------------------------------------------------------------------- /source/assets/images/dht-and-p2p_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/images/dht-and-p2p_8.png -------------------------------------------------------------------------------- /source/assets/images/dht-and-p2p_9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/images/dht-and-p2p_9.png -------------------------------------------------------------------------------- /source/assets/images/dwords2_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/images/dwords2_1.png -------------------------------------------------------------------------------- /source/assets/images/dwords2_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/images/dwords2_2.png -------------------------------------------------------------------------------- /source/assets/images/dwords2_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/images/dwords2_3.png -------------------------------------------------------------------------------- /source/assets/images/dwords_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/images/dwords_1.png -------------------------------------------------------------------------------- /source/assets/images/edit-distance_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/images/edit-distance_1.png -------------------------------------------------------------------------------- /source/assets/images/gperftools_1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/images/gperftools_1.gif -------------------------------------------------------------------------------- /source/assets/images/gperftools_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/images/gperftools_2.png -------------------------------------------------------------------------------- /source/assets/images/gperftools_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/images/gperftools_3.png -------------------------------------------------------------------------------- /source/assets/images/gzip-and-deflate_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/images/gzip-and-deflate_1.png -------------------------------------------------------------------------------- /source/assets/images/gzip-and-deflate_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/images/gzip-and-deflate_2.png -------------------------------------------------------------------------------- /source/assets/images/gzip-and-deflate_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/images/gzip-and-deflate_3.png -------------------------------------------------------------------------------- /source/assets/images/gzip-and-deflate_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/images/gzip-and-deflate_4.png -------------------------------------------------------------------------------- /source/assets/images/harmonic-series_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/images/harmonic-series_1.png -------------------------------------------------------------------------------- /source/assets/images/lock-free-queue_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/images/lock-free-queue_4.png -------------------------------------------------------------------------------- /source/assets/images/nvim_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/images/nvim_1.png -------------------------------------------------------------------------------- /source/assets/images/nvim_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/images/nvim_2.png -------------------------------------------------------------------------------- /source/assets/images/nvim_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/images/nvim_3.png -------------------------------------------------------------------------------- /source/assets/images/pass-fd-over-domain-socket_1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/images/pass-fd-over-domain-socket_1.gif -------------------------------------------------------------------------------- /source/assets/images/pathfinding-gen-graph_1.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/images/pathfinding-gen-graph_1.jpeg -------------------------------------------------------------------------------- /source/assets/images/pathfinding-gen-graph_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/images/pathfinding-gen-graph_2.png -------------------------------------------------------------------------------- /source/assets/images/pathfinding-gen-graph_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/images/pathfinding-gen-graph_3.png -------------------------------------------------------------------------------- /source/assets/images/pathfinding-gen-graph_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/images/pathfinding-gen-graph_4.png -------------------------------------------------------------------------------- /source/assets/images/pathfinding-gen-graph_5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/images/pathfinding-gen-graph_5.png -------------------------------------------------------------------------------- /source/assets/images/pathfinding-gen-graph_6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/images/pathfinding-gen-graph_6.png -------------------------------------------------------------------------------- /source/assets/images/pathfinding-gen-graph_7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/images/pathfinding-gen-graph_7.png -------------------------------------------------------------------------------- /source/assets/images/pathfinding-gen-graph_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/images/pathfinding-gen-graph_8.png -------------------------------------------------------------------------------- /source/assets/images/pathfinding-gen-graph_9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/images/pathfinding-gen-graph_9.png -------------------------------------------------------------------------------- /source/assets/images/pathfinding-graph-search_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/images/pathfinding-graph-search_1.png -------------------------------------------------------------------------------- /source/assets/images/pathfinding-graph-search_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/images/pathfinding-graph-search_2.png -------------------------------------------------------------------------------- /source/assets/images/pathfinding-graph-search_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/images/pathfinding-graph-search_3.png -------------------------------------------------------------------------------- /source/assets/images/pathfinding-graph-search_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/images/pathfinding-graph-search_4.png -------------------------------------------------------------------------------- /source/assets/images/pathfinding-graph-search_5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/images/pathfinding-graph-search_5.png -------------------------------------------------------------------------------- /source/assets/images/promise-and-deferred_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/images/promise-and-deferred_1.png -------------------------------------------------------------------------------- /source/assets/images/promise-and-deferred_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/images/promise-and-deferred_2.png -------------------------------------------------------------------------------- /source/assets/images/promise-and-deferred_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/images/promise-and-deferred_3.png -------------------------------------------------------------------------------- /source/assets/images/raspberry-nas_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/images/raspberry-nas_1.jpg -------------------------------------------------------------------------------- /source/assets/images/raspberry-nas_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/images/raspberry-nas_2.png -------------------------------------------------------------------------------- /source/assets/images/raspberry-nas_3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/images/raspberry-nas_3.jpg -------------------------------------------------------------------------------- /source/assets/images/raspberry-nas_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/images/raspberry-nas_4.png -------------------------------------------------------------------------------- /source/assets/images/regions-cut-by-slashes_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/images/regions-cut-by-slashes_1.png -------------------------------------------------------------------------------- /source/assets/images/regions-cut-by-slashes_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/images/regions-cut-by-slashes_2.png -------------------------------------------------------------------------------- /source/assets/images/regions-cut-by-slashes_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/images/regions-cut-by-slashes_3.png -------------------------------------------------------------------------------- /source/assets/images/regions-cut-by-slashes_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/images/regions-cut-by-slashes_4.png -------------------------------------------------------------------------------- /source/assets/images/regions-cut-by-slashes_5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/images/regions-cut-by-slashes_5.png -------------------------------------------------------------------------------- /source/assets/images/regions-cut-by-slashes_7.svg: -------------------------------------------------------------------------------- 1 |
0
0
1
1
1
1
0
0
Viewer does not support full SVG 1.1
-------------------------------------------------------------------------------- /source/assets/images/scheme-lang_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/images/scheme-lang_1.png -------------------------------------------------------------------------------- /source/assets/images/scheme-lang_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/images/scheme-lang_2.png -------------------------------------------------------------------------------- /source/assets/images/scheme-lang_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/images/scheme-lang_3.png -------------------------------------------------------------------------------- /source/assets/images/service-migration_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/images/service-migration_1.png -------------------------------------------------------------------------------- /source/assets/images/sqrt_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/images/sqrt_1.png -------------------------------------------------------------------------------- /source/assets/images/sqrt_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/images/sqrt_2.png -------------------------------------------------------------------------------- /source/assets/images/sudoku-solution_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/images/sudoku-solution_1.png -------------------------------------------------------------------------------- /source/assets/images/sudoku-solution_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/images/sudoku-solution_2.png -------------------------------------------------------------------------------- /source/assets/images/sudoku-solution_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/images/sudoku-solution_3.png -------------------------------------------------------------------------------- /source/assets/images/sudoku-solution_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/images/sudoku-solution_4.png -------------------------------------------------------------------------------- /source/assets/images/three-maximum-area-problems_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/images/three-maximum-area-problems_1.png -------------------------------------------------------------------------------- /source/assets/images/three-maximum-area-problems_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/images/three-maximum-area-problems_2.png -------------------------------------------------------------------------------- /source/assets/images/three-maximum-area-problems_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/images/three-maximum-area-problems_3.png -------------------------------------------------------------------------------- /source/assets/images/three-maximum-area-problems_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/images/three-maximum-area-problems_4.png -------------------------------------------------------------------------------- /source/assets/images/three-maximum-area-problems_5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/images/three-maximum-area-problems_5.png -------------------------------------------------------------------------------- /source/assets/images/three-maximum-area-problems_6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/images/three-maximum-area-problems_6.png -------------------------------------------------------------------------------- /source/assets/images/three-maximum-area-problems_7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/images/three-maximum-area-problems_7.png -------------------------------------------------------------------------------- /source/assets/images/tree-dp_1.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /source/assets/images/tree-dp_2.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /source/assets/images/tree-dp_3.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /source/assets/images/tree-dp_4.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/images/tree-dp_4.jpeg -------------------------------------------------------------------------------- /source/assets/images/tree-dp_4.svg: -------------------------------------------------------------------------------- 1 |
即使是边缘节点, 也依赖于其相邻节点
即使是边缘节点, 也依赖于其相邻节点
Viewer does not support full SVG 1.1
-------------------------------------------------------------------------------- /source/assets/images/tree-dp_6.svg: -------------------------------------------------------------------------------- 1 |
ip
ip
i
i
Viewer does not support full SVG 1.1
-------------------------------------------------------------------------------- /source/assets/images/unreliable-tcp-connections_1.svg: -------------------------------------------------------------------------------- 1 |
sender
sender
receiver
receiver
PSH
PSH
ACK
ACK
Viewer does not support full SVG 1.1
-------------------------------------------------------------------------------- /source/assets/images/use-coroutines-to-process-time-consuming-procedures_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/images/use-coroutines-to-process-time-consuming-procedures_1.png -------------------------------------------------------------------------------- /source/assets/images/vscode-rss_1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/images/vscode-rss_1.gif -------------------------------------------------------------------------------- /source/assets/images/zookeeper_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/images/zookeeper_1.jpg -------------------------------------------------------------------------------- /source/assets/images/zookeeper_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/images/zookeeper_2.jpg -------------------------------------------------------------------------------- /source/assets/videos/dwords2_1.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/videos/dwords2_1.mp4 -------------------------------------------------------------------------------- /source/assets/videos/dwords2_2.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/videos/dwords2_2.mp4 -------------------------------------------------------------------------------- /source/assets/videos/dwords2_3.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/videos/dwords2_3.mp4 -------------------------------------------------------------------------------- /source/assets/videos/nvim_1.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/videos/nvim_1.mp4 -------------------------------------------------------------------------------- /source/assets/videos/nvim_2.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/videos/nvim_2.mp4 -------------------------------------------------------------------------------- /source/assets/videos/nvim_3.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/videos/nvim_3.mp4 -------------------------------------------------------------------------------- /source/assets/videos/nvim_4.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/videos/nvim_4.mp4 -------------------------------------------------------------------------------- /source/assets/videos/tab-to-search_1.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/videos/tab-to-search_1.mp4 -------------------------------------------------------------------------------- /source/assets/videos/tab-to-search_2.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/assets/videos/tab-to-search_2.mp4 -------------------------------------------------------------------------------- /source/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/favicon.ico -------------------------------------------------------------------------------- /source/google1c9d1155cc80282c.html: -------------------------------------------------------------------------------- 1 | google-site-verification: google1c9d1155cc80282c.html -------------------------------------------------------------------------------- /source/img/avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/img/avatar.png -------------------------------------------------------------------------------- /source/img/default-avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/img/default-avatar.png -------------------------------------------------------------------------------- /source/img/default.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/img/default.jpg -------------------------------------------------------------------------------- /source/img/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuhuang/luyuhuang.github.io/a74e3ec6fbec3bc17caf2c68f916a3a7b6b65546/source/img/favicon-32x32.png -------------------------------------------------------------------------------- /source/links/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | key: page-friends 3 | layout: links 4 | title: links 5 | --- 6 | -------------------------------------------------------------------------------- /source/robots.txt: -------------------------------------------------------------------------------- 1 | Sitemap: https://luyuhuang.tech/sitemap.xml 2 | --------------------------------------------------------------------------------