Intro to TCP Socket Programming in C
Introduction and Context
First-sockets are not solely a network concept. Berkeley Sockets are merely an abstraction of passing messages between two parties. That could be two programs on your local machine (not using any IP addresses) or it could be two machines across the internet. Sockets are just a two-way pipe, and a pipe is just a first-in-first-out buffer, i.e. you receive messages in the order I send them. Sockets let both parties send messages simultaneously, and each party receives messages in the order they were sent.
That said, we often use sockets for communicating across the internet domain using the internet protocol (IP). This protocol allows two devices to communicate to one another using addresses, e.g. 192.168.1.2 → 192.168.1.3. You'll learn more about the protocol and its addressing later in the course.
On top of IP, we can use other protocols such as UDP or TCP. UDP lets us send singular messages with no guarantee that it will reach the destination—like sending a letter in the mail. TCP, on the other hand, is connection-based; it establishes a connection between both parties. This is more similar to a phone call wherein I request a connection by dialing you, you accept by picking up, we talk, and we end the connection. We'll revisit UDP next week, but for this lab we'll start with TCP.
The Essential Server
Let's start with a basic TCP server, a program that runs and receives incoming messages. We'll need to do the following:
- Create our socket
- Bind it to a port and address on our computer
- Listen for a queue of connections
- Accept an incoming connection
- Read and/or write to our socket to communicate with the client
To create a socket, we can include
the <sys/socket.h>
library and use the
aptly-name socket
function:
// my_server.c
int
The first argument to socket
tells the OS what type of socket we'd like to
open. We want one for communicating with version 4 of the internet protocol
(IPv4), so we'll use PF_INET. Other options are listed in the manual page. The
latter two arguments to socket
, SOCK_STREAM
and 0, indicate that we want a
stream-like protocol (TCP) and we have 0 special options to set. The function
will return an integer called a file
descriptor which helps keep
track of the socket as a place where our program can read from and write to. If
creating the socket fails, it will return a -1 and set the global errno
variable. The perror
function from <stdio.h>
uses this errno
value and can be helpful in printing
the reason behind a function's failure.
We're not done yet, though. We still need to
bind
our socket to to an address. In other words, we need to tell the operating
system we'd like all incoming traffic going to a specific network address and
port to be passed in to our socket. For a server, it might be useful to listen
to all of the IP addresses our computer has. For the port number, we can pick
any number at or above 1024 (ports 0-1023 are reserved and require special
privileges for our program to receive on).
To do this, we'll need to use a struct sockaddr
. Note that
we don't create an actual struct sockaddr
explicitly, but instead create a
sort-of sub-struct like
sockaddr_in
,
which is a type of sockaddr
address struct specifically for the internet
domain. If you're famiilar with inheritance or derived classes in other
languages, you might think of this as C's version.
We'll have to give it a family (once again, PF_INET
), an address, and a port.
When we do this, we need to use special functions that make sure the way our
system stores integers doesn't matter (you might've covered this topic,
endianness, in previous classes).
The "host to network short" function,
htons
helps us do this by taking an integer from our system and converting it into the
standard network order.
This way, both the client and server can agree on the ordering of bytes in
numbers. We also will use the special address INADDR_ANY
, which tells our OS
we'd like to listen on all of our available interfaces.
// my_server.c
int
Now we have a socket bound to an address. The next step is to open for business!
We can use the listen
command to let our server build up a queue of incoming connections. For this
lab, feel free to set the backlog to anything greater than or equal to 1, e.g.
10.
/* ... do our socket creation, binding, listening ... */
// listen for incoming connections
;
Next, it's time to handle individual connections. For this, we use the
accept
function. The
accept
parameters are a little more confusing than most. First, we pass in the
file descriptor of the socket we're accepting a connection from. Then, we need
to let accept
tell us the address information of the client that's connecting.
To do this, we pass in two pointers: one to a struct sockaddr
, and one to a
socklen_t
. The struct sockaddr_in
is an "output" parameter--the function
fills it out completely. The socklen_t
is an input AND output parameter, so we
set its value to the initial size of the struct sockaddr
we give accept
and
accept will modify it to reflect the true size of the struct sockaddr
it fills
out. Lastly, we'll store the return value, a file descriptor representing that
particular connection. Here's what this looks like in action:
/* .. other variable definitions ... */
// set up our client addr struct and size variable
struct sockaddr_in client_addr;
socklen_t client_addr_len = sizeof;
/* ... do our socket creation, binding, listening ... */
// accept a single connection and store the file descriptor
int conn_fd = ;
It may help to also error check your connection's file descriptor. Once we have
it, though, it's time to send or receive. For this, we'll use the
write
function and
read
function. Remember that read
will
be, by default, a blocking function--your program will halt and wait for
something to be available to read on your socket. Both of these accept similar
arguments: a file descriptor (conn_fd
in our case), a buffer to either write
from or read to, and a length. For write
, this is the number of bytes to be
written from your program's buffer to the socket. For read
, this is the number
of bytes that will be transferred from that socket to your program's buffer.
Let's first read from the client, then write a response:
/* ... other includes ... */
/* ... other defines ... */
/* ... other variable definitions ... */
char read_buf;
/* ... socket creation, binding, listening, accepting ... */
/* read from the client. Reserve the last byte for adding a null-terminator,
* making it safe for printing */
int num_bytes_read = ;
read_buf = '\0';
;
/* write a response to the client */
;
/* close the connection to our client */
;
And there we have it, a simple TCP server! If you haven't pieced together the above snippets, you should! I've included an abbreviated version below.
char read_buf;
int
Next Steps
There's a handful of things missing from this which you'll need to implement for your lab's server:
- Error checking and handling. Use
perror
for this as mentioned before. - The server should remain in a loop of accepting and handling client connections. After it finishes one, it should close that connection and accept the next.
- Uptime! We've just written an arbitrary message. It's up to you to use
popen
or another method to read the output from theuptime
command and send it back to the client.
You'll also need to implement the client. I'll leave this as an exercise for you. Just like we did for the server, use manual pages or other resources to figure out usage and implement a client which does the following:
- Create a socket using
socket
- (Optional) bind to an address. If you don't, the OS will select an address and port for you. This is usually OK for clients since the address and port portions of the IP header for the packets sent will tell the server where to respond to us at.
connect
to the server using astruct sockaddr_in
which holds the server's family, address, and port.- Similar to the server,
write
andread
on your socket's file descriptor!
Good luck! Ask your TA if you have any questions!