Separated inner classes from WebServer.
This commit is contained in:
@@ -0,0 +1,129 @@
|
|||||||
|
package com.foundation.web.server;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.channels.SelectionKey;
|
||||||
|
import java.nio.charset.Charset;
|
||||||
|
import java.nio.charset.CharsetDecoder;
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.TimeZone;
|
||||||
|
|
||||||
|
public abstract class AbstractSocketContext implements IChannelContext {
|
||||||
|
/** The size of the buffers used per connection. */
|
||||||
|
protected static final int BUFFER_SIZE = 100000;
|
||||||
|
protected static final int SEND_BUFFER_SIZE = 20480; //2048
|
||||||
|
protected static final int RECEIVE_BUFFER_SIZE = 20480; //2048
|
||||||
|
protected static final byte[] DOUBLE_ENTER = new byte[] {0x0D, 0x0A, 0x0D, 0x0A};
|
||||||
|
/** The character set used by the HTTP messages. */
|
||||||
|
protected static final Charset charset = Charset.forName("us-ascii");
|
||||||
|
/** The decoder used to decode the HTTP messages. */
|
||||||
|
protected static final CharsetDecoder decoder = charset.newDecoder();
|
||||||
|
/** The format used for dates in the HTTP header. */
|
||||||
|
protected static final ThreadLocal httpDateFormat = new ThreadLocal();
|
||||||
|
protected static final String httpDateFormatString = "EEE, d MMM yyyy HH:mm:ss z";
|
||||||
|
|
||||||
|
/** The next available socket context id. */
|
||||||
|
private static int nextSocketContextId = 1;
|
||||||
|
/** The network listener that created this socket context. */
|
||||||
|
private final NetworkListener networkListener;
|
||||||
|
/** The debug ID for the socket. */
|
||||||
|
public final int id;
|
||||||
|
/** The web server that created the socket context. */
|
||||||
|
protected WebServer webServer = null;
|
||||||
|
/** The key that represents the connection between the channel (socket) and the selector used to multiplex the listener. The code must synchronize on this attribute when accessing the isUsed functionality, or when interacting with the key's interestOps. */
|
||||||
|
protected SelectionKey key = null;
|
||||||
|
/** Whether the socket is currently being used by a thread designated by the network listener thread to read or write to the socket. Currently the socket type we use in Java only allows one thread to read and write at a time. Note: Always synchronize on <code>key</code> before using this attribute. */
|
||||||
|
private boolean isUsed = false;
|
||||||
|
/** A socket context related to this one (when two are tied together such that data from one immediately is sent to the other). */
|
||||||
|
protected AbstractSocketContext relatedSocketContext = null;
|
||||||
|
/**
|
||||||
|
* Gets the next available socket id.
|
||||||
|
* @return The ID useful for debugging.
|
||||||
|
*/
|
||||||
|
protected static int getNextSocketContextId() {return nextSocketContextId++;}
|
||||||
|
/**
|
||||||
|
* Gets the threads date format for processing HTTP header dates.
|
||||||
|
* <p>This uses a thread local because the date format class is not thread safe :(.</p>
|
||||||
|
* @return The date format for handling http header dates.
|
||||||
|
*/
|
||||||
|
protected static SimpleDateFormat getHttpDateFormat() {
|
||||||
|
SimpleDateFormat result = (SimpleDateFormat) httpDateFormat.get();
|
||||||
|
|
||||||
|
if(result == null) {
|
||||||
|
result = new SimpleDateFormat(httpDateFormatString);
|
||||||
|
result.setTimeZone(TimeZone.getTimeZone("GMT"));
|
||||||
|
httpDateFormat.set(result);
|
||||||
|
}//if//
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}//getHttpDateFormat()//
|
||||||
|
/**
|
||||||
|
* AbstractSocketContext constructor.
|
||||||
|
*/
|
||||||
|
public AbstractSocketContext(NetworkListener networkListener) {
|
||||||
|
this.networkListener = networkListener;
|
||||||
|
|
||||||
|
synchronized(AbstractSocketContext.class) {
|
||||||
|
this.id = getNextSocketContextId();
|
||||||
|
}//synchronized//
|
||||||
|
}//AbstractSocketContext()//
|
||||||
|
/** Gets the network listener the socket exists within. */
|
||||||
|
public NetworkListener getNetworkListener() {return networkListener;}
|
||||||
|
/** Gets the web server the socket exists within. */
|
||||||
|
public WebServer getWebServer() {return networkListener.getWebServer();}
|
||||||
|
/** Gets whether the socket context is currently in use by a thread. */
|
||||||
|
public boolean getIsUsed() {return isUsed;}
|
||||||
|
/** Sets whether the socket context is currently in use by a thread. */
|
||||||
|
public void setIsUsed(boolean isUsed) {this.isUsed = isUsed;}
|
||||||
|
/**
|
||||||
|
* Gets the lockable (synchronizable) object for this context. For contexts with a related context, only one of the two will be returned, such that a single synchronize block covers both contexts.
|
||||||
|
* @return The object to synchronize on such that two threads don't attempt to interact with the context at the same time (AsynchronousSocketChannel required for that).
|
||||||
|
*/
|
||||||
|
protected abstract Object getLock();
|
||||||
|
/**
|
||||||
|
* Writes the next responses/messages in the sequence.
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
protected abstract void writeOutgoingMessages() throws IOException;
|
||||||
|
/**
|
||||||
|
* Reads the next requests/messages received via the socket.
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
protected abstract void readIncomingMessages() throws IOException;
|
||||||
|
/**
|
||||||
|
* Passes the message through to a receiving process via a second socket.
|
||||||
|
* @param buffer The buffer containing the message. This buffer will not be retained by this method call, and can be reused by the caller.
|
||||||
|
* @return Whether the whole message was transfered.
|
||||||
|
*/
|
||||||
|
protected abstract boolean passThrough(ByteBuffer buffer);
|
||||||
|
/**
|
||||||
|
* Closes the socket context and cleans up.
|
||||||
|
*/
|
||||||
|
protected abstract void close();
|
||||||
|
/**
|
||||||
|
* Gets the socket context related to this one (when two are tied together such that data from one immediately is sent to the other).
|
||||||
|
* @return The related socket context, or null if none exists (data not forwarded to a remote server).
|
||||||
|
*/
|
||||||
|
protected AbstractSocketContext getRelatedSocketContext() {return relatedSocketContext;}
|
||||||
|
/**
|
||||||
|
* Determines whether the socket has a pending write operation.
|
||||||
|
*/
|
||||||
|
protected abstract boolean hasPendingWrite();
|
||||||
|
/**
|
||||||
|
* Called to notify the network listener that a pending write operation exists for this socket.
|
||||||
|
*/
|
||||||
|
protected void notifyListenerOfPendingWrite() {
|
||||||
|
synchronized(key) {
|
||||||
|
//Ignore if a thread is using this socket currently since all operation flags will be set at the end of the use of the socket.//
|
||||||
|
if(!isUsed) {
|
||||||
|
int ops = key.interestOps();
|
||||||
|
boolean hasWrite = (ops & SelectionKey.OP_WRITE) != 0;
|
||||||
|
|
||||||
|
if(!hasWrite) {
|
||||||
|
key.interestOps(ops | SelectionKey.OP_WRITE);
|
||||||
|
key.selector().wakeup();
|
||||||
|
}//if//
|
||||||
|
}//if//
|
||||||
|
}//synchronized//
|
||||||
|
}//notifyListenerOfPendingWrite()//
|
||||||
|
}//AbstractSocketContext//
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package com.foundation.web.server;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides a place for channel oriented data.
|
||||||
|
*/
|
||||||
|
public interface IChannelContext {
|
||||||
|
}//IChannelContext//
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
package com.foundation.web.server;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
|
import com.foundation.web.interfaces.IContent;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The response message buffer encapsulating the request generating the response, and the content, and chainable into a linked list.
|
||||||
|
*/
|
||||||
|
class MessageBuffer {
|
||||||
|
/** The actual underlying buffer containing the bytes to be sent. Will be null if the message buffer needs initializing or has finished. */
|
||||||
|
private ByteBuffer buffer = null;
|
||||||
|
/** The ability to chain message buffers into a linked list. */
|
||||||
|
private MessageBuffer next = null;
|
||||||
|
|
||||||
|
/** The optional response the message is based upon. */
|
||||||
|
private Response response = null;
|
||||||
|
/** The content if there is any. */
|
||||||
|
private IContent content = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MessageBuffer constructor.
|
||||||
|
*/
|
||||||
|
public MessageBuffer() {
|
||||||
|
}//MessageBuffer()//
|
||||||
|
/**
|
||||||
|
* MessageBuffer constructor.
|
||||||
|
* @param buffer The buffer to use for assembling the message bytes.
|
||||||
|
*/
|
||||||
|
public MessageBuffer(ByteBuffer buffer) {
|
||||||
|
setBuffer(buffer);
|
||||||
|
}//MessageBuffer()//
|
||||||
|
/**
|
||||||
|
* Sets the actual underlying buffer for the message buffer.
|
||||||
|
* @param buffer
|
||||||
|
*/
|
||||||
|
protected void setBuffer(ByteBuffer buffer) {
|
||||||
|
this.buffer = buffer;
|
||||||
|
|
||||||
|
if(buffer != null && buffer.position() != 0) buffer.flip();
|
||||||
|
}//setBuffer()//
|
||||||
|
/**
|
||||||
|
* MessageBuffer constructor.
|
||||||
|
* @param buffer The buffer to use for assembling the message bytes.
|
||||||
|
* @param response The optional response that generates this message.
|
||||||
|
* @param content The content if the response is not just a header.
|
||||||
|
*/
|
||||||
|
public MessageBuffer(ByteBuffer buffer, Response response, IContent content) {
|
||||||
|
this.buffer = buffer;
|
||||||
|
this.content = content;
|
||||||
|
|
||||||
|
//Fill the remaining buffer space with the content.//
|
||||||
|
if(content != null) {
|
||||||
|
content.get(buffer);
|
||||||
|
}//if//
|
||||||
|
|
||||||
|
//Flip the buffer (if not already flipped) so we can write out the bytes.//
|
||||||
|
if(buffer.position() != 0) buffer.flip();
|
||||||
|
this.response = response;
|
||||||
|
}//MessageBuffer()//
|
||||||
|
/**
|
||||||
|
* Initializes the message buffer for use.
|
||||||
|
* @return Whether initialization succeded. Intialization should be considered a success even if none is required or has already been performed. If it fails the caller should close the socket.
|
||||||
|
*/
|
||||||
|
public boolean initialize() {
|
||||||
|
//Does nothing by default. Subclasses may implement.//
|
||||||
|
return true;
|
||||||
|
}//initialize()//
|
||||||
|
/**
|
||||||
|
* Whether the message buffer is closed and all bytes have been sent.
|
||||||
|
* @return If the bytes have all been sent.
|
||||||
|
*/
|
||||||
|
public boolean isClosed() {
|
||||||
|
return buffer == null;
|
||||||
|
}//isClosed()//
|
||||||
|
/**
|
||||||
|
* Closes the message buffer.
|
||||||
|
*/
|
||||||
|
public void close() {
|
||||||
|
this.buffer = null;
|
||||||
|
}//close()//
|
||||||
|
/**
|
||||||
|
* Gets the byte buffer containing the current portion of the message to be sent.
|
||||||
|
* @return The buffer containing the next part of the message to be sent, or null if the message end has been reached.
|
||||||
|
*/
|
||||||
|
public ByteBuffer getBuffer() {return buffer;}
|
||||||
|
/**
|
||||||
|
* Loads the next part of the message into the buffer (any remaining bytes in the buffer will be compacted).
|
||||||
|
* @return Whether the buffer could be loaded with the next part of the message. If false, then the caller should try again in the future when additional message content may be available. Will always be false if there is no content to load from.
|
||||||
|
*/
|
||||||
|
public boolean loadBuffer() {
|
||||||
|
boolean result = true;
|
||||||
|
|
||||||
|
if(buffer != null) {
|
||||||
|
if(content != null) {
|
||||||
|
int getResult;
|
||||||
|
|
||||||
|
buffer.compact();
|
||||||
|
getResult = content.get(buffer);
|
||||||
|
|
||||||
|
if(getResult == IContent.CONTENT_PENDING) {
|
||||||
|
result = false; //Should never occur currently: See StreamedContent's javadocs.//
|
||||||
|
}//if//
|
||||||
|
else if(getResult == IContent.CONTENT_END) {
|
||||||
|
buffer = null;
|
||||||
|
}//else if//
|
||||||
|
|
||||||
|
if(buffer != null && buffer.position() != 0) buffer.flip();
|
||||||
|
}//if//
|
||||||
|
else if(!buffer.hasRemaining()) {
|
||||||
|
//Clear the buffer pointer indicating the message buffer is done.//
|
||||||
|
buffer = null;
|
||||||
|
result = false;
|
||||||
|
}//else if//
|
||||||
|
}//if//
|
||||||
|
else {
|
||||||
|
result = false;
|
||||||
|
}//else//
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}//loadBuffer()//
|
||||||
|
/** Gets the next message buffer (only used for pass through sockets). */
|
||||||
|
public MessageBuffer getNext() {return next;}
|
||||||
|
/** Sets the next message buffer (only used for pass through sockets). */
|
||||||
|
public void setNext(MessageBuffer next) {this.next = next;}
|
||||||
|
/** Gets the response object that created the message. This will be null for pass through sockets. */
|
||||||
|
public Response getResponse() {return response;}
|
||||||
|
}//MessageBuffer//
|
||||||
@@ -0,0 +1,267 @@
|
|||||||
|
package com.foundation.web.server;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.channels.SelectableChannel;
|
||||||
|
import java.nio.channels.SelectionKey;
|
||||||
|
import java.nio.channels.Selector;
|
||||||
|
import java.nio.channels.ServerSocketChannel;
|
||||||
|
import java.nio.channels.SocketChannel;
|
||||||
|
import java.util.Iterator;
|
||||||
|
|
||||||
|
import com.common.debug.Debug;
|
||||||
|
import com.common.thread.ThreadService;
|
||||||
|
import com.common.util.LiteList;
|
||||||
|
import com.foundation.web.server.WebServer.TlsFailureException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encapsulates the code that threads incomming socket requests and messages over sockets.
|
||||||
|
* <p>Note that the HTTP protocol requires that the reponses be sent in order of the received requests.</p>
|
||||||
|
*/
|
||||||
|
class NetworkListener implements Runnable {
|
||||||
|
private final WebServer webServer;
|
||||||
|
Selector selector = null;
|
||||||
|
private Iterator selectedKeys = null;
|
||||||
|
private volatile boolean stop = true;
|
||||||
|
private volatile boolean hasRunnables = false;
|
||||||
|
private LiteList runnables = new LiteList(10, 20);
|
||||||
|
public NetworkListener(WebServer webServer, Selector selector) {
|
||||||
|
this.webServer = webServer;
|
||||||
|
this.selector = selector;
|
||||||
|
}//NetworkListener()//
|
||||||
|
/** Gets the web server that created this network listener. */
|
||||||
|
public WebServer getWebServer() {return webServer;}
|
||||||
|
/**
|
||||||
|
* Stops the network listener.
|
||||||
|
* Note that this may take a short time to complete.
|
||||||
|
*/
|
||||||
|
public void stop() {
|
||||||
|
if(!stop) {
|
||||||
|
stop = true;
|
||||||
|
selector.wakeup();
|
||||||
|
}//if//
|
||||||
|
}//stop()//
|
||||||
|
/**
|
||||||
|
* Starts the network listener.
|
||||||
|
*/
|
||||||
|
public void start() {
|
||||||
|
if(stop) {
|
||||||
|
stop = false;
|
||||||
|
ThreadService.run(this);
|
||||||
|
// Thread t = new Thread(this);
|
||||||
|
// t.setName("Network Listener");
|
||||||
|
// t.start();
|
||||||
|
}//if//
|
||||||
|
}//start()//
|
||||||
|
/**
|
||||||
|
* Cleans up after the client channel.
|
||||||
|
* @param context The context associated with the client connection.
|
||||||
|
* @param channel The client connection that is now closed.
|
||||||
|
*/
|
||||||
|
private void cleanupClientChannel(SocketContext context, SocketChannel channel) {
|
||||||
|
if(this.webServer.debug) {
|
||||||
|
Debug.log("Connection closed to " + channel.socket().getInetAddress() + ":" + channel.socket().getPort());
|
||||||
|
}//if//
|
||||||
|
}//cleanupClientChannel()//
|
||||||
|
/**
|
||||||
|
* Adds a runnable to the list of runnables to be run next time the loop is woken.
|
||||||
|
* @param runnable The runnable to be run by the thread that is listening for socket events.
|
||||||
|
*/
|
||||||
|
public synchronized void queue(Runnable runnable) {
|
||||||
|
runnables.add(runnable);
|
||||||
|
hasRunnables = true;
|
||||||
|
}//queue()//
|
||||||
|
/**
|
||||||
|
* Checks for runnables and runs them if there are any.
|
||||||
|
*/
|
||||||
|
private void checkForRunnables() {
|
||||||
|
if(hasRunnables) {
|
||||||
|
synchronized(this) {
|
||||||
|
while(runnables.getSize() > 0) {
|
||||||
|
((Runnable) runnables.remove(0)).run();
|
||||||
|
}//while//
|
||||||
|
|
||||||
|
hasRunnables = false;
|
||||||
|
}//synchronized//
|
||||||
|
}//if//
|
||||||
|
}//checkForRunnables()//
|
||||||
|
/* (non-Javadoc)
|
||||||
|
* @see java.lang.Runnable#run()
|
||||||
|
*/
|
||||||
|
public void run() {
|
||||||
|
//Looping only occurs when we are at the maximum allowed number of threads handling messages.//
|
||||||
|
while(!stop) {
|
||||||
|
SelectionKey key = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
//If we don't have an iterator over the active channels then get one and block if necessary.//
|
||||||
|
if(selectedKeys == null) {
|
||||||
|
int keyCount = 0;
|
||||||
|
|
||||||
|
//Block until we have keys or were awakened by another thread.//
|
||||||
|
keyCount = selector.select();
|
||||||
|
//Check for any pending runnables that need executing on this thread.//
|
||||||
|
checkForRunnables();
|
||||||
|
|
||||||
|
//If we have active keys then retrieve them.//
|
||||||
|
if(keyCount > 0) {
|
||||||
|
selectedKeys = selector.selectedKeys().iterator();
|
||||||
|
}//if//
|
||||||
|
}//if//
|
||||||
|
|
||||||
|
//If we have an iterator over the active channels then get and remove the next one (clean up the iterator if empty).//
|
||||||
|
if(selectedKeys != null) {
|
||||||
|
key = (SelectionKey) selectedKeys.next();
|
||||||
|
selectedKeys.remove();
|
||||||
|
|
||||||
|
//Weed out invalid (cancelled) keys.//
|
||||||
|
if(!key.isValid()) {
|
||||||
|
key = null;
|
||||||
|
}//if//
|
||||||
|
|
||||||
|
if(!selectedKeys.hasNext()) {
|
||||||
|
selectedKeys = null;
|
||||||
|
}//if//
|
||||||
|
}//if//
|
||||||
|
}//try//
|
||||||
|
catch(Throwable e) {
|
||||||
|
//TODO: Can we recover?
|
||||||
|
Debug.log(e);
|
||||||
|
}//catch//
|
||||||
|
|
||||||
|
try {
|
||||||
|
if(key != null) {
|
||||||
|
final boolean isWrite = key.isWritable();
|
||||||
|
final IChannelContext context = (IChannelContext) key.attachment();
|
||||||
|
final SelectableChannel channel = key.channel();
|
||||||
|
final SelectionKey selectionKey = key;
|
||||||
|
|
||||||
|
if(channel instanceof ServerSocketChannel) {
|
||||||
|
try {
|
||||||
|
ServerSocketChannel serverSocketChannel = (ServerSocketChannel) channel;
|
||||||
|
SocketChannel socketChannel = serverSocketChannel.accept();
|
||||||
|
ServerSocketContext serverSocketContext = (ServerSocketContext) context;
|
||||||
|
SocketContext socketContext = new SocketContext(serverSocketContext, this);
|
||||||
|
|
||||||
|
socketChannel.configureBlocking(false);
|
||||||
|
socketChannel.socket().setSendBufferSize(AbstractSocketContext.SEND_BUFFER_SIZE);
|
||||||
|
socketChannel.socket().setReceiveBufferSize(AbstractSocketContext.RECEIVE_BUFFER_SIZE);
|
||||||
|
socketContext.key = socketChannel.register(selector, SelectionKey.OP_READ, socketContext);
|
||||||
|
socketContext.serverSocketContext = serverSocketContext;
|
||||||
|
|
||||||
|
//Debug.log("Connection opened to " + socketChannel.socket().getInetAddress() + ":" + socketChannel.socket().getPort());
|
||||||
|
|
||||||
|
if(serverSocketContext.serviceListener.type != IServiceListener.TYPE_SSL) {
|
||||||
|
socketContext.socketReadBuffer = ByteBuffer.allocate(AbstractSocketContext.BUFFER_SIZE);
|
||||||
|
}//if//
|
||||||
|
|
||||||
|
if(this.webServer.debug) {
|
||||||
|
Debug.log("Connection opened to " + socketChannel.socket().getInetAddress() + ":" + socketChannel.socket().getPort());
|
||||||
|
}//if//
|
||||||
|
}//try//
|
||||||
|
catch(Throwable e) {
|
||||||
|
//TODO: Can we recover?
|
||||||
|
Debug.log(e);
|
||||||
|
}//catch//
|
||||||
|
}//if//
|
||||||
|
else if(channel instanceof SocketChannel) {
|
||||||
|
// boolean socketClosed = false;
|
||||||
|
|
||||||
|
//Toggle the write or read flag.//
|
||||||
|
synchronized(key) {
|
||||||
|
// //Save the ops that will be set when the processing is complete.//
|
||||||
|
// ((AbstractSocketContext) context).setFlags(key.interestOps());
|
||||||
|
|
||||||
|
//Notes: Java (pre-jdk7) does not have the ability to read and write to a socket at the same time (two threads, one socket). Post jdk7 there is AsynchronousSocketChannel and AsynchronousServerSocketChannel which could be used to send/receive at the same time.
|
||||||
|
//Truely enabling Speedy would require a thread to read which when finished would flag read again BEFORE processing the message and BEFORE sending a response.
|
||||||
|
//For now (so we don't have to require jdk7 yet) we will simply allow Speedy to queue up messages, but only read, process, and then write them one at a time. Most of the speed loss is in the waiting for the WRITE to finish before handling the next request (due to it being broken into packets and the mechanics of TCP), and that is generally minimal (speed lose) since usually the bottleneck in speed is the browser's connection to the internet (most of us haven't got Gigabit Ethernet at home). Anyone with enough home juice to have this be a problem would only notice the difference for really porky websites (which is a problem in and of its self).
|
||||||
|
|
||||||
|
//Not allowing either reads or writes to continue until all processing of this message is done.//
|
||||||
|
((AbstractSocketContext) context).setIsUsed(true);
|
||||||
|
key.interestOps(0);
|
||||||
|
|
||||||
|
//The problem with this is that we'd have to use AsynchronousSocketChannel which would appear to require a complete rewrite of everything since it operates completely differently.//
|
||||||
|
// key.interestOps(key.interestOps() ^ (isWrite ? SelectionKey.OP_WRITE : SelectionKey.OP_READ));
|
||||||
|
}//synchronized//
|
||||||
|
|
||||||
|
if(((SocketChannel) channel).isOpen()) {
|
||||||
|
ThreadService.run(new Runnable() {
|
||||||
|
public void run() {
|
||||||
|
boolean socketClosed = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if(isWrite) {
|
||||||
|
//Prevent another thread from reading/writing on the same socket at the same time (safety). This would have to be removed if SPEEDY (or similar pipelining) were allowed, and AsynchronousSocketChannel/AsynchronousServerSocketChannel would have to be used (requiring jdk7).//
|
||||||
|
synchronized(((AbstractSocketContext) context).getLock()) {
|
||||||
|
//Process the pending write to the socket as much as is possible, then return.//
|
||||||
|
((AbstractSocketContext) context).writeOutgoingMessages();
|
||||||
|
}//synchronized//
|
||||||
|
}//if//
|
||||||
|
else {
|
||||||
|
//Prevent another thread from reading/writing on the same socket at the same time (safety). This would have to be removed if SPEEDY (or similar pipelining) were allowed, and AsynchronousSocketChannel/AsynchronousServerSocketChannel would have to be used (requiring jdk7).//
|
||||||
|
synchronized(((AbstractSocketContext) context).getLock()) {
|
||||||
|
//Process the incoming request and send the response (a partial response may be sent in which case the socket will be set to wait for a write opportunity and not a read opportunity).//
|
||||||
|
((AbstractSocketContext) context).readIncomingMessages();
|
||||||
|
}//synchronized//
|
||||||
|
}//else//
|
||||||
|
}//try//
|
||||||
|
catch(TlsFailureException e) {
|
||||||
|
//Allow the failure to be ignored. This occurs when the client fails to use TLS or fails to send the host name as part of the TLS handshake.//
|
||||||
|
try {((SocketChannel) channel).close();}catch(Throwable e2) {} //Release the socket so the message doesn't continue to be processed.//
|
||||||
|
}//catch//
|
||||||
|
catch(Throwable e) {
|
||||||
|
if(NetworkListener.this.webServer.debug) Debug.log(e);
|
||||||
|
|
||||||
|
//Force the socket to be closed (for sure).//
|
||||||
|
try {((SocketChannel) channel).close();} catch(Throwable e2) {}
|
||||||
|
//Debug.log(e);
|
||||||
|
socketClosed = true;
|
||||||
|
}//catch//
|
||||||
|
finally {
|
||||||
|
if(channel != null && !socketClosed && channel.isOpen() && context != null) {
|
||||||
|
try {
|
||||||
|
//Set the new ops for the selection key and notify the selector that ops have changed.//
|
||||||
|
synchronized(selectionKey) {
|
||||||
|
if(selectionKey.isValid()) {
|
||||||
|
//Always flag the socket for reading, only flag the socket for writing if a pending write operation exists.//
|
||||||
|
selectionKey.interestOps(SelectionKey.OP_READ | (((AbstractSocketContext) context).hasPendingWrite() ? SelectionKey.OP_WRITE : 0));
|
||||||
|
}//if//
|
||||||
|
else {
|
||||||
|
Debug.log(new RuntimeException("Woops! Somehow the selection key isn't valid, but the socket isn't closed either!"));
|
||||||
|
try {((SocketChannel) channel).close();} catch(Throwable e2) {}
|
||||||
|
cleanupClientChannel((SocketContext) context, (SocketChannel) channel);
|
||||||
|
}//else//
|
||||||
|
|
||||||
|
((AbstractSocketContext) context).setIsUsed(false);
|
||||||
|
}//synchronized//
|
||||||
|
|
||||||
|
selector.wakeup();
|
||||||
|
}//try//
|
||||||
|
catch(Throwable e) {
|
||||||
|
Debug.log(e);
|
||||||
|
}//catch//
|
||||||
|
}//if//
|
||||||
|
else if(channel != null && (!channel.isOpen() || socketClosed) && channel instanceof SocketChannel && context instanceof SocketContext) {
|
||||||
|
cleanupClientChannel((SocketContext) context, (SocketChannel) channel);
|
||||||
|
}//else if//
|
||||||
|
else {
|
||||||
|
//This shouldn't be called I don't think.//
|
||||||
|
Debug.log(new RuntimeException("Woops! Somehow we aren't closed and we didn't setup the interestOps for the HTTP socket!"));
|
||||||
|
}//else//
|
||||||
|
}//finally//
|
||||||
|
}//run()//
|
||||||
|
});
|
||||||
|
}//if//
|
||||||
|
}//else if//
|
||||||
|
}//if//
|
||||||
|
}//try//
|
||||||
|
catch(java.nio.channels.CancelledKeyException e) {
|
||||||
|
//Occurs if the socket is closed while we are handling the key.//
|
||||||
|
Debug.log(e); //TODO: Does anything need doing here? Should it be ignored?
|
||||||
|
}//catch//
|
||||||
|
catch(Throwable e) {
|
||||||
|
Debug.log(e);
|
||||||
|
//TODO: There needs to be more specfic error handling if we got here.
|
||||||
|
}//catch//
|
||||||
|
}//while//
|
||||||
|
}//run()//
|
||||||
|
}//NetworkListener//
|
||||||
@@ -0,0 +1,168 @@
|
|||||||
|
package com.foundation.web.server;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.InetSocketAddress;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.channels.SelectionKey;
|
||||||
|
import java.nio.channels.SocketChannel;
|
||||||
|
import com.foundation.web.server.WebServer.RegisterKeyRunnable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used by the SocketContext to create a connection to a remote process that will receive all client data once decrypted, and whose output will be encrypted and sent directly to the client.
|
||||||
|
* Allows the web server to act as an SSL front to another web server or service.
|
||||||
|
*/
|
||||||
|
public class PassThroughSocketContext extends AbstractSocketContext {
|
||||||
|
private MessageBuffer pendingMessageBuffer = null;
|
||||||
|
private MessageBuffer lastAddedMessageBuffer = null;
|
||||||
|
/** The byte buffer used to read data from the socket. */
|
||||||
|
public ByteBuffer socketReadBuffer = ByteBuffer.allocate(BUFFER_SIZE);
|
||||||
|
/**
|
||||||
|
* PassThroughSocketContext constructor.
|
||||||
|
* @param linkedClientContext
|
||||||
|
* @param address
|
||||||
|
* @param port
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
public PassThroughSocketContext(SocketContext linkedClientContext, String address, int port) throws IOException {
|
||||||
|
super(linkedClientContext.getNetworkListener());
|
||||||
|
this.relatedSocketContext = linkedClientContext;
|
||||||
|
SocketChannel channel = SocketChannel.open();
|
||||||
|
RegisterKeyRunnable runnable;
|
||||||
|
|
||||||
|
//Connect while still blocking - will wait until the connection finishes or times out.//
|
||||||
|
channel.connect(new InetSocketAddress(address, port));
|
||||||
|
//Setup the channel for non-blocking from here on out.//
|
||||||
|
channel.configureBlocking(false);
|
||||||
|
channel.socket().setSendBufferSize(SEND_BUFFER_SIZE);
|
||||||
|
channel.socket().setReceiveBufferSize(RECEIVE_BUFFER_SIZE);
|
||||||
|
channel.socket().setTcpNoDelay(false);
|
||||||
|
getNetworkListener().queue(runnable = new RegisterKeyRunnable(this, channel, linkedClientContext.key.selector()));
|
||||||
|
linkedClientContext.key.selector().wakeup();
|
||||||
|
runnable.waitForRun();
|
||||||
|
key = runnable.getKey();
|
||||||
|
|
||||||
|
//Set the initial interest ops to read.//
|
||||||
|
synchronized(key) {
|
||||||
|
key.interestOps(SelectionKey.OP_READ);
|
||||||
|
}//synchronized//
|
||||||
|
|
||||||
|
key.selector().wakeup();
|
||||||
|
}//PassThroughSocketContext()//
|
||||||
|
/* (non-Javadoc)
|
||||||
|
* @see com.foundation.web.server.WebServer.AbstractSocketContext#getLock()
|
||||||
|
*/
|
||||||
|
protected Object getLock() {
|
||||||
|
return getRelatedSocketContext();
|
||||||
|
}//getLock()//
|
||||||
|
/* (non-Javadoc)
|
||||||
|
* @see com.foundation.web.server.WebServer.AbstractSocketContext#processResponses()
|
||||||
|
*/
|
||||||
|
protected synchronized void writeOutgoingMessages() throws IOException {
|
||||||
|
//Actually this is called when a request is being sent via the pass through socket (sending the request to the remote server).//
|
||||||
|
//Synchronized to avoid accessing the pendingMessageBuffer and lastAddedMessageBuffer at the same time as a thread that is calling passThrough(ByteBuffer) which also accesses these variables.//
|
||||||
|
boolean result = true;
|
||||||
|
|
||||||
|
if(result && pendingMessageBuffer != null) {
|
||||||
|
//Check to see if the outbound message is prepared to send more content.//
|
||||||
|
if(!pendingMessageBuffer.getBuffer().hasRemaining()) {
|
||||||
|
//Load the next pending outbound message in the chain.//
|
||||||
|
if(pendingMessageBuffer.getNext() != null) {
|
||||||
|
pendingMessageBuffer = pendingMessageBuffer.getNext();
|
||||||
|
}//if//
|
||||||
|
else {
|
||||||
|
//Wait until additional message bytes are available.//
|
||||||
|
result = false;
|
||||||
|
pendingMessageBuffer = null;
|
||||||
|
lastAddedMessageBuffer = null;
|
||||||
|
}//else//
|
||||||
|
}//if//
|
||||||
|
|
||||||
|
//Keep sending encrypted frames until the output buffer is full, or we run out of message to send.//
|
||||||
|
while(result && (pendingMessageBuffer != null) && pendingMessageBuffer.getBuffer().hasRemaining()) {
|
||||||
|
//Write the bytes to the stream.//
|
||||||
|
((SocketChannel) key.channel()).write(pendingMessageBuffer.getBuffer());
|
||||||
|
|
||||||
|
//If not all the bytes could be written then we will need to wait until we can write more.//
|
||||||
|
if(pendingMessageBuffer.getBuffer().hasRemaining()) {
|
||||||
|
result = false;
|
||||||
|
}//if//
|
||||||
|
else {
|
||||||
|
//Load the next pending outbound message in the chain.//
|
||||||
|
if(pendingMessageBuffer.getNext() != null) {
|
||||||
|
pendingMessageBuffer = pendingMessageBuffer.getNext();
|
||||||
|
}//if//
|
||||||
|
else {
|
||||||
|
//Wait until additional message bytes are available.//
|
||||||
|
result = false;
|
||||||
|
pendingMessageBuffer = null;
|
||||||
|
lastAddedMessageBuffer = null;
|
||||||
|
}//else//
|
||||||
|
}//else//
|
||||||
|
}//while//
|
||||||
|
}//if//
|
||||||
|
}//processResponses()//
|
||||||
|
/* (non-Javadoc)
|
||||||
|
* @see com.foundation.web.server.WebServer.AbstractSocketContext#processRequest()
|
||||||
|
*/
|
||||||
|
protected void readIncomingMessages() throws IOException {
|
||||||
|
//Actually this is called when a response is being processed via the pass through socket (remote process that received the request).//
|
||||||
|
int count = 1;
|
||||||
|
int loopCount = 0;
|
||||||
|
boolean result = true;
|
||||||
|
SocketChannel channel = (SocketChannel) key.channel();
|
||||||
|
|
||||||
|
//While we have a count greater than zero, indicating that some data is comming through, keep reading and processing the data.//
|
||||||
|
//Note: We are throddling this for active connections to prevent a single connection from hogging all the resources.//
|
||||||
|
while(loopCount < 10 && result && count > 0) {
|
||||||
|
loopCount++;
|
||||||
|
count = channel.read(socketReadBuffer);
|
||||||
|
socketReadBuffer.flip();
|
||||||
|
|
||||||
|
if(count == -1) {
|
||||||
|
//The socket has been closed by the client.//
|
||||||
|
try {relatedSocketContext.close();} catch(Throwable e) {}
|
||||||
|
}//if//
|
||||||
|
else if(socketReadBuffer.hasRemaining()) {
|
||||||
|
result = relatedSocketContext.passThrough(socketReadBuffer);
|
||||||
|
socketReadBuffer.compact();
|
||||||
|
}//else//
|
||||||
|
else {
|
||||||
|
socketReadBuffer.compact();
|
||||||
|
break;
|
||||||
|
}//else//
|
||||||
|
}//while//
|
||||||
|
}//processRequest()//
|
||||||
|
/* (non-Javadoc)
|
||||||
|
* @see com.foundation.web.server.WebServer.AbstractSocketContext#passThrough(java.nio.ByteBuffer)
|
||||||
|
*/
|
||||||
|
protected synchronized boolean passThrough(ByteBuffer buffer) {
|
||||||
|
ByteBuffer messageBytes = ByteBuffer.allocate(buffer.remaining());
|
||||||
|
MessageBuffer message;
|
||||||
|
|
||||||
|
//Create a new buffer to hold the data so we don't modify the passed buffer (other than to update its position).//
|
||||||
|
messageBytes = ByteBuffer.allocate(buffer.remaining());
|
||||||
|
messageBytes.put(buffer);
|
||||||
|
message = new MessageBuffer(messageBytes);
|
||||||
|
|
||||||
|
//Chain the message into the linked list.
|
||||||
|
if(lastAddedMessageBuffer == null) {
|
||||||
|
pendingMessageBuffer = lastAddedMessageBuffer = message;
|
||||||
|
}//if//
|
||||||
|
else {
|
||||||
|
lastAddedMessageBuffer.setNext(message);
|
||||||
|
lastAddedMessageBuffer = message;
|
||||||
|
}//else//
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}//passThrough()//
|
||||||
|
protected synchronized void close() {
|
||||||
|
try {if(key != null && key.channel() != null) key.channel().close();} catch(Throwable e) {}
|
||||||
|
try {if(key != null) key.cancel();} catch(Throwable e) {}
|
||||||
|
}//close()//
|
||||||
|
/* (non-Javadoc)
|
||||||
|
* @see com.foundation.web.server.WebServer.AbstractSocketContext#hasPendingWrite()
|
||||||
|
*/
|
||||||
|
protected boolean hasPendingWrite() {
|
||||||
|
return pendingMessageBuffer != null;
|
||||||
|
}//hasPendingWrite()//
|
||||||
|
}//PassThroughSocketContext//
|
||||||
@@ -475,7 +475,7 @@ public class Request implements IRequest {
|
|||||||
else if(fieldName.equals("if-modified-since")) {
|
else if(fieldName.equals("if-modified-since")) {
|
||||||
if(fieldValue.trim().length() > 0) { //Mozilla sometimes sends an empty value. Bad Mozilla.//
|
if(fieldValue.trim().length() > 0) { //Mozilla sometimes sends an empty value. Bad Mozilla.//
|
||||||
try {
|
try {
|
||||||
cacheDate = WebServer.getHttpDateFormat().parse(fieldValue.trim());
|
cacheDate = SocketContext.getHttpDateFormat().parse(fieldValue.trim());
|
||||||
}//try//
|
}//try//
|
||||||
catch(Throwable e) {
|
catch(Throwable e) {
|
||||||
Debug.log("Unexpected date format sent by the web browser when using an If-Modified-Since request header: '" + fieldValue + "'. The server can recover from this error.", e);
|
Debug.log("Unexpected date format sent by the web browser when using an If-Modified-Since request header: '" + fieldValue + "'. The server can recover from this error.", e);
|
||||||
@@ -514,7 +514,7 @@ public class Request implements IRequest {
|
|||||||
else if(fieldName.equals("if-unmodified-since")) {
|
else if(fieldName.equals("if-unmodified-since")) {
|
||||||
if(fieldValue.trim().length() > 0) { //Shouldn't ever happen.//
|
if(fieldValue.trim().length() > 0) { //Shouldn't ever happen.//
|
||||||
try {
|
try {
|
||||||
unmodifiedSince = WebServer.getHttpDateFormat().parse(fieldValue.trim());
|
unmodifiedSince = SocketContext.getHttpDateFormat().parse(fieldValue.trim());
|
||||||
}//try//
|
}//try//
|
||||||
catch(Throwable e) {
|
catch(Throwable e) {
|
||||||
Debug.log("Unexpected date format sent by the web browser when using an Unmodified-Since request header: '" + fieldValue + "'. The server can recover from this error.", e);
|
Debug.log("Unexpected date format sent by the web browser when using an Unmodified-Since request header: '" + fieldValue + "'. The server can recover from this error.", e);
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package com.foundation.web.server;
|
||||||
|
|
||||||
|
import com.foundation.web.server.WebServer.ServiceListener;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides a place connection oriented data.
|
||||||
|
*/
|
||||||
|
public class ServerSocketContext implements IChannelContext {
|
||||||
|
public ServiceListener serviceListener = null;
|
||||||
|
|
||||||
|
public ServerSocketContext(ServiceListener serviceListener) {
|
||||||
|
super();
|
||||||
|
this.serviceListener = serviceListener;
|
||||||
|
}//ServerSocketContext()//
|
||||||
|
}//ServerSocketContext//
|
||||||
2634
Foundation Web Core/src/com/foundation/web/server/SocketContext.java
Normal file
2634
Foundation Web Core/src/com/foundation/web/server/SocketContext.java
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,150 @@
|
|||||||
|
package com.foundation.web.server;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
|
import com.common.debug.Debug;
|
||||||
|
import com.foundation.web.interfaces.IStreamedWebsocketMessage;
|
||||||
|
|
||||||
|
class WebsocketMessageBuffer extends MessageBuffer {
|
||||||
|
/** The streaming message handler which will be set only if the currently sending message is streaming. */
|
||||||
|
private IStreamedWebsocketMessage streamingMessage = null;
|
||||||
|
/** The buffer containing the next part of the streamed message, or the bytes of the whole message (if streamingMessage == null), or null if the buffer is closed or not yet initialized. */
|
||||||
|
private ByteBuffer messagePart = null;
|
||||||
|
/** The message to be sent. Will be null after the message buffer has initialized. */
|
||||||
|
private Object message = null;
|
||||||
|
|
||||||
|
public WebsocketMessageBuffer(Object message) {
|
||||||
|
this.message = message;
|
||||||
|
}//WebsocketMessageBuffer()//
|
||||||
|
public boolean initialize() {
|
||||||
|
if(message != null) {
|
||||||
|
messagePart = stream(message, true);
|
||||||
|
message = null;
|
||||||
|
}//if//
|
||||||
|
|
||||||
|
return super.initialize();
|
||||||
|
}//initialize()//
|
||||||
|
public boolean isClosed() {
|
||||||
|
return super.isClosed() && messagePart == null && streamingMessage == null;
|
||||||
|
}//isClosed()//
|
||||||
|
public void close() {
|
||||||
|
super.close();
|
||||||
|
messagePart = null;
|
||||||
|
streamingMessage = null;
|
||||||
|
}//close()//
|
||||||
|
private ByteBuffer stream(Object next, boolean isLast) {
|
||||||
|
ByteBuffer result = null;
|
||||||
|
byte[] bytes = null;
|
||||||
|
int opCode = 0;
|
||||||
|
int length = 0;
|
||||||
|
|
||||||
|
if(next instanceof String) {
|
||||||
|
try {bytes = ((String) next).getBytes("UTF-8");} catch(Throwable e) {Debug.log(e);}
|
||||||
|
opCode = streamingMessage == null ? 0x01 : 0;
|
||||||
|
length = bytes.length;
|
||||||
|
}//if//
|
||||||
|
else if(next instanceof byte[]) {
|
||||||
|
bytes = (byte[]) next;
|
||||||
|
opCode = streamingMessage == null ? 0x02 : 0;
|
||||||
|
length = bytes.length;
|
||||||
|
}//else if//
|
||||||
|
else if(next instanceof Byte) { //Control Message//
|
||||||
|
opCode = ((Byte) next).byteValue();
|
||||||
|
}//else if//
|
||||||
|
else if(next instanceof IStreamedWebsocketMessage) {
|
||||||
|
//TODO: Ensure that this is not recursive!
|
||||||
|
streamingMessage = (IStreamedWebsocketMessage) next;
|
||||||
|
next = streamingMessage.getNextPart();
|
||||||
|
isLast = !streamingMessage.hasNextPart();
|
||||||
|
|
||||||
|
if(next instanceof String) {
|
||||||
|
try {bytes = ((String) next).getBytes("UTF-8");} catch(Throwable e) {Debug.log(e);}
|
||||||
|
opCode = 0x01; //Text//
|
||||||
|
length = bytes.length;
|
||||||
|
}//if//
|
||||||
|
else if(next instanceof byte[]) {
|
||||||
|
bytes = (byte[]) next;
|
||||||
|
opCode = 0x02; //Binary//
|
||||||
|
length = bytes.length;
|
||||||
|
}//else if//
|
||||||
|
else {
|
||||||
|
throw new RuntimeException("Invalid streaming message part type.");
|
||||||
|
}//if//
|
||||||
|
}//else if//
|
||||||
|
|
||||||
|
result = ByteBuffer.allocate(14 + length);
|
||||||
|
result.put((byte) (isLast ? 0x8 : 0));
|
||||||
|
result.put((byte) opCode);
|
||||||
|
|
||||||
|
//Write the length differently based on how long the content is.//
|
||||||
|
if(length < 126) {
|
||||||
|
result.put((byte) length);
|
||||||
|
}//if//
|
||||||
|
else if(length < 65535) {
|
||||||
|
result.put((byte) 126);
|
||||||
|
result.putShort((short) (length & 0xFFFF));
|
||||||
|
result.putInt(0);
|
||||||
|
}//else if//
|
||||||
|
else {
|
||||||
|
result.put((byte) 127);
|
||||||
|
result.putLong((long) length);
|
||||||
|
}//else//
|
||||||
|
|
||||||
|
//Put the content at the end of the message.//
|
||||||
|
result.put(bytes);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}//stream()//
|
||||||
|
public boolean loadBuffer() {
|
||||||
|
boolean result = true;
|
||||||
|
|
||||||
|
getBuffer().compact();
|
||||||
|
|
||||||
|
//Copy remaining bytes from MessagePart to the buffer.//
|
||||||
|
if(messagePart != null && messagePart.remaining() > 0) {
|
||||||
|
int length = Math.min(getBuffer().remaining(), messagePart.remaining());
|
||||||
|
getBuffer().put(messagePart.array(), messagePart.position(), length);
|
||||||
|
messagePart.position(messagePart.position() + length);
|
||||||
|
|
||||||
|
if(messagePart.remaining() == 0) {
|
||||||
|
messagePart = null;
|
||||||
|
}//if//
|
||||||
|
}//if//
|
||||||
|
|
||||||
|
//Load a new message part if streaming and copy as many bytes from the MessagePart into the buffer as possible.//
|
||||||
|
if(messagePart == null && streamingMessage != null) {
|
||||||
|
Object next = null;
|
||||||
|
boolean isLastPart = true;
|
||||||
|
|
||||||
|
next = streamingMessage.getNextPart();
|
||||||
|
isLastPart = !streamingMessage.hasNextPart();
|
||||||
|
|
||||||
|
//Ensure that we got a string or byte array.//
|
||||||
|
if(!(next instanceof String || next instanceof byte[])) {
|
||||||
|
throw new RuntimeException("Invalid streaming message part type.");
|
||||||
|
}//if//
|
||||||
|
|
||||||
|
messagePart = stream(next, isLastPart);
|
||||||
|
|
||||||
|
if(isLastPart) {
|
||||||
|
streamingMessage = null;
|
||||||
|
}//if//
|
||||||
|
|
||||||
|
int length = Math.min(getBuffer().remaining(), messagePart.remaining());
|
||||||
|
getBuffer().put(messagePart.array(), messagePart.position(), length);
|
||||||
|
messagePart.position(messagePart.position() + length);
|
||||||
|
|
||||||
|
if(messagePart.remaining() == 0) {
|
||||||
|
messagePart = null;
|
||||||
|
}//if//
|
||||||
|
}//if//
|
||||||
|
|
||||||
|
//Close the message buffer if we were unable to add any bytes and there is nothing left to send.//
|
||||||
|
if(getBuffer().remaining() == 0) {
|
||||||
|
close();
|
||||||
|
result = false;
|
||||||
|
}//if//
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}//loadBuffer()//
|
||||||
|
}//WebsocketMessageBuffer//
|
||||||
Reference in New Issue
Block a user