看这篇文章的时候,千万不要惧怕代码,重要的核心的都加注释了,原理很简单!!祝阅读顺利javascript
当学习一门新的语言的时候,老是喜欢先创建一个Demo项目,经过构建一个基本的项目,来了解语言的特色。
对于web的交互,之前经常使用的技术主要是Ajax、Form表单提交这类,若是要作长链接,可使用Websocketphp
关于websocket和socket实际上是两个东西,若是要比较的话,应该是拿websocket和http 来比较。前端
websocket发送json这是一种常规的方式vue
值得一提的是,Vue框架中使用axios发送POST请求的时候,默认Content-Type是application/json,因此在后端接受的时候,要作流处理。java
好比像PHP的话,要用php://input
,若是是go,那么就要使用下面的代码,来获取请求body的所有内容,而后使用json.Unmarshal
来解析ios
func Receive(w http.ResponseWriter, r *http.Request) { b, _ := ioutil.ReadAll(r.Body) }
咱们继续来看Vue的websocket部分git
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
事件。github
经过websocket来发送json,其实是传递了一个json的字符串,对于基于golang的后端,咱们一样须要搭建websocket 服务端来接收消息。golang
websocket是在http协议上进行的升级。web
这里咱们使用的是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"` }
这里面用到的方法主要是
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来创建长链接,而且与go的websocket服务进行通讯
使用websocket来避免轮询已是一个减轻服务器请求压力的办法了,那么咱们可否在传输协议上在作一些改变,来减少传输的包体大小。
关于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
先引入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文件,须要配合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(); } }
只看上面注释的部分便可,其实分为两部分
注意:
proto.ChatResponse.deserializeBinary
是一个静态方法,不须要New必定要修改成arraybuffer,二进制数组
ws.binaryType = 'arraybuffer';
经过上面的流程,咱们已经基本了解了protobuf在websocket中的配合使用,除此以外还有一个protobuf.js 也很火,可是没有作特别的研究,比较喜欢官方的。
可是这里有个细节问题,若是要创建通讯,通常上来说,咱们不会直接将信息返回回去。由于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的聊天能力来讲有以下的流程
其中上面的例子,并无在client退出的时候,从clients中将连接删除,实际上能够用下面的形式,来删除,而且conn.close关闭链接。
delete(clients,cli)
但愿上面的内容,对你们有所帮助。详细问题能够留言讨论