-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathcontent.json
1 lines (1 loc) · 245 KB
/
content.json
1
{"pages":[],"posts":[{"title":"Collection","text":"本篇文章记录学习java集合中Collection接口的内容。 $Collection$接口 $Collection$接口是$List、Set和Queue$接口的父接口,该接口里定义的方法既可操作$Set$集合,也可用于操作$List$和$Queue$集合。 $jdk$不提供此接口的任何直接实现,而是提供更具体的子接口(如$Set,List$)实现。 在$java5$之前,$java$集合会丢失容器中所有对象的数据类型,把所有对象都当成$Object$类型处理;从$jdk5.0$增加了泛型之后,$java$集合可以记住容器中对象的数据类型。 $Iterator$迭代器接口 $Iterator$对象称为迭代器(设计模式的一种),主要用于遍历$Collection$集合中的元素。 $Collection$接口继承了$java.lang.Iterator$接口,该接口有一个$iterator()$方法,那么所有实现了$Collection$接口的集合类都能调用$iterator()$方法,返回一个实现了$Iterator$接口的对象。 $Iterator$仅用于遍历集合,$Iterator$本身并不提供承装对象的能力。如果需要创建$Iterator$对象,则必须有一个被迭代的集合。 集合对象每次调用$iterator()$方法都会得到一个全新的对象,默认指向集合第一个元素之前。 $List$接口 $List$集合类中元素有序,且可重复,集合中每个元素都有其对应的顺序索引。 List容器中的元素都对应一个整数型的序号记载其在容器中的位置,可以根据序号调用$get()$方法取容器中的元素。 $List$接口的常用实现类有$ArrayList、LinkedList$和$Vector$. $ArrayList$$jdk7$1ArrayList list = new ArrayList(); 在调用空参构造器时,创建了长度为$10$的$Object[]$数组$elementData$。当向$list$中增加元素达到$11$个时,数组容量不够,需要扩容,默认情况下,是扩容到原来的$1.5$倍,同时需要将原有数据中的数据复制到新的数组中。 $jdk8$底层$Object[] elementData$初始化空,当调用空参构造器时,并没有创建长度为$10$数组。当第一次调用$add()$时,才创建长度为$10$的数组,并将数组添加到数组中。 这种创建对象的方式类似于单例的饿汉式,延迟了数组的创建,节省内存。 具体创建的方法为$grow()$,这也是扩容的方法。 1234567891011121314151617/** * Increases the capacity to ensure that it can hold at least the * number of elements specified by the minimum capacity argument. * * @param minCapacity the desired minimum capacity */private void grow(int minCapacity) { // overflow-conscious code int oldCapacity = elementData.length; int newCapacity = oldCapacity + (oldCapacity >> 1); if (newCapacity - minCapacity < 0) newCapacity = minCapacity; if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity); // minCapacity is usually close to size, so this is a win: elementData = Arrays.copyOf(elementData, newCapacity);} 可以直接调用设置数组大小的构造器public ArrayList(int initialCapacity) $ArrayList$是线程不安全的,效率较高 $LinkedList$$LinkedList$底层使用双向链表实现,内部没有声明数组,而是定义了$Node$类型的$first$和$last$,用于记录首末元素。同时定义内部类$Node$,作为保存数据的基本结构。 1234567891011private static class Node<E> { E item; Node<E> next; Node<E> prev; Node(Node<E> prev, E element, Node<E> next) { this.item = element; this.next = next; this.prev = prev; }} 对于频繁的插入和删除元素操作,使用$LinkedList$效率比$ArrayList$高。 $Vector$在$jdk7$和$jdk8$中创建$Vector$对象,都创建了长度为$10$的数组。在扩容方面,默认扩容为原来数组长度的两倍。 大多数操作都和$ArrayList$相同,区别在于$Vector$是线程安全的,它总是比$ArrayList$慢,应避免使用。 $Stack$是$Vector$的子类。 $Set$接口 $Set$接口$Collection$的子接口,没有提供额外的方法,不允许包含相同的元素。 $Set$判断两个对象是否相同是根据$equals()$方法。 $HashSet$$HashSet$的内部采用了$HashMap$作为数据存储,$HashSet$其实就是在操作$HashMap$的$key$。 1234567/** * Constructs a new, empty set; the backing <tt>HashMap</tt> instance has * default initial capacity (16) and load factor (0.75). */public HashSet() { map = new HashMap<>();} 关于$hashMap$,可查看map $LinkedHashSet$是$HashSet$的子类,根据元素的$hashCode$值来决定元素存储位置,但它同时使用双向链表维护元素的次序,这使得元素看起来是以插入顺序保存的。 $LinkedHashSet$插入性能略低于$HashSet$,但在迭代访问$Set$全部元素时有很好的性能。 $TreeSet$ 是$SortedSet$接口的实现类,可以确保集合元素处于排序状态。 底层使用红黑树结构存储数据。 两种排序方法:自然排序(集合元素需实现$Comparable$接口,重写$CompareTo()$方法);定制排序(传入一个实现了$Comparator$接口重写$Compare()$方法的实例) $Collections$工具类 是一个操作$Set、List、Map$等集合的工具类 提供了一系列静态的方法对集合元素进行排序、查询和修改等操作,还提供了对集合对象设置不可变、对集合对象实现同步控制等方法","link":"/2021/11/23/Collection/"},{"title":"Hello Blog","text":"2021年10月21日,我终于开通了个人博客。 让我下定决心写博客的原因,是在昨天下午做到求数组中和为k的子数组个数的题目时,用到前缀和+哈希表,而这道题让我想起8月28号的CCPC网络赛F题Command Sequence也是一道前缀和+哈希表的题目,那时我却没有做出来,意识到自己已经打了将近半年的acm,知识还是很混乱的,所以下决心记录梳理一下知识。","link":"/2021/10/21/Hello-Blog/"},{"title":"Map","text":"本篇文章记录学习$java$集合中$Map$接口的内容。 $Map$接口 $Map$与$Collection$并列存在,存储具有映射关系的数据:key-value; $Map$中的$key$用$set$来存放,不允许包含同样的值。同一个$Map$对象实现的类,必须重写$equals()$和$hashCode()$方法。 $Map$中常使用$String$作为键。 常用实现类有$HashMap、LinkedHashMap、HashTable、Properties$,子接口$SortedMap$,其实现类$TreeMap$。 $HashMap$源码中的重要常量 123456789101112//默认初始化容量为16,必须是2的n次幂static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;//最大容量为2^30static final int MAXIMUM_CAPACITY = 1 << 30;//默认加载因子是0.75static final float DEFAULT_LOAD_FACTOR = 0.75f;//当链表长度过长时,超过这个阈值会转化为红黑树static final int TREEIFY_THRESHOLD = 8;//当红黑树上的元素个数减少到6个是,转化为链表static final int UNTREEIFY_THRESHOLD = 6;//链表转化成红黑树时数组的最小长度,这是为了避免数组扩容和树化的冲突static final int MIN_TREEIFY_CAPACITY = 64; $jdk7$HashMap的内部存储是数组加链表的结合。当实例化一个HashMap对象时,会创建一个长度为Capacity的Entry数组,在这个数组中存放元素的位置称之为桶($bucket$),每个$bucket$有自己的索引,可以根据索引找到其在数组中的位置。 关于put首先调用key所在类的hashCode()和hash方法计算key的哈希值,$jdk7$为了防止因$hash$碰撞引发的问题,在计算$hash$过程中引入随机种子,以增强$hash$的随机性,使得键值对均匀分布在桶数组中。 $hash$方法 12345678910final int hash(Object k) { int h = hashSeed; if (0 != h && k instanceof String) { return sun.misc.Hashing.stringHash32((String) k); } h ^= k.hashCode(); // 通过多次位运算,提高算法散列性 h ^= (h >>> 20) ^ (h >>> 12); return h ^ (h >>> 7) ^ (h >>> 4);} 然后根据得到的$h$值和$table$的长度计算出在$table$中的下标i=h&(length-1). 如果该位置上为空,则$key-value$添加成功。 若不为空,意味着此位置存在一个或多个数据(以链表形式存在),需要比较$key$和已经存在的数据的哈希值。 如果$key$的哈希值与已经存在的数据的哈希值都不相同,那么$key-value$添加成功,和原来的数据以链表的方式存储,且使用的是头插法,可能是出于后来的数据被访问到的可能性更大的出发点。 如果$key$的哈希值和已经存在的某一个数据$key1$的哈希值相同,继续调用$key$所在类的$equals()$方法比较是否相等。若不相等,则添加成功。如果相等,则用新的$value$更新原来的$value$。 $hashMap$的扩容当数组中元素数量达到扩容阈值$threshold$时,需要对原数组进行扩容,这便是$resize()$。数组的大小会扩展为$16*2=32$,即扩大一倍,并且需要重新计算每个元素在数组中的位置,这是一个很消耗时间的操作。 $resize()$方法 123456789101112131415void resize(int newCapacity) { Entry[] oldTable = table; int oldCapacity = oldTable.length; if (oldCapacity == MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return; } //创建新数组 Entry[] newTable = new Entry[newCapacity]; //将原table中的元素转移到新table中 transfer(newTable, initHashSeedAsNeeded(newCapacity)); table = newTable; //重新计算扩容阈值 threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);} $transfer()$方法 12345678910111213141516//转移元素void transfer(Entry[] newTable, boolean rehash) { int newCapacity = newTable.length; for (Entry<K,V> e : table) { while(null != e) { Entry<K,V> next = e.next; if (rehash) { e.hash = null == e.key ? 0 : hash(e.key); } int i = indexFor(e.hash, newCapacity); e.next = newTable[i]; newTable[i] = e; e = next; } }} 可以看到重新计算元素在数组中的位置仍需要调用$hash()$函数,这一点在$jdk1.8$有所改进。 为什么不直接复制到新数组中?因为数组下标计算公式为hash(key)&(length-1),当数组长度变化,数组所在位置也应该变化。 头插法的弊端使用头插,当数组扩容,链表上的元素顺序会倒置,在多线程环境下可能形成环。 在多线程的环境下有可能会使链表形成环状,在$getEntry()$方法查找元素时导致死循环。 $jdk8$在$jdk8$中$hashMap$的内部存储结构是数组+链表+红黑树的结合。$jdk8$中的数组是$Node$类型的,这与$jdk7$不同。当初始化一个$hashMap$时,会初始化initialCapacity和loadFactor,在$put$第一对映射关系时,会创建一个长度为initialCapacity的$Node$数组,这个长度是容量Capacity,在数组中存放元素的位置称之为桶(bucket),每个bucket有自己的索引,可根据索引查找到bucket中的元素。 为什么初始化数组长度为$16$当$put$一个新数据时会计算位于$table$数组中的下标index = hash(key)&(length-1),此处求下标使用按位与操作,如果$length-1$中某一位为$0$,那么该位按位与&必然为$0$,导致数组上有些位置永远访问不到,造成空间的浪费,也增加了$hash$冲突的可能性。而如果是$2$的$n$次幂形式,减一后低位全为$1$,保证计算后的$index$既可以是奇数也可以是偶数,且只要传进来的$key$足够分散均匀,那么$index$就会减少重复,这样就减少了$hash$碰撞。 为什么选$16$这个数?因为分配的太小很容易导致$Map$扩容影响性能,初始化分配太大又会浪费资源。 加载因子$0.75$是时间和空间的权衡。如果小于$0.75$如$0.5$,那么数组达到一半就会扩容,空间利用率大大降低。如果大于$0.75$如$0.8$,则会增大$hash$冲突的概率,影响查询效率。在源码注释中有更深层次解释,大概意思是当加载因子是$0.75$的情况下,桶中$Node$结点的分布服从参数为$0.5$的泊松分布,当一个桶中出现$8$个元素的概率,已经小于千万分之一了。 12345678910111213141516171819202122232425/** Because TreeNodes are about twice the size of regular nodes, we * use them only when bins contain enough nodes to warrant use * (see TREEIFY_THRESHOLD). And when they become too small (due to * removal or resizing) they are converted back to plain bins. In * usages with well-distributed user hashCodes, tree bins are * rarely used. Ideally, under random hashCodes, the frequency of * nodes in bins follows a Poisson distribution * (http://en.wikipedia.org/wiki/Poisson_distribution) with a * parameter of about 0.5 on average for the default resizing * threshold of 0.75, although with a large variance because of * resizing granularity. Ignoring variance, the expected * occurrences of list size k are (exp(-0.5) * pow(0.5, k) / * factorial(k)). The first values are: * * 0: 0.60653066 * 1: 0.30326533 * 2: 0.07581633 * 3: 0.01263606 * 4: 0.00157952 * 5: 0.00015795 * 6: 0.00001316 * 7: 0.00000094 * 8: 0.00000006 * more: less than 1 in ten million */ $put()$方法123public V put(K key, V value) { return putVal(hash(key), key, value, false, true);} 当$table$为空时调用$resize()$扩容。根据当前$key$的哈希值找到在数组中的下标,并判断当前位置是否存在元素,若没有,则把$key、value$包装成$Node$节点,直接添加到此位置。若已有元素,分三种情况。 当前位置元素的$hash$值等于传过来的$hash$值,且它们的$key$也相等,就覆盖。 如果当前已经是红黑树结果,就加入到红黑树中。 已存在元素,且是普通链表结构,则采用尾插法,把新节点加入到链表尾部。在插入过程中,若链表长度达到$8$,则转化为红黑树。 $hashMap$的扩容和$hash$方法当数组元素个数超过initialCapacity*loadFactor时,就会进行数组扩容,将数组容量扩大一倍。 $hash$方法 12345static final int hash(Object key) { int h; // >>>无符号右移,只保留高16位 return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);} 将$hashCode()$的值进行高$16$位值和低$16$值的异或运算,尽量保留高$16$位的特征,降低哈希碰撞的概率。如果不进行高低位异或运算,直接用低$16$位和n-1相与求数组下标,那么高位不同,低位相似度较高的两个$hashCode$值得到的数组下标可能相同,高位的特征被丢失了,这样哈希碰撞的概率大大增加。而异或运算,使$0$和$1$的比例达到$1:1$的平衡状态,使结果的随机性更大。 在$resize()$扩大数组长度为两倍后,重新计算原数组的位置时,如果原来的$hash$值在高$1$位为$0$,那么在新数组的位置不变,如果为$1$,则在新数组中的位置为原来的位置加上原来数组的长度。 这样在扩充时不需要像$jdk1.7$那样重新计算$hash$,只需要看看原来的$hash$新增的那个$bit$是$1$还是$0$即可,省去了重新计算$hash$的时间,这是优化的地方。 且$jdk1.7$中$rehash$时,旧链表迁移到新链表时,如果在新表的数组索引位置相同,则链表元素会倒置,可能形成环,而$jdk1.8$中使用尾插法不会使元素倒置倒置,在$resize()$中有所体现。 $hashMap$的树化在$put$数据到$hashMap$中时,如果是放到同一个位置上链表里,当链表长度达到$8$,会进行树化。会什么是$8$,在$jdk1.8$的源码注释中有深层次的解释,涉及泊松分布等概率知识,和上文加载因子$0.75$一致。 12if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); 但是,当数组长度小于$64$,会先进行扩容。如果数组长度达到了64,就转变为红黑树结构,结点类型由$Node$变成$TreeNode$类型。当元素被移除时,下次$resize()$方法判断树的结点个数低于$6$个,会将红黑树再转变为链表。 带参构造器当希望指定初始数组的大小时,调用了带有数组大小参数的构造器,但并不会真正创建那个长度的数组。由上文所讲,数组长度必须为$2$的$n$次幂形式。实际上会使用到$tableSizeFor()$函数,返回大于当前传入值最小的一个$2$的$n$次幂的值。 123456789101112/** * Returns a power of two size for the given target capacity. */static final int tableSizeFor(int cap) { int n = cap - 1; n |= n >>> 1; n |= n >>> 2; n |= n >>> 4; n |= n >>> 8; n |= n >>> 16; return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;} $hashMap$是线程安全的吗?不是,即使在扩容时不会引起死循环,但$put()$和$get()$方法都没有加同步锁,在多线程的情况下,无法保证上一秒$put$的值,下一秒$get$的仍是原值。 $Node$节点里的$hashCode$方法。 123public final int hashCode() { return Objects.hashCode(key) ^ Objects.hashCode(value);} $LinkedHashMap$ 是$hashMap$的子类,在$hashMap$的存储结构基础上,使用了一对双向链表来记录添加元素的顺序。 和$LinkedHashSet$类似,可以维护$map$的迭代顺序与插入顺序一致。 内部类$Entry$ 123456static class Entry<K,V> extends HashMap.Node<K,V> { Entry<K,V> before, after; Entry(int hash, K key, V value, Node<K,V> next) { super(hash, key, value, next); }} $TreeMap$ 存储$key-value$对时,会根据$key$进行排序。 使用自然排序,所有的$key$必须是同一个类的对象,并且实现$Comparable$接口 使用定制排序,创建$TreeMap$时需要传入一个$Comparator$对象,负责对$key$排序。 判断两个$key$相等的标准:通过$compareTo()$方法或者$compare()$方法返回$0$。 $HashTable$ 是一个古老的$Map$实现类,实现原理和$hashMap$相同,但是方法直接加上$synchronized$,是线程安全的。 $hashTable$不允许将$null$作为$key$或者$value$,而$hashMap$允许。 效率比$hashMap$低,一般不使用。在多线程环境下会使用$ConcurrentHashMap$。","link":"/2021/11/24/Map/"},{"title":"位运算","text":"本篇文章主要罗列一些位运算的技巧,加强记忆。 消去$x$二进制位的最后一位$1$ :x&(x-1) 判断$x$是否是$2$的幂 :x&(x-1)==0 判断数$x$的二进制有几个$1$ :每次去掉最低位$1$,统计次数 两个数$a,b$,$a\\bigoplus b\\bigoplus b=a$,一个数异或两次另一个数得到自身,由异或运算满足结合律很好理解。 不设中间值,交换两个变量$a,b$的值 : a=a^b,b=a^b,a=a^b. a^b<=a+b 例题:永夜的报应 取余操作$a\\pmod b$,当除数$b$为$2^n$形式,可以转化为a&(b-1),效率更高。 暂时想到这么多,,,","link":"/2021/11/05/%E4%BD%8D%E8%BF%90%E7%AE%97/"},{"title":"关于equals和hashCode方法","text":"本篇文章记录学习$equals()$方法和$hashCode()$方法的内容。 什么是$hashCode$通常所说的$hashCode$是一个经过哈希运算后的整型值,这个哈希运算的方法,定义在$Object$类中,通过一个本地方法$hashCode()$来实现(在$hashMap$中还会有一些其他的运算)。 1public native int hashCode(); 要了解这个方法到底是什么作用,最有效的方法就是直接看源码注释。 返回当前对象的一个哈希值。这个方法用于支持一些哈希表,例如$hashMap$。 通常来讲,它有一下一些约定: 若对象的信息没有被修改,那么,在一个程序的执行期间,对于相同的对象,不管调用多少次$hashCode$方法,都应该返回相同的值。当然,在相同程序的不同执行期间,不需要保持结果的一致。 若两个对象的$equals()$方法返回值相同,那么,他们调用各自的$hashCode$方法时,也必须返回相同的结果。 当两个对象的$equals()$方法返回值不同时,那么他们的$hashCode$方法不用保证必须返回不同的值。但是,我们应该知道,在这种情况下,最好也设计成$hashCode$返回不同的值。因为,这样做有助于提高哈希表的性能。 实际情况下,$Object$类的$hashCode$方法在不同的对象确实返回了不同的哈希值。这通常是通过把对象的内部地址转换成一个整数来实现的。 这里所说的内部地址就是物理地址。需要注意的是,虽然$hashCode$值是依据它的物理地址而得来的。但是,不能说$hashCode$就代表对象的内存地址。 $hashCode$有什么用在哈希表中,通过$key$计算出它的$hashCode$值,再进行处理就可以确定它在哈希表中的位置,这样,在查询时,就可以直接定位到当前元素,提高查询效率。对于要插入的一个新元素,先去计算它的$hashCode$值,如果此位置没有元素,那么就直接插入即可。如果此位置已经有值,可以通过$equals()$方法比较它们是否相等,不等则也插入到这个位置(可以用链表形式存放)。所以,$hashCode$提高了查询,插入元素的效率。 $equals$和$==$的区别$equals$比较的是内容,而 == 比较的是地址。 $equals()$方法是定义在$Object$类中的。 123public boolean equals(Object obj) { return (this == obj);} 可以看到,它的默认实现,就是 == ,用来比较两个对象的内存地址是否相等。如果一个对象不重写$equals$,那么效果和 == 是一样的。 因此,在使用自定义类的对象时,如果要让两个对象的内容相同时认为对象时相同的,则需要重写$equals$方法。 为什么要重写$equals$和$hashCode$前面已经说明为什么重写$equals$,重写$hashCode$就涉及到$Map$和$Set$(底层其实也是$Map$)的内容了。 以$hashMap$ $jdk1.8$的源码来看,如$put$方法。 可以看到,在插入元素时,代码中多次进行$hash$值的比较,只有当$hash$值相等时,才会去比较$equals()$方法。当哈希值和$equals$比较都相等时,才会覆盖元素。$get$方法也是如此。 只有当$hashCode$和$equals$都相等时,才认为是同一个元素。 重写$hashCode$和$equals$的目的,就是为了方便哈希表这样的结构快速的查询和插入,不重写,则无法比较元素,可能造成元素位置错乱。 重写了$equals$,就必须重写$hashCode$在$JDK$源码注释第二点就说明了这一点。如果不重写$hashCode$,对于我们自定义的类,创建的两个内容相同的对象,将其中一个对象加入到$hashmap$中,另一个对象调用其$hashCode$查找位置仍能插入成功,这是不正确的。 还需要注意的是,讲对象插入到了$hashMap$后,不要在使用过程中,改变对象的值,这样会导致$hashCode$值发生改变,无法再获取到插入的值。 $String$类具有不可变性,所以我们经常使用$String$类作为$hashMap$的$key$值。 $hashCode$相等,$equals$一定相等吗显然不是。在源码中,当通过$hashCode$值处理后计算出来的位置相等(产生哈希碰撞)时,还需要比较它们的$equals$,才能确定是否是同一个对象。因此,$hashCode$相等时,$equals$不一定相等。 反过来,$equals$相等,那么$hashCode$一定相等吗?是一定的。$equals$都相等了,那么在$hashMap$中认为它们是同一个元素,那么$hashCode$值必须保证相等。 总结 $hashCode$相等,$equals$不一定相等 $hashCode$不等,$equals$一定不等 $equals$相等,$hashCode$一定相等 $equals$不等,$hashCode$不一定不等 在源码注释第三点也提到,当$equals$不等时。不必保证它们的$hashCode$不等。但是为了提高哈希表的效率,最好设计成不等。因为,我们既然知道它们不相等了,那么当 $hashCode$ 设计成不等时。只要比较$hashCode$ 不相等,我们就可以直接返回 $null$,而不必再去比较 $equals$了。这样,就减少了比较的次数,无疑提高了效率。","link":"/2021/11/25/%E5%85%B3%E4%BA%8Eequals%E5%92%8ChashCode%E6%96%B9%E6%B3%95/"},{"title":"图论(1)","text":"TarjanTarjan算法是一种非常实用的图论算法,可以解决连通块、割点、缩点、桥等问题。 建图建图采用链式前向星方式。 12345678910111213141516171819int head[maxn]; //表头int nxt[maxn]; //链表下一位int edge[maxn]; //该边终点int weight[maxn]; //该边的权值int tot; //边的数量void addEdge(int u,int v,int w){ edge[++tot] = v; weight[tot] = w; nxt[tot] = head[u]; head[u] = tot;}//遍历方式void traverse(){ for(int i=1;i<=n;i++) for(int j=head[i];j;j=nxt[j]) cout<<i<<" "<<edge[j]<<endl;} 强连通分量强连通分量,即图的一个子集。如果两个顶点可以相互通达,则称这两个点强连通(strongly connected).如果有向图的每两个顶点都强连通,则称图G为强连通图。有向图的强连通子图,称为强连通分量。 弄懂Tarjan算法需要明白最重要的两个数组:$dfn、low$. $dfn[x]$记录每个点最早被遍历的时间,即在dfs过程中$x$的时间戳。 $low[x]$表示从$x$处搜索,能够回溯到的最早被遍历的点的时间戳。 用$vis$数组来记录每个点的分块编号,或者换个写法表示每个点所在的连通块的根的编号也可。 使用dfs遍历图,每次访问到新节点$u$时,向栈中压入$u$,记录$dfn[u]==low[u]$,并依次访问该节点的每一个相邻节点$v$.会出现三种情况: $dfn[v]==0$:此时$v$还没有搜索到,直接对$v$进行递归,会得到$low[v]$,故更新$low[u] = min(low[u],low[v])$ $vis[v]==0$:此时$v$早在$u$之前就已经访问过了,但是$u$的邻接点还未遍历完全,更新$low[u]=min(low[u],dfn[v])$. $vis[v]!=0$:此时$v$已经操作完全,略过。 对于第二种情况的$v$,我们用其$dfn$值更新$u$的$low$值的原因是,从$u$处能搜到曾经搜过的$v$,那么从$v$开始就有可能存在一个强联通块包含了$u$,因此我们将$u$的low值更新,再通过回溯时对low值的更新,一步步更新回$v$点,若$low[v]$与$dfn[v]$相等,就说明从$v$开始绕了一圈又找到了$v$,也就是说找到了强连通分量。那么如何对应地找到其中所有的节点呢?我们通过栈来实现,在dfs过程中,若遇到没搜过的点,则将其入栈,最后在$low[u]=dfn[u]$处,因为从$u$处最终能走回$u$,回溯到$u$时,途中的所有节点都在栈中,而中途可能遇到的分支都会在相同的过程中全部出栈(不在环中的自成一个强连通分量),因此从栈顶到栈中$u$的位置,中间的节点正好在一个强连通分量中,所以我们只需要不断弹栈,直到将$u$弹出栈,所有弹出的元素都归为一个强联通块中。可以证明,这样求得的强联通块是最大强联通块,将强联通块抽象成点,这便是缩点 12345678910111213141516171819202122232425void Tarjan(int u){ dfn[u] = low[u] = ++tit; //时间戳 st.push(u); for(int i=head[u];i;i=nxt[i]) { int v = edge[i]; if(!dfn[v]){ Tarjan(v); low[u] = min(low[u],low[v]); } else if(!vis[v]) low[u] = min(low[u],dfn[v]); } if(dfn[u]==low[u]) { vis[u] = ++cnt; //连通块编号 while(st.top()!=u) { vis[st.top()] = cnt; //栈顶到u的一块都在一个连通块中 st.pop(); } st.pop(); }} 上题目校园网 缩点 割点割点是指若将连通图的某个点及其连接的边删去后,图中的连通分量增加,则称这个点为割点。 对于点$u$,假如其从$fa$搜索而来,连接着某点$v$,若$low[v] \\geq dfn[u]$,则$u$是割点。 可以画出图来理解,如果我们将$u$和其连接的边删掉,$v$和$fa$必然不属于同一个连通块。若删掉后$v$和$fa$属于同一个连通块,那么$low[v]$必然会小于$dfn[u]$。通过这一点我们可以对每个走过的点都用其$dfn$值更新$u$的$low$值,就是为了保证求出的割点一定保证$fa$和$v$不在任何同一个连通块中。 那么这时我们会考虑一个特殊的点:第一个被搜索的点,这个点没有$fa$。该如何确定这个点是不是割点呢?我们直接记录其子树数量,如果其子树数量大于1,那么就是割点,因为把它去掉,其子树不能相互到达。 于是算法模板如下 12345678910111213141516171819202122void tarjan(int u,int root){ dfn[u] = low[u] = ++tit; int childtree = 0; //根的子树数量 for(int i=head[u];i;i=e[i].nxt) { int v = e[i].to; if(!dfn[v]){ tarjan(v,root); low[u] = min(low[u],low[v]); if(low[v]>=dfn[u]&&u!=root){ cut[u] = true; //记录u节点为割点 } if(u==root) childtree++; } else low[u] = min(low[u],dfn[v]); } if(childtree>=2&&u==root) cut[u] = true; // 根节点也为割点} 根据上述求法,我们还可以统计出删掉割点$u$之后连通块增加的个数 对于第一个搜索的点,连通块增加的个数是$childtree-1$。 对于其他点,连通块增加的个数是$u$被判为割点的次数 12345678910if(low[v]>=dfn[u]&&u!=root) cut[u] = true;改为if(low[v]>=dfn[u]&&u!=root) ++delta[u]; //删去u增加的连通图数量if(childtree>=2&&u==root) cut[u] = true; 改为if(childtree>=2&&u==root) delta[root] = childtree-1; 上例题 割点 嗅探器 桥在无向图中,删去一条边,使得图的连通块数量增加,则称这条边为桥。 在$tarjan$的过程中,若$u$连接着$v$,$low[v]>dfn[u]$,则连接$u、v$的边是桥,可画图理解。 123456789101112131415161718void Tarjan(int u, int fa){ dfn[u] = low[u] = ++cnt; //桥在无向图中是两条相同的边,所以边一般从0开始编号 for (int i = head[u]; i != -1; i = nxt[i]) { int v = edge[i]; if (dfn[v] == 0) { Tarjan(v, u); low[u] = min(low[u], low[v]); if (low[v] > dfn[u]) bridge[i] = bridge[i ^ 1] = true;//i和i^1这两条边是同一条边,是桥 } else if (fa != v) low[u] = min(low[u], dfn[v]); }} 上例题 炸铁路 拓扑排序在图论中,拓扑排序(Topological Sorting)是一个有向无环图(DAG,Directed Acyclic Graph)的所有顶点的线性序列。且该序列必须满足以下两个条件: 每个顶点出现且仅出现一次。 若存在一条从顶点A到顶点B的路径,则在序列中顶点A在顶点B的前面出现。 有向无环图(DAG)才有拓扑排序,非DAG图没有拓扑排序一说。根据定义可知,一个DAG图的拓扑排序也许不止一个。 直接上题 摄像头 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960#include<iostream>#include<queue>using namespace std;const int maxn = 510;int edge[maxn],head[maxn],nxt[maxn];int ind[maxn],outd[maxn];int has[maxn],a[maxn];int n,tot,ans;queue<int> q;void addEdge(int u,int v){ edge[++tot] = v; nxt[tot] = head[u]; head[u] = tot;}int main(){ ios::sync_with_stdio(false); cin.tie(0),cout.tie(0); cin>>n; for(int i=1;i<=n;i++) { int m,y; cin>>a[i]>>m; has[a[i]] = 1; //该位置被监视 while(m--) { cin>>y; addEdge(a[i],y); outd[a[i]]++,ind[y]++; } } for(int i=1;i<=n;i++) { if(ind[a[i]]==0) q.push(a[i]); } while(!q.empty()) { int u=q.front(); q.pop(); if(has[u]) ans++; //砸掉摄像头,该位置不被监视 for(int i=head[u];i;i=nxt[i]) { int v=edge[i]; ind[v]--; if(ind[v]==0&&has[v]==1) q.push(v); } } if(ans==n) cout<<"YES"<<endl; else cout<<n-ans<<endl; return 0;} 旅行计划","link":"/2021/10/27/%E5%9B%BE%E8%AE%BA-1/"},{"title":"图论(2)","text":"最短路最短路是非常常见的问题,有单源最短路和全源最短路,求解方法和各种应用也很多。来看一些常用、基本的最短路算法。 图的建立 用多个数组或一个结构体来直接存边。建图方便,操作效率低,在$Kruskal$算法中有应用。 邻接矩阵,$f[u][v]=w$表示$u$到$v$有一条权值为$w$的边。建图方便,遍历复杂度高,空间花费巨大,不适用于有重边的情况,一般用于稠密图,$Floyd$算法一般使用邻接矩阵。 vector存边。建图方便,可对边排序,操作效率高。 链式前向星。类似邻接表,理解后背模板即可,操作效率高,后文代码均使用此方式建图。 单源最短路单源最短路径指的是求从图上一个起点出发到其他所有点的最短路径。 以下默认一张图中$n$个点,$m$条边。 $Bellman-Ford$算法Bellman-Ford算法最核心的操作是松弛操作,其思想为:用当前节点的最短路去更新其邻接点的最短路。 容易想到,一条最短路上最多只有$n$个点和$n-1$条边,因此我们只需要对每一条边尝试松弛$n-1$次,若存在最短路,则这些操作后一定找齐了所有的最短路,且所有边均不能再松弛操作;若仍能进行松弛操作,表示存在负环。 Bellman-Ford算法非常暴力,时间复杂度很高。显然,在对一条边进行松弛时,只有它的前驱节点已经进行过最短路的估计,即$dist[u]$不为$\\infty$时,边$(u,v)$才能被松弛。在Bellman-Ford算法中,有大量的边在遍历时不需要被松弛。 我们可以利用队列进行优化得到$SPFA$算法。 $SPFA$算法对于Bellman-Ford,将所有更新过的点加入队列,每次取出一个点进行松弛,直到队列为空,这就是$spfa$. 1234567891011121314151617181920212223242526void spfa(int s){ for(int i=1;i<=n;i++) dist[i] = INF; //设置初始距离为无穷大,用来松弛 queue<int> q; dist[s] = 0; //到自身距离为0 q.push(s); flag[s] = true; //标记入队 while(!q.empty()) { int u = q.front(); q.pop(); flag[u] = false; //标记出队 for(int i=head[u];i;i=nxt[i]) { int v = edge[i],w = weight[i]; if(dist[v]>dist[u]+w){ dist[v] = dist[u]+w; //松弛操作 if(!flag[v]){ q.push(v); flag[v] = true; //标记入队 } } } }} 但是即使使用队列优化过,$spfa$时间复杂度依然很高,最坏情况下达到$O(|V|\\cdot |E|)$.很容易被卡掉。 负环判定$spfa$的一个优点在于可以判别图中是否存在负环。判定方法是:记录每个点的入队次数,如果这个次数达到了总的节点数则说明图中存在负环。 注意是判入队次数而不是松弛次数,如果存在重边导致了多次松弛,会对松弛次数的判断产生影响,可能会被$hack$。 123456789if(dist[v]>dist[u]+w){ dist[v] = dist[u] + w; //松弛操作 if(!flag[v]){ if(++cnt[v]>=n) //判入队次数,当大于等于n时说明存在负环 printf("存在负环\\n"); q.push(v); flag[v] = true; }} 但是这种方法可能会爆$int$,原因在于要让入队次数达到$n$,则遍历的总个数最大可达$n^2$. 考虑换一种思路,如果不存在负环,那么从某点出发到每个点的最短路应当是不存在环的。因此我们可以判断最短路径的路径边数是否小于$n$(即经过点数小于等于n,没有任何一个点重复走过),来更高效地判断负环。 12345678910if(dist[v]>dist[u]+w){ dist[v] = dist[u] + w; if(!flag[v]){ cnt[v] = cnt[u]+1; //v的最短路上经过点数比u多1 if(cnt[v]>=n) printf("存在负环\\n"); flag[v] = 1; q.push(v); }} 负环 $Dijkstra$算法$spfa$如此之慢,我们需要一种更快更稳定的最短路算法。 $dijkstra$算法的思想是贪心+$bfs$求最短路,它只适用于不含负边权的图。在稠密图中表现优秀。 我们将点分成两类,一类是已经确定最短路径的点,称为“白点”,另一类是未确定最短路径的点,称为“蓝点”。 $dijkstra$流程如下: 初始化$dist[s]==0$,其余节点的$dist$值为$\\infty$. 找到一个$dist$最小的蓝点$u$,将节点$u$变成白点,即找到最短路。 遍历$u$的所有出边$(u,v,w)$,若$dist[v]>dist[u]+w$,则$dist[v]=dist[u]+w$. 重复$2,3$两步,直到所有点变成白点。 这样做的时间复杂度是$O(n^2)$。 图解 另初始节点$s$为1,把$dist[s]$初始化为0,其余点初始化为$\\infty$. 第一轮循环找到$dist$最小的点$1$,将$1$变成白点,对所有与$1$相连的蓝点的$dist$进行松弛,使$dist[2]=2,dist[3]=4,dist[4]=7$ 第二轮循环找到$dist$最小的点$2$,将$2$变成白点,对所有与2相连的蓝点的$dist$进行松弛,使$dist[3]=3,dist[5]=4$ 第三轮循环找到$dist$最小的点是$3$,将3变成白点,对所有与3相连的蓝点进行松弛,使$dist[4]=4$ 接下来两轮循环分别将4、5设置为白点,算法结束,所有点最短路径找到。 为什么$dijkstra$不能处理负边权图 我们来看下面这张图 2到3的边权为-4,显然从1到3的最短路径为-2(1->2>3).但在循环开始时程序会找到当前$dist$最小的点3,并将其标记为白点。 这时$dist[3]=1$,然而1并不是起点到3的最短路径,且3已经被标记为白点,所以$dist[3]$不会再被修改。 我们在边权为负时得到了错误的答案。 $dijkstra$的堆优化观察流程,发现步骤2可以优化。我们用小根堆对$dist$数组进行维护,在$O(\\log n)$的时间取出堆顶元素并删除,用$O(\\log n)$遍历每条边,总的时间复杂度为$O((n+m)\\log n)$. $dijkstra$的正确性我们可以考虑反证法。假如点$u$在出队后优先队列中还有点$y$可以使$dist[u]$减小,那么$dist[y]$必然小于$dist[u]$.而根据优先队列的性质,$dist[u]$是堆中最小的元素,即$dist[u]\\leq dist[y]$,产生矛盾。因此可以保证$u$出队后$dist[u]$是最小的。 123456789101112131415161718192021222324252627282930313233struct Node{ int id; int val; Node(){} Node(int id,int val): id(id),val(val) {} bool operator < (const Node s) const{ //重载小于号,使其为小根堆 return val>s.val; }};priority_queue<Node> q;void dijkstra(int s){ for(int i=1;i<=n;i++) dist[i] = INF; //设置初始距离为无穷大 dist[s] = 0; q.push(Node(s,dist[s])); while(!q.empty()) { Node now = q.top(); q.pop(); int u = now.id; if(vis[u]) continue; //如果已经找到最短路,跳过 vis[u] = true; for(int i=head[u];i;i=nxt[i]){ int v = edge[i],w=weight[i]; if(dist[v]>dist[u]+w){ dist[v] = dist[u]+w; if(!vis[v]) q.push(Node(v,dist[v])); } } }} 上模板 $DAG$图最短路$DAG$图即有向无环图,可以直接对图进行拓扑排序,按照点的顺序一次进行松弛操作即可。 正确性拓扑排序后松弛的顺序即是最终结果中最短路边的顺序,每次松弛前边的起点都已经找到最短路,满足最优子结构。 时间复杂度线性 全源最短路全源最短路径指求图上任意两点之间的最短路径。 常见算法包括$Floyd$算法、$Johnson$算法。 $Floyd$算法$floyd$算法是一种动态规划求解最短路的方法,其基本思想是:对于每个起点和终点,枚举中间点,进行状态转移。 转移方程为$d_{x,y}=min(d_{x,k}+d_{k,y}|1\\leq k \\leq n)$ 时间复杂度为$O(n^3)$ 1234for(int k=1;k<=n;k++) for(int i=1;i<=n;i++) for(int j=1;j<=n;j++) dist[i][j] = min(dist[i][j],dist[i][k]+dist[k][j]); $floyd$算法还可以判断图中两个点是否连通,在数据范围较小时非常好用。 上例题Cow Contest S $Johnson$算法$johnson$算法求任意两点间的最短路,是通过枚举起点,跑$n$次$dijkstra$算法解决,算法的时间复杂度为$O(nm\\log m)$,在稀疏图上比$floyd$算法更加优秀。 但是$dijkstra$算法不能正确求解带负权边的最短路问题,因此我们需要在原图上做预处理,确保所有的边权非负。 一种任意想到的方法是给所有边的边权同时加上一个正数$x$,从而让所有的边权非负。如果新图上起点到终点的最短路经过了$k$条边,则将最短路减去$kx$得到实际最短路。 但这种方法是错误的。因为如果原图上最短路边数较多,再每条边加上正数$x$后,新图可能存在一条边数更少的最短路,这时已经不是原来的最短路了。 $johnson$算法通过另一种方法来给每条边重新标注边权。 我们建立一个超级源点(编号为$0$),从这个点向每一个点连一条权值为$0$的边。 首先可以用$spfa$求出源点到每个节点的最短路,记为$h_i$。 假设原图中存在一条从$u$到$v$,边权为$w$的边,我们将该边的边权重新设置为$w+h_u-h_v$。 接下来以每个点为起点,跑$n$轮$dijkstra$算法即可求出新图任意两点间的最短路。 以$u$为起点,$v$为终点的最短路结果求出为$dist[v]$,实际在原图上为$dist[v]+h_v-h_u$。 正确性证明 在重新标记的图上,从$s$点到$t$点的一条路径$s->p_1->p_2->\\cdots ->p_k->t$的长度表达式为: $(w(s,p_1)+h_s-h_{p_1})+(w(p_1,p_2)+h_{p_1}-h_{p_2})+\\cdots +(w(p_k,t)+h_{p_k}-h_t)$ 化简后得到: $w(s,p_1)+w(p_1,p_2)+\\cdots +w(p_k,t)+h_s-h_t$ 无论我们走哪一条路径,$h_s-h_t$的值不变的。这类似于两点间势能差的概念,只与两点位置有关。 为了方便,下面我们就把$d_i$称为$i$点的势能。 新图中的$s->t$的最短路长度表达式有两部分组成,前面的边权和为$s$到$t$的最短路,后面为两点的势能差。因为两点间的势能差为定值,因此原图上$s->t$的最短路与新图上$s->t$的最短路相对应。 至此我们证明了重新标注后图上最短路径仍是原图上的最短路径,接下来需要证明标注后所有边权非负,因为在非负边权上,$dijkstra$才能保证得出正确的结果。 根据三角形不等式,新图上任意一边$(u,v)$满足:$h_v\\leq h_u+w(u,v)$。这条边重新标注之后的边权为$w’(u,v)=w(u,v)+h_u-h_v\\geq 0$。这样证明了标注后边权均非负。 至此我们证明了$johnson$算法的正确性。 上模板johnson全源最短路 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145#include<iostream>#include<cstdio>#include<cstring>#include<queue>#include<algorithm>#define INF 0x3f3f3f3fusing namespace std;typedef long long ll;const int maxn = 1e5+10;inline int read(){ int x = 0, f = 1; char ch = getchar(); while (ch < '0' || ch > '9') { if (ch == '-') f = -1; ch = getchar(); } while (ch >= '0' && ch <= '9') { x = (x << 3) + (x << 1) + (ch ^ 48); ch = getchar(); } return x * f;}struct Node{ int id; int val; Node(){} Node(int id,int val): id(id),val(val) {} bool operator < (const Node s) const{ return val>s.val; }};ll head[maxn],nxt[maxn],edge[maxn],weight[maxn];ll dist[maxn];//距离ll d[maxn];ll cnt[maxn];bool vis[maxn];ll tot,n,m;void addEdge(int u,int v,int w){ edge[++tot] = v; weight[tot] = w; nxt[tot] = head[u]; head[u] = tot;}bool spfa(int s){ queue<int> q; for(int i=1;i<=n;i++) d[i] = 63; d[s] = 0; q.push(s); vis[s] = 1; while(!q.empty()) { int u = q.front(); q.pop(); vis[u] = 0; for(int i=head[u];i;i=nxt[i]) { int v = edge[i],w=weight[i]; if(d[v]-d[u]>w){ d[v] = d[u]+w; if(!vis[v]){ cnt[v] = cnt[u]+1; if(cnt[v]>=n+1) return false; //判负环 q.push(v); vis[v] = 1; } } } } return true;}void dijkstra(int s){ priority_queue<Node> q; for(int i=1;i<=n;i++){ dist[i] = INF; //设置初始距离为无穷大 vis[i] = 0; } dist[s] = 0; q.push(Node(s,dist[s])); while(!q.empty()) { Node now = q.top(); q.pop(); int u = now.id; if(vis[u]) continue; //如果已经找到最短路,跳过 vis[u] = true; for(int i=head[u];i;i=nxt[i]){ int v = edge[i],w=weight[i]; if(dist[v]>dist[u]+w){ dist[v] = dist[u]+w; if(!vis[v]) q.push(Node(v,dist[v])); } } }}int main(){ n =read(),m=read(); while(m--) { int u,v,w; u = read(),v=read(),w=read(); addEdge(u,v,w); } for(int i=1;i<=n;i++) addEdge(0,i,0); //建超级源点 if(!spfa(0)) { cout<<-1<<endl; //存在负环直接退出 return 0; } for(int u=1;u<=n;u++) for(int i=head[u];i;i=nxt[i]) weight[i] += d[u]-d[edge[i]]; for(int i=1;i<=n;i++) { dijkstra(i); ll ans=0; for(int j=1;j<=n;j++) { if(dist[j]==INF) ans += j*1e9; //注意相乘结果需要是long long型(卡这里wa了好久 else ans += (ll)j*(dist[j]+d[j]-d[i]); } printf("%lld\\n",ans); } return 0;} 差分约束系统还不会 $K$短路还不会 同余最短路还不会","link":"/2021/10/28/%E5%9B%BE%E8%AE%BA-2/"},{"title":"搜索题解","text":"本篇文章对一些搜索题目进行摘录。 赵子龙七进七出原题链接 题目描述 分析每隔三条边才能算作跳一步过去,一开始的想法是重新建图,利用最短路求解。但是重新建图并不方便,由于问最少前进步数,所以可以直接$bfs$.设置数组$mark[s][step]$,表示到点$s$时是隔了几步,对$s$的邻接点进行遍历,$step$变成$(step+1)\\pmod 3$.最后判断是否走到了终点,如果走到,步数除以$3$即可。 代码展开查看 >folded12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182#include <iostream>#include <cstring>#include <cmath>#include <algorithm>#include <queue>#include <stack>#include <vector>#include <unordered_map>#define INF 0x3f3f3f3fusing namespace std;typedef long long ll;typedef pair<int,int> PI;const int maxn = 1e5+10;const ll mod = 1e9+7;inline int read(){ int x = 0, f = 1; char ch = getchar(); while (ch < '0' || ch > '9') { if (ch == '-') f = -1; ch = getchar(); } while (ch >= '0' && ch <= '9') { x = (x << 3) + (x << 1) + (ch ^ 48); ch = getchar(); } return x * f;}int n,m;int s,t;vector<int>g[maxn];int mark[maxn][3]; //第走到这个点是哪步int dp[maxn][3]; //走了多少步void bfs(){ queue<PI> q; q.push(PI(s,0)); mark[s][0] = 1; //第0步走到的点才是能跳到的点 dp[s][0] = 0; //初始步数为0 while(!q.empty()) { PI now = q.front(); q.pop(); int u = now.first,step = now.second; for(auto v:g[u]) { int ss = (step+1)%3; if(mark[v][ss]==0) { mark[v][ss] = 1; //设置标记 dp[v][ss] = dp[u][step]+1; //步数加1 q.push(PI(v,ss)); } } }}int main(){ cin>>n>>m; while(m--) { int u,v; u = read(),v = read(); g[u].push_back(v); } cin>>s>>t; bfs(); if(mark[t][0]==0) { cout<<-1<<endl; return 0; } cout<<dp[t][0]/3; //第0步走到,步数除以三即可 return 0;}","link":"/2021/11/08/%E6%90%9C%E7%B4%A2%E9%A2%98%E8%A7%A3/"},{"title":"子数组和为k问题","text":"关于各种子数组的和小于(大于)或等于k的最长最短子数组或者求有多少个这样的子数组问题,可以分成两类。 当数组全是正数的时候,用双指针算法 当数组存在负数的时候,用前缀和+哈希表优化,也可使用单调队列优化 求一个数组中和为k的子数组的个数如果直接暴力做,复杂度为$O(n^2)$,不能通过。此题考虑前缀和+哈希表优化。我们定义$pre[i]$为$[0,i]$里所有的数的和,则$pre[i]$由$pre[i-1] + nums[i]$得来,即$$pre[i] = pre[i-1]+nums[i]$$那么找到一个子数组$[j…i]$和为$k$这个条件可以转化成$$pre[j-1]==pre[i]-k$$于是问题就变为考虑以$i$结尾的和为$k$的连续子数组时,只要统计有多少个前缀和为$pre[i]-k$的$pre[j]$即可。 我们建立哈希表$mp$,以和为键值,到当前位置出现次数为对应值,记录$pre[i]$出现的次数,从左往右边更新边计算答案,那么以$i$为结尾的答案$mp[pre[i]-k]$即可在$O(1)$时间内得到。最后答案即为所有$mp[pre[i]-k]$的和。 需要注意的是,因为我们从左往右边更新边计算时保证了$mp[pre[i]-k]$里记录的$pre[j]$的下标范围是$0 \\leq j \\leq i$.同时,由于$pre[i]$只与前一项$pre[j-1]$有关,因此我们不必建前缀和数组,直接使用$pre$变量来记录和即可。 123456789101112int subarraySum(vector<int>& nums,int k) { unordered_map<int,int> mp; int count = 0,pre = 0; mp[0] = 1; //当前前缀和就为k时是存在一个子数组的 for(auto x:nums){ pre += x; if(mp.find(pre-k) != mp.end()) count += mp[pre-k]; mp[pre]++; } return count;} 求一个数组中和为数组长度的子数组个数这个题是在训练群中听学长的面试官同事提的。咋一看比较没有思路,但是把数组每个元素减1,就变成了子数组和为0,和上题一样。 求一个数组中和为k的最长子数组长度仍然是前缀和+哈希表方式。这时哈希表以和为键,以右端点为对应值。当存在$mp[pre[i]-k]$时,也就是找到了一段和为$k$的子数组,这时比较$maxlen$和$i-mp[pre-k]$更新答案即可。 12345678910111213int maxlenOfArray(vector<int>& nums,int k) { unordered_map<int,int> mp; mp[0] = -1; int maxlen = 0,pre = 0; for(int i = 0;i < nums.size();i++) { pre += nums[i]; if(mp.find(pre-k)) maxlen = maxlen>i-mp[pre-k]?maxlen:i-mp[pre-k]; if(mp.find(pre) == mp.end()) mp[pre] = i//总是在第一次遇到这个和时插入map,使这个值尽可能早 } return maxlen;} 求一个01串中最长01数量相等的子串此题将$0$看做$-1$,则是找和为$0$的最长子串长度。 2021 CCPC Command Sequence题意一个机器人能上下左右移动,分别对应字符$UDLR$.给定一个机器人移动序列字符串,问有多少个子串可以使其按照子串的顺序来走能回到原点。 分析此题我们直接将每个字符看成一个数字,且上下互为相反数,左右互为相反数,那么就是找到这个数字序列有多少个和为$0$的子序列。依旧是前缀和+哈希表结构。需要注意的是,由于一个$2$可以由两个$-1$抵消,所以上下和左右代表的数字不能太接近。这里字符串长度最大为$10^5$,所以我们可以让$U$和$D$代表$1$和$-1$,让$L$和$R$代表$100000$和$-100000$。 代码123456789101112131415161718192021222324252627282930313233343536373839404142#include <iostream>#include <unordered_map>using namespace std;typedef long long ll;const int maxn = 1e5 + 10;int t, n;ll a[maxn];char s[maxn];int main(){ cin >> t; while (t--) { cin >> n; cin >> s + 1; for (int i = 1; i <= n; i++) { if (s[i] == 'U') a[i] = 1; if (s[i] == 'D') a[i] = -1; if (s[i] == 'L') a[i] = 100000; if (s[i] == 'R') a[i] = -100000; } unordered_map<ll, int> mp; mp[0] = 1; ll pre = 0, count = 0; for (int i = 1; i <= n; i++) { pre += a[i]; if (mp.find(pre) != mp.end()) count += mp[pre]; mp[pre]++; } cout << count << endl; } return 0;} 数学?题目描述给你一个长度为$n$的数组$a$和一个正整数$k$,问$a$有多少个和$\\geq k$的连续子序列。 分析如果连续子序列$[l,r]$的和已经$\\geq k$,那么从$l$到$r+1$、从$l$到$r+2$、$\\cdots$、从$l$到$n$的和都$\\geq k$。这样的子序列有$n-r+1$个。因此,我们可以枚举$l$从$1$到$n$,对于每一个$l$,找到最小的$r$使得$\\sum[l,r]\\geq k$,那么从$l$开始的符合条件的子序列就有$n-r+1$个。 对于给定的左边界$l$,如何快速找到最小满足条件的$r$,可以使用滑动窗口的办法。 我们得到上一个$l$的最小$r$之后,记录下这个最小的$r$和$[l,r]$的和。当$l$变成$l+1$时,让上次的和减去$a[l]$,得到的是$[l+1,r]$的和。 如果这个和仍大于等于$k$,那么$r$仍然是最小的$r$,更新答案即可。 否则$r$向右移动变成$r+1$,和也加上$a[r+1]$,直到和又大于等于$k$,此时的$r$又是最小的$r$。 上述过程中,一旦$r$超过数组范围就结束了。 $r$从$1$移到了$n$,时间复杂度为$O(n)$。 代码123456789101112131415161718192021222324252627#include <iostream>using namespace std;typedef long long ll;ll a[100010];int main() { int n; ll k; cin >> n >> k; for (int i = 0; i < n; i++) { scanf("%lld", &a[i]); } ll ans = 0; // 答案 ll left = 0, right = 0; // l和r初始值都是0 ll sum = a[0]; // sum代表[left, right]的和 while (right < n) { if (sum >= k) { // sum[l, r] >= k ans += n - right; sum -= a[left++]; // 枚举下一个l(l++),相应地sum也要减去不在范围内的a[l] } else { // sum[l, r] < k,还没找到最小的r sum += a[++right]; // r右移并累加入总和 } } cout << ans << endl; return 0;}","link":"/2021/10/21/%E5%AD%90%E6%95%B0%E7%BB%84%E5%92%8C%E4%B8%BAk%E9%97%AE%E9%A2%98/"},{"title":"线段树与树状数组","text":"线段树和树状数组是很常用的数据结构,用来处理区间问题,包括区间的修改和查询。 线段树线段树,顾名思义,是将区间分成一段一段来进行区间操作。对于每个子节点,都表示整个序列的一段子区间;对于叶子节点而言,都表示序列中单个元素信息;子节点不断向自己的父亲节点传递信息,而父节点存储的信息是它每一个子节点信息的整合。 线段树可以维护支持结合律的数据,比如加和,乘法,最大/最小值。 如何进行分段? 考虑将区间$[l,r]$分成两半$[l,mid]、[mid+1,r]$,相当于将区间对半分,对于每个区间都这样分段,最终段数为$O(n\\log n)$. 当我们对区间$[l,r]$进行操作时,从最大的区间开始,从此去找对应操作的区间。 $[l,r]$包含在当前节点的左区间内,接着往左区间走 $[l,r]$包含在当前节点的右区间内,接着往右区间走 $[l,r]$跨过当前节点区间的中点,左边操作$[l,mid]$,右边操作$[mid+1,r]$ 如此反复,直到找到的区间与操作区间一致,就进行操作。 线段树大致图像如下: 当涉及到区间的修改,如加减、乘除时,我们可以在找到的区间上打上懒标记($lazy tag$),这样不用每次往下遍历整棵线段树。在进行修改和查询时要注意懒标记的下传,还要向上更新节点维护的值。 线段树1 懒标记的优先级当区间的修改包括了加和乘时,需要设置两个标记,$add$加标记、$mul$乘标记。而这时候需要注意优先级问题。 先进行乘法后进行加法。 如果我维护的值是$a$,我的懒标记有$+b$和$\\times c$,可以发现$(a+b)\\times c \\neq a\\times c+b$. 而在记录懒标记时,加法和乘法两种标记放到一起,需要确定一个优先级。 我们分析一下两种顺序: 先加后乘:$(a+b)\\times c = a\\times c+b\\times c$ 先乘后加:$a\\times c+b$ 上面先加后乘的式子相当于下面的式子,在加法上面多乘了一个$c$ 所以,只要是先加后乘的式子,只要在$b$上$\\times c$就可以转化为先乘后加的式子 具体操作是在添加乘法标记的时候,先将加法标记$\\times c$即可 懒标记下传推导在传递懒标记$pushdown$时 假设当前节点是$o$,$add[o]$是当前节点的加法标记,$mul[o]$是当前节点的乘法标记,$sum[o]$是当前节点维护的和,$ls$是左儿子的编号,$rs$是右儿子的编号。这里当前节点的值已经维护好,儿子还没维护好。 以左儿子为例,根据先乘后加的顺序,给左儿子乘上自己的乘法标记,再加上自己的加法标记。 $$sum[ls] = sum[ls]\\times mul[o]+add[o]\\times (r-l+1)$$ 这样,左儿子的$sum$值就维护好了。那么如果儿子有懒标记呢? 如果儿子有懒标记,它的懒标记要维护一个值$s$,它维护后的值$s’$应该是 $$s’ = s\\times mul[ls]+add[ls]$$ 现在又要给它加上父节点$o$的懒标记,那么维护后的值应该是$$\\begin{equation}\\begin{split}s’’&=s’\\times mul[o] + add[o] \\ &=(s\\times mul[ls]+add[ls])\\times mul[o]+add[o] \\ &=s\\times mul[ls]\\times mul[o]+add[ls]\\times mul[o]+add[o]\\end{split}\\end{equation}$$如果$mul[ls]’,add[ls]’$是维护后的懒标记,我们就知道了懒标记应该怎么维护 $$mul[ls]’ = mul[ls]\\times mul[o]$$ $$add[ls]’=add[ls]\\times mul[o]+add[o]$$ 在维护懒标记时,乘法标记乘以父节点的乘标记,加法标记先乘以父节点的乘标记,再加上父节点的加标记即可。 线段树2 模板展开查看代码 >folded123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177#include <iostream>#include <cstring>#include <cmath>#include <algorithm>#include <queue>#include <stack>#include <vector>#define INF 0x3f3f3f3fusing namespace std;typedef long long ll;const int maxn = 1e5 + 10;inline ll read(){ ll x = 0, f = 1; char ch = getchar(); while (ch < '0' || ch > '9') { if (ch == '-') f = -1; ch = getchar(); } while (ch >= '0' && ch <= '9') { x = (x << 3) + (x << 1) + (ch ^ 48); ch = getchar(); } return x * f;}struct node{ int l, r; ll sum, add, mul = 1; //该点维护的和,加标记,乘标记初始为1} tree[maxn << 2];ll a[maxn];ll n, m, p;inline int ls(int pos) { return pos << 1; }inline int rs(int pos) { return pos << 1 | 1; }//向上更新节点维护的和void pushup(int pos){ tree[pos].sum = (tree[ls(pos)].sum + tree[rs(pos)].sum) % p;}void build(int pos, int l, int r) //当前节点,操作区间{ tree[pos].l = l, tree[pos].r = r; if (l == r) { tree[pos].sum = a[l] % p; return; } int mid = l + r >> 1; build(ls(pos), l, mid); build(rs(pos), mid + 1, r); pushup(pos);}//向下传递懒标记void pushdown(int pos){ if (tree[pos].mul != 1) { //更新两个标记与节点维护的和,直接乘 (tree[ls(pos)].mul *= tree[pos].mul) %= p; (tree[rs(pos)].mul *= tree[pos].mul) %= p; (tree[ls(pos)].add *= tree[pos].mul) %= p; (tree[rs(pos)].add *= tree[pos].mul) %= p; (tree[ls(pos)].sum *= tree[pos].mul) %= p; (tree[rs(pos)].sum *= tree[pos].mul) %= p; //已经传递,修改为1 tree[pos].mul = 1; } if (tree[pos].add) { (tree[ls(pos)].add += tree[pos].add) % p; (tree[rs(pos)].add += tree[pos].add) % p; //左子树的sum值加上 节点数*懒标记 (tree[ls(pos)].sum += (tree[ls(pos)].r - tree[ls(pos)].l + 1) * tree[pos].add % p) %= p; //右子树的sum值加上 节点数*懒标记 (tree[rs(pos)].sum += (tree[rs(pos)].r - tree[rs(pos)].l + 1) * tree[pos].add % p) %= p; //已经传递,修改为0 tree[pos].add = 0; }}//加法更新void updateAdd(int pos,int l,int r,int k){ if(tree[pos].l==l&&tree[pos].r==r){ //更新该节点维护的和 (tree[pos].sum += (r-l+1) * k % p) %= p; //更新该节点懒标记 (tree[pos].add += k) %= p; return; } //注意要先pushdown!!! pushdown(pos); int mid = tree[pos].l+tree[pos].r>>1; //只在左儿子节点更新 if(r<=mid) updateAdd(ls(pos),l,r,k); //只在右儿子节点更新 else if(l>mid) updateAdd(rs(pos),l,r,k); //在左右儿子节点更新 else updateAdd(ls(pos),l,mid,k),updateAdd(rs(pos),mid+1,r,k); //最后pushup!!! pushup(pos);}//乘法更新void updateMul(int pos,int l,int r,int k){ if(tree[pos].l==l&&tree[pos].r==r){ //更新该节点维护的和 (tree[pos].sum *= k) %= p; //更新该节点懒标记 (tree[pos].add *= k) %= p; (tree[pos].mul *= k) %= p; return; } //注意要先pushdown!!! pushdown(pos); int mid = tree[pos].l+tree[pos].r>>1; //只在左儿子节点更新 if(r<=mid) updateMul(ls(pos),l,r,k); //只在右儿子节点更新 else if(l>mid) updateMul(rs(pos),l,r,k); //在左右儿子节点更新 else updateMul(ls(pos),l,mid,k),updateMul(rs(pos),mid+1,r,k); //最后pushup!!! pushup(pos);}ll query(int pos,int l,int r){ if(tree[pos].l==l&&tree[pos].r==r){ return tree[pos].sum; } //注意先pushdown!!! pushdown(pos); int mid = tree[pos].l+tree[pos].r>>1; //只在左儿子节点查询 if(r<=mid) return query(ls(pos),l,r); //只在右儿子节点查询 else if(l>mid) return query(rs(pos),l,r); //在左右儿子节点查询 else return (query(ls(pos),l,mid)+query(rs(pos),mid+1,r))%p;}int main(){ cin>>n>>m>>p; for(int i=1;i<=n;i++) a[i] = read(); build(1,1,n); while(m--) { int opt; ll x,y,k; opt = read(),x = read(),y = read(); if(opt==1){ k = read(); updateMul(1,x,y,k); } else if(opt==2){ k = read(); updateAdd(1,x,y,k); } else if(opt==3){ printf("%lld\\n",query(1,x,y)); } } return 0;} 树状数组单点修改,区间查询树状数组用于维护前缀和,可以单点修改。 大致图像如下 每个节点维护值的长度是该节点二进制最低位1代表的值。 树状数组中$lowbit$函数是得到一个数的最低位1代表的值,比如$lowbit(5)=1,lowbit(4)=4$. 如果改变$x$位置的值,就加上该位置的$lowbit$,一直加到$n$,就维护了树状数组。 查询时反过来,从$x$位置开始,减去当前位置的$lowbit$,一直减到$1$,就得到$x$位置的前缀和。 树状数组1 12345678910111213141516171819202122232425262728293031323334353637383940414243int a[maxn],n,m;ll tree[maxn]; //树状数组int lowbit(int x) { return x & (-x); }void add(int x,int k){ for(int i=x;i<=n;i+=lowbit(i)) tree[i] += k;}ll query(int x){ ll ans = 0; for(int i=x;i;i-=lowbit(i)) ans += tree[i]; return ans;}int main(){ cin>>n>>m; for(int i=1;i<=n;i++) a[i] = read(); for(int i=1;i<=n;i++) add(i,a[i]); int opt,x,y,k; while(m--) { opt = read(); if(opt==1) { x = read(),k=read(); add(x,k); } else if(opt == 2) { x = read(),y = read(); printf("%d\\n",query(y)-query(x-1)); } } return 0;} 区间修改,单点查询区间修改用到差分数组的知识,若原数组为$a$,其差分数组为$b$,则$a[i] = b[1]+b[2]+\\cdots +b[i]$. 若要修改原数组$[l,r]$区间上的值,比如都加$2$,只需要在差分数组中$b[l]$位置$+2$,$b[r+1]$位置$-2$即可。 由于树状数组维护前缀和,所以用树状数组维护原数组,用差分数组建树状数组,查询时直接查询即可。 树状数组2 1234567891011121314151617181920212223242526272829303132333435363738394041424344int a[maxn],n,m;ll tree[maxn]; //树状数组int lowbit(int x) { return x & (-x); }void add(int x,int k){ for(int i=x;i<=n;i+=lowbit(i)) tree[i] += k;}ll query(int x){ ll ans = 0; for(int i=x;i;i-=lowbit(i)) ans += tree[i]; return ans;}int main(){ cin>>n>>m; for(int i=1;i<=n;i++) a[i] = read(); for(int i=1;i<=n;i++) add(i,a[i]-a[i-1]); int opt,x,y,k; while(m--) { opt = read(); if(opt==1) { x = read(),y = read(),k=read(); add(x,k); add(y+1,-k); } else if(opt == 2) { x = read(); printf("%d\\n",query(x)); } } return 0;} 总结虽说只是模板,但是线段树这两题还是写了很久,代码量巨大。不过也比之前理解地更清楚了,虽说线段树是很有用的工具,但对于我来讲估计用上它的概率不大,用上的也写不出(太菜了)。","link":"/2021/11/03/%E7%BA%BF%E6%AE%B5%E6%A0%91%E4%B8%8E%E6%A0%91%E7%8A%B6%E6%95%B0%E7%BB%84/"},{"title":"JDBC技术","text":"本篇文章记录学习JDBC的内容,虽然这部分最后都被封装起来,但是了解如何建立连接,执行sql语句,关闭连接很有必要。 $JDBC$概述 $JDBC$(java Database Connectivity)是一个独立于特定数据库管理系统、通用的SQL数据库存取和操作的公共接口(一组API),定义了用来访问数据库的标准java类库(java.sql,javax.sql)使用这些类库可以以一种标准的方法方便地访问数据库资源。 JDBC为访问不同的数据库提供了一种统一的途径。 有了JDBC之后,java程序访问数据库的方式如图 JDBC程序编写步骤 获取数据库连接$Driver$接口 java.sql.Driver接口是所有 JDBC 驱动程序需要实现的接口。这个接口是提供给数据库厂商使用的,不同数据库 厂商提供不同的实现。 在程序中不需要直接去访问实现了 Driver 接口的类,而是由驱动程序管理器类(java.sql.DriverManager)去调用 这些Driver实现。 mysql的驱动:com.mysql.cj.jdbc.Driver(mysql8.0之后) 加载与注册驱动 加载驱动::加载 JDBC 驱动需调用 Class 类的静态方法 forName(),向其传递要加载的 JDBC 驱动的类名 Class.forName("com.mysql.cj.jdbc.Driver") 注册驱动:DriverManager 类是驱动程序管理器类,负责管理驱动程序 使用DriverManager.registerDriver(com.mysql.jdbc.Driver)来注册驱动 通常不显式调用DriverManager 类的 registerDriver() 方法来注册驱动程序类的实例,因为 Driver 接口 的驱动程序类都包含了静态代码块,在这个静态代码块中,会调用 DriverManager.registerDriver() 方法 来注册自身的一个实例。下图是MySQL的Driver实现类的源码: 12345678// Register ourselves with the DriverManagerstatic { try { java.sql.DriverManager.registerDriver(new Driver()); } catch (SQLException E) { throw new RuntimeException("Can't register driver!"); }} $URL$ JDBC URL 用于标识一个被注册的驱动程序,驱动程序管理器通过这个 URL 选择正确的驱动程序,从而建立到 数据库的连接。 JDBC URL的标准由三部分组成,各部分间用冒号分隔。 jdbc:子协议:子名称 协议:JDBC URL中的协议总是jdbc 子协议:子协议用于标识一个数据库驱动程序 子名称:一种标识数据库的方法。子名称可以依不同的子协议而变化,用子名称的目的是为了定位数据库 提供足够的信息。包含主机名(对应服务端的ip地址),端口号,数据库名 MySQL的连接URL编写方式: jdbc:mysql://主机名称:mysql服务端口号/数据库名称?参数=值&参数=值 jdbc:mysql://localhost:3306/test?useSSL=false&serverTimezone=UTC 连接方式1234567891011121314151617181920212223242526public static Connection getConnection(){ Connection conn = null; Properties pros = new Properties(); InputStream is = ClassLoader.getSystemClassLoader().getResourceAsStream("jdbc.properties"); try { pros.load(is); } catch (IOException e) { e.printStackTrace(); } String url = pros.getProperty("url"); String user = pros.getProperty("user"); String password = pros.getProperty("password"); String driverName = pros.getProperty("driverName"); try { /** * 加载驱动,这一句也可以删除,在META-INF下的services的java.sql.Driver定义了驱动 * 但是不建议删除 */ Class.forName(driverName); //获取连接 conn = DriverManager.getConnection(url,user,password); } catch (Exception e) { e.printStackTrace(); } return conn;} 配置文件properties 1234user=rootpassword=123456url=jdbc:mysql://localhost:3306/test?useSSL=false&serverTimezone=UTCdriverName=com.mysql.cj.jdbc.Driver CRUD操作操作和访问数据库 数据库连接被用于向数据库服务器发送命令和 SQL 语句,并接受数据库服务器返回的结果。其实一个数据库连 接就是一个Socket连接。 在 java.sql 包中有 3 个接口分别定义了对数据库的调用的不同方式: Statement:用于执行静态 SQL 语句并返回它所生成结果的对象。 PrepatedStatement:SQL 语句被预编译并存储在此对象中,可以使用此对象多次高效地执行该语句。 CallableStatement:用于执行 SQL 存储过程 使用$Statement$操作数据表的弊端 存在拼串操作,繁琐 存在SQL注入问题 SQL 注入是利用某些系统没有对用户输入的数据进行充分的检查,而在用户输入数据中注入非法的 SQL 语句段 或命令(如select * from tablename where username=''or true or'' and password=''),在输入or true or后sql语句结构发生了变化,变为了或的逻辑关系,不管用户名和密码是否匹配该式的返回值永远为true。 $PreparedStatement$的使用 Preparement样式为select*from tablename where username=? and password=? SQL语句会在得到用户的输入之前先用数据库进行预编译,这样的话不管用户输入什么用户名和密码的判断始终都是并的逻辑关系,防止了SQL注入 可以通过调用 Connection 对象的 preparedStatement(String sql) 方法获取 PreparedStatement 对象 PreparedStatement 接口是 Statement 的子接口,它表示一条预编译过的 SQL 语句 preparedStatement使代码的可读性和可维护性提高。 PreparedStatement 能最大可能提高性能: DBServer会对预编译语句提供性能优化。因为预编译语句有可能被重复调用,所以语句在被DBServer的 编译器编译后的执行代码被缓存下来,那么下次调用时只要是相同的预编译语句就不需要编译,只要将参 数直接传入编译过的语句执行代码中就会得到执行。 在statement语句中,即使是相同操作但因为数据内容不一样,所以整个语句本身不能匹配,没有缓存语句的意 义.事实是没有数据库会对普通语句编译后的执行代码缓存。这样每执行一次都要对传入的语句编译一次。 通用的增删改查操作更新操作(增、删、改)123456789101112131415//统一的增删改操作(考虑事务)public static void update(Connection conn,String sql,Object ...args){ PreparedStatement ps = null; try { ps = conn.prepareStatement(sql); for(int i=0;i<args.length;i++){ ps.setObject(i+1,args[i]); } ps.executeUpdate(); } catch (SQLException e) { e.printStackTrace(); } finally { JDBCUtils.closeResource(null,ps); }} 查询操作1234567891011121314151617181920212223242526272829public static <T> T getInstance(Connection conn,Class<T> clazz,String sql,Object ...args){ PreparedStatement ps = null; ResultSet resultSet = null; try { ps = conn.prepareStatement(sql); for(int i=0;i<args.length;i++){ ps.setObject(i+1,args[i]); } resultSet = ps.executeQuery(); ResultSetMetaData rsmd = resultSet.getMetaData(); int columnCount = rsmd.getColumnCount(); if(resultSet.next()){ T t = clazz.newInstance(); for(int i=1;i<=columnCount;i++){ String columnLabel = rsmd.getColumnLabel(i); Object value = resultSet.getObject(columnLabel); Field field = clazz.getDeclaredField(columnLabel); field.setAccessible(true); field.set(t,value); } return t; } } catch (Exception e) { e.printStackTrace(); } finally { JDBCUtils.closeResource(null,ps,resultSet); } return null;} 资源的释放 释放ResultSet, Statement,Connection。 数据库连接(Connection)是非常稀有的资源,用完后必须马上释放,如果Connection不能及时正确的关闭将 导致系统宕机。Connection的使用原则是尽量晚创建,尽量早的释放。 操作$Blob$类型字段 MySQL中,BLOB是一个二进制大型对象,是一个可以存储大量数据的容器,它能容纳不同大小的数据。 插入BLOB类型的数据必须使用PreparedStatement,因为BLOB类型的数据无法使用字符串拼接写的。 MySQL的四种BLOB类型 类型 最大大小(单位:字节) TinyBlob 255 Blob 65K MediumBlob 16M LongBlob 4G 在mysql的安装目录下,可以在my.ini文件加上如下的配置参数: max_allowed_packet=16M,并重启数据库 插入$Blob$类型123FileInputStream fis = new FileInputStream("kele.jpg");ps.setBlob(4, fis);ps.executeUpdate(); 读取$Blob$类型12345678Blob photo = resultSet.getBlob("photo");is = photo.getBinaryStream(); //得到输入流fos = new FileOutputStream("mei.jpg");byte[] cbuf = new byte[1024];int len = 0;while ((len = is.read(cbuf)) != -1) { fos.write(cbuf, 0, len);} 数据库事务$JDBC$处理事务 事务:一组逻辑操作单元,使数据从一种状态变换到另一种状态。 事务处理:保证所有事务都作为一个工作单元来执行,即使出现了故障,都不能改变这种执行方 式。当在一个事务中执行多个操作时,要么所有的事务都被提交(commit),那么这些修改就永久地保存下来; 要么数据库管理系统将放弃所作的所有修改,整个事务回滚(rollback)到最初状态。 数据一旦提交,就不可回滚。 数据什么时候意味着提交? 当一个连接对象被创建时,默认情况下是自动提交事务:每次执行一个 SQL 语句时,如果执行成功,就会 向数据库自动提交,而不能回滚。 关闭数据库连接,数据就会自动的提交。如果多个操作,每个操作使用的是自己单独的连接,则无法保证 事务。即同一个事务的多个操作必须在同一个连接下。 JDBC程序中为了让多个 SQL 语句作为一个事务执行: 调用 Connection 对象的 setAutoCommit(false); 以取消自动提交事务。 在所有的 SQL 语句都成功执行后,调用 commit(); 方法提交事务。 在出现异常时,调用rollback方法,回滚事务。 $MySql$中设置隔离级别设置连接隔离级别123456//获取当前连接的隔离级别System.out.println(conn.getTransactionIsolation());//设置当前连接的隔离级别conn.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);//取消自动提交conn.setAutoCommit(false); 数据库连接池 JDBC 的数据库连接池使用 javax.sql.DataSource 来表示,DataSource 只是一个接口,该接口通常由服务器 (Weblogic, WebSphere, Tomcat)提供实现,也有一些开源组织提供实现,常用连接池: **C3P0 **是一个开源组织提供的一个数据库连接池,速度相对较慢,稳定性还可以。hibernate官方推荐使用 DBCP 是Apache提供的数据库连接池。tomcat 服务器自带dbcp数据库连接池。速度相对c3p0较快,但因 自身存在BUG,Hibernate3已不再提供支持。 Druid 是阿里提供的数据库连接池,是集DBCP 、C3P0 、Proxool 优点于一身的数据库连接池。 DataSource 通常被称为数据源,它包含连接池和连接池管理两个部分,习惯上也经常把 DataSource 称为连接 池。 DataSource用来取代DriverManager来获取Connection,获取速度快,同时可以大幅度提高数据库访问速 度。 数据源和数据库连接不同,数据源无需创建多个,它是产生数据库连接的工厂,因此整个应用只需要一个 数据源即可。 当数据库访问结束后,程序还是像以前一样关闭数据库连接:conn.close(); 但conn.close()并没有关闭数 据库的物理连接,它仅仅把数据库连接释放,归还给了数据库连接池。 $Druid$数据库连接池123456789101112131415161718192021private static DataSource dataSource = null;static{ try { Properties pros = new Properties(); InputStream is = ClassLoader.getSystemClassLoader().getResourceAsStream("druid.properties"); pros.load(is); //静态代码块中,只创建一个连接池 dataSource = DruidDataSourceFactory.createDataSource(pros); } catch (Exception e) { e.printStackTrace(); }}public static Connection getConnectionOfDrud(){ Connection conn = null; try { conn = dataSource.getConnection(); } catch (SQLException e) { e.printStackTrace(); } return conn;}druid.properties配置文件 Apache-DBUtils实现CRUD操作主要API的使用DbUtils DbUtils :提供如关闭连接、装载JDBC驱动程序等常规工作的工具类,里面的所有方法都是静态的。主要方法 如下: public static void closeQuietly(…): 这一类方法不仅能在Connection、Statement和ResultSet为NULL情 况下避免关闭,还能隐藏一些在程序中抛出的SQLEeception。 public static void rollback(Connection conn)throws SQLException:允许conn为null,因为方法内部做 了判断 QueryRunner类 该类简单化了SQL查询,它与ResultSetHandler组合在一起使用可以完成大部分的数据库操作,能够大大减少 编码量。 QueryRunner类的主要方法: 更新:public int update(Connection conn, String sql, Object… params) throws SQLException:用来执行 一个更新(插入、更新或删除)操作。 查询:public Object query(Connection conn, String sql, ResultSetHandler rsh,Object… params) throws SQLException:执行一个查询操作,在这个查询中,对象数组中的每个元素值被用来作为查询语句 的置换参数。该方法会自行处理 PreparedStatement 和 ResultSet 的创建和关闭。 ResultSetHandler接口及实现类 该接口用于处理 java.sql.ResultSet,将数据按要求转换为另一种形式。 ResultSetHandler 接口提供了一个单独的方法:Object handle (java.sql.ResultSet rs)。 接口的主要实现类: BeanHandler:将结果集中的第一行数据封装到一个对应的JavaBean实例中。 BeanListHandler:将结果集中的每一行数据都封装到一个对应的JavaBean实例中,存放到List里。 MapHandler:将结果集中的第一行数据封装到一个Map里,key是列名,value就是对应的值。 MapListHandler:将结果集中的每一行数据都封装到一个Map里,然后再存放到List。 ScalarHandler:查询单个值对象(查询类似于最大的,最小的,平均的,总和,个数相关的数据) 在使用这些接口实现类时一定要给JavaBean添加setter方法,因为其重写的hand方法内部会调用赋值。 crud测试1234567891011121314151617//BeanHandler是ResultHandler的一个实现类,用于封装表中的一条记录。@Testpublic void testQuery1(){ QueryRunner runner = new QueryRunner(); Connection conn = JDBCUtils.getConnectionOfDrud(); String sql = "select name,email,birth from customers where id=?"; BeanHandler<Customer> handler = new BeanHandler<>(Customer.class); try { Customer customer = runner.query(conn, sql, handler, 28); System.out.println(customer); } catch (SQLException e) { e.printStackTrace(); } finally { JDBCUtils.closeResource(conn,null); }}//BeanListHandler同理,返回的就是一个List 123456789101112131415161718/** * ScalarHandler:ResultSetHandler的一个实现类,返回特殊的值,如聚集函数 */@Testpublic void testQuery5(){ QueryRunner runner = new QueryRunner(); Connection conn = JDBCUtils.getConnectionOfDrud(); String sql = "select max(birth) from customers"; ScalarHandler handler = new ScalarHandler(); try { Date date = (Date) runner.query(conn, sql, handler); System.out.println(date); } catch (SQLException e) { e.printStackTrace(); } finally { JDBCUtils.closeResource(conn,null); }} 自定义ResultSetHandler实现类如果不想使用自带的实现类,还可以自己实现ResultSetHandler类,只需重写handle方法处理结果集即可 1234567891011121314ResultSetHandler<User> handler = new ResultSetHandler<User>() { @Override public User handle(ResultSet resultSet) throws SQLException { if(resultSet.next()) { int id = resultSet.getInt("id"); String username = resultSet.getString("username"); String password = resultSet.getString("password"); String email = resultSet.getString("email"); User user = new User(id, username, password, email); return user; } return null; }};","link":"/2021/12/19/JDBC%E6%8A%80%E6%9C%AF/"},{"title":"Servlet学习","text":"本篇文章记录学习Servlet技术的内容。 什么是Servlet Servlet是JavaEE的一个接口,是JavaWeb的三大组件之一。三大组件分别是:Servlet、Filter过滤器、Listener监听器。 处理请求和发送响应的过程是由Servlet程序来完成的,并且Servlet是为了解决实现动态页面而衍生的东西。 Servlet是运行在服务器上的一个java程序,它可以接收客户端发送过来的请求,并发送响应数据给客户端。 Tomcat和Servlet的关系 Tomcat是Web应用服务器,是一个Servlet/JSP容器。Tomcat作为Servlet容器,负责处理客户端发送的请求,将请求传给Servlet,并将Servlet的响应传回给客户端。 从http协议中的请求和响应可以知道,浏览器发出的请求是一个请求文本,浏览器接收到的也是一个响应文本。 从上图可以看出是如何请求和响应的。 ①:Tomcat接收客户端的http请求文本并解析,然后封装成HttpServletRequest类型的request对象,所有的http头数据都可以通过request对象调用对应方法查到。 ②:Tomcat同时会将响应的信息封装成HttpServletResponse类型的response对象,通过设置response的属性可以控制输出到浏览器中的内容,然后将response交给Tomcat,Tomcat会将其变成响应文本的格式发送给浏览器。 Java Servlet API是Servlet容器(Tomcat)和Servlet之间的接口,它定义了Servlet的各种方法,还定义了Tomcat传送给Servlet的对象类,其中最重要的是ServletRequest和ServletResponse。在编写Servlet程序时,需要实现Servlet接口,实现里面的方法。 实现Servlet 编写一个类去实现Servlet接口 实现service方法,处理请求,并响应数据。 在web.xml中配置servlet程序的访问地址。需要让浏览器知道发出的请求到达哪个servlet,也就是让tomcat将封装好的request找到对应的servlet让其使用。 实现的servlet类 1234567public class HelloServlet implements Servlet{ @Override public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException { System.out.println("HelloServlet被访问了"); } //其它实现的方法省略} web.xml中需要配置以下四个信息。 12345678910111213141516<servlet> <!--servlet-name给程序起一个别名--> <servlet-name>HelloServlet</servlet-name> <!--servlet的全类名--> <servlet-class>servlet.HelloServlet</servlet-class></servlet><!--servlet-mapping标签给servlet程序配置访问地址--><servlet-mapping> <!--告诉服务器,当前配置的地址给那个servlet程序使用,和上面的别名一致--> <servlet-name>HelloServlet</servlet-name> <!--配置访问地址 / 斜杠表示地址为:http://ip:port/工程路径 /hello 表示地址为:http://ip:port/工程路径/hello 这个地址就表示访问到HelloServlet这个类了--> <url-pattern>/hello</url-pattern></servlet-mapping> url地址到Servlet程序的访问 Servlet的生命周期 执行Servlet构造器方法 执行init初始化方法 前两步是在第一次访问时创建Servlet程序时调用。 3.执行service方法(这一步每次访问都会调用)。 4.执行destroy销毁方法。(在web工程停止时调用)。 load-on-startup作用 load-on-startup 元素标记容器是否应该在web应用程序启动的时候就加载这个servlet,(实例化并调用其init()方法)。 它的值必须是一个整数,表示servlet被加载的先后顺序。 如果该元素的值为负数或者没有设置,则容器会当Servlet被请求时再加载。 如果值为正整数或者0时,表示容器在应用启动时就加载并初始化这个servlet,值越小,servlet的优先级越高,就越先被加载。值相同时,容器就会自己选择顺序来加载。 12写在<servlet></servlet>中<load-on-startup>1</load-on-startup> 通过继承HttpServlet实现Servlet程序一般不会直接实现Servlet类实现Servlet程序,而是去继承HttpServlet类。在HttpServlet继承类中,只需要根据自己的需要去重写doGet和doPost方法既可。 查看HttpServlet类的继承结构,发现它继承于GenericServlet类。而GenericServlet类实现了Servlet类,对很多方法做了空实现。并且持有一个ServletConfig类的引用,对ServletConfig的使用定义了一些方法。 Servlet接口内容 可以看到,接口内有Servlet生命周期的三个关键方法,init、service、destroy,还有一个重要的getServletConfig方法来获取ServletConfig对象,ServletConfig对象可以获取到Servlet的信息,ServletName、ServletContext、InitParameter、InitParameterNames,通过查看ServletConfig这个接口就可以知道。 ServletConfig接口又可以获取ServletContext对象,具体内容下面讲解。 GenericServlet类内容 这个类实现了Servlet和ServletConfig的9个方法。其中init方法有两个,一个是带参数ServletConfig的,一个是无参的方法。 12345678910private transient ServletConfig config;public ServletConfig getServletConfig() { return this.config;}public void init(ServletConfig config) throws ServletException { this.config = config; this.init();}public void init() throws ServletException {} 通过这几个方法一起看,首先看init(ServletConfig config)方法,因为只有init(ServletConfig config)中带有ServletConfig对象,为了方便能够在其他地方也能直接使用ServletConfig对象,而不仅仅局限在init(ServletConfig config)方法中,所以创建一个私有的成员变量config,在init(ServletConfig config)方法中就将其赋值给config,然后通过getServletConfig()方法就能够获取ServletConfig对象了。而当想在init方法中做一些别的事情,就需要重写init(ServletConfig config)方法,这样的话在重写的方法中必须调用父类的init(ServletConfig config)方法,即super.init(ServletConfig config),否则GenericServlet类中的成员变量config会一直是null值,无法再得到ServletConfig对象。增加一个init()方法,那么需要在init内初始化别的数据时,就只需要重写init()方法,不需要覆盖init(ServletConfig config)了,仍然可以得到config对象。 1public abstract void service(ServletRequest var1, ServletResponse var2) throws ServletException, IOException; 这是GenericServlet类中的service方法,是一个抽象方法,那么也就是还有一个子类继承它,即HttpServlet类。这个类实现了service方法的各种细节,通过类名也可知道和http协议有关系了。 HttpServlet类内容 包含各种常量,比如GET和POST请求,还有各种请求的处理方法。 service(ServletRequest req, ServletResponse res)方法 123456789101112public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException { HttpServletRequest request; HttpServletResponse response; try { request = (HttpServletRequest)req; response = (HttpServletResponse)res; } catch (ClassCastException var6) { throw new ServletException("non-HTTP request or response"); } this.service(request, response);} 该方法只是把ServletRequest和ServletResponse这两个对象强转为HttpServletRequest和HttpServletResponse对象。之后,在调用service(HttpServletRequest req, HttpServletResponse resp)方法,这个方法就是判断浏览器过来的请求方式是哪种,每种的处理方式不一样,我们常用的就是get,post,并且,我们处理的方式可能有很多的内容,所以,在该方法内会将get,post等其他5种请求方式提取出来,变成单个的方法,然后我们需要编写servlet时,就可以直接重写doGet或者doPost方法就行了,而不是重写service方法,更加有针对性。 所以,以后编写servlet程序时,只需要继承于HttpServlet类,只要重写两个方法,doGet()和doPost()。 ServletConfig类是servlet程序的配置信息类,由Tomcat负责创建,为每一个Servlet程序都创建一个ServletConfig对象。 三大作用 可以获取Servlet程序的别名servlet-name的值。(getServletName()) 获取初始化参数init-param(只是该Servlet下的初始化参数)。(getInitParameter()) 获取ServletContext对象。(getServletContext()) 12345678910111213<servlet> <!--servlet-name给程序起一个别名--> <servlet-name>HelloServlet</servlet-name> <!--servlet的全类名--> <servlet-class>servlet.HelloServlet</servlet-class> <!--配置参数--> <init-param> <!--参数名--> <param-name>username</param-name> <!--参数值--> <param-value>root</param-value> </init-param></servlet> 1234567891011121314151617@Overrideprotected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { ServletConfig servletConfig = getServletConfig(); System.out.println("servlet2的get请求"); //获取Servlet程序别名 System.out.println(servletConfig.getServletName()); //获取初始化参数init-param System.out.println(servletConfig.getInitParameter("username")); System.out.println(servletConfig.getInitParameter("url")); //获取ServletContext对象 System.out.println(servletConfig.getServletContext()); Enumeration<String> initParameterNames = servletConfig.getInitParameterNames(); while(initParameterNames.hasMoreElements()){ String name = initParameterNames.nextElement(); System.out.println(name+":"+servletConfig.getInitParameter(name)); }} ServletContext类 ServletContext 是一个接口,它表示 Servlet 上下文对象 一个 web 工程,只有一个 ServletContext 对象实例。 ServletContext 对象是一个域对象。 ServletContext 是在 web 工程部署启动的时候创建。在 web 工程停止的时候销毁。 什么是域对象? 域对象,是可以像 Map 一样存取数据的对象,叫域对象。 这里的域指的是存取数据的操作范围,整个 web 工程。 四大作用 获取 web.xml 中配置的上下文参数 context-param.全局初始化参数,每个Servlet都可以得到该值(getServletContext()) 获取当前的工程路径,格式: /工程路径。(getContextPath()) 获取工程部署后在服务器硬盘上的绝对路径。(getRealPath()) 存取数据。(setAttribute(),getAttribute()) 1234567891011protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { ServletConfig servletConfig = getServletConfig(); ServletContext servletContext = servletConfig.getServletContext(); //获取web.xml配置的上下文参数context-param String username = servletContext.getInitParameter("username"); System.out.println("context参数username:"+username); //获取当前工程路径 System.out.println("当前工程路径为"+servletContext.getContextPath()); //获取工程部署后在硬盘上的绝对路径 System.out.println("绝对路径为"+servletContext.getRealPath("/"));} HttpServletRequest类每次只要有请求进入 Tomcat 服务器,Tomcat 服务器就会把请求过来的 HTTP 协议信息解析好封装到 Request 对象中。 然后传递到 service 方法(doGet 和 doPost)中给我们使用。我们可以通过 HttpServletRequest 对象,获取到所有请求的 信息。 常用api 12345678910111213@Overrideprotected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { //获取请求的资源路径 System.out.println("URI-->"+req.getRequestURI()); //获取请求的绝对路径 System.out.println("URL-->"+req.getRequestURL()); //获取客户端ip地址 System.out.println("ip-->"+req.getRemoteAddr()+" port-->"+req.getServerPort()); //获取请求头 System.out.println("header-->"+req.getHeader("User-Agent")); //获取请求方式 System.out.println("method-->"+req.getMethod());} 获取请求参数 12345678910<body> <form action="http://localhost:8080/2_Servlet/param" method="post"> 用户名:<input type="text" name="username"><br> 密码:<input type="password" name="password"><br> 兴趣爱好:<input type="checkbox" name="hobby" value="java">Java <input type="checkbox" name="hobby" value="C++">C++ <input type="checkbox" name="hobby" value="C#">C#<br> <input type="submit" value="提交吧"> </form></body> 12345678910protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { System.out.println("******GET******"); //获取请求参数 System.out.println(request.getParameter("username")); System.out.println(request.getParameter("password")); //只能获取到第一个 System.out.println(request.getParameter("hobby")); //获取多个值 System.out.println(Arrays.asList(request.getParameterValues("hobby")));} post请求中同样,但是会出现中文乱码,需要设置请求体的字符集为utf8. 1request.setCharacterEncoding("utf-8"); 请求转发请求转发是指,服务器收到请求后,从一个资源跳转到另一个资源的操作叫请求转发。 浏览器地址栏没有发生变化 是同一次请求 共享Request域中的数据 可以转发到WEB-INF下 不可以访问工程以外的资源 123456789101112131415161718//servlet1中的get方法protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String username = request.getParameter("username"); System.out.println("发送的"+username); request.setAttribute("password","123456"); //务必以"/"开头,表示地址为hhtp://ip:port/工程名/ RequestDispatcher requestDispatcher = request.getRequestDispatcher("/servlet2"); requestDispatcher.forward(request,response);}//servlet2中的get方法protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String username = request.getParameter("username"); System.out.println("转发收到的"+username); Object password = request.getAttribute("password"); System.out.println(password);}//浏览器地址栏http://localhost:8080/2_Servlet/servlet1?username=root base标签可以设置当前页面的相对路径工作时,参照哪个相对路径进行跳转。 1<base href="http://localhost:8080/07_servlet/a/b/"> Web中”/“的意义 如果被浏览器解析,得到的地址是:http://ip:port/ 如果被服务器解析,得到的地址是:http://ip:port/工程路径 特殊情况:response.sendRediect(“/“) 会把”/“发给浏览器解析,得到http://ip:port/ HttpServletResponse类HttpServletResponse 类和 HttpServletRequest 类一样。每次请求进来,Tomcat 服务器都会创建一个 Response 对象传 递给 Servlet 程序去使用。HttpServletRequest 表示请求过来的信息,HttpServletResponse 表示所有响应的信息, 我们如果需要设置返回给客户端的信息,都可以通过 HttpServletResponse 对象来进行设置。 两个输出流字节流:getOutputStream() 字符流:getWriter() 两个流同时只能使用一个。 向客户端回传数据 123456789101112@Overrideprotected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { System.out.println(resp.getCharacterEncoding());//默认ISO-8859-1 // //设置服务器字符集 // resp.setCharacterEncoding("UTF-8"); // //通过响应头,设置浏览器也使用utf8 // resp.setHeader("Content-Type","text/html;charset-UTF-8"); //也可以直接设置Content-Type,但要在获取流之前设置 resp.setContentType("text/html;charset-UTF-8");//推荐使用, 它会同时设置服务器和客户端都使用 UTF-8 字符集,还设置了响应头 PrintWriter writer = resp.getWriter(); writer.write("响应内容");} 请求重定向是指客户端给服务器发请求,然后服务器告诉客户端说,我给你一些地址。你去新地址访问。叫请求 重定向(因为之前的地址可能已经被废弃)。 浏览器地址栏会发生变化 两次请求 不能共享request域中的数据 不能访问到WEB-INF下的资源 可以访问工程外的资源,比如百度 123456789protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { System.out.println("经过了response1"); // //设置响应码302,表示重定向 // response.setStatus(302); // //设置响应头,说明新的地址在哪里 // response.setHeader("Location","http://localhost:8080/2_Servlet/response2"); response.sendRedirect("/2_Servlet/response2");//推荐使用 //也可以重定向到其他资源,比如百度} 1234567protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { System.out.println("response2's results"); response.getWriter().write("response2's results");}//结果经过了response1response2's results","link":"/2022/01/08/Servlet%E5%AD%A6%E4%B9%A0/"},{"title":"Git","text":"Git是一个分布式版本控制系统,可以对项目的版本进行管理,可以退回历史版本,也可以在历史版本的基础上开发新的版本,可以将分支的版本合并到主要版本。分布式意味着每台电脑上都是完整的版本,而不是版本的一部分。 基本概念 工作区:工作目录,主要进行开发的位置 暂存区:临时存储 本地库:在工作目录中的隐藏文件夹.git目录中,git用于管理的工作目录。 基本命令常用命令 git config –global user.name username:设置用户名 git config –global user.email 邮箱:设置邮箱名 git init:初始化本地库,创建.git目录进行管理 git status:查看本地库的状态 git add 文件名:将文件添加到暂存区 git commit -m “日志信息” 文件名:将暂存区的文件提交到本地库。会生成历史版本。必须写提交注释 git reflog:查看历史记录 git log:查看详细历史记录 git reset –hard 版本号:版本穿梭,通常用于回滚版本 Git分支操作 git branch 分支名:创建分支 git branch -v:查看分支 git checkout 分支名:切换分支 git merge 分支名:把指定的分支合并到当前分支来 GitHub git remote -v:查看远程库别名 git remote add 别名 远程地址:给远程库添加别名 git push 远程库别名 分支名:将分支推送到远程库中 git pull 远程库别名 分支名:将远程库中的代码拉取到本地库中,更新本地库的内容 当新建一个仓库时,如果创建了REMADE文件,需要先用pull命令将远程库拉取到本地,更新本地库。拉取时会出现:fatal: refusing to merge unrelated histories(拒绝合并不相关的历史)。这是因为本地库和远程库还是两个独立的仓库,如果之前直接是clone的方式在本地建立远程库的克隆仓库就不会有该问题。所以需要在pull后加上参数:git pull origin master --allow-unrelated-histories.再进行push提交。 git clone 远程库地址:克隆远程库,会拉取代码,初始化本地库,创建别名origin","link":"/2022/01/11/Git/"},{"title":"计算机网络概述","text":"学完了JavaWeb觉得网络这块知识很多都遗忘了,所以先复习一下计算机网络的知识。从网络的概述开始,每一层都需要再仔细学习一遍。 互联网的组成 边缘部分:所有连接在互联网上的主机组成。这部分是用户直接使用的。这些主机又称为端系统。主机A与主机B通信其实是指主机A的某个进程和主机B的某个进程进行通信,通常可以简称为“计算机之间的通信”。端系统之间的通信方式可划分成两大类。 客户-服务器方式(Client/Server):服务请求方和服务提供方。客户向服务器发送请求,必须知道服务器的地址,不需要特殊的硬件和复杂的操作系统;服务器可同时处理多个请求,一般需要有强大的硬件和高级的操作系统支持。 对等连接方式(P2P):只要两台主机都运行了对等连接软件(P2P)软件,它们就可以进行平等的、对等连接通信。每台主机既可以当客户端又可以当服务器。 核心部分:其特殊作用的是路由器,是一种专用的计算机(但不叫主机)。路由器是实现分组交换的关键构件,其任务是转发收到的分组,这是网络核心部分最重要的功能。交换方式有三种。 电路交换:必须经过“建立连接(占用通信资源)->通话(一直占用通信资源)->释放连接(归还通信资源)“三个步骤的连接方式。重要特点是:在通信的全部时间内,两个用户始终占用端到端的通信资源。 分组交换:采用存储转发技术。把一个报文划分为几个分组后再进行传送。先把较长的报文划分成一个个更小的等长数据段,在每一个数据段前加上必要控制信息组成的首部后,就构成了一个分组。在传送数据之前不必先占用一条端到端的链路的通信资源,分组在哪段链路上传送才占用这段链路的通信资源。 报文交换:整个报文先传送到相邻节点,,全部存储下来后查找转发表,转发到下一个结点。 网络的类别 广域网WAN:ISP(Internet Service Provider)因特网服务提供商,电信、联通、移动等, 它们在各个地方埋网线,有自己的主机、 然后我们出钱连入他们的网络,就能访问上网了。 局域网LAN:覆盖范围较小,需要自己花钱买设备来组建小型网络,宽带固定,自己维护。比如校园网。 个人区域网PAN:在个人工作的地方把属于个人使用的电子设备用无线技术连接起来的网络,范围很小。 计算机网络体系结构 为进行网络中的数据交换而建立的规则、标准称为网络协议。协议应该是层次式的。协议三要素: 语法:即数据和控制信息的结构或格式。 语义:即需要发出何种控制信息,完成何种动作以及做出何种相应。 同步:即事件实现顺序的详细说明。 OSI七层协议:应用层、表示层、会话层、运输层、网络层、数据链路层、物理层。缺少实际经验、实现复杂、效率低下、运作不合理。 TCP/IP体系结构:应用层(FTP,HTTP,SMTP等)、运输层(TCP、UDP)、网际层IP、网络接口层。实际运用。 五层协议体系:应用层、运输层、网络层、数据链路层、物理层。 实体表示任何可以发送或接收信息的硬件或软件进程。协议是控制两个对等实体(或多个实体)进行通信的规则的集合。在协议的控制下两个对等实体间的通信使得本层能够向上一层提供服务。要实现本层协议,还需要使用下面一层所提供的服务。 使用本层服务的实体只能看见服务而无法看见下面的协议,也就是说,下面的协议对上面的实体是透明的。协议是水平的,即协议是控制对等实体之间的通信规则;但服务是垂直的,即服务是由下层向上层通过层间接口提供的。只有那些能够被高一层实体“看得见”的功能才能称为服务。","link":"/2022/01/13/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C%E6%A6%82%E8%BF%B0/"},{"title":"数据链路层","text":"基本概念与基本问题数据链路层属于计算机网络的底层,使用的信道主要有点对点信道和广播信道。 基本概念 在研究数据链路层的问题时,许多情况下只关心在协议栈中水平方向的各数据链路层,不考虑物理层中运输的问题。 链路:从一个结点到相邻结点的一段物理链路,中间没有其他结点。 数据链路:除物理链路外,还需要加上必要的通信协议来控制数据的传输。把实现这些协议的硬件和软件加到链路上,就构成了数据链路。网络适配器(也就是网卡),用来实现数据链路上的协议。 帧:数据链路层数据单元。 三个基本问题 封装成帧:将IP数据报的前后添加首部和尾部来进行帧定界。当数据是ASCII码组成的文本文件时,可使用特殊的帧定界符。控制字符SOH放在帧的最前面,表示帧的首部开始。EOT表示帧的结束。SOH和EOT只是名称,二进制为00000001和00000100. 透明传输:如果在IP数据报中如果有一个和帧尾部一样的8位二进制数,则会提前结束接收数据,这样数据就被破坏了。解决方式是:发送端的数据链路层在数据中出现控制字符SOH或者EOT的前面插入一个转义字符“ESC”(00011011),而在接收端的数据链路层在把数据送往网络层之前删除这个插入的转义字符。这种方法称为字节填充。当接收端收到连续两个转义字符时,就删除前面的一个。 差错检测:传输过程中可能产生比特差错:1可能变成0,0可能变成1。循环冗余检验CRC:在发送端,先将数据分组,每组k个比特。假定待发送数据为M-101001,k=6.先在M后面添加差错检测用的n为冗余码,一共发送n+k位。冗余码的计算:用二进制的模2运算进行$2^n$乘M的运算,相当于在M后加上n个0.得到的n+k位数除以事先选定好的长度为n+1位的除数P,得出商为Q而余数是R,余数比除数少一位,余数即冗余码FCS。 在接收端,把接收到的每一个帧都除以同样的除数P,如果余数为0,则这个帧没有差错,否则有差错,舍弃这个帧。 这种冗余检错校验的特点: 并不能确定究竟是哪一个或哪几个比特出现了差错。 只要经过严格的挑选,并使用位数足够多的除数 P,那么出现检测不到的差错的概率就很小很小。 只能是无差错接受:凡是接受的帧(即不包括丢弃的帧),我们都能以非常接近于 1 的概率认为这些帧在传输过程中没有产生差错”。也就是说:“凡是接收端数据链路层接受的帧都没有传输差错”(有差错的帧就丢弃而不接受)。 这是”不可靠“的,是无比特差错,而不是无传输差错的检测机制,要做到可靠的 还要加上确认和重传机制。即考虑帧重复、帧丢失、帧乱序的情况。 点对点协议PPP是点对点信道数据链路层协议。互联网用户需要连接到某个ISP才能连入互联网。PPP协议就是用户计算机和ISP进行通信时所使用的数据链路层协议。 特点 简单:接收方每接收一个帧,就进行CRC检验,检验正确,就收下,否则就丢弃,它是不可靠传输,所以这就是简单的原因。 封装成帧:规定特殊字符作为帧定界符。 透明性:即透明传输。 多中网络协议:在同一条物理链路上同时支持多种网络层协议(如IP何IPX等)的运行。 多种类型链路:多种类型链路:比如,串行的、并行的,(串行:一个比特一个比特发送,只需要一条线路,并行:一次性传输n个比特,所以需要n条线路,所以叫并行)同步的、异步的(同步:以稳定的比特流的形式传输 异步:以字节为独立的传输单位,字节跟字节之间的时间间隔不确定,但字节中的每个比特仍是同步的。),低速或高速、电或光,等不同类型的链路都能支持。 差错检测:CRC检测。 检测连接状态:检测点跟点之间的连接状态,也就是在PC机和ISP之间的线路。 最大传送单元:MTU,帧的数据部分最大长度。 网路地址协商。 PPP协议的组成 数据链路层协可以用于异步串行或同步串行介质,通俗讲也就是可以适应多种性质的链路。 使用LCP(链路控制协议)建立并维护数据链路连接, 也就是上面讲的一些维护链路连接、检测连接状态等功能,就是用它来实现的。 网络控制协议(NCP)允许点到点连接上使用多种网络层协议,也就是因为跟网络层连接在一起,所以需要支持上一层的多种协议,这样才能完成一系列的功能,比如,网络层地址协商。 PPP协议的帧格式 标志字段F就是PPP帧的定界符。 字节填充:使用的是字节传输,也就是异步,所有的PPP帧的长度都是整数字节,所以会出现IP数据包中有字节跟开始结束标志字节相同的问题。解决方式:IP数据报中出现0x7E字节 : 转变为 0x7D、0x5E;IP数据报中出现0x7D:0x7D、0x5D;IP数据报中出现ASCII码的控制字符,则在该控制字符前面加0x7D。在接收端做相反的操作。 零比特填充:使用的是比特流传输(一连串的比特连续传送),也就是同步传输。在发送端扫描整个数据部分,只要发现5个连续1,则立即填入一个0.这样保证不会出现6个连续的1,导致和定界符0x7E相等。在接收端,当发现连续的5个1,就把后面的0删除。 PPP协议工作状态 为什么PPP协议不用可靠传输? 在数据链路层出现差错的概率不大时,使用比较简单的 PPP 协议较为合理。 在因特网环境下,PPP 的信息字段放入的数据是 IP 数据报。数据链路层的可靠传输并不能够保证网络层的传输也是可靠的。 帧检验序列 FCS 字段可保证无差错传输。 广播信道的数据链路层 以太网:以太网是通信协议标准,该标准定义了在局域网(LAN)中采用的电缆类型和信号处理方法,比如有CSMA/CD协议。 局域网:在较小范围内组件的网络,通过交换器什么的连接各个PC机,比如一个实验室,一栋楼,一个校园内,这都是局域网,拿网线将两台计算机连在一起,这也能算是局域网。 广播信道:就是一台PC机发送数据给另一台PC机,在同一个局域网中的计算机都能接收到该数据,这就像广播一样,所以这种就叫做广播信道。 局域网常见拓扑结构 为了通信的简便,以太网采取了两种重要的措施。 采用较为灵活的无连接的工作方式 不必先建立连接就可以直接发送数据。 n对发送的数据帧不进行编号,也不要求对方发回确认。 这样做的理由是局域网信道的质量很好,因信道质量产生差错的概率是很小的。 以太网提供的服务是不可靠的交付,即尽最大努力的交付。 当目的站收到有差错的数据帧时就丢弃此帧,其他什么也不做。差错的纠正由高层来决定。 如果高层发现丢失了一些数据而进行重传,但以太网并不知道这是一个重传的帧,而是当作一个新的数据帧来发送。 以太网发送的数据都使用曼彻斯特 (Manchester) 编码。 CSMA/CD协议(半双工通信) 多点接入:表示许多计算机以多点接入的方式连接在一根总线上。 载波监听:指每一个站在发送数据之前先要检测一下总线上是否有其他计算机在发送数据,如果有,则暂时不要发送数据,以免发生碰撞。 碰撞检测:也就是边发送边监听,当一个站检测到的信号电压摆动值超过一定的门限值时,就认为总线上至少有两个站同时在发送数据,表明产生了碰撞。 工作流程: 准备发送:适配器从网络层获得一个分组,加上以太网的首部和尾部,组成以太网帧。发送之前,必须先检测信道。 检测信道:若检测到信道忙,则应不停地检测,一直等待信道转为空闲。若检测到信道空闲,并在 96 比特时间内信道保持空闲(保证了帧间最小间隔),就发送这个帧。 检查碰撞:在发送过程中仍不停地检测信道,即网络适配器要边发送边监听。这里只有两种可能性: 发送成功:在争用期内一直未检测到碰撞。这个帧肯定能够发送成功。发送完毕后,其他什么也不做。然后回到 (1)。 发送失败:在争用期内检测到碰撞。这时立即停止发送数据,并按规定发送人为干扰信号。适配器接着就执行指数退避算法,等待 r 倍 512 比特时间后,返回到步骤 (2),继续检测信道。但若重传达 16 次仍不能成功,则停止重传而向上报错。 争用期 以太网的端到端往返时延 2t 称为争用期,或碰撞窗口。 经过争用期这段时间还没有检测到碰撞,才能肯定这次发送不会发生碰撞。 二进制指数退避算法 发生碰撞的站在停止发送数据后,要推迟(退避)一个随机时间才能再发送数据。 基本退避时间取为争用期 2t。 从整数集合 [0, 1, … , (2k -1)] 中随机地取出一个数,记为 r。重传所需的时延就是 r 倍的基本退避时间。 参数 k 按下面的公式计算:k = Min[重传次数, 10]。 n当 k $\\leq$10 时,参数 k 等于重传次数。 当重传达 16 次仍不能成功时即丢弃该帧,并向高层报告. 争用期长度 10 Mbit/s 以太网取 51.2 $\\mu s$ 为争用期的长度。 对于 10 Mbit/s 以太网,在争用期内可发送 512 bit,即 64 字节。 这意味着:以太网在发送数据时,若前 64 字节没有发生冲突,则后续的数据就不会发生冲突。 由于一检测到冲突就立即中止发送,这时已经发送出去的数据一定小于 64 字节。以太网规定了最短有效帧长为 64 字节,凡长度小于 64 字节的帧都是由于冲突而异常中止的无效帧。 强化碰撞 当发送数据的站一旦发现发生了碰撞时:立即停止发送数据,再继续发送若干比特的人为干扰信号 (jamming signal),以便让所有用户都知道现在已经发生了碰撞。 以太网的信道利用率 $$ \\alpha = \\frac{\\tau}{T_0} $$ *α* →0,表示一发生碰撞就立即可以检测出来, 并立即停止发送,因而信道利用率很高。*α* 越大,表明争用期所占的比例增大,每发生一次碰撞就浪费许多信道资源,使得信道利用率明显降低。为提高利用率,以太网的参数*a*的值应当尽可能小些。对以太网参数 *α* 的要求是: 当数据率一定时,以太网的连线的长度受到限制,否则 $\\tau$的数值会太大。 以太网的帧长不能太短,否则 $T_0$的值会太小,使 α 值太大。 以太网的MAC层 在局域网中,硬件地址又称为物理地址,或 MAC 地址,指计算机固化在适配器的ROM中的地址。 MAC帧格式:48bit,6个字节,前3个字节是由管理机构给各个厂家分配的。也就是说如果有厂家想生产网卡这类需要mac地址的东西,必须先像管理机构申请前三位字节,所以网卡上的前三个字节就代表着某个厂家,后三个字节就是由厂家自己来设定的。 发送的数据帧最小要是64个字节,6个目的MAC地址,6个源MAC地址,2个字节代表数据包的类型,还有4个字节是FCS,用来进行CRC算法检测的,剩下的46个字节就是数据包最少要发送的字节数了,如果数据包实际发的少于46,那么会给这个数据包自动补充0,来达到需要的字节数。 插入的8个字节中,前7个字节用来使发送的数据帧的的比特同步,也叫作前同步码,最后一个字节,帧的开始定界符,也就是告诉接收方,从这个字节开始,后面就是MAC帧了。在接受MAC帧后,并不能马上识别出帧开始定界符,没有那么快的反应分辨出来,所以需要在前面加同步码,使接收方有反应的时间,所以同步码都是1010101010101这样的bit。前7个字节的同步码跟最后一个字节中的前6个bit位相同。","link":"/2022/01/14/%E6%95%B0%E6%8D%AE%E9%93%BE%E8%B7%AF%E5%B1%82/"},{"title":"MySQL基础操作","text":"本篇文章记录MySQL的基本语法与操作,加强记忆。 数据库操作123456789101112131415161718-- 查看当前数据库 SELECT DATABASE();-- 显示当前时间、用户名、数据库版本 SELECT now(), user(), version();-- 创建库 CREATE DATABASE[ IF NOT EXISTS] 数据库名 数据库选项 数据库选项: CHARACTER SET charset_name COLLATE collation_name-- 查看已有库 SHOW DATABASES[ LIKE 'PATTERN']-- 查看当前库信息 SHOW CREATE DATABASE 数据库名-- 修改库的选项信息 ALTER DATABASE 库名 选项信息-- 删除库 DROP DATABASE[ IF EXISTS] 数据库名 同时删除该数据库相关的目录及其目录内容 表的操作1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768-- 创建表 CREATE [TEMPORARY] TABLE[ IF NOT EXISTS] [库名.]表名 ( 表的结构定义 )[ 表选项] 每个字段必须有数据类型 最后一个字段后不能有逗号 TEMPORARY 临时表,会话结束时表自动消失 对于字段的定义: 字段名 数据类型 [NOT NULL | NULL] [DEFAULT default_value] [AUTO_INCREMENT] [UNIQUE [KEY] | [PRIMARY] KEY] [COMMENT 'string']-- 表选项 -- 字符集 CHARSET = charset_name 如果表没有设定,则使用数据库字符集 -- 存储引擎 ENGINE = engine_name 表在管理数据时采用的不同的数据结构,结构不同会导致处理方式、提供的特性操作等不同 常见的引擎:InnoDB MyISAM Memory/Heap BDB Merge Example CSV MaxDB Archive 不同的引擎在保存表的结构和数据时采用不同的方式 MyISAM表文件含义:.frm表定义,.MYD表数据,.MYI表索引 InnoDB表文件含义:.frm表定义,表空间数据和日志文件 SHOW ENGINES -- 显示存储引擎的状态信息 SHOW ENGINE 引擎名 {LOGS|STATUS} -- 显示存储引擎的日志或状态信息 -- 自增起始数 AUTO_INCREMENT = 行数 -- 数据文件目录 DATA DIRECTORY = '目录' -- 索引文件目录 INDEX DIRECTORY = '目录' -- 表注释 COMMENT = 'string' -- 分区选项 PARTITION BY ... (详细见手册)-- 查看所有表 SHOW TABLES[ LIKE 'pattern'] SHOW TABLES FROM 库名-- 查看表结构 SHOW CREATE TABLE 表名 (信息更详细) DESC 表名 / DESCRIBE 表名 / EXPLAIN 表名 / SHOW COLUMNS FROM 表名 [LIKE 'PATTERN'] SHOW TABLE STATUS [FROM db_name] [LIKE 'pattern']-- 修改表 -- 修改表本身的选项 ALTER TABLE 表名 表的选项 eg: ALTER TABLE 表名 ENGINE=MYISAM; -- 对表进行重命名 RENAME TABLE 原表名 TO 新表名 RENAME TABLE 原表名 TO 库名.表名 (可将表移动到另一个数据库) -- RENAME可以交换两个表名 -- 修改表的字段机构(13.1.2. ALTER TABLE语法) ALTER TABLE 表名 操作名 -- 操作名 ADD[ COLUMN] 字段定义 -- 增加字段 AFTER 字段名 -- 表示增加在该字段名后面 FIRST -- 表示增加在第一个 ADD PRIMARY KEY(字段名) -- 创建主键 ADD UNIQUE [索引名] (字段名)-- 创建唯一索引 ADD INDEX [索引名] (字段名) -- 创建普通索引 DROP[ COLUMN] 字段名 -- 删除字段 MODIFY[ COLUMN] 字段名 字段属性 -- 支持对字段属性进行修改,不能修改字段名(所有原有属性也需写上) CHANGE[ COLUMN] 原字段名 新字段名 字段属性 -- 支持对字段名修改 DROP PRIMARY KEY -- 删除主键(删除主键前需删除其AUTO_INCREMENT属性) DROP INDEX 索引名 -- 删除索引 DROP FOREIGN KEY 外键 -- 删除外键-- 删除表 DROP TABLE[ IF EXISTS] 表名 ...-- 清空表数据 TRUNCATE [TABLE] 表名-- 复制表结构 CREATE TABLE 表名 LIKE 要复制的表名-- 复制表结构和数据 CREATE TABLE 表名 [AS] SELECT * FROM 要复制的表名","link":"/2022/01/14/MySQL%E5%9F%BA%E7%A1%80%E6%93%8D%E4%BD%9C/"},{"title":"网络层","text":"互联网的先驱者提出一种崭新的网络设计思路,不同于电信网提供端到端的可靠传输服务,网络层向上只提供简单灵活的、无连接的、尽最大努力交付的数据报服务。如果主机(即端系统)中的进程之间的通信需要是可靠的,那么就由网络的主机中的运输层负责可靠交付(包括差错处理、流量控制等) 。采用这种设计思路的好处是:网络的造价大大降低,运行方式灵活,能够适应多种应用。 网际协议IP 网际协议 IP 是 TCP/IP 体系中两个最主要的协议之一。 与 IP 协议配套使用的还有三个协议: 地址解析协议 ARP(Address Resolution Protocol) 网际控制报文协议 ICMP(Internet Control Message Protocol) 网际组管理协议 IGMP(Internet Group Management Protocol) 虚拟互连网络 将网络互相连接起来要使用一些中间设备。 物理层中间设备:转发器 (repeater)。 数据链路层中间设备:网桥 或 桥接器 (bridge)。 网络层中间设备:路由器(router)。 网络层以上中间设备:网关(gateway)。 当中间设备是转发器或网桥时,一般并不称之为网络互连,因为这仅仅是把一个网络扩大了,而这仍然是一个网络。 网络互连都是指用路由器进行网络互连和路由选择。 使用虚拟互连网络的好处是:当互联网上的主机进行通信时,就好像在一个网络上通信一样,而看不见互连的各具体的网络异构细节。 如果在这种覆盖全球的 IP 网的上层使用 TCP 协议,那么就是现在的互联网 (Internet)。 分类的IP地址 IP 地址就是给每个连接在互联网上的主机(或路由器)分配一个在全世界范围是唯一的 32 位的标识符。 IP地址的编制方法: 分类的 IP 地址。 子网的划分。 构成超网。 分类的IP地址每一类地址都由两个固定长度的字段组成,其中一个字段是网络号 net-id,它标志主机(或路由器)所连接到的网络,而另一个字段则是主机号 host-id,它标志该主机(或路由器)。 A类地址:网络号8位,1个字节,首位固定为0,只有7位可使用。主机号24位,3个字节。 可指派的网络号是126(即$2^7-2$)。减2原因是:网络号字段全0的IP地址是个保留地址,表示“本网络”。网络号为127(即01111111)保留作为本地软件环回测试本主机进程之间的通信之用。目的地址为环回地址的IP数据报永远不会出现在任何网络上,因为网络号127的地址根本不是一个网络地址。 每个A类网络可分配的最大主机数是$2^{24}-2$,减2是因为全0的主机号字段表示IP地址是“本主机”所在的网络地址,比如5.6.7.8所在的网络地址为5.0.0.0,而全1表示该网络上的所有主机,比如5.255.255.255表示在网络5.0.0.0上的所有主机。 IP地址空间共$2^{32}$个地址,整个A类地址空间共有$2^{31}$个地址,占整个IP地址空间的一半。 B类地址:网络号16位,2个字节,前两位固定为10,只有14位可用。主机号16位,2个字节。 由于前两位为10,所以网络号后14位不可能使网络号全0或全1,不存在减2,。但是实际上B类网络地址128.0.0.0是不指派的,可指派的最小网络地址是128.1.0.0。因此可指派的网络数为$2^{14}-1$,即16383。 每个B类网络可分配的最大主机数是$2^{16}-2$,即65534,减2是去掉全0和全1的主机号。 整个B类地址空间有$2^{30}$个地址,占整个IP地址空间的25%。 C类地址:网络号24位,3个字节,前三位固定为110,只有21位可用。主机号8位,1个字节。 C类网络地址192.0.0.0也是不指派的,可指派的最小网络地址为192.0.1.0。因此C类地址网络数为$2^{21}-1$,即2097151。 每个C类网络可分配的最大主机数是$2^8-2$,即254。 整个C类地址空间有$2^{29}$个地址,占整个IP地址空间的12.5%。 重要特点 IP 地址是一种分等级的地址结构。分两个等级的好处是: IP 地址管理机构在分配 IP 地址时只分配网络号,而剩下的主机号则由得到该网络号的单位自行分配。这样就方便了 IP 地址的管理。 路由器仅根据目的主机所连接的网络号来转发分组(而不考虑目的主机号),这样就可以使路由表中的项目数大幅度减少,从而减小了路由表所占的存储空间。 实际上 IP 地址是标志一个主机(或路由器)和一条链路的接口。 当一个主机同时连接到两个网络上时,该主机就必须同时具有两个相应的 IP 地址,其网络号 net-id 必须是不同的。这种主机称为多归属主机 (multihomed host)。 由于一个路由器至少应当连接到两个网络(这样它才能将 IP 数据报从一个网络转发到另一个网络),因此一个路由器至少应当有两个不同的 IP 地址。 用转发器或网桥连接起来的若干个局域网仍为一个网络,因此这些局域网都具有同样的网络号 net-id。 所有分配到网络号 net-id 的网络,无论是范围很小的局域网,还是可能覆盖很大地理范围的广域网,都是平等的。 IP地址和硬件地址 硬件地址已固化在网卡的ROM上,因此常可以称为物理地址。而局域网的MAC帧中源地址和目的地址都是物理地址,又可称为MAC地址。这三种叫法可作为同义词。 从层次的角度看,硬件地址是数据链路层和物理层使用的地址。IP 地址是网络层和以上各层使用的地址,是一种逻辑地址(称 IP 地址是逻辑地址是因为 IP 地址是用软件实现的)。 IP数据报放入数据链路层的MAC帧后,整个IP数据报就成为MAC帧的数据,因而在数据链路层看不见数据报的IP地址。 在IP 层抽象的互联网上只能看到IP数据报。虽然IP数据报可能会经过路由器的转发,但在它的首部中的源地址和目的地址始终不变,经过的路由器的IP地址不会出现在IP数据报中。 路由器只根据目的站的IP地址的网络号进行网络选择。 在局域网的链路层,只能看见MAC帧。链路层收到MAC帧要丢弃原来的首部和尾部,在转发时要重新加上首部和尾部,这时首部中的源地址和目的地址就发生了改变。这种变化在上面的IP层是看不见的。 IP 层抽象的互联网屏蔽了下层很复杂的细节。在抽象的网络层上讨论问题,就能够使用统一的、抽象的 IP 地址研究主机和主机或主机和路由器之间的通信 。 还有两个重要问题有待解决: 主机和路由器怎样知道应当在MAC帧的首部填入什么样的硬件地址? 路由器中的路由表是怎样得出的? 地址解析协议ARP地址解析协议 ARP 是用来解决已经知道了一个机器(主机或路由器)的IP地址,如何找出其相应的硬件地址这样的问题的。 每一个主机都设有一个 ARP 高速缓存 (ARP cache),里面有所在的局域网上的各主机和路由器的 IP 地址到硬件地址的映射表。当主机 A 欲向本局域网上的某个主机 B 发送 IP 数据报时,就先在其 ARP 高速缓存中查看有无主机 B 的 IP 地址。如果有,就可查出其对应的硬件地址,再将此硬件地址写入 MAC 帧,然后通过局域网将该 MAC 帧发往此硬件地址。如果没有,主机A就自动运行ARP,按以下步骤找到B的硬件地址。 ARP 进程在本局域网上广播发送一个 ARP 请求分组。请求分组中包含发送方硬件地址 、发送方 IP 地址 、 目标方硬件地址(未知时填 0) 、 目标方 IP 地址。 主机B的IP地址与请求分组中的IP地址一样,就收下这个ARP请求分组,并向A发送响应分组,包括主机B的IP地址和硬件地址。ARP请求分组是广播发送的,但ARP响应分组是普通的单播,即从一个源地址发到一个目的地址。 可能不久后B也会向A发送数据报,所以B在收到A的请求分组中,会把A的IP地址到硬件地址的映射写入自己的ARP高速缓存中。 主机A收到主机B的响应分组后,就在ARP高速缓存中写入B的IP地址到硬件地址的映射。 ARP对保存在高速缓存中的每一个映射地址项目都设置生存时间,超过生存时间的映射就从高速缓存中删掉。因为若主机B更换网络适配器,那么B的硬件地址就改变了,主机A再通过原先的映射无法找到主机B。过了生产时间后,A重新进行广播发送ARP请求分组,就可以再找到B。 需要注意的是,ARP是解决同一个局域网上的主机或路由器的IP地址到硬件地址的映射问题。如果所要找的主机和源主机不在同一个局域网上,那么就要通过 ARP 找到一个位于本局域网上的某个路由器的硬件地址,然后把分组发送给这个路由器,让这个路由器把分组转发给下一个网络。剩下的工作就由下一个网络来做。路由器包括物理层、链路层、网络层,也可使用ARP协议。 使用ARP四种典型情况: 发送方是主机,要把 IP 数据报发送到本网络上的另一个主机。这时用 ARP 找到目的主机的硬件地址。 发送方是主机,要把 IP 数据报发送到另一个网络上的一个主机。这时用 ARP 找到本网络上的一个路由器的硬件地址。剩下的工作由这个路由器来完成。 发送方是路由器,要把 IP 数据报转发到本网络上的一个主机。这时用 ARP 找到目的主机的硬件地址。 发送方是路由器,要把 IP 数据报转发到另一个网络上的一个主机。这时用 ARP 找到本网络上另一个路由器的硬件地址。剩下的工作由这个路由器来完成。 那为什么不直接使用硬件地址进行通信? 全世界存在着各式各样的网络,它们使用不同的硬件地址。要使这些异构网络能够互相通信就必须进行非常复杂的硬件地址转换工作,因此几乎是不可能的事。IP 编址把这个复杂问题解决了。连接到互联网的主机只需各自拥有一个唯一的 IP 地址,它们之间的通信就像连接在同一个网络上那样简单方便,因为上述的调用 ARP 的复杂过程都是由计算机软件自动进行的,对用户来说是看不见这种调用过程的。 IP数据报的格式IP数据报的格式能够说明IP协议具有什么功能。在TCP/IP的标准中,各种数据格式常以32位(即4字节)为单位来描述。 一个 IP 数据报由首部和数据两部分组成。首部的前一部分是固定长度,共 20 字节,是所有 IP 数据报必须具有的。在首部的固定部分的后面是一些可选字段,其长度是可变的。 版本:占 4 位,指 IP 协议的版本。目前的 IP 协议版本号为 4 (即 IPv4)。 首部长度:占 4 位,可表示的最大数值是 15 个单位(一个单位为 4 字节),因此 IP 的首部长度的最大值是 60 字节。 总长度:占 16 位,指首部和数据之和的长度,单位为字节,因此数据报的最大长度为 65535 字节。总长度必须不超过最大传送单元 MTU。 标志(flag) :占 3 位,目前只有前两位有意义。标志字段的最低位是 MF 。MF = 1 表示后面“还有分片”。MF = 0 表示最后一个分片。标志字段中间的一位是 DF 。只有当 DF = 0 时才允许分片。 片偏移:占13 位,指出:较长的分组在分片后某片在原分组中的相对位置。片偏移以 8 个字节为偏移单位。 生存时间:占8 位,记为 TTL (Time To Live),指示数据报在网络中可通过的路由器数的最大值。 协议:占8 位,指出此数据报携带的数据使用何种协议,以便目的主机的 IP 层将数据部分上交给哪个处理过程。 首部检验和:占16 位,只检验数据报的首部,不检验数据部分。这里不采用 CRC 检验码而采用简单的计算方法。 源地址和目的地址各占4字节。 IP层转发分组流程在路由表中,对每一条路由,最主要的是(目的网络地址,下一跳地址) IP 数据报的首部中没有地方可以用来指明“下一跳路由器的 IP 地址”,当路由器收到待转发的数据报,不是将下一跳路由器的 IP 地址填入 IP 数据报,而是送交下层的网络接口软件。网络接口软件使用 ARP 负责将下一跳路由器的 IP 地址转换成硬件地址,并将此硬件地址放在链路层的 MAC 帧的首部,然后根据这个硬件地址找到下一跳路由器。 路由分组转发算法 从数据报的首部提取目的主机的 IP 地址 D, 得出目的网络地址为 N。 若网络 N 与此路由器直接相连,则把数据报直接交付目的主机 D;否则是间接交付,执行3。 若路由表中有目的地址为 D 的特定主机路由,则把数据报传送给路由表中所指明的下一跳路由器;否则,执行4。 若路由表中有到达网络 N 的路由,则把数据报传送给路由表指明的下一跳路由器;否则,执行5。 若路由表中有一个默认路由,则把数据报传送给路由表中所指明的默认路由器;否则,执行6。 报告转发分组出错。 关于路由表,没有给分组指明到某个网络的完整路径,路由表指出,到某个网络应当先到某个路由器(即下一跳路由器)。在到达下一跳路由器后,再继续查找其路由表,知道再下一步应当到哪一个路由器。这样一步一步地查找下去,直到最后到达目的网络。 划分子网和构造超网划分子网三级IP地址分类的IP地址空间利用率很低,一个A类网络或B类网络可分配很多主机,但实际上连接主机可能并不多。划分子网是从主机号借用若干个位作为子网号 subnet-id,而主机号 host-id 也就相应减少了若干个位。划分的子网在整体上对外部仍表现为一个网络。 子网掩码从一个 IP 数据报的首部并无法判断源主机或目的主机所连接的网络是否进行了子网划分。那么路由器该如何将数据报转发到特定的子网? 子网掩码的长度也是32位,IP地址中网络号和子网号均为1,主机号为0就是这个子网的子网掩码。路由器将收到的数据报的目的IP地址和三级IP地址的子网掩码逐位相与,就得到了所要找的子网。 在划分子网的情况下,路由器的路由表中的每一个项目,除了要给出目的网络地址外,还必须同时给出该网络的子网掩码。分组转发最后需要找到目的IP地址所在的子网。 无分类编制CIDR划分子网在一定程度上缓解了互联网在发展中遇到的困难,但是IPv4地址空间还是消耗太快,路由表的项目数也急剧增长。所以又提出了现在所使用的无分类的IP地址。 CIDR使用各种长度的“网络前缀”(network-prefix)来代替分类地址中的网络号和子网号。 CIDR 使用“斜线记法”,即在 IP 地址面加上一个斜线,然后写上网络前缀所占的位数,比如220.78.168.0/24 CIDR 把网络前缀都相同的连续的 IP 地址组成“CIDR 地址块”。只要知道这个地址块中的任一个地址,就可以知道这个地址块的起始地址、最大地址以及地址数。 比如已知IP地址$128.14.34.7/20$,二进制$10000000$ $00001110$ $00100011$ $00000111$,其中前20位是网络前缀,后面12位是主机号,那这个地址块的最小地址就是主机号全为0,最大地址是主机号全为1。这个地址块共$2^{12}$个地址。 “CIDR不使用子网”是指CIDR并没有在32位地址中指明若干位作为子网字段,但分配到一个CIDR地址块的单位,仍然可以在本单位内根据需要划分一些子网。 最长前缀匹配 使用 CIDR 时,路由表中的每个项目由“网络前缀”和“下一跳地址”组成。在查找路由表时可能会得到不止一个匹配结果。 应当从匹配结果中选择具有最长网络前缀的路由:最长前缀匹配。网络前缀越长,其地址块就越小,因而路由就越具体。 网际控制报文协议ICMP ICMP 报文的种类有两种,即 ICMP 差错报告报文和 ICMP 询问报文。 ICMP 差错报告报文:检测在传送数据的过程中,发生的错误,如果发生了错误,会通过该协议返回给源主机一个带有错误原因的数据包 n终点不可达 时间超过 参数问题 改变路由(重定向)(Redirect) ICMP询问报文。 回送请求和回答报文:主机向特定目标发出询问,收到此报文必须返回一个ICMP回送回答报文。用于测试目的站是否可达。 时间戳请求和回答报文:请某个路由器或主机回答当前的日期和时间,用于进行时钟的同步和测量时间。 ICMP应用举例:Ping(Packet Internet Groper) PING 用来测试两个主机之间的连通性。 PING 使用了 ICMP 回送请求与回送回答报文。 PING 是应用层直接使用网络层 ICMP 的例子,它没有通过运输层的 TCP 或UDP。 路由选择协议内部网关协议RIP内部网关协议OSPF外部网关协议BGP","link":"/2022/01/16/%E7%BD%91%E7%BB%9C%E5%B1%82/"},{"title":"运输层","text":"运输层是整个网络体系结构的关键层次之一,在面试中也是高频考点。包括协议特点、进程之间通信和端口等概念,比较简单的UDP协议,复杂但十分重要的TCP协议和可靠传输的工作原理,包括停止等待协议和ARQ协议。以及三个重要问题:滑动窗口、流量控制和拥塞控制机制。还有TCP三次握手四次挥手过程。 运输层协议概述运输层的作用 从运输层的角度看,通信的真正端点并不是主机而是主机中的进程。也就是说,端到端的通信是应用进程之间的通信。 在一台主机中经常有多个应用进程同时分别和另一台主机中的多个应用进程通信,这表明运输层有一个很重要的功能——复用和**分用 **。 复用:指在发送方不同应用进程都可以使用同一个运输层协议传送数据(当然要加上适当的首部)。 分用:指接收方的运输层在剥去报文的首部后能够把这些数据正确交付目的应用进程。 网络层为主机之间提供逻辑通信,而运输层为应用进程之间提供端到端的逻辑通信。 这条逻辑通信信道对上层的表现却因运输层使用的不同协议而有很大的差别。当运输层采用面向连接的 TCP 协议时,尽管下面的网络是不可靠的(只提供尽最大努力服务),但这种逻辑通信信道就相当于一条全双工的可靠信道。当运输层采用无连接的 UDP 协议时,这种逻辑通信信道是一条不可靠信道。 运输层的端口 端口用一个 16 位端口号进行标志。可允许有65535个不同的端口。 端口号只具有本地意义,即端口号只是为了标志本计算机应用层中的各进程。不同计算机的相同端口号是没有联系的。 服务器端使用的端口号 熟知端口,数值一般为 0~1023。IANA把这些端口号指派给了TCP/IP最重要的一些应用程序,让所有用户都知道。比如HTTP使用的端口号是80,DNS是53. 登记端口:数值为 1024~49151,为没有熟知端口号的应用程序使用的。使用这个范围的端口号必须在 IANA 登记,以防止重复。 常用的熟知端口 客户端使用的端口号 又称为短暂端口号,数值为 49152~65535,留给客户进程选择暂时使用。当服务器进程收到客户进程的报文时,就知道了客户进程所使用的动态端口号。通信结束后,这个端口号可供其他客户进程以后使用。 用户数据报协议UDPUDP概述UDP 只在 IP 的数据报服务之上增加了很少一点的功能: 复用和分用的功能 差错检测的功能 虽然 UDP 用户数据报只能提供不可靠的交付,但 UDP 在某些方面有其特殊的优点。 重要特点 UDP 是无连接的,发送数据之前不需要建立连接,因此减少了开销和发送数据之前的时延。 UDP 使用尽最大努力交付,即不保证可靠交付,因此主机不需要维持复杂的连接状态表。 UDP 是面向报文的。UDP 对应用层交下来的报文,既不合并,也不拆分,而是保留这些报文的边界。UDP 一次交付一个完整的报文。因此,应用程序需选择合适大小的报文,若报文太长,UDP把它交给IP层时可能需要分片,降低IP层的效率,若太短,交给IP层后会使IP数据报首部相对长度太大,也降低IP层的效率。 UDP 没有拥塞控制,因此网络出现的拥塞不会使源主机的发送速率降低。这对某些实时应用是很重要的。很适合多媒体通信的要求。 UDP 支持一对一、一对多、多对一和多对多的交互通信。 UDP 的首部开销小,只有 8 个字节,比 TCP 的 20 个字节的首部要短。 UDP首部格式 用户数据报 UDP 有两个字段:数据字段和首部字段。首部字段很简单,只有 8 个字节。 源端口:源端口号。在需要对方回信时使用。不需要时可全为0. 目的端口:目的端口号。 长度:UDP用户数据报的长度,其最小值为8,仅有首部。 检验和:检验UDP用户数据报在传输中是否有错,有错就丢弃。 当运输层从 IP 层收到 UDP 数据报时,就根据首部中的目的端口,把 UDP 数据报通过相应的端口,上交最后的终点——应用进程。如果接收方UDP发现收到的报文中目的端口号不正确,即不存在对应于该端口的应用进程,就丢弃该报文,并由网际报文控制协议ICMP发送“端口不可达”差错报文给发送方。 虽然在 UDP 之间的通信要用到其端口号,但由于 UDP 的通信是无连接的,因此不需要使用套接字。 在计算检验和时,临时把“伪首部”和 UDP 用户数据报连接在一起。伪首部仅仅是为了计算检验和。UDP的检验和是把首部和数据部分一起都检验。而IP协议只检验首部。 一些使用UDP的例子: 应用层协议中DNS,也就是根据域名解析IP地址的一个协议,使用的就是UDP。 DHCP,这个是给各电脑分配IP地址的协议,其中用的也是UDP协议。 IGMP,我们说的多播,也就是使用的UDP,在多媒体教室,老师拿笔记本讲课,我们在下面通过各自的电脑看到老师的画面,这就是通过UDP传输数据,所以会出现有的同学卡,有的同学很流畅,就是因为其不可靠传输,但是卡一下,对接下来的观看并没有什么影响。 传输控制协议TCPTCP是面向连接的运输层协议,提供可靠交付、全双工通信,面向字节流,有流量控制、拥塞控制。每一条TCP连接只有两个端点。TCP连接的端点叫**套接字(socket)**。表示方法是(IP地址:端口号),每一条TCP连接唯一地被通信两端的两个套接字所确定。 TCP报文段首部格式 源端口和目的端口字段:各占 2 字节。端口是运输层与应用层的服务接口。运输层的复用和分用功能都要通过端口才能实现。 序号字段:占 4 字节。TCP 连接中传送的数据流中的每一个字节都编上一个序号。序号字段的值则指的是本报文段所发送的数据的第一个字节的序号 确认号字段:占 4 字节,是期望收到对方的下一个报文段的数据的第一个字节的序号。确认号为N,表明到序号N-1的所有数据都已正确收到。 数据偏移(即首部长度):占 4 位,它指出 TCP 报文段的数据起始处距离 TCP 报文段的起始处有多远。“数据偏移”的单位是 32 位字(以 4 字节为计算单位)。 确认 ACK :只有当 ACK = 1 时确认号字段才有效。当 ACK = 0 时,确认号无效。 同步 SYN :同步 SYN = 1 表示这是一个连接请求或连接接受报文。,在进行三次握手时和ACK配合使用。 终止 FIN :用来释放一个连接。FIN = 1 表明此报文段的发送端的数据已发送完毕,并要求释放运输连接。在释放连接时使用。 窗口字段 :占 2 字节,指发送本报文段一方的接收窗口,用来让对方设置发送窗口的依据,单位为字节。 检验和:占 2 字节。检验和字段检验的范围包括首部和数据这两部分。在计算检验和时,要在 TCP 报文段的前面加上 12 字节的伪首部。 选项字段:长度可变。TCP 最初只规定了一种选项,即最大报文段长度 MSS。MSS是数据字段的最大长度。 TCP可靠传输的实现滑动窗口协议发送方维持的发送窗口,它的意义是:位于发送窗口内的分组都可连续发送出去,而不需要等待对方的确认。连续 ARQ 协议规定,发送方每收到一个确认,就把发送窗口向前滑动一个分组的位置。接收方一般采用累积确认的方式。即不必对收到的分组逐个发送确认,而是对按序到达的最后一个分组发送确认,这样就表示:到这个分组为止的所有分组都已正确收到了。 以字节为单位的滑动窗口TCP的滑动窗口是以字节为单位的。 现假定 A 收到了 B 发来的确认报文段,其中窗口是 20 字节,而确认号是 31(这表明 B 期望收到的下一个序号是 31,而序号 30 为止的数据已经收到了)。根据这两个数据,A 就构造出自己的发送窗口。 发送窗口表示:在没有收到 B 的确认的情况下,A 可以连续把窗口内的数据都发送出去。显然,窗口越大,发送方就可以在收到对方确认之前连续发送更多的数据,因而可能获得更高的传输效率。 假设A发送了序号为31-41的数据 B收到了序号为32和33的数据,但没有按序到达,因为序号为31的数据没有收到,因此B发送的确认报文段的确认号仍然是31,即期望收到的序号。 假定B收到了序号为31的数据,接着把接收窗口向前移动三个序号。 A收到B的确认后,就可以把发送窗口向前滑动三个序号,但指针P2不变,A的可用窗口增大了。 A在继续发送完序号42-53的数据后,指针P2向前移动和P3重合。但没有收到确认。由于此时A的发送窗口已满,可用窗口为零,必须停止发送,但还没有收到确认。为了保证可靠传输,A在经过一段时间(由超时计时器控制)就重传这部分数据,重新设置超时计时器,直到收到B的确认为止。 发送缓存发送方的应用进程把字节流写入 TCP 的发送缓存。 接收缓存接收方的应用进程从 TCP 的接收缓存中读取字节流。 超时重传时间的选择TCP 每发送一个报文段,就对这个报文段设置一次计时器。 只要计时器设置的重传时间到但还没有收到确认,就要重传这一报文段。 选择确认SACK若收到的报文段无差错,只是未按序号,中间还缺少一些序号的数据,那么通过选择确认SACK可以只传送缺少的数据而不重传已经正确到达接收方的数据。 然而,由于SACK文档并没有指明发送方应当怎样响应SACK,因此大多数的实现还是重传所有未被确认的数据块。 TCP流量控制利用滑动窗口实现流量控制流量控制 (flow control) 就是让发送方的发送速率不要太快,既要让接收方来得及接收,也不要使网络发生拥塞。利用滑动窗口机制可以很方便地在 TCP 连接上实现流量控制。 可变窗口进行流量控制举例 B进行了三次流量控制。第一次把窗口减小到rwnd=300,第二次减到rwnd=100,最后减到rwnd=0,即不允许再发送数据。 但是可能会发生死锁。比如B 向 A 发送了零窗口的报文段后不久,B 的接收缓存又有了一些存储空间。于是 B 向 A 发送了 rwnd = 400 的报文段。但这个报文段在传送过程中丢失了。A 一直等待收到 B 发送的非零窗口的通知,而 B 也一直等待 A 发送的数据。 为了解决这个问题,TCP 为每一个连接设有一个持续计时器 (persistence timer)。只要 TCP 连接的一方收到对方的零窗口通知,就启动该持续计时器。若持续计时器设置的时间到期,就发送一个零窗口探测报文段(仅携带 1 字节的数据),而对方就在确认这个探测报文段时给出了现在的窗口值。若窗口仍然是零,则收到这个报文段的一方就重新设置持续计时器。若窗口不是零,就可以继续传输数据了。 TCP传输效率可以用不同的机制来控制 TCP 报文段的发送时机 第一种机制是TCP 维持一个变量,它等于最大报文段长度 MSS。只要缓存中存放的数据达到 MSS 字节时,就组装成一个 TCP 报文段发送出去。 第二种机制是由发送方的应用进程指明要求发送报文段,即 TCP 支持的推送 (push)操作。 第三种机制是发送方的一个计时器期限到了,这时就把当前已有的缓存数据装入报文段(但长度不能超过 MSS)发送出去。 TCP拥塞控制对网络中某资源的需求超过了该资源所能提供的可用部分,网络的性能就要变坏。这种现象称为拥塞。若网络中有许多资源同时产生拥塞,网络的性能就要明显变坏,整个网络的吞吐量将随输入负荷的增大而下降。 拥塞控制和流量控制的区别 拥塞控制就是防止过多的数据注入到网络中,使网络中的路由器或链路不致过载。拥塞控制是一个全局性的过程,涉及到所有的主机、所有的路由器,以及与降低网络传输性能有关的所有因素。 流量控制往往指点对点通信量的控制,是个端到端的问题(接收端控制发送端)。流量控制所要做的就是抑制发送端发送数据的速率,以便使接收端来得及接收。 TCP进行拥塞控制的算法有四种:慢开始,拥塞避免、快重传、快恢复。假定: 数据是单方向传送的,对方只传送确认报文。 接收方总是有足够大的缓存空间,因此发送窗口的大小由网络拥塞程度决定。 慢开始发送方维持一个叫做拥塞窗口cwnd的状态变量,动态地在变化。发送方让自己的发送窗口等于拥塞窗口。判断网络拥塞的依据是出现了超时。 算法的思路:由小到大逐渐增大拥塞窗口数值。 慢开始门限 ssthresh(状态变量):防止拥塞窗口cwnd 增长过大引起网络拥塞。 发送方每接收到一个报文的确认,cwnd就增加一。所以每经过一个传输轮次 ,拥塞窗口 cwnd 就加倍。 慢开始门限 ssthresh 的用法如下: 当 cwnd < ssthresh 时,使用慢开始算法。 当 cwnd > ssthresh 时,停止使用慢开始算法而改用拥塞避免算法。 当 cwnd = ssthresh 时,既可使用慢开始算法,也可使用拥塞避免算法。 拥塞避免让拥塞窗口 cwnd 缓慢地增大,即每经过一个往返时间 RTT 就把发送方的拥塞窗口 cwnd 加 1,而不是加倍,使拥塞窗口 cwnd 按线性规律缓慢增长。 当网络出现了超时,就要使ssthresh = cwnd/2,同时cwnd置为1,执行慢开始算法。 慢开始和拥塞避免算法的实现举例 当拥塞窗口cwnd = 16时(图中的点④),出现了一个新的情况,就是发送方一连收到 3 个对同一个报文段的重复确认(图中记为3-ACK)。发送方改为执行快重传和快恢复算法。 快重传发送方只要一连收到三个重复确认,就知道接收方确实没有收到报文段,因而应当立即进行重传(即“快重传”),这样就不会出现超时,发送方也不就会误认为出现了网络拥塞。 快恢复当发送端收到连续三个重复的确认时,由于发送方现在认为网络很可能没有发生拥塞,因此现在不执行慢开始算法,而是执行快恢复算法 FR (Fast Recovery) 算法: 慢开始门限 ssthresh = 当前拥塞窗口 cwnd / 2 ; 新拥塞窗口 cwnd = 慢开始门限 ssthresh ; 开始执行拥塞避免算法,使拥塞窗口缓慢地线性增大。 综上,TCP的拥塞控制流程如下 发送窗口的上限值发送方的发送窗口的上限值应当取为接收方窗口 rwnd 和拥塞窗口 cwnd 这两个变量中较小的一个。 当 rwnd < cwnd 时,是接收方的接收能力限制发送窗口的最大值。 当 cwnd < rwnd 时,则是网络的拥塞限制发送窗口的最大值。 rwnd 和 cwnd 中数值较小的一个,控制了发送方发送数据的速率。 TCP的连接建立 TCP 建立连接的过程叫做握手。 握手需要在客户和服务器之间交换三个 TCP 报文段。称之为三报文握手。 采用三报文握手主要是为了防止已失效的连接请求报文段突然又传送到了,因而产生错误。 A 的 TCP 向 B 发出连接请求报文段,其首部中的同步位 SYN = 1,并选择序号 seq = x,表明传送数据时的第一个数据字节的序号是 x。 B 的 TCP 收到连接请求报文段后,如同意,则发回确认。B 在确认报文段中应使 SYN = 1,使 ACK = 1,其确认号ack = x + 1,自己选择的序号 seq = y。 A 收到此报文段后向 B 给出确认,其 ACK = 1,确认号 ack = y + 1。A 的 TCP 通知上层应用进程,连接已经建立。 A为什么最后还要发送一次确认呢? 主要是为了防止已失效的连接请求报文段突然又传送到了B。 假设没有第三次握手,客户端发送一个连接请求报文过去,但是因为网络延迟,在等待了一个超时时间后,客户端就会在重新发一个请求连接报文过去,然后正常的进行,服务器端发回一个确认连接报文,然后就开始通讯,通讯结束后,释放连接。但一次发送的请求报文段这时到达了服务器,服务器端不知道这个报文已经失效,也发回了一个确认连接报文,客户端接收后,发现自己并没有发送连接请求(因为超时了,所以就认为自己没有发),所以对这个确认连接请求就什么也不做,但是此时服务器端不这么认为,他认为连接已经建立了,就一直打开着等待客户端传数据过来,服务器的许多资源就被浪费。 采用三报文握手,A不会向B发送确认,B由于收不到确认,就知道A没有请求建立连接。 TCP的连接释放TCP 连接释放过程是四报文挥手。 A 的应用进程先向其 TCP 发出连接释放报文段,并停止再发送数据,主动关闭 TCP 连接。A 把连接释放报文段首部的 FIN = 1,其序号seq = u,它等于前面已传送过的数据的最后一个字节的序号加1。这时A进入FIN-WAIT(终止等待1)状态,等待B的确认。 B 发出确认,确认号 ack = u + 1,而这个报文段自己的序号 seq = v。TCP 服务器进程通知高层应用进程。从 A 到 B 这个方向的连接就释放了,TCP 连接处于半关闭状态。B到A这个方向的连接并未关闭,这个状态可能会持续一段时间。B 若发送数据,A 仍要接收。 若 B 已经没有要向 A 发送的数据,其应用进程就通知 TCP 释放连接。B还必须重复上次已发送过的确认号。这时B就进入LAST-ACK(最后确认)状态,等待A的确认。 A在收到B的连接释放报文后,对此发出确认。ACK=1,ack=w+1,seq=u+1,进入到TIME-WAIT(时间等待)状态。这时TCP连接还没有释放掉,必须经过时间等待计时器设置的2MSL后,A才进入到CLOSED状态。 B在收到确认后就进入CLOSED状态。 A必须等待2MSL时间的原因 为了保证 A 发送的最后一个 ACK 报文段能够到达 B。B收到了确认报文,才能进入CLOSED状态。 防止 “已失效的连接请求报文段”出现在本连接中。A 在发送完最后一个 ACK 报文段后,再经过时间 2MSL,就可以使本连接持续的时间内所产生的所有报文段,都从网络中消失。这样就可以使下一个新的连接中不会出现这种旧的连接请求报文段。","link":"/2022/01/18/%E8%BF%90%E8%BE%93%E5%B1%82/"},{"title":"Cookie和Session","text":"HTTP协议是一种无状态协议,即每次服务器端收到客户端的请求时,都是一个全新的请求,服务器并不知道客户端的历史请求记录。Session和Cookie的主要目的就是为了弥补HTTP的无状态特性。 Cookie 是服务器发送到浏览器的Cookie,浏览器会进行存储,并与下一个请求一起发送到服务器。通常,它用于判断两个请求是否来自于同一个浏览器,例如用户保持登陆状态。 每个Cookie的大小不能超过4KB。 如何创建Cookie? 当服务器接收到客户端发出的HTTP请求时,服务器可以发送带有响应的$Set-Cookie$表头,Cookie由浏览器存储,然后将Cookie与HTTP标头一同向服务器发出请求。 下面是发送Cookie的例子: 此表头告诉浏览器要存储Cookie。 之后,浏览器每次发出的新请求都将使用Cookie头将以前存储的Cookie发送回服务器。 常用API getName():获取名称,cookie的key。 getValue():获取值,cookie的value。 setValue():设置内容,用于修改key对应的value值。 req.getCookies():servlet接收浏览器发送的所有Cookie。 setMaxAge(int expiry):设置Cookie的存活时间。 正数:表示在指定的秒数后过期。 负数:表示浏览器关闭就会过期,默认值是-1. 零:表示马上删除Cookie。 setPath(String url):Cookie 的 path 属性可以有效的过滤哪些 Cookie 可以发送给服务器,哪些不发。 path 属性是通过请求的地址来进行有效的过滤。例如设置path=/docs,则以下地址都会匹配: /docs /docs/Web/ /docs/Web/Http Cookie不能发送中文,如果要发送中文,就需要特殊处理。 发送Cookie:Cookie cookie = new Cookie(URLEncoder.encode("哈哈"),URLEncoder.encode("呵呵")); resp.addCookie(cookie); 获取Cookie的key和value:URLDecoder.decoder(request.getCookie().getName); URLDecoder.decoder(request.getCookie().getValue); 用处 会话管理:登录、购物车等应该记住的内容。 Session 客户端请求服务端,服务端会为这次请求开辟一块内存空间,这个对象便是Session对象,存储结构为ConcurrentHashMap. Session会话中,经常用来保持用户登录之后的信息。Session是个域对象。 创建Session request.getSession():第一次调用是创建Session会话,之后都是获取之前创建好的Session会话对象。 isNew():true为刚创建的,false表示获取之前创建。 每个Session都有一个ID,是它的唯一表示,getId()得到Session的会话id值。 Session的生命周期控制:setMaxInactiveInterval(int interval)设置Session的超时时间(以秒为单位)。为负数时表示永远有效。 invalidate()表示让当前Session立即失效。 Session如何判断是否是同一会话服务器每次接收到新的请求时,会创建Session对象,同时生成一个sessionId,并创建Cookie对象,这个Cookie对象的key为:JSESSIONID,value为sessionid。通过响应头的Set-Cookie向客户端发送要设置Cookie的响应。客户端收到响应后,会创建出一个Cookie对象,并在之后的每次请求都把Session的id以Cookie形式发送给服务器。服务器通过读取Cookie信息,获取名称为JSESSIONID的值,找到之前创建好的Session对象。","link":"/2022/01/20/Cookie%E5%92%8CSession/"},{"title":"Filter过滤器","text":"Filter过滤器是JavaWeb的三大组件之一。它是JavaEE的一个接口,作用是:拦截请求,过滤响应。比较常见的应用场景有:权限检查、日记操作、事务管理等。 Filter的使用当服务器端端收到客户端的请求时,先通过Filter过滤器检查对于要访问的资源是否有权限,如果有权限,程序就默认继续执行,如果没有权限,就跳转到其他界面进行提示。比如要进入后台管理界面,先检查是否登录,如果未登录跳到登录界面,登录了就默认进入后台管理界面。 1234567891011@Overridepublic void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest; User loginUser = (User) httpServletRequest.getSession().getAttribute("user"); if(loginUser==null){ httpServletRequest.getRequestDispatcher("/pages/user/login.jsp").forward(servletRequest,servletResponse); }else{ //放行,必须加上这句才继续进行正常动作 filterChain.doFilter(servletRequest,servletResponse); }} 使用步骤 编写一个类实现Filter接口。 实现过滤方法doFilter()。 在web.xml中配置Filter程序拦截的路径。 web.xml关于Filter的内容如下: 1234567891011121314<filter> <filter-name>ManagerFilter</filter-name> <filter-class>com.atguigu.filter.ManagerFilter</filter-class> <init-param> <param-name>username</param-name> <param-value>root</param-value> </init-param></filter><filter-mapping> <filter-name>ManagerFilter</filter-name> <!--拦截路径,要访问这些都要经过Filter过滤--> <url-pattern>/pages/manager/*</url-pattern> <url-pattern>/manager/bookServlet</url-pattern></filter-mapping> 配置基本和Servlet一样,<url-pattern>可以有多个,即可以过滤多个访问地址。 Filter生命周期 构造器方法 init初始化方法。//前两个在web工程启动时就执行,Filter已经创建。 doFilter过滤方法。//每次拦截到请求就会执行。 destroy摧毁方法。//停止web工程时会执行。 FilterConfig类是Filter过滤器的配置文件类。Tomcat每次创建Filter时,就会同时创建一个FilterConfig类的对象,包含了Filter的配置信息。 getFilterName():获取Filter的名称。 getInitParameter():获取Filter中配置的init-param参数。 getServletContext():获取ServletContext对象。 FilterChain过滤器链 FilterChain.doFilter()方法:执行下一个Filter过滤器(如果还有Filter),执行目标资源(没有Filter)。 多个Filter程序的执行顺序是由它们在web.xml中的配置顺序决定的。 所有Filter和目标资源默认在同一个线程中。 多个Filter对象共同执行时,使用同一个request对象。 Filter的拦截路径 精确匹配:<url-pattern>/target.jsp</url-pattern>表示请求地址为http://ip:port/工程路径/target.jsp才会被拦截。 目标匹配:<url-pattern>/admin/*</url-pattern>表示请求地址为http://ip:port/工程路径/admin/下的资源才会被拦截。 后缀名匹配:<url-pattern>*.html</url-pattern>表示请求地址以.html结尾才会被拦截。这个后缀名也可以随意写,Filter 过滤器它只关心请求的地址是否匹配,不关心请求的资源是否存在。","link":"/2022/01/23/Filter%E8%BF%87%E6%BB%A4%E5%99%A8/"},{"title":"应用层","text":"应用层是体系结构的最高层。应用层的任务是通过应用进程间的交互来完成特定网络网络应用。应用层协议定义的是应用进程间通信和交互的规则。应用层的协议很多,比如域名系统DNS,支持万维网应用的HTTP协议,支持电子邮件的SMTP协议等等。应用层交互的数据单元称为报文。","link":"/2022/01/21/%E5%BA%94%E7%94%A8%E5%B1%82/"},{"title":"MySQL约束","text":"为了保证数据的完整性,SQL规范以约束的方式对表数据进行额外的条件限制。 数据完整性 实体完整性: 实体完整性中的实体指的是表中的行,因为一行记录对应一个实体 实体完整性规定表的一行在表中是唯一的实体,不能出现重复。 实体完整性通过表的主键来实现。 参照完整性: 参照完整性指的就是多表之间的设计,主要使用外键约束。 例如:员工所在部门,在部门表中要能找到这个部门 用户自定义完整性: 例如:用户名唯一、密码不能为空等,本部门经理的工资不得高于本部门职工的平均工资的5倍。 非空约束 限定某个字段/某列的值不允许为空 NOT NULL 非空约束只能出现在表对象的列上,只能某个列单独限定非空,不能组合非空 一个表可以有很多列都分别限定了非空 添加非空约束建表时 12345CREATE TABLE 表名称( 字段名 数据类型, 字段名 数据类型 NOT NULL, 字段名 数据类型 NOT NULL); 建表后 1alter table tableName Modify 字段名 数据类型 NOT NULL; 删除非空约束123alter table 表名称 modify 字段名 数据类型 NULL;#去掉not null,相当于修改某个非注解字段,该字段允许为空或 alter table 表名称 modify 字段名 数据类型;#去掉not null,相当于修改某个非注解字段,该字段允许为空 唯一性约束 用来限制某个字段/某列的值不能重复 UNIQUE 唯一约束可以是某一个列的值唯一,也可以多个列组合的值唯一。 唯一性约束允许列值为空。 在创建唯一约束的时候,如果不给唯一约束命名,就默认和列名相同。 MySQL会给唯一约束的列上默认创建一个唯一索引。 添加唯一约束建表时 123456789create table 表名称( 字段名 数据类型, 字段名 数据类型 unique, 字段名 数据类型 unique key,);create table 表名称( 字段名 数据类型, [constraint 约束名] unique key(字段名) #表级约束); 举例 1234567CREATE TABLE USER( id INT NOT NULL unique, NAME VARCHAR(25), PASSWORD VARCHAR(16), -- 使用表级约束语法 CONSTRAINT uk_name_pwd UNIQUE(NAME,PASSWORD)); 建表后指定唯一键约束 12345#字段列表中如果是一个字段,表示该列的值唯一。如果是两个或更多个字段,那么复合唯一,即多个字段的组合是唯一的#方式1:alter table 表名称 add unique key(字段列表); #方式2:alter table 表名称 modify 字段名 字段类型 unique; 举例 123ALTER TABLE USER ADD UNIQUE(NAME,PASSWORD);ALTER TABLE USER ADD CONSTRAINT uk_name_pwd UNIQUE(NAME,PASSWORD);ALTER TABLE USER MODIFY NAME VARCHAR(20) UNIQUE; 删除唯一约束 删除唯一约束只能通过删除唯一索引的方式删除。 删除时需要指定唯一索引名,唯一索引名就和唯一约束名一样。 如果创建唯一约束时未指定名称,如果是单列,就默认和列名相同;如果是组合列,那么默认和()中排在第一个的列名相同。也可以自定义唯一性约束名。 1SELECT * FROM information_schema.table_constraints WHERE table_name = '表名'; #查看都有哪些约束 1ALTER TABLE USER DROP INDEX uk_name_pwd; #删除索引 可以通过show index from tableName查看表的索引 PRIMARY KEY约束 用来唯一标识表中的一行记录。primary key 主键约束相当于唯一约束+非空约束的组合,主键约束列不允许重复,也不允许出现空值 一个表最多只能有一个主键约束,建立主键约束可以在列级别创建,也可以在表级别上创建。 主键约束对应着表中的一列或者多列(复合主键) 如果是多列组合的复合主键约束,那么这些列都不允许为空值,并且组合的值不允许重复。 MySQL的主键名总是PRIMARY,就算自己命名了主键约束名也没用。 当创建主键约束时,系统默认会在所在的列或列组合上建立对应的主键索引(能够根据主键查询的,就根据主键查询,效率更高)。如果删除主键约束了,主键约束对应的索引就自动删除了。 需要注意的一点是,不要修改主键字段的值。因为主键是数据记录的唯一标识,如果修改了主键的值,就有可能会破坏数据的完整性。 添加主键约束建表时 1234567create table 表名称( 字段名 数据类型 primary key, #列级模式);create table 表名称( 字段名 数据类型, [constraint 约束名] primary key(字段名) #表级模式); 建表后 1ALTER TABLE 表名称 ADD PRIMARY KEY(字段列表); #字段列表可以是一个字段,也可以是多个字段,如果是多个字段的话,是复合主键 删除主键约束1alter table 表名称 drop primary key; 说明:删除主键约束,不需要指定主键名,因为一个表只有一个主键,删除主键约束后,非空还存在。 自增列:AUTO_INCREMENT 设置某个字段的值自增。auto_increment 一个表最多只能有一个自增长列 自增长列约束的列必须是键列(主键列,唯一键列) 自增约束的列的数据类型必须是整数类型 如果自增列指定了 0 和 null,会在当前最大值的基础上自增;如果自增列手动指定了具体值,直接赋值为具体值。 指定自增约束建表时 123456789101112create table 表名称( 字段名 数据类型 primary key auto_increment, 字段名 数据类型 unique key not null, 字段名 数据类型 unique key, 字段名 数据类型 not null default 默认值, );create table 表名称( 字段名 数据类型 default 默认值 , 字段名 数据类型 unique key auto_increment, 字段名 数据类型 not null default 默认值,, primary key(字段名)); 建表后 1alter table 表名称 modify 字段名 数据类型 auto_increment; 删除自增约束1alter table 表名称 modify 字段名 数据类型; #去掉auto_increment相当于删除,和not null差不多 FOREIGN KEY 约束 限定某个表的某个字段的引用完整性。FOREIGN KEY 从表的外键列,必须引用/参考主表的主键或唯一约束的列。因为被依赖/被参考的值必须是唯一的。 在创建外键约束时,如果不给外键约束命名,默认名不是列名,而是自动产生一个外键名(例如 student_ibfk_1;),也可以指定外键约束名。 创建(CREATE)表时就指定外键约束的话,先创建主表,再创建从表 删表时,先删从表(或先删除外键约束),再删除主表 从表的外键列与主表被参照的列名字可以不相同,但是数据类型必须一样,逻辑意义一致。 当创建外键约束时,系统默认会在所在的列上建立对应的普通索引。但是索引名是外键的约束名。(根据外键查询效率很高) 删除外键约束后,必须手动删除对应的索引 外键约束(FOREIGN KEY)不能跨引擎使用。 添加外键约束建表时 123456789101112create table 主表名称( 字段1 数据类型 primary key, 字段2 数据类型);create table 从表名称( 字段1 数据类型 primary key, 字段2 数据类型, [CONSTRAINT <外键约束名称>] FOREIGN KEY(从表的某个字段) references 主表名(被参考字段));说明:(1)主表dept必须先创建成功,然后才能创建emp表,指定外键成功。(2)删除表时,先删除从表emp,再删除主表dept 建表后 1ALTER TABLE 从表名 ADD [CONSTRAINT 约束名] FOREIGN KEY (从表的字段) REFERENCES 主表名(被引用字段) [on update xx][on delete xx]; 约束等级 Cascade方式:在父表上update/delete记录时,同步update/delete掉子表的匹配记录 Set null方式:在父表上update/delete记录时,将子表上匹配记录的列设为null,但是要注意子表的外键列不能为not null No action方式:如果子表中有匹配的记录,则不允许对父表对应候选键进行update/delete操作 Restrict方式:同no action, 都是立即检查外键约束 Set default方式(在可视化工具SQLyog中可能显示空白):父表有变更时,子表将外键列设置成一个默认的值,但Innodb不能识别 如果没有指定等级,就相当于Restrict方式。 对于外键约束,最好是采用: ON UPDATE CASCADE ON DELETE RESTRICT 的方式。 删除外键约束123456789(1)第一步先查看约束名和删除外键约束SELECT * FROM information_schema.table_constraints WHERE table_name = '表名称';#查看某个表的约束名ALTER TABLE 从表名 DROP FOREIGN KEY 外键约束名;(2)第二步查看索引名和删除索引。(注意,只能手动删除)SHOW INDEX FROM 表名称; #查看某个表的索引名ALTER TABLE 从表名 DROP INDEX 索引名; 阿里巴巴开发规范指出:不得使用外键与级联,一切外键概念必须在应用层解决。 外键与级联更新适用于单机低并发,不适合分布式、高并发集群;级联更新是强阻塞,存在数据库更新风暴的风险;外键影响数据库的插入速度。 CHECK 约束 检查某个字段的值是否符号xx要求,一般指的是值的范围。check MySQL5.7 可以使用check约束,但check约束对数据验证没有任何作用。添加数据时,没有任何错误或警告。但MySQL8.0可以使用了。 举例 123456create table employee( eid int primary key, ename varchar(5), gender char check ('男' or '女') CHECK(eid>=0 AND eid<3)); DEFAULT约束 给某个字段/某列指定默认值,一旦设置默认值,在插入数据时,如果此字段没有显式赋值,则赋值为默认值。DEFAULT 给字段加默认值建表时 123456create table employee( eid int primary key, ename varchar(20) not null, gender char default '男', tel char(11) not null default '' #默认是空字符串); 建表后 1234alter table 表名称 modify 字段名 数据类型 default 默认值;#如果这个字段原来有非空约束,你还保留非空约束,那么在加默认值约束时,还得保留非空约束,否则非空约束就被删除了#同理,在给某个字段加非空约束也一样,如果这个字段原来有默认值约束,你想保留,也要在modify语句中保留默认值约束,否则就删除了alter table 表名称 modify 字段名 数据类型 default 默认值 not null; 删除默认值约束12alter table 表名称 modify 字段名 数据类型 ;#删除默认值约束,也不保留非空约束alter table 表名称 modify 字段名 数据类型 not null; #删除默认值约束,保留非空约束","link":"/2022/01/22/MySQL%E7%BA%A6%E6%9D%9F/"},{"title":"文件上传下载与一次性验证码","text":"文件上传下载功能十分常见,本篇文章记录一下操作流程。 文件上传流程 要有一个 form 标签,method=post 请求。 form 标签的 encType 属性值必须为 multipart/form-data值。 在 form 标签中使用 input type=file 添加上传的文件。 编写服务器代码(Servlet 程序)接收,处理上传的数据。 encType=multipart/form-data 表示提交的数据,以多段(每一个表单项一个数据段)的形式进行拼接,然后以二进制流的形式发送给服务器。 需要使用到commons-fileupload-1.2.1.jar 和commons-io-1.4.jar这两个jar包。 ServletFileUpload 类,用于解析上传的数据。 FileItem 类,表示每一个表单项。 boolean ServletFileUpload.isMultipartContent(HttpServletRequest request);判断当前上传的数据格式是否是多段的格式。 public List parseRequest(HttpServletRequest request)解析上传的数据。 boolean FileItem.isFormField()。判断当前这个表单项是普通的表单项,还是上传的文件类型。 String FileItem.getFieldName()。获取表单项name值。 String FileItem.getString()。获取表单项的值。 String FileItem.getName()。获取上传的文件名。 void FileItem.write( file )。将上传的文件写到 参数 file 所指向抽硬盘位置 。 上传文件的表单: 123456<form action="http://192.168.31.74:8080/jsp/upload" method="post" enctype="multipart/form-data"> 用户名:<input type="text" name="username" /> <br> 头像:<input type="file" name="photo" > <br> <input type="submit" value="上传"></form> 解析上传数据的代码: 12345678910111213141516171819202122232425262728@Overrideprotected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { System.out.println("文件上传"); //判断上传的数据是否是多段数据,只有多段数据才是文件上传的 if(ServletFileUpload.isMultipartContent(req)){ //创建fileItemFactory工厂实现类 FileItemFactory fileItemFactory = new DiskFileItemFactory(); //创建解析上传数据的ServletFileUpload对象 ServletFileUpload servletFileUpload = new ServletFileUpload(fileItemFactory); try { List<FileItem> list = servletFileUpload.parseRequest(req); for(FileItem fileItem:list){ if(fileItem.isFormField()){ //是普通表单项 System.out.println("表单属性名"+fileItem.getFieldName()); System.out.println("表单属性值"+fileItem.getString("UTF-8")); }else{ //是上传的文件 System.out.println("表单属性名"+fileItem.getFieldName()); System.out.println("上传的文件名"+fileItem.getName()); fileItem.write(new File("e:\\\\"+fileItem.getName())); } } } catch (Exception e) { e.printStackTrace(); } }} 当文件上传时,http请求头中Content-Type内容: 文件下载常用API response.getOutputStream(); servletContext.getResourceAsStream(); servletContext.getMimeType(); response.setContentType(); response.setHeader(“Content-Disposition”, “attachment; fileName=1.jpg”); 这个响应头告诉浏览器。这是需要下载的。而 attachment 表示附件,也就是下载的一个文件。fileName=后面, 表示下载的文件名。 1234567891011121314151617181920@Overrideprotected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String downloadName = "a.jpg"; ServletContext servletContext = getServletContext(); //获取要下载的文件类型 String type = servletContext.getMimeType("/file/" + downloadName); System.out.println(type); //回传时,通过响应头告诉客户端返回的数据类型 resp.setContentType(type); //设置响应头告诉客户端收到的数据如何处理 //attachment表示附件 //filename是文件名 resp.setHeader("Content-Disposition","attachment;filename="+downloadName); //通过servletContext获取文件输入流 InputStream is = servletContext.getResourceAsStream("/file/" + downloadName); //response获取输出流 ServletOutputStream os = resp.getOutputStream(); //读取输入流的数据复制到输出流给客户端 IOUtils.copy(is,os);} 一次性验证码的使用表单重复提交有三种常见的情况: 一:提交完表单。服务器使用请求转来进行页面跳转。这个时候,用户按下功能键 F5,就会发起最后一次的请求。 造成表单重复提交问题。解决方法:使用重定向来进行跳转 二:用户正常提交服务器,但是由于网络延迟等原因,迟迟未收到服务器的响应,这个时候,用户以为提交失败, 就会着急,然后多点了几次提交操作,也会造成表单重复提交。 三:用户正常提交服务器。服务器也没有延迟,但是提交完成后,用户回退浏览器。重新提交。也会造成表单重复 提交。 使用谷歌验证码jar包:kaptcha-2.3.2.jar,在web.xml中配置Servlet程序。 在表单中使用 img 标签去显示验证码图片并使用它。 1234// 获取 Session 中的验证码String token = (String) req.getSession().getAttribute(KAPTCHA_SESSION_KEY);// 删除 Session 中的验证码,以防再次使用req.getSession().removeAttribute(KAPTCHA_SESSION_KEY);","link":"/2022/01/23/%E6%96%87%E4%BB%B6%E4%B8%8A%E4%BC%A0%E4%B8%8B%E8%BD%BD%E4%B8%8E%E4%B8%80%E6%AC%A1%E6%80%A7%E9%AA%8C%E8%AF%81%E7%A0%81/"},{"title":"SpringMVC","text":"SpringMVC是Spring的一个后续产品,是Spring的一个子项目。SpringMVC 是 Spring 为表述层开发提供的一整套完备的解决方案。基于原生的Servlet,通过了功能强大的前端控制器DispatcherServlet,对请求和响应进行统一处理。 什么是MVCMVC是一种软件架构的思想,将软件按照模型、视图、控制器来划分 M:Model,模型层,指工程中的JavaBean,作用是处理数据 JavaBean分为两类: 一类称为实体类Bean:专门存储业务数据的,如 Student、User 等 一类称为业务处理 Bean:指 Service 或 Dao 对象,专门用于处理业务逻辑和数据访问。 V:View,视图层,指工程中的html或jsp等页面,作用是与用户进行交互,展示数据 C:Controller,控制层,指工程中的servlet,作用是接收请求和响应浏览器 MVC的工作流程:用户通过视图层发送请求到服务器,在服务器中请求被Controller接收,Controller调用相应的Model层处理请求,处理完毕将结果返回到Controller,Controller再根据请求处理的结果找到相应的View视图,渲染数据后最终响应给浏览器。 环境搭建创建maven工程a>添加web模块b>打包方式:warc>引入依赖123456789101112131415161718192021222324252627<dependencies> <!-- springmvc --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>5.3.15</version> </dependency> <!--日志--> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>1.2.3</version> </dependency> <!-- servlet-api --> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>3.1.0</version> <scope>provided</scope> </dependency> <!-- Spring5和Thymeleaf整合包 --> <dependency> <groupId>org.thymeleaf</groupId> <artifactId>thymeleaf-spring5</artifactId> <version>3.0.12.RELEASE</version> </dependency></dependencies> 配置web.xml通过init-param标签设置SpringMVC配置文件的位置和名称,通过load-on-startup标签设置SpringMVC前端控制器DispatcherServlet的初始化时间 123456789101112131415161718192021222324252627<!-- 配置SpringMVC的前端控制器,对浏览器发送的请求统一进行处理 --><servlet> <servlet-name>springMVC</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <!-- 通过初始化参数指定SpringMVC配置文件的位置和名称 --> <init-param> <!-- contextConfigLocation为固定值 --> <param-name>contextConfigLocation</param-name> <!-- 使用classpath:表示从类路径查找配置文件,例如maven工程中的src/main/resources --> <param-value>classpath:springMVC.xml</param-value> </init-param> <!-- 作为框架的核心组件,在启动过程中有大量的初始化操作要做 而这些操作放在第一次请求时才执行会严重影响访问速度 因此需要通过此标签将启动控制DispatcherServlet的初始化时间提前到服务器启动时 --> <load-on-startup>1</load-on-startup></servlet><servlet-mapping> <servlet-name>springMVC</servlet-name> <!-- 设置springMVC的核心控制器所能处理的请求的请求路径 /所匹配的请求可以是/login或.html或.js或.css方式的请求路径 但是/不能匹配.jsp请求路径的请求 --> <url-pattern>/</url-pattern></servlet-mapping> <url-pattern>标签中使用/和/*的区别: /所匹配的请求可以是/login或.html或.js或.css方式的请求路径,但是/不能匹配.jsp请求路径的请求 因此就可以避免在访问jsp页面时,该请求被DispatcherServlet处理,从而找不到相应的页面 /*则能够匹配所有请求,例如在使用过滤器时,若需要对所有请求进行过滤,就需要使用/*的写法 创建SpringMVC配置文件12345678910111213141516171819202122232425262728293031323334353637383940414243444546<!-- 自动扫描包 --><context:component-scan base-package="com.atguigu.mvc.controller"/><!-- 配置Thymeleaf视图解析器 --><bean id="viewResolver" class="org.thymeleaf.spring5.view.ThymeleafViewResolver"> <property name="order" value="1"/> <property name="characterEncoding" value="UTF-8"/> <property name="templateEngine"> <bean class="org.thymeleaf.spring5.SpringTemplateEngine"> <property name="templateResolver"> <bean class="org.thymeleaf.spring5.templateresolver.SpringResourceTemplateResolver"> <!-- 视图前缀 --> <property name="prefix" value="/WEB-INF/templates/"/> <!-- 视图后缀 --> <property name="suffix" value=".html"/> <property name="templateMode" value="HTML5"/> <property name="characterEncoding" value="UTF-8" /> </bean> </property> </bean> </property></bean><!-- 处理静态资源,例如html、js、css、jpg 若只设置该标签,则只能访问静态资源,其他请求则无法访问 此时必须设置<mvc:annotation-driven/>解决问题 --><mvc:default-servlet-handler/><!-- 开启mvc注解驱动 --><mvc:annotation-driven> <mvc:message-converters> <!-- 处理响应中文内容乱码 --> <bean class="org.springframework.http.converter.StringHttpMessageConverter"> <property name="defaultCharset" value="UTF-8" /> <property name="supportedMediaTypes"> <list> <value>text/html</value> <value>application/json</value> </list> </property> </bean> </mvc:message-converters></mvc:annotation-driven> 创建请求控制器由于前端控制器对浏览器发送的请求进行了统一的处理,但是具体的请求有不同的处理过程,因此需要创建处理具体请求的类,即请求控制器 请求控制器中每一个处理请求的方法成为控制器方法 因为SpringMVC的控制器由一个POJO(普通的Java类)担任,因此需要通过@Controller注解将其标识为一个控制层组件,交给Spring的IoC容器管理,此时SpringMVC才能够识别控制器的存在 1234@Controllerpublic class HelloController { } 测试访问首页在请求控制器中创建处理请求的方法 12345678// @RequestMapping注解:处理请求和控制器方法之间的映射关系// @RequestMapping注解的value属性可以通过请求地址匹配请求,/表示的当前工程的上下文路径// localhost:8080/springMVC/@RequestMapping("/")public String index() { //设置视图名称 return "index";} 总结浏览器发送请求,若请求地址符合前端控制器的url-pattern,该请求就会被前端控制器DispatcherServlet处理。前端控制器会读取SpringMVC的核心配置文件,通过扫描组件找到控制器,将请求地址和控制器中@RequestMapping注解的value属性值进行匹配,若匹配成功,该注解所标识的控制器方法就是处理请求的方法。处理请求的方法需要返回一个字符串类型的视图名称,该视图名称会被视图解析器解析,加上前缀和后缀组成视图的路径,通过Thymeleaf对视图进行渲染,最终转发到视图所对应页面。 Crontroller中定义void方法时,一定要声明HttpServletResponse类型的参数,否则SpringMVC会自动从上下文对象获取,并认为@RequestMapping中的路径就是要返回的视图,导致出现错误。处理方案可以在方法上加上@ResponseBody,或者类上的@Crontroller改成@RestCrontroller,浏览器会跳到空白页. @RequestMapping注解@RequestMapping注解的功能从注解名称上我们可以看到,@RequestMapping注解的作用就是将请求和处理请求的控制器方法关联起来,建立映射关系。 SpringMVC 接收到指定的请求,就会来找到在映射关系中对应的控制器方法来处理这个请求。 @RequestMapping注解的位置 @RequestMapping标识一个类:设置映射请求的请求路径的初始信息 @RequestMapping标识一个方法:设置映射请求请求路径的具体信息。路径会加上类被标识的路径。 @RequestMapping注解的value属性 @RequestMapping注解的value属性通过请求的请求地址匹配请求映射 @RequestMapping注解的value属性是一个字符串类型的数组,表示该请求映射能够匹配多个请求地址所对应的请求 @RequestMapping注解的value属性必须设置,至少通过请求地址匹配请求映射 @RequestMapping注解的method属性 @RequestMapping注解的method属性通过请求的请求方式(get或post)匹配请求映射 @RequestMapping注解的method属性是一个RequestMethod类型的数组,表示该请求映射能够匹配多种请求方式的请求 若当前请求的请求地址满足请求映射的value属性,但是请求方式不满足method属性,则浏览器报错405:Request method ‘POST’ not supported 注: 1、对于处理指定请求方式的控制器方法,SpringMVC中提供了@RequestMapping的派生注解 处理get请求的映射–>@GetMapping 处理post请求的映射–>@PostMapping 处理put请求的映射–>@PutMapping 处理delete请求的映射–>@DeleteMapping @RequestMapping注解的params属性 @RequestMapping注解的params属性通过请求的请求参数匹配请求映射 @RequestMapping注解的params属性是一个字符串类型的数组,可以通过四种表达式设置请求参数和请求映射的匹配关系 “param”:要求请求映射所匹配的请求必须携带param请求参数 “!param”:要求请求映射所匹配的请求必须不能携带param请求参数 “param=value”:要求请求映射所匹配的请求必须携带param请求参数且param=value “param!=value”:要求请求映射所匹配的请求必须携带param请求参数但是param!=value 12345678@RequestMapping( value = {"/testRequestMapping", "/test"} ,method = {RequestMethod.GET, RequestMethod.POST} ,params = {"username","password!=123456"})public String testRequestMapping(){ return "success";} 注: 若当前请求满足@RequestMapping注解的value和method属性,但是不满足params属性,此时页面回报错400。 @RequestMapping注解的headers属性用法同params属性。 若当前请求满足@RequestMapping注解的value和method属性,但是不满足headers属性,此时页面显示404错误,即资源未找到。 SpringMVC支持ant风格的路径 ?:表示任意的单个字符 *:表示任意的0个或多个字符 **:表示任意的一层或多层目录 注意:在使用**时,只能使用/**/xxx的方式 SpringMVC支持路径中的占位符原始方式:/deleteUser?id=1 rest方式:/deleteUser/1 SpringMVC路径中的占位符常用于RESTful风格中,当请求路径中将某些数据通过路径的方式传输到服务器中,就可以在相应的@RequestMapping注解的value属性中通过占位符{xxx}表示传输的数据,在通过@PathVariable注解,将占位符所表示的数据赋值给控制器方法的形参. 1<a th:href="@{/testRest/1/admin}">测试路径中的占位符-->/testRest</a><br> 123456@RequestMapping("/testRest/{id}/{username}")public String testRest(@PathVariable("id") String id, @PathVariable("username") String username){ System.out.println("id:"+id+",username:"+username); return "success";}//最终输出的内容为-->id:1,username:admin SpringMVC获取请求参数通过控制器方法的形参获取请求参数在控制器方法的形参位置,设置和请求参数同名的形参,当浏览器发送请求,匹配到请求映射时,在DispatcherServlet中就会将请求参数赋值给相应的形参. 注: 若请求所传输的请求参数中有多个同名的请求参数(比如多选),此时可以在控制器方法的形参中设置字符串数组或者字符串类型的形参接收此请求参数 @RequestParam@RequestParam是将请求参数和控制器方法的形参创建映射关系 @RequestParam注解一共有三个属性: value:指定为形参赋值的请求参数的参数名 required:设置是否必须传输此请求参数,默认值为true defaultValue:不管required属性值为true或false,当value所指定的请求参数没有传输或传输的值为””时,则使用默认值为形参赋值 @RequestHeader@RequestHeader是将请求头信息和控制器方法的形参创建映射关系 @RequestHeader注解一共有三个属性:value、required、defaultValue,用法同@RequestParam @CookieValue@CookieValue是将cookie数据和控制器方法的形参创建映射关系 @CookieValue注解一共有三个属性:value、required、defaultValue,用法同@RequestParam 123456789101112@RequestMapping(value = "/param")public String testServletParam( //@RequestParam表示请求参数,value是界面中的参数 @RequestParam(value = "user_name",required = true,defaultValue = "xixi") String username, @RequestHeader(value = "Host") String host, @CookieValue("JSESSIONID") String JSESSIONID){ System.out.println("username:"+username); System.out.println("host:"+host); System.out.println("JESSIONID:"+JSESSIONID); return "success";} 通过POJO获取请求参数可以在控制器方法的形参位置设置一个实体类类型的形参,此时若浏览器传输的请求参数的参数名和实体类中的属性名一致,那么请求参数就会为此属性赋值。 解决获取请求参数的乱码问题解决获取请求参数的乱码问题,可以使用SpringMVC提供的编码过滤器CharacterEncodingFilter,但是必须在web.xml中进行注册。 阅读源码可知,需要设置encoding为UTF-8,forceResponseEncoding为true 1234567891011121314151617<!--配置springMVC的编码过滤器--><filter> <filter-name>CharacterEncodingFilter</filter-name> <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>UTF-8</param-value> </init-param> <init-param> <param-name>forceResponseEncoding</param-name> <param-value>true</param-value> </init-param></filter><filter-mapping> <filter-name>CharacterEncodingFilter</filter-name> <url-pattern>/*</url-pattern></filter-mapping> 注: SpringMVC中处理编码的过滤器一定要配置到其他过滤器之前,否则无效 域对象共享数据使用ModelAndView向request域对象共享数据1234567891011121314@RequestMapping("/testModelAndView")public ModelAndView testModelAndView(){ /** * ModelAndView有Model和View的功能 * Model主要用于向请求域共享数据 * View主要用于设置视图,实现页面跳转 */ ModelAndView mav = new ModelAndView(); //向请求域共享数据 mav.addObject("testScope", "hello,ModelAndView"); //设置视图,实现页面跳转 mav.setViewName("success"); return mav;} 使用Model向request域对象共享数据12345@RequestMapping("/testModel")public String testModel(Model model){ model.addAttribute("testScope", "hello,Model"); return "success";} 使用map向request域对象共享数据12345@RequestMapping("/testMap")public String testMap(Map<String, Object> map){ map.put("testScope", "hello,Map"); return "success";} 使用ModelMap向request域对象共享数据12345@RequestMapping("/testModelMap")public String testModelMap(ModelMap modelMap){ modelMap.addAttribute("testScope", "hello,ModelMap"); return "success";} Model、ModelMap、Map的关系Model、ModelMap、Map类型的参数其实本质上都是 BindingAwareModelMap 类型的 1234public interface Model{}public class ModelMap extends LinkedHashMap<String, Object> {}public class ExtendedModelMap extends ModelMap implements Model {}public class BindingAwareModelMap extends ExtendedModelMap {} 向session域共享数据12345@RequestMapping("/testSession")public String testSession(HttpSession session){ session.setAttribute("testSessionScope", "hello,session"); return "success";} 向application域共享数据123456@RequestMapping("/testApplication")public String testApplication(HttpSession session){ ServletContext application = session.getServletContext(); application.setAttribute("testApplicationScope", "hello,application"); return "success";} SpringMVC的视图 SpringMVC中的视图是View接口,视图的作用渲染数据,将模型Model中的数据展示给用户 SpringMVC视图的种类很多,默认有转发视图和重定向视图 若使用的视图技术为Thymeleaf,在SpringMVC的配置文件中配置了Thymeleaf的视图解析器,由此视图解析器解析之后所得到的是ThymeleafView ThymeleafView当控制器方法中所设置的视图名称没有任何前缀时,此时的视图名称会被SpringMVC配置文件中所配置的视图解析器解析,视图名称拼接视图前缀和视图后缀所得到的最终路径,会通过转发的方式实现跳转 转发视图 SpringMVC中默认的转发视图是InternalResourceView SpringMVC中创建转发视图的情况: 当控制器方法中所设置的视图名称以”forward:“为前缀时,创建InternalResourceView视图,此时的视图名称不会被SpringMVC配置文件中所配置的视图解析器解析,而是会将前缀”forward:”去掉,剩余部分作为最终路径通过转发的方式实现跳转 重定向视图 SpringMVC中默认的重定向视图是RedirectView 当控制器方法中所设置的视图名称以”redirect:”为前缀时,创建RedirectView视图,此时的视图名称不会被SpringMVC配置文件中所配置的视图解析器解析,而是会将前缀”redirect:”去掉,剩余部分作为最终路径通过重定向的方式实现跳转 视图控制器view-controller当控制器方法中,仅仅用来实现页面跳转,即只需要设置视图名称时,可以将处理器方法使用view-controller标签进行表示 12345<!-- path:设置处理的请求地址 view-name:设置请求地址所对应的视图名称--><mvc:view-controller path="/testView" view-name="success"></mvc:view-controller> 注 当SpringMVC中设置任何一个view-controller时,其他控制器中的请求映射将全部失效,此时需要在SpringMVC的核心配置文件中设置开启mvc注解驱动的标签: <mvc:annotation-driven /> RESTfulRESTful的实现 具体说,就是 HTTP 协议里面,四个表示操作方式的动词:GET、POST、PUT、DELETE。 它们分别对应四种基本操作:GET 用来获取资源,POST 用来新建资源,PUT 用来更新资源,DELETE 用来删除资源。 REST 风格提倡 URL 地址使用统一的风格设计,从前到后各个单词使用斜杠分开,不使用问号键值对方式携带请求参数,而是将要发送给服务器的数据作为 URL 地址的一部分,以保证整体风格的一致性。 操作 REST风格 传统方式 查询操作 user/1–>get请求方式 getUserById?id=1 保存操作 user–>post请求方式 saveUser 删除操作 user/1–>delete请求方式 deleteUser?id=1 更新操作 user–>put请求方式 updateUser HiddenHttpMethodFilter由于浏览器只支持发送get和post方式的请求,那么该如何发送put和delete请求呢? SpringMVC 提供了 HiddenHttpMethodFilter 帮助我们将 POST 请求转换为 DELETE 或 PUT 请求 HiddenHttpMethodFilter 处理put和delete请求的条件: a>当前请求的请求方式必须为post b>当前请求必须传输请求参数_method 满足以上条件,HiddenHttpMethodFilter 过滤器就会将当前请求的请求方式转换为请求参数_method的值,因此请求参数_method的值才是最终的请求方式 12345678<filter> <filter-name>HiddenHttpMethodFilter</filter-name> <filter-class>org.springframework.web.filter.HiddenHttpMethodFilter</filter-class></filter><filter-mapping> <filter-name>HiddenHttpMethodFilter</filter-name> <url-pattern>/*</url-pattern></filter-mapping> 注: 在web.xml中注册时,必须先注册CharacterEncodingFilter,再注册HiddenHttpMethodFilter 原因: 在 CharacterEncodingFilter 中通过 request.setCharacterEncoding(encoding) 方法设置字符集的 request.setCharacterEncoding(encoding) 方法要求前面不能有任何获取请求参数的操作 而 HiddenHttpMethodFilter 恰恰有一个获取请求方式的操作: ```javaString paramValue = request.getParameter(this.methodParam);//判断是否为POST请求 1234567891011121314151617181920## HttpMessageConverter- HttpMessageConverter,报文信息转换器,将请求报文转换为Java对象,或将Java对象转换为响应报文- HttpMessageConverter提供了两个注解和两个类型:@RequestBody,@ResponseBody,RequestEntity,ResponseEntity### @RequestBody@RequestBody可以获取请求体,需要在控制器方法设置一个**形参**,使用@RequestBody进行标识,当前请求的请求体就会为当前注解所标识的形参赋值```java@RequestMapping("/testRequestBody")public String testRequestBody(@RequestBody String requestBody){ System.out.println("requestBody:"+requestBody); return "success";}输出结果:requestBody:username=admin&password=123456 RequestEntityRequestEntity封装请求报文的一种类型,需要在控制器方法的形参中设置该类型的形参,当前请求的请求报文就会赋值给该形参,可以通过getHeaders()获取请求头信息,通过getBody()获取请求体信息 123456@RequestMapping("/testRequestEntity")public String testRequestEntity(RequestEntity<String> requestEntity){ System.out.println("requestHeader:"+requestEntity.getHeaders()); System.out.println("requestBody:"+requestEntity.getBody()); return "success";} @ResponseBody@ResponseBody用于标识一个控制器方法,可以将该方法的返回值直接作为响应报文的响应体响应到浏览器 12345@RequestMapping("/testResponseBody")@ResponseBodypublic String testResponseBody(){ return "success";} 结果:浏览器页面显示success SpringMVC处理json@ResponseBody处理json的步骤: a>导入jackson的依赖 12345<dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.12.1</version></dependency> b>在SpringMVC的核心配置文件中开启mvc的注解驱动,此时在HandlerAdaptor中会自动装配一个消息转换器:MappingJackson2HttpMessageConverter,可以将响应到浏览器的Java对象转换为Json格式的字符串 1<mvc:annotation-driven /> c>在处理器方法上使用@ResponseBody注解进行标识 d>将Java对象直接作为控制器方法的返回值返回,就会自动转换为Json格式的字符串 12345@RequestMapping("/testResponseUser")@ResponseBodypublic User testResponseUser(){ return new User(1001,"admin","123456",23,"男");} 浏览器的页面中展示的结果: {“id”:1001,”username”:”admin”,”password”:”123456”,”age”:23,”sex”:”男”} @RestController注解@RestController注解是springMVC提供的一个复合注解,标识在控制器的类上,就相当于为类添加了@Controller注解,并且为其中的每个方法添加了@ResponseBody注解 ResponseEntityResponseEntity用于控制器方法的返回值类型,该控制器方法的返回值就是响应到浏览器的响应报文 文件上传和下载文件下载使用ResponseEntity实现下载文件的功能 123456789101112131415161718192021222324@RequestMapping("/testDown")public ResponseEntity<byte[]> testResponseEntity(HttpSession session) throws IOException { //获取ServletContext对象 ServletContext servletContext = session.getServletContext(); //获取服务器中文件的真实路径 String realPath = servletContext.getRealPath("/static/img/1.jpg"); //创建输入流 InputStream is = new FileInputStream(realPath); //创建字节数组 byte[] bytes = new byte[is.available()]; //将流读到字节数组中 is.read(bytes); //创建HttpHeaders对象设置响应头信息 MultiValueMap<String, String> headers = new HttpHeaders(); //设置要下载方式以及下载文件的名字 headers.add("Content-Disposition", "attachment;filename=1.jpg"); //设置响应状态码 HttpStatus statusCode = HttpStatus.OK; //创建ResponseEntity对象 ResponseEntity<byte[]> responseEntity = new ResponseEntity<>(bytes, headers, statusCode); //关闭输入流 is.close(); return responseEntity;} 文件上传 文件上传要求form表单的请求方式必须为post,并且添加属性enctype=”multipart/form-data” SpringMVC中将上传的文件封装到MultipartFile对象中,通过此对象可以获取文件相关信息 a>添加依赖: 123456<!-- https://mvnrepository.com/artifact/commons-fileupload/commons-fileupload --><dependency> <groupId>commons-fileupload</groupId> <artifactId>commons-fileupload</artifactId> <version>1.3.3</version></dependency> b>在SpringMVC的配置文件中添加配置: 12<!--必须通过文件解析器的解析才能将文件转换为MultipartFile对象--><bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver"></bean> c>控制器方法: 12345678910111213141516171819@RequestMapping("/testUp")public String testUp(MultipartFile photo, HttpSession session) throws IOException { //获取上传的文件的文件名 String fileName = photo.getOriginalFilename(); //处理文件重名问题 String hzName = fileName.substring(fileName.lastIndexOf("."));//获取后缀 fileName = UUID.randomUUID().toString() + hzName; //获取服务器中photo目录的路径 ServletContext servletContext = session.getServletContext(); String photoPath = servletContext.getRealPath("photo"); File file = new File(photoPath); if(!file.exists()){ file.mkdir(); } String finalPath = photoPath + File.separator + fileName; //实现上传功能 photo.transferTo(new File(finalPath)); return "success";} 拦截器拦截器的配置 SpringMVC中的拦截器用于拦截控制器方法的执行 SpringMVC中的拦截器需要实现HandlerInterceptor SpringMVC的拦截器必须在SpringMVC的配置文件中进行配置: 1234567891011<bean class="com.atguigu.interceptor.FirstInterceptor"></bean><ref bean="firstInterceptor"></ref><!-- 以上两种配置方式都是对DispatcherServlet所处理的所有的请求进行拦截 --><mvc:interceptor> <mvc:mapping path="/**"/> <mvc:exclude-mapping path="/testRequestEntity"/> <ref bean="firstInterceptor"></ref></mvc:interceptor><!-- 以上配置方式可以通过ref或bean标签设置拦截器,通过mvc:mapping设置需要拦截的请求,通过mvc:exclude-mapping设置需要排除的请求,即不需要拦截的请求--> 拦截器的三个抽象方法SpringMVC中的拦截器有三个抽象方法: preHandle:控制器方法执行之前执行preHandle(),其boolean类型的返回值表示是否拦截或放行,返回true为放行,即调用控制器方法;返回false表示拦截,即不调用控制器方法 postHandle:控制器方法执行之后执行postHandle() afterComplation:处理完视图和模型数据,渲染视图完毕之后执行afterComplation() 多个拦截器的执行顺序 若每个拦截器的preHandle()都返回true。此时多个拦截器的执行顺序和拦截器在SpringMVC的配置文件的配置顺序有关: preHandle()会按照配置的顺序执行,而postHandle()和afterComplation()会按照配置的反序执行 若某个拦截器的preHandle()返回了false preHandle()返回false和它之前的拦截器的preHandle()都会执行,postHandle()都不执行,返回false的拦截器之前的拦截器的afterComplation()会执行 异常处理器基于配置的异常处理 SpringMVC提供了一个处理控制器方法执行过程中所出现的异常的接口:HandlerExceptionResolver HandlerExceptionResolver接口的实现类有:DefaultHandlerExceptionResolver和SimpleMappingExceptionResolver SpringMVC提供了自定义的异常处理器SimpleMappingExceptionResolver,使用方式: 123456789101112131415<bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver"> <property name="exceptionMappings"> <props> <!-- properties的键表示处理器方法执行过程中出现的异常 properties的值表示若出现指定异常时,设置一个新的视图名称,跳转到指定页面 --> <prop key="java.lang.ArithmeticException">error</prop> </props> </property> <!-- exceptionAttribute属性设置一个属性名,将出现的异常信息在请求域中进行共享 --> <property name="exceptionAttribute" value="ex"></property></bean> 基于注解的异常处理123456789101112//@ControllerAdvice将当前类标识为异常处理的组件@ControllerAdvicepublic class ExceptionController { //@ExceptionHandler用于设置所标识方法处理的异常 @ExceptionHandler(ArithmeticException.class) //ex表示当前请求处理中出现的异常对象 public String handleArithmeticException(Exception ex, Model model){ model.addAttribute("ex", ex); //直接跳转到返回值的界面 return "error"; }} 注解配置SpringMVC使用配置类和注解代替web.xml和SpringMVC配置文件的功能。 创建初始化类,代替web.xml在Servlet3.0环境中,容器会在类路径中查找实现javax.servlet.ServletContainerInitializer接口的类,如果找到的话就用它来配置Servlet容器。Spring提供了这个接口的实现,名为SpringServletContainerInitializer,这个类反过来又会查找实现WebApplicationInitializer的类并将配置的任务交给它们来完成。Spring3.2引入了一个便利的WebApplicationInitializer基础实现,名为AbstractAnnotationConfigDispatcherServletInitializer,当我们的类扩展了AbstractAnnotationConfigDispatcherServletInitializer并将其部署到Servlet3.0容器的时候,容器会自动发现它,并用它来配置Servlet上下文。 1234567891011121314151617181920212223242526272829303132333435363738public class WebInit extends AbstractAnnotationConfigDispatcherServletInitializer { /** * 指定spring的配置类 * @return */ @Override protected Class<?>[] getRootConfigClasses() { return new Class[]{SpringConfig.class}; } /** * 指定SpringMVC的配置类 * @return */ @Override protected Class<?>[] getServletConfigClasses() { return new Class[]{WebConfig.class}; } /** * 指定DispatcherServlet的映射规则,即url-pattern * @return */ @Override protected String[] getServletMappings() { return new String[]{"/"}; } /** * 添加过滤器 * @return */ @Override protected Filter[] getServletFilters() { CharacterEncodingFilter encodingFilter = new CharacterEncodingFilter(); encodingFilter.setEncoding("UTF-8"); encodingFilter.setForceRequestEncoding(true); HiddenHttpMethodFilter hiddenHttpMethodFilter = new HiddenHttpMethodFilter(); return new Filter[]{encodingFilter, hiddenHttpMethodFilter}; }} 创建SpringConfig配置类,代替spring的配置文件1234@Configurationpublic class SpringConfig { //ssm整合之后,spring的配置信息写在此类中} 创建WebConfig配置类,代替SpringMVC的配置文件1234567891011121314151617181920212223242526272829303132333435363738394041424344454647@Configuration//扫描组件@ComponentScan("com.atguigu.mvc.controller")//开启MVC注解驱动@EnableWebMvc//1.扫描组件 2.视图解析器 3.mvc注解驱动 4.default_servlet_handler 5.view_controller//6.文件上传解析器 7.拦截器 8.异常处理public class WebConfig implements WebMvcConfigurer { //使用默认的servlet处理静态资源 @Override public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) { configurer.enable(); } //配置文件上传解析器 @Bean public CommonsMultipartResolver multipartResolver(){ return new CommonsMultipartResolver(); } //配置拦截器 @Override public void addInterceptors(InterceptorRegistry registry) { FirstInterceptor firstInterceptor = new FirstInterceptor(); registry.addInterceptor(firstInterceptor).addPathPatterns("/**"); } //配置视图控制 @Override public void addViewControllers(ViewControllerRegistry registry) { registry.addViewController("/").setViewName("index"); } //配置异常映射 @Override public void configureHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) { SimpleMappingExceptionResolver exceptionResolver = new SimpleMappingExceptionResolver(); Properties prop = new Properties(); prop.setProperty("java.lang.ArithmeticException", "error"); //设置异常映射 exceptionResolver.setExceptionMappings(prop); //设置共享异常信息的键 exceptionResolver.setExceptionAttribute("ex"); resolvers.add(exceptionResolver); }} SpringMVC执行流程 用户向服务器发送请求,请求被SpringMVC 前端控制器 DispatcherServlet捕获。 DispatcherServlet对请求URL进行解析,得到请求资源标识符(URI),判断请求URI对应的映射: a) 不存在 i. 再判断是否配置了mvc:default-servlet-handler ii. 如果没配置,则控制台报映射查找不到,客户端展示404错误 iii. 如果有配置,则访问目标资源(一般为静态资源,如:JS,CSS,HTML),找不到客户端也会展示404错误 b) 存在则执行下面的流程 根据该URI,调用HandlerMapping获得该Handler配置的所有相关的对象(包括Handler对象以及Handler对象对应的拦截器),最后以HandlerExecutionChain执行链对象的形式返回。 DispatcherServlet 根据获得的Handler,选择一个合适的HandlerAdapter。 如果成功获得HandlerAdapter,此时将开始执行拦截器的preHandler(…)方法【正向】 提取Request中的模型数据,填充Handler入参,开始执行Handler(Controller)方法,处理请求。在填充Handler的入参过程中,根据你的配置,Spring将帮你做一些额外的工作: a) HttpMessageConveter: 将请求消息(如Json、xml等数据)转换成一个对象,将对象转换为指定的响应信息 b) 数据转换:对请求消息进行数据转换。如String转换成Integer、Double等 c) 数据格式化:对请求消息进行数据格式化。 如将字符串转换成格式化数字或格式化日期等 d) 数据验证: 验证数据的有效性(长度、格式等),验证结果存储到BindingResult或Error中 Handler执行完成后,向DispatcherServlet 返回一个ModelAndView对象。 此时将开始执行拦截器的postHandle(…)方法【逆向】。 根据返回的ModelAndView(此时会判断是否存在异常:如果存在异常,则执行HandlerExceptionResolver进行异常处理)选择一个适合的ViewResolver进行视图解析,根据Model和View,来渲染视图。 渲染视图完毕执行拦截器的afterCompletion(…)方法【逆向】。 将渲染结果返回给客户端。","link":"/2022/01/29/SpringMVC/"},{"title":"动态代理","text":"基于jdk的动态代理和基于cglib的动态代理实现方式。 基于jdk的动态代理(有接口)12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667//接口public interface User { void save();}//被代理类class UserImpl implements User{ public void save(){ System.out.println("save running......"); }}//增强方法class UserAdvice{ public void before(){ System.out.println("before save"); } public void after(){ System.out.println("after save"); }}//代理工厂类class ProxyFactory{ public static Object getProxy(){ MyInvocationHandler handler = new MyInvocationHandler(); //被代理对象 User user = new UserImpl(); //增强对象 UserAdvice userAdvice = new UserAdvice(); handler.setAdvice(userAdvice); handler.setObj(user); //返回代理对象 return Proxy.newProxyInstance(user.getClass().getClassLoader(), user.getClass().getInterfaces(),handler); }}class MyInvocationHandler implements InvocationHandler{ //被代理对象 private Object obj; public void setObj(Object obj){ this.obj = obj; } //增强对象 private UserAdvice advice; public void setAdvice(UserAdvice advice){ this.advice = advice; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { advice.before(); Object invoke = method.invoke(obj, args); advice.after(); return invoke; }}//测试class Test{ public static void main(String[] args) { User proxy = (User) ProxyFactory.getProxy(); proxy.save(); }} 基于cglib的动态代理(无接口)123456789101112131415161718192021222324252627282930313233343536373839404142434445//被代理对象public class Target { public void save(){ System.out.println("save running......"); }}//增强class Advice{ public void before(){ System.out.println("save before"); } public void after(){ System.out.println("save after"); }}class Test{ public static void main(String[] args) { Target target = new Target(); Advice advice = new Advice(); //创建增强器 Enhancer enhancer = new Enhancer(); //设置父类(被代理类) enhancer.setSuperclass(Target.class); //设置回调 enhancer.setCallback(new MethodInterceptor() { @Override public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable { //执行前置 advice.before(); //执行目标 Object invoke = method.invoke(target, args); //执行后置 advice.after(); return invoke; } }); //创建代理对象 Target proxy = (Target) enhancer.create(); proxy.save(); }}","link":"/2022/02/08/%E5%8A%A8%E6%80%81%E4%BB%A3%E7%90%86/"},{"title":"MySQL逻辑架构与存储引擎","text":"SQL语句的执行流程。 MySQL逻辑架构 客户端和服务器端建立连接,客户端发送 SQL 至服务器端; 对 SQL 语句进行查询处理;与数据库文件的存储方式无关; 与数据库文件打交道,负责数据的存储和读取。 SQL语句执行流程 查询缓存:Server 如果在查询缓存中发现了这条 SQL 语句,就会直接将结果返回给客户端;如果没 有,就进入到解析器阶段。因为查询缓存往往效率不高,所以在 MySQL8.0 之后就抛弃了这个功能。在 MySQL 中的查询缓存,不是缓存查询计划,而是查询对应的结果。两个查询请求在任何字符上的不同(如空格、注释、大小写)都会导致缓存不命中。 解析器:在解析器中对 SQL 语句进行语法分析、语义分析,生成语法树。 优化器:在优化器中会确定 SQL 语句的执行路径,比如是根据全表检索 ,还是根据 索引检索等,生成一个执行计划。 执行器:执行查询。 SQL 语句在 MySQL 中的流程是: SQL语句→查询缓存(MySQL8.0之前)→解析器→优化器→执行器 。 MySQL8查看SQL执行 确认profiling是否开启 12select @@profiling;set profiling=1; 多次执行一个SQL查询 查看profiles 1show profiles; 查看某一条具体的执行步骤: 1show profile for query 5; 可以看到没有查询缓存。 存储引擎InnoDB引擎:具有外键支持功能的事务存储引擎 InnoDB是MySQL的 默认事务型引擎 ,它被设计用来处理大量的短期(short-lived)事务。可以确保事务的完整提交(Commit)和回滚(Rollback)。 除了增加和查询外,还需要更新、删除操作,那么,应优先选择InnoDB存储引擎。 对比MyISAM的存储引擎, InnoDB写的处理效率差一些 ,并且会占用更多的磁盘空间以保存数据和索引。 MyISAM只缓存索引,不缓存真实数据;InnoDB不仅缓存索引还要缓存真实数据, 对内存要求较 高 ,而且内存大小对性能有决定性的影响。 MyISAM引擎:主要的非事务处理存储引擎 MyISAM 不支持事务、行级 锁、外键 ,崩溃后会无法恢复。 MySQL5.5之前默认的存储引擎。 优势是访问的速度快 ,对事务完整性没有要求或者以SELECT、INSERT为主的应用。","link":"/2022/02/17/MySQL%E9%80%BB%E8%BE%91%E6%9E%B6%E6%9E%84/"},{"title":"MySQL索引结构","text":"索引(Index)是帮助MySQL高效获取数据的数据结构,在InnoDB存储引擎中,默认的索引是B+tree形式的。 InnoDB中的B+Tree索引索引结构 存放数据的数据页都放在最下面一层,其他都为目录页。 一般用到的B+树不会超过4层,那我们通过主键值去查找某条记录最多只需要做4个页面内的查找(查找3个目录项页和一个用户记录页)。每个页面内可以通过二分法快速定位记录。假设所有存放用户记录 的叶子节点代表的数据页可以存放 100条用户记录 ,所有存放目录项记录的内节点代表的数据页可以存放1000条目录项记录 ,那么: 如果B+树只有1层,也就是只有1个用于存放用户记录的节点,最多能存放 100 条记录。 如果B+树有2层,最多能存放 1000×100=10,0000 条记录。 如果B+树有3层,最多能存放 1000×1000×100=1,0000,0000 条记录。 如果B+树有4层,最多能存放 1000×1000×1000×100=1000,0000,0000 条记录。 索引类型聚簇索引 使用记录主键值的大小进行记录和页的排序 页内的记录是按照主键的大小顺序排成一个单向链表 。 各个存放数据记录的页是根据页中用户记录的主键大小顺序排成一个双向链表 。 存放目录项记录的页分为不同的层次,在同一层次中的页也是根据页中目录项记录的主键大小顺序排成一个双向链表 。 B+树的叶子节点存储的是完整的数据记录。 聚簇索引将索引和数据保存在同一个B+树中,因此从聚簇索引中获取数据比非 聚簇索引更快。 聚簇索引对于主键的排序查找和范围查找速度非常快。 插入速度严重依赖于插入顺序。按照主键的顺序插入是最快的方式,否则将会出现页分裂,严重影响性能。因此,对于InnoDB表,一般都会定义一个自增的ID列为主键。 更新主键的代价很高。因为将会导致被更新的行移动。因此,对于InnoDB表,一般定义主键为不可更新。 二级索引访问需要两次索引查找。 二级索引(辅助索引、非聚簇索引) 对表中非主键字段建立的索引。 根据二级索引查找字段时,如果没有覆盖,会根据主键值到聚簇索引中再查找一遍,称为回表。 二级索引中,叶子节点存放的是该字段和其对应记录的主键值。存放的不是该记录的地址值,因为这样减少了出现行移动或者数据页分裂时二级索引的维护工作(当数据需要更新的时候,二级索引不需要修改,只需要修改聚簇索引,一个表只能有一个聚簇索引,其他的都是二级索引,这样只需要修改聚簇索引就可以了,不需要重新构建二级索引. 联合索引(属于二级索引) 为多个列的组合建立索引 InnoDB的B+树注意事项根页面位置不会发生改变当为一个表创建一个B+树索引时,都会为这个索引创建一个根节点页面。然后向表中插入数据时,先把数据记录存储到这个根节点中。当根节点可用空间用完再继续插入时,会将根节点所有记录分配到一个新的页,再对这个新页进行页分裂操作,得到另一个新页。新插入的记录根据键值大小分配到某个页中,根节点就升级为目录项存储的页。根节点常驻内存中。 内节点目录项记录唯一在二级联合索引中,非叶子节点存放的目录项中,如果字段值相同则无法区分进入到哪个页中。所以二级索引的内节点目录项记录内容需要加上主键,即三个部分:索引列的值、主键值、页号。 一个页面至少存储两条记录MyISAM中的索引 MyISAM引擎使用 B+Tree 作为索引结构,叶子节点的data域存放的是数据记录的地址。 MyISAM的索引方式都是“非聚簇”的。 InnoDB的数据文件本身就是索引文件,而MyISAM索引文件和数据文件是分离的 ,索引文件仅保存数据记录的地址。 InnoDB要求表必须有主键( MyISAM可以没有 )。如果没有显式指定,则MySQL系统会自动选择一个 可以非空且唯一标识数据记录的列作为主键。如果不存在这种列,则MySQL自动为InnoDB表生成一个隐 含字段作为主键,这个字段长度为6个字节,类型为长整型。 其它索引结构Hash结构 能在O(1)时间查找,但是失去了有序行,对于order by需要排序,B+树本身查找就有序。 只支持精确查找,范围查找效率低。 InnoDB 存储引擎有一个特殊的功能叫“自适应哈希索引”,当某个索引值被使用的非常频繁时,会在 B+Tree 索引之上再创建一个哈希索引,这样就让 B+Tree 索引具有哈希索引的一些优点,比如快速的哈希查找。 B树 B树在插入和删除节点时若导致树不平衡,会自动调整保持平衡。 叶子节点和非叶子节点都存放数据。 搜索性能等价于在关键字全集做一次二分查找。 最下面一层并没有通过链表连接。 B+树结构较B树的优点 B+树查询效率更稳定,因为B+树每次访问到叶子节点才会找到数据,而B树可能在非叶子节点查询到。 B+树查询效率更高,由于B+树非叶子节点存放目录而不是数据,所以B+树更矮,查询时磁盘IO更少,同样磁盘页大小,B+树可存储更多节点关键字。 范围查询上,效率比B树高,因为所以关键字都在叶子节点上,叶子节点形成链表,又是非递减的。而在B树中需要通过中序遍历才能完成范围查询。","link":"/2022/02/17/MySQL%E7%B4%A2%E5%BC%95/"},{"title":"MySQL索引优化、失效场景与事务","text":"MySQL的索引设计,索引失效场景,事务等,记录一些学习资料。 本文章只用于个人学习观看,不会传播盈利! MySQL事务、隔离级别与MVCC 索引失效场景 MySQL explain命令 MySQL全局锁、表级锁、行级锁","link":"/2022/02/18/MySQL%E7%B4%A2%E5%BC%95%E4%BC%98%E5%8C%96%E3%80%81%E5%A4%B1%E6%95%88%E5%9C%BA%E6%99%AF%E4%B8%8E%E4%BA%8B%E5%8A%A1/"},{"title":"Linux常用命令","text":"本篇文章记录常见的Linux命令,以CentOS7.6演示。 目录结构 Vim编辑器快捷键 正常模式下,yy:拷贝当前行,5yy:拷贝当前行向下的5行,并粘贴(p) 正常模式下,dd:删除当前行,5dd:删除当前行向下的5行 查找某个单词,命令行模式下,/关键字,回车查找,输入n是查找下一个 设置行号,取消行号。命令行模式下:set nu和set nonu 到文件最后一行:G,到文件首行:gg 撤销动作:正常模式下输入u 将光标移到20行: 显示行号:set nu 正常模式下输入20 shift+g Linux开关机/用户登录注销 sync:把内存数据同步到磁盘,在关机或者重启时,都应该先执行sync命令,防止数据丢失。 shutdown -h now:立即关机 shutdown -h 1:一分钟后关机 init 0或halt:立即关机 shutdown -r now:立即重启 init 6或reboot:立即重启 用户管理基本介绍 Linux系统是一个多用户多任务的操作系统,任何一个要使用系统资源的用户,都必须先向系统管理员申请一个账号,然后以这个账号的身份进入系统。 Linux的用户需要至少属于一组。 常用命令 添加用户:useradd [选项] 用户名,useradd -d 指定目录 用户名,如useradd -d /home/twilight twilight 指定/修改密码:passwd 用户名 删除用户:userdel 用户名(删除用户,但保留家目录),userdel -r 用户名(删除用户以及用户家目录) 查询用户信息:id 用户名 切换用户:su - 用户名,exit退出 常看当前用户:whoami/who an i 增加组:groupadd 组名 增加用户时直接加上组:useradd 用户名 -g 组名 删除组:groupdel 组名 修改用户所在组:usermod 用户名 -g 组名,如usermod xh -g police,把xh改到police组下 用户和组相关文件 /etc/passwd:用户的配置文件,记录用户各种信息,每行含义:用户名:口令:用户标识号:组标识号:注释行描述:组目录:登陆shell /etc/shadow:口令配置文件 /etc/group:组的配置文件,记录Linux包含组的信息。每行含义:组名:口令:组标识号:组内用户列表 实用命令运行级别 常用运行级别是3和5,修改默认运行级别可改文件/etc/inittab中id:3:initdefault:中的数字 切换到指定运行级别:init [0123456] 帮助指令 man [命令或配置文件]:获取该命令或者配置文件的帮助信息 help 命令:获取shell内置命令的帮助信息 文件目录类 pwd:显示正在工作的目录绝对路径 ls [选项] [目录或文件]:显示目录或文件 -a:显示当前目录所有文件和目录,包括隐藏的 -l:以列表方式显示信息 cd 目录:切换到指定目录,cd ~或者cd:回到自己的家目录,cd ..回到上一级目录 mkdir [选项] 要创建的目录名:创建目录 -p:创建多级目录,如mkdir -p /home/animal/tiger rmdir [选项] 要删除的空目录,如果目录下有内容,则无法删除,删除非空目录:rm -rf 删除的目录名 touch 文件名:创建新文件 cp [选项] 要复制的文件 指定目录名:把文件拷贝到指定目录下 -r:递归复制整个文件夹,如cp -r xh/ twilight/ rm [选项] 要删除的文件或目录 -r:递归删除整个文件夹 -f:强制删除不提示 mv 老名字 新名字:修改文件名,mv 文件名 指定目录:把文件移动到指定目录下 cat [选项] 查看的文件名,一般都会带上管道命令:|more(分页浏览) -n:显示行号 more 要查看的文件名 space:向下翻页 Enter:向下翻一行 q:离开 less 查看的文件名,对显示大型文件效率较高 内容 > 文件:把内容写到文件中(覆盖),如ls -l > a.txt ls -al >> 文件:把列表内容追加到文件末尾 cat 文件1 > 文件2:将文件1覆盖到文件2 echo 内容 >> 文件:输入内容到文件尾 echo 输出内容:输出内容到控制台,如echo $PATH head 文件名:查看文件头10行内容,head -n 5:查看文件头5行内容 tail 文件名:查看文件后10行,tail -n 5:查看文件后5行,tail -f 文件名:追踪该文件所有更新 ln -s 源文件或目录 软链接名:给源文件创建一个软链接 history n:查看最近使用过的n个命令 时间日期类 date:显示当前时间 date -s 字符串时间:设置当前时间 cal [年]:查看日历 搜索查找类 find [搜索范围] [选项]:从指定目录向下递归各个子目录 -name 文件名:按照指定文件名查找,如find /home -name hello.txt(*.txt) -user 用户名:按照属于用户查找,如find /home -user twilight -size 文件大小:按照文件大小查找,如find /home -size -20(+20)k,查找小于(大于)20k的文件 locate 文件名:快速定位文件位置。 locate指令基于数据库进行查询,第一次运行前,需要先updatedb创建locate数据库 grep:过滤查找,|:管道符,表示将前一个命令的处理结果传递给后面的命令处理 grep [选项] 查找内容 源文件 ,如cat hello.txt | grep -n yes -n:显示匹配行及行号 -i:忽略字母大小写 压缩和解压类 gzip 文件名:压缩文件(只能压缩为*.gz文件),使用这个命令压缩,不会保留原来的文件 gunzip 文件.gz:解压缩 zip [选项] xxx.zip 要压缩的内容:压缩文件 -r:递归压缩整个目录 unzip [选项] xxx.zip:解压缩 -d 目录:指定解压缩后文件的存放目录,如unzip -d /home/test/ mypackage.zip tar:打包命令,打包文件是.tar.gz文件,tar [选项] xxx.tar.gz 打包的内容 -c:产生.tar打包文件夹 -v:显示详细信息 -f:指定压缩后的文件名 -z:打包同时压缩 -x:解包.tar文件 打包:-zcvf,如tar -zcvf mypackage.tar.gz a.txt b.txt,把a.txt和b.txt打包 解压:-zxvf,如tar -zxvf my.tar.gz -C /home/,把压缩包解压到/home/下 组管理与权限管理组的管理 Linux中的每个用户必须属于一个组,不能独立于组外。在linux中每个文件有所有者、所在组、其它组的概念。 ls -ahl:查看文件所有者 chown 用户名 文件名:修改文件所有者为该用户 charp 组名 文件名:修改文件所在的组为该组 除文件的所有者和所在组的用户外,系统的其它用户都是文件的其他组 usermod -g 组名 用户名:改变用户所在组 usermod -d 目录名 用户名:改变用户登陆的初始目录 权限管理 ll查看内容如下 0~9位说明: 第0位确定文件类型:-(文件),d(目录),l(链接) 第1-3位确定该文件所有者拥有该文件的权限。 第4-6位确定同用户组其他用户拥有该文件的权限。 第7-9位确定其它用户拥有该文件的权限。 rwx作用到文件 【r】代表可读:可以读取、查看 【w】代表可写:可以修改,但是不代表可以删除该文件,删除文件的前提是对该文件所在目录有写权限 【x】代表可执行:可以被执行,./文件名执行 rwx作用到目录 【r】代表可读:可以读取,ls查看目录 【w】代表可写:可以修改目录内创建、删除、重命名目录 【x】代表可执行:可以进入该目录 修改权限-chmod u:所有者,g:同组其他用户,o:其它用户,a:所有人(u、g、o的总和) chmod u=rwx,g=rx,o=x 文件目录名,给三种用户权限 chmod o+w 文件目录名:给其他用户该文件的写权限 chmod a-x 文件目录名:移除所有用户对该文件或目录的执行权限 通过数字变更权限:r=4,w=2,x=1,rwx=4+2+1=7 chmod u=rwx,g=rx,o=x 文件目录名相当于chmod 751 文件目录名 chown newowner file:改变文件所有者 chown [-R] newowner:newgroup file改变文件的所有者和所有组,-R 如果是目录,其下所有子文件递归权限管理 定时任务调度(crontab) 任务调度就是指系统在某个时间执行的特点的命令或程序 系统工作:有些重要的工作必须周而复始地执行。如病毒扫描等 个别用户工作:个别用户可能希望执行某些程序,比如对mysql数据库的备份 crontab [选项] -e 编辑crontab定时任务 -l 查询crontab任务 -r 删除当前用户所有的crontab任务 service crond restart:重启任务调度 特殊符号: 执行案例: 举例:*/1 * * * * ls -l /etc/>/tmp/to.txt,意思是:每小时的每分钟执行ls -l /etc/ > tmp/to.txt命令 crontab -e进行编辑任务,保存后即生效 应用举例每隔一分钟,将当前的日期信息追加到/tmp/mydate文件中 先编写一个文件 /home/mytask.sh,内容为date >> /tmp/mydate. 给mytask.sh一个可执行权限 crontab -e编辑定时任务 添加*/1 * * * * /home/mytask.sh,保存退出 查看/tmp/mydate即可发现日期 进程管理查看进程 ps命令是用来查看目前系统中,有哪些正在进行的进程,以及他们的执行情况,可以不加任何参数,一般使用参数 -aux a:显示当前终端的所有进程信息 u:以用户的格式显示进程的信息 x:显示后台进程运行的参数 ps -ef | more:可查看到父进程 ps -aux | grep 进程名 或者 ps -ef | grep 进程名 终止进程kill和killall kill [选项] 进程号:通过进程号杀死进程,-9:强制杀死 killall 进程名称:通过进程名杀死进程,也支持通配符,这在系统因负载过大而变得很慢时很有用 踢掉非法登陆用户:ps -aux | grep sshd查看进程,kill 进程号 查看进程树pstree pstree [选项]:以树状形式查看进程信息 -p:显示进程PID -u:显示进程所属用户 服务管理 systemctl [start|stop|restart|reload|status] 服务名 查看防火墙状态:systemctl status firewalld.service 关闭防火墙:systemctl stop firewalld.service 开启防火墙:systemctl start firewalld.service 监控 top与ps命令很相似,他们都是用来显示正在执行的进程,top与ps命令最大的不同之处,在于top在执行一段时间可以更新正在运行的进程 top -d 5:每隔5秒自动更新状态 输入u,再输入用户名,查看某个用户的进程 输入k,再输入进程号,杀死某个进程 netstat -anp:查看所有网络服务,|grep 服务名,监听某个服务 包管理器yum包管理器 基于RPM包管理器,能够从指定的服务器自动下载RPM包并且安装,可以自动处理依赖关系,并且一次性安装所有依赖的软件包。前提是要能联网。 yum list | grep xxx:查询yum服务器是否有需要安装的这个软件 yum install xxx:安装指定的yum包 yum [options] [command] [package] options:可选:-y(安装过程中提示选择全部为yes),-q(不显示安装过程)等 command:要进行的操作 package:安装的包名 yum remove package_name:删除包 配置JDK环境 上传jdk到/opt目录下,解压 添加环境变量 vim /etc/profile 在末尾添加: 重启 编写java程序运行:","link":"/2022/02/21/Linux%E5%B8%B8%E7%94%A8%E5%91%BD%E4%BB%A4/"},{"title":"Redis常用数据类型","text":"Redis是NoSQL型数据库,数据都在内存中,支持持久化,支持多种数据结构的存储,如String、list、set、hash、zset。一般作为缓存数据库辅助持久化的数据库。本篇记录基本概念和操作,关于持久化、主从复制、事务、集群等内容的学习查看官方文档。 Redis中文文档 Redis安装配置Redis安装 下载官网最新redis压缩包,上传到虚拟机中并解压 安装c语言编译环境:yum install gcc 进入redis解压后目录,make进行编译 make install 安装目录在/usr/local/bin,包括 redis-benchmark:性能测试工具 redis-check-aof:修复有问题的AOF文件 redis-check-dump:修复有问题的dump.rdb文件 redis-sentinel:Redis集群使用 redis-server:Redis服务器启动命令 redis-cli:客户端操作入口 Redis启动 备份redis.conf到其它目录下,如cp redis.conf /etc/ 修改备份文件中daemonize no改为yes,可以让服务在后台启动 启动:redis-server /etc/redis.conf 连接:redis-cli 五大数据类型 Redis默认16个数据库,从0到15,select dbid 可以切换数据库,所有库统一密码。 dbsize:查看当前库的key数量 flushdb:清空当前库 flushall:清空所有库(慎用) Redis是单线程+多路IO复用技术 String keys *:查看当前库所有key exists key:判断某个key是否存在 type key:查看key是什么类型 del key:删除指定key的数据 unlink key:根据value选择非阻塞删除 expire key n:为key设置过期时间,ttl key可以查看过期时间,-1表示永不过期,-2表示已过期 set key value:添加键值对 get key:查询对应键值 append key value:将给定的value追加到原值末尾 strlen key:获取值的长度 setnx key value:只有key不存在时,设置key值成功 incr/decr key:将 key中储存的数字值增1/减1 该操作是原子操作,不会被线程调度机制打断,一旦开始,就一直运行到结束,中间不会切换到另一个线程,Redis单命令的原子性得益于Redis的单线程。 incrby/decrby key <步长>:将key中存储数值增减步长 mset key1 value1 key2 value2 …:同时设置多个key-value mget key1 key2…:同时获取多个value msetnx key1 value1 key2 value2:同时设置多个key-value,当且仅当所有key不存在成功。 原子性:有一个key存在则都设置失败 getrange key start end :获取值的范围,两端都闭 setrange key offset value:从offset开始,用value覆盖原字符串 setex key 过期时间 value:设置键值同时设置过期时间,单位秒 getset key value:获取key对应旧值,同时设置新值为value 数据结构:简单动态字符串(Simple Dynamic String,缩写SDS),是可以修改的字符串,内部结构实现上类似于Java的ArrayList,采用预分配冗余空间的方式来减少内存的频繁分配。 如图中所示,内部为当前字符串实际分配的空间capacity一般要高于实际字符串长度len。当字符串长度小于1M时,扩容都是加倍现有的空间,如果超过1M,扩容时一次只会多扩1M的空间。需要注意的是字符串最大长度为512M。 List lpush/rpush key value1 value2…:从左边/右边插入一个/多个值 lpop/rpop key:从左边/右边取出一个值。值取完,键也不存在了 rpoplpush key1 key2:从key1列表右侧取出一个值,查到key2的左侧 lrange key start stop:按照索引下标获得元素(从左到右),0 -1表示所有 lindex key index:按照索引下标获取元素 llen key:获得列表长度 linsert key before/after value newvalue:在value前/后插入newvalue lset key index value:将列表key 下标为index的值替换成value 数据结构:快速链表quickList。在列表元素较少的情况下会使用一块连续的内存存储,这个结构是ziplist,也即是压缩列表。它将所有的元素紧挨着一起存储,分配的是一块连续的内存。当数据量比较多的时候才会改成quicklist。 Redis将链表和ziplist结合起来组成了quicklist。也就是将多个ziplist使用双向指针串起来使用。这样既满足了快速的插入删除性能,又不会出现太大的空间冗余。 Set sadd key value1 value2…:将元素加入集合key中,已存在的元素被忽略 smembers key:查询该集合所有值 sismember key value:判断集合key是否含有该value值 scard key:返回集合元素个数 srem key value1 value2…:删除集合中的元素 spop key:从集合中随机去除一个值 srandmember key n:从集合中随机取出n个值。不会删除 smove key1 key2 value:把key1集合的value移到key2中去 sinter k1 k2:返回两集合交集 sunion k1 k2:返回两集合并集 sdiff k1 k2:返回两集合差集,在k1中不在k2中的 数据结构:Set数据结构是dict字典,字典是用哈希表实现的。Java中HashSet的内部实现使用的是HashMap,只不过所有的value都指向同一个对象。Redis的set结构也是一样,它的内部也使用hash结构,所有的value都指向同一个内部值。 Hash Redis hash是一个String类型的field和value的映射表,适合用于存储对象 hset key field value field value…:设置key中的field键为value 如hset user id 1 name jack age 20 hget key field:从key中取出键为field的值 hdel key field1 field2…:删除key中的field域 hexists key field:判断key中是否存在域field hkeys key:列出该key的所有field hvals key:列出该key的所有value hincrby key field increment:为key中field域加上increment hsetnx key field value:将key中域field值设置为value,仅当field域不存在时成功 数据结构:Hash类型对应的数据结构是两种:ziplist(压缩列表),hashtable(哈希表)。当field-value长度较短且个数较少时,使用ziplist,否则使用hashtable。 Zset(有序集合) 与普通集合set非常相似,是一个没有重复元素的字符串集合。不同之处是有序集合的每个成员都关联了一个评分(score),这个评分(score)被用来按照从最低分到最高分的方式排序集合中的成员 zadd key score1 value1 score2 value2…:将value值加入有序集合key中,并指定score值 zrange key start stop [withscores]:返回有序集合中,下标在[start,stop]之间的元素 zrangebyscore key min max:返回有序集 key 中,所有 score 值介于 min 和 max 之间的成员 zincrby:为元素加上增量 zrem key value:删除该集合指定元素 zcount key min max:返回该集合分数区间内元素个数 zrank key value:返回该值排名,从0开始 zset结构同时包含一个字典和一个跳跃表,跳跃表按score从小到大保存所有集合元素。字典保存着从member到score的映射。这两种结构通过指针共享相同元素的member和score,不会浪费额外内存。 跳跃表的结构:跳跃表 Jedis操作Redis依赖12345<dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>3.3.0</version></dependency> 连接Redis12345678public class JedisDemo { public static void main(String[] args) { Jedis jedis = new Jedis("192.168.57.101", 6379); String pong = jedis.ping(); System.out.println("连接成功:" + pong); jedis.close(); }} Key12345678910jedis.set("k1", "v1");jedis.set("k2", "v2");Set<String> keys = jedis.keys("*");System.out.println(keys.size());for (String key : keys) { System.out.println(key);}System.out.println(jedis.exists("k1"));System.out.println(jedis.ttl("k1")); System.out.println(jedis.get("k1")); String12jedis.mset("str1","v1","str2","v2","str3","v3");System.out.println(jedis.mget("str1","str2","str3")); List12345jedis.lpush("k1","v1","v2");List<String> list = jedis.lrange("mylist",0,-1);for (String element : list) { System.out.println(element);} Set1234567jedis.sadd("orders", "order01");jedis.sadd("orders", "order02");Set<String> smembers = jedis.smembers("orders");for (String order : smembers) { System.out.println(order);}jedis.srem("orders", "order02"); Hash1234567891011jedis.hset("hash1","userName","lisi");System.out.println(jedis.hget("hash1","userName"));Map<String,String> map = new HashMap<String,String>();map.put("telphone","13810169999");map.put("address","atguigu");map.put("email","[email protected]");jedis.hmset("hash2",map);List<String> result = jedis.hmget("hash2", "telphone","email");for (String element : result) { System.out.println(element);} Zset123456jedis.zadd("zset01", 100d, "z3");jedis.zadd("zset01", 90d, "l4");Set<String> zrange = jedis.zrange("zset01", 0, -1);for (String e : zrange) { System.out.println(e);}","link":"/2022/02/23/Redis%E5%B8%B8%E7%94%A8%E6%95%B0%E6%8D%AE%E7%B1%BB%E5%9E%8B/"}],"tags":[{"name":"java基础","slug":"java基础","link":"/tags/java%E5%9F%BA%E7%A1%80/"},{"name":"java集合","slug":"java集合","link":"/tags/java%E9%9B%86%E5%90%88/"},{"name":"experience","slug":"experience","link":"/tags/experience/"},{"name":"位运算","slug":"位运算","link":"/tags/%E4%BD%8D%E8%BF%90%E7%AE%97/"},{"name":"acm","slug":"acm","link":"/tags/acm/"},{"name":"图论","slug":"图论","link":"/tags/%E5%9B%BE%E8%AE%BA/"},{"name":"搜索","slug":"搜索","link":"/tags/%E6%90%9C%E7%B4%A2/"},{"name":"题解","slug":"题解","link":"/tags/%E9%A2%98%E8%A7%A3/"},{"name":"线段树","slug":"线段树","link":"/tags/%E7%BA%BF%E6%AE%B5%E6%A0%91/"},{"name":"树状数组","slug":"树状数组","link":"/tags/%E6%A0%91%E7%8A%B6%E6%95%B0%E7%BB%84/"},{"name":"JDBC","slug":"JDBC","link":"/tags/JDBC/"},{"name":"JavaWeb","slug":"JavaWeb","link":"/tags/JavaWeb/"},{"name":"Git","slug":"Git","link":"/tags/Git/"},{"name":"计算机网络","slug":"计算机网络","link":"/tags/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/"},{"name":"MySQL","slug":"MySQL","link":"/tags/MySQL/"},{"name":"Spring","slug":"Spring","link":"/tags/Spring/"},{"name":"设计模式","slug":"设计模式","link":"/tags/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/"},{"name":"Linux","slug":"Linux","link":"/tags/Linux/"},{"name":"Redis","slug":"Redis","link":"/tags/Redis/"}],"categories":[{"name":"science","slug":"science","link":"/categories/science/"},{"name":"life","slug":"life","link":"/categories/life/"}]}