The data-channels example demonstrates how to establish a WebRTC connection and exchange arbitrary data between a web browser and a Pion WebRTC server using DataChannels. This is the foundation for building chat applications, game state synchronization, file transfers, and more.
Overview
DataChannels provide a bidirectional peer-to-peer communication channel that can send arbitrary data. Unlike media tracks which are optimized for real-time audio/video, DataChannels are flexible and can be configured for different reliability and ordering requirements.
Key Features
Automatic DataChannel handling from browser
Bidirectional message exchange
Random message generation every 5 seconds
Connection state monitoring
Base64 session description encoding for easy copy-paste signaling
How It Works
Create PeerConnection
Initialize a new RTCPeerConnection with STUN server configuration: config := webrtc . Configuration {
ICEServers : [] webrtc . ICEServer {
{
URLs : [] string { "stun:stun.l.google.com:19302" },
},
},
}
peerConnection , err := webrtc . NewPeerConnection ( config )
if err != nil {
panic ( err )
}
defer peerConnection . Close ()
Register DataChannel Handler
Set up a handler that fires when the browser creates a DataChannel: peerConnection . OnDataChannel ( func ( dataChannel * webrtc . DataChannel ) {
fmt . Printf ( "New DataChannel %s %d \n " , dataChannel . Label (), dataChannel . ID ())
// Register handlers for this channel
dataChannel . OnOpen ( func () {
// Channel is ready for communication
})
dataChannel . OnMessage ( func ( msg webrtc . DataChannelMessage ) {
fmt . Printf ( "Message from DataChannel ' %s ': ' %s ' \n " ,
dataChannel . Label (), string ( msg . Data ))
})
})
Exchange Session Descriptions
Perform WebRTC handshake by exchanging offer and answer: // Receive offer from browser
offer := webrtc . SessionDescription {}
decode ( readUntilNewline (), & offer )
err = peerConnection . SetRemoteDescription ( offer )
if err != nil {
panic ( err )
}
// Create and send answer
answer , err := peerConnection . CreateAnswer ( nil )
if err != nil {
panic ( err )
}
err = peerConnection . SetLocalDescription ( answer )
if err != nil {
panic ( err )
}
// Wait for ICE gathering to complete
<- webrtc . GatheringCompletePromise ( peerConnection )
// Output answer to paste in browser
fmt . Println ( encode ( peerConnection . LocalDescription ()))
Send Messages
Once the channel is open, send periodic messages: ticker := time . NewTicker ( 5 * time . Second )
defer ticker . Stop ()
for range ticker . C {
message , err := randutil . GenerateCryptoRandomString (
15 , "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" ,
)
if err != nil {
panic ( err )
}
fmt . Printf ( "Sending ' %s ' \n " , message )
if err = dataChannel . SendText ( message ); err != nil {
panic ( err )
}
}
Complete Source Code
Main Application
Helper Functions
package main
import (
" bufio "
" encoding/base64 "
" encoding/json "
" errors "
" fmt "
" io "
" os "
" strings "
" time "
" github.com/pion/randutil "
" github.com/pion/webrtc/v4 "
)
func main () {
// Prepare the configuration
config := webrtc . Configuration {
ICEServers : [] webrtc . ICEServer {
{
URLs : [] string { "stun:stun.l.google.com:19302" },
},
},
}
// Create a new RTCPeerConnection
peerConnection , err := webrtc . NewPeerConnection ( config )
if err != nil {
panic ( err )
}
defer func () {
if cErr := peerConnection . Close (); cErr != nil {
fmt . Printf ( "cannot close peerConnection: %v \n " , cErr )
}
}()
// Set the handler for Peer connection state
peerConnection . OnConnectionStateChange ( func ( state webrtc . PeerConnectionState ) {
fmt . Printf ( "Peer Connection State has changed: %s \n " , state . String ())
if state == webrtc . PeerConnectionStateFailed {
fmt . Println ( "Peer Connection has gone to failed exiting" )
os . Exit ( 0 )
}
if state == webrtc . PeerConnectionStateClosed {
fmt . Println ( "Peer Connection has gone to closed exiting" )
os . Exit ( 0 )
}
})
// Register data channel creation handling
peerConnection . OnDataChannel ( func ( dataChannel * webrtc . DataChannel ) {
fmt . Printf ( "New DataChannel %s %d \n " , dataChannel . Label (), dataChannel . ID ())
// Register channel opening handling
dataChannel . OnOpen ( func () {
fmt . Printf (
"Data channel ' %s '-' %d ' open. Random messages will now be sent every 5 seconds \n " ,
dataChannel . Label (), dataChannel . ID (),
)
ticker := time . NewTicker ( 5 * time . Second )
defer ticker . Stop ()
for range ticker . C {
message , sendErr := randutil . GenerateCryptoRandomString (
15 , "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" ,
)
if sendErr != nil {
panic ( sendErr )
}
// Send the message as text
fmt . Printf ( "Sending ' %s ' \n " , message )
if sendErr = dataChannel . SendText ( message ); sendErr != nil {
panic ( sendErr )
}
}
})
// Register text message handling
dataChannel . OnMessage ( func ( msg webrtc . DataChannelMessage ) {
fmt . Printf ( "Message from DataChannel ' %s ': ' %s ' \n " ,
dataChannel . Label (), string ( msg . Data ))
})
})
// Wait for the offer to be pasted
offer := webrtc . SessionDescription {}
decode ( readUntilNewline (), & offer )
// Set the remote SessionDescription
err = peerConnection . SetRemoteDescription ( offer )
if err != nil {
panic ( err )
}
// Create an answer
answer , err := peerConnection . CreateAnswer ( nil )
if err != nil {
panic ( err )
}
// Create channel that is blocked until ICE Gathering is complete
gatherComplete := webrtc . GatheringCompletePromise ( peerConnection )
// Sets the LocalDescription, and starts our UDP listeners
err = peerConnection . SetLocalDescription ( answer )
if err != nil {
panic ( err )
}
// Block until ICE Gathering is complete
<- gatherComplete
// Output the answer in base64 so we can paste it in browser
fmt . Println ( encode ( peerConnection . LocalDescription ()))
// Block forever
select {}
}
Important Concepts
Connection State vs ICE Connection State
The example monitors PeerConnectionState rather than ICEConnectionState:
PeerConnectionStateFailed : No network activity for 30 seconds or another failure occurred
PeerConnectionStateDisconnected : Faster timeout detection, but connection may recover
PeerConnectionStateClosed : Explicit closure, usually from DTLS CloseNotify
Choose the appropriate state based on your reconnection strategy.
This example disables Trickle ICE for simplicity: // Wait for all ICE candidates to be gathered
<- webrtc . GatheringCompletePromise ( peerConnection )
In production, use Trickle ICE to allow ICE gathering and connection establishment to happen concurrently: peerConnection . OnICECandidate ( func ( candidate * webrtc . ICECandidate ) {
if candidate != nil {
// Send candidate to remote peer via signaling
}
})
By default, DataChannels are reliable and ordered (like TCP). You can configure them differently: // Unreliable, unordered (like UDP)
dataChannel , err := peerConnection . CreateDataChannel ( "unreliable" , & webrtc . DataChannelInit {
Ordered : & [] bool { false }[ 0 ],
MaxRetransmits : & [] uint16 { 0 }[ 0 ],
})
This is useful for real-time gaming or other latency-sensitive applications.
Running the Example
Start the application
cd examples/data-channels
go run main.go
Open in browser
Navigate to the examples server at http://localhost and select the Data Channels example
Complete handshake
Click to generate an offer in the browser
Copy the offer and paste it into the terminal
Copy the answer from the terminal
Paste the answer back into the browser
Exchange messages
The server will send random messages every 5 seconds
Type messages in the browser to send them to the server
Watch both the browser console and terminal for messages
DataChannels support both text and binary data. Use SendText() for strings and Send() for binary data ([]byte).
Use Cases
Chat Applications Build real-time messaging with low latency using DataChannels
File Transfer Send files peer-to-peer without going through a server
Game State Sync Synchronize game state between players in real-time
Remote Control Send control commands to IoT devices or remote applications