-
Notifications
You must be signed in to change notification settings - Fork 0
/
index.html
2439 lines (1129 loc) · 233 KB
/
index.html
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
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<!DOCTYPE html>
<html class="theme-next muse use-motion" lang="zh-Hans">
<head>
<meta charset="UTF-8"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"/>
<meta name="theme-color" content="#222">
<meta http-equiv="Cache-Control" content="no-transform" />
<meta http-equiv="Cache-Control" content="no-siteapp" />
<link href="/lib/fancybox/source/jquery.fancybox.css?v=2.1.5" rel="stylesheet" type="text/css" />
<link href="//fonts.googleapis.com/css?family=Lato:300,300italic,400,400italic,700,700italic&subset=latin,latin-ext" rel="stylesheet" type="text/css">
<link href="/lib/font-awesome/css/font-awesome.min.css?v=4.6.2" rel="stylesheet" type="text/css" />
<link href="/css/main.css?v=5.1.2" rel="stylesheet" type="text/css" />
<meta name="keywords" content="Hexo, NexT" />
<link rel="shortcut icon" type="image/x-icon" href="/img/lufei.ico?v=5.1.2" />
<meta name="description" content="世之奇伟、瑰怪,非常之观,常在于险远,而人之所罕至焉,故非有志者不能至也">
<meta property="og:type" content="website">
<meta property="og:title" content="Youmai の Blog">
<meta property="og:url" content="http://yoursite.com/index.html">
<meta property="og:site_name" content="Youmai の Blog">
<meta property="og:description" content="世之奇伟、瑰怪,非常之观,常在于险远,而人之所罕至焉,故非有志者不能至也">
<meta property="og:locale" content="zh-Hans">
<meta name="twitter:card" content="summary">
<meta name="twitter:title" content="Youmai の Blog">
<meta name="twitter:description" content="世之奇伟、瑰怪,非常之观,常在于险远,而人之所罕至焉,故非有志者不能至也">
<script type="text/javascript" id="hexo.configurations">
var NexT = window.NexT || {};
var CONFIG = {
root: '/',
scheme: 'Muse',
sidebar: {"position":"right","display":"post","offset":12,"offset_float":12,"b2t":false,"scrollpercent":false,"onmobile":false},
fancybox: true,
tabs: true,
motion: true,
duoshuo: {
userId: '0',
author: '博主'
},
algolia: {
applicationID: '',
apiKey: '',
indexName: '',
hits: {"per_page":10},
labels: {"input_placeholder":"Search for Posts","hits_empty":"We didn't find any results for the search: ${query}","hits_stats":"${hits} results found in ${time} ms"}
}
};
</script>
<link rel="canonical" href="http://yoursite.com/"/>
<title>Youmai の Blog</title>
<script>
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
ga('create', 'UA-63551049-1', 'auto');
ga('send', 'pageview');
</script>
</head>
<body itemscope itemtype="http://schema.org/WebPage" lang="zh-Hans">
<div class="container sidebar-position-right
page-home
">
<div class="headband"></div>
<header id="header" class="header" itemscope itemtype="http://schema.org/WPHeader">
<div class="header-inner"><div class="site-brand-wrapper">
<div class="site-meta ">
<div class="custom-logo-site-title">
<a href="/" class="brand" rel="start">
<span class="logo-line-before"><i></i></span>
<span class="site-title">Youmai の Blog</span>
<span class="logo-line-after"><i></i></span>
</a>
</div>
<p class="site-subtitle"></p>
</div>
<div class="site-nav-toggle">
<button>
<span class="btn-bar"></span>
<span class="btn-bar"></span>
<span class="btn-bar"></span>
</button>
</div>
</div>
<nav class="site-nav">
<ul id="menu" class="menu">
<li class="menu-item menu-item-home">
<a href="/" rel="section">
<i class="menu-item-icon fa fa-fw fa-home"></i> <br />
首页
</a>
</li>
<li class="menu-item menu-item-categories">
<a href="/categories/" rel="section">
<i class="menu-item-icon fa fa-fw fa-th"></i> <br />
分类
</a>
</li>
<li class="menu-item menu-item-about">
<a href="/about/" rel="section">
<i class="menu-item-icon fa fa-fw fa-user"></i> <br />
关于
</a>
</li>
<li class="menu-item menu-item-archives">
<a href="/archives/" rel="section">
<i class="menu-item-icon fa fa-fw fa-archive"></i> <br />
归档
</a>
</li>
<li class="menu-item menu-item-tags">
<a href="/tags/" rel="section">
<i class="menu-item-icon fa fa-fw fa-tags"></i> <br />
标签
</a>
</li>
</ul>
</nav>
</div>
</header>
<main id="main" class="main">
<div class="main-inner">
<div class="content-wrap">
<div id="content" class="content">
<section id="posts" class="posts-expand">
<article class="post post-type-normal" itemscope itemtype="http://schema.org/Article">
<div class="post-block">
<link itemprop="mainEntityOfPage" href="http://yoursite.com/2018/10/29/nginx代理socket-io服务踩坑/">
<span hidden itemprop="author" itemscope itemtype="http://schema.org/Person">
<meta itemprop="name" content="You Wangqiu">
<meta itemprop="description" content="">
<meta itemprop="image" content="/img/lufei.jpeg">
</span>
<span hidden itemprop="publisher" itemscope itemtype="http://schema.org/Organization">
<meta itemprop="name" content="Youmai の Blog">
</span>
<header class="post-header">
<h1 class="post-title" itemprop="name headline">
<a class="post-title-link" href="/2018/10/29/nginx代理socket-io服务踩坑/" itemprop="url">nginx代理socket.io服务踩坑</a></h1>
<div class="post-meta">
<span class="post-time">
<span class="post-meta-item-icon">
<i class="fa fa-calendar-o"></i>
</span>
<span class="post-meta-item-text">发表于</span>
<time title="创建于" itemprop="dateCreated datePublished" datetime="2018-10-29T23:39:02+08:00">
2018-10-29
</time>
</span>
<span class="post-category" >
<span class="post-meta-divider">|</span>
<span class="post-meta-item-icon">
<i class="fa fa-folder-o"></i>
</span>
<span class="post-meta-item-text">分类于</span>
<span itemprop="about" itemscope itemtype="http://schema.org/Thing">
<a href="/categories/socket-io/" itemprop="url" rel="index">
<span itemprop="name">socket.io</span>
</a>
</span>
</span>
</div>
</header>
<div class="post-body" itemprop="articleBody">
<h3 id="场景"><a href="#场景" class="headerlink" title="场景"></a>场景</h3><p>nginx代理了两台socket.io服务器。socket.io的工作模式是polling升级到websocket</p>
<h3 id="现象"><a href="#现象" class="headerlink" title="现象"></a>现象</h3><p>通过nginx请求服务时,出现了大量的400错误,有时候能升级到websocket,有时候会一直报错。但是直接通过<code>ip+端口</code>访问时,100%能成功。</p>
<p><img src="/img/400.jpg" alt="400"></p>
<h3 id="分析"><a href="#分析" class="headerlink" title="分析"></a>分析</h3><h4 id="sid"><a href="#sid" class="headerlink" title="sid"></a>sid</h4><p>sid是我们这个问题的关键。在初始创建连接时(polling模式就是在模拟一个长连接),客户端会发起这样的请求:</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div></pre></td><td class="code"><pre><div class="line">https://***/?EIO=3&transport=polling&t=1540820717277-0</div></pre></td></tr></table></figure>
<p>服务端收到后会创建一个对象,绑定在这个连接上,同时返回一个sid(session id),来标记这个会话。会话指什么呢,会话是一连串的交互,这些交互之间是有联系的,在我们这个场景下就是,下一次的http请求到来,我需要找到之前绑定在理论上的长连接(这里还没有websocket,所以是理论上的)上的那个对象。我们知道http请求是无状态的,每个请求之间独立,所以socket.io引入了sid来做这件事。服务端收到请求后会生成一个sid,看下response:</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div></pre></td><td class="code"><pre><div class="line">{"sid":"EoGaL3fRQlpTOaLp5eST","upgrades":["websocket"],"pingInterval":8000,"pingTimeout":10000}</div></pre></td></tr></table></figure>
<p>之后每次请求都需要带上这个sid,建立websocket请求的连接也不例外。所以说,sid是polling,以及polling升级到websocket的关键。这之后的请求类似于:</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div></pre></td><td class="code"><pre><div class="line">https://***/?EIO=3&transport=polling&t=1540820717314-1&sid=EoGaL3fRQlpTOaLp5eST</div><div class="line"></div><div class="line">or</div><div class="line"></div><div class="line">wss://***/?EIO=3&transport=websocket&t=1540820717314-1&sid=EoGaL3fRQlpTOaLp5eST</div></pre></td></tr></table></figure>
<p>那么问题来了,如果请求是带上的sid不是服务端生成的会怎样呢?服务端会不认识,给你返回一个400,并告诉你</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div></pre></td><td class="code"><pre><div class="line">invalid sid</div></pre></td></tr></table></figure>
<p>我们遇到的便是这个问题,nginx默认的负载均衡策略是轮询,所以请求有可能会打到不是生成这个sid的机器上去,这时候我们就会收到一个400,如果运气好,可能也会打到原来的机器上,运气更好一点,甚至能坚持到websocket连接建立。</p>
<h3 id="解决"><a href="#解决" class="headerlink" title="解决"></a>解决</h3><p>这里提出两种方案</p>
<ol>
<li>nginx的负载均衡采用ip_hash,这样能保证一个客户端的请求都走到一台服务器上</li>
<li>不使用polling模式,只使用websocket</li>
</ol>
<p>这两种方案各有利弊。第二种显而易见,不支持websocket的古老浏览器和客户端将没法工作。第一种的问题隐藏得比较深,试想,如果你增减了机器会怎样,这时候ip_hash策略的模将变化,之前的连接将全部失效,而对于微服务,扩缩容是很频繁的操作(特别是产品处于发展期),这种有损的扩缩容很大概率是不能接受的。</p>
<p>综上,建议直接使用websocket,毕竟不支持websocket的老版本占比很少,而且相对于先polling,耗时也会减少。</p>
</div>
<footer class="post-footer">
<div class="post-eof"></div>
</footer>
</div>
</article>
<article class="post post-type-normal" itemscope itemtype="http://schema.org/Article">
<div class="post-block">
<link itemprop="mainEntityOfPage" href="http://yoursite.com/2018/08/01/真实世界中的WebRTC/">
<span hidden itemprop="author" itemscope itemtype="http://schema.org/Person">
<meta itemprop="name" content="You Wangqiu">
<meta itemprop="description" content="">
<meta itemprop="image" content="/img/lufei.jpeg">
</span>
<span hidden itemprop="publisher" itemscope itemtype="http://schema.org/Organization">
<meta itemprop="name" content="Youmai の Blog">
</span>
<header class="post-header">
<h1 class="post-title" itemprop="name headline">
<a class="post-title-link" href="/2018/08/01/真实世界中的WebRTC/" itemprop="url">真实世界中的WebRTC:STUN, TURN and signaling</a></h1>
<div class="post-meta">
<span class="post-time">
<span class="post-meta-item-icon">
<i class="fa fa-calendar-o"></i>
</span>
<span class="post-meta-item-text">发表于</span>
<time title="创建于" itemprop="dateCreated datePublished" datetime="2018-08-01T10:16:13+08:00">
2018-08-01
</time>
</span>
<span class="post-category" >
<span class="post-meta-divider">|</span>
<span class="post-meta-item-icon">
<i class="fa fa-folder-o"></i>
</span>
<span class="post-meta-item-text">分类于</span>
<span itemprop="about" itemscope itemtype="http://schema.org/Thing">
<a href="/categories/WebRTC/" itemprop="url" rel="index">
<span itemprop="name">WebRTC</span>
</a>
</span>
</span>
</div>
</header>
<div class="post-body" itemprop="articleBody">
<p>WebRTC使端到端能够通信。</p>
<p>但是…</p>
<p>WebRTC仍然需要服务器:</p>
<ul>
<li>让客户端交换元数据来协调通信:这被称为信令(signaling)</li>
<li>处理网络地址转换(NATs)和防火墙</li>
</ul>
<p>这篇文章将会展示如何搭建一个信令服务</p>
<p>在本文中,我们将向您展示如何构建信令服务,以及如何使用STUN和TURN服务器处理真实连接的难题。 我们还解释了WebRTC应用程序如何处理多方通话以及与VoIP和PSTN(又称电话)等服务进行交互。</p>
<h3 id="什么是信令"><a href="#什么是信令" class="headerlink" title="什么是信令"></a>什么是信令</h3><p>信令是协调通信的过程。 为了使WebRTC应用程序能够建立一个“通话”,其客户端需要交换以下信息:</p>
<ul>
<li>会话控制消息用于打开或关闭通信</li>
<li>错误消息</li>
<li>媒体元数据,如编解码器和编解码器设置,带宽和媒体类型</li>
<li>密钥数据,用于建立安全的连接</li>
<li>网络数据,如:外界看到的主机IP地址和端口</li>
</ul>
<p>此信令过程需要一种方法让客户端来回传递消息。 WebRTC API不实现该机制:你需要自己构建它。 我们在下面描述了构建信令服务的一些方法。 首先,需要一点背景…</p>
<h3 id="为什么信令不是由WebRTC定义的?"><a href="#为什么信令不是由WebRTC定义的?" class="headerlink" title="为什么信令不是由WebRTC定义的?"></a>为什么信令不是由WebRTC定义的?</h3><p>为了避免出现冗余,并最大限度地提高与已有技术的兼容性,WebRTC标准并没有规定信令方法和协议。<a href="http://tools.ietf.org/html/draft-ietf-rtcweb-jsep-03#section-1.1" target="_blank" rel="external">JavaScript会话建立协议</a>JSEP概述了这种方法:</p>
<blockquote>
<p>WebRTC通话建立的思想是完全指定和控制媒体平面,但是尽可能将信令平面留给应用程序。其原理是,不同的应用程序可能更喜欢使用不同的协议,例如现有的SIP或Jingle呼叫信令协议,或者对于特定应用程序定制的东西,可能是针对新颖的用例。在这种方法中,需要交换的关键信息是多媒体会话描述,其指定了建立媒体平面所必需的传输和媒体配置信息。</p>
</blockquote>
<p>JSEP的架构也避免了浏览器不得不保存状态,即作为一个信令状态机。如果信令数据在每次刷新页面的时候都会发生丢失,就会出现问题。相反,信令状态机可以保存在服务器上。</p>
<p><img src="/img/jsep.png" alt="jsep arch"></p>
<p>JSEP要求端之间交换offer和answer:上面提到的媒体元数据。offer和answer以会话描述协议(SDP)的格式传递,如下所示:</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div><div class="line">12</div><div class="line">13</div><div class="line">14</div><div class="line">15</div><div class="line">16</div><div class="line">17</div></pre></td><td class="code"><pre><div class="line">v=0</div><div class="line">o=- 7614219274584779017 2 IN IP4 127.0.0.1</div><div class="line">s=-</div><div class="line">t=0 0</div><div class="line">a=group:BUNDLE audio video</div><div class="line">a=msid-semantic: WMS</div><div class="line">m=audio 1 RTP/SAVPF 111 103 104 0 8 107 106 105 13 126</div><div class="line">c=IN IP4 0.0.0.0</div><div class="line">a=rtcp:1 IN IP4 0.0.0.0</div><div class="line">a=ice-ufrag:W2TGCZw2NZHuwlnf</div><div class="line">a=ice-pwd:xdQEccP40E+P0L5qTyzDgfmW</div><div class="line">a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level</div><div class="line">a=mid:audio</div><div class="line">a=rtcp-mux</div><div class="line">a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:9c1AHz27dZ9xPI91YNfSlI67/EMkjHHIHORiClQe</div><div class="line">a=rtpmap:111 opus/48000/2</div><div class="line">…</div></pre></td></tr></table></figure>
<p>想知道所有这些SDP gobbledygook实际意味着什么? 看看<a href="http://datatracker.ietf.org/doc/draft-nandakumar-rtcweb-sdp/?include_text=1" target="_blank" rel="external">IETF的例子</a>。</p>
<p>需要记住的是,WebRTC被设计为在被设置为本地或者远端描述之前,通过编辑SDP文本中的值,可以扭转offer或者answer。例如,<a href="https://appr.tc/" target="_blank" rel="external">apprtc.tc</a>中的preferAudioCodec()函数可用于设置默认编解码器和比特率。使用 JavaScript处理SDP有些困难,有一些关于WebRTC的未来版本中是否应该使用JSON的讨论,但是坚持使用SDP还是有一些<a href="http://tools.ietf.org/html/draft-ietf-rtcweb-jsep-03#section-3.3" target="_blank" rel="external">优势</a>的。</p>
<h3 id="RTCPeerConnection-signaling-offer-answer-and-candidate"><a href="#RTCPeerConnection-signaling-offer-answer-and-candidate" class="headerlink" title="RTCPeerConnection + signaling: offer, answer and candidate"></a>RTCPeerConnection + signaling: offer, answer and candidate</h3><p>RTCPeerConnection是WebRTC应用程序用来创建端对端连接并传输音视频的API。</p>
<p>为初始化这个过程,RTCPeerConnection有两个工作要做:</p>
<ul>
<li>确定本地媒体条件,如分辨率和编解码器功能。这是用于offer和answer机制的元数据。</li>
<li>获取应用程序主机的潜在网络地址,成为候选人(candidates)</li>
</ul>
<p>一旦确定了本地数据,就必须通过信令机制与远端进行交换。</p>
<p>让我们假设一个场景:<a href="https://xkcd.com/177/" target="_blank" rel="external">Alice正在尝试呼叫Eve</a>。下面是完整的offer/answer机制:</p>
<ol>
<li>Alice创建一个RTCPeerConnection对象。</li>
<li>Alice使用RTCPeerConnection createOffer()方法产生一个offer(一个SDP会话描述)。</li>
<li>Alice用他的offer调用setLocalDescription()。</li>
<li>Alice将offer字符串化,并使用信令机制将其发送给Eve。</li>
<li>Eve用Alice的offer调用setRemoteDescription(),以便她的RTCPeerConnection知道Alice的设置。</li>
<li>Eve调用createAnswer(),成功的回调是传入一个本地的会话描述:Eve的answer。</li>
<li>Eve通过调用setLocalDescription()将其answer设置为本地描述。</li>
<li>Eve然后使用信令机制将她的字符串化的answer发回给Alice。</li>
<li>Alice使用setRemoteDescription()将Eve的应答设置为远程会话描述。</li>
</ol>
<p>Alice和Eve也需要交换网络信息。“查找候选人(find candidate)”这个表达是指使用<a href="http://en.wikipedia.org/wiki/Interactive_Connectivity_Establishment" target="_blank" rel="external">ICE框架</a>查找网络接口和端口的过程。</p>
<ol>
<li>Alice使用onicecandidate handler创建一个RTCPeerConnection对象。</li>
<li>handler在网络候选人变得可用时被调用。</li>
<li>在handler中,Alice通过他们的信令通道将字符串化的候选数据发送给Eve。</li>
<li>当Eve从Alice那里获得候选消息时,她调用addIceCandidate(),将候选项添加到远端描述中。</li>
</ol>
<p>JSEP支持<a href="http://tools.ietf.org/html/draft-ietf-rtcweb-jsep-03#section-3.4.1" target="_blank" rel="external">ICE Candidate Trickling</a>,它允许主叫方(caller)在最初的offer之后递增地向被叫方提供候选项(candidates),并使被叫方开始在通话中进行操作并建立连接而不用等所有候选项到达。</p>
<h3 id="WebRTC信令代码"><a href="#WebRTC信令代码" class="headerlink" title="WebRTC信令代码"></a>WebRTC信令代码</h3><p>下面的<a href="https://w3c.github.io/webrtc-pc/#simple-peer-to-peer-example" target="_blank" rel="external">W3C代码示例</a>总结了一个完整的信令过程。该代码假定存在一些信令机制,SignalingChannel。我们会在下文中更详细地讨论信令。</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div><div class="line">12</div><div class="line">13</div><div class="line">14</div><div class="line">15</div><div class="line">16</div><div class="line">17</div><div class="line">18</div><div class="line">19</div><div class="line">20</div><div class="line">21</div><div class="line">22</div><div class="line">23</div><div class="line">24</div><div class="line">25</div><div class="line">26</div><div class="line">27</div><div class="line">28</div><div class="line">29</div><div class="line">30</div><div class="line">31</div><div class="line">32</div><div class="line">33</div><div class="line">34</div><div class="line">35</div><div class="line">36</div><div class="line">37</div><div class="line">38</div><div class="line">39</div><div class="line">40</div><div class="line">41</div><div class="line">42</div><div class="line">43</div><div class="line">44</div><div class="line">45</div><div class="line">46</div><div class="line">47</div><div class="line">48</div><div class="line">49</div><div class="line">50</div><div class="line">51</div><div class="line">52</div><div class="line">53</div><div class="line">54</div><div class="line">55</div><div class="line">56</div><div class="line">57</div><div class="line">58</div><div class="line">59</div><div class="line">60</div><div class="line">61</div><div class="line">62</div><div class="line">63</div><div class="line">64</div><div class="line">65</div></pre></td><td class="code"><pre><div class="line"><span class="comment">// handles JSON.stringify/parse</span></div><div class="line"><span class="keyword">const</span> signaling = <span class="keyword">new</span> SignalingChannel();</div><div class="line"><span class="keyword">const</span> constraints = {<span class="attr">audio</span>: <span class="literal">true</span>, <span class="attr">video</span>: <span class="literal">true</span>};</div><div class="line"><span class="keyword">const</span> configuration = {<span class="attr">iceServers</span>: [{<span class="attr">urls</span>: <span class="string">'stuns:stun.example.org'</span>}]};</div><div class="line"><span class="keyword">const</span> pc = <span class="keyword">new</span> RTCPeerConnection(configuration);</div><div class="line"></div><div class="line"><span class="comment">// send any ice candidates to the other peer</span></div><div class="line">pc.onicecandidate = <span class="function">(<span class="params">{candidate}</span>) =></span> signaling.send({candidate});</div><div class="line"></div><div class="line"><span class="comment">// let the "negotiationneeded" event trigger offer generation</span></div><div class="line">pc.onnegotiationneeded = <span class="keyword">async</span> () => {</div><div class="line"> <span class="keyword">try</span> {</div><div class="line"> <span class="keyword">await</span> pc.setLocalDescription(<span class="keyword">await</span> pc.createOffer());</div><div class="line"> <span class="comment">// send the offer to the other peer</span></div><div class="line"> signaling.send({<span class="attr">desc</span>: pc.localDescription});</div><div class="line"> } <span class="keyword">catch</span> (err) {</div><div class="line"> <span class="built_in">console</span>.error(err);</div><div class="line"> }</div><div class="line">};</div><div class="line"></div><div class="line"><span class="comment">// once remote track media arrives, show it in remote video element</span></div><div class="line">pc.ontrack = <span class="function">(<span class="params">event</span>) =></span> {</div><div class="line"> <span class="comment">// don't set srcObject again if it is already set.</span></div><div class="line"> <span class="keyword">if</span> (remoteView.srcObject) <span class="keyword">return</span>;</div><div class="line"> remoteView.srcObject = event.streams[<span class="number">0</span>];</div><div class="line">};</div><div class="line"></div><div class="line"><span class="comment">// call start() to initiate</span></div><div class="line"><span class="keyword">async</span> <span class="function"><span class="keyword">function</span> <span class="title">start</span>(<span class="params"></span>) </span>{</div><div class="line"> <span class="keyword">try</span> {</div><div class="line"> <span class="comment">// get local stream, show it in self-view and add it to be sent</span></div><div class="line"> <span class="keyword">const</span> stream =</div><div class="line"> <span class="keyword">await</span> navigator.mediaDevices.getUserMedia(constraints);</div><div class="line"> stream.getTracks().forEach(<span class="function">(<span class="params">track</span>) =></span></div><div class="line"> pc.addTrack(track, stream));</div><div class="line"> selfView.srcObject = stream;</div><div class="line"> } <span class="keyword">catch</span> (err) {</div><div class="line"> <span class="built_in">console</span>.error(err);</div><div class="line"> }</div><div class="line">}</div><div class="line"></div><div class="line">signaling.onmessage = <span class="keyword">async</span> ({desc, candidate}) => {</div><div class="line"> <span class="keyword">try</span> {</div><div class="line"> <span class="keyword">if</span> (desc) {</div><div class="line"> <span class="comment">// if we get an offer, we need to reply with an answer</span></div><div class="line"> <span class="keyword">if</span> (desc.type === <span class="string">'offer'</span>) {</div><div class="line"> <span class="keyword">await</span> pc.setRemoteDescription(desc);</div><div class="line"> <span class="keyword">const</span> stream =</div><div class="line"> <span class="keyword">await</span> navigator.mediaDevices.getUserMedia(constraints);</div><div class="line"> stream.getTracks().forEach(<span class="function">(<span class="params">track</span>) =></span></div><div class="line"> pc.addTrack(track, stream));</div><div class="line"> <span class="keyword">await</span> pc.setLocalDescription(<span class="keyword">await</span> pc.createAnswer());</div><div class="line"> signaling.send({<span class="attr">desc</span>: pc.localDescription});</div><div class="line"> } <span class="keyword">else</span> <span class="keyword">if</span> (desc.type === <span class="string">'answer'</span>) {</div><div class="line"> <span class="keyword">await</span> pc.setRemoteDescription(desc);</div><div class="line"> } <span class="keyword">else</span> {</div><div class="line"> <span class="built_in">console</span>.log(<span class="string">'Unsupported SDP type.'</span>);</div><div class="line"> }</div><div class="line"> } <span class="keyword">else</span> <span class="keyword">if</span> (candidate) {</div><div class="line"> <span class="keyword">await</span> pc.addIceCandidate(candidate);</div><div class="line"> }</div><div class="line"> } <span class="keyword">catch</span> (err) {</div><div class="line"> <span class="built_in">console</span>.error(err);</div><div class="line"> }</div><div class="line">};</div></pre></td></tr></table></figure>
<p>要查看实际offer/answer和候选项的交流过程,请参阅<a href="https://simpl.info/rtcpeerconnection/" target="_blank" rel="external">simpl.info/pc</a>上的“单页”视频聊天示例的控制台日志。如果你想知道更多,请从Chrome的chrome://webrtc-internals页面或者Opera中的opera://webrtc-internals页面下载WebRTC 信令和统计数据的完整转储。</p>
<h3 id="对端发现"><a href="#对端发现" class="headerlink" title="对端发现"></a>对端发现</h3><p>这是“我该如何找到我要交谈的人”的一种高端说法。</p>
<p>对于电话来说,我们有电话号码和目录。对于在线视频聊天和消息发送,我们需要身份和状态管理系统,以及用户启动会话的方式。WebRTC应用程序需要一种方式让客户互相通知他们想要开始或加入一个通话。</p>
<p>端发现机制不是由WebRTC定义的。这个过程可以像发送电子邮件或发送一个URL一样简单:对于视频聊天应用程序,比如talky.io,tawk.com和browsermeeting.com,您可以通过共享自定义链接来邀请人们进行通话。开发人员Chris Ball已经搭建了一个有趣的<a href="https://blog.printf.net/articles/2013/05/17/webrtc-without-a-signaling-server/" target="_blank" rel="external">无服务器的webrtc实验</a>,使WebRTC呼叫参与者能够通过他们喜欢的任何消息服务来交换元数据。</p>
<h3 id="我怎么才能建立一个信令服务?"><a href="#我怎么才能建立一个信令服务?" class="headerlink" title="我怎么才能建立一个信令服务?"></a>我怎么才能建立一个信令服务?</h3><p>重申:信令协议和机制不是由WebRTC标准定义的。无论你选择什么,你都需要一个中间服务器来在客户端之间交换信令消息和应用程序数据。不走运的是,一个网络应用程序不能简单地向互联网喊“连接到我朋友那!”</p>
<p>幸亏信令消息很小,而且大多在通话开始的时候进行交换。在使用<a href="http://appr.tc/" target="_blank" rel="external">appr.tc</a>进行测试时,我们发现对于视频聊天会话,信令服务总共处理了30-45条消息,总共消息的大小大约为10KB。</p>
<p>WebRTC 信令业务在带宽方面的要求相对较低,因为它们只需要中继消息并保留少量的会话状态数据(如连接的客户端),同样不会消耗太多的处理或存储空间。</p>
<blockquote>
<p>小贴士:用于交换会话元数据的信令机制也可用于传送应用程序数据。这只是一个消息服务而已!。</p>
</blockquote>
<h3 id="将消息从服务器推送到客户端"><a href="#将消息从服务器推送到客户端" class="headerlink" title="将消息从服务器推送到客户端"></a>将消息从服务器推送到客户端</h3><p>信令的消息服务需要是双向的:客户端到服务器和服务器到客户端。双向通信违背了HTTP客户端/服务器的请求/响应模型,但是为了将数据从运行在Web服务器上的服务推送到运行在浏览器中的Web应用程序,多年来已经开发了诸如<a href="https://en.wikipedia.org/wiki/Comet_(programming" target="_blank" rel="external">长轮询</a>)之类的各种hac<br>k方法。</p>
<p>最近,<a href="http://www.html5rocks.com/en/tutorials/eventsource/basics/" target="_blank" rel="external">EventSource API</a>已经得到了广泛的实现。这开启了 “服务器发送的事件”:通过HTTP从Web服务器发送到浏览器客户端的数据。在<a href="http://simpl.info/es" target="_blank" rel="external">simpl.info/es</a>上有一个简单的演示。EventSource是为单向消息传递而设计的,但是它可以和XHR结合使用来搭建交换信令消息的服务:信令服务器通过XHR请求传递来自呼叫者的消息,通过EventSource推送给被叫者。</p>
<p><a href="http://www.html5rocks.com/en/tutorials/websockets/basics/" target="_blank" rel="external">WebSocket</a>是一个更自然的解决方案,专为全双工客户端-服务器通信而设计(消息可以同时在两个方向上传输)。使用纯WebSocket或Server-Sent Events(EventSource)构建的信令服务的一个优点是这些API的后端可以在大多数Web托管软件包通用的各种Web框架上实现,比如PHP,Python和Ruby。</p>
<p>大约四分之三的浏览器都<a href="http://caniuse.com/#search=websocket" target="_blank" rel="external">支持WebSocket</a>,更重要的是,所有支持WebRTC的浏览器都支持WebSocket,无论是在台式机还是手机上。应该为所有连接都使用<a href="https://en.wikipedia.org/wiki/Transport_Layer_Security" target="_blank" rel="external">TLS</a>,以确保消息不会因为没有加密而被截取,并且<a href="http://www.infoq.com/articles/Web-Sockets-Proxy-Servers" target="_blank" rel="external">减少代理遍历</a>的问题。(有关WebSocket和代理遍历的更多信息,请参阅Ilya Grigorik的高性能浏览器网络中的<a href="http://hpbn.co/webrtc" target="_blank" rel="external">WebRTC章节</a>。Peter Lubber的<a href="http://refcardz.dzone.com/refcardz/html5-websocket" target="_blank" rel="external">WebSocket备忘单</a>提供了有关WebSocket客户端和服务器的更多信息)</p>
<p>用于标准的<a href="http://appr.tc/" target="_blank" rel="external">appr.tc</a> WebRTC视频聊天应用程序的信令通过<a href="https://developers.google.com/appengine/docs/java/channel/" target="_blank" rel="external">Google App Engine Channel API</a>完成,该API使用<a href="http://en.wikipedia.org/wiki/Comet_(programming" target="_blank" rel="external">Comet</a>)技术(长轮询)来启用App Engine后端与Web客户端之间推送通信的信令传输。<a href="http://www.html5rocks.com/en/tutorials/webrtc/basics/" target="_blank" rel="external">HTML5 Rocks WebRTC文章</a>中<a href="http://www.html5rocks.com/en/tutorials/webrtc/basics/#toc-simple" target="_blank" rel="external">详细介绍</a>了该应用程序的代码演示。</p>
<p><img src="/img/apprtc_in_action.jpg" alt="apprtc_in_action"></p>
<p>也可以通过让WebRTC客户端通过Ajax轮询消息服务器来处理信令,但这回导致大量的冗余网络请求,尤其对于移动设备是有问题的。即使在会话建立之后,对端也需要轮询信令消息,以防其他端发生改变或终止会话。<a href="http://webrtcbook.com/" target="_blank" rel="external">WebRTC Book</a> app示例使用此选项,并对轮询频率进行了一些优化。</p>
<h3 id="扩展信令"><a href="#扩展信令" class="headerlink" title="扩展信令"></a>扩展信令</h3><p>尽管信令服务消耗客户端带宽和CPU相对较少,但是一个很受欢迎的应用程序的信令服务器可能需要处理来自不同位置的大量消息,而且并发性较高。有大量流量的WebRTC应用程序需要能够处理相当大负载的信令服务器。</p>
<p>在这里我们不会对其进行详细讨论,但是对于大容量、高性能的消息传递有很多选择,包括:</p>
<ul>
<li><a href="http://en.wikipedia.org/wiki/Xmpp" target="_blank" rel="external">eXtensible Messaging and Presence Protocol</a>(XMPP),最初是叫Jabber:一个为即时消息而开发的可用于信令的协议。服务器实现包括<a href="http://en.wikipedia.org/wiki/Ejabberd" target="_blank" rel="external">ejabberd</a>和<a href="http://en.wikipedia.org/wiki/Openfire" target="_blank" rel="external">Openfire</a>。Strophe.js等 JavaScript等客户端使用BOSH模拟双向流,但由于各种原因,BOSH可能不如WebSocket高效,同样的原因可能导致无法很好地扩展缩放。(跳离正题:Jingle是一个支持语音和视频的XMPP扩展;WebRTC项目使用来自libjingle库,一个Jingle的C++实现,的网络和传输组件。)</li>
<li>开源的库,如<a href="http://zeromq.org/" target="_blank" rel="external">ZeroMQ</a>(TokBox在他们的<a href="http://www.tokbox.com/blog/tokbox-builds-it%E2%80%99s-own-internal-messaging-infrastructure/" target="_blank" rel="external">Rumor</a>服务中使用它)和OpenMQ。NullMQ通过WebSocket使用<a href="http://stomp.github.io/" target="_blank" rel="external">STOMP协议</a>将ZeroMQ概念应用于Web平台。</li>
<li>使用WebSocket的商业云消息平台(尽管可能会回退到长轮询),例如Pusher,Kaazing和PubNub。(PubNub也有WebRTC的API)</li>
<li>像<a href="https://vline.com/" target="_blank" rel="external">vLine</a>这样的商业WebRTC平台。</li>
</ul>
<h3 id="在Node上使用Socket-io构建信令服务"><a href="#在Node上使用Socket-io构建信令服务" class="headerlink" title="在Node上使用Socket.io构建信令服务"></a>在Node上使用Socket.io构建信令服务</h3><p>下面是一个简单的Web应用程序的代码,它使用Node上的Socket.io构建的信令服务。Socket.io的设计使构建服务、交换信息变得简单,而且因为它内置了“房间”的概念,Socket.io特别适用于WebRTC 信令。<em>这个例子不是为了扩大产品级别的信令服务而设计的,但是对于相对较少的用户来说效果很好</em>。</p>
<p>Socket.io使用带有回退的WebSocket:AJAX长轮询,AJAX多部分流,Forever Iframe和JSONP轮询。它已被移植到各种后端,但也许最有名的是它的Node版本,我们将在下面的例子中使用它。</p>
<p>在这个例子中没有WebRTC:它的设计目的只是为了展示如何在一个Web应用程序中构建信令。查看控制台日志以查看客户端加入房间并且交换消息时发生的情况。我们的<a href="https://codelabs.developers.google.com/codelabs/webrtc-web/#0" target="_blank" rel="external">WebRTC codelab</a>提供了分步说明,解释了如何将这个例子集成到一个完整的WebRTC视频聊天应用程序。</p>
<p>下面是客户端, index.html:</p>
<figure class="highlight html"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div></pre></td><td class="code"><pre><div class="line"><span class="meta"><!DOCTYPE html></span></div><div class="line"><span class="tag"><<span class="name">html</span>></span></div><div class="line"> <span class="tag"><<span class="name">head</span>></span></div><div class="line"> <span class="tag"><<span class="name">title</span>></span>WebRTC client<span class="tag"></<span class="name">title</span>></span></div><div class="line"> <span class="tag"></<span class="name">head</span>></span></div><div class="line"> <span class="tag"><<span class="name">body</span>></span></div><div class="line"> <span class="tag"><<span class="name">script</span> <span class="attr">src</span>=<span class="string">'/socket.io/socket.io.js'</span>></span><span class="undefined"></span><span class="tag"></<span class="name">script</span>></span></div><div class="line"> <span class="tag"><<span class="name">script</span> <span class="attr">src</span>=<span class="string">'js/main.js'</span>></span><span class="undefined"></span><span class="tag"></<span class="name">script</span>></span></div><div class="line"> <span class="tag"></<span class="name">body</span>></span></div><div class="line"><span class="tag"></<span class="name">html</span>></span></div></pre></td></tr></table></figure>
<p>以及客户端中引用的JavaScript文件main.js::</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div><div class="line">12</div><div class="line">13</div><div class="line">14</div><div class="line">15</div><div class="line">16</div><div class="line">17</div><div class="line">18</div><div class="line">19</div><div class="line">20</div><div class="line">21</div><div class="line">22</div><div class="line">23</div><div class="line">24</div><div class="line">25</div><div class="line">26</div><div class="line">27</div><div class="line">28</div></pre></td><td class="code"><pre><div class="line"><span class="keyword">const</span> isInitiator;</div><div class="line"></div><div class="line">room = prompt(<span class="string">'Enter room name:'</span>);</div><div class="line"></div><div class="line"><span class="keyword">const</span> socket = io.connect();</div><div class="line"></div><div class="line"><span class="keyword">if</span> (room !== <span class="string">''</span>) {</div><div class="line"> <span class="built_in">console</span>.log(<span class="string">'Joining room '</span> + room);</div><div class="line"> socket.emit(<span class="string">'create or join'</span>, room);</div><div class="line">}</div><div class="line"></div><div class="line">socket.on(<span class="string">'full'</span>, (room) => {</div><div class="line"> <span class="built_in">console</span>.log(<span class="string">'Room '</span> + room + <span class="string">' is full'</span>);</div><div class="line">});</div><div class="line"></div><div class="line">socket.on(<span class="string">'empty'</span>, (room) => {</div><div class="line"> isInitiator = <span class="literal">true</span>;</div><div class="line"> <span class="built_in">console</span>.log(<span class="string">'Room '</span> + room + <span class="string">' is empty'</span>);</div><div class="line">});</div><div class="line"></div><div class="line">socket.on(<span class="string">'join'</span>, (room) => {</div><div class="line"> <span class="built_in">console</span>.log(<span class="string">'Making request to join room '</span> + room);</div><div class="line"> <span class="built_in">console</span>.log(<span class="string">'You are the initiator!'</span>);</div><div class="line">});</div><div class="line"></div><div class="line">socket.on(<span class="string">'log'</span>, (array) => {</div><div class="line"> <span class="built_in">console</span>.log.apply(<span class="built_in">console</span>, array);</div><div class="line">});</div></pre></td></tr></table></figure>
<p>完整的服务端app:</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div><div class="line">12</div><div class="line">13</div><div class="line">14</div><div class="line">15</div><div class="line">16</div><div class="line">17</div><div class="line">18</div><div class="line">19</div><div class="line">20</div><div class="line">21</div><div class="line">22</div><div class="line">23</div><div class="line">24</div><div class="line">25</div><div class="line">26</div><div class="line">27</div><div class="line">28</div><div class="line">29</div><div class="line">30</div><div class="line">31</div><div class="line">32</div><div class="line">33</div><div class="line">34</div><div class="line">35</div><div class="line">36</div><div class="line">37</div><div class="line">38</div><div class="line">39</div><div class="line">40</div><div class="line">41</div><div class="line">42</div><div class="line">43</div><div class="line">44</div><div class="line">45</div><div class="line">46</div><div class="line">47</div><div class="line">48</div><div class="line">49</div><div class="line">50</div></pre></td><td class="code"><pre><div class="line"><span class="keyword">const</span> <span class="keyword">static</span> = <span class="built_in">require</span>(<span class="string">'node-static'</span>);</div><div class="line"><span class="keyword">const</span> http = <span class="built_in">require</span>(<span class="string">'http'</span>);</div><div class="line"><span class="keyword">const</span> file = <span class="keyword">new</span>(<span class="keyword">static</span>.Server)();</div><div class="line"><span class="keyword">const</span> app = http.createServer(<span class="function"><span class="keyword">function</span> (<span class="params">req, res</span>) </span>{</div><div class="line"> file.serve(req, res);</div><div class="line">}).listen(<span class="number">2013</span>);</div><div class="line"></div><div class="line"><span class="keyword">const</span> io = <span class="built_in">require</span>(<span class="string">'socket.io'</span>).listen(app);</div><div class="line"></div><div class="line">io.sockets.on(<span class="string">'connection'</span>, (socket) => {</div><div class="line"></div><div class="line"> <span class="comment">// convenience function to log server messages to the client</span></div><div class="line"> <span class="function"><span class="keyword">function</span> <span class="title">log</span>(<span class="params"></span>)</span>{</div><div class="line"> <span class="keyword">const</span> array = [<span class="string">'>>> Message from server: '</span>];</div><div class="line"> <span class="keyword">for</span> (<span class="keyword">const</span> i = <span class="number">0</span>; i < <span class="built_in">arguments</span>.length; i++) {</div><div class="line"> array.push(<span class="built_in">arguments</span>[i]);</div><div class="line"> }</div><div class="line"> socket.emit(<span class="string">'log'</span>, array);</div><div class="line"> }</div><div class="line"></div><div class="line"> socket.on(<span class="string">'message'</span>, (message) => {</div><div class="line"> log(<span class="string">'Got message:'</span>, message);</div><div class="line"> <span class="comment">// for a real app, would be room only (not broadcast)</span></div><div class="line"> socket.broadcast.emit(<span class="string">'message'</span>, message);</div><div class="line"> });</div><div class="line"></div><div class="line"> socket.on(<span class="string">'create or join'</span>, (room) => {</div><div class="line"> <span class="keyword">const</span> numClients = io.sockets.clients(room).length;</div><div class="line"></div><div class="line"> log(<span class="string">'Room '</span> + room + <span class="string">' has '</span> + numClients + <span class="string">' client(s)'</span>);</div><div class="line"> log(<span class="string">'Request to create or join room '</span> + room);</div><div class="line"></div><div class="line"> <span class="keyword">if</span> (numClients === <span class="number">0</span>){</div><div class="line"> socket.join(room);</div><div class="line"> socket.emit(<span class="string">'created'</span>, room);</div><div class="line"> } <span class="keyword">else</span> <span class="keyword">if</span> (numClients === <span class="number">1</span>) {</div><div class="line"> io.sockets.in(room).emit(<span class="string">'join'</span>, room);</div><div class="line"> socket.join(room);</div><div class="line"> socket.emit(<span class="string">'joined'</span>, room);</div><div class="line"> } <span class="keyword">else</span> { <span class="comment">// max two clients</span></div><div class="line"> socket.emit(<span class="string">'full'</span>, room);</div><div class="line"> }</div><div class="line"> socket.emit(<span class="string">'emit(): client '</span> + socket.id +</div><div class="line"> <span class="string">' joined room '</span> + room);</div><div class="line"> socket.broadcast.emit(<span class="string">'broadcast(): client '</span> + socket.id +</div><div class="line"> <span class="string">' joined room '</span> + room);</div><div class="line"></div><div class="line"> });</div><div class="line"></div><div class="line">});</div></pre></td></tr></table></figure>
<p>(你不需要去学习node-static;它只是碰巧在这个例子里用到)</p>
<p>在localhost上运行这个程序,你需要安装Node,socket.io和node-static。Node可以从nodejs.org下载。要安装socket.io和node-static,请从你的应用程序目录中的终端运行Node Package Manager:</p>
<figure class="highlight shell"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div></pre></td><td class="code"><pre><div class="line">npm install socket.io</div><div class="line">npm install node-static</div></pre></td></tr></table></figure>
<p>启动服务器,在应用目录下运行下面的命令:</p>
<figure class="highlight shell"><table><tr><td class="gutter"><pre><div class="line">1</div></pre></td><td class="code"><pre><div class="line">node server.js</div></pre></td></tr></table></figure>
<p>在浏览器中打开localhost:2013。在任何浏览器中打开新的标签页或窗口,然后再次打开localhost:2013。如果想要查看发生了什么,请查看控制台:在Chrome和Opera中,可以通过Command-Option-J或Ctrl-Shift-J通过DevTools访问。</p>
<p>不管你选择使用什么方式进行信号传输,后端和客户端app都至少需要提供类似于此示例的服务。</p>
<h3 id="信令陷阱"><a href="#信令陷阱" class="headerlink" title="信令陷阱"></a>信令陷阱</h3><ul>
<li><p>在setLocalDescription()被调用之前,RTCPeerConnection并不会开始收集候选:这是在<a href="http://tools.ietf.org/html/draft-ietf-rtcweb-jsep-03#section-4.2.4" target="_blank" rel="external">JSEP IETF草案</a>中规定的。</p>
</li>
<li><p>利用Trickle ICE(见上文):候选到达后立即调用addIceCandidate()。</p>
</li>
</ul>
<h3 id="现成的信令服务器"><a href="#现成的信令服务器" class="headerlink" title="现成的信令服务器"></a>现成的信令服务器</h3><p>如果你不想自己做WebRTC 信令服务器,有一些现成的服务器可用,它们可以使用上面例子中使用Socket.io,并与WebRTC客户端 JavaScript库集成:</p>
<ul>
<li>webRTC.io:WebRTC最先出现的几个抽象库之一。</li>
<li>easyRTC:完整的WebRTC包。</li>
<li>Signalmaster:一个与SimpleWebRTC JavaScript客户端库一起使用的信号服务器。</li>
</ul>
<p>如果你一点代码都不想写的话,还可以从像vLine,OpenTok和Asterisk等公司获得完整的商业WebRTC平台。</p>
<p>爱立信在WebRTC早期的时候建立了一个在Apache上使用PHP的信令服务器。虽说这现在已经过时了,但是如果你正在考虑类似的东西,那么还是值得看一下代码的。</p>
<h3 id="信令安全"><a href="#信令安全" class="headerlink" title="信令安全"></a>信令安全</h3><blockquote>
<p>安全是无所作为的艺术。</p>
<p>— Salman Rushdie</p>
</blockquote>
<p>加密对于所有WebRTC组件来说都是强制性的。</p>
<p>但是信令机制并不是由WebRTC标准定义的,所以保护信令安全的责任就全在你的身上了。如果攻击者设法劫持了信令,他们就可以停止会话,重新定向连接和记录,更改或注入其他内容。</p>
<p>确保信令安全的最重要因素是使用安全协议,即HTTPS和WSS(即TLS),确保消息不会因未加密而截获。另外要小心,不要以可以被其他呼叫方能够获取的方式用同一个信令服务器广播信令消息。</p>
<h3 id="在信令之后:使用-ICE来对付NAT和防火墙"><a href="#在信令之后:使用-ICE来对付NAT和防火墙" class="headerlink" title="在信令之后:使用 ICE来对付NAT和防火墙"></a>在信令之后:使用 ICE来对付NAT和防火墙</h3><p>对于元数据信令,WebRTC应用程序使用中介服务器,但对于实际的媒体和数据流,一旦建立对话的话,RTCPeerConnection就会尝试点对点地直接连接客户端。</p>
<p>在简单的情况中,每个WebRTC端点都有一个唯一的地址,可以与其他端进行交换以便直接通信。</p>
<p><img src="/img/without_nat.png" alt="world without nat"></p>
<p>实际上大多数设备都是处在一层或者多层NAT之后的,其中有一些包含可以阻挡某些端口和协议的防病毒软件,还有很多设备是在代理和公司防火墙之后的。防火墙和NAT实际上可以由相同的设备实现,比如说家庭WiFi路由器。</p>
<p><img src="/img/nat_real_world.png" alt="nat_real_world"></p>
<p>WebRTC应用程序可以使用ICE框架来消除实际网络的复杂性。为了实现这一点,你的应用程序必须将 ICE服务器的URL传递给RTCPeerConnection,就像下面所描述的那样。</p>
<p>ICE试图找到连接对方的最佳途径。它会并行地尝试所有可能性,并选择最有效的选项。 ICE首先尝试使用从设备操作系统和网卡获取的主机地址进行连接;如果不成功的话(对于NAT后面的设备就会失败), ICE会使用 STUN服务器获取外部地址,如果还是失败的话,则通过 TURN中继服务器路由数据。</p>
<p>换句话说:</p>
<ul>
<li><p>STUN服务器是用来获取外部地址的。</p>
</li>
<li><p>TURN服务器是用来在直接连接(点到点)失败的情况下进行中继数据流量的</p>
</li>
</ul>
<p>每个 TURN服务器都支持 STUN: TURN服务器也是一个增加了内置中继功能的 STUN服务器。 ICE还可以应付NAT设置的复杂性:实际上,NAT“打孔”可能不仅仅需要一个公共IP:端口地址。</p>
<p>STUN 和/或 TURN服务器的URL(可选择地)由iceServers配置对象中的WebRTC应用程序指定,该配置对象是RTCPeerConnection构造函数的第一个参数。对于<a href="http://appr.tc/" target="_blank" rel="external">appr.tc</a>来说,值看起来是这样的:</p>
<figure class="highlight"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div><div class="line">12</div><div class="line">13</div><div class="line">14</div><div class="line">15</div><div class="line">16</div><div class="line">17</div></pre></td><td class="code"><pre><div class="line">{</div><div class="line"> 'iceServers': [</div><div class="line"> {</div><div class="line"> 'urls': 'stun:stun.l.google.com:19302'</div><div class="line"> },</div><div class="line"> {</div><div class="line"> 'urls': 'turn:192.158.29.39:3478?transport=udp',</div><div class="line"> 'credential': 'JZEOEt2V3Qb0y27GRntt2u2PAYA=',</div><div class="line"> 'username': '28224511:1379330808'</div><div class="line"> },</div><div class="line"> {</div><div class="line"> 'urls': 'turn:192.158.29.39:3478?transport=tcp',</div><div class="line"> 'credential': 'JZEOEt2V3Qb0y27GRntt2u2PAYA=',</div><div class="line"> 'username': '28224511:1379330808'</div><div class="line"> }</div><div class="line"> ]</div><div class="line">}</div></pre></td></tr></table></figure>
<p>注意:上面显示的 TURN 证书是有时间限制的,在2013年9月到期。 TURN服务器运行起来很昂贵,你需要为自己的服务器付费或者找一个服务提供商。要测试证书,你可以使用候选收集样本,并检查是否获得了类型为中继的候选。</p>
<p>一旦RTCPeerConnection具有该信息, ICE的作用就会自动发生:RTCPeerConnection使用 ICE框架 来计算到对端之间的最佳路径,并根据需要使用 STUN和 TURN服务器。</p>
<h3 id="STUN"><a href="#STUN" class="headerlink" title="STUN"></a>STUN</h3><p>NAT给设备提供了一个IP地址以使用专用局域网,但是这个地址不能在外部使用。由于没有公用地址,WebRTC端对端就无法进行通信。而WebRTC使用STUN来解决这个问题。</p>
<p>STUN服务器位于公共网络上,并且有一个简单的任务:检查传入请求的IP地址(来自运行在NAT后面的应用程序),并将该地址作为响应发送回去。换句话说,应用程序使用 STUN服务器从公共角度发现其IP:端口。这个过程使得WebRTC一端为自己获得一个可公开访问的地址,然后通过信令机制将其传递给另一端以建立直接连接。(实际上不同NAT工作方式都有所不同,可能有多个NAT层,但是原理是一样的)。</p>
<p>因为 STUN服务器不需要做太多的工作或者记特别多的东西,所以相对低规格的 STUN服务器就可以处理大量的请求。</p>
<p>根据<a href="http://webrtcstats.com/" target="_blank" rel="external">webrtcstats.com</a>的统计(2013年),大多数WebRTC通话都成功地使用 STUN进行连接,有86%。尽管对于防火墙之后的两端之间的呼叫以及复杂的NAT配置,成功通话量会更少一些。</p>
<p><img src="/img/stun.png" alt="stun"></p>
<h3 id="TURN"><a href="#TURN" class="headerlink" title="TURN"></a>TURN</h3><p>RTCPeerConnection尝试通过UDP建立对等端之间的直接通信。如果失败的话,RTCPeerConnection就会使用TCP进行连接。如果使用TCP还失败的话,可以用 TURN服务器作为后备,在终端之间转发数据。</p>
<p>重申: TURN用于中继对等端之间的音频/视频/数据流,而不是信令数据。</p>
<p>TURN服务器具有公共地址,因此即使对等端位于防火墙或代理之后也可以与其他人联系。 TURN服务器有一个概念上来讲简单的任务—中继数据流—但是与 STUN服务器不同的是,他们会消耗大量的带宽。换句话说, TURN服务器需要更加的强大。</p>
<p><img src="/img/turn.png" alt="turn"></p>
<p>上图显示了 TURN的作用:单纯的 STUN没有成功建立连接,所以每个对等端还需要使用 TURN服务器。</p>
<h3 id="部署-STUN和-TURN服务器"><a href="#部署-STUN和-TURN服务器" class="headerlink" title="部署 STUN和 TURN服务器"></a>部署 STUN和 TURN服务器</h3><p>为了进行测试,Google运行了一个公共 STUN服务器 stun.l.google.com:19302,就是<a href="http://appr.tc/" target="_blank" rel="external">appr.tc</a>所使用的那样。对于产品的 STUN/ TURN服务,我们推荐使用rfc5766-turn-server; STUN和 TURN服务器的源代码可从<a href="https://code.google.com/p/rfc5766-turn-server/" target="_blank" rel="external">code.google.com/p/rfc5766-turn-server</a>获得,该代码还提供了有关服务器安装的多个信息源的链接。<a href="https://groups.google.com/forum/#!msg/discuss-webrtc/X-OeIUC0efs/XW5Wf7Tt1vMJ" target="_blank" rel="external">Amazon Web Services的VM映像</a>也可用。</p>
<p>另一个 TURN服务器是restund,提供<a href="http://www.creytiv.com/restund.html" target="_blank" rel="external">源代码</a>,也有AWS服务。以下是如何在Google Compute Engine上设置restund的说明。</p>
<ol>
<li>根据需要打开防火墙,对于tcp = 443,udp/tcp = 3478</li>
<li>创建四个实例,每个公共IP标准一个,Standard Ubuntu 12.06映像</li>
<li>设置本地防火墙配置</li>
<li><p>安装工具</p>
<figure class="highlight shell"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div></pre></td><td class="code"><pre><div class="line">sudo apt-get install make</div><div class="line">sudo apt-get install gcc</div></pre></td></tr></table></figure>
</li>
<li><p>从creytiv.com/re.html安装libre</p>
</li>
<li>从creytiv.com/restund.html获取并解压缩restund</li>
<li>wget hancke.name/restund-auth.patch并且使用patch – p1</li>
<li>对libre和restund运行make, sudo make install</li>
<li>根据你的需要(替换IP地址并确保它包含相同的共享密钥)对restund.conf进行调整,并复制到/etc</li>
<li>复制restund/etc/restund到/etc/init.d/</li>
<li>配置restund:<ul>
<li>设置LD_LIBRARY_PATH</li>
<li>复制restund.conf到/etc/restund.conf</li>
<li>设置restund.conf以使用正确的 10. IP地址</li>
</ul>
</li>
<li>运行restund</li>
<li>从远端机上使用社团的客户端进行测试:./client IP:port</li>
</ol>
<h3 id="多方WebRTC"><a href="#多方WebRTC" class="headerlink" title="多方WebRTC"></a>多方WebRTC</h3><p>你可能还想查看一下Justin Uberti提出的用于访问TURN服务的<a href="http://tools.ietf.org/html/draft-uberti-rtcweb-turn-rest-00" target="_blank" rel="external">REST API的IETF标准</a>。</p>
<p>很容易想象媒体流的使用情况超出了简单的一对一呼叫:例如,一组同事之间的视频会议,或一个发言者和数百(数百万)个观众的公共事件。</p>
<p>WebRTC应用程序可以使用多个RTCPeerConnection,以便每个端点都可以连接到网格配置中的每个其他端点。这是<a href="http://talky.io/" target="_blank" rel="external">talky.io</a>等应用程序所采取的方法,对于只有少数几个对等端的情况来说可以很好的工作。除此之外,处理和带宽会过度消耗,对于移动客户端来说尤其是这样。</p>
<p><img src="/img/mesh_topo.png" alt="mesh_topo"></p>
<p>或者,WebRTC应用程序可以选择一个端点以星形配置将流分配给所有其他端点。也可以在服务器上运行WebRTC端点并构建自己的重新分配机制。(<a href="https://code.google.com/p/webrtc/source/browse/#svn%2Ftrunk%2Ftalk" target="_blank" rel="external">webrtc.org</a>提供了一个客户端应用示例)</p>
<p>从Chrome 31和Opera 18开始,来自一个RTCPeerConnection的MediaStream可以用作另一个的输入:在<a href="http://simpl.info/rtcpeerconnection/multi" target="_blank" rel="external">simpl.info/multi</a>上有一个演示。这可以启用更灵活的体系结构,因为它使Web应用程序能够通过选择要连接的其他对等端来处理呼叫路由。</p>
<h3 id="多点控制单元"><a href="#多点控制单元" class="headerlink" title="多点控制单元"></a>多点控制单元</h3><p>大量endpoint情况的更好选择是使用多点控制单元(Multipoint Control Unit,MCU)。它是一个服务器,可以作为在大量参与者之间分发媒体的桥。MCU可以处理视频会议中的不同分辨率,编解码器和帧速率,处理转码,选择性流转发,混音或录制音频和视频。对于多方通话,需要考虑许多问题:特别是如何显示多个视频输入并混合来自多个来源的音频。</p>
<p>你可以购买一个完整的MCU硬件包,或者建立自己的MCU。</p>
<p><img src="/img/mcu.jpg" alt="mcu"></p>
<p>有几个开源的MCU软件可供选择。比如说,<a href="http://lynckia.com/" target="_blank" rel="external">Licode</a>为WebRTC做了一个开源MCU;OpenTok也有Mantis。<br>Several open source MCU software options are available. For example, Licode (previously know as Lynckia) produces an open source MCU for WebRTC; OpenTok has <a href="http://www.tokbox.com/blog/mantis-next-generation-cloud-technology-for-webrtc/" target="_blank" rel="external">Mantis</a>.</p>
<h3 id="除了浏览器以外还有:VoIP,电话和消息"><a href="#除了浏览器以外还有:VoIP,电话和消息" class="headerlink" title="除了浏览器以外还有:VoIP,电话和消息"></a>除了浏览器以外还有:VoIP,电话和消息</h3><p>WebRTC的标准化特性使得在浏览器中运行的WebRTC应用程序与另一个通信平台运行的设备或停牌(例如电话或视频会议系统)之间建立通信成为可能。</p>
<p>SIP是VoIP和视频会议系统使用的信令协议。为了实现WebRTC应用程序与视频会议系统等SIP客户端之间的通信,WebRTC需要代理服务器来调解信令。信令必须通过网关流动,但一旦通信建立,SRTP流量(视频和音频)就可以直接流向对等端。</p>
<p>公共交换电话网(PSTN)是所有“普通老式”模拟电话的电路交换网络。对于WebRTC应用程序和电话之间的通话,通信必须通过PSTN网关。同样,WebRTC应用程序需要中间的XMPP服务器来与Jingle端点(如IM客户端)进行通信。Jingle由Google开发,作为XMPP的扩展,为语音和视频提供消息传递服务:当前的WebRTC实现是基于C++ libjingle库的,这是一个最初为Google Talk开发的Jingle实现。</p>
<p>许多应用程序,库,和平台利用WebRTC与外部世界的沟通能力:sipML5,jsSIP,Phono,Zingaya,Twilio和Uberconference等等。</p>
<p>sipML5开发者也构建了webrtc2sip网关。Tethr和Tropo展示了一个在灾难通信框架,使用OpenBTS单元通过WebRTC实现手机和计算机之间的通信。这是一个没有运营商在中间的电话通信!</p>
</div>
<footer class="post-footer">
<div class="post-eof"></div>
</footer>
</div>
</article>
<article class="post post-type-normal" itemscope itemtype="http://schema.org/Article">
<div class="post-block">
<link itemprop="mainEntityOfPage" href="http://yoursite.com/2018/07/19/使用一个新的hash一致性算法提升负载均衡/">
<span hidden itemprop="author" itemscope itemtype="http://schema.org/Person">
<meta itemprop="name" content="You Wangqiu">
<meta itemprop="description" content="">
<meta itemprop="image" content="/img/lufei.jpeg">
</span>
<span hidden itemprop="publisher" itemscope itemtype="http://schema.org/Organization">
<meta itemprop="name" content="Youmai の Blog">
</span>
<header class="post-header">
<h1 class="post-title" itemprop="name headline">
<a class="post-title-link" href="/2018/07/19/使用一个新的hash一致性算法提升负载均衡/" itemprop="url">[翻译]使用一个新的hash一致性算法提升负载均衡</a></h1>
<div class="post-meta">
<span class="post-time">
<span class="post-meta-item-icon">
<i class="fa fa-calendar-o"></i>
</span>
<span class="post-meta-item-text">发表于</span>
<time title="创建于" itemprop="dateCreated datePublished" datetime="2018-07-19T21:29:01+08:00">
2018-07-19
</time>
</span>
<span class="post-category" >
<span class="post-meta-divider">|</span>
<span class="post-meta-item-icon">
<i class="fa fa-folder-o"></i>
</span>
<span class="post-meta-item-text">分类于</span>
<span itemprop="about" itemscope itemtype="http://schema.org/Thing">
<a href="/categories/算法/" itemprop="url" rel="index">
<span itemprop="name">算法</span>
</a>
</span>
</span>
</div>
</header>
<div class="post-body" itemprop="articleBody">
<p><a href="https://medium.com/vimeo-engineering-blog/improving-load-balancing-with-a-new-consistent-hashing-algorithm-9f1bd75709ed" target="_blank" rel="external">原文: Improving load balancing with a new consistent-hashing algorithm</a></p>
<p>我们在云上运行Vimeo的动态视频打包器<code>Skyfire</code>,每天服务近<strong>10亿</strong>个DASH和HLS请求。 这个请求量是非常大的! 我们对它的表现非常满意,但将其扩展到适应今天的流量以及更高的流量是一个有趣的挑战。 今天我想谈谈一个新的算法,有限负载一致性哈希(bounded-load consistent hashing),以及它是如何消除我们视频传输的瓶颈的。</p>
<h3 id="动态打包"><a href="#动态打包" class="headerlink" title="动态打包"></a>动态打包</h3><p>Vimeo的视频文件存储为MP4文件,与浏览器中用于下载或“渐进式”播放的格式相同。但是,DASH和HLS不使用单个文件 - 它们使用单独的视频短片段。当播放器请求某个片段时,Skyfire会动态处理该请求。它仅获取MP4文件的必要部分,针对DASH或HLS格式进行一些调整,并将结果发送回用户。 </p>
<p>但是,当播放器请求(例如,文件的第37段)时,Skyfire如何知道需要获取哪些字节?它需要查看一个索引,该索引知道所有关键帧的位置以及文件中的所有数据包。在索引可以被查询之前,你需要生成它。这需要至少一个HTTP请求和一点CPU时间 - 或者,对于很长的视频,需要大量的CPU时间。由于我们对同一视频文件的请求很多,因此缓存索引并在以后重复使用是有意义的。 </p>
<p>当我们第一次在现实世界中开始测试Skyfire时,我们采用了一种简单的缓存方法:我们将索引缓存在生成它们的云服务器的内存中,并在HAProxy中使用一致性哈希将相同视频文件的请求发送到相同的云服务器。这样,我们可以重用缓存的数据。</p>
<h3 id="理解一致性哈希"><a href="#理解一致性哈希" class="headerlink" title="理解一致性哈希"></a>理解一致性哈希</h3><p>在继续前进之前,让我们深入研究一下一致性哈希,这是一种在多个服务器之间分配负载的技术。如果你已经熟悉一致性哈希,请随时跳转到下一部分。</p>
<p>要使用一致性哈希在服务器之间分发请求,HAProxy会获取<em>部分请求</em>的哈希值(我们使用的是包含视频ID的URL的一部分),并使用该哈希值来选择可用的后端服务器。使用传统的“模状哈希”,您只需将请求哈希值视为一个非常大的数字。如果以可用服务器数量为模数,则获取的是要使用的服务器的索引。这很简单,只要服务器列表稳定,它就能很好地工作。但是,当添加或删除服务器时,会出现问题:大多数请求将散列到与之前不同的服务器。如果你有九台服务器并且添加了十分之一,那么只有十分之一的请求会(通过运气)散列到与之前相同的服务器。</p>
<p>所以有了一致性哈希。一致性哈希使用更复杂的方案,其中每个服务器根据其名称或ID分配多个哈希值,并且每个请求都根据“最近”哈希值分配给服务器。这种增加的复杂性的好处是,当添加或删除服务器时,大多数请求将映射到他们之前执行的相同服务器。因此,如果您有九台服务器并添加十分之一,则大约1/10的请求将落在新添加的服务器哈希值附近,而另外9/10将落到与之前相同的最近服务器。好多了!因此,一致性哈希使我们可以添加和删除服务器,而不会完全干扰每个服务器所拥有的缓存。当这些服务器在云中运行时,这是一个非常重要的属性。</p>
<h3 id="一致性哈希-不理想的负载均衡"><a href="#一致性哈希-不理想的负载均衡" class="headerlink" title="一致性哈希 - 不理想的负载均衡"></a>一致性哈希 - 不理想的负载均衡</h3><p>但是,一致性哈希有其自身的问题:请求分布不均匀。由于其数学属性,当请求的分布均匀时,一致性哈希仅平衡负载以及为每个请求随机选择服务器。但是,如果某些内容比其他内容(互联网中很常见)更受欢迎,那可能会很糟糕。一致性哈希会将该流行内容的所有请求发送到相同的服务器,比其他服务器接收更多流量。这可能导致服务器过载,视频播放效果不佳以及用户不满意。</p>
<p>到2015年11月,由于Vimeo已经准备好向一群精心挑选的成员推出Skyfire,我们认为这个超载问题太严重,不容忽视,所以我们改变了缓存使用方法。我们在HAProxy中使用了“least connections”的负载均衡策略,而不是基于一致性哈希的均衡,因此负载将在服务器之间均匀分配。我们使用memcache添加了一个二级缓存,在服务器之间共享,这样一个服务器生成的索引可以被另一个服务器检索。共享缓存需要一些额外的带宽,但负载在服务器之间更均衡地平衡。这就是我们第二年愉快运行的方式。</p>
<h3 id="两者都有不是更好吗?"><a href="#两者都有不是更好吗?" class="headerlink" title="两者都有不是更好吗?"></a>两者都有不是更好吗?</h3><p>为什么没有办法说“使用一致性哈希,但请不要超载任何服务器”?早在2015年8月,我就试图提出一种算法,该算法基于两个随机选择的功能,这样做可以做到这一点,但是一些模拟表明它不起作用。向非理想服务器发送了太多请求。我很失望,但是我们没有浪费时间去拯救它,而是采用了上面最少的连接和共享缓存方法。</p>
<p>快进到2016年8月。我注意到不可估量的Damian Gryski推文中的一个URL,这是一篇名为Consistent Hashing with Bounded Loads的arXiv论文。我阅读了摘要,它似乎正是我想要的:一种算法,它将一致性哈希与任何一台服务器负载的上限相结合,相对于整个池的平均负载。我读了这篇论文,算法非常简单。实际上,该论文表示:</p>
<blockquote>
<p>虽然一致性哈希搭配转发来满足容量限制的想法似乎非常明显,但似乎以前没有被考虑过。</p>
</blockquote>
<h3 id="有界负载算法(The-bounded-load-algorithm)"><a href="#有界负载算法(The-bounded-load-algorithm)" class="headerlink" title="有界负载算法(The bounded-load algorithm)"></a>有界负载算法(The bounded-load algorithm)</h3><p>这是算法的简化草图。遗漏了一些细节,如果你打算自己实现它,你一定要去原始论文获取信息。</p>
<p>首先,定义一个大于1的平衡因子c。c控制服务器之间允许的不平衡程度。例如,如果c = 1.25,则服务器不应超过平均负载的125%。在c增加到∞的极限中,算法变得等效于普通一致性哈希,没有平衡;当c减小到接近1时,它变得更像是最少连接策略,并且哈希变得不那么重要。根据我的经验,1.25和2之间的值更适用于实际场景。</p>
<p>当请求到达时,计算平均负载(未完成请求的数量,m,包括刚刚到达的请求数除以可用服务器数n)。将平均负载乘以c得到“目标负载”,t。在原始论文中,将容量分配给服务器,以便每个服务器的容量为⌊t⌋或⌈t⌉,总容量为⌈cm⌉。因此,服务器的最大容量为⌈cm/n⌉,大于平均负载的c倍,小于1个请求。为了支持给服务器不同的“权重”,正如HAProxy所做的那样,算法必须略有改变,但精神是相同的 - 没有服务器可以超过负载份额1个请求。</p>
<p>分发一个请求时,像往常一样计算其哈希值和最近的服务器。如果该服务器负载低于其容量,则将请求分配给该服务器。否则,转到哈希环中的下一个服务器并检查其负载,继续,直到找到有剩余容量的服务器。肯定有一个服务器符合条件,因为最高容量高于平均负载,并且每个服务器的负载不可能高于平均值。这保证了一些不错的东西:</p>
<ol>
<li>不允许服务器负载超过(平均负载 * c 加 1) 个请求。</li>
<li>只要服务器未过载,请求的分配策略与一致性哈希相同。</li>
<li>如果服务器过载,则所选择的回退服务器列表对于相同的请求哈希将是相同的 - 即,相同的服务器将始终是流行的内容的“第二选择”。这对缓存很有用。</li>
<li>如果服务器过载,则回退服务器列表对于不同的请求哈希值通常会有所不同 - 即,过载服务器的溢出负载将在可用服务器之间分配,而不是全部落到某个服务器上。这取决于每个服务器在一致性哈希环中分配多少个点。</li>
</ol>
<h3 id="实际应用的结果"><a href="#实际应用的结果" class="headerlink" title="实际应用的结果"></a>实际应用的结果</h3><p>在模拟器中测试算法并获得比我的简单算法更积极的结果后,我开始搞清楚如何将其hack到HAProxy。向HAProxy添加代码并不算太糟糕。HAProxy代码非常干净,组织良好,经过几天的工作,我得到了一些运行良好的程序,我可以通过它重放一些流量,观察算法的作用。它奏效了!数学证明和模拟是很好的,但是直到你看到真正的流量击中正确的服务器才能真正相信。</p>
<p>有了这个成功,我在9月份向HAProxy发送了一个概念验证补丁。 HAProxy维护者Willy Tarreau(非常高兴能与之合作),认识到算法的价值。他并没有告诉我我的补丁有多糟糕。他做了彻底的代码审查,并提供了一些非常有价值的反馈。我花了一点时间来处理这些建议并把事情搞定了,几周之后我就有了一个精美的版本准备发送到列表中。还有一些小的调整,它在10月26日发布的HAProxy 1.7.0-dev5及时被接受。11月25日,HAProxy 1.7.0被指定为稳定版本,因此现在普遍可以使用有限负载一致性哈希。</p>
<p>但我确定你想知道的是,我们从这一切中获得了什么?</p>
<p>这是更改HAProxy配置之前和之后的缓存行为图。</p>
<p><img src="/img/cache_hit.png" alt="haproxy conf"></p>
<p>每日变化是由弹性伸缩引起的:在白天,流量会增加,因此我们启动更多服务器来处理它,本地缓存只能解决更少的请求。在晚上,流量较少,因此关闭服务器,本地缓存性能有所提升。切换到有界负载算法后,无论运行多少台服务器,都会有更大比例的请求到达本地缓存。</p>
<p>下面是共享缓存带宽在同一时间的图表:在更改之前,每个memcache服务器在高峰时段的出站带宽达到400到500 Mbit / s(总共大约8Gbit / s)。改进算法之后,变化较小,服务器带宽保持在100 Mbit / s以下。</p>
<p><img src="/img/bandwidth.png" alt="bandwidth"></p>
<p>从响应时间的角度来看,我们没有画出性能提升的图。为什么?因为他们保持几乎完全相同。最少连接策略在保持服务器不会过载方面做得很好,从memcache中获取内容的速度足够快,以至于它对响应时间没有可测量的影响。但是现在更少部分的请求会依赖于共享缓存,并且由于该部分不依赖于我们运行的服务器数量,因此我们可以期待在不使memcached服务器饱和的情况下处理更多流量。此外,如果memcache服务器出现故障,它对Skyfire的整体影响将会大大降低。</p>
<p>总而言之,很高兴看到一点算法工作将单点问题变得更好。</p>
</div>
<footer class="post-footer">
<div class="post-eof"></div>
</footer>
</div>
</article>
<article class="post post-type-normal" itemscope itemtype="http://schema.org/Article">
<div class="post-block">
<link itemprop="mainEntityOfPage" href="http://yoursite.com/2018/05/15/翻译-不使用第三方库/">
<span hidden itemprop="author" itemscope itemtype="http://schema.org/Person">
<meta itemprop="name" content="You Wangqiu">
<meta itemprop="description" content="">
<meta itemprop="image" content="/img/lufei.jpeg">
</span>
<span hidden itemprop="publisher" itemscope itemtype="http://schema.org/Organization">
<meta itemprop="name" content="Youmai の Blog">
</span>
<header class="post-header">
<h1 class="post-title" itemprop="name headline">
<a class="post-title-link" href="/2018/05/15/翻译-不使用第三方库/" itemprop="url">[翻译]不使用第三方库</a></h1>
<div class="post-meta">
<span class="post-time">
<span class="post-meta-item-icon">
<i class="fa fa-calendar-o"></i>
</span>
<span class="post-meta-item-text">发表于</span>
<time title="创建于" itemprop="dateCreated datePublished" datetime="2018-05-15T22:04:24+08:00">
2018-05-15
</time>
</span>
<span class="post-category" >
<span class="post-meta-divider">|</span>
<span class="post-meta-item-icon">