Ich habe einen Multithread-Server (Thread-Pool), der eine große Anzahl von Anforderungen verarbeitet (bis zu 500/s für einen Knoten), wobei 20 Threads verwendet werden. Es gibt einen Listener-Thread, der eingehende Verbindungen akzeptiert und in eine Warteschlange stellt, damit die Handler-Threads verarbeitet werden können. Sobald die Antwort fertig ist, schreiben die Threads zum Client und schließen den Socket. Bis vor kurzem schien alles in Ordnung zu sein. Nach dem Lesen der Antwort begann ein Test-Client-Programm zufällig zu hängen. Nach vielem Graben scheint es, als würde das close () vom Server den Socket nicht wirklich trennen. Ich habe dem Code einige Debugging-Drucke mit der Dateideskriptor-Nummer hinzugefügt, und ich bekomme diese Art von Ausgabe.
Processing request for 21
Writing to 21
Closing 21
Der Rückgabewert von close () ist 0, oder es wird eine andere Debug-Anweisung gedruckt. Nach dieser Ausgabe mit einem Client, der hängt, zeigt lsof eine hergestellte Verbindung an.
SERVER 8160 root 21u IPv4 32754237 TCP localhost: 9980-> localhost: 47530 (ESTABLISHED)
KUNDE 17747 root 12u IPv4 32754228 TCP localhost: 47530-> localhost: 9980 (ERSTELLT)
Es ist, als würde der Server niemals die Herunterfahren-Sequenz an den Client senden, und dieser Status hängt, bis der Client beendet wird, und der Server befindet sich in einem geschlossenen Wartezustand
SERVER 8160 root 21u IPv4 32754237 TCP localhost: 9980-> localhost: 47530 (CLOSE_WAIT)
Wenn für den Client ein Timeout festgelegt wurde, wird es statt hängen bleiben. Ich kann auch manuell laufen
call close(21)
auf dem Server von gdb, und der Client wird dann die Verbindung trennen. Dies geschieht möglicherweise einmal in 50.000 Anfragen, aber möglicherweise nicht für längere Zeiträume.
Linux-Version: 2.6.21.7-2.fc8xen Centos-Version: 5.4 (Final)
socket-Aktionen sind wie folgt
SERVER:
int client_socket; struct sockaddr_in client_addr; socklen_t client_len = sizeof (client_addr);
while(true) {
client_socket = accept(incoming_socket, (struct sockaddr *)&client_addr, &client_len);
if (client_socket == -1)
continue;
/* insert into queue here for threads to process */
}
Dann nimmt der Thread den Socket auf und baut die Antwort auf.
/* get client_socket from queue */
/* processing request here */
/* now set to blocking for write; was previously set to non-blocking for reading */
int flags = fcntl(client_socket, F_GETFL);
if (flags < 0)
abort();
if (fcntl(client_socket, F_SETFL, flags|O_NONBLOCK) < 0)
abort();
server_write(client_socket, response_buf, response_length);
server_close(client_socket);
server_write und server_close.
void server_write( int fd, char const *buf, ssize_t len ) {
printf("Writing to %d\n", fd);
while(len > 0) {
ssize_t n = write(fd, buf, len);
if(n <= 0)
return;// I don't really care what error happened, we'll just drop the connection
len -= n;
buf += n;
}
}
void server_close( int fd ) {
for(uint32_t i=0; i<10; i++) {
int n = close(fd);
if(!n) {//closed successfully
return;
}
usleep(100);
}
printf("Close failed for %d\n", fd);
}
KLIENT:
Clientseite verwendet libcurl v 7.27.0
CURL *curl = curl_easy_init();
CURLcode res;
curl_easy_setopt( curl, CURLOPT_URL, url);
curl_easy_setopt( curl, CURLOPT_WRITEFUNCTION, write_callback );
curl_easy_setopt( curl, CURLOPT_WRITEDATA, write_tag );
res = curl_easy_perform(curl);
Nichts Besonderes, nur eine grundlegende Curl-Verbindung. Client hängt in tranfer.c (in libcurl), da der Socket nicht als geschlossen erkannt wird. Es wartet auf weitere Daten vom Server.
Dinge, die ich bisher ausprobiert habe:
Vor dem Schließen herunterfahren
shutdown(fd, SHUT_WR);
char buf[64];
while(read(fd, buf, 64) > 0);
/* then close */
Einstellung, dass SO_LINGER in 1 Sekunde zwangsweise geschlossen wird
struct linger l;
l.l_onoff = 1;
l.l_linger = 1;
if (setsockopt(client_socket, SOL_SOCKET, SO_LINGER, &l, sizeof(l)) == -1)
abort();
Diese haben keinen Unterschied gemacht. Alle Ideen wären sehr dankbar.
BEARBEITEN - Dies wurde zu einem Thread-Sicherheitsproblem innerhalb einer Warteschlangenbibliothek, was dazu führte, dass der Socket von mehreren Threads falsch behandelt wurde.
Hier ist ein Code, den ich auf vielen Unix-ähnlichen Systemen verwendet habe (z. B. SunOS 4, SGI IRIX, HPUX 10.20, CentOS 5, Cygwin), um einen Socket zu schließen:
int getSO_ERROR(int fd) {
int err = 1;
socklen_t len = sizeof err;
if (-1 == getsockopt(fd, SOL_SOCKET, SO_ERROR, (char *)&err, &len))
FatalError("getSO_ERROR");
if (err)
errno = err; // set errno to the socket SO_ERROR
return err;
}
void closeSocket(int fd) { // *not* the Windows closesocket()
if (fd >= 0) {
getSO_ERROR(fd); // first clear any errors, which can cause close to fail
if (shutdown(fd, SHUT_RDWR) < 0) // secondly, terminate the 'reliable' delivery
if (errno != ENOTCONN && errno != EINVAL) // SGI causes EINVAL
Perror("shutdown");
if (close(fd) < 0) // finally call close()
Perror("close");
}
}
Das oben Gesagte garantiert jedoch nicht, dass gepufferte Schreibvorgänge gesendet werden.
Graceful close: Ich habe ungefähr 10 Jahre gebraucht, um herauszufinden, wie man eine Steckdose schließt. Aber für weitere 10 Jahre rief ich faul nur usleep(20000)
für eine kurze Verzögerung an, um 'sicherzustellen', dass der Schreibpuffer vor dem Schließen geleert wurde. Das ist offensichtlich nicht sehr klug, denn:
usleep()
zu beenden (normalerweise habe ich jedoch usleep()
zweimal aufgerufen, um diesen Fall zu bearbeiten - einen Hack).Aber einen richtigen Flush zu machen ist überraschend schwer. Die Verwendung von SO_LINGER
ist anscheinend nicht der Weg zu gehen; siehe zum Beispiel:
Und SIOCOUTQ
scheint Linux-spezifisch zu sein.
Hinweis shutdown(fd, SHUT_WR)
nicht stoppt das Schreiben, entgegen dem Namen und möglicherweise entgegen dem man 2 shutdown
.
Dieser Code flushSocketBeforeClose()
wartet, bis null Bytes gelesen werden oder der Timer abläuft. Die Funktion haveInput()
ist ein einfacher Wrapper für select (2) und kann bis zu 1/100 Sekunde blockieren.
bool haveInput(int fd, double timeout) {
int status;
fd_set fds;
struct timeval tv;
FD_ZERO(&fds);
FD_SET(fd, &fds);
tv.tv_sec = (long)timeout; // cast needed for C++
tv.tv_usec = (long)((timeout - tv.tv_sec) * 1000000); // 'suseconds_t'
while (1) {
if (!(status = select(fd + 1, &fds, 0, 0, &tv)))
return FALSE;
else if (status > 0 && FD_ISSET(fd, &fds))
return TRUE;
else if (status > 0)
FatalError("I am confused");
else if (errno != EINTR)
FatalError("select"); // tbd EBADF: man page "an error has occurred"
}
}
bool flushSocketBeforeClose(int fd, double timeout) {
const double start = getWallTimeEpoch();
char discard[99];
ASSERT(SHUT_WR == 1);
if (shutdown(fd, 1) != -1)
while (getWallTimeEpoch() < start + timeout)
while (haveInput(fd, 0.01)) // can block for 0.01 secs
if (!read(fd, discard, sizeof discard))
return TRUE; // success!
return FALSE;
}
Anwendungsbeispiel:
if (!flushSocketBeforeClose(fd, 2.0)) // can block for 2s
printf("Warning: Cannot gracefully close socket\n");
closeSocket(fd);
In der obigen Beschreibung ähnelt mein getWallTimeEpoch()
time(),
und Perror()
ist ein Wrapper für perror().
.
Edit: Einige Kommentare:
Mein erstes Eingeständnis ist etwas peinlich. Das OP und Nemo forderten die Notwendigkeit heraus, den internen so_error
vor dem Schließen zu löschen, aber ich kann jetzt keine Referenz dafür finden. Das fragliche System war HPUX 10.20. Nach einem fehlgeschlagenen connect()
wurde durch das Aufrufen von close()
der Dateideskriptor nicht freigegeben, da das System einen ausstehenden Fehler an mich senden wollte. Aber wie die meisten Leute habe ich mich nie darum gekümmert, den Rückgabewert von close.
zu überprüfen. Daher waren mir schließlich die Dateideskriptoren (ulimit -n),
erschöpft, die schließlich meine Aufmerksamkeit erregt haben.
(sehr nebensächlicher Punkt) Ein Kommentator beanstandete die hart codierten numerischen Argumente für shutdown()
und nicht z. SHUT_WR für 1. Die einfachste Antwort lautet, dass Windows unterschiedliche # define/enums verwendet, z. SD_SEND
. Und viele andere Autoren (z. B. Beej) verwenden Konstanten, wie auch viele Altsysteme.
Außerdem setze ich immer und immer FD_CLOEXEC auf alle meine Sockets, da ich in meinen Anwendungen niemals möchte, dass sie an ein Kind weitergegeben werden, und was noch wichtiger ist: Ich möchte nicht, dass ein aufgehängtes Kind mich beeinflusst.
Beispielcode zum Setzen von CLOEXEC:
static void setFD_CLOEXEC(int fd) {
int status = fcntl(fd, F_GETFD, 0);
if (status >= 0)
status = fcntl(fd, F_SETFD, status | FD_CLOEXEC);
if (status < 0)
Perror("Error getting/setting socket FD_CLOEXEC flags");
}
Tolle Antwort von Joseph Quinsey. Ich habe Kommentare zur Funktion haveInput
. Sie fragen sich, wie wahrscheinlich es ist, dass select ein fd zurückgibt, das Sie nicht in Ihr Set aufgenommen haben. Dies wäre ein schwerwiegender OS-Fehler. So würde ich prüfen, ob ich Unit-Tests für die select
-Funktion geschrieben habe, nicht in einer gewöhnlichen App.
if (!(status = select(fd + 1, &fds, 0, 0, &tv)))
return FALSE;
else if (status > 0 && FD_ISSET(fd, &fds))
return TRUE;
else if (status > 0)
FatalError("I am confused"); // <--- fd unknown to function
Mein anderer Kommentar bezieht sich auf den Umgang mit EINTR. Theoretisch könnten Sie in einer Endlosschleife stecken bleiben, wenn select
EINTR zurückgibt, da dieser Fehler die Schleife von vorne beginnen lässt. Angesichts des sehr kurzen Timeouts (0,01) ist dies höchst unwahrscheinlich. Ich denke jedoch, der beste Weg, um damit umzugehen, wäre die Rückgabe von Fehlern an den Aufrufer (flushSocketBeforeClose
). Der Aufrufer kann weiterhin aufrufen, dass haveInput
solange sein Timeout noch nicht abgelaufen ist, und für andere Fehler einen Fehler melden.
ZUSATZ # 1
flushSocketBeforeClose
wird nicht schnell beendet, wenn read
einen Fehler zurückgibt. Es läuft weiter, bis das Zeitlimit abgelaufen ist. Sie können sich nicht auf die select
in haveInput
verlassen, um alle Fehler zu antizipieren. read
hat eigene Fehler (zB: EIO
).
while (haveInput(fd, 0.01))
if (!read(fd, discard, sizeof discard)) <-- -1 does not end loop
return TRUE;
Das klingt für mich wie ein Fehler in Ihrer Linux-Distribution.
Die GNU C-Bibliotheksdokumentation sagt:
Wenn Sie mit dem Socket fertig sind, können Sie einfach seine Datei schließen Deskriptor mit
close
Nichts über das Löschen von Fehlerflags oder das Warten auf das Löschen der Daten oder ähnliches.
Ihr Code ist in Ordnung. Ihr O/S hat einen Fehler.