Google

quinta-feira, julho 17, 2008

Como construir um socket em linguagem C e Java

Resumo
Este projeto, o servidor de correio eletrônico, tem como objetivo trabalhar a programação de serviços orientados a conexão, mostrando a criação do circuito virtual e a transferência de dados entre processos servidores e clientes por meio de um protocolo conhecido. A aplicação será chamada de Send E-Mail.

Tecnicamente, o servidor de correio funciona como um serviço por trás de uma porta. Nas outras sessões deste documento, serão apresentados trechos de código que detalham cada processo da criação desse circuito lógico.

Introdução
A comunicação entre processos em máquinas diferentes, muita das vezes em locais diferentes, separadas por centenas de kilômetros de distância, requer a identificação dessas máquinas e a definição de protocolos de comunicação.

A arquitetura TCP/IP provê a estrutura necessária para essa comunicação, através dos sockets. Neste projeto, seguimos alguns passos e conjuntos de propriedades para dar suporte aos serviços orientados a conexão pelos sockets.

Sintaticamente, devemos seguir o seguinte processo:


  1. Criação de um objeto socket, a partir do qual serão criadas as conexões;

  2. Atribuição de um endereço lógico ao socket do processo host. Basicamente é necessário um IP ou um nome de servidor e uma porta;

  3. Preparação do socket para aceitar novas conexões no endereço previamente atribuído;

  4. E por fim, o socket é colocado para aguardar novas conexões.



Veja o diagrama abaixo, que ilustra os processos descritos acima, aplicados aos requisitos do servidor de correio (definidos a diante):



Requisitos do servidor de correios
Primando por facilidade de implementação e definição de protocolo de comunicação, o Send E-Mail trabalha com a mensagem inteira, ou seja, após efetuar a conexão com o servidor, o programa cliente envia toda a mensagem, encapsulada em um XML.

Assim, ao receber os bytes enviados pelo cliente, o servidor cria um processo filho que manipula este XML, armazenando a mensagem em uma estrutura de diretórios, conforme mostrado anteriormente no diagrama.

O servidor de correios Send E-Mail deve:

  1. Receber conexões de programas clientes na porta 1000, intencionalmente em localhost;

  2. Ler mensagem recebida do cliente no seguinte formato:
    <email to=”emailto@domain.com” from=”emailfrom@domain.com”>
    <content subject=”subject of message”>body of message</content>
    </email>

  3. Instanciar processo que recebe mensagem XML e armazena no sistema de arquivos.



Nota: A escolha do endereço localhost é didática. Como o socket é de domínio Internet e a família de endereços é IP, este nome poderia ser qualquer endereço válido, conforme especificação IP.

Veremos a seguir como foram implementados o servidor e o cliente do Send E-Mail.

Implementação do Send E-Mail
Inicialmente o projeto foi desenvolvido na linguagem Java, tanto a implementação do servidor quanto do cliente (inclusive a interface gráfica com usuário). Posteriormente para aprofundar os conhecimentos das diretivas de baixo nível da comunicação por socket, uma nova implementação foi feita, em linguagem C.

Linguagem C
Em C foram criados três projetos: EMailServer, EMailClient e EMail. O EMailServer é o servidor que aguarda conexões de EMailClient. Ao enviar a mensagem, o EMailServer instancia EMail que manipula a mensagem e a armazena no sistema de arquivos.

Há apenas um programa principal em EMailServer. Neste programa, criamos um socket, uma estrutura de endereço, preparamos o socket para receber as conexões do cliente e manipulamos a mensagem.

Precisamos preencher a estrutura do endereço local, ou seja, do servidor que irá escutar as conexões. Dizemos a família de endereços, que aceitaremos conexões de qualquer endereço na porta 1000 (definido em #define SERVER_PORT):

laddr.sin_family = AF_INET;
laddr.sin_addr.s_addr = htonl(INADDR_ANY);
laddr.sin_port = htons(SERVER_PORT);
memset(&(laddr.sin_zero), '\0', 8);


Logo após criamos o socket. Neste ponto, devemos informar o tipo de endereço aceitável, qual seu domínio e o protocolo a usar:

if((sd = socket(AF_INET, SOCK_STREAM, IPPROTO_IP)) < saddr_len =" sizeof(struct" ns =" accept(sd," xmlfrombuffer =" (char" bytes_readed =" read(ns,"> 0) {
total_bytes_readed += bytes_readed;
xmlFromBuffer = strcat(xmlFromBuffer, buffer);
xmlFromBuffer = realloc(xmlFromBuffer, strlen(xmlFromBuffer) + MAX_BUFFER);
}
xml = (char *)calloc(total_bytes_readed, sizeof(char));
memcpy(xml, xmlFromBuffer, total_bytes_readed);
free(xmlFromBuffer);

close(ns);

if(execl("/root/Documents/EMail/Debug/EMail", "", xml, NULL) < 0) { perror("Can't execute EMail!"); exit(-1); } exit(0); }


O EMailClient é um módulo que recebe cinco argumentos: from, to, subject e content. Com esses argumentos, o XML do protocolo de comunicação é montado. Logo após, o EMailClient tenta efetuar conexão com o servidor EMailServer. Em sucesso, todo XML é enviado. Veja:

if(argc < 5) { perror("Usage: EMail ");
exit(-1);
}
if((sd = socket(AF_INET, SOCK_STREAM, IPPROTO_IP)) < sin_family =" AF_INET;" sin_port =" htons(PORT);" sin_addr =" *((struct">h_addr);
bzero(&(saddr.sin_zero),8);

to = argv[1];
from = argv[2];
subject = argv[3];
content = argv[4];

char *CONST_TO = "";
char *CONST_END = "";

xmlToSend = (char *)calloc(strlen(CONST_FROM) + strlen(from) + strlen(CONST_TO) + strlen(to) + strlen(CONST_SUBJECT) + strlen(subject) + strlen(CONST_CONTENT) + strlen(content) + strlen(CONST_END), sizeof(char));

xmlToSend = strcat(xmlToSend, CONST_TO);
xmlToSend = strcat(xmlToSend, to);
...
if(connect(sd, (struct sockaddr *)&saddr, sizeof(struct sockaddr)) < 0) { perror("Client: Connect"); exit(1); } send(sd, xmlToSend, strlen(xmlToSend), 0); close(sd);


O último passo é armazenar a mensagem no sistema de arquivos. Isso é feito pelo módulo EMail. Ele recebe um único argumento, que é a mensagem encapsulada no XML. Ao extrair o atributo to do XML, é criado um diretório dentro de emails (diretório onde todas as mensagens são armazenadas) com o respectivo valor.

Logo após, um arquivo com extensão XML é criado dentro deste novo diretório, cujo nome segue o padrão: .xml. Assim, em tese, é praticamente impossível ter duas mensagens com mesmo nome. E essa tipologia organiza de forma trivial as mensagens por data de recebimento. Veja abaixo o trecho de extração do atributo do XML:


aux_to = strchr(argv[1], quote);
to = (char *)malloc(sizeof(char));
do {
to = realloc(to, (strlen(to) + 1) * sizeof(char));
to[new_source++] = aux_to[source++];
} while(aux_to[source] != '\"');


Linguagem Java
Grande parte do trabalho feito em C é abstraído pela máquina virtual de Java. Sendo assim, algumas características importantes na atribuição do tipo de conexão ou mesmo no estabelecimento do protocolo de comunicação ficam implícitos na implementação de um servidor nesta linguagem.

Devido às facilidades de desenho da interface com o usuário, alguns recursos extras foram adicionados nesta implementação. Por exemplo, é possível definir a porta na qual o servidor será levantando, assim como o diretório padrão onde serão salvas as mensagens.

Adotando o paradigma de programação orientado a objetos, o projeto passa a contar com cinco classes, distribuído em dois pacotes. No pacote br.ufu.facom.mail estão as classes de serviço, responsável por implementar os requisitos funcionais do projeto. Em br.ufu.facom.ui, fica a classe que pinta a interface com o usuário.

A classe EMail.java é um POJO que contém as informações da mensagem. A classe EMailServer.java contém a regra de negócio da conexão: Ao receber uma conexão da classe EMailClient.java essa classe cria uma nova thread para tratar essa conexão e armazenar a mensagem no sistema de arquivos. Esse último serviço é feito pela classe StoreEMail.java.


public class EMail {
private String from;
private String to;
private String subject;
private String content;
private Element message;
...
}


O objeto serverSocket escuta conexões na porta definida pela variável port e cria novas threads para manipular o armazenamento da mensagem no sistema de arquivos. Veja:


public class EMailServer implements Runnable {
...
public void run() {
try {
serverSocket = new ServerSocket(port);
while(!terminate) {
socket = serverSocket.accept();
StoreEMail storeEMail = new StoreEMail(socket, directory);

new Thread(storeEMail).start();
}
} catch (IOException ignored) {
}
}
...
}


O cliente cria um socket no endereço address na porta port, e através de um buffer escreve o XML neste socket e fecha-o. Veja:


public class EMailClient {
...
public void sendEMail() throws Exception {
socket = new Socket(address, port);
out = new DataOutputStream(socket.getOutputStream());
buffer = new StringBuffer();

if(message == null) {
throw new Exception("Não há mensagem a enviar!");
}

prepareMessageInBufferToSend();

out.writeUTF(buffer.toString());

out.flush();
out.close();
socket.close();
}
...
}


Esta classe é invocada pelo servidor na thread que trata as conexões de entrada. Assim como na solução em C, o sistema cria um diretório dentro de emails com o nome do destinatário e armazena a mensagem pela data de recebimento. Veja:


public class StoreEMail implements Runnable {
...
public void run() {
try {
in = new DataInputStream(socket.getInputStream());
buffer.append(in.readUTF());
email = new EMail(buffer);
saveEmail();
in.close();
socket.close();
} catch (IOException e) {
e.printStackTrace();
} catch (JDOMException e) {
e.printStackTrace();
}
}

private void saveEmail() throws IOException {
XMLOutputter out = new XMLOutputter();
File userDirectory = new File(directory, email.getTo());
userDirectory.mkdir();
File emailFile = new File(userDirectory, getNameOfFile(email.getSubject()));
FileWriter writer = new FileWriter(emailFile);
out.output(email.getMessage(), writer);
writer.flush();
writer.close();
}

private String getNameOfFile(String subject) {
StringBuffer nameOfFile = new StringBuffer();

DateFormat dateFormat = new SimpleDateFormat("yyyy_MM_dd_HH_mm_ss");
Date date = new Date();

nameOfFile.append(dateFormat.format(date));
nameOfFile.append(".xml");

return nameOfFile.toString();
}
}


Conclusões
Os serviços orientados a conexão são de extrema valia quando é necessário manter um circuito virtual entre duas pontas, ou seja, garantir a troca mensagens de maneira segura e confiável entre um servidor e um cliente.

Conhecer a abstração que o sistema operacional faz no nível da camada de rede pode ser de fundamental importância para garantir o desempenho de sistemas e evitar erros e desperdícios de espaço e tempo (armazenagem e transferência), em se tratando de aplicações cliente/servidor.

O uso de linguagens de programação deve estar diretamente ligado com o propósito da solução, uma vez que abstrações destas linguagens podem levar a resultados errôneos em aplicações de baixo nível, tal qual a construção de serviços atrás de portas.

Por último, a definição de um protocolo de comunicação para gerir serviços baseados na comunicação por rede é de suma importância, pois pode evitar erros, simplificar a programação e até aperfeiçoar os recursos despendidos no processamento da informação nas duas pontas.

Apêndice
Foram enviados por email os arquivos fonte e executáveis das soluções apresentadas. Veja abaixo detalhes para execução de cada solução.

Para rodar a solução em C (projeto_c.zip)
Para testar a solução escrita em C, rode o EMailServer sem argumentos. Para enviar uma mensagem, rode o EMailClient com os seguintes argumentos: EMailClient . O executável EMail deve estar em /tmp.
Para rodar a solução em Java (projeto_java.zip)
Descompacte o conteúdo em um diretório e digite a seguinte linha:


java -cp forms-1.2.0.jar;jdom.jar;send.jar br.ufu.facom.ui.SendEMail


Nota: A solução em Java possui uma interface amigável com o usuário, na qual é possível listar os e-mails que estão no servidor, assim como lê-los e removê-los. Apesar de ter o botão para iniciar o servidor em C, esse recurso não foi implementado. Para fazer qualquer operação pela interface gráfica, é necessário rodar o servidor. Toda a interface está no idioma Inglês.

Nota: Os arquivos forms-1.2.0.jar e jdom.jar são bibliotecas de terceiros que auxiliam no desenvolvimento da interface e na leitura e escrita de arquivos XML, respectivamente. São necessários para executar a solução em Java.

Referências bibliográficas

  1. Stallings, William. Arquitetura e Organização de Computadores. 1ª edição. Editora: Pearson Ed. ISBN: 8587918532

  2. Tanenbaum, Andrew S. Redes de Computadores. 4ª edição. Editora: Campus. ISBN: 8535211853

  3. Deitel, H. M. Como Programar em C. Editora: LTD. ISBN: 8521611919

  4. http://www.cs.cf.ac.uk/Dave/C/

  5. API specification for version 6 of the Java™ Platform, Standard Edition.



Qualquer dúvida é só falar!
Solicite os fontes por e-mail.

Bom final de semana!

4 comentários:

Oliveira disse...

opa, gostaria de rodar o projeto em java, tem como disponibilizar o projeto?

valew!

Jonata disse...

Qual seu e-mail?

Oliveira disse...

seypher@gmail.com
obrigado!

Anônimo disse...

Se me pudesse enviar o projecto ficaria muita grata.

E-mai:misacfernandes@hotmail.com

Obrigada.