HTTP 概述
HTTP 客戶程序必須先發出一個 HTTP 請求,然後才能接收到來自 HTTP 服器的響應,瀏覽器就是最常見的 HTTP 客戶程序。HTTP 客戶程序和 HTTP 服務器分別由不同的軟件開發商提供,它們都可以用任意的編程語言編寫。HTTP 嚴格規定了 HTTP 請求和 HTTP 響應的數據格式,只要 HTTP 服務器與客戶程序都遵守 HTTP,就能彼此看得懂對方發送的消息
1. HTTP 請求格式
下面是一個 HTTP 請求的例子
POST /hello.jsp HTTP/1.1
Accept:image/gif, image/jpeg, */*
Referer: http://localhost/login.htm
Accept-Language: en,zh-cn;q=0.5
Content-Type: application/x-www-form-urlencoded
Accept-Encoding: gzip, deflate
User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 10.0)
Host: localhost
Content-Length:43
Connection: Keep-Alive
Cache-Control: no-cache
username=root&password=12346&submit=submit
HTTP 規定,HTTP 請求由三部分構成,分別是:
-
請求方法、URI、HTTP 的版本
- HTTP 請求的第一行包括請求方式、URI 和協議版本這三項內容,以空格分開:
POST /hello.jsp HTTP/1.1
- HTTP 請求的第一行包括請求方式、URI 和協議版本這三項內容,以空格分開:
-
請求頭(Request Header)
-
請求頭包含許多有關客戶端環境和請求正文的有用信息。例如,請求頭可以聲明瀏覽器的類型、所用的語言、請求正文的類型,以及請求正文的長度等
Accept:image/gif, image/jpeg, */* Referer: http://localhost/login.htm Accept-Language: en,zh-cn;q=0.5 //瀏覽器所用的語言 Content-Type: application/x-www-form-urlencoded //正文類型 Accept-Encoding: gzip, deflate User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 10.0) //瀏覽器類型 Host: localhost //遠程主機 Content-Length:43 //正文長度 Connection: Keep-Alive Cache-Control: no-cache
-
-
請求正文(Request Content)
-
HTTP 規定,請求頭和請求正文之間必須以空行分割(即只有 CRLF 符號的行),這個空行非常重要,它表示請求頭已經結束,接下來是請求正文,請求正文中可以包含客戶以 POST 方式提交的表單數據
username=root&password=12346&submit=submit
-
2. HTTP 響應格式
下面是一個 HTTP 響應的例子
HTTP/1.1 200 0K
Server: nio/1.1
Content-type: text/html; charset=GBK
Content-length:97
<html>
<head>
<title>helloapp</title>
</head>
<body >
<h1>hello</h1>
</body>
</htm1>
HTTP 響應也由三部分構成,分別是:
-
HTTP 的版本、狀態代碼、描述
- HTTP 響應的第一行包括服務器使用的 HTTP 的版本、狀態代碼,以及對狀態代碼的描述,這三項內容之間以空格分割
-
響應頭 (Response Header)
-
響應頭也和請求頭一樣包含許多有用的信息,例如服務器類型、正文類型和正文長度等
Server: nio/1.1 //服務器類型 Content-type: text/html; charset=GBK //正文類型 Content-length:97 //正文長度
-
-
響應正文(Response Content)
-
響應正文就是服務器返回的具體的文檔,最常見的是 HTML 網頁。HTTP 響應頭與響應正文之間也必須用空行分隔
<html> <head> <title>helloapp</title> </head> <body > <h1>hello</h1> </body> </htm1>
-
創建阻塞的 HTTP 服務器
下例(SimpleHttpServer)創建了一個非常簡單的 HTTP 服務器,它接收客戶程序的 HTTP 請求,把它打印到控制檯。然後對 HTTP 請求做簡單的解析,如果客戶程序請求訪問 login.htm,就返回該網頁,否則一律返回 hello.htm 網頁。login.htm 和 hello.htm 文件位於 root 目錄下
SimpleHttpServer 監聽 80 端口,按照阻塞模式工作,採用線程池來處理每個客戶請求
public class SimpleHttpServer {
private int port = 80;
private ServerSocketChannel serverSocketChannel = null;
private ExecutorService executorService;
private static final int POOL MULTIPLE = 4;
private Charset charset = Charset.forName("GBK");
public SimpleHttpServer() throws IOException {
executorService= Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() * POOL MULTIPLE);
serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.socket().setReuseAddress(true);
serverSocketChannel.socket().bind(new InetSocketAddress(port));
System.out.println("服務器啓動");
}
public void service() {
while (true) {
SocketChannel socketChannel = null;
try {
socketChannel = serverSocketChannel.accept();
executorService.execute(new Handler(socketChannel));
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static void main(String args[])throws IOException {
new SimpleHttpServer().service();
}
public String decode(ByteBuffer buffer) {......} //解碼
public ByteBuffer encode(String str) {......} //編碼
//Handler是內部類,負責處理HTTP請求
class Handler implements Runnable {
private SocketChannel socketChannel;
public Handler(SocketChannel socketChannel) {
this.socketChannel = socketChannel;
}
public void run() {
handle(socketChannel);
}
public void handle(SocketChannel socketChannel) {
try {
Socket socket = socketChannel.socket();
ByteBuffer buffer = ByteBuffer.allocate(1024);
//接收HTTP請求,假定其長度不超過1024字節
socketChannel.read(buffer);
buffer.flip();
String request = decode(buffer);
//打印HTTP請求
System.out.print(request);
//生成HTTP響應結果
StringBuffer sb = new StringBuffer("HTTP/1.1 200 0K\r\n");
sb.append("Content-Type:text/html\r\n\r\n");
//發送HTTP響應的第1行和響應頭
socketChannel.write(encode(sb.toString()));
FileInputStream in;
//獲得HTTP請求的第1行
String firstLineOfRequest = request.substring(0, request.indexOf("\r\n"));
if(firstLineOfRequest.indexOf("login.htm") != -1) {
in = new FileInputStream("login.htm");
} else {
in = new FileInputStream("hello.htm");
}
FileChannel fileChannel = in.getChannel();
//發送響應正文
fileChannel.transferTo(0, fileChannel.size(), socketChannel);
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if(socketChannel != null) {
//關閉連接
socketChannel.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
創建非阻塞的 HTTP 服務器
下面是本節所介紹的非阻塞的 HTTP 服務器範例的模型
- HttpServer:服務器主程序,由它啓動服務器
- AcceptHandler:負責接收客戶連接
- RequestHandler:負責接收客戶的 HTTP 請求,對其解析,然後生成相應的 HTTP 響應,再把它發送給客戶
- Request:表示 HTTP 請求
- Response:表示 HTTP 響應
- Content:表示 HTTP 響應的正文
1. 服務器主程序 HttpServer
HttpServer 僅啓用了單個主線程,採用非阻塞模式來接收客戶連接,以及收發數據
public class HttpServer {
private Selector selector = null;
private ServerSocketChannel serverSocketChannel = null;
private int port = 80;
private Charset charset = Charset.forName("GBK");
public HttpServer() throws IOException {
//創建Selector和ServerSocketChannel
//把ServerSocketchannel設置爲非阻塞模式,綁定到80端口
......
}
public void service() throws IOException {
//註冊接收連接就緒事件
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT, new AcceptHandler());
while(true) {
int n = selector.select();
if(n==0) continue;
Set readyKeys = selector.selectedKeys();
Iterator it = readyKeys.iterator();
while(it.hasNext()) {
SelectionKey key = null;
try {
key = (SelectionKey) it.next();
it.remove();
final Handler handler = (Handler) key.attachment();
handler.handle(key); //由 Handler 處理相關事件
} catch(IOException e) {
e.printStackTrace();
try {
if(key != null) {
key.cancel();
key.channel().close();
}
} catch(Exception ex) {
e.printStackTrace();
}
}
}
}
}
public static void main(String args[])throws Exception {
final HttpServer server = new HttpServer();
server.service();
}
}
2. 具有自動增長的緩衝區的 ChannelIO 類
自定義的 ChannelIO 類對 SocketChannel 進行了包裝,增加了自動增長緩衝區容量的功能。當調用 socketChannel.read(ByteBuffer bufer) 方法時,如果 buffer 已滿,即使通道中還有未接收的數據,read 方法也不會讀取任何數據,而是直接返回 0,表示讀到了零字節
爲了能讀取通道中的所有數據,必須保證緩衝區的容量足夠大。在 ChannelIO 類中有一個 requestBuffer 變量,它用來存放客戶的 HTTP 請求數據,當 requestBuffer 剩餘容量已經不足 5%,並且還有 HTTP 請求數據未接收時,ChannellO 會自動擴充 requestBuffer 的容量,該功能由 resizeRequestBuffer() 方法完成
public class ChannelIO {
protected SocketChannel socketChannel;
protected ByteBuffer requestBuffer; //存放請求數據
private static int requestBufferSize = 4096;
public ChannelIO(SocketChannel socketChannel, boolean blocking) throws IOException {
this.socketChannel = socketChannel;
socketChannel.configureBlocking(blocking); //設置模式
requestBuffer = ByteBuffer.allocate(requestBufferSize);
}
public SocketChannel
() {
return socketChannel;
}
/**
* 如果原緩衝區的剩餘容量不夠,就創建一個新的緩衝區,容量爲原來的兩倍
* 並把原來緩衝區的數據拷貝到新緩衝區
*/
protected void resizeRequestBuffer(int remaining) {
if (requestBuffer.remaining() < remaining) {
ByteBuffer bb = ByteBuffer.allocate(requestBuffer.capacity() * 2);
requestBuffer.flip();
bb.put(requestBuffer); //把原來緩衝區中的數據拷貝到新的緩衝區
requestBuffer = bb;
}
}
/**
* 接收數據,把它們存放到requestBuffer
* 如果requestBuffer的剩餘容量不足5%
* 就通過resizeRequestBuffer()方法擴充容量
*/
public int read() throws IOException {
resizeRequestBuffer(requestBufferSize/20);
return socketChannel.read(requestBuffer);
}
/** 返回requestBuffer,它存放了請求數據 */
public ByteBuffer getReadBuf() {
return requestBuffer;
}
/** 發送參數指定的 ByteBuffer 的數據 */
public int write(ByteBuffer src) throws IOException {
return socketChannel.write(src);
}
/** 把FileChannel的數據寫到SocketChannel */
public long transferTo(FileChannel fc, long pos, long len) throws IOException {
return fc.transferTo(pos, len, socketChannel);
}
/** 關閉SocketChannel */
public void close() throws IOException {
socketChannel.close();
}
}
3. 負責處理各種事件的 Handler 接口
Handler 接口負責處理各種事件,它的定義如下:
public interface Handler {
public void handle(SelectionKey key) throws IOException;
}
Handler 接口有 AcceptHandler 和 RequestHandler 兩個實現類。AcceptHandler 負責處理接收連接就緒事件,RequestHandler 負責處理讀就緒和寫就緒事件。更確切地說,RequestHandler 負責接收客戶的 HTTP 請求,以及發送 HTTP 響應
4. 負責處理接收連接就緒事件的 AcceptHandler類
AcceptHandler 負責處理接收連接就緒事件,獲得與客戶連接的 SocketChannel,然後向 Selector 註冊讀就緒事件,並且創建了一個 RequestHandler,把它作爲 SelectionKey 的附件。當讀就緒事件發生時,將由這個 RequestHandler 來處理該事件
public class AcceptHandler implements Handler {
public void handle(SelectionKey key) throws IOException {
ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();
//在非阻塞模式下,serverSocketChannel.accept()有可能返回null
SocketChannel socketChannel = serverSocketChannel.accept();
if (socketChannel == null) return;
//ChannelIO設置爲採用非阻塞模式
ChannelIO cio = new ChannelIO(socketChannel, false);
RequestHandler rh = new RequestHandler(cio);
//註冊讀就緒事件,把RequestHandler作爲附件
socketChannel.register(key.selector(), SelectionKey.OP_READ, rh);
}
}
5. 負責接收 HTTP 請求和發送 HTTP 響應的 RequestHandler 類
RequestHandler 先通過 ChannelIO 來接收 HTTP 請求,當接收到 HTTP 請求的所有數據後,就對 HTTP 請求數據進行解析,創建相應的 Request 對象,然後依據客戶的請求內容,創建相應的 Response 對象,最後發送 Response 對象中包含的 HTTP 響應數據。爲了簡化程序,RequestHandler 僅僅支持 GET 和 HEAD 兩種請求方式
public class RequestHandler implements Handler {
private ChannelIO channelIO;
//存放HTTP請求的緩衝區
private ByteBuffer requestByteBuffer = null;
//表示是否已經接收到HTTP請求的所有數據
private boolean requestReceived = false;
//表示HTTP請求
private Request request = null;
//表示HTTP響應
private Response response = null;
RequestHandler(ChannelIO channelIO) {
this.channelIO = channelIO;
}
/** 接收HTTP請求,發送HTTP響應 */
public void handle(SelectionKey sk) throws IOException {
try {
//如果還沒有接收HTTP請求的所有數據,就接收HTTP請求
if (request == null) {
if (!receive(sk)) return;
requestByteBuffer.flip();
//如果成功解析了HTTP請求,就創建一個Response對象
if (parse()) build();
try {
//準備HTTP響應的內容
response.prepare();
} catch (IOException x) {
response.release();
response = new Response(Response.Code.NOT_FOUND, new StringContent(x.getMessage()));
response.prepare();
}
if (send()) {
//如果HTTP響應沒有發送完畢,則需要註冊寫就緒事件,以便在寫就緒事件發生時繼續發送數據
sk.interestOps(SelectionKey.OP_WRITE);
} else {
//如HTTP響應發送完畢,就斷開底層連接,並且釋放Response佔用資源
channelIO.close();
response.release();
}
} else {
//如果已經接收到HTTP請求的所有數據
//如果HTTP響應發送完畢
if (!send()) {
channelIO.close();
response.release();
}
}
} catch (IOException e) {
e.printStackTrace();
channelIO.close();
if (response != null) {
response.release();
}
}
}
/**
* 接收HTTP請求,如果已經接收到了HTTP請求的所有數據,就返回true,否則返回false
*/
private boolean receive(SelectionKey sk) throws IOException {
ByteBuffer tmp = null;
//如果已經接收到HTTP請求的所有數據,就返回true
if (requestReceived) return true;
//如果已經讀到通道的末尾,或者已經讀到HTTP請求數據的末尾標誌,就返回true
if ((channelIO.read() < 0) || Request.isComplete(channelIO.getReadBuf())) {
requestByteBuffer = channelIO.getReadBuf();
return (requestReceived = true);
}
return false;
}
/**
* 通過Request類的parse()方法,解析requestByteBuffer的HTTP請求數據
* 構造相應的Request對象
*/
private boolean parse() throws IOException {
try {
request = Request.parse(requestByteBuffer);
return true;
} catch (MalformedRequestException x) {
//如果HTTP請求的格式不正確,就發送錯誤信息
response = new Response(Response.Code.BAD_REQUEST, new StringContent(x))
}
return false;
}
/** 創建HTTP響應 */
private void build() throws IOException {
Request.Action action = request.action();
//僅僅支持GET和HEAD請求方式
if ((action != Request.Action.GET) && (action != Request.Action.HEAD)) {
response = new Response(Response.Code.METHOD_NOT_ALLOWED, new StringContent("Method Not Allowed"));
} else {
response = new Response(Response.Code.OK, new FileContent(request.uri()), action);
}
}
/** 發送HTTP響應,如果全部發送完畢,就返回false,否則返回true */
private boolean send() throws IOException {
return response.send(channelIO);
}
}
6. 代表 HTTP 請求的 Request 類
RequestHandler 通過 ChannelIO 讀取 HTTP 請求數據時,這些數據被放在 requestByteBuffer 中。當 HTTP 請求的所有數據接收完畢,就要對 requestByteBufer 的數據進行解析,然後創建相應的 Request 對象。Request 對象就表示特定的 HTTP 請求
public class Request {
//枚舉類,表示HTTP請求方式
static enum Action {
GET,PUT,POST,HEAD;
}
public static Action parse(String s) {
if (s.equals("GET"))
return GET;
if (s.equals("PUT"))
return PUT;
if (s.equals("POST"))
return POST;
if (s,equals("HEAD"))
return HEAD;
throw new IllegalArgumentException(s);
}
private Action action; //請求方式
private String version; //HTTP版本
private URI uri; //URI
public Action action() { return action; }
public String version() { return version; }
public URI uri() { return uri; }
private Request(Action a, String V, URI u) {
action = a;
version = v;
uri =u;
}
public String toString() {
return (action + " " + version + " " + uri);
}
private static Charset requestCharset = Charset.forName("GBK");
/**
* 判斷ByteBuffer是否包含HTTP請求的所有數據
* HTTP請求以”r\n\r\n”結尾
*/
public static boolean isComplete(ByteBuffer bb) {
ByteBuffer temp = bb.asReadOnlyBuffer();
temp.flip();
String data = requestCharset.decode(temp).toString();
if(data.indexOf("r\n\r\n") != -1) {
return true;
}
return false;
}
/**
* 刪除請求正文
*/
private static ByteBuffer deleteContent (ByteBuffer bb) {
ByteBuffer temp = bb.asReadOnlyBuffer();
String data = requestCharset.decode(temp).toString();
if(data.indexOf("\r\n\r\n") != -1) {
data = data.substrinq(0, data.indexOf("\r\n\r\n") + 4);
return requestCharset.encode(data);
}
return bb;
}
/**
* 設定用於解析HTTP請求的字符串匹配模式,對於以下形式的HTTP請求
* GET /dir/file HTTP/1.1
* Host: hostname
* 將被解析成:
* group[l] = "GET”
* group[2]="/dir/file"
* group[3]="1.1"
* group[4]="hostname"
*/
private static Pattern requestPattern =
Pattern.compile("\\A([A-Z]+) +([^]+) +HTTP/([0-9\\.]+)$"
+ ",*^Host:([]+)$.*\r\n\r\n\\z",
Pattern.MULTILINE | Pattern.DOTALL);
/** 解析HTTP請求,創建相應的Request對象 */
public static Request parse(ByteBuffer bb) throws MalformedRequestException {
bb = deleteContent(bb); //刪除請求正文
CharBuffer cb = requestCharset.decode(bb); //解碼
Matcher m = requestPattern.matcher(cb); //進行字符串匹配
//如果HTTP請求與指定的字符串式不匹配,說明請求數據不正確
if (!m.matches())
throw new MalformedRequestException();
Action a;
//獲得請求方式
try {
a = Action.parse(m.group(1));
} catch (IllegalArgumentException x) {
throw new MalformedRequestException();
}
//獲得URI
URI u;
try {
u=new URI("http://" + m.group(4) + m.group(2));
} catch (URISyntaxException x) {
throw new MalformedRequestException();
}
//創建一個Request對象,並將其返回
return new Request(a, m.group(3), u);
}
}
7. 代表 HTTP 響應的 Response 類
Response 類表示 HTTP 響應,它有三個成員變量:code、headerBufer 和 content,它們分別表示 HTTP 響應中的狀態代碼、響應頭和正文
public class Response implements Sendable {
//枚舉類,表示狀態代碼
static enum Code {
OK(200, "OK"),
BAD_REQUEST(400, "Bad Request"),
NOT_FOUND(404, "Not Found"),
METHOD_NOT_ALLOWED(405, "Method Not Allowed");
private int number;
private String reason;
private Code(int i, String r) {
number = i;
reason =r;
}
public String toString() {
return number + " " + reason;
}
}
private Code code; //狀態代碼
private Content content; //響應正文
private boolean headersOnly; //表示HTTP響應中是否僅包含響應頭
private ByteBuffer headerBuffer = null; //響應頭
public Response(Code rc, Content c) {
this(rc, c, null);
}
public Response(Code rc, Content c, Request.Action head) {
code = rc;
content = c;
headersOnly = (head == Request.Action.HEAD);
}
/** 創建響應頭的內容,把它存放到ByteBuffer */
private ByteBuffer headers() {
CharBuffer cb = CharBuffer.allocate(1024);
while(true) {
try {
cb.put("HTTP/1.1").put(code.toString()).put(CRLF);
cb.put("Server: nio/1.1").put(CRLF);
cb.put("Content-type: ") .put(content.type()).put(CRIE);
cb.put("Content-length: ").put(Long.toString(content.length())).put(CRLF);
cb.put(CRLF);
break;
} catch (BufferOverflowException x) {
assert(cb.capacity() < (1 << 16));
cb = CharBuffer.allocate(cb.capacity() * 2);
continue;
}
}
cb.flip();
return responseCharset.encode(cb); //編碼
}
/** 準備 HTTP 響應中的正文以及響應頭的內容 */
public void prepare() throws IOException {
content.prepare();
headerBuffer= headers();
}
/** 發送HTTP響應,如果全部發送完畢,就返回false,否則返回true */
public boolean send(ChannelIO cio) throws IOException {
if (headerBuffer == null) {
throw new IllegalStateException();
}
//發送響應頭
if (headerBuffer.hasRemaining()) {
if (cio.write(headerBuffer) <= 0)
return true;
}
//發送響應正文
if (!headersOnly) {
if (content.send(cio))
return true;
}
return false;
}
/** 釋放響應正文佔用的資源 */
public void release() throws IOException {
content.release();
}
}
8. 代表響應正文的 Content 接口及其實現類
Response 類有一個成員變量 content,表示響應正文,它被定義爲 Content 類型
public interface Content extends Sendable {
//正文的類型
String type();
//返回正文的長度
//在正文準備之前,即調用prepare()方法之前,length()方法返回“-1”
long length();
}
Content 接口繼承了 Sendable 接口,Sendable 接口表示服務器端可發送給客戶的內容
public interface Sendable {
// 準備發送的內容
public void prepare() throws IOException;
// 利用通道發送部分內容,如果所有內容發送完畢,就返回false
//如果還有內容未發送,就返回true
//如果內容還沒有準備好,就拋出 IlleqalstateException
public boolean send(ChannelIO cio) throws IOException;
//當服務器發送內容完畢,就調用此方法,釋放內容佔用的資源
public void release() throws IOException;
}
Content 接口有 StringContent 和 FileContent 兩個實現類,StringContent 表示字符串形式的正文,FileContent 表示文件形式的正文
FileContent 類有一個成員變量 fleChannel,它表示讀文件的通道。FileContent 類的 send() 方法把 fileChannel 中的數據發送到 ChannelIO 的 SocketChannel 中,如果文件中的所有數據發送完畢,send() 方法就返回 false
public class FileContent implements Content {
//假定文件的根目錄爲"root",該目錄應該位於classpath下
private static File ROOT = new File("root");
private File file;
public FileContent(URI uri) {
file = new File(ROOT, uri.getPath().replace('/', File,separatorChar));
}
private String type = null;
/** 確定文件類型 */
public String type() {
if (type != null) return type;
String nm = file.getName();
if (nm.endsWith(".html") || nm.endsWith(".htm"))
type = "text/html; charset=iso-8859-1"; //HTML網頁
else if ((nm.indexOf('.') < 0) || nm.endsWith(".txt"))
type = "text/plain; charset=iso-8859-1"; //文本文件
else
type = "application/octet-stream"; //應用程序
return type;
}
private FileChannel fileChannel = null;
private long length = -1; //文件長度
private long position = -1;//文件的當前位置
public long length() {
return length;
}
/** 創建 FileChannel 對象 */
public void prepare() throws IOException {
if (fileChannel == null)
fileChannel = new RandomAccessFile(file, "r").getChannel();
length = fileChannel.size();
position =0;
}
/** 發送正文,如果發送完畢,就返回 false,否則返回true */
public boolean send(ChannelIO channelIO) throws IOException {
if (fileChannel == null)
throw new IllegalStateException();
if (position < 0)
throw new IllegalStateException();
if (position >= length)
return false; //如果發送完畢,就返回false
position += channelIO,transferTo(fileChannel, position, length - position);
return (position < length);
}
public void release() throws IOException {
if (fileChannel != null) {
fileChannel.close(); //關閉fileChannel
fileChannel = null;
}
}
}