Spring之WebSocket网页聊天以及服务器推送


/Springframework /Spring之WebSocket网页聊天以及服务器推送



1. WebSocket protocol 是HTML5一种新的协议。它实现了浏览器与服务器全双工通信(full-duplex)。

2. 轮询是在特定的的时间间隔(如每1秒),由浏览器对服务器发出HTTP request,然后由服务器返回最新的数据给客服端的浏览器。这种传统的HTTP request 的模式带来很明显的缺点 – 浏览器需要不断的向服务器发出请求,然而HTTP request 的header是非常长的,里面包含的有用数据可能只是一个很小的值,这样会占用很多的带宽。

3. 比较新的技术去做轮询的效果是Comet – 用了AJAX。但这种技术虽然可达到全双工通信,但依然需要发出请求

4. 在 WebSocket API,浏览器和服务器只需要要做一个握手的动作,然后,浏览器和服务器之间就形成了一条快速通道。两者之间就直接可以数据互相传送

5. 在此WebSocket 协议中,为我们实现即时服务带来了两大好处:

 5.1. Header

  互相沟通的Header是很小的-大概只有 2 Bytes

 5.2. Server Push

浏览器支持情况

Chrome

4+

Firefox

4+

Internet Explorer

10+

Opera

10+

Safari

5+

服务器支持

jetty

7.0.1+

tomcat

7.0.27+

Nginx

1.3.13+

resin

4+

API



var         ws =         new         WebSocket(“ws:        //echo.websocket.org”);       


        ws.onopen =         function        (){ws.send(“Test!”); };       


        //当有消息时,会自动调用此方法       


        ws.onmessage =         function        (evt){console.log(evt.data);ws.close();};       


        ws.onclose =         function        (evt){console.log(“WebSocketClosed!”);};       


        ws.onerror =         function        (evt){console.log(“WebSocketError!”);};


Demo简介

模拟了两个用户的对话,张三和李四,然后还有发送一个广播,即张三和李四都是可以接收到的,登录的时候分别选择张三和李四即可

Demo效果

Maven依赖

<        dependency        >       


        <        groupId        >com.fasterxml.jackson.core</        groupId        >       


        <        artifactId        >jackson-annotations</        artifactId        >       


        <        version        >2.3.0</        version        >       


        </        dependency        >       


        <        dependency        >       


        <        groupId        >com.fasterxml.jackson.core</        groupId        >       


        <        artifactId        >jackson-core</        artifactId        >       


        <        version        >2.3.1</        version        >       


        </        dependency        >       


        <        dependency        >       


        <        groupId        >com.fasterxml.jackson.core</        groupId        >       


        <        artifactId        >jackson-databind</        artifactId        >       


        <        version        >2.3.3</        version        >       


        </        dependency        >       


        <        dependency        >       


        <        groupId        >org.springframework</        groupId        >       


        <        artifactId        >spring-messaging</        artifactId        >       


        <        version        >4.0.5.RELEASE</        version        >       


        </        dependency        >       


        <        dependency        >       


        <        groupId        >org.springframework</        groupId        >       


        <        artifactId        >spring-websocket</        artifactId        >       


        <        version        >4.0.5.RELEASE</        version        >       


        </        dependency        >       


        <        dependency        >       


        <        groupId        >org.springframework</        groupId        >       


        <        artifactId        >spring-webmvc</        artifactId        >       


        <        version        >4.0.5.RELEASE</        version        >       


        </        dependency        >       


        <        dependency        >       


        <        groupId        >com.google.code.gson</        groupId        >       


        <        artifactId        >gson</        artifactId        >       


        <        version        >2.3.1</        version        >       


        </        dependency        >       


        <        dependency        >       


        <        groupId        >javax.servlet</        groupId        >       


        <        artifactId        >javax.servlet-api</        artifactId        >       


        <        version        >3.1.0</        version        >       


        <        scope        >provided</        scope        >       


        </        dependency        >       


        <        dependency        >       


        <        groupId        >junit</        groupId        >       


        <        artifactId        >junit</        artifactId        >       


        <        version        >3.8.1</        version        >       


        <        scope        >test</        scope        >       


        </        dependency        >


Web.xml,spring-mvc.xml,User.java请查看附件

WebSocket相关的类

WebSocketConfig,配置WebSocket的处理器(MyWebSocketHandler)和拦截器(HandShake)

package         org.xdemo.example.websocket.websocket;       


        import         javax.annotation.Resource;       


        import         org.springframework.stereotype.Component;       


        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;       


        /**       


                * WebScoket配置处理器       


                * @author Goofy       


                * @Date 2015年6月11日 下午1:15:09       


                */       


        @Component       


        @EnableWebSocket       


        public         class         WebSocketConfig         extends         WebMvcConfigurerAdapter         implements         WebSocketConfigurer {       


        @Resource       


        MyWebSocketHandler handler;       


        public         void         registerWebSocketHandlers(WebSocketHandlerRegistry registry) {       


        registry.addHandler(handler,         "/ws"        ).addInterceptors(        new         HandShake());       


        registry.addHandler(handler,         "/ws/sockjs"        ).addInterceptors(        new         HandShake()).withSockJS();       


        }       


        }


MyWebSocketHandler

package         org.xdemo.example.websocket.websocket;       


        import         java.io.IOException;       


        import         java.text.SimpleDateFormat;       


        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.WebSocketHandler;       


        import         org.springframework.web.socket.WebSocketMessage;       


        import         org.springframework.web.socket.WebSocketSession;       


        import         org.xdemo.example.websocket.entity.Message;       


        import         com.google.gson.Gson;       


        import         com.google.gson.GsonBuilder;       


        /**       


                * Socket处理器       


                *        


                * @author Goofy       


                * @Date 2015年6月11日 下午1:19:50       


                */       


        @Component       


        public         class         MyWebSocketHandler         implements         WebSocketHandler {       


        public         static         final         Map<Long, WebSocketSession> userSocketSessionMap;       


        static         {       


        userSocketSessionMap =         new         HashMap<Long, WebSocketSession>();       


        }       


        /**       


                * 建立连接后       


                */       


        public         void         afterConnectionEstablished(WebSocketSession session)       


        throws         Exception {       


        Long uid = (Long) session.getAttributes().get(        "uid"        );       


        if         (userSocketSessionMap.get(uid) ==         null        ) {       


        userSocketSessionMap.put(uid, session);       


        }       


        }       


        /**       


                * 消息处理,在客户端通过Websocket API发送的消息会经过这里,然后进行相应的处理       


                */       


        public         void         handleMessage(WebSocketSession session, WebSocketMessage<?> message)         throws         Exception {       


        if        (message.getPayloadLength()==        0        )        return        ;       


        Message msg=        new         Gson().fromJson(message.getPayload().toString(),Message.        class        );       


        msg.setDate(        new         Date());       


        sendMessageToUser(msg.getTo(),         new         TextMessage(        new         GsonBuilder().setDateFormat(        "yyyy-MM-dd HH:mm:ss"        ).create().toJson(msg)));       


        }       


        /**       


                * 消息传输错误处理       


                */       


        public         void         handleTransportError(WebSocketSession session,       


        Throwable exception)         throws         Exception {       


        if         (session.isOpen()) {       


        session.close();       


        }       


        Iterator<Entry<Long, WebSocketSession>> it = userSocketSessionMap       


        .entrySet().iterator();       


        // 移除Socket会话       


        while         (it.hasNext()) {       


        Entry<Long, WebSocketSession> entry = it.next();       


        if         (entry.getValue().getId().equals(session.getId())) {       


        userSocketSessionMap.remove(entry.getKey());       


        System.out.println(        "Socket会话已经移除:用户ID"         + entry.getKey());       


        break        ;       


        }       


        }       


        }       


        /**       


                * 关闭连接后       


                */       


        public         void         afterConnectionClosed(WebSocketSession session,       


        CloseStatus closeStatus)         throws         Exception {       


        System.out.println(        "Websocket:"         + session.getId() +         "已经关闭"        );       


        Iterator<Entry<Long, WebSocketSession>> it = userSocketSessionMap       


        .entrySet().iterator();       


        // 移除Socket会话       


        while         (it.hasNext()) {       


        Entry<Long, WebSocketSession> entry = it.next();       


        if         (entry.getValue().getId().equals(session.getId())) {       


        userSocketSessionMap.remove(entry.getKey());       


        System.out.println(        "Socket会话已经移除:用户ID"         + entry.getKey());       


        break        ;       


        }       


        }       


        }       


        public         boolean         supportsPartialMessages() {       


        return         false        ;       


        }       


        /**       


                * 给所有在线用户发送消息       


                *        


                * @param message       


                * @throws IOException       


                */       


        public         void         broadcast(        final         TextMessage message)         throws         IOException {       


        Iterator<Entry<Long, WebSocketSession>> it = userSocketSessionMap       


        .entrySet().iterator();       


        // 多线程群发       


        while         (it.hasNext()) {       


        final         Entry<Long, WebSocketSession> entry = it.next();       


        if         (entry.getValue().isOpen()) {       


        // entry.getValue().sendMessage(message);       


        new         Thread(        new         Runnable() {       


        public         void         run() {       


        try         {       


        if         (entry.getValue().isOpen()) {       


        entry.getValue().sendMessage(message);       


        }       


        }         catch         (IOException e) {       


        e.printStackTrace();       


        }       


        }       


        }).start();       


        }       


        }       


        }       


        /**       


                * 给某个用户发送消息       


                *        


                * @param userName       


                * @param message       


                * @throws IOException       


                */       


        public         void         sendMessageToUser(Long uid, TextMessage message)       


        throws         IOException {       


        WebSocketSession session = userSocketSessionMap.get(uid);       


        if         (session !=         null         && session.isOpen()) {       


        session.sendMessage(message);       


        }       


        }       


        }


HandShake(每次建立连接都会进行握手)

package         org.xdemo.example.websocket.websocket;       


        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;       


        /**       


                * Socket建立连接(握手)和断开       


                *        


                * @author Goofy       


                * @Date 2015年6月11日 下午2:23:09       


                */       


        public         class         HandShake         implements         HandshakeInterceptor {       


        public         boolean         beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes)         throws         Exception {       


        System.out.println(        "Websocket:用户[ID:"         + ((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);       


        }        else        {       


        return         false        ;       


        }       


        }       


        return         true        ;       


        }       


        public         void         afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) {       


        }       


        }

一个Controller

package         org.xdemo.example.websocket.controller;       


        import         java.io.IOException;       


        import         java.util.Date;       


        import         java.util.HashMap;       


        import         java.util.Map;       


        import         javax.annotation.Resource;       


        import         javax.servlet.http.HttpServletRequest;       


        import         org.springframework.stereotype.Controller;       


        import         org.springframework.web.bind.annotation.ModelAttribute;       


        import         org.springframework.web.bind.annotation.RequestMapping;       


        import         org.springframework.web.bind.annotation.RequestMethod;       


        import         org.springframework.web.bind.annotation.ResponseBody;       


        import         org.springframework.web.servlet.ModelAndView;       


        import         org.springframework.web.socket.TextMessage;       


        import         org.xdemo.example.websocket.entity.Message;       


        import         org.xdemo.example.websocket.entity.User;       


        import         org.xdemo.example.websocket.websocket.MyWebSocketHandler;       


        import         com.google.gson.GsonBuilder;       


        @Controller       


        @RequestMapping        (        "/msg"        )       


        public         class         MsgController {       


        @Resource       


        MyWebSocketHandler handler;       


        Map<Long, User> users =         new         HashMap<Long, User>();       


                


                //模拟一些数据       


        @ModelAttribute       


        public         void         setReqAndRes() {       


        User u1 =         new         User();       


        u1.setId(1L);       


        u1.setName(        "张三"        );       


        users.put(u1.getId(), u1);       


        User u2 =         new         User();       


        u2.setId(2L);       


        u2.setName(        "李四"        );       


        users.put(u2.getId(), u2);       


        }       


        //用户登录       


        @RequestMapping        (value=        "login"        ,method=RequestMethod.POST)       


        public         ModelAndView doLogin(User user,HttpServletRequest request){       


        request.getSession().setAttribute(        "uid"        , user.getId());       


        request.getSession().setAttribute(        "name"        , users.get(user.getId()).getName());       


        return         new         ModelAndView(        "redirect:talk"        );       


        }       


        //跳转到交谈聊天页面       


        @RequestMapping        (value=        "talk"        ,method=RequestMethod.GET)       


        public         ModelAndView talk(){       


        return         new         ModelAndView(        "talk"        );       


        }       


        //跳转到发布广播页面       


        @RequestMapping        (value=        "broadcast"        ,method=RequestMethod.GET)       


        public         ModelAndView broadcast(){       


        return         new         ModelAndView(        "broadcast"        );       


        }       


        //发布系统广播(群发)       


        @ResponseBody       


        @RequestMapping        (value=        "broadcast"        ,method=RequestMethod.POST)       


        public         void         broadcast(String text)         throws         IOException{       


        Message msg=        new         Message();       


        msg.setDate(        new         Date());       


        msg.setFrom(-1L);       


        msg.setFromName(        "系统广播"        );       


        msg.setTo(0L);       


        msg.setText(text);       


        handler.broadcast(        new         TextMessage(        new         GsonBuilder().setDateFormat(        "yyyy-MM-dd HH:mm:ss"        ).create().toJson(msg)));       


        }       


        }


一个消息的封装的类

package         org.xdemo.example.websocket.entity;       


        import         java.util.Date;       


        /**       


                * 消息类       


                * @author Goofy       


                * @Date 2015年6月12日 下午7:32:39       


                */       


        public         class         Message {       


        //发送者       


        public         Long from;       


        //发送者名称       


        public         String fromName;       


        //接收者       


        public         Long to;       


        //发送的文本       


        public         String text;       


        //发送日期       


        public         Date date;       


        public         Long getFrom() {       


        return         from;       


        }       


        public         void         setFrom(Long from) {       


        this        .from = from;       


        }       


        public         Long getTo() {       


        return         to;       


        }       


        public         void         setTo(Long to) {       


        this        .to = to;       


        }       


        public         String getText() {       


        return         text;       


        }       


        public         void         setText(String text) {       


        this        .text = text;       


        }       


        public         String getFromName() {       


        return         fromName;       


        }       


        public         void         setFromName(String fromName) {       


        this        .fromName = fromName;       


        }       


        public         Date getDate() {       


        return         date;       


        }       


        public         void         setDate(Date date) {       


        this        .date = date;       


        }       


        }


聊天页面

<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%>       


        <%       


        String path = request.getContextPath();       


        String basePath = request.getServerName() + ":"       


        + request.getServerPort() + path + "/";       


        String basePath2 = request.getScheme() + "://"       


        + request.getServerName() + ":" + request.getServerPort()       


        + path + "/";       


        %>       


        <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN"       


        "http://www.w3.org/TR/html4/strict.dtd">       


        <        html         xmlns        =        "http://www.w3.org/1999/xhtml"        >       


        <        head        >       


        <        meta         http-equiv        =        "Content-Type"         content        =        "text/html; charset=utf-8"         />       


        <        title        ></        title        >       


        <        script         type        =        "text/javascript"         src="<%=basePath2%>resources/jquery.js"></        script        >       


        <        style        >       


        textarea {       


        height: 300px;       


        width: 100%;       


        resize: none;       


        outline: none;       


        }       


        input[type=button] {       


        float: right;       


        margin: 5px;       


        width: 50px;       


        height: 35px;       


        border: none;       


        color: white;       


        font-weight: bold;       


        outline: none;       


        }       


        .clear {       


        background: red;       


        }       


        .send {       


        background: green;       


        }       


        .clear:active {       


        background: yellow;       


        }       


        .send:active {       


        background: yellow;       


        }       


        .msg {       


        width: 100%;       


        height: 25px;       


        outline: none;       


        }       


        #content {       


        border: 1px solid gray;       


        width: 100%;       


        height: 400px;       


        overflow-y: scroll;       


        }       


        .from {       


        background-color: green;       


        width: 80%;       


        border-radius: 10px;       


        height: 30px;       


        line-height: 30px;       


        margin: 5px;       


        float: left;       


        color: white;       


        padding: 5px;       


        font-size: 22px;       


        }       


        .to {       


        background-color: gray;       


        width: 80%;       


        border-radius: 10px;       


        height: 30px;       


        line-height: 30px;       


        margin: 5px;       


        float: right;       


        color: white;       


        padding: 5px;       


        font-size: 22px;       


        }       


        .name {       


        color: gray;       


        font-size: 12px;       


        }       


        .tmsg_text {       


        color: white;       


        background-color: rgb(47, 47, 47);       


        font-size: 18px;       


        border-radius: 5px;       


        padding: 2px;       


        }       


        .fmsg_text {       


        color: white;       


        background-color: rgb(66, 138, 140);       


        font-size: 18px;       


        border-radius: 5px;       


        padding: 2px;       


        }       


        .sfmsg_text {       


        color: white;       


        background-color: rgb(148, 16, 16);       


        font-size: 18px;       


        border-radius: 5px;       


        padding: 2px;       


        }       


        .tmsg {       


        clear: both;       


        float: right;       


        width: 80%;       


        text-align: right;       


        }       


        .fmsg {       


        clear: both;       


        float: left;       


        width: 80%;       


        }       


        </        style        >       


        <        script        >       


        var path = '<%=basePath%>';       


        var uid=${uid eq null?-1:uid};       


        if(uid==-1){       


        location.href="<%=basePath2%>";       


        }       


        var from=uid;       


        var fromName='${name}';       


        var to=uid==1?2:1;       


        var websocket;       


        if ('WebSocket' in window) {       


        websocket = new WebSocket("ws://" + path + "/ws?uid="+uid);       


        } else if ('MozWebSocket' in window) {       


        websocket = new MozWebSocket("ws://" + path + "/ws"+uid);       


        } else {       


        websocket = new SockJS("http://" + path + "/ws/sockjs"+uid);       


        }       


        websocket.onopen = function(event) {       


        console.log("WebSocket:已连接");       


        console.log(event);       


        };       


        websocket.onmessage = function(event) {       


        var data=JSON.parse(event.data);       


        console.log("WebSocket:收到一条消息",data);       


        var textCss=data.from==-1?"sfmsg_text":"fmsg_text";       


        $("#content").append("<        div        ><        label        >"+data.fromName+" "+data.date+"</        label        ><        div         class        =        '"+textCss+"'        >"+data.text+"</        div        ></        div        >");       


        scrollToBottom();       


        };       


        websocket.onerror = function(event) {       


        console.log("WebSocket:发生错误 ");       


        console.log(event);       


        };       


        websocket.onclose = function(event) {       


        console.log("WebSocket:已关闭");       


        console.log(event);       


        }       


        function sendMsg(){       


        var v=$("#msg").val();       


        if(v==""){       


        return;       


        }else{       


        var data={};       


        data["from"]=from;       


        data["fromName"]=fromName;       


        data["to"]=to;       


        data["text"]=v;       


        websocket.send(JSON.stringify(data));       


        $("#content").append("<        div        ><        label        >我 "+new Date().Format("yyyy-MM-dd hh:mm:ss")+"</        label        ><        div        >"+data.text+"</        div        ></        div        >");       


        scrollToBottom();       


        $("#msg").val("");       


        }       


        }       


        function scrollToBottom(){       


        var div = document.getElementById('content');       


        div.scrollTop = div.scrollHeight;       


        }       


        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 send(event){       


        var code;       


        if(window.event){       


        code = window.event.keyCode; // IE       


        }else{       


        code = e.which; // Firefox       


        }       


        if(code==13){        


        sendMsg();                   


        }       


        }       


        function clearAll(){       


        $("#content").empty();       


        }       


        </        script        >       


        </        head        >       


        <        body        >       


        欢迎:${sessionScope.name }       


        <        div         id        =        "content"        ></        div        >       


        <        input         type        =        "text"         placeholder        =        "请输入要发送的信息"         id        =        "msg"         onkeydown        =        "send(event)"        >       


        <        input         type        =        "button"         value        =        "发送"         onclick        =        "sendMsg()"         >       


        <        input         type        =        "button"         value        =        "清空"         onclick        =        "clearAll()"        >       


        </        body        >       


        </        html        >


发布广播的页面

<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%>       


        <%       


        String path = request.getContextPath();       


        String basePath= request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort() + path + "/";       


        %>       


        <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN"       


        "http://www.w3.org/TR/html4/strict.dtd">       


        <        html         xmlns        =        "http://www.w3.org/1999/xhtml"        >       


        <        head        >       


        <        meta         http-equiv        =        "Content-Type"         content        =        "text/html; charset=utf-8"         />       


        <        title        ></        title        >       


        <        script         type        =        "text/javascript"         src="<%=basePath%>resources/jquery.js"></        script        >       


        <        script         type        =        "text/javascript"        >       


        var path='<%=basePath%>';       


        function broadcast(){       


        $.ajax({       


        url:path+'msg/broadcast',       


        type:"post",       


        data:{text:$("#msg").val()},       


        dataType:"json",       


        success:function(data){       


        alert("发送成功");       


        }       


        });       


        }       


        </        script        >       


        </        head        >       


        <        body        >       


        发送广播       


        <        textarea         style        =        "width:100%;height:300px;"         id        =        "msg"         ></        textarea        >       


        <        input         type        =        "button"         value        =        "发送"         onclick        =        "broadcast()"        >       


        </        body        >       


        </        html        >


Chrome的控制台网络信息

Type:websocket

Time:Pending

表示这是一个websocket请求,请求一直没有结束,可以通过此通道进行双向通信,即双工,实现了服务器推送的效果,也减少了网络流量。

Chrome控制台信息

Demo下载

百度网盘:http://pan.baidu.com/s/1dD0b15Z