天天看点

使用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;
    }

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

           

最后贴下代码的下载地址简单服务器代码

闻道有先后,术业有专攻。往大家看过后给出宝贵的意见,或不对的地方给与指出我并改正,并对这个小应用不断更新,尽可能的完善下,谢谢各位!