【Socket】基于 Java NIO 的 HTTP 请求过程

发布时间 2023-03-22 21:09:11作者: 酷酷-

1  前言

这节我们自己动手感受一下 HTTP的东西,我们知道 HTTP 协议是在应用层解析内容的,只需要按照它的报文的格式封装和解析数据就可以了,具体的传输还是使用的 Socket,我们基于上节的NIO Socket自己做一个简单的实现了HTTP协议的例子。

2  源码分析

因为HTTP 协议是在接收到数据之后才会用到的,所以我们只需要修改 NioServer 中的Handler 就可以了,在修改后的 HttpHandler 中首先获取到请求报文并打印出报文的头部(包含首行)、请求的方法类型、Url 和 Http 版本,最后将接收到的请求报文信息封装到响应报文的主体中返回给客户端。把 SelectionKey中操作类型的选择也放在了 HttpHandler 中,不过具体处理过程和前面的 NioServer 没有太大的区别,代码如下:

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
import java.util.Iterator;
import java.util.Objects;

/**
 * @author xjx
 * @description
 * @date 2023/3/21 7:48
 */
public class TestHttpServer {

    static class HttpHandler {

        private int bufferSize = 1024;
        private String localCharset = "UTF-8";
        private SelectionKey selectionKey;

        public HttpHandler(SelectionKey selectionKey) {
            this.selectionKey = selectionKey;
        }
        public void handleAccept() throws IOException {
            SocketChannel socketChannel = ((ServerSocketChannel) selectionKey.channel()).accept();
            socketChannel.configureBlocking(false);
            socketChannel.register(selectionKey.selector(), SelectionKey.OP_READ, ByteBuffer.allocate(bufferSize));
        }

        public void handleRead() throws IOException {
            // 获取 Channel
            SocketChannel socketChannel = ((SocketChannel) selectionKey.channel());
            ByteBuffer buffer = (ByteBuffer) selectionKey.attachment();
            buffer.clear();
            // 没有读取到内容就关闭
            if (socketChannel.read(buffer) == -1) {
                socketChannel.close();
            } else {
                // 将 buffer 转为读状态
                buffer.flip();
                // 将 buffer 中接收到的内容按编码格式保存
                String line = Charset.forName(localCharset).newDecoder().decode(buffer).toString();
                // 控制台打印请求头
                String[] reqMessage = line.split("\r\n");
                for (String mes : reqMessage) {
                    System.out.println(mes);
                    // 遇到空行说明报文头已经打印完
                    if (mes.isEmpty()) {
                        break;
                    }
                }
                // 控制台打印首行信息
                String[] firstLine = reqMessage[0].split(" ");
                System.out.println();
                System.out.println("Method:\t" + firstLine[0]);
                System.out.println("Url:\t" + firstLine[1]);
                System.out.println("HTTP Version:\t" + firstLine[2]);
                System.out.println();

                // 返回数据给客户端
                StringBuilder builder = new StringBuilder();
                // 响应报文首行,200 表示处理成功
                builder.append("HTTP/1.1 200 OK\r\n");
                builder.append("Content-Type:text/html;charset=" + localCharset + "\r\n");
                // 报文头结束后加一个空行
                builder.append("\r\n");
                builder.append("<html><head><title>显示报文</title></head><body>");
                builder.append("接收到请求报文是:<br/>");
                for(String s: reqMessage) {
                    builder.append(s + "<br/>");
                }
                builder.append("</body></html>");
                buffer = ByteBuffer.wrap(builder.toString().getBytes(localCharset));
                socketChannel.write(buffer);
                // 关闭 Socket
                socketChannel.close();
            }
        }

        public void handle() {
            try {
                // 接收到连接请求
                if (selectionKey.isAcceptable()) {
                    handleAccept();
                }
                // 读数据
                if (selectionKey.isReadable()) {
                    handleRead();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        try {
            // 创建 ServerSocketChannel 并监听 8080 端口
            ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
            serverSocketChannel.bind(new InetSocketAddress(8080));
            // 设置为非阻塞模式
            serverSocketChannel.configureBlocking(false);
            // 注册选择器
            Selector selector = Selector.open();
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
            // 创建处理器
            TestNioSocketServer.Handler handler = new TestNioSocketServer.Handler(1024);
            while (true) {
                // 等待请求,每次阻塞 3秒钟,超过3秒后线程继续向下运行,如果传入0或者不传参将一直阻塞
                if (selector.select(3000) == 0) {
                    System.out.println("等待请求超时...");
                    continue;
                }
                System.out.println("处理请求");
                // 获取待处理的 SelectionKey
                Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
                while (iterator.hasNext()) {
                    SelectionKey next = iterator.next();
                    iterator.remove();
                    // 处理
                    new HttpHandler(next).handle();
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

整个过程非常简单,按照报文的格式来读取和发送就可以了,接收到数据后按”irin”分割成每一行,在空行之前都是报文头(包含首行),空行下面如果有内容就是报文的主体,因为这里是 Get 请求所以就没有主体了,首行使用空格分制后可以得到请求的方法、Url 和 Http的版本,如果需要请求头的值只需要把头部的每一行用冒号分割开就行了。下面就来看一下运行效果,首先启动程序,然后在浏览器中输入 http://localhost:8080/发起请求,这时控制台就会打印出如下信息(不同的环境打印的结果会不同)。

这里也只是一个简单的示例,目的是让大家了解 HTTP 协议的实现方法,这里的功能还不够完善,它并不能真正处理请求,实际处理中应该根据不同的 Url和不同的请求方法进行不同的处理并返回不同的响应报文,另外这里的请求报文也必须在 buferSize(1024)范围内如果太长就会接收不全,而且也不能返回图片等流类型的数据(流类型只需要在响应报文中写清楚Content-Type 的类型,并将相应数据写人报文的主体就可以了),不过对于了解 HTTP协议实现的方法已经够用了。

我们看看效果:

3  小结

本节我们主要是感受下 HTTP 的大致请求过程,其实协议是什么就是一种规范或者标准,大家约定俗成的,都这么传输都这么规范执行,是不是,这个就好比 Tomcat 中 Connector 的责任,负责连接以及Request 和 Response的封装,然后交给 Container 进行处理,有理解不对的地方欢迎指正哈。