I’m trying to get straight in my head the relationship between HTTP and TCP.
I tried to resolve (what I perceived as) contradictory answers from a web search of "tcp vs http" by writing a server that listens at a TCP socket bound to some address+port, then typing that address+port into a web brower.
Having done so, I saw that the content received at the accept()
ed socket was text with human-readable "HTTP stuff" (my knowledge of HTTP isn’t enough to intelligently identify the content).
From Chrome, my server receives:
GET / HTTP/1.1
Host: 127.0.0.23:9018
Connection: keep-alive
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.121 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
…and from Firefox, my server receives:
GET / HTTP/1.1
Host: 127.0.0.23:9018
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:102.0) Gecko/20100101 Firefox/102.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Upgrade-Insecure-Requests: 1
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: none
Sec-Fetch-User: ?1
From the above results, I conjectured that HTTP is sending HTTP-conformant bytes (is it always ASCII?) over a TCP connection to a server’s socket that has been accept()
ed after listen()
ing to a specific address+port.
So I further conjectured that in order to get content to show up in a web browser that connects to the address+port that my server is listen()
ing at, my server should write()
some kind of HTTP-compliant response to the socket.
This Stack Overflow Q&A gave me a candidate minimal HTTP response.
Putting it all together, my server’s MCVE code is:
#include <arpa/inet.h>
#include <cerrno>
#include <cstdio>
#include <cstring>
#include <fcntl.h>
#include <iostream>
#include <netinet/in.h>
#include <pthread.h>
#include <semaphore.h>
#include <stdexcept>
#include <sstream>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#define IP "127.0.0.23"
#define PORT (9018)
/**
* A primitive, POC-level HTTP server that accepts its first incoming connection
* and sends back a minimal HTTP OK response.
*/
class Server {
private:
static const std::string ip_;
static const std::uint16_t port_{PORT};
int listen_sock_;
pthread_t tid_;
public:
/**
* Ctor: create and bind listen_sock_ and start a thread for startRoutine().
*/
Server() {
using namespace std;
int result;
if (! createSocket()) { throw runtime_error("failed creating socket"); }
if (! bindSocket()) { throw runtime_error("failed binding socket"); }
if ((result = pthread_create(&tid_, NULL, startRoutine, this))) {
std::stringstream ss;
ss << "pthread_create() error " << errno << "(" << result << ")";
std::cerr << ss.str() << std::endl;
throw runtime_error("failed spawning Server thread");
}
}
/** Dtor: wait for the spawned thread and destroy listen_sock_. */
~Server() {
pthread_join( tid_, NULL );
destroySocket();
}
private:
/** Creates listen_sock_ as a stream socket. */
bool createSocket() {
listen_sock_ = socket(PF_INET, SOCK_STREAM, 0);
if (listen_sock_ < 0) {
std::stringstream ss;
ss << "socket() error " << errno << "(" << strerror(errno) << ")";
std::cerr << ss.str() << std::endl;
}
return (listen_sock_ >= 0);
}
[138/573]
/** Shuts down and closes listen_sock_. */
void destroySocket() {
if (listen_sock_ >= 0) {
shutdown(listen_sock_, SHUT_RDWR);
close(listen_sock_);
}
}
/** Binds listen_sock_ to ip_ and port_. */
bool bindSocket() {
int ret;
sockaddr_in me;
me.sin_family = PF_INET;
me.sin_port = htons(port_);
me.sin_addr.s_addr = inet_addr(ip_.c_str());
int optval = 1;
setsockopt(listen_sock_, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof optval);
if ((ret = bind(listen_sock_, (sockaddr*)&me, sizeof me))) {
std::stringstream ss;
ss << "bind() error " << errno << "(" << strerror(errno) << ")";
std::cerr << ss.str() << std::endl;
}
return (! ret);
}
/**
* Accept a connection from listen_sock_.
* Caller guarantees listen_sock_ has been listen()ed to already.
* @param tv [in, out] How long to wait to accept a connection.
* @return accepted socket; -1 on any error.
*/
int acceptConnection(timeval& tv) {
int sock = -1;
int ret;
fd_set readfds;
sockaddr_in peer;
socklen_t addrlen = sizeof peer;
FD_ZERO(&readfds);
FD_SET(listen_sock_, &readfds);
ret = select(listen_sock_ + 1, &readfds, NULL, NULL, &tv);
if (ret < 0) {
std::stringstream ss;
ss << "select() error " << errno << "(" << strerror(errno) << ")";
std::cerr << ss.str() << std::endl;
return sock;
}
else if (! ret) {
std::cout << "no connections within " << tv.tv_sec << "seconds"
<< std::endl;
return sock;
}
if ((sock = accept(listen_sock_, (sockaddr*)&peer, &addrlen)) < 0) {
std::stringstream ss;
ss << "accept() error " << errno << "(" << strerror(errno) << ")";
std::cerr << ss.str() << std::endl;
}
else {
std::stringstream ss;
ss << "socket " << sock << " accepted connection from "
<< inet_ntoa( peer.sin_addr ) << ":" << ntohs(peer.sin_port);
std::cout << ss.str() << std::endl;
}
return sock;
}
[60/573]
/** Read from the specified socket and dump to stdout. */
static void dumpReceivedContent(const int& sock) {
fd_set readfds;
struct timeval tv = {30, 0};
int ret;
FD_ZERO(&readfds);
FD_SET(sock, &readfds);
ret = select(sock + 1, &readfds, NULL, NULL, &tv);
if (ret < 0) {
std::stringstream ss;
ss << "select() error " << errno << "(" << strerror(errno) << ")";
std::cerr << ss.str() << std::endl;
return;
}
else if (! ret) {
std::cout << "no content received within " << tv.tv_sec << "seconds"
<< std::endl;
return;
}
if (FD_ISSET(sock, &readfds)) {
ssize_t bytes_read;
char buf[80] = {0};
fcntl(sock, F_SETFL, fcntl(sock, F_GETFL, 0) | O_NONBLOCK);
std::cout << "received content:" << std::endl;
std::cout << "----" << std::endl;
while ((bytes_read = read(sock, buf, (sizeof buf) - 1)) >= 0) {
buf[bytes_read] = '';
std::cout << buf;
}
std::cout << std::endl << "----" << std::endl;
}
}
/** Write a minimal HTTP OK response to the specified socker. */
static void sendMinHttpResponse(const int& sock) {
static const std::string resp =
"HTTP/1.1 200 OKrn"
"Content-Length: 13rn"
"Content-Type: text/plainrnrnHello World!";
write(sock, resp.c_str(), resp.length());
}
/**
* Thread start routine: listen for, then accept connections; dump received
* content; send a minimal response.
*/
static void* startRoutine(void* arg) {
Server* s;
if (! (s = (Server*)arg)) {
std::cout << "Bad arg" << std::endl;
return NULL;
}
if (listen(s->listen_sock_, 3)) {
std::stringstream ss;
ss << "listen() error " << errno << "(" << strerror(errno) << ")";
std::cerr << ss.str() << std::endl;
return NULL;
}
std::cout << "Server accepting connections at "
<< s->ip_ << ":" << s->port_ << std::endl;
{
timeval tv = { 30, 0 };
int sock = s->acceptConnection(tv);
if (sock < 0) {
std::cout << "no connections accepted" << std::endl;
return NULL;
}
dumpReceivedContent(sock);
sendMinHttpResponse(sock);
shutdown(sock, SHUT_RDWR);
close(sock);
}
return NULL;
}
};
const std::string Server::ip_{IP};
int main( int argc, char* argv[] ) {
Server s;
return 0;
}
When I point Chrome and Chromium browsers to my server (127.0.0.23:9018), I get a blank page with no content, but when I point Firefox to my server, I get the "Hello world!
" string that I wanted.
Why does this only work with Firefox, and not with Chrome or Chromium?
2
Answers
Your server responds with an invalid data size
Content-Length: 13
.Hello World!
, the size is 12.resp.length()
does not count