ServerSocket实现超简单HTTP服务器

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/Kurozaki_Kun/article/details/78755869

1、相关知识简介

HTTP协议

HTTP是常用的应用层协议之一,是面向文本的协议。HTTP报文传输基于TCP协议,TCP协议包含头部与数据部分,而HTTP则是包含在TCP协议的数据部分,如下图

这里写图片描述

HTTP报文本质上是一个TCP报文,数据部分携带的内容为HTTP报文,HTTP报文多数情况下是一串文本,当然也可能携带二进制信息。

HTTP报文

HTTP报文包含头部和请求体,请求体内容可为空。请求头与请求体用单独的空行分隔,即”\r\n”。HTTP头部结构如下:

这里写图片描述

这里写图片描述

当报文为请求报文时,第一行信息为 {方法} {URI} {HTTP版本}
方法通常为GET, POST,URI为URL后面携带的参数信息,HTTP版本表示当前使用的HTTP版本。

当报文为响应报文时,第一行的信息为 {HTTP版本} 状态
HTTP版本同上,下面是部分常见的状态码

状态码 英文名称 含义
200 OK 请求成功
304 Not Modified 所请求的资源未修改
400 Bad Request 客户端请求的语法错误,服务器无法理解
403 Forbidden 服务器理解请求客户端的请求,但是拒绝执行此请求
404 Not Found 服务器无法根据客户端的请求找到资源(网页),常说的404错误就是指这个
405 Method Not Allowed 客户端请求中的方法被禁止
502 Bad Gateway 充当网关或代理的服务器,从远端服务器接收到了一个无效的请求


报文从第二行开始均为 {字段名}: {字段值} 的格式。字段名通常是英文字母与”-“的组合,有常用的几个,有时也可以使用自定义字段名,值得注意的是字段名最好不要包含空格,虽然我Postman上模拟没问题,但在Chrome上试解析会出问题。

HTTP报文的请求体就是一段数据,没有严格的格式限制,较为随意,但如果在头部声明Content-Type为Multipart/form-data后就会有一定的格式规范,具体可以看看我之前写的一篇文章
http://blog.csdn.net/kurozaki_kun/article/details/78646960

Socket

Socket是对TCP/IP的封装,为程序员提供了面向传输层及以上层的编程。Java中关于Socket的类主要是Socket,DatagramSocket,ServerSocket,还有NIO对应的类,这里实现主要基于前三者。Socket能够建立端到端的同通信。其实总结一句话,就是使用Socket能够帮助程序员传输TCP/UDP报文。

2、基于Socket实现简单的HTTP服务器

ServerSocket监听端口

ServerSocket用于监听特定端口,调用accept()方法会阻塞当前线程,直到接收到一个Socket,而我们需要处理所接收到的Socket。下面先写出一个大致的框架

class ServerListeningThread extends Thread {

    private int bindPort;
    private ServerSocket serverSocket;

    public ServerListeningThread(int port) {
        this.bindPort = port;
    }

    @Override
    public void run() {
        try {
            serverSocket = new ServerSocket(bindPort);
            while (true) {
                Socket rcvSocket = serverSocket.accept();

                //单独写一个类,处理接收的Socket,类的定义在下面
                HttpRequestHandler request = new HttpRequestHandler(rcvSocket);
                request.handle();

                rcvSocket.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            //最后要确保以下把ServerSocket关闭掉
            if (serverSocket != null && !serverSocket.isClosed()) {
                try {
                    serverSocket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

class HttpRequestHandler {
    private Socket socket;

    public HttpRequestHandler(Socket socket) {
        this.socket = socket;
    }

    public void handle() throws IOException {
        //TODO 这里写处理接收到的socket的逻辑
    }
}

回送简单的HTTP报文

接下来的关注点应该在如何处理Socket上,先从最简单的开始做起,不管socket里的是什么,都一律只回复一个响应报文,上面的handle()方法处理应该如下


class HttpRequestHandler {
    private Socket socket;

    public HttpRequestHandler(Socket socket) {
        this.socket = socket;
    }

    public void handle() throws IOException {
        socket.getOutputStream().
                write(("HTTP/1.1 200 OK\r\n" +  //响应头第一行
                        "Content-Type: text/html; charset=utf-8\r\n" +  //简单放一个头部信息
                        "\r\n" +  //这个空行是来分隔请求头与请求体的
                        "<h1>这是响应报文</h1>\r\n").getBytes());
    }
}

然后来试试效果,在main函数调用一下,这里监听8888端口

public static void main(String[] args) {
    new ServerListeningThread(8888).start();
}

用浏览器打开 127.0.0.1:8888 或 localhost:8888,能够显示下面结果
这里写图片描述

可以见到刚才通过socket回送的响应报文被浏览器成解析了,红色箭头位置是自己添加的头部信息。

读取请求并回送

一个HTTP请求真正处理起来还是比较繁琐的,这里只介绍下简单的情景,例如请求报文带有POST参数,先读取socket的数据,并控制台输出一下HTTP请求的报文是什么样的

class HttpRequestHandler {
    //此处代码省略

    public void handle() throws IOException {
        //获取输入流,读取数据
        StringBuilder builder = new StringBuilder();
        InputStreamReader isr = new InputStreamReader(socket.getInputStream());
        char[] charBuf = new char[1024];
        int mark;
        while ((mark = isr.read(charBuf)) != -1) {
            builder.append(charBuf, 0, mark);
            if (mark < charBuf.length) {
                break;
            }
        }
        System.out.println(builder.toString());


        socket.getOutputStream().
                write(("HTTP/1.1 200 OK\r\n" +
                        "Content-Type: text/html; charset=utf-8\r\n" +
                        "\r\n" +
                        "<h1>这是响应报文</h1>\r\n").getBytes());
    }
}

使用postman向8888端口发送一个携带POST参数的HTTP请求,如下
这里写图片描述

控制台输出结果为
这里写图片描述

其中三个提交的参数在body的表现形式为 参数名=值,多个参数用&连接成字符串,该字符串占一行。下面可以使用字符串操作将这些信息解析出来,并且将解析结果回送回去。

class HttpRequestHandler {
    //此处代码省略...

    public void handle() throws IOException {

        StringBuilder builder = new StringBuilder();
        InputStreamReader isr = new InputStreamReader(socket.getInputStream());
        char[] charBuf = new char[1024];
        int mark = -1;
        while ((mark = isr.read(charBuf)) != -1) {
            builder.append(charBuf, 0, mark);
            if (mark < charBuf.length) {
                break;
            }
        }
        if (mark == -1) {
            return;
        }

        Map<String, String> headers = new HashMap<>();
        Map<String, String> parameters = new HashMap<>();

        String[] splits = builder.toString().split("\r\n");
        int index = 1;

        //处理header
        while (splits[index].length() > 0) {
            String[] keyVal = splits[index].split(":");
            headers.put(keyVal[0], keyVal[1].trim());
            index++;
        }
        String body = splits[index + 1];
        String[] bodySplits = body.split("&");

        //处理body的参数
        for (String str : bodySplits) {
            String[] param = str.split("=");
            parameters.put(param[0], param[1]);
        }

        String respStr = "头部信息\r\n";
        for (Map.Entry<String, String> entry : headers.entrySet()) {
            respStr += "名称: " + entry.getKey() + ", 值: " + entry.getValue() + "<br/>";
        }

        respStr += "\r\nbody信息\r\n";
        for (Map.Entry<String, String> entry : parameters.entrySet()) {
            respStr += "名称: " + entry.getKey() + ", 值: " + entry.getValue() + "<br/>";
        }

        socket.getOutputStream().
                write(("HTTP/1.1 200 OK\r\n" +
                        "Content-Type: text/html; charset=utf-8\r\n" +
                        "\r\n" +
                        "<h1>这是响应报文</h1>\r\n" + respStr).getBytes());
    }
}

使用POST方法带参访问8888端口,其返回结果如下
这里写图片描述

在这基础上,还可以根据提交参数查询数据库等等操作,一个成熟的服务器实际上已经封装好了如上的解析步骤,然后监听主机的80端口(即HTTP默认端口),真正实现一个服务器要处理的情况远比这里讲述的多,例如处理文件传输等等。

小结

这里主要使用ServerSocket和Socket来实现,实际上还可以使用NIO的ServerSocketChannel和SocketChannel。服务器处理请求的步骤通常就是 监听端口->收到请求->处理->响应请求,中间的处理会有多层的步骤。

猜你喜欢

转载自blog.csdn.net/Kurozaki_Kun/article/details/78755869