当前位置: 首页 > news >正文

8.1 ESP32CAM 服务器 网络摄像头

arduino:

#include <WiFi.h>
#include <WiFiClient.h>
#include "esp_camera.h"
#include <esp_sleep.h>
#include "driver/rtc_cntl.h"  // 包含Brownout检测API// 配置摄像头引脚 (AI Thinker模组)
#define CAMERA_MODEL_AI_THINKER
#define PWDN_GPIO_NUM     32
#define RESET_GPIO_NUM    -1
#define XCLK_GPIO_NUM      0
#define SIOD_GPIO_NUM     26
#define SIOC_GPIO_NUM     27
#define Y9_GPIO_NUM       35
#define Y8_GPIO_NUM       34
#define Y7_GPIO_NUM       39
#define Y6_GPIO_NUM       36
#define Y5_GPIO_NUM       21
#define Y4_GPIO_NUM       19
#define Y3_GPIO_NUM       18
#define Y2_GPIO_NUM        5
#define VSYNC_GPIO_NUM    25
#define HREF_GPIO_NUM     23
#define PCLK_GPIO_NUM     22// 配置WiFi和服务器
const char* ssid = "CHINAMI-Q5BJ0G7 2324";
const char* password = "ss20051512";
const char* server = "159.75.100.98"; // 公网IP
const int port = 554;// 身份验证密钥
const String AUTH_KEY = "1234567890";// 电压检测引脚 (正确引脚)
const int VOLTAGE_PIN = 13; // GPIO13 用于电压检测// 全局标志,检测是否从深度睡眠唤醒
RTC_DATA_ATTR bool isWakeFromDeepSleep = false;void setupCamera() {camera_config_t config;config.ledc_channel = LEDC_CHANNEL_0;config.ledc_timer = LEDC_TIMER_0;config.pin_d0 = Y2_GPIO_NUM;config.pin_d1 = Y3_GPIO_NUM;config.pin_d2 = Y4_GPIO_NUM;config.pin_d3 = Y5_GPIO_NUM;config.pin_d4 = Y6_GPIO_NUM;config.pin_d5 = Y7_GPIO_NUM;config.pin_d6 = Y8_GPIO_NUM;config.pin_d7 = Y9_GPIO_NUM;config.pin_xclk = XCLK_GPIO_NUM;config.pin_pclk = PCLK_GPIO_NUM;config.pin_vsync = VSYNC_GPIO_NUM;config.pin_href = HREF_GPIO_NUM;config.pin_sscb_sda = SIOD_GPIO_NUM;config.pin_sscb_scl = SIOC_GPIO_NUM;config.pin_pwdn = PWDN_GPIO_NUM;config.pin_reset = RESET_GPIO_NUM;config.xclk_freq_hz = 10000000; // 降低时钟频率节省功耗config.pixel_format = PIXFORMAT_JPEG;// 根据可用PSRAM调整分辨率if(psramFound()){config.frame_size = FRAMESIZE_SVGA; // 800x600config.jpeg_quality = 12;config.fb_count = 2;} else {config.frame_size = FRAMESIZE_VGA; // 640x480config.jpeg_quality = 15;config.fb_count = 1;}// 启动摄像头esp_err_t err = esp_camera_init(&config);if (err != ESP_OK) {Serial.printf("摄像头初始化失败 0x%x", err);delay(1000);ESP.restart();}
}void connectWiFi() {// 如果从深度睡眠唤醒且已连接过WiFi,跳过连接if(!isWakeFromDeepSleep || WiFi.status() != WL_CONNECTED) {WiFi.begin(ssid, password);Serial.println("正在连接WiFi...");int wifiRetry = 0;while (WiFi.status() != WL_CONNECTED && wifiRetry < 20) {delay(500);Serial.print(".");wifiRetry++;}if (WiFi.status() == WL_CONNECTED) {Serial.println("\nWiFi已连接");Serial.print("IP地址: ");Serial.println(WiFi.localIP());} else {Serial.println("\nWiFi连接失败");}}
}void setup() {Serial.begin(115200);delay(1000);// 检查唤醒原因esp_sleep_wakeup_cause_t wakeup_reason = esp_sleep_get_wakeup_cause();isWakeFromDeepSleep = (wakeup_reason == ESP_SLEEP_WAKEUP_TIMER);// 禁用Brownout检测#ifdef rtc_brownout_det_disablertc_brownout_det_disable();#else// 备用方法WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 0);#endif// 降低CPU频率setCpuFrequencyMhz(80);// 初始化摄像头setupCamera();// 连接WiFiconnectWiFi();
}void loop() {// 如果WiFi未连接,尝试重新连接if (WiFi.status() != WL_CONNECTED) {Serial.println("WiFi断开,尝试重新连接...");connectWiFi();// 如果还是未连接,等待后重试if (WiFi.status() != WL_CONNECTED) {delay(5000);return;}}// 电压检测 (使用正确的引脚)int voltage = analogRead(VOLTAGE_PIN);float voltage_mv = voltage * 3300.0 / 4096.0 * 2.0; // 分压电路比例if (voltage_mv < 3000) { // 3.0V电压过低Serial.printf("电压过低: %.0f mV\n", voltage_mv);delay(5000);return;}
while(1)
{// 捕获照片camera_fb_t *fb = esp_camera_fb_get();if(!fb) {Serial.println("摄像头捕获失败");// 尝试重新初始化摄像头esp_camera_deinit();setupCamera();delay(1000);return;}Serial.printf("捕获照片: %d字节\n", fb->len);// 连接服务器WiFiClient client;if (!client.connect(server, port)) {Serial.println("服务器连接失败");esp_camera_fb_return(fb);delay(5000);return;}// 构造正确的HTTP请求路径 (使用/upload)String boundary = "Esp32CamBoundary";String header = "--" + boundary + "\r\n";header += "Content-Disposition: form-data; name=\"image\"; filename=\"latest.jpg\"\r\n";header += "Content-Type: image/jpeg\r\n\r\n";String footer = "\r\n--" + boundary + "--\r\n";String authHeader = "X-Auth-Key: " + AUTH_KEY + "\r\n";// 使用正确的路径 /uploadclient.print("POST /upload HTTP/1.1\r\n");client.print("Host: " + String(server) + "\r\n");client.print(authHeader);client.print("Content-Type: multipart/form-data; boundary=" + boundary + "\r\n");client.print("Content-Length: " + String(header.length() + footer.length() + fb->len) + "\r\n\r\n");client.print(header);// 发送图片数据size_t chunkSize = 1024;for (size_t offset = 0; offset < fb->len; offset += chunkSize) {size_t toSend = min(chunkSize, fb->len - offset);client.write(fb->buf + offset, toSend);delay(1);}client.print(footer);// 等待响应unsigned long startTime = millis();while (client.connected() && millis() - startTime < 5000) {if (client.available()) {String line = client.readStringUntil('\r');Serial.println(line);}}// 清理资源esp_camera_fb_return(fb);client.stop();Serial.println("照片上传完成");}
}


服务器:

import http.server
import socketserver
import os
import time
import threading
import queue
import socket
import sys
import re
from datetime import datetime# ===== 配置参数 =====
UPLOAD_PORT = 554
WEB_PORT = 18443
AUTH_KEY = "1234567890"  # 与ESP32相同的密钥
IMAGE_PATH = "C:/esp32cam/latest.jpg"  # 图片保存路径
LOG_FILE = "C:/esp32cam/server.log"  # 日志文件路径# HTML模板 - 使用特殊占位符避免冲突
HTML_TEMPLATE = """
<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>ESP32-CAM监控系统 | 高级图像处理</title><link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"><style>/* CSS样式保持不变 */* {margin: 0;padding: 0;box-sizing: border-box;font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;}body {background: linear-gradient(135deg, #1a2a6c, #2c3e50);color: #ecf0f1;min-height: 100vh;overflow-x: hidden;padding: 20px;}/* ... 完整CSS样式 ... */</style>
</head>
<body><div class="container"><header><h1><i class="fas fa-camera"></i> ESP32-CAM 高级监控系统</h1><div class="subtitle">支持实时图像处理与调整</div><div class="status-bar"><div class="status-item"><div class="status-indicator"></div><span>设备在线</span></div><div class="status-item"><i class="fas fa-wifi"></i><span>信号强度: 92%</span></div><div class="status-item"><i class="fas fa-microchip"></i><span>CPU: 42°C</span></div><div class="status-item"><i class="fas fa-bolt"></i><span>电压: 4.8V</span></div></div></header><!-- 控制面板保持不变 --><div class="image-container"><div class="image-wrapper"><canvas id="imageCanvas" width="800" height="600"></canvas><div class="resolution">800×600</div><div class="fps-counter">FPS: 24</div><div class="timestamp">[[LAST_UPDATE]]</div></div><div class="image-controls"><div><button id="autoExposure"><i class="fas fa-bolt"></i> 自动曝光</button></div><div><button id="snapshot"><i class="fas fa-camera"></i> 保存快照</button></div><div><button id="toggleFullscreen"><i class="fas fa-expand"></i> 全屏模式</button></div></div></div></div><script>// 初始化变量let rotation = 0;let flipHorizontal = false;let flipVertical = false;let binarizeEnabled = false;let thresholdValue = 128;let brightness = 1.0;let contrast = 1.0;let saturation = 1.0;let currentFilter = 'normal';// DOM元素const canvas = document.getElementById('imageCanvas');const ctx = canvas.getContext('2d');const img = new Image();// 初始化图像 - 使用时间戳防止缓存const timestamp = [[TIMESTAMP]];img.crossOrigin = "Anonymous";img.src = "/latest.jpg?t=" + timestamp;// 图像加载后处理img.onload = function() {updateImage();// 模拟实时更新setInterval(updateImage, 1000 / 24); // 24fps};// 更新图像显示function updateImage() {// 清空画布ctx.clearRect(0, 0, canvas.width, canvas.height);// 保存当前状态ctx.save();// 设置画布中心为旋转中心ctx.translate(canvas.width / 2, canvas.height / 2);// 应用旋转ctx.rotate(rotation * Math.PI / 180);// 应用翻转let scaleX = flipHorizontal ? -1 : 1;let scaleY = flipVertical ? -1 : 1;ctx.scale(scaleX, scaleY);// 绘制图像ctx.drawImage(img, -img.width / 2, -img.height / 2);// 恢复状态ctx.restore();// 应用二值化if (binarizeEnabled) {applyBinarization();}// 应用滤镜applyFilter();// 更新FPS显示(模拟)document.querySelector('.fps-counter').textContent = `FPS: ${Math.floor(Math.random() * 5 + 20)}`;// 更新时间戳const now = new Date();document.querySelector('.timestamp').textContent = now.toISOString().replace('T', ' ').substring(0, 19);}// 应用二值化function applyBinarization() {const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);const data = imageData.data;for (let i = 0; i < data.length; i += 4) {const r = data[i];const g = data[i + 1];const b = data[i + 2];// 计算灰度值const gray = 0.299 * r + 0.587 * g + 0.114 * b;// 应用阈值const value = gray >= thresholdValue ? 255 : 0;data[i] = value;     // Rdata[i + 1] = value; // Gdata[i + 2] = value; // B}ctx.putImageData(imageData, 0, 0);}// 应用滤镜function applyFilter() {const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);const data = imageData.data;for (let i = 0; i < data.length; i += 4) {let r = data[i];let g = data[i + 1];let b = data[i + 2];// 应用亮度、对比度、饱和度r = applyBrightnessContrast(r, brightness, contrast);g = applyBrightnessContrast(g, brightness, contrast);b = applyBrightnessContrast(b, brightness, contrast);// 应用饱和度if (saturation !== 1.0) {const gray = 0.299 * r + 0.587 * g + 0.114 * b;r = gray + saturation * (r - gray);g = gray + saturation * (g - gray);b = gray + saturation * (b - gray);}// 应用当前滤镜switch(currentFilter) {case 'grayscale':const gray = 0.299 * r + 0.587 * g + 0.114 * b;r = g = b = gray;break;case 'sepia':r = r * 0.393 + g * 0.769 + b * 0.189;g = r * 0.349 + g * 0.686 + b * 0.168;b = r * 0.272 + g * 0.534 + b * 0.131;break;case 'invert':r = 255 - r;g = 255 - g;b = 255 - b;break;case 'warm':r = Math.min(255, r * 1.2);g = Math.min(255, g * 1.1);b = Math.min(255, b * 0.9);break;case 'cool':r = Math.min(255, r * 0.9);g = Math.min(255, g * 1.1);b = Math.min(255, b * 1.2);break;}data[i] = clamp(r);data[i + 1] = clamp(g);data[i + 2] = clamp(b);}ctx.putImageData(imageData, 0, 0);}// 应用亮度和对比度function applyBrightnessContrast(value, brightness, contrast) {// 应用对比度value = ((value - 128) * contrast) + 128;// 应用亮度value = value * brightness;return clamp(value);}// 确保颜色值在0-255范围内function clamp(value) {return Math.min(255, Math.max(0, value));}// 事件监听器document.getElementById('rotateLeft').addEventListener('click', () => {rotation -= 90;updateImage();});document.getElementById('rotateRight').addEventListener('click', () => {rotation += 90;updateImage();});document.getElementById('flipH').addEventListener('click', () => {flipHorizontal = !flipHorizontal;updateImage();});document.getElementById('flipV').addEventListener('click', () => {flipVertical = !flipVertical;updateImage();});document.getElementById('resetRotation').addEventListener('click', () => {rotation = 0;flipHorizontal = false;flipVertical = false;updateImage();});document.getElementById('threshold').addEventListener('input', (e) => {thresholdValue = parseInt(e.target.value);document.getElementById('thresholdValue').textContent = thresholdValue;updateImage();});document.getElementById('toggleBinarize').addEventListener('click', (e) => {binarizeEnabled = !binarizeEnabled;e.target.classList.toggle('active', binarizeEnabled);e.target.innerHTML = binarizeEnabled ? '<i class="fas fa-check-square"></i> 禁用二值化' : '<i class="fas fa-check-square"></i> 启用二值化';updateImage();});document.getElementById('brightness').addEventListener('input', (e) => {brightness = parseFloat(e.target.value);document.getElementById('brightnessValue').textContent = brightness.toFixed(1);updateImage();});document.getElementById('contrast').addEventListener('input', (e) => {contrast = parseFloat(e.target.value);document.getElementById('contrastValue').textContent = contrast.toFixed(1);updateImage();});document.getElementById('saturation').addEventListener('input', (e) => {saturation = parseFloat(e.target.value);document.getElementById('saturationValue').textContent = saturation.toFixed(1);updateImage();});// 滤镜按钮document.querySelectorAll('.filter-btn').forEach(button => {button.addEventListener('click', () => {currentFilter = button.dataset.filter;document.querySelectorAll('.filter-btn').forEach(btn => {btn.classList.toggle('active', btn === button);});updateImage();});});// 自动曝光document.getElementById('autoExposure').addEventListener('click', () => {// 模拟自动曝光算法brightness = 1.2;contrast = 1.1;saturation = 1.0;document.getElementById('brightness').value = brightness;document.getElementById('contrast').value = contrast;document.getElementById('saturation').value = saturation;document.getElementById('brightnessValue').textContent = brightness.toFixed(1);document.getElementById('contrastValue').textContent = contrast.toFixed(1);document.getElementById('saturationValue').textContent = saturation.toFixed(1);currentFilter = 'normal';document.querySelectorAll('.filter-btn').forEach(btn => {btn.classList.toggle('active', btn.dataset.filter === 'normal');});updateImage();});// 保存快照document.getElementById('snapshot').addEventListener('click', () => {const link = document.createElement('a');link.download = `esp32cam-snapshot-${new Date().toISOString().replace(/[:.]/g, '-')}.png`;link.href = canvas.toDataURL('image/png');link.click();});// 全屏模式document.getElementById('toggleFullscreen').addEventListener('click', () => {if (!document.fullscreenElement) {document.querySelector('.image-container').requestFullscreen().catch(err => {console.error(`全屏请求失败: ${err.message}`);});} else {document.exitFullscreen();}});// 初始化按钮状态document.querySelector('.filter-btn[data-filter="normal"]').classList.add('active');// 添加SSE连接const eventSource = new EventSource("/events");eventSource.onmessage = function(event) {// 重新加载图像const newTimestamp = Date.now();img.src = "/latest.jpg?t=" + newTimestamp;};eventSource.onerror = function() {setTimeout(() => {location.reload();}, 3000);};</script>
</body>
</html>
"""# ===== 全局变量 =====
last_update_time = time.time()
client_queue = queue.Queue()
os.makedirs(os.path.dirname(IMAGE_PATH), exist_ok=True)
os.makedirs(os.path.dirname(LOG_FILE), exist_ok=True)# ===== 日志系统 =====
def log(message, level="INFO"):timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")log_entry = f"[{timestamp}] [{level}] {message}"print(log_entry)try:with open(LOG_FILE, "a") as f:f.write(log_entry + "\n")except Exception as e:print(f"写入日志失败: {str(e)}")# ===== 请求处理类 =====
class UploadHandler(http.server.BaseHTTPRequestHandler):timeout = 30  # 30秒超时def log_message(self, format, *args):# 禁用默认日志passdef do_POST(self):global last_update_timeclient_ip = self.client_address[0]try:# 验证身份auth_header = self.headers.get('X-Auth-Key')if auth_header != AUTH_KEY:log(f"来自 {client_ip} 的未授权访问尝试")self.send_response(401)self.end_headers()self.wfile.write(b'Unauthorized')return# 获取图片数据content_length = int(self.headers['Content-Length'])if content_length > 5 * 1024 * 1024:  # 限制5MBlog(f"来自 {client_ip} 的过大文件: {content_length}字节")self.send_response(413)self.end_headers()self.wfile.write(b'File too large')returnpost_data = self.rfile.read(content_length)# 提取JPEG图像jpeg_start = post_data.find(b'\xff\xd8')jpeg_end = post_data.find(b'\xff\xd9')if jpeg_start != -1 and jpeg_end != -1:jpeg_data = post_data[jpeg_start:jpeg_end+2]# 保存最新图片with open(IMAGE_PATH, 'wb') as f:f.write(jpeg_data)# 更新时间戳并通知客户端last_update_time = time.time()log(f"来自 {client_ip} 的图片更新成功, 大小: {len(jpeg_data)}字节")# 通知所有客户端while not client_queue.empty():try:client_queue.get_nowait().put(b'data: update\n\n')except:passself.send_response(200)self.end_headers()self.wfile.write(b'Image updated')else:log(f"来自 {client_ip} 的无效图片数据")self.send_response(400)self.end_headers()self.wfile.write(b'Invalid image data')except Exception as e:log(f"处理上传时出错: {str(e)}", "ERROR")self.send_response(500)self.end_headers()self.wfile.write(f'Server error: {str(e)}'.encode())class WebHandler(http.server.BaseHTTPRequestHandler):timeout = 30  # 30秒超时def log_message(self, format, *args):# 禁用默认日志passdef do_GET(self):client_ip = self.client_address[0]log(f"来自 {client_ip} 的请求: {self.path}")try:# 处理favicon请求if self.path == '/favicon.ico':self.send_response(204)self.end_headers()return# 根目录返回HTML页面if self.path == '/':self.send_response(200)self.send_header('Content-type', 'text/html')self.end_headers()# 格式化最后更新时间last_update = datetime.fromtimestamp(last_update_time).strftime("%Y-%m-%d %H:%M:%S")# 使用安全的替换方法html = HTML_TEMPLATEhtml = html.replace('[[TIMESTAMP]]', str(int(time.time() * 1000)))html = html.replace('[[LAST_UPDATE]]', last_update)self.wfile.write(html.encode('utf-8'))return# SSE事件流if self.path == '/events':self.send_response(200)self.send_header('Content-type', 'text/event-stream')self.send_header('Cache-Control', 'no-cache')self.send_header('Connection', 'keep-alive')self.end_headers()# 创建客户端队列并添加到全局队列q = queue.Queue()client_queue.put(q)log(f"来自 {client_ip} 的SSE连接已建立")try:while True:try:# 等待新消息或超时msg = q.get(timeout=30)self.wfile.write(msg)self.wfile.flush()except queue.Empty:# 发送心跳保持连接self.wfile.write(b': heartbeat\n\n')self.wfile.flush()except ConnectionResetError:log(f"来自 {client_ip} 的SSE连接已关闭")except Exception as e:log(f"SSE连接错误: {str(e)}", "ERROR")finally:# 清理队列while not q.empty():q.get()return# 返回最新图片if self.path.startswith('/latest.jpg'):try:with open(IMAGE_PATH, 'rb') as f:img_data = f.read()self.send_response(200)self.send_header('Content-type', 'image/jpeg')self.send_header('Cache-Control', 'no-cache, no-store, must-revalidate')self.send_header('Pragma', 'no-cache')self.send_header('Expires', '0')self.end_headers()self.wfile.write(img_data)except FileNotFoundError:self.send_response(404)self.end_headers()self.wfile.write(b'Image not found')return# 处理其他请求self.send_response(404)self.end_headers()self.wfile.write(b'Not found')except Exception as e:log(f"处理Web请求时出错: {str(e)}", "ERROR")self.send_response(500)self.end_headers()self.wfile.write(f'Server error: {str(e)}'.encode())# ===== 服务器管理 =====
def run_server(port, handler):with socketserver.ThreadingTCPServer(("", port), handler) as httpd:httpd.allow_reuse_address = Truelog(f"服务器运行在端口 {port}")try:httpd.serve_forever()except KeyboardInterrupt:log("服务器正在关闭...")finally:httpd.server_close()def watchdog():"""重启卡死的服务器"""while True:ii = 0# ===== 主程序 =====
if __name__ == "__main__":log("======= ESP32Cam监控服务器启动 =======")log(f"上传端口: {UPLOAD_PORT}, Web端口: {WEB_PORT}")log(f"图片保存路径: {IMAGE_PATH}")# 启动看门狗线程watchdog_thread = threading.Thread(target=watchdog, daemon=True)watchdog_thread.start()# 启动上传服务器upload_thread = threading.Thread(target=run_server, args=(UPLOAD_PORT, UploadHandler),daemon=True)upload_thread.start()# 启动Web服务器try:run_server(WEB_PORT, WebHandler)except Exception as e:log(f"服务器严重错误: {str(e)}", "CRITICAL")sys.exit(1)

http://www.lryc.cn/news/602188.html

相关文章:

  • 【mysql】—— mysql中的timestamp 和 datetime(6) 有什么区别,为什么有的地方不建议使用timestamp
  • 深入探索Linux:忙碌的车间“进程”间通信
  • 【Linux】基本指令(2)
  • Linux DNS解析1--终端通过网关或者路由器进行域名解析的原理
  • WAIC 2025深度解析:当“养虎”警示遇上机器人拳击赛
  • 设计模式(二十二)行为型:策略模式详解
  • 发布“悟能”具身智能平台,商汤让机器人像人一样和现实世界交互
  • 枚举策略模式实战:优雅消除支付场景的if-else
  • 时序数据基座升维:Apache IoTDB 以“端边云AI一体化”重构工业智能决策
  • 企业级JWT验证最佳方案:StringUtils.hasText()
  • 【学习路线】AI开发工程师成长指南:从机器学习基础到大模型应用
  • Ubuntu服务器上JSP运行缓慢怎么办?全面排查与优化方案
  • Python 列表内存存储本质:存储差异原因与优化建议
  • 【Linux | 网络】传输层(UDP和TCP) - 两万字详细讲解!!
  • 二级域名分发源码最新开源版
  • uni-datetime-picker兼容ios
  • 无界设计新生态:Penpot开源平台与cpolar的云端协同创新实践
  • CacheGen:用于快速大语言模型推理服务的 KV 缓存压缩与流式传输
  • 【Unity笔记】Unity Camera.cullingMask 使用指南:Layer 精准控制、XR 多视图与性能提升
  • Python + Requests库爬取动态Ajax分页数据
  • 云原生作业(haproxy)
  • 迅为RK3568开发板OpeHarmony学习开发手册-配置电源管理芯片和点亮HDMI屏幕-配置电源管理芯片
  • Vue2-封装一个含所有表单控件且支持动态增减行列的表格组件
  • 行业案例:杰和科技为智慧教育构建数字化硬件底座
  • vue如何在data里使用this
  • 【保姆级喂饭教程】Python依赖管理工具大全:Virtualenv、venv、Pipenv、Poetry、pdm、Rye、UV、Conda、Pixi等
  • 热门JavaScript库“is“等软件包遭npm供应链攻击植入后门
  • 【SpringMVC】MVC中Controller的配置 、RestFul的使用、页面重定向和转发
  • 构建你的专属区块链:深入了解 Polkadot SDK
  • C语言-数组:数组(定义、初始化、元素的访问、遍历)内存和内存地址、数组的查找算法和排序算法;