When I first implemented the connection sequence to connect to the peer to peer network in bitcoin-s-spv-node I simply followed the bitcoin developer reference which states

Until both peers have exchangedversion messages, no other messages will be accepted.
If a version message is accepted, the receiving node should send a verackmessage—but no node should send a verack message before initializing its half of the connection by first sending a version message.

Simple  enough, send a version message to a peer, then send a verack message to  be able to successfully connect to a peer on the network. Truthfully I  did not dig further into the actual implementation inside of Bitcoin  Core until today. The Core codebase is sufficiently complex to warrant  writing a blog post about following the logic of connecting to a peer.  Hopefully this can help future developers/users of Core get up to speed  quicker.

The very first thing we need to do is grab an address to connect to. All addresses are managed by a class called AddrMan. The address manager has three primary responsibilities (credit to sipa for this enumeration):

  1. Decide what connections to keep in our connection pool
  2. Decide what new connections to make
  3. Decide what to respond in case of receiving a GetAddr message

The next place we need to visit is the CConnMan.  CConnMan is the connection pool manager for the connection to our peers  on the p2p network. If we want to broadcast any message to the network,  CConnMan is where you need to do it. CConnMan has really nice quantifiers to be able to broadcast a message (or more generically pass an arbitrary function to).

Next we register a node with the CNodeSignals to receive a node's signals. I'm not 100% sure of  what this means or how it works, but it ends up calling the ProcessMessages inside of main.cpp.

Since we can receive more than one message at a time from a peer, ProcessMessages calls a function called ProcessMessage, which handles each message individually.

The first message type we check is the version message,  which is the first message a peer should send when connecting to a new  peer. It indicates various meta information about the protocol features  our peer supports. Eventually after checking a bunch of conditions to  see if our nodes are compatible, we send the connecting peer our version message. Soon after sending the peer our version message, we send a VERACK message acknowledging that we can communicate with our connecting peer.

Now  that we have broadcasted a VERACK message, we need to wait for our connecting peer to reply to with its own VERACK message. We have a case  inside of ProcessMessage to handle this. Finally we set the current state of the node to "connected" to indicate we can send normal messages to it on the peer to peer network.

This is a really simple overview of how process messages works inside of Bitcoin Core. There are other interesting things like CNode, CNodeState, CNodeStats that all manage various states of a connection to a peer which would  require another blog post to elaborate on. If this one gets enough  interest I'll consider writing another.

Feel free to follow me or my company on twitter or checkout my company's website https://suredbits.com