WEB聊天系统(基于长轮询comet服务器反推技术)

java 服务器端程序开发中,通常有这样的需求,就是服务器主动给客户端推送数据。在 B/S 架构中,通常是客户端向服务器发送请求,然后服务器给客户端做出响应。如果要做一个 Web 聊天工具, A 客户端向服务器发送聊天数据,另一个 B 客户端需要请求服务器才能获取数据(流程图如下)。
WEB聊天系统(基于长轮询comet服务器反推技术)_第1张图片


再比如一些股票、证券数据,客户端需要时刻去关注服务器的数据,按照传统的方式,客户端需要时刻刷新客户端,才能获取服务器数据。如下图

WEB聊天系统(基于长轮询comet服务器反推技术)_第2张图片


以上问题就需要使用服务器反推技术解决。
以聊天功能为例:B 客户端如果要时刻获取 A 发送到服务器的数据。

原始的解决方案:b 客户端不停的向服务端通过 AJAX 发送请求。来获取服务器的响应。
以下模拟A 客户端的聊天数据

以下模拟B 客户端如何不刷新页面获取 A 客户端的数据。
WEB聊天系统(基于长轮询comet服务器反推技术)_第3张图片

但是以上方案缺点太明显,即客户端不停的向服务器发送请求,服务器压力太大。
除了以上原始方式外,就需要使用一新种技术,我们把这种技术叫comet


comet 即基于长连接 Http 的服务器反推技术
务器端会主动以异步的方式向客户端程序推送数据,而不需要客户端显式的发出请求。Comet  架构非常适合 事件驱动  Web  应用,以及对 交互性 和实时性要求很强的应用,如股票交易行情分析、聊天室和 Web  版在线游戏等。

要使用基于长连接的服务器反推技术有两种,第一种是基于流的方式。原理如下。
WEB聊天系统(基于长轮询comet服务器反推技术)_第4张图片






第一步:修改Tomcat 的协议

将原来server.xml
改为

第二步服务器的代码:
首先服务器的代码就不能直接继承HttpServlet, 因为直接继承 HttpServlet 后,不能和客户端进行长时间的链接。所以需要继承 CometProcessor.
访问的url comet1 ,然后给客户端循环发送数据。
package cn.itcast.servlet;


import java.awt.peer.ComponentPeer;
import java.io.IOException;
import java.util.Random;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.catalina.comet.CometEvent;
import org.apache.catalina.comet.CometProcessor;
import org.springframework.beans.factory.annotation.Autowired;
import org.apache.catalina.comet.CometEvent.EventSubType;
import org.apache.catalina.comet.CometEvent.EventType;


public class AChatCometServlet extends HttpServlet implements CometProcessor {
        private static final long serialVersionUID = 1L;

        @Autowired
        @Override
        public void event(CometEvent event) throws IOException, ServletException {
                HttpServletResponse resp = event.getHttpServletResponse();
                HttpServletRequest request = event.getHttpServletRequest();
                if(event.getEventType() == EventType.BEGIN) {
                        // 连接已经建立
                        // 模拟发送数据给客户端

                        System.out.println(" 服务器和客户端建立了连接 "+" 客户端的 sessionid " + request.getSession().getId());
                        new Thread(){
                                public void run() {
                                        Random random = new Random();
                                        while(true) {
                                                try {
                                                        int num = random.nextInt(100);
                                                        Thread.sleep(5000);
                                                        System.out.println(num);
                                                        resp.getWriter().println(String.valueOf(num));
                                                        resp.getWriter().flush();
                                                } catch (IOException e) {
                                                        e.printStackTrace();
                                                } catch (InterruptedException e) {
                                                        e.printStackTrace();
                                                }
                                        }

                                };
                        }.start();

                        System.out.println(" 线程开启 ");
                }else if(event.getEventType() == EventType.END) {
                        // 客户端连接已经断开
                        event.close();
                }else if(event.getEventType() == EventType.ERROR) {
                        System.out.println(" 服务器出错 ");
                        event.close();
                }

        }

}

      客户端如何来做?
ajax 中,客户端和服务器的通信状态有一下几种。
WEB聊天系统(基于长轮询comet服务器反推技术)_第5张图片

我们需要做的就是在3 这一步接受服务器响应的部分数据。

WEB聊天系统(基于长轮询comet服务器反推技术)_第6张图片
注意:客户端接受数据的状态并不是在4 4 表示服务器全部发送完毕。而客户端接受数据应该是 3
服务器发送一部分数据后就需要获取。这样就不断获取服务器的数据,但是每次发送数据后responseText 中的数据就会变多,直接展示会重复。需要截取,截取的代码优化如下。

WEB聊天系统(基于长轮询comet服务器反推技术)_第7张图片
以上就是基于长连接的流的方式处理服务器反推问题。

程序运行后:
WEB聊天系统(基于长轮询comet服务器反推技术)_第8张图片


基于长连接流的方式存在一些问题:
* 客户端 API 必须使用原始的 AJAX ,因为只有原生的 ajax 才有状态的 API
* 有些浏览器不支持原始 ajax API



基于长连接的轮询方式,原理就是客户端和服务器连接后,服务器如果响应完毕数据给客户端,客户端要立即和服务器再次进行连接。

WEB聊天系统(基于长轮询comet服务器反推技术)_第9张图片 
第一步:服务器的代码
package cn.itcast.servlet;


import java.awt.peer.ComponentPeer;
import java.io.IOException;
import java.util.Random;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.catalina.comet.CometEvent;
import org.apache.catalina.comet.CometProcessor;
import org.springframework.beans.factory.annotation.Autowired;
import org.apache.catalina.comet.CometEvent.EventSubType;
import org.apache.catalina.comet.CometEvent.EventType;


public class AChatCometServlet2 extends HttpServlet implements CometProcessor {
        private static final long serialVersionUID = 1L;


        @Override
        public void event(CometEvent event) throws IOException, ServletException {
                final HttpServletResponse resp = event.getHttpServletResponse();
                HttpServletRequest request = event.getHttpServletRequest();
                if(event.getEventType() == EventType.BEGIN) {
                        //连接已经建立
                        //模拟发送数据给客户端

                        System.out.println("服务器和客户端建立了连接"+"客户端的sessionid是" + request.getSession().getId());
                        new Thread(){
                                public void run() {
                                        Random random = new Random();
                                        while(true) {
                                                try {
                                                        int num = random.nextInt(100);
                                                        Thread.sleep(5000);
                                                        System.out.println(num);
                                                        resp.getWriter().println(String.valueOf(num));
                                                        //注意:服务器端需要关闭response,否则就是一个长连接。
                                                        resp.getWriter().flush();
                                                        resp.getWriter().close();

                                                } catch (IOException e) {
                                                        e.printStackTrace();
                                                } catch (InterruptedException e) {
                                                        e.printStackTrace();
                                                }
                                        }

                                };
                        }.start();

                        System.out.println("线程开启");
                }else if(event.getEventType() == EventType.END) {
                        //客户端连接已经断开
                        event.close();
                }else if(event.getEventType() == EventType.ERROR) {
                        System.out.println("服务器出错");
                        event.close();
                }

        }

}


客户端代码:
客户端在状态为4 的时候,获取数据重新发起请求。
WEB聊天系统(基于长轮询comet服务器反推技术)_第10张图片

以上代码就是关于基于comet长连接的轮询方式,但是该代码存在一些问题需要修复,下一次讲解如何修复一些问题,以及如何利用comet实现web聊天。

运行上面代码会发现以下问题:
WEB聊天系统(基于长轮询comet服务器反推技术)_第11张图片
以上代码为什么存在空指针呢?而且第二次请求的Response 和第一次一样。
* 由于客户端向服务器发送数据后,服务器响应完毕数据后,关闭 response ,但是关闭可能还没有完毕,客户端又来连接(两次请求的是同一个连接,但是服务器的 response 已经处于关闭)。


* 每一个客户端看到的数据不同,因为连接后每次都创建一个新的线程来发送数据。

基于以上问题需要创建一个专门发送消息的线程类。该类的作用就是输出消息给客户端。
* 该线程启动后首先判断客户端和服务器的链接有没有创建起来,如果没有,当前线程就挂起。
* 如果服务器创建连接后调用setConnetion设置好连接后,唤醒发送消息的线程

WEB聊天系统(基于长轮询comet服务器反推技术)_第12张图片
/**
发送消息的工具类,该类要想能够发送消息,必须满足2个要求
1.客户端和服务器建立好连接
2.有消息
*/


public  class MessageSender  implements Runnable{

         private  boolean isRunning =  true;
         private String message;
         private  boolean hashMessage;
        //HttpServletResponse就是連接
        //private HttpServletResponse connection;
         private Map connetions =  new HashMap<>();
         private Object key =  new Object();
         private  boolean  isPoll =  true;

// 当客户端和服务器创建好链接后,唤醒发送功能发送消息
         public  void setConnetion(String sessionID,HttpServletResponse connection) {
                 synchronized (key) {
                        connetions.put(sessionID, connection);
                        key.notify();
                }

        }


         public   synchronized  void setMessage(String message) {
                 this.message = message;
                hashMessage =  true;
                 this.notify();
        }

        @Override
         public  void run() {
                System. out.println("启动消息发送线程");
                 while(isRunning) {
                        System. out.println("开始发送消息");
                        //当客户端与服务器创建起来链接后就发送消息,如果没有就挂起。
                         if(connetions.size() <= 0) {
                                 synchronized (key) {
                                         try {
                                                key.wait();
                                        }  catch (InterruptedException e) {
                                                e.printStackTrace();
                                        }
                                }

                        }

                        //消息必须有才能发送
                         if(!hashMessage) {

                                 try {
                                         synchronized ( this) {
                                                 this.wait();
                                        }

                                }  catch (Exception e) {
                                        e.printStackTrace();
                                }

                        }

                         try {
                                String message =  this.message;
                                hashMessage =  false;
                                 this.message =  null;
                                 for(HttpServletResponse connection : connetions.values()){
                                        connection.getWriter().write(message);
                                }


                                 if(isPoll) {
                                         for(HttpServletResponse connection : connetions.values()){
                                                connection.getWriter().close();
                                                connection =  null;

                                        }
connetions.clear();


                                }
                        }  catch (IOException e) {
                                e.printStackTrace();
                        }


                }
        }

         public  void close() {
                isRunning =  false;
        }

}



以上发送消息的线程类写好之后,我们就可以基于该线程类来完成Web 聊天。

最后我们使用长轮询的技术完成web聊天
步骤一:定义我们的登录页面和登录的Servlet
这个比较简单
WEB聊天系统(基于长轮询comet服务器反推技术)_第13张图片
WEB聊天系统(基于长轮询comet服务器反推技术)_第14张图片

步骤二:定义聊天的界面chat.jsp

WEB聊天系统(基于长轮询comet服务器反推技术)_第15张图片 
WEB聊天系统(基于长轮询comet服务器反推技术)_第16张图片 

步骤3::轮询的Servlet
下面及长轮询的Servlet URL 就是 comet.action

package cn.itcast.servlet;


import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.catalina.comet.CometEvent;
import org.apache.catalina.comet.CometEvent.EventType;
import org.apache.catalina.comet.CometProcessor;


/**
长连接的轮询 Servlet
*
*/
public class ChatServlet extends HttpServlet implements CometProcessor {
        private static final long serialVersionUID = 1L;
        /*
         * MessageSender 是用来发送消息的线程
         */
        private MessageSender sender;
        /*
         *  用来存放聊天的人
         */
        private Map users = new HashMap<>();

        /**
         *
         */

        @Override
        public void init(ServletConfig config) throws ServletException {
                // 初始化消息发送线程
                // 该线程必须有连接,并且有数据才发送给客户端数据
                sender = new MessageSender();
                new Thread(sender).start();

        }

        @Override
        public void event(CometEvent event) throws IOException, ServletException {
                 HttpServletResponse resp = event.getHttpServletResponse();
                resp.setContentType("application/json;charset=utf-8");
                HttpServletRequest request = event.getHttpServletRequest();
request.setCharacterEncoding("utf-8");

                if(request.getSession().getAttribute("loginUser") == null) {
                        request.getRequestDispatcher("/chat.jsp").forward(request, resp);
                        return;
                }


                if(event.getEventType() == EventType.BEGIN) {
                        // 连接已经建立
                        // 模拟发送数据给客户端
                        System.out.println(" 服务器和客户端建立了连接 "+" 客户端的 sessionid " + request.getSession().getId());
                        System.out.println(" 线程开启 ");
                        sender.setConnetion(request.getSession().getId(),resp);

                        String sessionID = request.getSession().getId();
                        String loginUser = (String) request.getSession().getAttribute("loginUser");
                        // 如果用户是新登录来到这个 Servlet 就将用户信息以及 sessionId 放到 users

                        MessageBean messageBean = new MessageBean();
                        List us = new ArrayList<>();
                        // 表示该用户是第一次登录
                        if(!users.containsKey(sessionID)) {
                                users.put(sessionID, loginUser);
                                us.addAll(users.values());
                                messageBean.setUsers(us);
                                messageBean.setChat(" 欢迎 " + loginUser +" 登录 ");
                                // 如果客户端是第一次登录
                                // 将数据发送给客户端
                                sender.setMessage(JSONUtil.pojo2JSON(messageBean));
                        }else {

                                // 如果客户端不是第一次登录,且发送了数据,就通过输出消息线程,将消息信息响应给客户端
                                String talk = request.getParameter("talk");
                                if(talk != null && !talk.equals("")) {
                                        us.addAll(users.values());
                                        messageBean.setUsers(us);
                                        messageBean.setChat(loginUser + " " + talk);
                                        sender.setMessage(JSONUtil.pojo2JSON(messageBean));
                                        // 如果客户端说话了
                                }

                                // 否则就不做任何事情
                        }




                }else if(event.getEventType() == EventType.END) {
                        // 客户端连接已经断开
                        //event.close();
                }else if(event.getEventType() == EventType.ERROR) {
                        System.out.println(" 服务器出错 ");
                        //event.close();
                }

        }


}
JSON  的辅助工具类
WEB聊天系统(基于长轮询comet服务器反推技术)_第17张图片
消息POJO类
WEB聊天系统(基于长轮询comet服务器反推技术)_第18张图片 
最后效果
WEB聊天系统(基于长轮询comet服务器反推技术)_第19张图片 

你可能感兴趣的:(技术交流)