主题
gRPC开发
📋 课程目标
- 了解gRPC的基本概念和优势
- 掌握Protocol Buffers (protobuf)的定义和使用
- 学习gRPC的四种服务类型和实现
- 掌握不同语言中的gRPC开发
- 了解gRPC的安全认证和错误处理
- 学习gRPC的性能优化和最佳实践
- 掌握gRPC与其他API技术的集成
🎯 适用人群
- 后端工程师
- API开发人员
- 微服务架构师
- 对高性能API感兴趣的开发人员
- 全栈开发人员
1. gRPC 概述
1.1 什么是gRPC
gRPC是由Google开发的高性能、开源的通用RPC框架,基于HTTP/2协议传输,使用Protocol Buffers作为接口描述语言。
1.2 gRPC的优势
- 高性能:基于HTTP/2,支持多路复用和二进制传输
- 强类型:使用Protocol Buffers定义接口,提供类型安全
- 跨语言:支持多种编程语言
- 自动代码生成:根据接口定义自动生成客户端和服务端代码
- 流式通信:支持单向和双向流式传输
- 内置认证:支持SSL/TLS、令牌认证等
1.3 gRPC与RESTful API的比较
| 特性 | gRPC | RESTful API |
|---|---|---|
| 传输协议 | HTTP/2 | HTTP/1.1或HTTP/2 |
| 数据格式 | Protocol Buffers (二进制) | JSON (文本) |
| 接口定义 | 强类型IDL | 无统一标准 |
| 代码生成 | 自动生成 | 手动编写 |
| 性能 | 更高 | 一般 |
| 流式通信 | 支持 | 有限支持 (WebSocket) |
| 浏览器支持 | 需gRPC-Web | 原生支持 |
| 生态系统 | 相对成熟 | 非常成熟 |
2. Protocol Buffers 基础
2.1 Protocol Buffers 简介
Protocol Buffers (protobuf)是一种与语言无关、平台无关的可扩展机制,用于序列化结构化数据。
2.2 定义.proto文件
protobuf
// user.proto
syntax = "proto3";
package user;
message User {
int32 id = 1;
string name = 2;
string email = 3;
repeated string roles = 4;
bool active = 5;
}
message GetUserRequest {
int32 id = 1;
}
message GetUserResponse {
User user = 1;
}
message ListUsersRequest {
int32 page = 1;
int32 page_size = 2;
}
message ListUsersResponse {
repeated User users = 1;
int32 total = 2;
}
message CreateUserRequest {
string name = 1;
string email = 2;
repeated string roles = 3;
}
message CreateUserResponse {
User user = 1;
}
message UpdateUserRequest {
int32 id = 1;
string name = 2;
string email = 3;
repeated string roles = 4;
bool active = 5;
}
message UpdateUserResponse {
User user = 1;
}
message DeleteUserRequest {
int32 id = 1;
}
message DeleteUserResponse {
bool success = 1;
}
service UserService {
rpc GetUser(GetUserRequest) returns (GetUserResponse);
rpc ListUsers(ListUsersRequest) returns (ListUsersResponse);
rpc CreateUser(CreateUserRequest) returns (CreateUserResponse);
rpc UpdateUser(UpdateUserRequest) returns (UpdateUserResponse);
rpc DeleteUser(DeleteUserRequest) returns (DeleteUserResponse);
}2.3 数据类型
| Proto类型 | Go类型 | Java类型 | Python类型 |
|---|---|---|---|
| double | float64 | double | float |
| float | float32 | float | float |
| int32 | int32 | int | int |
| int64 | int64 | long | int |
| uint32 | uint32 | int | int |
| uint64 | uint64 | long | int |
| sint32 | int32 | int | int |
| sint64 | int64 | long | int |
| bool | bool | boolean | bool |
| string | string | String | str |
| bytes | []byte | ByteString | bytes |
2.4 编译.proto文件
2.4.1 安装Protocol Buffers编译器
bash
# Ubuntu/Debian
apt install protobuf-compiler
# macOS
brew install protobuf
# Windows
# 从GitHub下载protobuf编译器2.4.2 编译生成代码
bash
# 生成Go代码
protoc --go_out=. --go-grpc_out=. user.proto
# 生成Java代码
protoc --java_out=. user.proto
# 生成Python代码
protoc --python_out=. user.proto
# 生成JavaScript代码
protoc --js_out=. user.proto3. gRPC 服务类型
3.1 简单RPC (Unary RPC)
最简单的RPC类型,客户端发送单个请求,服务器返回单个响应。
protobuf
rpc GetUser(GetUserRequest) returns (GetUserResponse);3.2 服务器流式RPC (Server Streaming RPC)
客户端发送单个请求,服务器返回多个响应。
protobuf
rpc ListUsers(ListUsersRequest) returns (stream ListUsersResponse);3.3 客户端流式RPC (Client Streaming RPC)
客户端发送多个请求,服务器返回单个响应。
protobuf
rpc CreateUsers(stream CreateUserRequest) returns (CreateUsersResponse);3.4 双向流式RPC (Bidirectional Streaming RPC)
客户端和服务器都可以发送多个请求和响应。
protobuf
rpc Chat(stream ChatMessage) returns (stream ChatMessage);4. gRPC 服务端实现
4.1 Go 实现
4.1.1 环境搭建
bash
# 安装依赖
go get google.golang.org/grpc
go get google.golang.org/protobuf/cmd/protoc-gen-go
go get google.golang.org/grpc/cmd/protoc-gen-go-grpc4.1.2 服务端代码
go
package main
import (
"context"
"fmt"
"log"
"net"
"sync"
"google.golang.org/grpc"
pb "path/to/proto/user"
)
// UserService 实现
type userService struct {
pb.UnimplementedUserServiceServer
users map[int32]*pb.User
nextUserID int32
mu sync.Mutex
}
// 初始化
func NewUserService() *userService {
return &userService{
users: make(map[int32]*pb.User),
nextUserID: 1,
}
}
// GetUser 实现
func (s *userService) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.GetUserResponse, error) {
s.mu.Lock()
defer s.mu.Unlock()
user, exists := s.users[req.Id]
if !exists {
return nil, fmt.Errorf("user not found")
}
return &pb.GetUserResponse{User: user}, nil
}
// ListUsers 实现
func (s *userService) ListUsers(ctx context.Context, req *pb.ListUsersRequest) (*pb.ListUsersResponse, error) {
s.mu.Lock()
defer s.mu.Unlock()
// 简单分页
var users []*pb.User
total := len(s.users)
start := (req.Page - 1) * req.PageSize
end := start + req.PageSize
for id, user := range s.users {
if int32(len(users)) >= req.PageSize {
break
}
if int32(len(users)) >= start {
users = append(users, user)
}
}
return &pb.ListUsersResponse{
Users: users,
Total: int32(total),
}, nil
}
// CreateUser 实现
func (s *userService) CreateUser(ctx context.Context, req *pb.CreateUserRequest) (*pb.CreateUserResponse, error) {
s.mu.Lock()
defer s.mu.Unlock()
user := &pb.User{
Id: s.nextUserID,
Name: req.Name,
Email: req.Email,
Roles: req.Roles,
Active: true,
}
s.users[s.nextUserID] = user
s.nextUserID++
return &pb.CreateUserResponse{User: user}, nil
}
// UpdateUser 实现
func (s *userService) UpdateUser(ctx context.Context, req *pb.UpdateUserRequest) (*pb.UpdateUserResponse, error) {
s.mu.Lock()
defer s.mu.Unlock()
user, exists := s.users[req.Id]
if !exists {
return nil, fmt.Errorf("user not found")
}
if req.Name != "" {
user.Name = req.Name
}
if req.Email != "" {
user.Email = req.Email
}
if len(req.Roles) > 0 {
user.Roles = req.Roles
}
if req.Active != user.Active {
user.Active = req.Active
}
return &pb.UpdateUserResponse{User: user}, nil
}
// DeleteUser 实现
func (s *userService) DeleteUser(ctx context.Context, req *pb.DeleteUserRequest) (*pb.DeleteUserResponse, error) {
s.mu.Lock()
defer s.mu.Unlock()
_, exists := s.users[req.Id]
if !exists {
return &pb.DeleteUserResponse{Success: false}, nil
}
delete(s.users, req.Id)
return &pb.DeleteUserResponse{Success: true}, nil
}
func main() {
// 创建监听
lis, err := net.Listen("tcp", ":50051")
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
// 创建gRPC服务器
s := grpc.NewServer()
// 注册服务
pb.RegisterUserServiceServer(s, NewUserService())
log.Println("gRPC server started on port 50051")
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}4.2 Python 实现
4.2.1 环境搭建
bash
# 安装依赖
pip install grpcio grpcio-tools protobuf4.2.2 服务端代码
python
# server.py
import grpc
from concurrent import futures
import time
import user_pb2
import user_pb2_grpc
class UserService(user_pb2_grpc.UserServiceServicer):
def __init__(self):
self.users = {}
self.next_user_id = 1
def GetUser(self, request, context):
user = self.users.get(request.id)
if not user:
context.set_code(grpc.StatusCode.NOT_FOUND)
context.set_details('User not found')
return user_pb2.GetUserResponse()
return user_pb2.GetUserResponse(user=user)
def ListUsers(self, request, context):
users = list(self.users.values())
# 简单分页
start = (request.page - 1) * request.page_size
end = start + request.page_size
paginated_users = users[start:end]
return user_pb2.ListUsersResponse(
users=paginated_users,
total=len(users)
)
def CreateUser(self, request, context):
user = user_pb2.User(
id=self.next_user_id,
name=request.name,
email=request.email,
roles=request.roles,
active=True
)
self.users[self.next_user_id] = user
self.next_user_id += 1
return user_pb2.CreateUserResponse(user=user)
def UpdateUser(self, request, context):
user = self.users.get(request.id)
if not user:
context.set_code(grpc.StatusCode.NOT_FOUND)
context.set_details('User not found')
return user_pb2.UpdateUserResponse()
if request.name:
user.name = request.name
if request.email:
user.email = request.email
if request.roles:
user.roles.extend(request.roles)
if request.active is not None:
user.active = request.active
return user_pb2.UpdateUserResponse(user=user)
def DeleteUser(self, request, context):
if request.id in self.users:
del self.users[request.id]
return user_pb2.DeleteUserResponse(success=True)
return user_pb2.DeleteUserResponse(success=False)
def serve():
server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
user_pb2_grpc.add_UserServiceServicer_to_server(UserService(), server)
server.add_insecure_port('[::]:50051')
server.start()
print("gRPC server started on port 50051")
try:
while True:
time.sleep(86400) # 一天
except KeyboardInterrupt:
server.stop(0)
if __name__ == '__main__':
serve()5. gRPC 客户端实现
5.1 Go 客户端
go
package main
import (
"context"
"fmt"
"log"
"google.golang.org/grpc"
pb "path/to/proto/user"
)
func main() {
// 连接服务器
conn, err := grpc.Dial("localhost:50051", grpc.WithInsecure(), grpc.WithBlock())
if err != nil {
log.Fatalf("did not connect: %v", err)
}
defer conn.Close()
// 创建客户端
c := pb.NewUserServiceClient(conn)
// 创建用户
createResp, err := c.CreateUser(context.Background(), &pb.CreateUserRequest{
Name: "John Doe",
Email: "john@example.com",
Roles: []string{"admin", "user"},
})
if err != nil {
log.Fatalf("could not create user: %v", err)
}
fmt.Printf("Created user: %v\n", createResp.User)
// 获取用户
getResp, err := c.GetUser(context.Background(), &pb.GetUserRequest{
Id: createResp.User.Id,
})
if err != nil {
log.Fatalf("could not get user: %v", err)
}
fmt.Printf("Got user: %v\n", getResp.User)
// 更新用户
updateResp, err := c.UpdateUser(context.Background(), &pb.UpdateUserRequest{
Id: createResp.User.Id,
Name: "John Smith",
Email: "john.smith@example.com",
})
if err != nil {
log.Fatalf("could not update user: %v", err)
}
fmt.Printf("Updated user: %v\n", updateResp.User)
// 列出用户
listResp, err := c.ListUsers(context.Background(), &pb.ListUsersRequest{
Page: 1,
PageSize: 10,
})
if err != nil {
log.Fatalf("could not list users: %v", err)
}
fmt.Printf("Total users: %d\n", listResp.Total)
for _, user := range listResp.Users {
fmt.Printf("User: %v\n", user)
}
// 删除用户
deleteResp, err := c.DeleteUser(context.Background(), &pb.DeleteUserRequest{
Id: createResp.User.Id,
})
if err != nil {
log.Fatalf("could not delete user: %v", err)
}
fmt.Printf("Delete user success: %v\n", deleteResp.Success)
}5.2 Python 客户端
python
# client.py
import grpc
import user_pb2
import user_pb2_grpc
def run():
# 连接服务器
with grpc.insecure_channel('localhost:50051') as channel:
# 创建客户端
stub = user_pb2_grpc.UserServiceStub(channel)
# 创建用户
create_response = stub.CreateUser(user_pb2.CreateUserRequest(
name='John Doe',
email='john@example.com',
roles=['admin', 'user']
))
print(f"Created user: {create_response.user}")
# 获取用户
get_response = stub.GetUser(user_pb2.GetUserRequest(
id=create_response.user.id
))
print(f"Got user: {get_response.user}")
# 更新用户
update_response = stub.UpdateUser(user_pb2.UpdateUserRequest(
id=create_response.user.id,
name='John Smith',
email='john.smith@example.com'
))
print(f"Updated user: {update_response.user}")
# 列出用户
list_response = stub.ListUsers(user_pb2.ListUsersRequest(
page=1,
page_size=10
))
print(f"Total users: {list_response.total}")
for user in list_response.users:
print(f"User: {user}")
# 删除用户
delete_response = stub.DeleteUser(user_pb2.DeleteUserRequest(
id=create_response.user.id
))
print(f"Delete user success: {delete_response.success}")
if __name__ == '__main__':
run()6. gRPC 安全认证
6.1 SSL/TLS 认证
6.1.1 生成证书
bash
# 生成CA密钥
openssl genrsa -out ca.key 2048
# 生成CA证书
openssl req -x509 -new -nodes -key ca.key -sha256 -days 365 -out ca.crt -subj "/CN=localhost"
# 生成服务器密钥
openssl genrsa -out server.key 2048
# 生成服务器CSR
openssl req -new -key server.key -out server.csr -subj "/CN=localhost"
# 生成服务器证书
openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt -days 365 -sha2566.1.2 服务端配置
go
// 加载证书
creds, err := credentials.NewServerTLSFromFile("server.crt", "server.key")
if err != nil {
log.Fatalf("failed to load credentials: %v", err)
}
// 创建gRPC服务器
opts := []grpc.ServerOption{grpc.Creds(creds)}
s := grpc.NewServer(opts...)6.1.3 客户端配置
go
// 加载CA证书
creds, err := credentials.NewClientTLSFromFile("ca.crt", "localhost")
if err != nil {
log.Fatalf("failed to load credentials: %v", err)
}
// 连接服务器
conn, err := grpc.Dial("localhost:50051", grpc.WithTransportCredentials(creds))6.2 令牌认证
6.2.1 实现认证中间件
go
// 认证中间件
func authInterceptor() grpc.UnaryServerInterceptor {
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
// 从上下文获取令牌
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return nil, status.Errorf(codes.Unauthenticated, "missing metadata")
}
tokens := md.Get("authorization")
if len(tokens) == 0 {
return nil, status.Errorf(codes.Unauthenticated, "missing authorization token")
}
token := tokens[0]
// 验证令牌
if !validateToken(token) {
return nil, status.Errorf(codes.Unauthenticated, "invalid authorization token")
}
// 调用处理函数
return handler(ctx, req)
}
}
// 验证令牌
func validateToken(token string) bool {
// 实际应用中应该验证JWT等
return token == "valid-token"
}6.2.2 客户端设置令牌
go
// 创建上下文并添加令牌
md := metadata.New(map[string]string{
"authorization": "valid-token",
})
ctx := metadata.NewOutgoingContext(context.Background(), md)
// 调用方法
resp, err := c.GetUser(ctx, &pb.GetUserRequest{Id: 1})7. gRPC 性能优化
7.1 服务端优化
- 使用连接池:复用gRPC连接
- 设置合理的超时:避免长时间阻塞
- 使用异步处理:处理耗时操作
- 优化序列化:减少数据大小
- 使用Protobuf优化:合理设计消息结构
- 开启HTTP/2特性:如头部压缩
7.2 客户端优化
- 复用连接:避免频繁创建连接
- 批量请求:减少网络往返
- 使用流式RPC:适合大量数据传输
- 设置合理的重试策略:处理临时错误
- 监控和限流:避免过载
7.3 网络优化
- 使用gRPC-Web:在浏览器中使用gRPC
- 设置合理的keepalive:保持连接活跃
- 使用压缩:减少网络传输量
- 优化DNS解析:使用缓存
8. gRPC 错误处理
8.1 标准错误码
| 错误码 | 描述 | HTTP状态码 |
|---|---|---|
| OK | 成功 | 200 |
| CANCELLED | 操作被取消 | 499 |
| UNKNOWN | 未知错误 | 500 |
| INVALID_ARGUMENT | 参数无效 | 400 |
| DEADLINE_EXCEEDED | 超出截止时间 | 504 |
| NOT_FOUND | 资源未找到 | 404 |
| ALREADY_EXISTS | 资源已存在 | 409 |
| PERMISSION_DENIED | 权限被拒绝 | 403 |
| UNAUTHENTICATED | 未认证 | 401 |
| RESOURCE_EXHAUSTED | 资源耗尽 | 429 |
| FAILED_PRECONDITION | 前置条件失败 | 400 |
| ABORTED | 操作被中止 | 409 |
| OUT_OF_RANGE | 超出范围 | 400 |
| UNIMPLEMENTED | 未实现 | 501 |
| INTERNAL | 内部错误 | 500 |
| UNAVAILABLE | 服务不可用 | 503 |
| DATA_LOSS | 数据丢失 | 500 |
8.2 服务端返回错误
go
// 返回错误
if !exists {
return nil, status.Errorf(codes.NotFound, "user not found: %d", req.Id)
}8.3 客户端处理错误
go
// 处理错误
resp, err := c.GetUser(ctx, &pb.GetUserRequest{Id: 1})
if err != nil {
if status.Code(err) == codes.NotFound {
fmt.Println("User not found")
} else {
log.Fatalf("Error: %v", err)
}
}9. gRPC 与其他技术集成
9.1 与HTTP/REST集成
9.1.1 使用gRPC-Gateway
bash
# 安装gRPC-Gateway
go get github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway
# 生成Gateway代码
protoc --grpc-gateway_out=. --grpc-gateway_opt=paths=source_relative user.proto9.1.2 配置Gateway
go
// 启动HTTP服务器
mux := runtime.NewServeMux()
opts := []grpc.DialOption{grpc.WithTransportCredentials(insecure.NewCredentials())}
// 注册Gateway
if err := pb.RegisterUserServiceHandlerFromEndpoint(ctx, mux, "localhost:50051", opts); err != nil {
log.Fatalf("Failed to register gateway: %v", err)
}
// 启动服务器
http.ListenAndServe(":8080", mux)9.2 与Kubernetes集成
9.2.1 部署gRPC服务
yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: user-service
spec:
replicas: 3
selector:
matchLabels:
app: user-service
template:
metadata:
labels:
app: user-service
spec:
containers:
- name: user-service
image: user-service:latest
ports:
- containerPort: 50051
---
apiVersion: v1
kind: Service
metadata:
name: user-service
spec:
selector:
app: user-service
ports:
- port: 50051
targetPort: 50051
type: ClusterIP9.2.2 使用服务发现
go
// 使用Kubernetes服务发现
conn, err := grpc.Dial("user-service:50051", grpc.WithInsecure())9.3 与Prometheus集成
9.3.1 监控gRPC服务
go
// 安装依赖
go get github.com/grpc-ecosystem/go-grpc-prometheus
// 注册监控
import "github.com/grpc-ecosystem/go-grpc-prometheus"
// 创建监控拦截器
prometheusInterceptor := grpc_prometheus.NewServerInterceptor()
// 添加到服务器选项
opts := []grpc.ServerOption{grpc.UnaryInterceptor(prometheusInterceptor)}
// 注册指标
grpc_prometheus.Register(s)
// 暴露指标
http.Handle("/metrics", promhttp.Handler())10. gRPC 最佳实践
10.1 服务设计最佳实践
- 使用有意义的服务和方法名称:清晰、一致的命名
- 合理设计消息结构:避免过大的消息
- 使用适当的服务类型:根据实际需求选择
- 实现错误处理:使用标准错误码
- 添加版本控制:在包名中包含版本
10.2 性能最佳实践
- 使用连接池:复用gRPC连接
- 设置合理的超时:避免长时间阻塞
- 使用流式RPC:适合大量数据传输
- 优化序列化:减少数据大小
- 监控性能指标:及时发现问题
10.3 安全最佳实践
- 使用SSL/TLS:加密传输
- 实现认证和授权:验证用户身份和权限
- 输入验证:验证所有用户输入
- 限流和速率控制:防止DoS攻击
- 定期更新依赖:修复安全漏洞
10.4 部署最佳实践
- 使用容器化:便于部署和扩展
- 实现健康检查:监控服务状态
- 使用服务网格:如Istio管理服务通信
- 实现优雅关闭:处理正在进行的请求
- 配置合理的资源限制:避免资源耗尽
11. gRPC 实战案例
11.1 案例一:微服务架构
需求:构建一个由多个微服务组成的系统
实现方案:
- 服务划分:用户服务、订单服务、产品服务
- 通信方式:使用gRPC进行服务间通信
- 服务发现:使用Kubernetes服务发现
- 负载均衡:使用Kubernetes负载均衡
优势:
- 高性能:gRPC的二进制传输比JSON更高效
- 强类型:减少服务间通信错误
- 自动代码生成:减少手动编码错误
11.2 案例二:实时数据处理
需求:构建一个实时数据处理系统
实现方案:
- 使用双向流式RPC:实时数据传输
- 实现背压机制:处理数据速率不匹配
- 使用Protobuf:高效序列化
优势:
- 低延迟:实时数据传输
- 高吞吐量:流式处理
- 可靠性:内置错误处理
11.3 案例三:跨语言系统
需求:构建一个由多种语言组成的系统
实现方案:
- 定义统一的.proto文件:语言无关的接口定义
- 生成各语言代码:自动代码生成
- 实现服务:不同语言实现不同服务
优势:
- 语言无关:支持多种编程语言
- 接口一致:统一的接口定义
- 易于集成:自动代码生成
📁 课程资料
参考文档
工具推荐
- Protocol Buffers编译器:protoc
- gRPC-Gateway:REST/gRPC转换
- gRPC-Web:浏览器中使用gRPC
- grpcurl:gRPC命令行工具
- BloomRPC:gRPC GUI客户端
代码示例
🎯 学习总结
gRPC是一种高性能、现代化的RPC框架,具有以下优势:
- 高性能:基于HTTP/2和Protocol Buffers
- 跨语言:支持多种编程语言
- 强类型:使用Protocol Buffers定义接口
- 流式通信:支持四种服务类型
- 内置安全:支持SSL/TLS和令牌认证
通过本课程的学习,你应该能够:
- 定义和编译Protocol Buffers文件
- 实现gRPC服务端和客户端
- 使用不同类型的gRPC服务
- 实现gRPC安全认证
- 优化gRPC性能
- 集成gRPC与其他技术
📝 课后作业
实践任务:
- 实现一个完整的gRPC服务
- 集成gRPC与HTTP/REST
- 实现流式RPC服务
思考问题:
- gRPC与RESTful API的各自优势是什么?
- 如何设计一个高性能的gRPC服务?
- 如何处理gRPC中的错误?
案例分析:
- 分析一个使用gRPC的实际项目
- 评估gRPC在该项目中的应用效果
🔗 相关课程
- [171-RESTful API设计规范](./171-RESTful API设计规范.md)
- 172-API认证和授权
- [173-GraphQL API开发](./173-GraphQL API开发.md)
- 175-WebSocket实时通信
📞 技术支持
如有任何问题或建议,欢迎通过以下方式联系:
- 📧 邮箱:your-email@example.com
- 💬 微信:your-wechat-id
- 🌐 网站:https://your-website.com
📜 版权声明
本课程内容基于 MIT 许可发布,欢迎学习和分享。
Copyright © 2026 叶哥的Linux技术分享