zookeeper 客户端源码之 MyCommandOptions

本文深入解析ZooKeeper客户端的MyCommandOptions类,详细介绍了其如何处理客户端连接参数和命令行指令,包括参数和指令的解析流程,以及在客户端界面的使用方式。

zookeeper 客户端源码之 MyCommandOptions

源码解析

MyCommandOptions 是 ZooKeeperMain 里的一个内部类,包含了两个概念:command 和 options。
它们分别被用在两处:进入客户端之后和进入客户端之前

  1. 进入客户端前:
    “zkCli.sh -server 127.0.0.1:2181”
    这里,进入 main 的 args 是一个 String 数组: ["-server", “127.0.0.1:2181”]:
    在这里插入图片描述
    这里就会被 parseOptions 方法转换为键值对存在 options里,具体看后文。
  2. 进入客户端之后,命令行由操作cmd 和参数args 组成,如:
    “create -s /zk”,是一个 String ,不再是 String[]
    这里就会被 parseCommand 方法转换为 command 和 cmdArgs ,具体看后文。

一开始,我这里也是糊了好久,才把这两个东西分开,现在开始看源码:

static class MyCommandOptions {
		//<选项,参数>,存放的是进入客户端之前的选项和参数,选项有server、timeout、readonly
        private Map<String,String> options = new HashMap<String,String>();
        //存放客户端界面里一条命令行里的参数,如"create -s /zk"里的"create"、"-s"、"/zk"
        private List<String> cmdArgs = null;
        //存放指令,不带参数,如"create"
        private String command = null;
        //针对客户端内的命令行,如"create -s /zk"
        public static final Pattern ARGS_PATTERN = Pattern.compile("\\s*([^\"\']\\S*|\"[^\"]*\"|'[^']*')\\s*");
        //匹配带有引号的参数
        public static final Pattern QUOTED_PATTERN = Pattern.compile("^([\'\"])(.*)(\\1)$");
		//默认值
        public MyCommandOptions() {
          options.put("server", "localhost:2181");
          options.put("timeout", "30000");
        }

        public String getOption(String opt) {
            return options.get(opt);
        }

        public String getCommand( ) {
            return command;
        }

        public String getCmdArgument( int index ) {
            return cmdArgs.get(index);
        }

        public int getNumArguments( ) {
            return cmdArgs.size();
        }

        public String[] getArgArray() {
        	//(String[])cmdArgs.toArray();
            return cmdArgs.toArray(new String[0]);
        }

        /**
         * Parses a command line that may contain one or more flags
         * before an optional command string
         * @param args command line arguments
         * @return true if parsing succeeded, false otherwise.
         */
         //args:"-server" "127.0.0.1:2181"
        public boolean parseOptions(String[] args) {
            List<String> argList = Arrays.asList(args);
            Iterator<String> it = argList.iterator();

            while (it.hasNext()) {
                String opt = it.next();
                //登陆的话就会到这里来,如"zkCli.sh -server 127.0.0.1:2181"
                try {
                    if (opt.equals("-server")) {
                        options.put("server", it.next());
                    } else if (opt.equals("-timeout")) {
                        options.put("timeout", it.next());
                    } else if (opt.equals("-r")) {
                        options.put("readonly", "true");
                    }
                } catch (NoSuchElementException e){
                    System.err.println("Error: no argument found for option "
                            + opt);
                    return false;
                }
				//这里感觉没有用武之地,比如 zkCli.sh -server 127.0.0.1:2181
				//就没有不带 - 的操作啊。但是后来博主想到了一个点,见后文
                if (!opt.startsWith("-")) {
                    command = opt;
                    cmdArgs = new ArrayList<String>( );
                    cmdArgs.add( command );
                    while (it.hasNext()) {
                        cmdArgs.add(it.next());
                    }
                    return true;
                }
            }
            return true;
        }

        /**
         * Breaks a string into command + arguments.
         * @param cmdstring string of form "cmd arg1 arg2..etc"
         * @return true if parsing succeeded.
         */
         //针对进入客户端内的命令
        public boolean parseCommand( String cmdstring ) {
            Matcher matcher = ARGS_PATTERN.matcher(cmdstring);

            List<String> args = new LinkedList<String>();
            //提取参数
            while (matcher.find()) {
            	//取第一个分组里的内容,也就是括号里的内容,去掉了两边的空白字符
                String value = matcher.group(1);
                if (QUOTED_PATTERN.matcher(value).matches()) {
                    // Strip off the surrounding quotes
                    //去掉两边的引号
                    value = value.substring(1, value.length() - 1);
                }
                args.add(value);
            }
            //没有参数是不行的
            if (args.isEmpty()){
                return false;
            }
            //args 的第一个是指令
            command = args.get(0);
            //把包括指令在内的整个数组给cmdArgs 
            cmdArgs = args;
            return true;
        }
    }

使用位置

这个类用在了哪里呢?
1)ZooKeeperMain 里的属性 cl,就是这个类的对象
在这里插入图片描述2)ZooKeeperMain 的 main 方法里,进行了创建 ZooKeeperMain 对象
在这里插入图片描述
在它的构造函数里:
在这里插入图片描述
重点是 parseOptions,后面就是在 parseOptions 拿到 “server” 对应的值。

parseOptions 好像就这里用到了:
在这里插入图片描述
所以我在之前的源码里注释了,应该不存在不是以"-"开头的情况才对,这个点一直存在问题,后面也会提到。

注意 MyCommandOptions的构造函数,就知道就算没有 server 参数一样可以连接(如果服务器在本地):
在这里插入图片描述
在这里插入图片描述
验证成功

3)ZooKeeperMain 的 main 方法里,创建 ZooKeeperMain 对象之后,调用了 run() 方法
在这里插入图片描述
在这里插入图片描述
getCommand() 返回的是 command,还记得 command 是什么吗?是登陆客户端之后的指令。所以为 null 就是这是登陆的命令,输出 “Welcome to ZooKeeper!”

4)在登陆的时候(登陆完成之前),存在两种情况:是否使用 jline,但无论如何都会执行 executeLine(line):
用 jline:
在这里插入图片描述
不用 jline :
在这里插入图片描述
那么 executeLine(line) 到底是什么呢?
在这里插入图片描述
可以看到,这里先做了一个判断如果不为空,然后有四步:

  1. 调用了 parseCommand 更新了cl 里的 command 和 args
  2. 将 commandCount 和 line 加入 history,这一点在客户端上很直观:
    在这里插入图片描述
  3. 调用 processCmd 执行命令
  4. commandCount 计数器加一

看一下 processCmd 里对 cl 做了什么:

protected boolean processCmd(MyCommandOptions co)
        throws KeeperException, IOException, InterruptedException
    {
        try {
            return processZKCmd(co);
        } catch (IllegalArgumentException e) {
            System.err.println("Command failed: " + e);
        } catch (KeeperException.NoNodeException e) {
            System.err.println("Node does not exist: " + e.getPath());
        } catch (KeeperException.NoChildrenForEphemeralsException e) {
            System.err.println("Ephemerals cannot have children: "
                    + e.getPath());
        } catch (KeeperException.NodeExistsException e) {
            System.err.println("Node already exists: " + e.getPath());
        } catch (KeeperException.NotEmptyException e) {
            System.err.println("Node not empty: " + e.getPath());
        } catch (KeeperException.NotReadOnlyException e) {
            System.err.println("Not a read-only call: " + e.getPath());
        }catch (KeeperException.InvalidACLException  e) {
            System.err.println("Acl is not valid : "+e.getPath());
        }catch (KeeperException.NoAuthException  e) {
            System.err.println("Authentication is not valid : "+e.getPath());
        }catch (KeeperException.BadArgumentsException   e) {
            System.err.println("Arguments are not valid : "+e.getPath());
        }catch (KeeperException.BadVersionException e) {
            System.err.println("version No is not valid : "+e.getPath());
        }
        return false;
    }

这里是用来处理异常信息的,真正的执行命令在 processZKCmd(co) 方法里,我把它分成几段来看:

protected boolean processZKCmd(MyCommandOptions co)
        throws KeeperException, IOException, InterruptedException
    {
        Stat stat = new Stat();
        //得到参数数组
        String[] args = co.getArgArray();
        //得到指令
        String cmd = co.getCommand();
        //如果参数数组长度为0,也就是没有输入
        if (args.length < 1) {
            usage();
            return false;
        }
		//如果不存在这个指令
        if (!commandMap.containsKey(cmd)) {
            usage();
            return false;
        }

什么是 usage ?
在这里插入图片描述
什么是 commandMap ?
在这里插入图片描述
在客户端试一下:
在这里插入图片描述
果然就是源码里的 usage() 的效果。继续 processZKCmd 的代码,下面的代码阅读性就很好了,基本上就是判断 cmd 是什么,然后做出对应的操作:

        boolean watch = args.length > 2;
        String path = null;
        List<ACL> acl = Ids.OPEN_ACL_UNSAFE;
        LOG.debug("Processing " + cmd);

        if (cmd.equals("quit")) {
            System.out.println("Quitting...");
            zk.close();
            System.exit(0);
        } else if (cmd.equals("redo") && args.length >= 2) {
            Integer i = Integer.decode(args[1]);
            if (commandCount <= i || i < 0){ // don't allow redoing this redo
                System.out.println("Command index out of range");
                return false;
            }
            cl.parseCommand(history.get(i));
            if (cl.getCommand().equals( "redo" )){
                System.out.println("No redoing redos");
                return false;
            }
            history.put(commandCount, history.get(i));
            processCmd( cl);
        } else if (cmd.equals("history")) {
            for (int i=commandCount - 10;i<=commandCount;++i) {
                if (i < 0) continue;
                System.out.println(i + " - " + history.get(i));
            }
        } else if (cmd.equals("printwatches")) {
            if (args.length == 1) {
                System.out.println("printwatches is " + (printWatches ? "on" : "off"));
            } else {
                printWatches = args[1].equals("on");
            }
        } else if (cmd.equals("connect")) {
            if (args.length >=2) {
                connectToZK(args[1]);
            } else {
                connectToZK(host);
            }
        }
        
        // Below commands all need a live connection
        if (zk == null || !zk.getState().isAlive()) {
            System.out.println("Not connected");
            return false;
        }
        
        if (cmd.equals("create") && args.length >= 3) {
            int first = 0;
            CreateMode flags = CreateMode.PERSISTENT;
            if ((args[1].equals("-e") && args[2].equals("-s"))
                    || (args[1]).equals("-s") && (args[2].equals("-e"))) {
                first+=2;
                flags = CreateMode.EPHEMERAL_SEQUENTIAL;
            } else if (args[1].equals("-e")) {
                first++;
                flags = CreateMode.EPHEMERAL;
            } else if (args[1].equals("-s")) {
                first++;
                flags = CreateMode.PERSISTENT_SEQUENTIAL;
            }
            if (args.length == first + 4) {
                acl = parseACLs(args[first+3]);
            }
            path = args[first + 1];
            String newPath = zk.create(path, args[first+2].getBytes(), acl,
                    flags);
            System.err.println("Created " + newPath);
        } else if (cmd.equals("delete") && args.length >= 2) {
            path = args[1];
            zk.delete(path, watch ? Integer.parseInt(args[2]) : -1);
        } else if (cmd.equals("rmr") && args.length >= 2) {
            path = args[1];
            ZKUtil.deleteRecursive(zk, path);
        } else if (cmd.equals("set") && args.length >= 3) {
            path = args[1];
            stat = zk.setData(path, args[2].getBytes(),
                    args.length > 3 ? Integer.parseInt(args[3]) : -1);
            printStat(stat);
        } else if (cmd.equals("aget") && args.length >= 2) {
            path = args[1];
            zk.getData(path, watch, dataCallback, path);
        } else if (cmd.equals("get") && args.length >= 2) {
            path = args[1];
            byte data[] = zk.getData(path, watch, stat);
            data = (data == null)? "null".getBytes() : data;
            System.out.println(new String(data));
            printStat(stat);
        } else if (cmd.equals("ls") && args.length >= 2) {
            path = args[1];
            List<String> children = zk.getChildren(path, watch);
            System.out.println(children);
        } else if (cmd.equals("ls2") && args.length >= 2) {
            path = args[1];
            List<String> children = zk.getChildren(path, watch, stat);
            System.out.println(children);
            printStat(stat);
        } else if (cmd.equals("getAcl") && args.length >= 2) {
            path = args[1];
            acl = zk.getACL(path, stat);
            for (ACL a : acl) {
                System.out.println(a.getId() + ": "
                        + getPermString(a.getPerms()));
            }
        } else if (cmd.equals("setAcl") && args.length >= 3) {
            path = args[1];
            stat = zk.setACL(path, parseACLs(args[2]),
                    args.length > 4 ? Integer.parseInt(args[3]) : -1);
            printStat(stat);
        } else if (cmd.equals("stat") && args.length >= 2) {
            path = args[1];
            stat = zk.exists(path, watch);
            if (stat == null) {
              throw new KeeperException.NoNodeException(path);
            }
            printStat(stat);
        } else if (cmd.equals("listquota") && args.length >= 2) {
            path = args[1];
            String absolutePath = Quotas.quotaZookeeper + path + "/" + Quotas.limitNode;
            byte[] data =  null;
            try {
                System.err.println("absolute path is " + absolutePath);
                data = zk.getData(absolutePath, false, stat);
                StatsTrack st = new StatsTrack(new String(data));
                System.out.println("Output quota for " + path + " "
                        + st.toString());

                data = zk.getData(Quotas.quotaZookeeper + path + "/" +
                        Quotas.statNode, false, stat);
                System.out.println("Output stat for " + path + " " +
                        new StatsTrack(new String(data)).toString());
            } catch(KeeperException.NoNodeException ne) {
                System.err.println("quota for " + path + " does not exist.");
            }
        } else if (cmd.equals("setquota") && args.length >= 4) {
            String option = args[1];
            String val = args[2];
            path = args[3];
            System.err.println("Comment: the parts are " +
                               "option " + option +
                               " val " + val +
                               " path " + path);
            if ("-b".equals(option)) {
                // we are setting the bytes quota
                createQuota(zk, path, Long.parseLong(val), -1);
            } else if ("-n".equals(option)) {
                // we are setting the num quota
                createQuota(zk, path, -1L, Integer.parseInt(val));
            } else {
                usage();
            }

        } else if (cmd.equals("delquota") && args.length >= 2) {
            //if neither option -n or -b is specified, we delete
            // the quota node for thsi node.
            if (args.length == 3) {
                //this time we have an option
                String option = args[1];
                path = args[2];
                if ("-b".equals(option)) {
                    delQuota(zk, path, true, false);
                } else if ("-n".equals(option)) {
                    delQuota(zk, path, false, true);
                }
            } else if (args.length == 2) {
                path = args[1];
                // we dont have an option specified.
                // just delete whole quota node
                delQuota(zk, path, true, true);
            } else if (cmd.equals("help")) {
                usage();
            }
        } else if (cmd.equals("close")) {
                zk.close();
        } else if (cmd.equals("sync") && args.length >= 2) {
            path = args[1];
            zk.sync(path, new AsyncCallback.VoidCallback() { public void processResult(int rc, String path, Object ctx) { System.out.println("Sync returned " + rc); } }, null );
        } else if (cmd.equals("addauth") && args.length >=2 ) {
            byte[] b = null;
            if (args.length >= 3)
                b = args[2].getBytes();

            zk.addAuthInfo(args[1], b);
        } else if (!commandMap.containsKey(cmd)) {
            usage();
        }
        return watch;
    }

其中,create 这个指令十分重要,我在另外一篇博客中会详细介绍。

  1. 如果不为null(接着3)后面),意思是运行 run() 方法前就有 command 了。
    在这里插入图片描述
    这里一直不理解,可对应 parseOptions 的代码,说明确实是有这个可能。于是我试了一下这样:
    在这里插入图片描述
    不行,报错。但,如果是这样呢?
zkCli.sh -server 127.0.0.1:2181
history

这段代码加了两个换行,全部复制下来,去执行:
在这里插入图片描述
一气呵成!理性分析,源码考虑的情况可能就是这里了。这里的"\r\n"通过了正则匹配(对应的是\\s*)。
另一种可能
上面的解释有点牵强,我认为还有一种可能,当输入某个命令行的时候,还没有来得及执行,突然连接断了。
这个时候客户端会自动重连一段时间,加入这段时间内重连成功,会不会继续处理之前的命令行呢?

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值