סוקטים של TCP#

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

מודול הנוחות הגבוה־רמה IO::Socket::INET אינו זמין ב־PetaPerl, ולכן כל מתכון כאן משתמש בשכבת socket המובנית ישירות, עם עוזרי כתובת מהמודול Socket. אין בכך הפסד: הצורה המובנית היא מה ש־IO::Socket::INET עוטף, וראייתה בגלוי הופכת את לחיצת היד לקריאה.

עוזרי הכתובת#

כתובת סוקט היא struct sockaddr_in ארוז של C. המודול Socket בונה ומפרק עבורך את המבנה הזה:

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 משמעו ”כל ממשק מקומי“.

use Socket;
my $ip = inet_aton("127.0.0.1");
print inet_ntoa($ip), "\n";          # 127.0.0.1

לקוח TCP#

הלקוח יוצר סוקט, מחבר אותו לכתובת שרת, ולאחר מכן קורא וכותב את המטפל כמו כל זרם אחר.

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 כך שלקוח איטי אחד אינו יכול לעכב את הבא:

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. המטפל אוסף את כל הילדים שסיימו באופן לא־חוסם בעזרת WNOHANG, כך שלולאת ה־accept לעולם אינה נעצרת כדי להמתין.

  • שני הצדדים סוגרים את ה־descriptor שאינם משתמשים בו. הילד סוגר את הסוקט המאזין; ההורה סוגר את סוקט הלקוח שהתקבל. אם אחד מהם מדלג על ה־close שלו, ה־file descriptor הבסיסי נשאר פתוח בתהליך הלא־נכון והחיבור לעולם אינו רואה EOF.

הרצת שני החצאים יחד#

להדגמה עצמאית, בצע fork לשרת אל הרקע והרץ את הלקוח בהורה. השרת מבצע bind לפורט ארעי (פורט 0) ומדווח על הפורט שהקרנל הקצה:

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 משחזר את הפורט שהקרנל בחר, שאליו הלקוח אז מתחבר - הדרך הסטנדרטית לכתוב בדיקה שאינה מקודדת פורט באופן קשיח.

הבא#

עבור סוקטים של זרם מקומיים בלבד ללא מחסנית הרשת, ועבור datagrams ללא חיבור, ראה UNIX-domain and UDP sockets.