Java 从零开始手撸一个 HTTP 服务器

先想想需要达到怎样的要求:

本来这是一个很小的课程设计作业,老师也是要求能达到简单的socket应答就行了。但是我还是觉得有必要自己手撸一个HTTP服务器,毕竟这样更炫酷。

在开始写之前,我们先想想应该达到一个怎样的效果,我自己罗列了一下:

  1. 能在浏览器访问网页,比如:http://localhost:8000/index.html,这样子能解析自己预先准备的index.html:
  2. 能读取文本信息:比如:http://localhost:8000/17.xml,这样子能直接读取文本显示。
  3. 能处理请求异常:比如:http://localhost:8000/we.html,我们不支持这个地址,就会弹出File not found
  4. 持续接受用户的请求(无论正确/错误)

根据我自己对于HTTP服务器的理解,设计出HTTP服务器的生命周期如下:

  • Step1-read:读取socket数据流
  • Step2-parser:解析数据流,分析报头,得到客户给予服务器的命令语义
  • Step3-process:处理客户给的命令语义,返回处理结果
  • Step4-response:把处理结果打包,增加报头
  • Step5-write:写入socket数据流

以上的五步,我们完全可以把他作为一个线程独立出来,我希望:每一次客户向服务器发起请求的时候,我们就独立开辟一条线程去处理客户的需求,说白了,用子线程去独立执行以上的过程。

开始撸码

你应该遵循一下函数设计的规范:

我个人的编码规范,对于一个有返回对象的函数,应该需要设计如下:

    public Object functionExample(Object params){
        Object object1 = null;
        Obejct object2 = null;
        try{
            object1 = new Object();
            object2 = new Object();
            //Do something
            return object1;//Here is the return
        }catch(Exception e){
            e.printStackTrace();
        }finally{
            try{
                //Close object1
                //CLose object2
            }catch(Exception e){
                e.printStackTrace();
            }
        }
        return null;
    }

部分代码如果想偷懒偶尔不写close,我们也可以用这个

@SuppressWarnings("resource") // Warnings iggnore

先写服务器的主线程框架:

package csdn_fake_server;

import java.io.*;
import java.util.logging.Logger;
import java.net.*;


public class Server extends Thread{
    int watchPort = 8000;
    Logger log = Logger.getLogger("SERVER-LOG-JT");//Open log

    public Server(){};
    /**
     * Server
     * @param port: The server listen port
     */
    public Server(int port) {
        watchPort = port;
    }

    //Class httpServer{} 这里将会实现我们上面提到的子线程

    @Override
    public void run() {
        super.run();
        try {
            @SuppressWarnings("resource") // Warnings iggnore
            ServerSocket serverSocket = new ServerSocket(watchPort);
            while (true) { // Waitting for clients
                Socket socket = serverSocket.accept();// Thread Join here
                //Child Thread here
                new httpServer(socket).start();// If connected, start a new server thread
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    public static void main(String[] args) {
        Server server = new Server();
        server.start();
    }
}

以上是我写出来一个自己脑海里服务器的线程框架,该类里面你应该要配备一个测试的函数main(String[] args)

我们在启动服务器线程之后,我们将利用ServerSocketaccept()通过线程拥塞来监听端口的信息,因此,我们一旦收到服务器的套接字信息之后,就可以开启我们的子线程去处理客户的需求了。

接着写服务器子线程框架实现

    class httpServer extends Thread {

        Socket currentSocket = null; //Client Socket
        InputStream inputStream = null;//Use for get header
        OutputStream outputStream = null;//Use for response

        final static int sucessStatus = 200;//HTTP response header status code
        final static int noResourceStatus = 404;//HTTP response header status code

        int resultStatus = 404; //Our response code
        long responseLength = 20; //@Attention!!! Error info length, Important

        public httpServer(Socket socket) {
            try {
                currentSocket = socket; //Get socket bridge
                inputStream = socket.getInputStream(); // Get inputstream
                outputStream = socket.getOutputStream(); // Get outputstream
            } catch (Exception e) {
                log.info("Connection aborted");
            }
        }

        // Read=>parser=>process=>response=>write
        @Override
        public void run() {
            try {
                String rawString = read(); // Get raw header
                String serverCommand = parser(rawString); // Parser the raw header
                String resultString = process(serverCommand); // Process command
                String rawResultString = response(resultString); // Pack the result
                write(rawResultString);// Write in responsed stream
                currentSocket.close();// CLose the client socket
                //End thread
            } catch (Exception e) {
                log.info("Failed connection");
                e.printStackTrace();
            }
        }

        private String read(){//Step 1
            try{
	            //Read Interface
                return null;
            }catch(Exception e){
                log.info("Read faild");
                e.printStackTrace();
            }
            return null;
        }

        /**
         * 
         * @param rawString: raw String from read()
         * @return server command from client
         */
        private String parser(String rawString) { //Step 2
            try{
            	//Parser Interface
                return null;
            }catch(Exception e){
                log.info("Parser faild");
                e.printStackTrace();
            }
            return null;
        }
        
        /**
         * 
         * @param commandString: command string from parser
         * @return
         */
        private String process(String commandString) { //Step 3   
            try{
            	//Process Interface
                return null;
            }catch(Exception e){
                log.info("Process failed");
                e.printStackTrace();
            }finally{
                try{
                    //Close IO
                }catch(Exception e){
                    log.info("Bad IO");
                    e.printStackTrace();
                }
            }
			return null;
        }
        
        private String response(String resultString) { // Step 4
            try{
	            // response Interface
                return null;
            }catch(Exception e){
                log.info("response failed");
                e.printStackTrace();
            }
            return null;
        }

        private void write(String rawResultString) { //Step 5
            try{
            	//Write Interface
                return;
            }catch(Exception e){
                log.info("write faild");
                e.printStackTrace();
            }
        }
    }

好了,至此,我们的子线程的框架机搭建好了,具体的五个函数还没有实现。

  • read
  • parser
  • process
  • response
  • write

我们来好好思考一下每一步需要完成些什么工作。

第一步:读取原始的报文数据

private String read();//Step 1

读取报文之前,我们应该需要知道Http的报文格式,我们尝试打开现在的代码,然后试一下在浏览器向我们这个还没完成的服务器发送请求:http://localhost:8000/

在这里插入图片描述
如果我们在浏览器发出的请求一般都是GET请求了,对报文数据是在下一步,我们不关心客户给我们发来的数据是什么,我们在这个步骤值考虑把我们的数据流读入进服务器就是了。

因为HTTP的报文数据以“\r\n”作为分割,我们可以选择读取完所有的报文之后,再处理,也可以我们简单地读取第一行就行了,我们可以选择用BufferedReader的IO工具来处理,因为以行作为单位这样的话就更加方便了。

我们写出我们的read接口:

        private String read(){//Step 1
            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
            try{
                String infoRead = bufferedReader.readLine(); // We only get the line1 as: GET /index.html HTTP/1.1
                log.info(infoRead);
                return infoRead;
            }catch(Exception e){
                log.info("Read faild");
                e.printStackTrace();
            }
            return null;
        }

我这里偷懒没有Close IO,为了控制博文的长度,还是希望以后能加上。

第二步:解析报文

因为我们在上一步读取数据的时候偷懒只读一行了,因此,我们处理解析的时候也是一笔带过就是了,用空格分开GET /index.html HTTP/1.1

        /**
         * @param rawString: raw String from read()
         * @return server command from client
         */
        private String parser(String rawString) { //Step 2
            try{
                String[] split = rawString.split(" ");
                if (split.length != 3) { //@example as: GET /index.html HTTP/1.1
                    throw new NullPointerException();
                }
                return split[1]; // Get path
            }catch(Exception e){
                log.info("Parser faild");
                e.printStackTrace();
            }
            return null;
        }

第三步:处理客户语义

通过上面的解析报文,我们已经知道了客户需要请求一个地址了。因此,我们根据可以给出的地址来从后台读取文件:

		/**
         * @param commandString: command string from parser
         * @return
         */
        private String process(String commandString) { //Step 3  
            FileReader fileReader = null;
            BufferedReader bufferedReader = null;   
            try{
                log.info(commandString);//Show request in server panel
                if(commandString.equals("/")){ //Default get resource
                    commandString = "index.html";
                }
                File file = new File("src/simple_webserver_lab/template/"+commandString);
                fileReader = new FileReader(file);
                bufferedReader = new BufferedReader(fileReader);
                StringBuffer stringBuffer = new StringBuffer();
                String line;
                while((line = bufferedReader.readLine())!=null){
                    stringBuffer.append(line+"\r\n");
                }
                resultStatus = 200; //Success
                responseLength = file.length(); // This is must be added for HTPP Header
                String result = stringBuffer.toString();
                return result;
            }catch(Exception e){
                resultStatus = 404;
                log.info("Process failed");
                e.printStackTrace();
            }finally{
                try{
                    bufferedReader.close();
                    fileReader.close();
                }catch(Exception e){
                    resultStatus = 404;
                    log.info("Bad IO");
                    e.printStackTrace();
                }
            }
            return null;
        }

第四步:(巨坑)响应客户返回数据包

HTTP报头的content length是一个大坑

  • 需要两次的\r\n
  • 长度必须匹配!!!!

看这里:

<h1>File not found...</h1>

为毛后面写这些点,就是为了填补上不够数的字节,如果你差一个字节,都会不出来结果的,浏览器会报错的。

我们给出的数据只能比要求的content length要长!

我在前面定义了一个全局的响应长度:

        long responseLength = 20; //@Attention!!! Error info length, Important

像上面如果你返回:<h1>Error</h1>就会空白,因为你的数据不满足http报文的数据长度,因此我们会丢弃掉这个分组的数据了。

还有就是:HTTP/1.1 之间不要空格,否则 Firefox 系列的浏览器检查会出错哦。

响应部分的代码:

        /**
         * @param resultString
         * @return raw response result
         */
        private String response(String resultString) { // Step 4
            try{
                StringBuffer responseInfo = new StringBuffer();
                switch(resultStatus){
                    case sucessStatus:{
                        responseInfo.append("HTTP/1.1 200 ok \r\n");
                        responseInfo.append("Content-Type:text/html \r\n");
                        responseInfo.append("Content-Length:"+Long.toString(responseLength)+" \r\n\r\n");
                        responseInfo.append(resultString);
                        break;
                    }
                    case noResourceStatus:{
                        responseInfo.append("HTTP/1.1 "+Integer.toString(noResourceStatus)+" file Not Found \r\n");
                        responseInfo.append("Content-Type:text/html \r\n");
                        responseInfo.append("Content-Length:"+Long.toString(responseLength)+" \r\n\r\n");
                        responseInfo.append("<h1>File not found...</h1>");
                        break;
                    }default:{
                        break;
                    }
                }
                return responseInfo.toString();
            }catch(Exception e){
                log.info("response failed");
                e.printStackTrace();
            }
            return null;
        }

第五步:写入Socket的数据流

这个就没啥好说的了

        /***
         * @param rawResultString
         */

        private void write(String rawResultString) { //Step 5
            try{
                outputStream.write(rawResultString.getBytes());
                outputStream.flush();
                outputStream.close();
                return;
            }catch(Exception e){
                log.info("write faild");
                e.printStackTrace();
            }
        }

测试结果:

启动服务器:

    public static void main(String[] args) {
        Server server = new Server();
        server.start(); //服务器的主线程启动
    }

前提你要有一个index.html在你的指定目录。

假如我们访问根和指定访问index.html,都会显示:
在这里插入图片描述

在这里插入图片描述
后台打印:
在这里插入图片描述
假如,我们访问目录下的文本文件:17.xml,将会显示文本内容:
在这里插入图片描述
终端:
在这里插入图片描述
假如我们访问一个不存在的路径:
在这里插入图片描述
后台打印:
在这里插入图片描述
至此,我们撸完了一个简易的 Http 服务器了。

完整代码:

项目文件夹:
在这里插入图片描述
Server.java


package simple_webserver_lab;

import java.io.*;
import java.util.logging.Logger;
import java.net.*;

/**
 * @author:JintuZheng
 * @version: 1.0.00
 */

public class Server extends Thread {  


    int watchPort = 8000;
    Logger log = Logger.getLogger("SERVER-LOG-JT");

    public Server(){};
    /**
     * Server
     * @param port: The server listen port
     */
    public Server(int port) {
        watchPort = port;
    }

    /**
     * @apiNote: The child httpServer
     */
    class httpServer extends Thread {

        Socket currentSocket = null; //Client Socket
        InputStream inputStream = null;//Use for get header
        OutputStream outputStream = null;//Use for response

        final static int sucessStatus = 200;//HTTP response header status code
        final static int noResourceStatus = 404;//HTTP response header status code

        int resultStatus = 404; //Our response code
        long responseLength = 20; //@Attention!!! Error info length, Important

        public httpServer(Socket socket) {
            try {
                currentSocket = socket; //Get socket bridge
                //log.info(socket.getInetAddress().toString()); //Show ip address
                inputStream = socket.getInputStream(); // Get inputstream
                outputStream = socket.getOutputStream(); // Get outputstream
            } catch (Exception e) {
                log.info("Connection aborted");
            }
        }

        // Read=>parser=>process=>response=>write
        @Override
        public void run() {
            try {
                String rawString = read(); // Get raw header
                String serverCommand = parser(rawString); // Parser the raw header
                String resultString = process(serverCommand); // Process command
                String rawResultString = response(resultString); // Pack the result
                write(rawResultString);// Write in responsed stream
                currentSocket.close();// CLose the client socket
                //End thread
            } catch (Exception e) {
                log.info("Failed connection");
                e.printStackTrace();
            }
        }

        private String read(){//Step 1
            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
            try{
                String infoRead = bufferedReader.readLine(); // We only get the line1 as: GET /index.html HTTP/1.1
                log.info(infoRead);
                return infoRead;
            }catch(Exception e){
                log.info("Read faild");
                e.printStackTrace();
            }
            return null;
        }

        /**
         * 
         * @param rawString: raw String from read()
         * @return server command from client
         */
        private String parser(String rawString) { //Step 2
            try{
                String[] split = rawString.split(" ");
                if (split.length != 3) { //@example as: GET /index.html HTTP/1.1
                    return null;
                }
                return split[1]; // Get path
            }catch(Exception e){
                log.info("Parser faild");
                e.printStackTrace();
            }
            return null;
        }
        
        /**
         * 
         * @param commandString: command string from parser
         * @return
         */
        private String process(String commandString) { //Step 3  
            FileReader fileReader = null;
            BufferedReader bufferedReader = null;   
            try{
                log.info(commandString);//Show request in server panel
                if(commandString.equals("/")){ //Default get resource
                    commandString = "index.html";
                }
                File file = new File("src/simple_webserver_lab/template/"+commandString);
                fileReader = new FileReader(file);
                bufferedReader = new BufferedReader(fileReader);
                StringBuffer stringBuffer = new StringBuffer();
                String line;
                while((line = bufferedReader.readLine())!=null){
                    stringBuffer.append(line+"\r\n");
                }
                resultStatus = 200;
                responseLength = file.length(); // This is must be added for HTPP Header
                String result = stringBuffer.toString();
                return result;
            }catch(Exception e){
                resultStatus = 404;
                log.info("Process failed");
                e.printStackTrace();
            }finally{
                try{
                    bufferedReader.close();
                    fileReader.close();
                }catch(Exception e){
                    resultStatus = 404;
                    log.info("Bad IO");
                    e.printStackTrace();
                }
            }
            return null;
        }
        
        /**
         * 
         * @param resultString
         * @return raw response result
         */
        private String response(String resultString) { // Step 4
            try{
                StringBuffer responseInfo = new StringBuffer();
                switch(resultStatus){
                    case sucessStatus:{
                        responseInfo.append("HTTP/1.1 200 ok \r\n");
                        responseInfo.append("Content-Type:text/html \r\n");
                        responseInfo.append("Content-Length:"+Long.toString(responseLength)+" \r\n\r\n");
                        responseInfo.append(resultString);
                        break;
                    }
                    case noResourceStatus:{
                        responseInfo.append("HTTP/1.1 "+Integer.toString(noResourceStatus)+" file Not Found \r\n");
                        responseInfo.append("Content-Type:text/html \r\n");
                        responseInfo.append("Content-Length:"+Long.toString(responseLength)+" \r\n\r\n");
                        responseInfo.append("<h1>File not found...</h1>");
                        break;
                    }default:{
                        break;
                    }
                }
                return responseInfo.toString();
            }catch(Exception e){
                log.info("response failed");
                e.printStackTrace();
            }
            return null;
        }
        /***
         * 
         * @param rawResultString
         */

        private void write(String rawResultString) { //Step 5
            try{
                outputStream.write(rawResultString.getBytes());
                outputStream.flush();
                outputStream.close();
                return;
            }catch(Exception e){
                log.info("write faild");
                e.printStackTrace();
            }
        }
    }

    @Override
    public void run() {
        super.run();
        try {
            @SuppressWarnings("resource") // Warnings iggnore
            ServerSocket serverSocket = new ServerSocket(watchPort);
            while (true) { // Waitting for clients
                Socket socket = serverSocket.accept();// Thread Join here
                new httpServer(socket).start();// If connected, start a new server thread
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

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

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值