├── .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 |
--------------------------------------------------------------------------------