长连接断开的原因

  • 连接超时,浏览器自动断开连接
  • 进程被杀死
  • 不可抗拒因素

根据不同情况,高效保活的方式

  • 连接超时:心跳机制
  • 进程保活
  • 断线重连

重点心跳机制

  • 产物
    • 心跳包
    • 心跳应答

轮询与心跳区别

  • 轮询一次相当于:建立一次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")

总结

  • 学到一招:对于有是接口的方法:直接去看相对应实现的源代码