手写简单Web服务器

一、认识web服务器

Web服务器的主要功能是和浏览器建立tcp连接,并不断接受浏览发送的请求,再向浏览器发送响应,下面是服务器运行的主要流程。
在这里插入图片描述
服务器要做的事情就是上面的四个过程。

二、创建连接,接受请求

Server类

想要获得一个服务器,那么我们就要创建一个服务器类,服务器类就是要重复上面的四个过程,我们可以在获取一个ServerSocket 类后不断再一个while循环里面获取Socket,建立连接获取请求和创建响应。此外,代码还要判断路由,我要去哪里找对应的资源给浏览器,以html文件和Servlet类为例子。

//接受请求

    public void reciveing() throws Exception{
        ServerSocket serverSocket = new ServerSocket(port);
        while (true){
            Socket socket = serverSocket.accept();
            System.out.println("接收到请求");
            //获取输入流
            InputStream inputStream = socket.getInputStream();
            OutputStream outputStream = socket.getOutputStream();
            int t = inputStream.available();
            System.out.println("请求长度是" + t);
            if(myRequest.path.contains(".html")) {
                //寻找html文件
            }else {
                //寻找Servlet文件
            }
            socket.close();
        }
    }

三、解析请求

Request类

我们可以通过InputStream获取浏览器传来的Http请求,但是在创建响应前我们还要读懂Http请求,并获取里面的数据。Request就是用来干这事的,Http请求分为请求行、请求头、请求体(Post方法)我们以Get方法为例子,我们的Request类先要获取InputStream,并保存里面的**请求方法、URI路径、传过来的数据(HashMap)、请求头的信息(HashMap)**并在创建是传入输入流

 	//请求方法
    public String method;
    //数据
    public Map<String, String> datas = new HashMap<>();
    //请求路径
    public String path;
    //请求头
    public Map<String, String> heads;
    //请求体
    public String body;
    public MyRequest(InputStream inputStream){
        try {
            parse(inputStream);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

解析请求行

GET /api/users?id=123 HTTP/1.1
Host: example.com
User-Agent: Mozilla/5.0
Accept: application/json

上面是一个简单的Http请求的例子,最上面的一行是请求行里面包含了传递的数据(id=123),请求方法(GET),Http版本(HTTP/1.1),Http路径(/api/users)。这就是我们要从里面提取的信息,可以读取第一行数据,使用String的split方法(根据某个字符串把字符串分割),根据空格把他分为“GET”,“/api/users?id=123” , HTTP/1.1。里面的第一个是请求方法GET,第二个包含了路径和数据,第三个是版本号。获取数据和路径只要对第二个重复上面的步骤,先用“?”分割,在用“=”分割。代码如下

//解读请求行
    public void parseLines(String line){
        System.out.println(line);
        //根据空格分割
        String[] infore = line.split(" ");
        //获取方法
        method = infore[0];
        if(method.equals("GET")){
        	//根据?分割
            String[] strs = infore[1].split("\\?");
            //获取路径
            path = strs[0];
            for(int i = 1; i < strs.length; i++){
            	//根据=分割,并存入HashMap
                String[] keys = strs[i].split("=");
                datas.put(keys[0], keys[1]);
            }
        }else {
            path = infore[1];
        }

    }

解析请求头

Host: example.com
User-Agent: Mozilla/5.0
Accept: application/json

上面的部分就是请求头,只要一行一行的读,并根据“:”分割,存入HashMap就可以实现了

//解读请求头
    public void parseHeads(BufferedReader br) throws Exception{
        heads = new HashMap<>();
        String line;
        while ((line = br.readLine()) != null && !line.trim().isEmpty()){
            String[] infore = line.split(": ");
            System.out.println(line);
            heads.put(infore[0], infore[1]);
        }
    }

上面就是Request类的核心代码,组装一下就好了。

//解读请求
    public void parse(InputStream inputStream) throws Exception {
        BufferedReader br = new BufferedReader(new InputStreamReader(inputStream));
        //解读请求行
        String line = br.readLine();
        parseLines(line);
        //解读请求头
        parseHeads(br);
    }

四、创建回响

Respone(一)

解读完之后,还要根据解读的信息组成对应的Http响应,发送给浏览器,Http响应主要是状态行,响应头,响应体。状态行表示响应的状态,当我们收到请求时,我们判断请求的资源是否存在,如果存在,那么我们返回一个200状态码,否则返回一个404的状态码。响应头里面存储响应的附带的信息。比如响应一个html文件,那么我们要传出的Http响应的头里面就包含text/html,文本信息就包含有text/plain,响应体包含要响应的数据。下面是一个简单的Http响应(传出的是JSON数据)

HTTP/1.1 200 OK
Content-Type: application/json

{
"id": 123,
"name": "John Doe",
"email": "john@example.com"
}

那么可以定义一个方法来拼接成Http响应格式,传入状态码,响应类型,响应的数据就可以自己组装成响应的方法。

//组装响应
    public String getResponse(String code, String type, String message){
        return "HTTP/1.1 " + code + "\r\n"
                + "Content-Type:" + type + ";charset=UTF-8\r\n"
                + "\r\n"
                + message;
    }

还要分为两种方法传递数据,一种是传递html文件,另外一种是响应数据。先看文件的,先判断有没有这个文件,如果有这个文件就用IO流读取html文件里面的内容,传递给浏览器。判断可以使用字符串拼接,拼出一个路径,并获取文件对象,调用exsist方法来判断有没有这个文件,在读取文件里面的所有的内容。传入200, text/html使用上面的方法组装Http响应。否则返回404状态码。具体的代码如下。

public void sendRedirect(MyRequest request) throws Exception{
        path = request.path;
        //判断资源是否存在
        String path = "C:\\Users\\wrui\\IdeaProjects\\test\\src\\main\\java\\webserver\\show" + this.path;
        File file = new File(path);
        if(file.exists()){
            FileInputStream fin = new FileInputStream(file);
            byte[] bytes = new byte[(int)file.length()];
            fin.read(bytes);
            String result = new String(bytes);
            String infore = getResponse("200","text/html", result);
            //System.out.println("我的响应是" + infore);
            outputStream.write(infore.getBytes());
        }else{
            String error = getResponse("404", "text/html","404 not found");
            outputStream.write(error.getBytes());
        }
    }

那如果我们需要的是一些业务处理呢?而不是返回一个html文件呢?。我们怎么判断给路由存不存在?怎么调用里面的方法执行相关的代码实现业务需求?这个时候我们需要Servlet

Servlet

我们可以创建一个抽象类,在类里面定义一个doGet的抽象方法。执行业务需求时,我们创建对应的类,继承Servelt,重写doGet,在里面事项业务需求。在定义一个注解,存储当前的类的路由位置。接受请求后,根据里面的请求的路径,搜索匹配注解里面的信息。Servlet的代码如下很简单。

public abstract class Servlet {
    //处理业务逻辑的规范
    public abstract void doGet(MyRequest request, MyResponse response);
}

注解的代码如下

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface WebPath {
    String value();
}

Response(二)

那么我们现在已经解决了复杂的业务处理的问题了,我们在获取路由时,判断是否存在该路由呢?可以把所以的Servlet的子类放在对应的一个包下面,在服务器启动时扫描获取所以类的字节文件Class,并获取注解WebPath里面的路径,把路径作为键,字节文件作为值存入HashMap里面。代码如下

public static void  loadClassesFromJavaFiles(String folderPath, String packageName) throws Exception {
        File dir = new File(folderPath);

        // 检查文件夹是否存在
        if (!dir.exists() || !dir.isDirectory()) {
            throw new IllegalArgumentException("文件夹不存在或不是目录: " + folderPath);
        }

        // 遍历文件夹下的所有 .java 文件
        for (File file : dir.listFiles((dir1, name) -> name.endsWith(".java"))) {
            // 提取类名(去掉 .java 后缀)
            String fileName = file.getName();
            String className = packageName + "." + fileName.substring(0, fileName.length() - 5);
            try {
                // 使用 Class.forName 加载类(要求 .class 文件已在类路径中)
                Class<?> clazz = Class.forName(className);
                if(clazz.isAnnotationPresent(WebPath.class)){
                    WebPath an = clazz.getAnnotation(WebPath.class);
                    class_map.put(an.value(), clazz);
                }
            } catch (ClassNotFoundException e) {
                System.err.println("无法加载类: " + className + "(请确保已编译并位于类路径中)");
            }
        }
    }

获取文件夹的路径,遍历文件夹下面所有路径带有“.java”的文件,这是类文件。在从里面去掉“.java”的字符串,使用Class的forName方法,获取对应的Class类。使用isAnnotationPresent方法,获取里面的注解对象,把他存入HashMap里面。
如果我们获取了路由,只要在这个HashMap里面看看有没有对应的键就好了。并取出对应的Class对象,调用getConstructor方法获取构造器Constructor,在用构造器调用newInstance获取对应的对象。最后调用Class类的getMethod方法获取方法对象,调用该对象的invoke方法。
代码如下

public void sendData(MyRequest request) throws Exception{
        //判断有没有该路径
        if(Server.class_map.containsKey(request.path)){
            System.out.println("找到了是" + request.path);
            Class<?> clazz = Server.class_map.get(request.path);
            //获取对象
            Constructor<?> constructor = clazz.getConstructor();
            Object servlet = constructor.newInstance();
            //调用方法
            Method method = clazz.getMethod("doGet", MyRequest.class, MyResponse.class);
            method.invoke(servlet, request, this);
        }else {
            //响应404,没有找到
            String error = getResponse("404", "text/html","404 not found");
            outputStream.write(error.getBytes());
        }
    }

    //输出数据,并拼接成Http响应格式
    public void write(String str) throws IOException {
        String data = getResponse("200", "text/plain", str);
        outputStream.write(data.getBytes());
    }

最后再整理一下把Server写完整

六、源码

下面是所有类的源码

Server类

public class Server {
    //端口号
    public int port;
    public static Map<String, Class<?>> class_map = new HashMap<>();
    public Server(int port){
        this.port = port;
    }

    //接受请求

    public void reciveing() throws Exception{
        ServerSocket serverSocket = new ServerSocket(port);
        getAllServlet();
        System.out.println("已经扫描了所有的类!");
        while (true){
            Socket socket = serverSocket.accept();
            System.out.println("接收到请求");
            //获取输入流
            InputStream inputStream = socket.getInputStream();
            OutputStream outputStream = socket.getOutputStream();
            int t = inputStream.available();
            System.out.println("请求长度是" + t);
            //创建Request对象 并解析请求
            MyRequest myRequest = new MyRequest(inputStream);
            System.out.println(myRequest.path);
            //创建响应
            MyResponse myResponse = new MyResponse(outputStream);
            //判断响应数据还是响应页面
            if(myRequest.path.contains(".html")) {
                myResponse.sendRedirect(myRequest);
            }else {
                myResponse.sendData(myRequest);
            }
            socket.close();
        }
    }

    public void getAllServlet() throws Exception {
        loadClassesFromJavaFiles("C:\\Users\\wrui\\IdeaProjects\\test\\src\\main\\java\\webserver\\main"
                , "webserver.main");
    }

    //获取含有注解的对象的Class
    public static void  loadClassesFromJavaFiles(String folderPath, String packageName) throws Exception {
        File dir = new File(folderPath);

        // 检查文件夹是否存在
        if (!dir.exists() || !dir.isDirectory()) {
            throw new IllegalArgumentException("文件夹不存在或不是目录: " + folderPath);
        }

        // 遍历文件夹下的所有 .java 文件
        for (File file : dir.listFiles((dir1, name) -> name.endsWith(".java"))) {
            // 提取类名(去掉 .java 后缀)
            String fileName = file.getName();
            String className = packageName + "." + fileName.substring(0, fileName.length() - 5);
            try {
                // 使用 Class.forName 加载类(要求 .class 文件已在类路径中)
                Class<?> clazz = Class.forName(className);
                if(clazz.isAnnotationPresent(WebPath.class)){
                    WebPath an = clazz.getAnnotation(WebPath.class);
                    class_map.put(an.value(), clazz);
                }
            } catch (ClassNotFoundException e) {
                System.err.println("无法加载类: " + className + "(请确保已编译并位于类路径中)");
            }
        }
    }
}

MyResponse类

public class MyResponse {
    OutputStream outputStream;
    String type = "text/html";
    String path = "";
    public MyResponse(OutputStream outputStream){
        this.outputStream = outputStream;
    }
    //响应页面
    public void sendRedirect(MyRequest request) throws Exception{
        path = request.path;
        //判断资源是否存在
        String path = "C:\\Users\\wrui\\IdeaProjects\\test\\src\\main\\java\\webserver\\show" + this.path;
        File file = new File(path);
        if(file.exists()){
            FileInputStream fin = new FileInputStream(file);
            byte[] bytes = new byte[(int)file.length()];
            fin.read(bytes);
            String result = new String(bytes);
            String infore = getResponse("200","text/html", result);
            //System.out.println("我的响应是" + infore);
            outputStream.write(infore.getBytes());
        }else{
            String error = getResponse("404", "text/html","404 not found");
            outputStream.write(error.getBytes());
        }
    }
    //组装响应
    public String getResponse(String code, String type, String message){
        return "HTTP/1.1 " + code + "\r\n"
                + "Content-Type:" + type + ";charset=UTF-8\r\n"
                + "\r\n"
                + message;
    }

    //响应字符串数据
    public void sendData(MyRequest request) throws Exception{
        //判断有没有该路径
        if(Server.class_map.containsKey(request.path)){
            System.out.println("找到了是" + request.path);
            Class<?> clazz = Server.class_map.get(request.path);
            //获取对象
            Constructor<?> constructor = clazz.getConstructor();
            Object servlet = constructor.newInstance();
            //调用方法
            Method method = clazz.getMethod("doGet", MyRequest.class, MyResponse.class);
            method.invoke(servlet, request, this);
        }else {
            //响应404,没有找到
            String error = getResponse("404", "text/html","404 not found");
            outputStream.write(error.getBytes());
        }
    }

    //输出数据,并拼接成Http响应格式
    public void write(String str) throws IOException {
        String data = getResponse("200", "text/plain", str);
        outputStream.write(data.getBytes());
    }

}

MyRequest类

public class MyRequest {
    //请求方法
    public String method;
    //数据
    public Map<String, String> datas = new HashMap<>();
    //请求路径
    public String path;
    //请求头
    public Map<String, String> heads;
    //请求体
    public String body;
    public MyRequest(InputStream inputStream){
        try {
            parse(inputStream);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
    //解读请求
    public void parse(InputStream inputStream) throws Exception {
        BufferedReader br = new BufferedReader(new InputStreamReader(inputStream));
        //解读请求行
        String line = br.readLine();
        parseLines(line);
        //解读请求头
        parseHeads(br);
    }
    //解读请求行
    public void parseLines(String line){
        System.out.println(line);
        String[] infore = line.split(" ");
        method = infore[0];
        if(method.equals("GET")){
            String[] strs = infore[1].split("\\?");
            path = strs[0];
            for(int i = 1; i < strs.length; i++){
                String[] keys = strs[i].split("=");
                datas.put(keys[0], keys[1]);
            }
        }else {
            path = infore[1];
        }

    }
    //解读请求头
    public void parseHeads(BufferedReader br) throws Exception{
        heads = new HashMap<>();
        String line;
        while ((line = br.readLine()) != null && !line.trim().isEmpty()){
            String[] infore = line.split(": ");
            System.out.println(line);
            heads.put(infore[0], infore[1]);
        }
    }


}

Servlet类

public abstract class Servlet {
    //处理业务逻辑的规范
    public abstract void doGet(MyRequest request, MyResponse response);
}

WebPath注解

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface WebPath {
    String value();
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值