nginx-push-stream-module介绍:
A pure stream http push technology for your NGINX setup,这是NGINX官网对nginx-push-stream-module的描述,简单来说就是基于http的推送技术。这个模块本身不包含在nginx安装模块中,需要单独安装,安装过程这里有详细的介绍。
一般的推送技术都是基于tcp的长连接或者websocket全双工通信构建,这样,客户端和服务端在联网条件下,时刻保持一个连接,服务端可以即时推送消息给客户端。http是网络传输中的最顶层的应用层协议,他是无状态的协议,一个请求发出去,服务端返回请求之后,该连接就断开了,服务端无法通过之前的任何请求来与客户端构建一个通道,将消息主动发送给客户端。因此一般的推送技术,不考虑http的方式。
直到nginx-push-stream-module的出现,在http上也可以构建一个类似于长连接的连接,这里我们叫伪长连接,其实是不太准确,因为http是没有连接的语义的。nginx-push-stream-module通过publish/subscribe的方式可以通过sub/lp建立一个挂起的连接,等待服务端来pub,如果一直没有pub消息,那么他会等待一个设置的keepalive-timeout之后,请求自动断开,表现就像请求超时一样,但是这个请求超时并不会造成服务端的压力。
nginx配置:
nginx.conf文件中server部分添加如下配置:
location /pub {
push_stream_publisher admin;
keepalive_timeout 1800;
push_stream_channels_path $arg_id;
push_stream_store_messages off;
client_max_body_size 100k;
}
location /lp {
push_stream_subscriber long-polling;
keepalive_timeout 1800;
push_stream_channels_path $arg_id;
# message template
push_stream_message_template "~text~";
# connection timeout
push_stream_longpolling_connection_ttl 30m;
more_clear_headers 'Server';
more_clear_headers 'Date';
more_clear_headers 'Expires';
more_clear_headers 'Cache-Control';
more_clear_headers 'Etag';
}
location /channels-stats {
push_stream_channels_statistics;
push_stream_channels_path $arg_id;
allow 127.0.0.1;
deny all;
}
为了让nginx-push-stream-module可以运行,还需要在http部分配置如下参数:
push_stream_shared_memory_size 8192m;
push_stream_max_channel_id_length 200;
push_stream_longpolling_connection_ttl 30ms;
push_stream_authorized_channels_only off;
测试:
命令行下输入:curl -s -v --no-buffer http://localhost/lp?id=1001,会等待pub
另起一个命令行界面,输入:curl -s -v -X POST http://localhost/pub?id=1001 -d
"<XML>hello,1001,now is 2018-09-06 09:59:30</XML>"
pub成功返回200状态码,然后再查看刚才挂起的lp请求:
刚才的lp请求会收到pub返回的数据,http请求返回200状态码,请求结束。
Java编码:
java编码的思路是建立一个servlet,接收push请求,请求进来之后,设置header,将请求交给lp?id=xxx,接着模拟一个服务端耗时操作,最后发送一个pub请求,将结果返回给lp,同时push请求也会收到该请求的结果。
App.java
package com.xxx.jettyserver;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import com.xxx.jettyserver.servlet.PushServlet;
public class App{
public static void main( String[] args ){
try {
Server server = new Server(8080);
ServletContextHandler context = new ServletContextHandler(ServletContextHandler.NO_SESSIONS);
context.setContextPath("/");
server.setHandler(context);
context.addServlet(new ServletHolder(new PushServlet()), "/push/*");
server.start();
server.join();
} catch (Exception e) {
e.printStackTrace();
}
}
}
PushServlet.java
package com.xxx.jettyserver.servlet;
import java.io.IOException;
import java.util.Collection;
import java.util.Date;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.log4j.Logger;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.client.util.StringContentProvider;
import org.eclipse.jetty.http.HttpHeader;
public class PushServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
private static final Logger LOGGER = Logger.getLogger(PushServlet.class);
private static final String NGINX_HOST = "10.119.9.149";
private static HttpClient httpClient;
static{
int maxConnectionsPerAddress = 1000;
httpClient = new HttpClient();
httpClient.setMaxConnectionsPerDestination(maxConnectionsPerAddress);
httpClient.setConnectTimeout(3000);
try {
httpClient.start();
} catch (Exception e) {
LOGGER.fatal("HttpClient init faild!", e);
}
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
doPost(req, resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
String id = req.getParameter("id");
String redirect = "/lp?id=" + id;
LOGGER.info("redirect = " + redirect);
resp.setHeader("Connection", "Keep-Alive");
resp.setHeader("X-Accel-Redirect",redirect);
LOGGER.info("ready to flush");
resp.flushBuffer();
//resp.getWriter().close();
Collection<String> headers = resp.getHeaderNames();
for(String header:headers){
LOGGER.info("header "+header+" -> " + resp.getHeader(header));
}
String result = "<XML>hello,"+id+", now is "+new Date()+"</XML>";
send(id, result);
return;
}
public void send(String id,String content){
String url = "http://"+NGINX_HOST+"/pub?id="+id;
try {
LOGGER.info("ready to pub");
//模拟后台一个耗时操作
Thread.sleep(2000);
Request request = httpClient.newRequest(url);
request.method("POST");
request.header(HttpHeader.CONNECTION, "keep-alive");
request.content(new StringContentProvider(content));
ContentResponse response = request.send();
LOGGER.info("send ok : "+response);
} catch (Exception e) {
e.printStackTrace();
}
}
}
重点:X-Accel-Redirect
这里没有直接通过请求lp?id=xxx来构建一个挂起的连接,而是通过一个叫做push的请求进来,然后通过response.setHeader("X-Accel-Redirect","lp?id=xxx")来将push请求重定向到nginx上,lp请求是nginx的,他是辅助我们建立一个挂起的连接,等待服务端pub消息。我们开发中的push请求才是处理业务的。
另外X-Accel-Redirect是nginx的一个header属性,并不是java servlet api中的一个header属性,但是这里仍然可以通过response.setHeader()来设置,通过X-Accel-Redirect可以做文件下载控制。
java测试:
部署:
配置反向代理:nginx.conf http部分增加
upstream pushserver {
server localhost:8080;
keepalive 128;
}
nginx.conf server部分增加
location ~ /push/(.*) {
proxy_pass http://pushserver;
proxy_set_header X-Real-IP $remote_addr;
keepalive_timeout 1800;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_http_version 1.1;
proxy_set_header Connection "";
allow all;
}
重启nginx
通过postman发送一个post请求:http://10.119.9.149/push/get?id=1001
这样借助nginx-push-stream-module,我们实现了一个基于http的推送。
关于http连接
一般的http请求如果服务端长时间(5s以内)无返回,我们会认为请求超时,让连接断开,让别的请求进来,以减轻服务器压力。这里通过nginx-push-stream-module构建的lp请求,他会一直等待服务器返回,不管是错误,还是正常的消息返回,只要返回了或者超过保活时间,连接还是会断开。但是在等待返回这段时间内,其实他建立了一个channel,我们可以通过这个channel来向lp主动发送消息,这就是与普通http请求不同的地方。
可以说nginx-push-stream-module提供了保活+通道的能力,让基于http的应用可以有伪长连接的功能,但是它缺少心跳,不是真正的长连接。