stun:stun.l.google.com:19302.Peer1 and Peer2 want to chat. Neither peer has a public IP address. Here is the step-by-step process:Peer1 needs to send packets to Peer2 so the NAT allows incoming traffic from Peer2. The same applies in the opposite direction.Peer1 provides the ID of Peer2, the server returns Peer2's public IP:port. It also notifies Peer2 that Peer1 wants to communicate by providing Peer1's public IP:port. In other words, it organizes the meeting (French: rendez-vous).for {
n, remoteAddr, err := conn.ReadFrom(buffer)
...
data := buffer[:n]
var message types.Message
err = json.Unmarshal(data, &message)
...
if message.Type == "REGISTRATION" {
s.registerNewClient(remoteAddr)
}
}
func (s *Server) registerNewClient(remoteAddr net.Addr) {
identifier := uuid.New().String()
s.Clients[identifier] = types.IPAddressPair{
Public: remoteAddr.String(),
}
messageToSend := types.Message{
Type: "REGISTRATION",
Payload: identifier,
}
...
for {
n, remoteAddr, err := conn.ReadFrom(buffer)
...
data := buffer[:n]
var message types.Message
err = json.Unmarshal(data, &message)
...
if message.Type == "REGISTRATION" {
s.registerNewClient(remoteAddr)
}
}
func (s *Server) registerNewClient(remoteAddr net.Addr) {
identifier := uuid.New().String()
s.Clients[identifier] = types.IPAddressPair{
Public: remoteAddr.String(),
}
messageToSend := types.Message{
Type: "REGISTRATION",
Payload: identifier,
}
...
REGISTRATION, the server generates a UUID for that client, stores the UUID together with the public IP:port from which the packet was received, and then returns the UUID to the client (as shown in Listing 4.2). In this implementation, the peer does not even need to know its own public IP:port. It only needs its assigned UUID. At this point, both peers are registered, but they still do not know how to reach each other directly.func (s *Server) startCommunicationHandler(w http.ResponseWriter,
r *http.Request) {
if r.Method == "POST" {
...
var startCommunication types.StartCommunication
err := json.NewDecoder(r.Body).Decode(&startCommunication)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
clientAddr, ok := s.Clients[startCommunication.Peer]
...
json.NewEncoder(w).Encode(clientAddr)
clientInitiatorAddr, ok :=
s.Clients[startCommunication.PeerInitiator]
...
adrr, err := netip.ParseAddrPort(clientAddr.Public)
mess := types.Message{
Type: "INITIATE_CHAT",
Payload: clientInitiatorAddr.Public,
}
messBytes, err := json.Marshal(mess)
...
fmt.Println("SENDING UDP ON" + adrr.String())
_, err = s.Conn.WriteToUDPAddrPort(messBytes, adrr)
...
}
}
Peer1 sends an HTTP request containing its own identifier and the identifier of the peer it wants to communicate with. The server responds with Peer2's public IP:port. At the same time, it sends a UDP packet toPeer2 containing Peer1's public IP:port. This HTTP request acts purely as a signaling mechanism and is not part of the data path. For both peers, receiving this information signals that they can begin the UDP hole punching process.REGISTRATION, INITIATE_CHAT, HELLO, SUCCESS, TEXT and one that's not shown here HEART-BEAT.REGISTRATION - this is a type of message that server sends as a response to the client registration. In this message, payload is just client's identifier. It's saved and shown to the client, so that client can send it to other clients.INITIATE_CHAT - this type of message is also from server. In the payload of message, there's a public IP:port of the client that wants to start chatting. So when client receives this message, it means that it needs to start the process of UDP hole punching.HELLO - this simple message with an empty payload carries important meaning. You have successfully punched a hole in your NAT! This is a first message that comes straight from the other client. The client then responds with a message of type SUCCESS to the other client, so that he knows that he can start chatting, and stop sending empty UDP packets.TEXT - it's a chat message. There's no special handling of this message. It's simply printed to the user.HEART-BEAT - handling of these messages is ignored, because for client it does not mean anything. It's there to keep NAT mapping open if there's no chat messages for a few seconds.func (c *Client) StartUdp() {
...
for {
n, remoteAddr, err := c.Conn.ReadFrom(buffer)
...
data := buffer[:n]
var message types.Message
err = json.Unmarshal(data, &message)
...
if message.Type == "REGISTRATION" {
c.Identifier = message.Payload
fmt.Println("Your identifier is: " + c.Identifier)
}
if message.Type == "INITIATE_CHAT" {
fmt.Println(message)
c.PeerAddress = types.IPAddressPair{
Public: message.Payload,
}
c.ChatInitiated = true
go c.StartChatting()
} else if message.Type == "HELLO" {
c.ContactSuccess = true
response := types.Message{
Type: "SUCCESS",
Payload: "...",
}
res, err := json.Marshal(response)
...
_, err = c.Conn.WriteTo(res, remoteAddr)
...
}
if message.Type == "SUCCESS" {
c.Message <- message
}
if message.Type == "TEXT" {
fmt.Println("Message: " + message.Payload)
}
}
}
func (c *Client) StartChatting() {
interval := 3 * time.Second
ticker := time.NewTicker(interval)
defer ticker.Stop()
for {
select {
case mess := <-c.Message:
if mess.Type == "SUCCESS" {
c.PeerContactSuccess = true
fmt.Println("Successful contact!")
if c.ChatInitiated {
fmt.Println("Press any key to start chatting!")
}
go c.startHeartBeat()
return
}
case t := <-ticker.C:
mess := types.Message{
Type: "HELLO",
Payload: "...",
}
message, err := json.Marshal(mess)
...
fmt.Println("SENDING ON: " + c.PeerAddress.Public)
remoteAddr, err := net.ResolveUDPAddr("udp",
c.PeerAddress.Public)
...
_, err = c.Conn.WriteTo(message, remoteAddr)
...
}
}
}
select statement that handles two cases. The first case listens for incoming messages on the message channel. If a message of type SUCCESS is received, it means the other client has responded to our UDP packet and has successfully punched a hole in its NAT. At that point, the loop exits and a goroutine is started to send HEART-BEAT messages every three seconds, as mentioned earlier. The second case executes every three seconds until a SUCCESS message is received. It sends a HELLO message to the peer's public IP:port. When this packet passes through the NAT, it creates (or maintains) a NAT mapping for the other client's address. This HELLO message therefore has two responsibilities: it creates the necessary NAT condition to allow incoming packets from the peer, and it allows the peer to detect that hole punching has succeeded.