22 package com.realtime.crossfire.jxclient.server.socket;
27 import java.io.EOFException;
28 import java.io.IOException;
29 import java.net.InetSocketAddress;
30 import java.net.SocketAddress;
31 import java.net.SocketException;
32 import java.nio.BufferOverflowException;
33 import java.nio.ByteBuffer;
34 import java.nio.ByteOrder;
35 import java.nio.channels.SelectableChannel;
36 import java.nio.channels.SelectionKey;
37 import java.nio.channels.Selector;
38 import java.nio.channels.SocketChannel;
39 import java.nio.channels.UnresolvedAddressException;
40 import java.util.Collection;
41 import org.jetbrains.annotations.NotNull;
42 import org.jetbrains.annotations.Nullable;
179 private final ByteBuffer
outputBuffer = ByteBuffer.allocate(2+MAXIMUM_PACKET_SIZE);
197 private final Thread
thread =
new Thread(this::process,
"JXClient:ClientSocket");
209 selector = Selector.open();
216 if (debugProtocol != null) {
226 public void stop() throws InterruptedException {
227 if (debugProtocol != null) {
233 }
catch (
final IOException ex) {
234 if (debugProtocol != null) {
239 if (debugProtocol != null) {
249 clientSocketListeners.
add(clientSocketListener);
257 clientSocketListeners.
remove(clientSocketListener);
265 public void connect(@NotNull
final String host,
final int port) {
266 if (debugProtocol != null) {
270 if (this.host == null || this.port == 0 || !this.host.equals(host) || this.port !=
port) {
272 reconnectReason =
"connect";
273 reconnectIsError =
false;
286 public void disconnect(@NotNull
final String reason,
final boolean isError) {
287 if (debugProtocol != null) {
288 debugProtocol.
debugProtocolWrite(
"socket:disconnect: "+reason+(isError ?
" [unexpected]" :
""));
291 if (host != null || port != 0) {
293 reconnectReason = reason;
294 reconnectIsError = isError;
307 while (!thread.isInterrupted()) {
313 }
catch (
final EOFException ex) {
314 final String tmp = ex.getMessage();
315 final String message = tmp == null ?
"EOF" : tmp;
316 if (debugProtocol != null) {
320 }
catch (
final IOException ex) {
321 final String tmp = ex.getMessage();
322 final String message = tmp == null ?
"I/O error" : tmp;
323 if (debugProtocol != null) {
336 final boolean notifyConnected;
338 if (isConnected || socketChannel == null) {
339 notifyConnected =
false;
341 isConnected = socketChannel.finishConnect();
343 interestOps = SelectionKey.OP_READ;
345 notifyConnected =
true;
347 notifyConnected =
false;
351 if (notifyConnected) {
353 clientSocketListener.connected();
363 assert Thread.currentThread() ==
thread;
366 final boolean doDisconnect;
367 @Nullable
final String disconnectReason;
368 final boolean disconnectIsError;
372 if (host != null && port != 0) {
374 doDisconnect =
false;
375 disconnectReason =
"reconnect to "+host+
":"+
port;
376 disconnectIsError =
false;
385 doDisconnect =
false;
386 disconnectReason = null;
387 disconnectIsError =
false;
391 assert disconnectReason != null;
393 final String connectHost;
394 final int connectPort;
396 disconnectPending =
true;
400 if (connectHost != null) {
415 final Collection<SelectionKey> selectedKeys = selector.selectedKeys();
416 if (selectedKeys.remove(selectionKey) &&
isConnected) {
420 assert selectedKeys.isEmpty();
429 private void processConnect(@NotNull
final String host,
final int port)
throws IOException {
430 assert Thread.currentThread() ==
thread;
432 if (debugProtocol != null) {
436 clientSocketListener.connecting();
439 final SocketAddress socketAddress =
new InetSocketAddress(host, port);
441 outputBuffer.clear();
445 socketChannel = SocketChannel.open();
446 selectableChannel = socketChannel.configureBlocking(
false);
448 isConnected = socketChannel.connect(socketAddress);
449 }
catch (
final UnresolvedAddressException ex) {
451 throw new IOException(
"Cannot resolve address: "+socketAddress, ex);
452 }
catch (
final IllegalArgumentException ex) {
453 throw new IOException(ex.getMessage(), ex);
456 socketChannel.socket().setTcpNoDelay(
true);
457 }
catch (
final SocketException ex) {
458 if (debugProtocol != null) {
459 debugProtocol.
debugProtocolWrite(
"socket:cannot set TCP_NODELAY option: "+ex.getMessage());
462 interestOps = SelectionKey.OP_CONNECT;
463 selectionKey = selectableChannel.register(selector, interestOps);
465 if (selectionKey == null) {
466 socketChannel = null;
467 selectableChannel = null;
481 assert Thread.currentThread() ==
thread;
483 if (debugProtocol != null) {
484 debugProtocol.
debugProtocolWrite(
"socket:disconnecting: "+reason+(isError ?
" [unexpected]" :
""));
486 final boolean notifyListeners;
489 disconnectPending =
false;
491 if (notifyListeners) {
494 clientSocketListener.disconnecting(reason, isError);
500 if (selectionKey != null) {
501 selectionKey.cancel();
503 outputBuffer.clear();
506 if (socketChannel != null) {
507 socketChannel.socket().shutdownOutput();
509 }
catch (
final IOException ignored) {
513 if (socketChannel != null) {
514 socketChannel.close();
516 }
catch (
final IOException ignored) {
519 socketChannel = null;
520 selectableChannel = null;
525 if (notifyListeners) {
528 clientSocketListener.disconnected(reason);
540 if (socketChannel == null) {
544 if (socketChannel.read(inputBuffer) == -1) {
545 throw new EOFException(
"EOF");
550 inputBuffer.compact();
558 if (inputLen == -1) {
559 if (inputBuffer.remaining() < 2) {
563 inputLen = (inputBuffer.get()&0xFF)*0x100+(inputBuffer.get()&0xFF);
566 if (inputBuffer.remaining() <
inputLen) {
570 final int start = inputBuffer.position();
572 inputBuffer.position(start+inputLen);
574 final ByteBuffer packet = ByteBuffer.wrap(inputBuf, start, end-start);
575 packet.order(ByteOrder.BIG_ENDIAN);
578 clientSocketListener.packetReceived(packet);
594 public void writePacket(@NotNull
final byte[] buf,
final int len) {
596 if (socketChannel == null) {
600 packetHeader[0] = (byte)(len/0x100);
601 packetHeader[1] = (byte)len;
604 outputBuffer.put(packetHeader);
605 outputBuffer.put(buf, 0, len);
606 }
catch (
final BufferOverflowException ex) {
607 throw new IOException(
"buffer overflow", ex);
609 }
catch (
final IOException ignored) {
612 socketChannel.close();
613 }
catch (
final IOException ignore) {
622 clientSocketListener.packetSent(buf, len);
633 if (outputBuffer.remaining() <= 0) {
639 if (socketChannel == null) {
640 outputBuffer.position(outputBuffer.limit());
642 socketChannel.write(outputBuffer);
645 outputBuffer.compact();
656 final int newInterestOps;
658 if (outputBuffer.position() > 0) {
659 newInterestOps = interestOps|SelectionKey.OP_WRITE;
661 newInterestOps = interestOps&~SelectionKey.OP_WRITE;
663 if (interestOps != newInterestOps) {
664 interestOps = newInterestOps;
675 if (debugProtocol != null) {
678 assert Thread.holdsLock(syncOutput);
679 if (selectionKey != null) {
680 selectionKey.interestOps(interestOps);
boolean isConnected
Whether socketChannel is connected.
void updateWriteInterestOps()
Updates interestOps's OP_WRITE according to whether outputBuffer has pending data.
void processReadCommand()
Parses data from inputBuffer into commands.
GuiStateManager getGuiStateManager()
void start()
Starts operation.
void disconnect(@NotNull final String reason, final boolean isError)
Terminates the connection.
void processConnect(@NotNull final String host, final int port)
Connects the socket.
A list of event listeners.
void stop()
Stops operation.
boolean reconnectIsError
Only valid if reconnect is set.
void doConnect()
Processes pending connect requests.
int interestOps
The currently set interest ops for selectionKey.
final Object syncOutput
Synchronization object for outputBuffer, selectionKey, interestOps, and socketChannel.
void processRead()
Reads data from the socket and parses the data into commands.
void debugProtocolWrite(@NotNull final CharSequence str)
Writes a message to the debug protocol.
void disconnected()
Called after the connection has been closed.
final ByteBuffer inputBuffer
The receive buffer.
void addClientSocketListener(@NotNull final ClientSocketListener clientSocketListener)
Adds a ClientSocketListener to be notified.
Writer debug information to a log file.
SelectionKey selectionKey
The SelectionKey registered to selectableChannel.
void processWrite()
Writes some pending data to the socket.
final Model model
The Model instance that is updated.
final EventListenerList2< ClientSocketListener > clientSocketListeners
The ClientSocketListeners to notify.
void updateInterestOps()
Updates selectionKey's interest ops to match interestOps.
final byte [] packetHeader
A buffer for sending packets.
String reconnectReason
Only valid if reconnect is set.
SelectableChannel selectableChannel
The SelectableChannel of socketChannel.
Combines all model classes that are updated.
SocketChannel socketChannel
The SocketChannel when connected.
boolean disconnectPending
If set, notify listeners.
A socket that processes incoming data.
int port
The port to connect to.
void disconnecting(@NotNull final String reason, final boolean isError)
Called when the connection is being teared down.
An UnknownCommandException is generated whenever an unknown message packet is received from the serve...
void add(@NotNull final T listener)
Adds a listener.
Interface for listeners interested in ClientSocket related events.
void doReconnect()
Processes pending re- or disconnect requests.
void writePacket(@NotNull final byte[] buf, final int len)
Writes a packet.
void processDisconnect(@NotNull final String reason, final boolean isError)
Disconnects the socket.
final Object syncConnect
Synchronization object for reconnect, host, port, and disconnectPending.
ClientSocket(@NotNull final Model model, @Nullable final DebugWriter debugProtocol)
Creates a new instance.
void doTransceive()
Processes pending data to receive of transmit.
void connect(@NotNull final String host, final int port)
Connects to a server.
final Thread thread
The Thread used to operate the socket.
final byte [] inputBuf
The receive buffer.
void removeClientSocketListener(@NotNull final ClientSocketListener clientSocketListener)
Removes a ClientSocketListener to be notified.
boolean reconnect
Set if host or port has changed and thus a reconnect is needed.
final Selector selector
The Selector used for waiting.
final DebugWriter debugProtocol
The appender to write state changes to.
void process()
Reads/writes data from/to the socket.
final ByteBuffer outputBuffer
The output buffer.
void remove(@NotNull final T listener)
Removes a listener.
String host
The host to connect to.
static final int MAXIMUM_PACKET_SIZE
The maximum payload size of a Crossfire protocol packet.