Sockets in action (part 2)

Ivan Borshukov
5 min readNov 18, 2016

--

Hi again! In the last post we talked about some operations on sockets — creating, binding, listening and accepting. This led us to a server application that accepts incoming connections. In this post, we’ll look from the other side of the socket — we’ll examine the operations needed to become a client and establish a connection.

Just to recap:

A network socket is an endpoint of a connection in a computer network. It is a handle (abstract reference) that a program can pass to the networking application programming interface (API) to use the connection for receiving and sending data. Sockets are often represented internally as integers.

Creating a socket

The creation is the same no matter if we want to create a server, a client, or just to send datagrams. It is explained in depth in the first post.

Connecting a socket

The next thing we need to do is to connect our local socket to a remote one. This is done via the connect system call.

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

As you know the sockets that are supposed to accept connections are bound a name (address). For establishing a connection, we need to specify two things — the local socket that we’d want to connect and the address of the remote peer.

Note that this makes sense only if the underlying protocol is connection-oriented (e.g. TCP). If this is not the case (UDP), connect basically just sets the default destination address for send.

So, the connect system call connects the socket referred to by the file descriptor sockfd to the address specified by addr. The addrlen argument specifies the size of addr. The format of the address in addr is determined by the address family of the socket.

Let’s see a rough example:

sockfd = socket(PF_INET, SOCK_STREAM, 0);memset(&serv_addr, 0, sizeof(serv_addr));serv_addr.sin_family = AF_INET;
inet_aton("127.0.0.1", &serv_addr.sin_addr);
serv_addr.sin_port = htons(8000);
connect(sockfd, (struct sockaddr *) &serv_addr, sizeof(serv_addr))

Sending data

After we have a client socket we can start exchanging data. Data exchange works in two ways — sending and receiving. First let’s send some data using the send family of functions.

We’ll explain the use of the following two:

ssize_t send(int socket, const void *buffer, size_t length, int flags);
ssize_t sendto(int
sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);

The differences between send and sendto is that send is intended only for connect-ed sockets and sendto may be used with sockets that use connectionless protocols.

Both functions take a pointer to a buffer which contains the actual data to be send plus the size (in bytes) of the buffer. The next argument is a flag which specifies some options for the operation — for example whether to block on the call or not, will there be more data and so on.

Enough talking, lets see an example when the socket is connect-ed.

// ...
// continues from connect example
connect(sockfd, (struct sockaddr *) &serv_addr, sizeof(serv_addr))nbytes = send(sockfd, buf, sizeof(buf), 0);

And an example where the socket uses UDP, thus it is not connect-ed and uses sendto.

sockfd = socket(PF_INET, SOCK_DGRAM, 0); // note SOCK_DGRAM typememset(&serv_addr, 0, sizeof(serv_addr));serv_addr.sin_family = AF_INET;
inet_aton("127.0.0.1", &serv_addr.sin_addr);
serv_addr.sin_port = htons(5553);
nbytes = sendto(sockfd, msg, strlen(msg)+1, 0,
(struct sockaddr *) &serv_addr, sizeof(serv_addr));

Receiving data

The things for receiving are pretty much the same as for sending. Let’s see the recv family of functions.

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);

Again, recv is intended for connect-ed sockets and recvfrom could be used with sockets that use connectionless protocol. The only specific is that the src_addr is actually a value-result argument which will be assigned with the sender’s address iff the underlying protocol provides it.

If we do not care about the remote address, NULL should be passed. The argument addrlen is also a value-result argument, which the caller should initialize before the call to the size of the buffer associated with src_addr, and modified on return to indicate the actual size of the source address.

Let’s see examples again. If we’re using a connection oriented protocol and we’ve already connect-ed, we simply need to call recv on the socket:

buf = malloc(1024);
nbytes = recv(sockfd, buf, 1024, 0);

But if we’re connectionless, we need to bind an address to the socket first, because we’re actually acting as the server, not the client!

sockfd = socket(PF_INET, SOCK_DGRAM, 0);memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = INADDR_ANY;
serv_addr.sin_port = htons(5553);
bind(sockfd, (struct sockaddr *) &serv_addr, sizeof(serv_addr));buf = malloc(1024);
nbytes = recvfrom(sockfd, buf, 1024, 0,
(struct sockaddr *) &peer_addr, &peer_addr_size);

Closing a socket

After we’re done using a socket, we should free the resources allocated with it. This can simply be done via close. If there is still data waiting to be transmitted over the connection, normally close tries to complete this transmission.

close(sockfd);

Closing will prevent any more reads and writes to the socket. Anyone attempting to read or write the socket on the remote end will receive an error.

But if we need to close the connection in only one way, say, we wan’t to shut down reception, we should use shutdown.

int shutdown(int sockfd, int how);

The how argument controls what should be shut down and it should have one of the following values:

SHUT_RD - further receptions will be disallowed
SHUT_WR - further transmissions will be disallowed
SHUT_RDWR - further receptions and transmissions will be disallowed

Note that calling shutdown does not actually close the socket descriptor. Thus calling also close is necessary if you want to free the descriptor.

shutdown(sockfd, SHUT_WR); // tell remote peer you're done
recv(sockfd, buf, 1024, 0); // read all what's left
close(sockfd); // free the socket descriptor

Summary

That’s it — we’ve created both a server and a client, using both connection-oriented and connectionless protocols. Here’s the system calls that we’ve used in this chapter:

connect - initiate a connection on a socket
send - send a message from a socket
recv - receive a message from a socket
shutdown - shut down part of a full-duplex connection on a socket
close - close a socket descriptor

Stay tuned for the next part, where we’ll examine other cool things to do with sockets.

--

--

Ivan Borshukov

Cloud developer, student, mountain lover, gopher. Are you enlightened? Views are my own.