# סוקטים של TCP סוקט TCP נושא זרם בתים אמין ומסודר בין שני תהליכים שעשויים להיות על מכונות שונות. עמוד זה בונה את שני החצאים של שיחת TCP מתוך הפונקציות המובנות הגולמיות: **לקוח** שמתחבר ומחליף שורה, ו**שרת המבצע fork** שמקבל חיבורים ומטפל בכל אחד בילד משלו. מודול הנוחות הגבוה־רמה `IO::Socket::INET` אינו זמין ב־PetaPerl, ולכן כל מתכון כאן משתמש בשכבת [`socket`](../../p5/core/perlfunc/socket.md) המובנית ישירות, עם עוזרי כתובת מהמודול `Socket`. אין בכך הפסד: הצורה המובנית היא מה ש־`IO::Socket::INET` עוטף, וראייתה בגלוי הופכת את לחיצת היד לקריאה. ## עוזרי הכתובת כתובת סוקט היא `struct sockaddr_in` ארוז של C. המודול `Socket` בונה ומפרק עבורך את המבנה הזה: ```perl use Socket; my $packed = sockaddr_in($port, $ip); # build my ($port, $ip) = sockaddr_in($packed); # take apart ``` `$ip` עצמו הוא כתובת ארוזה בת ארבעה בתים. `inet_aton` הופך מחרוזת dotted-quad לכזו; `inet_ntoa` הופך אותה חזרה. הקבוע `INADDR_LOOPBACK` הוא הצורה הארוזה של `127.0.0.1`, ו־`INADDR_ANY` משמעו ”כל ממשק מקומי“. ```perl use Socket; my $ip = inet_aton("127.0.0.1"); print inet_ntoa($ip), "\n"; # 127.0.0.1 ``` ## לקוח TCP הלקוח יוצר סוקט, מחבר אותו לכתובת שרת, ולאחר מכן קורא וכותב את המטפל כמו כל זרם אחר. ```perl use Socket; my $host = "127.0.0.1"; my $port = 8080; socket(my $sock, PF_INET, SOCK_STREAM, getprotobyname("tcp")) or die "socket: $!"; my $addr = sockaddr_in($port, inet_aton($host)); connect($sock, $addr) or die "connect: $!"; $sock->autoflush(1); # send each line as it is printed print $sock "ping\n"; my $reply = <$sock>; print "server said: $reply"; # server said: pong: ping close $sock; ``` השורה האחת שמכשילה את כולם היא `$sock->autoflush(1)`. בלעדיה, `print $sock "ping\n"` יושב בבופר הפלט של Perl, השרת לעולם אינו רואה אותו, ושני הצדדים נחסמים לנצח בהמתנה זה לזה. בכל סוקט שבו כותבים בקשה ואז קוראים תשובה, הגדר autoflush לפני הכתיבה הראשונה. ## שרת המבצע fork שרת מבצע bind לפורט, מאזין, ולאחר מכן עובר בלולאה ומקבל חיבורים. המבנה הקלאסי מטפל בכל לקוח בילד שנוצר ב־fork כך שלקוח איטי אחד אינו יכול לעכב את הבא: ```perl use Socket; use POSIX qw(WNOHANG); # for the non-blocking reaper my $port = 8080; my $backlog = 128; # pending-connection queue depth socket(my $server, PF_INET, SOCK_STREAM, getprotobyname("tcp")) or die "socket: $!"; # Let the address be reused immediately after a restart. setsockopt($server, SOL_SOCKET, SO_REUSEADDR, 1) or die "setsockopt: $!"; bind($server, sockaddr_in($port, INADDR_ANY)) or die "bind: $!"; listen($server, $backlog) or die "listen: $!"; # Reap finished children so they do not become zombies. $SIG{CHLD} = sub { 1 while waitpid(-1, WNOHANG) > 0 }; print "listening on port $port\n"; while (accept(my $client, $server)) { my $pid = fork(); die "fork: $!" unless defined $pid; if ($pid == 0) { # Child: serve this one connection, then exit. close $server; # child does not need the listener $client->autoflush(1); while (my $line = <$client>) { print $client "pong: $line"; } close $client; exit 0; } # Parent: hand the connection to the child and move on. close $client; # parent does not need this client } ``` שלושה פרטים הופכים את השרת הזה לנכון ולא רק לעובד: - **`SO_REUSEADDR` לפני `bind`.** לאחר שהשרת יוצא, הקרנל שומר את הפורט ב־`TIME_WAIT` למשך זמן מה. בלי אפשרות זו הפעלה מחדש נכשלת עם ”Address already in use“. הגדר אותה וההפעלה מחדש מבצעת bind מיד. - **אוסף ה־`$SIG{CHLD}`.** כל ילד שנוצר ב־fork הופך ל־zombie כשהוא יוצא, עד שההורה קורא ל־[`waitpid`](../../p5/core/perlfunc/waitpid.md). המטפל אוסף את כל הילדים שסיימו באופן לא־חוסם בעזרת `WNOHANG`, כך שלולאת ה־accept לעולם אינה נעצרת כדי להמתין. - **שני הצדדים סוגרים את ה־descriptor שאינם משתמשים בו.** הילד סוגר את הסוקט המאזין; ההורה סוגר את סוקט הלקוח שהתקבל. אם אחד מהם מדלג על ה־`close` שלו, ה־file descriptor הבסיסי נשאר פתוח בתהליך הלא־נכון והחיבור לעולם אינו רואה EOF. ## הרצת שני החצאים יחד להדגמה עצמאית, בצע fork לשרת אל הרקע והרץ את הלקוח בהורה. השרת מבצע bind לפורט ארעי (פורט `0`) ומדווח על הפורט שהקרנל הקצה: ```perl use Socket; # --- server child on an ephemeral port --- socket(my $server, PF_INET, SOCK_STREAM, getprotobyname("tcp")) or die "socket: $!"; setsockopt($server, SOL_SOCKET, SO_REUSEADDR, 1) or die "setsockopt: $!"; bind($server, sockaddr_in(0, INADDR_LOOPBACK)) or die "bind: $!"; listen($server, 128) or die "listen: $!"; my ($port) = sockaddr_in(getsockname($server)); my $kid = fork() // die "fork: $!"; if ($kid == 0) { accept(my $client, $server) or die "accept: $!"; $client->autoflush(1); my $line = <$client>; print $client "pong: $line"; close $client; exit 0; } # --- client in the parent --- socket(my $sock, PF_INET, SOCK_STREAM, getprotobyname("tcp")) or die; connect($sock, sockaddr_in($port, INADDR_LOOPBACK)) or die "connect: $!"; $sock->autoflush(1); print $sock "ping\n"; print "client got: ", scalar <$sock>; # client got: pong: ping close $sock; waitpid($kid, 0); ``` `getsockname($server)` מדווח על הכתובת המקומית שאליה הסוקט קשור. פירוקה בעזרת `sockaddr_in` משחזר את הפורט שהקרנל בחר, שאליו הלקוח אז מתחבר - הדרך הסטנדרטית לכתוב בדיקה שאינה מקודדת פורט באופן קשיח. ## קישורי הצלבה לעיון לכל קריאה יש עמוד עיון לכל פונקציה מובנית עם צורת הארגומנט המלאה וערך ההחזרה: - [`socket`](../../p5/core/perlfunc/socket.md) - יצירת ה־endpoint - [`bind`](../../p5/core/perlfunc/bind.md) - חיבורו לכתובת מקומית - [`listen`](../../p5/core/perlfunc/listen.md) - סימונו כמקבל חיבורים - [`accept`](../../p5/core/perlfunc/accept.md) - קבלת החיבור הממתין הבא - [`connect`](../../p5/core/perlfunc/connect.md) - פנייה אל שרת - [`setsockopt`](../../p5/core/perlfunc/setsockopt.md) - הגדרת `SO_REUSEADDR` ודומיו - [`getsockname`](../../p5/core/perlfunc/getsockname.md) - גילוי הפורט הקשור - [`fork`](../../p5/core/perlfunc/fork.md) ו־[`waitpid`](../../p5/core/perlfunc/waitpid.md) - יצירה ואיסוף של הילד לכל לקוח - [`%SIG`](../../p5/core/perlvar/signals.md) - מטפל האיסוף של `CHLD` ## הבא עבור סוקטים של זרם מקומיים בלבד ללא מחסנית הרשת, ועבור datagrams ללא חיבור, ראה [UNIX-domain and UDP sockets](sockets-unix-udp.md).