16 December 2006

這週整週都在搞 MSN,想要替便當系統加個即時通知的功能 (團購開啟後,用 MSN 通知所有人)。 基本上是採用韓國開發的 JMsn Library來實作, 在 web server 這一端養一隻 msn robot,然後利用 robot發送訊息,以下僅摘錄發送信息的程式碼片段給大家參考:

public boolean testSendMessage(final MSNMessenger messenger, 
          final String account, final String message) {

	MsnAdapter sender = null;
	try {
	    //確認訊息已發出,用 countdown 1
	
		final CountDownLatch sentLatch = new CountDownLatch(1);
		建立 listener 等待 session 開啟
		sender = new MsnAdapter() {
	
	        //當好友被繳請後,會通知 (呼叫) 這個 method 。
			@Override
			public void whoJoinSession(SwitchboardSession allSession,
					MsnFriend msnfriend) {
	
				if (!msnfriend.getLoginName().equals(account)) {
					return; //如果是繳請別人,則離開
				}
	
				//如果已通知過了,則離開。
				if (sentLatch.getCount() == 0) return;
	
				try {
					//送訊息
					allSession.sendInstantMessage(new MimeMessage(message));
					//送完訊息後倒數為 0,通知主 thread 已經通知完成了。
					sentLatch.countDown();
					logger.debug("done");
				} catch (Exception e) {
					logger.error("failed", e.getMessage());
				}
			}
		};

		//將 listner 加進 MSNMessenger 內,這樣就能接到 server 端的通知。
		messenger.addMsnListener(sender);
	
	    //呼叫好友
		messenger.doCall(account);
	
	    //等待 20 秒,如果沒有回應則跳出。
		if (sentLatch.await(20, TimeUnit.SECONDS)) {
			return true; //成功
		} else {
			logger.debug("fail to call friend " + account );
			return false; //失敗
		}
	} catch (Exception e) {
		logger.error(e.getMessage(), e);
		throw e;
	} finally {

		//將 session 和 listener 清除。
		if (messenger != null) {
			if (sender != null) {
				try {
					messenger.removeMsnListener(sender);
				} catch (RuntimeException ignore) {}
			}
			try {
				SwitchboardSession openedSession = messenger
						.findSwitchboardSessionAt(account);
				if (openedSession != null) {
					openedSession.cleanUp();
				}
			} catch (RuntimeException ignore) {}
		}
	}
}

簡單來說,就是替 MSNMessenger 先掛上一個 listener,然後以 MSNMessenger 繳請好友, 開一個 SwitchboardSession。待繳請成功之後,listener 的 whoJoinSession() 會被 fired,接著再發送 訊息。最後一步再把開啟的 session 和 listener 都清除乾淨。

理論上是這樣做啦,實際運作時也是行得通,可惜的是 MSN 的 Server 會擋 "rapid request",換句話說, 如果你開太多連線,太多 session,request 過多... etc,後面的 request 都會被 reject ,server 會回傳個 800 錯誤訊息給你。那多少算太多?我自己測試,大概 10 個 session,MSN server 就會 reject, 我還另外減慢 request 的頻率,改成每 5 秒開一次 session,但是 MSN server 還是 reject...

上限只有 10 個,在訂便當系統裡,一次團購就十幾二十個人,要同時通知就得開 20 個 session, 更何況同時間會有好幾個群組團購... 所以就玩完啦... 唉...不知有無其他方法可以規避 MSN server 的限制? 現在 MSN 行不通了,我還想過像是 RSS 和 Mail 等類型的服務,但它們都不具備及時性,所以不成。 而其他及時服務的安裝人數不多,而且大概也有類似的限制吧,我猜...

團購開啟時,若能夠及時的通知,呼朋引伴吸引人氣,團購的成功率便可大大的提高,可惜美夢破碎...

最後一些相關資料:

1. 目前查知 MSN 的好友人數上限是 600 人,一般人是夠用了,但如果是 robot 就不夠啦,要多建幾隻預備。

2. 目前 JMsn library (v1.3) 裡,其 MSNMessenger 在 logout() 時會有 thread leak 的問題, "即 MSNMessenger 裡的 NotificationProcessor 裡的 CallbackCleaner 在網路不正常時 logout 不會被清除", 看不懂?反正解決方法就用 reflection 硬把它殺掉就是了:

//修正後的 logout:
public void fixedLogout(MSNMessenger messenger) {
	if (messenger != null) {
		Thread leakedThread = null;
		try {
			leakedThread = getLeakedThread(messenger);
			messenger.logout();
		} catch (Exception ignore) {

		} finally {
			if (leakedThread != null) {
				if (!leakedThread.isInterrupted()) {
					leakedThread.interrupt();
				}
			}
		}
	}
}
/**
 * current MSNMessenger do not terminate internal callback thread if
 * messenger not logined.
 */
private Thread getLeakedThread(MSNMessenger messenger) {
	try {
		Field nsField = MSNMessenger.class.getDeclaredField("ns");
		nsField.setAccessible(true);
		NotificationProcessor ns = (NotificationProcessor) nsField
				.get(messenger);
		if (ns == null) return null;
		Field callbackField = NotificationProcessor.class
				.getDeclaredField("callbackCleaner");
		callbackField.setAccessible(true);
		return (Thread) callbackField.get(ns);
	} catch (SecurityException e) {
		throw new RuntimeException("unexpected", e);
	} catch (NoSuchFieldException e) {
		throw new RuntimeException("unexpected", e);
	} catch (IllegalAccessException e) {
		throw new RuntimeException("unexpected", e);
	}
}

不過我自己大概再也用不到了... orz