MySql数据目录初始化 第一次启动MySql的时候需要进行初始化,也就是初始化数据存放的目录(注意这个目录必须为空,或则不存在)这个目录路径可以在配置文件里面配置
[mysqld] port=3306 datadir=/home/lyer/tmp/data 默认的数据存放路径是:
$MYSQL_HOME/data MySql服务器启动会读取配置文件my.cnf,配置文件查找顺序为:
/etc/my.cnf /etc/mysql/my.cnf $MYSQL_HOME/my.cnf --default-extra-file #命令行自定配置文件路径 ~/.my.cnf 服务器会依次查找这些路径去读取并且合并配置
下面是数据库初始化命令
mysqld --initialize #初始化 会打印初始的root密码在终端 第一次登入需要密码 mysqld --initialize-insecure #初始化并且设置root密码为空 也就是初次不需要密码 初始化之后我们需要登入到数据库,并且需要修改root密码为自定义的密码
mysql -uroot -p #没有密码则回车即可 有初始密码则需要输入 alter user 'root'@'localhost' identified by '55555'; 如果觉得每次输入用户密码指定host麻烦的话则可以在my.conf里面配置客户端
[client] host=127.0.0.1 user=root password=55555 这样的话直接输入mysql就会读取客户端配置,这样就不需要输入密码了
MySql服务启动、停止 一般通过$MYSQL_HOME/support-files/mysql.server服务启动脚本启动,此脚本还会启动一个监控进程mysqld-safe,此进程会监控mysql服务的运行状态,如果出错了则会将错误日志记录起来xxx.err,还会帮助mysql服务重启
$MYSQL_HOME/support-files/mysql.server #不带参数执行可以查看Useage mysql.server start #启动 (第一次启动服务器需要首先初始化数据库) mysql.server status #查看mysql服务状态 mysql.server stop
用户管理 和用户相关的信息都保存在mysql.user表中,密码都是加密存储的
select host,user from user; User列表示用户名,Host列表示允许连接的客户端主机地址,为localhost则表示只允许本地连接
创建一个用户
create user lyer identified by '66666'; --不指定host的话默认全部可以连 create user 'lyer'@'%' identified by '55555'; --%表示允许全部IP连接 create user 'lyer'@'112.
为什么需要分布式锁 单机应用中(也就是一台机器中)的锁用于控制当前程序多个线程并发而引发的资源争夺问题,但是当应用场景扩展到了分布式环境就不一样了,这样的锁就没用了。因为每台机器都是独立的,如果还是之前的锁的话只能保证本机器不会引发资源竞争问题,分布式环境下相当于每台机器都有一个自己独立的锁,所以无法避免资源竞争问题
这个时候就需要将这把锁保存到第三方中 (比如Redis),多台机器同时到Redis中取抢锁,这就可以保证分布式环境下争抢的是同一把锁
Redis分布式锁实现思路 首先,Redis里面并没有锁的概念。所谓的锁其实就是Redis里的一个key,加锁就是设置这个key,释放锁就是删除这个key
一个进程如果在请求加锁的过程中发现这个key已经存在了则表示加锁失败,这个锁已经被别人持有了。如果发现这个key不存在则表示这个锁没有被人持有
Redis中可以借助如下命令来实现判断如果没有锁再加锁操作:
#此命令表示如果key不存才会设置key并且返回1 如果是普通的set则会覆盖 setnx lock 1 单纯的这样实现分布式锁貌似太简单了,并且有个问题: 死锁
也就是说如果一台机器设置了锁,在执行过程中宕机了或出错了,那么这把锁将永远得不到释放,其他机器进程就永远无法获取到锁,引发死锁问题
于是我们可以借助Redis的key过期功能来给锁设置一个过期时间,这样就不用怕锁永远得不到释放了。同时需要注意设置key以及对应的过期时间这一系列动作应该是原子的,否则在设置key时还没来得及设置过期时间这台机器又宕机了还是会引发死锁问题。
综上,我们使用Redis提供的set命令以及参数来实现
#如果key不存在则设置lock并且过期时间为10s (nx表示不存在才会设置 存在则失败返回0) set lock 1 ex 10 nx 但是这样还会引发一个锁被错误释放问题
想象一下下面的场景:
A加锁执行,但是在莫个操作上面阻塞很久,此时锁过期了被自动释放 B获得锁继续执行 在B执行过程中A从阻塞中恢复了,并且A执行完毕了 于是A再次释放了锁(注意此时A释放的是B设置的锁)但是此时B还在执行中 此时C过来了,于是C就抢到锁了 这样就引发了一个错误: 在B加锁执行过程中,B的锁被错误的释放了
为了解决锁被错误释放的问题,我们需要给锁设置一个唯一标识,这个锁标识了是哪个进程加的锁,并且只有加锁的进程本身才能释放这把锁
set lock <uuid> ex 10 nx 实现思路就是线程在释放锁的时候获取一下key对应的value也就是锁的标识,判断一下是否是自己的那把锁。
如果不是自己的说明自己的锁已经超时被自动释放了则不会再次释放别人的锁,如果是自己的则进行释放
完整代码:
//redis分布式锁的实现 type Lock struct { RedisClient *redis.Client Key string UUID string Expire time.Duration } func NewLock(key string, expire time.Duration) *Lock { uuid, err := exec.
什么是柔性数组 结构中最后一个元素允许是未知大小的数组,这个数组就是柔性数组
但结构中的柔性数组前面必须至少一个其他成员,柔性数组成员允许结构中包含一个大小可变的数组,sizeof返回的这种结构大小不包括柔性数组的内存。包含柔数组成员的结构体用malloc函数进行内存的动态分配,且分配的内存应该大于结构的大小以适应柔性数组的预期大小
为什么需要柔性数组 C中的结构体都是固定大小的,但是有些时候我们需要一个可变大小的结构体,比如有时候需要在结构体中存放一个长度动态的字符串
typedef struct mystr { int len; //记录字符串长度 char *data;//底层的char数组指针 }mystr; 我们需要为data malloc一段内存,然后通过这个指针访问这段内存。
首先我们按照常规的做法,不做任何处理,直接malloc,如下
typedef struct mystr { int len; char* data; }mystr; int main(int argc, char const *argv[]) { char* c = "hello,world"; //分别分配内存 mystr* s = (mystr*)malloc(sizeof(mystr)); s->data = (char*)malloc(strlen(c)+1); //+1是为\0分配的 strlen不会将\0计算进来 strcpy(s->data,c); s->len = strlen(s->data); printf("len:%d data:%s\n",s->len,s->data); //11 hello,world //分别释放空间 free(s->data); free(s); return 0; } 可以看到上面的操作比较麻烦,结构体和内部的data指针分配内存和释放内存操作都是分开的,data数据区和结构体不是连续的两块内存,这样会带来两个问题:
操作不方便(上面说了,分配和释放都需要手动进行两次) 两次malloc分配的内存不是连续的,容易造成内存碎片,并且多次的释放和分配内存也会带来一定的消耗 为了让他们两个内存连续,也就是说为了能够实现动态长度的struct,分配只需要分配一次,释放也只需要拿到struct的指针即可释放所有内存
方案一: 指针运算
typedef struct mystr { int len; char* data; }mystr; int main(int argc, char const *argv[]) { char* c = "hello,world"; //直接malloc一块连续的内存 然后强转为mystr指针 mystr *s = (mystr*)malloc(sizeof(mystr)+strlen(c)+1); s->data=NULL; //此data指针闲置不用 //mystr结构体大小为单位进行+1 结构体之后就是str的内存区域 //此处将数据copy到此区域 strcpy((char*)(s+1),c); s->len = strlen(c); //int所占字节数,64位机器为4字节 printf("sizeof(int): %ld\n", sizeof(int)); //内存对齐 此处是16字节 printf("sizeof(mystr): %ld\n", sizeof(*s)); //s的起始地址 data指针的地址 printf("mystr:%p data: %p\n",s,&(s->data)); //数据,null,此处为空,故此变量已经被浪费。访问对应字符串数据需要(char *)(p+1) printf("p->data: %s\n", s->data); //偏移后,对应的字符串 printf("data msg: %s\n", (char*)(s+1)); printf("data msg: %s\n",(char*)(&(s->data))+8); free(s); } 方案二: 柔性数组
链表分类 链表主要分为一下几类:
单链表 循环链表 双链表
各个链表比较 开发中最常用的链表就是 循环双链表
循环链表和单链表对比起来,循环链表有如下几个优点:
通过任意一个节点即可以遍历所有的节点 单链表和双链表对比起来,双链表有如下几个优点:
可以轻松获取一个节点的前后节点 双链表的删除和插入节点效率更高更方便 双链表遍历更方便,可以从一个节点开始可以往前往后遍历
链表和数组的比较 随机访问
数组支持随机访问,随机访问的时间复杂度为O(1),但是链表不支持随机访问,如果要访问指定位置的节点必须遍历前面所有的节点,时间复杂度为O(n)
查找元素
链表和数组查找元素的时间复杂度都是O(n) ,但是对于一个有序的数组可以使用二分查找来加快速度,此时时间复杂度为O(logn),但是对于链表就没有办法进行二分了
对于元素的插入和删除,链表的时间复杂度为O(1),而数组则为O(n),因为数组需要进行元素的移动,链表只需要改变指针即可
总结
如果是需要频繁进行随机访问的时候可以使用数组,如果需要频繁进行增加和删除则可以考虑使用链表
先说答案吧:
历史问题,C语言数组下标使用0,于是之后的语言都使用0 数组的偏移运算规则 历史问题这个不用多说了,下面说一下第二条原因
我们知道数组因为内存都是连续存放的,每个元素的大小都是固定的,所以支持随机访问,我们只需要根据偏移和元素大小来计算出地址即可实现随机任意的访问数组中的元素,所以随机访问效率为 O(1)
数组查找一个元素的效率为O(n),如果是有序数组查找一个元素使用二分的话那就是O(logn)
a[i]的地址 = a + i * size 我们看到,如果是第一个元素,那么i=0,如果是第二个元素那么i=1 ,这就是为什么数组下标从0开始的原因
但是如果硬要从1开始的话,那么上述的公式就会变成如下:
a[i]的地址 = a + (i-1) * size 这样就会多一次i-1的减法操作,产生不必要的消耗,因为数组可能会进行一个频繁的随机查找
单例模式 单例模式有两种实现方式:
饿汉式单例
类一加载就创建单例对象,如果对象比较多比较大并且在运行中始终没有用到那么就白白的消耗内存了
饿汉模式可以通过如下几个方法来实现:
直接赋予属性值 静态代码块 Enum枚举(Java推荐方式) 懒汉式单例
懒汉和饿汉相反,只有当对象用到的时候才创建,但是这种模式需要考虑一个并发问题,如果处理的不恰当的话就会破坏单例而创建出两个对象
实现饿汉的方式主要有下面几个:
静态内部类(Java语法特性,只有用到静态内部类的时候静态内部类才会被加载) 方法级加上同步锁 双重检查,控制锁的粒度更小 上面的单例都会被 反射 给破坏,所以Java推荐以Enum方式创建单例来防止反射破坏单例,Java在编译层面防止用反射创建Enum对象
容器注册式单例
还有一种 容器注册式单例 ,在Spring中会有一个IOC容器,如果没有显示指定对象需要多个,那么Spring都只会创建一个单例对象并且注册进IOC容器,需要的时候直接去这个容器中获取,如果容器中没有则进行创建并且保存到容器中,如果有则直接从容器中取出来返回,这个容器可以简单的看成是一个ConcurrentHashMap
容器注册式单例和对象式单例的区别?
下面列出一些具体的应用:
Runtime类使用的就是饿汉式单例,这是一个JVM运行时类,记录了JVM运行时的一些信息比如JVM可用的堆内存,可用的CPU个数等
Spring中会解析XML配置文件然后通过BeanFactory创建单例的Bean对象同时注册到IOC容器中供程序进行DI注入
我们在使用JDBC连接数据库的时候创建的JDBC连接对象是单例的,因为我们连接对象仅仅只是维护了一些连接数据库的参数,查询的时候会根据这些参数创建TCP连接,所以这样的连接上下文对象也只需要一个即可
工厂模式 工厂模式有三种,三种模式都是逐步演变过来的?
简单工厂
所有对象的创建都在一个工厂类里,工厂类职责过于复杂
工厂方法
定义一个工厂接口,每个类都定义一个创建他本身的工厂类,缺点就是工厂类随着类的增加会逐渐增加
抽象工厂
在工厂方法上面做的一个改进,不为每个类都创建工厂类了,而是只为同一类对象只创建一个工厂类,抽象工厂所关注的是如何创建一系列的类
比如美的和海尔各自有自己的工厂类,美的工厂只负责创建美的空调、美的冰箱、美的电磁炉等,而海尔工厂只负责创建海尔冰箱、海尔空调、海尔电磁炉等
依赖注入Dependency Injection其实就是用工厂模式来创建对象的
建造者模式 建造者模式和工厂模式的区别在于,建造者模式关注的是创建一个对象的过程,而工厂模式关注的是创建哪个对象
建造者模式相当于对一个类的创建过程的一个封装,将类的创建过程抽象为一个建造者类,这样类本身的构造和实现就分离开来了,我们可以很清晰的通过这个类的建造者类知道这个类是如何被创建的,并且很清晰的看见这个类本身的各个实现代码,而不是看见一坨混乱的代码
在构造者类里面我们还可以做一些参数校验的工作来保证类的正确安全的创建
StringBuilder就是建造者模式,里面有一个字节数组,每次append的时候都是往这个数组里面加,然后toString的时候就将这个字节数组转化为字符串
另外此类是并发不安全的,StringBuffer是并发安全的,所以在非并发场景下选择StringBuilder速度快没有锁的消耗,在并发场景下则选择StringBuffer保证并发安全
原型模式 就是 拷贝模式,是对原来类的一个拷贝,如果一个类的创建比较耗时间并且复杂,那么可以使用原型模式减少创建类的过程和消耗
代理模式 代理模式强调的是代替一个类去做一件事,强调的是控制访问,这件事可能需要很多前置准备或则后置收尾工作,这样的话类本身只需要关注自己的事情即可,其他多余的事情多余的前期准备工作都由代理类去完成,代理类会为你准备好一切供你使用
在Java代码中的体现就是 代理类持有一个被代理的对象,是一种组合的思想
Java的代理模式里面还需要了解一个JVM的动态代理,动态代理就是在JVM运行的时候通过反射来创建代理类,这样就不需要自己手动创建代理类了,因为代理类往往有很多模板代码
装饰器模式 装饰模式和代理模式的区别在于,装饰模式强调的是 功能增强,装饰模式可以在不改变原来代码的情况下增加一些功能,这样就不会破坏原来的代码还可以增加功能,这样就解耦合了
在Java代码中的体现就是 装饰类持有一个目标对象,同时装饰类也实现了目标对象的接口,在接口方法里面调用目标对象的方法的同时进行增强 ,这就是组合的典型应用,而不是使用继承
Java中的IO就有装饰器模式,BufferedInputStream里面包含一个InputStream,进行读取的时候先调用InputStream的read方法预读取一部分数据到自己的缓存区里,用户调用BufferedInputStream读取的时候就是从缓存区里读取数据,这样就加快了读取的速度,并且不用每次都调用InputerStream读取数据了,因为直接的InputStream的read往往会进行系统调用
适配器模式 适配器模式就是对老的接口做一个适配,这样就不需要改原来的代码即可和现有的系统接入,比如手机充电器必须将220v的电适配一下转化为手机电池受得了的电压
门面模式 门面模式强调的是整合多个接口为一个同一简单的接口,就是提供一个公共简洁的接口将多个复杂的接口合并为一个简单易用的接口供外部调用,主要的目的有下面几个:
封装内部细节,提供统一的接口 将多个接口调用合并为一个,比如可以减少网络传输的消耗,前端不需要多次请求,只需要请求一个公共合并的接口进行一次网络通讯即可获取所有信息 保证事务的原子型,将多个必须共同完成的接口放入到一个统一的接口中进行调用这样就保证了原子型 其实我们在SpringBoot后端程序的时候就是门面模式,每个Dao接口只负责查询自己负责的数据,Service可能需要多个数据,于是就会调用多个Dao接口来查询数据然后返回给Controller,同理Controller也可能需要调用多个Service接口,而前端只需要请求一个Controller接口即可获取数据
DNS协议 在讲解为什么之前我们先来讲解什么是DNS协议,以及DNS协议的一些细节
DNS协议的作用就是建立 域名和IP地址的映射,因为IP地址太长并且没有语义所以需要给IP一个域名,DNS协议的作用就是将域名转化为IP地址,这就是DNS协议的主要功能,当然还有其他功能比如记录CNAME、IP地址负载均衡等
浏览器在请求一个网址之前需要通过DNS协议获取到这个网址的IP地址这样才可以进行网络通讯建立TCP连接,因为网络中都是以IP地址来识别一个主机的
那么浏览器向谁请求DNS协议呢?这就是DNS服务器的功劳
DNS服务器是专门用来记录域名以及IP地址的映射的一个服务器,其他计算机可以通过DNS协议来请求DNS服务器来查询他所需要的信息,这些信息也是通过DNS协议返回回去的,主机只需要依照DNS协议的规范规则进行请求并且解析响应即可获取信息
DNS服务器主要会记录域名的如下几种记录:
类型 解释 A IP地址记录Address 记录域名对应的IP AAAA IPV6的地址记录 NS DNS服务器记录(Name Server),返回记录此服务器的DNS服务器域名 CNAME 规范名称记录(Canonical Name),返回另一个域名,即当前查询的域名是另一个域名的跳转 SRV 用于服务发现和负载均衡 下面来看一个DNS查询icepan.cloud的完整过程:
dig -t A icepan.cloud +trace .是根DNS服务器,查询会先根据缓存在本地的根域名服务器的NS记录和A记录去查询到权威域名服务器对应的A记录和NS记录
下面是13组根域名服务器对应的NS记录
. 7117 IN NS g.root-servers.net. . 7117 IN NS l.root-servers.net. . 7117 IN NS d.root-servers.net. . 7117 IN NS b.root-servers.net. . 7117 IN NS h.root-servers.net. . 7117 IN NS k.root-servers.net. . 7117 IN NS f.root-servers.net. . 7117 IN NS m.
概述 粘包 就是指应用层的多条消息被合并为一个TCP包了,这个其实在发送端和接收端都会产生
还有一种现象叫拆包,指的是应用层数据被拆分为多个TCP包了
发送端 正常的情况
每个应用层数据都会被封装为一个TCP包然后发送
两个包粘在一起的情况
如果应用层传递下来的数据太少那么TCP协议会等待更多的数据再发送,这样可以提高传输效率,防止大头儿子的情况
粘包和拆包一起的情况
如果应用层数据太大超过了TCP协议的MSS大小,则会发生拆包
接收端 接收端如果来不及处理数据,那么多个到达的TCP包都会加入缓存队列,此时也会发生粘包现象
为什么UDP不会产生粘包问题 因为TCP是面相流的,不认为应用层的消息是一条一条的,只是将应用层传递下来的数据看成一个数据流,没头没尾
而UDP则是基于报文的,将应用层的数据看成是一个完整的数据包,所以不会进行拆分和合并,不管应用层传递下来多大多小的数据都只会封装为一个UDP包进行传输,接收端也一样会将接受到的UDP报文完整的交付给应用层
参考 TCP粘包拆包及解决方法
什么是TCP粘包?怎么解决这个问题
TCP和UDP的区别 其实这个标题可以改为 【TCP和UDP的区别】
我们知道TCP只能进行一对一的传输,而UDP则可以进行一对一、多对多、一对多、多对一传输,主要原因就是TCP会建立虚拟通道建立一个连接来保证数据包的可靠传输,数据包丢了必须进行重传
UDP就不需要保证可靠传输,只要把数据封装为UDP报文填上目标的IP+端口发送出去就行,其它的都交给网络了,不管到没到。
所以一个主机可以发送UDP包给任何的主机而不需要提前建立连接,如果目标主机有监听这个端口那么就可以接收到数据,没有监听则会把这个包丢掉
同理一个主机也可以接受来自任意主机的UDP报文,只需要监听着端口等待数据即可
所以UDP报文其实只是相当于给IP报文加上了个IP+端口而已,继承了IP包、以太网数据帧的特性,不管网络拥塞不拥塞只要应用层有数据就发送,而TCP报文就复杂了,需要记录各个信息,这些信息都是一些维护连接,控制流量的一些信息
TCP是面相字节流的,而UDP是面相报文的
这句话估计已经耳熟能详倒背如流的,那么什么是面向字节流呢?什么是面相报文呢?
什么是面相字节流 字节流可以理解为水流,TCP连接会建立一个虚拟通道(其实就是双方维护连接的数据结构,发到哪了收到哪了下面该发那段数据了),建立通道之后这样就只能一对一的进行可靠传输了,TCP会将应用层的数据看成是一个无结构的字节流数据,简单来说就是没头没尾
如果应用层传递下来的数据太少则TCP可以等一等,积累一些数据再封装为一个TCP包发送,这样就提高传输的效率,因为如果数据大小都比TCP报文头的数据还小这样传输数据的效率是不划算的,这样将就会将应用层传递下来的多个数据合并为一个TCP包进行传输
如果应用层传递下来的数据太多,那么TCP就会拆分为多个TCP报文进行传输,因为TCP报文有一个MSS规定了报文的最大大小 ,这个值和以太网的MTU大小有关,如果一个TCP报文不规定合适的MSS 那么可能会被拆分为多个IP包进行传输,这个就会增加IP包拆分和组装的消耗,所以MSS应该和MTU大小接近不能超过这个值
每次发送的数据包大小都是不一致的,这个和网络状况以及对方的数据接受能力有关,也就是滑动窗口和拥塞控制窗口
什么是面相报文 UDP是面相报文的,UDP将应用层数据看作是一个有头有尾的完整的数据包,
也就是说不管应用层传递下来多少数据,多大的数据,UDP都会封装为一个完整的数据包发送出去
接收端也不管接受到了多大多小的UDP数据包都不会进行拆分和组装,而是完整的将UDP报文数据上交给应用层
总结 总的来说,TCP是以一次连接的建立和断开为单位进行传输数据的,在连接还没有断开之前一直可以往这个通道里传输数据流,并且不知道这个连接什么时候断开,只有当应用层通知连接可以关闭即开始4次握手断开连接,连接建立代表数据开始传输,连接结束代表数据传输完毕,所以说TCP是面相字节流
UDP则以一次数据包的发送接受为单位,只要应用层传递下来数据,UDP就封装为一个完整的UDP数据包发送出去,此时就可以表示此次的发送已经完成,后面的发送都是独立和无关的都代表着一次UDP连接发送
为什么TCP连接是3次握手 注意上面的初始seq号是随机生成的,对方返回的ack必须是seq+1
一般我们人和人打招呼一来一回两次即可,但是放到网络上就不能这样了,因为网络环境是比较复杂并且不稳定的
TCP进行3次握手主要为了如下几个原因:
防止重复的历史连接 建立双方都承认的稳定的TCP连接 初始化两端的seq序列号 防止重复的历史连接 我们首先假设TCP连接发送的包都没有产生丢包,产生丢包的情况下面再说
A向B发送请求连接的SYN包,此时网络比较拥塞导致A的SYN包还没有到达B端,超时之后A会继续发送SYN包请求连接直到超过了重试次数则放弃。
如果此时其中一个SYN包到达了B端,如果B同意连接则会回复A一个SYNACK
如果只进行两次连接,那么此时A和B就都认为已经建立了TCP连接,然后他们开始通讯。但是此次通讯时间很短很快,于是进过短暂的通讯之后就断开了。
此时如果之前A重试的SYN包又到达了,则B是无法判断这个是不是A之前重试的SYN包,以为A还需要继续建立连接,于是B又会回复一个SYNACK给A同时认为已经和A建立了连接。
但是A收到第二次的SYNACK时显然是可以判断出这个是上次重试的SYN包,于是A就不会建立连接,那么就会出现B单相思的情况,B就一直等待A发送数据,这样就空耗资源占用TCP队列,因此如果不进行第3次确认的话就会出现很多不符合预期的连接。
于是TCP需要三次握手才能认为建立连接,在B发送ACK的时候还需要等待A的应答ACK,如果A收到了B的ACK并且判断出这是之前重试的SYN包(因为A有足够的上下文来判断,而B没有),于是A就不会同意建立连接,发送RST包来终止这次请求,告诉B不需要进行重试了,B也就不会建立连接了
防止建立错误的连接 下面我们来考虑产生丢包的情况以及A没有收到ACK的情况
如果进行两次握手就建立连接,这种连接是不可靠的
A发送SYN包给B,B此时需要回复一个ACK,如果只进行两次握手的话,那么此时B就认为和A建立了连接,但是有下面两种情况会产生错误:
B的ACK丢了 但是如果B回复的ACK丢了,那么A没有收到B的ACK则以为B不想建立连接于是就一直重试直到超过规定次数,但是B却认为已经和A建立连接了,这纯属单相思
A挂了 如果A挂了,那么B发送ACK之后却认为已经和A建立连接,这样也是不合理的
A不同意建立连接而没有回复ACK 比如A判断这个SYN包是之前过期的包,于是不同意建立连接不回复ACK,但是此时B却认为建立了连接,这样也是不合理的,这个上面说了
所以B需要收到A的应答之应答之后才可以认为建立了连接
如果B的ACK到达了A并且A也同意建立连接那么A就会回复一个ACK给B,此时A就可以认为已经和B建立连接了,于是就可以发送数据给B或则等待B发数据
如果B一直没收到A的应答则会继续发ACK,此时A则会继续回复ACK直到B收到,如果B收到了A的ACK则可以认为建立了连接
按道理A也可以继续等待B的ACK,但是这样多次回合的握手和3次握手的效果都是一样的,所以进行3次即可认为建立连接
初始化Seq号 A 要告诉 B,我这面发起的包的序号起始是从哪个号开始的,B 同样也要告诉 A自己发起的包的序号起始是从哪个号开始的,只有三次握手才可以100%确定对方都收到了自己的seq号
seq号通常需要随机产生,如果都从1开始那么会出现问题,主要会产生2个问题
问题一
比如 A 连上 B 之后,发送了 1、2、3、4 这4个包,但是发送 4 的时候丢了,或者绕路了,于是重新发送
后来 A 掉线了,重新连上 B 后,序号又从 1 开始,然后发送1、2、3但是压根没想发送 4,但是上次绕路的那个 4 又回来了,发给了B,B认为,这就是下一个包,于是发生了错误
问题二
另外一个原因就是防止伪造TCP包进行攻击,seq号如果每次都固定,那么黑客能可以很轻松的判断一个TCP包属于此次连接的哪一部分,于是就很容易伪造TCP包进行攻击,如果seq号是随机的那么黑客就很难直到这个包到底属于哪个范围哪一部分,这样就加大了攻击的难度
总结 TCP的握手其实可以一直进行下去,4、5、6、400….
但是进行再多的握手其实和3次握手的结果都是一样的,是没必要的
3次握手之后双方都刚好进行了 一收一发,这样就可以确定建立连接了
参考 为什么 TCP 建立连接需要三次握手