HTTP包体的长度有可能非常大,不同业务可能对包体读取 处理不相同, 比如waf, 也许会读取body内容或者只是读取很少的前几十字节。所以根据不同业务特性,对http body 数据包处理方式不同,nginx核心本身不会主动读取请求体,这个工作是交给请求处理阶段的模块处理,ngx 里面目前提供了接口读取body接口ngx_http_read_client_request_body(ngx_http_request_t *r, )ngx_http_client_body_handler_pt post_handler);请求体的读取一般发生在nginx的content handler中,一些nginx内置的模块,比如proxy模块,fastcgi模块,uwsgi模块等,这些模块的行为必须将客户端过来的请求体(如果有的话)以相应协议完整的转发到后端服务进程,所有的这些模块都是调用了ngx_http_read_client_request_body()接口来完成请求体读取。值得注意的是这些模块会把客户端的请求体完整的读取后才开始往后端转发数据。

  ngx_http_read_client_request_body是一个异步方法,调用它只是说明要求Nginx开始接收请求的包体,并不表示是否已经接收完,当接收完所有的包体内容后,post_handler指向的回调方法会被调用。因此,即使在调用了ngx_http_read_client_request_body方法后它已经返回,也无法确定这时是否已经调用过post_handler指向的方法。换句话说,ngx_http_read_client_request_body返回时既有可能已经接收完请求中所有的包体(假如包体的长度很小),也有可能还没开始接收包体;

  由于内存的限制,ngx_http_read_client_request_body()接口读取的请求体会部分或者全部写入一个临时文件中。

  如果在读取包体的时候, 发现后续内容不需要,此时为了防止TCP报文阻塞,所以此时只能只能将报文读取并且“丢弃”,ngx 目前提供了ngx_http_discard_request_body 接口来处理此事件

  1. /* HTTP框架提供了两种方式处理HTTP包体,当然,这两种方式保持了完全无阻塞的事件驱动机制,非常高效。第一种方式就是把请求中的包体
  2. 接收到内存或者文件中,当然,由于包体的长度是可变的,同时内存又是有限的,因此,一般都是将包体存放到文件中。第二种方式是选择丢弃包体,
  3. 注意,丢弃不等于可以不接收包体,这样做可能会导致客户端出现发送请求超时的错误,所以,这个丢弃只是对于HTTP模块而言的,HTTP框架还是需
  4. 要“尽职尽责”地接收包体,在接收后直接丢弃。 一般都是需要访问上游服务器的时候才会读取包体,例如ngx_http_proxy_handler ,
  5.  
  6. 一般都是如果解析头部行后,后面有携带包体,则会走到这里,如果包体还没读完,下次也不会走到该函数,而是走ngx_http_do_read_client_request_body
  7. 实际上走到这里面的包体内容是在读取头部的时候,一起读出来的,读取地方见ngx_http_wait_request_handler
  8.  
  9. 在NGX_HTTP_CONTENT_PHASE阶段通过ngx_http_core_content_phase调用content阶段的handler从而执行ngx_http_proxy_handler ngx_http_redis2_handler ngx_http_fastcgi_handler等,在这些函数中开始读取包体
  10. */////*post_handler在ngx_http_do_read_client_request_body接收完所有包体后执行,或者在本函数能读取完包体后也会执行post_handler方法被回调时,
  11. 务必调用类似ngx_http_finalize_request的方法去结束请求,否则引用计数会始终无法清零,从而导致请求无法释放。*/
  12.  
  13. ngx_http_read_client_request_body(ngx_http_request_t *r, //只有在连接后端服务器的时候才会读取客户端请求包体,见ngx_http_xxx_handler(proxy fastcgi等)
  14. ngx_http_client_body_handler_pt post_handler)
  15.  
  16. {
  17. size_t preread;
  18. ssize_t size;
  19. ngx_int_t rc;
  20. ngx_buf_t *b;
  21. ngx_chain_t out, *cl;
  22. ngx_http_request_body_t *rb;
  23. ngx_http_core_loc_conf_t *clcf;
  24.  
  25. /*
  26. 首先把该请求对应的原始请求的引用计数加l。这同时是在要求每一个HTTP模块在传入的post_handler方法被回调时,务必调用类似
  27. ngx_http_finalize_request的方法去结束请求,否则引用计数会始终无法清零,从而导致请求无法释放。
  28. */
  29. r->main->count++;
  30. //因为执行该函数一般都是向后端转发,例如可以参考ngx_http_read_client_request_body(r, ngx_http_upstream_init);,在ngx_http_upstream_init没有执行count++操作,实际上在这里
  31.  
  32. #if (NGX_HTTP_V2)
  33. /* HTTP2 data帧以外的所有帧的数据读取在ngx_http_v2_read_handler,
  34. data帧读取在ngx_http_read_client_request_body->ngx_http_v2_read_request_body */
  35. if (r->stream && r == r->main) {
  36. r->request_body_no_buffering = 0;
  37. rc = ngx_http_v2_read_request_body(r, post_handler);
  38. goto done;
  39. }
  40. #endif
  41.  
  42. /*
  43. 检查请求ngx_http_request_t结构体中的request_body成员,如果它已经被分配过了,证明已经读取过HTTP包体了,不需要再次读取一遍;
  44. 再检查请求ngx_http_request_t结构体中的discard_body标志位,如果discard_body为1,则证明曾经执行过丢弃包体的方法,现在包体正在被丢弃中。
  45. 只有这两个条件都不满足,才说明真正需要接收HTTP包体。
  46. */
  47. if (r != r->main || r->request_body || r->discard_body) {
  48. r->request_body_no_buffering = 0;
  49. post_handler(r); //直接执行各HTTP模块提供的post_handler回调方法
  50. return NGX_OK;
  51. }
  52.  
  53. if (ngx_http_test_expect(r) != NGX_OK) {
  54. rc = NGX_HTTP_INTERNAL_SERVER_ERROR;
  55. goto done;
  56. }
  57.  
  58. if (r->request_body_no_buffering) { //如果不缓存包体,request_body_no_buffering和request_body_in_file_only是互斥的
  59. r->request_body_in_file_only = 0; //设置为不缓存包体,则就不能把包体写道文件中
  60. }
  61.  
  62. /* 分配请求的ngx_http_request_t结构体中的request_body成员(之前request_body是NULL空指针),准备接收包体。 */
  63. rb = ngx_pcalloc(r->pool, sizeof(ngx_http_request_body_t));
  64. if (rb == NULL) {
  65. rc = NGX_HTTP_INTERNAL_SERVER_ERROR;
  66. goto done;
  67. }
  68.  
  69. /*
  70. * set by ngx_pcalloc():
  71. *
  72. * rb->bufs = NULL;
  73. * rb->buf = NULL;
  74. * rb->free = NULL;
  75. * rb->busy = NULL;
  76. * rb->chunked = NULL;
  77. */
  78.  
  79. rb->rest = -1;
  80. rb->post_handler = post_handler;
  81.  
  82. r->request_body = rb; //把创建的ngx_http_request_body_t空间赋值给request_body
  83.  
  84. /* 检查请求的content-length头部,如果指定了包体长度的content-length字段小于或等于0,当然不用继续接收包体:
  85. 如果content-length大于0,则意味着继续执行,但HTTP模块定义的post_handler方法不会知道在哪一次事件的触发中会被回调,
  86. 所以先把它设置到request_body结构体的post_handler成员中。 */
  87. if (r->headers_in.content_length_n < 0 && !r->headers_in.chunked) {
  88. r->request_body_no_buffering = 0;
  89. post_handler(r);
  90. return NGX_OK;
  91. }
  92.  
  93. /* 接收HTTP头部的流程中,是有可能接收到HTTP包体的。首先我们需要检查在header_in缓冲区中已经接收到的包体长度,确定其是否大于或者等于
  94. content-length头部指定的长度,如果大干或等于则说明已经接收到完整的包体 */
  95. preread = r->header_in->last - r->header_in->pos;
  96.  
  97. if (preread) { //注意在ngx_http_wait_request_handler中第一次读的时候默认是读1024字节,有可能ngx_http_wait_request_handler已经把包体读了
  98.  
  99. /* there is the pre-read part of the request body */
  100.  
  101. ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
  102. "http client request body preread %uz", preread);
  103.  
  104. out.buf = r->header_in;//
  105. out.next = NULL;
  106.  
  107. //把最新读取到的buf数据添加到r->request_body->bufs中,并且让free指向该bufs中所有数据中已经解析了的数据节点信息(重复利用ngx_buf_t)
  108. //busy链表中的ngx_buf_t节点指向bufs中所有数据中还没有解析完毕的数据
  109. rc = ngx_http_request_body_filter(r, &out);
  110.  
  111. if (rc != NGX_OK) {
  112. goto done;
  113. }
  114.  
  115. r->request_length += preread - (r->header_in->last - r->header_in->pos);
  116.  
  117. /* 当上述条件不满足时,再检查header—in缓冲区里的剩余空闲空间是否可以存放下全部的包体(content-length头部指定),如果可以,就不用分配新的包体缓冲区浪费内存了 */
  118. if (!r->headers_in.chunked
  119. && rb->rest > 0 //还需要读取rb->rest才能保证包体读完
  120. && rb->rest <= (off_t) (r->header_in->end - r->header_in->last)) //判断header_in指向的剩余未用空间是否足够存取剩余的rest字节数据
  121. {
  122. /* the whole request body may be placed in r->header_in */
  123. //header_in中剩余的未用空间足够,例如还差rest = 1000字节才能读取完包体,但是header_in中剩余空间end - last超过1000,则不需要从新开辟空间
  124. //直接使用header_in剩余空间,开辟新的ngx_buf_t空间,使用新的ngx_buf_t中的各个指针指向header_in中剩余未用空间,用来继续读取
  125. b = ngx_calloc_buf(r->pool);
  126. if (b == NULL) {
  127. rc = NGX_HTTP_INTERNAL_SERVER_ERROR;
  128. goto done;
  129. }
  130.  
  131. b->temporary = 1;
  132. b->start = r->header_in->pos;
  133. b->pos = r->header_in->pos;
  134. b->last = r->header_in->last;
  135. b->end = r->header_in->end;
  136.  
  137. rb->buf = b;
  138.  
  139. r->read_event_handler = ngx_http_read_client_request_body_handler;
  140. r->write_event_handler = ngx_http_request_empty_handler;
  141.  
  142. rc = ngx_http_do_read_client_request_body(r);
  143. goto done;
  144. }
  145.  
  146. } else {
  147. /* set rb->rest */
  148.  
  149. if (ngx_http_request_body_filter(r, NULL) != NGX_OK) {
  150. rc = NGX_HTTP_INTERNAL_SERVER_ERROR;
  151. goto done;
  152. }
  153. }
  154.  
  155. if (rb->rest == 0) { //包体读取完毕
  156. /* the whole request body was pre-read */
  157.  
  158. if (r->request_body_in_file_only) { //如果配置"client_body_in_file_only" on | clean 表示包体存储在磁盘文件中
  159. if (ngx_http_write_request_body(r) != NGX_OK) {
  160. rc = NGX_HTTP_INTERNAL_SERVER_ERROR;
  161. goto done;
  162. }
  163.  
  164. if (rb->temp_file->file.offset != 0) {
  165.  
  166. cl = ngx_chain_get_free_buf(r->pool, &rb->free);
  167. if (cl == NULL) {
  168. rc = NGX_HTTP_INTERNAL_SERVER_ERROR;
  169. goto done;
  170. }
  171.  
  172. b = cl->buf;
  173.  
  174. ngx_memzero(b, sizeof(ngx_buf_t));
  175.  
  176. b->in_file = 1;
  177. b->file_last = rb->temp_file->file.offset;
  178. b->file = &rb->temp_file->file;
  179.  
  180. rb->bufs = cl; //如果包体存入临时文件中,则读取包体完成后,bufs指向的ngx_chain_t中的各个指针指向文件中的相关偏移
  181.  
  182. } else {
  183. rb->bufs = NULL;
  184. }
  185. }
  186.  
  187. r->request_body_no_buffering = 0;
  188.  
  189. post_handler(r);
  190.  
  191. return NGX_OK;
  192. }
  193.  
  194. //只有读取包体执行一次到该下面流程,则表示读取一次的时候没有读取完
  195.  
  196. //包体长度出错
  197. if (rb->rest < 0) {
  198. ngx_log_error(NGX_LOG_ALERT, r->connection->log, 0,
  199. "negative request body rest");
  200. rc = NGX_HTTP_INTERNAL_SERVER_ERROR;
  201. goto done;
  202. }
  203.  
  204. clcf = ngx_http_get_module_loc_conf(r, ngx_http_core_module);
  205.  
  206. size = clcf->client_body_buffer_size;
  207. size += size >> 2; //实际上就是四分之五5/4个client_body_buffer_size
  208.  
  209. /* TODO: honor r->request_body_in_single_buf */
  210. //走到这里之前至少在ngx_http_wait_request_handler函数中读取过一次,也就是读取头部的时候,可能会读取一部分包体,在读取头部的时候
  211. //读取的最大报文长度为client_header_buffer_size,所以包体有可能在那里读取后处理了头部行后,会走到本函数处理包体,这时候可能包体没有读完
  212.  
  213. if (!r->headers_in.chunked && rb->rest < size) {
  214. size = (ssize_t) rb->rest;
  215.  
  216. if (r->request_body_in_single_buf) { //需要缓存到同一个buf中,那么开辟的空间就必须一次分配完,这样可以存储后面所有的。
  217. size += preread; //如果是把读取的网络数据存到同一个single buffer中,则本次读到preread字节,但是还有size字节没读,所以需要相加,表示一共需要这么多空间,
  218. }
  219.  
  220. } else {
  221. size = clcf->client_body_buffer_size; //如果不是缓存到同一个buf,则一次最多开辟这么多空间,这样可能需要多个buf才能读取完
  222. }
  223.  
  224. /*
  225. 说明确实需要分配用于接收包体的缓冲区了。缓冲区长度由nginx.conf丈件中的client_body_buffer_size配置项指定,缓冲区就在ngx_http_request_body_t
  226. 结构体的buf成员中存放着,同时,bufs和to_ write这两个缓冲区链表首部也指向该buf。
  227. */
  228.  
  229. /*
  230. 这里开辟真正的读取数据的空间后,buf的指针指向终端空间的头尾以及解析完的数据的位置,
  231. buf1 buf---- bufN
  232. _________________________________________________________________________________
  233. | | | |
  234. |__________________________|_________________________|___________________________|
  235.  
  236. 1.第一次开辟好存储数据的空间ngx_create_temp_buf后,r->request_body->buf pos last start指向buf1的头部,end指向bufN尾部
  237. 2.假设第一次读取完内核协议栈的数据后填充好了buf1,r->request_body->buf中的pos start指向buf1的头部,last指向buf1尾部(buf2头部),end指向bufn尾部
  238. 3.开始调用ngx_http_request_body_filter,在该函数里面会重新分配一个ngx_buf_t,把r->request_body->buf成员赋值给她。然后把这个新的ngx_buf_t
  239. 添加到r->request_body->bufs链表中。赋值完后r->request_body->buf中的start指向buf1的头部,pos last指向buf1尾部(buf2头部),end指向bufn尾部
  240. 4.从复上面的2 3步骤
  241. 5.当解析完buf-n的内容后,发现r->request_body->buf从内核读取到buf空间中的网络数据包已经被三个新的ngx_buf_t指向,并且这三个ngx_buf_t
  242. 通过r->request_body->bufs链表连接在了一起,这时候r->request_body->buf中的end = last,也就是所有ngx_create_temp_buf开辟的内存空间
  243. 已经存满了(recv的数据存在该空间里面),并且数据分成三个ngx_buf_t指向这些空间,然后连接到了转存到了r->request_body->bufs链表上。在
  244. 6.ngx_http_request_body_save_filter中检测到rb->buf->last == rb->buf->end,上面的buf(buf1+buf2 + buf n)已经填满,然后通过r->request_body->bufs
  245. 把三个ngx_buf_t指向的内存空间一次性写入临时文件,写入临时文件后,r->request_body->buf中的pos last指针重新指向头部,又可以从新从
  246. 内核协议栈读取数据存储在里面了,然后从复1-5的过程
  247.  
  248. //读取客户包体即使是存入临时文件中,当所有包体读取完毕后(ngx_http_do_read_client_request_body),还是会让r->request_body->bufs指向文件中的相关偏移内存地址
  249. */
  250. rb->buf = ngx_create_temp_buf(r->pool, size); //这个是为下次读取准备的
  251. if (rb->buf == NULL) {
  252. rc = NGX_HTTP_INTERNAL_SERVER_ERROR;
  253. goto done;
  254. }
  255.  
  256. /*
  257. 设置请求ngx_http_request_t结构体的read_ event_ handler成员为上面介绍过的ngx_http_read_client_request_body_handler方法,
  258. 它意味着如果epoll再次检测到可读事件或者读事件的定时器超时,HTTP框架将调用ngx_http_read_client_request_body_handler方法处理
  259. */
  260. r->read_event_handler = ngx_http_read_client_request_body_handler;
  261. r->write_event_handler = ngx_http_request_empty_handler;
  262.  
  263. /*
  264. 调用ngx_http_do_read_client_request_body方法接收包体。该方法的意义在于把客户端与Nginx之间TCP连接上套接字缓冲区中的当前字符流全
  265. 部读出来,并判断是否需要写入文件,以及是否接收到全部的包体,同时在接收到完整的包体后激活post_handler回调方法
  266. */
  267. rc = ngx_http_do_read_client_request_body(r);//这里面添加ngx_handle_read_event的时候,对应的handler为ngx_http_read_client_request_body_handler
  268.  
  269. done:
  270.  
  271. if (r->request_body_no_buffering
  272. && (rc == NGX_OK || rc == NGX_AGAIN))
  273. {
  274. if (rc == NGX_OK) {
  275. r->request_body_no_buffering = 0;
  276.  
  277. } else {
  278. /* rc == NGX_AGAIN */
  279. r->reading_body = 1;
  280. }
  281.  
  282. r->read_event_handler = ngx_http_block_reading;
  283. post_handler(r);
  284. }
  285.  
  286. if (rc >= NGX_HTTP_SPECIAL_RESPONSE) {//如果返回出错
  287. r->main->count--; //该函数处理结束后-1,因为该函数开始处理的时候有+1
  288. }
  289.  
  290. return rc;
  291. }
  1. ngx_http_request_body_filter数据尝试写入到临时文件中
  1. //读取客户包体即使是存入临时文件中,当所有包体读取完毕后(ngx_http_do_read_client_request_body),还是会让r->request_body->bufs指向文件中的相关偏移内存地址
  2. */
  3. static ngx_int_t
  4. ngx_http_request_body_filter(ngx_http_request_t *r, ngx_chain_t *in)
  5. {//in其实也是从r->request_body->buf中来的
  6. if (r->headers_in.chunked) {
  7. return ngx_http_request_body_chunked_filter(r, in);
  8.  
  9. } else {
  10. return ngx_http_request_body_length_filter(r, in);
  11. }
  12. }
  13. /*
  14. 这里开辟真正的读取数据的空间后,buf的指针指向终端空间的头尾以及解析完的数据的位置,
  15. buf1 buf2 buf3
  16. _________________________________________________________________________________
  17. | | | |
  18. |__________________________|_________________________|___________________________|
  19.  
  20. 1.第一次开辟好存储数据的空间ngx_create_temp_buf后,r->request_body->buf pos last start指向buf1的头部,end指向buf3尾部
  21. 2.假设第一次读取完内核协议栈的数据后填充好了buf1,r->request_body->buf中的pos start指向buf1的头部,last指向buf1尾部(buf2头部),end指向buf3尾部
  22. 3.开始调用ngx_http_request_body_filter,在该函数里面会重新分配一个ngx_buf_t,把r->request_body->buf成员赋值给她。然后把这个新的ngx_buf_t
  23. 添加到r->request_body->bufs链表中。赋值完后r->request_body->buf中的start指向buf1的头部,pos last指向buf1尾部(buf2头部),end指向buf3尾部
  24. 4.从复上面的2 3步骤
  25. 5.当解析完buf3的内容后,发现r->request_body->buf从内核读取到buf空间中的网络数据包已经被三个新的ngx_buf_t指向,并且这三个ngx_buf_t
  26. 通过r->request_body->bufs链表连接在了一起,这时候r->request_body->buf中的end = last,也就是所有ngx_create_temp_buf开辟的内存空间
  27. 已经存满了(recv的数据存在该空间里面),并且数据分成三个ngx_buf_t指向这些空间,然后连接到了转存到了r->request_body->bufs链表上。在
  28. 6.ngx_http_request_body_save_filter中检测到rb->buf->last == rb->buf->end,上面的buf(buf1+buf2+buf3)已经填满,然后通过r->request_body->bufs
  29. 把三个ngx_buf_t指向的内存空间一次性写入临时文件,写入临时文件后,r->request_body->buf中的pos last指针重新指向头部,又可以从新从
  30. 内核协议栈读取数据存储在里面了,然后从复1-5的过程
  31.  
  32. //读取客户包体即使是存入临时文件中,当所有包体读取完毕后(见ngx_http_do_read_client_request_body),还是会让r->request_body->bufs指向文件中的相关偏移内存地址
  33.  
  34. */
  35.  
  36. /*
  37. ngx_http_request_body_filter 函数的目的就是要解析读取到的数据 in,追加到 request body 里的 bufs 列表中,busy 也指向要解析到的 chain 和 buf,
  38. 同时 函数会更新 request body 中 rest 的值,此值表示当前请求还有多少字节没有读取。
  39. */ //指向该函数后一般in->buf->last = in->buf->pos
  40. static ngx_int_t
  41. ngx_http_request_body_length_filter(ngx_http_request_t *r, ngx_chain_t *in)
  42. { //in其实也是从r->request_body->buf中来的
  43. size_t size;
  44. ngx_int_t rc;
  45. ngx_buf_t *b;
  46. ngx_chain_t *cl, *tl, *out, **ll;
  47. ngx_http_request_body_t *rb;
  48.  
  49. rb = r->request_body;
  50.  
  51. if (rb->rest == -1) {//第一次执行该函数 rest 设置为请求头的 content-length
  52.  
  53. ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
  54. "http request body content length filter");
  55.  
  56. rb->rest = r->headers_in.content_length_n;
  57. }
  58.  
  59. out = NULL;
  60. ll = &out;
  61.  
  62. //把in中的所有数据节点数据连接在一起添加到out头中
  63. for (cl = in; cl; cl = cl->next) {//遍历r->request_body中的所有buf
  64.  
  65. if (rb->rest == 0) {//表示包体数据已经处理完毕
  66. break;
  67. }
  68.  
  69. tl = ngx_chain_get_free_buf(r->pool, &rb->free); //从free链表中poll中获取ngx_chain_t空间,如果free中为空,则直接创建
  70. if (tl == NULL) {
  71. return NGX_HTTP_INTERNAL_SERVER_ERROR;
  72. }
  73.  
  74. b = tl->buf; //获取tl中的buf
  75.  
  76. ngx_memzero(b, sizeof(ngx_buf_t));
  77.  
  78. //b的相关成员指针指向in中的各个节点里面的对于成员,即新的b指向获取到的包体数据中的各个ngx_buf_t
  79. //b指向的数据和cl->buf指向的数据时一致的,最后实际读取到的数据的头尾用b指向
  80. b->temporary = 1;
  81. b->tag = (ngx_buf_tag_t) &ngx_http_read_client_request_body;
  82. b->start = cl->buf->pos;
  83. b->pos = cl->buf->pos;
  84. b->last = cl->buf->last;
  85. b->end = cl->buf->end;
  86. b->flush = r->request_body_no_buffering;
  87.  
  88. size = cl->buf->last - cl->buf->pos;
  89.  
  90. if ((off_t) size < rb->rest) { //说明数据还不够r->headers_in.content_length_n;
  91. cl->buf->pos = cl->buf->last;
  92. rb->rest -= size; //已经获取到size了,还差rb->rest才到content_length_n
  93.  
  94. } else { //说明已经获取到了content_length_n这么多包体,也就是包体已经够了
  95. cl->buf->pos += (size_t) rb->rest; //注意这时候的last没有移动,如果头部行content-length:len中的len小于实际携带的包体数据,就会造成pos小于last
  96. rb->rest = 0;//表示包体有这么多了
  97. b->last = cl->buf->pos; //实际读到的数据比我们期望的rest数据多,因此我们截取实际需要的数据即可
  98. b->last_buf = 1; //标记该buf是组成包体数据的buf数组中的最后一个ngx_buf_t
  99. }
  100.  
  101. *ll = tl;//二级指针 tl->next 赋值
  102. ll = &tl->next; //前面创建的所有tl(ngx_chain_t)通过next连接在一起,所有这些节点的头部是前面的out
  103. }
  104.  
  105. //把in表中的数据(通过从新创建ngx_buf_t指向in表中各个数据的成员)连接到r->request_body->bufs中,这样所有的out数据都会添加到r->request_body->bufs数组中
  106. //通过新创建的ngx_chain_t(之前out中的ngx_chain_t就通过下面的ngx_chain_update_chains进行回收)中的各个指针来执行新读取到的out数据,ngx_http_request_body_save_filter,通过该函数后所有的out数据都连接到rb->bufs中缓存了
  107. rc = ngx_http_top_request_body_filter(r, out); //ngx_http_request_body_save_filter
  108.  
  109. //rb中的bufs链表中的成员中的各个指针指向读取到的原始数据位置(如pos指向读取到数据的头,last指向读取到数据的尾部)
  110. //rb->busy最初是直接从out中拷贝的,指向的数据空间最初与bufs是一样的,但是一旦http模块从网络读取到的数据中解析出一部分数据,那么free中成员的各个指针就会移动,例如pos会
  111. //向last方向移动,直到pos=list,这时候busy中的这个ngx_buf_t节点成员就可以从busy链表中取出,然后添加到free链表中
  112. ngx_chain_update_chains(r->pool, &rb->free, &rb->busy, &out,
  113. (ngx_buf_tag_t) &ngx_http_read_client_request_body);
  114.  
  115. return rc;
  116. }
  117.  
  118. //把in表中的成员buff拼接到r->request_body后面,如果rb->buf->last == rb->buf->end则会把
  119. //当一个rb->buf填满后就会通过ngx_http_write_request_body把bufs链表中的所有ngx_chain_t->ngx_buf_t中指向的数据
  120. //写入到临时文件,并把ngx_buf_t结构加入poll->chain,通过poll统一释放他们
  121. ngx_int_t //通过ngx_http_top_request_body_filter调用
  122. ngx_http_request_body_save_filter(ngx_http_request_t *r, ngx_chain_t *in)
  123. {
  124. #if (NGX_DEBUG)
  125. ngx_chain_t *cl;
  126. #endif
  127. ngx_http_request_body_t *rb;
  128.  
  129. rb = r->request_body;
  130.  
  131. #if (NGX_DEBUG)
  132.  
  133. for (cl = rb->bufs; cl; cl = cl->next) {
  134. ngx_log_debug7(NGX_LOG_DEBUG_EVENT, r->connection->log, 0,
  135. "http body old buf t:%d f:%d %p, pos %p, size: %z "
  136. "file: %O, size: %O",
  137. cl->buf->temporary, cl->buf->in_file,
  138. cl->buf->start, cl->buf->pos,
  139. cl->buf->last - cl->buf->pos,
  140. cl->buf->file_pos,
  141. cl->buf->file_last - cl->buf->file_pos);
  142. }
  143.  
  144. for (cl = in; cl; cl = cl->next) {
  145. ngx_log_debug7(NGX_LOG_DEBUG_EVENT, r->connection->log, 0,
  146. "http body new buf t:%d f:%d %p, pos %p, size: %z "
  147. "file: %O, size: %O",
  148. cl->buf->temporary, cl->buf->in_file,
  149. cl->buf->start, cl->buf->pos,
  150. cl->buf->last - cl->buf->pos,
  151. cl->buf->file_pos,
  152. cl->buf->file_last - cl->buf->file_pos);
  153. }
  154.  
  155. #endif
  156.  
  157. /* TODO: coalesce neighbouring buffers */
  158.  
  159. if (ngx_chain_add_copy(r->pool, &rb->bufs, in) != NGX_OK) {
  160. return NGX_HTTP_INTERNAL_SERVER_ERROR;
  161. }
  162.  
  163. //当一个rb->buf填满后就会通过ngx_http_write_request_body把bufs链表中的所有ngx_chain_t->ngx_buf_t中指向的数据
  164. //写入到临时文件,并把ngx_buf_t结构加入poll->chain,通过poll统一释放他们
  165. if (rb->rest > 0
  166. && rb->buf && rb->buf->last == rb->buf->end
  167. && !r->request_body_no_buffering)
  168. //需要缓存数据,并且rb->buf数据已经解析完毕,并且buf已经满了,但是包体还没有读完,那么就可以把buf中的数据写入临时文件,
  169. //这样改buf指向的内存空间在该函数退出后可以继续用来读取数据
  170. {
  171. if (ngx_http_write_request_body(r) != NGX_OK) {
  172. return NGX_HTTP_INTERNAL_SERVER_ERROR;
  173. }
  174. }
  175.  
  176. return NGX_OK;
  177. }
  178.  
  179. /*
  180. 因为nginx可以提前flush输出,所以这些buf被输出后就可以重复使用,可以避免重分配,提高系统性能,被称为free_buf,而没有被输出的
  181. buf就是busy_buf。nginx没有特别的集成这个特性到自身,但是提供了一个函数ngx_chain_update_chains来帮助开发者维护这两个缓冲区队列
  182. */
  183. //该函数功能就是把新读到的out数据添加到busy表尾部,然后把busy表中已经处理完毕的buf节点从busy表中摘除,然后放到free表头部
  184. //未发送出去的buf节点既会在out链表中,也会在busy链表中
  185. void
  186. ngx_chain_update_chains(ngx_pool_t *p, ngx_chain_t **free, ngx_chain_t **busy,
  187. ngx_chain_t **out, ngx_buf_tag_t tag)
  188. {
  189. ngx_chain_t *cl;
  190.  
  191. if (*busy == NULL) {
  192. // busy 指向 out 指向的地方
  193. *busy = *out;
  194.  
  195. } else {
  196. for (cl = *busy; cl->next; cl = cl->next) { /* void */ } // cl 指向 busy chain 链条的最后一个
  197.  
  198. cl->next = *out; //out节点添加到busy表中的最后一个节点
  199. }
  200.  
  201. *out = NULL;
  202.  
  203. while (*busy) {
  204. cl = *busy;
  205.  
  206. // buf 大小不是 0,说明还没有输出;request body 中的 bufs 是输出用的,如上所述,bufs 中指向的 buf 和 busy 指向的 buf 对象是一模一样的
  207. if (ngx_buf_size(cl->buf) != 0) { //pos和last不相等,说明该buf中的内容没有处理完
  208. break;
  209. }
  210.  
  211. if (cl->buf->tag != tag) {// tag 中存储的是 函数指针
  212. *busy = cl->next;
  213. ngx_free_chain(p, cl);
  214. continue;
  215. }
  216.  
  217. //把该空间的pos last都指向start开始处,表示该ngx_buf_t没有数据在里面,因此可以把他加到free表中,可以继续读取数据到free中的ngx_buf_t节点了
  218. cl->buf->pos = cl->buf->start;
  219. cl->buf->last = cl->buf->start;
  220.  
  221. *busy = cl->next; //把cl从busy中拆除,然后添加到free头部
  222. cl->next = *free;
  223. *free = cl; // 这个 chain 放到 free 列表的最前面,添加到free头部
  224. }
  225. }
  • 怎么读取request body数据
  1. /*
  2. 在接收较大的包体时,无法在一次调度中完成。但是对HTTP模块而言,接收包体时只需要调用一次ngx_http_read_client_request_body方法就好,这时就需要有另一个方法在
  3. ngx_http_read_client_request_body没接收到完整的包体时,如果连接上再次接收到包体就被调用(触发ngx_http_request_handler),这个方
  4. 法就是ngx_http_read_client_request_body_handler。通过ngx_http_request_handler执行这里的handler
  5. */
  6. static void
  7. ngx_http_read_client_request_body_handler(ngx_http_request_t *r)
  8. {
  9. ngx_int_t rc;
  10.  
  11. /*
  12. 首先检查连接上读事件的timeout标志位,如果为l,则表示接收HTTP包体超时,这时把连接ngx_connection_t结构体上的timeout标志位也置为1,
  13. 同时调用ngx_http_finalize_request方法结束请求,并发送408超时错误码
  14. */
  15. if (r->connection->read->timedout) {
  16. r->connection->timedout = 1;
  17. ngx_http_finalize_request(r, NGX_HTTP_REQUEST_TIME_OUT);
  18. return;
  19. }
  20.  
  21. rc = ngx_http_do_read_client_request_body(r);
  22.  
  23. //检测这个方法的返回值,如果它大于300,那么一定表示希望返回错误码
  24. if (rc >= NGX_HTTP_SPECIAL_RESPONSE) {
  25. ngx_http_finalize_request(r, rc);
  26. }
  27. }
  28.  
  29. /*
  30. 调用ngx_http_do_read_client_request_body方法接收包体。把客户端与Nginx之间TCP连接上套接字缓冲区中的当前字符流全
  31. 部读出来,并判断是否需要写入文件,以及是否接收到全部的包体,同时在接收到完整的包体后激活post_handler回调方法
  32.  
  33. 负责具体的读取包体工作,该函数会在for循环中会反复读直到包体读取完毕,如果内核已经没有数据并且包体还没有读完,则添加读事件,并推出循环,
  34. 这样HTTP模块还能继续作用其他功能,避免阻塞
  35. 读取的时候一个buf装满后,会把buf中存储的数据写道临时文件中(),然后继续使用该buf读取数据,存储
  36. 数据的内存分配地方有两个:1.在读取报文头部的时候ngx_http_wait_request_handler 2.如果在1中读到的内容里面不包括完整包体,则需要在
  37. ngx_http_read_client_request_body中会重新分配内存读取,触发再次读取的地方未ngx_http_read_client_request_body中为读取到完整包体的时候
  38. 添加的ngx_handle_read_event */
  39. static ngx_int_t
  40. ngx_http_do_read_client_request_body(ngx_http_request_t *r)//返回值大于NGX_HTTP_SPECIAL_RESPONSE表示返回错误码
  41. {
  42. off_t rest;
  43. size_t size;
  44. ssize_t n;
  45. ngx_int_t rc;
  46. ngx_buf_t *b;
  47. ngx_chain_t *cl, out;
  48. ngx_connection_t *c;
  49. ngx_http_request_body_t *rb;
  50. ngx_http_core_loc_conf_t *clcf;
  51.  
  52. c = r->connection;
  53. rb = r->request_body;
  54.  
  55. ngx_log_debug0(NGX_LOG_DEBUG_HTTP, c->log, 0,
  56. "http read client request body");
  57.  
  58. for ( ;; ) {
  59. for ( ;; ) {
  60. /*
  61. 首先检查请求的request_body成员中的buf缓冲区,如果缓冲区还有空闲的空间,则跳过该if{}去读取内核中套接字缓冲区里的TCP字符流;
  62. 如果缓冲区已经写满,则调用ngx_http_write_request_body方法把缓冲区中的字符流写入文件。不管有没有配置request_body_in_file_only置1
  63. */
  64. if (rb->buf->last == rb->buf->end) {
  65. /*该buf内容已经计算完毕(也就是通过指针指向空间尾部),需要把该buf指向空间的所有内容拷贝到临时文件中,不管是否有配置
  66. request_body_in_file_only置1, 因为该buf指向的空间会重复利用来读取包体内容*/
  67.  
  68. if (rb->buf->pos != rb->buf->last) {
  69.  
  70. /* pass buffer to request body filter chain */
  71.  
  72. out.buf = rb->buf;
  73. out.next = NULL;
  74. //这里肯定会调用ngx_http_request_body_save_filter->ngx_http_write_request_body写该buf中的内容到临时文件,因为该buf指向的空间
  75. //会重复利用来读取包体内容
  76. rc = ngx_http_request_body_filter(r, &out);
  77.  
  78. if (rc != NGX_OK) {
  79. return rc;
  80. }
  81.  
  82. } else {
  83.  
  84. /* update chains */
  85.  
  86. rc = ngx_http_request_body_filter(r, NULL);
  87.  
  88. if (rc != NGX_OK) {
  89. return rc;
  90. }
  91. }
  92.  
  93. if (rb->busy != NULL) { //如果头部行中的content-length:LEN中的len长度表示后面的包体大小,如果后面的包体数据长度实际比头部中的LEN大,则会走这里,
  94. if (r->request_body_no_buffering) {
  95. if (c->read->timer_set) {
  96. ngx_del_timer(c->read, NGX_FUNC_LINE);
  97. }
  98.  
  99. if (ngx_handle_read_event(c->read, 0, NGX_FUNC_LINE) != NGX_OK) {
  100. return NGX_HTTP_INTERNAL_SERVER_ERROR;
  101. }
  102.  
  103. return NGX_AGAIN;
  104. }
  105.  
  106. return NGX_HTTP_INTERNAL_SERVER_ERROR;
  107. }
  108.  
  109. /*
  110. 为什么能下次还可以直接利用rb->buf空间来读取数据呢?
  111. 当一个rb->buf填满后就会通过ngx_http_write_request_body把bufs链表中的所有ngx_chain_t->ngx_buf_t中指向的数据
  112. 写入到临时文件,因此rb->buf中的内存就可以再次使用了
  113. */
  114. //只需要把缓冲区ngx_buf_t结构体的last指针指向start指针,缓冲区即可复用。
  115. rb->buf->pos = rb->buf->start;
  116. rb->buf->last = rb->buf->start;
  117. }
  118.  
  119. size = rb->buf->end - rb->buf->last; //buf中还剩余这么多空间
  120. rest = rb->rest - (rb->buf->last - rb->buf->pos); //还有多少字节包体没有读取
  121.  
  122. if ((off_t) size > rest) { //说明空间够用来存储剩余的没有读取的字节数
  123. size = (size_t) rest;
  124. }
  125. //负责具体的读取包体工作,该函数会在for循环中会反复读直到包体读取完毕,如果内核已经没有数据并且包体还没有读完,则添加读事件,并退出循环,这样HTTP模块还能继续作用其他功能,避免阻塞
  126. //调用封装了recv的方法从套接字缓冲区中读取包体到缓冲区中。
  127. n = c->recv(c, rb->buf->last, size); //在for循环中会反复读,直到读内核中数据读取完毕,如果读取完毕返回NGX_AGIN
  128.  
  129. ngx_log_debug1(NGX_LOG_DEBUG_HTTP, c->log, 0,
  130. "http client request body recv %z", n);
  131.  
  132. if (n == NGX_AGAIN) {
  133. break;
  134. }
  135.  
  136. if (n == 0) {//如果recv方法返回错误,或者客户端主动关闭了连接
  137. ngx_log_error(NGX_LOG_INFO, c->log, 0,
  138. "client prematurely closed connection");
  139. }
  140.  
  141. if (n == 0 || n == NGX_ERROR) {//如果recv方法返回错误,或者客户端主动关闭了连接
  142. c->error = 1;
  143. return NGX_HTTP_BAD_REQUEST;
  144. }
  145.  
  146. /*
  147. 根据接收到的TCP流长度,修改缓冲区参数。例如,把缓冲区ngx_buf_t结构体的last揩针加上接收到的长度,同时更新request_body结
  148. 构体中表示待接收的剩余包体长度的rest成员、更新ngx_http_request_t结构体中表示已接收请求长度的request_length成员。
  149. */ //从这里可以看出在多次读取包体的时候,需要先把前面开辟空间buf中没有填充的部分填满,如果buf填满了,则重新利用该buf读取数据
  150. //之前读取到填满buf中的数据取出来存放到临时文件中,参考前面的if (rb->buf->last == rb->buf->end)
  151. rb->buf->last += n;
  152. r->request_length += n;
  153.  
  154. if (n == rest) {//根据rest成员检查是否接收到完整的包体
  155. /* pass buffer to request body filter chain */
  156.  
  157. out.buf = rb->buf;
  158. out.next = NULL;
  159.  
  160. rc = ngx_http_request_body_filter(r, &out);
  161.  
  162. if (rc != NGX_OK) {
  163. return rc;
  164. }
  165. }
  166.  
  167. if (rb->rest == 0) {
  168. break; //所有包体读取处理完毕,则退出for
  169. }
  170.  
  171. if (rb->buf->last < rb->buf->end) {
  172. break;
  173. }
  174.  
  175. //break;//yang test xxxxxxxxxxx
  176. }
  177.  
  178. ngx_log_debug1(NGX_LOG_DEBUG_HTTP, c->log, 0,
  179. "http client request body rest %O", rb->rest);
  180.  
  181. if (rb->rest == 0) {
  182. break;
  183. }
  184.  
  185. //printf("yang test ngx agin xxxxxxxxxxxxxxxxxxxxxxxxx\n");
  186. //
  187. //return NGX_AGAIN; //yang test xxxxxxxxxxxxxx
  188. /*
  189. 如果当前已经没有可读的字符流,同时还没有接收到完整的包体,则说明需要把读事件添加到事件模块,等待可读事件发生时,事件框架可以再次
  190. 调度到这个方法接收包体。这一步是调用ngx_add_timer方法将读事件添加到定时器中,超时时间以nginx.conf文件中的client_body_timeout配置项参数为准。
  191. */ //说明前面的 n = c->recv(c, rb->buf->last, size);返回的是NGX_AGAIN,所以在recv中会把ready置0
  192. if (!c->read->ready) {
  193.  
  194. if (r->request_body_no_buffering
  195. && rb->buf->pos != rb->buf->last)
  196. {
  197. /* pass buffer to request body filter chain */
  198.  
  199. out.buf = rb->buf;
  200. out.next = NULL;
  201.  
  202. rc = ngx_http_request_body_filter(r, &out);
  203.  
  204. if (rc != NGX_OK) {
  205. return rc;
  206. }
  207. }
  208.  
  209. clcf = ngx_http_get_module_loc_conf(r, ngx_http_core_module);
  210. //当读取到完整的包体后,会删除该定时器,见后面的ngx_del_timer(c->read);
  211. ngx_add_timer(c->read, clcf->client_body_timeout, NGX_FUNC_LINE);//handle应该是ngx_http_request_handler
  212.  
  213. /*
  214. 这个请求连接上的读事件触发时的回调方法ngx_http_request_handler,从而会调用read_event_handler方法(ngx_http_read_client_request_body_handler)
  215. */
  216. if (ngx_handle_read_event(c->read, 0, NGX_FUNC_LINE) != NGX_OK) { //handle应该是ngx_http_request_handler,通过这里触发再次读取包体
  217. return NGX_HTTP_INTERNAL_SERVER_ERROR;
  218. }
  219.  
  220. return NGX_AGAIN; //把控制器交给HTTP框架,由框架感知读事件,当读事件发生,也就是数据到来,继续读取包体
  221. }
  222. }
  223.  
  224. //只有包体读取完毕,才会从上面的for()循环中退出
  225.  
  226. /*
  227. 表明已经接收到完整的包体,需要做一些收尾工作了。首先不需要检查是否接收HTTP包体超时了,要把读事件从定时器中取出,防止不必要的定时器触发。这一
  228. 步会检查读事件的timer set标志位,如果为1,则调用ngx_del_timer方法把读事件从定时器中移除。
  229. */
  230. if (c->read->timer_set) {
  231. ngx_del_timer(c->read, NGX_FUNC_LINE);
  232. }
  233.  
  234. //如果缓冲区中还有未写入文件的内容,调用ngx_http_write_request_body方法把最后的包体内容也写入文件。
  235. if (rb->temp_file || r->request_body_in_file_only) { //只要之前的内存有写入文件,那么剩余的部分也要写入文件
  236.  
  237. /* save the last part */
  238.  
  239. if (ngx_http_write_request_body(r) != NGX_OK) {
  240. return NGX_HTTP_INTERNAL_SERVER_ERROR;
  241. }
  242.  
  243. if (rb->temp_file->file.offset != 0) {
  244.  
  245. cl = ngx_chain_get_free_buf(r->pool, &rb->free);
  246. if (cl == NULL) {
  247. return NGX_HTTP_INTERNAL_SERVER_ERROR;
  248. }
  249.  
  250. b = cl->buf;
  251.  
  252. ngx_memzero(b, sizeof(ngx_buf_t));
  253.  
  254. b->in_file = 1;
  255. b->file_last = rb->temp_file->file.offset;
  256. b->file = &rb->temp_file->file;
  257.  
  258. rb->bufs = cl; //读取客户包体即使是存入临时文件中,当所有包体读取完毕后(ngx_http_do_read_client_request_body),还是会让r->request_body->bufs指向文件中的相关偏移内存地址
  259.  
  260. } else {
  261. rb->bufs = NULL;
  262. }
  263. }
  264.  
  265. /*
  266. 在之前read_event_handler成员设置为ngx_http_read_client_request_body_handler方法,现在既然已经接收到完整的包体了,就会把
  267. read_event_handler设为ngx_http_block_reading方法,表示连接上再有读事件将不做任何处理。
  268. */
  269. if (!r->request_body_no_buffering) {
  270. r->read_event_handler = ngx_http_block_reading;
  271. rb->post_handler(r); //执行ngx_http_read_client_request_body的第二个参数
  272. }
  273.  
  274. return NGX_OK;
  275. }

http 请求体数据--ngx的更多相关文章

  1. 在Express 中获取表单请求体数据

    在Express 中获取表单请求体数据 获取 GET 请求参数 获取 POST 请求体数据 安装 配置 获取 GET 请求参数 Express 内置了一个 API , 可以直接通过 req.query ...

  2. 获取请求体数据 POST

    POST获取请求体 请求体中封装了 POST请求的请求参数 获取流对象 再从流对象中那数据 一种字节流 一种字符流 BufferedReader getReader()获取字符输入流 只能操作字符 S ...

  3. 入门servlet:request获取请求体数据

    @WebServlet("/RequestDemo5") public class RequestDemo5 extends HttpServlet { protected voi ...

  4. Servlet获取POST方法请求体数据

    if ("POST".equalsIgnoreCase(request.getMethod())) { test = request.getReader().lines().col ...

  5. Django中获取参数(路径,查询,请求头,请求体)

    一.通常HTTP协议向服务器传参有几种途径 : 提取URL的特定部分,如/weather/shanghai/2018,可以在服务器端的路由中用正则表达式截取: 查询字符串(query string), ...

  6. koa2 的处理请求体koa-bodyparser koa-router 的中间件的学习

    1.官网 https://www.npmjs.com/package/koa-router https://www.npmjs.com/package/koa-bodyparser 2. demo / ...

  7. post请求体过大导致ngx.req.get_post_args()取不到参数体的问题

    http://nginx.org/en/docs/http/ngx_http_core_module.html#client_body_buffer_size 该地址对于client_body_buf ...

  8. 获取【请求体】数据的3种方式(精)(文末代码) request.getInputStream() request.getInputStream() request.getReader()

    application/x- www-form-urlencoded是Post请求默认的请求体内容类型,也是form表单默认的类型.Servlet API规范中对该类型的请求内容提供了request. ...

  9. SpringMvc获取前端的数据@RequestBody请求体/@PathVaribale/@RequestParam【支持Ajax】

    一.@RequestBody请求体 注意请求体只有form表单才有,而对于链接来说不使用 1).在Controller中写 @RequestBody String body是基本用法 另外可以封装对象 ...

随机推荐

  1. Redis GEO 功能使用场景

    本文来源:https://www.dazhuanlan.com/2020/02/05/5e3a0a3110649/ 背景 前段时间自己在做附近直播相关业务,其中有一个核心的点就是检索用户附近的主播,也 ...

  2. 小试牛刀-hello,world!(第一个程序)

    1.打开python的IDLE,启动Python解释器(按键盘的windows键,然后输入IDLE),在提示符下>>>输入命令:print("hello,world!&qu ...

  3. 多测师_高级讲师肖sir讲解html中 Button跳转连接方法归纳

    第一种方法: 1.1<a href="http://www.baidu.com">   <input type="button" name=& ...

  4. ngx_align 值对齐宏

    ngx_align 值对齐宏 ngx_align 为nginx中的一个值对齐宏.主要在需要内存申请的地方使用,为了减少在不同的 cache line 中内存而生. // d 为需要对齐的 // a 为 ...

  5. nginx优化:配置gzip压缩页面提高访问速度(nginx1.18.0)

    一,为什么nginx要使用gzip 1,压缩的作用: 页面使用gzip压缩之后, 页面大小可以压缩到原来的1/7左右, 传输速度和页面打开时间都可以大幅度提高, 有利于用户访问页面体验的提升 2,Ng ...

  6. 服务器同一个tomcat部署2两个相同的项目

    项目A,B(B 是A 的复制) 若把A,B工程同时部署到tomcat下,会发生只能访问A,B工程中的其中一个,而另一个会出现404错误(或者无法访问),此时可参照如下方法解决: 步骤1:找到工程下的w ...

  7. java安全编码指南之:ThreadPool的使用

    目录 简介 java自带的线程池 提交给线程池的线程要是可以被中断的 正确处理线程池中线程的异常 线程池中使用ThreadLocal一定要注意清理 简介 在java中,除了单个使用Thread之外,我 ...

  8. pyqt5为控件设置提示信息

    # 显示控件提示消息 import sys from PyQt5.QtWidgets import QHBoxLayout,QMainWindow,QApplication,QToolTip,QPus ...

  9. 使用Volley获取验证码

    时间紧张,直接上代码 public class MainActivity extends AppCompatActivity { private RequestQueue queues ; Strin ...

  10. WTM系列教学视频全免费

    WTM框架问世以来,受到越来越多开发者的喜爱,为了回报大家的厚爱,原本在CSDN上的教学视频已经全部免费,900多分钟的视频,而且还会继续更新. 为了方便大家观看,在B站上也同步更新,地址如下: CS ...