1

I'm using libev + non-blocking sockets to send a request to a server. I'm using Keep Alive because I need to send future requests to the destination over this same connection.

Behavior Run the program and it fetches the URL and logs to console, as expected. After doing this, wait and don't push ctrl+c to exit the program.

Expected App should stay open because event loop is waiting for future responses but should not console log anything after the initial response.

Actual Leave the app running. After 30+ seconds, it will start to console log the same response over and over and over again without end.

Question Why is libev calling my callback (example_cb) repeatedly when no new request was sent and no new response data was received? How can I fix this?

#include <ev.h>
#include <stdio.h>
#include <iostream>
#include <ctype.h>
#include <cstring>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <fcntl.h>
#include <netdb.h>
#include <netinet/in.h>
#include <unistd.h>
#include <sstream>
#include <fstream>
#include <string>

using namespace std;

void sendRequest(int sockfd)
{
  puts("------");
  puts("sendRequest() was called");
  stringstream ss;
  ss << "GET /posts/11 HTTP/1.1\r\n"
      << "Host: jsonplaceholder.typicode.com\r\n"
      << "Accept: application/json\r\n"
      << "\r\n";
  string request = ss.str();

  if (send(sockfd, request.c_str(), request.length(), 0) != (int)request.length()) {
    cout << "Error sending request." << endl;
    exit(1);
  }
  cout << "Request sent. No err occured." << endl;
}

static void delay_cb(EV_P_ ev_timer *w, int revents)
{
  puts("------");
  puts("delay_cb() was called");
  sendRequest(3);
}


static void example_cb(EV_P_ ev_io *w, int revents)
{
  puts("------");
  puts("example_cb() was called");
  int sockfd = 3;

  size_t len = 80*1024, nparsed; // response must be <= 80 Kb
  char buf[len];
  ssize_t recved;

  recved = recv(sockfd, &buf, len, 0);

  if (recved < 0) {
    perror("recved was <1");
  }

  // don't process keep alives
  if (buf[0] != '\0') {
    std::cout << buf << std::endl;
  }

  // clear buf
  buf[0] = '\0';
  std::cout << "buf after clear attempt: " << buf << std::endl;
}

int example_request()
{
  std::string hostname = "jsonplaceholder.typicode.com";
  int PORT = 80;

  struct sockaddr_in client;
  struct hostent * host = gethostbyname(hostname.c_str());
  if (host == NULL || host->h_addr == NULL) {
    cout << "Error retrieving DNS information." << endl;
    exit(1);
  }

  bzero(&client, sizeof(client));
  client.sin_family = AF_INET;
  client.sin_port = htons( PORT );
  memcpy(&client.sin_addr, host->h_addr, host->h_length);

  // create a socket
  int sockfd = socket(PF_INET, SOCK_STREAM, 0);
  if (sockfd < 0) {
    cout << "Error creating socket." << endl;
    exit(1);
  }
  cout << "Socket created" << endl;

  // enable keep alive
  int val = 1;
  setsockopt(sockfd, SOL_SOCKET, SO_KEEPALIVE, &val, sizeof val);

  if (connect(sockfd, (struct sockaddr *)&client, sizeof(client)) < 0) {
    close(sockfd);
    cout << "Could not connect" << endl;
    exit(1);
  }
  cout << "Socket connected" << endl;

  // make non-blocking
  int status = fcntl(sockfd, F_SETFL, fcntl(sockfd, F_GETFL, 0) | O_NONBLOCK);
  if (status == -1) {
    perror("ERROR making socket non-blocking");
  }
  std::cout << "Socket set to non-blocking" << std::endl;

  std::cout << "Sockfd is: " << sockfd << std::endl;
  return sockfd;
}

int main(void)
{
  // establish socket connection
  int sockfd = example_request();

  struct ev_loop *loop = EV_DEFAULT;

  ev_io example_watcher;
  ev_io_init(&example_watcher, example_cb, sockfd, EV_READ);
  ev_io_start(loop, &example_watcher);

  // used to send the request 2 sec later
  ev_timer delay_watcher;
  ev_timer_init(&delay_watcher, delay_cb, 2, 0.0);
  ev_timer_start(loop, &delay_watcher);

  ev_run(loop, 0);

  return 0;
}

Edit: Code updated with suggestions from comments

bwool84
  • 13
  • 3
  • Maybe it is because you are receiving keepalives of 0 length and you dumping `buf` (potentially filled with garbage) to `cout` as is, without putting terminating null at appropriate position or anyhow caring about size of valid data stored there. Also why `int sockfd = 5;`? You can not manually assign random numbers to socket descriptors just like that. – user7860670 Feb 15 '18 at 11:29
  • Sockfd is 5 just because that's what it is in my minimal test. It won't be hard coded later – bwool84 Feb 15 '18 at 11:45
  • You are adding a watcher for a file descriptor that isn't even open yet. Open the file (socket), and _then_ add the watcher. – davmac Feb 15 '18 at 12:15
  • @vtt Thanks. I updated `example_cb` to set the buf to `\0` after using the data and added a check for '\0' to determine if real data was received; this lets me process real data properly. Your keep alive theory seems plausible, however example_cb is called roughly 100x/second which seems more frequently than keep alive would be received. – bwool84 Feb 15 '18 at 12:15
  • @davmac Thanks. I updated `main()` to create the socket connection before adding watchers (and also updated my hardcoded sockfd's to 3 to keep it working after the change). But `example_cb()` is still called over and over. – bwool84 Feb 15 '18 at 12:25
  • @bwool84 Assuming you are dealing with linux then this is probably because you don't check `recved == 0` case, i.e. when client disconnects. Epoll (the default libev's backend under linux) always signals that since it marks such socket as always readable. You may want to read this as well: https://stackoverflow.com/questions/14563134/epoll-loops-on-disconnection-of-a-client What you actually have to do is close the socket on disconnect. – freakish Feb 15 '18 at 12:44
  • Thanks @freakish You're right. A return value of 0 from recv wasn't being handled and the recurring callbacks all have a value of 0 from recv. It appears the destination server was closing the connection even though I asked for it to be 'keep-alive'. Does recv continuously return 0 when that occurs until the socket is disconnected? I'm still trying to understand why the callback is continuously being called. – bwool84 Feb 15 '18 at 15:30
  • @bwool84 Socket returning 0 means that the other side closed the connection. It will never stop doing that. The callback is firing simply because the socket is ready for reading (which will return 0). So when you are in this case you have to (1) close the socket on your side (via `close` syscall on the fd) and (2) `ev_io_stop` the associated watcher. Now if you want to continue then you have to create new socket and restart everything. Some monitoring is adviced here. – freakish Feb 15 '18 at 15:37
  • @freakish Thank you for explaining! That makes sense and I think solves the question – bwool84 Feb 15 '18 at 15:57

1 Answers1

1

The source of the problem is that you do not check recved == 0 condition which corresponds to the other side closing the connection. When that happens the OS sets the socket into "closed mode" which (at least under linux) is always ready for reading and subsequent calls to recv will always return 0.

So what you need to do is to check for that condition, call close(fd); on the file descriptor (possibly with shutdown before) and ev_io_stop on the associated watcher. If you wish to continue at that point then you have to open a new socket and eo_io_start new watcher.

freakish
  • 54,167
  • 9
  • 132
  • 169