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。服務器處理請求的步驟通常就是 監聽端口->收到請求->處理->響應請求,中間的處理會有多層的步驟。