淺談web伺服器專案中request請求和response的相關響應處理
我們經常使用別人的伺服器進行構建網站,現在我們就自己來寫一個自己的服務來使用。
準備工作:下載所需的題材及文件
注:完整專案下載
一、request請求獲取
1、瞭解request請求
在寫伺服器之前,我們需要知道客戶端傳送給我們哪些資訊?以及要求我們返回哪些資訊?經過測試我們能夠知道使用者客戶端傳送的資訊有以下幾點:
客戶端傳送到伺服器端的請求訊息,我們稱之為請求(request),其實就是一個按照http協議的規則拼接而成的字串,Request請求訊息包含三部分: 請求行 訊息報頭 請求正文
第一部 請求行
格式:
Method Request-URI HTTP-Version CRLF
Method表示請求方法;一般為GET或者POST ;Request-URI是一個統一資源識別符號; HTTP-Version表示請求的HTTP協議版本; CRLF表示回車和換行
例如:
GET /test.html HTTP/1.1
第二部 訊息報頭 http header
例如:
GET /test.html HTTP/1.1
Host: 127.0.0.1:9999
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; rv:47.0) Gecko/20100101 Firefox/47.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Encoding: gzip,deflate
Connection: keep-alive
第三部 請求正文 http body
請求頭和請求正文之間是一個空行,這個行非常重要,它表示請求頭已經結束,接下來的是請求正文。請求正文中可以包含客戶提交的字串資訊
注意:在第二部分header和第三部分body之間有個空行,除非沒有請求正文(如果你想要親自看到效果,請參考:瀏覽器中GET和POST的區別),這是因為使用者在瀏覽網頁時提交給伺服器的資訊是不同的
2、實現
經過以上分析,我們就能夠清楚的知道,客戶端傳送給伺服器的請求,請求資訊有使用的協議、請求的方法、請求的資源路徑、請求的訊息報頭、判斷請求的內容是否為靜態資源、判斷請求的內容是否為動態資源、判斷是否為空請求,為了使用的方便,我們需要將其封裝起來,總不能使用一次讀取一次吧,這樣做實在是太浪費系統資源與時間了,如下程式碼,就是一個介面類,用於獲取客戶端傳送過來的屬性
package com.sample.http; import java.util.Map; // http協議的請求 public interface HttpRequest { //獲得請求的協議 public String getProtocol(); //獲得請求的方法 public String getRequestMethod(); //獲得請求的路徑 public String getRequestPath(); //獲得請求的訊息報頭 public Map<String,String> getRequestHeader(); //根據引數的名字獲得請求帶過來的引數值 public String getParameter(String parameterName); //判斷當前請求的否是靜態資源 public boolean isStaticResource(); //判斷當前請求的否是動態資源 public boolean isDynamicResource(); //判斷當前請求的否是為空請求(有些瀏覽器會自動傳送空請求) public boolean isNullRequest(); }
有了介面類之後,我們就可以建立類進行實現,下面就是實現類,用於對各個方法進行處理:
package com.sample.http; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.net.Socket; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.Map.Entry; import java.util.Set; public class HttpRequestImpl implements HttpRequest{ //客戶端的Socket private Socket s; private InputStream is=null;//輸入流 private BufferedReader br=null; private HashMap<String,String> hmHeader=new HashMap<String,String>();//訊息報頭 private HashMap<String,String> hmparameters=new HashMap<String,String>();//引數集合 private boolean isNullRequest=false;//是否為空請求,預設false private String protocol=null;//請求協議 private String requestMethod=null;//請求方法 private String requestPath=null;//請求路徑 public HttpRequestImpl(Socket s) { this.s=s; getInfos();//呼叫方法 } private void getInfos()//定義一個方法,用於處理獲取的客戶端資訊 { try { is=s.getInputStream(); br=new BufferedReader(new InputStreamReader(is)); //解析第一行 String str; str=br.readLine();//readLine使用回車換行判斷一行是否結束 if(str==null) { isNullRequest=true; return; } parseRequestMethodPathProtocol(str);//呼叫自己建立在本類裡邊的方法處理第一行資訊,方法在後面 //解析第二行---第八行 String header=null; //判斷是否結束,如果結束就退出,這裡的判斷按較為饒人 //首先應該明確br.readLine()的內容,當為true是對應的情況 //也就是說當“”(中間沒有空格)與br.readLine()相等時,就進入到while中 while(!"".equals((header=br.readLine()))){ parseRequestHeader(header); } //post和get if(br.ready())//post//POST提交方式判斷,如果還有下一行就繼續讀取資訊 { char[] buf=new char[1024]; int len=br.read(buf);//使用位元組進行讀取,因為這一行沒有回車換行,readLine無法判斷是否結束 String parameter=new String(buf,len); parseRequestParamByPost(parameter);//呼叫自己建立在本類裡邊的方法處理POST方式提交的正文資訊 } else {//get,get引數的位置在第一行連線處 parseRequestParamByGet(requestPath);//呼叫自己建立在本類裡邊的方法處理GET方式提交的正文資訊 } } catch (Exception e) { e.printStackTrace(); } } //GET方法處理 private void parseRequestParamByGet(String requestPath2) { String []str1=requestPath2.split("[?]");//使用“?”分割字串 if(str1.length==2) { parseRequestParamByPost(str1[1]); } this.requestPath=str1[0]; } //POST方法處理 private void parseRequestParamByPost(String parameter) { String[] strs=parameter.split("&"); if(strs.length>=1) { for(String str:strs) { String [] sp=str.split("="); hmparameters.put(sp[0],sp[1]); } } } //解析第二行到第八行 private void parseRequestHeader(String header) throws Exception{ String[] headHost=header.split(": "); if(headHost.length!=2) { throw new Exception("訊息報頭異常,請重新提交"); } hmHeader.put(headHost[0],headHost[1]); } //解析第一行 private void parseRequestMethodPathProtocol(String str) throws Exception { String[] protocolMethodPath=new String[3];//由於第一行含有三個內容,分割後需要三個String儲存 protocolMethodPath=str.split(" ");//使用空格分割 if(protocolMethodPath.length==3) { requestMethod=protocolMethodPath[0]; requestPath=protocolMethodPath[1]; protocol=protocolMethodPath[2]; } else { throw new Exception("首行引數不合適,請重新提交"); } } //獲得請求的協議 public String getProtocol() { return protocol; } //獲得請求的方法 public String getRequestMethod(){ return requestMethod; } //獲得請求的路徑 public String getRequestPath(){ return requestPath; } //獲得請求的訊息報頭 public Map<String,String> getRequestHeader(){ return this.hmHeader; } //根據引數的名字獲得請求帶過來的引數值 public String getParameter(String parameterName){ return hmparameters.get(parameterName); } //判斷當前請求的否是靜態資源 public boolean isStaticResource(){ return true; } //判斷當前請求的否是動態資源 public boolean isDynamicResource(){ return true; } //判斷當前請求的否是為空請求(有些瀏覽器會自動傳送空請求) public boolean isNullRequest(){ return isNullRequest; } }
以上內容是對客戶端(瀏覽器)請求內容的處理,即如何進行包裝客戶端請求的資訊,並且將其包裝成一個統一的整體,既然能夠獲取瀏覽器的內容,那麼,我們就必須採取一定的措施告訴瀏覽器我們找到了你想要的檔案,並且將返回給客戶端,下面我們就來實現如何返回給客戶端想要的資訊
二、response響應處理
1、瞭解response響應
伺服器在接收和解釋客戶端的請求訊息後,伺服器會返回給客戶端一個HTTP響應訊息,我們稱之為響應(response)。其實也是一個按照http協議的規則拼接而成的一個字串
HTTP響應也是由三個部分組成,分別是: 響應狀態行、訊息報頭、響應正文
第一部分 響應狀態行
格式如下:
HTTP-Version Status-Code Reason-Phrase CRLF
例如:
HTTP/1.1 200 OK
各部分分別為:
HTTP-Version表示伺服器HTTP協議的版本;
Status-Code表示伺服器發回的響應狀態程式碼;
Reason-Phrase表示狀態程式碼的文字描述。
CRLF表示回車和換行
第二部分 訊息報頭
HTTP訊息報頭包括普通報頭、請求報頭、響應報頭、實體報頭這四大類。
每一個 報頭域 都是由 名字+冒號+空格+值 組成,訊息報頭域的名字不區分大小寫。它們的作用是描述 客戶端或者伺服器 的屬性
1.普通報頭:即可用於請求,也可用於響應,是作為一個整體而不是特定資源與事務相關聯。
2.請求報頭:允許客戶端傳遞關於自身資訊和希望的響應形式。
3.響應報頭:允許伺服器傳遞關於自身資訊的響應。
4.實體報頭:定義被傳送資源的資訊。即可用於請求,也可用於響應。
什麼是 MIME Type?
首先,我們要了解瀏覽器是如何處理內容的。在瀏覽器中顯示的內容有 HTML、有 XML、有 GIF、還有 Flash ……那麼,瀏覽器是如何區分它們,決定什麼內容用什麼形式來顯示呢?答案是 MIME Type,也就是該資源的媒體型別。媒體型別通常是通過 HTTP 協議,由 Web 伺服器告知瀏覽器的,更準確地說,是通過響應的訊息報頭裡面的屬性 Content-Type 來表示的,例如:Content-Type: text/HTML表示內容是 text/HTML 型別,也就是超文字檔案。為什麼是“text/HTML”而不是“HTML/text”或者別的什麼?MIME Type 不是個人指定的,是經過 ietf 組織協商,以 RFC 的形式作為建議的標準釋出在網上的,大多數的 Web 伺服器和使用者代理都會支援這個規範 (順便說一句,Email 附件的型別也是通過 MIME Type 指定的)。
通常只有一些在網際網路上獲得廣泛應用的格式才會獲得一個 MIME Type,如果是某個客戶端自己定義的格式,一般只能以 application/x- 開頭。XHTML 正是一個獲得廣泛應用的格式,因此,在 RFC 3236 中,說明了 XHTML 格式檔案的 MIME Type 應該是 application/xHTML+XML。當然,處理本地的檔案,在沒有人告訴瀏覽器某個檔案的 MIME Type 的情況下,瀏覽器也會做一些預設的處理,這可能和你在作業系統中給檔案配置的 MIME Type 有關。比如在 Windows 下,開啟登錄檔的“HKEY_LOCAL_MACHINESOFTWAREClassesMIMEDatabaseContent Type”主鍵,你可以看到所有 MIME Type 的配置資訊
每個MIME型別由兩部分組成,前面是資料的大類別,例如聲音audio、圖象image等,後面定義具體的種類。
常見的MIME型別
超文字標記語言文字 .html,.html text/html
普通文字 .txt text/plain
RTF文字 .rtf application/rtf
GIF圖形 .gif image/gif
JPEG圖形 .ipeg,.jpg image/jpeg
au聲音檔案 .au audio/basic
MIDI音樂檔案 mid,.midi audio/midi,audio/x-midi
RealAudio音樂檔案 .ra,.ram audio/x-pn-realaudio
MPEG檔案 .mpg,.mpeg video/mpeg
AVI檔案 .avi video/x-msvideo
GZIP檔案 .gz application/x-gzip
TAR檔案 .tar application/x-tar
第三部分 響應正文
響應正文就是伺服器返回的資源的內容
2、實現
首先,我們需要進行抽象,即將瀏覽器想要的資訊,即如下內容包裝起來,如下所示,我們將其包裝成一個介面,在抽象時我們必須認識到使用者可能會犯的錯誤,所以儘量使用過載的方法進行避免,在下面的介面中,使用了過載進行處理部分方法:
package com.sample.http; import java.io.OutputStream; import java.io.PrintWriter; //http協議的響應 public interface HttpResponse { //獲得一個指向客戶端的位元組流 public OutputStream getOutputStream()throws Exception; //獲得一個指向客戶端的字元流 public PrintWriter getPrintWriter()throws Exception; //設定響應的狀態行 引數為String型別 public void setStatusLine(String statusCode); //設定響應的狀態行 引數為int型別 public void setStatusLine(int statusCode); //設定響應訊息報頭 public void setResponseHeader(String hName,String hValue); //設定響應訊息報頭中Content-Type屬性 public void setContentType(String contentType); //設定響應訊息報頭中Content-Type屬性 並且同時設定編碼 public void setContentType(String contentType,String charsetName); //設定CRLF 回車換行 \r\n public void setCRLF(); //把設定好的響應狀態行、響應訊息報頭、固定空行這三部分寫給瀏覽器 public void printResponseHeader(); //把響應正文寫給瀏覽器 public void printResponseContent(String requestPath); }
在介面中,我們能夠看到詳細的解釋,下面我們就來實現介面中的方法:
package com.sample.http; <pre name="code" class="java">import java.io.BufferedWriter; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.io.UnsupportedEncodingException; import java.net.Socket; import com.sample.utils.ConfigUtils; import com.sample.utils.StatusCodeUtils; //http協議的響應 public class HttpResponseImpl implements HttpResponse { //宣告初始變數 Socket s;//客戶端socket OutputStream os;//輸出端位元組流 BufferedWriter bw;//輸出端字元流 PrintWriter pw; StringBuffer sbuffer;//放狀態行,\r\n,FileInputStream fis; File file; //構造器 public HttpResponseImpl(Socket s) { this.s=s; System.out.println("HttpRequestImpl(Socket s)"); os=null; bw=null; pw=null; sbuffer=new StringBuffer();//初始化,記得,否則以下的操作會遇見空指標異常 fis=null; file=null; getInfos(); } private void getInfos() { try { os=s.getOutputStream(); bw=new BufferedWriter(new OutputStreamWriter(os,"GBK")); pw=new PrintWriter(bw); } catch (Exception e) { e.printStackTrace(); } } //獲得一個指向客戶端的位元組流 public OutputStream getOutputStream()throws Exception { return os; } //獲得一個指向客戶端的字元流 public PrintWriter getPrintWriter()throws Exception { return pw; } //設定響應的狀態行 引數為String型別 public void setStatusLine(String statusCode) { String str=StatusCodeUtils.getStatusCodeValue(statusCode); //System.out.println(str+"------"+str.length()); sbuffer.append("HTTP/1.1 "+statusCode+" "+str); setCRLF(); } //設定響應的狀態行 引數為int型別 public void setStatusLine(int statusCode) { setStatusLine(statusCode+"");//將int型別轉化為String型別 } //設定響應訊息報頭 public void setResponseHeader(String hName,String hValue) { sbuffer.append(hName+": "+hValue); setCRLF(); } //設定響應訊息報頭中Content-Type屬性 public void setContentType(String contentType) { setResponseHeader("Content-Type",contentType); } //設定響應訊息報頭中Content-Type屬性 並且同時設定編碼 public void setContentType(String contentType,String charsetName) {//text/html;charset=utf-8 setContentType(";charset= "+charsetName); } //設定CRLF 回車換行 \r\n public void setCRLF() { sbuffer.append("\r\n"); } //把設定好的響應狀態行、響應訊息報頭、固定空行這三部分寫給瀏覽器 public void printResponseHeader() { //設定setResponseLine,setResponseHeader,setResponseType String res=sbuffer.toString(); pw.print(res); pw.flush(); } //把響應正文寫給瀏覽器 public void printResponseContent(String requestPath) { //響應正文 String getPath= requestPath;//客戶請求的地址 String webHome=(new ConfigUtils()).getConfigValue("rootPath"); System.out.println("配置檔案中目錄:"+webHome);//輸出從配置檔案中獲取的地址 file=new File(webHome+getPath); if(file.exists())//如果檔案存在就執行 { try { fis=new FileInputStream(file); byte[] buf=new byte[1024]; int len=-1; while((len=fis.read(buf))!=-1) { //String str=buf.toString(); //bw.write(str);//字元流寫過去是一個地址,因為寫過去之後需要瀏覽器解析,如果是圖片或者其他(圖片或視訊是位元組流)的該怎麼解析呢? //System.out.println(str); os.write(buf,len); } bw.flush(); os.flush();//os要不要關??? } catch (IOException e) { e.printStackTrace(); }finally { try { if(bw!=null) bw.close(); } catch (IOException e) { e.printStackTrace(); } } } } }
在Eclipse中寫完以上程式碼我們會發現,其中有多處錯誤資訊,其原因是我們沒有進行建立以上程式碼所要求的類,現在我們進行建立,其使用方法請參見:java.util 類 Properties ,使用參考中的方法,我們能夠進行對所需要的資訊進行配置,在以上程式碼中使用的地方有兩處,分別是【注意:這樣做的好處是增減了專案的靈活性,使用者能夠在不檢視程式碼的情況下隨時更改配置檔案等一些檔案的資訊,】:
(1)設定狀態行處
//設定響應的狀態行 引數為String型別 public void setStatusLine(String statusCode) { String str=StatusCodeUtils.getStatusCodeValue(statusCode); //System.out.println(str+"------"+str.length()); sbuffer.append("HTTP/1.1 "+statusCode+" "+str); setCRLF(); }
其中StatusCodeUtils類建立如下所示,而對於status_code.properties檔案存放在下載的準備檔案中的/webservlet/project/中,直接複製到專案中即可:
package com.sample.utils; import java.io.IOException; import java.io.InputStream; import java.util.Properties; public class StatusCodeUtils { private static Properties p; static { InputStream in=null; p=new Properties(); try { //讀了xx.properties檔案 in=StatusCodeUtils.class.getResourceAsStream("status_code.properties"); //放置到p中,即放properties檔案中的key,value p.load(in); } catch (IOException e) { e.printStackTrace(); } finally { if(in!=null) try { in.close(); } catch (IOException e) { e.printStackTrace(); } } } public static String getStatusCodeValue(String status) { return p.getProperty(status); } public static String getStatusCodeValue(int status) { return getStatusCodeValue(status+"");//沒有空格 } /*public static void main(String[] args) {//輸出測試 //Properties p=new Properties(); // p.setProperty("rootPath","ddd"); //System.out.println(p.get("rootPath")); System.out.println(getStatusCodeValue("304")); System.out.println(getStatusCodeValue("200")); }*/ }
(2)響應正文處:
//響應正文 String getPath= requestPath;//客戶請求的地址 String webHome=(new ConfigUtils()).getConfigValue("rootPath"); System.out.println("配置檔案中目錄:"+webHome);//輸出從配置檔案中獲取的地址 file=new File(webHome+getPath);
在響應正文中使用了ConfigUtils類進行了專案路徑的獲取,程式碼如下所示,對於config.properties(注意:此檔案中檔案路徑應該注意,我使用的是Linux系統,檔案結構是/home/***,而對於windows系統,目錄結構為:"C://webapps/*****,最好在位址列複製地址,寫到配置中")檔案也在準備檔案中,請自行下載,然後複製到專案中:就是下面這個東西,路徑配置合適,然後你就可以將自己的專案放在webapps目錄下,讓自己的電腦作為伺服器供其他人訪問自己的網站了
ConfigUtils路徑配置類,用於獲取專案檔案目錄位置
package com.sample.utils; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.Properties; public class ConfigUtils { private static Properties p; static { InputStream in=null; OutputStream on=null; p=new Properties(); try { //讀了xx.properties檔案 in=ConfigUtils.class.getResourceAsStream("config.properties"); //放置到p中,即放properties檔案中的key,value p.load(in); } catch (IOException e) { e.printStackTrace(); } finally { if(in!=null) try { in.close(); } catch (IOException e) { e.printStackTrace(); } } } public static String getConfigValue(String config) { return p.getProperty(config); } public static void main(String[] args) {//輸出測試 // Properties p=new Properties(); // p.setProperty("rootPath","ddd"); // System.out.println(p.get("rootPath")); System.out.println(getConfigValue("rootPath")); } }
到此為止,我們已經實現了伺服器的主要任務,接受請求和處理請求,下面我們進行測試:
寫一個測試類如下:
package com.sample.http; import java.io.IOException; import java.net.ServerSocket; import java.net.Socket; import java.util.Iterator; import java.util.Map; import java.util.Map.Entry; import java.util.Set; public class ServerTest { public static void main(String[] args) { //宣告變數 ServerSocket ss=null; Socket s=null; boolean flag=true; try { int port=10002; System.out.println("Server Port:"+port); ss=new ServerSocket(port); //while(flag) { //接受客戶端傳送過來的Socket s=ss.accept(); HttpRequestImpl request=new HttpRequestImpl(s); // 用於測試收到的資訊 System.out.println("獲取的路徑:"+request.getRequestPath()); System.out.println("獲取的:"+request.getProtocol()); System.out.println("獲取的:"+request.getRequestMethod()); System.out.println(request.getParameter("name")); System.out.println(request.getParameter("id")); Map<String,String> m=request.getRequestHeader(); Set<Entry<String,String>> set=m.entrySet(); Iterator it=set.iterator(); while(it.hasNext()) { Entry entry=(Entry<String,String> )it.next(); System.out.println(entry.getKey()+"----"+entry.getValue()); } //寫響應給瀏覽器 /* * 封裝: * */ //輸出流 HttpResponseImpl response=new HttpResponseImpl(s); response.setStatusLine(200); response.setContentType("text/html"); response.printResponseHeader(); response.setCRLF(); response.printResponseHeader(); response.printResponseContent(request.getRequestPath()); //用於輸出資訊 } } catch (IOException e) { e.printStackTrace(); } } }
在瀏覽器中輸入地址回車:http://127.0.0.1:10002/test.html?id=1212&name=suguniang
,能夠看到瀏覽器解析後的介面,當其他電腦訪問時(其他電腦指的是同一個域內的),只要將127.0.0.1修改為本地的ip地址即可
此時控制檯上也輸出相應的資訊:
自己的web伺服器專案-靜態請求和動態請求處理(二)
到此這篇關於淺談web伺服器專案中request請求和response的相關響應處理的文章就介紹到這了,更多相關web伺服器 request請求 response響應處理內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!