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

news2024/11/20 8:29:48

在前面我们尝试解释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();
            }
        }
    }
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1427985.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

脚踏实地 造福客户——立聪堂助听器2023-2024年度会议召开

1月30日&#xff0c;中国专业领先的助听器专业验配连锁机构立聪堂&#xff0c;在南京召开了2023-2024年度会议。 本次会议以“脚踏实地 造福客户”为主题&#xff0c;立聪堂总部员工与一百多家门店验配师共同参加&#xff0c;一起回顾2023&#xff0c;展望2024。 立聪堂存在的…

leetcode刷题(剑指offer) 103.二叉树的锯齿形层序遍历

103.二叉树的锯齿形层序遍历 给你二叉树的根节点 root &#xff0c;返回其节点值的 锯齿形层序遍历 。&#xff08;即先从左往右&#xff0c;再从右往左进行下一层遍历&#xff0c;以此类推&#xff0c;层与层之间交替进行&#xff09;。 示例 1&#xff1a; 输入&#xff1a…

鸿蒙实战开发-全局UI方法的功能

主要开发内容 时间调节 使用全局UI的方法定义日期滑动选择器弹窗并弹出。 操作说明&#xff1a;首先创建一个包含按钮的用户界面&#xff0c;当用户点击“时间设置”按钮时&#xff0c;会弹出调用TimePickerDialog组件的show方法&#xff0c;显示一个时间选择对话框&#xff…

2024美国大学生数学建模C题网球运动中的势头详解思路+具体代码

2024美国大学生数学建模C题网球运动中的势头详解思路具体代码 E题数据已更新&#xff0c;做E题的小伙伴推荐看看博主的E题解析文章。那么废话不多说我们继续来做C题。 赛题分析 我们先阅题&#xff1a; 在2023年温布尔登男单决赛中&#xff0c;20岁的西班牙新星卡洛斯阿尔卡…

2024最新版IntelliJ IDEA安装使用指南

2024最新版IntelliJ IDEA安装使用指南 Installation and Usage Guide to the Latest JetBrains IntelliJ IDEA Community Editionn in 2024 By JacksonML JetBrains公司开发的IntelliJ IDEA一经问世&#xff0c;就受到全球Java/Kotlin开发者的热捧。这款集成开发环境&#xf…

match-case与if/elif/else(python)

if/elif/else语句应对一般场景&#xff0c;match-case主打复杂条件分支语句。 (笔记模板由python脚本于2024年01月28日 18:27:37创建&#xff0c;本篇笔记适合有一定编程基础&#xff0c;对python基础已比较扎实的coder翻阅) 【学习的细节是欢悦的历程】 Python 官网&#xff1…

Java Swing实现思聪吃热狗游戏

引言 Java Swing&#xff0c;一种灵活的图形用户界面库&#xff0c;让我们可以以相对简便的方式创建图形化应用程序。在本文中&#xff0c;我们将讲述如何借助Swing构建一个简单的游戏&#xff1a;DogGame&#xff0c;它的规则是控制一只名为Wsc的狗来捕捉飞来的热狗。让我们浏…

Github 2F2【解决】经验帖-PPHub登入

最近在做项目时,Github总是出问题,这是一经验贴 Github 2F2登入问题【无法登入】PPhub 2F2是为了安全,更好的生态 启用 2FA 二十八 (28) 天后,要在使用 GitHub.com 时 2FA 检查 物理安全密钥、Windows Hello 或面容 ID/触控 ID、SMS、GitHub Mobile 都可以作为 2F2 的工…

架构学习(三):scrapy-redis源码分析并实现自定义初始请求

scrapy-redis源码分析并实现自定义初始请求 前言关卡&#xff1a;如何自定义初始请求背景思考简单又粗暴的方式源码分析 结束 前言 通过这篇文章架构学习(二)&#xff1a;原生scrapy如何接入scrapy-redis&#xff0c;初步入局分布式&#xff0c;我们正式开启scrapy-redis分布式…

MySQL EXPLAIN查询执行计划

EXPLAIN 可用来查看SQL执行计划&#xff0c;常用来分析调试SQL语句&#xff0c;来使SQL语句达到更好的性能。 1 前置知识 在学习EXPLAIN 之前&#xff0c;有些基础知识需要清楚。 1.1 JSON类型 MySQL 5.7及以上版本支持JSON数据类型。可以将数组存为JSON格式的字符串&#…

【CSS】css选择器和css获取第n个元素(:nth-of-type(n)、:nth-child(n)、first-child和last-child)

:nth-of-type、:nth-child的区别 一、css选择器二、:nth-of-type、:nth-child的区别:nth-of-type(n)&#xff1a;选择器匹配属于父元素的特定类型的第N个子元素:nth-child(n)&#xff1a;选择器匹配属于其父元素的第 N 个子元素&#xff0c;不论元素的类型:first-child&#xf…

2017 年全国职业院校技能大赛高职组“信息安全管理与评估”赛项任务书(笔记解析)

1. 网络拓扑图 2. IP 地址规划表 3. 设备初始化信息 阶段一 任务1:网络平台搭建 1 根据网络拓扑图所示,按照 IP 地址参数表,对 WAF 的名称、各接口 IP 地址进 行配置。 主机名称 模式选择:透明模式 IP 地址:匹配参数表 WAF IP 地址 子网掩码 网口列表: eth0 和 eth1 2…

【操作宝典】IntelliJ IDEA新建maven项目详细教程

目录 &#x1f33c;1. 配置maven环境 &#x1f33c;2. 创建maven项目 &#x1f33c;3. 创建maven项目完整示例 a. 导入spring boot环境 b. 修改maven配置 c. 下载jar包 d. 创建Java类 &#x1f33c;1. 配置maven环境 【安装指南】maven下载、安装与配置详细教程-CSDN博客…

Vue3+vite引入Tailwind CSS

Tailwind CSS 是一个为快速创建定制化 UI 组件而设计的实用型框架。与其他 CSS 框架或库不同&#xff0c;Tailwind CSS 组件没有预先设置好样式。可以使用 Tailwind 的低级实用类来为 CSS 元素设置样式&#xff0c;如 margin、flex、color 等。 自从 2017 年发布以来&#xff…

基于python flask茶叶网站数据大屏设计与实现,可以做期末课程设计或者毕业设计

基于Python的茶叶网站数据大屏设计与实现是一个适合期末课程设计或毕业设计的项目。该项目旨在利用Python技术和数据可视化方法&#xff0c;设计和开发一个针对茶叶行业的数据大屏&#xff0c;用于展示和分析茶叶网站的相关数据。 项目背景 随着互联网的快速发展&#xff0c;越…

【Java程序设计】【C00196】基于(JavaWeb+SSM)的旅游管理系统(论文+PPT)

基于&#xff08;JavaWebSSM&#xff09;的旅游管理系统&#xff08;论文PPT&#xff09; 项目简介项目获取开发环境项目技术运行截图 项目简介 这是一个基于ssm的旅游平台 本系统分为前台、管理员2个功能模块。 前台&#xff1a;当游客打开系统的网址后&#xff0c;首先看到的…

使用 Dockerfile 定制镜像详解

使用 Dockerfile 定制镜像详解 1.DockerfileFROM 指定基础镜像RUN 执行命令构建镜像 2.COPY 复制文件3.ADD 更高级的复制文件4.CMD 容器启动命令5.ENTRYPOINT 入口点6.ENV 设置环境变量7.ARG 构建参数8.VOLUME 定义匿名卷9.EXPOSE 暴露端口10.WORKDIR 指定工作目录11.USER 指定…

鸿道(Intewell)操作系统是什么?

科东软件自主研发的鸿道&#xff08;Intewell&#xff09;新型工业操作系统历经30多年研发积累&#xff0c;采用业界领先的微内核架构&#xff0c;具备高实时、高安全及强扩展的特性&#xff0c;与自主研发的Hypervisor虚拟化技术相结合&#xff0c;既能满足工业现场对设备控制…

Ray on ACK 实践探索之旅 - RayCluster 篇

作者&#xff1a;张杰、霍智鑫、行疾 什么是 Ray&#xff1f; Ray 是一个开源框架&#xff0c;专为构建可扩展的分布式应用程序而设计&#xff0c;旨在通过提供简单直观的 API&#xff0c;简化分布式计算的复杂性&#xff0c;让开发者能够便捷高效地编写并行和分布式 Python …