פרוטוקול HTTP - HTTP protocol¶
הקדמה¶
HTTP (HyperText Transfer Protocol) הוא הפרוטוקול שעליו בנוי כל האינטרנט. כשאתם גולשים לאתר, הדפדפן שלכם שולח בקשת HTTP לשרת ומקבל תשובת HTTP חזרה. זה הכל.
והנה הדבר המפתיע: HTTP הוא פשוט טקסט על גבי TCP. אין פורמט בינארי מסובך, אין הצפנה מובנית (ב-HTTP רגיל), אין קסם. זה פשוט מחרוזות טקסט שנשלחות דרך סוקט TCP.
בהרצאה הזו נבנה גם לקוח HTTP וגם שרת HTTP מאפס, באמצעות כל מה שלמדנו בפרקים הקודמים.
מבנה בקשת HTTP - HTTP request¶
בקשת HTTP היא מחרוזת טקסט עם מבנה קבוע:
שורת הבקשה - request line¶
השורה הראשונה מכילה שלושה חלקים:
- מתודה - method - מה רוצים לעשות:
GET- קבלת משאב (הנפוץ ביותר - טעינת דף)POST- שליחת נתונים (טפסים, העלאת קבצים)PUT- עדכון משאבDELETE- מחיקת משאב-
HEAD- כמו GET אבל בלי ה-body (רק headers) -
נתיב - path - איזה משאב רוצים:
/index.html,/api/users,/images/logo.png -
גרסה - version -
HTTP/1.0אוHTTP/1.1(בדרך כלל 1.1)
דוגמה:
כותרות - headers¶
אחרי שורת הבקשה באות כותרות (headers) - מידע נוסף על הבקשה. כל כותרת בשורה נפרדת, בפורמט Key: Value:
הכותרת 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¶
התשובה דומה במבנה:
שורת המצב - status line¶
- גרסה -
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, 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 שמוריד דף אינטרנט. הרעיון פשוט:
- ליצור חיבור TCP לשרת (פורט 80)
- לשלוח בקשת HTTP כטקסט
- לקרוא את התשובה
- לפרסר אותה (להפריד 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;
}
קומפילציה והרצה:
שימו לב ל-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
נקודות חשובות בשרת¶
-
הגנה מpath traversal - בודקים שהנתיב לא מכיל
... בלי זה, לקוח יכול לבקשGET /../../etc/passwdולקרוא קבצים מחוץ לתיקיית השרת. זו פגיעות אבטחה קלאסית. -
Content-Type - חשוב לשלוח את סוג התוכן הנכון. הדפדפן משתמש בזה כדי להחליט איך להציג את התוכן.
-
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 - אותו פרוטוקול, עם שכבת הצפנה