unidade I Tecnologias de Redes de Computadores PROGRAMAÇÃO COM SOCKETS Olá! Na aula de hoje, faremos um estudo sobre sockets. Esta aula fornecerá uma boa base de como sockets são utilizados para a transmissão de informações entre aplicações cliente/servidor. Ao final da aula, você entenderá a diferença entre programação de sockets TCP e UDP, bem como o fluxo de sua programação em linguagem Java. Tenha uma ótima leitura! INTRODUÇÃO A primeira pergunta que você deve estar se fazendo é: O que é um socket? Para compreender o que é um socket inicialmente temos de entender que a comunicação entre processos de software tornou-se indispensável nos sistemas atuais. Quanto maior a quantidade de processos de software sendo executados, provavelmente maior será a necessidade de comunicação entre eles. O elo de ligação entre os processos do servidor e do cliente é o socket. Ele é a porta na qual os processos enviam e recebem mensagens. Neste sentido, podemos dizer que um socket é a interface entre a camada de aplicação e a de transporte dentro de uma máquina. Existem diversas aplicações cliente/servidor desenvolvidas, onde cliente(s) e servidor podem estar em máquinas diferentes, distantes umas das outras Os aplicativos do cliente e do servidor utilizam protocolos de transporte para se comunicarem. Quando um aplicativo interage com o software de protocolo, ele deve especificar detalhes, como por exemplo, se é um servidor ou um cliente. Além disso, os aplicativos que se comunicam devem especificar detalhes adicionais. Por exemplo, o remetente deve especificar os dados a serem enviados, e o receptor deve especificar onde os dados recebidos devem ser colocados. Analisando este esquema, percebemos que tudo acima da interface do socket, na camada de aplicação, é controlado pelo criador da aplicação. Por outro lado, o controle da camada de transporte é feito pelo Sistema Operacional. Outra característica importante é que, basicamente, temos dois tipos de serviços de transporte via socket: o confiável, orientado a cadeia de bytes (byte stream), e os datagramas não confiáveis. O protocolo na qual implementa-se o primeiro é o TCP, já o segundo é implementado no protocolo UDP. Veremos isto mais detalhadamente na sequência desta aula. HISTÓRICO Na década de 80 nos Estados Unidos, a ARPA (Advanced Research Projects Agency of the Department of Defense) confiou à Berkeley, uma importante universidade da Califórnia, a responsabilidade de construir um sistema operacional que pudesse ser utilizado no suporte à ARPAnet, a rede antecessora da atual Internet. Neste sentido, uma interface foi desenvolvida e adicionada ao sistema operacional Unix BSD (Berkeley Software Distribution). Tal interface tinha justamente a função de suporte à comunicação em rede. Esta interface ficou então conhecida como Berkeley Sockets Interface, e é a base para a maioria das interfaces entre protocolos de Internet TCP/IP existentes. Você já deve ter estudado em disciplinas anteriores que existe uma nomenclatura específica para se referir a cada lado da comunicação. Temos o servidor, que fica esperando por conexões de entrada e que fornece certos tipos de serviços à outra parte. Já o cliente vem a ser quem solicita a conexão ao servidor para fazer alguma requisição, algum pedido. É importante salientar que não é o computador que distingue quem é servidor e quem é cliente, mas sim a forma como certo programa usa os sockets. Às vezes também fazemos confusão no fato de 51
se pensar que um servidor precisa ser um mainframe. Desktops como os que usamos em casa funcionam tanto como cliente quanto como servidor, e é o que ocorre frequentemente. Cada socket tem um endereço único na Internet. Este endereço é formado por um número IP e por um número de porta. Devido às grandes dimensões da Internet, não há como uma pessoa, ou mesmo uma máquina, saber o endereço de todas as outras. Para resolver este problema foi criado protocolo DNS (Domain Name Service). Este protocolo tem a função de traduzir os nomes ou endereços de alto nível das máquinas para o seu respectivo número IP. Assim, ao se passar o endereço de um socket de um servidor, não se passa diretamente seu número IP, mas sim um nome mais fácil de recordar e então o DNS traduz para o endereço real, ou endereço IP. Os sockets podem ser usados para comunicação via qualquer um dos protocolos UDP ou TCP. Assim, é possível termos tanto comunicação orientada a conexão (via TCP), quanta não orientada a conexão (via UDP). O socket abstrai esse conceito, permitindo, assim, a utilização de qualquer um dos meios. PROGRAMAÇÃO DE APLICAÇÕES TCP Inicialmente o cliente deve contatar o servidor. Para isso, o processo servidor já deve estar executando o programa antes de ser contatado além de já ter criado o socket (porta) que aceita o contato do cliente. O cliente contata o servidor criando um socket TCP local e especifica o endereço IP e o número da porta do processo servidor. Quando o servidor é contatado o servidor cria um novo socket para se comunicar com o cliente, permitindo assim a liberação do socket de boas vindas para que possa ser contatado por outros clientes. A I.1 ilustra este conceito. Do ponto de vista da aplicação, a conexão TCP é um fluxo contínuo de dados. A mensagem é fragmentada em pacotes e não há duplicação. A entrega e a ordem dos pacotes são garantidas. Note que a conexão é ponto-aponto, ou seja, existe um remetente e um destinatário conectado por sockets. A figura I.2 ilustra uma aplicação cliente/servidor descrita em linguagem Java. 52
Vamos analisar as linhas de código, em linguagem Java, referentes às instruções que são utilizadas para a aplicação da figura I.2, começando com a aplicação que será hospedada no servidor. No início do programa, as seguintes linhas deverão ser inseridas para importar a biblioteca que contém as classes que são utilizadas em uma aplicação com Socket (java.net.*) e as classes de recepção de informação do teclado, do Socket do cliente ou do servidor (java.io.*). import java.io.*; import java.net.*; Neste ponto, criamos um objeto welcomesocket. Este é o socket do lado do servidor. O valor referente à numero_ da_porta deverá ser substituído pelo número da porta pela qual a aplicação cliente usará para conectar-se com o servidor. Este socket esperará a requisição de conexão de um cliente. ServerSocket welcomesocket = new ServerSocket(numero_da_porta); Como o programa servidor normalmente fica funcionando por tempo indefinido, costuma-se colocar o restante das instruções dentro de um loop infinito, como segue: while(true) {... } 53
Na sequência, a próxima instrução é responsável por criar um objeto connectionsocket do tipo Socket toda vez que um cliente conectar ao servidor. O TCP se encarregará de criar uma conexão virtual direta entre esse socket e o socket do cliente de forma que todos os bytes serão enviados ao servidor na ordem correta. Socket connectionsocket = welcomesocket.accept(); No caso do envio de um objeto do tipo String do cliente para o servidor, devemos utilizar as seguintes instruções para receber os dados do cliente. BufferedReader infodocliente = new BufferedReader(new InputStreamReader(c onnectionsocket.getinputstream()); String mensagemdocliente = infodocliente.readline(); Depois de processar a informação enviada pelo cliente, podemos enviar um outro objeto (mensagem_para_ cliente) do tipo String de volta para o cliente. Isto geralmente é feito para dar retorno, comunicando que o dado foi recebido ou algo similar. DataOutputStream infoparacliente = new DataOutputStream(connectionSocket. getoutputstream()); infoparacliente.writebytes(mensagem_para_cliente); Agora vamos analisar o programa cliente. Como no caso do programa servidor, o código do programa cliente inicia com a importação das bibliotecas que contém as classes de sockets e de envio de informações. import java.io.*; import java.net.*; Primeiramente, criamos o socket que conectará com o servidor. O primeiro parâmetro passado ao construtor é o nome do servidor, por exemplo, 127.0.0.1 se a aplicação servidor estiver rodando no mesmo computador que a aplicação cliente. O segundo parâmetro é o número da porta que é informado ao socket servidor. Socket clientsocket = new Socket( nome_do_servidor, numero_da_porta); Após a criação do socket cliente, precisamos criar os objetos de cadeia que serão ligados ao socket. O objeto infoparaservidor será a cadeia que enviará informações para o servidor. O objeto infodoservidor será a cadeia que receberá informações do servidor. DataOutputStream infoparaservidor = new DataOutputStream(clientSocket.getOutputStream()); BufferedReader infodoservidor = new BufferedReader(new InputStreamReader(client.getInputStream())); 54
Utilizaremos o mesmo modelo anterior do lado servidor. Enviaremos mensagem_do_cliente e receberemos mensagem_para_cliente, dois objetos do tipo String para exemplificar. infoparaservidor.writebytes(mensagem_do_cliente); String mensagem_para_cliente = infodoservidor.readline(); Por fim, podemos fechar o socket, e também a conexão TCP entre cliente e servidor. clientsocket.close(); PROGRAMAÇÃO DE SOCKETS COM UDP A comunicação entre processos também é possível por UDP. Entretanto, UDP é um serviço sem conexão. Com isso, não há o three-way handshaking (ou apresentação de três vias) inicial, e, assim, não há um canal preestabelecido entre os processos. Para que a comunicação entre os processos seja possível, deve-se incorporar ao conjunto de bytes enviados tanto o endereço IP do destino quanto a porta do processo de destino. Este conjunto (bytes + endereço IP + porta) recebe o nome de pacote. Com o pacote criado, ele é colocado na rede através do socket. O processo receptor deverá abrir o pacote para retirar as informações pertinentes. Sendo o UDP um serviço sem conexão, não há garantias de que o pacote realmente chegará ao seu destino. Na comunicação por TCP, não é necessário que todas as cadeias de bytes recebam endereço IP e número de porta, já que existe uma tubulação virtual, pela qual as cadeias fluem, a qual possui estas informações adicionais. Já na comunicação por UDP, não havendo essa tubulação virtual, torna-se necessário que as cadeias de bytes sejam organizadas em pacotes, todos com endereço IP e número de porta. Abaixo temos um exemplo de programação em socket com UDP. Vamos iniciar nossa análise pela parte do servidor. DatagramSocket serversocket = new DatagramSocket(9543); Diferentemente do TCP, aqui criamos um objeto do tipo DatagramSocket e não ServerSocket. Os dados que serão enviados e recebidos pelo servidor passarão todos através deste socket do tipo DatagramSocket. Não se faz necessária a criação de um objeto DatagramSocket para cada nova conexão. Todas as conexões dos clientes passarão por esse único socket serversocket. Isto não é um problema porque cada vez que recebe um pacote, o servidor responde e fica livre novamente, pois não existe conexão entre ambos. Em nosso exemplo, o socket escuta na porta 9543. while (true) {... } Temos um loop infinito, onde o servidor fica esperando o recebimento de pacotes. DatagramPacket receivepacket = new DatagramPacket(receiveData, receivedata.length); serversocket.receive(receivepacket); 55
O objeto receivepacket aloca memória onde serão salvos os dados de entrada, recebidos pelo servidor. O método receive fica esperando o recebimento e armazena o pacote em receivepacket. String sentence = new String(receivePacket.getData()); InetAddress IPAdress = receivepacket.getaddress(); int port = receivepacket.getport(); Por fim, estas linhas desmontam o pacote, retirando as informações necessárias à retransmissão. O objeto sentence recebe os dados. IPAdress é o número de IP do cliente, que é necessário para mandar a resposta assim como o número de porta port. Note que todos estes dados foram enviados pelo cliente. Após o processamento a resposta é enviada para o respectivo endereço IP e número de porta. Do lado do cliente, temos a criação do socket pelo qual será enviado um pacote da seguinte forma: DatagramSocket clientsocket = new DatagramSocket(); Note que a única diferença entre o socket cliente e o servidor é de que para o servidor foi necessário especificar um número de porta, e para o cliente não. Este comando não cria conexão alguma e nem contata o servidor, e é justamente por isso que não se faz necessário especificar nome do servidor nem seu número de porta. InetAddress IPAddress = InetAddress.getByName( hostname ); Na sequência, criamos um objeto para armazenar o número de IP do Server. Hostname é o nome (apelido) do servidor. O método getbyname() faz uma consulta ao servidor DNS para saber qual o número IP do server. byte[] senddata new byte[1024]; byte[] receivedata = new byte[1024]; Estes dois vetores armazenarão os dados enviados e recebidos respectivamente. DatagramPacket sendpacket = new DatagramPacket(sendData, senddata.length, IPAdress, 9543); Esta linha cria o pacote que será enviado ao Server. Nele estão gravados os dados em si, senddata, o tamanho dos dados, senddata.length, o número de IP do Server, IPAdress, e o número da porta onde o Server espera, 9543. clientsocket.send(sendpacket); 56
Depois de tudo pronto, enviamos o pacote através do método send do socket clientsocket. Uma vez enviado o pacote, o cliente fica esperando uma resposta. clientsocket.receive(receivepacket); Quando ocorre o recebimento do pacote, ele é armazenado em receivepacket. Note que sockets UDP são mais rápidos que sockets TCP. Eles são mais simples, porém menos confiáveis. Em UDP, não é necessário abrir conexão, deste modo a comunicação ocorre apenas com o envio da mensagem. Uma mensagem é um datagrama, que é composto de um remetente (sender) e um receptor (receiver) e a mensagem (content). Até a próxima aula! Referências: MENDES, Douglas Rocha. Redes de Computadores: Teoria e Prática. Editora Novatec, 2007. Wikiversidade: Programação com sockets. Disponível em <http://pt.wikiversity.org/wiki/ Sockets e aplicações em rede. Disponível em <http://www.geekvault.org/2008/02/sockets-e-aplicacoes-em-rede/>. Acesso em: 15 Jun. 2011. 57