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] = '\0';
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 OK\r\n"
"Content-Length: 13\r\n"
"Content-Type: text/plain\r\n\r\nHello 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;
}
Question: 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 Chrome or Chromium?
CodePudding user response:
Your server responds with an invalid data size Content-Length: 13
.
- The data is
Hello World!
, the size is 12. resp.length()
does not count\0
, thus the server does not sendHello World!\0
.
The header must be Content-Length: 12
.
CodePudding user response:
I addition to @273K's answer, another problem I see is your dumpReceivedContent()
method is completely ignoring the HTTP protocol and just reads and logs everything the client sends until the client disconnects or an error occurs, and then you call sendMinHttpResponse()
on a now-likely-invalid TCP connection, so the client probably won't be able to receive it.
You can't just blindly read from the TCP connection as you are doing. You MUST parse the client's data as it arrives so you can detect the end of the client's request properly and then leave the TCP connection open until your response has been sent.
Refer to RFC 2616 Section 4.4 and RFC 7230 Section 3.3.3 for the rules you must follow to detect when you have received the client's complete request without over-reading from the TCP connection.
I have a number of previous answers that provide pseudo code for demonstrating how to implement those rules.