Sending Response (C Networking Part 2)
In part 1, we covered the basics of creating a socket, binding it, listening on it, accepting a connection, and receiving a transmission. While this might be enough if all we needed was to receive data, usually we want to send something back, if only to confirm on the application layer that we received what we did. So, today we extend our server by learning how to send data back.
Enable port and address reuse
But first, you may have noted that you can’t connect to the server when you stop and start it rapidly in succession. Add this to your code, after calling socket()
but before calling bind()
:
int opt = 1;
setsockopt(socketfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
setsockopt(socketfd, SOL_SOCKET, SO_REUSEPORT, &opt, sizeof(opt));
This sets the following flags on our new socket, to let us immediately reuse addresses and ports. This is useful because, when we shut down the server, the OS maintains the binding to it for TIME_WAIT
to account for TCP packets that may have been received late. Since right now we are just testing locally, we don’t have to worry about packets arriving late.
Setting opt
to 1 just tells the flag option to be set to true. Passing in an opt
value of zero is how you would disable an option.
Sending Data
Sending data is done straightforwardly with the send
function, which, like recv
, operates on a socket file descriptor. Here’s a basic example where we send a basic HTTP response back to the client:
const char *response = "Custom server response\n";
send(clientfd, response, strlen(response), 0);
Fairly straightforward. Like before with the recv()
function, the final argument is for flags and can be safely just left at 0 for now. Now let’s use telnet to test what we have. It’s a barebones communciation protocol often used for passing simple textual messages back and forth. Boot up your server. Then, try connecting to it.
cbrown@typhoon ~ % telnet localhost 80
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
Now type something in and press enter.
Client:
cbrown@typhoon ~ % telnet localhost 80
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
Test message
Custom server response
Connection closed by foreign host.
Server:
cbrown@typhoon ~/Documents/repo/server % ./server
Test message
Success!
Looks like it works. Next, we want our server to respond with valid HTTP. Thankfully we can temporarily use HTTP version 0.9, which has no headers and simply consists of the content. Run your server again and use the following
Client:
cbrown@typhoon ~ % curl --http0.9 localhost
Custom server response
Server:
cbrown@typhoon ~/Documents/repo/server % ./server
GET / HTTP/1.1
Host: localhost
User-Agent: curl/8.6.0
Accept: */*
Success!
Sending valid HTTP
Now lets try sending and viewing valid HTTP using our browser. Update your response message to include valid headers for use with HTTP 1.1:
const char *response =
"HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n"
"<html><h1>HTML Response</h1><p>The server works!</p></html>";
Now, start up your server and try accessing it in your browser. You should see the HTML content being properly displayed! For reference, here’s the code is at is right now:
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <unistd.h>
#define PORT 80
int main() {
// Create socket
int socketfd = socket(PF_INET, SOCK_STREAM, 0);
int opt = 1;
setsockopt(socketfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
setsockopt(socketfd, SOL_SOCKET, SO_REUSEPORT, &opt, sizeof(opt));
// Assign address to socket
struct sockaddr_in socketAddress = {
.sin_family = AF_INET,
.sin_port = htons(PORT),
.sin_addr = INADDR_ANY,
};
bind(socketfd, (const struct sockaddr *)&socketAddress,
sizeof(socketAddress));
// Listen for and start queueing up connections on socket
listen(socketfd, 64);
// Wait for and accept incoming TCP connection
struct sockaddr_in clientAddress;
socklen_t clientAddressSize = sizeof(clientAddress);
int clientfd =
accept(socketfd, (struct sockaddr *)&clientAddress, &clientAddressSize);
// Wait for and print out data received
char receiveBuffer[1024];
ssize_t bytesReceived =
recv(clientfd, receiveBuffer, sizeof(receiveBuffer) - 1, 0);
receiveBuffer[bytesReceived] = '\0';
puts(receiveBuffer);
// Send HTTP response
const char *response =
"HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n"
"<html><h1>HTML Response</h1><p>The server works!</p></html>";
send(clientfd, response, strlen(response), 0);
close(socketfd);
puts("Success!");
return 0;
}
Congratulations! Now your server is receiving and sending input. When it comes to data processing there’s many other layers of complexity you could add. For example, you might want to store your website’s HTML, style, and script data in a separate file, and server that. You probably would also want to parse the client’s HTTP request to make sure it’s valid, and retrieve the proper document. What we have so far should be a good start, though. As mentioned before, in a real production environment, you would need to add proper error checking, too.
Coming up in the next part, we’ll learn more advanced options for managing client connections (other than just sending a message and closing it right away). Also, we’ll learn strategies for handling it when multiple connections come in at once.