/*******************************************************************************
* Copyright 2015 Klaus Pfeiffer <klaus@allpiper.com>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
******************************************************************************/
package com.jfastnet;
import com.jfastnet.messages.*;
import com.jfastnet.state.ClientStates;
import com.jfastnet.util.NullsafeHashMap;
import lombok.extern.slf4j.Slf4j;
import java.net.InetSocketAddress;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/** @author Klaus Pfeiffer - klaus@allpiper.com */
@Slf4j
public class Server extends PeerController {
/** Timestamp of time when a message was last received from client id.
* Key: client id; Value: timestamp */
private Map<Integer, Long> lastReceivedMap = new ConcurrentHashMap<>();
/** Track count of incoming messages. */
private Map<Class, Counter> incomingMessages = new NullsafeCounterHashMap();
/** Track count of outgoing messages. */
private Map<Class, Counter> outgoingMessages = new NullsafeCounterHashMap();
private long lastKeepAliveCheck;
public Server(Config config) {
super(config);
state.setHost(true);
}
@Override
public boolean start() {
boolean started = super.start();
if (started) {
state.connected = true;
}
return started;
}
@Override
public void process() {
super.process();
calculateNetworkQualityToClients();
long currentTime = config.timeProvider.get();
int clientSize = state.getClientStates().size();
if (clientSize > 0 && clientSize >= config.requiredClients.size() && lastKeepAliveCheck + config.keepAliveInterval < currentTime) {
// Potentially "Keep Alive" will be sent, when first client joins.
// This can lead to clients that join a few milliseconds later that
// request this message as a missing packet, because the id number
// is already raised. Only happens with the ReliableModeIdProvider class.
lastKeepAliveCheck = currentTime;
send(new SequenceKeepAlive());
}
for (Map.Entry<Integer, Long> entry : lastReceivedMap.entrySet()) {
Long lastReceivedTime = entry.getValue();
if (lastReceivedTime + config.timeoutThreshold < currentTime) {
// timed out
unregister(entry.getKey());
}
}
}
private void calculateNetworkQualityToClients() {
state.getClientStates().process();
}
@Override
public void receive(Message message) {
boolean isConnectRequest = message instanceof ConnectRequest;
ClientStates clientStates = state.getClientStates();
if (!clientStates.hasAddress(message.getSocketAddressSender())) {
if (!isConnectRequest) {
log.warn("No client found under {}", message.getSocketAddressSender());
log.warn("Message was: {}", message);
return;
}
}
incomingMessages.get(message.getClass()).value++;
long lastReceived = lastReceivedMap.getOrDefault(message.getSenderId(), 0L);
if (message.getSenderId() > 0) {
lastReceivedMap.put(message.getSenderId(), config.timeProvider.get());
}
if (message instanceof LeaveRequest) {
unregister(message.getSenderId());
} else if (isConnectRequest && config.timeProvider.get() - lastReceived > config.timeSinceLastConnectRequest) {
ConnectRequest connectRequest = (ConnectRequest) message;
int clientId = connectRequest.getClientId();
if (clientId == 0) {
// Sender (client) id was 0 and this is a connect request
// -> client needs an id
Integer clientIdBySocketAddress = clientStates.getIdBySocketAddress(message.getSocketAddressSender());
if (clientIdBySocketAddress != null) {
clientId = clientIdBySocketAddress;
log.info("Assign previous client id {} to {}.", clientId, message.getSocketAddressSender());
} else {
clientId = clientStates.newClientId();
log.info("Assign new client id {} to {}.", clientId, message.getSocketAddressSender());
}
connectRequest.setSenderId(clientId);
connectRequest.setClientId(clientId);
}
lastReceivedMap.put(clientId, config.timeProvider.get());
boolean clientAlreadyInMap = clientStates.hasId(clientId);
if (clientAlreadyInMap) {
log.info("Client {} is already in list - could be a re-join.", clientId);
unregisterClientAtProcessors(clientId);
}
clientStates.put(clientId, message.getSocketAddressSender());
log.info("Added {} with address {} to clients.", clientId, message.getSocketAddressSender());
registerClientAtProcessors(clientId);
}
if (message instanceof IInstantServerProcessable) {
message.process(config.context);
} else {
super.receive(message);
}
if (message.broadcast()) {
// clear id so a new id gets assigned to the message
message.clearId();
message.setReceiverId(0);
if (message.sendBroadcastBackToSender()) {
internalSend(message, 0);
} else {
// don't send broadcast message back to sender
internalSend(message, message.getSenderId());
}
}
}
public void registerClientAtProcessors(int finalClientId) {
state.getProcessorMap().values().stream().filter(o -> o instanceof IServerHooks).forEach(o1 -> ((IServerHooks) o1).onRegister(finalClientId));
}
@Override
public boolean send(Message message) {
if (getState().idProvider.resolveEveryClientMessage()) {
return internalSend(message, 0);
} else {
return internalSendSameIds(message, 0);
}
}
private boolean internalSend(Message message, int exceptId) {
int receiverId = message.getReceiverId();
if (receiverId > 0) {
return send(receiverId, message);
}
if (!resolveMessage(message)) {
return false;
}
if (!message.isResendMessage()) {
// only track messages sent to all players
outgoingMessages.get(message.getClass()).value++;
}
if (!getState().idProvider.resolveEveryClientMessage()) {
if (!createPayload(message)) {
return false;
}
}
boolean beforeSendState = true;
boolean afterSendState = true;
for (Map.Entry<Integer, InetSocketAddress> entry : state.getClientStates().addressEntrySet()) {
Integer clientId = entry.getKey();
if (exceptId > 0 && exceptId == clientId) {
continue;
}
message.setReceiverId(clientId);
if (getState().idProvider.resolveEveryClientMessage()) {
message.resolveId();
if (!createPayload(message)) {
beforeSendState = false;
}
}
message.socketAddressRecipient = entry.getValue();
beforeSendState &= super.beforeSend(message);
if (beforeSendState) {
state.getUdpPeer().send(message);
afterSendState &= super.afterSend(message);
}
}
log.trace("Sent message: {}", message);
if (!beforeSendState || !afterSendState) {
// Something went wrong
return false;
}
// Keep alive only has to be sent, when no other messages are sent
lastKeepAliveCheck = config.timeProvider.get();
return true;
}
private boolean internalSendSameIds(Message message, int exceptId) {
int receiverId = message.getReceiverId();
if (receiverId > 0) {
return send(receiverId, message);
}
if (!resolveMessage(message)) {
return false;
}
if (!createPayload(message)) {
return false;
}
if (!beforeSend(message)) {
return false;
}
if (!checkPayloadSize(message)) {
return false;
}
if (!message.isResendMessage()) {
// only track messages sent to all players
outgoingMessages.get(message.getClass()).value++;
}
for (Map.Entry<Integer, InetSocketAddress> entry : state.getClientStates().addressEntrySet()) {
Integer clientId = entry.getKey();
if (exceptId > 0 && exceptId == clientId) {
continue;
}
message.setReceiverId(clientId);
message.socketAddressRecipient = entry.getValue();
state.getUdpPeer().send(message);
}
log.trace("Sent message: {}", message);
// Clear receiver id
message.setReceiverId(0);
return super.afterSend(message);
}
public boolean send(int clientId, Message message) {
InetSocketAddress client = state.getClientStates().getById(clientId).getSocketAddress();
if (client == null) {
log.warn("Client address with id {} not found.", clientId);
return false;
}
message.socketAddressRecipient = client;
return super.send(message);
}
public void unregister(int clientId) {
log.info("Bye {} -> {}", clientId, state.getClientStates().getById(clientId));
state.getClientStates().remove(clientId);
lastReceivedMap.remove(clientId);
config.requiredClients.remove(clientId);
unregisterClientAtProcessors(clientId);
config.serverHooks.onUnregister(clientId);
}
public void unregisterClientAtProcessors(int clientId) {
state.getProcessorMap().values().stream().filter(o -> o instanceof IServerHooks).forEach(o1 -> ((IServerHooks) o1).onUnregister(clientId));
}
public static class Counter {
public int value;
}
private static class NullsafeCounterHashMap extends NullsafeHashMap<Class, Counter> {
@Override
protected Counter newInstance() {
return new Counter();
}
}
}