My original intent for this thread happened to be discussing the iterations of my
design, so this is how I will conclude.
Only having a working knowledge of C++ definitely added to my inability to settle
on a proper design -- or maybe I should say my lack of knowledge in object-oriented
and structured programming. The initial design was completely imperative in style,
with a lot of shared state and an inadequate use of classes. There was a plan in
place and I knew where I wanted to take it, but looking back it was not the
best approach for the problem.
For example, the hello message is the first message sent or received in the
protocol, so I had a function:
void v0 :: upstream :: priv :: recv_hello(const boost::system::error_code& ec,
const std::size_t bytes)
{
...
// setup recv_msg for hello
m_recv_msg.reset(new v0::msg_manifest());
boost::shared_ptr<v0::msg_manifest> msg =
boost::dynamic_pointer_cast<msg_manifest>(m_recv_msg);
...
}
There were similar functions for every message in the protocol, all shared a
number of variables that were data members of the upstream class. Two were
message variables descending from an abstract base class developed for
messages; one was for sending and one was for receiving. The two statements shown
above characterize their usage: a pointer to the base class is reset and then
dynamically cast into the type of message expected. Every function followed a
basic pattern: 1) setup the buffer, and 2) do a read or write on the socket -- if
anyone has looked at the code, there is a fatal flaw in the way I read in the msg;
however, I feel that could have easily been corrected.
Another major difference between the designs, is that I originally did not expect
to know the read or write size, so I used a message parser class. Boost.Tribool
came in handy for this purpose and the code had the pattern:
void v0 :: upstream :: priv :: recv_hello(const boost::system::error_code& ec,
const std::size_t bytes)
{
...
boost::tribool result(m_msg_parser.parse_hello(bytes, msg));
if (result)
{
// the next state
send_manifest_req_to_the_oracle(boost::system::error_code());
}
else if (!result)
{
m_socket.close();
start_accept();
}
else
{
m_socket.async_read_some(boost::asio::buffer(m_msg_parser.buffer()),
boost::bind(&v0::upstream::priv::recv_hello,
shared_from_this(),
boost::asio::placeholders::error,
boost::asio::placeholders::bytes_transferred));
}
...
}
The message parser class was responsible for reading in the raw data using a
stateful, somewhat optimistic, algorithm for the specific message that was being
received. It would then set the specific fields for the given message type to be
used by subsequent functions in the upstream class.
The aforementioned design is what I was referring to as the jumping-off point in
Part I. This code was all scrapped but was the genesis for the current design
briefly discussed in Part III. A more thorough explanation of the current design
can be found in doc/chasm-p2p-design.rst within the repository.
In conclusion, I can not say for a fact that my initial design was good or bad, it
happened to not be right for the project. The design process has been an
enlightening experience; it has provided an insightful look into the process that
I was severely lacking. The open-source development model is such a great way to
learn, I can not even imagine ever working on closed-source software again.
-mfm