Monday, November 15, 2010

Creating a std::iostream socket class (Part 3)

So far we've covered implementation of a custom stream buffer that can be used with standard library functions. Now all that's left to go is the actual socket code itself! For more information on modern socket programming, I highly recommend Beej's Guide to Network Programming. It's simple, easy to read, and covers what you need to know as you need to know it. It's also the reference I used while implementing my socket code. :)

Connecting to a server is done in three steps: Resolve the address, create a socket, and connect the socket to the address. Resolving the address looks like this:
  addrinfo hints, *info, *cur;
  memset(&hints, 0, sizeof(hints));
  hints.ai_family = AF_UNSPEC;
  hints.ai_socktype = SOCK_STREAM;
  int ret = getaddrinfo(host.c_str(), port.c_str(), &hints, &info);
  if (ret != 0) handle_error();


Once we've resolved the address, we create a socket. Since getaddrinfo() returns a linked list of address results, we need to find one that has an address type we can connect to. This way, it will automatically use IPv6 if that's all that's available. Once we have a connected socket we can free the returned address info.
  for (cur = info; cur != NULL && m_data->socket == -1; cur = cur->ai_next) {
    m_data->socket = socket(cur->ai_family,
      cur->ai_socktype, cur->ai_protocol);
    if (m_data->socket != -1) {
      // we can bind via this protocol, can we connect?
      if (::connect(m_data->socket, cur->ai_addr, cur->ai_addrlen) == -1) {
        ::close(m_data->socket);
        m_data->socket = -1;
      } else {
        m_data->remotehost = host + ":" + port;
      }
    }
  }
  freeaddrinfo(info);


To read from the socket, use either read() (which works with all file descriptors) or recv() which also takes socket-specific flags. Note that by default, either will block if there's nothing to be read yet.
  int num = recv(m_data->socket, m_data->buffer, BUFSIZE, 0);
  if (num <= 0) handle_socket_closed();


To check whether there's anything to read on a socket, use the select() function:
  timeval waittime = { 0, 0 };
  fd_set readset;
  FD_ZERO(&readset);
  FD_SET(m_data->socket, &readset);
  select(m_data->socket+1, &readset, NULL, NULL, &waittime);
  if (FD_ISSET(m_data->socket, &readset)) read_socket_data();


If you want to listen for incoming network connections the setup is slightly different. We use a 'listener socket' which listens on a given port, and then when a connection attempt is made, a call to accept() will return another socket which is connected to the remote client.

To listen:
  // bind to the requested port
  addrinfo hints, *info, *cur;
  memset(&hints, 0, sizeof(hints));
  hints.ai_family = AF_UNSPEC;
  hints.ai_socktype = SOCK_STREAM;
  hints.ai_flags = AI_PASSIVE;
  int r;
  if ((r = getaddrinfo(NULL, port.c_str(), &hints, &info)) != 0) return false;

  // try to get a socket to bind to the port
  for (cur = info; cur != NULL && m_data->socket == -1; cur = cur->ai_next) {
    m_data->socket = socket(cur->ai_family,
      cur->ai_socktype, cur->ai_protocol);
    if (m_data->socket != -1) {
      // insert lame joke about rings here
      if (bind(m_data->socket, cur->ai_addr, cur->ai_addrlen) == -1)
        close();
    }
    }
  freeaddrinfo(info);

  // if we have a socket, listen on it
  listen(m_data->socket, m_data->backlog);

  // accept the incoming connection
  sockaddr_storage addr;
  socklen_t addrlen = sizeof(addr);
  sock.m_data->socket = ::accept(m_data->socket, (sockaddr *)&addr, &addrlen);
  if (sock.m_data->socket == -1) return false; // fail :(


So there you go; almost everything you need to know to write socket code. And if you just want something that works, here's the full source of tcpstream.cpp and tcpstream.h. I've released it under attribution license, so feel free to use it in whatever projects you want, commercial or otherwise.

No comments:

Post a Comment