首先看我们的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,或者关掉浏览器。