├── .gitignore ├── README.md ├── bin ├── lib │ └── nio-http-proxy.jar └── run-nio-http-proxy.sh ├── nio-http-proxy ├── pom.xml └── src │ └── main │ ├── java │ └── org │ │ └── zlambda │ │ └── projects │ │ ├── ClientSocketChannelHandler.java │ │ ├── ConnectionListener.java │ │ ├── Dispatcher.java │ │ ├── EventHandler.java │ │ ├── HostSocketChannelHandler.java │ │ ├── Monitor.java │ │ ├── MonitorSingleton.java │ │ ├── MonitorThread.java │ │ ├── NIOHttpProxy.java │ │ ├── Worker.java │ │ ├── buffer │ │ ├── ChannelBuffer.java │ │ ├── ChannelBufferPool.java │ │ ├── ConnectionBuffer.java │ │ ├── DirectChannelBufferPool.java │ │ └── HeapChannelBufferPool.java │ │ ├── context │ │ ├── ConnectionContext.java │ │ ├── ProxyContext.java │ │ ├── SystemContext.java │ │ └── WorkerContext.java │ │ └── utils │ │ ├── Common.java │ │ ├── JsonUtils.java │ │ ├── SelectionKeyUtils.java │ │ └── SocketChannelUtils.java │ └── resources │ └── log4j.properties └── pom.xml /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | target 3 | *.iml 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | HTTP/HTTPS Forward Proxy 2 | ------------------------ 3 | This project contains Java implementation of HTTP/HTTPS (Tunneling over HTTP) Forward Proxy. 4 | 5 | Requires JDK 1.8 or higher. 6 | 7 | ### N.I.O Based 8 | 9 | #### overview 10 | `nio-http-proxy` contains the NIO based implementation of HTTP/HTTPS Forward Proxy, which is robust and cpu-memory efficient. 11 | 12 | #### quick start 13 | This repo comes with urbar jar stored in `bin/lib/nio-http-roxy.jar` so you can run the proxy by simply 14 | 15 | ```bash 16 | JAVA_HOME=${path_to_java8} \ 17 | ./bin/run-nio-http-proxy.sh 18 | ``` 19 | 20 | You can also rebuild and run 21 | 22 | ``` 23 | JAVA_HOME=${path_to_java8} \ 24 | ./bin/run-nio-http-proxy.sh rebuild 25 | ``` 26 | 27 | #### configuration 28 | 29 | Here is list of proxy configurations. To change the configuration, simple modify the `./bin/run-nio-http-proxy.sh`. 30 | 31 | ##### basic 32 | 33 | ``` 34 | -Dport=9999 # proxy port 35 | -Dworker=8 # the proxy application use a fixed worker pool, this option specifies the number of workers 36 | ``` 37 | 38 | ##### monitor 39 | 40 | Monitor is just for debug usage, which periodically dumps the proxy status like active connections, number of used buffers. By default the monitor thread is enable, but we can disable it. 41 | 42 | ``` 43 | -DenableMonitor=true 44 | -DmonitorUpdateInterval=30 # unit second 45 | ``` 46 | 47 | ##### buffer pool 48 | 49 | The proxy application, by default, uses a off-heap buffer pool. When it is disable, the proxy will use on-heap buffer without pooling, which means the buffer is managed by JVM. 50 | 51 | ``` 52 | -DuseDirectBuffer=true 53 | -DminNumBuffers=100 54 | -DmaxNumBuffers=200 55 | -DbufferSize=10 # unit KB, each client <-> proxy <-> host connection use two buffers 56 | ``` 57 | -------------------------------------------------------------------------------- /bin/lib/nio-http-proxy.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alanzplus/HTTP-Proxy/4671b8ab16d14862c94e28ef9158f9822bb2da4a/bin/lib/nio-http-proxy.jar -------------------------------------------------------------------------------- /bin/run-nio-http-proxy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | readonly script_dir=$(cd $(dirname $0); pwd) 5 | readonly root_dir=$(cd "${script_dir}/../"; pwd) 6 | readonly build_lib=$(echo "${root_dir}"/nio-http-proxy/target/*-jar-with-dependencies.jar) 7 | readonly lib="${script_dir}/lib/nio-http-proxy.jar" 8 | 9 | if [[ $1 == 'rebuild' ]]; then 10 | (cd "${root_dir}" && mvn clean package) 11 | rm -f "${lib}" 12 | cp -f "${build_lib}" "${lib}" 13 | fi 14 | 15 | PROXY_OPTS="-Dworker=16 -Dport=9999 -DbufferSize=20 -DminNumBuffers=200 -DmaxNumBuffers=400 -DenableMonitor=true -DuseDirectBuffer=true -DmonitorUpdateInterval=15" 16 | JVM_OPTS="-server -Xss228K -Xms512M -Xmx512M -XX:NewRatio=1 -XX:SurvivorRatio=8 -XX:TargetSurvivorRatio=90 -XX:+UseParNewGC -XX:+UseCMSInitiatingOccupancyOnly -XX:CMSInitiatingOccupancyFraction=60 -XX:+CMSScavengeBeforeRemark -XX:+CMSParallelRemarkEnabled -XX:+CMSClassUnloadingEnabled -XX:+UseCompressedOops -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintClassHistogram -XX:+UseConcMarkSweepGC -verbose:gc -Xloggc:gc.log" 17 | 18 | $JAVA_HOME/bin/java ${JVM_OPTS} ${PROXY_OPTS} -jar ${lib} 19 | -------------------------------------------------------------------------------- /nio-http-proxy/pom.xml: -------------------------------------------------------------------------------- 1 | 4 | 4.0.0 5 | 6 | org.zlambda.projects 7 | nio-http-proxy 8 | 1.0.0-SNAPSHOT 9 | jar 10 | 11 | 12 | 13 | org.slf4j 14 | slf4j-api 15 | 1.7.12 16 | 17 | 18 | 19 | org.slf4j 20 | slf4j-log4j12 21 | 1.7.12 22 | 23 | 24 | 25 | junit 26 | junit 27 | 4.13.1 28 | test 29 | 30 | 31 | 32 | com.google.guava 33 | guava 34 | 19.0 35 | 36 | 37 | 38 | commons-io 39 | commons-io 40 | 2.4 41 | 42 | 43 | 44 | com.fasterxml.jackson.core 45 | jackson-databind 46 | 2.10.0.pr1 47 | 48 | 49 | 50 | com.fasterxml.jackson.core 51 | jackson-core 52 | 2.9.9 53 | 54 | 55 | 56 | com.fasterxml.jackson.core 57 | jackson-annotations 58 | 2.9.9 59 | 60 | 61 | 62 | org.apache.commons 63 | commons-lang3 64 | 3.0 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | org.apache.maven.plugins 73 | maven-compiler-plugin 74 | 3.3 75 | 76 | 1.8 77 | 1.8 78 | 79 | 80 | 81 | maven-assembly-plugin 82 | 83 | 84 | jar-with-dependencies 85 | 86 | 87 | 88 | org.zlambda.projects.NIOHttpProxy 89 | 90 | 91 | 92 | 93 | 94 | package 95 | 96 | single 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | -------------------------------------------------------------------------------- /nio-http-proxy/src/main/java/org/zlambda/projects/ClientSocketChannelHandler.java: -------------------------------------------------------------------------------- 1 | package org.zlambda.projects; 2 | 3 | import org.slf4j.Logger; 4 | import org.zlambda.projects.buffer.ChannelBuffer; 5 | import org.zlambda.projects.context.ConnectionContext; 6 | import org.zlambda.projects.context.ProxyContext; 7 | import org.zlambda.projects.utils.Common; 8 | 9 | import java.io.IOException; 10 | import java.io.InputStream; 11 | import java.net.InetSocketAddress; 12 | import java.net.MalformedURLException; 13 | import java.net.URL; 14 | import java.nio.channels.SelectionKey; 15 | import java.nio.channels.SocketChannel; 16 | import java.nio.channels.UnresolvedAddressException; 17 | import java.util.ArrayList; 18 | import java.util.List; 19 | import java.util.Optional; 20 | import java.util.Scanner; 21 | import java.util.regex.Pattern; 22 | 23 | public class ClientSocketChannelHandler implements EventHandler { 24 | private static final Logger LOGGER = Common.getSystemLogger(); 25 | private final ProxyContext context; 26 | private HandlerState state = HandlerState.PARSING_INITIAL_REQUEST; 27 | 28 | public ClientSocketChannelHandler(ProxyContext context) { 29 | this.context = context; 30 | } 31 | 32 | @Override 33 | public void execute(SelectionKey selectionKey) { 34 | state = state.perform(context); 35 | context.cleanup(); 36 | } 37 | 38 | private enum HandlerState { 39 | PARSING_INITIAL_REQUEST { 40 | @Override 41 | public HandlerState perform(ProxyContext context) { 42 | ConnectionContext client = context.getClient(); 43 | if (!client.isReadable()) { 44 | return PARSING_INITIAL_REQUEST; 45 | } 46 | ChannelBuffer upstreamBuffer = context.getConnectionBuffer().upstream(); 47 | if (-1 == client.read(upstreamBuffer)) { 48 | client.closeIO(); 49 | return PARSING_INITIAL_REQUEST; 50 | } 51 | Optional> header = 52 | parseInitialRequestHeader(upstreamBuffer.toViewInputStream()); 53 | if (!header.isPresent()) { 54 | LOGGER.debug("<{}> cannot find header", client.getName()); 55 | return PARSING_INITIAL_REQUEST; 56 | } 57 | LOGGER.info("got initial request line <{}>.", header.get().get(0)); 58 | Optional requestLine = RequestLine.construct(header.get().get(0)); 59 | if (!requestLine.isPresent()) { 60 | LOGGER.error("cannot parse request line, so close client socket channel <{}>", 61 | client.getName()); 62 | client.closeIO(); 63 | return PARSING_INITIAL_REQUEST; 64 | } 65 | /** 66 | * for https request, discard the initial request 67 | */ 68 | if (requestLine.get().isHttps) { 69 | upstreamBuffer.clear(); 70 | } 71 | if (!createHostAndRegisterChannel(requestLine.get(), context)) { 72 | client.closeIO(); 73 | return PARSING_INITIAL_REQUEST; 74 | } 75 | /** 76 | * Wait Host to be connected 77 | */ 78 | client.unregister(SelectionKey.OP_READ); 79 | return BRIDGING; 80 | } 81 | }, 82 | 83 | /** 84 | * Host is connected 85 | */ 86 | BRIDGING { 87 | @Override 88 | public HandlerState perform(ProxyContext context) { 89 | ConnectionContext host = context.getHost(); 90 | ConnectionContext client = context.getClient(); 91 | /** 92 | * [Client -- IS --> Proxy] -- OS --> Host 93 | */ 94 | if (client.isReadable()) { 95 | if (host.isOutputShutdown()) { 96 | LOGGER.debug( 97 | "Host socket channel <{}> output stream is closed, so close Client socket channel <{}> input stream", 98 | host.getName(), client.getName()); 99 | client.shutdownIS(); 100 | } else { 101 | ChannelBuffer upstreamBuffer = context.getConnectionBuffer().upstream(); 102 | if (-1 == client.read(upstreamBuffer)) { 103 | client.shutdownIS(); 104 | } 105 | /** 106 | * Read Event always trigger output stream to listen on write event 107 | */ 108 | host.register(SelectionKey.OP_WRITE); 109 | } 110 | } 111 | 112 | /** 113 | * [Client <-- OS -- Proxy] < -- IS -- Host 114 | */ 115 | if (client.isWritable()) { 116 | ChannelBuffer downstreamBuffer = context.getConnectionBuffer().downstream(); 117 | if (host.isInputShutdown() && downstreamBuffer.empty()) { 118 | LOGGER.debug( 119 | "Host socket channel <{}> input stream is closed and downstream buffer is empty, so close Client <{}> output stream ", 120 | host.getName(), client.getName()); 121 | client.shutdownOS(); 122 | } else { 123 | if (downstreamBuffer.empty()) { 124 | // keep cpu free 125 | client.unregister(SelectionKey.OP_WRITE); 126 | } else if (-1 == client.write(downstreamBuffer)) { 127 | client.shutdownOS(); 128 | /** 129 | * error on output stream should always immediately terminate its corresponding input stream 130 | */ 131 | host.shutdownIS(); 132 | } 133 | } 134 | } 135 | return BRIDGING; 136 | } 137 | },; 138 | 139 | private static Optional> 140 | parseInitialRequestHeader(InputStream inputStream) { 141 | Scanner scanner = new Scanner(inputStream, "utf-8"); 142 | List ret = new ArrayList<>(); 143 | while (scanner.hasNextLine()) { 144 | String line = scanner.nextLine(); 145 | ret.add(line); 146 | if (line.equals("")) { 147 | break; 148 | } 149 | } 150 | if (ret.isEmpty() || !ret.get(ret.size() - 1).equals("")) { 151 | return Optional.empty(); 152 | } else { 153 | return Optional.of(ret); 154 | } 155 | } 156 | 157 | private static final Pattern PROTOCOL_MATCHER = Pattern.compile("^(https|http).*"); 158 | 159 | private static boolean 160 | createHostAndRegisterChannel(RequestLine line, ProxyContext context) { 161 | String uri = line.uri; 162 | /** 163 | * Java URL cannot parse uri without protocol 164 | */ 165 | if (line.isHttps && !PROTOCOL_MATCHER.matcher(uri).matches()) { 166 | uri = "https://" + uri; 167 | context.markAsHttps(); 168 | } 169 | 170 | SocketChannel hostSocketChannel = null; 171 | try { 172 | InetSocketAddress inetSocketAddress = constructInetSocketAddress(new URL(uri)); 173 | hostSocketChannel = SocketChannel.open(); 174 | hostSocketChannel.configureBlocking(false); 175 | HostSocketChannelHandler handler = new HostSocketChannelHandler(context); 176 | SelectionKey hostKey = hostSocketChannel.register( 177 | context.selector(), SelectionKey.OP_CONNECT, handler); 178 | context.setHost(new ConnectionContext(hostKey, inetSocketAddress.toString())); 179 | hostSocketChannel.connect(inetSocketAddress); 180 | return true; 181 | } catch (MalformedURLException e) { 182 | LOGGER.error("cannot parse URL <{}>.", uri, e); 183 | } catch (IOException e) { 184 | LOGGER.error("cannot create or register host socket channel", e); 185 | } catch (UnresolvedAddressException e) { 186 | LOGGER.error("cannot resolve address for <{}>.", uri, e); 187 | } 188 | Common.close(hostSocketChannel); 189 | return false; 190 | } 191 | 192 | private static InetSocketAddress constructInetSocketAddress(URL url) { 193 | int port = url.getPort() == -1 ? url.getDefaultPort() : url.getPort(); 194 | return new InetSocketAddress(url.getHost(), port); 195 | } 196 | 197 | abstract HandlerState perform(ProxyContext context); 198 | 199 | /** 200 | * Reference https://www.w3.org/Protocols/rfc2616/rfc2616-sec5.html 201 | */ 202 | private static class RequestLine { 203 | private static final Pattern CONNECT_MATCHER = Pattern.compile("^connect.*", Pattern.CASE_INSENSITIVE); 204 | private static final Pattern SPLIT_MATCHER = Pattern.compile("\\s+"); 205 | private final String method; 206 | private final String uri; 207 | private final String version; 208 | private final boolean isHttps; 209 | 210 | private RequestLine(String method, String uri, String version) { 211 | this.method = method; 212 | this.uri = uri; 213 | this.version = version; 214 | this.isHttps = CONNECT_MATCHER.matcher(method).matches(); 215 | } 216 | 217 | private static Optional construct(String requestLine) { 218 | String[] split = SPLIT_MATCHER.split(requestLine, 3); 219 | return (3 == split.length) ? Optional.of( 220 | new RequestLine(split[0], split[1], split[2])) : Optional.empty(); 221 | } 222 | } 223 | } 224 | 225 | } 226 | -------------------------------------------------------------------------------- /nio-http-proxy/src/main/java/org/zlambda/projects/ConnectionListener.java: -------------------------------------------------------------------------------- 1 | package org.zlambda.projects; 2 | 3 | import org.slf4j.Logger; 4 | import org.zlambda.projects.context.SystemContext; 5 | import org.zlambda.projects.utils.Common; 6 | import org.zlambda.projects.utils.SelectionKeyUtils; 7 | import org.zlambda.projects.utils.SocketChannelUtils; 8 | 9 | import java.io.IOException; 10 | import java.net.InetSocketAddress; 11 | import java.nio.channels.SelectableChannel; 12 | import java.nio.channels.SelectionKey; 13 | import java.nio.channels.Selector; 14 | import java.nio.channels.ServerSocketChannel; 15 | import java.nio.channels.SocketChannel; 16 | import java.util.Iterator; 17 | 18 | public class ConnectionListener extends Thread { 19 | private static final Logger LOGGER = Common.getSystemLogger(); 20 | private final SystemContext systemContext; 21 | 22 | public ConnectionListener(SystemContext systemContext) { 23 | super(ConnectionListener.class.getSimpleName()); 24 | this.systemContext = systemContext; 25 | } 26 | 27 | @Override 28 | public void run() { 29 | try { 30 | Selector serverSocketSelector = Selector.open(); 31 | SelectableChannel channel = ServerSocketChannel 32 | .open() 33 | .bind(new InetSocketAddress(systemContext.getPort())) 34 | .configureBlocking(false); 35 | channel.register(serverSocketSelector, SelectionKey.OP_ACCEPT, new Handler()); 36 | while (true) { 37 | LOGGER.info("listen on port {}, waiting for client connection...", systemContext.getPort()); 38 | int selected = serverSocketSelector.select(); 39 | if (0 == selected) { 40 | continue; 41 | } 42 | Iterator iterator = serverSocketSelector.selectedKeys().iterator(); 43 | while (iterator.hasNext()) { 44 | SelectionKey key = iterator.next(); 45 | if (!key.isValid()) { 46 | throw new IllegalStateException("got invalid selection key."); 47 | } 48 | ((EventHandler) key.attachment()).execute(key); 49 | iterator.remove(); 50 | } 51 | } 52 | } catch (Exception e) { 53 | LOGGER.error("got exception <{}>, so terminate proxy.", e.getMessage(), e); 54 | } finally { 55 | System.exit(-1); 56 | } 57 | } 58 | 59 | private class Handler implements EventHandler { 60 | @Override 61 | public void execute(SelectionKey key) { 62 | if (!key.isAcceptable()) { 63 | return; 64 | } 65 | ServerSocketChannel server = SelectionKeyUtils.getServerSocketChannel(key); 66 | try { 67 | SocketChannel client = server.accept(); 68 | LOGGER.info("connected with client. {}", SocketChannelUtils.getRemoteAddress(client)); 69 | client.configureBlocking(false); 70 | try { 71 | systemContext.getClientQueue().put(client); 72 | } catch (InterruptedException e) { 73 | LOGGER.error("got interrupted exception when publishing client.", e); 74 | Thread.currentThread().interrupt(); 75 | client.close(); 76 | } 77 | } catch (IOException e) { 78 | LOGGER.error("failed on connection", e); 79 | } 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /nio-http-proxy/src/main/java/org/zlambda/projects/Dispatcher.java: -------------------------------------------------------------------------------- 1 | package org.zlambda.projects; 2 | 3 | import org.slf4j.Logger; 4 | import org.zlambda.projects.context.ConnectionContext; 5 | import org.zlambda.projects.context.ProxyContext; 6 | import org.zlambda.projects.context.SystemContext; 7 | import org.zlambda.projects.context.WorkerContext; 8 | import org.zlambda.projects.utils.Common; 9 | import org.zlambda.projects.utils.SelectionKeyUtils; 10 | import org.zlambda.projects.utils.SocketChannelUtils; 11 | 12 | import java.nio.channels.ClosedChannelException; 13 | import java.nio.channels.SelectionKey; 14 | import java.nio.channels.Selector; 15 | import java.nio.channels.SocketChannel; 16 | import java.util.ArrayList; 17 | import java.util.HashSet; 18 | import java.util.Iterator; 19 | import java.util.List; 20 | import java.util.Set; 21 | import java.util.concurrent.ExecutorService; 22 | import java.util.concurrent.Executors; 23 | 24 | public class Dispatcher extends Thread { 25 | /** 26 | * For development debug usage 27 | */ 28 | private static final Logger LOGGER = Common.getSystemLogger(); 29 | private final SystemContext systemContext; 30 | // for synchronize the contextSet update when worker thread exit 31 | private final Object contextSetMonitor = new Object(); 32 | private final Set workerContextSet = new HashSet<>(); 33 | private final ExecutorService executorService; 34 | 35 | public Dispatcher(SystemContext systemContext) { 36 | super(Dispatcher.class.getSimpleName()); 37 | this.systemContext = systemContext; 38 | this.executorService = Executors.newFixedThreadPool(systemContext.getNumWorkers()); 39 | } 40 | 41 | private void createAndStartWorker() throws Exception { 42 | WorkerContext context = new WorkerContext.Builder() 43 | .selector(Selector.open()) 44 | .contextSet(workerContextSet) 45 | .contextSetMonitor(contextSetMonitor) 46 | .build(); 47 | workerContextSet.add(context); 48 | executorService.submit(new Worker(context)); 49 | } 50 | 51 | @Override 52 | public void run() { 53 | LOGGER.info("{} thread started", getName()); 54 | try { 55 | List activeChannelStats = new ArrayList<>(); 56 | while (true) { 57 | SocketChannel client; 58 | try { 59 | client = systemContext.getClientQueue().take(); 60 | LOGGER.info("got client connection {}", client); 61 | } catch (InterruptedException e) { 62 | LOGGER.error("got interruptedException while taking clientQueue.", e); 63 | Thread.currentThread().interrupt(); 64 | continue; 65 | } 66 | /** 67 | * Most of the time, contextSetMonitor is contention free. When worker thread exits due to 68 | * unexpected exception, the worker will try to remove its context from the context set. So 69 | * only at that time, there will be contention between dispatcher thread and worker thread. 70 | */ 71 | synchronized (contextSetMonitor) { 72 | if (workerContextSet.size() < systemContext.getNumWorkers()) { 73 | createAndStartWorker(); 74 | } 75 | int minNumChannels = Integer.MAX_VALUE; 76 | WorkerContext targetWorkerContext = null; 77 | Iterator it = workerContextSet.iterator(); 78 | int totalActives = 0; 79 | while (it.hasNext()) { 80 | WorkerContext ct = it.next(); 81 | int num = ct.getNumConnections(); 82 | totalActives += num; 83 | if (minNumChannels > num) { 84 | minNumChannels = num; 85 | targetWorkerContext = ct; 86 | } 87 | activeChannelStats.add(String.format("[%s:%d]", ct.getName(), num)); 88 | } 89 | /** 90 | * approximate statds 91 | */ 92 | LOGGER.info("approximate total active channels <{}>, stats <{}>", totalActives, 93 | activeChannelStats); 94 | activeChannelStats.clear(); 95 | try { 96 | synchronized (targetWorkerContext.getWakeupBarrier()) { 97 | targetWorkerContext.getSelector().wakeup(); 98 | ProxyContext proxyContext = new ProxyContext(systemContext); 99 | SelectionKey key = client.register( 100 | targetWorkerContext.getSelector(), 101 | SelectionKey.OP_READ, 102 | new ClientSocketChannelHandler(proxyContext)); 103 | proxyContext.setClient(new ConnectionContext(key, SelectionKeyUtils.getName(key))); 104 | MonitorSingleton.get().collectChannelPair( 105 | proxyContext.getClient(), 106 | null 107 | ); 108 | } 109 | } catch (ClosedChannelException e) { 110 | LOGGER.error("Failed to register socket channel <{}>, reason {}.", 111 | SocketChannelUtils.getRemoteAddress(client), e.getCause(), e); 112 | } 113 | } 114 | } 115 | } catch (Exception e) { 116 | LOGGER.error("got unexpected exception: {}, so terminate application.", e.getCause(), e); 117 | System.exit(-1); 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /nio-http-proxy/src/main/java/org/zlambda/projects/EventHandler.java: -------------------------------------------------------------------------------- 1 | package org.zlambda.projects; 2 | 3 | import java.nio.channels.SelectionKey; 4 | 5 | public interface EventHandler { 6 | void execute(SelectionKey key); 7 | } 8 | -------------------------------------------------------------------------------- /nio-http-proxy/src/main/java/org/zlambda/projects/HostSocketChannelHandler.java: -------------------------------------------------------------------------------- 1 | package org.zlambda.projects; 2 | 3 | import org.slf4j.Logger; 4 | import org.zlambda.projects.buffer.ChannelBuffer; 5 | import org.zlambda.projects.context.ConnectionContext; 6 | import org.zlambda.projects.context.ProxyContext; 7 | import org.zlambda.projects.utils.Common; 8 | 9 | import java.io.IOException; 10 | import java.nio.channels.SelectionKey; 11 | 12 | public class HostSocketChannelHandler implements EventHandler { 13 | private static final Logger LOGGER = Common.getSystemLogger(); 14 | private final ProxyContext context; 15 | 16 | private HandlerState state = HandlerState.WAIT_FOR_CONNECTION; 17 | 18 | public HostSocketChannelHandler(ProxyContext context) { 19 | this.context = context; 20 | } 21 | 22 | @Override 23 | public void execute(SelectionKey selectionKey) { 24 | state = state.perform(context); 25 | context.cleanup(); 26 | } 27 | 28 | private enum HandlerState { 29 | WAIT_FOR_CONNECTION { 30 | @Override 31 | public HandlerState perform(ProxyContext context) { 32 | ConnectionContext client = context.getClient(); 33 | ConnectionContext host = context.getHost(); 34 | if (!host.isConnectable()) { 35 | return WAIT_FOR_CONNECTION; 36 | } 37 | try { 38 | host.getChannel().finishConnect(); 39 | } catch (IOException e) { 40 | LOGGER.error("Host channel <{}> failed to connect, reason: {}.", 41 | host.getName(), e.getMessage(), e); 42 | client.closeIO(); 43 | host.closeIO(); 44 | return WAIT_FOR_CONNECTION; 45 | } 46 | host.unregister(SelectionKey.OP_CONNECT); 47 | host.register(SelectionKey.OP_READ | SelectionKey.OP_WRITE); 48 | client.register(SelectionKey.OP_READ | SelectionKey.OP_WRITE); 49 | if (context.isHttps()) { 50 | context.getConnectionBuffer().downstream().put( 51 | "HTTP/1.1 200 Connection Established\r\n\r\n".getBytes()); 52 | } 53 | MonitorSingleton.get().collectChannelPair(client, host); 54 | return BRIDGING; 55 | } 56 | }, 57 | BRIDGING { 58 | @Override 59 | public HandlerState perform(ProxyContext context) { 60 | ConnectionContext client = context.getClient(); 61 | ConnectionContext host = context.getHost(); 62 | /** 63 | * Client < -- OS -- [Proxy <-- IS -- Host] 64 | */ 65 | if (host.isReadable()) { 66 | if (client.isOutputShutdown()) { 67 | LOGGER.debug( 68 | "Client socket channel <{}> output stream is closed, so close Host socket channel <{}> input stream", 69 | client.getName(), host.getName()); 70 | host.shutdownIS(); 71 | } else { 72 | ChannelBuffer downstream = context.getConnectionBuffer().downstream(); 73 | if (-1 == host.read(downstream)) { 74 | host.shutdownIS(); 75 | } 76 | /** 77 | * Read Event always trigger output stream to listen on write event 78 | */ 79 | client.register(SelectionKey.OP_WRITE); 80 | } 81 | } 82 | 83 | /** 84 | * Client -- IS --> [Proxy -- OS --> Host] 85 | */ 86 | if (host.isWritable()) { 87 | ChannelBuffer upstream = context.getConnectionBuffer().upstream(); 88 | if (client.isInputShutdown() && upstream.empty()) { 89 | LOGGER.debug( 90 | "Client channel <{}> input stream is closed and upstream buffer is empty, so close Host socket channel <{}> output stream", 91 | client.getName(), host.getName()); 92 | host.shutdownOS(); 93 | } else { 94 | if (upstream.empty()) { 95 | // keep cpu free 96 | host.unregister(SelectionKey.OP_WRITE); 97 | } else if (-1 == host.write(upstream)) { 98 | host.shutdownOS(); 99 | /** 100 | * error on output stream should always immediately terminate its corresponding input stream 101 | */ 102 | client.shutdownIS(); 103 | } 104 | } 105 | } 106 | return BRIDGING; 107 | } 108 | },; 109 | 110 | abstract HandlerState perform(ProxyContext context); 111 | } 112 | 113 | } 114 | -------------------------------------------------------------------------------- /nio-http-proxy/src/main/java/org/zlambda/projects/Monitor.java: -------------------------------------------------------------------------------- 1 | package org.zlambda.projects; 2 | 3 | import org.zlambda.projects.context.ConnectionContext; 4 | 5 | public interface Monitor { 6 | void collectChannelPair(ConnectionContext client, ConnectionContext host); 7 | 8 | String dumpStats(); 9 | } 10 | -------------------------------------------------------------------------------- /nio-http-proxy/src/main/java/org/zlambda/projects/MonitorSingleton.java: -------------------------------------------------------------------------------- 1 | package org.zlambda.projects; 2 | 3 | import org.zlambda.projects.buffer.DirectChannelBufferPool; 4 | import org.zlambda.projects.context.ConnectionContext; 5 | import org.zlambda.projects.context.SystemContext; 6 | 7 | import java.util.HashMap; 8 | import java.util.Iterator; 9 | import java.util.Map; 10 | 11 | public class MonitorSingleton { 12 | private static Monitor INSTANCE = null; 13 | 14 | private MonitorSingleton() { 15 | } 16 | 17 | public static Monitor get() { 18 | return INSTANCE; 19 | } 20 | 21 | public static void init(final SystemContext context) { 22 | INSTANCE = context.enableMonitor() ? create(context) : createDummy(); 23 | } 24 | 25 | private static Monitor create(SystemContext context) { 26 | return new Monitor() { 27 | private final Object channelMapMonitor = new Object(); 28 | private final Map CHANNEL_STATS = new HashMap<>(); 29 | private final ConnectionContext dummyContext = new ConnectionContext(); 30 | 31 | @Override 32 | public void collectChannelPair(ConnectionContext client, ConnectionContext host) { 33 | synchronized (channelMapMonitor) { 34 | CHANNEL_STATS.put(client, null == host ? dummyContext : host); 35 | } 36 | } 37 | 38 | /** 39 | * This method can always dump the latest state of the channel, because the "toString" method 40 | * of socketChannel has internal locking ! (channel is thread-safe class) 41 | */ 42 | @Override 43 | public String dumpStats() { 44 | StringBuilder sb = new StringBuilder(); 45 | synchronized (channelMapMonitor) { 46 | Iterator> iterator = 47 | CHANNEL_STATS.entrySet().iterator(); 48 | int activeChannels = 0; 49 | while (iterator.hasNext()) { 50 | Map.Entry next = iterator.next(); 51 | ConnectionContext client = next.getKey(); 52 | ConnectionContext host = next.getValue(); 53 | if (!client.isOpen() && (dummyContext.equals(host) || !host.isOpen())) { 54 | iterator.remove(); 55 | } else { 56 | activeChannels++; 57 | String line = String.format( 58 | "%s -> %s\n", client.getChannel(), 59 | dummyContext.equals(host) ? "un-register" : host.getChannel()); 60 | sb.append(line.replaceAll("java\\.nio\\.channels\\.SocketChannel", "")); 61 | } 62 | } 63 | sb.append("active channels <" + activeChannels + ">\n"); 64 | } 65 | if (context.isUseDirectBuffer()) { 66 | DirectChannelBufferPool bufferPool = (DirectChannelBufferPool) context.getBufferPool(); 67 | sb.append(String.format("un-release buffers <%d>\n", bufferPool.numUsedBuffers())); 68 | } 69 | return sb.toString(); 70 | } 71 | }; 72 | } 73 | 74 | private static Monitor createDummy() { 75 | return new Monitor() { 76 | @Override 77 | public void collectChannelPair(ConnectionContext client, ConnectionContext host) { 78 | 79 | } 80 | 81 | @Override 82 | public String dumpStats() { 83 | return ""; 84 | } 85 | }; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /nio-http-proxy/src/main/java/org/zlambda/projects/MonitorThread.java: -------------------------------------------------------------------------------- 1 | package org.zlambda.projects; 2 | 3 | import org.slf4j.Logger; 4 | import org.zlambda.projects.context.SystemContext; 5 | import org.zlambda.projects.utils.Common; 6 | 7 | public class MonitorThread extends Thread { 8 | private static final Logger LOGGER = Common.getSystemLogger(); 9 | private final SystemContext context; 10 | private final int SECOND = 1000; 11 | 12 | public MonitorThread(SystemContext context) { 13 | super(MonitorThread.class.getSimpleName()); 14 | this.context = context; 15 | } 16 | 17 | @Override 18 | public void run() { 19 | if (!context.enableMonitor()) { 20 | return; 21 | } 22 | while (true) { 23 | try { 24 | sleep(context.getMonitorUpdateInterval() * SECOND); 25 | LOGGER.info("Monitor Stats\n{}", MonitorSingleton.get().dumpStats()); 26 | } catch (Exception e) { 27 | LOGGER.error("Debugger failed.", e); 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /nio-http-proxy/src/main/java/org/zlambda/projects/NIOHttpProxy.java: -------------------------------------------------------------------------------- 1 | package org.zlambda.projects; 2 | 3 | import org.slf4j.Logger; 4 | import org.zlambda.projects.context.SystemContext; 5 | import org.zlambda.projects.utils.Common; 6 | 7 | import java.util.Arrays; 8 | import java.util.List; 9 | import java.util.concurrent.LinkedBlockingQueue; 10 | 11 | public class NIOHttpProxy { 12 | private static final Logger LOGGER = Common.getSystemLogger(); 13 | private final SystemContext systemContext; 14 | private final List failThenTerminateJVM; 15 | 16 | public NIOHttpProxy() { 17 | systemContext = new SystemContext.Builder() 18 | .clientQueue(new LinkedBlockingQueue<>()) 19 | .numWorkers(Integer.parseInt(System.getProperty("worker", "8"))) 20 | .port(Integer.parseInt(System.getProperty("port", "9999"))) 21 | .enableMonitor(Boolean.parseBoolean(System.getProperty("enableMonitor", "true"))) 22 | /** 23 | * Each proxy connection use 2 channelBufferS, 24 | * one for upstream [Client -> Host] 25 | * one for downstream [Client <- Host] 26 | */ 27 | .minBuffers(Integer.parseInt(System.getProperty("minNumBuffers", "100"))) 28 | .maxBuffers(Integer.parseInt(System.getProperty("maxNumBuffers", "200"))) 29 | .bufferSize(Integer.parseInt(System.getProperty("bufferSize", "10"))) // unit KB 30 | .useDirectBuffer(Boolean.parseBoolean(System.getProperty("useDirectBuffer", "true"))) 31 | .monitorUpdateInterval(Integer.parseInt(System.getProperty("monitorUpdateInterval", "30"))) // second 32 | .build(); 33 | 34 | LOGGER.info("current system settings:\n{}", systemContext); 35 | MonitorSingleton.init(systemContext); 36 | failThenTerminateJVM = Arrays.asList( 37 | new ConnectionListener(systemContext), 38 | new Dispatcher(systemContext), 39 | new MonitorThread(systemContext) 40 | ); 41 | } 42 | 43 | public static void main(String[] args) { 44 | new NIOHttpProxy().start(); 45 | } 46 | 47 | public void start() { 48 | failThenTerminateJVM.forEach(Thread::start); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /nio-http-proxy/src/main/java/org/zlambda/projects/Worker.java: -------------------------------------------------------------------------------- 1 | package org.zlambda.projects; 2 | 3 | import org.slf4j.Logger; 4 | import org.zlambda.projects.context.WorkerContext; 5 | import org.zlambda.projects.utils.Common; 6 | import org.zlambda.projects.utils.SelectionKeyUtils; 7 | 8 | import java.nio.channels.SelectionKey; 9 | import java.nio.channels.Selector; 10 | import java.nio.channels.SocketChannel; 11 | import java.util.Iterator; 12 | 13 | public class Worker implements Runnable { 14 | private static final Logger LOGGER = Common.getSystemLogger(); 15 | private final WorkerContext context; 16 | 17 | public Worker(WorkerContext context) { 18 | this.context = context; 19 | } 20 | 21 | @Override 22 | public void run() { 23 | try { 24 | context.setName(Thread.currentThread().getName()); 25 | LOGGER.info("start worker <{}>.", context.getName()); 26 | Selector selector = context.getSelector(); 27 | while (true) { 28 | int selected = selector.select(); 29 | synchronized (context.getWakeupBarrier()) { 30 | } 31 | context.setNumConnections(selector.keys().size()); 32 | if (0 == selected) { 33 | continue; 34 | } 35 | Iterator iterator = selector.selectedKeys().iterator(); 36 | while (iterator.hasNext()) { 37 | SelectionKey key = iterator.next(); 38 | if (!key.isValid()) { 39 | LOGGER.error("got invalid key <{}>.", SelectionKeyUtils.getName(key)); 40 | } else { 41 | ((EventHandler) key.attachment()).execute(key); 42 | } 43 | iterator.remove(); 44 | } 45 | } 46 | } catch (Exception e) { 47 | LOGGER.error("got unexpected error. so terminate worker <{}>", context.getName(), e); 48 | synchronized (context.getContextSetMonitor()) { 49 | context.getContextSet().remove(context); 50 | } 51 | /** 52 | * close of selector will not immediate close the socket channel, 53 | * so we need to cleanup 54 | */ 55 | for (SelectionKey key : context.getSelector().keys()) { 56 | SocketChannel socketChannel = SelectionKeyUtils.getSocketChannel(key); 57 | Common.close(socketChannel); 58 | } 59 | Common.close(context.getSelector()); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /nio-http-proxy/src/main/java/org/zlambda/projects/buffer/ChannelBuffer.java: -------------------------------------------------------------------------------- 1 | package org.zlambda.projects.buffer; 2 | 3 | import java.io.IOException; 4 | import java.io.InputStream; 5 | import java.nio.channels.SocketChannel; 6 | 7 | public interface ChannelBuffer { 8 | /** 9 | * [Channel -> Buffer] Read from channel and write to buffer 10 | */ 11 | int read(SocketChannel channel) throws IOException; 12 | 13 | /** 14 | * [Buffer -> Channel] Read from buffer and write to buffer 15 | */ 16 | int write(SocketChannel channel) throws IOException; 17 | 18 | /** 19 | * Put {code bytes} into buffer 20 | */ 21 | void put(byte[] bytes); 22 | 23 | /** 24 | * Create a "view" input stream, which means the read operation of the input stream will not modify 25 | * the actual pointer of the internal buffer 26 | */ 27 | InputStream toViewInputStream(); 28 | 29 | /** 30 | * return the size of unconsumed data 31 | */ 32 | int size(); 33 | 34 | boolean empty(); 35 | 36 | void clear(); 37 | 38 | void free(); 39 | } 40 | 41 | -------------------------------------------------------------------------------- /nio-http-proxy/src/main/java/org/zlambda/projects/buffer/ChannelBufferPool.java: -------------------------------------------------------------------------------- 1 | package org.zlambda.projects.buffer; 2 | 3 | public interface ChannelBufferPool { 4 | T take(); 5 | void release(T buffer); 6 | 7 | int size(); 8 | 9 | /** 10 | * return the current number of used buffers 11 | */ 12 | int numUsedBuffers(); 13 | } 14 | -------------------------------------------------------------------------------- /nio-http-proxy/src/main/java/org/zlambda/projects/buffer/ConnectionBuffer.java: -------------------------------------------------------------------------------- 1 | package org.zlambda.projects.buffer; 2 | 3 | public class ConnectionBuffer { 4 | private final ChannelBuffer downstream, upstream; 5 | private final ChannelBufferPool pool; 6 | 7 | public ConnectionBuffer(ChannelBufferPool pool) { 8 | this.downstream = pool.take(); 9 | this.upstream = pool.take(); 10 | this.pool = pool; 11 | } 12 | 13 | public ChannelBuffer downstream() { 14 | return downstream; 15 | } 16 | 17 | public ChannelBuffer upstream() { 18 | return upstream; 19 | } 20 | 21 | public void free() { 22 | pool.release(upstream); 23 | pool.release(downstream); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /nio-http-proxy/src/main/java/org/zlambda/projects/buffer/DirectChannelBufferPool.java: -------------------------------------------------------------------------------- 1 | package org.zlambda.projects.buffer; 2 | 3 | import com.google.common.base.Preconditions; 4 | 5 | import org.slf4j.Logger; 6 | import org.zlambda.projects.utils.Common; 7 | 8 | import java.io.IOException; 9 | import java.io.InputStream; 10 | import java.nio.ByteBuffer; 11 | import java.nio.channels.SocketChannel; 12 | import java.util.concurrent.ArrayBlockingQueue; 13 | import java.util.concurrent.BlockingQueue; 14 | import java.util.concurrent.atomic.AtomicInteger; 15 | 16 | /** 17 | * Threadsafe Class 18 | * 19 | * Part of the code refers to https://github.com/midonet/midonet/blob/master/netlink/src/main/java/org/midonet/netlink/BufferPool.java 20 | * 21 | * This implementation removes the checking ownership checking of byte buffer when it is released 22 | */ 23 | public class DirectChannelBufferPool implements ChannelBufferPool { 24 | private static final int KB = 1024; 25 | private static final Logger LOGGER = Common.getSystemLogger(); 26 | private final int minNumBuffers; 27 | private final int maxNumBuffers; 28 | private final int bufferSize; 29 | private final BlockingQueue pool; 30 | private final AtomicInteger numBuffers; 31 | private final AtomicInteger usedBuffers; 32 | 33 | /** 34 | * @param bufferSize in KB 35 | */ 36 | public DirectChannelBufferPool(int minNumBuffers, int maxNumBuffers, int bufferSize) { 37 | Preconditions.checkArgument( 38 | minNumBuffers < maxNumBuffers, 39 | "minNumBuffers should not greater than maxNumBuffers" 40 | ); 41 | Preconditions.checkArgument( 42 | minNumBuffers > 0 && maxNumBuffers > 0 && bufferSize > 0, 43 | "minNumBuffers, maxNumBuffers, bufferSize should > 0" 44 | ); 45 | this.minNumBuffers = minNumBuffers; 46 | this.maxNumBuffers = maxNumBuffers; 47 | this.bufferSize = bufferSize * KB; 48 | this.pool = new ArrayBlockingQueue<>(maxNumBuffers); 49 | this.numBuffers = new AtomicInteger(0); 50 | this.usedBuffers = new AtomicInteger(0); 51 | init(); 52 | } 53 | 54 | private void init() { 55 | while (numBuffers.getAndIncrement() < minNumBuffers) { 56 | pool.offer(ByteBuffer.allocate(bufferSize)); 57 | } 58 | } 59 | 60 | @Override 61 | public int size() { 62 | return numBuffers.get(); 63 | } 64 | 65 | @Override 66 | public int numUsedBuffers() { 67 | return usedBuffers.get(); 68 | } 69 | 70 | @Override 71 | public ChannelBuffer take() { 72 | usedBuffers.incrementAndGet(); 73 | ByteBuffer byteBuffer = pool.poll(); 74 | if (null == byteBuffer) { 75 | if (numBuffers.incrementAndGet() <= maxNumBuffers) { 76 | byteBuffer = ByteBuffer.allocateDirect(bufferSize); 77 | } else { 78 | LOGGER.warn("onHeap buffer is used."); 79 | numBuffers.decrementAndGet(); 80 | byteBuffer = ByteBuffer.allocate(bufferSize); 81 | } 82 | } 83 | byteBuffer.clear(); 84 | return new DirectChannelBuffer(byteBuffer, this); 85 | } 86 | 87 | @Override 88 | public void release(ChannelBuffer channelBuffer) { 89 | channelBuffer.free(); 90 | } 91 | 92 | private void doRelease(ByteBuffer byteBuffer) { 93 | usedBuffers.decrementAndGet(); 94 | if (null == byteBuffer || !byteBuffer.isDirect()) { 95 | return; 96 | } 97 | pool.offer(byteBuffer); 98 | } 99 | 100 | private static class DirectChannelBuffer implements ChannelBuffer { 101 | private final ByteBuffer internal; 102 | private final DirectChannelBufferPool pool; 103 | private boolean isFree = false; 104 | 105 | public DirectChannelBuffer(ByteBuffer byteBuffer, DirectChannelBufferPool pool) { 106 | this.internal = byteBuffer; 107 | this.pool = pool; 108 | } 109 | 110 | /** 111 | * Invariant: 0 index should point to the first available byte in the buffer internal.position() 112 | * should points to first free space 113 | */ 114 | @Override 115 | public int read(SocketChannel channel) throws IOException { 116 | return channel.read(internal); 117 | } 118 | 119 | /** 120 | * Invariant: 0 index should point to the first available byte in the buffer internal.position() 121 | * should points to first free space 122 | */ 123 | @Override 124 | public int write(SocketChannel channel) throws IOException { 125 | internal.flip(); 126 | int ret = channel.write(internal); 127 | internal.compact(); 128 | return ret; 129 | } 130 | 131 | /** 132 | * Invariant: 0 index should point to the first available byte in the buffer internal.position() 133 | * should points to first free space 134 | */ 135 | @Override 136 | public void put(byte[] bytes) { 137 | internal.put(bytes); 138 | } 139 | 140 | /** 141 | * Invariant: @{code internal}'s state should not be modified 142 | */ 143 | @Override 144 | public InputStream toViewInputStream() { 145 | return new IncrementalInputStream(internal.asReadOnlyBuffer()); 146 | } 147 | 148 | /** 149 | * Invariant: 0 index should point to the first available byte in the buffer internal.position() 150 | * should points to first free space 151 | */ 152 | @Override 153 | public int size() { 154 | return internal.position(); // it is obvious from the invariant 155 | } 156 | 157 | @Override 158 | public boolean empty() { 159 | return size() == 0; 160 | } 161 | 162 | @Override 163 | public void clear() { 164 | internal.clear(); 165 | } 166 | 167 | @Override 168 | public void free() { 169 | if (isFree) { 170 | return; 171 | } 172 | pool.doRelease(internal); 173 | isFree = true; 174 | } 175 | 176 | /** 177 | * this input stream will not read everything in the byte buffer into the on heap cache. 178 | * Instead, it will read it CACHE_SIZE each time when there is not enough data in the on heap 179 | * cache. 180 | */ 181 | private static class IncrementalInputStream extends InputStream { 182 | private static int CACHE_SIZE = 1024; 183 | private final byte[] onHeapCache = new byte[CACHE_SIZE]; 184 | private final ByteBuffer byteBuffer; 185 | private int currentLimit; 186 | private int i; 187 | 188 | IncrementalInputStream(ByteBuffer readOnlyByteBuffer) { 189 | readOnlyByteBuffer.flip(); 190 | this.byteBuffer = readOnlyByteBuffer; 191 | this.currentLimit = 0; 192 | this.i = 0; 193 | } 194 | 195 | @Override 196 | public int read() throws IOException { 197 | if (i == currentLimit) { 198 | i = 0; 199 | this.currentLimit = Math.min(byteBuffer.remaining(), CACHE_SIZE); 200 | if (this.currentLimit == 0) { 201 | return -1; 202 | } 203 | byteBuffer.get(onHeapCache, 0, currentLimit); 204 | } 205 | return onHeapCache[i++]; 206 | } 207 | } 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /nio-http-proxy/src/main/java/org/zlambda/projects/buffer/HeapChannelBufferPool.java: -------------------------------------------------------------------------------- 1 | package org.zlambda.projects.buffer; 2 | 3 | import java.io.IOException; 4 | import java.io.InputStream; 5 | import java.nio.ByteBuffer; 6 | import java.nio.channels.SocketChannel; 7 | import java.util.concurrent.atomic.AtomicInteger; 8 | 9 | /** 10 | * Thread Safe Class 11 | */ 12 | public class HeapChannelBufferPool implements ChannelBufferPool { 13 | private final int bufferSize; 14 | private final AtomicInteger usedBuffers; 15 | 16 | public HeapChannelBufferPool(int bufferSize) { 17 | this.bufferSize = bufferSize; 18 | this.usedBuffers = new AtomicInteger(0); 19 | } 20 | 21 | @Override 22 | public ChannelBuffer take() { 23 | usedBuffers.incrementAndGet(); 24 | return new SimpleChannelBuffer(bufferSize, this); 25 | } 26 | 27 | @Override 28 | public void release(ChannelBuffer buffer) { 29 | buffer.free(); 30 | } 31 | 32 | private void doRelease() { 33 | usedBuffers.decrementAndGet(); 34 | } 35 | 36 | @Override 37 | public int size() { 38 | return usedBuffers.get(); 39 | } 40 | 41 | @Override 42 | public int numUsedBuffers() { 43 | return size(); 44 | } 45 | 46 | private static class SimpleChannelBuffer implements ChannelBuffer { 47 | private static final int KB = 1024; 48 | private final ByteBuffer internal; 49 | private boolean isFree = false; 50 | private final HeapChannelBufferPool pool; 51 | 52 | public SimpleChannelBuffer(int size, HeapChannelBufferPool pool) { 53 | this.internal = ByteBuffer.allocate(size * KB); 54 | this.pool = pool; 55 | } 56 | 57 | /** 58 | * Invariant: 0 index should point to the first available byte in the buffer internal.position() 59 | * should points to first free space 60 | */ 61 | @Override 62 | public int read(SocketChannel channel) throws IOException { 63 | return channel.read(internal); 64 | } 65 | 66 | /** 67 | * Invariant: 0 index should point to the first available byte in the buffer internal.position() 68 | * should points to first free space 69 | */ 70 | @Override 71 | public int write(SocketChannel channel) throws IOException { 72 | internal.flip(); 73 | int ret = channel.write(internal); 74 | internal.compact(); 75 | return ret; 76 | } 77 | 78 | /** 79 | * Invariant: 0 index should point to the first available byte in the buffer internal.position() 80 | * should points to first free space 81 | */ 82 | @Override 83 | public void put(byte[] bytes) { 84 | internal.put(bytes); 85 | } 86 | 87 | /** 88 | * Invariant: @{code internal}'s state should not be modified 89 | */ 90 | @Override 91 | public InputStream toViewInputStream() { 92 | return new InputStream() { 93 | int limit = internal.position(); 94 | int i = 0; 95 | 96 | @Override 97 | public int read() throws IOException { 98 | if (i < limit) { 99 | return internal.array()[i++]; 100 | } else { 101 | return -1; 102 | } 103 | } 104 | }; 105 | } 106 | 107 | /** 108 | * Invariant: 0 index should point to the first available byte in the buffer internal.position() 109 | * should points to first free space 110 | */ 111 | @Override 112 | public int size() { 113 | return internal.position(); // it is obvious from the invariant 114 | } 115 | 116 | @Override 117 | public boolean empty() { 118 | return size() == 0; 119 | } 120 | 121 | @Override 122 | public void clear() { 123 | internal.clear(); 124 | } 125 | 126 | @Override 127 | public void free() { 128 | if (isFree) { 129 | return; 130 | } 131 | isFree = true; 132 | pool.doRelease(); 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /nio-http-proxy/src/main/java/org/zlambda/projects/context/ConnectionContext.java: -------------------------------------------------------------------------------- 1 | package org.zlambda.projects.context; 2 | 3 | import org.slf4j.Logger; 4 | import org.zlambda.projects.buffer.ChannelBuffer; 5 | import org.zlambda.projects.utils.Common; 6 | import org.zlambda.projects.utils.SelectionKeyUtils; 7 | import org.zlambda.projects.utils.SocketChannelUtils; 8 | 9 | import java.io.IOException; 10 | import java.net.Socket; 11 | import java.nio.channels.SelectionKey; 12 | import java.nio.channels.SocketChannel; 13 | 14 | public class ConnectionContext { 15 | private static Logger LOGGER = Common.getSystemLogger(); 16 | private final SelectionKey key; 17 | private final String name; 18 | private final SocketChannel channel; 19 | 20 | public ConnectionContext(SelectionKey key, String name) { 21 | this.key = key; 22 | this.name = name; 23 | channel = SelectionKeyUtils.getSocketChannel(key); 24 | } 25 | 26 | public ConnectionContext() { 27 | this.key = null; 28 | this.name = ""; 29 | this.channel = null; 30 | } 31 | 32 | public int register(int ops) { 33 | return SelectionKeyUtils.addInterestOps(key, ops); 34 | } 35 | 36 | public int unregister(int ops) { 37 | return SelectionKeyUtils.removeInterestOps(key, ops); 38 | } 39 | 40 | public int read(ChannelBuffer buffer) { 41 | return SocketChannelUtils.readFromChannel(channel, buffer); 42 | } 43 | 44 | public int write(ChannelBuffer buffer) { 45 | return SocketChannelUtils.writeToChannel(channel, buffer); 46 | } 47 | 48 | public boolean isInputShutdown() { 49 | Socket socket = channel.socket(); 50 | return socket.isClosed() || !socket.isConnected() || socket.isInputShutdown(); 51 | } 52 | 53 | public boolean isOutputShutdown() { 54 | Socket socket = channel.socket(); 55 | return socket.isClosed() || !socket.isConnected() || socket.isOutputShutdown(); 56 | } 57 | 58 | public boolean isConnected() { 59 | return channel.isConnected(); 60 | } 61 | 62 | public boolean isOpen() { 63 | return channel.isOpen(); 64 | } 65 | 66 | public boolean isIOShutdown() { 67 | return isInputShutdown() && isOutputShutdown(); 68 | } 69 | 70 | public SelectionKey getKey() { 71 | return key; 72 | } 73 | 74 | public String getName() { 75 | return name; 76 | } 77 | 78 | public SocketChannel getChannel() { 79 | return channel; 80 | } 81 | 82 | public boolean isReadable() { 83 | return key.isValid() && key.isReadable(); 84 | } 85 | 86 | public boolean isAcceptable() { 87 | return key.isValid() && key.isAcceptable(); 88 | } 89 | 90 | public boolean isWritable() { 91 | return key.isValid() && key.isWritable(); 92 | } 93 | 94 | public boolean isConnectable() { 95 | return key.isValid() && key.isConnectable(); 96 | } 97 | 98 | public void shutdownIS() { 99 | tryShutdownIS(); 100 | closeIfNeed(); 101 | } 102 | 103 | private void tryShutdownIS() { 104 | if (!isInputShutdown()) { 105 | try { 106 | channel.shutdownInput(); 107 | unregister(SelectionKey.OP_READ); 108 | } catch (IOException e) { 109 | LOGGER.error("failed to shutdown output of socket channel <{}>", name, e); 110 | } 111 | } 112 | } 113 | 114 | public void shutdownOS() { 115 | tryShutdownOS(); 116 | closeIfNeed(); 117 | } 118 | 119 | private void tryShutdownOS() { 120 | if (!isOutputShutdown()) { 121 | try { 122 | channel.shutdownOutput(); 123 | unregister(SelectionKey.OP_WRITE); 124 | } catch (IOException e) { 125 | LOGGER.error("failed to shutdown output of socket channel <{}>", name, e); 126 | } 127 | } 128 | } 129 | 130 | public void closeIfNeed() { 131 | if (isInputShutdown() && isOutputShutdown()) { 132 | closeIO(); 133 | } 134 | } 135 | 136 | public void closeIO() { 137 | if (!isOpen()) { 138 | return; 139 | } 140 | tryShutdownIS(); 141 | tryShutdownOS(); 142 | try { 143 | channel.close(); 144 | } catch (IOException e) { 145 | LOGGER.error("failed to close socket channel <{}>", name, e); 146 | } 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /nio-http-proxy/src/main/java/org/zlambda/projects/context/ProxyContext.java: -------------------------------------------------------------------------------- 1 | package org.zlambda.projects.context; 2 | 3 | import org.zlambda.projects.buffer.ConnectionBuffer; 4 | 5 | import java.nio.channels.Selector; 6 | 7 | public class ProxyContext { 8 | private final ConnectionBuffer connectionBuffer; 9 | private ConnectionContext client; 10 | private ConnectionContext host; 11 | private boolean isHttps = false; 12 | 13 | public ProxyContext(SystemContext systemContext) { 14 | this.connectionBuffer = new ConnectionBuffer(systemContext.getBufferPool()); 15 | } 16 | 17 | public ConnectionContext getClient() { 18 | return client; 19 | } 20 | 21 | public void setClient(ConnectionContext client) { 22 | this.client = client; 23 | } 24 | 25 | public ConnectionContext getHost() { 26 | return host; 27 | } 28 | 29 | public void setHost(ConnectionContext host) { 30 | this.host = host; 31 | } 32 | 33 | public void markAsHttps() { 34 | this.isHttps = true; 35 | } 36 | 37 | public boolean isHttps() { 38 | return isHttps; 39 | } 40 | 41 | public ConnectionBuffer getConnectionBuffer() { 42 | return connectionBuffer; 43 | } 44 | 45 | public void cleanup() { 46 | if (!client.isOpen() && (null == host || !host.isOpen())) { 47 | connectionBuffer.free(); 48 | } 49 | } 50 | 51 | public Selector selector() { 52 | return getClient().getKey().selector(); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /nio-http-proxy/src/main/java/org/zlambda/projects/context/SystemContext.java: -------------------------------------------------------------------------------- 1 | package org.zlambda.projects.context; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnore; 4 | 5 | import org.zlambda.projects.buffer.ChannelBuffer; 6 | import org.zlambda.projects.buffer.ChannelBufferPool; 7 | import org.zlambda.projects.buffer.DirectChannelBufferPool; 8 | import org.zlambda.projects.buffer.HeapChannelBufferPool; 9 | import org.zlambda.projects.utils.JsonUtils; 10 | 11 | import java.nio.channels.SocketChannel; 12 | import java.util.concurrent.BlockingQueue; 13 | 14 | /** 15 | * Thread Safe Class 16 | */ 17 | public class SystemContext { 18 | @JsonIgnore 19 | private final BlockingQueue clientQueue; 20 | @JsonIgnore 21 | private final ChannelBufferPool bufferPool; 22 | private final int port; 23 | private final int numWorkers; 24 | private final boolean enableMonitor; 25 | private final boolean useDirectBuffer; 26 | private final int minBuffers; 27 | private final int maxBuffers; 28 | private final int bufferSize; 29 | private final int monitorUpdateInterval; 30 | 31 | private SystemContext(Builder builder) { 32 | this.clientQueue = builder.clientQueue; 33 | this.port = builder.port; 34 | this.numWorkers = builder.numWorkers; 35 | this.enableMonitor = builder.enableMonitor; 36 | this.maxBuffers = builder.maxBuffers; 37 | this.minBuffers = builder.minBuffers; 38 | this.bufferSize = builder.bufferSize; 39 | this.useDirectBuffer = builder.useDirectBuffer; 40 | this.monitorUpdateInterval = builder.monitorUpdateInterval; 41 | this.bufferPool = createBufferPoll(); 42 | } 43 | 44 | private ChannelBufferPool createBufferPoll() { 45 | return useDirectBuffer ? new DirectChannelBufferPool(minBuffers, maxBuffers, bufferSize) : 46 | new HeapChannelBufferPool(bufferSize); 47 | } 48 | 49 | public ChannelBufferPool getBufferPool() { 50 | return bufferPool; 51 | } 52 | 53 | public BlockingQueue getClientQueue() { 54 | return clientQueue; 55 | } 56 | 57 | public int getPort() { 58 | return port; 59 | } 60 | 61 | public int getNumWorkers() { 62 | return numWorkers; 63 | } 64 | 65 | public boolean enableMonitor() { 66 | return enableMonitor; 67 | } 68 | 69 | public boolean isUseDirectBuffer() { 70 | return useDirectBuffer; 71 | } 72 | 73 | public int getMonitorUpdateInterval() { 74 | return monitorUpdateInterval; 75 | } 76 | 77 | @Override 78 | public String toString() { 79 | try { 80 | return JsonUtils.serialize(this); 81 | } catch (Exception e) { 82 | throw new IllegalStateException(e); 83 | } 84 | } 85 | 86 | public static class Builder { 87 | private BlockingQueue clientQueue; 88 | private int port; 89 | private int numWorkers; 90 | private boolean enableMonitor; 91 | private boolean useDirectBuffer; 92 | private int minBuffers; 93 | private int maxBuffers; 94 | private int bufferSize; 95 | private int monitorUpdateInterval; 96 | 97 | public Builder clientQueue(BlockingQueue queue) { 98 | this.clientQueue = queue; 99 | return this; 100 | } 101 | 102 | public Builder port(int port) { 103 | this.port = port; 104 | return this; 105 | } 106 | 107 | public Builder numWorkers(int num) { 108 | this.numWorkers = num; 109 | return this; 110 | } 111 | 112 | public Builder bufferSize(int size) { 113 | this.bufferSize = size; 114 | return this; 115 | } 116 | 117 | public Builder enableMonitor(boolean enableMonitor) { 118 | this.enableMonitor = enableMonitor; 119 | return this; 120 | } 121 | 122 | public Builder minBuffers(int minBuffers) { 123 | this.minBuffers = minBuffers; 124 | return this; 125 | } 126 | 127 | public Builder maxBuffers(int maxBuffers) { 128 | this.maxBuffers = maxBuffers; 129 | return this; 130 | } 131 | 132 | public Builder useDirectBuffer(boolean useDirectBuffer) { 133 | this.useDirectBuffer = useDirectBuffer; 134 | return this; 135 | } 136 | 137 | public Builder monitorUpdateInterval(int monitorUpdateInterval) { 138 | this.monitorUpdateInterval = monitorUpdateInterval; 139 | return this; 140 | } 141 | 142 | public SystemContext build() { 143 | return new SystemContext(this); 144 | } 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /nio-http-proxy/src/main/java/org/zlambda/projects/context/WorkerContext.java: -------------------------------------------------------------------------------- 1 | package org.zlambda.projects.context; 2 | 3 | import java.nio.channels.Selector; 4 | import java.util.Set; 5 | 6 | public class WorkerContext { 7 | private final Object wakeupBarrier = new Object(); 8 | private final Object contextSetMonitor; 9 | private final Set contextSet; 10 | private final Selector selector; 11 | private volatile String name = "unnamed (not-start)"; 12 | private volatile int numConnections = 0; 13 | 14 | private WorkerContext(Builder builder) { 15 | this.selector = builder.selector; 16 | this.contextSet = builder.contextSet; 17 | this.contextSetMonitor = builder.contextSetMonitor; 18 | } 19 | 20 | public String getName() { 21 | return name; 22 | } 23 | 24 | public void setName(String name) { 25 | this.name = name; 26 | } 27 | 28 | public int getNumConnections() { 29 | return numConnections; 30 | } 31 | 32 | public void setNumConnections(int numConnections) { 33 | this.numConnections = numConnections; 34 | } 35 | 36 | public Object getWakeupBarrier() { 37 | return wakeupBarrier; 38 | } 39 | 40 | public Selector getSelector() { 41 | return selector; 42 | } 43 | 44 | public Object getContextSetMonitor() { 45 | return contextSetMonitor; 46 | } 47 | 48 | public Set getContextSet() { 49 | return contextSet; 50 | } 51 | 52 | public static class Builder { 53 | private Selector selector; 54 | private Set contextSet; 55 | private Object contextSetMonitor; 56 | 57 | public Builder() { 58 | } 59 | 60 | public Builder selector(Selector selector) { 61 | this.selector = selector; 62 | return this; 63 | } 64 | 65 | public Builder contextSetMonitor(Object contextSetMonitor) { 66 | this.contextSetMonitor = contextSetMonitor; 67 | return this; 68 | } 69 | 70 | public Builder contextSet(Set contextSet) { 71 | this.contextSet = contextSet; 72 | return this; 73 | } 74 | 75 | public WorkerContext build() { 76 | return new WorkerContext(this); 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /nio-http-proxy/src/main/java/org/zlambda/projects/utils/Common.java: -------------------------------------------------------------------------------- 1 | package org.zlambda.projects.utils; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.zlambda.projects.NIOHttpProxy; 6 | 7 | import java.io.Closeable; 8 | import java.io.IOException; 9 | 10 | public enum Common { 11 | ; 12 | 13 | private static final Logger systemLogger = LoggerFactory.getLogger(NIOHttpProxy.class); 14 | 15 | public static Logger getSystemLogger() { 16 | return systemLogger; 17 | } 18 | 19 | @SuppressWarnings("unchecked") 20 | public static Sub downCast(Super sp) { 21 | return (Sub) sp; 22 | } 23 | 24 | public static void close(Closeable closeable, String name) { 25 | if (null == closeable) { 26 | return; 27 | } 28 | try { 29 | closeable.close(); 30 | } catch (IOException e) { 31 | String msg = null == name ? "failed to close" : "failed to close <" + name + ">"; 32 | getSystemLogger().error("{}", msg, e); 33 | } 34 | } 35 | 36 | public static void close(Closeable closeable) { 37 | close(closeable, null); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /nio-http-proxy/src/main/java/org/zlambda/projects/utils/JsonUtils.java: -------------------------------------------------------------------------------- 1 | package org.zlambda.projects.utils; 2 | 3 | import com.fasterxml.jackson.annotation.JsonAutoDetect; 4 | import com.fasterxml.jackson.annotation.JsonInclude; 5 | import com.fasterxml.jackson.annotation.PropertyAccessor; 6 | import com.fasterxml.jackson.databind.DeserializationFeature; 7 | import com.fasterxml.jackson.databind.ObjectMapper; 8 | import com.fasterxml.jackson.databind.SerializationFeature; 9 | 10 | import java.io.IOException; 11 | 12 | public enum JsonUtils { 13 | ; 14 | 15 | private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper() 16 | .setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE) 17 | .setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY) 18 | .setSerializationInclusion(JsonInclude.Include.NON_NULL) 19 | .configure(SerializationFeature.INDENT_OUTPUT, true) 20 | .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); 21 | 22 | public static String serialize(Object obj) throws Exception { 23 | return OBJECT_MAPPER.writeValueAsString(obj); 24 | } 25 | 26 | public static T deserialize(String json, Class type) throws IOException { 27 | return OBJECT_MAPPER.readValue(json, type); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /nio-http-proxy/src/main/java/org/zlambda/projects/utils/SelectionKeyUtils.java: -------------------------------------------------------------------------------- 1 | package org.zlambda.projects.utils; 2 | 3 | import org.slf4j.Logger; 4 | 5 | import java.nio.channels.SelectableChannel; 6 | import java.nio.channels.SelectionKey; 7 | import java.nio.channels.ServerSocketChannel; 8 | import java.nio.channels.SocketChannel; 9 | 10 | public enum SelectionKeyUtils { 11 | ; 12 | private static final Logger LOGGER = Common.getSystemLogger(); 13 | 14 | public static SocketChannel getSocketChannel(SelectionKey key) { 15 | return getChannel(key); 16 | } 17 | 18 | public static ServerSocketChannel getServerSocketChannel(SelectionKey key) { 19 | return getChannel(key); 20 | } 21 | 22 | private static T getChannel(SelectionKey key) { 23 | return Common.downCast(key.channel()); 24 | } 25 | 26 | public static String getName(SelectionKey key) { 27 | return SocketChannelUtils.getName(getSocketChannel(key)); 28 | } 29 | 30 | public static int removeInterestOps(SelectionKey key, int ops) { 31 | if (!key.isValid()) { 32 | return -1; 33 | } 34 | int newOps = key.interestOps() & ~ops; 35 | key.interestOps(newOps); 36 | return newOps; 37 | } 38 | 39 | public static int addInterestOps(SelectionKey key, int ops) { 40 | if (!key.isValid()) { 41 | return -1; 42 | } 43 | int newOps = key.interestOps() | ops; 44 | key.interestOps(newOps); 45 | return newOps; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /nio-http-proxy/src/main/java/org/zlambda/projects/utils/SocketChannelUtils.java: -------------------------------------------------------------------------------- 1 | package org.zlambda.projects.utils; 2 | 3 | import org.slf4j.Logger; 4 | import org.zlambda.projects.buffer.ChannelBuffer; 5 | 6 | import java.io.IOException; 7 | import java.net.SocketAddress; 8 | import java.nio.channels.SocketChannel; 9 | 10 | public enum SocketChannelUtils { 11 | ; 12 | private static final Logger LOGGER = Common.getSystemLogger(); 13 | 14 | public static SocketAddress getRemoteAddress(SocketChannel socketChannel) { 15 | return socketChannel.socket().getRemoteSocketAddress(); 16 | } 17 | 18 | public static int readFromChannel(SocketChannel channel, ChannelBuffer buffer) { 19 | try { 20 | int numOfRead = buffer.read(channel); 21 | if (-1 == numOfRead) { 22 | LOGGER.debug("reading <{}> got EOF.", getRemoteAddress(channel)); 23 | } 24 | return numOfRead; 25 | } catch (IOException e) { 26 | if ("Connection reset by peer".equals(e.getMessage())) { 27 | LOGGER.debug("Failed to read from <{}>, reason <{}>.", getRemoteAddress(channel), 28 | e.getMessage()); 29 | } else { 30 | LOGGER.error("Failed to read from <{}>.", getRemoteAddress(channel), e); 31 | } 32 | return -1; 33 | } 34 | } 35 | 36 | public static int writeToChannel(SocketChannel channel, ChannelBuffer buffer) { 37 | try { 38 | return buffer.write(channel); 39 | } catch (IOException e) { 40 | if ("Broken pipe".equals(e.getMessage())) { 41 | LOGGER.debug("Failed to write to <{}>, reason <{}>.", getRemoteAddress(channel), 42 | e.getMessage()); 43 | } else { 44 | LOGGER.error("Failed to write to <{}>.", channel.toString(), e); 45 | } 46 | return -1; 47 | } 48 | } 49 | 50 | public static String getName(SocketChannel channel) { 51 | SocketAddress remoteSocketAddress = channel.socket().getRemoteSocketAddress(); 52 | return null == remoteSocketAddress ? "unconnected" : remoteSocketAddress.toString(); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /nio-http-proxy/src/main/resources/log4j.properties: -------------------------------------------------------------------------------- 1 | log4j.appender.CA=org.apache.log4j.ConsoleAppender 2 | log4j.appender.CA.layout=org.apache.log4j.PatternLayout 3 | log4j.appender.CA.layout.ConversionPattern=%d{yyyy/MM/dd HH:mm:ss,SSSS} %-5p %C [%t]: %m%n 4 | log4j.rootLogger=INFO,CA 5 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | 5 | org.zlambda.projects 6 | http-proxy 7 | 1.0.0-SNAPSHOT 8 | pom 9 | 10 | 11 | nio-http-proxy 12 | 13 | 14 | 15 | 16 | 17 | 18 | --------------------------------------------------------------------------------