Crossfire JXClient, Trunk
ClientSocket.java
Go to the documentation of this file.
1 /*
2  * This file is part of JXClient, the Fullscreen Java Crossfire Client.
3  *
4  * JXClient is free software; you can redistribute it and/or modify
5  * it under the terms of the GNU General Public License as published by
6  * the Free Software Foundation; either version 2 of the License, or
7  * (at your option) any later version.
8  *
9  * JXClient is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12  * GNU General Public License for more details.
13  *
14  * You should have received a copy of the GNU General Public License
15  * along with JXClient; if not, write to the Free Software
16  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
17  *
18  * Copyright (C) 2005-2008 Yann Chachkoff
19  * Copyright (C) 2006-2017,2019-2023 Andreas Kirschbaum
20  * Copyright (C) 2010-2012,2014-2018,2020-2023 Nicolas Weeger
21  */
22 
23 package com.realtime.crossfire.jxclient.server.socket;
24 
28 import java.io.EOFException;
29 import java.io.IOException;
30 import java.net.InetSocketAddress;
31 import java.net.SocketAddress;
32 import java.net.SocketException;
33 import java.nio.Buffer;
34 import java.nio.BufferOverflowException;
35 import java.nio.ByteBuffer;
36 import java.nio.ByteOrder;
37 import java.nio.channels.SelectableChannel;
38 import java.nio.channels.SelectionKey;
39 import java.nio.channels.Selector;
40 import java.nio.channels.SocketChannel;
41 import java.nio.channels.UnresolvedAddressException;
42 import java.util.Collection;
43 import org.jetbrains.annotations.NotNull;
44 import org.jetbrains.annotations.Nullable;
45 
55 public class ClientSocket {
56 
60  private static final int MAXIMUM_PACKET_SIZE = 65536;
61 
65  @NotNull
67 
72  @Nullable
73  private final DebugWriter debugProtocol;
74 
78  @NotNull
80 
84  @NotNull
85  private final Selector selector;
86 
91  @NotNull
92  private final Object syncConnect = new Object();
93 
98  private boolean reconnect = true;
99 
103  @NotNull
104  private String reconnectReason = "disconnect";
105 
109  private boolean reconnectIsError;
110 
114  @Nullable
115  private String host;
116 
120  private int port;
121 
125  private boolean disconnectPending;
126 
130  private final byte @NotNull [] packetHeader = new byte[2];
131 
135  @Nullable
136  private SelectableChannel selectableChannel;
137 
142  @Nullable
143  private SelectionKey selectionKey;
144 
148  private int interestOps;
149 
153  private final byte @NotNull [] inputBuf = new byte[2+MAXIMUM_PACKET_SIZE];
154 
158  @NotNull
159  private final ByteBuffer inputBuffer = ByteBuffer.wrap(inputBuf);
160 
166  private int inputLen = -1;
167 
172  @NotNull
173  private final Object syncOutput = new Object();
174 
178  @NotNull
179  private final ByteBuffer outputBuffer = ByteBuffer.allocate(2+MAXIMUM_PACKET_SIZE);
180 
185  @Nullable
186  private SocketChannel socketChannel;
187 
191  private boolean isConnected;
192 
196  @NotNull
197  private final Thread thread = new Thread(this::process, "JXClient:ClientSocket");
198 
206  public ClientSocket(@NotNull final GuiStateManager guiStateManager, @Nullable final DebugWriter debugProtocol) throws IOException {
207  this.guiStateManager = guiStateManager;
208  this.debugProtocol = debugProtocol;
209  selector = Selector.open();
210  }
211 
215  public void start() {
216  if (debugProtocol != null) {
217  debugProtocol.debugProtocolWrite("socket:start");
218  }
219  thread.start();
220  }
221 
226  public void stop() throws InterruptedException {
227  if (debugProtocol != null) {
228  debugProtocol.debugProtocolWrite("socket:stop");
229  }
230  thread.interrupt();
231  try {
232  selector.close();
233  } catch (final IOException ex) {
234  if (debugProtocol != null) {
235  debugProtocol.debugProtocolWrite("close failed: "+ex.getMessage());
236  }
237  }
238  thread.join();
239  if (debugProtocol != null) {
240  debugProtocol.debugProtocolWrite("socket:stopped");
241  }
242  }
243 
248  public void addClientSocketListener(@NotNull final ClientSocketListener clientSocketListener) {
249  clientSocketListeners.add(clientSocketListener);
250  }
251 
256  public void removeClientSocketListener(@NotNull final ClientSocketListener clientSocketListener) {
257  clientSocketListeners.remove(clientSocketListener);
258  }
259 
265  public void connect(@NotNull final String host, final int port) {
266  if (debugProtocol != null) {
267  debugProtocol.debugProtocolWrite("socket:connect "+host+":"+port);
268  }
269  synchronized (syncConnect) {
270  if (this.host == null || this.port == 0 || !this.host.equals(host) || this.port != port) {
271  reconnect = true;
272  reconnectReason = "connect";
273  reconnectIsError = false;
274  this.host = host;
275  this.port = port;
276  selector.wakeup();
277  }
278  }
279  }
280 
286  public void disconnect(@NotNull final String reason, final boolean isError) {
287  if (debugProtocol != null) {
288  debugProtocol.debugProtocolWrite("socket:disconnect: "+reason+(isError ? " [unexpected]" : ""));
289  }
290  synchronized (syncConnect) {
291  if (host != null || port != 0) {
292  reconnect = true;
293  reconnectReason = reason;
294  reconnectIsError = isError;
295  host = null;
296  port = 0;
297  selector.wakeup();
298  }
299  }
300  }
301 
306  private void process() {
307  while (!thread.isInterrupted()) {
308  try {
309  doReconnect();
310  doConnect();
312  doTransceive();
313  } catch (final EOFException ex) {
314  final String tmp = ex.getMessage();
315  final String message = tmp == null ? "EOF" : tmp;
316  if (debugProtocol != null) {
317  debugProtocol.debugProtocolWrite("socket:exception "+message, ex);
318  }
319  processDisconnect(message, false);
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) {
324  debugProtocol.debugProtocolWrite("socket:exception "+message, ex);
325  }
326  processDisconnect(message, true);
327  }
328  }
329  }
330 
335  private void doConnect() throws IOException {
336  final boolean notifyConnected;
337  synchronized (syncOutput) {
338  if (isConnected || socketChannel == null) {
339  notifyConnected = false;
340  } else {
341  isConnected = socketChannel.finishConnect();
342  if (isConnected) {
343  interestOps = SelectionKey.OP_READ;
345  notifyConnected = true;
346  } else {
347  notifyConnected = false;
348  }
349  }
350  }
351  if (notifyConnected) {
352  for (ClientSocketListener clientSocketListener : clientSocketListeners) {
353  clientSocketListener.connected();
354  }
355  }
356  }
357 
362  private void doReconnect() throws IOException {
363  assert Thread.currentThread() == thread;
364 
365  final boolean doReconnect;
366  final boolean doDisconnect;
367  @Nullable final String disconnectReason;
368  final boolean disconnectIsError;
369  synchronized (syncConnect) {
370  if (reconnect) {
371  reconnect = false;
372  if (host != null && port != 0) {
373  doReconnect = true;
374  doDisconnect = false;
375  disconnectReason = "reconnect to "+host+":"+port;
376  disconnectIsError = false;
377  } else {
378  doReconnect = false;
379  doDisconnect = true;
380  disconnectReason = reconnectReason;
381  disconnectIsError = reconnectIsError;
382  }
383  } else {
384  doReconnect = false;
385  doDisconnect = false;
386  disconnectReason = null;
387  disconnectIsError = false;
388  }
389  }
390  if (doReconnect) {
391  assert disconnectReason != null;
392  processDisconnect(disconnectReason, disconnectIsError);
393  final String connectHost;
394  final int connectPort;
395  synchronized (syncConnect) {
396  disconnectPending = true;
397  connectHost = host;
398  connectPort = port;
399  }
400  if (connectHost != null) {
401  processConnect(connectHost, connectPort);
402  }
403  }
404  if (doDisconnect) {
405  processDisconnect(disconnectReason, disconnectIsError);
406  }
407  }
408 
413  private void doTransceive() throws IOException {
414  selector.select();
415  if (Thread.currentThread().isInterrupted()) {
416  return;
417  }
418  final Collection<SelectionKey> selectedKeys = selector.selectedKeys();
419  final boolean process;
420  synchronized (syncOutput) {
421  process = selectedKeys.remove(selectionKey) && isConnected;
422  }
423  if (process) {
424  processRead();
425  processWrite();
426  }
427  assert selectedKeys.isEmpty();
428  }
429 
436  private void processConnect(@NotNull final String host, final int port) throws IOException {
437  assert Thread.currentThread() == thread;
438 
439  if (debugProtocol != null) {
440  debugProtocol.debugProtocolWrite("socket:connecting to "+host+":"+port);
441  }
442  for (ClientSocketListener clientSocketListener : clientSocketListeners) {
443  clientSocketListener.connecting();
444  }
445 
446  final SocketAddress socketAddress = new InetSocketAddress(host, port);
447  synchronized (syncOutput) {
448  ((Buffer)outputBuffer).clear();
449  ((Buffer)inputBuffer).clear();
450  selectionKey = null;
451  try {
452  socketChannel = SocketChannel.open();
453  selectableChannel = socketChannel.configureBlocking(false);
454  try {
455  isConnected = socketChannel.connect(socketAddress);
456  } catch (final UnresolvedAddressException ex) {
457  throw new IOException("Cannot resolve address: "+socketAddress, ex);
458  } catch (final IllegalArgumentException ex) {
459  throw new IOException(ex.getMessage(), ex);
460  }
461  try {
462  socketChannel.socket().setTcpNoDelay(true);
463  } catch (final SocketException ex) {
464  if (debugProtocol != null) {
465  debugProtocol.debugProtocolWrite("socket:cannot set TCP_NODELAY option: "+ex.getMessage());
466  }
467  }
468  interestOps = SelectionKey.OP_CONNECT;
470  } finally {
471  if (selectionKey == null) {
472  socketChannel = null;
473  selectableChannel = null;
474  isConnected = false;
475  interestOps = 0;
476  }
477  }
478  }
479  }
480 
486  private void processDisconnect(@NotNull final String reason, final boolean isError) {
487  assert Thread.currentThread() == thread;
488 
489  if (debugProtocol != null) {
490  debugProtocol.debugProtocolWrite("socket:disconnecting: "+reason+(isError ? " [unexpected]" : ""));
491  }
492  final boolean notifyListeners;
493  synchronized (syncConnect) {
494  notifyListeners = disconnectPending;
495  disconnectPending = false;
496  }
497  if (notifyListeners) {
498  guiStateManager.disconnecting(reason, isError);
499  }
500 
501  try {
502  synchronized (syncOutput) {
503  if (selectionKey != null) {
504  selectionKey.cancel();
505  selectionKey = null;
506  ((Buffer)outputBuffer).clear();
507 
508  try {
509  if (socketChannel != null) {
510  socketChannel.socket().shutdownOutput();
511  }
512  } catch (final IOException ignored) {
513  // ignore
514  }
515  try {
516  if (socketChannel != null) {
517  socketChannel.close();
518  }
519  } catch (final IOException ignored) {
520  // ignore
521  }
522  socketChannel = null;
523  selectableChannel = null;
524  ((Buffer)inputBuffer).clear();
525  }
526  }
527  } finally {
528  if (notifyListeners) {
530  for (ClientSocketListener clientSocketListener : clientSocketListeners) {
531  clientSocketListener.disconnected(reason);
532  }
533  }
534  }
535  }
536 
541  private void processRead() throws IOException {
542  synchronized (syncOutput) {
543  if (socketChannel == null) {
544  return;
545  }
546 
547  if (socketChannel.read(inputBuffer) == -1) {
548  throw new EOFException("EOF");
549  }
550  }
551  ((Buffer)inputBuffer).flip();
553  inputBuffer.compact();
554  }
555 
559  private void processReadCommand() {
560  while (true) {
561  if (inputLen == -1) {
562  if (inputBuffer.remaining() < 2) {
563  break;
564  }
565 
566  inputLen = (inputBuffer.get()&0xFF)*0x100+(inputBuffer.get()&0xFF);
567  }
568 
569  if (inputBuffer.remaining() < inputLen) {
570  break;
571  }
572 
573  final int start = inputBuffer.position();
574  final int end = start+inputLen;
575  ((Buffer)inputBuffer).position(start+inputLen);
576  inputLen = -1;
577  final ByteBuffer packet = ByteBuffer.wrap(inputBuf, start, end-start);
578  packet.order(ByteOrder.BIG_ENDIAN);
579  try {
580  for (ClientSocketListener clientSocketListener : clientSocketListeners) {
581  clientSocketListener.packetReceived(packet);
582  }
583  } catch (final UnknownCommandException ex) {
584  disconnect(ex.getMessage(), true);
585  break;
586  }
587  }
588  }
589 
598  public void writePacket(final byte @NotNull [] buf, final int len, @NotNull final ClientSocketMonitorCommand monitor) {
599  synchronized (syncOutput) {
600  if (socketChannel == null) {
601  return;
602  }
603 
604  packetHeader[0] = (byte)(len/0x100);
605  packetHeader[1] = (byte)len;
606  try {
607  try {
609  outputBuffer.put(buf, 0, len);
610  } catch (final BufferOverflowException ex) {
611  throw new IOException("buffer overflow", ex);
612  }
613  } catch (final IOException ignored) {
614  try {
615  socketChannel.close();
616  } catch (final IOException ignore) {
617  // ignore
618  }
619  return;
620  }
621  }
622 
623  selector.wakeup();
624  for (ClientSocketListener clientSocketListener : clientSocketListeners) {
625  clientSocketListener.packetSent(monitor);
626  }
627  }
628 
634  private void processWrite() throws IOException {
635  synchronized (syncOutput) {
636  if (outputBuffer.remaining() <= 0) {
637  return;
638  }
639 
640  ((Buffer)outputBuffer).flip();
641  try {
642  if (socketChannel == null) {
643  ((Buffer)outputBuffer).position(outputBuffer.limit());
644  } else {
646  }
647  } finally {
648  outputBuffer.compact();
649  }
650  }
651  }
652 
657  private void updateWriteInterestOps() {
658  synchronized (syncOutput) {
659  final int newInterestOps;
660  //noinspection IfMayBeConditional
661  if (outputBuffer.position() > 0) {
662  newInterestOps = interestOps|SelectionKey.OP_WRITE;
663  } else {
664  newInterestOps = interestOps&~SelectionKey.OP_WRITE;
665  }
666  if (interestOps != newInterestOps) {
667  interestOps = newInterestOps;
669  }
670  }
671  }
672 
678  private void updateInterestOps() {
679  if (debugProtocol != null) {
680  debugProtocol.debugProtocolWrite("socket:set interest ops to "+interestOps);
681  }
682  assert Thread.holdsLock(syncOutput);
683  if (selectionKey != null) {
684  selectionKey.interestOps(interestOps);
685  }
686  }
687 
688 }
com.realtime.crossfire.jxclient.guistate.GuiStateManager.disconnected
void disconnected()
Definition: GuiStateManager.java:222
com.realtime.crossfire.jxclient
com.realtime.crossfire.jxclient.server.socket.ClientSocket.guiStateManager
final GuiStateManager guiStateManager
Definition: ClientSocket.java:66
com.realtime.crossfire.jxclient.server.socket.ClientSocket.start
void start()
Definition: ClientSocket.java:215
com.realtime.crossfire.jxclient.server.socket.ClientSocket.connect
void connect(@NotNull final String host, final int port)
Definition: ClientSocket.java:265
com.realtime.crossfire.jxclient.server.socket.ClientSocket.syncConnect
final Object syncConnect
Definition: ClientSocket.java:92
com.realtime.crossfire.jxclient.server.socket.ClientSocket.doConnect
void doConnect()
Definition: ClientSocket.java:335
com.realtime.crossfire.jxclient.server.socket.ClientSocket.processDisconnect
void processDisconnect(@NotNull final String reason, final boolean isError)
Definition: ClientSocket.java:486
com.realtime.crossfire.jxclient.server.socket.ClientSocket.selector
final Selector selector
Definition: ClientSocket.java:85
com.realtime.crossfire.jxclient.server.socket.ClientSocket.selectableChannel
SelectableChannel selectableChannel
Definition: ClientSocket.java:136
com.realtime.crossfire.jxclient.server.socket.ClientSocket.isConnected
boolean isConnected
Definition: ClientSocket.java:191
com.realtime.crossfire.jxclient.server.socket.ClientSocket.inputLen
int inputLen
Definition: ClientSocket.java:166
com.realtime.crossfire.jxclient.server.socket.ClientSocket.process
void process()
Definition: ClientSocket.java:306
com.realtime.crossfire.jxclient.server.socket.ClientSocketListener
Definition: ClientSocketListener.java:33
com.realtime.crossfire.jxclient.util.EventListenerList2
Definition: EventListenerList2.java:37
com.realtime.crossfire.jxclient.server.socket.ClientSocket.updateWriteInterestOps
void updateWriteInterestOps()
Definition: ClientSocket.java:657
com.realtime.crossfire.jxclient.server.socket.ClientSocket.socketChannel
SocketChannel socketChannel
Definition: ClientSocket.java:186
com.realtime.crossfire.jxclient.server.socket.ClientSocket.syncOutput
final Object syncOutput
Definition: ClientSocket.java:173
com.realtime.crossfire.jxclient.server.socket.ClientSocket.writePacket
void writePacket(final byte @NotNull[] buf, final int len, @NotNull final ClientSocketMonitorCommand monitor)
Definition: ClientSocket.java:598
com.realtime.crossfire.jxclient.server.socket.ClientSocket.port
int port
Definition: ClientSocket.java:120
com.realtime.crossfire.jxclient.guistate.GuiStateManager
Definition: GuiStateManager.java:34
com.realtime.crossfire.jxclient.guistate
Definition: ClientSocketState.java:23
com.realtime.crossfire.jxclient.server.socket.ClientSocket.ClientSocket
ClientSocket(@NotNull final GuiStateManager guiStateManager, @Nullable final DebugWriter debugProtocol)
Definition: ClientSocket.java:206
com.realtime.crossfire.jxclient.server.socket.ClientSocket
Definition: ClientSocket.java:55
com.realtime.crossfire.jxclient.server.socket.ClientSocket.selectionKey
SelectionKey selectionKey
Definition: ClientSocket.java:143
com.realtime.crossfire.jxclient.server.socket.ClientSocket.reconnect
boolean reconnect
Definition: ClientSocket.java:98
com.realtime.crossfire.jxclient.server.socket.ClientSocket.host
String host
Definition: ClientSocket.java:115
com.realtime.crossfire.jxclient.server.socket.ClientSocket.clientSocketListeners
final EventListenerList2< ClientSocketListener > clientSocketListeners
Definition: ClientSocket.java:79
com.realtime.crossfire.jxclient.server.socket.ClientSocket.processReadCommand
void processReadCommand()
Definition: ClientSocket.java:559
com.realtime.crossfire.jxclient.server.socket.ClientSocket.MAXIMUM_PACKET_SIZE
static final int MAXIMUM_PACKET_SIZE
Definition: ClientSocket.java:60
com.realtime.crossfire.jxclient.server.socket.ClientSocket.addClientSocketListener
void addClientSocketListener(@NotNull final ClientSocketListener clientSocketListener)
Definition: ClientSocket.java:248
com.realtime.crossfire.jxclient.server.socket.ClientSocket.reconnectIsError
boolean reconnectIsError
Definition: ClientSocket.java:109
com.realtime.crossfire.jxclient.server.socket.ClientSocket.processWrite
void processWrite()
Definition: ClientSocket.java:634
com.realtime.crossfire.jxclient.server.socket.ClientSocket.disconnectPending
boolean disconnectPending
Definition: ClientSocket.java:125
com.realtime.crossfire.jxclient.util
Definition: Codec.java:23
com.realtime.crossfire.jxclient.server.socket.ClientSocket.debugProtocol
final DebugWriter debugProtocol
Definition: ClientSocket.java:73
com.realtime.crossfire.jxclient.server.socket.ClientSocket.disconnect
void disconnect(@NotNull final String reason, final boolean isError)
Definition: ClientSocket.java:286
com.realtime.crossfire.jxclient.server.socket.ClientSocket.inputBuffer
final ByteBuffer inputBuffer
Definition: ClientSocket.java:159
com.realtime.crossfire.jxclient.guistate.GuiStateManager.disconnecting
void disconnecting(@NotNull final String reason, final boolean isError)
Definition: GuiStateManager.java:211
com.realtime.crossfire
com.realtime.crossfire.jxclient.server.socket.ClientSocket.updateInterestOps
void updateInterestOps()
Definition: ClientSocket.java:678
com.realtime
com
com.realtime.crossfire.jxclient.server.socket.ClientSocket.removeClientSocketListener
void removeClientSocketListener(@NotNull final ClientSocketListener clientSocketListener)
Definition: ClientSocket.java:256
com.realtime.crossfire.jxclient.server.socket.ClientSocket.doTransceive
void doTransceive()
Definition: ClientSocket.java:413
com.realtime.crossfire.jxclient.server.socket.ClientSocket.processConnect
void processConnect(@NotNull final String host, final int port)
Definition: ClientSocket.java:436
com.realtime.crossfire.jxclient.server.socket.ClientSocket.thread
final Thread thread
Definition: ClientSocket.java:197
com.realtime.crossfire.jxclient.server.socket.UnknownCommandException
Definition: UnknownCommandException.java:34
com.realtime.crossfire.jxclient.util.DebugWriter
Definition: DebugWriter.java:36
com.realtime.crossfire.jxclient.server.socket.ClientSocket.outputBuffer
final ByteBuffer outputBuffer
Definition: ClientSocket.java:179
com.realtime.crossfire.jxclient.server.socket.ClientSocket.packetHeader
final byte[] packetHeader
Definition: ClientSocket.java:130
com.realtime.crossfire.jxclient.server.socket.ClientSocket.reconnectReason
String reconnectReason
Definition: ClientSocket.java:104
com.realtime.crossfire.jxclient.server.socket.ClientSocket.doReconnect
void doReconnect()
Definition: ClientSocket.java:362
com.realtime.crossfire.jxclient.server.socket.ClientSocketMonitorCommand
Definition: ClientSocketMonitorCommand.java:8
com.realtime.crossfire.jxclient.server.socket.ClientSocket.inputBuf
final byte[] inputBuf
Definition: ClientSocket.java:153
com.realtime.crossfire.jxclient.server.socket.ClientSocket.stop
void stop()
Definition: ClientSocket.java:226
com.realtime.crossfire.jxclient.server.socket.ClientSocket.interestOps
int interestOps
Definition: ClientSocket.java:148
com.realtime.crossfire.jxclient.server.socket.ClientSocket.processRead
void processRead()
Definition: ClientSocket.java:541
com.realtime.crossfire.jxclient.util.DebugWriter.debugProtocolWrite
void debugProtocolWrite(@NotNull final CharSequence str)
Definition: DebugWriter.java:68