1. HTTP协议
1.1 特点
1.支持客户/服务器模式。
2.简单快速:客户向服务器请求服务时,只需传送请求方法和路径。请求方法常用的有GET、HEAD、POST。每种方法规定了客户与服务器联系的类型不同。由于HTTP协议简单,使得HTTP服务器的程序规模小,因而通信速度很快。
3.灵活:HTTP允许传输任意类型的数据对象。正在传输的类型由Content-Type加以标记。
4.无连接:无连接的含义是限制每次连接只处理一个请求。服务器处理完客户的请求,并收到客户的应答后,即断开连接。采用这种方式可以节省传输时间。
5.无状态:HTTP协议是无状态协议。无状态是指协议对于事务处理没有记忆能力。缺少状态意味着如果后续处理需要前面的信息,则它必须重传,这样可能导致每次连接传送的数据量增大。另一方面,在服务器不需要先前信息时它的应答就较快。
1.2 请求部分
http请求由三部分组成,分别是:请求行、请求头、请求体
1.2.1 请求行
请求行以一个方法符号开头,以空格分开,后面跟着请求的URI和协议的版本,格式如下:Method Request-URI HTTP-Version CRLF
1. Method表示请求方法;
2. Request-URI是一个统一资源标识符;
3. HTTP-Version表示请求的HTTP协议版本;
4. CRLF表示回车和换行(除了作为结尾的CRLF外,不允许出现单独的CR或LF字符)。
例如:POST /index.html http/1.1(换行)
1.2.2 请求头
请求报头允许客户端向服务器端传递请求的附加信息以及客户端自身的信息。
格式:请求头以键值对的形式存在,最后用一个空行隔开,表示请求头结束了。
例如:
k1:v1
k2:v2
k3:v3
username=xxx
常用的请求报头
Accept
Accept请求报头域用于指定客户端接受哪些类型的信息。eg:Accept:image/gif,表明客户端希望接受GIF图象格式的资源;Accept:text/html,表明客户端希望接受html文本。
Accept-Charset
Accept-Charset请求报头域用于指定客户端接受的字符集。eg:Accept-Charset:iso-8859-1,gb2312.如果在请求消息中没有设置这个域,缺省是任何字符集都可以接受。
Accept-Encoding
Accept-Encoding请求报头域类似于Accept,但是它是用于指定可接受的内容编码。eg:Accept-Encoding:gzip.deflate.如果请求消息中没有设置这个域服务器假定客户端对各种内容编码都可以接受。
Accept-Language
Accept-Language请求报头域类似于Accept,但是它是用于指定一种自然语言。eg:Accept-Language:zh-cn.如果请求消息中没有设置这个报头域,服务器假定客户端对各种语言都可以接受。
Authorization
Authorization请求报头域主要用于证明客户端有权查看某个资源。当浏览器访问一个页面时,如果收到服务器的响应代码为401(未授权),可以发送一个包含Authorization请求报头域的请求,要求服务器对其进行验证。
Host(发送请求时,该报头域是必需的)
Host请求报头域主要用于指定被请求资源的Internet主机和端口号,它通常从HTTP URL中提取出来的,eg:
我们在浏览器中输入:http://www.guet.edu.cn/index.html
浏览器发送的请求消息中,就会包含Host请求报头域,如下:
Host:www.guet.edu.cn
此处使用缺省端口号80,若指定了端口号,则变成:Host:www.guet.edu.cn:指定端口号
User-Agent
我们上网登陆论坛的时候,往往会看到一些欢迎信息,其中列出了你的操作系统的名称和版本,你所使用的浏览器的名称和版本,这往往让很多人感到很神奇,实际上,服务器应用程序就是从User-Agent这个请求报头域中获取到这些信息。User-Agent请求报头域允许客户端将它的操作系统、浏览器和其它属性告诉服务器。不过,这个报头域不是必需的,如果我们自己编写一个浏览器,不使用User-Agent请求报头域,那么服务器端就无法得知我们的信息了。
请求报头举例:
GET /form.html HTTP/1.1 (CRLF)
Accept:image/gif,image/x-xbitmap,image/jpeg,application/x-shockwave-flash,application/vnd.ms-excel,application/vnd.ms-powerpoint,application/msword,*/* (CRLF)
Accept-Language:zh-cn (CRLF)
Accept-Encoding:gzip,deflate (CRLF)
If-Modified-Since:Wed,05 Jan 2007 11:21:25 GMT (CRLF)
If-None-Match:W/"80b1a4c018f3c41:8317" (CRLF)
User-Agent:Mozilla/4.0(compatible;MSIE6.0;Windows NT 5.0) (CRLF)
Host:www.guet.edu.cn (CRLF)
Connection:Keep-Alive (CRLF)
(CRLF)
1.2.3 请求体
POST方式才有请求体。请求头的空行之后的内容就是请求体,用于携带客户端向服务端传递数据的载体
1.3 响应部分
在接收和解释请求消息后,服务器返回一个HTTP响应消息。
HTTP响应也是由三个部分组成,分别是:状态行、消息报头、响应正文
1.3.1 状态行
状态行格式如下:HTTP-Version Status-Code Reason-Phrase CRLF
HTTP-Version表示服务器HTTP协议的版本;
Status-Code表示服务器发回的响应状态代码;
Reason-Phrase表示状态代码的文本描述。
1.3.2 消息报头
HTTP消息由客户端到服务器的请求和服务器到客户端的响应组成。请求消息和响应消息都是由开始行(对于请求消息,开始行就是请求行,对于响应消息,开始行就是状态行),消息报头(可选),空行(只有CRLF的行),消息正文(可选)组成。
HTTP消息报头包括普通报头、请求报头、响应报头、实体报头。
每一个报头域都是由名字+“:”+空格+值 组成,消息报头域的名字是大小写无关的。
1、普通报头
在普通报头中,有少数报头域用于所有的请求和响应消息,但并不用于被传输的实体,只用于传输的消息。
eg:
Cache-Control 用于指定缓存指令,缓存指令是单向的(响应中出现的缓存指令在请求中未必会出现),且是独立的(一个消息的缓存指令不会影响另一个消息处理的缓存机制),HTTP1.0使用的类似的报头域为Pragma。
请求时的缓存指令包括:no-cache(用于指示请求或响应消息不能缓存)、no-store、max-age、max-stale、min-fresh、only-if-cached;
响应时的缓存指令包括:public、private、no-cache、no-store、no-transform、must-revalidate、proxy-revalidate、max-age、s-maxage.
eg:为了指示IE浏览器(客户端)不要缓存页面,服务器端的JSP程序可以编写如下:response.sehHeader("Cache-Control","no-cache");
//response.setHeader("Pragma","no-cache");作用相当于上述代码,通常两者//合用
这句代码将在发送的响应消息中设置普通报头域:Cache-Control:no-cache
Date普通报头域表示消息产生的日期和时间
Connection普通报头域允许发送指定连接的选项。例如指定连接是连续,或者指定“close”选项,通知服务器,在响应完成后,关闭连接
2、请求报头
请求报头允许客户端向服务器端传递请求的附加信息以及客户端自身的信息。
常用的请求报头
Accept
Accept请求报头域用于指定客户端接受哪些类型的信息。eg:Accept:image/gif,表明客户端希望接受GIF图象格式的资源;Accept:text/html,表明客户端希望接受html文本。
Accept-Charset
Accept-Charset请求报头域用于指定客户端接受的字符集。eg:Accept-Charset:iso-8859-1,gb2312.如果在请求消息中没有设置这个域,缺省是任何字符集都可以接受。
Accept-Encoding
Accept-Encoding请求报头域类似于Accept,但是它是用于指定可接受的内容编码。eg:Accept-Encoding:gzip.deflate.如果请求消息中没有设置这个域服务器假定客户端对各种内容编码都可以接受。
Accept-Language
Accept-Language请求报头域类似于Accept,但是它是用于指定一种自然语言。eg:Accept-Language:zh-cn.如果请求消息中没有设置这个报头域,服务器假定客户端对各种语言都可以接受。
Authorization
Authorization请求报头域主要用于证明客户端有权查看某个资源。当浏览器访问一个页面时,如果收到服务器的响应代码为401(未授权),可以发送一个包含Authorization请求报头域的请求,要求服务器对其进行验证。
Host(发送请求时,该报头域是必需的)
Host请求报头域主要用于指定被请求资源的Internet主机和端口号,它通常从HTTP URL中提取出来的,eg:
我们在浏览器中输入:http://www.guet.edu.cn/index.html
浏览器发送的请求消息中,就会包含Host请求报头域,如下:
Host:www.guet.edu.cn
此处使用缺省端口号80,若指定了端口号,则变成:Host:www.guet.edu.cn:指定端口号
User-Agent
我们上网登陆论坛的时候,往往会看到一些欢迎信息,其中列出了你的操作系统的名称和版本,你所使用的浏览器的名称和版本,这往往让很多人感到很神奇,实际上,服务器应用程序就是从User-Agent这个请求报头域中获取到这些信息。User-Agent请求报头域允许客户端将它的操作系统、浏览器和其它属性告诉服务器。不过,这个报头域不是必需的,如果我们自己编写一个浏览器,不使用User-Agent请求报头域,那么服务器端就无法得知我们的信息了。
请求报头举例:
GET /form.html HTTP/1.1 (CRLF)
Accept:image/gif,image/x-xbitmap,image/jpeg,application/x-shockwave-flash,application/vnd.ms-excel,application/vnd.ms-powerpoint,application/msword,*/* (CRLF)
Accept-Language:zh-cn (CRLF)
Accept-Encoding:gzip,deflate (CRLF)
If-Modified-Since:Wed,05 Jan 2007 11:21:25 GMT (CRLF)
If-None-Match:W/"80b1a4c018f3c41:8317" (CRLF)
User-Agent:Mozilla/4.0(compatible;MSIE6.0;Windows NT 5.0) (CRLF)
Host:www.guet.edu.cn (CRLF)
Connection:Keep-Alive (CRLF)
(CRLF)
3、响应报头
响应报头允许服务器传递不能放在状态行中的附加响应信息,以及关于服务器的信息和对Request-URI所标识的资源进行下一步访问的信息。
常用的响应报头
Location
Location响应报头域用于重定向接受者到一个新的位置。Location响应报头域常用在更换域名的时候。
Server
Server响应报头域包含了服务器用来处理请求的软件信息。与User-Agent请求报头域是相对应的。下面是
Server响应报头域的一个例子:
Server:Apache-Coyote/1.1
WWW-Authenticate
WWW-Authenticate响应报头域必须被包含在401(未授权的)响应消息中,客户端收到401响应消息时候,并发送Authorization报头域请求服务器对其进行验证时,服务端响应报头就包含该报头域。
eg:WWW-Authenticate:Basic realm="Basic Auth Test!" //可以看出服务器对请求资源采用的是基本验证机制。
4、实体报头
请求和响应消息都可以传送一个实体。一个实体由实体报头域和实体正文组成,但并不是说实体报头域和实体正文要在一起发送,可以只发送实体报头域。实体报头定义了关于实体正文(eg:有无实体正文)和请求所标识的资源的元信息。
常用的实体报头
Content-Encoding
Content-Encoding实体报头域被用作媒体类型的修饰符,它的值指示了已经被应用到实体正文的附加内容的编码,因而要获得Content-Type报头域中所引用的媒体类型,必须采用相应的解码机制。Content-Encoding这样用于记录文档的压缩方法,eg:Content-Encoding:gzip
Content-Language
Content-Language实体报头域描述了资源所用的自然语言。没有设置该域则认为实体内容将提供给所有的语言阅读
者。eg:Content-Language:da
Content-Length
Content-Length实体报头域用于指明实体正文的长度,以字节方式存储的十进制数字来表示。
Content-Type
Content-Type实体报头域用语指明发送给接收者的实体正文的媒体类型。eg:
Content-Type:text/html;charset=ISO-8859-1
Content-Type:text/html;charset=GB2312
Last-Modified
Last-Modified实体报头域用于指示资源的最后修改日期和时间。
Expires
Expires实体报头域给出响应过期的日期和时间。为了让代理服务器或浏览器在一段时间以后更新缓存中(再次访问曾访问过的页面时,直接从缓存中加载,缩短响应时间和降低服务器负载)的页面,我们可以使用Expires实体报头域指定页面过期的时间。eg:Expires:Thu,15 Sep 2006 16:23:12 GMT
HTTP1.1的客户端和缓存必须将其他非法的日期格式(包括0)看作已经过期。eg:为了让浏览器不要缓存页面,我们也可以利用Expires实体报头域,设置为0,jsp中程序如下:response.setDateHeader("Expires","0");
2. Socket编程
2.1 模拟客户端
编写一个客户端用于获取服务端响应的数据
package cn.bjc.bs;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
public class TestClient {
/*
* 1. 建立一个Socket对象,连接网址与端口号
* 2. 获取输出流对象
* 3. 获取输入流对象
* 4. 将HTTP协议的请求部分发送到服务端
* 5. 读取来自服务端的数据打印到控制台
* 6. 释放资源
* */
public static void main(String[] args) throws Exception {
// 1. 建立一个Socket对象,连接网址与端口号
Socket socket = new Socket("yun.itheima.com",80);
// 2. 获取输出流对象
InputStream in = socket.getInputStream();
// 3. 获取输入流对象
OutputStream out = socket.getOutputStream();
// 4. 将HTTP协议的请求部分发送到服务端
// 4.1 请求资源 /open/c-4/p/1.html
out.write("GET /open/c-4/p/1.html HTTP 1.1\n".getBytes());
// 4.2 请求头
out.write("HOST:yun.itheima.com\n".getBytes());
// 4.3 空行
out.write("\n".getBytes());
// 5. 读取来自服务端的数据打印到控制台
int i = in.read();
while(i != -1){
System.out.print((char)i);
i = in.read();
}
// 6. 释放资源
in.close();
in = null;
out.close();
out=null;
}
}
2.2 模拟服务端
编写一段代码,用于模拟服务端响应浏览器的请求
package cn.bjc.bs;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
public class TestServer {
public static void main(String[] args) throws Exception{
ServerSocket serverSocket = null;
Socket socket = null;
OutputStream out = null;
try {
// 1. 创建ServerSocket对象,监听本机的8080端口
serverSocket = new ServerSocket(8080);
while(true){
// 2. 等待来自客户端的请求获取和客户端对应的Socket对象
socket = serverSocket.accept();
// 3. 通过获取到的Socket对象获取到输出流
out = socket.getOutputStream();
// 4. 通过获取到的输出流对象将HTTP协议响应部分发送到客户端
// 4.1 响应头
out.write("HTTP/1.1 200 OK\n".getBytes());
out.write("Content-Type:text/html;charset=utf-8\n".getBytes());
out.write("Server:Apache-Coyote/1.1\n".getBytes());
out.write("\n\n".getBytes());
// 4. 响应体
StringBuffer buffer = new StringBuffer();
buffer.append("<html>");
buffer.append(" <head><title>标题</title></head>");
buffer.append(" <body>");
buffer.append(" <h1>xxx</h1>");
buffer.append(" <a href='http://www.baidu.com'>百度</a>");
buffer.append(" </body>");
buffer.append("</html>");
out.write(buffer.toString().getBytes());
out.flush();
}
} finally{
// 5. 释放资源
out.close();
out = null;
socket.close();
socket = null;
}
}
}
在浏览器输入:http://localhost:8080/
服务器响应如图:
3. 手动实现TomCat
3.1 静态资源
案例:
1. 在webcontent下发布静态资源的demo1.html,demo2.html
2. 启动Tomcat
3. 当客户端对服务端发起不同的请求,localhost:8080/demo.html
4. 服务端可以将对应的HTML页面响应到客户端。
3.1.1 工程创建
新建一个java项目,如图:
3.1.2 实现服务端的准备工作
1. 定义静态变量WEB.ROOT用于存放WebContent目录的绝对路径
2. 定义静态变量url,存放本次请求服务端的静态资源的名称
例如:
// 定义静态变量,用于存放服务端weContent目录的绝对路径
public static String WEB_ROOT = System.getProperty("user.dir")+File.separator+"webContent";
// 定义静态变量,用于存放本次请求的静态页面的名称
private static String url;
3.1.3 实现服务端的启动代码
1. 创建ServerSocket对象,监听本机8080端口
2. 等待来自客户端的请求
public static void main(String[] args) throws Exception {
ServerSocket server = null;
Socket socket = null;
// 1. 创建ServerSocket,监听本地8080端口,等待来自客户端的请求
try {
server = new ServerSocket(8080);
while(true){
// 获取客户端对应的socket
socket = server.accept();
}
} catch (IOException e) {
e.printStackTrace();
}
3.1.4 实现服务端HTTP请求部分代码
1. 读取HTTP协议请求部分数据
2. 解析请求行,获取本次请求的静态资源名称
public static void main(String[] args) throws Exception {
ServerSocket server = null;
Socket socket = null;
OutputStream out = null;
InputStream in = null;
// 1. 创建ServerSocket,监听本地8080端口,等待来自客户端的请求
try {
server = new ServerSocket(8080);
while(true){
// 获取客户端对应的socket
socket = server.accept();
// 获取输入流对象
in = socket.getInputStream();
// 获取输出流对象
out = socket.getOutputStream();
// 获取HTTP协议的请求部分,截取客户端要访问的资源名称,将这个资源名称赋值给url
parse(in);
// 发送静态资源
sendStaticResource(out);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
}
}
3.1.5 实现服务端向客户端发送HTTP协议响应部分
1. 通过输入流读取静态资源到服务端的内存
2. 将HTTP协议响应部分发送到客户端
private static void sendStaticResource(OutputStream out) throws Exception {
// 1. 定义一个字节数组,用于存放本次请求的静态资源demo01.html的内容
byte[] bytes = new byte[2048];
// 2. 定义一个文件输入流,用户获取静态资源demo01.html中的内容
FileInputStream fis = null;
try {
// 3. 创建文件对象File,代表本次要请求的资源demo01.html
File file = new File(WEB_ROOT,url);
// 4. 如果文件存在
if(file.exists()){
// 4.1 向客户端输出HTTP协议的响应行/响应头
out.write("HTTP/1.1 200 OK\n".getBytes());
out.write("Server:apache-Coyote/1.1\n".getBytes());
out.write("Content-Type:text/html;charset=utf-8\n".getBytes());
out.write("\n".getBytes());
// 4.2 获取到文件输入流对象
fis = new FileInputStream(file);
// 4.3 读取静态资源demo01.html中的内容到数组中
int ch = fis.read(bytes);
while(ch != -1){
// 4.4 将读取到数组中的内容通过输出流发送到客户端
out.write(bytes, 0, ch);
ch = fis.read(bytes);
}
}else {
// 5. 如果文件不存在
// 5.1 向客户端响应文件不存在的消息
out.write("HTTP/1.1 404 not found\n".getBytes());
out.write("Server:apache-Coyote/1.1\n".getBytes());
out.write("Content-Type:text/html;charset=utf-8\n".getBytes());
out.write("\n".getBytes());
String errMsg = "file not found";
out.write(errMsg.getBytes());
}
} catch(Exception e){
e.printStackTrace();
}finally {
// 6. 释放资源
if(null != fis){
fis.close();
}
}
}
完整的代码:
package cn.bjc.mytomcat.v1;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
public class TestServer {
// 定义静态变量,用于存放服务端weContent目录的绝对路径
public static String WEB_ROOT = System.getProperty("user.dir")+File.separator+"webContent";
// 定义静态变量,用于存放本次请求的静态页面的名称
private static String url;
public static void main(String[] args) throws Exception {
ServerSocket server = null;
Socket socket = null;
OutputStream out = null;
InputStream in = null;
// 1. 创建ServerSocket,监听本地8080端口,等待来自客户端的请求
try {
server = new ServerSocket(8080);
while(true){
// 获取客户端对应的socket
socket = server.accept();
// 获取输入流对象
in = socket.getInputStream();
// 获取输出流对象
out = socket.getOutputStream();
// 获取HTTP协议的请求部分,截取客户端要访问的资源名称,将这个资源名称赋值给url
parse(in);
// 发送静态资源
sendStaticResource(out);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
}
}
private static void sendStaticResource(OutputStream out) throws Exception {
// 1. 定义一个字节数组,用于存放本次请求的静态资源demo01.html的内容
byte[] bytes = new byte[2048];
// 2. 定义一个文件输入流,用户获取静态资源demo01.html中的内容
FileInputStream fis = null;
try {
// 3. 创建文件对象File,代表本次要请求的资源demo01.html
File file = new File(WEB_ROOT,url);
// 4. 如果文件存在
if(file.exists()){
// 4.1 向客户端输出HTTP协议的响应行/响应头
out.write("HTTP/1.1 200 OK\n".getBytes());
out.write("Server:apache-Coyote/1.1\n".getBytes());
out.write("Content-Type:text/html;charset=utf-8\n".getBytes());
out.write("\n".getBytes());
// 4.2 获取到文件输入流对象
fis = new FileInputStream(file);
// 4.3 读取静态资源demo01.html中的内容到数组中
int ch = fis.read(bytes);
while(ch != -1){
// 4.4 将读取到数组中的内容通过输出流发送到客户端
out.write(bytes, 0, ch);
ch = fis.read(bytes);
}
}else {
// 5. 如果文件不存在
// 5.1 向客户端响应文件不存在的消息
out.write("HTTP/1.1 404 not found\n".getBytes());
out.write("Server:apache-Coyote/1.1\n".getBytes());
out.write("Content-Type:text/html;charset=utf-8\n".getBytes());
out.write("\n".getBytes());
String errMsg = "file not found";
out.write(errMsg.getBytes());
}
} catch(Exception e){
e.printStackTrace();
}finally {
// 6. 释放资源
if(null != fis){
fis.close();
}
}
}
private static void parse(InputStream in) throws Exception {
// 1. 定义一个变量,存放HTTP协议请求部分数据
StringBuffer content = new StringBuffer();
// 2. 定义一个数组,存放HTTP协议请求部分数据
byte[] buffer = new byte[2048];
// 3. 定义一个变量i,代表读取数据到数组中之后,数据量的大小
int i = -1;
// 4. 读取客户端发过来的数据,将数据读取到字节数组buffer中,i代表读取数据量的大小311字节
i = in.read(buffer);
// 5. 遍历字节数组,将数组中的数据追加到content变量中
for(int j = 0 ; j < i ; j++){
content.append((char)buffer[j]);
}
// 6. 打印HTTP协议请求部分数据
System.out.println(content);
// 7. 截取客户端要请求的资源路径 demo.html,赋值给url
parseUrl(content.toString());
System.out.println(url);
}
// 解析请求数据得到请求连接
private static void parseUrl(String content) {
/*
* GET /erp/dep/getAll HTTP/1.1
Host: localhost:8080
Connection: keep-alive
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng;q=0.8
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Cookie: __guid=111872281.1743065663273879000.1567260921404.8972
*
* */
// 1. 截取客户端请求资源的名称 /erp/dep/getAll
// 2. 定义2个变量,存放请求行的2个空格位置
int index1,index2;
// 3. 获取http请求行的第一个空格的位置
index1 = content.indexOf(" ");
if(index1 != -1){ // 表示存在第一个空格
// 从第一个空格之后开始查找
// 4. 获取http请求行的第二个空格的位置
index2 = content.indexOf(" ", index1+1);
if(index2 > index1){
// 5. 截取字符串获取到本次请求的资源的名称
url = content.substring(index1+2, index2);
}
}
}
}
运行效果: