Premise:
I'm building on newly-learned networking fundamentals learned from these two questions: one, two.
I'll call the the code at the bottom of this post my "http responder," and not a "http server," since I recently got an educational/appreciated slap on the wrist for calling it the latter.
The program functions as follows:
- it listens at
INADDR_ANY
port 9018 (a naively/randomly-chosen number) - it dumps (to stdout) the content received at the accepted socket until there's no more content to read
- it sends a minimal HTTP response with status OK.
(in case @Remy Lebeau visits this question, item 2, specifically, is why this program is not a http server: it does not parse the incoming HTTP request, it just dumbly dumps it and responds -- even in the case of a closed TCP connection -- but I believe this is not relevant to the question asked here).
From my second link, above, I learned about why a web server would want to listen to a specific port on all interfaces.
My understanding is that the way this is done in C-family languages is by binding to INADDR_ANY
(as opposed to a specific IP address, like "127.0.0.13").
Question:
When I run this program, I observe the expected result if I try to connect from a web browser that is running on the same PC as where the executable is run: my browser shows a minimal webpage with content "I'm the content" if I connect to 127.0.0.1:9018, 127.0.0.2:9018, 127.0.0.13.9018, 127.0.0.97:9018, etc.
Most relevant to this question, I also get the same minimal webpage by pointing my browser to 10.0.0.17:9018, which is the IP address assigned to my "wlpls0" interface:
$ ifconfig
...
wlp1s0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet 10.0.0.17 netmask 255.255.255.0 broadcast 10.0.0.255
inet6 fe80::5f8c:c301:a6a3:6e35 prefixlen 64 scopeid 0x20<link>
ether f8:59:71:01:89:cf txqueuelen 1000 (Ethernet)
RX packets 1272659 bytes 1760801882 (1.7 GB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 543118 bytes 74285210 (74.2 MB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
However, I only observe this desired webpage if the browser that I point to 10.0.0.7:9018 is running on the same PC as where the a.out is running.
From another PC on the same network, if I point its browser to 10.0.0.17:9018, the browser spins without connecting, and eventually says "Hmm...can't reach this page" and "10.0.0.17 took too long to respond".
So my question is: what are reasons why only a browser running on the same PC as the running a.out can connect to the "http responder"? Why do browsers on a different PC in the same network seem unable to connect?
What I have tried:
On the other PC, I am able to ping 10.0.0.17 -- and that just about exhausts my knowledge of how to debug networking issues.
I considered whether the issue at root is more likely to be "networking stuff", which might make this question better asked at Super User, but then I thought to start my inquiry with Stack Overflow, in case the issues is in the C++ code.
The code:
// main.cpp
#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 "0.0.0.0"
#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:
Server() { ///< create + bind listen_sock_; start thread for startRoutine().
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");
}
}
~Server() { ///< wait for the spawned thread and destroy listen_sock_.
pthread_join( tid_, NULL );
destroySocket();
}
private:
bool createSocket() { ///< Creates listen_sock_ as a stream socket.
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);
}
void destroySocket() { ///< shut down and closes listen_sock_.
if (listen_sock_ >= 0) {
shutdown(listen_sock_, SHUT_RDWR);
close(listen_sock_);
}
}
bool bindSocket() { ///< binds listen_sock_ to ip_ and port_.
int ret;
sockaddr_in me;
me.sin_family = PF_INET;
me.sin_port = htons(port_);
me.sin_addr.s_addr = INADDR_ANY;
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;
}
static void dumpReceivedContent(const int& sock) { ///< read & dump from 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;
}
}
static void sendMinHttpResponse(const int& sock) { ///< min HTTP OK + content.
static const std::string html =
"<!doctype html>"
"<html lang=en>"
"<head>"
"<meta charset=utf-8>"
"<title>blah</title>"
"</head>"
"<body>"
"<p>I'm the content</p>"
"</body>"
"</html>";
std::stringstream resp;
resp << "HTTP/1.1 200 OK\r\n"
<< "Content-Length: " << html.length() << "\r\n"
<< "Content-Type: text/html\r\n\r\n"
<< html;
write(sock, resp.str().c_str(), resp.str().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;
}
Compilation/execution:
This is a "working" case when the http responder receives a connection from a web browser on the same PC connecting to 10.0.0.17:9018:
$ g++ -g ./main.cpp -lpthread && ./a.out
Server accepting connections at 0.0.0.0:9018
socket 4 accepted connection from 10.0.0.17:56000
received content:
----
GET / HTTP/1.1
Host: 10.0.0.17:9018
Connection: keep-alive
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 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
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
----
This is the problem/question case when the http responder receives nothing from a web browser on a different PC in the same network connecting to 10.0.0.17:9018:
$ ./a.out
Server accepting connections at 0.0.0.0:9018
no connections within 0 seconds
no connections accepted
** The "no connections within 0 seconds" message is because select()
updated the struct timeval.tv_sec
field -- the program has actually waited 30 seconds.