基于Java手写web服务器(简易版)

更换账号,博客搬家,本文原发表时间 2017-12-21 19:38:05


本人尚在java 学习阶段,不是技术大咖, 自认是技术宅, 有一点写东西的能力,因最近学习了java网络编程,决定手写一个web服务器,不喜勿喷,大神也请高抬贵手,不足之处还望指点一二,不胜感激!

目录结构
目录结构
项目文件目录,是基于Maven的标准文件夹目录,src内包含7个类和一个接口,user包下的类为使用类。一个config文件夹用于存放服务器配置文件,webapps文件夹为服务器站点目录。

WebServer类

此类为本服务器的main线程类,负责启动服务器,不断接收客户端连接,使用ServerSocket对象实现基于tcp的网络对接,因为并发线程会很多,所以使用线程池来不断接收客户端的连接,也避免了频繁创建线程的效率低下问题。这里的端口号与线程池的大小都写入了config配置文件中,便于后期修改配置。接收到的soket对象放入ClientHandler线程类中使用。

构造函数中初始化ServerSocket对象与线程池对象。

public WebServer() {
        try {
            server = new ServerSocket(ServerContext.Port);
            threadPool = Executors.newFixedThreadPool(ServerContext.MaxThread);
        } catch (IOException e) {
            e.printStackTrace();
            System.out.println("服务器启动失败!");
        }
    }
}

写一个start方法不停循环接收浏览器请求,在调用ClientHandler线程类去处理浏览器请求。

public void start() {
        try {
            while (true) {
                System.out.println("等待客户端连接");
                Socket socket = server.accept();
                threadPool.execute(new ClientHandler(socket));
            }
        } catch (Exception e) {
        }
    }

在main函数中启动服务端程序。

public static void main(String[] args) {
        WebServer server = new WebServer();
        server.start();
    }

ClientHandler类

此类为客户端线程类,实现Runnable接口,是http连接的核心类,负责接收浏览器端发来的请求并返回响应消息,中转站的作用。

run方法首先获取输入输出流并使用HttpRequest对象解析请求。

        InputStream in = socket.getInputStream();
        OutputStream out = socket.getOutputStream();
        HttpRequset req = new HttpRequset(in);

        boolean isPost = "POST".equals(req.getMethod());
        HttpResponse res = new HttpResponse(out);

        //把消息分为静态内容和动态内容//--动态内容--
        //是否为用户数据处理接口(动态内容)
        if (ActionListen.isAction(req.getUri(), isPost)) {
            ActionListen.doAction(req, res, req.getUri(), isPost);
            return;
        }
        //--静态内容--
        //获取请求类型
        String type = req.getUri().substring(req.getUri().lastIndexOf(".") + 1);
        //设置响应头
        res.setHeader("Content-Type", ServerContext.getType(type));
        //获取静态文件
        File file = new File(ServerContext.WebRoot + req.getUri());

        if (!file.exists()) {
            //404 请求内容找不到
            res.setStatus(404);
            file = new File(ServerContext.WebRoot + "/" + ServerContext.NotFoundPage);
        } else {
            res.setStatus(200);
        }
        //响应静态内容
        BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file));
        byte[] bys = new byte[(int) file.length()];
        bis.read(bys);
        res.write(bys);
        bis.close();

HttpRequset类

解析http请求消息,存储资源标识符,请求类型和协议等,对请求头进行封装,保存所有的消息头序列,封装请求参数。写此类前必须先了解http协议格式。

HttpRequset类

在analysis方法中首先解析uri、method、protocol属性,

String line = buffer.readLine();
if (line != null && line.length() > 0) {
    String[] temp = line.split("\\s");
    this.method = temp[0];
    this.uri = temp[1];
    if (this.uri.indexOf("?") != -1) {
        String str = this.uri.substring(this.uri.indexOf("?") + 1);
        genarenal(str, false);
        this.uri = this.uri.substring(0, this.uri.indexOf("?"));
    }
    if (this.uri.endsWith("/")) {
        this.uri += "index.html";
    }
    this.protocol = temp[2];
}

然后解析header信息,这里要注意的一个问题点是使用buffer.readLine时读取到末尾并不会自动退出,会一直阻塞,因为浏览器

端发送消息后或一直等待服务端的响应直到请求超时,所以在等待期间输入流会一直打开,而服务端使用buffer.readLine读取时,读

取到最后时因为浏览器端输入流没关闭,所以会一直阻塞,造成了两端在相互等待的情况,形成死锁。所以在读取到空行时就break

出while循环。这里保存的Content-Length的值是为了根据这个值的大小去读取post请求的内容。

String l = null;
int len = 0;
while ((l = buffer.readLine()) != null) {
    if ("".equals(l)) {
        break;
    }
    String k = l.substring(0, l.indexOf(":")).trim();
    String v = l.substring(l.indexOf(":") + 1).trim();
    this.Header.put(k, v);
    if (l.indexOf("Content-Length") != -1) {
        len = Integer.parseInt(l.substring(l.indexOf(":") + 1).trim());
    }
}

如果请求的为post请求,则根据Content-Length读取消息体

if (method != null && method.toUpperCase().equals("POST")) {
    char[] bys = new char[len];
    buffer.read(bys);
    String paraStr = new String(bys);
    genarenal(paraStr, true);
}
/**
 * 对请求字符串解析成参数对象
 * @param str
 * @param isPost
 */
private void genarenal(String str, boolean isPost) {
    String[] arr = str.split("&");
    for (String s : arr) {
        String[] temp = s.split("=");
        if (isPost) {
            this.Form.put(temp[0], temp[1]);
        } else {
            this.QueryString.put(temp[0], temp[1]);
        }
        this.Parameter.put(temp[0], temp[1]);
    }
}

HttpResponse类

4个属性 :Status存储所有消息响应码与对应的内容,Header则是存储用于响应的所有消息头,整型status用于设置本次响应码

out流为响应流。

HttpResponse类

初始化属性:

public HttpResponse(OutputStream out) {
    this.out = out;
    
    Header = new LinkedHashMap<String, String>();
    Status = new HashMap<Integer, String>();
    
    Status.put(HttpContext.STATUS_CODE_OK, HttpContext.STATUS_REASON_OK);
    Status.put(HttpContext.STATUS_CODE_NOT_FOUND, HttpContext.STATUS_REASON_NOT_FOUND);
    Status.put(HttpContext.STATUS_CODE_ERROR, HttpContext.STATUS_REASON_ERROR);
    
    Header.put("Content-Type", "text/plain;charset=utf-8");
    Header.put("Date", new Date().toString());
    status = 200;
}

两个核心重载方法分别用于发送字节流和字符串

/**
* 响应方法,发送字符串
 * @param bys
 */
public void write(String str) {
    Header.put("Content-Length", Integer.toString(str.length()));
    PrintStream ps = new PrintStream(out);
    printHeader(ps);
    ps.println(str);
    ps.flush();
}
/**
 * 打印头信息
 * @param ps
 */
private void printHeader(PrintStream ps) {
	ps.println(ServerContext.Protocol + " " + status + " " + Status.get(status));
    Set<Entry<String, String>> set = Header.entrySet();
    for (Entry<String, String> entry : set) {
        String k = entry.getKey();
        String v = entry.getValue();
        ps.println(k + ":" + v);
    }
    ps.println("");
}

HttpContext类

静态变量类,目前用于存放响应状态码和状态信息。

public class HttpContext {
    public final static int STATUS_CODE_OK = 200;

    public final static String STATUS_REASON_OK = "OK";

    public final static int STATUS_CODE_NOT_FOUND = 404;

    public final static String STATUS_REASON_NOT_FOUND = "Not Found";
    
    public final static int STATUS_CODE_ERROR = 500;
    
    public final static String STATUS_REASON_ERROR = "Internal Server Error";
}

ServerContext类

读取配置文件类,首先在config文件夹中创建一个xml文件,存放配置信息

ServerContext类

这里使用dom4j来读取xml文件

private static void init() {
    Types = new HashMap<String, String>();
    try {
        SAXReader reader = new SAXReader();
        Document doc = reader.read("config/config.xml");
        Element root = doc.getRootElement();
        
        Element service = root.element("service");
        Element connector = service.element("connector");
        Protocol = connector.attributeValue("protocol");
        Port = Integer.parseInt(connector.attributeValue("port"));
        MaxThread = Integer.parseInt(connector.attributeValue("max-thread"));
        WebRoot = service.elementText("webroot");
        NotFoundPage = service.elementText("not-found-page");
        
        @SuppressWarnings("unchecked")
        List<Element> typeMappings = root.element("type-mappings").elements("type-mapping");
        for (Element e : typeMappings) {
            Types.put(e.attributeValue("ext"), e.attributeValue("type"));
        }
        
    } catch (Exception e) {
        e.printStackTrace();
    }
}

-----到此,本服务器大致已经搭建完成,可以请求响应静态内容,如html、js、css、图片等内容,接下来,我们开始动态内容的搭建

首先,写一个action.xml存放与config目录下,基本结构如下,主要放请求url,使用的请求方法和处理此请求的类方法。
action.xml

再写一个接口用于规范处理数据类,

public interface Action {
	void bridge(HttpRequset req, HttpResponse res);
}

ActionListen类

用于读取action.xml中的信息使用的也是dom4j来读取。

private static Map<String, String> GetMethod;
private static Map<String, String> PostMethod;

static {
	loadListenes();
}

private static void loadListenes() {
	GetMethod = new HashMap<String, String>();
	PostMethod = new HashMap<String, String>();
	try {
		SAXReader reader = new SAXReader();
		File file = new File("config/action.xml");
		Document doc = reader.read(file);
		Element root = doc.getRootElement();
		
		@SuppressWarnings("unchecked")
		List<Element> list = root.elements("listen");
		for (Element e : list) {
			Element action = e.element("action");
			if ("POST".equals(action.attributeValue("method").toUpperCase())) {
				PostMethod.put(action.getText(), e.elementText("target"));
			} else {
				GetMethod.put(action.getText(), e.elementText("target"));
			}
		}
	} catch (Exception e) {
		e.printStackTrace();
	}
}

使用反射执行xml里的处理类方法:

public static void doAction(HttpRequset req, HttpResponse res, String methodurl, boolean isPost) {
	try {
		String target = null;
		if (isPost) {
			target = PostMethod.get(methodurl);
		} else {
			target = GetMethod.get(methodurl);
		}
		Class<? extends Object> cls = Class.forName(target);
		Object obj = cls.newInstance();
		Method[] methods = cls.getDeclaredMethods();
		for (Method method : methods) {
			if ("bridge".equals(method.getName())) {
				method.invoke(obj, req, res);
			}
		}
	} catch (Exception e) {
		e.printStackTrace();
	}
}

到此,本服务器所有架构搭建完毕,接下来我们写一个测试用例,来测试服务器:

首先在webapps文件夹里写一个login.html文件

<div class="main">
	<h2>登录</h2>
	<form id="myform">
		<table>
			<tr>
				<td>用户名:</td><td><input type="text" name="user" id="user"></td>
			</tr>
			<tr>
				<td>密  码:</td><td><input type="password" name="password" id="pwd"></td>
			</tr>
			<tr>
				<td colspan="2">
					<input type="submit" value="提交"> <a href="register.html">还没帐号?现在注册</a>
				</td>
			</tr>
		</table>
	</form>
</div>
<script type="text/javascript">
	var form = document.getElementById('myform');
	form.onsubmit = function (e) {
		var user = document.getElementById('user').value;
		var pwd = document.getElementById('pwd').value;
		var data = "user="+user+"&password="+pwd;
		dosome(data, function (rel){
			alert(rel);
		});
		e.preventDefault();
	}
	function dosome (param, callback) {
		var xhr = new XMLHttpRequest();
		xhr.open("POST", "http://localhost:8080/login", true);
		xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
		xhr.send(param);
		xhr.onreadystatechange = function () {
			if (xhr.readyState == 4 && xhr.status == 200) {
				callback(xhr.responseText);
			}
		}
	}
</script>

这里我用ajax请求模拟登录操作
在这里插入图片描述

在user包中创建Lodin类,如下:

public class Login implements Action{

	public void bridge(HttpRequset req, HttpResponse res) {
		String user = req.Form("user");
		String pwd = req.Form("password");
		String rel = user + "--" + pwd;
		res.write(rel);
		
	}
	
}

测试:
在这里插入图片描述

测试成功!

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值