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

来源:互联网 发布:网络诗歌每日一诗 编辑:程序博客网 时间:2024/06/16 16:38

原文地址:http://www.xdemo.org/spring-websocket-comet/

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

浏览器支持情况

Chrome4+
Firefox4+Internet Explorer10+Opera10+Safari5+

服务器支持

jetty7.0.1+tomcat7.0.27+Nginx1.3.13+resin4+

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@EnableWebSocketpublic 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 */@Componentpublic 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下载

GitHub:https://github.com/yangchunjian/websocket


原创粉丝点击