Ftr: add nacos registry (#107)

relate #46
diff --git a/.gitignore b/.gitignore
index 8e58bcf..b0e6c5f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,5 +3,5 @@
 **/*.rs.bk
 .vscode/
 .idea/
+helloworld
 .DS_Store
-helloworld
\ No newline at end of file
diff --git a/Cargo.toml b/Cargo.toml
index 45c4492..785e903 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -3,6 +3,7 @@
   "xds",
   "registry",
   "registry-zookeeper",
+  "registry-nacos",
   "metadata",
   "common",
   "config",
diff --git a/registry-nacos/Cargo.toml b/registry-nacos/Cargo.toml
new file mode 100644
index 0000000..68b24fd
--- /dev/null
+++ b/registry-nacos/Cargo.toml
@@ -0,0 +1,18 @@
+[package]
+name = "dubbo-registry-nacos"
+version = "0.2.0"
+edition = "2021"
+license = "Apache-2.0"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+nacos-sdk = { version = "0.2", features = ["naming", "auth-by-http"] }
+dubbo = {path = "../dubbo/", version = "0.2.0"}
+serde_json = "1.0"
+serde = {version = "1.0.145",features = ["derive"]}
+tracing = "0.1"
+anyhow = "1.0.66"
+
+[dev-dependencies]
+tracing-subscriber = "0.3.16"
\ No newline at end of file
diff --git a/registry-nacos/LICENSE b/registry-nacos/LICENSE
new file mode 100644
index 0000000..d645695
--- /dev/null
+++ b/registry-nacos/LICENSE
@@ -0,0 +1,202 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
diff --git a/registry-nacos/src/lib.rs b/registry-nacos/src/lib.rs
new file mode 100644
index 0000000..aa8cc4d
--- /dev/null
+++ b/registry-nacos/src/lib.rs
@@ -0,0 +1,18 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+pub mod nacos_registry;
+mod utils;
diff --git a/registry-nacos/src/nacos_registry.rs b/registry-nacos/src/nacos_registry.rs
new file mode 100644
index 0000000..a020e6b
--- /dev/null
+++ b/registry-nacos/src/nacos_registry.rs
@@ -0,0 +1,711 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+use std::{
+    collections::{HashMap, HashSet},
+    sync::{Arc, Mutex},
+};
+
+use anyhow::anyhow;
+use dubbo::{
+    common::url::Url,
+    registry::{NotifyListener, Registry, ServiceEvent},
+};
+use nacos_sdk::api::naming::{NamingService, NamingServiceBuilder, ServiceInstance};
+use tracing::{error, info, warn};
+
+use crate::utils::{build_nacos_client_props, is_concrete_str, is_wildcard_str, match_range};
+
+const VERSION_KEY: &str = "version";
+
+const GROUP_KEY: &str = "group";
+
+const DEFAULT_GROUP: &str = "DEFAULT_GROUP";
+
+const PROVIDER_SIDE: &str = "provider";
+
+const DEFAULT_CATEGORY: &str = PROVIDERS_CATEGORY;
+
+const SIDE_KEY: &str = "side";
+
+const REGISTER_CONSUMER_URL_KEY: &str = "register-consumer-url";
+
+const SERVICE_NAME_SEPARATOR: &str = ":";
+
+const CATEGORY_KEY: &str = "category";
+
+const PROVIDERS_CATEGORY: &str = "providers";
+
+#[allow(dead_code)]
+const ADMIN_PROTOCOL: &str = "admin";
+
+#[allow(dead_code)]
+const INNERCLASS_SYMBOL: &str = "$";
+
+#[allow(dead_code)]
+const INNERCLASS_COMPATIBLE_SYMBOL: &str = "___";
+
+pub struct NacosRegistry {
+    nacos_naming_service: Arc<dyn NamingService>,
+    listeners: Mutex<HashMap<String, HashSet<Arc<NotifyListenerWrapper>>>>,
+}
+
+impl NacosRegistry {
+    pub fn new(url: Url) -> Self {
+        let (nacos_client_props, enable_auth) = build_nacos_client_props(&url);
+
+        let mut nacos_naming_builder = NamingServiceBuilder::new(nacos_client_props);
+
+        if enable_auth {
+            nacos_naming_builder = nacos_naming_builder.enable_auth_plugin_http();
+        }
+
+        let nacos_naming_service = nacos_naming_builder.build().unwrap();
+
+        Self {
+            nacos_naming_service: Arc::new(nacos_naming_service),
+            listeners: Mutex::new(HashMap::new()),
+        }
+    }
+
+    #[allow(dead_code)]
+    fn get_subscribe_service_names(&self, service_name: &NacosServiceName) -> HashSet<String> {
+        if service_name.is_concrete() {
+            let mut set = HashSet::new();
+            let service_subscribe_name = service_name.to_subscriber_str();
+            let service_subscriber_legacy_name = service_name.to_subscriber_legacy_string();
+            if service_subscribe_name.eq(&service_subscriber_legacy_name) {
+                set.insert(service_subscribe_name);
+            } else {
+                set.insert(service_subscribe_name);
+                set.insert(service_subscriber_legacy_name);
+            }
+
+            set
+        } else {
+            let list_view = self.nacos_naming_service.get_service_list(
+                1,
+                i32::MAX,
+                Some(
+                    service_name
+                        .get_group_with_default(DEFAULT_GROUP)
+                        .to_string(),
+                ),
+            );
+            if let Err(e) = list_view {
+                error!("list service instances occur an error: {:?}", e);
+                return HashSet::default();
+            }
+
+            let list_view = list_view.unwrap();
+            let set: HashSet<String> = list_view
+                .0
+                .into_iter()
+                .filter(|service_name| service_name.split(SERVICE_NAME_SEPARATOR).count() == 4)
+                .map(|service_name| NacosServiceName::from_service_name_str(&service_name))
+                .filter(|other_service_name| service_name.is_compatible(other_service_name))
+                .map(|service_name| service_name.to_subscriber_str())
+                .collect();
+            set
+        }
+    }
+}
+
+impl NacosRegistry {
+    fn create_nacos_service_instance(url: Url) -> ServiceInstance {
+        let ip = url.ip;
+        let port = url.port;
+        nacos_sdk::api::naming::ServiceInstance {
+            ip,
+            port: port.parse().unwrap(),
+            metadata: url.params,
+            ..Default::default()
+        }
+    }
+}
+
+impl Registry for NacosRegistry {
+    type NotifyListener = Arc<dyn NotifyListener + Sync + Send + 'static>;
+
+    fn register(&mut self, url: Url) -> Result<(), dubbo::StdError> {
+        let side = url.get_param(SIDE_KEY.to_owned()).unwrap_or_default();
+        let register_consumer = url
+            .get_param(REGISTER_CONSUMER_URL_KEY.to_owned())
+            .unwrap_or_else(|| false.to_string())
+            .parse::<bool>()
+            .unwrap_or(false);
+        if side.ne(PROVIDER_SIDE) && !register_consumer {
+            warn!("Please set 'dubbo.registry.parameters.register-consumer-url=true' to turn on consumer url registration.");
+            return Ok(());
+        }
+
+        let nacos_service_name = NacosServiceName::new(&url);
+
+        let group_name = Some(
+            nacos_service_name
+                .get_group_with_default(DEFAULT_GROUP)
+                .to_string(),
+        );
+        let nacos_service_name = nacos_service_name.to_register_str();
+
+        let nacos_service_instance = Self::create_nacos_service_instance(url);
+
+        info!("register service: {}", nacos_service_name);
+
+        let ret = self.nacos_naming_service.register_service(
+            nacos_service_name,
+            group_name,
+            nacos_service_instance,
+        );
+        if let Err(e) = ret {
+            error!("register to nacos occur an error: {:?}", e);
+            return Err(anyhow!("register to nacos occur an error: {:?}", e).into());
+        }
+
+        Ok(())
+    }
+
+    fn unregister(&mut self, url: Url) -> Result<(), dubbo::StdError> {
+        let nacos_service_name = NacosServiceName::new(&url);
+
+        let group_name = Some(
+            nacos_service_name
+                .get_group_with_default(DEFAULT_GROUP)
+                .to_string(),
+        );
+        let nacos_service_name = nacos_service_name.to_register_str();
+
+        let nacos_service_instance = Self::create_nacos_service_instance(url);
+
+        info!("deregister service: {}", nacos_service_name);
+
+        let ret = self.nacos_naming_service.deregister_instance(
+            nacos_service_name,
+            group_name,
+            nacos_service_instance,
+        );
+        if let Err(e) = ret {
+            error!("deregister service from nacos occur an error: {:?}", e);
+            return Err(anyhow!("deregister service from nacos occur an error: {:?}", e).into());
+        }
+        Ok(())
+    }
+
+    fn subscribe(&self, url: Url, listener: Self::NotifyListener) -> Result<(), dubbo::StdError> {
+        let service_name = NacosServiceName::new(&url);
+        let url_str = url.to_url();
+
+        info!("subscribe: {}", &url_str);
+
+        let nacos_listener: Arc<NotifyListenerWrapper> = {
+            let listeners = self.listeners.lock();
+            if let Err(e) = listeners {
+                error!("subscribe service failed: {:?}", e);
+                return Err(anyhow!("subscribe service failed: {:?}", e).into());
+            }
+
+            let mut listeners = listeners.unwrap();
+            let listener_set = listeners.get_mut(url_str.as_str());
+
+            let wrapper = Arc::new(NotifyListenerWrapper(listener));
+            if let Some(listener_set) = listener_set {
+                listener_set.insert(wrapper.clone());
+            } else {
+                let mut hash_set = HashSet::new();
+                hash_set.insert(wrapper.clone());
+                listeners.insert(url_str, hash_set);
+            }
+
+            wrapper
+        };
+
+        let ret = self.nacos_naming_service.subscribe(
+            service_name.to_subscriber_str(),
+            Some(
+                service_name
+                    .get_group_with_default(DEFAULT_GROUP)
+                    .to_string(),
+            ),
+            Vec::new(),
+            nacos_listener,
+        );
+
+        if let Err(e) = ret {
+            error!("subscribe service failed: {:?}", e);
+            return Err(anyhow!("subscribe service failed: {:?}", e).into());
+        }
+
+        Ok(())
+    }
+
+    fn unsubscribe(&self, url: Url, listener: Self::NotifyListener) -> Result<(), dubbo::StdError> {
+        let service_name = NacosServiceName::new(&url);
+        let url_str = url.to_url();
+        info!("unsubscribe: {}", &url_str);
+
+        let nacos_listener: Arc<NotifyListenerWrapper> = {
+            let listeners = self.listeners.lock();
+            if let Err(e) = listeners {
+                error!("unsubscribe service failed: {:?}", e);
+                return Err(anyhow!("unsubscribe service failed: {:?}", e).into());
+            }
+
+            let mut listeners = listeners.unwrap();
+            let listener_set = listeners.get_mut(url_str.as_str());
+            if listener_set.is_none() {
+                return Ok(());
+            }
+
+            let listener_set = listener_set.unwrap();
+
+            let listener = Arc::new(NotifyListenerWrapper(listener));
+            let listener = listener_set.take(&listener);
+            if listener.is_none() {
+                return Ok(());
+            }
+
+            listener.unwrap()
+        };
+
+        let ret = self.nacos_naming_service.unsubscribe(
+            service_name.to_subscriber_str(),
+            Some(
+                service_name
+                    .get_group_with_default(DEFAULT_GROUP)
+                    .to_string(),
+            ),
+            Vec::new(),
+            nacos_listener,
+        );
+
+        if let Err(e) = ret {
+            error!("unsubscribe service failed: {:?}", e);
+            return Err(anyhow!("unsubscribe service failed: {:?}", e).into());
+        }
+
+        Ok(())
+    }
+}
+
+struct NacosServiceName {
+    category: String,
+
+    service_interface: String,
+
+    version: String,
+
+    group: String,
+}
+
+impl NacosServiceName {
+    fn new(url: &Url) -> NacosServiceName {
+        let service_interface = url
+            .get_service_name()
+            .into_iter()
+            .next()
+            .unwrap_or_default();
+
+        let category = url.get_param(CATEGORY_KEY.to_owned()).unwrap_or_default();
+
+        let version = url.get_param(VERSION_KEY.to_owned()).unwrap_or_default();
+
+        let group = url.get_param(GROUP_KEY.to_owned()).unwrap_or_default();
+
+        Self {
+            category,
+            service_interface,
+            version,
+            group,
+        }
+    }
+
+    #[allow(dead_code)]
+    fn from_service_name_str(service_name_str: &str) -> Self {
+        let mut splitter = service_name_str.split(SERVICE_NAME_SEPARATOR);
+
+        let category = splitter.next().unwrap_or_default().to_string();
+        let service_interface = splitter.next().unwrap_or_default().to_string();
+        let version = splitter.next().unwrap_or_default().to_string();
+        let group = splitter.next().unwrap_or_default().to_string();
+
+        Self {
+            category,
+            service_interface,
+            version,
+            group,
+        }
+    }
+
+    #[allow(dead_code)]
+    fn version(&self) -> &str {
+        &self.version
+    }
+
+    #[allow(dead_code)]
+    fn get_version_with_default<'a>(&'a self, default: &'a str) -> &str {
+        if self.version.is_empty() {
+            default
+        } else {
+            &self.version
+        }
+    }
+
+    #[allow(dead_code)]
+    fn group(&self) -> &str {
+        &self.group
+    }
+
+    fn get_group_with_default<'a>(&'a self, default: &'a str) -> &str {
+        if self.group.is_empty() {
+            default
+        } else {
+            &self.group
+        }
+    }
+
+    #[allow(dead_code)]
+    fn category(&self) -> &str {
+        &self.category
+    }
+
+    #[allow(dead_code)]
+    fn get_category_with_default<'a>(&'a self, default: &'a str) -> &str {
+        if self.category.is_empty() {
+            default
+        } else {
+            &self.category
+        }
+    }
+
+    #[allow(dead_code)]
+    fn service_interface(&self) -> &str {
+        &self.service_interface
+    }
+
+    #[allow(dead_code)]
+    fn get_service_interface_with_default<'a>(&'a self, default: &'a str) -> &str {
+        if self.service_interface.is_empty() {
+            default
+        } else {
+            &self.service_interface
+        }
+    }
+
+    fn to_register_str(&self) -> String {
+        let category = if self.category.is_empty() {
+            DEFAULT_CATEGORY
+        } else {
+            &self.category
+        };
+        format!(
+            "{}:{}:{}:{}",
+            category, self.service_interface, self.version, self.group
+        )
+    }
+
+    fn to_subscriber_str(&self) -> String {
+        let category = if is_concrete_str(&self.service_interface) {
+            DEFAULT_CATEGORY
+        } else {
+            &self.category
+        };
+
+        format!(
+            "{}:{}:{}:{}",
+            category, self.service_interface, self.version, self.group
+        )
+    }
+
+    #[allow(dead_code)]
+    fn to_subscriber_legacy_string(&self) -> String {
+        let mut legacy_string = DEFAULT_CATEGORY.to_owned();
+        if !self.service_interface.is_empty() {
+            legacy_string.push_str(SERVICE_NAME_SEPARATOR);
+            legacy_string.push_str(&self.service_interface);
+        }
+
+        if !self.version.is_empty() {
+            legacy_string.push_str(SERVICE_NAME_SEPARATOR);
+            legacy_string.push_str(&self.version);
+        }
+
+        if !self.group.is_empty() {
+            legacy_string.push_str(SERVICE_NAME_SEPARATOR);
+            legacy_string.push_str(&self.group);
+        }
+
+        legacy_string
+    }
+
+    #[allow(dead_code)]
+    fn is_concrete(&self) -> bool {
+        is_concrete_str(&self.service_interface)
+            && is_concrete_str(&self.version)
+            && is_concrete_str(&self.group)
+    }
+
+    #[allow(dead_code)]
+    fn is_compatible(&self, other: &NacosServiceName) -> bool {
+        if !other.is_concrete() {
+            return false;
+        }
+
+        if !self.category.eq(&other.category) && !match_range(&self.category, &other.category) {
+            return false;
+        }
+
+        if is_wildcard_str(&self.version) {
+            return true;
+        }
+
+        if is_wildcard_str(&self.group) {
+            return true;
+        }
+
+        if !&self.version.eq(&other.version) && !match_range(&self.version, &other.version) {
+            return false;
+        }
+
+        if !self.group.eq(&other.group) && !match_range(&self.group, &other.group) {
+            return false;
+        }
+
+        true
+    }
+}
+
+struct NotifyListenerWrapper(Arc<dyn NotifyListener + Sync + Send + 'static>);
+
+impl std::hash::Hash for NotifyListenerWrapper {
+    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
+        let ptr = self.0.as_ref();
+        std::ptr::hash(ptr, state);
+    }
+}
+
+impl PartialEq for NotifyListenerWrapper {
+    fn eq(&self, other: &Self) -> bool {
+        let self_ptr = self.0.as_ref() as *const dyn NotifyListener;
+        let other_ptr = other.0.as_ref() as *const dyn NotifyListener;
+
+        let (self_data_ptr, _): (*const u8, *const u8) = unsafe { std::mem::transmute(self_ptr) };
+
+        let (other_data_ptr, _): (*const u8, *const u8) = unsafe { std::mem::transmute(other_ptr) };
+        self_data_ptr == other_data_ptr
+    }
+}
+
+impl Eq for NotifyListenerWrapper {}
+
+impl nacos_sdk::api::naming::NamingEventListener for NotifyListenerWrapper {
+    fn event(&self, event: Arc<nacos_sdk::api::naming::NamingChangeEvent>) {
+        let service_name = event.service_name.clone();
+        let instances = event.instances.as_ref();
+        let urls: Vec<Url>;
+        if let Some(instances) = instances {
+            urls = instances
+                .iter()
+                .filter_map(|data| {
+                    let url_str =
+                        format!("triple://{}:{}/{}", data.ip(), data.port(), service_name);
+                    Url::from_url(&url_str)
+                })
+                .collect();
+        } else {
+            urls = Vec::new();
+        }
+        let notify_event = ServiceEvent {
+            key: service_name,
+            action: String::from("CHANGE"),
+            service: urls,
+        };
+        self.0.notify(notify_event);
+    }
+}
+
+#[cfg(test)]
+pub mod tests {
+
+    use core::time;
+    use std::thread;
+
+    use tracing::metadata::LevelFilter;
+
+    use super::*;
+
+    #[test]
+    #[ignore]
+    pub fn test_register_to_nacos() {
+        tracing_subscriber::fmt()
+            .with_thread_names(true)
+            .with_file(true)
+            .with_level(true)
+            .with_line_number(true)
+            .with_thread_ids(true)
+            .with_max_level(LevelFilter::DEBUG)
+            .init();
+
+        let nacos_registry_url = Url::from_url("nacos://127.0.0.1:8848/org.apache.dubbo.registry.RegistryService?application=dubbo-demo-triple-api-provider&dubbo=2.0.2&interface=org.apache.dubbo.registry.RegistryService&pid=7015").unwrap();
+        let mut registry = NacosRegistry::new(nacos_registry_url);
+
+        let mut service_url = Url::from_url("tri://127.0.0.1:50052/org.apache.dubbo.demo.GreeterService?anyhost=true&application=dubbo-demo-triple-api-provider&background=false&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&interface=org.apache.dubbo.demo.GreeterService&methods=sayHello,sayHelloAsync&pid=7015&service-name-mapping=true&side=provider&timestamp=1670060843807").unwrap();
+        service_url
+            .params
+            .insert(SIDE_KEY.to_owned(), PROVIDER_SIDE.to_owned());
+
+        let ret = registry.register(service_url);
+
+        info!("register result: {:?}", ret);
+
+        let sleep_millis = time::Duration::from_secs(300);
+        thread::sleep(sleep_millis);
+    }
+
+    #[test]
+    #[ignore]
+    pub fn test_register_and_unregister() {
+        tracing_subscriber::fmt()
+            .with_thread_names(true)
+            .with_file(true)
+            .with_level(true)
+            .with_line_number(true)
+            .with_thread_ids(true)
+            .with_max_level(LevelFilter::DEBUG)
+            .init();
+
+        let nacos_registry_url = Url::from_url("nacos://127.0.0.1:8848/org.apache.dubbo.registry.RegistryService?application=dubbo-demo-triple-api-provider&dubbo=2.0.2&interface=org.apache.dubbo.registry.RegistryService&pid=7015").unwrap();
+        let mut registry = NacosRegistry::new(nacos_registry_url);
+
+        let mut service_url = Url::from_url("tri://127.0.0.1:9090/org.apache.dubbo.demo.GreeterService?anyhost=true&application=dubbo-demo-triple-api-provider&background=false&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&interface=org.apache.dubbo.demo.GreeterService&methods=sayHello,sayHelloAsync&pid=7015&service-name-mapping=true&side=provider&timestamp=1670060843807").unwrap();
+        service_url
+            .params
+            .insert(SIDE_KEY.to_owned(), PROVIDER_SIDE.to_owned());
+
+        let ret = registry.register(service_url);
+
+        info!("register result: {:?}", ret);
+
+        let sleep_millis = time::Duration::from_secs(10);
+        thread::sleep(sleep_millis);
+
+        let unregister_url = Url::from_url("tri://127.0.0.1:9090/org.apache.dubbo.demo.GreeterService?anyhost=true&application=dubbo-demo-triple-api-provider&background=false&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&interface=org.apache.dubbo.demo.GreeterService&methods=sayHello,sayHelloAsync&pid=7015&service-name-mapping=true&side=provider&timestamp=1670060843807").unwrap();
+        let ret = registry.unregister(unregister_url);
+
+        info!("deregister result: {:?}", ret);
+
+        let sleep_millis = time::Duration::from_secs(10);
+        thread::sleep(sleep_millis);
+    }
+
+    struct TestNotifyListener;
+    impl NotifyListener for TestNotifyListener {
+        fn notify(&self, event: ServiceEvent) {
+            info!("notified: {:?}", event.key);
+        }
+
+        fn notify_all(&self, event: ServiceEvent) {
+            info!("notify_all: {:?}", event.key);
+        }
+    }
+
+    #[test]
+    #[ignore]
+    fn test_subscribe() {
+        tracing_subscriber::fmt()
+            .with_thread_names(true)
+            .with_file(true)
+            .with_level(true)
+            .with_line_number(true)
+            .with_thread_ids(true)
+            .with_max_level(LevelFilter::DEBUG)
+            .init();
+
+        let nacos_registry_url = Url::from_url("nacos://127.0.0.1:8848/org.apache.dubbo.registry.RegistryService?application=dubbo-demo-triple-api-provider&dubbo=2.0.2&interface=org.apache.dubbo.registry.RegistryService&pid=7015").unwrap();
+        let mut registry = NacosRegistry::new(nacos_registry_url);
+
+        let mut service_url = Url::from_url("tri://127.0.0.1:50052/org.apache.dubbo.demo.GreeterService?anyhost=true&application=dubbo-demo-triple-api-provider&background=false&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&interface=org.apache.dubbo.demo.GreeterService&methods=sayHello,sayHelloAsync&pid=7015&service-name-mapping=true&side=provider&timestamp=1670060843807").unwrap();
+        service_url
+            .params
+            .insert(SIDE_KEY.to_owned(), PROVIDER_SIDE.to_owned());
+
+        let ret = registry.register(service_url);
+
+        info!("register result: {:?}", ret);
+
+        let subscribe_url = Url::from_url("provider://192.168.0.102:50052/org.apache.dubbo.demo.GreeterService?anyhost=true&application=dubbo-demo-triple-api-provider&background=false&bind.ip=192.168.0.102&bind.port=50052&category=configurators&check=false&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&interface=org.apache.dubbo.demo.GreeterService&ipv6=fd00:6cb1:58a2:8ddf:0:0:0:1000&methods=sayHello,sayHelloAsync&pid=44270&service-name-mapping=true&side=provider").unwrap();
+
+        let ret = registry.subscribe(subscribe_url, Arc::new(TestNotifyListener));
+
+        if let Err(e) = ret {
+            error!("error message: {:?}", e);
+            return;
+        }
+
+        let sleep_millis = time::Duration::from_secs(300);
+        thread::sleep(sleep_millis);
+    }
+
+    #[test]
+    #[ignore]
+    fn test_unsubscribe() {
+        tracing_subscriber::fmt()
+            .with_thread_names(true)
+            .with_file(true)
+            .with_level(true)
+            .with_line_number(true)
+            .with_thread_ids(true)
+            .with_max_level(LevelFilter::DEBUG)
+            .init();
+
+        let nacos_registry_url = Url::from_url("nacos://127.0.0.1:8848/org.apache.dubbo.registry.RegistryService?application=dubbo-demo-triple-api-provider&dubbo=2.0.2&interface=org.apache.dubbo.registry.RegistryService&pid=7015").unwrap();
+        let mut registry = NacosRegistry::new(nacos_registry_url);
+
+        let mut service_url = Url::from_url("tri://127.0.0.1:50052/org.apache.dubbo.demo.GreeterService?anyhost=true&application=dubbo-demo-triple-api-provider&background=false&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&interface=org.apache.dubbo.demo.GreeterService&methods=sayHello,sayHelloAsync&pid=7015&service-name-mapping=true&side=provider&timestamp=1670060843807").unwrap();
+        service_url
+            .params
+            .insert(SIDE_KEY.to_owned(), PROVIDER_SIDE.to_owned());
+
+        let ret = registry.register(service_url);
+
+        info!("register result: {:?}", ret);
+
+        let subscribe_url = Url::from_url("provider://192.168.0.102:50052/org.apache.dubbo.demo.GreeterService?anyhost=true&application=dubbo-demo-triple-api-provider&background=false&bind.ip=192.168.0.102&bind.port=50052&category=configurators&check=false&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&interface=org.apache.dubbo.demo.GreeterService&ipv6=fd00:6cb1:58a2:8ddf:0:0:0:1000&methods=sayHello,sayHelloAsync&pid=44270&service-name-mapping=true&side=provider").unwrap();
+
+        let listener = Arc::new(TestNotifyListener);
+
+        let ret = registry.subscribe(subscribe_url, listener.clone());
+
+        if let Err(e) = ret {
+            error!("error message: {:?}", e);
+            return;
+        }
+
+        let sleep_millis = time::Duration::from_secs(40);
+        thread::sleep(sleep_millis);
+
+        let unsubscribe_url = Url::from_url("provider://192.168.0.102:50052/org.apache.dubbo.demo.GreeterService?anyhost=true&application=dubbo-demo-triple-api-provider&background=false&bind.ip=192.168.0.102&bind.port=50052&category=configurators&check=false&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&interface=org.apache.dubbo.demo.GreeterService&ipv6=fd00:6cb1:58a2:8ddf:0:0:0:1000&methods=sayHello,sayHelloAsync&pid=44270&service-name-mapping=true&side=provider").unwrap();
+        let ret = registry.unsubscribe(unsubscribe_url, listener.clone());
+
+        if let Err(e) = ret {
+            error!("error message: {:?}", e);
+            return;
+        }
+
+        let sleep_millis = time::Duration::from_secs(40);
+        thread::sleep(sleep_millis);
+    }
+}
diff --git a/registry-nacos/src/utils/mod.rs b/registry-nacos/src/utils/mod.rs
new file mode 100644
index 0000000..94f905a
--- /dev/null
+++ b/registry-nacos/src/utils/mod.rs
@@ -0,0 +1,102 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+use dubbo::common::url::Url;
+use nacos_sdk::api::props::ClientProps;
+
+const APP_NAME_KEY: &str = "AppName";
+
+const UNKNOWN_APP: &str = "UnknownApp";
+
+const NAMESPACE_KEY: &str = "namespace";
+
+const DEFAULT_NAMESPACE: &str = "public";
+
+const USERNAME_KEY: &str = "username";
+
+const PASSWORD_KEY: &str = "password";
+
+const BACKUP_KEY: &str = "backup";
+
+const WILDCARD: &str = "*";
+
+const RANGE_STR_SEPARATOR: &str = ",";
+
+pub(crate) fn build_nacos_client_props(url: &Url) -> (nacos_sdk::api::props::ClientProps, bool) {
+    let host = &url.ip;
+    let port = &url.port;
+    let backup = url
+        .get_param(BACKUP_KEY.to_owned())
+        .map(|mut data| {
+            data.insert(0, ',');
+            data
+        })
+        .unwrap_or_default();
+    let server_addr = format!("{}:{}{}", host, port, backup);
+
+    let namespace = url
+        .get_param(NAMESPACE_KEY.to_owned())
+        .unwrap_or_else(|| DEFAULT_NAMESPACE.to_owned());
+    let app_name = url
+        .get_param(APP_NAME_KEY.to_owned())
+        .unwrap_or_else(|| UNKNOWN_APP.to_owned());
+    let username = url.get_param(USERNAME_KEY.to_owned()).unwrap_or_default();
+    let password = url.get_param(PASSWORD_KEY.to_owned()).unwrap_or_default();
+
+    let enable_auth = !password.is_empty() && !username.is_empty();
+
+    // todo ext parameters
+
+    let mut client_props = ClientProps::new();
+
+    client_props = client_props
+        .server_addr(server_addr)
+        .namespace(namespace)
+        .app_name(app_name)
+        .auth_username(username)
+        .auth_password(password);
+
+    (client_props, enable_auth)
+}
+
+pub(crate) fn is_wildcard_str(str: &str) -> bool {
+    str.eq(WILDCARD)
+}
+
+pub(crate) fn is_range_str(str: &str) -> bool {
+    let ret = str.split(RANGE_STR_SEPARATOR);
+    let count = ret.count();
+    count > 1
+}
+
+pub(crate) fn is_concrete_str(str: &str) -> bool {
+    !is_wildcard_str(str) && !is_range_str(str)
+}
+
+pub(crate) fn match_range(range: &str, value: &str) -> bool {
+    if range.is_empty() {
+        return true;
+    }
+
+    if !is_range_str(range) {
+        return false;
+    }
+
+    range
+        .split(RANGE_STR_SEPARATOR)
+        .any(|data| (*data).eq(value))
+}