鉴于聊天已然成为大部分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&param1=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 就这样完成了 可喜可贺

有事儿寻这儿

http://weibo.com/SandCu

 

posted @ 2015-12-16 15:54 SandCu 阅读(3606) 评论(0) 推荐(2) 编辑
摘要: 转眼间不做wp开发,投身于php事业已然一年了,转身看到8.1的发布,俨然一片欣欣向荣的景象,但是开发社区却没比一年前有过多大的提高,这并不是一个好现象,遂在git上开源了之前音频处理库,希望能对社区有所贡献,地址如下:https://github.com/sandcu/wpaudio 觉得有用的同... 阅读全文
posted @ 2014-05-09 17:03 SandCu 阅读(1147) 评论(7) 推荐(1) 编辑
摘要: wp系统中默认的给出的是WAV格式的音频,当然无论是用来存储还是与网络交互都显得过大了,不过在p7中只能是用c#进行处理,所以通常会将其保存为Amr格式比如QQ又或者是使用Speex进行编码然后打包成OGG, 鉴于Wp8中开放了Native Code,所以我们可以考虑性价比更高的格式如AAC或者MP3格式。 阅读全文
posted @ 2013-01-25 18:12 SandCu 阅读(3974) 评论(8) 推荐(5) 编辑
摘要: 在移动开发中,根据业务模式的不同主要分为两个阵营,即游戏阵营和应用阵营,在windows phone中也不例外,由于开发模式和技巧有比较明显的不同,所以做应用的人并不能很轻易的转到游戏开发上,同理做游戏的亲们也需要很多努力才能做出好的应用,如果不使用游戏引擎,诸如按钮或者列表等控件写起来会比较麻烦。应用开发中,整个系统的运行机制可以说主要是基于事件的被动通知,而在游戏开发中,运行机制则是基于主动查询的,以手指按下按钮举例,在应用开发中,首先注册按下事件,那么当用户在注册事件的按钮上上按下就会收到按下事件,而回调函数则可以根据相关的EventArgs获取参数从而进行处理,而在游戏开发中,则是在绘 阅读全文
posted @ 2012-10-15 14:28 SandCu 阅读(1390) 评论(5) 推荐(1) 编辑
摘要: 由于这一段发版忙得蛋疼,没时间去总结近一段的收获了,慢慢补上吧,撸妹920上市之后Windows phone又再次为人们所提及,然后这次升级对开发者的影响可能远比用户大,由于诺基亚的新机已经在发布会上展示了一部分wp8的功能,那在这里就针对开发者来展示一下新的sdk功能吧:1.主界面,新增了儿童模式,第一次进入儿童模式会进行一次设置,游戏默认不能玩。一些如market的图标等也都有变化。2.貌似有些bug,地下会一直存在一个显示你输入的框3短信界面,UI上有一定修改4人脉的更改5消息里直接集成了msn,喵的这是犯规啊;不给留饭吃啊;情何以堪啊;告你垄断啊;用排比句增强语气啊!6:market现 阅读全文
posted @ 2012-09-11 14:48 SandCu 阅读(1819) 评论(10) 推荐(3) 编辑
摘要: 本来计划中是没有此番外篇的,但是序章里大家的评论让我意识到,鉴于微软历来对开发者的“冷酷无情”,以及WP8中种种新特性的欲语还休,让很多持观望态度的开发者们很迷惑,此番外篇的目的是根据微软提供的合作伙伴SDK文档来给大家解释一下目前常提出的几个困惑,鉴于各种协议和规定,请大家不要向我索要文档,如果有疑问,请在尽量留言中提问,或者联系QQ:624709714,但是由于文档比较初期,很多内容也是草草带过,我会尽我所能回答大家,如有变动,请以微软正式公布的SDK documentation为准。1.WP8和WP7的内核不一样么? 是的,WP7采用的是CE的内核,WP8采用的是NT内核。2.WP8的分 阅读全文
posted @ 2012-06-25 01:15 SandCu 阅读(2522) 评论(9) 推荐(7) 编辑
摘要: 首先要阐述一点,MVVM并不是一个设计框架,而是一种设计模式,是MVP的一种进化,而MVVM的实现条件之一是UI与数据可以完美的分离,所以说,这种设计模式是为所有采用XAML进行UI制作,并用绑定的方式进行UI与后台数据的交互的软件量身定制的。譬如Silverlight,譬如WPF,而我们通常所说的MVVM框架指的则是galasoft的MVVMlight框架,地址如下所示,该框架支持windows phone 7.0与7.1 。http://mvvmlight.codeplex.com/然而,这里并不推荐大家使用该框架,虽然该框架支持Message,Command等诸多特性,并对设计器提供了良 阅读全文
posted @ 2012-06-24 16:04 SandCu 阅读(332) 评论(2) 推荐(1) 编辑
摘要: 本系列博文主要是讲博主从Windows Phone 7.0时期开始在工作中遇到的各类问题以及解决方案,本系列假设读者已经具有了一定的windows phone编程基础,文中所描述的很多解决方案并不是最优的,但是是可行的,如果读者有更好的方案请不吝赐教,博主在此先行致谢。以下是暂定的前几篇章的目录番外篇WP8与WP7WP8模拟器正篇1.MVVM设计模式与WP7通用框架。2.页面基类与导航(1)。3.页面基类与导航(2)。4.可定制的高效ListBox。5.单一页面内的Gif高效方法。6.wp7与Win8与Wp87.用做游戏的思想优化应用(1)8.用做游戏的思想优化应用(2)联系方式:QQ:624 阅读全文
posted @ 2012-06-24 14:28 SandCu 阅读(1250) 评论(6) 推荐(3) 编辑
点击右上角即可分享
微信分享提示