3
\$\begingroup\$

I needed to perform network communications in my Linux C++ project. I thought that it was a good idea to operate on a higher level of abstraction than raw system calls. Also I love OO design. So I'm implementing an object-oriented high-level Unix network open-source library (There are issues with tab-size if you watch source code on github, but the tabs are aligned well if watching in a text editor).

At the moment the library is partially implemented and is in a process of development. I've already tested some parts of it in real project and I find it's high level of abstraction extremely convenient.

The library checks all system calls for errors, so user shouldn't worry about checking every function's return status (thou exception system is still under construction). It allows easily convert socket addresses and strings in both directions. Socket classes provide convenient methods to get and set their properties, messages are sent and received as strings. TCP socket can recieve messages separated by terminator character or messages determined by required message size e.t.c.

I would like to know community's opinion if I made some design or implementation mistakes, if there could be done some improvements and if the library could be useful in some way to somebody. Any feedback is appreciated.

Here is a working example of a UDP echo-server implementation using my library.

#include <iostream>
#include <Unet/UdpSocket.hpp>
#include <Unet/Ipv4Address.hpp>

int main ( int argc , char** argv )
{

    //  Check program parameters to be provided
    if ( argc != 3 )
    {
        std::cout << "Usage: UdpEchoServer $(SERVER_IP) $(SERVER_PORT)" << std::endl;
        return -1;
    }

    //  Create and open UDP socket with Internet family address
    Unet::UdpSocket socket;
    socket.open();
    std::cout << "Server socket was opened successfully." << std::endl;

    //  Set server socket to reuse address
    socket.setOption(SO_REUSEADDR,1);
    std::cout << "Server socket option \"SO_REUSEADDR\" was set to \"1\"." << std::endl;

    //  Bind socket to the specified local address
    socket.bind(Unet::Ipv4Address(argv[1],argv[2]));
    std::cout << "Server socket was bound successfully." << std::endl;

    //  Datagram which will be recieved and sent back
    Unet::Datagram datagram;

    while ( true )
    {
        try
        {
            if ( socket.hasUnreadData() )
            {
                //  Recieve incomming datagram
                datagram = socket.recieveDatagram();
                std::cout << "[ <-- ] [" << Unet::Ipv4Address(datagram.address).toString() << "] - " << datagram.message << std::endl;

                //  And send it back
                socket.sendDatagram(datagram);
                std::cout << "[ --> ] [" << Unet::Ipv4Address(datagram.address).toString() << "] - " << datagram.message << std::endl;
            }
        }

        //  Catch and print all exceptions
        catch ( Unet::Exception exception )
        {
            std::cout << "Unet exception: " << exception.getMessage() << std::endl;
        }
        catch ( std::exception exception )
        {
            std::cout << "Standard exception: " << exception.what() << std::endl;
        }
        catch ( ... )
        {
            std::cout << "Unknown exception" << std::endl;
        }
    }
}


P.S. I'm not a Linux specialist, so I doubt which name to choose for the library Unix-net, Linux-net or Posix-net. Which is more appropriate and logical?

\$\endgroup\$
  • 2
    \$\begingroup\$ There are already a million of these. Writting your own is a bad idea as you have to fix all the same mistakes that have been fixed before. Don't re-invent the wheel find one that already works. \$\endgroup\$ – Martin York Sep 4 '13 at 19:17
2
\$\begingroup\$

As Loki stated: don't re-invent the wheel. In the interest of learning and sharing, here are some ideas to consider:

if ( argc != 3 )

Use an API already designed for parsing the command line. There must be dozens.

socket.setOption(SO_REUSEADDR,1);

Since this is an object, I would hide the dependency as follows:

socket.reuseAddress();

Since this is a networking object, I would prefer not to write the above statement at all. Rather, if the socket mustn't reuse the address, the API should provide a method to disable the behaviour.

socket.hasUnreadData()

A socket typically doesn't offer direct data reading and writing. The reason is that a data stream is a much more reusable concept. Were a developer to use your API, they would have to code functionality separately for reading from a socket vs. reading from a file. This restriction seems to limit the potential of software developed. Rather:

InputStream *dataIn = new SocketInputStream();
OutputStream *dataOut = new SocketOutputStream();
socket.setSource( dataIn );
socket.setDestination( dataOut );

Now functionality can be written that reads data from any type of stream, making the source of the data independent (socket, file, database). If you absolutely must have the socket read/write datagrams, then use a terser method name:

socket.isReady()

This line:

socket.bind(Unet::Ipv4Address(argv[1],argv[2]));

Could be:

socket.bind(argv[1],argv[2]);

This simplifies the API for the developer. Otherwise all developers of the API will have to reference Ipv4Address. They shouldn't have to care. It's great to have the flexibility of putting in an Ipv6Address, but that can be handled automatically.

Regarding the exception handling:

        std::cout << "Unet exception: " << exception.getMessage() << std::endl;
        std::cout << "Standard exception: " << exception.what() << std::endl;

Having the API override what(), eliminates the duplication:

        std::cout << "Exception: " << exception.what() << std::endl;

The what() method will differentiate the exception classes in its message.

One more pet peeve:

while ( true )

Since the program doesn't actually continue forever, the loop should have a logical termination condition that tells the next person reading the source code when the program actually terminates. Some examples:

while ( socket.isOpen() )      // Exit upon socket closure
while ( isRunning() )          // Exit upon user request
while ( !command.is( EXIT ) )  // Exit when the QUIT command is received

Regarding the bind method, consider the following pseudo-code:

void Socket::bind( String host, String port ) {
  Address *address = AddressFactory.newInstance( host );
  bind( address, atoi( port ) );
}

Since the socket class already knows about Unet::Ipv4Address, adding a convenience method makes the class easier to use. Java gained popularity for a several reasons, including the simplicity of its networking API for trivial tasks. Using an AddressFactory, internally, will allow programs to work with either IPv4, IPv6, or any as-of-yet-invented protocol in the future. In C++, the application might have to be recompiled, but in Java, porting from IPv4 to IPv6 addresses has been mostly transparent.

If developers are forced to hard-code a reference to IPv4 in their application, then they will have to do extra work to make the software use a different Internet Protocol version.

\$\endgroup\$
  • \$\begingroup\$ Thank you very much for your response. I agree with you in almost everything except the socket.bind(argv[1],argv[2]); idea. Socket could be bound to the Unix address which should be constructed from a single string. That is why the bind function expects a reference to an abstract address object. This also implements the dependency inversion principle. Bind is dependent on an abstract object. Regarding the don't re-invent the wheel this is an educational project which allowed me to increase my knowledge in C++ and network programming, so I believe I did not waste my time pointlessly. \$\endgroup\$ – Kolyunya Dec 19 '13 at 7:28

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.