(3) Peer To Peer Chat Tutorial in Go
- Jason Fantl
- Oct 25, 2022
- 3 min read
Updated: Oct 31, 2022
Concept
In order to build a robust network, each node will need to have multiple connections spread through the network. Below we see a network where one connection breaks and the network splits into two separate parts. We can avoid this by maintaining multiple connections and trying to spread out our connections to avoid clustering.

We also want to avoid a single node connecting to everyone, they would become a bottleneck for network communication. But a network is very likely to have a dedicated entry node which everyone makes their initial requests through. People could share their own address with their friends as an entry point to the the network, but people tend to just use the default option provided. So how do we create this robust network shape? The easiest solution is to randomly bounce multiple join requests (we'll call them JOIN_REQs from now on) through the network until enough (however many connections we want to maintain) are accepted. Nodes will accept the JOIN_REQ with a probability equal to one divided by the number of connections they already have plus 1 (plus one to avoid divide by zero errors). This means as nodes gain more connections they are less likely to accept a new connection, while nodes with fewer connections are more likely to accept. This would produce a randomly connected network which is hopefully balanced.
More on joining protocol ideas
If the JOIN_REQ had an equally random chance of being accepted at each hop, then any entry nodes would have much higher chance of being connected to. For example, if everyone enters the network through node A, and every join has a 50% chance of being accepted on each hop, then over 50% of the JOIN_REQs would end up at node A (more then 50% since with multiple hops it can end up back at node A). The chances of even seeing a JOIN_REQ would drop dramatically the farther from node A you are. If you lower the probability of accepting, the average distance to node A would increase, but on average the requests would still center around node A.
Perhaps we use a random walk of length l? We give the JOIN_REQ a hop-count which decreases each hop it makes, then when it reaches zero, it is accepted. What might this result in? It makes sense that the more connections a node has, the more likely our JOIN_REQ has of ending on it (when l is large, the steady state vector can be calculated and large degrees correspond with a higher likelihood of being landed on). That means nodes with many connections are more likely to gain a new connection, increasing their chances again, leading to a positive feedback loop.
You might consider a grid topology where nodes only track up to 4 connections, one for each direction on a compass, then trying to route messages such that a new node would connect to all surrounding nodes (why would you do this? I don't know, it certainly doesn't fit our desires, but it would be neat).
What if you require everyone to have exactly d connections, and will not accept any more then that, passing a JOIN_REQ on to someone else? Once the network has d+1 nodes, each will have d connections (every other node), and not a single one will accept any new JOIN_REQs. The network gets stuck. And in fact, it isn't even always possible for n nodes to have d connections. For example, a graph with 5 nodes, each with 3 connections, is not possible (and in general, regular graphs where n*d is odd do not exist).
Code
We added a default connection point, so if you just type 'connect', it will send a JOIN_REQ to the hard-coded default address. But if you want to join a different network, you just type 'connect some_persons_address.
Establishing connections will be a bit more complicated now. We need to establish a TCP connection in order to send a JOIN_REQ, but that connection shouldn't give us access to the network because the node hasn't accepted our JOIN_REQ yet, they may decide to pass it on. If a node does decide to accept, they will send a join acknowledgment (we will now call JOIN_ACK). This means when we are listening for connections we need to be ready to handle two kinds of requests: JOIN_REQs will be temporary connections we immediately throw away, we'll create a new connection if we decide to accept. Then JOIN_ACKs we will always accept (for now), and so want to maintain the connection and start receiving messages over it.
We need more then just our Message struct now, we need to be able to send JOIN_REQ and JOIN_ACK over connections now. And then our messages need to carry their original sender since we only know the address of the latest node it was announced from, we want to know who originally sent it (we will look at stopping spoofing identity later).
main.go
messaging.go
connections.go
Soon we will have a few more files, so lets start using Go's build command, rather then the run command.
go build
Then run the generated executable in three terminals. You should just be able to type 'connect' in the second and third terminal you run, and they should connect to the first. Senda a message from the second terminal, you should see the senders address in everyone's terminal, not the address of the person who last passed the message on.
Next
We will want to maintain a minimum number of connections. We also will need to alter the joining protocol a bit, since there are some issues with it. But before any of that, we should reorganize our code.
Comments