本文目录一览:
- golang中怎么处理socket长连接
- go语言实现一个简单的简单网关
- 【golang详解】go语言GMP(GPM)原理和调度
- golang如何构造http.ResponseWriter和http.Request
- [go 中怎么使用httptest测试http请求](#go 中怎么使用httptest测试http请求)
golang中怎么处理socket长连接
Socket通信的原理还是比较简单的,它大致分为以下几个步骤。服务器端的步骤如下。
- 建立服务器端的Socket,开始侦听整个网络中的连接请求。
- 当检测到来自客户端的连接请求时,向客户端发送收到连接请求的信息,并建立与客户端之间的连接。
go语言实现一个简单的简单网关
网关=反向代理+负载均衡+各种策略,技术实现也有多种多样,有基于 nginx 使用 lua 的实现,比如 openresty、kong;也有基于 zuul 的通用网关;还有就是 golang 的网关,比如 tyk。
这篇文章主要是讲如何基于 golang 实现一个简单的网关。
转自: troy.wang/docs/golang/posts/golang-gateway/
整理:go语言钟文文档:
启动两个后端 web 服务(代码)
这里使用命令行工具进行测试
具体代码
直接使用基础库 httputil 提供的 NewSingleHostReverseProxy
即可,返回的 reverseProxy 对象实现了 serveHttp 方法,因此可以直接作为 handler。
具体代码
director 中定义回调函数,入参为 *http.Request
,决定如何构造向后端的请求,比如 host 是否向后传递,是否进行 url 重写,对于 header 的处理,后端 target 的选择等,都可以在这里完成。
director 在这里具体做了:
modifyResponse
中定义回调函数,入参为 *http.Response
,用于修改响应的信息,比如响应的 Body,响应的 Header 等信息。
最终依旧是返回一个 ReverseProxy,然后将这个对象作为 handler 传入即可。
参考 2.2 中的 NewSingleHostReverseProxy
,只需要实现一个类似的、支持多 targets 的方法即可,具体实现见后面。
作为一个网关服务,在上面 2.3 的基础上,需要支持必要的负载均衡策略,比如:
随便 random 一个整数作为索引,然后取对应的地址即可,实现比较简单。
具体代码
使用 curIndex 进行累加计数,一旦超过 rss 数组的长度,则重置。
具体代码
轮询带权重,如果使用计数递减的方式,如果权重是 5,1,1 那么后端 rs 依次为 a,a,a,a,a,b,c,a,a,a,a…,其中 a 后端会瞬间压力过大;参考 nginx 内部的加权轮询,或者应该称之为平滑加权轮询,思路是:
后端真实节点包含三个权重:
操作步骤:
具体代码
一致性 hash 算法,主要是用于分布式 cache 热点/命中问题;这里用于基于某 key 的 hash 值,路由到固定后端,但是只能是基本满足流量绑定,一旦后端目标节点故障,会自动平移到环上最近的那么个节点。
实现:
具体代码
每一种不同的负载均衡算法,只需要实现添加以及获取的接口即可。
然后使用工厂方法,根据传入的参数,决定使用哪种负载均衡策略。
具体代码
作为网关,中间件必不可少,这类包括请求响应的模式,一般称作洋葱模式,每一层都是中间件,一层层进去,然后一层层出来。
中间件的实现一般有两种,一种是使用数组,然后配合 index 计数;一种是链式调用。
具体代码
【golang详解】go语言GMP(GPM)原理和调度
Goroutine 调度是一个很复杂的机制,下面尝试用简单的语言描述一下 Goroutine 调度机制,想要对其有更深入的了解可以去研读一下源码。 首先介绍一下 GMP 什么意思:
- G ----------- goroutine: 即 Go 协程,每个 go 关键字都会创建一个协程。
- M ---------- thread 内核级线程,所有的 G 都要放在 M 上才能运行。
- P ----------- processor 处理器,调度 G 到 M 上,其维护了一个队列,存储了所有需要它来调度的 G。 Goroutine 调度器 P 和 OS 调度器是通过 M 结合起来的,每个 M 都代表了 1 个内核线程,OS 调度器负责把内核线程分配到 CPU 的核上执行。 模型图: 避免频繁的创建、销毁线程,而是对线程的复用。
- work stealing 机制
当本线程无可运行的 G 时,尝试从其他线程绑定的 P 偷取 G,而不是销毁线程。 - hand off 机制
当本线程 M0 因为 G0 进行系统调用阻塞时,线程释放绑定的 P,把 P 转移给其他空闲的线程执行。进而某个空闲的 M1 获取 P,继续执行 P 队列中剩下的 G。而 M0 由于陷入系统调用而进被阻塞,M1 接替 M0 的工作,只要 P 不空闲,就可以保证充分利用 CPU。M1 的来源有可能是 M 的缓存池,也可能是新建的。当 G0 系统调用结束后,根据 M0 是否能获取到 P,将会将 G0 做不同的处理:- 如果有空闲的 P,则获取一个 P,继续执行 G0。
- 如果没有空闲的 P,则将 G0 放入全局队列,等待被其他的 P 调度。然后 M0 将进入缓存池睡眠。 如下图: GOMAXPROCS 设置 P 的数量,最多有 GOMAXPROCS 个线程分布在多个 CPU 上同时运行。 在 Go 中一个 goroutine 最多占用 CPU 10ms,防止其他 goroutine 被饿死。 具体可以去看另一篇文章: 【Golang详解】go语言调度机制 抢占式调度 当创建一个新的 G 之后优先加入本地队列,如果本地队列满了,会将本地队列的 G 移动到全局队列里面,当 M 执行 work stealing 从其他 P 偷不到 G 时,它可以从全局 G 队列获取 G。
协程经历过程
我们创建一个协程 go func()
经历过程如下图:
说明:
这里有两个存储 G 的队列,一个是局部调度器 P 的本地队列、一个是全局 G 队列。新创建的 G 会先保存在 P 的本地队列中,如果 P 的本地队列已经满了就会保存在全局的队列中;处理器本地队列是一个使用数组构成的环形链表,它最多可以存储 256 个待执行任务。
G 只能运行在 M 中,一个 M 必须持有一个 P,M 与 P 是 1:1 的关系。M 会从 P 的本地队列弹出一个可执行状态的 G 来执行,如果 P 的本地队列为空,就会想其他的 MP 组合偷取一个可执行的 G 来执行;
一个 M 调度 G 执行的过程是一个循环机制;会一直从本地队列或全局队列中获取 G。
上面说到 P 的个数默认等于 CPU 核数,每个 M 必须持有一个 P 才可以执行 G,一般情况下 M 的个数会略大于 P 的个数,这多出来的 M 将会在 G 产生系统调用时发挥作用。类似线程池,Go 也提供一个 M 的池子,需要时从池子中获取,用完放回池子,不够用时就再创建一个。
work-stealing 调度算法:当 M 执行完了当前 P 的本地队列里的所有 G 后,P 也不会就这么在那躺尸啥都不干,它会先尝试从全局队列寻找 G 来执行,如果全局队列为空,它会随机挑选另外一个 P,从它的队列里中拿走一半的 G 到自己的队列中执行。
如果一切正常,调度器会以上述的那种方式顺畅地运行,但这个世界没这么美好,总有意外发生,以下分析 goroutine 在两种例外情况下的行为。
Go runtime 会在下面的 goroutine 被阻塞的情况下运行另外一个 goroutine:
用户态阻塞/唤醒
当 goroutine 因为 channel 操作或者 network I/O 而阻塞时(实际上 golang 已经用 netpoller 实现了 goroutine 网络 I/O 阻塞不会导致 M 被阻塞,仅阻塞 G,这里仅仅是举个例子),对应的 G 会被放置到某个 wait 队列(如 channel 的 waitq),该 G 的状态由 _Gruning
变为 _Gwaitting
,而 M 会跳过该 G 尝试获取并执行下一个 G,如果此时没有可运行的 G 供 M 运行,那么 M 将解绑 P,并进入 sleep 状态;当阻塞的 G 被另一端的 G2 唤醒时(比如 channel 的可读/写通知),G 被标记为,尝试加入 G2 所在 P 的 runnext(runnext 是线程下一个需要执行的 Goroutine。),然后再是 P 的本地队列和全局队列。
系统调用阻塞
当 M 执行某一个 G 时候如果发生了阻塞操作,M 会阻塞,如果当前有一些 G 在执行,调度器会把这个线程 M 从 P 中摘除,然后再创建一个新的操作系统的线程(如果有空闲的线程可用就复用空闲线程)来服务于这个 P。当 M 系统调用结束时候,这个 G 会尝试获取一个空闲的 P 执行,并放入到这个 P 的本地队列。如果获取不到 P,那么这个线程 M 变成休眠状态,加入到空闲线程中,然后这个 G 会被放入全局队列中。
队列轮转
可见每个 P 维护着一个包含 G 的队列,不考虑 G 进入系统调用或 IO 操作的情况下,P 周期性的将 G 调度到 M 中执行,执行一小段时间,将上下文保存下来,然后将 G 放到队列尾部,然后从队列中重新取出一个 G 进行调度。 除了每个 P 维护的 G 队列以外,还有一个全局的队列,每个 P 会周期性地查看全局队列中是否有 G 待运行并将其调度到 M 中执行,全局队列中 G 的来源,主要有从系统调用中恢复的 G。之所以 P 会周期性地查看全局队列,也是为了防止全局队列中的 G 被饿死。
M0
M0 是启动程序后的编号为 0 的主线程,这个 M 对应的实例会在全局变量 runtime.m0
中,不需要在 heap 上分配,M0 负责执行初始化操作和启动第一个 G,在之后 M0 就和其他的 M 一样了。
G0
G0 是每次启动一个 M 都会第一个创建的 goroutine,G0 仅用于负责调度 G,G0 不指向任何可执行的函数,每个 M 都会有一个自己的 G0,在调度或系统调用时会使用 G0 的栈空间,全局变量的 G0 是 M0 的 G0。
一个 G 由于调度被中断,此后如何恢复?
中断的时候将寄存器里的栈信息,保存到自己的 G 对象里面。当再次轮到自己执行时,将自己保存的栈信息复制到寄存器里面,这样就接着上次之后运行了。 我这里只是根据自己的理解进行了简单的介绍,想要详细了解有关 GMP 的底层原理可以去看 Go 调度器 G-P-M 模型的设计者的文档或直接看源码。 参考:
golang如何构造http.ResponseWriter和http.Request
在做测试的时候,需要模拟 HTTP server 的 handle 函数直接调用:
就不用通过发送 curl 命令,而是直接调用 handler 函数的方式;这样就需要手动构造出一个 http.ResponseWriter
和 http.Request
,然后调用 Handler 函数。
好在 golang 自带的 net/http/httptest
包就有这个功能:
如果使用 github.com/gorilla/mux
的 router 包想使用 Vars 可以这么设置:
然后在 Handler 函数里,就能使用:
go 中怎么使用httptest测试http请求
优雅的 Golang Web 开发框架:Martini。Martini 是一个非常新的 Go 语言的 Web 框架,使用 Go 的 net/http
接口开发,类似 Sinatra 或者 Flask 之类的框架,你可以使用自己的 DB 层、会话管理和模板。
特性:
- 使用非常简单
- 无侵入设计