AndroidPN
https://github.com/dannytiehui/androidpn
郭霖 Android消息推送教学视频
http://www.imooc.com/learn/223
http://www.imooc.com/learn/358
Mina
官网 http://mina.apache.org/mina-project/index.html
资源
http://my.oschina.net/ielts0909/blog/92716?p=1#comments
http://my.oschina.net/makemyownlife/blog/122738
XMPP
官网 http://xmpp.org/
资源 http://www.cnblogs.com/hanyonglu/archive/2012/03/04/2378956.html
Mina
百度百科
ApacheMINA是一个网络应用程序框架,用来帮助用户简单地开发高性能和高可扩展性的网络应用程序。它提供了一个通过Java NIO在不同的传输例如TCP/IP和UDP/IP上抽象的事件驱动的异步API。
服务器
public class MinaTimeServer
{
public static void main( String[] args ) throws IOException
{
IoAcceptor acceptor = new NioSocketAcceptor();
//日志过滤器
acceptor.getFilterChain().addLast( "logger", new LoggingFilter() );
//ProtocolCodecFilter该过滤器将二进制或者协议特定的数据转换为消息对象
//TextLineCodecFactory处理文本信息(编解码处理)
acceptor.getFilterChain().addLast( "codec", new ProtocolCodecFilter(new
TextLineCodecFactory( Charset.forName( "UTF-8" ))));
//设置逻辑处理器(服务于客户端的请求)
acceptor.setHandler( new TimeServerHandler() );
acceptor.getSessionConfig().setReadBufferSize( 2048 );
//设置空闲时间
acceptor.getSessionConfig().setIdleTime( IdleStatus.BOTH_IDLE, 10 );
}
}
TimeServerHandler
public class TimeServerHandler extends IoHandlerAdapter
{
@Override
public void exceptionCaught( IoSession session, Throwable cause ) throws
Exception
{
cause.printStackTrace();
}
@Override
public void messageReceived( IoSession session, Object message ) throws Exception
{//服务器接收请求
String str = message.toString();
if( str.trim().equalsIgnoreCase("quit") ) {
session.close();
return;
}
Date date = new Date();
session.write( date.toString() );
System.out.println("Message written...");
}
@Override
public void sessionIdle( IoSession session, IdleStatus status ) throws Exception
{//服务器进入空闲状态
System.out.println( "IDLE " + session.getIdleCount( status ));
}
sessionOpen()...//服务器与客户端连接开启
sessionCreated()...//服务器与客户端连接创建
sessionClose()...//服务器与客户端连接关闭
messageSent()...//服务器发送消息,消息发送事件触发,当消息即响应发送(调用IoSession.write())
}
自定义编解码
http://www.himigame.com/apache-mina/839.html
http://blog.youkuaiyun.com/a600423444/article/details/6671035
(以下代码出处 郭霖消息推送视频)
XmppDecoder
public class XmppDecoder extends CumulativeProtocolDecoder {
// private final Log log = LogFactory.getLog(XmppDecoder.class);
@Override
public boolean doDecode(IoSession session, IoBuffer in,
ProtocolDecoderOutput out) throws Exception {
int startPosition = in.position();
while(in.hasRemaing()){
byte b = in.get();
if(b=='\n'){
int currentPosition = in.position();
int limit = in.limit();
in.position(startPosition);
in.limit(currentPosition);
IoBuffer buf= in.slince();
byte[] dest = new byte[buf.limit()];
buf.get(dest);
String str = new String(dest);
out.write(str);
in.position(currentPosition);
in.limit(limit);
}
}
}
}
XmppEncoder
public class XmppEncoder implements ProtocolEncoder {
public void encode(IoSession session, Object message,
ProtocolEncoderOutput out) throws Exception {
// log.debug("encode()...");
String s =null;
if(message instanceof String){
s = (String)message;
}
if(s!=null){
CharsetEncoder charsetEncoder =(CharsetEncoder)
session.getAttribute("encoder");
if(charsetEncoder==null){
charsetEncoder = Charset.defaultCharset().newEncoder();
session.setAttribute("encoder",charsetEncoder);
}
IoBuffer ioBuffer = IoBuffer.allocate(s.length);
ioBuffer.setAutoExpanded(true);
ioBuffer.putString(s,charsetEncoder);
ioBuffer.flip();
out.write(ioBuffer);
}
public void dispose(IoSession session) throws Exception {
// log.debug("dispose()...");
}
}
XmppFactory
public class XmppCodecFactory implements ProtocolCodecFactory {
private final XmppEncoder encoder;
private final XmppDecoder decoder;
/**
* Constructor.
*/
public XmppCodecFactory() {
encoder = new XmppEncoder();
decoder = new XmppDecoder();
}
/**
* Returns a new (or reusable) instance of ProtocolEncoder.
*/
public ProtocolEncoder getEncoder(IoSession session) throws Exception {
return encoder;
}
/**
* Returns a new (or reusable) instance of ProtocolDecoder.
*/
public ProtocolDecoder getDecoder(IoSession session) throws Exception {
return decoder;
}
}
客户端
待续
Spring MVC + Mina
AndroidPN服务器端采用Spring MVC架构,那么是如何配置Mina的呢?
http://my.oschina.net/u/2319071/blog/465369
spring-config
<!--Mina逻辑处理器Handler-->
<bean id="xmppHandler" class="org.androidpn.server.xmpp.net.XmppIoHandler" />
<!--Mina过滤器配置-->
<bean id="filterChainBuilder"
class="org.apache.mina.core.filterchain.DefaultIoFilterChainBuilder">
<property name="filters">
<map>
<entry key="executor">
<bean class="org.apache.mina.filter.executor.ExecutorFilter" />
</entry>
<entry key="codec">
<bean class="org.apache.mina.filter.codec.ProtocolCodecFilter">
<constructor-arg>
<!--配置自定义编码工厂-->
<bean class="org.androidpn.server.xmpp.codec.XmppCodecFactory" />
</constructor-arg>
</bean>
</entry>
<!--
<entry key="logging">
<bean class="org.apache.mina.filter.logging.LoggingFilter" />
</entry>
-->
</map>
</property>
</bean>
<!--注入Mina NioSocketAcceptor-->
<bean id="ioAcceptor" class="org.apache.mina.transport.socket.nio.NioSocketAcceptor"
init-method="bind" destroy-method="unbind">
<!--设置端口-->
<property name="defaultLocalAddress" value=":5222" />
<!--设置逻辑处理器-->
<property name="handler" ref="xmppHandler" />
<!--设置过滤器-->
<property name="filterChainBuilder" ref="filterChainBuilder" />
<property name="reuseAddress" value="true" />
</bean>
<!--Mina 配置IdleTime空闲时间-->
<bean id="getSessionConfig" factory-bean="ioAcceptor" factory-method="getSessionConfig">
<property name="readerIdleTime" value="30"></property>
</bean>
AndroidPN项目结构
config
conf.security、config.properties、config存keystore、truststore等信息
jdbc
使用hibernate框架配置数据库ORM
spring-config
spring mvc架构 配置bean
dispatcher-servlet
spring mvc架构 负责责任分配
decorators
页面装饰
用户在线状态项目流程
V
页面采用decorators
WEB-INF decorators.xml
<decorators defaultdir="/decorators">
<excludes>
<pattern>/includes/*</pattern>
<pattern>/40*.jsp</pattern>
<pattern>/dwr/*</pattern>
</excludes>
<decorator name="default" page="default.jsp">
<pattern>/*</pattern>
</decorator>
</decorators>
excludes不需要装饰的页面
decorator name装饰器名字 page装饰器文件
pattern 需要使用装饰器的访问地址,可以配置多个
此处为/* 即WEB-INF pages下的所有
default.jsp
位置WebRoot下的decorates内
<div id="page">
<div id="header">
<jsp:include page="/includes/header.jsp"/>
</div>
<div id="content">
<ul id="tabmenu">
<li><a href="/index.do"
class="<c:if test="${topMenu eq 'home'}"><c:out value="current" />
</c:if>">Home</a></li>
<li><a href="/user.do"
class="<c:if test="${topMenu eq 'user'}"><c:out value="current" />
</c:if>">Users</a></li>
<li><a href="/session.do"
class="<c:if test="${topMenu eq 'session'}"><c:out value="current" />
</c:if>">Sessions</a></li>
<li><a href="/notification.do"
class="<c:if test="${topMenu eq 'notification'}"><c:out value="current" />
</c:if>">Notifications</a></li>
</ul>
<div id="tabcontent">
<decorator:body/>
</div>
</div>
<div id="footer">
<jsp:include page="/includes/footer.jsp"/>
</div>
</div>
四个选项卡index、 user、 session 、notification,点击各个选项卡会发生什么呢?
C
dispatcher-servlet.xml
<bean id="urlMapping"
class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
<property name="mappings">
<value>
/user_api.do=userapiController
/notification_api.do=notificationapiController
/index.do=filenameController
/user.do=userController
/session.do=sessionController
/notification.do=notificationController
</value>
</property>
<property name="order" value="1" />
</bean>
<bean id="userapiController" class="org.androidpn.server.console.api.UserApiController">
<property name="methodNameResolver" ref="paramResolver" />
</bean>
<bean id="notificationapiController"
class="org.androidpn.server.console.api.NotificationApiController">
<property name="methodNameResolver" ref="paramResolver" />
</bean>
<bean id="filenameController"
class="org.springframework.web.servlet.mvc.UrlFilenameViewController" />
<bean id="userController" class="org.androidpn.server.console.controller.UserController">
<property name="methodNameResolver" ref="paramResolver" />
</bean>
<bean id="sessionController"
class="org.androidpn.server.console.controller.SessionController">
<property name="methodNameResolver" ref="paramResolver" />
</bean>
<bean id="notificationController"
class="org.androidpn.server.console.controller.NotificationController">
<property name="methodNameResolver" ref="paramResolver" />
</bean>
index.do UrlFilenameViewController检查URL,查找到文件请求的文件名,并为之视图的名字,直接跳转到index.jsp页面
user.do 交由UserController进行逻辑控制处理
UserController
public class UserController extends MultiActionController {
private UserService userService;
public UserController() {
//User数据操作服务
userService = ServiceLocator.getUserService();
}
public ModelAndView list(HttpServletRequest request,
HttpServletResponse response) throws Exception {
//Presence顾名思义即用户在线状态
PresenceManager presenceManager = new PresenceManager();
//获取用户列表
List<User> userList = userService.getUsers();
for (User user : userList) {
if (presenceManager.isAvailable(user)) {//判断用户状态,设置用户在线状态
// Presence presence = presenceManager.getPresence(user);
user.setOnline(true);
} else {
user.setOnline(false);
}
// logger.debug("user.online=" + user.isOnline());
}
ModelAndView mav = new ModelAndView();
//当前视图数据设置
mav.addObject("userList", userList);
//当前视图位置WEB_INF的pages下的user内的list.jsp
mav.setViewName("user/list");
//返回视图
return mav;
}
}
M
UserService
获取userservice bean实例
ApplicationContext context = new ClassPathXmlApplicationContext("spring-config.xml");
userService = context.getBean("userService");
spring-config.xml
<bean id="userDao" class="org.androidpn.server.dao.hibernate.UserDaoHibernate">
<property name="sessionFactory" ref="sessionFactory" />
</bean>
<bean id="userService" class="org.androidpn.server.service.impl.UserServiceImpl">
<property name="userDao" ref="userDao" />
</bean>
UserServiceImpl继承UserService,对UserDao进行数据操作再封装;UserDaoHibernate继承UserDao,对User进行数据操作
UserDaoHibernate
public class UserDaoHibernate extends HibernateDaoSupport implements UserDao {
public User getUser(Long id) {//根据id获取user
return (User) getHibernateTemplate().get(User.class, id);
}
public User saveUser(User user) {//保存user
getHibernateTemplate().saveOrUpdate(user);
getHibernateTemplate().flush();
return user;
}
public void removeUser(Long id) {//根据id删除user
getHibernateTemplate().delete(getUser(id));
}
public boolean exists(Long id) {
User user = (User) getHibernateTemplate().get(User.class, id);
return user != null;
}
@SuppressWarnings("unchecked")
public List<User> getUsers() {//获取所有user
return getHibernateTemplate().find(
"from User u order by u.createdDate desc");
}
@SuppressWarnings("unchecked")
public List<User> getUsersFromCreatedDate(Date createDate) {
return getHibernateTemplate()
.find("from User u where u.createdDate >= ? order by u.createdDate desc",
createDate);
}
@SuppressWarnings("unchecked")
public User getUserByUsername(String username) throws UserNotFoundException {//根据用户名获取user
List users = getHibernateTemplate().find("from User where username=?",
username);
if (users == null || users.isEmpty()) {
throw new UserNotFoundException("User '" + username + "' not found");
} else {
return (User) users.get(0);
}
}
}
User对象需要再hibernate.cfg.xml中配置
<mapping class="org.androidpn.server.model.User" />
服务器与客户端数据交互
服务器接收客户端数据(使用Mina框架)
XmppIoHandler Mina逻辑处理器Handler
...
public void sessionOpened(IoSession session) throws Exception {//服务器与客户端连接开启
log.debug("sessionOpened()...");
log.debug("remoteAddress=" + session.getRemoteAddress());
// Create a new XML parser
XMLLightweightParser parser = new XMLLightweightParser("UTF-8");
session.setAttribute(XML_PARSER, parser);
// Create a new connection
Connection connection = new Connection(session);
session.setAttribute(CONNECTION, connection);
session.setAttribute(STANZA_HANDLER, new StanzaHandler(serverName,
connection));//创建StanzaHandler
}
public void messageReceived(IoSession session, Object message)
throws Exception {//服务器接收客户端请求数据
log.debug("messageReceived()...");
log.debug("RCVD: " + message);
// Get the stanza handler 获取StanzaHandler
StanzaHandler handler = (StanzaHandler) session
.getAttribute(STANZA_HANDLER);
// Get the XMPP packet parser
int hashCode = Thread.currentThread().hashCode();
XMPPPacketReader parser = parsers.get(hashCode);
if (parser == null) {
parser = new XMPPPacketReader();
parser.setXPPFactory(factory);
parsers.put(hashCode, parser);
}
// The stanza handler processes the message
try {
handler.process((String) message, parser);//处理接收过来的数据
} catch (Exception e) {//异常则关闭连接
log.error(
"Closing connection due to error while processing message: "
+ message, e);
Connection connection = (Connection) session
.getAttribute(CONNECTION);
connection.close();
}
}
...
StanzaHandler
process
public void process(String stanza, XMPPPacketReader reader)
throws Exception {
//客户端发过来的数据是否开头为"<stream:stream",若是则创建新的session会话
boolean initialStream = stanza.startsWith("<stream:stream");
if (!sessionCreated || initialStream) {
if (!initialStream) {
return; // Ignore <?xml version="1.0"?>
}
if (!sessionCreated) {
sessionCreated = true;
MXParser parser = reader.getXPPParser();
parser.setInput(new StringReader(stanza));
//创建新的session
createSession(parser);
} else if (startedTLS) {
startedTLS = false;
//TLS处理
tlsNegotiated();
}
return;
}
// If end of stream was requested
if (stanza.equals("</stream:stream>")) {//会话结束
session.close();
return;
}
// Ignore <?xml version="1.0"?>
if (stanza.startsWith("<?xml")) {
return;
}
// Create DOM object
Element doc = reader.read(new StringReader(stanza)).getRootElement();
if (doc == null) {
return;
}
String tag = doc.getName();
if ("starttls".equals(tag)) {
if (negotiateTLS()) { // Negotiate TLS
startedTLS = true;
} else {
connection.close();
session = null;
}
} else if ("message".equals(tag)) {//处理message
processMessage(doc);
} else if ("presence".equals(tag)) {//处理presence
log.debug("presence...");
processPresence(doc);
} else if ("iq".equals(tag)) {//处理IQ
log.debug("iq...");
processIQ(doc);
} else {
log.warn("Unexpected packet tag (not message, iq, presence)"
+ doc.asXML());
session.close();
}
}
createSession 创建session会话
private void createSession(XmlPullParser xpp)
throws XmlPullParserException, IOException {
for (int eventType = xpp.getEventType(); eventType != XmlPullParser.START_TAG;) {
eventType = xpp.next();
}
// Create the correct session based on the sent namespace
String namespace = xpp.getNamespace(null);
if ("jabber:client".equals(namespace)) {
//session实例化
session = ClientSession.createSession(serverName, connection, xpp);
if (session == null) {
StringBuilder sb = new StringBuilder(250);
sb.append("<?xml version='1.0' encoding='UTF-8'?>");
sb.append("<stream:stream from=\"").append(serverName);
sb.append("\" id=\"").append(randomString(5));
sb.append("\" xmlns=\"").append(xpp.getNamespace(null));
sb.append("\" xmlns:stream=\"").append(
xpp.getNamespace("stream"));
sb.append("\" version=\"1.0\">");
// bad-namespace-prefix in the response
StreamError error = new StreamError(
StreamError.Condition.bad_namespace_prefix);
sb.append(error.toXML());
//写出流<?xml version='1.0' encoding='UTF-8'?><stream:steam...
connection.deliverRawText(sb.toString());
connection.close();
log
.warn("Closing session due to bad_namespace_prefix in stream header: "
+ namespace);
}
}
}
Connection的deliverRawText 服务器流写出
private void deliverRawText(String text, boolean asynchronous) {
log.debug("SENT: " + text);
if (!isClosed()) {
IoBuffer buffer = IoBuffer.allocate(text.length());//分配缓存大小
buffer.setAutoExpand(true);//可扩展缓存
boolean errorDelivering = false;
try {
buffer.put(text.getBytes("UTF-8"));
buffer.flip();
if (asynchronous) {
ioSession.write(buffer);//Mina写出iobuffer流
} else {
// Send stanza and wait for ACK
boolean ok = ioSession.write(buffer).awaitUninterruptibly(
Config.getInt("connection.ack.timeout", 2000));
if (!ok) {
log.warn("No ACK was received when sending stanza to: "
+ this.toString());
}
}
} catch (Exception e) {
log.debug("Connection: Error delivering raw text" + "\n"
+ this.toString(), e);
errorDelivering = true;
}
// Close the connection if delivering text fails
if (errorDelivering && asynchronous) {
close();
}
}
}
processIQ
processIQ处理IQ类型的请求数据(IQ类型的数据集成Packet)。
IQRouter路由将IQ数据根据namespace交由给指定IQHandler
IQRouter
private void handle(IQ packet) {
...
IQHandler handler = getHandler(namespace);//根据namespace获取指定的路由器
if (handler == null) {
sendErrorPacket(packet,
PacketError.Condition.service_unavailable);
} else {
handler.process(packet);
}
...
}
用户信息注册User,添加新User,命名空间是jabber:iq:register,选择的IQHandler是IQRegisterHandler
IQRegisterHandler handleIQ
...
userService.saveUser(user);//保存注册成功的用户
...
此文结束,其余细节可自行分析了,XMPP协议需要继续分析学习。
望共勉之。O(∩_∩)O谢谢