I'm trying to build a simple server software for training purpose, most likely a IRC server, but I'm not there yet.
I'm currently implementing a TCP socket class, to ease the use of the C socket API. It uses the addrinfo
struct and getaddrinfo()
(which add some constraints).
TcpSocket.hpp
#ifndef TCPSOCKET_H
#define TCPSOCKET_H
#include <netdb.h>
#include <string>
#include <memory>
namespace Socket {
/**
* TcpSocket class implementation to facilitate the use of sockets
*/
class TcpSocket
{
public:
TcpSocket();
TcpSocket(int family, int flags);
TcpSocket(int socket, addrinfo info, bool connected, bool bound);
virtual ~TcpSocket();
//Avoiding copy
TcpSocket(const TcpSocket &socket) = delete;
TcpSocket &operator=(const TcpSocket &socket) = delete;
void bind(int port);
void connect(std::string adress, int port);
void listen(int maxQueue);
std::shared_ptr<TcpSocket> accept();
void send(const char *data, unsigned int length, int flags);
/**
* Receive data (blocking)
*@return true if socket is still open, false otherwise
*/
bool receive(char* msg, int len, int flags);
void close();
private:
void setInfo(int port);
void setInfo(std::string adress, int port);
void openSocket(addrinfo *info);
addrinfo * mInfo;
int mSock = -1;
bool mSockCreated = false;
bool mBound = false;
bool mConnected = false;
bool mClosed = false;
};
}
#endif
TcpSocket.cpp
(Please don't mind the DEBUG
function, this is going away)
#include "TcpSocket.hpp"
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
#include <errno.h>
#include <unistd.h>
#include <cstring>
#include <string>
#include "exceptions.hpp"
//DEBUG
#include <iostream>
#define DEBUG_ACT true
namespace Socket {
void DEBUG(std::string message)
{
if(DEBUG_ACT)
std::cout << "DEBUG : " << message << std::endl;
}
// Public :
TcpSocket::TcpSocket()
{
mInfo = new addrinfo;
memset(mInfo, 0, sizeof *mInfo);
mInfo->ai_family = AF_UNSPEC;
mInfo->ai_socktype = SOCK_STREAM;
//Can't create socket now.
}
TcpSocket::TcpSocket(int family, int flags)
{
mInfo = new addrinfo;
memset(mInfo, 0, sizeof *mInfo);
mInfo->ai_family = family;
mInfo->ai_socktype = SOCK_STREAM;
mInfo->ai_flags = flags;
if(family == AF_UNSPEC)
//Can't create socket now
return;
mSock = socket(mInfo->ai_family, mInfo->ai_socktype, 0);
if(mSock == -1)
{
SocketCreationException except(std::string(strerror(errno)));
throw except;
}
//Socket succesfully "opened".
mSockCreated = true;
}
TcpSocket::TcpSocket(int socket, addrinfo info, bool bound, bool connected)
: mSock(socket), mBound(bound), mConnected(connected)
{
mInfo = new addrinfo(info);
}
TcpSocket::~TcpSocket()
{
if(!mClosed)
close();
freeaddrinfo(mInfo);
}
void TcpSocket::bind(int port)
{
if(mBound && mConnected)
throw SocketBindingException("Already bound");
setInfo(port);
addrinfo * result;
for(result = mInfo; result != NULL; result = mInfo->ai_next)
{
if(!mSockCreated)
{
try {
openSocket(result);
} catch(SocketCreationException &e) {
continue;
}
}
//Socket sucessfully opened from here
if( ::bind(mSock, result->ai_addr, result->ai_addrlen) == 0)
{
mBound = true;
return;
}
}
//Couldn't bind, throw
throw SocketBindingException("Can't bind to port");
}
void TcpSocket::connect(std::string address, int port)
{
if(mConnected)
throw SocketConnectException("Already connected");
setInfo(address, htons(port));
addrinfo * result;
for(result = mInfo; result != NULL; result = mInfo->ai_next)
{
if(!mSockCreated)
{
try {
openSocket(result);
} catch(SocketCreationException &e) {
continue;
}
}
//Socket sucessfully opened from here
if( ::connect(mSock, result->ai_addr, result->ai_addrlen) == 0)
{
mConnected = true;
return;
}
}
//Couldn't connect, throw
throw SocketConnectException("Can't connect to host");
}
void TcpSocket::listen(int maxQueue)
{
if( ::listen(mSock, maxQueue) != 0)
throw SocketListenException(std::string(strerror(errno)));
DEBUG("Listening...");
}
std::shared_ptr<TcpSocket> TcpSocket::accept()
{
DEBUG("Starting to accept");
union
{
sockaddr addr;
sockaddr_in in;
sockaddr_in6 in6;
sockaddr_storage s;
} address;
socklen_t addressSize = sizeof (sockaddr_storage);
int newSock;
if( (newSock = ::accept(mSock, (sockaddr*)&address.s, &addressSize)) == -1)
throw SocketAcceptException(std::string(strerror(errno)));
DEBUG("1 client accepted");
addrinfo info;
memset(&info, 0, sizeof info);
if(address.s.ss_family == AF_INET)
{
info.ai_family = AF_INET;
info.ai_addr = new sockaddr(address.addr);
}
else
{
info.ai_family = AF_INET6;
info.ai_addr = new sockaddr(address.addr);
}
return std::shared_ptr<TcpSocket>(new TcpSocket(newSock, info, true, false));
}
void TcpSocket::send(const char *data, unsigned int length, int flags)
{
const char * buff = data;
int status = 0;
int total_sent = 0;
int left_to_send = length;
while(total_sent < length)
{
status = ::send(mSock, buff + total_sent, left_to_send, flags);
if(status == -1)
{
throw SocketSendException(std::string(strerror(errno)));
}
else
{
total_sent += status;
left_to_send -= status;
}
}
}
bool TcpSocket::receive(char* msg, int len, int flags)
{
int status;
if( (status = ::recv(mSock, msg, len, flags)) == -1)
throw SocketReceiveException(std::string(strerror(errno)));
else if(status == 0)
return false;
return true;
}
void TcpSocket::close()
{
if( ::close(mSock) == -1)
throw SocketCloseException(std::string(strerror(errno)));
else
mClosed = true;
}
// Private :
void TcpSocket::setInfo(int port)
{
setInfo("null", port);
}
void TcpSocket::setInfo(std::string address, int port)
{
const char *charAddress;
if(address == "null")
charAddress = NULL;
else
charAddress = address.c_str();
addrinfo hints = *mInfo;
int status;
if( (status = getaddrinfo(charAddress, std::to_string(port).c_str(), &hints, &mInfo)) != 0)
{
delete charAddress;
throw SocketException("getaddrinfo returned non-zero : " + std::string(gai_strerror(status)));
}
delete charAddress;
}
void TcpSocket::openSocket(addrinfo *info)
{
mSock = socket(info->ai_family, info->ai_socktype, info->ai_protocol);
if(mSock == -1)
{
SocketCreationException except(std::string(strerror(errno)));
throw except;
}
}
}
main.cpp (usage example, you don't need to review this file)
#include "TcpSocket.hpp"
#include <iostream>
#include <exception>
#include <cstring>
#include <memory>
using namespace Socket;
using Socket_p = std::shared_ptr<TcpSocket>;
int main(int argc, char *argv[])
{
Socket_p sock(new TcpSocket);
Socket_p client;
try {
sock->bind(1170);
sock->listen(5);
client = sock->accept();
}
catch( std::exception &e)
{
std::cout << e.what() << std::endl;
}
//Welcoming the new user.
client->send("Welcome !\n\f", 15, 0);
//Closing the listening soket, we want nobody else.
sock->close();
char data[512];
memset(&data, 0, 512);
while( client->receive(data, sizeof data, 0) )
{
client->send(data, sizeof data, 0);
memset(&data, 0, 512);
}
client->close();
return 0;
}
What is good is that it's all-purpose, and it seems to function great. However, the use of so many booleans look a bit sloppy to me, but it seemed like the best way to avoid common problems.
I'm also wondering if this all-purposeness is that good, because it seems like this class can't be extended or specialized.
What do you think of my approach? I'm also interested by any security concerns you may have as this is the first time I work with the C standard lib.
-
2\$\begingroup\$ Welcome to Code Review! Don't worry about your question, it looks good :-) \$\endgroup\$Mast– Mast ♦2015年08月01日 20:31:16 +00:00Commented Aug 1, 2015 at 20:31
-
\$\begingroup\$ Well, you could replace those booleans with enum and use it as flags as in : stackoverflow.com/questions/1448396/… or you could use a bitset. \$\endgroup\$Zeks– Zeks2015年08月01日 21:18:56 +00:00Commented Aug 1, 2015 at 21:18
-
2\$\begingroup\$ I have a similar question you might take a look: codereview.stackexchange.com/q/68240/39810 \$\endgroup\$glampert– glampert2015年08月01日 22:15:40 +00:00Commented Aug 1, 2015 at 22:15
-
\$\begingroup\$ @Zeks Oh, nice trick. But redefining every useful operators would complicate the class more. Or I can create a special type Socket::StateFlags rather than Socket::TcpSocket::StateFlags. \$\endgroup\$florian-s– florian-s2015年08月02日 13:33:59 +00:00Commented Aug 2, 2015 at 13:33
1 Answer 1
The code is generally clear, readable and easy to understand, which is great. However, you can still improve a few things.
Generally speaking, you don't want to manage memory manually like you're currently doing for
mInfo
. There is an easy way to make it managed effortless. Create the following type:struct addrinfo_delete { void operator()(addrinfo* ptr) const { freeaddrinfo(ptr); } };
Now, instead of storing an
addrinfo*
field in yourTcpSocket
class, you can make it store andstd::unique_ptr<addrinfo, addrinfo_delete>
field instead and let it memory by itself. Call itaddrinfo_ptr
and you will probably want to use it in some other places where you need anaddrinfo*
.Moreover, having an
std::unique_ptr
pointer member won't make you lose anything fature-wise since you already explicity made your class uncopyable in the first place.When possible, use
std::make_shared<Foo>(bar);
instead ofstd::shared_ptr<Foo> foo(new Foo{bar});
. It will prevent thenew
/delete
visual mismatch and it can also save some reference counting. Therefore, instead of defining aSocket_p
class, I would be as explicit as possible and usestd::shared_ptr<TcpSocket>
directly.@Zeks advice is really good: you can pack your boolean flags into an
std::bitset<4>
to make your class more compact.You shouldn't have to write
client->close();
at the end of yourmain
since your class already does that in its destructor. Let the destructor do its job and it will be easier for everyone.It is good practice to always fully qualify every component from the standard library. For example use
std::memset
instead ofmemset
. Not only can it avoid some name clashes, but it will also make it easier to look forstd::
if you need to know which components from the standard library you're using.Whenever you use control flow statements like
if
, try to always use curly braces, even when you have a single statement following them. It will make it easier to add more statements (debug statements for example) and avoid problems of the Apple'sgoto fail
kind.Another good practice is to always use
nullptr
instead ofNULL
. While it doesn't matter most of the time, when it does you're glad you usednullptr
everywhere.
-
\$\begingroup\$ Those smart pointers start to make sense when you create a custom deleter. Thank you for your help ! The only thing that isn't clear is how I can use a std::bitset, wouldn't it make my code less semantic ? Doesn't it remove the possibility to name the flags ? \$\endgroup\$florian-s– florian-s2015年08月05日 20:50:42 +00:00Commented Aug 5, 2015 at 20:50
-
\$\begingroup\$ @yhoyhoj If you want to pack things, you'll need a single variable anyway. Create getters and setters for the bitset to get the small size and the semantics. \$\endgroup\$Morwenn– Morwenn2015年08月05日 20:56:34 +00:00Commented Aug 5, 2015 at 20:56