用Java編寫簡單HTTP服務器

一、  簡單的單文件服務器

該服務器的功能:無論接受到何種請求,都始終發送同一個文件。這個服務器命名爲SingleFileHTTPServer,文件名、本地端口和內容編碼方式從命令行讀取。如果缺省端口,則假定端口號爲80。如果缺省編碼方式,則假定爲ASCII。

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
 
 
public class SingleFileHTTPServer extends Thread {
	
	private byte[] content;
	private byte[] header;
	private int port=80;
	
	private SingleFileHTTPServer(String data, String encoding,
				String MIMEType, int port) throws UnsupportedEncodingException {
		this(data.getBytes(encoding), encoding, MIMEType, port);
	}
	
	public SingleFileHTTPServer(byte[] data, String encoding, String MIMEType, int port)throws UnsupportedEncodingException {
		this.content=data;
		this.port=port;
		String header="HTTP/1.0 200 OK\r\n"+
			"Server: OneFile 1.0\r\n"+
			"Content-length: "+this.content.length+"\r\n"+
			"Content-type: "+MIMEType+"\r\n\r\n";
		this.header=header.getBytes("ASCII");
	}
	
	public void run() {
		try {
			ServerSocket server=new ServerSocket(this.port);
			System.out.println("Accepting connections on port "+server.getLocalPort());
			System.out.println("Data to be sent:");
			System.out.write(this.content);
			
			while (true) {
				Socket connection=null;
				try {
					connection=server.accept();
					OutputStream out=new BufferedOutputStream(connection.getOutputStream());
					InputStream in=new BufferedInputStream(connection.getInputStream());
					
					StringBuffer request=new StringBuffer();
					while (true) {
						int c=in.read();
						if (c=='\r'||c=='\n'||c==-1) {
							break;
						}
						request.append((char)c);
						
					}
						
						//如果檢測到是HTTP/1.0及以後的協議,按照規範,需要發送一個MIME首部
						if (request.toString().indexOf("HTTP/")!=-1) {
							out.write(this.header);
						}
						
						out.write(this.content);
						out.flush();
					
				} catch (IOException e) {
					// TODO: handle exception
				}finally{
					if (connection!=null) {
						connection.close();
					}
				}
			}
			
		} catch (IOException e) {
			System.err.println("Could not start server. Port Occupied");
		}
	}
	
	public static void main(String[] args) {
		try {
			String contentType="text/plain";
			if (args[0].endsWith(".html")||args[0].endsWith(".htm")) {
				contentType="text/html";
			}
			
			InputStream in=new FileInputStream(args[0]);
			ByteArrayOutputStream out=new ByteArrayOutputStream();
			int b;
			while ((b=in.read())!=-1) {
				out.write(b);
			}
			byte[] data=out.toByteArray();
			
			//設置監聽端口
			int port;
			try {
				port=Integer.parseInt(args[1]);
				if (port<1||port>65535) {
					port=80;
				}
			} catch (Exception e) {
				port=80;
			}
			
			String encoding="ASCII";
			if (args.length>2) {
				encoding=args[2];
			}
			
			Thread t=new SingleFileHTTPServer(data, encoding, contentType, port);
			t.start();
			
		} catch (ArrayIndexOutOfBoundsException e) {
			 System.out.println("Usage:java SingleFileHTTPServer filename port encoding");
		}catch (Exception e) {
			System.err.println(e);// TODO: handle exception
		}
	}
}

SingleFileHTTPServer類本身是Thread的子類。它的run()方法處理入站連接。此服務器可能只是提供小文件,而且只支持低吞吐量的web網站。由於服務器對每個連接所需完成的所有工作就是檢查客戶端是否支持HTTP/1.0,併爲連接生成一兩個較小的字節數組,因此這可能已經足夠了。另一方面,如果你發現客戶端被拒絕,則可以使用多線程。許多事情取決於所提供文件的大小,每分鐘所期望連接的峯值數和主機上Java的線程模型。對弈這個程序複雜寫的服務器,使用多線程將會有明顯的收益。

Run()方法在指定端口創建一個ServerSocket。然後它進入無限循環,不斷地接受連接並處理連接。當接受一個socket時,就會由一個InputStream從客戶端讀取請求。它查看第一行是否包含字符串HTTP。如果包含此字符串,服務器就假定客戶端理解HTTP/1.0或以後的版本,因此爲該文件發送一個MIME首部;然後發送數據。如果客戶端請求不包含字符串HTTP,服務器就忽略首部,直接發送數據。最後服務器關閉連接,嘗試接受下一個連接。

而main()方法只是從命令行讀取參數。從第一個命令行參數讀取要提供的文件名。如果沒有指定文件或者文件無法打開,就顯示一條錯誤信息,程序退出。如果文件能夠讀取,其內容就讀入byte數組data.關於文件的內容類型,將進行合理的猜測,結果存儲在contentType變量中。接下來,從第二個命令行參數讀取端口號。如果沒有指定端口或第二個參數不是0到65535之間的整數,就使用端口80。從第三個命令行參數讀取編碼方式(前提是提供了)。否則,編碼方式就假定爲ASCII。然後使用這些值構造一個SingleFileHTTPServer對象,開始運行。這是唯一可能的接口。

下面是測試的結果,命令行編譯代碼並設置參數:

telnet::

首先,啓用telnet服務(如果不會,自行google之),接着測試該主機的端口:

結果(可以看到請求的輸出內容):

HTTP協議測試:

文檔(這是之前一篇文章--小車動畫的文檔)

二、  重定向服務器

實現的功能——將用戶從一個Web網站重定向到另一個站點。下例從命令行讀取URL和端口號,打開此端口號的服務器可能速度會很快,因此不需要多線程。儘管日次,使用多線程可能還是會帶來一些好處,尤其是對於網絡帶寬很低、吞吐量很小的網站。在此主要是爲了演示,所以,已經將該服務器做成多線程的了。這裏爲了簡單起見,爲每個連接都啓用了一個線程,而不是採用線程池。或許更便於理解,但這真的有些浪費系統資源並且顯得低效。

import java.io.BufferedInputStream;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.Writer;
import java.net.BindException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Date;
 
 
public class Redirector implements Runnable {
 
	private int port;
	private String newSite;
	
	public Redirector(String site, int port){
		this.port=port;
		this.newSite=site;
	}
	
	@Override
	public void run() {
		try {
			ServerSocket server=new ServerSocket(port);
			System.out.println("Redirecting connection on port"
					+server.getLocalPort()+" to "+newSite);
			
			while (true) {
				try {
					Socket socket=server.accept();
					Thread thread=new RedirectThread(socket);
					thread.start();
				} catch (IOException e) {
					// TODO: handle exception
				}
			}
		} catch (BindException e) {
			System.err.println("Could not start server. Port Occupied");
		}catch (IOException e) {
			System.err.println(e);
		}
		
	}
	
	class RedirectThread extends Thread {
 
		private Socket connection;
		
		RedirectThread(Socket s) {
			this.connection=s;
		}
		
		public void run() {
			try {
				Writer out=new BufferedWriter(
						new OutputStreamWriter(connection.getOutputStream(),"ASCII"));
				Reader in=new InputStreamReader(
						new BufferedInputStream(connection.getInputStream()));
				
				StringBuffer request=new StringBuffer(80);
				while (true) {
					int c=in.read();
					if (c=='\t'||c=='\n'||c==-1) {
						break;
					}
					request.append((char)c);
				}
				
				String get=request.toString();
				int firstSpace=get.indexOf(' ');
				int secondSpace=get.indexOf(' ', firstSpace+1);
				String theFile=get.substring(firstSpace+1, secondSpace);
				
				if (get.indexOf("HTTP")!=-1) {
					out.write("HTTP/1.0 302 FOUND\r\n");
					Date now=new Date();
					out.write("Date: "+now+"\r\n");
					out.write("Server: Redirector 1.0\r\n");
					out.write("Location: "+newSite+theFile+"\r\n");
					out.write("Content-Type: text/html\r\n\r\n");
					out.flush();
				}
				
				//並非所有的瀏覽器都支持重定向,
				//所以我們需要生成一個適用於所有瀏覽器的HTML文件,來描述這一行爲
				out.write("<HTML><HEAD><TITLE>Document moved</TITLE></HEAD>\r\n");
				out.write("<BODY><H1>Document moved</H1></BODY>\r\n");
				out.write("The document "+theFile
						+" has moved to \r\n<A HREF=\""+newSite+theFile+"\">"
						+newSite+theFile
						+"</A>.\r\n Please update your bookmarks");
				out.write("</BODY></HTML>\r\n");
				out.flush();
				} catch (IOException e) {
			}finally{
				try {
					if (connection!=null) {
						connection.close();
					}
				} catch (IOException e2) {
					
				}
			}
		}
		
	}
	
	/**
	 * @param args
	 */
	public static void main(String[] args) {
		int thePort;
		String theSite;
		
		try {
			theSite=args[0];
			
			//如果結尾有'/',則去除
			if (theSite.endsWith("/")) {
				theSite=theSite.substring(0,theSite.length()-1);
			}
		} catch (Exception e) {
			System.out.println("Usage: java Redirector http://www.newsite.com/ port");
			return;
		}
		
		try {
			thePort=Integer.parseInt(args[1]);
		} catch (Exception e) {
			thePort=80;
		}
		
		Thread t=new Thread(new Redirector(theSite, thePort));
		t.start();
 
	}
	
}

HTTP測試:偵聽8010端口,此處重定向到百度

main()方法提供一個非常簡單的界面,讀取新網站的URL(爲了把鏈接重定向到該URL)和監聽本地端口。它使用這些信息構造了一個Rredirector對象。然後它使用所生成的Runnable對象(Redirector實現了Runnable)來生成一個新線程並啓動。如果沒有指定端口,Rredirector則會監聽80端口。

Redirectro的run()方法將服務器socket綁定與此端口,顯示一個簡短的狀態消息,然後進入無限循環,監聽連接。每次接受連接,返回的Socket對象會用來構造一個RedirectThread。然後這個RedirectThread被啓動。所有與客戶端進一步的交互由此新線程完成。Redirector的run()方法只是等待下一個入站連接。

RedirectThread的run()方法完成了很多工作。它先把一個Writer鏈接到Socket的輸出流,把一個Reader鏈接到Socket的輸入流。輸入流和輸出流都有緩衝。然後run()方法讀取客戶端發送的第一行。雖然客戶端可能會發送整個Mime首部,但我們會忽略這些。第一行包含所有所需的信息。這一行內容可能會是這樣:

GET /directory/filename.html HTTP/1.0

可能第一個詞是POST或PUT,也可能沒有HTTP版本。

返回的輸出,第一行顯示爲:

HTTP/1.0 302 FOUND

這是一個HTTP/1.0響應嗎,告知客戶端要被重定向。第二行是“Date:”首部,給出服務器的當前時間。這一行是可選的。第三行是服務器的名和版本;這一行也是可選的,但蜘蛛程序可用它來統計記錄最流行的web服務器。下一行是“Location:”首部,對於此服務器這是必須的。它告知客戶端要重定向的位置。最後是標準的“Content-type:”首部。這裏發送內容類型text/html,只是客戶端將會看到的HTML。最後,發送一個空行來標識首部數據的結束。

如果瀏覽器不支持重定向,那麼那段HTML標籤就會被髮送。

三、  完整功能的HTTP服務器

這裏,我們來開發一個具有完整功能的HTTP服務器,成爲JHTTP,它可以提供一個完整的文檔樹,包括圖片、applet、HTML文件、文本文件等等。它與SingleFileHTTPServer非常相似,只不過它所關注的是GET請求。此服務器仍然是相當輕量級的;看過這個代碼後,我們將討論可能希望添加的其他特性。

由於這個服務器必須爲可能很慢的網絡連接提供文件系統的大文件,因此要改變其方式。這裏不再在執行主線程中處理到達的每個請求,而是將入站連接放入池中。由一個RequestProcessor類實例從池中移走連接並進行處理。
 

import java.io.File;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
 
import org.omg.CORBA.Request;
 
 
public class JHTTP extends Thread {
 
	private File documentRootDirectory;
	private String indexFileName="index.html";
	private ServerSocket server;
	private int numThreads=50;
	
	public JHTTP(File documentRootDirectory,int port , String indexFileName)throws IOException {
		if (!documentRootDirectory.isDirectory()) {
			throw new IOException(documentRootDirectory+" does not exist as a directory ");
		}
		this.documentRootDirectory=documentRootDirectory;
		this.indexFileName=indexFileName;
		this.server=new ServerSocket(port);
	}
	
	private JHTTP(File documentRootDirectory, int port)throws IOException {
		this(documentRootDirectory, port, "index.html");
	}
	
	public void run(){
		for (int i = 0; i < numThreads; i++) {
			Thread t=new Thread(new RequestProcessor(documentRootDirectory, indexFileName));
			t.start();
		}
		
		System.out.println("Accepting connection on port "
				+server.getLocalPort());
		System.out.println("Document Root: "+documentRootDirectory);
		while (true) {
			try {
				Socket request=server.accept();
				RequestProcessor.processRequest(request);
			} catch (IOException e) {
				// TODO: handle exception
			}
		}
	}
	
	
	/**
	 * @param args
	 */
	public static void main(String[] args) {
		File docroot;
		try {
			docroot=new File(args[0]);
		} catch (ArrayIndexOutOfBoundsException e) {
			System.out.println("Usage: java JHTTP docroot port indexfile");
			return;
		}
		
		int port;
		try {
			port=Integer.parseInt(args[1]);
			if (port<0||port>65535) {
				port=80;
			}
		} catch (Exception e) {
			port=80;
		}
		
		try {
			JHTTP webserver=new JHTTP(docroot, port);
			webserver.start();
		} catch (IOException e) {
			System.out.println("Server could not start because of an "+e.getClass());
			System.out.println(e);
		}
		
	}
 
}

JHTTP類的main()方法根據args[0]設置文檔的根目錄。端口從args[1]讀取,或者使用默認的80.然後構造一個新的JHTTP線程並啓動。此JHTTP線程生成50個RequestProcessor線程處理請求,每個線程在可用時從RequestProcessor池獲取入站連接請求。JHTTP線程反覆地接受入站連接,並將其放在RequestProcessor池中。每個連接由下例所示的RequestProcessor類的run()方法處理。此方法將一直等待,直到從池中得到一個Socket。一旦得到Socket,就獲取輸入和輸出流,並鏈接到閱讀器和書寫器。接着的處理,除了多出文檔目錄、路徑的處理,其他的同單文件服務器。

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.DataInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.Writer;
import java.net.Socket;
import java.util.Date;
import java.util.List;
import java.util.LinkedList;
import java.util.StringTokenizer;
 
 
public class RequestProcessor implements Runnable {
 
	private static List pool=new LinkedList();
	private File documentRootDirectory;
	private String indexFileName="index.html";
	
	public RequestProcessor(File documentRootDirectory,String indexFileName) {
		if (documentRootDirectory.isFile()) {
			throw new IllegalArgumentException();
		}
		this.documentRootDirectory=documentRootDirectory;
		try {
			this.documentRootDirectory=documentRootDirectory.getCanonicalFile();
		} catch (IOException e) {
		}
		
		if (indexFileName!=null) {
			this.indexFileName=indexFileName;
		}
	}
	
	public static void processRequest(Socket request) {
		synchronized (pool) {
			pool.add(pool.size(),request);
			pool.notifyAll();
		}
	}
	
	@Override
	public void run() {
		//安全性檢測
		String root=documentRootDirectory.getPath();
		
		while (true) {
			Socket connection;
			synchronized (pool) {
				while (pool.isEmpty()) {
					try {
						pool.wait();
					} catch (InterruptedException e) {
					}
					
				}
				connection=(Socket)pool.remove(0);
			}
			
			try {
				String fileName;
				String contentType;
				OutputStream raw=new BufferedOutputStream(connection.getOutputStream());
				Writer out=new OutputStreamWriter(raw);
				Reader in=new InputStreamReader(new BufferedInputStream(connection.getInputStream()), "ASCII");
				
				StringBuffer request=new StringBuffer(80);
				while (true) {
					int c=in.read();
					if (c=='\t'||c=='\n'||c==-1) {
						break;
					}
					request.append((char)c);
				}
				
				String get=request.toString();
				//記錄日誌
				System.out.println(get);
				
				StringTokenizer st=new StringTokenizer(get);
				String method=st.nextToken();
				String version="";
				if (method=="GET") {
					fileName=st.nextToken();
					if (fileName.endsWith("/")) {
						fileName+=indexFileName;
					}
					contentType=guessContentTypeFromName(fileName);
					if (st.hasMoreTokens()) {
						version=st.nextToken();
					}
					
					File theFile=new File(documentRootDirectory,fileName.substring(1,fileName.length()));
					if (theFile.canRead()&&theFile.getCanonicalPath().startsWith(root)) {
						DataInputStream fis=new DataInputStream(new BufferedInputStream(new FileInputStream(theFile)));
						byte[] theData=new byte[(int)theFile.length()];
						fis.readFully(theData);
						fis.close();
						if (version.startsWith("HTTP ")) {
							out.write("HTTP/1.0 200 OK\r\n");
							Date now=new Date();
							out.write("Date: "+now+"\r\n");
							out.write("Server: JHTTP 1.0\r\n");
							out.write("Content-length: "+theData.length+"\r\n");
							out.write("Content-Type: "+contentType+"\r\n\r\n");
							out.flush();
						}
						raw.write(theData);
						raw.flush();
					}else {
						if (version.startsWith("HTTP ")) {
							out.write("HTTP/1.0 404 File Not Found\r\n");
							Date now=new Date();
							out.write("Date: "+now+"\r\n");
							out.write("Server: JHTTP 1.0\r\n");
							out.write("Content-Type: text/html\r\n\r\n");
							out.flush();
						}
						out.write("<HTML>\r\n");
						out.write("<HEAD><TITLE>File Not Found</TITLE></HRAD>\r\n");
						out.write("<BODY>\r\n");
						out.write("<H1>HTTP Error 404: File Not Found</H1>");
						out.write("</BODY></HTML>\r\n");
					}
				}else {//方法不等於"GET"
					if (version.startsWith("HTTP ")) {
						out.write("HTTP/1.0 501 Not Implemented\r\n");
						Date now=new Date();
						out.write("Date: "+now+"\r\n");
						out.write("Server: JHTTP 1.0\r\n");
						out.write("Content-Type: text/html\r\n\r\n");
						out.flush();
					}
					out.write("<HTML>\r\n");
					out.write("<HEAD><TITLE>Not Implemented</TITLE></HRAD>\r\n");
					out.write("<BODY>\r\n");
					out.write("<H1>HTTP Error 501: Not Implemented</H1>");
					out.write("</BODY></HTML>\r\n");
				}
				
			} catch (IOException e) {
			}finally{
				try {
					connection.close();
				} catch (IOException e2) {
				}
				
			}
		}
	}
	
	public static String guessContentTypeFromName(String name) {
		if (name.endsWith(".html")||name.endsWith(".htm")) {
			return "text/html";
		}else if (name.endsWith(".txt")||name.endsWith(".java")) {
			return "text/plain";
		}else if (name.endsWith(".gif")) {
			return "image/gif";
		}else if (name.endsWith(".class")) {
			return "application/octet-stream";
		}else if (name.endsWith(".jpg")||name.endsWith(".jpeg")) {
			return "image/jpeg";
		}else {
			return "text/plain";
		}
	}
 
}

不足與改善:這個服務器可以提供一定的功能,但仍然十分簡單,還可以添加以下的一些特性:

(1)服務器管理界面

(2)支持CGI程序和Java Servlet API

(3)支持其他請求方法

(4)常見Web日誌文件格式的日誌文件

(5)支持多文檔根目錄,這樣各用戶可以有自己的網站

最後,花點時間考慮一下可以採用什麼方法來優化此服務器。如果真的希望使用JHTTP運行高流量的網站,還可以做一些事情來加速此服務器。第一點也是最重要的一點就是使用即時編譯器(JIT),如HotSpot。JIT可以將程序的性能提升大約一個數量級。第二件事就是實現智能緩存。記住接受的請求,將最頻繁的請求文件的數據存儲在Hashtable中,使之保存在內存中。使用低優先級的線程更新此緩存。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章