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

来源:互联网 发布:淘宝车秒贷首付怎么算 编辑:程序博客网 时间:2024/05/22 14:55

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

本项目源码已上传到github,本文章结尾将奉上地址。

目录结构

项目文件目录,是基于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协议格式。



在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流为响应流。


初始化属性:
    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文件,存放配置信息



这里使用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,使用的请求方法和处理此请求的类方法。


再写一个接口用于规范处理数据类,
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);}}


测试:


测试成功!
本例源码:https://github.com/zhenxin77520/JavaServer