简易Java web在线聊天-websocket

来源:互联网 发布:浙江软件行业协会 编辑:程序博客网 时间:2024/05/20 15:41

公司做了伪在线客服系统。自己为了技术,提前研究了使用websocket进行长连接通信。写了个简单的在线聊天demo,算是对自己的交代,后期会抽时间优化,完善流程等。
语言:Java
客户端:html5 实现的socket作为客户端。
服务器:使用spring-mvc + spring-websocket+spring-mesaging作为信息接收处理。
测试容器:jetty9+

1.【websocket是什么】

WebSocket协议是基于TCP的一种新的网络协议。它实现了浏览器与服务器全双工(full-duplex)通信——允许服务器主动发送信息给客户端。WebSocket通信协议于2011年被IETF定为标准RFC 6455,并被RFC7936所补充规范。
在实现websocket连线过程中,需要通过浏览器发出websocket连线请求,然后服务器发出回应,这个过程通常称为“握手” 。在 WebSocket API,浏览器和服务器只需要做一个握手的动作,然后,浏览器和服务器之间就形成了一条快速通道。两者之间就直接可以数据互相传送。在此WebSocket 协议中,为我们实现即时服务带来了两大好处:
1. Header 互相沟通的Header是很小的-大概只有 2 Bytes
2. Server Push 服务器的推送,服务器不再被动的接收到浏览器的请求之后才返回数据,而是在有新数据时就主动推送给浏览器。
------来源百度百科

2.【websocket API简单介绍】

var ws = new WebSocket("ws://www.websocket.org",[protocol]);
说明:WebSocket的构造函数需要一个URL参数和一个可选的协议参数(一个或者多个协议的名字),协议的参数例如XMPP(Extensible Messaging and Presence Protocol)、SOAP(Simple Object Access Protocol)或者自定义协议。而URL参数需要以WS://或者WSS://开头,例如:ws://www.websocket.org。
在第一次握手之后,和协议的名称一起,客户端会发送一个Sec-WebSocket-Protocol 头,服务端会选择0个或一个协议,响应会带上同样的Sec-WebSocket-Protocol 头,否则会关闭连接。通过协议协商(Protocol negotiation ),我们可以知道给定的WebSocket服务器所支持的协议和版本,然后应用选择协议使用。

2.1 【websocket事件】

WebSocket API是纯事件驱动,通过监听事件可以处理到来的数据和改变的链接状态。客户端不需要为了更新数据而轮训服务器。服务端发送数据后,消息和事件会异步到达。
WebSocket编程遵循一个异步编程模型,只需要对WebSocket对象增加回调函数就可以监听事件。你也可以使用addEventListener()方法来监听。而一个WebSocket对象分四类不同事件。

Open事件:一旦服务器响应连接,就会触发open事件。响应的回调函数为onopen。
// Event handler for the WebSocket connection openingws.onopen = function(e) {   console.log("Connection open...");};
Message事件:open事件后,意味着协议握手结束,服务器端同意客户端连接,进做好通信准备。发送消息触发的回调为:onmessage。
ws.onmessage = function(e) {   if(typeof e.data === "string"){      console.log("String message received", e, e.data);   } else {      console.log("Other message received", e, e.data);   }};
Error事件:如果发生意外的失败则会触发error事件,error事件会导致连接关闭,很快就会收到关闭事件回调。意外失败的回调为:onerror。
ws.onerror = function(e) {   console.log("WebSocket Error: " , e);   //Custom function for handling errors   handleErrors(e);};
close事件:当连接关闭的时候,触发改事件。对应的回调方法为:onclose。
ws.onclose = function(e) {   console.log("Connection closed", e);};

2.2:【websocket方法】

send方法:当onopen触发后,连接正常。则可以使用send方法向服务器发送消息, send方法还可以发送二进制文本。如:ws.send("hello world");
close方法:关闭websocket连接。如果连接已经关闭,则该方法什么也不做。

2.3 【websocket属性】

属性值状态WebSocket.CONNECTING0连接正在进行,但还没有建立WebSocket.OPEN1连接已经建立,可以发送消息。WebSocket.CLOSING2连接正在进行关闭握手WebSocket.CLOSED3连接已经关闭或不能打开

3 【Demo示例】

demo模拟多人在线聊天,当用户选中要发送信息的在线用户,则可进行在线聊天。


3.1主要依赖关系
<!-- spring dependency start --><dependency>  <groupId>org.springframework</groupId>  <artifactId>spring-context</artifactId>  <version>${spring-version}</version></dependency><dependency>  <groupId>org.springframework</groupId>  <artifactId>spring-webmvc</artifactId>  <version>${spring-version}</version></dependency><dependency>  <groupId>org.springframework</groupId>  <artifactId>spring-context-support</artifactId>  <version>${spring-version}</version></dependency><dependency>  <groupId>org.springframework</groupId>  <artifactId>spring-websocket</artifactId>  <version>${spring-version}</version></dependency><dependency>  <groupId>org.springframework</groupId>  <artifactId>spring-messaging</artifactId>  <version>${spring-version}</version></dependency><!-- spring dependency end --><dependency>  <groupId>com.alibaba</groupId>  <artifactId>fastjson</artifactId>  <version>1.2.20</version></dependency>

3.2 【消息处理类】

简单的消息处理,继承spring-websocket,文本消息处理类,实现文本消息处理。并缓存当前在线用户,当连接失败、发送消息失败,简单认为用户离线删除session。未处理socket session 连接超时、用户长时间无通信容器session超时、页面刷新重连等。
package cc.hu.test.websocket.skthandler;import java.io.IOException;import java.util.Date;import java.util.HashMap;import java.util.Iterator;import java.util.Map;import java.util.Map.Entry;import org.springframework.stereotype.Component;import org.springframework.web.socket.CloseStatus;import org.springframework.web.socket.TextMessage;import org.springframework.web.socket.WebSocketSession;import org.springframework.web.socket.handler.AbstractWebSocketHandler;import com.alibaba.fastjson.JSON;import cc.hu.test.websocket.common.User;import cc.hu.test.websocket.controller.MainController;/** * @author root * */@Componentpublic class MyTextWebSocketHandler extends AbstractWebSocketHandler {public static Map<Long, WebSocketSession> userWebsocketSessionMap;static {userWebsocketSessionMap = new HashMap<Long, WebSocketSession>();}@Overridepublic void afterConnectionEstablished(WebSocketSession session) throws Exception {Long uid = (Long) session.getAttributes().get("uid");userWebsocketSessionMap.put(uid, session);User u = MainController.loginUserMap.get(uid);CCMsg welcome = new CCMsg();welcome.setTo(uid);welcome.setMsg("[系统消息]欢迎[" + u.getNickName() + "]进入聊天");welcome.setMsgType(MsgType.CHAT_MSG);welcome.setFromName("系统");welcome.setSendDate(new Date());welcome.setFrom(0L);session.sendMessage(new TextMessage(JSON.toJSONString(welcome)));//群发通知在线列表更新sendOnlineUserMsg();}@Overridepublic void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {if (session.isOpen())session.close();Iterator<Entry<Long, WebSocketSession>> mapItr = userWebsocketSessionMap.entrySet().iterator();while (mapItr.hasNext()) {final Entry<Long, WebSocketSession> one = mapItr.next();Long key = one.getKey();WebSocketSession value = one.getValue();if (value.getId().equals(session.getId())) {mapItr.remove();System.out.println("发送消息错误, 移除socket[key:" + key + "\t" + MainController.loginUserMap.get(key));//移除在线用户MainController.loginUserMap.remove(key);break;}}exception.printStackTrace();//群发通知在线列表更新sendOnlineUserMsg();}@Overridepublic void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {System.out.println("websocket用户关闭:" + JSON.toJSONString(status));Iterator<Entry<Long, WebSocketSession>> mapItr = userWebsocketSessionMap.entrySet().iterator();while (mapItr.hasNext()) {final Entry<Long, WebSocketSession> one = mapItr.next();Long key = one.getKey();WebSocketSession value = one.getValue();if (value.getId().equals(session.getId())) {mapItr.remove();System.out.println("用户关闭, 移除socket[key:" + key + "\t" + MainController.loginUserMap.get(key));MainController.loginUserMap.remove(key);break;}}//群发通知在线列表更新sendOnlineUserMsg();}@Overrideprotected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {if (message.getPayloadLength() == 0)return;System.out.println("[收到消息====>]" + message.getPayload().toString());CCMsg msg = JSON.parseObject(message.getPayload().toString(), CCMsg.class);WebSocketSession toSession = userWebsocketSessionMap.get(msg.getTo());if (toSession != null && toSession.isOpen()) {msg.setSendDate(new Date());toSession.sendMessage(new TextMessage(JSON.toJSONString(msg)));} else  {CCMsg welcome = new CCMsg();welcome.setTo(msg.getFrom());welcome.setMsg("[系统消息]对方已下线,请稍后重试...");welcome.setMsgType(MsgType.CHAT_MSG);welcome.setFromName("系统");welcome.setSendDate(new Date());welcome.setFrom(0L);session.sendMessage(new TextMessage(JSON.toJSONString(welcome)));//群发通知在线列表更新sendOnlineUserMsg();}}//发送在线信息更新消息public static void sendOnlineUserMsg() {CCMsg msg = new CCMsg();msg.setMsg(JSON.toJSONString(MainController.getOnLineUserList()));msg.setMsgType(MsgType.ONLINE_USER_MSG);msg.setFromName("系统");msg.setSendDate(new Date());msg.setFrom(0L);Iterator<Entry<Long, WebSocketSession>> mapItr = userWebsocketSessionMap.entrySet().iterator();while (mapItr.hasNext()) {final Entry<Long, WebSocketSession> one = mapItr.next();Long key = one.getKey();msg.setTo(key);WebSocketSession value = one.getValue();try {value.sendMessage(new TextMessage(JSON.toJSONString(msg)));} catch (IOException e) {e.printStackTrace();}}}}

3.3 【websocket握手拦截器】

package cc.hu.test.websocket.skthandler;import java.util.Map;import javax.servlet.http.HttpSession;import org.springframework.http.server.ServerHttpRequest;import org.springframework.http.server.ServerHttpResponse;import org.springframework.http.server.ServletServerHttpRequest;import org.springframework.web.socket.WebSocketHandler;import org.springframework.web.socket.server.HandshakeInterceptor;/** * spring websocket 握手拦截器 * @author sen.hu * */public class HandShake implements HandshakeInterceptor {@Overridepublic boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler,Map<String, Object> attributes) throws Exception {System.out.println("Websocket用户:[uid:" + ((ServletServerHttpRequest) request).getServletRequest().getSession(false).getAttribute("uid") + "]已经建立连接" );if (request instanceof ServletServerHttpRequest) {ServletServerHttpRequest servletRequest = (ServletServerHttpRequest) request;HttpSession session = servletRequest.getServletRequest().getSession(false);Long uid = (Long) session.getAttribute("uid");//标记用户if (uid != null) {attributes.put("uid", uid);} elsereturn false;}return true;}@Overridepublic void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler,Exception exception) {}}

3.4  【spring websocket配置类】

package cc.hu.test.websocket.skthandler;import javax.annotation.Resource;import org.springframework.context.annotation.Configuration;import org.springframework.stereotype.Component;import org.springframework.web.servlet.config.annotation.EnableWebMvc;import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;import org.springframework.web.socket.config.annotation.EnableWebSocket;import org.springframework.web.socket.config.annotation.WebSocketConfigurer;import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;@Configuration@EnableWebMvc@EnableWebSocketpublic class WebSocketConfig extends WebMvcConfigurerAdapter implements WebSocketConfigurer {//@Resource//MyWebSocketHandler handler;@ResourceMyTextWebSocketHandler textHandler;@Overridepublic void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {/*registry.addHandler(handler, "/ws").addInterceptors(new HandShake());//sock js处理类registry.addHandler(handler, "/ws/sockjs").addInterceptors(new HandShake()).withSockJS();*/registry.addHandler(textHandler, "/ws").addInterceptors(new HandShake());//sock js处理类registry.addHandler(textHandler, "/ws/sockjs").addInterceptors(new HandShake()).withSockJS();}}

3.5 【聊天页面及JS】

页面显示:
<%@page language="java" contentType="text/html; charset=UTF-8"%><!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width"><meta http-equiv="X-UA-Compatible" content="IE=edge"><title>cc的聊天</title><link rel="stylesheet" type="text/css" href="http://127.0.0.1:8087/static/css/style.css"></head><body><input type="hidden" value="${uid }" id="uid"/><input type="hidden" value="${userName }" id="username"/><input type="hidden" value="${path }" id="path"/><input type="hidden" value="" id="toUser"/><!----><div>欢迎[${userName }]</div><div><div class="online_list">  <ul id="onlineList" class="onlinelist"></ul></div><div id="content" class="content"></div></div><input type="text" placeholder="请输入要发送的信息" id="msg" class="msg"><input type="button" value="发送" class="send" onclick="sendMsgToServer(2)" ><input type="button" value="清空" class="clear" onclick="clearAll()"><script type="text/javascript" src="http://cdn.bootcss.com/jquery/1.12.4/jquery.min.js"></script><script type="text/javascript" src="http://127.0.0.1:8087/static/js/talk.js"></script></body></html>
页面JS:
/** * Created by root on 7/15/17. */window.path = $("#path").val();window.uid = $("#uid").val();window.fromUserId = uid;window.sendTo = $("#toUser").val();window.fromUserName = $("#username").val();window.ws;Date.prototype.Format = function (fmt) { //author: meizz     var o = {        "M+": this.getMonth() + 1, //月份         "d+": this.getDate(), //日         "h+": this.getHours(), //小时         "m+": this.getMinutes(), //分         "s+": this.getSeconds(), //秒         "q+": Math.floor((this.getMonth() + 3) / 3), //季度         "S": this.getMilliseconds() //毫秒     };    if (/(y+)/.test(fmt)) fmt = fmt.replace(RegExp.$1, (this.getFullYear() + "").substr(4 - RegExp.$1.length));    for (var k in o)    if (new RegExp("(" + k + ")").test(fmt)) fmt = fmt.replace(RegExp.$1, (RegExp.$1.length == 1) ? (o[k]) : (("00" + o[k]).substr(("" + o[k]).length)));    return fmt;}function changeBgColor(id, type) {var toid = $("#toUser").val();    if (type == 1) {//add css    $("#li"+ id).css("cursor", "hand");    if (toid != id) {    $("#li" + toid).css("background", "white");    }                $("#li"+ id).css("background", "burlywood");            } else if (type == 2) {        if (toid != id) {            $("#li"+ id).css("background", "white");        }        $("#li"+ toid).css("background", "burlywood");        $("#li"+ id).css("cursor", "normal");    }}function sendMsgToServer(type) {    var msg = "";    var toUserId = $("#toUser").val();    if (toUserId == fromUserId) {        alert("不能给自己发消息!");        return;    }    switch(type) {        case 1:            msg = "hello";            break;        case 2:            msg = $("#msg").val();            break;        default:            msg = $("#msg").val();    }    msg = $.trim(msg);    if (msg.length == 0) {        alert("发送信息不能为空!");        return;    }    if (ws.readyState == WebSocket.OPEN) {            var sendData = {};        sendData["from"] = fromUserId;        sendData["fromName"] = fromUserName;        sendData["to"] = toUserId;        sendData["msg"] = msg;        console.log("发送消息====>" + JSON.stringify(sendData));        ws.send(JSON.stringify(sendData));        $("#msg").val("");        $("#content").append("<div class='tmsg'><label class='name'>我 "+new Date().Format("yyyy-MM-dd hh:mm:ss")+"</label><div class='tmsg_text'>"+sendData.msg+"</div></div>");    } else {        alert("Websocket:连接状态异常");        console.log("Websocket:连接状态异常,readyState=" + ws.readyState);    }}function setToUserId(toUserId) {    $("#toUser").val(toUserId);    $("#li"+ toUserId).css("background", "burlywood");}function updateOnlineList(rcv) {    console.log("在线用户更新:" + rcv.msg);    var onlineList =  eval(rcv.msg);    var html="";    for (var o in onlineList) {        console.log(o);        html += "<li id='li" + onlineList[o].userId +"' onclick='setToUserId("+ onlineList[o].userId+ ")' onmouseout='changeBgColor("+onlineList[o].userId+", 2)' onmouseenter='changeBgColor("+onlineList[o].userId+", 1)'>" + onlineList[o].nickName + "</li>";    }    $("#onlineList").html(html);}function updateMsgContent(rcv) {    var textCss = rcv.from == 0 ? "sfmsg_text" : "fmsg_text";    var html = "<div class='fmsg'>"        + "<label class='name'>"        + rcv.fromName + " " + rcv.sendDate        + "</label>"        + "<div class='" + textCss + "'>" + rcv.msg + "</div>"        + "</div>";    $("#content").append(html);    if (rcv.from != 0) {    $("#toUser").val(rcv.from);    $("#li"+ rcv.from).css("background", "burlywood");    }    scrollToBottom();}/**初始化websocket*/function initWebsocket() {    if ("WebSocket" in window)        ws = new WebSocket("ws://" + path + "/ws?uid=" + uid);    else if ("MozWebSocket" in window)        ws = new WebSocket("ws://" + path + "/ws?uid" + uid);    else        ws = new SockJS("http://" + path + "/ws/sockjs?uid=" + uid);        ws.onopen = function(event) {        console.log("websocket:链接...OK!");        console.log(event);        //sendMsgToServer(1);    };    ws.onmessage = function (event) {        var rcv = JSON.parse(event.data);        console.log("websocket:收到一条信息<=====" + rcv);        console.log(event);        var msgType = rcv.msgType;        switch (msgType) {            case 0x01:                updateOnlineList(rcv);                break;            case 0x10:                updateMsgContent(rcv);                break;            default:                updateMsgContent(rcv);        }    };    ws.onerror = function (event) {        console.log("websocket:发生错误!");        console.log(event);    };    ws.onclose = function (event) {        console.log("WebSocket:已关闭");        console.log(event);    }}function scrollToBottom() {    var div = document.getElementById('content');    div.scrollTop = div.scrollHeight;}function clear() {    $("#content").empty();}window.onload = function() {    initWebsocket();    document.onkeydown = function(event) {        var code;        if (window.event)            code = window.event.keyCode;        else            code = event.which;        if (code == 13)            sendMsgToServer();    }}

3.6【登陆界面及登陆处理Controller】

简单的登陆界面及登陆处理逻辑,代码及具体实现【略】


后记:只是一个简单的demo示例,很多细节没有考虑,处理。只实现了最简单文本消息处理,以后可以尝试二进制,增加自定义协议、及安全处理等,完善demo。



原创粉丝点击
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 英语中用词不当和拼写错误怎么办 爬楼梯的购物车车轮坏了怎么办? 帮别人买东西不给我钱怎么办 老师念错名字有同学指出来你怎么办 老公婚前买的房子婆婆想霸占怎么办 我想查我的基金收益情况怎么办 儿童票买好了但大人退票了怎么办 没有享受到国家政策的农民怎么办? 股票涨了没抛然后一直跌怎么办 苹果手机放久了开不了机怎么办 部门要辞退你你不想走该怎么办 口头说辞职现在又不想走了怎么办 网上买的理财不给退本金怎么办 买东西遇到态度不好的人你会怎么办 app在下载东西时被停用了怎么办 买东西填错地址 但已签收怎么办 网购手机受骗后电话打不通怎么办 淘宝买的东西发错了怎么办 淘宝上买的东西发错了怎么办 淘宝上买的东西发多了怎么办 淘宝上买的东西出现问题怎么办 淘宝买东西未收到货显示签收怎么办 网上一张车票两人同时付款了怎么办 微信付款时显示银行卡被锁定怎么办 微信忘记支付密码怎么办没有银行卡 支付宝赏金扫码支付不行怎么办 网购收货时发现货物已破损怎么办 货物丢失了不承认调查出来了怎么办 小米商城已签收未收到了怎么办 网购的一只荷兰猪现在怎么办 中通快递的掌中通好评怎么办 唯品会快递被签收却没收到货怎么办 拼多多快递签收了但没收到货怎么办 顺丰代收签收成功要强制退款怎么办 淘宝付了钱如果商家没货了怎么办 别人拿走我的货不给钱怎么办 快递出途中损坏签单后才发现怎么办 顾客支付宝少付了钱人走了怎么办 顾客通过收钱码付的钱少了怎么办 寄顺丰快递收件人号码填错了怎么办 京东退货售后说是有划痕怎么办