-
Notifications
You must be signed in to change notification settings - Fork 0
/
search.xml
498 lines (460 loc) · 119 KB
/
search.xml
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
<?xml version="1.0" encoding="utf-8"?>
<search>
<entry>
<title>浅谈Python函数、方法与描述符</title>
<url>/2020/03/20/python-descriptor/</url>
<content><![CDATA[<p>这两天在群里被科普了一个知识点,感觉很有意思,简单做个分享。</p>
<p>一直以来我都以为,实例方法隐式传入self的行为是解释器级别的特殊设计,当我们调用实例方法的时候,解释器会自动传入实例对象本身,很多文章和教程也是这么写的。虽然从结果上来讲,这个说法没有太大问题,但是严格意义上来讲,解释器并没有真正执行传入<code>self</code>这个动作。隐式传入<code>self</code>这个行为,仅仅是基于描述符(descriptor)的一个通用设计而已。</p>
<a id="more"></a>
<p>我们在定义方法和函数的时候,实际上声明的都是function类的实例。而function类是实现了<code>__get__</code>方法的,所以它们俩其实都是描述符,在我们真正获取到方法之前(也就是拿到<code>__get__</code>返回给我们的method对象前),方法和函数没什么本质区别。</p>
<p>但是显然方法的行为和函数并不一样,被实例调用的时候会被隐式地自动传入self参数(这里暂且不提classmethod、staticmethod这些)。如果这不是解释器干的,这个行为是如何被实现的?</p>
<p>首先简单介绍下描述符的定义和作用。</p>
<p>一个描述符就是一个实现了三个核心的属性访问操作(get, set, delete)的类, 分别为 <code>__get__()</code> 、<code>__set__()</code> 和 <code>__delete__()</code> 这三个特殊的方法。</p>
<p>举个例子:</p>
<figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Bar</span>:</span></span><br><span class="line"> <span class="function"><span class="keyword">def</span> <span class="title">__init__</span><span class="params">(self, value)</span>:</span></span><br><span class="line"> self.value = value</span><br><span class="line"> <span class="function"><span class="keyword">def</span> <span class="title">__get__</span><span class="params">(self, instance, cls)</span>:</span></span><br><span class="line"> print(<span class="string">f'传入实例:<span class="subst">{instance}</span> 传入类:<span class="subst">{cls}</span>'</span>)</span><br><span class="line"> <span class="keyword">if</span> instance <span class="keyword">is</span> <span class="keyword">not</span> <span class="literal">None</span>:</span><br><span class="line"> print(<span class="string">'作为实例属性调用'</span>)</span><br><span class="line"> <span class="keyword">return</span> self.value</span><br><span class="line"> <span class="keyword">else</span>:</span><br><span class="line"> print(<span class="string">'作为类属性调用'</span>)</span><br><span class="line"> <span class="keyword">return</span> <span class="number">0</span></span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Foo</span>:</span></span><br><span class="line"> bar = Bar(<span class="number">42</span>)</span><br><span class="line"></span><br><span class="line">Foo().bar <span class="comment"># 作为实例属性调用,返回self.value,即42</span></span><br><span class="line">Foo.bar <span class="comment"># 作为类属性调用,instance为None,返回0</span></span><br></pre></td></tr></table></figure>
<p>这个例子中<code>Bar</code>就是一个描述符,如果一个描述符被当做一个类属性来访问,那么 <code>instance</code> 参数被设置成None,作为实例属性调用的话,instance就会是那个实例本身。在<code>__get__</code>方法中就可以根据这个来判断调用形式,并决定要返回什么,比如上面的例子中,在作为类属性调用的时候会直接返回0。</p>
<p>明白描述符的大致工作原理后,我们回到原先的话题上,函数和方法实际上都是<code>function</code>类实例。而方法自动传入<code>self</code>参数的行为,正是借助<code>function</code>类下的<code>__get__</code>方法来实现的。</p>
<p>当解释器在获取类下面的一个方法(实质上是个函数)时,会触发描述符协议,先调用<code>function</code>类的<code>__get__</code>方法,<code>__get__</code>内部对函数进行了处理,会根据<code>instance</code>参数进行判断,如果不是None,也就是作为实例属性被获取,就会将接收到的<code>instance</code>绑定到函数的首位参数上(效果类似于<code>functools</code>里的<code>partial</code>),然后再将处理过的函数返回。所以,我们拿到的方法其实就是首位参数被绑定了实例变量的普通函数。而我们对它进行调用的时候,所谓的隐式传入<code>self</code>这个动作从来就没有发生过,因为它老早就被内定了。</p>
<p>为了加深理解,可以再来看一个简单的例子:</p>
<figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">plus</span><span class="params">(self, b)</span>:</span></span><br><span class="line"> <span class="keyword">return</span> self + b</span><br><span class="line">print(type(plus)) <span class="comment"># <class 'function'></span></span><br><span class="line"></span><br><span class="line">plus_two = plus.__get__(<span class="number">2</span>)</span><br><span class="line">plus_two(<span class="number">3</span>) <span class="comment"># 返回 5</span></span><br></pre></td></tr></table></figure>
<p>我们可以看到由<code>__get__</code>返回的新函数,参数<code>self</code>已经被固定为了2,再调用的时候只需要传入参数<code>b</code>就行了。</p>
<p>基于这个设计,我们可以通过描述符来很方便地自定义解释器调用方法的行为,例如@classmethod、@staticmethod、@property这些装饰器实际上就是用描述符实现的。</p>
<p>我们可以尝试实现一个简化版的classmethod,这里假定你已经知道Python的@语法糖原理了。</p>
<figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="keyword">from</span> functools <span class="keyword">import</span> partial</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">myclassmethod</span>:</span></span><br><span class="line"> <span class="function"><span class="keyword">def</span> <span class="title">__init__</span><span class="params">(self, func)</span>:</span></span><br><span class="line"> self.func = func</span><br><span class="line"> <span class="function"><span class="keyword">def</span> <span class="title">__get__</span><span class="params">(self, instance, cls=None)</span>:</span></span><br><span class="line"> <span class="keyword">return</span> partial(self.func, cls)</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Foo</span>:</span></span><br><span class="line"><span class="meta"> @myclassmethod</span></span><br><span class="line"> <span class="function"><span class="keyword">def</span> <span class="title">bar</span><span class="params">(cls)</span>:</span></span><br><span class="line"> print(cls)</span><br><span class="line"></span><br><span class="line">Foo.bar() <span class="comment"># 输出:<class '__main__.Foo'></span></span><br></pre></td></tr></table></figure>
<p>当我们尝试执行上面这段代码的时候,解释器做了什么呢</p>
<ol>
<li>首先是<code>myclassmethod</code>、<code>Foo</code>两个类的声明,<code>Foo</code>类的<code>bar</code>属性会被替换为<code>myclassmethod</code>实例</li>
<li>执行<code>Foo.bar()</code>,解释器尝试获取bar属性的时候,发现bar属性是个描述符,于是调用它的<code>__get__</code>方法</li>
<li>在<code>__get__</code>方法中,我们通过<code>partial</code>把第一个参数固定为<code>cls</code>,返回一个新的函数。</li>
<li>拿到新返回的函数作为<code>bar</code>属性,然后调用它,结束。实际上</li>
</ol>
<p>实际上这里如果是执行<code>Foo().bar()</code>,输出结果也是一样的,因为在<code>myclassmethod</code>的<code>__get__</code>方法中,并没有对传入的<code>instance</code>做判断,都是直接返回<code>partial(self.func, cls)</code>,所以<code>bar</code>无论作为类属性还是实例属性获取都不会有什么区别。</p>
<p>最后,个人觉得Python这个设计的精妙之处就在于,解释器自始自终都是按照事先设计好的一套通用协议在运行的,只要你提供了相应的接口,那么就把相应的逻辑托管给你,提供了高度的可扩展性,可谓是把鸭子类型与面向接口编程的思想发挥得淋漓尽致。</p>
]]></content>
<categories>
<category>技术</category>
</categories>
<tags>
<tag>Python</tag>
</tags>
</entry>
<entry>
<title>Python标准库lru_cache源码解读与改造</title>
<url>/2020/03/13/lru-cache/</url>
<content><![CDATA[<p>之前写一个小项目需要用到过期缓存功能,因为想尽量轻量一些,只在内存中进行缓存,不打算走IO。虽说Python官方的lru_cache很好用,但是偏偏又不提供过期功能。简单搜了下,发现有人提供了<a href="https://gist.github.com/Morreski/c1d08a3afa4040815eafd3891e16b945" target="_blank" rel="noopener">一个附带过期功能的版本</a>。<a id="more"></a>代码如下所示:</p>
<figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="keyword">from</span> datetime <span class="keyword">import</span> datetime, timedelta</span><br><span class="line"><span class="keyword">import</span> functools</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">timed_cache</span><span class="params">(**timedelta_kwargs)</span>:</span> </span><br><span class="line"> </span><br><span class="line"> <span class="function"><span class="keyword">def</span> <span class="title">_wrapper</span><span class="params">(f)</span>:</span> </span><br><span class="line"> update_delta = timedelta(**timedelta_kwargs) </span><br><span class="line"> next_update = datetime.utcnow() + update_delta </span><br><span class="line"> <span class="comment"># Apply @lru_cache to f with no cache size limit </span></span><br><span class="line"> f = functools.lru_cache(<span class="literal">None</span>)(f) </span><br><span class="line"> </span><br><span class="line"><span class="meta"> @functools.wraps(f) </span></span><br><span class="line"> <span class="function"><span class="keyword">def</span> <span class="title">_wrapped</span><span class="params">(*args, **kwargs)</span>:</span> </span><br><span class="line"> <span class="keyword">nonlocal</span> next_update </span><br><span class="line"> now = datetime.utcnow() </span><br><span class="line"> <span class="keyword">if</span> now >= next_update: </span><br><span class="line"> f.cache_clear() </span><br><span class="line"> next_update = now + update_delta </span><br><span class="line"> <span class="keyword">return</span> f(*args, **kwargs) </span><br><span class="line"> <span class="keyword">return</span> _wrapped </span><br><span class="line"> <span class="keyword">return</span> _wrapper</span><br></pre></td></tr></table></figure>
<p>看了下他的代码,实现方式很简单,就是在官方的lru_cache外面又包了一层,然后自己管理缓存的过期期限。但细看之后发现这个装饰器有个很大的问题,它会在过期的时候把所有缓存的函数返回结果清空掉,哪怕B比A要晚缓存了一个小时,只要A一过期,就会把其他的缓存一并清空掉。这显然不合理也不好用,理想情况应该是每个参数组合都有自己的过期期限,互相独立,互不干扰。</p>
<p>按理说这个逻辑并不复杂,只要在增加/获取缓存的同时,加上过期时间的存储和判断即可,原本我准备上面的样例那样给官方的装饰器再包装一层,但很快发现做不到这一点,因为官方的<code>lru_cache</code>只对外暴露了两个操作缓存的接口,分别是<code>func.cache_info()</code>和<code>func.cache_clear()</code>,查看缓存命中统计和清空缓存,没办法对cache字典的单个key进行操作(缓存其实就是存在了一个叫cache的字典里,后面看源码就知道了)。所以如果要对每个key做独立的过期,就必须改动内部逻辑了。其实到这里,从实用的角度出发,直接找第三方库是比较合理的。不过我出于兴趣,决定自己来实现一下。</p>
<p>要改,那么自然要先读。这里简单过一下Python3.6的<code>lru_cache</code>实现。</p>
<figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">lru_cache</span><span class="params">(maxsize=<span class="number">128</span>, typed=False)</span>:</span></span><br><span class="line"> <span class="keyword">if</span> maxsize <span class="keyword">is</span> <span class="keyword">not</span> <span class="literal">None</span> <span class="keyword">and</span> <span class="keyword">not</span> isinstance(maxsize, int):</span><br><span class="line"> <span class="keyword">raise</span> TypeError(<span class="string">'Expected maxsize to be an integer or None'</span>)</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">def</span> <span class="title">decorating_function</span><span class="params">(user_function)</span>:</span></span><br><span class="line"> wrapper = _lru_cache_wrapper(user_function, maxsize, typed, _CacheInfo)</span><br><span class="line"> <span class="keyword">return</span> update_wrapper(wrapper, user_function) <span class="comment"># 返回的是函数,不是函数结果</span></span><br><span class="line"> <span class="keyword">return</span> decorating_function</span><br></pre></td></tr></table></figure>
<p>要读懂这段代码需要对Python的装饰器工作原理有一定了解,本文不做赘述。这段代码显然没涉及太多逻辑,开头校验下参数,把函数和装饰器参数传进了<code>_lru_cache_wrapper</code>函数,后面的update_wrapper只是做了些属性转移的工作,跟缓存功能没太大关系。我们再进到<code>_lru_cache_wrapper</code>函数看看,实际上主要的缓存逻辑实现都在这里头了。接下来分段来做解读。</p>
<figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">_lru_cache_wrapper</span><span class="params">(user_function, maxsize, typed, _CacheInfo)</span>:</span></span><br><span class="line"> sentinel = object() <span class="comment"># 用来判断缓存是否命中的唯一标识</span></span><br><span class="line"> make_key = _make_key <span class="comment"># 根据参数生成缓存key的函数</span></span><br><span class="line"> PREV, NEXT, KEY, RESULT = <span class="number">0</span>, <span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span> <span class="comment"># 给下面链表的节点对应位置命名,提升可读性</span></span><br><span class="line"></span><br><span class="line"> cache = {} <span class="comment"># 用来缓存的字典</span></span><br><span class="line"> hits = misses = <span class="number">0</span> <span class="comment"># 缓存命中统计</span></span><br><span class="line"> full = <span class="literal">False</span> <span class="comment"># 缓存是不是满了</span></span><br><span class="line"> cache_get = cache.get</span><br><span class="line"> cache_len = cache.__len__</span><br><span class="line"> lock = RLock() <span class="comment"># 链表操作是非线程安全的,需要加锁</span></span><br><span class="line"> root = [] <span class="comment"># 环形双向链表的根节点</span></span><br><span class="line"> root[:] = [root, root, <span class="literal">None</span>, <span class="literal">None</span>] <span class="comment"># 链表初始化</span></span><br></pre></td></tr></table></figure>
<p>这里基本都是些初始化,光看这部分可能会有点云里雾水的,主要还得看后面都对这些常量、变量做了些什么事情。先简单说下<code>lru_cache</code>的实现原理,它是借助Python字典和双向环形链表来实现的。</p>
<p><code>root</code>就是这个链表的根节点,<code>root</code>是个数组,链表里的其他节点也是这个结构,分别包含四个部分:前节点(PREV),后节点(NEXT),字典键(KEY)和函数结果(RESULT)。在通常情况下,根节点的左侧(PREV)是最后被访问的数据,而右侧(NEXT)则是最旧的缓存。这个链表的作用是维护缓存的访问顺序,确保在缓存满了的情况下能够进行时间复杂度为O(1)的缓存增加与删除操作。</p>
<p>而<code>cache</code>是个字典,它的键是由函数参数生成的哈希对象,和链表结点里的KEY是同一种东西。而字典的value则是不一定的,在不同情况下存的东西并不一样,但抽象意义上来讲都可以理解为函数返回结果,它的作用就是用来快速获取缓存中的函数返回结果,时间复杂度为O(1)。</p>
<p>这么说还是有点抽象了,先看代码,官方的实现里对<code>maxsize</code>的两种特殊情况做了单独实现,一种是不需要缓存的情况,一种是缓存没有上限的情况。</p>
<figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="keyword">if</span> maxsize == <span class="number">0</span>:</span><br><span class="line"> <span class="function"><span class="keyword">def</span> <span class="title">wrapper</span><span class="params">(*args, **kwds)</span>:</span></span><br><span class="line"> <span class="keyword">nonlocal</span> misses</span><br><span class="line"> result = user_function(*args, **kwds)</span><br><span class="line"> misses += <span class="number">1</span></span><br><span class="line"> <span class="keyword">return</span> result</span><br><span class="line"><span class="keyword">elif</span> maxsize <span class="keyword">is</span> <span class="literal">None</span>:</span><br><span class="line"> <span class="function"><span class="keyword">def</span> <span class="title">wrapper</span><span class="params">(*args, **kwds)</span>:</span></span><br><span class="line"> <span class="comment"># Simple caching without ordering or size limit</span></span><br><span class="line"> <span class="keyword">nonlocal</span> hits, misses</span><br><span class="line"> key = make_key(args, kwds, typed)</span><br><span class="line"> result = cache_get(key, sentinel)</span><br><span class="line"> <span class="keyword">if</span> result <span class="keyword">is</span> <span class="keyword">not</span> sentinel:</span><br><span class="line"> hits += <span class="number">1</span></span><br><span class="line"> <span class="keyword">return</span> result</span><br><span class="line"> result = user_function(*args, **kwds)</span><br><span class="line"> cache[key] = result</span><br><span class="line"> misses += <span class="number">1</span></span><br><span class="line"> <span class="keyword">return</span> result</span><br></pre></td></tr></table></figure>
<p>第一种没啥说的,简单提下第二种,这里cache的value直接存的是函数的返回结果,逻辑很好读懂,如果有缓存则返回缓存,没有则调用<code>user_function</code>,并将函数结果缓存起来。<code>setinal</code>的作用就是作为唯一标识,判断函数结果是否已经缓存了,不用None是为了防止有些函数返回的可能就是None。并且由于上面对字典的操作都是线程安全的,所以也用不到锁。而且我们发现这里也压根没用到链表。</p>
<p>接下来就是第三种,设置了<code>maxsize</code>的情况。</p>
<figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="comment"># ...</span></span><br><span class="line"><span class="keyword">else</span>:</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">def</span> <span class="title">wrapper</span><span class="params">(*args, **kwds)</span>:</span></span><br><span class="line"> <span class="keyword">nonlocal</span> root, hits, misses, full</span><br><span class="line"> key = make_key(args, kwds, typed)</span><br><span class="line"> <span class="keyword">with</span> lock:</span><br><span class="line"> link = cache_get(key)</span><br><span class="line"> <span class="keyword">if</span> link <span class="keyword">is</span> <span class="keyword">not</span> <span class="literal">None</span>:</span><br><span class="line"> link_prev, link_next, _key, result = link</span><br><span class="line"> link_prev[NEXT] = link_next</span><br><span class="line"> link_next[PREV] = link_prev</span><br><span class="line"> last = root[PREV]</span><br><span class="line"> last[NEXT] = root[PREV] = link</span><br><span class="line"> link[PREV] = last</span><br><span class="line"> link[NEXT] = root</span><br><span class="line"> hits += <span class="number">1</span></span><br><span class="line"> <span class="keyword">return</span> result</span><br><span class="line"> result = user_function(*args, **kwds)</span><br><span class="line"> <span class="keyword">with</span> lock:</span><br><span class="line"> <span class="keyword">if</span> key <span class="keyword">in</span> cache:</span><br><span class="line"> <span class="keyword">pass</span></span><br><span class="line"> <span class="keyword">elif</span> full:</span><br><span class="line"> oldroot = root</span><br><span class="line"> oldroot[KEY] = key</span><br><span class="line"> oldroot[RESULT] = result</span><br><span class="line"> root = oldroot[NEXT]</span><br><span class="line"> oldkey = root[KEY]</span><br><span class="line"> oldresult = root[RESULT]</span><br><span class="line"> root[KEY] = root[RESULT] = <span class="literal">None</span></span><br><span class="line"> <span class="keyword">del</span> cache[oldkey]</span><br><span class="line"> cache[key] = oldroot</span><br><span class="line"> <span class="keyword">else</span>:</span><br><span class="line"> last = root[PREV]</span><br><span class="line"> link = [last, root, key, result]</span><br><span class="line"> last[NEXT] = root[PREV] = cache[key] = link</span><br><span class="line"> full = (cache_len() >= maxsize)</span><br><span class="line"> misses += <span class="number">1</span></span><br><span class="line"> <span class="keyword">return</span> result</span><br></pre></td></tr></table></figure>
<p>这部分主要分为两部分逻辑,缓存命中和未命中。前面初始化的链表和锁到这里才派上了用场。</p>
<p>缓存命中时,需要把原先的缓存节点移动到<code>root</code>节点的左侧,作为最近访问节点。</p>
<p>而未命中时,需要先调用<code>user_function</code>获取结果,然后又出现了两种分支逻辑:缓存满了和没满。</p>
<p>如果没满,就只需要简单地把新节点插入到<code>root</code>前面即可,记得更新下<code>full</code>变量。</p>
<p>如果缓存满了,就把新结果放进当前root节点,最旧的缓存节点设置为新的root,并清除数据,更新cache。</p>
<p>这里配合下面的数据结构图可能会好理解一点。</p>
<p><img src="/img/lru_cache/01.webp" alt=""></p>
<p>剩下的其实是些相对不太重要的代码了,就是对外暴露一些操作接口,这里不贴上来了。</p>
<p>看完源码后,现在知道工作原理了,其实要实现过期功能,就只需要在旧的逻辑里,插入处理过期逻辑的代码即可。这里就说下大致思路,不贴代码了。完整代码可以点<a href="https://gist.github.com/Orenoid/bc011c7bb60c128d2767739fead29cc1" target="_blank" rel="noopener">这里</a>查看。</p>
<ol>
<li>当<code>maxsize</code>为0的时候,没有缓存,所以不变。</li>
<li>当<code>maxsize</code>为<code>None</code>的时候,即缓存没有上限,我们对字典的<code>value</code>稍作调整,把函数结果和缓存时间一起存进去,然后在取值的时候,判断是否已经过期,过期的话就按照缓存未命中处理,重新调用<code>user_function</code>并缓存结果即可。</li>
<li>当<code>maxsize</code>为大于0的时候,我们把缓存的时间一并存入到链表的节点里,只被动过期,在命中缓存的时候,判断是否已经过期了,是的话,就把节点移除掉,当然也要记得更新<code>cache</code>,然后按照缓存未命中的逻辑继续往下走就行了。</li>
</ol>
<p>大致逻辑是这样,剩下的都是一些细节上的处理。另外我在重新实现的时候,重新封装了一个类来作为链表的节点,而不是像官方那样用的数组,差不多就这些了。</p>
]]></content>
<categories>
<category>技术</category>
</categories>
<tags>
<tag>Python</tag>
</tags>
</entry>
<entry>
<title>Docker build优化小技巧</title>
<url>/2019/12/17/docker-build-optimize/</url>
<content><![CDATA[<p>最近开发项目调试的时候改用Docker来运行,主要是为了让开发环境和生产环境尽可能一致。但开发过程中很快发现一个问题,Docker构建镜像太慢了,每次改了一点代码,想要debug就得重新构建,然后要等它下载一堆依赖。开发了一天后,忍不了了,抽空查了下Docker build缓存的相关资料。</p>
<a id="more"></a>
<p>虽然一直知道Docker有分层文件系统这回事,也知道文件层可以在构建过程中重复使用,但没弄清楚到底什么情况才能触发缓存功能。按照网上查到的说明,只要Dockerfile和相关文件没有改动,那么在重新构建的时候就可以利用在本地中缓存的一些镜像层。</p>
<p>如果我们只是改动了一些代码,而项目的依赖清单没有变化,那么显然是没有理由每次都要下载一遍的。那么为什么我之前老是要重新下载呢?先看一个Dockerfile。</p>
<figure class="highlight dockerfile"><table><tr><td class="code"><pre><span class="line"><span class="keyword">FROM</span> python:<span class="number">3.8</span>.<span class="number">2</span>-alpine3.<span class="number">10</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">WORKDIR</span><span class="bash"> /data/code/project</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">COPY</span><span class="bash"> app app</span></span><br><span class="line"><span class="keyword">COPY</span><span class="bash"> config.py manage.py ./</span></span><br><span class="line"><span class="keyword">COPY</span><span class="bash"> requirements requirements</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">RUN</span><span class="bash"> <span class="built_in">echo</span> https://mirrors.aliyun.com/alpine/v3.10/main > /etc/apk/repositories; \</span></span><br><span class="line"><span class="bash"> <span class="built_in">echo</span> https://mirrors.aliyun.com/alpine/v3.10/community >> /etc/apk/repositories</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">RUN</span><span class="bash"> apk add --no-cache --virtual .build-deps gcc libc-dev linux-headers tzdata;\</span></span><br><span class="line"><span class="bash"> cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime; \</span></span><br><span class="line"><span class="bash"> pip install --no-cache-dir -r requirements/dev.txt -i https://pypi.tuna.tsinghua.edu.cn/simple/; \</span></span><br><span class="line"><span class="bash"> apk del .build-deps;</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 剩余其他命令……</span></span><br></pre></td></tr></table></figure>
<p>这是我项目中使用的Dockerfile前半部分。根据搜索引擎提供的资料,Docker在根据Dockerfile构建镜像的时候,它会判断是否可以重复利用以前构建生产的镜像。对大多数命令来说,只要命令的文本没变化,那么就可以利用缓存。而对于COPY命令,还会额外判断复制的文件内容是不是变化了。</p>
<p>如果是这样,我只是改了一点代码,requirements文件夹和pip install命令都没有任何变化,为何还会重新下载?</p>
<p>我看了下构建时的日志,发现到WORKRDIR这一层还有Using Cache的标记,而到COPY app app的时候就没了。巧了,我改的代码就是在app目录里头的。于是我接着看网上的文章,发现里面还提到了一条缓存规则:</p>
<blockquote>
<p>如果某一层利用不了缓存,那么后续的层都将不会从缓存中加载</p>
</blockquote>
<p><img src="/img/docker_build_opt/cache-algorithm.png" alt=""></p>
<p>根据这条规则,再看回前面的Dockerfile,就找到问题所在了。由于我在安装依赖之前就先把所有代码先复制到镜像里,导致只要有任何代码改动就会导致后面所有层的缓存连带着失效。而实际上如果只是改了代码,而依赖没有变化,那么完全是没有必要重新下载的。所以我们对Dockerfile做一点调整:</p>
<figure class="highlight dockerfile"><table><tr><td class="code"><pre><span class="line"><span class="keyword">FROM</span> python:<span class="number">3.8</span>.<span class="number">2</span>-alpine3.<span class="number">10</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">WORKDIR</span><span class="bash"> /data/code/project</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">COPY</span><span class="bash"> requirements requirements</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">RUN</span><span class="bash"> <span class="built_in">echo</span> https://mirrors.aliyun.com/alpine/v3.10/main > /etc/apk/repositories; \</span></span><br><span class="line"><span class="bash"> <span class="built_in">echo</span> https://mirrors.aliyun.com/alpine/v3.10/community >> /etc/apk/repositories</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">RUN</span><span class="bash"> apk add --no-cache --virtual .build-deps gcc libc-dev linux-headers tzdata;\</span></span><br><span class="line"><span class="bash"> cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime; \</span></span><br><span class="line"><span class="bash"> pip install --no-cache-dir -r requirements/dev.txt -i https://pypi.tuna.tsinghua.edu.cn/simple/; \</span></span><br><span class="line"><span class="bash"> apk del .build-deps;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">COPY</span><span class="bash"> app app</span></span><br><span class="line"><span class="keyword">COPY</span><span class="bash"> config.py manage.py ./</span></span><br><span class="line"><span class="comment"># 剩余其他命令……</span></span><br></pre></td></tr></table></figure>
<p>这样一来,当初次构建完镜像之后,只要依赖没有变动,再次构建的时候都可以有效利用缓存,无需重新下载了。通过这个技巧,可以大幅提升构建速度,同时提高开发调试的效率。</p>
<p>参考链接:<a href="https://pythonspeed.com/articles/docker-caching-model/" target="_blank" rel="noopener">Faster or slower: the basics of Docker build caching</a></p>
]]></content>
<categories>
<category>技术</category>
</categories>
<tags>
<tag>Docker</tag>
</tags>
</entry>
<entry>
<title>Flask源码阅读笔记</title>
<url>/2019/11/23/flask-source-code/</url>
<content><![CDATA[<p>Flask和Django都是基于wsgi协议去实现的Python Web框架。本文将从与开发者相关度最高的地方入手:请求处理。</p>
<p>根据wsgi协议的规定,application需要提供一个callable对象给服务器(或中间件,后略),分别接收两个参数:environ字典与start_resposne函数(当然也可以是其他callable对象),这两个参数通常由服务器传入。Python官方提供了一个简单的例子:</p>
<figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">demo_app</span><span class="params">(environ,start_response)</span>:</span></span><br><span class="line"> <span class="keyword">from</span> io <span class="keyword">import</span> StringIO</span><br><span class="line"> stdout = StringIO()</span><br><span class="line"> print(<span class="string">"Hello world!"</span>, file=stdout)</span><br><span class="line"> print(file=stdout)</span><br><span class="line"> h = sorted(environ.items())</span><br><span class="line"> <span class="keyword">for</span> k,v <span class="keyword">in</span> h:</span><br><span class="line"> print(k,<span class="string">'='</span>,repr(v), file=stdout)</span><br><span class="line"> start_response(<span class="string">"200 OK"</span>, [(<span class="string">'Content-Type'</span>,<span class="string">'text/plain; charset=utf-8'</span>)])</span><br><span class="line"> <span class="keyword">return</span> [stdout.getvalue().encode(<span class="string">"utf-8"</span>)]</span><br></pre></td></tr></table></figure>
<a id="more"></a>
<p>当服务器收到请求时,就会去调用框架提供的callable对象,解析HTTP请求,构造environ字典,和start_response函数一并传入。而在flask中,这个callable对象就是由Flask类构造而来的,它的<code>__call__</code>方法是这样的:</p>
<figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">__call__</span><span class="params">(self, environ, start_response)</span>:</span></span><br><span class="line"> <span class="keyword">return</span> self.wsgi_app(environ, start_response)</span><br></pre></td></tr></table></figure>
<p>可以看到只是单纯的转交给了<code>wsgi_app</code>方法,根据源码文档里的解释,这么设计的目的是为了在我们需要使用wsgi中间件的时候,可以这样:</p>
<figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="comment"># 通常做法</span></span><br><span class="line">app = MyMiddleware(app)</span><br><span class="line"><span class="comment"># 第二种</span></span><br><span class="line">app.wsgi_app = MyMiddleware(app.wsgi_app)</span><br></pre></td></tr></table></figure>
<p>使用第二种方式添加中间件的话,可以保留对app对象的引用,避免被中间件包裹后失去对app原有API的使用权。</p>
<p>接着是<code>wsgi_app</code>方法的代码:</p>
<figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">wsgi_app</span><span class="params">(self, environ, start_response)</span>:</span></span><br><span class="line"> <span class="comment"># 构造上下文</span></span><br><span class="line"> ctx = self.request_context(environ)</span><br><span class="line"> error = <span class="literal">None</span></span><br><span class="line"> <span class="keyword">try</span>:</span><br><span class="line"> <span class="keyword">try</span>:</span><br><span class="line"> <span class="comment"># 将上下文推入栈中</span></span><br><span class="line"> ctx.push()</span><br><span class="line"> <span class="comment"># 处理请求</span></span><br><span class="line"> response = self.full_dispatch_request()</span><br><span class="line"> <span class="keyword">except</span> Exception <span class="keyword">as</span> e:</span><br><span class="line"> error = e</span><br><span class="line"> response = self.handle_exception(e)</span><br><span class="line"> <span class="keyword">except</span>: <span class="comment"># noqa: B001</span></span><br><span class="line"> error = sys.exc_info()[<span class="number">1</span>]</span><br><span class="line"> <span class="keyword">raise</span></span><br><span class="line"> <span class="keyword">return</span> response(environ, start_response)</span><br><span class="line"> <span class="keyword">finally</span>:</span><br><span class="line"> <span class="keyword">if</span> self.should_ignore_error(error):</span><br><span class="line"> error = <span class="literal">None</span></span><br><span class="line"> <span class="comment"># 最后必须确保将上下文弹出,避免干扰其他请求</span></span><br><span class="line"> ctx.auto_pop(error)</span><br></pre></td></tr></table></figure>
<p>整体逻辑相当简洁,基本都是高层级的抽象,先处理上下文,然后处理请求和捕获错误,最后确保上下文被弹出。</p>
<p>我们先展开讲一下flask的上下文机制。Flask提供了两种上下文,分别是请求上下文和应用上下文。这两类上下文的生命周期基本上相等的,每处理一个请求,就会相应地创建一个请求上下文和应用上下文。</p>
<p>实际上Flask在早期版本中,只有请求上下文的概念。但后来开发者发现,在有些场景下我们并没有构造请求的必要性,但是需要用到应用的一些内部资源或配置、参数(例如数据库连接),如果仅仅是为了获取应用上下文而强行构建一个请求上下文的话,这种方案又过于不合理,于是flask后来将这两类上下文区分开来了。</p>
<p>说完上下文机制,我们继续回来看上面的<code>wsgi_app</code>方法,上下文入栈之后,flask会调用<code>full_dispatch_request</code>方法来生成响应数据:</p>
<figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">full_dispatch_request</span><span class="params">(self)</span>:</span></span><br><span class="line"> <span class="string">"""Dispatches the request and on top of that performs request</span></span><br><span class="line"><span class="string"> pre and postprocessing as well as HTTP exception catching and</span></span><br><span class="line"><span class="string"> error handling.</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string"> .. versionadded:: 0.7</span></span><br><span class="line"><span class="string"> """</span></span><br><span class="line"> <span class="comment"># 先执行before_first_request注册的中间件函数列表</span></span><br><span class="line"> self.try_trigger_before_first_request_functions()</span><br><span class="line"> <span class="keyword">try</span>:</span><br><span class="line"> <span class="comment"># 触发request_started信号</span></span><br><span class="line"> request_started.send(self)</span><br><span class="line"> <span class="comment"># 调用开发者注册的before_request中间件</span></span><br><span class="line"> rv = self.preprocess_request()</span><br><span class="line"> <span class="keyword">if</span> rv <span class="keyword">is</span> <span class="literal">None</span>:</span><br><span class="line"> <span class="comment"># 若返回None,说明中间件没有拦截请求,继续处理</span></span><br><span class="line"> rv = self.dispatch_request()</span><br><span class="line"> <span class="keyword">except</span> Exception <span class="keyword">as</span> e:</span><br><span class="line"> <span class="comment"># 若执行过程出错,尝试使用开发者注册的错误处理器进行错误处理</span></span><br><span class="line"> rv = self.handle_user_exception(e)</span><br><span class="line"> <span class="comment"># 此处的rv实际上就是视图函数返回的响应数据实体,把它再包装成一个callable对象</span></span><br><span class="line"> <span class="keyword">return</span> self.finalize_request(rv)</span><br></pre></td></tr></table></figure>
]]></content>
<categories>
<category>技术</category>
</categories>
<tags>
<tag>Python</tag>
<tag>Flask</tag>
</tags>
</entry>
<entry>
<title>Hexo博客Docker化实践</title>
<url>/2019/10/15/docker-hexo/</url>
<content><![CDATA[<p>前段时间学习了Docker的基本使用之后,决定借助Docker来解决麻烦的博客迁移问题。</p>
<p>我的博客是基于Hexo与Github Pages来实现的,属于静态博客。实际上从17年就开始折腾了,然后中途换了两次电脑,每次把npm依赖环境以及博客文件迁移到新电脑上都很麻烦。比如安装新环境,因为我不是前端,对工具不熟悉,每次都得边操作边查。装完后还得研究Hexo的配置,如果版本跟以前的不兼容的话,旧的配置文件基本就作废了,又得从头看文档。同理还得安装主题,又得把上面的问题过一遍,而且中途还会因为国内糟糕的网络环境衍生出更多烦人的问题。</p>
<p>所以这次决定借助docker来简化迁移流程,让自己少掉点头发。</p>
<a id="more"></a>
<p>首先简单构思了下整个流程应该怎么走,理想的情况应该是,当我换新电脑后,不进行任何物理拷贝,只需要从Github拿到博客的源文件(文章,图片,配置文件等等),然后借助Docker快速把环境搭建起来,就可以开始写新文章了。</p>
<p>OK,需求有了,接下来就要看怎么设计了。</p>
<p>第一步,要把源文件放到Github上,最简单的方法就是在博客对应的Repo下开一个新分支,专门用来放源文件,master分支依旧用于部署博客的静态文件,很简单。</p>
<p>接着是Docker环境,在网上查了下,看到有人采用的方案是,构建一个镜像,然后每次运行hexo命令都要跑一个容器,这显然太过于笨重了。我的思路是构建好镜像后,只开一个容器,往后需要在npm环境下进行的操作都进到容器终端里运行,这个容器就一直用下去了,平时也没必要运行,毕竟我们这是静态博客,容器提供的是操作环境,而非服务,需要的时候再把容器跑起来就好了。</p>
<p>以下是我的Dockerfile:</p>
<figure class="highlight dockerfile"><table><tr><td class="code"><pre><span class="line"><span class="keyword">FROM</span> mhart/alpine-node:<span class="number">10</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 给apk换源</span></span><br><span class="line"><span class="keyword">RUN</span><span class="bash"> <span class="built_in">echo</span> https://mirrors.aliyun.com/alpine/v3.8/main > /etc/apk/repositories; \</span></span><br><span class="line"><span class="bash"> <span class="built_in">echo</span> https://mirrors.aliyun.com/alpine/v3.8/community >> /etc/apk/repositories</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">RUN</span><span class="bash"> \</span></span><br><span class="line"><span class="bash"> apk --update --no-progress --no-cache add git openssh</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">WORKDIR</span><span class="bash"> /hexo</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">RUN</span><span class="bash"> \</span></span><br><span class="line"><span class="bash"> npm --registry https://registry.npm.taobao.org install hexo-cli@3.1.0 -g \</span></span><br><span class="line"><span class="bash"> && hexo init . \</span></span><br><span class="line"><span class="bash"> && npm --registry https://registry.npm.taobao.org install hexo-generator-feed@2.1.1 hexo-generator-searchdb@1.2.0 hexo-deployer-git@2.1.0 \</span></span><br><span class="line"><span class="bash"> && git <span class="built_in">clone</span> --branch v7.5.0 --depth 1 https://github.com/theme-next/hexo-theme-next themes/next \</span></span><br><span class="line"><span class="bash"> && rm -rf /hexo/_config.yml /hexo/themes/next/_config.yml /hexo/<span class="built_in">source</span> \</span></span><br><span class="line"><span class="bash"> && yarn cache clean \</span></span><br><span class="line"><span class="bash"> && npm cache clean --force</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">EXPOSE</span> <span class="number">4000</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">VOLUME</span><span class="bash"> [<span class="string">"/hexo/_config.yml"</span>, <span class="string">"/hexo/themes/next/_config.yml"</span>, <span class="string">"/hexo/source"</span>, \</span></span><br><span class="line"><span class="bash"> <span class="string">"/hexo/themes/next/source/images/avatar.png"</span>, <span class="string">"/hexo/themes/next/source/images/favicon.ico"</span>, <span class="string">"/tmp/.ssh"</span>]</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">COPY</span><span class="bash"> docker-entrypoint.sh docker-entrypoint.sh</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">CMD</span><span class="bash"> [<span class="string">"sh"</span>, <span class="string">"docker-entrypoint.sh"</span>]</span></span><br></pre></td></tr></table></figure>
<p>前面就是依赖安装,主要是git和hexo要用到的npm包,为了防止以后出现版本不兼容问题,给每个依赖都指定了具体版本,包括next主题,通过git clone的时候指定tag来选择版本。这样一来,不管官方怎么更新,至少能确保我的博客保持一个稳定可用的状态。之后如果有什么好用新特性,再考虑迁移也不迟。</p>
<p>另一个重要部分就是文件挂载,得把从Github拿到的源文件挂载进容器里,主要就是那些需要我们自定义的东西,比如文章、配置、媒体资源等等,剩下的都由框架自动生成,打包在镜像里即可。</p>
<p>挂载的时候不允许对已存在文件进行挂载,而里头有几个文件,在框架初始化的时候会自动生成,所以要先手动删除掉。</p>
<figure class="highlight shell"><table><tr><td class="code"><pre><span class="line">rm -rf /hexo/_config.yml /hexo/themes/next/_config.yml /hexo/source</span><br></pre></td></tr></table></figure>
<p>部署的时候,需要用到ssh密钥,所以还需要把密钥也挂载进容器里,这里遇到个坑,因为我平时的个人主力电脑是Windows系统,密钥直接挂载进去的话,Windows与Linux的权限模型并不一致,部署的时候会遇到权限问题。这里采用的方案是先把密钥文件挂载到一个临时目录,然后在容器内部复制到<code>/root/.ssh</code>目录下,再对拷贝的副本进行权限修改。</p>
<p>这样流程基本上就可以走通了,把镜像构建起来后直接推送到Docker Hub上面,以后直接拉取即可,免去漫长而痛苦的构建过程。由于运行容器时要设置的参数还挺多的,写了个bat脚本,因为暂时没有在Linux下搭建环境的需求,就没写shell脚本。顺便写了个Readme文件,作为备忘指南。</p>
<p>这样一来,以后换新电脑,就只需要两步操作了:</p>
<ol>
<li>git clone获取源文件</li>
<li>拉取现成docker镜像,运行容器</li>
</ol>
<p>然后进容器就可以进行各种常规操作了,比如我这篇博客就是在新环境和新流程下写出来的。</p>
]]></content>
<categories>
<category>技术</category>
</categories>
<tags>
<tag>Hexo</tag>
<tag>Docker</tag>
</tags>
</entry>
<entry>
<title>从Python转向Java的一些感想</title>
<url>/2019/09/28/python2java/</url>
<content><![CDATA[<p>最近从Python转到了Java,写了一个多月,帮公司写了一个项目,谈谈感想。</p>
<a id="more"></a>
<p>主要分两方面,一个是Java本身,一个是spring全家桶。</p>
<p>首先谈谈从python切换到Java的一些感受。<br>这两门语言都是面向对象的,Java基础很快就上手了,学起来并不会觉得很困难,一开始也没觉得有什么很大的差异。到实际开始写项目的时候,差异就体现出来了,感受最明显的果然还是静态类型和动态类型的差别。</p>
<p>这里得先声明一点,我知道python也有一些类型检查的工具,大公司可能也有相应的规范,但至少我所在的公司没有,所以我也不知道这些工具能起到多大的作用。本文也只是以我个人的开发经验做的一些总结,并不是以踩python为目的。</p>
<p>我们知道函数是代码中非常重要的单元,理想的代码在高层次的逻辑中应当是以函数作为组织单位,同一个函数中应当只有一个抽象层级的逻辑。低层次的逻辑封装在更底层的函数内部,调用者只需要传参和获取结果,不需要关注具体实现细节。<br>而python的动态语言性质,会很容易诱导新手(比如我)写出破坏这种低耦合特性的代码。<br>一方面是类型的不确定性,不论参数还是结果,都有可能传个八竿子打不着关系的对象过来。这一点在python推出typing后稍微有所改善,但也只是起到提示作用,并没有约束效果。<br>如果接口处理的都是些基本类型或者封装好的类型还好,更要命的是,如果函数接收的是个字典、元组之类的,然后字典里嵌套了各种结构(就像我们Web API返回的复杂结构的JSON那样),或者元组里包含了不同类型的变量,然后用下标去取,这种情况下的代码可维护性基本为0了,鬼知道你会传个什么结构进来。事实上,我也确实在几个同事的项目里看过这种代码了,一个多层嵌套结构的字典或者数组,在几个函数内传了三四层,边传边改,而且变量名还一直变,到里头早就忘了这是个啥了(神奇的是其中一位同事以前还写过一两年的Java),幸好项目不大,如果是个大项目,不敢想象维护起来会是什么后果。</p>
<p>并且这种情况下,函数内外部都只能靠约定和信任来传参和接收结果,这样的话还谈何低耦合呢?不要拆分出一个函数可能还好点。我不知道有多少新手会犯这种错误,但是语言层面缺乏限制,又没有好的团队规范的情况下,必然会导致这种低可读性代码出现的可能性相对高很多,有了类型约束可以较为有效的减少这种情况发生的几率(但架不住有些人代码烂,静态语言也能给你写得跟动态的一样)</p>
<p>接着说说静态类型带来的麻烦,这个麻烦主要集中在无类型到有类型的转换上,具体场景通常为各种IO数据的处理,比如json,或者从数据库拿到的数据列表,这些用于传输的数据格式都是天生动态的。要在Java中使用,必然得想办法转化成有类型的,比如处理成POJO等等,通常会需要相应的数据解析工具(但是无论用什么工具,这个转换过程本质上都是基于传输双方的约定去进行的)。而在编写业务过程中,还需要用到不同框架与库来进行网络通信,每次需要处理序列化/反序列化的时候,都要去翻找对应框架的API,有些要配合Spring来使用还需要进行一堆繁琐的配置,相当麻烦。当然这方面主要还是Java这边的框架我不熟悉,写多了应该还是能克服的,不过相比动态类型要耗费更多时间是肯定的。</p>
<p>说完类型约束,在讨论spring全家桶之前,先说说我这两天接手一个离职同事的(Java Web)项目的经历。我上面一直在说静态类型的好处,结果把我同事的代码拉下来一看,看得我脑壳疼。好好一个Java,写得跟动态语言一样,满天飞的Object和Map。除此之外,到处是缺乏语义的命名,一堆list、entity、temp、map、xx1、xx2这种看不出是什么的名字,看几行就得回去确认下这个变量指代的是什么。而且不同抽象层级的代码也混在一起,业务层和表现层不分开来,写了一大段你以为他在处理什么复杂逻辑,其实他就是在拼装JSON。也算是另一种意义上的“事在人为”了。<br>后来我又回顾了自己的一些项目,虽然没他那么糟糕(真的没有),但类似的错误我代码里好像也有一些,但是自己写的时候并没有注意到这些问题。<br>由此我又得出另一个初步的结论,稍微读点别人的烂代码对提升自己代码质量还是有一定帮助的。如果光看自己代码,所有逻辑是自己写的,自然不会觉得难懂,也就很容易忽视很多缺陷。所以想要保证团队项目的代码质量与可维护性的话,我觉得Code Review应该是很有必要的。</p>
<p>好了,回头说说spring全家桶。这东西一开始真的把我搞得一脸懵逼(现在依旧懵逼)。<br>后来直接从spring boot上手,啃了好几天官方的文档,包括spring,springmvc,spring data这些,都简单过了一遍,才大致弄明白一些概念。随后又看了不少demo和开源项目,才磕磕碰碰地把之前一个python小项目用Java重新实现了一遍。<br>老实说,我还是没太明白spring这个IOC到底起到了什么实质作用,我知道是控制反转/依赖注入,把对象的生命周期和控制权交给spring来处理,我们负责接收就行了。但是我并没有直接感受到这样跟自己new一个会有多大的差别,带来了什么好处,是性能上的?还是扩展性上的?不知道,学的很浅,惭愧。</p>
<p>在项目开发过程中,接触到了分层架构的思想。这个要说是从spring学到的好像又不是很严谨,毕竟架构和设计模式这些东西是不局限于语言的。但比较奇怪的是我在写python web的时候,很少看到别人python项目也很少看到有按这个去设计的,而后来看过的Java项目代码,或多或少都是按这个结构来的。不过虽说大多数Java web项目都按照三层架构来设计的,但就像上面我提到的,同事项目的表现层和业务层逻辑界限模糊不清,比如在业务层拼装json等等。所以这东西还是看人。<br>总之,不管是不是从spring上学到的,但至少是我在这段时间写Java吸收到的一个设计思路。并且也可以应用到python web这边,让项目结构清晰很多。重新看了自己过去写的一些项目,发现很多地方也没做好逻辑的划分,便重新调整了代码布局,定制了一些规范,之后在网上搜索时看到<a href="https://zhuanlan.zhihu.com/p/28717374" target="_blank" rel="noopener">这篇文章</a>,发现里头很多规范和我做法几乎是一致的,可见我的思路多半是没有问题的。</p>
<p>以上就是我在过去一个多月以来,从python转到Java的一些感想和收获了,才疏学浅,若有不对的地方,欢迎指正。</p>
]]></content>
<categories>
<category>技术</category>
</categories>
<tags>
<tag>Python</tag>
<tag>Java</tag>
</tags>
</entry>
<entry>
<title>Python装饰器与单元测试</title>
<url>/2019/08/07/decorator-unittest/</url>
<content><![CDATA[<p>最近在给一个项目写单元测试的时候遇到一个问题,如何对带装饰器的函数进行测试。</p>
<p>先说说问题是怎么来的。在我看来,单元测试,顾名思义应该以最小单元作为测试对象,而装饰器与原函数明显是两个不同的功能单元,所以我觉得两者应该分开进行测试。<br>尽管我们可以图省事直接对新函数直接进行测试,但是会对导致测试结果不够直观,比如说我改动装饰器的时候写了个BUG,但是从单元测试体现出来的是,原函数出问题了,尽管我压根没动过它的代码。所以就诞生了这么一个需求,如何获取被装饰过的原函数。</p>
<a id="more"></a>
<p>在群里跟别人讨论这个问题的时候,有人反驳我说你可以给装饰器和新函数各写一个测试,装饰器的测试用例没出问题,新函数出问题了,那么不就是原函数出问题了吗?<br>从结果上来说,确实可以判断BUG来源,但是这样真的太丑了,得靠多个测试用例才能联合判断出BUG出在谁身上了,这样毫无“单元”性可言了。<br>并且装饰器大概率是要复用在不同函数上的,这就意味着你需要在每个测试用例里都写一遍测试装饰器功能的逻辑,这显然也违背了DRY原则。</p>
<p>总的来讲,我的看法依旧是装饰器与原函数应当分开测试。</p>
<p>然后问题就来了,我们知道装饰器在Python里其实是一个语法糖,它本质上是一个赋值操作。<br>比如下面的两种写法是等价的:</p>
<figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">decorator</span><span class="params">(func)</span>:</span></span><br><span class="line"> <span class="function"><span class="keyword">def</span> <span class="title">wrapper</span><span class="params">(*args, **kwargs)</span>:</span></span><br><span class="line"> print(<span class="string">'decorated'</span>)</span><br><span class="line"> <span class="keyword">return</span> func(*args, **kwargs)</span><br><span class="line"> <span class="keyword">return</span> wrapper</span><br><span class="line"></span><br><span class="line"><span class="comment"># 写法一</span></span><br><span class="line"><span class="meta">@decorator</span></span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">foo</span><span class="params">()</span>:</span></span><br><span class="line"> <span class="keyword">pass</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 写法二</span></span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">bar</span><span class="params">()</span>:</span></span><br><span class="line"> <span class="keyword">pass</span></span><br><span class="line">bar = decorator(bar)</span><br></pre></td></tr></table></figure>
<p>像这样我们在导入函数foo的时候,它其实已经被替换掉了,我们并不能直接拿到原函数,想要对原有逻辑做测试也就无从说起了。</p>
<p>我在网上搜了一下,好像没有太多人有这个疑问,搜索结果和解决方案并不多。<br>这里以Python3为讨论前提,我大致找到了两个解决方案。</p>
<p>一个是stackoverflow上的一个<a href="https://stackoverflow.com/questions/30327518/how-to-unit-test-decorated-functions" target="_blank" rel="noopener">提问</a>,提问人的需求几乎跟我一模一样。回答里没给出啥靠谱的答案,最后提问者自己提出了一个方案,把装饰器的逻辑再拆到一个函数里,然后测试的时候对这个函数进行mock。<br>这个方案第一眼看过去好像确实可行,但仔细想了下,大多数情况下,装饰器里的逻辑没办法那么简单地拆分到一个函数里,每个装饰器还都得这么拆开来写,可行性较低,没做更深入的尝试(主要是立马又搜到下面这个解决方案了)。</p>
<p>随后在stackoverflow的另一个<a href="https://stackoverflow.com/questions/14942282/accessing-original-decorated-function-for-test-purposes" target="_blank" rel="noopener">提问</a>里,我得知functools的wraps会给外层wrapper提供一个<code>__wrapped__</code>属性,指向原函数。</p>
<p>不过在进一步搜索后,根据《Python Cookbook》提供的<a href="https://python3-cookbook.readthedocs.io/zh_CN/latest/c09/p03_unwrapping_decorator.html" target="_blank" rel="noopener">说法</a>,wraps提供的<code>__wrapped__</code>在遇到多层装饰器的时候,根据Python版本不同,表现并不稳定。<br>有些版本里它会直接指向最原始的函数,有些版本里它仅仅指向装饰器嵌套里的第二层,估计是没处理顶层的原函数也是被装饰过的情况。针对这种情况,我决定自己手动处理多层嵌套的情况,结合群友给的优化,写了个获取原始函数的函数,单元测试的时候可以拿来使用:</p>
<figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">get_original_func</span><span class="params">(func)</span>:</span></span><br><span class="line"> <span class="keyword">while</span> hasattr(func, <span class="string">'__wrapped__'</span>):</span><br><span class="line"> func = func.__wrapped__</span><br><span class="line"> <span class="keyword">return</span> func</span><br></pre></td></tr></table></figure>
<p>但是这种也仅仅对使用了wraps装饰器的函数有效,如果提供装饰器的人没有用到它,似乎还真没什么手段来拿到原函数了。至少我没找到什么办法,有的话欢迎探讨。</p>
<p>参考链接:</p>
<p><a href="https://stackoverflow.com/questions/30327518/how-to-unit-test-decorated-functions" target="_blank" rel="noopener">https://stackoverflow.com/questions/30327518/how-to-unit-test-decorated-functions</a></p>
<p><a href="https://stackoverflow.com/questions/14942282/accessing-original-decorated-function-for-test-purposes" target="_blank" rel="noopener">https://stackoverflow.com/questions/14942282/accessing-original-decorated-function-for-test-purposes</a></p>
<p><a href="https://python3-cookbook.readthedocs.io/zh_CN/latest/c09/p03_unwrapping_decorator.html" target="_blank" rel="noopener">https://python3-cookbook.readthedocs.io/zh_CN/latest/c09/p03_unwrapping_decorator.html</a></p>
]]></content>
<categories>
<category>技术</category>
</categories>
<tags>
<tag>Python</tag>
</tags>
</entry>
<entry>
<title>socket编程指南</title>
<url>/2018/12/02/python-socket/</url>
<content><![CDATA[<blockquote>
<p>本文翻译自Python官方文档中的这篇<a href="https://docs.python.org/3.8/howto/sockets.html" target="_blank" rel="noopener">《Socket Programming HOWTO》</a>,作者:Gordon McMillan</p>
</blockquote>
<h4 id="摘要"><a href="#摘要" class="headerlink" title="摘要"></a>摘要</h4><p>socket几乎到处都有被用到,但却是被误解得最多的技术之一。本文将对对socket进行一个总体的概述,但这并不是真正的教程,要会使用的话还得进一步自己去研究。文章不会对socket相关细节面面俱到(太多了),但是我希望它能提供足够的背景知识,让你像模像样的开始使用套接字。</p>
<a id="more"></a>
<h4 id="socket"><a href="#socket" class="headerlink" title="socket"></a>socket</h4><p>本文只打算讨论<code>INET</code>类型的socket(例如IPv4),事实上至少有99%的场景下用的都是这一类,同时我们也只讨论<code>STREAM</code>类型的(例如TCP)。除非你很清楚实际应该怎么选择(那你也没必要阅读这篇指南了),使用 STREAM 类型的套接字将会得到比其它类型更好的表现与性能。 我会尽量帮你弄明白socket是啥以及如何使用阻塞/非阻塞socket,首先会从阻塞式socket开始介绍,先把这个弄明白了才能进一步研究非阻塞式是如何工作的。</p>
<p>理解这些东西的难点之一在于「套接字」可以表示很多微妙差异的东西,这取决于上下文。所以首先,让我们先分清楚「客户端」套接字和「服务端」套接字之间的不同,客户端相当于一个会话终端,而服务端则更像是个接线员。客户端应用(比如浏览器)只会用到客户端socket,而对于服务器来说,这两类socket都要使用到。</p>
<h4 id="历史"><a href="#历史" class="headerlink" title="历史"></a>历史</h4><p>进程间通信有各种各样的方法,但目前socket是最受欢迎的一种。如果任意指定一个平台的话,socket可能没某些其他形式的IPC那么快,但如果要做到跨平台通信的话,socket几乎就是唯一选择了。</p>
<p>socket是Berkeley发明的,是Unix的BSD风格的一部分。socket与INET的结合使得与世界各地的计算机进行通信变得异常容易(至少与其他方案相比),因此理所当然地,socket这一通信方案在互联网中得到了迅速扩散。</p>
<h4 id="创建socket"><a href="#创建socket" class="headerlink" title="创建socket"></a>创建socket</h4><p>简略地说,当你点击一个链接来到现在这个页面的时候,你的浏览器就已经做了下面这几件事情:</p>
<figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="comment"># create an INET, STREAMing socket</span></span><br><span class="line">s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)</span><br><span class="line"><span class="comment"># now connect to the web server on port 80 - the normal http port</span></span><br><span class="line">s.connect((<span class="string">"www.python.org"</span>, <span class="number">80</span>))</span><br></pre></td></tr></table></figure>
<p>当<code>connect</code>成功后,我们就可以通过socket <code>s</code>来对页面上的内容进行请求了,然后再通过同一个socket获取响应内容,这些完成之后这个socket就会被销毁(是的,销毁)。客户端套接字通常用来做一次交换(或者说一小组序列的交换)。</p>
<p>上面是客户端的情况,而服务器上的流程稍微复杂一些。首先,服务器会创建一个服务端socket:</p>
<figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="comment"># create an INET, STREAMing socket</span></span><br><span class="line">serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)</span><br><span class="line"><span class="comment"># bind the socket to a public host, and a well-known port</span></span><br><span class="line">serversocket.bind((socket.gethostname(), <span class="number">80</span>))</span><br><span class="line"><span class="comment"># become a server socket</span></span><br><span class="line">serversocket.listen(<span class="number">5</span>)</span><br></pre></td></tr></table></figure>
<p>这里头有几个需要注意的点:我们用的是<code>socket.gethostname()</code>,这样的话这个soccket才是对外部暴露的。如果用的是 <code>s.bind(('localhost', 80))</code> 或者 <code>s.bind(('127.0.0.1', 80))</code> ,尽管这依旧是个服务端socket,但是它只会对主机内部可见。而<code>s.bind(('', 80))</code>指定套接字可以由机器碰巧拥有的任何地址访问。</p>
<p>另一个需要注意的点是:那些数字较小的端口号通常会保留用于一些常见的服务(例如HTTP、SNMP等)。建议自己开发的时候设置一个较大的端口号数字(4位)。</p>
<p>最后,<code>listen</code> 方法的参数会告诉socket库,我们希望在队列中累积多达 5 个(通常的最大值)连接请求后再拒绝外部连接。如果你其他带代码写得没问题的话,通常来讲这个数量是足够的了。</p>
<p>现在,我们有了一个监听80端口的服务端socket,可以开始服务端的主循环逻辑了:</p>
<figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="keyword">while</span> <span class="literal">True</span>:</span><br><span class="line"> <span class="comment"># accept connections from outside</span></span><br><span class="line"> (clientsocket, address) = serversocket.accept()</span><br><span class="line"> <span class="comment"># now do something with the clientsocket</span></span><br><span class="line"> <span class="comment"># in this case, we'll pretend this is a threaded server</span></span><br><span class="line"> ct = client_thread(clientsocket)</span><br><span class="line"> ct.run()</span><br></pre></td></tr></table></figure>
<p>实际上有三种方式来实现这个循环逻辑:要么分配一个线程来处理客户端线程,要么创建一个新进程来处理,第三种方法则是重构应用逻辑,使用非阻塞式socket,并使用select在我们的服务端socket与任何活动的客户端socket之间进行多路复用,但这个我们之后再介绍。目前最重要的是弄清楚这一点:上面这些全都是服务端socket要干的事情,服务端socket不发送和接收任何数据,它只负责生成客户端socket。每当有某个客户端socket对前面绑定的host和port进行<code>connect()</code>操作的时候,服务端socket就会创建一个新的客户端socket来响应对方的请求。而之后,这两个客户端socket之间就可以自由地进行互相通信了,它们所用的端口是由系统动态分配的,当会话结束的时候就会被回收掉。</p>
<h4 id="进程间通信(IPC)"><a href="#进程间通信(IPC)" class="headerlink" title="进程间通信(IPC)"></a>进程间通信(IPC)</h4><p>如果你需要在同一台机器上进行两个进程间的快速 IPC 通信,你应该了解管道或者共享内存。如果你决定使用 AF_INET 类型的套接字,绑定「服务端」套接字到 <code>'localhost'</code> 。在大多数平台,这将会使用一个许多网络层间的通用快捷方式(本地回环地址)并且速度会快很多</p>
<blockquote>
<p>参见: <a href="https://docs.python.org/zh-cn/3.7/library/multiprocessing.html#module-multiprocessing" target="_blank" rel="noopener"><code>multiprocessing</code></a> 模块使跨平台 IPC 通信成为一个高层的 API</p>
</blockquote>
<h4 id="使用socket"><a href="#使用socket" class="headerlink" title="使用socket"></a>使用socket</h4><p>首先要注意的就是,浏览器所用的客户端socket和服务器所用的客户端socket本质上都是相同的,这其实是一个端对端通信。或者换一种说法,就是你作为设计者需要制定一个通信规则。通常来讲,发起连接一方的socket通过发送一个请求或者信号来开始一次会话。但这属于设计决定,并不是socket规则。</p>
<p>我们可以使用<code>send</code>和<code>recv</code>这两个操作来进行通信,或者也可以把客户端socket转换成file-like对象,然后对其进行读写操作。Java中采用的就是后一种形式,这里不做详谈,不过要提醒一点,采用后一种方案的时候要注意对socket进行<code>flush</code>操作。因为它们都是被缓冲了的“文件”,如果不进行<code>flush</code>,仅仅是写入数据后等待读取响应数据,那么你有可能永远都收不到响应,因为你写入的请求数据可能还在缓冲里面,压根没有被发送出去。</p>
<p>现在我们来看看socket的主要绊脚石:在网络缓冲区上的<code>send</code>和<code>recv</code>操作。这两个操作并不一定会完全地处理好你交给他们的数据(或者期望从他们那接收到的数据),因为其主要职责还是处理网络缓冲。通常他们只会在网络缓冲区被存入(send)或取出(recv)的时候才会返回,同时告诉你实际上处理了多少字节,而开发者必须自己多次重复调用这两个方法来确保数据被完全处理好了。</p>
<p>当<code>recv</code>操作返回0个字节的时候,这意味着另一侧已经关闭了连接,我们不会从那边收到任何数据了。而你发送的数据可能已经成功送达,这个之后再做讨论。</p>
<p>HTTP协议只会用一个socket来进行一次数据传输,客户端先发送一个请求,随后读取服务端返回的响应,随后这个socket就废弃了。这意味着客户端可以通过检测是否收到0字节响应来判断数据是否已经传输完毕了。(译注:这段应该是指HTTP没有启用Keep-Alive的情况)</p>
<p>但如果你想要复用这个socket,那就意味着你没法在socket上拿到像0字节数据这种传输结束标志的。这里再重复一遍:如果<code>send</code>和<code>recv</code>处理了0个字节后返回,那么这个连接就已经断开了,反之,如果连接没断开,那么你可能就要一直阻塞在<code>recv</code>操作上,socket并不会告诉你接下来已经没有数据了(目前来讲是这样的)。你可已经能想到了:在通信中,一个消息必须要么是固定长度的,要么是有明确边界的,或者是告诉了你这则消息的实际长度,或者就像HTTP协议那样,通过断开连接来告诉你消息已经结束了。这里面任意一个方案都是可行的(但有些方案相对更优一些)。</p>
<p>假如你不想使用断开连接这种方法的话,那么最简单的还是给消息设置一个固定长度。</p>
<figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">MySocket</span>:</span></span><br><span class="line"> <span class="string">"""demonstration class only</span></span><br><span class="line"><span class="string"> - coded for clarity, not efficiency</span></span><br><span class="line"><span class="string"> """</span></span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">def</span> <span class="title">__init__</span><span class="params">(self, sock=None)</span>:</span></span><br><span class="line"> <span class="keyword">if</span> sock <span class="keyword">is</span> <span class="literal">None</span>:</span><br><span class="line"> self.sock = socket.socket(</span><br><span class="line"> socket.AF_INET, socket.SOCK_STREAM)</span><br><span class="line"> <span class="keyword">else</span>:</span><br><span class="line"> self.sock = sock</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">def</span> <span class="title">connect</span><span class="params">(self, host, port)</span>:</span></span><br><span class="line"> self.sock.connect((host, port))</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">def</span> <span class="title">mysend</span><span class="params">(self, msg)</span>:</span></span><br><span class="line"> totalsent = <span class="number">0</span></span><br><span class="line"> <span class="keyword">while</span> totalsent < MSGLEN:</span><br><span class="line"> sent = self.sock.send(msg[totalsent:])</span><br><span class="line"> <span class="keyword">if</span> sent == <span class="number">0</span>:</span><br><span class="line"> <span class="keyword">raise</span> RuntimeError(<span class="string">"socket connection broken"</span>)</span><br><span class="line"> totalsent = totalsent + sent</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">def</span> <span class="title">myreceive</span><span class="params">(self)</span>:</span></span><br><span class="line"> chunks = []</span><br><span class="line"> bytes_recd = <span class="number">0</span></span><br><span class="line"> <span class="keyword">while</span> bytes_recd < MSGLEN:</span><br><span class="line"> chunk = self.sock.recv(min(MSGLEN - bytes_recd, <span class="number">2048</span>))</span><br><span class="line"> <span class="keyword">if</span> chunk == <span class="string">b''</span>:</span><br><span class="line"> <span class="keyword">raise</span> RuntimeError(<span class="string">"socket connection broken"</span>)</span><br><span class="line"> chunks.append(chunk)</span><br><span class="line"> bytes_recd = bytes_recd + len(chunk)</span><br><span class="line"> <span class="keyword">return</span> <span class="string">b''</span>.join(chunks)</span><br><span class="line">The sending code here <span class="keyword">is</span> usable <span class="keyword">for</span> almost any messagi</span><br></pre></td></tr></table></figure>
<p>这里的发送代码几乎适用于任何消息传递方案。在Python里你可以发送字符串并使用<code>send()</code>来获取其长度(即使里面包含有<code>\0</code>字符也不会有问题)。但很多情况下,负责接收的代码会更复杂一些。(在C语言中可能不会那么麻烦,除非消息已经嵌入<code>\0</code>,这种情况下不能使用strlen。)</p>
<p>一个最简单的改进方案是,把消息的第一个字符用来指示消息类型,进而根据类型来确定长度。这时候你需要执行两步<code>recv</code>操作,第一次至少获取一个字符,从而得到消息长度,接着循环获取消息剩余的部分。如果你打算采用分隔符的方案,你可以接收任意大小的数据块(对于网络缓冲来说,4096或者8192是一个较为常用以及合适的尺寸),然后在接收到的数据里查找你定好的分隔符。</p>
<p>需要注意的一个复杂情况是:如果您的会话协议允许多个消息被发送到后面(没有某种类型的应答),并且您向<code>recv</code>传递了一个任意的块大小,那么您最终可能会读取下面消息的开头。<br>你需要把它放在一边,直到需要的时候再拿出来。</p>
<p>在消息头部声明其长度(比如用5位数字字符)这种方法会更复杂,因为你可能没法在一次<code>recv</code>中完全接收到这5个字符(信不信由你)。如果只是平时随便写着玩的,你可能不会遇到这种情况,但在高网络负载的情况下,你的代码就会很快出问题,除非你用两个循环来分别获取长度和消息剩余的数据部分。当进行<code>send</code>操作时,你会发现也有这种令人讨厌的情况,<code>send</code>也并不总是在一次传输中就处理完所有数据。即使读过这篇文章,知道这么一回事了,但你迟早还是会被这个问题坑到的。</p>
<h4 id="二进制数据"><a href="#二进制数据" class="headerlink" title="二进制数据"></a>二进制数据</h4><p>通过套接字传送二进制数据是可行的。主要问题在于并非所有机器都用同样的二进制数据格式。比如 Motorola 芯片用两个十六进制字节 00 01 来表示一个 16 位整数值 1。而 Intel 和 DEC 则会做字节反转 —— 即用 01 00 来表示 1。套接字库要求转换 16 位和 32 位整数 —— <code>ntohl, htonl, ntohs, htons</code>。 其中的<code>n</code>表示 network,<code>h</code>表示 host,<code>s</code>表示 short,<code>l</code>表示 long。在网络序列就是主机序列时它们什么都不做,但是如果机器是字节反转的则会适当地交换字节序。</p>
<p>在现今的 32 位机器中,二进制数据的 ascii 表示往往比二进制表示要小。这是因为在非常多的时候所有 long 的值均为 0 或者 1。字符串形式的 “0” 为两个字节,而二进制形式则为四个。当然这不适用于固定长度的信息。自行决定,请自行决定。</p>
<h4 id="断开连接"><a href="#断开连接" class="headerlink" title="断开连接"></a>断开连接</h4><p>严格地说,在断开(<code>close</code>)socket之前,需要先对其进行<code>shutdown</code>操作。<code>shutdown</code>相当于对socket另一侧的一个通知,根据你传递的参数,它可能会传达以下两种不同的信息:“我不会再发送数据了,但还能继续接收”,或者“我不再管你发过来的数据了,谢谢”。但大多数socket库都习惯了开发者会忽略这一规则,索性在<code>close</code>的时候,一并执行了真正意义上的<code>shutdown</code>和<code>close</code>操作。所以大多数情况下,并不需要明确地进行<code>shutdown</code>。</p>
<h4 id="socket何时销毁"><a href="#socket何时销毁" class="headerlink" title="socket何时销毁"></a>socket何时销毁</h4><p>当使用阻塞式socket的时候,可能遇到的最坏的情况是对面还没有执行<code>close</code>就突然挂掉了,这种时候你的、socket可能就会一直在那干等着。TCP是一个可靠的协议,这会导致它会等很长很长时间才放弃这个连接。如果你是用线程来处理的,那么这个线程实际上已经跟死掉没什么区别了。对这种情况还真没太多处理办法,但其实这个线程并不会消耗太多的资源,只要你别干什么蠢事,比如进行一个阻塞读取操作的时候又还占有着一个锁之类的。千万不要尝试去杀掉这个线程,线程之所以高效,其中一部分原因就是它们避免了与资源自动回收相关的开销。换句话说,如果你设法把线程给干掉了,那么你可能会把整个程序给搞砸掉。</p>
<h4 id="非阻塞式socket"><a href="#非阻塞式socket" class="headerlink" title="非阻塞式socket"></a>非阻塞式socket</h4><p>如果你已经把前面说的这些都弄明白了,那么就已经基本掌握了socket开发的技术要点。接下来你还是会去调用一些相同的接口,但是如果换一个更加正确的方式,你的应用会完全变了个样。</p>
<p>在Python中,可以通过<code>socket.setblocking(0)</code>来将一个socket设置为非阻塞的。在C语言里,想这么干会更复杂一些,(一方面,你需要在 BSD 风格的 <code>O_NONBLOCK</code> 和几乎无法区分的 Posix 风格的 <code>O_NDELAY</code> 之间做出选择,这与 <code>TCP_NODELAY</code> 完全不同。)但思路是相同的,先创建socket,然后在使用前变更设置。(如果你疯了的话,你也可以来回切换)</p>
<p>这样做带来的最主要差别就是,<code>send</code>、<code>recv</code>、<code>connect</code>以及<code>accept</code>这些操作可以不处理任何东西就进行返回。 你(当然)有很多选择,一种是你自己手动检查返回码和错误码,不过这个大概率会让你疯掉。不信的话,有时候你可以试试看。你的程序将会变得臃肿、易错而且非常废CPU。所以我们还是跳过这个及其费脑的方案,看下一个,一个更正确的方案。</p>
<p>那就是使用<code>select</code>。</p>
<p>在C语言中,<code>select</code>编程是相当复杂的,而Python中就简单多了,但它与 C 版本也足够接近,如果你在 Python 中理解 <code>select</code> ,那么在 C 中你会几乎不会遇到麻烦:</p>
<figure class="highlight python"><table><tr><td class="code"><pre><span class="line">ready_to_read, ready_to_write, in_error = \</span><br><span class="line"> select.select(</span><br><span class="line"> potential_readers,</span><br><span class="line"> potential_writers,</span><br><span class="line"> potential_errs,</span><br><span class="line"> timeout)</span><br></pre></td></tr></table></figure>
<p><code>select</code>一共接收三个列表:第一个包含的是那些你想读取数据的socket,第二个是你想写入数据的,第三个(通常是空的)是出错了的,用于排查。需要注意的一点是,一个socket是可以进入上面多个列表的。<code>select</code>操作本身是阻塞的,不过你可以给它设置一个合理的超时期限(比如1分钟),这是个更明智的选择,除非你有充分的理由不这么做。</p>
<p><code>select</code>也会返回给我们三个列表,它们分别包含了可读的、可写的以及报错的socket。对应你上面传入的三个列表,返回的列表都是相应列表的子集(也可能是空的)。</p>
<p>如果一个socket出现在了返回的可读socket列表里,那么你可以认为,对这个socket进行<code>recv</code>操作必定能返回些什么。同理,在可写入列表里,你也可以对socket进行发送数据的操作。可能你想要接收/发送的是全部数据,但有总比没有好。(事实上,任何健康的socket都是可写入的,代表的就是网络缓冲区已经可用了而已)</p>
<p>如果你手里有一个服务端socket,可用把它放到上面的<code>potential_readers</code>列表里,当它变成可读状态的时候,你就可用对它进行<code>accept</code>操作了。而如果你创建了一个新的socket,并尝试与其他人进行<code>connect</code>,那么可以把它放进<code>potential_writers</code>里面,当这个socket变为可写入状态的时候,就说明已经连接成功了。</p>
<p>实际上,<code>select</code>对于阻塞式socket也是很好用的。这是判断是否阻塞的一种方法——当缓冲区中有内容时,socket作为可读状态返回。然而,这仍然无法帮助确定另一端是否已经完成,或者只是忙于其他事情。</p>
<p><strong>可移植性警告:</strong>在Unix平台,<code>select</code>对socket和文件都有用,但在Windows平台不要尝试这么做。Windows下,<code>select</code>仅对socket起作用。在C语言中也同样要注意,很多socket的高级功能在Windows上是以不同方式去实现的。事实上,在Windows平台我通常是用线程来处理socket的(效果非常,非常的好)。</p>
]]></content>
<categories>
<category>技术</category>
</categories>
<tags>
<tag>Python</tag>
<tag>网络</tag>
<tag>翻译</tag>
</tags>
</entry>
<entry>
<title>Git与VS Code笔记</title>
<url>/2017/02/15/Git%E4%B8%8EVS-Code%E7%AC%94%E8%AE%B0/</url>
<content><![CDATA[<h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><blockquote>
<p>本文仅作笔记自用,不作参考</p>
</blockquote>
<p> 由于平时没怎么有用Git的习惯,每次用的时候才去现查,结果到头来就记得init/add/commit这几个基础的不能再基础的命令。另一方面虽然用了有一段时间VSCode了,知道它自带Git支持,却因为连Git都不怎么会用,就懒得去了解这个功能了。<br> 这几天又要用到Git,实在觉得有点烦了,决定把Git的基本功能都过一遍,顺带研究下VSCode的Git支持,写个博文记录一下,免得以后又要查老半天。 </p>
<a id="more"></a>
<h1 id="Git"><a href="#Git" class="headerlink" title="Git"></a>Git</h1><p> 本来是想着系统地学习一下Git的,然后发现这东西远比我想象的要复杂,就怂了,记录一下比较常用的场景和命令就行了。</p>
<h2 id="创建修改提交"><a href="#创建修改提交" class="headerlink" title="创建修改提交"></a>创建修改提交</h2><p>很基本的命令,没什么好说的</p>
<figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">git init</span><br><span class="line">git add file</span><br><span class="line">git commit -m "update filename"</span><br><span class="line">git log --pretty=oneline //参数是为了简化</span><br><span class="line">git status</span><br></pre></td></tr></table></figure>
<p>关于工作区/暂存区/版本库的区分,我觉得购物车对暂存区是一个很形象的比喻。 </p>
<p><img src="/img/gitvscode/0.jpg" alt=""></p>
<h2 id="版本跳转"><a href="#版本跳转" class="headerlink" title="版本跳转"></a>版本跳转</h2><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">git reset --hard HEAD~ // 跳回上一版本</span><br><span class="line">git reset --hard commit_id // 跳回指定版本号</span><br><span class="line">git log // 查看提交历史</span><br><span class="line">git reflog // 更全,包括reset的记录</span><br></pre></td></tr></table></figure>
<h2 id="管理修改"><a href="#管理修改" class="headerlink" title="管理修改"></a>管理修改</h2><p>针对修改文件,撤销修改,删除文件等情况</p>
<figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">git diff HEAD -- file //查看工作区与版本库区别</span><br><span class="line">git checkout -- file //撤销工作区的修改</span><br><span class="line">git reset HEAD file //将暂存区移回工作区</span><br><span class="line"></span><br><span class="line">rm file //删除文件后</span><br><span class="line">git rm file //类似于git add</span><br><span class="line">git commit -m "rm" //提交</span><br><span class="line"></span><br><span class="line">git checkout -- test.txt //误删后从版本库恢复</span><br></pre></td></tr></table></figure>
<!-- git checkout其实是用版本库里的版本替换工作区的版本,无论工作区是修改还是删除,都可以“一键还原”。 git add以后修改呢?? -->
<h2 id="远程仓库"><a href="#远程仓库" class="headerlink" title="远程仓库"></a>远程仓库</h2><p> 通常来讲就是各大代码托管平台了,像国外的Gayhub,国内的Coding等等,我的博客就分别托管在这两个平台上,对国内外IP进行分别解析访问。当然也可以在自己服务器上搭建远程仓库,由于我个人暂时没这个需求,就不研究了。</p>
<figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">//ssh密钥,部署代码用</span><br><span class="line">ssh-keygen -t rsa -C "youremail@example.com"</span><br><span class="line"></span><br><span class="line">git clone gitURL //将某仓库复制到本地</span><br><span class="line">git remote add origin url //绑定远程仓库</span><br><span class="line">git push -u origin master //推送到远程主分支</span><br><span class="line">git remote -v //查看远程仓库推送方式</span><br></pre></td></tr></table></figure>
<blockquote>
<p>clone下来的仓库会自动绑定远程仓库<br>使用http推送会一直需要口令</p>
</blockquote>
<h2 id="管理分支"><a href="#管理分支" class="headerlink" title="管理分支"></a>管理分支</h2><p>Git的分支系统比较复杂,我仅仅是一知半解,所以只记录一下对我比较常用的场景和命令。</p>
<figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">git checkout -b branch1 //创建并切换分支</span><br><span class="line">git checkout master //切换分支</span><br><span class="line">git merge branch1 //合并到当前分支</span><br><span class="line"></span><br><span class="line">git branch --all //查看分支</span><br><span class="line">git branch -d branch1 //删除分支 -D强制删除</span><br></pre></td></tr></table></figure>
<p>合并分支有个–no-off参数,我没太搞明白具体意义,回头再看看:<a href="https://www.liaoxuefeng.com/wiki/0013739516305929606dd18361248578c67b8067c8c017b000/0013758410364457b9e3d821f4244beb0fd69c61a185ae0000" target="_blank" rel="noopener">分支管理策略</a></p>
<h2 id="处理冲突"><a href="#处理冲突" class="headerlink" title="处理冲突"></a>处理冲突</h2><p>创建分支后,若两边都进行了commit,merge的时候可能会产生冲突,需要手动修改解决。冲突部分会在文件里标出来,像这样:</p>
<figure class="highlight plain"><table><tr><td class="code"><pre><span class="line"><<<<<<< HEAD</span><br><span class="line">Creating a new branch is quick & simple.</span><br><span class="line">=======</span><br><span class="line">Creating a new branch is quick AND simple.</span><br><span class="line">>>>>>>> branch1</span><br></pre></td></tr></table></figure>
<p>把这整一部分改成最终的即可(包括<<<>>>这几行),然后add并commit,就完成merge了。具体参考:<a href="https://www.liaoxuefeng.com/wiki/0013739516305929606dd18361248578c67b8067c8c017b000/001375840202368c74be33fbd884e71b570f2cc3c0d1dcf000" target="_blank" rel="noopener">解决冲突</a></p>
<h2 id="远程分支"><a href="#远程分支" class="headerlink" title="远程分支"></a>远程分支</h2><p>如果远程仓库后在clone后新建了分支,需要先用git pull把新分支拉下来。</p>
<figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">git pull</span><br><span class="line">git branch --all //查看所有分支,包括远程的</span><br></pre></td></tr></table></figure>
<p>不过本地还是需要创建一个新分支,再绑定远程分支。</p>
<figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">git checkout -b branch1 origin/branch1</span><br><span class="line"></span><br><span class="line">...</span><br><span class="line"></span><br><span class="line">git push origin branch1</span><br></pre></td></tr></table></figure>
<p>另外如果远程仓库有其他人进行了push,那么就需要先pull下来再进行push,并且pull后有可能会与本地产生冲突。 </p>
<p>还有其他很多功能,命令,参数,我暂时就不写了,Git部分到此为止。</p>
<h1 id="VSCode-Git"><a href="#VSCode-Git" class="headerlink" title="VSCode Git"></a>VSCode Git</h1><p> 知道Git怎么用之后,VSCode的Git功能就没什么好说的了,一方面VSCode没有Git原生功能多,另一方面VSCode里直接集成了终端,可以在里面直接使用Git。不过借助VSCode在一些交互上还是比在命令行里操作方便不少的。</p>
<h2 id="操作命令"><a href="#操作命令" class="headerlink" title="操作命令"></a>操作命令</h2><p>在VSCode里按下ctrl+shift+P,输入git,会看到VSCode支持的所有git命令。<br><img src="/img/gitvscode/1.jpg" alt=""></p>
<p>与命令行不同,VSCode里与git add相对应的是git stage,功能比较接近。<br><img src="/img/gitvscode/2.jpg" alt=""></p>
<p>然后就是branch/checkout/commit这些,在ctrl+shift+P输入对应的关键词,就可以看到相关的命令,并且都有具体的解释,没什么好介绍的。</p>
<h2 id="冲突合并"><a href="#冲突合并" class="headerlink" title="冲突合并"></a>冲突合并</h2><p>VS Code 会检测文件冲突,并以<<<<<,>>>>,====和颜色区分出来。<br><img src="/img/gitvscode/3.jpg" alt=""></p>
<h2 id="更改比较"><a href="#更改比较" class="headerlink" title="更改比较"></a>更改比较</h2><p>在git文件列表中,单击一个未提交更改的文件,就会打开两个窗口来显示变更的内容。<br><img src="/img/gitvscode/4.jpg" alt=""></p>
<p>总之就写这么多了,上面这些基本足够我日常使用了(大概),后续如果发现了什么更好的功能再做补充,以上。</p>
<p>参考资料:</p>
<blockquote>
<p><a href="https://www.liaoxuefeng.com/wiki/0013739516305929606dd18361248578c67b8067c8c017b000" target="_blank" rel="noopener">参考资料</a></p>
<p><a href="http://www.cnblogs.com/xuanhun/p/6019038.html?utm_source=tuicool&utm_medium=referral" target="_blank" rel="noopener">Visual Studio Code 使用Git进行版本控制</a></p>
</blockquote>
]]></content>
<categories>
<category>技术</category>
</categories>
<tags>
<tag>Git</tag>
<tag>VSCode</tag>
</tags>
</entry>
<entry>
<title>关于V社游戏开头的Logo以及Mr.Valve</title>
<url>/2017/01/21/Valve-logo/</url>
<content><![CDATA[<blockquote class="blockquote-center"><big><b>Open your mind , Open your eyes</b></big></blockquote>
<p> 如果你玩过V社的游戏的话,那么下图右边这位吴克你应该会有点面熟,在V社知名游戏Dota2,传送门系列,以及求生之路系列游戏里的Logo都有他的背影。<br><img src="/img/valve/1.png" class="full-image" /></p>
<a id="more"></a>
<p> 根据V社给的说法,右边的吴克就是我们所说的Mr.Valve中的其中一位,同时左边这个山羊胡子大叔也是Mr.Valve之一,他们两个人都作为模特出现在V社的游戏开场Logo上而被人们所熟知。<br> 他们眼睛和脑袋上的阀门分别寓意为 “Open your mind. Open your eyes” —— 放眼未来,自由想象。关于这两位模特的身份,据Reddit论坛上某位V社员工所说,当初V社对相关机构为他们提供的传统高颜值模特不满意,于是要求员工到大街上找来更多有趣的普通路人作为模特。最后从众多路人中选出了我们现在看到的这两位。所以并不是像网传的那样随便在大街上拉了个人就拍成logo了,虽然区别不大。同时V社似乎并没有将他们的身份信息记录下来,所以如今这两个人的真实身份应该也是个谜了。</p>
<p> 但实际上,在更早的时候,第一位 Mr.Valve 尽管是以右图的模特为原型,但并不是以真人照片的形式出现的。在1998年GoldSrc引擎的一次功能演示中,V社以他为原型在演示地图中建立了一个人物模型,模型由Steve Theodore设计,地图由Greg Coomer设计。模型如下图所示,脑袋上安装了一个阀门,其寓意正是后来用于Logo2的口号:”Open your mind.”这应该算是Logo最初始的原型了,尽管几乎没有被使用过。<br><br><img src="/img/valve/2.gif" alt=""><br> 后来这个原型终究还是被上面的左图所取代,并作为Logo1,并很快在 《半条命:第一天》 与 《半条命》中使用。这是Logo1的初版,其最后一次出现是在Codename Gordon中,一个半条命横版过关游戏。<br> 随后在2004年该Logo在半条命2中迎来了一个更新。V社将其原本偏黄的色调改为近乎黑白,只留下鲜红的阀门,并改进了帧数,在边缘处添加了一些新的抖动效果。这个版本被一直使用到了2006年,最后一次出现是在Episode One中。更新后的效果如下图GIF所示:<br><br><img src="/img/valve/3.gif" alt=""><br> 紧接着在2007年,Logo1在《橙盒》中被Logo2所取代,同时进一步添加了一些抖动特效。尽管这两个镜头都是在九十年代后期制作的,但Logo2直到2007年才首次在游戏中出现,在此之前,该Logo仅仅在V社官网中才能看到。<br><br><img src="/img/valve/4.gif" alt=""><br>然后就是第三个Logo了,也就是那个给不少人留下阴影的扭头动画。<br><br><img src="/img/valve/5.gif" alt=""><br>2011年,Dota2 Beta版发布,这个新Logo正式面世,具体效果为图中的吴克慢慢的把脸朝向镜头,但很快又转了回去,随后V社的图标出现。估计很多人都以为仅仅是在Logo2的基础上添加了一个扭头的特效而已,但实际上这个新的动画效果是由V社新请的模特拍摄的。Logo3在CSGO beta中使用过一段时间,但后来又换回Logo2了,在传送门2里也使用了这个新Logo。</p>
<p>以上这些就是Mr.Valve的相关历史了,绝大部分资料翻译自wiki:<a href="http://combineoverwiki.net/wiki/Mr._Valve" target="_blank" rel="noopener">Mr.Valve词条</a><br><br>另外给下Reddit上那个帖子的地址:<a href="https://www.reddit.com/r/gaming/comments/guw36/the_guy_in_the_valve_splash_screen/" target="_blank" rel="noopener">查看原帖</a><br><br>历代Mr.Valve视频(Youtube):<a href="https://www.youtube.com/watch?v=IRyAuSE949w" target="_blank" rel="noopener">Every Valve Logo (1998-2012)</a> </p>
<p>不敢保证翻译的完全正确,如有错误欢迎指正,感谢阅读。未经同意,请勿转载。</p>
]]></content>
<categories>
<category>游戏</category>
</categories>
<tags>
<tag>Steam</tag>
<tag>Valve</tag>
</tags>
</entry>
<entry>
<title>VPS贴吧云签到搭建教程 基于Ubuntu 14.04</title>
<url>/2017/01/19/VPS%E8%B4%B4%E5%90%A7%E4%BA%91%E7%AD%BE%E5%88%B0%E6%90%AD%E5%BB%BA%E6%95%99%E7%A8%8B-Ubuntu-14-04/</url>
<content><![CDATA[<p><img src="/img/15.png" alt=""><br> 之前在新浪云上也搭过一个云签到,因为懒的原因一直没想把它迁移到自己的服务器上,奈何新浪后来改了扣费标准,云豆的消耗翻了一番,所以这几天抽空在服务器上搭了一个,顺便升到了最新版。<br> 安装之前在网上搜了一下,发现云签到的相关教程基本上都是基于各种PaaS平台,基于VPS搭建的屈指可数,找到的也不够详细。所以搭建完成以后决定写下整个流程,一方面是供别人参考,一方面是做个记录,以及给自己博客填充点东西。。<br> 开始之前先强调一下,本流程并不保证能完全正确,跟你的实际环境很可能会有出入,所以遇到问题请善用搜索。</p>
<a id="more"></a>
<h1 id="准备工作"><a href="#准备工作" class="headerlink" title="准备工作"></a>准备工作</h1><p>整个搭建流程需要具备以下这些东西: </p>
<ol>
<li>VPS , Ubuntu系统 </li>
<li>apache2 , php , mysql , phpmyadmin </li>
<li><a href="http://www.liaoxuefeng.com/wiki/0013739516305929606dd18361248578c67b8067c8c017b000" target="_blank" rel="noopener">Git 工具</a> </li>
<li><a href="https://github.com/MoeNetwork/Tieba-Cloud-Sign" target="_blank" rel="noopener">云签到源码</a> </li>
<li>基本的linux命令行操作知识</li>
</ol>
<p>附:这一部分是我在比较早之前就配置过的了,所以是直接参照当初用的教程写的。</p>
<h2 id="安装apache2"><a href="#安装apache2" class="headerlink" title="安装apache2"></a>安装apache2</h2><p>在命令行输入: </p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="variable">$sudo</span> apt-get install apache2</span><br></pre></td></tr></table></figure>
<p>安装完成后运行如下命令重启一下: </p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="variable">$sudo</span> service apache2 restart</span><br></pre></td></tr></table></figure>
<p>在本地浏览器输入服务器IP(如果有域名就输入域名,下面同理),如果打开的是这样一个页面,显示”It works!”,那么就说明安装成功了。<br><img src="/img/1.png" alt=""><br>Apache2的默认文件目录是/var/www/html,浏览器所访问的初始文件index.html就在这里面。<br>附:Apache2本身的配置文件目录是 /etc/apache2 </p>
<h2 id="安装php"><a href="#安装php" class="headerlink" title="安装php"></a>安装php</h2><p>命令行输入:</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="variable">$sudo</span> apt-get install libapache2-mod-php5 php5</span><br></pre></td></tr></table></figure>
<p>此外建议安装扩展 php5-gd php5-mysql 安装方式同上(这里出自原参考文档,我也不确定自己当时装了没)<br>安装完成后再重启下apache2:</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="variable">$sudo</span> service apache2 restart</span><br></pre></td></tr></table></figure>
<p>接下来我们在html目录下新建一个test.php文件来测试是否能正常运行:</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="variable">$sudo</span> vi /var/www/html/test.php</span><br></pre></td></tr></table></figure>
<p>然后输入:</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><?php <span class="built_in">echo</span> <span class="string">"hello,world!"</span>?></span><br></pre></td></tr></table></figure>
<p>保存退出后,在本地浏览器输入:服务器IP/test.php,若能显示hello,world字样,则证明安装成功了。 </p>
<h2 id="安装MySQL数据库"><a href="#安装MySQL数据库" class="headerlink" title="安装MySQL数据库"></a>安装MySQL数据库</h2><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="variable">$sudo</span> apt-get install mysql-server mysql-client</span><br></pre></td></tr></table></figure>
<p>apt-get程序会自动下载安装最新的mysql版本。在安装的最后,它会要求里输入root的密码,注意,这里的root密码指的不是Ubuntu的root密码,是你要给MySQL设定的root密码。 </p>
<h2 id="安装phpmyadmin-Mysql数据库管理"><a href="#安装phpmyadmin-Mysql数据库管理" class="headerlink" title="安装phpmyadmin-Mysql数据库管理"></a>安装phpmyadmin-Mysql数据库管理</h2><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="variable">$sudo</span> apt-get install phpmyadmin</span><br></pre></td></tr></table></figure>
<p>phpmyadmin设置:<br>在安装过程中会要求选择Web server:apache2或lighttpd,使用空格键选定apache2,按tab键然后确定。然后会要求输入设置的Mysql数据库密码连接密码Password of the database’s administrative user。<br>然后将phpmyadmin与apache2建立连接,以我的为例:www目录在/var/www/html,phpmyadmin在/usr/share/phpmyadmin目录,所以就用命令:</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="variable">$sudo</span> ln -s /usr/share/phpmyadmin /var/www/html</span><br></pre></td></tr></table></figure>
<p>phpmyadmin测试:在浏览器地址栏中打开:服务器IP/phpmyadmin,这里后面搭建的时候还要用到。</p>
<h2 id="设置Ubuntu文件执行读写权限"><a href="#设置Ubuntu文件执行读写权限" class="headerlink" title="设置Ubuntu文件执行读写权限"></a>设置Ubuntu文件执行读写权限</h2><p> 组建安装好之后,PHP网络服务器根目录默认设置是在:/var/www。由于Linux系统的安全性原则,改目录下的文件读写权限是只允许root用户操作的,所以我们不能在www文件夹中新建php文件,也不能修改和删除,必须要先修改/var/www目录的读写权限。在界面管理器中通过右键属性不能修改文件权限,得执行root终端命令:</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="variable">$sudo</span> chmod 777 /var/www</span><br></pre></td></tr></table></figure>
<p>然后就可以写入html或php文件了。777是linux中的最高权限,表示可读,可写,可执行。 </p>
<h2 id="Git-安装"><a href="#Git-安装" class="headerlink" title="Git 安装"></a>Git 安装</h2><p>这个主要是为了能从GitHub上方便快捷的获取各种源码用的,如果你非要不装也不是不可以,能把源码下下来就行。<br>具体流程恕我不再详细说明了,内容较多,请自行查阅,<a href="http://www.liaoxuefeng.com/wiki/0013739516305929606dd18361248578c67b8067c8c017b000" target="_blank" rel="noopener">廖老师的教程</a>已经足够详细了。</p>
<p>至此,准备工作就算基本完成了。</p>
<h1 id="云签到安装"><a href="#云签到安装" class="headerlink" title="云签到安装"></a>云签到安装</h1><p>接下来可以开始搭建工作了,首先我们要到GitHub上面获取站点源码。</p>
<h2 id="下载源码"><a href="#下载源码" class="headerlink" title="下载源码"></a>下载源码</h2><p>通过Git命令可以很快速地下载到本地:</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="variable">$git</span> <span class="built_in">clone</span> https://github.com/MoeNetwork/Tieba-Cloud-Sign.git</span><br></pre></td></tr></table></figure>
<p>将代码放到 /var/www/html 目录下,记得将里面原来的index.html改名或删除。<br>注意:是将Tieba-Cloud-Sign里的文件放到该目录下,而不是Tieba-Cloud-Sign整个文件夹。</p>
<h2 id="修改config-php"><a href="#修改config-php" class="headerlink" title="修改config.php"></a>修改config.php</h2><p>找到根目录下的config.php文件,修改相关数据。不建议修改其它的,数据库名称默认就可,只设置好你的数据库的密码即可,最后一行填写乱码就可。<br><img src="https://dn-chinmax.qbox.me/wp-content/uploads/2015/10/tiebayunqiandaoconf.png" alt=""><br>(这里借下<a href="https://okwoo.com/build-baidu-cloud-vps-registration-tutorial" target="_blank" rel="noopener">原博</a>的图,懒得截自己的图打码了)</p>
<h2 id="安装数据库"><a href="#安装数据库" class="headerlink" title="安装数据库"></a>安装数据库</h2><p>为了安装过程顺利,我们最好手动安装数据库。首先在浏览器输入:服务器IP/phpmyadmin<br>成功进入后台之后,新建一个名字叫tiebacloud的数据库(与你在上一步中设定的数据库名称相同)。<br><br><img src="/img/9.png" alt=""></p>
<h2 id="开始安装"><a href="#开始安装" class="headerlink" title="开始安装"></a>开始安装</h2><p>在本地浏览器输入服务器IP,根据一系列提示开始安装。 </p>
<h3 id="点击“前往安装”"><a href="#点击“前往安装”" class="headerlink" title="点击“前往安装”"></a>点击“前往安装”</h3><p><img src="/img/10.png" alt=""></p>
<h3 id="阅读协议"><a href="#阅读协议" class="headerlink" title="阅读协议"></a>阅读协议</h3><p>(反正你也不会读)选择“我接受”。在弹出的对话框内,点击“确定”。<br><img src="/img/11.png" alt=""></p>
<h3 id="功能检查界面"><a href="#功能检查界面" class="headerlink" title="功能检查界面"></a>功能检查界面</h3><p>在这里我遇到了一个问题,印象中是不支持Curl导致无法签到之类的:<a href="http://jingyan.baidu.com/article/a681b0de39c47d3b1943467a.html" target="_blank" rel="noopener">解决方案</a><br>没什么问题的话就直接下一步<br><br><img src="/img/12.png" alt=""><br>附:虽然我上面说有问题,但我还是直接下一步了,后面搭建好运行时才又出了问题,用的是上面提供的解决方案。</p>
<h3 id="设置运行环境界面"><a href="#设置运行环境界面" class="headerlink" title="设置运行环境界面"></a>设置运行环境界面</h3><p>由于我们的VPS是Linux系统,选择“不,我不是”。<br><img src="/img/13.png" alt=""></p>
<h3 id="设置所需信息界面"><a href="#设置所需信息界面" class="headerlink" title="设置所需信息界面"></a>设置所需信息界面</h3><p>在自动获得数据库配置信息里,选择“是”。这里填的就是你之后登录用的管理员账号,填写相关信息后“下一步”。<br><img src="/img/14.png" alt=""></p>
<h3 id="安装完成"><a href="#安装完成" class="headerlink" title="安装完成"></a>安装完成</h3><p><img src="https://dn-chinmax.qbox.me/wp-content/uploads/2015/10/tiebayunqiandaoguide6.png" alt=""><br>然后如果又跳回一开始的界面,就按照它说的在setup目录下创建一个install.lock空文件即可。</p>
<h3 id="crontab定时设置"><a href="#crontab定时设置" class="headerlink" title="crontab定时设置"></a>crontab定时设置</h3><p>接着在服务器计划任务里添加do.php,命令行输入:</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">crontab -e</span><br></pre></td></tr></table></figure>
<p>然后在文件里添加这一行代码:</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">* * * * * /usr/bin/php -f /var/www/html/do.php</span><br></pre></td></tr></table></figure>
<p>保存退出,然后重启服务:</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="variable">$sudo</span> service cron restart</span><br></pre></td></tr></table></figure>
<p>改一下do.php的执行权限:</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="variable">$sudo</span> chmod +x /var/www/html/do.php</span><br></pre></td></tr></table></figure>
<h1 id="后续配置"><a href="#后续配置" class="headerlink" title="后续配置"></a>后续配置</h1><p>到这里就算是搭建完成了,剩下的就是对网站做一些设置,以及装一些插件(可选)。</p>
<h2 id="网站设置"><a href="#网站设置" class="headerlink" title="网站设置"></a>网站设置</h2><p>用管理员账号登录后,按照网站提示绑定百度账号,推荐用手动绑定。<br><br><img src="/img/2.png" alt=""><br>绑定账号之后,进入云签到设置,刷新自己的贴吧列表。<br><br><img src="/img/3.png" alt=""><br>在设置中心-签到设置里,保持默认即可。签到时间设置“0”,即从凌晨1点开始签到。<br><br><img src="/img/4.png" alt=""><br>如果要手动测试能否执行签到,在浏览器输入:服务器IP/do.php,会即刻执行。<br><img src="/img/5.png" alt=""></p>
<h2 id="安装插件和样式"><a href="#安装插件和样式" class="headerlink" title="安装插件和样式"></a>安装插件和样式</h2><p>接下来讲一下怎么给网站安装一些作者提供的插件:<a href="http://git.oschina.net/kenvix/Tieba-Cloud-Sign/wikis/%E8%B4%B4%E5%90%A7%E4%BA%91%E7%AD%BE%E5%88%B0%E6%8F%92%E4%BB%B6%E5%BA%93" target="_blank" rel="noopener">插件库</a><br><br><img src="/img/6.png" alt=""><br>这里我们以更换背景插件为例:<br><br><img src="/img/7.png" alt=""><br>获取插件git链接后,进入plugins目录:</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="variable">$cd</span> /var/www/html/plugins</span><br><span class="line"><span class="variable">$sudo</span> git <span class="built_in">clone</span> https://github.com/chajianku/mok_bgimg.git</span><br></pre></td></tr></table></figure>
<p>如果你没有安装Git的话,就直接下下来放到plugins文件夹里。<br>安装完成后进入插件管理页面就可以看到新装的插件了:<br><img src="/img/8.png" alt=""><br>然后安装并激活插件就可以了,样式的安装方法同理。</p>
<h1 id="结尾"><a href="#结尾" class="headerlink" title="结尾"></a>结尾</h1><p>至此,整个流程就算是结束了,如果有什么不对的地方欢迎指正。 </p>
<p>转载请标明出处,谢谢。 </p>
<p>参考资料:</p>
<blockquote>
<p><a href="https://okwoo.com/build-baidu-cloud-vps-registration-tutorial" target="_blank" rel="noopener">VPS搭建百度贴吧云签到详细教程</a></p>
<p><a href="http://www.cnblogs.com/lynch_world/archive/2012/01/06/2314717.html" target="_blank" rel="noopener">ubuntu下安装Apache+PHP+Mysql</a></p>
<p><a href="http://www.jb51.net/article/49983.htm" target="_blank" rel="noopener">linux使用crontab实现PHP执行计划定时任务</a></p>
</blockquote>
]]></content>
<categories>
<category>技术</category>
</categories>
<tags>
<tag>Ubuntu</tag>
<tag>云签到</tag>
</tags>
</entry>
<entry>
<title>Hello World</title>
<url>/2017/01/16/hello-world/</url>
<content><![CDATA[<p>Welcome to <a href="https://hexo.io/" target="_blank" rel="noopener">Hexo</a>! This is your very first post. Check <a href="https://hexo.io/docs/" target="_blank" rel="noopener">documentation</a> for more info. If you get any problems when using Hexo, you can find the answer in <a href="https://hexo.io/docs/troubleshooting.html" target="_blank" rel="noopener">troubleshooting</a> or you can ask me on <a href="https://github.com/hexojs/hexo/issues" target="_blank" rel="noopener">GitHub</a>.</p>
<h2 id="Quick-Start"><a href="#Quick-Start" class="headerlink" title="Quick Start"></a>Quick Start</h2><h3 id="Create-a-new-post"><a href="#Create-a-new-post" class="headerlink" title="Create a new post"></a>Create a new post</h3><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">$ hexo new <span class="string">"My New Post"</span></span><br></pre></td></tr></table></figure>
<p>More info: <a href="https://hexo.io/docs/writing.html" target="_blank" rel="noopener">Writing</a></p>
<h3 id="Run-server"><a href="#Run-server" class="headerlink" title="Run server"></a>Run server</h3><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">$ hexo server</span><br></pre></td></tr></table></figure>
<p>More info: <a href="https://hexo.io/docs/server.html" target="_blank" rel="noopener">Server</a></p>
<h3 id="Generate-static-files"><a href="#Generate-static-files" class="headerlink" title="Generate static files"></a>Generate static files</h3><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">$ hexo generate</span><br></pre></td></tr></table></figure>
<p>More info: <a href="https://hexo.io/docs/generating.html" target="_blank" rel="noopener">Generating</a></p>
<h3 id="Deploy-to-remote-sites"><a href="#Deploy-to-remote-sites" class="headerlink" title="Deploy to remote sites"></a>Deploy to remote sites</h3><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">$ hexo deploy</span><br></pre></td></tr></table></figure>
<p>More info: <a href="https://hexo.io/docs/deployment.html" target="_blank" rel="noopener">Deployment</a></p>
]]></content>
</entry>
</search>