Sockets in C

It has been a while since I have done anything with sockets in C, so I figured I should refresh my memory. Beej already has a great guide to network programming, so I’ll try to make a very simple and small introduction. If you want to know more, you should refer to his guide.

To be able to send (or receive) something through sockets, you need perform at least two steps:

  1. Create a socket with socket(3).
  2. Attach an address to it by:
    • binding (bind(3)) a local address to it (if you are the server),
    • or connecting (connect(3)) it to a destination address (usually as a client).

In addition to these, you have to perform other steps if you are the server:

  1. Allow the socket to receive incoming connections by using listen(3)
  2. and finally wait for a new client with accept(3).

After these are successfully completed, you can use recv(3) and send(3) to interact with the remote end. The steps above are valid for TCP sockets; with UDP you don’t have to do all of them since it uses connectionless communication. After the communication is done, you should close the socket using close(3).

Let’s start by creating a socket for a client application. The prototype is:

socket function prototype
1
int socket(int domain, int type, int protocol);

where domain is basically AF_INET for IPv4 connections and AF_INET6 for IPv6, type is SOCK_STREAM for TCP connections and SOCK_DGRAM for UDP. By leaving protocol to 0, we say that we support only one protocol family and protocol is then deduced from the domain. Naturally there are more options than these and you can read more from the man-pages. The return value is a file descriptor to newly created socket (in a successful case). So to create an IPv4 socket for TCP communication, we could do:

1
2
3
4
5
int sock_fd = socket(AF_INET, SOCK_STREAM, 0);
if (sock_fd < 0) {
    perror("socket failed");
    return -1;
}

Now we need to assign an address to the socket by using bind or connect, which are pretty similar to each other:

bind and connect function prototypes
1
2
3
4
5
int bind(int sockfd, const struct sockaddr *addr,
         socklen_t addrlen);

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

Here the sockfd is the file descriptor returned by the socket, addr is a pointer to an address in the network format and addrlen is its length. In both cases return value is zero on success and -1 on error.

You would basically have your IP address as a string, e.g. "127.0.0.1" which is in printable format. To get a network format from that, you would most likely use function like inet_pton(3):

inet_pton function prototype
1
int inet_pton(int af, const char *src, void *dst);

With inet_pton you transform printable (pton) address to network address format (pton), for example like this:

1
2
3
4
5
6
struct sockaddr_in sock_addr = {0};
int ret = inet_pton(AF_INET, "127.0.0.1", &sock_addr.sin_addr);
if (ret != 1) {
    fprintf(stderr, "inet_pton failed: %d\n", ret);
    return -1;
}

Then after setting the port (using 6000 as an example) and the family (IPv4),

1
2
sock_addr.sin_family = AF_INET;
sock_addr.sin_port = htons(6000);

you could connect the previously created sock_fd to the destination address:

1
2
3
4
5
6
ret = connect(sock_fd, (const struct sockaddr *)&sock_addr, sizeof(sock_addr));
if (ret < 0) {
    perror("connect failed");
    close(sock_fd);
    return -1;
}

Now that the socket is created and connected to the destination address, we can send something (with send(3)):

send function prototype
1
ssize_t send(int socket, const void *buffer, size_t length, int flags);

Here socket is the sock_fd that we have connected, buffer contains the message to be sent and length is its length in bytes. flags argument is not interesting for us, so we can leave it as 0. send will return the number of bytes sent or -1 on failure. But basically it will either send all the bytes or fail with -1, so you can just check that the return value is not negative.

1
2
3
4
5
6
7
8
const char buf[] = "test string";

ssize_t sent_bytes = send(sock_fd, buf, sizeof(buf), 0);
if (sent_bytes < 0) {
    perror("send failed");
    close(sock_fd);
    return -1;
}

To create the server application, we will create the socket same way as before but now we will use bind instead of connect.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int sock_fd = socket(AF_INET, SOCK_STREAM, 0);
if (sock_fd < 0) {
    perror("socket failed");
    return -1;
}

struct sockaddr_in sock_addr = {0};
sock_addr.sin_family = AF_INET;
sock_addr.sin_addr.s_addr = INADDR_ANY;
sock_addr.sin_port = htons(6000);

int ret = bind(sock_fd, (const struct sockaddr *)&sock_addr, sizeof(sock_addr));
if (ret < 0) {
    perror("bind failed");
    close(sock_fd);
    return -1;
}

We use constant INADDR_ANY as an address to say that we want to bind to all interfaces on the machine (i.e. not restricting us only to “127.0.0.1” but to allow packets to arrive in any address assigned to us). Next we have to inform the operating system that we are interested in incoming connections by using listen:

listen function prototype
1
int listen(int socket, int backlog);

Here socket is the socket file descriptor that we have bound and backlog is basically means how many clients there can be queued for this socket. We are expecting only one client now so let’s just use 1 as a backlog. listen returns 0 on success and -1 on failure, so remember to check the return value also here.

1
2
3
4
5
6
ret = listen(sock_fd, 1);
if (ret < 0) {
    perror("listen failed");
    close(sock_fd);
    return -1;
}

The actual waiting for a new client happens in accept:

accept function prototype
1
2
int accept(int socket, struct sockaddr *restrict address,
           socklen_t *restrict address_len);

Here socket is the file descriptor that has been bound and listened, address is a structure where the address of connected client is stored and address_len is its length. Last two arguments are optional, so if you are not interested in client’s address, you may just give NULL to both. Return value is a socket file descriptor or -1 on failure. Notice though that accept will block until a client arrives, meaning that this function call will not return until someone connects to our server.

1
2
3
4
5
6
    int client_sock = accept(sock_fd, NULL, NULL);
    if (client_sock < 0) {
        perror("accept failed");
        close(sock_fd);
        return -1;
    }

Now we can use client_sock for sending and receiving. Since our client program sends a message to the server, we might as well use recv here and print the received bytes:

recv function prototype
1
ssize_t recv(int socket, void *buffer, size_t length, int flags);

recv function prototype is basically the same as with send. flags can be omitted also here for now. Only difference is the return value; recv will return the length of the message received, -1 on failure or 0 if client has properly closed the socket. So if zero is returned, client didn’t send anything.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
    char buf[1024];

    ssize_t received_bytes = recv(client_sock, buf, sizeof(buf), 0);
    if (received_bytes < 0) {
        perror("recv failed");
        close(client_sock);
        close(sock_fd);
        return -1;
    } else if (received_bytes == 0) {
        fprintf(stderr, "client closed the connection\n");
    } else {
        /* Someting was received, print it. */
        printf("received: '");
        for (ssize_t i = 0; i < received_bytes; i++) {
            putchar(buf[i]);
        }
        printf("'\n");
    }

    close(client_sock);
    close(sock_fd);

And that’s all there is to get a simple client/server application in C. Remember to check the return values and print errors with perror(3) if you run in to any problems. As always, you can download the complete files for the client and the server.

Comments