一、首先,需要了解一下websocket基本原理:here
二、go语言的websocket实现:
基于go语言的websocket也有不少,比如github.com/gorilla/websocket。这里选用的应该算是官方的实现code.google.com/p/go.net/websocket
使用go get安装下载即可。(不过,由于周知的原因:(,我是通过golangtc.com的第三方包下载功能才下载来的)
三、server端
第一个遇到的问题,websocket如何和martini集成?
安装websocket的文档,和http服务集成,应该使用如下方式
func ChatService (ws *websocket.Conn) { for{
io.Copy(ws,ws);
}
}
http.Handle("/echo", websocket.Handler(EchoServer))
但是,如果注册到martini的route,运行时会报错
m.Any("/chat", websocket.Handler(ChatService))
经阅读Server.go代码,发现,需要使用websocket.Handler的方法ServeHTTP来注册(ps:因为websoket.Handler是个函数签名的自定义类型,所以,我们把ChatService转为websocket.Handler之后,就拥有了它的方法)
m.Any("/chat", websocket.Handler(ChatService).ServeHTTP)
服务端代码,基于某些原因,这里贴上部分代码,其余请大家根据框架自己很容易实现:
From string `json:"from"`
To string `json:"to"`
At string `json:"at"`
Type string `json:"type"`
Success bool `json:"success"`
Msg string `json:"msg"`
Data interface{} `json:"data"`
}
var connections map[*websocket.Conn]*chatClient
var msgQueue *list.List
var locker, lockQueue *sync.RWMutex
var activeClient int64 =
var chanNewClient chan *websocket.Conn func init() {
connections = make(map[*websocket.Conn]*chatClient)
msgQueue = list.New()
locker = &sync.RWMutex{}
lockQueue = &sync.RWMutex{}
chanNewClient = make(chan *websocket.Conn, )
go msgMonitor()
}
func ChatService(ws *websocket.Conn) {
var err error
var user string
var hasUser bool
var session *sessions.Session
var srvMsg *chatMsg
defer func() {
if ex := recover(); ex != nil {
glog.Errorf("[ChatService]get session panic: %v ,\nstack trace:%v", err, debug.Stack())
}
}()
defer deleteClient(ws)
for {
if session, err = session_store.Get(ws.Request(), SESSION_NAME); err != nil {
glog.Errorf("[ChatService]get session error: %v", err)
return
}
hasUser = false
if iuser, has := session.Values["user"]; has {
user = iuser.(string)
hasUser = true
} if hasUser {
registerClient(ws, user)
}
if ptMsg, good, err := readClient(ws); err != nil {
return
} else if good {
if !hasUser && ptMsg.Type != "signin" {
srvMsg = &chatMsg{From: "server", Success: false, Msg: "signin first please!", Type: "needsignin"}
if err := sendClient(ws, srvMsg); err != nil {
return
}
}
switch ptMsg.Type {
case "":
fallthrough
case "msg":
if ptMsg.Msg != "" {
if err := relayMsg(ws, ptMsg); err != nil {
return
}
}
case "listuser":
users := getUsersList()
if jsdata, err := json.Marshal(users); err != nil {
srvMsg := &chatMsg{From: "server", Type: "listuser", Success: false, Msg: "get users error:" + err.Error()}
if err := sendClient(ws, srvMsg); err != nil {
return
}
} else {
srvMsg := &chatMsg{From: "server", Type: "listuser", Success: true, Data: string(jsdata)}
if err := sendClient(ws, srvMsg); err != nil {
return
}
}
}
} } //end for
}
几点说明:
1如果读写数据遇到错误,甭犹豫,关掉连接;
2活动连接自己记录;
3实现多些的功能,必要定个通讯协议;
4websocket的错误只有一个类型,但是我发现有一个错误内容是”not implemented“的错误(好像来自firefox),可以安全的忽略;
上面第四点,应该是websocket服务缺少什么协议的实现,但是我没看出来,下面是调试信息,有知者望不吝赐教:
2015/01/14 13:43:46 glog.go:169 [[info] [[ChatService] *websocket.ProtocolError when read socket. Request:
Head: [map[Sec-Websocket-Key:[GH09xUWLdTOVB0u3RJdOgA==] User-Agent:[Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; WOW64; Trident/6.0)] Cache-Control:[no-cache] Cookie:[my_session=MTQyMTIxNDE2MXxRdXBlSjlVeTR4SG9TVTFSYUJkN1FyMW1GU0c2U05XdjBEc0F0NXZSQnZSbE1BaVdKem51aE44TktZdWNaaUtfaVZBVmVONE9JY1JUcFU0MVliVkFxR3BnUi1LQ2F0RV82b1FxUE9jMXlIV3NGdlhfbWZiLTBSLTQySHFKNmJlVVZJSWlScmpGZXZLWmZuXzc4aXM5UEZQY2ZlRzF8CISa785vyuDilrrpQTg4IKLyiH-U12yGai4ah-ixbV8=] Origin:[http://192.168.5.92:8088] Connection:[Upgrade] Upgrade:[Websocket] Sec-Websocket-Version:[13] Dnt:[1]]
Body:&struct { http.eofReaderWithWriteTo; io.Closer }{eofReaderWithWriteTo:http.eofReaderWithWriteTo{}, Closer:ioutil.nopCloser{Reader:io.Reader(nil)}}]]]
四、基于网页的client端实现:(客户端浏览器当然需要支持websocket协议,当前最新的浏览器基本都支持了,IE需要10以上)
<!doctype html>
<html>
<head>
<title>websocket</title>
<style>
#log{height: 300px;overflow-y: scroll;border: 1px solid #CCC;padding:0;}
#signinpad{display:none;background-color:#99F}
.r-msg{color:#111;}
.s-msg{color:#090;}
.msg-from{font-family:fangsong;}
.chat-msg{padding-left:15px;}
</style>
<script type="text/javascript" src="/js/easyui-1.4.1/jquery.min.js" ></script>
<script type="text/javascript">
var sock = null;
var wsuri = "ws://192.168.5.92:8088/chat";
$(document).on("ready",function() {
console.log("onload");
jQuery.fn.flash = function( size, duration )
{
try{
var current = this.css( 'border-width' );
if(current=="")current="0px"
current = parseInt(current)
this.animate( { 'borderWidth': 5 }, duration / 2 );
this.animate( { 'borderWidth': current }, duration / 2 );
}catch(ex){
console.log(ex)
}
}
$("#message").on("keydown",function(evt){if(evt.keyCode==13){send();}})
try{
conn();
}catch(ex){
console.log(ex);
alert("连接websocket务器报错:"+ex);
$("#btnsend").attr("disabled","disabled");
$("#message").attr("disabled","disabled");
$("#btngetusers").attr("disabled","disabled");
}
});
function conn(){ sock = new WebSocket(wsuri);
regevt();
}
function regevt(){
sock.onopen = function() {
console.log("connected to " + wsuri);
loaduser();
}
sock.onerror = function(e) {
console.log(" error from connect " + e);
}
sock.onclose = function(e) {
console.log("connection closed (" + e.code + ")");
console.log("reconnecting....");
setTimeout( conn,1000);
}
sock.onmessage = onMsg;
}
function onMsg(e) {
console.log("message received: " + e.data);
var msg = window.JSON.parse(e.data);
if (msg.type=="" ||msg.type=="msg"){
appendMsg(msg.from,msg.msg);
}else if (msg.type=="needsignin"){
alert("signin first,please!");
$("#signinpad").css("display","block");
$('#btnsignin').removeAttr("disabled")
$('#user').focus();
$('#btnsignin').on("click",function(){
var user = $('#user').val();
var pass = $('#password').val();
if( user==""){
alert("user name cannot be blank!")
$('#user').focus();return
}
if( pass==""){
alert("password cannot be blank!")
$('#password').focus();return;
}
appendMsg("self","signining");
$('#btnsignin').attr("disabled","disabled")
// 这里的url需要替换为你自己web应用的地址,服务需要返回json
$.ajax({url:"/api/usr/signin",method:"post",data:{name:user,password:pass},dataType:"json"
,success:function(data){
console.log("receive data's type=",typeof(data)," data:",data);
if (data.success) {
onSok();
}else{
onSerr(data.msg)
}
}
,error:function(R,err){
onSerr(err)
}
})
});
}else if(msg.type=="listuser"){
if(msg.success){
var list = $("#userlist");
var ind=0;
var ccc = list[0].length-1;
while (ind<ccc){
$("#userlist").get(0).remove(1);
ind++;
}
var users = window.JSON.parse(msg.data)
for (ind=0;ind<users.length;ind++){
list.append("<option value=\""+users[ind]+"\">"+users[ind]+"</option>")
}
}else{
appendMsg(msg.from,msg.msg);
}
}else if(msg.type=="signin"){
if(msg.success){
onSok()
}else{
onSerr(msg.msg);
}
} }
function onSok(){
$("#signinpad").css("display","none");
appendMsg("self","signin ok,ready to send message");
$("#message").focus();
document.location.href = document.location.href
sock.send('{"type":"listuser","msg":""}')
return;
}
function onSerr(err){
appendMsg("self",err)
appendMsg("self","signin again,please!")
$('#btnsignin').removeAttr("disabled")
$('#user').focus();
return;
}
function loaduser(){
sock.send('{"type":"listuser","msg":""}')
}
function send() {
var msg = $('#message').val();
if (msg.length==0){
$("#error").text("enter somthing first!")
$("#error").flash(5,1000)
$('#message').on("keydown",function(){$("#error").text("");$('#message').off("keydown");})
return;
}else{
$("#error").text("")
}
$('#message').val('');
var to = $("#userlist").val();
appendMsg("myself",msg)
msgObj={"msg":msg,"type":"msg","to":to};
jss=window.JSON.stringify(msgObj);
sock.send(jss);
console.log("I sent:",jss)
} var lastMsg=""
function appendMsg(from,msg){
msgPad = $('#log');
if (msg==lastMsg && from=="myself"){
p=msgPad.find("p:last-child")
//alert(p[0].outerHTML);
p.flash(5,500)
return;
}
lastMsg = msg;
str="";
if (from=="myself" || from=="self" || from=="me" ||from=="I"){
from="self"
str ='<p class="s-msg"><i class="msg-from"> self ';
}else{
str ='<p class="r-msg"><i class="msg-from">'+from+' ';
}
str = str+ '('+new Date().toLocaleString()+') :</i> <br/><span class="chat-msg">'+msg+'</span></p>'
msgPad.append(str);
msgPad.get(0).scrollTop = $('#log').get(0).scrollHeight;
}
function cls(){
msgPad = $('#log');
msgPad.empty();
}
</script>
</head>
<body>
<h1> 即时网上聊天WebSocket </h1>
<div id="log">
</div>
<div>
<table >
<tr id="signinpad" ><td><label for="user">user:</label></td>
<td><input id="user" name="user" type="text""></input></td>
<td><label for="password">pass:</label></td>
<td><input id="password" name="password" type="password"></input></td>
<td><input id="btnsignin" name="btnsignin" type="button" value="登录"></input></td>
</tr>
<tr><td><label from="userlist">to who:</label></td>
<td>
<select id="userlist" style="length:200px;">
<option value="all">所有人</option>
</select>
</td>
<td><button id="btngetusers" onclick="loaduser()">rload users</button>
<td><button id="btncls" onclick="cls()">clear</button>
</tr> </table>
<p>
Message: <input id="message" type="text" value="Hello, world!" ><button id="btnsend" onclick="send();">Send Message</button>
</p>
<div id="error" style="color:red;"></div>
</div>
</body>
</html>
说明:其中,用户登录需要通过form提交到web后台的登录服务,我试着通过websocket自身服务实现登录,没有搞定保存session,不过如果聊天功能绑定在web网站上的话,应该也不需要单独登录功能。