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.
How do we access the kernel level information? We do system calls! The kernel is really everything below our sockets API.
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
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:
- Setup uses
connect()/accept()
- Use uses
send()/recv()
- Teardown uses
close()
on both ends
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
- TCP
- Is Handshake Protocol (Connection Oriented)
- TCP is a reliable service (all data in order, error free)
- It uses a byte stream in order to send data
- Socket Programming:
SOCK_STREAM
- UDP:
- Is a Connection-Less (Protocol)
- UDP is a best effort service (drop it in and forget)
- It uses individual message transfers to send data
- Socket Programming:
SOCK_DGRAM
Call
int socket(address_family, type, protocol);
address_family
- Use
AF_INET6
: supports IPv6 and IPv4 (use this one over the other) - Use
AF_INET4
: supports just IPv4
- Use
- Type of communications
- TCP use
SOCK_STREAM
- UDP use
SOCK_DGRAM
- TCP use
- Protocol = 0 (means use default)
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):
- Protocol Family (
AF_INET6
) - IP address of the server (since it goes across the network).
- 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:
- Are maintained by the protocol stack (OS)
- All sockets (client/server) will have a port number
To obtain it you do: - Explicitly:
bind()
- Implicitly: if you
send()
on a socket without a port number, then one is assigned by the kernel.
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:
- Creates a
socket()
fd
- Configure the server socket with
bind()
andlisten()
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:
int connect(int socket, const struct sockaddr *addr, socklen_t addrlen)
- Here
addr
is the server's naming (family, IP, port number).- Here
addrlen
is justsizeof(struct sockaddr)
The port number (for the client) is gotten as a runtime argument, ie:argv[2]
.
- Here
- Here
Since bind()
doesn't get you a port number, you call getsockname()
to get the port number.
To translate a serverName
into an IP number, use getIPAddress6()
.
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.
UDP is different here. It doesn't merge these together.
The problem is that TCP doesn't respect message boundaries.
- If the client sends 100 bytes then 200 bytes, then the server sees one 300 byte write. It cannot know it was split between two sends.
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;
MSG_WAITALL
This is a flag to use on the TCP recv()
to:
block and
recv()
onlylength
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:
- Returns 0 (only Apple does this lol)
- Returns -1 and
errno = ECONNRESET
.
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
You use the poll()
to know which socket to deal with, either:
- The main server socket
- One of the client sockets
To deal withaccept(), recv()
blocking, we use thepoll()
function to know when it's okay to call these.
On the client we callpoll()
usingSTDIN_FIILENO
and the socket to the server.
There's two types of problems we need to solve for the program:
- Server working with many clients at the same time (program 2)
- Waiting to
recv()
for a specific quanta
poll()
works on file descriptors:
- add to the set each socket (within main server socket)
- poll on the set of sockets
- on return check which socket called the
poll()
to return
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!