用Java編寫你自己的簡單HTTP伺服器
HTTP是個大協議,完整功能的HTTP伺服器必須響應資源請求,將URL轉換為本地系統的資源名。響應各種形式的HTTP請求(GET、POST等)。處理不存在的檔案請求,返回各種形式的狀態碼,解析MIME型別等。但許多特定功能的HTTP伺服器並不需要所有這些功能。例如,很多網站只是想顯示“建設中“的訊息。很顯然,Apache對於這樣的網站是大材小用了。這樣的網站完全可以使用只做一件事情的定製伺服器。Java網路類庫使得編寫這樣的單任務伺服器輕而易舉。
定製伺服器不只是用於小網站。大流量的網站如Yahoo,也使用定製伺服器,因為與一般用途的伺服器相比,只做一件事情的伺服器通常要快得多。針對某項任務來優化特殊用途的伺服器很容易;其結果往往比需要響應很多種請求的一般用途伺服器高效得多。例如,對於重複用於多頁面或大流量頁面中的圖示和圖片,用一個單獨的伺服器處理會更好(並且還可以避免在請求時攜帶不必要的Cookie,因而可以減少請求/響應資料,從而減少下載頻寬,提升速度);這個伺服器在啟動時把所有圖片檔案讀入記憶體,從RAM中直接提供這些檔案,而不是每次請求都從磁碟上讀取。此外,如果你不想在包含這些圖片的頁面請求之外單獨記錄這些圖片,這個單獨伺服器則會避免在日誌記錄上浪費時間。
本篇為大家簡要演示三種HTTP伺服器:
(1) 簡單的單檔案伺服器
(2) 重定向伺服器
(3) 完整功能的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中,使之儲存在記憶體中。使用低優先順序的執行緒更新此快取。
——參考自網路程式設計