Permalink
Browse files

Added initial networking to client, logs into gameserver now, but doe…

…sn't do any other communication yet.
  • Loading branch information...
1 parent fc60aad commit ddfa66e68a125774d4b23dde43cf55aa5d5a80d4 @zzorn committed May 22, 2012
View
@@ -80,3 +80,6 @@ libraryDependencies ++= Seq( // Core lib
)
+// Networking
+
+libraryDependencies += "org.apache.mina" % "mina-core" % "2.0.4"
@@ -21,6 +21,8 @@ import com.jme3.util.SkyFactory
import com.jme3.asset.AssetManager
import com.jme3.scene.{Geometry, Node, Spatial}
import com.jme3.post.filters.FogFilter
+import network.protocol.Message
+import network.{ServerHandler, ClientNetworking}
import sky.Sky
import terrain._
import com.jme3.light.{AmbientLight, DirectionalLight}
@@ -33,6 +35,7 @@ import com.jme3.bounding.BoundingSphere
import com.jme3.texture.{Image, TextureCubeMap, Texture}
import com.jme3.scene.control.AbstractControl
import com.jme3.renderer.{RenderManager, ViewPort}
+import org.skycastle.utils.Logging
/**
*
@@ -51,7 +54,31 @@ object ClipmapTerrainSpike extends SimpleApplication {
private val lightDir = new Vector3f(-4.9f, -1.3f, 5.9f)
+ private var networking: ClientNetworking = null
+
def main(args: Array[String]) {
+ Logging.initializeLogging()
+
+ networking = new ClientNetworking(new ServerHandler {
+ def onConnected() {
+ println("Connected")
+ }
+ def onDisconnected(reason: String, cause: Exception) {
+ println("Disconnected")
+ }
+ def onMessage(message: Message) {
+ println("Got message " + message)
+ }
+ def onConnectionFailed(reason: String, cause: Exception) {
+ println("Connection failed " + reason + ": " + cause.getMessage)
+ }
+ })
+
+ networking.setup();
+
+ networking.createAccount("localhost", 6283, "TestUser1", "testPass%31# 32sdf");
+ //networking.login("localhost", 6283, "TestUser1", "testPass%31# 32sdf");
+
val settings: AppSettings = new AppSettings(true)
if (limitFps) {
settings.setFrameRate(60)
@@ -0,0 +1,102 @@
+package org.skycastle.client.network
+
+import org.apache.mina.transport.socket.nio.NioSocketConnector
+import protocol.binary.BinaryProtocol
+import protocol.Message
+import org.apache.mina.core.session.{IdleStatus, IoSession}
+import java.net.InetSocketAddress
+import org.skycastle.utils.Logging
+import org.apache.mina.filter.codec.ProtocolCodecFilter
+import org.apache.mina.filter.logging.LoggingFilter
+import org.apache.mina.core.service.{IoHandlerAdapter, IoHandler}
+import com.sun.deploy.util.ArrayUtil
+import java.util.Arrays
+
+/**
+ * Client side network logic.
+ * Takes a ServerHandler as parameter. For now a client can only be connected to one server.
+ */
+// NOTE: If we want to connect to several servers from one client, pass in some ServerHandlerFactory instead.
+class ClientNetworking(serverHandler: ServerHandler) extends Logging {
+
+ private var connector: NioSocketConnector = null
+ private var session: IoSession = null
+
+ val connectionTimeout = 10 // conf[Int]("ti", "timeout", 10, "Seconds before aborting a connection attempt when there is no answer.")
+ val logMessages = true //conf[Boolean]("lm", "log-messages", false, "Wether to log all network messages. Only recommended for debugging purposes.")
+
+ private class SessionHandler(serverHandler: ServerHandler) extends IoHandlerAdapter {
+ override def sessionOpened(session: IoSession) { serverHandler.onConnected() }
+ override def messageReceived(session: IoSession, message: Any) { serverHandler.onMessage(message.asInstanceOf[Message]) }
+ override def sessionClosed(session: IoSession) { serverHandler.onDisconnected("Session closed", null) }
+ override def exceptionCaught(session: IoSession, cause: Throwable) { log.warn("Exception when handling network connection", cause) }
+ }
+
+ /**
+ * Attempts to connect to the specified server, using an existing account.
+ */
+ def login(serverAddress: String,
+ serverPort: Int,
+ account: String,
+ password: String) {
+ connect(serverAddress, serverPort)
+
+ // Login
+ sendMessage(new Message('login, Map('account -> account, 'pw -> new String(password))))
+ }
+
+ /**
+ * Attempts to connect to the specified server, creating a new account.
+ */
+ def createAccount(serverAddress: String,
+ serverPort: Int,
+ account: String,
+ password: String) {
+ connect(serverAddress, serverPort)
+
+ // Create account
+ sendMessage(new Message('createAccount, Map('account -> account, 'pw -> new String(password))))
+ }
+
+
+ def connect(serverAddress: String, serverPort: Int) {
+ if (session != null) throw new IllegalStateException("Session has already been created")
+
+ val connectFuture = connector.connect(new InetSocketAddress(serverAddress, serverPort));
+ connectFuture.awaitUninterruptibly();
+ session = connectFuture.getSession;
+ }
+
+ def disconnect() {
+ if (session != null) {
+ val closeFuture = session.close(false)
+ closeFuture.awaitUninterruptibly
+ session = null
+ }
+ }
+
+ def sendMessage(message: Message) {
+ session.write(message)
+ }
+
+ def setup() {
+ connector = new NioSocketConnector()
+ connector.setConnectTimeoutMillis(connectionTimeout * 1000)
+ // TODO: Add encryption filter
+ connector.getFilterChain.addLast( "codec", new ProtocolCodecFilter(new BinaryProtocol()))
+ if (logMessages) connector.getFilterChain.addLast( "logger", new LoggingFilter() )
+
+ connector.setHandler(new SessionHandler(serverHandler));
+ }
+
+ def shutdown() {
+ if (session != null) {
+ val closeFuture = session.close(true)
+ closeFuture.awaitUninterruptibly
+ session = null
+ }
+ }
+
+
+}
+
@@ -0,0 +1,8 @@
+package org.skycastle.client.network
+
+/**
+ * A reference to an entity with the specified id.
+ */
+case class EntityId(id: Long)
+
+case object UndefinedId extends EntityId(0)
@@ -0,0 +1,17 @@
+package org.skycastle.client.network
+
+import protocol.Message
+
+/**
+ * Something that receives messages or other connection info from a server.
+ */
+trait ServerHandler {
+
+ def onConnectionFailed(reason: String, cause: Exception)
+
+ def onConnected()
+
+ def onMessage(message: Message)
+
+ def onDisconnected(reason: String, cause: Exception)
+}
@@ -0,0 +1,16 @@
+package org.skycastle.client.network.protocol
+
+import org.skycastle.utils.ParameterChecker
+
+
+/**
+ * A message sent over the network.
+ *
+ * @param action the action or perception the message is invoking. Should be a valid identifier.
+ * @param parameters parameters, or an empty map for no parameters.
+ */
+case class Message(action: Symbol, parameters: Map[Symbol, Any]) {
+ ParameterChecker.requireIsIdentifier(action, 'action)
+
+ override def toString = "Message " + action.name + " {" + parameters.mkString(", ") + "}"
+}
@@ -0,0 +1,54 @@
+package org.skycastle.client.network.protocol
+
+import org.apache.mina.core.buffer.IoBuffer
+import org.apache.mina.core.session.IoSession
+import org.apache.mina.filter.codec._
+import org.skycastle.utils.Logging
+
+/**
+ * A way of encoding and decoding Messages to and from a stream of bytes.
+ * Used for communication between client and server.
+ */
+trait MessageProtocol extends ProtocolCodecFactory with Logging {
+
+ /**
+ * Encodes a Data object to a buffer.
+ * Throws an Exception if there was some problem.
+ */
+ def encodeMessage(message: Message): IoBuffer
+
+ /**
+ * Decodes a message from a buffer to a Message object.
+ * Throws an Exception if there was some problem.
+ */
+ def decodeMessage(receivedBytes: IoBuffer): List[Message]
+
+ val encoder: ProtocolEncoder = new ProtocolEncoderAdapter {
+ def encode(session: IoSession, message: Any, out: ProtocolEncoderOutput) {
+ message match {
+ case data: Message => out.write(encodeMessage(data))
+ case _ => throw new Exception("Unknown type for outgoing message: " + message)
+ }
+ }
+ }
+
+ val decoder: ProtocolDecoder = new ProtocolDecoderAdapter {
+ def decode(session: IoSession, in: IoBuffer, out: ProtocolDecoderOutput) {
+ try {
+ decodeMessage(in) foreach ((d: Message) => out.write(d))
+ }
+ catch {
+ case e: Exception =>
+ log.warn("Error while decoding incoming message: " + e.getMessage + ", closing session", e)
+ session.close(false)
+ }
+ }
+ }
+
+ def getEncoder(session: IoSession): ProtocolEncoder = encoder
+
+ def getDecoder(session: IoSession): ProtocolDecoder = decoder
+
+}
+
+
@@ -0,0 +1,8 @@
+package org.skycastle.client.network.protocol
+
+/**
+ */
+class ProtocolException(description: String) extends Exception(description) {
+
+}
+
@@ -0,0 +1,52 @@
+package org.skycastle.client.network.protocol.binary
+
+import _root_.org.apache.mina.core.buffer.IoBuffer
+import org.skycastle.client.network.protocol.{MessageProtocol, Message}
+
+
+/**
+ * A binary message encoding protocol.
+ *
+ * NOTE: This will be called to deserialize messages from the client before it has logged in,
+ * so should not allow any security breahces (in particular, instantiated class constructors should not alter game state)
+ */
+// TODO: Create one that packs commonly used Symbols with lookup tables - server tells client about added aliases
+// TODO: We could use a cached buffer array in each protocol that is the size of the maximum allowed size of a message?
+// TODO: May there be a situation where we get only half of a message from Mina? In that case we would need to buffer it or leave it unread or somesuch.
+class BinaryProtocol extends MessageProtocol {
+
+ private val serializer: BinarySerializer = new BinarySerializer()
+
+ def decodeMessage(receivedBytes: IoBuffer): List[Message] = {
+ var messages: List[Message] = Nil
+
+ // There may be multiple messages in the buffer, decode until it is empty
+ while (receivedBytes.hasRemaining) {
+
+ // TODO: Start each message with a byte message type id? Allows easy adding of protocol related messages such as defining aliases, or introducing new protocol types. As well as stuff like disconnect, timeout, etc?
+
+ val data = serializer.decode[Message](receivedBytes)
+ messages = messages ::: List(data)
+ }
+
+ messages
+ }
+
+ def encodeMessage(message: Message): IoBuffer = {
+
+ // TODO: Inefficient, this should be thread local?
+ val buffer = IoBuffer.allocate(256)
+ buffer.setAutoExpand(true)
+
+ serializer.encode(buffer, message)
+
+ buffer.flip()
+
+ buffer
+ }
+
+}
+
+
+
+
Oops, something went wrong.

0 comments on commit ddfa66e

Please sign in to comment.