手写webserver服务器
前言
webserver 服务器是网络通信必不可少的工具,手写web服务器有助于我们理解网络通信和tomcat这类服务器的执行流程。
手写Web服务器所需要的知识较多,大体包括:容器、io、多线程、反射、HTTP协议、xml解析等等,通过手写服务器能让我们更加熟练的掌握这些知识,同时这也是一个挑战。
一、web server执行流程
我们这个简易版的web服务器大致分为14个流程,具体如下图
是不是感jio特别熟悉,没错这个图我就是仿照Spring MVC执行流程来画的。为了便于理解,其中的一些命名也是和Spring MVC类似,但是请记住这是webserver的执行流程
- 浏览器发送http请求给服务器,服务器通
Server
建立连接,将请求交给Dispatcher
处理 Dispatcher
启动多线程与客户端进行交互,将请求信息交给Request
进行解析,得到请求方法、请求URL、请求参数Dispatcher
根据得到的URL,调用WebApp
的getServletFromUrl
方法反射创建相应的Servlet
类- 执行对应的
Servlet
,Servlet
添加响应信息,Response
将信息交给ResponseReslove
处理得到规范格式的响应的信息 Dispatcher
调用Response
的pushToClient
方法将响应结果响应给用户
组件说明
-
Server
管理与客户端的连接,是整个webserver的入口 -
Dispatcher 控制器
整个流程的控制中心,控制其他组件处理请求 -
Request
解析HTTP请求,获得请求方法、请求URL、请求参数 -
WebApp 处理映射器
通过调用WebHandler
处理解析web.xml文件,WebContext
获得类路径反射创建Servlet对象 -
WebHandler 配置文件处理器
处理解析web.xml文件 -
WebContext
将实体类封装为map -
Entity
web.xml文件中servlet节点实体 -
Mapping
web.xml文件中servlet-mapping节点 -
Servlet
Servlet为抽象类,包含service方法 -
Response
封装响应信息、包括响应头、响应正文、响应行 -
ResponseHandler
解析html文件,将其转化为字符串
项目地址
我已经将源代码上传至 github地址,各位大佬可以给我一个小星星吗
二、代码实现
Server 建立与客户端连接端口号为 8888
ip地址为localhost
,start()启动服务,receive()接收连接处理
public class Server {
//是否启动
private boolean isRunning=true;
ServerSocket serverSocket=null;
public static void main(String[] args) {
Server server = new Server();
server.start();
}
//启动服务
public void start() {
//创建连接
try {
serverSocket = new ServerSocket(8888);
receive();
} catch (IOException e) {
e.printStackTrace();
System.out.println("服务器启动失败...");
}
}
//接收连接处理
public void receive(){
while(isRunning){
try {
Socket client = serverSocket.accept();
System.out.println("一个客户端建立了连接");
new Thread(new Dispatcher(client)).start();
} catch (IOException e) {
e.printStackTrace();
System.out.println("客户端连接失败...");
}
}
}
//停止服务
public void stop(){
isRunning=false;
CloseUtil.closeIo(serverSocket);
}
}
Dispartcher控制其他组件完成请求响应,完成首页跳转、404页面、500页面和成功页面的跳转
public class Dispatcher implements Runnable{
private Socket client;
private Request request;
private Response response;
Dispatcher(Socket client) {
this.client=client;
//获取请求协议
try {
request = new Request(client);
response=new Response(client);
} catch (IOException e) {
CloseUtil.closeIo(client);
}
}
@Override
public void run() {
//返回响应信息
try {
//首页
if(request.getUrl().equals("")){
response.printPath("index.html");
CloseUtil.closeIo(client);
return ;
}
Servlet servlet = WebApp.getServletFromUrl(request.getUrl());
if(servlet!=null){
servlet.service(request,response);
response.pushToClient(200);
}else {
//错误 404 not found
response.printPath("404.html");
//为响应添加状态码
response.pushToClient(404);
}
}catch (Exception e){
e.printStackTrace();
try {
//500 error
response.printPath("500.html");
response.pushToClient(500);
} catch (IOException ioException) {
ioException.printStackTrace();
}
}
CloseUtil.closeIo(client);
}
}
Request:解析http请求,对于post 和 get 不同的方法解析的位置不一样
get请求格式,我们只需要关注第一行:也就是请求行
post请求格式 我们只需要关注post中的请求行和最后一行请求参数
Request代码
public class Request {
//请求方式
private String method;
//请求资源
private String url;
//请求参数
private Map<String, List<String>> paramMap=new HashMap<>();
// private ;
private final String CRLF="\r\n";
private String requestInfo;
private InputStream is;
public Request(Socket client) throws IOException {
this(client.getInputStream());
}
public Request(InputStream is){
this.is=is;
//获取请求协议
byte[] datas=new byte[1024*1024];
int len= 0;
try {
len = is.read(datas);
requestInfo=new String(datas,0,len);
} catch (IOException e) {
e.printStackTrace();
return;
}
//分解字符串
parseRequestInfo();
}
/**
* 分析请求信息
*/
private void parseRequestInfo() {
String paramString =""; //接收请求参数
if(requestInfo==null||(requestInfo=requestInfo.trim()).equals("")){
return ;
}
//获取请求方式
String firstLine=requestInfo.substring(0,requestInfo.indexOf(CRLF));
// /的位置
int idx=firstLine.indexOf("/");
method=firstLine.substring(0,idx).trim();
//获取请求url和参数
String urlStr=firstLine.substring(idx+1,firstLine.indexOf("HTTP/")).trim();
System.out.println("urlStr=="+urlStr);
//判断方法是get 还是 post
if(method.equalsIgnoreCase("post")){
url=urlStr;
//请求参数
paramString=requestInfo.substring(requestInfo.lastIndexOf(CRLF)).trim();
}else if(method.equalsIgnoreCase("get")){
//是否含有参数
if(urlStr.contains("?")){
url=urlStr.substring(0,urlStr.indexOf("?"));
paramString=urlStr.substring(urlStr.indexOf("?")+1).trim();
}else {
url=urlStr;
}
}
//不存在请求参数
if(paramString.equals("")){
return ;
}
//将请求参数封装到Map
parseParams(paramString);
}
/**
* 将请求参数封装到Map
* @param paramString
*/
private void parseParams(String paramString) {
//分割 将字符产转成数组
StringTokenizer token = new StringTokenizer(paramString, "&");
while(token.hasMoreTokens()){
String keyValue=token.nextToken();
String[] split = keyValue.split("=");
split = Arrays.copyOf(split, 2);
//获取key value
String key=split[0];
String value=split[1]==null?null:decode(split[1],"utf-8");
//存储到map中
if(!paramMap.containsKey(key)){
paramMap.put(key,new ArrayList<String>());
}
paramMap.get(key).add(value);
}
}
/**
* 通过name 获取多个值value
* @param key
* @return
*/
public String[] getParameterValues(String key){
if(!paramMap.containsKey(key)){
return null;
}
List<String> values=paramMap.get(key);
return values.toArray(new String[0]);
}
private String decode(String value,String code) {
try {
return java.net.URLDecoder.decode(value,code);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return null;
}
/**
* 通过name 获取一个值value
* @param key
* @return
*/
public String getParameterValue(String key){
String[] values = getParameterValues(key);
return values==null?null:values[0];
}
/**
* 关闭资源
*/
public void close(){
CloseUtil.closeIo(is);
}
public String getMethod() {
return method;
}
public String getUrl() {
return url;
}
public Map<String, List<String>> getParamMap() {
return paramMap;
}
}
WebApp解析web.xml
文件,通过获得当前请求的URL
匹配到web.xml
文件中相应的servlet-mapping
通过servlet-mapping
中的name
获得 servlet
中的servlet-class
,得到servlet-class
就可以反射创建servelt对象。其中通过webhandler
解析配置文件,webContext
将解析到的配置文件转换为Map
public class WebApp {
private static WebContext webContext;
static {
try {
//1、获取解析工厂
SAXParserFactory factory =SAXParserFactory.newInstance();
//2、获取解析器
SAXParser sax =factory.newSAXParser();
//3、指定xml+处理器
WebHandler handler = new WebHandler();
sax.parse(Thread.currentThread().getContextClassLoader()
.getResourceAsStream("web.xml"),handler);
webContext = new WebContext(handler.getEntityList(),handler.getMappingList());
}catch (Exception e){
e.printStackTrace();
}
}
/**
* 通过url获得servlet
* @param url
* @return
*/
public static Servlet getServletFromUrl(String url) throws Exception {
//反射创建servlet对象
String clzName = webContext.getClz("/"+url);
if(clzName!=null){
Class clz = null;
clz = Class.forName(clzName);
Servlet servlet= (Servlet) clz.getConstructor().newInstance();
return servlet;
}
return null;
}
}
WebContext 将实体类封装为Map<String,String>
public class WebContext {
private List<Entity> entityList = null;
private List<Mapping> mappingList = null;
//key->servlet-name v->servlet-class
private Map<String, String> entityMap = new HashMap<>();
//key->url-pattern v->servlet-name
private Map<String, String> mappingMap = new HashMap<>();
public WebContext(List<Entity> entityList, List<Mapping> mappingList) {
this.entityList = entityList;
this.mappingList = mappingList;
//将entity的list转换成了对应的map
for (Entity entity : entityList) {
entityMap.put(entity.getName(), entity.getClz());
}
//将mapping的list转换成了对应的map
for (Mapping mapping : mappingList) {
for (String pattern : mapping.getPattern()) {
mappingMap.put(pattern, mapping.getName());
}
}
}
public String getClz (String pattern){
String s = mappingMap.get(pattern);
String clz = entityMap.get(s);
return clz;
}
}
WebHandler 使用SAX解析web.xml,web.xml文件格式有严格要求 web.xml格式见下文
public class WebHandler extends DefaultHandler {
private List<Entity> entityList;
private List<Mapping> mappingList;
private tjoker.server.core.Entity entity;
private tjoker.server.core.Mapping mapping;
private boolean isMapping;
private String tag;//存储操作表示符
@Override
public void startDocument() throws SAXException {
//文档解析开始初始化
entityList=new ArrayList<>();
mappingList=new ArrayList<>();
}
@Override
public void endDocument() throws SAXException {
//文档结束
}
@Override
public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
if(qName!=null){
tag=qName;
if(qName.equals("servlet")){
isMapping=false;
entity = new Entity();
}else if(qName.equals("servlet-mapping")){
isMapping=true;
mapping = new Mapping();
}
}
}
@Override
public void characters(char[] ch, int start, int length) throws SAXException {
if(tag!=null){
String str =new String(ch,start,length).trim();
if(isMapping==false){
if(tag.equals("servlet-name")){
entity.setName(str);
}else if(tag.equals("servlet-class")){
entity.setClz(str);
}
}else {
if(tag.equals("servlet-name")){
mapping.setName(str);
}else if(tag.equals("url-pattern")){
mapping.addPattern(str);
}
}
}
}
@Override
public void endElement(String uri, String localName, String qName) throws SAXException {
if(qName!=null){
if(qName.equals("servlet")){
entityList.add(entity);
}else if(qName.equals("servlet-mapping")){
mappingList.add(mapping);
}
}
tag=null;
}
public List<Entity> getEntityList() {
return entityList;
}
public List<Mapping> getMappingList() {
return mappingList;
}
public Entity getEntity() {
return entity;
}
public Mapping getMapping() {
return mapping;
}
}
Entity 对web.xml节点的servlet 其中 servlet-name–>name servlet-class–>clz
public class Entity {
private String name;
private String 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;
}
}
Mapping 对应 web.xml
文件中的servlet-mapping
节点
public class Mapping {
private String name;
private Set<String> pattern;
public Mapping() {
pattern=new HashSet<>();
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Set<String> getPattern() {
return pattern;
}
public void setPattern(Set<String> pattern) {
this.pattern = pattern;
}
public void addPattern(String pattern){
this.pattern.add(pattern);
}
}
Servlet 抽象方法
public abstract class Servlet {
public void service(Request req,Response rep) throws Exception{
this.doGet(req,rep);
this.doPost(req,rep);
}
protected abstract void doGet(Request req,Response rep) throws Exception;
protected abstract void doPost(Request req,Response rep) throws Exception;
}
Response 封装响应头、响应行、响应正文,其中每一个都有严格的格式要求
public class Response {
//常量
private final String CRLF="\r\n";
private final String BLANK=" ";
//响应头
private StringBuilder headInfo;
//响应内容
private StringBuilder context;
//流
private BufferedWriter bw;
//长度
int len;
private Response(){
context=new StringBuilder();
headInfo=new StringBuilder();
len=0;
}
public Response(Socket client){
this();
try {
bw=new BufferedWriter(new OutputStreamWriter(client.getOutputStream()));
} catch (IOException e) {
e.printStackTrace();
headInfo=null;
}
}
public Response(OutputStream os){
this();
bw=new BufferedWriter(new OutputStreamWriter(os));
}
/**
* 响应正文,传递文件名
* @param fileName
* @return
*/
public Response printPath(String fileName) {
//通过response处理响应信息
ResponseResolver responseHandler = new ResponseResolver(fileName);
String s = responseHandler.getString();
context.append(s).append(CRLF);
len+=(s+CRLF).getBytes().length;
return this;
}
/**
* 响应正文+换行
* @param info
* @return
*/
public Response println(String info){
context.append(info).append(CRLF);
len+=(info+CRLF).getBytes().length;
return this;
}
/**
* 创建头部信息
* @param code
*/
private void createHeader(int code){
//1) HTTP协议版本、状态代码、描述
headInfo.append("HTTP/1.1").append(BLANK).append(code).append(BLANK);
switch(code){
case 200:
headInfo.append("OK");
break;
case 404:
headInfo.append("NOT FOUND");
break;
case 500:
headInfo.append("SEVER ERROR");
break;
}
headInfo.append(CRLF);
//2) 响应头(Response Head)
headInfo.append("Server:tjoker Server/0.0.1").append(CRLF);
headInfo.append("Date:").append(new Date()).append(CRLF);
headInfo.append("Content-type:text/html;charset=utf-8").append(CRLF);
//正文长度 :字节长度
headInfo.append("Content-Length:").append(len).append(CRLF);
headInfo.append(CRLF); //分隔符
}
/**
* 推送到客户端
*/
void pushToClient(int code) throws IOException {
if(headInfo==null){
code=500;
}
createHeader(code);
//头信息+分隔符
bw.append(headInfo.toString());
//正文信息
bw.append(context.toString());
bw.flush();
}
/**
* 关闭资源
*/
public void close(){
CloseUtil.closeIo(bw);
}
}
ResponseResolver
public class ResponseResolver {
private StringBuilder sb;
public ResponseResolver(String fileName) {
//解析路径
File file=new File("src/main/java/tjoker/server/servlet/"+fileName);
InputStream is = null;
try {
is = new FileInputStream(file);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
if(is==null) {
System.out.println("页面不存在");
return;
}
System.out.println("页面存在");
byte[] bytes= new byte[1024*1024];
int lens=0;
sb=new StringBuilder();
while(true) {
try {
if (!(-1 != (lens = is.read(bytes)))) break;
} catch (IOException e) {
e.printStackTrace();
}
//输出 字节数组转成字符串
String info = new String(bytes, 0, lens);
sb.append(info);
}
}
public String getString(){
return sb.toString();
}
}
CloseUtil 关闭io资源流
public class CloseUtil {
/**
* 关闭io流
* @param io
*/
public static void closeIo(Closeable... io){
for(Closeable temp:io){
try {
if (null != temp) {
temp.close();
}
} catch (Exception e) {
// TODO: handle exception
}
}
}
}
三、 效果展示
我们需要新建两个html文件,一个login.html
用于测试,一个success.html
用于成功后跳转,同样我们需要一个LoginServlet
用于逻辑处理
测试项目结构如下
新建一个login.html
文件,用于测试
<html>
<head>
<meta charset="UTF-8">
<title>头部</title>
</head>
<body >
<!--action为项请求的路径,在web.xml中进行配置-->
<form method="post" action="http://localhost:8888/g">
用户名:<input type="text" name="uname" id="uname"/>
密码:<input type="password" name="pwd" id="pwd"/>
<input type="submit" value="提交"/>
</form>
</body>
</html>
同样新建一个success.html
文件,用于请求成功后跳转的页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>首页</title>
</head>
<body >
<div >
<h2 style="width:360px;margin:36px auto;">欢迎来到</h2>
<h3 style="width:580px;margin:0 auto;">TJOKER----手写简易版WEBSERVER</h3>
</div>
</body>
</html>
新建LoginServelt
类进行逻辑处理,继承抽象父类Servlet
重写Service方法
public class LoginServlet extends Servlet {
@Override
public void service(Request request, Response response) {
StringBuffer responseContext = new StringBuffer("登录成功了");
response.printPath("index.html");
}
@Override
protected void doGet(Request req, Response rep) throws Exception {
}
@Override
protected void doPost(Request req, Response rep) throws Exception {
}
}
现在万事具备,只欠 web.xml
文件,配置文件格式严有格规定,必须按照特定格式书写,否则会出现项目无法启动的情况。则写过web的同学应该知道。web.xml
文件的位置也有特定要求,需放在webapp下
或者资源目录下resourse
web.xml文件
<?xml version="1.0" encoding="UTF-8"?>
<web-app>
<servlet>
<!--登录-->
<servlet-name>login</servlet-name>
<servlet-class>tjoker.server.servlet.LoginServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>login</servlet-name>
<url-pattern>/g</url-pattern>
<url-pattern>/y</url-pattern>
</servlet-mapping>
<servlet>
<!--注册-->
<servlet-name>reg</servlet-name>
<servlet-class>tjoker.server.servlet.RegServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>reg</servlet-name>
<url-pattern>/r</url-pattern>
<url-pattern>/x</url-pattern>
</servlet-mapping>
</web-app>
到此你所有准备工作已经完成了,上线(上线前默念:佛祖保佑,无bug,无bug)
用颤抖的双手点击run运行Server
类,用浏览器开打login.html
文件
输入用户名,密码,点击提交按钮(佛祖保佑,永无bug)
页面成功跳转,,感谢bug ,感谢佛祖保佑。
页面简单,各位看官别嫌弃。到此你的webserver已经成功完成了,恭喜你又向大佬迈进了一步。
四、总结
项目地址:我已经将源代码上传到 github中https://github.com/Tjoker-cell/webserver.git,各位看官不要忘了给我标个小星星