add jetty-rhttp from codehaus to jetty-9
This commit is contained in:
parent
553df4580a
commit
d8e51cca72
|
@ -0,0 +1,33 @@
|
|||
Reverse HTTP
|
||||
|
||||
The HTTP server paradigm is a valuable abstraction for browsing and accessing data and applications in a RESTful fashion from thin clients or
|
||||
other applications. However, when it comes to mobile devices, the server paradigm is often not available because those devices exist on
|
||||
restricted networks that do not allow inbound connections. These devices (eg. phones, tablets, industrial controllers, etc.) often have
|
||||
signficant content (eg. photos, video, music, contacts, etc.) and services (eg. GPS, phone, modem, camera, sound) that are worthwile to access
|
||||
remotely and often the HTTP server model is very applicable.
|
||||
|
||||
The Jetty reverse HTTP module provides a gateway that efficiently allows HTTP connectivety to servers running in outbound-only networks. There are two key components:
|
||||
|
||||
The reverse HTTP connector is a jetty connector (like the HTTP, SSL, AJP connectors) that accepts HTTP requests for the Jetty server instance. However, the reverse HTTP connector does not accept inbound TCP/IP connections. Instead it makes an outbound HTTP connection to the reverse HTTP gateway and uses a long polling mechanism to efficiently and asynchronously fetch requests and send responses.
|
||||
|
||||
The reverse HTTP gateway is a jetty server that accepts inbound connections from one or more Reverse HTTP connectors and makes them available as normal HTTP targets.
|
||||
|
||||
To demonstrate this from a source release, first run a gateway instance:
|
||||
|
||||
cd jetty-reverse-http/reverse-http-gateway
|
||||
mvn exec:java
|
||||
|
||||
In another window, you can run 3 test servers with reverse connectors with:
|
||||
|
||||
cd jetty-reverse-http/reverse-http-connector
|
||||
mvn exec:java
|
||||
|
||||
|
||||
The three servers are using context path ID's at the gateway (virtual host and cookie based mappings can also be done), so you can access the
|
||||
three servers via the gateway at:
|
||||
|
||||
http://localhost:8080/gw/A
|
||||
http://localhost:8080/gw/B
|
||||
http://localhost:8080/gw/C
|
||||
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<parent>
|
||||
<groupId>org.eclipse.jetty.rhttp</groupId>
|
||||
<artifactId>jetty-rhttp-project</artifactId>
|
||||
<version>9.0.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<artifactId>reverse-http-client</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
<name>Jetty :: Reverse HTTP :: Client</name>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty</groupId>
|
||||
<artifactId>jetty-util</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty</groupId>
|
||||
<artifactId>jetty-io</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty</groupId>
|
||||
<artifactId>jetty-client</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty</groupId>
|
||||
<artifactId>jetty-http</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.httpcomponents</groupId>
|
||||
<artifactId>httpclient</artifactId>
|
||||
<version>4.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>net.jcip</groupId>
|
||||
<artifactId>jcip-annotations</artifactId>
|
||||
<version>1.0</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty</groupId>
|
||||
<artifactId>jetty-server</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
|
@ -0,0 +1,270 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
|
||||
// ------------------------------------------------------------------------
|
||||
// All rights reserved. This program and the accompanying materials
|
||||
// are made available under the terms of the Eclipse Public License v1.0
|
||||
// and Apache License v2.0 which accompanies this distribution.
|
||||
//
|
||||
// The Eclipse Public License is available at
|
||||
// http://www.eclipse.org/legal/epl-v10.html
|
||||
//
|
||||
// The Apache License v2.0 is available at
|
||||
// http://www.opensource.org/licenses/apache2.0.php
|
||||
//
|
||||
// You may elect to redistribute this code under either of these licenses.
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.rhttp.client;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URLEncoder;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
|
||||
import org.eclipse.jetty.util.component.AbstractLifeCycle;
|
||||
import org.eclipse.jetty.util.log.Log;
|
||||
import org.eclipse.jetty.util.log.Logger;
|
||||
|
||||
/**
|
||||
* @version $Revision$ $Date$
|
||||
*/
|
||||
public abstract class AbstractClient extends AbstractLifeCycle implements RHTTPClient
|
||||
{
|
||||
private final Logger logger = Log.getLogger("org.mortbay.jetty.rhttp.client");
|
||||
private final List<RHTTPListener> listeners = new CopyOnWriteArrayList<RHTTPListener>();
|
||||
private final List<ClientListener> clientListeners = new CopyOnWriteArrayList<ClientListener>();
|
||||
private final String targetId;
|
||||
private volatile Status status = Status.DISCONNECTED;
|
||||
|
||||
public AbstractClient(String targetId)
|
||||
{
|
||||
this.targetId = targetId;
|
||||
}
|
||||
|
||||
public String getGatewayURI()
|
||||
{
|
||||
return "http://"+getHost()+":"+getPort()+getPath();
|
||||
}
|
||||
|
||||
public String getTargetId()
|
||||
{
|
||||
return targetId;
|
||||
}
|
||||
|
||||
public Logger getLogger()
|
||||
{
|
||||
return logger;
|
||||
}
|
||||
|
||||
public void addListener(RHTTPListener listener)
|
||||
{
|
||||
listeners.add(listener);
|
||||
}
|
||||
|
||||
public void removeListener(RHTTPListener listener)
|
||||
{
|
||||
listeners.remove(listener);
|
||||
}
|
||||
|
||||
public void addClientListener(ClientListener listener)
|
||||
{
|
||||
clientListeners.add(listener);
|
||||
}
|
||||
|
||||
public void removeClientListener(ClientListener listener)
|
||||
{
|
||||
clientListeners.remove(listener);
|
||||
}
|
||||
|
||||
protected void notifyRequests(List<RHTTPRequest> requests)
|
||||
{
|
||||
for (RHTTPRequest request : requests)
|
||||
{
|
||||
for (RHTTPListener listener : listeners)
|
||||
{
|
||||
try
|
||||
{
|
||||
listener.onRequest(request);
|
||||
}
|
||||
catch (Throwable x)
|
||||
{
|
||||
logger.warn("Listener " + listener + " threw", x);
|
||||
try
|
||||
{
|
||||
deliver(newExceptionResponse(request.getId(), x));
|
||||
}
|
||||
catch (IOException xx)
|
||||
{
|
||||
logger.debug("Could not deliver exception response", xx);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected RHTTPResponse newExceptionResponse(int requestId, Throwable x)
|
||||
{
|
||||
try
|
||||
{
|
||||
int statusCode = 500;
|
||||
String statusMessage = "Internal Server Error";
|
||||
Map<String, String> headers = new HashMap<String, String>();
|
||||
byte[] body = x.toString().getBytes("UTF-8");
|
||||
return new RHTTPResponse(requestId, statusCode, statusMessage, headers, body);
|
||||
}
|
||||
catch (UnsupportedEncodingException xx)
|
||||
{
|
||||
throw new AssertionError(xx);
|
||||
}
|
||||
}
|
||||
|
||||
protected void notifyConnectRequired()
|
||||
{
|
||||
for (ClientListener listener : clientListeners)
|
||||
{
|
||||
try
|
||||
{
|
||||
listener.connectRequired();
|
||||
}
|
||||
catch (Throwable x)
|
||||
{
|
||||
logger.warn("ClientListener " + listener + " threw", x);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void notifyConnectException()
|
||||
{
|
||||
for (ClientListener listener : clientListeners)
|
||||
{
|
||||
try
|
||||
{
|
||||
listener.connectException();
|
||||
}
|
||||
catch (Throwable x)
|
||||
{
|
||||
logger.warn("ClientListener " + listener + " threw", x);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void notifyConnectClosed()
|
||||
{
|
||||
for (ClientListener listener : clientListeners)
|
||||
{
|
||||
try
|
||||
{
|
||||
listener.connectClosed();
|
||||
}
|
||||
catch (Throwable xx)
|
||||
{
|
||||
logger.warn("ClientListener " + listener + " threw", xx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void notifyDeliverException(RHTTPResponse response)
|
||||
{
|
||||
for (ClientListener listener : clientListeners)
|
||||
{
|
||||
try
|
||||
{
|
||||
listener.deliverException(response);
|
||||
}
|
||||
catch (Throwable x)
|
||||
{
|
||||
logger.warn("ClientListener " + listener + " threw", x);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected String urlEncode(String value)
|
||||
{
|
||||
try
|
||||
{
|
||||
return URLEncoder.encode(value, "UTF-8");
|
||||
}
|
||||
catch (UnsupportedEncodingException x)
|
||||
{
|
||||
getLogger().debug("", x);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
protected boolean isConnected()
|
||||
{
|
||||
return status == Status.CONNECTED;
|
||||
}
|
||||
|
||||
protected boolean isDisconnecting()
|
||||
{
|
||||
return status == Status.DISCONNECTING;
|
||||
}
|
||||
|
||||
protected boolean isDisconnected()
|
||||
{
|
||||
return status == Status.DISCONNECTED;
|
||||
}
|
||||
|
||||
public void connect() throws IOException
|
||||
{
|
||||
if (isDisconnected())
|
||||
status = Status.CONNECTING;
|
||||
|
||||
syncHandshake();
|
||||
this.status = Status.CONNECTED;
|
||||
|
||||
asyncConnect();
|
||||
}
|
||||
|
||||
public void disconnect() throws IOException
|
||||
{
|
||||
if (isConnected())
|
||||
{
|
||||
status = Status.DISCONNECTING;
|
||||
try
|
||||
{
|
||||
syncDisconnect();
|
||||
}
|
||||
finally
|
||||
{
|
||||
status = Status.DISCONNECTED;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void deliver(RHTTPResponse response) throws IOException
|
||||
{
|
||||
asyncDeliver(response);
|
||||
}
|
||||
|
||||
protected abstract void syncHandshake() throws IOException;
|
||||
|
||||
protected abstract void asyncConnect();
|
||||
|
||||
protected abstract void syncDisconnect() throws IOException;
|
||||
|
||||
protected abstract void asyncDeliver(RHTTPResponse response);
|
||||
|
||||
protected void connectComplete(byte[] responseContent) throws IOException
|
||||
{
|
||||
List<RHTTPRequest> requests = RHTTPRequest.fromFrameBytes(responseContent);
|
||||
getLogger().debug("Client {} connect returned from gateway, requests {}", getTargetId(), requests);
|
||||
|
||||
// Requests are arrived, reconnect while we process them
|
||||
if (!isDisconnecting() && !isDisconnected())
|
||||
asyncConnect();
|
||||
|
||||
notifyRequests(requests);
|
||||
}
|
||||
|
||||
protected enum Status
|
||||
{
|
||||
CONNECTING, CONNECTED, DISCONNECTING, DISCONNECTED
|
||||
}
|
||||
}
|
|
@ -0,0 +1,156 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
|
||||
// ------------------------------------------------------------------------
|
||||
// All rights reserved. This program and the accompanying materials
|
||||
// are made available under the terms of the Eclipse Public License v1.0
|
||||
// and Apache License v2.0 which accompanies this distribution.
|
||||
//
|
||||
// The Eclipse Public License is available at
|
||||
// http://www.eclipse.org/legal/epl-v10.html
|
||||
//
|
||||
// The Apache License v2.0 is available at
|
||||
// http://www.opensource.org/licenses/apache2.0.php
|
||||
//
|
||||
// You may elect to redistribute this code under either of these licenses.
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.rhttp.client;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.apache.http.HttpEntity;
|
||||
import org.apache.http.HttpHost;
|
||||
import org.apache.http.HttpResponse;
|
||||
import org.apache.http.HttpStatus;
|
||||
import org.apache.http.NoHttpResponseException;
|
||||
import org.apache.http.client.HttpClient;
|
||||
import org.apache.http.client.methods.HttpPost;
|
||||
import org.apache.http.entity.ByteArrayEntity;
|
||||
import org.apache.http.util.EntityUtils;
|
||||
|
||||
/**
|
||||
* Implementation of {@link RHTTPClient} that uses Apache's HttpClient.
|
||||
*
|
||||
* @version $Revision$ $Date$
|
||||
*/
|
||||
public class ApacheClient extends AbstractClient
|
||||
{
|
||||
private final HttpClient httpClient;
|
||||
private final String gatewayPath;
|
||||
|
||||
public ApacheClient(HttpClient httpClient, String gatewayPath, String targetId)
|
||||
{
|
||||
super(targetId);
|
||||
this.httpClient = httpClient;
|
||||
this.gatewayPath = gatewayPath;
|
||||
}
|
||||
|
||||
public String getHost()
|
||||
{
|
||||
return ((HttpHost)httpClient.getParams().getParameter("http.default-host")).getHostName();
|
||||
}
|
||||
|
||||
public int getPort()
|
||||
{
|
||||
return ((HttpHost)httpClient.getParams().getParameter("http.default-host")).getPort();
|
||||
}
|
||||
|
||||
public String getPath()
|
||||
{
|
||||
return gatewayPath;
|
||||
}
|
||||
|
||||
protected void syncHandshake() throws IOException
|
||||
{
|
||||
HttpPost handshake = new HttpPost(gatewayPath + "/" + urlEncode(getTargetId()) + "/handshake");
|
||||
HttpResponse response = httpClient.execute(handshake);
|
||||
int statusCode = response.getStatusLine().getStatusCode();
|
||||
HttpEntity entity = response.getEntity();
|
||||
if (entity != null)
|
||||
entity.consumeContent();
|
||||
if (statusCode != HttpStatus.SC_OK)
|
||||
throw new IOException("Handshake failed");
|
||||
getLogger().debug("Client {} handshake returned from gateway", getTargetId(), null);
|
||||
}
|
||||
|
||||
protected void asyncConnect()
|
||||
{
|
||||
new Thread()
|
||||
{
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
try
|
||||
{
|
||||
HttpPost connect = new HttpPost(gatewayPath + "/" + urlEncode(getTargetId()) + "/connect");
|
||||
getLogger().debug("Client {} connect sent to gateway", getTargetId(), null);
|
||||
HttpResponse response = httpClient.execute(connect);
|
||||
int statusCode = response.getStatusLine().getStatusCode();
|
||||
HttpEntity entity = response.getEntity();
|
||||
byte[] responseContent = EntityUtils.toByteArray(entity);
|
||||
if (statusCode == HttpStatus.SC_OK)
|
||||
connectComplete(responseContent);
|
||||
else if (statusCode == HttpStatus.SC_UNAUTHORIZED)
|
||||
notifyConnectRequired();
|
||||
else
|
||||
notifyConnectException();
|
||||
}
|
||||
catch (NoHttpResponseException x)
|
||||
{
|
||||
notifyConnectClosed();
|
||||
}
|
||||
catch (IOException x)
|
||||
{
|
||||
getLogger().debug("", x);
|
||||
notifyConnectException();
|
||||
}
|
||||
}
|
||||
}.start();
|
||||
}
|
||||
|
||||
protected void syncDisconnect() throws IOException
|
||||
{
|
||||
HttpPost disconnect = new HttpPost(gatewayPath + "/" + urlEncode(getTargetId()) + "/disconnect");
|
||||
HttpResponse response = httpClient.execute(disconnect);
|
||||
int statusCode = response.getStatusLine().getStatusCode();
|
||||
HttpEntity entity = response.getEntity();
|
||||
if (entity != null)
|
||||
entity.consumeContent();
|
||||
if (statusCode != HttpStatus.SC_OK)
|
||||
throw new IOException("Disconnect failed");
|
||||
getLogger().debug("Client {} disconnect returned from gateway", getTargetId(), null);
|
||||
}
|
||||
|
||||
protected void asyncDeliver(final RHTTPResponse response)
|
||||
{
|
||||
new Thread()
|
||||
{
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
try
|
||||
{
|
||||
HttpPost deliver = new HttpPost(gatewayPath + "/" + urlEncode(getTargetId()) + "/deliver");
|
||||
deliver.setEntity(new ByteArrayEntity(response.getFrameBytes()));
|
||||
getLogger().debug("Client {} deliver sent to gateway, response {}", getTargetId(), response);
|
||||
HttpResponse httpResponse = httpClient.execute(deliver);
|
||||
int statusCode = httpResponse.getStatusLine().getStatusCode();
|
||||
HttpEntity entity = httpResponse.getEntity();
|
||||
if (entity != null)
|
||||
entity.consumeContent();
|
||||
if (statusCode == HttpStatus.SC_UNAUTHORIZED)
|
||||
notifyConnectRequired();
|
||||
else if (statusCode != HttpStatus.SC_OK)
|
||||
notifyDeliverException(response);
|
||||
}
|
||||
catch (IOException x)
|
||||
{
|
||||
getLogger().debug("", x);
|
||||
notifyDeliverException(response);
|
||||
}
|
||||
}
|
||||
}.start();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
|
||||
// ------------------------------------------------------------------------
|
||||
// All rights reserved. This program and the accompanying materials
|
||||
// are made available under the terms of the Eclipse Public License v1.0
|
||||
// and Apache License v2.0 which accompanies this distribution.
|
||||
//
|
||||
// The Eclipse Public License is available at
|
||||
// http://www.eclipse.org/legal/epl-v10.html
|
||||
//
|
||||
// The Apache License v2.0 is available at
|
||||
// http://www.opensource.org/licenses/apache2.0.php
|
||||
//
|
||||
// You may elect to redistribute this code under either of these licenses.
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.rhttp.client;
|
||||
|
||||
/**
|
||||
* A listener for network-related events happening on the gateway client.
|
||||
*
|
||||
* @version $Revision$ $Date$
|
||||
*/
|
||||
public interface ClientListener
|
||||
{
|
||||
/**
|
||||
* Called when the client detects that the server requested a new connect.
|
||||
*/
|
||||
public void connectRequired();
|
||||
|
||||
/**
|
||||
* Called when the client detects that the connection has been closed by the server.
|
||||
*/
|
||||
public void connectClosed();
|
||||
|
||||
/**
|
||||
* Called when the client detects a generic exception while trying to connect to the server.
|
||||
*/
|
||||
public void connectException();
|
||||
|
||||
/**
|
||||
* Called when the client detects a generic exception while tryint to deliver to the server.
|
||||
* @param response the Response object that should have been sent to the server
|
||||
*/
|
||||
public void deliverException(RHTTPResponse response);
|
||||
|
||||
public static class Adapter implements ClientListener
|
||||
{
|
||||
public void connectRequired()
|
||||
{
|
||||
}
|
||||
|
||||
public void connectClosed()
|
||||
{
|
||||
}
|
||||
|
||||
public void connectException()
|
||||
{
|
||||
}
|
||||
|
||||
public void deliverException(RHTTPResponse response)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,306 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
|
||||
// ------------------------------------------------------------------------
|
||||
// All rights reserved. This program and the accompanying materials
|
||||
// are made available under the terms of the Eclipse Public License v1.0
|
||||
// and Apache License v2.0 which accompanies this distribution.
|
||||
//
|
||||
// The Eclipse Public License is available at
|
||||
// http://www.eclipse.org/legal/epl-v10.html
|
||||
//
|
||||
// The Apache License v2.0 is available at
|
||||
// http://www.opensource.org/licenses/apache2.0.php
|
||||
//
|
||||
// You may elect to redistribute this code under either of these licenses.
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.rhttp.client;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.EOFException;
|
||||
import java.io.IOException;
|
||||
|
||||
import org.eclipse.jetty.client.Address;
|
||||
import org.eclipse.jetty.client.ContentExchange;
|
||||
import org.eclipse.jetty.client.HttpClient;
|
||||
import org.eclipse.jetty.client.HttpExchange;
|
||||
import org.eclipse.jetty.http.HttpMethods;
|
||||
import org.eclipse.jetty.http.HttpURI;
|
||||
import org.eclipse.jetty.io.Buffer;
|
||||
import org.eclipse.jetty.io.ByteArrayBuffer;
|
||||
import org.eclipse.jetty.io.EofException;
|
||||
|
||||
/**
|
||||
* Implementation of {@link RHTTPClient} that uses Jetty's HttpClient.
|
||||
*
|
||||
* @version $Revision$ $Date$
|
||||
*/
|
||||
public class JettyClient extends AbstractClient
|
||||
{
|
||||
private final HttpClient httpClient;
|
||||
private final Address gatewayAddress;
|
||||
private final String gatewayPath;
|
||||
|
||||
public JettyClient(HttpClient httpClient, Address gatewayAddress, String gatewayPath, String targetId)
|
||||
{
|
||||
super(targetId);
|
||||
this.httpClient = httpClient;
|
||||
this.gatewayAddress = gatewayAddress;
|
||||
this.gatewayPath = gatewayPath;
|
||||
}
|
||||
|
||||
public JettyClient(HttpClient httpClient, String gatewayURI, String targetId)
|
||||
{
|
||||
super(targetId);
|
||||
|
||||
HttpURI uri = new HttpURI(gatewayURI);
|
||||
|
||||
this.httpClient = httpClient;
|
||||
this.gatewayAddress = new Address(uri.getHost(),uri.getPort());
|
||||
this.gatewayPath = uri.getPath();
|
||||
}
|
||||
|
||||
public String getHost()
|
||||
{
|
||||
return gatewayAddress.getHost();
|
||||
}
|
||||
|
||||
public int getPort()
|
||||
{
|
||||
return gatewayAddress.getPort();
|
||||
}
|
||||
|
||||
public String getPath()
|
||||
{
|
||||
return gatewayPath;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doStart() throws Exception
|
||||
{
|
||||
httpClient.start();
|
||||
super.doStart();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doStop() throws Exception
|
||||
{
|
||||
super.doStop();
|
||||
httpClient.stop();
|
||||
}
|
||||
|
||||
protected void syncHandshake() throws IOException
|
||||
{
|
||||
HandshakeExchange exchange = new HandshakeExchange();
|
||||
exchange.setMethod(HttpMethods.POST);
|
||||
exchange.setAddress(gatewayAddress);
|
||||
exchange.setURI(gatewayPath + "/" + urlEncode(getTargetId()) + "/handshake");
|
||||
httpClient.send(exchange);
|
||||
getLogger().debug("Client {} handshake sent to gateway", getTargetId(), null);
|
||||
|
||||
try
|
||||
{
|
||||
int exchangeStatus = exchange.waitForDone();
|
||||
if (exchangeStatus != HttpExchange.STATUS_COMPLETED)
|
||||
throw new IOException("Handshake failed");
|
||||
if (exchange.getResponseStatus() != 200)
|
||||
throw new IOException("Handshake failed");
|
||||
getLogger().debug("Client {} handshake returned from gateway", getTargetId(), null);
|
||||
}
|
||||
catch (InterruptedException x)
|
||||
{
|
||||
Thread.currentThread().interrupt();
|
||||
throw newIOException(x);
|
||||
}
|
||||
}
|
||||
|
||||
private IOException newIOException(Throwable x)
|
||||
{
|
||||
return (IOException)new IOException().initCause(x);
|
||||
}
|
||||
|
||||
protected void asyncConnect()
|
||||
{
|
||||
try
|
||||
{
|
||||
ConnectExchange exchange = new ConnectExchange();
|
||||
exchange.setMethod(HttpMethods.POST);
|
||||
exchange.setAddress(gatewayAddress);
|
||||
exchange.setURI(gatewayPath + "/" + urlEncode(getTargetId()) + "/connect");
|
||||
httpClient.send(exchange);
|
||||
getLogger().debug("Client {} connect sent to gateway", getTargetId(), null);
|
||||
}
|
||||
catch (IOException x)
|
||||
{
|
||||
getLogger().debug("Could not send exchange", x);
|
||||
throw new RuntimeException(x);
|
||||
}
|
||||
}
|
||||
|
||||
protected void syncDisconnect() throws IOException
|
||||
{
|
||||
DisconnectExchange exchange = new DisconnectExchange();
|
||||
exchange.setMethod(HttpMethods.POST);
|
||||
exchange.setAddress(gatewayAddress);
|
||||
exchange.setURI(gatewayPath + "/" + urlEncode(getTargetId()) + "/disconnect");
|
||||
httpClient.send(exchange);
|
||||
getLogger().debug("Client {} disconnect sent to gateway", getTargetId(), null);
|
||||
try
|
||||
{
|
||||
int status = exchange.waitForDone();
|
||||
if (status != HttpExchange.STATUS_COMPLETED)
|
||||
throw new IOException("Disconnect failed");
|
||||
if (exchange.getResponseStatus() != 200)
|
||||
throw new IOException("Disconnect failed");
|
||||
getLogger().debug("Client {} disconnect returned from gateway", getTargetId(), null);
|
||||
}
|
||||
catch (InterruptedException x)
|
||||
{
|
||||
Thread.currentThread().interrupt();
|
||||
throw newIOException(x);
|
||||
}
|
||||
}
|
||||
|
||||
protected void asyncDeliver(RHTTPResponse response)
|
||||
{
|
||||
try
|
||||
{
|
||||
DeliverExchange exchange = new DeliverExchange(response);
|
||||
exchange.setMethod(HttpMethods.POST);
|
||||
exchange.setAddress(gatewayAddress);
|
||||
exchange.setURI(gatewayPath + "/" + urlEncode(getTargetId()) + "/deliver");
|
||||
exchange.setRequestContent(new ByteArrayBuffer(response.getFrameBytes()));
|
||||
httpClient.send(exchange);
|
||||
getLogger().debug("Client {} deliver sent to gateway, response {}", getTargetId(), response);
|
||||
}
|
||||
catch (IOException x)
|
||||
{
|
||||
getLogger().debug("Could not send exchange", x);
|
||||
throw new RuntimeException(x);
|
||||
}
|
||||
}
|
||||
|
||||
protected class HandshakeExchange extends ContentExchange
|
||||
{
|
||||
protected HandshakeExchange()
|
||||
{
|
||||
super(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onConnectionFailed(Throwable x)
|
||||
{
|
||||
getLogger().warn(x.toString());
|
||||
getLogger().debug(x);
|
||||
}
|
||||
}
|
||||
|
||||
protected class ConnectExchange extends ContentExchange
|
||||
{
|
||||
private final ByteArrayOutputStream content = new ByteArrayOutputStream();
|
||||
|
||||
protected ConnectExchange()
|
||||
{
|
||||
super(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResponseContent(Buffer buffer) throws IOException
|
||||
{
|
||||
buffer.writeTo(content);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResponseComplete()
|
||||
{
|
||||
int responseStatus = getResponseStatus();
|
||||
if (responseStatus == 200)
|
||||
{
|
||||
try
|
||||
{
|
||||
connectComplete(content.toByteArray());
|
||||
}
|
||||
catch (IOException x)
|
||||
{
|
||||
onException(x);
|
||||
}
|
||||
}
|
||||
else if (responseStatus == 401)
|
||||
{
|
||||
notifyConnectRequired();
|
||||
}
|
||||
else
|
||||
{
|
||||
notifyConnectException();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onException(Throwable x)
|
||||
{
|
||||
getLogger().debug(x);
|
||||
if (x instanceof EofException || x instanceof EOFException)
|
||||
{
|
||||
notifyConnectClosed();
|
||||
}
|
||||
else
|
||||
{
|
||||
notifyConnectException();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onConnectionFailed(Throwable x)
|
||||
{
|
||||
getLogger().debug(x);
|
||||
}
|
||||
}
|
||||
|
||||
protected class DisconnectExchange extends ContentExchange
|
||||
{
|
||||
protected DisconnectExchange()
|
||||
{
|
||||
super(true);
|
||||
}
|
||||
}
|
||||
|
||||
protected class DeliverExchange extends ContentExchange
|
||||
{
|
||||
private final RHTTPResponse response;
|
||||
|
||||
protected DeliverExchange(RHTTPResponse response)
|
||||
{
|
||||
super(true);
|
||||
this.response = response;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResponseComplete() throws IOException
|
||||
{
|
||||
int responseStatus = getResponseStatus();
|
||||
if (responseStatus == 401)
|
||||
{
|
||||
notifyConnectRequired();
|
||||
}
|
||||
else if (responseStatus != 200)
|
||||
{
|
||||
notifyDeliverException(response);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onException(Throwable x)
|
||||
{
|
||||
getLogger().debug(x);
|
||||
notifyDeliverException(response);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onConnectionFailed(Throwable x)
|
||||
{
|
||||
getLogger().debug(x);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,133 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
|
||||
// ------------------------------------------------------------------------
|
||||
// All rights reserved. This program and the accompanying materials
|
||||
// are made available under the terms of the Eclipse Public License v1.0
|
||||
// and Apache License v2.0 which accompanies this distribution.
|
||||
//
|
||||
// The Eclipse Public License is available at
|
||||
// http://www.eclipse.org/legal/epl-v10.html
|
||||
//
|
||||
// The Apache License v2.0 is available at
|
||||
// http://www.opensource.org/licenses/apache2.0.php
|
||||
//
|
||||
// You may elect to redistribute this code under either of these licenses.
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.rhttp.client;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* <p><tt>RHTTPClient</tt> represent a client of the gateway server.</p>
|
||||
* <p>A <tt>Client</tt> has a server side counterpart with which communicates
|
||||
* using a comet protocol.<br /> The <tt>Client</tt>, its server-side
|
||||
* counterpart and the comet protocol form the <em>Half-Object plus Protocol</em>
|
||||
* pattern.</p>
|
||||
* <p>A <tt>Client</tt> must first connect to the gateway server, to let the gateway
|
||||
* server know its targetId, an identifier that uniquely distinguish this
|
||||
* <tt>Client</tt> from other <tt>Client</tt>s.</p>
|
||||
* <p>Once connected, the gateway server will use a comet procotol to notify the
|
||||
* <tt>Client</tt> of server-side events, and the <tt>Client</tt> can send
|
||||
* information to the gateway server to notify it of client-side events.</p>
|
||||
* <p>Server-side event are notified to {@link RHTTPListener}s, while relevant
|
||||
* network events are communicated to {@link ClientListener}s.</p>
|
||||
*
|
||||
* @version $Revision$ $Date$
|
||||
*/
|
||||
public interface RHTTPClient
|
||||
{
|
||||
/**
|
||||
* @return The gateway uri, typically "http://gatewayhost:gatewayport/gatewaypath".
|
||||
*/
|
||||
public String getGatewayURI();
|
||||
|
||||
/**
|
||||
* @return The gateway host
|
||||
*/
|
||||
public String getHost();
|
||||
|
||||
/**
|
||||
* @return The gateway port
|
||||
*/
|
||||
public int getPort();
|
||||
|
||||
/**
|
||||
* @return The gateway path
|
||||
*/
|
||||
public String getPath();
|
||||
|
||||
/**
|
||||
* @return the targetId that uniquely identifies this client.
|
||||
*/
|
||||
public String getTargetId();
|
||||
|
||||
/**
|
||||
* <p>Connects to the gateway server, establishing the long poll communication
|
||||
* with the gateway server to be notified of server-side events.</p>
|
||||
* <p>The connect is performed in two steps:
|
||||
* <ul>
|
||||
* <li>first, a connect message is sent to the gateway server; the gateway server
|
||||
* will notice this is a first connect message and reply immediately with
|
||||
* an empty response</li>
|
||||
* <li>second, another connect message is sent to the gateway server which interprets
|
||||
* it as a long poll request</li>
|
||||
* </ul>
|
||||
* The long poll request may return either because one or more server-side events
|
||||
* happened, or because it expired. </p>
|
||||
* <p>Any connect message after the first is treated as a long poll request.</p>
|
||||
*
|
||||
* @throws IOException if it is not possible to connect to the gateway server
|
||||
* @see #disconnect()
|
||||
*/
|
||||
public void connect() throws IOException;
|
||||
|
||||
/**
|
||||
* <p>Disconnects from the gateway server.</p>
|
||||
* <p>Just after the disconnect request is processed by to the gateway server, it will
|
||||
* return the currently outstanding long poll request.</p>
|
||||
* <p>If this client is not connected, it does nothing</p>
|
||||
*
|
||||
* @throws IOException if it is not possible to contact the gateway server to disconnect
|
||||
* @see #connect()
|
||||
*/
|
||||
public void disconnect() throws IOException;
|
||||
|
||||
/**
|
||||
* <p>Sends a response to the gateway server.</p>
|
||||
*
|
||||
* @param response the response to send
|
||||
* @throws IOException if it is not possible to contact the gateway server
|
||||
*/
|
||||
public void deliver(RHTTPResponse response) throws IOException;
|
||||
|
||||
/**
|
||||
* <p>Adds the given listener to this client.</p>
|
||||
* @param listener the listener to add
|
||||
* @see #removeListener(RHTTPListener)
|
||||
*/
|
||||
public void addListener(RHTTPListener listener);
|
||||
|
||||
/**
|
||||
* <p>Removes the given listener from this client.</p>
|
||||
* @param listener the listener to remove
|
||||
* @see #addListener(RHTTPListener)
|
||||
*/
|
||||
public void removeListener(RHTTPListener listener);
|
||||
|
||||
/**
|
||||
* <p>Adds the given client listener to this client.</p>
|
||||
* @param listener the client listener to add
|
||||
* @see #removeClientListener(ClientListener)
|
||||
*/
|
||||
public void addClientListener(ClientListener listener);
|
||||
|
||||
/**
|
||||
* <p>Removes the given client listener from this client.</p>
|
||||
* @param listener the client listener to remove
|
||||
* @see #addClientListener(ClientListener)
|
||||
*/
|
||||
public void removeClientListener(ClientListener listener);
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
|
||||
// ------------------------------------------------------------------------
|
||||
// All rights reserved. This program and the accompanying materials
|
||||
// are made available under the terms of the Eclipse Public License v1.0
|
||||
// and Apache License v2.0 which accompanies this distribution.
|
||||
//
|
||||
// The Eclipse Public License is available at
|
||||
// http://www.eclipse.org/legal/epl-v10.html
|
||||
//
|
||||
// The Apache License v2.0 is available at
|
||||
// http://www.opensource.org/licenses/apache2.0.php
|
||||
//
|
||||
// You may elect to redistribute this code under either of these licenses.
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.rhttp.client;
|
||||
|
||||
/**
|
||||
* <p>Implementations of this class listen for requests arriving from the gateway server
|
||||
* and notified by {@link RHTTPClient}.</p>
|
||||
*
|
||||
* @version $Revision$ $Date$
|
||||
*/
|
||||
public interface RHTTPListener
|
||||
{
|
||||
/**
|
||||
* Callback method called by {@link RHTTPClient} to inform that the gateway server
|
||||
* sent a request to the gateway client.
|
||||
* @param request the request sent by the gateway server.
|
||||
* @throws Exception allowed to be thrown by implementations
|
||||
*/
|
||||
public void onRequest(RHTTPRequest request) throws Exception;
|
||||
}
|
|
@ -0,0 +1,266 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
|
||||
// ------------------------------------------------------------------------
|
||||
// All rights reserved. This program and the accompanying materials
|
||||
// are made available under the terms of the Eclipse Public License v1.0
|
||||
// and Apache License v2.0 which accompanies this distribution.
|
||||
//
|
||||
// The Eclipse Public License is available at
|
||||
// http://www.eclipse.org/legal/epl-v10.html
|
||||
//
|
||||
// The Apache License v2.0 is available at
|
||||
// http://www.opensource.org/licenses/apache2.0.php
|
||||
//
|
||||
// You may elect to redistribute this code under either of these licenses.
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.rhttp.client;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.eclipse.jetty.http.HttpParser;
|
||||
import org.eclipse.jetty.io.Buffer;
|
||||
import org.eclipse.jetty.io.ByteArrayBuffer;
|
||||
|
||||
/**
|
||||
* <p>Represents the external request information that is carried over the comet protocol.</p>
|
||||
* <p>Instances of this class are converted into an opaque byte array of the form:</p>
|
||||
* <pre>
|
||||
* <request-id> SPACE <request-length> CRLF
|
||||
* <external-request>
|
||||
* </pre>
|
||||
* <p>The byte array form is carried as body of a normal HTTP response returned by the gateway server
|
||||
* to the gateway client.</p>
|
||||
* @see RHTTPResponse
|
||||
* @version $Revision$ $Date$
|
||||
*/
|
||||
public class RHTTPRequest
|
||||
{
|
||||
private static final String CRLF = "\r\n";
|
||||
private static final byte[] CRLF_BYTES = CRLF.getBytes();
|
||||
|
||||
private final int id;
|
||||
private final byte[] requestBytes;
|
||||
private final byte[] frameBytes;
|
||||
private volatile String method;
|
||||
private volatile String uri;
|
||||
private volatile Map<String, String> headers;
|
||||
private volatile byte[] body;
|
||||
|
||||
public static List<RHTTPRequest> fromFrameBytes(byte[] bytes)
|
||||
{
|
||||
List<RHTTPRequest> result = new ArrayList<RHTTPRequest>();
|
||||
int start = 0;
|
||||
while (start < bytes.length)
|
||||
{
|
||||
// Scan until we find the space
|
||||
int end = start;
|
||||
while (bytes[end] != ' ') ++end;
|
||||
int requestId = Integer.parseInt(new String(bytes, start, end - start));
|
||||
start = end + 1;
|
||||
|
||||
// Scan until end of line
|
||||
while (bytes[end] != '\n') ++end;
|
||||
int length = Integer.parseInt(new String(bytes, start, end - start - 1));
|
||||
start = end + 1;
|
||||
|
||||
byte[] requestBytes = new byte[length];
|
||||
System.arraycopy(bytes, start, requestBytes, 0, length);
|
||||
RHTTPRequest request = fromRequestBytes(requestId, requestBytes);
|
||||
result.add(request);
|
||||
start += length;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public static RHTTPRequest fromRequestBytes(int requestId, byte[] requestBytes)
|
||||
{
|
||||
return new RHTTPRequest(requestId, requestBytes);
|
||||
}
|
||||
|
||||
public RHTTPRequest(int id, String method, String uri, Map<String, String> headers, byte[] body)
|
||||
{
|
||||
this.id = id;
|
||||
this.method = method;
|
||||
this.uri = uri;
|
||||
this.headers = headers;
|
||||
this.body = body;
|
||||
this.requestBytes = toRequestBytes();
|
||||
this.frameBytes = toFrameBytes(requestBytes);
|
||||
}
|
||||
|
||||
private RHTTPRequest(int id, byte[] requestBytes)
|
||||
{
|
||||
this.id = id;
|
||||
this.requestBytes = requestBytes;
|
||||
this.frameBytes = toFrameBytes(requestBytes);
|
||||
// Other fields are lazily initialized
|
||||
}
|
||||
|
||||
private void initialize()
|
||||
{
|
||||
try
|
||||
{
|
||||
final ByteArrayOutputStream body = new ByteArrayOutputStream();
|
||||
HttpParser parser = new HttpParser(new ByteArrayBuffer(requestBytes), new HttpParser.EventHandler()
|
||||
{
|
||||
@Override
|
||||
public void startRequest(Buffer method, Buffer uri, Buffer httpVersion) throws IOException
|
||||
{
|
||||
RHTTPRequest.this.method = method.toString("UTF-8");
|
||||
RHTTPRequest.this.uri = uri.toString("UTF-8");
|
||||
RHTTPRequest.this.headers = new LinkedHashMap<String, String>();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startResponse(Buffer httpVersion, int statusCode, Buffer statusMessage) throws IOException
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void parsedHeader(Buffer name, Buffer value) throws IOException
|
||||
{
|
||||
RHTTPRequest.this.headers.put(name.toString("UTF-8"), value.toString("UTF-8"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void content(Buffer content) throws IOException
|
||||
{
|
||||
content.writeTo(body);
|
||||
}
|
||||
});
|
||||
parser.parse();
|
||||
this.body = body.toByteArray();
|
||||
}
|
||||
catch (IOException x)
|
||||
{
|
||||
// Cannot happen: we're parsing from a byte[], not from an I/O stream
|
||||
throw new AssertionError(x);
|
||||
}
|
||||
}
|
||||
|
||||
public int getId()
|
||||
{
|
||||
return id;
|
||||
}
|
||||
|
||||
public byte[] getRequestBytes()
|
||||
{
|
||||
return requestBytes;
|
||||
}
|
||||
|
||||
public byte[] getFrameBytes()
|
||||
{
|
||||
return frameBytes;
|
||||
}
|
||||
|
||||
public String getMethod()
|
||||
{
|
||||
if (method == null)
|
||||
initialize();
|
||||
return method;
|
||||
}
|
||||
|
||||
public String getURI()
|
||||
{
|
||||
if (uri == null)
|
||||
initialize();
|
||||
return uri;
|
||||
}
|
||||
|
||||
public Map<String, String> getHeaders()
|
||||
{
|
||||
if (headers == null)
|
||||
initialize();
|
||||
return headers;
|
||||
}
|
||||
|
||||
public byte[] getBody()
|
||||
{
|
||||
if (body == null)
|
||||
initialize();
|
||||
return body;
|
||||
}
|
||||
|
||||
private byte[] toRequestBytes()
|
||||
{
|
||||
try
|
||||
{
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||
bytes.write(method.getBytes("UTF-8"));
|
||||
bytes.write(' ');
|
||||
bytes.write(uri.getBytes("UTF-8"));
|
||||
bytes.write(' ');
|
||||
bytes.write("HTTP/1.1".getBytes("UTF-8"));
|
||||
bytes.write(CRLF_BYTES);
|
||||
for (Map.Entry<String, String> entry : headers.entrySet())
|
||||
{
|
||||
bytes.write(entry.getKey().getBytes("UTF-8"));
|
||||
bytes.write(':');
|
||||
bytes.write(' ');
|
||||
bytes.write(entry.getValue().getBytes("UTF-8"));
|
||||
bytes.write(CRLF_BYTES);
|
||||
}
|
||||
bytes.write(CRLF_BYTES);
|
||||
bytes.write(body);
|
||||
bytes.close();
|
||||
return bytes.toByteArray();
|
||||
}
|
||||
catch (IOException x)
|
||||
{
|
||||
throw new AssertionError(x);
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] toFrameBytes(byte[] requestBytes)
|
||||
{
|
||||
try
|
||||
{
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||
bytes.write(String.valueOf(id).getBytes("UTF-8"));
|
||||
bytes.write(' ');
|
||||
bytes.write(String.valueOf(requestBytes.length).getBytes("UTF-8"));
|
||||
bytes.write(CRLF_BYTES);
|
||||
bytes.write(requestBytes);
|
||||
bytes.close();
|
||||
return bytes.toByteArray();
|
||||
}
|
||||
catch (IOException x)
|
||||
{
|
||||
throw new AssertionError(x);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
// Use fields to avoid initialization
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.append(id).append(" ");
|
||||
builder.append(method).append(" ");
|
||||
builder.append(uri).append(" ");
|
||||
builder.append(requestBytes.length).append("/");
|
||||
builder.append(frameBytes.length);
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
public String toLongString()
|
||||
{
|
||||
// Use getters to trigger initialization
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.append(id).append(" ");
|
||||
builder.append(getMethod()).append(" ");
|
||||
builder.append(getURI()).append(CRLF);
|
||||
for (Map.Entry<String, String> header : getHeaders().entrySet())
|
||||
builder.append(header.getKey()).append(": ").append(header.getValue()).append(CRLF);
|
||||
builder.append(getBody().length).append(" body bytes").append(CRLF);
|
||||
return builder.toString();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,256 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
|
||||
// ------------------------------------------------------------------------
|
||||
// All rights reserved. This program and the accompanying materials
|
||||
// are made available under the terms of the Eclipse Public License v1.0
|
||||
// and Apache License v2.0 which accompanies this distribution.
|
||||
//
|
||||
// The Eclipse Public License is available at
|
||||
// http://www.eclipse.org/legal/epl-v10.html
|
||||
//
|
||||
// The Apache License v2.0 is available at
|
||||
// http://www.opensource.org/licenses/apache2.0.php
|
||||
//
|
||||
// You may elect to redistribute this code under either of these licenses.
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.rhttp.client;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.eclipse.jetty.http.HttpParser;
|
||||
import org.eclipse.jetty.io.Buffer;
|
||||
import org.eclipse.jetty.io.ByteArrayBuffer;
|
||||
|
||||
/**
|
||||
* <p>Represents the resource provider response information that is carried over the comet protocol.</p>
|
||||
* <p>Instances of this class are converted into an opaque byte array of the form:</p>
|
||||
* <pre>
|
||||
* <request-id> SPACE <response-length> CRLF
|
||||
* <resource-response>
|
||||
* </pre>
|
||||
* <p>The byte array form is carried as body of a normal HTTP request made by the gateway client to
|
||||
* the gateway server.</p>
|
||||
* @see RHTTPRequest
|
||||
* @version $Revision$ $Date$
|
||||
*/
|
||||
public class RHTTPResponse
|
||||
{
|
||||
private static final String CRLF = "\r\n";
|
||||
private static final byte[] CRLF_BYTES = CRLF.getBytes();
|
||||
|
||||
private final int id;
|
||||
private final byte[] responseBytes;
|
||||
private final byte[] frameBytes;
|
||||
private volatile int code;
|
||||
private volatile String message;
|
||||
private volatile Map<String, String> headers;
|
||||
private volatile byte[] body;
|
||||
|
||||
public static RHTTPResponse fromFrameBytes(byte[] bytes)
|
||||
{
|
||||
int start = 0;
|
||||
// Scan until we find the space
|
||||
int end = start;
|
||||
while (bytes[end] != ' ') ++end;
|
||||
int responseId = Integer.parseInt(new String(bytes, start, end - start));
|
||||
start = end + 1;
|
||||
|
||||
// Scan until end of line
|
||||
while (bytes[end] != '\n') ++end;
|
||||
int length = Integer.parseInt(new String(bytes, start, end - start - 1));
|
||||
start = end + 1;
|
||||
|
||||
byte[] responseBytes = new byte[length];
|
||||
System.arraycopy(bytes, start, responseBytes, 0, length);
|
||||
return fromResponseBytes(responseId, responseBytes);
|
||||
}
|
||||
|
||||
public static RHTTPResponse fromResponseBytes(int id, byte[] responseBytes)
|
||||
{
|
||||
return new RHTTPResponse(id, responseBytes);
|
||||
}
|
||||
|
||||
public RHTTPResponse(int id, int code, String message, Map<String, String> headers, byte[] body)
|
||||
{
|
||||
this.id = id;
|
||||
this.code = code;
|
||||
this.message = message;
|
||||
this.headers = headers;
|
||||
this.body = body;
|
||||
this.responseBytes = toResponseBytes();
|
||||
this.frameBytes = toFrameBytes(responseBytes);
|
||||
}
|
||||
|
||||
private RHTTPResponse(int id, byte[] responseBytes)
|
||||
{
|
||||
this.id = id;
|
||||
this.responseBytes = responseBytes;
|
||||
this.frameBytes = toFrameBytes(responseBytes);
|
||||
// Other fields are lazily initialized
|
||||
}
|
||||
|
||||
private void initialize()
|
||||
{
|
||||
try
|
||||
{
|
||||
final ByteArrayOutputStream body = new ByteArrayOutputStream();
|
||||
HttpParser parser = new HttpParser(new ByteArrayBuffer(responseBytes), new HttpParser.EventHandler()
|
||||
{
|
||||
@Override
|
||||
public void startRequest(Buffer method, Buffer uri, Buffer httpVersion) throws IOException
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startResponse(Buffer httpVersion, int statusCode, Buffer statusMessage) throws IOException
|
||||
{
|
||||
RHTTPResponse.this.code = statusCode;
|
||||
RHTTPResponse.this.message = statusMessage.toString("UTF-8");
|
||||
RHTTPResponse.this.headers = new LinkedHashMap<String, String>();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void parsedHeader(Buffer name, Buffer value) throws IOException
|
||||
{
|
||||
RHTTPResponse.this.headers.put(name.toString("UTF-8"), value.toString("UTF-8"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void content(Buffer content) throws IOException
|
||||
{
|
||||
content.writeTo(body);
|
||||
}
|
||||
});
|
||||
parser.parse();
|
||||
this.body = body.toByteArray();
|
||||
}
|
||||
catch (IOException x)
|
||||
{
|
||||
// Cannot happen: we're parsing from a byte[], not from an I/O stream
|
||||
throw new AssertionError(x);
|
||||
}
|
||||
}
|
||||
|
||||
public int getId()
|
||||
{
|
||||
return id;
|
||||
}
|
||||
|
||||
public byte[] getResponseBytes()
|
||||
{
|
||||
return responseBytes;
|
||||
}
|
||||
|
||||
public byte[] getFrameBytes()
|
||||
{
|
||||
return frameBytes;
|
||||
}
|
||||
|
||||
public int getStatusCode()
|
||||
{
|
||||
if (code == 0)
|
||||
initialize();
|
||||
return code;
|
||||
}
|
||||
|
||||
public String getStatusMessage()
|
||||
{
|
||||
if (message == null)
|
||||
initialize();
|
||||
return message;
|
||||
}
|
||||
|
||||
public Map<String, String> getHeaders()
|
||||
{
|
||||
if (headers == null)
|
||||
initialize();
|
||||
return headers;
|
||||
}
|
||||
|
||||
public byte[] getBody()
|
||||
{
|
||||
if (body == null)
|
||||
initialize();
|
||||
return body;
|
||||
}
|
||||
|
||||
private byte[] toResponseBytes()
|
||||
{
|
||||
try
|
||||
{
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||
bytes.write("HTTP/1.1".getBytes("UTF-8"));
|
||||
bytes.write(' ');
|
||||
bytes.write(String.valueOf(code).getBytes("UTF-8"));
|
||||
bytes.write(' ');
|
||||
bytes.write(message.getBytes("UTF-8"));
|
||||
bytes.write(CRLF_BYTES);
|
||||
for (Map.Entry<String, String> entry : headers.entrySet())
|
||||
{
|
||||
bytes.write(entry.getKey().getBytes("UTF-8"));
|
||||
bytes.write(':');
|
||||
bytes.write(' ');
|
||||
bytes.write(entry.getValue().getBytes("UTF-8"));
|
||||
bytes.write(CRLF_BYTES);
|
||||
}
|
||||
bytes.write(CRLF_BYTES);
|
||||
bytes.write(body);
|
||||
bytes.close();
|
||||
return bytes.toByteArray();
|
||||
}
|
||||
catch (IOException x)
|
||||
{
|
||||
throw new AssertionError(x);
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] toFrameBytes(byte[] responseBytes)
|
||||
{
|
||||
try
|
||||
{
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||
bytes.write(String.valueOf(id).getBytes("UTF-8"));
|
||||
bytes.write(' ');
|
||||
bytes.write(String.valueOf(responseBytes.length).getBytes("UTF-8"));
|
||||
bytes.write(CRLF_BYTES);
|
||||
bytes.write(responseBytes);
|
||||
return bytes.toByteArray();
|
||||
}
|
||||
catch (IOException x)
|
||||
{
|
||||
throw new AssertionError(x);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
// Use fields to avoid initialization
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.append(id).append(" ");
|
||||
builder.append(code).append(" ");
|
||||
builder.append(message).append(" ");
|
||||
builder.append(responseBytes.length).append("/");
|
||||
builder.append(frameBytes.length);
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
public String toLongString()
|
||||
{
|
||||
// Use getters to trigger initialization
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.append(id).append(" ");
|
||||
builder.append(getStatusCode()).append(" ");
|
||||
builder.append(getStatusMessage()).append(CRLF);
|
||||
for (Map.Entry<String, String> header : getHeaders().entrySet())
|
||||
builder.append(header.getKey()).append(": ").append(header.getValue()).append(CRLF);
|
||||
builder.append(getBody().length).append(" body bytes").append(CRLF);
|
||||
return builder.toString();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,112 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
|
||||
// ------------------------------------------------------------------------
|
||||
// All rights reserved. This program and the accompanying materials
|
||||
// are made available under the terms of the Eclipse Public License v1.0
|
||||
// and Apache License v2.0 which accompanies this distribution.
|
||||
//
|
||||
// The Eclipse Public License is available at
|
||||
// http://www.eclipse.org/legal/epl-v10.html
|
||||
//
|
||||
// The Apache License v2.0 is available at
|
||||
// http://www.opensource.org/licenses/apache2.0.php
|
||||
//
|
||||
// You may elect to redistribute this code under either of these licenses.
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.rhttp.client;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.apache.http.client.HttpClient;
|
||||
|
||||
/**
|
||||
* @version $Revision$ $Date$
|
||||
*/
|
||||
public class RetryingApacheClient extends ApacheClient
|
||||
{
|
||||
public RetryingApacheClient(HttpClient httpClient, String gatewayURI, String targetId)
|
||||
{
|
||||
super(httpClient, gatewayURI, targetId);
|
||||
addClientListener(new RetryClientListener());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void syncHandshake() throws IOException
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
try
|
||||
{
|
||||
super.syncHandshake();
|
||||
break;
|
||||
}
|
||||
catch (IOException x)
|
||||
{
|
||||
getLogger().debug("Handshake failed, backing off and retrying");
|
||||
try
|
||||
{
|
||||
Thread.sleep(1000);
|
||||
}
|
||||
catch (InterruptedException xx)
|
||||
{
|
||||
throw (IOException)new IOException().initCause(xx);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class RetryClientListener implements ClientListener
|
||||
{
|
||||
public void connectRequired()
|
||||
{
|
||||
getLogger().debug("Connect requested by server");
|
||||
try
|
||||
{
|
||||
connect();
|
||||
}
|
||||
catch (IOException x)
|
||||
{
|
||||
// The connect() method is retried, so if it fails, it's a hard failure
|
||||
getLogger().debug("Connect failed after server required connect, giving up");
|
||||
}
|
||||
}
|
||||
|
||||
public void connectClosed()
|
||||
{
|
||||
connectException();
|
||||
}
|
||||
|
||||
public void connectException()
|
||||
{
|
||||
getLogger().debug("Connect failed, backing off and retrying");
|
||||
try
|
||||
{
|
||||
Thread.sleep(1000);
|
||||
asyncConnect();
|
||||
}
|
||||
catch (InterruptedException x)
|
||||
{
|
||||
// Ignore and stop retrying
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
public void deliverException(RHTTPResponse response)
|
||||
{
|
||||
getLogger().debug("Deliver failed, backing off and retrying");
|
||||
try
|
||||
{
|
||||
Thread.sleep(1000);
|
||||
asyncDeliver(response);
|
||||
}
|
||||
catch (InterruptedException x)
|
||||
{
|
||||
// Ignore and stop retrying
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
|
||||
// ------------------------------------------------------------------------
|
||||
// All rights reserved. This program and the accompanying materials
|
||||
// are made available under the terms of the Eclipse Public License v1.0
|
||||
// and Apache License v2.0 which accompanies this distribution.
|
||||
//
|
||||
// The Eclipse Public License is available at
|
||||
// http://www.eclipse.org/legal/epl-v10.html
|
||||
//
|
||||
// The Apache License v2.0 is available at
|
||||
// http://www.opensource.org/licenses/apache2.0.php
|
||||
//
|
||||
// You may elect to redistribute this code under either of these licenses.
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.rhttp.client;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.apache.http.HttpHost;
|
||||
import org.apache.http.client.HttpRequestRetryHandler;
|
||||
import org.apache.http.conn.ClientConnectionManager;
|
||||
import org.apache.http.conn.scheme.PlainSocketFactory;
|
||||
import org.apache.http.conn.scheme.Scheme;
|
||||
import org.apache.http.conn.scheme.SchemeRegistry;
|
||||
import org.apache.http.impl.client.DefaultHttpClient;
|
||||
import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
|
||||
import org.apache.http.params.BasicHttpParams;
|
||||
import org.apache.http.params.HttpParams;
|
||||
import org.apache.http.protocol.HttpContext;
|
||||
import org.eclipse.jetty.rhttp.client.ApacheClient;
|
||||
import org.eclipse.jetty.rhttp.client.RHTTPClient;
|
||||
import org.eclipse.jetty.util.log.Log;
|
||||
import org.eclipse.jetty.util.log.StdErrLog;
|
||||
|
||||
|
||||
/**
|
||||
* @version $Revision$ $Date$
|
||||
*/
|
||||
public class ApacheClientTest extends ClientTest
|
||||
{
|
||||
{
|
||||
((StdErrLog)Log.getLog()).setHideStacks(!Log.getLog().isDebugEnabled());
|
||||
}
|
||||
|
||||
private ClientConnectionManager connectionManager;
|
||||
|
||||
protected RHTTPClient createClient(int port, String targetId) throws Exception
|
||||
{
|
||||
SchemeRegistry schemeRegistry = new SchemeRegistry();
|
||||
schemeRegistry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), port));
|
||||
connectionManager = new ThreadSafeClientConnManager(new BasicHttpParams(), schemeRegistry);
|
||||
HttpParams httpParams = new BasicHttpParams();
|
||||
httpParams.setParameter("http.default-host", new HttpHost("localhost", port));
|
||||
DefaultHttpClient httpClient = new DefaultHttpClient(connectionManager, httpParams);
|
||||
httpClient.setHttpRequestRetryHandler(new NoRetryHandler());
|
||||
return new ApacheClient(httpClient, "", targetId);
|
||||
}
|
||||
|
||||
protected void destroyClient(RHTTPClient client) throws Exception
|
||||
{
|
||||
connectionManager.shutdown();
|
||||
}
|
||||
|
||||
private class NoRetryHandler implements HttpRequestRetryHandler
|
||||
{
|
||||
public boolean retryRequest(IOException x, int failedAttempts, HttpContext httpContext)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,299 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
|
||||
// ------------------------------------------------------------------------
|
||||
// All rights reserved. This program and the accompanying materials
|
||||
// are made available under the terms of the Eclipse Public License v1.0
|
||||
// and Apache License v2.0 which accompanies this distribution.
|
||||
//
|
||||
// The Eclipse Public License is available at
|
||||
// http://www.eclipse.org/legal/epl-v10.html
|
||||
//
|
||||
// The Apache License v2.0 is available at
|
||||
// http://www.opensource.org/licenses/apache2.0.php
|
||||
//
|
||||
// You may elect to redistribute this code under either of these licenses.
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.rhttp.client;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import junit.framework.TestCase;
|
||||
|
||||
import org.eclipse.jetty.rhttp.client.ClientListener;
|
||||
import org.eclipse.jetty.rhttp.client.RHTTPClient;
|
||||
import org.eclipse.jetty.rhttp.client.RHTTPResponse;
|
||||
import org.eclipse.jetty.server.Connector;
|
||||
import org.eclipse.jetty.server.Request;
|
||||
import org.eclipse.jetty.server.Server;
|
||||
import org.eclipse.jetty.server.bio.SocketConnector;
|
||||
import org.eclipse.jetty.server.handler.AbstractHandler;
|
||||
import org.eclipse.jetty.server.nio.SelectChannelConnector;
|
||||
import org.eclipse.jetty.util.log.Log;
|
||||
|
||||
|
||||
/**
|
||||
* @version $Revision$ $Date$
|
||||
*/
|
||||
public abstract class ClientTest extends TestCase
|
||||
{
|
||||
protected abstract RHTTPClient createClient(int port, String targetId) throws Exception;
|
||||
|
||||
protected abstract void destroyClient(RHTTPClient client) throws Exception;
|
||||
|
||||
public void testConnectNoServer() throws Exception
|
||||
{
|
||||
RHTTPClient client = createClient(8080, "test1");
|
||||
try
|
||||
{
|
||||
client.connect();
|
||||
fail();
|
||||
}
|
||||
catch (IOException x)
|
||||
{
|
||||
}
|
||||
finally
|
||||
{
|
||||
destroyClient(client);
|
||||
}
|
||||
}
|
||||
|
||||
public void testServerExceptionOnHandshake() throws Exception
|
||||
{
|
||||
final CountDownLatch serverLatch = new CountDownLatch(1);
|
||||
|
||||
Server server = new Server();
|
||||
Connector connector = new SelectChannelConnector();
|
||||
server.addConnector(connector);
|
||||
server.setHandler(new AbstractHandler()
|
||||
{
|
||||
public void handle(String target, Request request, HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws IOException, ServletException
|
||||
{
|
||||
request.setHandled(true);
|
||||
if (target.endsWith("/handshake"))
|
||||
{
|
||||
serverLatch.countDown();
|
||||
throw new TestException();
|
||||
}
|
||||
}
|
||||
});
|
||||
server.start();
|
||||
try
|
||||
{
|
||||
RHTTPClient client = createClient(connector.getLocalPort(), "test2");
|
||||
try
|
||||
{
|
||||
try
|
||||
{
|
||||
client.connect();
|
||||
fail();
|
||||
}
|
||||
catch (IOException x)
|
||||
{
|
||||
}
|
||||
|
||||
assertTrue(serverLatch.await(1000, TimeUnit.MILLISECONDS));
|
||||
}
|
||||
finally
|
||||
{
|
||||
destroyClient(client);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
server.stop();
|
||||
}
|
||||
}
|
||||
|
||||
public void testServerExceptionOnConnect() throws Exception
|
||||
{
|
||||
final CountDownLatch serverLatch = new CountDownLatch(1);
|
||||
|
||||
Server server = new Server();
|
||||
Connector connector = new SelectChannelConnector();
|
||||
server.addConnector(connector);
|
||||
server.setHandler(new AbstractHandler()
|
||||
{
|
||||
public void handle(String target, Request request, HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws IOException, ServletException
|
||||
{
|
||||
request.setHandled(true);
|
||||
if (target.endsWith("/connect"))
|
||||
{
|
||||
serverLatch.countDown();
|
||||
throw new TestException();
|
||||
}
|
||||
}
|
||||
});
|
||||
server.start();
|
||||
try
|
||||
{
|
||||
RHTTPClient client = createClient(connector.getLocalPort(), "test3");
|
||||
try
|
||||
{
|
||||
final CountDownLatch connectLatch = new CountDownLatch(1);
|
||||
client.addClientListener(new ClientListener.Adapter()
|
||||
{
|
||||
@Override
|
||||
public void connectException()
|
||||
{
|
||||
connectLatch.countDown();
|
||||
}
|
||||
});
|
||||
client.connect();
|
||||
|
||||
assertTrue(serverLatch.await(1000, TimeUnit.MILLISECONDS));
|
||||
assertTrue(connectLatch.await(1000, TimeUnit.MILLISECONDS));
|
||||
}
|
||||
finally
|
||||
{
|
||||
destroyClient(client);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
server.stop();
|
||||
}
|
||||
}
|
||||
|
||||
public void testServerExceptionOnDeliver() throws Exception
|
||||
{
|
||||
final CountDownLatch serverLatch = new CountDownLatch(1);
|
||||
|
||||
Server server = new Server();
|
||||
Connector connector = new SelectChannelConnector();
|
||||
server.addConnector(connector);
|
||||
server.setHandler(new AbstractHandler()
|
||||
{
|
||||
public void handle(String target, Request request, HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws IOException, ServletException
|
||||
{
|
||||
request.setHandled(true);
|
||||
if (target.endsWith("/connect"))
|
||||
{
|
||||
serverLatch.countDown();
|
||||
try
|
||||
{
|
||||
// Simulate a long poll timeout
|
||||
Thread.sleep(10000);
|
||||
}
|
||||
catch (InterruptedException x)
|
||||
{
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
else if (target.endsWith("/deliver"))
|
||||
{
|
||||
// Throw an exception on deliver
|
||||
throw new TestException();
|
||||
}
|
||||
}
|
||||
});
|
||||
server.start();
|
||||
try
|
||||
{
|
||||
RHTTPClient client = createClient(connector.getLocalPort(), "test4");
|
||||
try
|
||||
{
|
||||
final CountDownLatch deliverLatch = new CountDownLatch(1);
|
||||
client.addClientListener(new ClientListener.Adapter()
|
||||
{
|
||||
@Override
|
||||
public void deliverException(RHTTPResponse response)
|
||||
{
|
||||
deliverLatch.countDown();
|
||||
}
|
||||
});
|
||||
client.connect();
|
||||
|
||||
assertTrue(serverLatch.await(1000, TimeUnit.MILLISECONDS));
|
||||
|
||||
client.deliver(new RHTTPResponse(1, 200, "OK", new LinkedHashMap<String, String>(), new byte[0]));
|
||||
|
||||
assertTrue(deliverLatch.await(1000, TimeUnit.MILLISECONDS));
|
||||
}
|
||||
finally
|
||||
{
|
||||
destroyClient(client);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
server.stop();
|
||||
}
|
||||
}
|
||||
|
||||
public void testServerShutdownAfterConnect() throws Exception
|
||||
{
|
||||
final CountDownLatch connectLatch = new CountDownLatch(1);
|
||||
final CountDownLatch stopLatch = new CountDownLatch(1);
|
||||
|
||||
Server server = new Server();
|
||||
Connector connector = new SocketConnector();
|
||||
server.addConnector(connector);
|
||||
server.setHandler(new AbstractHandler()
|
||||
{
|
||||
public void handle(String target, Request request, HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws IOException, ServletException
|
||||
{
|
||||
request.setHandled(true);
|
||||
if (target.endsWith("/connect"))
|
||||
{
|
||||
connectLatch.countDown();
|
||||
try
|
||||
{
|
||||
Thread.sleep(10000);
|
||||
}
|
||||
catch (InterruptedException e)
|
||||
{
|
||||
stopLatch.countDown();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
server.start();
|
||||
try
|
||||
{
|
||||
RHTTPClient client = createClient(connector.getLocalPort(), "test5");
|
||||
try
|
||||
{
|
||||
final CountDownLatch serverLatch = new CountDownLatch(1);
|
||||
client.addClientListener(new ClientListener.Adapter()
|
||||
{
|
||||
@Override
|
||||
public void connectClosed()
|
||||
{
|
||||
serverLatch.countDown();
|
||||
}
|
||||
});
|
||||
client.connect();
|
||||
|
||||
assertTrue(connectLatch.await(2000, TimeUnit.MILLISECONDS));
|
||||
|
||||
server.stop();
|
||||
assertTrue(stopLatch.await(2000, TimeUnit.MILLISECONDS));
|
||||
|
||||
assertTrue(serverLatch.await(2000, TimeUnit.MILLISECONDS));
|
||||
}
|
||||
finally
|
||||
{
|
||||
destroyClient(client);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
server.stop();
|
||||
}
|
||||
}
|
||||
|
||||
public static class TestException extends NullPointerException
|
||||
{
|
||||
|
||||
}
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
|
||||
// ------------------------------------------------------------------------
|
||||
// All rights reserved. This program and the accompanying materials
|
||||
// are made available under the terms of the Eclipse Public License v1.0
|
||||
// and Apache License v2.0 which accompanies this distribution.
|
||||
//
|
||||
// The Eclipse Public License is available at
|
||||
// http://www.eclipse.org/legal/epl-v10.html
|
||||
//
|
||||
// The Apache License v2.0 is available at
|
||||
// http://www.opensource.org/licenses/apache2.0.php
|
||||
//
|
||||
// You may elect to redistribute this code under either of these licenses.
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.rhttp.client;
|
||||
|
||||
import org.eclipse.jetty.client.Address;
|
||||
import org.eclipse.jetty.client.HttpClient;
|
||||
import org.eclipse.jetty.rhttp.client.JettyClient;
|
||||
import org.eclipse.jetty.rhttp.client.RHTTPClient;
|
||||
import org.eclipse.jetty.util.log.Log;
|
||||
import org.eclipse.jetty.util.log.StdErrLog;
|
||||
|
||||
|
||||
/**
|
||||
* @version $Revision$ $Date$
|
||||
*/
|
||||
public class JettyClientTest extends ClientTest
|
||||
{
|
||||
{
|
||||
((StdErrLog)Log.getLog()).setHideStacks(!Log.getLog().isDebugEnabled());
|
||||
}
|
||||
|
||||
private HttpClient httpClient;
|
||||
|
||||
protected RHTTPClient createClient(int port, String targetId) throws Exception
|
||||
{
|
||||
((StdErrLog)Log.getLog()).setSource(true);
|
||||
httpClient = new HttpClient();
|
||||
httpClient.start();
|
||||
return new JettyClient(httpClient, new Address("localhost", port), "", targetId);
|
||||
}
|
||||
|
||||
protected void destroyClient(RHTTPClient client) throws Exception
|
||||
{
|
||||
httpClient.stop();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,85 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
|
||||
// ------------------------------------------------------------------------
|
||||
// All rights reserved. This program and the accompanying materials
|
||||
// are made available under the terms of the Eclipse Public License v1.0
|
||||
// and Apache License v2.0 which accompanies this distribution.
|
||||
//
|
||||
// The Eclipse Public License is available at
|
||||
// http://www.eclipse.org/legal/epl-v10.html
|
||||
//
|
||||
// The Apache License v2.0 is available at
|
||||
// http://www.opensource.org/licenses/apache2.0.php
|
||||
//
|
||||
// You may elect to redistribute this code under either of these licenses.
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.rhttp.client;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.eclipse.jetty.rhttp.client.RHTTPRequest;
|
||||
|
||||
import junit.framework.TestCase;
|
||||
|
||||
/**
|
||||
* @version $Revision$ $Date$
|
||||
*/
|
||||
public class RequestTest extends TestCase
|
||||
{
|
||||
public void testRequestConversions() throws Exception
|
||||
{
|
||||
int id = 1;
|
||||
String method = "GET";
|
||||
String uri = "/test";
|
||||
Map<String, String> headers = new LinkedHashMap<String, String>();
|
||||
headers.put("X", "X");
|
||||
headers.put("Y", "Y");
|
||||
headers.put("Z", "Z");
|
||||
byte[] body = "BODY".getBytes("UTF-8");
|
||||
headers.put("Content-Length", String.valueOf(body.length));
|
||||
RHTTPRequest request1 = new RHTTPRequest(id, method, uri, headers, body);
|
||||
byte[] requestBytes1 = request1.getRequestBytes();
|
||||
RHTTPRequest request2 = RHTTPRequest.fromRequestBytes(id, requestBytes1);
|
||||
assertEquals(id, request2.getId());
|
||||
assertEquals(method, request2.getMethod());
|
||||
assertEquals(uri, request2.getURI());
|
||||
assertEquals(headers, request2.getHeaders());
|
||||
assertTrue(Arrays.equals(request2.getBody(), body));
|
||||
|
||||
byte[] requestBytes2 = request2.getRequestBytes();
|
||||
assertTrue(Arrays.equals(requestBytes1, requestBytes2));
|
||||
}
|
||||
|
||||
public void testFrameConversions() throws Exception
|
||||
{
|
||||
int id = 1;
|
||||
String method = "GET";
|
||||
String uri = "/test";
|
||||
Map<String, String> headers = new LinkedHashMap<String, String>();
|
||||
headers.put("X", "X");
|
||||
headers.put("Y", "Y");
|
||||
headers.put("Z", "Z");
|
||||
byte[] body = "BODY".getBytes("UTF-8");
|
||||
headers.put("Content-Length", String.valueOf(body.length));
|
||||
RHTTPRequest request1 = new RHTTPRequest(id, method, uri, headers, body);
|
||||
byte[] frameBytes1 = request1.getFrameBytes();
|
||||
List<RHTTPRequest> requests = RHTTPRequest.fromFrameBytes(frameBytes1);
|
||||
assertNotNull(requests);
|
||||
assertEquals(1, requests.size());
|
||||
RHTTPRequest request2 = requests.get(0);
|
||||
assertEquals(id, request2.getId());
|
||||
assertEquals(method, request2.getMethod());
|
||||
assertEquals(uri, request2.getURI());
|
||||
assertEquals(headers, request2.getHeaders());
|
||||
assertTrue(Arrays.equals(request2.getBody(), body));
|
||||
|
||||
byte[] frameBytes2 = request2.getFrameBytes();
|
||||
assertTrue(Arrays.equals(frameBytes1, frameBytes2));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,85 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
|
||||
// ------------------------------------------------------------------------
|
||||
// All rights reserved. This program and the accompanying materials
|
||||
// are made available under the terms of the Eclipse Public License v1.0
|
||||
// and Apache License v2.0 which accompanies this distribution.
|
||||
//
|
||||
// The Eclipse Public License is available at
|
||||
// http://www.eclipse.org/legal/epl-v10.html
|
||||
//
|
||||
// The Apache License v2.0 is available at
|
||||
// http://www.opensource.org/licenses/apache2.0.php
|
||||
//
|
||||
// You may elect to redistribute this code under either of these licenses.
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.rhttp.client;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.eclipse.jetty.rhttp.client.RHTTPResponse;
|
||||
import org.eclipse.jetty.util.log.Log;
|
||||
import org.eclipse.jetty.util.log.StdErrLog;
|
||||
|
||||
import junit.framework.TestCase;
|
||||
|
||||
/**
|
||||
* @version $Revision$ $Date$
|
||||
*/
|
||||
public class ResponseTest extends TestCase
|
||||
{
|
||||
{
|
||||
((StdErrLog)Log.getLog()).setHideStacks(!Log.getLog().isDebugEnabled());
|
||||
}
|
||||
|
||||
public void testResponseConversions() throws Exception
|
||||
{
|
||||
int id = 1;
|
||||
int statusCode = 200;
|
||||
String statusMessage = "OK";
|
||||
Map<String, String> headers = new LinkedHashMap<String, String>();
|
||||
headers.put("X", "X");
|
||||
headers.put("Y", "Y");
|
||||
headers.put("Z", "Z");
|
||||
byte[] body = "BODY".getBytes("UTF-8");
|
||||
RHTTPResponse response1 = new RHTTPResponse(id, statusCode, statusMessage, headers, body);
|
||||
byte[] responseBytes1 = response1.getResponseBytes();
|
||||
RHTTPResponse response2 = RHTTPResponse.fromResponseBytes(id, responseBytes1);
|
||||
assertEquals(id, response2.getId());
|
||||
assertEquals(statusCode, response2.getStatusCode());
|
||||
assertEquals(statusMessage, response2.getStatusMessage());
|
||||
assertEquals(headers, response2.getHeaders());
|
||||
assertTrue(Arrays.equals(response2.getBody(), body));
|
||||
|
||||
byte[] responseBytes2 = response2.getResponseBytes();
|
||||
assertTrue(Arrays.equals(responseBytes1, responseBytes2));
|
||||
}
|
||||
|
||||
public void testFrameConversions() throws Exception
|
||||
{
|
||||
int id = 1;
|
||||
int statusCode = 200;
|
||||
String statusMessage = "OK";
|
||||
Map<String, String> headers = new LinkedHashMap<String, String>();
|
||||
headers.put("X", "X");
|
||||
headers.put("Y", "Y");
|
||||
headers.put("Z", "Z");
|
||||
byte[] body = "BODY".getBytes("UTF-8");
|
||||
RHTTPResponse response1 = new RHTTPResponse(id, statusCode, statusMessage, headers, body);
|
||||
byte[] frameBytes1 = response1.getFrameBytes();
|
||||
RHTTPResponse response2 = RHTTPResponse.fromFrameBytes(frameBytes1);
|
||||
assertEquals(id, response2.getId());
|
||||
assertEquals(statusCode, response2.getStatusCode());
|
||||
assertEquals(response2.getStatusMessage(), statusMessage);
|
||||
assertEquals(headers, response2.getHeaders());
|
||||
assertTrue(Arrays.equals(response2.getBody(), body));
|
||||
|
||||
byte[] frameBytes2 = response2.getFrameBytes();
|
||||
assertTrue(Arrays.equals(frameBytes1, frameBytes2));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<parent>
|
||||
<groupId>org.eclipse.jetty.rhttp</groupId>
|
||||
<artifactId>jetty-rhttp-project</artifactId>
|
||||
<version>9.0.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<artifactId>reverse-http-connector</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
<name>Jetty :: Reverse HTTP :: Connector</name>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.codehaus.mojo</groupId>
|
||||
<artifactId>exec-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<classpathScope>test</classpathScope>
|
||||
<mainClass>org.eclipse.jetty.rhttp.connector.TestReverseServer</mainClass>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty</groupId>
|
||||
<artifactId>reverse-http-client</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty</groupId>
|
||||
<artifactId>jetty-util</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty</groupId>
|
||||
<artifactId>jetty-io</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty</groupId>
|
||||
<artifactId>jetty-server</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty</groupId>
|
||||
<artifactId>example-jetty-embedded</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>javax.servlet</groupId>
|
||||
<artifactId>servlet-api</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
|
@ -0,0 +1,23 @@
|
|||
<?xml version="1.0"?>
|
||||
<!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure.dtd">
|
||||
|
||||
<!-- =============================================================== -->
|
||||
<!-- Configure the Jetty Reverse HTTP Connector -->
|
||||
<!-- =============================================================== -->
|
||||
<Configure id="Server" class="org.eclipse.jetty.server.Server">
|
||||
|
||||
<Call name="addConnector">
|
||||
<Arg>
|
||||
<New class="org.eclipse.jetty.rhttp.connector.ReverseHTTPConnector">
|
||||
<New class="org.eclipse.jetty.rhttp.client.JettyClient">
|
||||
<Arg>
|
||||
<New class="HttpClient">
|
||||
</New>
|
||||
</Arg>
|
||||
<Arg>http://localhost:8888/</Arg>
|
||||
<Arg>nodeA</Arg>
|
||||
</New>
|
||||
</New>
|
||||
</Arg>
|
||||
</Call>
|
||||
</Configure>
|
|
@ -0,0 +1,170 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
|
||||
// ------------------------------------------------------------------------
|
||||
// All rights reserved. This program and the accompanying materials
|
||||
// are made available under the terms of the Eclipse Public License v1.0
|
||||
// and Apache License v2.0 which accompanies this distribution.
|
||||
//
|
||||
// The Eclipse Public License is available at
|
||||
// http://www.eclipse.org/legal/epl-v10.html
|
||||
//
|
||||
// The Apache License v2.0 is available at
|
||||
// http://www.opensource.org/licenses/apache2.0.php
|
||||
//
|
||||
// You may elect to redistribute this code under either of these licenses.
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.rhttp.connector;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
|
||||
import org.eclipse.jetty.io.ByteArrayEndPoint;
|
||||
import org.eclipse.jetty.io.Connection;
|
||||
import org.eclipse.jetty.io.EndPoint;
|
||||
import org.eclipse.jetty.rhttp.client.RHTTPClient;
|
||||
import org.eclipse.jetty.rhttp.client.RHTTPListener;
|
||||
import org.eclipse.jetty.rhttp.client.RHTTPRequest;
|
||||
import org.eclipse.jetty.rhttp.client.RHTTPResponse;
|
||||
import org.eclipse.jetty.server.AbstractConnector;
|
||||
import org.eclipse.jetty.server.AbstractHttpConnection;
|
||||
import org.eclipse.jetty.server.BlockingHttpConnection;
|
||||
import org.eclipse.jetty.util.component.LifeCycle;
|
||||
import org.eclipse.jetty.util.log.Log;
|
||||
import org.eclipse.jetty.util.log.Logger;
|
||||
|
||||
/**
|
||||
* An implementation of a Jetty connector that uses a {@link RHTTPClient} connected
|
||||
* to a gateway server to receive requests, feed them to the Jetty server, and
|
||||
* forward responses from the Jetty server to the gateway server.
|
||||
*
|
||||
* @version $Revision$ $Date$
|
||||
*/
|
||||
public class ReverseHTTPConnector extends AbstractConnector implements RHTTPListener
|
||||
{
|
||||
private static final Logger LOG = Log.getLogger(ReverseHTTPConnector.class);
|
||||
|
||||
private final BlockingQueue<RHTTPRequest> requests = new LinkedBlockingQueue<RHTTPRequest>();
|
||||
private final RHTTPClient client;
|
||||
|
||||
public ReverseHTTPConnector(RHTTPClient client)
|
||||
{
|
||||
this.client = client;
|
||||
super.setHost(client.getHost());
|
||||
super.setPort(client.getPort());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setHost(String host)
|
||||
{
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPort(int port)
|
||||
{
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doStart() throws Exception
|
||||
{
|
||||
if (client instanceof LifeCycle)
|
||||
((LifeCycle)client).start();
|
||||
super.doStart();
|
||||
client.connect();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doStop() throws Exception
|
||||
{
|
||||
client.disconnect();
|
||||
super.doStop();
|
||||
if (client instanceof LifeCycle)
|
||||
((LifeCycle)client).stop();
|
||||
}
|
||||
|
||||
public void open()
|
||||
{
|
||||
client.addListener(this);
|
||||
}
|
||||
|
||||
public void close()
|
||||
{
|
||||
client.removeListener(this);
|
||||
}
|
||||
|
||||
public int getLocalPort()
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
public Object getConnection()
|
||||
{
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void accept(int acceptorId) throws IOException, InterruptedException
|
||||
{
|
||||
RHTTPRequest request = requests.take();
|
||||
IncomingRequest incomingRequest = new IncomingRequest(request);
|
||||
getThreadPool().dispatch(incomingRequest);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void persist(EndPoint endpoint) throws IOException
|
||||
{
|
||||
// Signals that the connection should not be closed
|
||||
// Do nothing in this case, as we run from memory
|
||||
}
|
||||
|
||||
public void onRequest(RHTTPRequest request) throws Exception
|
||||
{
|
||||
requests.add(request);
|
||||
}
|
||||
|
||||
private class IncomingRequest implements Runnable
|
||||
{
|
||||
private final RHTTPRequest request;
|
||||
|
||||
private IncomingRequest(RHTTPRequest request)
|
||||
{
|
||||
this.request = request;
|
||||
}
|
||||
|
||||
public void run()
|
||||
{
|
||||
byte[] requestBytes = request.getRequestBytes();
|
||||
|
||||
ByteArrayEndPoint endPoint = new ByteArrayEndPoint(requestBytes, 1024);
|
||||
endPoint.setGrowOutput(true);
|
||||
|
||||
AbstractHttpConnection connection = new BlockingHttpConnection(ReverseHTTPConnector.this, endPoint, getServer());
|
||||
|
||||
connectionOpened(connection);
|
||||
try
|
||||
{
|
||||
// Loop over the whole content, since handle() only
|
||||
// reads up to the connection buffer's capacities
|
||||
while (endPoint.getIn().length() > 0)
|
||||
connection.handle();
|
||||
|
||||
byte[] responseBytes = endPoint.getOut().asArray();
|
||||
RHTTPResponse response = RHTTPResponse.fromResponseBytes(request.getId(), responseBytes);
|
||||
client.deliver(response);
|
||||
}
|
||||
catch (Exception x)
|
||||
{
|
||||
LOG.debug(x);
|
||||
}
|
||||
finally
|
||||
{
|
||||
connectionClosed(connection);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,187 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
|
||||
// ------------------------------------------------------------------------
|
||||
// All rights reserved. This program and the accompanying materials
|
||||
// are made available under the terms of the Eclipse Public License v1.0
|
||||
// and Apache License v2.0 which accompanies this distribution.
|
||||
//
|
||||
// The Eclipse Public License is available at
|
||||
// http://www.eclipse.org/legal/epl-v10.html
|
||||
//
|
||||
// The Apache License v2.0 is available at
|
||||
// http://www.opensource.org/licenses/apache2.0.php
|
||||
//
|
||||
// You may elect to redistribute this code under either of these licenses.
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.rhttp.connector;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import junit.framework.TestCase;
|
||||
|
||||
import org.eclipse.jetty.rhttp.client.ClientListener;
|
||||
import org.eclipse.jetty.rhttp.client.RHTTPClient;
|
||||
import org.eclipse.jetty.rhttp.client.RHTTPListener;
|
||||
import org.eclipse.jetty.rhttp.client.RHTTPRequest;
|
||||
import org.eclipse.jetty.rhttp.client.RHTTPResponse;
|
||||
import org.eclipse.jetty.rhttp.connector.ReverseHTTPConnector;
|
||||
import org.eclipse.jetty.server.Server;
|
||||
import org.eclipse.jetty.server.handler.AbstractHandler;
|
||||
|
||||
/**
|
||||
* @version $Revision$ $Date$
|
||||
*/
|
||||
public class ReverseHTTPConnectorTest extends TestCase
|
||||
{
|
||||
public void testGatewayConnectorWithoutRequestBody() throws Exception
|
||||
{
|
||||
testGatewayConnector(false);
|
||||
}
|
||||
|
||||
public void testGatewayConnectorWithRequestBody() throws Exception
|
||||
{
|
||||
testGatewayConnector(true);
|
||||
}
|
||||
|
||||
private void testGatewayConnector(boolean withRequestBody) throws Exception
|
||||
{
|
||||
Server server = new Server();
|
||||
final CountDownLatch handlerLatch = new CountDownLatch(1);
|
||||
CountDownLatch clientLatch = new CountDownLatch(1);
|
||||
AtomicReference<RHTTPResponse> responseRef = new AtomicReference<RHTTPResponse>();
|
||||
ReverseHTTPConnector connector = new ReverseHTTPConnector(new TestClient(clientLatch, responseRef));
|
||||
server.addConnector(connector);
|
||||
final String method = "POST";
|
||||
final String uri = "/test";
|
||||
final byte[] requestBody = withRequestBody ? "REQUEST-BODY".getBytes("UTF-8") : new byte[0];
|
||||
final int statusCode = HttpServletResponse.SC_CREATED;
|
||||
final String headerName = "foo";
|
||||
final String headerValue = "bar";
|
||||
final byte[] responseBody = "RESPONSE-BODY".getBytes("UTF-8");
|
||||
server.setHandler(new AbstractHandler()
|
||||
{
|
||||
public void handle(String pathInfo, org.eclipse.jetty.server.Request request, HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws IOException, ServletException
|
||||
{
|
||||
assertEquals(method, httpRequest.getMethod());
|
||||
assertEquals(uri, httpRequest.getRequestURI());
|
||||
assertEquals(headerValue, httpRequest.getHeader(headerName));
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
InputStream input = httpRequest.getInputStream();
|
||||
int read;
|
||||
while ((read = input.read()) >= 0)
|
||||
baos.write(read);
|
||||
baos.close();
|
||||
assertTrue(Arrays.equals(requestBody, baos.toByteArray()));
|
||||
|
||||
httpResponse.setStatus(statusCode);
|
||||
httpResponse.setHeader(headerName, headerValue);
|
||||
OutputStream output = httpResponse.getOutputStream();
|
||||
output.write(responseBody);
|
||||
output.flush();
|
||||
request.setHandled(true);
|
||||
handlerLatch.countDown();
|
||||
}
|
||||
});
|
||||
server.start();
|
||||
|
||||
HashMap<String, String> headers = new HashMap<String, String>();
|
||||
headers.put("Host", "localhost");
|
||||
headers.put(headerName, headerValue);
|
||||
headers.put("Content-Length", String.valueOf(requestBody.length));
|
||||
RHTTPRequest request = new RHTTPRequest(1, method, uri, headers, requestBody);
|
||||
request = RHTTPRequest.fromRequestBytes(request.getId(), request.getRequestBytes());
|
||||
connector.onRequest(request);
|
||||
|
||||
assertTrue(handlerLatch.await(1000, TimeUnit.MILLISECONDS));
|
||||
assertTrue(clientLatch.await(1000, TimeUnit.MILLISECONDS));
|
||||
RHTTPResponse response = responseRef.get();
|
||||
assertEquals(request.getId(), response.getId());
|
||||
assertEquals(statusCode, response.getStatusCode());
|
||||
assertEquals(headerValue, response.getHeaders().get(headerName));
|
||||
assertTrue(Arrays.equals(response.getBody(), responseBody));
|
||||
}
|
||||
|
||||
private class TestClient implements RHTTPClient
|
||||
{
|
||||
private final CountDownLatch latch;
|
||||
private final AtomicReference<RHTTPResponse> responseRef;
|
||||
|
||||
private TestClient(CountDownLatch latch, AtomicReference<RHTTPResponse> response)
|
||||
{
|
||||
this.latch = latch;
|
||||
this.responseRef = response;
|
||||
}
|
||||
|
||||
public String getTargetId()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public void connect() throws IOException
|
||||
{
|
||||
}
|
||||
|
||||
public void disconnect() throws IOException
|
||||
{
|
||||
}
|
||||
|
||||
public void deliver(RHTTPResponse response) throws IOException
|
||||
{
|
||||
responseRef.set(response);
|
||||
latch.countDown();
|
||||
}
|
||||
|
||||
public void addListener(RHTTPListener listener)
|
||||
{
|
||||
}
|
||||
|
||||
public void removeListener(RHTTPListener listener)
|
||||
{
|
||||
}
|
||||
|
||||
public void addClientListener(ClientListener listener)
|
||||
{
|
||||
}
|
||||
|
||||
public void removeClientListener(ClientListener listener)
|
||||
{
|
||||
}
|
||||
|
||||
public String getHost()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public int getPort()
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
public String getGatewayURI()
|
||||
{
|
||||
// TODO Auto-generated method stub
|
||||
return null;
|
||||
}
|
||||
|
||||
public String getPath()
|
||||
{
|
||||
// TODO Auto-generated method stub
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
|
||||
// ------------------------------------------------------------------------
|
||||
// All rights reserved. This program and the accompanying materials
|
||||
// are made available under the terms of the Eclipse Public License v1.0
|
||||
// and Apache License v2.0 which accompanies this distribution.
|
||||
//
|
||||
// The Eclipse Public License is available at
|
||||
// http://www.eclipse.org/legal/epl-v10.html
|
||||
//
|
||||
// The Apache License v2.0 is available at
|
||||
// http://www.opensource.org/licenses/apache2.0.php
|
||||
//
|
||||
// You may elect to redistribute this code under either of these licenses.
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.rhttp.connector;
|
||||
|
||||
import org.eclipse.jetty.client.HttpClient;
|
||||
import org.eclipse.jetty.embedded.HelloHandler;
|
||||
import org.eclipse.jetty.rhttp.client.JettyClient;
|
||||
import org.eclipse.jetty.rhttp.client.RHTTPClient;
|
||||
import org.eclipse.jetty.rhttp.connector.ReverseHTTPConnector;
|
||||
import org.eclipse.jetty.server.Server;
|
||||
import org.eclipse.jetty.util.log.Log;
|
||||
|
||||
/**
|
||||
* A Test content server that uses a {@link ReverseHTTPConnector}.
|
||||
* The main of this class starts 3 TestReversionServers with IDs A, B and C.
|
||||
*/
|
||||
public class TestReverseServer extends Server
|
||||
{
|
||||
TestReverseServer(String targetId)
|
||||
{
|
||||
setHandler(new HelloHandler("Hello "+targetId,"Hi from "+targetId));
|
||||
|
||||
HttpClient httpClient = new HttpClient();
|
||||
RHTTPClient client = new JettyClient(httpClient,"http://localhost:8080/__rhttp",targetId);
|
||||
ReverseHTTPConnector connector = new ReverseHTTPConnector(client);
|
||||
|
||||
addConnector(connector);
|
||||
}
|
||||
|
||||
public static void main(String... args) throws Exception
|
||||
{
|
||||
Log.getLogger("org.mortbay.jetty.rhttp.client").setDebugEnabled(true);
|
||||
|
||||
TestReverseServer[] node = new TestReverseServer[] { new TestReverseServer("A"),new TestReverseServer("B"),new TestReverseServer("C") };
|
||||
|
||||
for (TestReverseServer s : node)
|
||||
s.start();
|
||||
|
||||
for (TestReverseServer s : node)
|
||||
s.join();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<parent>
|
||||
<groupId>org.eclipse.jetty.rhttp</groupId>
|
||||
<artifactId>jetty-rhttp-project</artifactId>
|
||||
<version>9.0.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<artifactId>reverse-http-gateway</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
<name>Jetty :: Reverse HTTP :: Gateway</name>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.codehaus.mojo</groupId>
|
||||
<artifactId>exec-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<mainClass>org.mortbay.jetty.rhttp.gateway.Main</mainClass>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty</groupId>
|
||||
<artifactId>reverse-http-client</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>javax.servlet</groupId>
|
||||
<artifactId>servlet-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty</groupId>
|
||||
<artifactId>jetty-io</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty</groupId>
|
||||
<artifactId>jetty-continuation</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty</groupId>
|
||||
<artifactId>jetty-server</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty</groupId>
|
||||
<artifactId>jetty-servlet</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty</groupId>
|
||||
<artifactId>jetty-client</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
|
@ -0,0 +1,92 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
|
||||
// ------------------------------------------------------------------------
|
||||
// All rights reserved. This program and the accompanying materials
|
||||
// are made available under the terms of the Eclipse Public License v1.0
|
||||
// and Apache License v2.0 which accompanies this distribution.
|
||||
//
|
||||
// The Eclipse Public License is available at
|
||||
// http://www.eclipse.org/legal/epl-v10.html
|
||||
//
|
||||
// The Apache License v2.0 is available at
|
||||
// http://www.opensource.org/licenses/apache2.0.php
|
||||
//
|
||||
// You may elect to redistribute this code under either of these licenses.
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.rhttp.gateway;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
import org.eclipse.jetty.rhttp.client.RHTTPRequest;
|
||||
|
||||
|
||||
/**
|
||||
* <p>A <tt>ClientDelegate</tt> is the server-side counterpart of a gateway client.</p>
|
||||
* <p>The gateway client, the comet protocol and the <tt>ClientDelegate</tt> form the
|
||||
* <em>Half-Object plus Protocol</em> pattern that is used between the gateway server
|
||||
* and the gateway client.</p>
|
||||
* <p><tt>ClientDelegate</tt> offers a server-side API on top of the comet communication.<br />
|
||||
* The API allows to enqueue server-side events to the gateway client, allows to
|
||||
* flush them to the gateway client, and allows to close and dispose server-side
|
||||
* resources when the gateway client disconnects.</p>
|
||||
*
|
||||
* @version $Revision$ $Date$
|
||||
*/
|
||||
public interface ClientDelegate
|
||||
{
|
||||
/**
|
||||
* @return the targetId that uniquely identifies this client delegate.
|
||||
*/
|
||||
public String getTargetId();
|
||||
|
||||
/**
|
||||
* <p>Enqueues the given request to the delivery queue so that it will be sent to the
|
||||
* gateway client on the first flush occasion.</p>
|
||||
* <p>Requests may fail to be queued, for example because the gateway client disconnected
|
||||
* concurrently.</p>
|
||||
*
|
||||
* @param request the request to add to the delivery queue
|
||||
* @return whether the request has been queued or not
|
||||
* @see #process(HttpServletRequest)
|
||||
*/
|
||||
public boolean enqueue(RHTTPRequest request);
|
||||
|
||||
/**
|
||||
* <p>Flushes the requests that have been {@link #enqueue(RHTTPRequest) enqueued}.</p>
|
||||
* <p>If no requests have been enqueued, then this method may suspend the current request for
|
||||
* the long poll timeout. <br />
|
||||
* The request is suspended only if all these conditions holds true:
|
||||
* <ul>
|
||||
* <li>it is not the first time that this method is called for this client delegate</li>
|
||||
* <li>no requests have been enqueued</li>
|
||||
* <li>this client delegate is not closed</li>
|
||||
* <li>the previous call to this method did not suspend the request</li>
|
||||
* </ul>
|
||||
* In all other cases, a response if sent to the gateway client, possibly containing no requests.
|
||||
*
|
||||
* @param httpRequest the HTTP request for the long poll request from the gateway client
|
||||
* @return the list of requests to send to the gateway client, or null if no response should be sent
|
||||
* to the gateway client
|
||||
* @throws IOException in case of I/O exception while flushing content to the gateway client
|
||||
* @see #enqueue(RHTTPRequest)
|
||||
*/
|
||||
public List<RHTTPRequest> process(HttpServletRequest httpRequest) throws IOException;
|
||||
|
||||
/**
|
||||
* <p>Closes this client delegate, in response to a gateway client request to disconnect.</p>
|
||||
* @see #isClosed()
|
||||
*/
|
||||
public void close();
|
||||
|
||||
/**
|
||||
* @return whether this delegate client is closed
|
||||
* @see #close()
|
||||
*/
|
||||
public boolean isClosed();
|
||||
}
|
|
@ -0,0 +1,224 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
|
||||
// ------------------------------------------------------------------------
|
||||
// All rights reserved. This program and the accompanying materials
|
||||
// are made available under the terms of the Eclipse Public License v1.0
|
||||
// and Apache License v2.0 which accompanies this distribution.
|
||||
//
|
||||
// The Eclipse Public License is available at
|
||||
// http://www.eclipse.org/legal/epl-v10.html
|
||||
//
|
||||
// The Apache License v2.0 is available at
|
||||
// http://www.opensource.org/licenses/apache2.0.php
|
||||
//
|
||||
// You may elect to redistribute this code under either of these licenses.
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.rhttp.gateway;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.ServletOutputStream;
|
||||
import javax.servlet.http.HttpServlet;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.eclipse.jetty.rhttp.client.RHTTPRequest;
|
||||
import org.eclipse.jetty.rhttp.client.RHTTPResponse;
|
||||
import org.eclipse.jetty.util.log.Log;
|
||||
import org.eclipse.jetty.util.log.Logger;
|
||||
|
||||
/**
|
||||
* The servlet that handles the communication with the gateway clients.
|
||||
* @version $Revision$ $Date$
|
||||
*/
|
||||
public class ConnectorServlet extends HttpServlet
|
||||
{
|
||||
private final Logger logger = Log.getLogger(getClass().toString());
|
||||
private final TargetIdRetriever targetIdRetriever = new StandardTargetIdRetriever();
|
||||
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
|
||||
private final ConcurrentMap<String, Future<?>> expirations = new ConcurrentHashMap<String, Future<?>>();
|
||||
private final Gateway gateway;
|
||||
private long clientTimeout=15000;
|
||||
|
||||
public ConnectorServlet(Gateway gateway)
|
||||
{
|
||||
this.gateway = gateway;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init() throws ServletException
|
||||
{
|
||||
String t = getInitParameter("clientTimeout");
|
||||
if (t!=null && !"".equals(t))
|
||||
clientTimeout=Long.parseLong(t);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
|
||||
{
|
||||
String targetId = targetIdRetriever.retrieveTargetId(request);
|
||||
|
||||
String uri = request.getRequestURI();
|
||||
String path = uri.substring(request.getServletPath().length());
|
||||
String[] segments = path.split("/");
|
||||
if (segments.length < 3)
|
||||
throw new ServletException("Invalid request to " + getClass().getSimpleName() + ": " + uri);
|
||||
|
||||
String action = segments[2];
|
||||
if ("handshake".equals(action))
|
||||
serviceHandshake(targetId, request, response);
|
||||
else if ("connect".equals(action))
|
||||
serviceConnect(targetId, request, response);
|
||||
else if ("deliver".equals(action))
|
||||
serviceDeliver(targetId, request, response);
|
||||
else if ("disconnect".equals(action))
|
||||
serviceDisconnect(targetId, request, response);
|
||||
else
|
||||
throw new ServletException("Invalid request to " + getClass().getSimpleName() + ": " + uri);
|
||||
}
|
||||
|
||||
private void serviceHandshake(String targetId, HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws IOException
|
||||
{
|
||||
ClientDelegate client = gateway.getClientDelegate(targetId);
|
||||
if (client != null)
|
||||
throw new IOException("Client with targetId " + targetId + " is already connected");
|
||||
|
||||
client = gateway.newClientDelegate(targetId);
|
||||
ClientDelegate existing = gateway.addClientDelegate(targetId, client);
|
||||
if (existing != null)
|
||||
throw new IOException("Client with targetId " + targetId + " is already connected");
|
||||
|
||||
flush(client, httpRequest, httpResponse);
|
||||
}
|
||||
|
||||
private void flush(ClientDelegate client, HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws IOException
|
||||
{
|
||||
List<RHTTPRequest> requests = client.process(httpRequest);
|
||||
if (requests != null)
|
||||
{
|
||||
// Schedule before sending the requests, to avoid that the remote client
|
||||
// reconnects before we have scheduled the expiration timeout.
|
||||
if (!client.isClosed())
|
||||
schedule(client);
|
||||
|
||||
ServletOutputStream output = httpResponse.getOutputStream();
|
||||
for (RHTTPRequest request : requests)
|
||||
output.write(request.getFrameBytes());
|
||||
// I could count the framed bytes of all requests and set a Content-Length header,
|
||||
// but the implementation of ServletOutputStream takes care of everything:
|
||||
// if the request was HTTP/1.1, then flushing result in a chunked response, but the
|
||||
// client know how to handle it; if the request was HTTP/1.0, then no chunking.
|
||||
// To avoid chunking in HTTP/1.1 I must set the Content-Length header.
|
||||
output.flush();
|
||||
logger.debug("Delivered to device {} requests {} ", client.getTargetId(), requests);
|
||||
}
|
||||
}
|
||||
|
||||
private void schedule(ClientDelegate client)
|
||||
{
|
||||
Future<?> task = scheduler.schedule(new ClientExpirationTask(client), clientTimeout, TimeUnit.MILLISECONDS);
|
||||
Future<?> existing = expirations.put(client.getTargetId(), task);
|
||||
assert existing == null;
|
||||
}
|
||||
|
||||
private void unschedule(String targetId)
|
||||
{
|
||||
Future<?> task = expirations.remove(targetId);
|
||||
if (task != null)
|
||||
task.cancel(false);
|
||||
}
|
||||
|
||||
private void serviceConnect(String targetId, HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws IOException
|
||||
{
|
||||
unschedule(targetId);
|
||||
|
||||
ClientDelegate client = gateway.getClientDelegate(targetId);
|
||||
if (client == null)
|
||||
{
|
||||
// Expired client tries to connect without handshake
|
||||
httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED);
|
||||
return;
|
||||
}
|
||||
|
||||
flush(client, httpRequest, httpResponse);
|
||||
|
||||
if (client.isClosed())
|
||||
gateway.removeClientDelegate(targetId);
|
||||
}
|
||||
|
||||
private void expireConnect(ClientDelegate client, long time)
|
||||
{
|
||||
String targetId = client.getTargetId();
|
||||
logger.info("Client with targetId {} missing, last seen {} ms ago, closing it", targetId, System.currentTimeMillis() - time);
|
||||
client.close();
|
||||
// If the client expired, means that it did not connect,
|
||||
// so there no request to resume, and we cleanup here
|
||||
// (while normally this cleanup is done in serviceConnect())
|
||||
unschedule(targetId);
|
||||
gateway.removeClientDelegate(targetId);
|
||||
}
|
||||
|
||||
private void serviceDeliver(String targetId, HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws ServletException, IOException
|
||||
{
|
||||
if (gateway.getClientDelegate(targetId) == null)
|
||||
{
|
||||
// Expired client tries to deliver without handshake
|
||||
httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED);
|
||||
return;
|
||||
}
|
||||
|
||||
byte[] body = Utils.read(httpRequest.getInputStream());
|
||||
|
||||
RHTTPResponse response = RHTTPResponse.fromFrameBytes(body);
|
||||
|
||||
ExternalRequest externalRequest = gateway.removeExternalRequest(response.getId());
|
||||
if (externalRequest != null)
|
||||
{
|
||||
externalRequest.respond(response);
|
||||
logger.debug("Deliver request from device {}, gateway request {}, response {}", new Object[] {targetId, externalRequest, response});
|
||||
}
|
||||
else
|
||||
{
|
||||
// We can arrive here for a race with the continuation expiration, which expired just before
|
||||
// the gateway client responded with a valid response; log this case ignore it.
|
||||
logger.debug("Deliver request from device {}, missing gateway request, response {}", targetId, response);
|
||||
}
|
||||
}
|
||||
|
||||
private void serviceDisconnect(String targetId, HttpServletRequest request, HttpServletResponse response)
|
||||
{
|
||||
// Do not remove the ClientDelegate from the gateway here,
|
||||
// since closing the ClientDelegate will resume the connect request
|
||||
// and we remove the ClientDelegate from the gateway there
|
||||
ClientDelegate client = gateway.getClientDelegate(targetId);
|
||||
if (client != null)
|
||||
client.close();
|
||||
}
|
||||
|
||||
private class ClientExpirationTask implements Runnable
|
||||
{
|
||||
private final long time = System.currentTimeMillis();
|
||||
private final ClientDelegate client;
|
||||
|
||||
public ClientExpirationTask(ClientDelegate client)
|
||||
{
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
public void run()
|
||||
{
|
||||
expireConnect(client, time);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
|
||||
// ------------------------------------------------------------------------
|
||||
// All rights reserved. This program and the accompanying materials
|
||||
// are made available under the terms of the Eclipse Public License v1.0
|
||||
// and Apache License v2.0 which accompanies this distribution.
|
||||
//
|
||||
// The Eclipse Public License is available at
|
||||
// http://www.eclipse.org/legal/epl-v10.html
|
||||
//
|
||||
// The Apache License v2.0 is available at
|
||||
// http://www.opensource.org/licenses/apache2.0.php
|
||||
//
|
||||
// You may elect to redistribute this code under either of these licenses.
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.rhttp.gateway;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.eclipse.jetty.rhttp.client.RHTTPRequest;
|
||||
import org.eclipse.jetty.rhttp.client.RHTTPResponse;
|
||||
|
||||
|
||||
/**
|
||||
* <p><tt>ExternalRequest</tt> represent an external request made to the gateway server.</p>
|
||||
* <p><tt>ExternalRequest</tt>s that arrive to the gateway server are suspended, waiting
|
||||
* for a response from the corresponding gateway client.</p>
|
||||
*
|
||||
* @version $Revision$ $Date$
|
||||
*/
|
||||
public interface ExternalRequest
|
||||
{
|
||||
/**
|
||||
* <p>Suspends this <tt>ExternalRequest</tt> waiting for a response from the gateway client.</p>
|
||||
* @return true if the <tt>ExternalRequest</tt> has been suspended, false if the
|
||||
* <tt>ExternalRequest</tt> has already been responded.
|
||||
*/
|
||||
public boolean suspend();
|
||||
|
||||
/**
|
||||
* <p>Responds to the original external request with the response arrived from the gateway client.</p>
|
||||
* @param response the response arrived from the gateway client
|
||||
* @throws IOException if responding to the original external request fails
|
||||
*/
|
||||
public void respond(RHTTPResponse response) throws IOException;
|
||||
|
||||
/**
|
||||
* @return the request to be sent to the gateway client
|
||||
*/
|
||||
public RHTTPRequest getRequest();
|
||||
}
|
|
@ -0,0 +1,88 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
|
||||
// ------------------------------------------------------------------------
|
||||
// All rights reserved. This program and the accompanying materials
|
||||
// are made available under the terms of the Eclipse Public License v1.0
|
||||
// and Apache License v2.0 which accompanies this distribution.
|
||||
//
|
||||
// The Eclipse Public License is available at
|
||||
// http://www.eclipse.org/legal/epl-v10.html
|
||||
//
|
||||
// The Apache License v2.0 is available at
|
||||
// http://www.opensource.org/licenses/apache2.0.php
|
||||
//
|
||||
// You may elect to redistribute this code under either of these licenses.
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.rhttp.gateway;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServlet;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.eclipse.jetty.rhttp.client.RHTTPRequest;
|
||||
import org.eclipse.jetty.util.log.Log;
|
||||
import org.eclipse.jetty.util.log.Logger;
|
||||
|
||||
/**
|
||||
* The servlet that handles external requests.
|
||||
*
|
||||
* @version $Revision$ $Date$
|
||||
*/
|
||||
public class ExternalServlet extends HttpServlet
|
||||
{
|
||||
private final Logger logger = Log.getLogger(getClass().toString());
|
||||
private final Gateway gateway;
|
||||
private TargetIdRetriever targetIdRetriever;
|
||||
|
||||
public ExternalServlet(Gateway gateway, TargetIdRetriever targetIdRetriever)
|
||||
{
|
||||
this.gateway = gateway;
|
||||
this.targetIdRetriever = targetIdRetriever;
|
||||
}
|
||||
|
||||
public TargetIdRetriever getTargetIdRetriever()
|
||||
{
|
||||
return targetIdRetriever;
|
||||
}
|
||||
|
||||
public void setTargetIdRetriever(TargetIdRetriever targetIdRetriever)
|
||||
{
|
||||
this.targetIdRetriever = targetIdRetriever;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void service(HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws ServletException, IOException
|
||||
{
|
||||
logger.debug("External http request: {}", httpRequest.getRequestURL());
|
||||
|
||||
String targetId = targetIdRetriever.retrieveTargetId(httpRequest);
|
||||
if (targetId == null)
|
||||
throw new ServletException("Invalid request to " + getClass().getSimpleName() + ": " + httpRequest.getRequestURI());
|
||||
|
||||
ClientDelegate client = gateway.getClientDelegate(targetId);
|
||||
if (client == null) throw new ServletException("Client with targetId " + targetId + " is not connected");
|
||||
|
||||
ExternalRequest externalRequest = gateway.newExternalRequest(httpRequest, httpResponse);
|
||||
RHTTPRequest request = externalRequest.getRequest();
|
||||
ExternalRequest existing = gateway.addExternalRequest(request.getId(), externalRequest);
|
||||
assert existing == null;
|
||||
logger.debug("External request {} for device {}", request, targetId);
|
||||
|
||||
boolean delivered = client.enqueue(request);
|
||||
if (delivered)
|
||||
{
|
||||
externalRequest.suspend();
|
||||
}
|
||||
else
|
||||
{
|
||||
// TODO: improve this: we can temporarly queue this request elsewhere and wait for the client to reconnect ?
|
||||
throw new ServletException("Could not enqueue request to client with targetId " + targetId);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,99 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
|
||||
// ------------------------------------------------------------------------
|
||||
// All rights reserved. This program and the accompanying materials
|
||||
// are made available under the terms of the Eclipse Public License v1.0
|
||||
// and Apache License v2.0 which accompanies this distribution.
|
||||
//
|
||||
// The Eclipse Public License is available at
|
||||
// http://www.eclipse.org/legal/epl-v10.html
|
||||
//
|
||||
// The Apache License v2.0 is available at
|
||||
// http://www.opensource.org/licenses/apache2.0.php
|
||||
//
|
||||
// You may elect to redistribute this code under either of these licenses.
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.rhttp.gateway;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
/**
|
||||
* <p>Gateway instances are responsible of holding the state of the gateway server.</p>
|
||||
* <p>The state is composed by:
|
||||
* <ul>
|
||||
* <li>{@link ExternalRequest external requests} that are suspended waiting for the response</li>
|
||||
* <li>{@link ClientDelegate gateway clients} that are connected with the gateway server</li>
|
||||
* </ul></p>
|
||||
* <p>Instances of this class are created by the {@link GatewayServer}.</p>
|
||||
*
|
||||
* @version $Revision$ $Date$
|
||||
*/
|
||||
public interface Gateway
|
||||
{
|
||||
/**
|
||||
* <p>Returns the {@link ClientDelegate} with the given targetId.<br />
|
||||
* If there is no such ClientDelegate returns null.</p>
|
||||
*
|
||||
* @param targetId the targetId of the ClientDelegate to return
|
||||
* @return the ClientDelegate associated with the given targetId
|
||||
*/
|
||||
public ClientDelegate getClientDelegate(String targetId);
|
||||
|
||||
/**
|
||||
* <p>Creates and configures a new {@link ClientDelegate} with the given targetId.</p>
|
||||
* @param targetId the targetId of the ClientDelegate to create
|
||||
* @return a newly created ClientDelegate
|
||||
* @see #addClientDelegate(String, ClientDelegate)
|
||||
*/
|
||||
public ClientDelegate newClientDelegate(String targetId);
|
||||
|
||||
/**
|
||||
* <p>Maps the given ClientDelegate to the given targetId.</p>
|
||||
* @param targetId the targetId of the given ClientDelegate
|
||||
* @param client the ClientDelegate to map
|
||||
* @return the previously existing ClientDelegate mapped to the same targetId
|
||||
* @see #removeClientDelegate(String)
|
||||
*/
|
||||
public ClientDelegate addClientDelegate(String targetId, ClientDelegate client);
|
||||
|
||||
/**
|
||||
* <p>Removes the {@link ClientDelegate} associated with the given targetId.</p>
|
||||
* @param targetId the targetId of the ClientDelegate to remove
|
||||
* @return the removed ClientDelegate, or null if no ClientDelegate was removed
|
||||
* @see #addClientDelegate(String, ClientDelegate)
|
||||
*/
|
||||
public ClientDelegate removeClientDelegate(String targetId);
|
||||
|
||||
/**
|
||||
* <p>Creates a new {@link ExternalRequest} from the given HTTP request and HTTP response.</p>
|
||||
* @param httpRequest the HTTP request of the external request
|
||||
* @param httpResponse the HTTP response of the external request
|
||||
* @return a newly created ExternalRequest
|
||||
* @throws IOException in case of failures creating the ExternalRequest
|
||||
* @see #addExternalRequest(int, ExternalRequest)
|
||||
*/
|
||||
public ExternalRequest newExternalRequest(HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws IOException;
|
||||
|
||||
/**
|
||||
* Maps the given ExternalRequest with the given requestId into the gateway state.
|
||||
* @param requestId the id of the ExternalRequest
|
||||
* @param externalRequest the ExternalRequest to map
|
||||
* @return the previously existing ExternalRequest mapped to the same requestId
|
||||
* @see #removeExternalRequest(int)
|
||||
*/
|
||||
public ExternalRequest addExternalRequest(int requestId, ExternalRequest externalRequest);
|
||||
|
||||
/**
|
||||
* Removes the ExternalRequest mapped to the given requestId from the gateway state.
|
||||
* @param requestId the id of the ExternalRequest
|
||||
* @return the removed ExternalRequest
|
||||
* @see #addExternalRequest(int, ExternalRequest)
|
||||
*/
|
||||
public ExternalRequest removeExternalRequest(int requestId);
|
||||
}
|
|
@ -0,0 +1,228 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
|
||||
// ------------------------------------------------------------------------
|
||||
// All rights reserved. This program and the accompanying materials
|
||||
// are made available under the terms of the Eclipse Public License v1.0
|
||||
// and Apache License v2.0 which accompanies this distribution.
|
||||
//
|
||||
// The Eclipse Public License is available at
|
||||
// http://www.eclipse.org/legal/epl-v10.html
|
||||
//
|
||||
// The Apache License v2.0 is available at
|
||||
// http://www.opensource.org/licenses/apache2.0.php
|
||||
//
|
||||
// You may elect to redistribute this code under either of these licenses.
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.rhttp.gateway;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.eclipse.jetty.client.Address;
|
||||
import org.eclipse.jetty.client.ContentExchange;
|
||||
import org.eclipse.jetty.client.HttpClient;
|
||||
import org.eclipse.jetty.client.HttpExchange;
|
||||
import org.eclipse.jetty.io.Buffer;
|
||||
import org.eclipse.jetty.io.ByteArrayBuffer;
|
||||
import org.eclipse.jetty.rhttp.client.JettyClient;
|
||||
import org.eclipse.jetty.rhttp.client.RHTTPClient;
|
||||
import org.eclipse.jetty.rhttp.client.RHTTPListener;
|
||||
import org.eclipse.jetty.rhttp.client.RHTTPRequest;
|
||||
import org.eclipse.jetty.rhttp.client.RHTTPResponse;
|
||||
import org.eclipse.jetty.server.Connector;
|
||||
import org.eclipse.jetty.server.nio.SelectChannelConnector;
|
||||
import org.eclipse.jetty.util.log.Log;
|
||||
import org.eclipse.jetty.util.log.Logger;
|
||||
|
||||
/**
|
||||
* <p>This class combines a gateway server and a gateway client to obtain the functionality of a simple proxy server.</p>
|
||||
* <p>This gateway proxy server starts on port 8080 and can be set as http proxy in browsers such as Firefox, and used
|
||||
* to browse the internet.</p>
|
||||
* <p>Its functionality is limited (for example, it only supports http, and not https).</p>
|
||||
* @version $Revision$ $Date$
|
||||
*/
|
||||
public class GatewayProxyServer
|
||||
{
|
||||
private static final Logger logger = Log.getLogger(GatewayProxyServer.class.toString());
|
||||
|
||||
public static void main(String[] args) throws Exception
|
||||
{
|
||||
GatewayServer server = new GatewayServer();
|
||||
|
||||
Connector plainConnector = new SelectChannelConnector();
|
||||
plainConnector.setPort(8080);
|
||||
server.addConnector(plainConnector);
|
||||
|
||||
((StandardGateway)server.getGateway()).setExternalTimeout(180000);
|
||||
((StandardGateway)server.getGateway()).setGatewayTimeout(20000);
|
||||
server.setTargetIdRetriever(new ProxyTargetIdRetriever());
|
||||
server.start();
|
||||
|
||||
HttpClient httpClient = new HttpClient();
|
||||
httpClient.setConnectorType(HttpClient.CONNECTOR_SOCKET);
|
||||
httpClient.start();
|
||||
|
||||
RHTTPClient client = new JettyClient(httpClient, new Address("localhost", plainConnector.getPort()), server.getContext().getContextPath() + "/gw", "proxy");
|
||||
client.addListener(new ProxyListener(httpClient, client));
|
||||
client.connect();
|
||||
|
||||
Runtime.getRuntime().addShutdownHook(new Shutdown(server, httpClient, client));
|
||||
logger.info("{} started", GatewayProxyServer.class.getSimpleName());
|
||||
}
|
||||
|
||||
private static class Shutdown extends Thread
|
||||
{
|
||||
private final GatewayServer server;
|
||||
private final HttpClient httpClient;
|
||||
private final RHTTPClient client;
|
||||
|
||||
public Shutdown(GatewayServer server, HttpClient httpClient, RHTTPClient client)
|
||||
{
|
||||
this.server = server;
|
||||
this.httpClient = httpClient;
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
try
|
||||
{
|
||||
client.disconnect();
|
||||
httpClient.stop();
|
||||
server.stop();
|
||||
logger.info("{} stopped", GatewayProxyServer.class.getSimpleName());
|
||||
}
|
||||
catch (Exception x)
|
||||
{
|
||||
logger.debug("Exception while stopping " + GatewayProxyServer.class.getSimpleName(), x);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class ProxyListener implements RHTTPListener
|
||||
{
|
||||
private final HttpClient httpClient;
|
||||
private final RHTTPClient client;
|
||||
|
||||
private ProxyListener(HttpClient httpClient, RHTTPClient client)
|
||||
{
|
||||
this.httpClient = httpClient;
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
public void onRequest(RHTTPRequest request) throws Exception
|
||||
{
|
||||
ProxyExchange exchange = new ProxyExchange();
|
||||
Address address = Address.from(request.getHeaders().get("Host"));
|
||||
if (address.getPort() == 0) address = new Address(address.getHost(), 80);
|
||||
exchange.setAddress(address);
|
||||
exchange.setMethod(request.getMethod());
|
||||
exchange.setURI(request.getURI());
|
||||
for (Map.Entry<String, String> header : request.getHeaders().entrySet())
|
||||
exchange.setRequestHeader(header.getKey(), header.getValue());
|
||||
exchange.setRequestContent(new ByteArrayBuffer(request.getBody()));
|
||||
int status = syncSend(exchange);
|
||||
if (status == HttpExchange.STATUS_COMPLETED)
|
||||
{
|
||||
int statusCode = exchange.getResponseStatus();
|
||||
String statusMessage = exchange.getResponseMessage();
|
||||
Map<String, String> responseHeaders = exchange.getResponseHeaders();
|
||||
byte[] responseBody = exchange.getResponseBody();
|
||||
RHTTPResponse response = new RHTTPResponse(request.getId(), statusCode, statusMessage, responseHeaders, responseBody);
|
||||
client.deliver(response);
|
||||
}
|
||||
else
|
||||
{
|
||||
int statusCode = HttpServletResponse.SC_SERVICE_UNAVAILABLE;
|
||||
String statusMessage = "Gateway error";
|
||||
HashMap<String, String> responseHeaders = new HashMap<String, String>();
|
||||
responseHeaders.put("Connection", "close");
|
||||
byte[] responseBody = new byte[0];
|
||||
RHTTPResponse response = new RHTTPResponse(request.getId(), statusCode, statusMessage, responseHeaders, responseBody);
|
||||
client.deliver(response);
|
||||
}
|
||||
}
|
||||
|
||||
private int syncSend(ProxyExchange exchange) throws Exception
|
||||
{
|
||||
long start = System.nanoTime();
|
||||
httpClient.send(exchange);
|
||||
int status = exchange.waitForDone();
|
||||
long end = System.nanoTime();
|
||||
long millis = TimeUnit.NANOSECONDS.toMillis(end - start);
|
||||
long micros = TimeUnit.NANOSECONDS.toMicros(end - start - TimeUnit.MILLISECONDS.toNanos(millis));
|
||||
logger.debug("Proxied request took {}.{} ms", millis, micros);
|
||||
return status;
|
||||
}
|
||||
}
|
||||
|
||||
private static class ProxyExchange extends ContentExchange
|
||||
{
|
||||
private String responseMessage;
|
||||
private Map<String, String> responseHeaders = new HashMap<String, String>();
|
||||
private ByteArrayOutputStream responseBody = new ByteArrayOutputStream();
|
||||
|
||||
private ProxyExchange()
|
||||
{
|
||||
super(true);
|
||||
}
|
||||
|
||||
public String getResponseMessage()
|
||||
{
|
||||
return responseMessage;
|
||||
}
|
||||
|
||||
public Map<String, String> getResponseHeaders()
|
||||
{
|
||||
return responseHeaders;
|
||||
}
|
||||
|
||||
public byte[] getResponseBody()
|
||||
{
|
||||
return responseBody.toByteArray();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResponseStatus(Buffer version, int code, Buffer message) throws IOException
|
||||
{
|
||||
super.onResponseStatus(version, code, message);
|
||||
this.responseMessage = message.toString("UTF-8");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResponseHeader(Buffer nameBuffer, Buffer valueBuffer) throws IOException
|
||||
{
|
||||
super.onResponseHeader(nameBuffer, valueBuffer);
|
||||
String name = nameBuffer.toString("UTF-8");
|
||||
String value = valueBuffer.toString("UTF-8");
|
||||
// Skip chunked header, since we read the whole body and will not re-chunk it
|
||||
if (!name.equalsIgnoreCase("Transfer-Encoding") || !value.equalsIgnoreCase("chunked"))
|
||||
responseHeaders.put(name, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResponseContent(Buffer buffer) throws IOException
|
||||
{
|
||||
responseBody.write(buffer.asArray());
|
||||
super.onResponseContent(buffer);
|
||||
}
|
||||
}
|
||||
|
||||
public static class ProxyTargetIdRetriever implements TargetIdRetriever
|
||||
{
|
||||
public String retrieveTargetId(HttpServletRequest httpRequest)
|
||||
{
|
||||
return "proxy";
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,171 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
|
||||
// ------------------------------------------------------------------------
|
||||
// All rights reserved. This program and the accompanying materials
|
||||
// are made available under the terms of the Eclipse Public License v1.0
|
||||
// and Apache License v2.0 which accompanies this distribution.
|
||||
//
|
||||
// The Eclipse Public License is available at
|
||||
// http://www.eclipse.org/legal/epl-v10.html
|
||||
//
|
||||
// The Apache License v2.0 is available at
|
||||
// http://www.opensource.org/licenses/apache2.0.php
|
||||
//
|
||||
// You may elect to redistribute this code under either of these licenses.
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.rhttp.gateway;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.eclipse.jetty.http.MimeTypes;
|
||||
import org.eclipse.jetty.rhttp.client.RHTTPClient;
|
||||
import org.eclipse.jetty.rhttp.client.RHTTPRequest;
|
||||
import org.eclipse.jetty.rhttp.client.RHTTPResponse;
|
||||
import org.eclipse.jetty.server.Connector;
|
||||
import org.eclipse.jetty.server.Server;
|
||||
import org.eclipse.jetty.server.handler.HandlerCollection;
|
||||
import org.eclipse.jetty.servlet.DefaultServlet;
|
||||
import org.eclipse.jetty.servlet.ServletContextHandler;
|
||||
import org.eclipse.jetty.servlet.ServletHolder;
|
||||
import org.eclipse.jetty.util.log.Log;
|
||||
import org.eclipse.jetty.util.log.Logger;
|
||||
|
||||
/**
|
||||
* <p>The gateway server is a server component that acts as intermediary between
|
||||
* <em>external clients</em> which perform requests for resources, and the
|
||||
* <em>resource providers</em>.</p>
|
||||
* <p>The particularity of the gateway server is that the resource providers
|
||||
* connect to the gateway using a comet protocol. <br />
|
||||
* The comet procotol functionality is implemented by a gateway client. <br />
|
||||
* This is quite different from a normal proxy server where it is the proxy that
|
||||
* connects to the resource providers.</p>
|
||||
* <p>Schematically, this is how the gateway server works:</p>
|
||||
* <pre>
|
||||
* External Client Gateway Server Gateway Client Resource Provider
|
||||
* | |
|
||||
* | <-- comet req. 1 --- |
|
||||
* | --- ext. req. 1 --> | |
|
||||
* | | --- comet res. 1 --> |
|
||||
* | | <-- comet req. 2 --- |
|
||||
* | | --- ext. req. 1 --> |
|
||||
* |
|
||||
* | | <-- ext. res. 1 --- |
|
||||
* | | <-- ext. res. 1 --- |
|
||||
* | <-- ext. res. 1 --- |
|
||||
*
|
||||
* | --- ext. req. 2 --> |
|
||||
* | | --- comet res. 2 --> |
|
||||
* . . .
|
||||
* </pre>
|
||||
* <p>The gateway server is made of two servlets:
|
||||
* <ul>
|
||||
* <li>the external servlet, that handles external requests</li>
|
||||
* <li>the gateway servlet, that handles the communication with the gateway client</li>
|
||||
* </ul>
|
||||
* </p>
|
||||
* <p>External requests are suspended using Jetty continuations until a response for
|
||||
* that request arrives from the resource provider, or a
|
||||
* {@link #getExternalTimeout() configurable timeout} expires. <br />
|
||||
* Comet requests made by the gateway client also expires after a (different)
|
||||
* {@link #getGatewayTimeout() configurable timeout}.</p>
|
||||
* <p>External requests are packed into {@link RHTTPRequest} objects, converted into an
|
||||
* opaque byte array and sent as the body of the comet reponse to the gateway
|
||||
* {@link RHTTPClient}.</p>
|
||||
* <p>The gateway client uses a notification mechanism to alert listeners interested
|
||||
* in external requests that have been forwarded through the gateway. It is up to the
|
||||
* listeners to connect to the resource provider however they like.</p>
|
||||
* <p>When the gateway client receives a response from the resource provider, it packs
|
||||
* the response into a {@link RHTTPResponse} object, converts it into an opaque byte array
|
||||
* and sends it as the body of a normal HTTP request to the gateway server.</p>
|
||||
* <p>It is possible to connect more than one gateway client to a gateway server; each
|
||||
* gateway client is identified by a unique <em>targetId</em>. <br />
|
||||
* External requests must specify a targetId that allows the gateway server to forward
|
||||
* the requests to the specific gateway client; how the targetId is retrieved from an
|
||||
* external request is handled by {@link TargetIdRetriever} implementations.</p>
|
||||
*
|
||||
* @version $Revision$ $Date$
|
||||
*/
|
||||
public class GatewayServer extends Server
|
||||
{
|
||||
public final static String DFT_EXT_PATH="/gw";
|
||||
public final static String DFT_CONNECT_PATH="/__rhttp";
|
||||
private final Logger logger = Log.getLogger(getClass().toString());
|
||||
private final Gateway gateway;
|
||||
private final ServletHolder externalServletHolder;
|
||||
private final ServletHolder connectorServletHolder;
|
||||
private final ServletContextHandler context;
|
||||
|
||||
public GatewayServer()
|
||||
{
|
||||
this("",DFT_EXT_PATH,DFT_CONNECT_PATH,new StandardTargetIdRetriever());
|
||||
}
|
||||
|
||||
public GatewayServer(String contextPath, String externalServletPath,String gatewayServletPath, TargetIdRetriever targetIdRetriever)
|
||||
{
|
||||
HandlerCollection handlers = new HandlerCollection();
|
||||
setHandler(handlers);
|
||||
context = new ServletContextHandler(handlers, contextPath, ServletContextHandler.SESSIONS);
|
||||
|
||||
// Setup the gateway
|
||||
gateway = createGateway();
|
||||
|
||||
// Setup external servlet
|
||||
ExternalServlet externalServlet = new ExternalServlet(gateway, targetIdRetriever);
|
||||
externalServletHolder = new ServletHolder(externalServlet);
|
||||
context.addServlet(externalServletHolder, externalServletPath + "/*");
|
||||
logger.debug("External servlet mapped to {}/*", externalServletPath);
|
||||
|
||||
// Setup gateway servlet
|
||||
ConnectorServlet gatewayServlet = new ConnectorServlet(gateway);
|
||||
connectorServletHolder = new ServletHolder(gatewayServlet);
|
||||
connectorServletHolder.setInitParameter("clientTimeout", "15000");
|
||||
context.addServlet(connectorServletHolder, gatewayServletPath + "/*");
|
||||
logger.debug("Gateway servlet mapped to {}/*", gatewayServletPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and configures a {@link Gateway} object.
|
||||
* @return the newly created and configured Gateway object.
|
||||
*/
|
||||
protected Gateway createGateway()
|
||||
{
|
||||
StandardGateway gateway = new StandardGateway();
|
||||
return gateway;
|
||||
}
|
||||
|
||||
public ServletContextHandler getContext()
|
||||
{
|
||||
return context;
|
||||
}
|
||||
|
||||
public Gateway getGateway()
|
||||
{
|
||||
return gateway;
|
||||
}
|
||||
|
||||
public ServletHolder getExternalServlet()
|
||||
{
|
||||
return externalServletHolder;
|
||||
}
|
||||
|
||||
public ServletHolder getConnectorServlet()
|
||||
{
|
||||
return connectorServletHolder;
|
||||
}
|
||||
|
||||
public void setTargetIdRetriever(TargetIdRetriever retriever)
|
||||
{
|
||||
((ExternalServlet)externalServletHolder.getServletInstance()).setTargetIdRetriever(retriever);
|
||||
}
|
||||
|
||||
public TargetIdRetriever getTargetIdRetriever()
|
||||
{
|
||||
return ((ExternalServlet)externalServletHolder.getServletInstance()).getTargetIdRetriever();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
|
||||
// ------------------------------------------------------------------------
|
||||
// All rights reserved. This program and the accompanying materials
|
||||
// are made available under the terms of the Eclipse Public License v1.0
|
||||
// and Apache License v2.0 which accompanies this distribution.
|
||||
//
|
||||
// The Eclipse Public License is available at
|
||||
// http://www.eclipse.org/legal/epl-v10.html
|
||||
//
|
||||
// The Apache License v2.0 is available at
|
||||
// http://www.opensource.org/licenses/apache2.0.php
|
||||
//
|
||||
// You may elect to redistribute this code under either of these licenses.
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.rhttp.gateway;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
/**
|
||||
* @version $Revision$ $Date$
|
||||
*/
|
||||
public class HostTargetIdRetriever implements TargetIdRetriever
|
||||
{
|
||||
private final String suffix;
|
||||
|
||||
public HostTargetIdRetriever(String suffix)
|
||||
{
|
||||
this.suffix = suffix;
|
||||
}
|
||||
|
||||
public String retrieveTargetId(HttpServletRequest httpRequest)
|
||||
{
|
||||
String host = httpRequest.getHeader("Host");
|
||||
if (host != null)
|
||||
{
|
||||
// Strip the port
|
||||
int colon = host.indexOf(':');
|
||||
if (colon > 0)
|
||||
{
|
||||
host = host.substring(0, colon);
|
||||
}
|
||||
|
||||
if (suffix != null && host.endsWith(suffix))
|
||||
{
|
||||
return host.substring(0, host.length() - suffix.length());
|
||||
}
|
||||
}
|
||||
return host;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,128 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
|
||||
// ------------------------------------------------------------------------
|
||||
// All rights reserved. This program and the accompanying materials
|
||||
// are made available under the terms of the Eclipse Public License v1.0
|
||||
// and Apache License v2.0 which accompanies this distribution.
|
||||
//
|
||||
// The Eclipse Public License is available at
|
||||
// http://www.eclipse.org/legal/epl-v10.html
|
||||
//
|
||||
// The Apache License v2.0 is available at
|
||||
// http://www.opensource.org/licenses/apache2.0.php
|
||||
//
|
||||
// You may elect to redistribute this code under either of these licenses.
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.rhttp.gateway;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.eclipse.jetty.server.Connector;
|
||||
import org.eclipse.jetty.server.nio.SelectChannelConnector;
|
||||
import org.eclipse.jetty.servlet.DefaultServlet;
|
||||
import org.eclipse.jetty.servlet.ServletHolder;
|
||||
|
||||
/**
|
||||
* <p>Main class that starts the gateway server.</p>
|
||||
* <p>This class supports the following arguments:</p>
|
||||
* <ul>
|
||||
* <li>--port=<port> specifies the port on which the gateway server listens to, by default 8080</li>
|
||||
* <li>--retriever=<retriever> specifies the
|
||||
* {@link GatewayServer#setTargetIdRetriever(TargetIdRetriever) target id retriever}</li>
|
||||
* <li>--resources=<resources file path> specifies the resource file path for the gateway</li>
|
||||
* </ul>
|
||||
* <p>Examples</p>
|
||||
* <p> <tt>java --port=8080</tt> </p>
|
||||
* <p> <tt>java --port=8080 --resources=/tmp/gateway-resources</tt> </p>
|
||||
* <p> <tt>java --port=8080 --retriever=standard</tt> </p>
|
||||
* <p> <tt>java --port=8080 --retriever=host,.rhttp.example.com</tt> </p>
|
||||
* <p>The latter example specifies the {@link HostTargetIdRetriever} with a suffix of <tt>.rhttp.example.com</tt></p>
|
||||
*
|
||||
* @see GatewayServer
|
||||
* @version $Revision$ $Date$
|
||||
*/
|
||||
public class Main
|
||||
{
|
||||
private static final String PORT_ARG = "port";
|
||||
private static final String RESOURCES_ARG = "resources";
|
||||
private static final String RETRIEVER_ARG = "retriever";
|
||||
|
||||
public static void main(String[] args) throws Exception
|
||||
{
|
||||
Map<String, Object> arguments = parse(args);
|
||||
|
||||
int port = 8080;
|
||||
if (arguments.containsKey(PORT_ARG))
|
||||
port = (Integer)arguments.get(PORT_ARG);
|
||||
|
||||
String resources = null;
|
||||
if (arguments.containsKey(RESOURCES_ARG))
|
||||
resources = (String)arguments.get(RESOURCES_ARG);
|
||||
|
||||
TargetIdRetriever retriever = null;
|
||||
if (arguments.containsKey(RETRIEVER_ARG))
|
||||
retriever = (TargetIdRetriever)arguments.get(RETRIEVER_ARG);
|
||||
|
||||
GatewayServer server = new GatewayServer();
|
||||
|
||||
Connector connector = new SelectChannelConnector();
|
||||
connector.setPort(port);
|
||||
server.addConnector(connector);
|
||||
|
||||
if (resources != null)
|
||||
{
|
||||
server.getContext().setResourceBase(resources);
|
||||
ServletHolder resourcesServletHolder = server.getContext().addServlet(DefaultServlet.class,"__r/*");
|
||||
resourcesServletHolder.setInitParameter("dirAllowed", "true");
|
||||
}
|
||||
|
||||
if (retriever != null)
|
||||
server.setTargetIdRetriever(retriever);
|
||||
|
||||
server.start();
|
||||
}
|
||||
|
||||
private static Map<String, Object> parse(String[] args)
|
||||
{
|
||||
Map<String, Object> result = new HashMap<String, Object>();
|
||||
|
||||
Pattern pattern = Pattern.compile("--([^=]+)=(.+)");
|
||||
for (String arg : args)
|
||||
{
|
||||
Matcher matcher = pattern.matcher(arg);
|
||||
if (matcher.matches())
|
||||
{
|
||||
String argName = matcher.group(1);
|
||||
if (PORT_ARG.equals(argName))
|
||||
{
|
||||
result.put(PORT_ARG, Integer.parseInt(matcher.group(2)));
|
||||
}
|
||||
else if (RESOURCES_ARG.equals(argName))
|
||||
{
|
||||
String argValue = matcher.group(2);
|
||||
result.put(RESOURCES_ARG, argValue);
|
||||
}
|
||||
else if (RETRIEVER_ARG.equals(argName))
|
||||
{
|
||||
String argValue = matcher.group(2);
|
||||
if (argValue.startsWith("host,"))
|
||||
{
|
||||
String[] typeAndSuffix = argValue.split(",");
|
||||
if (typeAndSuffix.length != 2)
|
||||
throw new IllegalArgumentException("Invalid option " + arg + ", must be of the form --" + RETRIEVER_ARG + "=host,suffix");
|
||||
|
||||
result.put(RETRIEVER_ARG, new HostTargetIdRetriever(typeAndSuffix[1]));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,172 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
|
||||
// ------------------------------------------------------------------------
|
||||
// All rights reserved. This program and the accompanying materials
|
||||
// are made available under the terms of the Eclipse Public License v1.0
|
||||
// and Apache License v2.0 which accompanies this distribution.
|
||||
//
|
||||
// The Eclipse Public License is available at
|
||||
// http://www.eclipse.org/legal/epl-v10.html
|
||||
//
|
||||
// The Apache License v2.0 is available at
|
||||
// http://www.opensource.org/licenses/apache2.0.php
|
||||
//
|
||||
// You may elect to redistribute this code under either of these licenses.
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.rhttp.gateway;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
import org.eclipse.jetty.continuation.Continuation;
|
||||
import org.eclipse.jetty.continuation.ContinuationSupport;
|
||||
import org.eclipse.jetty.rhttp.client.RHTTPRequest;
|
||||
import org.eclipse.jetty.util.log.Log;
|
||||
import org.eclipse.jetty.util.log.Logger;
|
||||
|
||||
/**
|
||||
* <p>Default implementation of {@link ClientDelegate}.</p>
|
||||
*
|
||||
* @version $Revision$ $Date$
|
||||
*/
|
||||
public class StandardClientDelegate implements ClientDelegate
|
||||
{
|
||||
private final Logger logger = Log.getLogger(getClass().toString());
|
||||
private final Object lock = new Object();
|
||||
private final List<RHTTPRequest> requests = new ArrayList<RHTTPRequest>();
|
||||
private final String targetId;
|
||||
private volatile boolean firstFlush = true;
|
||||
private volatile long timeout;
|
||||
private volatile boolean closed;
|
||||
private Continuation continuation;
|
||||
|
||||
public StandardClientDelegate(String targetId)
|
||||
{
|
||||
this.targetId = targetId;
|
||||
}
|
||||
|
||||
public String getTargetId()
|
||||
{
|
||||
return targetId;
|
||||
}
|
||||
|
||||
public long getTimeout()
|
||||
{
|
||||
return timeout;
|
||||
}
|
||||
|
||||
public void setTimeout(long timeout)
|
||||
{
|
||||
this.timeout = timeout;
|
||||
}
|
||||
|
||||
public boolean enqueue(RHTTPRequest request)
|
||||
{
|
||||
if (isClosed())
|
||||
return false;
|
||||
|
||||
synchronized (lock)
|
||||
{
|
||||
requests.add(request);
|
||||
resume();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void resume()
|
||||
{
|
||||
synchronized (lock)
|
||||
{
|
||||
// Continuation may be null in several cases:
|
||||
// 1. there always is something to deliver so we never suspend
|
||||
// 2. concurrent calls to add() and close()
|
||||
// 3. concurrent close() with a long poll that expired
|
||||
// 4. concurrent close() with a long poll that resumed
|
||||
if (continuation != null)
|
||||
{
|
||||
continuation.resume();
|
||||
// Null the continuation, as there is no point is resuming multiple times
|
||||
continuation = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public List<RHTTPRequest> process(HttpServletRequest httpRequest) throws IOException
|
||||
{
|
||||
// We want to respond in the following cases:
|
||||
// 1. It's the first time we process: the client will wait for a response before issuing another connect.
|
||||
// 2. The client disconnected, so we want to return from this connect before it times out.
|
||||
// 3. We've been woken up because there are responses to send.
|
||||
// 4. The continuation was suspended but timed out.
|
||||
// The timeout case is different from a non-first connect, in that we want to return
|
||||
// a (most of the times empty) response and we do not want to wait again.
|
||||
// The order of these if statements is important, as the continuation timed out only if
|
||||
// the client is not closed and there are no responses to send
|
||||
List<RHTTPRequest> result = Collections.emptyList();
|
||||
if (firstFlush)
|
||||
{
|
||||
firstFlush = false;
|
||||
logger.debug("Connect request (first) from device {}, delivering requests {}", targetId, result);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Synchronization is crucial here, since we don't want to suspend if there is something to deliver
|
||||
synchronized (lock)
|
||||
{
|
||||
int size = requests.size();
|
||||
if (size > 0)
|
||||
{
|
||||
assert continuation == null;
|
||||
result = new ArrayList<RHTTPRequest>(size);
|
||||
result.addAll(requests);
|
||||
requests.clear();
|
||||
logger.debug("Connect request (resumed) from device {}, delivering requests {}", targetId, result);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (continuation != null)
|
||||
{
|
||||
continuation = null;
|
||||
logger.debug("Connect request (expired) from device {}, delivering requests {}", targetId, result);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (isClosed())
|
||||
{
|
||||
logger.debug("Connect request (closed) from device {}, delivering requests {}", targetId, result);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Here we need to suspend
|
||||
continuation = ContinuationSupport.getContinuation(httpRequest);
|
||||
continuation.setTimeout(getTimeout());
|
||||
continuation.suspend();
|
||||
result = null;
|
||||
logger.debug("Connect request (suspended) from device {}", targetId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public void close()
|
||||
{
|
||||
closed = true;
|
||||
resume();
|
||||
}
|
||||
|
||||
public boolean isClosed()
|
||||
{
|
||||
return closed;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,189 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
|
||||
// ------------------------------------------------------------------------
|
||||
// All rights reserved. This program and the accompanying materials
|
||||
// are made available under the terms of the Eclipse Public License v1.0
|
||||
// and Apache License v2.0 which accompanies this distribution.
|
||||
//
|
||||
// The Eclipse Public License is available at
|
||||
// http://www.eclipse.org/legal/epl-v10.html
|
||||
//
|
||||
// The Apache License v2.0 is available at
|
||||
// http://www.opensource.org/licenses/apache2.0.php
|
||||
//
|
||||
// You may elect to redistribute this code under either of these licenses.
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.rhttp.gateway;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.servlet.ServletOutputStream;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.eclipse.jetty.continuation.Continuation;
|
||||
import org.eclipse.jetty.continuation.ContinuationListener;
|
||||
import org.eclipse.jetty.continuation.ContinuationSupport;
|
||||
import org.eclipse.jetty.rhttp.client.RHTTPRequest;
|
||||
import org.eclipse.jetty.rhttp.client.RHTTPResponse;
|
||||
import org.eclipse.jetty.util.log.Log;
|
||||
import org.eclipse.jetty.util.log.Logger;
|
||||
|
||||
/**
|
||||
* Default implementation of {@link ExternalRequest}.
|
||||
*
|
||||
* @version $Revision$ $Date$
|
||||
*/
|
||||
public class StandardExternalRequest implements ExternalRequest
|
||||
{
|
||||
private final Logger logger = Log.getLogger(getClass().toString());
|
||||
private final RHTTPRequest request;
|
||||
private final HttpServletRequest httpRequest;
|
||||
private final HttpServletResponse httpResponse;
|
||||
private final Gateway gateway;
|
||||
private final Object lock = new Object();
|
||||
private volatile long timeout;
|
||||
private Continuation continuation;
|
||||
private boolean responded;
|
||||
|
||||
public StandardExternalRequest(RHTTPRequest request, HttpServletRequest httpRequest, HttpServletResponse httpResponse, Gateway gateway)
|
||||
{
|
||||
this.request = request;
|
||||
this.httpRequest = httpRequest;
|
||||
this.httpResponse = httpResponse;
|
||||
this.gateway = gateway;
|
||||
}
|
||||
|
||||
public long getTimeout()
|
||||
{
|
||||
return timeout;
|
||||
}
|
||||
|
||||
public void setTimeout(long timeout)
|
||||
{
|
||||
this.timeout = timeout;
|
||||
}
|
||||
|
||||
public boolean suspend()
|
||||
{
|
||||
synchronized (lock)
|
||||
{
|
||||
// We suspend only if we have no responded yet
|
||||
if (!responded)
|
||||
{
|
||||
assert continuation == null;
|
||||
continuation = ContinuationSupport.getContinuation(httpRequest);
|
||||
continuation.setTimeout(getTimeout());
|
||||
continuation.addContinuationListener(new TimeoutListener());
|
||||
continuation.suspend(httpResponse);
|
||||
logger.debug("Request {} suspended", getRequest());
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.debug("Request {} already responded", getRequest());
|
||||
}
|
||||
return !responded;
|
||||
}
|
||||
}
|
||||
|
||||
public void respond(RHTTPResponse response) throws IOException
|
||||
{
|
||||
responseCompleted(response);
|
||||
}
|
||||
|
||||
private void responseCompleted(RHTTPResponse response) throws IOException
|
||||
{
|
||||
synchronized (lock)
|
||||
{
|
||||
// Could be that we complete exactly when the response is being expired
|
||||
if (!responded)
|
||||
{
|
||||
httpResponse.setStatus(response.getStatusCode());
|
||||
|
||||
for (Map.Entry<String, String> header : response.getHeaders().entrySet())
|
||||
httpResponse.setHeader(header.getKey(), header.getValue());
|
||||
|
||||
ServletOutputStream output = httpResponse.getOutputStream();
|
||||
output.write(response.getBody());
|
||||
output.flush();
|
||||
|
||||
// It may happen that the continuation is null,
|
||||
// because the response arrived before we had the chance to suspend
|
||||
if (continuation != null)
|
||||
{
|
||||
continuation.complete();
|
||||
continuation = null;
|
||||
}
|
||||
|
||||
// Mark as responded, so we know we don't have to suspend
|
||||
// or respond with an expired response
|
||||
responded = true;
|
||||
|
||||
if (logger.isDebugEnabled())
|
||||
{
|
||||
String eol = System.getProperty("line.separator");
|
||||
logger.debug("Request {} responded {}{}{}{}{}", new Object[]{request, response, eol, request.toLongString(), eol, response.toLongString()});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void responseExpired() throws IOException
|
||||
{
|
||||
synchronized (lock)
|
||||
{
|
||||
// Could be that we expired exactly when the response is being completed
|
||||
if (!responded)
|
||||
{
|
||||
httpResponse.sendError(HttpServletResponse.SC_GATEWAY_TIMEOUT, "Gateway Time-out");
|
||||
|
||||
continuation.complete();
|
||||
continuation = null;
|
||||
|
||||
// Mark as responded, so we know we don't have to respond with a completed response
|
||||
responded = true;
|
||||
|
||||
logger.debug("Request {} expired", getRequest());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public RHTTPRequest getRequest()
|
||||
{
|
||||
return request;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
return request.toString();
|
||||
}
|
||||
|
||||
private class TimeoutListener implements ContinuationListener
|
||||
{
|
||||
public void onComplete(Continuation continuation)
|
||||
{
|
||||
}
|
||||
|
||||
public void onTimeout(Continuation continuation)
|
||||
{
|
||||
ExternalRequest externalRequest = gateway.removeExternalRequest(getRequest().getId());
|
||||
// The gateway request can be null for a race with delivery
|
||||
if (externalRequest != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
responseExpired();
|
||||
}
|
||||
catch (Exception x)
|
||||
{
|
||||
logger.warn("Request " + getRequest() + " expired but failed", x);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,131 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
|
||||
// ------------------------------------------------------------------------
|
||||
// All rights reserved. This program and the accompanying materials
|
||||
// are made available under the terms of the Eclipse Public License v1.0
|
||||
// and Apache License v2.0 which accompanies this distribution.
|
||||
//
|
||||
// The Eclipse Public License is available at
|
||||
// http://www.eclipse.org/legal/epl-v10.html
|
||||
//
|
||||
// The Apache License v2.0 is available at
|
||||
// http://www.opensource.org/licenses/apache2.0.php
|
||||
//
|
||||
// You may elect to redistribute this code under either of these licenses.
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.rhttp.gateway;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Enumeration;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.eclipse.jetty.rhttp.client.RHTTPRequest;
|
||||
import org.eclipse.jetty.util.log.Log;
|
||||
import org.eclipse.jetty.util.log.Logger;
|
||||
|
||||
/**
|
||||
* Default implementation of {@link Gateway}.
|
||||
*
|
||||
* @version $Revision$ $Date$
|
||||
*/
|
||||
public class StandardGateway implements Gateway
|
||||
{
|
||||
private final Logger logger = Log.getLogger(getClass().toString());
|
||||
private final ConcurrentMap<String, ClientDelegate> clients = new ConcurrentHashMap<String, ClientDelegate>();
|
||||
private final ConcurrentMap<Integer, ExternalRequest> requests = new ConcurrentHashMap<Integer, ExternalRequest>();
|
||||
private final AtomicInteger requestIds = new AtomicInteger();
|
||||
private volatile long gatewayTimeout=20000;
|
||||
private volatile long externalTimeout=60000;
|
||||
|
||||
public long getGatewayTimeout()
|
||||
{
|
||||
return gatewayTimeout;
|
||||
}
|
||||
|
||||
public void setGatewayTimeout(long timeout)
|
||||
{
|
||||
this.gatewayTimeout = timeout;
|
||||
}
|
||||
|
||||
public long getExternalTimeout()
|
||||
{
|
||||
return externalTimeout;
|
||||
}
|
||||
|
||||
public void setExternalTimeout(long externalTimeout)
|
||||
{
|
||||
this.externalTimeout = externalTimeout;
|
||||
}
|
||||
|
||||
public ClientDelegate getClientDelegate(String targetId)
|
||||
{
|
||||
return clients.get(targetId);
|
||||
}
|
||||
|
||||
public ClientDelegate newClientDelegate(String targetId)
|
||||
{
|
||||
StandardClientDelegate client = new StandardClientDelegate(targetId);
|
||||
client.setTimeout(getGatewayTimeout());
|
||||
return client;
|
||||
}
|
||||
|
||||
public ClientDelegate addClientDelegate(String targetId, ClientDelegate client)
|
||||
{
|
||||
return clients.putIfAbsent(targetId, client);
|
||||
}
|
||||
|
||||
public ClientDelegate removeClientDelegate(String targetId)
|
||||
{
|
||||
return clients.remove(targetId);
|
||||
}
|
||||
|
||||
public ExternalRequest newExternalRequest(HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws IOException
|
||||
{
|
||||
int requestId = requestIds.incrementAndGet();
|
||||
RHTTPRequest request = convertHttpRequest(requestId, httpRequest);
|
||||
StandardExternalRequest gatewayRequest = new StandardExternalRequest(request, httpRequest, httpResponse, this);
|
||||
gatewayRequest.setTimeout(getExternalTimeout());
|
||||
return gatewayRequest;
|
||||
}
|
||||
|
||||
protected RHTTPRequest convertHttpRequest(int requestId, HttpServletRequest httpRequest) throws IOException
|
||||
{
|
||||
Map<String, String> headers = new HashMap<String, String>();
|
||||
for (Enumeration headerNames = httpRequest.getHeaderNames(); headerNames.hasMoreElements();)
|
||||
{
|
||||
String name = (String)headerNames.nextElement();
|
||||
// TODO: improve by supporting getHeaders(name)
|
||||
String value = httpRequest.getHeader(name);
|
||||
headers.put(name, value);
|
||||
}
|
||||
|
||||
byte[] body = Utils.read(httpRequest.getInputStream());
|
||||
return new RHTTPRequest(requestId, httpRequest.getMethod(), httpRequest.getRequestURI(), headers, body);
|
||||
}
|
||||
|
||||
public ExternalRequest addExternalRequest(int requestId, ExternalRequest externalRequest)
|
||||
{
|
||||
ExternalRequest existing = requests.putIfAbsent(requestId, externalRequest);
|
||||
if (existing == null)
|
||||
logger.debug("Added external request {}/{} - {}", new Object[]{requestId, requests.size(), externalRequest});
|
||||
return existing;
|
||||
}
|
||||
|
||||
public ExternalRequest removeExternalRequest(int requestId)
|
||||
{
|
||||
ExternalRequest externalRequest = requests.remove(requestId);
|
||||
if (externalRequest != null)
|
||||
logger.debug("Removed external request {}/{} - {}", new Object[]{requestId, requests.size(), externalRequest});
|
||||
return externalRequest;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
|
||||
// ------------------------------------------------------------------------
|
||||
// All rights reserved. This program and the accompanying materials
|
||||
// are made available under the terms of the Eclipse Public License v1.0
|
||||
// and Apache License v2.0 which accompanies this distribution.
|
||||
//
|
||||
// The Eclipse Public License is available at
|
||||
// http://www.eclipse.org/legal/epl-v10.html
|
||||
//
|
||||
// The Apache License v2.0 is available at
|
||||
// http://www.opensource.org/licenses/apache2.0.php
|
||||
//
|
||||
// You may elect to redistribute this code under either of these licenses.
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.rhttp.gateway;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
/**
|
||||
* <p>This implementation retrieves the targetId from the request URI following this pattern:</p>
|
||||
* <pre>
|
||||
* /contextPath/servletPath/<targetId>/other/paths
|
||||
* </pre>
|
||||
* @version $Revision$ $Date$
|
||||
*/
|
||||
public class StandardTargetIdRetriever implements TargetIdRetriever
|
||||
{
|
||||
public String retrieveTargetId(HttpServletRequest httpRequest)
|
||||
{
|
||||
String uri = httpRequest.getRequestURI();
|
||||
String path = uri.substring(httpRequest.getServletPath().length());
|
||||
String[] segments = path.split("/");
|
||||
if (segments.length < 2) return null;
|
||||
return segments[1];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
|
||||
// ------------------------------------------------------------------------
|
||||
// All rights reserved. This program and the accompanying materials
|
||||
// are made available under the terms of the Eclipse Public License v1.0
|
||||
// and Apache License v2.0 which accompanies this distribution.
|
||||
//
|
||||
// The Eclipse Public License is available at
|
||||
// http://www.eclipse.org/legal/epl-v10.html
|
||||
//
|
||||
// The Apache License v2.0 is available at
|
||||
// http://www.opensource.org/licenses/apache2.0.php
|
||||
//
|
||||
// You may elect to redistribute this code under either of these licenses.
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.rhttp.gateway;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
/**
|
||||
* <p>Implementations should retrieve a <em>targetId</em> from an external request.</p>
|
||||
* <p>Implementations of this class may return a fixed value, or inspect the request
|
||||
* looking for URL patterns (e.g. "/<targetId>/resource.jsp"), or looking for request
|
||||
* parameters (e.g. "/resource.jsp?targetId=<targetId>), or looking for virtual host
|
||||
* naming patterns (e.g. "http://<targetId>.host.com/resource.jsp"), etc.</p>
|
||||
*
|
||||
* @version $Revision$ $Date$
|
||||
*/
|
||||
public interface TargetIdRetriever
|
||||
{
|
||||
/**
|
||||
* Extracts and returns the targetId.
|
||||
* @param httpRequest the external request from where the targetId could be extracted
|
||||
* @return the extracted targetId
|
||||
*/
|
||||
public String retrieveTargetId(HttpServletRequest httpRequest);
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
|
||||
// ------------------------------------------------------------------------
|
||||
// All rights reserved. This program and the accompanying materials
|
||||
// are made available under the terms of the Eclipse Public License v1.0
|
||||
// and Apache License v2.0 which accompanies this distribution.
|
||||
//
|
||||
// The Eclipse Public License is available at
|
||||
// http://www.eclipse.org/legal/epl-v10.html
|
||||
//
|
||||
// The Apache License v2.0 is available at
|
||||
// http://www.opensource.org/licenses/apache2.0.php
|
||||
//
|
||||
// You may elect to redistribute this code under either of these licenses.
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.rhttp.gateway;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* @version $Revision$ $Date$
|
||||
*/
|
||||
class Utils
|
||||
{
|
||||
static byte[] read(InputStream input) throws IOException
|
||||
{
|
||||
ByteArrayOutputStream body = new ByteArrayOutputStream();
|
||||
byte[] buffer = new byte[1024];
|
||||
int read;
|
||||
while ((read = input.read(buffer)) >= 0)
|
||||
body.write(buffer, 0, read);
|
||||
body.close();
|
||||
return body.toByteArray();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,115 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
|
||||
// ------------------------------------------------------------------------
|
||||
// All rights reserved. This program and the accompanying materials
|
||||
// are made available under the terms of the Eclipse Public License v1.0
|
||||
// and Apache License v2.0 which accompanies this distribution.
|
||||
//
|
||||
// The Eclipse Public License is available at
|
||||
// http://www.eclipse.org/legal/epl-v10.html
|
||||
//
|
||||
// The Apache License v2.0 is available at
|
||||
// http://www.opensource.org/licenses/apache2.0.php
|
||||
//
|
||||
// You may elect to redistribute this code under either of these licenses.
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.rhttp.gateway;
|
||||
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import junit.framework.TestCase;
|
||||
|
||||
import org.eclipse.jetty.client.Address;
|
||||
import org.eclipse.jetty.client.HttpClient;
|
||||
import org.eclipse.jetty.rhttp.client.ClientListener;
|
||||
import org.eclipse.jetty.rhttp.client.JettyClient;
|
||||
import org.eclipse.jetty.rhttp.client.RHTTPClient;
|
||||
import org.eclipse.jetty.rhttp.gateway.GatewayServer;
|
||||
import org.eclipse.jetty.rhttp.gateway.StandardGateway;
|
||||
import org.eclipse.jetty.server.Connector;
|
||||
import org.eclipse.jetty.server.nio.SelectChannelConnector;
|
||||
|
||||
|
||||
/**
|
||||
* @version $Revision$ $Date$
|
||||
*/
|
||||
public class ClientTimeoutTest extends TestCase
|
||||
{
|
||||
public void testClientTimeout() throws Exception
|
||||
{
|
||||
GatewayServer server = new GatewayServer();
|
||||
Connector connector = new SelectChannelConnector();
|
||||
server.addConnector(connector);
|
||||
final long clientTimeout = 2000L;
|
||||
server.getConnectorServlet().setInitParameter("clientTimeout",""+clientTimeout);
|
||||
final long gatewayTimeout = 4000L;
|
||||
((StandardGateway)server.getGateway()).setGatewayTimeout(gatewayTimeout);
|
||||
server.start();
|
||||
try
|
||||
{
|
||||
Address address = new Address("localhost", connector.getLocalPort());
|
||||
|
||||
HttpClient httpClient = new HttpClient();
|
||||
httpClient.setConnectorType(HttpClient.CONNECTOR_SELECT_CHANNEL);
|
||||
httpClient.start();
|
||||
try
|
||||
{
|
||||
String targetId = "1";
|
||||
final RHTTPClient client = new JettyClient(httpClient, address, server.getContext().getContextPath()+GatewayServer.DFT_CONNECT_PATH, targetId)
|
||||
{
|
||||
private final AtomicInteger connects = new AtomicInteger();
|
||||
|
||||
@Override
|
||||
protected void asyncConnect()
|
||||
{
|
||||
if (connects.incrementAndGet() == 2)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Wait here instead of connecting, so that the client expires on the server
|
||||
Thread.sleep(clientTimeout * 2);
|
||||
}
|
||||
catch (InterruptedException x)
|
||||
{
|
||||
throw new RuntimeException(x);
|
||||
}
|
||||
}
|
||||
super.asyncConnect();
|
||||
}
|
||||
};
|
||||
|
||||
final CountDownLatch connectLatch = new CountDownLatch(1);
|
||||
client.addClientListener(new ClientListener.Adapter()
|
||||
{
|
||||
@Override
|
||||
public void connectRequired()
|
||||
{
|
||||
connectLatch.countDown();
|
||||
}
|
||||
});
|
||||
client.connect();
|
||||
try
|
||||
{
|
||||
assertTrue(connectLatch.await(gatewayTimeout + clientTimeout * 3, TimeUnit.MILLISECONDS));
|
||||
}
|
||||
finally
|
||||
{
|
||||
client.disconnect();
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
httpClient.stop();
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
server.stop();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,93 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
|
||||
// ------------------------------------------------------------------------
|
||||
// All rights reserved. This program and the accompanying materials
|
||||
// are made available under the terms of the Eclipse Public License v1.0
|
||||
// and Apache License v2.0 which accompanies this distribution.
|
||||
//
|
||||
// The Eclipse Public License is available at
|
||||
// http://www.eclipse.org/legal/epl-v10.html
|
||||
//
|
||||
// The Apache License v2.0 is available at
|
||||
// http://www.opensource.org/licenses/apache2.0.php
|
||||
//
|
||||
// You may elect to redistribute this code under either of these licenses.
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.rhttp.gateway;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import junit.framework.TestCase;
|
||||
|
||||
import org.eclipse.jetty.client.Address;
|
||||
import org.eclipse.jetty.client.HttpClient;
|
||||
import org.eclipse.jetty.rhttp.client.JettyClient;
|
||||
import org.eclipse.jetty.rhttp.client.RHTTPClient;
|
||||
import org.eclipse.jetty.rhttp.gateway.GatewayServer;
|
||||
import org.eclipse.jetty.server.Connector;
|
||||
import org.eclipse.jetty.server.nio.SelectChannelConnector;
|
||||
|
||||
|
||||
/**
|
||||
* @version $Revision$ $Date$
|
||||
*/
|
||||
public class DisconnectClientTest extends TestCase
|
||||
{
|
||||
public void testDifferentClientDisconnects() throws Exception
|
||||
{
|
||||
GatewayServer server = new GatewayServer();
|
||||
Connector connector = new SelectChannelConnector();
|
||||
server.addConnector(connector);
|
||||
server.start();
|
||||
try
|
||||
{
|
||||
Address address = new Address("localhost", connector.getLocalPort());
|
||||
|
||||
HttpClient httpClient = new HttpClient();
|
||||
httpClient.setConnectorType(HttpClient.CONNECTOR_SELECT_CHANNEL);
|
||||
httpClient.start();
|
||||
try
|
||||
{
|
||||
final CountDownLatch latch = new CountDownLatch(1);
|
||||
String targetId = "1";
|
||||
final RHTTPClient client1 = new JettyClient(httpClient, address, server.getContext().getContextPath()+GatewayServer.DFT_CONNECT_PATH, targetId)
|
||||
{
|
||||
@Override
|
||||
protected void connectComplete(byte[] responseContent) throws IOException
|
||||
{
|
||||
// If the other client can disconnect this one, this method is called soon after it disconnected
|
||||
latch.countDown();
|
||||
super.connectComplete(responseContent);
|
||||
}
|
||||
};
|
||||
client1.connect();
|
||||
try
|
||||
{
|
||||
final RHTTPClient client2 = new JettyClient(httpClient, address, server.getContext().getContextPath()+GatewayServer.DFT_CONNECT_PATH, targetId);
|
||||
// Disconnect client 2, this should not disconnect client1
|
||||
client2.disconnect();
|
||||
|
||||
// We want the await() to expire, it means it has not disconnected
|
||||
assertFalse(latch.await(1000, TimeUnit.MILLISECONDS));
|
||||
}
|
||||
finally
|
||||
{
|
||||
client1.disconnect();
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
httpClient.stop();
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
server.stop();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
|
||||
// ------------------------------------------------------------------------
|
||||
// All rights reserved. This program and the accompanying materials
|
||||
// are made available under the terms of the Eclipse Public License v1.0
|
||||
// and Apache License v2.0 which accompanies this distribution.
|
||||
//
|
||||
// The Eclipse Public License is available at
|
||||
// http://www.eclipse.org/legal/epl-v10.html
|
||||
//
|
||||
// The Apache License v2.0 is available at
|
||||
// http://www.opensource.org/licenses/apache2.0.php
|
||||
//
|
||||
// You may elect to redistribute this code under either of these licenses.
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.rhttp.gateway;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import junit.framework.TestCase;
|
||||
|
||||
import org.eclipse.jetty.client.Address;
|
||||
import org.eclipse.jetty.client.HttpClient;
|
||||
import org.eclipse.jetty.rhttp.client.JettyClient;
|
||||
import org.eclipse.jetty.rhttp.client.RHTTPClient;
|
||||
import org.eclipse.jetty.rhttp.gateway.GatewayServer;
|
||||
import org.eclipse.jetty.server.Connector;
|
||||
import org.eclipse.jetty.server.nio.SelectChannelConnector;
|
||||
|
||||
|
||||
/**
|
||||
* @version $Revision$ $Date$
|
||||
*/
|
||||
public class DuplicateClientTest extends TestCase
|
||||
{
|
||||
public void testDuplicateClient() throws Exception
|
||||
{
|
||||
GatewayServer server = new GatewayServer();
|
||||
Connector connector = new SelectChannelConnector();
|
||||
server.addConnector(connector);
|
||||
server.start();
|
||||
try
|
||||
{
|
||||
Address address = new Address("localhost", connector.getLocalPort());
|
||||
|
||||
HttpClient httpClient = new HttpClient();
|
||||
httpClient.setConnectorType(HttpClient.CONNECTOR_SELECT_CHANNEL);
|
||||
httpClient.start();
|
||||
try
|
||||
{
|
||||
String targetId = "1";
|
||||
final RHTTPClient client1 = new JettyClient(httpClient, address, server.getContext().getContextPath()+GatewayServer.DFT_CONNECT_PATH, targetId);
|
||||
client1.connect();
|
||||
try
|
||||
{
|
||||
final RHTTPClient client2 = new JettyClient(httpClient, address, server.getContext().getContextPath()+GatewayServer.DFT_CONNECT_PATH, targetId);
|
||||
try
|
||||
{
|
||||
client2.connect();
|
||||
fail();
|
||||
}
|
||||
catch (IOException x)
|
||||
{
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
client1.disconnect();
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
httpClient.stop();
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
server.stop();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,186 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
|
||||
// ------------------------------------------------------------------------
|
||||
// All rights reserved. This program and the accompanying materials
|
||||
// are made available under the terms of the Eclipse Public License v1.0
|
||||
// and Apache License v2.0 which accompanies this distribution.
|
||||
//
|
||||
// The Eclipse Public License is available at
|
||||
// http://www.eclipse.org/legal/epl-v10.html
|
||||
//
|
||||
// The Apache License v2.0 is available at
|
||||
// http://www.opensource.org/licenses/apache2.0.php
|
||||
//
|
||||
// You may elect to redistribute this code under either of these licenses.
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.rhttp.gateway;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URLEncoder;
|
||||
import java.util.HashMap;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import junit.framework.TestCase;
|
||||
|
||||
import org.eclipse.jetty.client.Address;
|
||||
import org.eclipse.jetty.client.ContentExchange;
|
||||
import org.eclipse.jetty.client.HttpClient;
|
||||
import org.eclipse.jetty.client.HttpExchange;
|
||||
import org.eclipse.jetty.http.HttpMethods;
|
||||
import org.eclipse.jetty.io.ByteArrayBuffer;
|
||||
import org.eclipse.jetty.rhttp.client.JettyClient;
|
||||
import org.eclipse.jetty.rhttp.client.RHTTPClient;
|
||||
import org.eclipse.jetty.rhttp.client.RHTTPListener;
|
||||
import org.eclipse.jetty.rhttp.client.RHTTPRequest;
|
||||
import org.eclipse.jetty.rhttp.client.RHTTPResponse;
|
||||
import org.eclipse.jetty.rhttp.gateway.ExternalRequest;
|
||||
import org.eclipse.jetty.rhttp.gateway.Gateway;
|
||||
import org.eclipse.jetty.rhttp.gateway.GatewayServer;
|
||||
import org.eclipse.jetty.rhttp.gateway.StandardGateway;
|
||||
import org.eclipse.jetty.server.nio.SelectChannelConnector;
|
||||
|
||||
|
||||
/**
|
||||
* @version $Revision$ $Date$
|
||||
*/
|
||||
public class ExternalRequestNotSuspendedTest extends TestCase
|
||||
{
|
||||
public void testExternalRequestNotSuspended() throws Exception
|
||||
{
|
||||
final CountDownLatch respondLatch = new CountDownLatch(1);
|
||||
final CountDownLatch suspendLatch = new CountDownLatch(1);
|
||||
final AtomicBoolean suspended = new AtomicBoolean(true);
|
||||
GatewayServer server = new GatewayServer()
|
||||
{
|
||||
@Override
|
||||
protected Gateway createGateway()
|
||||
{
|
||||
StandardGateway gateway = new StandardGateway()
|
||||
{
|
||||
@Override
|
||||
public ExternalRequest newExternalRequest(HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws IOException
|
||||
{
|
||||
return new SlowToSuspendExternalRequest(super.newExternalRequest(httpRequest, httpResponse), respondLatch, suspendLatch, suspended);
|
||||
}
|
||||
};
|
||||
return gateway;
|
||||
}
|
||||
};
|
||||
SelectChannelConnector connector = new SelectChannelConnector();
|
||||
server.addConnector(connector);
|
||||
server.start();
|
||||
try
|
||||
{
|
||||
Address address = new Address("localhost", connector.getLocalPort());
|
||||
|
||||
HttpClient httpClient = new HttpClient();
|
||||
httpClient.setConnectorType(HttpClient.CONNECTOR_SELECT_CHANNEL);
|
||||
httpClient.start();
|
||||
try
|
||||
{
|
||||
String targetId = "1";
|
||||
final RHTTPClient client = new JettyClient(httpClient, address, server.getContext().getContextPath()+GatewayServer.DFT_CONNECT_PATH, targetId);
|
||||
final AtomicReference<Exception> exception = new AtomicReference<Exception>();
|
||||
client.addListener(new RHTTPListener()
|
||||
{
|
||||
public void onRequest(RHTTPRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
RHTTPResponse response = new RHTTPResponse(request.getId(), 200, "OK", new HashMap<String, String>(), request.getBody());
|
||||
client.deliver(response);
|
||||
}
|
||||
catch (Exception x)
|
||||
{
|
||||
exception.set(x);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
client.connect();
|
||||
try
|
||||
{
|
||||
// Make a request to the gateway and check response
|
||||
ContentExchange exchange = new ContentExchange(true);
|
||||
exchange.setMethod(HttpMethods.POST);
|
||||
exchange.setAddress(address);
|
||||
exchange.setURI(server.getContext().getContextPath()+GatewayServer.DFT_EXT_PATH + "/" + URLEncoder.encode(targetId, "UTF-8"));
|
||||
String requestContent = "body";
|
||||
exchange.setRequestContent(new ByteArrayBuffer(requestContent.getBytes("UTF-8")));
|
||||
httpClient.send(exchange);
|
||||
|
||||
int status = exchange.waitForDone();
|
||||
assertEquals(HttpExchange.STATUS_COMPLETED, status);
|
||||
assertEquals(HttpServletResponse.SC_OK, exchange.getResponseStatus());
|
||||
assertNull(exception.get());
|
||||
|
||||
suspendLatch.await();
|
||||
assertFalse(suspended.get());
|
||||
}
|
||||
finally
|
||||
{
|
||||
client.disconnect();
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
httpClient.stop();
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
server.stop();
|
||||
}
|
||||
}
|
||||
|
||||
private class SlowToSuspendExternalRequest implements ExternalRequest
|
||||
{
|
||||
private final ExternalRequest delegate;
|
||||
private final CountDownLatch respondLatch;
|
||||
private final CountDownLatch suspendLatch;
|
||||
private final AtomicBoolean suspended;
|
||||
|
||||
private SlowToSuspendExternalRequest(ExternalRequest delegate, CountDownLatch respondLatch, CountDownLatch suspendLatch, AtomicBoolean suspended)
|
||||
{
|
||||
this.delegate = delegate;
|
||||
this.respondLatch = respondLatch;
|
||||
this.suspendLatch = suspendLatch;
|
||||
this.suspended = suspended;
|
||||
}
|
||||
|
||||
public boolean suspend()
|
||||
{
|
||||
try
|
||||
{
|
||||
respondLatch.await();
|
||||
boolean result = delegate.suspend();
|
||||
suspended.set(result);
|
||||
suspendLatch.countDown();
|
||||
return result;
|
||||
}
|
||||
catch (InterruptedException x)
|
||||
{
|
||||
throw new AssertionError(x);
|
||||
}
|
||||
}
|
||||
|
||||
public void respond(RHTTPResponse response) throws IOException
|
||||
{
|
||||
delegate.respond(response);
|
||||
respondLatch.countDown();
|
||||
}
|
||||
|
||||
public RHTTPRequest getRequest()
|
||||
{
|
||||
return delegate.getRequest();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,126 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
|
||||
// ------------------------------------------------------------------------
|
||||
// All rights reserved. This program and the accompanying materials
|
||||
// are made available under the terms of the Eclipse Public License v1.0
|
||||
// and Apache License v2.0 which accompanies this distribution.
|
||||
//
|
||||
// The Eclipse Public License is available at
|
||||
// http://www.eclipse.org/legal/epl-v10.html
|
||||
//
|
||||
// The Apache License v2.0 is available at
|
||||
// http://www.opensource.org/licenses/apache2.0.php
|
||||
//
|
||||
// You may elect to redistribute this code under either of these licenses.
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.rhttp.gateway;
|
||||
|
||||
import java.net.URLEncoder;
|
||||
import java.util.HashMap;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import junit.framework.TestCase;
|
||||
|
||||
import org.eclipse.jetty.client.Address;
|
||||
import org.eclipse.jetty.client.ContentExchange;
|
||||
import org.eclipse.jetty.client.HttpClient;
|
||||
import org.eclipse.jetty.client.HttpExchange;
|
||||
import org.eclipse.jetty.http.HttpMethods;
|
||||
import org.eclipse.jetty.io.ByteArrayBuffer;
|
||||
import org.eclipse.jetty.rhttp.client.JettyClient;
|
||||
import org.eclipse.jetty.rhttp.client.RHTTPClient;
|
||||
import org.eclipse.jetty.rhttp.client.RHTTPListener;
|
||||
import org.eclipse.jetty.rhttp.client.RHTTPRequest;
|
||||
import org.eclipse.jetty.rhttp.client.RHTTPResponse;
|
||||
import org.eclipse.jetty.rhttp.gateway.GatewayServer;
|
||||
import org.eclipse.jetty.rhttp.gateway.StandardGateway;
|
||||
import org.eclipse.jetty.server.nio.SelectChannelConnector;
|
||||
|
||||
|
||||
/**
|
||||
* @version $Revision$ $Date$
|
||||
*/
|
||||
public class ExternalTimeoutTest extends TestCase
|
||||
{
|
||||
public void testExternalTimeout() throws Exception
|
||||
{
|
||||
GatewayServer server = new GatewayServer();
|
||||
SelectChannelConnector connector = new SelectChannelConnector();
|
||||
server.addConnector(connector);
|
||||
final long externalTimeout = 5000L;
|
||||
((StandardGateway)server.getGateway()).setExternalTimeout(externalTimeout);
|
||||
server.start();
|
||||
try
|
||||
{
|
||||
Address address = new Address("localhost", connector.getLocalPort());
|
||||
|
||||
HttpClient httpClient = new HttpClient();
|
||||
httpClient.setConnectorType(HttpClient.CONNECTOR_SELECT_CHANNEL);
|
||||
httpClient.start();
|
||||
try
|
||||
{
|
||||
String targetId = "1";
|
||||
final RHTTPClient client = new JettyClient(httpClient, address, server.getContext().getContextPath()+GatewayServer.DFT_CONNECT_PATH, targetId);
|
||||
final AtomicReference<Integer> requestId = new AtomicReference<Integer>();
|
||||
final AtomicReference<Exception> exceptionRef = new AtomicReference<Exception>();
|
||||
client.addListener(new RHTTPListener()
|
||||
{
|
||||
public void onRequest(RHTTPRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Save the request id
|
||||
requestId.set(request.getId());
|
||||
|
||||
// Wait until external timeout expires
|
||||
Thread.sleep(externalTimeout + 1000L);
|
||||
RHTTPResponse response = new RHTTPResponse(request.getId(), 200, "OK", new HashMap<String, String>(), request.getBody());
|
||||
client.deliver(response);
|
||||
}
|
||||
catch (Exception x)
|
||||
{
|
||||
exceptionRef.set(x);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
client.connect();
|
||||
try
|
||||
{
|
||||
// Make a request to the gateway and check response
|
||||
ContentExchange exchange = new ContentExchange(true);
|
||||
exchange.setMethod(HttpMethods.POST);
|
||||
exchange.setAddress(address);
|
||||
exchange.setURI(server.getContext().getContextPath()+GatewayServer.DFT_EXT_PATH + "/" + URLEncoder.encode(targetId, "UTF-8"));
|
||||
String requestContent = "body";
|
||||
exchange.setRequestContent(new ByteArrayBuffer(requestContent.getBytes("UTF-8")));
|
||||
httpClient.send(exchange);
|
||||
|
||||
int status = exchange.waitForDone();
|
||||
assertEquals(HttpExchange.STATUS_COMPLETED, status);
|
||||
assertEquals(HttpServletResponse.SC_GATEWAY_TIMEOUT, exchange.getResponseStatus());
|
||||
assertNull(exceptionRef.get());
|
||||
|
||||
assertNull(server.getGateway().removeExternalRequest(requestId.get()));
|
||||
}
|
||||
finally
|
||||
{
|
||||
client.disconnect();
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
httpClient.stop();
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
server.stop();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,109 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
|
||||
// ------------------------------------------------------------------------
|
||||
// All rights reserved. This program and the accompanying materials
|
||||
// are made available under the terms of the Eclipse Public License v1.0
|
||||
// and Apache License v2.0 which accompanies this distribution.
|
||||
//
|
||||
// The Eclipse Public License is available at
|
||||
// http://www.eclipse.org/legal/epl-v10.html
|
||||
//
|
||||
// The Apache License v2.0 is available at
|
||||
// http://www.opensource.org/licenses/apache2.0.php
|
||||
//
|
||||
// You may elect to redistribute this code under either of these licenses.
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.rhttp.gateway;
|
||||
|
||||
import java.util.HashMap;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
import org.eclipse.jetty.client.Address;
|
||||
import org.eclipse.jetty.client.HttpClient;
|
||||
import org.eclipse.jetty.rhttp.client.JettyClient;
|
||||
import org.eclipse.jetty.rhttp.client.RHTTPClient;
|
||||
import org.eclipse.jetty.rhttp.client.RHTTPListener;
|
||||
import org.eclipse.jetty.rhttp.client.RHTTPRequest;
|
||||
import org.eclipse.jetty.rhttp.client.RHTTPResponse;
|
||||
import org.eclipse.jetty.rhttp.gateway.GatewayServer;
|
||||
import org.eclipse.jetty.rhttp.gateway.TargetIdRetriever;
|
||||
import org.eclipse.jetty.server.Connector;
|
||||
import org.eclipse.jetty.server.nio.SelectChannelConnector;
|
||||
|
||||
|
||||
/**
|
||||
* @version $Revision$ $Date$
|
||||
*/
|
||||
public class GatewayEchoServer
|
||||
{
|
||||
private volatile GatewayServer server;
|
||||
private volatile Address address;
|
||||
private volatile String uri;
|
||||
private volatile HttpClient httpClient;
|
||||
private volatile RHTTPClient client;
|
||||
|
||||
public void start() throws Exception
|
||||
{
|
||||
server = new GatewayServer();
|
||||
Connector connector = new SelectChannelConnector();
|
||||
server.addConnector(connector);
|
||||
server.setTargetIdRetriever(new EchoTargetIdRetriever());
|
||||
server.start();
|
||||
server.dumpStdErr();
|
||||
address = new Address("localhost", connector.getLocalPort());
|
||||
uri = server.getContext().getContextPath()+GatewayServer.DFT_EXT_PATH;
|
||||
|
||||
httpClient = new HttpClient();
|
||||
httpClient.setConnectorType(HttpClient.CONNECTOR_SELECT_CHANNEL);
|
||||
httpClient.start();
|
||||
|
||||
client = new JettyClient(httpClient, new Address("localhost", connector.getLocalPort()), server.getContext().getContextPath()+GatewayServer.DFT_CONNECT_PATH, "echo");
|
||||
client.addListener(new EchoListener(client));
|
||||
client.connect();
|
||||
}
|
||||
|
||||
public void stop() throws Exception
|
||||
{
|
||||
client.disconnect();
|
||||
httpClient.stop();
|
||||
server.stop();
|
||||
}
|
||||
|
||||
public Address getAddress()
|
||||
{
|
||||
return address;
|
||||
}
|
||||
|
||||
public String getURI()
|
||||
{
|
||||
return uri;
|
||||
}
|
||||
|
||||
public static class EchoTargetIdRetriever implements TargetIdRetriever
|
||||
{
|
||||
public String retrieveTargetId(HttpServletRequest httpRequest)
|
||||
{
|
||||
return "echo";
|
||||
}
|
||||
}
|
||||
|
||||
private static class EchoListener implements RHTTPListener
|
||||
{
|
||||
private final RHTTPClient client;
|
||||
|
||||
public EchoListener(RHTTPClient client)
|
||||
{
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
public void onRequest(RHTTPRequest request) throws Exception
|
||||
{
|
||||
RHTTPResponse response = new RHTTPResponse(request.getId(), 200, "OK", new HashMap<String, String>(), request.getBody());
|
||||
client.deliver(response);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,77 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
|
||||
// ------------------------------------------------------------------------
|
||||
// All rights reserved. This program and the accompanying materials
|
||||
// are made available under the terms of the Eclipse Public License v1.0
|
||||
// and Apache License v2.0 which accompanies this distribution.
|
||||
//
|
||||
// The Eclipse Public License is available at
|
||||
// http://www.eclipse.org/legal/epl-v10.html
|
||||
//
|
||||
// The Apache License v2.0 is available at
|
||||
// http://www.opensource.org/licenses/apache2.0.php
|
||||
//
|
||||
// You may elect to redistribute this code under either of these licenses.
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.rhttp.gateway;
|
||||
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import junit.framework.TestCase;
|
||||
|
||||
import org.eclipse.jetty.client.ContentExchange;
|
||||
import org.eclipse.jetty.client.HttpClient;
|
||||
import org.eclipse.jetty.client.HttpExchange;
|
||||
import org.eclipse.jetty.http.HttpMethods;
|
||||
import org.eclipse.jetty.io.ByteArrayBuffer;
|
||||
|
||||
/**
|
||||
* @version $Revision$ $Date$
|
||||
*/
|
||||
public class GatewayEchoTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* Tests that the basic functionality of the gateway works,
|
||||
* by issuing a request and by replying with the same body.
|
||||
*
|
||||
* @throws Exception in case of test exceptions
|
||||
*/
|
||||
public void testEcho() throws Exception
|
||||
{
|
||||
GatewayEchoServer server = new GatewayEchoServer();
|
||||
server.start();
|
||||
try
|
||||
{
|
||||
HttpClient httpClient = new HttpClient();
|
||||
httpClient.setConnectorType(HttpClient.CONNECTOR_SELECT_CHANNEL);
|
||||
httpClient.start();
|
||||
try
|
||||
{
|
||||
// Make a request to the gateway and check response
|
||||
ContentExchange exchange = new ContentExchange(true);
|
||||
exchange.setMethod(HttpMethods.POST);
|
||||
exchange.setAddress(server.getAddress());
|
||||
exchange.setURI(server.getURI() + "/");
|
||||
String requestBody = "body";
|
||||
exchange.setRequestContent(new ByteArrayBuffer(requestBody.getBytes("UTF-8")));
|
||||
httpClient.send(exchange);
|
||||
int status = exchange.waitForDone();
|
||||
assertEquals(HttpExchange.STATUS_COMPLETED, status);
|
||||
assertEquals(HttpServletResponse.SC_OK, exchange.getResponseStatus());
|
||||
String responseContent = exchange.getResponseContent();
|
||||
assertEquals(responseContent, requestBody);
|
||||
}
|
||||
finally
|
||||
{
|
||||
httpClient.stop();
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
server.stop();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,202 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
|
||||
// ------------------------------------------------------------------------
|
||||
// All rights reserved. This program and the accompanying materials
|
||||
// are made available under the terms of the Eclipse Public License v1.0
|
||||
// and Apache License v2.0 which accompanies this distribution.
|
||||
//
|
||||
// The Eclipse Public License is available at
|
||||
// http://www.eclipse.org/legal/epl-v10.html
|
||||
//
|
||||
// The Apache License v2.0 is available at
|
||||
// http://www.opensource.org/licenses/apache2.0.php
|
||||
//
|
||||
// You may elect to redistribute this code under either of these licenses.
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.rhttp.gateway;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import junit.framework.TestCase;
|
||||
|
||||
import org.eclipse.jetty.client.ContentExchange;
|
||||
import org.eclipse.jetty.client.HttpClient;
|
||||
import org.eclipse.jetty.http.HttpMethods;
|
||||
import org.eclipse.jetty.io.ByteArrayBuffer;
|
||||
|
||||
/**
|
||||
* @version $Revision$ $Date$
|
||||
*/
|
||||
public class GatewayLoadTest extends TestCase
|
||||
{
|
||||
private final ConcurrentMap<Long, AtomicLong> latencies = new ConcurrentHashMap<Long, AtomicLong>();
|
||||
private final AtomicLong responses = new AtomicLong(0L);
|
||||
private final AtomicLong failures = new AtomicLong(0L);
|
||||
private final AtomicLong minLatency = new AtomicLong(Long.MAX_VALUE);
|
||||
private final AtomicLong maxLatency = new AtomicLong(0L);
|
||||
private final AtomicLong totLatency = new AtomicLong(0L);
|
||||
|
||||
public void testEcho() throws Exception
|
||||
{
|
||||
GatewayEchoServer server = new GatewayEchoServer();
|
||||
server.start();
|
||||
|
||||
HttpClient httpClient = new HttpClient();
|
||||
httpClient.setConnectorType(HttpClient.CONNECTOR_SELECT_CHANNEL);
|
||||
httpClient.start();
|
||||
|
||||
String uri = server.getURI() + "/";
|
||||
|
||||
char[] chars = new char[1024];
|
||||
Arrays.fill(chars, 'x');
|
||||
String requestBody = new String(chars);
|
||||
|
||||
int count = 1000;
|
||||
CountDownLatch latch = new CountDownLatch(count);
|
||||
for (int i = 0; i < count; ++i)
|
||||
{
|
||||
GatewayLoadTestExchange exchange = new GatewayLoadTestExchange(latch);
|
||||
exchange.setMethod(HttpMethods.POST);
|
||||
exchange.setAddress(server.getAddress());
|
||||
exchange.setURI(uri + i);
|
||||
exchange.setRequestContent(new ByteArrayBuffer(requestBody.getBytes("UTF-8")));
|
||||
exchange.setStartNanos(System.nanoTime());
|
||||
httpClient.send(exchange);
|
||||
Thread.sleep(5);
|
||||
}
|
||||
latch.await();
|
||||
printLatencies(count);
|
||||
assertEquals(count, responses.get() + failures.get());
|
||||
}
|
||||
|
||||
private void updateLatencies(long start, long end)
|
||||
{
|
||||
long latency = end - start;
|
||||
|
||||
// Update the latencies using a non-blocking algorithm
|
||||
long oldMinLatency = minLatency.get();
|
||||
while (latency < oldMinLatency)
|
||||
{
|
||||
if (minLatency.compareAndSet(oldMinLatency, latency)) break;
|
||||
oldMinLatency = minLatency.get();
|
||||
}
|
||||
long oldMaxLatency = maxLatency.get();
|
||||
while (latency > oldMaxLatency)
|
||||
{
|
||||
if (maxLatency.compareAndSet(oldMaxLatency, latency)) break;
|
||||
oldMaxLatency = maxLatency.get();
|
||||
}
|
||||
totLatency.addAndGet(latency);
|
||||
|
||||
latencies.putIfAbsent(latency, new AtomicLong(0L));
|
||||
latencies.get(latency).incrementAndGet();
|
||||
}
|
||||
|
||||
public void printLatencies(long expectedCount)
|
||||
{
|
||||
if (latencies.size() > 1)
|
||||
{
|
||||
long maxLatencyBucketFrequency = 0L;
|
||||
long[] latencyBucketFrequencies = new long[20];
|
||||
long latencyRange = maxLatency.get() - minLatency.get();
|
||||
for (Iterator<Map.Entry<Long, AtomicLong>> entries = latencies.entrySet().iterator(); entries.hasNext();)
|
||||
{
|
||||
Map.Entry<Long, AtomicLong> entry = entries.next();
|
||||
long latency = entry.getKey();
|
||||
Long bucketIndex = (latency - minLatency.get()) * latencyBucketFrequencies.length / latencyRange;
|
||||
int index = bucketIndex.intValue() == latencyBucketFrequencies.length ? latencyBucketFrequencies.length - 1 : bucketIndex.intValue();
|
||||
long value = entry.getValue().get();
|
||||
latencyBucketFrequencies[index] += value;
|
||||
if (latencyBucketFrequencies[index] > maxLatencyBucketFrequency) maxLatencyBucketFrequency = latencyBucketFrequencies[index];
|
||||
entries.remove();
|
||||
}
|
||||
|
||||
System.out.println("Messages - Latency Distribution Curve (X axis: Frequency, Y axis: Latency):");
|
||||
for (int i = 0; i < latencyBucketFrequencies.length; i++)
|
||||
{
|
||||
long latencyBucketFrequency = latencyBucketFrequencies[i];
|
||||
int value = Math.round(latencyBucketFrequency * (float) latencyBucketFrequencies.length / maxLatencyBucketFrequency);
|
||||
if (value == latencyBucketFrequencies.length) value = value - 1;
|
||||
for (int j = 0; j < value; ++j) System.out.print(" ");
|
||||
System.out.print("@");
|
||||
for (int j = value + 1; j < latencyBucketFrequencies.length; ++j) System.out.print(" ");
|
||||
System.out.print(" _ ");
|
||||
System.out.print(TimeUnit.NANOSECONDS.toMillis((latencyRange * (i + 1) / latencyBucketFrequencies.length) + minLatency.get()));
|
||||
System.out.print(" (" + latencyBucketFrequency + ")");
|
||||
System.out.println(" ms");
|
||||
}
|
||||
}
|
||||
|
||||
long responseCount = responses.get();
|
||||
System.out.print("Messages success/failed/expected = ");
|
||||
System.out.print(responseCount);
|
||||
System.out.print("/");
|
||||
System.out.print(failures.get());
|
||||
System.out.print("/");
|
||||
System.out.print(expectedCount);
|
||||
System.out.print(" - Latency min/ave/max = ");
|
||||
System.out.print(TimeUnit.NANOSECONDS.toMillis(minLatency.get()) + "/");
|
||||
System.out.print(responseCount == 0 ? "-/" : TimeUnit.NANOSECONDS.toMillis(totLatency.get() / responseCount) + "/");
|
||||
System.out.println(TimeUnit.NANOSECONDS.toMillis(maxLatency.get()) + " ms");
|
||||
}
|
||||
|
||||
private class GatewayLoadTestExchange extends ContentExchange
|
||||
{
|
||||
private final CountDownLatch latch;
|
||||
private volatile long start;
|
||||
|
||||
private GatewayLoadTestExchange(CountDownLatch latch)
|
||||
{
|
||||
super(true);
|
||||
this.latch = latch;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResponseComplete() throws IOException
|
||||
{
|
||||
if (getResponseStatus() == HttpServletResponse.SC_OK)
|
||||
{
|
||||
long end = System.nanoTime();
|
||||
responses.incrementAndGet();
|
||||
updateLatencies(start, end);
|
||||
}
|
||||
else
|
||||
{
|
||||
failures.incrementAndGet();
|
||||
}
|
||||
latch.countDown();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onException(Throwable throwable)
|
||||
{
|
||||
failures.incrementAndGet();
|
||||
latch.countDown();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onExpire()
|
||||
{
|
||||
failures.incrementAndGet();
|
||||
latch.countDown();
|
||||
}
|
||||
|
||||
public void setStartNanos(long value)
|
||||
{
|
||||
start = value;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,130 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
|
||||
// ------------------------------------------------------------------------
|
||||
// All rights reserved. This program and the accompanying materials
|
||||
// are made available under the terms of the Eclipse Public License v1.0
|
||||
// and Apache License v2.0 which accompanies this distribution.
|
||||
//
|
||||
// The Eclipse Public License is available at
|
||||
// http://www.eclipse.org/legal/epl-v10.html
|
||||
//
|
||||
// The Apache License v2.0 is available at
|
||||
// http://www.opensource.org/licenses/apache2.0.php
|
||||
//
|
||||
// You may elect to redistribute this code under either of these licenses.
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.rhttp.gateway;
|
||||
|
||||
import java.net.URLEncoder;
|
||||
import java.util.HashMap;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import junit.framework.TestCase;
|
||||
|
||||
import org.eclipse.jetty.client.Address;
|
||||
import org.eclipse.jetty.client.ContentExchange;
|
||||
import org.eclipse.jetty.client.HttpClient;
|
||||
import org.eclipse.jetty.client.HttpExchange;
|
||||
import org.eclipse.jetty.http.HttpMethods;
|
||||
import org.eclipse.jetty.io.ByteArrayBuffer;
|
||||
import org.eclipse.jetty.rhttp.client.JettyClient;
|
||||
import org.eclipse.jetty.rhttp.client.RHTTPClient;
|
||||
import org.eclipse.jetty.rhttp.client.RHTTPListener;
|
||||
import org.eclipse.jetty.rhttp.client.RHTTPRequest;
|
||||
import org.eclipse.jetty.rhttp.client.RHTTPResponse;
|
||||
import org.eclipse.jetty.rhttp.gateway.GatewayServer;
|
||||
import org.eclipse.jetty.rhttp.gateway.StandardGateway;
|
||||
import org.eclipse.jetty.server.Connector;
|
||||
import org.eclipse.jetty.server.nio.SelectChannelConnector;
|
||||
|
||||
|
||||
/**
|
||||
* @version $Revision$ $Date$
|
||||
*/
|
||||
public class GatewayTimeoutTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* Tests a forwarded request that lasts longer than the gateway timeout.
|
||||
* The gateway client will perform 2 long polls before the forwarded request's response is returned.
|
||||
*
|
||||
* @throws Exception in case of test exceptions
|
||||
*/
|
||||
public void testGatewayTimeout() throws Exception
|
||||
{
|
||||
GatewayServer server = new GatewayServer();
|
||||
Connector connector = new SelectChannelConnector();
|
||||
server.addConnector(connector);
|
||||
final long gatewayTimeout = 5000L;
|
||||
((StandardGateway)server.getGateway()).setGatewayTimeout(gatewayTimeout);
|
||||
final long externalTimeout = gatewayTimeout * 2;
|
||||
((StandardGateway)server.getGateway()).setExternalTimeout(externalTimeout);
|
||||
server.start();
|
||||
try
|
||||
{
|
||||
Address address = new Address("localhost", connector.getLocalPort());
|
||||
|
||||
HttpClient httpClient = new HttpClient();
|
||||
httpClient.setConnectorType(HttpClient.CONNECTOR_SELECT_CHANNEL);
|
||||
httpClient.start();
|
||||
try
|
||||
{
|
||||
String targetId = "1";
|
||||
final RHTTPClient client = new JettyClient(httpClient, address, server.getContext().getContextPath()+GatewayServer.DFT_CONNECT_PATH, targetId);
|
||||
final AtomicReference<Exception> exceptionRef = new AtomicReference<Exception>();
|
||||
client.addListener(new RHTTPListener()
|
||||
{
|
||||
public void onRequest(RHTTPRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Wait until gateway timeout expires
|
||||
Thread.sleep(gatewayTimeout + 1000L);
|
||||
RHTTPResponse response = new RHTTPResponse(request.getId(), 200, "OK", new HashMap<String, String>(), request.getBody());
|
||||
client.deliver(response);
|
||||
}
|
||||
catch (Exception x)
|
||||
{
|
||||
exceptionRef.set(x);
|
||||
}
|
||||
}
|
||||
});
|
||||
client.connect();
|
||||
try
|
||||
{
|
||||
// Make a request to the gateway and check response
|
||||
ContentExchange exchange = new ContentExchange(true);
|
||||
exchange.setMethod(HttpMethods.POST);
|
||||
exchange.setAddress(address);
|
||||
exchange.setURI(server.getContext().getContextPath()+GatewayServer.DFT_EXT_PATH + "/" + URLEncoder.encode(targetId, "UTF-8"));
|
||||
String requestContent = "body";
|
||||
exchange.setRequestContent(new ByteArrayBuffer(requestContent.getBytes("UTF-8")));
|
||||
httpClient.send(exchange);
|
||||
|
||||
int status = exchange.waitForDone();
|
||||
assertEquals(HttpExchange.STATUS_COMPLETED, status);
|
||||
assertEquals(HttpServletResponse.SC_OK, exchange.getResponseStatus());
|
||||
assertNull(exceptionRef.get());
|
||||
String responseContent = exchange.getResponseContent();
|
||||
assertEquals(responseContent, requestContent);
|
||||
}
|
||||
finally
|
||||
{
|
||||
client.disconnect();
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
httpClient.stop();
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
server.stop();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
|
||||
// ------------------------------------------------------------------------
|
||||
// All rights reserved. This program and the accompanying materials
|
||||
// are made available under the terms of the Eclipse Public License v1.0
|
||||
// and Apache License v2.0 which accompanies this distribution.
|
||||
//
|
||||
// The Eclipse Public License is available at
|
||||
// http://www.eclipse.org/legal/epl-v10.html
|
||||
//
|
||||
// The Apache License v2.0 is available at
|
||||
// http://www.opensource.org/licenses/apache2.0.php
|
||||
//
|
||||
// You may elect to redistribute this code under either of these licenses.
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.rhttp.gateway;
|
||||
|
||||
import junit.framework.TestCase;
|
||||
|
||||
import org.eclipse.jetty.client.Address;
|
||||
import org.eclipse.jetty.client.HttpClient;
|
||||
import org.eclipse.jetty.rhttp.client.JettyClient;
|
||||
import org.eclipse.jetty.rhttp.client.RHTTPClient;
|
||||
import org.eclipse.jetty.rhttp.gateway.GatewayServer;
|
||||
import org.eclipse.jetty.rhttp.gateway.StandardGateway;
|
||||
import org.eclipse.jetty.server.nio.SelectChannelConnector;
|
||||
|
||||
|
||||
/**
|
||||
* @version $Revision$ $Date$
|
||||
*/
|
||||
public class HandshakeClientTest extends TestCase
|
||||
{
|
||||
public void testConnectReturnsImmediately() throws Exception
|
||||
{
|
||||
GatewayServer server = new GatewayServer();
|
||||
SelectChannelConnector connector = new SelectChannelConnector();
|
||||
server.addConnector(connector);
|
||||
long gwt=5000L;
|
||||
((StandardGateway)server.getGateway()).setGatewayTimeout(gwt);
|
||||
server.start();
|
||||
try
|
||||
{
|
||||
HttpClient httpClient = new HttpClient();
|
||||
httpClient.setConnectorType(HttpClient.CONNECTOR_SELECT_CHANNEL);
|
||||
httpClient.start();
|
||||
try
|
||||
{
|
||||
RHTTPClient client = new JettyClient(httpClient, new Address("localhost", connector.getLocalPort()), server.getContext().getContextPath()+GatewayServer.DFT_CONNECT_PATH, "test1");
|
||||
long start = System.currentTimeMillis();
|
||||
client.connect();
|
||||
try
|
||||
{
|
||||
long end = System.currentTimeMillis();
|
||||
assertTrue(end - start < gwt / 2);
|
||||
}
|
||||
finally
|
||||
{
|
||||
client.disconnect();
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
httpClient.stop();
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
server.stop();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,107 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
|
||||
// ------------------------------------------------------------------------
|
||||
// All rights reserved. This program and the accompanying materials
|
||||
// are made available under the terms of the Eclipse Public License v1.0
|
||||
// and Apache License v2.0 which accompanies this distribution.
|
||||
//
|
||||
// The Eclipse Public License is available at
|
||||
// http://www.eclipse.org/legal/epl-v10.html
|
||||
//
|
||||
// The Apache License v2.0 is available at
|
||||
// http://www.opensource.org/licenses/apache2.0.php
|
||||
//
|
||||
// You may elect to redistribute this code under either of these licenses.
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.rhttp.gateway;
|
||||
|
||||
import java.lang.reflect.InvocationHandler;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Proxy;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
import org.eclipse.jetty.rhttp.gateway.HostTargetIdRetriever;
|
||||
|
||||
import junit.framework.TestCase;
|
||||
|
||||
/**
|
||||
* @version $Revision$ $Date$
|
||||
*/
|
||||
public class HostTargetIdRetrieverTest extends TestCase
|
||||
{
|
||||
public void testHostTargetIdRetrieverNoSuffix()
|
||||
{
|
||||
String host = "test";
|
||||
Class<HttpServletRequest> klass = HttpServletRequest.class;
|
||||
HttpServletRequest request = (HttpServletRequest)Proxy.newProxyInstance(klass.getClassLoader(), new Class<?>[]{klass}, new Request(host));
|
||||
|
||||
HostTargetIdRetriever retriever = new HostTargetIdRetriever(null);
|
||||
String result = retriever.retrieveTargetId(request);
|
||||
|
||||
assertEquals(host, result);
|
||||
}
|
||||
|
||||
public void testHostTargetIdRetrieverWithSuffix()
|
||||
{
|
||||
String suffix = ".rhttp.example.com";
|
||||
String host = "test";
|
||||
Class<HttpServletRequest> klass = HttpServletRequest.class;
|
||||
HttpServletRequest request = (HttpServletRequest)Proxy.newProxyInstance(klass.getClassLoader(), new Class<?>[]{klass}, new Request(host + suffix));
|
||||
|
||||
HostTargetIdRetriever retriever = new HostTargetIdRetriever(suffix);
|
||||
String result = retriever.retrieveTargetId(request);
|
||||
|
||||
assertEquals(host, result);
|
||||
}
|
||||
|
||||
public void testHostTargetIdRetrieverWithSuffixAndPort()
|
||||
{
|
||||
String suffix = ".rhttp.example.com";
|
||||
String host = "test";
|
||||
Class<HttpServletRequest> klass = HttpServletRequest.class;
|
||||
HttpServletRequest request = (HttpServletRequest)Proxy.newProxyInstance(klass.getClassLoader(), new Class<?>[]{klass}, new Request(host + suffix + ":8080"));
|
||||
|
||||
HostTargetIdRetriever retriever = new HostTargetIdRetriever(suffix);
|
||||
String result = retriever.retrieveTargetId(request);
|
||||
|
||||
assertEquals(host, result);
|
||||
}
|
||||
|
||||
public void testHostTargetIdRetrieverNullHost()
|
||||
{
|
||||
Class<HttpServletRequest> klass = HttpServletRequest.class;
|
||||
HttpServletRequest request = (HttpServletRequest)Proxy.newProxyInstance(klass.getClassLoader(), new Class<?>[]{klass}, new Request(null));
|
||||
|
||||
HostTargetIdRetriever retriever = new HostTargetIdRetriever(".rhttp.example.com");
|
||||
String result = retriever.retrieveTargetId(request);
|
||||
|
||||
assertNull(result);
|
||||
}
|
||||
|
||||
private static class Request implements InvocationHandler
|
||||
{
|
||||
private final String host;
|
||||
|
||||
private Request(String host)
|
||||
{
|
||||
this.host = host;
|
||||
}
|
||||
|
||||
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable
|
||||
{
|
||||
if ("getHeader".equals(method.getName()))
|
||||
{
|
||||
if (args.length == 1 && "Host".equals(args[0]))
|
||||
{
|
||||
return host;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
# LOG4J levels: OFF, FATAL, ERROR, WARN, INFO, DEBUG, ALL
|
||||
#
|
||||
log4j.rootLogger=ALL,CONSOLE
|
||||
|
||||
log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender
|
||||
#log4j.appender.CONSOLE.threshold=INFO
|
||||
log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout
|
||||
#log4j.appender.CONSOLE.layout.ConversionPattern=%d %t [%5p][%c{1}] %m%n
|
||||
log4j.appender.CONSOLE.layout.ConversionPattern=%d [%5p][%c] %m%n
|
||||
|
||||
# Level tuning
|
||||
log4j.logger.org.eclipse.jetty=INFO
|
||||
log4j.logger.org.mortbay.jetty.rhttp=INFO
|
|
@ -0,0 +1,58 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<parent>
|
||||
<groupId>org.eclipse.jetty.rhttp</groupId>
|
||||
<artifactId>jetty-rhttp-project</artifactId>
|
||||
<version>9.0.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<artifactId>reverse-http-loadtest</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
<name>Jetty :: Reverse HTTP :: Load Test</name>
|
||||
|
||||
<profiles>
|
||||
<profile>
|
||||
<id>loader</id>
|
||||
<activation>
|
||||
<activeByDefault>true</activeByDefault>
|
||||
</activation>
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.codehaus.mojo</groupId>
|
||||
<artifactId>exec-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<mainClass>org.eclipse.jetty.rhttp.loadtest.Loader</mainClass>
|
||||
<classpathScope>runtime</classpathScope>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</profile>
|
||||
<profile>
|
||||
<id>server</id>
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.codehaus.mojo</groupId>
|
||||
<artifactId>exec-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<mainClass>org.eclipse.jetty.rhttp.loadtest.Server</mainClass>
|
||||
<classpathScope>runtime</classpathScope>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</profile>
|
||||
</profiles>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty</groupId>
|
||||
<artifactId>reverse-http-gateway</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
|
@ -0,0 +1,429 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
|
||||
// ------------------------------------------------------------------------
|
||||
// All rights reserved. This program and the accompanying materials
|
||||
// are made available under the terms of the Eclipse Public License v1.0
|
||||
// and Apache License v2.0 which accompanies this distribution.
|
||||
//
|
||||
// The Eclipse Public License is available at
|
||||
// http://www.eclipse.org/legal/epl-v10.html
|
||||
//
|
||||
// The Apache License v2.0 is available at
|
||||
// http://www.opensource.org/licenses/apache2.0.php
|
||||
//
|
||||
// You may elect to redistribute this code under either of these licenses.
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.rhttp.loadtest;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Random;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
import org.eclipse.jetty.client.Address;
|
||||
import org.eclipse.jetty.client.ContentExchange;
|
||||
import org.eclipse.jetty.client.HttpClient;
|
||||
import org.eclipse.jetty.io.ByteArrayBuffer;
|
||||
import org.eclipse.jetty.util.thread.QueuedThreadPool;
|
||||
import org.mortbay.jetty.rhttp.client.RHTTPClient;
|
||||
import org.mortbay.jetty.rhttp.client.JettyClient;
|
||||
import org.mortbay.jetty.rhttp.client.RHTTPListener;
|
||||
import org.mortbay.jetty.rhttp.client.RHTTPRequest;
|
||||
import org.mortbay.jetty.rhttp.client.RHTTPResponse;
|
||||
|
||||
/**
|
||||
* @version $Revision$ $Date$
|
||||
*/
|
||||
public class Loader
|
||||
{
|
||||
private final List<RHTTPClient> clients = new ArrayList<RHTTPClient>();
|
||||
private final AtomicLong start = new AtomicLong();
|
||||
private final AtomicLong end = new AtomicLong();
|
||||
private final AtomicLong responses = new AtomicLong();
|
||||
private final AtomicLong failures = new AtomicLong();
|
||||
private final AtomicLong minLatency = new AtomicLong();
|
||||
private final AtomicLong maxLatency = new AtomicLong();
|
||||
private final AtomicLong totLatency = new AtomicLong();
|
||||
private final ConcurrentMap<Long, AtomicLong> latencies = new ConcurrentHashMap<Long, AtomicLong>();
|
||||
private final String nodeName;
|
||||
|
||||
public static void main(String[] args) throws Exception
|
||||
{
|
||||
String nodeName = "";
|
||||
if (args.length > 0)
|
||||
nodeName = args[0];
|
||||
|
||||
Loader loader = new Loader(nodeName);
|
||||
loader.run();
|
||||
}
|
||||
|
||||
public Loader(String nodeName)
|
||||
{
|
||||
this.nodeName = nodeName;
|
||||
}
|
||||
|
||||
private void run() throws Exception
|
||||
{
|
||||
HttpClient httpClient = new HttpClient();
|
||||
httpClient.setMaxConnectionsPerAddress(40000);
|
||||
QueuedThreadPool threadPool = new QueuedThreadPool();
|
||||
threadPool.setMaxThreads(500);
|
||||
threadPool.setDaemon(true);
|
||||
httpClient.setThreadPool(threadPool);
|
||||
httpClient.setIdleTimeout(5000);
|
||||
httpClient.start();
|
||||
|
||||
Random random = new Random();
|
||||
|
||||
BufferedReader console = new BufferedReader(new InputStreamReader(System.in));
|
||||
|
||||
System.err.print("server [localhost]: ");
|
||||
String value = console.readLine().trim();
|
||||
if (value.length() == 0)
|
||||
value = "localhost";
|
||||
String host = value;
|
||||
|
||||
System.err.print("port [8080]: ");
|
||||
value = console.readLine().trim();
|
||||
if (value.length() == 0)
|
||||
value = "8080";
|
||||
int port = Integer.parseInt(value);
|
||||
|
||||
System.err.print("context []: ");
|
||||
value = console.readLine().trim();
|
||||
if (value.length() == 0)
|
||||
value = "";
|
||||
String context = value;
|
||||
|
||||
System.err.print("external path [/]: ");
|
||||
value = console.readLine().trim();
|
||||
if (value.length() == 0)
|
||||
value = "/";
|
||||
String externalPath = value;
|
||||
|
||||
System.err.print("gateway path [/__gateway]: ");
|
||||
value = console.readLine().trim();
|
||||
if (value.length() == 0)
|
||||
value = "/__gateway";
|
||||
String gatewayPath = value;
|
||||
|
||||
int clients = 100;
|
||||
int batchCount = 1000;
|
||||
int batchSize = 5;
|
||||
long batchPause = 5;
|
||||
int requestSize = 50;
|
||||
|
||||
while (true)
|
||||
{
|
||||
System.err.println("-----");
|
||||
|
||||
System.err.print("clients [" + clients + "]: ");
|
||||
value = console.readLine();
|
||||
if (value == null)
|
||||
break;
|
||||
value = value.trim();
|
||||
if (value.length() == 0)
|
||||
value = "" + clients;
|
||||
clients = Integer.parseInt(value);
|
||||
|
||||
System.err.println("Waiting for clients to be ready...");
|
||||
|
||||
Address gatewayAddress = new Address(host, port);
|
||||
String gatewayURI = context + gatewayPath;
|
||||
|
||||
// Create or remove the necessary clients
|
||||
int currentClients = this.clients.size();
|
||||
if (currentClients < clients)
|
||||
{
|
||||
for (int i = 0; i < clients - currentClients; ++i)
|
||||
{
|
||||
final RHTTPClient client = new JettyClient(httpClient, gatewayAddress, gatewayURI, nodeName + (currentClients + i));
|
||||
client.addListener(new EchoListener(client));
|
||||
client.connect();
|
||||
this.clients.add(client);
|
||||
|
||||
// Give some time to the server to accept connections and
|
||||
// reply to handshakes and connects
|
||||
if (i % 10 == 0)
|
||||
{
|
||||
Thread.sleep(100);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (currentClients > clients)
|
||||
{
|
||||
for (int i = 0; i < currentClients - clients; ++i)
|
||||
{
|
||||
RHTTPClient client = this.clients.remove(currentClients - i - 1);
|
||||
client.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
System.err.println("Clients ready");
|
||||
|
||||
currentClients = this.clients.size();
|
||||
if (currentClients > 0)
|
||||
{
|
||||
System.err.print("batch count [" + batchCount + "]: ");
|
||||
value = console.readLine().trim();
|
||||
if (value.length() == 0)
|
||||
value = "" + batchCount;
|
||||
batchCount = Integer.parseInt(value);
|
||||
|
||||
System.err.print("batch size [" + batchSize + "]: ");
|
||||
value = console.readLine().trim();
|
||||
if (value.length() == 0)
|
||||
value = "" + batchSize;
|
||||
batchSize = Integer.parseInt(value);
|
||||
|
||||
System.err.print("batch pause [" + batchPause + "]: ");
|
||||
value = console.readLine().trim();
|
||||
if (value.length() == 0)
|
||||
value = "" + batchPause;
|
||||
batchPause = Long.parseLong(value);
|
||||
|
||||
System.err.print("request size [" + requestSize + "]: ");
|
||||
value = console.readLine().trim();
|
||||
if (value.length() == 0)
|
||||
value = "" + requestSize;
|
||||
requestSize = Integer.parseInt(value);
|
||||
String requestBody = "";
|
||||
for (int i = 0; i < requestSize; i++)
|
||||
requestBody += "x";
|
||||
|
||||
String externalURL = "http://" + host + ":" + port + context + externalPath;
|
||||
if (!externalURL.endsWith("/"))
|
||||
externalURL += "/";
|
||||
|
||||
reset();
|
||||
|
||||
long start = System.nanoTime();
|
||||
long expected = 0;
|
||||
for (int i = 0; i < batchCount; ++i)
|
||||
{
|
||||
for (int j = 0; j < batchSize; ++j)
|
||||
{
|
||||
int clientIndex = random.nextInt(this.clients.size());
|
||||
RHTTPClient client = this.clients.get(clientIndex);
|
||||
String targetId = client.getTargetId();
|
||||
String url = externalURL + targetId;
|
||||
|
||||
ExternalExchange exchange = new ExternalExchange();
|
||||
exchange.setMethod("GET");
|
||||
exchange.setURL(url);
|
||||
exchange.setRequestContent(new ByteArrayBuffer(requestBody, "UTF-8"));
|
||||
exchange.send(httpClient);
|
||||
++expected;
|
||||
}
|
||||
|
||||
if (batchPause > 0)
|
||||
Thread.sleep(batchPause);
|
||||
}
|
||||
long end = System.nanoTime();
|
||||
long elapsedNanos = end - start;
|
||||
if (elapsedNanos > 0)
|
||||
{
|
||||
System.err.print("Messages - Elapsed | Rate = ");
|
||||
System.err.print(TimeUnit.NANOSECONDS.toMillis(elapsedNanos));
|
||||
System.err.print(" ms | ");
|
||||
System.err.print(expected * 1000 * 1000 * 1000 / elapsedNanos);
|
||||
System.err.println(" requests/s ");
|
||||
}
|
||||
|
||||
waitForResponses(expected);
|
||||
printReport(expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void reset()
|
||||
{
|
||||
start.set(0L);
|
||||
end.set(0L);
|
||||
responses.set(0L);
|
||||
failures.set(0L);
|
||||
minLatency.set(Long.MAX_VALUE);
|
||||
maxLatency.set(0L);
|
||||
totLatency.set(0L);
|
||||
}
|
||||
|
||||
private void updateLatencies(long start, long end)
|
||||
{
|
||||
long latency = end - start;
|
||||
|
||||
// Update the latencies using a non-blocking algorithm
|
||||
long oldMinLatency = minLatency.get();
|
||||
while (latency < oldMinLatency)
|
||||
{
|
||||
if (minLatency.compareAndSet(oldMinLatency, latency)) break;
|
||||
oldMinLatency = minLatency.get();
|
||||
}
|
||||
long oldMaxLatency = maxLatency.get();
|
||||
while (latency > oldMaxLatency)
|
||||
{
|
||||
if (maxLatency.compareAndSet(oldMaxLatency, latency)) break;
|
||||
oldMaxLatency = maxLatency.get();
|
||||
}
|
||||
totLatency.addAndGet(latency);
|
||||
|
||||
latencies.putIfAbsent(latency, new AtomicLong(0L));
|
||||
latencies.get(latency).incrementAndGet();
|
||||
}
|
||||
|
||||
private boolean waitForResponses(long expected) throws InterruptedException
|
||||
{
|
||||
long arrived = responses.get() + failures.get();
|
||||
long lastArrived = 0;
|
||||
int maxRetries = 20;
|
||||
int retries = maxRetries;
|
||||
while (arrived < expected)
|
||||
{
|
||||
System.err.println("Waiting for responses to arrive " + arrived + "/" + expected);
|
||||
Thread.sleep(500);
|
||||
if (lastArrived == arrived)
|
||||
{
|
||||
--retries;
|
||||
if (retries == 0) break;
|
||||
}
|
||||
else
|
||||
{
|
||||
lastArrived = arrived;
|
||||
retries = maxRetries;
|
||||
}
|
||||
arrived = responses.get() + failures.get();
|
||||
}
|
||||
if (arrived < expected)
|
||||
{
|
||||
System.err.println("Interrupting wait for responses " + arrived + "/" + expected);
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
System.err.println("All responses arrived " + arrived + "/" + expected);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public void printReport(long expectedCount)
|
||||
{
|
||||
long responseCount = responses.get() + failures.get();
|
||||
System.err.print("Messages - Success/Failures/Expected = ");
|
||||
System.err.print(responses.get());
|
||||
System.err.print("/");
|
||||
System.err.print(failures.get());
|
||||
System.err.print("/");
|
||||
System.err.println(expectedCount);
|
||||
|
||||
long elapsedNanos = end.get() - start.get();
|
||||
if (elapsedNanos > 0)
|
||||
{
|
||||
System.err.print("Messages - Elapsed | Rate = ");
|
||||
System.err.print(TimeUnit.NANOSECONDS.toMillis(elapsedNanos));
|
||||
System.err.print(" ms | ");
|
||||
System.err.print(responseCount * 1000 * 1000 * 1000 / elapsedNanos);
|
||||
System.err.println(" responses/s ");
|
||||
}
|
||||
|
||||
if (latencies.size() > 1)
|
||||
{
|
||||
long maxLatencyBucketFrequency = 0L;
|
||||
long[] latencyBucketFrequencies = new long[20];
|
||||
long latencyRange = maxLatency.get() - minLatency.get();
|
||||
for (Iterator<Map.Entry<Long, AtomicLong>> entries = latencies.entrySet().iterator(); entries.hasNext();)
|
||||
{
|
||||
Map.Entry<Long, AtomicLong> entry = entries.next();
|
||||
long latency = entry.getKey();
|
||||
Long bucketIndex = (latency - minLatency.get()) * latencyBucketFrequencies.length / latencyRange;
|
||||
int index = bucketIndex.intValue() == latencyBucketFrequencies.length ? latencyBucketFrequencies.length - 1 : bucketIndex.intValue();
|
||||
long value = entry.getValue().get();
|
||||
latencyBucketFrequencies[index] += value;
|
||||
if (latencyBucketFrequencies[index] > maxLatencyBucketFrequency) maxLatencyBucketFrequency = latencyBucketFrequencies[index];
|
||||
entries.remove();
|
||||
}
|
||||
|
||||
System.err.println("Messages - Latency Distribution Curve (X axis: Frequency, Y axis: Latency):");
|
||||
for (int i = 0; i < latencyBucketFrequencies.length; i++)
|
||||
{
|
||||
long latencyBucketFrequency = latencyBucketFrequencies[i];
|
||||
int value = Math.round(latencyBucketFrequency * (float) latencyBucketFrequencies.length / maxLatencyBucketFrequency);
|
||||
if (value == latencyBucketFrequencies.length) value = value - 1;
|
||||
for (int j = 0; j < value; ++j) System.err.print(" ");
|
||||
System.err.print("@");
|
||||
for (int j = value + 1; j < latencyBucketFrequencies.length; ++j) System.err.print(" ");
|
||||
System.err.print(" _ ");
|
||||
System.err.print(TimeUnit.NANOSECONDS.toMillis((latencyRange * (i + 1) / latencyBucketFrequencies.length) + minLatency.get()));
|
||||
System.err.println(" ms (" + latencyBucketFrequency + ")");
|
||||
}
|
||||
}
|
||||
|
||||
System.err.print("Messages - Latency Min/Ave/Max = ");
|
||||
System.err.print(TimeUnit.NANOSECONDS.toMillis(minLatency.get()) + "/");
|
||||
System.err.print(responseCount == 0 ? "-/" : TimeUnit.NANOSECONDS.toMillis(totLatency.get() / responseCount) + "/");
|
||||
System.err.println(TimeUnit.NANOSECONDS.toMillis(maxLatency.get()) + " ms");
|
||||
}
|
||||
|
||||
private class ExternalExchange extends ContentExchange
|
||||
{
|
||||
private volatile long sendTime;
|
||||
|
||||
private ExternalExchange()
|
||||
{
|
||||
super(true);
|
||||
}
|
||||
|
||||
private void send(HttpClient httpClient) throws IOException
|
||||
{
|
||||
this.sendTime = System.nanoTime();
|
||||
httpClient.send(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResponseComplete() throws IOException
|
||||
{
|
||||
if (getResponseStatus() == 200)
|
||||
responses.incrementAndGet();
|
||||
else
|
||||
failures.incrementAndGet();
|
||||
|
||||
long arrivalTime = System.nanoTime();
|
||||
if (start.get() == 0L)
|
||||
start.set(arrivalTime);
|
||||
end.set(arrivalTime);
|
||||
updateLatencies(sendTime, arrivalTime);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onException(Throwable x)
|
||||
{
|
||||
failures.incrementAndGet();
|
||||
}
|
||||
}
|
||||
|
||||
private static class EchoListener implements RHTTPListener
|
||||
{
|
||||
private final RHTTPClient client;
|
||||
|
||||
public EchoListener(RHTTPClient client)
|
||||
{
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
public void onRequest(RHTTPRequest request) throws Exception
|
||||
{
|
||||
RHTTPResponse response = new RHTTPResponse(request.getId(), 200, "OK", new HashMap<String, String>(), request.getBody());
|
||||
client.deliver(response);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
|
||||
// ------------------------------------------------------------------------
|
||||
// All rights reserved. This program and the accompanying materials
|
||||
// are made available under the terms of the Eclipse Public License v1.0
|
||||
// and Apache License v2.0 which accompanies this distribution.
|
||||
//
|
||||
// The Eclipse Public License is available at
|
||||
// http://www.eclipse.org/legal/epl-v10.html
|
||||
//
|
||||
// The Apache License v2.0 is available at
|
||||
// http://www.opensource.org/licenses/apache2.0.php
|
||||
//
|
||||
// You may elect to redistribute this code under either of these licenses.
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.rhttp.loadtest;
|
||||
|
||||
import org.eclipse.jetty.rhttp.gateway.GatewayServer;
|
||||
import org.eclipse.jetty.rhttp.gateway.StandardTargetIdRetriever;
|
||||
import org.eclipse.jetty.server.Connector;
|
||||
import org.eclipse.jetty.server.nio.SelectChannelConnector;
|
||||
|
||||
/**
|
||||
* @version $Revision$ $Date$
|
||||
*/
|
||||
public class Server
|
||||
{
|
||||
public static void main(String[] args) throws Exception
|
||||
{
|
||||
int port = 8080;
|
||||
if (args.length > 0)
|
||||
port = Integer.parseInt(args[0]);
|
||||
|
||||
GatewayServer server = new GatewayServer();
|
||||
Connector connector = new SelectChannelConnector();
|
||||
connector.setLowResourceMaxIdleTime(connector.getMaxIdleTime());
|
||||
connector.setPort(port);
|
||||
server.addConnector(connector);
|
||||
server.setTargetIdRetriever(new StandardTargetIdRetriever());
|
||||
server.start();
|
||||
server.getServer().dumpStdErr();
|
||||
Runtime.getRuntime().addShutdownHook(new Shutdown(server));
|
||||
}
|
||||
|
||||
private static class Shutdown extends Thread
|
||||
{
|
||||
private final GatewayServer server;
|
||||
|
||||
public Shutdown(GatewayServer server)
|
||||
{
|
||||
this.server = server;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
try
|
||||
{
|
||||
server.stop();
|
||||
}
|
||||
catch (Exception ignored)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
# LOG4J levels: OFF, FATAL, ERROR, WARN, INFO, DEBUG, ALL
|
||||
#
|
||||
log4j.rootLogger=ALL,CONSOLE
|
||||
|
||||
log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender
|
||||
#log4j.appender.CONSOLE.threshold=INFO
|
||||
log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout
|
||||
#log4j.appender.CONSOLE.layout.ConversionPattern=%d %t [%5p][%c{1}] %m%n
|
||||
log4j.appender.CONSOLE.layout.ConversionPattern=%d [%5p][%c] %m%n
|
||||
|
||||
# Level tuning
|
||||
log4j.logger.org.eclipse.jetty=INFO
|
||||
log4j.logger.org.mortbay.jetty.rhttp=INFO
|
|
@ -0,0 +1,108 @@
|
|||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||
<parent>
|
||||
<groupId>org.eclipse.jetty</groupId>
|
||||
<artifactId>jetty-project</artifactId>
|
||||
<version>9.0.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>org.eclipse.jetty.rhttp</groupId>
|
||||
<artifactId>jetty-rhttp-project</artifactId>
|
||||
<packaging>pom</packaging>
|
||||
<name>Jetty :: Reverse HTTP</name>
|
||||
|
||||
<properties>
|
||||
</properties>
|
||||
|
||||
<modules>
|
||||
<module>reverse-http-client</module>
|
||||
<module>reverse-http-connector</module>
|
||||
<module>reverse-http-gateway</module>
|
||||
<module>reverse-http-loadtest</module>
|
||||
</modules>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.sonatype.maven.plugin</groupId>
|
||||
<artifactId>emma-maven-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>process-classes</phase>
|
||||
<goals>
|
||||
<goal>instrument</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<configuration>
|
||||
<classesDirectory>${project.build.directory}/generated-classes/emma/classes</classesDirectory>
|
||||
<systemProperties>
|
||||
<property>
|
||||
<name>emma.coverage.out.file</name>
|
||||
<value>${project.build.directory}/coverage.ec</value>
|
||||
</property>
|
||||
</systemProperties>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.sonatype.maven.plugin</groupId>
|
||||
<artifactId>emma4it-maven-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>report</id>
|
||||
<phase>post-integration-test</phase>
|
||||
<goals>
|
||||
<goal>report</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<sourceSets>
|
||||
<sourceSet>
|
||||
<directory>${project.build.sourceDirectory}</directory>
|
||||
</sourceSet>
|
||||
</sourceSets>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>javax.servlet</groupId>
|
||||
<artifactId>servlet-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty</groupId>
|
||||
<artifactId>jetty-server</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty</groupId>
|
||||
<artifactId>jetty-client</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty</groupId>
|
||||
<artifactId>jetty-io</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty</groupId>
|
||||
<artifactId>jetty-util</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
<groupId>org.eclipse.jetty.rhttp</groupId>
|
||||
</project>
|
Loading…
Reference in New Issue