Ethernet/WLAN/PPP etc. -> Internet Protocol -> TCP/DNS/HTTP etc.
Everything over IP, IP over everything.
IP Address
- Each host (not just end hosts) is identified by one or more IP addresses
- Usually as many IP addresses as network adapters
- End hosts usually have one
- Exceptions e.g. laptops with Wi-Fi and ethernet
- Both identifies the host and helps with routing
- DNS used to translate human readable host names to IPv4 addresses
Representation
- 32-bit wide address
- Worldwide unique (with caveats)
- Dotted-decimal notation: 192.168.1.1
IP Service - Best Effort
The basic IP service is packet delivery. It is:
- Stateless: no connection/shared state
- Unacknowledged: the IP service does not send acknowledgement that a package has been received
- Unreliable: no retransmissions on the IP level
- Unordered: does not guarantee in-sequence delivery
UDP and TCP
-
UDP: user datagram protocol
-
TCP: transmission control protocols
-
Both operate on top of IPv4: they generate packets and use that as the IPv4 packet payload: protocol layering
-
Both are unicast: between a pair of IP addresses
-
They add their own addressing capabilities: port numbers
- IPv4 address refers to end host; port number refers to particular application running on the end host
UDP
IPv4, plus port numbers. It is:
- Connectionless
- Unacknowledged
- Unreliable
- Unordered
TCP
TCP allows safe and reliable transfer over the internet. It is:
- Connection-oriented
- Different from a connection in a circuit-switched network: resources are NOT reserved
- Goes through the same three phases of a connection: setup, use and teardown
- Reliable and in-sequence
- Byte-stream oriented
- Full-duplex
Ports
- 16-bit port number
- A process can allocate one or more ports
- One port is allocated to at most one process
- Some ports, well-known ports, are allocated to well-known applications
- 22: SSH; 25: SMTP; 53: DNS; 80: HTTP; 443: HTTPS
- This is convention, not a rule
- Ports 1023 and below are usually reserved for system services
- Admin permissions required in Linux
Processes and blocking socket calls
An OS creates an abstraction for processes to make processes think that they have their own exclusive processor and allocate memory exclusively (memory not accessible by other processes). Below the abstraction, time-sharing is used. A process can be:
- Running: running on the processor
- A process will be given some quantum of time to run, after which it will be swapped out with another runnable process
- Runnable: ready to run on the processor, but not currently running
- Blocked: waiting for external input e.g. file from disk, socket
- Blocked processes use no CPU resources
- Much better than busy-looping (polling)
- NB: of one thread calls a blocking call, the whole process will be blocked
Socket API
- Follows the Unix philosophy that (almost) everything is a file (or acts like a file)
- One socket is bound to exactly one port
- Within the same process, multiple sockets can be bound to the same port
- Associated with its underlying protocol (e.g. UDP or TCP)
- Application does not need to know the details of the abstraction, but needs to specify which one to use
- Associated with buffers - decouples the application from the underlying data protocol
- Receiver’s TCP/UDP entity places incoming data into the receiver buffer; process will
read()from the buffer at its own discretion - Transmitter
write()s data into the transmit buffer; TCP/UDP sends it at its own discretion
- Receiver’s TCP/UDP entity places incoming data into the receiver buffer; process will
- When writing to a socket:
- UDP: data encapsulated in UDP datagram and transferred - one write, one datagram
- TCP: data buffered and may be combined with data from previous/future
write()s before being transmitted - Successful write means data was successfully put into the buffer
Client/Server Paradigm
Client applications must:
- Know the server IP address and port number
- Have its own IP address and port number
- Have a socket bound to these
- Through the socket, it can initiate contact with the server
Server applications must:
- Have an IP address and port number
- Must be known to clients
- Must have a socket bound to these
- Accept service requests from clients and respond to them
The client and server can run on the same machine: 127.0.0.1 (localhost) maps a device to itself.
Socket Types
Differ by OS, but these are universal:
- Stream socket: TCP (usually)
- Reliable, in-sequence transmission of a byte stream
- The application does know what protocol is being used under the hood - usually TCP, but not guaranteed
- If delivery fails (for a prolonged time), the sending application is notified
- i.e. receiver not responding
- A
write()does not guarantee a packet - the underlying protocol will determine on its own if it should send a packet or combine it with data from otherwrite()s - Receiver cannot detect the boundary between
write()s - record boundaries not preserved
- Datagram socket: UDP (usually)
- Does not guarantee reliable or in-sequence delivery
- If delivery fails, sending application is not notified
- One
write()of x bytes, one UDP packet - A
read()will return a block of at most x bytes (when no bits are lost) - Record boundaries are preserved
Example Workflow: TCP client
socket()- Creates a socket, allocates resources (buffer, socket handle etc.)
- Initially assigned to a random, unused port number
- Can fail (e.g. lack of buffer space, not enough handles, need root)
connect()- Specify IP address and port number of server
- (Tries to) establish connection with the server
read()/write()- Or
sendto()/recvfrom()- additional parameters - Reads from/sends data to the server
- Or
close- Closes the connection; frees up resources
Example workflow: TCP Server
socket()creates socketbind()binds to a particular port number and IPlisten()declares the willingness to accept incoming connections and allocate resources for incoming connection requests- Buffer size specified
accept()- Accept an incoming connection request
- Takes the oldest connection request from the queue
- Generates a new socket with the same port number (as the socket
listen()is using) - Allows a separate thread to process it for parallel processing
- On the new socket:
read(),write()close()
close()on the initial socket - to stop accepting all connections
Example workflow: UDP client
socket()rcvfrom(),sendto()sendtorequires you to explicitly specify the receiver addressrcvfromgives you the IP address of the sender as well as the data
close()connect(), thenread()/write()can also be used.connect()gives you a default sender and receiver IP address, and can be called multiple times.
Example workflow: UDP server
socket()bind()rcvfrom(),sendto()close()
Socket API Functions
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
Returns an integer. If negative, error, and sets the errno variable.
Socket (non-blocking)
- Creates a new socket structure
- Allocates resources e.g. socket buffer
- Assigns to random un-used port number
- Returns file descriptor representing the socket
int socket(int domain, int type, int protocol);.
domain: type of networking technology e.g.AE_INET(IPv4),AF_NET6(IPv6)- type: type of socket e.g.
SOCK_STREAM(TCP-like),SOCK_DGRAM(UDP-like),SOCK_RAW - protocol: protocol for given socket protocol. Usually only one sensible option. If in doubt, set to
0
If successful, returns file descriptor (positive integer). If fails, returns -1 and sets errno.
Bind (non-blocking)
- Links a socket to a particular IP address/port number/address family combination
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockfd: socket descriptor (returned from socket())addr: pointer to struct containing address information. If IPv4, need to castsockaddr_intosockaddraddrlen: length of address
Tries to be general, so is big enough to fit in any type of address. sa_family is the same value as used in protocol.
struct sockaddr {
sa_family_t sa_family;
char sa_data[14];
}
Need to cast sockaddr_in to a char array.
struct sockaddr_in {
short sin_family;
unsigned short sin_port;
struct in_addr sin_addr;
char sin_zero[8];
};
struct in_addr {
unsigned long s_addr;
};
Listen (non-blocking)
int listen(int sockfd, int backlog);
Declares that you are willing to accept incoming steams (SOCK_STREAM or SOCK_SEQPACKET connections), and allocates resources (queue).
backlog: how long the queue can be
Accept (blocking)
- Waits for incoming connection requests
- Return a new socket (with same port number) over which you can talk to the sender, and returns the socket descriptor
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
addr: pointer to address struct. It will fill in the address details of the sender
Connect (blocking)
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- For steam sockets: request establishment of connection with server
- For datagram sockets: used to specify default receiver for datagrams when
send()orwrite()used
Fate of connection setup must be known.
Blocks until the fate of the connection setup is known:
- For stream sockets, the connection must be accepted using
acceptand that message must be received - For datagram sockets, there is no confirmation of setup or anything, so it is non-blocking
Read
ssize_t recv(int sockfd, void *buf, size_t len, int flags); ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
ssize_t read(int fd, void *buf, size_t count);
read: prepare buffer area, size of the buffer area, and pass it to read, which will fill it up with data.
recvfrom: the caller also provides memory for address structure and the function fills it in with the address/port/address family of the host.
Read will be blocking if there is no data (unless a flag is set using recv/recvfrom, in which case it will return an error code).
Write
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
ssize_t write(int fd, const void *buf, size_t count);
write: pass it a buffer of data to send, and the number of bytes to send.
If the underlying socket buffer is too full, it will be blocking (unless the flag is set, which will cause it to return an error code).
send/write require the addressee to be specified using connect.
Close
int close(int fd);
Frees up socket resources.
Endianness
For certain data types (e.g. 16 bit unsigned port number), the packet header in particular, there must be a canonical representation for data being transmitted - the network byte order. Hosts must convert between their own internal representation (host byte order) and network representation.
The canonical representation for the internet is big-endian (most significant byte at lowest memory address). Intel hates it.
Host (h) to network (n) conversion, or vice versa, available as helper functions for 16/32 bit integers.
#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong); // host to network long (32 bits)
uint16_t htons(uint16_t hostshort); // host to network short (16 bits)
uint32_t ntohl(uint32_t netlong); // network to host long
uint16_t ntohs(uint16_t netshort); // network to host short
Helper functions for address manipulation.
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int inet_aton(const char *cp, struct in_addr *inp);
in_addr_t inet_addr(const char *cp);
in_addr_t inet_network(const char *cp);
char *inet_ntoa(struct in_addr in);
struct in_addr inet_makeaddr(int net, int host);
in_addr_t inet_lnaof(struct in_addr in);
in_addr_t inet_netof(struct in_addr in);
TCP Client Example
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdint.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
int main() {
char[] server_name = "localhost";
uint16_t portno = 80;
struct sockaddr_in_serv_addr; // Address structure for server
struct hostent *server; // Result of DNS resolver
char[] buffer[256] = "A message to send"; // Buffer for data to send
// Create socket with protocol 0 (default) for given address family - TCP
int sockfd = socket(AE_INET, SOCK_STREAM, 0);
if (sockfd < 0) {
perror("ERROR opening socket");
exit(1);
}
server = gethostname(server_name); // DNS resolution
if (server == NULL) {
perror("ERROR no such host");
close(sockfd); // Not needed, but stylistically bad to not close the socket
exit(0);
}
bzero((char*) &serv_addr, sizeof(serv_addr)); // Zero out the struct
serv_addr.sin_family = AE_NET;
// Copy IP address from DNS response
bcopy((char*)server->h_addr,
(char*)&serv_addr.sin_addr.s_addr,
server->h_length
);
// Set port number
serv_addr.sin_port = htons(portno);
if (connect(sockfd, (struct sockaddr*) &serv_addr, sizeof(serv_addr)) < 0) {
perror("ERROR connecting");
close(sockfd);
exit(1);
}
int n = write(sockfd, buffer, strlen(buffer));
if (n < 0) {
perror("ERROR writing to socket");
exit(1);
}
bzero(buffer, 256); // Zero out buffer; wait for response
n = read(sockfd, buffer, 255);
if (n < 0) {
perror("ERROR reading from socket");
exit(1);
}
printf("%s\n", buffer);
}
TCP Server Example
int main() {
int portno = 80;
int sockfd = socket(AE_INET, SOCK_STREAM, 0);
struct sockaddr_in serv_addr;
struct sockaddr_in cli_addr;
if (sockfd < 0) {
perror("ERROR opening socket");
exit(1);
}
bzero((char *) &serv_addr, sizeof(serv_addr));
serv_addr.sin_family = IENET;
serv_addr.sin_addr.s_addr = INADDR_ANY; // Bound to all local interfaces. Otherwise, use the IP address of a specific interface
serv_addr.sin_port = htons(portno);
if (bind(sockfd, (struct sockaddr *) &serv_addr, sizeof(serv_addr)) < 0) {
eprint("ERROR on binding");
exit(1);
}
listen(sockfd, 5); // Allow maximum of 5 queued onnections
clilen = sizeof(cli_addr);
// Wait (blocking) for incoming connection request with `accept`
int newsockfd = accept(sockfd, (struct sockaddr *) &cli_addr, &clilen);
if (newsockfd < 0) {
eprint("ERROR on accept");
exit(1);
}
bzero(buffer, 256);
n = read(newsockfd, buffer, 255);
if (n < 0) {
eprint("ERROR reading from socket");
exit(1);
}
printf("%s\n", buffer);
n = write(newsocketfd, "Message received", 17);
if (n < 0) {
eprint("ERROR writing to socket");
exit(0);
}
close(newsockfd);
close(sockfd);
return 0;
}