Notez que j'ai l'intention de faire de cette réponse un travail en cours reflétant un projet similaire, auquel j'ai consacré énormément d'efforts par le passé, mais en raison de circonstances sans rapport, j'ai perdu du temps. futures mises à jour! J'ai vu quelqu'un dire que le regroupement de connexions est une technique côté client. ... s'il n'y a pas de connexion, qui déclencherait accept() du côté serveur et lancerait un thread? Premièrement, le regroupement de connexions n'est pas seulement une technique côté client; C'est une technique en mode connexion. Cela s'applique aux deux types d'homologue (le "serveur" et le "client").
Deuxièmement, accept
n'a pas besoin d'être appelé pour démarrer un thread. Les programmes peuvent démarrer des threads pour n'importe quelle raison qu'ils aiment ... Ils peuvent démarrer des threads juste pour démarrer plus de threads, dans une boucle de création de thread massivement parallélisée. (edit: nous appelons cela une « bombe fourchette »)
Enfin, une mise en œuvre du fil de mise en commun efficace ne démarrer un thread pour chaque client. Chaque thread occupe généralement entre 512KB et 4MB (espace de pile de comptage et autres informations de contexte), donc si vous avez 10000 clients occupant chacun autant, c'est beaucoup de mémoire gaspillée.
Je veux le faire, mais je ne sais pas comment le faire dans le cas du multithreading.
Vous ne devriez pas utilisation multithreading ici ... Au moins, pas jusqu'à ce que vous avez une solution qui utilise un seul fil, et vous décidez que ce n'est pas assez vite.En ce moment, vous n'avez pas cette information. vous êtes juste devinant, et deviner ne garantit pas l'optimisation.
Au tournant du siècle, il y avait des serveurs FTP qui ont résolu the C10K problem; ils étaient capables de gérer 10 000 clients à tout moment, de les parcourir, de les télécharger ou de les laisser tourner au ralenti, comme les utilisateurs ont tendance à le faire sur des serveurs FTP. Ils ont résolu ce problème non pas en utilisant des fils, mais en utilisant non-blocage et/ou prises asynchrones et/ou des appels.
Pour clarifier, ces serveurs Web géré des milliers de connexions sur un seul thread! Une façon typique est d'utiliser select
, mais je ne suis pas particulièrement friands de cette méthode, car elle nécessite une série plutôt laide de boucles. Je préfère utiliser ioctlsocket
pour Windows et fcntl
pour d'autres systèmes d'exploitation POSIX pour définir le descripteur de fichier en mode non-bloquant, par exemple .:
#ifdef WIN32
ioctlsocket(fd, FIONBIO, (u_long[]){1});
#else
fcntl(fd, F_SETFL, fcntl(fd, F_GETFL, 0) | O_NONBLOCK);
#endif
À ce stade, recv
et read
ne bloque pas lors du fonctionnement sur fd
; Si aucune donnée n'est disponible, ils renvoient une valeur d'erreur immédiatement plutôt que d'attendre l'arrivée des données. Cela signifie que vous pouvez boucler sur plusieurs prises.
Si le regroupement de connexions doit également être implémenté côté serveur, comment puis-je savoir d'où provient une requête?
magasin le client fd
le long du côté de son struct sockaddr_storage
et toute autre information stateful vous devez stocker sur les clients, dans un struct
que vous déclarez cependant que vous ressentez. Si cela finit par être 4KB (qui est un assez grand struct
, généralement environ aussi grand qu'ils doivent obtenir), alors 10000 d'entre eux occuperont seulement environ 40000KB (~ 40MB). Même les téléphones mobiles d'aujourd'hui ne devraient avoir aucun problème à gérer cela. Tenir compte de remplir le code suivant pour vos besoins:
struct client {
struct sockaddr_storage addr;
socklen_t addr_len;
int fd;
/* Other stateful information */
};
#define BUFFER_SIZE 4096
#define CLIENT_COUNT 10000
int main(void) {
int server;
struct client client[CLIENT_COUNT] = { 0 };
size_t client_count = 0;
/* XXX: Perform usual bind/listen */
#ifdef WIN32
ioctlsocket(server, FIONBIO, (u_long[]){1});
#else
fcntl(server, F_SETFL, fcntl(server, F_GETFL, 0) | O_NONBLOCK);
#endif
for (;;) {
/* Accept connection if possible */
if (client_count < sizeof client/sizeof *client) {
struct sockaddr_storage addr = { 0 };
socklen_t addr_len = sizeof addr;
int fd = accept(server, &addr, &addr_len);
if (fd != -1) {
# ifdef WIN32
ioctlsocket(fd, FIONBIO, (u_long[]){1});
# else
fcntl(fd, F_SETFL, fcntl(fd, F_GETFL, 0) | O_NONBLOCK);
# endif
client[client_count++] = (struct client) { .addr = addr
, .addr_len = addr_len
, .fd = fd };
}
}
/* Loop through clients */
char buffer[BUFFER_SIZE];
for (size_t index = 0; index < client_count; index++) {
ssize_t bytes_recvd = recv(client[index].fd, buffer, sizeof buffer, 0);
# ifdef WIN32
int closed = bytes_recvd == 0
|| (bytes_recvd < 0 && WSAGetLastError() == WSAEWOULDBLOCK);
# else
int closed = bytes_recvd == 0
|| (bytes_recvd < 0 && errno == EAGAIN) || errno == EWOULDBLOCK;
# endif
if (closed) {
close(client[index].fd);
client_count--;
memmove(client + index, client + index + 1, (client_count - index) * sizeof client);
continue;
}
/* XXX: Process buffer[0..bytes_recvd-1] */
}
sleep(0); /* This is necessary to pass control back to the kernel,
* so it can queue more data for us to process
*/
}
}
Supposant que vous voulez connexions piscine sur le côté client, le code serait très similaire, sauf évidemment il n'y aurait pas besoin pour le accept
Code concernant la PI . Supposons que vous avez un tableau de client
s que vous voulez connect
, vous pouvez utiliser sans blocage des appels à effectuer toutes les connexions à la fois comme ceci:
size_t index = 0, in_progress = 0;
for (;;) {
if (client[index].fd == 0) {
client[index].fd = socket(/* TODO */);
# ifdef WIN32
ioctlsocket(client[index].fd, FIONBIO, (u_long[]){1});
# else
fcntl(client[index].fd, F_SETFL, fcntl(client[index].fd, F_GETFL, 0) | O_NONBLOCK);
# endif
}
# ifdef WIN32
in_progress += connect(client[index].fd, (struct sockaddr *) &client[index].addr, client[index].addr_len) < 0
&& (WSAGetLastError() == WSAEALREADY
|| WSAGetLastError() == WSAEWOULDBLOCK
|| WSAGetLastError() == WSAEINVAL);
# else
in_progress += connect(client[index].fd, (struct sockaddr *) &client[index].addr, client[index].addr_len) < 0
&& (errno == EALREADY
|| errno == EINPROGRESS);
# endif
if (++index < sizeof client/sizeof *client) {
continue;
}
index = 0;
if (in_progress == 0) {
break;
}
in_progress = 0;
}
En ce qui concerne l'optimisation, étant donné que cela devrait pouvoir Pour gérer 10000 clients avec peut-être quelques modifications mineures, vous ne devriez pas avoir besoin de plusieurs threads.
Néanmoins, en associant des éléments d'une collection mutex
avec client
s et précédant la opération de socket non bloquant avec un non bloquant pthread_mutex_trylock
, les boucles ci-dessus pourrait être adapté pour fonctionner simultanément dans plusieurs threads tandis que le traitement de la même groupe de prises. Cela fournit un modèle de travail pour toutes les plates-formes POSIX, que ce soit Windows, BSD ou Linux, mais ce n'est pas un modèle parfaitement optimal. Pour y parvenir optimalité, nous devons entrer dans le monde asynchrone , qui varie d'un système à:
Il peut verser à codifier que « opération de socket non bloquant » abstraction mentionné précédemment, les deux mécanismes asynchrones varient considérablement quant à leur interface. Comme tout le reste, nous devons malheureusement écrire des abstractions pour que notre code Windows reste lisible sur les systèmes compatibles POSIX. En prime, cela nous permettra de mélanger le traitement du serveur (ie accept
et tout ce qui suit) avec le traitement du client (ie connect
et tout ce qui suit), ainsi notre boucle serveur peut devenir une boucle client (ou vice versa).
Y a-t-il une raison pour ne pas créer une seule connexion au début et continuer à l'utiliser? –
Je veux le faire, mais je ne sais pas comment le faire dans le cas du multithreading. Comme je l'ai dit dans la question, j'utilise accept() pour connaître une demande à venir. Si vous utilisez une connexion toujours ouverte, comment puis-je savoir quand la demande arrive? – Tony
Enquêter sur «boost :: asio», peut vous rendre la vie plus facile ... – Nim