本文是对陶辉《深入理解Nginx》第5章内容的梳理以及实现,代码和注释基本出自此书。
首先要明确的是,这里是编写一个使用upstream的模块,而不是编写upstream模块。因此,和HelloWorld类似,模块结构体ngx_http_mytest_module、模块上下文结构体ngx_http_mytest_module_ctx、数组ngx_http_mytest_command[]、方法ngx_http_mytest()和ngx_http_mytest_handler()的框架是不可少而且又十分相似的。如果忘记了它们之间的关系,请回顾原书或《深入理解Nginx》阅读与实践(一):Nginx安装配置与HelloWorld。
模块处理的请求是ngx_http_request_t结构对象r,它包含了一个ngx_http_upstream_t类型的成员upstream。当upstream非NULL时,将会根据其中设置的内容定制访问第三方服务的方式。而这个请求的处理是由upstream模块来完成的。从这里开始,要注意区分请求r中的upstream成员和Nginx提供的upstream模块不是一回事,而是由前者来指导后者的工作。前者的设定与开启(即告知upstream模块需要进行处理,通过ngx_http_upstream_init()实现)是由我们编写的的第三方模块(本文中是mytest)来完成的。
upstream工作方式的配置可以通过填写由ngx_http_upstream_create(ngx_http_request_t *r)所传入的请求r中的ngx_http_upstream_t结构体来完成。这个函数成功返回时,即把请求r中的upstream设置为非NULL。ngx_http_upstream_t结构体主要包含了一下几个成员,由于原书对这里模块编写所需要用到的成员已做详细介绍(暂时用不到的成员在12章介绍),这里只做一个部分的概括:
typedef ngx_http_upstream_s ngx_http_upstream_t; sturct ngx_http_upstream_s { ... ngx_chain_t request_bufs;//发给上游服务器的请求,由create_request()完成 ngx_http_upstream_conf_t conf;//超时时间等限制性参数 ngx_http_upstream_resolved_t resolved;//用于直接指定的上游服务器地址 //设定方法请见mytest模块的ngx_http_mytest_handler()方法 /* 3个必须实现的回调方法 */ ngx_int_t (*create_request)(ngx_http_request_t *r);//构造向上游服务器发送的请求内容。调用mytest时,只调用一次 ngx_int_t (*process_header)(ngx_http_request_t *r);//收到上游服务器后对包头进行处理的方法 void (*finalize_request) (ngx_http_request_t *r, ngx_int_t rc);//销毁upstream请求时调用 /* 5个可选的回调方法,本文中用不到*/ ngx_int_t (*input_filter_init)(void *data);//处理上游包体 ngx_int_t (*input_filter)(void *data,ssize_t bytes);//处理上游包体 ngx_int_t (*reinit_request)(ngx_http_request_t *r);//第一次向上游服务器建立连接失败时调用 void (*abort_request)(ngx_http_request_t *r); ngx_int_t (*rewrite_redirect)(ngx_http_request_t *r, ngx_table_elt_t *h, size_t prefix); //主要用于反向代理 ... }
可见,使用upstream功能时,除了需要按HelloWorld编写自己的模块和提供处理配置项的方法ngx_http_mytest_create_loc_conf()、ngx_http_mytest_merge_loc_conf()外,还需要填写ngx_http_upstream_t结构体并实现3个必备的回调方法。要注意的是,这些回调方法都是由模块的编写者提供、再由upstream模块来调用的。
原书例子是将访问的URL请求/test?lumia转化成对www.google.com的搜索请求/search?q=lumia。为了简化,大部分参数采用硬编码的形式nginx.conf的添加的内容和以前一样:
location /test {
mytest;
}
typedef struct { ngx_http_upstream_conf_t upstream; } ngx_http_mytest_conf_t; static void* ngx_http_mytest_create_loc_conf(ngx_conf_t *cf) { ngx_http_mytest_conf_t *mycf; mycf = (ngx_http_mytest_conf_t *)ngx_pcalloc(cf->pool,sizeof(ngx_http_mytest_conf_t)); if(mycf == NULL) { return NULL; } mycf->upstream.connect_timeout = 60000; mycf->upstream.send_timeout = 60000; mycf->upstream.read_timeout = 60000; mycf->upstream.store_access = 0600; mycf->upstream.buffering = 0; mycf->upstream.bufs.num = 8; mycf->upstream.bufs.size =ngx_pagesize; mycf->upstream.buffer_size = ngx_pagesize; mycf->upstream.busy_buffers_size = 2*ngx_pagesize; mycf->upstream.max_temp_file_size = 1024*1024*1024; mycf->upstream.hide_headers = NGX_CONF_UNSET_PTR; mycf->upstream.pass_headers = NGX_CONF_UNSET_PTR; return mycf; } static char* ngx_http_mytest_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child) { ngx_http_mytest_conf_t *prev = (ngx_http_mytest_conf_t *)parent; ngx_http_mytest_conf_t *conf = (ngx_http_mytest_conf_t *)child; //ngx_conf_merge_str_value(conf->my_str,prev->my_str,"defaultstr"); ngx_hash_init_t hash; hash.max_size = 100; hash.bucket_size = 1024; hash.name = "proxy_headers_hash"; if(ngx_http_upstream_hide_headers_hash(cf,&conf->upstream, &prev->upstream,ngx_http_proxy_hide_headers,&hash)!=NGX_OK) { return NGX_CONF_ERROR; } return NGX_CONF_OK; }
另外根据作者网页上的源码,需要补充上ngx_http_proxy_hide_headers作为默认设置:
static ngx_str_t ngx_http_proxy_hide_headers[] = { ngx_string("Date"), ngx_string("Server"), ngx_string("X-Pad"), ngx_string("X-Accel-Expires"), ngx_string("X-Accel-Redirect"), ngx_string("X-Accel-Limit-Rate"), ngx_string("X-Accel-Buffering"), ngx_string("X-Accel-Charset"), ngx_null_string };
首先定义ngx_http_mytest_ctx_t结构体用于保存process_header()方法的解析状态,注意结构体的第二个成员原书没有写,需要补充上。
typedef struct { ngx_http_status_t status; ngx_str_t backendServer; } ngx_http_mytest_ctx_t;
原书上3个回调函数的代码如下,详细的注释请参考原书:
static ngx_int_t mytest_upstream_create_request(ngx_http_request_t *r) { static ngx_str_t backendQueryLine = ngx_string("GET /search?q=%V HTTP/1.1\r\nHost: www.google.com\r\nConnection:close\r\n\r\n"); ngx_int_t queryLineLen = backendQueryLine.len + r->args.len - 2; ngx_buf_t *b = ngx_create_temp_buf(r->pool,queryLineLen); if(b==NULL) return NGX_ERROR; b->last = b->pos + queryLineLen; ngx_snprintf(b->pos,queryLineLen, (char*)backendQueryLine.data, &r->args); r->upstream->request_bufs = ngx_alloc_chain_link(r->pool); if(r->upstream->request_bufs == NULL) return NGX_ERROR; r->upstream->request_bufs->buf = b; r->upstream->request_bufs->next = NULL; r->upstream->request_sent = 0; r->upstream->header_sent = 0; r->header_hash = 1; return NGX_OK; }
static ngx_int_t mytest_process_status_line(ngx_http_request_t *r) { size_t len; ngx_int_t rc; ngx_http_upstream_t *u; ngx_http_mytest_ctx_t* ctx = ngx_http_get_module_ctx(r,ngx_http_mytest_module); if(ctx == NULL) { return NGX_ERROR; } u = r->upstream; rc = ngx_http_parse_status_line(r,&u->buffer,&ctx->status); if(rc == NGX_AGAIN) { return rc; } if(rc == NGX_ERROR) { ngx_log_error(NGX_LOG_ERR,r->connection->log,0,"upstream sent no valid HTTP/1.0 header"); r->http_version = NGX_HTTP_VERSION_9; u->state->status = NGX_HTTP_OK; return NGX_OK; } if(u->state) { u->state->status = ctx->status.code; } u->headers_in.status_n = ctx->status.code; len = ctx->status.end - ctx->status.start; u->headers_in.status_line.len = len; u->headers_in.status_line.data = ngx_pnalloc(r->pool,len); if(u->headers_in.status_line.data == NULL) { return NGX_ERROR; } ngx_memcpy(u->headers_in.status_line.data, ctx->status.start,len); u->process_header = mytest_upstream_process_header; return mytest_upstream_process_header(r); } static ngx_int_t mytest_upstream_process_header(ngx_http_request_t *r) { ngx_int_t rc; ngx_table_elt_t *h; ngx_http_upstream_header_t *hh; ngx_http_upstream_main_conf_t *umcf; umcf = ngx_http_get_module_main_conf(r, ngx_http_upstream_module); for ( ;; ) { rc = ngx_http_parse_header_line(r, &r->upstream->buffer, 1); if (rc == NGX_OK) { h = ngx_list_push(&r->upstream->headers_in.headers); if (h == NULL) { return NGX_ERROR; } h->hash = r->header_hash; h->key.len = r->header_name_end - r->header_name_start; h->value.len = r->header_end - r->header_start; h->key.data = ngx_pnalloc(r->pool, h->key.len + 1 + h->value.len + 1 + h->key.len); if (h->key.data == NULL) { return NGX_ERROR; } h->value.data = h->key.data + h->key.len + 1; h->lowcase_key = h->key.data + h->key.len + 1 + h->value.len + 1; ngx_memcpy(h->key.data, r->header_name_start, h->key.len); h->key.data[h->key.len] = '\0'; ngx_memcpy(h->value.data, r->header_start, h->value.len); h->value.data[h->value.len] = '\0'; if (h->key.len == r->lowcase_index) { ngx_memcpy(h->lowcase_key, r->lowcase_header, h->key.len); } else { ngx_strlow(h->lowcase_key, h->key.data, h->key.len); } hh = ngx_hash_find(&umcf->headers_in_hash, h->hash, h->lowcase_key, h->key.len); if (hh && hh->handler(r, h, hh->offset) != NGX_OK) { return NGX_ERROR; } continue; } if (rc == NGX_HTTP_PARSE_HEADER_DONE) { if (r->upstream->headers_in.server == NULL) { h = ngx_list_push(&r->upstream->headers_in.headers); if (h == NULL) { return NGX_ERROR; } h->hash = ngx_hash(ngx_hash(ngx_hash(ngx_hash(ngx_hash('s', 'e'), 'r'), 'v'), 'e'), 'r'); ngx_str_set(&h->key, "Server"); ngx_str_null(&h->value); h->lowcase_key = (u_char *) "server"; } if (r->upstream->headers_in.date == NULL) { h = ngx_list_push(&r->upstream->headers_in.headers); if (h == NULL) { return NGX_ERROR; } h->hash = ngx_hash(ngx_hash(ngx_hash('d', 'a'), 't'), 'e'); ngx_str_set(&h->key, "Date"); ngx_str_null(&h->value); h->lowcase_key = (u_char *) "date"; } return NGX_OK; } if (rc == NGX_AGAIN) { return NGX_AGAIN; } ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "upstream sent invalid header"); return NGX_HTTP_UPSTREAM_INVALID_HEADER; } }
static void mytest_upstream_finalize_request(ngx_http_request_t *r, ngx_int_t rc) { ngx_log_error(NGX_LOG_DEBUG,r->connection->log,0, "mytest_upstream_finalize_request"); }
值得注意的是mytest_upstream_create_request()中计算queryLineLen中有一项-2。这是因为格式控制符"%V"是会被替换成要输出的变量的,在len成员里计算了它的长度,需要减去。这种处理不要忽略,在subrequest的mytest_post_handler()中也出现了类似的处理。
完成的工作是关联HTTP上下文与请求、填写upstream配置结构体和调用ngx_http_upstream_init()启动upstream。
static ngx_int_t ngx_http_mytest_handler(ngx_http_request_t *r) { ngx_http_mytest_ctx_t* myctx = ngx_http_get_module_ctx(r,ngx_http_mytest_module); if(myctx == NULL) { myctx = ngx_palloc(r->pool,sizeof(ngx_http_mytest_ctx_t)); } if(myctx == NULL) { return NGX_ERROR; } ngx_http_set_ctx(r,myctx,ngx_http_mytest_module); if(ngx_http_upstream_create(r)!=NGX_OK) { ngx_log_error(NGX_LOG_ERR, r->connection->log,0, "ngx_http_upstream_create() failed"); return NGX_ERROR; } ngx_http_mytest_conf_t *mycf = (ngx_http_mytest_conf_t *) ngx_http_get_module_loc_conf(r,ngx_http_mytest_module); ngx_http_upstream_t *u = r->upstream; u->conf = &mycf->upstream; u->buffering = mycf->upstream.buffering; u->resolved = (ngx_http_upstream_resolved_t*)ngx_pcalloc(r->pool,sizeof(ngx_http_upstream_resolved_t)); if(u->resolved == NULL) { ngx_log_error(NGX_LOG_ERR,r->connection->log,0,"ngx_pcalloc resolved error.%s.",strerror(errno)); return NGX_ERROR; } static struct sockaddr_in backendSockAddr; struct hostent *pHost = gethostbyname((char*)"www.google.com"); if(pHost == NULL) { ngx_log_error(NGX_LOG_ERR,r->connection->log,0,"gethostbyname fail.%s",strerror(errno)); return NGX_ERROR; } backendSockAddr.sin_family = AF_INET; backendSockAddr.sin_port = htons((in_port_t)80); char* pDmsIP = inet_ntoa(*(struct in_addr*)(pHost->h_addr_list[0])); backendSockAddr.sin_addr.s_addr = inet_addr(pDmsIP); myctx->backendServer.data = (u_char*)pDmsIP; myctx->backendServer.len = strlen(pDmsIP); u->resolved->sockaddr = (struct sockaddr *)&backendSockAddr; u->resolved->socklen = sizeof(struct sockaddr_in); u->resolved->naddrs = 1; u->create_request = mytest_upstream_create_request; u->process_header = mytest_process_status_line; u->finalize_request = mytest_upstream_finalize_request; r->main->count++; ngx_http_upstream_init(r); return NGX_DONE; }
启动nginx,在浏览器中输入http://localhost:8080/test?lumia,可以看到返回的是http://www.google.com.hk/search?q=lumia。
(8080是作者提供的下载源码中nginx.conf设置的侦听端口号;使用hk是由于被重定向了)
1.URL中的问号"?"代表什么?它在参数传递时有什么用?
答:GET方法中的参数请求以问号开始。换句话说,这个"?"后面跟随的是GET方法的参数。
2.HTTP响应行、HTTP头部、HTTP包体的区分
(下面的请求和应答例子来自于维基百科)
客户端请求:
GET / HTTP/1.1
Host:www.google.com
(末尾有一个空行。第一行指定方法、资源路径、协议版本;第二行是在1.1版里必带的一个header作用指定主机)
服务器应答:
HTTP/1.1 200 OK Content-Length: 3059 Server: GWS/2.0 Date: Sat, 11 Jan 2003 02:44:04 GMT Content-Type: text/html Cache-control: private Set-Cookie: PREF=ID=73d4aef52e57bae9:TM=1042253044:LM=1042253044:S=SMCc_HRPCQiqy X9j; expires=Sun, 17-Jan-2038 19:14:07 GMT; path=/; domain=.google.com Connection: keep-alive
...
在这个包头中,第一行就是HTTP响应行,HTTP/1.1表示支持的版本,200是HTTP状态码,表示处理成功,OK是对状态码200的一个简短描述。
根据RFC2616,可能使用“状态行”来描述会更好一些?毕竟本文中处理它的函数是mytest_process_status_line(),而且《TCP/IP详解(卷三)》也翻译为“状态行”。下面用“状态行”代替。
Status-Line The first line of a Response message is the Status-Line, consisting of the protocol version followed by a numeric status code and its associated textual phrase, with each element separated by SP characters. No CR or LF is allowed except in the final CRLF sequence.
Status-Line = HTTP-Version SP Status-Code SP Reason-Phrase CRLF
除了第一行,其余部分中有一部分是HTTP响应头部,即Response Header Fields,它们提供无法放入状态行的信息。RFC2616很清楚的表明,状态行和HTTP头部是两回事:
The response-header fields allow the server to pass additional information about the response which cannot beplaced in the Status- Line. These header fields give information about the server and about further access to the resource identified by the Request-URI.response-header = Accept-Ranges ; Section 14.5 | Age ; Section 14.6 | ETag ; Section 14.19 | Location ; Section 14.30 | Proxy-Authenticate ; Section 14.33 | Retry-After ; Section 14.37 | Server ; Section 14.38 | Vary ; Section 14.44 | WWW-Authenticate ; Section 14.47
在响应头部后,可能还有实体(Entity),而它又分为实体头部(Entity Header Fields)和实体主体(Entity Body),这在RFC2616是进行区分的:
7.1 Entity Header Fields
Entity-header fields define metainformation about the entity-body or,if no body is present, about the resourceidentified by the request. Someof this metainformation is OPTIONAL; some might be REQUIRED by portions of
this specification. entity-header = Allow ; Section 14.7 | Content-Encoding ; Section 14.11 | Content-Language ; Section 14.12 | Content-Length ; Section 14.13 | Content-Location ; Section 14.14 | Content-MD5 ; Section 14.15 | Content-Range ; Section 14.16 | Content-Type ; Section 14.17 | Expires ; Section 14.21 | Last-Modified ; Section 14.29 | extension-header extension-header = message-header
阅读《深入理解Nginx》第3.6.3节可以看出,Nginx是将Response Header Fields和Entity Header Fields合称为HTTP头部一并处理的。
可见,造成理解混乱的原因可能是RFC2616进行区分的Response Header Fields和Entity Header Fields两部分被Nginx一步处理所致。
最后再看看实体主体,可以视之为HTTP传送的正文:
7.2 Entity Body
The entity-body (if any) sent with an HTTP request or response is in a format and encoding defined by the entity-header fields.
这样,就把这几个名词的脉络理清楚了。
subrequest由HTTP框架提供,可以把原始请求分解为许多子请求。
阅读原书5.4和5.5节,可以把使用subrequest的流程概括为:
[HTTP请求需要调用mytest模块处理] -> [mytest模块创建子请求] -> [发送并等待上游服务器处理子请求的响应]
-> (可选)[postpone模块将待转发相应包体放入链表并等待发送 ]
->[执行子请求处理完毕的回调方法ngx_http_post_subrequest_pt]
->[执行父请求被重新激活后的回调方法mytest_post_handler]
这部分使用了代理模块,但在这里不做详细介绍。下面的代码中没有使用postpone。
由于子请求需要访问新浪的服务器,并且URL为http://hq.sinajs.cn/list=s_sh000001,因此设置为
location /list { //上游服务器地址 proxy_pass http://hq.sinajs.cn;
//不希望第三方服务对HTTP包体进行gzip压缩 proxy_set_header Accept-Encoding ""; }
同时,用户访问nginx服务器mytest模块的URI依然要进行配置
location /query {
mytest;
}
新浪服务器返回的数据格式是这样的:
var hq_str_s_sh000001="上证指数,2070.369,-15.233,-0.73,1023439,8503131";
因此把只用来保存请求回调方法中的股票数据的请求上下文定义如下:
typedef struct { ngx_str_t stock[6]; } ngx_http_mytest_ctx;
static ngx_int_t mytest_subrequest_post_handler(ngx_http_request_t *r, void *data, ngx_int_t rc) { //当前请求r是子请求,它的parent成员就指向父请求 ngx_http_request_t *pr = r->parent; //注意,上下文是保存在父请求中的(参见5.6.5节),所以要由pr中取上下文。 //其实有更简单的方法,即参数data就是上下文,初始化subrequest时 //我们就对其进行设置了的,这里仅为了说明如何获取到父请求的上下文 ngx_http_mytest_ctx_t* myctx = ngx_http_get_module_ctx(pr, ngx_http_mytest_module); pr->headers_out.status = r->headers_out.status; //如果返回NGX_HTTP_OK(也就是200)意味着访问新浪服务器成功,接着将 //开始解析http包体 if (r->headers_out.status == NGX_HTTP_OK) { int flag = 0; //在不转发响应时,buffer中会保存着上游服务器的响应。特别是在使用 //反向代理模块访问上游服务器时,如果它使用upstream机制时没有重定义 //input_filter方法,upstream机制默认的input_filter方法会试图 //把所有的上游响应全部保存到buffer缓冲区中 ngx_buf_t* pRecvBuf = &r->upstream->buffer; //以下开始解析上游服务器的响应,并将解析出的值赋到上下文结构体 //myctx->stock数组中 for (; pRecvBuf->pos != pRecvBuf->last; pRecvBuf->pos++) { if (*pRecvBuf->pos == ',' || *pRecvBuf->pos == '\"') { if (flag > 0) { myctx->stock[flag - 1].len = pRecvBuf->pos - myctx->stock[flag - 1].data; } flag++; myctx->stock[flag - 1].data = pRecvBuf->pos + 1; } if (flag > 6) break; } } //这一步很重要,设置接下来父请求的回调方法 pr->write_event_handler = mytest_post_handler; return NGX_OK; }
static void mytest_post_handler(ngx_http_request_t * r) { printf("mytest_post_handler"); //如果没有返回200则直接把错误码发回用户 if (r->headers_out.status != NGX_HTTP_OK) { ngx_http_finalize_request(r, r->headers_out.status); return; } //当前请求是父请求,直接取其上下文 ngx_http_mytest_ctx_t* myctx = ngx_http_get_module_ctx(r, ngx_http_mytest_module); //定义发给用户的http包体内容,格式为: //stock[…],Today current price: …, volumn: … ngx_str_t output_format = ngx_string("stock[%V],Today current price: %V, volumn: %V"); //计算待发送包体的长度 int bodylen = output_format.len + myctx->stock[0].len + myctx->stock[1].len + myctx->stock[4].len - 6; r->headers_out.content_length_n = bodylen; //在内存池上分配内存保存将要发送的包体 ngx_buf_t* b = ngx_create_temp_buf(r->pool, bodylen); ngx_snprintf(b->pos, bodylen, (char*)output_format.data, &myctx->stock[0], &myctx->stock[1], &myctx->stock[4]); b->last = b->pos + bodylen; b->last_buf = 1; ngx_chain_t out; out.buf = b; out.next = NULL; //设置Content-Type,注意汉字编码新浪服务器使用了GBK static ngx_str_t type = ngx_string("text/plain; charset=GBK"); r->headers_out.content_type = type; r->headers_out.status = NGX_HTTP_OK; r->connection->buffered |= NGX_HTTP_WRITE_BUFFERED; ngx_int_t ret = ngx_http_send_header(r); ret = ngx_http_output_filter(r, &out); //注意,这里发送完响应后必须手动调用ngx_http_finalize_request //结束请求,因为这时http框架不会再帮忙调用它 ngx_http_finalize_request(r, ret); }
static void mytest_post_handler(ngx_http_request_t * r) { printf("mytest_post_handler"); //如果没有返回200则直接把错误码发回用户 if (r->headers_out.status != NGX_HTTP_OK) { ngx_http_finalize_request(r, r->headers_out.status); return; } //当前请求是父请求,直接取其上下文 ngx_http_mytest_ctx_t* myctx = ngx_http_get_module_ctx(r, ngx_http_mytest_module); //定义发给用户的http包体内容,格式为: //stock[…],Today current price: …, volumn: … ngx_str_t output_format = ngx_string("stock[%V],Today current price: %V, volumn: %V"); //计算待发送包体的长度 int bodylen = output_format.len + myctx->stock[0].len + myctx->stock[1].len + myctx->stock[4].len - 6; r->headers_out.content_length_n = bodylen; //在内存池上分配内存保存将要发送的包体 ngx_buf_t* b = ngx_create_temp_buf(r->pool, bodylen); ngx_snprintf(b->pos, bodylen, (char*)output_format.data, &myctx->stock[0], &myctx->stock[1], &myctx->stock[4]); b->last = b->pos + bodylen; b->last_buf = 1; ngx_chain_t out; out.buf = b; out.next = NULL; //设置Content-Type,注意汉字编码新浪服务器使用了GBK static ngx_str_t type = ngx_string("text/plain; charset=GBK"); r->headers_out.content_type = type; r->headers_out.status = NGX_HTTP_OK; r->connection->buffered |= NGX_HTTP_WRITE_BUFFERED; ngx_int_t ret = ngx_http_send_header(r); ret = ngx_http_output_filter(r, &out); //注意,这里发送完响应后必须手动调用ngx_http_finalize_request //结束请求,因为这时http框架不会再帮忙调用它 ngx_http_finalize_request(r, ret); }
mytest_post_handler()中的-6的含义与上文upstream部分mytest_upstream_create_request()中计算queryLineLen的-2类似,不再重述。
模块安装后并开启nginx后,输入下面的内容即可看到返回的内容(nginx.conf设置侦听端口号为8080):
http://localhost:8080/query?s_sh000001
另外,如果发现没有响应,请在当前环境(如虚拟机)中尝试直连http://hq.sinajs.cn/list=s_sh000001以保证网络连通性。
本文完整源代码请到《Nginx深入理解》作者陶辉提供的支持页面下载。