Ralph 实验:构建 SQLite UI

摘要

  • 我使用 Ralph 技巧配合 Claude Code,根据生成的 PRD 自主构建了一个基于浏览器的 SQLite UI。
  • Claude 逐个处理需求,在极少指导且不使用框架的情况下,生成了一个简单的静态应用。
  • 这种方法效果出奇地好,但速度慢、消耗 token 多,且在没有测试或版本控制的情况下存在风险。
  • 更强的护栏、更短的冲刺周期和更好的项目结构将显著提高可靠性和效率。
  • 总的来说,当时间和 token 成本可接受时,Ralph 被证明对这种无需人工干预的从零开发是有效的。

最终结果可以在 lochie.dev/sqlite-ui 找到。

简介

首先 —— Ralph Wiggum 和软件开发有什么关系?

Geoffrey Huntley 在 2025 年 7 月首次创造了这个术语

Ralph 是一种技巧。在其最纯粹的形式中,Ralph 就是一个 Bash 循环。

1
while :; do cat PROMPT.md | claude-code ; done

Ralph 可以取代大多数公司在全新项目上的大部分外包工作。它有缺陷,但这些缺陷是可以识别的,并且可以通过各种风格的提示词来解决。

本周早些时候,Matt Pocock 关于他的 Ralph 工作流的精彩 视频 出现在我的视野中。Matt 采纳了 Geoffrey 的最初想法,并将其调整以适应他自己的开发风格。

阅读全文

Claude Code 使用

Codex CLI vs Gemini CLI vs Claude Code: Which is the Best?

https://www.analyticsvidhya.com/blog/2025/08/codex-cli-vs-gemini-cli-vs-claude-code/

In 2025, several AI coding assistants have been released, which can be accessed directly from the terminal. Codex CLI, Gemini CLI, and Claude Code are some of the popular names that embed large language models into command-line workflows. These programming tools that can generate and fix code via natural language prompts are truly incredible. We document our evaluation of all three of these across different tasks to determine which is most useful.

阅读全文

Codex CLI vs Gemini CLI vs Claude Code:谁是最佳选择?

在 2025 年,市面上发布了几款可以直接通过终端访问的 AI 编程助手Codex CLIGemini CLIClaude Code 是其中几个热门名称,它们都将大型语言模型嵌入到命令行工作流程中。这些编程工具能够通过自然语言提示来生成和修复代码,能力着实令人惊叹。我们记录了对这三款工具在不同任务中的评估结果,以确定哪一个才是最实用的。

每款助手都基于先进的 AI 模型,如 o4-miniGemini 2.5 ProClaude Sonnet 4,旨在提高生产力。我们将它们放置在相同的环境中,并通过特定的指标在真实的编程任务中对它们进行测试。这些任务涵盖了从 Web 开发数据分析等多个领域,通过此评估,我们旨在清晰地展现每款助手的优势!

💻 参赛者介绍:Codex CLI、Gemini CLI 与 Claude Code

命令行正迅速成为下一代 AI 编程助手的战场。包括 OpenAI、谷歌和 Anthropic 在内的公司都发布了先进的 CLI(命令行界面)AI 编程助手,每款工具都将强大且令人印象深刻的功能直接带入终端。但它们之间有何不同?哪一个最适合你的工作流程?让我们来详细了解一下这些工具。

阅读全文

我给每个模型服务商『捐』了10块钱,只为了...

我整理了几家大厂的模型服务的地址、文档和基本介绍,方便大家可以使用和参考。
当然还有其他一些厂商提供了通用的模型服务或自家的模型服务,就不一一介绍了,未来模型服务最终会集中到几家大厂手里。

deepseek 官方服务

官方网站上的操作还是很简洁的,不像有些云服务商搞得人晕头转向。你可以很方便的充值和创建API key。
我年初的时候充了10块钱,现在还剩8块多。

它提供了OPENAI兼容的API, 所以下面三个信息非常关键:

阅读全文

Go之禅 - 基于Rob Pike思想的Go语言哲学

Go之禅 - 基于Rob Pike思想的Go语言哲学

1. 简单胜过聪明 (Simple is better than clever)

不要炫技,要解决问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// ✅ 简单直接的代码
func Max(a, b int) int {
if a > b {
return a
}
return b
}

// ❌ 过度聪明的代码
func Max(a, b int) int {
return (a + b + abs(a-b)) / 2 // 聪明但难懂
}

func abs(x int) int {
if x < 0 {
return -x
}
return x
}

2. 清晰胜过简洁 (Clear is better than concise)

代码首先要让人读懂

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// ✅ 清晰的代码
func CalculateUserScore(user *User) int {
totalPoints := 0
for _, activity := range user.Activities {

<!--more-->

totalPoints += activity.Points
}

if len(user.Activities) == 0 {
return 0
}

return totalPoints / len(user.Activities)

}

// ❌ 过度简洁的代码
func CalcScore(u *User) int {
if len(u.Acts) == 0 { return 0 }
return func() int { s := 0; for _, a := range u.Acts { s += a.Pts }; return s }() / len(u.Acts)
}

3. 组合胜过继承 (Composition over inheritance)

Go的接口和嵌入体现组合思想

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// ✅ 使用组合和接口
type Writer interface {
Write([]byte) (int, error)
}

type Logger interface {
Log(string)
}

type FileLogger struct {
writer Writer
prefix string
}

func (f *FileLogger) Log(message string) {
f.writer.Write([]byte(f.prefix + message))
}

type NetworkLogger struct {
writer Writer
endpoint string
}

func (n *NetworkLogger) Log(message string) {
n.writer.Write([]byte(n.endpoint + ": " + message))
}

// ❌ 如果Go有继承(反例)
// type BaseLogger struct { ... }
// type FileLogger struct { BaseLogger ... } // 继承会带来复杂性

4. 接口要小而专注 (Interfaces should be small and focused)

接口越小越好,单一职责

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// ✅ 小而专注的接口
type Reader interface {
Read([]byte) (int, error)
}

type Writer interface {
Write([]byte) (int, error)
}

type Closer interface {
Close() error
}

// 组合小接口
type ReadWriter interface {
Reader
Writer
}

type ReadWriteCloser interface {
Reader
Writer
Closer
}

// ❌ 大而全的接口
type FileHandler interface {
Read([]byte) (int, error)
Write([]byte) (int, error)
Seek(int64, int) (int64, error)
Close() error
Stat() (os.FileInfo, error)
Chmod(os.FileMode) error
// ... 太多方法
}

5. 并发不是并行 (Concurrency is not parallelism)

并发是关于结构,并行是关于执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// ✅ 并发结构 - 使用goroutine和channel组织程序
func ProcessTasks(tasks []Task) {
taskChan := make(chan Task, len(tasks))
resultChan := make(chan Result, len(tasks))

// 启动工作者goroutine
for i := 0; i < 3; i++ {
go worker(taskChan, resultChan)
}

// 发送任务
for _, task := range tasks {
taskChan <- task
}
close(taskChan)

// 收集结果
for i := 0; i < len(tasks); i++ {
result := <-resultChan
fmt.Printf("任务完成: %v\n", result)
}
}

func worker(tasks <-chan Task, results chan<- Result) {
for task := range tasks {
result := processTask(task)
results <- result
}
}

6. 通过通信来共享内存,而不是通过共享内存来通信 (Share memory by communicating)

Channel是Go的核心哲学

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// ✅ 通过channel通信
type Counter struct {
ch chan int
value int
}

func NewCounter() *Counter {
c := &Counter{
ch: make(chan int),
}
go c.run()
return c
}

func (c *Counter) run() {
for increment := range c.ch {
c.value += increment
}
}

func (c *Counter) Increment() {
c.ch <- 1
}

func (c *Counter) Add(n int) {
c.ch <- n
}

// ❌ 通过共享内存通信(需要锁)
type MutexCounter struct {
mu sync.Mutex
value int
}

func (c *MutexCounter) Increment() {
c.mu.Lock()
defer c.mu.Unlock()
c.value++
}

7. 错误是值,不是异常 (Errors are values, not exceptions)

显式错误处理胜过异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// ✅ 错误作为值处理
func ReadConfig(filename string) (*Config, error) {
data, err := os.ReadFile(filename)
if err != nil {
return nil, fmt.Errorf("读取配置文件失败: %w", err)
}

var config Config
if err := json.Unmarshal(data, &config); err != nil {
return nil, fmt.Errorf("解析配置失败: %w", err)
}

return &config, nil
}

func main() {
config, err := ReadConfig("config.json")
if err != nil {
log.Fatal(err)
}
// 使用config...
}

// ❌ 如果Go有异常(反例)
// func ReadConfig(filename string) *Config {
// data := mustReadFile(filename) // 可能抛出异常
// return mustParseConfig(data) // 隐藏的错误处理
// }

8. 不要设计大型接口 (Don't design with interfaces, discover them)

接口应该从使用中发现,而不是预先设计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// ✅ 从实际使用中发现接口
// 首先写具体实现
type FileStorage struct {
basePath string
}

func (f *FileStorage) Save(key string, data []byte) error {
return os.WriteFile(filepath.Join(f.basePath, key), data, 0644)
}

func (f *FileStorage) Load(key string) ([]byte, error) {
return os.ReadFile(filepath.Join(f.basePath, key))
}

// 当需要不同实现时,才抽象出接口
type Storage interface {
Save(string, []byte) error
Load(string) ([]byte, error)
}

// ❌ 预先设计大型接口
// type DataStore interface {
// Save(string, []byte) error
// Load(string) ([]byte, error)
// Delete(string) error
// List() ([]string, error)
// Backup() error
// Restore() error
// // ... 很多可能用不到的方法
// }

9. 空接口什么都不说 (The empty interface says nothing)

避免过度使用interface{}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// ✅ 使用具体类型或有意义的接口
func ProcessUsers(users []User) {
for _, user := range users {
fmt.Printf("处理用户: %s\n", user.Name)
}
}

type Processor interface {
Process() error
}

func RunProcessors(processors []Processor) {
for _, p := range processors {
if err := p.Process(); err != nil {
log.Printf("处理失败: %v", err)
}
}
}

// ❌ 过度使用空接口
func ProcessAnything(items []interface{}) {
for _, item := range items {
// 需要类型断言,失去了类型安全
switch v := item.(type) {
case User:
fmt.Printf("用户: %s\n", v.Name)
case Product:
fmt.Printf("产品: %s\n", v.Name)
default:
fmt.Printf("未知类型: %T\n", v)
}
}
}

10. Gofmt的风格就是每个人的风格 (Gofmt's style is no one's favorite, yet gofmt is everyone's favorite)

统一的代码格式胜过个人偏好

1
2
3
4
5
6
7
8
9
10
11
12
13
// ✅ 使用gofmt格式化的代码
func CalculateTotal(items []Item, taxRate float64) float64 {
var subtotal float64
for _, item := range items {
subtotal += item.Price * float64(item.Quantity)
}

tax := subtotal * taxRate
return subtotal + tax
}

// 所有Go代码都应该用gofmt格式化,保持一致性
// 不要手动调整格式,让工具处理

11. 小规模清晰胜过大规模复杂 (A little copying is better than a little dependency)

适度的代码重复胜过不必要的依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// ✅ 简单的重复实现
package userservice

func ValidateEmail(email string) bool {
return strings.Contains(email, "@") && len(email) > 5
}

package orderservice

func ValidateEmail(email string) bool {
return strings.Contains(email, "@") && len(email) > 5
}

// ❌ 为了避免重复引入复杂依赖
// import "github.com/complex-validation-library/v2/email"
//
// func ValidateEmail(email string) bool {
// validator := email.NewValidator(email.WithComplexRules())
// return validator.Validate(email)
// }

12. 系统调用、操作系统线程和互斥锁的代价很高,尽量避免 (Syscalls, OS threads, and mutexes are expensive)

理解并发原语的成本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// ✅ 使用goroutine和channel,避免系统级同步
func FanOut(input <-chan int, workers int) <-chan int {
output := make(chan int)

for i := 0; i < workers; i++ {
go func() {
for n := range input {
// 处理数据
result := process(n)
output <- result
}
}()
}

return output
}

// ❌ 过度使用互斥锁
type ExpensiveCounter struct {
mu sync.Mutex
value int
}

func (c *ExpensiveCounter) Get() int {
c.mu.Lock() // 每次读取都要加锁
defer c.mu.Unlock()
return c.value
}

func (c *ExpensiveCounter) Set(v int) {
c.mu.Lock()
defer c.mu.Unlock()
c.value = v
}

13. 缓存是有用的 (Caching is important)

但要简单明了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// ✅ 简单的缓存实现
type Cache struct {
mu sync.RWMutex
data map[string]interface{}
}

func NewCache() *Cache {
return &Cache{
data: make(map[string]interface{}),
}
}

func (c *Cache) Get(key string) (interface{}, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
value, exists := c.data[key]
return value, exists
}

func (c *Cache) Set(key string, value interface{}) {
c.mu.Lock()
defer c.mu.Unlock()
c.data[key] = value
}

// 使用示例
var userCache = NewCache()

func GetUser(id string) (*User, error) {
if cached, ok := userCache.Get(id); ok {
return cached.(*User), nil
}

user, err := fetchUserFromDB(id)
if err != nil {
return nil, err
}

userCache.Set(id, user)
return user, nil
}

14. 测试先行,但不要过度测试 (Test first, but don't over-test)

测试重要行为,不是实现细节

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// ✅ 测试重要的业务逻辑
func TestCalculateDiscount(t *testing.T) {
tests := []struct {
name string
amount float64
userType string
want float64
}{
{"普通用户无折扣", 100.0, "regular", 100.0},
{"VIP用户9折", 100.0, "vip", 90.0},
{"金牌用户8折", 100.0, "gold", 80.0},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := CalculateDiscount(tt.amount, tt.userType)
if got != tt.want {
t.Errorf("CalculateDiscount() = %v, want %v", got, tt.want)
}
})
}
}

// ❌ 过度测试内部实现
func TestCalculateDiscountInternals(t *testing.T) {
// 测试私有方法或内部状态变化
// 这种测试很脆弱,实现改变时就会失败
}

15. 如果你觉得需要泛型,请三思 (If you think you need generics, think again)

Go 1.18+有了泛型,但要谨慎使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// ✅ 合理使用泛型的场景
func Map[T, U any](slice []T, fn func(T) U) []U {
result := make([]U, len(slice))
for i, v := range slice {
result[i] = fn(v)
}
return result
}

func Filter[T any](slice []T, predicate func(T) bool) []T {
var result []T
for _, v := range slice {
if predicate(v) {
result = append(result, v)
}
}
return result
}

// 使用
numbers := []int{1, 2, 3, 4, 5}
doubled := Map(numbers, func(x int) int { return x * 2 })
evens := Filter(numbers, func(x int) bool { return x%2 == 0 })

// ❌ 过度使用泛型
type GenericProcessor[T, U, V, W any] interface {
Process(T, U) (V, W, error)
Validate(T) bool
Transform(U) V
// 过于复杂的泛型接口
}

16. 性能问题通常出在算法,不是语言 (Performance problems are usually algorithmic)

优化算法比优化语言特性更重要

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// ✅ 好的算法
func FindDuplicates(nums []int) []int {
seen := make(map[int]bool)
var duplicates []int

for _, num := range nums {
if seen[num] {
duplicates = append(duplicates, num)
} else {
seen[num] = true
}
}

return duplicates // O(n) 时间复杂度
}

// ❌ 差的算法
func FindDuplicatesSlow(nums []int) []int {
var duplicates []int

for i := 0; i < len(nums); i++ {
for j := i + 1; j < len(nums); j++ {
if nums[i] == nums[j] {
duplicates = append(duplicates, nums[i])
break
}
}
}

return duplicates // O(n²) 时间复杂度
}

17. 数据结构,不是算法,才是编程的核心 (Data structures, not algorithms, are central to programming)

正确的数据结构让程序简单明了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// ✅ 选择正确的数据结构
type UserIndex struct {
byID map[string]*User
byEmail map[string]*User
users []*User
}

func NewUserIndex() *UserIndex {
return &UserIndex{
byID: make(map[string]*User),
byEmail: make(map[string]*User),
users: make([]*User, 0),
}
}

func (ui *UserIndex) AddUser(user *User) {
ui.byID[user.ID] = user
ui.byEmail[user.Email] = user
ui.users = append(ui.users, user)
}

func (ui *UserIndex) FindByID(id string) *User {
return ui.byID[id] // O(1) 查找
}

func (ui *UserIndex) FindByEmail(email string) *User {
return ui.byEmail[email] // O(1) 查找
}

func (ui *UserIndex) GetAllUsers() []*User {
return ui.users // O(1) 获取所有用户
}

// ❌ 错误的数据结构选择
type BadUserStorage struct {
users []*User // 只用切片存储
}

func (us *BadUserStorage) FindByID(id string) *User {
for _, user := range us.users { // O(n) 查找
if user.ID == id {
return user
}
}
return nil
}

Go之禅总结: Go语言强调简单、清晰、组合和并发。Rob Pike的设计哲学体现在语言的每个角落:小而专注的接口、显式的错误处理、通过通信共享内存的并发模型,以及对简单性的坚持。Go不追求语言特性的丰富性,而是追求解决实际问题的有效性。

godotenv 库介绍

godotenv 是一个 Go 语言库,用于从 .env 文件加载环境变量到应用程序中 1 。它是 Ruby dotenv 项目的 Go 移植版本 1

背景

该库遵循十二要素应用方法论,将配置与代码分离。核心理念是:任何可能在部署环境之间变化的内容(如数据库资源句柄或外部服务凭证)都应该从代码中提取到环境变量中。

但在开发机器或运行多个项目的持续集成服务器上设置环境变量并不总是实用的。godotenv 在环境启动时从 .env 文件加载变量到 ENV 中。

安装方法

作为库使用

1
go get github.com/joho/godotenv

作为命令行工具

Go >= 1.17:

1
go install github.com/joho/godotenv/cmd/godotenv@latest

使用方法

基本用法

  1. 在项目根目录创建 .env 文件:
1
2
S3_BUCKET=YOURS3BUCKET
SECRET_KEY=YOURSECRETKEYGOESHERE

阅读全文

goskills:Claude Skills 功能强大,为我所用

在去年年底Claude推出MCP的功能后,MCP热度维持了小半年,MCP开发和研究风生水起。一年后,Claude又推出了一个新的概念:Skills

Claude 的各种应用(desktop、code cli、claude.ai等)现在可以使用 “Skills” 来改进其执行特定任务的方式。“Skills”是包含指令、脚本和资源的文件夹,Claude 应用可以根据需要加载这些文件夹。

Claude 应用只会在Skill与当前任务相关时才会使用该Skill。使用Skill后,克劳德可以更高效地完成特定任务,例如使用 Excel 或操作pdf。

在执行任务时,Claude 应用会扫描可用Skill以查找相关匹配项。找到匹配项后,它只会加载所需的最少信息和文件——既保证了Claude的运行速度,又能让他快速获取专业知识。

阅读全文

langchain + MCP:如虎添翼

MCP技术毋须多言了,上半年火的一塌糊涂,现在进入冷静期了。

langchain 本身就很方便的集成进程内的工具,但是加上 MCP的功能,就如虎添翼,可以充分利用网上上万的MCP的服务。

langchain 自从上个月融资了1.25亿美元之后,资金充足,也更加有动力推进产品的演化,相继发布了langchain/langgraph 1.0的版本。 langchain 1.0中统一了agent的创建,使用create_agent代替之前的create_tool_calling_agentcreate_react_agentcreate_json_agentcreate_xml_agent等。

阅读全文

Linux 中网络包的一生

从 write() 到 recv() 的实用导览。

你运行了 curl http://example.com,现在在终端里得到了一些 HTML,但实际上发生了什么?Linux 会让你的字节经过一套明确的步骤:选定一条路径,查找邻居的 MAC 地址,把包放在,请求网卡发送,然后在另一端执行反向的操作。

这篇文章尽量简单地解释这条路。如果你用过 Linux,运行过 curl,或者试过 ip addr,你完全有能够读懂这篇文章。不需要多么高深的背景。

注意:当我在这篇文章中说“内核”时,我实际上指的是“Linux 内核及其网络栈”,即内核中运行并移动数据包的部分。

我们要讲的内容

以下是我们将介绍的简化路径:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
your app
↓ write()/send()
TCP (segments your bytes)

IP (chooses where to send them)


<!--more-->

Neighbor/ARP (find the next-hop MAC)

qdisc (queueing, pacing)

driver/NIC (DMA to hardware)

wire / Wi‑Fi / fiber

NIC/driver (other host)

IP (checks, decides it's for us)

TCP (reassembles, ACKs)

server app

第一部分 传输:从 write() 到网线

步骤1:你的应用将字节传递给内核

你在 TCP 套接字上调用 send() 或 write()。 内核接受你的缓冲区并顺序发送。

  • TCP 会将大的缓冲区拆分成大小适合路径的 数据段 (segments)。通信双方会在 TCP 握手 期间通告各自的 最大数据段大小 (MSS),发送方会将自身的数据段大小限制在对方通告的 MSS 内,同时还要受到当前 路径最大传输单元 (Path MTU) 以及任何 IP/TCP 选项(如:时间戳)的进一步约束。
  • 它会为每个数据段标记 序列号 (sequence numbers),以便接收方能够按正确的顺序进行重组。

[!info] 🔌 套接字
套接字 只是你程序的一个 通信端点。对于 TCP 而言,内核会为每个套接字维护状态信息,包括:序列号拥塞窗口 (congestion window)定时器 等。

[!info] 🤝 TCP 握手 (TCP Handshake)
TCP 握手 在任何write()到达对端之前,TCP 会进行快速三步设置:1)客户端-> 服务器:SYN,并带有选项(MSS、SACK 允许、窗口规模、时间戳、ECN)。2)服务器 -> 客户端:SYN-ACK 及其选项。3) 客户端 -> 服务器:ACK.双方就初始序列号和选项达成一致;州已成立。TLS 说明:对于 HTTPS 来说,TLS 握手是在 TCP 建立后运行的。

[!todo] 试试看
下载东西时运行 ss -tni。你会看到随着数据在线路上传输并被应用消耗,TCP 的发送和接收队列大小会波动。

步骤2:内核决定将数据发送到哪里(路由)

内核会查看目标 IP 并选择最匹配的路由。在典型的主机上,问题归结为:这个 IP 是在我的本地网络上,还是我应该交给网关?

  • 如果地址位于直接连接的网络上,则会通过该接口发送。
  • 否则,它会连接到你的默认网关(通常是路由器)。

[!todo] 试试看
ip route get 192.0.2.10
它会打印接口、下一跳(如果有)以及内核将使用的源 IP。

[!info] 策略路由
内核可以使用 ip rule查询多个路由表(例如按源地址或标记选择路由)。大多数笔记本和服务器使用主路由表。

步骤 3:内核学习下一跳 MAC(邻居/ARP)

IP 路由选择下一跳。为了实际发送以太网帧,内核需要该跳的 MAC 地址。

  • 如果内核已经知道下一跳(在邻居/ARP 缓存里),那很好。
  • 如果没有,它会发送广播 ARP 请求:“谁拥有 10.0.0.1?告诉我你的 MAC。”回复已被缓存。

[!todo] 试试看
ip neigh show
你会看到像 10.0.0.1 lladdr 00:11:22:33:44:55 REACHABLE 这样的条目。

[!info] ARP vs NDP
IPv4 使用 ARP(广播)。IPv6 使用NDP(组播)。原理相同:找到你网络中某个 IP 的链路层地址。

步骤 4:数据包等待其轮到(qdisc)

在 NIC 发送任何内容之前,数据包会进入队列领域(qdisc)。你可以把它看作是一个小的等待队伍加上一个交通警察,内核可以:

  • 平滑突发流量,避免链路泛滥和缓冲膨胀(大队列->高延迟),
  • 在不同流之间公平共享带宽,
  • 如果你已经配置了整形/速率限制规则,请强制执行。

[!todo] 试试看
tc qdisc show dev eth0
tc -s qdisc show dev eth0 # same, but with counters/stats
把 eth0 替换成你的实际接口名称(例如 enp3s0,wlp2s0)。

[!info] MTU 与 MSS
MTU 是链路能承载的最大 L2 负载(典型以太网为 1500 字节)。
MSS 是段内最大的 TCP 有效载荷,仅次于 IP + TCP 头部和选项。
在 TCP 握手过程中,双方都宣布自己可以接收的 MSS,发送方不会发送比对方通告 MSS 更大的段,并且也会遵守路径 MTU(PMTU)。
在 IPv4 常见的无选项情况下,MSS≈MTU−40 字节。选项进一步降低 MSS。

步骤 5:网卡驱动和 NIC 负责繁重工作

内核的网络驱动将你的数据包交给网卡(NIC),并将其放入一个小的传输队列,卡从中读取。NIC 随后:

  • 直接从内存(使用 DMA)提取字节,并将其转化为链路上的比特流、铜缆上的微小电压变化、光纤上的光脉冲,或者如果你用 Wi-Fi 时是无线电波。

那才是真正的“接线”时刻:内存中的数据变成了网络上的信号。

[!todo] 试试看
ip -s link show dev eth0
ethtool -S eth0 # NIC stats
ethtool -k eth0 # offloads enabled
把 eth0 替换成你实际的接口名称。

[!info] offloads 卸载
TSO/GSO:让网卡或栈将大型缓冲区拆分为 MTU 大小的帧。
校验和卸载:传输时,网卡在内核递交包后填写 IP/TCP 校验和,发送前,接收时网卡可以验证校验和并告知内核结果。GRO(接收时):将许多小数据包合并成更大的块以节省 CPU。

[!info] DMA
直接内存访问(DMA)允许网卡通过总线(例如 PCIe)直接读写你在 RAM 中的数据,而 CPU 无需复制字节。这就是网卡能高效地从transmit ring拉取帧(并放置接收帧)的原因。

步骤 6:上线

在以太网上,网卡发送一个帧,内容如下:

1
[ dst MAC | src MAC | EtherType (IPv4) | IP header | TCP header | payload | FCS ]

交换机关心以太网头部:它们查看目标 MAC 地址,并将帧转发到正确的端口。

路由器会查看 IP 头部,减少 TTL / Hop Limit,并在(IPv4)更新头部校验和后再将数据包转发到下一跳。

每台交换机和路由器逐跳重复此作,直到路由器最终获得直接到目的网络的路由,并将数据包传送到服务器的局域网。

 [!info] frame vs packet 帧与包
 数据包是 IP 级单元(IP 头部+TCP/UDP+有效载荷)。 帧是指该数据包在特定链路上(例如以太网)上通过 src/dst MAC 和校验和传输的方式。

第二部分 接收:从线路回传到你的应用

步骤 7:网卡将数据传递给内核(NAPI)

在服务器端,网卡将收到的帧写入receive rings(内存中的小队列)。Linux 内核随后使用 NAPI 高效拉取数据包:快速中断后切换为轮询,一次性处理一批数据包。

[!info] NAPI
如果每个数据包都触发了满中断,忙碌的网卡可能会让 CPU 不堪重负。NAPI 的诀窍是:

  • 发起一次中断,
  • 暂时切换到轮询以排空大量数据包,
  • 然后重新启用中断。

中断减少,吞吐量更好。

步骤 8:IP 检查数据包并决定下一步行动

内核会验证 IP 头部(版本、校验和、TTL 等),然后问:“这个包是给我的吗?”

  • 如果目标 IP 与服务器的某个地址匹配,则该 IP 是本地的,并在堆栈中向上移动。
  • 如果没有,且启用了 IP 转发,内核可能会将它转发, 类似 Linux 路由器的表现。
  • 否则,数据包会被丢弃。

如果你使用防火墙,这时像 PREROUTING 和 INPUT(nftables/iptables)这样的钩子可以过滤、记录或 DNAT 流量,然后才会被送到本地套接字。在 POSTROUTING 中会发生 SNAT/假面舞会。对于本地生成的数据包,DNAT 也可能出现在输出中。

[!todo] 试试看
sudo nft list ruleset
# or, with iptables:
sudo iptables -L -n -v
sudo iptables -t nat -L -n -v

步骤 9:TCP 重新组装、确认并唤醒应用

TCP 协议栈会将段排序,检查缺失部分,并发送 ACK。当数据准备好时,它会唤醒在 recv() 中等待的进程。

[!todo] 试试看
ss -tni 'sport = :80 or dport = :80'
随着应用读取,接收队列(Recv-Q)会随着增长和缩小。

简短实用笔记

回环很特别(而且速度快)

发送到 127.0.0.1 的数据包从未到达物理网卡。路由仍然会进行,但所有内容都保留在仅软件的 lo 接口内存中。

桥接与路由(同一盒子,不同角色)

如果盒子是桥接器(例如带有 br0),它会在第 2 层转发帧,不会改变 TTL。如果是路由,它会在第 3 层转发,TTL 下降一跳。

NAT hairpin(为什么内部客户端会访问外部 IP)

通过路由器的公共 IP 从同一局域网访问服务需要“发夹式 NAT”。如果在这种情况下连接重置,请检查预路由后路由 NAT 规则。

IPv6

把 ARP 换成新民主党。否则,路径是相同的:

1
2
ip -6 route
ip -6 neigh

UDP 是故意的不同

UDP 不做排序、重传或拥塞控制。发送路径使用 udp_sendmsg,接收路径传输完整的数据报。你的应用自己负责处理数据丢失。

亲自看看(10个快速指令)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# 1) Where would the kernel send a packet?
ip route get 192.0.2.10

# 2) What routes and rules exist?
ip route; ip rule

# 3) Who's my next hop?
ip neigh show

# 4) What's my firewall/NAT doing?
sudo nft list ruleset
# or:
sudo iptables -L -n -v
sudo iptables -t nat -L -n -v

# 5) Which sockets are active?
ss -tni

# 6) What's on the wire (swap eth0/host as needed)?
sudo tcpdump -ni eth0 -e -vvv 'host 192.0.2.10 and tcp port 80'

# 7) Are my queues healthy?
tc -s qdisc show dev eth0

# 8) Is my NIC happy?
ip -s link show dev eth0
ethtool -S eth0

# 9) Are counters hinting at a problem?
nstat -a | grep -E 'InErrors|OutErrors|InNoRoutes|InOctets|OutOctets'
# (Use `-z` instead of `-a` if you explicitly want to zero the counters.)

# 10) Is the path MTU safe?
tracepath 192.0.2.10 # discovers PMTU via ICMP: IPv4 "Fragmentation Needed" (Type 3, Code 4) / IPv6 "Packet Too Big" (Type 2)

ARP/邻居问题

IP neigh 显示失败或不断切换状态——> L2 可达性、VLAN 标记或交换机过滤问题。

MTU / PMTU 黑洞

小 ping 可以正常,大传输会卡顿——>MTU 不匹配或 ICMP 被阻。

允许 PMTU 信号通过防火墙(IPv4:ICMP 类型 3 代码 4“需要分段”,IPv6:ICMPv6 类型 2“数据包过大”)或修复 MTU。

反向路径滤波器的痛点

非对称路由 + rp_filter=1 会丢弃返回流量。使用 rp_filter=2(松散)或使路由对称。

NAT 的惊喜

SNAT/MASQUERADE 错误地重写了来源,所以回复无效。检查 NAT 规则和 conntrack -L。

Backlog/accept pressure

新连接在大负载下重置 -> 增加应用backlognet.core.somaxconn,确保应用能及时处理accept

爆发产生的缓冲膨胀

如果遇到队列过大严重延迟尖峰的问题,请选择 fq_codel(或 fq)作为 队列规则 (qdisc),并且如果应用程序支持,请启用 数据包限速 (pacing)

内核调用路径(如果你感兴趣的话)发送(典型的 TCP 路径):

1
2
3
4
5
6
7
8
9
10
tcp_sendmsg
-> tcp_push_pending_frames
-> __tcp_transmit_skb
-> ip_queue_xmit
-> ip_local_out / ip_output
-> ip_finish_output
-> neigh_output
-> dev_queue_xmit
-> qdisc / sch_direct_xmit
-> ndo_start_xmit (driver)

接收(典型的 IPv4 TCP 路径):

1
2
3
4
5
6
7
8
9
10
napi_gro_receive / netif_receive_skb
-> __netif_receive_skb_core
-> ip_rcv
-> ip_rcv_finish
-> ip_local_deliver
-> ip_local_deliver_finish
-> tcp_v4_rcv
-> tcp_v4_do_rcv
-> tcp_data_queue (wake reader)

一份小清单,随时备着

  • Socket - Your program’s handle for network I/O.
  • MTU / MSS - Max link payload / max TCP payload.
  • ARP / NDP - Find the link-layer address (IPv4 / IPv6).
  • qdisc - Per-device queueing policy (fairness, shaping).
  • NAPI - Efficient receive: interrupt, then poll a batch.
  • TSO/GSO/GRO - Offloads to split/merge packets and save CPU.
  • Conntrack - Kernel’s flow table (used by NAT and filtering).
  • PREROUTING/INPUT/OUTPUT/POSTROUTING - Firewall hook points.
  • DMA (Direct Memory Access) - Hardware reads/writes RAM without CPU copies, NICs use this for TX/RX rings.
  • TTL / Hop Limit - Per‑packet counter decremented by each router (TTL in IPv4, Hop Limit in IPv6). When it hits zero, the packet is dropped.
  • FCS (Frame Check Sequence) - Link‑layer CRC at the end of an Ethernet frame, used to detect bit errors on the wire.