使用HTTP超文本传输协议,了解浏览器与服务器的具体通信流畅,手写TomCat服务器
目录
一、HTTP协议规则
要求浏览器与服务端之间必须遵循一问一答的规则,即:浏览器与服务端建立TCP连接后需要
先发送一个请求(问)然后服务端接收到请求并予以处理后再发送响应(答)。注意,服务端永远
不会主动给浏览器发送信息
二、Request与Response
1.Request请求
请求是浏览器发送给服务端的内容,HTTP协议中一个请求由三部分构成:
分别是:请求行,消息头,消息正文。消息正文部分可以没有。
1).请求行
请求行是一行字符串,以连续的两个字符(回车符和换行符)作为结束这一行的标志。
回车符(不可见字符):在ASC编码中2进制内容对应的整数是13.回车符通常用cr表示。
换行符(不可见字符):在ASC编码中2进制内容对应的整数是10.换行符通常用lf表示。
请求行组成: 请求方式(SP)抽象路径(SP)协议版本(CRLF) 注:SP是空格
GET /myweb/index.html HTTP/1.1
GET / HTTP/1.1
URL地址格式:
协议://主机地址信息/抽象路径
http://localhost:8088/TeduStore/index.html
GET /TeduStore/index.html HTTP/1.1
2).消息头
消息头是浏览器可以给服务端发送的一些附加信息
消息头组成:
消息头由若干行组成,每行结束也是以CRLF标志。
每个消息头的格式为:消息头的名字(:SP)消息的值(CRLF)
消息头部分结束是以单独的(CRLF)标志。
Host: localhost:8088(CRLF)
Connection: keep-alive(CRLF)
Upgrade-Insecure-Requests: 1(CRLF)
3).消息正文
消息正文是2进制数据,通常是用户上传的信息,比如:在页面输入的注册信息,上传的
附件等内容。
GET /index.html HTTP/1.1
Host: localhost:8088
Connection: keep-alive
Upgrade-Insecure-Requests: 1
2.Response响应
响应是服务端发送给客户端的内容。一个响应包含三部分:状态行,响应头,响应正文
1).状态行
状态行是一行字符串(CRLF结尾),并且状态行由三部分组成,格式为:
protocol(SP)statusCode(SP)statusReason(CRLF)
协议版本(SP)状态代码(SP)状态描述(CRLF)
HTTP/1.1 200 OK
状态代码是一个3位数字,分为5类:
1xx:保留
2xx:成功,表示处理成功,并正常响应
3xx:重定向,表示处理成功,但是需要浏览器进一步请求
4xx:客户端错误,表示客户端请求错误导致服务端无法处理
5xx:服务端错误,表示服务端处理请求过程出现了错误
2).响应头
响应头与请求中的消息头格式一致,表示的是服务端发送给客户端的附加信息。
3).响应正文
Content-Type是用来告知浏览器响应正文中的内容是什么类型的数据(图片,页面等等)
Content-Length是用来告知浏览器响应正文的长度,单位是字节。
HTTP/1.1 404 NotFound(CRLF)
Content-Type: text/html(CRLF)
Content-Length: 2546(CRLF)(CRLF)
1011101010101010101......
三.代码业务实现
本项目的包结构
Controller接口: 实现SpringMvc中的@Controller注解
RequestMapping接口: 实现SpringMvc中@RequestMapping注解
controller包: 测试程序的业务包
ClientHandler: 多线程分配处理每一个连接信息
DispatcherServlet: SpringMvc的核心类,处理请求的环节
HandlerMapping: 反射机制扫描请求路径对应的方法(相应的注解的参数)
WebServerApplication: maven项目的启动类
User: 测试的用户实体类
EmptyRequestException:处理空请求的异常类
HttpServletRequest: 处理请求,解析请求参数
HttpServletResponse: 发行响应的类
static包: 测试使用的html静态文件
1.启动类
目的:在maven环境下的启动类上获取网络连接,创建线程处理连接
在构造方法上,项目启动时创建serverSocket ,创建线程连接池
public WebServerApplication(){
try {
System.out.println("正在启动服务端...");
System.out.println("\n" +
" . ____ _ __ _ _\n" +
" /\\\\ / ___'_ __ _ _(_)_ __ __ _ \\ \\ \\ \\\n" +
"( ( )\\___ | '_ | '_| | '_ \\/ _` | \\ \\ \\ \\\n" +
" \\\\/ ___)| |_)| | | | | || (_| | ) ) ) )\n" +
" ' |____| .__|_| |_|_| |_\\__, | / / / /\n" +
" =========|_|==============|___/=/_/_/_/\n" +
" :: Spring Boot :: (v2.7.2)\n" +
"\n");
serverSocket = new ServerSocket(8088);
threadPool = Executors.newFixedThreadPool(50); //创建一个大小为50的线程池
System.out.println("服务器启动完毕...");
} catch (IOException exception) {
exception.printStackTrace();
}
}
通过start方法获取浏览器的连接,并调用封装的ClientHandler分配当前连接的线程
public void start(){
try {
while (true){
System.out.println("等待客户端连接...");
Socket socket = serverSocket.accept();
System.out.println("客户端连接了...");
//启动一个线程处理该客户的交互
ClientHandler handler = new ClientHandler(socket);
threadPool.execute(handler); //执行线程体
}
} catch (IOException exception) {
exception.printStackTrace();
}
}
2.线程类(ClientHandler)
与客户端完成一次HTTP的交互
按照HTTP协议要求,与客户端完成一次交互流程为一问一答
因此,这里分为三步完成该工作:
1:解析请求 目的:将浏览器发送的请求内容读取并整理
2:处理请求 目的:根据浏览器的请求进行对应的处理工作
3:发送响应 目的:将服务端的处理结果回馈给浏览器
public void run() {
try {
//1解析请求,实例化请求对象的过程就是解析的过程
HttpServletRequest request = new HttpServletRequest(socket);
HttpServletResponse response = new HttpServletResponse(socket);
//2处理请求
DispatcherServlet.getService().service(request,response);
//3发送响应
response.response();
} catch (IOException e) {
e.printStackTrace();
} catch (EmptyRequestException e) {
//不需要进行处理,报错的代码下面的代码都不会执行
} finally {
try {
//按照HTTP协议要求,一问一答后断开连接
socket.close();
} catch (IOException exception) {
exception.printStackTrace();
}
}
}
3.HttpServletRequest解析请求类
请求包括三部分:
private String method; //请求方式
private String uri; //抽象路径
private String protocol;//协议版本
在构造方法中调用处理这三部分的方法:
public HttpServletRequest(Socket socket) throws IOException, EmptyRequestException {
this.socket = socket;
//1解析请求行
parseRequestLine();
//2解析消息头
parseHeaders();
//3解析消息正文
parseContent();
}
1).解析请求行
思路: 1.每次读取一行请求的数据(以回车加换行结尾CRLF)
2.判断读到的数据是否为空 --> 是-->抛出空请求异常
3.GET /index.html HTTP/1.1 将读到的字符按照空格(SP)拆分 请求方式method . 抽象路径uri . 协议版本protect
4.抽象路径部分 : /index.html 静态页面
/reg?name=XXX&password=XXX 业务
5.url是业务 按照 ? 拆分-->?前半部分为请求业务名requestURI , ?后半部分为请求业务的参数queryString 参数部分按照&拆分 保存在parameters的Map集合中
自定义方法: 每次读取一行数据(每一行都是以回车加换行结尾)
每次读取一行的方法readLine
InputStream in = socket.getInputStream();
int d;
StringBuilder builder = new StringBuilder();
char pre = 'a',cur = 'a';//pre记录上次读到的字符 cur记录本次读取到的字符
while ((d=in.read())!=-1){//read()返回值是读到的一个字节的二进制低八位
//readline()方法会块读8000个char[],会把消息正文也读进去
cur = (char) d;
if (pre==13&&cur==10){//判断是否连续读到回车+换行
break;
}
builder.append(cur);
pre = cur;
}
return builder.toString().trim();
1>. 解析请求的数据
得到请求方式 , 抽象路径 , 协议版本
private void parseRequestLine() throws IOException, EmptyRequestException {
String line = readLine();
if (line.isEmpty()){//若请求行是空字符串,则说明本次是空请求
throw new EmptyRequestException("request is empty");
}
String[] data = line.split("\\s");
method = data[0];
uri = data[1];
protocol = data[2];
//进一步解析URI
parseURI();
}
2>. 解析URI(parseURI方法)
判断是业务还是具体的静态页面
String[] data = uri.split("\\?");
requestURI = data[0];
if(data.length>1){//有参数
//queryString:username=fancq&password=&nickname=chuanqi&age=22
queryString = data[1];
//paras:[username=fancq, password=, nickname=chuanqi, age=22]
parseParameter(queryString);
3>. 解析URI中的参数部分
分离参数名和值
private void parseParameter(String line){
//浏览器将汉字通过utf-8转化为2进制-->16进制,支持ISO_8859_1编码从而支持请求行
//获取拆分参数是在通过utf-8将16进制转换为汉字
try {
line = URLDecoder.decode(line,"UTF-8");
//URLDecoder是JAVA提供的一个API,可以将URL地址中%XX的内容进行解码,汉字在
//请求行中以十六进制的方式显示
//"/loginUser?username=%E8%B4%BA%E5%9D%A4&password=123 HTTP/1.1"
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
String[] parses = line.split("&");//parse解析
for(String parse : parses){
//array:[username,fancq] 若没参数值array:[password]
String[] array = parse.split("=");
parameters.put(array[0],array.length>1?array[1]:"");
}
}
2).解析消息头
思路: 1.消息头由若干行组成,每行结束也是以CRLF标志。
每个消息头的格式为:消息头的名字(:SP)消息的值(CRLF)
消息头部分结束是以单独的(CRLF)标志。
2.把每次读到的数据按照 ''冒号空格(':\\s')''拆分 并放到headers的Map集合中
3.循环结束的条件是:读到的数据书否为空串
例如: Accept-Language: zh-CN,zh;q=0.9(CRLF)(CRLF)
private void parseHeaders() throws IOException{
String line;
while (!(line= readLine()).isEmpty()){
System.out.println("消息头:"+line);
String[] data = line.split(":\\s");
headers.put(data[0],data[1]);
}
}
3).解析消息正文
思路: 1. 判断请求类型 : get请求 参数在请求路径url中
post请求业务的参数在 正文部分
2. post请求需要从消息头map集合中获取 正文长度 Content-Length , 再次读取此长度的正文数据得到参数
3.从消息头map集合中获取正文类型 Content-Type 判断数据提交的类型
private void parseContent() throws IOException{
//判断请求方式是否为POST请求
if("post".equalsIgnoreCase(method)){
int contentLength = 0;
if (headers.containsKey("Content-Length")){
contentLength = Integer.parseInt(headers.get("Content-Length"));
System.out.println("正文长度:"+contentLength);
}
//读取正文数据
InputStream in = socket.getInputStream();
byte[] data = new byte[contentLength];
in.read(data);
/*
* 根据Content-Type来分析正文是什么以便进行对应的处理
* */
String contentType = headers.get("Content-Type");
if ("application/x-www-form-urlencoded".equals(contentType)){//是否为form表单
String line = new String(data,StandardCharsets.ISO_8859_1);
parseParameter(line);
}
}
4.HttpServletResponse响应类
响应包括三部分
状态行 : 协议版本 + 状态码statusCode + 状态描述statusReason
响应头
响应正文
在处理类中会调用相应的set/get方法,为各个变量赋值
自定义方法: println自定义响应方法(每次相应一条数据)
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);
}
自定义方法: 发送响应前判断是否响应动态数据还是静态页面
//动态数据可以通过该流写出到其内部维护的字节数组中,发送响应是将该数组内容作为正文
private ByteArrayOutputStream out;
private void sendBefore(){
if (out!=null){//说明有动态数据
addHeader("Content-Length",out.size()+"");
}
}
1).发送状态行
在处理类中 设置状态行的相关参数发送
private void sendStatusLine() throws IOException{
//HTTP/1.1 200 OK
println("HTTP/1.1"+" "+statusCode+" "+statusReason);
}
2).发送响应头
思路: 从响应头的map集合中获取相关的key-value值
private void sendHeaders() throws IOException{
Set<Map.Entry<String,String>> entrySet = headers.entrySet();
for (Map.Entry<String, String> e : entrySet) {//每个Set集合的元素都是一个entry对象
String name = e.getKey();
String value = e.getValue();
println(name+": "+value);
}
//单独发送个回车+换行表示响应头发送完毕
println("");
}
在setContentFile方法中(在处理类调用并赋值)
public void setContentFile(File contentFile) { this.contentFile = contentFile; //添加用于说明正文的响应头Content-Type和Content-Length; try { String contentType = Files.probeContentType(contentFile.toPath()); if (contentType!=null){ addHeader("Content-Type",contentType); }//如果不发送contentType头,浏览器会自己去猜,能发尽量发 } catch (IOException exception) { exception.printStackTrace(); } addHeader("Content-Length",contentFile.length()+""); }
3).发送正文
思路: 1. 在处理类中如果要发送的是动态数据(动态的html页面) 则从 ByteArrayOutputStream数组中获取数据
2.如果是静态页面, 使用流块读数据响应
private void sendContent() throws IOException{
OutputStream out = socket.getOutputStream();
if (this.out!=null){
byte[] data = this.out.toByteArray();
out.write(data);//将动态数据作为正文发送给浏览器
}else if (contentFile!=null){//静态文件
FileInputStream fis = new FileInputStream(contentFile);
byte[] buf = new byte[1024*10];//10kb
int len = 0;//记录每次实际读取的字节数
while ((len = fis.read(buf)) != -1){
out.write(buf,0,len);
}
}
//如果为空,响应也可以没有页面
}
5.DispatcherServlet处理类
用于完成一个http交互流程中处理请求的环节工作
实际上这个类是Spring MVC 框架提供的一个核心类,用于和Web容器(Tomcat)整合,
使得处理请求的环节可以有Spring MVC框架完成
处理类只能有一个, 不能多个同时多个处理对象,因此设置成单例模式
private DispatcherServlet(){}
private static DispatcherServlet service = new DispatcherServlet();
public static DispatcherServlet getService(){
return service;
}
1).要查看是否为请求业务:
根据注解的参数调用HandlerMapping中的方法
Method method = HandlerMapping.getMethod(path);//传入抽象路径RequestUri 获取方法
if (method!=null){//说明是一个业务
/**
* //通过方法对象可以获取其所属类的类对象
* Class cls = method.getDeclaringClass();
* Object obj = cls.newInstance();
*/
method.invoke(method.getDeclaringClass().newInstance(),
request,response);//invoke方法需要传入当前方法的类对象和参数名
return;
}
2).非处理业务,那么执行下面请求静态资源的操作
不是页面-->发送静态404页面
File file = new File(staticDir,path);
if (file.isFile()){ //浏览器请求的资源是否存在且是一个文件
//正确响应 "HTTP/1.1 200 OK";
response.setContentFile(file);
}else { //不是页面就404
//响应 "HTTP/1.1 404 NotFound";
response.setStatusCode(404);
response.setStatusReason("NotFound");
file = new File(staticDir,"/root/404.html");
response.setContentFile(file);
}
6.HandlerMapping类
反射机制 ---维护请求路径对应的业务处理方法
获取/com/webserver/controller包下的所有.class文件
获取被@Controller修饰的类
获取被@RequestMapping修饰的方法
将参和方法名放在map中
File dir = new File(
HandlerMapping.class.getClassLoader().getResource(".").toURI()
);
File controllerDir = new File(dir,"/com/webserver/controller");
File[] subs = controllerDir.listFiles(f->f.getName().endsWith(".class"));
for (File sub : subs) {
String name = sub.getName().substring(0,sub.getName().lastIndexOf("."));
Class cls = Class.forName("com.webserver.controller."+name);
if (cls.isAnnotationPresent(Controller.class)){
Method[] methods = cls.getMethods();
for (Method method : methods) {
if (method.isAnnotationPresent(RequestMapping.class)){
RequestMapping requestMapping = method.getAnnotation(RequestMapping.class);
String value = requestMapping.value();
//value: 注解的参数/regUser
//mmethod: 注解对应的方法reg()
mapping.put(value,method);
}
}
}
}
在DispatcherServlet类中 调用method.invoke(类对象,参数名)执行方法体
7.annotations自定义注解
通过注解,获取controller各个业务的参数 , 从而可以调用指定的方法执行业务
Controller类-->扫描启动类下的特定类
@Target(ElementType.TYPE)//修饰类
@Retention(RetentionPolicy.RUNTIME)//保留级别
public @interface Controller {
}
RequestMapping类-->扫描指定的方法
@Target(ElementType.METHOD)//修饰方法
@Retention(RetentionPolicy.RUNTIME)//保留级别
public @interface RequestMapping {
String value();//定义参数
}