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 OKrn"+
"Server: OneFile 1.0rn"+
"Content-length: "+this.content.length+"rn"+
"Content-type: "+MIMEType+"rnrn";
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 FOUNDrn");
Date now=new Date();
out.write("Date: "+now+"rn");
out.write("Server: Redirector 1.0rn");
out.write("Location: "+newSite+theFile+"rn");
out.write("Content-Type: text/htmlrnrn");
out.flush();
}
//并非所有的瀏覽器都支持重定向,
//所以我們需要生成一個適用于所有瀏覽器的HTML文件,來描述這一行為
out.write("<HTML><HEAD><TITLE>Document moved</TITLE></HEAD>rn");
out.write("<BODY><H1>Document moved</H1></BODY>rn");
out.write("The document "+theFile
+" has moved to rn<A HREF=""+newSite+theFile+"">"
+newSite+theFile
+"</A>.rn Please update your bookmarks");
out.write("</BODY></HTML>rn");
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 OKrn");
Date now=new Date();
out.write("Date: "+now+"rn");
out.write("Server: JHTTP 1.0rn");
out.write("Content-length: "+theData.length+"rn");
out.write("Content-Type: "+contentType+"rnrn");
out.flush();
}
raw.write(theData);
raw.flush();
}else {
if (version.startsWith("HTTP ")) {
out.write("HTTP/1.0 404 File Not Foundrn");
Date now=new Date();
out.write("Date: "+now+"rn");
out.write("Server: JHTTP 1.0rn");
out.write("Content-Type: text/htmlrnrn");
out.flush();
}
out.write("<HTML>rn");
out.write("<HEAD><TITLE>File Not Found</TITLE></HRAD>rn");
out.write("<BODY>rn");
out.write("<H1>HTTP Error 404: File Not Found</H1>");
out.write("</BODY></HTML>rn");
}
}else {//方法不等于"GET"
if (version.startsWith("HTTP ")) {
out.write("HTTP/1.0 501 Not Implementedrn");
Date now=new Date();
out.write("Date: "+now+"rn");
out.write("Server: JHTTP 1.0rn");
out.write("Content-Type: text/htmlrnrn");
out.flush();
}
out.write("<HTML>rn");
out.write("<HEAD><TITLE>Not Implemented</TITLE></HRAD>rn");
out.write("<BODY>rn");
out.write("<H1>HTTP Error 501: Not Implemented</H1>");
out.write("</BODY></HTML>rn");
}
} 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中,使之保存在內存中。使用低優先級的線程更新此緩存。