-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathatom.xml
533 lines (288 loc) · 163 KB
/
atom.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
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
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<title>Mind Walker</title>
<link href="/atom.xml" rel="self"/>
<link href="https://njuwuyuxin.github.io/"/>
<updated>2020-07-03T04:36:57.323Z</updated>
<id>https://njuwuyuxin.github.io/</id>
<author>
<name>吴雨昕</name>
</author>
<generator uri="http://hexo.io/">Hexo</generator>
<entry>
<title>从零实现HTTP服务器——Minihttpd(四)——半连接半反应堆线程池</title>
<link href="https://njuwuyuxin.github.io/2020/07/03/%E4%BB%8E%E9%9B%B6%E5%AE%9E%E7%8E%B0HTTP%E6%9C%8D%E5%8A%A1%E5%99%A8%E2%80%94%E2%80%94Minihttpd%EF%BC%88%E5%9B%9B%EF%BC%89%E2%80%94%E2%80%94%E5%8D%8A%E8%BF%9E%E6%8E%A5%E5%8D%8A%E5%8F%8D%E5%BA%94%E5%A0%86%E7%BA%BF%E7%A8%8B%E6%B1%A0/"/>
<id>https://njuwuyuxin.github.io/2020/07/03/从零实现HTTP服务器——Minihttpd(四)——半连接半反应堆线程池/</id>
<published>2020-07-03T04:31:42.000Z</published>
<updated>2020-07-03T04:36:57.323Z</updated>
<content type="html"><![CDATA[<hr><p>在我们使用了epoll实现了上万并发请求的处理后,我们开始考虑程序中存在的另一瓶颈,即多线程处理请求时存在的问题。<br>在之前的代码中,当收到了客户端的一条请求后,我们是这样做的</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"> //处理客户连接上接收到的数据</span><br><span class="line">else if (events[i].events & EPOLLIN){</span><br><span class="line"> thread accept_thread(accept_request,sockfd,this);</span><br><span class="line"> accept_thread.detach();</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>每次收到一条请求时,我们都<strong>创建了一个新的线程</strong>,去执行这条请求的处理。</p><ul><li>对于低并发量的情况,同时I/O操作密集型的线程函数,这样的方式还基本可以接受,因为此时程序运行效率的瓶颈主要在I/O相关操作上,此时为每个请求创建一个新的线程是可以接受的。</li><li>但是当遇到高并发请求,同时每个线程函数执行的内容相对简单,为CPU密集型函数时,每次创建新的线程就会产生非常明显的效率问题——CPU大量时间用于线程创建和线程切换上。</li></ul><p>为了解决这个问题,一个经典的方案是使用“线程池”进行多线程操作,我们设定好线程池中初始线程数量,在初始化阶段让所有线程运行起来,避免反复创建线程、销毁线程造成的额外开销</p><h3 id="半连接半反应堆线程池"><a href="#半连接半反应堆线程池" class="headerlink" title="半连接半反应堆线程池"></a>半连接半反应堆线程池</h3><p>本项目中实现了一个半连接半反应堆线程池,其主要工作原理如下图<br><img src="https://upload-images.jianshu.io/upload_images/16734657-09d5ea66198dac77.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" alt="半连接半反应堆线程池"><br>实际上就是主线程用于接受客户端请求,将所有请求放入请求队列中(该队列对所有工作线程可见),各个工作线程以竞争方式读取工作队列中的请求进行处理。<br>这里主要涉及到线程同步的问题,由于主线程和各个工作线程都需要对工作队列进行操作,因此需要保证同一时间只有一个线程对工作队列进行操作</p><h4 id="互斥锁"><a href="#互斥锁" class="headerlink" title="互斥锁"></a>互斥锁</h4><p>保证线程同步的一个基本方法是使用互斥锁,通过对关键区代码进行“加锁”,“解锁”操作,保证同一时间只有一个线程访问到关键区代码。当一个线程想要对关键区代码进行“加锁”操作,但该段代码已处于“锁定”状态,则该线程会被阻塞住,等待这段代码被释放后再获取操作权。</p><p>具体代码实现如下,这里为了便于线程管理,抽象出了一个线程池对象</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br></pre></td><td class="code"><pre><span class="line">//ThreadPool.h</span><br><span class="line">class ThreadPool{</span><br><span class="line">public:</span><br><span class="line"> ThreadPool(HttpServer* server, int workthread = 8);</span><br><span class="line"> void append(int sockfd); //把事件加入请求队列</span><br><span class="line"> void init(); //创建N个工作线程并运行</span><br><span class="line"> void init(int count); //手动指定创建N个工作线程并运行</span><br><span class="line"> static void work(ThreadPool* pool); //运行工作线程</span><br><span class="line">private:</span><br><span class="line"> HttpServer* http_server; //与之绑定的HttpServer对象</span><br><span class="line"> int thread_count; //线程池线程数</span><br><span class="line"> queue<int> request_list; //请求队列</span><br><span class="line"> Sem request_list_sem; //请求队列信号量</span><br><span class="line"> mutex request_list_mutex; //请求队列互斥锁</span><br><span class="line"></span><br><span class="line"> void run(); //每个工作线程执行函数</span><br><span class="line">};</span><br><span class="line"></span><br><span class="line">//ThreadPool.cpp</span><br><span class="line">void ThreadPool::init(int count){</span><br><span class="line"> thread_count = count;</span><br><span class="line"> for(int i=0;i<thread_count;i++){</span><br><span class="line"> thread work_thread(ThreadPool::work,this);</span><br><span class="line"> work_thread.detach();</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">void ThreadPool::run(){</span><br><span class="line"> while(1){</span><br><span class="line"> request_list_mutex.lock();</span><br><span class="line"> if(request_list.empty()){</span><br><span class="line"> request_list_mutex.unlock();</span><br><span class="line"> continue;</span><br><span class="line"> }</span><br><span class="line"> int sockfd = request_list.front();</span><br><span class="line"> request_list.pop();</span><br><span class="line"> request_list_mutex.unlock();</span><br><span class="line"> HttpServer::accept_request(sockfd,http_server); //请求处理</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>在init函数初始化阶段创建N个线程并设为分离态,使各工作线程开始运作。<br>每个工作线程循环读取请求队列,同时进行加解锁操作保证线程同步,之后进行相应的请求处理。<br>至此我们实现了基本的,多个工作线程以竞争方式处理请求的线程池。</p><h3 id="存在问题"><a href="#存在问题" class="headerlink" title="存在问题"></a>存在问题</h3><p>使用线程池代替了每次创建线程的操作后,使用压力测试进行性能检验,却发现在面对高并发请求时,使用这样的线程池,<strong>反而大大的降低了程序的吞吐量,造成了严重的性能问题</strong></p><p>在每次创建线程池,面对上万并发请求时,其吞吐量大约为80w QPS左右,但使用线程池后,吞吐量骤降为8w QPS左右,降低了整整一个数量级。</p><p>重新审视我们实现线程池的代码,发现了一个非常明显的问题:<br><strong>当请求队列为空时,各个工作线程持续不断的对请求队列进行加锁、解锁操作,同时与主线程发生竞争,导致工作队列长时间被工作线程抢夺,却并未执行有意义的操作。而主线程却请求队列被阻塞而无法把新的请求添加入队列。</strong><br>为了解决这个问题,这里使用了信号量机制</p><h4 id="信号量"><a href="#信号量" class="headerlink" title="信号量"></a>信号量</h4><p>使用信号量机制可以实现一个简单的“生产者——消费者”模型,其工作流程主要是:</p><ul><li>当主线程向请求队列中加入请求时,使信号量+1</li><li>工作线程每次循环首先使用sem_wait 等待信号量,若信号量=0,则代表队列中无请求需要处理,此时线程睡眠。若信号量>0,则线程被唤醒,同时信号量-1,代表消耗掉一个请求。</li></ul><p>使用这样一个“生产者——消费者”模型,可以实现在请求队列为空时,各工作线程处于休眠态,避免不必要的竞争和阻塞。而当有请求需要处理时,又可以将工作线程唤醒进行工作。</p><p>具体代码实现也非常简单,其中Sem为本文对c原生semaphore操作进行的封装类</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line">//主线程调用,把新的请求加入请求队列</span><br><span class="line">void ThreadPool::append(int sockfd){</span><br><span class="line"> request_list_mutex.lock();</span><br><span class="line"> request_list.push(sockfd);</span><br><span class="line"> request_list_mutex.unlock();</span><br><span class="line"> request_list_sem.post(); //信号量+1</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">void ThreadPool::run(){</span><br><span class="line"> while(1){</span><br><span class="line"> request_list_sem.wait(); //等待信号量>0,并消耗</span><br><span class="line"> request_list_mutex.lock();</span><br><span class="line"> if(request_list.empty()){</span><br><span class="line"> request_list_mutex.unlock();</span><br><span class="line"> continue;</span><br><span class="line"> }</span><br><span class="line"> int sockfd = request_list.front();</span><br><span class="line"> request_list.pop();</span><br><span class="line"> request_list_mutex.unlock();</span><br><span class="line"> HttpServer::accept_request(sockfd,http_server);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>此时使用Webbench进行压力测试,测试10000并发请求时,测试结果显示,此时吞吐量约为250w QPS,其效率相比单纯用互斥锁进行同步有了极大提升,相比每次创建线程也有了明显提升。<br><img src="https://upload-images.jianshu.io/upload_images/16734657-6e536bfc401efe83.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" alt="压力测试结果"></p><ul><li>运行测试时,服务器启用32个工作线程,具体线程数需要针对服务器cpu核心数,I/O操作和CPU操作的时间占比等进行制定。</li></ul><h3 id="Github"><a href="#Github" class="headerlink" title="Github"></a>Github</h3><p><a href="https://github.com/njuwuyuxin/MiniHttpd" target="_blank" rel="noopener">https://github.com/njuwuyuxin/MiniHttpd</a></p>]]></content>
<summary type="html">
<hr>
<p>在我们使用了epoll实现了上万并发请求的处理后,我们开始考虑程序中存在的另一瓶颈,即多线程处理请求时存在的问题。<br>在之前的代码中,当收到了客户端的一条请求后,我们是这样做的</p>
<figure class="highlight plain"><tabl
</summary>
<category term="从零开始" scheme="https://njuwuyuxin.github.io/categories/%E4%BB%8E%E9%9B%B6%E5%BC%80%E5%A7%8B/"/>
<category term="后端" scheme="https://njuwuyuxin.github.io/tags/%E5%90%8E%E7%AB%AF/"/>
<category term="http" scheme="https://njuwuyuxin.github.io/tags/http/"/>
<category term="计算机网络" scheme="https://njuwuyuxin.github.io/tags/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/"/>
</entry>
<entry>
<title>从零实现HTTP服务器——Minihttpd(三)——使用epoll实现高并发</title>
<link href="https://njuwuyuxin.github.io/2020/06/29/%E4%BB%8E%E9%9B%B6%E5%AE%9E%E7%8E%B0HTTP%E6%9C%8D%E5%8A%A1%E5%99%A8%E2%80%94%E2%80%94Minihttpd%EF%BC%88%E4%B8%89%EF%BC%89%E2%80%94%E2%80%94%E4%BD%BF%E7%94%A8epoll%E5%AE%9E%E7%8E%B0%E9%AB%98%E5%B9%B6%E5%8F%91/"/>
<id>https://njuwuyuxin.github.io/2020/06/29/从零实现HTTP服务器——Minihttpd(三)——使用epoll实现高并发/</id>
<published>2020-06-29T15:17:08.000Z</published>
<updated>2020-06-29T15:21:46.851Z</updated>
<content type="html"><![CDATA[<hr><p>在实现了基本的接受请求,返回响应这一基本功能后,我们尝试提高该服务器能同时处理的并发请求数,实现面对海量请求时的高并发处理,主要使用了linux下的epoll机制。本文主要对epoll的基本原理进行讲解,同时展示epoll简单的使用方法。</p><h2 id="epoll"><a href="#epoll" class="headerlink" title="epoll"></a>epoll</h2><p>linux下的多路复用IO接口主要有select、poll和epoll,其中epoll是对select和poll的改进。所谓多路复用IO接口,就是当需要处理大批量文件描述符时使用的系统接口。文件、管道IO、socket等均使用了文件描述符,而epoll因为在处理大批量文件描述符时的高效性而收到了广泛的应用。</p><h2 id="epoll高效的原因"><a href="#epoll高效的原因" class="headerlink" title="epoll高效的原因"></a>epoll高效的原因</h2><p>传统的select、poll方式,通过维护一个文件描述符数组,将所有的文件描述符统一管理,而在某一时刻,某一文件描述符可能有多种状态:缓冲区有内容可读、缓冲区有内容可写、空闲等等情况。而传统方式每次返回所有文件描述符,对所有描述符进行轮询处理,判断哪些描述符被“激活”需要进行处理。<br>显然,使用轮询的方式,其算法复杂度为O(n)级别,也就是说随着文件描述符的增长,其耗时也为线性增长,这就导致其难以处理高并发请求的情况(海量文件描述符),因此select方式限制了文件描述符的最大数量为1024.</p><p>而epoll最大的特点是通过epoll_wait函数,每次返回的是已就绪的文件描述符列表,而所有空闲的文件描述符并不进行返回。这首先避免了大量文件描述符从内核态拷贝到用户态内存的开销,同时避免了轮询请求大量无用的判断,其算法复杂度为O(1)级别。</p><h3 id="epoll的实现方式"><a href="#epoll的实现方式" class="headerlink" title="epoll的实现方式"></a>epoll的实现方式</h3><ul><li>epoll能够“选择性”的返回就绪态的文件描述符,主要依赖于其底层的数据结构。<br>epoll使用了一个红黑树维护所有文件描述符的集合,这为查询,插入,删除某一描述符提供了很大便利。</li><li>另外epoll使用了一个双向链表用于维护当前所有就绪态的文件描述符,每次调用epoll_wait函数时,就是将该双向链表中的文件描述符返回。</li><li>同时epoll使用了回调机制,在把文件描述符加入到epoll红黑树中的同时,注册了相应的一些事件(如收到消息),当某一描述符的事件被触发,则将该描述符加入至双向链表中。<br>通过这样的数据结构,使得epoll每次不必返回所有的文件描述符,降低了算法复杂度的同时节约了大量系统资源,在并发请求数线性增长的情况下,其复杂度并不会线性增加,从而轻松实现百万级别的高并发处理。</li></ul><p>epoll机制的详细解读可以参考这篇博客:<br><a href="https://blog.csdn.net/daaikuaichuan/article/details/83862311" target="_blank" rel="noopener">https://blog.csdn.net/daaikuaichuan/article/details/83862311</a></p><h3 id="使用案例"><a href="#使用案例" class="headerlink" title="使用案例"></a>使用案例</h3><p>在本文实现的http服务器中,我们尝试使用epoll修改底层处理逻辑,主要修改了 server类下的start_listen函数。</p><p><strong>epoll的核心api主要有三个</strong></p><ul><li>int epoll_create(int size)<br>用于初始化epoll描述符,参数为epoll事件列表最大值(在linux内核2.6版本之后已弃用,可以忽略)</li><li>int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)<br>用于将某一描述符注册到epoll内核事件表中</li><li>int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout)<br>用于获取当前就绪的文件描述符</li></ul><p>对于这三个api的详细内容可以参考下面这篇博客:<br><a href="https://www.jianshu.com/p/31cdfd6f5a48" target="_blank" rel="noopener">https://www.jianshu.com/p/31cdfd6f5a48</a></p><p>需要注意的是,epoll支持LT、ET两种模式</p><ul><li>LT 水平触发,即当某一描述符就绪时,每次epoll_wait均会将其返回</li><li>ET 边缘触发,即当某一描述符由空闲转换为就绪时,epoll_wait将其返回一次,之后无视</li></ul><p>这两种模式类似数字信号中电平的高低,LT模式类似高电平时持续触发,而ET模式则在低电平转换成高电平这一“边缘”时触发一次。<br>其区别主要是LT模式可以不一次性将描述符缓冲区读完,下次epoll_wait仍然会将其返回可以继续读取。<br>而MT模式则必须一次性将缓冲区内容读完,否则即使仍有内容未读,epoll_wait也不会返回该描述符,造成内容丢失。<br><strong>epoll默认使用LT模式</strong></p><h3 id="demo"><a href="#demo" class="headerlink" title="demo"></a>demo</h3><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br></pre></td><td class="code"><pre><span class="line">void HttpServer::add_epoll_fd(int event_fd){</span><br><span class="line"> epoll_event event;</span><br><span class="line"> event.data.fd = event_fd;</span><br><span class="line"> event.events = EPOLLIN | EPOLLET | EPOLLRDHUP;</span><br><span class="line"> epoll_ctl(epollfd, EPOLL_CTL_ADD, event_fd, &event);</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">void HttpServer::start_listen(){</span><br><span class="line"> stringstream ss;</span><br><span class="line"> string s_port;</span><br><span class="line"> ss<<port;</span><br><span class="line"> ss>>s_port;</span><br><span class="line"> Log::log("Minihttpd running on port "+s_port,INFO);</span><br><span class="line"></span><br><span class="line"> epollfd = epoll_create(5);</span><br><span class="line"> add_epoll_fd(server_sock); //把监听socket加入内核事件表</span><br><span class="line"></span><br><span class="line"> epoll_event events[MAX_EVENT_NUMBER];</span><br><span class="line"> while(1){</span><br><span class="line"> int number = epoll_wait(epollfd, events, MAX_EVENT_NUMBER, -1);</span><br><span class="line"> stringstream s;</span><br><span class="line"> string s_number;</span><br><span class="line"> s<<number;</span><br><span class="line"> s>>s_number;</span><br><span class="line"> Log::log("current tcp links="+s_number,DEBUG);</span><br><span class="line"> for (int i = 0; i < number; i++){</span><br><span class="line"> int sockfd = events[i].data.fd;</span><br><span class="line"></span><br><span class="line"> //处理新到的客户连接</span><br><span class="line"> if (sockfd == server_sock){</span><br><span class="line"> int client_sock = -1;</span><br><span class="line"> struct sockaddr_in client_name;</span><br><span class="line"> socklen_t client_name_len = sizeof(client_name);</span><br><span class="line"> client_sock = accept(server_sock,</span><br><span class="line"> (struct sockaddr *)&client_name,</span><br><span class="line"> &client_name_len);</span><br><span class="line"> if (client_sock == -1){</span><br><span class="line"> Log::log("accept failed",ERROR);</span><br><span class="line"> continue;</span><br><span class="line"> }</span><br><span class="line"> add_epoll_fd(client_sock); //把客户端socket加入内核事件表</span><br><span class="line"> }</span><br><span class="line"> else if (events[i].events & (EPOLLRDHUP | EPOLLHUP | EPOLLERR)){</span><br><span class="line"> Log::log("epoll end",DEBUG);</span><br><span class="line"> //服务器端关闭连接</span><br><span class="line"> }</span><br><span class="line"> //处理客户连接上接收到的数据</span><br><span class="line"> else if (events[i].events & EPOLLIN){</span><br><span class="line"> thread accept_thread(accept_request,sockfd,this);</span><br><span class="line"> accept_thread.detach();</span><br><span class="line"> }</span><br><span class="line"> else if (events[i].events & EPOLLOUT){</span><br><span class="line"> Log::log("epoll out",DEBUG);</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> close(server_sock);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>其中epoll_event结构体结构为</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">typedef union epoll_data {</span><br><span class="line"> void *ptr; /* 指向用户自定义数据 */</span><br><span class="line"> int fd; /* 注册的文件描述符 */</span><br><span class="line"> uint32_t u32; /* 32-bit integer */</span><br><span class="line"> uint64_t u64; /* 64-bit integer */</span><br><span class="line">} epoll_data_t;</span><br><span class="line"></span><br><span class="line">struct epoll_event {</span><br><span class="line"> uint32_t events; /* 描述epoll事件 */</span><br><span class="line"> epoll_data_t data; /* 见上面的结构体 */</span><br><span class="line">};</span><br></pre></td></tr></table></figure><h3 id="Github"><a href="#Github" class="headerlink" title="Github"></a>Github</h3><p><a href="https://github.com/njuwuyuxin/MiniHttpd" target="_blank" rel="noopener">https://github.com/njuwuyuxin/MiniHttpd</a></p>]]></content>
<summary type="html">
<hr>
<p>在实现了基本的接受请求,返回响应这一基本功能后,我们尝试提高该服务器能同时处理的并发请求数,实现面对海量请求时的高并发处理,主要使用了linux下的epoll机制。本文主要对epoll的基本原理进行讲解,同时展示epoll简单的使用方法。</p>
<h2 id="
</summary>
<category term="从零开始" scheme="https://njuwuyuxin.github.io/categories/%E4%BB%8E%E9%9B%B6%E5%BC%80%E5%A7%8B/"/>
<category term="后端" scheme="https://njuwuyuxin.github.io/tags/%E5%90%8E%E7%AB%AF/"/>
<category term="http" scheme="https://njuwuyuxin.github.io/tags/http/"/>
<category term="计算机网络" scheme="https://njuwuyuxin.github.io/tags/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/"/>
</entry>
<entry>
<title>从零实现HTTP服务器——Minihttpd(二)</title>
<link href="https://njuwuyuxin.github.io/2020/06/28/%E4%BB%8E%E9%9B%B6%E5%AE%9E%E7%8E%B0HTTP%E6%9C%8D%E5%8A%A1%E5%99%A8%E2%80%94%E2%80%94Minihttpd%EF%BC%88%E4%BA%8C%EF%BC%89/"/>
<id>https://njuwuyuxin.github.io/2020/06/28/从零实现HTTP服务器——Minihttpd(二)/</id>
<published>2020-06-28T12:45:09.000Z</published>
<updated>2020-06-29T15:21:58.310Z</updated>
<content type="html"><![CDATA[<hr><p>上一篇中我们实现了接受浏览器的请求,并返回本地的网页给浏览器展示,接下来对该简单的功能进行下一步完善</p><h3 id="Content-Type"><a href="#Content-Type" class="headerlink" title="Content-Type"></a>Content-Type</h3><p>http响应头中非常重要的一个字段是Content-Type,它决定了浏览器如何解析返回的响应内容,如果该字段缺失则默认为text/html格式,因此我们上文返回的简单html网页并没有添加该字段,浏览器也能正常解析。<br>但是稍微复杂的前端页面都包含了css样式文件,JavaScript脚本文件等,如果不指定Content-Type字段,则浏览器无法正确解析这些文件(有些浏览器超强的兼容性可以一定程度上自动判断内容格式)<br>因此我们在返回本地的文件作为响应时,需要手动设置Content-Type字段,判断依据为文件的扩展名,这里使用了一个map维护扩展名与Content-Type映射关系。</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br></pre></td><td class="code"><pre><span class="line">void HttpResponse::init_content_type_map(){</span><br><span class="line"> content_type_map.insert(pair<string,string>("html","text/html"));</span><br><span class="line"> content_type_map.insert(pair<string,string>("htm","text/html"));</span><br><span class="line"> content_type_map.insert(pair<string,string>("shtml","text/html"));</span><br><span class="line"> content_type_map.insert(pair<string,string>("css","text/css"));</span><br><span class="line"> content_type_map.insert(pair<string,string>("js","text/javascript"));</span><br><span class="line"> content_type_map.insert(pair<string,string>("txt","text/plain"));</span><br><span class="line"> content_type_map.insert(pair<string,string>("js","text/javascript"));</span><br><span class="line"> content_type_map.insert(pair<string,string>("xml","text/xml"));</span><br><span class="line"></span><br><span class="line"> content_type_map.insert(pair<string,string>("ico","image/x-icon"));</span><br><span class="line"> content_type_map.insert(pair<string,string>("jpg","image/jpeg"));</span><br><span class="line"> content_type_map.insert(pair<string,string>("jpeg","image/jpeg"));</span><br><span class="line"> content_type_map.insert(pair<string,string>("jpe","image/jpeg"));</span><br><span class="line"> content_type_map.insert(pair<string,string>("gif","image/gif"));</span><br><span class="line"> content_type_map.insert(pair<string,string>("png","image/png"));</span><br><span class="line"> content_type_map.insert(pair<string,string>("tiff","image/tiff"));</span><br><span class="line"> content_type_map.insert(pair<string,string>("tif","image/tiff"));</span><br><span class="line"> content_type_map.insert(pair<string,string>("rgb","image/x-rgb"));</span><br><span class="line"></span><br><span class="line"> content_type_map.insert(pair<string,string>("mpeg","video/mpeg"));</span><br><span class="line"> content_type_map.insert(pair<string,string>("mpg","video/mpeg"));</span><br><span class="line"> content_type_map.insert(pair<string,string>("mpe","video/mpeg"));</span><br><span class="line"> content_type_map.insert(pair<string,string>("qt","video/quicktime"));</span><br><span class="line"> content_type_map.insert(pair<string,string>("mov","video/quicktime"));</span><br><span class="line"> content_type_map.insert(pair<string,string>("avi","video/x-msvideo"));</span><br><span class="line"> content_type_map.insert(pair<string,string>("movie","video/x-sgi-movie"));</span><br><span class="line"></span><br><span class="line"> content_type_map.insert(pair<string,string>("woff","application/font-woff"));</span><br><span class="line"> content_type_map.insert(pair<string,string>("ttf","application/octet-stream"));</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这里添加了一些常用格式的Content-Type类型,后续涉及到更复杂的文件类型时对其进一步扩展</p><h3 id="gzip压缩"><a href="#gzip压缩" class="headerlink" title="gzip压缩"></a>gzip压缩</h3><p>在http响应的结构中,我们常常可以看到一个名为Content-Encoding的字段,其值大多为gzip,deflate等。该字段决定的是http响应体的编码格式。目前主流浏览器均支持gzip等格式的压缩格式。使用压缩格式的最大好处就是减少网络传输的信息量,提高网页加载速度,但由于服务端多了压缩的步骤,也一定程度增加了服务器的负担(客户端单次处理时,解压的影响可以忽略不计)。<br>而gzip格式又是应用最广泛的一种压缩格式,其对文本内容的压缩率常常可以达到40%以上,对于html,css,javascript文件均有着非常好的压缩效果。</p><p>本文实现的Minihttpd为了增加gzip格式的压缩功能使用了zlib库,其代码均为c编写,使用方法相对简单,这里列出部分供参考</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br></pre></td><td class="code"><pre><span class="line">//raw_data为原始数据,buffer为压缩后数据存储缓冲区,buffer_size为缓冲区大小,返回值为压缩后数据字节数</span><br><span class="line">uLong gzip_compress(string raw_data,Bytef*& buffer,int buffer_size){</span><br><span class="line"> size_t raw_data_size = raw_data.size();</span><br><span class="line"> z_stream strm;</span><br><span class="line"> z_stream d_stream;</span><br><span class="line"> d_stream.zalloc = NULL;</span><br><span class="line"> d_stream.zfree = NULL;</span><br><span class="line"> d_stream.opaque = NULL;</span><br><span class="line"> d_stream.next_in = (Bytef*)raw_data.c_str();</span><br><span class="line"> d_stream.avail_in = raw_data_size;</span><br><span class="line"> d_stream.next_out = buffer;</span><br><span class="line"> d_stream.avail_out = buffer_size;</span><br><span class="line"></span><br><span class="line"> int ret = deflateInit2(&d_stream, Z_DEFAULT_COMPRESSION, Z_DEFLATED,</span><br><span class="line">MAX_WBITS + 16, 8, Z_DEFAULT_STRATEGY);</span><br><span class="line"> if (Z_OK != ret)</span><br><span class="line"> {</span><br><span class="line"> Log::log("init deflate error",ERROR);</span><br><span class="line"> // cout<< ret <<endl;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> int err = 0;</span><br><span class="line"> int flag = 0;</span><br><span class="line"> for(;;) {</span><br><span class="line"> if((err = deflate(&d_stream, Z_FINISH)) == Z_STREAM_END) break;</span><br><span class="line"> if(flag > 3){</span><br><span class="line"> stringstream ss;</span><br><span class="line"> ss<< "deflate failed,errNo = "<<err;</span><br><span class="line"> Log::log(ss.str(),ERROR);</span><br><span class="line"> return 0;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> //输出缓冲区不足,尝试扩容,最多三次扩容失败则放弃压缩</span><br><span class="line"> if(err == Z_BUF_ERROR){</span><br><span class="line"> flag++;</span><br><span class="line"> delete buffer;</span><br><span class="line"> buffer_size = buffer_size*1.5;</span><br><span class="line"> buffer = new Bytef[buffer_size];</span><br><span class="line"> d_stream.next_out = buffer;</span><br><span class="line"> d_stream.avail_out = buffer_size;</span><br><span class="line"> stringstream ss;</span><br><span class="line"> ss<< "deflate buffer error,try larger buffer :"<<flag;</span><br><span class="line"> Log::log(ss.str(),WARN);</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> if(deflateEnd(&d_stream) != Z_OK){</span><br><span class="line"> Log::log("deflate failed when end",ERROR);</span><br><span class="line"> return 0;</span><br><span class="line"> }</span><br><span class="line"> return d_stream.total_out;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>主要工作流程为:</p><ol><li>deflateInit2() 设置压缩格式等信息</li><li>deflate() 进行压缩</li><li>deflateEnd() 压缩完毕释放临时空间等收尾工作</li></ol><h4 id="编码格式判别"><a href="#编码格式判别" class="headerlink" title="编码格式判别"></a>编码格式判别</h4><p>接收到http请求后,首先判断请求头中是否包含Accept-Encoding字段,如果存在,检查其后面接受的压缩格式等,决定是否使用gzip压缩(注意响应头也需要添加Content-Encoding:gzip字段)</p><p>####Github<br><a href="https://github.com/njuwuyuxin/MiniHttpd" target="_blank" rel="noopener">https://github.com/njuwuyuxin/MiniHttpd</a><br>欢迎共同学习</p>]]></content>
<summary type="html">
<hr>
<p>上一篇中我们实现了接受浏览器的请求,并返回本地的网页给浏览器展示,接下来对该简单的功能进行下一步完善</p>
<h3 id="Content-Type"><a href="#Content-Type" class="headerlink" title="Conte
</summary>
<category term="从零开始" scheme="https://njuwuyuxin.github.io/categories/%E4%BB%8E%E9%9B%B6%E5%BC%80%E5%A7%8B/"/>
<category term="后端" scheme="https://njuwuyuxin.github.io/tags/%E5%90%8E%E7%AB%AF/"/>
<category term="http" scheme="https://njuwuyuxin.github.io/tags/http/"/>
<category term="计算机网络" scheme="https://njuwuyuxin.github.io/tags/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/"/>
</entry>
<entry>
<title>libiconv 未定义的引用解决</title>
<link href="https://njuwuyuxin.github.io/2020/06/25/libiconv%E6%9C%AA%E5%AE%9A%E4%B9%89%E7%9A%84%E5%BC%95%E7%94%A8%E8%A7%A3%E5%86%B3/"/>
<id>https://njuwuyuxin.github.io/2020/06/25/libiconv未定义的引用解决/</id>
<published>2020-06-25T04:33:44.000Z</published>
<updated>2020-06-25T04:35:20.300Z</updated>
<content type="html"><![CDATA[<hr><p>最近在安装libconfig库时,编译期间出现找不到libiconv库的问题<br><code>/usr/local/lib/../lib64/libstdc++.so: undefined reference to "libiconv"</code><br>在仔细检查重新安装了libiconv库之后,问题依然无法解决。因此根据make时的记录进行追溯,发现链接错误出现在/example/c++样例编译期间</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">make[3]: 离开目录“/home/downloads/libconfig-1.7.2/examples/c”</span><br><span class="line">Making all in c++</span><br><span class="line">make[3]: 进入目录“/home/downloads/libconfig-1.7.2/examples/c++”</span><br><span class="line"> CXX example1.o</span><br><span class="line"> CXXLD example1</span><br><span class="line">/usr/local/lib/../lib64/libstdc++.so: undefined reference to `libiconv'</span><br><span class="line">/usr/local/lib/../lib64/libstdc++.so: undefined reference to `libiconv_close'</span><br><span class="line">/usr/local/lib/../lib64/libstdc++.so: undefined reference to `libiconv_open'</span><br><span class="line">collect2: error: ld returned 1 exit status</span><br></pre></td></tr></table></figure><p>手动去查看/example/c++/下的Makefile文件时发现</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">LIBOBJS =</span><br><span class="line">LIBS = </span><br><span class="line">LIBTOOL = $(SHELL) $(top_builddir)/libtool</span><br><span class="line">LIPO =</span><br></pre></td></tr></table></figure><p>其中LIBS变量存放编译时所需引用的外部库,而这里默认置空,我的环境使用的是CentOS7,而这里需要手动添加libiconv库的引用,因此将其改为</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">LIBOBJS =</span><br><span class="line">LIBS = -liconv</span><br><span class="line">LIBTOOL = $(SHELL) $(top_builddir)/libtool</span><br><span class="line">LIPO =</span><br></pre></td></tr></table></figure><p>手动指定链接libiconv库。之后重新执行make,成功编译并链接。</p><h3 id="后记"><a href="#后记" class="headerlink" title="后记"></a>后记</h3><p>类似库找不到定义的问题原因可能有很多,但大致都可以按照一下思路进行解决</p><ul><li>首先判断库是否确实未安装(最简单的情况,安装该库即可)</li><li>如果该库已安装,查看其所在位置是否加入到系统搜索范围内</li><li>如果以上均无问题,可能需要检查Makefile或其他编译选项,尝试手动链接该动态链接库</li></ul>]]></content>
<summary type="html">
<hr>
<p>最近在安装libconfig库时,编译期间出现找不到libiconv库的问题<br><code>/usr/local/lib/../lib64/libstdc++.so: undefined reference to &quot;libiconv&quot;</c
</summary>
<category term="踩坑整理" scheme="https://njuwuyuxin.github.io/categories/%E8%B8%A9%E5%9D%91%E6%95%B4%E7%90%86/"/>
<category term="Makefile" scheme="https://njuwuyuxin.github.io/tags/Makefile/"/>
<category term="常见问题" scheme="https://njuwuyuxin.github.io/tags/%E5%B8%B8%E8%A7%81%E9%97%AE%E9%A2%98/"/>
<category term="c++" scheme="https://njuwuyuxin.github.io/tags/c/"/>
</entry>
<entry>
<title>CMake基础使用整理</title>
<link href="https://njuwuyuxin.github.io/2020/06/24/CMake%E5%9F%BA%E7%A1%80%E4%BD%BF%E7%94%A8%E6%95%B4%E7%90%86/"/>
<id>https://njuwuyuxin.github.io/2020/06/24/CMake基础使用整理/</id>
<published>2020-06-24T12:55:12.000Z</published>
<updated>2020-06-24T13:00:27.502Z</updated>
<content type="html"><![CDATA[<hr><p>CMake是一个跨平台的编译工具,可以一次编写,在不同平台自动生成对应的Makefile文件,减少了手写Makefile以及适配不同平台时的耗时。</p><h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>之前大部分时候在windows端使用VS开发,因此对Makefile、CMake等工具接触较少。最近尝试从头实现一个简单的HTTP服务器,主要开发环境在Linux,因此借此契机熟悉一下CMake等构建工具的使用。</p><h2 id="目录结构"><a href="#目录结构" class="headerlink" title="目录结构"></a>目录结构</h2><p>目前项目文件较少,使用了较简单的目录结构</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line">┣━ src</span><br><span class="line">┃ ┣━ CMakeLists.txt</span><br><span class="line">┃ ┣━ HttpRequest.cpp</span><br><span class="line">┃ ┣━ HttpResponse.cpp</span><br><span class="line">┃ ┣━ HttpServer.cpp</span><br><span class="line">┃ ...</span><br><span class="line">┣━ include</span><br><span class="line">┃ ┣━ HttpRequest.h</span><br><span class="line">┃ ┣━ HttpResponse.h</span><br><span class="line">┃ ┣━ HttpServer.h</span><br><span class="line">┃ ...</span><br><span class="line">┣━ cmake-build-debug</span><br><span class="line">┃ ┣━...</span><br><span class="line">┃ ┗━...</span><br><span class="line">┣━ main.cpp</span><br><span class="line">┣━ CMakeLists.txt</span><br></pre></td></tr></table></figure><p>可以看到源文件和头文件分别存储在对应目录中,根目录下以main.cpp作为程序入口,最终构建目标及中间文件存放在cmake-build-debug这一独立文件夹中。</p><h2 id="CMakeLists编写"><a href="#CMakeLists编写" class="headerlink" title="CMakeLists编写"></a>CMakeLists编写</h2><p>为了使上述目录结构能够正确编译链接,我们需要编写CMakeLists.txt,同时CMake能够一定程度上减少多文件多目录时来回链接顺序等头疼的问题。在这个项目里,根目录下和src目录下各有一个CMakeLists.txt文件,这也是CMake的特点,可以将Makefile拆分,每个目录各自进行编译,最终链接起来。</p><p><strong>根目录下的CMakeLists.txt内容如下</strong></p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line">cmake_minimum_required (VERSION 2.6)</span><br><span class="line"></span><br><span class="line">add_definitions(-std=c++11)</span><br><span class="line">set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11 -g -Wall -Wno-unused-variable -pthread")</span><br><span class="line"></span><br><span class="line">project (Minihttpd)</span><br><span class="line"></span><br><span class="line">include_directories(include)</span><br><span class="line">add_subdirectory(src)</span><br><span class="line"></span><br><span class="line"># 顺序不可修改,先link_directories 再 add_executable 最后 target_link_libraries</span><br><span class="line">link_directories(/usr/local/lib)</span><br><span class="line"></span><br><span class="line">add_executable(Minihttpd main.cpp)</span><br><span class="line"></span><br><span class="line">target_link_libraries(Minihttpd src)</span><br><span class="line">target_link_libraries(Minihttpd -lconfig++)</span><br></pre></td></tr></table></figure><p>接下来对每条语句进行简单的解释</p><ul><li>add_definations 指令可以用来手动设置某些宏的开闭,控制编译选项,这里主要是标注使用c++11标准</li><li>set指令能够用前面的变量替代后面的字符串,这里实际上是对预定义的CMAKE_CXX_FLAGS变量进行了一个修改,来设置某些选项,主要链接pthread库使用多线程</li><li>project指令用来设置项目名(包括版本、所用语言等信息,这里缺省)</li><li>include_directories指令用来指定寻找头文件的路径,这里把include目录加入到头文件搜索范围内,使得项目内文件可以找到对应头文件</li><li>add_subdirectory指令用来把子目录加入到构建列表中,最终构建结果存放在src变量中</li></ul><p>之后的几行是在项目需要引用其他动态链接库,非常需要注意的地方</p><ul><li>add_executable指令用来指定项目最终构建的目标文件,以及所需要的所有源文件。可以看到这里只有main.cpp,为什么没有包含src目录下其他源文件?这里其实在下面使用 target_link_libraries 指令,以动态链接库的形式引入进来。其顺序是在子目录中首先进行了部分构建,在src目录下生成了相应的libsrc.a文件,最终链接到程序入口文件上,实现了构建。</li><li>link_directories和target_link_libraries指令用来引入外部的动态链接库。其中<ul><li>link_directories用来指定该动态链接库所在目录</li><li>target_link_libraries用来把所需的动态链接库引入到该项目中</li></ul></li></ul><p><strong>这里非常需要注意的是几条语句的顺序,一定是</strong></p><ol><li>link_directories 把动态链接库所在目录加入寻找列表中</li><li>add_executable 指定最终构建目标名称</li><li>target_link_libraries 把需要的动态链接库加入到项目中</li></ol><p><strong>这里的顺序错误将导致链接失败,出现找不到动态链接库等各种问题(踩过的坑,心酸的泪)</strong></p><p><strong>子目录下的CMakeLists.txt内容如下</strong></p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">aux_source_directory(. srcs)</span><br><span class="line">add_library(src ${srcs})</span><br></pre></td></tr></table></figure><p>这里的内容就非常简单</p><ul><li>aux_source_directory指令把当前目录下所有源文件加入到srcs变量中存储</li><li>add_library使用srcs变量中所有源文件进行构建,结果输出为src。不同于add_executable,add_library的构建的最终目标为动态链接库文件,而add_executable的构建结果为一个可执行文件。这里构建成动态链接库文件也是为了在根目录下构建时进行链接</li></ul><h2 id="扩展"><a href="#扩展" class="headerlink" title="扩展"></a>扩展</h2><p>在理解了多目录下CMakeLists的编写后,如果需要把源文件存放在多个不同目录中,也可以以动态链接库的形式分别进行构建、链接。而对于多级目录,也可以依次逐级构建并链接。</p><p>对于需要引用的外部动态链接库,也可以通过link_directories和target_link_libraries指令的配合进行引入。</p><p>同时本项目内使用了ninja作为构建工具,更方便了项目的构建,主要使用方法为</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">cd cmake-build-debug</span><br><span class="line">cmake -G Ninja .. //cmake支持根据CMakeLists.txt自动化生成ninja构建所需要的ninja.build等文件</span><br><span class="line">ninja //在该目录下构建</span><br></pre></td></tr></table></figure>]]></content>
<summary type="html">
<hr>
<p>CMake是一个跨平台的编译工具,可以一次编写,在不同平台自动生成对应的Makefile文件,减少了手写Makefile以及适配不同平台时的耗时。</p>
<h2 id="前言"><a href="#前言" class="headerlink" title="前言
</summary>
<category term="工具学习" scheme="https://njuwuyuxin.github.io/categories/%E5%B7%A5%E5%85%B7%E5%AD%A6%E4%B9%A0/"/>
<category term="C++" scheme="https://njuwuyuxin.github.io/tags/C/"/>
<category term="CMake" scheme="https://njuwuyuxin.github.io/tags/CMake/"/>
<category term="构建工具" scheme="https://njuwuyuxin.github.io/tags/%E6%9E%84%E5%BB%BA%E5%B7%A5%E5%85%B7/"/>
</entry>
<entry>
<title>从零实现HTTP服务器——Minihttpd(一)</title>
<link href="https://njuwuyuxin.github.io/2020/06/23/%E4%BB%8E%E9%9B%B6%E5%AE%9E%E7%8E%B0HTTP%E6%9C%8D%E5%8A%A1%E5%99%A8%E2%80%94%E2%80%94Minihttpd%EF%BC%88%E4%B8%80%EF%BC%89/"/>
<id>https://njuwuyuxin.github.io/2020/06/23/从零实现HTTP服务器——Minihttpd(一)/</id>
<published>2020-06-23T15:15:28.000Z</published>
<updated>2020-06-29T15:21:55.253Z</updated>
<content type="html"><![CDATA[<hr><h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>最近学习了一下Tinyhttpd的源码,对http服务器的基本工作原理有了简单的理解,而Tinyhttpd一方面年头较为久远(上个世纪的代码),另一方面基本全部由C语言实现,因此便萌生了用C++从头造轮子的想法,同时加深对TCP、HTTP等协议,socket编程等理解。</p><h2 id="HTTP服务器基本工作流程"><a href="#HTTP服务器基本工作流程" class="headerlink" title="HTTP服务器基本工作流程"></a>HTTP服务器基本工作流程</h2><p>一个最简单的HTTP服务器,其基本功能主要是接受来自浏览器的http请求,之后根据请求内容返回相应的http response。因此对于我们要实现的最基本的http服务器,首先要完成的就是接受请求和发送响应。</p><h2 id="HTTP报文格式"><a href="#HTTP报文格式" class="headerlink" title="HTTP报文格式"></a>HTTP报文格式</h2><h3 id="HTTP请求"><a href="#HTTP请求" class="headerlink" title="HTTP请求"></a>HTTP请求</h3><p>HTTP请求主要由请求头(header)和请求体(body)构成,中间使用了空行(\r\n)进行分隔,具体结构如图所示<br><img src="https://s1.ax1x.com/2020/06/23/NUzklD.png" alt="HTTP请求格式"><br>以浏览器访问某一网站为例,在除去最开始的 “请求方法 URL 协议版本” 这一行后,剩余部分均为请求头部字段,没有列出的首行格式形如 <code>GET /index.html http/1.1</code></p><p><img src="https://s1.ax1x.com/2020/06/23/NUxQsJ.png" alt="浏览器请求"></p><h3 id="HTTP响应"><a href="#HTTP响应" class="headerlink" title="HTTP响应"></a>HTTP响应</h3><p>HTTP响应结构与请求类似,分为响应头和和响应体,中间以空行分隔。<br>响应头首行为 “协议版本 HTTP状态码“(OK可省略),剩余均为头部字段,按需求可自行添加。<br><img src="https://s1.ax1x.com/2020/06/23/NapnRf.jpg" alt="HTTP响应格式"></p><h2 id="基本的HTTP服务器实现"><a href="#基本的HTTP服务器实现" class="headerlink" title="基本的HTTP服务器实现"></a>基本的HTTP服务器实现</h2><p>在理解了http服务器的简单工作流程和http请求相关格式后,我们便可以动手编写最基础的http服务器。为了方便各模块抽象,目前主要使用三个类进行基础维护,分别为:HttpServer、HttpRequest、HttpResponse</p><h3 id="HttpRequest和HttpResponse"><a href="#HttpRequest和HttpResponse" class="headerlink" title="HttpRequest和HttpResponse"></a>HttpRequest和HttpResponse</h3><p>这两个类主要是便于进行http请求的解析与响应报文的构造,也可以方便的看出http请求和响应的简单结构。<br>HttpRequest结构如下,分别对应了请求报文格式,可以方便的读取头部各字段信息</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">class HttpRequest{</span><br><span class="line">public:</span><br><span class="line"> HttpRequest(string raw_data);</span><br><span class="line"> inline const string get_method(){ return method; };</span><br><span class="line"> inline const string get_url(){ return url; };</span><br><span class="line"> inline const map<string,string>& get_header(){ return header; };</span><br><span class="line">private:</span><br><span class="line"> string method; //该http请求方法</span><br><span class="line"> string url; //请求URL</span><br><span class="line"> string version; //http版本</span><br><span class="line"> map<string,string> header;</span><br><span class="line">};</span><br></pre></td></tr></table></figure><p>HttpResponse结构如下,对于通用字段单独列出方便快速设置,同时提供自定义字段设置方法,而load_from_file则提供了文件读取相关功能,主要对应于解析请求中的url字段,找到服务器上相应文件进行返回。</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><span class="line">class HttpResponse{</span><br><span class="line">public:</span><br><span class="line"> HttpResponse(int st);</span><br><span class="line"> void set_header(string key, string val); //设置头部自定义字段</span><br><span class="line"> void load_from_file(string url);</span><br><span class="line"> string get_response();</span><br><span class="line"></span><br><span class="line"> /* 基础头部字段,供快速填充,自定义字段需手动设置 */</span><br><span class="line"> string Allow;</span><br><span class="line"> string Content_Encoding;</span><br><span class="line"> string Content_Length;</span><br><span class="line"> string Content_Type;</span><br><span class="line"> string Expires;</span><br><span class="line"> string Last_Modified;</span><br><span class="line"> string Location;</span><br><span class="line"> string Refresh;</span><br><span class="line"> string Set_Cookie;</span><br><span class="line"> string WWW_Authenticate;</span><br><span class="line"> </span><br><span class="line">private:</span><br><span class="line"> string version; //http版本</span><br><span class="line"> string status; //http状态码</span><br><span class="line"> string date; //response生成时间</span><br><span class="line"> string server; //http服务器名称</span><br><span class="line"> map<string,string> custom_header; //自定义头部字段</span><br><span class="line"> string generate_header(); //使用全部信息组装HTTP Response头部</span><br><span class="line"> string response_body; //返回内容体</span><br><span class="line">};</span><br></pre></td></tr></table></figure><h3 id="HttpServer"><a href="#HttpServer" class="headerlink" title="HttpServer"></a>HttpServer</h3><p>HttpServer类主要用于维护单个服务器实例,也是服务器的最核心功能。目前的基本功能便是建立套接字,接受请求并返回,其类结构为</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line">class HttpServer{</span><br><span class="line">public:</span><br><span class="line"> HttpServer();</span><br><span class="line"> HttpServer(u_short p);</span><br><span class="line"> inline int get_sock_id(){ return server_sock; };</span><br><span class="line"> inline u_short get_port(){ return port; };</span><br><span class="line"> void start_listen();</span><br><span class="line"> static void accept_request(int client_sock,HttpServer* t);</span><br><span class="line">private:</span><br><span class="line"> int server_sock;</span><br><span class="line"> u_short port;</span><br><span class="line"> string baseURL;</span><br><span class="line"> void startup();</span><br><span class="line">};</span><br></pre></td></tr></table></figure><p>其中startup函数用于创建套接字用于之后监听请求,HTTP协议基于的是TCP协议,因此套接字需正确设置,配置端口号、本地网卡IP等信息,这里为了便于使用,如果不指定端口号会随机使用某一可用端口。</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><span class="line">int on = 1;</span><br><span class="line"> struct sockaddr_in name;</span><br><span class="line"></span><br><span class="line"> server_sock = socket(PF_INET, SOCK_STREAM, 0); //使用TCP协议</span><br><span class="line"> if (server_sock == -1)</span><br><span class="line"> cerr<<"[ERROR]: create socket failed"<<endl;</span><br><span class="line"> memset(&name, 0, sizeof(name));</span><br><span class="line"> name.sin_family = AF_INET;</span><br><span class="line"> name.sin_port = htons(port);</span><br><span class="line"> name.sin_addr.s_addr = htonl(INADDR_ANY);</span><br><span class="line"> if ((setsockopt(server_sock, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on))) < 0) </span><br><span class="line"> { </span><br><span class="line"> cerr<< "[ERROR]: setsockopt failed"<<endl;</span><br><span class="line"> }</span><br><span class="line"> if (bind(server_sock, (struct sockaddr *)&name, sizeof(name)) < 0)</span><br><span class="line"> cerr<<"[ERROR]: bind failed"<<endl;</span><br><span class="line"></span><br><span class="line"> if (port == 0) /* if dynamically allocating a port */</span><br><span class="line"> {</span><br><span class="line"> socklen_t namelen = sizeof(name);</span><br><span class="line"> if (getsockname(server_sock, (struct sockaddr *)&name, &namelen) == -1)</span><br><span class="line"> cerr<<"[ERROR]: getsockname failed"<<endl;</span><br><span class="line"> port = ntohs(name.sin_port);</span><br><span class="line"> }</span><br><span class="line"> //listen第二个参数为连接请求队列长度,5代表最多同时接受5个连接请求</span><br><span class="line"> if (listen(server_sock, 5) < 0) </span><br><span class="line"> cerr<<"[ERROR]: listen failed"<<endl;</span><br></pre></td></tr></table></figure><p>在创建了socket后,我们便可使用该socket监听发来的tcp数据包,从中识别出http请求,这部分工作交由start_listen()函数完成</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line">cout<<"httpd running on port "<<port<<endl;</span><br><span class="line"> int client_sock = -1;</span><br><span class="line"> struct sockaddr_in client_name;</span><br><span class="line"> socklen_t client_name_len = sizeof(client_name);</span><br><span class="line"> pthread_t newthread;</span><br><span class="line"></span><br><span class="line"> while (1)</span><br><span class="line"> {</span><br><span class="line"> //accept函数用来保存请求客户端的地址相关信息</span><br><span class="line"> client_sock = accept(server_sock,</span><br><span class="line"> (struct sockaddr *)&client_name,</span><br><span class="line"> &client_name_len);</span><br><span class="line"> if (client_sock == -1)</span><br><span class="line"> cerr<<"[ERROR]: accept failed"<<endl;</span><br><span class="line"></span><br><span class="line"> thread accept_thread(accept_request,client_sock,this);</span><br><span class="line"> accept_thread.join();</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> close(server_sock);</span><br></pre></td></tr></table></figure><p>这里主要使用accept函数保存客户端socket相关信息,在接收到客户端发送的一条请求后,创建一个新的线程用于该请求的处理,具体处理部分如下,主要通过read读取原始数据存入buffer,将该信息交给HttpRequest类进行简单解析,同时利用HttpResponse类构造响应报文,使用send将该响应发送回客户端,之后关闭该套接字。</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line">int client = client_sock;</span><br><span class="line"> char buf[1024];</span><br><span class="line"> read(client_sock,(void*)buf,1024);</span><br><span class="line"> string req(buf);</span><br><span class="line"> HttpRequest request(req);</span><br><span class="line"> cout<<"url: "<<request.get_url()<<endl;</span><br><span class="line"> string req_url = t->baseURL + request.get_url();</span><br><span class="line"> </span><br><span class="line"> auto header = request.get_header();</span><br><span class="line"> cout<<"[GET REQUEST]: Host = "<<header.find("Host")->second<<endl;</span><br><span class="line"></span><br><span class="line"> HttpResponse response(200);</span><br><span class="line"> response.load_from_file(req_url);</span><br><span class="line"> response.Content_Type = "text/html";</span><br><span class="line"> string res_string = response.get_response();</span><br><span class="line"> // cout<<res_string<<endl;</span><br><span class="line"> send(client,res_string.c_str(),strlen(res_string.c_str()),0);</span><br><span class="line"> close(client);</span><br></pre></td></tr></table></figure><p>至此一条http请求便可以被正确解析并返回,总体流程为:</p><ul><li>创建server_socket</li><li>监听某一端口请求</li><li>接收数据解析请求</li><li>构造响应报文</li><li>发送响应,关闭客户端套接字</li></ul><p>到这里一个具备基础功能的http服务器已经初具雏形,可以解析简单的http请求,同时根据请求路径读取本地的html文档进行返回,交由浏览器展示。之后我们会不断完善该服务器,实现更复杂的一些功能。</p><p>Github地址:<a href="https://github.com/njuwuyuxin/MiniHttpd" target="_blank" rel="noopener">https://github.com/njuwuyuxin/MiniHttpd</a></p>]]></content>
<summary type="html">
<hr>
<h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>最近学习了一下Tinyhttpd的源码,对http服务器的基本工作原理有了简单的理解,而Tinyhttpd一方面年头较为久远(上个
</summary>
<category term="从零开始" scheme="https://njuwuyuxin.github.io/categories/%E4%BB%8E%E9%9B%B6%E5%BC%80%E5%A7%8B/"/>
<category term="后端" scheme="https://njuwuyuxin.github.io/tags/%E5%90%8E%E7%AB%AF/"/>
<category term="http" scheme="https://njuwuyuxin.github.io/tags/http/"/>
<category term="计算机网络" scheme="https://njuwuyuxin.github.io/tags/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/"/>
</entry>
<entry>
<title>《UnityShader入门精要》学习笔记(三)——UnityShader初探</title>
<link href="https://njuwuyuxin.github.io/2020/05/26/%E3%80%8AUnityShader%E5%85%A5%E9%97%A8%E7%B2%BE%E8%A6%81%E3%80%8B%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0%EF%BC%88%E4%B8%89%EF%BC%89%E2%80%94%E2%80%94UnityShader%E5%88%9D%E6%8E%A2/"/>
<id>https://njuwuyuxin.github.io/2020/05/26/《UnityShader入门精要》学习笔记(三)——UnityShader初探/</id>
<published>2020-05-26T14:33:35.000Z</published>
<updated>2020-05-26T14:39:38.444Z</updated>
<content type="html"><![CDATA[<hr><h2 id="何为Unity-Shader"><a href="#何为Unity-Shader" class="headerlink" title="何为Unity Shader"></a>何为Unity Shader</h2><p>在传统的开发模式中,开发者如果想要设置自定义的渲染模式,需要和大量文件和设置打交道(包括编写顶点着色器、片元着色器、选择图形API、加载资源到GPU等等),非常繁琐复杂。Unity Shader就是在此之上的更高级的一层封装,开发者只需要在Shader Lab中编写Unity Shader文件,即可交由Unity引擎实现自定义渲染效果。<br>因此Unity Shader与传统的Shader有所不同,它定义的是要显示一个材质的所有东西,<strong>而不仅仅是着色器代码</strong>。</p><p>而在使用Unity Shader时,我们首先需要创建一个材质,将编写的Unity Shader文件挂载到该材质上,之后再为各个物体应用该材质。由此可以看出,Unity Shader是与材质牢牢绑定的。</p><h2 id="Unity-Shader文件基本结构"><a href="#Unity-Shader文件基本结构" class="headerlink" title="Unity Shader文件基本结构"></a>Unity Shader文件基本结构</h2><p>一个UnityShader的基本文件结构大致如下</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">Shader "ShaderName"{</span><br><span class="line"> Properties{</span><br><span class="line"> //相关属性</span><br><span class="line"> }</span><br><span class="line"> SubShader{</span><br><span class="line"> //显卡A使用的子着色器</span><br><span class="line"> }</span><br><span class="line"> SubShader{</span><br><span class="line"> //显卡B使用的子着色器</span><br><span class="line"> }</span><br><span class="line"> Fallback "VertexLit"</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>一个完整的Unity Shader包括shader名 ShaderName、shader包含的属性Properties、使用的子着色器SubShader、以及无法调用任何子着色器时执行的Fallback</p><h4 id="Properties"><a href="#Properties" class="headerlink" title="Properties"></a>Properties</h4><p>Properties定义了一系列属性,这些属性将会出现在材质面板中,Properties语句块的定义如下:</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">Properties{</span><br><span class="line"> Name("display name",PropertyType) = DefaultValue</span><br><span class="line"> Name("display name",PropertyType) = DefaultValue</span><br><span class="line">}</span><br></pre></td></tr></table></figure><ul><li>Name为属性名,可以在后续代码中使用,<strong>一般以下划线开头</strong></li><li>display name指的是在材质面板中显示的名称</li><li>PropertyType为属性类型</li><li>DefaultValue为属性的默认值,第一次为某个材质应用该Shader时就会使用该默认值</li></ul><p>在properties声明变量后,我们还需要在cg代码中定义变量来进行使用。<br>需要注意的是,即使我们不在properties中声明变量,我们也可以通过脚本的方式向shader传递变量的值,因此properties块的功能仅仅是为了让对应属性出现在材质面板中。</p><h4 id="SubShader"><a href="#SubShader" class="headerlink" title="SubShader"></a>SubShader</h4><p>SubShader主要包含这样几个部分</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">SubShader{</span><br><span class="line"> [Tags]</span><br><span class="line"> [RenderSetup]</span><br><span class="line"> Pass{</span><br><span class="line"> ...</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><ul><li>其中 标签是一系列“键值对”,用来进行和渲染引擎的“沟通”,告知其应该怎样、何时渲染这个对象</li><li>渲染设置是用来设置显卡渲染时的一些选项,如是否开启混合等</li><li>Pass是最重要的语句块,可以有多个,每个Pass在渲染流程中执行一次,但是为了避免多个pass造成性能下降,我们应该尽可能用最少的Pass实现渲染功能。</li></ul><p>对于每个Pass,结构类似SubShader</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">Pass{</span><br><span class="line"> [name]</span><br><span class="line"> [Tags]</span><br><span class="line"> [RenderSetup]</span><br><span class="line"> ...</span><br><span class="line">}</span><br></pre></td></tr></table></figure><ul><li>名称:可以设置Pass的名称,这样我们在其他SubShader中,可以通过UsePass来使用其他Shader中定义的Pass,实现代码复用(需要指明路径)。注意,Unity会自动将Pass的名称转换为全大写字母</li><li>标签:其中Pass可以使用的标签和SubShader略有不同</li><li>渲染设置:和SubShader一致。如果我们在SubShader中进行设置,则会应用于所有Pass。如果不想这样,可以分别为每个Pass设置不同的RenderSetup</li></ul><p>一个UnityShader文件中可以包含多个SubShader,主要用来实现不同显卡上的兼容性,如果第一个SubShader中的某些指令显卡不支持,则会使用下面的SubShader,以此类推。如果没有一个SubShader兼容,则会使用Fallback</p><h4 id="Fallback"><a href="#Fallback" class="headerlink" title="Fallback"></a>Fallback</h4><p>Fallback可以认为是所有SubShader都无法使用时最迫不得已的选项,是最基础的渲染。当然也可以手动关闭,意味着如果没有SubShader支持,我们就不去管他。</p>]]></content>
<summary type="html">
<hr>
<h2 id="何为Unity-Shader"><a href="#何为Unity-Shader" class="headerlink" title="何为Unity Shader"></a>何为Unity Shader</h2><p>在传统的开发模式中,开发者如果想要
</summary>
<category term="UnityShader学习笔记" scheme="https://njuwuyuxin.github.io/categories/UnityShader%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/"/>
<category term="Unity" scheme="https://njuwuyuxin.github.io/tags/Unity/"/>
<category term="Shader" scheme="https://njuwuyuxin.github.io/tags/Shader/"/>
<category term="学习笔记" scheme="https://njuwuyuxin.github.io/tags/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/"/>
</entry>
<entry>
<title>《Unity Shader 入门精要》学习笔记(二)—— GPU流水线</title>
<link href="https://njuwuyuxin.github.io/2020/05/07/%E3%80%8AUnityShader%E5%85%A5%E9%97%A8%E7%B2%BE%E8%A6%81%E3%80%8B%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0%EF%BC%88%E4%BA%8C%EF%BC%89%E2%80%94%E2%80%94GPU%E6%B5%81%E6%B0%B4%E7%BA%BF/"/>
<id>https://njuwuyuxin.github.io/2020/05/07/《UnityShader入门精要》学习笔记(二)——GPU流水线/</id>
<published>2020-05-07T12:22:21.000Z</published>
<updated>2020-05-07T12:42:26.108Z</updated>
<content type="html"><![CDATA[<hr><h2 id="GPU流水线"><a href="#GPU流水线" class="headerlink" title="GPU流水线"></a>GPU流水线</h2><p>GPU流水线的大致流程和步骤如下图所示<br><img src="https://upload-images.jianshu.io/upload_images/5633236-a8b9670d9900afe0.png" alt="GPU流水线"><br>其中深灰色的步骤为可编程的,浅色步骤不可编程但可配置,灰色步骤不可编程也不可配置。下面简单介绍每个步骤的基本任务。</p><h3 id="几何阶段"><a href="#几何阶段" class="headerlink" title="几何阶段"></a>几何阶段</h3><h4 id="顶点着色器"><a href="#顶点着色器" class="headerlink" title="顶点着色器"></a>顶点着色器</h4><p>顶点着色器步骤的功能仅仅是对上一阶段CPU输出的顶点信息进行坐标变换以及计算顶点颜色光照等。这里的各顶点信息都是完全独立的,并且无法获取顶点之间的关系,因此可以被GPU并行计算,效率较高。</p><p>这里的坐标变换指的是:从模型空间坐标变换到齐次剪裁坐标(均为三维空间)</p><h4 id="曲面细分着色器"><a href="#曲面细分着色器" class="headerlink" title="曲面细分着色器"></a>曲面细分着色器</h4><p>一个可选的着色器,用于细分图元。</p><h4 id="几何着色器"><a href="#几何着色器" class="headerlink" title="几何着色器"></a>几何着色器</h4><p>一个可选的着色器,用于逐图元进行着色。</p><h4 id="裁剪"><a href="#裁剪" class="headerlink" title="裁剪"></a>裁剪</h4><p>一个非常重要的步骤,用于将每个图元在摄像机视野外的部分裁剪掉。完全不在视野内的图元会直接被舍弃掉不传递给下一阶段,完全在视野内的图元会直接传递给下一阶段。因此裁剪主要针对一部分在摄像机视野内,另一部分在视野外的图元。</p><h4 id="屏幕映射"><a href="#屏幕映射" class="headerlink" title="屏幕映射"></a>屏幕映射</h4><p>屏幕映射阶段主要工作为:将之前得到的顶点的坐标映射到屏幕坐标系中,即完成从三维空间向二维平面的投影。<br>注意:OpenGL和DirectX的屏幕坐标有所差异,OpenGL的原点在左下角,DirectX的原点在左上角。</p><h3 id="光栅化阶段"><a href="#光栅化阶段" class="headerlink" title="光栅化阶段"></a>光栅化阶段</h3><h4 id="三角形设置"><a href="#三角形设置" class="headerlink" title="三角形设置"></a>三角形设置</h4><p>在几何阶段,我们得到了一系列在屏幕坐标系中的顶点坐标。而一个三角形由三个顶点组成,因此我们需要通过这些顶点坐标,计算出每个三角形图元具体覆盖了哪些像素,这个过程就叫做 三角形设置。<br>三角形设置就是计算每个三角形边界的表示方式,为下一阶段做准备。</p><h4 id="三角形遍历"><a href="#三角形遍历" class="headerlink" title="三角形遍历"></a>三角形遍历</h4><p>在得到了一系列三角形网格的表示数据后,我们需要判断屏幕上的每个像素,是否被某一个三角形网格所覆盖,因此对于每个像素都需要对所有三角形网格进行遍历。如果一个像素被覆盖,那么会通过插值等方式计算出他的片元信息。<br>注意:这里的“片元”并不等同于“像素”,片元所包含的信息更加丰富(包括他的坐标、深度信息、顶点信息等等),这些信息用来最终生成一个像素的颜色。<br>三角形遍历阶段的最终输出就是一个片元序列,用于下一阶段的片元着色器。</p><h4 id="片元着色器"><a href="#片元着色器" class="headerlink" title="片元着色器"></a>片元着色器</h4><p>我们知道片元是用来最终生成一个像素所需的前置数据结构,这个由片元生成像素的过程,就是交由片元着色器完成的。这个过程是可以高度编程的,我们所能够实现的大部分效果,也都是在这一步进行的。片元着色器的最终输出是每个片元的一个或多个颜色值。在这里仍然没有得到最终的像素,但已经得到了每个片元的颜色信息,在下一步中,我们会得到每个像素的最终颜色值。</p><h4 id="逐片元操作"><a href="#逐片元操作" class="headerlink" title="逐片元操作"></a>逐片元操作</h4><p>逐片元操作的工作主要有两个:</p><ol><li>可见性测试</li><li>合并</li></ol><p>先说第一点,上面得到的片元信息中包括了每个片元的深度信息,这可以帮我们判断每个片元之间的覆盖情况,没有通过深度测试的片元意味着被其他片元所遮挡,因此会被舍弃。可见性测试不仅仅是深度测试,还包括模板测试等一系列其他测试,只有通过了可见性测试的片元才能进入接下来的合并阶段。</p><p>合并阶段相对而言容易理解,因为我们在渲染每一帧画面时遵照的顺序是依次渲染每一个三角形图元,因此为了生成一幅完整的画面,我们需要将当前渲染的三角形与之前已渲染的部分画面进行合并,最终得到一幅完整的画面。这个阶段也是可以高度配置的,我们可以指定合并的具体规则,从而实现包括透明之类的效果。</p><p>在整个阶段完成后,我们终于将片元信息转化为了每个像素的颜色值,最终生成了一幅画面。</p>]]></content>
<summary type="html">
<hr>
<h2 id="GPU流水线"><a href="#GPU流水线" class="headerlink" title="GPU流水线"></a>GPU流水线</h2><p>GPU流水线的大致流程和步骤如下图所示<br><img src="https://upload-i
</summary>
<category term="UnityShader学习笔记" scheme="https://njuwuyuxin.github.io/categories/UnityShader%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/"/>
<category term="Unity" scheme="https://njuwuyuxin.github.io/tags/Unity/"/>
<category term="Shader" scheme="https://njuwuyuxin.github.io/tags/Shader/"/>
<category term="学习笔记" scheme="https://njuwuyuxin.github.io/tags/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/"/>
</entry>
<entry>
<title>《Unity Shader 入门精要》学习笔记(一)—— 渲染流水线</title>
<link href="https://njuwuyuxin.github.io/2020/05/06/%E3%80%8AUnityShader%E3%80%8B%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0%EF%BC%88%E4%B8%80%EF%BC%89/"/>
<id>https://njuwuyuxin.github.io/2020/05/06/《UnityShader》学习笔记(一)/</id>
<published>2020-05-06T12:32:35.000Z</published>
<updated>2020-05-07T12:42:28.024Z</updated>
<content type="html"><![CDATA[<hr><p>最近在掌握了一些图形学基础后,下定决心要学习一下Shader相关内容,加之平时开发一些游戏Demo基本上基于Unity实现,于是搬出来《Unity Shader 入门精要》开始研读,顺便整理一下笔记加深理解。<br>本文主要是在每章学习后,首先凭印象整理出大致框架和重点概念,之后再参照原文进行校对勘误,如此一来加深了理解同时又能保证知识的准确性。</p><h2 id="渲染流水线"><a href="#渲染流水线" class="headerlink" title="渲染流水线"></a>渲染流水线</h2><p>渲染流水线就是将一幅画面渲染的过程拆分为多个阶段,每个阶段执行各自功能同时可以并行计算以提高速度。由于采用了流水线模式,因此整个渲染过程的耗时基本上取决于整条流水线中速度最慢的步骤。</p><p>概念上的渲染流水线主要分为三个阶段:应用阶段、几何阶段、光栅化阶段<br>他们只是从概念上将渲染流水线划分为三个主要步骤,实际GPU的流水线步骤会更加细化。</p><h4 id="应用阶段"><a href="#应用阶段" class="headerlink" title="应用阶段"></a>应用阶段</h4><p>应用阶段是用户可编程程度最高的步骤,通常是由CPU负责的部分。这个阶段主要任务是:</p><ol><li>准备好场景数据(包括需要渲染的物体、摄像机角度等)</li><li>进行粗粒度的剔除(剔除被遮挡的物体,与光栅化阶段有所不同)</li><li>设置每个模型的渲染状态(包括使用的材质、纹理、shader等)</li></ol><p>该阶段的最终输出就是渲染所需的几何信息,即渲染图元,重点是三维信息!</p><h4 id="几何阶段"><a href="#几何阶段" class="headerlink" title="几何阶段"></a>几何阶段</h4><p>几何阶段的主要工作是将上一步的渲染图元进行逐顶点、逐边的操作。<br>通俗来说就是决定需要绘制的图元是什么,绘制在什么位置,如何绘制等等。</p><p>这里的遮挡计算与应用阶段不同,应用阶段进行的是粗粒度的剔除,即完全被遮挡的物体会直接被剔除,以减少之后几何阶段的计算等。而几何阶段则需要计算各图元之间的部分遮挡,即一个图元哪些部分被遮挡不需绘制,哪些部分未被遮挡需要绘制。</p><p>其中最重要的一项工作就是将顶点坐标转换到屏幕空间中。<br>该阶段通常在GPU上进行,最终输出是屏幕空间的二维顶点坐标、每个顶点的相关信息等,重点是二维信息!</p><h4 id="光栅化阶段"><a href="#光栅化阶段" class="headerlink" title="光栅化阶段"></a>光栅化阶段</h4><p>光栅化阶段主要使用上一阶段提供的数据来产生屏幕上的像素。<br>这个阶段的主要任务是决定每个渲染图元中的哪些像素需要被绘制出来,这里需要用到上阶段提供的逐顶点信息,进行插值,进行逐像素的处理。</p><h2 id="CPU与GPU的通信"><a href="#CPU与GPU的通信" class="headerlink" title="CPU与GPU的通信"></a>CPU与GPU的通信</h2><p>通过上面我们了解到渲染流水线的开始是应用阶段,而应用阶段主要是由CPU进行,因此整个渲染过程也是由CPU发起,之后交由GPU进行处理。主要分为三个阶段:</p><ol><li>加载数据至显存(硬盘->内存->显存)</li><li>设置渲染状态(使用的材质、纹理、shader等)</li><li>发起DrawCall,告诉GPU已经准备好渲染所需信息,可以开始工作啦</li></ol><p>下一节主要整理关于GPU流水线的主要步骤和重点功能。</p>]]></content>
<summary type="html">
<hr>
<p>最近在掌握了一些图形学基础后,下定决心要学习一下Shader相关内容,加之平时开发一些游戏Demo基本上基于Unity实现,于是搬出来《Unity Shader 入门精要》开始研读,顺便整理一下笔记加深理解。<br>本文主要是在每章学习后,首先凭印象整理出大致框架
</summary>
<category term="UnityShader学习笔记" scheme="https://njuwuyuxin.github.io/categories/UnityShader%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/"/>
<category term="Unity" scheme="https://njuwuyuxin.github.io/tags/Unity/"/>
<category term="Shader" scheme="https://njuwuyuxin.github.io/tags/Shader/"/>
<category term="学习笔记" scheme="https://njuwuyuxin.github.io/tags/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/"/>
</entry>
<entry>
<title>从零搭建教务抢课系统(五)</title>
<link href="https://njuwuyuxin.github.io/2020/01/13/%E4%BB%8E%E9%9B%B6%E6%90%AD%E5%BB%BA%E6%95%99%E5%8A%A1%E6%8A%A2%E8%AF%BE%E7%B3%BB%E7%BB%9F%EF%BC%88%E4%BA%94%EF%BC%89/"/>
<id>https://njuwuyuxin.github.io/2020/01/13/从零搭建教务抢课系统(五)/</id>
<published>2020-01-13T10:42:21.000Z</published>
<updated>2020-05-06T12:25:27.814Z</updated>
<content type="html"><![CDATA[<hr><h3 id="目录"><a href="#目录" class="headerlink" title="目录"></a>目录</h3><p><a href="https://njuwuyuxin.github.io/2019/12/26/%E4%BB%8E%E9%9B%B6%E6%90%AD%E5%BB%BA%E6%95%99%E5%8A%A1%E6%8A%A2%E8%AF%BE%E7%B3%BB%E7%BB%9F%EF%BC%88%E4%B8%80%EF%BC%89/">(一)核心功能:模拟登陆</a><br><a href="https://njuwuyuxin.github.io/2019/12/27/%E4%BB%8E%E9%9B%B6%E6%90%AD%E5%BB%BA%E6%95%99%E5%8A%A1%E6%8A%A2%E8%AF%BE%E7%B3%BB%E7%BB%9F%EF%BC%88%E4%BA%8C%EF%BC%89/">(二)使用Cookie进行模拟登录</a><br><a href="https://njuwuyuxin.github.io/2019/12/28/%E4%BB%8E%E9%9B%B6%E6%90%AD%E5%BB%BA%E6%95%99%E5%8A%A1%E6%8A%A2%E8%AF%BE%E7%B3%BB%E7%BB%9F%EF%BC%88%E4%B8%89%EF%BC%89/">(三)获取教务网选课列表</a><br><a href="https://njuwuyuxin.github.io/2019/12/29/%E4%BB%8E%E9%9B%B6%E6%90%AD%E5%BB%BA%E6%95%99%E5%8A%A1%E6%8A%A2%E8%AF%BE%E7%B3%BB%E7%BB%9F%EF%BC%88%E5%9B%9B%EF%BC%89/">(四)循环选课</a><br><a href="https://njuwuyuxin.github.io/2020/01/13/%E4%BB%8E%E9%9B%B6%E6%90%AD%E5%BB%BA%E6%95%99%E5%8A%A1%E6%8A%A2%E8%AF%BE%E7%B3%BB%E7%BB%9F%EF%BC%88%E4%BA%94%EF%BC%89/">(五)断线重连</a></p><h6 id="Github链接:-https-jackfan.us.kg-njuwuyuxin-CourseGrabber"><a href="#Github链接:-https-jackfan.us.kg-njuwuyuxin-CourseGrabber" class="headerlink" title="Github链接: https://github.com/njuwuyuxin/CourseGrabber"></a>Github链接: <a href="https://github.com/njuwuyuxin/CourseGrabber" target="_blank" rel="noopener">https://github.com/njuwuyuxin/CourseGrabber</a></h6><h3 id="断线重连"><a href="#断线重连" class="headerlink" title="断线重连"></a>断线重连</h3><p>目前的抢课脚本的一个缺陷在于,当用户挂机进行自动抢课时,如果出现临时网络波动造成短时间断网,会造成抢课过程中断,程序异常退出。<br>而我们希望对于临时的网络中断或网络波动,可以自动尝试重连,自动恢复,防止一次临时断网导致程序退出。<br>因此这里为我们的抢课脚本加入了断线重连处理,主要思路为对requests请求失败时抛出的异常进行处理,如果发现了如连接失败或请求超时等情况,自动进行重试。<br>代码如下:</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">while True:</span><br><span class="line"> try:</span><br><span class="line"> selectResult = session.post(host+'student/elective/selectCourse.do',selectCourse_reqdata)</span><br><span class="line"> except requests.exceptions.ConnectionError:</span><br><span class="line"> connectionFailedFlag=True</span><br><span class="line"> print("连接超时,正在尝试重新连接")</span><br><span class="line"> time.sleep(1)</span><br><span class="line"> else:</span><br><span class="line"> if connectionFailedFlag:</span><br><span class="line"> connectionFailedFlag=False</span><br><span class="line"> print("重连成功,继续为您抢课")</span><br><span class="line"> break</span><br></pre></td></tr></table></figure><p>使用requests发出的post请求,当请求失败时会返回一个requests.exceptions.ConnectionError类型的异常。我们在外层使用了一个循环,如果请求成功则终止循环,如果接收到异常,则继续进行请求。</p><p>测试时首先启动抢课脚本开始抢课,期间手动断开电脑网络,一段时间后再重新连接网络,检查脚本是否能继续抢课。</p><p>测试结果如下:<br><img src="https://upload-images.jianshu.io/upload_images/16734657-b3edb7589a419553.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" alt="测试成功"></p>]]></content>
<summary type="html">
<hr>
<h3 id="目录"><a href="#目录" class="headerlink" title="目录"></a>目录</h3><p><a href="https://njuwuyuxin.github.io/2019/12/26/%E4%BB%8E%E9%9B%
</summary>
<category term="简单尝试" scheme="https://njuwuyuxin.github.io/categories/%E7%AE%80%E5%8D%95%E5%B0%9D%E8%AF%95/"/>
<category term="爬虫" scheme="https://njuwuyuxin.github.io/tags/%E7%88%AC%E8%99%AB/"/>
<category term="抢课系统" scheme="https://njuwuyuxin.github.io/tags/%E6%8A%A2%E8%AF%BE%E7%B3%BB%E7%BB%9F/"/>
<category term="http" scheme="https://njuwuyuxin.github.io/tags/http/"/>
</entry>
<entry>
<title>从零搭建教务抢课系统(四)</title>
<link href="https://njuwuyuxin.github.io/2019/12/29/%E4%BB%8E%E9%9B%B6%E6%90%AD%E5%BB%BA%E6%95%99%E5%8A%A1%E6%8A%A2%E8%AF%BE%E7%B3%BB%E7%BB%9F%EF%BC%88%E5%9B%9B%EF%BC%89/"/>
<id>https://njuwuyuxin.github.io/2019/12/29/从零搭建教务抢课系统(四)/</id>
<published>2019-12-29T06:17:18.000Z</published>
<updated>2020-05-06T12:25:27.814Z</updated>
<content type="html"><![CDATA[<h3 id="目录"><a href="#目录" class="headerlink" title="目录"></a>目录</h3><hr><p><a href="https://njuwuyuxin.github.io/2019/12/26/%E4%BB%8E%E9%9B%B6%E6%90%AD%E5%BB%BA%E6%95%99%E5%8A%A1%E6%8A%A2%E8%AF%BE%E7%B3%BB%E7%BB%9F%EF%BC%88%E4%B8%80%EF%BC%89/">(一)核心功能:模拟登陆</a><br><a href="https://njuwuyuxin.github.io/2019/12/27/%E4%BB%8E%E9%9B%B6%E6%90%AD%E5%BB%BA%E6%95%99%E5%8A%A1%E6%8A%A2%E8%AF%BE%E7%B3%BB%E7%BB%9F%EF%BC%88%E4%BA%8C%EF%BC%89/">(二)使用Cookie进行模拟登录</a><br><a href="https://njuwuyuxin.github.io/2019/12/28/%E4%BB%8E%E9%9B%B6%E6%90%AD%E5%BB%BA%E6%95%99%E5%8A%A1%E6%8A%A2%E8%AF%BE%E7%B3%BB%E7%BB%9F%EF%BC%88%E4%B8%89%EF%BC%89/">(三)获取教务网选课列表</a><br><a href="https://njuwuyuxin.github.io/2019/12/29/%E4%BB%8E%E9%9B%B6%E6%90%AD%E5%BB%BA%E6%95%99%E5%8A%A1%E6%8A%A2%E8%AF%BE%E7%B3%BB%E7%BB%9F%EF%BC%88%E5%9B%9B%EF%BC%89/">(四)循环选课</a><br><a href="https://njuwuyuxin.github.io/2020/01/13/%E4%BB%8E%E9%9B%B6%E6%90%AD%E5%BB%BA%E6%95%99%E5%8A%A1%E6%8A%A2%E8%AF%BE%E7%B3%BB%E7%BB%9F%EF%BC%88%E4%BA%94%EF%BC%89/">(五)断线重连</a></p><h6 id="Github链接:-https-jackfan.us.kg-njuwuyuxin-CourseGrabber"><a href="#Github链接:-https-jackfan.us.kg-njuwuyuxin-CourseGrabber" class="headerlink" title="Github链接: https://github.com/njuwuyuxin/CourseGrabber"></a>Github链接: <a href="https://github.com/njuwuyuxin/CourseGrabber" target="_blank" rel="noopener">https://github.com/njuwuyuxin/CourseGrabber</a></h6><h3 id="循环选课"><a href="#循环选课" class="headerlink" title="循环选课"></a>循环选课</h3><p>在成功实现了登陆系统,拉取课程列表之后,我们离成功只差最后一步,只需要模拟浏览器,向对应端口发送选课请求即可。<br>手动在网页上选择任意一门课之后发现,选课请求体结构非常简单,同样为Post请求</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">{</span><br><span class="line"> 'method':'addSpecialitySelect',</span><br><span class="line"> 'classId':'xxxx'</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这里的classID就是上一篇中提到的后台为每个课程标记的ID,并不是大家平时使用的课程号。好在上一篇中,我们已经对每门课的序号和课程ID进行了映射。</p><p>基本思路理清后,代码的部分就相对非常简单。</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line">def GrabCourse(courseID,interval=0):</span><br><span class="line"> while(True):</span><br><span class="line"> selectCourse_reqdata={}</span><br><span class="line"> selectCourse_reqdata['method']="addSpecialitySelect"</span><br><span class="line"> selectCourse_reqdata['classId']=str(courseID)</span><br><span class="line"> selectResult = s.post(host+'student/elective/selectCourse.do',selectCourse_reqdata)</span><br><span class="line"> soup = BeautifulSoup(selectResult.content,"html.parser",from_encoding='utf-8')</span><br><span class="line"> for tag in soup.find_all('div'):</span><br><span class="line"> if tag.get('id')=="successMsg":</span><br><span class="line"> print("抢课成功!")</span><br><span class="line"> return</span><br><span class="line"> elif tag.get('id')=="errMsg": </span><br><span class="line"> if tag.string.find("已经")!=-1:</span><br><span class="line"> print("您已经抢到该课程啦~")</span><br><span class="line"> exit()</span><br><span class="line"> elif tag.string.find("错误")!=-1:</span><br><span class="line"> print("出现错误,添加失败")</span><br><span class="line"> exit()</span><br><span class="line"> else:</span><br><span class="line"> print("当前班级已满,仍在为您持续抢课")</span><br><span class="line"> else:</span><br><span class="line"> pass</span><br><span class="line"> if interval!=0:</span><br><span class="line"> time.sleep(interval)</span><br></pre></td></tr></table></figure><p>这里的GrabCourse函数接收两个参数,第一个就是课程ID,第二个为一个可调的时间间隔。为了避免对教务系统造成过大负担(防止被查水表),这里默认设置了每次发送选课请求的时间间隔为1秒。</p><p>同时对每次选课请求的返回进行一下检验,主要分为四种情况:</p><ol><li>选课成功:理想情况</li><li>已经选课:证明课表这已经选中这门课</li><li>班级已满:抢课系统主要针对的正是这种情况,班级满时需要循环发送请求,等待班级空出位置的瞬间。</li><li>出现错误:多为课程ID填写错误,或者选择了其他院系专业课(没有选课权限)等情况</li></ol><p>对每种情况分别处理即可</p><h3 id="其他尝试"><a href="#其他尝试" class="headerlink" title="其他尝试"></a>其他尝试</h3><p>由于之前拉取课程列表时,尝试通过填写其他院系编号来构造请求体,成功拉取到了其他院系的课表,可知教务系统后端对院系方面审核并不十分严格。因此在选课阶段同样进行了类似的尝试(作死),方法同样是在构造选课请求时,填写其他院系课程的课程ID<br>结果:返回“出现错误,添加失败”(笑)<br>可见教务平台至少在选课的时候还是稍微做了一下身份验证。不过至此,整个抢课系统的基本功能已经实现。可以成功登录、获取列表、循环发送选课请求。接下来的工作就是优化人机交互以及断线重连相关功能。</p>]]></content>
<summary type="html">
<h3 id="目录"><a href="#目录" class="headerlink" title="目录"></a>目录</h3><hr>
<p><a href="https://njuwuyuxin.github.io/2019/12/26/%E4%BB%8E%E9%9B%
</summary>
<category term="简单尝试" scheme="https://njuwuyuxin.github.io/categories/%E7%AE%80%E5%8D%95%E5%B0%9D%E8%AF%95/"/>
<category term="爬虫" scheme="https://njuwuyuxin.github.io/tags/%E7%88%AC%E8%99%AB/"/>
<category term="抢课系统" scheme="https://njuwuyuxin.github.io/tags/%E6%8A%A2%E8%AF%BE%E7%B3%BB%E7%BB%9F/"/>
<category term="http" scheme="https://njuwuyuxin.github.io/tags/http/"/>
</entry>
<entry>
<title>从零搭建教务抢课系统(三)</title>
<link href="https://njuwuyuxin.github.io/2019/12/28/%E4%BB%8E%E9%9B%B6%E6%90%AD%E5%BB%BA%E6%95%99%E5%8A%A1%E6%8A%A2%E8%AF%BE%E7%B3%BB%E7%BB%9F%EF%BC%88%E4%B8%89%EF%BC%89/"/>
<id>https://njuwuyuxin.github.io/2019/12/28/从零搭建教务抢课系统(三)/</id>
<published>2019-12-27T16:26:23.000Z</published>
<updated>2020-05-06T12:25:27.812Z</updated>
<content type="html"><![CDATA[<hr><h3 id="目录"><a href="#目录" class="headerlink" title="目录"></a>目录</h3><p><a href="https://njuwuyuxin.github.io/2019/12/26/%E4%BB%8E%E9%9B%B6%E6%90%AD%E5%BB%BA%E6%95%99%E5%8A%A1%E6%8A%A2%E8%AF%BE%E7%B3%BB%E7%BB%9F%EF%BC%88%E4%B8%80%EF%BC%89/">(一)核心功能:模拟登陆</a><br><a href="https://njuwuyuxin.github.io/2019/12/27/%E4%BB%8E%E9%9B%B6%E6%90%AD%E5%BB%BA%E6%95%99%E5%8A%A1%E6%8A%A2%E8%AF%BE%E7%B3%BB%E7%BB%9F%EF%BC%88%E4%BA%8C%EF%BC%89/">(二)使用Cookie进行模拟登录</a><br><a href="https://njuwuyuxin.github.io/2019/12/28/%E4%BB%8E%E9%9B%B6%E6%90%AD%E5%BB%BA%E6%95%99%E5%8A%A1%E6%8A%A2%E8%AF%BE%E7%B3%BB%E7%BB%9F%EF%BC%88%E4%B8%89%EF%BC%89/">(三)获取教务网选课列表</a><br><a href="https://njuwuyuxin.github.io/2019/12/29/%E4%BB%8E%E9%9B%B6%E6%90%AD%E5%BB%BA%E6%95%99%E5%8A%A1%E6%8A%A2%E8%AF%BE%E7%B3%BB%E7%BB%9F%EF%BC%88%E5%9B%9B%EF%BC%89/">(四)循环选课</a><br><a href="https://njuwuyuxin.github.io/2020/01/13/%E4%BB%8E%E9%9B%B6%E6%90%AD%E5%BB%BA%E6%95%99%E5%8A%A1%E6%8A%A2%E8%AF%BE%E7%B3%BB%E7%BB%9F%EF%BC%88%E4%BA%94%EF%BC%89/">(五)断线重连</a></p><h6 id="Github链接:-https-jackfan.us.kg-njuwuyuxin-CourseGrabber"><a href="#Github链接:-https-jackfan.us.kg-njuwuyuxin-CourseGrabber" class="headerlink" title="Github链接: https://github.com/njuwuyuxin/CourseGrabber"></a>Github链接: <a href="https://github.com/njuwuyuxin/CourseGrabber" target="_blank" rel="noopener">https://github.com/njuwuyuxin/CourseGrabber</a></h6><h3 id="获取选课列表"><a href="#获取选课列表" class="headerlink" title="获取选课列表"></a>获取选课列表</h3><p>在研究了教务网的js代码以及抓包分析之后,发现教务网拉取专业选课列表的接口接收不同数量的参数(有默认参数)<br>专业选课页面的加载逻辑为,初次进入页面自动发送一条post请求,请求体为</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">{</span><br><span class="line"> 'method':'specialityCourseList'</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>该请求只包含调用的方法名,其余均为默认参数,用来拉取默认显示的课程列表</p><p>当用户在下拉选单中手动选择某项之后,会向同一端口再次发送一条post请求,请求体为</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">{</span><br><span class="line"> 'method':'specialityCourseList',</span><br><span class="line"> 'specialityCode':'221',</span><br><span class="line"> 'courseGrade':'2016</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这次请求体中包含了所在专业的专业编号,以及对应年级</p><h4 id="解析网页获得课程列表"><a href="#解析网页获得课程列表" class="headerlink" title="解析网页获得课程列表"></a>解析网页获得课程列表</h4><p>按照请求格式构造好请求体中,response返回的HTML文档就是包含了课程列表的页面<br>这里使用了Beautiful Soup进行解析,可以看到课程列表是以<tr>、 <td>标签的形式进行显示,按对应格式解析即可</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line">soup = BeautifulSoup(courseList.content,"html.parser",from_encoding='utf-8')</span><br><span class="line">soup = BeautifulSoup(courseList.content,"html.parser",from_encoding='utf-8')</span><br><span class="line">trs = soup.find_all('tr',{'class':'TABLE_TR_01'})</span><br><span class="line">print("序号\t课程号\t\t课程名\t\t\t学分\t学时\t类型\t开课院系")</span><br><span class="line">for tr in trs:</span><br><span class="line"> tds = tr.find_all('td')</span><br><span class="line"> courseNo = tds[0].find('a').find('u').string</span><br><span class="line"> if(tds[1].string.__len__()<=7):</span><br><span class="line"> print(str(trs.index(tr)+1)+'\t'+courseNo+'\t'+tds[1].string+'\t\t'+tds[2].string+'\t'+tds[3].string+'\t'+tds[4].string+'\t'+tds[6].string)</span><br><span class="line"> else:</span><br><span class="line"> print(str(trs.index(tr)+1)+'\t'+courseNo+'\t'+tds[1].string+'\t'+tds[2].string+'\t'+tds[3].string+'\t'+tds[4].string+'\t'+tds[6].string)</span><br><span class="line"> click_td = tr.find('td',{'onclick':True})</span><br><span class="line"> if click_td==None:</span><br><span class="line"> courseIdList.append("")</span><br><span class="line"> pass</span><br><span class="line"> else:</span><br><span class="line"> # print(click_td['onclick'])</span><br><span class="line"> js = click_td['onclick']</span><br><span class="line"> args = js.split(',')</span><br><span class="line"> courseID = args[4][0:5]</span><br><span class="line"> courseIdList.append(courseID)</span><br></pre></td></tr></table></figure><p>由于教务网后端较为特殊,选课的请求体中课程id有单独编号需要提取,而不是使用课程编号(后文有讲),因此额外做了一些解析,HTML解析这里不具有普遍参考价值.</p><p>获取的课程列表展示如下:<br><img src="https://upload-images.jianshu.io/upload_images/16734657-21ed6aa2ccdb6724.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" alt="课程列表"></p><h4 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h4><p><a href="https://www.crummy.com/software/BeautifulSoup/bs3/documentation.zh.html" target="_blank" rel="noopener">Beautiful Soup4 中文文档</a></p>]]></content>
<summary type="html">
<hr>
<h3 id="目录"><a href="#目录" class="headerlink" title="目录"></a>目录</h3><p><a href="https://njuwuyuxin.github.io/2019/12/26/%E4%BB%8E%E9%9B%
</summary>
<category term="简单尝试" scheme="https://njuwuyuxin.github.io/categories/%E7%AE%80%E5%8D%95%E5%B0%9D%E8%AF%95/"/>
<category term="爬虫" scheme="https://njuwuyuxin.github.io/tags/%E7%88%AC%E8%99%AB/"/>
<category term="抢课系统" scheme="https://njuwuyuxin.github.io/tags/%E6%8A%A2%E8%AF%BE%E7%B3%BB%E7%BB%9F/"/>
<category term="http" scheme="https://njuwuyuxin.github.io/tags/http/"/>
</entry>
<entry>
<title>从零搭建教务抢课系统(二)</title>
<link href="https://njuwuyuxin.github.io/2019/12/27/%E4%BB%8E%E9%9B%B6%E6%90%AD%E5%BB%BA%E6%95%99%E5%8A%A1%E6%8A%A2%E8%AF%BE%E7%B3%BB%E7%BB%9F%EF%BC%88%E4%BA%8C%EF%BC%89/"/>
<id>https://njuwuyuxin.github.io/2019/12/27/从零搭建教务抢课系统(二)/</id>
<published>2019-12-27T12:56:17.000Z</published>
<updated>2020-05-06T12:25:27.813Z</updated>
<content type="html"><![CDATA[<hr><h3 id="目录"><a href="#目录" class="headerlink" title="目录"></a>目录</h3><p><a href="https://njuwuyuxin.github.io/2019/12/26/%E4%BB%8E%E9%9B%B6%E6%90%AD%E5%BB%BA%E6%95%99%E5%8A%A1%E6%8A%A2%E8%AF%BE%E7%B3%BB%E7%BB%9F%EF%BC%88%E4%B8%80%EF%BC%89/">(一)核心功能:模拟登陆</a><br><a href="https://njuwuyuxin.github.io/2019/12/27/%E4%BB%8E%E9%9B%B6%E6%90%AD%E5%BB%BA%E6%95%99%E5%8A%A1%E6%8A%A2%E8%AF%BE%E7%B3%BB%E7%BB%9F%EF%BC%88%E4%BA%8C%EF%BC%89/">(二)使用Cookie进行模拟登录</a><br><a href="https://njuwuyuxin.github.io/2019/12/28/%E4%BB%8E%E9%9B%B6%E6%90%AD%E5%BB%BA%E6%95%99%E5%8A%A1%E6%8A%A2%E8%AF%BE%E7%B3%BB%E7%BB%9F%EF%BC%88%E4%B8%89%EF%BC%89/">(三)获取教务网选课列表</a><br><a href="https://njuwuyuxin.github.io/2019/12/29/%E4%BB%8E%E9%9B%B6%E6%90%AD%E5%BB%BA%E6%95%99%E5%8A%A1%E6%8A%A2%E8%AF%BE%E7%B3%BB%E7%BB%9F%EF%BC%88%E5%9B%9B%EF%BC%89/">(四)循环选课</a><br><a href="https://njuwuyuxin.github.io/2020/01/13/%E4%BB%8E%E9%9B%B6%E6%90%AD%E5%BB%BA%E6%95%99%E5%8A%A1%E6%8A%A2%E8%AF%BE%E7%B3%BB%E7%BB%9F%EF%BC%88%E4%BA%94%EF%BC%89/">(五)断线重连</a></p><h6 id="Github链接:-https-jackfan.us.kg-njuwuyuxin-CourseGrabber"><a href="#Github链接:-https-jackfan.us.kg-njuwuyuxin-CourseGrabber" class="headerlink" title="Github链接: https://github.com/njuwuyuxin/CourseGrabber"></a>Github链接: <a href="https://github.com/njuwuyuxin/CourseGrabber" target="_blank" rel="noopener">https://github.com/njuwuyuxin/CourseGrabber</a></h6><h3 id="使用cookie模拟登录"><a href="#使用cookie模拟登录" class="headerlink" title="使用cookie模拟登录"></a>使用cookie模拟登录</h3><p>在成功实现了基本登陆后,为了方便使用,这里尝试使用cookie进行登录</p><p>首先我们在创建session时,初始cookie为空,可以打印进行查看</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">s = requests.session()</span><br><span class="line">print(s.cookies.get_dict())</span><br></pre></td></tr></table></figure><p>在构造登陆请求体,成功登陆之后,session中的cookies被自动更新,可以打印查看,南大教务平台的Cookie形如</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">{</span><br><span class="line"> 'user_id':'"1612xxxxx 1577448172273"',</span><br><span class="line"> 'ARRAffinity':'80372ade9da56061dc1cfb0f216b6917726c2c01e3d804e60cad7fce0e0af662',</span><br><span class="line"> 'JSESSIONID':'8D6204D5EDBE04DC6088DB9BE43A5924'</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>可以看到共有三个表项,这里在成功登录之后,手动将其保存到本地文件中。这里没有使用相关库函数,而是手动实现了简单的cookie存储</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">def SaveCookie(session):</span><br><span class="line"> with open(".cookie",'w') as f:</span><br><span class="line"> for key,val in session.cookies.get_dict().items():</span><br><span class="line"> f.write(key+":"+val+'\n')</span><br></pre></td></tr></table></figure><p>同时实现了从文件读取cookie的方法</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">def GetCookie(session):</span><br><span class="line"> cookie = {}</span><br><span class="line"> if ".cookie" not in os.listdir():</span><br><span class="line"> return cookie</span><br><span class="line"> with open(".cookie",'r') as f:</span><br><span class="line"> for line in f:</span><br><span class="line"> line = line.replace('\n','').replace('\r','')</span><br><span class="line"> item = line.split(':')</span><br><span class="line"> cookie[item[0]] = item[1]</span><br><span class="line"> return cookie</span><br></pre></td></tr></table></figure><p>cookie的保存与读取实现之后,登陆部分的逻辑可以改为:</p><ol><li>首先检查本地是否存在cookie</li><li>如果存在cookie,尝试使用cookie登录;如果不存在,直接使用账号密码登录</li><li>如果cookie登录成功,直接进入系统;如果cookie已过期,则重新使用账号密码登录,并更新本地cookie</li></ol><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">#获取cookie,如果本地有cookie,尝试使用cookie登录</span><br><span class="line"> c = GetCookie(session)</span><br><span class="line"> session.cookies.update(c)</span><br><span class="line"> if c:</span><br><span class="line"> rs = session.get(host+"student/index.do") #教务平台首页,如果能够进入,说明已成功登录</span><br><span class="line"> if rs.content.__len__() > 5000:</span><br><span class="line"> print("登陆成功!")</span><br><span class="line"> return True</span><br><span class="line"> else:</span><br><span class="line"> print("登录已过期,请重新登录")</span><br></pre></td></tr></table></figure><p>这样我们基本实现了使用cookie进行登录,避免了重复输入账号密码及验证码的验证</p>]]></content>
<summary type="html">
<hr>
<h3 id="目录"><a href="#目录" class="headerlink" title="目录"></a>目录</h3><p><a href="https://njuwuyuxin.github.io/2019/12/26/%E4%BB%8E%E9%9B%
</summary>
<category term="简单尝试" scheme="https://njuwuyuxin.github.io/categories/%E7%AE%80%E5%8D%95%E5%B0%9D%E8%AF%95/"/>
<category term="爬虫" scheme="https://njuwuyuxin.github.io/tags/%E7%88%AC%E8%99%AB/"/>
<category term="抢课系统" scheme="https://njuwuyuxin.github.io/tags/%E6%8A%A2%E8%AF%BE%E7%B3%BB%E7%BB%9F/"/>
<category term="http" scheme="https://njuwuyuxin.github.io/tags/http/"/>
</entry>
<entry>
<title>从零搭建教务抢课系统(一)</title>
<link href="https://njuwuyuxin.github.io/2019/12/26/%E4%BB%8E%E9%9B%B6%E6%90%AD%E5%BB%BA%E6%95%99%E5%8A%A1%E6%8A%A2%E8%AF%BE%E7%B3%BB%E7%BB%9F%EF%BC%88%E4%B8%80%EF%BC%89/"/>
<id>https://njuwuyuxin.github.io/2019/12/26/从零搭建教务抢课系统(一)/</id>
<published>2019-12-26T06:52:16.000Z</published>
<updated>2020-01-13T10:44:28.849Z</updated>
<content type="html"><![CDATA[<hr><h3 id="目录"><a href="#目录" class="headerlink" title="目录"></a>目录</h3><p><a href="https://njuwuyuxin.github.io/2019/12/26/%E4%BB%8E%E9%9B%B6%E6%90%AD%E5%BB%BA%E6%95%99%E5%8A%A1%E6%8A%A2%E8%AF%BE%E7%B3%BB%E7%BB%9F%EF%BC%88%E4%B8%80%EF%BC%89/">(一)核心功能:模拟登陆</a><br><a href="https://njuwuyuxin.github.io/2019/12/27/%E4%BB%8E%E9%9B%B6%E6%90%AD%E5%BB%BA%E6%95%99%E5%8A%A1%E6%8A%A2%E8%AF%BE%E7%B3%BB%E7%BB%9F%EF%BC%88%E4%BA%8C%EF%BC%89/">(二)使用Cookie进行模拟登录</a><br><a href="https://njuwuyuxin.github.io/2019/12/28/%E4%BB%8E%E9%9B%B6%E6%90%AD%E5%BB%BA%E6%95%99%E5%8A%A1%E6%8A%A2%E8%AF%BE%E7%B3%BB%E7%BB%9F%EF%BC%88%E4%B8%89%EF%BC%89/">(三)获取教务网选课列表</a><br><a href="https://njuwuyuxin.github.io/2019/12/29/%E4%BB%8E%E9%9B%B6%E6%90%AD%E5%BB%BA%E6%95%99%E5%8A%A1%E6%8A%A2%E8%AF%BE%E7%B3%BB%E7%BB%9F%EF%BC%88%E5%9B%9B%EF%BC%89/">(四)循环选课</a><br><a href="https://njuwuyuxin.github.io/2020/01/13/%E4%BB%8E%E9%9B%B6%E6%90%AD%E5%BB%BA%E6%95%99%E5%8A%A1%E6%8A%A2%E8%AF%BE%E7%B3%BB%E7%BB%9F%EF%BC%88%E4%BA%94%EF%BC%89/">(五)断线重连</a></p><h6 id="Github链接:-https-jackfan.us.kg-njuwuyuxin-CourseGrabber"><a href="#Github链接:-https-jackfan.us.kg-njuwuyuxin-CourseGrabber" class="headerlink" title="Github链接: https://github.com/njuwuyuxin/CourseGrabber"></a>Github链接: <a href="https://github.com/njuwuyuxin/CourseGrabber" target="_blank" rel="noopener">https://github.com/njuwuyuxin/CourseGrabber</a></h6><h3 id="一、前言"><a href="#一、前言" class="headerlink" title="一、前言"></a>一、前言</h3><p>响应群里学弟学妹号召,心血来潮想做一个南大教务网的抢课系统。在仔细研究了一下教务平台的js代码之后,发现几乎没有什么防护措施,于是便开始着手尝试起来。</p><p>抢课系统的主要思路无非就是以下几步:</p><ol><li>模拟教务平台的网页登录,获取session</li><li>登入平台后拉取各个课程列表</li><li>找到对应课程编号,构造选课请求体,循环发送</li></ol><p>为了方便调试检验,还使用了wireshark进行简单抓包(可省略),有需要的小伙伴可以自行下载</p><h3 id="二、模拟登陆"><a href="#二、模拟登陆" class="headerlink" title="二、模拟登陆"></a>二、模拟登陆</h3><p>首先进入教务网登陆界面,尝试一次普通登录<br><img src="https://s2.ax1x.com/2019/12/26/lAPbLT.png" alt="登录请求体.png"><br>可以发现登录的请求体主要由四个表项组成,returnUrl暂无具体含义,默认为null,其余均为用户提交表单。<br>这里唯一比较棘手的一点是验证码的获取。</p><h4 id="验证码获取"><a href="#验证码获取" class="headerlink" title="验证码获取"></a>验证码获取</h4><p>南大教务平台的验证码是通过向后端jsp请求获得。因此在代码里我们同样用模拟浏览器的方式进行请求。</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">#首先创建一个session</span><br><span class="line">session = requests.session()</span><br><span class="line"></span><br><span class="line">#取得验证码图片</span><br><span class="line">now_time = str(int(time.time()))</span><br><span class="line">pic_url = host + 'ValidateCode.jsp'</span><br><span class="line">pic = session.get(pic_url).content</span><br><span class="line">im = Image.open(BytesIO(pic)) #直接打开图片</span><br><span class="line"> im.show()</span><br><span class="line">filename = '' + now_time + '.jpg' </span><br><span class="line">with open(filename, 'wb') as f:</span><br><span class="line"> f.write(pic)</span><br></pre></td></tr></table></figure><p>这里首先创建了一个session,确保获取验证码和登录请求为同一个session,向对应jsp请求,将请求获得的图片保存在本地。<br>之后尝试使用了ocr进行验证码的自动识别,由于验证码干扰严重,OCR无法识别,因而放弃</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">#尝试使用OCR自动识别验证码,但是由于验证码干扰较多,不能正确识别,因此采用手动输入方式</span><br><span class="line"># img = Image.open(filename)</span><br><span class="line"># img=img.convert('L')</span><br><span class="line"># vcode = pytesseract.image_to_string(img) # 使用ocr技术将图片中的验证码读取出来</span><br><span class="line"># time.sleep(0.3) </span><br><span class="line"># print(vcode)</span><br></pre></td></tr></table></figure><p>OCR无法自动识别,那么我们只能采用手动输入验证码的方式,每次登陆时根据获取到本地的验证码进行输入,登陆后自动删除临时图片。<br>同时发现验证码大约有100秒有效时间,因此需及时输入,否则验证码过期需要重新获取</p><h4 id="登录请求体构造"><a href="#登录请求体构造" class="headerlink" title="登录请求体构造"></a>登录请求体构造</h4><p>之后我们就可以构造登录请求体,这里为了方便测试,可以选择性读取存储用户信息的配置文件,也可以控制台进行输入</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line">login_data={}</span><br><span class="line">files = os.listdir()</span><br><span class="line">if "user.cfg" in files:</span><br><span class="line"> with open("user.cfg",'r') as f:</span><br><span class="line"> for line in f:</span><br><span class="line"> items = line.split(":")</span><br><span class="line"> items[1]=items[1].replace('\n','').replace('\r','')</span><br><span class="line"> login_data[items[0]]=items[1]</span><br><span class="line">else:</span><br><span class="line"> print("请输入用户名")</span><br><span class="line"> login_data['userName']=input()</span><br><span class="line"> print("请输入密码")</span><br><span class="line"> login_data['password']=input()</span><br><span class="line"> </span><br><span class="line">login_data['retrunURL']="null"</span><br><span class="line">print("请输入验证码(Please enter the ValidateCode)")</span><br><span class="line">vcode=input()</span><br><span class="line">os.remove(filename) #输入完验证码后自动删除本地图片 </span><br><span class="line">login_data['ValidateCode']=vcode</span><br></pre></td></tr></table></figure><h4 id="发送登录请求"><a href="#发送登录请求" class="headerlink" title="发送登录请求"></a>发送登录请求</h4><p>构造好请求体之后,我们将对应post请求发送到后端端口即可,这里由于无论登陆成功或失败,都会返回200表示请求成功,并不代表登陆成功。而返回的response分别对应错误页面的html和成功页面的html,因此这里简单对response长度进行判断来判断是否登陆成功。</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">#发送登录请求</span><br><span class="line">response = session.post(host+"login.do",login_data)</span><br><span class="line">if response.content.__len__() > 1100:</span><br><span class="line"> print("登陆成功!")</span><br><span class="line"> return True</span><br><span class="line">else:</span><br><span class="line"> print("登录失败,请检查账号密码及验证码")</span><br><span class="line"> return False</span><br></pre></td></tr></table></figure><p>输入完用户信息后,成功登录后,wireshark抓包可以看到对应数据包<br><img src="https://s2.ax1x.com/2019/12/26/lAEzMn.png" alt="lAEzMn.png"><br>打印请求体后,可以发现正是教务平台登陆成功后的主页的html,至此,抢课系统的核心登录部分已经完成。之后可以解析HTML获得相关信息(类似爬虫),或发送选课请求等均可。</p>]]></content>
<summary type="html">
<hr>
<h3 id="目录"><a href="#目录" class="headerlink" title="目录"></a>目录</h3><p><a href="https://njuwuyuxin.github.io/2019/12/26/%E4%BB%8E%E9%9B%
</summary>
<category term="简单尝试" scheme="https://njuwuyuxin.github.io/categories/%E7%AE%80%E5%8D%95%E5%B0%9D%E8%AF%95/"/>
<category term="爬虫" scheme="https://njuwuyuxin.github.io/tags/%E7%88%AC%E8%99%AB/"/>
<category term="抢课系统" scheme="https://njuwuyuxin.github.io/tags/%E6%8A%A2%E8%AF%BE%E7%B3%BB%E7%BB%9F/"/>
<category term="http" scheme="https://njuwuyuxin.github.io/tags/http/"/>
</entry>
<entry>
<title>nodejs+express框架搭建简单后端服务</title>
<link href="https://njuwuyuxin.github.io/2019/06/07/nodejs-express%E6%A1%86%E6%9E%B6%E6%90%AD%E5%BB%BA%E7%AE%80%E5%8D%95%E5%90%8E%E7%AB%AF%E6%9C%8D%E5%8A%A1/"/>
<id>https://njuwuyuxin.github.io/2019/06/07/nodejs-express框架搭建简单后端服务/</id>
<published>2019-06-07T08:51:40.000Z</published>
<updated>2019-12-26T10:55:30.583Z</updated>
<content type="html"><![CDATA[<hr><h2 id="Node安装"><a href="#Node安装" class="headerlink" title="Node安装"></a>Node安装</h2><p>由于后端服务通常部署在linux服务器上,因此简单说下linux环境下node的安装。 可以选择去官网下载编译好的二进制文件,软链接到环境目录下。也可以使用apt工具直接安装</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">sudo apt-get install node</span><br></pre></td></tr></table></figure><h3 id="Express框架"><a href="#Express框架" class="headerlink" title="Express框架"></a>Express框架</h3><p>express是一个功能十分强大的框架,可以同时兼顾前后端开发。但由于这次只是想用express实现后端服务,因此不需要express提供的前端开发模板相关功能。所以只是在项目中引入了express模块</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npm install express</span><br></pre></td></tr></table></figure><p>之后就可以在项目中通过require的方式使用express模块</p><h4 id="Express的使用"><a href="#Express的使用" class="headerlink" title="Express的使用"></a>Express的使用</h4><p>首先需要在需要的文件中引入express模块</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">var express = require('epxress');</span><br><span class="line">var app = express();</span><br></pre></td></tr></table></figure><p>之后需要创建一个http服务器,但是由于我的网站而言,需要提供https服务,因此创建了一个https服务器</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">var httpsServer = https.createServer(options, app);</span><br><span class="line">httpsServer.listen(parseInt(config.port),function(){</span><br><span class="line"> console.log("Https server is running on: https://localhost:"+config.port);</span><br><span class="line">});</span><br></pre></td></tr></table></figure><p>创建https服务器时需要一个额外参数option,用来指定服务器所需证书的路径,只有证书有效,才能创建https服务。<br>至于端口号,可以自行指定,由于网站前端运行在默认443端口,因此选择不冲突的端口即可。</p><p>创建好服务器之后,我们就可以用app实例去监听对应的请求。<br>express框架为我们实现了路由功能,因此可以很方便的通过路径来区分各种请求。</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">app.get('/api/activities',newsApi.getActivities);</span><br><span class="line">app.get('/api/activityCards',newsApi.getActivityCards);</span><br><span class="line">app.post('/api/reviewCards',newsApi.getReviewCards);</span><br><span class="line"></span><br><span class="line">function getActivities(req, res){</span><br><span class="line"> ...</span><br><span class="line"> ...</span><br><span class="line"> res.send('...')</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>通过调用app的get和post方法,我们可以处理get和post请求,第一个参数即为路由的路径,第二个参数为一个函数闭包,用来处理对应的请求。该闭包会接受两个参数req和res,分别对应请求体和返回的内容</p>]]></content>
<summary type="html">
<hr>
<h2 id="Node安装"><a href="#Node安装" class="headerlink" title="Node安装"></a>Node安装</h2><p>由于后端服务通常部署在linux服务器上,因此简单说下linux环境下node的安装。 可以选择去
</summary>
<category term="后端学习" scheme="https://njuwuyuxin.github.io/categories/%E5%90%8E%E7%AB%AF%E5%AD%A6%E4%B9%A0/"/>
<category term="nodejs" scheme="https://njuwuyuxin.github.io/tags/nodejs/"/>
<category term="express" scheme="https://njuwuyuxin.github.io/tags/express/"/>
<category term="后端" scheme="https://njuwuyuxin.github.io/tags/%E5%90%8E%E7%AB%AF/"/>
</entry>
<entry>
<title>Git快速上手</title>
<link href="https://njuwuyuxin.github.io/2019/04/18/git%E5%BF%AB%E9%80%9F%E4%B8%8A%E6%89%8B/"/>
<id>https://njuwuyuxin.github.io/2019/04/18/git快速上手/</id>
<published>2019-04-18T06:54:47.000Z</published>
<updated>2020-06-24T12:57:06.450Z</updated>
<content type="html"><![CDATA[<hr><h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>git作为一个先进的版本管理工具,已经被广泛应用在大量项目中。近来发现了一个非常不错的git学习网站,虽然比较基础,但是可视化的界面能够帮助新人快速理解git每项指令的功能,同时也可以一定程度上的查漏补缺。<br>网站地址:<a href="https://learngitbranching.js.org/" target="_blank" rel="noopener">https://learngitbranching.js.org/</a><br>而本文也记录了一些常用的git指令和使用技巧</p><h2 id="常用git指令"><a href="#常用git指令" class="headerlink" title="常用git指令"></a>常用git指令</h2><h3 id="新建仓库"><a href="#新建仓库" class="headerlink" title="新建仓库"></a>新建仓库</h3><p>在当前目录初始化一个git仓库<br><code>git init</code><br>新建一个目录,初始化一个git仓库<br><code>git init [projectName]</code><br>用于从远程仓库进行克隆,一般可以选择通过https或者ssh方式<br><code>git clone [url]</code> </p><h3 id="配置"><a href="#配置" class="headerlink" title="配置"></a>配置</h3><p>对于刚安装git的新人,一般需要配置邮箱和用户名,建议使用全局方式配置<br><code>git conifg [--global] user.name "[username]"</code><br><code>git conifg [--global] user.email "[email]"</code><br>可以查看当前的git配置<br><code>git config --list</code><br>可以直接编辑git配置文件,之前通过命令行配置的在此也可以看到<br><code>git config -e [--global]</code> </p><h3 id="增加、删除文件"><a href="#增加、删除文件" class="headerlink" title="增加、删除文件"></a>增加、删除文件</h3><p>这里需要简单介绍一下git中工作区和暂存区的概念</p><ul><li>工作区可以简单理解为你在当前仓库的种种改动,git可以检测到但是并未将之准备为下次提交的内容。需要用户将之添加到暂存区。</li><li>暂存区可以简单理解为,下次执行提交中会被提交上去的文件。</li><li>整个工作流为: 你修改了某个文件 -> 该文件变为工作区文件 -> 你添加该文件进入暂存区 -> 提交暂存区文件,该文件被提交</li></ul><p>添加指定文件到暂存区<br><code>git add [file1] [file2] ...</code><br>添加指定目录到暂存区(包含该目录下所有文件)<br><code>git add [dir]</code><br>添加当前目录下所有文件到暂存区<br><code>git add .</code><br>删除工作区文件,并将“删除”这个操作放入暂存区<br><code>git rm [file1] [file2] ...</code><br>停止追踪文件,但是该文件会保留在工作区,类似gitignore的作用<br><code>git rm --cached [file]</code><br>改名文件,并将“改名”这个操作放入暂存区<br><code>git mv [origin-name] [target-name]</code></p><h3 id="代码提交"><a href="#代码提交" class="headerlink" title="代码提交"></a>代码提交</h3><p>把暂存区内容提交到仓库, 最常用的提交指令<br><code>git commit -m "message"</code><br>提交暂存区中的指定文件到仓库<br><code>git commit [file1] [file2] ... -m "message"</code><br>直接将工作区自从上次commit之后的变化,提交到仓库(跳过暂存区)<br><code>git commit -a</code><br>使用一次新的commit,替代上一次提交,常用于简单修复<br>如果代码没有任何新变化,则用来改写上一次commit的提交信息<br><code>git commit --amend -m [message]</code></p><h3 id="分支操作"><a href="#分支操作" class="headerlink" title="分支操作"></a>分支操作</h3><p>git中的分支是一个非常强大的功能,新建、删除、切换分支速度极快,可以多多使用</p><p>列出所有本地分支<br><code>git branch</code><br>列出所有远程分支<br><code>git branch -r</code><br>列出所有本地和远程分支<br><code>git branch -a</code><br>新建一个分支,并且留在当前分支<br><code>git branch [branch-name]</code><br>新建一个分支,并切换到新的分支上<br><code>git checkout -b [branch-name]</code><br>从某一个commit记录为起点,新建一个分支;其中commit中填入commit的hash或者tag(如果有标签)(下同)<br><code>git branch [branch-name] [commit]</code><br>新建一个分支,并于远程的一个分支建立追踪关系<br><code>git branch --track [branch-name] [remote-branch]</code><br>切换分支<br><code>git checkout [branch-name]</code><br>合并指定分支到当前分支<br><code>git merge [branch-name]</code><br>合并指定分支到当前分支,并生成线性的记录<br><code>git rebase [branch-name]</code><br>交互式的rebase<br><code>git rebase [branch] -i</code><br>选择某一次提交(任意分支上的),合并到当前分支<br><code>git cherry-pick [commit]</code><br>删除分支<br><code>git branch -d [branch-name]</code><br>删除远程分支<br><code>git branch -dr [origin/branch]</code><br><code>git push origin --delete [branch-name]</code></p><h3 id="标签"><a href="#标签" class="headerlink" title="标签"></a>标签</h3><p>标签可以用来给某一次提交添加一个可以追踪的标记,该标记不受分支影响,不会变化,可以在任何情况下被追踪。对于某一次重大提交,常常可以用标签予以标记(如某一次版本发布)</p><p>列出所有tag<br><code>git tag</code><br>在当前的commit上新建一个标签<br><code>git tag [tag-name]</code><br>给指定的commit上新建一个标签<br><code>git tag [tag-name] [commit]</code><br>删除本地的一个标签<br><code>git tag -d [tag-name]</code><br>删除远程的一个标签<br><code>git push origin :refs/tags/[tag-name]</code><br>查看某个标签对应的提交信息<br><code>git show [tag-name]</code><br>提交指定tag, remote指远程仓库的名字,一般为origin<br><code>git push [remote] [tag]</code><br>提交所有tag<br><code>git push [remote] --tags</code><br>以某个标签指定的commit为基点,新建一个分支<br><code>git branch [branch-name] [tag-name]</code></p><h3 id="查看信息"><a href="#查看信息" class="headerlink" title="查看信息"></a>查看信息</h3><p>显示有变更的文件<br><code>git status</code><br>显示当前分支的版本历史<br><code>git log</code><br>显示commit历史,以及每次commit发生变化的文件<br><code>git log --stat</code><br>显示指定文件的每一次改动<br><code>git log -p [file]</code><br>显示指定文件是什么时间被什么人修改的<br><code>git blame [file]</code><br>显示暂存区与工作区的差异<br><code>git diff</code><br>显示暂存区与上一次commit之间的差异 (可指定文件)<br><code>git diff --cached [file]</code><br>显示工作区与当前分支最新commit之间的差异<br><code>git diff HEAD</code><br>显示你今天写了多少行代码<br><code>git diff --shortstat "@{0 day ago}"</code><br>显示当前分支最近的几次提交记录(常用来进行恢复)<br><code>git reflog</code></p><h3 id="远程同步"><a href="#远程同步" class="headerlink" title="远程同步"></a>远程同步</h3><p>下载远程仓库的所有变动<br><code>git fetch [remote]</code><br>显示所有远程仓库<br><code>git remote -v</code><br>显示某个远程仓库的信息<br><code>git remote show [remote]</code><br>新增一个远程仓库,并命名<br><code>git remote add [name] [url]</code><br>拉取远程仓库的变化,并与本地分支合并<br><code>git pull [remote] [branch]</code><br>上传本地分支到远程仓库<br><code>git push [remote] [branch]</code><br>强行推送当前分支到远程仓库,即使有冲突<br><code>git push [remote] --force</code><br>推送所有分支到远程仓库<br><code>git push [remote] --all</code></p><h3 id="撤销"><a href="#撤销" class="headerlink" title="撤销"></a>撤销</h3><p>恢复暂存区的指定文件到工作区<br><code>git checkout [file]</code><br>恢复某个commit的指定文件到暂存区与工作区<br><code>git checkout [commit] [file]</code><br>恢复暂存区的所有文件到工作区<br><code>git checkout .</code><br>重置暂存区的指定文件,与上一次commit保持一致,但工作区不变<br><code>git reset [file]</code><br>重置工作区与暂存区,与上一次commit保持一致<br><code>git reset --hard</code><br>重置当前分支的指针为指定commit,同时重置暂存区,但工作区不变<br><code>git reset [commit]</code><br>重置当前分支的HEAD为指定commit,同时重置暂存区和工作区,与指定commit一致<br><code>git reset --hard [commit]</code><br>新建一个commit,用来撤销指定commit<br>后者的所有变化都将被前者抵消,并且应用到当前分支<br>常用来对远程仓库进行恢复<br><code>git revert [commit]</code><br>暂时将未提交的变化移除,稍后再移入<br><code>git stash</code><br><code>git stash pop</code></p><p>关于git reset指令,其实有 –soft –hard –mixed三种参数,默认为 –mixed参数。<br>具体详细用法可以参考这篇文章 <a href="https://segmentfault.com/a/1190000009658888" target="_blank" rel="noopener">git reset详解</a></p><h3 id="HEAD移动"><a href="#HEAD移动" class="headerlink" title="HEAD移动"></a>HEAD移动</h3><p>HEAD在git中是一个非常重要的概念,因此在这里把这部分单独列出来。<br>HEAD是git中用来标记当前位置的一个指针。<br>形象的说法就是:你现在在哪,HEAD就指向哪,因为HEAD,git才知道你在哪。</p><p>一般情况下,HEAD指向当前分支(上最近的提交),但是在有些时候,我们可以让HEAD指向某一次具体的提交,这也叫做分离HEAD。比如创建分支时,如果不指定commit,那么会在当前HEAD的位置创建分支。</p><p>移动HEAD的方法是使用checkout指令,指定一个commid的hash值进行绝对定位<br><code>git checkout [commit-id]</code><br>我们也可以使用相对定位,以当前HEAD或分支名等可以追踪位置的标记为基准。 ^代表当前位置的前一个提交<br><code>git checkout HEAD^</code><br><code>git checkout master^</code><br>我们也可以用 <del>[number] 来一次移动多次提交<br>`git checkout HEAD</del>3<code></code>git checkout master~5`</p><p>关于HEAD的更多用法可以进一步去搜集资料</p><h2 id="后记"><a href="#后记" class="headerlink" title="后记"></a>后记</h2><p>以上仅仅为git 入门常用的一些指令,熟练之后可以应对一般git的使用场景。<br>在这里依然十分推荐<a href="https://learngitbranching.js.org/" target="_blank" rel="noopener">https://learngitbranching.js.org/</a>进行实际操作一次,相信对git的使用有很大帮助。</p>]]></content>
<summary type="html">
<hr>
<h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>git作为一个先进的版本管理工具,已经被广泛应用在大量项目中。近来发现了一个非常不错的git学习网站,虽然比较基础,但是可视化的界
</summary>
<category term="工具学习" scheme="https://njuwuyuxin.github.io/categories/%E5%B7%A5%E5%85%B7%E5%AD%A6%E4%B9%A0/"/>
<category term="git" scheme="https://njuwuyuxin.github.io/tags/git/"/>
<category term="版本控制" scheme="https://njuwuyuxin.github.io/tags/%E7%89%88%E6%9C%AC%E6%8E%A7%E5%88%B6/"/>
<category term="常用指令" scheme="https://njuwuyuxin.github.io/tags/%E5%B8%B8%E7%94%A8%E6%8C%87%E4%BB%A4/"/>
</entry>
<entry>
<title>用Hexo和Github pages快速搭建个人博客</title>
<link href="https://njuwuyuxin.github.io/2019/04/09/%E7%94%A8Hexo%E5%92%8CGithub%20pages%E5%BF%AB%E9%80%9F%E6%90%AD%E5%BB%BA%E4%B8%AA%E4%BA%BA%E5%8D%9A%E5%AE%A2/"/>
<id>https://njuwuyuxin.github.io/2019/04/09/用Hexo和Github pages快速搭建个人博客/</id>
<published>2019-04-09T12:07:32.000Z</published>
<updated>2019-06-07T07:54:30.208Z</updated>
<content type="html"><![CDATA[<hr><h2 id="为什么要写博客"><a href="#为什么要写博客" class="headerlink" title="为什么要写博客"></a>为什么要写博客</h2><p>一直以来都有想写博客的想法,但一方面又觉得自己没有什么技术积累,言之无物,另一方面又担心没有毅力能够坚持下去。终于还是决定先行动起来,即便是记录下日常学习的心得,踩过的坑,也或许对自己对他人有些微帮助</p><p>于是今天动手用hexo简单搭建了这样一个静态博客,搭建的过程也并不复杂,感兴趣的朋友可以参照下面步骤搭建一个自己的静态博客</p><h2 id="开始搭建"><a href="#开始搭建" class="headerlink" title="开始搭建"></a>开始搭建</h2><h3 id="准备工作"><a href="#准备工作" class="headerlink" title="准备工作"></a>准备工作</h3><ul><li><p>首先hexo是基于Node.js实现的,因此我们想要用hexo搭建个人主页,首先要安装Node.js</p><ul><li>对于windows用户,建议去官网下载安装包,安装时选择 add to path, 添加环境变量</li><li>对于mac用户 可以选择使用nvm进行安装,优点在于可以方便的控制node版本(对于搭建个人博客意义不大)<br><code>$ curl https://raw.github.com/creationix/nvm/v0.33.11/install.sh | sh</code><br>安装好nvm后执行<br><code>nvm install stable</code><br>安装最新稳定版node</li></ul></li><li><p>安装好node后,为了将其发布在Github pages上,我们还需要安装git</p><ul><li>对于windows用户,去官网下载 <a href="https://git-scm.com/download/win" target="_blank" rel="noopener">git</a>,为了方便使用命令行,建议安装git bash</li><li>对于mac用户,可以用homebrew进行安装<br><code>brew install git</code></li></ul></li></ul><h3 id="安装hexo"><a href="#安装hexo" class="headerlink" title="安装hexo"></a>安装hexo</h3><p>准备工作完成后,我们就可以安装Hexo了<br><code>npm install -g hexo-cli</code><br>-g 参数指定以全局方式安装</p><p>安装好hexo后,便可以在命令行使用hexo指令</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">hexo init <folder></span><br><span class="line">cd <folder></span><br><span class="line">npm install</span><br></pre></td></tr></table></figure><p>其中folder为你想创建的文件夹路径,如果不指定folder,则默认会在当前文件夹创建(要求当前文件夹为空)</p><p>新建完成后,文件夹目录结构如下</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">.</span><br><span class="line">├── _config.yml</span><br><span class="line">├── package.json</span><br><span class="line">├── scaffolds</span><br><span class="line">├── source</span><br><span class="line">| ├── _drafts</span><br><span class="line">| └── _posts</span><br><span class="line">└── themes</span><br></pre></td></tr></table></figure><ul><li>_config.yml为全局配置文件,可以配置网站的基础信息</li><li>scaffolds文件夹存放页面的模版信息</li><li>source文件夹中的_posts文件夹用来存放我们的博文</li><li>themes文件夹存放页面所使用的主题</li></ul><h3 id="配置网站"><a href="#配置网站" class="headerlink" title="配置网站"></a>配置网站</h3><p>到了这里,我们的网站已经初步成型了,为了看到我们网站的具体样子,我们可以执行<br><code>hexo server</code><br>在本地运行一个服务,默认4000端口,信息如下</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">INFO Start processing</span><br><span class="line">INFO Hexo is running at http://localhost:4000 . Press Ctrl+C to stop.</span><br></pre></td></tr></table></figure><p>看到这样的提示,代表已经成功运行了,打开浏览器输入 localhost:4000 即可看到我们的页面</p><p>但是此时的网站没有名称,作者等一系列信息,需要我们手动配置</p><p>打开根目录下的_config.yml 如下</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"># Hexo Configuration</span><br><span class="line">## Docs: https://hexo.io/docs/configuration.html</span><br><span class="line">## Source: https://github.com/hexojs/hexo/</span><br><span class="line"></span><br><span class="line"># Site</span><br><span class="line">title: </span><br><span class="line">subtitle:</span><br><span class="line">description:</span><br><span class="line">keywords:</span><br><span class="line">author: </span><br><span class="line">language: zh-Hans</span><br><span class="line">timezone:</span><br></pre></td></tr></table></figure><p> 可以修改各个字段的值,如标题、作者、语言等等。可以给博客起一个喜欢的名字,并落上自己的署名</p><h3 id="发布文章"><a href="#发布文章" class="headerlink" title="发布文章"></a>发布文章</h3><p> 博客配置好后,我们便可以开始书写文章了,用hexo创建一篇新文章也很简单<br> <code>hexo new [layout] <title></code><br>layout不指定的话默认试用post的布局,默认布局可以在_config.yml中修改<br>创建好文章后,我们就可以在source/_posts文件夹下找到并编写了,书写博文使用markdown</p><pre><code>文章写好后,我们需要把markdown文件转换成静态的html文件以便显示在网页上,hexo为我们提供了一个简单的指令</code></pre><p><code>hexo generate</code><br>可以简写为<code>hexo g</code> </p><p>在生成好文章后,刷新我们本地打开的博客网站(localhost:4000),可以看到我们的文章已经可以显示出来啦</p><h3 id="部署网站"><a href="#部署网站" class="headerlink" title="部署网站"></a>部署网站</h3><p>至此我们的博客基本功能已经实现了,但是所有的操作都只能通过本地运行的服务进行查看。为了把博客放到互联网上供所有人浏览,我们还需要将我们的博客部署到服务器上。</p><p>一个令人兴奋的消息是,github为我们提供了这样一个静态网站托管的服务,并且完全免费!<br>我们所需要做的,仅仅是拥有一个github账号,并且创建一个用于维护github page的仓库</p><ul><li>首先在github上创建一个仓库,仓库名称为 yourName.github.io ,yourName需要替换成你的github昵称</li><li>如果想要通过ssh验证,需要先在本机生成ssh密钥,将公钥添加到github账户上</li><li>之后需要配置本地博客网站的部署配置,依然是在_config.yml中,在文件最下方找到deploy字段如下<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">deploy:</span><br><span class="line"> type:</span><br></pre></td></tr></table></figure>在type字段中填写 git<br>之后在下一行新增一个字段 repo,填入你刚刚创建的git仓库地址,应该是如下形式<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">deploy:</span><br><span class="line"> type: git</span><br><span class="line"> repo: https://github.com/xxxx/xxx.github.io.git</span><br></pre></td></tr></table></figure>repo字段根据选择的不同协议,可以选择https或者ssh认证</li></ul><p>一切都配置完毕后,我们就可以将网站部署到github page上去了!<br><code>hexo deploy</code><br>可以简写为 <code>hexo d</code><br>首次部署需要进行身份验证,如果采用https协议,需要输入github账号密码。如果采用ssh协议则不需要。</p><p>如果没有提示什么错误,稍等片刻,我们在浏览器输入与刚刚创建好的仓库的同名域名 xxx.github.io 即可以看到我们创建好的个人网站了!</p><h3 id="个性化域名"><a href="#个性化域名" class="headerlink" title="个性化域名"></a>个性化域名</h3><p>如果想要为自己的网站设置一个个性化的域名,那么我们需要向域名供应商购买一个域名并且配置相应的dns服务,更多内容可以自行查阅,本文不再过多阐述。</p><h3 id="相关阅读"><a href="#相关阅读" class="headerlink" title="相关阅读"></a>相关阅读</h3><p><a href="https://hexo.io/zh-cn/docs/front-matter" target="_blank" rel="noopener">hexo官方文档</a><br><a href="https://pages.github.com/" target="_blank" rel="noopener">github pages官方指南</a><br><a href="https://www.jianshu.com/p/191d1e21f7ed" target="_blank" rel="noopener">markdown语法简介</a></p>]]></content>
<summary type="html">
<hr>
<h2 id="为什么要写博客"><a href="#为什么要写博客" class="headerlink" title="为什么要写博客"></a>为什么要写博客</h2><p>一直以来都有想写博客的想法,但一方面又觉得自己没有什么技术积累,言之无物,另一方面又担心没
</summary>
<category term="环境搭建" scheme="https://njuwuyuxin.github.io/categories/%E7%8E%AF%E5%A2%83%E6%90%AD%E5%BB%BA/"/>
<category term="hexo" scheme="https://njuwuyuxin.github.io/tags/hexo/"/>
<category term="node" scheme="https://njuwuyuxin.github.io/tags/node/"/>
<category term="指南" scheme="https://njuwuyuxin.github.io/tags/%E6%8C%87%E5%8D%97/"/>
</entry>
</feed>