meta data for this page

TCP #1

This task is about TCP (rfc).

TCP is connection oriented so before you can send data a connection between peers must be established. The usual process for server:

socket()        // Create new (listening) socket
bind()          // Bind the socket to some address+port
listen()        // Start listening for new connections
accept()        // Accept new connection and get the connection socket
send() / recv() // Send and/or receive data over the connection socket
close()         // Close the socket

And for the client:

socket()        // Create new socket
connect()       // Connect to server
send() / recv() // Send and/or receive data
close()         // Close the socket

Task for week 44

Use the tcpexample as basis.

1. The function listen()

  • Why you cannot use the same socket for listen() and for receiving data from the clients?

  • Answer: The listening socket (connection-mode socket) is for new, incoming connections, which is bound to the address:port of the server. After the server has accepted a new client a new socket for communication between these two parties is created.

  • How many clients you can serve, if you have used listen() with following:
    int listen_rval = listen(socket,10);

  • Answer: It does not limit the client amount. It only sets the socket to listening mode (which has to be bound to an address:port). The 10 only states that your incoming connection queue (backlog) is max. 10 clients – the amount of clients that are awaiting for accept()ing the connection.

2. Which one of the following is best practice for the server? Explain.

  1. After accepting a new client (with accept()) try to immediately read data from the connection socket.
  2. Accept the client, add the client to the lists and wait for something to arrive (using select()).
  3. Accept the client, use non-blocking socket to check if there is data and read it immediately, if not, remove the client.

  • Answers:
    1. Here you'd have to use select() or non-blocking socket in order to avoid potential denial-of-service attack (the client connects, but does not send anything). If you can fully rely to your clients this can be used – with reservations, since connection latencies can cause problems and make the server process delayed since you'd have to wait for the time the data is being transferred (unless threads are used).
    2. Does not block in any case, any data that is sent by the client is reacted (almost) immediately. Requires the use of the return value of the select – it returns the amount of sockets/fd's that have data incoming - so you could react to multiple clients at once, and stop when returned amount of sockets have been processed (helps with large lists). Also the timeout value can be changed by the select() and the “remaining” time can be utilized for other purposes.
    3. Makes it possible to reduce resource consumption at the server, clients attempting ddos are removed immediately. If the clients have lagged connection with high latencies it might be possible that legitimate clients are removed because of packet delays if you are not waiting for long enough. Usually with non-blocking sockets you just check if there is anything and return right after, either with 0 or more bytes of data. Can be done also with select() by setting both fields of struct timeval to 0.

You have following packet structure:

uint8_t uint16_t charstring
PPID LENGTH FIRSTNAME

The content of the packet is:

  • PPID = 42
  • LENGTH = packet length
  • FIRSTNAME = your first name

3. Send the data using the tcpexample (and receive!) in either direction:

  1. Sending all at once in one buffer
    • What things have to be noted here?
    • How the receiver should read this?

  • Answer:
    #define NAME_MAX 128
    #define BUF_MAX 1024
    .
    .
    .
          /* Filling the packet with example data and sending it */
          uint8_t ppid = 42;
          char firstname[NAME_MAX] = { "Name" };
     
          /* Calculate packet length */
          uint16_t length = sizeof(ppid) + sizeof(length) + strlen(firstname);
     
          /* Erase packet contents and encode new packet */
          memset(&dgram, 0, BUF_MAX);
          /* Keep track of position in character buffer */
          unsigned int offset = 0;
          /* Pack ppid and update offset */
          *(uint8_t*)&dgram[0] = ppid;
          offset += sizeof(uint8_t);
          /* Pack packet length and update offset */
          *(uint16_t*)&dgram[offset] = htons(length);
          offset += sizeof(uint16_t);
          /* Pack the first name */
          memcpy(&dgram[offset], &firstname, NAME_MAX);
     
          /* Send all */
          int sent = 0;
          int total_sent = 0;
          /* Try to send all - if all is not sent, increment the position by sent data and continue
             from that position until all is sent */
          while((sent = send(socket,&dgram[total_sent],length-total_sent,0)) < length-total_sent) {
            total_sent += sent;
          }
  • Remember to put numbers in network order if they exceed 2 bytes, usually for length: 32/16 bit integers. The terminating character for charstring – it can be omitted from the packet but the length variable should define the length of the whole packet.
    • It might be tempting to define a struct and send – don't! The paddings have to be defined carefully and it might work in some cases without padding but cause problems in another. The packing is referred as serialization – you change the formats for transferring data over network.
  • Reading:
    • Read a byte, if the PPID is valid, read 2 more bytes that define the length and then read the length which defines the packet boundary for that packet.
    • Or alternatively you could try to read the whole header (3 bytes) and after it read the rest.
    • Or use a large buffer for reading and read all that is in the receive buffer. Here you have to utilize the length more carefully, since multiple packets can be received at once. You cannot expect that there is just only one packet received, Nagle algorithm can combine smaller packets into one. First check the first packet for validity and length, then process data and move to next, if the next packet is received in full. If not use message buffers and read the remaining later - note here that one big buffer for all clients is not a good approach.
      • Define separate buffers for each client, e.g., use a struct
        typedef struct t_clientdata {
          int id;
          int socket;
          struct sockaddr_storage address;
          char readbuffer[BUF_MAX];
          int readbuffer_position;
          char sendbuffer[BUF_MAX];
          int sendbuffer_position;
          struct t_clientdata *next; // Pointer to next to be used with, e.g., linked list
        } clientdata;

In code you can create new structs by using the clientdata without struct.

  • Remember to be careful with partial reads and to calculate correct packet lengths when sending.
  • It is also possible to check if there is enough data to be read (especially if all of your packets are expected to be of certain size) using:
    //MSG_PEEK - "Peeks at an incoming message. The data is treated as unread and the next recv() or similar function shall still return this data."
    int bytes_in_buffer = recv(client->socket, 
                               &(client->readbuffer[client->readbuffer_position]),
                               BUF_MAX - client->readbuffer_position,
                               MSG_PEEK);

  1. Send the data in pieces (first PPID, then LENGTH, and last the FIRSTNAME)
    • Compared to previous approach, would the receiver have to change the code for adapting to this approach?

  • Answer:
            uint8_t ppid = 42;
    	char firstname[NAME_MAX] = {0};
    	uint16_t length = 0;
            int sent = 0;
     
            length = htons(sizeof(ppid) + sizeof(length) + strlen(firstname));
            strncpy(&firstname,"Name",NAME_MAX-1); // Keeps a terminating null
     
    	if((sent = send(sock, &ppid, sizeof(ppid), 0)) < sizeof(ppid)) do_handle_error();
    	if((sent = send(sock, &length, sizeof(length), 0)) < sizeof(length)) do_handle_error();
    	if((sent = send(sock, &firstname[0], strlen(firstname), 0)) < strlen(firstname)) do_handle_error();
    • Since TCP is stream oriented there is no need for the server to change the code.

4. What if the server has multiple clients and the packet sizes are small. Clients send packets with high frequency to the server. Try to implement this into tcpexample using, e.g., data packets that are 10bytes each with 1 or 2 byte length variation containing different numbers in character format.

  • How should the server take care of the packets?
  • How about partial sending and receiving? What must be noted?
  • What additional measures are required from the server?

Tools for the task

socket(), bind(), listen(), accept(), connect(), recv(), send(), select(), htons(), ntohs(), htonl(), ntohl(), shutdown(), close()

getaddrinfo(), memcpy(), memset(), strcpy(), strncpy(), strlen(), sizeof()

Additional help