V1部分
- 创建主启动类BirdBootApplication,初始化ServerSocket,设置端口,start方法中启动一个线程处理与该客户端交互,先实现一次功能,后期加入循环操作。创建客户端处理器ClientHandler,与指定的客户端完成一次HTTP的交互。使浏览器可以连接到服务器。
package com.websever.core;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
public class BirdBootApplication {
private ServerSocket serverSocket;
public BirdBootApplication(){
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) {
BirdBootApplication application = new BirdBootApplication();
application.start();
}
}
package com.webserver.core;
import java.io.IOException;
import java.io.InputStream;
import java.net.Socket;
/**
* 该线程任务负责与指定的客户端进行HTTP交互
*/
public class ClientHandler implements Runnable{
private Socket socket;
public ClientHandler(Socket socket){
this.socket = socket;
}
public void run(){
try {
InputStream in = socket.getInputStream();
int d;
/*
在浏览器输入路径:
http://localhost:8088/index.html
查看浏览器发送过来的请求内容
注意:
如果这里打桩输出的是乱码,则说明浏览器地址栏输入测试路径时没有输入
最开始的http://。这会导致浏览器可能自行使用了https://。这是一个
加密的HTTP协议,导致浏览器读取乱码。
如果控制台只输出一个客户端连接,没有请求内容,通常是因为浏览器使用
缓存而没有真实发送请求内容。
解决办法:
1:更换浏览器测试
2:浏览器尽量不要用"后退,前进"这个功能测试,而是使用刷新访问。
*/
while((d = in.read())!=-1){
System.out.print((char)d);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
v1部分结束
v2部分
- ClientHandler中设置处理客户端三部曲
- 该线程任务负责与指定的客户端
进行HTTP交互按照HTTP协议的交互规则,与浏览器采取一问一答。
规划三部:
1:解析请求(生成HttpServletRequest对象):读取请求行、解析消息头、解析消息正文。
2:处理请求(交给SpringMVC框架处理)
3:发送响应(将HttpServletResponse内容发送给浏览器) 断开TCP连接
首先设置解析请求中的, 1.1读取请求行
1.1读取请求行:获取socket输入流,循环读取,一个循环读取的pre字符,将pre字符添加在builder.append上,并将读取的pre赋值给cur,继续循环。直到读取pre=13,cur=10,换行回车符,停止循环,即读完一行。将读到的第一行按空格拆分,将拆分的三项装在数组中,并设置相应的对象名。
package com.webserver.core;
import java.io.IOException;
import java.io.InputStream;
import java.net.Socket;
/**
* 该线程任务负责与指定的客户端进行HTTP交互
* 按照HTTP协议的交互规则,与浏览器采取一问一答。
* 规划三部:
* 1:解析请求(生成HttpServletRequest对象)
* 2:处理请求(交给SpringMVC框架处理)
* 3:发送响应(将HttpServletResponse内容发送给浏览器)
* 断开TCP连接
*
*/
public class ClientHandler implements Runnable{
private Socket socket;
public ClientHandler(Socket socket){
this.socket = socket;
}
public void run(){
try {
//1解析请求
//1.1:读取请求行
//测试读取一行字符串的操作
InputStream in = socket.getInputStream();
StringBuilder builder = new StringBuilder();//保存拼接的一行内容
char cur='a',pre='a';//cur记录本次读取的字符,pre记录上次读取的字符
int d;//每次读取的字节
while((d = in.read())!=-1){
cur = (char)d;//将本次读取到的字节转换为char记录在cur上。
if(pre==13 && cur==10){//如果上次读取的为回车符,本次读取的是换行符
break;//停止读取(一行结束了)
}
builder.append(cur);//将本次读取的字符拼接到已经读取的字符串中
pre = cur;//在下次读取前将本次读取的字符记作"上次读取的字符"
}
String line = builder.toString().trim();
System.out.println("请求行:"+line);
//将请求行内容按照空格拆分为三部分,并赋值给下面三个变量
String[]data = line.split("\s");
String method = data[0];//请求方式
String uri = data[1];//抽象路径
String protocol = data[2];//协议版本
System.out.println("method:"+method);
System.out.println("uri:"+uri);
System.out.println("protocol:"+protocol);
//2处理请求
//3发送响应
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
//一问一答后断开TCP连接
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
v2部分结束
v3部分
- 设置解析请求中的1.2:解析消息头
- 将循环读取的操作,封装在一个方法中,readLien(),即读取一行内容。
1.2解析消息头:创建Map引用类型对象泛型都为String,循环读取方法,直到读取为空字符串,停止读取。将读取到的每一行都按冒号空格: 拆分,装进String数组,之后将两个对象装入map集合中。
package com.webserver.core;
import java.io.IOException;
import java.io.InputStream;
import java.net.Socket;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
public class ClientHandler implements Runnable{
private Socket socket;
public ClientHandler(Socket socket){
this.socket = socket;
}
public void run(){
try {
//1解析请求
//1.1:读取请求行
String line = readLine();
System.out.println("请求行:"+line);
//将请求行内容按照空格拆分为三部分,并赋值给下面三个变量
String[]data = line.split("\s");
String method = data[0];//请求方式
String uri = data[1];//抽象路径
String protocol = data[2];//协议版本
System.out.println("method:"+method);
System.out.println("uri:"+uri);
System.out.println("protocol:"+protocol);
//1.2解析消息头
Map<String,String> headers = new HashMap<>();
while(true) {
line = readLine();
if(line.isEmpty()){//若读取到了空行,则说明消息头部分读取完毕
break;
}
System.out.println("消息头:" + line);
//每个消息头都按照": "(冒号空格)拆分为消息头的名字和值并以key,value存入headers
data = line.split(":\s");
/*
将消息头存入Map时,消息头的名字转换为全小写,便于后期获取消息头
(获取时不用在考虑大小写问题)
*/
headers.put(data[0].toLowerCase(),data[1]);
}
System.out.println("所有消息头:"+headers);
//2处理请求
//3发送响应
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
//一问一答后断开TCP连接
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 读取客户端发送过来的一行字符串
* @return
*/
private String readLine() throws IOException {//被重用的代码对应的方法通常不自己处理异常
InputStream in = socket.getInputStream();
StringBuilder builder = new StringBuilder();//保存拼接的一行内容
char cur='a',pre='a';//cur记录本次读取的字符,pre记录上次读取的字符
int d;//每次读取的字节
while((d = in.read())!=-1){
cur = (char)d;//将本次读取到的字节转换为char记录在cur上。
if(pre==13 && cur==10){//如果上次读取的为回车符,本次读取的是换行符
break;//停止读取(一行结束了)
}
builder.append(cur);//将本次读取的字符拼接到已经读取的字符串中
pre = cur;//在下次读取前将本次读取的字符记作"上次读取的字符"
}
return builder.toString().trim();
}
}
V3结束
v4部分
- 将clientHanlder中的解析请求的方法细节放入HttpServletRequest类中,并将每一步解析封装在独立方法中,并在HrttpServletRequeset构造方法中调用,最后在clientHandler中将HrttpServletRequeset实例化出来,自动调用构造方法。
package com.webserver.http;
import java.io.IOException;
import java.io.InputStream;
import java.net.Socket;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
/**
* 请求对象
* 该类的每一个实例用于表示浏览器发送过来的一个HTTP请求
* 一个请求由三部分构成的:
* 1:请求行
* 2:消息头
* 3:消息正文
*/
public class HttpServletRequest {
private Socket socket;
//请求行的相关信息
private String method; //请求方式
private String uri; //抽象路径
private String protocol; //协议版本
//消息头的相关信息
private Map<String, String> headers = new HashMap<>(); //key:消息头名字 value:对应的值
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()) {
break;
}
System.out.println("消息头:" + line);
String[] data = line.split(":\s");
headers.put(data[0].toLowerCase(), data[1]);
}
System.out.println("所有消息头:" + headers);
}
//解析消息正文
private void parseContent(){
}
/**
* 读取客户端发送过来的一行字符串
*
* @return
*/
private String readLine() throws IOException {//被重用的代码对应的方法通常不自己处理异常
InputStream in = socket.getInputStream();
StringBuilder builder = new StringBuilder();//保存拼接的一行内容
char cur = 'a', pre = 'a';//cur记录本次读取的字符,pre记录上次读取的字符
int d;//每次读取的字节
while ((d = in.read()) != -1) {
cur = (char) d;//将本次读取到的字节转换为char记录在cur上。
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) {
/*
因为headers中消息头名字以全小写形式保存,
所以要将获取的消息头名字也转换为全小写再提取
这样的好处就是不要求外界获取消息头时指定的名字必须区分大小写了
*/
name = name.toLowerCase();
return headers.get(name);
}
}
v4结束
V5部分
- ClientHandler run方法中写出根据解析请求中获得的uri,去找到路径下的相应文件。
- 写出println方法,作用于发出字符串后,自动换行回车。
- 设置发送响应部分:3.1发送状态行、3.2发送响应头、3.3发送相应正文
package com.webserver.core;
import com.webserver.http.HttpServletRequest;
import java.io.*;
import java.net.Socket;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
/**
* 该线程任务负责与指定的客户端进行HTTP交互
* 按照HTTP协议的交互规则,与浏览器采取一问一答。
* 规划三部:
* 1:解析请求(生成HttpServletRequest对象)
* 2:处理请求(交给SpringMVC框架处理)
* 3:发送响应(将HttpServletResponse内容发送给浏览器)
* 断开TCP连接
*
*/
public class ClientHandler implements Runnable{
private Socket socket;
public ClientHandler(Socket socket){
this.socket = socket;
}
public void run(){
try {
//1解析请求
HttpServletRequest request = new HttpServletRequest(socket);
//2处理请求
/*
后期开发中常用的相对路径:类加载路径(保存这所有当前项目中class文件和包的根目录)
在MAVEN项目中将我们的项目编译后就会把src/main/java中的内容全部编译并且
和src/main/resources下的内容合并存放在target/classes中。
因此target/classes目录就是类加载路径。
定位类加载路径,固定写法:
任何当前项目中的类的类名(通常在哪个类中需要访问类加载路径就用哪个类)
类名.class.getClassLoader().getResource(".")
*/
//类加载路径:target/classes
File root = new File(
ClientHandler.class.getClassLoader().getResource(".").toURI()
);
//定位target/classes/static目录(SpringBoot中存放所有静态资源的目录)
File staticDir = new File(root,"static");
//定位target/classes/static目录中的"index.html"
// File file = new File(staticDir,"index.html");
/*
index.html
classtable.html
static目录
v
http://localhost:8080/index.html
http://localhost:8080/classtable.html
*/
String path = request.getUri();
File file = new File(staticDir,path);
//3发送响应
/*
HTTP/1.1 200 OK(CRLF)
Content-Type: text/html(CRLF)
Content-Length: 2546(CRLF)
(CRLF)
1011101010101010101......
*/
//3.1发送状态行
println("HTTP/1.1 200 OK");
//3.2发送响应头
println("Content-Type: text/html");
println("Content-Length: "+file.length());
println("");//单独发送空行(回车+换行)表示响应头部分发送完毕
//3.3发送响应正文(file表示的文件内容)
OutputStream out = socket.getOutputStream();
FileInputStream fis = new FileInputStream(file);
// FileOutputStream fos = new FileOutputStream("xxx.xx");
int len;//每次读取的字节量
byte[] buf = new byte[1024*10];//10kb
while((len = fis.read(buf))!=-1){
// fos.write(buf,0,len);
out.write(buf,0,len);
}
} catch (IOException e) {
e.printStackTrace();
} catch (URISyntaxException e) {
e.printStackTrace();
} finally {
try {
//一问一答后断开TCP连接
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
private void println(String line) throws IOException {
OutputStream out = socket.getOutputStream();
byte[] data = line.getBytes(StandardCharsets.ISO_8859_1);
out.write(data);
out.write(13);//发送回车符
out.write(10);//发送换行符
}
}
V5部分结束