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


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

    转自:http://www.xdemo.org/spring-websocket-comet/

    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;
     100%;
    resize: none;
    outline: none;
    }
    input[type=button] {
    float: right;
    margin: 5px;
     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 {
     100%;
    height: 25px;
    outline: none;
    }
    #content {
    border: 1px solid gray;
     100%;
    height: 400px;
    overflow-y: scroll;
    }
    .from {
    background-color: green;
     80%;
    border-radius: 10px;
    height: 30px;
    line-height: 30px;
    margin: 5px;
    float: left;
    color: white;
    padding: 5px;
    font-size: 22px;
    }
    .to {
    background-color: gray;
     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;
     80%;
    text-align: right;
    }
    .fmsg {
    clear: both;
    float: left;
     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+"&nbsp;"+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>我&nbsp;"+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="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

  • 相关阅读:
    C语言温习杂记
    C语言变量类型与内存管理
    解析搜狗新闻语料库
    关于clang, scan-build, 和clang test
    Clang checker类总结
    让你的checker出现在clang的checker list中
    Clang安装配置解释
    Operators 操作
    cumulative_distribution累积分布
    SVM
  • 原文地址:https://www.cnblogs.com/1995hxt/p/5125615.html
Copyright © 2020-2023  润新知