小鸟WebServer: 模拟Tomcat的基础功能,实现一个简易版的Web容器.基于TCP协议作为通讯协议,使用HTTP协议与客户端进行交互,完成一系列网络操作!
那现在我们要知道就是客户端给服务端发送的http请求的组成及其内容,然后服务器怎么解析这个http请求的,然后响应此请求发送的内容是什么,下面我们来了解了解http协议
这里我不展开细讲http协议,只讲解两部分(重要):
1、 浏览器给服务端发送的内容称为请求Request
;
2、 服务端给浏览器发送的内容称为响应Response
;
注意:请求和响应中大部分内容都是文本信息(字符串),并且这些文本数据使用的字符集为:
ISO8859-1
.这是一个欧洲的字符集,里面是不支持中文的!!!。而实际上请求和响应出现的字符也就是英文,数字,符号。
请求是浏览器发送给服务端的内容,HTTP协议中一个请求由三部分构成: 分别是:
请求行,消息头,消息正文
。消息正文部分可以没有。
1:请求行
请求行是一行字符串,以连续的两个字符(回车符和换行符)作为结束这一行的标志。
回车符:在ASC编码中2进制内容对应的整数是13.回车符通常用cr表示。
换行符:在ASC编码中2进制内容对应的整数是10.换行符通常用lf表示。 回车符和换行符实际上都是不可见字符。 请求行分为三部分:
请求方式(SP)抽象路径
(SP)协议版本(CRLF) 注:SP是空格
GET /index.html HTTP/1.1
消息头是浏览器可以给服务端发送的一些附加信息,有的用来说明浏览器自身内容,有的用来告知服务端交互细节,有的告知服务端消息正文详情等。
消息头由若干行组成,每行结束也是以CRLF标志。 每个消息头的格式为:消息头的名字(:SP)消息的值(CRLF)
消息头部分结束是以单独的(CRLF)标志。
3:消息正文
消息正文是2进制数据,通常是用户上传的信息,比如:在页面输入的注册信息,上传的附件等内容。
响应是服务端发送给客户端的内容。一个响应包含三部分:
状态行,响应头,响应正文
1:状态行
状态行是一行字符串(CRLF结尾),并且状态行由三部分组成,格式为:
protocol(SP)statusCode(SP)statusReason(CRLF)
协议版本(SP)状态代码(SP)状态描述(CRLF)
例如:
HTTP/1.1 200 OK
2:响应头
响应头与请求中的消息头格式一致,表示的是服务端发送给客户端的附加信息。
3:响应正文
2进制数据部分,包含的通常是客户端实际请求的资源内容。
响应的大致内容:
HTTP/1.1 200 OK(CRLF)
Content-Type: text/html(CRLF)
Content-Length: 2546(CRLF)(CRLF)
1011101010101010101......
补充:线程池
补充:BIO模型
补充:IP地址
分析:
WebServer:
在core核心包下创建WebServer类
public class WebServer {
private ServerSocket server;
private ExecutorService threadPool;
public WebServer(){
try {
System.out.println("正在启动服务端...");
server = new ServerSocket(8088);
threadPool = Executors.newFixedThreadPool(30);
System.out.println("服务端启动完毕!");
} catch (IOException e) {
e.printStackTrace();
}
}
public void start(){
try {
while (true) {
System.out.println("等待客户端连接...");
Socket socket = server.accept();
System.out.println("一个客户端连接了!");
//启动一个线程处理该客户端交互
ClientHandler handler = new ClientHandler(socket);
threadPool.execute(handler);
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
WebServer server = new WebServer();
server.start();
}
}
ClientHandler:
在core核心包下创建ClientHandler
public class ClientHandler implements Runnable{
private Socket socket;
public ClientHandler(Socket socket){
this.socket = socket;
}
public void run() {
1:解析请求(读取客户端发送过来的HTTP请求内容)
2:处理请求
3:响应客户端(发送一个HTTP响应给客户端)
}
}
上面就是我们 ClientHandler 目前要做的三大事情,后面增加业务还会往里面添加业务处理!
/**
* 请求对象
* 该类的每一个实例用于表示浏览器发送过来的一个HTTP请求
* HTTP协议要求一个请求由三部分构成:
* 请求行,消息头,消息正文
*/
public class HttpRequest {
//请求行相关信息
private String method;//请求行中的请求方式
private String uri;//请求行中的抽象路径
private String protocol;//请求行中的协议版本
//消息头相关信息
private Map<String,String> headers = new HashMap<>();
private Socket socket;
public HttpRequest(Socket socket){
System.out.println("HttpRequest:开始解析请求...");
this.socket = socket;
//1解析请求行
parseRequestLine();
//2解析消息头
parseHeaders();
//3解析消息正文
parseContent();
System.out.println("HttpRequest:请求解析完毕!");
}
private void parseRequestLine(){
System.out.println("HttpRequest:开始解析请求行...");
try {
String line = readLine();
System.out.println("请求行:" + line);
String[] data = line.split("\\s");
method = data[0];
uri = data[1];
protocol = data[2];
System.out.println("method:" + method);
System.out.println("uri:" + uri);
System.out.println("protocol:" + protocol);
}catch(IOException e){
e.printStackTrace();
}
System.out.println("HttpRequest:请求行解析完毕");
}
private void parseHeaders(){
System.out.println("HttpRequest:开始解析消息头...");
try {
while (true) {
String line = readLine();
if (line.isEmpty()) {
break;
}
String[] arr = line.split(":\\s");
headers.put(arr[0], arr[1]);
System.out.println("消息头:" + line);
}
System.out.println("headers:" + headers);
}catch(IOException e){
e.printStackTrace();
}
System.out.println("HttpRequest:消息头解析完毕!");
}
private void parseContent(){
System.out.println("HttpRequest:开始解析消息正文...");
System.out.println("HttpRequest:消息正文解析完毕!");
}
public String readLine() throws IOException {
/*
socket相同时,无论调用多少次getInputStream()方法,获取的输入流始终是同一个
*/
InputStream in = socket.getInputStream();
StringBuilder builder = new StringBuilder();
int d;
char cur='a';//本次读取到的字符
char pre='a';//上次读取到的字符
while((d = in.read())!=-1){
cur = (char)d;
if(pre==13 && cur==10){
break;
}
builder.append(cur);
pre = cur;
}
return builder.toString().trim();
}
}
对上述代码方法说明:
请求行、消息头、解析正文
,因为这三部分每行都以:CRLF 结尾,所以我们可以把读取每行封装成一个方法:readLine() 读取到空车+换行符就停止! private String method;//请求行中的请求方式
private String uri;//请求行中的抽象路径
private String protocol;//请求行中的协议版本
分别保存拆分的三部分!
//消息头相关信息
private Map<String,String> headers = new HashMap<>();
其中key为拆分消息头的前部分,value为拆分消息头的后半部分;最后当我们 readline()方法单独读取到:CRLF ,说明读取到了消息头末尾,结束!
System.out.println("HttpRequest:开始解析消息正文...");
System.out.println("HttpRequest:消息正文解析完毕!");
http://localhost:8088/index.html
来访问public class HttpResponse {
//状态行相关信息
private int statusCode = 200;//状态代码,默认值为200
private String statusReason = "OK";//状态描述,默认值为OK
//响应头相关信息
//响应正文相关信息
private File entity;//响应正文对应的实体文件
private Socket socket;
public HttpResponse(Socket socket){
this.socket = socket;
}
/**
* 将当前响应对象内容以标准的HTTP响应格式发送给客户端
*/
public void flush(){
System.out.println("开始发送响应");
//发送一个响应
//1发送状态行
sendStatusLine();
//2发送响应头
sendHeaders();
//3发送响应正文
sendContent();
System.out.println("响应发送完毕");
}
private void sendStatusLine(){
System.out.println("开始发送状态行");
try {
String line = "HTTP/1.1"+" "+statusCode+" "+statusReason;
System.out.println("状态行:"+line);
println(line);
}catch(IOException e){
e.printStackTrace();
}
System.out.println("状态行发送完毕");
}
private void sendHeaders(){
System.out.println("开始发送响应头");
try{
String line = "Content-Type: text/html";
println(line);
line = "Content-Length: "+entity.length();
println(line);
//单独发送CRLF表示响应头发送完毕!
println("");
}catch(IOException e){
e.printStackTrace();
}
System.out.println("响应头发送完毕");
}
private void sendContent(){
System.out.println("开始发送响应正文");
try{
OutputStream out = socket.getOutputStream();
FileInputStream fis = new FileInputStream(entity);
int len;
byte[] data = new byte[1024*10];
while((len = fis.read(data))!=-1){
out.write(data,0,len);
}
}catch(IOException e){
e.printStackTrace();
}
System.out.println("响应正文发送完毕");
}
private void println(String line) throws IOException {
OutputStream out = socket.getOutputStream();
out.write(line.getBytes("ISO8859-1"));
out.write(13);//发送一个回车符
out.write(10);//发送一个换行符
}
public File getEntity() {
return entity;
}
public void setEntity(File entity) {
this.entity = entity;
}
public int getStatusCode() {
return statusCode;
}
public void setStatusCode(int statusCode) {
this.statusCode = statusCode;
}
public String getStatusReason() {
return statusReason;
}
public void setStatusReason(String statusReason) {
this.statusReason = statusReason;
}
}
HTTP/1.1 200 OK(CRLF) //状态行
Content-Type: text/html(CRLF) //两行响应头
Content-Length: 2546(CRLF)(CRLF)
1011101010101010101...... //响应正文
HTTP/1.1 404 NotFound
我们对外提供对应的 get、set方法,就可以设置状态代码和状态描述!
private File entity;//响应正文对应的实体文件
然后通过 sendContent() 方法,把文件响应给客户端
改进一:
- 1:用户在浏览器输入网址(URL)时的格式如:http://localhost:8088/xxx/xxx
- 2:服务端中ClientHandler第一步就是解析请求,而解析请求的请求行中得到的抽象路径(uri属性)
保存的值就是URL中的/xxx/xxx这部分.- 3:因此我们可以从HttpRequest中获取uri属性的值,再去webapps下找到对应的文件后给客户端发送回去即可!
改进二:
当用户输入的资源路径无法在服务端找到该资源时,应当响应用户404页面告知.
- 1:在webapps下新建一个目录root 这个目录下存放所有网络应用都会用到的页面.404页面就是其中之一,因为无论用户请求哪个网络 ,应用都可能存在资源找不到的情况.
- 2:在root目录下新建一个页面:404.html 该页面居中显示一行字:404,资源不存在!
- 3:在ClientHandler处理请求的环节添加一个分支判断,如果根据抽象路径实例化File后,发现该文件
不存在时,就响应404给客户端. 响应中状态行中的状态代码为404,状态描述为NotFound- 响应头信息与正常响应一致,只不过Content-Length的值应当为404页面的文件长度 响应正文则是将404页面内容发送给客户端
ClientHandler:
public class ClientHandler implements Runnable{
private Socket socket;
public ClientHandler(Socket socket){
this.socket = socket;
}
public void run() {
try{
//1解析请求
HttpRequest request = new HttpRequest(socket);
HttpResponse response = new HttpResponse(socket);
//2处理请求
//通过request获取抽象路径
String path = request.getUri();
//根据抽象路径去webapps下找到对应的资源
File file = new File("./webapps" + path);
//检查该资源是否真实存在
if(file.exists()&&file.isFile()){
System.out.println("该资源已找到!");
//响应该资源
response.setEntity(file);
}else{
System.out.println("该资源不存在!");
//响应404
File notFoundPage = new File("./webapps/root/404.html");
response.setStatusCode(404);
response.setStatusReason("NotFound");
response.setEntity(notFoundPage);
}
//3响应客户端
response.flush();
}catch(Exception e){
e.printStackTrace();
}finally{
try {
//响应客户端后断开连接
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
html.index:
<html lang="en">
<head>
<meta charset="UTF-8">
<title>我的首页title>
head>
<body>
<center>
<img src="./logo.png"><br>
<input type="text" size="32">
<input type="button" value="百度一下" onclick="alert('点你妹啊!')"><br>
<a href="http://www.baidu.com">百度a>
center>
body>
html>
404.html:
<html lang="en">
<head>
<meta charset="UTF-8">
<title>404title>
head>
<body>
<center>
<h1>404,资源不存在!h1>
center>
body>
html>
测试: 打开服务器 WebServer,我们在浏览器输入:
http://localhost:8088/myweb/index.html
这就是访问我们服务器 myweb包下的index.html 文件,我们看看运行结果:
执行结果正确,我们再来验证下 输入错误的资源路径,服务器会不会给我们响应 404界面,我们输入:(注意:xxx.html是错误的资源路径)
http://localhost:8088/myweb/xxxx.html
下面我们要做的就是:
当我们可以重复接受多次请求时,会时不时的在HttpRequest的解析请求行方法parseRequestLine中出现数组下标越界异常,这是由于空请求引起的
有人肯定疑惑:不对啊,我们的请求怎么可能是空请求,我们不是输入的:
http://localhost:8088/myweb/资源名
要么服务器返回客户端请求的资源,要么返回404界面,空请求怎么来的?
HTTP协议中对此有说明:
为了保证服务器的健壮性,应当忽略客户端空的请求(客户端建立TCP连接后只发送了CRLF,并没有发送标准的HTTP请求内容,之后便于服务端断开连接了)
实现:
EmptyRequestException类:
/**
* 空请求异常
* 当HttpRequest解析请求时发现本次请求为空请求时会抛出该异常
*/
public class EmptyRequestException extends Exception{
public EmptyRequestException() {
}
public EmptyRequestException(String message) {
super(message);
}
public EmptyRequestException(String message, Throwable cause) {
super(message, cause);
}
public EmptyRequestException(Throwable cause) {
super(cause);
}
public EmptyRequestException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}
修改HttpRequest类:
public class HttpRequest {
//请求行相关信息
private String method;//请求行中的请求方式
private String uri;//请求行中的抽象路径
private String protocol;//请求行中的协议版本
//消息头相关信息
private Map<String,String> headers = new HashMap<>();
//消息正文相关信息
private Socket socket;
public HttpRequest(Socket socket) throws EmptyRequestException {
System.out.println("HttpRequest:开始解析请求...");
this.socket = socket;
//1解析请求行
parseRequestLine();
//2解析消息头
parseHeaders();
//3解析消息正文
parseContent();
System.out.println("HttpRequest:请求解析完毕!");
}
private void parseRequestLine() throws EmptyRequestException {
System.out.println("HttpRequest:开始解析请求行...");
try {
String line = readLine();
if(line.isEmpty()){//如果是空字符串,说明是空请求!!!
throw new EmptyRequestException();
}
System.out.println("请求行:" + line);
String[] data = line.split("\\s");
method = data[0];
uri = data[1];
protocol = data[2];
System.out.println("method:" + method);
System.out.println("uri:" + uri);
System.out.println("protocol:" + protocol);
}catch(IOException e){
e.printStackTrace();
}
System.out.println("HttpRequest:请求行解析完毕");
}
private void parseHeaders(){
System.out.println("HttpRequest:开始解析消息头...");
try {
while (true) {
String line = readLine();
if (line.isEmpty()) {
break;
}
String[] arr = line.split(":\\s");
headers.put(arr[0], arr[1]);
System.out.println("消息头:" + line);
}
System.out.println("headers:" + headers);
}catch(IOException e){
e.printStackTrace();
}
System.out.println("HttpRequest:消息头解析完毕!");
}
private void parseContent(){
System.out.println("HttpRequest:开始解析消息正文...");
System.out.println("HttpRequest:消息正文解析完毕!");
}
private String readLine() throws IOException {
/*
socket相同时,无论调用多少次getInputStream()方法,获取的输入流始终是同一个
*/
InputStream in = socket.getInputStream();
StringBuilder builder = new StringBuilder();
int d;
char cur='a';//本次读取到的字符
char pre='a';//上次读取到的字符
while((d = in.read())!=-1){
cur = (char)d;
if(pre==13 && cur==10){
break;
}
builder.append(cur);
pre = cur;
}
return builder.toString().trim();
}
public String getMethod() {
return method;
}
public String getUri() {
return uri;
}
public String getProtocol() {
return protocol;
}
public String getHeader(String name) {
return headers.get(name);
}
}
修改ClientHandler类:
public class ClientHandler implements Runnable{
private Socket socket;
public ClientHandler(Socket socket){
this.socket = socket;
}
public void run() {
try{
//1解析请求
HttpRequest request = new HttpRequest(socket);
HttpResponse response = new HttpResponse(socket);
//2处理请求
//通过request获取抽象路径
String path = request.getUri();
//根据抽象路径去webapps下找到对应的资源
File file = new File("./webapps" + path);
//检查该资源是否真实存在
if(file.exists()&&file.isFile()){
System.out.println("该资源已找到!");
//响应该资源
response.setEntity(file);
}else{
System.out.println("该资源不存在!");
//响应404
File notFoundPage = new File("./webapps/root/404.html");
response.setStatusCode(404);
response.setStatusReason("NotFound");
response.setEntity(notFoundPage);
}
//3响应客户端
response.flush();
}catch(EmptyRequestException e){
//单独捕获空请求异常,但是不需要做任何处理,这个异常抛出仅为了忽略处理操作
}catch(Exception e){
e.printStackTrace();
}finally{
try {
//响应客户端后断开连接
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
改变一:
服务端发送响应中的响应头Content-Type的值是固定的text/html.这会导致无论浏览器请求的资源是什么类型的内容,我们统一回复客户端时告诉它这是一个“页面”,从而浏览器无法正确解释这个资源,出现展示页面不正确的情况
对此,我们要对HttpResponse发送响应头的工作进行重构,这个过程分为两部分进行
1: 虽然当前项目我们需要发送的响应头只有Content-Type和Content-Length.但是实际上服务端给客户端发送的
响应头还有一些,可以结合请求的处理结果发送对应的其他响应头。因此我们要支持向HttpResponse中设置不同
的响应头,将来在flush时可以将这些头都发送给客户端,而不是固定的只发上述的两个响应头
2: 发送Content-Type的值时要结合客户端实际请求的资源对应的类型来发送,而不能发送固定的text/html.否则
浏览器可能无法正确理解其请求的资源从而导致显示异常。
实现:
一:实现HttpResponse可以根据设置的响应头进行发送
1:在HttpResponse中添加一个属性:private Map
用这个Map保存所有要给客户端发送的响应头,其中key是响应头的名字,value为这个响应头的值
2:在sendHeaders方法中改为通过遍历headers将所有的响应头发送给客户端
3:添加一个方法putHeader,允许外界设置要发送的响应头到HttpResponse中。
这样一来,在flush之前只要将需要发送的响应头都放到headers这个Map中,在flush时就可以将他们全部发送了
改变二:
定义一个类来保存所有HTTP协议规定的不会变的内容,以便更好的重用他们.
改变三:
ClientHandler还有一个操作可以被重用,在处理请求的环节,当我们将正文文件设置到response后总是还要添加两个说明正文的响应头Content-Type和Content-Length.既然这两个头和正文是密切相关的,
我们完全可以将设置这两个响应头的操作放在HttpResponse的setEntity方法中.这样一来将来只需要将正文文件设置好就可以了,两个头就自动被添加了.
HttpContext 类:
/**
* HTTP协议规定的内容都定义在这里,以便将来重用
*/
public class HttpContext {
/**
* Content-Type信息
* key:资源的后缀名
* value:对应的Content-Type的值
*/
private static Map<String,String> mimeMapping = new HashMap<>();
static{
initMimeMapping();
}
private static void initMimeMapping(){
// mimeMapping.put("html","text/html");
// mimeMapping.put("css","text/css");
// mimeMapping.put("js","application/javascript");
// mimeMapping.put("png","image/png");
// mimeMapping.put("jpg","image/jpeg");
// mimeMapping.put("gif","image/gif");
/*
通过解析config/web.xml文件初始化mimeMapping
将根标签下所有名为<mime-mapping>的子标签获取到
并将其中的子标签:
<extension>中间的文本作为key
<mime-type>中间的文本作为value
保存到mimeMapping这个Map中
初始化完毕后,mimeMapping中应当有1011个元素
*/
try{
SAXReader reader = new SAXReader();
Document doc = reader.read("./config/web.xml");
Element root = doc.getRootElement();
List<Element> list = root.elements("mime-mapping");
for(Element mime : list){
String key = mime.elementText("extension");
String value = mime.elementText("mime-type");
mimeMapping.put(key,value);
}
}catch(Exception e){
e.printStackTrace();
}
System.out.println(mimeMapping.size());//1011
}
/**
* 根据资源后缀名获取对应的Content-Type的值
* @param ext
* @return
*/
public static String getMimeType(String ext){
return mimeMapping.get(ext);
}
}
HttpResponse类:
/**
* 响应对象
* 当前类的每一个实例表示给客户端发送的一个HTTP响应
* 一个响应应当包含三部分:状态行,响应头,响应正文
*/
public class HttpResponse {
//状态行相关信息
private int statusCode = 200;//状态代码,默认值为200
private String statusReason = "OK";//状态描述,默认值为OK
//响应头相关信息
private Map<String,String> headers = new HashMap<>();
//响应正文相关信息
private File entity;//响应正文对应的实体文件
private Socket socket;
public HttpResponse(Socket socket){
this.socket = socket;
}
/**
* 将当前响应对象内容以标准的HTTP响应格式发送给客户端
*/
public void flush(){
System.out.println("开始发送响应");
//发送一个响应
//1发送状态行
sendStatusLine();
//2发送响应头
sendHeaders();
//3发送响应正文
sendContent();
System.out.println("响应发送完毕");
}
private void sendStatusLine(){
System.out.println("开始发送状态行");
try {
String line = "HTTP/1.1"+" "+statusCode+" "+statusReason;
System.out.println("状态行:"+line);
println(line);
}catch(IOException e){
e.printStackTrace();
}
System.out.println("状态行发送完毕");
}
private void sendHeaders(){
System.out.println("开始发送响应头");
try{
// String line = "Content-Type: text/html";
// println(line);
// line = "Content-Length: "+entity.length();
// println(line);
//遍历headers这个Map,将所有的响应头发送给客户端
Set<Map.Entry<String,String>> entrySet = headers.entrySet();
for(Map.Entry<String,String> e : entrySet){
String key = e.getKey();//响应头的名字
String value = e.getValue();//响应头的值
String line = key + ": " + value;
System.out.println("响应头:" + line);
println(line);
}
//单独发送CRLF表示响应头发送完毕!
println("");
}catch(IOException e){
e.printStackTrace();
}
System.out.println("响应头发送完毕");
}
private void sendContent(){
System.out.println("开始发送响应正文");
try{
OutputStream out = socket.getOutputStream();
FileInputStream fis = new FileInputStream(entity);
int len;
byte[] data = new byte[1024*10];
while((len = fis.read(data))!=-1){
out.write(data,0,len);
}
}catch(IOException e){
e.printStackTrace();
}
System.out.println("响应正文发送完毕");
}
private void println(String line) throws IOException {
OutputStream out = socket.getOutputStream();
out.write(line.getBytes("ISO8859-1"));
out.write(13);//发送一个回车符
out.write(10);//发送一个换行符
}
/**
* 向当前响应对象中添加一个响应头
* @param name 响应头的名字
* @param value 响应头的值
*/
public void putHeader(String name,String value){
headers.put(name,value);
}
public File getEntity() {
return entity;
}
public void setEntity(File entity) {
this.entity = entity;
//根据资源文件名获取后缀名 test.1.1.css
String ext = entity.getName().substring(entity.getName().lastIndexOf(".")+1);
String type = HttpContext.getMimeType(ext);
//根据正文文件设置响应头
putHeader("Content-Type",type);
putHeader("Content-Length",entity.length()+"");
}
public int getStatusCode() {
return statusCode;
}
public void setStatusCode(int statusCode) {
this.statusCode = statusCode;
}
public String getStatusReason() {
return statusReason;
}
public void setStatusReason(String statusReason) {
this.statusReason = statusReason;
}
}
链接https://pan.baidu.com/s/1HP9QgdNQn0XiKpC6T_jVFQ
提取码:4897