When you want to start developing an application using quickP2P API you first need to register as a provider. Then you should register your application in the network using one of the provider portals hooked up with the network. All entities of your application regarding quickP2P will be enclosed in application scope. This means that although multiple providers having many applications that can be presented in the network, your application sees only objects that belong to it. Of course what is usually common is that you can have your own super-node(s) hosted on your servers for a single application you have.
So, you start by registering as a provider. After successful registration and login, you get to dashboard screen: This is dashboard screen of provider portal:
Next thing you need to do is to register your application. Click on "Applications" button below provider contact data.
In the bottom left corner, there is a button "New Application Request". There you enter basic information about your application and submit a request to us so we could enable it. It will appear in applications list but it will not have status active until we enable it.
On a picture above you see example of enabled application. Notice 3 buttons:
These 4 options are most useful during development. It is so because programmers can easily see if they are making some mistake.
Let's take a look at peers list:
On the picture you can see 3 records of currently active peers we filtered by query:
"Properties.platform":"windows" (note quotes for later discussions about queries done from API)
Besides common networking data, we see in records, most important column would be "Properties". You notice that it has some JSON data format like:
{ "someproperty1" : "some value", "someproperty2" : "some value" }
What is interesting about that is that we can put any property by our will and later be able to search peers by their value. Our query is exactly that "Properties.platform":"windows" ."platform" property would be custom property someone decided it would need meta-data from its application (although he had OS column given automatically from API).
Let us now look at Virtual User List:
Virtual users and Virtual networks are part of the permanent indexing meta-data storage system. Do not mix that with peers. Record about peer exists as long as a peer is online. VUser and VNetwork on other hands we can imagine like two database tables on which we can do INSERT/UPDATE/DELETE/SELECT operations.
We also see "Properties" column. Here we can use in the same way as for peers. The difference is that this data stays saved after peer using VUser object closes session with super-node. You notice first property "Username". Because it is so common that peer-to-peer applications have username/password authentication, we explicitly embedded their use. Same stands for VNetwork "name" property. You can add other properties to your will. In special case if you don't need username/password authentication and you need to use permanent meta-data storage, you can just fix them in some way.
VUser has also two additional fields (looking at VNetwork) "Member Of networks" and "Currently bound Peers".
"Member Of networks" iled that stores list of the unique identification of VNetworks VUser belongs to. The list we see on the picture is from an application that does not use VNetworks at all so all are empty.
"Currently bound Peers" shows a list of peers that are currently authenticated with VUser object. Multiple peers can be authenticated using the same VUser object.
We will not talk about Virtual networks and objects lists because they are very similar to VUser list. We mentioned all important things regarding this document section.
Next thing towards starting our application development would be to decide what to use because on some platforms you can choose between .NET and native c++. Here is some listing that may help you decide:
|
.NET API |
Native C++ API |
Java wrapper for native c++ API |
Windows |
Normal .Net DLL |
.dll +.lib+headers |
Dll + java wrapper sources |
Linux |
Mono |
.so + headers |
so + java wrapper sources |
Mac OS |
Mono |
Cocoa .a + headers - you can mix with Obj C++ or use just c++ |
|
IOS |
Mono |
Cocoa touch .a + headers - you can mix with Obj C++ |
|
Android |
Mono |
(NDK) .so + headers |
(SDK)so + java wrapper sources |
1 Connection manager and result acceptance handlers/callbacks
4 Peer state tracing / changes notifications
6 NAT traversal - Direct communication tunnel creation between peers
8 Security, key exchange and data encryption/decryption
9 Add firewall exception for your application in application installer
To use quickP2P with your .NET(or Mono) project you first need to add a reference to quickP2Plib. Then include it in the class file where you need to use it and that's pretty much all. QuickP2Plib .NET works both with x86/x64 so you don't need to worry about that regarding this particular library. If you use Mono you need Mono build for the particular platform.
ConnectionManager is the main object you use with API. All client operations are initiated using methods in this class with exception of virtual index manager which is a subobject of ConnectionManager(ConnnectionManager.VNIndexManager).
quickP2P API is based on the asynchronous model. You initiate operation by making a request and you wait for the result in the event handler or provided the callback. For most of the requests, there is an option to choose between event handler and callback to handle response depending what you want to do or what you prefer.
This would be the comparison of using events and callbacks.
- When you hook event once, it will execute response every time correspondent request arrives until you unhook it.
- If you have both events hooked and callback provided to execute on the request completion first event is triggered then the callback
- quickP2P will execute event handlers on UI thread (if UI thread available). This makes easy for you to interact with visual elements directly from handler method. But that also creates one dangerous situation if you start long running an operation like large file transfer or video transfer in handler method your application UI may block. So, in this case, create a new thread to run that. That would be the best option. Or place something like Application.doEvents() inside the long-running loop if you need "on-fly" solution. To demonstrate that, we will turn again to demo project provided with the application. See Form1.cs
In some moment we initiate tunnel creation to another peer:
....
//we found peer we will connect to now. Note that track_connect_transaction_uid will match TransactionUID argument in [void cm_onPeerConnect(...]
Guid track_connect_transaction_uid =
cm.Connect(FoundPeers.First(), ConnectionType.TCP);//NEXT WE WAIT FOR RESULT IN HANDLER [void cm_onPeerConnect(...] AND ON OTHER SIDE [void cm_onPeerAccepted(...] will trigger instead
....
When operation finishes with success, this handler will be triggered:
....
//After successful connect operation this handler is triggered
void cm_onPeerConnect(Peer Peer, Guid TransactionUID)
{
//TUNNEL IS CREATED!
//Peer.Socket is System.Net.Sockets.Socket object!
//EVENTS ARE RAISED ON UI THREAD MAKING EASIER FOR YOU TO INTERACT DIRECTLY WITH UI FROM HANDLER BODY
//BUT THAT HAS SIDE EFFECT THAT LONG RUNNING OPERATIONS COULD BLOCK UI SO WE EXECUTE THEM IN SEPARATE THREAD
Thread t = new Thread(new ParameterizedThreadStart(delegate(object state)
{
var FS = System.IO.File.OpenRead(FileToSend);//we open file for reading
Peer.Socket.Send(BitConverter.GetBytes(FS.Length));//we send file length first
//we read and send file blocks
byte[] buff = new byte[8192];
int read = 0;
while ((read = FS.Read(buff, 0, buff.Length)) > 0)
Peer.Socket.Send(buff, 0, read, System.Net.Sockets.SocketFlags.None);
FS.Close();
//we wait for some response as confirmation
read = Peer.Socket.Receive(buff);
if (read > 0)
showMessageFromUIThread("File Sent!");
//we close socket
Peer.Socket.Close();
}));
t.Start();
}
....
So you see we have created a new thread to run file transfer in anonymous thread method leaving UI thread to do its job. Note that for showing message box on file transfer completion we did not just call MessageBox.Show("....."). Instead we called showMessageFromUIThread("File Sent!");. For file transfer, we created a separate thread. But when we finished and wanted to show message box which is a visual element we needed to make sure that message box is executed for UI thread. So that method looks like this:
delegate void showMessageFromUIThreadDelegate(string message);
public void showMessageFromUIThread(string message) {
if (this.InvokeRequired){
showMessageFromUIThreadDelegate bmd = new showMessageFromUIThreadDelegate(showMessageFromUIThread);
this.Invoke(bmd, new object[] { message });//we this invoke method on UI thread
}else //we are now in UI thread
MessageBox.Show(message);
}
Callbacks are usually provided as optional arguments to request invoking methods. They are executed on request completion only once .
Callback is executed from "some" thread that accepted request respons. Watch for that when interacting with UI elements.
Callbacks are handy because they provide you a way to concentrate request and response handler execution code in one place. So, they are good for things like connect operation. You don't need to distinguish from which request response came back using Transaction UID . If your application is multi-purpose let's say transfers files and enables video chat and you want to have two connections to the same peer at once and you use event handler:
....
cm.onPeerConnect += new ConnectionManager.PeerConnect(cm_onPeerConnect);
Guid track_file_transaction_uid;
Guid track_video_transaction_uid;
....
track_file_transaction_uid =
cm.Connect(PeerToConnectTo, ConnectionType.TCP);
....
....
track_video_transaction_uid =
cm.Connect(PeerToConnectTo, ConnectionType.TCP);
....
void cm_onPeerConnect(Peer PeerConnectedTo, Guid TransactionUID)
{
if (TransactionUID == track_file_transaction_uid){
//do file transfer
}
else if (TransactionUID == track_video_transaction_uid) {
//do video transfer
}
}
....
So we had to check if tunnel creation was for file or video by comparing UIDs. The same thing with callback would look like this:
Guid track_file_transaction_uid;
Guid track_video_transaction_uid;
track_file_transaction_uid =
cm.Connect(PeerToConnectTo, ConnectionType.TCP, callback: new ConnectionManager.PeerConnectCallback(delegate(Peer PeerConnectedTo, Guid PeerCheckpointUID, Guid PeerUID, Guid TransactionUID,bool Success, quickP2PLib.ConnectionManager.PeerConnectFailureReason FailReason) {
//do file transfer
}));
track_video_transaction_uid =
cm.Connect(PeerToConnectTo, ConnectionType.TCP, callback: new ConnectionManager.PeerConnectCallback(delegate(Peer PeerConnectedTo, Guid PeerCheckpointUID, Guid PeerUID, Guid TransactionUID,bool Success, quickP2PLib.ConnectionManager.PeerConnectFailureReason FailReason) {
//do video transfer
}));
Here is for example ConnectionManger.Open request with the provided callback to execute:
.........
ConnectionManager cm = new ConnectionManager(new IPEndPoint(checkpoint_address,
checkpoint_port));
.........
cm.Open(new ConnectionManager.SessionOpenCallback(delegate(ConnectionManager sender, bool success, ConnectionManager.SessionFailReason Failreason) {
//I'm an anonymous callback handler method
if (success)
showMessageFromUIThread("From callback: Session opened first time");
else
showMessageFromUIThread("From callback: Session open failed because of this reason:" + Failreason.ToString());
}));
.........
Unlike event provided callback is executed once. So in this particular case, if ConnectionManager is set to automatically recreate session in case of temporal loss of connection with super-node (default behavior - for example, you lost internet connection for some time), the callback would not trigger on session re-establishment but event handler would.
The equivalent thing done with events looks like this:
.........
ConnectionManager cm = new ConnectionManager(new IPEndPoint(checkpoint_address,
checkpoint_port));
//raised when we connect to checkpoint, or session automatically recovers after downtime (example you drive your laptop through underground tunnel and you have wifi connection)
cm.onSessionOpenSuccess += new ConnectionManager.SessionOpenSuccess(cm_onSessionOpenSuccess);
//raised when we try to connect to checkpoint, but session creation fails because of some reason
cm.onSessionOpenFailure += new ConnectionManager.SessionOpenFailure(cm_onSessionOpenFailure);
.........
void cm_onSessionOpenSuccess(ConnectionManager ConnectionManager)
{//we have successfully registered peer in supernode network!
MessageBox.Show("From event handler: Session opened first time or re-established");
}
void cm_onSessionOpenFailure(ConnectionManager ConnectionManager, ConnectionManager.SessionFailReason Reason)
{//something went wrong
MessageBox.Show("From event handler: Session open failed because of this reason:" + Reason.ToString());
}
.........
For quickP2P client, to be able to do any operation like tunnel opening, sending an instant message, searching peers ... we first need to join an abstract peer-to-peer network like you connect to the internet when your computer powers up. Steps for joining application peer to the super-node network would be this:
1 - Create ConnectionManager object to handle operations, set required C.M. properties, handles...
It's always required to set access tokens:
cm.setAccessTokens(Guid.Parse("00000000-XXXX-XXXX-XXXX-XXXXXXXXXXXX"), Guid.Parse("00000000-XXXX-XXXX-XXXX-XXXXXXXXXXXX"), "");
First argument is provider UID , second application UID , third is access key. You can see this values in you provider portal.
2 - Set some meta-data so other peers can find us and we can find them by filtering values
cm.LocalPeer["some_meta"] = "bla bla bla";
cm.LocalPeer["email"] = "This email address is being protected from spambots. You need JavaScript enabled to view it.";
cm.LocalPeer["gender"] = "male";
you can also alter these properties later after session open, you need to call
cm.BrodcastStateChange() to update local peer information on network.
3-Initate ConnectionManager.Open to open session
4- Wait for open session completion handler to execute
(note, after session open completion avoid initiating connect - "tunnel open" operation in next 2-3 sec to give time to API to inspect your network environment properly)
The actual code for the session open is in an example from the previous section.
Once we have established the session with super-node we can:
- Query for other peers on network (Peer lookup) with mongo db-like queries in our application scope
- Register for peers’ status tracking and receive notifications about changes
- Send/Receive instant messages
- Open communication tunnels to other peers and other peers can open tunnels to us
- Use virtual index manager if we need it:
- Create|Delete|Edit Virtual Networks|Users
- Search virtual networks|users with mongo DB like queries
- Join|Un-join users to networks
....
We will talk about all of these operations in following texts
Peer lookup queries to query for peers that are currently online. The query request is initiated using
Guid QueryTransactionID = cm.Query(string QueryText, PeerQueryCompleted Callback = null, int page = 0, int pageLimit = 64)
or handy overload if we need to check single peer:
Guid QueryTransactionID = cm.Query(Guid PeerUID, PeerQueryCompleted Callback = null)
methods of a ConnectionManager object. Callback and event handler method has in-print like this:
delegate void PeerQueryCompleted(List<Peer> FoundPeers, bool Completed, Guid QueryTransactionID, int Page, int PageLimit, int TotalPeers)
method arguments:
FoundPeers: List of peers returned by query
Completed: If completed then true, not that sometimes result will contain pees but this value will be false. This can happen for example if 10.000.000 peers are found by search criteria and operation timeout expires before all results are collected
QueryTransactionID: Transaction ID of query request
Page: Page returned if pagination exists
PageLimit: Number of peers per page if pagination exists
TotalPeers: Total number of peers matched by query
Event hook example:
cm.onPeerQueryResponse += new ConnectionManager.PeerQueryCompleted(cm_onPeerQueryResponse);
Query text format is mongo DB like a query with the exception that you don't put opening and closing brackets.
Let's say we defined some meta properties for our peer like this:
cm.LocalPeer["City"] = "Palo Alto";
cm.LocalPeer["Sex"] = "female";
cm.LocalPeer["Email"] = "This email address is being protected from spambots. You need JavaScript enabled to view it.";
cm.LocalPeer["Age"] = "31";
Here are some examples of queries that will include this peer in result:
Find peer with email "This email address is being protected from spambots. You need JavaScript enabled to view it." that is currently online :
cm.Query("\"Properties.Email\":\"This email address is being protected from spambots. You need JavaScript enabled to view it.\"") ;
Find peers with email contained in this list [This email address is being protected from spambots. You need JavaScript enabled to view it., This email address is being protected from spambots. You need JavaScript enabled to view it., This email address is being protected from spambots. You need JavaScript enabled to view it.] that are currently online:
cm.Query("\"Properties.Email\":{$in:[\"This email address is being protected from spambots. You need JavaScript enabled to view it. \",\"This email address is being protected from spambots. You need JavaScript enabled to view it. \",\"This email address is being protected from spambots. You need JavaScript enabled to view it.\"]}") ;
Find female peers form Palo Alto that are currently online:
cm.Query("\"Properties.City \":\"Palo Alto\", \"Properties.Sex\":\"female\"") ;
Find female peers form Palo Alto with above 30 that are currently online:
cm.Query("\"Properties.City \":\"Palo Alto\", \"Properties.Sex\":\"female\", \"Properties.Age\":{$gt:30}") ;
You can test your queries on provider portal.
There is often a need for some peer to have some state on the network like away, busy, available... You probably saw that as a common feature of chat applications. If you think you could achieve the same functionality using Instant messages or something else, you are right. But this provides you a more elegant way of transferring such information to peers instantly. Also, there are certain situations when this comes handy as an irreplaceable feature. Imagine you have some chat like application and you keep your peer buddies in some list or dictionary... your buddy kid rips power cables from his computer so his application
doesn't manage to notify you that he is offline. Super-node will notice that in max 90sec, and if you registered for that peer state tracking you will get the notification that he is offline.
To use this feature you need to hook event to accept notifications:
cm.onPeerStateChanged += new ConnectionManager.PeerStateChanged(cm_onPeerStateChanged);
.....
void cm_onPeerStateChanged(Peer Peer, uint State)
{
//do something
}
This is a rare case where you don't have the option to use callback because you never know when notification will come.
Also, you have to inform super-nodes that you want to be notified about that peer changes. You do that using following ConnectionManager functions:
cm.RegisterStateTracking
method having 4 overloads. Commonplace to place RegisterStateTrackingmethod is in a body of peer query response handler because that is a moment when you get fresh information of who is online. If peer goes offline you will get Peer.DisconnectedState (0) state argument value. If he comes back he needs to inform you that he is back online and you again need to register for its state tracking again. So status track is valid until peer goes offline, if he comes back you need to register state tracking again. Also, you see there is a need to combine instant messages with this to make it fully usable, why:
- You do peer query and you get fresh information about online peers. But after 30 sec some peer that should be visible to you by application terms appears on the network. You don't know that he is online in that moment and you would not know that he is online until you do peer query again. But he also does peer query and he knows you are online so all he has to do is to inform you about that. He can do that by simply sending you some instant message.
cm.SendInstantMessage(YourPeer,"some text - not nessery",YourApplicationMessagesEnum.IM_ONLINE);
void cm_onReceiveInstantMessage(Guid FromPeer, Guid FromPeerCheckpoint, Guid MessageGUID, int MessageType, byte[] Data)
{
if(MessageType == (int) YourApplicationMessagesEnum.IM_ONLINE){
//update your list or do query to check all again
}else....
}
We will talk about instant messages later so you could fully understand this code.
In state changed notification handler: cm_onPeerStateChanged(Peer Peer, uint State)you see two arguments Peer and State. First is complete fresh peer object of peer whose state changed and State is a new state of that peer. You notice it is unsigned int value so you are free to use any value in range from 2 to uint.MAX for your application.
Values 0 and 1 are reserved for
Peer.DisconnectedState = 0;
Peer.AuthentificatedState = 1;
The system depends on them so you cannot use these values for anything else.
To notify other peers about your state change code would be something like this:
- for example, you are now busy
cm.LocalPeer.State = (uint)MyApplicationPeerStateEnums.BUSY;
cm.BrodcastStateChange();
So we use cm.BrodcastStateChange(); to inform others of our state changes. This function also updates our peer object information on super-node so we also use it when we change meta properties while the session is active.
You don't need to do state broadcast when you are closing session/disposing C.M. because ConnectionManager will do that automatically.
Instant messages are carried using super-nodes. Their purpose is to transfer short messages between peers in the network. Often it is not practical to always open direct communication channel between two peers if one needs to inform other about something. It is because tunnel creation takes 1-15s and also maybe we want first another peer to confirm that he willingly accepts that connection. These are commonly some control messages from your application or something like chat messages.
To be able to receive them you need to hook following event handler:
cm.onReceiveInstantMessage += new ConnectionManager.ReceiveInstantMessage(cm_onReceiveInstantMessage);
.....
void cm_onReceiveInstantMessage(Guid FromPeer, Guid FromPeerCheckpoint, Guid MessageGUID, int MessageType, byte[] Data)
{
if (MessageType == (int)YourApplicationMessagesEnum.TextMessage) {
//do something
}
else if (MessageType == (int)YourApplicationMessagesEnum.IM_ONLINE) {
//do something
}
else if (MessageType == (int)YourApplicationMessagesEnum.ALOW_FILE_TRANSFER){
//do something
}...
}
To send an instant message you use one of 4 overloads of cm.SendInstantMessage function:
public Guid SendInstantMessage(Guid RemotePeerCheckpointUID, Guid RemotePeerUID, byte[] messageBytes, int InstantMessageType = 0, SendInstantMessageComplete Callback = null)
public Guid SendInstantMessage(Guid RemotePeerCheckpointUID, Guid RemotePeerUID, string Message, int InstantMessageType = 0, SendInstantMessageComplete Callback = null)
public Guid SendInstantMessage(Peer Peer, byte[] messageBytes, int InstantMessageType = 0, SendInstantMessageComplete Callback = null)
public Guid SendInstantMessage(Peer Peer, string Message, int InstantMessageType = 0, SendInstantMessageComplete Callback = null)
Example of sending instant message request:
cm.SendInstantMessage(OtherPeer,"Hi man!",YourApplicationMessagesEnum.TextMessage);
You notice all have an optional argument to InstantMessageTypeused to distinguish them. for example, you have a number of control messages and text messages so this gives you an easy way to distinguish them.
You also notice there is callback argument. That callback will trigger when the message is delivered to your super-node. It does not mean it is delivered to another peer when callback triggers. The message will travel to destination peer super-node and then it will be delivered to it.
The main API feature and probably the main reason why you use it is the ability to create direct communication channels between two computers that are both behind NAT (router) devices using NAT traversal techniques. The main advantage of this API is that as a result you will get standard platform socket to use from then on. The system will self-inspect what is the best method of tunnel criterion between two peers and create it. So you don't need to know anything about STUN, NAT port mapping prediction, UPnP, NAT PMP, PCP... or anything else that happens in the background and just wait for your prepared socket.
(Note - special situations when both peers are on the same local network or even on the same computer are handled by API. Data will be transferred locally and not going over the Internet) .
In past texts, we talked about how to find some peer. When you know that, and you want to open direct communication channel you initiate connect request. On the other side, the peer needs to accept this request so handshake procedure could start.
You need this two event handlers:
//When we make connect request (tunnel creation) we wait for operation completion to trigger event
cm.onPeerConnect += new ConnectionManager.PeerConnect(cm_onPeerConnect);
//When some other peer initiate tunnel creation to us, operation completion will trigger event
cm.onPeerAccepted += new ConnectionManager.PeerAccept(cm_onPeerAccepted);
.....
//After successful connect operation this handler is triggered
void cm_onPeerConnect(Peer Peer, Guid TransactionUID)
{
// Peer.Socket is tunneled socket ready for data transfer
Peer.Socket.Send(Encoding.UTF8.GetBytes("Hello through tunnel!"));
//do something else
}
.....
//THIS WILL TRIGGER WHEN OTHER SIDE CALL cm.connect(... TARGETING LOCAL PEER
void cm_onPeerAccepted(Peer Peer, Guid TransactionUID)
{
// Peer.Socket is tunneled socket ready for data transfer
byte[] buff = new byte[8192];
Peer.Socket.Receive(buff);
Peer.Socket.Send(Encoding.UTF8.GetBytes("Hello through tunnel to you to!"));
//do something else
}
One peer initiates connect request that would in this case use event handlers that look like this:
Guid track_connect_transaction_uid =
cm.Connect(OtherPeer, ConnectionType.TCP);
and on completion void cm_onPeerConnect(Peer Peer, Guid TransactionUID)would trigger on his side. On other side voie void cm_onPeerAccepted(Peer Peer, Guid TransactionUID) would trigger.
On a side of peer accepting connection if you need to control access - how it can connect and how it cannot, you can override cm.AcceptPeerConnectionReslover
cm.AcceptPeerConnectionReslover = delegate (Peer formPeer, ConnectionType ConnType)
{
if(???your condition to accept tunnel creation???)
return true;
else
return true;
};
By default, all connection request will be accepted unless you set cm.BlockIncoming = true;
Same thing using callbacks would look like this:
cm.PeerAccepted = delegate(Peer Peer, Guid PeerCheckpointUID, Guid PeerUID, Guid TransactionUID, bool Success, ConnectionManager.PeerConnectFailureReason FailReason)
{
if(sucess){
//Peer.Socket is tunneled socket ready for data transfer
byte[] buff = new byte[8192];
Peer.Socket.Receive(buff);
Peer.Socket.Send(Encoding.UTF8.GetBytes("Hello through tunnel to you to!"));
//do something ...
} else){
//do something ...
}
};
track_transaction_uid =
cm.Connect(PeerToConnectTo, ConnectionType.TCP, callback: new ConnectionManager.PeerConnectCallback(delegate(Peer PeerConnectedTo, Guid PeerCheckpointUID, Guid PeerUID, Guid TransactionUID ,bool Success, quickP2PLib.ConnectionManager.PeerConnectFailureReason FailReason) {
if(sucess){
//Peer.Socket is tunneled socket ready for data transfer
Peer.Socket.Send(Encoding.UTF8.GetBytes("Hello through tunnel!"));
//do something ...
} else){
//do something ...
}
}));
Note that events handles are executed in UI thread if found so starting long-running operations can block UI so read under: "Connection manager and result acceptance handlers/callbacks " text on how to handle that if you haven't done so already.
There is nothing special we need to talk about this, you have a ready socket so you do what you want with it from then on. Just have in mind that you are responsible for the socket from then on, so you need to close it when you no longer need it. Also as a good practice to follow is that you can just save that socket when you finish the task for new use if needed. So if you need do new task involving data transfer with that same peer, you can just pool out that socket, instead of invoking tunnel creation again (1-15sec). You can destroy such socket when you leave application or if that peer goes offline.
Since quickP2P system goal is to provide you complete environment so you could focus just on the application, we introduced permanent index storage sub-system. We figured out that it would be convenient to provide a way to store and be able to search some commonly used things like user registration data that is more closely aware of peers. You don't need to use this sub-system if you have your own in mind. You would just need some key you would store in peer properties to associate peers with your objects later.
You can imagine index system as a remote database with two tables User and Network. Of course, these are not ordinary tables that have fixed columns. You can store whatever property (name, value) you need and later search them. Besides common operations like SELECT/INSERT/UPDATE/DELETE, the most important is that User object is aware which peers are authenticated with it. Multiple peers can be authenticated with single user object and also open peer can be authenticated with multiple user objects. Both user and network object have UID and custom property bag. Constraints are that User object must have "Username" property and Network object needs to have "Name" property. There cannot be two User objects with the same username in your application scope and same stands for a name for networks. If you somehow need to enable different situation for some reason, you can generate these properties in some special way based on some other property.
The user object has columns "Member of networks" because you can join/un-join some user from the network and "Currently bound Peers" which holds a list of currently bound peer UIDs. User object also has password property you cannot see in provider portal.
An object used for virtual index management is cm.VNIndexManager. All index operations complete synchronously because there are no critical time-dependent functions. If you need asynchronous execution you need to implement it. Also if there is something wrong exception is thrown.
This is a list of all Virtual index manager operations:
public VUser SaveUser(VUser user,string user_password = "")
- Inserts or updates virtual user. You pass user object you want to save and save it providing a password for that user. If VUser UID property is 00000000-0000-0000-0000-000000000000 system will do INSERT otherwise it will do UPDATE. The result of the operation is fresh User object like on index server at that moment. This also meant PeerUIDS property will have a fresh list of authenticated peers. So if UID was empty UID will get some real value from the server after the operation completes.
This would be code to create a new user:
VUser newUser = new VUser();
newUser.Username = "some_username";//THIS IS REQUIRED, IT IS ALIAS FOR newUser["username"]
newUser["name"] = "Mike";
newUser["email"] = "This email address is being protected from spambots. You need JavaScript enabled to view it.";
newUser = cm.VNIndexManager.SaveUser(newUser, "somepassword");
if (newUser.UID != Guid.Empty) {
Console.WriteLine("User is saved , assigned UID is:" + newUser.UID.ToString());
}
then update operation would be:
newUser["name"] = "Mike2";
newUser = cm.VNIndexManager.SaveUser(newUser, "somepassword");
Note that after this operation, in case of creating a new user, PeerUIDS property will be empty. Peer need to call in AuthenticateUser method in order to have his UID appearing on this list.
public VUser AuthenticateUser(string Username, string Password)
- Checks for existing username with a password, and if it exists adds pees peer UID to VUser PeerUIDSlist, then returns VUser object.
VUser u;
...
u = cm.VNIndexManager.AuthenticateUser("some_username", "somepassword");
If you perform AuthenticateUser aser is already authenticated that is not a bad operation. It will not have side effects and you can use it to get a fresh VUser object from index server.
public List<VUser> QueryUsers(string QueryCommandText, int skip = 0, int limit = 0, string ascOrderBy = null, string descOrderBy = null)
- Searches for VUsers matching query, then returns a list of found VUsers to a requester
List<VUser> users = cm.VNIndexManager.QueryUsers("\"Properties.City\":\"Palo Alto\"");
If you expect large number of objects to be returned consider using pagination. In last two optional parameters you enter comma separated property names .
public long QueryUsersCount(string QueryCommandText)
- Searches for VUsers matching a query, then returns a number of objects found
public void DeleteUser(Guid userToDeleteUID)
- Deletes VUser object having exact UID:
cm.VNIndexManager.DeleteUser(u.UID);
public void ChangeUserPassword(Guid userUID, string old_password, string new_password)
- Changes VUser object password. Passwords are not visible to anyone. In case that user forgets a password and wants to reset it, you need to use magic value:
(requesterPeerUID.ToString() + ApplicationUID.ToString() + userUID.ToString()).ToLower()
for old password to be able to reset it.
public void JoinUserToNetwork(Guid userUID , Guid networkUID)
- Updates NetworkUIDS list on index server in VUser object identified by userUID argument by adding provided networkUID. You need to update your local VUser object manually after that or to reload.
public void UnJoinUserFromNetwork(Guid userUID, Guid networkUID)
- Updates NetworkUIDSlist on index server in VUser object identified by userUID argument by removing provided networkUID. You need to update your local VUser object manually after that or to reload it.
public VUser LogOffUser(string Username)
- Removes Peer UID from PeerUIDSlist in VUser object on index server. Returns fresh copy of VUser object.
public VNetwork SaveNetwork(VNetwork network)
- Inserts or updates (based on UID value) VNetwrok object. If UID is empty insert will be performed. The result is a fresh copy of VNetwork object from index server.
VNetwork newNetwork = new VNetwork();
newNetwork.Name = "network1";//THIS IS REQUIRED, IT IS ALIAS FOR newNetwork["name"]
newNetwork["owner"] = "Mike";
newNetwork = cm.VNIndexManager.SaveNetwork(newNetwork);
if (newNetwork.UID != Guid.Empty)
{
Console.WriteLine("Network is saved , assigned UID is:" + newNetwork.UID.ToString());
}
then update operation would be:
newNetwork ["owner"] = "Jeff";
newNetwork = cm.VNIndexManager.SaveNetwork(newNetwork);
there cannot be to networks with the same name in application scope.
public void DeleteNetwork(Guid networkToDeleteUID)
- Deletes VNetwork object having exact UID:
cm.VNIndexManager.DeleteNetwork(n.UID);
public List<VNetwork> QueryNeworks(string QueryCommandText, int skip = 0, int limit = 0, string ascOrderBy = null, string descOrderBy = null)
- Searches for VNetworks matching query, then returns a list of found VNetworks to requester
List<VNetwork> networks = cm.VNIndexManager.QueryNetworks("\"Properties.Interes\":\"Ecology\"");
If you expect a large number of object to be returned consider using pagination. In last two optional parameters, you enter comma separated property names.
public long QueryNeworksCount(string QueryCommandText)
- Searches for VNetworks matching query, then returns a number of objects found
public VObject SaveObject(VObject obj)
- Inserts or updates (based on UID value) VObject object (general purpose record). If UID is empty insert will be performed. The result is a fresh copy of VObject object from index server.
VObject newObject = new VObject();
newObject["something"] = "[\"1\",\"2\",\"9\"]";
newObject = cm.VNIndexManager.SaveObject(newObject);
if (newObject.UID != Guid.Empty)
{
Console.WriteLine("Object is saved , assigned UID is:" + newObject.UID.ToString());
}
then update operation would be:
newObject ["something"] = "[\"1\",\"2\",\"9\",\"15\"]";
newObject = cm.VNIndexManager.SaveObject(newObject);
public void DeleteObject(Guid objectToDeleteUID)
- Deletes VObject object having exact UID:
cm.VNIndexManager.DeleteObject(obj.UID);
public List<VObject> QueryObjects(string QueryCommandText, int skip = 0, int limit = 0, string ascOrderBy = null, string descOrderBy = null)
- Searches for VObject-s matching query, then returns a list of found VObject-s to a requester
List<VObject> objs = cm.VNIndexManager.QueryObjectss("\"Properties.something\":\"15\"");
If you expect a large number of object to be returned consider using pagination. In last two optional parameters, you enter comma separated property names .
public long QueryObjectsCount(string QueryCommandText)
- Searches for VObject-s matching query, then returns a number of objects found
Note we used json array for newObject["something"] value. Index server will automatically detect json array string shape or json object string shape and store it in that form. It is important that you don't have blank characters before "[" or "{" in this cases because index server will not do object/array check in that case . This enables you full-scale use of sub-objects in search queries.
You might need to have administrative application or site so you can give support to your users that can perform all index operation. When creating it have in mind that you must open session (become network peer) to gain access to the index server.
QuickP2P API provides you an easy way to obtain secure keys to be known only to peers involved with a tunnel. You use this keys to crypt/decrypt data during data transfer.
The secure key exchange is based on "Diffie–Hellman" algorithm with encampment to the original concept. There is no "real" fixed key part, instead of some short-lived data existent for few seconds during handshake operation that is used to generate what is called "Fixed part" in the original concept. This eliminates the possibility for an attacker knowing fixed part to intrude data integrity using any encryption breaking theory.
32 bytes of security data is known only to two peers involved with the tunnel. Security data is generated always during handshake procedure. After each successful connect/accept, generated bytes can be obtained from handler function Peer argument object:
void ConnectionManager_onPeerConnected(Peer Peer,Guid TransactionUID)
{
byte[] myKey = Peer.TunnelSecretData.Take(16).ToArray();
....
}
void ConnectionManager_onPeerAccepted(Peer Peer,Guid TransactionUID)
{
byte[] myKey = Peer.TunnelSecretData.Take(16).ToArray();
....
}
quickP2P API has built-in classes that for AES encryption (chippering).
AES class - core AES encryption
If support following AES variants:
Key size:
Chipering mode :
Padding:
AESNetworkStream - used to prepare data for crypt transfer when the tunnel is stream based (TCP). AESNetworkStream is not standardized and it's designed for easy use with quickP2P tunnels (you can use to else ware if both sides have it as the first gate). AESNetworkStream enables you to use CBC mode for real-time communication because provides you the ability to use fairly sized blocks crypt with CBC chippering. Normally in CBC mode you would need to get all data (like the whole file), then decrypt it so you could use it. AESNetworkStream can use more secure CBC mode and provide you partial data decryption.
For UDP datagrams you can simply use core AES class.
One thing to note when working with this classes is that original and crypted data do not have the same size. Crypted data is usually slightly larger than original.
Next is a simple example of crypting and decrypting text with core AES class:
var key = Guid.NewGuid().ToByteArray();//we generate some random key just for testing
quickP2PLib.Security.AES a = new quickP2PLib.Security.AES(quickP2PLib.Security.AES.KeySize.Bits128, key, quickP2PLib.Security.AES.Padding.PCKS7, quickP2PLib.Security.AES.Mode.CBC);
//we generate some test text and convert it to bytes
var original = ASCIIEncoding.ASCII.GetBytes("Hello I'am text to crypt");
byte[] buff1 = new byte[512];
//we crypt
int cryptlen = a.Crypt(original, buff1);
byte[] buff2 = new byte[512];
//we decrypt
int decryptlen = a.Decrypt(buff1, 0, cryptlen, buff2, 0);
//we convert bytes to text
var decrypted_text = ASCIIEncoding.ASCII.GetString(buff2, 0, decryptlen);
This is the same thing done with AESNetworkStream:
var key = Guid.NewGuid().ToByteArray();//we generate some random key just for testing
quickP2PLib.Security.AESNetworkStream ans
= new quickP2PLib.Security.AESNetworkStream(quickP2PLib.Security.AES.KeySize.Bits128, key);
//we generate some text and converti it to bytes
var original = ASCIIEncoding.ASCII.GetBytes("Hello I'am text to crypt");
//we write data to stream to be crypted
ans.WriteForCrypt(original);
byte[] buff1 = new byte[512];
//we read crypted data form stream
int crypted_len = ans.ReadCrypted(buff1);
//we now write crypted data for decryption
ans.WriteForDeCrypt(buff1,0,crypted_len);
byte[] buff2 = new byte[512];
//we read decrypted data
int decrypted_len = ans.ReadDeCrypted(buff2, 0, ans.AvailableDeCrypted);
//we convert bytes back to text
var test2 = ASCIIEncoding.ASCII.GetString(buff2, 0, decrypted_len);
Usually, you also need to set CBC initial vector (commonly "IV" ) if you are using CBC mode. Quick P2P AES class will generate it if not provided based on key value. In above example, we didn't set it but it's recommended you do so. Both classes have means to provide you pointer to internal CBC internal vector buffer you can
set bytes before you do any crypt/decrypt operation.
It's recommended that you add a firewall exception for your application. Communication will find its paths anyway but firewall exception can drastically increase performance. While developing you can do that manually but you would probably need a way to do that on user computers.
You can use quickP2PLib.FirewallHelper.Allow in application code but that will not have effect unless current user is a system administrator. Next are instructions how that can be done in case of using standard VS Setup project for application distribution .
The best place to execute a programmatic instruction for the user computer to do firewall exception creation is during application installation because the installer has boosted privileges. See InstallerFirewallException.cs class inside "qp2p_SimplestDemo_FileTransfer " project that you use with this document. So you need to create installer class in your project and provide a method: InstallerRoutine_AfterInstall
private void InstallerRoutine_AfterInstall(object sender, InstallEventArgs e)
{
quickP2PLib.FirewallHelper.Allow(this.Context.Parameters["assemblypath"], "simple_file_transfer_example" + DateTime.Now.ToFileTimeUtc().ToString());
}
For this to be executed by installer you need to add a custom action in setup project to execute on "Commit" even, you state assembly containing installer class (in our demo class is InstallerFirewallException.cs located in project itself so we would put "Primary output from qp2p_SimplestDemo_FileTransfer(Active)" for commit action)
These steps would count for windows distribution with standard VS Setup project. If you have other situation, like if you are creating Mono project for a non-windows platform with .NET API you would probably need different steps.
1 Connection manager , useful classes for API and result acceptance handlers/callbacks
4 Peer state tracing / changes notifications
6 NAT Traversal - Direct communication tunnel creation between peers
8 Security, key exchange and data encryption/decryption
9 Add firewall exception for your application in application installer
To use quickP2P c++ stl API with your project you first need to include:
- linux / Android : libquickP2PLib.so
- Mac OS / IOS: libquickP2PLib.a
- Windows: quickP2PLib.dll
into your project. As for CPU architecture x86/x64/arm6/arm7/arm8/arm9/mips you need to make sure you have right build.
Also you need to include <qp2p.h> located in header files shipped with lib into source files using qp2p library directly.
If you want to use qp2p API on some little known platform it's not problem to obtain correct build from us (such request before making contract with us is not possible) if we can have access to right build tool. If platform has support for BSD sockets and POSIX standard we should be able to make build without much of problems.
ConnectionManager is the main object you use with API. All client operations are initiated using methods in this class with exception of virtual index manager which is the subobject of ConnectionManager(ConnnectionManager.VNIndexManager()).
quickP2P API is based on an asynchronous model. You initiate operation by making the request and you wait for the result in the event handler or provided a callback. For most of the requests, there is an option to choose between event handler and callback to handle response depending what you want to do or what you prefer.
This would be the comparison of using events and callbacks.
Since we wanted that all our API versions look and function as much same as they can be we invented special delegate/event system that is similar to in .NET with an easy way of controlling object lifetime.
Since asynchronous API heavily depends on delegate/events, we will describe in details use of this features although they are not directly related to main API functionalities. You need to understand these specific concepts first to be able to efficiently use API.
quickP2Plib c++ STL lib has its own lightweight delegate/event implementation. The entire code for this subsystem is placed in single file - Event.h. QuickP2P events/delegates concept is almost same as in .NET.
These template classes are important for use:
- qp2plib::DelegateStatic - points to static class functions or global functions
- qp2plib::DelegateMember - points to class or struct member functions
- qp2plib::DelegateStatic_objc - points to static class functions or global functions defined in ObjectiveC
- qp2plib::DelegateMember_objc - points to class or struct member functions defined in ObjectiveC
- qp2plib::Event - can be described as a list you can add/remove delegates to. On rising all contained delegates are executed with given arguments
Each delegate/event can be specified to have 0-8 arguments for handler function. Delegates you add to some Event must have matching handler argument types.
Next, we will show the use of each of them, best from example program:
#include <qp2p.h>
void IamGlobalFunction(char* text,int number){
printf("\nfrom global function text: %s , number: %d",text,number);
}
class SomeClass{
public:
static void IamStaticFunction(char* text,int number){
printf("\nfrom static class function text: %s , number: %d",text,number);
}
void IamMemberFunction(char* text,int number){
printf("\nfrom class member function text: %s , number: %d",text,number);
}
};
int main(int argc, char* argv[])
{
//we create delegate objects , we set constructor argument deleteAutomaticaly = true so we dont need to delete manually
qp2plib::Delegate<char*,int>* twoParmDelegate1
= new qp2plib::DelegateStatic<char*,int>(IamGlobalFunction,true);
qp2plib::Delegate<char*,int>* twoParmDelegate2
= new qp2plib::DelegateStatic<char*,int>(&SomeClass::IamStaticFunction,true);
SomeClass *temp = new SomeClass();
//note firts template argument is name of class, also we set deleteAutomaticaly = true and deleteMemberObject = true
//so we dont need to delete after temp and twoParmDelegate3 becuse they will be deleted after execution
qp2plib::Delegate<char*,int>* twoParmDelegate3
= new qp2plib::DelegateMember<SomeClass,char*,int>(temp,&SomeClass::IamMemberFunction,true,true);
//If you want invoking and executing code in same code block:
struct my_functor{
void doCallback(char* text,int number){
printf("\nfrom functor text: %s , number: %d",text,number);
}
};
my_functor *temp2 = new my_functor();
qp2plib::Delegate<char*,int>* twoParmDelegate4
= new qp2plib::DelegateMember<my_functor,char*,int>(temp2,&my_functor::doCallback,true);
//we invoke our delegates
(*twoParmDelegate1)("some text for global fn",1);
(*twoParmDelegate2)("some text for static fn",2);
(*twoParmDelegate3)("some text for class member fn",3);
(*twoParmDelegate4)("some text for functor member fn",4);
return 0;
}
In above example, we demonstrated the use of qp2p delegates to invoke the global function, static class member function, class member function and functor that gave us the ability to place invoking and executing code in the same code block.
Next, see a modified example where we invoke all 4 of them at once with the event:
#include <qp2p.h>
void IamGlobalFunction(char* text,int number){
printf("\nfrom global function text: %s , number: %d",text,number);
}
class SomeClass{
public:
static void IamStaticFunction(char* text,int number){
printf("\nfrom static class function text: %s , number: %d",text,number);
}
void IamMemberFunction(char* text,int number){
printf("\nfrom class member function text: %s , number: %d",text,number);
}
};
int main(int argc, char* argv[])
{
qp2plib::Event<char*,int> myEvent;
myEvent += new qp2plib::DelegateStatic<char*,int>(IamGlobalFunction,true);
myEvent += new p2plib::DelegateStatic<char*,int>(&SomeClass::IamStaticFunction,true);
SomeClass *temp = new SomeClass();
myEvent += new qp2plib::DelegateMember<SomeClass,char*,int>(temp,&SomeClass::IamMemberFunction,true,true);
struct my_functor{
void doCallback(char* text,int number){
printf("\nfrom functor text: %s , number: %d",text,number);
}
};
my_functor *temp2 = new my_functor();
myEvent += new qp2plib::DelegateMember<my_functor,char*,int>(temp2,&my_functor::doCallback,true);
//now we will invoke
myEvent("Event invoked me",1000);
return 0;
}
The event must have same template argument signature as delegates you put in it. += and -= are operators designed so event object would look similar as in C#. deleteAutomaticaly argument in delegate constructor would get other meaning if the delegate is put into the event. Instead of deleting after execution deleteAutomaticaly will be used by event object to mark delegate deletion when itself is in disposal state. Note that in this examples we talked about cases when we use dynamic HEAP allocation. If you mark deleteAutomaticaly on the delegate that is constructed statically delete would be applied and that would cause memory access fault.
If you looked these examples with care you got a point of qp2p event/delegates use. If you used .NET events, you notice they function very similar except this are native c++ cross-platform events/ delegates. You note there is no delete statement at end of the program and we did not miss that because this is scrap example. We controlled HEAP disposal by providing deleteAutomatically and deleteMembersObject arguments to true. You can copy/past above codes to your project sources if you wish to practice.
qp2plib::DelegateStatic_objc and qp2plib::DelegateMember_objc basically accept Objective C selectors to point to objective c methods and id type in case of qp2plib::DelegateMember_objc to point to object having method to execute. This makes qp2p c++ API fully mixable with objective c:
_ConnectionManager->onStatusChange += new qp2plib::DelegateMember_objc<qp2plib::ConnectionManager *, qp2plib::ConnectionManager::ConnectionManagerStatus>(self,@selector(onStatusChange::),true);
......
-(void) onStatusChange: (qp2plib::ConnectionManager*) sender : (qp2plib::ConnectionManager::ConnectionManagerStatus) newStatus{
.........
}
Note @selector(onStatusChange::) has two ':' at the end after method name which corresponds to a number of arguments.
Making your own cross-platform code entities can sometimes be harsh and having ready solutions can really push thing forward. Since we had to make few such things for API itself we also exposed them so you could use them. Here we will mention few we think could really help you with other parts of your project.
- qp2plib::IP4Address
Represents IP4 address object in machine byte order. It has handy ToString() and can be easily converted to sockaddr_in. Also you can do IP4Address myAddress = "88.20.0.20"; or IP4Address myAddress = "somedomain.com";
-qp2plib::IPEndPoint
Represent IP4Address:port pair. It's implicitly castable to sockaddr_in and vice versa. It has handy ToString() ,ToByteArray and FromByteArray methods. sockadr is different on almost all platforms and using IPEndPoint enables you to easily overcome this. You can cast it anytime to sockadr_in when you need to pass it to socket functions. It has built-in DNS resolve so IPEndPoint ep = "somedomin.com:80" or IPEndPoint ep = "88.20.0.22:80" are valid.
-qp2plib::Guid
.NET like guid (compatible). Represents 16 byte object you use as unique identificator. You can extract random values by taking last bytes form this object .
-qp2plib::DnsHelper::GetHostName - gets hostname of localhost
-qp2plib::DnsHelper::GetFirstHostAddress - gets a first IP4 address of some host, there is also function to get all addresses of some dns name
-qp2plib::UTIL::GetCurrentTimeMilisec - gets the current time in milliseconds, it's not accurate in terms of current daytime but it's good for synchronization
-qp2plib::UTIL::DownloadURL - can be used do download pages/files using HTTP/HTTPS protocol
-qp2plib::UTIL::GetOS - gets current operating system name
-qp2plib::NetworkHelper::getBindAddressForRemoteAddress - when machine/device has multiple network interfaces this is best cross-platform utility to check which local adder will socket take to connect to some remote address
qp2plib:UInt64 and qp2plib:Int64 are useful because incompatibilities of long and unsigned long type between platforms.
- When you hook event once it will execute response every time correspondent request arrives until you unhook it.
- If you have both events hooked and callback provided to execute on the request completion first event is triggered then the callback
- Arguments values are guaranteed inside handler functions bodies. After execution(s) argument values are disposed so be sure if you use this values to set other variables to do it by copying values NOT BY REF!
One characteristic example would be the handler that executes on instant message reception:
void onReceiveInstantMessage(qp2plib::Guid *FromPeerUID,qp2plib::Guid *FromPeerCheckpointUID,qp2plib::Guid *IMessageUID,int MessageType,unsigned char* data,int len){
....
Note argument data of type unsigned char*. If you plan to store bytes from that argument to some variable for later use make sure you do it by copying bytes with memcpy or whatever you use.
- quickP2P will execute event handlers and callbacks on one of its 3 running threads. You must be careful not to do long running tasks inside event or callback handler body because you may block API execution. Instead, make sure all your long running task you starts from the event of callback body are transferred to other threads. Note this behavior is specific for c++ API, C# and Java wrapped API have other rules.
To demonstrate that, we will turn again to demo project provided with the application to see how is this solved. See qp2p_SimplestDemo_FileTransfer.cpp
In some moment we initiate tunnel creation to another peer:
....
//STEP5 we found peer so now we will do NAT travaresal operation to open direct communication tunnel to it
//here we initioate NAT Traversal tunnel opening operation
//on completeion onPeerConnectEventHandler of onPeerConnectFailureEventHandler will trigger
qp2plib::Guid connectTUID = CM->Connect(Peers[0],qp2plib::CT_TCP);
//NEXT we wait for result in onPeerConnectEventHandler, on other side onPeerAcceptEventHandler will trigger
....
When operation finishes with success, this handler will be triggered:
....
void onPeerConnectEventHandler(qp2plib::Peer *remotePeer,qp2plib::Guid * transactionUID){
//STEP6 - 1 - we have tunnel we will now do file transfer - send
std::map<std::string,std::string>::iterator it = sendFileTasks.find(transactionUID->ToString());
if(it == sendFileTasks.end()) {
printf("\nUnrecognised peer connect tranasaction UID: %. Transaction will be terminated!\n",transactionUID->ToString().c_str());
qp2plib::Uninetwork::SocketClose(remotePeer->TunnelSocket);
}else{
//file transfer could be long running operation that could block api if we do it all in event hendler body.
//So we create new thread and pass it all information needed for file transfer.
FileTransferInfo *ftr = new FileTransferInfo();
ftr->File = (*it).second;//this is send
ftr->isSend = true;
ftr->P = *remotePeer;//remotePeer->TunnelSocket is traversaled connected socket - note this is just regular socket regardless of alias type qp2plib::Socket
printf("\nSending file: %s [0%%]\n>>",ftr->File.c_str());
//When we get connected/ready socket it's our responisibility from then on.
//In case we end application while file transfer active socked handle may stay
//unclosed. So we store our sockets in some list for taht case if we need to
//close them before they finish their job.
kill_me.push_back(remotePeer->TunnelSocket);
//we create thread and pass it all needed information for file transfer
tthread::thread *send_thr = new tthread::thread(FileTransferWorker,ftr);
//we no longer need to keep track of transaction
sendFileTasks.erase(it);
}
}
....
So you see we have created a new thread to run file transfer in method FileTransferWorker. onPeerConnectEventHandler method is executed from internal API thread. If we would just do file send loop in that method body, we would block that API thread until file transfer completes. That's why we created a new thread to execute file transfer leaving API internal thread to do its job.
Callbacks are usually provided as optional arguments to request invoking methods, and are executed on request completion only once.
Callback is executed from "some" of 3 internal threads that accepted request response. Watch for that not to block them using long-running operations inside of their body. Make sure you transfer long running tasks to other thread.
You would use callback when you don't want to hook event to execute every time you receive a response to some common action. Callbacks are sometimes particularly handy in combination with a functor. You can see from demo application provided with this document way we used functor for session open callback that enabled us to define request and response process code in the same method body. Also, a functor is basically a structure that can have other members so you can define additional variables to store some info usually needed to relate particular request with a correspondent response. We will describe that referring to the way we did CM->connect (NAT Traversal) operation. It is possible to issue multiple connect request in short time period. All of them would result in onPeerConnectEventHandler triggering. In the demo we used additional data structure std::map<std::string,std::string> sendFileTasks; so we could know which event triggering is for which file to send by transaction UID. Using transaction UID is a way to do that using events, and also we could use the same approach in case we use just callback. Here we will describe how the same thing could be done using callback in functor that contains additional info data.
struct onPeerConnectCallbackFunctor{
std::string FileToSend;
onPeerConnectCallbackFunctor(std::string file){
FileToSend = file;
}
void doCallback(qp2plib::Peer *remotePeer, qp2plib::Guid *PeerCheckpointUID, qp2plib::Guid *PeerUID, qp2plib::Guid *transactionUID, bool success, qp2plib::ConnectionManager::PeerConnectFailureReason reason){
if(success){
FileTransferInfo *ftr = new FileTransferInfo();
//we have availabe FileToSend variable from functor memeber
ftr->File = this->FileToSend;
ftr->isSend = true;
//remotePeer->TunnelSocket is traversaled connected socket - note this is just regular socket regardless of alias type qp2plib::Socket
ftr->P = *remotePeer;
printf("\nSending file: %s [0%%]\n>>",ftr->File.c_str());
kill_me.push_back(remotePeer->TunnelSocket);
//we create thread and pass it all needed information for file transfer
tthread::thread *send_thr = new tthread::thread(FileTransferWorker,ftr);
}else
printf("\nNAT Travesal operation with transaction UID: %s intended for creation of tunnel for send transfer of [%s] failed because of: %s\n\n>>", transactionUID->ToString().c_str(),this->FileToSend.c_str(), qp2plib::ConnectionManager::PeerConnectFailureReasonName(reason).c_str());
}
};
onPeerConnectCallbackFunctor *pccf = new onPeerConnectCallbackFunctor((*it).second);
CM->Connect(
Peers[0],
qp2plib::CT_TCP,
qp2plib::IPEndPoint::ANY(),
new qp2plib::DelegateMember<onPeerConnectCallbackFunctor,qp2plib::Peer*, qp2plib::Guid*, qp2plib::Guid*, qp2plib::Guid*, bool, qp2plib::ConnectionManager::PeerConnectFailureReason>(pccf,&onPeerConnectCallbackFunctor::doCallback,true,true));
So we added FileToSend variable to our functor struct and we use it to pass the file path to callback executing handler. Here we did not need std::map<std::string,std::string> sendFileTasks; struct.
There are situations where you might find using both callback and event handle useful. For example session opening. If CM->MaintainSession is set to true (default), on session loss connection manager will wait until session creation is possible again (internet connection temporal loss for example) and initiate session open again in the background. On success, it will trigger event
ConnectionManager::onSessionOpenSuccess. If we did not use this event just callback we will never know that happened. Also, we might want to execute different handler for first time session open and later session recoveries. So this might be practice:
- For first time session open use callback
- In callback handler body then hook event ConnectionManager::onSessionOpenSuccess
- On ConnectionManager::onSessionOpenSuccess triggering you can execute code for a case of session recovery
Ok, we could use event and some bool variable for that but point was to describe to you one more concept.
For a quickP2P client, to be able to do any operation like tunnel opening, sending an instant message, searching peers... we first need to join an abstract peer-to-peer network like you connect to the internet when your computer powers up. Steps for joining application peer to super-node network would be this:
1 - Create ConnectionManager object to handle operations, set required C.M. properties, handles...
It's always required to set access tokens:
CM->setAccessTokens("00000000-XXXX-XXXX-XXXX-XXXXXXXXXXXX","00000000-XXXX-XXXX-XXXX-XXXXXXXXXXXX","");
Fhe first argument is provider UID, second application UID, third is access key. You can see this values in you provider portal.
2 - Set some meta-data so other peers can find us and we can find them by filtering values
CM->LocalPeer["some_meta"] = "bla bla bla";
CM->LocalPeer["email"] = "This email address is being protected from spambots. You need JavaScript enabled to view it.";
CM->LocalPeer["gender"] = "male";
you can also alter these properties later after session open, you need to call
CM->BrodcastStateChange() to update local peer information on network.
3-Initate ConnectionManager::Open to open session
4- Wait for open session completion handler to execute
(note, after session open completion avoid initiating connect - "tunnel open" operation in next 2-3 sec to give time to API to inspect your network environment properly)
Once we have established a session with super-node we can:
- Query for other peers on the network (Peer lookup) with mongo db-like queries in our application scope
- Register for peers’ status tracking and receive notifications about changes
- Send/Receive instant messages
- Open communication tunnels to other peers and other peers can open tunnels to us
- Use virtual index manager if we need it:
- Create|Delete|Edit Virtual Networks|Users
- Search virtual networks|users with mongo db like queries
- Join|Un-join users to networks
....
We will talk about all of these operations in following texts
Peer lookup queries query for peers that are currently online. Query request is initiated using
qp2plib::Guid QueryTransactionID = CM->Query(::std::string QueryText , qp2plib::ConnectionManager::PeerQueryCompleted Callback = NULL, int page = 0, int pageLimit = 64);
or handy overload if we need to check single peer:
qp2plib::Guid QueryTransactionID = CM->Query(qp2plib::Guid PeerUID, qp2plib::ConnectionManager::PeerQueryCompleted Callback = NULL)
methods of ConnectionManager object. Callback and event handler method has in-print like this:
typedef Delegate<Peer*,int, bool, Guid*, int, int, int>* PeerQueryCompleted;
Agruments:
FoundPeers List of peers returned by query
Count Number of peers returned by query
Completed If completed then true, not that somitimes result will contain peers but this value will be false. This can happen for example if 10000000 peers are found by serch creterium , and operation timeout expires before all results are collected
QueryTransactionID Transaction ID of query request
Page Page returned if pagination exists
PageLimit Number of peers per page if pagination exists
TotalPeers Total number of peers matched by query
Event hook example:
CM->onPeerQueryResponse += new qp2plib::DelegateStatic<qp2plib::Peer*,int,bool,qp2plib::Guid*,int,int,int>(onPeerQueryCompleted,true);
Query text format is mongo db like query with the exception that you don't put opening and closing brackets.
Let's say we defined some meta properties for our peer like this:
CM->LocalPeer["City"] = "Palo Alto";
CM->LocalPeer["Sex"] = "female";
CM->LocalPeer["Email"] = "This email address is being protected from spambots. You need JavaScript enabled to view it.";
CM->LocalPeer["Age"] = "31";
Here are some examples of queries that will include this peer in result:
Find peer with email "This email address is being protected from spambots. You need JavaScript enabled to view it." that is currently online:
CM->Query((std::string)"\"Properties.Email\":\"This email address is being protected from spambots. You need JavaScript enabled to view it.\"") ;
Find peers with email contained in this list [This email address is being protected from spambots. You need JavaScript enabled to view it., This email address is being protected from spambots. You need JavaScript enabled to view it., This email address is being protected from spambots. You need JavaScript enabled to view it.] that are currently online:
CM->Query((std::string)"\"Properties.Email\":{$in:[\"This email address is being protected from spambots. You need JavaScript enabled to view it.\",\"This email address is being protected from spambots. You need JavaScript enabled to view it. \",\"This email address is being protected from spambots. You need JavaScript enabled to view it.\"]}") ;
Find female peers from Palo Alto that are currently online:
CM->Query((std::string)"\"Properties.City \":\"Palo Alto\", \"Properties.Sex\":\"female\"") ;
Find female peers from Palo Alto with above 30 that are currently online:
CM->Query((std::string)"\"Properties.City \":\"Palo Alto\", \"Properties.Sex\":\"female\", \"Properties.Age\":{$gt:30}") ;
You can test your queries on provider portal.
There is often a need for some peer to have some state on the network like away, busy, available... You probably saw that as a common feature of chat applications. If you think you could achieve the same functionality using Instant messages or something else, you are right. But this provides you a more elegant way of transferring such information to peers instantly. Also, there are certain situations when this comes handy as an irreplaceable feature. Imagine you have some chat like application and you keep your peer buddies in some list or dictionary... your buddy kid rips power cables from his computer so his application doesn't manage to notify you that he is offline. Super-node will notice that in max 90sec, and if you registered for that peer state tracking you will get the notification that he is offline.
To use this feature you need to hook event to accept notifications:
CM->onPeerStateChanged += new qp2plib::DelegateStatic<qp2plib::Peer*,qp2plib::UInt32>(onPeerStateChanged,true);
.....
void onPeerStateChanged(qp2plib::Peer *P,qp2plib::UInt32 newPeerState){
if(newPeerState == 0){//0 - is reserved for disconnected state, 1 - initial connected state, all values > 1 are available for free use
Peers_List.erase(P->UID);//Peer got disconnected we remove it from our internal list
}
}
This is a rare case where you don't have the option to use callback because you never know when notification will come.
Also, you have to inform super-nodes that you want to be notified about that peer changes. You do that using:
CM->RegisterStateTracking
Comonplace to place RegisterStateTrackingmethod is in a body of peer query response handler. It is because that is the moment when you get fresh information of who is online. If peer goes offline you will get qp2plib::Peer::DisconnectedState (=0) state argument value. If he comes back he needs to inform you that he is back online and you again need to register for its state tracking again. So status track is valid until peer goes offline, if he comes back you need to register state tracking again. Also, you see there is a need to combine instant messages with this to make it fully usable, why:
- You do peer query and you get fresh information about online peers. But after 30 sec some peer that should be visible to you by application terms appears on the network. You don't know that he is online in that moment and you would not know that he is online until you do peer query again. He also does peer query and he knows you are online. All he has to do is to inform you about that. He can do that by simply sending you some instant message.
CM->SendInstantMessage
(YourPeer,"some text - not nessery",YourApplicationMessagesEnum::IM_ONLINE);
void onReceiveInstantMessage(qp2plib::Guid *FromPeerUID,qp2plib::Guid *FromPeerCheckpointUID,qp2plib::Guid *IMessageUID,int MessageType,unsigned char* data,int len){
{
if(MessageType == (int)YourApplicationMessagesEnum::IM_ONLINE){
//update your list or do query to check all again
}else....
}
We will talk about instant messages later so you could fully understand this code.
In state changed notification handler: void onPeerStateChanged(qp2plib::Peer *P,qp2plib::UInt32 newPeerState){
you see two arguments Peer and State. First is complete fresh peer object of peer whose state changed and State is new state of that peer. You notice it is unsigned int value so you are free to use any value in range from 2 to uint.MAX for your application.
Values 0 and 1 are reserved for
qp2plib:Peer::DisconnectedState = 0;
qp2plib:Peer::AuthentificatedState = 1;
System depends on them so you cannot use these values for anything else.
To notify other peers about your state change code would be something like this:
- for example, you are now busy
CM->LocalPeer.State = (uint)MyApplicationPeerStateEnums.BUSY;
CM->BrodcastStateChange();
So we use CM->BrodcastStateChange(); to inform others of our state changes. This function also updates our peer object information on super-node so we also use it when we change meta properties while session is active.
You don't need to do state broadcast when you are closing session/disposing C.M. because ConnectionManager will do that automatically.
Instant messages are carried using super-nodes. Their purpose is to transfer short messages between peers in the network. Often it is not practical to always open direct communication channel between two peers if one needs to inform other about something. It is because tunnel creation takes 1-15s and also maybe we want first another peer to confirm that he willingly accepts that connection. These are commonly some control messages from your application or something like chat messages.
To be able to receive them you need to hook following event handler:
CM->onReceiveInstantMessage
+= new qp2plib::DelegateStatic<qp2plib::Guid*,qp2plib::Guid*,qp2plib::Guid*,int,unsigned char*,int>(onReceiveInstantMessage,true);
.....
void onReceiveInstantMessage(qp2plib::Guid *FromPeerUID,qp2plib::Guid *FromPeerCheckpointUID,qp2plib::Guid *IMessageUID,int MessageType,unsigned char* data,int len){
if (MessageType == (int)YourApplicationMessagesEnum::TextMessage) {
//do something
}
else if (MessageType == (int)YourApplicationMessagesEnum::IM_ONLINE) {
//do something
}
else if (MessageType == (int)YourApplicationMessagesEnum::ALLOW_FILE_TRANSFER){
//do something
}...
}
To send an instant message you use one of 4 overloads of cm.SendInstantMessage function:
Guid SendInstantMessage(Guid RemotePeerCheckpointUID, Guid RemotePeerUID, unsigned char* message_buffer, int message_len, int InstantMessageType = 0, SendInstantMessageComplete Callback = NULL);
Guid SendInstantMessage(Guid RemotePeerCheckpointUID, Guid RemotePeerUID, ::std::string Message, int InstantMessageType = 0, SendInstantMessageComplete Callback = NULL);
Guid SendInstantMessage(Peer &Peer, unsigned char* message_buffer, int message_len, int InstantMessageType = 0, SendInstantMessageComplete Callback = NULL);
Guid SendInstantMessage(Peer &Peer, ::std::string Message, int InstantMessageType = 0, SendInstantMessageComplete Callback = NULL);
Example of sending instant message request:
CM->SendInstantMessage(OtherPeer,"Hi man!",YourApplicationMessagesEnum::TextMessage);
You notice all have an optional argument to InstantMessageTypeused to distinguish them. For example, you have a number of control messages and text messages so this gives you an easy way to distinguish them.
You also notice there is callback argument. That callback will trigger when a message is delivered to your super-node. It does not mean it is delivered to another peer when callback triggers. The message will travel to destination peer super node and then it will be delivered to it.
The main API feature and probably main reason why you use it is the ability to create direct communication channels between two computers that are both behind NAT (router) devices using NAT traversal techniques. The main advantage of this API is that as a result you will get standard platform socket to use from then on. The system will self-inspect what is the best method of tunnel criterion between two peers and create it. So you don't need to know anything about STUN, NAT port mapping prediction, UPnP, NAT PMP, PCP... or anything else that happens in the background and just wait for your prepared socket.
(Note - special situations when both peers are on the same local network, or even on the same computer, are handled by API. In that case, data will be transferred locally and not going over the Internet).
In past texts, we talked about how to find some peer. When you know that, and you want to open direct communication channel you initiate connect request. On the other side, peer needs to accept this request so handshake procedure could start.
You need this two event handlers:
//When we make connect request (tunnel creation) we wait for operation completion to trigger event
CM->onPeerConnected += new qp2plib::DelegateStatic<qp2plib::Peer*,qp2plib::Guid*>(onPeerConnectEventHandler,true);
//When some other peer initiate tunnel creation to us, operation completion will trigger event
CM->onPeerAccepted += new qp2plib::DelegateStatic<qp2plib::Peer*,qp2plib::Guid*>(onPeerAcceptEventHandler,true);
.....
//After successful connect operation this handler is triggered
void onPeerConnectEventHandler(qp2plib::Peer *remotePeer,qp2plib::Guid * transactionUID){
{
// remotePeer->TunnelSocket is tunneled socket ready for data transfer
char *tunneled_message = "Hello through tunnel!" ;
send(remotePeer->TunnelSocket,tunneled_message,strlen(tunneled_message),0);
//do something else
}
.....
//THIS WILL TRIGGER WHEN OTHER SIDE CALL cm.connect(... TARGETING LOCAL PEER
void onPeerAcceptEventHandler(qp2plib::Peer *remotePeer,qp2plib::Guid * transactionUID){
{
// remotePeer->TunnelSocket is tunneled socket ready for data transfer
char buff[512];
int read = recv(remotePeer->TunnelSocket,buff,512,0);
char *tunneled_message = "Hello through tunnel to you to!" ;
send(remotePeer->TunnelSocket,tunneled_message,strlen(tunneled_message),0);
//do something else
}
One peer initiates connect request that would in this case use event handlers that look like this:
qp2plib::Guid transactionUID = CM->Connect(OtherPeers,qp2plib::CT_TCP);
and on completion void onPeerConnectEventHandler(qp2plib::Peer *remotePeer,qp2plib::Guid * transactionUID) would trigger on his side. On other side void onPeerAcceptEventHandler(qp2plib::Peer *remotePeer,qp2plib::Guid * transactionUID) would trigger on a side of peer accepting connection.
if you need to control access - how it can connect and how it cannot, you can override Accept Connection Reslover by providing your resolver function:
....
void onDecideToAcceptPeerConnection(qp2plib::ConnectionManager* sender,qp2plib::Peer* fromPeer, qp2plib::ConnectionType connType ,bool* result){
if(???your condition to accept tunnel creation???)
(*result) = true;
else
(*result) = false;
}
....
qp2plib::DelegateStatic<qp2plib::ConnectionManager*,qp2plib::Peer*, qp2plib::ConnectionType,bool*>
acceptResolveDelegate (onDecideToAcceptPeerConnection,false);
....
int main(int argc, char* argv[]){
....
CM->SetAcceptPeerConnectionResolver(&acceptResolveDelegate);
....
Note that we set deleteAutomaticaly = false because we used static stack allocation in this case. Static allocation is good for this case because for acceptResolveDelegate we would not worry about deleting object. If we would use dynamic allocation with 'new' we would also need to set deleteAutomaticaly = false because we can expect that will execute multiple times during application lifetime and letter we would need to kill it with delete statement manually.
By default all connection request will be accepted unless you set CM->BlockIncoming = true;
Tunnel accept/connect using callbacks would look like this:
void onPeerConnectCallback(qp2plib::Peer *remotePeer, qp2plib::Guid *PeerCheckpointUID, qp2plib::Guid *PeerUID, qp2plib::Guid *transactionUID, bool success, qp2plib::ConnectionManager::PeerConnectFailureReason reason){
if(success){
// remotePeer->TunnelSocket is tunneled socket ready for data transfer
char *tunneled_message = "Hello through tunnel!" ;
send(remotePeer->TunnelSocket,tunneled_message,strlen(tunneled_message),0);
}else
printf("\nNAT Travesal operation failed");
}
};
....
//invoke connect
CM->Connect(
remotePeer,
qp2plib::CT_TCP,
qp2plib::IPEndPoint::ANY(),
new qp2plib::DelegateStatic<qp2plib::Peer*, qp2plib::Guid*, qp2plib::Guid*, qp2plib::Guid*, bool, qp2plib::ConnectionManager::PeerConnectFailureReason>( onPeerConnectCallback,true,true));
void onPeerAcceptCallback(qp2plib::Peer *remotePeer, qp2plib::Guid *PeerCheckpointUID, qp2plib::Guid *PeerUID, qp2plib::Guid *transactionUID, bool success, qp2plib::ConnectionManager::PeerConnectFailureReason reason){
// remotePeer->TunnelSocket is tunneled socket ready for data transfer
char buff[512];
int read = recv(remotePeer->TunnelSocket,buff,512,0);
char *tunneled_message = "Hello through tunnel to you to!" ;
send(remotePeer->TunnelSocket,tunneled_message,strlen(tunneled_message),0);
//do something else
}
some ware in CM preparation like in int main(....
CM->PeerAccepted = new qp2plib::DelegateStatic<qp2plib::Peer*, qp2plib::Guid*, qp2plib::Guid*, qp2plib::Guid*, bool, qp2plib::ConnectionManager::PeerConnectFailureReason>(
onPeerAcceptCallback,
false
);
Note we set deleteAutomaticaly = false because we can expect that callback would execute multiple times during application lifetime so we don't want to delete delegate automatically after the first execution. We would need to delete CM->PeerAccepted with delete statement at program end in this case or we could use static allocation like this:
Some ware as the global variable where onPeerAcceptCallback function is visible in scope:
qp2plib::DelegateStatic<Peer*, Guid*, Guid*, Guid*, bool, PeerConnectFailureReason> acceptDelegate(onPeerAcceptCallback,
false);
then some ware in CM preparation like in int main(....
CM->PeerAccepted = &acceptDelegate;
Note that events handles are executed in one of 3 API threads so long running tasks in handler body can block API. Read under: "Connection manager and result acceptance handlers/callbacks " text on how to handle that if you haven't done so already.
There is nothing special we need to talk about this, you have a ready socket so you do what you want with it from then on. Just have in mind that you are responsible for a socket from then on so you need to close it when you no longer need it. Also as a good practice to follow is that you can just save that socket when you finish the task for new use if needed. So if you need do new task involving data transfer with that same peer you can just pool out that socket instead of invoking tunnel creation again (1-15sec). You can destroy such socket when you leave application or if that peer goes offline.
Since quickP2P system goal is to provide you complete environment so you could focus just on an application, we introduced permanent index storage sub-system. We figured out that it would be convenient to provide a way to store and be able to search some commonly used things like user registration data that is more closely aware of peers. You don't need to use this sub-system if you have your own in mind. You would just need some key you would store in peer properties to associate peers with your objects later.
You can imagine index system as a remote database with two tables User and Network. Of course, these are not ordinary tables that have fixed columns. You can store whatever property (name, value) you need and later search them. Besides common operations like SELECT/INSERT/UPDATE/DELETE what is most important is that User object is aware which peers are authenticated with it. Multiple peers can be authenticated with single user object and also open peer can be authenticated with multiple user objects. Both user and network object have UID and custom property bag. Constraints are that User object must have "Username" property and Network object needs to have "Name" property. There cannot be two User objects with the same username in your application scope and same stands for a name for networks. If you somehow need to enable different situation for some reason you can generate these properties in some special way based on some other property.
The user object has columns "Member of networks" because you can join/un-join some user from the network and "Currently bound Peers" which holds a list of currently bound peer UIDs. User object also has password property you cannot see in provider portal.
Ohe object used for virtual index management is cm.VNIndexManager. All index operations complete synchronously because there are no critical time dependent functions. If you need asynchronous execution you need to implement it. Also if there is something wrong exception is thrown.
This is a list of all Virtual index manager operations:
qp2plib::VUser SaveUser(qp2plib::VUser &user,::std::string user_password = "");
- Inserts or updates virtual user. You pass user object you want to save and save it providing a password for that user. If VUser UID property is 00000000-0000-0000-0000-000000000000 system will do INSERT otherwise it will do UPDATE. The result of the operation is fresh User object like on index server at that moment. This also meant PeerUIDS property will have a fresh list of authenticated peers. So if UID was empty UID will get some real value from the server after the operation completes.
This would be code to create a new user:
qp2plib::VUser newUser;
newUser.Username("some_username");//THIS IS REQUIRED, IT IS ALIAS FOR newUser["username"]
newUser["name"] = "Mike";
newUser["email"] = "This email address is being protected from spambots. You need JavaScript enabled to view it.";
newUser = CM->VNIndexManager()->SaveUser(newUser, "somepassword");
if (newUser.UID != qp2plib::Guid::Empty()) {
printf("User is saved , assigned UID is: %s",newUser.UID.ToString().c_str());
}
then update operation would be:
newUser["name"] = "Mike2";
newUser = cm.VNIndexManager->SaveUser(newUser, "somepassword");
Note that after this operation in case of creating new usr PeerUIDS property will be empty. Peer need to call in AuthenticateUser method in order to have his UID appearing in this list.
VUser AuthenticateUser(::std::string Username, ::std::string Password);
- Checks for existing username with password and if it exists adds peer UID to VUser PeerUIDSlist, then returns VUser object:
qp2plib::VUser u;
...
u = CM->VNIndexManager().AuthenticateUser("some_username", "somepassword");
If you perform AuthenticateUser and user is already authenticated that is not bad operation. It will not have side effects and you can use it to get fresh VUser object from index server.
void QueryUsers(::std::list<qp2plib::VUser> *result_list, ::std::string QueryCommandText, int skip = 0, int limit = 0, ::std::string ascOrderBy = "", ::std::string descOrderBy = "");
- Searches for VUsers matching query, adds list of found VUsers to provided result_list
CM->VNIndexManager()->QueryUsers(&list_to_fill,"\"Properties.City\":\"Palo Alto\"");
If you expect large number of objects to be returned consider using pagination. In last two optional parameters you enter comma separated property names .
long QueryUsersCount(::std::string QueryCommandText);
- Searches for VUsers matching query , then returns number of objects found
void DeleteUser(qp2plib::Guid userToDeleteUID);
- Deletes VUser object having exact UID:
cm.VNIndexManager.DeleteUser(u.UID);
void ChangeUserPassword(qp2plib::Guid userUID, ::std::string old_password, ::std::string new_password);
- Changes VUser object password. Passwords are not visible to anyone. In case that user forgets password and wants to reset it, you need to use magic value:
(requesterPeerUID.ToString() + ApplicationUID.ToString() + userUID.ToString()).ToLower()
for old password to be able to reset it.
void JoinUserToNetwork(qp2plib::Guid userUID , qp2plib::Guid networkUID);
- Updates NetworkUIDS list on index server in VUser object identified by userUID argument by adding provided networkUID. You need to update your local VUser object manually after that or to reload.
void UnJoinUserFromNetwork(qp2plib::Guid userUID, qp2plib::Guid networkUID);
- Updates NetworkUIDSlist on index server in VUser object identified by userUID argument by removing provided networkUID. You need to update your local VUser object manually after that or to reload it.
qp2plib::VUser LogOffUser(::std::string Username);
- Removes Peer UID from PeerUIDSlist in VUser object on index server. Returns fresh copy of VUser object.
qp2plib::VNetwork SaveNetwork(qp2plib::VNetwork &network);
- Inserts or updates (based on UID value) VNetwrok object. If UID is empty insert will be performed. Result is fresh copy of VNetwork object from index server.
VNetwork newNetwork;
newNetwork.Name("network1");//THIS IS REQUIRED, IT IS ALIAS FOR newNetwork["name"]
newNetwork["owner"] = "Mike";
newNetwork = CM->VNIndexManager()->SaveNetwork(newNetwork);
if (newNetwork.UID != qp2plib::Guid::Empty())
{
printf("Network is saved , assigned UID is: %s", newNetwork.UID.ToString().c_str());
}
then update operation would be:
newNetwork ["owner"] = "Jeff";
newNetwork = CM->VNIndexManager()->SaveNetwork(newNetwork);
there cannot be to networks with same name in application scope.
void DeleteNetwork(qp2plib::Guid networkToDeleteUID);
- Deletes VNetwork object having exact UID:
CM->VNIndexManager()->DeleteNetwork(n.UID);
void QueryNetworks(::std::list<qp2plib::VNetwork> *result_list,::std::string QueryCommandText, int skip = 0, int limit = 0, ::std::string ascOrderBy = "", ::std::string descOrderBy ="");
- Searches for VNetworks matching query, then returns list of found VNetworks to requester
CM->VNIndexManager()->QueryNetworks(&result_list,"\"Properties.Interes\":\"Ecology\"");
If you expect a large number of object to be returned consider using pagination. In last two optional parameters, you enter comma separated property names.
long QueryNetworksCount(::std::string QueryCommandText);
- Searches for VNetworks matching query, then returns number of objects found
qp2plib::VObject SaveObject(qp2plib::VObject &obj);
- Inserts or updates (based on UID value) VObject object. If UID is empty insert will be performed. Result is fresh copy of VObject object from index server.
VObject newObject;
newObject["something"] = "[\"1\",\"2\",\"9\"]";
newObject = CM->VNIndexManager()->SaveObject(newObject);
if (newObject.UID != qp2plib::Guid::Empty())
{
printf("Object is saved , assigned UID is: %s", newObject.UID.ToString().c_str());
}
then update operation would be:
newObject["something"] = "[\"1\",\"2\",\"9\",\"15\"]";
newObject = CM->VNIndexManager()->SaveObject(newObject);
void DeleteObject(qp2plib::Guid objectToDeleteUID);
- Deletes VObject object having exact UID:
CM->VNIndexManager()->DeleteObject(newObject.UID);
void QueryObjects(::std::list<qp2plib::VObject> *result_list,::std::string QueryCommandText, int skip = 0, int limit = 0, ::std::string ascOrderBy = "", ::std::string descOrderBy ="");
- Searches for VObject-s matching query, then returns list of found VObject-s to requester
CM->VNIndexManager()->QueryObjects(&result_list("\"Properties.something\":\"15\"");
If you expect a large number of object to be returned consider using pagination. In last two optional parameters, you enter comma separated property names.
long QueryObjectssCount(::std::string QueryCommandText);
- Searches for VObjectss matching query , then returns number of objects found
Note we used json array for newObject["something"] value. Index server will automatically detect json array string shape or json object string shape and store it in that form. It is important that you don't have blank characters before "[" or "{" in this cases because index server will not do object/array check in that case . This enables you full-scale use of sub-objects in search queries.
You might need to have administrative application or site so you can give support to your user that can perform all index operation. When creating it have in mind that you must open session (become network peer) to gain access to the index server.
QuickP2P API provides you an easy way to obtain secure keys to be known only to peers involved with a tunnel. You use this keys to crypt/decrypt data during data transfer.
The secure key exchange is based on "Diffie–Hellman" algorithm with encampment to the original concept. There is no "real" fixed key part, instead some short-lived data existent for few seconds during handshake operation is used to generate what is called "Fixed part" in the original concept. This eliminates the possibility for an attacker knowing fixed part to intrude data integrity using any encryption breaking theory.
32 bytes of security data is known only to two peers involved with the tunnel. Security data is generated always during handshake procedure. After each successful connect/accept, generated bytes can be obtained from handler function Peer argument object:
void on_PeerConnect(qp2plib::Peer *remotePeer,qp2plib::Guid * transactionUID){
unsigned char key[16];
memcpy(key,remotePeer->TunnelSecretData,16);
....
}
void on_PeerAccept(qp2plib::Peer *remotePeer,qp2plib::Guid * transactionUID){
unsigned char key[16];
memcpy(key,remotePeer->TunnelSecretData,16);
....
}
quickP2P API has built-in classes that for AES encryption (chippering).
AES class - core AES encryption
If support following AES variants:
Key size:
Chipering mode :
Padding:
AESNetworkStream - used to prepare data for crypt transfer when a tunnel is a stream based (TCP). AESNetworkStream is not standardized. It's designed for easy use with quickP2P tunnels (you can use to else ware if both sides have it as the first gate). AESNetworkStream enables you to use CBC mode for real-time communication. It is because it provides you the ability to use fairly sized blocks crypt with CBC chippering. Normally in CBC mode you would need to get all data (like the whole file), then decrypt it so you could use it. AESNetworkStream can use more secure CBC mode and provide you partial data decryption.
For UDP datagrams you can simply use core AES class.
One thing to note when working with this classes is that original and crypted data do not have the same size. Crypted data is usually slightly larger than original.
Next is a simple example of crypting and decrypting text with core AES class:
qp2plib::Guid g;
unsigned char key[16];
g.WriteToByteArray(key);//we generate some random key just for test
qp2plib::AES a(qp2plib::AES::AESKS_Bits128,key, qp2plib::AES::AESPAD_PCKS7, qp2plib::AES::AESM_CBC);
//we generate some test text and convert it to bytes
const char *original = "Hello I'am text to crypt";
int sLen = strlen(original);
unsigned char buff1[512];
//we crypt
int cryptlen = a.Crypt((unsigned char*)original,0,sLen,buff1,0);
unsigned char buff2[512];
//we decrypt
int dlen = a.Decrypt(buff1, 0, cryptlen, buff2, 0);
//we convert bytes to text
buff2[dlen] = '\0';
const char *decrypted_text = (const char *)buff2;
This is the same thing done with AESNetworkStream:
qp2plib::Guid g;
unsigned char key[16];
g.WriteToByteArray(key);//we generate some random key
qp2plib::AESNetworkStream ans( qp2plib::AES::AESKS_Bits128 , key);
//we generate some test text
const char * original = "Hello I'am text to crypt";
int slen = strlen((const char*)original);
//we write data to stream to be crypted
ans.WriteForCrypt((unsigned char*)original,0,slen);
unsigned char buff1[512];
//we read crypted data form stream
int len = ans.ReadCrypted(buff1,0,8192);
//we now write crypted data for decryption
ans.WriteForDeCrypt(buff1,0,len);
unsigned char buff2[512];
//we read decrypted data
len = ans.ReadDeCrypted(buff2, 0, ans.AvailableDeCrypted());
//we convert bytes back to text
buff2[len] = '\0';
const char* decrypted = (const char*)buff2;
Usually, you also need to set CBC initial vector (commonly "IV" ) if you are using CBC mode. quickP2P AES class will generate it if not provided based on key value. In above example we didn't set it but it's recommended you do so. Both classes have means to provide you pointer to internal CBC internal vector buffer you can set bytes before you do any crypt/decrypt operation.
It's recommended that you add firewall exception in local OS for your application. Communication will find its paths anyway but firewall exception can drastically increase performance. While developing you can do that manually but you would probably need a way to do that on user computers. This is generally the most important for Windows operating systems. For another system this API version is defined in app bundle itself, so system ask a user once to allow this behavior and there is no need to do anything.
In order to start efficient development using qp2p it's recommended you first get basic knowledge about system concepts.
quickP2P is a complete environment that enables you to develop "out of the box" peer-to-peer applications. quickP2P is super-node based peer-to-peer system. Server (super-node) infrastructure is not part of this document thematic. Although the network is fully scalable and unlimitedly spreadable where new super-nodes form "point of peer's view" all it needs to know is that he needs to contact one super-node to become part of the network.
Main quickP2P system aspects are:
· NAT Traversal: a process of direct communication channel creation between two peers no matter if they are both behind of NAT devices or no. The result of the operation is in connected TCP or ready to send/receive UDP socket. Then you just use as in regular application using regular sockets API.
· Peer lookup: an easy way of finding or distinguishing one or many peers currently online by fully customizable metadata. Lookup scope is limited to particular application you use.
· Peer state tracking and broadcasting. Ability to instantly get notification of peers’ state changes and to be able to broadcast local peer state to others easily. Example of that you can see commonly in chat applications where you can find something like user status indicator away, offline, busy ... so this is a handy feature for every peer-to-peer application.
· Instant messaging. Sub instant message system is a handy tool for cases of tunnel pre-negotiation, passing control messages and chat like communication. NAT traversal operation can be completed in 1-15 seconds from moment of tunnel request to the moment when you can start using tunneled socket. So it's not always practical to initiate tunnel opening just to pass a small amount of data to another peer
· Index storage. There is always a need to store permanently (until explicitly not requested to change) some meta-data about your application users. We offer two abstractions: Virtual User and Virtual Network. API index storage interface provides you methods for their management and lookup by meta-data you define at your will. Network scope is limited to particular application you use.
· Data encryption and secure key exchange. Peer-to-peer (direct) communications are by its nature more secure in terms of data secrecy than client-server and client-server-client communications. Since many applications goal it to exploit this feature, we also added an easy way to secure key exchange and data encryption/decryption.
This enables you to focus directly on your application and start developing straight away, not worrying about auxiliary web services you would normally need.
Client API is available for .Net, native c++ STL fully mixable with Objective C++ and java as wrapper (android SDK). This puts no limits to platforms you are planning to support with your application. All API versions are very look-a-like with the only difference in programming language syntax.
quickP2P API (java/android SDKversion) -programming instructions
AUXILIARYMATERIAL:
- "qp2p_SimplestDemo_FileTransfer" demoeclipse android application project
quickP2P API (java/android SDKversion) -programming instructions
Startingwithjava/android SDK API(withandroid SDKsamples)
1. quickP2P lib delegate/eventsystem
1.1Events in quickP2PAPI
1.2 Callbacks in quickP2PAPI
1.3Event/Callback handler execution
2. Peer session creation
3. Peer lookup queries
4. Peer state tracing /changes notifications
5. Instantmessages
6. Direct communicationtunnelcreationbetweenpeers
7. Using virtualindexstorage
8. Security, keyexchangeand dataencryption/decryption
9. Required app permissions
10.Completelistofdelegates interfacesusedin ConnectionManagerwiththeirin-prints
qP2P lib Java API delegate/event system is based on delegates. Since Java supports anonymous objects creation, this becomes quite usable. Just watch out for garbage collector when using anonymous objects. You need to provide object holding right named method to serve as a delegate to event/callback handler. We provided interfaces your object can implement those methods from but important to note is fact that this is not necessary. The method needs to have the right name and right arguments to be invoked by event/callback in delegate object.
*note: One problem we have with this currently is lack of ability of easy code fuscation because of method name textual bindings. Fuscation will change method names and JNI wrapper will be unable to find them. This still can be achieved if you close all qp2p API interaction in separate classes then you can just omit those classes from fuscation.
Now we will show you some basic examples of using events/callbacks and there is no better way than giving simple examples. Once hooked event handler will be executed every time corresponding event triggers until we un-hook it manually or simply destroy objects. On the contrary, callback is usually passed as some method argument. It will execute only once to handle particular method execution asynchronous response.
- When you hook event once, it will execute response every time correspondent request arrives until you unhook it.
- If you have both event hooked and callback provided to execute on request completion first event is triggered then callback
We will now give you examples of hooking events. Here is example of hooking event to handler that is member of calling class:
publicclass TestActivity extends Activity implements
qp2plib.ConnectionManager.iPeerConnect_delegate {
private qp2plib.ConnectionManager cm = null;
…
@Override
protectedvoid onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test);
//we create new instance of connection manager class
cm = new qp2plib.ConnectionManager(new InetSocketAddress("supernode1.p2p-api.com",80));
…
cm.addEventHandler(ConnectionManager.ConnectionManagerEvent.onPeerConnected.getCode(), this);
…
}
//this is implemented method qp2plib.ConnectionManager.iPeerConnect_delegate
publicvoid onConnectionManager_PeerConnect(Peer peer, Guid TransactionUID) throws IOException {
//... handler body.
}
…
}
Here is how we would hook the same event to anonymous object method:
publicclass TestActivity extends Activity {
private qp2plib.ConnectionManager cm = null;
…
@Override
protectedvoid onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test);
//we create new instance of connection manager class
cm = new qp2plib.ConnectionManager(new InetSocketAddress("supernode1.p2p-api.com",80));
…
cm.addEventHandler(ConnectionManager.ConnectionManagerEvent.onPeerConnected.getCode(),
new qp2plib.ConnectionManager.iPeerConnect_delegate(){
publicvoid onConnectionManager_PeerConnect(Peer peer, Guid TransactionUID) throws IOException {
//... handler body.
}
});
…
}
…
}
Reference to delegate object will be kept in inner event/callback object. That will prevent the garbage collector from destroying an anonymous object that serves as a delegate.
1.2 Callbacksin quickP2PAPI
Callbacks are usually given as arguments of functions having unpredictable time for completion to trigger when that result becomes available. There are related only to particular function call/response.
When some event handler is set and we also have callback defend to trigger on the same particular event, both event handler and callback handler will be executed, first event handler then callback handler.
Now here is a use of callbacks demonstration on cm.Connect method. Callback delegate is the last argument of ConnectionManager.connect method. This callback executes when a nat-traversal operation completes. Here is callback handler in an anonymous object:
cm.Connect(remotePeer, ConnectionType.TCP.getCode() , null, new
ConnectionManager.iPeerConnectCallback_delegate(){
publicvoid onConnectionManager_PeerConnectCallback(Peer peer,
Guid PeerCheckpointUID, Guid PeerUID, Guid TransactionUID,
boolean Success, int PeerConnectFailureReason_code) {
//...
peer.getTunnelSocket().getOutputStream().writeBytes("Hello through NAT!");
//...
}
});
Reference to delegate object will be kept in inner event/callback object. That will prevent the garbage collector from destroying an anonymous object that serves as a delegate.
And here would be an example using calling class as a delegate with the appropriate method defined in it:
cm.Connect(remotePeer, ConnectionType.TCP.getCode() , null, this);
….
//this is implemented method qp2plib.ConnectionManager.iPeerConnect_delegate
publicvoid onConnectionManager_PeerConnect(Peer peer, Guid TransactionUID) {
//...
peer.getTunnelSocket().getOutputStream().writeBytes("Hello through NAT!");
//...
}
So basically we use common programming patterns you probably used by now in Java and Android SDK. The important thing to note about event and callback handlers triggered by quickP2P API is that they will be executed in “some” thread API may create on need. That thread will probably not be UI thread. Watch out when interacting with UI elements you need to transfer UI directives to UI thread. This is a rule that stands for all platforms/languages and not just for Java. Consider following example:
publicvoid onConnectionManager_SessionOpenCallback(ConnectionManager sender,boolean success,int SessionFailReason_code){
finalint code_val = SessionFailReason_code;
runOnUiThread(
new Runnable() {
privateintcode = code_val;
publicvoid run() {
((TextView) findViewById( R.id.textView1)).setText("Session opened!");
}
}
);
cm.Query("\"Properties.some_meta\":\"test456\"",null, 0, 0);
}
We want to set text for textView1 textbox , our handler onConnectionManager_SessionOpenCallback will be exacted from “some” thread so we need to transfer text changing directive to UI thread using runOnUiThread method.
In quickP2P Java, event/callback handlers are executed in threads API creates by need. This means that if you plan to start some long running operation when even/callback triggers you can just stay in the same thread and do tasks. API will create other new threads if needed for events that happen while that long-running task is being executed.
Also, there is conservation mechanism, because thread creation in java can be expensive. After the thread is used for handler execution it will not be destroyed for some time in order to possibly execute some new handler if it arrives.
Fact that event/callback arguments are executed in "some" threads imposes the rule that you can't interact with UI directly from them. You need to transfer execution of UI interaction code to UI thread by using context.runOnUiThread or something similar.
Consider following code from demo project:
Peer remotePeer = peers[0];
//we found our remote peer, now we will create socket tunnel to him
//we found that peer now we will connect to it. Note that track_connect_transaction_uid will match TransactionUID argument in [void onConnectionManager_PeerConnect(...]
Guid track_connect_transaction_uid =
cm.Connect(remotePeer, ConnectionManager.ConnectionType.TCP.getCode(),null, new ConnectionManager.iPeerConnectCallback_delegate() {//NEXT WE WAIT FOR RESULT IN [void onConnectionManager_PeerConnectCallback(...] in anonymous object below , ON OTHER SIDE [void onConnectionManager_PeerAccept(...] will trigger instead
publicvoid onConnectionManager_PeerConnectCallback(Peer peer,
Guid PeerCheckpointUID,
Guid PeerUID,
Guid TransactionUID,
boolean Success,
int PeerConnectFailureReason_code) {
//...failed handling code omitted...
if(Success){
//TUNNEL IS CREATED!
//In quickP2P java we can just keep this thread because it is certainly not UI thread and other threads for other
//handlers will be automatically created when needed, so we can continue our long running operation form this thread
DataInputStream in = null;
DataOutputStream out = null;
try {
in = new DataInputStream(peer.getTunnelTCPSocket().getInputStream());
out = new DataOutputStream(peer.getTunnelTCPSocket().getOutputStream());
} catch (IOException e) {
try{
peer.getTunnelTCPSocket().close();
}catch (IOException exs) {}
ShowMessageOnUIThread("Error while sending file(s)! Aborted...");
return;
}
ByteBuffer little_endian_buff = ByteBuffer.allocate(8);
byte[] buff = newbyte[8192];
for(String FileToSend : MainActivity.this.FilesToSend)
{
String[] tmp = FileToSend.trim().split(File.separator);
String name = tmp[tmp.length - 1];
BufferedInputStream FS = null;
try {//we open file for reading
FS = new BufferedInputStream(new FileInputStream(new File(FileToSend)));
char[] cname = name.toCharArray();
out.write(newbyte[] {(byte)cname.length});//we send name length
out.writeBytes(name);
little_endian_buff.order(ByteOrder.LITTLE_ENDIAN);
little_endian_buff.putLong(new File(FileToSend).length());
little_endian_buff.rewind();
out.write(little_endian_buff.array());//we send file length first
int read = 0;
//we read and send file blocks
while ((read = FS.read(buff, 0, buff.length)) > 0)
out.write(buff, 0, read);
FS.close();
//we wait for some response as confirmation
read = in.read(buff);
final String fileName = FileToSend;
if (read > 0)
ShowMessageOnUIThread("File " + fileName +" sent!");
} catch (IOException e) {
ShowMessageOnUIThread("Error while sending file(s)! Aborted...");
break;
}
}
try {//we close socket
in.close();
out.close();
} catch (IOException e) {}
try{
peer.getTunnelTCPSocket().close();
}catch (IOException e) {}
}
}
} );
We put our code for sending files through tunneled TCP socket right in handler body. This is the ok place because we know this will not block API and that other events happening to meanwhile will be served by other threads API will create on need. So we can just place here our possibly long-running file transfer operation. But also note there few calls to ShowMessageOnUIThread (a function that looks like this:
protectedvoid ShowMessageOnUIThread(String message){
final String msg = message;
MainActivity.this.runOnUiThread(//Event and callback handlers in quickP2P java are executed in random threads so when we interact with UI we need to send execution to UI thread
new Runnable() {
publicvoid run() {
ShowMessage(msg);
}
}
);
}
protectedvoid ShowMessage(String message){
Toast.makeText(this, message, Toast.LENGTH_SHORT).show();
}
We want to show a message box to the user, and a message box is UI element so we send execution of message box creation to UI thread.
For quickP2P client, to be able to do any operation like tunnel opening, sending an instant message, searching peers, virtual index operation ... we first need to join an abstract peer-to-peer network like you connect to the internet when your computer powers up. Steps for joining application peer to the super-node network would be this:
1- CreateConnectionManager object to handle operations, set required C.M. properties, handles...
It's always requiredtosetaccesstokens:
cm.setAccessTokens("00000000-XXXX-XXXX-XXXX-XXXXXXXXXXXX","00000000-XXXX-XXXX-XXXX- XXXXXXXXXXXX","");
The first argument is provider UID, second application UID, third is access key. You can see this values in you provider portal.
2- Set some meta-data so other peers can find us and we can find them by filtering values
cm.getLocalPeer().set("some_meta","blablabla");
cm.getLocalPeer().set("email", "someone@somepeople.com");
cm.getLocalPeer().set("gender","male");
you can also alter these properties later while the session is opened, you need to call
cm.BrodcastStateChange()toupdatelocalpeerinformationonnetwork.
3 - InitateConnectionManager.Open(callback) toopensession
4 -Wait open session completion handler to execute
(note, after session open completion avoid initiating connect - "tunnel open" operation in next 2-3 sec to give time to API to inspect your network environment properly)
Once we have established a session with super-node we can:
- Query for other peers on the network (Peer lookup) with mongo DB-like queries in our application scope
- Register for peers’ status tracking and receive notifications about changes
- Send/Receiveinstantmessages
- Open communication tunnels to other peers and other peers can open tunnels to us
- Use virtual index manager if we need it:
- Create|Delete|EditVirtualNetworks|Users
- Search virtual networks|users with mongo DB like queries
- Join|Un-joinusers tonetworks
....
We will talk about all these operations in following texts
Peer lookup queries query for peers that are currently online. A query request is initiated using
Guid transaction_uid = ConnectionManager.Query(String QueryText , iPeerQueryCompleted_delegate callback_delegate, int page , int pageLimit );
or handy version if we need to check single peer:
Guid transaction_uid = ConnectionManager.QuerySingle(Guid PeerUID, iPeerQueryCompleted_delegate callback_delegate);
methods of ConnectionManager object. Callback and event handler method has in-print like this:
publicinterface iPeerQueryCompleted_delegate{
void onConnectionManager_PeerQueryCompleted(Peer[] peers,int count,boolean Completed, Guid QueryTransactionID,int Page,int PageLimit,int TotalPeers);
}
Agruments:
FoundPeersListofpeersreturnedbyquery
CountNumberofpeersreturnedbyquery
CompletedIfcompletedthentrue,notthatsometimesresultwillcontainpeersbutthis valuewillbe false.Thiscanhappenforexampleif10000000peersarefoundbysearch criteria, and operationtimeoutexpiresbeforeallresultsarecollected
QueryTransactionIDTransactionIDofqueryrequest
PagePagereturnedifpaginationexists
PageLimitNumberofpeersperpageifpaginationexists
TotalPeersTotalnumberofpeersmatchedbyquery
Eventhookexample:
...
cm.addEventHandler(ConnectionManager.ConnectionManagerEvent.onPeerQueryResponse.getCode(), this);
...
publicvoid onConnectionManager_PeerQueryCompleted(Peer[] peers, int count,
boolean Completed, Guid QueryTransactionID, int Page,
int PageLimit, int TotalPeers) {....
Query text format is mongo DB like a query with the exception that you don't put opening and closing brackets.
Let's say we defined some meta properties for our peer like this:
cm.getLocalPeer().set("City","PaloAlto");
cm.getLocalPeer().set("Sex","female");
cm.getLocalPeer().set("Email","peer@peers.com");
cm.getLocalPeer().set("Age","31");
Here are some examples of queries that will include this peer in result:
Findpeerwithemail"peer@peers.com"thatiscurrentlyonline:
cm.Query("\"Properties.Email\":\"peer@peers.com\"");
Findpeerswithemailcontainedinthislist[peer@peers.com,mike@peers.com,a123@bla.com]thatarecurrently online:
cm.Query("\"Properties.Email\":{$in:[\"peer@peers.com\",\"mike@peers.com\",\"a123@bla.com\"]}");
FindfemalepeersfromPaloAltothatarecurrentlyonline:
cm.Query("\"Properties.City\":\"PaloAlto\",\"Properties.Sex\":\"female\"");
FindfemalepeersfromPaloAltowithabove30thatarecurrentlyonline:
cm.Query("\"Properties.City\":\"PaloAlto\",\"Properties.Sex\":\"female\",\"Properties.Age\":{$gt:30}");
You can test your queries on provider portal. To make more advance queries check out mongo dg query syntax.
Thereisoften aneedfor somepeertohavesomestate onnetwork, likeaway, busy, available .... You probablyseen thatascommonfeatureof chat applications. Ifyouthinkyoucouldachievesame functionalityusingInstantmessagesor somethingelseyouarerightbutthis providesyoumoreelegant wayoftransferringsuchinformationtopeersinstantly. Alsotherearecertainsituationswhen thiscomes handyasirreplaceablefeature. Imagine youhavesomechatlikeapplicationandyoukeepyour peer buddies in somelistor dictionary...your buddykid rips powercablesfromhis computersohis application
don'tmanagetonotify youthatheisoffline. Super-nodewillnoticethatin max90sec,andifyouregistered for thatpeerstatetrackingyouwillgetnotificationthatheisoffline.
Tousethis featureyouneedtohookeventtoacceptnotifications:
cm.addEventHandler(ConnectionManager.ConnectionManagerEvent.onPeerStateChanged.getCode(), this);
...
publicvoid onConnectionManager_PeerStateChanged(Peer peer, int State) {
if(State==0){
//0-isreservedfordisconnectedstate,1-initialconnected state,allvalues>1areavailablefor freeuse
ActiveFriends.erase(peer.getUID());//Peergotdisconnectedweremoveitfromour internallist
}
}
This is rarecasewhereyoudon'thave optiontousecallbackbecauseyouneverknowwhen notification willcome.
Alsoyouhavetoinformsuper-nodesthatyouwanttobenotified aboutthatpeer changes.Youdothat using following ConnectionManager functions :
public Guid RegisterStateTracking(Guid PeerUID);
public Guid RegisterStateTrackingMultiple(Guid[] PeerUIDList);
Usually you will call this function in handler of peer query response. From that moment you will receive notifications about state changes of peers that you registered tracking for. You can also cancel tracking of peers using:
public Guid UnregisterStateTracking(Guid PeerUID);
public Guid UnregisterStateTrackingMultiple(Guid[] PeerUIDList);
CommonplacetoplaceRegisterStateTrackingmethodisin abodyof peer query responsehandlerbecausethatis momentwhenyougetfresh informationofwhoisonline. If peer goesoffline youwill get qp2plib.Peer.DisconnectedState (=0)stateargumentvalue. If he comesbackhe needs toinform youthatheis back onlineand youagainneedtoregisterfor itsstatetrackingagain.So statustrackisvaliduntilpeergoesoffline, if he comes backyouneedtoregisterstatetrackingagain.Also youseethereisa needtocombineinstant messageswith thistomakeitfullyusable ,why:
- You could dopeerqueryand you wouldgetfresh informationabout onlinepeers, butafter30secsomepeerthat shouldbe visibleto youby particular applicationimplementation appearsonnetwork. Youdon'tknowthatheisonlineinthat moment andyouwouldnotknowthatuntil youdopeerqueryagain. Buthealsodoespeer queryandheknows thatyouareonlineso allhehas todois to informyouaboutthat. Hecando thatbysimply sendingyousomeinstantmessage.
cm.SendInstantMessage(remotePeer,"Hey, I'am here!", APP_COMMANDS.IM_ONLINE );
...
Then in receive instant message handler you would know you need to update your peer list.
publicvoid onConnectionManager_ReceiveInstantMessage(Guid FromPeer, Guid FromPeerCheckpoint, Guid MessageUID, int MessageType, byte[] Data) {
if(MessageType==APP_COMMANDS.IM_ONLINE.getCode()){
//updateyour list or doquery tocheckallagain
}else....
}
Wewilltalkaboutinstantmessageslater soyoucouldfullyunderstandthiscode.
Instatechanged notificationhandler:
publicvoid onConnectionManager_PeerStateChanged(Peer peer, int State){...
youseetwoargumentsPeerandState. Firstis completefreshpeerobjectofpeerwhosestatechanged and Stateisnewstateof thatpeer.Younoticeit is just intvaluesoyouarefreetouseanyvalueinrange from2toInteger.MAX_VALUEfor your application.
Values0and1arereservedfor
qp2plib.Peer.DisconnectedState = 0;
qp2plib.Peer.AuthentificatedState= 1;
Systemdepends onthemsoyoucannotusethesevaluesforanythingelse.
Tonotifyother peers aboutyour statechangecodewouldbesomethinglikethis:
- forexampleyouarenowbusy
cm.getLocalPeer().setState(MyApplicationPeerStateEnums.BUSY.getCode());
cm.BrodcastStateChange();
So weuse cm.BrodcastStateChange(); toinformothers ofourstatechanges.This functionalsoupdates our peerobjectinformationonsuper-nodesowealsouseitwhenwechangemetapropertieswhile sessionisactive.
Youdon'tneedtodostatebroadcastwhenyouareclosingsession/disposingC.M. because
ConnectionManager willdo thatautomatically.
Instant messages arecarriedusingsuper-nodes,andtheir purposeisto transfershortmessagesbetween peers innetwork. Often itis notpracticaltoalwaysopen directcommunicationchannel betweentwo peers ifoneneedstoinformotheraboutsomethingbecausetunnelcreationtakes1-15s andalso maybe wewant firstother peertoconfirm thathewillinglyacceptsthat connection.Thesearecommonlysome controlmessagesfromyour applicationor somethinglikechatmessages.
Tobeabletoreceivethemyouneed tohookfollowingevent handler:
cm.addEventHandler(ConnectionManager.ConnectionManagerEvent.onReceiveInstantMessage.getCode(), this);
...
publicvoid onConnectionManager_ReceiveInstantMessage(Guid FromPeer, Guid FromPeerCheckpoint, Guid MessageUID, int MessageType, byte[] Data) {
if(MessageType ==APP_COMMANDS.TextMessage.getCode()) {
//dosomething
}
elseif (MessageType== APP_COMMANDS.IM_ONLINE.getCode()){
//dosomething
}
elseif (MessageType== APP_COMMANDS.ALLOW_FILE_TRANSFER.getCode()){
//dosomething
}...
}
Tosend instantmessageyouuseoneof4 overloadsofcm.SendInstantMessagefunction:
public Guid SendInstantMessage(Guid RemotePeerCheckpointUID, Guid RemotePeerUID, byte[] message_buffer, int message_len, int InstantMessageType , iSendInstantMessageComplete_delegate callback_delegate)
public Guid SendInstantMessage(Guid RemotePeerCheckpointUID, Guid RemotePeerUID, String text_message, int InstantMessageType, iSendInstantMessageComplete_delegate callback_delegate
public Guid SendInstantMessage(Peer Peer, byte[] message_buffer, int message_len, int InstantMessageType, iSendInstantMessageComplete_delegate callback_delegate
public Guid SendInstantMessage(Peer Peer, String text_message, int InstantMessageType , iSendInstantMessageComplete_delegate callback_delegate)
Exampleof sendinstantmessage request:
cm.SendInstantMessage(OtherPeer,"Hi man!", APP_COMMANDS.TextMessage.getCode());
Younoticeall haveoptional argument toInstantMessageTypeused todistinguishthem, for exampleyou could havenumberof controlmessagesandtextmessages so this givesyoueasywaytodistinguishthem.
Youalsonoticethereiscallbackargument .That callbackwilltriggerwhenmessageis delivered toyour super-node,itdoesnotmeanitis delivered toother peerwhen callbacktriggers.Messagewilltravel to destinationpeersupernodeandthen itwillbedelivered to it.
ThemainAPIfeatureandprobablymainreasonwhy youuseitistheabilitytocreatedirectcommunication channels betweentwocomputersthat areboth behindNAT(router)devicesusingNATtraversal techniques.Themainadvantage ofthis API isthat asaresultyouwill getstandardplatformsockettouse fromthenon.Systemwillself inspectwhatisthebestmethodof tunnelcriterionbetween two peersand createit. Soyoudon'tneedtoknowanything aboutSTUN, NATportmappingprediction,UPnP, NATPMP, PCP...or anythingelsethathappens in thebackgroundand justwaitforyour prepared socket.
(Note - specialsituations when both peersareonsamelocal networkor evenonsame computerare handled byAPIsodatawillbetransferred locallyin thatcasenotgoing overtheInternet).
Inpast textswetalkedabouthow tofindsomepeer.When youknowthat,andyouwantto open direct communicationchannel youinitiateconnectrequest. On theotherside peer needs toacceptthis request sohandshakeprocedurecouldstart.
Youneed thistwoeventhandlers:
//Whenwemakeconnectrequest(tunnelcreation)wewaitforoperationcompletiontotrigger event
cm.addEventHandler(ConnectionManager.ConnectionManagerEvent.onPeerConnected.getCode(), this);
//Whensomeotherpeer initiatetunnelcreationtous,operationcompletionwilltrigger event
cm.addEventHandler(ConnectionManager.ConnectionManagerEvent.onPeerAccepted.getCode(), this);
...
//Aftersuccessfulconnectoperationthishandleristriggered
public void onConnectionManager_PeerConnect(Peer peer, Guid TransactionUID) { {
//peer.getTunnelTCPSocket() or peer.getTunnelUDPSocket() istunneledsocketreadyfordatatransfer
DataOutputStream out = new DataOutputStream(peer.getTunnelTCPSocket().getOutputStream());
out.writeUTF("Hellothroughtunnel!");
//dosomethingelse
}
.....
//THISWILLTRIGGERWHENOTHERSIDECALL cm.connect(...TARGETINGLOCALPEER
voidonPeerAcceptEventHandler(qp2plib::Peer*remotePeer,qp2plib::Guid*transactionUID){
{
//peer.getTunnelTCPSocket() or peer.getTunnelUDPSocket() istunneledsocketreadyfordatatransfer
DataInputStream in = new DataInputStream(peer.getTunnelTCPSocket().getInputStream());
DataOutputStream out = new DataOutputStream(peer.getTunnelTCPSocket().getOutputStream());
byte[] buff = new byte [512];
int read= in.read(buff,0,512);
out.writeUTF("Hellothroughtunneltoyouto!");
//dosomethingelse
}
One peerinitiatesconnectrequestthatwouldinthiscaseuseeventhandlersthatlooklikethis:
Guid track_connect_transaction_uid =cm.Connect(remotePeer, ConnectionManager.ConnectionType.TCP.getCode(),null, null);
andoncompletion:
publicvoid onConnectionManager_PeerConnect(Peer peer, Guid TransactionUID)
wouldtrigger onhis side. Onother side:
publicvoid onConnectionManager_PeerAccept(Peer peer, Guid TransactionUID)
would triggeronacceptingconnection.
if youneedtocontrolaccess- whocanconnectandwho cannot,youcanoverride "AcceptConnection
Reslover" byproviding your resolver callback function:
cm.setAcceptPeerConnectionResolver(new ConnectionManager.iAcceptPeerConnection_delegate() {
publicboolean onConnectionManager_AcceptPeerConnection(
ConnectionManager sender, Peer peer, int connectionType_code) {
if(<???some condition???>)
returntrue;
else
returnfalse;
}
});
Bydefaultall connectionrequest will beacceptedunless yousetcm.setBlockIncoming(true);
Tunnel accept/connectusingcallbackswouldlooklikethis:
SIDETHAT DOESCONNECT:
cm.Connect(remotePeer, ConnectionType.TCP, null,
new ConnectionManager.iPeerConnectCallback_delegate() {
publicvoid onConnectionManager_PeerConnectCallback(Peer peer,
Guid PeerCheckpointUID, Guid PeerUID, Guid TransactionUID,
boolean Success, int PeerConnectFailureReason_code) {
if(Success){
DataOutputStream out = new DataOutputStream(peer.getTunnelTCPSocket().getOutputStream());
out.writeUTF("Hello through tunnel!");
//...do something else...
}else{
Log.e("QuickP2P_API","Failed to connect, reason:" + ConnectionManager.PeerConnectFailureReason.values()[PeerConnectFailureReason_code].toString());
}
}
}
);
SIDETHAT DOESACCEPT:
cm.setPeerAcceptedCallback(
new ConnectionManager.iPeerConnectCallback_delegate() {
publicvoid onConnectionManager_PeerConnectCallback(Peer peer,
Guid PeerCheckpointUID, Guid PeerUID, Guid TransactionUID,
boolean Success, int PeerConnectFailureReason_code) {
if(Success){
DataInputStream in =
new DataInputStream(peer.getTunnelTCPSocket().getInputStream());
DataOutputStream out =
new DataOutputStream(peer.getTunnelTCPSocket().getOutputStream());
byte[] buff = newbyte [512];
int read = in.read(buff,0,512);
out.writeUTF("Hello through tunnel to you to!");
//...do something else...
}else{
Log.e("QuickP2P_API","Some peer tried to connect to us and failed, reason:" + ConnectionManager.PeerConnectFailureReason.values()[PeerConnectFailureReason_code].toString());
}
}
}
);
Thereisnothingspecialweneedtotalkaboutthis, youhaveareadysocketsoyoudo whatyouwantwith it fromthenon. Justhaveinmindthatyouareresponsible forsocketfromthenonsoyouneed tocloseit when younolongerneedit. Alsoasagoodpracticetofollowisthatyoucanjust savethat socketwhenyou finishthetaskfor newuse if needed.Soifyouneeddonewtaskinvolvingdatatransferwiththatsame
peer youcanjustpooloutthat socketinsteadof invokingtunnel creationagain(1-15sec). Youcandestroy suchsocketwhenyouleaveapplicationorif thatpeergoesoffline.
Since quickP2Psystemgoal istoprovideyoucompleteenvironmentso youcouldfocus justonapplication we introduced permanent indexstoragesub-system. Wefigured outthat it wouldbeconvenient toprovide waytostoreandbeabletosearchsomecommonlyused things likeuser registrationdatathat is more closelyawareof peers.Youdon'tneed tousethis sub-systemif youhaveyourownin mind.You wouldjust needsomekey you wouldstoreinpeer properties toassociatepeerswithyourobjectslater.
Youcanimagineindexsystemasremotedatabasewith two tablesUserandNetwork.Of coursetheseare notordinarytables thathavefixedcolumns.Youcanstorewhateverproperty(name,value)youneedand later search them.BesidescommonoperationslikeSELECT/INSERT/UPDATE/DELETEwhatismost importantisthatUserobjectis awarewhichpeersareauthenticated with it.Multiple peers canbe authenticated withsingle userobjectandalsoopen peer canbeauthenticated with multipleuserobjects. Both userandnetworkobjecthaveUIDandcustompropertybag. Constrains arethatUserobjectmust have"Username"propertyandNetworkobjectneedstohave"Name"property.Therecannotbetwo User objects with sameusernamein your applicationscopeandsamestands for namefor networks. Ifyou somehowneed toenable differentsituationforsomereasonyoucangeneratethesepropertiesinsome special waybased onsomeotherproperty.
Userobjecthas columns "Memberof networks"becauseyoucanjoin/un-joinsomeuserfromnetwork and "Currentlybound Peers"whichholds listofcurrently boundpeerUIDs. Userobjectalsohaspassword propertyyoucannotseeinproviderportal.
Objectused for virtualindex management is accessed by cm.VNIndexManager().All index operations complete synchronouslybecausethereare nocriticaltimedependent functions . If youneedasynchronousexecution youneed toimplementit. Alsoifthereis somethingwrongexceptionisthrown.
This is listofall Virtual index manageroperations:
publicnative VUser SaveUser(VUser user,String user_password);
- Insertsor updates virtualuser. Youpass user objectyouwantto saveandsaveit providingpasswordfor that user.IfVUser UIDpropertyis00000000-0000-0000-0000-000000000000systemwill doINSERTotherwiseit willdoUPDATE.Resultofoperationisfresh Userobjectlikeonindexserverinthatmomentthis also
meant PeerUIDSproperty willhavefreshlistofauthenticatedpeers. SoifUIDwas empty UIDwillgetsome real valuefromserverafteroperationcompletes.
This wouldbecodetocreatenewuser:
VUser newUser = new VUser();
newUser.setUsername("some_username");//THIS IS REQUIRED, IT IS ALIAS FOR newUser ["username"]
newUser.set("name", "Mike");
newUser.set("email", "This email address is being protected from spambots. You need JavaScript enabled to view it.");
newUser = cm.VNIndexManager().SaveUser(newUser, "somepassword");
if (newUser.getUID().equals(Guid.Empty)) {
System.out.println("User is saved , assigned UID is: " + ewUser.getUID().toString());
}
then updateoperationwouldbe:
newUser.set("name", "Mike2");
newUser = cm.VNIndexManager().SaveUser(newUser, "somepassword");
Notethat afterthis operationin caseof creatingnew user getPeerUIDS()will beempty.Peer needto call inAuthenticateUsermethodinorder tohave hisUIDappearingin this list.
public VUser AuthenticateUser(String Username, String Password);
- Checksforexistingusernamewith passwordandifitexistsaddspeerUIDtoVUser getPeerUIDS()list, then returns VUserobject:
qp2plib.VUser u;
...
u = cm.VNIndexManager().AuthenticateUser("some_username", "somepassword");
If youperformAuthenticateUseranduser isalreadyauthenticated thatis notbadoperation. Itwill not havesideeffectsand youcanuseittogetfreshVUser objectfromindex server.
public VUser[] QueryUsers( String QueryCommandText, int skip, int limit , String ascOrderBy, String descOrderBy);
- SearchesforVUsersmatchingquery,addslistof found VUsers toprovidedresult_list
cm.getVNIndexManager().QueryUsers(list_to_fill,"\"Properties.City\":\"PaloAlto\""); If youexpectlargenumberofobjectstobereturned considerusingpagination. Inlasttwo optional parameters youentercomma separatedpropertynames.
publiclong QueryUsersCount(String QueryCommandText);
- SearchesforVUsersmatchingquery,then returnsnumberofobjectsfound
public void DeleteUser(Guid userToDeleteUID);
- DeletesVUserobjecthavingexactUID:
cm.getVNIndexManager.DeleteUser(u.getUID());
publicvoid ChangeUserPassword(Guid userUID, String old_password, String new_password);
- ChangesVUserobjectpassword.Passwordsarenotvisible toanyone. Incasethat user forgets password andwantstoresetit,youneedtousemagicvalue:
(requesterPeerUID.ToString()+ ApplicationUID.ToString()+userUID.ToString()).ToLower()
foroldpasswordtobeabletoresetit.
publicvoid JoinUserToNetwork(Guid userUID , Guid networkUID);
- UpdatesNetworkUIDSlistonindexserverinVUserobjectidentified byuserUIDargument byadding providednetworkUID. Youneedtoupdate your localVUserobjectmanuallyafter thator toreload.
publicvoid UnJoinUserFromNetwork(Guid userUID, Guid networkUID);
- UpdatesNetworkUIDSlistonindexserverinVUserobjectidentified byuserUIDargument byremoving providednetworkUID. Youneedtoupdate your localVUserobjectmanuallyafter thator toreloadit.
public VUser LogOffUser(String Username);
- RemovesPeerUIDfromPeerUIDSlistin VUserobject onindexserver. Returnsfresh copyofVUser
object.
public VNetwork SaveNetwork(VNetwork network);
- Insertsor updates (basedonUIDvalue)VNetwrokobject. IfUIDisemptyinsertwillbeperformed.Result is fresh copyofVNetworkobjectfromindexserver.
qp2plib.VNetwork newNetwork = new qp2plib.VNetwork();
newNetwork.setName("network1");//THIS IS REQUIRED, IT IS ALIAS FOR newNetwork["name"]
newNetwork.set("owner","Mike");
newNetwork = cm.VNIndexManager().SaveNetwork(newNetwork);
if (newNetwork.getUID().equals(qp2plib.Guid.Empty))
{
System.out.println("Network is saved , assigned UID is: " + newNetwork.getUID().toString());
}
then updateoperationwouldbe:
newNetwork.set("owner","Jeff");
newNetwork = cm.VNIndexManager().SaveNetwork(newNetwork);
therecannotbetonetworkswithsamenamein applicationscope.
publicvoid DeleteNetwork(Guid networkToDeleteUID);
- Deletes VNetworkobjecthavingexactUID:
cm.VNIndexManager().DeleteNetwork(n.getUID());
public VNetwork[] QueryNetworks(String QueryCommandText, int skip, int limit , String ascOrderBy , String descOrderBy);
- SearchesforVNetworksmatchingquery,then returns listof foundVNetworkstorequester
cm.VNIndexManager().QueryNetworks(result_list,"\"Properties.Interes\":\"Ecology\""); If youexpectlargenumberofobject tobereturnedconsiderusingpagination. Inlasttwo optional parameters youentercomma separatedpropertynames.
publiclong QueryNetworksCount(String QueryCommandText);
- SearchesforVNetworksmatchingquery,then returns numberofobjectsfound
public VObject SaveObject(VObject obj);
- Insertsor updates (basedonUIDvalue)VObjectobject.If UIDisemptyinsertwill be performed.Resultis fresh copyofVObjectobjectfromindexserver.
qp2plib.VObject newObject = new qp2plib.VObject();
newObject.set("something","[\"1\",\"2\",\"9\"]");
newObject = cm.VNIndexManager().SaveObject(newObject);
if (newObject.getUID().equals(qp2plib.Guid.Empty)){
System.out.println("Object is saved , assigned UID is: " + newObject.getUID().toString());
}
then updateoperationwouldbe:
newObject.set("something","[\"1\",\"2\",\"9\",\"15\"]");
newObject = cm.VNIndexManager().SaveObject(newObject);
public