Skip to content

Commit b6d0a83

Browse files
sippykupTooTallNate
authored andcommitted
Fix to asynch I/O
1 parent eaba3cf commit b6d0a83

File tree

3 files changed

+136
-33
lines changed

3 files changed

+136
-33
lines changed

src/net/tootallnate/websocket/WebSocket.java

Lines changed: 65 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,12 @@
33
import java.io.IOException;
44
import java.nio.ByteBuffer;
55
import java.nio.channels.NotYetConnectedException;
6+
import java.nio.channels.SelectionKey;
7+
import java.nio.channels.Selector;
68
import java.nio.channels.SocketChannel;
79
import java.nio.charset.Charset;
810
import java.security.NoSuchAlgorithmException;
11+
import java.util.concurrent.BlockingQueue;
912

1013
/**
1114
* Represents one end (client or server) of a single WebSocket connection.
@@ -76,6 +79,15 @@ public final class WebSocket {
7679
* The bytes that make up the current text frame being read.
7780
*/
7881
private ByteBuffer currentFrame;
82+
/**
83+
* Queue of buffers that need to be sent to the client.
84+
*/
85+
private BlockingQueue<ByteBuffer> bufferQueue;
86+
/**
87+
* Lock object to ensure that data is sent from the bufferQueue in
88+
* the proper order
89+
*/
90+
private Object bufferQueueMutex = new Object();
7991

8092

8193
// CONSTRUCTOR /////////////////////////////////////////////////////////////
@@ -84,11 +96,16 @@ public final class WebSocket {
8496
* @param socketChannel The <tt>SocketChannel</tt> instance to read and
8597
* write to. The channel should already be registered
8698
* with a Selector before construction of this object.
99+
* @param bufferQueue The Queue that we should use to buffer data that
100+
* hasn't been sent to the client yet.
87101
* @param listener The {@link WebSocketListener} to notify of events when
88102
* they occur.
89103
*/
90-
WebSocket(SocketChannel socketChannel, WebSocketListener listener) {
104+
WebSocket(SocketChannel socketChannel, BlockingQueue<ByteBuffer> bufferQueue,
105+
WebSocketListener listener)
106+
{
91107
this.socketChannel = socketChannel;
108+
this.bufferQueue = bufferQueue;
92109
this.handshakeComplete = false;
93110
this.remoteHandshake = this.currentFrame = null;
94111
this.buffer = ByteBuffer.allocate(1);
@@ -133,7 +150,11 @@ public void close() throws IOException {
133150
this.wsl.onClose(this);
134151
}
135152

136-
public void send(String text) throws IOException {
153+
/**
154+
* @return True if all of the text was sent to the client by this thread.
155+
* False if some of the text had to be buffered to be sent later.
156+
*/
157+
public boolean send(String text) throws IOException {
137158
if (!this.handshakeComplete) throw new NotYetConnectedException();
138159
if (text == null) throw new NullPointerException("Cannot send 'null' data to a WebSocket.");
139160

@@ -143,12 +164,50 @@ public void send(String text) throws IOException {
143164
b.put(START_OF_FRAME);
144165
b.put(textBytes);
145166
b.put(END_OF_FRAME);
167+
b.rewind();
146168

147-
// Write the ByteBuffer to the socket
148-
b.rewind();
149-
this.socketChannel.write(b);
169+
// See if we have any backlog that needs to be sent first
170+
if (handleWrite()) {
171+
// Write the ByteBuffer to the socket
172+
this.socketChannel.write(b);
173+
}
174+
175+
// If we didn't get it all sent, add it to the buffer of buffers
176+
if (b.remaining() > 0) {
177+
if (!this.bufferQueue.offer(b)) {
178+
throw new IOException("Buffers are full, message could not be sent to" +
179+
this.socketChannel.socket().getRemoteSocketAddress());
180+
}
181+
return false;
182+
}
183+
184+
return true;
150185
}
151186

187+
boolean hasBufferedData() {
188+
return !this.bufferQueue.isEmpty();
189+
}
190+
191+
/**
192+
* @return True if all data has been sent to the client, false if there
193+
* is still some buffered.
194+
*/
195+
boolean handleWrite() throws IOException {
196+
synchronized (this.bufferQueueMutex) {
197+
ByteBuffer buffer = this.bufferQueue.peek();
198+
while (buffer != null) {
199+
this.socketChannel.write(buffer);
200+
if (buffer.remaining() > 0) {
201+
return false; // Didn't finish this buffer. There's more to send.
202+
} else {
203+
this.bufferQueue.poll(); // Buffer finished. Remove it.
204+
buffer = this.bufferQueue.peek();
205+
}
206+
}
207+
return true;
208+
}
209+
}
210+
152211
public SocketChannel socketChannel() {
153212
return this.socketChannel;
154213
}
@@ -255,4 +314,5 @@ private void completeHandshake(byte[] handShakeBody) throws IOException, NoSuchA
255314
close();
256315
}
257316
}
317+
258318
}

src/net/tootallnate/websocket/WebSocketClient.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import java.util.Iterator;
1313
import java.util.Random;
1414
import java.util.Set;
15+
import java.util.concurrent.LinkedBlockingQueue;
1516

1617
import net.tootallnate.websocket.WebSocketListener.Draft;
1718

@@ -119,7 +120,7 @@ public void run() {
119120

120121
Selector selector = Selector.open();
121122

122-
this.conn = new WebSocket(client, this);
123+
this.conn = new WebSocket(client, new LinkedBlockingQueue<ByteBuffer>(), this);
123124
client.register(selector, client.validOps());
124125

125126
// Continuous loop that is only supposed to end when close is called

src/net/tootallnate/websocket/WebSocketServer.java

Lines changed: 69 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@
1111
import java.util.Iterator;
1212
import java.util.Properties;
1313
import java.util.Set;
14+
import java.util.concurrent.BlockingQueue;
1415
import java.util.concurrent.CopyOnWriteArraySet;
16+
import java.util.concurrent.LinkedBlockingQueue;
1517

1618
/**
1719
* <tt>WebSocketServer</tt> is an abstract class that only takes care of the
@@ -167,6 +169,18 @@ public Draft getDraft() {
167169
return draft;
168170
}
169171

172+
/**
173+
* @return A BlockingQueue that should be used by a WebSocket
174+
* to hold data that is waiting to be sent to the client.
175+
* The default implementation returns an unbounded
176+
* LinkedBlockingQueue, but you may choose to override
177+
* this to return a bounded queue to protect against
178+
* running out of memory.
179+
*/
180+
protected BlockingQueue<ByteBuffer> newBufferQueue() {
181+
return new LinkedBlockingQueue<ByteBuffer>();
182+
}
183+
170184

171185
// Runnable IMPLEMENTATION /////////////////////////////////////////////////
172186
public void run() {
@@ -179,33 +193,61 @@ public void run() {
179193
server.register(selector, server.validOps());
180194

181195
while(true) {
182-
selector.select();
183-
Set<SelectionKey> keys = selector.selectedKeys();
184-
Iterator<SelectionKey> i = keys.iterator();
185-
186-
while(i.hasNext()) {
187-
SelectionKey key = i.next();
188-
189-
// Remove the current key
190-
i.remove();
191-
192-
// if isAccetable == true
193-
// then a client required a connection
194-
if (key.isAcceptable()) {
195-
SocketChannel client = server.accept();
196-
client.configureBlocking(false);
197-
WebSocket c = new WebSocket(client, this);
198-
client.register(selector, SelectionKey.OP_READ, c);
199-
}
200-
201-
// if isReadable == true
202-
// then the server is ready to read
203-
if (key.isReadable()) {
204-
WebSocket conn = (WebSocket)key.attachment();
205-
conn.handleRead();
206-
}
207-
}
208-
}
196+
try {
197+
selector.select(100L);
198+
Set<SelectionKey> keys = selector.selectedKeys();
199+
Iterator<SelectionKey> i = keys.iterator();
200+
201+
while(i.hasNext()) {
202+
SelectionKey key = i.next();
203+
204+
// Remove the current key
205+
i.remove();
206+
207+
// if isAcceptable == true
208+
// then a client required a connection
209+
if (key.isAcceptable()) {
210+
SocketChannel client = server.accept();
211+
client.configureBlocking(false);
212+
WebSocket c = new WebSocket(client, newBufferQueue(), this);
213+
client.register(selector, SelectionKey.OP_READ, c);
214+
}
215+
216+
// if isReadable == true
217+
// then the server is ready to read
218+
if (key.isReadable()) {
219+
WebSocket conn = (WebSocket)key.attachment();
220+
conn.handleRead();
221+
}
222+
223+
// if isWritable == true
224+
// then we need to send the rest of the data to the client
225+
if (key.isWritable()) {
226+
WebSocket conn = (WebSocket)key.attachment();
227+
if (conn.handleWrite()) {
228+
conn.socketChannel().register(selector,
229+
SelectionKey.OP_READ, conn);
230+
}
231+
}
232+
}
233+
234+
for (WebSocket conn : this.connections) {
235+
// We have to do this check here, and not in the thread that
236+
// adds the buffered data to the WebSocket, because the
237+
// Selector is not thread-safe, and can only be accessed
238+
// by this thread.
239+
if (conn.hasBufferedData()) {
240+
conn.socketChannel().register(selector,
241+
SelectionKey.OP_READ | SelectionKey.OP_WRITE, conn);
242+
}
243+
}
244+
245+
} catch (IOException e) {
246+
e.printStackTrace();
247+
} catch (RuntimeException e) {
248+
e.printStackTrace();
249+
}
250+
}
209251
} catch (IOException ex) {
210252
ex.printStackTrace();
211253
} catch (NoSuchAlgorithmException e) {

0 commit comments

Comments
 (0)