1、相關知識簡介
HTTP協議
HTTP是常用的應用層協議之一,是面向文本的協議。HTTP報文傳輸基於TCP協議,TCP協議包含頭部與數據部分,而HTTP則是包含在TCP協議的數據部分,如下圖
HTTP報文本質上是一個TCP報文,數據部分攜帶的內容為HTTP報文,HTTP報文多數情況下是一串文本,當然也可能攜帶二進制信息。
HTTP報文
HTTP報文包含頭部和請求體,請求體內容可為空。請求頭與請求體用單獨的空行分隔,即”\r\n”。HTTP頭部結構如下:
當報文為請求報文時,第一行信息為 {方法} {URI} {HTTP版本}
方法通常為GET, POST,URI為URL后面攜帶的參數信息,HTTP版本表示當前使用的HTTP版本。
當報文為響應報文時,第一行的信息為 {HTTP版本} 狀態
HTTP版本同上,下面是部分常見的狀態碼
狀態碼 | 英文名稱 | 含義 |
---|---|---|
200 | OK | 請求成功 |
304 | Not Modified | 所請求的資源未修改 |
400 | Bad Request | 客戶端請求的語法錯誤,服務器無法理解 |
403 | Forbidden | 服務器理解請求客戶端的請求,但是拒絕執行此請求 |
404 | Not Found | 服務器無法根據客戶端的請求找到資源(網頁),常說的404錯誤就是指這個 |
405 | Method Not Allowed | 客戶端請求中的方法被禁止 |
502 | Bad Gateway | 充當網關或代理的服務器,從遠端服務器接收到了一個無效的請求 |
報文從第二行開始均為 {字段名}: {字段值} 的格式。字段名通常是英文字母與”-“的組合,有常用的幾個,有時也可以使用自定義字段名,值得注意的是字段名最好不要包含空格,雖然我Postman上模擬沒問題,但在Chrome上試解析會出問題。
HTTP報文的請求體就是一段數據,沒有嚴格的格式限制,較為隨意,但如果在頭部聲明Content-Type為Multipart/form-data后就會有一定的格式規范,具體可以看看我之前寫的一篇文章
http://blog.csdn.net/kurozaki_kun/article/details/78646960
Socket
Socket是對TCP/IP的封裝,為程序員提供了面向傳輸層及以上層的編程。Java中關於Socket的類主要是Socket,DatagramSocket,ServerSocket,還有NIO對應的類,這里實現主要基於前三者。Socket能夠建立端到端的同通信。其實總結一句話,就是使用Socket能夠幫助程序員傳輸TCP/UDP報文。
2、基於Socket實現簡單的HTTP服務器
ServerSocket監聽端口
ServerSocket用於監聽特定端口,調用accept()方法會阻塞當前線程,直到接收到一個Socket,而我們需要處理所接收到的Socket。下面先寫出一個大致的框架
class ServerListeningThread extends Thread { private int bindPort; private ServerSocket serverSocket; public ServerListeningThread(int port) { this.bindPort = port; } @Override public void run() { try { serverSocket = new ServerSocket(bindPort); while (true) { Socket rcvSocket = serverSocket.accept(); //單獨寫一個類,處理接收的Socket,類的定義在下面 HttpRequestHandler request = new HttpRequestHandler(rcvSocket); request.handle(); rcvSocket.close(); } } catch (IOException e) { e.printStackTrace(); } finally { //最后要確保以下把ServerSocket關閉掉 if (serverSocket != null && !serverSocket.isClosed()) { try { serverSocket.close(); } catch (IOException e) { e.printStackTrace(); } } } } } class HttpRequestHandler { private Socket socket; public HttpRequestHandler(Socket socket) { this.socket = socket; } public void handle() throws IOException { //TODO 這里寫處理接收到的socket的邏輯 } }
回送簡單的HTTP報文
接下來的關注點應該在如何處理Socket上,先從最簡單的開始做起,不管socket里的是什么,都一律只回復一個響應報文,上面的handle()方法處理應該如下
class HttpRequestHandler {
private Socket socket; public HttpRequestHandler(Socket socket) { this.socket = socket; } public void handle() throws IOException { socket.getOutputStream(). write(("HTTP/1.1 200 OK\r\n" + //響應頭第一行 "Content-Type: text/html; charset=utf-8\r\n" + //簡單放一個頭部信息 "\r\n" + //這個空行是來分隔請求頭與請求體的 "<h1>這是響應報文</h1>\r\n").getBytes()); } }
然后來試試效果,在main函數調用一下,這里監聽8888端口
public static void main(String[] args) { new ServerListeningThread(8888).start(); }
用瀏覽器打開 127.0.0.1:8888 或 localhost:8888,能夠顯示下面結果
可以見到剛才通過socket回送的響應報文被瀏覽器成解析了,紅色箭頭位置是自己添加的頭部信息。
讀取請求並回送
一個HTTP請求真正處理起來還是比較繁瑣的,這里只介紹下簡單的情景,例如請求報文帶有POST參數,先讀取socket的數據,並控制台輸出一下HTTP請求的報文是什么樣的
class HttpRequestHandler {
//此處代碼省略 public void handle() throws IOException { //獲取輸入流,讀取數據 StringBuilder builder = new StringBuilder(); InputStreamReader isr = new InputStreamReader(socket.getInputStream()); char[] charBuf = new char[1024]; int mark; while ((mark = isr.read(charBuf)) != -1) { builder.append(charBuf, 0, mark); if (mark < charBuf.length) { break; } } System.out.println(builder.toString()); socket.getOutputStream(). write(("HTTP/1.1 200 OK\r\n" + "Content-Type: text/html; charset=utf-8\r\n" + "\r\n" + "<h1>這是響應報文</h1>\r\n").getBytes()); } }
使用postman向8888端口發送一個攜帶POST參數的HTTP請求,如下
控制台輸出結果為
其中三個提交的參數在body的表現形式為 參數名=值,多個參數用&連接成字符串,該字符串占一行。下面可以使用字符串操作將這些信息解析出來,並且將解析結果回送回去。
class HttpRequestHandler { //此處代碼省略... public void handle() throws IOException { StringBuilder builder = new StringBuilder(); InputStreamReader isr = new InputStreamReader(socket.getInputStream()); char[] charBuf = new char[1024]; int mark = -1; while ((mark = isr.read(charBuf)) != -1) { builder.append(charBuf, 0, mark); if (mark < charBuf.length) { break; } } if (mark == -1) { return; } Map<String, String> headers = new HashMap<>(); Map<String, String> parameters = new HashMap<>(); String[] splits = builder.toString().split("\r\n"); int index = 1; //處理header while (splits[index].length() > 0) { String[] keyVal = splits[index].split(":"); headers.put(keyVal[0], keyVal[1].trim()); index++; } String body = splits[index + 1]; String[] bodySplits = body.split("&"); //處理body的參數 for (String str : bodySplits) { String[] param = str.split("="); parameters.put(param[0], param[1]); } String respStr = "頭部信息\r\n"; for (Map.Entry<String, String> entry : headers.entrySet()) { respStr += "名稱: " + entry.getKey() + ", 值: " + entry.getValue() + "<br/>"; } respStr += "\r\nbody信息\r\n"; for (Map.Entry<String, String> entry : parameters.entrySet()) { respStr += "名稱: " + entry.getKey() + ", 值: " + entry.getValue() + "<br/>"; } socket.getOutputStream(). write(("HTTP/1.1 200 OK\r\n" + "Content-Type: text/html; charset=utf-8\r\n" + "\r\n" + "<h1>這是響應報文</h1>\r\n" + respStr).getBytes()); } }
使用POST方法帶參訪問8888端口,其返回結果如下
在這基礎上,還可以根據提交參數查詢數據庫等等操作,一個成熟的服務器實際上已經封裝好了如上的解析步驟,然后監聽主機的80端口(即HTTP默認端口),真正實現一個服務器要處理的情況遠比這里講述的多,例如處理文件傳輸等等。
小結
這里主要使用ServerSocket和Socket來實現,實際上還可以使用NIO的ServerSocketChannel和SocketChannel。服務器處理請求的步驟通常就是 監聽端口->收到請求->處理->響應請求,中間的處理會有多層的步驟。