GIN+GORILLA=A GOLANG WEBSOCKET SERVER
鉴于聊天已然成为大部分app的基础功能,而大部分app用户基数有没有辣么大,常用的聊天server架构如xmpp或者消息队列实现之类的用起来还挺麻烦的,有比较难跟网页端做交互,加之H5标准落地,所以websocket已然成为一个轻巧可用性高的聊天server实现方法;
websocket的server常见的是用nodejs或者java的netty框架实现,netty相对重一点,direct buffer的内存泄露调起来比较麻烦,试了一下go,轻巧,稳定性不错,性能不错,所以用go实现了一下;
websocket的协议标准和基本概念网上一搜一片,这里不赘述;
http server用gin来做,websocket的handler则用gorilla,由于不重复造轮子,所以整个搭建的过程很快;
import (
"util"
"os"
"fmt"
"github.com/DeanThompson/ginpprof"
"github.com/gin-gonic/gin"
"runtime"
)
var (
logger * util.LogHelper
)
func main() {
runtime.GOMAXPROCS(runtime.NumCPU())
logFile,err := os.OpenFile("/var/log/gows.log",os.O_CREATE|os.O_RDWR,0777)
if err!=nil {
fmt.Println(err.Error())
os.Exit(0)
}
defer logFile.Close()
logger = util.NewLogger(logFile)
logger.Info("Starting system...")
wsHandler := new(WebSocketHandler)
gin.SetMode(gin.ReleaseMode)
r := gin.Default()
r.GET("/", func(c *gin.Context) {
wsHandler.HandleConn(c.Writer, c.Request)
})
ginpprof.Wrapper(r)//调试用 可以看到堆栈状态和所有goroutine状态
//err = r.Run(listenPath, certPath, keyPath) 这样可以支持wss
err = r.Run("127.0.0.1:8888")
if err != nil {
fmt.Println(err)
}
}
这样我们的入口就有了~
websocket的模式大概是 onopen onmessage onerror onclose四个callback来覆盖整个通信流程
所以我们来看下简易版本的websockethandler的实现
package main import (
"bytes"
"compress/gzip"
"encoding/json"
"errors"
"net/http"
"strconv"
"time"
"util"
"github.com/gorilla/websocket"
) var (
ctxHashMap = util.NewConcurrentMap()
)
//用来升级http协议到ws协议
type WebSocketHandler struct {
wsupgrader websocket.Upgrader
} func (wsh *WebSocketHandler) NewWebSocketHandler() {
wsh.wsupgrader = websocket.Upgrader{
ReadBufferSize: 4096,
WriteBufferSize: 4096,
}
} func (wsh *WebSocketHandler) onMessage(conn *websocket.Conn, ctx *ConnContext, msg []byte, msgType int) {
//处理文本消息 或者 2进制消息 2进制通常是些 gzip的文本 语音或者图片视频之类的一般会用其他云服务不然带宽会爆掉
if msgType == websocket.TextMessage {
wsh.processIncomingTextMsg(conn, ctx, msg)
}
if msgType == websocket.BinaryMessage { }
} func (wsh *WebSocketHandler) onOpen(conn *websocket.Conn, r *http.Request) (ctx *ConnContext, err error) {
if err := r.ParseForm(); err != nil {
return nil, errors.New("参数校验错误")
}
specialKey := r.FormValue("specialKey")
supportGzip := r.FormValue("support_gzip") ctx = &ConnContext{specialKey, supportGzip}
//用来标识一个tcp链接
keyString := ctx.AsHashKey() if oldConn, ok := ctxHashMap.Get(keyString); ok {
wsh.onClose(oldConn.(*websocket.Conn), ctx)
oldConn.(*websocket.Conn).Close()
}
ctxHashMap.Set(keyString, conn)
return ctx, nil
} func (wsh *WebSocketHandler) onClose(conn *websocket.Conn, ctx *ConnContext) {
logger.Info("client close itself as " + ctx.String())
wsh.closeConnWithCtx(ctx)
return
} func (wsh *WebSocketHandler) onError(errMsg string) {
logger.Error(errMsg)
}
func (wsh *WebSocketHandler) HandleConn(w http.ResponseWriter, r *http.Request) {
wsh.wsupgrader.CheckOrigin = func(r *http.Request) bool { return true }
conn, err := wsh.wsupgrader.Upgrade(w, r, nil)
if err != nil {
logger.Error("Failed to set websocket upgrade: " + err.Error())
return
}
defer conn.Close()
if ctx, err := wsh.onOpen(conn, r); err != nil {
logger.Error("Open connection failed " + err.Error() + r.URL.RawQuery)
return
} else {
conn.SetPingHandler(func(message string) error {
conn.WriteControl(websocket.PongMessage, []byte(message), time.Now().Add(time.Second))
return nil
})
for {
t, msg, err := conn.ReadMessage()
if err != nil {
logger.Error("READ ERR FROM " + ctx.String() + " ERR " + err.Error())
wsh.onClose(conn, ctx)
return
} switch t {
case websocket.TextMessage, websocket.BinaryMessage:
wsh.onMessage(conn, ctx, msg, t)
case websocket.CloseMessage:
wsh.onClose(conn, ctx)
return
case websocket.PingMessage:
case websocket.PongMessage:
} }
}
} func (wsh *WebSocketHandler) closeConnWithCtx(ctx *ConnContext) {
keyString := ctx.AsHashKey()
ctxHashMap.Remove(keyString)
return
}
func (wsh *WebSocketHandler) processIncomingTextMsg(conn *websocket.Conn, ctx *ConnContext, msg []byte) {
logger.Debug("CLIENT SAID " + string(msg))
sendMessageToAll(msg)
} func (wsh *WebSocketHandler) sendMessageToAll(msg []byte]) {
var gzMsg bytes.Buffer
gzWriter := gzip.NewWriter(&gzMsg)
gzWriter.Write(msg)
gzWriter.Flush()
gzWriter.Close()
for key, conn := range ctxHashMap.Items() {
if ctx, err := HashKeyAsCtx(key.(string)); err != nil {
wsh.onError(err.Error())
} else {
if ctx.supportGzip == "1" {
err = conn.(*websocket.Conn).WriteMessage(websocket.BinaryMessage, gzMsg.Bytes())
logger.Debug("send binary msg to " + ctx.String())
} else {
err = conn.(*websocket.Conn).WriteMessage(websocket.TextMessage, []byte(msg))
logger.Debug("send text msg to " + ctx.String())
}
if err != nil {
wsh.onClose(conn.(*websocket.Conn), ctx)
conn.(*websocket.Conn).Close()
wsh.onError("WRITE ERR TO " + key.(string) + " ERR:" + err.Error())
}
}
} }
因为删了一些线上代码的敏感信息 所以未必编译的过,不过差不多一个意思,主要看气质
里面的一个莫名其妙的叫做ctx的东西出现了很多次其实是connectionContext的缩写,一般链接形如ws://ip:port/?param=value¶m1=value1之类的形式,当然会加密,所以在onopen的时候会对url做一次基础校验,并且回记录url的一些关键参数标记,以用来确认消息到底要发送给谁
一个简单connContext实现如下
// connContext.go
package main import (
"errors"
"strings"
"util"
) type ConnContext struct {
specialKey string
supportGzip string
}
func HashKeyAsCtx(hashKey string) (*ConnContext,error){
values := strings.Split(hashKey,":")
if(len(values)!=2){
return nil,errors.New("艾玛 key不对: "+hashKey)
}else{
return &ConnContext{values[0],values[1]},nil
}
}
func (ctx *ConnContext) AsHashKey() string{
return strings.Join([]string{ctx.specialKey, ctx.supportGzip},":")
}
func (ctx * ConnContext) String () string{
return util.NewStringBuilder("specialkey: ",ctx.specialkey, " gzip ",ctx.supportGzip).String()
}
以上 一个简易的websocket server 就这样完成了 可喜可贺
有事儿寻这儿
GIN+GORILLA=A GOLANG WEBSOCKET SERVER的更多相关文章
- 使用Jetty搭建Java Websocket Server,实现图像传输
https://my.oschina.net/yushulx/blog/298140 How to Implement a Java WebSocket Server for Image Transm ...
- SpringBoot报错:Failed to load ApplicationContext(javax.websocket.server.ServerContainer not available)
引起条件: WebSocket+单元测试,单元测试报错! 解决方法: SpringBootTest增加webEnvironment参数. https://docs.spring.io/spring-b ...
- Golang websocket推送
Golang websocket推送 在工作用主要使用的是Java,也做过IM(后端用的netty websocket).最近想通过Golang重写下,于是通过websocket撸了一个聊天室. 项目 ...
- io.undertow.websockets.jsr.ServerWebSocketContainer cannot be cast to org.apache.tomcat.websocket.server.WsServerContainer
Caused by: java.lang.ClassCastException: io.undertow.websockets.jsr.ServerWebSocketContainer cannot ...
- springboot整合websocket后运行测试类报错:javax.websocket.server.ServerContainer not available
springboot项目添加websocket依赖后运行测试类报如下错误: org.springframework.beans.factory.BeanCreationException: Error ...
- ClassCastException: org.apache.tomcat.websocket.server.WsServerContainer cannot be cast to javax.websocket.server.ServerContainer
21:09:22.221 [MessageBroker-3] INFO c.t.s.s.impl.StockNewsServiceImpl - [2017-12-16 21:09:22] execut ...
- Caused by: java.lang.IllegalStateException: javax.websocket.server.ServerContainer not available
java.lang.IllegalStateException: Failed to load ApplicationContext at org.springframework.test.co ...
- swoole之建立 websocket server
一.代码部分 <?php /** * 为什么用WebSocket? * HTTP的通信只能由客户端发起 * * WebSocket 协议是基于TCP的一种新的网络协议.实现了浏览器与服务器全双工 ...
- springboot整合websocket后打包报错:javax.websocket.server.ServerContainer not available
项目整合了websocket以后,打包多次都没有成功,原来是报错了,报错内容如下: Error starting ApplicationContext. To display the conditio ...
随机推荐
- 常用工具篇(二)死链接扫描工具–Xenu
一个网站上线一段时间之后,可能出现很多的死链接,死链接就是那些打不开的链接,或者是请求是404的,可能是因为有的文件位置移动了,或者有的功能不好使了,可能会影响我们网的功能,我们就要隔一阵扫描一下网站 ...
- Codeforces 600E Lomsat gelral (树上启发式合并)
题目链接 Lomsat gelral 占坑……等深入理解了再来补题解…… #include <bits/stdc++.h> using namespace std; #define rep ...
- Zab算法详解
Zookeeper使用了一种称为Zab(Zookeeper Atomic Broadcast)的协议作为其一致性复制的核心,据其作者说这是一种新发算法,其特点是充分考虑了Yahoo的具体情况:高吞吐量 ...
- MongoDb 出现配置服务不同步的处理
主要片方法就是用正常的配置文件的数据覆盖有问题的就行. 引用: http://dba.stackexchange.com/questions/48232/mongodb-config-servers- ...
- GCD CoreData 简化CoreData操作(转)
来自会员带睡帽的青蛙的分享: 短话长说,开始写这个小工具到现在有两个月了,虽然东西少,但是很精练,改了又改,期间有不少问题 在坛子里获得了不少帮助 谢谢各位大大. 就是两个文件一个类 CoreData ...
- mac 下配置 protobuf golang插件 并使用
介绍 Google Protocol Buffer( 简称 Protobuf) 是 Google 公司内部的混合语言数据标准Protocol Buffers 是一种轻便高效的结构化数据存储格式 可以用 ...
- 基于HTML5的PACS--HTML5图像处理
在此之前,此系统是结合DICOM的WADO标准,在浏览器里通过javascript操作返回的JPG图片.这种服务器端解析,客户端展现的方式,对实现图像的移动.缩放.旋转.测量等图像操作能够实现实时的交 ...
- List<InvestInfoDO> invest = advertiseDao6.qryInvestInfo(InvestInfoDO1);怎样获得list的实体类;
List<InvestInfoDO> invest = advertiseDao6.qryInvestInfo(InvestInfoDO1); 怎样获得List的实体类呢,就是怎样获得I ...
- 百科知识 .e,.ec文件如何打开
1 .e是易语言源文件,你可以从以下网址下载e语言编程环境: http://www.xiazaiba.com/html/409.html 2 安装之后会自动关联.e文件. 3 打开一个e语言文 ...
- PS 如何制作眼泪效果
1.用钢笔工具勾出眼泪的路径然后按Ctrl + Enter转为选区 2.按Ctrl + J 把选区复制出来,执行滤镜 > 扭曲 > 球面化 同样的方法制作流出的眼泪,然后添加图层样式选择投 ...