记录一下手写webserver的学习过程
整体的目录结构
一、通过ServerSocket建立服务器与浏览器之间的连接
创建包com.webserver.core
创建类Server,Dispatcher
Server类
/**
* @Author rainc
* @create 2019/10/5 17:03
*/
public class Server {
private ServerSocket serverSocket;
//判断服务器是否已启动
private boolean isRunning;
//线程池
private ExecutorService threadPool;
public static void main(String[] args) {
Server server = new Server();
//启动服务
server.start();
}
/**
* 启动服务
*/
public void start() {
try {
//初始化ServerSocket对象并配置端口
serverSocket = new ServerSocket(8888);
//初始化线程池
threadPool = new ThreadPoolExecutor(10, 200, 0, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(512), // 使用有界队列,避免OOM
new ThreadPoolExecutor.DiscardPolicy());
isRunning = true;
//接收连接进行处理
receive();
} catch (IOException e) {
e.printStackTrace();
System.out.println("服务器启动失败");
//若启动异常停止服务器
stop();
}
}
/**
* 接受连接处理
*/
public void receive() {
//循环处理使得服务器能一直运行
while (isRunning) {
try {
//通过accept方法接受建立新的连接并获取到Socket对象
Socket client = serverSocket.accept();
System.out.println("一个客户端建立了连接");
//多线程处理每当有新连接就初始化一个Dispatcher对象并将Socket传入Dispatcher对象中进行详细的处理
threadPool.execute(new Dispatcher(client));
} catch (IOException e) {
e.printStackTrace();
System.out.println("客户端错误");
}
}
}
/**
* 停止服务
*/
public void stop() {
isRunning = false;
try {
this.serverSocket.close();
System.out.println("服务器已停止");
} catch (IOException e) {
e.printStackTrace();
}
}
}
分发器
/**
* @Author rainc
* @create 2019/10/5 17:11
* 分发器,用于详细处理请求并响应
*/
public class Dispatcher implements Runnable {
private Socket client;
public Dispatcher(Socket client) {
this.client = client;
}
public void run() {
try {
//获取输入流
BufferedReader br = new BufferedReader(new InputStreamReader(client.getInputStream()));
String temp;
//判断当下一行长度为零时停止读取
while ((temp = br.readLine()).length() != 0) {
System.out.println(temp);
}
} catch (IOException e) {
e.printStackTrace();
}
//结束时释放Socket
relesase();
}
/**
* 释放Socket
*/
private void relesase() {
try {
client.close();
} catch (IOException ex) {
ex.printStackTrace();
}
}
}
启动Server的main方法,打开浏览器输入http://localhost:8888/login?uname=rainc,可以看到捕捉到的请求
二、对得到的http协议请求进行解析
创建request类进行请求处理
/**
* @Author rainc
* @create 2019/10/6 11:55
*/
public class Request {
//请求方式
private String method;
//请求url
private String url;
//存储参数可能存在一键多值因此值使用了list
private Map<String, List<String>> parameterMap;
private BufferedReader br;
public Request(Socket client) {
try {
//初始化输入流
this.br = new BufferedReader(new InputStreamReader(client.getInputStream()));
//初始化map
parameterMap = new HashMap<>();
//解析请求
parseRequest();
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 解析请求行
*/
private void parseRequest() {
try {
//读取第一行内容 GET /login?uname=a HTTP/1.1
String line = br.readLine();
//如果为空则停止接下来的步骤
if (line==null)
return;
//通过空格拆分为三部分取得请求方式和url
String[] temp = line.split(" ");
method = temp[0].trim();
//通过问号分析是否有传递参数
if (temp[1].contains("?")) {
//通过分割问号得到url和参数
temp = temp[1].split("\\?");
url = temp[0].trim();
convertMap(temp[1].trim());
} else {
url = temp[1].trim();
}
System.out.println("method:" + method);
System.out.println("url:" + url);
//如果请求方式为post则读取post请求内容
if (method.equals("POST"))
parsePostParams();
System.out.println(parameterMap);
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 解析post参数
*/
private void parsePostParams() {
String line;
int len = 0;
try {
//请求体与post参数中间有一个空行,长度会被解析成0,这个循环会正好在读取参数前结束
while ((line = br.readLine()).length() != 0) {
//若为post方式则请求中会有Content-Length行,通过Content-Length可以得到参数长度并读取
if (line.contains("Content-Length")) {
//对Content-Length行进行拆分得到长度
String[] temp = line.split(":");
len = Integer.parseInt(temp[1].trim());
}
}
char[] buffers = new char[len];
br.read(buffers);
convertMap(new String(buffers));
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 将参数分割存入map
*/
private void convertMap(String params) {
//1、分割字符串&
String[] keyValues = params.split("&");
for (String temp : keyValues) {
//2、再次分割字符串 =
String[] kv = temp.split("=");
kv = Arrays.copyOf(kv, 2);
//获取key和value
String key = kv[0];
//存入时对内容进行中文处理防止中文乱码
String value = kv[1] == null ? null : decode(kv[1], "utf-8");
//存储到map中如果没有该键则创建该键对应的list
if (!parameterMap.containsKey(key)) {
parameterMap.put(key, new ArrayList<String>());
}
//如果有则直接存入
parameterMap.get(key).add(value);
}
}
/**
* 处理中文乱码
*/
private String decode(String value, String enc) {
try {
return java.net.URLDecoder.decode(value, enc);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
return null;
}
}
/**
* 对外提供get方法获取
*
* @return
*/
public String getMethod() {
return method;
}
public String getUrl() {
return url;
}
/**
* 通过key获取对应的多个值
*
* @param key
* @return
*/
public String[] getParameterValues(String key) {
List<String> values = this.parameterMap.get(key);
if (null == values || values.size() < 1) {
return null;
}
return values.toArray(new String[0]);
}
/**
* 通过key获取对应的一个值
*
* @param key
* @return
*/
public String getParameter(String key) {
//调用getParameterValues,并获取第一个值
String[] values = getParameterValues(key);
return values == null ? "" : values[0];
}
修改Dispatcher类的构造器,和run方法
public Dispatcher(Socket client) {
this.client = client;
//初始化请求处理
request=new Request(client);
}
public void run() {
//结束时释放Socket
relesase();
}
运行服务器,通过http测试插件(我这里用的是RESTer)模拟post请求
可以看到内容成功获取到了
三、对请求进行响应
创建Response类进行响应处理
/**
* 响应处理
*
* @Author rainc
* @create 2019/10/6 15:15
*/
public class Response {
BufferedWriter bw;
//正文
private StringBuilder content;
//协议头(状态行与请求头 回车)信息
private StringBuilder responseInfo;
//协议头返回的
private int len = 0;
//通过BLACK来表示一个空格
private final String BLACK = " ";
//通过CRLF来表示回车加换行
private final String CRLF = "\r\n";
private Response() {
content = new StringBuilder();
responseInfo = new StringBuilder();
}
public Response(Socket client) {
//回调无参构造器进行初始化
this();
try {
bw = new BufferedWriter(new OutputStreamWriter(client.getOutputStream()));
} catch (IOException e) {
responseInfo = null;
}
}
//动态添加内容
public Response print(String info) {
content.append(info);
len += info.getBytes().length;
return this;
}
//动态添加内容并且换行
public Response println(String info) {
content.append(info).append(CRLF);
len += (info + CRLF).getBytes().length;
return this;
}
//推送响应信息
public void pushToBrower(int code) throws IOException {
//如果错误则返回505错误
if (responseInfo == null) {
code = 505;
}
//构建头信息
createResponseInfo(code);
//放入头信息
bw.append(responseInfo);
System.out.println(responseInfo);
//放入正文信息
bw.append(content);
//刷新输出流
bw.flush();
}
//构建头信息
private void createResponseInfo(int code) {
//1.响应行:HTTP/1.1 200 OK 响应的状态码 200表示正常应答
responseInfo.append("HTTP/1.1").append(BLACK);
responseInfo.append(code).append(BLACK);
switch (code) {
case 200:
responseInfo.append("OK").append(CRLF);
break;
case 404:
responseInfo.append("NOT FOUND").append(CRLF);
break;
case 505:
responseInfo.append("SERVER ERROR").append(CRLF);
}
//2、响应头(最后一行存在空行)
//Date: 生成消息的具体时间和日期
responseInfo.append("Date:").append(new Date()).append(CRLF);
//Server:apache 生成服务器的名称
responseInfo.append("Server:").append("apache").append(CRLF);
//Content-Type: text/html;charset=utf-8 http服务器告诉浏览器自己响应的对象类型和字符集(并且告诉客户端实际返回的内容的内容类型)
responseInfo.append("Content-Type:").append("text/html;charset=utf-8").append(CRLF);
//Content-Length: http服务器的响应实体正文的长度
responseInfo.append("Content-length:").append(len).append(CRLF);//必须过去字节长度
//以回车换行标记结束
responseInfo.append(CRLF);
}
}
修改Dispatcher类的构造器,和run方法
public Dispatcher(Socket client) {
this.client = client;
//初始化请求处理
request = new Request(client);
//初始化响应处理
response = new Response(client);
}
public void run() {
try {
//写入内容
response.print("你好"+request.getParameter("uname")/*通过键获取参数值*/);
//推送
response.pushToBrower(200);
} catch (IOException e) {
e.printStackTrace();
}
//结束时释放Socket
relesase();
}
运行服务器,在网页上输入http://localhost:8888/?uname=rainc,可以看到网页上显示了我们想要输出的内容
四、建立Servlet接口标准,创建Servlet的实例进行业务处理
创建接口Servlet
/**
* servlet接口,统一servlet类标准
* @Author rainc
* @create 2019/10/6 17:02
*/
public interface Servlet {
void service(Request request, Response response);
}
创建IndexServlet类并引入Servlet接口
/**
* @Author rainc
* @create 2019/10/6 17:07
*/
public class IndexServlet implements Servlet {
@Override
public void service(Request request, Response response) {
//将之前Dispatcher中的业务代码转移到了这里
response.print("你好" + request.getParameter("uname")/*通过键获取参数值*/);
}
}
修改Dispatcher的run方法
public void run() {
//推送
try {
//通过IndexServlet实现Servlet接口
Servlet servlet = new IndexServlet();
//调用service方法进行业务处理
servlet.service(request, response);
response.pushToBrower(200);
} catch (IOException e) {
e.printStackTrace();
}
//结束时释放Socket
relesase();
}
运行服务器,再次在网页上输入http://localhost:8888/?uname=rainc,成功的话结果和上面那次相同。
五、创建web.xml,和WebApp通过反射来动态生成servlet
由于web服务是动态的,因此通过new的方法来静态的新建对象是不可取的,因此需要通过反射来进行动态生成servlet
大致过程:
- 通过request解析得到url
- 解析web.xml
- 通过得到的url在servlet-mapping中找到对应的servlet-name
- 通过servlet-mapping中的servlet-name找到servlet中对应的servlet-class
- 使用servlet-class通过反射动态生成对应的servlet对象
创建web.xml通过web.xml来管理servlet
<?xml version="1.0" encoding="UTF-8"?>
<web-app>
<!--真实路径-->
<servlet>
<servlet-name>index</servlet-name>
<servlet-class>com.webserver.servlet.IndexServlet</servlet-class>
</servlet>
<!--路径映射-->
<servlet-mapping>
<servlet-name>index</servlet-name>
<url-pattern>/index</url-pattern>
</servlet-mapping>
</web-app>
创建Entity类对应真实路径
package com.webserver.core;
/**
* <servlet>
* <servlet-name>index</servlet-name>
* <servlet-class>com.webserver.servlet.IndexServlet</servlet-class>
* </servlet>
* @Author rainc
* @create 2019/10/6 17:53
*/
public class Entity {
private String name;
private String clz;
public Entity() {
}
public Entity(String name, String clz) {
this.name = name;
this.clz = clz;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getClz() {
return clz;
}
public void setClz(String clz) {
this.clz = clz;
}
@Override
public String toString() {
return "Entity{" +
"name='" + name + '\'' +
", clz='" + clz + '\'' +
'}';
}
}
创建Mapping对应映射路径
package com.webserver.core;
import java.util.HashSet;
import java.util.Set;
/**
* <servlet-mapping>
* <servlet-name>index</servlet-name>
* <url-pattern>/index</url-pattern>
* </servlet-mapping>
* @Author rainc
* @create 2019/10/6 17:56
*/
public class Mapping {
private String name;
private Set<String> patterns;
public Mapping() {
patterns = new HashSet<>();
}
public Mapping(String name, Set<String> pattens) {
this.name = name;
this.patterns = pattens;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Set<String> getPattens() {
return patterns;
}
public void setPattens(Set<String> pattens) {
this.patterns = pattens;
}
public void addPattern(String pattern) {
this.patterns.add(pattern);
}
}
创建Webapp对象
package com.webserver.core;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import java.io.File;
/**
* @Author rainc
* @create 2019/10/6 17:51
*/
public class WebApp {
private static WebContext context;
static {
try {
//1、获取解析工厂
SAXParserFactory factory = SAXParserFactory.newInstance();
//2、从解析工厂获取解析器
SAXParser parse = factory.newSAXParser();
//3、加载文档 Document 注册处理器
//4、编写处理器
WebHandler handler = new WebHandler();
//5、解析数据
parse.parse(Thread.currentThread().getContextClassLoader().getResourceAsStream("web.xml"), handler);
//将数据存入context中进行统一管理
context = new WebContext(handler.getEntities(), handler.getMappings());
} catch (Exception e) {
System.out.println("解析配置文件错误");
}
}
//通过url获取对应的servlet
public static Servlet getServletFromUrl(String url) {
//通过url获取class对应的路径名
String className = context.getClz(url);
if (className == null)
return null;
try {
//通过路径名反射生成相应的Servlet
Class clz = Class.forName(className);
Servlet servlet = (Servlet) clz.getConstructor().newInstance();
return servlet;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}
创建 WebHandler类编写处理器对web.xml进行处理
package com.webserver.core;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;
import java.util.ArrayList;
import java.util.List;
/**
* web.xml处理器
* @Author rainc
* @create 2019/10/6 18:01
*/
class WebHandler extends DefaultHandler {
//创建容器保存全部entity和mapping
private List<Entity> entities;
private List<Mapping> mappings;
private Entity entity;
private Mapping mapping;
//保存xml的标记
private String tag;
//用来判断是映射还是实体
private boolean isMapping = false;
//对外提供get方法
public List<Entity> getEntities() {
return entities;
}
public List<Mapping> getMappings() {
return mappings;
}
//开始解析
@Override
public void startDocument() {
entities = new ArrayList<>();
mappings = new ArrayList<>();
}
//开始接收某个元素
@Override
public void startElement(String uri, String localName, String qName, Attributes attributes) {
tag = qName;
//通过标记来判断是映射还是实体并生成相应的对象
switch (tag) {
case "servlet":
entity = new Entity();
isMapping = false;
break;
case "servlet-mapping":
mapping = new Mapping();
isMapping = true;
default:
break;
}
}
//解析元素中内容
@Override
public void characters(char[] ch, int start, int length) {
String contents = new String(ch, start, length).trim();
//判断是映射还是实体并对相应的对象赋值
if (!isMapping) {
switch (tag) {
case "servlet-name":
entity.setName(contents);
break;
case "servlet-class":
entity.setClz(contents);
}
} else {
switch (tag) {
case "servlet-name":
mapping.setName(contents);
break;
case "url-pattern":
mapping.addPattern(contents);
}
}
}
//结束某个元素接收
@Override
public void endElement(String uri, String localName, String qName) throws SAXException {
//判断实体还是映射保存至容器中
switch (qName) {
case "servlet":
entities.add(entity);
break;
case "servlet-mapping":
mappings.add(mapping);
default:
break;
}
//重置tag
tag = "";
}
//结束解析
@Override
public void endDocument() throws SAXException {
}
}
创建WebContext对解析获得的内容进行统一管理
package com.webserver.core;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Web上下文统一管理
*
* @Author rainc
* @create 2019/10/6 18:00
*/
public class WebContext {
List<Entity> entities;
List<Mapping> mappings;
//key-->servlet-name value-->serblet-class
private Map<String, String> entityMap = new HashMap<>();
//key-->url-pattern value-->servlet-name
private Map<String, String> mappingMap = new HashMap<>();
public WebContext(List<Entity> entities, List<Mapping> mappings) {
this.entities = entities;
this.mappings = mappings;
//将entityList转成了对应的map
for (Entity temp : entities) {
entityMap.put(temp.getName(), temp.getClz());
}
//将mappingsList转成了对应的map
for (Mapping temp : mappings) {
for (String st : temp.getPattens()) {
mappingMap.put(st, temp.getName());
}
}
}
/**
* 通过传入的路径得到对应的名字,再通过名字找到对应的class路径
*
* @param pattern
* @return
*/
public String getClz(String pattern) {
String name = mappingMap.get(pattern);
return entityMap.get(name);
}
}
修改Dispatcher的run方法
public void run() {
//推送
try {
//通过反射动态生成Servlet
Servlet servlet = WebApp.getServletFromUrl(request.getUrl());
//判断servlet是否存在
if (null != servlet) {
//调用service方法进行业务处理
servlet.service(request, response);
response.pushToBrower(200);
} else {
response.print("网页没有");
response.pushToBrower(404);
}
} catch (IOException e) {
try {
response.print("网页炸了");
response.pushToBrower(500);
} catch (IOException ex) {
ex.printStackTrace();
}
e.printStackTrace();
}
//结束时释放Socket
relesase();
}
到这里一个简单的服务器就基本完成了,接下来尝试着使用一下
六、模拟登录
创建login.html
<!DOCTYPE html>
<html lang="ch">
<head>
<meta charset="UTF-8">
<title>login</title>
</head>
<body>
<form method="post" action="http://localhost:8888/index">
用户名:<input type="text" name="uname" id="uname"/>
<br/>
密码:<input type="password" name="pwd" id="pwd"/>
<br/>
<input type="submit" value="登录"/>
</form>
</body>
</html>
创建LoginServlet
package com.webserver.servlet;
import com.webserver.core.Request;
import com.webserver.core.Response;
import com.webserver.core.Servlet;
import java.io.IOException;
import java.io.InputStream;
/**
* @Author rainc
* @create 2019/10/6 19:13
*/
public class LoginServlet implements Servlet {
@Override
public void service(Request request, Response response) {
try {
//读取login.html的内容并使用response输出到浏览器端
InputStream is = Thread.currentThread().getContextClassLoader().getResourceAsStream("login.html");
response.print(new String(is.readAllBytes()));
} catch (IOException e) {
e.printStackTrace();
}
}
}
修改IndexServlet的service方法
public void service(Request request, Response response) {
String uname = request.getParameter("uname");
String pwd = request.getParameter("pwd");
if (uname.equals("rainc") && pwd.equals("123456")) {
response.print("你好" + request.getParameter("uname")/*通过键获取参数值*/);
} else {
response.print("账号或密码错误");
}
}
在web.xml中加入login的配置内容
<servlet>
<servlet-name>login</servlet-name>
<servlet-class>com.webserver.servlet.LoginServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>login</servlet-name>
<url-pattern>/login</url-pattern>
</servlet-mapping>
在浏览器上输入http://localhost:8888/login,进入了下图的登录界面
输入正确的账号密码便会跳转到下图
如果错误则会显示账号或密码错误
参考内容:https://www.bilibili.com/video/av30023103/?p=251
个人博客:https://www.rainc.top/2019/10/05/java-study/webserver-study/
最后附上我的项目做参考
github:https://github.com/amerainc/Server_Study
码云:https://gitee.com/amerainc/Server_Study