Socket Application Protocols

Created

Unix Sockets

Unix sockets: API to allow programs to communicate over networks.

  • Designed to work with many underlying protocols

A socket is a "communications endpoint" that appears to process as a file descriptor (think of a port). Important kinds of sockets:

  • server socket: used by server to accept connections from clients (not used for exchange of data)
  • client socket: used to exchange data between client and server systems.

System Calls

socket: creates an unconnected socket. bind: associate a socket with a network interface identified by a network address. This is because a system typically has multiple network interfaces. listen: (server only) makes a socket a server socket (to allow incoming connections). accept: (server only) wait for an incoming connection from a client system. This is called repeatedly. connect: (client only) initiate connection to a remote system

Socket Addresses

A Socket API is desigend to work with many underlying network technologies.

struct sockaddr is the "supertype" for all network addresses.

  • A "type" filed is at the beginning of struct to distinguish variants
  • E.g., if type field contains AF_INET, it is an IP address.

E.g., struct sockaddr_in is the "subtype" for IP addresses (IPv4)

Example

Create a Server Socket:

int create_server_socket(int port) {
	struct sockaddr_in serveraddr = {0}; // create a socket address ipv4 interface
	int ssock_fd = socket(AF_INET, SOCK_STREAM, 0); // defines for ip address family with TCP
	if (ssock_fd < 0)
		fatal("socket failed");
	
	serveraddr.sin_family = AF_INET;
	serveraddr.sin_addr.s_addr = htonl(INADDR_ANY); // "wildcard"; listen for all available network interfaces
	serveraddr.sin_port = htons((unsigned short)port); // associated with which applications we want to connect with
	
	if (bind(ssock_fd, (struct sockaddr *) &serveraddr, sizeof(serveraddr)) < 0)
		fatal("bind failed");
	
	if (listen(ssock_fd, 5) < 0) // 5 is the "backlog", defining how many requests can be queued before refusing connections
		fatal("listen failed");
	
	return ssock_fd;
}
  • Common TCP ports: 88 (HTTP), 443 (HTTPS), 22 (SSH)
  • Found in csapp.c

Wait for incoming connection:

int accept_connection(int ssock_fd, struct sockaddr_in* clientaddr) {
	unsigned clientlen = sizeof(clientaddr);
	int childfd = accept(ssock_fd, (struct sockaddr *) clientaddr, &clientlen);
	if (childfd < 0)
		fatal("accept failed");
	return childfd;
}
  • clientaddr defines client's IP address so that the server can know. Use cases: restrict which IP address that can be connect.
  • By using accept, we block all connections until a connection request arrives.

Server Loop: (terrible example, but bare minimum)

int main(int argc, char **argv) {
	char buf[256];
	int port = atoi(argv[1]);
	int ssock_fd = create_server_socket(port);
	
	while (1) {
		struct sockaddr_in clientaddr;
		int clientfd = accept_connection(ssock_fd, &clientaddr);
		ssize_t rc = read(clientfd, buf, sizeof(buf)); // read some data 
		if (rc > 0) {
			write(clientfd, buf, rc);
	}
	close(clientfd);
	}

}
  • This program reads the incoming data and sends it right back.

Testing the server: Run with:

$ gcc -Wall -o server server.c
$ ./server 30000

Test using telnet program (test text based protocols) (or use nc)

$ telnet localhost 30000
  • Port numbers < 1024 can only be used by admin users on Linux
  • localhost is the loopback network Output:
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
hey there!
hey there!
Connection closed by foreign host.

When writing network applications keep in mind:

  • reading from socket (endpoint of a TCP connection) can return fewer bytes than requested, so you generally need to call read and write in a loop.
    • rio_readn and rio_writen help with this exact problem
  • network connections can be broken (so you need to check the result of read and write, error often indicates that the connection no longer exists.)

Host Names

DNS = Domain Name Service It assigns meaningful names (e.g., ugradx.cs.jhu.edu) to network addresses (such as 128.220.224.100) getaddrinfo lookup network address for hostname.

csapp

The textbook Computer Systems: A Programmer's Perspective provides a library of convenient functions for writing network applications. It includes routines that reduce the complexity of implementing network applications in C and C++.

  • open_listenfd opens a sever socket given port name as string
  • open_clientfd simplified interface for connecting to a server by specifying host name (or address) and port.
  • rio_functions are robust I/O routines, handle looping for short reads/writes and interruptions from signals automatically.
    • rio_t: data type wrapping a file descriptor and allowing buffered input
    • rio_readn read n bytes from a rio_t.
    • rio_readlineb read a line of input from a rio_t

Application Protocols

Application Protocols determine how data is exchanged by instances of an application program.

  • Usually a server and a client
  • Another possibility: peer to peer (p2p) applications.
  • E.g., HTTP (HyperText Transport Protocol) is used by web browsers and web servers.

Synchronous: the connected peers take turns talking. Asynchronous protocols: possible, but much more complicated to implement.

Client/server protocol: client sends request, server sends response

Message format: both peers must be able to determine where each message starts and end. Each peer must also be able to determine the meaning of each recieved message.

Text-based protocols: common because they are easy to debut and reason about.

HTTP

HTTP is a synchronous client/server protocol used by web browsers, web servers, web clients, and web services.

Clients send request to server, and server sends back a response.

  • The request specifies a verb (GET, POST, PUT, etc.) and a name of a resource.

Requests and responses may have a body containing data.

  • The content type specifies what kind of data the body contains.

Example of a HTTP request:

GET /1024/768 HTTP/1.1
Host: placekitten.com
User-Agent: curl/7.58.0
Accept: */*
  • This request is sent via TCP port 80

Response sent by placekitten.com

HTTP/1.1 200 OK
Date: Wed, 13 Nov 2019 12:33:20 GMT
Content-Type: image/jpeg
Transfer-Encoding: chunked
Connection: keep-alive
Set-Cookie: __cfduid=de2a22cdd3ed939398e0a56f41ce0e4a31573648400; expires=T
Access-Control-Allow-Origin: *
Cache-Control: public, max-age=86400
Expires: Thu, 31 Dec 2020 20:00:00 GMT
CF-Cache-Status: HIT
Age: 51062
Server: cloudflare
CF-RAY: 5350c608682a957e-IAD
  • Headers were followed by a body containing 40,473 bytes of binary data.

Full Example

This example reads integer values, computes, the sum, sends sum back to client.

  • The client can send a quit message

Server Code:

int main(int argc, char *argv[]) {
	if (argc != 2) { fatal("Usage: ./arithserver <port>"); }
	
	int server_fd = open_listenfd(argv[1]);
	if (server_fd < 0) { fatal("Couldn't open server socket\n"); }

	int keep_going = 1;
	while (keep_going) {
		int client_fd = Accept(server_fd, NULL, NULL);
		if (client_fd > 0) {
			keep_going = chat_with_client(client_fd);
			close(client_fd); // close the connection
		}	
	}
	close(server_fd); // close server socket
	return 0;
}
  • It usees open_listenfd to create a server socket
  • In the main loop: we call Accept to wait for client to connect, then we call chat_with_client to read and decode request, do computation, and then send response back to cliet
  • chat_with_client can return 0 to end the loop and shut down the server

Server chat_with_client function:

int chat_with_client(int client_fd) {
	rio_t rio; int sum = 0, val;
	rio_readinitb(&rio, client_fd);
	
	// Read line from client
	char buf[1024];
	ssize_t rc = rio_readlineb(&rio, buf, sizeof(buf));
	if (rc < 0) { return 1; } // error reading data from client
	
	if (strcmp(buf, "quit\n") == 0 || strcmp(buf, "quit\r\n") == 0) {
		return 0;	
	} else {
		FILE *in = fmemopen(buf, (size_t) rc, "r");
		while (fscanf(in, "%d", &val) == 1) { sum += val; }
		fclose(in);
		snprintf(buf, sizeof(buf), "Sum is %d\n", sum);
		rio_writen(client_fd, buf, strlen(buf));
		return 1;
	}
}
  • Uses rio_t object and rio ("robust I/O") functions for I/O
  • buffered reads (e.g., rio_readlineb to read a complete input line).
  • It reads message from client, scans for integer values, and sends sum as response.
    • More relistic implementation would have a loop to allow clients to send multiple requests.

Test the server with telnet: Run with ./arithserver 40000 Test with telnet localhost 40000

Client implementation

int main(int argc, char *argv[]) {
	if (argc != 4) { fatal("Usage: ./arithclient <hostname> <port> <message>"); }
	
	int fd = open_clientfd(argv[1], argv[2]);
	if (fd < 0) { fatal("Couldn't connect to server"); }
	
	rio_writen(fd, argv[3], strlen(argv[3])); // send message to server
	rio_writen(fd, "\n", 1);
	rio_t rio;                                // read response from server
	rio_readinitb(&rio, fd);
	char buf[1000];
	ssize_t n = rio_readlineb(&rio, buf, sizeof(buf));
	if (n > 0) { // print response
		printf("Received from server: %s", buf);
	}
	close(fd);
	return 0;
}
  • open_clientfd connects to server
  • rio_writen sends data to server
  • rio_readinitb recieve response from server
    • Received data is not NUL-terminated