-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy path2215add6.html
More file actions
512 lines (402 loc) · 71.2 KB
/
2215add6.html
File metadata and controls
512 lines (402 loc) · 71.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width">
<meta name="theme-color" content="#222"><meta name="generator" content="Hexo 6.3.0">
<link rel="apple-touch-icon" sizes="180x180" href="/images/apple-touch-icon-next.png">
<link rel="icon" type="image/png" sizes="32x32" href="/images/favicon.png">
<link rel="icon" type="image/png" sizes="16x16" href="/images/favicon.png">
<link rel="mask-icon" href="/images/logo.svg" color="#222">
<link rel="stylesheet" href="/css/main.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" integrity="sha256-HtsXJanqjKTc8vVQjO4YMhiqFoXkfBsjBWcX91T1jr8=" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/3.1.1/animate.min.css" integrity="sha256-PR7ttpcvz8qrF57fur/yAx1qXMFJeJFiA6pSzWi0OIE=" crossorigin="anonymous">
<script class="next-config" data-name="main" type="application/json">{"hostname":"tallate.github.io","root":"/","images":"/images","scheme":"Gemini","darkmode":false,"version":"8.18.0","exturl":false,"sidebar":{"position":"left","display":"post","padding":18,"offset":12},"copycode":{"enable":false,"style":null},"fold":{"enable":false,"height":500},"bookmark":{"enable":false,"color":"#222","save":"auto"},"mediumzoom":false,"lazyload":false,"pangu":false,"comments":{"style":"tabs","active":null,"storage":true,"lazyload":false,"nav":null},"stickytabs":false,"motion":{"enable":true,"async":false,"transition":{"menu_item":"fadeInDown","post_block":"fadeIn","post_header":"fadeInDown","post_body":"fadeInDown","coll_header":"fadeInLeft","sidebar":"fadeInUp"}},"prism":false,"i18n":{"placeholder":"搜索...","empty":"没有找到任何搜索结果:${query}","hits_time":"找到 ${hits} 个搜索结果(用时 ${time} 毫秒)","hits":"找到 ${hits} 个搜索结果"}}</script><script src="/js/config.js"></script>
<meta name="description" content="基于注册中心目录服务,使服务消费方能动态的查找服务提供方,使地址透明,不再需要写死服务提供方地址,注册中心基于接口名查询服务提供者的 IP 地址,使服务提供方可以平滑增加或减少机器。 角色分类以功能角度来说服务可以分成以下几种: 服务提供者; 服务消费者; 服务提供者兼消费者。 注册中心分类可以分成以下几种注册中心: Simple 注册中心 点对点直连 Multicast 注册中心 多播">
<meta property="og:type" content="article">
<meta property="og:title" content="Dubbo服务调用过程">
<meta property="og:url" content="https://tallate.github.io/2215add6.html">
<meta property="og:site_name" content="Tallate">
<meta property="og:description" content="基于注册中心目录服务,使服务消费方能动态的查找服务提供方,使地址透明,不再需要写死服务提供方地址,注册中心基于接口名查询服务提供者的 IP 地址,使服务提供方可以平滑增加或减少机器。 角色分类以功能角度来说服务可以分成以下几种: 服务提供者; 服务消费者; 服务提供者兼消费者。 注册中心分类可以分成以下几种注册中心: Simple 注册中心 点对点直连 Multicast 注册中心 多播">
<meta property="og:locale" content="zh_CN">
<meta property="og:image" content="https://tallate.github.io/imgs/Dubbo/Dubbo%E6%9C%8D%E5%8A%A1%E6%B3%A8%E5%86%8C%E4%BF%A1%E6%81%AF.png">
<meta property="og:image" content="https://tallate.github.io/imgs/Dubbo/%E6%B3%A8%E5%86%8C%E5%92%8C%E6%B3%A8%E9%94%80%E6%9C%8D%E5%8A%A1.png">
<meta property="og:image" content="https://tallate.github.io/imgs/Dubbo/Dubbo%E6%9C%8D%E5%8A%A1%E5%AF%BC%E5%87%BA.png">
<meta property="og:image" content="https://tallate.github.io/imgs/Dubbo/%E6%9C%8D%E5%8A%A1%E8%AE%A2%E9%98%85%E5%92%8C%E5%8F%96%E6%B6%88.png">
<meta property="og:image" content="https://tallate.github.io/imgs/Dubbo/Dubbo%E7%BB%84%E4%BB%B6.png">
<meta property="article:published_time" content="2019-09-07T10:51:58.000Z">
<meta property="article:modified_time" content="2025-07-06T17:56:20.854Z">
<meta property="article:author" content="tallate">
<meta property="article:tag" content="Dubbo">
<meta name="twitter:card" content="summary">
<meta name="twitter:image" content="https://tallate.github.io/imgs/Dubbo/Dubbo%E6%9C%8D%E5%8A%A1%E6%B3%A8%E5%86%8C%E4%BF%A1%E6%81%AF.png">
<link rel="canonical" href="https://tallate.github.io/2215add6.html">
<script class="next-config" data-name="page" type="application/json">{"sidebar":"","isHome":false,"isPost":true,"lang":"zh-CN","comments":true,"permalink":"https://tallate.github.io/2215add6.html","path":"/2215add6.html","title":"Dubbo服务调用过程"}</script>
<script class="next-config" data-name="calendar" type="application/json">""</script>
<title>Dubbo服务调用过程 | Tallate</title>
<noscript>
<link rel="stylesheet" href="/css/noscript.css">
</noscript>
</head>
<body itemscope itemtype="http://schema.org/WebPage" class="use-motion">
<div class="headband"></div>
<main class="main">
<div class="column">
<header class="header" itemscope itemtype="http://schema.org/WPHeader"><div class="site-brand-container">
<div class="site-nav-toggle">
<div class="toggle" aria-label="切换导航栏" role="button">
<span class="toggle-line"></span>
<span class="toggle-line"></span>
<span class="toggle-line"></span>
</div>
</div>
<div class="site-meta">
<a href="/" class="brand" rel="start">
<i class="logo-line"></i>
<p class="site-title">Tallate</p>
<i class="logo-line"></i>
</a>
<p class="site-subtitle" itemprop="description">该吃吃该喝喝 啥事别往心里搁</p>
</div>
<div class="site-nav-right">
<div class="toggle popup-trigger" aria-label="搜索" role="button">
<i class="fa fa-search fa-fw fa-lg"></i>
</div>
</div>
</div>
<nav class="site-nav">
<ul class="main-menu menu"><li class="menu-item menu-item-home"><a href="/" rel="section"><i class="home fa-fw"></i>首页</a></li><li class="menu-item menu-item-about"><a href="/about/" rel="section"><i class="user fa-fw"></i>关于</a></li><li class="menu-item menu-item-tags"><a href="/tags/" rel="section"><i class="tags fa-fw"></i>标签<span class="badge">84</span></a></li><li class="menu-item menu-item-categories"><a href="/categories/" rel="section"><i class="th fa-fw"></i>分类<span class="badge">25</span></a></li><li class="menu-item menu-item-archives"><a href="/archives/" rel="section"><i class="archive fa-fw"></i>归档<span class="badge">192</span></a></li>
<li class="menu-item menu-item-search">
<a role="button" class="popup-trigger"><i class="fa fa-search fa-fw"></i>搜索
</a>
</li>
</ul>
</nav>
<div class="search-pop-overlay">
<div class="popup search-popup"><div class="search-header">
<span class="search-icon">
<i class="fa fa-search"></i>
</span>
<div class="search-input-container">
<input autocomplete="off" autocapitalize="off" maxlength="80"
placeholder="搜索..." spellcheck="false"
type="search" class="search-input">
</div>
<span class="popup-btn-close" role="button">
<i class="fa fa-times-circle"></i>
</span>
</div>
<div class="search-result-container no-result">
<div class="search-result-icon">
<i class="fa fa-spinner fa-pulse fa-5x"></i>
</div>
</div>
</div>
</div>
</header>
<aside class="sidebar">
<div class="sidebar-inner sidebar-nav-active sidebar-toc-active">
<ul class="sidebar-nav">
<li class="sidebar-nav-toc">
文章目录
</li>
<li class="sidebar-nav-overview">
站点概览
</li>
</ul>
<div class="sidebar-panel-container">
<!--noindex-->
<div class="post-toc-wrap sidebar-panel">
<div class="post-toc animated"><ol class="nav"><li class="nav-item nav-level-2"><a class="nav-link" href="#%E8%A7%92%E8%89%B2%E5%88%86%E7%B1%BB"><span class="nav-number">1.</span> <span class="nav-text">角色分类</span></a></li><li class="nav-item nav-level-2"><a class="nav-link" href="#%E6%B3%A8%E5%86%8C%E4%B8%AD%E5%BF%83%E5%88%86%E7%B1%BB"><span class="nav-number">2.</span> <span class="nav-text">注册中心分类</span></a></li><li class="nav-item nav-level-2"><a class="nav-link" href="#%E9%85%8D%E7%BD%AE"><span class="nav-number">3.</span> <span class="nav-text">配置</span></a></li><li class="nav-item nav-level-2"><a class="nav-link" href="#%E6%9F%A5%E7%9C%8B%E6%9C%8D%E5%8A%A1%E6%B3%A8%E5%86%8C-%E6%9A%B4%E9%9C%B2%E7%BB%93%E6%9E%9C"><span class="nav-number">4.</span> <span class="nav-text">查看服务注册/暴露结果</span></a></li><li class="nav-item nav-level-2"><a class="nav-link" href="#%E6%9C%8D%E5%8A%A1%E8%87%AA%E5%8A%A8%E5%8F%91%E7%8E%B0%E6%B5%81%E7%A8%8B"><span class="nav-number">5.</span> <span class="nav-text">服务自动发现流程</span></a></li><li class="nav-item nav-level-2"><a class="nav-link" href="#%E6%B3%A8%E5%86%8C%E5%92%8C%E6%B3%A8%E9%94%80%E6%9C%8D%E5%8A%A1%EF%BC%88Provider-%E6%89%A7%E8%A1%8C%E6%B5%81%E7%A8%8B%EF%BC%89"><span class="nav-number">6.</span> <span class="nav-text">注册和注销服务(Provider 执行流程)</span></a></li><li class="nav-item nav-level-2"><a class="nav-link" href="#%E6%9C%8D%E5%8A%A1%E8%AE%A2%E9%98%85%E5%92%8C%E5%8F%96%E6%B6%88%EF%BC%88Consumer-%E6%89%A7%E8%A1%8C%E6%B5%81%E7%A8%8B%EF%BC%89"><span class="nav-number">7.</span> <span class="nav-text">服务订阅和取消(Consumer 执行流程)</span></a></li><li class="nav-item nav-level-2"><a class="nav-link" href="#Consumer%E7%AB%AF%E6%9C%8D%E5%8A%A1%E8%B0%83%E7%94%A8%E8%BF%87%E7%A8%8B"><span class="nav-number">8.</span> <span class="nav-text">Consumer端服务调用过程</span></a><ol class="nav-child"><li class="nav-item nav-level-3"><a class="nav-link" href="#%E8%B0%83%E7%94%A8%E4%BB%A3%E7%90%86%E7%B1%BB%E7%9A%84%E6%96%B9%E6%B3%95"><span class="nav-number">8.1.</span> <span class="nav-text">调用代理类的方法</span></a></li><li class="nav-item nav-level-3"><a class="nav-link" href="#Registry-Directory"><span class="nav-number">8.2.</span> <span class="nav-text">Registry & Directory</span></a><ol class="nav-child"><li class="nav-item nav-level-4"><a class="nav-link" href="#Registry-%E5%B0%86%E6%B3%A8%E5%86%8C%E4%BF%A1%E6%81%AF%E4%BF%9D%E5%AD%98%E5%88%B0%E6%9C%AC%E5%9C%B0%E7%9A%84Directory"><span class="nav-number">8.2.1.</span> <span class="nav-text">Registry 将注册信息保存到本地的Directory</span></a></li><li class="nav-item nav-level-4"><a class="nav-link" href="#Invoker%E4%BD%BF%E7%94%A8Directory"><span class="nav-number">8.2.2.</span> <span class="nav-text">Invoker使用Directory</span></a></li><li class="nav-item nav-level-4"><a class="nav-link" href="#Registry%E7%9A%84%E5%87%A0%E7%A7%8D%E5%AE%9E%E7%8E%B0"><span class="nav-number">8.2.3.</span> <span class="nav-text">Registry的几种实现</span></a></li><li class="nav-item nav-level-4"><a class="nav-link" href="#Directory%E7%9A%84%E5%87%A0%E7%A7%8D%E5%AE%9E%E7%8E%B0"><span class="nav-number">8.2.4.</span> <span class="nav-text">Directory的几种实现</span></a></li></ol></li><li class="nav-item nav-level-3"><a class="nav-link" href="#Cluster"><span class="nav-number">8.3.</span> <span class="nav-text">Cluster</span></a><ol class="nav-child"><li class="nav-item nav-level-4"><a class="nav-link" href="#Cluster%E7%9A%84%E5%AE%9E%E7%8E%B0"><span class="nav-number">8.3.1.</span> <span class="nav-text">Cluster的实现</span></a></li></ol></li><li class="nav-item nav-level-3"><a class="nav-link" href="#LoadBalance"><span class="nav-number">8.4.</span> <span class="nav-text">LoadBalance</span></a><ol class="nav-child"><li class="nav-item nav-level-4"><a class="nav-link" href="#LoadBalance%E7%9A%84%E5%A4%9A%E7%A7%8D%E5%AE%9E%E7%8E%B0"><span class="nav-number">8.4.1.</span> <span class="nav-text">LoadBalance的多种实现</span></a></li></ol></li><li class="nav-item nav-level-3"><a class="nav-link" href="#Filter-Invoker-%E5%B1%82"><span class="nav-number">8.5.</span> <span class="nav-text">Filter & Invoker 层</span></a></li><li class="nav-item nav-level-3"><a class="nav-link" href="#Exchange"><span class="nav-number">8.6.</span> <span class="nav-text">Exchange</span></a></li><li class="nav-item nav-level-3"><a class="nav-link" href="#%E6%95%B0%E6%8D%AE%E7%BC%96%E7%A0%81-%E5%8F%91%E9%80%81"><span class="nav-number">8.7.</span> <span class="nav-text">数据编码 & 发送</span></a></li></ol></li><li class="nav-item nav-level-2"><a class="nav-link" href="#Provider%E7%AB%AF%E6%8E%A5%E5%8F%97%E8%B0%83%E7%94%A8%E7%9A%84%E8%BF%87%E7%A8%8B"><span class="nav-number">9.</span> <span class="nav-text">Provider端接受调用的过程</span></a></li></ol></div>
</div>
<!--/noindex-->
<div class="site-overview-wrap sidebar-panel">
<div class="site-author animated" itemprop="author" itemscope itemtype="http://schema.org/Person">
<p class="site-author-name" itemprop="name">tallate</p>
<div class="site-description" itemprop="description"></div>
</div>
<div class="site-state-wrap animated">
<nav class="site-state">
<div class="site-state-item site-state-posts">
<a href="/archives/">
<span class="site-state-item-count">192</span>
<span class="site-state-item-name">日志</span>
</a>
</div>
<div class="site-state-item site-state-categories">
<a href="/categories/">
<span class="site-state-item-count">25</span>
<span class="site-state-item-name">分类</span></a>
</div>
<div class="site-state-item site-state-tags">
<a href="/tags/">
<span class="site-state-item-count">84</span>
<span class="site-state-item-name">标签</span></a>
</div>
</nav>
</div>
</div>
</div>
</div>
</aside>
</div>
<div class="main-inner post posts-expand">
<div class="post-block">
<article itemscope itemtype="http://schema.org/Article" class="post-content" lang="zh-CN">
<link itemprop="mainEntityOfPage" href="https://tallate.github.io/2215add6.html">
<span hidden itemprop="author" itemscope itemtype="http://schema.org/Person">
<meta itemprop="image" content="/images/avatar.gif">
<meta itemprop="name" content="tallate">
</span>
<span hidden itemprop="publisher" itemscope itemtype="http://schema.org/Organization">
<meta itemprop="name" content="Tallate">
<meta itemprop="description" content="">
</span>
<span hidden itemprop="post" itemscope itemtype="http://schema.org/CreativeWork">
<meta itemprop="name" content="Dubbo服务调用过程 | Tallate">
<meta itemprop="description" content="">
</span>
<header class="post-header">
<h1 class="post-title" itemprop="name headline">
Dubbo服务调用过程
</h1>
<div class="post-meta-container">
<div class="post-meta">
<span class="post-meta-item">
<span class="post-meta-item-icon">
<i class="far fa-calendar"></i>
</span>
<span class="post-meta-item-text">发表于</span>
<time title="创建时间:2019-09-07 18:51:58" itemprop="dateCreated datePublished" datetime="2019-09-07T18:51:58+08:00">2019-09-07</time>
</span>
<span class="post-meta-item">
<span class="post-meta-item-icon">
<i class="far fa-calendar-check"></i>
</span>
<span class="post-meta-item-text">更新于</span>
<time title="修改时间:2025-07-07 01:56:20" itemprop="dateModified" datetime="2025-07-07T01:56:20+08:00">2025-07-07</time>
</span>
<span class="post-meta-item">
<span class="post-meta-item-icon">
<i class="far fa-folder"></i>
</span>
<span class="post-meta-item-text">分类于</span>
<span itemprop="about" itemscope itemtype="http://schema.org/Thing">
<a href="/categories/Dubbo/" itemprop="url" rel="index"><span itemprop="name">Dubbo</span></a>
</span>
</span>
</div>
</div>
</header>
<div class="post-body" itemprop="articleBody"><span id="more"></span>
<p>基于注册中心目录服务,使服务消费方能动态的查找服务提供方,使地址透明,不再需要写死服务提供方地址,注册中心基于接口名查询服务提供者的 IP 地址,使服务提供方可以平滑增加或减少机器。</p>
<h2 id="角色分类"><a href="#角色分类" class="headerlink" title="角色分类"></a>角色分类</h2><p>以功能角度来说服务可以分成以下几种:</p>
<ul>
<li>服务提供者;</li>
<li>服务消费者;</li>
<li>服务提供者兼消费者。</li>
</ul>
<h2 id="注册中心分类"><a href="#注册中心分类" class="headerlink" title="注册中心分类"></a>注册中心分类</h2><p>可以分成以下几种注册中心:</p>
<ul>
<li>Simple 注册中心 点对点直连</li>
<li>Multicast 注册中心 多播</li>
<li>Zookeeper 注册中心</li>
<li>Redis 注册中心</li>
</ul>
<h2 id="配置"><a href="#配置" class="headerlink" title="配置"></a>配置</h2><p>服务提供者(provider)配置:</p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><!-- 应用名称,可显示依赖关系 --></span><br><span class="line"><dubbo:application name="dubbo-order-server" /></span><br><span class="line"></span><br><span class="line"><!-- 注册中心是ZooKeeper,也可以选择Redis做注册中心 --></span><br><span class="line"><dubbo:registry address="zookeeper://127.0.0.1:2181"</span><br><span class="line"> client="zkclient" /></span><br><span class="line"></span><br><span class="line"><!-- 通过dubbo协议在注册中心(127.0.0.1表示本机)的20880端口暴露服务 --></span><br><span class="line"><dubbo:protocol name="dubbo" host="127.0.0.1" port="20880" /></span><br><span class="line"></span><br><span class="line"><!-- 提供服务用地的是service标签,将该接口暴露到dubbo中 --></span><br><span class="line"><dubbo:service interface="com.dubbo.service.OrderService"</span><br><span class="line"> ref="orderService" /></span><br><span class="line"></span><br><span class="line"><!-- Spring容器加载具体的实现类--></span><br><span class="line"><bean id="orderService" class="dubbo.service.impl.OrderServiceImpl" /></span><br><span class="line"></span><br><span class="line"><dubbo:monitor protocol="registry" /></span><br></pre></td></tr></table></figure>
<p>服务消费者(consumer)配置:</p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><!-- 应用名称,可显示依赖关系 --></span><br><span class="line"><dubbo:application name="dubbo-user-consumer" /></span><br><span class="line"></span><br><span class="line"><!-- zookeeper作为注册中心 ,也可以选择Redis做注册中心 --></span><br><span class="line"><dubbo:registry address="zookeeper://127.0.0.1:2181"</span><br><span class="line"> client="zkclient" /></span><br><span class="line"></span><br><span class="line"><dubbo:protocol host="127.0.0.1" /></span><br><span class="line"></span><br><span class="line"><!-- 调用服务使用reference标签,从注册中心中查找服务 --></span><br><span class="line"><dubbo:reference id="orderService" interface="com.dubbo.service.OrderService" /></span><br></pre></td></tr></table></figure>
<h2 id="查看服务注册-暴露结果"><a href="#查看服务注册-暴露结果" class="headerlink" title="查看服务注册/暴露结果"></a>查看服务注册/暴露结果</h2><p><img src="/imgs/Dubbo/Dubbo%E6%9C%8D%E5%8A%A1%E6%B3%A8%E5%86%8C%E4%BF%A1%E6%81%AF.png" alt="Dubbo服务注册信息" title="Dubbo服务注册信息"><br>Dubbo 在 ZooKeeper 中以树形结构维护服务注册信息:</p>
<ul>
<li>服务提供者启动时: 向 /dubbo/com.foo.BarService/providers 目录下写入自己的 URL 地址;</li>
<li>服务消费者启动时: 订阅 /dubbo/com.foo.BarService/providers 目录下的提供者 URL 地址。并向 /dubbo/com.foo.BarService/consumers 目录下写入自己的 URL 地址;</li>
<li>监控中心启动时: 订阅 /dubbo/com.foo.BarService 目录下的所有提供者和消费者 URL 地址。</li>
</ul>
<p>ZooKeeper 启动的时候会把配置信息加载进内存并持久化到数据库,然后启动定时器脏数据检查定时器 DirtyCheckTask,分别检查消费者和提供者的地址列表缓存、消费者和提供者地址列表的数据库数据,清理不存活的消费者和提供者数据,对于缓存中的存在的消费者和提供者而数据库不存在,提供者重新注册和消费者重新订阅。</p>
<p>Dubbo 提供了一些异常情况下的兜底方案:</p>
<ul>
<li>当提供者出现断电等异常停机时,注册中心能自动删除提供者信息</li>
<li>当注册中心重启时,能自动恢复注册数据,以及订阅请求</li>
<li>当会话过期时,能自动恢复注册数据,以及订阅请求</li>
<li>当设置 <dubbo:registry check=”false” /> 时,记录失败注册和订阅请求,后台定时重试</li>
</ul>
<p>在了解 ZooKeeper 基础上,还可以增加一些配置来修改注册细节:<br>可通过 <code><dubbo:registry username="admin" password="1234" /></code> 设置 ZooKeeper 登录信息<br>可通过 <code><dubbo:registry group="dubbo" /></code> 设置 ZooKeeper 的根节点,不设置将使用无根树<br>支持 * 号通配符 <code><dubbo:reference group="*" version="*" /></code> ,可订阅服务的所有分组和所有版本的提供者</p>
<p>在 Provider 启动完毕后,可以登录到 ZooKeeper 上查看注册的结果:</p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">[zk: localhost:2181(CONNECTED) 11] ls /</span><br><span class="line">[dubbo, zookeeper]</span><br><span class="line">[zk: localhost:2181(CONNECTED) 12] ls /dubbo</span><br><span class="line">[com.alibaba.dubbo.monitor.MonitorService, com.tallate.UserServiceBo]</span><br><span class="line">[zk: localhost:2181(CONNECTED) 13] ls /dubbo/com.tallate.UserServiceBo</span><br><span class="line">[configurators, consumers, providers, routers]</span><br><span class="line">[zk: localhost:2181(CONNECTED) 14] ls /dubbo/com.tallate.UserServiceBo/providers</span><br><span class="line">[dubbo%3A%2F%2F192.168.96.194%3A20880%2Fcom.tallate.UserServiceBo%3Fanyhost%3Dtrue%26application%3DdubboProvider%26dubbo%3D2.0.2%26generic%3Dfalse%26group%3Ddubbo%26interface%3Dcom.tallate.UserServiceBo%26methods%3DsayHello%2CtestPojo%2CsayHello2%26pid%3D28129%26revision%3D1.0.0%26side%3Dprovider%26timeout%3D3000%26timestamp%3D1575202776615%26version%3D1.0.0]</span><br></pre></td></tr></table></figure>
<h2 id="服务自动发现流程"><a href="#服务自动发现流程" class="headerlink" title="服务自动发现流程"></a>服务自动发现流程</h2><p>服务自动发现功能完成下面这个流程,我们接下来分点概述:</p>
<ol>
<li>服务提供者在启动时,向注册中心注册自己提供的服务。</li>
<li>服务消费者在启动时,向注册中心订阅自己所需的服务。</li>
<li>注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者。</li>
<li>服务消费者,从提供者地址列表中,基于软负载均衡算法(基于软件的负载均衡,与 F5 相对),选一台提供者进行调用,如果调用失败,再选另一台调用。</li>
</ol>
<h2 id="注册和注销服务(Provider-执行流程)"><a href="#注册和注销服务(Provider-执行流程)" class="headerlink" title="注册和注销服务(Provider 执行流程)"></a>注册和注销服务(Provider 执行流程)</h2><p>服务的注册与注销,是对服务提供方角色而言,大致流程如下所示:<br><img src="/imgs/Dubbo/%E6%B3%A8%E5%86%8C%E5%92%8C%E6%B3%A8%E9%94%80%E6%9C%8D%E5%8A%A1.png" alt="注册和注销服务" title="注册和注销服务"></p>
<ol>
<li>在接口提供者初始化时,每个接口都会创建一个 Invoker 和 Exporter,Exporter 持有 Invoker 实例,通过 Invocation 中的信息就可找到对应的 Exporter 和 Invoker</li>
<li>同 Consumer 的过程类似,调用 Invoker 前会调用 Invoker-Filter。</li>
<li>调用 Invoker.invoke() 时,通过反射调用最终的服务实现执行相关逻辑。</li>
</ol>
<p>ServiceBean 负责了服务的暴露:</p>
<ul>
<li>继承自 ServiceConfig,export 方法实现了服务暴露的逻辑;</li>
<li>实现了 Spring 中的 InitializingBean, DisposableBean, ApplicationContextAware, ApplicationListener<ContextRefreshedEvent>, BeanNameAware</li>
</ul>
<p>启动时,ServiceBean 主要负责以下任务:</p>
<ul>
<li>生成 DubboExporter 对象并缓存起来</li>
<li>添加过滤器和监听器支持</li>
<li>在 zk 上注册相关信息,暴露服务,方便被感知到</li>
<li>监听端口,等待通信的到来</li>
</ul>
<p><img src="/imgs/Dubbo/Dubbo%E6%9C%8D%E5%8A%A1%E5%AF%BC%E5%87%BA.png" alt="Dubbo服务导出" title="Dubbo服务导出"></p>
<ol>
<li>前置工作,主要用于检查参数和组装 URL;<br>ServiceBean#onApplicationEvent: 接收 Spring 上下文刷新事件后执行服务导出操作<br>-> ServiceBean#export: 导出服务<br>-> ProviderConfig.getExport、getDelay 获取配置,如果 export 为 false 则无法提供给其他服务调用、一般只提供给本地调试时使用,如果需要 delay 则将任务交给一个 ScheduledExecutorService 延迟执行,否则调用 doExport 暴露服务<br>-> ServiceConfig.doExport 一堆配置检查</li>
<li>导出服务,包含导出服务到本地(JVM)和导出服务到远程两个过程;<br><code>ServiceConfig.doExportUrls</code><br>导出服务,Dubbo 中所有服务都通过 URL 导出,支持多协议多注册中心导出服务(遍历 ProtocolConfig 集合导出每个服务)<br><code>AbstractInterfaceConfig#loadRegistries</code><br>加载注册中心链接<br><code>ServiceConfig#doExportUrlsFor1Protocol</code><br>组装 URL,将服务注册到注册中心<br><code>JavassistProxyFactory#getInvoker</code><br>获取 Invoker 实例,用于接收请求<br><code>ServiceConfig#exportLocal、DubboProtocol#export</code><br>根据配置信息导出服务到本地或远程,远程默认取Dubbo协议<br><code>DubboProtocol#openServer</code><br>开始监听请求</li>
<li>向注册中心注册服务,用于服务发现<br>Dubbo 服务注册本质是在 zk 指定目录下创建临时节点,路径是<code>{group}/{Interface}/providers/{url}</code>。<br>RegistryProtocol#register<br>-> RegistryFactory#getRegistry<br>-> AbstractRegistry#register</li>
</ol>
<p>因为Dubbo一般使用ZooKeeper作为注册中心,所以完全可以利用ZooKeeper的临时节点自动删除机制来实现服务器下线自动踢出的机制。</p>
<h2 id="服务订阅和取消(Consumer-执行流程)"><a href="#服务订阅和取消(Consumer-执行流程)" class="headerlink" title="服务订阅和取消(Consumer 执行流程)"></a>服务订阅和取消(Consumer 执行流程)</h2><p>为了满足应用系统的需求,服务消费方的可能需要从服务注册中心订阅指定的有服务提供方发布的服务,在得到通知可以使用服务时,就可以直接调用服务。反过来,如果不需要某一个服务了,可以取消该服务。<br><img src="/imgs/Dubbo/%E6%9C%8D%E5%8A%A1%E8%AE%A2%E9%98%85%E5%92%8C%E5%8F%96%E6%B6%88.png" alt="服务订阅和取消" title="服务订阅和取消"></p>
<p>有两种服务引入方式:</p>
<ol>
<li>饿汉式:Spring 容器调用 ReferenceBean 的 afterPropertiesSet 方法时引用服务,可通过配置 <code><dubbo:reference></code> 的 init 属性开启。</li>
<li>懒汉式:ReferenceBean 对应的服务被注入到其他类中时引用</li>
</ol>
<p>服务提供的方式有三种:</p>
<ol>
<li>引用本地 (JVM) 服务;</li>
<li>通过直连方式引用远程服务;</li>
<li>通过注册中心引用远程服务。</li>
</ol>
<p>不管是哪种引用方式,最后都会得到一个 Invoker 实例。如果有多个注册中心,多个服务提供者,这个时候会得到一组 Invoker 实例,此时需要通过集群管理类 Cluster 将多个 Invoker 合并成一个实例。</p>
<p>获取客户端Proxy:</p>
<ol>
<li>在 Consumer 初始化的时候,会生成一个代理注册到容器中,该代理回调中持有一个 Invoker 实例,消费调用服务接口时它的 invoke() 方法会被调用。<br>spring.ReferenceBean#getObject<br>ReferenceConfig#createProxy<br>创建代理实例,根据 url 的协议、scope 以及 injvm 等参数检测是否需要本地引用,不是本地引用的情况下默认采用Dubbo协议。<br>Protocol#refer<br> -> DubboProtocol#getClients 获取客户端实例,实例类型为 ExchangeClient,ExchangeClient 不具备通信能力,它需要依赖更底层的客户端实例<br> -> DubboProtocol#getSharedClient 默认获取共享客户端<br> -> DubboProtocol#initClient 创建客户端实例,默认为 Netty<br> -> Exchangers#connect(URL url, ExchangeHandler handler)</li>
<li>使用 Cluster 合并 Invoker<br>org.apache.dubbo.rpc.cluster.Cluster#join<br>如果配置了多个 URL,则使用 Cluster 合并多个 Invoker</li>
<li>创建动态代理<br>-> ProxyFactory#getProxy(Invoker<T> invoker)<br>常用的动态代理技术有 javassist、cglib、jdk,其中 dubbo 使用的是 javassist。<blockquote>
<p>根据早期 Dubbo 作者梁飞(<a target="_blank" rel="noopener" href="http://javatar.iteye.com/blog/814426%EF%BC%89%E7%9A%84%E8%AF%B4%E6%B3%95%EF%BC%8C%E4%BD%BF%E7%94%A8">http://javatar.iteye.com/blog/814426)的说法,使用</a> javassist 是为了性能。</p>
</blockquote>
</li>
</ol>
<h2 id="Consumer端服务调用过程"><a href="#Consumer端服务调用过程" class="headerlink" title="Consumer端服务调用过程"></a>Consumer端服务调用过程</h2><p><img src="/imgs/Dubbo/Dubbo%E7%BB%84%E4%BB%B6.png" alt="Dubbo组件" title="Dubbo组件"></p>
<h3 id="调用代理类的方法"><a href="#调用代理类的方法" class="headerlink" title="调用代理类的方法"></a>调用代理类的方法</h3><p>请求实际调用的是<code>InvokerInvocationHandler.invoke</code>。</p>
<h3 id="Registry-Directory"><a href="#Registry-Directory" class="headerlink" title="Registry & Directory"></a>Registry & Directory</h3><h4 id="Registry-将注册信息保存到本地的Directory"><a href="#Registry-将注册信息保存到本地的Directory" class="headerlink" title="Registry 将注册信息保存到本地的Directory"></a>Registry 将注册信息保存到本地的Directory</h4><p>启动服务时需要给一个Dubbo接口创建代理,这时需要将注册URL转换为Invoker对象:<br><code>org.apache.dubbo.registry.integration.RegistryProtocol#refer</code></p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line">public <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException {</span><br><span class="line"> url = getRegistryUrl(url);</span><br><span class="line"> Registry registry = registryFactory.getRegistry(url);</span><br><span class="line"> if (RegistryService.class.equals(type)) {</span><br><span class="line"> return proxyFactory.getInvoker((T) registry, type, url);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> // group="a,b" or group="*"</span><br><span class="line"> Map<String, String> qs = StringUtils.parseQueryString(url.getParameterAndDecoded(REFER_KEY));</span><br><span class="line"> String group = qs.get(GROUP_KEY);</span><br><span class="line"> if (group != null && group.length() > 0) {</span><br><span class="line"> if ((COMMA_SPLIT_PATTERN.split(group)).length > 1 || "*".equals(group)) {</span><br><span class="line"> return doRefer(getMergeableCluster(), registry, type, url);</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> return doRefer(cluster, registry, type, url);</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>引用一个服务时,会注册一个<code>zkListener</code>,监听注册服务的命名空间的变更情况。<br><code>org.apache.dubbo.registry.zookeeper.ZookeeperRegistry#doSubscribe</code><br>那么服务是怎么注册的呢?其实就是上边Provider注册服务的过程。<br>监听到注册中心的变更后,更新本地的Invoker列表,同时删除不可用的。<br><code>org.apache.dubbo.registry.integration.RegistryDirectory#refreshInvoker</code></p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><span class="line">private void refreshInvoker(List<URL> invokerUrls) {</span><br><span class="line"> Assert.notNull(invokerUrls, "invokerUrls should not be null");</span><br><span class="line"></span><br><span class="line"> if (invokerUrls.size() == 1</span><br><span class="line"> && invokerUrls.get(0) != null</span><br><span class="line"> && EMPTY_PROTOCOL.equals(invokerUrls.get(0).getProtocol())) {</span><br><span class="line"> this.forbidden = true; // Forbid to access</span><br><span class="line"> this.invokers = Collections.emptyList();</span><br><span class="line"> routerChain.setInvokers(this.invokers);</span><br><span class="line"> destroyAllInvokers(); // Close all invokers</span><br><span class="line"> } else {</span><br><span class="line"> this.forbidden = false; // Allow to access</span><br><span class="line"> Map<String, Invoker<T>> oldUrlInvokerMap = this.urlInvokerMap; // local reference</span><br><span class="line"> </span><br><span class="line"> ...</span><br><span class="line"> </span><br><span class="line"> Map<String, Invoker<T>> newUrlInvokerMap = toInvokers(invokerUrls);// Translate url list to Invoker map</span><br><span class="line"></span><br><span class="line"> ...</span><br><span class="line"></span><br><span class="line"> try {</span><br><span class="line"> destroyUnusedInvokers(oldUrlInvokerMap, newUrlInvokerMap); // Close the unused Invoker</span><br><span class="line"> } catch (Exception e) {</span><br><span class="line"> logger.warn("destroyUnusedInvokers error. ", e);</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<h4 id="Invoker使用Directory"><a href="#Invoker使用Directory" class="headerlink" title="Invoker使用Directory"></a>Invoker使用Directory</h4><p>为了服务高可用同一个服务一般会有多个应用服务器提供,要先挑选一个提供者提供服务。在服务接口消费者初始化时,接口方法和提供者 Invoker 对应关系保存在 Directory。 中,通过调用的方法名称(或方法名称+第一个参数)获取该方法对应的提供者 Invoker 列表,如注册中心设置了路由规则,对这些 Invoker 根据路由规则进行过滤。<br>启动时订阅某个服务:<br><code>org.apache.dubbo.registry.integration.RegistryProtocol#doRefer</code></p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line">private <T> Invoker<T> doRefer(Cluster cluster, Registry registry, Class<T> type, URL url) {</span><br><span class="line"> RegistryDirectory<T> directory = new RegistryDirectory<T>(type, url);</span><br><span class="line"> directory.setRegistry(registry);</span><br><span class="line"> directory.setProtocol(protocol);</span><br><span class="line"> // all attributes of REFER_KEY</span><br><span class="line"> Map<String, String> parameters = new HashMap<String, String>(directory.getUrl().getParameters());</span><br><span class="line"> URL subscribeUrl = new URL(CONSUMER_PROTOCOL, parameters.remove(REGISTER_IP_KEY), 0, type.getName(), parameters);</span><br><span class="line"> if (!ANY_VALUE.equals(url.getServiceInterface()) && url.getParameter(REGISTER_KEY, true)) {</span><br><span class="line"> directory.setRegisteredConsumerUrl(getRegisteredConsumerUrl(subscribeUrl, url));</span><br><span class="line"> registry.register(directory.getRegisteredConsumerUrl());</span><br><span class="line"> }</span><br><span class="line"> directory.buildRouterChain(subscribeUrl);</span><br><span class="line"> // 订阅providers、configurators、routers这几个namespace</span><br><span class="line"> directory.subscribe(subscribeUrl.addParameter(CATEGORY_KEY,</span><br><span class="line"> PROVIDERS_CATEGORY + "," + CONFIGURATORS_CATEGORY + "," + ROUTERS_CATEGORY));</span><br><span class="line"></span><br><span class="line"> // 使用Cluster组合Invoker</span><br><span class="line"> Invoker invoker = cluster.join(directory);</span><br><span class="line"> return invoker;</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>添加监听器:<br><code>org.apache.dubbo.registry.integration.RegistryDirectory#subscribe</code></p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">public void subscribe(URL url) {</span><br><span class="line"> setConsumerUrl(url);</span><br><span class="line"> CONSUMER_CONFIGURATION_LISTENER.addNotifyListener(this);</span><br><span class="line"> serviceConfigurationListener = new ReferenceConfigurationListener(this, url);</span><br><span class="line"> registry.subscribe(url, this);</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>Consumer端监听服务变更事件,刷新Invoker列表:<br><code>org.apache.dubbo.registry.integration.RegistryDirectory#refreshInvoker</code></p>
<h4 id="Registry的几种实现"><a href="#Registry的几种实现" class="headerlink" title="Registry的几种实现"></a>Registry的几种实现</h4><ul>
<li>ZooKeeperRegistry</li>
<li>RedisRegistry<br>注册信息的存储,是在启动时调用的:<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br></pre></td><td class="code"><pre><span class="line">@Override</span><br><span class="line">public void doRegister(URL url) {</span><br><span class="line"> // key = dubbo/com.package.to.InterfaceName/providers</span><br><span class="line"> String key = toCategoryPath(url);</span><br><span class="line"> // url的全名</span><br><span class="line"> String value = url.toFullString();</span><br><span class="line"> String expire = String.valueOf(System.currentTimeMillis() + expirePeriod);</span><br><span class="line"> boolean success = false;</span><br><span class="line"> RpcException exception = null;</span><br><span class="line"> for (Map.Entry<String, JedisPool> entry : jedisPools.entrySet()) {</span><br><span class="line"> JedisPool jedisPool = entry.getValue();</span><br><span class="line"> try {</span><br><span class="line"> Jedis jedis = jedisPool.getResource();</span><br><span class="line"> try {</span><br><span class="line"> // 使用hash结构,可以providers一个key下面存多个url</span><br><span class="line"> jedis.hset(key, value, expire);</span><br><span class="line"> jedis.publish(key, Constants.REGISTER);</span><br><span class="line"> success = true;</span><br><span class="line"> if (! replicate) {</span><br><span class="line"> break; // 如果服务器端已同步数据,只需写入单台机器</span><br><span class="line"> }</span><br><span class="line"> } finally {</span><br><span class="line"> jedisPool.returnResource(jedis);</span><br><span class="line"> }</span><br><span class="line"> } catch (Throwable t) {</span><br><span class="line"> exception = new RpcException("Failed to register service to redis registry. registry: " + entry.getKey() + ", service: " + url + ", cause: " + t.getMessage(), t);</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> if (exception != null) {</span><br><span class="line"> if (success) {</span><br><span class="line"> logger.warn(exception.getMessage(), exception);</span><br><span class="line"> } else {</span><br><span class="line"> throw exception;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
注册信息的主动删除,进程关闭时:<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"></span><br></pre></td></tr></table></figure></li>
</ul>
<h4 id="Directory的几种实现"><a href="#Directory的几种实现" class="headerlink" title="Directory的几种实现"></a>Directory的几种实现</h4><ul>
<li>RegistryDirectory<br>保存注册中心的服务注册信息,包括routers、configurators、provider。</li>
<li>StaticDirectory<br>Invoker列表是固定的。</li>
</ul>
<h3 id="Cluster"><a href="#Cluster" class="headerlink" title="Cluster"></a>Cluster</h3><p>封装了服务降级和容错机制,比如,如果调用失败则执行其他(<code>FailoverClusterInvoker</code>)、仍然调用失败则降级执行 mock(<code>MockClusterInvoker</code>)。<br>调用的第一层是<code>MockClusterInvoker</code>:</p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br></pre></td><td class="code"><pre><span class="line">public Result invoke(Invocation invocation) throws RpcException {</span><br><span class="line"> Result result = null;</span><br><span class="line"> </span><br><span class="line"> String value = directory.getUrl().getMethodParameter(invocation.getMethodName(), MOCK_KEY, Boolean.FALSE.toString()).trim();</span><br><span class="line"> // 没有设置mock属性或设置为false,则直接调就完了</span><br><span class="line"> if (value.length() == 0 || "false".equalsIgnoreCase(value)) {</span><br><span class="line"> //no mock</span><br><span class="line"> result = this.invoker.invoke(invocation);</span><br><span class="line"> }</span><br><span class="line"> // 配成force了,直接调mock方法</span><br><span class="line"> else if (value.startsWith("force")) {</span><br><span class="line"> if (logger.isWarnEnabled()) {</span><br><span class="line"> logger.warn("force-mock: " + invocation.getMethodName() + " force-mock enabled , url : " + directory.getUrl());</span><br><span class="line"> }</span><br><span class="line"> //force:direct mock</span><br><span class="line"> result = doMockInvoke(invocation, null);</span><br><span class="line"> }</span><br><span class="line"> // fail-mock的方式</span><br><span class="line"> else {</span><br><span class="line"> try {</span><br><span class="line"> result = this.invoker.invoke(invocation);</span><br><span class="line"></span><br><span class="line"> //fix:#4585</span><br><span class="line"> if(result.getException() != null && result.getException() instanceof RpcException){</span><br><span class="line"> RpcException rpcException= (RpcException)result.getException();</span><br><span class="line"> if(rpcException.isBiz()){</span><br><span class="line"> throw rpcException;</span><br><span class="line"> }else {</span><br><span class="line"> result = doMockInvoke(invocation, rpcException);</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> } catch (RpcException e) {</span><br><span class="line"> if (e.isBiz()) {</span><br><span class="line"> throw e;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> if (logger.isWarnEnabled()) {</span><br><span class="line"> logger.warn("fail-mock: " + invocation.getMethodName() + " fail-mock enabled , url : " + directory.getUrl(), e);</span><br><span class="line"> }</span><br><span class="line"> result = doMockInvoke(invocation, e);</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> return result;</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>实际invoke调用的是父类<code>AbstractClusterInvoker</code>的invoke方法,这个方法的主要功能是提供负载均衡:</p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line">public Result invoke(final Invocation invocation) throws RpcException {</span><br><span class="line"> checkWhetherDestroyed();</span><br><span class="line"></span><br><span class="line"> // binding attachments into invocation.</span><br><span class="line"> Map<String, String> contextAttachments = RpcContext.getContext().getAttachments();</span><br><span class="line"> if (contextAttachments != null && contextAttachments.size() != 0) {</span><br><span class="line"> ((RpcInvocation) invocation).addAttachments(contextAttachments);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> // 找到所有可调用的服务器</span><br><span class="line"> List<Invoker<T>> invokers = list(invocation);</span><br><span class="line"> // 发送时要经过负载均衡</span><br><span class="line"> LoadBalance loadbalance = initLoadBalance(invokers, invocation);</span><br><span class="line"> RpcUtils.attachInvocationIdIfAsync(getUrl(), invocation);</span><br><span class="line"> return doInvoke(invocation, invokers, loadbalance);</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>上面的<code>doInvoke</code>是一个模板方法,由子类实现,默认子类是<code>FailoverClusterInvoker</code>,可以看到,它先通过负载均衡策略得到一个Invoker,再调用该Invoker,Invoker的默认实现是<code>DubboInvoker</code>,表示使用的是Dubbo协议。</p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br></pre></td><td class="code"><pre><span class="line">private Result doInvoke(List<Invoker<T>> invokers,</span><br><span class="line"> final List<Invoker<T>> invoked,</span><br><span class="line"> Holder<RpcException> lastException,</span><br><span class="line"> final Set<String> providers,</span><br><span class="line"> final Invocation invocation,</span><br><span class="line"> final LoadBalance loadbalance,</span><br><span class="line"> final int totalRetries,</span><br><span class="line"> int retries,</span><br><span class="line"> Holder<Invoker<T>> lastInvoked) throws RpcException {</span><br><span class="line"> if (retries < totalRetries) {</span><br><span class="line"> checkWheatherDestoried();</span><br><span class="line"> invokers = list(invocation);</span><br><span class="line"> checkInvokers(invokers, invocation);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> // 负载均衡</span><br><span class="line"> final Invoker<T> invoker = select(loadbalance, invocation, invokers, invoked);</span><br><span class="line"> invoked.add(invoker);</span><br><span class="line"> lastInvoked.value = invoker;</span><br><span class="line"> RpcContext.getContext().setInvokers((List) invoked);</span><br><span class="line"></span><br><span class="line"> try {</span><br><span class="line"> return invoker.invoke(invocation);</span><br><span class="line"> } catch (RpcException e) {</span><br><span class="line"> //业务异常不重试</span><br><span class="line"> if (e.isBiz()) {</span><br><span class="line"> throw e;</span><br><span class="line"> }</span><br><span class="line"> lastException.value = e;</span><br><span class="line"> } catch (Throwable e) {</span><br><span class="line"> lastException.value = new RpcException(e.getMessage(), e);</span><br><span class="line"> } finally {</span><br><span class="line"> providers.add(invoker.getUrl().getAddress());</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> if (--retries == 0) {</span><br><span class="line"> throw populateException(invokers, lastException.value, providers, invocation, totalRetries);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> return doInvoke(invokers, invoked, lastException, providers, invocation, loadbalance, totalRetries, retries, lastInvoked);</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<h4 id="Cluster的实现"><a href="#Cluster的实现" class="headerlink" title="Cluster的实现"></a>Cluster的实现</h4><ul>
<li>MockClusterInvoker<br>调用失败降级到mock接口;</li>
<li>BroadcastClusterInvoker<br>每个Invoker都调一次,忽略了LoadBalance;</li>
<li>AvailableClusterInvoker<br>把处于可用状态的Invoker都调一遍。</li>
<li>FailoverClusterInvoker<br>一个Invoker失败就换个Invoker重试几次。</li>
<li>FailbackClusterInvoker<br>如果调用失败就放到一个线程池中延迟5秒再发,一般用于发消息。</li>
<li>FailfastClusterInvoker<br>失败立刻报错</li>
<li>FailsafeClusterInvoker<br>失败就忽略,一般是用于记日志这种失败了影响也不大的场景。</li>
<li>ForkingClusterInvoker<br>一次性选n个Invoker,并行调用,只要有一个调用成功就返回,线程间通过<code>LinkedBlockingQueue</code>通信。</li>
</ul>
<h3 id="LoadBalance"><a href="#LoadBalance" class="headerlink" title="LoadBalance"></a>LoadBalance</h3><p>Cluster 层包含多个 Invoker,LoadBalance 负责从中选出一个来调用,有多种 LoadBalance 策略,比如随机选一个(<code>RandomLoadBalance</code>)、轮询(<code>RoundRobinLoadBalance</code>)、一致性hash(ConsistentHashLoadBalance)。<br>实例化LoadBalance:<code>com.alibaba.dubbo.rpc.cluster.support.AbstractClusterInvoker#invoke</code><br>使用LoadBalance选择一个Invoker:<code>com.alibaba.dubbo.rpc.cluster.support.AbstractClusterInvoker#select</code></p>
<h4 id="LoadBalance的多种实现"><a href="#LoadBalance的多种实现" class="headerlink" title="LoadBalance的多种实现"></a>LoadBalance的多种实现</h4><ul>
<li>RandomLoadBalance<br>计算权重,然后根据每个Invoker的权重调一个。</li>
<li>LeastActiveLoadBalance<br>找最近最不活跃的Invoker调用,如果这样的Invoker有多个,则按权重来随机选一个。</li>
<li>RoundRobinLoadBalance<br>轮询</li>
<li>ConsistentHashLoadBalance<br>一致性哈希,启动时会将Invoker排列在一个圆环上:<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line">public ConsistentHashSelector(List<Invoker<T>> invokers, String methodName, int identityHashCode) {</span><br><span class="line"> this.virtualInvokers = new TreeMap<Long, Invoker<T>>();</span><br><span class="line"> this.identityHashCode = identityHashCode;</span><br><span class="line"> URL url = invokers.get(0).getUrl();</span><br><span class="line"></span><br><span class="line"> String[] index = Constants.COMMA_SPLIT_PATTERN.split(url.getMethodParameter(methodName, "hash.arguments", "0"));</span><br><span class="line"> argumentIndex = new int[index.length];</span><br><span class="line"> for (int i = 0; i < index.length; i++) {</span><br><span class="line"> argumentIndex[i] = Integer.parseInt(index[i]);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> int replicaNumber = url.getMethodParameter(methodName, "hash.nodes", 160);</span><br><span class="line"> for (Invoker<T> invoker : invokers) {</span><br><span class="line"> String address = invoker.getUrl().getAddress();</span><br><span class="line"> // 多复制几个,更均匀,避免所有请求都被hash到同一个Invoker</span><br><span class="line"> for (int i = 0; i < replicaNumber / 4; i++) {</span><br><span class="line"> byte[] digest = md5(address + i);</span><br><span class="line"> for (int h = 0; h < 4; h++) {</span><br><span class="line"> long m = hash(digest, h);</span><br><span class="line"> // 放入圆环上</span><br><span class="line"> virtualInvokers.put(m, invoker);</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
将Invoker保存到virtualInvokers上,但是virtualInvokers本身是一个HashMap,如果新来的请求不能精确hash到其中的某个Invoker怎么办?是通过tailMap找到的下一个Invoker:<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line">private Invoker<T> selectForKey(long hash) {</span><br><span class="line"> Invoker<T> invoker;</span><br><span class="line"> Long key = hash;</span><br><span class="line"></span><br><span class="line"> if (!virtualInvokers.containsKey(key)) {</span><br><span class="line"> SortedMap<Long, Invoker<T>> tailMap = virtualInvokers.tailMap(key);</span><br><span class="line"> if (tailMap.isEmpty()) {</span><br><span class="line"> key = virtualInvokers.firstKey();</span><br><span class="line"> } else {</span><br><span class="line"> key = tailMap.firstKey();</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> invoker = virtualInvokers.get(key);</span><br><span class="line"> return invoker;</span><br><span class="line">}</span><br></pre></td></tr></table></figure></li>
</ul>
<h3 id="Filter-Invoker-层"><a href="#Filter-Invoker-层" class="headerlink" title="Filter & Invoker 层"></a>Filter & Invoker 层</h3><p>不过,在实际网络调用之前,Dubbo还提供Filter功能,Cluster会先激活Filter链然后最终调到<code>DubboInvoker.invoke(RpcInvocation)</code>:</p>
<ol>
<li><code>ConsumerContextFilter</code>可以将请求对象<code>Invocation</code>添加到上下文<code>RpcContext</code>中,其实就是存储到一个ThreadLocal变量中。</li>
<li><code>FutureFilter</code>在调用完毕后唤醒调用者线程。</li>
<li>或许还会有一些自定义的Filter,比如增加线程的TraceId、打印一些调用日志之类的,Filter结束后才最终调用到<code>DubboInvoker</code>。</li>
</ol>
<p><code>DubboInvoker</code>封装了同步和异步调用,Dubbo 实现同步和异步调用比较关键的一点就在于由谁调用 ResponseFuture 的 get 方法。同步调用模式下,由框架自身调用 ResponseFuture 的 get 方法。异步调用模式下,则由用户调用该方法。<br>DubboInvoker是通过Netty发送消息的,消息本身如何发送的就不多说了。</p>
<h3 id="Exchange"><a href="#Exchange" class="headerlink" title="Exchange"></a>Exchange</h3><p>封装了网络客户端的发送逻辑,包括:</p>
<ul>
<li>HeaderExchangeChannel<br>对 Request 的序列化</li>
<li>ReferenceCountExchangeClient<br>无引用时自动关闭客户端</li>
<li>HeaderExchangeClient<br>心跳检测</li>
</ul>
<h3 id="数据编码-发送"><a href="#数据编码-发送" class="headerlink" title="数据编码 & 发送"></a>数据编码 & 发送</h3><p>DubboCodec<br>NettyChannel#send</p>
<h2 id="Provider端接受调用的过程"><a href="#Provider端接受调用的过程" class="headerlink" title="Provider端接受调用的过程"></a>Provider端接受调用的过程</h2><ol>
<li>接收请求<br>NettyClient<br>请求被接收后,通过 Netty 调用链向下传递执行<br>NettyHandler#messageReceived<br>NettyChannel</li>
<li>解码<br><code>ExchangeCodec</code></li>
<li>线程派发<br><code>Dispatcher</code><br>IO 线程接收请求后分发给事件处理线程执行,具体的派发逻辑在<code>ChannelHandler</code>中实现,比如<code>AllChannelHandler</code>。</li>
<li>请求分发<br><code>ChannelEventRunnable</code><br>根据请求类型将请求分发给不同的<code>ChannelHandler</code>处理。</li>
</ol>
<p>Provider 端响应</p>
<p>Consumer 端接收响应</p>
<ol>
<li>发送完请求后阻塞<br><code>HeaderExchangeHandler</code><br>用户线程在发送完请求后,会调用 <code>DefaultFuture</code> 的 <code>get</code> 方法等待响应对象的到来,这时每个<code>DefaultFuture</code>都会关联一个<strong>调用编号</strong>,用于在接收到响应时能对应上请求的<code>DefaultFuture</code>。<br>当响应对象到来后,IO 线程根据<strong>调用编号</strong>可以找到<code>DefaultFuture</code>,之后会将响应对象保存到<code>DefaultFuture</code>,并唤醒用户线程。</li>
</ol>
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/kity@2.0.4/dist/kity.min.js"></script><script type="text/javascript" src="https://cdn.jsdelivr.net/npm/kityminder-core@1.4.50/dist/kityminder.core.min.js"></script><script defer="true" type="text/javascript" src="https://cdn.jsdelivr.net/npm/hexo-simple-mindmap@0.6.0/dist/mindmap.min.js"></script><link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/hexo-simple-mindmap@0.6.0/dist/mindmap.min.css">
</div>
<footer class="post-footer">
<div class="post-tags">
<a href="/tags/Dubbo/" rel="tag"># Dubbo</a>
</div>
<div class="post-nav">
<div class="post-nav-item">
<a href="/353ec849.html" rel="prev" title="Dubbo 概述">
<i class="fa fa-angle-left"></i> Dubbo 概述
</a>
</div>
<div class="post-nav-item">
<a href="/4e8abc71.html" rel="next" title="Java-线程">
Java-线程 <i class="fa fa-angle-right"></i>
</a>
</div>
</div>
</footer>
</article>
</div>
</div>
</main>
<footer class="footer">
<div class="footer-inner">
<div class="copyright">
©
<span itemprop="copyrightYear">2025</span>
<span class="with-love">
<i class="fa fa-heart"></i>
</span>
<span class="author" itemprop="copyrightHolder">tallate</span>
</div>
<div class="powered-by">由 <a href="https://hexo.io/" rel="noopener" target="_blank">Hexo</a> & <a href="https://theme-next.js.org/" rel="noopener" target="_blank">NexT.Gemini</a> 强力驱动
</div>
</div>
</footer>
<div class="back-to-top" role="button" aria-label="返回顶部">
<i class="fa fa-arrow-up fa-lg"></i>
<span>0%</span>
</div>
<a href="https://github.com/tallate" class="github-corner" title="在 GitHub 上关注我" aria-label="在 GitHub 上关注我" rel="noopener" target="_blank"><svg width="80" height="80" viewBox="0 0 250 250" aria-hidden="true"><path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path><path d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2" fill="currentColor" style="transform-origin: 130px 106px;" class="octo-arm"></path><path d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z" fill="currentColor" class="octo-body"></path></svg></a>
<noscript>
<div class="noscript-warning">Theme NexT works best with JavaScript enabled</div>
</noscript>
<script src="https://cdnjs.cloudflare.com/ajax/libs/animejs/3.2.1/anime.min.js" integrity="sha256-XL2inqUJaslATFnHdJOi9GfQ60on8Wx1C2H8DYiN1xY=" crossorigin="anonymous"></script>
<script src="/js/comments.js"></script><script src="/js/utils.js"></script><script src="/js/motion.js"></script><script src="/js/next-boot.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/hexo-generator-searchdb/1.4.1/search.js" integrity="sha256-1kfA5uHPf65M5cphT2dvymhkuyHPQp5A53EGZOnOLmc=" crossorigin="anonymous"></script>
<script src="/js/third-party/search/local-search.js"></script>
<script class="next-config" data-name="mermaid" type="application/json">{"enable":true,"version":"7.1.2","options":null,"js":{"url":"https://cdnjs.cloudflare.com/ajax/libs/mermaid/10.3.0/mermaid.min.js","integrity":"sha256-9y71g5Lz/KLsHjB8uXwnkuWDtAMDSzD/HdIbqhJfTAI="}}</script>
<script src="/js/third-party/tags/mermaid.js"></script>
</body>
</html>