zoukankan      html  css  js  c++  java
  • 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

  • 相关阅读:
    ICONS-图标库
    图形资源
    vue项目中,如果修改了组件名称,vscode编辑器会在引入修改组件的名字处提示红色波浪线 The file is in the program because:Imported via xxx Root file specified for compilation .
    接口在dev环境报跨域问题(has been blocked by CORS policy:Response to preflight request doesn't pass access control check:No 'Access-Control-Allow-Origin' header ispresent on the requested resource.),qa环境正常
    阿里云occ的图片文件URL用浏览器直接打开无法访问,提示This XML file does noe appear to have any style information associated with it. The document tree is shown below.
    vue 项目使用element ui 中tree组件 check-strictly 用法(父子不互相关联的反显情况)
    高德地图进行线路规划绘制标记点操作(vue)
    vue中实现拖拽调整顺序功能
    2021-01-22 浏览器相关知识
    2021-01-22 js 相关知识点
  • 原文地址:https://www.cnblogs.com/1995hxt/p/5125615.html
Copyright © 2011-2022 走看看