Dubbo 微服务系列(03)服务注册

Spring Cloud Alibaba 系列目录 - Dubbo 篇

1. 背景介绍

图1 Dubbo经典架构图

注:本图来源 Dubbo官方架构图

表1 节点角色说明

节点 角色说明
Provider 暴露服务的服务提供方
Consumer 调用远程服务的服务消费方
Registry 服务注册与发现的注册中心
Monitor 统计服务的调用次数和调用时间的监控中心
Container 服务运行容器

在 Dubbo 微服务体系中,注册中心是其核心组件之一,Dubbo 通过注册中心实现服务的注册与发现。Dubbo 的注册中心有 zookeeper、nacos 等。

  • dubbo-registry-api 注册中心抽象 API
  • dubbo-registry-default Dubbo 基于内存的默认实现
  • dubbo-registry-multicast multicast 注册中心
  • dubbo-registry-zookeeper zookeeper 注册中心
  • dubbo-registry-nacos nacos 注册中心

2. 数据结构

不同的注册中心,数据结构稍有区别。下面以 Zookeeper 为例,说明 dubbo 注册中心的数据结构。

(1)路径结构

例如:/dubbo/com.dubbo.DemoService1/providers 是服务都在 ZK 上的注册路径,该路径结构分为四层:

  • 一是root(根节点,默认为 /dubbo);
  • 二是 serviceInterface(接口名称);
  • 三是服务类型(providers、consumers、routers、configurators);
  • 四是具体注册的元信息 URL。

注: 前三层相当于 serviceKey,最后一层则是对应的 serviceValue。

+ /dubbo
+-- serviceInterface
+-- providers
+-- consumers
+-- routers
+-- configurators

(2)四种服务类型(category)分别是 providers、consumers、routers、configurators:

  • /dubbo/serviceInterface/providers 服务提供者注册信息,包含多个服务者 URL 元数据信息。eg: dubbo://192.168.139.101:20880/com.dubbo.DemoService1?key=value&...
  • /dubbo/serviceInterface/consumers 服务消费才注册信息,包含多个消费者 URL 元数据信息。eg: dubbo://192.168.139.101:8888/com.dubbo.DemoService1?key=value&...
  • /dubbo/serviceInterface/router 路由配置信息,包含消费者路由策略 URL 元数据信息。eg: condition://0.0.0.0/com.dubbo.DemoService1?category=routers&key=value&...
  • /dubbo/serviceInterface/configurators 外部化配置信息,包含服务者动态配置 URL 元数据信息。eg: override://0.0.0.0/com.dubbo.DemoService1?category=configurators&key=value&...

图2 Zookeeper 注册中心数据结构

graph TB
ROOT((/dubbo)) --> DemoService1(com.deme.DemoService1)
ROOT --> DemoService2(com.deme.DemoService2)
DemoService1 -.-> providers(providers)
DemoService1 -.-> consumers(consumers)
providers -.-> dubbo://192.168.139.101:20080
DemoService2 -.-> routes(routes)
DemoService2 -.-> configurators(configurators)
configurators -.-> override://0.0.0.0/...

3. 源码分析

图3 Dubbo注册中心类图

  • AbstractRegistry 缓存机制
  • FailbackRegistry 重试机制
  • ZookeeperRegistry、NacosRegistry 具体的注册中心实现,每个注册中心都有一个对应的工厂类,如 ZookeeperRegistryFactory、NacosRegistryFactory。当消费者 URL 订阅的注册信息发生变化时,ZookeeperRegistry 会回调 notify(URL url, NotifyListener listener, List<URL> urls) 方法,更新内存和磁盘上本地缓存的注册信息,并通知监听者。
public interface RegistryService {

    // 注册服务
// dubbo://10.20.153.10/dubbo.BarService?version=1.0.0&application=kylin
void register(URL url);
void unregister(URL url); // 订阅指定服务
// consumer://10.20.153.10/dubbo.BarService?version=1.0.0&application=kylin
void subscribe(URL url, NotifyListener listener);
void unsubscribe(URL url, NotifyListener listener); // 查找指定服务
// consumer://10.20.153.10/dubbo.BarService?version=1.0.0&application=kylin
List<URL> lookup(URL url);
}

3.1 缓存机制

消费者从注册中心获取注册信息后会做本地缓存,本地缓存保存两份:一是内存中保存一份,通过 notified 的 Map 结构进行保存;二是磁盘上保存一份,通过 file 保持引用。

private final Properties properties = new Properties();
private File file; // 磁盘文件服务缓存对象
private final ConcurrentMap<URL, Map<String, List<URL>>> notified =
new ConcurrentHashMap<>(); // 内存中的服务缓存对象
  • notified 是内存中的服务缓存对象,外层 Key 是消费者 URL,内层的 kye 是分类(category),包含 providers、consumers、routers、configurators 四种,value 则是对应的服务列表。
  • file 磁盘缓存对象,当订阅的信息发生变更时先更新 properties 的内容,通过 properties 再写入磁盘。

3.1.1 缓存的加载

当初始化注册中心时,会通过 AbstractRegistry 的默认构造器加载磁盘缓存文件 file 中的订阅信息。当注册中心无法连接或宕机时使用缓存。

// 初始化加载磁盘缓存文件 file 中的订阅信息
private void loadProperties() {
if (file != null && file.exists()) {
...
InputStream in = new FileInputStream(file);
properties.load(in);
}
}

3.1.2 缓存的更新

当订阅的注册信息发生变量时,ZookeeperRegistry 会回调 notify 方法更新缓存中的数据,其中第一个参数为消费者 url,第三个参数为注册中心注册的 urls。

/**
* 当订阅的注册信息发生变更时,通知 consumer url 更新注册列表
*
* @param url consumer side url
* @param listener listener
* @param urls provider latest urls
*/
protected void notify(URL url, NotifyListener listener, List<URL> urls) {
...
// 按category进行分类,并根据消费者url过滤订阅的urls
Map<String, List<URL>> result = new HashMap<>();
for (URL u : urls) {
if (UrlUtils.isMatch(url, u)) {
String category = u.getParameter(CATEGORY_KEY, DEFAULT_CATEGORY);
List<URL> categoryList = result.computeIfAbsent(category, k -> new ArrayList<>());
categoryList.add(u);
}
}
if (result.size() == 0) {
return;
}
Map<String, List<URL>> categoryNotified =
notified.computeIfAbsent(url, u -> new ConcurrentHashMap<>());
for (Map.Entry<String, List<URL>> entry : result.entrySet()) {
String category = entry.getKey();
List<URL> categoryList = entry.getValue();
// 1. 更新内存中的本地缓存:notified
categoryNotified.put(category, categoryList);
// 2. 更新磁盘中的本地缓存:properties -> file
saveProperties(url);
// 3. 通知监听者
listener.notify(categoryList);
}
}

总结: 当注册信息发生变量时,主要做了三件事:一是更新内存中的注册信息 notified;二是更新磁盘中的数据 properties;三是通知监听者。

// 参数url为消费者url,将内存中 notified 对应的消费者 url 对应的注册信息缓存到磁盘上。
private void saveProperties(URL url) {
StringBuilder buf = new StringBuilder();
// 1. 将 notified 对应的 url 注册信息保存为字符串,用于持久化
Map<String, List<URL>> categoryNotified = notified.get(url);
if (categoryNotified != null) {
for (List<URL> us : categoryNotified.values()) {
for (URL u : us) {
if (buf.length() > 0) {
buf.append(URL_SEPARATOR);
}
buf.append(u.toFullString());
}
}
}
// 2. 同步到磁盘 file
properties.setProperty(url.getServiceKey(), buf.toString());
long version = lastCacheChanged.incrementAndGet();
if (syncSaveFile) {
doSaveProperties(version);
} else {
registryCacheExecutor.execute(new SaveProperties(version));
}
}

总结: doSaveProperties 调用 properties.store(outputFile, "Dubbo Registry Cache") 将内存中的注册信息保存到文件中。properties 的 key、value 分别如下:

  • key 消费者 URL#getServiceKey,即 {group/}serviceInterface{:version} ,其中 group 和 version 都是可选。
  • value 消费者 URL 订阅的注册信息 urls,多个 URL 用空格分隔。示例如下:
"binarylei.dubbo.api.EchoService" -> "empty://192.168.139.1/binarylei.dubbo.api.EchoService?application=dubbo-consumer&category=configurators&dubbo=2.6.0&interface=binarylei.dubbo.api.EchoService&methods=echo&pid=21540&side=consumer&timestamp=1570799586361 empty://192.168.139.1/binarylei.dubbo.api.EchoService?application=dubbo-consumer&category=routers&dubbo=2.6.0&interface=binarylei.dubbo.api.EchoService&methods=echo&pid=21540&side=consumer&timestamp=1570799586361 dubbo://192.168.139.1:20880/binarylei.dubbo.api.EchoService?anyhost=true&application=dubbo-provider&dubbo=2.6.0&generic=false&interface=binarylei.dubbo.api.EchoService&methods=echo&pid=2460&side=provider&timestamp=1570798917842 empty://192.168.139.1/binarylei.dubbo.api.EchoService?application=dubbo-consumer&category=providers,configurators,routers&dubbo=2.6.0&interface=binarylei.dubbo.api.EchoService&methods=echo&pid=21540&side=consumer&timestamp=1570799586361"

3.2 重试机制

FailbackRegistry 继承自 AbstractRegistry,并在此基础上增加了失败重试的能力。FailbackRegistry 内部定义 HashedWheelTimer retryTimer ,会将调用失败需要重试的任务添加到 retryTimer 中。

// 发起注册失败的 URL 集合
private final ConcurrentMap<URL, FailedRegisteredTask> failedRegistered =
new ConcurrentHashMap<URL, FailedRegisteredTask>(); // 取消注册失败的 URL 集合
private final ConcurrentMap<URL, FailedUnregisteredTask> failedUnregistered =
new ConcurrentHashMap<URL, FailedUnregisteredTask>(); // 发起订阅失败的监听器集合
private final ConcurrentMap<Holder, FailedSubscribedTask> failedSubscribed =
new ConcurrentHashMap<Holder, FailedSubscribedTask>(); // 取消订阅失败的监听器集合
private final ConcurrentMap<Holder, FailedUnsubscribedTask> failedUnsubscribed =
new ConcurrentHashMap<Holder, FailedUnsubscribedTask>(); // 通知失败的 URL 集合
private final ConcurrentMap<Holder, FailedNotifiedTask> failedNotified =
new ConcurrentHashMap<Holder, FailedNotifiedTask>();

总结: FailbackRegistry 对注册、订阅、通知失败的情况都进行了重试处理,对于需要重试的任务都保存在对应的集合中,并通过 retryTimer.newTimeout 定时器定时处理。下面以注册 register 为例分析重试机制。

图4 Dubbo失败重试机制

sequenceDiagram
participant ZookeeperRegistry
participant FailbackRegistry
participant FailedRegisteredTask
participant AbstractRetryTask
participant failedRegistered
participant HashedWheelTimer
note left of ZookeeperRegistry : register方法
ZookeeperRegistry ->> FailbackRegistry : removeFailedRegistered
FailbackRegistry ->> failedRegistered : remove:从failedRegistered集合中删除重试任务
ZookeeperRegistry ->> FailbackRegistry : removeFailedRegistered
FailbackRegistry -->> ZookeeperRegistry : doRegister
opt doRegister 注册失败
ZookeeperRegistry ->> FailbackRegistry : addFailedRegistered
FailbackRegistry ->> FailedRegisteredTask : new
FailbackRegistry ->> failedRegistered : putIfAbsent:添加到failedRegistered集合中
FailbackRegistry ->> HashedWheelTimer : newTimeout:如果是新任务,则添加重试任务
opt 重试
HashedWheelTimer -->> AbstractRetryTask : run
AbstractRetryTask -->> FailedRegisteredTask : doRetry
FailedRegisteredTask -->> FailbackRegistry : doRegister
FailedRegisteredTask -->> FailbackRegistry : removeFailedRegisteredTask
AbstractRetryTask -->> AbstractRetryTask : reput
end
end

总结: 当注册失败时,Dubbo 会将注册失败的 URL 添加到重试任务中。HashedWheelTimer 本质和 Timer 一样是一个定时器。如果重试成功就会删除 failedRegistered 队列中的任务,失败则调用 reput 继续重试。在 AbstractRetryTask 配置了两个默认参数 retryPeriod=5s 和 retryTimes=3,即 5s 重试一次,最多重试 3 次。

@Override
public void register(URL url) {
super.register(url);
removeFailedRegistered(url);
removeFailedUnregistered(url);
try {
doRegister(url);
} catch (Exception e) {
...
// 失败重试
addFailedRegistered(url);
}
} private void addFailedRegistered(URL url) {
FailedRegisteredTask oldOne = failedRegistered.get(url);
if (oldOne != null) {
return;
}
FailedRegisteredTask newTask = new FailedRegisteredTask(url, this);
oldOne = failedRegistered.putIfAbsent(url, newTask);
if (oldOne == null) {
retryTimer.newTimeout(newTask, retryPeriod, TimeUnit.MILLISECONDS);
}
}

3.3 ZookeeperRegistry

ZookeeperRegistry 等具体的实现类,主要功能是实现具体的注册、订阅、查找方法 doRegister、doUnregister、doSubscribe、doUnsubscribe、lookup

3.3.1 初始化

ZookeeperRegistry 初始化主要完成两件事:一是 zkClient 客户端初始化;二是注册监听器,一旦注册中心无法连接则将当前注册和订阅的 URL 添加到重试任务中。

// url 是注册中心地址,ZookeeperTransporter 是 ZK 客户端,默认是 curator
public ZookeeperRegistry(URL url, ZookeeperTransporter zookeeperTransporter) {
super(url);
if (url.isAnyHost()) {
throw new IllegalStateException("registry address == null");
}
// 获取组名,默认为 dubbo
String group = url.getParameter(GROUP_KEY, DEFAULT_ROOT);
if (!group.startsWith(PATH_SEPARATOR)) {
group = PATH_SEPARATOR + group;
}
// ZK 注册的根据路径是 '/dubbo'
this.root = group;
// 创建 Zookeeper 客户端,默认为 CuratorZookeeperTransporter
zkClient = zookeeperTransporter.connect(url);
// 添加状态监听器,当 ZK 无法连接时从内存中保存的注册信息恢复
zkClient.addStateListener(state -> {
if (state == StateListener.RECONNECTED) {
try {
recover();
} catch (Exception e) {
logger.error(e.getMessage(), e);
}
}
});
}

总结: ZookeeperRegistry 初始化时的两件事,一是创建客户端,二是自动恢复。

  1. ZookeeperTransporter 客户端有 curator 和 ZkClient 两种实现。通过 URL 的 client 或 transporter 进行动态适配,默认的实现是 CuratorZookeeperTransporter。

  2. 当注册中心无法连接时,将当前注册和订阅的 URL 添加到重试任务中,一旦网络正常则自动恢复。recover 是在 FailbackRegistry 中实现的。

@Override
protected void recover() throws Exception {
// register:将当前注册的 URL 添加到定时器中进行重试
Set<URL> recoverRegistered = new HashSet<URL>(getRegistered());
if (!recoverRegistered.isEmpty()) {
for (URL url : recoverRegistered) {
addFailedRegistered(url);
}
}
// subscribe:将当前订阅的 URL 添加到定时器中进行重试
Map<URL, Set<NotifyListener>> recoverSubscribed = new HashMap<URL, Set<NotifyListener>>(getSubscribed());
if (!recoverSubscribed.isEmpty()) {
for (Map.Entry<URL, Set<NotifyListener>> entry : recoverSubscribed.entrySet()) {
URL url = entry.getKey();
for (NotifyListener listener : entry.getValue()) {
addFailedSubscribed(url, listener);
}
}
}
}

3.3.2 注册

注册直接调用 zkClient 的 create 方法创建节点,delete 方法删除节点,默认为临时节点。consumer 注册主要是为了方便 Admin 使用。

@Override
public void doRegister(URL url) {
try {
zkClient.create(toUrlPath(url), url.getParameter(DYNAMIC_KEY, true));
} catch (Throwable e) {
throw new RpcException("Failed to register " + url + " to zookeeper " +
getUrl() + ", cause: " + e.getMessage(), e);
}
}

3.3.3 订阅

订阅相对注册复杂很多,分两种情况,一是 url.getServiceInterface() 是 * ,也就是全量获取注册信息,一般是 Admin 使用;二是订阅指定的 serviceInterface。这里主要分析第二种情况。

@Override
public void doSubscribe(final URL url, final NotifyListener listener) {
try {
// 1. 获取所有的注册信息,一般 Admin 会获取所有的服务 ANY_VALUE=*
if (ANY_VALUE.equals(url.getServiceInterface())) {
...
// 2. 获取指定的服务 serviceInterface
} else {
List<URL> urls = new ArrayList<>();
// 2.1 '/dubbo/serviceInterface/{providers、routers、configurators}'
for (String path : toCategoriesPath(url)) {
ConcurrentMap<NotifyListener, ChildListener> listeners = zkListeners.get(url);
if (listeners == null) {
zkListeners.putIfAbsent(url, new ConcurrentHashMap<>());
listeners = zkListeners.get(url);
}
ChildListener zkListener = listeners.get(listener); // 2.2 创建zkListener,当注册信息发生变化时,调用notify(url,listener,urls)
if (zkListener == null) {
listeners.putIfAbsent(listener, (parentPath, currentChilds) ->
ZookeeperRegistry.this.notify(url, listener, toUrlsWithEmpty(url, parentPath, currentChilds)));
zkListener = listeners.get(listener);
}
// 2.3 创建{providers、routers、configurators}目录,永久性节点
zkClient.create(path, false);
// 2.4 注册zkListener,并获取{providers}的子节点信息
List<String> children = zkClient.addChildListener(path, zkListener);
if (children != null) {
urls.addAll(toUrlsWithEmpty(url, path, children));
}
}
// 2.5 通知 listener
notify(url, listener, urls);
}
} catch (Throwable e) {
throw new RpcException("Failed to subscribe " + url + " to zookeeper " +
getUrl() + ", cause: " + e.getMessage(), e);
}
}

总结: 消费者 URL 会指定需要订阅的 category{providers、routers、configurators} 类型,依次遍历这几个目录。如果指定的目录不存在,首先会创建一个永久性的目录(2.3),并注册对应的 zkListener(2.4),zkListener 在节点发生变化时调用 notify 通知 listener(2.2)。在注册 zkListener 时会返回对应的子节点注册信息,并通知 listener(2.5)。

也就是说,订阅时首先获取该服务在 ZK 上全量注册信息,之后消费者感知注册信息变化,则是通过 zkListener 事件通知的方式。

3.4 NacosRegistry

NacosRegistry 注册中心和 ZookeeperRegistry 类似,也是通过事件通知的方式感知服务注册信息变化。

3.4.1 注册

NacosRegistry 注册时会将 URL 转化为 Nacos 的实例对象 Instance,调用 registerInstance 进行注册,deregisterInstance 取消注册。

public void doRegister(URL url) {
final String serviceName = getServiceName(url);
final Instance instance = createInstance(url);
execute(namingService -> namingService.registerInstance(serviceName, instance));
}

总结: NacosRegistry 注册非常简单,主要分析一下在 Nacos 上注册的数据结构。

  • serviceName:{category}:{serviceInterface}:{version}:{group}。其中 version 和 group 可以缺省,eg: providers:org.apache.dubbo.demo.DemoService::

  • instance:这是 Nacos 的服务实例模型。

Nacos 注册示例如下:

"providers:org.apache.dubbo.demo.DemoService::" -> {"enabled":true,"ephemeral":true,"healthy":true,"instanceHeartBeatInterval":5000,"instanceHeartBeatTimeOut":15000,"ip":"192.168.139.1","ipDeleteTimeout":30000,"metadata":{"side":"provider","methods":"sayHello","release":"","deprecated":"false","dubbo":"2.0.2","pid":"1128","interface":"org.apache.dubbo.demo.DemoService","generic":"false","path":"org.apache.dubbo.demo.DemoService","protocol":"dubbo","application":"dubbo-provider","dynamic":"true","category":"providers","anyhost":"true","bean.name":"org.apache.dubbo.demo.DemoService","register":"true","timestamp":"1570933206811"},"port":20880,"weight":1.0}

3.4.2 订阅

订阅时,首先获取需要订阅的服务名称,和 ZK 一样,也分为 Admin 和普通 serviceInterface 订阅二种情况。

public void doSubscribe(final URL url, final NotifyListener listener) {
Set<String> serviceNames = getServiceNames(url, listener);
doSubscribe(url, listener, serviceNames);
} // 订阅指定的 serviceNames
private void doSubscribe(final URL url, final NotifyListener listener,
final Set<String> serviceNames) {
execute(namingService -> {
for (String serviceName : serviceNames) {
List<Instance> instances = namingService.getAllInstances(serviceName);
// 通知 listener,将 Nacos Instance 适配成 Dubbo URL 后通知 listener
notifySubscriber(url, listener, instances);
// 注册监听器,感知服务注册信息变化
subscribeEventListener(serviceName, url, listener);
}
});
}

总结: 和 ZookeeperRegistry 类似,首先获取对应服务名称的服务实例,通过 notifySubscriber 通知 listener。之后服务感知也是通过 subscribeEventListener 事件机制。

private void subscribeEventListener(String serviceName, final URL url,
final NotifyListener listener) throws NacosException {
if (!nacosListeners.containsKey(serviceName)) {
EventListener eventListener = event -> {
if (event instanceof NamingEvent) {
NamingEvent e = (NamingEvent) event;
// 服务注册信息变化时通知 listener
notifySubscriber(url, listener, e.getInstances());
}
};
// 注册EventListener
namingService.subscribe(serviceName, eventListener);
nacosListeners.put(serviceName, eventListener);
}
}

4. 总结

Dubbo 对注册中心进行了统一的抽象,核心接口是 RegistryService,其子类 AbstractRegistry 实现了缓存机制,FailbackRegistry 实现了重试机制。

ZookeeperRegistry、NacosRegistry 则具体等实现,则是完成具体的服务注册和订阅。注册比较简单,订阅主要是通过事件机制,当注册的服务发生变化时调用 notify(URL url, NotifyListener listener, List<URL> urls) 方法,更新内存和磁盘上本地缓存的注册信息,并通知监听者。

其中 RegistryDirectory 就是其中一个监听者,会感知服务信息的变化,管理某个服务对应的所有注册信息。

4.1 服务自省

我们知道 Dubbo 的注册是以服务接口 serviceInterface 为单位进行注册的,而大多数注册中心的设计都是以服务实例为单位进行注册的,如 Nacos、eureka、Spring Cloud 等。以服务实例进行注册更接近云原先,而且以服务接口为单位进行注册,会造成注册中心数据冗余,网络通信压力增大,减少注册中心的吞吐量。

Dubbo 计划在 2.7.5 实现服务自省的功能,而 Spring Cloud alibaba-2.1.0 则已经完成了服务的自省。

图5 Dubbo服务自省架构图

注:本图来源 小马哥技术周报


每天用心记录一点点。内容也许不重要,但习惯很重要!

Dubbo 微服务系列(03)服务注册的更多相关文章

  1. 玩转Windows服务系列——Windows服务小技巧

    伴随着研究Windows服务,逐渐掌握了一些小技巧,现在与大家分享一下. 将Windows服务转变为控制台程序 由于默认的Windows服务程序,编译后为Win32的窗口程序.我们在程序启动或运行过程 ...

  2. 玩转Windows服务系列——Windows服务启动超时时间

    最近有客户反映,机房出现断电情况,服务器的系统重新启动后,数据库服务自启动失败.第一次遇到这种情况,为了查看是不是断电情况导致数据库文件损坏,从客户的服务器拿到数据库的日志,进行分析. 数据库工作机制 ...

  3. 玩转Windows服务系列——Windows服务小技巧

    原文:玩转Windows服务系列——Windows服务小技巧 伴随着研究Windows服务,逐渐掌握了一些小技巧,现在与大家分享一下. 将Windows服务转变为控制台程序 由于默认的Windows服 ...

  4. 微服务系列之 Consul 注册中心

    原文链接:https://mrhelloworld.com/posts/spring/spring-cloud/consul-service-registry/ Netflix Eureka 2.X ...

  5. go微服务系列(三) - 服务调用(http)

    1. 关于服务调用 2. 基本方式调用服务 3. 服务调用正确姿势(初步) 3.1 服务端代码 3.2 客户端调用(重要) 1. 关于服务调用 这里的服务调用,我们调用的可以是http api也可以是 ...

  6. go微服务系列(二) - 服务注册/服务发现

    目录 1. 服务注册 1.1 代码演示 1.2 在go run的时候传入服务注册的参数 2. 服务发现均衡负载 2.1 均衡负载算法 2.2 服务发现均衡负载的演示 1. 服务注册 1.1 代码演示 ...

  7. 玩转Windows服务系列——Debug、Release版本的注册和卸载,及其原理

    Windows服务Debug版本 注册 Services.exe -regserver 卸载 Services.exe -unregserver Windows服务Release版本 注册 Servi ...

  8. 玩转Windows服务系列——Debug、Release版本的注册和卸载,及其原理

    原文:玩转Windows服务系列——Debug.Release版本的注册和卸载,及其原理 Windows服务Debug版本 注册 Services.exe -regserver 卸载 Services ...

  9. 玩转Windows服务系列——给Windows服务添加COM接口

    当我们运行一个Windows服务的时候,一般情况下,我们会选择以非窗口或者非控制台的方式运行,这样,它就只是一个后台程序,没有界面供我们进行交互. 那么当我们想与Windows服务进行实时交互的时候, ...

  10. 玩转Windows服务系列——使用Boost.Application快速构建Windows服务

    玩转Windows服务系列——创建Windows服务一文中,介绍了如何快速使用VS构建一个Windows服务.Debug.Release版本的注册和卸载,及其原理和服务运行.停止流程浅析分别介绍了Wi ...

随机推荐

  1. Linux下docker安装教程

    目前最新版本的docker19.03支持nvidia显卡与容器的无缝对接,从而摆脱了对nvidia-docker的依赖.因此毫不犹豫安装19.03版本的docker,安装教程可参考官方教程Centos ...

  2. javaScript Queue

    function Queue() { var items = []; this.enqueue = function(element) { items.push(element) } this.deq ...

  3. .gz文件解压

    有时我们明明已经使用gunzip命令解压.gz文件了,可解压生成的文件却依然无法正常读取.如输入命令gunzip HelloWorld.java.gz后,解压生成HelloWorld.java文件,却 ...

  4. TP框架的模板路径问题以及常用的模板常量的定义

    在TP框架中,为了各个模块加载静态文件方便,往往是不需要按照默认的方式放置静态文件到/app/模块名/VIEWS/下面,而是在顶级目录下创建一个新的目录(比如说./tpl目录下),来存放静态文件   ...

  5. 如何使用 VLD 检测程序中的内存泄漏?

    下载地址:https://kinddragon.github.io/vld/ 下载 windows 安装包,进行安装即可,它会给你设置好 vs 的环境变量,使用时,直接在 vs ide 中包含即可. ...

  6. 【转】modulenotfounderror: no module named 'matplotlib._path'问题的解决

    今天在装matplotlib包的时候遇到这样的问题,在网上找了很长时间没有类似很好的解决方法,最后自己 研究找到了解决的方法. 之前在pycharm里面已经装了matplotlib包,之后觉着下载包挺 ...

  7. 配置进程外Session 同时解决一个奇怪的BUG 因为SQLserver 服务器名不是默认的.或者localhost而引发的一系列问题

    用公司的电脑学习如鹏网的视频,开发一个项目,用到了进程外session,因为公司电脑SQLServer 是2008 服务器名称是.  然后参考这篇文章进行设置进程外session 很顺利 完成了设置. ...

  8. LeetCode Array Easy 283. Move Zeroes

    Description Given an array nums, write a function to move all 0's to the end of it while maintaining ...

  9. 【记录】@Configuration注解作用 mybatis @Param作用

    参考地址: 1:https://www.cnblogs.com/duanxz/p/7493276.html 2:https://www.wandouip.com/t5i91156/ 3:https:/ ...

  10. UML的9种图例解析(转)

    原帖已经不知道是哪一个,特在此感谢原作者.如有侵权,请私信联系.看到后即可删除. UML图中类之间的关系:依赖,泛化,关联,聚合,组合,实现 类与类图 1) 类(Class)封装了数据和行为,是面向对 ...