基于funambol ds 的云同步服务研究(四)-源码的研究摘记

来源:互联网 发布:黄金罗盘 知乎 编辑:程序博客网 时间:2024/04/28 19:17
 

准备工作

  下载ds源码,  或从funambol官网提供的SVN地址直接检出, 建立成Web Project工程. 主要分为三部分核心代码,  ds-server中的framework和server包, 以及modules组件中的foundation包.

 

项目结构

  Framework是主要是用于处理协议的, server是处理交互流程, fundation主要是处理数据层操作.

  整个项目没有使用spring, hibernate等主流框架, 数据层和服务层结构不像三层开发那么清晰, 使用JDBC方式直接对数据进行操作,  配备了三个数据源,  应用于不同类型的数据操作, 在tomcat 配置 jndi 进行调用. 项目大量使用了beanxml配置.  Log4j在里面配置使用得很好, 对每个重要的处理过程都会有不同的级别控制, 并且设置了MDC动态变量记录交互设备信息, 便于跟踪测试及维护.

 

接口说明

  Funambol的同步接口不像其他soket接口通讯,  有多种接口和复杂的参数, 这里只有一个servlet接口, 通过post方式进行调用,  不用传递其他参数,  只要遵照syncML协议, 可以正常通讯. 数据同步传递的信息可能会过大, 并且移动设备的带宽资源有限, 需要把数据进行压缩,  一般采用Gzip+wbxml方式. 如果数据有压缩, 需要在请求头信息中加以描述.

 

主要代码

  对于比较复杂的代码, 要作深入的研究或改造, 最好是先弄清楚核心的代码及功能, 做好摘记记录下来, 不至于越看越迷糊. 下面是我所摘记的主要代码, 初次看起来会比较繁琐些, 可大致了解下.

  看servlet代码, 首先是解析头部信息参数, 然后获取传递的SyncML数据流, 接下来是查找负责处理核心业务的接口缓存,  如果没有, 则创建该接口.

// 获取客户端传送的二进制数据

requestData = getRequestContent(httpRequest, contentEncoding, sessionId);

// 获取或创建缓存

holder = createHolder(httpRequest.getSession());

 

接着是进入数据的处理

resp = holder.processXMLMessage(requestURL, requestData, params, headers);

 

先要把二进制数据转换成SyncML对象, 使用的是JIBX工具进行解析.

IBindingFactory f = BindingDirectory.getFactory("binding", SyncML.class);

IUnmarshallingContext c = f.createUnmarshallingContext();

Object syncML = c.unmarshalDocument(new StringReader(msg));

return (SyncML) syncML

 

通过JIBX配置转换,  把SyncML转换成JAVA对象, 每个指令, 作为一个子对象.

 

下面进入协议流程走向的处理,  调用sessionHandler接口操作.

进入processMessage方法,  通过局部变量currentState标识状态, 这样可以知道是交互过程中的第几次请求.  共有10中状态,  可以正确处理交互过程中的各种情形.

 

1. 用户鉴权判断: 第一次请求, 状态为STATE_START. 首先会根据设备标识读取设备, 若没有, 则新增. 然后进行鉴权处理, 根据算法解析cread信息, 获取用户名和密码, 去数据库查找相应信息进行验证.

// 服务端对用户密码校验代码片段:

UserProvisioningOfficer : protected Sync4jUser authenticateBasicCredential(Cred credential)

 --> DBOfficer : protected Sync4jUser getUser(String userName, String password ..)

 --> DBUserManager : public Sync4jUser[] getUsers(Clause clause ..)

 

处理完成后, 状态会标为STATE_PKG1_RECEIVING, 调用processInitSyncMapMessage方法, 返回SyncML处理指令.

// 处理返回交互结果

response = processInitSyncMapMessage(message);

 

 

2. 存储设备信息: 对于首次交互的设备, 会发送服务器的概要信息给客户端,

后续交互将不再发送. 控制代码:

DevicePersistentStore :          public boolean store(Object o)

// 新增设备, 首次交互, 发送服务器的吞吐信息     

d.setSentServerCaps(false);

 

3. 通讯录数据的加密解密:

// 服务端进行解密操作:

Sync4jEngine.sync(…

 

// 可进行B64和DES的加密, DES的密钥采用的是用户的密码applyDataTransformationsOnIncomingItems(clientSource.getUpdatedSyncItems());

 

服务端进行加密操作(通过 DataTransformerManager.xml中的sourceUriTrasformationsRequired属性去控制是否需加密, 以及用何种方式加密, 默认用户密码作为密钥)

// 先设置加密类型

PIMContactSyncSource.createSyncItem()

//  在执行加密操作

DataTransformerManager.transformOutgoingItem()

 

4. 通讯录数据同步更新

// 处理数据更新

SyncSessionHandler: List responseCommands = processModifications(modifications)

//把客户端传递数据通过服务端的映射表关联起来, 并设置相应的操作标识,  便于后续处理

SyncSessionHandler: private void prepareMemorySource(MemorySyncSource source …

         List<SyncItem> items = Arrays.asList(syncEngine.itemsToSyncItems(source,

                                                                                                       (ModificationCommand) commands[i],

                                                                                                       syncItemState, nextTimestamp.start));

 

 

// 处理SyncML中的Item指令,转换为同步对象

Sync4jEngine : public SyncItem[] itemsToSyncItems(…

EngineHelpler : public static SyncItem[] itemsToSyncItems(…

 

// 执行数据更新操作

Sync4jEngine : public void sync(final Sync4jPrincipal principal) ;

         // 开始执行更新操作   

         serverSource.beginSync(syncContext)

         …

         // 使用快速同步, 按照同步矩阵图获取并记录需要执行的增/删/改操作数据      Sync4jStrategy.prepareFastSync(sources,  p, mapping, syncFilterType,

                                                                           since, syncTimestamp,

                                                                           EngineHelper.getLastAnchor(uri, dbs), isLastMessage)

 

         //保存增/删/改数据, 数据正式生效, 同时封装返回STATUS指令

         Sync4jStrategy.sync(sources,  false,  mapping, lastAnchor,  (SyncOperation[])      operations.get(uri), syncFilterType){

                            // 执行数据更新

                            Sync4jStrategy : private SyncOperationStatus[]

                            execUpdateOperation(ContactSyncSource …

                            public SyncItem updateSyncItem( SyncItem syncItem)

                            //执行SQL

                           ContactDAO :  public String updateItem(ContactWrapper cw) 

                             //可以在此方法内记录客户端传递给服务端的更新日志记录

                            …

         }

 

5. 获取服务端需要同步的数据

  同步给客户端的数据, 服务端主要分两步进行处理, 第一, 先整理好需要同步的增删改数据; 第二, 根据整理好的数据ID获取数据信息, 并转换成VCARD格式, 同步给客户端.

  1) 整理同步数据:

  调用此方法, 会返回封装好的同步增删改数据概要

      syncStrategy.prepareFastSync(sources, p, mapping, syncFilterType, since, syncTimestamp,  EngineHelper.getLastAnchor(uri, dbs), isLastMessage)

 

  当改变同步时间戳, 客户端进行全同步时, 如果根据映射表找不到服务端的数据, 会根据姓名或电话号码等关键字进行匹配

    fixMappedItems(newlyMappedItems, updatedA, sources[0], validMapping, principal);   // 进行匹配获取

       twinsKey = source.getSyncItemKeysFromTwin(clone);

       // 建议注掉, 不允许根据部分字段匹配获取, 因为这样可能会对数据产生误差

       PIMContactSyncSource -> getSyncItemKeysFromTwin()

 

  最后, 再由下面的方法, 根据操作标识过滤不必要的数据

      commands = syncEngine.operationsToCommands(operations, uri, contentType);

  备注: 服务端的标记为新增的数据, 并且检测到客户端已存在该数据, 会标识为更新操作.

  对于服务端传递给客户端的删除数据, 客户端执行成功后, 服务端会删除相应的映射信息, 但不会物理删除该条联系人数据, 删除操作相关代码:

      Sync4jEngine : public void storeMappings(..)

      ClientMappingPersistentStore :  public boolean store(Object o)  throws PersistentStoreException {…

 

  2) 填充封装同步数据

         //之前整理的同步数据, 会记录在缓冲中, 接下来是直接从缓冲里获取数据

         SyncSessionHandler: private AbstractCommand[] commandsToSend(

                   //从syncState.cmdCache3缓存中获取, 该数据是之前调用的方法处理获取的

                   syncStrategy.prepareFastSync(sources,  p, mapping, syncFilterType,

                   since, syncTimestamp,  EngineHelper.getLastAnchor(uri, dbs), isLastMessage)

                  

                   // 填充数据相关代码:

                   private boolean splitSyncCommand( ..)

                   public void completeItemInfo(..

                   tmp = (AbstractSyncItem) serverSyncSource.getSyncItemFromId(key);

 

6. 其他相关代码:

1) 关于映射数据表中的上次同步时间标记作用, 如果客户端的上次同步时间和数据表中的时间一致时, 将忽略该数据, 不执行更新.

但是客户端丢失时间戳刷新全同步时,  两端时间标记如果都为0, 会产生数据丢失的情况,需要注意下. 相关代码:

EngineHelper : public static SyncItem[] createSyncItems(SyncItemKey[] keys, char state,

                                                        if (!"0".equals(lastAnchor) && StringUtils.equals(lastAnchor,  lastAnchorInMapping)) {…              

                                       

2) 客户端进行刷新全同步时, 服务端会标记为慢同步,  同时会删除之前的映射数据,

映射缓存也会清空, 这样会影响服务端和客户端的数据关联, 造成重复数据.

解决办法: 禁止删除之前映射, 保留映射缓存(全同步, 仍按之前业务规则处理, 只是数据量的大小区别) 相关代码:

Sync4jEngine : public ClientMapping getMapping(Sync4jPrincipal principal, String uri…

 

3) 对于客户端的刷新全同步, 服务端若发现有更新的数据, 不会传递给客户端, 需修改, 注掉相关代码:

SyncSessionHandler: private List processModifications(SyncModifications modifications)

//                         if (AlertCode.isClientOnlyCode(dbs[i].getMethod())) {

//                                  continue;

//                   }

 

4) 客户端的同步时间与服务端所记录的时间不一致时, 将会传送指令, 要求客户端全同步,

判断时间是否一致的代码:

 Sync4jEngine :  public void prepareDatabases(Sync4jPrincipal principal, Database[] dbs,

                            // 其中会判断上次同步时间是否一致.

if (!(last.tagServer.equals(dbs[i].getAnchor().getLast())) && (dbs[i].getMethod() != AlertCode.REFRESH_FROM_SERVER) && (dbs[i].getMethod() != AlertCode.REFRESH_FROM_CLIENT) && (dbs[i].getMethod() != AlertCode.SMART_ONE_WAY_FROM_CLIENT)) { …

 

5) 服务端全同步操作标识, 由客户端发送指令, 修改代码:

SyncInitialization : public Database[] getDatabasesToBeSynchronized(Sync4jPrincipal principal) {

                   // 作为服务端, 可通过修改此处进行服务端全同步的调测

                   //db.setMethod(clientAlerts[i].getData());

                   db.setMethod(AlertCode.REFRESH_FROM_SERVER);

                                   

                                                                              

7. 如何返写客户端执行数据更新后的结果, 简要流程

1) 交互时, 服务端从数据库中取出账户关联的映射数据

         ClientMapping.initializeFromMapping(Map<String, ClientMappingEntry> mapping) {

            resetMapping();

            clientMapping.putAll(mapping);

        }

   

2) 客户端更新完后, 会返回执行结果,  以客户端的LUID作为标识, 从缓存中查找关联的数    据, 并更新数据同步时间标记

         Sync4jEngine.getGuidsLuids(Status status, List<String> guids, List<String> luids,       ClientMapping mapping)

                   if (status.getSourceRef() != null && status.getSourceRef().size() > 0)

                   {

                            //需修改, 客户端传来的数据, 因为LUID而非GUID

                            luid = ((SourceRef) status.getSourceRef().get(0)).getValue();

                   }

 

8. 客户端同步完成后, 下次若没数据更新, 很多代码是不会执行, 为方便调测, 可以把同步时间下, 相关代码:

 

Sync4jEngine: public void prepareDatabases(…

         store.read(last);

                            //  时间可以根据需要进行设置, 减少5天

                            //      last.start = DateUtils.addDays(new Date(), -8).getTime();

                            dbs[i].setServerAnchor(new Anchor(last.tagClient, next.tagClient));

                            dbs[i].setSyncStartTimestamp(last.start);

 

 

 结束

  这些是对funambol ds server研究过程中的一些代码摘记, 也是数据同步的主要代码. 使用同步客户端的测试工具, 结合搭建好的工程进行DEBUG跟踪调测, 会更利于理解服务端代码. 下次, 将会介绍客户端工具的使用.

 

 

 

原创粉丝点击