2024年11月

在 chatGPT 的推动下。LLM 简直火出天际,各行各业都在蹭。听说最近 meta 开源的 llama3 模型可以轻松在普通 PC 上运行,这让我也忍不住来蹭一层。以下是使用 ollama 试玩 llama3 的一些记录。

什么是 llama

LLaMA(Large Language Model Meta AI)是Meta开发的大规模预训练语言模型,基于Transformer架构,具有强大的自然语言处理能力。它在文本生成、问答系统、机器翻译等任务中表现出色。LLaMA模型有多个规模,从几亿到上千亿参数,适用于不同的应用场景。用户可以通过开源平台如Hugging Face获取LLaMA模型,并根据需要进行微调。LLaMA的灵活性和可扩展性使其在自然语言处理领域具有广泛的应用前景。

什么是 ollama

Ollama是一款用于本地安装和管理大规模预训练语言模型的工具。它简化了模型的下载、安装和使用流程,支持多种流行的模型如GPT-4和llama。Ollama通过易于使用的命令行界面和API,帮助用户快速部署和运行自然语言处理任务。它还支持多GPU配置和模型微调,适应各种计算资源和应用需求。总之,Ollama为研究人员和开发者提供了一个高效、灵活的本地化大模型解决方案。

下载 ollama

ollama 官网提供了各种平台的安装包,那么这里选择 windows 系统的。以下是下载地址:
https://ollama.com/download

在 windows 上安装

在 windows 上安装那简直太简单了,一路 next 就行了。

安装成功后可以在命令行下执行

ollama -v


如果能成功打印出版本信息,那么说明你安装成功了。

下载模型并运行

安装好 ollama 之后我们需要把训练好的模型拉到本地,然后才能运行它。

查找 模型

ollama 提供了一个页面供用户查询可以使用的开源模型。

https://ollama.com/search?q=&p=1

可以看到主流的开源 LLM 几乎都能找到。什么 llama3 啊,phi3 啊,国产的 qwen2 啊。让我们点击 llama3 看看详情。

里面可以选模型的参数大小。这里我们选 8b 试一下。模型大小是 4.7 GB。复制右上角的命令并在命令行运行:

ollama run llama3:8b

程序会开始下载模型到本地。这里得夸一下,ollama 是不是在国内接了 CDN,这速度杠杆的,直接跑满了我的千兆网络。

对话

下载完成后命令行就会跳转到对话模型,等待你输入问题。随便先来一个吧。
Q:飞机为什么会飞?
A: balabala 一大堆,都是英文。

Q: what is SOLID principle?
A:

总结

到这,我们本地运行大模型基本上是初步成功了。简直超级无敌简单,属于有手就行。问题就是本地限制于PC的性能,回答的速度比较慢,大概一秒2-3个单词。CPU大概吃掉50%。当然如果你有 N 卡可能会好很多。内存倒是还好才吃了300多M。好了,下一次我们来试试 open-webui,把本地的模型搞的跟 chatGPT 一样。

【1】引言(完整代码在最后面)

在本文中,我们将介绍如何使用鸿蒙系统(HarmonyOS)开发一个简单的指南针应用。通过这个案例,你可以学习如何使用传感器服务、状态管理以及UI构建等基本技能。

【2】环境准备

电脑系统:windows 10

开发工具:DevEco Studio NEXT Beta1 Build Version: 5.0.3.806

工程版本:API 12

真机:Mate 60 Pro

语言:ArkTS、ArkUI

【3】算法分析

1. 角度差计算算法

计算当前角度与目标角度之间的差值,考虑了角度的周期性(0度和360度等效)。

private calculateAngleDifference(currentAngle: number, targetAngle: number): number {
    let diff = targetAngle - currentAngle;

    if (diff > 180) {
        diff -= 360; // 顺时针旋转超过180度,调整为负值
    } else if (diff < -180) {
        diff += 360; // 逆时针旋转超过180度,调整为正值
    }

    return diff;
}

2. 累计旋转角度算法

累计计算旋转角度,确保角度在0到360度之间。以便旋转动画能正确实现

private updateRotationAngle(angleDifference: number, newAngle: number): void {
    this.cumulativeRotation += angleDifference; // 累加旋转角度
    this.rotationAngle += angleDifference; // 更新当前旋转角度
    this.currentAngle = newAngle; // 更新当前传感器角度

    this.rotationAngle = (this.rotationAngle % 360 + 360) % 360; // 保持在0到360度之间
}

3. 方向计算算法

根据传感器角度计算当前方向,匹配角度范围对应的方向名称。

private calculateDirection(angle: number): string {
    for (const range of DIRECTION_RANGES) {
        if (angle >= range.min && angle < range.max) {
            return range.name; // 返回对应的方向名称
        }
    }
    return '未知方向'; // 如果角度不在任何范围内,返回未知方向
}

【完整代码】

import { sensor } from '@kit.SensorServiceKit'; // 导入传感器服务模块
import { BusinessError } from '@kit.BasicServicesKit'; // 导入业务错误处理模块

// 定义方向范围类
class DirectionRange {
  name: string = ''; // 方向名称
  min: number = 0; // 最小角度
  max: number = 0; // 最大角度
}

// 定义各个方向的范围
const DIRECTION_RANGES: DirectionRange[] = [
  { name: '北', min: 337.5, max: 360 },
  { name: '北', min: 0, max: 22.5 },
  { name: '东北', min: 22.5, max: 67.5 },
  { name: '东', min: 67.5, max: 112.5 },
  { name: '东南', min: 112.5, max: 157.5 },
  { name: '南', min: 157.5, max: 202.5 },
  { name: '西南', min: 202.5, max: 247.5 },
  { name: '西', min: 247.5, max: 292.5 },
  { name: '西北', min: 292.5, max: 337.5 }
];

// 定义指南针组件
@Entry
@Component
struct Compass {
  @State directionMessage: string = ''; // 当前方向的名称
  @State rotationAngle: number = 0; // 当前旋转角度
  @State currentAngle: number = 0; // 当前传感器角度
  @State cumulativeRotation: number = 0; // 累计旋转角度
  private threshold: number = 1; // 设置阈值,用于过滤小的旋转变化

  // 组件即将出现时调用
  aboutToAppear(): void {
    sensor.getSensorList((error: BusinessError) => {
      if (error) {
        console.error('获取传感器列表失败', error); // 如果获取传感器列表失败,打印错误信息
        return;
      }
      this.startOrientationUpdates(); // 开始监听传感器数据
    });
  }

  // 开始监听传感器的方位数据
  private startOrientationUpdates(): void {
    sensor.on(sensor.SensorId.ORIENTATION, (orientationData) => {
      const alpha = orientationData.alpha; // 获取当前的方位角
      this.directionMessage = this.calculateDirection(alpha); // 计算当前方向
      const angleDifference = this.calculateAngleDifference(this.currentAngle, alpha); // 计算角度差

      if (Math.abs(angleDifference) > this.threshold) { // 如果角度变化超过阈值
        this.updateRotationAngle(angleDifference, alpha); // 更新旋转角度
      }
    }, { interval: 10000000 }); // 设置传感器更新间隔,单位为纳秒,10000000表示1秒
  }

  // 计算两个角度之间的差异
  private calculateAngleDifference(currentAngle: number, targetAngle: number): number {
    let diff = targetAngle - currentAngle; // 计算角度差

    if (diff > 180) {
      diff -= 360; // 顺时针旋转超过180度,调整为负值
    } else if (diff < -180) {
      diff += 360; // 逆时针旋转超过180度,调整为正值
    }

    return diff; // 返回调整后的角度差
  }

  // 更新旋转角度
  private updateRotationAngle(angleDifference: number, newAngle: number): void {
    this.cumulativeRotation += angleDifference; // 累加旋转角度
    this.rotationAngle += angleDifference; // 更新当前旋转角度
    this.currentAngle = newAngle; // 更新当前传感器角度

    // 动画更新
    animateToImmediately({}, () => {
      this.rotationAngle = this.cumulativeRotation; // 将旋转角度设置为累计旋转角度
    });

    console.log(`累计旋转角度: ${this.cumulativeRotation}`); // 打印累计旋转角度
  }

  // 根据角度计算方向
  private calculateDirection(angle: number): string {
    for (const range of DIRECTION_RANGES) {
      if (angle >= range.min && angle < range.max) {
        return range.name; // 返回对应的方向名称
      }
    }
    return '未知方向'; // 如果角度不在任何范围内,返回未知方向
  }

  // 构建用户界面
  build() {
    Column({ space: 20 }) { // 创建一个列布局,设置间距为20
      Row({ space: 5 }) { // 创建一个行布局,设置间距为5
        Text(this.directionMessage) // 显示当前方向
          .layoutWeight(1) // 设置布局权重
          .textAlign(TextAlign.End) // 文本对齐方式
          .fontColor('#dedede') // 文本颜色
          .fontSize(50); // 文本大小
        Text(`${Math.floor(this.currentAngle)}°`) // 显示当前角度
          .layoutWeight(1) // 设置布局权重
          .textAlign(TextAlign.Start) // 文本对齐方式
          .fontColor('#dedede') // 文本颜色
          .fontSize(50); // 文本大小
      }.width('100%').margin({ top: 50 }); // 设置宽度和上边距

      Stack() { // 创建一个堆叠布局
        Stack() { // 内部堆叠布局
          Circle() // 创建一个圆形
            .width(250) // 设置宽度
            .height(250) // 设置高度
            .fillOpacity(0) // 设置填充透明度
            .strokeWidth(25) // 设置边框宽度
            .stroke('#f95941') // 设置边框颜色
            .strokeDashArray([1, 5]) // 设置边框虚线样式
            .strokeLineJoin(LineJoinStyle.Round); // 设置边框连接方式
          Text('北') // 创建一个文本,显示“北”
            .height('100%') // 设置高度
            .width(40) // 设置宽度
            .align(Alignment.Top) // 设置对齐方式
            .fontColor('#ff4f3f') // 设置文本颜色
            .rotate({ angle: 0 }) // 设置旋转角度
            .padding({ top: 80 }) // 设置内边距
            .textAlign(TextAlign.Center); // 设置文本对齐方式
          Text('东') // 创建一个文本,显示“东”
            .height('100%') // 设置高度
            .width(40) // 设置宽度
            .align(Alignment.Top) // 设置对齐方式
            .fontColor('#fcfdfd') // 设置文本颜色
            .rotate({ angle: 90 }) // 设置旋转角度
            .padding({ top: 80 }) // 设置内边距
            .textAlign(TextAlign.Center); // 设置文本对齐方式
          Text('南') // 创建一个文本,显示“南”
            .height('100%') // 设置高度
            .width(40) // 设置宽度
            .align(Alignment.Top) // 设置对齐方式
            .fontColor('#fcfdfd') // 设置文本颜色
            .rotate({ angle: 180 }) // 设置旋转角度
            .padding({ top: 80 }) // 设置内边距
            .textAlign(TextAlign.Center); // 设置文本对齐方式
          Text('西') // 创建一个文本,显示“西”
            .height('100%') // 设置高度
            .width(40) // 设置宽度
            .align(Alignment.Top) // 设置对齐方式
            .fontColor('#fcfdfd') // 设置文本颜色
            .rotate({ angle: 270 }) // 设置旋转角度
            .padding({ top: 80 }) // 设置内边距
            .textAlign(TextAlign.Center); // 设置文本对齐方式
        }
        .width('100%') // 设置宽度
        .height('100%') // 设置高度
        .borderRadius('50%') // 设置圆角
        .margin({ top: 50 }) // 设置上边距
        .rotate({ angle: -this.rotationAngle }) // 设置旋转角度
        .animation({}); // 设置动画效果

        Line() // 创建一个线条
          .width(5) // 设置宽度
          .height(40) // 设置高度
          .backgroundColor('#fdfffe') // 设置背景颜色
          .borderRadius('50%') // 设置圆角
          .margin({ bottom: 200 }); // 设置下边距
      }
      .width(300) // 设置宽度
      .height(300); // 设置高度
    }
    .height('100%') // 设置高度
    .width('100%') // 设置宽度
    .backgroundColor('#18181a'); // 设置背景颜色
  }
}

前言

JetBrains官方前段时间宣布重磅消息,其两款知名IDE应用
WebStorm和Rider现已面向社区开放,允许用户免费用于非商业用途。此举旨在支持学习、开源项目开发、内容创作及业余开发等活动。

Rider介绍

JetBrains Rider 是一款一体化 IDE,适合使用整个 .NET 技术堆栈以及参与游戏开发的开发者。它的强大功能集允许您开发各种面向 .NET、ASP.NET Core、MAUI 等框架或 Unity、Unreal Engine 或
Godot 等游戏引擎的应用程序。

商业用途与非商业用途对比

根据 Toolbox 非商业用途订阅协议中的定义
[1]
,商业产品是指有偿分发或提供、或者作为您的商业活动的一部分使用的产品。但某些类别被明确排除在这一定义之外。常见的非商业用例包括学习和自我教育、任何形式的内容创作、
开源代码和业余爱好开发。

跨平台支持

JetBrains Rider 是一款真正的跨平台 IDE,提供跨 Windows、macOS 和 Linux 的无缝开发体验。它还可以通过对 Android、iOS 和 Mac Catalyst 开发的额外支持扩展您的项目范围。

下载安装体验

代码质量提升

JetBrains Rider 可以简化开发流程并提高代码质量。

快速推进开发

Rider 适用于 C#、C++、F#、JS/TS 和其他语言的智能代码补全和代码模板将让您事半功倍。

强大的调试功能

JetBrains Rider 配备了用于本地和远程调试的强大工具,可以让您检查应用程序状态、控制执行流,以及实时对表达式求值。在预测模式下,Rider 的调试器可以主动识别潜在问题,且无需执行代码。

卓越单元测试

Rider 借助对 NUnit 和 xUnit.net 等主流 .NET 测试框架以及游戏引擎特定测试选择的支持来促进单元测试,允许在 IDE 中编写、执行和调试测试。

轻松的 NuGet 管理

得益于 IDE 对中央软件包管理 (CPM) 的支持及其直观的 NuGet 工具窗口,在 Rider 中管理 NuGet 软件包既简单又高效。

无缝版本控制

Rider 与 Git、
Perforce 和其他流行版本控制系统紧密集成,确保无缝适配一系列工作流和偏好。

.NET开发效率工具

本文已收录至.NET开发效率工具栏目中,欢迎订阅。

参考资料

https://www.jetbrains.com.cn/en-us/legal/docs/toolbox/license_non-commercial/

首先引入一个概念,什么是Java类加载器?

一句话总结:类加载器(class loader)用来加载 Java 类到 Java 虚拟机中。

官方总结:Java类加载器(英语:Java Classloader)是Java运行时环境(Java Runtime Environment)的一部分,负责动态加载Java类到Java虚拟机的内存空间中。类通常是按需加载,即第一次使用该类时才加载。由于有了类加载器,Java运行时系统不需要知道文件与文件系统。

类与类加载器

先来看一下JVM中默认的类加载器

分类

实现通过类的全限定名获取该类的二进制字节流的代码块叫做类加载器。

类加载器虽然只用于实现类的加载动作,但它在Java程序中起到的作用却远超类加载阶段

对于任意一个类,都必须由加载它的类加载器和这个类本身一起共同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。这句话可以表达得更通俗一些:比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个Class文件,被同一个Java虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等

为什么要有三个类加载器?一方面是分工,各自负责各自的区块,就如Application Class Loader主要负责加载用户之间开发的代码,另一方面为了实现委托模型。

启动类加载器

引导类加载器属于JVM的一部分,由C++代码实现。

引导类加载器负责加载<JAVA_HOME>\jre\lib路径下的核心类库,由于安全考虑只加载 包名 java、javax、sun开头的类。

public class Demo1 {
    public static void main(String[] args) {
        //Bootstrap 引导类加载器
        //打印为null,是因为Bootstrap是C++实现的。
        ClassLoader classLoader = Object.class.getClassLoader();
        System.out.println(classLoader);

        //查看引导类加载器会加载那些jar包
        URL[] urLs = Launcher.getBootstrapClassPath().getURLs();
        for (URL urL : urLs) {
            System.out.println(urL);
        }
    }

}

输出:
null
file:/D:/JavaSoftware/jdk1.8.0_131/jre/lib/resources.jar
file:/D:/JavaSoftware/jdk1.8.0_131/jre/lib/rt.jar
file:/D:/JavaSoftware/jdk1.8.0_131/jre/lib/sunrsasign.jar
file:/D:/JavaSoftware/jdk1.8.0_131/jre/lib/jsse.jar
file:/D:/JavaSoftware/jdk1.8.0_131/jre/lib/jce.jar
file:/D:/JavaSoftware/jdk1.8.0_131/jre/lib/charsets.jar
file:/D:/JavaSoftware/jdk1.8.0_131/jre/lib/jfr.jar
file:/D:/JavaSoftware/jdk1.8.0_131/jre/classes

拓展类加载器

全类名:sum.misc.Launch$ExtClassLoader,Java语言实现。

扩展类加载器的父加载器是Bootstrap启动类加载器 (注:不是继承关系)

扩展类加载器负责加载<JAVA_HOME>\jre\lib\ext目录下的类库。

import com.sun.javafx.webkit.WebPageClientImpl;

public class Demo1 {
    public static void main(String[] args) {
        //ext目录下的类,获取加载器
        ClassLoader classLoader = WebPageClientImpl.class.getClassLoader();
        System.out.println(classLoader);
    }
}

输出:
sun.misc.Launcher$ExtClassLoader@330bedb4

应用程序类加载器

全类名: sun.misc.Launcher$AppClassLoader

系统类加载器的父加载器是ExtClassLoader扩展类加载器(注: 不是继承关系)。

系统类加载器负责加载 classpath环境变量所指定的类库,是用户自定义类的默认类加载器。

public class Demo1 {
    public static void main(String[] args) {
        ClassLoader classLoader = Demo1.class.getClassLoader();
        System.out.println(classLoader);
    }

}

输出:
sun.misc.Launcher$AppClassLoader@18b4aac

类的加载方式

类加载有三种方式:

  1. 命令行启动应用时候由JVM初始化加载
  2. 通过Class.forName()方法动态加载
  3. 通过ClassLoader.loadClass()方法动态加载
package com.pdai.jvm.classloader;
public class loaderTest { 
        public static void main(String[] args) throws ClassNotFoundException { 
                ClassLoader loader = HelloWorld.class.getClassLoader(); 
                System.out.println(loader); 
                //使用ClassLoader.loadClass()来加载类,不会执行初始化块 
                loader.loadClass("Test2"); 
                //使用Class.forName()来加载类,默认会执行初始化块 
//                Class.forName("Test2"); 
                //使用Class.forName()来加载类,并指定ClassLoader,初始化时不执行静态块 
//                Class.forName("Test2", false, loader); 
        } 
}

public class Test2 { 
        static { 
                System.out.println("静态初始化块执行了!"); 
        } 
}

Class.forName()和ClassLoader.loadClass()的区别:

  • Class.forName(): 将类的.class文件加载到jvm中之外,还会对类进行解释,执行类中的static块;

  • ClassLoader.loadClass(): 只干一件事情,就是将.class文件加载到jvm中,不会执行static中的内容,只有在newInstance才会去执行static块。

  • Class.forName(name, initialize, loader)带参函数也可控制是否加载static块。并且只有调用了newInstance()方法采用调用构造函数,创建类的对象 。

JVM类加载机制

  • 全盘负责:当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入

  • 缓存机制:缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。这就是为什么修改了Class后,必须重启JVM,程序的修改才会生效

  • 双亲委派机制:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类。

一个应用程序总是由n多个类组成,Java程序启动时,并不是一次把所有的类全部加载后再运行,它总是先把保证程序运行的基础类一次性加载到JVM中,其它类等到JVM用到的时候再加载。

双亲委派模型

一个类加载器收到一个类的加载请求时,它首先不会自己尝试去加载它,而是把这个请求委派给父类加载器去完成,这样层层委派,因此所有的加载请求最终都会传送到顶层的启动类加载器中,只有当父类加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己去加载。

双亲委派模型的具体实现代码在 java.lang.ClassLoader 中,此类的 loadClass() 方法运行过程如下:先检查类是否已经加载过,如果没有则让父类加载器去加载。当父类加载器加载失败时抛出ClassNotFoundException ,此时尝试自己去加载。

源码

protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
        // 1.首先查找该类是否已经被该类加载器加载过了
        Class<?> c = findLoadedClass(name);
        //如果没有被加载过
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                if (parent != null) {
                    // 2. 有上级的话,委派上级 loadClass
                    c = parent.loadClass(name, false);
                } else {
                    // 3. 如果没有上级了(ExtClassLoader),则委派BootstrapClassLoader
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // ClassNotFoundException thrown if class not found
                // from the non-null parent class loader
                //捕获异常,但不做任何处理
            }

            if (c == null) {
                // 4. 每一层找不到,调用 findClass 方法(每个类加载器自己扩展)来加载
                long t1 = System.nanoTime();
                c = findClass(name);

                // 记录时间
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
                }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}

例如:

public class Demo1 {
    public static void main(String[] args) throws ClassNotFoundException {
        Class<?> aClass = Demo1.class.getClassLoader().loadClass("com.seven.jvm.F");
        System.out.println(aClass.getClassLoader());
    }
}

执行流程为:

  1. sun.misc.Launcher$AppClassLoader //1 处, 开始查看已加载的类,结果没有
  2. sun.misc.Launcher$AppClassLoader // 2 处,委派上级
  3. sun.misc.Launcher$ExtClassLoader // 1 处,查看已加载的类,结果没有
  4. sun.misc.Launcher$ExtClassLoader // 3 处,没有上级了,则委派 BootstrapClassLoader 查找
  5. BootstrapClassLoader 是在 JAVA_HOME/jre/lib 下找 F 这个类,显然没有,就会捕获异常,但是不处理
  6. sun.misc.Launcher$ExtClassLoader // 4 处,调用自己的 findClass 方法,是在JAVA_HOME/jre/lib/ext 下找 F 这个类,显然没有,回到 sun.misc.Launcher $AppClassLoader 的 // 2 处
  7. 继续执行到 sun.misc.Launcher$AppClassLoader // 4 处,调用它自己的 findClass 方法,在 classpath 下查找,找到了

双亲委派模型目的

可以防止内存中出现多份同样的字节码。如果没有双亲委派模型而是由各个类加载器自行加载的话,如果用户编写了一个 java.lang.Object 的同名类并放在 ClassPath 中,多个类加载器都去加载这个类到内存中,系统中将会出现多个不同的 Object 类,那么类之间的比较结果及类的唯一性将无法保证。

依赖传递原则

假设类C是由加载器L1定义加载的,那么类C中所依赖的其他类将会通过L1进行加载。

如下:假设Demo2是由L1定义加载的,那么类Demo2中所依赖的类F和接口IDemo将会通过L1进行加载。甚至还用到了String类,String也会由L1加载,但由于双亲委派模型,String类最终会由Bootstrap ClassLoader进行加载。(前提是这些依赖的类尚未加载)

import com.seven.jvm2.F;
import com.seven.jvm2.IDemo;

public class Demo2 implements IDemo {

    @Override
    public void run() {
        F f = new F();
       String s = f.toString().toLowerCase();
        System.out.println(s);
    }
}

打破双亲委派模型

什么时候需要打破双亲委派模型?比如类A已经有一个classA,恰好类B也有一个clasA 但是两者内容不一致,如果不打破双亲委派模型,那么类A只会加载一次

只要在加载类的时候,不按照UserCLASSlOADER->Application ClassLoader->Extension ClassLoader->Bootstrap ClassLoader的顺序来加载就算打破打破双亲委派模型了。比如自定义个ClassLoader,重写loadClass方法(不依照往上开始寻找类加载器),那就算是打破双亲委派机制了。

打破双亲委派模型有两种方式:

  1. 自定义一个类加载器的类,并覆盖抽象类java.lang.ClassL oader中loadClass..)方法,不再优先委派“父”加载器进行类加载。(比如Tomcat)

  2. 主动违背类加载器的依赖传递原则


    • 例如在一个BootstrapClassLoader加载的类中,又通过APPClassLoader来加载所依赖的其它类,这就打破了“双亲委派模型”中的层次结构,逆转了类之间的可见性。

    • 典型的是Java SPI机制,它在类ServiceLoader中,会使用线程上下文类加载器来逆向加载classpath中的第三方厂商提供的Service Provider类。(比如JDBC)

Tomcat

在Tomcat部署项目时,是把war包放到tomcat的webapp下,这就意味着一个tomcat可以运行多个Web应用程序。

假设现在有两个Web应用程序,它们都有一个类,叫User,并且它们的类全限定名都一样,比如都是com.yyy.User,但是他们的具体实现是不一样的。那么Tomcat如何保证它们不会冲突呢?

Tomcat给每个 Web 应用创建一个类加载器实例(WebAppClassLoader),该加载器重写了loadClass方法,优先加载当前应用目录下的类,如果当前找不到了,才一层一层往上找,这样就做到了Web应用层级的隔离。

但是并不是Web应用程序的所有依赖都需要隔离的,比如要用到Redis的话,Redis就可以再Web应用程序之间贡献,没必要每个Web应用程序每个都独自加载一份。因此Tomcat就在WebAppClassLoader上加个父加载器ShareClassLoader,如果WebAppClassLoader没有加载到这个类,就委托给ShareClassLoader去加载。(意思就类似于将需要共享的类放到一个共享目录下)

Web应用程序有类,但是Tomcat本身也有自己的类,为了隔绝这两个类,就用CatalinaClassLoader类加载器进行隔离,CatalinaClassLoader加载Tomcat本身的类

Tomcat与Web应用程序还有类需要共享,那就再用CommonClassLoader作为CatalinaClassLoader和ShareClassLoader的父类加载器,来加载他们之间的共享类

Tomcat加载结构图如下:

JDBC

实际上JDBC定义了接口,具体的实现类是由各个厂商进行实现的(比如MySQL)

类加载有个规则:如果一个类由类加载器A加载,那么这个类的依赖类也是由「相同的类加载器」加载。

而在用JDBC的时候,是使用DriverManager获取Connection的,DriverManager是在java.sql包下的,显然是由BootStrap类加载器进行装载的。当使用DriverManager.getConnection ()时,需要得到的一定是对应厂商(如Mysql)实现的类。这里在去获取Connection的时候,是使用「线程上下文加载器」去加载Connection的,线程上下文加载器会直接指定对应的加载器去加载。也就是说,在BootStrap类加载器利用「线程上下文加载器」指定了对应的类的加载器去加载

线程上下文加载器

Java 提供了很多服务提供者接口(Service Provider Interface,SPI),允许第三方为这些接口提供实现。常见的 SPI 有 JDBC 。

这些 SPI 的接口由 Java 核心库来提供,而这些 SPI 的实现代码则是作为 Java 应用所依赖的 jar 包被包含进类路径(CLASSPATH)里。SPI接口中的代码经常需要加载具体的实现类。那么问题来了,SPI的接口是Java核心库的一部分,是由启动类加载器来加载的;SPI的实现类是由系统类加载器来加载的。启动类加载器是无法找到 SPI 的实现类的,因为它只加载 Java 的核心库。它也不能委派给系统类加载器,因为它是系统类加载器的祖先类加载器。

线程上下文类加载器正好解决了这个问题。如果不做任何的设置,Java 应用的线程的上下文类加载器默认就是系统上下文类加载器。在 SPI 接口的代码中使用线程上下文类加载器,就可以成功的加载到 SPI 实现的类。线程上下文类加载器在很多 SPI 的实现中都会用到。

线程上下文加载器的一般使用模式(获取 - 使用 - 还原)

ClassLoader calssLoader = Thread.currentThread().getContextClassLoader();
 
try {
    //设置线程上下文类加载器为自定义的加载器
    Thread.currentThread.setContextClassLoader(targetTccl);
    myMethod(); //执行自定义的方法
} finally {
    //还原线程上下文类加载器
    Thread.currentThread().setContextClassLoader(classLoader);
}
源码实现
Connection conn = DriverManager.getConnection(url,username,password);

这就是普通的连接数据库的代码,可以直接获取数据库连接进行操作,这段代码没有了加载驱动的代码,那要怎么去确定使用哪个数据库连接的驱动呢?这里就涉及到使用Java的SPI扩展机制来查找相关驱动的东西了,关于驱动的查找其实都在DriverManager中,DriverManager是Java中的实现,用来获取数据库连接,在DriverManager中有一个静态代码块如下:

static {
    loadInitialDrivers();
    println("JDBC DriverManager initialized");
}

可以看到是加载实例化驱动的,接着看loadInitialDrivers方法:

private static void loadInitialDrivers() {
    String drivers;
    try {
        drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
            public String run() {
                return System.getProperty("jdbc.drivers");
            }
        });
    } catch (Exception ex) {
        drivers = null;
    }

    AccessController.doPrivileged(new PrivilegedAction<Void>() {
        public Void run() {
            //使用SPI的ServiceLoader来加载接口的实现
            ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
            Iterator<Driver> driversIterator = loadedDrivers.iterator();
            try{
                while(driversIterator.hasNext()) {
                    driversIterator.next();
                }
            } catch(Throwable t) {
            // Do nothing
            }
            return null;
        }
    });

    println("DriverManager.initialize: jdbc.drivers = " + drivers);

    if (drivers == null || drivers.equals("")) {
        return;
    }
    String[] driversList = drivers.split(":");
    println("number of Drivers:" + driversList.length);
    for (String aDriver : driversList) {
        try {
            println("DriverManager.Initialize: loading " + aDriver);
            Class.forName(aDriver, true,
                    ClassLoader.getSystemClassLoader());
        } catch (Exception ex) {
            println("DriverManager.Initialize: load failed: " + ex);
        }
    }
}

上面的代码主要步骤是:

  1. 从系统变量中获取有关驱动的定义。
  2. 使用SPI来获取驱动的实现。
  3. 遍历使用SPI获取到的具体实现,实例化各个实现类。
  4. 根据第一步获取到的驱动列表来实例化具体实现类。

主要关注2,3步,这两步是SPI的用法,首先看第二步,使用SPI来获取驱动的实现,对应的代码是:

ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);

这里没有去META-INF/services目录下查找配置文件,也没有加载具体实现类,做的事情就是封装了我们的接口类型和类加载器,并初始化了一个迭代器。

接着看第三步,遍历使用SPI获取到的具体实现,实例化各个实现类,对应的代码如下:

//获取迭代器
Iterator<Driver> driversIterator = loadedDrivers.iterator();
//遍历所有的驱动实现
while(driversIterator.hasNext()) {
    driversIterator.next();
}

在遍历的时候,首先调用driversIterator.hasNext()方法,这里会搜索classpath下以及jar包中所有的META-INF/services目录下的java.sql.Driver文件,并找到文件中的实现类的名字,此时并没有实例化具体的实现类(ServiceLoader具体的源码实现在下面)。

然后是调用driversIterator.next();方法,此时就会根据驱动名字具体实例化各个实现类了。现在驱动就被找到并实例化了。

可以看下截图,我在测试项目中添加了两个jar包,mysql-connector-java-6.0.6.jar和postgresql-42.0.0.0.jar,跟踪到DriverManager中之后:

可以看到此时迭代器中有两个驱动,mysql和postgresql的都被加载了。

自定义类加载器加载 java.lang.String

很多人都有个误区:双亲委派机制不能被打破,不能使用自定义类加载器加载java.lang.String

但是事实上并不是,只要重写ClassLoader的loadClass()方法,就能打破了。

import java.io.File;
import java.net.URL;
import java.net.URLClassLoader;

public class MyClassLoader extends URLClassLoader {

    public MyClassLoader(URL[] urls) {
        super(urls);
    }
    
    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        //只对MyClassLoader和String使用自定义的加载,其他的还是走双亲委派
        if(name.equals("MyClassLoader") || name.equals("java.lang.String")) {
            return super.findClass(name);
        } else {
            return getParent().loadClass(name);
        }
    }

    public static void main(String[] args) throws Exception {
        //urls指定自定义类加载器的加载路径
        URL url = new File("J:/apps/demo/target/classes/").toURI().toURL();
        URL url3 = new File("C:/Program Files/Java/jdk1.8.0_191/jre/lib/rt.jar").toURI().toURL();
        URL[] urls = {
                url
                , url3
        };
        MyClassLoader myClassLoader = new MyClassLoader(urls);

        Class<?> c1 = MyClassLoader.class.getClassLoader().loadClass("MyClassLoader");
        Class<?> c2 = myClassLoader.loadClass("MyClassLoader");
        System.out.println(c1 == c2); //false
        System.out.println(c1.getClassLoader()); //AppClassLoader
        System.out.println(c2.getClassLoader()); //MyClassLoader

        System.out.println(myClassLoader.loadClass("java.lang.String")); //Exception 
    }

}

加载同一个类MyClassLoader,使用的类加载器不同,说明这里是打破了双亲委派机制的,但是尝试加载String类的时候报错了

看代码是ClassLoader类里面的限制,
只要加载java开头的包就会报错
。所以真正原因是
JVM安全机制
,并不是因为双亲委派。

那么既然是ClassLoader里面的代码做的限制,那把ClassLoader.class修改了不就好了吗。

写了个java.lang.ClassLoader,把preDefineClass()方法里那段if直接删掉,再用编译后的class替换rt.jar里面的,直接通过命令jar uvf rt.jar java/lang/ClassLoader/class即可。

不过事与愿违,修改之后还是报错:

Exception in thread "main" java.lang.SecurityException: Prohibited package name: java.lang
    at java.lang.ClassLoader.defineClass1(Native Method)
    at java.lang.ClassLoader.defineClass(ClassLoader.java:756)
    at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
    at java.net.URLClassLoader.defineClass(URLClassLoader.java:468)
    at java.net.URLClassLoader.access$100(URLClassLoader.java:74)
    at java.net.URLClassLoader$1.run(URLClassLoader.java:369)
    at java.net.URLClassLoader$1.run(URLClassLoader.java:363)
    at java.security.AccessController.doPrivileged(Native Method)
    at java.net.URLClassLoader.findClass(URLClassLoader.java:362)
    at com.example.demo.mini.test.MyClassLoader.loadClass(MyClassLoader.java:17)
    at com.example.demo.mini.test.MyClassLoader.main(MyClassLoader.java:31)

仔细看报错和之前的不一样了,这次是native方法报错了。这就比较难整了,看来要自己重新编译个JVM才行了。理论上来说,编译JVM的时候把校验的代码去掉就行了。

结论:
自定义类加载器加载java.lang.String,必须修改jdk的源码,自己重新编译个JVM才行

总结

  1. JDK中有三个默认类加载器:AppClassLoader、ExtClassLoader、BootStrapClassLoader。AppClassLoader的父加载器为Ext ClassLoader、Ext ClassLoader的父加载器为BootStrap ClassLoader。

  2. 什么是双亲委派机制:加载器在加载过程中,先把类交由父加载器进行加载,父加载器没找到才由自身加载。

  3. 双亲委派机制目的:为了防止内存中存在多份同样的字节码(安全)

  4. 依赖传递原则:如果一个类由类加载器A加载,那么这个类的依赖类也是由「相同的类加载器」加载。

  5. 什么时候需要打破双亲委派模型?比如类A已经有一个classA,恰好类B也有一个clasA 但是两者内容不一致,如果不打破双亲委派模型,那么类A只会加载一次

  6. 如何打破双亲委派机制:


    • 自定义ClassLoader,重写loadClass方法(只要不依次往上交给父加载器进行加载,就算是打破双亲委派机制)(Tomcat)

    • 主动违背类加载器的依赖传递原则(JDBC)

  7. 打破双亲委派机制案例:Tomcat


    • 为了Web应用程序类之间隔离,为每个应用程序创建WebAppClassLoader类加载器

    • 为了Web应用程序类之间共享,把ShareClassLoader作为WebAppClassLoader的父类加载器,如果WebAppClassLoader加载器找不到,则尝试用ShareClassLoader进行加载

    • 为了Tomcat本身与Web应用程序类隔离,用CatalinaClassLoader类加载器进行隔离,CatalinaClassLoader加载Tomcat本身的类

    • 为了Tomcat与Web应用程序类共享,用CommonClassLoader作为CatalinaClassLoader和ShareClassLoader的父类加载器

  8. 线程上下文加载器:由于类加载的规则,很可能导致父加载器加载时依赖子加载器的类,导致无法加载成功(BootStrap ClassLoader无法加载第三方库的类),所以存在「线程上下文加载器」来进行加载。

  9. 打破双亲委派机制案例:JDBC


    • JDBC的接口的具体的实现类是由各个厂商进行实现的(比如MySQL),因此在用JDBC的时候,需要得到厂商实现的类。这里就使用「线程上下文加载器」,线程上下文加载器直接指定对应的加载器去加载对应厂商的具体的实现类。
  10. 自定义类加载器加载java.lang.String,必须修改jdk的源码,自己重新编译个JVM才行。

面试题专栏

Java面试题专栏
已上线,欢迎访问。

  • 如果你不知道简历怎么写,简历项目不知道怎么包装;
  • 如果简历中有些内容你不知道该不该写上去;
  • 如果有些综合性问题你不知道怎么答;

那么可以私信我,我会尽我所能帮助你。

liwen01 2024.10.27

前言

无线 WiFi 的优点是方便、灵活,可以接入各种设备。缺点就是信号容易被干扰、信号覆盖范围有限。下面几个问题应该很多人都有遇到过:

  1. 为何很多洗手间的 WiFi 信号都不太好?

  2. 市面上的穿墙路由器真的就比其它路由器效果好么?

  3. 为何有时候 WiFi 信号强度很好,但网速却很慢?

  4. 如果房间比较大,需要怎样才能实现 WiFi 全屋覆盖?

  5. 如果是独栋多层的楼房,又要如何组建 WiFi 网络?

  6. 商场、公司、学校等人员密集场所,它们的 WiFi 又是如何组建?

  7. 什么是 WiFi 漫游、它有什么好处、又要如何实现 WiFi 漫游功能?

  8. 什么是胖 AP,什么是廋 AP?AC+AP 又是什么?

因为企业 WiFi 组网比较复杂,这里主要介绍家庭无线 WiFi 组网方案,主要包括:
路由器的串并联、WiFi 信号放大器、mesh 组网、AC+AP 模式组网
。它们各有优缺点又各有适合的应用场景。

(一) WiFi 漫游

WiFi漫游 (WiFi Roaming) 指的是当一个移动设备 (如智能手机、平板、本电脑等) 在一个由多个无线接入点 (AP) 覆盖的网络中移动时,设备能够无缝切换到信号更强或更合适的接入点,而不中断网络连接或影响应用程序的运行。

比如在大的商场、写字楼、图书馆等环境,我们拿着手机,从 AP1 所在区域,移动到 AP2、再到 PA3 这个区域。

(1) 无漫游场景

在没有漫游功能的时候,当跨区域时,只有等一个 AP 区域的信号弱到断开连接之后,才会主动去连接另外一个区域的 AP,用户可以明显地感觉到手机网络变慢变卡,最后断网,且要等上几秒之后才能恢复。

(2) 有漫游场景

在 AP1 ~ AP3 是支持漫游功能时,当要跨区域的时候,手机能够根据 AP 之间的信号强度、信噪比、AP 负载、移动数据和位置变化去自动切换所需要连接的 AP,效果就是用户一般感觉不到明显的网络卡顿,APP 的数据也不会中断。

(3) 协议支持

要实现 WiFi 漫游功能,需要 AP 和用户设备同时都支持 802.11k/v/r 协议

  • 802.11k
    :无线资源测量协议,可帮助终端快速搜索附近可作为漫游目标的 AP。
  • 802.11v
    :无线网络管理协议,用来解决AP之间的负荷均衡,以及终端节电等功能。
  • 802.11r
    :快速漫游协议,用于加速手机或者电脑在漫游时的认证流程。

要实现 WiFi 的漫游,还需要组建合适的网络。

(二) 普通家庭 WiFi 组网

(1) 常规 WiFi 使用

现在大部分家庭使用 WiFi 的模式是外面通过光纤接入到屋内,先接入光猫,这一部分主要是由运营商提供,比如国内的联通、电信、移动。

有些光猫也兼备路由器的功能,但是它的接口和接入的设备有限,整体性能也比较弱,而且很多家庭的光猫是放置在弱电箱里,所以整体信号不太好。

目前大部分人都是从光猫接根网线,接到一个路由器上,日常使用 WiFi 都是通过这个路由器来实现。

上图这种房间布局是比较常见的,一般将路由器放置在客厅电视柜上,为所有房间提供无线 WiFi 网络。

当把路由器放在客厅电视柜上的时候,客厅、餐厅、厨房可以享受到最佳的WiFi信号,远离客厅的书房、主卧、主卧洗手间因为中间隔了墙壁,WiFi 信号会被衰减,所以整体信号会比较弱。

为了使主卧和书房都有比较好的 WiFi 使用体验,如果去市面上购买一个穿墙路由器,能否解问题呢?

使用穿墙路由器实际效果正常不会很理想,市面上的穿墙路由器只不过是一个噱头。因为在中国《微功率短距离无线电发射设备目录和技术要求》有规定

  • 2.4GHz频段(2400–2483.5 MHz)
    :最大发射功率限制为 100 mW(20 dBm),等效全向辐射功率(EIRP)为 20 dBm。
  • 5GHz频段(5150–5350 MHz)
    :室内使用最大发射功率限制为 200 mW(23 dBm)。
  • 5GHz频段(5725–5850 MHz)
    :最大发射功率限制为 25 mW(14 dBm)。

在其它国家和地区也有相应的限制,最大发射功率被限制,不管路由器有几根天线,它的穿墙能力都不可能有显著提升,除非它违反规定增强了发射功率。

具体原因可以查看之前的文章《
wifi基础(一):无线电波与WIFI信号干扰、衰减

(2) 有线路由

对于上面这种户型,如果要使书房、主卧、主卧洗手间都有良好的 WiFi 信号,可以使用路由器串并联的方式来解决这个问题。

路由器串并联可以是通过有线的方式来串并联,也可以通过无线的方式来实现。

有线的方式信号会稳定很多,但是需要布置网线,如果房间之前有布网线推荐使用有线路由的方式。

有线路由的方式对路由器没有特殊的要求,任意两个路由器都可以实现。如果家里有多个路由器,可以将性能较好的作为主路由器,性能交差点的最为副路由器。

  • 将光猫出来的网线接到路由器1的WAN口,常规方式设置路由器上网。
  • 将路由器1任意一个LAN口,接到路由器2的WAN口,也是常规方式设置路由器上网。

这样就实现了两个路由器的串联,它们各自独立分配 IP,一般它们处于不同网段。

这种方式,就算路由器1与路由器2设置相同的 WiFi 名字和密码,它们也不能实现漫游功能。

(3) 有线中继

两个路由器串并联,通过不同的接线,可以实现不同的模式。如果主路由器1 LAN 口接到副路由器2的 LAN 口,这种模式需要做一些特殊的设置。

  • 将主路由器1 LAN 口接到副路由器2的 LAN 口
  • 将副路由器2 的 DHCP 功能关闭
  • 到路由器2 的 LAN 口设置处设置 LAN 口 IP,该 IP 与路由器1同一个网段,且不与路由器1的其它设备相冲突
  • 路由器2无线设置按正常设置就可以

这种模式,路由器1与路由器2的设备是处于同一网段,因为两个路由器的IP分配工作都是由路由器1来实现。同样的,它们也不支持漫游功能。

(4) WiFi 串并联

如果家里有多个闲置的路由器,选一个性能好的做主路由器,可以同时串并联多个路由器来解决 WiFi 信号不好的问题。

这种方式的限制就是需要布网线,优点就是对路由器的要求低,且可以各种不同品牌的路由器同时使用,可以将家里闲置的路由器很好地利用上。

(5) 无线桥接

WDS(Wireless Distribution System,无线分布式系统)是一种允许多个无线接入点(AP,Access Point)通过无线方式相互连接的技术,目的是扩展无线网络的覆盖范围。通过 WDS,多个路由器或 AP 可以彼此无线桥接,而不需要通过网线连接。

这种方式是在路由器2中去设置 WDS 功能,根据设置引导去连接路由器1。

这种方式的好处就是可以不用布线,缺点就是信号容易被干扰,效果没有有线路由的好。

对于最开始的房屋布局,我们应该要将路由器2放置到洗手间外面才能实现最佳的效果。因为路由器2 所有对外的所有数据都要通过路由器1传输出去。但因为它们是通过无线连接的,所以路由器2与路由器1间的信号不能太差。

如果将路由器放置到书房或是主卧里面,就会出现路由器 WiFi 信号强度很好,但是网络数据很慢的情况,主要原因是路由器2与路由器1间信号不好通信丢包严重导致速率严重下降。

(6) WiFi 信号放大器

WiFi 信号放大器,也叫 WiFi 扩展器或 WiFi 中继器,是一种通过接收现有 WiFi 信号并将其重新广播来扩展 WiFi 覆盖范围的设备。它能够解决家中或办公区域中 WiFi 信号弱、死角等问题,确保无线网络覆盖更广的区域。

它的工作原理是:

  • 信号接收
    :放大器通过内置的无线网卡,接收来自主路由器的 WiFi 信号。

  • 信号放大
    :放大器将接收到的 WiF i信号进行放大或重复(中继),以增强信号的强度和覆盖范围。

  • 信号转发
    :放大后的信号再次通过放大器的无线电广播出去,覆盖到原本信号无法到达的区域。

整体而言,使用 WiFi 信号放大器的效果并没有使用多个路由器中继的效果好,但一般 WiFi 信号放大器相比于路由器体积会小些,而且价格要便宜。

(三) 胖瘦 AP

(1) 胖 AP

上面我们介绍的,也是我们日常比较常使用的路由器,它都是属于胖 AP(Fat AP)。

胖 AP 它是一种独立工作的无线接入点,它具备较为完整的网络管理和控制功能。它可以自行进行认证、加密、管理无线信道和带宽分配,并执行许多其它功能,因此它可以在不依赖外部设备的情况下独立提供无线网络服务。

它的优点是:
独立性强,适用与小型网络

它的缺点是:
配置相对复杂
。对于复杂场景,比如大型写字楼,需要在不同位置放置很多个 AP,网络管理员不太可能每个 AP 去独立配置管理,所以就出现了瘦 AP(Thin AP)。

(2) 瘦 AP

瘦 AP 是一种功能简化的无线接入点,主要依赖于一个集中管理的设备——通常是无线局域网控制器(WLC)。瘦 AP 本身只负责基本的无线信号转发功能,而所有的复杂管理和控制功能(如认证、加密、频谱管理等)都交由 WLC 来处理。

瘦AP的特点是:

  • 集中管理
    :通过无线控制器统一管理多个瘦 AP,使得大规模部署更加容易,并且可以进行集中配置和故障排查。

  • 简化设备
    :瘦 AP 硬件相对简单,因为不需要集成完整的管理功能,从而降低了每个 AP 的成本。

  • 适用场景
    :瘦 AP 通常应用于大型企业、校园或高密度无线网络环境中,在这些场景下,多个 AP 的管理通过控制器集中进行,便于维护和扩展

(四) 大范围 WiFi 网络组网

如果对上网体验要求高,且要实现 WiFi 漫游功能,可以使用 mesh 组网方式或是 AC+AP 模式。

WiFi 漫游功能,可以让你在房间、楼层间移动的时候实现 WiFi 自动切换,用户基本上无法感知到网络的切换,网络也不会中断。对于家庭使用,mesh组网是个不错的选择。

(1) mesh 组网

WiFi 的 mesh 组网通过多个无线节点(通常是 mesh 路由器或 AP )相互连接,形成一个自我修复的网状网络,使得每个节点都能与其他节点通信并动态调整网络路径,以优化信号覆盖和流量传输。

(a) WiFi mesh 组网的核心概念

多跳传输
:在 mesh 网络中,数据可以通过多个节点传递到目标设备。这种方式有效地扩展了 WiFi 信号的覆盖范围。即使某个节点离主路由器较远,仍可以通过其他中继节点进行数据传输。

自我修复(Self-Healing)
:如果某个节点失效或断开连接,mesh 网络会自动寻找新的路径,使网络保持正常运行。这种冗余性使得网络更加稳定和可靠。

节点协作
:所有的 mesh 节点(mesh AP 或路由器)相互通信,并能够动态调整信号传输的路径和信号强度,以确保网络性能最佳化。这种协作机制可以减少网络拥堵并优化流量。

无缝漫游
:由于 mesh 网络中的节点都属于同一个网络,用户设备可以在各个节点之间无缝漫游,而不会中断网络连接。这对于覆盖大范围的家庭、办公楼、酒店等场景尤为重要。

(b) WiFi mesh 组网的结构

主节点(Primary Node/Gateway Node)
:这是连接互联网的主路由器或接入点,通常位于用户的互联网入口处。所有的网络流量最终都会经过主节点进行互联网通信。

从节点(Secondary Nodes/Child Nodes)
:这些节点与主节点或其他从节点连接,用于扩展网络覆盖。每个从节点都能与多个其他节点通信,并动态调整网络路径以优化连接。

设备通信
:设备(如手机、电脑等)可以与任意一个距离较近的节点连接,而不必强制连接到主节点,从而保证稳定和快速的连接。

(c) mesh 组网的优势

广覆盖
:通过多个节点扩展网络,消除信号盲区。

无缝漫游
:设备可在各节点间自由切换,无断线。

自我修复
:节点失效时,网络会自动调整路径,保证稳定性。

易扩展
:添加新节点简单,适合大范围网络覆盖。

稳定性能
:动态调整信道和路径,优化网络流量。

(d) mesh 组网的缺点

成本较高
:设备价格昂贵,尤其是需要多个节点时。

带宽下降
:多跳传输会导致带宽逐步降低。

延迟增加
:多节点传输会增加网络延迟。

配置复杂
:需要精心规划节点布局,且企业级网络配置复杂。

兼容性问题
:不同品牌的 mesh 系统可能不兼容。

mesh 可以是有线组网也可以是无线组网,有线模式需要拉网布线,无线模式整体性能较差。

mesh 组网需要给路由器独立供电并放置在一个遮挡较少的位置,多少也有些影响美观。但是如果换成 AC+AP 的模式就不存在这个问题了。

另外需要特别注意的是,
各品牌商的路由器之间 mesh 组网可能会存在不兼容的情况,所以最好是使用同一个品牌的 mesh 路由器来组网

(2) AC + AP 模式

在 WiFi 组网中,AC(Access Controller)+AP(Access Point)主要用于大型或企业级的无线网络环境。这种模式通常由一个或多个AP和一个AC组成,目的是集中管理和优化无线网络。

(a) AC(Access Controller)
AC 是无线网络的集中控制器,负责管理和配置所有接入点(AP)。它协调AP之间的工作,进行负载均衡、无线信道管理、认证、接入控制等。

可以简单理解为:这里的 AC 就是上面介绍的路由器,AP 就是路由器的天线,AP 负责无线信号的收发,AC 负责无线的管理。变相的把 WiFi 天线布置到房间的各个位置上。

(b) AC 主要功能有

集中管理
:通过AC,可以在一个地方配置和管理所有的 AP,简化网络管理。

负载均衡
:AC 可以监控 AP 的负载情况,并在用户移动时将其重新分配到负载较低的 AP。

无线信道管理
:AC 优化无线信道的使用,减少干扰,提高网络效率。

安全管理
:进行集中化的认证和加密管理,提高网络安全性。

(c) AP (Access Point)

AP 是无线网络的接入点,负责将无线信号转换为有线信号,反之亦然,允许无线设备连接到网络。

(d) AP 主要功能有

无线覆盖
:AP 提供无线信号覆盖,支持无线设备的接入。

数据传输
:AP 将无线设备的数据传输到有线网络,或者将有线网络的数据传输到无线设备。

信号强度调节
:AP 可以调整其发射功率,优化无线覆盖范围。

(e) AC+AP模式的优势

高效管理
:通过集中控制和管理,简化了无线网络的维护和配置工作。

灵活性
:可以根据实际需要扩展 AP 的数量,增加无线覆盖范围。

优化性能
:通过 AC 的干预和管理,提升了网络的性能和稳定性。

增强安全
:集中化的安全策略和认证机制提升了整体网络的安全性。

AP 有面板式和吸顶式两类,家用一般是使用面板式,可以直接嵌入墙体,跟插座一样。吸顶式的多用在写字楼等场所,直接放置在天花板处,整体比较美观。

另外,这类 AP 一般都支持 POE 供电,也就是可以直接使用网线进行供电,少了另外拉电的麻烦。

整体而言,AP 的性能会比 mesh 组网稍微差一点点,如果是家庭或是小范围使用,推荐使用 mesh 组网,mesh 相对 AC+AP来说价格也便宜点。

如果是写字楼,学校等需要大面积覆盖的场景,还是推荐使用AC+AP的模式,方便控制和管理。

结尾

上面内容简单地介绍了下目前家庭比较常用的 WiFi 组网方案,这些方案具体的使用配置,可以参考具体品牌商设备上的介绍。下一篇将介绍 WiFi 认证与加密相关基础知识。

上面内容,如有错误,欢迎评论区提示指出,不胜感激。

------------------End------------------
如需获取更多内容
请关注 liwen01 公众号