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

news/2025/3/15 15:35:04/

在前面我们尝试解释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;}// 获取待处理的SelectionKeyIterator<SelectionKey> keyIter=selector.selectedKeys().iterator();while(keyIter.hasNext()){SelectionKey key=keyIter.next();// 启动新线程处理SelectionKeynew Thread(new HttpHandler(key)).run();// 处理完后,从待处理的SelectionKey迭代器中移除当前所使用的keykeyIter.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;}@Overridepublic 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 {// 获取channelSocketChannel 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;}// 获取待处理的SelectionKeyIterator<SelectionKey> keyIter=selector.selectedKeys().iterator();while(keyIter.hasNext()){SelectionKey key=keyIter.next();// 启动新线程处理SelectionKeynew Thread(new HttpHandler(key)).run();// 处理完后,从待处理的SelectionKey迭代器中移除当前所使用的keykeyIter.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 {// 获取channelSocketChannel 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();}}@Overridepublic void run() {try{// 接收到连接请求时if(key.isAcceptable()){handleAccept();}// 读数据if(key.isReadable()){handleRead();}} catch(IOException ex) {ex.printStackTrace();}}}
}


http://www.ppmy.cn/news/1340742.html

相关文章

没有外网Nginx如何配置如何开启https

判断是否支持open-ssl 在服务器执行如下命令 openssl version没有则安装open-ssl&#xff0c;由于服务器没有外网&#xff0c;可以离线安装openssl-3.0.1.tar.gz&#xff0c;我是在有网的服务器直接下载的&#xff0c;然后再上传到这台无网的服务器上 wget https://www.open…

Mac brew教程

一、安装brew /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"二、查看brew版本 brew -vbrew -v 三、搜索软件 命令格式&#xff1a;brew search 软件名 eg&#xff1a; brew search nginx四、安装软件 命令格…

【文件上传WAF绕过】<?绕过、.htaccess木马、.php绕过

&#x1f36c; 博主介绍&#x1f468;‍&#x1f393; 博主介绍&#xff1a;大家好&#xff0c;我是 hacker-routing &#xff0c;很高兴认识大家~ ✨主攻领域&#xff1a;【渗透领域】【应急响应】 【python】 【VulnHub靶场复现】【面试分析】 &#x1f389;点赞➕评论➕收藏…

IntelliJ IDEA的常用插件收集

Alibaba Java Coding Guidelines : (代码质量检查)ChatGPT GPT-4 - Bito AI (使用GPT4.0的AI工具)Tabnine: AI Code Completion (使用AI自动完成代码编写)Translation (中英文翻译)jclasslib Bytecode viewer (字节码源文件查看&#xff0c;主要用来分析底层JVM的调用流程)Free…

小程序的应用、页面、组件生命周期(超全版)

小程序生命周期 应用的生命周期 onLaunch: 初始化小程序完成时触发&#xff0c;且全局只触发一次&#xff1b; onShow: 小程序初始化完成&#xff08;启动&#xff09;或从后台切换到前台显示时触发&#xff1b; onHide: 小程序从前台切换到后台隐藏时触发&#xff08;如切换…

ios app与H5页面交互踩坑

ios 与 H5 页面交互是异步的&#xff0c;有坑 这两端的交互我这边写的如下&#xff1a; const platform 判断的平台 export const getIosData () > {let returnPromise;try {if (platform "android" ) {returnPromise Promise.resolve((window as any).androi…

oracle 19c上安装样例数据库

样例schema的分类 HR: Human Resources OE: Order Entry PM: Product Media IX: Information Exchange SH: Sales History BI: Business Intelligence 安装样例数据库 1&#xff1a;HR的安装&#xff0c;通过dbca时候 2&#xff1a;HR的安装&#xff0c;安装完数据库后&#…

通俗易懂理解通道注意力机制(CAM)与空间注意力机制(SAM)

重要说明&#xff1a;本文从网上资料整理而来&#xff0c;仅记录博主学习相关知识点的过程&#xff0c;侵删。 一、参考资料 通道注意力&#xff0c;空间注意力&#xff0c;像素注意力 通道注意力机制和空间注意力机制 视觉 注意力机制——通道注意力、空间注意力、自注意力…