天天看點

使用NIO實作一個簡單的伺服器

最近學習了下Java NIO,又看了網上一些大神的文章文章,感覺鹈鹕蓋頂,受益匪淺。模仿大神代碼,實作了一個簡單的伺服器(浏覽器做用戶端),在這裡和大家分享和學習,初來乍到,有什麼不對的地方還往大家及時的指出,我好及時改正。下面還是直接一步一步,看代碼實作吧。

伺服器啟動

伺服器啟動步驟總結如下幾步:

  1. 打開ServerSocketChannel通道
  2. 綁定一個伺服器,并設定通道為非阻塞(因為下面使用了Selector)
  3. 建立選擇器對象,并把ServerSocketChannel通道注冊到選擇器上面,并設定“接收”這個感興趣的事件
  4. 讓伺服器可以不斷的去接收請求
  5. 調用Selector的select(),得到目前準備就緒的通道數,如果不等于0,說明目前有通道可以處理事件
  6. 調用Selector的selectedKeys()方法,擷取所有任務對象的Set集合
  7. 通過疊代器逐個取出事件,并判斷該事件感興趣的事件做出處理

    【這裡隻是接收請求,和讀取請求資訊并給出回報,所有隻做了接收和讀取的處理,具體代碼如下】

/**
     * 伺服器啟動
     *
     * @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對象。

使用NIO實作一個簡單的伺服器

讀取請求資訊并解析代碼

/**
     * 解析請求
     *
     * @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這個請求頭(要回報資訊的格式),

使用NIO實作一個簡單的伺服器

做出響應并回報代碼

/**
     * 響應并回報
     *
     * @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;
    }

    /************************************************************/
    /************************************************************/
}

           

最後貼下代碼的下載下傳位址簡單伺服器代碼

聞道有先後,術業有專攻。往大家看過後給出寶貴的意見,或不對的地方給與指出我并改正,并對這個小應用不斷更新,盡可能的完善下,謝謝各位!