热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

ZookeeperWatcher核心机制·安全认证(ACL)·实际应用

zookeeper有watch事件,是一次性触发的,当watch监视的数据发生变化时,通知设置了该watch的client,即watcher。同样,其watcher是监听数据发送了某些变化

zookeeper有watch事件,是一次性触发的,当watch监视的数据发生变化时,通知设置了该watch的client,即watcher。

同样,其watcher是监听数据发送了某些变化,那就一定会有相应的事件类型和状态类型

事件类型:

  • EventType.NodeCreated 节点创建
  • EventType.NodeDeleted节点删除
  • EventType.NodeDataChanged节点数据改变
  • EventType.NodeChildChanged子节点改变

状态类型

  • KeeperState.Disconnected
  • KeeperState.SyncConnected
  • KeeperState.AuthFailed
  • KeeperState.Expired

watcher简单示例

/** * Zookeeper Wathcher * 本类就是一个Watcher类(实现了org.apache.zookeeper.Watcher类) */
public class ZooKeeperWatcher implements Watcher {

    /** 定义原子变量 */
    AtomicInteger seq = new AtomicInteger();
    /** 定义session失效时间 */
    private static final int SESSION_TIMEOUT = 10000;
    /** zookeeper服务器地址 */
    private static final String CONNECTION_ADDR = "192.168.252.132:2181";
    /** zk父路径设置 */
    private static final String PARENT_PATH = "/testWatch";
    /** zk子路径设置 */
    private static final String CHILDREN_PATH = "/testWatch/children";
    /** 进入标识 */
    private static final String LOG_PREFIX_OF_MAIN = "【Main】";
    /** zk变量 */
    private ZooKeeper zk = null;
    /** 信号量设置,用于等待zookeeper连接建立之后 通知阻塞程序继续向下执行 */
    private CountDownLatch cOnnectedSemaphore= new CountDownLatch(1);

    /** * 创建ZK连接 * @param connectAddr ZK服务器地址列表 * @param sessionTimeout Session超时时间 */
    public void createConnection(String connectAddr, int sessionTimeout) {
        this.releaseConnection();
        try {
            zk = new ZooKeeper(connectAddr, sessionTimeout, this);
            System.out.println(LOG_PREFIX_OF_MAIN + "开始连接ZK服务器");
            connectedSemaphore.await();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /** * 关闭ZK连接 */
    public void releaseConnection() {
        if (this.zk != null) {
            try {
                this.zk.close();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    /** * 创建节点 * @param path 节点路径 * @param data 数据内容 * @return boolean */
    public boolean createPath(String path, String data) {
        try {
            //watcher是一次性的
            //设置监控(由于zookeeper的监控都是一次性的所以 每次必须设置监控)
            this.zk.exists(path, true);
            System.out.println(LOG_PREFIX_OF_MAIN + "节点创建成功, Path: " + 
                               this.zk.create(  /**路径*/ 
                                                path, 
                                                /**数据*/
                                                data.getBytes(), 
                                                /**所有可见*/
                                                Ids.OPEN_ACL_UNSAFE, 
                                                /**永久存储*/
                                                CreateMode.PERSISTENT ) +   
                               ", content: " + data);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
        return true;
    }

    /** * 读取指定节点数据内容 * @param path 节点路径 * @return */
    public String readData(String path, boolean needWatch) {
        try {
            return new String(this.zk.getData(path, needWatch, null));
        } catch (Exception e) {
            e.printStackTrace();
            return "";
        }
    }

    /** * 更新指定节点数据内容 * @param path 节点路径 * @param data 数据内容 * @return */
    public boolean writeData(String path, String data) {
        try {
            System.out.println(LOG_PREFIX_OF_MAIN + "更新数据成功,path:" + path + ", stat: " +
                                this.zk.setData(path, data.getBytes(), -1));
        } catch (Exception e) {
            e.printStackTrace();
        }
        return false;
    }

    /** * 删除指定节点 * * @param path * 节点path */
    public void deleteNode(String path) {
        try {
            this.zk.delete(path, -1);
            System.out.println(LOG_PREFIX_OF_MAIN + "删除节点成功,path:" + path);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /** * 判断指定节点是否存在 * @param path 节点路径 */
    public Stat exists(String path, boolean needWatch) {
        try {
            return this.zk.exists(path, needWatch);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    /** * 获取子节点 * @param path 节点路径 */
    private List getChildren(String path, boolean needWatch) {
        try {
            return this.zk.getChildren(path, needWatch);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    /** * 删除所有节点 */
    public void deleteAllTestPath() {
        if(this.exists(CHILDREN_PATH, false) != null){
            this.deleteNode(CHILDREN_PATH);
        }
        if(this.exists(PARENT_PATH, false) != null){
            this.deleteNode(PARENT_PATH);
        }       
    }

    /** * 收到来自Server的Watcher通知后的处理。 */
    @Override
    public void process(WatchedEvent event) {

        System.out.println("进入 process 。。。。。event = " + event);

        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        if (event == null) {
            return;
        }

        // 连接状态
        KeeperState keeperState = event.getState();
        // 事件类型
        EventType eventType = event.getType();
        // 受影响的path
        String path = event.getPath();

        String logPrefix = "【Watcher-" + this.seq.incrementAndGet() + "】";

        System.out.println(logPrefix + "收到Watcher通知");
        System.out.println(logPrefix + "连接状态:\t" + keeperState.toString());
        System.out.println(logPrefix + "事件类型:\t" + eventType.toString());

        if (KeeperState.SyncCOnnected== keeperState) {
            // 成功连接上ZK服务器
            if (EventType.NOne== eventType) {
                System.out.println(logPrefix + "成功连接上ZK服务器");
                connectedSemaphore.countDown();
            } 
            //创建节点
            else if (EventType.NodeCreated == eventType) {
                System.out.println(logPrefix + "节点创建");
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                this.exists(path, true);
            } 
            //更新节点
            else if (EventType.NodeDataChanged == eventType) {
                System.out.println(logPrefix + "节点数据更新");
                System.out.println("我看看走不走这里........");
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(logPrefix + "数据内容: " + this.readData(PARENT_PATH, true));
            } 
            //更新子节点
            else if (EventType.NodeChildrenChanged == eventType) {
                System.out.println(logPrefix + "子节点变更");
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(logPrefix + "子节点列表:" + this.getChildren(PARENT_PATH, true));
            } 
            //删除节点
            else if (EventType.NodeDeleted == eventType) {
                System.out.println(logPrefix + "节点 " + path + " 被删除");
            }
            else ;
        } 
        else if (KeeperState.DiscOnnected== keeperState) {
            System.out.println(logPrefix + "与ZK服务器断开连接");
        } 
        else if (KeeperState.AuthFailed == keeperState) {
            System.out.println(logPrefix + "权限检查失败");
        } 
        else if (KeeperState.Expired == keeperState) {
            System.out.println(logPrefix + "会话失效");
        }
        else ;

        System.out.println("--------------------------------------------");

    }

    /** * 方法名称:测试zookeeper监控
* 概要说明:主要测试watch功能
* @param args * @throws Exception */
public static void main(String[] args) throws Exception { //建立watcher ZooKeeperWatcher zkWatch = new ZooKeeperWatcher(); //创建连接 zkWatch.createConnection(CONNECTION_ADDR, SESSION_TIMEOUT); //System.out.println(zkWatch.zk.toString()); Thread.sleep(1000); // 清理节点 zkWatch.deleteAllTestPath(); if (zkWatch.createPath(PARENT_PATH, System.currentTimeMillis() + "")) { Thread.sleep(1000); // 读取数据 System.out.println("---------------------- read parent ----------------------------"); zkWatch.readData(PARENT_PATH, true); // 读取子节点 System.out.println("---------------------- read children path ----------------------------"); zkWatch.getChildren(PARENT_PATH, true); // 更新数据 zkWatch.writeData(PARENT_PATH, System.currentTimeMillis() + ""); Thread.sleep(1000); // 创建子节点 zkWatch.createPath(CHILDREN_PATH, System.currentTimeMillis() + ""); Thread.sleep(1000); zkWatch.writeData(CHILDREN_PATH, System.currentTimeMillis() + ""); } Thread.sleep(50000); // 清理节点 zkWatch.deleteAllTestPath(); Thread.sleep(1000); zkWatch.releaseConnection(); } }

Zookeeper中的Access Control(ACL)

概述
传统的文件系统中,ACL分为两个维度,一个是属组,一个是权限,子目录/文件默认继承父目录的ACL。而在Zookeeper中,node的ACL是没有继承关系的,是独立控制的。Zookeeper的ACL,可以从三个维度来理解:一是scheme; 二是user; 三是permission,通常表示为scheme:id:permissions, 下面从这三个方面分别来介绍:

  1. scheme: scheme对应于采用哪种方案来进行权限管理,zookeeper实现了一个pluggable的ACL方案,可以通过扩展scheme,来扩展ACL的机制。zookeeper-3.4.4缺省支持下面几种scheme:

    • world: 它下面只有一个id, 叫anyone, world:anyone代表任何人,zookeeper中对所有人有权限的结点就是属于world:anyone的

    • auth: 它不需要id, 只要是通过authentication的user都有权限(zookeeper支持通过kerberos来进行authencation, 也支持username/password形式的authentication)

    • digest: 它对应的id为username:BASE64(SHA1(password)),它需要先通过username:password形式的authentication

    • ip: 它对应的id为客户机的IP地址,设置的时候可以设置一个ip段,比如ip:192.168.1.0/16, 表示匹配前16个bit的IP段

    • super: 在这种scheme情况下,对应的id拥有超级权限,可以做任何事情(cdrwa)

    • sasl: sasl的对应的id,是一个通过sasl authentication用户的id,zookeeper-3.4.4中的sasl authentication是通过kerberos来实现的,也就是说用户只有通过了kerberos认证,才能访问它有权限的node.

2 . id: id与scheme是紧密相关的,具体的情况在上面介绍scheme的过程都已介绍,这里不再赘述。
3. permission: zookeeper目前支持下面一些权限:

CREATE(c): 创建权限,可以在在当前node下创建child node DELETE(d): 删除权限,可以删除当前的node
READ(r): 读权限,可以获取当前node的数据,可以list当前node所有的child nodes WRITE(w):
写权限,可以向当前node写数据 ADMIN(a): 管理权限,可以设置当前node的permission


digest模式下的示例

public class ZookeeperAuth implements Watcher {

    /** 连接地址 */
    final static String CONNECT_ADDR = "192.168.252.132:2181";
    /** 测试路径 */
    final static String PATH = "/testAuth";
    final static String PATH_DEL = "/testAuth/delNode";
    /** 认证类型 */
    final static String authentication_type = "digest";
    /** 认证正确方法 */
    final static String correctAuthentication = "123456";
    /** 认证错误方法 */
    final static String badAuthentication = "654321";

    static ZooKeeper zk = null;
    /** 计时器 */
    AtomicInteger seq = new AtomicInteger();
    /** 标识 */
    private static final String LOG_PREFIX_OF_MAIN = "【Main】";

    private CountDownLatch cOnnectedSemaphore= new CountDownLatch(1);

    @Override
    public void process(WatchedEvent event) {
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        if (event==null) {
            return;
        }
        // 连接状态
        KeeperState keeperState = event.getState();
        // 事件类型
        EventType eventType = event.getType();
        // 受影响的path
        String path = event.getPath();

        String logPrefix = "【Watcher-" + this.seq.incrementAndGet() + "】";

        System.out.println(logPrefix + "收到Watcher通知");
        System.out.println(logPrefix + "连接状态:\t" + keeperState.toString());
        System.out.println(logPrefix + "事件类型:\t" + eventType.toString());
        if (KeeperState.SyncCOnnected== keeperState) {
            // 成功连接上ZK服务器
            if (EventType.NOne== eventType) {
                System.out.println(logPrefix + "成功连接上ZK服务器");
                connectedSemaphore.countDown();
            } 
        } else if (KeeperState.DiscOnnected== keeperState) {
            System.out.println(logPrefix + "与ZK服务器断开连接");
        } else if (KeeperState.AuthFailed == keeperState) {
            System.out.println(logPrefix + "权限检查失败");
        } else if (KeeperState.Expired == keeperState) {
            System.out.println(logPrefix + "会话失效");
        }
        System.out.println("--------------------------------------------");
    }
    /** * 创建ZK连接 * * @param connectString * ZK服务器地址列表 * @param sessionTimeout * Session超时时间 */
    public void createConnection(String connectString, int sessionTimeout) {
        this.releaseConnection();
        try {
            zk = new ZooKeeper(connectString, sessionTimeout, this);
            //添加节点授权
            zk.addAuthInfo(authentication_type,correctAuthentication.getBytes());
            System.out.println(LOG_PREFIX_OF_MAIN + "开始连接ZK服务器");
            //倒数等待
            connectedSemaphore.await();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /** * 关闭ZK连接 */
    public void releaseConnection() {
        if (this.zk!=null) {
            try {
                this.zk.close();
            } catch (InterruptedException e) {
            }
        }
    }

    /** * * 方法名称:测试函数
* 概要说明:测试认证
* @param args * @throws Exception */
public static void main(String[] args) throws Exception { ZookeeperAuth testAuth = new ZookeeperAuth(); testAuth.createConnection(CONNECT_ADDR,2000); List acls = new ArrayList(1); for (ACL ids_acl : Ids.CREATOR_ALL_ACL) { acls.add(ids_acl); } try { zk.create(PATH, "init content".getBytes(), acls, CreateMode.PERSISTENT); System.out.println("使用授权key:" + correctAuthentication + "创建节点:"+ PATH + ", 初始内容是: init content"); } catch (Exception e) { e.printStackTrace(); } try { zk.create(PATH_DEL, "will be deleted! ".getBytes(), acls, CreateMode.PERSISTENT); System.out.println("使用授权key:" + correctAuthentication + "创建节点:"+ PATH_DEL + ", 初始内容是: init content"); } catch (Exception e) { e.printStackTrace(); } // 获取数据 getDataByNoAuthentication(); getDataByBadAuthentication(); getDataByCorrectAuthentication(); // 更新数据 updateDataByNoAuthentication(); updateDataByBadAuthentication(); updateDataByCorrectAuthentication(); // 删除数据 deleteNodeByBadAuthentication(); deleteNodeByNoAuthentication(); deleteNodeByCorrectAuthentication(); // Thread.sleep(1000); deleteParent(); //释放连接 testAuth.releaseConnection(); } /** 获取数据:采用错误的密码 */ static void getDataByBadAuthentication() { String prefix = "[使用错误的授权信息]"; try { ZooKeeper badzk = new ZooKeeper(CONNECT_ADDR, 2000, null); //授权 badzk.addAuthInfo(authentication_type,badAuthentication.getBytes()); Thread.sleep(2000); System.out.println(prefix + "获取数据:" + PATH); System.out.println(prefix + "成功获取数据:" + badzk.getData(PATH, false, null)); } catch (Exception e) { System.err.println(prefix + "获取数据失败,原因:" + e.getMessage()); } } /** 获取数据:不采用密码 */ static void getDataByNoAuthentication() { String prefix = "[不使用任何授权信息]"; try { System.out.println(prefix + "获取数据:" + PATH); ZooKeeper nozk = new ZooKeeper(CONNECT_ADDR, 2000, null); Thread.sleep(2000); System.out.println(prefix + "成功获取数据:" + nozk.getData(PATH, false, null)); } catch (Exception e) { System.err.println(prefix + "获取数据失败,原因:" + e.getMessage()); } } /** 采用正确的密码 */ static void getDataByCorrectAuthentication() { String prefix = "[使用正确的授权信息]"; try { System.out.println(prefix + "获取数据:" + PATH); System.out.println(prefix + "成功获取数据:" + zk.getData(PATH, false, null)); } catch (Exception e) { System.out.println(prefix + "获取数据失败,原因:" + e.getMessage()); } } /** * 更新数据:不采用密码 */ static void updateDataByNoAuthentication() { String prefix = "[不使用任何授权信息]"; System.out.println(prefix + "更新数据: " + PATH); try { ZooKeeper nozk = new ZooKeeper(CONNECT_ADDR, 2000, null); Thread.sleep(2000); Stat stat = nozk.exists(PATH, false); if (stat!=null) { nozk.setData(PATH, prefix.getBytes(), -1); System.out.println(prefix + "更新成功"); } } catch (Exception e) { System.err.println(prefix + "更新失败,原因是:" + e.getMessage()); } } /** * 更新数据:采用错误的密码 */ static void updateDataByBadAuthentication() { String prefix = "[使用错误的授权信息]"; System.out.println(prefix + "更新数据:" + PATH); try { ZooKeeper badzk = new ZooKeeper(CONNECT_ADDR, 2000, null); //授权 badzk.addAuthInfo(authentication_type,badAuthentication.getBytes()); Thread.sleep(2000); Stat stat = badzk.exists(PATH, false); if (stat!=null) { badzk.setData(PATH, prefix.getBytes(), -1); System.out.println(prefix + "更新成功"); } } catch (Exception e) { System.err.println(prefix + "更新失败,原因是:" + e.getMessage()); } } /** * 更新数据:采用正确的密码 */ static void updateDataByCorrectAuthentication() { String prefix = "[使用正确的授权信息]"; System.out.println(prefix + "更新数据:" + PATH); try { Stat stat = zk.exists(PATH, false); if (stat!=null) { zk.setData(PATH, prefix.getBytes(), -1); System.out.println(prefix + "更新成功"); } } catch (Exception e) { System.err.println(prefix + "更新失败,原因是:" + e.getMessage()); } } /** * 不使用密码 删除节点 */ static void deleteNodeByNoAuthentication() throws Exception { String prefix = "[不使用任何授权信息]"; try { System.out.println(prefix + "删除节点:" + PATH_DEL); ZooKeeper nozk = new ZooKeeper(CONNECT_ADDR, 2000, null); Thread.sleep(2000); Stat stat = nozk.exists(PATH_DEL, false); if (stat!=null) { nozk.delete(PATH_DEL,-1); System.out.println(prefix + "删除成功"); } } catch (Exception e) { System.err.println(prefix + "删除失败,原因是:" + e.getMessage()); } } /** * 采用错误的密码删除节点 */ static void deleteNodeByBadAuthentication() throws Exception { String prefix = "[使用错误的授权信息]"; try { System.out.println(prefix + "删除节点:" + PATH_DEL); ZooKeeper badzk = new ZooKeeper(CONNECT_ADDR, 2000, null); //授权 使用错误的授权码badAuthentication badzk.addAuthInfo(authentication_type,badAuthentication.getBytes()); Thread.sleep(2000); Stat stat = badzk.exists(PATH_DEL, false); if (stat!=null) { badzk.delete(PATH_DEL, -1); System.out.println(prefix + "删除成功"); } } catch (Exception e) { System.err.println(prefix + "删除失败,原因是:" + e.getMessage()); } } /** * 使用正确的密码删除节点 */ static void deleteNodeByCorrectAuthentication() throws Exception { String prefix = "[使用正确的授权信息]"; try { System.out.println(prefix + "删除节点:" + PATH_DEL); Stat stat = zk.exists(PATH_DEL, false); if (stat!=null) { zk.delete(PATH_DEL, -1); System.out.println(prefix + "删除成功"); } } catch (Exception e) { System.out.println(prefix + "删除失败,原因是:" + e.getMessage()); } } /** * 使用正确的密码删除节点 */ static void deleteParent() throws Exception { try { Stat stat = zk.exists(PATH_DEL, false); if (stat == null) { zk.delete(PATH, -1); } } catch (Exception e) { e.printStackTrace(); } } }

推荐阅读
  • 先看官方文档TheJavaTutorialshavebeenwrittenforJDK8.Examplesandpracticesdescribedinthispagedontta ... [详细]
  • 向QTextEdit拖放文件的方法及实现步骤
    本文介绍了在使用QTextEdit时如何实现拖放文件的功能,包括相关的方法和实现步骤。通过重写dragEnterEvent和dropEvent函数,并结合QMimeData和QUrl等类,可以轻松实现向QTextEdit拖放文件的功能。详细的代码实现和说明可以参考本文提供的示例代码。 ... [详细]
  • 开发笔记:加密&json&StringIO模块&BytesIO模块
    篇首语:本文由编程笔记#小编为大家整理,主要介绍了加密&json&StringIO模块&BytesIO模块相关的知识,希望对你有一定的参考价值。一、加密加密 ... [详细]
  • 关键词:Golang, Cookie, 跟踪位置, net/http/cookiejar, package main, golang.org/x/net/publicsuffix, io/ioutil, log, net/http, net/http/cookiejar ... [详细]
  • 个人学习使用:谨慎参考1Client类importcom.thoughtworks.gauge.Step;importcom.thoughtworks.gauge.T ... [详细]
  • CF:3D City Model(小思维)问题解析和代码实现
    本文通过解析CF:3D City Model问题,介绍了问题的背景和要求,并给出了相应的代码实现。该问题涉及到在一个矩形的网格上建造城市的情景,每个网格单元可以作为建筑的基础,建筑由多个立方体叠加而成。文章详细讲解了问题的解决思路,并给出了相应的代码实现供读者参考。 ... [详细]
  • 本文介绍了解决Netty拆包粘包问题的一种方法——使用特殊结束符。在通讯过程中,客户端和服务器协商定义一个特殊的分隔符号,只要没有发送分隔符号,就代表一条数据没有结束。文章还提供了服务端的示例代码。 ... [详细]
  • Iamtryingtomakeaclassthatwillreadatextfileofnamesintoanarray,thenreturnthatarra ... [详细]
  • 在Android开发中,使用Picasso库可以实现对网络图片的等比例缩放。本文介绍了使用Picasso库进行图片缩放的方法,并提供了具体的代码实现。通过获取图片的宽高,计算目标宽度和高度,并创建新图实现等比例缩放。 ... [详细]
  • CSS3选择器的使用方法详解,提高Web开发效率和精准度
    本文详细介绍了CSS3新增的选择器方法,包括属性选择器的使用。通过CSS3选择器,可以提高Web开发的效率和精准度,使得查找元素更加方便和快捷。同时,本文还对属性选择器的各种用法进行了详细解释,并给出了相应的代码示例。通过学习本文,读者可以更好地掌握CSS3选择器的使用方法,提升自己的Web开发能力。 ... [详细]
  • Java容器中的compareto方法排序原理解析
    本文从源码解析Java容器中的compareto方法的排序原理,讲解了在使用数组存储数据时的限制以及存储效率的问题。同时提到了Redis的五大数据结构和list、set等知识点,回忆了作者大学时代的Java学习经历。文章以作者做的思维导图作为目录,展示了整个讲解过程。 ... [详细]
  • 阿,里,云,物,联网,net,core,客户端,czgl,aliiotclient, ... [详细]
  • JavaSE笔试题-接口、抽象类、多态等问题解答
    本文解答了JavaSE笔试题中关于接口、抽象类、多态等问题。包括Math类的取整数方法、接口是否可继承、抽象类是否可实现接口、抽象类是否可继承具体类、抽象类中是否可以有静态main方法等问题。同时介绍了面向对象的特征,以及Java中实现多态的机制。 ... [详细]
  • Java在运行已编译完成的类时,是通过java虚拟机来装载和执行的,java虚拟机通过操作系统命令JAVA_HOMEbinjava–option来启 ... [详细]
  • CEPH LIO iSCSI Gateway及其使用参考文档
    本文介绍了CEPH LIO iSCSI Gateway以及使用该网关的参考文档,包括Ceph Block Device、CEPH ISCSI GATEWAY、USING AN ISCSI GATEWAY等。同时提供了多个参考链接,详细介绍了CEPH LIO iSCSI Gateway的配置和使用方法。 ... [详细]
author-avatar
蔡中均扬玫韵如
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有