BLOG ARTICLE Web Server | 2 ARTICLE FOUND

  1. 2008/03/13 Java로 Web Server 구현하기 - Part III
  2. 2008/03/13 Java로 Web Server 구현하기 - Part I

사용자 삽입 이미지
3. HttpHandler and HandlerPool(Thread Pooling) 
     HttpWebServer에서 client connection후 Handler thread를 start하지 않고, HandlerPool의 execute(Socket sck) method를 call했죠. HandlerPool 객체는      HttpHandler라는 Thread객체의 pooling이 구현되어 있습니다.
 

  Thread pooling에 대한 필요성을 위해서 언급했고, 구현 방법은  HandlerPool의 source보면 되겠죠.

package com.javapattern.http;

import java.util.*;
import java.net.*;
import com.javapattern.util.PropManager;
import com.javapattern.util.LogManager;

/*
*   HandlerPool.java - Thread Pooling
*   2001.05.17
*/
public class HandlerPool
{
private Vector pool;
//  Singleton
private static HandlerPool instance;
private ThreadGroup tGroup;

private static int HANDLER=5;
   
public HandlerPool()
{
    pool = new Vector(1,3);
    tGroup = new ThreadGroup("HttWebServerHandler");
    init();
}

public static HandlerPool getInstance()
{
    //  Double checked Locking
    if(instance==null) {
        synchronized(HandlerPool.class) {
            if(instance==null) {
                instance=new HandlerPool();
            }
        }
    }
    return instance;
}

//  initialize the pooling
private void init() {
    PropManager propMgr=PropManager.getInstance();
    try {
        HANDLER=Integer.parseInt(propMgr.getProperty("handler","5"));
    } catch(NumberFormatException e) {
        LogManager.log(e,"at the HandlerPool.init() method."+
            propMgr.getProperty("handler"));
        LogManager.log("The 'HANDLER' property is setted by 5.");  
    }
   
    for(int i=0;i<HANDLER;i++)
    {
        HttpHandler newHandler=new HttpHandler();
        new Thread(tGroup,newHandler,"handler"+i).start();
        pool.addElement(newHandler);
    }
    LogManager.log("HandlerPool is initialized. - "+pool.size());
    //tGroup.list();
}

//  process the client request( notify a thread. )
public synchronized void execute(Socket sck) {
    if(pool.isEmpty()) {
        HttpHandler newHandler=new HttpHandler();
        newHandler.setSocket(sck);
        new Thread(tGroup,newHandler,"New Handler").start();
    } else {
        HttpHandler handler=(HttpHandler)pool.remove(pool.size()-1);
        handler.setSocket(sck);
    }  
}

public synchronized void release(HttpHandler handler)
{
    if(pool.size()>=HANDLER) {
        LogManager.log("Too many threads, exit this thread.["+
            Thread.currentThread().getName()+"]");
        handler.stop();
    } else {
        pool.addElement(handler);
    }
    //tGroup.list();
}  
}
▶ HandlerPool.java

위의 ThreadPool의 source만 봐서는 실재 pooling의 원리를 이해 하지는 못하겠죠.
실제 thread의 wait, notify는 HttpHandler의 setSocket(), run() method에 구현되어 있습니다.
그래서, client request시에 setSocket() method에서 notify()를 시키고, notify된 thread는
일어나 request를 처리하고, 다시 wait() 하게 되는 것입니다.
그리고, HttpHandler는 processReqeust() method를 통해 실재 HttpRequest를 처리합니다.

package com.javapattern.http;

import java.io.*;
import java.net.*;
import com.javapattern.util.LogManager;
import com.javapattern.util.PropManager;

/*
*   HttpHandler.java - Client의 reqeust를 처리하는 thread
*   2001.05.17
*/
public class HttpHandler implements Runnable,HttpConstants
{
private Socket sck;
private static int BUFF_SIZE=1024*2;
private byte[] buff;

private static boolean SHUTTDOWN;
private static final byte[] EOL={(byte)'\r',(byte)'\n'};
private static final byte GET=(byte)0;
private static final byte HEAD=(byte)1;
private static final String[] methodStr={"GET","HEAD"};

public HttpHandler()
{
    String buffer=PropManager.getInstance().getProperty("buffer.size","2048");
    try {
        BUFF_SIZE=Integer.parseInt(buffer);
    } catch(NumberFormatException e) {
        LogManager.log(e,"at the HttpHandler constructor.");
        LogManager.log("The Response buffer.size is setted 2048K");
    }
    buff = new byte[BUFF_SIZE];        
}

//  client request시 call됨. ==> notify a thread
public synchronized void setSocket(Socket sck)
{
    this.sck = sck;
    notify();  
}
   
public synchronized void run()
{
    SHUTTDOWN=true;
    while(SHUTTDOWN)
    {
        if(sck==null)
        {
            try {
                wait();
            } catch(InterruptedException e) {
                LogManager.log(e,"at the HttpHandler.run() method.");
                continue;  
            }                  
        }
        try {
            processRequest();
        } catch(IOException e) {
            LogManager.log(e,"at the HttpHandler.run() method.");
            LogManager.log("For processing the request, It is occured a Exception.");
        }
        sck=null;
        //  clear the buffer
        for(int i=0;i<BUFF_SIZE;i++)    buff[i]=0;
   
        HandlerPool pool = HandlerPool.getInstance();
        pool.release(this);
    }
       
}

protected void processRequest()
    throws IOException
{
    InputStream in = sck.getInputStream();
    OutputStream out = sck.getOutputStream();
    BufferedOutputStream bos = new BufferedOutputStream(out);
    PrintStream ps = new PrintStream(bos);
    PropManager propMgr = PropManager.getInstance();
    //get timeout
    String timeout = propMgr.getProperty("timeout","5000");
   
    sck.setSoTimeout(Integer.parseInt(timeout));
    sck.setTcpNoDelay(true);
           
    int reqLength=0,r=0;
    try {
        /*  HTTP GET/HEAD만 지원,
         *  request message 첫 라인을 read.
         */
outer:      while(reqLength<BUFF_SIZE)
    {
        r = in.read(buff,reqLength,BUFF_SIZE-reqLength);
        if(r==-1) return;
       
        int i=reqLength;
        reqLength+=r;
        for(;i<reqLength;i++)
        {
            if(buff[i]==(byte)'\n'||buff[i]==(byte)'\r')
                break outer;   
        }                  
    }
   
    byte reqMethod;
    int index=0;
    if(buff[0]==(byte)'G'&&
       buff[1]==(byte)'E'&&
       buff[2]==(byte)'T'&&
       buff[3]==(byte)' ') {
        reqMethod = GET;
        index=4;   
    } else if(buff[0]=='H'&&
              buff[1]=='E'&&
              buff[2]=='A'&&
              buff[3]=='D'&&
              buff[4]==' ') {
        reqMethod=HEAD;
        index=5;               
    } else {
        //send 405 : Bad method type
        String[] msg = {"unsupported method type:",new String(buff,0,5)};
        sendHeader(ps,HTTP_BAD_METHOD,msg[0]);
        sendError(ps,HTTP_BAD_METHOD,msg);
        return;
    }
   
    /*  get request file name
     */
    int i=0;
    for(i=index;i<reqLength;i++) {
        if(buff[i]==(byte)' ') {
            break; 
        }
    }
   
    String fname = new String(buff,index,i-index);
    fname=fname.replace('/',File.separatorChar);
    if(fname.startsWith(File.separator)) {
        fname=fname.substring(1);  
    }
    String docRoot =
      propMgr.getProperty("DocumentRoot",System.getProperty("user.dir"));
    File target = new File(docRoot,fname);
   
    //target resource : DocumentRoot일 경우
    if(target.isDirectory()) {
        File welcome = new File(target,"index.html");
        if(welcome.exists()) {
            target=welcome;
        }  
    }
    if( !target.exists() && !fname.equals("") ) {
        // 404 : NotFound
        sendHeader(ps,HTTP_NOT_FOUND,"");
        return;
    }
    LogManager.log("From "+sck.getInetAddress().getHostAddress()+": "+
            methodStr[reqMethod]+" "+target.getAbsolutePath());
   
    switch(reqMethod)
    {
    case GET : if( target.isDirectory() )
           {
               sendHeader(ps, HTTP_OK,"OK",target);
               sendDirList(ps,target);
           } else if(target.isFile()) {
               sendHeader(ps, HTTP_OK,"OK",target);
               sendFile(ps,target);
           }
        break;
    case HEAD :
       
        break;
    }
   
    LogManager.log("From "+sck.getInetAddress().getHostAddress()+": "+
            methodStr[reqMethod]+" "+target.getAbsolutePath()+"-->"+HTTP_OK);
} finally {
    if(ps!=null) {
        ps.flush();
        ps.close();
    }
    if(bos!=null) bos.close();
    if(in!=null) in.close();
    if(out!=null) out.close();
    if(sck!=null) sck.close();
}
   
}

protected void sendHeader(PrintStream ps,int resCode,String msg)
    throws IOException
{      
    ps.print("HTTP/1.0 "+resCode+" "+msg);
    ps.write(EOL);
    ps.print("Date: "+new java.util.Date());
    ps.write(EOL);
    ps.print("Server: HttpWebServer 1.0");
    ps.write(EOL);
}

protected void sendHeader(PrintStream ps, int resCode, String msg, File target)
    throws IOException
{
    sendHeader(ps,resCode,msg);
    if(target.isDirectory()) {
        ps.print("Content-type: text/html");
        ps.write(EOL); 
    } else {
        ps.print("Content-length: "+target.length());
        ps.write(EOL);
        ps.print("Last Modified: "+new java.util.Date(target.lastModified()));
        ps.write(EOL);
        String fName=target.getName();
        int idx=fName.lastIndexOf('.');
        String mime=null;
        if(idx>0) mime = (String)MimeDB.get(fName.substring(idx));
        if(mime==null) mime="unknown/unknown";
        ps.print("Content-type: "+mime);
        ps.write(EOL);
    }  
}

protected void sendError(PrintStream ps, int resCode, String[] msg)
    throws IOException
{
    ps.print("Content-type: text/html");
    ps.write(EOL);
    ps.write(EOL);
    ps.print(resCode + " : "+msg[0]);
    for(int i=1;i<msg.length;i++) {
        ps.write(EOL);
        ps.print(msg[i]);  
    }
}

protected void sendFile(PrintStream ps, File target)
    throws IOException
{      
    ps.write(EOL);
    InputStream is = new FileInputStream(target.getAbsolutePath());

    try {
        int n;
        while ((n = is.read(buff)) > 0) {
            ps.write(buff, 0, n);
        }
    } finally {
        is.close();
    }
}
protected void sendDirList(PrintStream ps,File dir)
    throws IOException
{
    PropManager propMgr = PropManager.getInstance();
    String docRoot = propMgr.getProperty("DocumentRoot",System.getProperty("user.dir"));
    String path = dir.getPath();
    String viewPath=path.substring(docRoot.length()).replace(File.separatorChar,'/');

    ps.println("<TITLE>Directory listing</TITLE><P>\n");
    ps.println("<body>");
    ps.println("<A HREF=\"..\">Parent Directory</A><BR> \n");
    ps.println("<TABLE BORDER=\"0\">");
    ps.println("<TR><TD><u>File Name</u></TD>");
    ps.println("<TD><u>Size</u></TD>");
    ps.println("<TD><u>Last Modified</u></TD></TR>");
    File[] list = dir.listFiles();
    for (int i = 0; list != null && i < list.length; i++) {
    ps.println("<TR><TD width=40%>");
    if (list[i].isDirectory()) {
        ps.println("<li><A HREF=\""+viewPath+"/"+list[i].getName()+"/\">");
        ps.println("<font size=-1>"+list[i].getName()+
                "/</font></A><BR>");
    } else {
        ps.println("<li><A HREF=\""+viewPath+"/"+list[i].getName()+"\">");
        ps.println("<font size=-1>"+list[i].getName()+
                "</font></A><BR>");
    }
    ps.println("</TD><TD width=15%><font size=-1>"+
                list[i].length()+"</font></TD>");
    ps.println("<TD width=45%><font size=-1>"+
                new java.util.Date(list[i].lastModified()));
    ps.println("</font></TD></TR>");
    }
    ps.println("</TABLE>");
ps.println("<P><HR><BR><I>" + (new java.util.Date()) +
                "</I></body>");
}

public void stop()
{
    SHUTTDOWN=false;   
}  
}
▶ HttpHandler.java
▶ HttpConstants.java


이렇게 해서 Web Server에 대한 프로그램을 다 소개를 했네요.
Network Server programming 의 이해에 도움이 되었는지 모르겠네요.



크리에이티브 커먼즈 라이센스
Creative Commons License
2008/03/13 09:35 2008/03/13 09:35

사용자 삽입 이미지
※ Web Server 만들기 
Networking, Multi-thread programming을 이해하기 위해 Java로 WebServer를 구현한
program을 소개하고자 합니다.
Server application을 구현하기 위해 Network, Stream I/O, Thread알고 있어야하고,
그리고, Server의 동작 구조에 대해 이해를 하고 있어야 합니다. 여기서 소개하고자
하는 WebServer에서는 Dynamic configuration기능, Logging, Thread Pooling의 기능을
구현해 놓았고, Server programming에 대한 이해와 함께 이런 기능을 응용하는
방법을 이해하면 될것 같습니다. 

1. Dynamic Configuration 
     Dynamic configuration이라는 의미는 configuration의 값들의 hard coding을 피해서
     runtime에 결정하자는 뜻입니다. 이런 기능의 예로는 Applet의 param tag, Servlet의
     InitParameter, EJB의 Environment Entry등이 있습니다.
     즉 변경될 가능성이 있는 값들을 hard coding하게 되면, 값이 변경될때 마다
     재 compile해야 한다는 번거러움이 있죠. 그래서, configuration file에 name/value값들을
     설정하고, programming할때 name의 값에 해당하는 value값을 읽어 변수에 저장해
     사용하게 되면, 값이 변경될때 만다 재 compile하는 번거러움은 없어지는 것이죠.
     그리고, runtime에 값의 변경을 지금 실행 중인 프로세스에 반영할수도 있겠죠.
     web server에서는 httpd.conf file에 web server의 configuration을 다음과 같이
     설정해 놓았습니다.

## HttpWebServer Configuration file : httpd.conf
    DocumentRoot=d:
    handler=5
    timeout=10000
    port=8080
    verbose=true
    buffer.size=2048
    mime.file=mime.txt

▶ httpd.conf
     httpd.conf file의 내용을 읽어 program에서 이 값들을 access하기 위한 객체를
     정의 해야 하는데, 이것을 정의할때 java.util.Properties 객체를 이용하면 되겠죠.
     Properties객체의 이용은 article "Properties class and properties file Example"을 참고하세요.
     그리고, 아래에 PropManager는 httpd.conf의 configuration을 읽어 Properties객체에
     저장하는 것과 runtime에 configuration에 대한 service를 해 주는 객체입니다.
     load() method에서 httpd.conf file의 내용을 읽어 Properties객체에 저장을 하는데,
     PROPERTIES_FILE의 httpd.conf file은 CLASSPATH경로 위치에서 찾게 됩니다.


package com.javapattern.util;
import java.util.*;
import java.io.*;
/*
* PropManager.java : Configuration Properties management.
* 2001.05.15
*/
public class PropManager {
    //Singleton
    private static PropManager instance;
    private Properties prop;
    private static String PROPERTIES_FILE="httpd.conf";
    static
    {
        instance=new PropManager();
    }
    private PropManager()
    {
        prop = new Properties();
        try {
            load();
        } catch(IOException e) {
            e.printStackTrace();
        }
    }
   
    public static PropManager getInstance()
    {
        if(instance==null) {
            instance=new PropManager();
        }
        return instance;
    }

    public synchronized void load() throws IOException
    {
        InputStreamReader isr=null;
        BufferedReader br=null;
        ClassLoader cl = getClass().getClassLoader();
        InputStream in= cl.getResourceAsStream(PROPERTIES_FILE);
        if(in==null) {
            throw new FileNotFoundException("The configuration file is not found.");
        }
        try {
            isr = new InputStreamReader(in);
            br = new BufferedReader(isr);
            String line=null;
            while(true) {
                line=br.readLine();
                if(line==null) break;
                if(line.startsWith("#")) continue;
                parseLine(line);
            }
        } catch(IOException e) {
            throw e;
        } finally {
            try {
                if(br!=null) br.close();
                if(isr!=null) isr.close();
                if(in!=null) in.close();
            } catch(IOException e) { e.printStackTrace(); }
        }
    }

    public synchronized void store(String header)
    throws IOException
    {
        FileOutputStream fo = null;
        OutputStreamWriter osw = null;
        BufferedWriter awriter = null;
        try {
            fo = new FileOutputStream(PROPERTIES_FILE);
            osw = new OutputStreamWriter(fo);
            awriter = new BufferedWriter(osw);
            if (header != null)
                writeln(awriter, "# " + header);
            writeln(awriter, "# " + new Date().toString());
            Enumeration e = prop.keys();
            while(e.hasMoreElements()) {
                String key = (String)e.nextElement();
                String val = (String)prop.get(key);
                writeln(awriter, key + "=" + val);
            }
            awriter.flush();
        } catch(IOException e) {
            throw e;
        } finally {
            try {
                if(awriter!=null) awriter.close();
                if(osw!=null) osw.close();
                if(fo!=null) fo.close();
            } catch(IOException e) { e.printStackTrace(); }
        }
    }

    public String getProperty(String name)
    {
        return prop.getProperty(name);
    }

    public String getProperty(String name,String defaultValue)
    {
        return prop.getProperty(name,defaultValue);
    }

    public void setProperty(String name,String value)
    {
        prop.setProperty(name,value);
    }

    public Enumeration propertyNames()
    {
        return prop.propertyNames();
    }

    public void list() {
        System.out.println("--"+PROPERTIES_FILE+" configuration --");
        prop.list(System.out);
        System.out.println("------------------------");
    }

    private void parseLine(String line)
    {
        if(line.indexOf("=") != -1 ) {
            int idx=line.indexOf("=");
            prop.put(line.substring(0,idx),line.substring(idx+1));
        }
    }

    private void writeln(BufferedWriter bw, String s)
    throws IOException
    {
        bw.write(s);
        bw.newLine();
    }
}

▶ PropManager.java
//PropManager Test Application
import com.javapattern.util.*;
import java.io.*;
public class PropMgrEx {
    public static void main(String args[])
    throws IOException
    {
        PropManager pMgr = PropManager.getInstance("httpd.conf");
        pMgr.list();
        System.out.println(pMgr.getProperty("DocumentRoot"));
    }
}

▶ PropMgrEx.java 
  
  다음은 web server에서 이용하게 될 MIME Type에 대한 data를 이용하기 위해
  구현한 코드를 보겠습니다. MIME Type도 configuration과 유사하게 이용될수 있지만,
  되게 read only의 성격의 data로 볼수가 있습니다. 그래서 PropManager를 이용하기 보다는
read only의 성격을 갖고있는 class를 따로 정의 했죠.

먼저 mime.txt file의 내용입니다.

# MIME TYPE : mime.txt file
.uu=application/octet-stream
.exe=application/octet-stream
.ps=application/postscript
.zip=application/zip
.sh=application/x-shar
.tar=application/x-tar
.snd=audio/basic
.au=audio/basic
.wav=audio/x-wav
.gif=image/gif
.jpg=image/jpeg
.jpeg=image/jpeg
.htm=text/html
.html=text/html
.text=text/plain
.c=text/plain
.cc=text/plain
.c++=text/plain
.h=text/plain
.pl=text/plain
.txt=text/plain
.java=text/plain

▶ mime.txt
  mime.txt file의 내용을 loading해서 runtime에 MIME TYPE에 대해 service해 주는 class입니다.
package com.javapattern.http;
import java.io.*;
import java.util.*;
import com.javapattern.util.*;
/*
* MimeDB.java : MIME TYPE data loading,service
* 2001.05.15
 */
public class MimeDB implements Serializable {
    private static Properties db;
    static
    {
        db = new Properties();
        String mimeFile=PropManager.getInstance().getProperty("mime.file","mime.txt");
        ClassLoader cl = MimeDB.class.getClassLoader();
        InputStream is= cl.getResourceAsStream(mimeFile);
        try {
            if(is==null)
                throw new FileNotFoundException("The configuration file is not found.");
            db.load(is);
        } catch(IOException e) {
            LogManager.log(e,"MIME Configuration file is not loaded.");
        } finally {
       
            try {
                if(is!=null) is.close();
            } catch(IOException e) { e.printStackTrace(); }
        }
    }

    private MimeDB() {}
    public static String get(String name)
    {
        return db.getProperty(name,"unknown/unknown");
    }

    private static void set(String name,String value)
    {
        if(db!=null) db.setProperty(name,value);
    }

    public static void list() {
        System.out.println("-- mime.txt configuration --");
        db.list(System.out);
        System.out.println("-------------------------");
    }
}

▶ MimeDB.java


위의 PropManager는 configuration peroperties를 dynamic하게 반영하는 것은 구현되어
  있지 않은데, 이것은 Thread를 이용해 configuration file의 modified time을 주기적으로
  check해서 loading하게끔 구현하면, 되겠죠.

크리에이티브 커먼즈 라이센스
Creative Commons License
2008/03/13 09:26 2008/03/13 09:26