zoukankan      html  css  js  c++  java
  • ServerSocket实现超简单HTTP服务器

    1、相关知识简介

    HTTP协议

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

    这里写图片描述

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

    HTTP报文

    HTTP报文包含头部和请求体,请求体内容可为空。请求头与请求体用单独的空行分隔,即” ”。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
    " +  //响应头第一行
                            "Content-Type: text/html; charset=utf-8
    " +  //简单放一个头部信息
                            "
    " +  //这个空行是来分隔请求头与请求体的
                            "<h1>这是响应报文</h1>
    ").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
    " +
                            "Content-Type: text/html; charset=utf-8
    " +
                            "
    " +
                            "<h1>这是响应报文</h1>
    ").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("
    ");
            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 = "头部信息
    ";
            for (Map.Entry<String, String> entry : headers.entrySet()) {
                respStr += "名称: " + entry.getKey() + ", 值: " + entry.getValue() + "<br/>";
            }
    
            respStr += "
    body信息
    ";
            for (Map.Entry<String, String> entry : parameters.entrySet()) {
                respStr += "名称: " + entry.getKey() + ", 值: " + entry.getValue() + "<br/>";
            }
    
            socket.getOutputStream().
                    write(("HTTP/1.1 200 OK
    " +
                            "Content-Type: text/html; charset=utf-8
    " +
                            "
    " +
                            "<h1>这是响应报文</h1>
    " + respStr).getBytes());
        }
    }

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

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

    小结

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

  • 相关阅读:
    window server2019+vmware16+Ubuntu20部署网站记录
    CentOS7源码安装MySQL
    CentOS7源码安装Python、virtualenv虚拟环境安装、uwsgi安装配置
    CentOS7 源码安装Nginx及Nginx基本管理设置
    Ubuntu 64位桌面版 16.04.1 设置桥接模式和固定静态IP方法
    Windows 下日志保存至Linux rsyslog日志服务器
    python 接参数的一个小坑
    历旧服务器配置注意事项
    gitlab设置邮件通知
    Linux基础篇之目录与文件
  • 原文地址:https://www.cnblogs.com/yunlongaimeng/p/9470865.html
Copyright © 2011-2022 走看看