• 学习Pushlet(三):后台服务代码详解


    首先看我们的Pushlet类里面的init()方法

    public class Pushlet extends HttpServlet implements Protocol {
    
        public void init() throws ServletException {
            try {
                // Load configuration (from classpath or WEB-INF root path)
                String webInfPath = getServletContext().getRealPath("/") + "/WEB-INF";
                Config.load(webInfPath);
    
                Log.init();
    
                // Start
                Log.info("init() Pushlet Webapp - version=" + Version.SOFTWARE_VERSION + " built=" + Version.BUILD_DATE);
    
                // Start session manager
                SessionManager.getInstance().start();
    
                // Start event Dispatcher
                Dispatcher.getInstance().start();
    
    
                if (Config.getBoolProperty(Config.SOURCES_ACTIVATE)) {
                    EventSourceManager.start(webInfPath);
                } else {
                    Log.info("Not starting local event sources");
                }
            } catch (Throwable t) {
                throw new ServletException("Failed to initialize Pushlet framework " + t, t);
            }
        }

    其中Config.load方法是获取pushlet的全局配置,方法里面它会去classes下读取pushlet.properties,如果读取不到才会到WEB-INF下面读取,配置信息会保存在一个Properties对象里面供其它类使用。

    Config是个工厂类,里面有getClass方法:

    /**
         * Factory method: create object from property denoting class name.
         *
         * @param aClassNameProp property name e.g. "session.class"
         * @return a Class object denoted by property
         * @throws PushletException when class cannot be instantiated
         */
        public static Class getClass(String aClassNameProp, String aDefault) throws PushletException {
            // Singleton + factory pattern:  create object instance
            // from configured class name
            String clazz = (aDefault == null ? getProperty(aClassNameProp) : getProperty(aClassNameProp, aDefault));
    
            try {
                return Class.forName(clazz);
            } catch (ClassNotFoundException t) {
                // Usually a misconfiguration
                throw new PushletException("Cannot find class for " + aClassNameProp + "=" + clazz, t);
            }
        }

    方法返回一个Class对象,其中入参aClassNameProp为properties中配置的一个key,通过这个key获取value(类的全路径)后返回一个Class对象,代码里面很多地方都是使用了这里的工厂模式,看一下SessionManager中的应用:

    /**
    
     * Singleton pattern: single instance.
    
    */
    private static SessionManager instance;
    
    static {
    // Singleton + factory pattern: create single instance
    // from configured class name
    try {
    instance = (SessionManager) Config.getClass(SESSION_MANAGER_CLASS, "nl.justobjects.pushlet.core.SessionManager").newInstance();
    Log.info("SessionManager created className=" + instance.getClass());
    } catch (Throwable t) {
    Log.fatal("Cannot instantiate SessionManager from config", t);
    }
    }
    public static final String SESSION_MANAGER_CLASS = "sessionmanager.class";

    在pushlet.properties中:

    sessionmanager.class=nl.justobjects.pushlet.core.SessionManager

    SessionManager.getInstance()返回一个单例对象,这里并没有通过构造函数初始化而是像上面那样获取,这样的好处是扩展性好,可以在pushlet.properties中改掉sessionmanager.class,使用自定义的SessionManager实现其它功能,比如我在做单点推送的时候就用到了自己扩展的SessionManager,后面例子中会详细介绍为什么要这样修改。

    SessionManager:会话管理,在pushlet中每一个客户端的都会生成一个Session(id唯一)并保存在SessionManager中,这个Session跟浏览器HttpSession意图相似用以保持浏览器跟pushlet server的通信

    SessionManager.getInstance().start(); 会启动一个TimerTask,每隔一分钟会检测所有Session是否失效,每个Session会保存一个timeToLive (存活时间),这个也可以在pushlet.properties中配置默认是5分钟,当浏览器发送新的请求时会重置timeToLive为默认值,也就是说如果5分钟内没有收到浏览器请求则此Session过期会做一系列操作。

    Dispatcher.getInstance().start();只是一些初始化,做的事情不多。里面有个内部类,当调用multicast、unicast等发布事件时都会委托到这个内部类中。

    if (Config.getBoolProperty(Config.SOURCES_ACTIVATE)) {
    EventSourceManager.start(webInfPath);
    } else {
    Log.info("Not starting local event sources");
    }

    这里判断配置中的

    # should local sources be loaded ?
    sources.activate=false

    是否为true,true时会去读取sources.properties文件,启动定时推送(网上例子很多)。默认是true,也就是默认必须有sources.properties文件,否则启动servlet报错。
    到此init方法结束。

    doGet,doPost都会把request里面的参数封装到一个Event里面,最后调用doRequest:

    /**
    * Generic request handler (GET+POST).
    */
    protected void doRequest(Event anEvent, HttpServletRequest request, HttpServletResponse response) {
    // Must have valid event type.
    String eventType = anEvent.getEventType();
    try {
    
    // Get Session: either by creating (on Join eventType)
    // or by id (any other eventType, since client is supposed to have joined).
    Session session = null;
    if (eventType.startsWith(Protocol.E_JOIN)) {
    // Join request: create new subscriber
    session = SessionManager.getInstance().createSession(anEvent);
    
    String userAgent = request.getHeader("User-Agent");
    if (userAgent != null) {
    userAgent = userAgent.toLowerCase();
    } else {
    userAgent = "unknown";
    }
    session.setUserAgent(userAgent);
    
    } else {
    // Must be a request for existing Session
    
    // Get id
    String id = anEvent.getField(P_ID);
    
    // We must have an id value
    if (id == null) {
    response.sendError(HttpServletResponse.SC_BAD_REQUEST, "No id specified");
    Log.warn("Pushlet: bad request, no id specified event=" + eventType);
    return;
    }
    
    // We have an id: get the session object
    session = SessionManager.getInstance().getSession(id);
    
    // Check for invalid id
    if (session == null) {
    response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Invalid or expired id: " + id);
    Log.warn("Pushlet: bad request, no session found id=" + id + " event=" + eventType);
    return;
    }
    }
    
    // ASSERTION: we have a valid Session
    
    // Let Controller handle request further
    // including exceptions
    Command command = Command.create(session, anEvent, request, response);
    session.getController().doCommand(command);
    } catch (Throwable t) {
    // Hmm we should never ever get here
    Log.warn("Pushlet: Exception in doRequest() event=" + eventType, t);
    t.printStackTrace();
    response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
    }
    
    }

    当eventType为join、join_listen时代表浏览器第一次请求,会创建Session:

    /**
    * Create new Session (but add later).
    */
    public Session createSession(Event anEvent) throws PushletException {
    // Trivial
    return Session.create(createSessionId());
    }

    createSessionId()会参数一个随机字符串(随机且不重复)后调用Session.create方法,Session源码如下:

    package nl.justobjects.pushlet.core;
    
    import nl.justobjects.pushlet.util.Log;
    import nl.justobjects.pushlet.util.PushletException;
    
    /**
    * Represents client pushlet session state.
    *
    * @author Just van den Broecke - Just Objects ©
    * @version $Id: Session.java,v 1.8 2007/11/23 14:33:07 justb Exp $
    */
    public class Session implements Protocol, ConfigDefs {
    private Controller controller;
    private Subscriber subscriber;
    
    private String userAgent;
    private long LEASE_TIME_MILLIS = Config.getLongProperty(SESSION_TIMEOUT_MINS) * 60 * 1000;
    private volatile long timeToLive = LEASE_TIME_MILLIS;
    
    public static String[] FORCED_PULL_AGENTS = Config.getProperty(LISTEN_FORCE_PULL_AGENTS).split(",");
    
    private String address = "unknown";
    private String format = FORMAT_XML;
    
    private String id;
    
    /**
    * Protected constructor as we create through factory method.
    */
    protected Session() {
    }
    
    /**
    * Create instance through factory method.
    *
    * @param anId a session id
    * @return a Session object (or derived)
    * @throws PushletException exception, usually misconfiguration
    */
    public static Session create(String anId) throws PushletException {
    Session session;
    try {
    session = (Session) Config.getClass(SESSION_CLASS, "nl.justobjects.pushlet.core.Session").newInstance();
    } catch (Throwable t) {
    throw new PushletException("Cannot instantiate Session from config", t);
    }
    
    // Init session
    session.id = anId;
    session.controller = Controller.create(session);
    session.subscriber = Subscriber.create(session);
    return session;
    }
    
    /**
    * Return (remote) Subscriber client's IP address.
    */
    public String getAddress() {
    return address;
    }
    
    /**
    * Return command controller.
    */
    public Controller getController() {
    return controller;
    }
    
    /**
    * Return Event format to send to client.
    */
    public String getFormat() {
    return format;
    }
    
    /**
    * Return (remote) Subscriber client's unique id.
    */
    public String getId() {
    return id;
    }
    
    /**
    * Return subscriber.
    */
    public Subscriber getSubscriber() {
    return subscriber;
    }
    
    /**
    * Return remote HTTP User-Agent.
    */
    public String getUserAgent() {
    return userAgent;
    }
    
    /**
    * Set address.
    */
    protected void setAddress(String anAddress) {
    address = anAddress;
    }
    
    /**
    * Set event format to encode.
    */
    protected void setFormat(String aFormat) {
    format = aFormat;
    }
    
    /**
    * Set client HTTP UserAgent.
    */
    public void setUserAgent(String aUserAgent) {
    userAgent = aUserAgent;
    }
    
    /**
    * Decrease time to live.
    */
    public void age(long aDeltaMillis) {
    timeToLive -= aDeltaMillis;
    }
    
    /**
    * Has session timed out?
    */
    public boolean isExpired() {
    return timeToLive <= 0;
    }
    
    /**
    * Keep alive by resetting TTL.
    */
    public void kick() {
    timeToLive = LEASE_TIME_MILLIS;
    }
    
    public void start() {
    SessionManager.getInstance().addSession(this);
    }
    
    public void stop() {
    subscriber.stop();
    SessionManager.getInstance().removeSession(this);
    }
    
    /**
    * Info.
    */
    public void info(String s) {
    Log.info("S-" + this + ": " + s);
    }
    
    /**
    * Exceptional print util.
    */
    public void warn(String s) {
    Log.warn("S-" + this + ": " + s);
    }
    
    /**
    * Exceptional print util.
    */
    public void debug(String s) {
    Log.debug("S-" + this + ": " + s);
    }
    
    public String toString() {
    return getAddress() + "[" + getId() + "]";
    }
    }
    
    /*
    * $Log: Session.java,v $
    * Revision 1.8 2007/11/23 14:33:07 justb
    * core classes now configurable through factory
    *
    * Revision 1.7 2005/02/28 15:58:05 justb
    * added SimpleListener example
    *
    * Revision 1.6 2005/02/28 12:45:59 justb
    * introduced Command class
    *
    * Revision 1.5 2005/02/28 09:14:55 justb
    * sessmgr/dispatcher factory/singleton support
    *
    * Revision 1.4 2005/02/25 15:13:01 justb
    * session id generation more robust
    *
    * Revision 1.3 2005/02/21 16:59:08 justb
    * SessionManager and session lease introduced
    *
    * Revision 1.2 2005/02/21 12:32:28 justb
    * fixed publish event in Controller
    *
    * Revision 1.1 2005/02/21 11:50:46 justb
    * ohase1 of refactoring Subscriber into Session/Controller/Subscriber
    *
    *
    */
    
    // Init session
    session.id = anId;
    session.controller = Controller.create(session);
    session.subscriber = Subscriber.create(session);

    同时创建Controller跟Subscriber对象, 它们的create都使用了同样的Config提供的工厂方法创建一个实例,并设置session属性为传入的session,它们跟Session都相互引用,创建Session同时会获取请求头中的User-Agent,记录浏览器特征(id,firefox,chrome...),有些浏览器不支持js流推送时会使用ajax轮询方式。可以看到Session有个id属性, 就是SessionManager里产生的随机字符串,这个id会被传回浏览器,浏览器在后续的pushlet请求中都会带着这个id,就像doRequest里面的判断一样,当不是join或者join_listen时 会主动获取sessionId,并以此获取Session,如果没有则请求失败。
    拿到Session后:

    // ASSERTION: we have a valid Session
    
    // Let Controller handle request further
    // including exceptions
    Command command = Command.create(session, anEvent, request, response);
    session.getController().doCommand(command);
    
    封装一个Command对象交由Controller处理这次请求。
    
    public void doCommand(Command aCommand)
    {
    try
    {
    this.session.kick();
    
    
    this.session.setAddress(aCommand.httpReq.getRemoteAddr());
    
    debug("doCommand() event=" + aCommand.reqEvent);
    
    
    String eventType = aCommand.reqEvent.getEventType();
    if (eventType.equals("refresh")) {
    doRefresh(aCommand);
    } else if (eventType.equals("subscribe")) {
    doSubscribe(aCommand);
    } else if (eventType.equals("unsubscribe")) {
    doUnsubscribe(aCommand);
    } else if (eventType.equals("join")) {
    doJoin(aCommand);
    } else if (eventType.equals("join-listen")) {
    doJoinListen(aCommand);
    } else if (eventType.equals("leave")) {
    doLeave(aCommand);
    } else if (eventType.equals("hb")) {
    doHeartbeat(aCommand);
    } else if (eventType.equals("publish")) {
    doPublish(aCommand);
    } else if (eventType.equals("listen")) {
    doListen(aCommand);
    }
    if ((eventType.endsWith("listen")) || (eventType.equals("refresh"))) {
    getSubscriber().fetchEvents(aCommand);
    } else {
    sendControlResponse(aCommand);
    }
    }
    catch (Throwable t)
    {
    warn("Exception in doCommand(): " + t);
    t.printStackTrace();
    }
    }
    
    首先调用kick重置session的存活时间,然后根据请求中传来的eventType做出相应处理也就是放浏览器写数据。
    if ((eventType.endsWith("listen")) || (eventType.equals("refresh"))) {
    getSubscriber().fetchEvents(aCommand);
    } else {
    sendControlResponse(aCommand);
    }
    
    listen对应了长链接方式,refresh对应了ajax轮询,所以最后数据的写入都是在Subscriber的fetchEvents方法里做的。
    /**
    * Get events from queue and push to client.
    */
    public void fetchEvents(Command aCommand) throws PushletException {
    
    String refreshURL = aCommand.httpReq.getRequestURI() + "?" + P_ID + "=" + session.getId() + "&" + P_EVENT + "=" + E_REFRESH;
    
    // This is the only thing required to support "poll" mode
    if (mode.equals(MODE_POLL)) {
    queueReadTimeoutMillis = 0;
    refreshTimeoutMillis = Config.getLongProperty(POLL_REFRESH_TIMEOUT_MILLIS);
    }
    
    // Required for fast bailout (tomcat)
    aCommand.httpRsp.setBufferSize(128);
    
    // Try to prevent caching in any form.
    aCommand.sendResponseHeaders();
    
    // Let clientAdapter determine how to send event
    ClientAdapter clientAdapter = aCommand.getClientAdapter();
    Event responseEvent = aCommand.getResponseEvent();
    try {
    clientAdapter.start();
    
    // Send first event (usually hb-ack or listen-ack)
    clientAdapter.push(responseEvent);
    
    // In pull/poll mode and when response is listen-ack or join-listen-ack,
    // return and force refresh immediately
    // such that the client recieves response immediately over this channel.
    // This is usually when loading the browser app for the first time
    if ((mode.equals(MODE_POLL) || mode.equals(MODE_PULL))
    && responseEvent.getEventType().endsWith(Protocol.E_LISTEN_ACK)) {
    sendRefresh(clientAdapter, refreshURL);
    
    // We should come back later with refresh event...
    return;
    }
    } catch (Throwable t) {
    bailout();
    return;
    }
    
    
    Event[] events = null;
    
    // Main loop: as long as connected, get events and push to client
    long eventSeqNr = 1;
    while (isActive()) {
    // Indicate we are still alive
    lastAlive = Sys.now();
    
    // Update session time to live
    session.kick();
    
    // Get next events; blocks until timeout or entire contents
    // of event queue is returned. Note that "poll" mode
    // will return immediately when queue is empty.
    try {
    // Put heartbeat in queue when starting to listen in stream mode
    // This speeds up the return of *_LISTEN_ACK
    if (mode.equals(MODE_STREAM) && eventSeqNr == 1) {
    eventQueue.enQueue(new Event(E_HEARTBEAT));
    }
    
    events = eventQueue.deQueueAll(queueReadTimeoutMillis);
    } catch (InterruptedException ie) {
    warn("interrupted");
    bailout();
    }
    
    // Send heartbeat when no events received
    if (events == null) {
    events = new Event[1];
    events[0] = new Event(E_HEARTBEAT);
    }
    
    // ASSERT: one or more events available
    
    // Send events to client using adapter
    // debug("received event count=" + events.length);
    for (int i = 0; i < events.length; i++) {
    // Check for abort event
    if (events[i].getEventType().equals(E_ABORT)) {
    warn("Aborting Subscriber");
    bailout();
    }
    
    // Push next Event to client
    try {
    // Set sequence number
    events[i].setField(P_SEQ, eventSeqNr++);
    
    // Push to client through client adapter
    clientAdapter.push(events[i]);
    } catch (Throwable t) {
    bailout();
    return;
    }
    }
    
    // Force client refresh request in pull or poll modes
    if (mode.equals(MODE_PULL) || mode.equals(MODE_POLL)) {
    sendRefresh(clientAdapter, refreshURL);
    
    // Always leave loop in pull/poll mode
    break;
    }
    }
    }

    这里面可以清楚的看,基于不同的方式数据写入也不同
    // Let clientAdapter determine how to send event
    ClientAdapter clientAdapter = aCommand.getClientAdapter();
    获取ClientAdapter实现,共有三中实现,长链接方式使用BrowserAdapter,ajax轮询方式使用XMLAdapter。
    很明显,长链接方式时while循环将不会结束,除非浏览器发送了leave请求使isActive()为false,或者关掉浏览器。

  • 相关阅读:
    代理模式
    栈和队列----按照左右半区的方式重新组合单链表
    栈和队列----合并两个有序的单链表
    Java中的线程池
    SpringMVC中的参数绑定
    Mybatis常见面试题汇总
    ADC裸机程序
    用uart实现printf函数
    uarts裸机程序
    定时器裸机程序
  • 原文地址:https://www.cnblogs.com/liuyuan1227/p/10884971.html
Copyright © 2020-2023  润新知