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)簡(jiǎn)單介紹過了(這里只是處理一些簡(jiǎn)單的請(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)文比較簡(jiǎn)單一些。
所以,這篇博客就先只介紹如何組裝報(bào)文(注意這里雖然是簡(jiǎn)單的報(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è)簡(jiǎn)單的模擬,這里其實(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è)簡(jiǎn)單的模擬,這里只能接收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è)簡(jiǎn)單的程序,已經(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)文章
Java簡(jiǎn)化復(fù)雜系統(tǒng)調(diào)用的門面設(shè)計(jì)模式
Java門面模式是一種結(jié)構(gòu)性設(shè)計(jì)模式,它為復(fù)雜系統(tǒng)提供了一個(gè)簡(jiǎn)單的接口,使得系統(tǒng)的客戶端能夠更加方便地使用系統(tǒng)功能。門面模式通過封裝復(fù)雜的子系統(tǒng),隱藏系統(tǒng)的實(shí)現(xiàn)細(xì)節(jié),提高了系統(tǒng)的易用性和靈活性2023-04-04
dependencies導(dǎo)致的Maven依賴出錯(cuò)包紅問題解決方法
多模塊和分布式開發(fā)一般都是有專門的的dependencies來進(jìn)行jar包的版本依賴問題,本文主要介紹了dependencies導(dǎo)致的Maven依賴出錯(cuò)包紅問題解決方法,具有一定的參考價(jià)值,感興趣的可以了解一下2022-05-05
在Spring Boot2中使用CompletableFuture的方法教程
這篇文章主要給大家介紹了關(guān)于在Spring Boot2中使用CompletableFuture的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來一起看看吧2019-01-01
java實(shí)現(xiàn)響應(yīng)重定向發(fā)送post請(qǐng)求操作示例
這篇文章主要介紹了java實(shí)現(xiàn)響應(yīng)重定向發(fā)送post請(qǐng)求操作,結(jié)合實(shí)例形式分析了java請(qǐng)求響應(yīng)、重定向及數(shù)據(jù)處理相關(guān)操作技巧,需要的朋友可以參考下2020-04-04
MyBatis學(xué)習(xí)教程(四)-如何快速解決字段名與實(shí)體類屬性名不相同的沖突問題
我們經(jīng)常會(huì)遇到表中的字段名和表對(duì)應(yīng)實(shí)體類的屬性名稱不一定都是完全相同的情況,如何解決呢?下面腳本之家小編給大家介紹MyBatis學(xué)習(xí)教程(四)-如何快速解決字段名與實(shí)體類屬性名不相同的沖突問題,一起學(xué)習(xí)吧2016-05-05
SpringBoot+MinIO實(shí)現(xiàn)文件切片極速詳解
在現(xiàn)代Web應(yīng)用中,文件上傳是一個(gè)常見的需求,尤其是對(duì)于大文件的上傳,如視頻、音頻或大型文檔,所以本文就來為大家介紹一下如何使用Spring Boot和MinIO實(shí)現(xiàn)文件切片極速上傳技術(shù)吧2023-12-12

