HTTP學習與Web伺服器程式設計
這次的主題是查詢HTTP協議的相關資料,基於此編寫一個簡單的Web伺服器。
需要完成的幾大主要的要求有:
1)編寫一個簡單的Web伺服器;
2)實現的伺服器應能與標準的瀏覽器進行簡單的互動;
3)記錄瀏覽器與服務的互動過程;
4)利用HTML語言編寫網頁瀏覽器可通過編寫的Web伺服器正常訪問該網頁;
5)支援多使用者併發訪問;
6)擴充套件編寫的簡單Web伺服器,使瀏覽器能夠瀏覽Web上儲存的影象
一.瞭解http協議(參考百度百科)
HTTP是一個客戶端和伺服器端請求和應答的標準(TCP)。客戶端是終端使用者,伺服器端是網站。通過使用Web瀏覽器、網路爬蟲或者其它的工具,客戶端發起一個到伺服器上指定埠(預設埠為80)的HTTP請求。(我們稱這個客戶端)叫使用者代理,應答的伺服器上儲存著(一些)資源,比如HTML檔案和影象;(我們稱)這個應答伺服器為源伺服器。“客戶”與“伺服器”是一個相對的概念,只存在於一個特定的連線期間,即在某個連線中的客戶在另一個連線中可能作為伺服器。基於HTTP協議的客戶/伺服器模式的資訊交換過程,它分四個過程:建立連線、傳送請求資訊、傳送響應資訊、關閉連線。
HTTP協議是基於請求/響應正規化的。一個客戶機與伺服器建立連線後,傳送一個請求給伺服器,請求方式的格式為,統一資源識別符號、協議版本號,後邊是MIME資訊包括請求修飾符、客戶機資訊和可能的內容。伺服器接到請求後,給予相應的響應資訊,其格式為一個狀態行包括資訊的協議版本號、一個成功或錯誤的程式碼,後邊是MIME資訊包括伺服器資訊、實體資訊和可能的內容。
其實簡單說就是任何伺服器除了包括HTML檔案以外,還有一個HTTP駐留程式,用於響應使用者請求。你的瀏覽器是HTTP客戶,向伺服器傳送請求,當瀏覽器中輸入了一個開始檔案或點選了一個超級連結時,瀏覽器就向伺服器傳送了HTTP請求,此請求被送往由IP地址指定的URL。駐留程式接收到請求,在進行必要的操作後回送所要求的檔案。在這一過程中,在網路上傳送和接收的資料已經被分成一個或多個數據包,每個資料包包括:要傳送的資料;控制資訊,即告訴網路怎樣處理資料包。TCP/IP決定了每個資料包的格式。如果事先不告訴你,你可能不會知道資訊被分成用於傳輸和再重新組合起來的許多小塊。
HTTP報文由從客戶機到伺服器的請求和從伺服器到客戶機的響應構成。
請求報文格式為:請求行 - 通用資訊頭 - 請求頭 - 實體頭 - 報文主體。請求行以方法欄位開始,後面分別是 URL 欄位和 HTTP 協議版本欄位,並以 CRLF 結尾。SP 是分隔符。除了在最後的 CRLF 序列中 CF 和 LF 是必需的之外,其他都可以不要。有關通用資訊頭,請求頭和實體頭方面的具體內容可以參照相關檔案。
應答報文格式為:狀態行 - 通用資訊頭 - 響應頭 - 實體頭 - 報文主體。狀態碼元由3位數字組成,表示請求是否被理解或被滿足。原因分析是對原文的狀態碼作簡短的描述,狀態碼用來支援自動操作,而原因分析用來供使用者使用。客戶機無需用來檢查或顯示語法。有關通用資訊頭,響應頭和實體頭方面的具體內容可以參照相關檔案。
簡而言之,使用http就像我們打電話訂貨一樣,我們可以打電話給商家,告訴他我們需要什麼規格的商品,然後商家再告訴我們什麼商品有貨,什麼商品缺貨。這些,我們是通過電話線用電話聯絡(HTTP是通過TCP/IP),當然我們也可以通過傳真,只要商家那邊也有傳真。
二.建立簡單的web伺服器
我們將使用java語言,基於java.net.Socket和java.net.ServerSocket實現一個簡單的web伺服器。
首先我們來看一下整個程式的大致結構(使用的IDE為eclipse):
有三個Java類,分別是伺服器類MyWebServer.java和需要在伺服器類裡呼叫的Request類和Responses類,分別用來處理接收到的http協議報文的解析工作和響應工作。在本工程目錄下建一個資料夾resource用來儲存所有的html檔案和圖片。
現在主要說說整個程式的思路:
1.建立一個ServerSocket物件;
2.呼叫ServerSocket物件的accept方法,等待連線,連線成功會返回一個Socket物件,否則一直阻塞等待;
3.從Socket物件中獲取InputStream和OutputStream位元組流,這兩個流分別對應request請求和response響應;
4.處理請求:讀取InputStream位元組流資訊,轉成字串形式,並解析
5.處理響應:根據解析出來的uri資訊,從WEB_ROOT目錄中尋找請求的資源資原始檔, 讀取資原始檔,並將其寫入到OutputStream位元組流中;
6.關閉Socket物件;
7.轉到步驟2,繼續等待連線請求;
(1)伺服器類
//MyWebServer.java
package homework3;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintStream;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
public class MyWebServer {
public static int PORT=8888;
public static final String WEB_ROOT="resource";
public static void main(String[] args) {
System.out.println("開啟伺服器");
try {
ServerSocket WebServer=null;
WebServer = new ServerSocket(PORT,1,InetAddress.getByName("127.0.0.1"));
while(true)
//不斷迴圈監聽是否有新的請求,有的話啟動一個執行緒響應
{
Socket Client=WebServer.accept();
new HttpConnectThread(Client).start();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
class HttpConnectThread extends Thread
{
private Socket Client;
public HttpConnectThread(Socket s)
{
Client=s;
}
public void run()
{
try {
InputStream input=null;
OutputStream output=null;
input=Client.getInputStream();
output=Client.getOutputStream();
//從Socket物件中獲取InputStream和OutputStream位元組流,
//這兩個流分別對應request請求和response響應;
Request request=new Request(input);
if(request.parse(Client)==1){
//處理請求:讀取InputStream位元組流資訊,轉成字串形式,並解析
Response response=new Response(output);
response.setRequest(request);
response.sendStaticResource(Client);
}
//處理響應:根據解析出來的資訊,從WEB_ROOT目錄中尋找請求的資源資原始檔,
//讀取資原始檔,並將其寫入到OutputStream位元組流中;
Client.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
簡單的說一下這一塊:
先是定義了伺服器的埠port(8888)和基目錄webroot(resource),然後利用ServerSocket建立了一個本機上8888埠的伺服器。為了完成要求5)支援多使用者併發訪問,我們在這邊做了兩件事,首先用一個while迴圈不斷地監聽是否有對應本伺服器的請求,第二件事對於每一個請求訪問的ip地址,新建一個執行緒與其進行互動。所以while裡邊就是利用新建一個socket並且讓他WebServer.accept()去監聽別人的請求,沒聽到就阻塞在這裡一直監聽,聽到了new一個我們寫好的執行緒HttpConnectThread並且start它。
再來看看這個執行緒的run函式。首先利用socket的getInputStream()和getOutputStream()從Socket物件中獲取InputStream和OutputStream位元組流,這兩個流分別對應request請求和response響應。request處理請求主要工作為讀取InputStream位元組流資訊,轉成字串形式,並解析。Response處理響應會根據解析出來的資訊,從WEB_ROOT目錄中尋找請求的資原始檔,讀取資原始檔,並將其寫入到OutputStream位元組流中。需要說明的是這裡我使用if(request.parse(Client)==1)這句話是因為這裡出現的一個小bug,後邊我在說明request類再詳細說明。
(2)請求類
//request.java
package homework3;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
import java.net.Socket;
public class Request {
private InputStream input;
private String uri;
public Request(InputStream input){
this.input=input;
}
public int parse(Socket socket){
//Read a set of characters from the socket
StringBuffer request=new StringBuffer(2048);
int i;
byte[] buffer=new byte[2048];
try {
i=input.read(buffer);
} catch (Exception e) {
e.printStackTrace();
i=-1;
}
for(int j=0;j<i;j++){
request.append((char)buffer[j]);
}
System.out.print(request.toString());
uri=parseUri(request.toString());
if(request.toString().split("\n")[0].contains("html")||request.toString().split("\n")[0].contains("jpg")){
return 1;
}
else{
// 下面是由伺服器直接生成的主頁內容
// 1、首先向瀏覽器輸出響應頭資訊
PrintStream out;
try {
out = new PrintStream(socket.getOutputStream(), false, "GB2312");
out.println("HTTP/1.1 200\r");
out.println("Content_Type:text/html\r");
out.println("");//報文頭和資訊之間要空一行
// 2、輸出主頁資訊
out.println(
"<HTML><BODY>"
+ "<center>"
+ "<H1>HTTP協議測試伺服器"
+ "</H1>"
+ "<form method='get' action='http://127.0.0.1:8888/'>username:<input type='text' name='username'/>password:<input type='text' name='password'/><input type='submit' value='GET測試'/></form><br/>"
+ "<form method='post' enctype='text/plain' action='http://127.0.0.1:8888/'>username:<input type='text' name='username'/>password:<input type='text' name='password'/><input type='submit' value='POST測試'/></form><br/>"
+ "</center>您提交的資料如下:<pre>" + request.toString()
+ "</pre></BODY></HTML>");
out.flush();
out.close();
System.out.println("msg.toString() "+request.toString());
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
return 1;
}
public String parseUri(String requestString){
int index1,index2;
index1=requestString.indexOf(" ");
if(index1!=-1){
index2=requestString.indexOf(" ",index1+1);
if(index2>index1){
return requestString.substring(index1+1,index2);
}
}
return null;
}
public String getUri(){
return this.uri;
}
}
這個程式我調了很久的bug,因為本來並沒有寫html檔案,而是像上邊程式中一樣直接通過printStream輸出流往socket的getOutputStream輸出html語句就可以出現對應的網頁,但是在顯示圖片的時候出現了坑爹的情況。首先使用絕對路徑的時候瀏覽器會報錯“Not allowed to load local resource”,究其原因是瀏覽器基於安全考慮不允許直接訪問。換成相對路徑吧,伺服器當前工作路徑下建一個resource放圖片,寫地址用/resource/xxx.jpg。好麼,圖片死活不出來,在瀏覽器按F12審查了半天也沒審查個所以然來,感覺就是傳過去的檔案type是text/html型別的而不是jpg型別的(但是最後改完可以顯示了我一看還是text/html型別),總之經歷了很久debug的絕望以後我換成了現在這種寫法,即要顯示圖片的話還是老老實實寫一個html頁面,如果是動態顯示記錄的瀏覽器與服務的互動過程(是的本程式你不知可以在console裡看,在瀏覽器也可以直接看),沒涉及圖片輸出,用我原來的想法。這就解釋了我為什麼要把require返回給伺服器的return分為1和0了,解析報文如果有請求圖片和html檔案,那麼返回1,呼叫response迴應對應檔案,否則返回0不呼叫response的內容,而是直接通過printStream輸出html語句動態顯示get和post過程中的報文協議。
(3)迴應請求類
//response.Java
package homework3;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintStream;
import java.net.Socket;
/**
* HTTP Response = Status-Line
* *(( general-header | response-header | entity-header ) CRLF)
* CRLF
* [message-body]
* Status-Line=Http-Version SP Status-Code SP Reason-Phrase CRLF
*
*/
public class Response {
private static final int BUFFER_SIZE=1024*1024;
Request request;
OutputStream output;
public Response(OutputStream output){
this.output=output;
}
public void setRequest(Request request){
this.request=request;
}
public void sendStaticResource(Socket socket)throws IOException{
byte[] bytes=new byte[BUFFER_SIZE];
FileInputStream fis=null;
try {
File file=new File(MyWebServer.WEB_ROOT,request.getUri());
if(file.exists()){
fis=new FileInputStream(file);
int ch=fis.read(bytes,0,BUFFER_SIZE);
String header = "HTTP/1.1 200\r\n" + "Content-Type: text/html\r\n"+
"Content-Length: " + file.length() + "\r\n" + "\r\n";
output.write(header.getBytes());
while(ch!=-1){
output.write(bytes, 0, BUFFER_SIZE);
ch=fis.read(bytes, 0, BUFFER_SIZE);
}
}else{
//file not found
String errorMessage="HTTP/1.1 404 File Not Found\r\n"+
"Content-Type:text/html\r\n"+
"Content-Length:23\r\n"+
"\r\n"+
"<h1>File Not Found</h1>";
output.write(errorMessage.getBytes());
}
} catch (Exception e) {
System.out.println(e.toString());
}finally{
if(fis!=null){
output.close();
fis.close();
}
}
}
}
(2)(3)兩個類就不詳細一句句說明了,就著註釋應該很容易明白。
三.實現效果
點開MyWebServer檔案,點選執行,console會跳出:
隨便開啟一個瀏覽器(我用的是chrome),輸入127.0.0.1:8888
在get和post隨便輸入資料點選按鈕,可以觀察下邊的資料變化情況
OK~如果想要訪問圖片和伺服器裡的html檔案,輸入127.0.0.1:8888/index.html
根據頁面內容點選連結可以訪問不同的檔案。當然回頭看一下console裡面記錄著所有的protocol資訊。
最後附上index.html和index2.html(圖片大家自己找啦)
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Web伺服器</title>
</head>
<body>
<h1>This is yaozonghai's webserver</h1>
<img src="pic\a3.jpg" /><br>
<h5>This is a simple index.Your request will be sent to my WebServer</h5>
<p>you can click <a href="index2.html"> more image</a> to scan picture on web<p>
<img src="pic\a5.jpg" /><br>
<p>you can click <a href="xxx"> observe the http protocol</a> to observe the http protocol<p>
<h3>Thank you for using<h3>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>LBJ</title>
</head>
<body>
<h1>so handsome the man is</h1>
<img src="pic\a1.jpg" />
<img src="pic\a2.jpg" />
<img src="pic\a3.jpg" />
<img src="pic\a4.jpg" />
<a href="index.html"> 返回</a>
</body>
</html>
That’s all,thank you!