LessonsExtensions: Built inLesson 67

Peer to peer with dataChannelXT

Let two programs talk directly, with no server carrying their messages, through WebRTC data channels: peers, the offer and answer they swap, channels that open, text and bytes, events, and closing down.

  • Extensions
  • Lesson 67 of 83
  • About 13 minutes
  • 7 examples

Straight from one program to another

WebRTC lets two programs talk directly, peer to peer, even when both sit behind home routers, with no server in the middle carrying their messages. Browsers use it for video calls. The xTalk Suite's dataChannelXT uses its data channels, through libdatachannel, to send text and bytes; its handlers begin with dc, and its helpers, such as dcStateName, come with it.

The examples run both peers in one script on one computer, so each waits a moment for the other with wait until, never more than five seconds. In an app each peer is a different program, and events tell it when something happens.

A peer

A peer is your end of one connection. dcCreatePeer makes one; its text names ICE servers, helpers that find a way through routers (empty is enough on one computer, and a line such as stun:stun.l.google.com:19302 helps across the internet). Its state and how far it has got finding its addresses are numbers, which dcStateName and dcGatheringName say in words. dcFreePeer frees it, and its handle stops working:

ExampleRun it in the playground
put dcInit() && "(0 is good)"
put dcLibraryVersion() begins with "libdatachannel"
put dcCreatePeer(empty) into tPeer
put tPeer > 0
put dcStateName(dcPeerState(tPeer))
put dcGatheringName(dcGatheringState(tPeer))
get dcFreePeer(tPeer)
put dcStateName(dcPeerState(tPeer))
Output
0 (0 is good)
true
true
new
new
unknown

Offer and answer

Before two peers can talk they swap two short texts: an offer and an answer. Each lists the addresses its peer can be reached at, its candidates. Making a channel makes a peer write its offer, which is complete when dcGatheringState is 2; dcLocalDescription gives the text. How the text travels is up to you, a web server, a chat, an email or a QR code, and is called signalling:

ExampleRun it in the playground
put dcCreatePeer(empty) into tA
put dcCreateChannel(tA, "chat") into tChannel
put the milliseconds + 5000 into tUntil
wait until dcGatheringState(tA) = 2 or the milliseconds > tUntil with messages
put dcLocalDescriptionType(tA)
put dcLocalDescription(tA) into tOffer
put line 1 of tOffer
put tOffer contains "a=candidate"
get dcFreePeer(tA)
Output
offer
v=0
true

Connecting the two

With both peers in one script, the texts go straight across: A writes its offer, B takes it with dcSetRemoteDescription and writes its answer, and A takes B's answer the same way. The peers then try each other's addresses, and the channel opens:

ExampleRun it in the playground
put dcCreatePeer(empty) into tA
put dcCreatePeer(empty) into tB
put dcCreateChannel(tA, "chat") into tChannel
put the milliseconds + 5000 into tUntil
wait until dcGatheringState(tA) = 2 or the milliseconds > tUntil with messages
get dcSetRemoteDescription(tB, dcLocalDescription(tA), "offer")
get dcSetRemoteDescription(tA, dcLocalDescription(tB), "answer")
wait until dcChannelIsOpen(tChannel) or the milliseconds > tUntil with messages
put "A is" && dcStateName(dcPeerState(tA))
put dcChannelIsOpen(tChannel)
put dcChannelLabel(tChannel)
get dcFreePeer(tA)
get dcFreePeer(tB)
get dcPoll()
Output
A is connected
true
chat

The last line reads and drops the events the run left over, so that they do not reach the next example.

Messages are events

dcSendText sends text on an open channel, and dcSendData sends bytes, up to 60000 a message (-4 means too big). On the other side each message is an event: dcPoll gives every event since the last poll, oldest first, as an array, each with a "name", the "peer" and "channel" it is about, and more. A message is a "dcMessage" event, with its text under "text" and bytes under "payload". In an app, poll on a timer, or let dcStartPolling do it:

ExampleRun it in the playground
put dcCreatePeer(empty) into tA
put dcCreatePeer(empty) into tB
put dcCreateChannel(tA, "chat") into tChannel
put the milliseconds + 5000 into tUntil
wait until dcGatheringState(tA) = 2 or the milliseconds > tUntil with messages
get dcSetRemoteDescription(tB, dcLocalDescription(tA), "offer")
get dcSetRemoteDescription(tA, dcLocalDescription(tB), "answer")
wait until dcChannelIsOpen(tChannel) or the milliseconds > tUntil with messages
put dcSendText(tChannel, "Hello, B!") && "(0 is good)"
put dcSendData(tChannel, numToByte(0) & numToByte(255)) && "(0 is good)"
put dcSendData(tChannel, binaryEncode("x60001", "")) && "for 60001 bytes"
repeat until the number of lines of tGot is 2 or the milliseconds > tUntil
   wait 20 milliseconds with messages
   repeat for each element tEvent in dcPoll()
      if tEvent["name"] is "dcMessage" and tEvent["text"] is not empty then
         put "B got the text" && tEvent["text"] & return after tGot
      else if tEvent["name"] is "dcMessage" then
         put "B got" && the number of bytes in tEvent["payload"] && "bytes" & return after tGot
      end if
   end repeat
end repeat
put line 1 to -1 of tGot
get dcFreePeer(tA)
get dcFreePeer(tB)
get dcPoll()
Output
0 (0 is good)
0 (0 is good)
-4 for 60001 bytes
B got the text Hello, B!
B got 2 bytes

Channels your way

dcCreateChannelEx makes a channel with its options: a protocol name; unordered, so a message need not wait for the one before it; retries and a time limit in milliseconds (0 retries means a lost message stays lost, fine for game moves where only the newest counts, and -1 means no limit); and negotiated with a stream id, so both sides make the channel themselves and nothing is announced:

ExampleRun it in the playground
put dcCreatePeer(empty) into tA
-- label, protocol, unordered, retries, time limit, negotiated, stream id
put dcCreateChannelEx(tA, "moves", "game", true, 0, -1, false, -1) into tChannel
put dcChannelLabel(tChannel) && dcChannelProtocol(tChannel)
put dcChannelPeer(tChannel) = tA
put dcCreateChannelEx(tA, "control", empty, false, -1, -1, true, 5) into tChannel
put dcChannelStreamId(tChannel)
get dcFreePeer(tA)
Output
moves game
true
5

Closing down

dcCloseChannel closes a channel, and both ends get a "dcChannelClosed" event. dcClosePeer hangs up the whole connection, and the other side sees its state go to closed. dcFreePeer then frees the peer and its channels:

ExampleRun it in the playground
put dcCreatePeer(empty) into tA
put dcCreatePeer(empty) into tB
put dcCreateChannel(tA, "chat") into tChannel
put the milliseconds + 5000 into tUntil
wait until dcGatheringState(tA) = 2 or the milliseconds > tUntil with messages
get dcSetRemoteDescription(tB, dcLocalDescription(tA), "offer")
get dcSetRemoteDescription(tA, dcLocalDescription(tB), "answer")
wait until dcChannelIsOpen(tChannel) or the milliseconds > tUntil with messages
get dcCloseChannel(tChannel)
get dcClosePeer(tA)
wait until dcPeerState(tB) = 5 or the milliseconds > tUntil with messages
put dcChannelIsOpen(tChannel)
put "B is" && dcStateName(dcPeerState(tB))
get dcFreePeer(tA)
get dcFreePeer(tB)
get dcPoll()
Output
false
B is closed

Across the internet, two peers also need a way to swap their offer and answer, and sometimes a TURN server to relay for them when no direct route exists. Learn dataChannelXT (Help > Examples) and the Dictionary say more.

Try it yourself

Write a command connectPair that makes two peers and a channel, connects them and returns the three handles as items (A, B, channel), or empty after five seconds without an open channel. Use it to send three numbered lines from A to B, and put what B gets.

Show a solution
ExampleRun it in the playground
function connectPair
   local tA, tB, tChannel, tUntil
   put dcCreatePeer(empty) into tA
   put dcCreatePeer(empty) into tB
   put dcCreateChannel(tA, "chat") into tChannel
   put the milliseconds + 5000 into tUntil
   wait until dcGatheringState(tA) = 2 or the milliseconds > tUntil with messages
   get dcSetRemoteDescription(tB, dcLocalDescription(tA), "offer")
   get dcSetRemoteDescription(tA, dcLocalDescription(tB), "answer")
   wait until dcChannelIsOpen(tChannel) or the milliseconds > tUntil with messages
   if not dcChannelIsOpen(tChannel) then
      get dcFreePeer(tA)
      get dcFreePeer(tB)
      return empty
   end if
   return tA, tB, tChannel
end connectPair

put connectPair() into tPair
repeat with tIndex = 1 to 3
   get dcSendText(item 3 of tPair, "line" && tIndex)
end repeat
put the milliseconds + 5000 into tUntil
repeat until the number of lines of tGot is 3 or the milliseconds > tUntil
   wait 20 milliseconds with messages
   repeat for each element tEvent in dcPoll()
      if tEvent["name"] is "dcMessage" then put tEvent["text"] & return after tGot
   end repeat
end repeat
put line 1 to -1 of tGot
get dcFreePeer(item 1 of tPair)
get dcFreePeer(item 2 of tPair)
get dcPoll()
Output
line 1
line 2
line 3

Words in this lesson

Each one finds every lesson that uses it. The Dictionary has the whole story of each, with its syntax and more examples.