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

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

目录结构

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

基于Java手写web服务器(简易版)_第1张图片

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协议格式。

基于Java手写web服务器(简易版)_第2张图片

 

 

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

基于Java手写web服务器(简易版)_第3张图片

 

初始化属性:

    public HttpResponse(OutputStream out) {
        this.out = out;
        
        Header = new LinkedHashMap();
        Status = new HashMap();
        
        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> set = Header.entrySet();
        for (Entry 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文件,存放配置信息

 

基于Java手写web服务器(简易版)_第4张图片

 

这里使用dom4j来读取xml文件

    private static void init() {
        Types = new HashMap();
        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 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,使用的请求方法和处理此请求的类方法。

基于Java手写web服务器(简易版)_第5张图片

 

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

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

 

ActionListen类

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

    private static Map GetMethod;
    private static Map PostMethod;
	
	static {
		loadListenes();
	}
	
	private static void loadListenes() {
		GetMethod = new HashMap();
		PostMethod = new HashMap();
		try {
			SAXReader reader = new SAXReader();
			File file = new File("config/action.xml");
			Document doc = reader.read(file);
			Element root = doc.getRootElement();
			
			@SuppressWarnings("unchecked")
			List 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 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文件
 

登录

用户名:
密 码:
还没帐号?现在注册


这里我用ajax请求模拟登录操作

 

基于Java手写web服务器(简易版)_第6张图片

 

在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);
		
	}
	
}

 

测试:

基于Java手写web服务器(简易版)_第7张图片

 

测试成功!

 

 

你可能感兴趣的:(java网络编程)