按值调用(call by value)表示方法接收的是调用者提供的值,而按引用调用(call by reference)表示方法接收的是调用者提供的变量地址。一个方法可以修改传递引用所对应的变量值,而不能修改传递值调用所对应的变量值。
基础类型传递:
一个方法不能修改一个基本数据类型的参数(即数值型或布尔型)。
对象传递:
实现一个改变对象参数状态的方法并不是一件难事。理由很简单,方法得到的是对象引用的拷贝,对象引用及其他的拷贝同时引用同一个对象。
引用传递:
一个方法不能让对象参数引用一个新的对象。
方法并没有改变存储在变量 s1 和 s2 中的对象引用。swap 方法的参数 x 和 y 被初始化为两个对象引用的拷贝,这个方法交换的是这两个拷贝
hashCode() 的作用是获取哈希码,也称为散列码;它实际上是返回一个 int 整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。
例如在HashMap中,因为我们如果在设计两个对象相等的逻辑时,如果不重写Equals方法,那么一个类有两个对象A1, A2,他们的A1.equals(A2)为true,A1.hashcode和A2.hashcode不一样,当将A1和A2都作为HashMap的key时, HashMap会认为它两不相等,因为HashMap在判断key值相不相等时会判断key的hashcode是不是一样, hashcode一样相等,所以在这种场景下会出现我们认为这两个对象相等,但是hashmap不这么认为,所以会有问题。
hashCode()与 equals()的相关规定:
来源:https://www.zhihu.com/question/21162041/answer/1268903417
在Java中,方法调用有两类,动态方法调用与静态方法调用。
多态就是指程序中定义的引⽤变量所指向的具体类型和通过该引⽤变量发出的⽅法调⽤在编译时并不确定,⽽是在程序运⾏期间才确定,即⼀个引⽤变量到底会指向哪个类的实例对象,该引⽤变量发出的⽅法调⽤到底是哪个类中实现的⽅法,必须在由程序运⾏期间才能决定。
多态的具体实现:
(1) 方法重载(类内部之间的多态):就是在类中可以创建多个方法,它们具有相同的名字,但可具有不同的参数列表、返回值类型。
(2)方法重写(父类与子类之间的多态):子类可继承父类中的方法,但有时子类并不想原封不动地继承父类的方法,而是想作一定的修改,这就需要采用方法的重写。重写的参数列表和返回类型均不可修改。
多态分析:
从JVM的角度看:多态方法存在于方法区。java堆存的是就是我们建立的一个个实例对象,而方法区存的就是类的类型信息。而且这个方法区中的类型信息跟在堆中存放的class对象是不同的。在方法区中,这个class的类型信息只有唯一的实例(所以方法区是各个线程共享的内存区域),而在堆中可以有多个该class对象。也就是说方法区的类型信息就是像一个模板,那些class对象就好比通过这些模板创建的一个个实例。
整体流程:
第一步:虚拟机通过reference查询java栈中的本地变量表,得到堆中的对象类型数据的指针,
第二步:通过到对象的指针找到方法区中的对象类型数据
第三步:查询方法表定位到实际类的方法运行。
方法表存在于方法区中的,它是实现多态的关键所在,这里面保存的就是实例方法的引用,而且是直接引用。java虚拟机在执行程序的时候就是通过这个方法表来确定运行哪一个多态方法的。
当子类方法重写父类方法时,新的数据会覆盖原有的数据,也就是说原来指向父类的那个引用会被替换成指向子类的引用(占据原来表中的位置)
符号引用和直接引用的区别:
符号引用就是字符串,这个字符串包含足够的信息,以供实际使用时可以找到相应的位置。你比如说某个方法的符号引用,如:“java/io/PrintStream.println:(Ljava/lang/String;)V”。里面有类的信息,方法名,方法参数等信息。当第一次运行时,要根据字符串的内容,到该类的方法表中搜索这个方法。运行一次之后,符号引用会被替换为直接引用,下次就不用搜索了。直接引用就是偏移量,通过偏移量虚拟机可以直接在该类的内存区域中找到方法字节码的起始位置。
封装:
封装把一个对象的属性私有化,同时提供一些可以被外界访问的属性的方法,如果属性不想被外界访问,我们大可不必提供方法给外界访问。但是如果一个类没有提供给外界访问的方法,那么这个类也没有什么意义了。
为什么需要封装?
封装将不需要对外提供的内容都隐藏起来**,把属性隐藏,提供公共方法对其访问**。好处就是:隐藏实现细节,提供公共的访问方式;提高了代码的复用性;提高了安全性。
继承
继承是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,**但不能选择性地继承父类。**通过使用继承我们能够非常方便地复用以前的代码。
关于继承如下 3 点请记住:
为什么String要设计为不可变的?
便于实现字符串池(String pool)
在Java中,由于会大量的使用String常量,如果每一次声明一个String都创建一个String对象,那将会造成极大的空间资源的浪费。Java提出了String pool的概念,在堆中开辟一块存储空间String pool,当初始化一个String变量时,如果该字符串已经存在了,就不会去创建一个新的字符串变量,而是会返回已经存在了的字符串的引用。
使多线程安全
在并发场景下,多个线程同时读一个资源,是安全的,不会引发竞争,但对资源进行写操作时是不安全的,不可变对象不能被写,所以保证了多线程的安全。
避免安全问题
在网络连接和数据库连接中字符串常常作为参数,例如,网络连接地址URL,文件路径path,反射机制所需要的String参数。其不可变性可以保证连接的安全性。如果字符串是可变的,黑客就有可能改变字符串指向对象的值,那么会引起很严重的安全问题。
加快字符串处理速度
由于String是不可变的,保证了hashcode的唯一性,于是在创建对象时其hashcode就可以放心的缓存了,不需要重新计算。这也就是Map喜欢将String作为Key的原因,处理速度要快过其它的键对象。所以HashMap中的键往往都使用String。
JAVA 反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为 java 语言的反射机制。
反射应用场景:
① 我们在使用 JDBC 连接数据库时使用 Class.forName()通过反射加载数据库的驱动程序;②Spring 框架也用到很多反射机制,最经典的就是 xml 的配置模式。Spring 通过 XML 配置模式装载 Bean 的过程:1) 将程序内所有 XML 或 Properties 配置文件加载入内存中;2)Java 类里面解析 xml 或 properties 里面的内容,得到对应实体类的字节码字符串以及相关的属性信息; 3)使用反射机制,根据这个字符串获得某个类的 Class 实例; 4)动态配置实例的属性
装箱: 把一个基本数据类型换成其对应的包装类,基本数据类型存在栈中,而包装类存在堆中,所以装箱时会在堆中分配内存空间,创建一个新对象,性能损耗较大。
拆箱: 把一个包装类转换成其对应的基本数据类型,包装类在堆中,基本数据类型在栈中,由于拆箱后返回的值为基本数据类型,存在栈中,性能损耗不大。
如何实现自动装箱/拆箱?
在**装箱时调用Integer.valueOf()方法,在拆箱时调用Integer.intValue()**方法。同理,其他的基本数据类型装箱时都执行 对应包装类名.valueOf(),拆箱时执行 对应包装类名.xxValue() (xx为基本数据类型标识)。
Int类型的自动装箱:
装箱操作就要麻烦一点,会先判断装箱操作的值是否在一个范围之间:如果在这个范围内,就从缓存中返回一个缓存对象(该范围通常为-128~127);若不在这个范围内,则再新建一个对象返回。
public static Integer valueOf(int i) { if (i >= IntegerCache.low && i <= IntegerCache.high) return IntegerCache.cache[i + (-IntegerCache.low)]; return new Integer(i); }
final 在 Java 中是一个保留的关键字,可以声明成员变量、方法、类以及本地变量。一旦你将引用声明作 final,你将不能改变这个引用了,编译器会检查代码,如果试图将变量再次初始化的话,编译器会报编译错误。
**final变量:**凡是对成员变量或者本地变量(在方法中的或者代码块中的变量称为本地变量)声明为 final 的都叫作 final 变量。final 变量是只读的。
final方法: Java 里用 final 修饰符去修饰一个方法的唯一正确用途就是表达:这个方法原本是一个虚方法,现在通过 final 来声明这个方法不允许在派生类中进一步被覆写(override)
final类: 使用 final 来修饰的类叫作 final 类,final类通常功能是完整的,它们不能被继承,Java 中有许多类是 final 的,比如 String, Interger 以及其他包装类。
内存模型中的final :
final 变量,编译器和处理器都要遵守两个重排序规则,保证 final 变量在对其他线程可见之前,能够正确的初始化完成:
final关键字的好处:
同步与异步是对应于调用者与被调用者,它们是线程之间的关系,两个线程之间要么是同步的,要么是异步的
阻塞与非阻塞是对同一个线程来说的,在某个时刻,线程要么处于阻塞,要么处于非阻塞
阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态:
同步阻塞方式:
发送方发送请求之后一直等待响应。
接收方处理请求时进行的IO操作如果不能马上等到返回结果,就一直等到返回结果后,才响应发送方,期间不能进行其他工作。
同步非阻塞方式:
发送方发送请求之后,一直等待响应。
接受方处理请求时进行的IO操作如果不能马上的得到结果,就立即返回,取做其他事情。
但是由于没有得到请求处理结果,不响应发送方,发送方一直等待。
当IO操作完成以后,将完成状态和结果通知接收方,接收方再响应发送方,发送方才进入下一次请求过程。(实际不应用)
异步阻塞方式:
发送方向接收方请求后,不等待响应,可以继续其他工作。
接收方处理请求时进行IO操作如果不能马上得到结果,就一直等到返回结果后,才响应发送方,期间不能进行其他操作。 (实际不应用)
异步非阻塞方式:
发送方向接收方请求后,不等待响应,可以继续其他工作。
接收方处理请求时进行IO操作如果不能马上得到结果,也不等待,而是马上返回去做其他事情。
当IO操作完成以后,将完成状态和结果通知接收方,接收方再响应发送方。(效率最高)
1. 饿汉式:
**是否 Lazy 初始化:**否
**是否多线程安全:**是
**实现难度:**易
**描述:**这种方式比较常用,但容易产生垃圾对象。
**优点:**没有加锁,执行效率会提高。
**缺点:**类加载时就初始化,浪费内存。
它基于 classloader 机制避免了多线程的同步问题,不过,instance 在类装载时就实例化,虽然导致类装载的原因有很多种,在单例模式中大多数都是调用 getInstance 方法, 但是也不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化 instance 显然没有达到 lazy loading 的效果。
public class Singleton { // 创建一个实例对象 private static Singleton instance = new Singleton(); /** * 私有构造方法,防止被实例化 */ private Singleton(){} /** * 静态get方法 */ public static Singleton getInstance(){ return instance; } }
2. 懒汉式:
线程不安全的:
public class Singleton { private static Singleton instance; private Singleton (){} public static Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } }
对getInstance方法加锁即为线程安全的,但是,效率很低,99% 情况下不需要同步。
**懒汉和饿汉的对比:**大家可以发现两者的区别基本上就是第一次创作时候的开销问题,以及线程安全问题(线程不安全模式的懒汉)。
那有了这个对比,那他们的场景好理解了,在很多电商场景,如果这个数据是经常访问的热点数据,那我就可以在系统启动的时候使用饿汉模式提前加载(类似缓存的预热)这样哪怕是第一个用户调用都不会存在创建开销,而且调用频繁也不存在内存浪费了。
而懒汉式呢我们可以用在不怎么热的地方,比如那个数据你不确定很长一段时间是不是有人会调用,那就用懒汉,如果你使用了饿汉,但是过了几个月还没人调用,提前加载的类在内存中是有资源浪费的。
3. 双重校验锁模式
public class Singleton { private volatile static Singleton instance = null; private Singleton(){} public static Singleton getInstance(){ //先检查实例是否存在,如果不存在才进入下面的同步块 if(instance == null){ //同步块,线程安全的创建实例 synchronized (Singleton.class) { //再次检查实例是否存在,如果不存在才真正的创建实例 if(instance == null){ instance = new Singleton(); } } } return instance; } }
另外,需要注意 instance 采⽤ volatile 关键字修饰也是很有必要。instance 采⽤ volatile 关键字修饰也是很有必要的, instance = new Singleton();
这段代码其实是分为三步执⾏:
参考:设计模式之禅
观察者模式(Observer Pattern) 也叫做发布订阅模式(Publish/subscribe) ,它是一个在项目中经常使用的模式, 其定义如下:
Define a one-to-many dependency between objects so that when one object changes state,all its dependents are notified and updated automatically.(定义对象间一种一对多的依赖关系, 使得每当一个对象改变状态, 则所有依赖于它的对象都会得到通知并被自动更新)
● Subject被观察者
定义被观察者必须实现的职责, 它必须能够动态地增加、 取消观察者。 它一般是抽象类或者是实现类, 仅仅完成作为被观察者必须实现的职责: 管理观察者并通知观察者。
● Observer观察者
观察者接收到消息后, 即进行update(更新方法) 操作, 对接收到的信息进行处理。
● ConcreteSubject具体的被观察者
定义被观察者自己的业务逻辑, 同时定义对哪些事件进行通知。
● ConcreteObserver具体的观察者
每个观察在接收到消息后的处理反应是不同, 各个观察者有自己的处理逻辑。
被观察者:
public abstract class Subject { //定义一个观察者数组 private Vector<Observer> obsVector = new Vector<Observer>(); //增加一个观察者 public void addObserver(Observer o){ this.obsVector.add(o); } //删除一个观察者 public void delObserver(Observer o){ this.obsVector.remove(o); } //通知所有观察者 public void notifyObservers(){ for(Observer o:this.obsVector){ o.update(); } } } //具体被观察者 public class ConcreteSubject extends Subject { //具体的业务 public void doSomething(){ /* * do something */ super.notifyObservers(); } }
观察者:
public interface Observer { //更新方法 public void update(); } public class ConcreteObserver implements Observer { //实现更新方法 public void update() { System.out.println("接收到信息, 并进行处理! "); } }
观察者模式的使用场景
● 关联行为场景。 需要注意的是, 关联行为是可拆分的, 而不是“组合”关系。
● 事件多级触发场景。
● 跨系统的消息交换场景, 如消息队列的处理机制
ArrayList和Vector的区别?
Vector 类的所有⽅法都是同步的(所有方法都添加了Synchronized关键字)。可以由两个线程安全地访问⼀个Vector对象、但是⼀个线程访问Vector的话代码要在同步操作上耗费⼤量的时间。而ArrayList不是线程安全的
ArrayList的扩容机制?
add方法:
public void add(int index, E element) { //1. 检查是否错过数组大小/小于0 rangeCheckForAdd(index); //2. 确保容量是否满足条件 ensureCapacityInternal(size + 1); // Increments modCount!! //3. 创建新数组,将所需添加元素插入新数组 System.arraycopy(elementData, index, elementData, index + 1, size - index); elementData[index] = element; size++; }
private void ensureCapacityInternal(int minCapacity) { //如果当前数组为空数组,则取DEFAULT_CAPACITY(10),和minCapacity中最大值作为minCapacity扩容 if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); } ensureExplicitCapacity(minCapacity); } private void ensureExplicitCapacity(int minCapacity) { modCount++; // overflow-conscious code //如果minCapacity大于数组长度 if (minCapacity - elementData.length > 0) //数组扩容 grow(minCapacity); }
扩容:
private void grow(int minCapacity) { // overflow-conscious code int oldCapacity = elementData.length; //取原容量+原容量的一半之和作为新的容量 int newCapacity = oldCapacity + (oldCapacity >> 1); //取最大容量 if (newCapacity - minCapacity < 0) newCapacity = minCapacity; //如果满足条件,(minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE :MAX_ARRAY_SIZE; if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity); // 创建新的数组 elementData = Arrays.copyOf(elementData, newCapacity); }
HashMap源码解析见 https://blog.csdn.net/Zeroowt/article/details/113819853
ConcurrentHashMap的put插入方法:
public V put(K key, V value) { return putVal(key, value, false); } /** Implementation for put and putIfAbsent */ final V putVal(K key, V value, boolean onlyIfAbsent) { if (key == null || value == null) throw new NullPointerException(); //1.计算hash值 int hash = spread(key.hashCode()); int binCount = 0; for (Node<K,V>[] tab = table;;) { Node<K,V> f; int n, i, fh; //2.如果没有初始化,则进行初始化 if (tab == null || (n = tab.length) == 0) tab = initTable(); // 3.如果数组中无此节点,则直接插入 else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { //通过cas插入节点 if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null))) break; // no lock when adding to empty bin } else if ((fh = f.hash) == MOVED) tab = helpTransfer(tab, f); else { //如果存在Hash冲突,则在synchronized中插入 V oldVal = null; synchronized (f) { if (tabAt(tab, i) == f) { if (fh >= 0) { binCount = 1; for (Node<K,V> e = f;; ++binCount) { K ek; if (e.hash == hash && ((ek = e.key) == key || (ek != null && key.equals(ek)))) { oldVal = e.val; if (!onlyIfAbsent) e.val = value; break; } Node<K,V> pred = e; if ((e = e.next) == null) { pred.next = new Node<K,V>(hash, key, value, null); break; } } } else if (f instanceof TreeBin) { Node<K,V> p; binCount = 2; if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key, value)) != null) { oldVal = p.val; if (!onlyIfAbsent) p.val = value; } } } } if (binCount != 0) { if (binCount >= TREEIFY_THRESHOLD) treeifyBin(tab, i); if (oldVal != null) return oldVal; break; } } } addCount(1L, binCount); return null; }
网络IO
该模型的整体思路是有一个独立的Acceptor线程负责监听客户端的链接,它接收到客户端链接请求之后为每个客户端创建一个新的线程进行链路处理,处理完成之后,通过输出流返回应答给客户端,线程销毁。这就是典型的一请求一应答的通讯模型。
该模型的最大问题就是缺乏弹性伸缩能力,当客户端并发访问量增加后,服务端的线程数和客户端并发访问数呈现1:1的正比关系,由于线程是Java虚拟机非常宝贵的系统资源,当线程数膨胀之后,系统性能将极具下降,随着并发访问量的继续增大,系统会发生线程对栈溢出、创建新线程失败等问题,并最终导致进程宕机或者僵死,不能对外提供服务。
Java NIO 由以下几个核心部分组成:
虽然Java NIO 中除此之外还有很多类和组件,但在我看来,Channel,Buffer 和 Selector 构成了核心的API。其它组件,如Pipe和FileLock,只不过是与三个核心组件共同使用的工具类。
Java NIO的通道类似流,但又有些不同:
Channel的实现
这些是Java NIO中最重要的通道的实现:
Java NIO中的Buffer用于和NIO通道进行交互。如你所知,数据是从通道读入缓冲区,从缓冲区写入到通道中的。
缓冲区本质上是一块可以写入数据,然后可以从中读取数据的内存。这块内存被包装成NIO Buffer对象,并提供了一组方法,用来方便的访问该块内存。
使用Buffer读写数据一般遵循以下四个步骤:
一旦读完了所有的数据,就需要清空缓冲区,让它可以再次被写入。有两种方式能清空缓冲区:调用clear()或compact()方法。
缓冲区本质上是一块可以写入数据,然后可以从中读取数据的内存。这块内存被包装成NIO Buffer对象,并提供了一组方法,用来方便的访问该块内存。
为了理解Buffer的工作原理,需要熟悉它的三个属性:capacity,position,limit
Selector selector = Selector.open(); channel.configureBlocking(false); SelectionKey key = channel.register(selector, SelectionKey.OP_READ); while(true) { int readyChannels = selector.select(); if(readyChannels == 0) continue; Set selectedKeys = selector.selectedKeys(); Iterator keyIterator = selectedKeys.iterator(); while(keyIterator.hasNext()) { SelectionKey key = keyIterator.next(); if(key.isAcceptable()) { // a connection was accepted by a ServerSocketChannel. } else if (key.isConnectable()) { // a connection was established with a remote server. } else if (key.isReadable()) { // a channel is ready for reading } else if (key.isWritable()) { // a channel is ready for writing } keyIterator.remove(); } }
总结NIO和IO的区别:
在 Java 中,所有的异常都有⼀个共同的祖先 java.lang 包中的 Throwable 类。 Throwable: 有两个重要的⼦类: Exception(异常) 和 Error(错误) ,⼆者都是 Java 异常处理的重要⼦类,各⾃都包含⼤量⼦类。
Error(错误) : 是程序⽆法处理的错误,表示运⾏应⽤程序中较严重问题。⼤多数错误与代码编写者执⾏的操作⽆关,⽽表示代码运⾏时 JVM(Java 虚拟机)出现的问题。这些错误表示故障发⽣于虚拟机⾃身、或者发⽣在虚拟机试图执⾏应⽤时,如 Java 虚拟机运⾏错误(Virtual MachineError)、类定义错误(NoClassDefFoundError)等。
Exception(异常) :是程序本身可以处理的异常。 Exception 类有⼀个重要的⼦类RuntimeException。 RuntimeException 异常由 Java 虚拟机抛出。 NullPointerException(要访问的变量没有引⽤任何对象时,抛出该异常)、 ArithmeticException(算术运算异常,⼀个整数除以 0时,抛出该异常)和ArrayIndexOutOfBoundsException (下标越界异常)。
第二种方式:将对象序列化为字节序列后,默认会将该对象的整个对象图进行序列化,再通过反序列即可完美地实现深拷贝。
https://github.com/beyondfengyu/SnowFlake
ID是数据的唯一标识,在互联网企业中,大部分公司使用的都是Mysql,并且因为需要事务支持,所以通常会使用Innodb存储引擎,传统的做法是利用UUID和数据库的自增ID,UUID太长以及无序,所以并不适合在Innodb中来作为主键,自增ID比较合适,
但是 但是随着公司的业务发展,数据量将越来越大,需要对数据进行分表,而分表后,每个表中的数据都会按自己的节奏进行自增,很有可能出现ID冲突。
这时就需要一个单独的机制来负责生成唯一ID,生成出来的ID也可以叫做 分布式ID,或全局ID。
**参考自:**小林coding公众号
什么是TCP?
TCP 是⾯向连接的、可靠的、基于字节流的传输层通信协议
**⾯向连接:**⼀定是「⼀对⼀」才能连接,不能像 UDP 协议可以⼀个主机同时向多个主机发送消息,也就是⼀对多是⽆法做到的;
**可靠的:**⽆论的⽹络链路中出现了怎样的链路变化, TCP 都可以保证⼀个报⽂⼀定能够到达接收端;
**字节流:**消息是「没有边界」的,所以⽆论我们消息有多⼤都可以进⾏传输。并且消息是「有序的」,当「前⼀个」消息没有收到的时候,即使它先收到了后⾯的字节,那么也不能扔给应⽤层去处理,同时对「重复」的报⽂会⾃动丢弃。
TCP报文:
IP 层是不可靠的,它不保证⽹络包的交付、不保证⽹络包的按序交付、也不保证⽹络包中的数据的完整性。
如果需要保障⽹络数据包的可靠性,那么就需要由上层(传输层)的 TCP 协议来负责。
因为 TCP 是⼀个⼯作在传输层的可靠数据传输的服务,它能确保接收端接收的⽹络包是⽆损坏、⽆间隔、⾮冗余
和按序的。
OSI网络模型:
TCP/IP网络模型:
应⽤层,负责向⽤户提供⼀组应⽤程序,⽐如 HTTP、 DNS、 FTP 等;
传输层,负责端到端的通信,⽐如 TCP、 UDP 等;
⽹络层,负责⽹络包的封装、分⽚、路由、转发,⽐如 IP、 ICMP 等;
⽹络接⼝层,负责⽹络包在物理⽹络中的传输,⽐如⽹络包的封帧、 MAC 寻址、差错检测,以及通过⽹卡传输⽹络帧等;
什么是TCP连接?
⽤于保证可靠性和流量控制维护的某些状态信息,这些信息的组合,包括Socket、序列号和窗⼝⼤⼩称为连接。
IP 层是「不可靠」的,它不保证⽹络包的交付、不保证⽹络包的按序交付、也不保证⽹络包中的数据的完整性。
Socket:由 IP 地址和端⼝号组成
序列号:⽤来解决乱序问题等
窗⼝⼤⼩:⽤来做流量控制
TCP 四元组可以唯⼀的确定⼀个连接,四元组包括如下:源地址、源端⼝、⽬的地址、⽬的端⼝
TCP 是⾯向连接的协议,所以使⽤ TCP 前必须先建⽴连接,⽽建⽴连接是通过三次握⼿来进⾏的。
- ⼀开始,客户端和服务端都处于 CLOSED 状态。先是服务端主动监听某个端⼝,处于 LISTEN 状态
如何在 Linux 系统中查看 TCP 状态?
TCP 的连接状态查看,在 Linux 可以通过 netstat -napt 命令查看。
** &为什么是三次握⼿?不是两次、四次**
TCP三次连接是为了初始化Socket、序列号和窗⼝⼤⼩信息。
以三个⽅⾯分析三次握⼿的原因:
如果是两次握⼿连接,就不能判断当前连接是否是历史连接,三次握⼿则可以在客户端(发送⽅)准备发送第三次
报⽂时,客户端因有⾜够的上下⽂来判断当前连接是否是历史连接
如果只有「两次握⼿」,当客户端的 SYN 请求连接在⽹络中阻塞,客户端没有接收到 ACK 报⽂,就会重新发送 SYN ,由于没有第三次握⼿,服务器不清楚客户端是否收到了⾃⼰发送的建⽴连接的 ACK 确认信号,所以每收到⼀个 SYN 就只能先主动建⽴⼀个连接,这会造成什么情况呢?
如果客户端的 SYN 阻塞了,重复发送多次 SYN 报⽂,那么服务器在收到请求后就会建⽴多个冗余的⽆效链接,造成不必要的资源浪费。
TCP 断开连接是通过四次挥⼿⽅式。双⽅都可以主动断开连接,断开连接后主机中的资源将被释放
为什么挥⼿需要四次?
为什么 TIME_WAIT 等待的时间是 2MSL?
MSL 是 Maximum Segment Lifetime, 报⽂最⼤⽣存时间,它是任何报⽂在⽹络上存在的最⻓时间,超过这个时间报⽂将被丢弃。因为 TCP 报⽂基于是 IP 协议的,⽽ IP 头中有⼀个 TTL 字段,是 IP 数据报可以经过的最⼤路由数,每经过⼀个处理他的路由器此值就减 1,当此值为 0 则数据报将被丢弃,同时发送 ICMP 报⽂通知源主机。
TIME_WAIT 等待 2 倍的 MSL,⽐较合理的解释是: ⽹络中可能存在来⾃发送⽅的数据包,当这些发送⽅的数据包被接收⽅处理后⼜会向对⽅发送响应,所以⼀来⼀回需要等待 2 倍的时间。
⽐如如果被动关闭⽅没有收到断开连接的最后的 ACK 报⽂,就会触发超时重发 Fin 报⽂,另⼀⽅接收到 FIN 后,会重发 ACK 给被动关闭⽅, ⼀来⼀去正好 2 个 MSL。
因此,保证「被动关闭连接」的⼀⽅能被正确的关闭,即保证最后的 ACK 能让被动关闭⽅接收,从⽽帮助其正常关闭,所以需要Time-wait状态
如果已经建⽴了连接,但是客户端突然出现故障了怎么办?
TCP 有⼀个机制是保活机制。这个机制的原理是这样的:
定义⼀个时间段,在这个时间段内,如果没有任何连接相关的活动, TCP 保活机制会开始作⽤,每隔⼀个时间间隔,发送⼀个探测报⽂,该探测报⽂包含的数据⾮常少,如果连续⼏个探测报⽂都没有得到响应,则认为当前的TCP 连接已经死亡,系统内核将错误信息通知给上层应⽤程序
因为http协议是无状态的,也就是说每个客户访问服务器资源时,服务器并不知道该客户端是谁,所以需要会话技术识别客户端的状态。会话技术是帮助服务器记住客户端状态(区分客户端)
从打开一个浏览器访问某个站点,到关闭这个浏览器的整个过程,成为一次会话,会话技术就是记录这次会话中客户端的状态和数据的
**Cookie:**存在客户端本身,相对不安全,减少服务器压力
**Session:**存在服务器端,安全性相对好,但服务器压力大一些