最近學習了下Java NIO,又看了網上一些大神的文章文章,感覺鹈鹕蓋頂,受益匪淺。模仿大神代碼,實作了一個簡單的伺服器(浏覽器做用戶端),在這裡和大家分享和學習,初來乍到,有什麼不對的地方還往大家及時的指出,我好及時改正。下面還是直接一步一步,看代碼實作吧。
伺服器啟動
伺服器啟動步驟總結如下幾步:
- 打開ServerSocketChannel通道
- 綁定一個伺服器,并設定通道為非阻塞(因為下面使用了Selector)
- 建立選擇器對象,并把ServerSocketChannel通道注冊到選擇器上面,并設定“接收”這個感興趣的事件
- 讓伺服器可以不斷的去接收請求
- 調用Selector的select(),得到目前準備就緒的通道數,如果不等于0,說明目前有通道可以處理事件
- 調用Selector的selectedKeys()方法,擷取所有任務對象的Set集合
-
通過疊代器逐個取出事件,并判斷該事件感興趣的事件做出處理
【這裡隻是接收請求,和讀取請求資訊并給出回報,所有隻做了接收和讀取的處理,具體代碼如下】
/**
* 伺服器啟動
*
* @param port 伺服器端口号
*/
public void server(int port) {
try {
//打開ServerSocketChannel通道,并綁定一個伺服器,設定為非堵塞
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.socket().bind(new InetSocketAddress(port));
serverSocketChannel.configureBlocking(false);
//建立選擇器對象,并把伺服器通道注冊到選擇器,和設定了“接收”這個感興趣的事件
Selector selector = Selector.open();
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
//該方法是一個堵塞方法,傳回已經準備就緒的通道數
int select = selector.select();
if (select != 0) {
//對任務對象進行周遊,分别處理每個任務
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
//每個任務
SelectionKey selectionKey = iterator.next();
//持有任務去處理的幫助類
ServerHolder serverHolder = new ServerHolder();
if (selectionKey.isAcceptable()) {
//對連接配接任務處理
SocketChannel socketChannel = serverHolder.connectSocketChannel(selectionKey);
} else if (selectionKey.isReadable()) {
//獲得到請求的request資訊對象
Request request = serverHolder.parse(selectionKey);
//判斷請求對象是否傳過了是null,是null就不操作,
// 并從任務中删除掉這個請求,結束掉這次請求
if (null == request) {
selectionKey.cancel();
continue;
}
//根據request請求,給出相應的響應
serverHolder.couple(selectionKey, request);
}
//執行完,從任務集合中删除這個任務對象
iterator.remove();
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
接收事件的處理
1.通過事件任務對象得到伺服器對象,并接受請求
2.并把接受的這個請求(SocketChannel)注冊到Selector上
3.并設定這個請求感興趣的事件“讀取“
【其實這理我是這樣了解的:ServerSocketChannel相當于一個泳池,Selector相當遠管理者,請求端相當于客戶。泳池對遊泳這件事情比較”感興趣“,客戶來遊泳,找到了管理者,管理者檢視泳池得到了一個沒人的可以遊泳的通道,管理者把客戶領到了這條湧到上。代碼如下】
/**
* 處理接收事件
*
* @param selectionKey 任務對象
* @return
*/
public SocketChannel connectSocketChannel(SelectionKey selectionKey) {
try {
ServerSocketChannel channel = (ServerSocketChannel) selectionKey.channel();
SocketChannel socketChannel = channel.accept();
socketChannel.configureBlocking(false);
Selector selector = selectionKey.selector();
socketChannel.register(selector, SelectionKey.OP_READ);
return socketChannel;
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
讀取并解析
當連接配接接通之後,接下來就是讀取用戶端發過來的資料并解析出我們需要的内容。這裡使用浏覽器作為用戶端,了解浏覽器請求的人都知道,一個簡單的請求包括四部分:請求行、請求頭、空行 、請求體 。盜用圖15-4:
如圖所示:
1.請求行包含3部分,沒部分之間都用空格分隔,并且占一行
2.請求頭沒一行對應一個請求頭資訊
3.空行,分割請求頭和請求體
4.請求體内(伺服器隻做簡單的GET請求,是以這裡沒有對請求體進行操作)
按照這樣的格式:我們讀取到用戶端發送過來的請求資訊,然後按照上面的格式在對其進行解析,進而得到Request對象。

讀取請求資訊并解析代碼
/**
* 解析請求
*
* @param selectionKey
* @return
*/
public Request parse(SelectionKey selectionKey) {
//初始化Request對象
Request request = new Request(selectionKey);
try {
//用來拼接存儲讀取到的資料
StringBuilder stringBuilder = new StringBuilder();
SocketChannel channel = (SocketChannel) selectionKey.channel();
ByteBuffer allocate = ByteBuffer.allocate(1024);
int read = channel.read(allocate);
while (read > 0) {
allocate.flip();
stringBuilder.append(new String(allocate.array(), 0, read));
allocate.clear();
read = channel.read(allocate);
}
String string = stringBuilder.toString();
//如果沒有讀取到資料,就傳回一個null
if (string.equals("") || null == string) {
return null;
}
//對得到的資料進行解析
String[] strings = string.split("\r\n");
//請求行
String httpRequestLine = strings[0];
String[] data = httpRequestLine.split(" ");
//System.out.println("data:" + httpRequestLine);
request.setMethod(data[0]);
request.setRequestURI(data[1]);
request.setVersion(data[2]);
//請求頭
ArrayList<Header> list = new ArrayList<>();
for (int i = 1; i < strings.length; i++) {
String headerInfo = strings[i];
String[] item = headerInfo.split(": ");
Header header = new Header(item[0], item[1]);
list.add(header);
}
request.setHeaders(list);
return request;
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
Request對象代碼
public class Request {
private String method; // 請求方式
private String requestURI; // 請求的uri
private String version; //http協定版本
private List<Header> headers; // 封裝請求頭
private final SelectionKey selectionKey; // 用于擷取通道
/*****************************************************************************************/
/*****************************************************************************************/
public Request(SelectionKey selectionKey) {
this.selectionKey = selectionKey;
}
public String getMethod() {
return method;
}
public Request setMethod(String method) {
this.method = method;
return this;
}
public String getRequestURI() {
return requestURI;
}
public Request setRequestURI(String requestURI) {
this.requestURI = requestURI;
return this;
}
public String getVersion() {
return version;
}
public Request setVersion(String version) {
this.version = version;
return this;
}
public List<Header> getHeaders() {
return headers;
}
public Request setHeaders(List<Header> headers) {
this.headers = headers;
return this;
}
public SelectionKey getSelectionKey() {
return selectionKey;
}
/*****************************************************************************************/
/*****************************************************************************************/
}
對請求做出響應
上面我擷取到了請求的資料資訊Requestrian,如果請求資訊不為空,我們就根據請求做出響應,我們這裡的回報依據的請求的URL資訊。同樣,我們要封裝好浏覽器能解析的資料,也就是響應體(如下盜圖),給出回報。其他重要的資料有,請求行(協定版本、狀态碼、狀态描述),還有Content-Type這個請求頭(要回報資訊的格式),
做出響應并回報代碼
/**
* 響應并回報
*
* @param selectionKey
* @param request
*/
public void couple(SelectionKey selectionKey, Request request) {
try {
//初始化Responce對象
Responce responce = new Responce(selectionKey, request);
//擷取請求過來的URI
String requestURI = request.getRequestURI();
//URI帶"/",跳過這條線
String substring = requestURI.substring(1);
File file = new File(substring);
boolean isfile = file.exists();
System.out.println(substring + ":" + isfile);
//檔案是否存在
if (isfile) {
//判斷URI是否通路的是一個檔案夾
if (file.isDirectory()) {
responce.setStatus("202");
responce.setDes("Disagreeing to Requests");
responce.setContentType("text/html;charset=UTF-8");
} else {
responce.setStatus("200");
responce.setDes("OK");
//是一個檔案,通過字尾名得到該檔案的所屬類型,隻是部分常用檔案類型
if (requestURI.endsWith("txt")) {
responce.setContentType("text/html;charset=UTF-8");
} else if (requestURI.endsWith("jpg")) {
responce.setContentType("image/jpeg");
} else if (requestURI.endsWith("png")) {
responce.setContentType("image/png");
} else if (requestURI.endsWith("gif")) {
responce.setContentType("image/gif");
}
}
} else {
//如果隻是一個"/"說明通路的是伺服器的預設端口号
if (requestURI.equals("/")) {
responce.setStatus("200");
responce.setDes("MAIN SERVER!");
} else {
//檔案沒有找到
responce.setStatus("404");
responce.setDes("Not File!");
}
responce.setContentType("text/html;charset=UTF-8");
}
/*************************************拼接響應資訊*************************************/
//響應行
//String responseLine = this.version + " " + this.status + " " + this.des + "\r\n";
String responseLine = responce.getVersion() + " " + responce.getStatus() + " " + responce.getDes() + "\r\n";
StringBuilder stringBuilder = new StringBuilder(responseLine);
//響應頭
for (Header header : responce.getHeaders()) {
stringBuilder.append(header.getName()).append(": ").append(header.getValue()).append("\r\n");
}
//空行
stringBuilder.append("\r\n");
//無響應體的響應資訊
String responseHeader = stringBuilder.toString();
//響應
SocketChannel channel = (SocketChannel) selectionKey.channel();
ByteBuffer byteBuffer = byteBuffer = ByteBuffer.wrap(responseHeader.getBytes("UTF-8"));
channel.write(byteBuffer);
/*************************************做出回報*************************************/
if (isfile) {
if (file.isDirectory()) {
//檔案夾
String content = "拒絕通路檔案夾,喵喵喵…";
ByteBuffer buffer = ByteBuffer.wrap(content.getBytes("UTF-8"));
channel.write(buffer);
} else {
//檔案
byte[] content = getContent(substring);
if (content != null) {
ByteBuffer buffer = ByteBuffer.wrap(content);
channel.write(buffer);
}
}
} else {
String content;
if (requestURI.equals("/")) {
content = "歡迎通路【大菊為重】伺服器,喵喵喵…";
} else {
content = "沒有找到你要的檔案,喵喵喵喵…";
}
ByteBuffer buffer = ByteBuffer.wrap(content.getBytes("UTF-8"));
channel.write(buffer);
}
channel.close();
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
擷取檔案的byte[]代碼
/**
* 擷取檔案的byte[]數組
*
* @param dataOrigin
* @return
*/
public byte[] getContent(String dataOrigin) {
try {
RandomAccessFile randomAccessFile = new RandomAccessFile(dataOrigin, "rw");
FileChannel channel = randomAccessFile.getChannel();
//如果資料超過int最大值,就需要用流來傳遞了
ByteBuffer buffer = ByteBuffer.allocate((int) channel.size());
channel.read(buffer);
buffer.flip();
return buffer.array();
} catch (FileNotFoundException e) {
e.printStackTrace();
return null;
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
Responce對象代碼
public class Responce {
private final String version = "HTTP/1.1";
private String status;
private String des;
private String contentType;
private List<Header> headers = new ArrayList<>();
private final SelectionKey selectionKey;
private final Request Request;
public Responce(SelectionKey selectionKey, com.wlt.tomcat.modle.Request request) {
this.selectionKey = selectionKey;
this.Request = request;
}
/************************************************************/
/************************************************************/
public String getVersion() {
return version;
}
public String getStatus() {
return status;
}
public Responce setStatus(String status) {
this.status = status;
return this;
}
public String getDes() {
return des;
}
public Responce setDes(String des) {
this.des = des;
return this;
}
public String getContentType() {
return contentType;
}
public Responce setContentType(String contentType) {
this.contentType = contentType;
Header header = new Header();
header.setName("Content-Type");
header.setValue(contentType);
headers.add(header);
return this;
}
public List<Header> getHeaders() {
return headers;
}
public Responce setHeaders(List<Header> headers) {
this.headers = headers;
return this;
}
public SelectionKey getSelectionKey() {
return selectionKey;
}
public com.wlt.tomcat.modle.Request getRequest() {
return Request;
}
/************************************************************/
/************************************************************/
}
最後貼下代碼的下載下傳位址簡單伺服器代碼
聞道有先後,術業有專攻。往大家看過後給出寶貴的意見,或不對的地方給與指出我并改正,并對這個小應用不斷更新,盡可能的完善下,謝謝各位!