`
qingkangxu
  • 浏览: 43071 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
最新评论

JSP和Servlet那些事儿系列--初探HTTP服务器

阅读更多

  《JSP和Servlet那些事儿 》系列文章旨在阐述Servlet(Struts和Spring的MVC架构基础)和JSP内部原理以及一些比较容易混淆的概念(比如forward和redirect区别、静态include和<jsp:include标签区别等)和使用,本文为系列文章之启蒙篇--初探HTTP服务器,基本能从本文中折射出Tomcat和Apache HTTPD等处理静态文件的原理。敬请关注连载!

 

  在学习Servlet和JSP的过程中,如果对HTTP协议本身以及HTTP服务器运行原理有初步的认识的话,这会使得后边的学习更加容易。HTTP服务器本身的内部原理对于Java而言是比较简单的,就是一个Socket处理;请求的解析就是Socket InputStream的读取和分析,所谓的响应仅仅是按照HTTP协议规定的顺序把字节流写入到Socket OutputStream里面。以下是一个简单的HTTP服务器,希望读者在阅读代码的过程中能够想起RFC2616的一些相关术语、或者能够很容易的理解代码。

 

  一个简单的HTTP服务器需要注意以下几点

  1,HTTP服务器监听主机和端口:也就是Java Socket的监听主机和端口

  2,DocRoot:也就是文档根路径,就是http服务器查找客户端请求资源的根路径。

  3,处理线程:需要有至少一个处理线程用于解析Socket输入流及回写请求到Socket输出流,Socket输出流就是发给客户端的通道

 4,以上配置最好提供配置文件(类似具有HTTP服务功能的tomcat配置文件conf/server.xml,Apache HTTPD的httpd.conf文件)

 

  根据以上几点,Java实现基本的HTTP服务器的思路如下

  1,需要写一个类,代码全局的HTTP服务器配置(端口,线程数等);对应本文中Configure类

  2,需要一个Main类,实质就是主线程,主线程需要绑定ServerSocket用于监听客户端请求,并启动多个处理线程处理客户端Socket请求; 对应本文中的HttpServer类

  3,需要多个处理线程,用于处理主线程分配的Socket处理任务; 对应本文中的ProcessThread类

  4,需要一个专门用于解析HTTP请求的类,该类从Socket中获取到输入流,然后读取输入流中的字节,从而解析出客户端希望请求的资源; 对应本文中的HttpRequest类

  5,需要一个专门回写请求的类,把客户端请求的资源对应的文件流输出到Socket的输出流,如果资源找不到的话,就返回404给客户端; 对应本文中的HttpResponse类

  以下是全部的代码:

 

  常量类:

    主要定义了一些常量,比如HTTP服务器默认的监听主机和端口、默认的文档根路径、默认处理线程数、默认配置文件等。

 

package lesson1.server;

import java.util.HashMap;

public final class Constants {
    /**
     * Listener's default values.
     */
    public final static String DEFAULT_HOST = "localhost";
    public final static int DEFAULT_PORT = 8080;
    public final static String DEFAULT_DOC_ROOT = "./webapps";
    
    /**
     * Default work thread count. 
     */
    public final static int DEFAULT_WORKER_COUNT = 10;

    public static final byte CR = (byte) '\r';
    public static final byte LF = (byte) '\n';
    public static final byte SP = (byte) ' ';
    public static final byte HT = (byte) '\t';
    public static final String CRLF = "\r\n";
    public static final byte COLON = (byte) ':';
    
    public static final String DEFAULT_CHARACTER_ENCODING="ISO-8859-1";

    public static final int HTTP_CODE_200 = 200;
    public static final int HTTP_CODE_403 = 403;
    public static final int HTTP_CODE_404 = 404;
    public static final int HTTP_CODE_500 = 500;
    public static final int HTTP_CODE_503 = 503;

    /**
     * 定义HTTP Response Code对应的Message
     */
    public static HashMap<Integer, String> CODE2MESSAGE = new HashMap<Integer, String>();
    static{
        CODE2MESSAGE.put(HTTP_CODE_200, "OK");
        CODE2MESSAGE.put(HTTP_CODE_403, "Forbidden");
        CODE2MESSAGE.put(HTTP_CODE_404, "Not Found");
        CODE2MESSAGE.put(HTTP_CODE_500, "Internal Server Error");
        CODE2MESSAGE.put(HTTP_CODE_503, "Service Unavailable");
    }

    /**
     * 定义MIME Type
     */
    public static HashMap<String, String> MIMETYPES = new HashMap<String, String>();
    static{
        MIMETYPES.put("html", "text/html");
        MIMETYPES.put("htm", "text/html");
        MIMETYPES.put("txt", "text/plain");
        MIMETYPES.put("xml", "application/xml");
        MIMETYPES.put("js", "text/javascript");
        MIMETYPES.put("css", "text/css");
        MIMETYPES.put("jpe", "image/jpeg");
        MIMETYPES.put("jpeg", "image/jpeg");
        MIMETYPES.put("jpg", "image/jpeg");
    }
    
    public static final String DEFAULE_CONFIG_FILE ="server.properties";    
    public static final String CONFIG_HOST = "host";
    public static final String CONFIG_PORT ="port";
    public static final String CONFIG_DOCROOT ="docRoot";
    public static final String CONFIG_THREAD_COUNT ="threadCount";
    
}

  

 

  全局配置类:

    该类主要负责从配置文件中读取到HTTP服务器的监听主机、端口、DocRoot等重要信息。HTTP服务器的其他代码全部都会应用这个类的属性。

 

package lesson1.server;

import java.io.FileInputStream;
import java.io.InputStream;
import java.util.Properties;

/**
 * HTTP服务器全局配置项
 * 
 * @author sta
 * 
 */
public class Configure {

    // listening host
    private String host = Constants.DEFAULT_HOST;
    // listening port
    private int port = Constants.DEFAULT_PORT;
    // Document Root which locate the static resource
    private String docRoot = Constants.DEFAULT_DOC_ROOT;

    /**
     * Http Server config file path
     */
    private String configFile = Constants.DEFAULE_CONFIG_FILE;

    /**
     * Worker thread count.
     */
    private int workerCount = Constants.DEFAULT_WORKER_COUNT;

    // 发送HTTP响应的缓冲区大小
    private static final int DEFAULT_SEND_BUFFER_SIZE = 8 * 1024; // default 8k
    private int sendBufferSize = DEFAULT_SEND_BUFFER_SIZE;

    private static final Configure instance = new Configure();

    // for singleton
    private Configure() {
        Properties properties = new Properties();
        InputStream in = null;
        try {
            in = new FileInputStream(configFile);
            properties.load(in);

            setHost(properties.getProperty(Constants.CONFIG_HOST));
            setPort(Integer.parseInt(properties.getProperty(Constants.CONFIG_PORT)));
            setDocRoot(properties.getProperty(Constants.CONFIG_DOCROOT));
            setWorkerCount(Integer.parseInt(properties
                    .getProperty(Constants.CONFIG_THREAD_COUNT)));
        } catch (Exception e) {
            System.out.println("Failed to load the config from file["
                    + configFile
                    + "], Http Server will use the default config.");
            e.printStackTrace();
        } finally {
            try {
                in.close();
            } catch (Exception e) {
            }
        }
    }

    public String getHost() {
        return host;
    }

    public void setHost(String host) {
        this.host = host;
    }

    public int getPort() {
        return port;
    }

    public void setPort(int port) {
        this.port = port;
    }

    public String getDocRoot() {
        return docRoot;
    }

    public void setDocRoot(String docRoot) {
        this.docRoot = docRoot;
    }

    public static Configure getInstance() {
        return instance;
    }

    public int getWorkerCount() {
        return workerCount;
    }

    public void setWorkerCount(int workerCount) {
        this.workerCount = workerCount;
    }

    public int getSendBufferSize() {
        return sendBufferSize;
    }

    public void setSendBufferSize(int sendBufferSize) {
        this.sendBufferSize = sendBufferSize;
    }
}

  Main类:

    该类中定义了一个队列(taskQueue)用于保存客户端请求对应的Socket;

    初始化方法中启动多个处理线程,并同时把taskQueue的引用传递给处理线程;

    run方法主要是绑定ServerSocket监听,然后while循环不断接受客户端请求,客户端请求对应的socket全部保存到taskQueue队列中,然后被处理线程取走进行处理。

 

package lesson1.server;

import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.LinkedBlockingQueue;

/**
 * This class is the mock HTTP Server which can only process static
 * resource(*.html,*.js,*.css etc.).
 * 
 * @author sta
 * 
 */
public class HttpServer {

    /**
     * HttpServer will add socket to this queue, ProcessThread will get task
     * from this queue.
     */
    private LinkedBlockingQueue<Socket> taskQueue = new LinkedBlockingQueue<Socket>();

    /**
     * @param args
     */
    public static void main(String[] args) {
        HttpServer instance = new HttpServer();
        try {
            instance.init();
            instance.run();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * Init method mainly start the worker thread.
     */
    private void init() {
        // use fixed thread to processing message
        for (int i = 0; i < Configure.getInstance().getWorkerCount(); i++) {
            // Use "taskQueue" as the constructor parameter, ProcessThread will
            // block at getting task util server get task.
            Thread processThread = new ProcessThread(taskQueue);
            processThread.setName("HttpServer-ProcessThread" + i);
            processThread.start();
        }
        System.out.println(Configure.getInstance().getWorkerCount()
                + " work thread had been started.");
    }

    /**
     * Bind the socket to specified host and port, waiting the connection from
     * client.
     * 
     * @throws Exception
     */
    private void run() throws Exception {
        InetSocketAddress address = new InetSocketAddress(Configure
                .getInstance().getHost(), Configure.getInstance().getPort());
        ServerSocket serverSocket = new ServerSocket();
        serverSocket.bind(address);

        System.out.println("Server is listening on Host["
                + Configure.getInstance().getHost() + "],Port["
                + Configure.getInstance().getPort() + "]");

        System.out.println("Server is waiting the connection from Client.");
        while (true) {
            Socket s = serverSocket.accept();
            // just only add the Socket into taskQueue,the worker threads will get
            // this socket and process it.
            taskQueue.add(s);
        }
    }
}

   处理线程类:

       处理线程持有以上主线程的taskQueue引用,然后不断从该队列中获取socket,获得socket之后便实例化HttpRequest和HttpResponse对象,并调用HttpRequest的parse方法进行请求解析,根据解析结果查找本地资源,如果请求的资源存在,那么就把本地资源对应的流传递给HttpResponse,由HttpResponse进行读取,HttpResponse读取到的流全部回写到客户端,从而完成请求。

package lesson1.server;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.util.concurrent.LinkedBlockingQueue;

/**
 * This is the main thread which will read resource from server machine, then
 * return the byte to client.
 * 
 * @author sta
 * 
 */
public class ProcessThread extends Thread {
    LinkedBlockingQueue<Socket> queue = null;

    public ProcessThread(LinkedBlockingQueue<Socket> queue) {
        this.queue = queue;
    }

    @Override
    public void run() {
        while (true) {
            HttpRequest request = null;
            HttpResponse response = null;
            InputStream inStream = null;
            OutputStream outStream = null;
            Socket socket = null;
            try {
                socket = queue.take();
                inStream = socket.getInputStream();
                request = new HttpRequest(inStream);
                outStream = socket.getOutputStream();
                response = new HttpResponse(outStream);

                // 解析请求消息
                try {
                    request.parse();
                    response.setHttpRequest(request);
                } catch (IOException e) {
                }

                /*
                 * HTTP服务器真正处理逻辑,主要是: 1,根据URI查找响应的流 2,把流输出给客户端
                 */
                String uri = request.getUri();
                File resourceFile = new File(Configure.getInstance()
                        .getDocRoot()
                        + uri);
                if (!resourceFile.exists() || !resourceFile.canRead()) {
                    response.setStatus(Constants.HTTP_CODE_404);
                } else {
                    response.setStatus(Constants.HTTP_CODE_200);
                    response.setResource(new FileInputStream(resourceFile));
                }

                response.send();

            } catch (Exception e) {
                response.setStatus(Constants.HTTP_CODE_500);
                try {
                    response.send();
                } catch (IOException e1) {
                    e1.printStackTrace();
                }
            } finally {
                try {
                    inStream.close();
                } catch (Exception e2) {
                }
                try {
                    outStream.close();
                } catch (Exception e2) {
                }
                // 默认对socket进行关闭
                try {
                    socket.close();
                } catch (Exception e2) {
                }
            }
        }
    }
}

 

   请求解析类:

      HTTP请求类的职责很简单,就是从输入流中读取字节,解析出请求的资源名称。其中parseRequestLine方法就是最重要的处理逻辑,这个基本是参照tomcat来实现的。

package lesson1.server;

import java.io.IOException;
import java.io.InputStream;

/**
 * 代表HTTP请求,主要包含: 1,HTTP方法 2,请求URI:也就是请求的资源 3,协议:区分HTTP协议版本
 * 
 * @author sta
 * 
 */
public class HttpRequest {
    String method = "GET";
    String uri;

    String protocol;

    InputStream in = null;

    public HttpRequest(InputStream in) {
        this.in = in;
    }

    public void parse() throws IOException {
        parseRequestLine();
    }

    private void parseRequestLine() throws IOException {
        int start = 0;
        int pos = 0;
        byte chr = 0;
        // 1024 byte is enough for test.
        byte[] buf = new byte[1024];
        in.read(buf);
        // ignore blank line
        do {
            chr = buf[pos++];
        } while (chr == Constants.CR || chr == Constants.LF);
        pos--;

        start = pos;
        // parse HTTP Method
        boolean space = false;
        while (!space) {
            if (buf[pos] == Constants.SP) {
                space = true;
                method = new String(buf, start, pos - start);
            }
            pos++;
        }

        start = pos;
        // parse URI
        space = false;
        while (!space) {
            if (buf[pos] == Constants.SP) {
                space = true;
                uri = new String(buf, start, pos - start);
            }
            pos++;
        }

        start = pos;
        // parse protocol
        space = false;
        while (!space) {
            if (buf[pos] == Constants.SP || buf[pos] == Constants.CR
                    || (buf[pos] == Constants.LF)) {
                space = true;
                protocol = new String(buf, start, pos - start);
            }
            pos++;
        }
    }

    public String getMethod() {
        return method;
    }

    public void setMethod(String method) {
        this.method = method;
    }

    public String getUri() {
        return uri;
    }

    public void setUri(String uri) {
        this.uri = uri;
    }

    public String getProtocol() {
        return protocol;
    }

    public void setProtocol(String protocol) {
        this.protocol = protocol;
    }
}

 

   响应处理类:

      相应处理类主要是把HTTP状态码,和服务端找到的资源流回写到输出流中。此外,HTTP响应头中也包含了Content-Length属性.

package lesson1.server;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

public class HttpResponse {
    int status;

    OutputStream toClientStream = null;
    HttpRequest httpRequest = null;

    private InputStream resource = null;
    private String resourceType = "text/plain";

    public void setHttpRequest(HttpRequest httpRequest) {
        this.httpRequest = httpRequest;
    }

    public HttpResponse(OutputStream out) {
        this.toClientStream = out;
    }

    public void send() throws IOException {
        writeStatusLine();
        writeHeaderAndResponseBody();
    }

    private void writeStatusLine() throws IOException {
        toClientStream.write(httpRequest.getProtocol().getBytes());
        toClientStream.write(Constants.SP);
        toClientStream.write(String.valueOf(status).getBytes());
        toClientStream.write(Constants.SP);
        toClientStream.write(Constants.CODE2MESSAGE.get(status).getBytes());
        toClientStream.write(Constants.CRLF.getBytes());
        // 没写HTTP响应消息(比如200对应的OK)
    }

    private void writeHeaderAndResponseBody() throws IOException {
        if (resource != null) {
            try {
                // Content-Length和Content-Type头
                String contentLengthLine = "Content-Length: "
                        + resource.available();
                toClientStream.write(contentLengthLine.getBytes());
                toClientStream.write(Constants.CRLF.getBytes());
                
                String contentType = "Content-Type: "
                        + Constants.MIMETYPES.get(resourceType);
                toClientStream.write(contentType.getBytes());
                toClientStream.write(Constants.CRLF.getBytes());
                
                // 头和消息体之间是两个回车换行符
                toClientStream.write(Constants.CRLF.getBytes());

                //HTTP响应消息体数据
                byte[] bytePerTime = new byte[Configure.getInstance()
                        .getSendBufferSize()];
                int count = -1;
                while ((count = resource.read(bytePerTime)) > 0) {
                    toClientStream.write(bytePerTime,0,count);
                    toClientStream.flush();
                }
            } catch (IOException e) {
                throw e;
            } finally {
                try {
                    resource.close();
                } catch (Exception e2) {
                }
            }
        }else{
            toClientStream.flush();
        }
    }

    public void setResource(InputStream resource) {
        this.resource = resource;
    }

    public void setResourceType(String fileSuffix) {
        this.resourceType = fileSuffix;
    }

    public int getStatus() {
        return status;
    }

    public void setStatus(int status) {
        this.status = status;
    }
}

 

  以上代码基本具备了HTTP请求处理能力,为了尽可能的简化和描述出HTTP服务器最本质的东西,省略了很多处理(比如静态资源缓存,HTTP头处理等)。验证HTTP服务器步骤:

  1,新建一个server.properties文件,并正确配置主机、端口等

    ---

    host=localhost
    port=8080
    docRoot=./webapps
    threadCount=10

  2,需要保证server.properties文件中配置的docRoot目录存在,拷贝一些静态文件(html...)到docRoot目录下

  3,java lesson1.server.HttpServer启动

  4,通过浏览器输入http://$host:$port/$resourceName便可,其中resourceName为相对于server.properties中配置的docRoot目录的相对路径。可以使用firefox的httpwatch查看请求和响应的细节。

 

注:本文于2013年4月3号进行了一次修改维护,主要是解决一下问题:

1,HTTP响应格式不正确,特别是响应的状态码不正确、以及未设置content-type导致图片数据在浏览器可能不见的问题。

2,本次附上所有打包好的源代码,包括测试用静态文件

 

0
0
分享到:
评论
1 楼 lw_China 2012-07-06  
写得不错,想学习的可以多看看。

相关推荐

Global site tag (gtag.js) - Google Analytics