About The Software

Sometime during my senior year in college (1989-90 (I know I'm old)) I needed to write a client and server for an AT&T 3B20 running BSD 4.3. The World Wide Web wasn't yet world-wide, so my only guide was my personal multi-volume hard copy of the man pages.

This is not that code. That code survived on a 3.5" floppy disk that followed me around for a few years, until I needed it, revised it, and copied that code to a hard drive. That code is also not this code.

This code is the result of several iterations of periodically having to remember how to do something or other with sockets and updating my samples to use the latest techniques. Well, not quite. Since trying to remember where I put the code was taking up so much time, decided to formalize it and put it in a location that's easy to find. That code is code that I will describe here.

This isn't 1990 and there are tons of socket examples on the Wold Wide Web and there are even more copies of all of the man pages there. Before describing my code, I'll provide brief descriptions of TCP and UDP as well as clients and servers here. However, if that's not enough for you, just take advantage of your favorite search engine.


TCP (Transmission Control Protocol)

TCP is a connection oriented, reliable, byte stream protocol. So what does that mean?

Byte stream protocol means that data is made available as a stream of bytes. Other byte streams include the Linux file system. The smallest granularity of data in a byte stream is one byte, and though bytes may be written and read in groups, there is no guarantee that the receiver will receive the bytes in the same grouping that they were sent.

e.g. A sender may send six bytes grouped [0x00, 0x01, 0x02], [0x03, 0x04, 0x05] and a receiver may receive the six bytes grouped [0x00, 0x01], [0x02, 0x03, 0x04, 0x05].

In the case of TCP, being reliable means two things:

  1. If the receiver does not acknowledge the receipt of data, the sender will resend it.
  2. Even if the receiver receives data out of order, the data will be delivered to the application in order.

Connection oriented means that server must listen for and accept a connection request from a client before data may be exchanged (more on this soon). Once a connection is established a fixed data route is established between the client and the server; it lasts until either the client or the server closes its connected socket.

Each end of a TCP connection is associated with an IP address and a port number.

TCP Clients and Servers

TCP clients and TCP servers must connect to each other before exchanging data. A TCP client establishes a connection with one server port, a TCP server may accept connections from multiple clients on the same server port.

Establishing a Connection as a TCP Client

  1. Populate a struct addrinfo with information about the server that you want to connect to. At minimum:
  2. Use the getaddrinfo() command to get a linked list of potential Internet addresses matching your request.
  3. Create a socket with the socket() command on one of the addresses returned by getaddrinfo().
  4. Use the connect() command to connect the socket to the server.
  5. The socket from step 3 will be used for all communication.

Exchanging Data

Closing the Connection


Listening for Connections as a TCP Server

  1. Create a TCP/IP socket with the socket() command.
  2. Populate a struct sockaddr_in with information about the allowable incoming addresses and the port being used.
  3. Use the bind() command to bind the socket to the port specified in the struct sockaddr_in stucture.
  4. Use the listen() command to cause the socket to listen for incoming connections.

Accepting Connections as a TCP Servers

  1. Use the accept() command on the listening socket to accept the incoming connections.
  2. Threads, the select() command, or the poll() command may be used to perform other tasks while waiting accept other connections.

Exchanging Data

Closing the Connection


UDP (User Datagram Protocol)

UDP is a packet oriented, connectionless protocol. So what does that mean?

Data is sent and received in packets called datagrams. Datagrams are just groups of bytes. A receiver will receive data in the same byte grouping that the sender sent them.

e.g. A sender may send six bytes grouped [0x00, 0x01, 0x02], [0x03, 0x04, 0x05] and if they are received, the receiver will only receive the six bytes group as [0x00, 0x01, 0x02], [0x03, 0x04, 0x05]. More on this later.

Connectionless means that server does not listen for or accept connection requests from clients, and fixed routes are not established for packets. Two packets sent from a sender to the same receiver may take different routes and arrive out of order, and a packet sent from a sender is not guaranteed to be received by the receiver.

e.g. The above mentioned packets of [0x00, 0x01, 0x02] and [0x03, 0x04, 0x05] may be received as:

UDP Clients and Servers

Since UDP is connectionless, there is no need for UDP clients and UDP servers to connect to each other before exchanging data. The only thing a UDP client needs to do is open a socket for UDP communication with the server's address and port. The client's socket may only be used to communicate with one server. A UDP server must bind a datagram socket to a local port for sending and receiving UDP messages to UDP clients. The server may use the same socket for communicating with multiple servers.

Opening a Socket as a UDP Client

  1. Populate a struct addrinfo with information about the server that you want to exchange datagrams with. At minimum:
  2. Use the getaddrinfo() command to get a linked list of potential Internet addresses matching your request.
  3. Create a socket with the socket() command on one of the addresses returned by getaddrinfo().
  4. Copy the server address from the ai_addr field of the address that was used to create the socket.
  5. Use freeaddrinfo() to free-up the memory that getaddrinfo() allocated.
  6. The socket from step 3 and address from step 4 will be used for all communication.

Exchanging Data

Closing the Connection


Opening a Socket as a UDP Server

  1. Create a UDP/IP socket with the socket() command.
  2. Populate a struct sockaddr_in with information about the allowable incoming addresses and port being used.
  3. Use the bind() command to bind the socket to the port specified in the struct sockaddr_in stucture.

Exchanging Data

Closing the Connection


Echo Examples

I have implemented slightly fancy echo examples demonstrating how everything comes together. The examples are purely educational and should not be used as-is to implement a real world platform. In the traditional echo example a echo server will send data received from a client back to that client. The example code that I provide implements a server that will echo data received from a client to all known clients.

Any client communicating with a server is tracked by the file descriptor for the socket used to communicate with it. The TCP server is made aware of a client disconnects by a receive event on the connected socket. UDP is connectionless and there is no mechanism built in to the protocol to alert the server that the client is closing the socket used to communicate with it. In my implementation the UDP client sends a 0 byte packet to the server prior to closing the socket. The 0 byte message is something that I made up. It is not part of the UDP protocol.

The servers and clients require the ability to multitask. I chose to implement multitasking with poll(); it blocks until an event occurs on any of the file descriptors it has been passed.

Running the Executables

All of my examples are console programs that may be executed from a command line prompt in the directory containing their binary images.

NOTE: It is not possible to mix and match TCP and UDP client/server pairs.

echoserver or echoserver_udp

echoserver <port number>
echoserver_udp <port number>

The echoserver will not exit until CTRL-c is pressed.

echoclient or echoclient_udp

echoclient <server address> <port number>
echoclient_udp <server address> <port number>

Hit Enter on a blank line to exit from an echoclient.
Multiple echoclients may connect to a single echoserver instance. A message sent from any echoclient will be echoed to all echoclients.

Building Executables from Source

To build these files with GNU make and gcc:

  1. Open a terminal
  2. Change directory to the directory containing the source files
  3. Enter the command make from the command line

Actual Software

I am releasing my Berkeley Socket examples under the BSD License. There's no rocket science in the examples. If you don't like the BSD License, you could look at my examples and write your own version of code pretty easily. The source code repository containing my examples is available on GitHub. I recommend that you checkout the latest revision of the master branch, unless you're looking for something specific.

Repository Location https://github.com/michaeldipperstein/sockets

Portability

All the source code that I have provided is written in strict ANSI C. However, it requires Berkeley Sockets, poll, and signalfd. So it is only likely to work on Linux systems; it may be re-written without signalfd for all POSIX compliant systems, including BSD.

If you have any further questions or comments, you may contact me by e-mail. My e-mail address is: mdipperstein@gmail.com

Home
Last updated on September 13, 2020