一、 web 概念
这是百度百科的解释:
web(World Wide Web)即全球广域网,也称为万维网,它是一种基于超文本和HTTP的、全球性的、动态交互的、跨平台的分布式图形信息系统。是建立在Internet上的一种网络服务,为浏览者在Internet上查找和浏览信息提供了图形化的、易于访问的直观界面,其中的文档及超级链接将Internet上的信息节点组织成一个互为关联的网状结构。
万维网不等于互联网,万维网是互联网的一个应用,简单的理解万维网就是由大量的服务器,比如百度、比如淘宝、比如我们的元动力网站组成,用户可以通过在浏览器中使用网址(资源定位符)来获取网络资源。如果我们想开发一个web应用,就需要开发应用,并部署在服务器上,当然这个服务器可以被其他人访问。
1、软件架构
C/S: 客户端/服务器端 ------------> QQ , 360 ... client server
B/S: 浏览器/服务器端 ------------> 京东, 网易 , 淘宝 brower/server
2、资源分类
- 静态资源: 所有用户访问后,得到的结果都是一样的,称为静态资源。静态资源可以直接被浏览器解析。如图片、视频等。
- 动态资源: 每个用户访问相同资源后,得到的结果可能不一样 , 称为动态资源。动态资源被访问后,需要先转换为静态资源,再返回给浏览器,通过浏览器进行解析。比如我们之前写的登录,不同的人登录后显示的用户名并不相同。
- 如:servlet,jsp,php,asp…
3、常见的web服务器
(1)概念
- 服务器:安装了服务器软件的计算机
- 服务器软件:接收用户的请求,处理请求,做出响应
- web服务器软件:接收用户的请求,处理请求,做出响应。
在web服务器软件中,可以部署web项目,让用户通过浏览器来访问这些项目
(2)常见服务器软件
动态服务器
- webLogic:oracle公司,大型的JavaEE服务器,支持JavaEE规范,收费的。
- webSphere:IBM公司,大型的JavaEE服务器,支持JavaEE规范,收费的。
- JBOSS:JBOSS公司的,大型的JavaEE服务器,支持JavaEE规范,收费的。
- Tomcat:Apache基金组织,中小型的JavaEE服务器,仅仅支持少量的JavaEE规范servlet/jsp。开源的,免费的。(300左右的并发)
静态的服务器
- Nginx:(代理,反向代理等)极高的并发 Nginx处理静态文件、索引文件,自动索引的效率非常高。当然除了当做高性能的静态服务器,它还有很多强大的功能,我们后边会有专项课程学习。
二、Hello World
我们已经学习了javase、mysql以及前端知识,jdbc负责java和持久层,那前端怎么和我们的java配合使用呢?
我们回想mysql的链接,不同的客户端通过url访问mysql,是通过套接字进行链接的:
与此同时,我们的浏览器也是使用url访问网站的啊,我们不妨尝试一下,建立一个服务器监听在8888端口,
很明显,我们启动的服务是基于TCP协议的套接字:
public static void main(String[] args) throws Exception {
// 启动一个服务器
ServerSocket serverSocket = new ServerSocket(8080);
Socket accept = serverSocket.accept();
// 获得输入流
InputStream inputStream = accept.getInputStream();
byte[] buf = new byte[1024];
int len;
while ((len = inputStream.read(buf)) != -1){
System.out.print(new String(buf,0,len));
}
inputStream.close();;
accept.close();
}
我们打开一个浏览器,在浏览器中输入:
我们观察一下后台的输出:
GET / HTTP/1.1
Host: 127.0.0.1:8888
Connection: keep-alive
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36
Sec-Fetch-User: ?1
Accept:text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
我们发现浏览器给我们发送了一系列的消息,当然我们现在可能看不懂。这其实就是一个报文,是浏览器传递给你的一些消息:比如这个User-Agent
就是告诉服务器我是从什么样的客户端来的,Host就是这个消息的目标主机。
很明显,浏览器貌似也是通过socket和我们的服务器建立了TCP连接。
我们不妨把浏览器给我们发送的信息称之为请求
,而这种请求的格式满足了http这样一个协议,在浏览器中我们打开百度后会显示对应的页面,是因为百度的服务器收到请求后会给浏览器响应,而我们刚才写的服务器,并没有对请求做出任何的响应。
这也就意味着,我们接收到http请求后,按照http协议的规范,书写http的响应,就能让浏览器做出相应的回应。
这里有一个最简单的响应报文:
HTTP/1.1 200 OK
Content-Length: 39
Content-Type: text/html;charset=UTF-8
<h1 style=\"color:red\">hello server!<h1>
这个报文的意图很明显:
- 第一行:服务器告诉浏览器,我们这个请求成功了。
- 第二行和第三行:是服务器告诉浏览器,我们这个响应的类型是个网页,内容长度是39字符。
- 最后一行是具体的网页数据。
所以,我们的Java代码可以这样去写:
public class Server {
public static void main(String[] args) throws IOException {
// 创建一个服务器监听在8888端口
ServerSocket serverSocket = new ServerSocket(8888);
Socket server = serverSocket.accept();
OutputStream outputStream = server.getOutputStream();
// 按照http协议的格式封装一个报文
String response = "HTTP/1.1 200 OK\r\n" +
"Content-Length: 39\r\n" +
"Content-Type: text/html;charset=UTF-8\r\n\r\n" +
"<h1 style=\"color:red\">hello server!<h1>";
// 将报文写出给浏览器
outputStream.write(response.getBytes());
outputStream.flush();
// 这个输出流不要着急关,因为突然的关闭会导致浏览器和服务器的连接断开
}
}
我们再来从浏览器访问我们的服务器看看:
发现红色的hello server已经在浏览器上了。
三、深入HTTP协议
1、HTTP协议简介
超文本传输协议(英文:HyperText Transfer Protocol,缩写:HTTP)是一种用于分布式、协作式和超媒体信息系统的应用层协议。HTTP是万维网的数据通信的基础,它和TCP/IP协议簇的其他协议一样,也是用于客户端和服务端的通信。
HTTP的发展是由蒂姆·伯纳斯-李于1989年在欧洲核子研究组织(CERN)所发起。HTTP的标准制定由万维网协会(World Wide Web Consortium,W3C)和互联网工程任务组(Internet Engineering Task Force,IETF)进行协调,最终发布了一系列的RFC,其中最著名的是1999年6月公布的 RFC 2616,定义了HTTP协议中现今广泛使用的一个版本——HTTP 1.1。
2014年12月,互联网工程任务组(IETF)的Hypertext Transfer Protocol Bis(httpbis)工作小组将HTTP/2标准提议递交至IESG进行讨论,于2015年2月17日被批准。 HTTP/2标准于2015年5月以RFC 7540正式发表,取代HTTP 1.1成为HTTP的实现标准。
注:什么是超文本
在互联网早期,我们输入的信息只能保存在本地,信息都是以文本的形式存在,但随着计算机的发展,人们不再满足与两台电脑之间的文字传输,还想要传输图片、音频、视频,甚至点击文字能实现超链接跳转,此时文本的语义就被扩大了,这种扩大后的文本就称之为超文本。
2、HTTP协议概述
HTTP是一个客户端终端(用户)和服务器端(网站)请求和应答的标准协议。我们通过使用网页浏览器或者其它的工具发起HTTP请求,这个客户端为我们称之为用户代理程序(user agent),服务器上存储着一些资源,比如HTML文件和图像。我们称这个应答服务器为源服务器(origin server)。
通常,由HTTP客户端发起一个请求,此时创建一个到服务器指定端口(默认是80端口)的tcp连接。HTTP服务器则在那个端口监听客户端的请求。一旦收到请求,服务器会向客户端返回一个状态,比如"HTTP/1.1 200 OK",以及返回的内容,如请求的文件、错误消息、或者其它信息。
3、HTTP工作原理
以下是 HTTP 请求/响应的步骤:
- 客户端连接到Web服务器。
浏览器向 DNS 服务器请求解析该 URL 中的域名所对应的 IP 地址,一个HTTP客户端,通常是浏览器,与 Web服务器的HTTP端口(默认为80)建立一个TCP套接字连接。
-
发送HTTP请求。
通过【TCP套接字】,客户端向Web服务器发送一个文本的请求报文,一个请求报文由【请求行、请求头部、空行和请求数据】4部分组成。
-
服务器接受请求并返回HTTP响应
Web服务器【解析请求,定位请求资源】,然后将资源的复本写到TCP套接字,由客户端读取。一个响应由【状态行、响应头部、空行和响应数据】4部分组成。
-
服务器释放连接TCP连接。
若connection 模式为close,则服务器主动关闭TCP连接,客户端被动关闭连接,释放TCP连接。
若connection 模式为keepalive,则该连接会保持一段时间,在该时间内可以继续接收请求。无论如何都会释放。
-
客户端浏览器解析HTML内容
客户端浏览器首先解析状态行,查看表明请求是否成功的状态代码。然后解析每一个响应头,响应头告知以下为若干字节的HTML文档和文档的字符集。客户端浏览器读取响应数据HTML,根据HTML的语法对其进行格式化,并在浏览器窗口中显示。
从以上的内容我们大致可以总结出以下几点:
1、Http是基于请求和响应的。
2、需要依托TCP协议进行三次握手连接、传输数据。
3、 TCP的连接会被主动断开,并不是一直保持连接。
4、HTTP报文格式
一个完整的HTTP协议的报文主要由以下三个部分组成:
- 起始行(请求行、响应行):起始行 start line : 描述请求或响应的基本信息。
- 首部字段(请求头、响应头):使用key-value的形式更加详细的说明报文。
- 消息正文(请求体、响应体):实际的传输数据,不一定是文本,也有可能是图片、音频、视频等二进制数据。
一个请求报文的格式如下:
一个响应的报文格式如下:
接下来我们一点一点拨开http的面纱。
(1)HTTP请求方法
HTTP/1.1协议中共定义了八种方法(也叫“动作”)来以不同方式操作指定的资源,我们目前最常见的有两种一种get,另外一种叫post。
请求的目的就是获取或操作资源,互联网的任何数据,我们都能称之为资源,数据库内的一条数据,一个网页,一个视频都是资源。请求的方法决定了我们怎么去操作这个资源。
GET
向指定的资源发出“显示”请求。使用GET方法应该只用在读取数据,而不应当被用于产生“副作用”的操作中,常用语查询数据的请求。
POST
向指定资源提交数据,请求服务器进行处理(例如提交表单或者上传文件)。数据被包含在请求本文中。这个请求可能会创建新的资源或修改现有资源,或二者皆有。常用于对数据的增删改操作。
请求方式: get与post请求(通过form表单我们自己写写看)
- GET提交的数据会放在URL之后,也就是请求行里面,以?分割URL和传输数据,参数之间以&相连,如EditBook?name=test1&id=123456.(请求头里面那个content-type做的这种参数形式,后面讲) POST方法是把提交的数据放在HTTP包的请求体中.
- GET提交的数据大小有限制(因为浏览器对URL的长度有限制),而POST方法提交的数据没有限制.
- GET与POST请求在服务端获取请求数据方式不同,就是我们自己在服务端取请求数据的时候的方式不同了,这句废话昂。
(2)URI
URI叫统一资源标识符 Uniform Resource Identifier,这是一个比较广的概念。
目前,我们有几种方式来表示本机或者网络的一个资源:
- 通过【定位】的方式来标识资源,这种方式叫【统一资源定位符】,也就是我们说的【URL】(Uniform Resource Locator)。这种方式下我们可以这样表示一个资源,【http://www.aaa.com/image/girl.png】。很明显URL和位置密切相关,一旦目标主机挂了,或者目标资源更换了位置,URL就失效了。
- 通过【命名】的方式来标识资源,这种方式叫【统一资源命名符】,也就是我们说的【URN】(Uniform Resource Name)。这种方式下每一个资源都有一个独立的资源名称,比如【DFAS12B12G3HJK1GHJ3G1HJG23G】,根据这个名字我们就能找到对应的资源,但是这种方式下,我们需要有一个解析器负责根据名字找到对应的资源位置,好处是不管资源怎么变动,我们都可以根据资源名字获取资源。
但是事实上,理论上URN对我们更友好,但是互联网的资源这么多,专门为这么多资源搭建一个资源解析服务器也不太靠谱,所以我们见到的URI主要是以URL为主,可以说URL 约等于 URI。
我们不妨再回顾一下之前学过的URL格式:
超文本传输协议(HTTP)的统一资源定位符将从因特网获取信息的五个基本元素包括在一个简单的地址中:
- 协议:一般为http或https。
- URI:直接定位到对应的资源。
- 主机:通常为域名,有时为IP地址。
- 端口号:以数字方式表示,若为HTTP的默认值“:80”可省略,数字为0~65536。
- uri:以“/”字符区别路径中的每一个目录名称,根路径为‘/’。
- 查询:GET模式的窗体参数,以“?”字符为起点,每个参数以“&”隔开,再以“=”分开参数名称与数据,通常以UTF8的URL编码,避开字符冲突的问题。
以http://www.ydlclass.com:80/news/index.html?id=250&age=1 为例, 其中:
【http】是协议;【www.xinzhi.com】是服务器; 【80】,是服务器上的默认网络端口号,默认不显示; 【/news/index.html】,是路径(URI:直接定位到对应的资源); 【?id=250&page=1】,是查询条件。 大多数网页浏览器不要求用户输入网页中“[http://”的部分,因为绝大多数网页内容是超文本传输协议文件。 “80”是超文本传输协议文件的常用默认端口号,因此一般也不必写明。一般来说用户只要键入统一资源定位符的一部分
(3)响应码
- 1xx消息——请求已被服务器接收,继续处理
- 2xx成功——请求已成功被服务器接收、理解、并接受
- 3xx重定向——需要后续操作才能完成这一请求
- 4xx请求错误——请求含有词法错误或者无法被执行,客户端
- 5xx服务器错误——服务器在处理某个正确请求时发生错误,500
一些常见的响应码
200 | OK | 从客户端发送的请求,服务端已经正常处理了。 |
204 | No Content | 服务端已经正常处理了,但是响应中没有实体,也不允许有实体。 |
301 | Moved Permanently | 永久性,重定向。表示请求的资源已经拥有了新的uri,需要重新访问。 |
302 | Moved Temporarily | 临时重定向。 |
400 | Bad Request | 请求报文中存在语法错去。 |
401 | Unauthorized | 请求需要有通过HTTP请求的认证信息。 |
403 | Forbidden | 请求被阻止,可能因为某些权限问题,比如访问的文件没有权限等。 |
404 | Not Found | 表示在服务器上没有你要找的资源 |
500 | Internal server Error | 服务器执行程序出现异常 |
我们用一个简单的例子感受一下重定向:
public class Server302 {
public static void main(String[] args) throws IOException {
// 创建一个服务器监听在8888端口
ServerSocket serverSocket = new ServerSocket(8888);
Socket server = serverSocket.accept();
OutputStream outputStream = server.getOutputStream();
// 按照http协议的格式封装一个可以重定向的报文
String response = "HTTP/1.1 302 Moved Temporarily\r\n" +
"Location: https://www.baidu.com\r\n\r\n";
// 将报文写出给浏览器
outputStream.write(response.getBytes());
outputStream.flush();
// 这个输出流不要着急关,因为突然的关闭会导致浏览器和服务器的连接断开
}
}
当我们访问 127.0.0.1:8888 时,发现网页居然打开了百度,就相当于自动给我们在浏览器输入http://www.baidu.com,并按下了回车。
(3)http首部字段
http首部字段是构成http报文的重要元素,它能起到传递额外重要信息的作用,首部信息一般会提供报文类型、编码和大小、认证信息,缓存策略等信息。
**不用记、不用记。**如果需要记忆和深入目前只有一个Content-Type
HTTP/1.1 规范定义了如下 47 种首部字段,分为四大类,我们大致预览一下,不能一一讲解,详情可以通过看书深入理解
1、通用首部字段 9个
首部字段名 | 说明 |
---|---|
Cache-Control | 控制缓存的行为 |
Connection | 连接的管理 |
Date | 创建报文的日期时间 |
Pragma | 报文指令 |
Trailer | 报文末端的首部一览 |
Transfer-Encoding | 指定报文主体的传输编码方式 |
Upgrade | 升级为其他协议 |
Via | 代理服务器的相关信息 |
Warning | 错误通知 |
2、请求首部字段 共18个
首部字段名 | 说明 |
---|---|
Accept | 用户代理可处理的媒体类型 |
Accept-Charset | 优先的字符集 |
Accept-Encoding | 优先的内容编码 |
Accept-Language | 优先的语言(自然语言) |
AuthorizationWeb | 认证信息 |
Expect | 期待服务器的特定行为 |
From | 用户的电子邮箱地址 |
Host | 请求资源所在服务器 |
If-Match | 比较实体标记(ETag) |
If-Modified-Since | 比较资源的更新时间 |
If-None-Match | 比较实体标记(与 If-Match 相反) |
If-Range | 资源未更新时发送实体 Byte 的范围请求 |
If-Unmodified-Since | 比较资源的更新时间(与If-Modified-Since相反) |
Max-Forwards | 最大传输逐跳数 |
Proxy-Authorization | 代理服务器要求客户端的认证信息 |
Range | 实体的字节范围请求 |
Referer | 对请求中 URI 的原始获取方 |
TE | 传输编码的优先级 |
User-Agent | 客户端程序的信息 |
3、响应首部字段 共9个
首部字段名 | 说明 |
---|---|
Accept-Ranges | 是否接受字节范围请求 |
Age | 推算资源创建经过时间 |
ETag | 资源的匹配信息 |
Location | 令客户端重定向至指定URI |
Proxy-Authenticate | 代理服务器对客户端的认证信息 |
Retry-After | 对再次发起请求的时机要求 |
Server | HTTP服务器的安装信息 |
Vary | 代理服务器缓存的管理信息 |
WWW-Authenticate | 服务器对客户端的认证信息 |
4、实体首部字段 共10个
首部字段名 | 说明 |
---|---|
Allow | 资源可支持的HTTP方法 |
Content-Encoding | 实体主体适用的编码方式 |
Content-Language | 实体主体的自然语言 |
Content-Length | 实体主体的大小(单位:字节) |
Content-Location | 替代对应资源的URI |
Content-MD5 | 实体主体的报文摘要 |
Content-Range | 实体主体的位置范围 |
Content-Type | 实体主体的媒体类型 |
Expires | 实体主体过期的日期时间 |
Last-Modified | 资源的最后修改日期时间 |
(4)http内容协商
同一个web网页可能存在多个相同内容的网页,比如英文版和中文版,它们内容相同,语言却不同。当浏览器默认的语言不同,访问相同uri会出现不同结果,这种机制就是内容协商。
内容协商机制是指客户端和服务器就响应的资源内容进行协商交涉,然后提供给客户端最合适的资源。内容协商会以响应资源的语言、字符集、编码等方式作为判断的标准。
共有3种不同的方法可以决定服务器上哪个页面最适合客户端:让客户端来选择、服务器自动判定、让中间代理来选。这3种技术分别称为客户端驱动的协商、服务器驱动的协商以及透明协商。
客户端驱动
客户端发起请求,服务器发送可选项列表,客户端作出选择后再发送第二次请求。
- 优点:比较容易实现。
- 缺点:增加了时延,至少要发送两次请求,第一次请求获取资源列表,第二次获取选择的副本。
服务器驱动
服务器检查客户端的请求首部集并决定提供哪个版本的页面。
- 优点:比客户端驱动的协商要快。
- 缺点:首部集不匹配,服务器要做猜测。
透明协商
某个中间设备(通常是缓存代理)代表客户端进行协商。
- 优点:免除了web服务器的协商开销,比客户端驱动的协商要快。
- 缺点:HTTP并没有提供相应的规范。
其中,服务器驱动的解决方案应用较为广泛。
通用的内容协商首部集
客户端可以用下面列出的HTTP首部集发送用户的偏好信息:
- Accept:告知服务器发送何种媒体类型;
- Accept-Language:告知服务器发送何种语言;
- Accept-Charset:告知服务器发送何种字符集;
- Accept-Encoding:告知服务器采用何种编码。
【媒体类型】
因特网上有数千种不同类型的数据,HTTP仔细地给每种要通过web传输的对象都打上了名为MIME类型(MIME type)的数据格式标签。最初设计MIME(Multipurpose Internet Mali Extension,多用途英特网邮件扩藏)是为了解决在不同的电子邮件系统之间搬移报文时存在的问题。MIME 在电子邮件系统中工作得非常好,因此 HTTP 也采纳了它,用它来描述并标记多媒体内容。
MIME 类型是一种文本标记,表示一种【主要的对象类型】和一个特定的【子类型】,中间由一条斜杠来分隔。
- HTML 格式的文本文档由【text/html】 类型来标记
- 普通的 ASCII 文本文档由 【text/plain】 类型来标
- JPEG 版本的图片为 【image/jpeg】 类型
- GIF 格式的图片为【image/gif】 类型
- Apple 的 QuickTime 电影为【video/quicktime 】类型
- 微软的 PowerPoint 演示文件为【application/vnd.ms-powerpoint】类型
当然还有很多很多…
而我们以后见的最多的要数以下两种,这两种类型都是用来传递数据:
- application/json,学习了前端知识后,想必大家对json已经不再陌生了。
- application/x-www-form-urlencoded,我们之前都学习过表单,urlencoded格式,又叫 form 格式,它是一种表单格式。它使用键值对的方式进行表示,键和值之间用=,多个键值对之间用&
比如我们想在客户端和服务之间传递信息:
可以是这样的
name=polo&age=35&smoke=false
也可以是
{
"name" :"polo",
"age":35,
"smoke":false
}
更多的mimeType可以查看:https://www.w3school.com.cn/media/media_mimeref.asp
【注意】这些首部与实体首部非常类似。不过,这两种首部的用途截然不同。
实体首部集像运输标签,它们描述了把报文从服务器传输给客户端的过程中必须的各种报文主体属性。
而内容协商首部集是由客户端发送给服务器用于交换偏好信息的,以便服务器可以从文档的不同版本中选择出最符合客户端偏好的那个来提供服务。
服务器用下面列出的实体首部集来匹配客户端的Accept首部集:
Accept首部 | 实体首部 |
---|---|
Accept | Content-Type |
Accept-Language | Content-Language |
Accept-Charset | Content-Type |
Accept-Encoding | Content-Encoding |
目前为止,关于http协议的基础知识我们讲的差不多了,更多的知识会在后期的学习中不断的深入,我们不妨先将我们的小项目完善一下吧。
四、项目完善
本次项目的目的是实现一个小程序,在浏览器中输入URL能够打开一个文件夹下的html页面。
我们不妨将请求和响应封装成两个对象,毕竟字符串的操作实在是痛苦:
/**
* 将接收的请求报文转化为请求对象
*/
public class Request {
private String protocol;
// 请求方式
private String type;
// uri
private String uri;
// 请求头
private Map<String,String> header = new HashMap<>();
// 请求体
private String body;
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getUri() {
return uri;
}
public void setUri(String uri) {
this.uri = uri;
}
public Map<String, String> getHeaders() {
return header;
}
public void setHeaders(Map<String, String> header) {
this.header = header;
}
public String getBody() {
return body;
}
public void setBody(String body) {
this.body = body;
}
public String getHeader(String key){
return header.get(key);
}
public void addHeader(String key,String value){
header.put(key,value);
}
public String getProtocol() {
return protocol;
}
public void setProtocol(String protocol) {
this.protocol = protocol;
}
}
/**
* 处理请求报文
*/
public class RequestHandler {
/**
* 将获取的请求报文封装成一个请求对象
* @param requestMessage
* @return
*/
public static Request hand(String requestMessage){
Request request = new Request();
// 通过大量的截串获取对应信息
String[] headerAndBody = requestMessage.split("\r\n\r\n");
// 判断有没有请求体
if(headerAndBody.length > 1){
request.setBody(headerAndBody[1]);
}
// 将请求行和首部信息截取
String[] lineAndHeader = headerAndBody[0].split("\r\n");
String line = lineAndHeader[0];
// 使用空格截取请求行信息
String[] lines = line.split(" ");
request.setType(lines[0]);
request.setUri(lines[1]);
request.setProtocol(lines[2]);
// 遍历请求头
for (int i = 1; i < lineAndHeader.length; i++) {
String[] split = lineAndHeader[i].split(": ");
request.addHeader(split[0],split[1]);
}
return request;
}
}
响应:
/**
* 响应
*/
public class Response {
// 协议
private String protocol = "Http/1.1";
// 响应码
private String code = "200";
// 信息
private String message = "ok";
// 响应头
private Map<String,String> header = new HashMap<>();
// 响应体
private String body;
public String getProtocol() {
return protocol;
}
public void setProtocol(String protocol) {
this.protocol = protocol;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public Map<String, String> getHeaders() {
return header;
}
public void setHeaders(Map<String, String> header) {
this.header = header;
}
public String getHeader(String key){
return header.get(key);
}
public void addHeader(String key,String value){
header.put(key,value);
}
public String getBody() {
return body;
}
public void setBody(String body) {
this.body = body;
}
/**
* @author itnanls(私信联系)
* 处理响应的工具类
*/
public class ResponseHandler {
// 定义我们网站的根目录
public static final String BASE_PATH = "D:/www/";
/**
* 此方法用来生成一个响应的字符串
* @param path
* @return
*/
public static String build(String path){
String htmlPath = BASE_PATH + path;
try ( FileInputStream fis = new FileInputStream(htmlPath)){
// 使用输入流读取文件的内容
String body = IOUtils.readString(fis);
Response response = new Response();
response.setBody(body);
response.addHeader("Content-Type","text/html;charset=UTF-8");
response.addHeader("Content-Length",Integer.toString(body.length()));
return build(response);
}catch (IOException e){
e.printStackTrace();
}
return null;
}
// 将响应对象序列化成字符串报文
public static String build(Response response){
StringBuilder sb = new StringBuilder();
sb.append(response.getProtocol()).append(" ")
.append(response.getCode()).append(" ")
.append(response.getMessage()).append("\r\n");
for(Map.Entry<String,String> entry : response.getHeaders().entrySet()){
sb.append(entry.getKey()).append(": ").append(entry.getValue()).append("\r\n");
}
if(response.getBody() != null){
sb.append("\r\n").append(response.getBody());
}
return sb.toString();
}
}
创建一个IO工具类负责从流中读取数据:
public class IOUtils {
// 读取流中的数据
public static String readString(InputStream inputStream){
try {
int len;
byte[] buffer = new byte[1024];
StringBuilder sb = new StringBuilder();
while (inputStream.available() > 0){
len = inputStream.read(buffer);
sb.append(new String(buffer,0,len));
}
return sb.toString();
}catch (IOException e){
e.printStackTrace();
}
return null;
}
}
/**
* 每个客户端的请求使用独立的线程处理
* 当然你可以写成NIO的模式,只是比较复杂而已
*/
public class UserThread implements Runnable {
private final Socket socket;
public UserThread(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try (InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream();
) {
String requestMessage = IOUtils.readString(inputStream);
Request request = RequestHandler.hand(requestMessage);
String uri = request.getUri();
// 按照http协议的响应格式封装响应报文
// 浏览器除了发送我们的请求,还会发送/favicon.ico请求,用来获取网站图标
// 我们把它排除掉,不处理
if (!"/favicon.ico".equals(uri)){
// 直接使用输出流输出到浏览器
String response = ResponseHandler.build(uri);
outputStream.write(response.getBytes());
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
public class Server {
public static void main(String[] args) throws IOException {
// 创建一个服务器监听在8888端口
ServerSocket serverSocket = new ServerSocket(8888);
ExecutorService executorService = Executors.newFixedThreadPool(100);
while (true){
Socket server = serverSocket.accept();
executorService.submit(new UserThread(server));
}
}
}
五、处理其他请求
我们给项目做一个升级版本,做一个登录的功能,能和数据库交互。
我们在D:/www下新建一个登录的页面,如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<form action="/register.do" method="post">
用户名:<input type="text" name="username" id="username" /> <br />
密码:<input type="password" name="password" id="password" /><br />
<input type="submit" value="注册" />
</form>
</body>
</html>
我们首先拿个页面做注册,注册的本质就是插入一条数据,我们也是简单演示,其他额外的判断我们统统不做。
我们不妨先把数据层面的代码准备好:
首先,我们用最简单的方式搞一个获取连接的工具类,这里是为了简单的观察本质,所以我们也不用连接池,怎么简单怎么来。
public class JDBCUtil {
public static Connection getConnection(){
// 1.数据库连接的4个基本要素:
InputStream in = JDBCUtil.class.getClassLoader().getResourceAsStream("jdbc.properties");
Properties properties = new Properties();
try {
properties.load(in);
} catch (IOException e) {
e.printStackTrace();
}
String url = properties.getProperty("url");
String user = properties.getProperty("username");
String password = properties.getProperty("password");
//2.获取连接
try {
return DriverManager.getConnection(url, user, password);
} catch (SQLException e) {
e.printStackTrace();
}
return null;
}
}
//User
public class User implements Serializable {
private static final Long serialVersionUID = 1L;
private Integer id;
private String username;
private String password;
.... 省略其他
}
// 注册的本质就是插入一条数据,登录的本质就是通过用户名获取用户比较密码,我们先将这两个方法准备好。
public class UserDao {
// 插入一个用户
public void insertUser(User user){
String sql = "insert into user (username,password)values(?,?)";
try(Connection connection = JDBCUtil.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement(sql)) {
preparedStatement.setString(1,user.getUsername());
preparedStatement.setString(2,user.getPassword());
preparedStatement.execute();
} catch (SQLException e) {
e.printStackTrace();
}
}
// 根据用户名获取用户
public User findUserByUsername(String username){
String sql = "select id,username,password from user where username = ?";
try(Connection connection = JDBCUtil.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement(sql)) {
preparedStatement.setString(1,username);
ResultSet resultSet = preparedStatement.executeQuery();
if(resultSet.first()){
User user = new User();
user.setId(resultSet.getInt("id"));
user.setUsername(resultSet.getString("username"));
user.setPassword(resultSet.getString("password"));
return user;
}
} catch (SQLException e) {
e.printStackTrace();
}
return null;
}
}
核心的问题来了,
当页面点击提交按钮实际上是发送一个post请求,从浏览器我们可以看得出来。
与此同时,后台也出现了问题:
在处理注册信息的时候,还是以文件的方式去寻找了,但是我们不希望如此,
我们希望的是将来能有一个类去独立处理这个请求,那怎么办?
此时我们希望不同的请求能有不同的方式,
此时我们将响应的处理抽象成一个接口:
public interface Servlet {
/**
* 这个方法专门用来处理请求和响应
* @param request
* @param response
*/
void service(Request request,Response response);
}
将之前的RespsoneHandler改为HTMLRespsoneHandler,专门处理网页
/**
* @author itnanls(私信联系)
* 处理响应的工具类
*/
public class HTMLResponseHandler implements ResponseHandler {
private HTMLResponseHandler(){}
private final static HTMLResponseHandler htmlResponseHandler
= new HTMLResponseHandler();
public static HTMLResponseHandler getInstance(){
return htmlResponseHandler;
}
// 定义我们网站的根目录
public static final String BASE_PATH = "D:/www/";
/**
* 此方法用来生成一个响应的字符串
* @param request
* @return
*/
public String build(Request request){
String htmlPath = BASE_PATH + request.getUri();
try ( FileInputStream fis = new FileInputStream(htmlPath)){
// 使用输入流读取文件的内容
String body = IOUtils.readString(fis);
Response response = new Response();
response.setBody(body);
response.addHeader("Content-Type","text/html;charset=UTF-8");
response.addHeader("Content-Length",Integer.toString(body.getBytes().length));
return response.buildMessage();
}catch (IOException e){
e.printStackTrace();
}
return null;
}
}
于此同时,我们再创建一个:
/**
* @author itnanls(私信联系)
* 处理响应的工具类
*/
public class DoResponseHandler implements ResponseHandler {
private DoResponseHandler(){}
private final static DoResponseHandler htmlResponseHandler
= new DoResponseHandler();
public static DoResponseHandler getInstance(){
return htmlResponseHandler;
}
/**
* 此方法用来生成一个响应的字符串
* @param request
* @return
*/
public String build(Request request){
// 我要根据不同的uri得到不同的处理结果
// 我们需要一个统一的出来请求响应的工具,不妨起个名字叫servlet
Servlet servlet = Container.ENV.get(request.getUri());
Response response = new Response();
servlet.service(request,response);
return response.buildMessage();
}
}
于是我们可以根据不同的请求处理不同的响应了:
public class RegisterServlet implements Servlet{
UserDao userDao = new UserDao();
@Override
public void service(Request request, Response response) {
String username = request.getParameter("username");
String password = request.getParameter("password");
User user = new User(username,password);
userDao.insertUser(user);
response.setBody("成功");
response.addHeader("Content-Type","text/plain;charset=UTF-8");
response.addHeader("Content-Length",Integer.toString(response.getBody().getBytes().length));
}
}
接下来就是登录了:
我们写一个登录的servlet来处理这个uri
public class LoginServlet implements Servlet{
UserDao userDao = new UserDao();
@Override
public void service(Request request, Response response) {
String username = request.getParameter("username");
User user = userDao.findUserByUsername(username);
// 1、没有查询到用户
// 2、查询到用户,但是密码不对
if(user == null || user.getPassword().equals(request.getParameter("password"))){
response.setBody("登陆失败");
response.addHeader("Content-Type","text/html;charset=UTF-8");
response.addHeader("Content-Length",Integer.toString(response.getBody().getBytes().length));
} else {
response.setBody("登陆成功");
response.addHeader("Content-Type","text/html;charset=UTF-8");
response.addHeader("Content-Length",Integer.toString(response.getBody().getBytes().length));
}
}
}
并且注册进去
ENV.put("/login.do",new LoginServlet());
登录是成功了,但是登录没有状态,哪里能说明这个客户端登录成功了呢?
六、状态的保存
http本身是不保存状态的,啥意思?就是客户端到服务器的连接不是长时间保持的,一个连接断开后,下一个请求会建立新的tcp连接,服务器不会知道我们之前连接过,说的简单一点就是,你登录了但是服务器不知道。
所以推出了session和cookie机制
cookie是记录在浏览器端的一个字符串,是一段不超过4KB的小型文本数据,由一个名称(Name)、一个值(Value)和其它几个用于控制Cookie有效期、安全性、使用范围的可选属性组成,
session是保存在服务器端的一个对象,比如map。它们俩互相配合让服务器有了能识别客户端一些状态的能力,意思就是服务就能知道这个客户端有没有登录等。cookie就相当于通行证,session就是门房,进去时需要从门房识别一个身份。
创建过程:
- 当浏览器第一次向客户端发送请求时,服务器会为它创建一个session,同时相应会加一个头(Set-Cookie: jsessionid=ewrwerwer123)
- 浏览器察觉到这个响应头之后会将信息保存在cookie当中,以后访问这个网站的时候就会一直带着这个cookie。
- 当下一个请求发起时,会带着这个cookie的信息,服务器通过查询id找的session,通过session内保存的信息,就能获得这个客户端的状态。
那我们继续改造:
我们先看看请求中有没有cookie
我们尝试给它加个cookie
目前代码多了,我们就在打开html的地方处理一下
response.addHeader("set-Cookie", "jsessionid="+UUID.randomUUID());
重新观察
一次访问没有:
二次访问已经有了:
当然我们只是想在没有Cookie的创建:
if(cookie == null || !cookie.contains("jsessionid")){
response.addHeader("set-Cookie", "jsessionid="+UUID.randomUUID());
}
处理请求和响应事实上是一个很麻烦的工作,我们确实需要一个工具帮我们方便的处理请求和响应。
这样有了id,我们还需要给每个客户端分配一个柜子:
先搞一个大柜子,每个session我们就用map处理,不就是存一点数据嘛!
public static final ConcurrentHashMap<String, Map<String,Object>> SESSIONS
= new ConcurrentHashMap<>(8);
为每一个客户端分配一点点空间
if(cookie == null || !cookie.contains("jsessionid")){
String sessionId = UUID.randomUUID().toString();
response.addHeader("set-Cookie", "jsessionid="+sessionId);
Container.SESSIONS.put(sessionId,new HashMap<>());
} else {
String id = cookie.split("=")[1];
Map<String, Object> session = Container.SESSIONS.get(id);
if(session == null)
Container.SESSIONS.put(id,new HashMap<>());
}
登录之后我们就能搞点事情了:
在登录servlet中添加:
String cookie = request.getHeader("Cookie");
if(cookie != null){
String sessionId = cookie.split("=")[1];
Map<String, Object> session = Container.SESSIONS.get(sessionId);
session.put("user",user);
}
不妨我们再添加一个首页:
如果登录了就显示首页欢迎您:xxx,没有登录就显示首页两个字。
public class IndexServlet implements Servlet {
@Override
public void service(Request request, Response response) {
String cookie = request.getHeader("Cookie");
if (cookie != null) {
String sessionId = cookie.split("=")[1];
Map<String, Object> session = Container.SESSIONS.get(sessionId);
if (session != null && session.get("user") != null) {
Object user = session.get("user");
User u = (User) user;
response.setBody("首页,欢迎您:" + u.getUsername());
response.addHeader("Content-Type", "text/html;charset=UTF-8");
response.addHeader("Content-Length", Integer.toString(response.getBody().getBytes().length));
return;
}
}
response.setBody("首页");
response.addHeader("Content-Type", "text/html;charset=UTF-8");
response.addHeader("Content-Length", Integer.toString(response.getBody().getBytes().length));
}
}
ENV.put("/index.do",new IndexServlet());
我们的项目目前先到此为止,从这个项目中,我们体会了很多。
配置文件
<web-app>
<data-source>
<properties name="url">jdbc:mysql://127.0.0.1:3306/ydlclass?characterEncoding=utf8&serverTimezone=Asia/Shanghai</properties>
<properties name="username">root</properties>
<properties name="password">root</properties>
<properties name="driverName">com.mysql.cj.jdbc.Driver</properties>
</data-source>
<servlets>
<servlet>
<url>/user</url>
<servlet-class>com.ydlclass.controller.UserServlet</servlet-class>
</servlet>
<servlet>
<url>/login</url>
<servlet-class>com.ydlclass.controller.LoginServlet</servlet-class>
</servlet>
<servlet>
<url>/register</url>
<servlet-class>com.ydlclass.controller.RegisterServlet</servlet-class>
</servlet>
<servlet>
<url>/index</url>
<servlet-class>com.ydlclass.controller.IndexServlet</servlet-class>
</servlet>
</servlets>
</web-app>
评论区