3 Sockets Program, recv() and poll()

Socket Programming

Recall the Protocol Layers. The user application is at the highest level, and a user application needs to communicate this payload with/to the server application.

Pasted image 20250122105536.png

How do we access the kernel level information? We do system calls! The kernel is really everything below our sockets API.

system call

A setup call in user space that then traps to the kernel.

They are a horribly written set of functions, but we have to live with them as they are now.

TCP Client/Server

Pasted image 20250122105747.png

The server has to run first (since the server waits for the first client to connect). We'll go through each of these functions one at a time.

accept()

accept() will traditionally block, waiting for a client.

The client on the other hand will try to connect() to the server, stopping accept()'s blocking. This is using a Handshake Protocol (Connection Oriented), specifically:

socket() Call

We already know how to do file operations:

int fd = open(...); // open() is a system-call
// if fd < 0 then there's an error. This is common for system calls. 
// stdin is 0, 1 is stdout, 2 is stderr

Here open() creates a structure in the kernel to track this file IO.

Sockets are opened to create a network connection in a similar way:

int fd = socket(...); // check for <0 on error

Here the sockets are file descriptors. socket() creates a structure in the kernel to track/manage network IO. To input and output to a socket use recv() and send() or recvfrom() or sendto().

Types of Sockets
Call
int socket(address_family, type, protocol);
Naming a Socket

If the client writes to stdout and the server writes to stdout, they don't affect each other since the fd's are machine dependent. Now, for sockets, it's the same thing. If the fd's are made, then they are local. We're going to need other information for our client to talk to our server (for each client/server instance):

  1. Protocol Family (AF_INET6)
  2. IP address of the server (since it goes across the network).
  3. Port Number (these are agreed upon beforehand, the kernel only gives one port number per socket so there are no conflicts).
    These pieces of information is called naming the socket. These are worldwide unique since once you know this information for a server you can connect to the client (and vice versa).

More on the port numbers:

Which to use?

Use bind() on the server, while on the client let the kernel choose via send().

Some ports are reserved, and just agreed upon by the client and server (see /etc/services for really common port). Usually 0-1024 are reserved. Note that bind() only needs to be called on the server.

#include <sys/socket.h>
int bind(int socket, const struct sockaddr* myaddr, socklen_t socket_len)

The structure is really ugly :( :

struct sockaddr_in6 {
	sa_family_t sin6_family; /* AF_INET6 */
	in_port_t   sin6_port;   /* port number */
	uint32_t    sin6_flowinfo; /* IPv6 flow info */
	struct in6_addr sin6_addr; /* IPv6 address */
	uint32_t    sin6_scope_id; /** Scope ID (new in 2.4) */
};

// Now NAME the socket
server.sin6_family = AF_INET6;
server.sin6_addr = in6addr_any; // wildcard machine address
server.sin6_port = htons(portNumber); // give me a random port number if portNumber = 0

Getting the Kernel Ready

How can the client know what port number to use (chosen from the server)? Namely, the server:

  1. Creates a socket() fd
  2. Configure the server socket with bind() and listen()

Assume the client has made it's own socket(). Now
3. BOTH the client and server need to call connect() and listen(); accept() (respectively), where:
1. int listen(int socket, int backlog) here queues connections to the server, so this is called ONCE to allow connections to be accepted.
2. int accept(int socket, struct sockaddr *client_addr, socklen_t *addrlen): client_addr will be filled in.
For instead the client:

Note

Since bind() doesn't get you a port number, you call getsockname() to get the port number.

Note

To translate a serverName into an IP number, use getIPAddress6().

Pasted image 20250122130717.png

Note that the stuff above listen() is run once (setup), but each client/server connection needs a connect(), accept() call.

TCP Receive

For both the client and server you call send() (client) and recv() (receive). TCP merges all the send's together, and one recv merges them all.

Note

UDP is different here. It doesn't merge these together.

The problem is that TCP doesn't respect message boundaries.

To recv() our PDU - Protocol Data Unit in TCP, we'll need our application pdu. The structure is as follows:

typedef struct app_pdu_tag {
	uint16_t len; // length of the entire PDU
} app_pdu_t;

send(); // one send
recv(sizeof(app_pdu_t)); // we receive just the header...
recv(pdu.len); // we then try to receive the data based on the length;
Aside

MSG_WAITALL
This is a flag to use on the TCP recv() to:

block and recv() only length number of bytes.

It's really important to meet the spec of the PDU so that the test server can connect to your client (and vice versa).

Lab 3 Program

You'll make two functions:

/**
adds the PDU length to the PDU
Does one send()
Constructs the PDU with the buffer data in the network order
Make sure the length is in host order. 
*/
int sendPDU(int clientSocket, uint8_t *dataBuffer, int lengthOfData);

/**
recv(sk, &PDU_len, 2, MSG_WAITALL); 
recv(sk, buffer+2, PDU_len-2, MSG_WAITALL);
You need to convert the length 
Note that the PDU length is in network order, so convert it before using it. 
*/
int recvPDU(int socketNumber, uint8_t * dataBuffer, int bufferSize);

Detecting a Closed Socket when recv()-ing

How do we know if the other end closed the connection? Check the value of recv(). There's two cases:

send() and SIGPIPE (broken pipe)

Our safeRecv() handles this, so often it's because an error value is not used, so check that.

TCP Connection

Pasted image 20250122140126.png

Note

You use the poll() to know which socket to deal with, either:

  • The main server socket
  • One of the client sockets
    To deal with accept(), recv() blocking, we use the poll() function to know when it's okay to call these.
    On the client we call poll() using STDIN_FIILENO and the socket to the server.

There's two types of problems we need to solve for the program:

  1. Server working with many clients at the same time (program 2)
  2. Waiting to recv() for a specific quanta

poll() works on file descriptors:

Note

If you poll() and do a recv() on the socket and get 0 bytes, then that indicates a closed connection, so you need to close up!