深入理解:NO_PROXY 如何绕过代理

深入理解:NO_PROXY 如何绕过代理

🔍 问题:为什么设置 NO_PROXY 就能绕过代理?

python 复制代码
os.environ['NO_PROXY'] = 'localhost,127.0.0.1,::1'

这一行代码为什么就能解决代理问题?让我们从底层机制说起。


一、HTTP 代理的工作原理

1.1 正常的 HTTP 请求

复制代码
你的程序                          目标服务器
   │                                  │
   │  GET http://example.com         │
   ├─────────────────────────────────>│
   │                                  │
   │<─────────────────────────────────┤
   │  200 OK + 数据                   │

直接连接,简单明了。

1.2 使用代理的 HTTP 请求

复制代码
你的程序              代理服务器              目标服务器
   │                      │                      │
   │  CONNECT             │                      │
   │  example.com:80      │                      │
   ├─────────────────────>│                      │
   │                      │  GET example.com     │
   │                      ├─────────────────────>│
   │                      │                      │
   │                      │<─────────────────────┤
   │<─────────────────────┤  200 OK + 数据       │
   │  200 OK + 数据       │                      │

关键点

  1. 你的程序不直接连接目标服务器
  2. 而是先连接到代理服务器
  3. 代理服务器代替你去请求目标服务器
  4. 再把结果返回给你

二、问题出在哪里?

2.1 访问本地服务时的错误流程

当你访问 http://localhost:11434 时,如果系统配置了代理:

复制代码
Python 程序                代理服务器            Ollama (localhost:11434)
   │                          │                           │
   │  请求 localhost:11434    │                           │
   ├─────────────────────────>│                           │
   │                          │  ??? localhost ???        │
   │                          │  我在哪找这个地址?       │
   │                          │  这是你本地的地址!       │
   │                          │                           │
   │<─────────────────────────┤                           │
   │  502 Bad Gateway         │                           │
   │  (代理无法处理)           │                           │

问题本质

  • localhost 是相对地址,指的是"当前机器"
  • 对于你的程序 来说,localhost = 你的电脑
  • 对于代理服务器 来说,localhost = 代理服务器自己
  • 代理服务器去自己身上找 Ollama 服务,当然找不到!

2.2 为什么命令行工具不受影响?

复制代码
# Ollama CLI (原生程序)
ollama list
  └─> 直接读取本地配置,不走 HTTP 协议
  └─> 或者直接建立 TCP 连接,不经过 HTTP 代理层

# Python 的 ollama 库
import ollama
ollama.list()
  └─> 使用 requests/httpx 库
  └─> 发起 HTTP 请求: GET http://localhost:11434/api/tags
  └─> requests 库自动检测系统代理
  └─> 把请求发给代理 ❌

三、NO_PROXY 的工作机制

3.1 requests 库的源码逻辑

让我们看看 requests 库是怎么处理代理的:

python 复制代码
# requests/utils.py (简化版)

def get_environ_proxies(url, no_proxy=None):
    """
    返回这个 URL 应该使用的代理
    """
    # 1. 获取环境变量中的代理配置
    proxies = {}
    
    # 读取 HTTP_PROXY
    http_proxy = os.environ.get('HTTP_PROXY') or os.environ.get('http_proxy')
    if http_proxy:
        proxies['http'] = http_proxy
    
    # 读取 HTTPS_PROXY
    https_proxy = os.environ.get('HTTPS_PROXY') or os.environ.get('https_proxy')
    if https_proxy:
        proxies['https'] = https_proxy
    
    # 2. 检查 NO_PROXY - 关键部分!
    if no_proxy is None:
        no_proxy = os.environ.get('NO_PROXY') or os.environ.get('no_proxy')
    
    # 3. 判断目标 URL 是否在 NO_PROXY 列表中
    if should_bypass_proxies(url, no_proxy):
        # 如果在列表中,返回空代理(即不使用代理)
        return {}
    
    # 4. 否则返回代理配置
    return proxies


def should_bypass_proxies(url, no_proxy):
    """
    判断这个 URL 是否应该绕过代理
    """
    if not no_proxy:
        return False
    
    # 解析 URL,提取主机名
    parsed = urlparse(url)
    host = parsed.hostname
    
    # 解析 NO_PROXY 列表
    no_proxy_list = [x.strip() for x in no_proxy.split(',')]
    
    # 逐个检查
    for pattern in no_proxy_list:
        if pattern == '*':
            # * 表示所有地址都不走代理
            return True
        
        # 检查是否匹配
        if host == pattern:
            # 精确匹配:localhost == localhost
            return True
        
        if host == pattern.lstrip('.'):
            # .example.com 匹配 example.com
            return True
        
        if host.endswith('.' + pattern.lstrip('.')):
            # .example.com 匹配 sub.example.com
            return True
    
    return False

3.2 实际执行流程

当你设置 NO_PROXY = 'localhost,127.0.0.1,::1' 后:

python 复制代码
# 你的代码
import os
os.environ['NO_PROXY'] = 'localhost,127.0.0.1,::1'
import ollama

# 调用 ollama.list() 时
ollama.list()
  └─> requests.get('http://localhost:11434/api/tags')
      └─> get_environ_proxies('http://localhost:11434/api/tags')
          │
          ├─> 读取到 NO_PROXY = 'localhost,127.0.0.1,::1'
          │
          ├─> should_bypass_proxies('http://localhost:11434', 'localhost,127.0.0.1,::1')
          │   │
          │   ├─> 解析 URL: host = 'localhost'
          │   │
          │   ├─> NO_PROXY 列表: ['localhost', '127.0.0.1', '::1']
          │   │
          │   ├─> 检查: host('localhost') == pattern('localhost') ✅ 匹配!
          │   │
          │   └─> 返回 True (应该绕过代理)
          │
          └─> 返回 {} (空代理配置)
      
      └─> 直接连接 localhost:11434,不经过代理 ✅

四、图解对比

4.1 没有设置 NO_PROXY

python 复制代码
# 没有 NO_PROXY
import ollama
ollama.list()
复制代码
┌──────────────────────────────────────────────┐
│ Python 进程                                  │
│                                              │
│  import ollama                               │
│  ollama.list()                               │
│    └─> requests.get('http://localhost:11434')│
│          └─> 检查代理配置                    │
│                ├─ HTTP_PROXY: 未设置         │
│                ├─ 系统代理: 127.0.0.1:7890 ✅│
│                └─ NO_PROXY: 无 ❌            │
│                                              │
│          └─> 使用代理: 127.0.0.1:7890        │
└──────────────────────────────────────────────┘
                    │
                    ↓
┌──────────────────────────────────────────────┐
│ 代理服务器 (127.0.0.1:7890)                  │
│                                              │
│  收到请求: GET http://localhost:11434/api/tags│
│                                              │
│  localhost? 这是哪里?                       │
│  在我(代理服务器)本地找? 找不到!            │
│                                              │
│  返回: 502 Bad Gateway ❌                    │
└──────────────────────────────────────────────┘

4.2 设置了 NO_PROXY

python 复制代码
# 设置了 NO_PROXY
import os
os.environ['NO_PROXY'] = 'localhost,127.0.0.1,::1'
import ollama
ollama.list()
复制代码
┌──────────────────────────────────────────────┐
│ Python 进程                                  │
│                                              │
│  os.environ['NO_PROXY'] = 'localhost,...'    │
│  import ollama                               │
│  ollama.list()                               │
│    └─> requests.get('http://localhost:11434')│
│          └─> 检查代理配置                    │
│                ├─ HTTP_PROXY: 未设置         │
│                ├─ 系统代理: 127.0.0.1:7890   │
│                └─ NO_PROXY: localhost ✅      │
│                                              │
│          └─> 匹配到 NO_PROXY!               │
│          └─> 绕过代理,直接连接 ✅            │
└──────────────────────────────────────────────┘
                    │
                    ↓ (直连,跳过代理)
                    │
┌──────────────────────────────────────────────┐
│ Ollama 服务 (localhost:11434)                │
│                                              │
│  收到请求: GET /api/tags                     │
│  处理请求...                                 │
│  返回: 200 OK + 模型列表 ✅                  │
└──────────────────────────────────────────────┘

五、为什么必须在导入前设置?

5.1 错误的顺序

python 复制代码
# ❌ 错误示例
import ollama  # 1. requests 库此时已经初始化,读取了系统代理配置

import os
os.environ['NO_PROXY'] = 'localhost'  # 2. 太晚了!requests 已经缓存了代理配置

ollama.list()  # 3. 使用的还是旧的代理配置

5.2 正确的顺序

python 复制代码
# ✅ 正确示例
import os
os.environ['NO_PROXY'] = 'localhost'  # 1. 先设置环境变量

import ollama  # 2. requests 库初始化时,会读取到 NO_PROXY

ollama.list()  # 3. 使用正确的配置(绕过代理)

5.3 更深层的原因

python 复制代码
# requests 库内部(简化)
class Session:
    def __init__(self):
        # 初始化时读取代理配置
        self.proxies = get_environ_proxies()  # 读取环境变量
        # 之后一般不会再重新读取

如果你在 import requests 之后才设置环境变量,Session 对象已经创建并缓存了代理配置,再改环境变量就来不及了。


六、实验验证

让我写一个完整的验证脚本:

python 复制代码
# test_no_proxy.py
import os
import sys

def test_without_no_proxy():
    """测试:不设置 NO_PROXY"""
    print("=" * 50)
    print("测试 1: 不设置 NO_PROXY")
    print("=" * 50)
    
    # 清除 NO_PROXY
    os.environ.pop('NO_PROXY', None)
    os.environ.pop('no_proxy', None)
    
    try:
        import ollama
        result = ollama.list()
        print("✅ 成功连接(如果有系统代理可能失败)")
        print(f"结果: {result}")
    except Exception as e:
        print(f"❌ 连接失败: {e}")
        print(f"错误类型: {type(e).__name__}")

def test_with_no_proxy():
    """测试:设置 NO_PROXY"""
    print("\n" + "=" * 50)
    print("测试 2: 设置 NO_PROXY")
    print("=" * 50)
    
    # 设置 NO_PROXY
    os.environ['NO_PROXY'] = 'localhost,127.0.0.1,::1'
    os.environ['no_proxy'] = 'localhost,127.0.0.1,::1'
    print(f"NO_PROXY 已设置: {os.environ['NO_PROXY']}")
    
    # 重新导入 ollama(强制重新初始化)
    if 'ollama' in sys.modules:
        del sys.modules['ollama']
    
    try:
        import ollama
        result = ollama.list()
        print("✅ 成功连接")
        print(f"可用模型数: {len(result.get('models', []))}")
    except Exception as e:
        print(f"❌ 连接失败: {e}")

def test_requests_directly():
    """测试:直接使用 requests"""
    print("\n" + "=" * 50)
    print("测试 3: 验证 requests 库的行为")
    print("=" * 50)
    
    import requests
    
    # 获取当前的代理配置
    session = requests.Session()
    url = 'http://localhost:11434/api/tags'
    
    print(f"目标 URL: {url}")
    print(f"Session 代理配置: {session.proxies}")
    
    # 检查这个 URL 是否会绕过代理
    from requests.utils import should_bypass_proxies
    no_proxy = os.environ.get('NO_PROXY', os.environ.get('no_proxy'))
    will_bypass = should_bypass_proxies(url, no_proxy)
    
    print(f"NO_PROXY 环境变量: {no_proxy}")
    print(f"是否绕过代理: {will_bypass}")

if __name__ == '__main__':
    # test_without_no_proxy()  # 可能失败
    test_with_no_proxy()       # 应该成功
    test_requests_directly()   # 显示内部状态

运行结果:

bash 复制代码
> python test_no_proxy.py

==================================================
测试 2: 设置 NO_PROXY
==================================================
NO_PROXY 已设置: localhost,127.0.0.1,::1
✅ 成功连接
可用模型数: 1

==================================================
测试 3: 验证 requests 库的行为
==================================================
目标 URL: http://localhost:11434/api/tags
Session 代理配置: {}
NO_PROXY 环境变量: localhost,127.0.0.1,::1
是否绕过代理: True

七、总结

核心原理

复制代码
NO_PROXY 环境变量
    ↓
被 requests 库读取
    ↓
用于判断目标 URL 是否应该绕过代理
    ↓
如果目标主机在 NO_PROXY 列表中
    ↓
直接连接,不使用代理
    ↓
问题解决!✅

关键要素

  1. NO_PROXY 是标准规范

    • 不是 Python 特有的,是通用的环境变量标准
    • 几乎所有 HTTP 客户端库都支持(curl, wget, requests, axios 等)
  2. 匹配机制

    python 复制代码
    NO_PROXY = 'localhost,127.0.0.1,::1,.example.com,*.local'
    
    # 匹配规则:
    # localhost      → 精确匹配主机名
    # 127.0.0.1      → 精确匹配 IP
    # .example.com   → 匹配域名及所有子域名
    # *.local        → 通配符匹配
  3. 优先级

    复制代码
    代码中设置 > 环境变量 > 系统代理设置
  4. 必须在导入前设置

    python 复制代码
    # 对
    import os
    os.environ['NO_PROXY'] = '...'
    import requests
    
    # 错
    import requests
    os.environ['NO_PROXY'] = '...'  # 太晚了

一句话总结

NO_PROXY 告诉 HTTP 客户端库:"这些地址不要走代理,直接连接!"


希望这个解释能帮你彻底理解 NO_PROXY 的工作原理!🎯

相关推荐
lkbhua莱克瓦242 小时前
深入理解HTTP协议:从理论到SpringBoot实践
网络·笔记·后端·网络协议·http·javaweb
华硕之声2 小时前
苏式废土美学游戏
网络·数据·华硕
@@123456胡斌3 小时前
渗透文件内容
网络
欧洵.3 小时前
从输入URL到页面展示:完整过程拆解
网络
沉默-_-3 小时前
微信小程序网络请求 wx.request 详解
网络·学习·微信小程序·小程序
newsxun3 小时前
从一条国道,到一个时代符号——申晨案例解析:如何打造公路文旅现象级IP「此生必驾318」
网络·网络协议·tcp/ip
Bruce_Liuxiaowei4 小时前
基于抓包分析的组播视频流故障排查与解决
网络·网络协议·wireshark·信号处理
wniuniu_4 小时前
增加依据。。
服务器·网络·数据库
KKKlucifer4 小时前
数据安全服务的技术深耕与场景适配:行业实践全景解析
网络·安全