Seu próprio servidor HTTP em Java

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:

 

Dummy Response
Dummy Response

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.

Sobre: Thiago Galbiatti Vespa

Thiago Galbiatti Vespa é mestre em Ciências da Computação e Matemática Computacional pela USP e bacharel em Ciências da Computação pela UNESP. Coordenador de projetos do JavaNoroeste, membro do JCP (Java Community Process), consultor Oracle, arquiteto de software de empresas de médio e grande porte, palestrante de vários eventos e colaborador de projetos open source. Possui as certificações: Oracle Certified Master, Java EE 5 Enterprise Architect – Step 1, 2 and 3; Oracle WebCenter Portal 11g Certified Implementation Specialist; Oracle Service Oriented Architecture Infrastructure Implementation Certified Expert; Oracle Certified Professional, Java EE 5 Web Services Developer; Oracle Certified Expert, NetBeans Integrated Development Environment 6.1 Programmer; Oracle Certified Professional, Java Programmer; Oracle Certified Associate, Java SE 5/SE 6