长连接断开的原因
- 连接超时,浏览器自动断开连接
- 进程被杀死
- 不可抗拒因素
根据不同情况,高效保活的方式
- 连接超时:心跳机制
- 进程保活
- 断线重连
重点心跳机制
- 产物
- 心跳包
- 心跳应答
轮询与心跳区别
- 轮询一次相当于:建立一次TCP连接+断开连接
- 心跳:在已有的连接上进行保活
心跳设计要点
- 心跳包的规格(内容&大小)
- 心跳发送间隔时间(按照项目的特性进行判断)
- 断线重连机制(核心= 如何判断长连接的有效性)
心跳具体实现(基于sse的长连接)
-
客户端做心跳机制:客户端长时间没有反应,使用心跳机制,证明客户端的存在
-
服务端做心跳机制:服务端长时间没有反应,使用心跳机制,证明服务端还存在
-
服务端做心跳机制
思考点:
- 如何判断连接中断信号(单独的思考,在本次的代码中,没有用于跟心跳机制有关,以后有想法,会补上)
notify := w.(http.CloseNotifier).CloseNotify()
// log.Println("notify:",<- notify) 会直接堵住的,因为notify它接收连接中断信号
go func(){
// 太迷了,正确想法就是:只能接收异常的信号,就是网络中断的信号
fmt.Println("接收连接中断信号")
<-notify
userData[r.RemoteAddr] = r.RemoteAddr
offUser <- r.RemoteAddr
log.Println(r.RemoteAddr,"just close")
}()
- 如何将一一对应的客户端和服务端保存
// 接收发送给客户端数据
type RW struct{
Rw http.ResponseWriter
T time.Time
}
var rw = make(map[int64]*RW)
// 考虑使用map。记得当正确的数据发送给客户端之后要将对应的map键值删除
delete(rw,a) // 当发送完之后,就要将这个客户端删除了。a时键值
- 利用golang中的time.Ticker机制,监听是否有服务端等待,然后进行轮询保活。心跳机制重点(利用协程进行监听)
// 保活,心跳
go func(){
defer func(){
if err := recover();err!=nil{
fmt.Println(err)
}
}()
fmt.Println("开启保活")
keepAliveInterval := time.Duration(6000)
fmt.Println(keepAliveInterval)
ticker := time.NewTicker(3*time.Second)
for {
select{
case <-ticker.C:
fmt.Println("保活,心跳机制")
t1 := time.Now()
for _,value:= range rw{
fmt.Println(value)
if t1.Sub(value.T)>keepAliveInterval{
fmt.Println("进入保活")
f,ok:=value.Rw.(http.Flusher)
if !ok{
fmt.Fprintf(value.Rw,"不能用来做sse")
return
}
fmt.Fprintf(value.Rw,"data:请耐心等待,我正在努力的加载数据\n\n")
f.Flush()
}
}
}
}
}()
样例代码
server.go
package main
import(
"fmt"
"log"
"time"
"sync"
"net/http"
)
// 接收发送给客户端数据
type RW struct{
Rw http.ResponseWriter
T time.Time
}
var offUser = make(chan string,0)
var userData = make(map[string]string)
var rw = make(map[int64]*RW)
var i int64 = 0
var lock sync.Mutex
func init(){
log.SetFlags(log.Ltime|log.Lshortfile)
}
func sseService(w http.ResponseWriter,r *http.Request){
var a int64 // 用来接收key值
defer func(){
if err := recover();err!=nil{
fmt.Println(err)
}
}()
lock.Lock()
i++
a=i
lock.Unlock()
// 提取get请求参数
fmt.Println("a =",a)
f,ok := w.(http.Flusher)
if !ok{
http.Error(w,"cannot support sse",http.StatusInternalServerError)
return
}
// 用于监听客户端时候已经断开了连接
notify := w.(http.CloseNotifier).CloseNotify()
// log.Println("notify:",<- notify) 会直接堵住的,因为notify它接收网络中断信号
go func(){
fmt.Println("接收关闭信号")
<-notify
offUser <- r.RemoteAddr
log.Println(r.RemoteAddr,"just close")
}()
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")
w.Header().Set("Transfer-Encoding", "chunked")
w.Header().Set("Access-Control-Allow-Origin","*")
fmt.Fprintf(w,"data:welcome\n\n")
f.Flush()
// 将当前的w保存
fmt.Println("心跳")
t := time.Now()
rr := &RW{Rw:w,T:t}
fmt.Println("rr =",rr)
rw[a] = rr
// 模拟服务端接收发送数据阻塞
fmt.Println("模拟服务端发送数据阻塞")
time.Sleep(time.Second*30)
fmt.Fprintf(w,"data:12345加油\n\n")
f.Flush()
delete(rw,a) // 当发送完之后,就要将这个客户端删除了
}
func testClose(w http.ResponseWriter,r *http.Request){
fmt.Println("remoteAddr:",r.RemoteAddr)
fmt.Println("userData:",userData)
// 用于监听客户端时候已经断开了连接
notify := w.(http.CloseNotifier).CloseNotify()
go func(){
fmt.Println("接收连接中断信号")
<-notify
userData[r.RemoteAddr] = r.RemoteAddr
offUser <- r.RemoteAddr
log.Println(r.RemoteAddr,"just close")
}()
time.Sleep(time.Second*1)
fmt.Fprintln(w,"这里任意数字")
}
func main(){
fmt.Println("sse1")
// 获取中断的客户端
go func(){
fmt.Println("监听关闭的客户端")
for{
select{
case user:=<-offUser:
log.Println("userOff:",user)
}
}
}()
// 保活,心跳
go func(){
defer func(){
if err := recover();err!=nil{
fmt.Println(err)
}
}()
fmt.Println("开启保活")
keepAliveInterval := time.Duration(6000)
fmt.Println(keepAliveInterval)
ticker := time.NewTicker(3*time.Second)
for {
select{
case <-ticker.C:
fmt.Println("保活,心跳机制")
t1 := time.Now()
for _,value:= range rw{
fmt.Println(value)
if t1.Sub(value.T)>keepAliveInterval{
fmt.Println("进入保活")
f,ok:=value.Rw.(http.Flusher)
if !ok{
fmt.Fprintf(value.Rw,"不能用来做sse")
return
}
fmt.Fprintf(value.Rw,"data:请耐心等待,我正在努力的加载数据\n\n")
f.Flush()
}
}
}
}
}()
http.HandleFunc("/sse",sseService)
http.HandleFunc("/testClose",testClose)
http.ListenAndServe(":8080",nil)
}
client(angular)
sse(){
let that = this
if ("EventSource" in window){
console.log("可以使用EventSource")
}else{
return
}
var url = "http://localhost:8080/sse?pid="+12345
var es = new EventSource(url)
// 监听事件
// 连接事件
es.onopen = function(e:any){
console.log("我进来啦")
console.log(e)
}
// message事件
es.onmessage = function(e){
that.Data = e.data
if (e.data=="12345加油"){ // 后端通知前端结束发送信息
console.log("12345加油,这是服务端正确想发送的数据")
es.close()
}else{
console.log(e.data)
}
}
es.addEventListener("error",(e:any)=>{
// 这里的e要声明变量,否则回报没有readyState属性
console.log("e.target",e.target)
console.log("SSEERROR:",e.target.readyState)
if(e.target.readyState == 0){
// 重连
console.log("Reconnecting...")
es.close() // 不开启服务端,直接关闭
}
if(e.target.readyState==2){
// 放弃
console.log("give up.")
}
},false);
}
学习心跳机制附带的知识点
angular设置轮询
- setInterval()方法重复调用一个函数或执行一个代码段,在每次调用之间具有固定的时间延迟
- clearInterval()删除重复调用
myTest = setInterval(()=>{
var i:number = 1
console.log("轮询还是心跳")
if(i===4){
return
}
i++
},1500) // 一旦实例化,就会直接运行
test(){
clearInterval(this.myTest) // 清除重复运行函数
}
time.Duration
- Duration的基本单位是纳秒
- 作用:打印时间时,根据最合适的时间单位打印;用于时间比较
keepAliveInterval := time.Duration(3)
// 打印数据值
3ns
time.NewTicker
- 创建一个轮询机制,规定隔一段时间处理一次函数
ticker := time.NewTicker(500 * time.Millisecond)
done := make(chan bool)
go func(){
for{
select{
case <-done:
return
case t := <-ticker.C: // 500微秒轮询一次
fmt.Println("Tick at",t)
}
}
}()
time.Sleep(10*time.Second)
ticker.Stop()
done<-true
fmt.Println("ticker stopper")
总结
- 学到一招:对于有是接口的方法:直接去看相对应实现的源代码