mini_db · Optim
▶ Aprender mini_db (Optim) paso a paso · explicación de mini_db
Código línea a línea
Tres columnas: el código, el trozo del subject o del main que obliga a escribir esa línea, y el por qué.
mini_db.hpp
| Código | Subject | Por qué |
|---|---|---|
#ifndef MINI_DB_HPP | Expected files : *. cpp *. hpp | Guarda: que el .hpp no se incluya dos veces. |
# define MINI_DB_HPP | ||
| ||
# include <map> | will maintain a database of key-values. | std::map: la base de datos. |
# include <string> | std::string. | |
| ||
class Db | will maintain a database of key-values. | La base de datos: lo que sabe hacer (cargar, guardar, ejecutar un comando) y lo que guarda. |
{ | ||
public: | ||
Db(const std::string &path); | a port and a path | Nace sabiendo en qué fichero vive. |
void load(); | Each time your program starts, it will look for the presence of this file and load it. | Fichero → map. |
void save() const; | it must save the current database | Map → fichero. const: no cambia nada. |
std::string run(const std::string &line); | Your program will receive commands from the connected clients | Un comando → su respuesta. |
| ||
private: | ||
std::string _path; | to the file path given as an argument | El fichero. |
std::map<std::string, std::string> _data; | a database of key-values. Your program must be efficient. | Clave → valor. Un map busca en O(log n): eficiente. |
}; | ||
| ||
#endif | Fin de la guarda. |
mini_db.cpp
| Código | Subject | Por qué |
|---|---|---|
#include "mini_db.hpp" | El .cpp implementa la clase del .hpp. | |
#include <iostream> | it will print "ready\n" on the standard output. | std::cout, std::cerr. |
#include <fstream> | to the file path given as an argument | std::ifstream, std::ofstream. |
#include <sstream> | POST <key> <value> | std::istringstream: partir el comando por espacios. |
#include <csignal> | When your program receives the SIGINT signal | signal, SIGINT, sig_atomic_t. |
#include <cstdlib> | a port | atoi, exit. |
#include <cstring> | memset. | |
#include <unistd.h> | close. | |
#include <sys/select.h> | The connections are persistent | select, fd_set: varios clientes a la vez. |
#include <sys/socket.h> | listen for clients to connect | socket, bind, listen, accept, send, recv, setsockopt. |
#include <netinet/in.h> | 127.0.0.1 | sockaddr_in, htons, htonl, INADDR_LOOPBACK. |
| ||
static volatile sig_atomic_t g_stop = 0; | When your program receives the SIGINT signal | La bandera que levanta la señal. volatile sig_atomic_t: el único tipo seguro de tocar desde un handler. static: solo en este fichero. |
| ||
static void on_sigint(int) | When your program receives the SIGINT signal, it must save the current database | El handler solo levanta la bandera: guardar aquí dentro no es seguro (streams, malloc…). El bucle del servidor verá la bandera y guardará. |
{ | ||
g_stop = 1; | ||
} | ||
| ||
static void fatal() | Error de sistema antes de servir: mensaje y fuera. | |
{ | ||
std::cerr << "Fatal error" << std::endl; | Por stderr. | |
exit(1); | ||
} | ||
| ||
// ---------- la base de datos: un map y un fichero ---------- | ||
| ||
Db::Db(const std::string &path) : _path(path) {} | a path | Solo guarda la ruta. Nada se carga aún. |
| ||
void Db::load() | Each time your program starts, it will look for the presence of this file and load it. | |
{ | ||
std::ifstream in(_path.c_str()); | it will look for the presence of this file | Si no existe, el stream está mal y el while no entra: no es un error. |
std::string key, value; | ||
| ||
while (in >> key >> value) | Keys and values will be of arbitrary size and will not contain any white spaces, | Sin espacios dentro: >> separa por espacios y basta. Para cuando no quedan dos palabras. |
_data[key] = value; | add an entry to the db | Crea o sobreescribe. |
} | ||
| ||
void Db::save() const | it must save the current database (i.e. all the key-value pairs) to the file path given as an argument | |
{ | ||
std::ofstream out(_path.c_str()); | to the file path given as an argument | Crea o trunca el fichero. |
| ||
for (std::map<std::string, std::string>::const_iterator it = _data.begin(); it != _data.end(); ++it) | all the key-value pairs | Todo el map. const_iterator porque save es const (C++98: sin auto). |
out << it->first << ' ' << it->second << '\n'; | clave valor por línea: justo lo que load lee con >>. | |
} | ||
| ||
// una línea de comando -> la respuesta, con su '\n' | ||
std::string Db::run(const std::string &line) | Your program will receive commands from the connected clients | Una línea, una respuesta. No sabe de sockets: fácil de probar. |
{ | ||
std::istringstream ss(line); | Para leer la línea palabra a palabra. | |
std::string cmd, key, value, extra; | POST <key> <value> | Comando, clave, valor y lo que sobre. |
| ||
ss >> cmd >> key >> value >> extra; | will not contain any white spaces, | Lo que falte queda vacío: así compruebo cuántas palabras venían. |
if (cmd == "POST" && !value.empty() && extra.empty()) | POST <key> <value> | POST con exactamente clave y valor. |
{ | ||
_data[key] = value; | add an entry to the db | Crea o sobreescribe. |
return ("0\n"); | return value: "0" | Con el \n: el cliente lee líneas. |
} | ||
if (cmd == "GET" && !key.empty() && value.empty()) | GET <key> | GET con solo una clave. |
{ | ||
if (!_data.count(key)) | "1" if the key does not exist | |
return ("1\n"); | ||
return ("0 " + _data[key] + "\n"); | "0 <value>" | |
} | ||
if (cmd == "DELETE" && !key.empty() && value.empty()) | DELETE <key> | |
return (_data.erase(key) ? "0\n" : "1\n"); | return "0" if the key was successfully erased or "1" if the key does not exist | erase(clave) devuelve cuántos borró: 1 o 0. |
return ("2\n"); | If a command is not recognized your program must respond "2". | Todo lo demás. |
} | ||
| ||
// ---------- el servidor: select sobre 127.0.0.1:port ---------- | ||
| ||
static int listen_on(int port) | Your program must listen for clients to connect on 127.0.0.1 on the given port. | Crea el socket de escucha y lo devuelve. |
{ | ||
int sock = socket(AF_INET, SOCK_STREAM, 0); | IPv4 + TCP. | |
| ||
if (sock < 0) | ||
fatal(); | ||
int yes = 1; | ||
setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes)); | $> ./mini_db 1111 .save & | Poder reabrir el mismo puerto nada más matar el servidor (como en el ejemplo). Sin esto, bind falla un rato con "address already in use". |
sockaddr_in addr; | La dirección donde escucho. | |
memset(&addr, 0, sizeof(addr)); | A ceros antes de rellenar. | |
addr.sin_family = AF_INET; | IPv4. | |
addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); | 127.0.0.1 | INADDR_LOOPBACK = 127.0.0.1, a orden de red. |
addr.sin_port = htons(port); | on the given port | A orden de red. |
if (bind(sock, (sockaddr *)&addr, sizeof(addr)) < 0 || listen(sock, 128) < 0) | listen for clients to connect | bind: me quedo esa dirección. listen: cola de 128 conexiones pendientes. |
fatal(); | ||
return (sock); | ||
} | ||
| ||
static void serve(int sock, Db &db) | The connections are persistent, which means that a program can send multiple commands in a single session. | El bucle select: acepta, lee, responde. Hasta que llegue SIGINT. |
{ | ||
fd_set all, rd; | all: fds abiertos. rd: copia que select rellena con los que tienen algo que leer. | |
int maxfd = sock; | select necesita el fd más alto + 1. | |
std::map<int, std::string> bufs; | a program can send multiple commands in a single session | Lo recibido de cada cliente que aún no acaba en \n. |
| ||
FD_ZERO(&all); | Conjunto vacío… | |
FD_SET(sock, &all); | …con el socket de escucha dentro. | |
while (!g_stop) | When your program receives the SIGINT signal | La señal corta el select y levanta la bandera: salgo del bucle. |
{ | ||
rd = all; | select modifica el conjunto: le paso una copia. | |
if (select(maxfd + 1, &rd, NULL, NULL, NULL) < 0) | Espera a que algún fd tenga datos. Sin timeout. | |
continue ; | Error (p. ej. la señal): compruebo la bandera y, si no, sigo esperando. | |
for (int fd = 0; fd <= maxfd; fd++) | Reviso todos los fds. | |
{ | ||
if (!FD_ISSET(fd, &rd)) | Este no tiene nada. | |
continue ; | Error (p. ej. la señal): compruebo la bandera y, si no, sigo esperando. | |
if (fd == sock) | listen for clients to connect | Lectura en el socket de escucha = conexión nueva. |
{ | ||
int client = accept(sock, NULL, NULL); | Un fd nuevo para el cliente. Su dirección no me importa. | |
if (client < 0) | Ya estamos sirviendo: no es fatal. | |
continue ; | Error (p. ej. la señal): compruebo la bandera y, si no, sigo esperando. | |
FD_SET(client, &all); | select lo vigila a partir de ahora. | |
if (client > maxfd) | ||
maxfd = client; | ||
bufs[client].clear(); | Buffer vacío para él (crea la entrada en el map). | |
continue ; | Error (p. ej. la señal): compruebo la bandera y, si no, sigo esperando. | |
} | ||
char buf[1024]; | the size of a request will never exceed 1000 characters. | Un trozo: recv devuelve lo que haya, no necesariamente una línea. |
int n = recv(fd, buf, sizeof(buf), 0); | ||
if (n <= 0) | 0 = el cliente cerró; -1 = error. Fuera. | |
{ | ||
FD_CLR(fd, &all); | ||
close(fd); | Sin leaks de fd. | |
bufs.erase(fd); | Y su buffer. | |
continue ; | Error (p. ej. la señal): compruebo la bandera y, si no, sigo esperando. | |
} | ||
bufs[fd].append(buf, n); | a program can send multiple commands in a single session | Pego lo nuevo. append(ptr, n): buf no acaba en \0. |
size_t nl; | Posición del próximo \n. | |
while ((nl = bufs[fd].find('\n')) != std::string::npos) | Cada línea completa es un comando. Pueden venir varias en un recv. | |
{ | ||
std::string reply = db.run(bufs[fd].substr(0, nl)); | Your program will receive commands from the connected clients | La línea sin su \n. |
bufs[fd].erase(0, nl + 1); | Quito la línea ya tratada; queda lo que venga detrás. | |
send(fd, reply.c_str(), reply.size(), 0); | La respuesta al que preguntó. | |
} | ||
} | ||
} | ||
} | ||
| ||
int main(int argc, char **argv) | Write a program that takes as an argument a port and a path. | |
{ | ||
if (argc != 3) | Programa + puerto + ruta. | |
{ | ||
std::cerr << "Wrong number of arguments" << std::endl; | ||
return (1); | ||
} | ||
Db db(argv[2]); | a path | El segundo argumento es el fichero. |
db.load(); | Each time your program starts, it will look for the presence of this file and load it. | Antes de atender a nadie. |
signal(SIGINT, on_sigint); | When your program receives the SIGINT signal | Ctrl-C ya no mata el programa: levanta la bandera. |
int sock = listen_on(atoi(argv[1])); | a port | El primer argumento es el puerto. |
std::cout << "ready" << std::endl; | When your program is ready to accept incoming connections, it will print "ready\n" on the standard output. | Justo después de listen: ya se puede conectar. endl hace flush: el que espera lo ve ya. |
serve(sock, db); | Hasta SIGINT. | |
db.save(); | it must save the current database (i.e. all the key-value pairs) to the file path given as an argument and exit. | Al salir del bucle, con calma, fuera del handler. |
close(sock); | ||
return (0); | ||
} |