V1:
``首先创建服务器,端口号为8088,代码如下:
package com.webserver.core;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
/**
* WebServer主类
*/
public class WebServerApplication {
private ServerSocket serverSocket;
public WebServerApplication(){
try {
System.out.println("正在启动服务端...");
serverSocket = new ServerSocket(8088);
System.out.println("服务端启动完毕!");
} catch (IOException e) {
e.printStackTrace();
}
}
public void start(){
try {
System.out.println("等待客户端链接...");
Socket socket = serverSocket.accept();
System.out.println("一个客户端链接了!");
//启动一个线程处理与该客户端的交互
ClientHandler handler = new ClientHandler(socket);
Thread t = new Thread(handler);
t.start();
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
WebServerApplication application = new WebServerApplication();
application.start();
}
}
V2
HTTP协议要求浏览器连接服务端后应当发送一个请求,因此本版本实现读取请求并输出到控制台来了解请求
的格式和内容。
实现:
由于服务端可以同时接收多客户端的连接,因此与聊天室相同,主线程仅负责接受客户端的连接,一旦一个
客户端连接后则启动一个线程来处理。
1:在com.webserver.core下新建类:ClientHandler(实现Runnable接口),作为线程任务。
工作是负责与连接的客户端进行HTTP交互
2:WebServerApplication主线程接收连接后启动线程执行ClientHandler这个任务处理客户端交互
3:在ClientHandler中读取客户端发送过来的内容(请求内容)并打桩输出
package com.webserver.core;
import java.io.IOException;
import java.io.InputStream;
import java.net.Socket;
/**
* 处理一次与客户端的HTTP交互操作
* 处理一次HTTP交互由三步完成:
* 1:解析请求
* 2:处理请求
* 3:发送响应
*/
public class ClientHandler implements Runnable {
private Socket socket;
public ClientHandler(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try {
//1解析请求
InputStream in = socket.getInputStream();
int d;
while((d = in.read())!=-1){
System.out.print((char)d);
}
//2处理请求
//3发送响应
} catch (IOException e) {
e.printStackTrace();
}
}
}
V3
解析请求
HTTP协议要求客户端连接后会发送一个请求,每个请求由三部分构成:
请求行 消息头 消息正文
首先请求行和消息头有一个共同的特点:都是以CRLF结尾的一行字符串.
因此先实现读取一行字符串的操作,测试将请求行读取出来并进行解析.之后可以再利用这个
操作完成消息头的读取并解析.
实现:
在ClientHandler中完成读取一行字符串的操作.
package com.webserver.core;
import java.io.IOException;
import java.io.InputStream;
import java.net.Socket;
/**
* 处理一次与客户端的HTTP交互操作
* 处理一次HTTP交互由三步完成:
* 1:解析请求
* 2:处理请求
* 3:发送响应
*/
public class ClientHandler implements Runnable {
private Socket socket;
public ClientHandler(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try {
//1解析请求
//测试读取一行字符串(以CRLF结尾)
InputStream in = socket.getInputStream();
int d;//每次读取到的字节
char cur='a',pre='a';//cur表示本次读取到的字符,pre表示上次读取到的字符
StringBuilder builder = new StringBuilder();
while((d = in.read())!=-1){
cur = (char)d;
if(pre==13&&cur==10){
//是否已经连续读取到了回车+换行符
break;
}
builder.append(cur);
pre = cur;
}
String line = builder.toString().trim();
System.out.println("请求行:"+line);
//请求行相关信息
String method; //请求方式
String uri; //抽象路径
String protocol;//协议版本
String[] data = line.split("\\s");
method = data[0];
uri = data[1];//这里可能出现数组下标越界,这是因为浏览器发送了空请求导致的。后期会解决。现在出现该异常先忽略。重新启动服务端重新测试。
protocol = data[2];
//测试路径:http://localhost:8088/index.html
System.out.println("method:"+method);//method:GET
System.out.println("uri:"+uri);//uri:/index.html
System.out.println("protocol:"+protocol);//protocol:HTTP/1.1
//2处理请求
//3发送响应
} catch (IOException e) {
e.printStackTrace();
}
}
}
V4
继续解析请求
上一个版本完成了解析请求行的操作,继续使用该操作完成解析消息头
实现:
1:在ClientHandler中定义方法:readLine,用于将读取一行字符串的操作重用
2:将解析请求行中读取第一行内容的操作改为调用readLine
3:继续利用readLine读取消息头并保存每个消息头
package com.webserver.core;
import java.io.IOException;
import java.io.InputStream;
import java.net.Socket;
import java.util.HashMap;
import java.util.Map;
/**
* 处理一次与客户端的HTTP交互操作
* 处理一次HTTP交互由三步完成:
* 1:解析请求
* 2:处理请求
* 3:发送响应
*/
public class ClientHandler implements Runnable {
private Socket socket;
public ClientHandler(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try {
//1解析请求
//1.1解析请求行
String line = readLine();
System.out.println("请求行:"+line);
//请求行相关信息
String method; //请求方式
String uri; //抽象路径
String protocol;//协议版本
String[] data = line.split("\\s");
method = data[0];
uri = data[1];//这里可能出现数组下标越界,这是因为浏览器发送了空请求导致的。后期会解决。现在出现该异常先忽略。重新启动服务端重新测试。
protocol = data[2];
//测试路径:http://localhost:8088/index.html
System.out.println("method:"+method);//method:GET
System.out.println("uri:"+uri);//uri:/index.html
System.out.println("protocol:"+protocol);//protocol:HTTP/1.1
//1.2:解析消息头
Map<String,String> headers = new HashMap<>();
while(true) {
line = readLine();
if(line.isEmpty()){
//如果readLine返回空字符串,说明单独读取到了回车+换行
break;
}
System.out.println("消息头:" + line);
/*
将每一个消息头按照": "(冒号+空格拆)分为消息头的名字和消息头的值
并以key,value的形式存入到headers中
*/
data = line.split(":\\s");
headers.put(data[0],data[1]);
}//while循环结束,消息头解析完毕
System.out.println("headers:"+headers);
//2处理请求
//3发送响应
} catch (IOException e) {
e.printStackTrace();
}
}
private String readLine() throws IOException {
//通常被重用的代码都不自己处理异常
//同一个socket对象无论调用多少次getInputStream()获取的始终是同一个输入流
InputStream in = socket.getInputStream();
int d;//每次读取到的字节
char cur='a',pre='a';//cur表示本次读取到的字符,pre表示上次读取到的字符
StringBuilder builder = new StringBuilder();
while((d = in.read())!=-1){
cur = (char)d;
if(pre==13&&cur==10){
//是否已经连续读取到了回车+换行符
break;
}
builder.append(cur);
pre = cur;
}
return builder.toString().trim();
}
}
V5
重构
进行功能个拆分,将ClientHandler中第一个环节解析请求的细节拆分出去,使得
ClientHandler仅关心处理一次HTTP交互的流程控制.
实现:
1:新建一个包:com.webserver.http
2:在http包下新建类:HttpServletRequest 请求对象
使用这个类的每一个实例表示客户端发送过来的一个HTTP请求
3:在HttpServletRequest的构造方法中完成解析请求的工作
4:ClientHandler第一步解析请求只需要实例化一个HttpServletRequest即可.
package com.webserver.http;
import java.io.IOException;
import java.io.InputStream;
import java.net.Socket;
import java.util.HashMap;
import java.util.Map;
/**
* 请求对象
* 该类的每一个实例用于表示浏览器发送过来的一个HTTP的请求内容
* 每个请求HTTP协议要求由三部分构成:请求行,消息头,消息正文
*/
public class HttpServletRequest {
private Socket socket;
//请求行相关信息
private String method; //请求方式
private String uri; //抽象路径
private String protocol;//协议版本
//消息头相关信息
private Map<String,String> headers = new HashMap<>();
public HttpServletRequest(Socket socket) throws IOException {
this.socket = socket;
//1.1解析请求行
parseRequestLine();
//1.2:解析消息头
parseHeaders();
//1.3:解析消息正文
parseContent();
}
//解析请求行
private void parseRequestLine() throws IOException {
String line = readLine();
System.out.println("请求行:"+line);
String[] data = line.split("\\s");
method = data[0];
uri = data[1];//这里可能出现数组下标越界,这是因为浏览器发送了空请求导致的。后期会解决。现在出现该异常先忽略。重新启动服务端重新测试。
protocol = data[2];
}
//解析消息头
private void parseHeaders() throws IOException {
while(true) {
String line = readLine();
if(line.isEmpty()){
//如果readLine返回空字符串,说明单独读取到了回车+换行
break;
}
System.out.println("消息头:" + line);
/*
将每一个消息头按照": "(冒号+空格拆)分为消息头的名字和消息头的值
并以key,value的形式存入到headers中
*/
String[] data = line.split(":\\s");
headers.put(data[0],data[1]);
}//while循环结束,消息头解析完毕
System.out.println("headers:"+headers);
}
//解析消息正文
private void parseContent(){
}
private String readLine() throws IOException {
//通常被重用的代码都不自己处理异常
//同一个socket对象无论调用多少次getInputStream()获取的始终是同一个输入流
InputStream in = socket.getInputStream();
int d;//每次读取到的字节
char cur='a',pre='a';//cur表示本次读取到的字符,pre表示上次读取到的字符
StringBuilder builder = new StringBuilder();
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);
}
}
package com.webserver.core;
import com.webserver.http.HttpServletRequest;
import java.io.IOException;
import java.io.InputStream;
import java.net.Socket;
import java.util.HashMap;
import java.util.Map;
/**
* 处理一次与客户端的HTTP交互操作
* 处理一次HTTP交互由三步完成:
* 1:解析请求
* 2:处理请求
* 3:发送响应
*/
public class ClientHandler implements Runnable {
private Socket socket;
public ClientHandler(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try {
//1解析请求
HttpServletRequest request = new HttpServletRequest(socket);
System.out.println(request.getMethod());//GET
//2处理请求
//3发送响应
} catch (IOException e) {
e.printStackTrace();
}
}
}
V6
此版本完成响应客户端的工作
这里先将ClientHandler中处理一次交互的第三步:响应客户端 实现出来。
目标:将一个固定的html页面通过发送一个标准的HTTP响应回复给浏览器使其呈现出来。
需要的知识点:
1:HTML基础语法,html是超文本标记语言,用于构成一个"网页"的语言。
2:HTTP的响应格式。
实现:
一:先创建第一个页面index.html
1:在src/main/resource下新建目录static
这个目录用于存放当前服务端下所有的静态资源。
2:在static目录下新建目录新建第一个页面:index.html
二:实现将index.html页面响应给浏览器
在ClientHandler第三步发送响应处,按照HTTP协议规定的响应格式,将该页面包含在正文部分将其发送给浏览器即可。
三:第二步测试成功后,我们就可以根据请求中浏览器传递过来的抽象路径去static目录下定位浏览器实际
请求的页面,然后用上述方式将该页面响应给浏览器,达到浏览器自主请求其需要的资源。
四:一问一答实现后,在ClientHandler异常处理机制最后添加finally并最终与客户端断开链接。这也是
HTTP协议的要求,因此在这里调用socket.close()
五:可以在WebServerApplication中的start方法里将接受客户端链接的动作放在死循环里重复进行了。
无论浏览器请求多少次,我们都可以遵从一问一答的原则响应用户请求的所有页面了。
classtable.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>课程表</title>
</head>
<body>
<center>
<h1>课程表</h1>
<table border="1">
<tr>
<td> </td>
<td>星期一</td>
<td>星期二</td>
<td>星期三</td>
<td>星期四</td>
<td>星期五</td>
</tr>
<tr>
<td rowspan="4">上午</td>
<td>语文</td>
<td>数学</td>
<td>英语</td>
<td>体育</td>
<td>生物</td>
</tr>
<tr>
<td>生物</td>
<td>数学</td>
<td>英语</td>
<td>语文</td>
<td>体育</td>
</tr>
<tr>
<td>生物</td>
<td>数学</td>
<td>英语</td>
<td>语文</td>
<td>体育</td>
</tr>
<tr>
<td>生物</td>
<td>数学</td>
<td>英语</td>
<td>语文</td>
<td>体育</td>
</tr>
<tr>
<td colspan="6" align="center"><a href="http://www.bilibili.com">午休,bilibili一下</a></td>
</tr>
<tr>
<td rowspan="4">下午</td>
<td>语文</td>
<td>数学</td>
<td>英语</td>
<td>体育</td>
<td>生物</td>
</tr>
<tr>
<td>生物</td>
<td>数学</td>
<td>英语</td>
<td>语文</td>
<td>体育</td>
</tr>
<tr>
<td>生物</td>
<td>数学</td>
<td>英语</td>
<td>语文</td>
<td>体育</td>
</tr>
<tr>
<td>生物</td>