HTTP 客戶程式必須先發出一個 HTTP 請求,然後才能接收到來自 HTTP 服器的響應,瀏覽器就是最常見的 HTTP 客戶程式。HTTP 客戶程式和 HTTP 伺服器分別由不同的軟體開發商提供,它們都可以用任意的程式語言編寫。HTTP 嚴格規定了 HTTP 請求和 HTTP 響應的資料格式,只要 HTTP 伺服器與客戶程式都遵守 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 的版本
POST /hello.jsp HTTP/1.1
請求頭(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
下面是一個 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 的版本、狀態程式碼、描述
響應頭 (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>
下例(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 伺服器範例的模型
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();
}
}
自定義的 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();
}
}
Handler 介面負責處理各種事件,它的定義如下:
public interface Handler {
public void handle(SelectionKey key) throws IOException;
}
Handler 介面有 AcceptHandler 和 RequestHandler 兩個實現類。AcceptHandler 負責處理接收連線就緒事件,RequestHandler 負責處理讀就緒和寫就緒事件。更確切地說,RequestHandler 負責接收客戶的 HTTP 請求,以及傳送 HTTP 響應
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);
}
}
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);
}
}
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);
}
}
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();
}
}
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;
}
}
}