├── fast-install.sh ├── test-fast-fail.sh ├── release.sh ├── .gitignore ├── src ├── main │ ├── java │ │ └── com │ │ │ └── jfastnet │ │ │ ├── ISimpleProcessable.java │ │ │ ├── events │ │ │ ├── Event.java │ │ │ ├── EventVisitor.java │ │ │ ├── DisabledStackedMessagesEvent.java │ │ │ ├── RequestedMessageNotInLogEvent.java │ │ │ └── EventLog.java │ │ │ ├── time │ │ │ ├── ITimeProvider.java │ │ │ └── SystemTimeProvider.java │ │ │ ├── messages │ │ │ ├── IAckMessage.java │ │ │ ├── IBatchable.java │ │ │ ├── IInstantProcessable.java │ │ │ ├── IDontFrame.java │ │ │ ├── LeaveRequest.java │ │ │ ├── features │ │ │ │ ├── MessageFeature.java │ │ │ │ ├── TimestampFeature.java │ │ │ │ ├── ChecksumFeature.java │ │ │ │ └── MessageFeatures.java │ │ │ ├── LeaveConfirmationResponse.java │ │ │ ├── IInstantOnSendProcessable.java │ │ │ ├── IInstantServerProcessable.java │ │ │ ├── GenericMessage.java │ │ │ ├── IsReadyMessage.java │ │ │ ├── comparators │ │ │ │ ├── MessageIdComparator.java │ │ │ │ └── MessageExecutionPriorityComparator.java │ │ │ ├── StackAckMessage.java │ │ │ ├── SequenceKeepAlive.java │ │ │ ├── AckMessage.java │ │ │ ├── IOrderedMessage.java │ │ │ ├── StackedMessage.java │ │ │ ├── ClientTimerSyncMessage.java │ │ │ ├── CompressedMessage.java │ │ │ ├── ConnectRequest.java │ │ │ ├── ConnectResponse.java │ │ │ ├── RequestSeqIdsMessage.java │ │ │ ├── TimerSyncMessage.java │ │ │ └── Message.java │ │ │ ├── IMessageReceiver.java │ │ │ ├── IStartStop.java │ │ │ ├── config │ │ │ └── SerialiserConfig.java │ │ │ ├── processors │ │ │ ├── IMessageSenderPreProcessor.java │ │ │ ├── IMessageSenderPostProcessor.java │ │ │ ├── IMessageReceiverPostProcessor.java │ │ │ ├── IMessageReceiverPreProcessor.java │ │ │ ├── DiscardWrongChecksumMessagesHandler.java │ │ │ ├── AddChecksumProcessor.java │ │ │ ├── AbstractMessageProcessor.java │ │ │ ├── DiscardMessagesHandler.java │ │ │ ├── MessageLogProcessor.java │ │ │ └── StackedMessageProcessor.java │ │ │ ├── IPeerController.java │ │ │ ├── ConfigStateContainer.java │ │ │ ├── IServerHooks.java │ │ │ ├── IPeer.java │ │ │ ├── util │ │ │ ├── FifoMap.java │ │ │ ├── SizeLimitedSet.java │ │ │ ├── SizeLimitedMap.java │ │ │ ├── SizeLimitedList.java │ │ │ ├── NullsafeHashMap.java │ │ │ └── ConcurrentSizeLimitedMap.java │ │ │ ├── serialiser │ │ │ ├── ISerialiser.java │ │ │ └── KryoSerialiser.java │ │ │ ├── exceptions │ │ │ └── DeserialiseException.java │ │ │ ├── MessageKey.java │ │ │ ├── state │ │ │ ├── ClientState.java │ │ │ ├── NetworkQuality.java │ │ │ └── ClientStates.java │ │ │ ├── idprovider │ │ │ ├── SimpleIdProvider.java │ │ │ ├── IIdProvider.java │ │ │ ├── ReliableModeIdProvider.java │ │ │ └── ClientIdReliableModeIdProvider.java │ │ │ ├── IMessageSender.java │ │ │ ├── peers │ │ │ ├── netty │ │ │ │ ├── FutureGenericFutureListener.java │ │ │ │ └── KryoNettyPeer.java │ │ │ ├── CongestionControl.java │ │ │ └── javanet │ │ │ │ └── JavaNetPeer.java │ │ │ ├── NetStats.java │ │ │ ├── MessageLog.java │ │ │ ├── Client.java │ │ │ ├── State.java │ │ │ └── Config.java │ └── resources │ │ └── log4j.properties └── test │ └── java │ └── com │ └── jfastnet │ ├── util │ └── ConcurrentSizeLimitedMapTest.java │ ├── examples │ └── HelloWorld.java │ ├── messages │ ├── MessageTest.java │ ├── CompressedMessageTest.java │ ├── features │ │ └── ChecksumFeatureTest.java │ └── MessagePartTest.java │ └── MessageLogTest.java ├── CHANGES.md └── README.md /fast-install.sh: -------------------------------------------------------------------------------- 1 | mvn install -DskipTests -Dgpg.skip 2 | -------------------------------------------------------------------------------- /test-fast-fail.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Switch to script directory 3 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 4 | cd ${DIR} 5 | 6 | mvn clean test -DskipAfterFailureCount=false -------------------------------------------------------------------------------- /release.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Switch to script directory 3 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 4 | cd "${DIR}" 5 | 6 | set +x # Disable debugging 7 | set -u # Stop on uninitialised variable 8 | set -e # Stop on first non-null return value 9 | 10 | echo " * Perform unit tests..." 11 | mvn test 12 | 13 | echo " * Prepare the release" 14 | mvn --batch-mode release:prepare -Darguments="-DskipTests" 15 | 16 | echo " * Prepare the release" 17 | mvn release:perform -Darguments="-DskipTests" 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Maven template 2 | target/ 3 | pom.xml.tag 4 | pom.xml.releaseBackup 5 | pom.xml.versionsBackup 6 | pom.xml.next 7 | release.properties 8 | dependency-reduced-pom.xml 9 | buildNumber.properties 10 | .mvn/timing.properties 11 | ### Java template 12 | *.class 13 | 14 | # Mobile Tools for Java (J2ME) 15 | .mtj.tmp/ 16 | 17 | # Package Files # 18 | *.jar 19 | *.war 20 | *.ear 21 | 22 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 23 | hs_err_pid* 24 | 25 | .idea/ 26 | *.iml -------------------------------------------------------------------------------- /src/main/java/com/jfastnet/ISimpleProcessable.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2018 Klaus Pfeiffer - klaus@allpiper.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | ******************************************************************************/ 16 | 17 | package com.jfastnet; 18 | 19 | /** @author Klaus Pfeiffer - klaus@allpiper.com */ 20 | public interface ISimpleProcessable { 21 | void process(); 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/com/jfastnet/events/Event.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2018 Klaus Pfeiffer - klaus@allpiper.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | ******************************************************************************/ 16 | 17 | package com.jfastnet.events; 18 | 19 | /** @author Klaus Pfeiffer - klaus@allpiper.com */ 20 | public interface Event { 21 | void accept(EventVisitor visitor); 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/com/jfastnet/time/ITimeProvider.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2018 Klaus Pfeiffer - klaus@allpiper.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | ******************************************************************************/ 16 | 17 | package com.jfastnet.time; 18 | 19 | /** @author Klaus Pfeiffer - klaus@allpiper.com */ 20 | public interface ITimeProvider { 21 | 22 | /** @return current time in ms. */ 23 | long get(); 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/jfastnet/messages/IAckMessage.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2018 Klaus Pfeiffer - klaus@allpiper.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | ******************************************************************************/ 16 | 17 | package com.jfastnet.messages; 18 | 19 | /** @author Klaus Pfeiffer - klaus@allpiper.com */ 20 | public interface IAckMessage { 21 | 22 | /** Id that is acknowledged. */ 23 | long getAckId(); 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/jfastnet/messages/IBatchable.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2018 Klaus Pfeiffer - klaus@allpiper.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | ******************************************************************************/ 16 | 17 | package com.jfastnet.messages; 18 | 19 | import java.util.Collection; 20 | 21 | /** @author Klaus Pfeiffer - klaus@allpiper.com */ 22 | public interface IBatchable { 23 | void set(Collection batch); 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/jfastnet/IMessageReceiver.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2018 Klaus Pfeiffer - klaus@allpiper.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | ******************************************************************************/ 16 | 17 | package com.jfastnet; 18 | 19 | import com.jfastnet.messages.Message; 20 | 21 | /** @author Klaus Pfeiffer - klaus@allpiper.com */ 22 | public interface IMessageReceiver { 23 | 24 | /** Receive message. */ 25 | void receive(Message message); 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/jfastnet/messages/IInstantProcessable.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2018 Klaus Pfeiffer - klaus@allpiper.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | ******************************************************************************/ 16 | 17 | package com.jfastnet.messages; 18 | 19 | /** Message will not be forwarded to the external receiver. Instead it will 20 | * be immediately processed. 21 | * @author Klaus Pfeiffer - klaus@allpiper.com */ 22 | public interface IInstantProcessable {} 23 | -------------------------------------------------------------------------------- /src/main/java/com/jfastnet/messages/IDontFrame.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2018 Klaus Pfeiffer - klaus@allpiper.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | ******************************************************************************/ 16 | 17 | package com.jfastnet.messages; 18 | 19 | /** The message implementing this marker interface will not be processed 20 | * within the boundaries of the lockstepping mechanism. 21 | * @author Klaus Pfeiffer - klaus@allpiper.com */ 22 | public interface IDontFrame {} 23 | -------------------------------------------------------------------------------- /src/main/java/com/jfastnet/IStartStop.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2018 Klaus Pfeiffer - klaus@allpiper.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | ******************************************************************************/ 16 | 17 | package com.jfastnet; 18 | 19 | /** @author Klaus Pfeiffer - klaus@allpiper.com */ 20 | public interface IStartStop { 21 | 22 | /** @return true if start was successful, false otherwise. */ 23 | boolean start(); 24 | 25 | /** Stop peer and free resources. */ 26 | void stop(); 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/jfastnet/config/SerialiserConfig.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2018 Klaus Pfeiffer - klaus@allpiper.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | ******************************************************************************/ 16 | 17 | package com.jfastnet.config; 18 | 19 | /** @author Klaus Pfeiffer - klaus@allpiper.com */ 20 | public class SerialiserConfig { 21 | 22 | /** Use basic Java's In-/Deflater compression. Can only be used when de-/ 23 | * serialising with streams. */ 24 | public boolean useBasicCompression; 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/jfastnet/events/EventVisitor.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2018 Klaus Pfeiffer - klaus@allpiper.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | ******************************************************************************/ 16 | 17 | package com.jfastnet.events; 18 | 19 | /** @author Klaus Pfeiffer - klaus@allpiper.com */ 20 | public interface EventVisitor { 21 | default void visit(Event event) {} 22 | default void visit(DisabledStackedMessagesEvent event) {} 23 | default void visit(RequestedMessageNotInLogEvent event) {} 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/jfastnet/processors/IMessageSenderPreProcessor.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2018 Klaus Pfeiffer - klaus@allpiper.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | ******************************************************************************/ 16 | 17 | package com.jfastnet.processors; 18 | 19 | import com.jfastnet.messages.Message; 20 | 21 | /** @author Klaus Pfeiffer - klaus@allpiper.com */ 22 | public interface IMessageSenderPreProcessor { 23 | /** Called before sending the message. */ 24 | Message beforeSend(Message message); 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/jfastnet/time/SystemTimeProvider.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2018 Klaus Pfeiffer - klaus@allpiper.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | ******************************************************************************/ 16 | 17 | package com.jfastnet.time; 18 | 19 | /** @author Klaus Pfeiffer - klaus@allpiper.com */ 20 | public class SystemTimeProvider implements ITimeProvider { 21 | 22 | @Override 23 | public long get() { 24 | // return System.nanoTime() / 1000000; 25 | return System.currentTimeMillis(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/jfastnet/processors/IMessageSenderPostProcessor.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2018 Klaus Pfeiffer - klaus@allpiper.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | ******************************************************************************/ 16 | 17 | package com.jfastnet.processors; 18 | 19 | import com.jfastnet.messages.Message; 20 | 21 | /** @author Klaus Pfeiffer - klaus@allpiper.com */ 22 | public interface IMessageSenderPostProcessor { 23 | /** Called after the message was successfully sent. */ 24 | Message afterSend(Message message); 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/jfastnet/IPeerController.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2018 Klaus Pfeiffer - klaus@allpiper.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | ******************************************************************************/ 16 | 17 | package com.jfastnet; 18 | 19 | /** Controls a peer interface by behaving like a peer, client or server. Every 20 | * peer controller can send and receive messages. 21 | * @author Klaus Pfeiffer - klaus@allpiper.com */ 22 | public interface IPeerController extends IMessageSender, IMessageReceiver, ISimpleProcessable, IStartStop {} 23 | -------------------------------------------------------------------------------- /src/main/java/com/jfastnet/ConfigStateContainer.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2018 Klaus Pfeiffer - klaus@allpiper.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | ******************************************************************************/ 16 | 17 | package com.jfastnet; 18 | 19 | /** @author Klaus Pfeiffer - klaus@allpiper.com */ 20 | public class ConfigStateContainer { 21 | 22 | public final Config config; 23 | public final State state; 24 | 25 | public ConfigStateContainer(Config config, State state) { 26 | this.config = config; 27 | this.state = state; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/jfastnet/messages/LeaveRequest.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2018 Klaus Pfeiffer - klaus@allpiper.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | ******************************************************************************/ 16 | 17 | package com.jfastnet.messages; 18 | 19 | /** Sent from the client to the server. 20 | * @author Klaus Pfeiffer - klaus@allpiper.com */ 21 | public class LeaveRequest extends Message implements IDontFrame { 22 | 23 | @Override 24 | public ReliableMode getReliableMode() { 25 | return ReliableMode.UNRELIABLE; 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/jfastnet/messages/features/MessageFeature.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2018 Klaus Pfeiffer - klaus@allpiper.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | ******************************************************************************/ 16 | 17 | package com.jfastnet.messages.features; 18 | 19 | import com.jfastnet.Config; 20 | 21 | import java.io.Serializable; 22 | 23 | /** @author Klaus Pfeiffer - klaus@allpiper.com */ 24 | public interface MessageFeature extends Serializable { 25 | /** Provide the config to the feature. */ 26 | default void resolveConfig(Config config) {} 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/jfastnet/messages/LeaveConfirmationResponse.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2018 Klaus Pfeiffer - klaus@allpiper.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | ******************************************************************************/ 16 | 17 | package com.jfastnet.messages; 18 | 19 | /** Sent from the server to the client. 20 | * @author Klaus Pfeiffer - klaus@allpiper.com */ 21 | public class LeaveConfirmationResponse extends Message implements IDontFrame { 22 | 23 | @Override 24 | public ReliableMode getReliableMode() { 25 | return ReliableMode.ACK_PACKET; 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/jfastnet/processors/IMessageReceiverPostProcessor.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2018 Klaus Pfeiffer - klaus@allpiper.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | ******************************************************************************/ 16 | 17 | package com.jfastnet.processors; 18 | 19 | import com.jfastnet.messages.Message; 20 | 21 | /** @author Klaus Pfeiffer - klaus@allpiper.com */ 22 | public interface IMessageReceiverPostProcessor { 23 | 24 | /** Message is received and forwarded to the external receiver at this time. */ 25 | Message afterReceive(Message message); 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/jfastnet/messages/IInstantOnSendProcessable.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2018 Klaus Pfeiffer - klaus@allpiper.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | ******************************************************************************/ 16 | 17 | package com.jfastnet.messages; 18 | 19 | /** Marker interface to determine, that a message should be processed, before 20 | * sending it to the receiver. Use with caution, this will easily get you 21 | * into trouble when using the lockstep system. 22 | * @author Klaus Pfeiffer - klaus@allpiper.com */ 23 | public interface IInstantOnSendProcessable {} 24 | -------------------------------------------------------------------------------- /src/main/java/com/jfastnet/messages/IInstantServerProcessable.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2018 Klaus Pfeiffer - klaus@allpiper.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | ******************************************************************************/ 16 | 17 | package com.jfastnet.messages; 18 | 19 | /** Marker interface for instant processable actions on the server side upon receiving. 20 | * Be cautious with concurrency! 21 | *

Can only be transferred unreliable, because all systems are skipped.

22 | * @author Klaus Pfeiffer - klaus@allpiper.com */ 23 | public interface IInstantServerProcessable {} 24 | -------------------------------------------------------------------------------- /src/main/resources/log4j.properties: -------------------------------------------------------------------------------- 1 | ################################################################################ 2 | # Copyright 2018 Klaus Pfeiffer - klaus@allpiper.com 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | ################################################################################ 16 | 17 | ## standard out appender 18 | log4j.appender.Stdout=org.apache.log4j.ConsoleAppender 19 | log4j.appender.Stdout.layout=org.apache.log4j.PatternLayout 20 | log4j.appender.Stdout.layout.ConversionPattern=%d{dd-MM-yyyy HH:mm:ss,SS} %-5p %C - %m%n 21 | 22 | log4j.rootLogger=ERROR,Stdout 23 | 24 | log4j.logger.com.allpiper=INFO 25 | log4j.logger.com.jfastnet=INFO 26 | 27 | log4j.logger.io.netty=INFO 28 | -------------------------------------------------------------------------------- /src/main/java/com/jfastnet/messages/features/TimestampFeature.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2018 Klaus Pfeiffer - klaus@allpiper.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | ******************************************************************************/ 16 | 17 | package com.jfastnet.messages.features; 18 | 19 | import com.jfastnet.Config; 20 | 21 | /** @author Klaus Pfeiffer - klaus@allpiper.com */ 22 | public class TimestampFeature implements MessageFeature { 23 | 24 | public long timestamp; 25 | 26 | @Override 27 | public void resolveConfig(Config config) { 28 | timestamp = config.timeProvider.get(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/jfastnet/IServerHooks.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2018 Klaus Pfeiffer - klaus@allpiper.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | ******************************************************************************/ 16 | 17 | package com.jfastnet; 18 | 19 | /** Server hooks are called on various occasions. 20 | * @author Klaus Pfeiffer - klaus@allpiper.com */ 21 | public interface IServerHooks { 22 | 23 | /** Called when a client joins the server. */ 24 | default void onRegister(int clientId) {} 25 | 26 | /** Called when a client leaves the server. */ 27 | default void onUnregister(int clientId) {} 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/jfastnet/messages/GenericMessage.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2018 Klaus Pfeiffer - klaus@allpiper.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | ******************************************************************************/ 16 | 17 | package com.jfastnet.messages; 18 | 19 | /** Used to send a plain object. 20 | * @author Klaus Pfeiffer - klaus@allpiper.com */ 21 | public class GenericMessage extends Message { 22 | 23 | public Object object; 24 | 25 | /** no-arg constructor required for serialization. */ 26 | public GenericMessage() {} 27 | 28 | public GenericMessage(Object object) { 29 | this.object = object; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/jfastnet/processors/IMessageReceiverPreProcessor.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2018 Klaus Pfeiffer - klaus@allpiper.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | ******************************************************************************/ 16 | 17 | package com.jfastnet.processors; 18 | 19 | import com.jfastnet.messages.Message; 20 | 21 | /** Called before the message is handed over to the external receiver. 22 | * @author Klaus Pfeiffer - klaus@allpiper.com */ 23 | public interface IMessageReceiverPreProcessor { 24 | 25 | /** Called before the message is handed over to the external receiver. */ 26 | Message beforeReceive(Message message); 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/jfastnet/IPeer.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2018 Klaus Pfeiffer - klaus@allpiper.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | ******************************************************************************/ 16 | 17 | package com.jfastnet; 18 | 19 | import com.jfastnet.messages.Message; 20 | 21 | /** @author Klaus Pfeiffer - klaus@allpiper.com */ 22 | public interface IPeer extends IStartStop, IMessageSender, ISimpleProcessable { 23 | 24 | /** After the method call message.payload should be non-null. 25 | * @param message message to create payload for 26 | * @return true if creation of payload was successful */ 27 | boolean createPayload(Message message); 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/jfastnet/messages/IsReadyMessage.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2018 Klaus Pfeiffer - klaus@allpiper.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | ******************************************************************************/ 16 | 17 | package com.jfastnet.messages; 18 | 19 | import lombok.extern.slf4j.Slf4j; 20 | 21 | /** Sent when client is ready. 22 | * @author Klaus Pfeiffer - klaus@allpiper.com */ 23 | @Slf4j 24 | public class IsReadyMessage extends Message implements IDontFrame { 25 | 26 | @Override 27 | public void process(Object context) { 28 | log.info("Player {} is ready.", getSenderId()); 29 | int clientId = getSenderId(); 30 | getConfig().requiredClients.put(clientId, true); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/jfastnet/util/FifoMap.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2018 Klaus Pfeiffer - klaus@allpiper.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | ******************************************************************************/ 16 | 17 | package com.jfastnet.util; 18 | 19 | import java.util.LinkedHashMap; 20 | import java.util.Map; 21 | 22 | /** @author Klaus Pfeiffer - klaus@allpiper.com */ 23 | public class FifoMap extends LinkedHashMap { 24 | 25 | private int maxEntries; 26 | 27 | public FifoMap() {} 28 | 29 | public FifoMap(int maxEntries) { 30 | super(maxEntries * 10 / 7, 0.7f, false); 31 | this.maxEntries = maxEntries; 32 | } 33 | 34 | @Override 35 | protected boolean removeEldestEntry(Map.Entry eldest) { 36 | return size() > maxEntries; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/com/jfastnet/messages/comparators/MessageIdComparator.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2018 Klaus Pfeiffer - klaus@allpiper.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | ******************************************************************************/ 16 | 17 | package com.jfastnet.messages.comparators; 18 | 19 | import com.jfastnet.messages.Message; 20 | 21 | import java.util.Comparator; 22 | 23 | /** Compares the id of messages. 24 | * @author Klaus Pfeiffer - klaus@allpiper.com */ 25 | public class MessageIdComparator implements Comparator { 26 | 27 | public static final MessageIdComparator INSTANCE = new MessageIdComparator(); 28 | 29 | @Override 30 | public int compare(Message o1, Message o2) { 31 | return Long.compare(o1.getMsgId(), o2.getMsgId()); 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/jfastnet/events/DisabledStackedMessagesEvent.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2018 Klaus Pfeiffer - klaus@allpiper.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | ******************************************************************************/ 16 | 17 | package com.jfastnet.events; 18 | 19 | import com.jfastnet.messages.StackedMessage; 20 | 21 | /** @author Klaus Pfeiffer - klaus@allpiper.com */ 22 | public class DisabledStackedMessagesEvent implements Event { 23 | 24 | private final StackedMessage offendingStackedMessage; 25 | 26 | public DisabledStackedMessagesEvent(StackedMessage offendingStackedMessage) { 27 | this.offendingStackedMessage = offendingStackedMessage; 28 | } 29 | 30 | @Override 31 | public void accept(EventVisitor visitor) { 32 | visitor.visit(this); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/jfastnet/messages/StackAckMessage.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2018 Klaus Pfeiffer - klaus@allpiper.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | ******************************************************************************/ 16 | 17 | package com.jfastnet.messages; 18 | 19 | import lombok.Getter; 20 | 21 | /** @author Klaus Pfeiffer - klaus@allpiper.com */ 22 | public class StackAckMessage extends Message { 23 | 24 | @Getter 25 | private long lastReceivedId; 26 | 27 | /** no-arg constructor required for serialization. */ 28 | private StackAckMessage() {} 29 | 30 | public StackAckMessage(long lastReceivedId) { 31 | this.lastReceivedId = lastReceivedId; 32 | } 33 | 34 | @Override 35 | public ReliableMode getReliableMode() { 36 | return ReliableMode.UNRELIABLE; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/com/jfastnet/messages/SequenceKeepAlive.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2018 Klaus Pfeiffer - klaus@allpiper.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | ******************************************************************************/ 16 | 17 | package com.jfastnet.messages; 18 | 19 | /** A keep alive message, so we can be sure that the last message also gets 20 | * retrieved. Without sending this message one peer could be stuck, because 21 | * it never retrieves the last message and also has no way of detecting, 22 | * because a message with an higher id is not sent. 23 | * @author Klaus Pfeiffer - klaus@allpiper.com */ 24 | public class SequenceKeepAlive extends Message implements IDontFrame { 25 | @Override 26 | public boolean stackable() { 27 | return getConfig().stackKeepAliveMessages; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/jfastnet/util/SizeLimitedSet.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2018 Klaus Pfeiffer - klaus@allpiper.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | ******************************************************************************/ 16 | 17 | package com.jfastnet.util; 18 | 19 | import java.util.TreeSet; 20 | 21 | /** @author Klaus Pfeiffer - klaus@allpiper.com */ 22 | public class SizeLimitedSet extends TreeSet { 23 | 24 | private int maximumSize; 25 | 26 | public SizeLimitedSet() {} 27 | 28 | public SizeLimitedSet(int maximumSize) { 29 | this.maximumSize = maximumSize; 30 | } 31 | 32 | @Override 33 | public boolean add(E e) { 34 | boolean add = super.add(e); 35 | checkSize(); 36 | return add; 37 | } 38 | 39 | private void checkSize() { 40 | if (size() > maximumSize) { 41 | remove(first()); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/com/jfastnet/events/RequestedMessageNotInLogEvent.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2018 Klaus Pfeiffer - klaus@allpiper.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | ******************************************************************************/ 16 | 17 | package com.jfastnet.events; 18 | 19 | import com.jfastnet.MessageKey; 20 | import lombok.Getter; 21 | 22 | /** @author Klaus Pfeiffer - klaus@allpiper.com */ 23 | public class RequestedMessageNotInLogEvent implements Event { 24 | 25 | @Getter private final MessageKey messageKey; 26 | @Getter private final int requesterId; 27 | 28 | public RequestedMessageNotInLogEvent(MessageKey messageKey, int requesterId) { 29 | this.messageKey = messageKey; 30 | this.requesterId = requesterId; 31 | } 32 | 33 | @Override 34 | public void accept(EventVisitor visitor) { 35 | visitor.visit(this); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/jfastnet/util/SizeLimitedMap.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2018 Klaus Pfeiffer - klaus@allpiper.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | ******************************************************************************/ 16 | 17 | package com.jfastnet.util; 18 | 19 | import java.util.TreeMap; 20 | 21 | /** @author Klaus Pfeiffer - klaus@allpiper.com */ 22 | public class SizeLimitedMap extends TreeMap { 23 | 24 | private int maximumSize; 25 | 26 | public SizeLimitedMap() {} 27 | 28 | public SizeLimitedMap(int maximumSize) { 29 | this.maximumSize = maximumSize; 30 | } 31 | 32 | @Override 33 | public V put(K key, V value) { 34 | V put = super.put(key, value); 35 | checkSize(); 36 | return put; 37 | } 38 | 39 | private void checkSize() { 40 | if (size() > maximumSize) { 41 | pollFirstEntry(); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/com/jfastnet/serialiser/ISerialiser.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2018 Klaus Pfeiffer - klaus@allpiper.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | ******************************************************************************/ 16 | 17 | package com.jfastnet.serialiser; 18 | 19 | import com.jfastnet.messages.Message; 20 | 21 | import java.io.InputStream; 22 | import java.io.OutputStream; 23 | import java.util.zip.CRC32; 24 | 25 | /** @author Klaus Pfeiffer - klaus@allpiper.com */ 26 | public interface ISerialiser { 27 | 28 | byte[] serialise(Message message); 29 | 30 | Message deserialise(byte[] serialisedMessage, int offset, int length); 31 | 32 | void serialiseWithStream(Message message, OutputStream _outputStream); 33 | 34 | Message deserialiseWithStream(InputStream _is); 35 | 36 | /** Set payload is a requirement. */ 37 | CRC32 getChecksum(Message message, byte[] salt); 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/com/jfastnet/messages/comparators/MessageExecutionPriorityComparator.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2018 Klaus Pfeiffer - klaus@allpiper.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | ******************************************************************************/ 16 | 17 | package com.jfastnet.messages.comparators; 18 | 19 | import com.jfastnet.messages.Message; 20 | 21 | import java.util.Comparator; 22 | 23 | /** Higher priority messages will come first when using this comparator. 24 | * @author Klaus Pfeiffer - klaus@allpiper.com */ 25 | public class MessageExecutionPriorityComparator implements Comparator { 26 | 27 | public static final MessageExecutionPriorityComparator INSTANCE = new MessageExecutionPriorityComparator(); 28 | 29 | @Override 30 | public int compare(Message o1, Message o2) { 31 | return -Integer.compare(o1.executionPriority(), o2.executionPriority()); 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/jfastnet/messages/AckMessage.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2018 Klaus Pfeiffer - klaus@allpiper.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | ******************************************************************************/ 16 | 17 | package com.jfastnet.messages; 18 | 19 | import lombok.extern.slf4j.Slf4j; 20 | 21 | /** @author Klaus Pfeiffer - klaus@allpiper.com */ 22 | @Slf4j 23 | public class AckMessage extends Message implements IAckMessage { 24 | 25 | /** Message id to acknowledge. */ 26 | public long id; 27 | 28 | public AckMessage() {} 29 | 30 | public AckMessage(long id) { 31 | this.id = id; 32 | } 33 | 34 | @Override 35 | public ReliableMode getReliableMode() { 36 | return ReliableMode.UNRELIABLE; 37 | } 38 | 39 | public String toString() { 40 | return super.toString() + ", acknowledge id=" + id; 41 | } 42 | 43 | @Override 44 | public long getAckId() { 45 | return id; 46 | } 47 | } -------------------------------------------------------------------------------- /src/main/java/com/jfastnet/processors/DiscardWrongChecksumMessagesHandler.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2018 Klaus Pfeiffer - klaus@allpiper.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | ******************************************************************************/ 16 | 17 | package com.jfastnet.processors; 18 | 19 | import com.jfastnet.messages.Message; 20 | import com.jfastnet.messages.features.ChecksumFeature; 21 | 22 | /** Check incoming messages for correct checksums. 23 | * @author Klaus Pfeiffer - klaus@allpiper.com */ 24 | public class DiscardWrongChecksumMessagesHandler implements IMessageReceiverPreProcessor { 25 | 26 | @Override 27 | public Message beforeReceive(Message incomingMessage) { 28 | ChecksumFeature checksumFeature = incomingMessage.getFeatures().get(ChecksumFeature.class); 29 | if (checksumFeature != null && !checksumFeature.check(incomingMessage)) { 30 | return null; 31 | } 32 | return incomingMessage; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/jfastnet/exceptions/DeserialiseException.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2018 Klaus Pfeiffer - klaus@allpiper.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | ******************************************************************************/ 16 | 17 | package com.jfastnet.exceptions; 18 | 19 | /** @author Klaus Pfeiffer - klaus@allpiper.com */ 20 | public class DeserialiseException extends RuntimeException { 21 | 22 | public DeserialiseException() { 23 | } 24 | 25 | public DeserialiseException(String message) { 26 | super(message); 27 | } 28 | 29 | public DeserialiseException(String message, Throwable cause) { 30 | super(message, cause); 31 | } 32 | 33 | public DeserialiseException(Throwable cause) { 34 | super(cause); 35 | } 36 | 37 | public DeserialiseException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { 38 | super(message, cause, enableSuppression, writableStackTrace); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/com/jfastnet/messages/IOrderedMessage.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2018 Klaus Pfeiffer - klaus@allpiper.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | ******************************************************************************/ 16 | 17 | package com.jfastnet.messages; 18 | 19 | /** A message that needs to be received in a particular order has to implement 20 | * this interface and respect the conditions. 21 | *
Usually a better way is to use a reliable sequence instead. 22 | * @author Klaus Pfeiffer - klaus@allpiper.com */ 23 | public interface IOrderedMessage { 24 | 25 | /** @return the message id this message is based on. The message with this 26 | * id has to be received, before this message can be processed. From this 27 | * it follows, that the message this message is based on has to be a 28 | * reliable udp message. Otherwise you could run into a kind of 29 | * "udp processing deadlock". */ 30 | long getBasedOnMessageId(); 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/jfastnet/messages/StackedMessage.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2018 Klaus Pfeiffer - klaus@allpiper.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | ******************************************************************************/ 16 | 17 | package com.jfastnet.messages; 18 | 19 | import lombok.Getter; 20 | 21 | import java.util.List; 22 | 23 | /** @author Klaus Pfeiffer - klaus@allpiper.com */ 24 | public class StackedMessage extends Message { 25 | 26 | @Getter 27 | List messages; 28 | 29 | /** no-arg constructor required for serialization. */ 30 | private StackedMessage() {} 31 | 32 | public StackedMessage(List messages) { 33 | this.messages = messages; 34 | } 35 | 36 | @Override 37 | public ReliableMode getReliableMode() { 38 | return ReliableMode.UNRELIABLE; 39 | } 40 | 41 | public Message getLastMessage() { 42 | if (messages.size() <= 0) { 43 | return null; 44 | } 45 | return messages.get(messages.size() - 1); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/com/jfastnet/events/EventLog.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2018 Klaus Pfeiffer - klaus@allpiper.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | ******************************************************************************/ 16 | 17 | package com.jfastnet.events; 18 | 19 | import com.jfastnet.Config; 20 | import com.jfastnet.State; 21 | import lombok.Getter; 22 | import org.apache.commons.collections4.queue.CircularFifoQueue; 23 | 24 | /** @author Klaus Pfeiffer - klaus@allpiper.com */ 25 | public class EventLog { 26 | 27 | private final Config config; 28 | private final State state; 29 | 30 | @Getter 31 | private CircularFifoQueue eventQueue; 32 | 33 | public EventLog(Config config, State state) { 34 | this.config = config; 35 | this.state = state; 36 | this.eventQueue = new CircularFifoQueue<>(config.eventLogSize); 37 | } 38 | 39 | public void add(Event event) { 40 | eventQueue.add(event); 41 | } 42 | 43 | public void clear() { 44 | eventQueue.clear(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/com/jfastnet/processors/AddChecksumProcessor.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2018 Klaus Pfeiffer - klaus@allpiper.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | ******************************************************************************/ 16 | 17 | package com.jfastnet.processors; 18 | 19 | import com.jfastnet.messages.Message; 20 | import com.jfastnet.messages.features.ChecksumFeature; 21 | 22 | /** @author Klaus Pfeiffer - klaus@allpiper.com */ 23 | public class AddChecksumProcessor implements IMessageSenderPreProcessor { 24 | 25 | /** WIP! */ 26 | private byte[] salt = "".getBytes(); 27 | 28 | @Override 29 | public Message beforeSend(Message message) { 30 | ChecksumFeature checksumFeature = message.getFeatures().get(ChecksumFeature.class); 31 | if (checksumFeature != null) { 32 | checksumFeature.calculate(message); 33 | // FIXME needs two times of serialisation 34 | message.payload = message.getConfig().serialiser.serialise(message); 35 | } 36 | return message; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/com/jfastnet/util/SizeLimitedList.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2018 Klaus Pfeiffer - klaus@allpiper.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | ******************************************************************************/ 16 | 17 | package com.jfastnet.util; 18 | 19 | import java.util.ArrayList; 20 | 21 | /** @author Klaus Pfeiffer - klaus@allpiper.com */ 22 | public class SizeLimitedList extends ArrayList { 23 | 24 | private int maximumSize; 25 | 26 | public SizeLimitedList() {} 27 | 28 | public SizeLimitedList(int maximumSize) { 29 | this.maximumSize = maximumSize; 30 | } 31 | 32 | @Override 33 | public boolean add(E e) { 34 | boolean add = super.add(e); 35 | checkSize(); 36 | return add; 37 | } 38 | 39 | @Override 40 | public void add(int index, E element) { 41 | super.add(index, element); 42 | if (index > 0) { 43 | checkSize(); 44 | } 45 | } 46 | 47 | private void checkSize() { 48 | if (size() > maximumSize) { 49 | remove(0); 50 | } 51 | } 52 | } 53 | 54 | -------------------------------------------------------------------------------- /src/main/java/com/jfastnet/processors/AbstractMessageProcessor.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2018 Klaus Pfeiffer - klaus@allpiper.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | ******************************************************************************/ 16 | 17 | package com.jfastnet.processors; 18 | 19 | import com.jfastnet.Config; 20 | import com.jfastnet.State; 21 | 22 | /** @author Klaus Pfeiffer - klaus@allpiper.com 23 | * @param Type of processor config object */ 24 | public abstract class AbstractMessageProcessor { 25 | 26 | public Config config; 27 | 28 | public State state; 29 | 30 | public E processorConfig; 31 | 32 | public AbstractMessageProcessor(Config config, State state) { 33 | assert config != null : "Config may not be null!"; 34 | assert state != null : "State may not be null!"; 35 | this.config = config; 36 | this.state = state; 37 | this.processorConfig = (E) config.additionalConfigMap.get(getConfigClass()); 38 | } 39 | 40 | public Class getConfigClass() { 41 | return null; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/com/jfastnet/MessageKey.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2018 Klaus Pfeiffer - klaus@allpiper.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | ******************************************************************************/ 16 | 17 | package com.jfastnet; 18 | 19 | import com.jfastnet.messages.Message; 20 | import lombok.EqualsAndHashCode; 21 | import lombok.ToString; 22 | 23 | /** Reliable mode / ClientID / MessageID - Key 24 | * @author Klaus Pfeiffer - klaus@allpiper.com */ 25 | @ToString 26 | @EqualsAndHashCode 27 | public class MessageKey { 28 | public final long messageId; 29 | public final Message.ReliableMode reliableMode; 30 | public final int clientId; 31 | 32 | public MessageKey(Message.ReliableMode reliableMode, int clientId, long messageId) { 33 | this.reliableMode = reliableMode; 34 | this.clientId = clientId; 35 | this.messageId = messageId; 36 | } 37 | 38 | public static MessageKey newKey(Message.ReliableMode reliableMode, int clientId, long msgId) { 39 | return new MessageKey(reliableMode, clientId, msgId); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/com/jfastnet/state/ClientState.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2018 Klaus Pfeiffer - klaus@allpiper.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | ******************************************************************************/ 16 | 17 | package com.jfastnet.state; 18 | 19 | import com.jfastnet.Config; 20 | import lombok.Getter; 21 | import lombok.ToString; 22 | 23 | import java.net.InetSocketAddress; 24 | 25 | /** @author Klaus Pfeiffer - klaus@allpiper.com */ 26 | @ToString 27 | public class ClientState { 28 | 29 | @Getter private final NetworkQuality networkQuality; 30 | @Getter private final InetSocketAddress socketAddress; 31 | 32 | private final Config config; 33 | 34 | public ClientState(Config config) { 35 | this.config = config; 36 | this.socketAddress = null; 37 | networkQuality = new NetworkQuality(this.config); 38 | } 39 | 40 | public ClientState(Config config, InetSocketAddress socketAddress) { 41 | this.config = config; 42 | this.socketAddress = socketAddress; 43 | networkQuality = new NetworkQuality(this.config); 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/com/jfastnet/idprovider/SimpleIdProvider.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2018 Klaus Pfeiffer - klaus@allpiper.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | ******************************************************************************/ 16 | 17 | package com.jfastnet.idprovider; 18 | 19 | import com.jfastnet.messages.Message; 20 | 21 | import java.util.concurrent.atomic.AtomicLong; 22 | 23 | /** Provides a new id for every message. Usually not advisable to use this 24 | * provider. 25 | * @author Klaus Pfeiffer - klaus@allpiper.com */ 26 | public class SimpleIdProvider implements IIdProvider { 27 | 28 | AtomicLong id = new AtomicLong(); 29 | 30 | @Override 31 | public long createIdFor(Message message) { 32 | return id.incrementAndGet(); 33 | } 34 | 35 | @Override 36 | public long getLastIdFor(Message message) { 37 | return id.get(); 38 | } 39 | 40 | @Override 41 | public long stepBack(Message message) { 42 | return id.decrementAndGet(); 43 | } 44 | 45 | @Override 46 | public int compare(Message m1, Message m2) { 47 | return Long.compare(m1.getMsgId(), m2.getMsgId()); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | The API of this library is subject to change. 4 | 5 | ## 0.3.3 6 | 7 | * Missing receiver id led to server sending specific message to all clients (only with ACK reliable mode) 8 | 9 | ## 0.3.2 10 | 11 | * added empty constructors to fix de-/serialising 12 | 13 | ## 0.3.1 14 | 15 | * When compression fails, message will be sent uncompressed 16 | * Congestion Control 17 | * don't split up resent messages 18 | * added CompressedMessage for convenient sending of single compressed messages 19 | * fixed bug in ReliableModeAckProcessor when messages got sent to all clients from the server 20 | * configurable maximumNumberOfResentMessagesPerCheck in ReliableModeAckProcessor added 21 | * StackedMessageProcessor bugfix: lastAckMessageIdMap was not set correctly on re-join 22 | 23 | ## 0.3.0 24 | 25 | * Crucial bugfixes for stacked message processing 26 | * Added events and an event queue to notify other components of critical events 27 | .* RequestedMessageNotInLogEvent 28 | .* DisabledStackedMessagesEvent 29 | * Exceptions 30 | * Improved message log 31 | * Added processor config for the ReliableModeSequenceProcessor 32 | 33 | ## 0.2.4 34 | 35 | * Added context to message processing 36 | 37 | ## 0.2.3 38 | 39 | * No auto splitting of unreliable messages -> splitted messages must be sent reliably 40 | 41 | ## 0.2.2 42 | 43 | * Fail safe if compression failed 44 | 45 | ## 0.2.1 46 | 47 | * TimerSyncMessage missing MessageFeatures bugfix 48 | 49 | ## 0.2.0 50 | 51 | * Stackable messages (new reliable sending mode, where all unacknowledged messages get stacked onto the most recent message) 52 | * Cleaner separation of config and state 53 | * Individual configs for the processors 54 | * Faster message log 55 | 56 | ## 0.1.5 57 | 58 | * auto split bugfix 59 | -------------------------------------------------------------------------------- /src/main/java/com/jfastnet/IMessageSender.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2018 Klaus Pfeiffer - klaus@allpiper.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | ******************************************************************************/ 16 | 17 | package com.jfastnet; 18 | 19 | import com.jfastnet.messages.GenericMessage; 20 | import com.jfastnet.messages.Message; 21 | 22 | /** @author Klaus Pfeiffer - klaus@allpiper.com */ 23 | public interface IMessageSender { 24 | 25 | /** 26 | * @param message message to send 27 | * @return false if message could not be sent, true otherwise. If true is 28 | * returned, there can still be errors due to a possible non-blocking 29 | * send of the message. */ 30 | boolean send(Message message); 31 | 32 | /** Used to send a plain object. */ 33 | default boolean send(Object message) { 34 | return send(new GenericMessage(message)); 35 | } 36 | 37 | /** Queue message for later sending. It is advisable to queue big number of 38 | * messages like message parts created from big messages. */ 39 | default boolean queue(Message message) { 40 | // Default implementation is to just send the message 41 | return send(message); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/com/jfastnet/idprovider/IIdProvider.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2018 Klaus Pfeiffer - klaus@allpiper.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | ******************************************************************************/ 16 | 17 | package com.jfastnet.idprovider; 18 | 19 | import com.jfastnet.messages.Message; 20 | 21 | import java.util.Comparator; 22 | 23 | /** Provides the id for messages. It depends on the use case which provider 24 | * you should choose. 25 | * @author Klaus Pfeiffer - klaus@allpiper.com */ 26 | public interface IIdProvider extends Comparator { 27 | 28 | /** Get new id for message. */ 29 | long createIdFor(Message message); 30 | 31 | /** Get last created id for message type. */ 32 | long getLastIdFor(Message message); 33 | 34 | /** Revert last id. */ 35 | long stepBack(Message message); 36 | 37 | /** Return true, if broadcasted messages should resolve the message id 38 | * again after setting the correct receiver id. 39 | * 40 | * Otherwise every client receives the same id for a particular message, 41 | * which is required for the lock stepping extension implementation. */ 42 | default boolean resolveEveryClientMessage() { 43 | return false; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/test/java/com/jfastnet/util/ConcurrentSizeLimitedMapTest.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2018 Klaus Pfeiffer - klaus@allpiper.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | ******************************************************************************/ 16 | 17 | package com.jfastnet.util; 18 | 19 | import lombok.extern.slf4j.Slf4j; 20 | import org.junit.Test; 21 | 22 | import java.util.Arrays; 23 | import java.util.Map; 24 | 25 | import static org.hamcrest.MatcherAssert.assertThat; 26 | import static org.hamcrest.Matchers.is; 27 | 28 | /** @author Klaus Pfeiffer - klaus@allpiper.com */ 29 | @Slf4j 30 | public class ConcurrentSizeLimitedMapTest { 31 | 32 | @Test 33 | public void testConcurrentSizeLimitedMap() { 34 | Map map = new com.jfastnet.util.ConcurrentSizeLimitedMap(3); 35 | map.put(1, 1); 36 | map.put(2, 2); 37 | map.put(3, 3); 38 | log.info("map: " + Arrays.toString(map.keySet().toArray())); 39 | assertThat(map.size(), is(3)); 40 | assertThat(map.get(1), is(1)); 41 | assertThat(map.get(3), is(3)); 42 | 43 | map.put(4, 4); 44 | map.put(5, 5); 45 | log.info("map: " + Arrays.toString(map.keySet().toArray())); 46 | assertThat(map.size(), is(3)); 47 | assertThat(map.get(3), is(3)); 48 | } 49 | } -------------------------------------------------------------------------------- /src/main/java/com/jfastnet/util/NullsafeHashMap.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2018 Klaus Pfeiffer - klaus@allpiper.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | ******************************************************************************/ 16 | 17 | package com.jfastnet.util; 18 | 19 | import java.util.HashMap; 20 | import java.util.function.Supplier; 21 | 22 | /** If on "get" the map is null, a new object is inserted. 23 | * @author Klaus Pfeiffer - klaus@allpiper.com 24 | * @param key for this map containing another Object of entities and their ids. 25 | * @param value of this map */ 26 | public class NullsafeHashMap extends HashMap { 27 | 28 | private Supplier supplier; 29 | 30 | public NullsafeHashMap() {} 31 | 32 | public NullsafeHashMap(Supplier supplier) { 33 | this.supplier = supplier; 34 | } 35 | 36 | public NullsafeHashMap(int componentsInitialCapacity) { 37 | super(componentsInitialCapacity); 38 | } 39 | 40 | @Override 41 | public F get(final Object key) { 42 | F map = super.get(key); 43 | if (map == null) { 44 | map = newInstance(); 45 | put((E) key, map); 46 | } 47 | return map; 48 | } 49 | 50 | /** If on get it is null, this object is created. Returning null would 51 | * imitate the usual HashMap behavior. */ 52 | protected F newInstance() { 53 | return supplier.get(); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/com/jfastnet/messages/features/ChecksumFeature.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2018 Klaus Pfeiffer - klaus@allpiper.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | ******************************************************************************/ 16 | 17 | package com.jfastnet.messages.features; 18 | 19 | import com.jfastnet.Config; 20 | import com.jfastnet.messages.Message; 21 | import lombok.Getter; 22 | import lombok.extern.slf4j.Slf4j; 23 | 24 | import java.util.zip.CRC32; 25 | 26 | /** @author Klaus Pfeiffer - klaus@allpiper.com */ 27 | @Slf4j 28 | public class ChecksumFeature implements MessageFeature { 29 | 30 | // TODO make configurable 31 | public static final byte[] salt = "".getBytes(); 32 | 33 | /** Checksum of message. */ 34 | @Getter 35 | private long crcValue; 36 | 37 | /** Must be called after the payload was created. */ 38 | public void calculate(Message message) { 39 | CRC32 crc32 = message.getConfig().serialiser.getChecksum(message, salt); 40 | crcValue = crc32.getValue(); 41 | } 42 | 43 | public boolean check(Message message) { 44 | long crcValue = this.crcValue; 45 | // Set CRC value to 0 so we can correctly compare both checksum'd messages 46 | this.crcValue = 0L; 47 | Config config = message.getConfig(); 48 | message.payload = config.serialiser.serialise(message); 49 | CRC32 crc32 = config.serialiser.getChecksum(message, salt); 50 | log.trace("Sent CRC: {}, Calculated CRC: {}", crcValue, crc32.getValue()); 51 | this.crcValue = crcValue; 52 | return crcValue == crc32.getValue(); 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /src/test/java/com/jfastnet/examples/HelloWorld.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2018 Klaus Pfeiffer - klaus@allpiper.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | ******************************************************************************/ 16 | 17 | package com.jfastnet.examples; 18 | 19 | import com.jfastnet.Client; 20 | import com.jfastnet.Config; 21 | import com.jfastnet.Server; 22 | import com.jfastnet.messages.GenericMessage; 23 | 24 | import java.util.concurrent.atomic.AtomicInteger; 25 | 26 | /** @author Klaus Pfeiffer - klaus@allpiper.com */ 27 | public class HelloWorld { 28 | 29 | private static final AtomicInteger received = new AtomicInteger(0); 30 | 31 | public static class PrintMessage extends GenericMessage { 32 | 33 | /** no-arg constructor required for serialization. */ 34 | private PrintMessage() {} 35 | 36 | PrintMessage(Object object) { super(object); } 37 | 38 | @Override 39 | public void process(Object context) { 40 | System.out.println(object); 41 | received.incrementAndGet(); 42 | } 43 | } 44 | 45 | public static void main(String[] args) throws InterruptedException { 46 | Server server = new Server(new Config().setBindPort(15150)); 47 | Client client = new Client(new Config().setPort(15150)); 48 | 49 | server.start(); 50 | client.start(); 51 | client.blockingWaitUntilConnected(); 52 | 53 | server.send(new PrintMessage("Hello Client!")); 54 | client.send(new PrintMessage("Hello Server!")); 55 | 56 | while (received.get() < 2) Thread.sleep(100); 57 | 58 | client.stop(); 59 | server.stop(); 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /src/main/java/com/jfastnet/util/ConcurrentSizeLimitedMap.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2018 Klaus Pfeiffer - klaus@allpiper.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | ******************************************************************************/ 16 | 17 | package com.jfastnet.util; 18 | 19 | import java.util.ArrayList; 20 | import java.util.Collections; 21 | import java.util.List; 22 | import java.util.concurrent.ConcurrentHashMap; 23 | 24 | /** @author Klaus Pfeiffer - klaus@allpiper.com */ 25 | public class ConcurrentSizeLimitedMap, V> extends ConcurrentHashMap { 26 | 27 | /** Maximum number of entries in this map. */ 28 | private int maximumSize; 29 | 30 | /** How many items get deleted if we are above maximum. */ 31 | private int deleteCount; 32 | 33 | private volatile int size = 0; 34 | 35 | public ConcurrentSizeLimitedMap() {} 36 | 37 | public ConcurrentSizeLimitedMap(int maximumSize) { 38 | this.maximumSize = maximumSize; 39 | this.deleteCount = Math.max(1, maximumSize / 10); 40 | } 41 | 42 | public ConcurrentSizeLimitedMap(int maximumSize, int deleteCount) { 43 | this.maximumSize = maximumSize; 44 | this.deleteCount = deleteCount; 45 | } 46 | 47 | @Override 48 | public V put(K key, V value) { 49 | V put = super.put(key, value); 50 | if (put == null) { 51 | size++; 52 | } 53 | checkSize(); 54 | return put; 55 | } 56 | 57 | private void checkSize() { 58 | if (size > maximumSize) { 59 | List keys = new ArrayList<>(keySet()); 60 | Collections.sort(keys); 61 | int removed = 0; 62 | for (int i = 0; i < keys.size() && removed < deleteCount; i++) { 63 | K key = keys.get(i); 64 | remove(key); 65 | size--; 66 | removed++; 67 | } 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/main/java/com/jfastnet/messages/features/MessageFeatures.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2018 Klaus Pfeiffer - klaus@allpiper.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | ******************************************************************************/ 16 | 17 | package com.jfastnet.messages.features; 18 | 19 | import java.io.Serializable; 20 | import java.util.HashMap; 21 | import java.util.HashSet; 22 | import java.util.Map; 23 | import java.util.Set; 24 | 25 | /** @author Klaus Pfeiffer - klaus@allpiper.com */ 26 | public class MessageFeatures implements Serializable, MessageFeature { 27 | 28 | private Set features = new HashSet<>(); 29 | 30 | private transient Map featureMap = new HashMap<>(); 31 | 32 | public void resolve() { 33 | if (features.size() > 0) { 34 | featureMap = new HashMap<>(); 35 | for (MessageFeature feature : features) { 36 | featureMap.put(feature.getClass(), feature); 37 | } 38 | } 39 | } 40 | 41 | public void add(MessageFeature feature) { 42 | features.add(feature); 43 | featureMap.put(feature.getClass(), feature); 44 | } 45 | 46 | @SuppressWarnings("unchecked") 47 | public T get(Class featureClass) { 48 | return (T) featureMap.get(featureClass); 49 | } 50 | 51 | public static class Immutable extends MessageFeatures { 52 | @Override 53 | public void add(MessageFeature feature) { 54 | throw new UnsupportedOperationException( 55 | "Add your own messageFeatures field with a getter to use " + 56 | "additional features! This is done to spare precious payload" + 57 | " size for messages that don't need additional features." + 58 | "See com.jfastnet.messages.features.ChecksumFeatureTest.ChecksumTestMsg " + 59 | "for an example."); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/main/java/com/jfastnet/messages/ClientTimerSyncMessage.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2018 Klaus Pfeiffer - klaus@allpiper.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | ******************************************************************************/ 16 | 17 | package com.jfastnet.messages; 18 | 19 | import lombok.AccessLevel; 20 | import lombok.Getter; 21 | import lombok.Setter; 22 | import lombok.extern.slf4j.Slf4j; 23 | 24 | /** Send the client his offsetToHost. 25 | * @author Klaus Pfeiffer - klaus@allpiper.com */ 26 | @Slf4j 27 | public class ClientTimerSyncMessage extends Message implements IDontFrame { 28 | 29 | private long retrievedOffsetToHost = 0; 30 | private long retrievedRoundTripTime = 0; 31 | 32 | /** Retrieved offset. */ 33 | @Getter 34 | @Setter(value = AccessLevel.PROTECTED) 35 | private static long realOffsetToHost = 0; 36 | 37 | /** Slowly adapted offset. */ 38 | @Setter @Getter 39 | private static long offsetToHost = 0; 40 | 41 | @Getter 42 | @Setter(value = AccessLevel.PROTECTED) 43 | private static long roundTripTime = 0; 44 | 45 | /** no-arg constructor required for serialization. */ 46 | private ClientTimerSyncMessage() {} 47 | 48 | @java.beans.ConstructorProperties({"retrievedOffsetToHost", "retrievedRoundTripTime"}) 49 | public ClientTimerSyncMessage(long retrievedOffsetToHost, long retrievedRoundTripTime) { 50 | this.retrievedOffsetToHost = retrievedOffsetToHost; 51 | this.retrievedRoundTripTime = retrievedRoundTripTime; 52 | } 53 | 54 | @Override 55 | public void process(Object context) { 56 | realOffsetToHost = retrievedOffsetToHost; 57 | roundTripTime = retrievedRoundTripTime; 58 | log.trace("RTT: {}", roundTripTime); 59 | } 60 | 61 | @Override 62 | public ReliableMode getReliableMode() { 63 | return ReliableMode.UNRELIABLE; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/test/java/com/jfastnet/messages/MessageTest.java: -------------------------------------------------------------------------------- 1 | package com.jfastnet.messages; 2 | 3 | import com.esotericsoftware.kryo.Kryo; 4 | import com.jfastnet.AbstractTest; 5 | import com.jfastnet.Config; 6 | import lombok.extern.slf4j.Slf4j; 7 | import org.junit.Test; 8 | 9 | import static org.hamcrest.MatcherAssert.assertThat; 10 | import static org.hamcrest.Matchers.*; 11 | 12 | 13 | /** @author Klaus Pfeiffer - klaus@allpiper.com */ 14 | @Slf4j 15 | public class MessageTest extends AbstractTest { 16 | 17 | boolean registerClass = false; 18 | 19 | public static class Message1 extends Message {} 20 | public static class Message2 extends Message { 21 | long value = 0; 22 | public Message2(long value) { this.value = value; } 23 | } 24 | 25 | @Test 26 | public void sizeTest() { 27 | Config config; 28 | 29 | config = newClientConfig(); 30 | byte[] b1 = config.serialiser.serialise(new Message1()); 31 | log.info("size unregistered message class: {}", b1.length); 32 | 33 | registerClass = true; 34 | config = newClientConfig(); 35 | 36 | byte[] b2 = config.serialiser.serialise(new Message1()); 37 | log.info("size registered message class: {}", b2.length); 38 | 39 | assertThat("Message with registered class has to be smaller", b2.length, is(lessThan(b1.length))); 40 | assertThat("Naked message should have at max 4 bytes", b2.length, lessThanOrEqualTo(4)); 41 | } 42 | 43 | @Test 44 | public void sizeTestLongValue() { 45 | Config config; 46 | 47 | config = newClientConfig(); 48 | byte[] b1; 49 | b1 = config.serialiser.serialise(new Message2(0)); 50 | log.info("size unreg. zero long value: {}", b1.length); 51 | b1 = config.serialiser.serialise(new Message2(Long.MAX_VALUE)); 52 | log.info("size unreg. max long value: {}", b1.length); 53 | 54 | registerClass = true; 55 | config = newClientConfig(); 56 | 57 | b1 = config.serialiser.serialise(new Message2(0)); 58 | log.info("size reg. zero long value: {}", b1.length); 59 | b1 = config.serialiser.serialise(new Message2(112233)); 60 | log.info("size reg. long value 112233: {}", b1.length); 61 | b1 = config.serialiser.serialise(new Message2(Long.MAX_VALUE)); 62 | log.info("size reg. max long value: {}", b1.length); 63 | } 64 | 65 | @Override 66 | public void customizeKryo(Kryo kryo) { 67 | super.customizeKryo(kryo); 68 | if (registerClass) { 69 | kryo.register(Message1.class); 70 | kryo.register(Message2.class); 71 | } 72 | } 73 | } -------------------------------------------------------------------------------- /src/main/java/com/jfastnet/idprovider/ReliableModeIdProvider.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2018 Klaus Pfeiffer - klaus@allpiper.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | ******************************************************************************/ 16 | 17 | package com.jfastnet.idprovider; 18 | 19 | import com.jfastnet.messages.Message; 20 | import com.jfastnet.util.NullsafeHashMap; 21 | import lombok.extern.slf4j.Slf4j; 22 | 23 | import java.util.Map; 24 | import java.util.concurrent.atomic.AtomicLong; 25 | 26 | /** Ids begin with 1. Every reliable mode has its stream of ids. 27 | * @author Klaus Pfeiffer - klaus@allpiper.com */ 28 | @Slf4j 29 | public class ReliableModeIdProvider implements IIdProvider { 30 | 31 | private Map idMap = new NullsafeHashMap() { 32 | @Override 33 | protected AtomicLong newInstance() { 34 | return new AtomicLong(); 35 | } 36 | }; 37 | 38 | @Override 39 | public long createIdFor(Message message) { 40 | final long newId = idMap.get(message.getReliableMode()).incrementAndGet(); 41 | log.trace("Created new id {} for {}", newId, message); 42 | return newId; 43 | } 44 | 45 | @Override 46 | public long getLastIdFor(Message message) { 47 | return idMap.get(message.getReliableMode()).get(); 48 | } 49 | 50 | @Override 51 | public long stepBack(Message message) { 52 | return idMap.get(message.getReliableMode()).decrementAndGet(); 53 | } 54 | 55 | @Override 56 | public int compare(Message m1, Message m2) { 57 | 58 | // compare by reliable mode at first 59 | int compare = Integer.compare(m1.getReliableMode().ordinal(), m2.getReliableMode().ordinal()); 60 | if (compare != 0) return compare; 61 | 62 | // second by id 63 | compare = Long.compare(m1.getMsgId(), m2.getMsgId()); 64 | if (compare != 0) return compare; 65 | 66 | return compare; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/main/java/com/jfastnet/processors/DiscardMessagesHandler.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2018 Klaus Pfeiffer - klaus@allpiper.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | ******************************************************************************/ 16 | 17 | package com.jfastnet.processors; 18 | 19 | import com.jfastnet.messages.Message; 20 | import lombok.EqualsAndHashCode; 21 | 22 | import java.util.HashMap; 23 | import java.util.Map; 24 | 25 | /** @author Klaus Pfeiffer - klaus@allpiper.com */ 26 | public class DiscardMessagesHandler implements IMessageReceiverPreProcessor{ 27 | 28 | // TODO potentially growing over time 29 | private Map msgs = new HashMap<>(); 30 | 31 | @Override 32 | public Message beforeReceive(Message incomingMessage) { 33 | Object discardableKey = incomingMessage.getDiscardableKey(); 34 | if (discardableKey != null) { 35 | Message message = msgs.get(discardableKey); 36 | if (message == null) { 37 | msgs.put(discardableKey, incomingMessage); 38 | return incomingMessage; 39 | } else { 40 | long incomingMessageTimestamp = incomingMessage.getTimestamp(); 41 | long existingMessageTimestamp = message.getTimestamp(); 42 | if (incomingMessageTimestamp >= existingMessageTimestamp) { 43 | // incoming message is newer 44 | msgs.put(discardableKey, incomingMessage); 45 | return incomingMessage; 46 | } else { 47 | // discard message, because incoming message is older than 48 | // already retrieved message 49 | return null; 50 | } 51 | } 52 | } 53 | return incomingMessage; 54 | } 55 | 56 | /** Can be used to discard messages of a certain class and a certain key. */ 57 | @EqualsAndHashCode 58 | public static class ClassKey { 59 | private Class clazz; 60 | private Object key; 61 | 62 | public ClassKey(Class clazz, Object key) { 63 | this.clazz = clazz; 64 | this.key = key; 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/main/java/com/jfastnet/processors/MessageLogProcessor.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2018 Klaus Pfeiffer - klaus@allpiper.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | ******************************************************************************/ 16 | 17 | package com.jfastnet.processors; 18 | 19 | import com.jfastnet.Config; 20 | import com.jfastnet.MessageLog; 21 | import com.jfastnet.State; 22 | import com.jfastnet.messages.Message; 23 | import lombok.Getter; 24 | import lombok.Setter; 25 | import lombok.experimental.Accessors; 26 | 27 | import java.util.function.Predicate; 28 | 29 | /** Puts filtered messages into the message log. 30 | * @author Klaus Pfeiffer - klaus@allpiper.com */ 31 | public class MessageLogProcessor extends AbstractMessageProcessor 32 | implements IMessageSenderPostProcessor, IMessageReceiverPreProcessor { 33 | 34 | /** Message log collects messages for resending. */ 35 | @Getter 36 | private MessageLog messageLog; 37 | 38 | public MessageLogProcessor(Config config, State state) { 39 | super(config, state); 40 | this.messageLog = new MessageLog(config, processorConfig); 41 | } 42 | 43 | @Override 44 | public Message beforeReceive(Message message) { 45 | messageLog.addReceived(message); 46 | return message; 47 | } 48 | 49 | @Override 50 | public Message afterSend(Message message) { 51 | messageLog.addSent(message); 52 | return message; 53 | } 54 | 55 | @Override 56 | public Class getConfigClass() { 57 | return ProcessorConfig.class; 58 | } 59 | 60 | @Setter @Getter 61 | @Accessors(chain = true) 62 | public static class ProcessorConfig { 63 | public int receivedMessagesLimit = 1000; 64 | public int sentMessagesMapLimit = 16000; 65 | public Predicate messageLogReceiveFilter = new MessageLog.NoMessagesPredicate(); 66 | public Predicate messageLogSendFilter = new MessageLog.ReliableMessagesPredicate(); 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /src/main/java/com/jfastnet/messages/CompressedMessage.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2018 Klaus Pfeiffer - klaus@allpiper.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | ******************************************************************************/ 16 | 17 | package com.jfastnet.messages; 18 | 19 | import com.jfastnet.State; 20 | import com.jfastnet.exceptions.DeserialiseException; 21 | import lombok.extern.slf4j.Slf4j; 22 | 23 | /** @author Klaus Pfeiffer - klaus@allpiper.com */ 24 | @Slf4j 25 | public class CompressedMessage extends Message { 26 | 27 | byte[] compressedPayload; 28 | 29 | private CompressedMessage() {} 30 | 31 | public static Message createFrom(State state, Message containerMessage) { 32 | CompressedMessage compressedMessage = new CompressedMessage<>(); 33 | compressedMessage.copyAttributesFrom(containerMessage); 34 | containerMessage.setSenderId(0); 35 | 36 | state.getUdpPeer().createPayload(containerMessage); 37 | if (containerMessage.payload instanceof byte[]) { 38 | byte[] bytes = (byte[]) containerMessage.payload; 39 | if (state.getConfig().compressBigMessages) { 40 | byte[] compressedBytes = MessagePart.compress(bytes); 41 | if (compressedBytes != null) { 42 | compressedMessage.compressedPayload = compressedBytes; 43 | return compressedMessage; 44 | } 45 | } 46 | } 47 | return containerMessage; 48 | } 49 | 50 | @Override 51 | public Message beforeReceive() { 52 | byte[] decompressed = MessagePart.decompress(compressedPayload); 53 | Message deserialised = getConfig().serialiser.deserialise(decompressed, 0, decompressed.length); 54 | if (deserialised == null) { 55 | log.error("Deserialised message was null! See previous errors."); 56 | throw new DeserialiseException("Deserialised message was null! See previous errors."); 57 | } 58 | deserialised.copyAttributesFrom(this); 59 | deserialised.getFeatures().resolve(); 60 | deserialised.msgId = this.msgId; 61 | return deserialised; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/test/java/com/jfastnet/messages/CompressedMessageTest.java: -------------------------------------------------------------------------------- 1 | package com.jfastnet.messages; 2 | 3 | import com.esotericsoftware.kryo.Kryo; 4 | import com.jfastnet.AbstractTest; 5 | import lombok.extern.slf4j.Slf4j; 6 | import org.junit.Test; 7 | 8 | import java.util.Random; 9 | import java.util.function.Supplier; 10 | 11 | /** @author Klaus Pfeiffer - klaus@allpiper.com */ 12 | @Slf4j 13 | public class CompressedMessageTest extends AbstractTest { 14 | 15 | private static int received; 16 | 17 | public static class Message1 extends Message { 18 | long value = 0; 19 | public Message1(long value) { this.value = value; } 20 | 21 | @Override 22 | public void process(Object context) { 23 | received++; 24 | } 25 | } 26 | 27 | public static class Message2 extends Message { 28 | long[] longArray; 29 | 30 | public Message2(long[] longArray) { 31 | this.longArray = longArray; 32 | } 33 | 34 | @Override 35 | public void process(Object context) { 36 | received++; 37 | } 38 | } 39 | 40 | @Test 41 | public void testCompressedMessages() { 42 | start(1); 43 | Message msg; 44 | 45 | log.info(" * Test with one additional long field value 0"); 46 | testWithMessage(() -> new Message1(1)); 47 | 48 | log.info(" * Test with one additional long field value Long.MAX_VALUE"); 49 | testWithMessage(() -> new Message1(Long.MAX_VALUE)); 50 | 51 | log.info(" * Test with long array field and random values"); 52 | testWithMessage(() -> new Message2(createLongArrayWithRandomValues(200))); 53 | } 54 | 55 | private void testWithMessage(Supplier messageSupplier) { 56 | received = 0; 57 | Message msg; 58 | msg = messageSupplier.get(); 59 | server.send(msg); 60 | log.info("Payload length uncompressed message: {}", msg.payloadLength()); 61 | waitForCondition("message not received.", 1, () -> received == 1); 62 | 63 | msg = CompressedMessage.createFrom(server.getState(), messageSupplier.get()); 64 | server.send(msg); 65 | log.info("Payload length compressed message: {}", msg.payloadLength()); 66 | waitForCondition("message not received.", 1, () -> received == 2); 67 | } 68 | 69 | private long[] createLongArrayWithRandomValues(int size) { 70 | Random random = new Random(System.currentTimeMillis()); 71 | long[] arr = new long[size]; 72 | for (int i = 0; i < size; i++) { 73 | arr[i] = random.nextLong(); 74 | } 75 | return arr; 76 | } 77 | 78 | @Override 79 | public void customizeKryo(Kryo kryo) { 80 | super.customizeKryo(kryo); 81 | kryo.register(CompressedMessage.class); 82 | kryo.register(Message1.class); 83 | kryo.register(Message2.class); 84 | } 85 | 86 | 87 | } -------------------------------------------------------------------------------- /src/main/java/com/jfastnet/messages/ConnectRequest.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2018 Klaus Pfeiffer - klaus@allpiper.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | ******************************************************************************/ 16 | 17 | package com.jfastnet.messages; 18 | 19 | import com.jfastnet.processors.StackedMessageProcessor; 20 | import lombok.Getter; 21 | import lombok.Setter; 22 | import lombok.extern.slf4j.Slf4j; 23 | 24 | /** Sent from the client to the server. 25 | * @author Klaus Pfeiffer - klaus@allpiper.com */ 26 | @Slf4j 27 | public class ConnectRequest extends Message implements IDontFrame { 28 | 29 | /** Dummy message only used to retrieve the last reliable id. */ 30 | public static final Message DUMMY = new Message() { 31 | @Override 32 | public ReliableMode getReliableMode() { 33 | return ReliableMode.SEQUENCE_NUMBER; 34 | } 35 | }; 36 | 37 | @Setter @Getter 38 | int clientId; 39 | 40 | public ConnectRequest() {} 41 | 42 | public ConnectRequest(int clientId) { 43 | this.clientId = clientId; 44 | } 45 | 46 | @Override 47 | public ReliableMode getReliableMode() { 48 | return ReliableMode.ACK_PACKET; 49 | } 50 | 51 | @Override 52 | public void process(Object context) { 53 | final ConnectResponse connectResponse = new ConnectResponse(getMsgId(), clientId); 54 | DUMMY.setReceiverId(clientId); 55 | connectResponse.lastReliableSeqId = getState().idProvider.getLastIdFor(DUMMY); 56 | putLastAckMessageIdIntoStackedMessageProcessor(connectResponse.lastReliableSeqId); 57 | log.info("Last reliable ID: {} - send to {}", connectResponse.lastReliableSeqId, clientId); 58 | connectResponse.setReceiverId(clientId); 59 | getConfig().internalSender.send(connectResponse); 60 | } 61 | 62 | private void putLastAckMessageIdIntoStackedMessageProcessor(long lastReliableSeqId) { 63 | StackedMessageProcessor stackedMessageProcessor = getState().getProcessorOf(StackedMessageProcessor.class); 64 | stackedMessageProcessor.getLastAckMessageIdMap().put(clientId, lastReliableSeqId); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/test/java/com/jfastnet/messages/features/ChecksumFeatureTest.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2018 Klaus Pfeiffer - klaus@allpiper.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | ******************************************************************************/ 16 | 17 | package com.jfastnet.messages.features; 18 | 19 | import com.jfastnet.AbstractTest; 20 | import com.jfastnet.messages.Message; 21 | import lombok.Getter; 22 | import lombok.extern.slf4j.Slf4j; 23 | import org.junit.Test; 24 | 25 | import static org.hamcrest.MatcherAssert.assertThat; 26 | import static org.hamcrest.Matchers.instanceOf; 27 | import static org.junit.Assert.assertNotEquals; 28 | import static org.junit.Assert.assertNotNull; 29 | 30 | /** @author Klaus Pfeiffer - klaus@allpiper.com */ 31 | @Slf4j 32 | public class ChecksumFeatureTest extends AbstractTest { 33 | 34 | static { 35 | log.info("ChecksumFeatureTest class is being loaded."); 36 | } 37 | 38 | public static final long V1 = 52353254L; 39 | public static final long V2 = 9654787L; 40 | 41 | public static class ChecksumTestMsg extends Message { 42 | long v1 = V1; 43 | long v2 = V2; 44 | 45 | @Getter 46 | MessageFeatures features = new MessageFeatures(); 47 | 48 | public ChecksumTestMsg() { 49 | getFeatures().add(new ChecksumFeature()); 50 | } 51 | } 52 | 53 | @Test 54 | public void testChecksum() { 55 | start(); 56 | 57 | server.send(new ChecksumTestMsg()); 58 | 59 | waitForCondition("No message received.", 1, () -> allClientsReceivedMessageTypeOf(ChecksumTestMsg.class)); 60 | 61 | Message message = getLastReceivedMessage(0); 62 | 63 | assertThat("Message is of wrong type.", message, instanceOf(ChecksumTestMsg.class)); 64 | assertNotNull("Message is null.", message); 65 | assertNotNull("Features are null.", message.getFeatures()); 66 | 67 | ChecksumFeature checksumFeature = message.getFeatures().get(ChecksumFeature.class); 68 | assertNotNull("Checksum feature is null.", checksumFeature); 69 | 70 | assertNotEquals("Checksum may not be 0.", checksumFeature.getCrcValue(), 0); 71 | //assertTrue("Wrong checksum, but this test should have failed earlier!", checksumFeature.check(message)); 72 | } 73 | 74 | } -------------------------------------------------------------------------------- /src/main/java/com/jfastnet/state/NetworkQuality.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2018 Klaus Pfeiffer - klaus@allpiper.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | ******************************************************************************/ 16 | 17 | package com.jfastnet.state; 18 | 19 | import com.jfastnet.Config; 20 | import com.jfastnet.peers.CongestionControl; 21 | import com.jfastnet.processors.ReliableModeSequenceProcessor; 22 | import lombok.ToString; 23 | 24 | import java.util.SortedSet; 25 | import java.util.TreeSet; 26 | 27 | /** @author Klaus Pfeiffer - klaus@allpiper.com */ 28 | @ToString(of = "qualityFactor") 29 | public class NetworkQuality { 30 | 31 | /** 1f => Best, 0f => Worst quality */ 32 | public float qualityFactor = 1f; 33 | 34 | private int countRequestedMessages = 0; 35 | private SortedSet missingMessageTimestamps = new TreeSet<>(); 36 | 37 | private final Config config; 38 | private final long consideredTimeFrameInMs; 39 | 40 | public NetworkQuality(Config config) { 41 | this.config = config; 42 | CongestionControl.CongestionControlConfig congestionControlConfig = config.getAdditionalConfig(CongestionControl.CongestionControlConfig.class); 43 | consideredTimeFrameInMs = congestionControlConfig.consideredTimeFrameInMs; 44 | } 45 | 46 | public void requestedMissingMessages(int size, long timeStamp) { 47 | countRequestedMessages += size; 48 | for (int i = 0; i < size; i++) { 49 | missingMessageTimestamps.add(timeStamp); 50 | } 51 | } 52 | 53 | void calculateQuality() { 54 | final long currentTimestamp = config.timeProvider.get(); 55 | missingMessageTimestamps.removeIf(timestamp -> timestamp < currentTimestamp - consideredTimeFrameInMs); 56 | int missingMessagesInTimeFrame = missingMessageTimestamps.size(); 57 | ReliableModeSequenceProcessor.ProcessorConfig processorConfig = config.getAdditionalConfig(ReliableModeSequenceProcessor.ProcessorConfig.class); 58 | float maximumNumberOfRequestedMessages = consideredTimeFrameInMs / (float) processorConfig.requestMissingIdsIntervalMs * processorConfig.maximumMissingIdsRequestCount; 59 | float missingFactor = Math.min(1f, missingMessagesInTimeFrame / maximumNumberOfRequestedMessages); 60 | this.qualityFactor = 1f - missingFactor; 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/com/jfastnet/peers/netty/FutureGenericFutureListener.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2018 Klaus Pfeiffer - klaus@allpiper.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | ******************************************************************************/ 16 | 17 | package com.jfastnet.peers.netty; 18 | 19 | import com.jfastnet.messages.Message; 20 | import io.netty.buffer.ByteBuf; 21 | import io.netty.util.concurrent.Future; 22 | import io.netty.util.concurrent.GenericFutureListener; 23 | import lombok.Setter; 24 | import lombok.experimental.Accessors; 25 | import lombok.extern.slf4j.Slf4j; 26 | 27 | /** @author Klaus Pfeiffer - klaus@allpiper.com */ 28 | @Slf4j 29 | @Accessors(chain = true) 30 | public class FutureGenericFutureListener implements GenericFutureListener> { 31 | 32 | String action; 33 | Object callerObj; 34 | Object messageObj; 35 | 36 | @Setter 37 | String msgType; 38 | 39 | public FutureGenericFutureListener() {} 40 | 41 | public FutureGenericFutureListener(String msgType) { 42 | this.msgType = msgType; 43 | } 44 | 45 | public FutureGenericFutureListener(String action, Object callerObj, Object messageObj) { 46 | this.action = action; 47 | this.callerObj = callerObj; 48 | this.messageObj = messageObj; 49 | } 50 | 51 | @Override 52 | public void operationComplete(final Future future) throws Exception { 53 | if (!future.isSuccess()) { 54 | if (callerObj != null) { 55 | log.error( 56 | "Operation failed for '{}'. Caller: {}, Message: {}, toString: {}", 57 | new Object[] { action, callerObj.getClass().getSimpleName(), messageObj.getClass().getSimpleName(), 58 | messageObj.toString(), future.cause() } 59 | ); 60 | if (messageObj instanceof Message) { 61 | Message message = (Message) messageObj; 62 | if (message.payload instanceof ByteBuf) { 63 | ByteBuf payload = (ByteBuf) message.payload; 64 | int writerIndex = payload.writerIndex(); 65 | log.error("Size of failed message: {}", writerIndex); 66 | } 67 | } 68 | } else if (msgType == null) { 69 | log.error("Operation failed", future.cause()); 70 | } else { 71 | log.error("Operation failed for {}", msgType, future.cause()); 72 | } 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/main/java/com/jfastnet/idprovider/ClientIdReliableModeIdProvider.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2018 Klaus Pfeiffer - klaus@allpiper.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | ******************************************************************************/ 16 | 17 | package com.jfastnet.idprovider; 18 | 19 | import com.jfastnet.messages.Message; 20 | import com.jfastnet.util.NullsafeHashMap; 21 | 22 | import java.util.Map; 23 | import java.util.concurrent.atomic.AtomicLong; 24 | 25 | /** Attention! This won't work with Lockstepping, because the 26 | * lockstepping impl requires the same id for messages on all clients. 27 | * @author Klaus Pfeiffer - klaus@allpiper.com */ 28 | public class ClientIdReliableModeIdProvider implements IIdProvider { 29 | 30 | private Map> idMap = new NullsafeHashMap>() { 31 | @Override 32 | protected Map newInstance() { 33 | return new NullsafeHashMap() { 34 | @Override 35 | protected AtomicLong newInstance() { 36 | return new AtomicLong(); 37 | } 38 | }; 39 | } 40 | }; 41 | 42 | @Override 43 | public long createIdFor(Message message) { 44 | return idMap.get(message.getReceiverId()).get(message.getReliableMode()).incrementAndGet(); 45 | } 46 | 47 | @Override 48 | public long getLastIdFor(Message message) { 49 | return idMap.get(message.getReceiverId()).get(message.getReliableMode()).get(); 50 | } 51 | 52 | @Override 53 | public long stepBack(Message message) { 54 | return idMap.get(message.getReceiverId()).get(message.getReliableMode()).decrementAndGet(); 55 | } 56 | 57 | @Override 58 | public int compare(Message m1, Message m2) { 59 | 60 | // compare by player id first 61 | int compare = Integer.compare(m1.getReceiverId(), m2.getReceiverId()); 62 | if (compare != 0) return compare; 63 | 64 | // compare by reliable mode 65 | compare = Integer.compare(m1.getReliableMode().ordinal(), m2.getReliableMode().ordinal()); 66 | if (compare != 0) return compare; 67 | 68 | // by id 69 | compare = Long.compare(m1.getMsgId(), m2.getMsgId()); 70 | if (compare != 0) return compare; 71 | 72 | return compare; 73 | } 74 | 75 | @Override 76 | public boolean resolveEveryClientMessage() { 77 | return true; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JFastNet 2 | Fast, reliable and easy UDP messaging for Java. Designed for games. 3 | 4 | JFastNet is tolerant towards packet loss and when used in the right way it can 5 | provide your players with a smooth network gaming experience even in bad network conditions. 6 | 7 | _The API of this library is subject to change._ 8 | 9 | # Maven 10 | 11 | The dependency for your POM: 12 | ```xml 13 | 14 | com.jfastnet 15 | jfastnet 16 | 0.3.8 17 | 18 | ``` 19 | 20 | # Example code 21 | The following code shows the important parts of a server-client communication: 22 | ```java 23 | Server server = new Server(new Config().setBindPort(15150)); 24 | Client client = new Client(new Config().setPort(15150)); 25 | 26 | server.start(); 27 | client.start(); 28 | client.blockingWaitUntilConnected(); 29 | 30 | server.send(new PrintMessage("Hello Client!")); 31 | client.send(new PrintMessage("Hello Server!")); 32 | ``` 33 | [Click to see full sample code of HelloWorld.java](https://github.com/klaus7/jfastnet/blob/master/src/test/java/com/jfastnet/examples/HelloWorld.java) 34 | 35 | # Roadmap 36 | 37 | * More documentation 38 | 39 | # Documentation 40 | 41 | A comprehensive documentation can be found in the [DOCUMENTATION.md](DOCUMENTATION.md) file. 42 | 43 | The documentation is still a work-in-progress. 44 | 45 | The most important classes to look for in the beginning are the `Config` and the `Message` class. The JavaDoc there should provide you with the basic configuration possibilities of the library. 46 | 47 | ## Reliable sending 48 | There are currently two ways you can use to send a message in a reliable way. Sending the message unreliably is of course also an option. 49 | 50 | 1. Acknowledge packet 51 | 2. Sequence number 52 | 53 | ### Acknowledge packet 54 | The receiver of a message with reliable mode set to `ACK_PACKET` will send an acknowledge packet to the other end upon receipt of the message. 55 | As long as the sender of the prior mentioned message doesn't receive an acknowledge packet it will keep resending the message. 56 | 57 | Attribute | Value 58 | --------- |:---: 59 | Reliable | yes 60 | Ordered | no 61 | 62 | ### Sequence number 63 | The receiver of a message with reliable mode set to `SEQUENCE_NUMBER` will do nothing as long as the messages arrive in the expected order. 64 | But if a message with an id greater than expected is received, the receiver will stop processing the messages and send a `RequestSeqIdsMessage` to the other end. 65 | Processing will not continue until all required messages are received. 66 | 67 | Attribute | Value 68 | --------- |:---: 69 | Reliable | yes 70 | Ordered | yes 71 | 72 | It's usually advisable to use sequence numbers, as there will be less overhead and also the ordered delivery is guaranteed. 73 | 74 | # Build 75 | Use maven to build JFastNet: 76 | ```bash 77 | mvn clean install 78 | ``` 79 | 80 | # Thanks 81 | [Kryo](https://github.com/EsotericSoftware/kryo) is the default serialiser used in JFastNet and is a pleasure to work with! Thanks very much for this awesome library! 82 | 83 | [Project Lombok](https://projectlombok.org/) also deserves a mention, as it makes working with Java much more comfortable and the code looks cleaner. Check it out if you don't have already. 84 | 85 | # Contact 86 | Post issues to [the issues page](https://github.com/klaus7/jfastnet/issues) or contact me via email at [support@jfastnet.com](mailto:support@jfastnet.com) for other inquiries. 87 | -------------------------------------------------------------------------------- /src/test/java/com/jfastnet/MessageLogTest.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2018 Klaus Pfeiffer - klaus@allpiper.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | ******************************************************************************/ 16 | 17 | package com.jfastnet; 18 | 19 | import com.jfastnet.messages.Message; 20 | import com.jfastnet.processors.MessageLogProcessor; 21 | import org.junit.Test; 22 | 23 | import static org.hamcrest.MatcherAssert.assertThat; 24 | import static org.hamcrest.Matchers.is; 25 | import static org.hamcrest.Matchers.notNullValue; 26 | import static org.junit.Assert.assertNotNull; 27 | 28 | /** @author Klaus Pfeiffer - klaus@allpiper.com */ 29 | public class MessageLogTest { 30 | 31 | private volatile int i; 32 | private volatile int j; 33 | 34 | static class NumberMessage extends Message { 35 | int number; 36 | public NumberMessage(int number) { 37 | this.number = number; 38 | } 39 | } 40 | 41 | @Test 42 | public void testMessageLog() throws InterruptedException { 43 | Config config = new Config(); 44 | State state = new State(config); 45 | MessageLogProcessor.ProcessorConfig processorConfig = new MessageLogProcessor.ProcessorConfig(); 46 | MessageLog messageLog = new MessageLog(config, processorConfig); 47 | processorConfig.messageLogReceiveFilter = new MessageLog.ReliableMessagesPredicate(); 48 | assertThat(messageLog.getReceived().size(), is(0)); 49 | messageLog.addReceived(new Message() { 50 | @Override 51 | public ReliableMode getReliableMode() { 52 | return ReliableMode.UNRELIABLE; 53 | } 54 | }); 55 | assertThat(messageLog.getReceived().size(), is(0)); 56 | messageLog.addReceived(new Message() { 57 | @Override 58 | public ReliableMode getReliableMode() { 59 | return ReliableMode.SEQUENCE_NUMBER; 60 | } 61 | }); 62 | assertThat(messageLog.getReceived().size(), is(1)); 63 | 64 | Thread t1 = new Thread(() -> { 65 | while (true) { 66 | if (i < j + config.getAdditionalConfig(MessageLogProcessor.ProcessorConfig.class).getSentMessagesMapLimit() - 1500) { 67 | i++; 68 | NumberMessage msg = new NumberMessage(i); 69 | msg.resolve(config, state); 70 | msg.resolveId(); 71 | messageLog.addSent(msg); 72 | } 73 | } 74 | }); 75 | t1.start(); 76 | 77 | int count = 4000; 78 | // Wait for count number to be generated 79 | while (i < count); 80 | j = 3000; 81 | while (j < count) { 82 | j++; 83 | while (i < j + 1000); 84 | 85 | Message msg; 86 | 87 | msg = messageLog.getSent(MessageKey.newKey(Message.ReliableMode.SEQUENCE_NUMBER, 0, j)); 88 | assertNotNull("msg id: " + j + ", i=" + i, msg); 89 | assertThat(msg.getMsgId(), is((long) j)); 90 | 91 | int offset = 500; 92 | msg = messageLog.getSent(MessageKey.newKey(Message.ReliableMode.SEQUENCE_NUMBER, 0, j - offset)); 93 | assertThat(msg, is(notNullValue())); 94 | assertThat(msg.getMsgId(), is((long) j - offset)); 95 | } 96 | t1.interrupt(); 97 | } 98 | } -------------------------------------------------------------------------------- /src/main/java/com/jfastnet/messages/ConnectResponse.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2018 Klaus Pfeiffer - klaus@allpiper.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | ******************************************************************************/ 16 | 17 | package com.jfastnet.messages; 18 | 19 | import com.jfastnet.Config; 20 | import com.jfastnet.processors.ReliableModeSequenceProcessor; 21 | import com.jfastnet.processors.StackedMessageProcessor; 22 | import lombok.Getter; 23 | import lombok.extern.slf4j.Slf4j; 24 | 25 | import java.util.Map; 26 | import java.util.concurrent.atomic.AtomicLong; 27 | 28 | /** Sent from the server to the client to confirm the connection. 29 | * @author Klaus Pfeiffer - klaus@allpiper.com */ 30 | @Slf4j 31 | public class ConnectResponse extends Message implements IDontFrame, IInstantProcessable, IAckMessage { 32 | 33 | /** Last used reliable sequence id on the server. */ 34 | long lastReliableSeqId; 35 | 36 | private long connectRequestMsgId; 37 | 38 | /** The client id is sent back from the server to the client. If it was 0 39 | * before, the server assigend a new id. */ 40 | @Getter 41 | int clientId; 42 | 43 | /** no-arg constructor required for serialization. */ 44 | private ConnectResponse() {} 45 | 46 | public ConnectResponse(long connectRequestMsgId, int clientId) { 47 | this.connectRequestMsgId = connectRequestMsgId; 48 | this.clientId = clientId; 49 | } 50 | 51 | @Override 52 | public ReliableMode getReliableMode() { 53 | return ReliableMode.ACK_PACKET; 54 | } 55 | 56 | @Override 57 | public void ackCallback() { 58 | Config config = getConfig(); 59 | if (config.expectedClientIds.isEmpty() || config.expectedClientIds.contains(clientId)) { 60 | config.requiredClients.put(clientId, false); 61 | } 62 | config.serverHooks.onRegister(clientId); 63 | } 64 | 65 | /** Called on receit of message. Message#process() would be called too late. */ 66 | public void setLastReliableSeqIdInSequenceProcessor() { 67 | log.info("Connection established! Last reliable sequence id is {}", lastReliableSeqId); 68 | setInSequenceProcessor(); 69 | setInStackedMessageProcessor(); 70 | } 71 | 72 | private void setInSequenceProcessor() { 73 | final Map lastMessageIdMap = getState().getProcessorOf(ReliableModeSequenceProcessor.class).getLastMessageIdMap(); 74 | AtomicLong lastId = lastMessageIdMap.get(getSenderId()); 75 | if (lastId == null || lastId.get() == 0L) { 76 | lastMessageIdMap.put(getSenderId(), new AtomicLong(lastReliableSeqId)); 77 | log.info(" * Last reliable sequence id set to {}", lastReliableSeqId); 78 | } else { 79 | log.warn(" * Last reliable sequence id was already set to {}", lastId); 80 | } 81 | } 82 | 83 | private void setInStackedMessageProcessor() { 84 | StackedMessageProcessor stackedMessageProcessor = getState().getProcessorOf(StackedMessageProcessor.class); 85 | stackedMessageProcessor.setMyLastAckMessageId(lastReliableSeqId); 86 | } 87 | 88 | @Override 89 | public long getAckId() { 90 | return connectRequestMsgId; 91 | } 92 | 93 | } 94 | -------------------------------------------------------------------------------- /src/main/java/com/jfastnet/messages/RequestSeqIdsMessage.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2018 Klaus Pfeiffer - klaus@allpiper.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | ******************************************************************************/ 16 | 17 | package com.jfastnet.messages; 18 | 19 | import com.jfastnet.MessageKey; 20 | import com.jfastnet.MessageLog; 21 | import com.jfastnet.events.RequestedMessageNotInLogEvent; 22 | import com.jfastnet.processors.MessageLogProcessor; 23 | import com.jfastnet.state.ClientState; 24 | import lombok.extern.slf4j.Slf4j; 25 | 26 | import java.util.ArrayList; 27 | import java.util.Arrays; 28 | import java.util.List; 29 | 30 | /** This message is used to request missing sequenced ids from the other side. 31 | * @author Klaus Pfeiffer - klaus@allpiper.com */ 32 | @Slf4j 33 | public class RequestSeqIdsMessage extends Message implements IDontFrame { 34 | 35 | /** List of sequence message ids we require. */ 36 | private List missingIds = new ArrayList<>(); 37 | 38 | /** no-arg constructor required for serialization. */ 39 | private RequestSeqIdsMessage() {} 40 | 41 | public RequestSeqIdsMessage(List missingIds, int receiverId) { 42 | this.missingIds = missingIds; 43 | setReceiverId(receiverId); 44 | log.info("Request absent-Ids from {}: {}", receiverId, Arrays.toString(missingIds.toArray())); 45 | } 46 | 47 | @Override 48 | public ReliableMode getReliableMode() { 49 | // If this message gets lost, the ids will be requested again from the other side. 50 | return ReliableMode.UNRELIABLE; 51 | } 52 | 53 | @Override 54 | public void process(Object context) { 55 | int senderId = getSenderId(); 56 | log.info("Resend absent ids: {} to {}", Arrays.toString(missingIds.toArray()), senderId); 57 | 58 | int keySenderId = senderId; 59 | if (!getState().idProvider.resolveEveryClientMessage()) { 60 | // Clear sender id, if every client receives the same id for a particular message 61 | keySenderId = 0; 62 | } 63 | degradeNetworkQuality(); 64 | MessageLog messageLog = getState().getProcessorOf(MessageLogProcessor.class).getMessageLog(); 65 | for (Long absentId : missingIds) { 66 | MessageKey key = MessageKey.newKey(Message.ReliableMode.SEQUENCE_NUMBER, keySenderId, absentId); 67 | Message message = messageLog.getSent(key); 68 | if (message == null) { 69 | log.error("Requested message {} not in log.", key); 70 | getState().getEventLog().add(new RequestedMessageNotInLogEvent(key, senderId)); 71 | continue; 72 | } 73 | message.setReceiverId(senderId); 74 | message.setResendMessage(true); 75 | log.info("Resend {} to {}", message, senderId); 76 | getConfig().internalSender.send(message); 77 | getConfig().netStats.resentMessages.incrementAndGet(); 78 | } 79 | } 80 | 81 | private void degradeNetworkQuality() { 82 | // If other side requests missing messages, it means we should slow down sending. 83 | // Thus we degrade the network quality to this peer. 84 | ClientState clientState = getState().getClientStates().getById(getSenderId()); 85 | if (clientState != null) { 86 | clientState.getNetworkQuality().requestedMissingMessages(missingIds.size(), getConfig().timeProvider.get()); 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/main/java/com/jfastnet/state/ClientStates.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2018 Klaus Pfeiffer - klaus@allpiper.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | ******************************************************************************/ 16 | 17 | package com.jfastnet.state; 18 | 19 | import com.jfastnet.Config; 20 | import com.jfastnet.ISimpleProcessable; 21 | import lombok.Getter; 22 | import lombok.ToString; 23 | 24 | import java.net.InetSocketAddress; 25 | import java.util.Comparator; 26 | import java.util.Map; 27 | import java.util.Optional; 28 | import java.util.Set; 29 | import java.util.concurrent.ConcurrentHashMap; 30 | 31 | /** @author Klaus Pfeiffer - klaus@allpiper.com */ 32 | @ToString 33 | public class ClientStates implements ISimpleProcessable { 34 | 35 | private final ClientState emptyClientState; 36 | private final Config config; 37 | 38 | private Map clientAddressMap = new ConcurrentHashMap<>(); 39 | @Getter 40 | private Map clientStateMap = new ConcurrentHashMap<>(); 41 | 42 | public ClientStates(Config config) { 43 | this.config = config; 44 | emptyClientState = new ClientState(config); 45 | } 46 | 47 | public ClientState getById(int id) { 48 | return clientStateMap.getOrDefault(id, emptyClientState); 49 | } 50 | 51 | public boolean hasAddress(InetSocketAddress socketAddress) { 52 | return clientAddressMap.containsValue(socketAddress); 53 | } 54 | 55 | public ClientState getBySocketAddress(InetSocketAddress socketAddress) { 56 | Integer id = getIdBySocketAddress(socketAddress); 57 | if (id != null) { 58 | return getById(id); 59 | } 60 | return null; 61 | } 62 | 63 | public Integer getIdBySocketAddress(InetSocketAddress socketAddress) { 64 | Optional> socketAddressOptional = clientAddressMap.entrySet().stream() 65 | .filter(entry -> entry.getValue().equals(socketAddress)) 66 | .findFirst(); 67 | if (socketAddressOptional.isPresent()) { 68 | return socketAddressOptional.get().getKey(); 69 | } 70 | return null; 71 | } 72 | 73 | public int newClientId() { 74 | Integer maximumId = clientStateMap.keySet().stream().max(Comparator.naturalOrder()).orElse(0); 75 | int clientId = maximumId == null ? 1 : maximumId + 1; 76 | return clientId; 77 | } 78 | 79 | public boolean hasId(int clientId) { 80 | return clientStateMap.containsKey(clientId); 81 | } 82 | 83 | public void put(int clientId, InetSocketAddress socketAddress) { 84 | clientAddressMap.put(clientId, socketAddress); 85 | clientStateMap.put(clientId, new ClientState(config, socketAddress)); 86 | } 87 | 88 | public int size() { 89 | return clientStateMap.size(); 90 | } 91 | 92 | public Set> addressEntrySet() { 93 | return clientAddressMap.entrySet(); 94 | } 95 | 96 | public void remove(int clientId) { 97 | clientAddressMap.remove(clientId); 98 | clientStateMap.remove(clientId); 99 | } 100 | 101 | public Set idSet() { 102 | return clientStateMap.keySet(); 103 | } 104 | 105 | @Override 106 | public void process() { 107 | clientStateMap.forEach((id, clientState) -> clientState.getNetworkQuality().calculateQuality()); 108 | } 109 | 110 | } 111 | -------------------------------------------------------------------------------- /src/main/java/com/jfastnet/NetStats.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2018 Klaus Pfeiffer - klaus@allpiper.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | ******************************************************************************/ 16 | 17 | package com.jfastnet; 18 | 19 | import lombok.extern.slf4j.Slf4j; 20 | 21 | import java.io.FileWriter; 22 | import java.text.DateFormat; 23 | import java.text.SimpleDateFormat; 24 | import java.util.ArrayList; 25 | import java.util.Date; 26 | import java.util.List; 27 | import java.util.concurrent.atomic.AtomicLong; 28 | 29 | /** Statistics about the network traffic. 30 | * @author Klaus Pfeiffer - klaus@allpiper.com */ 31 | @Slf4j 32 | public class NetStats { 33 | public static final DateFormat FILE_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd_HH-mm-ss_SSS"); 34 | 35 | private static final String CSV_SEPARATOR = ";"; 36 | 37 | private List data = new ArrayList<>(); 38 | 39 | /** Reliable sequence resent messages. */ 40 | public AtomicLong resentMessages = new AtomicLong(); 41 | 42 | /** Reliable sequence sent messages. */ 43 | public AtomicLong sentMessages = new AtomicLong(); 44 | public AtomicLong requestedMissingMessages = new AtomicLong(); 45 | 46 | public synchronized List getData() { 47 | return data; 48 | } 49 | 50 | public void clear() { 51 | data = new ArrayList<>(); 52 | } 53 | 54 | public void writeLog() { 55 | log.info("Sent sequenced messages: {}", sentMessages); 56 | log.info("Resent sequenced messages: {}", resentMessages); 57 | log.info("Requested missing messages: {}", requestedMissingMessages); 58 | log.info("Lost percentage: {} %", String.format("%.2f", ((float) resentMessages.get() / sentMessages.get()) * 100)); 59 | } 60 | 61 | public void write() { 62 | String filename = "netstats-" + FILE_DATE_FORMAT.format(new Date()) + ".csv"; 63 | log.info("Write netstats to {}", filename); 64 | try { 65 | FileWriter fw = new FileWriter(filename); 66 | // write header 67 | fw.write("sent;timestamp;frame;clientid;class;size"); 68 | fw.write("\n"); 69 | synchronized (data) { 70 | for (Line line : new ArrayList<>(getData())) { 71 | fw.write(String.valueOf(line.sent)); 72 | fw.write(CSV_SEPARATOR); 73 | fw.write(String.valueOf(line.timestamp)); 74 | fw.write(CSV_SEPARATOR); 75 | fw.write(String.valueOf(line.frame)); 76 | fw.write(CSV_SEPARATOR); 77 | fw.write(String.valueOf(line.clientId)); 78 | fw.write(CSV_SEPARATOR); 79 | fw.write(line.clazz.getSimpleName()); 80 | fw.write(CSV_SEPARATOR); 81 | fw.write(String.valueOf(line.byteBufSize)); 82 | fw.write("\n"); 83 | } 84 | } 85 | fw.flush(); 86 | } catch (Exception e) { 87 | log.error("Couldn't write netstats file.", e); 88 | } 89 | } 90 | 91 | public static class Line { 92 | public boolean sent; 93 | public int clientId; 94 | public int frame; 95 | public long timestamp; 96 | public Class clazz; 97 | public int byteBufSize; 98 | 99 | public Line(boolean sent, int clientId, int frame, long timestamp, Class clazz, int byteBufSize) { 100 | this.sent = sent; 101 | this.clientId = clientId; 102 | this.frame = frame; 103 | this.timestamp = timestamp; 104 | this.clazz = clazz; 105 | this.byteBufSize = byteBufSize; 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/test/java/com/jfastnet/messages/MessagePartTest.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2018 Klaus Pfeiffer - klaus@allpiper.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | ******************************************************************************/ 16 | 17 | package com.jfastnet.messages; 18 | 19 | import com.jfastnet.AbstractTest; 20 | import com.jfastnet.Config; 21 | import com.jfastnet.State; 22 | import lombok.extern.slf4j.Slf4j; 23 | import org.junit.Test; 24 | 25 | import java.util.List; 26 | import java.util.Random; 27 | 28 | import static org.junit.Assert.*; 29 | 30 | /** @author Klaus Pfeiffer - klaus@allpiper.com */ 31 | @Slf4j 32 | public class MessagePartTest extends AbstractTest { 33 | 34 | static class BigMessage extends Message { 35 | String s; 36 | 37 | public BigMessage(int lines) { 38 | Random r = new Random(System.currentTimeMillis()); 39 | StringBuilder sb = new StringBuilder(); 40 | for (int i = 0; i < lines; i++) { 41 | //sb.append("asdfasdfasdfasdasdfadfasdsadfa"); // 30 chars 42 | for (int j = 0; j < 15; j++) { 43 | sb.append((char) r.nextInt()); 44 | } 45 | } 46 | // 1 char UTF-8 encoded in ASCII space consumes 1 byte of memory 47 | // payload size = 50 * 30 byte = 1500 byte 48 | sb.append("END"); 49 | // + 3 = 1503 byte 50 | s = sb.toString(); 51 | } 52 | 53 | @Override 54 | public ReliableMode getReliableMode() { 55 | return ReliableMode.ACK_PACKET; 56 | } 57 | 58 | @Override 59 | public void process(Object context) {} 60 | } 61 | 62 | 63 | boolean bigMsgReceived = false; 64 | @Test 65 | public void testPartsReceived() { 66 | 67 | Config config = newClientConfig(); 68 | config.externalReceiver = message -> { 69 | if (message instanceof BigMessage) { 70 | BigMessage bigMessage = (BigMessage) message; 71 | bigMsgReceived = true; 72 | } 73 | }; 74 | BigMessage bigMessage = new BigMessage(150); 75 | bigMessage.setConfig(config); 76 | State state = new State(config); 77 | bigMessage.setState(state); 78 | List messageParts = MessagePart.createFromMessage(state, 0, bigMessage, 1024, Message.ReliableMode.ACK_PACKET); 79 | 80 | assertNotNull("message parts are null", messageParts); 81 | messageParts.forEach(messagePart -> messagePart.copyAttributesFrom(bigMessage)); 82 | MessagePart lastPart = messageParts.get(messageParts.size() - 1); 83 | assertTrue("last messag part must have set last flag to true", lastPart.last); 84 | 85 | lastPart.process(null); 86 | assertFalse(bigMsgReceived); 87 | 88 | messageParts.forEach(messagePart -> { 89 | if (messagePart.partNumber % 2 == 0) messagePart.process(null); 90 | }); 91 | assertFalse(bigMsgReceived); 92 | 93 | messageParts.forEach(messagePart -> { 94 | if (messagePart.partNumber % 2 == 1) messagePart.process(null); 95 | }); 96 | assertTrue(bigMsgReceived); 97 | } 98 | 99 | @Test 100 | public void testCompression() { 101 | String input = "This is a compression test"; 102 | byte[] bytes = input.getBytes(); 103 | byte[] compressed = MessagePart.compress(bytes); 104 | assertTrue("Check for compression failed.", MessagePart.isCompressed(compressed)); 105 | byte[] decompressed = MessagePart.decompress(compressed); 106 | assertFalse("Check for compression failed.", MessagePart.isCompressed(decompressed)); 107 | String decompressedString = new String(decompressed); 108 | log.info("Decompressed message: " + decompressedString); 109 | assertEquals(input, decompressedString); 110 | } 111 | 112 | } -------------------------------------------------------------------------------- /src/main/java/com/jfastnet/messages/TimerSyncMessage.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2018 Klaus Pfeiffer - klaus@allpiper.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | ******************************************************************************/ 16 | 17 | package com.jfastnet.messages; 18 | 19 | import com.jfastnet.ISimpleProcessable; 20 | import com.jfastnet.messages.features.MessageFeatures; 21 | import com.jfastnet.messages.features.TimestampFeature; 22 | import lombok.Getter; 23 | import lombok.Setter; 24 | import lombok.extern.slf4j.Slf4j; 25 | 26 | import java.util.HashMap; 27 | import java.util.Map; 28 | 29 | /** This message is sent from the server to the client and then back to the 30 | * server. It is used to measure the round trip time. 31 | * @author Klaus Pfeiffer - klaus@allpiper.com */ 32 | @Slf4j 33 | public class TimerSyncMessage extends Message implements IInstantServerProcessable, IDontFrame { 34 | 35 | /* -- */ 36 | public static volatile boolean started = false; 37 | public static ISimpleProcessable initiateSynchronousStart; 38 | private static final Object checkStartedLock = new Object(); 39 | /* -- */ 40 | 41 | /** Client ID returning the sync request. */ 42 | private int clientId; 43 | 44 | /** System timestamp when last sync action was requested. Only set by server. */ 45 | @Setter @Getter 46 | private static long lastTimestamp; 47 | 48 | @Getter 49 | private static Map roundTrip = new HashMap<>(); 50 | 51 | /** System playerIndex plus timestampDifferenceToHost equals host time. */ 52 | @Getter 53 | private static Map timestampDifferenceToHost = new HashMap<>(); 54 | 55 | @Getter 56 | private MessageFeatures features = new MessageFeatures(); 57 | 58 | /** no-arg constructor required for serialization. */ 59 | private TimerSyncMessage() {} 60 | 61 | public TimerSyncMessage(int clientId) { 62 | this.clientId = clientId; 63 | getFeatures().add(new TimestampFeature()); 64 | } 65 | 66 | /* 67 | * IMPORTANT: the server processes this method! 68 | */ 69 | @Override 70 | public void process(Object context) { 71 | /* 72 | * The host sends out this action, which gets immediately returned (without processing) 73 | * by the clients and received by the server, 74 | * who then calculates the roundtrips for all clients. 75 | */ 76 | long retrievedTs = getTimestamp(); 77 | long sysTs = System.currentTimeMillis(); 78 | long roundTripTime = sysTs - lastTimestamp; 79 | 80 | log.trace("RTT for player {}: {} ms", clientId, roundTripTime); 81 | roundTrip.put(clientId, roundTripTime); 82 | 83 | long offsetToHost = -(retrievedTs - sysTs - (roundTripTime / 2)); 84 | timestampDifferenceToHost.put(clientId, offsetToHost); 85 | 86 | // send information back to client, so he can compute actions accordingly 87 | ClientTimerSyncMessage clientTimerSyncMessage = new ClientTimerSyncMessage(offsetToHost, roundTripTime); 88 | clientTimerSyncMessage.setReceiverId(clientId); 89 | getConfig().internalSender.send(clientTimerSyncMessage); 90 | 91 | synchronized (checkStartedLock) { 92 | if (!started && getConfig().requiredClients.size() > 0) { 93 | Map requiredClients = getConfig().requiredClients; 94 | boolean allReady = true; 95 | for (Boolean ready : requiredClients.values()) { 96 | if (!ready) { 97 | allReady = false; 98 | break; 99 | } 100 | } 101 | if (allReady) { 102 | started = true; 103 | initiateSynchronousStart.process(); 104 | } 105 | } 106 | } 107 | } 108 | 109 | @Override 110 | public ReliableMode getReliableMode() { 111 | return ReliableMode.UNRELIABLE; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/main/java/com/jfastnet/peers/CongestionControl.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2018 Klaus Pfeiffer - klaus@allpiper.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | ******************************************************************************/ 16 | 17 | package com.jfastnet.peers; 18 | 19 | import com.jfastnet.ConfigStateContainer; 20 | import com.jfastnet.messages.Message; 21 | import com.jfastnet.state.ClientState; 22 | import com.jfastnet.state.NetworkQuality; 23 | import lombok.Getter; 24 | import lombok.Setter; 25 | import lombok.experimental.Accessors; 26 | import lombok.extern.slf4j.Slf4j; 27 | 28 | import java.net.InetSocketAddress; 29 | import java.util.ArrayDeque; 30 | import java.util.Queue; 31 | import java.util.function.Consumer; 32 | 33 | /** @author Klaus Pfeiffer - klaus@allpiper.com */ 34 | @Slf4j 35 | public class CongestionControl { 36 | 37 | private final ConfigStateContainer configStateContainer; 38 | private final Consumer packetSender; 39 | private final CongestionControlConfig congestionControlConfig; 40 | 41 | private final Queue packetQueue = new ArrayDeque<>(); 42 | 43 | private long delay; 44 | 45 | public CongestionControl(ConfigStateContainer configStateContainer, Consumer packetSender) { 46 | this.configStateContainer = configStateContainer; 47 | this.packetSender = packetSender; 48 | this.congestionControlConfig = configStateContainer.config.getAdditionalConfig(CongestionControlConfig.class); 49 | } 50 | 51 | public void send(Message message, T packet) { 52 | if (!configStateContainer.state.isHost() || message.isResendMessage()) { 53 | immediateSend(packet); 54 | return; 55 | } 56 | InetSocketAddress socketAddressRecipient = message.socketAddressRecipient; 57 | // Congestion Control only supported for server right now 58 | ClientState clientState = configStateContainer.state.getClientStates().getBySocketAddress(socketAddressRecipient); 59 | float qualityFactor = 1f; 60 | if (clientState != null) { 61 | NetworkQuality networkQuality = clientState.getNetworkQuality(); 62 | qualityFactor = networkQuality.qualityFactor; 63 | } 64 | 65 | if (qualityFactor >= congestionControlConfig.immediateSendQualityFactorThreshold && packetQueue.isEmpty()) { 66 | immediateSend(packet); 67 | delay = 0; 68 | } else { 69 | delay = (long) ((1f - qualityFactor) * congestionControlConfig.maximumDelayFactor); 70 | 71 | long sendTimeStamp; 72 | DelayedPacket lastDelayedPacket = packetQueue.peek(); 73 | if (lastDelayedPacket != null) { 74 | sendTimeStamp = lastDelayedPacket.sendTimeStamp + delay; 75 | } else { 76 | long currentTime = configStateContainer.config.timeProvider.get(); 77 | sendTimeStamp = currentTime + delay; 78 | } 79 | packetQueue.add(new DelayedPacket(sendTimeStamp, packet)); 80 | } 81 | } 82 | 83 | private void immediateSend(T packet) { 84 | packetSender.accept(packet); 85 | } 86 | 87 | public void process() { 88 | long currentTime = configStateContainer.config.timeProvider.get(); 89 | for (DelayedPacket delayedPacket = packetQueue.peek(); 90 | delayedPacket != null && delayedPacket.sendTimeStamp <= currentTime; 91 | delayedPacket = packetQueue.peek()) { 92 | 93 | immediateSend(delayedPacket.packet); 94 | packetQueue.poll(); 95 | } 96 | } 97 | 98 | @Setter 99 | @Getter 100 | @Accessors(chain = true) 101 | public static class CongestionControlConfig { 102 | public int maximumDelayFactor = 1000; 103 | public float immediateSendQualityFactorThreshold = 0.9f; 104 | /** Considered time frame for network quality. */ 105 | public int consideredTimeFrameInMs = 3000; 106 | } 107 | 108 | private class DelayedPacket { 109 | long sendTimeStamp; 110 | T packet; 111 | DelayedPacket(long sendTimeStamp, T packet) { 112 | this.sendTimeStamp = sendTimeStamp; 113 | this.packet = packet; 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/main/java/com/jfastnet/MessageLog.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2018 Klaus Pfeiffer - klaus@allpiper.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | ******************************************************************************/ 16 | 17 | package com.jfastnet; 18 | 19 | import com.jfastnet.messages.Message; 20 | import com.jfastnet.processors.MessageLogProcessor; 21 | import com.jfastnet.util.FifoMap; 22 | import lombok.Getter; 23 | import lombok.extern.slf4j.Slf4j; 24 | import org.apache.commons.collections4.queue.CircularFifoQueue; 25 | 26 | import java.util.concurrent.locks.Lock; 27 | import java.util.concurrent.locks.ReadWriteLock; 28 | import java.util.concurrent.locks.ReentrantReadWriteLock; 29 | import java.util.function.Predicate; 30 | 31 | /** Logs incoming and outgoing messages. Per default only reliable messages 32 | * get logged and an upper bound for the message log is used. 33 | * 34 | * The reliable sequence processor will grab required messages from the message 35 | * log. So don't change the filter unless you know what you're doing. 36 | * @author Klaus Pfeiffer - klaus@allpiper.com */ 37 | @Slf4j 38 | public class MessageLog { 39 | 40 | @Getter 41 | private CircularFifoQueue received; 42 | 43 | /** First-in, first-out map of sent messages. If a missing id is requested 44 | * by another peer, this map gets queried for it. */ 45 | private FifoMap sentMap; 46 | 47 | private final ReadWriteLock sentMapLock = new ReentrantReadWriteLock(); 48 | 49 | private Config config; 50 | 51 | private MessageLogProcessor.ProcessorConfig processorConfig; 52 | 53 | public MessageLog(Config config, MessageLogProcessor.ProcessorConfig processorConfig) { 54 | this.config = config; 55 | this.processorConfig = processorConfig; 56 | received = new CircularFifoQueue<>(processorConfig.receivedMessagesLimit); 57 | sentMap = new FifoMap<>(processorConfig.sentMessagesMapLimit); 58 | } 59 | 60 | public synchronized void addReceived(Message message) { 61 | if (processorConfig.messageLogReceiveFilter.test(message)) { 62 | received.add(message); 63 | } 64 | } 65 | 66 | public Message getSent(MessageKey key) { 67 | Lock readLock = sentMapLock.readLock(); 68 | readLock.lock(); 69 | try { 70 | return sentMap.get(key); 71 | } finally { 72 | readLock.unlock(); 73 | } 74 | } 75 | 76 | public void addSent(Message message) { 77 | if (!message.isResendMessage() && processorConfig.messageLogSendFilter.test(message)) { 78 | MessageKey messageKey = MessageKey.newKey(message.getReliableMode(), message.getReceiverId(), message.getMsgId()); 79 | Lock readLock = sentMapLock.readLock(); 80 | readLock.lock(); 81 | try { 82 | if (sentMap.containsKey(messageKey)) { 83 | //log.trace("Message already in map! Skipping!"); 84 | return; 85 | } 86 | } finally { 87 | readLock.unlock(); 88 | } 89 | Lock writeLock = sentMapLock.writeLock(); 90 | writeLock.lock(); 91 | try { 92 | log.trace("Put into sent-log: {} -- {}", messageKey, message); 93 | sentMap.put(messageKey, message); 94 | } finally { 95 | writeLock.unlock(); 96 | } 97 | } 98 | } 99 | 100 | /** All messages are logged. */ 101 | public static class AllMessagesPredicate implements Predicate { 102 | @Override 103 | public boolean test(Message message) { 104 | return true; 105 | } 106 | } 107 | 108 | /** No messages are logged. */ 109 | public static class NoMessagesPredicate implements Predicate { 110 | @Override 111 | public boolean test(Message message) { 112 | return false; 113 | } 114 | } 115 | 116 | /** Only reliable messages are logged. */ 117 | public static class ReliableMessagesPredicate implements Predicate { 118 | @Override 119 | public boolean test(Message message) { 120 | return !Message.ReliableMode.UNRELIABLE.equals(message.getReliableMode()); 121 | } 122 | } 123 | 124 | } 125 | -------------------------------------------------------------------------------- /src/main/java/com/jfastnet/serialiser/KryoSerialiser.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2018 Klaus Pfeiffer - klaus@allpiper.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | ******************************************************************************/ 16 | 17 | package com.jfastnet.serialiser; 18 | 19 | import com.esotericsoftware.kryo.Kryo; 20 | import com.esotericsoftware.kryo.io.Input; 21 | import com.esotericsoftware.kryo.io.Output; 22 | import com.jfastnet.config.SerialiserConfig; 23 | import com.jfastnet.messages.Message; 24 | import lombok.extern.slf4j.Slf4j; 25 | 26 | import java.io.EOFException; 27 | import java.io.IOException; 28 | import java.io.InputStream; 29 | import java.io.OutputStream; 30 | import java.util.zip.CRC32; 31 | import java.util.zip.DeflaterOutputStream; 32 | import java.util.zip.InflaterInputStream; 33 | 34 | /** Blazing fast kryo serialiser. Strongly recommended! 35 | * @author Klaus Pfeiffer - klaus@allpiper.com */ 36 | @Slf4j 37 | public class KryoSerialiser implements ISerialiser{ 38 | 39 | private final SerialiserConfig config; 40 | 41 | private ThreadLocal outputs = ThreadLocal.withInitial(() -> new Output(128, 1048576)); 42 | 43 | private ThreadLocal kryos; 44 | 45 | private Kryo kryo; 46 | 47 | private CRC32 crc = new CRC32(); 48 | 49 | public KryoSerialiser(SerialiserConfig config, ThreadLocal kryos) { 50 | this.config = config; 51 | this.kryos = kryos; 52 | } 53 | 54 | public KryoSerialiser(SerialiserConfig config, Kryo kryo) { 55 | this.config = config; 56 | this.kryo = kryo; 57 | } 58 | 59 | @Override 60 | public byte[] serialise(Message message) { 61 | log.info("Serialising message: {}", message); 62 | Output output = outputs.get(); 63 | try { 64 | output.reset(); 65 | getKryo().writeClassAndObject(output, message); 66 | output.flush(); 67 | byte[] bytes = output.toBytes(); 68 | log.info("Serialised message to {} bytes", bytes.length); 69 | return bytes; 70 | } catch (Exception e) { 71 | log.error("Couldn't create output byte array.", e); 72 | if (output != null) { 73 | log.error("Output position: {}, Buffer length: {}", output.position(), output.getBuffer().length); 74 | } 75 | } finally { 76 | output.close(); 77 | } 78 | return null; 79 | } 80 | 81 | @Override 82 | public Message deserialise(byte[] byteArray, int offset, int length) { 83 | log.info("Deserialising message from byte array with length: {}", length); 84 | try (Input input = new Input(byteArray, offset, length)) { 85 | Message message = (Message) getKryo().readClassAndObject(input); 86 | if (message != null) { 87 | log.info("Deserialised message: {}", message); 88 | message.payload = byteArray; 89 | return message; 90 | } 91 | } catch (Exception e) { 92 | log.error("Couldn't stream from byte array.", e); 93 | } 94 | return null; 95 | } 96 | 97 | @Override 98 | public void serialiseWithStream(Message message, OutputStream _outputStream) { 99 | if (config.useBasicCompression) { 100 | // It usually requires more bytes if used with deflater. Depends on the size. 101 | _outputStream = new DeflaterOutputStream(_outputStream); 102 | } 103 | try (OutputStream outputStream = _outputStream) { 104 | Output output = new Output(outputStream); 105 | getKryo().writeClassAndObject(output, message); 106 | output.close(); 107 | } catch (IOException e) { 108 | log.error("Couldn't stream to byte buffer.", e); 109 | } 110 | } 111 | 112 | @Override 113 | public Message deserialiseWithStream(InputStream _is) { 114 | if (this.config.useBasicCompression) { 115 | _is = new InflaterInputStream(_is); 116 | } 117 | try (InputStream is = _is) { 118 | Input input = new Input(is); 119 | Message message = (Message) getKryo().readClassAndObject(input); 120 | if (message != null) { 121 | //message.payload = content; 122 | return message; 123 | } 124 | } catch (EOFException e) { 125 | log.error("EOFException", e); 126 | } catch (IOException e) { 127 | log.error("Couldn't stream from byte buffer.", e); 128 | } 129 | return null; 130 | } 131 | 132 | @Override 133 | public CRC32 getChecksum(Message message, byte[] salt) { 134 | Object output = message.payload; 135 | crc.reset(); 136 | if (output instanceof byte[]) { 137 | byte[] bytes = (byte[]) output; 138 | crc.update(bytes); 139 | crc.update(salt); 140 | } else { 141 | if (output == null) { 142 | log.error("Payload was null."); 143 | } else { 144 | log.error("Type '{}' not supported", output.getClass()); 145 | } 146 | return null; 147 | } 148 | return crc; 149 | } 150 | 151 | private Kryo getKryo() { 152 | return kryos != null ? kryos.get() : this.kryo; 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/main/java/com/jfastnet/Client.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2018 Klaus Pfeiffer - klaus@allpiper.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | ******************************************************************************/ 16 | 17 | package com.jfastnet; 18 | 19 | import com.jfastnet.messages.*; 20 | import com.jfastnet.messages.features.TimestampFeature; 21 | import lombok.AccessLevel; 22 | import lombok.Getter; 23 | import lombok.Setter; 24 | import lombok.extern.slf4j.Slf4j; 25 | 26 | import java.net.InetSocketAddress; 27 | import java.util.concurrent.locks.ReentrantLock; 28 | 29 | /** @author Klaus Pfeiffer - klaus@allpiper.com */ 30 | @Slf4j 31 | public class Client extends PeerController { 32 | 33 | /** Time of the last received message. */ 34 | @Getter 35 | private long lastReceivedMessageTime; 36 | 37 | /** If not set it is retrieved from Config.senderId */ 38 | @Setter(AccessLevel.PACKAGE) 39 | private int clientId; 40 | 41 | /** Timestamp of last keep alive check. */ 42 | private long lastKeepAliveCheck; 43 | 44 | /** Timestamp of sent connect request. */ 45 | private long connectRequestTimestamp; 46 | 47 | /** Address of the server. */ 48 | private InetSocketAddress serverSocketAddress; 49 | 50 | private ReentrantLock connectLock = new ReentrantLock(); 51 | 52 | public Client(Config config) { 53 | super(config); 54 | state.setHost(false); 55 | clientId = config.senderId; 56 | } 57 | 58 | @Override 59 | public void process() { 60 | super.process(); 61 | sendKeepAliveSequencePacket(); 62 | } 63 | 64 | public void sendKeepAliveSequencePacket() { 65 | if (state.isConnected()) { 66 | long currentTime = config.timeProvider.get(); 67 | if (lastKeepAliveCheck + config.keepAliveInterval < currentTime) { 68 | lastKeepAliveCheck = currentTime; 69 | send(new SequenceKeepAlive()); 70 | } 71 | } 72 | } 73 | 74 | @Override 75 | public boolean start() { 76 | boolean start = super.start(); 77 | serverSocketAddress = new InetSocketAddress(config.host, config.port); 78 | if (start) { 79 | log.info("Say hello to server at {}:{}.", config.host, config.port); 80 | send(new ConnectRequest(clientId)); 81 | connectRequestTimestamp = config.timeProvider.get(); 82 | } 83 | lastReceivedMessageTime = config.timeProvider.get(); 84 | return start; 85 | } 86 | 87 | /** Wait until connect response is received. */ 88 | public void blockingWaitUntilConnected() { 89 | process(); 90 | connectLock.lock(); 91 | try { 92 | try { 93 | final int processTimeInterval = 100; 94 | while (!state.connected && config.timeProvider.get() - connectRequestTimestamp < config.connectTimeout) { 95 | connectLock.unlock(); 96 | process(); 97 | Thread.sleep(processTimeInterval); 98 | connectLock.lock(); 99 | } 100 | } catch (InterruptedException e) { 101 | connectLock.lock(); 102 | log.error("Wait for connection interrupted.", e); 103 | } 104 | if (state.connected) { 105 | log.info("Connection established!"); 106 | } else { 107 | log.error("Connection failed!"); 108 | state.connectionFailed = true; 109 | stop(); 110 | } 111 | } finally { 112 | connectLock.unlock(); 113 | } 114 | } 115 | 116 | @Override 117 | public boolean send(Message message) { 118 | // Keep alive only has to be sent, when no other messages are sent 119 | lastKeepAliveCheck = config.timeProvider.get(); 120 | message.setSenderId(config.senderId); 121 | message.socketAddressRecipient = serverSocketAddress; 122 | return super.send(message); 123 | } 124 | 125 | @Override 126 | public void receive(Message message) { 127 | InetSocketAddress senderAddress = message.getSocketAddressSender(); 128 | if (senderAddress == null || !senderAddress.equals(serverSocketAddress)) { 129 | log.warn("Message not from server {}, but from {}! {}", new Object[]{serverSocketAddress, senderAddress, message}); 130 | return; 131 | } 132 | log.trace("Received message: {}", message); 133 | 134 | lastReceivedMessageTime = System.currentTimeMillis(); 135 | if (message instanceof ConnectResponse && !state.connected) { 136 | connectLock.lock(); 137 | try { 138 | if (!state.connectionFailed) { 139 | ConnectResponse connectResponse = (ConnectResponse) message; 140 | connectResponse.setLastReliableSeqIdInSequenceProcessor(); 141 | state.connected = true; 142 | clientId = connectResponse.getClientId(); 143 | log.info("Set client id to {}", clientId); 144 | config.setSenderId(clientId); 145 | config.newSenderIdConsumer.accept(clientId); 146 | } else { 147 | log.warn("ConnectResponse received, but connection failed already!"); 148 | } 149 | } finally { 150 | connectLock.unlock(); 151 | } 152 | } 153 | if (state.connected) { 154 | if (message instanceof TimerSyncMessage) { 155 | // received timer sync message 156 | // also used for the heart beat 157 | if (clientId != 0) { 158 | TimerSyncMessage timerSyncAction = new TimerSyncMessage(clientId); 159 | TimestampFeature timestampFeature = new TimestampFeature(); 160 | timestampFeature.timestamp = System.currentTimeMillis(); 161 | timerSyncAction.getFeatures().add(timestampFeature); 162 | send(timerSyncAction); 163 | } 164 | } else { 165 | super.receive(message); 166 | } 167 | } 168 | if (message instanceof LeaveConfirmationResponse) { 169 | log.info("Received LeaveConfirmationResponse"); 170 | super.receive(message); 171 | this.state.getUdpPeer().stop(); 172 | } 173 | } 174 | 175 | /** @return true if timeout for last received message reached. */ 176 | public boolean noResponseFromServer() { 177 | return config.timeProvider.get() - lastReceivedMessageTime > config.timeoutThreshold; 178 | } 179 | 180 | public boolean isConnected() { 181 | return state.connected; 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /src/main/java/com/jfastnet/peers/netty/KryoNettyPeer.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2018 Klaus Pfeiffer - klaus@allpiper.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | ******************************************************************************/ 16 | 17 | package com.jfastnet.peers.netty; 18 | 19 | import com.jfastnet.Config; 20 | import com.jfastnet.IPeer; 21 | import com.jfastnet.NetStats; 22 | import com.jfastnet.messages.Message; 23 | import io.netty.bootstrap.Bootstrap; 24 | import io.netty.buffer.ByteBuf; 25 | import io.netty.buffer.ByteBufInputStream; 26 | import io.netty.buffer.ByteBufOutputStream; 27 | import io.netty.buffer.Unpooled; 28 | import io.netty.channel.*; 29 | import io.netty.channel.nio.NioEventLoopGroup; 30 | import io.netty.channel.socket.DatagramPacket; 31 | import io.netty.channel.socket.nio.NioDatagramChannel; 32 | import lombok.extern.slf4j.Slf4j; 33 | 34 | import java.util.Random; 35 | 36 | /** @author Klaus Pfeiffer - klaus@allpiper.com */ 37 | @Slf4j 38 | public class KryoNettyPeer implements IPeer { 39 | 40 | ChannelHandler channelHandler; 41 | 42 | Channel channel; 43 | EventLoopGroup group; 44 | 45 | Config config; 46 | private Random debugRandom = new Random(); 47 | 48 | public KryoNettyPeer(Config config, ChannelHandler channelHandler) { 49 | this.config = config; 50 | this.channelHandler = channelHandler; 51 | } 52 | 53 | public KryoNettyPeer(Config config) { 54 | this.config = config; 55 | } 56 | 57 | @Override 58 | public boolean start() { 59 | group = new NioEventLoopGroup(); 60 | try { 61 | Bootstrap b = new Bootstrap(); 62 | b.group(group) 63 | .channel(NioDatagramChannel.class) 64 | .option(ChannelOption.SO_BROADCAST, true) 65 | .option(ChannelOption.SO_SNDBUF, config.socketSendBufferSize) 66 | .option(ChannelOption.SO_RCVBUF, config.socketReceiveBufferSize) 67 | .option(ChannelOption.RCVBUF_ALLOCATOR, new FixedRecvByteBufAllocator(config.receiveBufferAllocator)) 68 | .handler(channelHandler != null ? channelHandler : new UdpHandler()); 69 | 70 | channel = b.bind(config.bindPort).sync().channel(); 71 | 72 | } catch (Exception e) { 73 | log.error("Couldn't start server.", e); 74 | return false; 75 | } 76 | return true; 77 | } 78 | 79 | @Override 80 | public void stop() { 81 | try { 82 | if (channel != null) { 83 | channel.close().await(); 84 | } 85 | } catch (Exception e) { 86 | log.error("Closing of channel failed.", e); 87 | } finally { 88 | if (group != null) { 89 | group.shutdownGracefully(); 90 | } 91 | } 92 | } 93 | 94 | @Override 95 | public boolean createPayload(Message message) { 96 | ByteBuf data = getByteBuf(message); 97 | message.payload = data; 98 | return data != null; 99 | } 100 | 101 | @Override 102 | public boolean send(Message message) { 103 | if (message.payload == null) { 104 | log.error("Payload of message {} may not be null.", message.toString()); 105 | return false; 106 | } 107 | if (message.socketAddressRecipient == null) { 108 | log.error("Recipient of message {} may not be null.", message.toString()); 109 | return false; 110 | } 111 | if (config.trackData) { 112 | int frame = 0; 113 | config.netStats.getData().add( 114 | new NetStats.Line(true, message.getSenderId(), frame, message.getTimestamp(), message.getClass(), ((ByteBuf)message.payload).writerIndex())); 115 | } 116 | 117 | channel.writeAndFlush(new DatagramPacket((ByteBuf) message.payload, message.socketAddressRecipient)) 118 | .addListener(new FutureGenericFutureListener("writeAndFlush", KryoNettyPeer.this, message)); 119 | return true; 120 | } 121 | 122 | public ByteBuf getByteBuf(Message message) { 123 | ByteBuf data = Unpooled.buffer(); 124 | config.serialiser.serialiseWithStream(message, new ByteBufOutputStream(data)); 125 | 126 | int length = data.writerIndex(); 127 | log.trace("Message size of {} is {}", message, length); 128 | // if (length > config.maximumUdpPacketSize) { 129 | // log.error("Message {} exceeds maximum size of {}! Size is {} byte", new Object[]{message, config.maximumUdpPacketSize, length}); 130 | // return null; 131 | // } 132 | return data.retain(); 133 | } 134 | 135 | public void receive(ChannelHandlerContext ctx, DatagramPacket packet) { 136 | ByteBuf content = packet.content(); 137 | Message message = config.serialiser.deserialiseWithStream(new ByteBufInputStream(content)); 138 | if (message == null) { 139 | return; 140 | } 141 | // TODO set config and state 142 | message.payload = content; 143 | if (message.getFeatures() != null) { 144 | message.getFeatures().resolve(); 145 | } 146 | 147 | if (config.debug.simulateLossOfPacket()) { 148 | // simulated N % loss rate 149 | log.warn("DEBUG: simulated loss of packet: {}", message); 150 | return; 151 | } 152 | 153 | message.socketAddressSender = packet.sender(); 154 | message.socketAddressRecipient = packet.recipient(); 155 | 156 | if (config.trackData) { 157 | int frame = 0; 158 | config.netStats.getData().add( 159 | new NetStats.Line(false, message.getSenderId(), frame, message.getTimestamp(), message.getClass(), ((ByteBuf)message.payload).writerIndex())); 160 | } 161 | 162 | // Let the controller receive the message. 163 | // Processors are called there. 164 | config.internalReceiver.receive(message); 165 | } 166 | 167 | @Override 168 | public void process() {} 169 | 170 | public class UdpHandler extends SimpleChannelInboundHandler { 171 | 172 | @Override 173 | protected void channelRead0(ChannelHandlerContext ctx, DatagramPacket packet) throws Exception { 174 | receive(ctx, packet); 175 | } 176 | 177 | @Override 178 | public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { 179 | ctx.flush(); 180 | } 181 | 182 | @Override 183 | public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { 184 | log.error("UdpHandler error.", cause); 185 | ctx.close(); 186 | // We don't close the channel because we can keep serving requests. 187 | } 188 | } 189 | 190 | } 191 | -------------------------------------------------------------------------------- /src/main/java/com/jfastnet/peers/javanet/JavaNetPeer.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2018 Klaus Pfeiffer - klaus@allpiper.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | ******************************************************************************/ 16 | 17 | package com.jfastnet.peers.javanet; 18 | 19 | import com.jfastnet.*; 20 | import com.jfastnet.messages.Message; 21 | import com.jfastnet.peers.CongestionControl; 22 | import lombok.extern.slf4j.Slf4j; 23 | 24 | import java.io.IOException; 25 | import java.net.DatagramPacket; 26 | import java.net.DatagramSocket; 27 | import java.net.InetSocketAddress; 28 | import java.net.SocketException; 29 | 30 | /** @author Klaus Pfeiffer - klaus@allpiper.com */ 31 | @Slf4j 32 | public class JavaNetPeer implements IPeer { 33 | 34 | private final Config config; 35 | private final State state; 36 | 37 | private CongestionControl congestionControl; 38 | 39 | private DatagramSocket socket; 40 | private Thread receiveThread; 41 | 42 | public JavaNetPeer(Config config, State state) { 43 | this.config = config; 44 | this.state = state; 45 | } 46 | 47 | @Override 48 | public boolean start() { 49 | try { 50 | stop(); 51 | createSocket(); 52 | congestionControl = new CongestionControl<>(new ConfigStateContainer(config, state), this::socketSend); 53 | receiveThread = new Thread(new MessageReceivingRunnable()); 54 | receiveThread.setName("JavaNetPeer-receiver"); 55 | receiveThread.start(); 56 | } catch (Exception e) { 57 | log.error("Couldn't start peer.", e); 58 | return false; 59 | } 60 | return true; 61 | } 62 | 63 | private void createSocket() throws SocketException { 64 | socket = new DatagramSocket(config.bindPort); 65 | socket.setSendBufferSize(config.socketSendBufferSize); 66 | socket.setReceiveBufferSize(config.socketReceiveBufferSize); 67 | } 68 | 69 | @Override 70 | public void stop() { 71 | try { 72 | if (socket != null && !socket.isClosed()) { 73 | socket.close(); 74 | } 75 | socket = null; 76 | if (receiveThread != null && receiveThread.isAlive()) { 77 | receiveThread.join(3000); 78 | if (receiveThread.isAlive()) { 79 | log.warn("Receiver thread should be destroyed by now."); 80 | } 81 | } 82 | receiveThread = null; 83 | } catch (Exception e) { 84 | log.error("Closing of socket failed.", e); 85 | } 86 | } 87 | 88 | @Override 89 | public boolean createPayload(Message message) { 90 | byte[] data = getByteArray(message); 91 | message.payload = data; 92 | return data != null; 93 | } 94 | 95 | @Override 96 | public boolean send(Message message) { 97 | if (message.payload == null) { 98 | log.error("Payload of message {} may not be null.", message.toString()); 99 | return false; 100 | } 101 | if (message.socketAddressRecipient == null) { 102 | log.error("Recipient of message {} may not be null.", message.toString()); 103 | return false; 104 | } 105 | byte[] payload = (byte[]) message.payload; 106 | if (config.trackData) { 107 | int frame = 0; 108 | config.netStats.getData().add( 109 | new NetStats.Line(true, message.getSenderId(), frame, System.currentTimeMillis(), message.getClass(), (payload).length)); 110 | } 111 | 112 | log.trace("Send message: {}", message); 113 | log.trace("Payload length: {}", payload.length); 114 | DatagramPacket datagramPacket = new DatagramPacket(payload, payload.length, message.socketAddressRecipient); 115 | congestionControl.send(message, datagramPacket); 116 | return true; 117 | } 118 | 119 | private void socketSend(DatagramPacket datagramPacket) { 120 | if (socket == null || socket.isClosed()) { 121 | log.warn("Couldn't send message: Socket is closed!"); 122 | return; 123 | } 124 | try { 125 | socket.send(datagramPacket); 126 | } catch (IOException e) { 127 | log.error("Couldn't send message.", e); 128 | } 129 | } 130 | 131 | public byte[] getByteArray(Message message) { 132 | byte[] data = config.serialiser.serialise(message); 133 | log.trace("Message size of {} is {}", message, data != null ? data.length : -1); 134 | return data; 135 | } 136 | 137 | public void receive(DatagramPacket packet) { 138 | Message message = config.serialiser.deserialise(packet.getData(), packet.getOffset(), packet.getLength()); 139 | if (message == null) { 140 | return; 141 | } 142 | log.trace("Received message: {}", message); 143 | if (config.debug.simulateLossOfPacket()) { 144 | // simulated N % loss rate 145 | log.warn("DEBUG: simulated loss of packet: {}", message); 146 | return; 147 | } 148 | 149 | trackData(message); 150 | 151 | message.socketAddressSender = new InetSocketAddress(packet.getAddress(), packet.getPort()); 152 | 153 | message.setConfig(config); 154 | message.setState(state); 155 | 156 | message.getFeatures().resolve(); 157 | 158 | message = message.beforeReceive(); 159 | 160 | // Let the controller receive the message. 161 | // Processors are called there. 162 | config.internalReceiver.receive(message); 163 | } 164 | 165 | private void trackData(Message message) { 166 | if (config.trackData) { 167 | int frame = 0; 168 | config.netStats.getData().add( 169 | new NetStats.Line(false, message.getSenderId(), frame, message.getTimestamp(), message.getClass(), ((byte[]) message.payload).length)); 170 | } 171 | } 172 | 173 | @Override 174 | public void process() { 175 | congestionControl.process(); 176 | } 177 | 178 | private class MessageReceivingRunnable implements Runnable { 179 | @Override 180 | public void run() { 181 | log.info("Start receiver thread {} for senderId {}", Thread.currentThread().getId(), config.senderId); 182 | final byte[] receiveData = new byte[config.socketReceiveBufferSize]; 183 | final DatagramPacket receivePacket = new DatagramPacket(receiveData, receiveData.length); 184 | while(true) { 185 | try { 186 | if (socket == null || socket.isClosed()) { 187 | log.info("Receiving socket closed."); 188 | break; 189 | } 190 | socket.receive(receivePacket); 191 | receive(receivePacket); 192 | } catch (Exception e) { 193 | if (socket != null && !socket.isClosed()) { 194 | log.warn("Error receiving packet.", e); 195 | } else { 196 | log.warn("Error receiving packet. Socket is already closed!"); 197 | } 198 | } 199 | } 200 | log.info("Receiver thread {} ended", Thread.currentThread().getId()); 201 | } 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /src/main/java/com/jfastnet/State.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2018 Klaus Pfeiffer - klaus@allpiper.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | ******************************************************************************/ 16 | 17 | package com.jfastnet; 18 | 19 | import com.jfastnet.events.EventLog; 20 | import com.jfastnet.idprovider.IIdProvider; 21 | import com.jfastnet.messages.MessagePart; 22 | import com.jfastnet.processors.IMessageReceiverPostProcessor; 23 | import com.jfastnet.processors.IMessageReceiverPreProcessor; 24 | import com.jfastnet.processors.IMessageSenderPostProcessor; 25 | import com.jfastnet.processors.IMessageSenderPreProcessor; 26 | import com.jfastnet.state.ClientStates; 27 | import lombok.Getter; 28 | import lombok.Setter; 29 | import lombok.experimental.Accessors; 30 | import lombok.extern.slf4j.Slf4j; 31 | 32 | import java.lang.reflect.Constructor; 33 | import java.lang.reflect.InvocationTargetException; 34 | import java.util.*; 35 | 36 | /** @author Klaus Pfeiffer - klaus@allpiper.com */ 37 | @Slf4j 38 | @Getter 39 | @Accessors(chain = true) 40 | public class State { 41 | 42 | /** UDP peer system to use. (e.g. KryoNetty) */ 43 | private IPeer udpPeer; 44 | 45 | /** Provides the message ids. */ 46 | public IIdProvider idProvider; 47 | 48 | /** Are we the host? Server sets this to true on creation. */ 49 | @Setter 50 | private boolean isHost; 51 | 52 | /** The server holds track of its clients. */ 53 | private final ClientStates clientStates; 54 | 55 | /** Client will only receive messages, if connected. */ 56 | public volatile boolean connected = false; 57 | public volatile boolean connectionFailed = false; 58 | 59 | private final EventLog eventLog; 60 | 61 | /** Stacked messages can be temporarily disabled. e.g. if the packet size 62 | * of the stacked messages is too big. */ 63 | @Setter 64 | private boolean enableStackedMessages = true; 65 | 66 | /** Used for receiving bigger messages. Only one byte array buffer may 67 | * be processed at any given time. 68 | * Key: channel id; value: (key: message part position; value: message part) */ 69 | private SortedMap> byteArrayBufferMap = new TreeMap<>(); 70 | 71 | /** Map of all added processors. */ 72 | private final Map processorMap = new HashMap<>(); 73 | 74 | /** List of systems that need to be processed every tick. */ 75 | private final List processables = new ArrayList<>(); 76 | private final List messageSenderPreProcessors = new ArrayList<>(); 77 | private final List messageSenderPostProcessors = new ArrayList<>(); 78 | private final List messageReceiverPreProcessors = new ArrayList<>(); 79 | private final List messageReceiverPostProcessors = new ArrayList<>(); 80 | 81 | @Getter 82 | private Config config; 83 | 84 | public State(Config config) { 85 | this.config = config; 86 | this.eventLog = new EventLog(config, this); 87 | createIdProvider(config); 88 | createUdpPeer(config); 89 | createProcessors(config); 90 | clientStates = new ClientStates(config); 91 | } 92 | 93 | private void createIdProvider(Config config) { 94 | try { 95 | Constructor[] constructors = config.idProviderClass.getConstructors(); 96 | // Search full-blown constructor 97 | for (Constructor constructor : constructors) { 98 | Class[] parameterTypes = constructor.getParameterTypes(); 99 | if (parameterTypes.length == 2 && parameterTypes[0] == Config.class && parameterTypes[1] == State.class) { 100 | idProvider = (IIdProvider) constructor.newInstance(config, this); 101 | } 102 | } 103 | if (idProvider == null) { 104 | // Try default constructor 105 | idProvider = config.idProviderClass.newInstance(); 106 | } 107 | } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) { 108 | log.error("Couldn't create id provider {}", config.udpPeerClass, e); 109 | } 110 | } 111 | 112 | private void createUdpPeer(Config config) { 113 | try { 114 | Constructor[] constructors = config.udpPeerClass.getConstructors(); 115 | // Search full-blown constructor 116 | for (Constructor constructor : constructors) { 117 | Class[] parameterTypes = constructor.getParameterTypes(); 118 | if (parameterTypes.length == 2 && parameterTypes[0] == Config.class && parameterTypes[1] == State.class) { 119 | udpPeer = (IPeer) constructor.newInstance(config, this); 120 | } 121 | } 122 | if (udpPeer == null) { 123 | // Try default constructor 124 | udpPeer = config.udpPeerClass.newInstance(); 125 | } 126 | } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) { 127 | log.error("Couldn't create udp peer {}", config.udpPeerClass, e); 128 | } 129 | } 130 | 131 | private void createProcessors(Config config) { 132 | config.processorClasses.forEach(processorClass -> { 133 | try { 134 | Constructor[] constructors = processorClass.getConstructors(); 135 | // Search full-blown constructor 136 | for (Constructor constructor : constructors) { 137 | Class[] parameterTypes = constructor.getParameterTypes(); 138 | if (parameterTypes.length == 2 && parameterTypes[0] == Config.class && parameterTypes[1] == State.class) { 139 | addProcessor(constructor.newInstance(config, this)); 140 | return; 141 | } 142 | } 143 | // Try default constructor 144 | addProcessor(processorClass.newInstance()); 145 | } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) { 146 | log.error("Couldn't create processor object {}", processorClass, e); 147 | } 148 | }); 149 | } 150 | 151 | public void addProcessor(Object processor) { 152 | processorMap.put(processor.getClass(), processor); 153 | if (processor instanceof ISimpleProcessable) { 154 | ISimpleProcessable processable = (ISimpleProcessable) processor; 155 | processables.add(processable); 156 | } 157 | if (processor instanceof IMessageSenderPreProcessor) { 158 | IMessageSenderPreProcessor messageSenderPreProcessor = (IMessageSenderPreProcessor) processor; 159 | messageSenderPreProcessors.add(messageSenderPreProcessor); 160 | } 161 | if (processor instanceof IMessageSenderPostProcessor) { 162 | IMessageSenderPostProcessor messageSenderPostProcessor = (IMessageSenderPostProcessor) processor; 163 | messageSenderPostProcessors.add(messageSenderPostProcessor); 164 | } 165 | if (processor instanceof IMessageReceiverPreProcessor) { 166 | IMessageReceiverPreProcessor messageReceiverPreProcessor = (IMessageReceiverPreProcessor) processor; 167 | messageReceiverPreProcessors.add(messageReceiverPreProcessor); 168 | } 169 | if (processor instanceof IMessageReceiverPostProcessor) { 170 | IMessageReceiverPostProcessor messageReceiverPostProcessor = (IMessageReceiverPostProcessor) processor; 171 | messageReceiverPostProcessors.add(messageReceiverPostProcessor); 172 | } 173 | } 174 | 175 | public E getProcessorOf(Class clazz) { 176 | return (E) processorMap.get(clazz); 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /src/main/java/com/jfastnet/messages/Message.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2018 Klaus Pfeiffer - klaus@allpiper.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | ******************************************************************************/ 16 | 17 | package com.jfastnet.messages; 18 | 19 | import com.jfastnet.Config; 20 | import com.jfastnet.State; 21 | import com.jfastnet.messages.features.MessageFeatures; 22 | import com.jfastnet.messages.features.TimestampFeature; 23 | import lombok.EqualsAndHashCode; 24 | import lombok.Getter; 25 | import lombok.Setter; 26 | import lombok.experimental.Accessors; 27 | import lombok.extern.slf4j.Slf4j; 28 | 29 | import java.io.Serializable; 30 | import java.net.InetSocketAddress; 31 | 32 | /** The base class for all messages. Subclass this class for your own messages. 33 | * @param context object for the processing of the message 34 | * @author Klaus Pfeiffer - klaus@allpiper.com */ 35 | @Slf4j 36 | @Getter 37 | @EqualsAndHashCode 38 | @Accessors(chain = true) 39 | public abstract class Message implements Serializable, Comparable { 40 | /** */ 41 | private static final long serialVersionUID = 1L; 42 | 43 | public static final MessageFeatures DEFAULT_MESSAGE_FEATURES = new MessageFeatures.Immutable(); 44 | 45 | /** Unique message id. */ 46 | @Getter 47 | long msgId; 48 | 49 | /** Sender id of message. Attention! Don't use this in responses, 50 | * as it will always be the host's id! */ 51 | @Setter 52 | private int senderId; 53 | 54 | /** Received id is only used during sending. */ 55 | @Setter @Getter 56 | private transient int receiverId; 57 | 58 | /** A message that is getting resent, because of unsuccessful transmission. 59 | * These messages may not be stopped from sending, when we are over threshold 60 | * because then the server could stall out if enough messages get lost and 61 | * it is waiting for acknowledge messages. */ 62 | @Setter @Getter 63 | private transient boolean resendMessage; 64 | 65 | /** Address from receiving or to sending socket. */ 66 | public transient InetSocketAddress socketAddressSender; 67 | 68 | /** Address from recipient. */ 69 | public transient InetSocketAddress socketAddressRecipient; 70 | 71 | /** Serialized payload of message. The data that actually gets transmitted. */ 72 | public transient Object payload; 73 | 74 | /** Config gets set upon sending / receiving of message. */ 75 | @Setter @Getter 76 | private transient Config config; 77 | 78 | /** State gets set upon sending / receiving of message. */ 79 | @Setter @Getter 80 | private transient State state; 81 | 82 | public Message() {} 83 | 84 | public Message copyAttributesFrom(Message message) { 85 | config = message.config; 86 | state = message.state; 87 | socketAddressSender = message.socketAddressSender; 88 | senderId = message.senderId; 89 | return this; 90 | } 91 | 92 | /** Additional features can be specified for every message. 93 | * E.g. when a message should contain a timestamp or when the message 94 | * has to be secured with a hash value. */ 95 | public MessageFeatures getFeatures() { 96 | return DEFAULT_MESSAGE_FEATURES; 97 | } 98 | 99 | public void resolve(Config config, State state) { 100 | this.config = config; 101 | this.state = state; 102 | this.senderId = config.senderId; 103 | getFeatures().resolveConfig(config); 104 | } 105 | 106 | /** Clear id. */ 107 | public void clearId() { 108 | this.msgId = 0; 109 | } 110 | 111 | /** Resolve id via id provider. */ 112 | public void resolveId() { 113 | this.msgId = state.idProvider.createIdFor(this); 114 | log.trace(" * Resolved ID {} for {}", msgId, this); 115 | } 116 | 117 | /** Method called on processing of message. 118 | * @param context context object */ 119 | public void process(E context) {} 120 | 121 | @Override 122 | public int compareTo(final Message o) { 123 | if (config != null) { 124 | return state.idProvider.compare(this, o); 125 | } 126 | // compare by player id 127 | int compare = Integer.compare(getReceiverId(), o.getReceiverId()); 128 | if (compare != 0) return compare; 129 | 130 | // compare by reliable mode 131 | compare = Integer.compare(getReliableMode().ordinal(), o.getReliableMode().ordinal()); 132 | if (compare != 0) return compare; 133 | 134 | // second by id 135 | compare = Long.compare(msgId, o.msgId); 136 | if (compare != 0) return compare; 137 | 138 | return compare; 139 | } 140 | 141 | /** Override if you need something done before sending. */ 142 | public void prepareToSend() {} 143 | 144 | /** Override if you need something done before receiving. */ 145 | public Message beforeReceive() { return this; } 146 | 147 | /** Specify the reliable mode for this message. */ 148 | public ReliableMode getReliableMode() { return ReliableMode.SEQUENCE_NUMBER; } 149 | 150 | /** If this message is sent with the ACK reliable mode and is then 151 | * acknowledged from the other side, this callback is called. */ 152 | public void ackCallback() {} 153 | 154 | /** If this message can be stacked. Only use in conjunction with SEQUENCE_NUMBER. */ 155 | public boolean stackable() { return false; } 156 | 157 | /** If this message should be broadcasted by the server upon receipt. */ 158 | public boolean broadcast() { return false; } 159 | 160 | /** Whether message should be sent back to sender when broadcasting. */ 161 | public boolean sendBroadcastBackToSender() { return true; } 162 | 163 | /** You can specify a unique key for this message. If the same key is 164 | * received another time, the message will be discarded. */ 165 | public Object getDiscardableKey() { return null; } 166 | 167 | /** If timeout is greater than zero the message will be discarded if 168 | * received too late. 169 | * @return timeout in ms */ 170 | public int getTimeOut() { return 0; } 171 | 172 | /** If used, must be handled by the receiver manually. */ 173 | public int executionPriority() { return 0; } 174 | 175 | /** @return true if message should be discarded. */ 176 | public boolean discard() { 177 | if (getTimeOut() > 0 && config.timeProvider.get() > getTimestamp() + getTimeOut()) { 178 | log.trace("Message {} discarded. TimeProvider: {}, TimeStamp+TimeOut: {}", new Object[]{this, config.timeProvider.get(), getTimestamp() + getTimeOut()}); 179 | return true; 180 | } else { 181 | return false; 182 | } 183 | } 184 | 185 | public String toString() { 186 | return getClass().getName() + "(msgId=" + this.msgId + ", reliableMode=" + getReliableMode() + ", senderId=" + this.senderId + ", receiverId=" + this.receiverId + ")"; 187 | } 188 | 189 | public long getTimestamp() { 190 | TimestampFeature timestampFeature = getFeatures().get(TimestampFeature.class); 191 | if (timestampFeature != null) { 192 | return timestampFeature.timestamp; 193 | } 194 | return 0L; 195 | } 196 | 197 | public int payloadLength() { 198 | if (payload instanceof byte[]) { 199 | byte[] payload = (byte[]) this.payload; 200 | return payload.length; 201 | } else { 202 | return -1; 203 | } 204 | } 205 | 206 | public enum ReliableMode { 207 | /** Message doesn't have to be transmitted reliably. */ 208 | UNRELIABLE, 209 | /** Receiving side sends acknowledge packet to confirm receit. */ 210 | ACK_PACKET, 211 | /** Receiving side checks consecutively numbered messages. */ 212 | SEQUENCE_NUMBER, 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /src/main/java/com/jfastnet/Config.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2018 Klaus Pfeiffer - klaus@allpiper.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | ******************************************************************************/ 16 | 17 | package com.jfastnet; 18 | 19 | import com.esotericsoftware.kryo.Kryo; 20 | import com.jfastnet.config.SerialiserConfig; 21 | import com.jfastnet.idprovider.ClientIdReliableModeIdProvider; 22 | import com.jfastnet.idprovider.IIdProvider; 23 | import com.jfastnet.peers.CongestionControl; 24 | import com.jfastnet.peers.javanet.JavaNetPeer; 25 | import com.jfastnet.processors.*; 26 | import com.jfastnet.serialiser.ISerialiser; 27 | import com.jfastnet.serialiser.KryoSerialiser; 28 | import com.jfastnet.time.ITimeProvider; 29 | import com.jfastnet.time.SystemTimeProvider; 30 | import lombok.Getter; 31 | import lombok.Setter; 32 | import lombok.experimental.Accessors; 33 | 34 | import java.util.*; 35 | import java.util.concurrent.ConcurrentHashMap; 36 | import java.util.function.Consumer; 37 | 38 | /** Configure JFastNet with this configuration class. We don't care much about 39 | * visibility here, because it's only used for configuration of the system and 40 | * access to the fields look much cleaner without the setter/getter boilerplate. 41 | * 42 | * @author Klaus Pfeiffer - klaus@allpiper.com */ 43 | @Setter 44 | @Getter 45 | @Accessors(chain = true) 46 | public class Config { 47 | 48 | /** Message receiver that will simply call process on the message. */ 49 | public static final IMessageReceiver DEFAULT_MESSAGE_RECEIVER = message -> message.process(null); 50 | 51 | public static final List DEFAULT_MESSAGE_PROCESSORS = new ArrayList<>(); 52 | static { 53 | // add default processors in the order in which they get called 54 | DEFAULT_MESSAGE_PROCESSORS.add(AddChecksumProcessor.class); 55 | DEFAULT_MESSAGE_PROCESSORS.add(DiscardWrongChecksumMessagesHandler.class); 56 | DEFAULT_MESSAGE_PROCESSORS.add(MessageLogProcessor.class); 57 | DEFAULT_MESSAGE_PROCESSORS.add(StackedMessageProcessor.class); 58 | DEFAULT_MESSAGE_PROCESSORS.add(ReliableModeAckProcessor.class); 59 | DEFAULT_MESSAGE_PROCESSORS.add(ReliableModeSequenceProcessor.class); 60 | DEFAULT_MESSAGE_PROCESSORS.add(DiscardMessagesHandler.class); 61 | } 62 | 63 | /** Map for additional configuration (e.g. for the processors). */ 64 | public Map additionalConfigMap = new HashMap<>(); 65 | { 66 | setAdditionalConfig(new StackedMessageProcessor.ProcessorConfig()); 67 | setAdditionalConfig(new ReliableModeAckProcessor.ProcessorConfig()); 68 | setAdditionalConfig(new ReliableModeSequenceProcessor.ProcessorConfig()); 69 | setAdditionalConfig(new MessageLogProcessor.ProcessorConfig()); 70 | setAdditionalConfig(new CongestionControl.CongestionControlConfig()); 71 | } 72 | 73 | public Object context; 74 | 75 | /** Hostname or IP address. */ 76 | public String host = "127.0.0.1"; 77 | 78 | /** Port number to accept or request new UDP connections on. */ 79 | public int port = 0; 80 | 81 | /** On client this can be 0 so a free port is automatically used. */ 82 | public int bindPort = 0; 83 | 84 | /** Optional configured sender id. 0 is reserved for the server. */ 85 | public int senderId; 86 | 87 | /** Consumer is called when a new client id is retrieved through the 88 | * ConnectResponse message. */ 89 | public Consumer newSenderIdConsumer = id -> {}; 90 | 91 | /** UDP peer system to use. (e.g. KryoNetty) */ 92 | public Class udpPeerClass = JavaNetPeer.class; 93 | 94 | /** Set to true if you want that a CSV file is created after every run with 95 | * data about all the sent and received messages and their data size. */ 96 | public boolean trackData = false; 97 | 98 | /** Collected data. Only used if trackData is set to true. */ 99 | public NetStats netStats = new NetStats(); 100 | 101 | /** Used for the timestamp for new messages. */ 102 | public ITimeProvider timeProvider = new SystemTimeProvider(); 103 | 104 | /** Provides the message ids. */ 105 | public Class idProviderClass = ClientIdReliableModeIdProvider.class; 106 | 107 | /** JFastNet internal message sender. Don't change. */ 108 | public IMessageSender internalSender; 109 | 110 | /** JFastNet internal message receiver for received messages. Don't change. */ 111 | public IMessageReceiver internalReceiver; 112 | 113 | /** Configure an external receiver for incoming messages. Must be thread-safe. */ 114 | public IMessageReceiver externalReceiver = DEFAULT_MESSAGE_RECEIVER; 115 | 116 | /** Serialisation system. Some peers require specific serialisation 117 | * return types. */ 118 | public ISerialiser serialiser = new KryoSerialiser(new SerialiserConfig(), new Kryo()); 119 | 120 | /** Compress MessagePart messages. */ 121 | public boolean compressBigMessages = false; 122 | 123 | /** Required for the reliable sequence mode. Interval in ms. */ 124 | public int keepAliveInterval = 3000; 125 | 126 | /** If keepalive messages can be stacked. */ 127 | public boolean stackKeepAliveMessages = false; 128 | 129 | /** Time in ms when peer considers other side as not reachable. */ 130 | public int timeoutThreshold = keepAliveInterval * 6; //2; 131 | 132 | // BEGIN server config 133 | /** All client ids that are expected to join. */ 134 | public List expectedClientIds = new ArrayList<>(); 135 | 136 | /** Map of clients that are required to connect. 137 | * Key: client id, value: true if connected. */ 138 | public Map requiredClients = new ConcurrentHashMap<>(); 139 | 140 | /** Time that has to be passed to consider a received connect request as 141 | * new. */ 142 | public int timeSinceLastConnectRequest = 3000; 143 | 144 | /** Called by the server. */ 145 | public IServerHooks serverHooks = new IServerHooks() {}; 146 | // END server config 147 | 148 | // BEGIN client config 149 | /** Time in ms the client tries to connect to the server. */ 150 | public int connectTimeout = 5000; 151 | // END client config 152 | 153 | /** Packets above this size will log an error or will be automatically 154 | * splitted into multiple messages. */ 155 | public int maximumUdpPacketSize = 1024; 156 | 157 | /** Automatically split too big messages into multiple smaller messages. */ 158 | public boolean autoSplitTooBigMessages = true; 159 | 160 | public int messageQueueThreshold = 37; 161 | //SocketOption send buffer SO_SNDBUF 162 | public int socketSendBufferSize = 131072; //65536; 163 | public int socketReceiveBufferSize = 65536; 164 | 165 | public int receiveBufferAllocator = 65536; 166 | 167 | /** Maximum size of event log queue. */ 168 | public int eventLogSize = 4096; 169 | 170 | /** Delay in ms between sending of queued messages. */ 171 | public int queuedMessagesDelay = 50; 172 | 173 | /** List of all added processors. */ 174 | public List processorClasses = DEFAULT_MESSAGE_PROCESSORS; 175 | 176 | public void setAdditionalConfig(E config) { 177 | additionalConfigMap.put(config.getClass(), config); 178 | } 179 | public E getAdditionalConfig(Class configClass) { 180 | return (E) additionalConfigMap.get(configClass); 181 | } 182 | 183 | public final Debug debug = new Debug(); 184 | 185 | @Setter 186 | @Getter 187 | @Accessors(chain = true) 188 | public static class Debug { 189 | private final Random debugRandom = new Random(); 190 | 191 | /** Set to true if you want to simulate packet loss or an otherwise 192 | * rough environment. */ 193 | public boolean enabled = false; 194 | 195 | /** Set to true if you want to simulate packet loss for the next 196 | * received packet. */ 197 | public boolean discardNextPacket = false; 198 | 199 | /** Specify percentage of lost packages from 0 to 100 where 100 means 200 | * every packet. */ 201 | public int lostPacketsPercentage = 1; 202 | 203 | public boolean simulateLossOfPacket() { 204 | if (discardNextPacket) { 205 | discardNextPacket = false; 206 | return true; 207 | } 208 | return enabled && debugRandom.nextInt(100) < lostPacketsPercentage; 209 | } 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /src/main/java/com/jfastnet/processors/StackedMessageProcessor.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2018 Klaus Pfeiffer - klaus@allpiper.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | ******************************************************************************/ 16 | 17 | package com.jfastnet.processors; 18 | 19 | import com.jfastnet.Config; 20 | import com.jfastnet.IServerHooks; 21 | import com.jfastnet.State; 22 | import com.jfastnet.idprovider.ReliableModeIdProvider; 23 | import com.jfastnet.messages.Message; 24 | import com.jfastnet.messages.StackAckMessage; 25 | import com.jfastnet.messages.StackedMessage; 26 | import lombok.Getter; 27 | import lombok.Setter; 28 | import lombok.experimental.Accessors; 29 | import lombok.extern.slf4j.Slf4j; 30 | 31 | import java.util.*; 32 | import java.util.concurrent.ConcurrentHashMap; 33 | 34 | /** @author Klaus Pfeiffer - klaus@allpiper.com */ 35 | @Slf4j 36 | public class StackedMessageProcessor extends AbstractMessageProcessor implements IMessageReceiverPreProcessor, IMessageSenderPreProcessor, IServerHooks { 37 | 38 | @SuppressWarnings("unchecked") 39 | private static final Stack EMPTY_STACK = new Stack(0, Collections.EMPTY_LIST); 40 | 41 | /** Client id, Msg Id. */ 42 | @Getter 43 | private Map lastAckMessageIdMap = new ConcurrentHashMap<>(); 44 | 45 | /** Stacked messaged id that I acknowledged to the other side. */ 46 | @Setter 47 | private long myLastAckMessageId; 48 | private long myLastAckMessageTimestamp; 49 | 50 | private Map unacknowledgedSentMessagesMap = new ConcurrentHashMap<>(); 51 | 52 | public StackedMessageProcessor(Config config, State state) { 53 | super(config, state); 54 | if (!config.idProviderClass.equals(ReliableModeIdProvider.class)) { 55 | log.warn("StackedMessageProcessor only works with the ReliableModeIdProvider."); 56 | } 57 | } 58 | 59 | @Override 60 | public void onUnregister(int clientId) { 61 | lastAckMessageIdMap.remove(clientId); 62 | } 63 | 64 | @Override 65 | public Message beforeReceive(Message message) { 66 | if (message instanceof StackAckMessage) { 67 | StackAckMessage stackAckMessage = (StackAckMessage) message; 68 | log.trace("Received acknowledge message for id {}", stackAckMessage.getLastReceivedId()); 69 | long lastId = lastAckMessageIdMap.getOrDefault(message.getSenderId(), 0L); 70 | if (lastId < stackAckMessage.getLastReceivedId()) { 71 | lastAckMessageIdMap.put(message.getSenderId(), stackAckMessage.getLastReceivedId()); 72 | } 73 | return null; 74 | } 75 | if (message instanceof StackedMessage) { 76 | StackedMessage stackedMessage = (StackedMessage) message; 77 | if (stackedMessage.getMessages().size() > 0) { 78 | receiveStackedMessage(stackedMessage); 79 | Message lastReceivedMessage = stackedMessage.getMessages().get(stackedMessage.getMessages().size() - 1); 80 | long lastReceivedId = lastReceivedMessage.getMsgId(); 81 | boolean receivedMessageCountExceedsAckThreshold = lastReceivedId - myLastAckMessageId >= processorConfig.stackedMessagesAckThreshold; 82 | boolean timeFrameExceedsAckThreshold = config.timeProvider.get() > myLastAckMessageTimestamp + processorConfig.stackedMessagesAckTimeThresholdMs; 83 | if (receivedMessageCountExceedsAckThreshold || timeFrameExceedsAckThreshold) { 84 | config.internalSender.send(new StackAckMessage(lastReceivedId)); 85 | myLastAckMessageId = lastReceivedId; 86 | myLastAckMessageTimestamp = config.timeProvider.get(); 87 | log.trace("Send acknowledge message for id: {}", lastReceivedId); 88 | } 89 | } 90 | return null; 91 | } 92 | return message; 93 | } 94 | 95 | private void receiveStackedMessage(StackedMessage stackedMessage) { 96 | for (Message message : stackedMessage.getMessages()) { 97 | message.copyAttributesFrom(stackedMessage); 98 | log.trace("Received stack message: {}", message); 99 | config.internalReceiver.receive(message); 100 | } 101 | } 102 | 103 | @Override 104 | public Message beforeSend(Message message) { 105 | // Check if message is "stackable" and don't stack messages that get resent 106 | // Check for ReliableMode.SEQUENCE_NUMBER, so we can be sure about correct ascending message ids 107 | if (state.isEnableStackedMessages() && message.stackable() && !message.isResendMessage() && message.getReliableMode() == Message.ReliableMode.SEQUENCE_NUMBER) { 108 | checkCorrectIdProvider(); 109 | cleanUpUnacknowledgedSentMessagesMap(); 110 | unacknowledgedSentMessagesMap.put(message.getMsgId(), message); 111 | if (isSentToAllFromServer(message.getReceiverId())) { 112 | state.getProcessorOf(MessageLogProcessor.class).afterSend(message); 113 | Set clientIds = state.getClientStates().idSet(); 114 | clientIds.forEach(id -> { 115 | Stack stack = createStackForReceiver(message, id); 116 | stack.send(config, state); 117 | }); 118 | return null; // Discard message 119 | } else { 120 | Stack stack = createStackForReceiver(message, message.getReceiverId()); 121 | if (stack.stackSendingIsReasonable()) { 122 | state.getProcessorOf(MessageLogProcessor.class).afterSend(message); 123 | stack.send(config, state); 124 | return null; // Discard message 125 | } 126 | } 127 | } 128 | return message; 129 | } 130 | 131 | private void checkCorrectIdProvider() { 132 | if (!config.idProviderClass.equals(ReliableModeIdProvider.class)) { 133 | throw new UnsupportedOperationException("StackedMessageProcessor only works with the ReliableModeIdProvider!"); 134 | } 135 | } 136 | 137 | private void cleanUpUnacknowledgedSentMessagesMap() { 138 | if (state.isHost()) { 139 | Set clientIds = state.getClientStates().idSet(); 140 | long maxAckId = 0L; 141 | for (Integer clientId : clientIds) { 142 | long clientLastAckId = lastAckMessageIdMap.getOrDefault(clientId, 0L); 143 | maxAckId = Math.min(maxAckId, clientLastAckId); 144 | } 145 | final long finalMaxAckId = maxAckId; 146 | unacknowledgedSentMessagesMap.keySet().removeIf(id -> id < finalMaxAckId); 147 | } else { 148 | long serverLastAckId = lastAckMessageIdMap.getOrDefault(0, 0L); 149 | unacknowledgedSentMessagesMap.keySet().removeIf(id -> id < serverLastAckId); 150 | } 151 | } 152 | 153 | /** Create individual stack for receiver id. */ 154 | private Stack createStackForReceiver(Message newMessage, int receiverId) { 155 | long startStackMsgId = lastAckMessageIdMap.getOrDefault(receiverId, -1L) + 1L; 156 | Stack stack = EMPTY_STACK; 157 | // At least the new message id must be present 158 | if (newMessage.getMsgId() >= startStackMsgId) { 159 | List messages = new ArrayList<>(); 160 | log.trace( " ++++ begin stack ++++"); 161 | for (long msgId = startStackMsgId; 162 | msgId <= newMessage.getMsgId() && messages.size() < processorConfig.maximumNumberOfMessagesPerStack; 163 | msgId++) { 164 | 165 | Message stackMsg = unacknowledgedSentMessagesMap.get(msgId); 166 | if (stackMsg != null) { 167 | // Be tolerant about missing ids. 168 | // Not every id has to be present, because some messages 169 | // may not be stackable. 170 | messages.add(stackMsg); 171 | log.trace(" ** added to stack: {}", stackMsg); 172 | } else { 173 | log.trace(" ** not added to stack: {}", msgId); 174 | } 175 | } 176 | stack = new Stack(receiverId, messages); 177 | log.trace( " ++++ end stack ++++"); 178 | } 179 | return stack; 180 | } 181 | 182 | private boolean isSentToAllFromServer(int receiverId) { 183 | return config.senderId == 0 && receiverId == 0; 184 | } 185 | 186 | @Override 187 | public Class getConfigClass() { 188 | return ProcessorConfig.class; 189 | } 190 | 191 | @Setter @Getter 192 | @Accessors(chain = true) 193 | public static class ProcessorConfig { 194 | /** After X received stacked messages we send an ack packet. */ 195 | public int stackedMessagesAckThreshold = 7; 196 | public int maximumNumberOfMessagesPerStack = stackedMessagesAckThreshold * 3; 197 | /** The next message after X milliseconds we send an ack packet. */ 198 | public int stackedMessagesAckTimeThresholdMs = 300; 199 | } 200 | 201 | private static class Stack { 202 | private final int receiverId; 203 | private final List messages; 204 | 205 | public Stack(int receiverId, List messages) { 206 | this.receiverId = receiverId; 207 | this.messages = messages; 208 | } 209 | 210 | boolean stackSendingIsReasonable() { 211 | return messages.size() > 1; 212 | } 213 | 214 | void send(Config config, State state) { 215 | if (messages.isEmpty()) { 216 | log.trace("Stack was empty on send."); 217 | return; 218 | } 219 | StackedMessage stackedMessage = new StackedMessage(messages); 220 | stackedMessage.setReceiverId(receiverId); 221 | if (!state.idProvider.resolveEveryClientMessage()) { 222 | // Clear receiver id, if every client receives the same id for a particular message 223 | messages.forEach(message -> message.setReceiverId(0)); 224 | } 225 | config.internalSender.send(stackedMessage); 226 | } 227 | } 228 | } 229 | --------------------------------------------------------------------------------