Social Icons

^^

quarta-feira, 6 de abril de 2011

Utilizando o Winsock em Delphi

É muito comum o uso dos componentes ClientSocket e ServerSocket quando queremos trabalhar com aplicações que utilizam sockets em Delphi.

Neste breve tutorial, iremos abordar o uso da API nativa do Windows para trabalharmos com sockets - WSA ou Winsock API.

Na tentativa de simplificar e melhorar a compreensão do artigo, iremos trabalhar no modo console ao invés do modo gráfico - eu acho console muito estiloso ^^.

Neste artigo, utilizei a versão 7 do Delphi.
Para criar um novo programa console, faça o seguinte:

1. Vá até o Menu File » New » Other (Arquivo, novo, outro);
2. Na aba New (Novo) selecione Console Application (Aplicação Console);
3. OK 

Bem, vejamos um simples programa para console em Delphi:
program programa1; // Nome do programa

{$APPTYPE CONSOLE}

uses
SysUtils;

begin
// Código aqui !
end.
Assim como nas linguagens C, C++, Perl e Visual Basic, por exemplo, devemos incluir headers/módulos/bibliotecas para podermos trabalhar com sockets.

Como iremos trabalhar com o Winsock, deveremos incluir todas as referências necessárias, veja:
program programa1;

{$APPTYPE CONSOLE}

uses
SysUtils,Winsock; // inclui-se a unit Winsock para trabalharmos com a API do Winsock

begin
// Código aqui !
end.
O primeiro passo a ser tomado para que possamos trabalhar com o Winsock, é inicializar sua biblioteca - WSOCK32.DLL - através da função WSAStartup:
program programa1;

{$APPTYPE CONSOLE}

uses
SysUtils,Winsock;

var
wsa: WSADATA;
begin

if(WSAStartup($101,wsa) = -1) then
begin
writeln('Ocorreu um erro ao inicializar o Winsock.');
exit;
end;

end.
Temos nossa primeira variável:
var
wsa: WSADATA;
Uma variável do tipo WSADATA armazena informações sobre a inicialização do Winsock. Esta é passada como segundo argumento da função WSAStartup:
if(WSAStartup($101,wsa) = -1) then // Se ocorrer um erro
begin
writeln('Ocorreu um erro ao inicializar o Winsock.'); // Mostra mensagem
exit; // Encerra
end;
No trecho acima, tentamos inicializar a versão 1.1 do Winsock (o símbolo $ é utilizado para números hexadecimais. $101 = 257).

Veja a sintaxe:
WSAStartup(VERSÃO: WORD,var VARIÁVEL_WSA: WSADATA);

VERSÃO:
versão do winsock a ser inicializada;

VARIÁVEL_WSA:
variável do tipo WSADATA.

A função irá retornar o valor -1 se falhar. Do contrário, retornará ZERO.
A função WSAStartup(), na verdade, requer um ponteiro para uma variável do tipo WSADATA como segundo parâmetro, no entanto, só é necessário passar o nome da variável. Isso se explica devido à declaração do parâmetro:
var VARIÁVEL_WSA: WSADATA);
Quando temos "var" antes do parâmetro, significa que será passado o endereço (ponteiro) da variável, e não o seu valor =)

Muito bem, após inicializarmos o Winsock, podemos utilizar as funções contidas na Winsock API.

Temos agora que criar um socket:
program programa1;

{$APPTYPE CONSOLE}

uses
SysUtils,Winsock;

var
wsa: WSADATA;
sock: integer;
begin

if(WSAStartup($101,wsa) = -1) then
begin
writeln('Ocorreu um erro ao inicializar o Winsock.');
exit;
end;

sock := socket(AF_INET,SOCK_STREAM,0);
if(sock = -1) then
begin
writeln('Ocorreu um erro ao criar o socket.');
exit;
end;


end.
Temos mais uma variável declarada:
sock: integer;
Esta variável irá armazenar a identificação do nosso socket criado. Geralmente, ao invés do tipo Integer (inteiro), variáveis deste tipo são declaradas como SOCKET. Entretanto, "SOCKET" é apenas um "novo nome" para o tipo inteiro, ou seja, o tipo "SOCKET" é, na verdade, o tipo inteiro =)

Vejamos a criação do socket:
sock := socket(AF_INET,SOCK_STREAM,0);
if(sock = -1) then
begin
writeln('Ocorreu um erro ao criar o socket.');
exit;
end;
O trecho acima tenta criar um socket para ser utilizado com o protocolo TCP. Isto é definido pelo segundo parâmetro: SOCK_STREAM.

Sintaxe:
sock := socket(FAMÍLIA: Integer,PROTOCOLO: Integer,TIPO: Integer);

sock:
variável do tipo inteiro que irá identificar o socket;

FAMÍLIA:
família do socket. Embora existam diversas constantes, use-se a AF_INET = INTERNET.

PROTOCOLO:
protocolo com o qual o socket irá trabalhar:

SOCK_STREAM = TCP
SOCK_DGRAM = UDP
SOCK_RAW = RAW

TIPO:
opcional, define opções relacionados ao tipo do protocolo:

IPPROTO_IP = protocolo IP;
IPPROTO_TCP = protocolo TCP;
IPPROTO_UDP = protocolo UDP;
IPPROTO_RAW = protocolo RAW;
IPPROTO_TCP = protocolo ICMP;

O parâmetro só é obrigatório quando estamos trabalhando com raw sockets, podendo ser passado como ZERO, caso contrário.

A função irá retornar o valor -1 se falhar. Do contrário, retornará ZERO.
Iremos começar pelo protocolo TCP por ser mais utilizado

Após termos criado o socket, iremos definir uma tarefa para este: atuar como cliente ou servidor.
Para tal, teremos que configurar este socket de acordo com uma estrutura denominada "SOCKADDR_IN".

Vamos começar definindo o socket para trabalhar com servidor:
program programa1;

{$APPTYPE CONSOLE}

uses
SysUtils,Winsock;

var
wsa: WSADATA;
sock: integer;
addr: sockaddr_in;
begin

if(WSAStartup($101,wsa) = -1) then
begin
writeln('Ocorreu um erro ao inicializar o Winsock.');
exit;
end;

sock := socket(AF_INET,SOCK_STREAM,0);
if(sock = -1) then
begin
writeln('Ocorreu um erro ao criar o socket.');
exit;
end;

addr.sin_family := AF_INET;
addr.sin_port := htons(1234);
addr.sin_addr.S_addr := INADDR_ANY;

if(bind(sock,addr,sizeof(addr)) = -1) then
begin
writeln('Ocorreu um erro ao configurar o socket.');
exit;
end;

if(listen(sock,1) = -1) then
begin
writeln('Ocorreu um erro ao colocar o socket na escuta.');
exit;
end;

sock := accept(sock,nil,nil);
if(sock = -1) then
begin
writeln('Ocorreu um erro ao aceitar uma conexão.');
exit;
end;

writeln('Um cliente conectou-se!');
closesocket(sock);
WSACleanup();

end.
Antes de tudo, para configurarmos um socket, devemos declarar uma estrutura do tipo SOCKADDR_IN:
addr: sockaddr_in;
Vamos ver como o socket é configurado:
addr.sin_family := AF_INET; // Corresponde à família a qual o socket pertence
addr.sin_port := htons(1234); // Corresponde à porta na qual o socket irá aguardar conexões
addr.sin_addr.S_addr := INADDR_ANY; // Permite que o socket aceite conexão de qualquer host
Há dois detalhes que devem ser observados:
addr.sin_port := htons(1234); // Correto

O membro "sin_port", da estrutura "addr", é um inteiro de 2 bytes e requer um valor expresso em network byte. Para convertemos um valor para tal, utilizamos a função htons(). A forma abaixo estaria incorreta:
addr.sin_port := 1234; // INCORRETO =(
O membro "sin_addr" armazena o endereço IP do host remoto.
Este membro, na verdade, pertence a uma estrutura chamada "in_addr" que armazena endereços IP. Quando queremos transformar um IP para network byte, utilizamos a função inet_addr():
addr.sin_addr.S_addr := inet_addr('127.0.0.1'); // Transforma o IP 127.0.0.1 para network byte.
Se você observar bem, utilizamos um membro dentro desta estrutura: "S_addr". Note ainda que seria incorreto fazer:
addr.sin_addr := inet_addr('127.0.0.1'); // Errado ;(
Vamos ver o por quê.
addr -> estrutura sockaddr_in;
sin_addr -> estrutura in_addr dentro de "addr";
S_addr -> membro dentro de "sin_addr", um inteiro de 4 bytes.
A função "inet_addr()" retorna um valor inteiro (também de 4 bytes), expresso em network byte, de um respectivo endereço IP.

Quando tentamos fazer:
addr.sin_addr := inet_addr('127.0.0.1'); // Errado ;(
Estamos querendo atribuir um valor inteiro de 4 bytes de forma direta a uma estrutura, por isso ocorreria o erro.

Já no outro exemplo:
addr.sin_addr.S_addr := inet_addr('127.0.0.1'); // CERTO
O membro "S_addr" (dentro de "sin_addr") é um inteiro de 4 bytes e a função inet_addr() também retorna um inteiro de 4 bytes, por isso a atribuição é válida

É importante notar que só podemos utilizar a função inet_addr() com endereços IP:
addr.sin_addr.S_addr := inet_addr('www.google.com.br'); // INCORRETO!
Veremos como obter o endereço IP de um host pelo seu nome mais adiante.

Após configuramos o socket, devemos chamar a função bind() para prepará-lo no computador local, permitindo-o aguardar conexões:
if(bind(sock,addr,sizeof(addr)) = -1) then
begin
writeln('Ocorreu um erro ao configurar o socket.');
exit;
end;

Vejamos a sintaxe:
bind(sock: Integer; var addr: sockaddr_in; tamanho: Integer);

sock:
nome do nosso socket;

addr:
variável pertencente à estrutura "sockaddr_in";

tamanho:
tamanho da estrutura "sockaddr_in".

A função irá retornar o valor -1 se falhar. Do contrário, retornará ZERO.
Como já havia dito antes, um socket é identificado por uma variável do tipo inteiro. Isso pode ser constatado observando o tipo do primeiro parâmetro: "Integer"

Havia dito também que, quando temos "var" antes de um parâmetro, signfica que iremos passar o endereço de uma variável e não seu valor - é justamente o endereço da variável "addr" que temos que passar.

Veja que, para passar o tamanho da estrutura "sockaddr_in", utilizamos o operador sizeof(). Tanto faz escrever "sizeof(addr)" ou "sizeof(sockaddr_in)" - embora esta última forma seja mais adequada.

Após configurado localmente, podemos colocar o socket em modo de escuta:
if(listen(sock,1) = -1) then
begin
writeln('Ocorreu um erro ao colocar o socket na escuta.');
exit;
end;

No exemplo acima, fazemos com que o socket aguarde uma conexão na porta configurada previamente.

Vejamos a sintaxe da função:
listen(sock: Integer; num: Integer);

sock:
nome do nosso socket;

num:
número de conexões que podem ser aceitas;

Assim como as outras funções, se a função listen() tiver êxito, esta retorna ZERO, senão, -1 é retornado.
O trecho abaixo faz com que o programa fique aguardando até que um pedido de conexão seja feito:
sock := accept(sock,nil,nil);
if(sock = -1) then
begin
writeln('Ocorreu um erro ao aceitar uma conexão.');
exit;
end;

Se, após recebido o pedido de conexão, o valor retornado for -1, é sinal que houve um erro ao aceitar esta conexão.

Veja a sintaxe:
novo_sock := accept(sock: Integer; var pt_addr: PSOCKADDR; pt_tamanho: PInteger);

novo_sock:
uma nova variável (inteiro) que irá armazenar a identificação do novo socket; se o nome do próprio socket for utilizado, não será possível aceitar novas conexões;

sock:
nome do nosso socket;

pt_addr:
ponteiro para uma variável do tipo "SOCKADDR" que irá armazenar informações sobre o cliente que se conectou;

tamanho:
ponteiro para uma variável do tipo inteiro que armazena o tamanho da estrutura "SOCKADDR";

A função irá retornar o valor -1 se falhar. Do contrário, retornará ZERO.
No nosso exemplo, usamos:
sock := accept(sock,nil,nil);
No caso, a variável "sock" será utilizada para armazenar a identificação do novo socket após um pedido de conexão ter sido feito. Observe que, como não iremos armazenar informações sobre o cliente, passamos os dois últimos argumentos como NULOS, isto é, passando o ponteiro nulo: "nil".

Vejamos agora:
closesocket(sock);
WSACleanup();
São duas funções ainda não vistas anteriormente. Utiliza-se a função closesocket() para fechar um socket após seu uso e WSACleanup() para finalizar o uso do Winsock.

Sintaxe:
closesocket(sock);

sock:
nome do socket.
A função WSACleanup() não possui parâmetros.

Mais adiante, quando abordaremos algumas funções, veremos como obter informações do cliente conectado.

No exemplo acima, criamos um socket para atuar com servidor. A seguir, iremos criar um socket para atuar como cliente:
program programa2;

{$APPTYPE CONSOLE}

uses
SysUtils,Winsock;

var
wsa: WSADATA;
sock: integer;
addr: sockaddr_in;
begin

if(WSAStartup($101,wsa) = -1) then
begin
writeln('Ocorreu um erro ao inicializar o Winsock.');
exit;
end;

sock := socket(AF_INET,SOCK_STREAM,0);
if(sock = -1) then
begin
writeln('Ocorreu um erro ao criar o socket.');
exit;
end;

addr.sin_family := AF_INET;
addr.sin_port := htons(1234);
addr.sin_addr.S_addr := inet_addr('127.0.0.1');

if(connect(sock,addr,sizeof(addr)) = -1) then
begin
writeln('Ocorreu um erro ao conectar-se.');
exit;
end;

writeln('Conectado!');
closesocket(sock);
WSACleanup();

end.
Como você pode observar, o código necessário para tal é bem menor
Vejamos as diferenças:
addr.sin_addr.S_addr := inet_addr('127.0.0.1');
A linha acima configura o socket para se conectar no IP "127.0.0.1".

Em seguida, fazemos com que o socket tente conectar-se:
if(connect(sock,addr,sizeof(addr)) = -1) then
begin
writeln('Ocorreu um erro ao conectar-se.');
exit;
end;
A sintaxe da função connect() é similar à da função bind() - que não é necessária quando estamos trabalhando com um socket cliente - veja:
connect(sock: Integer; var addr: sockaddr_in; tamanho: Integer);

sock:
nome do nosso socket;

addr:
variável pertencente à estrutura "sockaddr_in";

tamanho:
tamanho da estrutura "sockaddr_in".

A função irá retornar o valor -1 se falhar. Do contrário, retornará ZERO.
Novamente, fechamos o socket e finalizamos o winsock (respectivamente):
closesocket(sock);
WSACleanup();
O próximo passo é trabalhar com o envio e recebimento de dados através do socket. Quando estamos trabalhando com o protocolo TCP, utilizamos as funções recv() e send(), respectivamente:

Enviando dados:
var
buffer: array[0..99] of char;

begin

buffer := 'Apenas um exemplo!';
send(sock,buffer,strlen(buffer),0);

end.
No exemplo acima, declaramos um array de caracteres (uma string em C) com capacidade de armazenar 100 elementos, isto é, 100 caracteres: buffer.

Escrevemos "Apenas um exemplo!" neste array e o enviamos.

A sintaxe é:
send(sock: Integer; var Buf; tam_buffer: Integer; Flags: Integer);

sock:
nome do nosso socket;

Buf:
array de caracteres ou um ponteiro para char que contém os dados que serão enviados;

tam_buffer:
número de bytes que serão enviados;

flags:
valores opcionais que especificam o modo de envio.

A função, em situação normal, retorna o número de bytes enviados. Se algum erro ocorrer, a função retorna -1.
Note que utilizamos a função "strlen()" para retornar o tamanho do buffer: strlen(buffer);


Recebendo dados:
var
buffer: array[0..99] of char;

begin
ZeroMemory(@buffer,100); // Limpa o buffer, é necessário incluir "Windows" na clásula "Uses".
recv(sock,buffer,100,0);
end.
A sintaxe é:
recv(sock: Integer; var Buf; tam_buffer: Integer; Flags: Integer);

sock:
nome do nosso socket;

Buf:
buffer que irá armazenar os dados recebidos;

tam_buffer:
tamanho do buffer;

flags:
valores opcionais que especificam o modo de recebimento.

A função, em situação normal, retorna o número de bytes recebidos. Se algum erro ocorrer, a função retorna -1.


Vejamos um exemplo:


servidor:
program servidor;

{$APPTYPE CONSOLE}

uses
SysUtils,Winsock, Windows; // Windows -> para usar ZeroMemory()

var
wsa: WSADATA;
sock: integer;
addr: sockaddr_in;
buffer: array[0..100] of char; // Buffer para enviar/receber dados
envia: string; // Armazenar uma string digitada
bytes: integer; // Número de bytes recebidos
begin

if(WSAStartup($101,wsa) = -1) then
begin
writeln('Ocorreu um erro ao inicializar o Winsock.');
exit;
end;

sock := socket(AF_INET,SOCK_STREAM,0);
if(sock = -1) then
begin
writeln('Ocorreu um erro ao criar o socket.');
exit;
end;

addr.sin_family := AF_INET;
addr.sin_port := htons(1234);
addr.sin_addr.S_addr := INADDR_ANY;

if(bind(sock,addr,sizeof(addr)) = -1) then
begin
writeln('Ocorreu um erro ao configurar o socket.');
exit;
end;

if(listen(sock,1) = -1) then
begin
writeln('Ocorreu um erro ao colocar o socket na escuta.');
exit;
end;

sock := accept(sock,nil,nil);
if(sock = -1) then
begin
writeln('Ocorreu um erro ao aceitar uma conexão.');
exit;
end;

bytes := 0; // 0 byte recebido

while(bytes <> -1) do // Enquanto o número de bytes retornando for diferente de -1 = conectado
begin
ZeroMemory(@buffer,100); // Zera buffer
recv(sock,buffer,100,0); // Recebe dados do cliente
writeln(buffer); // Mostra-os
ZeroMemory(@buffer,100); // Limpa o buffer novamente
readln(envia); // Lê uma string
StrLCopy(buffer,PChar(envia),100); // Copia até 100 caracteres para o buffer
send(sock,buffer,strlen(buffer),0); // Envia os dados
end;

// Encerra
closesocket(sock);
WSACleanup();

end.


cliente:
program cliente;

{$APPTYPE CONSOLE}

uses
SysUtils,Winsock, Windows; // Windows -> para usar ZeroMemory()

var
wsa: WSADATA;
sock: integer;
addr: sockaddr_in;
buffer: array[0..100] of char; // Buffer para enviar/receber dados
envia: string; // Armazenar uma string digitada
bytes: integer; // Número de bytes recebidos
begin

if(WSAStartup($101,wsa) = -1) then
begin
writeln('Ocorreu um erro ao inicializar o Winsock.');
exit;
end;

sock := socket(AF_INET,SOCK_STREAM,0);
if(sock = -1) then
begin
writeln('Ocorreu um erro ao criar o socket.');
exit;
end;

addr.sin_family := AF_INET;
addr.sin_port := htons(1234);
addr.sin_addr.S_addr := inet_addr('127.0.0.1');

if(connect(sock,addr,sizeof(addr)) = -1) then
begin
writeln('Ocorreu um erro ao conectar-se.');
exit;
end;

bytes := 0; // 0 byte recebido

while(bytes <> -1) do // Enquanto o número de bytes retornando for diferente de -1 = conectado
begin
ZeroMemory(@buffer,100); // Limpa o buffer
readln(envia); // Lê uma string
StrLCopy(buffer,PChar(envia),100); // Copia até 100 caracteres para o buffer
send(sock,buffer,strlen(buffer),0); // Envia os dados
ZeroMemory(@buffer,100); // Limpa o buffer novamente
recv(sock,buffer,100,0); // Recebe dados do cliente
writeln(buffer); // Mostra-os
end;

// Encerra
closesocket(sock);
WSACleanup();

closesocket(sock);
WSACleanup();

end.
O programa acima é o clássico chat cliente-servidor. Geralmente, exemplos como este são apresentados quando estamos estudando sockets

Veremos agora duas funções para trabalharmos com endereços IP e hostname's:

1) gethostbyname()

Vamos retomar:
addr.sin_addr.S_addr := inet_addr('www.google.com.br'); // INCORRETO!
Ocorreria um erro acima pois "www.google.com.br" não é um endereço IP, e sim um hostname.
Antes de podermos configurar o socket para se conectar neste host, devemos obter o seu IP utilizando a função gethostbyname():
var
...
host: PHostEnt; // PHostEnt = ponteiro para a estrutura hostent
addr: sockaddr_in;
begin
...
host := gethostbyname('www.google.com.br');
if(host = nil) then
writeln('Erro ao resolver o host!')
else
addr.sin_addr := PInAddr(host.h_addr^)^;
Declaramos um ponteiro para a estrutura "hostent" que irá armazenar as informações sobre o computador remoto: "host".

A função retorna um ponteiro nulo (nil) caso não consiga resolver o hostname. O endereço deste host é armazenando no membro "h_addr" desta estrutura, expresso em network byte.

Veja:
addr.sin_addr := PInAddr(host.h_addr^)^;
A linha acima parece ser um pouco complicada, mas não é . Sabemos que "host" é um ponteiro para a estrutura "hostent". O membro "host.h_addr" é um ponteiro para CHAR que aponta para o endereço IP do host, expresso em network byte.

Para acessar o valor de um membro apontado por um pointeiro, fazemos:
ponteiro.membro^ que equivale a host.h_addr^; // retorna o endereço apontado pelo membro "h_addr";
No caso acima, como o membro "h_addr" é um ponteiro e não uma simples variável, o seu valor é o endereço para onde ele aponta.

PInAddr é um ponteiro global para a estrutura "in_addr"(mesmo tipo do membro "sin_addr").

Quando temos "PInAddr(host.h_addr^)", estamos fazendo com que o ponteiro "PInAddr" aponte para o mesmo endereço que "host.h_addr" aponta, isto é, aquele endereço que armazena o endereço IP do host. A diferença é que o valor deste endereço será obtido como próprio para a estrutura "in_addr" e não mais como "CHAR".

Até aí, temos:
addr.sin_addr = ENDEREÇO apontado por "host.h_addr" já obtido como "in_addr";
No entanto, "sin_addr" requer um valor "in_addr" e não um ponteiro, por isso fazemos:
PInAddr(host.h_addr^)^; // Valor "in_addr" do endereço apontado
Portanto, quando temos:
addr.sin_addr := PInAddr(host.h_addr^)^;
Estamos atribuindo ao membro "sin_addr" o valor apontado pelo membro "host.h_addr" convertido para "in_addr".

Para compreender melhor o processo, é recomendável o estudo de ponteiros


2) inet_ntoa()

Com esta função, podemos transformar um IP expresso em network byte para string. Esta função faz o processo inverso da função inet_addr().

Veja:
var
...
addr: sockaddr_in;
begin
...
addr.sin_addr.S_addr = inet_addr("127.0.0.1"); // Converte o IP de string para network byte
writeln('O IP e: ' + inet_ntoa(addr.sin_addr)); // Converte de network byte para string
É importante notar que, para utilizar a função inet_ntoa(), devemos passar um valor do tipo "in_addr" como parâmetro.

Veja outro exemplo:
var
...
host: PHostEnt; // PHostEnt = ponteiro para a estrutura hostent
addr: sockaddr_in;
begin
...
host := gethostbyname('www.google.com.br');
if(host = nil) then
writeln('Erro ao resolver o host!')
else
writeln('O IP e: ' + inet_ntoa(PInAddr(host.h_addr^)^));
No exemplo acima, tentamos resolver o host "www.google.com.br" e mostrar o seu respectivo IP. Como vimos anteriormente, a expressão "PInAddr(host.h_addr^)^" retorna o endereço IP de um host como valor "in_addr" - justamente o tipo de valor requirido pela função inet_ntoa().

FIM da primeira parte
Na continuação do artigo, veremos melhor como trabalhar com a função gethostbyname() e outras funções. Veremos ainda o uso do protocolo UDP.

Pois bem, continuemos com o nosso artigo.

Abaixo segue uma pequena lista de aspectos abordados no artigo anterior:

1) Vimos o que é necessário para trabalhar com a API do Winsock (WSA) no Delphi;

2) Foram mostrados os passos que devem ser seguidos para criar um socket simples, abordando, exclusivamene, o protocolo TCP;

3) Ainda com base no protocolo TCP, foram ilustrados exemplos de um programa que atuava como cliente e um outro servidor;

4) Utilizando as funções send() e recv(), aprendemos a como enviar e receber dados, respectivamente através de um socket;

5) Algumas noções de variáveis, estruturas, funções e conversões;

Neste último item, ficou pendente a explicação do uso de função gethostbyname() aplicada em outras situações, além de outras funções do ramo, como getservbyport(), que também será aborda nesta parte do artigo.

Para começar, vamos retomar o último exemplo da primeira parte do tutorial:
var
...
host: PHostEnt; // PHostEnt = ponteiro para a estrutura hostent
addr: sockaddr_in;
begin
...
host := gethostbyname('www.google.com.br');
if(host = nil) then
writeln('Erro ao resolver o host!')
else
writeln('O IP e: ' + inet_ntoa(PInAddr(host.h_addr^)^));

No exemplo acima, tentamos resolver o host "www.google.com.br" e mostrar o seu respectivo IP. Como vimos anteriormente, a expressão "PInAddr(host.h_addr^)^" retorna o endereço IP de um host como valor "in_addr" - justamente o tipo de valor requirido pela função inet_ntoa().

Como se sabe, devemos especificar dados sobre um host remoto para que seja possível uma conexão entre o computador local e este. Sabemos ainda que, para tanto, devemos utilizar uma estrutura denominada "sockaddr_in", que contém três principais membros:
sin_family -> indica a família do socket;
sin_port -> indica a porta na qual o socket irá atuar;
sin_addr -> indica o endereço utilizado pelo socket, seja este local ou remoto;

Dando ênfase ao último, sabemos que o utilizamos quando queremos, por exemplo, estabelecer uma conexão entre o computador local e um outro remoto cujo IP é 200.123.123.123:
addr.sin_addr.S_addr = inet_addr('200.123.123.123');

Como se sabe, com o auxílio da função inet_addr(), podemos converter um endereço IP escrito na forma de string ('200.123.123.123') para seu valor correspondente na forma in_addr (network byte) que é a adequada.

Existem casos, no entanto, em que não sabemos o endereço IP do computador remoto, e pior ainda: às vezes, até sabemos, entretanto, este IP pode ser dinâmico, isto é, não-fixo. Em situações assim, se tivéssimos que tomar como base apenas o endereço IP de hosts remotos, teríamos grande dificuldades de comunicação. Felizmente, podemos utlizar o "hostname" de um computador como referência, dessa forma, não importa qual endereço IP que este computador esteja utlizando, pois, ao contrário de endereços IPs, o hostname é fixo, ou seja, mesmo que o endereço IP de um servidor seja alterado, através do seu hostname podemos acessá-lo.

Mas a questão é: como aplicar isso em nossos programas? É para responder a essa pergunta que este artigo foi começado com último exemplo da parte 1 do tutorial xD

Vamos fazer uma análise rápida no seguinte trecho:
var
wsa: WSADATA; // Para inicializar o winsock
host: PHostEnt; // PHostEnt = ponteiro para a estrutura hostent
begin

if(WSAStartup($101,wsa) = -1) then
exit; // Encerra se falhar
host := gethostbyname('www.google.com.br');
end.

É um exemplo um pouco que repetitivo, eu diria. Mas continuemos:
var
wsa: WSADATA;
host: PHostEnt; // PHostEnt = ponteiro para a estrutura hostent
begin

if(WSAStartup($101,wsa) = -1) then
exit;

host := gethostbyname('www.google.com.br');
if(host = nil) then
// Erro
else
// Host resolvido com sucesso xD
end.


Quando a função gethostbyname() falha em resolver um hostname, um ponteiro nulo (nil) é retornado. Fazemos o tratamento de erros com base nessa propriedade.
var
wsa: WSADATA;
host: PHostEnt; // PHostEnt = ponteiro para a estrutura hostent
addr: sockaddr_in; // Uma estrutura do tipo sockaddr_in
begin
if(WSAStartup($101,wsa) = -1) then
exit;

host := gethostbyname('www.google.com.br');
if(host = nil) then
exit // Encerra se falhar
else
addr.sin_addr := PInAddr(host.h_addr^)^;

end.


Com base no código acima, você consegue responder a questão inicial xD ?

O que exatamente ocorre é que, ao utilizar a função gethostbyname() passando como parâmetro o hostname 'www.google.com.br', esta função tentará resolver este hostname, isto é, obter seu endereço IP, seu nome oficial, dentre outros dados. Como vimos na parte anterior do texto (e no início desta), a combinação PInAddr(host.h_addr^)^ nos retorna o endereço IP de um host já convertido para "in_addr" que nos possibilita usá-lo como valor para o membro "sin_addr" da estrutura "sockaddr_in".

A partir deste ponto, já se pode utilizar a função connect(), por exemplo, para criar uma conexão com o computador cujo hostname utlizamos. 

Veja o exemplo:
program exemplo;

{$APPTYPE CONSOLE}

uses
SysUtils,Winsock;

var
wsa: WSADATA;
sock: integer;
addr: sockaddr_in;
host: PHostEnt;
begin

if(WSAStartup($101,wsa) = -1) then
begin
writeln('Ocorreu um erro ao inicializar o Winsock.');
exit;
end;

sock := socket(AF_INET,SOCK_STREAM,0);
if(sock = -1) then
begin
writeln('Ocorreu um erro ao criar o socket.');
exit;
end;

host := gethostbyname('www.google.com.br');
if(host = nil) then
begin
writeln('Ocorreu um erro ao resolver o hostname.');
exit;
end;

addr.sin_family := AF_INET;
addr.sin_port := htons(80);
addr.sin_addr := PInAddr(host.h_addr^)^;

if(connect(sock,addr,sizeof(addr)) = -1) then
begin
writeln('Erro ao se conectar.');
exit;
end
else
begin
writeln('Conectado xD');
closesocket(sock);
end;

WSACleanup();
end.


Um exemplo bem simples. Tentamos resolver o hostname 'www.google.com.br' e, posteriormente, utilizamos a função connect() para que o programa tente conectar-se ao host pela porta 80. Se a conexão falhar, o programa simplesmente encerra, do contrário, uma mensagem avisa que a conexão foi feita com sucesso e o socket é fechado logo em seguida.

Uma outra função muito interessante existente na API do Winsock é a getservbyport(). Com esta função, podemos obter o serviço associado a uma determinada porta, como por exemplo, o serviço HTTP que geralmente roda sob a porta 80.

Vejamos um exemplo de uso:
program exemplo;

{$APPTYPE CONSOLE}

uses
SysUtils,Winsock;

var
wsa: WSADATA;
serv: PServEnt; // Ponteiro para a estrutura servent
begin

if(WSAStartup($101,wsa) = -1) then
begin
writeln('Ocorreu um erro ao inicializar o Winsock.');
exit;
end;

serv := getservbyport(htons(80),'tcp'); // Tenta obter o servico associado à porta 80 tcp
if(serv = nil) then // Falha ao obter
writeln('Servico desconhecido')
else 
writeln('Servico: ' + serv.s_name); // Imprime o nome do serviço 

WSACleanup();
end.


Neste exemplo, tentamos obter o nome do serviço associado à porta 80 sob o protocolo TCP. Para tal, usamos a função getservbyport(), cuja sintaxe simpificada é:
getservbyname (porta_em_network_byte,protocolo_string);

porta_em_network_byte:
é a porta utilizada pelo serviço que queremos obter, convertida para network byte (htons());

protocolo_string: 
é o protocolo sob o qual o serviço atua;


Geralmente, quando a função retorna um ponteiro nulo, é porque não foi encontrado um serviço associado à porta especificada. No entanto, a função também pode retornar este tipo de ponteiro mesmo que o serviço exista. Veja como isso é possível:
program exemplo;

{$APPTYPE CONSOLE}

uses
SysUtils,Winsock;

var
wsa: WSADATA;
serv: PServEnt;
begin

{

Comentamos esta parte do código responsável pela inicialização do Winsock;

Como se sabe, sem esta inicialização prévia, todas as outras funções
dependentes retornarão em erro, assim como getservbyname()

if(WSAStartup($101,wsa) = -1) then
begin
writeln('Ocorreu um erro ao inicializar o Winsock.');
exit;
end;
}

serv := getservbyport(htons(80),'tcp');
if (serv = nil) then
writeln('Servico desconhecido.')
else
writeln('Servico: ' + serv.s_name);

WSACleanup();
end.

Provalmente, ao executar o primeiro código, a saída do programa deveria ter sido:
Servico: http

E neste último, a saída seria:
Servico desconhecido

Como se vê no código, o programa não chama pela função WSAStartup() e, por esta razão, a função getservbyname() retornou em erro. Se, por algum motivo, o código fosse aplicado em um programa, o bug estaria evidenciado. 

Para resolver este impasse, podemos utitilizar uma função muito útil: WSAGetLastError(), que nos retorna um inteiro (integer) com último código de erro ocorrido no uso do winsock. 

Veja:
program exemplo;

{$APPTYPE CONSOLE}

uses
SysUtils,Winsock;

var
wsa: WSADATA;
serv: PServEnt;
begin

{

Comentamos esta parte do código responsável pela inicialização
do Winsock;

Como se sabe, sem esta inicialização prévia, todas as outras funçõa
dependentes retoraram em erro, assim como getservbyname()

if(WSAStartup($101,wsa) = -1) then
begin
writeln('Ocorreu um erro ao inicializar o Winsock.');
exit;
end;
}

serv := getservbyport(htons(80),'tcp');

if (serv = nil) and (WSAGetLastError() = 0) then
writeln('Servico desconhecido.')

else if(serv = nil) and (WSAGetLastError() <> 0) then
begin
writeln('Erro na funcao getservbyport(). ID: ' + IntToStr(WSAGetLastError()));
exit
end

else writeln('Servico: ' + serv.s_name);

WSACleanup();
end.


Agora, o programa não mais mostra que o serviço é desconhecido, mas sim que um erro ocorreu e a ID deste erro. Quando a função WSAGetLastError() retorna ZERO, é sinal que nenhum erro ocorreu até o ponto em que foi chamada. Então, a lógica seria:
Se "serv" = nil e "WSALastError()" = 0 -> não ocorreram erros no winsock, no entanto, a função getservbyport() não conseguiu obter o serviço;

Se "serv" = nil e "WSALastError" não for ZERO, ocorreu um erro no uso do Winsock;

Se "serv' não retornar um ponteiro nulo, o serviço foi obtido com sucesso.


No exemplo, foram utilizados if, elseif e else, mas você pode reorganizar a lógica tornando o código mais legível e bonito xD

Uma curiosidade: existe um arquivo no qual existem todos os serviços/portas reconhecidos pelo sistema. Este arquivo é acessado pela função getservbyport() para nos retornar um serviço desejado. No Windows, sua localização é: 

C:\WINDOWS\SYSTEM32\Drivers\etc\services.

Ainda falando sobre erros gerados durante o uso do winsock, a função WSAGetLastError() possui uma outra oposta: WSASetLastError. Esta última, por sua vez, é utilizada para definir a ID do último erro ocorrido. Exemplo:
WSASetLastError(0);

Na linha acima, simplesmente zeramos o status de erro. No final desta página, postarei uma tabela contendo os principais códigos de erro, suas constantes e seus respectivos significados.

Voltando a falar um pouco sobre hostname's. Existe um endereço denominado loopback, que é o próprio endereço da máquina local, geralmente atribuído ao hostname "localhost". No entanto, podemos ter um nome alternativo para esse endereço, como por exemplo, o nome da máquina. É possível obter este nome através de uma função bem simples: gethostname(). Veja:
program exemplo;

{$APPTYPE CONSOLE}

uses
SysUtils,Winsock;

var
wsa: WSADATA;
nome_local: array[0..99] of char;
begin

if(WSAStartup($101,wsa) = -1) then
begin
writeln('Ocorreu um erro ao inicializar o Winsock.');
exit;
end;

if(gethostname(nome_local,100) = 0) then
writeln('Host Name local: ' + nome_local)
else
writeln('Erro ao obter o hostname local.');

WSACleanup();
end.

Observe que declaramos o array de char "nome_local" de 100 caracteres. É neste buffer em que o hostname local do computador será armazenado. A sintaxe da função é:
gethostname(buffer,tamanho);
buffer:
é um array de char no qual o hostname local será armazenado;
tamanho:
é o tamanho do buffer em bytes

Se a função falhar, o valor retornado é -1; em caso de êxito, a função retornará ZERO.


Muito bem. Vamos abordar o uso do protocolo UDP através do winsock. Porém, antes de fazê-lo, vejamos algumas diferenças básica entre os protocolo TCP e este último:

Protocolo TCP

1) É um protocolo orientado a conexão, isto é, um fluxo de dados entre cliente/servidor só poderá ser feito caso haja uma conexão entre estes dois hosts;

2) Por retransmitir pacotes de dados perdidos, é considerado um protocolo confiável no aspecto de entrega/recibo de dados;

3) Como conseqüência das duas características acima, o protocolo TCP é mais lento do que o UDP


Protocolo UDP

1) Não é orientado a conexão, ou seja, são somente necessárias as informações de porta/endereço remoto para que dados possam ser enviados/recebidos

2) Por não ser um protocolo dependente de conexão, não há garantia de entrega de um determinado pacote enviado (ao contrário do TCP) o que torna o protocolo não-confiável.

3) Em contra-partida, o protocolo é mais rápido do que o TCP quando se trata de fluxo de dados. 

Uma vez em que o protocolo UDP não é orientado a conexão, podemos deduzir que o uso da funções connect() e listen()/accept() são desnecessárias em sua aplicação. Como foi dito anteriormente, basta saber o endereço remoto de um host e sua porta para que possamos enviar dados para este, utilizando o protocolo UDP. O mesmo se aplica à entrada de dados. Com o protocolo TCP, utitlizamos, respectivamente, as funções send() e recv(). No protocolo UDP, as coisas mudam um pouco: utiliza-se sendto() para o envio de dados, e recvfrom() para recebermos dados. Segue abaixo um código que mostra a criação de um socket que trabalhe sob o protocolo UDP:
program exemplo;

{$APPTYPE CONSOLE}

uses
SysUtils,Winsock;

var
wsa: WSADATA;
sock: integer;
addr: sockaddr_in;
buffer: array [0..99] of char;

begin

if(WSAStartup($101,wsa) = -1) then
begin
writeln('Ocorreu um erro ao inicializar o Winsock.');
exit;
end;

sock := socket(AF_INET,SOCK_DGRAM,0); // Utilizamos SOCK_DGRAM e não SOCK_STREAM
if(sock = -1) then
begin
writeln('Ocorreu um erro ao criar o socket.');
exit;
end;

addr.sin_family := AF_INET;
addr.sin_port := htons(1234);
addr.sin_addr.S_addr := inet_addr('200.123.123.123');

buffer := 'Enviando um string através do protocolo UDP!';
if(sendto(sock,buffer,strlen(buffer), 0,addr,sizeof(addr)) = -1) then
begin
writeln('Erro ao enviar dados!');
exit;
end;
closesocket(sock);
WSACleanup();
end.

O primeiro ponto a ser observado no código acima é que, ao invés de utilizarmos SOCK_STREAM como segundo parâmetro da função socket(), utilizamos SOCK_DGRAM que é a constante correta para o protocolo UDP.

Nota-se que, no código, apenas especificamos as informações do host e utilizamos a função sendto() para enviar a string contida no array "buffer" para este computador (200.123.123.123). A sintaxe da função sendto() é bastante semelhante à da função send(), com exceção apenas dos dois últimos parâmetros:

send(sock: Integer; var Buf; tam_buffer: Integer; Flags: Integer, addr: sockaddr_in; tam_addr: integer);

sock:
nome do nosso socket;

Buf:
array de caracteres ou um ponteiro para char que contém os dados que serão enviados;

tam_buffer:
número de bytes que serão enviados;

flags:
valores opcionais que especificam o modo de envio.

addr:
variável do tipo sockaddr_in que contém as informações de endereço/porta do host remoto;

tam_addr:
tamanho da estrutura sockaddr_in;

Assim como a função send(), sendto() também retorna o número de bytes enviados. Se algum erro ocorrer, a função retorna -1.

Vejamos um exemplo de um servidor UDP:
program exemplo;

{$APPTYPE CONSOLE}

uses
SysUtils,Winsock,Windows; // ZeroMemory -> definida em Windows

var
wsa: WSADATA;
sock: integer;
addr,addr_remoto: sockaddr_in;
buffer: array [0..99] of char;
tam_addr: integer;
begin

if(WSAStartup($101,wsa) = -1) then
begin
writeln('Ocorreu um erro ao inicializar o Winsock.');
exit;
end;

sock := socket(AF_INET,SOCK_DGRAM,0);
if(sock = -1) then
begin
writeln('Ocorreu um erro ao criar o socket.');
exit;
end;

addr.sin_family := AF_INET;
addr.sin_port := htons(1234);
addr.sin_addr.S_addr := INADDR_ANY ;

if(bind(sock,addr,sizeof(addr))=-1) then
begin
writeln('Erro na funcao bind()');
exit;
end;

ZeroMemory(@buffer,100);
tam_addr := sizeof(addr);
if(recvfrom(sock,buffer,100,0,addr_remoto,tam_addr ) = -1) then
begin
writeln('Erro na funcao recvfrom()');
exit;
end;

writeln('Dados recebidos!' + #10);
writeln('IP: ' + inet_ntoa(addr_remoto.sin_addr));
writeln('Porta: ' + inttostr(ntohs(addr_remoto.sin_port)));
writeln('Dados:' + buffer);

closesocket(sock);
WSACleanup();
end. 

Neste exemplo, o programa utiliza a função bind() para que, posteriormente, possau utilizar a porta 1234 para receber dados de um possível cliente. Vamos ver mais detalhadamente:

addr,addr_remoto: sockaddr_in;
buffer: array [0..99] of char;
tam_addr: integer;

Observe que o código há pontos importantes em negrito. Além da comum declaração da variável "addr", declare-se outra variável, do mesmo tipo (sockaddr_in), denominada "addr_remoto". Esta variável será utilizada como parâmetro para a função recvfrom(), seu uso será explicado mais adiante. 

A variável "buffer" é um array de caracteres de 100 bytes e será utilizada para armazenar os dados recebidos através da função recvfrom().

Por fim, declaramos "tam_addr" para passarmos seu valor como último parâmetro da função. O valor desta variável deve ser o tamanho da estrutura "sock_addr" em bytes.
if(bind(sock,addr,sizeof(addr))=-1) then
begin
writeln('Erro na funcao bind()');
exit;
end;


Nada de novo. Apenas configura-se localmente o socket de tal forma que este utilize a porta 1234 para receber/enviados dados.
ZeroMemory(@buffer,100);
tam_addr := sizeof(sockaddr_in);
if(recvfrom(sock,buffer,100,0,addr_remoto,tam_addr ) = -1) then
begin
writeln('Erro na funcao recvfrom()');
exit;
end;


Neste trecho, preenche-se com zero todo o buffer, ou podemos simplesmente dizer que limpa-se o "buffer" para que ele possa armazenar ocasionais dados. Na próxima linha, estamos atribuindo à variável "tam_addr" o tamanho, em bytes, da estrutura "sockaddr_in". E, desmembrando um pouco mais o código, temos:
if(recvfrom(sock,buffer,100,0,addr_remoto,tam_addr ) = -1) then
begin
writeln('Erro na funcao recvfrom()');
exit;
end;


É no trecho acima que, de fato, aguardamos por dados vindos de algum cliente.

Vejamos a sintaxe:

recvfrom(sock: Integer; var Buf; tam_buffer: Integer; flags: Integer, addr:sockaddr_in, var tam);

sock:
nome do nosso socket;

Buf:
buffer que irá armazenar os dados recebidos;

tam_buffer:
tamanho do buffer;

flags:
valores opcionais que especificam o modo de recebimento.

addr:
variável do tipo sockaddr_in que contém as informações de endereço/porta do host remoto;

tam_addr:
variável que armazena o tamanho da estrutura sockaddr_in;

A função, assim como recv(), retornará o número de bytes recebidos, exceto quando algum erro ocorrer, onde o valor -1 é retornado.


Para finalizar:
writeln('Dados recebidos!' + #10);
writeln('IP: ' + inet_ntoa(addr_remoto.sin_addr));
writeln('Porta: ' + inttostr(ntohs(addr_remoto.sin_port)));
writeln('Dados:' + buffer);


Nesta parte, apenas são mostrados informações sobre um cliente remoto que enviou determinados dados: ip, porta e os dados em si.

A função inet_ntoa() você já conhece: é responsável por transformar um IP expresso em network byte para string.


Uma função que ainda não foi abordada é ntohs(). Esta faz o trabalho inverso ao da função htons(). Enquanto esta última transforma um valor denominado host byte order para network byte order, a função ntohs() obtém um valor em network byte e o transforma em host byte order. Network byte order é o tipo de valor utilizado para comunicação, sobretudo, nas estruturas de sockets, no qual o byte mais significativo (também chamado octeto) é o primeiro. Em host byte order, o byte menos significativo é o primeiro. Exemplo:
Valor em network byte order: 5376
Valor em host byte order: 21 


Vale lembrar que as funções htons() e ntohs() retornam valores numéricos (de 2 bytes). Por esta razão, utilizamos IntToStr() - para conveter de inteiro para string - na seguinte linha:
writeln('Porta: ' + inttostr(ntohs(addr_remoto.sin_port)));

Bem, antes de encerrar esta parte do artigo, irei disponibilizar uma tabela contendo os tipos mais comuns de erros gerados pelo winsock, sobretudo, para serem usados em conjunto com as funções WSAGetLastError() e WSASetLastError():
Código Constante Significado
WSA_INVALID_HANDLE 6 Identificador inválido.
WSA_NOT_ENOUGH_MEMORY 8 Memória insuficiente.
WSA_NOT_ENOUGH_MEMORY 8 Memória insuficiente.
WSA_INVALID_PARAMETER 87 Algum parâmetro inválido foi passado a uma função.
WSAEACCES 10013 Permissão negada.
WSAEFAULT 10014 Endereço inválido.
WSAENOTSOCK 10038 Uma função foi utilizada com um socket inválido.
WSAEPROTOTYPE 10041 O tipo de socket não suporta o uso de uma determinada função.
WSAENOPROTOOPT 10042 Protocolo inválido especificado.
WSAESOCKTNOSUPPORTM 10044 Tipo de socket não suportado.
WSAEPFNOSUPPORT 10046 Família de protocolo não suportada.
WSAEADDRINUSE 10048 Endereço já em uso.
WSAEADDRNOTAVAIL 10049 Erro ao atribuir endereço requisitado.
WSAECONNRESET 10054 Conexão resetada pelo host remoto.
WSAEISCONN 10056 Socket já conectado.
WSAENOTCONN 10057 Socket não conectado.
WSAECONNREFUSED 10061 Conexão recusada.
WSANOTINITIALISED 10093 O winsock não foi inicializado. 
A tabela acima está bem simplificada. 


  Créditos e autoria: Dark_Side

Um comentário:

Popular Posts

- Arquivo -

 

Seguidores

Hora exata:

Total de visualizações de página