// Copyright (c) 2014 Tom Zhou<iwebpp@gmail.com> package com.iwebpp.node.net; import java.io.UnsupportedEncodingException; import java.nio.ByteBuffer; import com.iwebpp.libuvpp.Address; import com.iwebpp.libuvpp.cb.StreamCloseCallback; import com.iwebpp.libuvpp.cb.StreamConnectCallback; import com.iwebpp.libuvpp.cb.StreamReadCallback; import com.iwebpp.libuvpp.cb.StreamShutdownCallback; import com.iwebpp.libuvpp.cb.StreamWriteCallback; import com.iwebpp.libuvpp.handles.LoopHandle; import com.iwebpp.libuvpp.handles.StreamHandle; import com.iwebpp.node.Dns; import com.iwebpp.node.EventEmitter; import com.iwebpp.node.NodeContext; import com.iwebpp.node.Timers; import com.iwebpp.node.Util; import com.iwebpp.node.http.IncomingParser; import com.iwebpp.node.stream.Duplex; import com.iwebpp.node.stream.Readable2; import com.iwebpp.node.stream.Writable2; import com.iwebpp.node.stream.Writable2.WriteReq; public abstract class AbstractSocket extends Duplex { private final static String TAG = "AbstractSocket"; private boolean _connecting; private boolean _hadError; protected StreamHandle _handle; private Object _pendingData; private String _pendingEncoding; private boolean allowHalfOpen; private boolean destroyed; /** * @return the destroyed */ public boolean isDestroyed() { return destroyed; } private int bytesRead; private int _bytesDispatched; private AbstractServer abstractServer; private Address _sockname; private Address _peername; private NodeContext context; private boolean _paused = false; private EventEmitter _httpMessage; private IncomingParser parser; public static class Options { public Options( StreamHandle handle, boolean readable, boolean writable, boolean allowHalfOpen) { super(); this.handle = handle; this.readable = readable; this.writable = writable; this.allowHalfOpen = allowHalfOpen; } public StreamHandle handle; public long fd = -1; public boolean readable; public boolean writable; public boolean allowHalfOpen; @SuppressWarnings("unused") private Options(){} }; public AbstractSocket(NodeContext context, Options options) throws Exception { // TBD... super(context, new Duplex.Options(new Readable2.Options(-1, null, false, "utf8", options.readable), new Writable2.Options(-1, false, "utf8", false, options.writable), options.allowHalfOpen) ); final AbstractSocket self = this; // node context this.context = context; ///if (!(this instanceof AbstractSocket)) return new AbstractSocket(options); this._connecting = false; this.set_hadError(false); this._handle = null; /*if (Util.isNumber(options)) options = { fd: options }; // Legacy interface. else if (Util.isUndefined(options)) options = {}; */ ///stream.Duplex.call(this, options); if (options.handle != null) { this._handle = options.handle; // private } /*TBD...else if (!Util.isUndefined(options.fd)) { this._handle = createHandle(options.fd); this._handle.open(options.fd); if ((options.fd == 1 || options.fd == 2) && (this._handle instanceof Pipe) && process.platform === 'win32') { // Make stdout and stderr blocking on Windows var err = this._handle.setBlocking(true); if (err) throw errnoException(err, 'setBlocking'); } this.readable = options.readable; this.writable = options.writable; } */else { // these will be set once there is a connection this.readable(false); this.writable(false); } // shut down the socket when we're finished with it. // the user has called .end(), and all the bytes have been // sent out to the other side. // If allowHalfOpen is false, or if the readable side has // ended already, then destroy. // If allowHalfOpen is true, then we need to do a shutdown, // so that only the writable side will be cleaned up. final Listener onSocketFinish = new Listener(){ @Override public void onEvent(Object data) throws Exception { // If still connecting - defer handling 'finish' until 'connect' will happen if (self._connecting) { debug(TAG, "osF: not yet connected"); self.once("connect", this); return; } debug(TAG, "onSocketFinish"); if (!self.readable() || self.get_readableState().isEnded()) { debug(TAG, "oSF: ended, destroy "+self.get_readableState()); self.destroy(null); return; } debug(TAG, "oSF: not ended, call shutdown()"); // otherwise, just shutdown, or destroy() if not possible if (self._handle==null /*|| !self._handle.shutdown*/) { self.destroy(null); return; } /*var req = { oncomplete: afterShutdown }; var err = this._handle.shutdown(req); if (err) return this._destroy(errnoException(err, 'shutdown')); */ self._handle.setShutdownCallback(new StreamShutdownCallback(){ @Override public void onShutdown(int status, Exception error) throws Exception { ///var self = handle.owner; debug(TAG, "afterShutdown destroyed="+self.destroyed+","+ self.get_readableState()); // callback may come after call to destroy. if (self.destroyed) return; if (self.get_readableState().isEnded()) { debug(TAG, "readableState ended, destroying"); self.destroy(null); } else { ///self.once("_socketEnd", self.destroy); self.once("_socketEnd", new Listener(){ @Override public void onEvent(Object data) throws Exception { self.destroy(null); } }); } } }); int err = self._handle.closeWrite(); if (err!=0) { self._destroy("shutdown", null); return; } } }; this.on("finish", onSocketFinish); // the EOF has been received, and no more bytes are coming. // if the writable side has ended already, then clean everything // up. Listener onSocketEnd = new Listener(){ @Override public void onEvent(Object data) throws Exception { // XXX Should not have to do as much crap in this function. // ended should already be true, since this is called *after* // the EOF errno and onread has eof'ed debug(TAG, "onSocketEnd "+self.get_readableState()); self.get_readableState().setEnded(true); if (self.get_readableState().isEndEmitted()) { self.readable(false); maybeDestroy(self); } else { self.once("end", new Listener(){ public void onEvent(final Object data) throws Exception { self.readable(false); maybeDestroy(self); } }); self.read(0); } if (!self.allowHalfOpen) { // TBD... ///self.write = writeAfterFIN; self.destroySoon(); } } }; this.on("_socketEnd", onSocketEnd); initSocketHandle(this); this._pendingData = null; this._pendingEncoding = ""; // handle strings directly this._writableState.setDecodeStrings(false); // default to *not* allowing half open sockets this.allowHalfOpen = options.allowHalfOpen; // if we have a handle, then start the flow of data into the // buffer. if not, then this will happen when we connect if (this._handle!=null && options.readable) this.read(0); } public void destroySoon() throws Exception { final AbstractSocket self = this; if (this.writable()) this.end(null, null, null); if (this._writableState.isFinished()) this.destroy(null); else this.once("finish", new Listener(){ @Override public void onEvent(Object data) throws Exception { self.destroy(null); } }); } // called when creating new AbstractSocket, or when re-using a closed AbstractSocket private static void initSocketHandle(final AbstractSocket self) { self.destroyed = false; self.bytesRead = 0; self._bytesDispatched = 0; // Handle creation may be deferred to bind() or connect() time. if (self._handle != null) { ///self._handle.owner = self; // This function is called whenever the handle gets a // buffer, or when there's an error reading. StreamReadCallback onread = new StreamReadCallback(){ @Override public void onRead(ByteBuffer buffer) throws Exception { int nread = (buffer == null) ? 0 : buffer.capacity(); ///var handle = this; ///AbstractSocket self = handle.owner; StreamHandle handle = self._handle; ///assert(handle === self._handle, 'handle != self._handle'); Timers._unrefActive(self); ///debug('onread', nread); debug(TAG, "onread "+nread); if (nread > 0) { ///debug('got data'); debug(TAG, "got data"); // read success. // In theory (and in practice) calling readStop right now // will prevent this from being called again until _read() gets // called again. // if it's not enough data, we'll just call handle.readStart() // again right away. self.bytesRead += nread; // Optimization: emit the original buffer with end points boolean ret = self.push(buffer, null); if (/*handle.reading &&*/ !ret) { ///handle.reading = false; debug(TAG, "readStop"); handle.readStop(); ///var err = handle.readStop(); ///if (err) /// self._destroy(errnoException(err, "read")); } return; } // if we didn't get any bytes, that doesn't necessarily mean EOF. // wait for the next one. if (nread == 0) { debug(TAG, "not any data, keep waiting"); return; } // Error, possibly EOF. ///if (nread != uv.UV_EOF) { /// return self._destroy(errnoException(nread, "read")); ///} debug(TAG, "EOF"); if (self.get_readableState().getLength() == 0) { self.readable(false); maybeDestroy(self); } // push a null to signal the end of data. self.push(null, null); // internal end event so that we know that the actual socket // is no longer readable, and we can start the shutdown // procedure. No need to wait for all the data to be consumed. self.emit("_socketEnd"); } }; ///self._handle.onread = onread; self._handle.setReadCallback(onread); } } // Call whenever we set writable=false or readable=false protected static void maybeDestroy(AbstractSocket abstractSocket) throws Exception { if ( !abstractSocket.readable() && !abstractSocket.writable() && !abstractSocket.destroyed && !abstractSocket._connecting && abstractSocket._writableState.getLength()==0) { abstractSocket.destroy(null); } } public void destroy(String exception) throws Exception { debug(TAG, "destroy "+exception); this._destroy(exception, null); } private void _destroy(final String exception, final Listener cb) throws Exception { debug(TAG, "destroy"); final AbstractSocket self = this; Listener fireErrorCallbacks = new Listener() { @Override public void onEvent(Object data) throws Exception { if (cb != null) cb.onEvent(exception); if (exception!=null && !self._writableState.isErrorEmitted()) { // TBD... ///process.nextTick(function() { context.nextTick(new NodeContext.nextTickListener() { public void onNextTick() throws Exception { self.emit("error", exception); } }); self._writableState.setErrorEmitted(true); } } }; if (this.destroyed) { debug(TAG, "already destroyed, fire error callbacks"); fireErrorCallbacks.onEvent(null); return; } self._connecting = false; this.readable(false); this.writable(false); Timers.unenroll(this); debug(TAG, "close"); if (this._handle != null) { ///if (this !== process.stderr) ///debug('close handle'); debug(TAG, "close handle"); final boolean isException = exception != null ? true : false; /*this._handle.close(function() { debug('emit close'); self.emit('close', isException); });*/ this._handle.close(new StreamCloseCallback() { @Override public void onClose() throws Exception { debug(TAG, "emit close"); self.emit("close", isException); } }); ///this._handle.onread = noop; this._handle.setReadCallback(new StreamReadCallback(){ @Override public void onRead(ByteBuffer data) throws Exception { } }); this._handle = null; } // we set destroyed to true before firing error callbacks in order // to make it re-entrance safe in case AbstractSocket.prototype.destroy() // is called within callbacks this.destroyed = true; fireErrorCallbacks.onEvent(null); if (this.abstractServer != null) { // TBD... ///COUNTER_NET_SERVER_CONNECTION_CLOSE(this); debug(TAG, "has abstractServer"); this.abstractServer .set_connections(this.abstractServer.get_connections() - 1); ///if (this.server._emitCloseIfDrained) { this.abstractServer._emitCloseIfDrained(); ///} } } private AbstractSocket() {super(null, null);} // TBD... /*AbstractSocket.prototype.listen = function() { debug('socket.listen'); var self = this; self.on('connection', arguments[0]); listen(self, null, null, null); }; AbstractSocket.prototype.setTimeout = function(msecs, callback) { if (msecs > 0 && isFinite(msecs)) { timers.enroll(this, msecs); timers._unrefActive(this); if (callback) { this.once('timeout', callback); } } else if (msecs === 0) { timers.unenroll(this); if (callback) { this.removeListener('timeout', callback); } } }; AbstractSocket.prototype._onTimeout = function() { debug('_onTimeout'); this.emit('timeout'); }; */ public void setTimeout(int msecs, Listener callback) throws Exception { if (msecs > 0 /*&& isFinite(msecs)*/) { Timers.enroll(this, msecs); Timers._unrefActive(this); if (callback != null) { this.once("timeout", callback); } } else if (msecs == 0) { Timers.unenroll(this); if (callback != null) { this.removeListener("timeout", callback); } } } public String remoteAddress() { return this._getpeername().getIp(); } public int remotePort() { return this._getpeername().getPort(); } public String remoteFamily() { return this._getpeername().getFamily(); } private Address _getpeername() { if (null == this._handle /*|| !this._handle.getpeername*/) { return null; } if (null == this._peername) { Address out = this._getPeerName(); if (null == out) return null; // FIXME(bnoordhuis) Throw? this._peername = out; } return this._peername; } public Address address() { return this._getsockname(); } public String localAddress() { return this._getsockname().getIp(); } public int localPort() { return this._getsockname().getPort(); } public String family() { return this._getsockname().getFamily(); } private Address _getsockname() { if (null == this._handle /*|| !this._handle.getsockname*/) { return null; } if (null == this._sockname) { Address out = this._getSocketName(); if (null == out) return null; // FIXME(bnoordhuis) Throw? this._sockname = out; } return this._sockname; } public int bytesRead() { return this.bytesRead; } public int bytesWritten() throws UnsupportedEncodingException { int bytes = this._bytesDispatched; com.iwebpp.node.stream.Writable2.State state = this._writableState; Object data = this._pendingData; String encoding = this._pendingEncoding; /*state.buffer.forEach(function(el) { if (util.isBuffer(el.chunk)) bytes += el.chunk.length; else bytes += Buffer.byteLength(el.chunk, el.encoding); });*/ for (WriteReq el : state.getBuffer()) { if (Util.isBuffer(el.getChunk())) bytes += Util.chunkLength(el.getChunk()); else bytes += Util.stringByteLength((String) el.getChunk(), el.getEncoding()); } if (data != null) { if (Util.isBuffer(data)) bytes += Util.chunkLength(data); else bytes += Util.stringByteLength((String) data, encoding); } return bytes; } @Override public Object read(int n) throws Exception { if (n == 0) return super.read(n); return super.read(n); } @Override public boolean write(Object chunk, String encoding, WriteCB cb) throws Exception { // check on writeAfterFIN if (!this.allowHalfOpen && this.get_readableState().isEnded()) { return writeAfterFIN(chunk, encoding, cb); } else { if (!Util.isString(chunk) && !Util.isBuffer(chunk)) throw new Exception("invalid data"); return super.write(chunk, encoding, cb); } } // Provide a better error message when we call end() as a result // of the other side sending a FIN. The standard 'write after end' // is overly vague, and makes it seem like the user's code is to blame. private boolean writeAfterFIN(Object chunk, String encoding, final WriteCB cb) throws Exception { /*if (util.isFunction(encoding)) { cb = encoding; encoding = null; }*/ ///var er = new Error('This socket has been ended by the other party'); ///er.code = 'EPIPE'; final String er = "This socket has been ended by the other party"; AbstractSocket self = this; // TODO: defer error events consistently everywhere, not just the cb self.emit("error", er); ///if (util.isFunction(cb)) { if (cb != null) { ///process.nextTick(function() { context.nextTick(new NodeContext.nextTickListener() { @Override public void onNextTick() throws Exception { cb.writeDone(er); } }); } return false; } public String readyState() { if (this._connecting) { return "opening"; } else if (this.readable() && this.writable()) { return "open"; } else if (this.readable() && !this.writable()) { return "readOnly"; } else if (!this.readable() && this.writable()) { return "writeOnly"; } else { return "closed"; } } public int bufferSize() { if (this._handle != null) { return (int) (this._handle.writeQueueSize() + this._writableState.getLength()); } return 0; } // Just call handle.readStart until we have enough in the buffer @Override public void _read(final int n) throws Exception { final AbstractSocket self = this; debug(TAG, "_read"); if (this._connecting || null==this._handle) { debug(TAG, "_read wait for connection"); ///this.once("connect", this._read.bind(this, n)); this.once("connect", new Listener(){ @Override public void onEvent(Object data) throws Exception { self._read(n); } }); } else if (!this._handle.reading) { // not already reading, start the flow debug(TAG, "AbstractSocket._read readStart"); this._handle.reading = true; ///var err = this._handle.readStart(); ///if (err) /// this._destroy(errnoException(err, "read")); this._handle.readStart(); } } @Override public boolean end(Object data, String encoding, WriteCB cb) throws Exception { ///stream.Duplex.prototype.end.call(this, data, encoding); super.end(data, encoding, null); this.writable(false); ///DTRACE_NET_STREAM_END(this); // just in case we're waiting for an EOF. if (this.readable() && !this.get_readableState().isEndEmitted()) this.read(0); else maybeDestroy(this); return false; } /* AbstractSocket.prototype._writev = function(chunks, cb) { this._writeGeneric(true, chunks, '', cb); };*/ @Override public void _write(Object chunk, String encoding, WriteCB cb) throws Exception { this._writeGeneric(false, chunk, encoding, cb); } private void _writeGeneric(final boolean writev, final Object data, final String encoding, final WriteCB cb) throws Exception { final AbstractSocket self = this; // If we are still connecting, then buffer this for later. // The Writable logic will buffer up any more writes while // waiting for this one to be done. if (this._connecting) { this._pendingData = data; this._pendingEncoding = encoding; /* this.once("connect", function() { this._writeGeneric(writev, data, encoding, cb); });*/ this.once("connect", new Listener(){ @Override public void onEvent(Object dummy) throws Exception { self._writeGeneric(writev, data, encoding, cb); } }); return; } this._pendingData = null; this._pendingEncoding = ""; Timers._unrefActive(this); if (null == this._handle) { ///this._destroy("This socket is closed.", cb); this._destroy("This socket is closed.", new Listener(){ @Override public void onEvent(Object data) throws Exception { cb.writeDone("This socket is closed."); } }); return; } /* var req = { oncomplete: afterWrite, async: false }; var err; if (writev) { var chunks = new Array(data.length << 1); for (var i = 0; i < data.length; i++) { var entry = data[i]; var chunk = entry.chunk; var enc = entry.encoding; chunks[i * 2] = chunk; chunks[i * 2 + 1] = enc; } err = this._handle.writev(req, chunks); // Retain chunks if (err == 0) req._chunks = chunks; } else { String enc; if (Util.isBuffer(data)) { req.buffer = data; // Keep reference alive. enc = "buffer"; } else { enc = encoding; } err = createWriteReq(req, this._handle, data, enc); } if (err) return this._destroy(errnoException(err, "write", req.error), cb); this._bytesDispatched += req.bytes; // If it was entirely flushed, we can write some more right now. // However, if more is left in the queue, then wait until that clears. if (req.async && this._handle.writeQueueSize != 0) req.cb = cb; else cb(); */ // afterWrite this._handle.setWriteCallback(new StreamWriteCallback(){ @Override public void onWrite(int status, Exception err) throws Exception { ///var self = handle.owner; ///if (self !== process.stderr && self !== process.stdout) debug(TAG, "afterWrite "+status); // callback may come after call to destroy. if (self.destroyed) { debug(TAG, "afterWrite destroyed"); return; } if (status < 0) { ///var ex = errnoException(status, 'write', err); String ex = "" + status + " write " + err; debug(TAG, "write failure:" + ex); ///self._destroy(ex, req.cb); self._destroy(ex, null); return; } Timers._unrefActive(self); ///if (self !== process.stderr && self !== process.stdout) debug(TAG, "afterWrite call cb"); // TBD... ///if (req.cb) /// req.cb.call(self); } }); int err = 0; if (Util.isBuffer(data)) { err = this._handle.write((ByteBuffer)data); } else if (Util.isString(data)) { err = this._handle.write((String)data, encoding); } else { this._destroy("write invalid data", new Listener(){ @Override public void onEvent(Object data) throws Exception { cb.writeDone("write invalid data"); } }); return; } if (err != 0) { this._destroy("write invalid data", new Listener(){ @Override public void onEvent(Object data) throws Exception { cb.writeDone("write invalid data"); } }); return; } // TBD... ///this._bytesDispatched += req.bytes; // If it was entirely flushed, we can write some more right now. // However, if more is left in the queue, then wait until that clears. ///if (req.async && this._handle.writeQueueSize() != 0) /// req.cb = cb; ///else cb.writeDone(null); return; } public void connect(int port, final ConnectListener cb) throws Exception { // check handle ////////////////////// if (this.destroyed) { this.get_readableState().setReading(false); this.get_readableState().setEnded(false); this.get_readableState().setEndEmitted(false); this._writableState.setEnded(false); this._writableState.setEnding(false); this._writableState.setFinished(false); this._writableState.setErrorEmitted(false); this.destroyed = false; this._handle = null; } AbstractSocket self = this; ///var pipe = !!options.path; ///debug('pipe', pipe, options.path); if (null == this._handle) { ///this._handle = pipe ? createPipe() : createHandle(context.getLoop()); this._handle = _createHandle(context.getLoop()); initSocketHandle(this); } ///if (util.isFunction(cb)) { if (cb != null) { self.once("connect", new Listener(){ @Override public void onEvent(Object data) throws Exception { cb.onConnect(); } }); } Timers._unrefActive(this); self._connecting = true; // TBD... change true to false ///self.writable(true); self.writable(false); /////////////////////////////////////////////// connect(4, null, port, null, -1); } public void connect(String address ,int port, final ConnectListener cb) throws Exception { // check handle ////////////////////// if (this.destroyed) { this.get_readableState().setReading(false); this.get_readableState().setEnded(false); this.get_readableState().setEndEmitted(false); this._writableState.setEnded(false); this._writableState.setEnding(false); this._writableState.setFinished(false); this._writableState.setErrorEmitted(false); this.destroyed = false; this._handle = null; } AbstractSocket self = this; ///var pipe = !!options.path; ///debug('pipe', pipe, options.path); if (null == this._handle) { ///this._handle = pipe ? createPipe() : createHandle(context.getLoop()); this._handle = _createHandle(context.getLoop()); initSocketHandle(this); } ///if (util.isFunction(cb)) { if (cb != null) { self.once("connect", new Listener(){ @Override public void onEvent(Object data) throws Exception { cb.onConnect(); } }); } Timers._unrefActive(this); self._connecting = true; // TBD... change true to false ///self.writable(true); self.writable(false); /////////////////////////////////////////////// // DNS lookup // TBD... async String ip = null; if (Util.isIP(address)) { ip = address; } else { ip = Dns.lookup(address); if (ip == null) throw new Exception("Invalid address: "+address); } connect(Util.ipFamily(ip), ip, port, null, -1); } public void connect( String address ,int port, int localPort, final ConnectListener cb) throws Exception { // check handle ////////////////////// if (this.destroyed) { this.get_readableState().setReading(false); this.get_readableState().setEnded(false); this.get_readableState().setEndEmitted(false); this._writableState.setEnded(false); this._writableState.setEnding(false); this._writableState.setFinished(false); this._writableState.setErrorEmitted(false); this.destroyed = false; this._handle = null; } AbstractSocket self = this; ///var pipe = !!options.path; ///debug('pipe', pipe, options.path); if (null == this._handle) { ///this._handle = pipe ? createPipe() : createHandle(context.getLoop()); this._handle = _createHandle(context.getLoop()); initSocketHandle(this); } ///if (util.isFunction(cb)) { if (cb != null) { self.once("connect", new Listener(){ @Override public void onEvent(Object data) throws Exception { cb.onConnect(); } }); } Timers._unrefActive(this); self._connecting = true; // TBD... change true to false ///self.writable(true); self.writable(false); /////////////////////////////////////////////// // DNS lookup // TBD... async String ip = null; if (Util.isIP(address)) { ip = address; } else { ip = Dns.lookup(address); if (ip == null) throw new Exception("Invalid address: "+address); } connect(Util.ipFamily(ip), ip, port, null, localPort); } public void connect( String address ,int port, String localAddress, final ConnectListener cb) throws Exception { // check handle ////////////////////// if (this.destroyed) { this.get_readableState().setReading(false); this.get_readableState().setEnded(false); this.get_readableState().setEndEmitted(false); this._writableState.setEnded(false); this._writableState.setEnding(false); this._writableState.setFinished(false); this._writableState.setErrorEmitted(false); this.destroyed = false; this._handle = null; } AbstractSocket self = this; ///var pipe = !!options.path; ///debug('pipe', pipe, options.path); if (null == this._handle) { ///this._handle = pipe ? createPipe() : createHandle(context.getLoop()); this._handle = _createHandle(context.getLoop()); initSocketHandle(this); } ///if (util.isFunction(cb)) { if (cb != null) { self.once("connect", new Listener(){ @Override public void onEvent(Object data) throws Exception { cb.onConnect(); } }); } Timers._unrefActive(this); self._connecting = true; // TBD... change true to false ///self.writable(true); self.writable(false); /////////////////////////////////////////////// // DNS lookup // TBD... async String ip = null; if (Util.isIP(address)) { ip = address; } else { ip = Dns.lookup(address); if (ip == null) throw new Exception("Invalid address: "+address); } String localip = null; if (localAddress != null) { if (Util.isIP(localAddress)) { localip = localAddress; } else { localip = Dns.lookup(localAddress); if (localip == null) throw new Exception("Invalid localAddress: "+localAddress); } } connect(Util.ipFamily(ip), ip, port, localip, -1); } public void connect( String address ,int port, String localAddress, int localPort, final ConnectListener cb) throws Exception { // check handle ////////////////////// if (this.destroyed) { this.get_readableState().setReading(false); this.get_readableState().setEnded(false); this.get_readableState().setEndEmitted(false); this._writableState.setEnded(false); this._writableState.setEnding(false); this._writableState.setFinished(false); this._writableState.setErrorEmitted(false); this.destroyed = false; this._handle = null; } AbstractSocket self = this; ///var pipe = !!options.path; ///debug('pipe', pipe, options.path); if (null == this._handle) { ///this._handle = pipe ? createPipe() : createHandle(context.getLoop()); this._handle = _createHandle(context.getLoop()); initSocketHandle(this); } ///if (util.isFunction(cb)) { if (cb != null) { self.once("connect", new Listener(){ @Override public void onEvent(Object data) throws Exception { cb.onConnect(); } }); } Timers._unrefActive(this); self._connecting = true; // TBD... change true to false ///self.writable(true); self.writable(false); /////////////////////////////////////////////// // DNS lookup // TBD... async String ip = null; if (Util.isIP(address)) { ip = address; } else { ip = Dns.lookup(address); if (ip == null) throw new Exception("Invalid address: "+address); } String localip = null; if (localAddress != null) { if (Util.isIP(localAddress)) { localip = localAddress; } else { localip = Dns.lookup(localAddress); if (localip == null) throw new Exception("Invalid localAddress: "+localAddress); } } connect(Util.ipFamily(ip), ip, port, localip, localPort); } public void connect(int addressType, String address ,int port, final ConnectListener cb) throws Exception { // check handle ////////////////////// if (this.destroyed) { this.get_readableState().setReading(false); this.get_readableState().setEnded(false); this.get_readableState().setEndEmitted(false); this._writableState.setEnded(false); this._writableState.setEnding(false); this._writableState.setFinished(false); this._writableState.setErrorEmitted(false); this.destroyed = false; this._handle = null; } AbstractSocket self = this; ///var pipe = !!options.path; ///debug('pipe', pipe, options.path); if (null == this._handle) { ///this._handle = pipe ? createPipe() : createHandle(context.getLoop()); this._handle = _createHandle(context.getLoop()); initSocketHandle(this); } ///if (util.isFunction(cb)) { if (cb != null) { self.once("connect", new Listener(){ @Override public void onEvent(Object data) throws Exception { cb.onConnect(); } }); } Timers._unrefActive(this); self._connecting = true; // TBD... change true to false ///self.writable(true); self.writable(false); /////////////////////////////////////////////// // DNS lookup // TBD... async String ip = null; if (Util.isIP(address)) { ip = address; } else { ip = Dns.lookup(address); if (ip == null) throw new Exception("Invalid address: "+address); } connect(Util.ipFamily(ip), ip, port, null, -1); } private void connect(int addressType, String ip, int port, String localIP, int localPort) throws Exception { final AbstractSocket self = this; // TODO return promise from AbstractSocket.prototype.connect which // wraps _connectReq. /*assert.ok(self._connecting); String err; if (localAddress!=null || localPort>0) { if (!Util.zeroString(localAddress) && !exports.isIP(localAddress)) err = new TypeError( 'localAddress should be a valid IP: ' + localAddress); if (localPort && !util.isNumber(localPort)) err = new TypeError('localPort should be a number: ' + localPort); var bind; switch (addressType) { case 4: if (!localAddress) localAddress = "0.0.0.0"; bind = self._handle.bind; break; case 6: if (!localAddress) localAddress = '::'; bind = self._handle.bind6; break; default: err = new TypeError('Invalid addressType: ' + addressType); break; } if (err) { self._destroy(err); return; } debug('binding to localAddress: %s and localPort: %d', localAddress, localPort); bind = bind.bind(self._handle); err = bind(localAddress, localPort); if (err) { self._destroy(errnoException(err, 'bind')); return; } } */ // Always bind first // TBD... isIP if (Util.zeroString(localIP)) { localIP = (addressType == 6) ? "::" : "0.0.0.0"; } if (localPort < 0 || localPort >= 65536) { localPort = 0; } debug(TAG, "binding to localAddress: " + localIP + " and localPort: " + localPort); int err = 0; if (addressType == 6) { err = this._bind6(localIP, localPort); } else { err = this._bind(localIP, localPort); } if (err != 0) { error(TAG, "err bind"); ///self._destroy(errnoException(err, 'bind')); this._destroy("err bind", null); return; } // Try to connect ... // afterConnect StreamConnectCallback afterConnect = new StreamConnectCallback() { @Override public void onConnect(int status, Exception error) throws Exception { ///var self = handle.owner; // callback may come after call to destroy if (self.destroyed) { return; } ///assert(handle === self._handle, 'handle != self._handle'); ///assert.ok(self._connecting); self._connecting = false; if (status >= 0) { self.readable(self._handle.isReadable()); self.writable(self._handle.isWritable()); Timers._unrefActive(self); self.emit("connect"); // start the first read, or get an immediate EOF. // this doesn't actually consume any bytes, because len=0. if (readable()) self.read(0); } else { error(TAG, "err connect status: "+status); self._connecting = false; ///self._destroy(errnoException(status, 'connect')); self._destroy("err connect status: "+status, null); } } }; this._handle.setConnectCallback(afterConnect); // TBD... IP validation if (Util.zeroString(ip)) { ip = (addressType == 6) ? "::1" : "127.0.0.1"; } if (port <= 0 || port > 65535) throw new Exception("Port should be > 0 and < 65536"); if (addressType == 6) { err = this._connect6(ip, port); } else { err = this._connect(ip, port); } if (err != 0) { error(TAG, "err connect"); ///self._destroy(errnoException(err, 'connect')); this._destroy("err connect", null); } debug(TAG, "connect to address: " + ip + " and port: " + port); /* var req = { oncomplete: afterConnect }; if (addressType === 6 || addressType === 4) { port = port | 0; if (port <= 0 || port > 65535) throw new RangeError('Port should be > 0 and < 65536'); if (addressType === 6) { err = self._handle.connect6(req, address, port); } else if (addressType === 4) { err = self._handle.connect(req, address, port); } } else { err = self._handle.connect(req, address, afterConnect); } if (err) { self._destroy(errnoException(err, 'connect')); } */ } public void ref() { this._handle.ref(); } public void unref() { this._handle.unref(); } /** * @return the _hadError */ public boolean is_hadError() { return _hadError; } /** * @param _hadError the _hadError to set */ public void set_hadError(boolean _hadError) { this._hadError = _hadError; } /** * @return the _httpMessage */ public EventEmitter get_httpMessage() { return _httpMessage; } /** * @param _httpMessage the _httpMessage to set */ public void set_httpMessage(EventEmitter _httpMessage) { this._httpMessage = _httpMessage; } /** * @return the parser */ public IncomingParser getParser() { return parser; } /** * @param parser the parser to set */ public void setParser(IncomingParser parser) { this.parser = parser; } /** * @return the _paused */ public boolean is_paused() { return _paused; } /** * @param _paused the _paused to set */ public void set_paused(boolean _paused) { this._paused = _paused; } // Event listeners public void onceConnect(final ConnectListener cb) throws Exception { this.once("connect", new Listener(){ @Override public void onEvent(Object data) throws Exception { cb.onConnect(); } }); } public interface ConnectListener { public void onConnect() throws Exception; } public void onData(final DataListener cb) throws Exception { this.on("data", new Listener(){ @Override public void onEvent(Object data) throws Exception { cb.onData(data); } }); } public interface DataListener { public void onData(Object data) throws Exception; } public void onceEnd(final EndListener cb) throws Exception { this.once("end", new Listener(){ @Override public void onEvent(Object data) throws Exception { cb.onEnd(); } }); } public interface EndListener { public void onEnd() throws Exception; } /* TBD... public void onTimeout(final TimeoutListener cb) throws Exception { this.on("timeout", new Listener(){ @Override public void onEvent(Object data) throws Exception { cb.onTimeout(); } }); } public interface TimeoutListener { public void onTimeout() throws Exception; }*/ public void onDrain(final DrainListener cb) throws Exception { this.on("drain", new Listener(){ @Override public void onEvent(Object data) throws Exception { cb.onDrain(); } }); } public interface DrainListener { public void onDrain() throws Exception; } public void onError(final ErrorListener cb) throws Exception { this.on("error", new Listener(){ @Override public void onEvent(Object data) throws Exception { cb.onError(data!=null? data.toString() : ""); } }); } public interface ErrorListener { public void onError(String error) throws Exception; } public void onceClose(final CloseListener cb) throws Exception { this.once("close", new Listener(){ @Override public void onEvent(Object data) throws Exception { Boolean had_error = (Boolean)data; cb.onClose(had_error); } }); } public interface CloseListener { public void onClose(boolean had_error) throws Exception; } // Abstract socket methods protected abstract StreamHandle _createHandle(final LoopHandle loop); protected abstract int _bind(final String ip, final int port); protected abstract int _bind6(final String ip, final int port); protected abstract int _connect(final String ip, final int port); protected abstract int _connect6(final String ip, final int port); protected abstract Address _getSocketName(); protected abstract Address _getPeerName(); public abstract int setNoDelay(final boolean enable); public abstract int setKeepAlive(final boolean enable, final int delay); }