Vue 使用websocket + json + protobuf 与后端交互golang

栏目: IT技术 · 发布时间: 4年前

内容简介:看这篇文章的时候,千万不要害怕代码,重要的核心的都加注释了,原理很简单!!祝阅读顺利当学习一门新的语言的时候,总是喜欢先建立一个Demo项目,通过构建一个基本的项目,来了解语言的特点。对于web的交互,以前常用的技术主要是Ajax、Form表单提交这类,如果要做长连接,可以使用Websocket

看这篇文章的时候,千万不要害怕代码,重要的核心的都加注释了,原理很简单!!祝阅读顺利

当学习一门新的语言的时候,总是喜欢先建立一个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 函数如下定义,不要害怕这么长一段代码,其实核心很简单

  1. 我们首先建立结构体
// 消息回复类型,包含了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"`
}
  1. 下面是主要的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();
    }
}

只看上面注释的部分即可,其实分为两部分

  1. 按照约定的消息结构,set数据
  2. 按照约定的消息结构,get数据

注意:

  • proto.ChatResponse.deserializeBinary 是一个静态方法,不需要New
  • 一定要修改为arraybuffer,二进制数组

    ws.binaryType = 'arraybuffer';

通过上面的流程,我们已经基本了解了protobuf在websocket中的配合使用,除此之外还有一个protobuf.js 也很火,但是没有做特别的研究,比较喜欢官方的。

广播boardcast

但是这里有个细节问题,如果要建立通信,一般上来讲,我们不会直接将信息返回回去。因为websocket是全双工通信,不像http一样请求一次、返回一次、close。

如果我们要用websocket,第一个反应是长连接,搞个聊天室实时聊天。那么下面我们来实现一个聊天能力。

那么聊天能力,其实有一个特点就是广播,一个人说话,所有人都能收到。这样特别有意思,很久以前用ajax来做的话,还需要把数据存起来,然后每次再轮询读取输出给前端。现在都不用存到数据库里了。

核心:

  1. 广播实际上就是把消息广播给所有与服务器建立连接的客户端

看下实现

先建立一个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的聊天能力来说有如下的流程

  1. 双方要实现websocket通信
  2. 约定proto消息协议,生成go和js两个版本
  3. 使用goroutine协程,来进行广播

其中上面的例子,并没有在client退出的时候,从clients中将链接删除,实际上可以用下面的形式,来删除,并且conn.close关闭连接。

delete(clients,cli)

希望上面的内容,对大家有所帮助。详细问题可以留言讨论


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Text Processing in Python

Text Processing in Python

David Mertz / Addison-Wesley Professional / 2003-6-12 / USD 54.99

Text Processing in Python describes techniques for manipulation of text using the Python programming language. At the broadest level, text processing is simply taking textual information and doing som......一起来看看 《Text Processing in Python》 这本书的介绍吧!

随机密码生成器
随机密码生成器

多种字符组合密码

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具