Node Application API documentation - SkycoinProject/skywire GitHub Wiki
App programming interface (located within the skywire/pkg/app
module) should expose methods for Apps to connect to a piped connection, perform handshake and exchange data with remote nodes.
App interface should expose following methods:
// Addr implements net.Addr for App connections.
type Addr struct {
PubKey transport.PubKey
Port uint16
}
// LoopAddr stores addressing parameters of a loop package.
type LoopAddr struct {
Port uint16
Remote Addr
}
// Packet represents message exchanged between App and Node.
type Packet struct {
Addr *LoopAddr
Payload []byte
}
// Config defines configuration parameters for an App
type Config struct {
AppName string
AppVersion string
ProtocolVersion string
}
// Setup sets up an app using default pair of pipes and performs handshake.
func Setup(config *Config) (*App, error) {}
// Accept awaits for incoming loop confirmation request from a Node and
// returns net.Conn for a received loop.
func (app *App) Accept() (net.Conn, error) {}
// Dial sends create loop request to a Node and returns net.Conn for created loop.
func (app *App) Dial(raddr *Addr) (net.Conn, error) {}
// Addr returns empty Addr, implements net.Listener.
func (app *App) Addr() net.Addr {}
// Close implements io.Closer for an App.
func (app *App) Close() error {}
Communication between Node and an App happens over the piped connection using binary multiplexed protocol.
The following is the expected format of a App Packet:
| Packet Len | Type | Message ID | JSON Body |
| 2 bytes | 1 byte | 1 byte | ~ |
- Packet Len specifies the total packet length in bytes (exclusive of the Packet Len field).
- Type specifies the App Packet Type.
- Message ID specifies multiplexing ID of a message, response for this message should contain the same ID.
- JSON Body is the packet body (in JSON format) that is unique depending on the packet type.
App Packet Types Summary:
Type | Name |
---|---|
0x00 | Init |
0x01 | CreateLoop |
0x02 | ConfirmLoop |
0x03 | Send |
0x04 | Close |
0xfe | ResponseFailure |
0xff | ResponseSuccess |
Sent by an App to a Node. This packet is used to handshake connection between an App and a Node. Node will typically check if app is allowed by the config file and which port should be statically allocated it.
JSON Body:
{
"app-name": "foo",
"app-version": "0.0.1",
"protocol-version": "0.0.1"
}
Response:
-
ResponseFailure
witherror
. -
ResponseSuccess
without body.
Sent by an App to a Node. This packet is used to open new Loop to a remote Node.
JSON Body:
{
"pk": "<remote-pk>",
"port": <remote-port>
}
Response:
-
ResponseFailure
witherror
. -
ResponseSuccess
with{ "pk": "<local-pk>", "port": <local-port> }
Sent by a Node to an App to notify about request to open new Loop from a remote Node
JSON Body:
[
{
"pk": "<local-pk>",
"port": <local-port>
},
{
"pk": "<remote-pk>",
"port": <remote-port>
}
]
Response:
-
ResponseFailure
witherror
. -
ResponseSuccess
with empty body.
Sent by a Node and an App. This message is used to exchange messages through a previously established Loop.
JSON Body:
{
"addr": {
"port": <local-port>,
"remote": {
"pk": "<remote-pk>",
"port": <remote-port>
}
},
"payload": "<binary-data>"
}
Response:
-
ResponseFailure
witherror
. -
ResponseSuccess
with empty body.
Sent by a Node and an App. App uses this message to notify about closed Loop. Node sends this message after remote node is requested to close established Loop.
JSON Body:
{
"port": <local-port>,
"remote": {
"pk": "<remote-pk>",
"port": <remote-port>
}
}
Response:
-
ResponseFailure
witherror
. -
ResponseSuccess
with empty body.
Simple ping-pong
client and server apps can be implemented in such way:
Server:
package server
import (
"log"
"github.com/watercompany/skywire/pkg/app"
)
func main() {
// Open connection with Node
helloworldApp, err := app.Setup(&app.Config{AppName: "helloworld-server", AppVersion: "1.0", ProtocolVersion: "0.0.1"})
if err != nil {
log.Fatal("Setup failure: ", err)
}
defer helloworldApp.Close()
log.Println("listening for incoming connections")
// Start listening loop
for {
// Wait for new Loop
conn, err := helloworldApp.Accept()
if err != nil {
log.Fatal("Failed to accept conn: ", err)
}
log.Println("got new connection from:", conn.RemoteAddr())
// Handle incoming connection
go func() {
buf := make([]byte, 4)
if _, err := conn.Read(buf); err != nil {
log.Println("Failed to read remote data: ", err)
}
log.Printf("Message from %s: %s", conn.RemoteAddr().String(), string(buf))
if _, err := conn.Write([]byte("pong")); err != nil {
log.Println("Failed to write to a remote node: ", err)
}
}()
}
}
Client:
package server
import (
"log"
"os"
"github.com/watercompany/skywire/pkg/app"
"github.com/watercompany/skywire/pkg/cipher"
)
func main() {
// Open connection with Node
helloworldApp, err := app.Setup(&app.Config{AppName: "helloworld-client", AppVersion: "1.0", ProtocolVersion: "0.0.1"})
if err != nil {
log.Fatal("Setup failure: ", err)
}
defer helloworldApp.Close()
// Read remote PK from stdin
remotePK := cipher.PubKey{}
if err := remotePK.UnmarshalText([]byte(os.Args[1])); err != nil {
log.Fatal("Failed to construct PubKey: ", err, os.Args[1])
}
// Dial to remote Node
conn, err := helloworldApp.Dial(&app.Addr{PubKey: remotePK, Port: 10})
if err != nil {
log.Fatal("Failed to open remote conn: ", err)
}
// Send payload
if _, err := conn.Write([]byte("ping")); err !=
log.Fatal("Failed to write to a remote node: ", err)
}
// Receive payload
buf := make([]byte, 4)
if _, err = conn.Read(buf); err != nil {
log.Fatal("Failed to read remote data: ", err)
}
log.Printf("Message from %s: %s", conn.RemoteAddr().String(), string(buf))
}