2024-08-08

'# 【Linux】Kill Process 后依然占用显卡空间并显示 No Such Process

一、背景与问题

在Linux系统中,使用kill命令终止进程后,常出现以下现象:

  1. 使用nvidia-smi查看显卡资源时,仍显示占用显存
  2. 使用ps查看进程时显示"No such process"
  3. 使用lsof查看文件句柄时仍显示开放文件
  4. 使用fuser查看文件锁时显示进程不存在

这种现象的本质是进程资源未正确释放,涉及操作系统进程管理机制、显卡驱动资源管理机制以及系统缓存机制的复杂交互。本文将深入分析其原理,探讨解决方案,并结合真实开发场景进行实践。

二、基本原理

1. 进程终止的生命周期

Linux系统中进程终止分为以下阶段:

  1. 信号接收:进程接收到SIGKILL或SIGTERM信号
  2. 信号处理:进程执行信号处理函数或默认处理逻辑
  3. 资源释放:进程释放文件描述符、内存、锁等资源
  4. 进程退出:进程状态变为Zombie(僵尸进程)
  5. 进程清理:父进程调用wait()回收僵尸进程

2. 显卡资源管理机制

NVIDIA显卡驱动通过nvidia-smi接口管理显存资源,其核心机制包括:

  • 显存分配:通过cudaMalloc等API分配显存
  • 显存释放:通过cudaFree显式释放显存
  • 进程绑定:通过nvidia-smi查询进程的显存使用情况
  • 缓存机制:驱动层维护进程资源的缓存信息,可能不会立即更新

3. 系统缓存机制

Linux内核维护以下缓存:

  • 进程表缓存:进程信息缓存(/proc文件系统)
  • 文件描述符缓存:lsof等工具的缓存信息
  • 显卡资源缓存:驱动层的资源管理缓存

这些缓存可能导致进程终止后,系统仍显示进程信息。

三、环境准备

# 安装nvidia驱动和工具
sudo apt-get install nvidia-driver nvidia-smi

# 安装CUDA工具包(可选)
sudo apt-get install cuda-toolkit

# 安装调试工具
sudo apt-get install ltrace strace

四、核心实现

1. 信号处理与资源释放

#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <cuda_runtime.h>

void signal_handler(int signum) {
    printf("Received signal %d\n", signum);
    cudaFree(NULL); // 显式释放显存
    exit(0);
}

int main() {
    // 注册信号处理函数
    signal(SIGTERM, signal_handler);
    signal(SIGINT, signal_handler);

    // 分配显存
    void* d_data;
    cudaMalloc(&d_data, 1024 * 1024); // 分配1MB显存

    // 保持进程运行
    while (1) {
        sleep(1);
    }

    return 0;
}

关键代码解释:

  • signal()函数注册信号处理函数
  • cudaFree()显式释放显存资源
  • sleep()保持进程运行

2. 显存占用监测

# 查看显存占用
nvidia-smi --query=utilization.gpu --format=csv

# 查看进程显存使用
nvidia-smi --query=process.memory.used --format=csv

3. 进程状态检查

# 查看进程状态
ps -ef | grep process_name

# 查看文件句柄
lsof | grep process_name

# 查看僵尸进程
ps aux | grep defunct

五、完整案例

案例:CUDA进程资源释放测试

#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <cuda_runtime.h>

// 显存释放函数
void release_gpu_resources() {
    printf("Releasing GPU resources...\n");
    cudaFree(NULL); // 强制释放显存
    cudaDeviceReset(); // 重置设备
}

// 信号处理函数
void signal_handler(int signum) {
    printf("Received signal %d\n", signum);
    release_gpu_resources();
    exit(0);
}

int main() {
    // 注册信号处理函数
    signal(SIGTERM, signal_handler);
    signal(SIGINT, signal_handler);

    // 分配显存
    void* d_data;
    cudaMalloc(&d_data, 1024 * 1024); // 分配1MB显存

    // 保持进程运行
    while (1) {
        sleep(1);
    }

    return 0;
}

运行流程:

  1. 编译并运行程序:gcc -o cuda_test cuda_test.c -lcuda
  2. 使用nvidia-smi查看显存占用
  3. 使用kill -9 PID终止进程
  4. 再次使用nvidia-smi查看显存释放情况

关键点:

  • 显式调用cudaFree()和cudaDeviceReset()确保资源释放
  • 使用SIGTERM信号处理,避免强制终止导致的资源泄漏

六、源码解析

1. CUDA资源管理机制

// CUDA驱动API源码片段(简化版)
void cudaFree(void** ptr) {
    // 检查指针有效性
    if (ptr && *ptr) {
        // 释放显存
        // 调用底层驱动接口
        // 更新显存管理器状态
    }
}

void cudaDeviceReset() {
    // 重置设备
    // 清除所有资源
    // 更新驱动状态
}

2. 进程终止流程

// Linux内核进程终止流程(简化版)
void do_exit(struct task_struct *tsk) {
    // 执行清理操作
    // 释放文件描述符
    // 释放内存
    // 更新进程状态
    // 通知父进程
}

七、进阶使用

1. 增强的资源管理

#include <sys/resource.h>

void check_resource_limit() {
    struct rlimit rlim;
    getrlimit(RLIMIT_AS, &rlim);
    printf("Memory limit: %ld KB\n", rlim.rlim_cur / 1024);
}

2. 系统调用监控

#include <sys/syscall.h>
#include <unistd.h>

void monitor_syscalls() {
    syscall(SYS_ptrace, PTRACE_TRACEME, 0, 0);
    // 启用调试模式
}

八、性能与工程实践

1. 性能优化

  • 使用cudaMallocManaged优化显存分配
  • 使用cudaMemPool管理显存池
  • 避免频繁的显存分配/释放
  • 使用cudaMemGetInfo监控显存使用

2. 安全风险

  • 显存泄漏可能导致显卡资源耗尽
  • 进程僵尸状态可能占用内存
  • 未授权的进程可能访问显存
  • 资源竞争可能导致系统不稳定

3. 安全防护

  • 使用sudo控制进程资源分配
  • 使用cgroups限制资源使用
  • 使用SELinux进行访问控制
  • 使用auditd监控资源使用

九、常见问题与踩坑

1. 常见错误

错误示例1:

// 忘记释放显存
void* d_data;
cudaMalloc(&d_data, 1024 * 1024);

错误分析:进程终止后显存未释放,导致资源泄漏

解决方法:添加cudaFree()调用

错误示例2:

// 未处理信号
void signal_handler(int signum) {
    // 无任何操作
}

错误分析:进程终止后资源未释放

解决方法:添加显存释放逻辑

2. 系统缓存问题

现象:nvidia-smi显示占用资源,但ps显示进程不存在

原因:驱动层缓存未更新

解决方法:

# 强制刷新nvidia-smi缓存
nvidia-smi --query=memory.used --format=csv --noheader

3. 环境配置问题

现象:nvidia-smi未显示任何信息

原因:未正确安装驱动或环境变量未设置

解决方法:

# 检查驱动版本
nvidia-smi --version

# 检查环境变量
echo $PATH

十、最佳实践

  1. 显存管理:始终显式释放显存,使用cudaFree()和cudaDeviceReset()
  2. 信号处理:注册信号处理函数,捕获SIGTERM和SIGINT
  3. 资源监控:定期检查显存使用情况,使用nvidia-smi
  4. 进程管理:使用wait()回收僵尸进程
  5. 安全防护:使用cgroups限制资源使用,避免资源耗尽
  6. 调试工具:使用ltrace和strace调试资源释放问题

十一、总结

本文深入探讨了Linux系统中kill进程后仍占用显卡资源并显示"No such process"的现象,分析了其背后涉及的进程管理机制、显卡驱动资源管理机制以及系统缓存机制。通过三个代码示例和一个完整案例,展示了如何正确管理显存资源,避免资源泄漏。

在实际开发中,应特别注意显存的显式释放,尤其是在使用CUDA等高性能计算库时。对于需要精确控制资源释放的场景,建议使用信号处理函数进行资源清理。但需注意避免在不可控的外部进程中使用此方案,以免造成资源竞争或系统不稳定。

通过合理使用nvidia-smi、lsof等工具进行监控,结合cgroups等安全机制,可以有效管理显卡资源,确保系统稳定运行。同时,注意处理系统缓存带来的潜在问题,确保资源状态的实时性。

2024-08-08

'# Django操作cookie、Django操作session、Django中的Session配置、CBV添加装饰器、中间件、csrf跨站请求

一、背景与问题

在Web开发中,状态管理是核心问题之一。Django提供了完整的解决方案,包括基于Cookie的会话管理、基于Session的用户状态维护、中间件的全局处理机制,以及CSRF防护体系。本文将深入解析这些机制的工作原理、实现细节和实际应用场景。

二、基本原理

1. Cookie与Session的协同工作

Cookie是服务器发送给客户端的键值对存储,而Session是服务器端的存储结构。Django通过session框架将二者结合:

  • 客户端发送请求时携带Cookie
  • 服务器根据Cookie中的sessionid查找Session存储
  • 服务器将业务数据存储到Session中
  • 服务器生成新的sessionid并更新Cookie

这个过程涉及到以下几个关键点:

  • Session的存储介质(内存/数据库/缓存)
  • Cookie的过期策略(SESSION_COOKIE_AGE)
  • Session的加密机制(secure、httponly标志)

2. 中间件的处理流程

Django中间件分为请求处理和响应处理两个阶段:

def process_request(self, request):
    # 请求处理阶段

def process_response(self, request, response):
    # 响应处理阶段

中间件链执行顺序:

  1. 请求处理阶段按定义顺序依次执行
  2. 响应处理阶段按反向顺序执行

3. CSRF防护机制

CSRF攻击的核心是利用用户身份进行恶意操作。Django通过以下机制防护:

  • 每个表单生成一个csrf token(csrf_token模板标签)
  • 表单提交时验证token有效性
  • 使用@csrf_exempt或@csrf_protect控制验证行为
  • 支持AJAX请求的X-CSRFToken头验证

三、环境准备

# 创建虚拟环境
python -m venv django_env
source django_env/bin/activate

# 安装Django
pip install django==4.2

四、核心实现

1. Cookie操作

# views.py
from django.http import HttpResponse

def set_cookie(request):
    response = HttpResponse("Cookie设置成功")
    response.set_cookie(
        key='user_id',
        value='12345',
        max_age=3600,  # 1小时后过期
        secure=True,   # 只通过HTTPS传输
        httponly=True  # 防止JavaScript访问
    )
    return response

def get_cookie(request):
    user_id = request.COOKIES.get('user_id')
    return HttpResponse(f"获取到的用户ID: {user_id}")

关键点解释:

  • set_cookie方法的参数设置影响安全性
  • max_age控制Cookie的生命周期
  • secure和httponly标志是防御XSS攻击的关键

2. Session操作

# views.py
from django.http import HttpResponse
from django.shortcuts import redirect

def login(request):
    if request.method == 'POST':
        # 假设验证通过
        request.session['user_id'] = '12345'
        return redirect('home')
    return HttpResponse("登录页面")

def home(request):
    if 'user_id' in request.session:
        return HttpResponse("欢迎回来!")
    else:
        return redirect('login')

关键点解释:

  • Session数据存储在Django的django_session表中
  • 默认使用数据库存储,可通过SESSION_ENGINE配置
  • request.session是Session的接口

3. Session配置

# settings.py
SESSION_COOKIE_NAME = 'my_custom_cookie'
SESSION_COOKIE_DOMAIN = '.example.com'
SESSION_COOKIE_SECURE = True
SESSION_COOKIE_HTTPONLY = True
SESSION_EXPIRE_AT_BROWSER_CLOSE = True
SESSION_SAVE_EVERY_REQUEST = True

配置说明:

  • SESSION_COOKIE_DOMAIN影响Cookie的域名匹配
  • SESSION_COOKIE_SECURE强制HTTPS传输
  • SESSION_EXPIRE_AT_BROWSER_CLOSE设置关闭浏览器时失效
  • SESSION_SAVE_EVERY_REQUEST影响性能与数据一致性

五、完整案例

1. 完整项目结构

myproject/
├── myapp/
│   ├── migrations/
│   ├── models.py
│   ├── views.py
│   └── urls.py
├── myproject/
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
└── manage.py

2. 完整案例代码

# myapp/views.py
from django.http import HttpResponse, HttpResponseRedirect
from django.shortcuts import render
from django.views import View
from django.views.decorators.csrf import csrf_exempt
from django.middleware.csrf import get_token
from django.contrib.auth import authenticate, login

class LoginView(View):
    def get(self, request):
        return render(request, 'login.html')

    def post(self, request):
        username = request.POST['username']
        password = request.POST['password']
        user = authenticate(username=username, password=password)
        if user is not None:
            login(request, user)
            return HttpResponseRedirect('/dashboard')
        return HttpResponse("登录失败")

class DashboardView(View):
    def get(self, request):
        if not request.user.is_authenticated:
            return HttpResponseRedirect('/login')
        return render(request, 'dashboard.html', {'user': request.user})
# myapp/urls.py
from django.urls import path
from .views import LoginView, DashboardView

urlpatterns = [
    path('login/', LoginView.as_view(), name='login'),
    path('dashboard/', DashboardView.as_view(), name='dashboard'),
]
# settings.py
# 配置CSRF
CSRF_COOKIE_NAME = 'my_csrf_token'
CSRF_COOKIE_DOMAIN = '.example.com'
CSRF_COOKIE_SECURE = True
CSRF_COOKIE_HTTPONLY = True
CSRF_TRUSTED_ORIGINS = ['https://example.com']

六、源码解析

1. Session中间件源码

# django/middleware/session.py
class SessionMiddleware:
    def process_request(self, request):
        engine = get_session_engine()
        request.session = engine.SessionStore(request)
        request.session.modified = False

    def process_response(self, request, response):
        if request.session.modified:
            request.session.save()
        return response

关键点:

  • get_session_engine()根据SESSION_ENGINE配置加载不同的存储引擎
  • SessionStore类实现了不同的存储方式(数据库/缓存等)
  • modified标志用于控制是否需要保存Session

2. CSRF中间件源码

# django/middleware/csrf.py
class CsrfViewMiddleware:
    def process_request(self, request):
        if request.method in ('POST', 'PUT', 'DELETE'):
            if not request.META.get('HTTP_X_CSRFTOKEN'):
                token = get_token(request)
                request.csrf_token = token
                return None
            else:
                token = request.META.get('HTTP_X_CSRFTOKEN')
                if token != get_token(request):
                    return HttpResponseForbidden("CSRF verification failed")

关键点:

  • 通过X_CSRFTOKEN头验证AJAX请求
  • 使用get_token函数生成token
  • 支持@csrf_exempt和@csrf_protect装饰器控制行为

七、进阶使用

1. 自定义Session存储

# settings.py
SESSION_ENGINE = 'myapp.custom_session.RedisSessionEngine'
# myapp/custom_session.py
from django.contrib.sessions.backends.db import SessionStore as DBStore
from django.core.cache import caches

class RedisSessionEngine:
    def __init__(self):
        self.cache = caches['default']

    def get_session_store(self, session_key):
        return RedisSessionStore(session_key, self.cache)

2. 中间件链自定义

# myapp/middleware.py
class MyMiddleware:
    def process_request(self, request):
        print("MyMiddleware: process_request")
        request.my_data = "custom data"
    
    def process_response(self, request, response):
        print("MyMiddleware: process_response")
        return response
# settings.py
MIDDLEWARE = [
    'myapp.middleware.MyMiddleware',
    'django.middleware.security.SecurityMiddleware',
    # 其他中间件...
]

八、性能与工程实践

1. 性能优化策略

优化策略说明示例
使用缓存将Session存储到RedisSESSION_ENGINE = 'django.contrib.sessions.backends.cache'
减少Cookie大小压缩sessionid设置SESSION_COOKIE_DOMAIN
增加Session超时减少无效存储SESSION_COOKIE_AGE = 3600

2. 安全实践

安全措施实现方式说明
防止CSRF使用@csrf_exempt暂时禁用防护
防止XSS设置httponly防止JavaScript访问
加密传输设置secure强制HTTPS传输

九、常见问题与踩坑

1. 常见错误案例

# 错误示例:AJAX请求未携带CSRF token
$.ajax({
    url: '/api/data',
    method: 'POST',
    data: { key: 'value' }
});

问题分析:

  • 缺少X-CSRFToken头
  • 未使用csrf_token模板标签生成token
  • 未在请求中携带Cookie

解决方案:

# 在模板中添加
{% csrf_token %}
// 在AJAX请求中添加
$.ajax({
    url: '/api/data',
    method: 'POST',
    data: { key: 'value' },
    headers: {
        'X-CSRFToken': $('input[name=csrfmiddlewaretoken]').val()
    }
});

2. 中间件执行顺序问题

错误案例:

# 中间件顺序错误
MIDDLEWARE = [
    'myapp.middleware.MyMiddleware',
    'django.middleware.security.SecurityMiddleware',
]

问题分析:

  • SecurityMiddleware需要在CommonMiddleware之后
  • 某些中间件需要特定顺序才能正常工作

解决方案:

# 正确顺序
MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'myapp.middleware.MyMiddleware',
]

十、最佳实践

1. 推荐方案

场景推荐方案说明
用户认证使用Django内置的@login_required简单可靠
高并发场景使用RedisSessionEngine提高性能
跨域请求配置CSRF_TRUSTED_ORIGINS安全可靠
复杂业务使用自定义中间件灵活扩展

2. 使用建议

  • 对敏感操作必须使用CSRF保护
  • Session存储选择要考虑性能和可靠性
  • 中间件要按功能分类组织
  • 对关键数据要进行加密处理
  • 跨域请求要配置CSRF_TRUSTED_ORIGINS

十一、总结

Django的会话管理机制是Web开发中不可或缺的核心组件。通过深入理解Cookie和Session的协同工作、中间件的处理流程、CSRF防护机制,我们可以构建更安全、更高效的Web应用。在实际开发中需要注意:

  • 理解不同存储介质的性能差异
  • 合理配置中间件顺序
  • 正确处理跨域请求
  • 定期审查安全配置

特别是在涉及用户认证和敏感数据时,必须严格遵守安全规范。通过本文的深入分析和实际案例,相信读者能够更好地理解和应用Django的会话管理机制,构建更可靠的Web应用。

2024-08-08

'# Express中使用Redis中间件,报错TypeError: Router.use() requires a middleware function but got a undefined方法解决

一、背景与问题

在Express项目中引入Redis中间件时,开发者常遇到TypeError: Router.use() requires a middleware function but got a undefined的错误。这个错误表明调用Router.use()时传入的参数不是有效的中间件函数,而是undefined。该问题在以下场景中尤为常见:

  1. Redis连接未正确初始化导致中间件未定义
  2. 异步函数未正确返回中间件函数
  3. 中间件函数未正确导出或暴露
  4. Redis客户端库版本兼容性问题

本篇文章将深入分析该错误的底层原理,结合完整代码示例和真实开发场景,探讨Express与Redis中间件的整合方案。


二、基本原理

1. Express中间件机制

Express的中间件本质是函数,其执行流程遵循以下规则:

  • 中间件函数必须接收(req, res, next)三个参数
  • 当调用next()时,控制权传递给下一个中间件
  • 若未调用next()且未处理请求,则请求被终止
app.use((req, res, next) => {
  console.log('Middleware executed');
  next();
});

2. Redis中间件的特殊性

Redis中间件需要在请求处理前/处理后与Redis进行交互,其核心特征包括:

  • 建立与Redis的连接(redis.createClient())
  • 使用Promise或async/await处理异步操作
  • 管理缓存命中/未命中逻辑
  • 处理连接异常和超时

三、环境准备

npm init -y
npm install express redis

创建基础项目结构:

express-redis-demo/
├── app.js
├── config/
│   └── redis.js
├── middleware/
│   └── redis.js
└── package.json

四、核心实现

1. 正确的中间件定义(推荐方式)

// middleware/redis.js
const Redis = require('redis');
const { createClient } = Redis;

// 1. 创建连接池(推荐方式)
const redisClient = createClient({
  host: '127.0.0.1',
  port: 6379,
  password: process.env.REDIS_PASSWORD,
  db: 0
});

// 2. 定义中间件函数
const redisMiddleware = (req, res, next) => {
  // 3. 异步处理逻辑
  redisClient.get(req.originalUrl, (err, data) => {
    if (err) {
      return next(err);
    }
    if (data) {
      // 缓存命中
      res.locals.cache = data;
      return next();
    }
    // 缓存未命中
    next();
  });
};

module.exports = redisMiddleware;

关键点说明:

  • 使用createClient()创建连接池而非单例
  • 中间件函数必须接收req, res, next参数
  • 异步操作后必须调用next()传递控制权

2. 错误示例:未定义中间件

// 错误写法(会导致undefined)
const redisMiddleware = () => {
  // 未定义中间件函数
};

app.use(redisMiddleware); // 此时传入的是undefined

错误原因: 中间件函数未正确定义,导致Router.use()接收到undefined。

3. 异步中间件的正确写法

// 使用async/await处理异步逻辑
const redisMiddleware = async (req, res, next) => {
  try {
    const data = await redisClient.get(req.originalUrl);
    if (data) {
      res.locals.cache = data;
      return next();
    }
    next();
  } catch (err) {
    next(err);
  }
};

注意事项:

  • 必须使用async/await或.then()处理异步逻辑
  • 必须确保函数返回值为中间件函数(即必须有next()调用)

五、完整案例:缓存中间件实现

1. 项目结构

express-redis-demo/
├── app.js
├── config/
│   └── redis.js
├── middleware/
│   └── redis.js
└── package.json

2. 完整代码示例

// app.js
const express = require('express');
const redisMiddleware = require('./middleware/redis');

const app = express();

// 设置路由
app.get('/users', (req, res) => {
  res.json({ message: 'This is a cached response' });
});

// 使用缓存中间件
app.use(redisMiddleware);

// 启动服务
app.listen(3000, () => {
  console.log('Server is running on port 3000');
});

3. Redis配置文件

// config/redis.js
module.exports = {
  host: '127.0.0.1',
  port: 6379,
  password: process.env.REDIS_PASSWORD,
  db: 0
};

4. 中间件实现

// middleware/redis.js
const Redis = require('redis');
const { createClient } = Redis;
const { host, port, password, db } = require('../config/redis');

// 创建连接池
const redisClient = createClient({
  host,
  port,
  password,
  db
});

// 中间件函数
const redisMiddleware = async (req, res, next) => {
  try {
    const data = await redisClient.get(req.originalUrl);
    if (data) {
      res.locals.cache = data;
      return next();
    }
    next();
  } catch (err) {
    next(err);
  }
};

module.exports = redisMiddleware;

运行流程说明:

  1. 客户端请求/users路径
  2. 中间件先尝试从Redis获取缓存
  3. 若存在缓存则直接返回,否则继续处理
  4. 确保所有异常都被正确传递和处理

六、源码解析

1. Redis客户端初始化

const redisClient = createClient({
  host: '127.0.0.1',
  port: 6379,
  password: process.env.REDIS_PASSWORD,
  db: 0
});

关键点:

  • 使用连接池模式(默认行为)
  • 密码认证需要配置password字段
  • db参数指定使用哪个数据库(0-15)

2. 中间件函数执行流程

const redisMiddleware = async (req, res, next) => {
  try {
    const data = await redisClient.get(req.originalUrl);
    if (data) {
      res.locals.cache = data;
      return next();
    }
    next();
  } catch (err) {
    next(err);
  }
};

执行流程:

  1. 调用get()方法获取缓存
  2. 若存在数据则设置res.locals.cache并调用next()
  3. 若无数据则直接调用next()继续后续处理
  4. 异常情况通过next(err)传递错误

七、进阶使用

1. 带过期时间的缓存

const redisMiddleware = async (req, res, next) => {
  try {
    const data = await redisClient.get(req.originalUrl);
    if (data) {
      res.locals.cache = data;
      return next();
    }
    // 未命中时设置缓存并继续处理
    next();
  } catch (err) {
    next(err);
  }
};

建议:

  • 在未命中时设置TTL(Time To Live)
  • 使用setex()方法设置带过期时间的缓存

2. 缓存更新策略

app.get('/users', (req, res) => {
  const data = { users: ['Alice', 'Bob'] };
  res.locals.cache = JSON.stringify(data);
  
  // 设置缓存(带过期时间)
  redisClient.setex(req.originalUrl, 3600, JSON.stringify(data));
  
  res.json(data);
});

注意事项:

  • 缓存更新应与业务逻辑解耦
  • 建议使用setex()代替set()设置缓存

八、性能与工程实践

1. 性能优化策略

优化项解决方案
连接池使用createClient()创建连接池
异步处理使用async/await避免阻塞
缓存命中率优化缓存键的设计
错误处理增加重试机制和日志记录

2. 安全风险分析

  • 未授权访问: Redis默认开放端口,需配置密码和防火墙
  • 缓存注入: 需要对请求参数进行过滤
  • 数据泄露: 建议使用redis-cli --raw进行安全访问

推荐做法:

  • 使用redis-cli配置密码保护
  • 使用redis-sentinel或redis-cluster集群部署
  • 对敏感数据进行加密处理

3. 错误处理机制

redisClient.on('error', (err) => {
  console.error('Redis connection error:', err);
  // 可以在此触发全局错误处理
});

建议:

  • 为每个Redis连接添加错误监听
  • 在中间件中处理所有可能的异常

九、常见问题与踩坑

1. 常见错误场景

场景错误表现解决方案
未初始化Redis连接undefined确保连接池正确创建
异步函数未返回TypeError使用async/await或.then()
中间件未导出undefined确保module.exports正确
密码错误连接失败检查配置文件中的密码

2. 版本兼容性问题

Node.js 18+ 需要使用ioredis库:

npm install ioredis

替代实现:

const Redis = require('ioredis');
const redisClient = new Redis({
  host: '127.0.0.1',
  port: 6379,
  password: process.env.REDIS_PASSWORD,
});

注意:

  • ioredis支持更多高级功能(如集群、哨兵)
  • 原生redis库在Node.js 18+可能存在兼容性问题

十、最佳实践

1. 推荐方案

场景推荐方案
缓存热点数据使用setex()设置带过期时间的缓存
处理异常使用try/catch和next()传递错误
错误重试使用retry库进行重试机制
性能监控集成Prometheus进行监控

2. 使用建议

  • 应该使用:

    • 需要缓存频繁请求的数据
    • 需要降低数据库压力
    • 需要支持分布式缓存
  • 不应该使用:

    • 需要实时更新的数据
    • 需要高安全性的敏感数据
    • 需要处理大量并发写操作

十一、总结

通过本文的深入分析,我们可以看到在Express中使用Redis中间件时,TypeError: Router.use() requires a middleware function but got a undefined错误的根源在于中间件函数未正确定义或异步处理不当。解决该问题需要:

  1. 正确初始化Redis连接池
  2. 确保中间件函数接收三个参数
  3. 正确处理异步逻辑并调用next()
  4. 处理可能的异常和错误

在实际开发中,建议使用ioredis库以获得更好的兼容性,同时注意安全配置和性能优化。通过合理使用缓存策略,可以显著提升应用性能,但需注意其适用场景和潜在风险。掌握这些核心原理,开发者可以更安全、高效地在Express项目中集成Redis中间件。

2024-08-08

'# Nestjs中间件常见使用方式(class、函数中间件)

一、背景与问题

在构建复杂的Node.js应用时,中间件是实现请求处理流程的核心组件。Nestjs作为基于TypeScript的渐进式Node.js框架,提供了两种中间件实现方式:函数式中间件和基于类的中间件。这两种实现方式在底层原理上存在本质差异,但在实际开发中各有适用场景。

当前开发中常见的中间件使用问题包括:

  1. 中间件执行顺序理解错误导致逻辑混乱
  2. 未正确处理异常导致程序崩溃
  3. 未考虑性能影响造成请求延迟
  4. 安全性配置不当暴露敏感信息
  5. 依赖注入失效导致代码耦合

二、基本原理

1. 函数式中间件原理

函数式中间件是通过use方法注册的普通函数,其执行流程如下:

function logger(req: Request, res: Response, next: Function) {
  console.log(`Request: ${req.method} ${req.url}`);
  next();
}

底层实现通过fastify或express的中间件机制,将请求处理流程组织为链式调用。每个中间件函数接收三个参数:请求对象、响应对象和next函数。

2. 类中间件原理

类中间件通过@Injectable()装饰器注册,其执行流程如下:

@Injectable()
export class LoggerMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: Function) {
    console.log(`Request: ${req.method} ${req.url}`);
    next();
  }
}

底层通过@nestjs/common模块的中间件系统,将类方法注册为中间件实例。类中间件支持依赖注入和装饰器,可以更灵活地组织业务逻辑。

3. 中间件执行顺序

Nestjs中间件的执行顺序遵循以下规则:

  • 与路由绑定的中间件按声明顺序执行
  • 全局中间件在路由中间件之前执行
  • @UseFilters装饰器注册的异常处理中间件在最后执行

三、环境准备

创建Nestjs项目:

npm i -g @nestjs/cli
nest new nest-middleware-demo
cd nest-middleware-demo
npm install

项目结构:

src/
├── main.ts
├── app.controller.ts
├── app.module.ts
├── middleware/
│   ├── logger.middleware.ts
│   └── auth.middleware.ts
└── common/
    └── filters/
        └── http-exception.filter.ts

四、核心实现

1. 函数式中间件实现

// src/middleware/logger.middleware.ts
export function loggerMiddleware(req: Request, res: Response, next: Function) {
  console.log(`Request: ${req.method} ${req.url}`);
  next();
}

在路由中使用:

// src/app.controller.ts
import { Controller, Get, UseMiddleware } from '@nestjs/common';
import { loggerMiddleware } from '../middleware/logger.middleware';

@Controller()
export class AppController {
  @Get()
  @UseMiddleware(loggerMiddleware)
  getHello(): string {
    return 'Hello World';
  }
}

关键点说明:

  • 中间件函数必须接受三个参数
  • next()函数调用控制流程继续
  • 中间件可以修改请求/响应对象

2. 类中间件实现

// src/middleware/logger.middleware.ts
import { Injectable } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';

@Injectable()
export class LoggerMiddleware {
  use(req: Request, res: Response, next: NextFunction) {
    console.log(`Request: ${req.method} ${req.url}`);
    next();
  }
}

在路由中使用:

// src/app.controller.ts
import { Controller, Get, UseMiddleware } from '@nestjs/common';
import { LoggerMiddleware } from '../middleware/logger.middleware';

@Controller()
export class AppController {
  @Get()
  @UseMiddleware(LoggerMiddleware)
  getHello(): string {
    return 'Hello World';
  }
}

关键点说明:

  • 使用@Injectable()进行依赖注入
  • 支持装饰器和类型校验
  • 更适合复杂业务逻辑处理

3. 异常处理中间件

// src/common/filters/http-exception.filter.ts
import { ExceptionFilter, Catch, HttpException } from '@nestjs/common';
import { Request, Response } from 'express';

@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
  catch(exception: HttpException, host: any) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    const request = ctx.getRequest<Request>();
    const status = exception.getStatus();
    
    response.status(status).json({
      message: exception.message,
      statusCode: status,
      timestamp: new Date().toISOString(),
      path: request.url,
    });
  }
}

在模块中注册:

// src/app.module.ts
import { Module } from '@nestjs/common';
import { HttpExceptionFilter } from './common/filters/http-exception.filter';

@Module({
  imports: [],
  providers: [HttpExceptionFilter],
  controllers: [],
})
export class AppModule {}

关键点说明:

  • 通过@Catch装饰器捕获异常
  • 支持自定义异常处理逻辑
  • 适合统一错误处理

五、完整案例

构建一个用户认证系统:

// src/middleware/auth.middleware.ts
import { Injectable } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';

@Injectable()
export class AuthMiddleware {
  use(req: Request, res: Response, next: NextFunction) {
    const token = req.headers['authorization'];
    
    if (!token || token !== 'secret-token') {
      throw new HttpException('Unauthorized', 401);
    }
    
    next();
  }
}
// src/app.controller.ts
import { Controller, Get, UseMiddleware } from '@nestjs/common';
import { AuthMiddleware } from './middleware/auth.middleware';

@Controller()
export class AppController {
  @Get()
  @UseMiddleware(AuthMiddleware)
  getHello(): string {
    return 'Hello World';
  }
}
// src/common/filters/http-exception.filter.ts
import { ExceptionFilter, Catch, HttpException } from '@nestjs/common';
import { Request, Response } from 'express';

@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
  catch(exception: HttpException, host: any) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    const request = ctx.getRequest<Request>();
    const status = exception.getStatus();
    
    response.status(status).json({
      message: exception.message,
      statusCode: status,
      timestamp: new Date().toISOString(),
      path: request.url,
    });
  }
}
// src/app.module.ts
import { Module } from '@nestjs/common';
import { HttpExceptionFilter } from './common/filters/http-exception.filter';

@Module({
  imports: [],
  providers: [HttpExceptionFilter],
  controllers: [],
})
export class AppModule {}

运行测试:

npm run start

访问 http://localhost:3000 时会返回 401 Unauthorized 错误,而使用正确 token 时返回正常响应。

六、源码解析

以类中间件的执行流程为例,源码中关键部分如下:

// @nestjs/common/src/middleware/middleware.ts
export class NestMiddleware {
  // 中间件注册逻辑
  static registerMiddleware(
    app: FastifyInstance,
    middleware: NestMiddleware,
  ): void {
    const middlewares = app.middlewares;
    middlewares.push(middleware);
  }
  
  // 中间件执行逻辑
  static applyMiddlewares(
    req: Request,
    res: Response,
    next: Function,
    middlewares: NestMiddleware[],
  ): void {
    const executeMiddleware = (index: number) => {
      if (index >= middlewares.length) {
        return next();
      }
      const middleware = middlewares[index];
      if (middleware instanceof Function) {
        middleware(req, res, () => executeMiddleware(index + 1));
      } else {
        middleware.use(req, res, () => executeMiddleware(index + 1));
      }
    };
    executeMiddleware(0);
  }
}

关键点分析:

  • 中间件注册采用链式调用方式
  • 支持函数式和类中间件的统一处理
  • 通过递归方式执行中间件链

七、进阶使用

1. 中间件链式调用

@UseMiddlewares(LoggerMiddleware, AuthMiddleware)
getHello(): string {
  return 'Hello World';
}

2. 中间件装饰器组合

@UsePipes(new ValidationPipe())
@UseInterceptors(new LoggingInterceptor())

3. 中间件参数注入

@Injectable()
export class ConfigMiddleware {
  constructor(private readonly configService: ConfigService) {}
  
  use(req: Request, res: Response, next: Function) {
    console.log(this.configService.get('APP_NAME'));
    next();
  }
}

八、性能与工程实践

1. 性能优化策略

  • 中间件顺序优化:将耗时中间件放在最后
  • 避免不必要的中间件调用
  • 使用缓存中间件处理重复请求
  • 对关键中间件进行性能测试

2. 异常处理最佳实践

  • 为每个中间件单独处理异常
  • 使用@Catch装饰器统一处理
  • 避免在中间件中直接throw异常

3. 安全性考虑

  • 中间件不应暴露敏感信息
  • 对敏感中间件进行加密处理
  • 使用@UseFilters装饰器统一处理异常

4. 代码组织建议

  • 将中间件按功能分类组织
  • 使用@Injectable()进行依赖注入
  • 对复杂中间件进行单元测试

九、常见问题与踩坑

1. 中间件顺序错误

错误示例:

@UseMiddleware(AuthMiddleware, LoggerMiddleware)

问题:认证中间件应该在日志中间件之前执行

正确顺序:

@UseMiddleware(LoggerMiddleware, AuthMiddleware)

2. 未正确处理异常

错误示例:

throw new HttpException('...', 401);

问题:未使用@Catch装饰器处理异常

正确方式:

@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
  // ...
}

3. 中间件未注册

错误示例:

@UseMiddleware(LoggerMiddleware)

问题:未在模块中注册中间件

正确方式:

import { LoggerMiddleware } from './middleware/logger.middleware';

@Module({
  providers: [LoggerMiddleware],
  // ...
})

4. 未正确处理请求参数

错误示例:

req.headers['authorization'] // 未处理undefined情况

改进方式:

const token = req.headers['authorization'] || '';

十、最佳实践

  1. 类中间件推荐场景:

    • 需要依赖注入时
    • 逻辑复杂需要拆分时
    • 需要装饰器支持时
    • 需要类型校验时
  2. 函数中间件推荐场景:

    • 简单逻辑处理时
    • 不需要依赖注入时
    • 需要快速实现时
  3. 中间件使用规范:

    • 所有中间件必须使用@Injectable()装饰器
    • 异常处理必须使用@Catch装饰器
    • 中间件必须使用use方法
    • 中间件顺序必须符合业务逻辑
  4. 性能优化建议:

    • 对关键中间件进行缓存
    • 避免不必要的中间件调用
    • 使用性能分析工具进行优化
    • 对中间件进行单元测试

十一、总结

Nestjs中间件作为请求处理的核心组件,其合理使用对系统性能和可维护性至关重要。通过对比函数式中间件和类中间件的实现方式,我们可以发现:

  • 类中间件更适合复杂业务逻辑
  • 函数中间件适合简单处理逻辑
  • 中间件顺序直接影响执行流程
  • 异常处理是必须考虑的部分
  • 安全性需要特别注意

在实际开发中,建议遵循以下原则:

  1. 根据业务需求选择合适的中间件类型
  2. 保持中间件逻辑的单一职责
  3. 合理组织中间件的执行顺序
  4. 对关键中间件进行性能测试
  5. 使用统一的异常处理机制

通过合理使用中间件,可以显著提升系统的可维护性、可扩展性和安全性。在大型项目中,建议建立中间件管理规范,确保团队成员的代码质量和开发效率。

2024-08-08

'# express学习笔记5 - 自定义路由异常处理中间件

一、背景与问题

在Express应用中,异常处理是确保系统健壮性的关键环节。当我们开发复杂的业务逻辑时,往往会遇到各种潜在的错误场景:数据库连接失败、未授权的访问、无效的请求参数、第三方服务调用异常等。传统的错误处理方式存在两个痛点:

  1. 错误处理分散:每个路由处理函数需要单独处理异常,导致代码重复和维护困难
  2. 错误信息暴露:未处理的错误会暴露敏感的堆栈信息,存在安全风险

本文将深入探讨如何通过自定义路由异常处理中间件,实现统一的错误处理机制,并分析其工作原理、实现方式和最佳实践。

二、基本原理

Express的中间件机制基于函数链式调用,每个中间件函数接收req、res和next参数。当发生未处理的错误时,Express会将错误作为第一个参数传递给错误处理中间件。

错误中间件的特殊性

错误处理中间件必须符合特定的函数签名:

function (err, req, res, next) {
  // 处理错误逻辑
}

这种特殊签名允许Express识别并捕获未处理的错误。当错误处理中间件被调用时,Express会自动停止后续中间件的执行。

错误传播机制

Express的错误传播遵循以下规则:

  1. 同步错误:在路由处理函数中直接抛出的错误
  2. 异步错误:在async/await中未捕获的Promise拒绝
  3. 异常处理中间件:通过next()传递错误给下一个中间件

三、环境准备

npm init -y
npm install express

创建基本项目结构:

express-error-handling/
├── app.js
├── routes/
│   └── index.js
└── utils/
    └── error.js

四、核心实现

1. 基础错误处理中间件

// utils/error.js
function createErrorMiddleware() {
  return (err, req, res, next) => {
    console.error('Error occurred:', err.message);
    
    // 防止暴露敏感信息
    const errorMessage = 'Internal Server Error';
    
    // 设置响应头
    res.status(500).json({
      error: errorMessage
    });
    
    // 继续执行后续中间件
    next();
  };
}

module.exports = createErrorMiddleware;

关键点解释:

  • 错误日志记录:通过console.error记录错误信息
  • 响应格式统一:返回标准的JSON响应格式
  • 错误隔离:通过next()确保错误处理不影响后续处理流程

2. 带参数的错误处理中间件

// utils/error.js
function createErrorMiddleware() {
  return (err, req, res, next) => {
    console.error('Error occurred:', {
      message: err.message,
      stack: err.stack
    });
    
    const statusCode = err.status || 500;
    const errorMessage = err.message || 'Internal Server Error';
    
    res.status(statusCode).json({
      error: errorMessage
    });
    
    next();
  };
}

3. 异步错误处理中间件

// utils/error.js
function createErrorMiddleware() {
  return (err, req, res, next) => {
    console.error('Error occurred:', {
      message: err.message,
      stack: err.stack
    });
    
    const statusCode = err.status || 500;
    const errorMessage = err.message || 'Internal Server Error';
    
    // 异步处理错误日志
    setTimeout(() => {
      res.status(statusCode).json({
        error: errorMessage
      });
    }, 100);
    
    next();
  };
}

五、完整案例

1. 创建路由文件

// routes/index.js
const express = require('express');
const router = express.Router();

// 模拟业务逻辑
function simulateBusinessLogic() {
  return new Promise((resolve, reject) => {
    // 模拟数据库操作
    setTimeout(() => {
      const error = new Error('Database connection failed');
      error.status = 503;
      reject(error);
    }, 100);
  });
}

router.get('/users', async (req, res, next) => {
  try {
    await simulateBusinessLogic();
    res.json({ data: 'User data' });
  } catch (err) {
    // 将错误传递给错误处理中间件
    next(err);
  }
});

module.exports = router;

2. 创建主应用文件

// app.js
const express = require('express');
const createErrorMiddleware = require('./utils/error');
const routes = require('./routes/index');

const app = express();

// 错误处理中间件
app.use((err, req, res, next) => {
  console.error('Error occurred:', {
    message: err.message,
    stack: err.stack
  });
  
  const statusCode = err.status || 500;
  const errorMessage = err.message || 'Internal Server Error';
  
  res.status(statusCode).json({
    error: errorMessage
  });
});

// 路由中间件
app.use('/', routes);

const PORT = 3000;
app.listen(PORT, () => {
  console.log(`Server is running on port ${PORT}`);
});

3. 测试案例

启动服务后,访问http://localhost:3000/users会触发模拟的数据库错误,返回:

{
  "error": "Database connection failed"
}

同时在控制台会记录完整的错误信息。

六、源码解析

1. 错误中间件执行流程

当请求到达/users路由时:

  1. 调用simulateBusinessLogic模拟数据库操作
  2. 触发Promise拒绝,进入catch块
  3. 调用next(err)将错误传递给错误处理中间件
  4. 错误处理中间件记录错误信息,构建响应
  5. 发送响应给客户端

2. 错误处理逻辑

// 错误处理中间件核心逻辑
function (err, req, res, next) {
  console.error('Error occurred:', {
    message: err.message,
    stack: err.stack
  });
  
  const statusCode = err.status || 500;
  const errorMessage = err.message || 'Internal Server Error';
  
  res.status(statusCode).json({
    error: errorMessage
  });
  
  next();
}

关键点分析:

  • 错误日志记录:记录错误信息和堆栈跟踪
  • 响应格式统一:返回标准的JSON格式
  • 错误隔离:通过next()确保错误处理不影响后续处理流程

七、进阶使用

1. 多级错误处理

// 错误处理中间件链
app.use((err, req, res, next) => {
  // 第一级处理
  console.log('First error handler');
  next(err);
});

app.use((err, req, res, next) => {
  // 第二级处理
  console.log('Second error handler');
  next(err);
});

2. 按错误类型处理

app.use((err, req, res, next) => {
  if (err instanceof CustomError) {
    // 处理特定错误类型
    res.status(400).json({ error: 'Custom error' });
  } else {
    next(err);
  }
});

3. 日志记录集成

const winston = require('winston');

const logger = winston.createLogger({
  level: 'error',
  transports: [
    new winston.transports.Console(),
    new winston.transports.File({ filename: 'error.log' })
  ]
});

app.use((err, req, res, next) => {
  logger.error('Error occurred:', {
    message: err.message,
    stack: err.stack
  });
  
  next();
});

八、性能与工程实践

1. 性能优化

  1. 异步日志记录:使用setTimeout或队列处理日志记录,避免阻塞主线程
  2. 错误处理分离:将错误处理逻辑抽离到独立模块,避免污染主流程
  3. 响应缓存:对常见错误返回缓存的响应体,减少重复处理

2. 安全考量

  1. 敏感信息过滤:确保错误响应中不包含敏感数据
  2. 错误码标准化:使用统一的错误码体系,便于系统间对接
  3. 速率限制:对频繁的错误请求进行限制,防止暴力攻击

3. 异常处理策略

场景处理方式说明
数据库连接失败重试机制通过重试策略提升系统可用性
无效请求参数验证中间件提前拦截无效请求,减少后续处理
未授权访问身份验证中间件在路由处理前进行权限校验

九、常见问题与踩坑

1. 常见错误

问题原因解决方案
错误未被捕获忘记调用next(err)在catch块中确保调用next(err)
错误响应格式不统一不同路由返回不同格式使用统一的错误处理中间件
错误信息暴露未过滤堆栈信息禁用stack字段输出
错误处理阻塞同步处理错误使用异步处理或队列机制

2. 陷阱分析

错误中间件顺序问题:

// 错误的顺序
app.use((req, res, next) => {
  next();
});

app.use((err, req, res, next) => {
  // 无法捕获错误
});

正确的顺序:

// 正确的顺序
app.use((req, res, next) => {
  next();
});

app.use((err, req, res, next) => {
  // 正确捕获错误
});

十、最佳实践

  1. 统一错误处理:所有错误应通过统一的中间件处理
  2. 错误分类处理:根据错误类型进行差异化处理
  3. 日志记录:记录完整的错误信息和堆栈跟踪
  4. 安全响应:确保错误响应不暴露敏感信息
  5. 异常处理分离:将错误处理逻辑抽离到独立模块
  6. 性能优化:使用异步处理机制避免阻塞主线程
  7. 测试覆盖:对错误处理逻辑进行充分的单元测试

十一、总结

自定义路由异常处理中间件是构建健壮Express应用的关键组件。通过统一的错误处理机制,我们可以:

  • 简化错误处理逻辑
  • 提高代码可维护性
  • 增强系统安全性
  • 提供一致的错误响应格式

在实际开发中,应根据具体业务需求选择合适的错误处理策略。对于复杂系统,建议采用分层的错误处理机制,结合日志记录、性能优化和安全防护措施,构建完整的错误处理体系。同时要避免常见的陷阱,如错误中间件顺序问题和错误信息暴露等,确保系统的稳定性和安全性。

2024-08-08

'# Django模板,Django中间件,ORM操作(pymysql + SQL语句),连接池,session和cookie, 缓存

一、背景与问题

在Django开发中,模板系统、中间件、ORM操作、连接池、session和cookie、缓存是构建高性能Web应用的核心要素。这些技术看似独立,实则相互关联:模板负责前端渲染,中间件控制请求生命周期,ORM操作数据库,连接池管理数据库连接,session和cookie处理用户状态,缓存提升性能。

实际开发中常遇到的挑战包括:

  • ORM查询性能瓶颈
  • 中间件逻辑冲突
  • 缓存失效导致的数据不一致
  • session存储的分布式问题
  • 数据库连接池配置不当引发的资源浪费

本文将深入剖析这些技术的原理和实现,结合完整案例展示最佳实践。

二、基本原理

1. Django模板系统

Django模板系统采用模板继承和变量替换机制,通过Template和Context对象实现动态渲染。其核心原理是将模板中的变量和标签解析为Python代码,最后执行生成HTML。

2. 中间件(Middleware)

Django中间件是处理请求的钩子框架,按顺序执行process_request和process_response方法。每个中间件可以修改请求对象或响应对象,影响整个请求生命周期。

3. ORM操作

Django ORM通过代理模式实现数据库操作,将模型类实例与数据库表映射。底层使用SQLAlchemy的ORM模式,通过query对象构建SQL语句。

4. 连接池

连接池通过池化技术管理数据库连接,避免频繁创建和销毁连接的开销。Django默认使用dbutils库实现连接池,通过pool参数配置最大连接数。

5. session和cookie

session是服务器端的会话状态存储,通过cookie保存会话ID。Django支持多种session存储方式(内存、数据库、缓存),通过SESSION_ENGINE配置。

6. 缓存

缓存通过缓存中间件实现,支持内存、数据库、Redis等后端。Django提供cache模块,通过@cache_page装饰器和cache视图函数实现缓存。

三、环境准备

# 安装依赖
pip install django==4.2.1
pip install pymysql
pip install redis

项目结构:

myproject/
├── myapp/
│   ├── models.py
│   ├── views.py
│   ├── middleware.py
│   └── templates/
│       └── index.html
├── settings.py
├── urls.py
└── manage.py

四、核心实现

1. ORM操作(pymysql + SQL语句)

# models.py
from django.db import models
from django.db import connection

class User(models.Model):
    name = models.CharField(max_length=100)
    email = models.EmailField()

# 使用ORM
users = User.objects.filter(name__startswith='A').values('id', 'name')

# 使用原始SQL
with connection.cursor() as cursor:
    cursor.execute("SELECT * FROM myapp_user WHERE name LIKE 'A%'")
    results = cursor.fetchall()

关键代码解释:

  • connection.cursor()获取数据库连接
  • execute()执行SQL语句
  • fetchall()获取查询结果
  • 使用__startswith等字段查询操作符

2. 连接池配置

# settings.py
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'mydb',
        'USER': 'root',
        'PASSWORD': 'password',
        'HOST': 'localhost',
        'PORT': '3306',
        'OPTIONS': {
            'init_command': "SET NAMES utf8mb4",
            'charset': 'utf8mb4',
            'pool_size': 10,  # 最大连接数
            'max_overflow': 5,  # 超过池大小的连接数
        }
    }
}

3. session和cookie处理

# views.py
from django.http import HttpResponse
from django.shortcuts import render

def login(request):
    if request.method == 'POST':
        username = request.POST['username']
        request.session['user'] = username  # 存储session
        return HttpResponse('Login successful')
    return render(request, 'login.html')

def profile(request):
    user = request.session.get('user')  # 获取session
    return HttpResponse(f'Welcome, {user}')

五、完整案例

1. 博客系统案例

项目需求:

  • 使用模板展示博客列表
  • 中间件记录访问日志
  • ORM操作数据库
  • 缓存热门文章
  • session管理用户登录状态
# urls.py
from django.urls import path
from . import views

urlpatterns = [
    path('', views.index, name='index'),
    path('login/', views.login, name='login'),
    path('article/<int:article_id>/', views.article_detail, name='article_detail'),
]

# views.py
from django.shortcuts import render
from .models import Article
from django.core.cache import cache
from django.http import HttpResponse

def index(request):
    # 缓存热门文章
    articles = cache.get('hot_articles')
    if not articles:
        articles = Article.objects.filter(is_hot=True).all()
        cache.set('hot_articles', articles, 60*15)  # 缓存15分钟
    
    return render(request, 'index.html', {'articles': articles})

def article_detail(request, article_id):
    article = Article.objects.get(id=article_id)
    return render(request, 'article.html', {'article': article})
# middleware.py
from django.utils.deprecation import MiddlewareMixin

class LoggingMiddleware(MiddlewareMixin):
    def process_request(self, request):
        print(f"Request: {request.path}")
        # 记录访问日志到数据库
        # Log.objects.create(path=request.path, method=request.method)

六、源码解析

1. ORM查询执行流程

# django/db/models/manager.py
def get_queryset(self):
    if self._queryset is None:
        self._queryset = self.model._default_manager.all()
    return self._queryset

def all(self):
    return self._get_queryset().all()

当调用User.objects.all()时,会触发get_queryset()方法,最终调用QuerySet.all()生成SQL语句。

2. 缓存中间件源码

# django/core/cache/backends/base.py
def get(self, key, default=None):
    key = self.make_key(key)
    value = self._cache.get(key)
    if value is not None:
        return value
    return default

def set(self, key, value, timeout=None):
    key = self.make_key(key)
    self._cache.set(key, value, timeout)

缓存中间件通过get()和set()方法实现缓存的读取和写入。

七、进阶使用

1. ORM性能优化

  • 使用select_related()关联查询
  • 使用prefetch_related()批量查询
  • 添加索引优化查询速度
# 使用select_related
User.objects.select_related('profile').all()

# 使用prefetch_related
User.objects.prefetch_related('articles').all()

2. 缓存策略优化

  • 使用@cache_page装饰器缓存视图
  • 设置合理的缓存时间
  • 使用Redis替代内存缓存
# settings.py
CACHES = {
    'default': {
        'BACKEND': 'django_redis.cache.RedisCache',
        'LOCATION': 'redis://127.0.0.1:6379/1',
        'OPTIONS': {
            'REDIS_CONNECTION_POOL_MAXSIZE': 10,
        }
    }
}

八、性能与工程实践

1. 数据库性能优化

  • 使用EXPLAIN分析查询计划
  • 为常用查询字段添加索引
  • 避免N+1查询问题
EXPLAIN SELECT * FROM myapp_user WHERE name LIKE 'A%';

2. 缓存失效策略

  • 设置合理的缓存过期时间
  • 使用缓存更新策略(write-through/ read-through)
  • 实现缓存降级机制

3. session安全策略

  • 使用SESSION_COOKIE_SECURE=True强制HTTPS
  • 设置SESSION_COOKIE_HTTPONLY=True防止XSS攻击
  • 使用SESSION_COOKIE_DOMAIN控制Cookie作用域

九、常见问题与踩坑

1. ORM查询性能问题

错误示例:

for user in User.objects.all():
    print(user.articles.all())

问题:产生N+1查询,导致性能下降

解决办法:使用prefetch_related

for user in User.objects.prefetch_related('articles').all():
    print(user.articles.all())

2. 中间件顺序问题

错误示例:日志中间件在认证中间件之前执行

后果:未认证的请求会被记录日志,但后续处理可能被拦截

解决办法:调整中间件顺序

# settings.py
MIDDLEWARE = [
    'myapp.middleware.LoggingMiddleware',
    'myapp.middleware.AuthMiddleware',
]

3. 缓存未命中问题

错误示例:缓存键名不一致

# 错误
cache.set('articles', articles, 60)
cache.get('articles')  # 正确

# 错误
cache.set('articles', articles, 60)
cache.get('Article')  # 错误

十、最佳实践

1. ORM使用规范

  • 优先使用ORM查询,避免直接执行SQL
  • 使用values()获取特定字段
  • 为查询添加select_related()和prefetch_related()

2. 缓存策略建议

  • 热点数据使用缓存
  • 避免缓存敏感数据
  • 使用Redis作为缓存后端
  • 设置合适的缓存过期时间

3. session管理规范

  • 使用SESSION_COOKIE_DOMAIN控制Cookie作用域
  • 设置SESSION_COOKIE_HTTPONLY=True防止XSS
  • 定期清理过期session

十一、总结

Django的模板系统、中间件、ORM操作、连接池、session和cookie、缓存等技术构成了Web开发的核心体系。通过深入理解这些技术的原理和实现,我们可以在实际开发中做出更优的决策:

  • 使用ORM进行数据库操作时,要合理使用查询优化技术
  • 中间件需要谨慎处理请求生命周期,避免逻辑冲突
  • 缓存需要设计合理的失效策略和更新机制
  • session和cookie管理要兼顾安全性和可用性
  • 连接池配置要根据业务需求调整参数

在实际项目中,应根据业务场景选择合适的方案:

  • 对于高频访问的接口,优先使用缓存
  • 对于复杂查询,使用ORM的查询优化功能
  • 对于分布式系统,使用Redis作为session存储
  • 对于数据敏感的场景,启用数据库事务和日志记录

通过合理组合这些技术,我们可以构建出高性能、可维护的Django应用。

2024-08-08

'# Express利用multer中间件实现文件上传并查看

一、背景与问题

在Web应用开发中,文件上传是一个常见需求。传统HTTP协议中,文件上传需要使用multipart/form-data格式,而Express框架本身并不直接支持这种格式的解析。multer作为Express的官方文件上传中间件,提供了完整的解决方案。

传统方案的痛点包括:

  1. 需要手动解析请求体
  2. 难以控制文件大小和类型
  3. 缺乏文件存储策略管理
  4. 安全性保障不足

multer通过封装这些复杂逻辑,提供了更优雅的解决方案,但其背后涉及多个技术细节需要深入理解。

二、基本原理

multer的工作原理可以分为三个核心阶段:

1. 请求解析

当客户端发送multipart/form-data请求时,multer会:

  • 解析Content-Type头
  • 根据boundary分割数据块
  • 区分字段数据和文件数据

2. 存储处理

multer支持多种存储策略:

  • 内存存储(in-memory)
  • 磁盘存储(disk)
  • 自定义存储策略(custom)

其核心处理流程如下:

请求到达 → multer中间件 → 解析multipart数据 → 根据存储策略保存文件 → 返回响应

3. 文件处理

multer提供文件对象(File)和文件数组(Files):

  • 文件元数据(name, size, mimetype等)
  • 文件路径(path, destination等)
  • 错误处理机制

三、环境准备

1. 依赖安装

npm install express multer

2. 项目结构

./
├── app.js
├── uploads/
└── views/
    └── upload.html

四、核心实现

1. 基础文件上传

// app.js
const express = require('express');
const multer = require('multer');
const app = express();

// 设置存储策略
const storage = multer.diskStorage({
  destination: function (req, file, cb) {
    cb(null, 'uploads/');
  },
  filename: function (req, file, cb) {
    cb(null, Date.now() + '-' + file.originalname);
  }
});

// 初始化multer
const upload = multer({ storage: storage });

// 路由处理
app.get('/upload', (req, res) => {
  res.sendFile(__dirname + '/views/upload.html');
});

app.post('/upload', upload.single('file'), (req, res) => {
  if (!req.file) {
    return res.status(400).send('No file uploaded.');
  }
  res.send('File uploaded: ' + req.file.filename);
});

app.listen(3000, () => {
  console.log('Server started on http://localhost:3000');
});

2. 高级配置示例

// 带验证的文件上传
const upload = multer({
  storage: storage,
  fileFilter: (req, file, cb) {
    if (file.mimetype.startsWith('image/')) {
      cb(null, true);
    } else {
      cb(new Error('Only images are allowed!'));
    }
  },
  limits: {
    fileSize: 1024 * 1024 * 5 // 5MB
  }
});

3. 多文件上传

app.post('/upload-multiple', upload.array('files', 5), (req, res) => {
  if (!req.files || req.files.length === 0) {
    return res.status(400).send('No files uploaded.');
  }
  res.send('Files uploaded: ' + req.files.length);
});

五、完整案例

1. 前端页面(upload.html)

<!DOCTYPE html>
<html>
<head>
  <title>File Upload</title>
</head>
<body>
  <h2>Upload File</h2>
  <form action="/upload" method="post" enctype="multipart/form-data">
    <input type="file" name="file" required>
    <button type="submit">Upload</button>
  </form>
  
  <h2>Upload Multiple Files</h2>
  <form action="/upload-multiple" method="post" enctype="multipart/form-data">
    <input type="file" name="files" multiple required>
    <button type="submit">Upload Multiple</button>
  </form>
</body>
</html>

2. 后端逻辑扩展

// 增加文件查看功能
app.get('/files', (req, res) => {
  const files = fs.readdirSync('uploads');
  res.json(files);
});

// 增加文件删除功能
app.delete('/files/:filename', (req, res) => {
  const filePath = `uploads/${req.params.filename}`;
  if (fs.existsSync(filePath)) {
    fs.unlinkSync(filePath);
    res.json({ message: 'File deleted' });
  } else {
    res.status(404).json({ message: 'File not found' });
  }
});

3. 文件存储结构

uploads/
├── 1632345678901-test.jpg
├── 1632345678902-example.png
└── 1632345678903-sample.pdf

六、源码解析

1. multer核心逻辑

multer的multer函数内部会创建一个中间件对象,其核心处理流程如下:

function multer(options) {
  const storage = options.storage || new Storage();
  const fileFilter = options.fileFilter || (req, file, cb) => cb(null, true);
  const limits = options.limits || {};
  
  return (req, res, next) => {
    // 解析multipart/form-data请求
    const form = new formidable.IncomingForm({
      uploadDir: storage._getUploadPath(req),
      keepExtensions: true
    });
    
    form.on('file', (name, file) => {
      storage._handleFile(req, file, (err, filename) => {
        if (err) return next(err);
        req.file = { filename, ...file };
        next();
      });
    });
    
    form.on('error', (err) => {
      next(err);
    });
    
    form.parse(req);
  };
}

2. 文件存储策略

class Storage {
  _getUploadPath(req) {
    const path = this.options.destination(req);
    if (!fs.existsSync(path)) {
      fs.mkdirSync(path, { recursive: true });
    }
    return path;
  }
  
  _handleFile(req, file, callback) {
    const stream = fs.createWriteStream(`${file.path}.tmp`);
    file.stream.pipe(stream);
    
    stream.on('close', () => {
      const newFile = {
        ...file,
        path: `${file.path}.tmp`,
        destination: this.options.destination(req)
      };
      callback(null, newFile);
    });
  }
}

七、进阶使用

1. 动态存储路径

const storage = multer.diskStorage({
  destination: (req, file, cb) => {
    const year = new Date().getFullYear();
    const month = String(new Date().getMonth() + 1).padStart(2, '0');
    cb(null, `uploads/${year}/${month}`);
  },
  filename: (req, file, cb) => {
    cb(null, `${Date.now()}-${file.originalname}`);
  }
});

2. 自定义存储策略

const storage = multer.memoryStorage({
  limits: {
    fieldNameSize: 255,
    fields: 10,
    fileSize: 1024 * 1024 * 5
  }
});

3. 上传速度限制

const upload = multer({
  storage: storage,
  limits: {
    fileSize: 1024 * 1024 * 5, // 5MB
    fields: 10,
    files: 10,
    parts: 10,
    headers: 1024 * 1024 * 10
  }
});

八、性能与工程实践

1. 性能优化方案

优化策略说明适用场景
内存存储适用于小文件临时文件处理
磁盘存储适用于大文件生产环境
文件压缩压缩图片/视频资源受限环境
并行上传分片上传大文件传输
拆分处理分批处理文件海量文件存储

2. 安全加固措施

安全措施实现方式说明
文件类型限制fileFilter防止恶意文件上传
文件名处理filename防止路径遍历攻击
上传大小限制limits防止资源耗尽
身份验证路由中间件防止未授权访问
文件内容扫描第三方库防止恶意代码

3. 异常处理方案

app.post('/upload', (req, res, next) => {
  try {
    if (!req.file) {
      throw new Error('No file uploaded');
    }
    // 业务逻辑处理
  } catch (err) {
    next(err);
  }
}, (err, req, res, next) => {
  res.status(500).json({ error: err.message });
});

九、常见问题与踩坑

1. 典型错误案例

错误代码:

app.post('/upload', (req, res) => {
  console.log(req.file); // 未正确使用multer中间件
});

错误原因:
未在路由前使用multer中间件,导致req.file未定义

解决方案:

app.post('/upload', upload.single('file'), (req, res) => {
  console.log(req.file);
});

2. 常见问题分析

问题原因解决方案
文件未保存未正确配置storage检查destination配置
上传失败文件类型未限制添加fileFilter验证
路径错误文件名未处理使用UUID生成文件名
性能瓶颈未使用磁盘存储切换到磁盘存储策略
安全漏洞未处理文件名使用hash生成文件名

3. 常见错误修复

错误示例:

const upload = multer();

错误原因:
未配置storage策略,默认使用内存存储,可能导致内存溢出

改进方案:

const upload = multer({
  storage: multer.diskStorage({
    destination: 'uploads/',
    filename: (req, file, cb) => {
      cb(null, file.originalname);
    }
  })
});

十、最佳实践

1. 推荐配置方案

场景推荐配置说明
生产环境磁盘存储 + 文件类型限制稳定可靠
临时文件内存存储低资源消耗
多文件上传array方法灵活处理
高并发场景分片上传提升性能
安全敏感场景严格验证 + 身份验证防止恶意上传

2. 推荐目录结构

./
├── uploads/
│   ├── 2023/
│   │   ├── 01/
│   │   └── 02/
│   └── 2024/
│       └── 01/
├── logs/
├── config/
├── routes/
├── controllers/
└── middlewares/

3. 推荐开发规范

  1. 每个文件上传接口应包含:

    • 文件类型验证
    • 上传大小限制
    • 文件名处理
    • 错误处理机制
    • 上传日志记录
  2. 推荐使用UUID生成文件名:

    const uuid = require('uuid');
    filename = `${uuid.v4()}-${file.originalname}`;

十一、总结

Express结合multer中间件实现文件上传是一个典型的中间件应用案例。通过深入理解multer的内部机制,我们可以更好地控制文件上传的各个方面。在实际开发中,需要根据具体场景选择合适的存储策略,配置合理的上传限制,并加强安全防护。

需要注意的是,multer虽然功能强大,但也有其适用边界。对于需要实时处理、处理超大文件或需要自定义存储逻辑的场景,可能需要结合其他方案(如使用AWS S3或MinIO)。同时,在开发过程中要特别注意文件名处理、大小限制和安全性验证,避免潜在的安全风险。

掌握multer的使用方法,不仅能提升文件上传功能的稳定性,还能为后续的文件管理、版本控制、访问控制等扩展功能打下坚实基础。在实际项目中,建议结合具体业务需求进行合理配置,形成可复用的文件上传解决方案。

2024-08-08

'# 认识爬虫:如何使用 requests 模块模拟浏览器请求爬取网页信息?

一、背景与问题

在现代 Web 开发中,爬虫技术是数据采集的重要手段。无论是构建数据仓库、实现价格监控系统,还是进行市场分析,爬虫都扮演着关键角色。然而,传统浏览器请求和爬虫请求存在本质差异:浏览器会发送完整的 HTTP 头信息(如 User-Agent、Accept-Language 等),而简单的 requests 请求可能因缺少这些信息被服务器识别为非人类请求,从而触发反爬机制。

本文将深入解析 requests 模块的工作原理,结合真实开发场景,展示如何通过模拟浏览器行为安全地爬取网页信息。

二、基本原理

1. HTTP 协议基础

HTTP 是客户端与服务器通信的协议,其核心是请求-响应模型。当使用 requests 发起请求时,实际上是构建一个 HTTP 请求报文,包含以下要素:

  • 请求方法(GET/POST/PUT/DELETE)
  • 请求头(Headers):包含 User-Agent、Accept、Referer 等关键字段
  • 请求体(Body):仅在 POST/PUT 等方法中存在
  • 请求路径(Path):URL 的路径部分

服务器收到请求后,会根据规则返回响应报文,包含状态码(如 200 OK/403 Forbidden/503 Service Unavailable)和响应体(HTML 内容/JSON 数据等)。

2. requests 的工作原理

requests 是 Python 中最受欢迎的 HTTP 库,其底层依赖 urllib3,通过以下机制模拟浏览器行为:

  • 自动处理重定向:自动跟随 301/302 状态码的跳转链接
  • 会话管理:通过 Session 对象维护 cookies 和 headers
  • 连接池:复用 TCP 连接提升性能
  • 异常处理:内置连接超时、HTTP 错误码处理机制

三、环境准备

1. 安装依赖

pip install requests

2. 开发环境

  • Python 3.8+
  • 建议使用虚拟环境(venv)隔离依赖
  • 可选:配合 requests-cache 或 fake-useragent 等辅助库

四、核心实现

1. 基础 GET 请求

import requests

# 发起GET请求
response = requests.get('https://example.com')

# 打印响应状态码
print(f"Status Code: {response.status_code}")

# 打印响应内容
print(response.text)

关键代码解析:

  • requests.get() 构造了一个 HTTP GET 请求
  • 默认会发送 User-Agent: Python-requests/2.x.x 的头信息
  • 响应对象包含 status_code(HTTP 状态码)、text(响应内容)等属性

2. 模拟浏览器头信息

headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
    'Accept-Language': 'zh-CN,zh;q=0.9',
    'Referer': 'https://www.google.com/'
}

response = requests.get('https://httpbin.org/headers', headers=headers)
print(response.json())

关键代码解析:

  • 自定义 User-Agent 模拟 Chrome 浏览器
  • Referer 字段用于告知服务器请求来源
  • httpbin.org 是测试用的 API 网站,返回请求头信息

3. 处理响应内容

if response.status_code == 200:
    # 解析HTML内容
    from bs4 import BeautifulSoup
    soup = BeautifulSoup(response.text, 'html.parser')
    print(soup.title.string)
else:
    print(f"请求失败: {response.status_code}")

关键代码解析:

  • 使用 BeautifulSoup 解析 HTML 文本
  • html.parser 是 Python 内置的解析器
  • 需要安装 beautifulsoup4 依赖(pip install beautifulsoup4)

五、完整案例

案例:爬取豆瓣图书Top250信息

import requests
from bs4 import BeautifulSoup
import time

def get_books(page):
    headers = {
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
        'Referer': 'https://book.douban.com/'
    }
    url = f'https://book.douban.com/top250?start={page * 25}&filter= '  # 每页25条数据
    response = requests.get(url, headers=headers)
    
    if response.status_code != 200:
        print(f"请求失败: {response.status_code}")
        return []
    
    soup = BeautifulSoup(response.text, 'html.parser')
    items = soup.find_all('div', class_='item')
    books = []
    
    for item in items:
        title = item.find('span', class_='title').text.strip()
        author = item.find('div', class_='info').find('p').text.strip()
        rating = item.find('span', class_='rating_num').text.strip()
        books.append({
            'title': title,
            'author': author,
            'rating': rating
        })
    
    return books

# 爬取前10页数据
all_books = []
for page in range(4):  # 前4页共100条数据
    print(f"正在爬取第 {page+1} 页...")
    books = get_books(page)
    all_books.extend(books)
    time.sleep(1)  # 模拟人类操作间隔

# 输出结果
for book in all_books[:10]:
    print(f"书名: {book['title']}, 作者: {book['author']}, 评分: {book['rating']}")

关键代码解析:

  • 豆瓣图书Top250页面通过 start 参数分页
  • 每页包含25条数据,需爬取前4页获取100条数据
  • 使用 time.sleep(1) 模拟人类操作间隔,避免触发反爬机制
  • 使用 BeautifulSoup 提取书籍标题、作者、评分信息

六、源码解析

1. requests.get() 的实现原理

requests.get() 实际上调用了 requests.Session().get(),其核心流程如下:

def get(self, url, **kwargs):
    return self.request('GET', url, **kwargs)
  • 构造 HTTP GET 请求报文
  • 设置默认 headers(包含 User-Agent 等)
  • 发送请求并处理响应
  • 自动处理重定向(可配置 allow_redirects 参数)

2. 会话管理(Session)

session = requests.Session()
session.headers.update({
    'User-Agent': 'Custom User Agent',
    'Accept-Encoding': 'gzip, deflate'
})
response = session.get('https://example.com')
  • Session 对象可以持久化 cookies
  • 可以设置全局 headers,避免重复配置
  • 支持添加代理、验证证书等高级功能

七、进阶使用

1. 处理 cookies

cookies = {
    'session_id': '123456',
    'user_token': 'abcdefg'
}
response = requests.get('https://example.com', cookies=cookies)

2. 设置超时和重试

response = requests.get(
    'https://example.com',
    timeout=5,  # 设置超时时间
    allow_redirects=False  # 禁用重定向
)

3. 使用代理服务器

proxies = {
    'http': 'http://10.10.1.10:3128',
    'https': 'http://10.10.1.10:1080'
}
response = requests.get('https://example.com', proxies=proxies)

八、性能与工程实践

1. 并发请求优化

from concurrent.futures import ThreadPoolExecutor

def fetch_page(page):
    # 实现爬取逻辑
    return f"Page {page} data"

with ThreadPoolExecutor(max_workers=5) as executor:
    results = list(executor.map(fetch_page, range(10)))

2. 使用缓存减少请求

import requests_cache

requests_cache.install_cache('douban_cache', expire_after=3600)  # 缓存1小时
response = requests.get('https://example.com')

3. 异常处理机制

try:
    response = requests.get('https://example.com', timeout=5)
    response.raise_for_status()  # 如果响应状态码不是200,抛出异常
except requests.exceptions.RequestException as e:
    print(f"请求异常: {e}")

九、常见问题与踩坑

1. 常见错误

错误类型原因解决方案
403 Forbidden未正确设置 headers添加 User-Agent、Referer 等字段
503 Service Unavailable服务器暂时不可用增加重试机制,设置 timeout
429 Too Many Requests被限速增加请求间隔,使用代理服务器
10054 连接被拒绝服务器主动断开检查防火墙设置,更换代理

2. 常见问题分析

问题:爬虫被封IP

  • 原因:短时间内发送大量请求,触发服务器限流机制
  • 解决方案:增加请求间隔,使用代理池,设置 headers 模拟真实用户

问题:无法解析响应内容

  • 原因:服务器返回的是二进制数据(如图片)而非 HTML
  • 解决方案:检查响应 content-type,使用 response.content 获取原始数据

问题:请求超时

  • 原因:网络不稳定或服务器处理时间过长
  • 解决方案:设置合理的 timeout 值,增加重试机制

十、最佳实践

1. 推荐实践方案

场景推荐方案原因
简单数据采集requests + BeautifulSoup简单易用,适合静态页面
复杂交互Selenium可模拟真实浏览器行为
高并发爬取asyncio + aiohttp非阻塞IO,提升性能
需要验证requests + PyQuery结合 CSS 选择器提高解析效率

2. 推荐配置参数

  • headers:设置完整的 User-Agent、Referer 等字段
  • timeout:设置合理超时时间(建议 3-5 秒)
  • proxies:使用代理服务器避免IP被封
  • verify:验证SSL证书(生产环境建议启用)

十一、总结

requests 模块是 Python 中最常用的 HTTP 请求库,其通过模拟浏览器行为实现网页数据采集。本文深入解析了其工作原理,结合真实开发场景展示了如何通过合理设置 headers、处理响应内容、优化性能等手段实现高效爬虫。

在实际项目中,requests 适用于静态页面数据采集、简单的 API 调用等场景。但需注意:对于需要复杂交互的网页(如 JavaScript 渲染内容)、有严格反爬机制的网站(如电商平台),应考虑使用 Selenium、Playwright 等工具。同时,需遵守目标网站的 robots.txt 规则,避免对服务器造成过大负担。

通过合理配置 headers、添加异常处理、优化请求频率等措施,可以显著提高爬虫的稳定性和安全性。在开发过程中,始终要保持对技术原理的理解,才能应对各种实际问题和挑战。

2024-08-08

'# 爬虫爬取网页时报错:requests.exceptions.SSLError: HTTPSConnectionPool(host=‘www.cnblogs.com‘, port=443): Max r

一、背景与问题

在Python爬虫开发中,requests.exceptions.SSLError 是最常见的HTTPS连接错误之一。当我们尝试通过HTTPS协议爬取某些网站时,可能会遇到如下错误:

requests.exceptions.SSLError: HTTPSConnectionPool(host='www.cnblogs.com', port=443): Max retries exceeded with url

这个错误的核心原因是:爬虫在建立HTTPS连接时,SSL/TLS协议握手失败。常见原因包括:

  1. 服务器证书验证失败(证书过期、自签名证书、证书链不完整)
  2. 客户端SSL库版本过低(缺少支持现代TLS协议的实现)
  3. 网络代理配置错误
  4. 服务器端配置异常(如SSL证书与域名不匹配)

本文将深入解析该错误的原理,并提供完整的解决方案。

二、基本原理

1. HTTPS协议原理

HTTPS通过SSL/TLS协议对HTTP进行加密,其核心流程如下:

  1. 客户端发起HTTPS请求
  2. 服务器返回证书(包含公钥)
  3. 客户端验证证书有效性(CA签名、域名匹配、有效期等)
  4. 双方协商加密算法和密钥
  5. 建立加密通道进行数据传输

2. SSL证书验证机制

Python的requests库默认会验证服务器证书的以下要素:

  • 证书是否由可信CA签发
  • 证书是否在有效期内
  • 证书中的域名是否与请求的域名匹配
  • 证书链是否完整

当验证失败时会抛出SSLError异常。

三、环境准备

pip install requests

需要验证的网站:https://www.cnblogs.com(注意:该网站可能已变更HTTPS配置)

四、核心实现

1. 基础请求示例

import requests

response = requests.get('https://www.cnblogs.com')
print(response.status_code)

预期结果:返回200 OK状态码

实际结果:抛出SSLError异常

2. 处理SSL证书验证错误

方案一:忽略证书验证(不推荐生产环境使用)

import requests

response = requests.get(
    'https://www.cnblogs.com',
    verify=False  # 忽略证书验证
)
print(response.status_code)

说明:通过verify=False参数禁用证书验证,但会带来安全风险

方案二:使用本地CA证书

import requests

# 假设我们有一个本地CA证书文件 ca.crt
response = requests.get(
    'https://www.cnblogs.com',
    verify='/path/to/ca.crt'  # 指定本地CA证书路径
)
print(response.status_code)

说明:需要确保证书文件与服务器证书链匹配

3. 处理证书过期问题

import requests
from urllib3.exceptions import InsecureRequestWarning

# 禁用SSL警告
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)

response = requests.get(
    'https://www.cnblogs.com',
    verify=False
)
print(response.status_code)

说明:在测试环境中可临时禁用警告,生产环境应避免

五、完整案例

案例:爬取CSDN博客内容(模拟场景)

import requests
from urllib3.exceptions import InsecureRequestWarning

# 配置参数
base_url = 'https://blog.csdn.net'
headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'
}

# 禁用SSL警告
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)

def fetch_page(url):
    try:
        response = requests.get(url, headers=headers, verify=False, timeout=10)
        response.raise_for_status()  # 抛出HTTP错误
        return response.text
    except requests.exceptions.RequestException as e:
        print(f"请求失败: {e}")
        return None

# 使用示例
content = fetch_page(f"{base_url}/xxx")
if content:
    print("成功获取页面内容")
else:
    print("获取内容失败")

关键代码解释:

  1. verify=False:临时禁用证书验证
  2. timeout=10:设置超时时间防止无限等待
  3. raise_for_status():检查HTTP响应状态码

六、源码解析

1. requests库的SSL验证流程

# requests/sessions.py 中的核心逻辑
def send(self, request, **kwargs):
    # ... 其他代码
    if self.verify is not True:
        # 当 verify=False 时跳过证书验证
        kwargs['verify'] = False
    # ... 其他代码

2. SSL证书验证逻辑

# urllib3/connection.py 中的验证逻辑
def connect(self):
    # ... 其他代码
    if self.assert_hostname:
        # 验证证书域名匹配
        if not cert_name_matches(cert, self.host):
            raise SSLCertVerificationError(...)
    # ... 其他代码

七、进阶使用

1. 使用代理服务器

proxies = {
    'https': 'https://proxy.example.com:8888'
}

response = requests.get(
    'https://www.cnblogs.com',
    proxies=proxies,
    verify=False
)

2. 使用自定义SSL上下文

import ssl

context = ssl.create_default_context()
context.check_hostname = False  # 关闭域名验证
context.verify_mode = ssl.CERT_NONE  # 关闭证书验证

response = requests.get(
    'https://www.cnblogs.com',
    verify=context
)

3. 使用连接池优化性能

from requests.adapters import HTTPAdapter
from urllib3.util import retry

session = requests.Session()
session.mount('https://', HTTPAdapter(max_retries=3))

八、性能与工程实践

1. 性能优化

方案说明适用场景
使用连接池复用TCP连接高频访问同一服务器
设置超时防止无限等待网络不稳定场景
并行请求多线程/异步处理大量请求场景
压缩数据减少传输体积带宽受限场景

2. 安全注意事项

  • 证书验证:务必启用证书验证(verify=True),防止中间人攻击
  • 证书更新:定期更新CA证书库(certifi包)
  • 加密算法:优先使用TLSv1.2及以上协议
  • 证书匹配:确保证书域名与请求域名完全匹配(包括子域名)

3. 异常处理

try:
    response = requests.get(url, timeout=5)
except requests.exceptions.SSLError as e:
    print(f"SSL错误: {e}")
except requests.exceptions.Timeout:
    print("请求超时")
except requests.exceptions.RequestException as e:
    print(f"其他错误: {e}")

九、常见问题与踩坑

1. 常见错误及解决方案

问题原因解决方案
证书过期服务器证书未及时更新联系服务器管理员
自签名证书证书未被CA签名使用verify=False临时处理
域名不匹配证书域名与请求域名不一致检查域名拼写
协议版本不兼容服务器使用旧版TLS升级客户端SSL库

2. 常见陷阱

  • 忽略证书验证的隐患:可能导致中间人攻击(MITM)
  • 证书链不完整:需要包含所有中间证书
  • SSLv3协议不安全:需强制使用TLSv1.2及以上版本

十、最佳实践

1. 推荐方案

  1. 优先启用证书验证(verify=True)
  2. 使用最新版requests库(确保支持现代TLS协议)
  3. 配置合理的超时时间(建议5-10秒)
  4. 使用代理时配置验证(防止代理服务器劫持流量)

2. 不推荐方案

  1. 在生产环境使用verify=False
  2. 使用过期的CA证书库
  3. 不处理SSL协议版本
  4. 忽略证书域名匹配检查

十一、总结

requests.exceptions.SSLError 错误本质上是HTTPS协议安全机制的正常反馈,而非程序错误。在爬虫开发中,我们需要理解SSL/TLS协议的工作原理,正确配置证书验证,同时权衡安全性和功能性需求。

建议在实际项目中:

  • 开发阶段:启用证书验证,确保安全性
  • 测试阶段:可临时禁用验证进行调试
  • 生产环境:严格配置证书验证,防止中间人攻击

通过合理配置SSL参数、处理证书验证、优化连接管理,可以有效解决SSL相关错误,同时保证爬虫系统的安全性和稳定性。

2024-08-08

'# Presto------分布式SQL查询引擎

一、背景与问题

在大数据时代,企业常常面临两个核心问题:

  1. 如何高效处理海量数据:传统关系型数据库在处理PB级数据时性能急剧下降
  2. 如何实现跨源数据的统一分析:企业通常部署了Hive、HDFS、S3、MySQL、PostgreSQL等多种数据源

Presto(原名Calcite)作为一款开源的分布式SQL查询引擎,完美解决了这两个问题。它支持毫秒级响应的交互式查询,能够同时连接多个数据源,并且支持动态分区和列式存储等高级特性。在Netflix、Airbnb等企业中,Presto已成为核心分析平台。

二、基本原理

Presto采用分层架构设计,包含以下核心组件:

+---------------------+
|     Coordinator     |  // 协调器
+---------------------+
       | 
       v
+---------------------+     +---------------------+
|      Worker         |<--->|      Worker         |
+---------------------+     +---------------------+
       |                     |
       v                     v
+---------------------+     +---------------------+
|   Data Source       |     |   Data Source       |
+---------------------+     +---------------------+

1. 查询处理流程

  1. 解析阶段:将SQL语句转换为抽象语法树(AST)
  2. 优化阶段:进行谓词下推、列裁剪、分区剪枝等优化
  3. 执行阶段:分布式执行计划生成和调度

2. 分布式执行模型

  • 数据本地性:Worker节点优先处理本地数据
  • 并行计算:每个Worker独立执行任务
  • 结果聚合:通过中间节点进行数据汇总

三、环境准备

1. 系统要求

  • Java 8+
  • 64位操作系统
  • 至少4GB内存
  • 2核CPU

2. 安装部署

# 下载Presto服务器
wget https://repo1.maven.org/maven2/io/prestosql/presto-server/0.283/presto-server-0.283.tar.gz

# 解压并配置
tar -xzf presto-server-0.283.tar.gz
cd presto-server-0.283

# 配置JVM参数(示例)
echo 'Xmx4G' >> presto-server/config/jvm.config

3. 配置数据源

# presto-server/config/config.properties
query.max-memory-per-node=2GB
query.max-total-memory=4GB

# presto-server/config/hive.properties
hive.sasl.enabled=false
hive.server principal=HTTP@EXAMPLE.COM

四、核心实现

1. 基础查询执行

// Presto的QueryRunner接口实现
public class PrestoQueryExecutor {
    private final QueryRunner queryRunner;
    
    public PrestoQueryExecutor(String coordinatorHost) {
        this.queryRunner = new QueryRunner(
            new ConfigFactory()
                .set("coordinator.http.address", coordinatorHost)
                .create()
        );
    }
    
    public void executeQuery(String sql) {
        try {
            ResultSet resultSet = queryRunner.executeQuery(sql);
            while (resultSet.next()) {
                System.out.println(resultSet.getString(1));
            }
        } catch (Exception e) {
            System.err.println("Query execution failed: " + e.getMessage());
        }
    }
}

2. 分布式查询优化

-- 示例:使用分区剪枝
SELECT * FROM hive.default.sales
WHERE date >= '2023-01-01'
  AND date <= '2023-12-31'
  AND region = 'North America'

3. 聚合计算优化

-- 示例:分布式聚合计算
SELECT 
    region, 
    COUNT(*) AS total_sales,
    SUM(sales_amount) AS total_revenue
FROM hive.default.sales
GROUP BY region
ORDER BY total_revenue DESC
LIMIT 10;

五、完整案例

1. 场景描述

某电商平台需要分析用户行为数据,数据存储在Hive和MySQL中,需要实时查询。

2. 系统架构

+-------------------+
|   Presto Cluster  |
+---------+---------+
         |         |
         v         v
+----------------+ +----------------+
|   Hive Metastore |   MySQL Server |
+----------------+ +----------------+

3. 查询案例

-- 查询最近一周的用户活跃数据
SELECT 
    user_id, 
    COUNT(*) AS active_days
FROM (
    SELECT 
        user_id, 
        DATE(timestamp) AS login_date
    FROM hive.default.user_activity
    WHERE DATE(timestamp) >= DATE_SUB(CURRENT_DATE, 7)
) AS daily_activity
GROUP BY user_id
HAVING active_days > 3;

4. 执行结果

user_id | active_days
--------|------------
12345   | 8
67890   | 5
...

六、源码解析

1. Coordinator核心逻辑

// Coordinator处理查询请求
public class Coordinator {
    public void handleQuery(String sql) {
        // 1. 解析SQL
        SqlParser parser = new SqlParser(sql);
        SqlNode sqlNode = parser.parse();
        
        // 2. 优化查询计划
        Optimizer optimizer = new Optimizer(sqlNode);
        Plan plan = optimizer.optimize();
        
        // 3. 生成执行计划
        PlanGenerator generator = new PlanGenerator(plan);
        ExecutionPlan executionPlan = generator.generate();
        
        // 4. 分发任务
        TaskScheduler scheduler = new TaskScheduler(executionPlan);
        scheduler.schedule();
    }
}

2. Worker执行引擎

// Worker执行分布式任务
public class Worker {
    public void executeTask(Task task) {
        // 1. 获取数据源
        DataSource dataSource = task.getDataSource();
        
        // 2. 执行查询
        ResultSet resultSet = dataSource.executeQuery(task.getSql());
        
        // 3. 聚合结果
        Aggregator aggregator = new Aggregator(resultSet);
        AggregatedResult aggregatedResult = aggregator.aggregate();
        
        // 4. 返回结果
        task.setResult(aggregatedResult);
    }
}

七、进阶使用

1. 动态分区处理

-- 使用动态分区加载数据
INSERT INTO hive.default.sales
SELECT 
    user_id, 
    sale_date, 
    amount
FROM 
    s3://data-bucket/user_activity
WHERE 
    sale_date >= '2023-01-01'
    AND sale_date <= '2023-12-31';

2. 列式存储优化

-- 启用列式存储
SET hive.mapred.mode=nonstrict;
SET hive.exec.dynamic.partition=true;
SET hive.exec.dynamic.partition.mode=nonstrict;

3. 跨源查询

-- 跨Hive和MySQL查询
SELECT 
    h.user_id, 
    m.purchase_amount
FROM 
    hive.default.user_activity h
JOIN 
    mysql.purchase_log m
ON 
    h.user_id = m.user_id
WHERE 
    h.login_date > '2023-01-01';

八、性能与工程实践

1. 性能优化策略

  1. 分区策略:按时间/地域划分数据
  2. 列裁剪:只读取需要的列
  3. 缓存策略:使用Redis缓存热点数据
  4. 并行度控制:通过--max-workers参数调整

2. 安全风险分析

  • 数据泄露风险:需配置RBAC访问控制
  • SQL注入风险:使用预编译语句
  • 网络传输风险:启用TLS加密

3. 异常处理机制

try {
    queryRunner.executeQuery(sql);
} catch (QueryExecutionException e) {
    logger.error("Query failed: {}", e.getMessage());
    if (e.getCode() == 400) {
        // 处理无效SQL
    } else if (e.getCode() == 500) {
        // 处理系统错误
    }
}

九、常见问题与踩坑

1. 常见错误

  • 错误1:连接超时

    $ presto --server http://localhost:8200
    ERROR: Could not connect to server: Connection refused

    解决方法:检查防火墙配置和端口开放

  • 错误2:权限不足

    ERROR: Permission denied: user=anonymous, access=select, database=default

    解决方法:配置Hive的访问控制

2. 性能陷阱

  • 陷阱1:未使用分区导致全表扫描

    -- 错误:未使用分区字段
    SELECT * FROM hive.default.large_table;

    改进方法:添加分区条件

    SELECT * FROM hive.default.large_table
    WHERE date_partition >= '2023-01-01';
  • 陷阱2:未使用列裁剪

    -- 错误:读取所有列
    SELECT * FROM hive.default.sales;

    改进方法:明确指定需要的列

    SELECT user_id, amount FROM hive.default.sales;

十、最佳实践

1. 推荐方案

  1. 数据存储:使用列式存储(如ORC、Parquet)
  2. 查询优化:启用谓词下推和列裁剪
  3. 资源管理:设置合理的内存和线程池
  4. 安全配置:启用TLS和RBAC

2. 实施建议

  • 开发阶段:使用Presto的SQL接口进行数据分析
  • 生产环境:部署集群并配置负载均衡
  • 监控系统:集成Prometheus进行性能监控

十一、总结

Presto作为分布式SQL查询引擎,通过其独特的架构设计和优化策略,解决了传统数据库在大数据处理中的诸多痛点。在实际应用中,我们应根据业务需求选择合适的使用场景:

  • 推荐使用场景:

    • 需要跨多个数据源进行统一分析
    • 需要实时查询PB级数据
    • 需要支持动态分区和列式存储
  • 不推荐场景:

    • 需要高并发的OLTP操作
    • 数据量较小的场景
    • 对延迟要求极高的实时系统

通过合理配置和优化,Presto能够显著提升数据分析效率,但需注意其在分布式环境下的特殊性。在实际开发中,建议结合具体业务需求,灵活应用Presto的各项特性,以达到最佳的性能和可靠性。