לדלג לתוכן

פרוטוקול HTTP - HTTP protocol

הקדמה

HTTP (HyperText Transfer Protocol) הוא הפרוטוקול שעליו בנוי כל האינטרנט. כשאתם גולשים לאתר, הדפדפן שלכם שולח בקשת HTTP לשרת ומקבל תשובת HTTP חזרה. זה הכל.

והנה הדבר המפתיע: HTTP הוא פשוט טקסט על גבי TCP. אין פורמט בינארי מסובך, אין הצפנה מובנית (ב-HTTP רגיל), אין קסם. זה פשוט מחרוזות טקסט שנשלחות דרך סוקט TCP.

בהרצאה הזו נבנה גם לקוח HTTP וגם שרת HTTP מאפס, באמצעות כל מה שלמדנו בפרקים הקודמים.


מבנה בקשת HTTP - HTTP request

בקשת HTTP היא מחרוזת טקסט עם מבנה קבוע:

METHOD PATH HTTP/VERSION\r\n
Header1: value1\r\n
Header2: value2\r\n
\r\n
[body]

שורת הבקשה - request line

השורה הראשונה מכילה שלושה חלקים:

  1. מתודה - method - מה רוצים לעשות:
  2. GET - קבלת משאב (הנפוץ ביותר - טעינת דף)
  3. POST - שליחת נתונים (טפסים, העלאת קבצים)
  4. PUT - עדכון משאב
  5. DELETE - מחיקת משאב
  6. HEAD - כמו GET אבל בלי ה-body (רק headers)

  7. נתיב - path - איזה משאב רוצים: /index.html, /api/users, /images/logo.png

  8. גרסה - version - HTTP/1.0 או HTTP/1.1 (בדרך כלל 1.1)

דוגמה:

GET /index.html HTTP/1.1\r\n

כותרות - headers

אחרי שורת הבקשה באות כותרות (headers) - מידע נוסף על הבקשה. כל כותרת בשורה נפרדת, בפורמט Key: Value:

Host: www.example.com\r\n
User-Agent: MyClient/1.0\r\n
Accept: text/html\r\n
Connection: close\r\n

הכותרת Host חובה ב-HTTP/1.1. היא אומרת לשרת לאיזה דומיין הבקשה מיועדת (שרת אחד יכול לארח כמה אתרים).

שורה ריקה

שורה ריקה (\r\n) מפרידה בין הכותרות לגוף הבקשה. זה הסימן "הכותרות נגמרו".

גוף - body

אופציונלי. בקשות GET בדרך כלל בלי body. בקשות POST כוללות body עם הנתונים שנשלחים.

דוגמה מלאה

GET /index.html HTTP/1.1\r\n
Host: www.example.com\r\n
User-Agent: MyClient/1.0\r\n
Accept: */*\r\n
Connection: close\r\n
\r\n

מבנה תשובת HTTP - HTTP response

התשובה דומה במבנה:

HTTP/VERSION STATUS_CODE REASON\r\n
Header1: value1\r\n
Header2: value2\r\n
\r\n
[body]

שורת המצב - status line

HTTP/1.1 200 OK\r\n
  • גרסה - HTTP/1.1
  • קוד מצב - status code - מספר שמתאר את התוצאה
  • סיבה - reason phrase - תיאור טקסטואלי (לנוחות, לא חובה)

קודי מצב נפוצים

קוד שם משמעות
200 OK הבקשה הצליחה
301 Moved Permanently המשאב עבר כתובת (redirect קבוע)
302 Found המשאב עבר כתובת (redirect זמני)
400 Bad Request בקשה שגויה
403 Forbidden אין הרשאה
404 Not Found המשאב לא נמצא
500 Internal Server Error שגיאה בשרת

כותרות תשובה

Content-Type: text/html\r\n
Content-Length: 1234\r\n
Server: MyServer/1.0\r\n
Connection: close\r\n

כותרות חשובות:
- Content-Type - סוג התוכן: text/html, text/plain, image/png, application/json
- Content-Length - אורך הbody בבתים. הלקוח יודע כמה בתים לקרוא

דוגמה מלאה

HTTP/1.1 200 OK\r\n
Content-Type: text/html\r\n
Content-Length: 44\r\n
\r\n
<html><body><h1>Hello!</h1></body></html>

בניית לקוח HTTP בC

עכשיו שאנחנו מבינים את הפרוטוקול, בואו נבנה לקוח HTTP שמוריד דף אינטרנט. הרעיון פשוט:

  1. ליצור חיבור TCP לשרת (פורט 80)
  2. לשלוח בקשת HTTP כטקסט
  3. לקרוא את התשובה
  4. לפרסר אותה (להפריד headers מ-body)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>

#define BUFFER_SIZE 4096

/* פתרון שם דומיין לכתובת IP */
int resolve_hostname(const char *hostname, struct sockaddr_in *addr)
{
    struct hostent *he = gethostbyname(hostname);
    if (!he) {
        fprintf(stderr, "cannot resolve: %s\n", hostname);
        return -1;
    }
    addr->sin_family = AF_INET;
    memcpy(&addr->sin_addr, he->h_addr_list[0], he->h_length);
    return 0;
}

int main(int argc, char *argv[])
{
    if (argc != 3) {
        fprintf(stderr, "usage: %s <hostname> <path>\n", argv[0]);
        fprintf(stderr, "example: %s example.com /index.html\n", argv[0]);
        exit(1);
    }

    const char *hostname = argv[1];
    const char *path = argv[2];

    /* פתרון שם דומיין */
    struct sockaddr_in server_addr;
    memset(&server_addr, 0, sizeof(server_addr));
    if (resolve_hostname(hostname, &server_addr) == -1)
        exit(1);
    server_addr.sin_port = htons(80);

    char ip[INET_ADDRSTRLEN];
    inet_ntop(AF_INET, &server_addr.sin_addr, ip, sizeof(ip));
    printf("connecting to %s (%s)...\n", hostname, ip);

    /* חיבור TCP */
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd == -1) {
        perror("socket");
        exit(1);
    }

    if (connect(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
        perror("connect");
        close(sockfd);
        exit(1);
    }

    /* בניית בקשת HTTP */
    char request[1024];
    snprintf(request, sizeof(request),
        "GET %s HTTP/1.1\r\n"
        "Host: %s\r\n"
        "User-Agent: MyHTTPClient/1.0\r\n"
        "Accept: */*\r\n"
        "Connection: close\r\n"
        "\r\n",
        path, hostname);

    printf("sending request:\n%s", request);

    /* שליחת הבקשה */
    write(sockfd, request, strlen(request));

    /* קריאת התשובה */
    char response[BUFFER_SIZE * 16];  /* באפר גדול לתשובה */
    int total = 0;
    ssize_t n;

    while ((n = read(sockfd, response + total, sizeof(response) - total - 1)) > 0) {
        total += n;
    }
    response[total] = '\0';

    printf("\nreceived %d bytes\n", total);

    /* מציאת ההפרדה בין headers ל-body */
    char *body = strstr(response, "\r\n\r\n");
    if (body) {
        *body = '\0';  /* מסיימים את ה-headers */
        body += 4;     /* מדלגים על \r\n\r\n */

        printf("\n=== HEADERS ===\n%s\n", response);
        printf("\n=== BODY (%ld bytes) ===\n%s\n", strlen(body), body);
    } else {
        printf("could not parse response\n");
    }

    close(sockfd);
    return 0;
}

קומפילציה והרצה:

gcc -o httpclient httpclient.c
./httpclient example.com /

שימו לב ל-Connection: close. זה אומר לשרת "אחרי שתשלח את התשובה, סגור את החיבור". בלי זה, השרת עלול להשאיר את החיבור פתוח (keep-alive) ו-read לא יחזיר 0.


בניית שרת HTTP בC

עכשיו נבנה שרת HTTP שמגיש קבצים סטטיים. שרת שמקבל בקשת GET, מוצא את הקובץ המבוקש, ומחזיר אותו עם headers מתאימים.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define PORT 8080
#define BUFFER_SIZE 4096
#define ROOT_DIR "./www"  /* תיקיית השורש לקבצים */

/* זיהוי סוג קובץ לפי סיומת */
const char *get_content_type(const char *path)
{
    const char *ext = strrchr(path, '.');
    if (!ext) return "application/octet-stream";

    if (strcmp(ext, ".html") == 0 || strcmp(ext, ".htm") == 0)
        return "text/html";
    if (strcmp(ext, ".css") == 0)
        return "text/css";
    if (strcmp(ext, ".js") == 0)
        return "application/javascript";
    if (strcmp(ext, ".json") == 0)
        return "application/json";
    if (strcmp(ext, ".png") == 0)
        return "image/png";
    if (strcmp(ext, ".jpg") == 0 || strcmp(ext, ".jpeg") == 0)
        return "image/jpeg";
    if (strcmp(ext, ".gif") == 0)
        return "image/gif";
    if (strcmp(ext, ".txt") == 0)
        return "text/plain";

    return "application/octet-stream";
}

/* שליחת תשובת שגיאה */
void send_error(int client_fd, int code, const char *reason)
{
    char body[256];
    snprintf(body, sizeof(body),
        "<html><body><h1>%d %s</h1></body></html>", code, reason);

    char response[512];
    snprintf(response, sizeof(response),
        "HTTP/1.1 %d %s\r\n"
        "Content-Type: text/html\r\n"
        "Content-Length: %ld\r\n"
        "Connection: close\r\n"
        "\r\n"
        "%s",
        code, reason, strlen(body), body);

    write(client_fd, response, strlen(response));
}

/* טיפול בבקשה */
void handle_request(int client_fd)
{
    char buffer[BUFFER_SIZE];
    ssize_t n = read(client_fd, buffer, sizeof(buffer) - 1);
    if (n <= 0) return;
    buffer[n] = '\0';

    /* פירסור שורת הבקשה */
    char method[16], path[256], version[16];
    if (sscanf(buffer, "%15s %255s %15s", method, path, version) != 3) {
        send_error(client_fd, 400, "Bad Request");
        return;
    }

    printf("%s %s %s\n", method, path, version);

    /* תומכים רק ב-GET */
    if (strcmp(method, "GET") != 0) {
        send_error(client_fd, 405, "Method Not Allowed");
        return;
    }

    /* הגנה מ-path traversal: אסור .. בנתיב */
    if (strstr(path, "..")) {
        send_error(client_fd, 403, "Forbidden");
        return;
    }

    /* בניית נתיב מלא */
    char filepath[512];
    if (strcmp(path, "/") == 0)
        snprintf(filepath, sizeof(filepath), "%s/index.html", ROOT_DIR);
    else
        snprintf(filepath, sizeof(filepath), "%s%s", ROOT_DIR, path);

    /* פתיחת הקובץ */
    FILE *fp = fopen(filepath, "rb");
    if (!fp) {
        send_error(client_fd, 404, "Not Found");
        return;
    }

    /* מציאת גודל הקובץ */
    fseek(fp, 0, SEEK_END);
    long file_size = ftell(fp);
    fseek(fp, 0, SEEK_SET);

    /* שליחת headers */
    char headers[512];
    snprintf(headers, sizeof(headers),
        "HTTP/1.1 200 OK\r\n"
        "Content-Type: %s\r\n"
        "Content-Length: %ld\r\n"
        "Connection: close\r\n"
        "\r\n",
        get_content_type(filepath), file_size);

    write(client_fd, headers, strlen(headers));

    /* שליחת תוכן הקובץ */
    char file_buf[BUFFER_SIZE];
    size_t bytes;
    while ((bytes = fread(file_buf, 1, sizeof(file_buf), fp)) > 0) {
        write(client_fd, file_buf, bytes);
    }

    fclose(fp);
}

int main(void)
{
    int server_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (server_fd == -1) {
        perror("socket");
        exit(1);
    }

    int opt = 1;
    setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

    struct sockaddr_in server_addr;
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(PORT);
    server_addr.sin_addr.s_addr = INADDR_ANY;

    if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
        perror("bind");
        exit(1);
    }

    if (listen(server_fd, 10) == -1) {
        perror("listen");
        exit(1);
    }

    printf("HTTP server on port %d, serving files from %s\n", PORT, ROOT_DIR);

    while (1) {
        struct sockaddr_in client_addr;
        socklen_t client_len = sizeof(client_addr);
        int client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &client_len);
        if (client_fd == -1) {
            perror("accept");
            continue;
        }

        handle_request(client_fd);
        close(client_fd);
    }

    close(server_fd);
    return 0;
}

לבדיקה:

# יצירת תיקייה ודף
mkdir -p www
echo '<html><body><h1>Hello from C!</h1></body></html>' > www/index.html

# קומפילציה והרצה
gcc -o httpserver httpserver.c
./httpserver

# בטרמינל אחר, או בדפדפן:
curl http://localhost:8080/
curl http://localhost:8080/index.html

נקודות חשובות בשרת

  1. הגנה מpath traversal - בודקים שהנתיב לא מכיל ... בלי זה, לקוח יכול לבקש GET /../../etc/passwd ולקרוא קבצים מחוץ לתיקיית השרת. זו פגיעות אבטחה קלאסית.

  2. Content-Type - חשוב לשלוח את סוג התוכן הנכון. הדפדפן משתמש בזה כדי להחליט איך להציג את התוכן.

  3. Content-Length - אומר ללקוח כמה בתים לצפות. בלי זה, הלקוח לא יודע מתי התוכן נגמר (חוץ מלחכות לסגירת החיבור).


שרת HTTP עם epoll

השרת שלנו מטפל בלקוח אחד בכל פעם. בואו נשלב אותו עם epoll מפרק 11.2 כדי לטפל בכמה לקוחות במקביל:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <fcntl.h>

#define PORT 8080
#define MAX_EVENTS 64
#define BUFFER_SIZE 4096
#define ROOT_DIR "./www"

void set_nonblocking(int fd)
{
    int flags = fcntl(fd, F_GETFL, 0);
    fcntl(fd, F_SETFL, flags | O_NONBLOCK);
}

const char *get_content_type(const char *path)
{
    const char *ext = strrchr(path, '.');
    if (!ext) return "application/octet-stream";
    if (strcmp(ext, ".html") == 0) return "text/html";
    if (strcmp(ext, ".css") == 0) return "text/css";
    if (strcmp(ext, ".js") == 0) return "application/javascript";
    if (strcmp(ext, ".png") == 0) return "image/png";
    if (strcmp(ext, ".jpg") == 0) return "image/jpeg";
    if (strcmp(ext, ".txt") == 0) return "text/plain";
    return "application/octet-stream";
}

void send_response(int fd, int code, const char *reason,
                   const char *content_type, const char *body, long body_len)
{
    char headers[512];
    int hlen = snprintf(headers, sizeof(headers),
        "HTTP/1.1 %d %s\r\n"
        "Content-Type: %s\r\n"
        "Content-Length: %ld\r\n"
        "Connection: close\r\n"
        "\r\n",
        code, reason, content_type, body_len);

    write(fd, headers, hlen);
    if (body && body_len > 0)
        write(fd, body, body_len);
}

void handle_request(int client_fd)
{
    char buffer[BUFFER_SIZE];
    ssize_t n = read(client_fd, buffer, sizeof(buffer) - 1);
    if (n <= 0) return;
    buffer[n] = '\0';

    char method[16], path[256];
    if (sscanf(buffer, "%15s %255s", method, path) != 2) {
        const char *err = "<h1>400 Bad Request</h1>";
        send_response(client_fd, 400, "Bad Request", "text/html", err, strlen(err));
        return;
    }

    if (strcmp(method, "GET") != 0) {
        const char *err = "<h1>405 Method Not Allowed</h1>";
        send_response(client_fd, 405, "Method Not Allowed", "text/html", err, strlen(err));
        return;
    }

    if (strstr(path, "..")) {
        const char *err = "<h1>403 Forbidden</h1>";
        send_response(client_fd, 403, "Forbidden", "text/html", err, strlen(err));
        return;
    }

    char filepath[512];
    if (strcmp(path, "/") == 0)
        snprintf(filepath, sizeof(filepath), "%s/index.html", ROOT_DIR);
    else
        snprintf(filepath, sizeof(filepath), "%s%s", ROOT_DIR, path);

    FILE *fp = fopen(filepath, "rb");
    if (!fp) {
        const char *err = "<h1>404 Not Found</h1>";
        send_response(client_fd, 404, "Not Found", "text/html", err, strlen(err));
        return;
    }

    fseek(fp, 0, SEEK_END);
    long file_size = ftell(fp);
    fseek(fp, 0, SEEK_SET);

    char headers[512];
    int hlen = snprintf(headers, sizeof(headers),
        "HTTP/1.1 200 OK\r\n"
        "Content-Type: %s\r\n"
        "Content-Length: %ld\r\n"
        "Connection: close\r\n"
        "\r\n",
        get_content_type(filepath), file_size);
    write(client_fd, headers, hlen);

    char file_buf[BUFFER_SIZE];
    size_t bytes;
    while ((bytes = fread(file_buf, 1, sizeof(file_buf), fp)) > 0)
        write(client_fd, file_buf, bytes);

    fclose(fp);
}

int main(void)
{
    int server_fd = socket(AF_INET, SOCK_STREAM, 0);
    int opt = 1;
    setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

    struct sockaddr_in addr = {
        .sin_family = AF_INET,
        .sin_port = htons(PORT),
        .sin_addr.s_addr = INADDR_ANY
    };

    bind(server_fd, (struct sockaddr *)&addr, sizeof(addr));
    listen(server_fd, 64);
    set_nonblocking(server_fd);

    int epoll_fd = epoll_create1(0);
    struct epoll_event ev = {.events = EPOLLIN, .data.fd = server_fd};
    epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_fd, &ev);

    printf("HTTP server (epoll) on port %d\n", PORT);

    struct epoll_event events[MAX_EVENTS];
    while (1) {
        int n = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
        for (int i = 0; i < n; i++) {
            if (events[i].data.fd == server_fd) {
                /* חיבור חדש */
                struct sockaddr_in client_addr;
                socklen_t len = sizeof(client_addr);
                int client_fd;
                while ((client_fd = accept(server_fd,
                    (struct sockaddr *)&client_addr, &len)) != -1) {
                    ev.events = EPOLLIN;
                    ev.data.fd = client_fd;
                    epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_fd, &ev);
                }
            } else {
                /* בקשה מלקוח */
                handle_request(events[i].data.fd);
                epoll_ctl(epoll_fd, EPOLL_CTL_DEL, events[i].data.fd, NULL);
                close(events[i].data.fd);
            }
        }
    }

    return 0;
}

עכשיו השרת מטפל בכמה חיבורים במקביל, ב-thread אחד. זה הדפוס הבסיסי שעליו בנויים שרתים כמו nginx.


הערה על HTTPS

HTTPS הוא HTTP + TLS (Transport Layer Security). TLS מצפין את החיבור כך שאף אחד באמצע לא יכול לקרוא את התעבורה.

מבחינת הקוד שלנו, ההבדל הוא בשכבת התחבורה: במקום read/write ישירות על הסוקט, משתמשים בפונקציות של ספריית הצפנה (כמו OpenSSL) שמצפינות ומפענחות את הנתונים. חלק הHTTP עצמו - הבקשות, התשובות, הheaders - נשאר בדיוק אותו דבר.

ספריות כמו OpenSSL מספקות API שנראה דומה מאוד ל-read/write:

/* במקום: */
write(sockfd, data, len);
read(sockfd, buffer, size);

/* משתמשים ב: */
SSL_write(ssl, data, len);
SSL_read(ssl, buffer, size);

לא ניכנס למימוש TLS בקורס הזה, אבל חשוב להבין את העיקרון: HTTPS הוא אותו HTTP, רק עטוף בשכבת הצפנה.


סיכום

  • HTTP הוא פרוטוקול טקסט פשוט על גבי TCP
  • בקשה: שורת בקשה (method + path + version), headers, שורה ריקה, body
  • תשובה: שורת מצב (version + status code + reason), headers, שורה ריקה, body
  • \r\n מפריד בין שורות, \r\n\r\n מפריד בין headers ל-body
  • לקוח HTTP: חיבור TCP לפורט 80, שליחת בקשה כטקסט, קריאת תשובה
  • שרת HTTP: האזנה, קבלת חיבור, קריאת בקשה, שליחת קובץ עם headers מתאימים
  • בדיקת path traversal (..) היא הגנת אבטחה בסיסית אבל קריטית
  • שילוב עם epoll הופך את השרת ליעיל לטיפול בהרבה חיבורים
  • HTTPS = HTTP + TLS - אותו פרוטוקול, עם שכבת הצפנה