Author: Johnnie Rose, Jr.
Tutorial do Winsock de Johnnie
Se você propositadamente chegou no meu tutorial do Winsock, provavelmente achou a idéia de seus próprios aplicativos se comunicarem pela Internet como fascinante como um prospecto. Ou, talvez, alguém tenha encontrado a perspectiva igualmente interessante e você foi encarregado de trazer essa visão em realidade. Em ambos os casos, o serviço de rede Winsock e este tutorial irão ajudá-lo a alcançar seus objetivos de empresa comercial, simplesmente explorando o domínio da programação de rede para uso pessoal ou algo intermediário.
Aqui estamos o que vamos cobrir:
- Criando um soquete de escuta: Dado um pequeno exército de funções de rede, podemos construir um programa que espera pacientemente conexões recebidas? (Sim, nós podemos.)
- Fazendo suas próprias conexões: Dado mais algumas funções, podemos criar um programa que seja conectado com sucesso a um servidor de escuta? (Sim, nós podemos.)
- Envio e recebimento: Uma vez que conseguimos uma conexão ativa, como usamos isso para trocar dados entre os dois programas? (Você adivinhou it---send() e recv().)
- Soquetes não bloqueáveis e assíncronos: Como podemos aumentar a eficiência do nosso código implementando um esquema de rede diferente? (Nós trabalhamos com nosso procedimento de janela um pouco.)
- Mais Tutoriais e Links: Quais recursos existem acima e além desse tutorial? I highlight 3 Isso deve mantê-lo ocupado por um tempo (depois de ter digerido meu tutorial, é claro :-).
- Comentários & Criticas: Aqui está a sua oportunidade de Avalie o tutorial, faça perguntas ou publique comentários..
Embora você esteja ansioso para alcançar esse ponto inspirador no qual sua aplicação tenha feito com sucesso sua primeira conexão, esteja ciente dos conceitos por trás do código. Tente evitar simplesmente manipular o código fornecido de acordo com suas necessidades imediatas e, em vez disso, identificar os requisitos de sua aplicação e somente implementar o que parece ser a melhor solução. Isso é o suficiente da minha Zen of Software Development conselhos para agora; vamos fazer alguma programação de rede ...
As aplicações que atendem máquinas externas são chamadas de servidores. As aplicações de servidor escutam os clientes inicializando um ou mais soquetes de escuta. Quando um cliente se conecta a um desses soquetes de escuta, o servidor recebe uma notificação da Winsock, aceita a conexão e começa a despachar e interceptar mensagens de e para o novo cliente. Talvez o método mais simples pelo qual os servidores manipulem múltiplos clientes é gerar um novo tópico para cada conexão de cliente. Este modelo de servidor geralmente utiliza sockets de bloqueio, que pausam temporariamente para aguardar dados recebidos, uma nova conexão e outros eventos de rede. Em primeiro lugar, vamos identificar algumas estruturas que precisaremos para inicializar um soquete de bloqueio:
- WSADATA: Essa estrutura é usada para consultar o sistema operacional para a versão do Winsock que nosso código requer. Um aplicativo chama WSAStartup () para inicializar a Winsock DLL correta.
- SOCKET: Um objeto (na verdade, é definido como um u_int, inteiro não assinado, em winsock.h---good to know for smalltalk at parties) usado por aplicativos para armazenar um identificador de soquete.
- SOCKADDR_IN: Um aplicativo utiliza essa estrutura para especificar como um soquete deve operar. SOCKADDR_IN contém campos para um endereço IP e um número de porta:
struct sockaddr_in { short sin_family; // Protocol type u_short sin_port; // Port number of socket struct in_addr sin_addr; // IP address char sin_zero[8]; // Unused };
O primeiro campo é o tipo de protocolo, que geralmente é AF_INET (TCP / IP). Como um soquete de escuta não está preocupado com o endereço de rede da máquina em que reside, o Winsock atribui automaticamente um endereço de IP e um número de porta para soquetes de escuta após a criação.
Construiremos nosso primeiro servidor de escuta com as estruturas acima e um pequeno exército de funções de rede:
#include <windows.h> #include <winsock.h> #include <stdio.h> #define NETWORK_ERROR -1 #define NETWORK_OK 0 void ReportError(int, const char *); int WINAPI WinMain(HINSTANCE hInst, HINSTANCE hPrevInst, LPSTR lpCmd, int nShow) { WORD sockVersion; WSADATA wsaData; int nret; sockVersion = MAKEWORD(1, 1); // We'd like Winsock version 1.1 // We begin by initializing Winsock WSAStartup(sockVersion, &wsaData); // Next, create the listening socket SOCKET listeningSocket; listeningSocket = socket(AF_INET, // Go over TCP/IP SOCK_STREAM, // This is a stream-oriented socket IPPROTO_TCP); // Use TCP rather than UDP if (listeningSocket == INVALID_SOCKET) { nret = WSAGetLastError(); // Get a more detailed error ReportError(nret, "socket()"); // Report the error with our custom function WSACleanup(); // Shutdown Winsock return NETWORK_ERROR; // Return an error value } // Use a SOCKADDR_IN struct to fill in address information SOCKADDR_IN serverInfo; serverInfo.sin_family = AF_INET; serverInfo.sin_addr.s_addr = INADDR_ANY; // Since this socket is listening for connections, // any local address will do serverInfo.sin_port = htons(8888); // Convert integer 8888 to network-byte order // and insert into the port field // Bind the socket to our local server address nret = bind(listeningSocket, (LPSOCKADDR)&serverInfo, sizeof(struct sockaddr)); if (nret == SOCKET_ERROR) { nret = WSAGetLastError(); ReportError(nret, "bind()"); WSACleanup(); return NETWORK_ERROR; } // Make the socket listen nret = listen(listeningSocket, 10); // Up to 10 connections may wait at any // one time to be accept()'ed if (nret == SOCKET_ERROR) { nret = WSAGetLastError(); ReportError(nret, "listen()"); WSACleanup(); return NETWORK_ERROR; } // Wait for a client SOCKET theClient; theClient = accept(listeningSocket, NULL, // Optionally, address of a SOCKADDR_IN struct NULL); // Optionally, address of variable containing // sizeof ( struct SOCKADDR_IN ) if (theClient == INVALID_SOCKET) { nret = WSAGetLastError(); ReportError(nret, "accept()"); WSACleanup(); return NETWORK_ERROR; } // Send and receive from the client, and finally, closesocket(theClient); closesocket(listeningSocket); // Shutdown Winsock WSACleanup(); return NETWORK_OK; } void ReportError(int errorCode, const char *whichFunc) { char errorMsg[92]; // Declare a buffer to hold // the generated error message ZeroMemory(errorMsg, 92); // Automatically NULL-terminate the string // The following line copies the phrase, whichFunc string, and integer errorCode into the buffer sprintf(errorMsg, "Call to %s returned error %d!", (char *)whichFunc, errorCode); MessageBox(NULL, errorMsg, "socketIndication", MB_OK); }
Uma coisa que você pode notar imediatamente sobre o código é a quantidade de esforço colocada na verificação de erros. Sempre que ocorre um erro, o código obtém um código de erro específico com WSAGetLastError () e armazena o resultado em nret. O código de erro é então enviado juntamente com uma seqüência de caracteres indicando o nome da função falhada para uma função personalizada chamada ReportError (). Lá, uma mensagem de erro é construída e mostrada ao usuário com uma chamada para MessageBox (), que faz parte do WinAPI padrão. Por exemplo, a escutar () falhou com um código de erro de 10093 (definido como WSANOTINITIALISED), a seqüência de erro concluída seria "Ligar para ouvir () retornou o erro 10093!". Você, o desenvolvedor prudente, procuraria o código e descobriria que o erro ocorreu porque uma chamada bem-sucedida para o WSAStartup () ainda não havia sido feita.
Também estão incluídos para NETWORK_ERROR e NETWORK_OK. Estes podem ser úteis ao verificar o valor de retorno de suas próprias funções de rede. Se suas funções retornassem um desses valores, a função de chamada poderia realizar um teste de igualdade simples para revelar quaisquer erros: se (myNetworkingFunction () == NETWORK_ERROR) {...}. A função de chamada pode então obter um código específico com WSAGetLastError () e lidar com o erro em conformidade. Em última análise, a implementação de um bom esquema de tratamento de erros agora salvará muitos dias ou semanas de tempo de desenvolvimento, pois você saberá instantaneamente por que seu programa falhou.
Além de retornar uma nova conexão de cliente, accept () permite que o servidor extraia informações sobre o cliente e não através de métodos que exigem chamadas de função extra ou tempo (o que pode se tornar um problema nos servidores de jogos, onde a velocidade do loop de aceitação é especialmente crítico). Para tirar proveito desta funcionalidade, passe o endereço de um sockaddr_in struct cast para um sockaddr pointer, ou seja, (LPSOCKADDR) e aSockaddrInStructure. Além disso, declare uma variável inteira, defina o valor do int para o tamanho da estrutura sockaddr e passe o endereço do inteiro como o terceiro parâmetro. Se a informação de endereço for devolvida após a chamada de função, o parâmetro de comprimento deve estar presente.
Este não é um servidor, uma vez que espera que apenas um usuário se conecte e depois desconecte imediatamente, mas esse é o design mais básico. Apenas para limpar as coisas, uma chamada para WSAStartup () inclui uma PALAVRA especificando a versão que deseja carregar (neste caso é 1.1) e o endereço de uma estrutura WSADATA. Em seguida, abordaremos como se conectar a outros computadores.
Fazendo suas próprias conexões
Criar um soquete para se conectar a outra pessoa usa a maioria das mesmas funções, com exceção da estrutura HOSTENT:
- HOSTENT: Uma estrutura usada para informar o soquete a qual computador e porta se conectar. Essas estruturas geralmente aparecem como variáveis LPHOSTENT, que são apenas ponteiros para as estruturas HOSTENT. Como você codifica para o Windows, você geralmente descobrirá que qualquer tipo de dados precedido por LP denota que o tipo é realmente um ponteiro para um tipo "base" (por exemplo, LPCSTR é um ponteiro para uma string C, também conhecido como char * ).
Então, vamos direto ao código:
#include <windows.h> #include <winsock.h> #include <stdio.h> #define NETWORK_ERROR -1 #define NETWORK_OK 0 void ReportError(int, const char *); int WINAPI WinMain(HINSTANCE hInst, HINSTANCE hPrevInst, LPSTR lpCmd, int nShow) { WORD sockVersion; WSADATA wsaData; int nret; sockVersion = MAKEWORD(1, 1); // Initialize Winsock as before WSAStartup(sockVersion, &wsaData); // Store information about the server LPHOSTENT hostEntry; hostEntry = gethostbyname("www.yahoo.com"); // Specifying the server by its name; // another option: gethostbyaddr() if (!hostEntry) { nret = WSAGetLastError(); ReportError(nret, "gethostbyname()"); // Report the error as before WSACleanup(); return NETWORK_ERROR; } // Create the socket SOCKET theSocket; theSocket = socket(AF_INET, // Go over TCP/IP SOCK_STREAM, // This is a stream-oriented socket IPPROTO_TCP); // Use TCP rather than UDP if (theSocket == INVALID_SOCKET) { nret = WSAGetLastError(); ReportError(nret, "socket()"); WSACleanup(); return NETWORK_ERROR; } // Fill a SOCKADDR_IN struct with address information SOCKADDR_IN serverInfo; serverInfo.sin_family = AF_INET; // At this point, we've successfully retrieved vital information about the server, // including its hostname, aliases, and IP addresses. Wait; how could a single // computer have multiple addresses, and exactly what is the following line doing? // See the explanation below. serverInfo.sin_addr = *((LPIN_ADDR)*hostEntry->h_addr_list); serverInfo.sin_port = htons(80); // Change to network-byte order and // insert into port field // Connect to the server nret = connect(theSocket, (LPSOCKADDR)&serverInfo, sizeof(struct sockaddr)); if (nret == SOCKET_ERROR) { nret = WSAGetLastError(); ReportError(nret, "connect()"); WSACleanup(); return NETWORK_ERROR; } // Successfully connected! // Send/receive, then cleanup: closesocket(theSocket); WSACleanup(); } void ReportError(int errorCode, const char *whichFunc) { char errorMsg[92]; // Declare a buffer to hold // the generated error message ZeroMemory(errorMsg, 92); // Automatically NULL-terminate the string // The following line copies the phrase, whichFunc string, and integer errorCode into the buffer sprintf(errorMsg, "Call to %s returned error %d!", (char *)whichFunc, errorCode); MessageBox(NULL, errorMsg, "socketIndication", MB_OK); }
A linha mais complicada na listagem é a seguinte:
porque executa várias operações - uma delas relativamente oculta - de uma só vez. Vamos separá-lo passo a passo:
O membro h_addr_list da estrutura HOSTENT é basicamente definido como char ** h_addr_list, que é uma matriz de strings, ou char * 's. gethostbyname () identificou e copiou todos os endereços conhecidos do servidor para esta lista. No entanto, o conceito de múltiplos endereços é fundamentalmente sensato? Na verdade, sim. O seu computador, de fato, tem uma série de endereços de rede geral. Seu endereço de Internet pode ser 205.182.67.96, seu endereço de LAN pode ser 10.0.0.2 e todos os computadores nos quais o Windows está instalado naturalmente têm um endereço "loopback" de 127.0.0.1, usado pelo computador para se referir a si próprio na rede local . O mesmo conceito se aplica ao domínio dos endereços da Internet ou IP, razão pela qual é necessária uma lista em vez de espaço de armazenamento para um único endereço. Observe que o preferido O endereço, ou seja, o endereço mais acessível, é sempre copiado para o primeiro elemento da lista, seguido do segundo ou outros endereços preferidos.
O que é * hostEntry-> h_addr_list fazendo? Você pode adivinhar que o operador de deferência (*) está sendo usado para acessar um único endereço na lista. No entanto, ao não fornecer um índice específico, a operação de desreferência revela automaticamente o primeiro endereço preferido. Essa seção específica é equivalente a * hostEntry-> h_addr_list [0], que é garantido existir, uma vez que o servidor deve ter pelo menos um endereço.
Em seguida, o char * retornado pela operação de desenergização é lançado em um in_addr * ou LPIN_ADDR. Finalmente, outra operação de deferência é realizada para retornar a estrutura in_addr referida pelo ponteiro, que só pode conter um único endereço. A estrutura resultante in_addr é atribuída a serverInfo.sin_addr. A conexão subseqüente () leva o endereço como um parâmetro ao formar uma conexão com o servidor.
Se o endereço IP do servidor for conhecido, um HOSTANTE válido pode ser obtido através do uso de gethostbyaddr () (em oposição a gethostbyname () usado na lista anterior):
LPHOSTENT hostEntry; in_addr iaHost; iaHost.s_addr = inet_addr("204.52.135.52"); hostEntry = gethostbyaddr((const char *)&iaHost, sizeof(struct in_addr), AF_INET); if (!hostEntry) { // Handle accordingly }
Nesse caso, inet_addr () é utilizado para copiar uma string que denote o endereço IP diretamente em uma estrutura in_addr. Posteriormente, o endereço da estrutura é convertido em um char const * conforme exigido por gethostbyaddr (). Ambos os métodos são referidos como a resolução do endereço do servidor, uma vez que o Winsock retorna registros de endereços completos a partir de informações parciais.
Algumas notas adicionais: a porta 80 foi usada simplesmente porque as transferências de páginas da Internet ocorrem por essa porta. Se você enviasse uma string para um servidor web solicitando um arquivo específico e tentasse receber algo de volta, você teria um navegador web muito simples. Claro, essa string deve incluir um comando HTTP completo. É ótimo que possamos ouvir e conectar-se a outros computadores, mas a comunicação também envolve o envio e o recebimento.
O envio é manipulado, convenientemente, pela função send ():
int send( SOCKET s, const char * FAR buf, int len, int flags );
Basicamente, você copiaria o que queria em um buffer e usaria a função send () em um soquete conectado para fazer com que os dados fossem para a outra extremidade:
char buffer[256]; // Declaring a buffer on the stack char *buffer = new char[256]; // or on the heap ZeroMemory(buffer, 256); strcpy(buffer, "Pretend this is important data."); nret = send(theSocket, buffer, strlen(buffer), // Note that this specifies the length of the string; not // the size of the entire buffer 0); // Most often is zero, but see MSDN for other options delete [] buffer; // If and only if the heap declaration was used if (nret == SOCKET_ERROR) { // Get a specific code // Handle accordingly return NETWORK_ERROR; } else { // nret contains the number of bytes sent }
Receber é o mesmo processo, para trás:
char buffer[256]; // On the stack char *buffer = new char[256]; // or on the heap nret = recv(theSocket, buffer, 256, // Complete size of buffer 0); delete [] buffer; // Manipulate buffer, then delete if and only if // buffer was allocated on heap if (nret == SOCKET_ERROR) { // Get a specific code // Handle accordingly return NETWORK_ERROR; } else { // nret contains the number of bytes received }
What's interesting to O interessante é notar que há um botão na barra de ferramentas no Microsoft Outlook com o nome "Enviar / Recv". O "Receber" é abreviado para "Recv" simplesmente para garantir que o botão esteja certo ou o hábito de um programador digitar recv () tantas vezes? Formar suas próprias teorias de conspiração (novamente, bom para smalltalk em festas).
Foi aqui que encontrei um pequeno problema ao escrever meus próprios programas Winsock. O uso de recv () é ótimo quando você sabe exatamente quanto você receberá dados (como em um jogo, onde o primeiro byte pode ser um comando e o próximo byte ser um parâmetro, etc.), mas quando você don Não sei, o que você faz? Se os dados que você está recebendo são encerrados por um caractere de nova linha (um problema comum com clientes Java falando com servidores C), você pode escrever uma função readLine () para capturar tudo até esse personagem. Aqui eu usei:
char * readLine() { vectortheVector; char buffer; int bytesReceived; while (true) { bytesReceived = recv(theSocket, &buffer, 1, 0); if (bytesReceived <= 0) return NULL; if (buffer == '\n') { char *pChar = new char[theVector.size() + 1]; memset(pChar, 0, theVector.size() + 1); for (int f = 0; f < theVector.size(); f++) pChar[f] = theVector[f]; return pChar; } else { theVector.push_back(buffer); } } }
Um vetor é utilizado em vez de uma matriz porque o seu espaço de armazenamento pode ser aumentado automaticamente para se adequar ao comprimento da linha. Se recv () retornar um erro (indicado por bytesReceived sendo menor que zero), NULL é retornado. Uma vez que esta é uma possibilidade, as funções de chamada devem garantir que a string retornada da readLine () seja válida antes de usar. Dentro do loop, um único char é recebido do soquete e, se não um caractere de nova linha, adicionado ao vetor. Se for um caractere de nova linha, o conteúdo do vetor é copiado para uma string C e retornado. A string é declarada como um char maior que o vetor e memset () 'ted to zero para que a linha retornada seja automaticamente encerrada em NULL. Terminar strings com NULL impede erros incomuns e, geralmente, é uma boa prática de programação.
Nor apresenta esta versão inteligentemente aprimorada com suporte para backspaces e a capacidade de mudar o personagem da nova linha facilmente:
// Code originally written by Nor. Modified slightly to // support the MessageBox() API, make logic more readable, // align spacing, and add comments. Posted with permission. #define backKey '\b' // To disable backspaces, #define backKey NULL #define newLine '\n' #define endStr '\0' char *readLine(SOCKET s) { vectortheVector; char buffer; char *pChar; int bytesReceived; while (true) { bytesReceived = recv(s, &buffer, 1, 0); if (bytesReceived <= 0) { MessageBox(NULL, "recv() returned nothing.", "socketIndication", MB_OK); return NULL; } switch (buffer) { case backKey: // Handle backspace if (theVector.size() > 0) theVector.pop_back(); break; case endStr: // If end of string char reached, case newLine: // or if end of line char reached, pChar = new char[theVector.size() + 1]; memset(pChar, 0, theVector.size() + 1); for (int f = 0; f < theVector.size(); f++) pChar[f] = theVector[f]; return pChar; break; default: // Any regular char theVector.push_back(buffer); break; } } }
Soquetes não bloqueáveis e assíncronos
Até este ponto, falamos sobre o bloqueio de sockets, onde chamar uma função, como aceitar (), espera indefinidamente para que um usuário se conecte. Um soquete sem bloqueio retorna imediatamente sempre que é dito para fazer algo, seja com um resultado bem-sucedido, um erro ou nada (indicando que haverá algo para receber mais tarde). A desvantagem de usar este tipo é que você precisará consultar manualmente o soquete para ver se um resultado veio em cada função que você chama. Você pode passar um conjunto de soquetes para a função select () para ver quais estão prontos para leitura, escrita ou erros devolvidos.
As funções que utilizam soquetes assíncronos também retornam imediatamente, mas você pode especificar uma mensagem para enviar ao seu procedimento de janela quando ocorreu um evento especificado. Por exemplo, você pode enviar o Soquete a uma mensagem SOCKET_GOTMSG sempre que receber algo. Normalmente, é inteligente verificar erros (pesados, mas necessários) quando você recebe uma mensagem de soquete para evitar problemas desnecessários mais tarde. Primeiro, vamos definir algumas funções que usaremos para configurar um soquete assíncrono:
- int WSAAsyncSelect ( SOCKET s, HWND hwnd, unsigned int wMsg, long lEvent )
Esta função é usada para identificar um soquete como assíncrono e associar uma mensagem com ele. s é o soquete com o qual você está trabalhando. hwnd é o identificador para a janela que receberá a mensagem quando o soquete gerar um evento. O wMsg é a mensagem que deseja enviar ao seu procedimento de janela (um exemplo é a mensagem SOCKET_GOTMSG acima). O parâmetro lEvent requer uma ou mais bandeiras que informam o soquete sobre quais eventos enviarem sua mensagem. Algumas dessas bandeiras são:- FD_READ: Socket está pronto para receber dados
- FD_WRITE: Socket está pronto para enviar dados
- FD_ACCEPT: Usado em servidores, esta mensagem indica que um usuário conectou
- FD_CONNECT: Usado em aplicativos do cliente, esta mensagem informa que a tomada foi conectada
- FD_CLOSE: O soquete acabou de ser fechado
- WSAGETSELECTERROR ( LPARAM lparam )
Determina se o soquete retornou um erro. Tecnicamente, esta não é uma função, mas uma macro (Você pode realmente gerar smalltalk em festas com esse pequeno fato).
- WSAGETSELECTEVENT ( LPARAM lparam )
Outra macro útil definida no winsock2.h é WSAGETSELECTEVENT (), que é usado para ver exatamente o que o soquete fez.
Então, vamos configurar um soquete assíncrono:
// We begin by creating a flag that Windows will use to contact us when something happens #define THERE_WAS_A_SOCKET_EVENT WM_USER + 100 // WM_USER is a base for custom messages
// Somewhere in our initialization code after CreateWindow (), we call WSAAsyncSelect () WSAAsyncSelect ( theSocket, hwnd, THERE_WAS_A_SOCKET_EVENT, FD_READ | FD_WRITE | FD_CONNECT | ... ); // This translates: Windows, please contact me using the THERE_WAS_A_SOCKET_EVENT flag that I // previously defined whenever there's data to read (FD_READ), or when I'm free to send data // (FD_WRITE), or when I've successfully connected to someone else (FD_CONNECT), or when...etc.
// In our window procedure (the function which handles all the messages that Windows sends to your app) LRESULT WINAPI TheWindowProcedure ( HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam ) { switch ( msg ) { case THERE_WAS_A_SOCKET_EVENT: if ( WSAGETSELECTERROR ( lParam ) ) { // If an error occurred, closesocket ( theSocket ); WSACleanup (); // Shutdown Winsock return NETWORK_ERROR; } switch ( WSAGETSELECTEVENT ( lParam ) ) { // What happened, exactly? case FD_READ: // Receive data break; case FD_WRITE: // Write data break; case FD_CONNECT: // Just connected to server break; case ... // Same setup for other flags break; } break; // other case statements with logic that handles other Windows messages } }
Observe que você não pode definir uma mensagem para cada evento, como SOCKET_GOTMSG para FD_READ e, em seguida, SOCKET_CONNECTED para FD_CONNECT. Isso ocorre porque as chamadas repetidas para WSAAsyncSelect () para configurar cada sinalizador cancelarão os efeitos da última chamada para WSAAsyncSelect ().
Eu escrevi este tutorial em dezembro de 2000, e os sete ou mais anos desde então viram um fluxo constante de visitantes e melhorias. Espero que tenha gostado de ler tanto quanto gostei de escrever: obrigado pelo uso do Tutorial Winsock de Johnnie. O acima é apenas uma breve visão geral das possibilidades que você pode alcançar através do Winsock, e outros fizeram um trabalho muito melhor do que eu ao examinar as especificidades desse assunto:
- Perguntas frequentes sobre o programador do Winsock
- O MadWizard Winsock Networking Tutorial em C ++ (uma versão de montagem também está disponível)
- Uma série de 7 partes na programação de jogos de rede apresentado por flipcode
Eu concordo com Thomas Bleeker (MadWizard) que "a programação de rede parece mais fácil do que é". Não consigo impressionar a importância de praticar o uso dessas funções junto com um depurador para que você possa ver o que está acontecendo. Em última análise, você terá uma compreensão muito melhor de como as coisas funcionam se você entende errado, investigue por que você entendeu errado e experimente o prazer de corrigir isso. Cometer erros, em outras palavras, é como aprendemos.