2012-03-07 11 views
3

Je suis en train de concevoir un système serveur/client distribué avec C++, dans lequel de nombreux clients envoient des requêtes à plusieurs serveurs via TCP et le serveur lancent un thread pour gérer la requête et renvoyer réponse. Dans mon cas d'utilisation, seul un nombre limité de clients accèdera au serveur et j'ai besoin de très hautes performances. Les données envoyées par le client et le serveur sont toutes petites, mais très fréquentes. Donc créer une connexion et la déchirer après utilisation est cher. Donc, je veux utiliser la mise en cache de connexion pour résoudre ce problème: une fois la connexion créée, elle sera stockée dans un cache pour une utilisation future (supposons que le nombre de clients ne dépassera pas la taille du cache).Comment faire un pool de connexion TCP en C/C++

Ma question est:

  1. j'ai vu quelqu'un a dit que la mise en commun de connexion est une technique côté client. Si ce regroupement de connexions n'est utilisé que du côté client, il doit d'abord se connecter à un serveur et envoyer des données. Cette action de création de connexion déclenche la fonction accept() côté serveur qui retourne un socket socket pour recevoir du client. Ainsi, lorsque le client veut utiliser une connexion existante (en cache), il ne crée pas de nouvelle connexion, mais envoie simplement des données. Le problème est, s'il n'y a pas de connexion, qui déclencherait accept() côté serveur et lancerait un thread?
  2. Si le regroupement de connexions doit également être implémenté côté serveur, comment puis-je savoir d'où provient une requête? Puisque seulement de accept() je peux obtenir l'adresse de client, mais pendant ce temps accept() fait déjà un nouveau socket pour cette demande, donc aucun point d'employer une connexion mise en cache.

Toute réponse et suggestion seront appréciées. Ou quelqu'un peut-il me donner un exemple de pool de connexion ou de cache de connexion?

+0

Y a-t-il une raison pour ne pas créer une seule connexion au début et continuer à l'utiliser? –

+0

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

+0

Enquêter sur «boost :: asio», peut vous rendre la vie plus facile ... – Nim

Répondre

4

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).

Questions connexes