基于 Java NIO 实现简单的 HTTP 服务器

小编 2026-06-16 阅读:332 评论:0
1.简介 本文是上一篇文章实践篇,在上一篇文章中,我分析了选择器 Selector 的原理。本篇文章,我们来说说 Selector 的应用,如标题所示,这里我基于 Java NIO 实现了一个简单的 HT...

1.简介

本文是上一篇文章实践篇,在上一篇文章中,我分析了选择器 Selector 的原理。本篇文章,我们来说说 Selector 的应用,如标题所示,这里我基于 Java NIO 实现了一个简单的 HTTP 服务器。在接下来的章节中,我会详细讲解 HTTP 服务器实现的过程。另外,本文所对应的代码已经上传到 GitHub 上了,需要的自取,仓库地址为 toyhttpd。好了,废话不多说,进入正题吧。

 2. 实现

本节所介绍的 HTTP 服务器是一个很简单的实现,仅支持 HTTP 协议极少的特性。包括识别文件后缀,并返回相应的 Content-Type。支持200、400、403、404、500等错误码等。由于支持的特性比较少,所以代码逻辑也比较简单,这里罗列一下:

  1. 处理请求,解析请求头
  2. 响应请求,从请求头中获取资源路径, 检测请求的资源路径是否合法
  3. 根据文件后缀匹配 Content-Type
  4. 读取文件数据,并设置 Content-Length,如果文件不存在则返回404
  5. 设置响应头,并将响应头和数据返回给浏览器。

接下来我们按照处理请求和响应请求两步操作,来说说代码实现。先来看看核心的代码结构,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
/**
 * TinyHttpd
 *
 * @author code4wt
 * @date 2018-03-26 22:28:44
 */
public class TinyHttpd {

    private static final int DEFAULT_PORT = 8080;
    private static final int DEFAULT_BUFFER_SIZE = 4096;
    private static final String INDEX_PAGE = \"index.html\";
    private static final String STATIC_RESOURCE_DIR = \"static\";
    private static final String META_RESOURCE_DIR_PREFIX = \"/meta/\";
    private static final String KEY_VALUE_SEPARATOR = \":\";
    private static final String CRLF = \"\\r\\n\";

    private int port;

    public TinyHttpd() {
        this(DEFAULT_PORT);
    }

    public TinyHttpd(int port) {
        this.port = port;
    }

    public void start() throws IOException {
        // 初始化 ServerSocketChannel
        ServerSocketChannel ssc = ServerSocketChannel.open();
        ssc.socket().bind(new InetSocketAddress(\"localhost\", port));
        ssc.configureBlocking(false);

        // 创建 Selector
        Selector selector = Selector.open();
        
        // 注册事件
        ssc.register(selector, SelectionKey.OP_ACCEPT);

        while(true) {
            int readyNum = selector.select();
            if (readyNum == 0) {
                continue;
            }

            Set<SelectionKey> selectedKeys = selector.selectedKeys();
            Iterator<SelectionKey> it = selectedKeys.iterator();
            while (it.hasNext()) {
                SelectionKey selectionKey = it.next();
                it.remove();

                if (selectionKey.isAcceptable()) {
                    SocketChannel socketChannel = ssc.accept();
                    socketChannel.configureBlocking(false);
                    socketChannel.register(selector, SelectionKey.OP_READ);
                } else if (selectionKey.isReadable()) {
                    // 处理请求
                    request(selectionKey);
                    selectionKey.interestOps(SelectionKey.OP_WRITE);
                } else if (selectionKey.isWritable()) {
                    // 响应请求
                    response(selectionKey);
                }
            }
        }
    }
    
    private void request(SelectionKey selectionKey) throws IOException {...}
    private Headers parseHeader(String headerStr) {...}
    private void response(SelectionKey selectionKey) throws IOException {...}
    
    private void handleOK(SocketChannel channel, String path) throws IOException {...}
    private void handleNotFound(SocketChannel channel)  {...}
    private void handleBadRequest(SocketChannel channel) {...}
    private void handleForbidden(SocketChannel channel) {...}
    private void handleInternalServerError(SocketChannel channel) {...}
    private void handleError(SocketChannel channel, int statusCode) throws IOException {...}
    
    private ByteBuffer readFile(String path) throws IOException {...}
    private String getExtension(String path) {...}
    private void log(String ip, Headers headers, int code) {}
}

上面的代码是 HTTP 服务器的核心类的代码结构。其中 request 负责处理请求,response 负责响应请求。handleOK 方法用于响应正常的请求,handleNotFound 等方法用于响应出错的请求。readFile 方法用于读取资源文件,getExtension 则是获取文件后缀。

 2.1 处理请求

处理请求的逻辑比较简单,主要的工作是解析消息头。相关代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
private void request(SelectionKey selectionKey) throws IOException {
    // 从通道中读取请求头数据
    SocketChannel channel = (SocketChannel) selectionKey.channel();
    ByteBuffer buffer = ByteBuffer.allocate(DEFAULT_BUFFER_SIZE);
    channel.read(buffer);

    buffer.flip();
    byte[] bytes = new byte[buffer.limit()];
    buffer.get(bytes);
    String headerStr = new String(bytes);
    try {
        // 解析请求头
        Headers headers = parseHeader(headerStr);
        // 将请求头对象放入 selectionKey 中
        selectionKey.attach(Optional.of(headers));
    } catch (InvalidHeaderException e) {
        selectionKey.attach(Optional.empty());
    }
}

private Headers parseHeader(String headerStr) {
    if (Objects.isNull(headerStr) || headerStr.isEmpty()) {
        throw new InvalidHeaderException();
    }

    // 解析请求头第一行
    int index = headerStr.indexOf(CRLF);
    if (index == -1) {
        throw new InvalidHeaderException();
    }

    Headers headers = new Headers();
    String firstLine = headerStr.substring(0, index);
    String[] parts = firstLine.split(\" \");

    /*
     * 请求头的第一行必须由三部分构成,分别为 METHOD PATH VERSION
     * 比如:
     *     GET /index.html HTTP/1.1
     */
    if (parts.length < 3) {
        throw new InvalidHeaderException();
    }

    headers.setMethod(parts[0]);
    headers.setPath(parts[1]);
    headers.setVersion(parts[2]);

    // 解析请求头属于部分
    parts = headerStr.split(CRLF);
    for (String part : parts) {
        index = part.indexOf(KEY_VALUE_SEPARATOR);
        if (index == -1) {
            continue;
        }
        String key = part.substring(0, index);
        if (index == -1 || index + 1 >= part.length()) {
            headers.set(key, \"\");
            continue;
        }
        String value = part.substring(index + 1);
        headers.set(key, value);
    }

    return headers;
}

简单总结一下上面的代码逻辑,首先是从通道中读取请求头,然后解析读取到的请求头,最后将解析出的 Header 对象放入 selectionKey 中。处理请求的逻辑很简单,不多说了。

 2.2 响应请求

看完处理请求的逻辑,接下来再来看看响应请求的逻辑。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
private void response(SelectionKey selectionKey) throws IOException {
    SocketChannel channel = (SocketChannel) selectionKey.channel();
    // 从 selectionKey 中取出请求头对象
    Optional<Headers> op = (Optional<Headers>) selectionKey.attachment();

    // 处理无效请求,返回 400 错误
    if (!op.isPresent()) {
        handleBadRequest(channel);
        channel.close();
        return;
    }

    String ip = channel.getRemoteAddress().toString().replace(\"/\", \"\");
    Headers headers = op.get();
    // 如果请求 /meta/ 路径下的资源,则认为是非法请求,返回 403 错误
    if (headers.getPath().startsWith(META_RESOURCE_DIR_PREFIX)) {
        handleForbidden(channel);
        channel.close();
        log(ip, headers, FORBIDDEN.getCode());
        return;
    }

    try {
        handleOK(channel, headers.getPath());
        log(ip, headers, OK.getCode());
    } catch (FileNotFoundException e) {
        // 文件未发现,返回 404 错误
        handleNotFound(channel);
        log(ip, headers, NOT_FOUND.getCode());
    } catch (Exception e) {
        // 其他异常,返回 500 错误
        handleInternalServerError(channel);
        log(ip, headers, INTERNAL_SERVER_ERROR.getCode());
    } finally {
        channel.close();
    }
}

// 处理正常的请求
private void handleOK(SocketChannel channel, String path) throws IOException {
    ResponseHeaders headers = new ResponseHeaders(OK.getCode());

    // 读取文件
    ByteBuffer bodyBuffer = readFile(path);
    // 设置响应头
    headers.setContentLength(bodyBuffer.capacity());
    headers.setContentType(ContentTypeUtils.getContentType(getExtension(path)));
    ByteBuffer headerBuffer = ByteBuffer.wrap(headers.toString().getBytes());

    // 将响应头和资源数据一同返回
    channel.write(new ByteBuffer[]{headerBuffer, bodyBuffer});
}

// 处理请求资源未发现的错误
private void handleNotFound(SocketChannel channel)  {
    try {
        handleError(channel, NOT_FOUND.getCode());
    } catch (Exception e) {
        handleInternalServerError(channel);
    }
}

private void handleError(SocketChannel channel, int statusCode) throws IOException {
    ResponseHeaders headers = new ResponseHeaders(statusCode);
    // 读取文件
    ByteBuffer bodyBuffer = readFile(String.format(\"/%d.html\", statusCode));
    // 设置响应头
    headers.setContentLength(bodyBuffer.capacity());
    headers.setContentType(ContentTypeUtils.getContentType(\"html\"));
    ByteBuffer headerBuffer = ByteBuffer.wrap(headers.toString().getBytes());

    // 将响应头和资源数据一同返回
    channel.write(new ByteBuffer[]{headerBuffer, bodyBuffer});
}

上面的代码略长,不过逻辑仍然比较简单。首先,要判断请求头存在,以及资源路径是否合法。如果都合法,再去读取资源文件,如果文件不存在,则返回 404 错误码。如果发生其他异常,则返回 500 错误。如果没有错误发生,则正常返回响应头和资源数据。这里只贴了核心代码,其他代码就不贴了,大家自己去看吧。

 2.3 效果演示

分析完代码,接下来看点轻松的吧。下面贴一张代码的运行效果图,如下:

\"tinyhttpd1_w\"

 3.总结

本文所贴的代码是我在学习 Selector 过程中写的,核心代码不到 300 行。通过动手写代码,也使得我加深了对 Selector 的了解。在学习 JDK 的过程中,强烈建议大家多动手写代码。通过写代码,并踩一些坑,才能更加熟练运用相关技术。这个是我写 NIO 系列文章的一个感触。

好了,本文到这里结束。谢谢阅读!

from: http://www.tianxiaobo.com/2018/04/04/%E5%9F%BA%E4%BA%8E-Java-NIO-%E5%AE%9E%E7%8E%B0%E7%AE%80%E5%8D%95%E7%9A%84-HTTP-%E6%9C%8D%E5%8A%A1%E5%99%A8/ 

版权声明

本文仅代表作者观点,不代表百度立场。
本文系作者授权百度百家发表,未经许可,不得转载。

上一篇:cookie标准话 下一篇:正则大全
热门文章
  • 机房智能化温湿度解决方式之POE供电以太网温湿度传感器

    机房智能化温湿度解决方式之POE供电以太网温湿度传感器
    机房智能化温湿度解决方式之POE供电以太网温湿度传感器 北京盈创力和电子科技有限公司 智能型TCP网口温湿度记录仪 北京IP网络温湿度记录仪厂家,北京盈创力和 北京智能型TCP网口温湿度记录仪IP网络温湿度记录仪是一种新型的基于TCP/IP协议双绞线以太网标准温湿度采集模块,利用它可以实现现场温度值、相对湿度值的采集,同时利用其自身的RJ45通信接口可以方便地和机房监控主机或交换机集线器进行联网。 工作于-40℃~85℃工业级带...
  • Sequential Monte Carlo Methods (SMC) 序列蒙特卡洛/粒子滤波/Bootstrap Filtering

    Sequential Monte Carlo Methods (SMC) 序列蒙特卡洛/粒子滤波/Bootstrap Filtering
    Problem Statement 我们考虑一个具有马尔可夫性质、非线性、非高斯的状态空间模型(State Space Model):对于一个时间序列上的观测结果{yt,t∈N}\\{ y_t , t \\in N \\}{yt​,t∈N},我们认为每个观测结果yty_tyt​的生成依赖于一个无法直接观察的隐变量xt∈{xt,t∈N}x_t \\in \\{x_t , t \\in N \\}xt​∈{xt​,t∈N},即:p(...
  • HTTP状态保持的原理

    HTTP状态保持的原理
    a)在用户登录之后,浏览器返回响应的时候会在响应中添加上cookieb)浏览器接收到cookie之后会自动保存c)当用户再次请求同一服务器中的其他网页的时候,浏览器会自动带上之前保存的cookied)服务接收到请求之后可以请 request 对象中取到cookie 判断当前用户是否登录  Http是无状态的,就是连接时数据互通,关闭后...
  • Hive 系统函数及示例

    Hive 系统函数及示例
    查看所有系统函数 show functions; 函数分类 内置函数【系统函数】 数学函数: floor、round、ceil、cos、log2等 字符串函数: length、reverse、trim、lower、get_json_object、repeat等 收集函数: size 转换函数: cast 日期函数: year、month、datediff、date、date_add等 条件函数: coalesce、case…w...
  • CSRF的原理和防范措施

    CSRF的原理和防范措施
    a)攻击原理:i.用户C访问正常网站A时进行登录,浏览器保存A的cookieii.用户C再访问攻击网站B,网站B上有某个隐藏的链接或者图片标签会自动请求网站A的URL地址,例如表单提交,传指定的参数iii.而攻击网站B在访问网站A的时候,浏览器会自动带上网站A的cookieiv.所以网站A在接收到请求之后可判断当前用户是登录状态,所以...
标签列表