`
jaesonchen
  • 浏览: 311297 次
  • 来自: ...
社区版块
存档分类
最新评论

从ZooKeeper源代码看如何实现分布式系统(四)session管理

 
阅读更多

这篇看看ZooKeeper如何管理Session。 Session相关的接口如下: 

Session: 表示session的实体类,维护sessionId和timeout两个主要状态

SessionTracker: Session生命周期管理相关的操作

SessionExpier: Session过期的操作

 

先看看Session接口和它的实现类SessionImpl,维护了5个属性:sessionId, timeout表示超时时间,tickTime表示客户端和服务器的心跳时间,isClosing表示是否关闭,owner表示对应的客户端

 

[java] view plain copy
 
 在CODE上查看代码片派生到我的代码片
  1. public static interface Session {  
  2.         long getSessionId();  
  3.         int getTimeout();  
  4.         boolean isClosing();  
  5.     }  
  6.   
  7. public static class SessionImpl implements Session {  
  8.         SessionImpl(long sessionId, int timeout, long expireTime) {  
  9.             this.sessionId = sessionId;  
  10.             this.timeout = timeout;  
  11.             this.tickTime = expireTime;  
  12.             isClosing = false;  
  13.         }  
  14.   
  15.         final long sessionId;  
  16.         final int timeout;  
  17.         long tickTime;  
  18.         boolean isClosing;  
  19.   
  20.         Object owner;  
  21.   
  22.         public long getSessionId() { return sessionId; }  
  23.         public int getTimeout() { return timeout; }  
  24.         public boolean isClosing() { return isClosing; }  
  25.     }  




 

SessionTracker的实现类是SessionTrackerImpl,它是一个单独运行的线程,根据tick周期来批量检查处理当前的session。SessionTrackerImpl直接继承了Thread类,它的定义和构造函数如下,几个主要的属性:

expirer是SessionExpirer的实现类

expirationInterval表示过期的周期,可以看到它的值是tickTime,即如果服务器端在tickTime里面没有收到客户端的心跳,就认为该session过期了

sessionsWithTimeout是一个ConcurrentHashMap,维护了一组sessionId和它对应的timeout过期时间

nextExpirationTime表示下次过期时间,线程会在nextExpirationTime时间来批量过期session

nextSessionId是根据sid计算出的下一个新建的sessionId

sessionById这个HashMap保存了sessionId和Session对象的映射

sessionSets这个HashMap保存了一个过期时间和一组保存在SessionSet中的Session的映射,用来批量清理过期的Session

 

[java] view plain copy
 
 在CODE上查看代码片派生到我的代码片
  1. public interface SessionTracker {  
  2.     public static interface Session {  
  3.         long getSessionId();  
  4.         int getTimeout();  
  5.         boolean isClosing();  
  6.     }  
  7.     public static interface SessionExpirer {  
  8.         void expire(Session session);  
  9.   
  10.         long getServerId();  
  11.     }  
  12.   
  13.     long createSession(int sessionTimeout);  
  14.   
  15.     void addSession(long id, int to);  
  16.   
  17.     boolean touchSession(long sessionId, int sessionTimeout);  
  18.   
  19.     void setSessionClosing(long sessionId);  
  20.   
  21.     void shutdown();  
  22.   
  23.     void removeSession(long sessionId);  
  24.   
  25.     void checkSession(long sessionId, Object owner) throws KeeperException.SessionExpiredException, SessionMovedException;  
  26.   
  27.     void setOwner(long id, Object owner) throws SessionExpiredException;  
  28.   
  29.     void dumpSessions(PrintWriter pwriter);  
  30. }  
  31.   
  32. public class SessionTrackerImpl extends Thread implements SessionTracker {  
  33.     private static final Logger LOG = LoggerFactory.getLogger(SessionTrackerImpl.class);  
  34.   
  35.     HashMap<Long, SessionImpl> sessionsById = new HashMap<Long, SessionImpl>();  
  36.   
  37.     HashMap<Long, SessionSet> sessionSets = new HashMap<Long, SessionSet>();  
  38.   
  39.     ConcurrentHashMap<Long, Integer> sessionsWithTimeout;  
  40.     long nextSessionId = 0;  
  41.     long nextExpirationTime;  
  42.   
  43.     int expirationInterval;  
  44.   
  45. public SessionTrackerImpl(SessionExpirer expirer,  
  46.             ConcurrentHashMap<Long, Integer> sessionsWithTimeout, int tickTime,  
  47.             long sid)  
  48.     {  
  49.         super("SessionTracker");  
  50.         this.expirer = expirer;  
  51.         this.expirationInterval = tickTime;  
  52.         this.sessionsWithTimeout = sessionsWithTimeout;  
  53.         nextExpirationTime = roundToInterval(System.currentTimeMillis());  
  54.         this.nextSessionId = initializeNextSession(sid);  
  55.         for (Entry<Long, Integer> e : sessionsWithTimeout.entrySet()) {  
  56.             addSession(e.getKey(), e.getValue());  
  57.         }  
  58.     }  


 


 

看一下SessionTrackerImpl这个线程的run方法实现,实现了批量处理过期Session的逻辑

1. 如果下一次过期时间nextExpirationTime大于当前时间,那么当前线程等待nextExpirationTime - currentTime时间

2. 如果到了过期时间,就从sessionSets里面把当前过期时间对应的一组SessionSet取出

3. 批量关闭和过期这组session

4. 把当前过期时间nextExpirationTime 加上 expirationInterval作为下一个过期时间nextExpiration,继续循环

 

其中expirer.expire(s)这个操作,这里的expirer的实现类是ZooKeeperServer,它的expire方法会给给客户端发送session关闭的请求

 

[java] view plain copy
 
 在CODE上查看代码片派生到我的代码片
  1. // SessionTrackerImpl   
  2. synchronized public void run() {  
  3.         try {  
  4.             while (running) {  
  5.                 currentTime = System.currentTimeMillis();  
  6.                 if (nextExpirationTime > currentTime) {  
  7.                     this.wait(nextExpirationTime - currentTime);  
  8.                     continue;  
  9.                 }  
  10.                 SessionSet set;  
  11.                 set = sessionSets.remove(nextExpirationTime);  
  12.                 if (set != null) {  
  13.                     for (SessionImpl s : set.sessions) {  
  14.                         setSessionClosing(s.sessionId);  
  15.                         expirer.expire(s);  
  16.                     }  
  17.                 }  
  18.                 nextExpirationTime += expirationInterval;  
  19.             }  
  20.         } catch (InterruptedException e) {  
  21.             LOG.error("Unexpected interruption", e);  
  22.         }  
  23.         LOG.info("SessionTrackerImpl exited loop!");  
  24.     }  
  25.   
  26. // ZookeeperServer  
  27. public void expire(Session session) {  
  28.         long sessionId = session.getSessionId();  
  29.         LOG.info("Expiring session 0x" + Long.toHexString(sessionId)  
  30.                 + ", timeout of " + session.getTimeout() + "ms exceeded");  
  31.         close(sessionId);  
  32.     }  
  33.   
  34. private void close(long sessionId) {  
  35.         submitRequest(null, sessionId, OpCode.closeSession, 0nullnull);  
  36.     }  


再看一下创建Session的过程

 

1. createSession方法只需要一个sessionTimeout参数来指定Session的过期时间,会把当前全局的nextSessionId作为sessionId传给addSession方法

2. addSession方法先把sessionId和过期时间的映射添加到sessionsWithTimeout这个Map里面来,如果在sessionById这个Map里面没有找到对应sessionId的session对象,就创建一个Session对象,然后放到sessionById Map里面。最后调用touchSession方法来设置session的过期时间等信息

3. touchSession方法首先判断session状态,如果关闭就返回。计算当前session的过期时间,如果是第一次touch这个session,它的tickTime会被设置成它的过期时间expireTime,然后把它加到对应的sessuibSets里面。如果不是第一次touch,那么它的tickTime会是它当前的过期时间,如果还没过期,就返回。如果过期了,就重新计算一个过期时间,并设置给tickTime,然后从对应的sessionSets里面先移出,再加入到新的sessionSets里面。 touchSession方法主要是为了更新session的过期时间。

 

[java] view plain copy
 
 在CODE上查看代码片派生到我的代码片
  1. synchronized public long createSession(int sessionTimeout) {  
  2.         addSession(nextSessionId, sessionTimeout);  
  3.         return nextSessionId++;  
  4.     }  
  5.   
  6. synchronized public void addSession(long id, int sessionTimeout) {  
  7.         sessionsWithTimeout.put(id, sessionTimeout);  
  8.         if (sessionsById.get(id) == null) {  
  9.             SessionImpl s = new SessionImpl(id, sessionTimeout, 0);  
  10.             sessionsById.put(id, s);  
  11.             if (LOG.isTraceEnabled()) {  
  12.                 ZooTrace.logTraceMessage(LOG, ZooTrace.SESSION_TRACE_MASK,  
  13.                         "SessionTrackerImpl --- Adding session 0x"  
  14.                         + Long.toHexString(id) + " " + sessionTimeout);  
  15.             }  
  16.         } else {  
  17.             if (LOG.isTraceEnabled()) {  
  18.                 ZooTrace.logTraceMessage(LOG, ZooTrace.SESSION_TRACE_MASK,  
  19.                         "SessionTrackerImpl --- Existing session 0x"  
  20.                         + Long.toHexString(id) + " " + sessionTimeout);  
  21.             }  
  22.         }  
  23.         touchSession(id, sessionTimeout);  
  24.     }  
  25.   
  26. synchronized public boolean touchSession(long sessionId, int timeout) {  
  27.         if (LOG.isTraceEnabled()) {  
  28.             ZooTrace.logTraceMessage(LOG,  
  29.                                      ZooTrace.CLIENT_PING_TRACE_MASK,  
  30.                                      "SessionTrackerImpl --- Touch session: 0x"  
  31.                     + Long.toHexString(sessionId) + " with timeout " + timeout);  
  32.         }  
  33.         SessionImpl s = sessionsById.get(sessionId);  
  34.         // Return false, if the session doesn't exists or marked as closing  
  35.         if (s == null || s.isClosing()) {  
  36.             return false;  
  37.         }  
  38.         long expireTime = roundToInterval(System.currentTimeMillis() + timeout);  
  39.         if (s.tickTime >= expireTime) {  
  40.             // Nothing needs to be done  
  41.             return true;  
  42.         }  
  43.         SessionSet set = sessionSets.get(s.tickTime);  
  44.         if (set != null) {  
  45.             set.sessions.remove(s);  
  46.         }  
  47.         s.tickTime = expireTime;  
  48.         set = sessionSets.get(s.tickTime);  
  49.         if (set == null) {  
  50.             set = new SessionSet();  
  51.             sessionSets.put(expireTime, set);  
  52.         }  
  53.         set.sessions.add(s);  
  54.         return true;  
  55.     }  


SessionTracker这个接口主要被ZooKeeperServer这个类来使用,ZooKeeperServer表示ZooKeeper的服务器类,负责维护ZooKeeper服务器状态。

 

在ZooKeeperServer的startup方法中,如果sessionTracker对象为空,就先创建一个SessionTracker对象,然后调用startSessionTracker方法启动SessionTrackerImpl这个线程

 

[java] view plain copy
 
 在CODE上查看代码片派生到我的代码片
  1.  public void startup() {          
  2.         if (sessionTracker == null) {  
  3.             createSessionTracker();  
  4.         }  
  5.         startSessionTracker();  
  6.         setupRequestProcessors();  
  7.   
  8.         registerJMX();  
  9.   
  10.         synchronized (this) {  
  11.             running = true;  
  12.             notifyAll();  
  13.         }  
  14.     }  
  15.   
  16.  protected void createSessionTracker() {  
  17.         sessionTracker = new SessionTrackerImpl(this, zkDb.getSessionWithTimeOuts(),  
  18.                 tickTime, 1);  
  19.     }   
  20.   
  21. protected void startSessionTracker() {  
  22.         ((SessionTrackerImpl)sessionTracker).start();  
  23.     }  


在ZooKeeperServer的shutdown方法中,调用sessionTracker的shutdown方法来关闭sessionTrackerImpl线程

 

 

[java] view plain copy
 
 在CODE上查看代码片派生到我的代码片
  1.  public void shutdown() {  
  2.         LOG.info("shutting down");  
  3.   
  4.         // new RuntimeException("Calling shutdown").printStackTrace();  
  5.         this.running = false;  
  6.         // Since sessionTracker and syncThreads poll we just have to  
  7.         // set running to false and they will detect it during the poll  
  8.         // interval.  
  9.         if (sessionTracker != null) {  
  10.             sessionTracker.shutdown();  
  11.         }  
  12.         if (firstProcessor != null) {  
  13.             firstProcessor.shutdown();  
  14.         }  
  15.         if (zkDb != null) {  
  16.             zkDb.clear();  
  17.         }  
  18.   
  19.         unregisterJMX();  
  20.     }  
  21.   
  22. // SessionTrackerImpl  
  23. public void shutdown() {  
  24.         LOG.info("Shutting down");  
  25.   
  26.         running = false;  
  27.         if (LOG.isTraceEnabled()) {  
  28.             ZooTrace.logTraceMessage(LOG, ZooTrace.getTextTraceLevel(),  
  29.                                      "Shutdown SessionTrackerImpl!");  
  30.         }  
  31.     }  


ZooKeeperServer的createSession方法给连接ServerCnxn创建一个对应的session,然后给客户端发送一个创建了session的请求

 

 

[java] view plain copy
 
 在CODE上查看代码片派生到我的代码片
  1. long createSession(ServerCnxn cnxn, byte passwd[], int timeout) {  
  2.         long sessionId = sessionTracker.createSession(timeout);  
  3.         Random r = new Random(sessionId ^ superSecret);  
  4.         r.nextBytes(passwd);  
  5.         ByteBuffer to = ByteBuffer.allocate(4);  
  6.         to.putInt(timeout);  
  7.         cnxn.setSessionId(sessionId);  
  8.         submitRequest(cnxn, sessionId, OpCode.createSession, 0, to, null);  
  9.         return sessionId;  
  10.     }  


ZooKeeperServer的reopenSession会给断开了连接后又重新连接的session更新状态,使session继续可用

 

1. 如果session的密码不对,调用finishSessionInit方法来关闭session,如果密码正确,调用revalidateSession方法

2. revalidateSession方法会调用sessionTracker的touchSession,如果session已经过期,rc = false,如果session未过期,更新session的过期时间信息。最后也调用finishSessionInit方法

3. finishSessionInit方法会给客户端发送响应对象ConnectResponse,如果验证不通过,会关闭连接  cnxn.sendBuffer(ServerCnxnFactory.closeConn)。验证通过,调用cnxn.enableRecv(); 方法来设置连接状态,使服务器端连接注册SelectionKey.OP_READ事件,准备接收客户端请求

 

[java] view plain copy
 
 在CODE上查看代码片派生到我的代码片
  1. public void reopenSession(ServerCnxn cnxn, long sessionId, byte[] passwd,  
  2.             int sessionTimeout) throws IOException {  
  3.         if (!checkPasswd(sessionId, passwd)) {  
  4.             finishSessionInit(cnxn, false);  
  5.         } else {  
  6.             revalidateSession(cnxn, sessionId, sessionTimeout);  
  7.         }  
  8.     }  
  9.   
  10. protected void revalidateSession(ServerCnxn cnxn, long sessionId,  
  11.             int sessionTimeout) throws IOException {  
  12.         boolean rc = sessionTracker.touchSession(sessionId, sessionTimeout);  
  13.         if (LOG.isTraceEnabled()) {  
  14.             ZooTrace.logTraceMessage(LOG,ZooTrace.SESSION_TRACE_MASK,  
  15.                                      "Session 0x" + Long.toHexString(sessionId) +  
  16.                     " is valid: " + rc);  
  17.         }  
  18.         finishSessionInit(cnxn, rc);  
  19.     }  
  20.   
  21. public void finishSessionInit(ServerCnxn cnxn, boolean valid) {  
  22.         // register with JMX  
  23.         try {  
  24.             if (valid) {  
  25.                 serverCnxnFactory.registerConnection(cnxn);  
  26.             }  
  27.         } catch (Exception e) {  
  28.                 LOG.warn("Failed to register with JMX", e);  
  29.         }  
  30.   
  31.         try {  
  32.             ConnectResponse rsp = new ConnectResponse(0, valid ? cnxn.getSessionTimeout()  
  33.                     : 0, valid ? cnxn.getSessionId() : 0// send 0 if session is no  
  34.                             // longer valid  
  35.                             valid ? generatePasswd(cnxn.getSessionId()) : new byte[16]);  
  36.             ByteArrayOutputStream baos = new ByteArrayOutputStream();  
  37.             BinaryOutputArchive bos = BinaryOutputArchive.getArchive(baos);  
  38.             bos.writeInt(-1"len");  
  39.             rsp.serialize(bos, "connect");  
  40.             if (!cnxn.isOldClient) {  
  41.                 bos.writeBool(  
  42.                         this instanceof ReadOnlyZooKeeperServer, "readOnly");  
  43.             }  
  44.             baos.close();  
  45.             ByteBuffer bb = ByteBuffer.wrap(baos.toByteArray());  
  46.             bb.putInt(bb.remaining() - 4).rewind();  
  47.             cnxn.sendBuffer(bb);      
  48.   
  49.             if (!valid) {  
  50.                 LOG.info("Invalid session 0x"  
  51.                         + Long.toHexString(cnxn.getSessionId())  
  52.                         + " for client "  
  53.                         + cnxn.getRemoteSocketAddress()  
  54.                         + ", probably expired");  
  55.                 cnxn.sendBuffer(ServerCnxnFactory.closeConn);  
  56.             } else {  
  57.                 LOG.info("Established session 0x"  
  58.                         + Long.toHexString(cnxn.getSessionId())  
  59.                         + " with negotiated timeout " + cnxn.getSessionTimeout()  
  60.                         + " for client "  
  61.                         + cnxn.getRemoteSocketAddress());  
  62.                 cnxn.enableRecv();  
  63.             }  
  64.                   
  65.         } catch (Exception e) {  
  66.             LOG.warn("Exception while establishing session, closing", e);  
  67.             cnxn.close();  
  68.         }  
  69.     }  
  70.   
  71. // NIOServerCnxn  
  72. public void enableRecv() {  
  73.         synchronized (this.factory) {  
  74.             sk.selector().wakeup();  
  75.             if (sk.isValid()) {  
  76.                 int interest = sk.interestOps();  
  77.                 if ((interest & SelectionKey.OP_READ) == 0) {  
  78.                     sk.interestOps(interest | SelectionKey.OP_READ);  
  79.                 }  
  80.             }  
  81.         }  
  82.     }  

 

分享到:
评论

相关推荐

    ZooKeeper分布式系统协调 v3.6.3.gz

    ZooKeeper的源代码提供了深入了解其实现细节的机会,对于学习分布式系统原理和Java NIO编程有很大帮助。源码中包含了服务器端的处理逻辑、客户端的API实现、ZAB协议的实现等。 ### 六、应用场景 ZooKeeper广泛应用...

    ZOOKEEPER3.4.5

    在实际项目中,开发者可以通过解压`zookeeper-3.4.5`压缩包,了解ZooKeeper的源代码,学习其实现原理,以便更好地运用到自己的分布式系统设计中。 总之,ZooKeeper 3.4.5 在服务治理和分布式部署中的作用不可忽视,...

    zookeeper-3.6.3.zip

    10. **集群管理**:Zookeeper可以用来监控和管理分布式系统的节点状态,比如监控服务的上线、下线,或者健康检查。 在实际应用中,Zookeeper常被用于Hadoop、HBase、Kafka等分布式系统中,作为它们的协调器,确保...

    zookeeper-3.8.0安装包下载

    Apache ZooKeeper 是一个高度可靠的分布式协调系统,广泛应用于云原生环境中的服务发现...总之,Apache Zookeeper 在分布式系统中扮演着重要角色,理解和掌握其原理与使用方法,对提升系统架构和运维能力具有显著作用。

    zookeeper-3.4.6.zip

    1. **源代码**:包含了ZooKeeper的全部源代码,允许开发者深入理解其工作原理,进行自定义修改或扩展。 2. **文档**:可能包括用户手册、API参考、开发者指南等,帮助用户和开发者快速熟悉ZooKeeper的使用和开发。 3...

    zookeeper-3.4.14.zip

    在分布式系统中,ZooKeeper 被广泛用于实现命名服务、配置管理、集群同步、领导者选举等多种功能。 在 ZooKeeper 3.4.14 版本中,这个稳定性的声明意味着它经过了大量的测试和社区验证,适合在生产环境中使用。相比...

    zookeeper+redis打包jar

    在分布式系统中,Zookeeper常被用来解决分布式锁、配置管理、服务发现等问题。 Redis则是一款开源的高性能键值对数据库,它支持多种数据结构如字符串、哈希、列表、集合、有序集合等,而且具备高速读写能力,通常...

    zookeeper-3.3.6.tar.gz

    这是一款Zookeeper的3.3.6版本的压缩包,通常包含了Zookeeper的源代码、编译脚本、配置文件、文档以及相关的依赖库。解压后,我们可以看到包括bin(二进制可执行文件)、conf(配置文件)、src(源代码)等目录,...

    zookeeper-server-client 简单例子

    Zookeeper 的设计目标是简化分布式环境下的数据一致性问题,使得在分布式系统中实现高可用性和扩展性变得更加容易。 **Zookeeper 的主要特点** 1. **原子性**:Zookeeper 的所有操作都是原子性的,一次操作要么...

    zookeeper cdh5.11 tar包

    ZooKeeper-3.4.5-cdh5.11.0的tar包是针对这个特定CDH版本的打包文件,通常包含源代码、编译好的二进制文件、配置文件以及相关的文档。 **ZooKeeper核心概念与功能:** 1. **节点(Znode)**:ZooKeeper的数据存储...

    适用jdk7环境的,zookeeper3.4版本

    ZooKeeper是一个分布式协调服务,它为分布式应用提供一致性服务,如命名服务、配置管理、组服务、分布式锁和分布式队列等。...同时,了解并熟练掌握ZooKeeper的基本概念和操作对于管理和协调分布式系统至关重要。

    zookeeper资料

    而“zookeeper-trunk”可能是一个源码仓库,包含Zookeeper的源代码,适合深入理解其内部机制和进行二次开发。 通过深入学习这些资料,你可以掌握如何配置和管理Zookeeper集群,如何使用Zookeeper API进行数据操作,...

    PyPI 官网下载 | score.session-0.4.4.tar.gz

    结合以上信息,我们可以推测`score.session`可能是一个Python库,它专注于分布式系统中的会话管理,可能集成或与Apache ZooKeeper协同工作,提供在云环境中安全、可靠的会话存储和服务发现。开发者可以利用这个库在...

    zookeeper-3.4.10.zip

    Zookeeper-3.4.10.zip 文件是 Apache ZooKeeper 的一个重要版本,包含了该版本的源代码、构建文件以及相关的校验文件。 1. **Zookeeper 的核心概念** - **节点(Znode)**:Zookeeper 数据存储的基本单位,类似于...

    PyPI 官网下载 | flask-session-cookie-manager-1.2.1.tar.gz

    5. **分布式**:这暗示该扩展可能设计用于处理分布式系统中的会话一致性问题,比如在多个服务器之间同步用户会话或管理Cookie,以确保用户在不同服务器间的体验连续性。 根据压缩包子文件的文件名称“flask-session...

    zookeeper.rar

    这个版本的压缩包名为`ZooKeeper-3.4.5.tar.gz`,包含了完整的源代码和必要的文档,供开发者研究和部署。 **ZooKeeper的基本概念** 1. **节点(ZNode)**:ZooKeeper的数据存储结构类似于文件系统,由一系列的节点...

    apache-zookeeper-3.5.8-bin.zip

    ZooKeeper 被广泛应用于大数据、云计算和其他分布式系统中,用于管理命名服务、配置管理、集群同步、领导者选举等任务。 标题 "apache-zookeeper-3.5.8-bin.zip" 指的是 Apache ZooKeeper 的一个特定版本——3.5.8...

    zookeeper-3.5.3-beta.tar.gz压缩包

    Zookeeper在大数据生态系统中扮演着重要角色,它被广泛应用于Hadoop、HBase、Kafka等众多分布式系统中,作为数据共享、服务发现、配置管理等核心功能的基石。 这个"zookeeper-3.5.3-beta.tar.gz"压缩包包含了...

    PyPI 官网下载 | pyramid_pluggable_session-0.0.0a2.tar.gz

    在`pyramid_pluggable_session-0.0.0a2.tar.gz`压缩包中,包含了这个扩展的源代码和其他相关文件。解压后,开发者可以通过Python的setuptools工具进行安装,并在Pyramid项目中导入和配置,以启用会话管理功能。安装...

    apache-zookeeper-3.7.0-bin

    6. **src** 目录:虽然不在压缩包中,但在源码树里,此目录包含了 ZooKeeper 的源代码,可供开发者查看和调试。 7. **zkpython** 和 **zkperl** 目录:提供了 Python 和 Perl 的客户端接口,使得这些语言可以方便地...

Global site tag (gtag.js) - Google Analytics