基于HTTP、NIO、单线程实现浏览器并发非阻塞访问服务器文件

本文深入探讨了NIO的工作原理,对比传统IO的差异,并通过一个详细的服务器实例展示了如何使用NIO来处理并发请求,包括代码实现和技术要点。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1. 理解NIO原理:

  1. NIO与传统I/O的区别:
    1>能够设置套接字通道是否阻塞。
    阻塞:一个线程处理IO的read()或者write()时该线程将会发生阻塞,直到数据读完或者写完才释放。
    非阻塞:一个线程处理准备好了的数据,有数据到IO,线程不会立马去读取,而是等数据都完全准备好了,通知处理线程可以读或者写了。这里的通知是用selector监听
    2>能够与selector结合使用实现并发请求
    只有NIO能够使用selector监听IO,负责通知处理线程准备好的数据,通知的状态有:是否可连接,是否可接受,是否可读,是否可写
    3>IO是面向流的,而NIO是面向缓冲
    例如:读取数据(写也一样)
    IO: socket.read(byte 或者 byte[])
    NIO: socketChannel.read(ByteBuffer)

  2. NIO与IO的优势和劣势
    注意:
    1、纯粹的使用NIO处理数据,有时候并不高效。比如要明确知道按行写的文件,每一行的内容进行数据处理,传统IO可以按行读取获得数据,而NIO读取到缓冲,但并不知道读取了多少行,每一行是否是所需要的数据。
    2、NIO可让您只使用一个(或几个)单线程管理多个通道(网络连接或文件),但付出的代价是解析数据可能会比从一个阻塞流中读取数据更复杂。
    3、少量的连接使用非常高的带宽,一次发送大量的数据,也许典型的IO服务器实现可能非常契合

详情:

http://tutorials.jenkov.com/java-nio/nio-vs-io.html

2.NIO服务器实例解析

一个线程管理多个连接,如果是多线程,这样就可以管理大量的连接,处理数据比较耗时采用多线程处理

程序入口是: /HttpServerReactor/src/com/StartServer.java

访问1,浏览:http://localhost:8989/files/1.txt
访问2,下载:http://localhost:8989/files/2.zip

测试并发,可以先下载,再浏览。

实现效果:

访问1,浏览:<a href=http://localhost:8989/files/1.txt” title=”” />

访问2,下载:<a href=http://localhost:8989/files/2.zip” title=”” />

3.实现代码

1.启动程序StartServer 。

package com;

import com.core.Reactor;

public class StartServer {

    private static int port = 8989;

    public static void main(String[] args) {
        try {
            new Reactor(port).run();
        } catch (Exception e) {
            // TODO: handle exception
            e.printStackTrace();
        }
    }
}

2.反应器Reactor.java

package com.core;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.util.Iterator;
import java.util.Set;

/**
 * 反应器模式 用于解决多用户访问并发问题
 * 
 * 举个例子:餐厅服务问题
 * 
 * 传统线程池做法:来一个客人(请求)去一个服务员(线程)
 * 反应器模式做法:当客人点菜的时候,服务员就可以去招呼其他客人了,等客人点好了菜,直接招呼一声“服务员”
 * 
 * @author zjx
 */
public class Reactor {
    public final Selector selector;
    public final ServerSocketChannel serverSocketChannel;

    public Reactor(int port) throws IOException {

        // 创建serverSocketChannel
        serverSocketChannel = ServerSocketChannel.open();
        // InetSocketAddress inetSocketAddress=new
        // InetSocketAddress(InetAddress.getLocalHost(),port);
        serverSocketChannel.socket().bind(new InetSocketAddress(port));

        System.out.println("服务器已经启动");

        // 设置通道非阻塞
        serverSocketChannel.configureBlocking(false);

        // 创建selector
        selector = Selector.open();

        // 向selector注册该channel
        SelectionKey acceptKey = serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

        // 利用selectionKey的attache功能绑定Acceptor 如果有事情,触发Acceptor
        acceptKey.attach(new Acceptor(this));
    }

    public void run() {
        //DebugMethod d = new DebugMethod();
        try {
            while (!Thread.interrupted()) {
                selector.select();
                Set<SelectionKey> selectionKeys = selector.selectedKeys();
                //d.debug("all keys number: " + (selector.keys().size()));
                /*for (SelectionKey key : selector.keys()) {
                    if(key.isValid())
                        d.printKeyInfo(key);
                }*/
                //d.debug("selected keys number: " + (selectionKeys.size()));
                Iterator<SelectionKey> it = selectionKeys.iterator();
                // Selector如果发现channel有OP_ACCEPT或READ事件发生,下列遍历就会进行。
                while (it.hasNext()) {
                    // 来一个事件 第一次触发一个accepter线程
                    // 以后触发SocketReadHandler
                    SelectionKey selectionKey = it.next();
                    //d.printKeyInfo(selectionKey);
                    it.remove();
                    dispatch(selectionKey);

                    //d.printKeyInfo(selectionKey);
                    //selectionKeys.clear();
                }
            }
        } catch (IOException e) {

            e.printStackTrace();
        }
    }

    /**
     * 运行Acceptor或SocketReadHandler
     * 
     * @param key
     */
    void dispatch(SelectionKey key) {
        CommonMethod r = (CommonMethod) (key.attachment());
        if (r != null) {
            r.run();
        }
    }

}

3.接受连接accepter.java

package com.core;

import java.io.IOException;  
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;  

public class Acceptor implements CommonMethod {  

    private static int acceptCount = 0;
    private Reactor reactor; 
    public Acceptor(Reactor reactor){  
        this.reactor=reactor;  
    }  
    @Override  
    public void run() {  
        try {  
            SocketChannel socketChannel=reactor.serverSocketChannel.accept();  
            acceptCount++;
            System.out.println("成功连接客户端!连接客户端数量acceptCount="+acceptCount);
            ByteBuffer inputBuffer = ByteBuffer.allocate(1024);//一个socketChannel对应一个buf
            //一个请求过来,新建一个变量,用于记录读取服务器文件的位置
            if(socketChannel!=null && socketChannel.isOpen()) {//调用Handler来处理channel  
                new SocketReadHandler(reactor.selector, socketChannel, inputBuffer, acceptCount);
            }
        } catch (IOException e) {  
            e.printStackTrace();  
        }  
    }  
} 

4、读取socketchannel,SocketReadHandler.java

package com.core;

import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.HashMap;
import java.util.Map;
import com.core.header.Request;

public class SocketReadHandler implements CommonMethod {

    private SocketChannel socketChannel;
    private SelectionKey key;
    private ByteBuffer inputBuffer;
    private int acceptCount;

    public SocketReadHandler(Selector selector, SocketChannel socketChannel, ByteBuffer inputBuffer, int acceptCount) throws IOException {

        this.socketChannel = socketChannel;
        this.inputBuffer = inputBuffer;
        this.acceptCount = acceptCount;

        socketChannel.configureBlocking(false);
        SelectionKey selectionKey = socketChannel.register(selector, 0);
        this.key = selectionKey;
        // 将SelectionKey绑定为本Handler 下一步有事件触发时,将调用本类的run方法。
        // 参看dispatch(SelectionKey key)
        selectionKey.attach(this);

        // 同时将SelectionKey标记为可读,以便读取。
        selectionKey.interestOps(SelectionKey.OP_READ);
        selector.wakeup();
    }

    /**
     * 处理读取数据
     */
    public void run() {
        try {
            // 读并解析请求
            Request req = new Request(socketChannel, inputBuffer);
            req.parseRequest();

            String uri = req.getUri();
            String web_root = System.getProperty("user.dir");
            //获取服务器文件通道
            RandomAccessFile raf = new RandomAccessFile(web_root+uri, "r");
            //激活新线程读取服务器文件
            //Map<String, Object> dealResult = new DoFile().readServerFile(uri);
            Map<String, Object> dealResult = new HashMap<String, Object>();
            dealResult.put("uri", uri);
            dealResult.put("raf", raf);
            dealResult.put("acceptCount", acceptCount);
            new SocketWriteHandlel(dealResult, key);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

5、写取sockechannel

package com.core;

import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.FileChannel;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Map;
import com.core.header.Response;

public class SocketWriteHandlel implements CommonMethod {

    private SocketChannel sc;
    private SelectionKey key;
    private Selector selector;
    private int acceptCount;
    private RandomAccessFile raf;
    private static int[] positions = new int[1000];//数组大小取决于并发的连接数

    public SocketWriteHandlel(Map<String, Object> dealResult, SelectionKey key) throws ClosedChannelException {

        this.key = key;
        this.raf = (RandomAccessFile) dealResult.get("raf");
        sc = (SocketChannel) key.channel();
        selector = key.selector();
        this.acceptCount = (Integer)dealResult.get("acceptCount");
        // 重新绑定为写对象
        key.attach(this);
        // 同时将SelectionKey标记为可读,以便读取。
        key.interestOps(SelectionKey.OP_WRITE);
        selector.wakeup();
    }

    public void run() {

        //System.out.println("acceptCount-1="+(acceptCount-1));
        try {
            //生成随机数
            FileChannel fc = raf.getChannel();
            Integer fileSize = (int) fc.size();
            if (fileSize > 1024 * 1024 * 10) {// 只在文件大于10M且第一次写头信息
                if (positions[acceptCount] == 0) {
                    // 响应信息
                    String responseCode = "200".trim();
                    String contentType = "application/octet-stream".trim();
                    String contenLen = Integer.toString(fileSize);

                    Response response = new Response(responseCode, contentType, contenLen);
                    String responseStr = response.getRespHeadInfo();

                    // 第一部分:响应头部信息
                    sc.write(ByteBuffer.wrap(responseStr.getBytes()));
                }
                // 第二部分
                long readbytes = -1;
                System.out.println("position111111111=" + positions[acceptCount]);
                readbytes = fc.transferTo(positions[acceptCount], 128, sc);//1、定义128字节,为了下载慢一点

                positions[acceptCount] += (int) readbytes;
                System.out.println("position222222222=" + positions[acceptCount]);
            } else {

                long readbytes = -1;
                readbytes = fc.transferTo(positions[acceptCount], 2048, sc);//2、定义2048为了浏览快一点,1和2这样写为了方便测试并发。

                positions[acceptCount] += (int) readbytes;
                System.out.println("**************position=" + positions[acceptCount]);
            }

            // 重新绑定为写对象
            key.attach(this);
            // 同时将SelectionKey标记为可读,以便读取。
            key.interestOps(SelectionKey.OP_WRITE);
            selector.wakeup();

            // 关闭通道
            if (positions[acceptCount] == fileSize) {
                raf.close();
                fc.close();
                sc.close();
                key.cancel();
            }
        } catch (Exception e) {
            try {
                sc.close();
            } catch (IOException e1) {
                e1.printStackTrace();
            }
            e.printStackTrace();
        }
    }
}

读写接口:

package com.core;

public interface CommonMethod {
    public void run();
}

6、解析头文件,拼接response文件Request.java Response.java

package com.core.header;

import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;

import com.core.util.DealStringOrByteBuffer;

public class Request {

    private SocketChannel sc;
    private String uri;
    private ByteBuffer readbuf;

    public Request(SocketChannel sc, ByteBuffer readbuf) {
        this.sc = sc;
        this.readbuf = readbuf;
    }
    public void parseRequest() {
        StringBuffer strbuf = new StringBuffer();
        int len = -1;
        try {
            while((len = sc.read(readbuf)) > 0) {

                readbuf.flip();
                String str = DealStringOrByteBuffer.getString(readbuf);
                strbuf.append(str);
                readbuf.clear();
            }

            if(len == -1) {
                System.out.println("断开连接!");
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        String requestStr = strbuf.toString();
        System.out.println("requestStr=" + requestStr);
        int firstSpace = requestStr.indexOf(" ");
        if(firstSpace !=-1) {

            int secondSpace = requestStr.indexOf(" ", firstSpace+1);
            if(secondSpace > firstSpace) {
                this.uri = requestStr.substring(firstSpace+1, secondSpace);//截取uri
            }
        }
    }

    public void printRequestInfo(ByteBuffer buf) {

        System.out.println("requestStr="+DealStringOrByteBuffer.getString(buf));
    }

    public String getUri() {
        return uri;
    }

    public void setUri(String uri) {
        this.uri = uri;
    }
}


package com.core.header;

package com.core.header;

/**
 * 在这里拼装响应头信息,包括,响应HTTP包括:状态行、消息报头、响应正文。 这里处理状态行、消息报头 响应头说明:
 * 
 * Location: http://www.it315.org/index.jsp(控制浏览器显示哪个页面)
 * 
 * Server:apache tomcat(服务器的类型)
 * 
 * Content-Encoding: gzip(服务器发送的压缩编码方式)
 * 
 * Content-Length: 80(服务器发送显示的字节码长度)
 * 
 * Content-Language: zh-cn(服务器发送内容的语言和国家名)
 * 
 * Content-Type: image/jpeg; charset=UTF-8(服务器发送内容的类型和编码类型)
 * 
 * Last-Modified: Tue, 11 Jul 2000 18:23:51 GMT(服务器最后一次修改的时间)
 * 
 * Refresh: 1;url=http://www.it315.org(控制浏览器1秒钟后转发URL所指向的页面)
 * 
 * Content-Disposition: attachment; filename=aaa.jpg(服务器控制浏览器发下载方式打开文件)
 * 
 * Transfer-Encoding: chunked(服务器分块传递数据到客户端)
 * 
 * Set-Cookie:SS=Q0=5Lb_nQ; path=/search(服务器发送Cookie相关的信息)
 * 
 * Expires: -1(服务器控制浏览器不要缓存网页,默认是缓存)
 * 
 * Cache-Control: no-cache(服务器控制浏览器不要缓存网页)
 * 
 * Pragma: no-cache(服务器控制浏览器不要缓存网页)
 * 
 * Connection: close/Keep-Alive(HTTP请求的版本的特点)
 * 
 * Date: Tue, 11 Jul 2000 18:23:51 GMT(响应网站的时间)
 * 
 * @author pactera
 * 
 */
public class Response {

    // 状态行
    private String proolVersion;
    private String responseCode;
    private String responseResult;

    // 消息报文
    private String location;
    private String refresh;
    private String contentType;
    private String contenLen;
    private String contentEncode;
    private String contentDisposition;
    private String transferEncoding;

    // ...

    public Response(String responseCode, String contentType, String contenLen) {
        this.proolVersion = "HTTP/1.1";
        this.responseCode = responseCode;
        if (responseCode.equals("200")) {
            this.responseResult = "OK";
        }
        this.contentType = contentType;
        this.contenLen = contenLen;
    }

    // 简单的拼装
    public String getRespHeadInfo() {

        StringBuffer responseStr = new StringBuffer();
        location = "http://localhost:8989/WebRoot/index.jsp";
        refresh = "5;url=http://www.baidu.com";
        // 拼装HTTP响应头信息
        responseStr.append(proolVersion + " " + responseCode + " " + responseResult).append("\r\n");
        // responseStr.append("Location: " + location).append("\r\n");
        // responseStr.append("Refresh: " + refresh).append("\r\n");
        // responseStr.append("Content-Disposition: attachment; filename=a.txt").append("\r\n");
        responseStr.append("Content-Type: " + contentType).append("\r\n");
        // responseStr.append("Content-Encoding: gzip").append("\r\n");
        responseStr.append("Content-Length: " + contenLen).append("\r\n");
        // responseStr.append("Connection: close/Keep-Alive").append("\r\n");
        responseStr.append("\r\n");

        System.out.println("respone: " + responseStr.toString());

        return responseStr.toString();
    }

    public String getProolVersion() {
        return proolVersion;
    }

    public void setProolVersion(String proolVersion) {
        this.proolVersion = proolVersion;
    }

    public String getResponseCode() {
        return responseCode;
    }

    public void setResponseCode(String responseCode) {
        this.responseCode = responseCode;
    }

    public String getResponseResult() {
        return responseResult;
    }

    public void setResponseResult(String responseResult) {
        this.responseResult = responseResult;
    }

    public String getContentType() {
        return contentType;
    }

    public void setContentType(String contentType) {
        this.contentType = contentType;
    }

    public String getContenLen() {
        return contenLen;
    }

    public void setContenLen(String contenLen) {
        this.contenLen = contenLen;
    }

    public String getLocation() {
        return location;
    }

    public void setLocation(String location) {
        this.location = location;
    }

    public String getRefresh() {
        return refresh;
    }

    public void setRefresh(String refresh) {
        this.refresh = refresh;
    }

    public String getContentEncode() {
        return contentEncode;
    }

    public void setContentEncode(String contentEncode) {
        this.contentEncode = contentEncode;
    }

    public String getContentDisposition() {
        return contentDisposition;
    }

    public void setContentDisposition(String contentDisposition) {
        this.contentDisposition = contentDisposition;
    }

    public String getTransferEncoding() {
        return transferEncoding;
    }

    public void setTransferEncoding(String transferEncoding) {
        this.transferEncoding = transferEncoding;
    }

}

7.自定义工具类

package com.core.util;

import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;

public class DealStringOrByteBuffer {

    public static String getString(ByteBuffer buffer) {

        String requestStr = "";
        try {
            byte[] content = new byte[buffer.limit()];
            buffer.get(content);
            requestStr = new String(content, "GBK");
        } catch (UnsupportedEncodingException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

        return requestStr;
    }
}

package com.core.util;

import java.nio.channels.SelectionKey;

public class DebugMethod {

    public void debug(String s) {
        System.out.println(s);
    }

    public void printKeyInfo(SelectionKey sk) {
        String s = new String();

        s = "Att: " + (sk.attachment() == null ? "no" : "yes");
        s += ", Read: " + sk.isReadable();
        s += ", Acpt: " + sk.isAcceptable();
        s += ", Cnct: " + sk.isConnectable();
        s += ", Wrt: " + sk.isWritable();
        s += ", Valid: " + sk.isValid();
        s += ", Ops: " + sk.interestOps();
        debug(s);
    }
}

资源下载地址
http://download.youkuaiyun.com/download/qq_23489303/9803918

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值