Home > Back-end >  Server-sent events using POSIX Sockets API
Server-sent events using POSIX Sockets API

Time:08-27

I'm writing messenger app. Server using C, client - JavaScript/React. To send a message from user A to user B - I use Server-sent events. After the client logs in, it sends a POST /login request to the server. The server stores the client's socket descriptor in a hash table. When client A wants to send a message to client B, the socket descriptor of B will be obtained from the hash table on the server. After that, this socket will be used to send the message over the previously established HTTP connection.

POST /login handler:

void POST_login(void* data) {
    task_args* args = (task_args*)data;
    /*
      HTTP and JSON parsing here
    */
    //map client_socket in hash table by client username
    ht_insert(args->online_users, username, args->client_socket);
    
    //setup HTTP response
    http response;
    http_set_status_code(&response, "200 OK");
    http_set_connection_status(&response, "Keep-Alive");
    http_set_content_type(&response, "text/event-stream");
    http_set_body(&response, "data: ok_1\r\n\r\n");
    /*
     response looks like this:

     HTTP/1.1 200 OK\r\n
     Connection: Keep-Alive\r\n
     Content-Type: text/event-stream\r\n\r\n
     data: ok_1\r\n\r\n
    */
    //send data to client
    http_response(&response, args->client_socket);    
  
    //close(args->client_socket);

    /*
     memory free here
    */
}

The code above seems work fine as the data reaches the client.

However, when client A sends a request to the server, after processing it, the server should send a message to client B, but something goes wrong.

void GET_message(void* data) {
    task_args* args = (task_args*)data;        
    /*
      HTTP and JSON parsing here
    */
    //get all the necessary information about the receiver client
    table_element receiver = ht_get_item(args->online_users, "teleportboy");
    
    //setup HTTP response
    http response;    
    http_set_status_code(&response, "200 OK");
    http_set_connection_status(&response, "Keep-Alive");
    http_set_content_type(&response, "text/event-stream");
    http_set_body(&response, "data: ok_2\r\n\r\n");        
    /*
     response looks like this:

     HTTP/1.1 200 OK\r\n
     Connection: Keep-Alive\r\n
     Content-Type: text/event-stream\r\n\r\n
     data: ok_1\r\n\r\n
    */
 
    //broken pipe occurs on second http_response call
    for (int i = 0; i < 10; i  ) {
        http_response(&response, receiver.user.socket);
    }

    /*
     memory free here and etc...
    */        
}

It seems that this code should work just as well as the previous one (if it actually works properly). But this time, although the data is sent to the client, the EventSource API does not work this time. And if I try to send the data again, then a broken pipe error occurs.

http_response function code listing:

void http_response(http* response, socket_descriptor client_socket) {
    char* buffer = calloc(4096, sizeof(char));

    int length = 0;
    length  = sprintf(buffer,          "HTTP/1.1 %s\r\n", response->status_code);
    length  = sprintf(buffer   length, "Connection: %s\r\n", response->connection_status);
    length  = sprintf(buffer   length, "Content-Type: %s\r\n\r\n", response->content_type);
    length  = sprintf(buffer   length, "%s", response->body);
    
    printf("in response:\n%s", buffer);

    int bytes_sent = 0;
    while (bytes_sent < length) {
        bytes_sent = send(client_socket, buffer, length, 0);
        if (bytes_sent == -1) {
            printf("oops\n");
            printf("response length %d bytes sent %d\n", strlen(buffer), bytes_sent);
            return;
        }
    }

    printf("response length %d bytes sent %d\n", strlen(buffer), bytes_sent);

    free(buffer);
}

To make Event Source requests on the client side, I used the @microsoft/fetch-event-source API. But I think this does not change the situation, since using the default EventSource API - still the same error.

Client side code of making Event Source request:

  const onSubmit = (event) => {
    console.log("clicked!");
    event.preventDefault();

    const formData = new FormData(event.target.parentNode);
    const formProps = Object.fromEntries(formData);
    console.log(formProps);
    
    fetchEventSource('/login', {
      method: "POST",
      headers: {
        "Content-Type": "application/json"
      },
      body: JSON.stringify(formProps),
      onmessage(msg) {
        console.log(msg.data);
      }
    });

The whole strangeness of the situation lies in the fact that no matter how I change the HTTP response in the GET_message handler, the outcome is always the same. The message does not reach the client (maybe it does, but the EventSource onmessage callback does not process it). Well, when you try to send a message again - broken pipe.

And the situation in the POST_login handler is quite the opposite. All messages from there reach and all are processed on the client side. Even if in this case you do not immediately answer the client with anything, but simply save his socket in memory, then sending data to him later in the GET_message handler will not work. It's all the same problem.

Thus, I hope I have clearly described the problem.

I will attach a link to the entire code https://github.com/teleportboy/talkie-smalkie/tree/master/server True, there is such a mess, but all the logic is concentrated in the files task_executors.c & main.c

CodePudding user response:

\r\n\r\n does not terminate the body. An empty line after the headers is important: it tells the client that there are no more headers. Inside the body it does not tell anything.

There are three ways to tell the client where the body ends:

  1. Closing connection. When the client sees the closed connection, it knows that it read the entire body. Not your case obviously.

  2. Content-length: header. When client parses this header, it knows how many octets to read. You don't do it.

  3. In HTTP 1.1, you may send the body in chunks (sending a Transfer-Encoding: chunked header. The chunk of length 0 is the last one. You don't do it either.

In other words, the client has no way to know where the transfer ends. I don't the details of fetch-event-source API, but it is absolutely clear that it never believes that it received an entire message, and therefore never calls the callback. It may time out, or it may outright reject your response as malformed, and either way close the socket.

To fix it, implement (2) or (3).

  • Related