AndroidPN服务器源码简要分析

本文简要分析AndroidPN项目的源码,重点探讨Mina网络库在服务器和客户端中的应用,以及Spring MVC如何与Mina结合。文章提及服务器端的TimeServerHandler和自定义编解码,客户端的实现待续。此外,还介绍了服务器与客户端的数据交互过程,以及项目结构和用户在线状态的处理流程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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谢谢

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值