// Copyright (c) 2014 Tom Zhou<iwebpp@gmail.com> package com.iwebpp.node.http; import java.util.Hashtable; import java.util.LinkedList; import java.util.List; import java.util.Map; import com.iwebpp.node.EventEmitter2; import com.iwebpp.node.NodeContext; import com.iwebpp.node.Util; import com.iwebpp.node.net.AbstractSocket; import com.iwebpp.node.net.AbstractSocket.ConnectListener; import com.iwebpp.node.net.TCP; import com.iwebpp.node.net.UDT; //New Agent code. //The largest departure from the previous implementation is that //an Agent instance holds connections for a variable number of host:ports. //Surprisingly, this is still API compatible as far as third parties are //concerned. The only code that really notices the difference is the //request object. //Another departure is that all code related to HTTP parsing is in //ClientRequest.onSocket(). The Agent is now *strictly* //concerned with managing a connection pool. public abstract class Agent extends EventEmitter2 { private final static String TAG = "Agent"; public static final int defaultMaxSockets = Integer.MAX_VALUE; private int defaultPort; /** * @return the defaultPort */ public int defaultPort() { return defaultPort; } private String protocol; /** * @return the protocol */ public String protocol() { return protocol; } private ReqOptions options; private Map<String, List<ClientRequest>> requests; /** * @return the requests */ public Map<String, List<ClientRequest>> requests() { return requests; } private Map<String, List<AbstractSocket>> sockets; /** * @return the sockets */ public Map<String, List<AbstractSocket>> sockets() { return sockets; } private Map<String, List<AbstractSocket>> freeSockets; private int keepAliveMsecs; private boolean keepAlive; private int maxSockets; private int maxFreeSockets; private NodeContext context; public Agent(NodeContext ctx, ReqOptions options) throws Exception { final Agent self = this; self.context = ctx; self.defaultPort = 80; self.protocol = options.protocol!=null ? options.protocol : "http:"; self.options = options; // don't confuse net and make it think that we're connecting to a pipe ///self.options.path = null; self.requests = new Hashtable<String, List<ClientRequest>>(); self.sockets = new Hashtable<String, List<AbstractSocket>>(); self.freeSockets = new Hashtable<String, List<AbstractSocket>>(); self.keepAliveMsecs = self.options.keepAliveMsecs > 0 ? self.options.keepAliveMsecs : 1000; self.keepAlive = self.options.keepAlive;/// || false; self.maxSockets = self.options.maxSockets > 0 ? self.options.maxSockets : Agent.defaultMaxSockets; self.maxFreeSockets = self.options.maxFreeSockets > 0 ? self.options.maxFreeSockets : 256; self.on("free", new Listener() { @Override public void onEvent(Object raw) throws Exception { socket_options_b data = (socket_options_b)raw; AbstractSocket socket = data.socket; ReqOptions options = data.options; String name = self.name(options); debug(TAG, "agent.on(free) " + name); if (!socket.isDestroyed() && self.requests.containsKey(name) && self.requests.get(name).size() > 0 ) { ///self.requests[name].shift().onSocket(socket); self.requests.get(name).remove(0).onSocket(socket); if (self.requests.get(name).size() == 0) { // don't leak ///delete self.requests[name]; self.requests.remove(name); } } else { // If there are no pending requests, then put it in // the freeSockets pool, but only if we're allowed to do so. ClientRequest req = (ClientRequest) socket.get_httpMessage(); if (req!=null && req.shouldKeepAlive && !socket.isDestroyed() && self.options.keepAlive) { List<AbstractSocket> freeSockets = self.freeSockets.containsKey(name) ? self.freeSockets.get(name) : null; int freeLen = freeSockets != null ? freeSockets.size() : 0; int count = freeLen; if (self.sockets.containsKey(name)) count += self.sockets.get(name).size(); if (count >= self.maxSockets || freeLen >= self.maxFreeSockets) { self.removeSocket(socket, options); socket.destroy(null); } else { freeSockets = freeSockets!=null ? freeSockets : new LinkedList<AbstractSocket>(); self.freeSockets.put(name, freeSockets); socket.setKeepAlive(true, self.keepAliveMsecs); socket.unref(); socket.set_httpMessage(null); self.removeSocket(socket, options); freeSockets.add(socket); } } else { self.removeSocket(socket, options); socket.destroy(null); } } } }); } // POJO beans public static final class socket_options_b { public AbstractSocket socket; public ReqOptions options; public socket_options_b(AbstractSocket socket, ReqOptions options) { this.socket = socket; this.options = options; } } // Get the key for a given set of request options public String name(ReqOptions options) { String name = ""; if (!Util.zeroString(options.host)) name += options.host; else name += "localhost"; name += ":"; if (options.port > 0) name += options.port; name += ':'; if (!Util.zeroString(options.localAddress)) name += options.localAddress; name += ':'; if (options.localPort > 0) name += options.localPort; return name; } public void addRequest(ClientRequest req, ReqOptions options) throws Exception { // Legacy API: addRequest(req, host, port, path) /*if (typeof options === 'string') { options = { host: options, port: arguments[2], path: arguments[3] }; }*/ String name = this.name(options); /*if (!this.sockets[name]) { this.sockets[name] = []; }*/ if (!this.sockets.containsKey(name)) this.sockets.put(name, new LinkedList<AbstractSocket>()); ///var freeLen = this.freeSockets[name] ? this.freeSockets[name].length : 0; ///var sockLen = freeLen + this.sockets[name].length; int freeLen = this.freeSockets.containsKey(name) ? this.freeSockets.get(name).size() : 0; int sockLen = freeLen + this.sockets.get(name).size(); if (freeLen > 0) { // we have a free socket, so use that. AbstractSocket socket = this.freeSockets.get(name).remove(0); debug(TAG, "have free socket"); // don't leak if (this.freeSockets.get(name).isEmpty()) this.freeSockets.remove(name); socket.ref(); req.onSocket(socket); this.sockets.get(name).add(socket); } else if (sockLen < this.maxSockets) { debug(TAG, "call onSocket " + sockLen + " " + freeLen); // If we are under maxSockets create a new one. req.onSocket(this.createSocket(req, options)); } else { debug(TAG, "wait for socket"); // We are over limit so we'll add it to the queue. if (!this.requests.containsKey(name)) { this.requests.put(name, new LinkedList<ClientRequest>()); } this.requests.get(name).add(req); } } /** * @return the maxSockets */ public int maxSockets() { return maxSockets; } /** * @return the keepAlive */ public boolean keepAlive() { return keepAlive; } // Abstract interface protected abstract AbstractSocket createConnection( NodeContext ctx, String address, int port, String localAddress, int localPort, final AbstractSocket.ConnectListener cb) throws Exception; public AbstractSocket createSocket(ClientRequest req, final ReqOptions options) throws Exception { final Agent self = this; ///options = util._extend({}, options); ///options = util._extend(options, self.options); options.servername = options.host; if (req!=null) { List<String> hostHeader = req.getHeaders("host"); if (hostHeader!=null && hostHeader.size()>0) { String hh = hostHeader.get(0); options.servername = hh.replaceAll(":.*$", ""); } } String name = self.name(options); debug(TAG, "createConnection "+name+" "+options); options.encoding = null; final AbstractSocket s = createConnection( context, options.servername, options.port, options.localAddress, options.localPort, null); if (!self.sockets.containsKey(name)) { self.sockets.put(name, new LinkedList<AbstractSocket>()); } this.sockets.get(name).add(s); debug(TAG, "sockets "+ name+" "+ this.sockets.get(name).size()); final Listener onFree = new Listener(){ @Override public void onEvent(Object data) throws Exception { self.emit("free", new socket_options_b(s, options)); } }; s.on("free", onFree); final Listener onClose = new Listener(){ @Override public void onEvent(Object data) throws Exception { debug(TAG, "CLIENT socket onClose"); // This is the only place where sockets get removed from the Agent. // If you want to remove a socket from the pool, just close it. // All socket errors end in a close event anyway. self.removeSocket(s, options); } }; s.on("close", onClose); final Listener onRemove = new Listener(){ @Override public void onEvent(Object data) throws Exception { // We need this function for cases like http 'upgrade' // (defined by WebSockets) where we need to remove a socket from the // pool because it'll be locked up indefinitely debug(TAG, "CLIENT socket onRemove"); self.removeSocket(s, options); s.removeListener("close", onClose); s.removeListener("free", onFree); s.removeListener("agentRemove", this); } }; s.on("agentRemove", onRemove); return s; } public void removeSocket(AbstractSocket s, ReqOptions options) throws Exception { String name = this.name(options); debug(TAG, "removeSocket "+ name+ " destroyed:" + s.isDestroyed()); if (this.sockets.containsKey(name)) if (this.sockets.get(name).contains(s)) { this.sockets.get(name).remove(s); if (this.sockets.get(name).size() == 0) this.sockets.remove(name); } // If the socket was destroyed, remove it from the free buffers too. if (s.isDestroyed()) if (this.freeSockets.containsKey(name)) if (this.freeSockets.get(name).contains(s)) { this.freeSockets.get(name).remove(s); if (this.freeSockets.get(name).size() == 0) this.freeSockets.remove(name); } if (this.requests.containsKey(name) && this.requests.get(name).size() > 0) { debug(TAG, "removeSocket, have a request, make a socket"); ClientRequest req = this.requests.get(name).get(0); // If we have pending requests and a socket gets closed make a new one this.createSocket(req, options).emit("free"); } } public void destroy() throws Exception { /* var sets = [this.freeSockets, this.sockets]; sets.forEach(function(set) { Object.keys(set).forEach(function(name) { set[name].forEach(function(socket) { socket.destroy(); }); }); });*/ for (List<AbstractSocket> ss : this.freeSockets.values()) for (AbstractSocket s : ss) s.destroy(null); for (List<AbstractSocket> ss : this.sockets.values()) for (AbstractSocket s : ss) s.destroy(null); } ///public static Agent globalAgent = new Agent(); // HTTP agent public static class HttpAgent extends Agent { public HttpAgent(NodeContext ctx, ReqOptions options) throws Exception { super(ctx, options); // TODO Auto-generated constructor stub } private HttpAgent() throws Exception{super(null, null);} @Override protected AbstractSocket createConnection( NodeContext ctx, String address, int port, String localAddress, int localPort, ConnectListener cb) throws Exception { return TCP.createConnection(ctx, address, port, localAddress, localPort, cb); } } // HTTPP agent public static class HttppAgent extends Agent { public HttppAgent(NodeContext ctx, ReqOptions options) throws Exception { super(ctx, options); // TODO Auto-generated constructor stub } private HttppAgent() throws Exception{super(null, null);} @Override protected AbstractSocket createConnection( NodeContext ctx, String address, int port, String localAddress, int localPort, ConnectListener cb) throws Exception { return UDT.createConnection(ctx, address, port, localAddress, localPort, cb); } } }