Http學(xué)習(xí)之組裝報(bào)文
前面介紹了一些,基本的概念和需要具備的編程知識(shí)。下面開始來進(jìn)行代碼的編寫,前面已經(jīng)提到了最終的代碼會(huì)是一個(gè)的http服務(wù)器的小demo–一個(gè)圖床網(wǎng)站。
主要目標(biāo)介紹
這里主要涉及的知識(shí)點(diǎn)就是解析報(bào)文和組裝報(bào)文。 解析報(bào)文就是指解析HTTP請(qǐng)求報(bào)文,你需要知道報(bào)文請(qǐng)求的資源是什么。 組裝報(bào)文就是指組裝HTTP響應(yīng)報(bào)文,你需要返回客戶請(qǐng)求的相應(yīng)資源。
目標(biāo)分析
解析報(bào)文,需要獲取完整的報(bào)文,利用報(bào)文的特定結(jié)構(gòu),獲取報(bào)文里面的信息。然后依據(jù)這些信息,先客戶端返回響應(yīng)報(bào)文。這里涉及到自己解析報(bào)文,比較有難度,因?yàn)樾枰獔?bào)文的結(jié)構(gòu)特定。上一篇博客已經(jīng)簡單介紹過了(這里只是處理一些簡單的請(qǐng)求和響應(yīng)報(bào)文,不是那種特別復(fù)雜的,畢竟只是學(xué)習(xí),沒有必要自己和自己過不去?。?/p>
組裝報(bào)文,需要將客戶需要的信息組裝好,然后發(fā)送給客戶端。報(bào)文會(huì)由客戶端(通常是瀏覽器)自動(dòng)解析,這里就不需要解析了,只是把報(bào)文發(fā)送給客戶端。對(duì)于編程來說,只是涉及到IO流的處理而已。所以,組裝報(bào)文比較簡單一些。
所以,這篇博客就先只介紹如何組裝報(bào)文(注意這里雖然是簡單的報(bào)文,但也是符合規(guī)定的報(bào)文)。
組裝報(bào)文
我們來回顧一下前面介紹的知識(shí),通常一個(gè)完整的報(bào)文包括報(bào)文頭和報(bào)文體。(當(dāng)然了,GET請(qǐng)求方式是沒有請(qǐng)求體的。)
主要的代碼就是下面三行了。
out.write(header); //寫入Http報(bào)文頭部部分 out.write(content); //寫入Http報(bào)文數(shù)據(jù)部分 out.flush(); //刷新輸出流,確保緩沖區(qū)內(nèi)數(shù)據(jù)已經(jīng)發(fā)送完成
是不是感覺很神奇,所謂的HTTP報(bào)文,在TCP這個(gè)層次來看,不過就是一個(gè)字節(jié)流。(這里 header 和 content 在網(wǎng)絡(luò)上是可以看成串行的流。)
啟動(dòng)服務(wù)類
這個(gè)類和平時(shí)使用的 ServerSocket 類用法沒有什么區(qū)別,就是使用多線程來處理每一個(gè)客戶端的連接。啟動(dòng)一個(gè)ServerSocket,監(jiān)聽10000端口。
package com.dragon; import java.io.IOException; import java.net.ServerSocket; import java.net.Socket; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class HttpServer { private static ServerSocket server; /** * 啟動(dòng)服務(wù) * */ public void start() { try { server = new ServerSocket(10000); System.out.println("服務(wù)啟動(dòng)成功..."); this.receiveRequest(); } catch (IOException e) { e.printStackTrace(); System.out.println("服務(wù)啟動(dòng)失敗!"); } } /** * 接收請(qǐng)求 * */ public void receiveRequest() { ExecutorService pool = Executors.newFixedThreadPool(10); while (true) { try { Socket client = server.accept(); System.out.println("用戶"+client.getInetAddress().toString()+"建立連接" + client.toString()); pool.submit(new Connection(client)); //使用線程處理每一個(gè)請(qǐng)求。 } catch (IOException e) { e.printStackTrace(); } } } /** * 停止服務(wù) * 注:一般是不需要關(guān)閉服務(wù)的。 * */ public void stop() { try { if (server != null) { server.close(); } } catch (IOException e) { e.printStackTrace(); System.out.println("服務(wù)器關(guān)閉失?。?); } } }
連接類
對(duì)于每一個(gè)客戶端的連接,獲取用戶的請(qǐng)求,并返回響應(yīng)。因?yàn)橹皇且粋€(gè)簡單的模擬,這里其實(shí)獲取用戶的請(qǐng)求也不進(jìn)行處理(因?yàn)樘幚硇枰馕稣?qǐng)求報(bào)文),對(duì)于任何的請(qǐng)求返回的響應(yīng)都是同一個(gè)。所以,它實(shí)際上還具有一個(gè)非常有趣的特點(diǎn)–消滅了404。 相信經(jīng)常使用瀏覽器的人應(yīng)該都知道404這個(gè)錯(cuò)誤吧,404的意思是對(duì)于當(dāng)前的請(qǐng)求沒有找到請(qǐng)求的資源。所以,通??梢钥吹?Not Found 這兩個(gè)英文單詞,當(dāng)然了也可以自定義成其它的形式。因?yàn)檫@個(gè)程序,只具有接收請(qǐng)求,返回響應(yīng)的基本功能,所以,我間接消滅了404,哈哈!
注:頭部字段中,我只是返回了幾個(gè)必要的頭部。因?yàn)镠TTP頭部還是比較多的,有些也不是必要的。具體的信息,可以參考一些專業(yè)的書籍來了解更多的知識(shí)或者直接閱讀這方面的權(quán)威–RFC文檔,哈哈(不過我也沒有看,就是瞅了一眼。)。
package com.dragon; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.Socket; import java.nio.charset.Charset; import java.nio.file.Files; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Locale; import java.util.TimeZone; public class Connection implements Runnable { private static final String BLANK = " "; private static final String CRLF = "\r\n"; private byte[] content; private byte[] header; private Socket client; public Connection(Socket client) { this.client = client; } @Override public void run() { //這里不對(duì)請(qǐng)求進(jìn)行處理,只是收到請(qǐng)求之后會(huì)進(jìn)行響應(yīng),而不管是什么請(qǐng)求 //這里模擬服務(wù)器最原始的功能:請(qǐng)求、響應(yīng)。 this.response(); System.out.println("線程執(zhí)行結(jié)束了!"); } /** * 接收請(qǐng)求信息,這里只是一個(gè)簡單的模擬,這里只能接收get請(qǐng)求。 * @throws IOException * */ public String getRequestInfo(InputStream in) throws IOException { //只讀取第一行,這是我們需要的全部內(nèi)容 StringBuilder requestLine = new StringBuilder(80); while (true) { int c = in.read(); if (c == '\r' || c == '\n' || c == -1) break; requestLine.append((char)c); } return requestLine.toString(); } /** * 響應(yīng)信息 * */ public void response() { InputStream in = null; OutputStream out = null; try { in = new BufferedInputStream(client.getInputStream()); out = new BufferedOutputStream(client.getOutputStream()); //獲取輸出流 String requestInfo = this.getRequestInfo(in); //如果不讀取客戶端發(fā)來的數(shù)據(jù),服務(wù)器就會(huì)出錯(cuò)。 System.out.println(requestInfo); } catch (IOException e1) { e1.printStackTrace(); } //獲取輸入流 //響應(yīng)體數(shù)據(jù) File file = new File("D:/DragonFile/target/attitude.jpg"); String contentType = null; //文件的 MIME 類型 try { content = Files.readAllBytes(file.toPath()); //使用 Files 工具類,一次性讀取文件 contentType = Files.probeContentType(file.toPath()); //獲取文件的 MIME 類型 long length = file.length(); //獲取文件字節(jié)長度 header = this.getHeader(contentType, length); // 填充響應(yīng)頭 } catch (IOException e) { e.printStackTrace(); } try { out.write(header); //寫入Http報(bào)文頭部部分 out.write(content); //寫入Http報(bào)文數(shù)據(jù)部分 out.flush(); //刷新輸出流,確保緩沖區(qū)內(nèi)數(shù)據(jù)已經(jīng)發(fā)送完成 System.out.println("報(bào)文總大?。ㄗ止?jié)):" + (header.length + content.length)); } catch (IOException e) { e.printStackTrace(); System.out.println("客戶斷開連接或者發(fā)送失敗!"); } finally { //此處關(guān)閉 client 會(huì)導(dǎo)致程序出現(xiàn)問題,但是原因不清楚。 try { if (client != null) { client.close(); } System.out.println("請(qǐng)求結(jié)束了"); } catch (IOException e) { e.printStackTrace(); } } } //響應(yīng)頭 private byte[] getHeader(String contentType, long length) { return new StringBuilder() .append("HTTP/1.1").append(BLANK).append(200).append(BLANK).append("OK").append(CRLF) // 響應(yīng)頭部 .append("Server:"+"CrazyDragon").append(CRLF) .append("Date:").append(BLANK).append(this.getDate()).append(CRLF) .append("Content-Type:").append(BLANK).append(contentType).append(CRLF) //文件的 Content-Type 可通過Java獲取。 .append("Content-Length:").append(BLANK).append(length).append(CRLF).append(CRLF) .toString() .getBytes(Charset.forName("UTF-8")); } //獲取時(shí)間 private String getDate() { Date date = new Date(); SimpleDateFormat format = new SimpleDateFormat("EEE, d MMM yyyy HH:mm:ss 'GMT'", Locale.US); format.setTimeZone(TimeZone.getTimeZone("GMT")); // 設(shè)置時(shí)區(qū)為GMT return format.format(date); } }
主類
package com.dragon; public class Test { public static void main(String[] args) { HttpServer httpServer = new HttpServer(); httpServer.start(); } }
運(yùn)行結(jié)果
打印輸出:
瀏覽器訪問:
注:上面可以看到會(huì)有兩個(gè)請(qǐng)求,這是因?yàn)闉g覽器訪問的時(shí)候,會(huì)請(qǐng)求網(wǎng)站的圖標(biāo),通常的路徑為 /favicon.ico,這里似乎是因?yàn)榫彺妫瑳]有看到它的請(qǐng)求,如果存在請(qǐng)求行應(yīng)該為:GET /favicon.ico HTTP/1.1。
隨便訪問一個(gè)地址,看一看沒有404的網(wǎng)站!
訪問結(jié)果
注意:這里可能會(huì)遇到異常。 但是這個(gè)對(duì)于程序的運(yùn)行結(jié)果沒有影響。報(bào)錯(cuò)的原因處在這句話:out.wirte(content);
可能的原因有一下幾點(diǎn):
①:服務(wù)器的并發(fā)連接數(shù)超過了其承載量,服務(wù)器會(huì)將其中一些連接Down掉;
②:客戶關(guān)掉了瀏覽器,而服務(wù)器還在給客戶端發(fā)送數(shù)據(jù);
③:瀏覽器端按了Stop 按鈕。
④:用servlet的outputstream輸出流下載圖片時(shí),當(dāng)用戶點(diǎn)擊取消也會(huì)報(bào)這個(gè)錯(cuò)誤。
總結(jié)
目前,組裝報(bào)文這個(gè)功能已經(jīng)實(shí)現(xiàn)了,現(xiàn)在已經(jīng)很有意思了吧。通過這個(gè)簡單的程序,已經(jīng)可以真正理解為什么HTTP是建立在TCP協(xié)議之上的了吧。這里我特別選了一張圖片來進(jìn)行展示,效果應(yīng)該還是很不錯(cuò)的。
到此這篇關(guān)于Http學(xué)習(xí)之組裝報(bào)文的文章就介紹到這了,更多相關(guān)Http組裝報(bào)文內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
SpringBoot項(xiàng)目jar發(fā)布后如何獲取jar包所在目錄路徑
這篇文章主要介紹了SpringBoot項(xiàng)目jar發(fā)布后如何獲取jar包所在目錄路徑,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-11-11Java使用雙異步實(shí)現(xiàn)將Excel的數(shù)據(jù)導(dǎo)入數(shù)據(jù)庫
在開發(fā)中,我們經(jīng)常會(huì)遇到這樣的需求,將Excel的數(shù)據(jù)導(dǎo)入數(shù)據(jù)庫中,這篇文章主要來和大家講講Java如何使用雙異步實(shí)現(xiàn)將Excel的數(shù)據(jù)導(dǎo)入數(shù)據(jù)庫,感興趣的可以了解下2024-01-01SpringBoot集成redis實(shí)現(xiàn)共享存儲(chǔ)session
這篇文章主要介紹了SpringBoot集成redis實(shí)現(xiàn)共享存儲(chǔ)session的流程步驟,文中通過代碼示例介紹的非常詳細(xì),并總結(jié)了一些常見的錯(cuò)誤及解決方法,需要的朋友可以參考下2024-03-03Aop動(dòng)態(tài)代理和cglib實(shí)現(xiàn)代碼詳解
這篇文章主要介紹了Aop動(dòng)態(tài)代理和cglib實(shí)現(xiàn)代碼詳解,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-12-12MyBatis-Plus中最簡單的查詢操作教程(Lambda)
這篇文章主要給大家介紹了關(guān)于MyBatis-Plus中最簡單的查詢操作的相關(guān)資料,文中通過實(shí)例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2022-03-03Java中Array、List、Map相互轉(zhuǎn)換的方法詳解
這篇文章主要介紹了Java中Array、List、Map相互轉(zhuǎn)換的方法詳解,在實(shí)際項(xiàng)目開發(fā)中或者一些算法面試題目中經(jīng)常需要用到Java中這三種類型的相互轉(zhuǎn)換,比如對(duì)于一個(gè)整型數(shù)組中尋找一個(gè)整數(shù)與所給的一個(gè)整數(shù)值相同,需要的朋友可以參考下2023-08-08基于Apache組件分析對(duì)象池原理的實(shí)現(xiàn)案例分析
本文從對(duì)象池的一個(gè)簡單案例切入,主要分析common-pool2組件關(guān)于:池、工廠、配置、對(duì)象管理幾個(gè)角色的源碼邏輯,并且參考其在Redis中的實(shí)踐,對(duì)Apache組件分析對(duì)象池原理相關(guān)知識(shí)感興趣的朋友一起看看吧2022-04-04