Sockets in action (part 2)
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 exampleconnect(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.