1、消息类中间件 RabbitMQ AMQP协议(高级消息队列协议)
优点:异步处理、服务解耦、流量控制(削峰)。 AMQP模型的几大组件模型? 交换器“exchange”接收发布应用程序发送的消息,并根据一定的规则将这些消息路由到“消息队列”。 队列“message queue”存储消息,位于硬盘或内存中,直到这些消息被消费者安全处理完为止。 绑定“binding”定义了exchange和message queue之间的关联,提供路由规则,告知交换器消息应该将消息投递给哪个队列。 使用这个模型我们可以很容易的模拟出存储转发队列和主题订阅这些典型的消息中间件概念。 一个AMQP服务器类似于邮件服务器,exchange类似于消息传输代理(email里的概念),message queue类似于邮箱。Binding定义了每一个传输代理中的消息路由表,发布者将消息发给特定的传输代理,然后传输代理将这些消息路由到邮箱中,消费者从这些邮箱中取出消息。
2、golang 单元测试中常用以下 4 个库方便测试代码的编写
gostub 主要用来给变量、函数、过程打桩 但是给函数打桩时,需要做侵入式修改 convey主要用途是用来组织测试用例的,提供了很多断言,兼容go test,有 web ui ,保存代码可自动跑测试 gomock主要用来给接口打桩的。 mockgen 可以生成对应的接口测试文件。 gomonkey 主要也是用来给变量、函数、方法打桩的 gostub打桩的原理式通过反射,所以要求调用gostub函数传入第一个参数必须是指针,然而函数并没有指针的概念,所以需要对函数做侵入式修改。
3、拷贝大切片一定比小切片代价大吗?
并不是,所有切片的大小相同;三个字段(一个 uintptr,两个int)。切片中的第一个字是指向切片底层数组的指针,这是切片的存储空间,第二个字段是切片的长度,第三个字段是容量。将一个 slice 变量分配给另一个变量只会复制三个机器字。所以 拷贝大切片跟小切片的代价应该是一样的。
4、知道golang的内存逃逸吗?什么情况下会发生内存逃逸?
golang程序变量会携带有一组校验数据,用来证明它的整个生命周期是否在运行时完全可知。如果变量通过了这些校验,它就可以在栈上分配。否则就说它逃逸 了,必须在堆上分配。 能引起变量逃逸到堆上的典型情况: 1)在方法内把局部变量指针返回,局部变量原本应该在栈中分配,在栈中回收。但是由于返回时被外部引用,因此其生命周期大于栈,则溢出。 2)发送指针或带有指针的值到 channel中。 在编译时,是没有办法知道哪个 goroutine 会在 channel 上接收数据。所以编译器没法知道变量什么时候才会被释放。 3) 在一个切片上存储指针或带指针的值。 一个典型的例子就是 []*string 。这会导致切片的内容逃逸。尽管其后面的数组可能是在栈上分配的,但其引用的值一定是在堆上。 slice 的背后数组被重新分配了,因为append 时可能会超出其容量( cap )。 slice初始化的地方在编译时是可以知道的,它最开始会在栈上分配。如果切片背后的存储要基于运行时的数据进行扩充,就会在堆上分配。 4)在 interface 类型上调用方法。 在 interface 类型上调用方法都是动态调度的 —— 方法的真正实现只能在运行时知道。想像一个 io.Reader 类型的变量 r , 调用 r.Read(b) 会使得 r 的值和切片b 的背后存储都逃逸掉,所以会在堆上分配。
go语言编译器会自动决定把一个变量放在栈还是放在堆,编译器会做逃逸分析(escape analysis),当发现变量的作用域没有跑出函数范围,就可以在栈上,反之则必须分配在堆。 go语言声称这样可以释放程序员关于内存的使用限制,更多的让程序员关注于程序功能逻辑本身。
5、协程同步
mutex channel waitgroup
6、go的内存管理
基于google的tcmalloc(thread-caching-malloc)实现的,常见的内存分配器还有ptmalloc、jemalloc,但是tcmalloc的性能更高,尤其是高并发场景下。 关于tcmalloc: tcmalloc的分配的内存主要来源于:全局缓存堆、进程私有缓存,小容量的内存申请使用私有缓存,如果私有缓存不够,则从全局缓存堆中申请一部分作为私有缓存。大对象会直接从全局缓存中申请,至于大小的分界是32k。 TCMalloc算法核心思想就是把内存分为多级管理,从而降低锁的粒度。它将可用的堆内存采用二级分配的方式进行管理:每个线程都会自行维护一个独立的内存池,进行内存分配时优先从该内存池中分配,当内存池不足时才会向全局内存池申请,以避免不同线程对全局内存池的频繁竞争。 内存分配器主要解决小对象的分配管理和多线程的内存分配问题。小对象的内存分配是通过一级一级的缓存来实现的,目的就是为了提升内存分配释放的速度以及避免内存碎片等问题。
7、GC什么情况下被调用?
GC调用方式 所在位置 代码 1) 定时调用 runtime/proc.go:forcegchelper() gcStart(gcTrigger{kind:gcTriggerTime, now: nanotime()}) 2)分配内存时调用 runtime/malloc.go:mallocgc() gcTrigger{kind: gcTriggerHeap} 一旦堆的大小增加了一倍,内存分配器就会触发执行 GC 。通过设置 GODEBUG=gctrace=1 ,来打印出若干循环的信息就能够证实这一点: GC 关注的第二个指标是在两次 GC 之间的时间间隔。如果超过两分钟 GC还未执行,那么就会强制启动一次 GC 循环。 3)手动调用 runtime/mgc.go:GC() gcStart(gcTrigger{kind: gcTriggerCycle, n: n + 1})
8、GC 三色标记算法
因为 GC 和我们的 Go 程序并行,GC 扫描期间内存中某些对象的状态可能被改变,所以需要一个检测这种可能的变化的方法。为了解决这个潜在的问题,实现了 写屏障[3] 算法。 GC 可以追踪到任何的指针修改。使写屏障生效的唯一条件是短暂终止程序,又名 “Stop the World”。 worker需要一种记录哪些内存需要扫描的方法。GC 使用一种 三色标记算法[4],工作流程如下: 开始时,所有对象都被认为是白色 root,接着对象(栈,堆,全局变量)被标记为灰色,这个初始步骤完成后,GC 会: 选择一个灰色的对象,标记为黑色。 追踪这个对象的所有指针,把所有引用的对象标记为灰色 然后,GC重复以上两步,直到没有对象可被标记。在这一时刻,对象非黑即白,没有灰色。白色的对象表示没有其他对象引用,可以被回收。 黑色和灰色表示的意义相同。处理的不同之处在于,标记为灰色时是把对象加入到扫描队列,而标记为黑色时,不再扫描。 GC 最终STW,清除每一次写屏障对 work pool 做的改变,继续后续的标记。
9、channel相关
1)golang channel这种方式的优点是通过提供原子的通信原语,避免了竞态情形(race condition)下复杂的锁机制。 2)channel 可能会引发 goroutine 泄漏。 泄漏的原因是 goroutine 操作 channel后,处于发送或接收阻塞状态,而 channel 处于满或空的状态,一直得不到改变。同时,垃圾回收器也不会回收此类资源,进而导致 gouroutine 会一直处于等待队列中,不见天日。 chan T // 接收和发送类型为T的数据 chan<- T // 只可以用来发送T 类型的数据 <- chan T // 只可以用来接收 T 类型的数据 3) channel应用场景? 数据交流:当作并发的 buffer 或者 queue,解决生产者 - 消费者问题。多个 goroutine 可以并发当作生产者(Producer)和消费者(Consumer)。 数据传递:一个goroutine将数据交给另一个goroutine,相当于把数据的拥有权托付出去。 信号通知:一个goroutine可以将信号(closing,closed,data ready等)传递给另一个或者另一组goroutine。 任务编排:可以让一组goroutine按照一定的顺序并发或者串行的执行,这就是编排功能。 锁机制:利用channel实现互斥机制。 4) channel 如何实现 ? 环形队列作为其缓冲区,队列的长度是创建chan时指定的 buf是有缓冲的channel所特有的结构,用来存储缓存数据。是个环形队列 创建channel实际上就是在内存中实例化了一个hchan的结构体,并返回一个ch指针,我们使用过程中channel在函数之间的传递都是用的这个指针,这就是为什么函数传递中无需使用channel的指针,而直接用channel就行了,因为channel本身就是一个指针。 hchan结构体 makechan channel初始化 chansend channel 写 chanrecv channel 读 closechan channel 关闭 新建一个 chan 后,内存在堆上分配 Golang的Channel,发送一个数据到Channel和 从Channel接收一个数据 都是 原子性的。 而且Go的设计思想就是:不要通过共享内存来通信,而是通过通信来共享内存,前者就是传统的加锁,后者就是Channel。也就是说,设计Channel的主要目的就是在多任务间传递数据的,这当然是安全的 。 5)对已经关闭的的 chan 进行读写,会怎么样?为什么? 读已经关闭的 chan 能一直读到东西,但是读到的内容根据通道内关闭前是否有元素而不同。 如果 chan 关闭前,buffer 内有元素还未读 , 会正确读到 chan 内的值,且返回的第二个 bool 值(是否读成功)为 true。 如果 chan关闭前,buffer 内有元素已经被读完,chan 内无值,接下来所有接收的值都会非阻塞直接成功,返回 channel元素的零值,但是第二个 bool 值一直为 false。 c.closed != 0 && c.qcount == 0指通道已经关闭,且缓存为空的情况下(已经读完了之前写到通道里的值) 如果接收值的地址 ep 不为空 那接收值将获得是一个该类型的零值 typedmemclr 会根据类型清理相应地址的内存 这就解释了上面代码为什么关闭的 chan 会返回对应类型的零值 6) 写已经关闭的 chan会 panic 当 c.closed != 0 则为通道关闭,此时执行写,源码chansend函数中提示直接panic,输出的内容就是上面提到的 “send on closed channel”。 7)向nil的channel中入队会一直阻塞,导致死锁。 8)从nil的channel中接收数据会一直阻塞。 9)死锁 1、golang死锁全部发生在主线程 2、死锁的原因无外乎以下2种情况: —— 主线程读取一个空的,且没有被close的通道 —— 通道缓冲不足时,主线程向没有协程消费的通道写入。 因为都是调用mallocgc方法进行内存分配,所以channel都是在堆上创建的,会进行垃圾回收,不关闭close方法也是没有问题的(但是想写出漂亮的代码就不建议你这么做了)。
10、Golang实现select
Golang实现select时,定义了一个数据结构表示每个case语句(含defaut,default实际上是一种特殊的case),select执行过程可以类比成一个函数,函数输入case数组,输出选中的case,然后程序流程转到选中的case块。 select是Golang在语言层面提供的多路IO复用的机制,其可以检测多个channel是否ready(即是否可读或可写),使用起来非常方便。 select语句中除default外,每个case操作一个channel,要么读要么写 select语句中除default外,各case执行顺序是随机的 select语句中如果没有default语句,则会阻塞等待任一case select语句中读操作要判断是否成功读取,关闭的channel也可以读取
11、go map不是并发安全,解决方法加锁
golang的map是hashmap,是使用数组+链表的形式实现的,使用拉链法消除hash冲突。 golang的map由两种重要的结构,hmap和bmap(下文中都有解释),主要就是hmap中包含一个指向bmap数组的指针,key经过hash函数之后得到一个数,这个数低位用于选择bmap(当作bmap数组指针的下表),高位用于放在bmap的[8]uint8数组中,用于快速试错。然后一个bmap可以指向下一个bmap(拉链)。
12、go性能分析
runtime/pprof:采集程序(非 Server)的运行数据进行分析 net/http/pprof:采集 HTTP Server 的运行时数据进行分析 CPU Profiling:CPU 分析,按照一定的频率采集所监听的应用程序 CPU(含寄存器)的使用情况,可确定应用程序在主动消耗 CPU 周期时花费时间的位置 Memory Profiling:内存分析,在应用程序进行堆分配时记录堆栈跟踪,用于监视当前和历史内存使用情况,以及检查内存泄漏 Block Profiling:阻塞分析,记录 goroutine 阻塞等待同步(包括定时器通道)的位置 Mutex Profiling:互斥锁分析,报告互斥锁的竞争情况
13、go 栈分配
Goroutine 初始栈大小 v1.0 ~ v1.1 — 最小栈内存空间为 4KB; v1.2 — 将最小栈内存提升到了 8KB; v1.3 — 使用连续栈替换之前版本的分段栈; v1.4 — 将最小栈内存降低到了 2KB; 分段栈(segmented stacks)是Go语言最初用来处理栈的方案。当创建一个goroutine时,Go运行时会分配一段8K字节的内存用于栈供goroutine运行使用。 连续栈(continuous stacks)可以解决分段栈中存在的两个问题,其核心原理就是每当程序的栈空间不足时,初始化一片更大的栈空间并将原栈中的所有值都迁移到新的栈中,新的局部变量或者函数调用就有了充足的内存空间。使用连续栈机制时,栈空间不足导致的扩容会经历以下几个步骤: 1.在内存空间中分配更大的栈内存空间; 2.将旧栈中的所有内容复制到新的栈中; 3.将指向旧栈对应变量的指针重新指向新栈; 4.销毁并回收旧栈的内存空间; 因为需要拷贝变量和调整指针,连续栈增加了栈扩容时的额外开销,但是通过合理栈缩容机制就能避免热分裂带来的性能问题,在 GC 期间如果 Goroutine 使用了栈内存的四分之一,那就将其内存减少一半,这样在栈内存几乎充满时也只会扩容一次,不会因为函数调用频繁扩缩容。
14、go 采坑
defer channel 一定记得close goroutine记得return或者中断,不然容易造成goroutine占用大量CPU for range v是一个临时分配出来的的内存,赋值为当前遍历的值。因此就可能会导致两个问题 对其本身没有操作引用的是同一个变量地址
15、new和make区别
go语言中的内建函数new和make是两个用于内存分配的原语(allocation primitives)。简单来说,new只分配内存,make用于slice,map,和channel的初始化。 内置函数new按指定类型长度分配零值内存,返回指针,并不关心类型内部构造和初始化方式。 内置函数make对引用类型进行创建,编译器会将make转换为目标类型专用的创建函数,以确保完成全部内存分配和相关属性初始化。 注意的点: func new(Type) *Type func make(t Type, size …IntegerType) Type 1、new(T) 返回的是 T 的指针 new(T) 为一个 T 类型新值分配空间并将此空间初始化为 T 的零值,返回的是新值的地址,也就是T 类型的指针 *T,该指针指向 T 的新分配的零值。 2、make 只能用于 slice,map,channel make 只能用于slice,map,channel 三种类型,make(T, args) 返回的是初始化之后的 T 类型的值,这个新值并不是 T 类型的零值,也不是指针 *T,是经过初始化之后的 T 的引用。
16、死锁
一、死锁的四大条件 互斥条件:一个资源每次只能被一个进程使用,即在一段时间内某资源仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待。 请求与保持条件:进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。 不可剥夺条件:进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能 由获得该资源的进程自己来释放(只能是主动释放)。 循环等待条件: 若干进程间形成首尾相接循环等待资源的关系
二、死锁具体避免方法 1)设置加锁顺序 假如在多线程中,一个线程需要锁,那么他必须按照一定得顺序获得锁。 2)设置加锁时限在获取锁的时候尝试加一个获取锁的时限,超过时限不需要再获取锁,放弃操作对锁的请求。 3)死锁检测当一个线程获取锁的时候,会在相应的数据结构中记录下来,相同下,如果有其他线程请求锁,也会在相应的结构中记录下来。当一个线程请求失败时,需要遍历一下这个数据结构检查是否有死锁产生。
17、go调度
1.G代表一个goroutine对象,每次go调用的时候,都会创建一个G对象 2.M代表一个线程,每次创建一个M的时候,都会有一个底层线程创建;所有的G任务,最终还是在M上执行 3.P代表一个处理器,每一个运行的M都必须绑定一个P,就像线程必须在么一个CPU核上执行一样 P的个数就是GOMAXPROCS(最大256),启动时固定的,一般不修改; M的个数和P的个数不一定一样多(会有休眠的M或者不需要太多的M)(最大10000);每一个P保存着本地G任务队列,也有一个全局G任务队列;
18、排序
1.稳定的排序 冒泡排序(bubble sort) — O(n2) 鸡尾酒排序 (Cocktail sort, 双向的冒泡排序) — O(n2) 插入排序 (insertion sort)— O(n2) 桶排序 (bucket sort)— O(n); 需要 O(k) 额外记忆体 归并排序 (merge sort)— O(n log n); 需要 O(n) 额外记忆体 原地归并排序 — O(n2) 二叉树排序(Binary tree sort) — O(n log n); 需要 O(n) 额外记忆体 2.不稳定的排序 选择排序 (selection sort)— O(n2) 希尔排序 (shell sort)— O(n log n) 如果使用最佳的现在版本 堆排序 (heapsort)— O(n log n) 快速排序 (quicksort)— O(n log n)期望时间, O(n2) 最坏情况; 对大的、乱数串列一般相信是最快的已知排序
19、go reflect如何调用对应的方法
通过rtype中的kind信息确定保存方法信息的偏移量。 相对于rtype起始地址,使用上面偏移量获取方法信息组。 通过方法信息中的偏移量和模块信息中记录的代码块起始地址,确定方法的地址。 reportVal := reflect.ValueOf(this) reportMethod := reportVal.MethodByName(methodName) if !reportMethod.IsValid() { return } var args []reflect.Value args = append(args, reflect.ValueOf(ctx)) reportMethod.Call(args) 便取到了函数地址。 rtype.MethodByName方法实现比较简单,它只是遍历并通过函数名匹配方法信息,然后返回 反射出来的函数使用Call方法调用。其底层就是调用上面确定的函数地址。
20、进程虚拟空间分布,全局变量放哪里
内核空间 与进程有关的数据结构段 每个进程都自己独特的PCB和页表,映射到不同的物理内存。 内核代码段所有进程的内核代码段都映射到同样的物理内存,并在内存中持续存在。 进程空间 1.正文段 存放代码和常量值(字面值常量) 2.未初始化数据段(BSS段) 存放未初始化的全局变量 3.初始化数据段(data段) 存放已经初始化的全局变量 4.堆 动态内存的分配 5.内存映射段 常被用来加载共享库。 内存映射 将虚拟内存空间与磁盘上的文件关联起来,来初始化这个虚拟内存空间的内容,这个过程叫内存映射。 共享库 1)几乎每个程序都会用到如printf之类的标准I/O函数,如果只使用静态库,这些函数的代码将会被复制到正文段中,对于一个运行上百个进程的系统来说,这是一种对内存的浪费,所以提出共享库 2)程序第一此执行时,用动态链接的方法将程序和共享库链接,减少了可执行文件的长度 6.栈 存放局部变量
21、进程和线程的主要区别
根本区别:进程是操作系统资源分配的基本单位,而线程是任务调度和执行的基本单位 在开销方面:每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。 所处环境:在操作系统中能同时运行多个进程(程序);而在同一个进程(程序)中有多个线程同时执行(通过CPU调度,在每个时间片中只有一个线程执行) 内存分配方面:系统在运行的时候会为每个进程分配不同的内存空间;而对线程而言,除了CPU外,系统不会为线程分配内存(线程所使用的资源来自其所属进程的资源),线程组之间只能共享资源。 包含关系:没有线程的进程可以看做是单线程的,如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的;线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程。
22、CAP理论的理解
CAP理论作为分布式系统的基础理论,它描述的是一个分布式系统在以下三个特性中: 一致性(Consistency)可用性(Availability) 分区容错性(Partition tolerance) (1) CA: 优先保证一致性和可用性,放弃分区容错。 这也意味着放弃系统的扩展性,系统不再是分布式的,有违设计的初衷。 (2) CP: 优先保证一致性和分区容错性,放弃可用性。在数据一致性要求比较高的场合(譬如:zookeeper,Hbase) 是比较常见的做法,一旦发生网络故障或者消息丢失,就会牺牲用户体验,等恢复之后用户才逐渐能访问。 (3) AP: 优先保证可用性和分区容错性,放弃一致性。NoSQL中的Cassandra就是这种架构。跟CP一样,放弃一致性不是说一致性就不保证了,而是逐渐的变得一致。
23、网络编程 TCP UDP
网络编程的实质就是两个(或多个)设备(例如计算机)之间的数据传输。 IP 协议只是一个地址协议,并不保证数据包的完整。如果路由器丢包(比如缓存满了,新进来的数据包就会丢失),就需要发现丢了哪一个包,以及如何重新发送这个包。这就要依靠 TCP 协议。 简单说,TCP 协议的作用是,保证数据通信的完整性和可靠性,防止丢包。 保证数据安全的方法 TCP主要提供了检验和、序列号/确认应答、超时重传、最大消息长度、滑动窗口控制等方法实现了可靠性传输。
TCP和UDP的区别及各自优缺点 区别 区别一、是否基于连接 TCP是面向连接的协议,而UDP是无连接的协议。即TCP面向连接;UDP是无连接的,即发送数据之前不需要建立连接。 区别二、可靠性 和 有序性 区别 TCP 提供交付保证(Tcp通过校验和,重传控制,序号标识,滑动窗口、确认应答实现可靠传输),无差错,不丢失,不重复,且按序到达,也保证了消息的有序性。该消息将以从服务器端发出的同样的顺序发送到客户端,尽管这些消息到网络的另一端时可能是无序的。TCP协议将会为你排好序。 UDP不提供任何有序性或序列性的保证。UDP尽最大努力交付,数据包将以任何可能的顺序到达。 TCP的逻辑通信信道是全双工的可靠信道,UDP则是不可靠信道 区别三、实时性 UDP具有较好的实时性,工作效率比TCP高,适用于对高速传输和实时性有较高的通信或广播通信。 区别四、协议首部大小 TCP首部开销20字节; UDP的首部开销小,只有8个字节 。 区别五、运行速度 TCP速度比较慢,而UDP速度比较快,因为TCP必须创建连接,以保证消息的可靠交付和有序性,毕竟TCP协议比UDP复杂。 区别六、拥塞机制 UDP没有拥塞控制,因此网络出现拥塞不会使源主机的发送速率降低(对实时应用很有用,如IP电话,实时视频会议等) 区别七、流模式(TCP)与数据报模式(UDP); TCP面向字节流,实际上是TCP把数据看成一连串无结构的字节流; UDP是面向报文的 。 区别八、资源占用 TCP对系统资源要求较多,UDP对系统资源要求较少。 TCP被认为是重量级的协议,而与之相比,UDP协议则是一个轻量级的协议。因为UDP传输的信息中不承担任何间接创造连接,保证交货或秩序的的信息。这也反映在用于承载元数据的头的大小。 区别九、应用 每一条TCP连接只能是点到点的;UDP支持一对一,一对多,多对一和多对多的交互通信 。基于UDP不需要建立连接,所以且适合多播的环境,UDP是大量使用在游戏和娱乐场所。
优缺点 基于上面的区别;TCP和UDP的优缺点也很明显了。 UDP 优点:简单、传输快。 (1)网速的提升给UDP的稳定性提供可靠网络保障,丢包率很低,如果使用应用层重传,能够确保传输的可靠性。 (2)TCP为了实现网络通信的可靠性,使用了复杂的拥塞控制算法,建立了繁琐的握手过程,由于TCP内置的系统协议栈中,极难对其进行改进。采用TCP,一旦发生丢包,TCP会将后续的包缓存起来,等前面的包重传并接收到后再继续发送,延时会越来越大,基于UDP对实时性要求较为严格的情况下,采用自定义重传机制,能够把丢包产生的延迟降到最低,尽量减少网络问题对游戏性造成影响。 缺点:不可靠,不稳定;
UDP应用场景: 1.面向数据报方式 2.网络数据大多为短消息 3.拥有大量Client 4.对数据安全性无特殊要求 5.网络负担非常重,但对响应速度要求高
TCP: 优点:可靠 稳定 TCP的可靠体现在TCP在传输数据之前,会有三次握手来建立连接,而且在数据传递时,有确认. 窗口. 重传. 拥塞控制机制,在数据传完之后,还会断开来连接用来节约系统资源。 缺点:慢,效率低,占用系统资源高,易被攻击 TCP应用场景: 当对网络质量有要求时,比如HTTP,HTTPS,FTP等传输文件的协议;POP,SMTP等邮件传输的协议。
第一次握手:建立连接时,客户端发送syn包(syn=x)到服务器,并进入SYN_SENT状态,等待服务器确认;SYN:同步序列编号(Synchronize Sequence Numbers)。 第二次握手:服务器收到syn包,必须确认客户的SYN(ack=x+1),同时自己也发送一个SYN包(syn=y),即SYN+ACK包,此时服务器进入SYN_RECV状态; 第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=y+1),此包发送完毕,客户端和服务器进入ESTABLISHED(TCP连接成功)状态,完成三次握手。 1)客户端进程发出连接释放报文,并且停止发送数据。释放数据报文首部,FIN=1,其序列号为seq=u(等于前面已经传送过来的数据的最后一个字节的序号加1),此时,客户端进入FIN-WAIT-1(终止等待1)状态。 TCP规定,FIN报文段即使不携带数据,也要消耗一个序号。 2)服务器收到连接释放报文,发出确认报文,ACK=1,ack=u+1,并且带上自己的序列号seq=v,此时,服务端就进入了CLOSE-WAIT(关闭等待)状态。TCP服务器通知高层的应用进程,客户端向服务器的方向就释放了,这时候处于半关闭状态,即客户端已经没有数据要发送了,但是服务器若发送数据,客户端依然要接受。这个状态还要持续一段时间,也就是整个CLOSE-WAIT状态持续的时间。 3)客户端收到服务器的确认请求后,此时,客户端就进入FIN-WAIT-2(终止等待2)状态,等待服务器发送连接释放报文(在这之前还需要接受服务器发送的最后的数据)。 4)服务器将最后的数据发送完毕后,就向客户端发送连接释放报文,FIN=1,ack=u+1,由于在半关闭状态,服务器很可能又发送了一些数据,假定此时的序列号为seq=w,此时,服务器就进入了LAST-ACK(最后确认)状态,等待客户端的确认。 5)客户端收到服务器的连接释放报文后,必须发出确认,ACK=1,ack=w+1,而自己的序列号是seq=u+1,此时,客户端就进入了TIME-WAIT(时间等待)状态。注意此时TCP连接还没有释放,必须经过2 MSL(最长报文段寿命)的时间后,当客户端撤销相应的TCB后,才进入CLOSED状态。 6)服务器只要收到了客户端发出的确认,立即进入CLOSED状态。同样,撤销TCB后,就结束了这次的TCP连接。可以看到,服务器结束TCP连接的时间要比客户端早一些。 【问题1】为什么连接的时候是三次握手,关闭的时候却是四次握手? 答:因为当Server端收到Client端的SYN连接请求报文后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来同步的。但是关闭连接时,当Server端收到FIN报文时,很可能并不会立即关闭SOCKET,所以只能先回复一个ACK报文,告诉Client端,“你发的FIN报文我收到了”。只有等到我Server端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送。故需要四步握手。
【问题2】为什么TIME_WAIT状态需要经过2MSL(最大报文段生存时间)才能返回到CLOSE状态? 答:虽然按道理,四个报文都发送完毕,我们可以直接进入CLOSE状态了,但是我们必须假象网络是不可靠的,有可以最后一个ACK丢失。所以TIME_WAIT状态就是用来重发可能丢失的ACK报文。在Client发送出最后的ACK回复,但该ACK可能丢失。Server如果没有收到ACK,将不断重复发送FIN片段。所以Client不能立即关闭,它必须确认Server接收到了该ACK。Client会在发送出ACK之后进入到TIME_WAIT状态。Client会设置一个计时器,等待2MSL的时间。如果在该时间内再次收到FIN,那么Client会重发ACK并再次等待2MSL。所谓的2MSL是两倍的MSL(Maximum Segment Lifetime)。MSL指一个片段在网络中最大的存活时间,2MSL就是一个发送和一个回复所需的最大时间。如果直到2MSL,Client都没有再次收到FIN,那么Client推断ACK已经被成功接收,则结束TCP连接。
【问题3】为什么不能用两次握手进行连接? 答:3次握手完成两个重要的功能,既要双方做好发送数据的准备工作(双方都知道彼此已准备好),也要允许双方就初始序列号进行协商,这个序列号在握手过程中被发送和确认。 现在把三次握手改成仅需要两次握手,死锁是可能发生的。作为例子,考虑计算机S和C之间的通信,假定C给S发送一个连接请求分组,S收到了这个分组,并发 送了确认应答分组。按照两次握手的协定,S认为连接已经成功地建立了,可以开始发送数据分组。可是,C在S的应答分组在传输中被丢失的情况下,将不知道S 是否已准备好,不知道S建立什么样的序列号,C甚至怀疑S是否收到自己的连接请求分组。在这种情况下,C认为连接还未建立成功,将忽略S发来的任何数据分 组,只等待连接确认应答分组。而S在发出的分组超时后,重复发送同样的分组。这样就形成了死锁。
【问题4】如果已经建立了连接,但是客户端突然出现故障了怎么办? TCP还设有一个保活计时器,显然,客户端如果出现故障,服务器不能一直等下去,白白浪费资源。服务器每收到一次客户端的请求后都会重新复位这个计时器,时间通常是设置为2小时,若两小时还没有收到客户端的任何数据,服务器就会发送一个探测报文段,以后每隔75秒钟发送一次。若一连发送10个探测报文仍然没反应,服务器就认为客户端出了故障,接着就关闭连接
24、在浏览器输入一个URL,按下回车之后会经过哪些流程?
DNS域名解析,得到IP地址 DNS解析流程: 1.在主机查询DNS缓存,如果没有就会给本地的DNS发送查询请求 2.本地的DNS服务器向根域名服务器发送查询请求,根域名服务器返回该域名的一级域名服务器 3.该本地服务器给该一级域名服务器发送查询请求,然后依次类推直到查询到该域名的IP地址 解析出IP地址后,根据IP地址和默认端口80和服务器建立连接 浏览器发出读取文件(URL中域名后边部分对应的文件)的HTTP请求,该请求报文作为TCP三次握手的第三个报文的数据发送给服务器 服务器对浏览器的请求作出响应,并把对应的html文本发送给浏览器 释放TCP连接(四次挥手断开连接) 浏览器解析该HTML文本并显示内容
25、线程间共享和独享的东西
线程共享的环境包括:进程代码段、进程的公有数据(利用这些共享的数据,线程很容易的实现相互之间的通讯)、进程打开的文件描述符、信号的处理器、进程的当前目录和进程用户ID与进程组ID。 线程拥有这许多共性的同时,还拥有自己的个性。有了这些个性,线程才能实现并发性。这些个性包括: 1.线程ID 每个线程都有自己的线程ID,这个ID在本进程中是唯一的。进程用此来标 识线程。 2.寄存器组的值 由于线程间是并发运行的,每个线程有自己不同的运行线索,当从一个线 程切换到另一个线程上 时,必须将原有的线程的寄存器集合的状态保存,以便 将来该线程在被重新切换到时能得以恢复。 3.线程的堆栈 堆栈是保证线程独立运行所必须的。 线程函数可以调用函数,而被调用函数中又是可以层层嵌套的,所以线程 必须拥有自己的函数堆栈, 使得函数调用可以正常执行,不受其他线程的影 响。 线程栈是在进程的堆中分配栈空间。 4.错误返回码 由于同一个进程中有很多个线程在同时运行,可能某个线程进行系统调用 后设置了errno值,而在该 线程还没有处理这个错误,另外一个线程就在此时 被调度器投入运行,这样错误值就有可能被修改。 所以,不同的线程应该拥有自己的错误返回码变量。 5.线程的信号屏蔽码 由于每个线程所感兴趣的信号不同,所以线程的信号屏蔽码应该由线程自 己管理。但所有的线程都 共享同样的信号处理器。 6.线程的优先级 由于线程需要像进程那样能够被调度,那么就必须要有可供调度使用的参 数,这个参数就是线程的 优先级。
26、go context
context主要用于父子任务之间的同步取消信号,本质上是一种协程调度的方式。另外在使用context时有两点值得注意:上游任务仅仅使用context通知下游任务不再需要,但不会直接干涉和中断下游任务的执行,由下游任务自行决定后续的处理操作,也就是说context的取消操作是无侵入的;context是线程安全的,因为context本身是不可变的(immutable),因此可以放心地在多个协程中传递使用。
27、map实现原理
HashMap的底层主要是基于数组和链表来实现的,它之所以有相当快的查询速度主要是因为它是通过计算散列码来决定存储的位置。HashMap中主要是通过key的hashCode来计算hash值的,只要hashCode相同,计算出来的hash值就一样。 冲突处理主要分两种,一种是开放定址法,另一种是链地址法。HashMap的实现中采用的是链地址法。
28、进程的三种状态及转换
运行态:进程占用CPU,并在CPU上运行; 就绪态:进程已经具备运行条件,但是CPU还没有分配过来; 阻塞态:进程因等待某件事发生而暂时不能运行; 运行—》就绪:这是有调度引起的,主要是进程占用CPU的时间过长 就绪—》运行:运行的进程的时间片用完,调度就转到就绪队列中选择合适的进程分配CPU 运行—》阻塞:发生了I/O请求或等待某件事的发生 阻塞—》就绪:进程所等待的事件发生,就进入就绪队列 以上4种情况可以相互正常转换,不是还有两种情况吗? 阻塞–》运行:即使给阻塞进程分配CPU,也无法执行,操作系统載进行调度时不会載阻塞队列进行挑选,其调度的选择对象为就绪队列: 就绪–》阻塞:因为就绪态根本就没有执行,何来进入阻塞态?
|