Pushlet2.0.4源码分析及实例(iframe流 + ajax轮询)

来源:互联网 发布:java程序员常用工具 编辑:程序博客网 时间:2024/06/06 01:32

从官网Pushlet下载最新的版本也就是2.0.4,下载的压缩包里面包含了lib,src,webapps几个主要文件夹,lib下面有开发所用到的pushlet.jar,src下面是源代码,webapps下面有个pushlet.war可以直接丢进tomcat跑起来,里面演示了pushlet提供的各种功能。

使用pushlet时需要配置一个servlet,用来接受pushlet的浏览器请求并返回特定的响应配置前端的js。

servlet配置如下:

<servlet><servlet-name>pushlet</servlet-name><servlet-class>nl.justobjects.pushlet.servlet.Pushlet</servlet-class><load-on-startup>1</load-on-startup></servlet><servlet-mapping><servlet-name>pushlet</servlet-name><!-- pattern 与js中pushletURL 对应 --><url-pattern>/pushlet.srv</url-pattern></servlet-mapping>
需要注意的是"/pushlet.srv"是在js中写死的,pattern里面设置的url如果要修改js也要修改

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();// StartLog.info("init() Pushlet Webapp - version=" + Version.SOFTWARE_VERSION + " built=" + Version.BUILD_DATE);// Start session managerSessionManager.getInstance().start();// Start event DispatcherDispatcher.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是个工厂类,

/**

<span style="white-space:pre"></span> * 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 nameString clazz = (aDefault == null ? getProperty(aClassNameProp) : getProperty(aClassNameProp, aDefault));try {return Class.forName(clazz);} catch (ClassNotFoundException t) {// Usually a misconfigurationthrow 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 nametry {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 subscribersession = 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 idString id = anEvent.getField(P_ID);// We must have an id valueif (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 objectsession = SessionManager.getInstance().getSession(id);// Check for invalid idif (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 exceptionsCommand command = Command.create(session, anEvent, request, response);session.getController().doCommand(command);} catch (Throwable t) {// Hmm we should never ever get hereLog.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 {// Trivialreturn Session.create(createSessionId());}

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

// Copyright (c) 2000 Just Objects B.V. <just@justobjects.nl>// Distributable under LGPL license. See terms of license at gnu.org.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 &copy; * @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 sessionsession.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 sessionsession.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 exceptionsCommand 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" modeif (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 eventClientAdapter 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 timeif ((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 clientlong eventSeqNr = 1;while (isActive()) {// Indicate we are still alivelastAlive = Sys.now();// Update session time to livesession.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_ACKif (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 receivedif (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 eventif (events[i].getEventType().equals(E_ABORT)) {warn("Aborting Subscriber");bailout();}// Push next Event to clienttry {// Set sequence numberevents[i].setField(P_SEQ, eventSeqNr++);// Push to client through client adapterclientAdapter.push(events[i]);} catch (Throwable t) {bailout();return;}}// Force client refresh request in pull or poll modesif (mode.equals(MODE_PULL) || mode.equals(MODE_POLL)) {sendRefresh(clientAdapter, refreshURL);// Always leave loop in pull/poll modebreak;}}}

这里面可以清楚的看,基于不同的方式数据写入也不同

// Let clientAdapter determine how to send eventClientAdapter clientAdapter = aCommand.getClientAdapter();
获取ClientAdapter实现,共有三中实现,长链接方式使用BrowserAdapter,ajax轮询方式使用XMLAdapter。

很明显,长链接方式时while循环将不会结束,除非浏览器发送了leave请求使isActive()为false,或者关掉浏览器。

而轮询方式时会在sendRefresh(clientAdapter, refreshURL);后跳出循环,请求也就结束了。


网上大部分都是ajax轮询的例子,我这里说下长链接方式的使用,其实也很简单,除了jar包外,还需要js-pushlet-client.js,和js-pushlet-net.html ,在官网下载的压缩包里都有。

页面引入js后,需要执行p_embed(); 这个在js-pushlet-client.js里面有定义,就是去加载js-pushlet-net.html。

这里面可能需要根据你自己项目的结构配置相应的路径,js-pushlet-net.html复制发送pushlet请求,并保持长链接,写入的js代码会回调到js-pushlet-client.js中去,最后调用到你自己编写的事件回调如onData等。

调用订阅:p_join_listen('subject'); 这里会使用加载的iframe往server发请求,建立session等如上文描述过程。


这是效果图

发布通知:

Event event = Event.createDataEvent("");                event.setField("test", "test");                Dispatcher.getInstance().multicast(event);

/** * Routes Events to Subscribers. * * @author Just van den Broecke - Just Objects &copy; * @version $Id: Dispatcher.java,v 1.9 2007/12/04 13:55:53 justb Exp $ */public class Dispatcher implements Protocol, ConfigDefs {/** * Singleton pattern:  single instance. */private static Dispatcher instance;protected SessionManagerVisitor sessionManagerVisitor;static {try {instance = (Dispatcher) Config.getClass(DISPATCHER_CLASS, "nl.justobjects.pushlet.core.Dispatcher").newInstance();Log.info("Dispatcher created className=" + instance.getClass());} catch (Throwable t) {Log.fatal("Cannot instantiate Dispatcher from config", t);}}/** * Singleton pattern with factory method: protected constructor. */protected Dispatcher() {}/** * Singleton pattern: get single instance. */public static Dispatcher getInstance() {return instance;}/** * Send event to all subscribers. */public synchronized void broadcast(Event anEvent) {try {// Let the SessionManager loop through Sessions, calling// our Visitor Method for each Session. This is done to guard// synchronization with SessionManager and to optimize by// not getting an array of all sessions.Object[] args = new Object[2];args[1] = anEvent;Method method = sessionManagerVisitor.getMethod("visitBroadcast");SessionManager.getInstance().apply(sessionManagerVisitor, method, args);} catch (Throwable t) {Log.error("Error calling SessionManager.apply: ", t);}}/** * Send event to subscribers matching Event subject. */public synchronized void multicast(Event anEvent) {try {// Let the SessionManager loop through Sessions, calling// our Visitor Method for each Session. This is done to guard// synchronization with SessionManager and to optimize by// not getting an array of all sessions.Method method = sessionManagerVisitor.getMethod("visitMulticast");Object[] args = new Object[2];args[1] = anEvent;SessionManager.getInstance().apply(sessionManagerVisitor, method, args);} catch (Throwable t) {Log.error("Error calling SessionManager.apply: ", t);}}/** * Send event to specific subscriber. */public synchronized void unicast(Event event, String aSessionId) {// Get subscriber to send event toSession session = SessionManager.getInstance().getSession(aSessionId);if (session == null) {Log.warn("unicast: session with id=" + aSessionId + " does not exist");return;}// Send Event to subscriber.session.getSubscriber().onEvent((Event) event.clone());}/** * Start Dispatcher. */public void start() throws PushletException {Log.info("Dispatcher started");// Create callback for SessionManager visits.sessionManagerVisitor = new SessionManagerVisitor();}/** * Stop Dispatcher. */public void stop() {// Send abort control event to all subscribers.Log.info("Dispatcher stopped: broadcast abort to all subscribers");broadcast(new Event(E_ABORT));}/** * Supplies Visitor methods for callbacks from SessionManager. */private class SessionManagerVisitor {private final Map visitorMethods = new HashMap(2);SessionManagerVisitor() throws PushletException {try {// Setup Visitor Methods for callback from SessionManager// This is a slight opitmization over creating Method objects// on each invokation.Class[] argsClasses = {Session.class, Event.class};visitorMethods.put("visitMulticast", this.getClass().getMethod("visitMulticast", argsClasses));visitorMethods.put("visitBroadcast", this.getClass().getMethod("visitBroadcast", argsClasses));} catch (NoSuchMethodException e) {throw new PushletException("Failed to setup SessionManagerVisitor", e);}}/** * Return Visitor Method by name. */public Method getMethod(String aName) {return (Method) visitorMethods.get(aName);}/** * Visitor method called by SessionManager. */public void visitBroadcast(Session aSession, Event event) {aSession.getSubscriber().onEvent((Event) event.clone());}/** * Visitor method called by SessionManager. */public void visitMulticast(Session aSession, Event event) {Subscriber subscriber = aSession.getSubscriber();Event clonedEvent;Subscription subscription;// Send only if the subscriber's criteria// match the event.if ((subscription = subscriber.match(event)) != null) {// Personalize eventclonedEvent = (Event) event.clone();// Set subscription id and optional labelclonedEvent.setField(P_SUBSCRIPTION_ID, subscription.getId());if (subscription.getLabel() != null) {event.setField(P_SUBSCRIPTION_LABEL, subscription.getLabel());}subscriber.onEvent(clonedEvent);}}}}

所有通知的发布,都是在SessionManagerVisitor里完成,start方法中实例化了这个对象(servlet启动时调用)

broadcast向所有订阅者发布消息,无需匹配subject;

multicast向所有匹配了subject的订阅者发布消息;

subject 支持级联和多事件

比如:

p_join_listen('/subject1, /subject2');//这里'/'不是必须只要是字符串就行,官网的demo都是带'/'

订阅这个事件时,下面几个通知都会被订阅

"/subject1";"/subject2";"/subject1234"; "/subject232";

unicast向指定的订阅者发布通知,需要传入sessionId;

上文中已经详述默认生成Session时都会生成一个唯一且随机的id,每个浏览器对以一个sessinId,这个id会贯穿请求始终。

如果我要向特定的浏览器的推送消息,就必须获取到这个sessionId, 因此采用随机生成的id实现单点推送很不方便(要实现是可以的,比如在前端获取到sessionId后系统中用户标识(userId, userName)进行绑定,一一关联后,如果要向特定用户推送可以根据用户标识获取绑定的sessionId,进行unicast)。

如果在生成Session的时候就使用userId作为Session的id,那么就可以在任何地方通过userId进行推送。通过扩展SessionManager可以实现这个功能。

session = SessionManager.getInstance().createSession(anEvent);

createSession的本来实现是这样的:

/** * Create new Session (but add later). */public Session createSession(Event anEvent) throws PushletException {// Trivialreturn Session.create(createSessionId());}
我们要重写这个方法, 前文已经提到可以在pushlet.properties中指定类名来让pushlet使用外部定义的类。

import nl.justobjects.pushlet.core.Event;import nl.justobjects.pushlet.core.Session;import nl.justobjects.pushlet.core.SessionManager;import nl.justobjects.pushlet.util.PushletException;/** * 重写createSession方法,产生与系统用户绑定的Session * @author main 2014年4月9日 下午2:03:39 * @version V1.0 */public class CustomSessionManager extends SessionManager {@Overridepublic Session createSession(Event anEvent) throws PushletException {//从传入的anEvent中获取usId字段用作sessionId, 如果没有(未登录用户)则随机生成String sessionId = anEvent.getField("usId", createSessionId());return Session.create(sessionId);}}
并修改配置:

sessionmanager.class=com.xxx.xxx.CustomSessionManager

js-pushlet-client.js中修改少许代码在请求发送时传入usId参数即可。

然后通过调用Dispatcher.getInstance().unicast(event,1); 实现往id为1的用户推送消息。


写的不好,大家见谅。希望对新手有所帮助。











2 0