java+nginx-push-stream-module实现消息推送功能

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的应用可以有伪长连接的功能,但是它缺少心跳,不是真正的长连接。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

luffy5459

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值