Υποδοχές TCP#
Μια υποδοχή TCP μεταφέρει μια αξιόπιστη, διατεταγμένη ροή bytes μεταξύ δύο διεργασιών που μπορεί να βρίσκονται σε διαφορετικά μηχανήματα. Αυτή η σελίδα χτίζει τα δύο μισά μιας συνομιλίας TCP από τις χαμηλού επιπέδου ενσωματωμένες συναρτήσεις: έναν πελάτη που συνδέεται και ανταλλάσσει μια γραμμή, και έναν διακομιστή που κάνει fork που αποδέχεται συνδέσεις και χειρίζεται την καθεμία στο δικό του παιδί.
Το άρθρωμα ευκολίας υψηλού επιπέδου IO::Socket::INET δεν είναι διαθέσιμο στην PetaPerl, οπότε κάθε συνταγή εδώ χρησιμοποιεί απευθείας το ενσωματωμένο επίπεδο socket με βοηθούς διευθύνσεων από το άρθρωμα Socket. Αυτό δεν είναι απώλεια: η ενσωματωμένη μορφή είναι αυτό που περιτυλίγει το IO::Socket::INET, και το να τη βλέπετε ανοιχτά καθιστά τη χειραψία ευανάγνωστη.
Οι βοηθοί διευθύνσεων#
Μια διεύθυνση υποδοχής είναι ένα πακεταρισμένο C struct sockaddr_in. Το άρθρωμα Socket χτίζει και αποσυναρμολογεί αυτή τη δομή για εσάς:
use Socket;
my $packed = sockaddr_in($port, $ip); # build
my ($port, $ip) = sockaddr_in($packed); # take apart
Το $ip είναι το ίδιο μια πακεταρισμένη διεύθυνση τεσσάρων bytes. Η inet_aton μετατρέπει μια συμβολοσειρά τετράδας με τελείες σε τέτοια· η 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#
Ένας διακομιστής δένεται σε μια θύρα, ακούει, και κατόπιν επαναλαμβάνει αποδεχόμενος συνδέσεις. Η κλασική δομή χειρίζεται κάθε πελάτη σε ένα παιδί που έχει δημιουργηθεί με 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». Ορίστε την και η επανεκκίνηση δένεται αμέσως.Ο συγκομιστής
$SIG{CHLD}. Κάθε παιδί που δημιουργείται με fork γίνεται zombie όταν τερματίζει, μέχρι ο γονέας να καλέσειwaitpid. Ο χειριστής συγκομίζει όλα τα ολοκληρωμένα παιδιά χωρίς μπλοκάρισμα μεWNOHANG, ώστε ο βρόχος αποδοχής να μην σταματά ποτέ για να περιμένει.Και οι δύο πλευρές κλείνουν τον περιγραφέα που δεν χρησιμοποιούν. Το παιδί κλείνει την υποδοχή που ακούει· ο γονέας κλείνει την αποδεκτή υποδοχή πελάτη. Αν κάποια από τις δύο παραλείψει το
closeτης, ο υποκείμενος περιγραφέας αρχείου παραμένει ανοιχτός στη λάθος διεργασία και η σύνδεση δεν βλέπει ποτέ EOF.
Εκτέλεση και των δύο μισών μαζί#
Για μια αυτοτελή επίδειξη, κάντε fork τον διακομιστή στο παρασκήνιο και εκτελέστε τον πελάτη στον γονέα. Ο διακομιστής δένεται σε μια εφήμερη θύρα (θύρα 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 ανακτά τη θύρα που επέλεξε ο πυρήνας, στην οποία στη συνέχεια συνδέεται ο πελάτης - ο τυπικός τρόπος να γράψετε ένα test που δεν ενσωματώνει σταθερά μια θύρα.
Διασταυρούμενοι σύνδεσμοι αναφοράς#
Κάθε κλήση έχει μια σελίδα αναφοράς ανά ενσωματωμένη συνάρτηση με την πλήρη μορφή ορισμάτων και την τιμή επιστροφής:
socket- δημιουργεί το άκροbind- το προσαρτά σε μια τοπική διεύθυνσηlisten- το σημειώνει ως αποδεχόμενο συνδέσειςaccept- παίρνει την επόμενη εκκρεμή σύνδεσηconnect- προσεγγίζει έναν διακομιστήsetsockopt- ορίζει τοSO_REUSEADDRκαι τα συναφήgetsockname- ανακαλύπτει τη δεμένη θύραforkκαιwaitpid- δημιουργεί και συγκομίζει το παιδί ανά πελάτη%SIG- ο χειριστής συγκομιδήςCHLD
Επόμενο#
Για υποδοχές ροής μόνο τοπικές χωρίς τη στοίβα δικτύου, και για datagrams χωρίς σύνδεση, δείτε Υποδοχές UNIX-domain και UDP.