看这篇文章的时候,千万不要害怕代码,重要的核心的都加注释了,原理很简单!!祝阅读顺利
当学习一门新的语言的时候,总是喜欢先建立一个Demo项目,通过构建一个基本的项目,来了解语言的特点。
对于web的交互,以前常用的技术主要是Ajax、Form表单提交这类,如果要做长连接,可以使用Websocket
关于websocket和socket其实是两个东西,如果要比较的话,应该是拿websocket和http 来比较。
websocket 发送json
websocket发送json这是一种常规的方式
值得一提的是,Vue框架中使用axios发送POST请求的时候,默认Content-Type是application/json,所以在后端接受的时候,要做流处理。
比如像PHP的话,要用php://input
,如果是go,那么就要使用下面的代码,来获取请求body的全部内容,然后使用json.Unmarshal
来解析
func Receive(w http.ResponseWriter, r *http.Request) {
b, _ := ioutil.ReadAll(r.Body)
}
我们继续来看Vue的websocket部分
export default {
data () {
return {
username:'',
email:'',
content:'',
message_list:[],
ws:null
}
},
methods: {
handleRecv:function(data) {
var jsonData = JSON.parse(data)
this.message_list.unshift(jsonData.data)
},
wsOpen: function () {
var that = this
var ws = new WebSocket("ws://localhost:9000/ws")
ws.onopen = function () {
console.info("ws open")
}
ws.onmessage = function (evt) {
that.handleRecv(evt.data)
}
ws.onclose = function () {
console.info("ws close")
}
this.ws = ws
},
wsSend: function() {
if(this.ws == null) {
console.info("连接尚未打开")
}
this.ws.send(JSON.stringify({
email:this.email,
content:this.content
}))
}
},
mounted(){
this.wsOpen();
}
}
上面这段代码,是定义在Vue的组件中的。其实核心就是通过mounted
组件挂载完成之后,来调用new WebSocket
创建连接,并且注册onOpen
,onMessage
,onClose
事件。
通过websocket来发送json,实际上是传递了一个json的字符串,对于基于golang的后端,我们同样需要搭建websocket 服务端来接收消息。
golang的websocket服务
websocket是在http协议上进行的升级。
这里我们使用的是websocket包
"github.com/gorilla/websocket"
对于main函数,我们构建如下
// 定义了flag参数
var addr = flag.String("addr",":9000","http service address")
var upgrader = websocket.Upgrader{}
func main() {
flag.Parse()
http.HandleFunc("/ws",echo)
err := http.ListenAndServe(*addr, nil)
if err != nil {
log.Fatalf("%v", err)
}
}
echo 函数如下定义,不要害怕这么长一段代码,其实核心很简单
- 我们首先建立结构体
// 消息回复类型,包含了code和data主体
type MessageRet struct {
Code int `json:"code"`
Data interface{} `json:"data"`
}
// card包含了接受参数,存储json解析后的对象
type Card struct {
Email string `json:"email"`
Content string `json:"content"`
}
- 下面是主要的echo函数,完成的就是升级http,监听某个链接的请求,并且进行回复
这里面用到的方法主要是
- upgrader.CheckOrigin 解决跨域问题
- upgrader.Upgrade()
- c.ReadMessage()
- c.WriteMessage()
func echo(w http.ResponseWriter, r *http.Request) {
// 跨域
upgrader.CheckOrigin = func(r *http.Request) bool {
return true
}
// 升级http
c, err := upgrader.Upgrade(w,r,nil)
if err != nil {
log.Fatalf("%v",err)
}
defer c.Close()
// 结构体指针
var card = &Card{}
for {
// 阻塞,等待读取消息
mt, message, err := c.ReadMessage()
if err !=nil {
log.Println("read",nil)
break
}
// 解析参数
err = json.Unmarshal(message, card)
if err != nil {
log.Fatalf("json parse error %v",err)
break
}
// 这里可以自定义handle处理
card.Email = "server - "+card.Email
card.Content = "server - "+card.Content
// 重新打包,返回~~~~
var ret = MessageRet{Code:http.StatusOK,Data:*card}
b, _ := json.Marshal(ret)
log.Printf("recv : %s", b)
err = c.WriteMessage(mt, b)
if err != nil {
log.Fatalf("write",nil)
break
}
}
}
注意上面c.WriteMessage(mt,b)
的第一个参数是MessageType类型,有两个值分别是代表 二进制、文本
websocket结合protobuf
在上面的例子当中,我们看到通过websocket来建立长连接,并且与go的websocket服务进行通信
使用websocket来避免轮询已经是一个减轻服务器请求压力的办法了,那么我们能否在传输协议上在做一些改变,来减小传输的包体大小。
使用protobuf
关于protobuf,是一种编码协议,可以想象一下json、xml。
protoc是proto文件的编译器,proto-gen-go是protoc的插件,可以根据proto文件,编译成go文件。
google-protobuf 现在也支持了生成 js文件。
用protobuf的还有一个好处是指,如果我在go的服务端,定义好了Request的包体内容,以及Response的包体内容,那么生成go文件后,也可以同时生成js文件
这样双方就可以按照同样的参数模式来进行开发,就等于proto文件,相当于接口文档了
那我们先生成一个proto文件,比如说websocket要发请求,goserver要返回内容,那就涉及了两个消息结构的定义
syntax = "proto3";
// 请求
message ChatRequest {
string email = 1;
string content = 2;
}
// 响应
message ChatResponse {
int32 code = 1;
ChatRequest data = 2;
}
然后使用protoc来生成文件go/js
protoc --go_out=plugins=grpc:. *.proto
protoc --js_out=import_style=commonjs,binary:. *.proto
proto go文件的使用
先引入go的proto文件
import (
pb "message_board_api/proto/chat"
)
然后再代码中获取到websocket的消息后,进行proto解析,非常简单~~~~
// 使用protobuf解析
pbr := &pb.ChatRequest{}
err = proto.Unmarshal(message, pbr)
if err != nil {
log.Fatalf("proto 解析失败 %v", err)
break
}
proto js文件使用
proto的js文件,需要配合google-protobuf.js
一起使用,根据官网文档来讲,如果要在浏览器中使用,需要用browserify进行打包。
在Vue的组件中,引入包
import "google-protobuf"
import proto from "../proto/chat_pb.js"
下面我们来看websocket结合protobuf,与传统的json有什么不同,同样不要害怕这么一大段代码,我们主要看method部分
export default {
data () {
return {
username:'',
email:'',
content:'',
message_list:[
{
email:'',
content:'',
}
],
ws:null
}
},
methods: {
// 处理protobuf内容
handleRecv:function(data) {
// 这里对接收到的二进制消息进行解码
var rep = proto.ChatResponse.deserializeBinary(data)
// 可以获取data和code
console.info(rep.getData())
console.info(rep.getCode())
// 这里拼接消息,message_list是vue的一个列表,不要关心
this.message_list.unshift({email:rep.getData().getEmail(),content:rep.getData().getContent()})
},
wsOpen: function () {
var that = this
var ws = new WebSocket("ws://localhost:9000/ws")
// 这个地方特别重要,websocket默认是Uint8array
ws.binaryType = 'arraybuffer';
ws.onopen = function () {
console.info("ws open")
}
ws.onmessage = function (evt) {
console.info(evt)
console.info("Received message:"+evt.data)
that.handleRecv(evt.data)
}
ws.onclose = function () {
console.info("ws close")
}
this.ws = ws
},
wsSend: function() {
if(this.ws == null) {
console.info("连接尚未打开")
}
// 发送消息同样很简单,我们不需要关心格式
var chat = new proto.ChatRequest()
chat.setEmail(this.email)
chat.setContent(this.content)
this.ws.send(chat.serializeBinary())
}
},
mounted(){
this.wsOpen();
}
}
只看上面注释的部分即可,其实分为两部分
- 按照约定的消息结构,set数据
- 按照约定的消息结构,get数据
注意:
proto.ChatResponse.deserializeBinary
是一个静态方法,不需要New一定要修改为arraybuffer,二进制数组
ws.binaryType = 'arraybuffer';
通过上面的流程,我们已经基本了解了protobuf在websocket中的配合使用,除此之外还有一个protobuf.js 也很火,但是没有做特别的研究,比较喜欢官方的。
广播boardcast
但是这里有个细节问题,如果要建立通信,一般上来讲,我们不会直接将信息返回回去。因为websocket是全双工通信,不像http一样请求一次、返回一次、close。
如果我们要用websocket,第一个反应是长连接,搞个聊天室实时聊天。那么下面我们来实现一个聊天能力。
那么聊天能力,其实有一个特点就是广播,一个人说话,所有人都能收到。这样特别有意思,很久以前用ajax来做的话,还需要把数据存起来,然后每次再轮询读取输出给前端。现在都不用存到数据库里了。
核心:
- 广播实际上就是把消息广播给所有与服务器建立连接的客户端
看下实现
先建立一个map,用来储存客户端连接,在建立个消息缓冲通道
// 客户端集合
var clients = make(map[*websocket.Conn]string)
// 消息缓冲通道
var messages = make(chan *pb.ChatRequest, 100)
每次新建连接之后,都会将链接保存到client当中,进行缓存。
c, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Fatalf("%v", err)
}
defer c.Close()
// 将链接的地址写入到clients中
who := c.RemoteAddr().String()
log.Println(who)
clients[c] = who
proto解析的部分已经讲过了,这里将解析的内容写入到messages通道里面。
// 使用protobuf解析
pbr := &pb.ChatRequest{}
err = proto.Unmarshal(message, pbr)
if err != nil {
log.Fatalf("proto 解析失败 %v", err)
break
}
// 解析后的内容写入messages 进行广播
pbr.Email = pbr.Email + "<" + who + ">"
messages <- pbr
下面就是核心的boardcast方法
func boardcast() {
// 始终读取messages
for msg := range messages {
// 读取到之后进行广播,启动协程,是为了立即处理下一条msg
go func() {
for cli := range clients {
// protobuf协议
pbrp := &pb.ChatResponse{Code: http.StatusOK, Data: &pb.ChatRequest{Email: msg.Email, Content: msg.Content}}
b, err := proto.Marshal(pbrp)
if err != nil {
log.Fatalf("proto marshal error %v", err)
}
// 二进制发送
cli.WriteMessage(websocket.BinaryMessage, b)
}
}()
}
}
上面的boardcast方法,要交给协程goroutine处理,不然for range messages 会阻塞,所以在main方法中使用协程
func main() {
flag.Parse()
http.HandleFunc("/ws", echo)
// 广播
go boardcast()
// pprof 这是一个状态监控,可以忽略,有空也可以研究下
go func() {
log.Println(http.ListenAndServe("localhost:6060", nil))
}()
// 这里的ListenAndServe 已经开启了goroutine协程了
err := http.ListenAndServe(*addr, nil)
if err != nil {
log.Fatalf("%v", err)
}
}
小结一下
对于创建websocket和protobuf的聊天能力来说有如下的流程
- 双方要实现websocket通信
- 约定proto消息协议,生成go和js两个版本
- 使用goroutine协程,来进行广播
其中上面的例子,并没有在client退出的时候,从clients中将链接删除,实际上可以用下面的形式,来删除,并且conn.close关闭连接。
delete(clients,cli)