A股上市公司传智教育(股票代码 003032)旗下技术交流社区北京昌平校区

 找回密码
 加入黑马

QQ登录

只需一步,快速开始

本帖最后由 西安Java组 于 2017-12-30 21:54 编辑

java服务器端程序开发中,通常有这样的需求,就是服务器主动给客户端推送数据。在B/S架构中,通常是客户端向服务器发送请求,然后服务器给客户端做出响应。如果要做一个Web聊天工具,A客户端向服务器发送聊天数据,另一个B客户端需要请求服务器才能获取数据(流程图如下)。


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



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

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

以下模拟B客户端如何不刷新页面获取A客户端的数据。

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


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

要使用基于长连接的服务器反推技术有两种,第一种是基于流的方式。原理如下。






第一步:修改Tomcat的协议

将原来server.xml
<Connector connectionTimeout="20000" port="8080" protocol="HTTP/1.1" redirectPort="8443"/>
改为
<Connector connectionTimeout="20000" port="8080" protocol="org.apache.coyote.http11.Http11NioProtocol" redirectPort="8443"/>

第二步服务器的代码:
首先服务器的代码就不能直接继承HttpServlet,因为直接继承HttpServlet后,不能和客户端进行长时间的链接。所以需要继承CometProcessor.
访问的urlcomet1,然后给客户端循环发送数据。
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中,客户端和服务器的通信状态有一下几种。

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

注意:客户端接受数据的状态并不是在44表示服务器全部发送完毕。而客户端接受数据应该是3
服务器发送一部分数据后就需要获取。这样就不断获取服务器的数据,但是每次发送数据后responseText中的数据就会变多,直接展示会重复。需要截取,截取的代码优化如下。

以上就是基于长连接的流的方式处理服务器反推问题。

程序运行后:


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



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


第一步:服务器的代码
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的时候,获取数据重新发起请求。

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

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


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

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

/**
发送消息的工具类,该类要想能够发送消息,必须满足2个要求
1.客户端和服务器建立好连接
2.有消息
*/


public class MessageSender implements Runnable{

        private boolean isRunning = true;
        private String message;
        private boolean hashMessage;
        //HttpServletResponse就是連接
        //private HttpServletResponse connection;
        private Map<String,HttpServletResponse> 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
这个比较简单

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




步骤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<String,String> 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<String> 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 的辅助工具类
消息POJO类

最后效果


5 个回复

正序浏览
棒棒棒~
回复 使用道具 举报
哇,好棒
回复 使用道具 举报
文章写得不错,有没有源码呀,我们的代码需要这个技术,或者老师能留下您的QQ吗?
回复 使用道具 举报
尊敬的黑马老师,我们正在做一个项目,目前客户那边的浏览器基本都是IE,不支持websorket,感觉你这个功能好像在IE上面支持,能不能把你的qq留一下,我想请教您一个问题。谢谢啦
回复 使用道具 举报
回复 使用道具 举报
您需要登录后才可以回帖 登录 | 加入黑马