学习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,或者关掉浏览器。

猜你喜欢

转载自www.cnblogs.com/liuyuan1227/p/10884971.html