docs: function Client(rwStream, options)
  {
  	events.EventEmitter.call(this);
  	
  	var tidCounter = 0;
  	var requestMap = {};
  	var isClosed = true;
  	var address = '';
  	var host = '';
  	
  	var idlePingTimeout 	= getOption(options, 'idlePingTimeout',		0);
  	var idleCloseTimeout	= getOption(options, 'idleCloseTimeout',	0);
  	var autoRegister		= getOption(options, 'autoRegister', 		false);
  	var logConnection		= getOption(options, 'logConnection', 		true);
  	var autoResponseUnhandledCommand = getOption(options, 'autoResponseUnhandledCommand', true);
  	
  	this.isServer = options.isServer;
  	
  	function getAddress()
  	{
  		return util.format('%s:%s', rwStream.remoteAddress.replace(/^::ffff:/i, ''), rwStream.remotePort);		
  	}
  	
  	function log(event)
  	{
  		if ( logConnection )
  		{
  			console.log(util.format('%s %s(%s)', event, host, address));
  		}
  	}
  	
  	this.closeStream = function() {
  
  		//console.log('isClosed ' + isClosed);
  		
  		if ( !isClosed )
  		{
  			isClosed = true;
  			
  			log('CLOSE');
  			
  			if ( rwStream instanceof stream.Writable )
  			{
  				rwStream.end();
  			}
  		
  			for ( var tid in requestMap )
  			{
  				if ( requestMap.hasOwnProperty(tid) )
  				{
  					clearTimeout(requestMap[tid].timer);
  					requestMap[tid].callback({code: '500', tid: tid, args: ['socket_closed'], body: null, isValid: true});
  					delete requestMap[tid];
  				}
  			}
  			
  			this.emit('close');
  		}
  	};
  	
  	if ( rwStream instanceof stream.Writable )
  	{
  		this.sendMessage = function(code, tid, args, body, callback) {
  			var bodyBuf = Buffer.isBuffer(body) ? body : 
  				typeof(body) === 'string' ? Buffer.from(body) : 
  					null;
  			tid = tid === -1 ? ++tidCounter : tid;
  			tid = '' + tid;
  			var bodyLen = bodyBuf ? bodyBuf.length : 0;
  			var header = util.format('%s %s %d%s\r\n', code, tid, bodyLen, encodeArgs(args));
  			var headerLen = Buffer.byteLength(header);
  			var msg = {code: code, tid: tid, args: args, body: body};
  			if ( options.debugHeader )
  			{
  				console.log("send msg: %s", msg2str(msg));
  			}
  			rwStream.write(header, callback);
  			if ( bodyLen > 0 )
  			{
  				rwStream.write(bodyBuf);
  			}
  		};
  
  		this.sendRequest = function(code, args, body, timeout, callback) {
  			var tid = ++tidCounter;
  			this.sendMessage(code, tid, args, body);
  			requestMap[tid] = {
  				callback: callback,
  				timer: setTimeout(() => this.emit('message_timeout', tid), timeout)
  			};
  		};
  	}
  	
  	if ( rwStream instanceof stream.Readable )
  	{
  		var parser = textParser.createParser((msg) => {
  
  			if ( options.debugHeader )
  			{
  				console.log("recv msg: %s", msg2str(msg));
  			}
  
  			if ( !msg.isValid && this.sendMessage )
  			{
  				this.sendMessage('201', 0, 'invalid_command');
  			}
  			else
  			{
  				if ( /^\d+$/.test(msg.code) )
  				{
  					var tid = msg.tid !== null && /^\d+$/.test(msg.tid) ? msg.tid * 1 : 0;
  					
  					if ( tid > 0 )
  					{
  						if ( requestMap.hasOwnProperty(msg.tid) )
  						{
  							clearTimeout(requestMap[msg.tid].timer);
  							requestMap[msg.tid].callback(msg);
  							delete requestMap[msg.tid];
  						}
  						else
  						{
  							console.log(util.format('ERR dangling response: %s %s [%s]', msg.code, msg.tid, msg.args.join(' ')));
  						}
  					}
  					else
  					{
  						console.log(util.format('ERR invalid tid: %s %s [%s]', msg.code, msg.tid, msg.args.join(' ')));
  					}
  				}
  				else
  				{
  					var isHandled = false;
  					
  					if ( this.listenerCount(msg.code) > 0 )
  					{
  						isHandled = true;
  						this.emit(msg.code, msg);
  					}
  					
  					if ( this.listenerCount('message') > 0 )
  					{
  						isHandled = true;
  						this.emit('message', msg);
  					} 
  					
  					if ( !isHandled && autoResponseUnhandledCommand && this.sendMessage )
  					{
  						this.sendMessage('201', msg.tid, 'unknown_command');
  					}
  				}
  			}
  		});
  		
  		rwStream.on('data', (data) => {
  			parser.feed(data);
  		});
  		
  		rwStream.on('end', (e) => {
  			this.closeStream();
  		});
  	}
  	
  	if ( rwStream instanceof net.Socket )
  	{
  		if ( !rwStream.connecting )
  		{
  			this.address = address = getAddress();
  			isClosed = false;
  			log('ACCEPT');
  		}
  
  		if ( idlePingTimeout > 0 )
  		{
  			rwStream.setTimeout(idlePingTimeout);
  		}
  		else if ( idleCloseTimeout > 0 )
  		{
  			rwStream.setTimeout(idleCloseTimeout);
  		}
  		
  		rwStream.on('timeout', () => {
  			if ( idlePingTimeout > 0 )
  			{
  				this.sendMessage('PING', 0, [], null);
  				log('PING');
  			}
  			else if ( idleCloseTimeout > 0 )
  			{
  				this.closeStream();
  			}
  			else
  			{
  				this.emit('timeout');
  			}
  		});
  		
  		rwStream.on('connect', () => {
  			this.address = address = getAddress();
  			isClosed = false;
  			log('CONNECT');
  			if ( autoRegister )
  			{
  				this.sendMessage('RGST', 0, os.hostname());
  				log('RGST');
  			}
  			this.emit('connect');
  		});
  		
  		rwStream.on('close', (had_error) => {
  			this.closeStream();
  		});
  		
  		this._getSessionManager = function() {
  			if ( !this.sessionManager )
  			{
  				this.sessionManager = new SessionManager(this, options);
  			}
  			return this.sessionManager;
  		};
  		
  		this.onSession = function(protocol, callback) {
  			var smgr = this._getSessionManager();
  			var listener_options = Object.assign({}, options, {protocol: protocol});
  			var slnr = new SessionListener(listener_options);
  			slnr.on('session', callback);
  			smgr.registerListener(slnr);
  		};
  		
  		this.createSession = function(protocol, session_args) {
  			var smgr = this._getSessionManager();
  			var session_options = Object.assign({}, options, {protocol: protocol, session_args: session_args});
  			return smgr.createSession(session_options);
  		};
  	} 
  	
  	rwStream.on('error', (e) => {
  		log('ERROR ' + e.message);
  		this.closeStream();
  		this.emit('error', e);
  	});
  	
  	this.on('message_timeout', (tid) => {
  		if ( requestMap.hasOwnProperty(tid) )
  		{
  			requestMap[tid].callback({code: '500', tid: tid, args:['message_timeout'], body: null, isValid: true});
  			delete requestMap[tid];
  		}
  	});
  	
  	this.on('RGST', (msg) => {
  		host = msg.args.length > 0 ? msg.args[0] : '';
  		log('RGST');
  	});
  	
  	this.on('PING', (msg) => {
  		log('PING');
  	});
  }