家庭网络设计方案:从理想到实践的两年演进

前言 早在毕业后的租房时期,我就时常期待拥有自己的家后要如何改造家庭网络。每次看到网上博主分享内网万兆传输、服务器机柜、NAS 阵列等内容,都不禁心生羡慕。2023年新家入住后,我对家庭网络的改造断断续续持续了两年。到了2025年,终于是时候对这段历程做一些总结了。 这篇文章将重点回顾这两年来家庭网络结构的演进过程及背后的思考。 房屋结构和网络需求 房屋为精装交付,出于成本考虑,入住时未对结构进行大幅改造(这也为后续网络改造埋下了一些隐患😭)。简略结构如下: 入住前暴露的主要问题有以下几点: 弱电箱嵌入在进门鞋柜内,空间非常有限,难以容纳较多设备,散热也较差。 网口数量不足,无线AP只能放置在客厅,书房存在 WiFi死角。 预埋网线为超五类,虽然短距离可支持万兆,但稳定性不佳。 我对家庭网络能力的主要需求如下: 需求 必要性 备注 全屋透明代理 ⭐⭐⭐⭐⭐ 满足全屋代理的同时,需要过滤PT流量/大陆域名请求等 内网2.5G速率 ⭐⭐⭐⭐⭐ 综合考虑,部署万兆的成本过高,日常使用2.5G已够使用 内网穿透 ⭐⭐⭐⭐ 游戏联机/家庭相册共享需要 支持内网私有域名 ⭐⭐⭐⭐ 简化日常内网部署应用的使用 网络监控/恢复能力 ⭐⭐⭐ 提升故障问题发现率 智能家居内网部署 ⭐⭐ 提升响应速度并支持离线使用 隐私设备子网隔离 ⭐⭐ 摄像头、传感器类iot设备屏蔽公网 容灾能力 ⭐ 减少故障发生后的恢复时间 基于以上的需求,得到以下的初步结论 由于需要全屋透明代理,故需要使用软路由做主路由的方案,这里我选用OpenWrt作为我的软路由系统。(个人不喜欢旁路由模式,不够透明也不够旁🐶) 由于需要全屋2.5G速率,硬件上交换机/AP/设备网口均需要支持2.5G协商速率。(被只支持1G和10G的万兆卡坑过) 由于需要内网穿透+私有域名等需求,需要软路由系统有相关的功能支持.(关注版本功能) 由于需要智能家居内网部署,购买设备时需要关注是否支持 Zigbee / Matter 等协议。 2023年:从零搭建,先跑起来 物理拓扑 2023年的主题是建设,在入住后不久,我便设计了如下的网络拓扑: 可以看到这里用了不太常见的单臂路由作为主路由连接方案,并不是因为软路由只有一个网口,纯粹是当时考虑如果软路由放在弱电箱内散热不太好。 我的主要网络设备如下: 软路由:零刻EQ12(n100/16G) 交换机:TP-LINK网管交换机 + 水星非网管交换机 无线ap:小米AX7000 服务器:X11SCA-F + E2144G + 64G ECC 硬件配置上普普通通,甚至有些富裕。当然,在闲鱼淘的交换机还是带起了后续的一些变化(笑 系统架构 系统拓扑如下图所示 软路由 我在ESXi上虚拟化运行OpenWrt,主要出于以下考虑: ...

August 9, 2025 · 2 min · 421 words · luyanliang

Play framework源码解析 Part3:Play的初始化与启动

在上一篇中,我们分析了play的2种启动方式,这一篇,我们来看看Play类的初始化过程 Play类 无论是Server还是ServletWrapper方式运行,在他们的入口中都会运行Play.init()来对Play类进行初始化。那在解析初始化之前,我们先来看看Play类是做什么的,它里面有什么重要的方法。 首先要明确的一点是,Play类是整个Play framework框架的管理、配置中心,它存放了大部分框架需要的成员变量,例如id,配置信息,所有加载的class,使用的插件管理器等等。下图就是Play类中的方法列表。 这其中加注释的几个方法是比较重要的,我们下面便来从init开始一点点剖析Play类中的各个方法。 Play的初始化 public static void init(File root, String id) { // Simple things Play.id = id; Play.started = false; Play.applicationPath = root; // 加载所有 play.static 中的记录的类 initStaticStuff(); //猜测play framework的路径 guessFrameworkPath(); // 读取配置文件 readConfiguration(); Play.classes = new ApplicationClasses(); // 初始化日志 Logger.init(); String logLevel = configuration.getProperty("application.log", "INFO"); //only override log-level if Logger was not configured manually if( !Logger.configuredManually) { Logger.setUp(logLevel); } Logger.recordCaller = Boolean.parseBoolean(configuration.getProperty("application.log.recordCaller", "false")); Logger.info("Starting %s", root.getAbsolutePath()); //设置临时文件夹 if (configuration.getProperty("play.tmp", "tmp").equals("none")) { tmpDir = null; Logger.debug("No tmp folder will be used (play.tmp is set to none)"); } else { tmpDir = new File(configuration.getProperty("play.tmp", "tmp")); if (!tmpDir.isAbsolute()) { tmpDir = new File(applicationPath, tmpDir.getPath()); } if (Logger.isTraceEnabled()) { Logger.trace("Using %s as tmp dir", Play.tmpDir); } if (!tmpDir.exists()) { try { if (readOnlyTmp) { throw new Exception("ReadOnly tmp"); } tmpDir.mkdirs(); } catch (Throwable e) { tmpDir = null; Logger.warn("No tmp folder will be used (cannot create the tmp dir)"); } } } // 设置运行模式 try { mode = Mode.valueOf(configuration.getProperty("application.mode", "DEV").toUpperCase()); } catch (IllegalArgumentException e) { Logger.error("Illegal mode '%s', use either prod or dev", configuration.getProperty("application.mode")); fatalServerErrorOccurred(); } if (usePrecompiled || forceProd) { mode = Mode.PROD; } // 获取http使用路径 ctxPath = configuration.getProperty("http.path", ctxPath); // 设置文件路径 VirtualFile appRoot = VirtualFile.open(applicationPath); roots.add(appRoot); javaPath = new CopyOnWriteArrayList<VirtualFile>(); javaPath.add(appRoot.child("app")); javaPath.add(appRoot.child("conf")); // 设置模板路径 if (appRoot.child("app/views").exists()) { templatesPath = new ArrayList<VirtualFile>(2); templatesPath.add(appRoot.child("app/views")); } else { templatesPath = new ArrayList<VirtualFile>(1); } // 设置路由文件 routes = appRoot.child("conf/routes"); // 设置模块路径 modulesRoutes = new HashMap<String, VirtualFile>(16); // 加载模块 loadModules(); // 模板路径中加入框架自带的模板文件 templatesPath.add(VirtualFile.open(new File(frameworkPath, "framework/templates"))); // 初始化classloader classloader = new ApplicationClassloader(); // Fix ctxPath if ("/".equals(Play.ctxPath)) { Play.ctxPath = ""; } // 设置cookie域名 Http.Cookie.defaultDomain = configuration.getProperty("application.defaultCookieDomain", null); if (Http.Cookie.defaultDomain!=null) { Logger.info("Using default cookie domain: " + Http.Cookie.defaultDomain); } // 加载插件 pluginCollection.loadPlugins(); // 如果是prod直接启动 if (mode == Mode.PROD || System.getProperty("precompile") != null) { mode = Mode.PROD; //预编译 if (preCompile() && System.getProperty("precompile") == null) { start(); } else { return; } } else { Logger.warn("You're running Play! in DEV mode"); } pluginCollection.onApplicationReady(); Play.initialized = true; } 如上面的代码所示,初始化过程主要的顺序为: ...

January 20, 2018 · 8 min · 1547 words · luyanliang

Play framework源码解析 Part2:Server与ServletWrapper

在上一节中我们剖析了Play framework的启动原理,很容易就能发现Play framework的启动主入口在play.server.Server中,在本节,我们来一起看看Server类中主要发生了什么。 Server类 既然是程序运行的主入口,那么必然是由main方法进入的,Server类中的main方法十分简单。源码如下: public static void main(String[] args) throws Exception { File root = new File(System.getProperty("application.path")); //获取参数中的precompiled if (System.getProperty("precompiled", "false").equals("true")) { Play.usePrecompiled = true; } //获取参数中的writepid if (System.getProperty("writepid", "false").equals("true")) { //这个方法的作用是检查当前目录下是否存在server.pid文件,若存在表明当前已有程序在运行 writePID(root); } //Play类的初始化 Play.init(root, System.getProperty("play.id", "")); if (System.getProperty("precompile") == null) { //Server类初始化 new Server(args); } else { Logger.info("Done."); } } main方法执行的操作很简单: 获取程序路径 检查是否存在precompiled参数 检查是否存在writepid参数,若存在则检查是否存在server.pid文件,若存在则表明已有程序在运行,不存在则将当前程序pid写入server.pid play类初始化 检查是否存在precompile参数项,若存在表示是个预编译行为,结束运行,若没有则启动服务 这其中最重要的便是Play类的初始化以及Server类的初始化 这里我们先来看Server类的初始化过程,现在可以先简单的将Play类的初始化理解为Play框架中一些常量的初始化以及日志、配置文件、路由信息等配置的读取。 这里贴一下Server类的初始化过程: public Server(String[] args) { //设置文件编码为UTF-8 System.setProperty("file.encoding", "utf-8"); //p为Play类初始化过程中读取的配置文件信息 final Properties p = Play.configuration; //获取参数中的http与https端口信息,若不存在则用配置文件中的http与https端口信息 httpPort = Integer.parseInt(getOpt(args, "http.port", p.getProperty("http.port", "-1"))); httpsPort = Integer.parseInt(getOpt(args, "https.port", p.getProperty("https.port", "-1"))); //若没有配置则设置默认端口为9000 if (httpPort == -1 && httpsPort == -1) { httpPort = 9000; } //http与https端口不能相同 if (httpPort == httpsPort) { Logger.error("Could not bind on https and http on the same port " + httpPort); Play.fatalServerErrorOccurred(); } InetAddress address = null; InetAddress secureAddress = null; try { //获取配置文件中的默认http地址,若不存在则在系统参数中查找 //之前还是参数配置大于配置文件,这里不知道为什么又变成了配置文件的优先级高于参数配置,很迷 if (p.getProperty("http.address") != null) { address = InetAddress.getByName(p.getProperty("http.address")); } else if (System.getProperties().containsKey("http.address")) { address = InetAddress.getByName(System.getProperty("http.address")); } } catch (Exception e) { Logger.error(e, "Could not understand http.address"); Play.fatalServerErrorOccurred(); } try { //同上,获取https地址 if (p.getProperty("https.address") != null) { secureAddress = InetAddress.getByName(p.getProperty("https.address")); } else if (System.getProperties().containsKey("https.address")) { secureAddress = InetAddress.getByName(System.getProperty("https.address")); } } catch (Exception e) { Logger.error(e, "Could not understand https.address"); Play.fatalServerErrorOccurred(); } //netty服务器启动类初始化,使用nio服务器,无限制线程池 //这里的线程池是netty的主线程池与工作线程池,是处理连接的线程池,而Play实际执行业务操作的线程池在另一个地方配置 ServerBootstrap bootstrap = new ServerBootstrap(new NioServerSocketChannelFactory( Executors.newCachedThreadPool(), Executors.newCachedThreadPool()) ); try { //初始化http端口 if (httpPort != -1) { //设置管道工厂类 bootstrap.setPipelineFactory(new HttpServerPipelineFactory()); //绑定端口 bootstrap.bind(new InetSocketAddress(address, httpPort)); bootstrap.setOption("child.tcpNoDelay", true); if (Play.mode == Mode.DEV) { if (address == null) { Logger.info("Listening for HTTP on port %s (Waiting a first request to start) ...", httpPort); } else { Logger.info("Listening for HTTP at %2$s:%1$s (Waiting a first request to start) ...", httpPort, address); } } else { if (address == null) { Logger.info("Listening for HTTP on port %s ...", httpPort); } else { Logger.info("Listening for HTTP at %2$s:%1$s ...", httpPort, address); } } } } catch (ChannelException e) { Logger.error("Could not bind on port " + httpPort, e); Play.fatalServerErrorOccurred(); } //下面是https端口服务器的启动过程,和http一致 bootstrap = new ServerBootstrap(new NioServerSocketChannelFactory( Executors.newCachedThreadPool(), Executors.newCachedThreadPool()) ); try { if (httpsPort != -1) { //这里的管道工厂类变成了SslHttpServerPipelineFactory bootstrap.setPipelineFactory(new SslHttpServerPipelineFactory()); bootstrap.bind(new InetSocketAddress(secureAddress, httpsPort)); bootstrap.setOption("child.tcpNoDelay", true); if (Play.mode == Mode.DEV) { if (secureAddress == null) { Logger.info("Listening for HTTPS on port %s (Waiting a first request to start) ...", httpsPort); } else { Logger.info("Listening for HTTPS at %2$s:%1$s (Waiting a first request to start) ...", httpsPort, secureAddress); } } else { if (secureAddress == null) { Logger.info("Listening for HTTPS on port %s ...", httpsPort); } else { Logger.info("Listening for HTTPS at %2$s:%1$s ...", httpsPort, secureAddress); } } } } catch (ChannelException e) { Logger.error("Could not bind on port " + httpsPort, e); Play.fatalServerErrorOccurred(); } if (Play.mode == Mode.DEV) { // print this line to STDOUT - not using logger, so auto test runner will not block if logger is misconfigured (see #1222) //输出启动成功,以便进行自动化测试 System.out.println("~ Server is up and running"); } } server类的初始化没什么好说的,重点就在于那2个管道工厂类,HttpServerPipelineFactory与SslHttpServerPipelineFactory ...

January 11, 2018 · 9 min · 1803 words · luyanliang

Play framework源码解析 Part1: Play framework 介绍、项目构成及启动脚本解析

注:本系列文章所用play版本为1.2.6 介绍 Play framework是个轻量级的RESTful框架,致力于让java程序员实现快速高效开发,它具有以下几个方面的优势: 热加载。在调试模式下,所有修改会及时生效。 抛弃xml配置文件。约定大于配置。 支持异步编程 无状态mvc框架,拓展性良好 简单的路由设置 这里附上Play framework的文档地址,官方有更为详尽的功能叙述。Play framework文档 项目构成 play framework的初始化非常简单,只要下载了play的软件包后,在命令行中运行play new xxx即可初始化一个项目。 自动生成的项目结构如下: 运行play程序也非常简单,在项目目录下使用 play run 即可运行。 启动脚本解析 play framework软件包目录 为了更好的了解play framework的运作原理,我们来从play framework的启动脚本开始分析,分析启动脚本有助于我们了解play framework的运行过程。 play的启动脚本是使用python编写的,脚本的入口为play软件包根目录下的play文件,下面我们将从play这个脚本的主入口开始分析。 play脚本解析 play脚本在开头引入了3个类,分别为play.cmdloader,play.application,play.utils,从添加的系统参数中可以看出play启动脚本的存放路径为 framework/pym cmdloader.py的主要作用为加载framework/pym/commands下的各个脚本文件,用于之后对命令参数的解释运行 application.py的主要作用为解析项目路径下conf/中的配置文件、加载模块、拼接最后运行的java命令 sys.path.append(os.path.join(os.path.dirname(os.path.realpath(sys.argv[0])), 'framework', 'pym')) from play.cmdloader import CommandLoader from play.application import PlayApplication from play.utils import * 在脚本的开头,有这么一段代码,意味着只要在play主程序根目录下创建一个名为id的文件,即可设置默认的框架id play_env["id_file"] = os.path.join(play_env['basedir'], 'id') if os.path.exists(play_env["id_file"]): play_env["id"] = open(play_env["id_file"]).readline().strip() else: play_env["id"] = '' 命令参数的分隔由以下代码完成,例如使用了play run –%test,那么参数列表就是 [“xxxx\play”,“run”,"–%test"] 这段代码也说明了,play的命令格式为 ...

January 3, 2018 · 5 min · 1041 words · luyanliang

Play framework源码解析目录

这里对Play framework源码解析做一个大致规划目录,要写的篇章如下 目录 Play framework 介绍、项目构成及启动脚本解析–已完成 Server与ServletWrapper–已完成 Play的初始化与启动–已完成 ActionInvoker与mvc 模板渲染 Play插件 classloader与字节码增强 测试 数据库拓展 辅助工具类 对Play的一些思考及优化方案

January 1, 2018 · 1 min · 16 words · luyanliang