CXYVIP官网源码交易平台_网站源码_商城源码_小程序源码平台-丞旭猿论坛
CXYVIP官网源码交易平台_网站源码_商城源码_小程序源码平台-丞旭猿论坛
CXYVIP官网源码交易平台_网站源码_商城源码_小程序源码平台-丞旭猿论坛

简单实现一个JavaWeb服务器框架-永久免费的源码丞旭猿

Web 服务器原理

最近刚刚入门 Java Web,仅使用过 Node.js 进行后端开发,所以对 Java EE 的整体设计非常模糊,很可能会想当然地用错名词、或者张冠李戴,麻烦能在评论区指出,感谢。

最近非常苦恼于 tomcat 这个黑箱子,在这里记录一下我的认识。

Tomcat

我们经常将 tomcat 称为服务器,然而这让我迷惑了很久,这个说法挺通俗,就是有点难懂 (

我认为 tomcat 所作的工作比较接近Web 框架这个概念。只不过,在 Java EE 的生态中,Servlet 才应该被称为框架,而 tomcat,我们可以认为它是框架的实现

要理解这些东西的含义和 Java Web 服务器(如 tomcat)原理,我们应该首先来谈一下 Servlet。

Servlet

Servlet 本质是一个接口,Java 并没有对它做任何实现,仅提供了 Servlet 这么一个规范,它作为一个抽象层,用来连接所谓框架的实现和开发者的 Web 业务逻辑。

以 tomcat 为例,tomcat 被称为 Web 服务器,实际上他也是一个 Java 程序,本质上讲则是 Servlet 相关规范的实现。

当我们启动一个 Java Web 项目时,程序的主控制流是由 tomcat 实现和接管的。即我们启动的是 tomcat,而不是我们编写的业务代码,这些业务代码更像是一个 tomcat 的插件。

形象来看是下图这样的:

在细节中举例的话,例如我们使用的 HttpRequest 和 HttpResponse 对象,这些类型、接口都由 tomcat 实现。

我们现在知道了,Servlet 相关规范定义了一个没有任何实现的 Web 框架。

我们还应该知道,tomcat 也是一个 java 程序,启动 Java Web 项目就是启动 tomcat。

此外我们还要明白,tomcat 它并不特殊,他仅仅是实现了一个 Web 服务器(后面我会带着你手动实现一个最简单的 Web 服务器),然后遵循了 Servlet 规范,让我们的应用可以通过 Servlet 接口对接到 tomcat 的主控制流中。

它并不特殊,还体现在这一点,你有没有疑惑过,为什么我们在 IDEA 中要创建一个 Java Enterprise 项目?而不是一个普通的 Java Module?普通的 Java 项目就没有作为服务端的能力吗?

答案是有的,我们想要实现一个 Web 服务器,真的跟 Java EE 关系不是很大,我们甚至可以手调汇编写出一个服务端程序。

Java EE 只是定义了一套规范,我们不是必须要去遵循他,至少为了感知 Web 服务端,揭开这个黑盒子上的布,我们先不去考虑规范。

下面我将剔除 Servlet,为你展示如何实现一个最小功能的 Web 服务端开发框架。

Web 服务器的基本实现

如果你使用过 Node.js 开发 Web 后端应用,那么一定使用过或者听说过 express,下面我们就来开发一个类似 express 风格的 Web 服务器框架。

http 协议

首先是实现 http 协议。

先别着急害怕,我们只需要了解一丢丢,下面给出请求报文和响应报文的例子。

GET/index/testHTTP/1.1Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9Accept-Encoding: gzip, deflate, brAccept-Language: zh-CN,zh;q=0.9,sq;q=0.8Connection: keep-aliveHost: localhostUser-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.66 Safari/537.36

第一行是请求行,分别用空格分割了三个部分:method、url、protocol

后面是请求头,最后再空一行,写请求体,只不过这个实例没有请求体。

HTTP/1.1 200Content-Type: text/html

Hello, web Framework!

第一行是响应行,分别用空格分割了两个部分:protocol 和 status

接下来是响应头,这里只有一个 Content-Type,最后空一行,再写响应体,可以看到,这里的响应体是 html 代码。

立刻实现

有了这些,我们就可以花几行代码,立刻实现一个简单的 Web 服务器。

看好了。

要知道,所有网络通信均基于 TCP/IP 协议,而该协议的实现就是一个被称为 socket 的东西,几乎任何操作系统都对上层提供了 socket 的接口,很多编程语言的标准库也对这些系统调用做了封装。

在 Java 中,我们只需要这样做,就能创建一个服务端 Socket,并监听 80 端口:

ServerSocketserverSocket=newServerSocket(80);

然后呢,Web 服务器在入口处从不拒绝任何连接,我们要接收所有的 socket 请求:

accept方法将阻塞当前程序,直到下一个 socket 连接请求到来,然后建立一个长连接。

while(true){Socketclient=serverSocket.accept();}

当我们成功建立一个 socket 连接,此时我们先不关注请求报文,而是直接对它发送一个 http 响应报文:

我们先调用write向对方写一些数据,flush将强行写出所有数据,最后调用close关闭这个链接(http 是无状态的,如果不主动关闭该连接,客户端将认为此次请求并未结束)。

while(true){Socketclient=serverSocket.accept();OutputStreamclientOutStream=client.getOutputStream();clientOutStream.write(("HTTP/1.1 200\n"+"Content-Type: text/html\n"+"\n"+"

Hello, web Framework!

"
).getBytes());clientOutStream.flush();clientOutStream.close();}

这就结束了,所有代码:

importjava.io.IOException;importjava.io.OutputStream;importjava.net.ServerSocket;importjava.net.Socket;publicclassMain{publicstaticvoidmain(String[]args){try{ServerSocketserverSocket=newServerSocket(80);while(true){Socketclient=serverSocket.accept();OutputStreamclientOutStream=client.getOutputStream();clientOutStream.write(("HTTP/1.1 200\n"+"Content-Type: text/html\n"+"\n"+"

Hello, web Framework!

"
).getBytes());clientOutStream.flush();clientOutStream.close();}}catch(IOExceptione){e.printStackTrace();}}}

现在运行这个 Java 程序,然后用浏览器请求本地的 80 端口,你将看到:

Web 框架的基本实现

到这里,实际上我要表达的所有思想已经全部说清楚了,tomcat 底层就是做了这些事情,接收 socket 连接,然后发送报文,只不过在这中间他还调用了开发者的 Servlet 实现类 —— 因为它需要知道到底应该发送什么内容的报文。

下面的内容是这中间的部分的简单实现,如果你对如何简单地分析请求报文,如何设计一个简单的回调机制感兴趣,可以继续看下去。

如果你有 Java Web 开发基础,那一定知道 web.xml 这个东西,这也是 Servlet 相关规范的一环,他提示 Web 服务器应该如何理解我们的 web 应用配置。

例如路由(或者叫 url)对应的处理程序,需要配置 servlet-mapping,我认为这并不是一种好的设计。

好的设计应该让框架提供足够的自由度,尽量少用配置,把逻辑尽量体现在代码里。

但这也存在 Java Web 本身依赖关系的限制,如果启动入口是我们的程序,这种关系下设计的框架就很容易实现足够的自由度,但如果入口本身就在框架中,那么框架的设计将比较受限。

框架设计

下面要设计一个 express like 的框架,我们将不把框架作为程序的入口,而是在其他程序中引用该框架编写业务代码。

他使用起来就像这样:

Angieapp=newAngie();app.use("/test",(req,res)->{res.setStatus(200).setHeaders("Content-Type","text/html").send("

Hello, web framework!

"
);});app.listen(80);

仓库 –>https://github.com/Drincann/Angie-java

该框架将存在一个入口类Angie,该类实例化的对象将作为一个独立的 web 服务。

use方法将一个回调方法注册到一个路由上,listen方法用来开始监端口。

入口类

我们先来实现入口类的listen方法,listen用来不断建立 socket 连接,并向客户端(浏览器)发送数据:

publicclassAngie{publicvoidlisten(intport){try{// 监听 port 端口ServerSocketserverSocket=newServerSocket(port);while(true){// 接受连接Socketclient=serverSocket.accept();// 创建新线程,发送数据newThread(()->{try{OutputStreamclientOutStream=client.getOutputStream();clientOutStream.write(("HTTP/1.1 200\n"+"Content-Type: text/html\n"+"\n"+"

Hello, web Framework!

"
).getBytes());clientOutStream.flush();clientOutStream.close();}catch(IOExceptione){e.printStackTrace();}}).start();}}catch(IOExceptione){e.printStackTrace();}}}
注意这里对每个 socket 请求都创建了一个新线程处理,用来提高并发性能。

这时候我们就可以这样写:

Angieapp=newAngie();app.listen(80);

然后我们要实现一个另一个方法use,注册一个回调方法到某个路由:

为了记录路由到回调方法的映射,我们使用一个哈希表:

privatefinalHashMap<String,Processor>routeMap=newHashMap();

然后在开发者调用use时记录这个映射关系:

publicvoiduse(Stringroute,Processorprocessor){routeMap.put(route,processor);}

Processor是回调方法的函数式类型,正常来讲,应该接收一个 request 参数和一个 response 参数。

这时候可以回头改一下listen中的 while,不要再回复固定内容了,而是向不同路由的回调方法分发消息:

while(true){// 接受连接Socketclient=serverSocket.accept();// 创建新线程,发送数据newThread(()->{try{// 分发消息Requestrequest=newRequest(client.getInputStream());Responseresponse=newResponse(client.getOutputStream());if(routeMap.containsKey(request.getUrl())){routeMap.get(request.getUrl()).callback(request,response);}else{response.setStatus(404).send(request.getUrl()+" not found");}}catch(IOExceptione){e.printStackTrace();}}).start();}

到现在为止,入口类Angie的代码就已经全部完成了:

packagecool.gaolihai;importjava.io.IOException;importjava.net.ServerSocket;importjava.net.Socket;importjava.util.HashMap;publicclassAngie{privatefinalHashMap<String,Processor>routeMap=newHashMap();publicvoiduse(Stringroute,Processorprocessor){routeMap.put(route,processor);}publicvoidlisten(intport){try{ServerSocketserverSocket=newServerSocket(port);while(true){Socketclient=serverSocket.accept();newThread(()->{try{Requestrequest=newRequest(client.getInputStream());Responseresponse=newResponse(client.getOutputStream());if(routeMap.containsKey(request.getUrl())){routeMap.get(request.getUrl()).callback(request,response);}else{response.setStatus(404).send(request.getUrl()+" not found");}}catch(IOExceptione){e.printStackTrace();}}).start();}}catch(IOExceptione){e.printStackTrace();}}}

RequestResponse就是刚才Processor需要接收的参数,下面我们来实现这些类型。

回调方法

Processor非常简单,他就是一个函数式接口:

packagecool.gaolihai;@FunctionalInterfacepublicinterfaceProcessor{voidcallback(Requestrequest,Responseresponse);}

解析请求报文

Request负责解析请求报文,在这里,我们仅解析 url、GET 请求的 params 和 method,我们创建对应的属性和访问器:

privateStringurl;privateStringparams;privateStringmethod;publicStringgetUrl(){returnurl;}publicStringgetParams(){returnparams;}publicStringgetMethod(){returnmethod;}

可以看到,在入口类中,我们将 socket 的输入流作为构造参数进行实例化,所以构造函数如下:

我们把请求行 “GET /test HTTP/1.1” 通过空格分割,分别得到了 method、fullUrl,在 url 中又通过 “?” 得到了 url 和 params。

publicRequest(InputStreaminputStream){try{String[]requestLine=newBufferedReader(newInputStreamReader(inputStream)).readLine().split(" ");if(requestLine.length==3&&requestLine[2].equals("HTTP/1.1")){this.method=requestLine[0];StringfullUrl=requestLine[1];if(fullUrl.contains("?")){this.url=fullUrl.substring(0,fullUrl.indexOf("?"));this.params=fullUrl.substring(fullUrl.indexOf("?")+1);}else{this.url=fullUrl;}}}catch(IOExceptione){e.printStackTrace();}}

Request这就完成了,完整代码如下:

packagecool.gaolihai;importjava.io.BufferedReader;importjava.io.IOException;importjava.io.InputStream;importjava.io.InputStreamReader;publicclassRequest{privateStringurl;privateStringparams;privateStringmethod;publicRequest(InputStreaminputStream){try{String[]requestLine=newBufferedReader(newInputStreamReader(inputStream)).readLine().split(" ");if(requestLine.length==3&&requestLine[2].equals("HTTP/1.1")){this.method=requestLine[0];StringfullUrl=requestLine[1];if(fullUrl.contains("?")){this.url=fullUrl.substring(0,fullUrl.indexOf("?"));this.params=fullUrl.substring(fullUrl.indexOf("?")+1);}else{this.url=fullUrl;}}}catch(IOExceptione){e.printStackTrace();}}publicStringgetUrl(){returnurl;}publicStringgetParams(){returnparams;}publicStringgetMethod(){returnmethod;}}

响应客户端

Response类中,我们需要给开发者提供设置请求头、设置响应状态吗和响应数据的能力:

对于设置请求头,我们使用一个哈希表来存,响应数据时再拼接:

privateHashMap<String,String>headers=newHashMap<>();publicResponsesetHeaders(Stringkey,Stringvalue){this.headers.put(key,value);returnthis;}

对于响应状态码,我们这样实现:

privateintstatus;publicResponsesetStatus(intstatusCode){this.status=statusCode;returnthis;}

在这里,我们还要将输出流放在内部维护:

privateOutputStreamoutputStream;

构造函数则是直接初始化输出流:

publicResponse(OutputStreamoutputStream){this.outputStream=outputStream;}

最后一个功能,发送数据,实际上是向输出流中写字符串:

在这个方法中,我们让开发者提供请求体的字符串内容。

publicvoidsend(Stringdata){try{StringBuilderdataBuilder=newStringBuilder();// 拼接请求行dataBuilder.append("HTTP/1.1 ").append(this.status).append("\n");// 拼接请求头for(Stringkey:this.headers.keySet()){dataBuilder.append(key).append(": ").append(this.headers.get(key)).append("\n");}// 拼接请求体dataBuilder.append("\n").append(data);outputStream.write(dataBuilder.toString().getBytes());outputStream.flush();outputStream.close();}catch(IOExceptione){e.printStackTrace();}}

到这里Response也实现完毕了,代码如下:

packagecool.gaolihai;importjava.io.IOException;importjava.io.OutputStream;importjava.util.HashMap;publicclassResponse{privateOutputStreamoutputStream;privateHashMap<String,String>headers=newHashMap<>();privateintstatus;publicResponse(OutputStreamoutputStream){this.outputStream=outputStream;}publicResponsesetHeaders(Stringkey,Stringvalue){this.headers.put(key,value);returnthis;}publicResponsesetStatus(intstatusCode){this.status=statusCode;returnthis;}publicvoidsend(Stringdata){try{StringBuilderdataBuilder=newStringBuilder();dataBuilder.append("HTTP/1.1 ").append(this.status).append("\n");for(Stringkey:this.headers.keySet()){dataBuilder.append(key).append(": ").append(this.headers.get(key)).append("\n");}dataBuilder.append("\n").append(data);outputStream.write(dataBuilder.toString().getBytes());outputStream.flush();outputStream.close();}catch(IOExceptione){e.printStackTrace();}}}

测试

现在我们有一个入口类Angie,其每一个实例都是一个独立的 Web 服务端。

还有一个回调方法的函数式类型Processor,用来通过use方法接入框架的处理流程,还有与之相关的RequestResponse类型,用来解析请求报文和提供开发者响应客户端的能力。

现在你可以随意测试这个框架,就像这样:

importcool.gaolihai.Angie;publicclassapp{publicstaticvoidmain(String[]args){Angieapp=newAngie();app.use("/test",(req,res)->{res.setStatus(200).setHeaders("Content-Type","text/html").send("

Hello, web framework!

"
);});app.listen(80);}}

由于笔者没有基本的 Java 功底,对一些机制并不了解,目前这个框架偶然会玄学地在线程中抛出NullPointerException异常,在异常的栈轨迹中也没有触发任何断点。

如果读者发现了问题所在,麻烦请不吝赐教,非常感谢!

再放一遍仓库地址 –>https://github.com/Drincann/Angie-java

Java Web 服务器的基本实现

这里的 Java Web 服务器指符合 Servlet 规范的 Web 服务器。然而实际上其体系规模略庞大,我们不可能简单地实现它,但我们可以自己定义一个 Servlet 类似的简单规范,然后去尝试实现。这样可以搞明白 tomcat 这种服务器到底在实现什么,以及他可以如何实现这些东西。

如果想了解,可以看这篇博客https://blog.csdn.net/weixin_35586546/article/details/81226887,我下面的内容大概就是对这篇博客里代码的解释。

手指骨折,暂时鸽了。

声明:本文部分素材转载自互联网,如有侵权立即删除 。

© 版权声明
THE END
喜欢就支持一下吧
点赞0赞赏 分享
相关推荐
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片

    暂无评论内容