Applying C - Sockets The Client
Written by Harry Fairhead   
Tuesday, 26 April 2022
Article Index
Applying C - Sockets The Client
Connect a socket to an address
A Web Client
Connecting Using a URL

Connect a socket to an address

To connect a socket as a client of another use the connect function: 

int connect(int sockfd,const struct sockaddr *addr,
                                    socklen_t addrlen);

The sockfd parameter is just the socket file descriptor returned from the socket function. The addr parameter points at a sockaddr struct which contains the address of the socket you want to connect to. Of course addrlen just specifies the size of the struct. The socket address type depend on the underlying communications medium that the socket uses, but in most cases it is just an IP address.

Bind a socket to an address

To assign a server socket to the address it will respond to, use bind: 

int bind(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);

Beginners often ask what the difference is between connect and bind. The answer is that connect makes a connection to the socket with the specified address whereas bind makes the socket respond to that address. Put another way, use connect with a client socket and bind with a server socket. 

Reading and Writing

As an open socket is just a file, you can use the standard read and write functions that you would use to work with a file. There are two additional functions, send and recv, which work in the same way as write and read but have an additional final parameter that can be used to control exactly how the transaction is performed. If you set the file parameter to 0 then send and recv are identical to write and read. Under some C standards you may also need:

#include <unistd.h>

To emphasize the unity between the file system and sockets, the examples in this chapter use read and write, but they could just as easily use recv and send with 0 as the final parameter. It is worth noting that Windows sockets do not support read and write but they do work with recv and send and if you are developing a cross-platform program then it might be easier to use these alternatives.

Listen and Accept

There is one small matter that we have to deal with that takes us beyond simple file use semantics. If you have opened a socket and bound it to an IP address then it is acting as a server socket and is ready to wait for a connection. How do you know when there is a connection, and how do you know when to read or write data? Notice this problem doesn't arise with a client socket because it initiates the complete connection and sends and receives data when it is ready.  

The function:

int listen(int sockfd, int backlog);

sets the socket as an active server. From this point on it listens for the IP address it is bound to and accepts incoming connections. The backlog parameter sets how many pending connections will be queued for processing. The actual processing of a connection is specified by:

int accept(int sockfd, struct sockaddr *addr,
socklen_t *addrlen);

The accept command provides the address of the client trying to make the connection in the sockaddr structure. It also returns a new socket file descriptor to use to talk to the client. The original socket carries on operating as before. Notice that this is slightly more complicated than you might expect in that it is not the socket that you created that is used to communicate with the client. The socket you created just listens out for clients and creates a queue of pending requests. The accept function processes these requests and creates new sockets used to communicate with the client.

This still doesn't solve the problem of how the server detects that there are clients pending. This is a complicated question with many different solutions. You can set up the listening socket to be either blocking or non-blocking. If it is blocking then a call to accept will not return until there is a client ready to be processed. If it is non-blocking then a call to accept returns at once with an error code equal to  EAGAIN  or  EWOULDBLOCK. So you can either use a blocking call or you can poll for clients to be ready.

A more complex approach would be to use another thread to call the poll() function which performs a wait with no CPU overhead while the file descriptor isn't ready, see Chapter 14 for an example.

A Web Client

We now have enough information to implement our first socket program, a web client. It has to be admitted that a web client isn't as common a requirement as a web server, but it is simpler and illustrates most of the points of using sockets to implement an HTTP transaction.

The first thing we have to do is create a socket and the TCP needed for an HTTP transaction:

int sockfd = socket(AF_INET, SOCK_STREAM, 0);

To allow this to work you have to add:

#include <sys/socket.h>

Next we need to get the address of the server we want to connect to. For the web this would usually be done using a DNS lookup on a domain name. To make things simple, we will skip the lookup and use a known IP address. Example.com is a domain name provided for use by examples and you can find its address by pinging it. At the time of writing it was hosted at:

93.184.216.34

This could change so check before concluding that "nothing works".

There are three fields in the address structure. The first is: 

struct sockaddr_in addr;

Then comes sin_family, which is set to:

addr.sin_family = AF_INET;

to indicate an internet IPv4 address.

The next field is the port number of the IP address, but you can't simply use:

addr.sin_port = 80;

because the bit order used on the Internet isn't the same as used on most processors. Instead you have to use a utility function that will ensure the correct bit order:

addr.sin_port = htons(80);

The function name stands for “host to network short” and there are other similarly named functions. 

The actual address is defined in the in_addr field. This is a struct with only one field, s_addr, a 32-bit representation of an IP address. The format is fairly simple. Regard the 32-bit value as four bytes with each byte coding one value of the "dotted" IP address. That is, if the IP address is w.x.y.z then w, x, y and z are the bytes of s_addr. For example, the IP address of example.com is 93.184.216.34  and converting each value into its byte equivalent in hex gives 5d.b8.d8.22, which would be the hex value we have to store in s_addr if it wasn't for the fact that the bytes are stored in reverse order. So, the hex equivalent of the IP address is 0x22d8b85d and this is used to initialize the address struct:

addr.sin_addr.s_addr = 0x22d8b85d;

To make all this work you need to add:

#include <sys/types.h>

and:

#include <netinet/in.h>

With the address worked out and safely stored we can now make the connection:

connect(sockfd, &addr, sizeof (addr));

This will return 0 if it successfully connects and we do need to test for this condition. You will also get a type warning because the pointer to the addr structure isn't as defined in the function. In fact there are many variations which you could pass and it is the standard idiom to cast them to the function's pointer type:

connect(sockfd, (struct sockaddr *) &addr, 
sizeof (addr));

Finally we need to check for an error:

if(connect(sockfd,(struct sockaddr*)&addr,
sizeof(addr))<0)return -1;


Last Updated ( Tuesday, 26 April 2022 )