Introduction au socket 1.2
Transcription
Introduction au socket 1.2
Langage C : Introduction aux Sockets Environnement Windows 32 bits Ce tutorial a pour but de vous expliquer le fonctionnement des sockets sous Windows, leur utilisation est légèrement différente sous les systèmes UNIX et dérivés (Linux, BSD, MAC OS) néanmoins le principe de fonctionnement est le même et seuls quelques lignes sont à revoir pour un code porté. 1 – Qu’est-ce qu’un socket ? C’est certainement la question que vous vous posez depuis le début de ce document, je vais vous l’expliquer maintenant. Un socket est ce qui est utilisé en programmation pour envoyer et/ou recevoir des données sur le réseau. Cela sert donc de fil conducteur à votre programme pour communiquer avec un programme distant. 2 – Comment programme-t-on avec les Sockets ? Je n’expliquerais ici que le côté client des sockets. Initialisation de Winsock. Pour commencer il va falloir inclure deux directives préprocesseur supplémentaires celle-ci sont les suivantes : #include <winsock2.h> #pragma comment(lib, "ws2_32.lib") Cela indique qu’on va utiliser la bibliothèque winsock2.h (on est sous Windows ne l’oublions pas) couplée à la bibliothèque ws2_32.lib. Vous n’avez pas à vous préoccuper de ces lignes faites simplement un copier/coller de celles-ci dans votre programme. Sachez tout de même pour votre culture personnelle qu’il existe winsock.h dans ce cas on utilisera alors la lib wsock32.lib mais ceci est sans grande importance. Pour finir d’initialiser Winsock il vous reste deux lignes à implémenter dans votre fonction : WSADATA WSAData; WSAStartup(MAKEWORD(2,0), &WSAData); Ces lignes-ci, vont pour la première déclarer une variable WSADATA nommée WSAData, le type WSADATA est un type définit dans le fichier winsock2.h. La seconde ligne sert à démarrer la bibliothèque Winsock. A la fonction WSAStartup(), on passe deux arguments, le premier MAKEWORD(2,0) indique que vous aller utiliser la version 2 de Winsock vous pouvez remplacer (2,0) par (1,0) si vous utiliser la version 1 de winsock (suivant votre include au début). Le deuxième argument n’est autre que l’adresse de votre variable de type WSADATA crée à la ligne du dessus. ©2007 – Camille Sauvaget – http://camille.caskroot.com Vous êtes priés de ne pas copier et/ou diffuser ce document sans ma permission. Note : vous remarquez qu’en début de programme utilisant les sockets on appelle la fonction WSAStartup() sachez qu’il est recommandé en fin de programme de libérer votre WSA en appelant la fonction WSACleanup() mais uniquement lorsque vous en aurez fini avec les sockets sinon il vous faudra reprendre à la ligne WSADATA WSAData; pour réutiliser un socket plus loin. Arrivé à ce point nous avons terminé l’initialisation de Winsock rien de tel qu’un petit récapitulatif donc. Voici ce que vous devez avoir à ce moment là : #include <winsock2.h> #pragma comment(lib, "ws2_32.lib") int _tmain(int argc, _TCHAR* argv[]) { WSADATA WSAData; WSAStartup(MAKEWORD(2,0), &WSAData); } Notez simplement que dans mon cas cela se situe dans la fonction main de mon programme mais vous pouvez faire de même dans n’importe quelle fonction. Initialisation de votre socket Nous y voilà donc, nous venons d’initialiser la bibliothèque, il nous faut à présent définir notre socket. Pour cela il va falloir déclarer la chose suivante : SOCKET mon_socket; SOCKADDR_IN sin; sin.sin_addr.s_addr sin.sin_family sin.sin_port = inet_addr("127.0.0.1"); = AF_INET; = htons(80); Ouh là ! ca en fait des choses ! Rien de bien compliquer vous allez voir. La première ligne crée mon_socket du type SOCKET, ca ça va ce n’est pas trop dur. Ensuite on crée une structure sin de type SOCKADDR_IN. Ceci va contenir tous les paramètres de votre socket : sin.sin_addr.s_addr : correspond à l’adresse sur laquelle votre socket va se connecter notez qu’on utilise ici la fonction inet_addr() qui converti l’adresse IP en xxx.xxx.xxx.xxx en une variable de type long. Pensez à remplacer 127.0.0.1 par l’IP du serveur sur lequel vous voulez vous connecter j’indiquerais plus loin comment on fait pour faire une requête DNS. sin.sin_family : ce paramètre indique le type de votre socket. Le plus souvent on utilisera AF_INET pour des sockets Internet. Pour votre culture là encore il existe plein de type de socket comme les X.25 qui ne sont plus utilisés aujourd’hui. sin.sin_port : la on spécifie simplement le port de connexion du socket. Pour cela ne pas oublier que le numéro du port doit passer par la fonction htons(). je vais simplement vous expliquer pourquoi, à l’intérieur de votre machine les nombre sont transférés octets par octets en commençant par celui de poids le plus faible sur un réseau c’est le contraire voilà donc pourquoi on utilise la fonction htons(). ©2007 – Camille Sauvaget – http://camille.caskroot.com Vous êtes priés de ne pas copier et/ou diffuser ce document sans ma permission. Votre socket est maintenant paramétré il suffit de le créer. Je ne comprends plus là on a fait notre socket plus haut il faut recommencer ? Non rassurer-vous je vais vous montrer ce qu’on fait à présent. Pour commencer voici les lignes de codes : mon_socket = socket(AF_INET,SOCK_STREAM,0); bind(mon_socket, (SOCKADDR *)&sin, sizeof(sin)); La fonction socket() crée le socket à proprement dit. On lui passe trois arguments AF_INET (ou le type que vous avez mit dans sin_family), SOCK_STREAM on dit que ce sera un socket de type STREAM sachez qu’il existe des SOCK_DGRAM qui correspondent aux paquets UDP vous en aurez donc déduit qu’avec les SOCK_STREAM on fait des paquets TCP. Enfin le dernier paramètre ne vous en souciez pas mettez simplement 0. La deuxième fonction assigne votre socket au port défini dans votre structure SOCKADDR_IN, vous lui passez trois arguments le nom de votre socket, l’adresse de votre structure SOCKADDR_IN « castée » en SOCKADDR d’où le (SOCKADDR *) et le dernier argument la taille de votre structure. Il n’est pas nécessaire d’appeler bind() si notre programme est client cela dit je la mets quand même ça ne coûte rien. Nous y sommes donc votre socket étant crée il vous faut maintenant le connecter. Pour cela la fonction connect() est adaptée. connect(mon_socket, (SOCKADDR *)&sin, sizeof(sin)); Cette fonction est très simple à utiliser, vous lui passez en argument votre socket, l’adresse de votre structure SOCKADDR_IN « castée » en SOCKADDR et la taille de votre structure. Il semblerait qu’à partir de maintenant les préliminaires soient terminés et que vous puissiez envoyer ou recevoir des données via votre socket. Ce n’est pas magnifique çà ? Avant de vous apprendre comment on envoie et on reçoit des données via un socket je me suis dit un petit qu’un résumé commenté ne sera pas de trop. #include <winsock2.h> //On demande d’inclure Winsock2 #pragma comment(lib, "ws2_32.lib") //On dit qu’on va avoir besoin de la lib int _tmain(int argc, _TCHAR* argv[]) { // On crée nos variables SOCKET mon_socket; SOCKADDR_IN sin; //On initialise Winsock WSADATA WSAData; WSAStartup(MAKEWORD(2,0), &WSAData); //On crée une varable WSAData //On initialise Winsock //On défini nos paramètres sin.sin_addr.s_addr = inet_addr("127.0.0.1"); //Ne pas oublier inet_addr sin.sin_family = AF_INET; sin.sin_port = htons(80); //Le port de notre socket avec htons() //On crée le socket ! mon_socket = socket(AF_INET,SOCK_STREAM,0); bind(mon_socket, (SOCKADDR *)&sin, sizeof(sin)); //On connecte notre socket ©2007 – Camille Sauvaget – http://camille.caskroot.com Vous êtes priés de ne pas copier et/ou diffuser ce document sans ma permission. connect(mon_socket, (SOCKADDR *)&sin, sizeof(sin)); //on n’oublie pas cette fonction, enfin ce n’est pas obligatoire WSACleanup(); } N’oubliez pas en C vous devez définir toutes vos variables avant tout appel de fonctions sinon je compilateur vous fera n’importe quoi ! Ceci est un inconvénient du C par rapport au C++. Les commandes essentielles Après avoir crée votre socket il peut être intéressant de lui faire passer des données non ? Voici pour démarrer la commande pour envoyer des données : send(mon_socket, paquet, strlen(paquet), 0); Cette commande envoie donc sur mon_socket, la variable paquet. On précise dans les arguments le socket, la chaîne à envoyer, la longueur de la chaîne que l’on envoie - un petit strlen(paquet) - et le dernier argument là encore mettez simplement 0. Note : Pour utiliser strlen() pensez à inclure string.h à votre projet ! #include <string.h> Et pour recevoir : recv(mon_socket, tampon, sizeof(tampon), 0); Cette commande permet donc de lire sur votre socket. Cette fonction prend en argument le nom du socket à employer, le tampon de réception, la taille de votre tampon (sizeof(tampon) et on en parle plus) et le dernier paramètre pour changer on met 0 . Qu’est ce que le tampon ? En programmation socket vous êtes amené à stocker des chaînes lors de l’exécution de votre programme. Dans le monde informatique le mot utilisé pour cela est tampon, buffer en anglais. Dans notre cas tampon ne sera qu’un tableau de char d’une taille assez grande. Si vous ne voulez pas trop vous souciez de ça mettez un tampon d’1Ko, ça marchera bien dans la plupart des cas. A la fin on ferme notre socket : closesocket(mon_socket) ; Une fois votre échange avec le serveur terminé vous devez fermer votre socket avec la fonction la plus simple qui soit certainement closesocket() qui prend en argument votre socket. Ouf on en a terminé vous savez à peu près tout ce qu’il y a à savoir pour vous débrouiller avec les sockets. Pour conclure ce tutoriel je vous donne un petit exemple de code utilisant les sockets : ©2007 – Camille Sauvaget – http://camille.caskroot.com Vous êtes priés de ne pas copier et/ou diffuser ce document sans ma permission. #include <stdio.h> #include <stdlib.h> #include <winsock2.h> //On demande d’inclure Winsock2 #pragma comment(lib, "ws2_32.lib") //On dit qu’on va avoir besoin de la lib int _tmain(int argc, _TCHAR* argv[]) { // On crée nos variables SOCKET mon_socket; SOCKADDR_IN sin; char tampon[1024] = {0}; //On initialise Winsock WSADATA WSAData; WSAStartup(MAKEWORD(2,0), &WSAData); //On crée une varable WSAData //On initialise Winsock //On défini nos paramètres sin.sin_addr.s_addr = inet_addr("212.27.48.4"); sin.sin_family = AF_INET; sin.sin_port = htons(25); //On crée le socket ! mon_socket = socket(AF_INET,SOCK_STREAM,0); bind(mon_socket, (SOCKADDR *)&sin, sizeof(sin)); //On connecte notre socket connect(mon_socket, (SOCKADDR *)&sin, sizeof(sin)); recv(mon_socket, tampon, sizeof(tampon),0); printf("%s\n", tampon); closesocket(mon_socket); //on n’oublie pas cette fonction, enfin ce n’est pas obligatoire WSACleanup(); } Ce programme se connecte sur un serveur SMTP et affiche le message de connexion à l’écran. Notez qu’aucune vérification n’est faite ce programme n’est donc qu’une présentation grossière de ce qui pourrait être un client SMTP. Requête DNS Si tu me demande smtp.free.fr je te réponds 212.27.48.4…. Jusque là je vous ai expliqué comment vous connecter avec une adresse IP. Il est tout de même préférable lorsqu’on veut que dans notre programme l’utilisateur soit amené à paramétrer la connexion qu’il puisse rentrer un nom de serveur et non pas une IP. Je vais donc vous donner la ou plutôt les fonctions permettant de résoudre un nom en une IP et de façon transparente pour l’utilisateur. Ce paragraphe expose des notions assez avancés en ce qui concerne la syntaxe du code. Voici le prototype de la fonction utilisée : struct hostent *gethostbyname(const char *name); On a donc une fonction gethostbyname() qui prend en argument une chaîne comportant le nom du serveur. C’est donc en appelant la fonction gethostbyname(« www.google.fr ») que vous obtiendrez ©2007 – Camille Sauvaget – http://camille.caskroot.com Vous êtes priés de ne pas copier et/ou diffuser ce document sans ma permission. euh… un pointeur vers une structure hostent aie là ca se complique. Voici le prototype de la structure hostent : struct hostent { char *h_name; char **h_aliases; int h_addrtype; int h_length; char **h_addr_list; }; #define h_addr h_addr_list[0] Qu’y-t-il là dedans ? h_name : n’est autre que le nom que vous avez entré. h_aliases : sont des noms alternatifs de l’hôte. h_addrtype : correspond au type d’adresse (AF_INET là encore). h_length : la longueur de l’adresse en octet. h_addr_list : un tableau comportant toutes les IPs retournées pour un même hôte. h_addr : un pointeur vers la première adresse dans h_addr_list. Pour la suite je ne vais retenir que h_addr et h_name si vous souhaitez vous pouvez toujours faire des tests avec les autres membres de cette structure. Petite précision la fonction renvoie un pointeur NULL si la résolution échoue très facile donc de faire un contrôle d’erreur. Que diriez-vous d’un code exemple pour vous montrer comment faire pour utiliser ces fonctions ? struct hostent *host ; host = gethostbyname( “www.google.fr”) ; if (host != NULL) { printf(“%s\n”, host->h_name); printf(“%s\n”, inet_ntoa(*((struct in_addr *)host->h_addr))); } else exit(1); Ici on déclare donc en premier lieu un pointeur vers structure hostent nommée host. On affecte ensuite à notre pointeur la sortie de la fonction qui rappelez-vous renvoie un pointeur. Je me suis permis à la ligne suivante de vous inclure le test si jamais la résolution à échouée. Si notre pointeur ne vaut pas la valeur NULL, on affiche à l’écran le nom d’hôte et l’adresse IP du serveur. On voit donc qu’il n’y a rien de bien compliqué pour le nom d’hôte, on prend dans notre structure host la valeur pointée par le pointeur h_name. Pour ce qui est de l’adresse IP en revanche c’est très différent, celle-ci est stockée sous la forme d’un char* dans la structure et de plus non formatée xxx.xxx.xxx.xxx. Afin de l’afficher sous ce format on utilise la fonction inet_ntoa() qui elle n’accepte que les struct in_addr. On doit donc avant d’appeler cette fonction « caster » notre h_addr en (struct in_addr*) ce qui nous donne donc ((struct in_addr *)host->h_addr) et on dé-référence le pointeur avec l’étoile d’où *((struct in_addr *)host->h_addr) . Ce format est accepté par inet_ntoa() qui nous affiche l’adresse IP compréhensible par l’être humain. ©2007 – Camille Sauvaget – http://camille.caskroot.com Vous êtes priés de ne pas copier et/ou diffuser ce document sans ma permission. Si vous n’avez pas tout compris ce n’est pas grave mais sachez appliquer ceci, c’est le plus important ! Dernière question : Et si je veux mettre cette IP sur mon socket ? Alors là il n’y a pas plus simple ! Souvenez-vous : sin.sin_addr.s_addr = inet_addr("212.27.48.4"); On met l’IP sous forme compréhensible par l’homme, il nous suffit donc de remplacer l’IP écrite en dur par le résultat de notre fonction soit : sin.sin_addr.s_addr = inet_addr(inet_ntoa(*((struct in_addr *)host->h_addr))); Au passage on enlève les guillemets... Voilà comme toute bonne chose ce tutoriel a une fin. J'espère qu'il vous aura plut N’hésitez pas à me faire part de vos remarques concernant ce document sur mon blog : http://camille.caskroot.com/blog. 5 – Remerciement : Brian "Beej" Hall: http://www.ecst.csuchico.edu/~beej/guide/net/ The Walrus: http://c.developpez.com/WalrusSock/ ©2007 – Camille Sauvaget – http://camille.caskroot.com Vous êtes priés de ne pas copier et/ou diffuser ce document sans ma permission.