【Tomcat与网络11】如何自己实现一个简单的HTTP服务器

在前面我们尝试解释Tomcat的理论,但是呢,很多时候那些复杂的架构和设计会让我们眼花缭乱,以至于忽略了最进本的问题——服务器到底是什么?今天我们就用尽量简单的代码实现一个简易的HTTP服务器。

HTTP启动之后要持续监听,所以我们可以使用NioServer中的Handler就可以了,在修改后的HttpHandler中首先获取到请求报文并打印出报文的头部,包括协议的首行、请求方法的类型、Url和Http版本等,之后将接收到的请求消息(也就是报文信息)封装在一起,最后将这些信息打包成一个报文发送给客户端。

我们这里为了简单,将HttpHandler使用单线程来处理,并且选择SelectionKey的操作类型等都放在Handler中了。

主体代码:

    public static void main(String[] args) throws Exception{
        //创建ServerSocketChannel,监听8040端口
        ServerSocketChannel ssc=ServerSocketChannel.open();
        ssc.socket().bind(new InetSocketAddress(8040));
        //设置为非阻塞模式
        ssc.configureBlocking(false);
        //为ssc注册选择器
        Selector selector=Selector.open();
        ssc.register(selector, SelectionKey.OP_ACCEPT);
        //创建处理器
        while(true){
            // 等待请求,每次等待阻塞5s,超过5s后线程继续向下运行
            // 这里如果传入0或者不传参数将一直阻塞
            if(selector.select(5000)==0){
                continue;
            }
            // 获取待处理的SelectionKey
            Iterator<SelectionKey> keyIter=selector.selectedKeys().iterator();

            while(keyIter.hasNext()){
                SelectionKey key=keyIter.next();
                // 启动新线程处理SelectionKey
                new Thread(new HttpHandler(key)).run();
                // 处理完后,从待处理的SelectionKey迭代器中移除当前所使用的key
                keyIter.remove();
            }
        }
    }

我们在上面将端口设置为8040了,因为8080有时候会和其他软件冲突,有时候会被浏览器隐藏,所以我们使用一个更可控的。

之后,我们就来写真正需要干活的Hander:

  private static class HttpHandler implements Runnable{

        private int bufferSize = 2048;
        private String  localCharset = "UTF-8";
        private SelectionKey key;

        public HttpHandler(SelectionKey key){
            this.key = key;
        }

        @Override
        public void run() {
            try{
                // 接收到连接请求时
                if(key.isAcceptable()){
                    handleAccept();
                }
                // 读数据
                if(key.isReadable()){
                    handleRead();
                }
            } catch(IOException ex) {
                ex.printStackTrace();
            }
        }
}

我们使用一个线程来处理清楚,所以我们需要继续实现run()方法,根据选择器的状态来完成接收数据还是读取数据。

先看接收数据:

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

这段代码看似简单,其实包含的逻辑并不少,我们可以看到这里是从key获得Socket通信使用了哪个通道(对于HTTP的就是不同端口号),这个key是哪里的呢?是我们再创建HttpHandler的时候传过来的,也就是这一行:

  new Thread(new HttpHandler(key)).run();

 这相当于老板让你干活的时候,给你的锤子。

之后的这一行,就是自己创建了一个channel注册给key。

clientChannel.register(key.selector()...)

这里相当于打仗之前,你去军长那里报道,说你能打,然后军长Key就记住你了。

之后我们看读取数据的处理逻辑:

 public void handleRead() throws IOException {
            // 获取channel
            SocketChannel sc=(SocketChannel)key.channel();
            // 获取buffer并重置
            ByteBuffer buffer=(ByteBuffer)key.attachment();
            buffer.clear();
            // 没有读到内容则关闭
            if(sc.read(buffer)==-1){
                sc.close();
            } else {
                // 接收请求数据
                buffer.flip();
                String receivedString = Charset.forName(localCharset).newDecoder().decode(buffer).toString();

                // 控制台打印请求报文头
                String[] requestMessage = receivedString.split("\r\n");
                for(String s: requestMessage){
                    System.out.println(s);
                    // 遇到空行说明报文头已经打印完
                    if(s.isEmpty())
                        break;
                }

                // 控制台打印首行信息
                String[] firstLine = requestMessage[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 sendString = new StringBuilder();
                sendString.append("HTTP/1.1 200 OK\r\n");//响应报文首行,200表示处理成功
                sendString.append("Content-Type:text/html;charset=" + localCharset+"\r\n");
                sendString.append("\r\n");// 报文头结束后加一个空行

                sendString.append("<html><head><title>显示报文</title></head><body>");
                sendString.append("接收到请求报文是:<br/>");
                for(String s: requestMessage){
                    sendString.append(s + "<br/>");
                }
                sendString.append("</body></html>");
                buffer = ByteBuffer.wrap(sendString.toString().getBytes(localCharset));
                sc.write(buffer);
                sc.close();
            }
        }

这里,我们可以看到执行读的时候,我们使用key里获得channel的。这就相当于司令要求你所在的连队突袭,然后你的军长key就从自己的小本本上找到你,然后让你们行动。我们看一下执行效果:

在浏览器输入http://localhost:8040/

然后在控制台,我们看到http收到的相应如下:


最后附上完整代码:

public class HttpServer {
    public static void main(String[] args) throws Exception{
        //创建ServerSocketChannel,监听8040端口
        ServerSocketChannel ssc=ServerSocketChannel.open();
        ssc.socket().bind(new InetSocketAddress(8040));
        //设置为非阻塞模式
        ssc.configureBlocking(false);
        //为ssc注册选择器
        Selector selector=Selector.open();
        ssc.register(selector, SelectionKey.OP_ACCEPT);
        //创建处理器
        while(true){
            // 等待请求,每次等待阻塞5s,超过5s后线程继续向下运行
            // 这里如果传入0或者不传参数将一直阻塞
            if(selector.select(5000)==0){
                continue;
            }
            // 获取待处理的SelectionKey
            Iterator<SelectionKey> keyIter=selector.selectedKeys().iterator();

            while(keyIter.hasNext()){
                SelectionKey key=keyIter.next();
                // 启动新线程处理SelectionKey
                new Thread(new HttpHandler(key)).run();
                // 处理完后,从待处理的SelectionKey迭代器中移除当前所使用的key
                keyIter.remove();
            }
        }
    }

    private static class HttpHandler implements Runnable{
        private int bufferSize = 2048;
        private String  localCharset = "UTF-8";
        private SelectionKey key;

        public HttpHandler(SelectionKey key){
            this.key = key;
        }

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

        public void handleRead() throws IOException {
            // 获取channel
            SocketChannel sc=(SocketChannel)key.channel();
            // 获取buffer并重置
            ByteBuffer buffer=(ByteBuffer)key.attachment();
            buffer.clear();
            // 没有读到内容则关闭
            if(sc.read(buffer)==-1){
                sc.close();
            } else {
                // 接收请求数据
                buffer.flip();
                String receivedString = Charset.forName(localCharset).newDecoder().decode(buffer).toString();

                // 控制台打印请求报文头
                String[] requestMessage = receivedString.split("\r\n");
                for(String s: requestMessage){
                    System.out.println(s);
                    // 遇到空行说明报文头已经打印完
                    if(s.isEmpty())
                        break;
                }

                // 控制台打印首行信息
                String[] firstLine = requestMessage[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 sendString = new StringBuilder();
                sendString.append("HTTP/1.1 200 OK\r\n");//响应报文首行,200表示处理成功
                sendString.append("Content-Type:text/html;charset=" + localCharset+"\r\n");
                sendString.append("\r\n");// 报文头结束后加一个空行

                sendString.append("<html><head><title>显示报文</title></head><body>");
                sendString.append("接收到请求报文是:<br/>");
                for(String s: requestMessage){
                    sendString.append(s + "<br/>");
                }
                sendString.append("</body></html>");
                buffer = ByteBuffer.wrap(sendString.toString().getBytes(localCharset));
                sc.write(buffer);
                sc.close();
            }
        }

        @Override
        public void run() {
            try{
                // 接收到连接请求时
                if(key.isAcceptable()){
                    handleAccept();
                }
                // 读数据
                if(key.isReadable()){
                    handleRead();
                }
            } catch(IOException ex) {
                ex.printStackTrace();
            }
        }
    }
}

相关推荐

  1. Golang:使用net/http实现一个简易http服务器

    2024-02-02 23:08:01       35 阅读
  2. C++网络编程——实现一个简单echo服务器

    2024-02-02 23:08:01       35 阅读
  3. 如何使用Python实现一个简单Web服务器

    2024-02-02 23:08:01       21 阅读
  4. golang实现一个简单HTTP server

    2024-02-02 23:08:01       57 阅读

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-02-02 23:08:01       94 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-02-02 23:08:01       101 阅读
  3. 在Django里面运行非项目文件

    2024-02-02 23:08:01       82 阅读
  4. Python语言-面向对象

    2024-02-02 23:08:01       91 阅读

热门阅读

  1. 本题又主要考察了贪心

    2024-02-02 23:08:01       52 阅读
  2. 第十章 函数 (上)第一节-第九节

    2024-02-02 23:08:01       43 阅读
  3. uniapp uni.redirectTo() 跳转失效

    2024-02-02 23:08:01       57 阅读
  4. Centos7环境安装PHP8

    2024-02-02 23:08:01       57 阅读
  5. PHP面试题

    2024-02-02 23:08:01       55 阅读
  6. 2款网络监控系统软件,你更喜欢哪款?

    2024-02-02 23:08:01       51 阅读
  7. 速盾:服务器接入免备案CDN节点的好处有哪些

    2024-02-02 23:08:01       48 阅读
  8. 用于Web导出excel

    2024-02-02 23:08:01       51 阅读
  9. 关于后端异步+前端进度条的简单实现

    2024-02-02 23:08:01       45 阅读
  10. Three.js PBR 物理渲染

    2024-02-02 23:08:01       57 阅读
  11. this.$set()用法,强制刷新,新删改查

    2024-02-02 23:08:01       53 阅读
  12. JVM 内存配置参数积累

    2024-02-02 23:08:01       53 阅读
  13. vue + element 页面滚动计算百分比 + 节流函数

    2024-02-02 23:08:01       50 阅读