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;}@Overridepublic 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();}}@Overridepublic 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中,使之儲存在記憶體中。使用低優先順序的線程更新此緩衝。
——參考自網路編程