Vamos criar um servidor HTTP sem a necessidade de bibliotecas externas. Com isso iremos entender o funcionamento do protocolo HTTP e uso de classes Java de conexão.
Para criarmos esse servidor, precisamos ver como funciona o HTTP. O HTTP é um protocolo baseado em requisição e resposta. O cliente faz uma requisição e o servidor envia uma resposta (simples assim). Normalmente ocorre a comunicação na porta 80, mas isso não é regra. A versão atual é a 1.1 e a especificação completa pode ser encontrada em: http://www.w3.org/Protocols/HTTP/1.1/rfc2616.pdf.
Uma requisição normalmente é composta por:
- uma lista especificando o método (GET, POST, PUT, ...), a URI e o protocolo e a versão;
- cabeçalhos de requisição;
- corpo.
De maneira semelhante uma resposta é composta também por três elementos:
- protocolo/versão, código do status e descrição;
- cabeçalhos de resposta;
- corpo.
Para verificar isso, vamos utilizar telnet para fazer uma requisição HTTP para o meu blog. Abra a telnet especificando o endereço e a porta:
telnet www.thiagovespa.com.br 80
Dica: Você pode redirecionar a saída para um arquivo ou para o less para visualizar melhor a resposta dos dados.
Digite a seguinte requsição:
GET /blog/ HTTP/1.1 Accept: text/plain; text/html Accept-Language: pt-br Connection: Keep-Alive Host: www.thiagovespa.com.br User-Agent: Telnet para teste
A linha 1 especifica o tipo do método que no nosso caso é "GET", com a URI "/blog/" e o protocolo "HTTP/1.1". As linhas seguintes são os cabeçalhos da requisição. Não há corpo nessa requisição, pois não estamos enviando nenhuma informação adicional (só queremos a página que responde pela URI /blog/). Se, por exemplo, estivéssemos fazendo um POST, poderíamos passar os valores a serem enviados no post no corpo da requisição.
Ao pular uma linha (CRLF) o servidor entende como fim da requisição e irá enviar a resposta. Você deverá obter algo similar a isso:
HTTP/1.1 200 OK Date: Sun, 06 Feb 2011 13:08:51 GMT Server: Apache/2.2.8 (Ubuntu) DAV/2 SVN/1.4.6 mod_jk/1.2.25 mod_python/3.3.1 Python/2.5.2 PHP/5.2.4-2ubuntu5.14 with Suhosin-Patch mod_ruby/1.2.6 Ruby/1.8.6(2007-09-24) mod_ssl/2.2.8 OpenSSL/0.9.8g mod_perl/2.0.3 Perl/v5.8.8 X-Powered-By: PHP/5.2.4-2ubuntu5.14 Set-Cookie: PHPSESSID=1142b7e285308028ff49c8535e4c3027; path=/ X-Pingback: http://www.thiagovespa.com.br/blog/xmlrpc.php Keep-Alive: timeout=15, max=100 Connection: Keep-Alive Transfer-Encoding: chunked Content-Type: text/html; charset=UTF-8 1fad <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" dir="ltr" lang="pt-BR" xml:lang="pt-BR"> <head profile="http://gmpg.org/xfn/11"> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> ...
A primeira linha é o protocolo da versão com o código de status (200 OK), da linha 2 à linha 10 são os cabeçalhos da resposta e o restante é o corpo com o resultado da página que eu solicitei. Você pode fazer o mesmo pelo Java, criei uma classe simples que faz o trabalho:
package br.com.thiagovespa.http.utils; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.PrintWriter; import java.net.Socket; import java.net.UnknownHostException; /** * Cliente HTTP simples para somente requisições GET * * @author Thiago Galbiatti Vespa - <a * href="mailto:thiago@thiagovespa.com.br">thiago@thiagovespa.com.br</a> * @version 1.0 * */ public class HttpClient { /** * Versão do protocolo utilizada */ public final static String HTTP_VERSION = "HTTP/1.1"; private String host; private int port; /** * Construtor do cliente HTTP * @param host host para o cliente acessar * @param port porta de acesso */ public HttpClient(String host, int port) { super(); this.host = host; this.port = port; } /** * Realiza uma requisição HTTP e devolve uma resposta * @param path caminho a ser feita a requisição * @return resposta do protocolo HTTP * @throws UnknownHostException quando não encontra o host * @throws IOException quando há algum erro de comunicação */ public String getURIRawContent(String path) throws UnknownHostException, IOException { Socket socket = null; try { // Abre a conexão socket = new Socket(this.host, this.port); PrintWriter out = new PrintWriter(socket.getOutputStream(), true); BufferedReader in = new BufferedReader(new InputStreamReader( socket.getInputStream())); // Envia a requisição out.println("GET " + path + " " + HTTP_VERSION); out.println("Host: " + this.host); out.println("Connection: Close"); out.println(); boolean loop = true; StringBuffer sb = new StringBuffer(); // recupera a resposta quando ela estiver disponível while (loop) { if (in.ready()) { int i = 0; while ((i = in.read()) != -1) { sb.append((char) i); } loop = false; } } return sb.toString(); } finally { if (socket != null) { socket.close(); } } } }
Na linha 51 abrimos uma conexão Socket para o endereço e porta especificado e na linha 57, 58 e 59 escrevemos a requisição igual foi feito por telnet. Para executar e visualizar no console a resposta da requisição, execute o seguinte dentro do método main (utilize os devidos try/catch):
HttpClient client = new HttpClient("www.thiagovespa.com.br", 80); System.out.println(client.getURIRawContent("/blog/"));
Para criarmos um servidor que responde à requisições desse tipo, é necessário utilizar a classe ServerSocket. Essa classe é responsável por deixar uma conexão que fica "ouvindo" por requisições de clientes em uma determinada porta. Você pode fazer igual ao descrito na linha 9 e 10 do seguinte código:
public void serve() { ServerSocket serverSocket = null; logger.info("Iniciando servidor no endereço: " + this.host + ":" + this.port); try { // Cria a conexão servidora serverSocket = new ServerSocket(port, 1, InetAddress.getByName(host)); } catch (IOException e) { logger.log(Level.SEVERE, "Erro ao iniciar servidor!", e); return; } logger.info("Conexão com o servidor aberta no endereço: " + this.host + ":" + this.port); // Fica esperando pela conexão cliente while (true) { logger.info("Aguardando conexões..."); Socket socket = null; InputStream input = null; OutputStream output = null; try { socket = serverSocket.accept(); input = socket.getInputStream(); output = socket.getOutputStream(); // Realiza o parse da requisição recebida String requestString = convertStreamToString(input); logger.info("Conexão recebida. Conteúdo: " + requestString); Request request = new Request(); request.parse(requestString); // recupera a resposta de acordo com a requisicao Response response = ResponseFactory.createResponse(request); String responseString = response.respond(); logger.info("Resposta enviada. Conteúdo: " + responseString); output.write(responseString.getBytes()); // Fecha a conexão socket.close(); } catch (Exception e) { logger.log(Level.SEVERE, "Erro ao executar servidor!", e); continue; } } }
Feito isso é só recuperar o input stream para pegar a requisição (linha 26) e escrever a resposta com o output stream (linha 39), igual fizemos via telnet. Fiz um projeto exemplo e na resposta escrevi o seguinte:
@Override public String respond() { StringBuilder sb = new StringBuilder(); // Cria primeira linha do status code, no caso sempre 200 OK sb.append("HTTP/1.1 200 OK").append("\r\n"); // Cria os cabeçalhos sb.append("Date: ").append(HTTP_DATE_FORMAT.format(new Date())) .append("\r\n"); sb.append("Server: Test Server - http://www.thiagovespa.com.br") .append("\r\n"); sb.append("Connection: Close").append("\r\n"); sb.append("Content-Type: text/html; charset=UTF-8").append("\r\n"); sb.append("\r\n"); // Agora vem o corpo em HTML sb.append("<html><head><title>Dummy Response</title></head><body><h1>HttpServer Response</h1>"); sb.append("Method: ").append(request.getMethod()).append("<br/>"); sb.append("URI: ").append(request.getUri()).append("<br/>"); sb.append("Protocol: ").append(request.getProtocol()).append("<br/>"); sb.append("</body></html>"); return sb.toString(); }
Utilizei a porta 8091. Ao acessar a URL: http://localhost:8091/olaMundo pelo browser, o resultado é o seguinte:
Seguindo o mesmo princípio você pode criar qualquer tipo de resposta, exibir páginas html, imagens, etc., de arquivos localizados em disco, exibir conteúdo de banco de dados ou qualquer outro tipo de requisição.
O código com o projeto no eclipse está disponível aqui. Qualquer dúvida ou melhoria é só avisar.
[...] This post was mentioned on Twitter by Bruno Gualda and Rafael Paulino, Thiago G. Vespa. Thiago G. Vespa said: Novo post: Seu próprio servidor HTTP em Java http://j.mp/hdTHAW [...]
Belo post.
Onde posso encontrar material sobre o assunto?
Tenho que fazer um projeto da faculdade igual ao seu, porém queria aprender e não copiar. Baixei seus códigos e não entendi por que tantas classes. Queria entender melhor a estrutura WebService
Afff!!! Depois de um mês procurando algo sobre o assunto, achei a resposta no seu post, mesmo assim, na 4a página de busca do google.
Valeu Brother, salvou minha vida, quer dizer minha nota...
Usei o seu como base e implementei algumas páginas.
Vlw msm.
Cara, muuuuuiiitoooo bom teu post. Fiquei dois dias procurando como fazer um cabeçalho de resposta e encontre no teu blog. Obrigado.