Send messages via relays (fixes #26).
This commit is contained in:
parent
1add05b72f
commit
4a36fdbef2
17 changed files with 251 additions and 94 deletions
|
@ -85,7 +85,7 @@ version, type and ID, followed by the length of the message.
|
|||
0 1 2 3
|
||||
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
| Version | Protocol-Type | Hop Limit | Hop Count |
|
||||
| Version | Protocol-Type | Tokens | Hop Limit |
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
| Length |
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
|
@ -111,8 +111,8 @@ where such a packet came from MAY be closed.
|
|||
Protocol-Type is one of those specified in section Protocol Messages,
|
||||
or 255 for Content Messages.
|
||||
|
||||
Hop Limit SHOULD be set to `20` on message creation, and
|
||||
MUST NOT be changed by a forwarding node.
|
||||
Tokens is the number of times this message should be copied to
|
||||
different relays.
|
||||
|
||||
Hop Count specifies the number of nodes a message may pass. When
|
||||
creating a package, it is initialized to 0. Whenever a node forwards
|
||||
|
|
|
@ -65,7 +65,7 @@ class BluetoothTransferThread(context: Context, device: Device, socket: Bluetoot
|
|||
new IntentFilter(BluetoothDevice.ACTION_ACL_DISCONNECTED))
|
||||
|
||||
send(crypto.sign(new Message(new MessageHeader(ConnectionInfo.Type,
|
||||
Address.Null, Address.Null, 0), new ConnectionInfo(crypto.getLocalPublicKey))))
|
||||
Address.Null, Address.Null, 0, 0), new ConnectionInfo(crypto.getLocalPublicKey))))
|
||||
|
||||
while (socket.isConnected) {
|
||||
try {
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
package com.nutomic.ensichat.core
|
||||
|
||||
import java.security.InvalidKeyException
|
||||
import java.util.{TimerTask, Timer, Date}
|
||||
import java.util.Date
|
||||
|
||||
import com.nutomic.ensichat.core.body._
|
||||
import com.nutomic.ensichat.core.header.{ContentHeader, MessageHeader}
|
||||
import com.nutomic.ensichat.core.header.{AbstractHeader, ContentHeader, MessageHeader}
|
||||
import com.nutomic.ensichat.core.interfaces._
|
||||
import com.nutomic.ensichat.core.internet.InternetInterface
|
||||
import com.nutomic.ensichat.core.util._
|
||||
import com.typesafe.scalalogging.Logger
|
||||
import org.joda.time.{DateTime, Duration}
|
||||
import org.joda.time.Duration
|
||||
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
import scala.concurrent.Future
|
||||
|
@ -27,8 +27,6 @@ final class ConnectionHandler(settings: SettingsInterface, database: Database,
|
|||
|
||||
private val logger = Logger(this.getClass)
|
||||
|
||||
private val CheckMessageRetryInterval = Duration.standardMinutes(1)
|
||||
|
||||
private var transmissionInterfaces = Set[TransmissionInterface]()
|
||||
|
||||
private lazy val seqNumGenerator = new SeqNumGenerator(settings)
|
||||
|
@ -83,12 +81,13 @@ final class ConnectionHandler(settings: SettingsInterface, database: Database,
|
|||
FutureHelper {
|
||||
val messageId = settings.get("message_id", 0L)
|
||||
val header = new ContentHeader(crypto.localAddress, target, seqNumGenerator.next(),
|
||||
body.contentType, Some(messageId), Some(new Date()))
|
||||
body.contentType, Some(messageId), Some(new Date()), AbstractHeader.InitialForwardingTokens)
|
||||
settings.put("message_id", messageId + 1)
|
||||
|
||||
val msg = new Message(header, body)
|
||||
val encrypted = crypto.encryptAndSign(msg)
|
||||
router.forwardMessage(encrypted)
|
||||
forwardMessageToRelays(encrypted)
|
||||
onNewMessage(msg)
|
||||
}
|
||||
}
|
||||
|
@ -98,7 +97,7 @@ final class ConnectionHandler(settings: SettingsInterface, database: Database,
|
|||
val seqNum = seqNumGenerator.next()
|
||||
val targetSeqNum = localRoutesInfo.getRoute(target).map(_.seqNum).getOrElse(-1)
|
||||
val body = new RouteRequest(target, seqNum, targetSeqNum, 0)
|
||||
val header = new MessageHeader(body.protocolType, crypto.localAddress, Address.Broadcast, seqNum)
|
||||
val header = new MessageHeader(body.protocolType, crypto.localAddress, Address.Broadcast, seqNum, 0)
|
||||
|
||||
val signed = crypto.sign(new Message(header, body))
|
||||
logger.trace(s"sending new $signed")
|
||||
|
@ -108,7 +107,7 @@ final class ConnectionHandler(settings: SettingsInterface, database: Database,
|
|||
private def replyRoute(target: Address, replyTo: Address): Unit = {
|
||||
val seqNum = seqNumGenerator.next()
|
||||
val body = new RouteReply(seqNum, 0)
|
||||
val header = new MessageHeader(body.protocolType, crypto.localAddress, replyTo, seqNum)
|
||||
val header = new MessageHeader(body.protocolType, crypto.localAddress, replyTo, seqNum, 0)
|
||||
|
||||
val signed = crypto.sign(new Message(header, body))
|
||||
logger.trace(s"sending new $signed")
|
||||
|
@ -118,7 +117,7 @@ final class ConnectionHandler(settings: SettingsInterface, database: Database,
|
|||
private def routeError(address: Address, packetSource: Option[Address]): Unit = {
|
||||
val destination = packetSource.getOrElse(Address.Broadcast)
|
||||
val header = new MessageHeader(RouteError.Type, crypto.localAddress, destination,
|
||||
seqNumGenerator.next())
|
||||
seqNumGenerator.next(), 0)
|
||||
val seqNum = localRoutesInfo.getRoute(address).map(_.seqNum).getOrElse(-1)
|
||||
val body = new RouteError(address, seqNum)
|
||||
|
||||
|
@ -211,6 +210,7 @@ final class ConnectionHandler(settings: SettingsInterface, database: Database,
|
|||
|
||||
if (msg.header.target != crypto.localAddress) {
|
||||
router.forwardMessage(msg)
|
||||
forwardMessageToRelays(msg)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -234,6 +234,20 @@ final class ConnectionHandler(settings: SettingsInterface, database: Database,
|
|||
onNewMessage(plainMsg)
|
||||
}
|
||||
|
||||
private def forwardMessageToRelays(message: Message): Unit = {
|
||||
var tokens = message.header.tokens
|
||||
val relays = database.pickLongestConnectionDevice(connections())
|
||||
var index = 0
|
||||
while (tokens > 1) {
|
||||
val forwardTokens = tokens / 2
|
||||
val headerCopy = message.header.asInstanceOf[ContentHeader].copy(tokens = forwardTokens)
|
||||
router.forwardMessage(message.copy(header = headerCopy), relays.lift(index))
|
||||
tokens -= forwardTokens
|
||||
database.updateMessageForwardingTokens(message, tokens)
|
||||
index += 1
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to send messages in [[MessageBuffer]] again, after we acquired a new route.
|
||||
*/
|
||||
|
@ -244,11 +258,8 @@ final class ConnectionHandler(settings: SettingsInterface, database: Database,
|
|||
}
|
||||
|
||||
private def noRouteFound(message: Message): Unit = {
|
||||
if (message.header.origin == crypto.localAddress) {
|
||||
messageBuffer.addMessage(message)
|
||||
requestRoute(message.header.target)
|
||||
} else
|
||||
routeError(message.header.target, Option(message.header.origin))
|
||||
messageBuffer.addMessage(message)
|
||||
requestRoute(message.header.target)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -317,6 +328,9 @@ final class ConnectionHandler(settings: SettingsInterface, database: Database,
|
|||
settings.get(SettingsInterface.KeyUserStatus, "")))
|
||||
callbacks.onConnectionsChanged()
|
||||
resendMissingRouteMessages()
|
||||
messageBuffer.getAllMessages
|
||||
.filter(_.header.tokens > 1)
|
||||
.foreach(forwardMessageToRelays)
|
||||
true
|
||||
}
|
||||
|
||||
|
|
|
@ -7,6 +7,8 @@ import com.nutomic.ensichat.core.util.LocalRoutesInfo
|
|||
|
||||
object Router extends Comparator[Int] {
|
||||
|
||||
private val HopLimit = 20
|
||||
|
||||
/**
|
||||
* Compares which sequence number is newer.
|
||||
*
|
||||
|
@ -50,7 +52,7 @@ private[core] class Router(routesInfo: LocalRoutesInfo, send: (Address, Message)
|
|||
* true.
|
||||
*/
|
||||
def forwardMessage(msg: Message, nextHopOption: Option[Address] = None): Unit = {
|
||||
if (msg.header.hopCount + 1 >= msg.header.hopLimit)
|
||||
if (msg.header.hopCount + 1 >= Router.HopLimit)
|
||||
return
|
||||
|
||||
val nextHop = nextHopOption.getOrElse(msg.header.target)
|
||||
|
@ -79,10 +81,8 @@ private[core] class Router(routesInfo: LocalRoutesInfo, send: (Address, Message)
|
|||
*/
|
||||
private def incHopCount(msg: Message): Message = {
|
||||
val updatedHeader = msg.header match {
|
||||
case ch: ContentHeader => new ContentHeader(ch.origin, ch.target, ch.seqNum, ch.contentType,
|
||||
ch.messageId, ch.time, ch.hopCount + 1, ch.hopLimit)
|
||||
case mh: MessageHeader => new MessageHeader(mh.protocolType, mh.origin, mh.target, mh.seqNum,
|
||||
mh.hopCount + 1, mh.hopLimit)
|
||||
case ch: ContentHeader => ch.copy(hopCount = ch.hopCount + 1)
|
||||
case mh: MessageHeader => mh.copy(hopCount = mh.hopCount + 1)
|
||||
}
|
||||
new Message(updatedHeader, msg.crypto, msg.body)
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ import com.nutomic.ensichat.core.util.BufferUtils
|
|||
|
||||
object AbstractHeader {
|
||||
|
||||
val DefaultHopLimit = 20
|
||||
val InitialForwardingTokens = 3
|
||||
|
||||
val Version = 0
|
||||
|
||||
|
@ -25,7 +25,7 @@ object AbstractHeader {
|
|||
trait AbstractHeader {
|
||||
|
||||
def protocolType: Int
|
||||
def hopLimit: Int
|
||||
def tokens: Int
|
||||
def hopCount: Int
|
||||
def origin: Address
|
||||
def target: Address
|
||||
|
@ -41,7 +41,7 @@ trait AbstractHeader {
|
|||
|
||||
BufferUtils.putUnsignedByte(b, AbstractHeader.Version)
|
||||
BufferUtils.putUnsignedByte(b, protocolType)
|
||||
BufferUtils.putUnsignedByte(b, hopLimit)
|
||||
BufferUtils.putUnsignedByte(b, tokens)
|
||||
BufferUtils.putUnsignedByte(b, hopCount)
|
||||
|
||||
BufferUtils.putUnsignedInt(b, length + contentLength)
|
||||
|
@ -63,7 +63,7 @@ trait AbstractHeader {
|
|||
override def equals(a: Any): Boolean = a match {
|
||||
case o: AbstractHeader =>
|
||||
protocolType == o.protocolType &&
|
||||
hopLimit == o.hopLimit &&
|
||||
tokens == o.tokens &&
|
||||
hopCount == o.hopCount &&
|
||||
origin == o.origin &&
|
||||
target == o.target &&
|
||||
|
|
|
@ -25,7 +25,7 @@ object ContentHeader {
|
|||
val time = BufferUtils.getUnsignedInt(b)
|
||||
|
||||
val ch = new ContentHeader(mh.origin, mh.target, mh.seqNum, contentType, Some(messageId),
|
||||
Some(new Date(time * 1000)), mh.hopCount)
|
||||
Some(new Date(time * 1000)), mh.tokens, mh.hopCount)
|
||||
|
||||
val remaining = new Array[Byte](b.remaining())
|
||||
b.get(remaining, 0, b.remaining())
|
||||
|
@ -45,8 +45,8 @@ final case class ContentHeader(override val origin: Address,
|
|||
contentType: Int,
|
||||
override val messageId: Some[Long],
|
||||
override val time: Some[Date],
|
||||
override val hopCount: Int = 0,
|
||||
override val hopLimit: Int = AbstractHeader.DefaultHopLimit)
|
||||
override val tokens: Int,
|
||||
override val hopCount: Int = 0)
|
||||
extends AbstractHeader {
|
||||
|
||||
override val protocolType = ContentHeader.ContentMessageType
|
||||
|
|
|
@ -23,7 +23,7 @@ object MessageHeader {
|
|||
if (version != AbstractHeader.Version)
|
||||
throw new ReadMessageException("Failed to parse message with unsupported version " + version)
|
||||
val protocolType = BufferUtils.getUnsignedByte(b)
|
||||
val hopLimit = BufferUtils.getUnsignedByte(b)
|
||||
val tokens = BufferUtils.getUnsignedByte(b)
|
||||
val hopCount = BufferUtils.getUnsignedByte(b)
|
||||
|
||||
val length = BufferUtils.getUnsignedInt(b)
|
||||
|
@ -34,7 +34,7 @@ object MessageHeader {
|
|||
|
||||
val seqNum = BufferUtils.getUnsignedShort(b)
|
||||
|
||||
(new MessageHeader(protocolType, origin, target, seqNum, hopCount, hopLimit), length.toInt)
|
||||
(new MessageHeader(protocolType, origin, target, seqNum, tokens, hopCount), length.toInt)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -48,8 +48,8 @@ final case class MessageHeader(override val protocolType: Int,
|
|||
override val origin: Address,
|
||||
override val target: Address,
|
||||
override val seqNum: Int,
|
||||
override val hopCount: Int = 0,
|
||||
override val hopLimit: Int = AbstractHeader.DefaultHopLimit)
|
||||
override val tokens: Int,
|
||||
override val hopCount: Int = 0)
|
||||
extends AbstractHeader {
|
||||
|
||||
def length: Int = MessageHeader.Length
|
||||
|
|
|
@ -50,7 +50,7 @@ private[core] class InternetConnectionThread(socket: Socket, crypto: Crypto,
|
|||
logger.info("Connection opened to " + socket.getInetAddress)
|
||||
|
||||
send(crypto.sign(new Message(new MessageHeader(ConnectionInfo.Type,
|
||||
Address.Null, Address.Null, 0), new ConnectionInfo(crypto.getLocalPublicKey))))
|
||||
Address.Null, Address.Null, 0, 0), new ConnectionInfo(crypto.getLocalPublicKey))))
|
||||
|
||||
try {
|
||||
socket.setKeepAlive(true)
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
package com.nutomic.ensichat.core.util
|
||||
|
||||
import java.io.File
|
||||
import java.sql.DriverManager
|
||||
import java.util.Date
|
||||
|
||||
import com.nutomic.ensichat.core.body.Text
|
||||
import com.nutomic.ensichat.core.header.ContentHeader
|
||||
import com.nutomic.ensichat.core.interfaces.CallbackInterface
|
||||
import com.nutomic.ensichat.core.interfaces.{CallbackInterface, SettingsInterface}
|
||||
import com.nutomic.ensichat.core.{Address, Message, User}
|
||||
import com.typesafe.scalalogging.Logger
|
||||
import org.joda.time
|
||||
|
@ -20,10 +21,15 @@ import scala.concurrent.duration.Duration
|
|||
*
|
||||
* @param path The database file.
|
||||
*/
|
||||
class Database(path: File, callbackInterface: CallbackInterface) {
|
||||
class Database(path: File, settings: SettingsInterface, callbackInterface: CallbackInterface) {
|
||||
|
||||
private val logger = Logger(this.getClass)
|
||||
|
||||
private val DatabaseVersionKey = "database_version"
|
||||
private val DatabaseVersion = 2
|
||||
|
||||
private val DatabasePath = "jdbc:h2:" + path.getAbsolutePath + ";DATABASE_TO_UPPER=false"
|
||||
|
||||
private class Messages(tag: Tag) extends Table[Message](tag, "MESSAGES") {
|
||||
def id = primaryKey("id", (origin, messageId))
|
||||
def origin = column[String]("origin")
|
||||
|
@ -31,20 +37,22 @@ class Database(path: File, callbackInterface: CallbackInterface) {
|
|||
def messageId = column[Long]("message_id")
|
||||
def text = column[String]("text")
|
||||
def date = column[Long]("date")
|
||||
def * = (origin, target, messageId, text, date) <> [Message, (String, String, Long, String, Long)]( {
|
||||
def tokens = column[Int]("tokens")
|
||||
def * = (origin, target, messageId, text, date, tokens) <> [Message, (String, String, Long, String, Long, Int)]( {
|
||||
tuple =>
|
||||
val header = new ContentHeader(new Address(tuple._1),
|
||||
new Address(tuple._2),
|
||||
-1,
|
||||
Text.Type,
|
||||
Some(tuple._3),
|
||||
Some(new Date(tuple._5)))
|
||||
Some(new Date(tuple._5)),
|
||||
tuple._6)
|
||||
val body = new Text(tuple._4)
|
||||
new Message(header, body)
|
||||
}, message =>
|
||||
Option((message.header.origin.toString(), message.header.target.toString(),
|
||||
message.header.messageId.get, message.body.asInstanceOf[Text].text,
|
||||
message.header.time.get.getTime))
|
||||
message.header.time.get.getTime, message.header.tokens))
|
||||
)
|
||||
}
|
||||
private val messages = TableQuery[Messages]
|
||||
|
@ -67,7 +75,7 @@ class Database(path: File, callbackInterface: CallbackInterface) {
|
|||
}
|
||||
private val knownDevices = TableQuery[KnownDevices]
|
||||
|
||||
private val db = Database.forURL("jdbc:h2:" + path.getAbsolutePath, driver = "org.h2.Driver")
|
||||
private val db = Database.forURL(DatabasePath, driver = "org.h2.Driver")
|
||||
|
||||
// Create tables if database doesn't exist.
|
||||
{
|
||||
|
@ -75,7 +83,25 @@ class Database(path: File, callbackInterface: CallbackInterface) {
|
|||
val dbFile = new File(path.getAbsolutePath + ".mv.db")
|
||||
if (!dbFile.exists()) {
|
||||
logger.info("Database does not exist, creating tables")
|
||||
Await.result(db.run((messages.schema ++ contacts.schema).create), Duration.Inf)
|
||||
val query = (messages.schema ++ contacts.schema ++ knownDevices.schema).create
|
||||
Await.result(db.run(query), Duration.Inf)
|
||||
settings.put(DatabaseVersionKey, DatabaseVersion)
|
||||
}
|
||||
}
|
||||
|
||||
// Apparently, slick doesn't support ALTER TABLE, so we have to write raw SQL for this...
|
||||
{
|
||||
val oldVersion = settings.get(DatabaseVersionKey, 0)
|
||||
if (oldVersion != DatabaseVersion) {
|
||||
logger.info(s"Upgrading database from version $oldVersion to $DatabaseVersion")
|
||||
val connection = DriverManager.getConnection(DatabasePath)
|
||||
if (oldVersion <= 2) {
|
||||
connection.createStatement().executeUpdate("ALTER TABLE MESSAGES ADD COLUMN (tokens INT);")
|
||||
connection.commit()
|
||||
Await.result(db.run(knownDevices.schema.create), Duration.Inf)
|
||||
}
|
||||
connection.close()
|
||||
settings.put(DatabaseVersionKey, DatabaseVersion)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -138,8 +164,25 @@ class Database(path: File, callbackInterface: CallbackInterface) {
|
|||
Await.result(db.run(query), Duration.Inf)
|
||||
}
|
||||
|
||||
def getKnownDevices: Seq[(Address, time.Duration)] = {
|
||||
Await.result(db.run(knownDevices.result), Duration.Inf)
|
||||
/**
|
||||
* Returns neighbors sorted by connection time, according to [[KnownDevices]].
|
||||
*/
|
||||
def pickLongestConnectionDevice(connections: Set[Address]): List[Address] = {
|
||||
val map = Await.result(db.run(knownDevices.result), Duration.Inf).toMap
|
||||
connections
|
||||
.toList
|
||||
.sortBy(map(_).getMillis)
|
||||
.reverse
|
||||
}
|
||||
|
||||
def updateMessageForwardingTokens(message: Message, tokens: Int): Unit = {
|
||||
val query = messages.filter { c =>
|
||||
c.origin === message.header.origin.toString &&
|
||||
c.messageId === message.header.messageId
|
||||
}
|
||||
.map(_.tokens)
|
||||
.update(tokens)
|
||||
Await.result(db.run(query), Duration.Inf)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -82,6 +82,8 @@ class MessageBuffer(retryMessageSending: (Address) => Unit) {
|
|||
ret.map(_.message)
|
||||
}
|
||||
|
||||
def getAllMessages: Set[Message] = values.map(_.message)
|
||||
|
||||
private def handleTimeouts(): Unit = {
|
||||
values = values.filter { e =>
|
||||
e.retryCount < MaxRetryCount
|
||||
|
|
|
@ -40,7 +40,7 @@ class MessageTest extends TestCase {
|
|||
}
|
||||
|
||||
def testSerializeSigned(): Unit = {
|
||||
val header = new MessageHeader(ConnectionInfo.Type, AddressTest.a4, AddressTest.a2, 0)
|
||||
val header = new MessageHeader(ConnectionInfo.Type, AddressTest.a4, AddressTest.a2, 0, 3)
|
||||
val m = new Message(header, ConnectionInfoTest.generateCi())
|
||||
|
||||
val signed = crypto.sign(m)
|
||||
|
|
|
@ -46,7 +46,7 @@ class RouterTest extends TestCase {
|
|||
assertEquals(msg.header.seqNum, m.header.seqNum)
|
||||
assertEquals(msg.header.protocolType, m.header.protocolType)
|
||||
assertEquals(msg.header.hopCount + 1, m.header.hopCount)
|
||||
assertEquals(msg.header.hopLimit, m.header.hopLimit)
|
||||
assertEquals(msg.header.tokens, m.header.tokens)
|
||||
assertEquals(msg.body, m.body)
|
||||
assertEquals(msg.crypto, m.crypto)
|
||||
}, _ => ())
|
||||
|
@ -93,14 +93,14 @@ class RouterTest extends TestCase {
|
|||
|
||||
def testHopLimit(): Unit = Range(19, 22).foreach { i =>
|
||||
val msg = new Message(
|
||||
new ContentHeader(AddressTest.a1, AddressTest.a2, 1, 1, Some(1), Some(new Date()), i), new Text(""))
|
||||
new ContentHeader(AddressTest.a1, AddressTest.a2, 1, 1, Some(1), Some(new Date()), 3, i), new Text(""))
|
||||
val router = new Router(new LocalRoutesInfo(neighbors), (a, m) => fail(), _ => ())
|
||||
router.forwardMessage(msg)
|
||||
}
|
||||
|
||||
private def generateMessage(sender: Address, receiver: Address, seqNum: Int): Message = {
|
||||
val header = new ContentHeader(sender, receiver, seqNum, UserInfo.Type, Some(5),
|
||||
Some(new GregorianCalendar(2014, 6, 10).getTime))
|
||||
Some(new GregorianCalendar(2014, 6, 10).getTime), 3)
|
||||
new Message(header, new UserInfo("", ""))
|
||||
}
|
||||
|
||||
|
|
|
@ -11,9 +11,9 @@ object MessageHeaderTest {
|
|||
0)
|
||||
|
||||
val h2 = new MessageHeader(ContentHeader.ContentMessageType, Address.Null, Address.Broadcast,
|
||||
ContentHeader.SeqNumRange.last, 0xff)
|
||||
ContentHeader.SeqNumRange.last, 0xff, 3)
|
||||
|
||||
val h3 = new MessageHeader(ContentHeader.ContentMessageType, Address.Broadcast, Address.Null, 0)
|
||||
val h3 = new MessageHeader(ContentHeader.ContentMessageType, Address.Broadcast, Address.Null, 0, 3)
|
||||
|
||||
val headers = Set(h1, h2, h3)
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@ class RouteMessageInfoTest extends TestCase {
|
|||
* Test case in which we have an entry with the same type, origin and target.
|
||||
*/
|
||||
def testSameMessage(): Unit = {
|
||||
val header = new MessageHeader(RouteRequest.Type, AddressTest.a1, AddressTest.a2, 1)
|
||||
val header = new MessageHeader(RouteRequest.Type, AddressTest.a1, AddressTest.a2, 1, 0)
|
||||
val msg = new Message(header, new RouteRequest(AddressTest.a3, 2, 3, 1))
|
||||
val rmi = new RouteMessageInfo()
|
||||
assertFalse(rmi.isMessageRedundant(msg))
|
||||
|
@ -24,12 +24,12 @@ class RouteMessageInfoTest extends TestCase {
|
|||
* Forward a message with a seqnum that is older than the latest.
|
||||
*/
|
||||
def testSeqNumOlder(): Unit = {
|
||||
val header1 = new MessageHeader(RouteRequest.Type, AddressTest.a1, AddressTest.a2, 1)
|
||||
val header1 = new MessageHeader(RouteRequest.Type, AddressTest.a1, AddressTest.a2, 1, 0)
|
||||
val msg1 = new Message(header1, new RouteRequest(AddressTest.a3, 0, 0, 0))
|
||||
val rmi = new RouteMessageInfo()
|
||||
assertFalse(rmi.isMessageRedundant(msg1))
|
||||
|
||||
val header2 = new MessageHeader(RouteRequest.Type, AddressTest.a1, AddressTest.a2, 3)
|
||||
val header2 = new MessageHeader(RouteRequest.Type, AddressTest.a1, AddressTest.a2, 3, 0)
|
||||
val msg2 = new Message(header2, new RouteRequest(AddressTest.a3, 2, 0, 0))
|
||||
assertTrue(rmi.isMessageRedundant(msg2))
|
||||
}
|
||||
|
@ -38,12 +38,12 @@ class RouteMessageInfoTest extends TestCase {
|
|||
* Announce a route with a metric that is worse than the existing one.
|
||||
*/
|
||||
def testMetricWorse(): Unit = {
|
||||
val header1 = new MessageHeader(RouteRequest.Type, AddressTest.a1, AddressTest.a2, 1)
|
||||
val header1 = new MessageHeader(RouteRequest.Type, AddressTest.a1, AddressTest.a2, 1, 0)
|
||||
val msg1 = new Message(header1, new RouteRequest(AddressTest.a3, 1, 0, 2))
|
||||
val rmi = new RouteMessageInfo()
|
||||
assertFalse(rmi.isMessageRedundant(msg1))
|
||||
|
||||
val header2 = new MessageHeader(RouteRequest.Type, AddressTest.a1, AddressTest.a2, 2)
|
||||
val header2 = new MessageHeader(RouteRequest.Type, AddressTest.a1, AddressTest.a2, 2, 0)
|
||||
val msg2 = new Message(header2, new RouteRequest(AddressTest.a3, 1, 0, 4))
|
||||
assertTrue(rmi.isMessageRedundant(msg2))
|
||||
}
|
||||
|
@ -52,12 +52,12 @@ class RouteMessageInfoTest extends TestCase {
|
|||
* Announce route with a better metric.
|
||||
*/
|
||||
def testMetricBetter(): Unit = {
|
||||
val header1 = new MessageHeader(RouteRequest.Type, AddressTest.a1, AddressTest.a2, 1)
|
||||
val header1 = new MessageHeader(RouteRequest.Type, AddressTest.a1, AddressTest.a2, 1, 0)
|
||||
val msg1 = new Message(header1, new RouteReply(0, 4))
|
||||
val rmi = new RouteMessageInfo()
|
||||
assertFalse(rmi.isMessageRedundant(msg1))
|
||||
|
||||
val header2 = new MessageHeader(RouteRequest.Type, AddressTest.a1, AddressTest.a2, 2)
|
||||
val header2 = new MessageHeader(RouteRequest.Type, AddressTest.a1, AddressTest.a2, 2, 0)
|
||||
val msg2 = new Message(header2, new RouteReply(0, 2))
|
||||
assertFalse(rmi.isMessageRedundant(msg2))
|
||||
}
|
||||
|
@ -68,7 +68,7 @@ class RouteMessageInfoTest extends TestCase {
|
|||
def testTimeout(): Unit = {
|
||||
val rmi = new RouteMessageInfo()
|
||||
DateTimeUtils.setCurrentMillisFixed(DateTime.now.getMillis)
|
||||
val header = new MessageHeader(RouteRequest.Type, AddressTest.a1, AddressTest.a2, 1)
|
||||
val header = new MessageHeader(RouteRequest.Type, AddressTest.a1, AddressTest.a2, 1, 0)
|
||||
val msg = new Message(header, new RouteRequest(AddressTest.a3, 0, 0, 0))
|
||||
assertFalse(rmi.isMessageRedundant(msg))
|
||||
|
||||
|
|
|
@ -45,12 +45,12 @@ class LocalNode(val index: Int, configFolder: File) extends CallbackInterface {
|
|||
private val databaseFile = new File(configFolder, "database")
|
||||
private val keyFolder = new File(configFolder, "keys")
|
||||
|
||||
private val database = new Database(databaseFile, this)
|
||||
private val settings = new SettingsInterface {
|
||||
private var values = Map[String, Any]()
|
||||
override def get[T](key: String, default: T): T = values.get(key).map(_.asInstanceOf[T]).getOrElse(default)
|
||||
override def put[T](key: String, value: T): Unit = values += (key -> value.asInstanceOf[Any])
|
||||
}
|
||||
private val database = new Database(databaseFile, settings, this)
|
||||
|
||||
val crypto = new Crypto(settings, keyFolder)
|
||||
val connectionHandler = new ConnectionHandler(settings, database, this, crypto, 0, port)
|
||||
|
|
|
@ -20,40 +20,141 @@ import scalax.file.Path
|
|||
*/
|
||||
object Main extends App {
|
||||
|
||||
val nodes = createMesh()
|
||||
System.out.println("\n\nAll nodes connected!\n\n")
|
||||
// NOTE: These tests are somewhat fragile, and might fail randomly. It helps to run only
|
||||
// one of the following functions at a time.
|
||||
testNeighborSending()
|
||||
testMeshMessageSending()
|
||||
testIndirectRelay()
|
||||
testNeighborRelay()
|
||||
testMessageDeliveryOnConnect()
|
||||
testSendDelayed()
|
||||
testRouteChange()
|
||||
|
||||
sendMessages(nodes)
|
||||
System.out.println("\n\nMessages sent!\n\n")
|
||||
private def testNeighborSending(): Unit = {
|
||||
val node1 = Await.result(createNode(1), Duration.Inf)
|
||||
val node2 = Await.result(createNode(2), Duration.Inf)
|
||||
|
||||
// Stop node 1, forcing route errors and messages to use the (longer) path via nodes 7 and 8.
|
||||
nodes(1).connectionHandler.stop()
|
||||
System.out.println("Node 1 stopped")
|
||||
sendMessages(nodes)
|
||||
System.out.println("\n\nMessages after route change sent!\n\n")
|
||||
connectNodes(node1, node2)
|
||||
sendMessage(node1, node2)
|
||||
|
||||
// Create new node 9, send message from node 0 to its address, before actually connecting it.
|
||||
// The message is automatically delivered when node 9 connects as neighbor.
|
||||
val node9 = Await.result(createNode(9), Duration.Inf)
|
||||
val timer = new Timer()
|
||||
timer.schedule(new TimerTask {
|
||||
override def run(): Unit = {
|
||||
connectNodes(nodes(0), node9)
|
||||
}
|
||||
}, Duration(10, TimeUnit.SECONDS).toMillis)
|
||||
sendMessage(nodes(0), node9, 30)
|
||||
Set(node1, node2).foreach(_.stop())
|
||||
|
||||
// Create new node 10, send message from node 7 to its address, before connecting it to the mesh.
|
||||
// The message is delivered after node 7 starts a route discovery triggered by the message buffer.
|
||||
val node10 = Await.result(createNode(10), Duration.Inf)
|
||||
timer.schedule(new TimerTask {
|
||||
override def run(): Unit = {
|
||||
connectNodes(nodes(0), node10)
|
||||
timer.cancel()
|
||||
}
|
||||
}, Duration(5, TimeUnit.SECONDS).toMillis)
|
||||
sendMessage(nodes(7), node10, 30)
|
||||
System.out.println("\n\nMessages after delay sent!\n\n")
|
||||
System.out.println("Test neighbor sending successful!")
|
||||
}
|
||||
|
||||
private def testNeighborRelay(): Unit = {
|
||||
val nodes = createNodes(3)
|
||||
|
||||
connectNodes(nodes(0), nodes(1))
|
||||
|
||||
val timer = new Timer()
|
||||
timer.schedule(new TimerTask {
|
||||
override def run(): Unit = {
|
||||
nodes(0).stop()
|
||||
connectNodes(nodes(1), nodes(2))
|
||||
}
|
||||
}, Duration(10, TimeUnit.SECONDS).toMillis)
|
||||
sendMessage(nodes(0), nodes(2), 30)
|
||||
|
||||
timer.cancel()
|
||||
nodes.foreach(_.stop())
|
||||
|
||||
System.out.println("Test neighbor relay successful!")
|
||||
}
|
||||
|
||||
private def testIndirectRelay(): Unit = {
|
||||
val nodes = createNodes(5)
|
||||
|
||||
|
||||
connectNodes(nodes(0), nodes(1))
|
||||
connectNodes(nodes(1), nodes(2))
|
||||
connectNodes(nodes(2), nodes(3))
|
||||
|
||||
val timer = new Timer()
|
||||
timer.schedule(new TimerTask {
|
||||
override def run(): Unit = {
|
||||
nodes(0).stop()
|
||||
connectNodes(nodes(3), nodes(4))
|
||||
}
|
||||
}, Duration(10, TimeUnit.SECONDS).toMillis)
|
||||
sendMessage(nodes(0), nodes(4), 30)
|
||||
|
||||
timer.cancel()
|
||||
nodes.foreach(_.stop())
|
||||
|
||||
System.out.println("Test indirect sending successful!")
|
||||
}
|
||||
|
||||
private def testMeshMessageSending(): Unit = {
|
||||
val nodes = createMesh()
|
||||
|
||||
sendMessages(nodes)
|
||||
|
||||
nodes.foreach(_.stop())
|
||||
|
||||
System.out.println("Test mesh message sending successful!")
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop node 1, forcing route errors and messages to use the (longer) path via nodes 7 and 8.
|
||||
*/
|
||||
private def testRouteChange() {
|
||||
val nodes = createMesh()
|
||||
nodes(1).connectionHandler.stop()
|
||||
sendMessages(nodes)
|
||||
|
||||
nodes.foreach(_.stop())
|
||||
|
||||
System.out.println("Test route change successful!")
|
||||
}
|
||||
|
||||
/**
|
||||
* Create new node 9, send message from node 0 to its address, before actually connecting it.
|
||||
* The message is automatically delivered when node 9 connects as neighbor.
|
||||
*/
|
||||
private def testMessageDeliveryOnConnect() {
|
||||
val nodes = createMesh()
|
||||
val node9 = Await.result(createNode(9), Duration.Inf)
|
||||
val timer = new Timer()
|
||||
timer.schedule(new TimerTask {
|
||||
override def run(): Unit = {
|
||||
connectNodes(nodes(0), node9)
|
||||
timer.cancel()
|
||||
}
|
||||
}, Duration(10, TimeUnit.SECONDS).toMillis)
|
||||
sendMessage(nodes(0), node9, 30)
|
||||
|
||||
(nodes :+ node9).foreach(_.stop())
|
||||
|
||||
System.out.println("Test message delivery on connect successful!")
|
||||
}
|
||||
|
||||
/**
|
||||
* Create new node 10, send message from node 7 to its address, before connecting it to the mesh.
|
||||
* The message is delivered after node 7 starts a route discovery triggered by the message buffer.
|
||||
*/
|
||||
private def testSendDelayed(): Unit = {
|
||||
val nodes = createMesh()
|
||||
val timer = new Timer()
|
||||
val node10 = Await.result(createNode(10), Duration.Inf)
|
||||
timer.schedule(new TimerTask {
|
||||
override def run(): Unit = {
|
||||
connectNodes(nodes(0), node10)
|
||||
timer.cancel()
|
||||
}
|
||||
}, Duration(5, TimeUnit.SECONDS).toMillis)
|
||||
sendMessage(nodes(7), node10, 30)
|
||||
|
||||
(nodes :+ node10).foreach(_.stop())
|
||||
|
||||
System.out.println("Test send delayed successful!")
|
||||
}
|
||||
|
||||
private def createNodes(count: Int): Seq[LocalNode] = {
|
||||
val nodes = Await.result(Future.sequence((0 until count).map(createNode)), Duration.Inf)
|
||||
nodes.foreach(n => System.out.println(s"Node ${n.index} has address ${n.crypto.localAddress}"))
|
||||
nodes
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new mesh with a predefined layout.
|
||||
|
@ -68,8 +169,7 @@ object Main extends App {
|
|||
* @return List of [[LocalNode]]s, ordered from 0 to 8.
|
||||
*/
|
||||
private def createMesh(): Seq[LocalNode] = {
|
||||
val nodes = Await.result(Future.sequence(0.to(8).map(createNode)), Duration.Inf)
|
||||
sys.addShutdownHook(nodes.foreach(_.stop()))
|
||||
val nodes = createNodes(9)
|
||||
|
||||
connectNodes(nodes(0), nodes(1))
|
||||
connectNodes(nodes(0), nodes(2))
|
||||
|
@ -82,7 +182,6 @@ object Main extends App {
|
|||
connectNodes(nodes(3), nodes(7))
|
||||
connectNodes(nodes(0), nodes(8))
|
||||
connectNodes(nodes(7), nodes(8))
|
||||
nodes.foreach(n => System.out.println(s"Node ${n.index} has address ${n.crypto.localAddress}"))
|
||||
|
||||
nodes
|
||||
}
|
||||
|
@ -119,7 +218,6 @@ object Main extends App {
|
|||
sendMessage(nodes(3), nodes(5))
|
||||
sendMessage(nodes(4), nodes(6))
|
||||
sendMessage(nodes(2), nodes(3))
|
||||
sendMessage(nodes(0), nodes(3))
|
||||
sendMessage(nodes(3), nodes(6))
|
||||
sendMessage(nodes(3), nodes(2))
|
||||
}
|
||||
|
|
|
@ -22,7 +22,7 @@ object Main extends App with CallbackInterface {
|
|||
|
||||
private lazy val settings = new Settings(ConfigFile)
|
||||
private lazy val crypto = new Crypto(settings, KeyFolder)
|
||||
private lazy val database = new Database(DatabaseFile, this)
|
||||
private lazy val database = new Database(DatabaseFile, settings, this)
|
||||
private lazy val connectionHandler = new ConnectionHandler(settings, database, this, crypto, 7)
|
||||
|
||||
init()
|
||||
|
|
Reference in a new issue