跳到主要内容

获取机器 IP 地址的命令

· 阅读需 2 分钟
ip a  | grep 'inet ' | grep eth0 | cut -d/ -f 1 | awk '{print $2}'

这个命令将输出分配给机器的 eth0 网络接口的 IP 地址。

以下是该命令的每个部分的详细说明:

  • ip a:该命令显示有关机器上所有网络接口的信息。
  • grep 'inet ':该命令过滤前一个命令的输出,仅显示包含单词 "inet" 的行,该单词指示 IP 地址。
  • grep eth0:该命令进一步过滤输出,仅显示包含 "eth0" 的行,这是特定网络接口的名称。
  • cut -d/ -f 1:该命令使用 "/" 字符作为分隔符,并从过滤后的输出中选择第一个字段(即 IP 地址)。
  • awk '{print $2}':该命令打印输出的第二个字段,即 IP 地址本身。

About

· 阅读需 1 分钟

Hi, I'm Zhao Wang, a software development engineer.

Education Background

Work Experience

  • 2019.07 ~ 2019.09
    • Intern Software Development Engineer, Hulu LLC., Beijing, China
  • 2020.07~2023.07
    • Software Development Engineer, Megvii Inc., Beijing, China
  • 2023.10 ~ Present

Voyager —— 使用基于 LLM 驱动的 Agent 探索 Minecraft

· 阅读需 17 分钟

官方网址:https://voyager.minedojo.org/

用基于 LLM 的 Agent,通过“提问→求解→技能池”循环来不断探索游戏内容。和 SOTA 比,建造物品的数量是 3.3 倍、探索范围是 2.3 倍、解锁关键科技结点的速度是 15.3 倍。

关键概念

系统整体逻辑循环如下:

  • Automatic Curriculum: Curriculum Agent —— 提出目标
  • Iterative Prompting Mechanism: Action Agent —— 实现目标
    • Self-Verification: Critic Agent —— 检查是否完成目标
  • Skill Library: Skill Manager —— 技能库

使用方式

from voyager import Voyager

azure_login = ...
openai_api_key = ...

voyager = Voyager(
azure_login=azure_login,
openai_api_key=openai_api_key,
)

# start lifelong learning
voyager.learn()

系统循环

https://github.com/MineDojo/Voyager/blob/01fb04666a8f3ba47dec74fb4cfd46e0125fe5a0/voyager/voyager.py

  • __init__: 定义上述 3 个 Agent (Action, Curriculum, Critic),1 个 Manager (Skill Manager)
# init agents
self.action_agent = ActionAgent(
model_name=action_agent_model_name, # gpt-4
...
)
self.curriculum_agent = CurriculumAgent(
model_name=curriculum_agent_model_name, # gpt-4
temperature=curriculum_agent_temperature,
qa_model_name=curriculum_agent_qa_model_name, # gpt-3.5-turbo
qa_temperature=curriculum_agent_qa_temperature,
...
)
self.critic_agent = CriticAgent(
model_name=critic_agent_model_name, # gpt-4
...
)
self.skill_manager = SkillManager(
model_name=skill_manager_model_name, # gpt-3.5-turbo
temperature=skill_manager_temperature,
retrieval_top_k=skill_manager_retrieval_top_k,
...
)
  • learn: 学习循环,提出任务 → 尝试完成任务 → 记录新技能
while True:
if self.recorder.iteration > self.max_iterations:
print("Iteration limit reached")
break
task, context = self.curriculum_agent.propose_next_task( # 提议下一个任务
events=self.last_events,
chest_observation=self.action_agent.render_chest_observation(),
max_retries=5,
)
...
try:
messages, reward, done, info = self.rollout( # 尝试完成任务
task=task,
context=context,
reset_env=reset_env,
)
except Exception as e:
...

if info["success"]:
self.skill_manager.add_new_skill(info) # 如果成功,则记录新技能

self.curriculum_agent.update_exploration_progress(info) # 更新探索进度
...
  • rollout: 完成任务,准备完毕后最多尝试执行 N=4 次
    • reset: 准备工作
      • 使用 Skill Manager 获取与当前上下文匹配的技能信息,准备 Action Agent 的人设、提问等
    • step: 执行一次完成任务的尝试
      • 调用 Action Agent 背后的 LLM 得到 bot 的指令代码
      • 尝试使用 MC 插件运行代码,解析结果
      • 调用 Critic Agent 背后的 LLM 检查任务是否完成
      • 使用 Skill Manager 更新与当前上下文匹配的技能信息,准备 Action Agent 的人设、提问等
def rollout(self, *, task, context, reset_env=True):
self.reset(task=task, context=context, reset_env=reset_env) # 准备工作:获取技能信息、准备 Action Agent 的人设、提问等
while True:
messages, reward, done, info = self.step() # 尝试一次
if done:
break
return messages, reward, done, info

def reset(self, task, context="", reset_env=True):
...
skills = self.skill_manager.retrieve_skills(query=self.context) # 获取相关技能
...
# 生成 Action Agent 所需的系统设定和提问消息
system_message = self.action_agent.render_system_message(skills=skills)
human_message = self.action_agent.render_human_message(
events=events, code="", task=self.task, context=context, critique=""
)
self.messages = [system_message, human_message]
...
return self.messages

def step(self):
...
ai_message = self.action_agent.llm(self.messages) # Action Agent 生成代码
...
self.conversations.append(
(self.messages[0].content, self.messages[1].content, ai_message.content)
)
parsed_result = self.action_agent.process_ai_message(message=ai_message) # 处理生成的 js 代码
success = False
if isinstance(parsed_result, dict):
code = parsed_result["program_code"] + "\n" + parsed_result["exec_code"]
events = self.env.step( # 运行处理好的 js 代码
code,
programs=self.skill_manager.programs,
)
...
success, critique = self.critic_agent.check_task_success( # 检查任务是否完成
...
)

if self.reset_placed_if_failed and not success:
...
# 根据当前上下文重新检索技能
new_skills = self.skill_manager.retrieve_skills(
query=self.context
+ "\n\n"
+ self.action_agent.summarize_chatlog(events) # 从代码运行结果中手动提取事件
)
# 更新 Action Agent 的系统设定和提问信息
system_message = self.action_agent.render_system_message(skills=new_skills)
human_message = self.action_agent.render_human_message(
events=events,
code=parsed_result["program_code"],
task=self.task,
context=self.context,
critique=critique,
)
self.last_events = copy.deepcopy(events)
self.messages = [system_message, human_message]
else:
...
...
done = ( # 超过重试次数,算失败
self.action_agent_rollout_num_iter >= self.action_agent_task_max_retries
or success
)
...
return self.messages, 0, done, info

Curriculum Agent

https://github.com/MineDojo/Voyager/blob/01fb04666a8f3ba47dec74fb4cfd46e0125fe5a0/voyager/agents/curriculum.py

  • __init__: 定义 LLM, warm_up schedule, qa_cache vectordb 等
...
from langchain.chat_models import ChatOpenAI
from langchain.embeddings.openai import OpenAIEmbeddings
...
from langchain.vectorstores import Chroma

class CurriculumAgent:
def __init__(
self,
...
):
self.llm = ChatOpenAI(
model_name=model_name,
temperature=temperature,
request_timeout=request_timout,
)
self.qa_llm = ChatOpenAI(
model_name=qa_model_name,
temperature=qa_temperature,
request_timeout=request_timout,
)
...
# vectordb for qa cache
self.qa_cache_questions_vectordb = Chroma(
collection_name="qa_cache_questions_vectordb",
embedding_function=OpenAIEmbeddings(),
persist_directory=f"{ckpt_dir}/curriculum/vectordb",
)
...
# if warm up not defined, initialize it as a dict, else, initialize all the missing value as a default value
if not warm_up:
warm_up = self.default_warmup
self.warm_up = {}
...
  • propose_next_task: 提议下个任务
    • 硬编码:首个任务必须是砍树
    • 硬编码:当身上物品太多时,尝试造储物箱,并存储身上的物品
    • 否则,调用 LLM 来决定下个任务是什么
def propose_next_task(self, *, events, chest_observation, max_retries=5):
if self.progress == 0 and self.mode == "auto": # 硬编码,首个任务是砍树
task = "Mine 1 wood log"
...
return task, context

# hard code task when inventory is almost full
inventoryUsed = events[-1][1]["status"]["inventoryUsed"]
if inventoryUsed >= 33: # 硬编码,当身上物品太多时,造储物箱放东西
if chest_observation != "Chests: None\n\n": # 发现地上有储物箱,直接存物品
chests = chest_observation[8:-2].split("\n")
for chest in chests:
content = chest.split(":")[1]
if content == " Unknown items inside" or content == " Empty":
position = chest.split(":")[0]
task = f"Deposit useless items into the chest at {position}"
...
return task, context
if "chest" in events[-1][1]["inventory"]: # 如果身上有储物箱,则放置之
task = "Place a chest"
...
else: # 上述条件都不满足,就搓一个储物箱
task = "Craft 1 chest"
...
return task, context

messages = [
self.render_system_message(),
self.render_human_message(
events=events, chest_observation=chest_observation
),
]

if self.mode == "auto":
return self.propose_next_ai_task(messages=messages, max_retries=max_retries)
...

def propose_next_ai_task(self, *, messages, max_retries=5):
curriculum = self.llm(messages).content # 调用 LLM
...
try:
response = self.parse_ai_message(curriculum) # 尝试从结果中提取任务描述
...
context = self.get_task_context(response["next_task"]) # LLM 给出任务的解决方案
...
return response["next_task"], context
except Exception as e:
...
return self.propose_next_ai_task(
messages=messages,
max_retries=max_retries - 1,
)

def get_task_context(self, task):
# if include ore in question, gpt will try to use tool with skill touch enhancement to mine
question = (
f"How to {task.replace('_', ' ').replace(' ore', '').replace(' ores', '').replace('.', '').strip().lower()}"
f" in Minecraft?"
)
if question in self.qa_cache:
answer = self.qa_cache[question]
else:
answer = self.run_qa_step2_answer_questions(question=question)
...
context = f"Question: {question}\n{answer}"
return context
  • render_system_message: 定义人设
  • render_human_message: 设计提问信息
    • 首先从插件获取角色信息、周边环境的信息备用
    • 15 个 iteration 后,激活追加提问,最多 5 次
    • warmup 机制
      • 对于每一类环境信息,都需要在经过一定 iteration 后才能稳定在提问信息中出现,在这之前按 80% 概率出现
      • 目的是确保学习过程从基础技能开始逐步走向进阶、复杂的技能

def render_human_message(self, *, events, chest_observation):
content = ""
observation = self.render_observation( # 获取角色、周边环境信息
events=events, chest_observation=chest_observation
)
if self.progress >= self.warm_up["context"]: # 15 个 iter 后进行追加提问,最多 5 次
questions, answers = self.run_qa(
events=events, chest_observation=chest_observation
)
i = 1
for question, answer in zip(questions, answers):
if "Answer: Unknown" in answer or "language model" in answer:
continue
observation["context"] += f"Question {i}: {question}\n"
observation["context"] += f"{answer}\n\n"
i += 1
if i > 5:
break

for key in self.curriculum_observations: # warmup 机制
if self.progress >= self.warm_up[key]:
if self.warm_up[key] != 0:
should_include = random.random() < 0.8
else:
should_include = True
if should_include:
content += observation[key]

...
return HumanMessage(content=content)
def run_qa(self, *, events, chest_observation):
questions_new, _ = self.run_qa_step1_ask_questions(
events=events, chest_observation=chest_observation
)
questions = []
answers = []
for question in questions_new:
if self.qa_cache_questions_vectordb._collection.count() > 0:
docs_and_scores = (
self.qa_cache_questions_vectordb.similarity_search_with_score(
question, k=1
)
)
if docs_and_scores and docs_and_scores[0][1] < 0.05:
...
continue
answer = self.run_qa_step2_answer_questions(question=question)
...
...
return questions, answers

Action Agent

https://github.com/MineDojo/Voyager/blob/01fb04666a8f3ba47dec74fb4cfd46e0125fe5a0/voyager/agents/action.py

  • __init__: 定义 LLM
...
from langchain.chat_models import ChatOpenAI
...

class ActionAgent:
def __init__(
self,
...
):
...
self.llm = ChatOpenAI(
model_name=model_name,
temperature=temperature,
request_timeout=request_timout,
)
def render_system_message(self, skills=[]):
system_template = load_prompt("action_template") # 载入 action_template.txt,说明各个接口的用法,以及规定使用方式
# FIXME: Hardcoded control_primitives
base_skills = [
"exploreUntil",
"mineBlock",
"craftItem",
"placeItem",
"smeltItem",
"killMob",
]
if not self.llm.model_name == "gpt-3.5-turbo":
base_skills += [
"useChest",
"mineflayer",
]
programs = "\n\n".join(load_control_primitives_context(base_skills) + skills) # 载入 API 代码
response_format = load_prompt("action_response_format") # 载入 action_response_format.txt,规定回复方式
system_message_prompt = SystemMessagePromptTemplate.from_template(
system_template
)
system_message = system_message_prompt.format(
programs=programs, response_format=response_format
)
assert isinstance(system_message, SystemMessage)
return system_message
  • render_human_message: 从游戏中获取角色自身和附近信息、上下文等
  • summarize_chatlog: 手动匹配上轮代码运行结果中的问题
def summarize_chatlog(self, events):
def filter_item(message: str):
craft_pattern = r"I cannot make \w+ because I need: (.*)"
craft_pattern2 = (
r"I cannot make \w+ because there is no crafting table nearby"
)
mine_pattern = r"I need at least a (.*) to mine \w+!"
if re.match(craft_pattern, message):
return re.match(craft_pattern, message).groups()[0]
elif re.match(craft_pattern2, message):
return "a nearby crafting table"
elif re.match(mine_pattern, message):
return re.match(mine_pattern, message).groups()[0]
else:
return ""

chatlog = set()
for event_type, event in events:
if event_type == "onChat":
item = filter_item(event["onChat"])
if item:
chatlog.add(item)
return "I also need " + ", ".join(chatlog) + "." if chatlog else ""

Critic Agent

def ai_check_task_success(self, messages, max_retries=5):
...
critic = self.llm(messages).content
print(f"\033[31m****Critic Agent ai message****\n{critic}\033[0m")
try:
response = fix_and_parse_json(critic) # 解析 LLM 返回的 json 数据
assert response["success"] in [True, False]
if "critique" not in response:
response["critique"] = ""
return response["success"], response["critique"]
except Exception as e:
print(f"\033[31mError parsing critic response: {e} Trying again!\033[0m")
return self.ai_check_task_success(
messages=messages,
max_retries=max_retries - 1,
)

def check_task_success(
self, *, events, task, context, chest_observation, max_retries=5
):
human_message = self.render_human_message(
events=events,
task=task,
context=context,
chest_observation=chest_observation,
)

messages = [
self.render_system_message(),
human_message,
]

if self.mode == "manual":
return self.human_check_task_success() # 人手动判断
elif self.mode == "auto":
return self.ai_check_task_success( # LLM 判断
messages=messages, max_retries=max_retries
)
else:
raise ValueError(f"Invalid critic agent mode: {self.mode}")

Skill Manager

  • __init__: 定义 LLM & vectordb
from langchain.chat_models import ChatOpenAI
from langchain.embeddings.openai import OpenAIEmbeddings
from langchain.vectorstores import Chroma

class SkillManager:
def __init__(
self,
...
):
self.llm = ChatOpenAI(
model_name=model_name,
temperature=temperature,
request_timeout=request_timout,
)
...
self.vectordb = Chroma(
collection_name="skill_vectordb",
embedding_function=OpenAIEmbeddings(),
persist_directory=f"{ckpt_dir}/skill/vectordb",
)
...
  • add_new_skill: 添加新技能
    • 先试用 LLM 生成技能说明
    • 再检查 vectordb 中是否已经有该技能
      • 如果有,则更新技能代码
    • 存储技能描述至 vectordb,内存中记录技能代码
def add_new_skill(self, info):
...
skill_description = self.generate_skill_description(program_name, program_code) # 生成技能描述
...
if program_name in self.skills: # 更新技能代码
...
self.vectordb._collection.delete(ids=[program_name])
i = 2
while f"{program_name}V{i}.js" in os.listdir(f"{self.ckpt_dir}/skill/code"):
i += 1
dumped_program_name = f"{program_name}V{i}"
else:
dumped_program_name = program_name
self.vectordb.add_texts( # 将技能描述添加至 vectordb
texts=[skill_description],
ids=[program_name],
metadatas=[{"name": program_name}],
)
self.skills[program_name] = {
"code": program_code,
"description": skill_description,
}
...
self.vectordb.persist() # 确认 embedding 被写入磁盘
def generate_skill_description(self, program_name, program_code):
messages = [
SystemMessage(content=load_prompt("skill")),
HumanMessage(
content=program_code
+ "\n\n"
+ f"The main function is `{program_name}`."
),
]
skill_description = f" // { self.llm(messages).content}"
return f"async function {program_name}(bot) {{\n{skill_description}\n}}"
  • retrieve_skills: 检索技能
def retrieve_skills(self, query):
k = min(self.vectordb._collection.count(), self.retrieval_top_k)
if k == 0:
return []
...
docs_and_scores = self.vectordb.similarity_search_with_score(query, k=k) # 根据描述查询 vectordb 中类似的技能
...
skills = []
for doc, _ in docs_and_scores: # 根据描述查询功能代码,返回
skills.append(self.skills[doc.metadata["name"]]["code"])
return skills

运行实例日志

采样 from iteration 11

  • 主循环 learn: Curriculum Agent propose_next_task
    • Curriculum Agent human message: render_human_message
    • Curriculum Agent ai message: propose_next_ai_task
****Curriculum Agent human message****
Nearby blocks: stone, gravel, coal_ore, dirt, grass_block, lapis_ore, copper_ore, andesite

Nearby entities: chicken

Position: x=84.7, y=60.0, z=258.5

Equipment: [None, None, None, None, 'wooden_pickaxe', None]

Inventory (7/36): {'crafting_table': 1, 'wooden_pickaxe': 1, 'jungle_log': 3, 'stick': 2, 'jungle_planks': 3, 'cobblestone': 3, 'dirt': 4}

Chests: None

Completed tasks so far: Mine 1 wood log, Craft a crafting table, Craft 4 wooden planks, Craft a wooden pickaxe, Mine 3 cobblestone

Failed tasks that are too hard: None

****Curriculum Agent ai message****
Reasoning: You have enough cobblestone to upgrade your pickaxe, which will allow you to mine more diverse blocks like lapis ore and copper ore.
Task: Craft a stone pickaxe.
Curriculum Agent Question: How to craft a stone pickaxe in Minecraft?
Curriculum Agent Answer: To craft a stone pickaxe in Minecraft, you will need three pieces of cobblestone and two sticks. Arrange the cobblestone in a "V" shape in the top row of the crafting table, and place the two sticks in a vertical line in the middle row. This will create a stone pickaxe.
  • 主循环 rollout/reset: Skill Manager retrieve_skills
Skill Manager retrieving for 5 skills
Skill Manager retrieved skills: craftWoodenPickaxe, mineCobblestone, craftCraftingTable, craftWoodenPlanks, mineWoodLog
  • 主循环 rollout/reset: Action Agent render_human_message
****Action Agent human message****
Code from the last round: No code in the first round

Execution error: No error

Chat log: None

Biome: sparse_jungle

Time: noon

Nearby blocks: stone, gravel, coal_ore, dirt, grass_block, lapis_ore, copper_ore, andesite

Nearby entities (nearest to farthest): chicken

Health: 20.0/20

Hunger: 20.0/20

Position: x=84.7, y=60.0, z=258.5

Equipment: [None, None, None, None, 'wooden_pickaxe', None]

Inventory (7/36): {'crafting_table': 1, 'wooden_pickaxe': 1, 'jungle_log': 3, 'stick': 2, 'jungle_planks': 3, 'cobblestone': 3, 'dirt': 4}

Chests: None

Task: Craft a stone pickaxe

Context: Question: How to craft a stone pickaxe in Minecraft?
Answer: To craft a stone pickaxe in Minecraft, you will need three pieces of cobblestone and two sticks. Arrange the cobblestone in a "V" shape in the top row of the crafting table, and place the two sticks in a vertical line in the middle row. This will create a stone pickaxe.

Critique: None
  • 主循环 rollout/step: Action Agent LLM 生成代码
****Action Agent ai message****
Explain: No explanation needed as there are no errors or issues in the given information.

Plan:
1) Check if there are enough cobblestone and sticks in the inventory.
2) If there are enough cobblestone and sticks, place the crafting table.
3) Craft a stone pickaxe using the cobblestone and sticks.
4) Notify the completion of the task.

Code:
async function craftStonePickaxe(bot) {
// Check if there are enough cobblestone and sticks in the inventory
const cobblestoneNeeded = 3;
const sticksNeeded = 2;
const cobblestone = bot.inventory.findInventoryItem(mcData.itemsByName.cobblestone.id);
const sticks = bot.inventory.findInventoryItem(mcData.itemsByName.stick.id);

if (cobblestone && cobblestone.count >= cobblestoneNeeded && sticks && sticks.count >= sticksNeeded) {
// Place the crafting table
const craftingTablePosition = bot.entity.position.offset(1, 0, 0);
await placeItem(bot, "crafting_table", craftingTablePosition);

// Craft a stone pickaxe using the cobblestone and sticks
await craftItem(bot, "stone_pickaxe", 1);
bot.chat("Stone pickaxe crafted.");
} else {
bot.chat("Not enough cobblestone or sticks to craft a stone pickaxe.");
}
}
  • 主循环 rollout/step: Critic Agent check_task_success
    • Critic Agent human message: render_human_message
    • Critic Agent ai message: ai_check_task_success
****Critic Agent human message****
Biome: sparse_jungle

Time: noon

Nearby blocks: stone, gravel, coal_ore, dirt, grass_block, lapis_ore, crafting_table, copper_ore, andesite

Health: 20.0/20

Hunger: 20.0/20

Position: x=84.7, y=60.0, z=258.5

Equipment: [None, None, None, None, None, None]

Inventory (6/36): {'stone_pickaxe': 1, 'wooden_pickaxe': 1, 'jungle_log': 3, 'cobblestone': 1, 'jungle_planks': 3, 'dirt': 4}

Chests: None

Task: Craft a stone pickaxe

Context: Question: How to craft a stone pickaxe in Minecraft?
Answer: To craft a stone pickaxe in Minecraft, you will need three pieces of cobblestone and two sticks. Arrange the cobblestone in a "V" shape in the top row of the crafting table, and place the two sticks in a vertical line in the middle row. This will create a stone pickaxe.

****Critic Agent ai message****
{
"reasoning": "You have a stone pickaxe in your inventory, which means you successfully crafted it.",
"success": true,
"critique": ""
}
  • 主循环 rollout/step: Skill Manager retrieve_skills
Skill Manager retrieving for 5 skills
Skill Manager retrieved skills: craftWoodenPickaxe, mineCobblestone, craftCraftingTable, craftWoodenPlanks, mineWoodLog
  • 主循环 learn: Skill Manager add_new_skill
Skill Manager generated description for craftStonePickaxe:
async function craftStonePickaxe(bot) {
// The function is about crafting a stone pickaxe using cobblestone and sticks. It checks if there are enough cobblestone and sticks in the inventory. If there are, it places a crafting table and crafts a stone pickaxe. If there aren't enough materials, it sends a message to the chat.
}

使用约定式提交进行语义化版本控制

· 阅读需 21 分钟

全文机翻,有小幅语法调整,不会或无法翻译的部分保留原文。

原文: Semantic Versioning with Conventional Commits

使用约定式提交进行语义化版本控制

版本控制很重要。我不必告诉你这个。然而,我看到它一遍又一遍地做得不好。

我经常遇到的缺陷是缺乏明确的开发和发布流程以及较差的工具支持。如果你的开发人员坐在那里想知道如何完成某些任务,那么这个过程就被打破了。他们应该知道如何发布新版本或如何在主干移动时修复生产。您的 CI/CD 流程也应该支持这些场景!

许多人依靠他们的 CI/CD 工具来确定他们应用程序的下一个版本。我也为此感到内疚,但在某些时候,我意识到这是只有开发人员才能做出的决定。我们的工具还不够智能,无法查看代码更改并告诉我它是功能、修复还是其他什么。

这让我产生了改变我们做事方式的强烈愿望。我的目标是拥有一个涵盖所有这些用例的明确流程,对开发人员友好,并在我们倾向于跨项目使用的各种 CI/CD 工具中得到支持。

这篇文章记录了这种方法。

语义版本控制

我们将在这里使用语义版本控制(semver)。这可能是当今软件中使用的最突出的版本控制方案。

我们构建的大多数应用程序和库都倾向于公开一个 API,无论是 REST API、接口等。Semver 就是对这个 API 进行版本控制。

语法是众所周知的:

<MAJOR>.<MINOR>.<PATCH>

MAJORMINORPATCH
引入新的向后不兼容更改引入新的向后兼容更改在保持向后兼容性的同时修复一个错误
1.0.0 -> 2.0.01.0.0 -> 1.1.01.0.0 -> 1.0.1

如表所示,向后兼容性是版本冲突的一大区别。

有些人仍然没有搞懂,所以在这里我将使用 JSON REST API 来说明什么构成补丁、功能或重大更改。

让我们假设一个称职的开发人员已经实现了这个 REST API,并且它成功地遵循了 Postel 定律

Major

  • 删除一个操作,比如删除 HTTP 动词/路径组合
  • 向某个请求添加必填字段
  • 向某个请求添加/删除字段
    • 如果所有用户被认为足够宽容,那么添加一个字段可能不会是一个重大变化

Minor

  • 添加一个新操作,即新的 HTTP 动词/路径组合
  • 向请求添加可选字段

Patch

  • 没有接口变化

约定式提交

如今,尽管工具非常好,但它们还无法识别代码更改的本质。通过机器学习的那一天将会到来,这将是可能的,但就目前而言,我们必须依靠良好的老式人类智能。

约定式提交提供了在开发人员和 CI/CD 工具之间传达提交中更改的性质的机制。

简而言之,开发人员提供了一个提交消息,该消息明确地标识了更改的性质。然后 CI/CD 工具可以扫描自上一个版本以来的所有提交消息,并确定如何更新版本。

除了这种自动化之外,这种方法还为其他团队成员提供了清晰的变更沟通,甚至让我们自动生成发布 release note 和 changelog。

约定是提交的官方总结示例非常简洁,我没有必要在这里重复它们。来看看吧。我会在这里等你。

以下是我们的 JSON REST API 的一些示例提交:

Major

  • feat: disable deletion of records

    BREAKING CHANGE: removed and endpoint

  • feat: expiry date must be provided by the user

    BREAKING CHANGE: new mandatory field in create operation

Minor

  • feat(my-operation): allow users to provide an optional name to override the default
  • feat: add operation to retrieve sub-records

Patch

  • fix: remove the infinite loop
  • fix(my-operation): handle null input field

N/A

  • test: refactor user management test cases
  • ci: point to the new registry
  • docs: add missing method documentation in create operation

如果您的团队更“有趣”,您可以随时尝试这些替代信号:✨ (feat), 🐛 (fix), 📚 (docs), 💎 (style), ♻️ (refactor), 🚀 (perf), ✅ (test), 📦 (build), 👷 (ci), 🔧 (chore)

你在构建什么?

如果您曾在软件开发中花费过任何时间,您就会知道提倡“一刀切”的人需要滚蛋了。因此,在这篇文章中,我想研究两种截然不同的软件发布方法,以及如何将这种技术应用于这两种方法。

合并后发布

  • 描述
    • 主干分支上的每个提交都是具有新语义版本的发布
    • 新版本没什么大不了的,版本不被视为神圣的有限资源
    • 一个应用程序每天可能会经历多个版本
    • 修改需要通过 QA 关卡,比如单元测试、同事评审、MR 构建甚至 review app
  • 优势
    • 无预先发版计划。在需要时选择要发布的版本
    • 对测试人员和最终用户的反馈循环更短
  • 使用时机
    • 供应商/顾问交付的项目
    • 有多个非生产环境可用于快速部署和测试更改的项目

预发布后正式发布

  • 描述
    • 每个版本之前都有一个或多个 alpha 和 beta 版本形式的预发布
    • 版本更新被认为是珍贵的
    • 从预发布版本中删除版本时需要重新测试
  • 优势
    • 通过在引入稳定版本之前发布预发布来获得充分的信心
    • 您的用户不必像在其他方法中那样想知道预发布版本发生了什么
  • 使用时机
    • 库、软件产品和开源项目
    • 稳定发布节奏较慢的项目

我已经在很多地方看到了这两种方法的许多变体,因此我们将在这里讨论的内容也应该同样适用于这些方法。 虽然我应该提到我倾向于避免破坏版本不变性的样式,例如 Maven 中的 SNAPSHOT 版本或 NPM 中的 @next disributaion channel。

Pull Requests

无论您如何发布,我都希望您通过 PR(有时称为 MR)引入新功能。 如果你不是,我们有比版本控制更大的问题。

理想情况下,每个拉取请求都应包含一个功能或修复。 作为拉取请求审查的一部分,开发人员可能需要提交更多更改以解决审查意见。 但是这些额外的提交并不是主干上的新功能或代码修复。

我的解决方案是使用 squash merge 策略。 这样,开发人员可以在功能/修复分支上对他们的提交消息做任何他们喜欢的事情。 这些提交将全部消失,开发人员可以在合并点为整个拉取请求提供约定是提交消息。

大多数体面的 Git 存储库还允许您使用您的拉取请求名称作为您的 squash 提交消息。 如果您希望从开发人员那里看到一致的拉取请求名称并让审阅者甚至在批准之前审阅 squash 提交消息,这很好。

合并后发布的分支策略

不管发布风格如何,我倾向于基于主干(主线)的分支。我避免使用 Gitflow,因为我足够关心我的开发人员,以免他们每天都花在解决合并冲突上。更不用说,仅仅因为你从 develop 合并到 master 就重新构建相同版本的应用程序,面对“一次构建,多次部署”的CI/CD实践。

现在我已经完成了我对 GitFlow 的每日一喷,接下来让我们来谈谈我们在每次合并时发布时如何进行分支。

这很简单:创建一个功能/修复分支并遵循上面的拉取请求过程。

作为 CI/CD 流程设计师,您的主要目标之一应该是:开发人员最常做的任何事情都应该是最容易做到的。我觉得上面符合这个标准。

不太常见的情况也没有那么困难。假设开发人员正在构建应用程序的新主要版本,但之前的主要版本存在生产缺陷,需要热修复。这是执行此修补程序的剧本:

  1. 找出生产中的次要版本。比方说v1.3。
  2. 从该次要版本的最新补丁版本创建 v1.3.x 分支,例如v1.3.6。
  3. 创建一个修复分支并将修复请求拉回 v1.3.x
  4. 从 v1.3.x 分支构建、部署和升级到生产
  5. 通过将 v1.3.x 分支合并到 master 将修复移植到主线

预发布后正式发布的分支策略

在此方法中,您将在预发布分支上引入重大更改。 你可以使用任何你喜欢的滑稽名称,但我会坚持使用传统的 alpha/beta 术语。

本质上,您在一个或多个预发布分支上工作,直到您准备好向您的用户发布新版本;此时,您只需将预发布分支合并到主干中。

该图通过在下一个规范版本升级之前发布 2 个 alpha 和 1 个 beta 版本来证明这一点。

上图还展示了在所有这些预发布工作期间对生产的修补程序。

不用说,您可以拥有更少或更多的预发布分支,甚至可以根据需要在它们之间来回合并。 这完全取决于您的个人发布风格。

工具中的实现

让我们看看我们的版本控制和分支方法如何应用于各种源代码管理和 CI/CD 平台。

虽然您可以让开发人员遵守约定是提交,但这需要我在我们的专业中几乎看不到的僧侣般的纪律水平。因此,在相关分支上强制执行提交消息格式更为明智。大多数 SCM 提供此功能作为服务器端挂钩。

对于一些人来说,意识到他们的提交消息在服务器上不正确已经太晚了。幸运的是,有一些工具,例如 Commitizencommitlint 甚至是一个简单的 pre-commit,可以尽早警告用户。

虽然如果你遵循我们的 squash merge 策略,你就不必关心你的本地提交消息。

然而,我们并没有免除关心长期存在的分支上的提交。因此,始终建议确保构建过程验证自上次构建以来这些分支上的提交历史记录,以确保所有提交消息都符合要求。

通常,我们在 CI/CD 流程中至少具备以下条件:

  • 在每次 PR 构建时,验证自上次构建以来的所有提交消息,以确保它们遵循约定式提交
  • 在主干构建中,除了常规测试之外:
  1. 验证提交消息
  2. 通过分析提交消息和以前的标签确定下一个版本
  3. 用新版本标记当前提交
  4. 可选择生成 relase note
  5. 发布 artifacts

给提交打 tag 以将其标识为应用程序版本的来源是一种有价值的做法。它使 Git 存储库成为一个独立的真实来源,并消除了对 CI/CD 工具的一些过度依赖。我不止一次看到团队在使用 CI/CD 作为事实来源时不得不重新开始他们的版本控制,但随后他们不得不迁移到不同的工具,或者该工具出现了不可恢复的故障。

现在我们知道需要做什么,我们需要弄清楚如何去做。这似乎需要实现很多功能。幸运的是,已经有一些工具可以为我们完成所有这些工作。我目前首选的工具是 semantic-release,它提供了以上所有以及更多。

semantic-release

semantic-release 提供了一个命令行界面 (CLI),任何 CI/CD 工具都可以调用它。 先决条件是 NodeJS 和 Git。

语义发布的优势之一是它可以使用插件进行扩展。 官方插件让您可以创建发布说明和变更日志文件,将发布发布到 GitHub 和 Gitlab,将包发布到 NPM 和 APM 等。还有很多社区插件。

我将使用 GitLab 来说明如何配置和使用这个工具,但这种方法可以很容易地转换为其他 CI/CD 工具。

我将使用 GitLab 的 Docker 执行器在容器中运行我的构建,所以首先,我需要创建一个具有语义发布和我需要的所有插件的 Docker 映像。

FROM node:14.3.0
LABEL maintainer="sohrab"
RUN npm install --global \\
[email protected] \\
@semantic-release/[email protected] \\
@semantic-release/[email protected]

(是的,我对所有内容都进行了版本控制。就像专业人士一样。“可重复、可靠的构建”是另一个 CI/CD 原则。如果您要重复使用它,请查看 npmjs.com 以获取最新版本。)

接下来,我需要为我的存储库配置语义发布。 有几种方法可以做到这一点,但在这里我将 .releaserc.json 文件放在我的存储库的根目录中,其中包含以下内容:

{
"plugins": [
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
"@semantic-release/gitlab",
[
"@semantic-release/exec", {
"successCmd": "echo \\"VERSION=${nextRelease.version}\\" >> vars.env"
}
]
]
}

这将只启用我想要使用的插件,在这种情况下:

  • commit-analyzer 通过分析 repo 的提交历史来确定下一个版本
  • release-notes-generator 以传统的变更日志格式生成发行说明
  • gitlab 将发行说明发布为 GitLab Release
  • exec 将发布版本写入 dot-env 文件,以便在后续阶段中可用

最后,我们需要配置 CI/CD 本身。 这里它显示在 GitLab YAML 中。 即使您以前从未使用过 GitLab,这也应该是不言自明的,并且可以翻译成其他 CI/CD 工具:

stages:
- version
- build

version:
image: semantic-release
script:
- semantic-release
artifacts:
reports:
dotenv: vars.env
rules:
- if: '$CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "master"'
when: on_success

build:
image: ...
script:
# run all tests, build, package and publish the artifact
- ...
rules:
- if: '$VERSION'
when: on_success

由 version stage 生成的 vars.env 文件中的 VERSION 环境变量随后可以被构建阶段用于对 artifact 进行版本控制。值得注意的是,如果没有产生新版本,我们会跳过构建和发布。

最后一点配置是确保语义发布可以将标签推送到您的存储库中。为此,您需要向该工具提供 Git 身份验证详细信息。在我的用例中,这是设置 GITLAB_TOKEN 环境变量的问题。

提示:如果您使用的是自托管 Gitlab 实例,您还需要配置 GITLAB_URL 以指向您的实例。如果您使用的是 gitlab.com,则不需要这样做。

我应该注意,如果构建失败,版本阶段不应再次运行,因为它已经用版本标记了提交。因此,如果您的 CI/CD 工具允许您从构建阶段恢复失败的管道,则上述顺序是合适的。如果此支持不存在,那么您需要:

在重新运行管道之前手动清理标签,或者更改管道以使用 --dry-run 标志执行语义发布以获取新版本,运行构建并最终真正运行语义发布。

就是这样!

我们在相对成功的项目中使用了这种方法,尤其是每次合并发布风格。

我必须对你说实话,如果你没有好的工具,你将需要良好的开发人员纪律。 如果您两者都没有,那么这可能不适合您。 但是,如果您可以使用这些技术,那么您将永远不必考虑在提交和拉取请求中交付的内容之后的版本控制。

Backend Developer Roadmap 4

· 阅读需 4 分钟

Roadmap: https://roadmap.sh/backend

本文隶属于 Roadmap 中的 Internet --> DNS and how it works?

原文:https://www.cloudflare.com/en-gb/learning/dns/what-is-dns/

什么是 DNS?

DNS 是互联网的电话簿。人们通过域名来在线获取信息。Web浏览器通过 IP 地址进行交互。DNS 将域名翻译成 IP 地址,以便浏览器加载互联网资源。

每个连接到互联网的设备都有一个唯一的 IP 地址,其他设备使用该地址来查找该设备。DNS 服务器消除了人们记忆 IP 地址的需求。

DNS 如何工作?

为了了解 DNS 解析背后的过程,需要学习 DNS 查询必须经过的不同硬件组件。对于 Web 浏览器,DNS 查询在“幕后”进行,除了初始请求外,不需要用户计算机的任何交互。

加载页面时用到的 4 个 DNS 服务器:

  • DNS recursor - 可以被视为图书馆员,被要求在图书馆中找到一本特定的书。 DNS recursor 是一个服务器,旨在通过应用程序(例如 Web 浏览器)从客户机接收查询。通常,递归器负责发出其他请求,以满足客户端的 DNS 查询。
  • Root nameserver - 将人类可读的主机名转换(解析)为 IP 地址的第一步。 它可以被视为指向不同书架的索引 - 通常作为指向其他更具体位置的参考。
  • TLD(Top Level Domain) nameserver - 可以将其视为图书馆中特定的书架。 这个名称服务器是寻找特定 IP 地址的下一步,它托管主机名的最后一部分(在 example.com 中,TLD 服务器是“com”)。
  • Authoritative nameserver - 最终的名称服务器可以被视为书架上的字典,其中特定名称可以被翻译为其定义。 授权名称服务器是名称服务器查询的最后一站。 如果授权名称服务器可以访问所请求的记录,则会将所请求主机名的 IP 地址返回给进行初始请求的 DNS Recursor(图书馆员)。

Authoritative DNS server 和 Recursive DNS resolver 之间的区别

在 DNS 基础设施中,这两个概念都涉及到服务器(服务器组),但每个服务器都有不同的角色,并且位于 DNS 查询管道的不同位置。可以将递归 DNS 解析器视为 DNS 查询的开始,而权威名称服务器位于其末尾。

Recursive DNS resolver

递归解析器是响应客户端递归请求并花费时间查找 DNS 记录的计算机。它通过进行一系列请求来查找所请求记录的权威 DNS 名称服务器(如果找不到记录,则超时或返回错误)。

幸运的是,递归 DNS 解析器可以利用缓存来快速返回结果。

算法基础 1

· 阅读需 4 分钟

https://oi-wiki.org/dp/knapsack/#0-1-背包

0-1 背包问题是指有一个固定大小、能够携带重量为 W 的背包,以及一组有价值和重量的物品,需要决定将哪些物品放入背包中,以便在不超过背包容量的前提下,让背包中的总价值最大。

状态转移方程

dp[i][j] = max(dp[i-1][j], dp[i-1][j-w[i]] + v[i])

其中 dp[i][j] 表示前 i 件物品放入容量为 j 的背包可以获得的最大价值,w[i]v[i] 分别表示第 i 件物品的重量和价值。

优化

将二维数组压缩为一维数组,即 dp[j] = max(dp[j], dp[j - w[i]] + v[i])。这是因为当前状态只与上一个状态有关,因此可以使用滚动数组来优化空间复杂度。

例题

LeetCode 416

https://leetcode.com/problems/partition-equal-subset-sum/

题意:给定一个只包含正整数的非空数组,判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。

答:转移方程为 dp[i][j] = dp[i-1][j] or dp[i-1][j-nums[i]] ,其中 dp[i][j] 表示前 i 个数组成的数组,其某个子集的和是否等于 j ,再配合滚动数组优化变为 1d DP

核心代码:

dp = [False] * (half_s + 1)
dp[0] = True
for num in nums:
for j in range(half_s, num - 1, -1):
dp[j] = dp[j] or dp[j - num]

LeetCode 474

https://leetcode.com/problems/ones-and-zeroes/

题意:给定一个字符串数组,每个字符串只包含 0 和 1。你有 m 个 0 和 n 个 1,问最多能选出多少个字符串,使得这些字符串中的 0 的个数不超过 m,1 的个数不超过 n。

答:令 dp[i][j][k] 表示前 i 个字符串中元素最多的子集,该子集的元素至多有 j 个 0 和 k 个 1。最终的解为 dp[len(strs) - 1][m][n]

转移方程为 dp[i][j][k] = max(dp[i-1][j][k], dp[i-1][j-f[strs[i]]][k-g[str[i]]]+1),其中 fg 代表 0 和 1 的个数。可以通过滚动数组优化为二维 DP。

核心代码:

dp = [[0 for _ in range(n + 1)] for _ in range(m + 1)]
for i in range(N):
for j in range(m, f[i] - 1, -1):
for k in range(n, g[i] - 1, -1):
dp[j][k] = max(dp[j][k], dp[j - f[i]][k - g[i]] + 1)
return dp[-1][-1]

LeetCode 1049

https://leetcode.com/problems/last-stone-weight-ii/

给定一个正整数数组 stones,其中每个元素表示一块石头的重量。现在你需要将这些石头分成两组,使得每组石头的重量之和尽量接近。求这个最小的差值。

答:可以将问题转化求两组石头重量和之差的最小值。

dp[i][j]表示前i块石头中是否存在子集,其重量之和为j。状态转移方程为dp[i][j] = dp[i-1][j] or dp[i-1][j-w[i]]。最终的解为sum(stones) - j * 2, if dp[len(stones) - 1][j] == True

核心代码:

for stone in stones:
for j in range(s_half, stone - 1, -1):
dp[j] = dp[j] or dp[j - stone]
for j in range(s_half, -1, -1):
if dp[j]:
return s - j - j

算法基础 2

· 阅读需 4 分钟

https://oi-wiki.org/dp/knapsack/#完全背包

完全背包问题与 01 背包问题的区别在于每种物品的数量都是无限的,因此可以对每种物品的体积进行遍历,而不是仅考虑取或不取。

状态转移方程

dp[i][j] 为只能选前i个物品时,容量为j的背包可以达到的最大价值。

则有状态转移方程:dp[i][j] = max(dp[i-1][j],dp[i][j-w[i]]+v[i])

其中 w[i]v[i] 分别为第 i 个物品的重量和价值。

优化

dp[j] 表示容量为 j 的背包可以达到的最大价值。则有状态转移方程:dp[j] = max(dp[j], dp[j-w[i]]+v[i])。这里的 w[i]v[i] 分别为第 i 个物品的重量和价值。

这种优化方式可以将空间复杂度从O(nm)降低至O(m),其中 n 为物品数量,m 为背包容量。

例题

LeetCode 322

https://leetcode.com/problems/coin-change/

题意:给定面额不同的硬币和一个总金额 amount,编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果不能凑成总金额,返回 -1

dp[i][j]表示前i个硬币凑成j元钱,所需最少的硬币个数,那么dp[len(coins)-1][amount]则为最后的解。状态转移方程为dp[i][j]=min(dp[i-1][j],dp[i][j-coins[i]]+1)

核心代码:

N = len(coins)
dp = [math.inf] * (amount + 1)
dp[0] = 0
for coin in coins:
for j in range(coin, amount + 1):
dp[j] = min(dp[j], dp[j - coin] + 1)
return dp[-1] if not math.isinf(dp[-1]) else -1

LeetCode 377

https://leetcode.com/problems/combination-sum-iv/

题意:给定一个无重复元素的数组nums 和一个目标值 target ,求由数组中的元素组成和为 target 的组合的个数。每个元素可以被选取无限次。

dp[i]表示由数组中的元素组成和为 i 的组合的个数,状态转移方程为dp[i] = \sum_{num \in nums} dp[i-num],其中num为数组中的元素,i-num>=0

核心代码:

dp = [0] * (target + 1)
dp[0] = 1
for i in range(1, target + 1):
for num in nums:
if i - num >= 0:
dp[i] += dp[i-num]
return dp[-1]

LeetCode 139

https://leetcode.com/problems/word-break/

题意:给定一个字符串和一个单词字典,确定该字符串是否可以分割成一个或多个单词的空格分隔序列。

dp[i]表示字符串s的前i个字符能否被空格分割成若干个单词,状态转移方程为dp[i] = OR_{word} dp[i-len(word)] AND s[i-len(word):i]==word

核心代码:

N = len(s)
dp = [False] * (N + 1)
dp[0] = True
for i in range(1, N + 1):
for word in wordDict:
m = len(word)
if i - m >= 0:
dp[i] = dp[i] or (dp[i - m] and s[(i - m):i] == word)
if dp[i]:
break
return dp[-1]

Backend Developer Roadmap 3

· 阅读需 18 分钟

Roadmap: https://roadmap.sh/backend

本文隶属于 Roadmap 中的 Internet --> Browsers and how they work?

原文:https://developer.mozilla.org/en-US/docs/Web/Performance/How_browsers_work

用户希望网站内容快速加载且交互顺畅,因此开发人员应该努力实现这两个目标。为了理解如何提高性能和感知性能,有必要了解浏览器的工作原理。

总览

快速的网站提供更好的用户体验。用户希望并期望具有快速加载和顺畅交互的内容的 Web 体验。

Web 性能的两个主要问题与延迟有关,并且与浏览器大多数情况下是单线程有关。

延迟是影响页面加载速度的主要因素,开发人员的目标是让站点尽可能快地加载,以便用户尽快获得所需信息。浏览器大多数情况下是单线程的,这意味着它们在占用另一个任务之前从头到尾执行一个任务。为了确保网站流畅互动,开发人员需要确保主线程可以完成所有工作并在可能和适当的情况下最小化主线程的责任,以提高 Web 性能。

导航

导航是加载网页的第一步。它发生在用户通过地址栏输入 URL、点击链接、提交表单以及其他操作请求页面时。Web 性能的目标之一是尽量减少导航完成所需的时间。在理想的情况下,这通常不会花费太长时间,但延迟和带宽可能会导致时延。

DNS 查找

访问网页的第一步是查找页面资源的位置。如果访问https://example.com,那么 HTML 页面位于 IP 地址为93.184.216.34的服务器上。如果你从未访问过此站点,则需要进行 DNS 查找。

浏览器请求 DNS 查找,最终由名称服务器(name server)响应 IP 地址。此后,IP 地址可能会被缓存一段时间,这样就可以通过从缓存中检索IP地址而不是再次联系名字服务器来加速后续请求。

页面加载通常只需要对每个主机名进行一次 DNS 查找。但是,对于所请求页面引用的每个唯一主机名都必须进行 DNS 查找。如果您的字体、图像、脚本、广告和指标都具有不同的主机名,则必须对每个主机名进行 DNS 查找。这可能会影响性能,特别是在移动网络上。

TCP 握手

浏览器通过 TCP 三次握手与服务器建立连接,以便在传输数据之前,协商网络 TCP 套接字连接的参数。

TCP 的三次握手技术通常被称为“SYN-SYN-ACK”或更准确地说是SYN、SYN-ACK 和 ACK,因为 TCP 需要发送三个消息来协商和启动两台计算机之间的 TCP 会话。这意味着每台服务器之间需要三个以上的消息传递,而且此时请求还尚未完成。

TLS 协商

对于建立在 HTTPS 上的安全连接,需要进行另一次“握手”。这个握手,或者更准确地说是 TLS 协商,确定要用于加密通信的密码,验证服务器,并在开始实际数据传输之前建立安全连接。这需要向服务器发送三次往返之前,才会实际发送内容请求。

https://developer.mozilla.org/en-US/docs/Web/Performance/How_browsers_work/ssl.jpg

建立安全连接虽然增加了页面加载时间,但是安全连接的延迟成本是值得的,因为在浏览器和 Web 服务器之间传输的数据无法被第三方解密。

经过 8 次往返后,浏览器终于能够发出请求。

响应

当浏览器与 Web 服务器成功建立连接后,浏览器会代表用户发送初始的 HTTP GET 请求,往往是一个 HTML 文件。Web 服务器接收请求后,会回复相关响应头以及 HTML 的内容。

<!DOCTYPE html>
<html lang="en-US">
<head>
<meta charset="UTF-8" />
<title>My simple page</title>
<link rel="stylesheet" href="styles.css" />
<script src="myscript.js"></script>
</head>
<body>
<h1 class="heading">My Page</h1>
<p>A paragraph with a <a href="<https://example.com/about>">link</a></p>
<div>
<img src="myimage.jpg" alt="image description" />
</div>
<script src="anotherscript.js"></script>
</body>
</html>

这个初始请求的响应包含了收到的第一个数据字节。Time to First Byte (TTFB) 是用户发出请求(比如点击链接)到收到 HTML 第一个包的时间。第一块内容通常包含 14KB 数据。

在上面的例子中,请求肯定小于 14KB,但链接资源直到浏览器在解析过程中遇到链接才会请求,解析过程在下面描述。

TCP 慢启动 / 14KB 规则

TCP 慢启动是一个算法,它平衡了网络连接的速度。初始响应包的大小为 14KB,这是 TCP 慢启动的一部分,它逐渐建立适合网络速度的传输速度,以避免拥塞。当接收到初始数据包时,服务器会将下一个数据包的大小加倍到约 28KB,随后的数据包会逐渐增加,直到达到预定的阈值或经历了拥塞。

拥塞控制

当服务器发送 TCP 数据包时,客户端通过返回确认信息(ACKs)来确认收到数据。连接的容量取决于硬件和网络条件。如果服务器发送太多的数据包,速度过快,它们将被丢弃。这意味着没有确认信息,服务器会将其记录为缺失的 ACKs。拥塞控制算法使用发送的数据包和 ACKs 的流量来确定发送速率。

解析

浏览器接收到第一块数据后,就可以开始解析接收到的信息。解析是浏览器将从网络接收到的数据转换为 DOM 和 CSSOM 的步骤,由渲染器用于将页面绘制到屏幕上。DOM 是浏览器的标记语言的内部表示。即使请求的页面 HTML 大于初始的 14KB 包,浏览器也会开始解析并尝试根据它所拥有的数据渲染体验。在任何内容呈现到屏幕之前,必须解析 HTML、CSS 和 JavaScript。

建立 DOM 树

关键渲染路径包括五个步骤。

第一步是处理 HTML 标记并构建 DOM 树。HTML 解析包括 tokenization 和树构造。当解析器发现非阻塞资源时,浏览器将请求这些资源并继续解析。当遇到 CSS 文件时,解析可以继续,但<script>标记,特别是那些没有 async 或 defer 属性的标记,会阻止呈现并暂停 HTML 的解析。过多的脚本可能成为一个重要的瓶颈。

预加载扫描

浏览器在构建 DOM 树时,占用主线程。这时,预加载扫描器会解析可用内容并请求高优先级资源,例如 CSS、JavaScript 和 Web 字体。由于存在预加载扫描器,我们不必等到解析器找到对外部资源的引用才请求它。它会在后台检索资源,以便在主 HTML 解析器到达请求的资源时,它们可能已经在传输过程中,或已经被下载了。预加载扫描器提供的优化可以减少阻塞。

<link rel="stylesheet" href="styles.css" />
<script src="myscript.js" async></script>
<img src="myimage.jpg" alt="image description" />
<script src="anotherscript.js" async></script>

在这个例子中,当主线程解析 HTML 和 CSS 时,预加载扫描器会找到脚本和图像,并开始下载它们。为了确保脚本不会阻塞进程,请添加async属性,或者如果 JavaScript 解析和执行顺序很重要,则添加defer属性。

等待获取 CSS 不会阻塞 HTML 解析或下载,但会阻塞 JavaScript,因为通常使用 JavaScript 来查询 CSS属性对元素的影响。

建立 CSSOM 树

关键渲染路径的第二步是处理 CSS 并构建 CSSOM 树。CSS 对象模型类似于 DOM。

构建 CSSOM 非常快,从性能优化的角度来看,这一步没有什么优化空间,因为创建 CSSOM 的总时间通常少于一个 DNS 查找所需的时间。

渲染

渲染步骤包括样式、布局、绘制和在某些情况下的合成。解析步骤中创建的 CSSOM 和 DOM 树会合并成一个渲染树,然后用于计算每个可见元素的布局,然后将其绘制到屏幕上。在某些情况下,内容可以被提升到自己的图层并进行合成,通过在 GPU 上绘制屏幕的部分而不是 CPU,释放主线程,从而提高性能。

风格

关键渲染路径的第三步是将 DOM 和 CSSOM 合并为一个渲染树。

构建渲染树的过程从 DOM 树的根开始,遍历每个可见节点。

不会被显示的标记,如<head>和其子元素以及任何带有display:none的节点不包括在渲染树中,因为它们不会出现在渲染输出中。带有visibility:hidden属性的节点包括在渲染树中,因为它们占据空间。

布局

关键渲染路径的第四个步骤是在渲染树上运行布局计算每个节点的几何形状。_布局_是决定渲染树中所有节点的宽度、高度和位置的过程,以及页面上每个对象的大小和位置的确定。_回流_是页面或整个文档的任何部分进行的任何后续大小和位置确定。

一旦构建了渲染树,布局就开始了。渲染树确定了哪些节点(即使是不可见的)以及它们的计算样式,但不确定每个节点的尺寸或位置。

在网页上,几乎所有内容都是一个框。不同的设备和桌面首选项意味着有无限数量的不同视口大小。在这个阶段,考虑视口的大小,浏览器确定了所有不同框在屏幕上的尺寸。以视口的大小为基础,布局通常从 body 开始,布置所有 body 后代的尺寸,每个元素的框模型属性提供替换元素的占位空间,它不知道尺寸,比如我们的图片。

第一次确定节点的大小和位置称为_布局_。对节点大小和位置的后续重新计算称为_回流_。在我们的示例中,假设在返回图像之前进行了初始布局。由于我们没有声明图片的大小,因此在了解图像大小后会出现回流。

绘制

关键渲染路径的最后一步是将每个节点绘制到屏幕上,第一次出现的节点称为 First Contentful Paint (首次有意义绘制)。

在绘制或光栅化阶段,浏览器将从网络接收到的每个框转换为 DOM 和 CSSOM,由渲染器用于在屏幕上绘制页面。

DOM 是浏览器标记语言的内部表示。确保平滑滚动和动画,包括计算样式以及回流和重绘在内的占用主线程的所有任务,必须在不到 16.67ms 的时间内完成。

为了确保重绘可以更快地完成,绘制到屏幕通常被分解成几个图层。如果这种情况发生,则需要合成。

将绘制树中的元素分成图层可以提高绘制和重绘性能。将内容推广到 GPU 上的图层(而不是 CPU 上的主线程)可以提高绘制和重绘性能。

有特定的属性和元素会实例化一个图层,包括<video><canvas>,以及具有 CSS 属性的任何元素,例如opacity、三维transformwill-change等等。这些节点将与其后代一起绘制到自己的图层上,除非后代因上述原因之一需要自己的图层。图层确实提高了性能,但在内存管理方面是昂贵的,因此不应在 Web 性能优化策略中过度使用。

合成

当文档的不同部分在不同的层中绘制时,合成是必要的,以确保它们以正确的顺序绘制到屏幕上并正确呈现内容。

随着页面继续加载资源,可能会发生回流。回流会引发重新绘制和重新合成。

如果我们定义了图像的大小,就不需要回流,只会重新绘制需要重新绘制的层,并在必要时进行合成。

当从服务器获取图像时,渲染过程回到布局步骤并从那里重新开始。

Interactivity

当主线程完成页面绘制后,你可能认为我们已经“准备好了”。但这并不一定是这样。如果加载包括 JavaScript,那么必须正确地延迟执行,并且只有在 onload 事件触发后才执行。此时,主线程可能很忙,无法用于滚动、触摸和其他交互。

交互时间(TTI)是从引起 DNS 查找和 SSL 连接的第一次请求到页面交互的测量时间,交互是指在 First Contentful Paint 之后,页面在 50ms 内响应用户交互的时间点。如果主线程正忙于解析、编译和执行 JavaScript,则不可用,因此无法在及时(少于 50ms)响应用户交互。

在我们的示例中,也许图片加载得很快,但是 anotherscript.js 文件的大小为 2MB,我们的用户的网络连接很慢。在这种情况下,用户会非常快速地看到页面,但是在下载、解析和执行脚本之前,无法滚动。这不是一个良好的用户体验。

Untitled

在这个示例中,JavaScript执行需要超过1.5秒,主线程在整个时间内都被完全占用,无法响应点击事件或屏幕点击。

Building a FastAPI Application with Prometheus Metrics

· 阅读需 10 分钟

Introduction

FastAPI is a modern, fast (high-performance), web framework for building APIs with Python 3.7+ based on standard Python type hints. It is designed to be easy to use, fast to develop with, and fast to run. Prometheus is a popular open-source monitoring system that collects metrics from various sources and stores them in a time-series database. It is used to monitor and alert on system metrics, application metrics, and business metrics.

Requirements

Before we start, make sure that you have the following requirements installed:

  • Python 3.7+
  • FastAPI
  • Prometheus Client
  • Uvicorn

You can install these packages using pip:

pip install fastapi prometheus_client uvicorn

Creating a FastAPI Application

We will start by creating a new FastAPI application. Create a new Python file called app.py and add the following code:

from fastapi import FastAPI

app = FastAPI()

@app.get("/")
async def root():
return {"message": "Hello World"}

This creates a new FastAPI application with a single endpoint / that returns a JSON response with the message "Hello World".

To run the application, you can use the uvicorn server:

uvicorn app:app --reload

This will start the server on http://localhost:8000.

Adding Prometheus Metrics

Now, let's add Prometheus metrics to the application. We will use the prometheus_client library to create and expose metrics.

First, let's create a new endpoint /metrics that will return the Prometheus metrics. Update app.py with the following code:

from fastapi import FastAPI
from prometheus_client import Counter, generate_latest, CONTENT_TYPE_LATEST

app = FastAPI()

REQUESTS = Counter('requests_total', 'Total HTTP requests.')

@app.get("/")
async def root():
REQUESTS.inc()
return {"message": "Hello World"}

@app.get("/metrics")
async def metrics():
return Response(generate_latest(), media_type=CONTENT_TYPE_LATEST)

We have created a new counter REQUESTS that will keep track of the total number of HTTP requests. Every time the / endpoint is called, we increment the counter using REQUESTS.inc().

We have also added a new endpoint /metrics that returns the Prometheus metrics. We use the generate_latest() function to generate the metrics in the Prometheus exposition format.

To run the application with the metrics endpoint, you can use the following command:

uvicorn app:app --reload --workers 1 --host 0.0.0.0 --port 8000

This will start the server on http://localhost:8000 with a single worker.

Adding Prometheus Histograms

In addition to counters, we can also use histograms to measure the latency of our requests. A histogram measures the distribution of values over a set of buckets. We will use the Histogram class from the prometheus_client library to create histograms.

Update app.py with the following code:

from fastapi import FastAPI, Response, Request
from prometheus_client import Counter, Histogram, generate_latest, CONTENT_TYPE_LATEST, CollectorRegistry
import time

app = FastAPI()
registry = CollectorRegistry()

REQUESTS = Counter('requests_total', 'Total HTTP requests.', registry=registry)
EXCEPTIONS = Counter('exceptions_total', 'Total HTTP exceptions.')
LATENCY = Histogram('latency_seconds', 'HTTP request latency (seconds).')

@app.get("/")
async def root():
REQUESTS.inc()
return {"message": "Hello World"}

@app.get("/metrics")
async def metrics():
return Response(generate_latest(registry=registry), media_type=CONTENT_TYPE_LATEST)

@app.exception_handler(Exception)
async def exceptions_handler(request, exc):
EXCEPTIONS.inc()
raise exc

@app.middleware("http")
async def add_process_time_header(request: Request, call_next):
start_time = time.time()
response = await call_next(request)
process_time = time.time() - start_time
LATENCY.observe(process_time)
response.headers["X-Process-Time"] = str(process_time)
return response

We have defined a new histogram LATENCY that will measure the HTTP request latency in seconds. We use the Histogram class to define the histogram and specify the buckets that we want to use. We have also defined a new counter EXCEPTIONS that will keep track of the total number of HTTP exceptions.

We have also defined a new middleware function add_process_time_header that will measure the HTTP request latency and add the X-Process-Time header to the response.

Using CollectorRegistry

In the code above, we used registry variable to create our Prometheus counters and histograms.

registry = CollectorRegistry()

The CollectorRegistry is a container for all the metrics that you want to expose to Prometheus. It provides a way to register metric collectors and expose them as a single endpoint using the /metrics endpoint.

When you create a new metric, you can specify the registry that you want to use. By default, the prometheus_client library uses the global registry, but you can create your own registry and use it to isolate your metrics.

In the example above, we created a new registry and passed it to the Counter and Histogram constructors:

REQUESTS = Counter('requests_total', 'Total HTTP requests.', registry=registry)
LATENCY = Histogram('latency_seconds', 'HTTP request latency (seconds).')

We also passed the registry to the generate_latest function when returning the metrics:

return Response(generate_latest(registry=registry), media_type=CONTENT_TYPE_LATEST)

This ensures that only the metrics that we defined using this registry are returned.

Conclusion

In this tutorial, we have learned how to build a FastAPI application with Prometheus metrics. We have added counters to measure the total number of HTTP requests and histograms to measure the HTTP request latency. Prometheus metrics can be used to monitor and alert on system metrics, application metrics, and business metrics.


介绍

FastAPI 是一个现代化、高效的 Web 框架,用于基于 Python 3.7+ 构建 API,基于标准 Python 类型提示。它被设计成易于使用、开发快速、运行快速。Prometheus 是一个流行的开源监控系统,它从各种来源收集指标并将它们存储在时间序列数据库中。它用于监视和警报系统指标、应用程序指标和业务指标。

要求

在开始之前,请确保已安装以下要求:

  • Python 3.7+
  • FastAPI
  • Prometheus 客户端
  • Uvicorn

您可以使用 pip 安装这些软件包:

pip install fastapi prometheus_client uvicorn

创建 FastAPI 应用程序

我们将从创建一个新的 FastAPI 应用程序开始。创建一个名为 app.py 的新 Python 文件,并添加以下代码:

from fastapi import FastAPI

app = FastAPI()

@app.get("/")
async def root():
return {"message": "Hello World"}

这将创建一个具有单个端点 / 的新 FastAPI 应用程序,该端点返回一个带有消息 "Hello World" 的 JSON 响应。

要运行应用程序,您可以使用 uvicorn 服务器:

uvicorn app:app --reload

这将在 http://localhost:8000 上启动服务器。

添加 Prometheus 指标

现在,让我们为应用程序添加 Prometheus 指标。我们将使用 prometheus_client 库来创建和公开指标。

首先,让我们创建一个新的端点 /metrics,该端点将返回 Prometheus 指标。使用以下代码更新 app.py

from fastapi import FastAPI
from prometheus_client import Counter, generate_latest, CONTENT_TYPE_LATEST

app = FastAPI()

REQUESTS = Counter('requests_total', 'Total HTTP requests.')

@app.get("/")
async def root():
REQUESTS.inc()
return {"message": "Hello World"}

@app.get("/metrics")
async def metrics():
return Response(generate_latest(), media_type=CONTENT_TYPE_LATEST)

我们创建了一个名为 REQUESTS 的新计数器,该计数器将跟踪 HTTP 请求的总数。每次调用 / 端点时,我们使用 REQUESTS.inc() 增加计数器。

我们还添加了一个新的端点 /metrics,该端点返回 Prometheus 指标。我们使用 generate_latest() 函数以 Prometheus 暴露格式生成指标。

要使用带有指标端点的应用程序运行,可以使用以下命令:

uvicorn app:app --reload --workers 1 --host 0.0.0.0 --port 8000

这将在 http://localhost:8000 上启动服务器,并使用单个 worker。

添加 Prometheus 直方图

除了计数器之外,我们还可以使用直方图来测量请求的延迟。直方图测量值在一组存储桶上的分布。我们将使用 prometheus_client 库中的 Histogram 类来创建直方图。

使用以下代码更新 app.py

from fastapi import FastAPI, Response, Request
from prometheus_client import Counter, Histogram, generate_latest, CONTENT_TYPE_LATEST, CollectorRegistry
import time

app = FastAPI()
registry = CollectorRegistry()

REQUESTS = Counter('requests_total', 'Total HTTP requests.', registry=registry)
EXCEPTIONS = Counter('exceptions_total', 'Total HTTP exceptions.')
LATENCY = Histogram('latency_seconds', 'HTTP request latency (seconds).')

@app.get("/")
async def root():
REQUESTS.inc()
return {"message": "Hello World"}

@app.get("/metrics")
async def metrics():
return Response(generate_latest(registry=registry), media_type=CONTENT_TYPE_LATEST)

@app.exception_handler(Exception)
async def exceptions_handler(request, exc):
EXCEPTIONS.inc()
raise exc

@app.middleware("http")
async def add_process_time_header(request: Request, call_next):
start_time = time.time()
response = await call_next(request)
process_time = time.time() - start_time
LATENCY.observe(process_time)
response.headers["X-Process-Time"] = str(process_time)
return response

我们定义了一个名为 LATENCY 的新直方图,该直方图将以秒为单位测量 HTTP 请求延迟。我们使用 Histogram 类来定义直方图并指定要使用的桶。我们还定义了一个新计数器 EXCEPTIONS,该计数器将跟踪 HTTP 异常的总数。

我们还定义了一个新的中间件函数 add_process_time_header,该函数将测量 HTTP 请求延迟并将 X-Process-Time 标头添加到响应中。

使用 CollectorRegistry

在上面的代码中,我们使用 registry 变量来创建我们的 Prometheus 计数器和直方图。

registry = CollectorRegistry()

CollectorRegistry 是用于容纳要公开给 Prometheus 的所有指标的容器。它提供了一种注册度量收集器并使用 /metrics 端点将它们公开为单个端点的方法。

当您创建新指标时,可以指定要使用的注册表。默认情况下,prometheus_client 库使用全局注册表,但是您可以创建自己的注册表并使用它来隔离指标。

在上面的示例中,我们创建了一个新的注册表并将其传递给 CounterHistogram 构造函数:

REQUESTS = Counter('requests_total', 'Total HTTP requests.', registry=registry)
LATENCY = Histogram('latency_seconds', 'HTTP request latency (seconds).')

我们还在返回指标时将注册表传递给 generate_latest 函数:

return Response(generate_latest(registry=registry), media_type=CONTENT_TYPE_LATEST)

这确保我们只返回使用此注册表定义的指标。

结论

在本教程中,我们学习了如何构建带有 Prometheus 指标的 FastAPI 应用程序。我们添加了计数器以测量 HTTP 请求的总数,并添加了直方图以测量 HTTP 请求的延迟。Prometheus 指标可用于监视和警报系统指标、应用程序指标和业务指标。

Backend Developer Roadmap 2

· 阅读需 25 分钟

Roadmap: https://roadmap.sh/backend

本文隶属于 Roadmap 中的 Internet --> What is HTTP?

原文:https://cs.fyi/guide/http-in-depth

什么是 HTTP?

HTTP 是一个基于 TCP/IP 的应用层通信协议,标准化客户端和服务器之间的通信方式。它定义了互联网上请求和传输内容的方式。所谓应用层协议,是指它简单地抽象出了主机(客户端和服务器)之间的通信标准。HTTP 本身依赖于 TCP/IP,在客户端和服务器之间进行请求和响应。默认情况下,使用 TCP 端口 80,但也可以使用其他端口。HTTPS 使用端口 443。

HTTP/0.9 - 1991

HTTP/0.9 是 HTTP 的第一个版本,于 1991 年提出。它是迄今为止最简单的协议,只有一个叫做 GET 的方法。如果客户端需要访问服务器上的某个网页,它会发送以下简单请求:

GET /index.html

然后服务器的响应会如下所示:

(response body)
(connection closed)

也就是说,服务器会接收请求,响应 HTML 并在内容传输完成后立即关闭连接。HTTP/0.9 没有头部,只允许 GET 方法,响应的内容必须是 HTML。可以看出,该协议实际上只是后来版本的垫脚石。

HTTP/1.0 - 1996

HTTP/1.0 是 HTTP 的第一个版本,于 1996 年提出。与只能处理 HTML 响应的 HTTP/0.9 不同,HTTP/1.0 现在可以处理其他响应格式,例如图像、视频文件、纯文本或任何其他内容类型。它添加了更多的方法(例如 POST 和 HEAD),请求/响应格式发生了变化,HTTP 头被添加到请求和响应中,状态码被添加以标识响应,引入了字符集支持,多部分类型,授权,缓存,内容编码等。

以下是 HTTP/1.0 请求和响应的示例:

GET / HTTP/1.0
Host: cs.fyi
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5)
Accept: */*

正如您所看到的,客户端除了请求之外,还发送了自己的个人信息、所需的响应类型等信息。而在 HTTP/0.9 中,客户端永远无法发送此类信息,因为没有头部。

对于上述请求的示例响应可能如下所示:

HTTP/1.0 200 OK
Content-Type: text/plain
Content-Length: 137582
Expires: Thu, 05 Dec 1997 16:00:00 GMT
Last-Modified: Wed, 5 August 1996 15:55:28 GMT
Server: Apache 0.84

(response body)
(connection closed)

在响应的最开始,有 HTTP/1.0(HTTP 后跟版本号),然后是状态码 200,后跟原因短语(或状态码的描述,如果您愿意)。

在这个更新的版本中,请求和响应头仍然保持为 ASCII 编码,但响应正文可以是任何类型,例如图像、视频、HTML、纯文本或任何其他内容类型。因此,现在服务器可以向客户端发送任何内容类型;在介绍后不久,HTTP 中的“超文本”一词成为了误称。HMTP 或超媒体传输协议可能更有意义,但我想我们终身都会被这个名称困住。

HTTP/1.0 的主要缺点之一是无法在一个连接中处理多个请求。也就是说,每当客户端需要从服务器获取东西时,它都必须打开一个新的 TCP 连接,并且在单个请求被满足后,连接将被关闭。对于任何下一个需求,它都必须在新连接上进行。为什么这是不好的?好吧,假设您访问一个具有 10 个图像、5 个样式表和 5 个 JavaScript 文件的网页,总计有 20 个项目需要在请求该网页时获取。由于服务器在请求已被满足后立即关闭连接,因此将有一系列 20 个单独的连接,其中每个项目都将在其单独的连接上依次服务。这种大量的连接会导致严重的性能损失,因为需要新的 TCP 连接会因三次握手和慢启动而造成重大的性能损失。

三次握手

TCP 连接开始于三次握手,客户端和服务器在开始共享应用程序数据之前共享一系列数据包。

  • SYN - 客户端选择一个随机数,例如 x,并将其发送到服务器。
  • SYN ACK - 服务器通过向客户端发送一个由服务器选取的随机数(例如 y)和数字 x + 1 组成的 ACK 数据包来确认请求。
  • ACK - 客户端递增从服务器接收到的数字 y 并发送一个数字为 y + 1 的 ACK 数据包。

完成三次握手后,客户端和服务器之间的数据共享可以开始。需要注意的是,客户端可能会在发送最后一个 ACK 数据包后立即开始发送应用程序数据,但服务器仍然必须等待接收到 ACK 数据包才能满足请求。

然而,一些 HTTP/1.0 的实现尝试通过引入一个名为 Connection: keep-alive 的新头部来克服这个问题,该头部旨在告诉服务器“嘿服务器,不要关闭这个连接,我还需要它”。但它仍然没有得到广泛的支持,问题仍然存在。

除了是无连接的之外,HTTP 还是一种无状态协议,即服务器不维护有关客户端的信息,因此每个请求都必须具有服务器自行满足请求所需的信息,而不与任何旧请求相关联。因此,这加剧了问题,即除了客户端必须打开大量连接之外,它还必须在连接上发送一些冗余数据,导致带宽使用增加。

HTTP/1.1 - 1997

HTTP/1.1 是 HTTP 的下一个版本,于 1999 年发布。相较于 HTTP/1.0,它有很多改进:

  • 新的 HTTP 方法被添加,包括 PUT、PATCH、OPTIONS、DELETE;
  • 在 HTTP/1.0 中,Host 头部不是必须的,但在 HTTP/1.1 中,它是必须的;
  • 持久连接:在 HTTP/1.0 中,每次连接只有一个请求,连接在请求被满足后立即关闭,这导致性能和延迟问题。HTTP/1.1 引入了持久连接,连接默认不关闭,允许多个连续请求。要关闭连接,请求中必须有头部 Connection: close。客户端通常在最后一个请求中发送此头部以安全地关闭连接;
  • 管道化:它还引入了管道化的支持,客户端可以在同一连接上向服务器发送多个请求,而无需等待服务器响应,服务器必须按照收到请求的相同顺序发送响应。但是,您可能会问客户端如何知道第一个响应下载完成并且下一个响应的内容开始。为了解决这个问题,必须存在 Content-Length 头部,客户端可以使用它来标识响应结束并且可以开始等待下一个响应。

持久连接或管道化请求需要在响应中有 Content-Length 头部,以便客户端知道何时传输完成,可以发送下一个请求(按照正常顺序发送请求)或开始等待下一个响应(启用管道化)。但是,这种方法仍然存在问题。如果数据是动态的,服务器无法提前找到内容长度怎么办?在这种情况下,持久连接无法发挥作用!为了解决这个问题,HTTP/1.1 引入了分块编码。在这种情况下,服务器可以省略 Content-Length,而采用分块编码(稍后详细介绍)。但是,如果两种方法都不可用,则必须在请求结束时关闭连接。

  • Chunked Transfers 是一种传输方式,通常用于传输动态内容。在传输开始时,由于服务器无法确定 Content-Length,因此它可能会分块(每个块为一小部分)发送内容,并在发送每个块时添加 Content-Length。当所有块都被发送完毕,即整个传输完成时,服务器会发送一个空块,即 Content-Length 设置为零,以便通知客户端传输已完成。为了通知客户端使用了 Chunked Transfers,服务器会包括头部 Transfer-Encoding: chunked。
  • HTTP/1.1 不同于仅有基本认证的 HTTP/1.0,它包括摘要认证和代理认证
  • 缓存
  • 字节范围
  • 字符集
  • 语言协商
  • 客户端cookies
  • 增强的压缩支持
  • 新的状态码
  • ...等等

HTTP/1.1 在 1999 年发布,成为标准多年。虽然它相对于前身做了很多改进,但随着 Web 技术的日新月异,HTTP/1.1 开始显得有些过时。如今,加载网页比以往任何时候都更加资源密集。一个简单的网页需要打开超过 30 个连接。虽然 HTTP/1.1 有持久连接,但为什么还需要这么多连接呢?原因是,在 HTTP/1.1 中,任何时刻只能有一个未完成的连接。HTTP/1.1 尝试通过引入管道化来解决这个问题,但它并没有完全解决,因为头部阻塞会阻止慢速或重量级请求,一旦请求被卡在管道中,它将不得不等待下一个请求的响应。为了克服 HTTP/1.1 的这些缺点,开发人员开始实现各种解决方法,例如使用 spritesheets、在 CSS 中编码图像、单个巨大的 CSS/JavaScript 文件、域分片等。

SPDY - 2009

2009 年,Google 开始尝试使用替代协议来加速网络并改善 Web 安全性,同时减少 Web 页面的延迟。他们宣布了 SPDY 协议。

SPDY 是 Google 的商标,不是缩写。

人们发现,如果我们不断增加带宽,网络性能会在开始时增加,但在没有太多性能增益的时候会出现瓶颈。但是,如果您以同样的方式处理延迟,即如果我们不断降低延迟,就会有不断的性能增益。这是 SPDY 的性能提升的核心思想:降低延迟以提高网络性能。

对于那些不知道区别的人,延迟是延迟,即数据在源和目的地之间传输需要多长时间(以毫秒为单位),带宽是每秒传输的数据量(每秒比特数)。

SPDY 的功能包括复用、压缩、优先级、安全等。我不会详细介绍 SPDY,因为当我们进入下一节 HTTP/2 的细节时,您将会了解到它的主要灵感来源于 SPDY。

SPDY 实际上并没有试图替换 HTTP;它是 HTTP 上的翻译层,在应用程序层存在并在将请求发送到线上之前修改请求。它开始成为事实标准,并且大多数浏览器开始实现它。

2015 年,在 Google,他们不想有两个竞争标准,因此他们决定将其合并到 HTTP 中,同时推出 HTTP/2,废弃 SPDY。

HTTP/2 - 2015

HTTP/2 的设计旨在进行内容的低延迟传输。从旧版本 HTTP/1.1 的关键特性或区别来看,HTTP/2 采用了以下特性:

  • 使用二进制代替文本
  • 多路复用 - 在单个连接上进行多个异步 HTTP 请求
  • 使用 HPACK 进行报头压缩
  • 服务器推送 - 为单个请求提供多个响应
  • 请求优先级
  • 安全性

二进制协议

HTTP/2 试图通过将其变为二进制协议来解决 HTTP/1.x 中存在的延迟增加问题。作为二进制协议,它更易于解析,但与 HTTP/1.x 不同,它不再可读。HTTP/2 的主要构建块是帧和流

帧和流

HTTP 消息现在由一个或多个帧组成。有一个 HEADERS 帧用于元数据,一个 DATA 帧用于有效负载,还有几种其他类型的帧(HEADERS、DATA、RST_STREAM、SETTINGS、PRIORITY 等),你可以通过 HTTP/2 规范检查。

每个 HTTP/2 请求和响应都被赋予一个唯一的流 ID,并被划分为帧。帧只是二进制数据块。一组帧称为一个流。每个帧都有一个流 ID,用于标识它所属的流,每个帧都有一个公共头。另外,除了流 ID 是唯一的,值得一提的是,任何由客户端发起的请求都使用奇数,服务器的响应具有偶数流 ID。

除了 HEADERS 和 DATA 之外,我认为值得在此提到的另一种帧类型是 RST_STREAM,它是一种特殊的帧类型,用于中止某个流,即客户端可以发送此帧,让服务器知道我不再需要此流。在 HTTP/1.1 中,使服务器停止向客户端发送响应的唯一方法是关闭连接,这导致了延迟增加,因为必须为任何连续请求打开新连接。而在 HTTP/2 中,客户端可以使用 RST_STREAM 并停止接收特定流,而连接仍将保持打开状态,其他流仍将继续播放。

多路复用

HTTP/2 现在是二进制协议,正如我上面所说,它使用帧和流来请求和响应,一旦打开 TCP 连接,所有流都通过同一连接异步发送,而不需要打开任何其他连接。反过来,服务器也以相同的异步方式响应,响应没有顺序,客户端使用分配的流 ID 来标识特定数据包属于哪个流。这也解决了 HTTP/1.x 中存在的阻塞问题,即客户端不必等待需要时间的请求,其他请求仍在处理中。

报头压缩

HTTP/2 引入了报头压缩来优化发送的头部。该功能是在一个独立的 RFC 中实现的,旨在特别优化发送的头部。其要点在于,当我们始终从同一客户端访问服务器时,我们会反复发送许多冗余数据在头部中,有时可能会有增加头部大小的 cookie,导致带宽使用和延迟增加。为了克服这个问题,HTTP/2 引入了报头压缩。

与请求和响应不同,头部不是以 gzip 或 compress 等格式压缩的,而是采用了不同的头部压缩机制。该机制使用 Huffman 编码将文字值编码,并由客户端和服务器维护头部表。客户端和服务器都会在随后的请求中忽略所有重复的头部(例如用户代理等),并使用双方维护的头部表引用它们。

在谈论头部时,让我在这里添加一个信息,即头部仍然与 HTTP/1.1 中的头部相同,除了添加了一些伪头部,例如 :method:scheme:host:path

服务器推送

HTTP/2 的另一个巨大的功能是服务器推送,在这种情况下,服务器知道客户端将要请求某个资源,因此可以将其推送到客户端,而无需客户端请求。例如,假设浏览器加载一个网页,它解析整个页面以找出必须从服务器加载的远程内容,然后发送相应的请求以获取该内容。

服务器推送允许服务器通过推送它知道客户端将要请求的数据来减少往返次数。它是通过发送称为PUSH_PROMISE 的特殊帧来完成的,该帧通知客户端:“嘿,我要将此资源发送给你!不要请求它。” PUSH_PROMISE 帧与引发推送的流关联,并包含承诺的流ID,即服务器将推送的资源所在的流。

请求优先级

客户端可以在打开流的头部帧中包含优先级信息,以为流分配优先级。在任何其他时间,客户端都可以发送 PRIORITY 帧来更改流的优先级。

如果没有分配任何优先级信息,则服务器异步处理请求,即不按顺序处理请求。如果为流分配了优先级,则服务器根据此优先级信息决定需要分配多少资源来处理哪个请求。

安全性

在 HTTP/2 的安全性问题上进行了广泛的讨论,是否应该强制要求使用 TLS。最终决定不强制要求,但大多数供应商表示,只有在使用 TLS 时才支持 HTTP/2。因此,尽管 HTTP/2 不需要加密,但从某种程度上来说,它已经默认成为强制要求。当使用 TLS 实现 HTTP/2 时,会有一些要求,例如必须使用 TLS 版本 1.2 或更高版本,必须具有一定水平的最小密钥大小,需要使用临时密钥等。

HTTP/3 - 2022

HTTP/3 是 HTTP 的下一个版本。HTTP/3 是基于 QUIC 的协议。QUIC 是一个建立在 UDP 之上的传输层协议,旨在取代 TCP。它是一个多路复用、安全、基于流的协议,旨在降低延迟并提高性能。它是 TCP 和 HTTP/2 的继承者。

QUIC 是一个多路复用、安全、基于流的协议,旨在降低延迟并提高性能。它是 TCP 和 HTTP/2 的继承者。

多路复用

QUIC 是一种多路复用协议,意味着可以在单个连接上发送多个流。这类似于 HTTP/2,其中可以在单个连接上发送多个流。然而,与 HTTP/2 不同,QUIC 不仅限于 HTTP。它可以用于任何需要可靠、有序和容错的流传输的应用程序。

基于流

QUIC 是基于流的协议,这意味着数据以流的形式发送。每个流都由唯一的流 ID 标识。QUIC 使用单个流来双向发送数据。这类似于 HTTP/2,其中每个流由唯一的流 ID 标识,并且每个流是双向的。

不可靠数据报文

QUIC 使用不可靠的数据报来发送数据。这意味着 QUIC 不能保证数据将被传送到接收者。不过,QUIC 保证数据将按照发送的顺序传送。这类似于使用 UDP,其中数据以数据报的形式发送,而数据报不能保证会传送到接收者。

连接迁移

QUIC 支持连接迁移,这意味着 QUIC 连接可以从一个 IP 地址迁移到另一个 IP 地址。这类似于 TCP,其中 TCP 连接可以从一个 IP 地址迁移到另一个 IP 地址。

丢失恢复

QUIC 使用丢失恢复来从数据包丢失中恢复。QUIC 使用拥塞控制和丢失恢复的组合来从数据包丢失中恢复。这类似于 TCP,TCP 使用拥塞控制和丢失恢复的组合来从数据包丢失中恢复。

拥塞控制

QUIC 使用拥塞控制来控制网络上数据发送的速率。QUIC 使用拥塞控制和丢包恢复相结合来从数据包丢失中恢复。这类似于 TCP,其中 TCP 使用拥塞控制和丢失恢复相结合来从数据包丢失中恢复。

握手

QUIC 使用握手来在客户端和服务器之间建立安全连接。QUIC 使用 TLS 1.3 在客户端和服务器之间建立安全连接。这类似于 HTTP/2,其中使用 TLS 1.2 在客户端和服务器之间建立安全连接。

报头压缩

QUIC 使用头部压缩来减小头部的大小。QUIC 使用 HPACK 来压缩头部。这与 HTTP/2 类似,其中使用 HPACK 来压缩头部。

安全性

QUIC 使用 TLS 1.3 在客户端和服务器之间建立安全连接。这类似于 HTTP/2,其中使用 TLS 1.2 在客户端和服务器之间建立安全连接。

总结

在本文中,我们讨论了 HTTP/1.1,HTTP/2 和 HTTP/3。我们还讨论了 HTTP/1.1 和 HTTP/2 以及HTTP/2 和 HTTP/3 之间的区别。希望您会发现这篇文章有所帮助。