引言
HTTP協議作為Web開發的基礎一直被大多數人所熟知,不過相信有很多人只知其一不知其二。比如咱們經常用到的session會話機制是如何實現的,可能很多人都說不出來吧。其實session會話就是HTTP協議中的一個header屬性cookie所支持的,在你了解了HTTP協議之后,其實這些都非常容易理解。
本文會嘗試從各位的日常開發去解釋一下HTTP到底是做什么的,文章篇幅有限,如果有什么本文沒有提到的,各位請自行百度或者看書補腦。接下來,咱們先來看一個小A和小B的故事。
小故事:兩個人的任務
小A和小B是中國著名的互聯網公司BAT的兩個員工,時間走到2015年4月15日下午6點半,北京風沙漫天,仿佛是妖怪來臨,小A和小B興奮的正准備收拾回家,順便享受一下免費的風沙晚餐。
就在這時,項目經理大S的聲音不合時宜的響了起來,“小A,小B,你們倆先別走,給你們倆一個任務”。
兩人聽到這個聲音,苦逼的相視一笑,之后便異口同聲的說道,“頭兒,有事兒您說話”。
“你倆也別不樂意,自從來到BAT,你倆單身問題解決了,房子也買了,存款也有了,加會班也是應該的”。大S注意到二人苦逼的眼神,淡定的說道。看到二人心悅誠服的點了點頭,大S也就不再多說,吩咐道:“咱們項目里有不少地方需要判斷String是否為空,是否為空串等等,小B你寫一個工具類,小A你把調用的地方改一下。”
兩人一聽,這還不簡單,趕緊點頭哈腰的說,“包在我倆身上”。
看到二人的反應,大S滿意的點了點頭,正色說道:“記得我一直教導你們的,bug毀一時,重復毀一生,重構要趁早。”說完這句話,大S便頭也不回的離開二人,數十秒后便鑽進了風沙之中。
小A和小B簡單商量了一下,由小B來編寫String的工具類,小A來調用小B的工具類方法。工具類名叫StringUtils,里面共有兩個方法,一個叫isNull,一個叫isEmpty,參數都是一個String,返回值都是一個boolean。
由於這個類非常簡單,小B很快就搞定了它,代碼如下。
/** * 工具類 * * @author 小B * @since 4/17/2015 2:47 PM */
public abstract class StringUtils { public static boolean isNull(String s) { return s == null; } public static boolean isEmpty(String s) { return isNull(s) || s.length() == 0; } }
小B把工具類寫好以后,小A也很快把其中一個用於驗證身份證號的類改成了調用小B工具類的方式,代碼如下。
/** * 身份證號驗證工具類 * * @author 小A * @since 4/17/2015 2:50 PM */
public class CardNumberValidator { /** * 判斷身份證號是否有效 (忽略身份證號復雜的判斷規則) */
public boolean valid(String cardNumber) { if (StringUtils.isNull(cardNumber)) { throw new NullPointerException("cardNumber is null!"); } else if (StringUtils.isEmpty(cardNumber)) { throw new IllegalArgumentException("cardNumber is empty!"); } else if (cardNumber.length() == 18) { return true; } else { throw new IllegalArgumentException("the length of cardNumber must be 18!"); } } }
最后小A又寫了一個簡單的測試類測試了一下,測試類如下。
/** * 客戶端 * * @since 4/17/2015 3:01 PM */
public class Client { /** * 主函數 * * @param args */
public static void main(String[] args) { CardNumberValidator validator = new CardNumberValidator(); String cardNumber = "433182198803211232"; if (validator.valid(cardNumber)) { System.out.println(cardNumber + "是一個有效的身份證號!"); } else { System.out.println(cardNumber + "是一個無效的身份證號!"); } } }
運行之后,程序運行的結果為正確的,如下。
433182198803211232是一個有效的身份證號!
看到程序很快就正確運行,小A和小B都欣喜不已,感覺自己的技術水平又得到了質的提升。由於兩人住的比較近,於是干完活之后,二人便一同鑽進了風沙之中。
花絮與故事分析
一個小小的京城故事,兩個快樂的2B程序員。沒有復雜的故事情節,沒有高深的技術含量,但卻蘊含着一個像極了WEB資源請求的過程。回想一下剛才小A的程序調用小B程序的過程,其實包含了以下三個步驟。
1,小A的程序通過【類名.方法名(參數列表)】的形式找到了小B程序當中的方法。
2,小A的程序傳給小B的程序一個參數,小B的程序對這個請求進行相應的處理,比如判斷是否為空等等。
3,小B的程序根據處理結果返回給小A的程序,小A的程序根據返回結果進行后續的處理。
這整個過程與一個簡單的WEB資源請求如出一撤,具體的內容咱們在接下來的過程中再去討論。這里,咱們先簡單的回顧一下,小A和小B都商量了哪些東西以后,開始了各自的編程。
1,首先小A和小B定義了類名,方法名和參數類型。根據這三個內容,小A就可以通過命名找到小B的兩個方法(比如StringUtils.isNull)。那么簡單點說,類名,方法名以及參數類型就可以解決“小A怎么找到小B的方法”。
2,其次,由於規定了小A需要給小B傳送String類型的數據,那么小B就可以按照String類型進行相應的處理。因此,小A和小B對於方法參數類型的約定,就可以解決“小A傳什么數據和小B接到以后按照什么類型去處理”。
3,最后,小B和小A約定返回的類型為boolean,那么小A這邊收到結果以后就可以按照boolean類型去處理。返回結果的約定,就可以解決“小B返回什么數據和小A接收到以后按照什么類型去處理結果”。
HTTP與小故事
一次WEB資源請求的過程,其實就和一次方法調用特別相似,上面小A的程序其實就相當於瀏覽器,小B的程序就相當於服務器,而小B提供的方法就相當於服務器上的資源。上面咱們分析了方法調用的過程,咱們來看看一次WEB資源請求大致分為哪三步。
1,瀏覽器需要根據某種字符串格式(類似於故事當中的【類名.方法(參數列表)】的方式)找到服務器當中的資源。
2,瀏覽器傳給服務器一個請求,服務器對這個請求進行相應的處理(比如增刪改查)。
3,服務器根據處理結果返回給瀏覽器,瀏覽器根據返回結果進行相應的處理(比如顯示網頁,顯示圖片等)。
可以看出,這三個步驟是非常相似的。既然是相似的步驟,那么就會存在相似的問題。接下來,咱們簡單的分析一下都有哪些問題,以及這些問題如何處理。
【1】如何找到服務器當中的資源
故事當中,小A根據【類名.方法(參數列表)】的方式找到小B的方法,那么在WEB資源請求當中,瀏覽器如何找到服務器的資源呢?
相信大部分人腦子里已經浮現出了那三個字母。
是的,就是URL。URL就是專門用來定位資源的。URL的一般格式如下。
protocol :// hostname[:port] / path / [;parameters][?query]#fragment
其中各個部分的含義相信大部分人都知道,這里咱們就不過多解釋了。最重要的就是protocol,hostname和port,分別代表着應用層的協議(比如http,https,ftp等等),主機名或IP以及服務端口。
【2】瀏覽器和服務器互相傳輸的數據如何解析
這個問題其實就是第二和第三步所面臨的問題,瀏覽器要給服務器發請求,但是服務器哪知道你發的是什么玩意。同理,如果服務器給瀏覽器返回數據,瀏覽器同樣也不知道服務器返回的是什么東西。
在故事當中,小A和小B商量好了,小A給小B傳String,小B給小A返回boolean,這就很好的解決了程序之間數據交換的解析工作。當然,由於上面的程序非常簡單,所以解析的工作還不是體現的特別明顯。
假設小B的方法是一個save(Map<String,String> user)的形式,返回值也是一個Map<String,String>。這時候,如果小B和小A不商量好Map里面都需要put點啥東西的話,估計這程序也沒法寫下去了。
所以問題就出現在這里,如果不給瀏覽器和服務器制定好一個規則的話,不管是開發瀏覽器的程序員,還是開發服務的程序員,都會出現程序不知道怎么寫的情況。最可怕的是,開發瀏覽器的程序員和開發服務的程序員可不一定是同事,他們無法面對面或者通過通訊工具去商量你給我傳什么,我給你傳什么這種問題。
所以HTTP協議就應運而生了,這是一群外國人勾搭以后產生的(聽說這群外國人叫World Wide Web Consortium和Internet Engineering Task Force)。HTTP協議自出現以來,主要解決的就是瀏覽器和服務器數據交換的格式問題。
既然是解決數據交換的格式問題,所以不用去想,也知道HTTP其實是定義了一套數據格式。咱們來看一個實際的例子,一個HTTP請求到底都有哪些數據,以及這些數據是什么格式。
GET http://www.cnblogs.com/mvc/Follow/GetFollowStatus.aspx HTTP/1.1
Host: www.cnblogs.com User-Agent: Mozilla/5.0 (Windows NT 6.3; WOW64; rv:37.0) Gecko/20100101 Firefox/37.0 Accept: text/plain, */*; q=0.01 Accept-Language: zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3 Accept-Encoding: gzip, deflate X-Requested-With: XMLHttpRequest Referer: http://www.cnblogs.com Cookie: _ga=GA1.2.1892107667.1429258898; __gads=ID=ab3bc12f51ce0821:T=1429258914:S=ALNI_MYk8_dptVuXGkQkQbTMuiNN4DdPcA; .CNBlogsCookie=D84CF73E03B7891789045C601A78AAF52AB67268B93E15C1138FE9A9CE858E98FE769CE0249E5C5C68D6E84DB2CBC9D9BA77ACA3993260AA8671E17F2C3AC7B3267298C6160C09A737AF94A353F58D0B7FF5C0AD6287EFC4DBE122883CBDD2CB42C3052C; _gat=1; CNZZDATA1684828=cnzz_eid%3D1208659555-1429265886-%26ntime%3D1429265886 Connection: keep-alive
可以看到,最上面的那一行其實是協議當中定義的首行。第一個GET代表的是,這是一個get請求。后面緊跟着的是訪問的URL,最后是HTTP協議版本。
再往下就是HTTP當中定義的header了,具體每個header都代表什么意思這里就不一一解釋了,這不是本文的重點,可以去參考網絡上其它的文章。這些Header其實就相當於HTTP協議提供的一些方便的功能,你設置相應的header,可以讓服務器產生相應的行為。
舉個例子,比如Cookie這個header,大家應該再熟悉不過。它的作用就是告訴服務器當前請求者的身份,而大部分的服務器也都會自動去管理Cookie。
除了上面出現的首行和header以外,對於一些特定的請求,HTTP還有特定的數據格式。比如post請求的時候,在【Connection: keep-alive】下面會多出來一個json格式的字符串,這個字符串就是post請求時所發送的表單數據。
同樣的,服務器返回的數據格式也是相似的。一個比較實際的例子如下。
HTTP/1.1 200 OK Date: Fri, 17 Apr 2015 10:18:06 GMT Content-Type: text/html; charset=utf-8 Content-Length: 128 Connection: keep-alive Cache-Control: private X-UA-Compatible: IE=10
<a href="javascript:void(0);" onclick="cnblogs.UserManager.FollowBlogger('83f9460b-63cf-dd11-9e4d-001cf0cd104b')">+加關注</a>
響應當中依然有首行,而首行就是協議版本,加上狀態碼和狀態描述。接下來就是一堆header,這點與請求相同。不同的是,請求和響應所支持的header並不一樣。比如請求的時候,瀏覽器給服務器傳送Cookie時使用的header是Cookie。但是當服務器返回響應給瀏覽器時,如果要更新Cookie的話,對應的header是Set-Cookie。最后一行則是服務器所返回的內容,格式是由Content-Type所指定的,類型為html,字符編碼為UTF-8。對於其它的header這里就不一一解釋了,請各位自行補腦。
HTTP與WEB開發的聯系
說到這里,需要簡單提一下HTTP與WEB開發的聯系。比如大家在做J2EE開發時所熟知的request和response對象,咱們來看一下request和response接口都有哪些方法。
public interface HttpServletRequest extends ServletRequest { String BASIC_AUTH = "BASIC"; String FORM_AUTH = "FORM"; String CLIENT_CERT_AUTH = "CLIENT_CERT"; String DIGEST_AUTH = "DIGEST"; String getAuthType(); Cookie[] getCookies(); long getDateHeader(String var1); String getHeader(String var1); Enumeration getHeaders(String var1); Enumeration getHeaderNames(); int getIntHeader(String var1); String getMethod(); String getPathInfo(); String getPathTranslated(); String getContextPath(); String getQueryString(); String getRemoteUser(); boolean isUserInRole(String var1); Principal getUserPrincipal(); String getRequestedSessionId(); String getRequestURI(); StringBuffer getRequestURL(); String getServletPath(); HttpSession getSession(boolean var1); HttpSession getSession(); boolean isRequestedSessionIdValid(); boolean isRequestedSessionIdFromCookie(); boolean isRequestedSessionIdFromURL(); /** @deprecated */
boolean isRequestedSessionIdFromUrl(); }
public interface HttpServletResponse extends ServletResponse { int SC_CONTINUE = 100; int SC_SWITCHING_PROTOCOLS = 101; int SC_OK = 200; int SC_CREATED = 201; int SC_ACCEPTED = 202; int SC_NON_AUTHORITATIVE_INFORMATION = 203; int SC_NO_CONTENT = 204; int SC_RESET_CONTENT = 205; int SC_PARTIAL_CONTENT = 206; int SC_MULTIPLE_CHOICES = 300; int SC_MOVED_PERMANENTLY = 301; int SC_MOVED_TEMPORARILY = 302; int SC_FOUND = 302; int SC_SEE_OTHER = 303; int SC_NOT_MODIFIED = 304; int SC_USE_PROXY = 305; int SC_TEMPORARY_REDIRECT = 307; int SC_BAD_REQUEST = 400; int SC_UNAUTHORIZED = 401; int SC_PAYMENT_REQUIRED = 402; int SC_FORBIDDEN = 403; int SC_NOT_FOUND = 404; int SC_METHOD_NOT_ALLOWED = 405; int SC_NOT_ACCEPTABLE = 406; int SC_PROXY_AUTHENTICATION_REQUIRED = 407; int SC_REQUEST_TIMEOUT = 408; int SC_CONFLICT = 409; int SC_GONE = 410; int SC_LENGTH_REQUIRED = 411; int SC_PRECONDITION_FAILED = 412; int SC_REQUEST_ENTITY_TOO_LARGE = 413; int SC_REQUEST_URI_TOO_LONG = 414; int SC_UNSUPPORTED_MEDIA_TYPE = 415; int SC_REQUESTED_RANGE_NOT_SATISFIABLE = 416; int SC_EXPECTATION_FAILED = 417; int SC_INTERNAL_SERVER_ERROR = 500; int SC_NOT_IMPLEMENTED = 501; int SC_BAD_GATEWAY = 502; int SC_SERVICE_UNAVAILABLE = 503; int SC_GATEWAY_TIMEOUT = 504; int SC_HTTP_VERSION_NOT_SUPPORTED = 505; void addCookie(Cookie var1); boolean containsHeader(String var1); String encodeURL(String var1); String encodeRedirectURL(String var1); /** @deprecated */ String encodeUrl(String var1); /** @deprecated */ String encodeRedirectUrl(String var1); void sendError(int var1, String var2) throws IOException; void sendError(int var1) throws IOException; void sendRedirect(String var1) throws IOException; void setDateHeader(String var1, long var2); void addDateHeader(String var1, long var2); void setHeader(String var1, String var2); void addHeader(String var1, String var2); void setIntHeader(String var1, int var2); void addIntHeader(String var1, int var2); void setStatus(int var1); /** @deprecated */
void setStatus(int var1, String var2); }
可以看到,request和response里面有好幾個方法都和header有關,使用這些方法就可以取到相應的HTTP請求當中的header內容,也可以返回相應的header內容給瀏覽器。還有一點,就是response接口當中定義了一大把狀態碼和狀態描述,比如200對應OK,404對應NOT_FOUND,500對應內部錯誤等等。
可以預見的是,在編寫HttpServletRequest和HttpServletResponse這兩個接口的時候,一定是參照HTTP協議去定義的,而且每當HTTP協議進行一次大的變更,這兩個接口都要跟着進行相應的變化。
總的來說,把對HTTP的了解和日常的開發聯系起來,更加有助於你理解HTTP協議,而且有時候也可以利用HTTP協議擴展一些功能,比如授權服務,自定義的狀態保持等等。
小結
到此,大家應該基本上了解HTTP主要是用來做什么的了,具體HTTP協議當中都規定了哪些內容,大家可以去找各種資料翻閱。個人覺得,只要深刻理解HTTP協議是做什么的,了解一些常用的協議內容就行了,你並不需要把HTTP所有的header都給背下來並記住它們的作用。
最后,重復一下那句與HTTP無關的話:bug毀一時,重復毀一生,重構要趁早。