Socket实现轻量级HTTP服务器

  1. package com.http;
    
    import java.io.IOException;
    import java.net.ServerSocket;
    import java.net.Socket;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    
    /**
     * 此服务支持图片静态资源请求、get、post(application/x-www-form-urlencoded)方法
     * post中的其它编码格式multipart/form-data、application/json暂时未实现
     */
    public class HttpServer implements Runnable {
    
        @Override
        public void run() {
            ServerSocket socket = null;
            Socket clientSocket=null;
            ExecutorService executor = Executors.newCachedThreadPool();
    
            try {
                socket = new ServerSocket(Constants.SERVER_PORT);
                while(true) {
                    clientSocket = socket.accept();
                    System.out.println("有新的连接接入");
                    executor.submit(new MyHttpHandler(clientSocket));//开户启一个新线程处理新的连接请求,一个线程可以处理同一条连接上的多个请求,而不是每个请求创建一个连接
                }
            } catch (IOException e) {
                e.printStackTrace();
                System.out.println("启动http 服务器失败 端口:" + Constants.SERVER_PORT);
                System.exit(1);
            }
        }
    
        public static void main(String[] args){
            System.out.println("服务已启动");
            System.out.println("测试get  http://localhost:1234/   http://localhost:1234/path?name=value");
            System.out.println("测试post http://localhost:1234/   http://localhost:1234/path?name=value 注意此方法需要自建表单或使用postman测试");
            System.out.println("测试图片请求 http://localhost:1234/static/1.png");
            new HttpServer().run();
        }
    }
    
  2. package com.http;
    
    import java.io.IOException;
    import java.net.Socket;
    
    public class MyHttpHandler extends HttpHandler {
        public MyHttpHandler(Socket socket) {
            super(socket);
        }
    
        @Override
        public void get(HttpRequest httpRequest, HttpResponse httpResponse) {
            try {
                if (httpRequest.getRequestPath().equals("/")) {
                    httpResponse.sendHtml("<html>\n" +
                            "\t<head>\n" +
                            "\t\t<meta charset=\"utf-8\" />\n" +
                            "\t\t<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n" +
                            "\t\t<title>主页</title>\n" +
                            "\t</head>\n" +
                            "\t<body>\n" +
                            "返回网页内容省略..." +
                            "\t</body>\n" +
                            "</html>");
    
                } else {
                    httpResponse.sendPlain("返回普通文字 请求路径:"+httpRequest.getRequestPath());
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    
        @Override
        public void post(HttpRequest httpRequest, HttpResponse httpResponse) {
            try {
                  httpResponse.sendPlain("post 请求路径:"+httpRequest.getRequestPath());
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    
  3. package com.http;
    
    import java.io.BufferedInputStream;
    import java.io.File;
    import java.io.FileInputStream;
    import java.io.IOException;
    import java.net.ProtocolException;
    import java.net.Socket;
    import java.net.SocketException;
    
    public abstract class HttpHandler implements Runnable {
    
        private Socket clientSocket;
    
        public HttpHandler(Socket socket) {
            this.clientSocket = socket;
        }
    
        @Override
        public void run() {
            HttpRequest httpRequest = null;
            HttpResponse httpResponse = null;
            try {
                //当浏览器主动断开链接或关闭浏览器时,HttpRequst会抛出ProtocolException跳出循环,并关闭clientSocket
                //同一个clientSocket连接,可能会发送多个请求,也就是http1.1默认的长连接Connection: keepAlive
                //服务器默认不会主动断开连接,什么时候断开由浏览器决定,一般来说当网页中的最后一个资源请求完毕,等一会没有新请求就关闭此连接
                while (true) {
                    httpRequest = new HttpRequest(clientSocket);
                    httpResponse = new HttpResponse(clientSocket);
                    handleRequest(httpRequest, httpResponse);
                }
            } catch (SocketException e) {
                e.printStackTrace();
            } catch (ProtocolException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            } catch (Exception e) {
                e.printStackTrace();
            } catch (Throwable e) {
                e.printStackTrace();
            } finally {
                try {
                    clientSocket.close();
                    System.out.println("java.net.SocketException socket顺利close()");
                } catch (IOException e1) {
                    e1.printStackTrace();
                    System.out.println("java.net.SocketException socket close()不顺利");
                }
            }
    
        }
    
        public void handleRequest(HttpRequest httpRequest, HttpResponse httpResponse) {
            if (RequestStatusLine.GET.equalsIgnoreCase(httpRequest.getMethod())) {
                if (httpRequest.getRequestPath().indexOf(Constants.REQUEST_RESOURCE_PATH) == 1) { //处理静态资源请求,图片、word文档、pdf...
                    handlerStaticResource(httpRequest, httpResponse);
                } else {                                                                          //处理正常get请求
                    get(httpRequest, httpResponse);
                }
            } else if (RequestStatusLine.POST.equalsIgnoreCase(httpRequest.getMethod())) {
                post(httpRequest, httpResponse);
            } else {
                throw new RuntimeException("暂时不支持请求方法 " + httpRequest.getMethod());
            }
        }
    
        public abstract void get(HttpRequest httpRequest, HttpResponse httpResponse);
    
        public abstract void post(HttpRequest httpRequest, HttpResponse httpResponse);
    
        /**
         * 处理静态资源请求,需要提前配置静态资源路径Constants.REQUEST_RESOURCE_PATH 的值
         * @param httpRequest
         * @param httpResponse
         */
        private void handlerStaticResource(HttpRequest httpRequest, HttpResponse httpResponse) {
            System.out.println("请求的是资源文件");
    
            String fileName = httpRequest.getRequestPath().substring(8);
            File file = new File(Constants.LOCAL_RESOURCE_PATH + File.separator + fileName);
    
            byte[] bytes = new byte[1024000];
    
            try {
    
                FileInputStream inputStream = new FileInputStream(file); //定位资源文件并转换成stream
                BufferedInputStream bufferedInputStream = new BufferedInputStream(inputStream); //增强文件读取速度,添加缓冲区
    
                httpResponse.sendMetadata(HttpResponse.checkContentType(fileName),file.length(),false);//1.先发送响应报文头信息
                int length;
                while ((length=bufferedInputStream.read(bytes))!=-1){                                           //2. 后发送dataBody
                    System.out.println("length="+length);
                    httpResponse.send(bytes,0,length);
                }
    
    //            inputStream.close();
                bufferedInputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    
    }
    
  4. package com.http;
    
    import java.io.BufferedInputStream;
    import java.io.IOException;
    import java.io.InputStream;
    import java.net.ProtocolException;
    import java.net.Socket;
    import java.net.SocketException;
    import java.util.HashMap;
    import java.util.Map;
    
    public class HttpRequest {
        private RequestStatusLine requestStatusLine;
        private Map<String, String> headers;
        private byte[] bodyData;
        private InputStream inputStream;
        static int CACHE_MAX_LENGTH = 10240; //缓存header信息10kb够大了吧
    
    
        public HttpRequest(Socket socket) throws IOException {
    
                //读取接收的stream
                // 1. 读取header和requestLine
                this.inputStream= socket.getInputStream();
                BufferedInputStream bin = new BufferedInputStream(inputStream);
                String metadata = generateMetadata(bin);
    
                parseMetadata(metadata);
                System.out.println("demo/common/http/HttpRequest.java:32 "+Thread.currentThread().getName()+" "+getRequestPath());
                //如果是get请求,按照http协议body是没有数据的
                if (getMethod().equalsIgnoreCase(RequestStatusLine.GET)) { return;}
    
                // 2. 读取requestBody
                byte[] bodyData = new byte[getContentLength()];
                for (int i = 0; i < bodyData.length; i++) {
                    bodyData[i] = (byte) bin.read();
                }
                String contentType = getContentType();
                switch (contentType) {
                    case "application/x-www-form-urlencoded":
                        this.bodyData=bodyData;
                        String requestParameter = new String(bodyData);
                        System.out.println("requestParameter " + requestParameter);
                        break;
                    case "multipart/form-data":
                        throw new RuntimeException("不支持的post multipart/form-data 编码类型 等待重写");
    //                    break;
                    case "application/json":
                        throw new RuntimeException("不支持的post application/json 编码类型 等待重写");
    //                    break;
                    case "text/xml":
                        throw new RuntimeException("不支持的post text/xml 编码类型 等待重写");
    //                    break;
                    default:
                        throw new RuntimeException("不支持的请求方法");
                }
        }
    
        public RequestStatusLine getRequestStatusLine() {
            return requestStatusLine;
        }
    
        public Map<String, String> getHeaders() {
            return headers;
        }
    
        public byte[] getBodyData() {
            return bodyData;
        }
    
        private String generateMetadata(BufferedInputStream bis) throws SocketException,IOException {
            byte[] cache = new byte[CACHE_MAX_LENGTH];
            int read;
            int index = 0;
            String metadata = null;
            while ((read = bis.read()) != -1) {
    
                if ((read == '\r' && bis.read() == '\n') || (read == '\n' && bis.read() == '\r')) {
                    cache[index] = '\r';
                    cache[++index] = '\n';
                    if ((cache[index - 2] == '\n' && cache[index - 3] == '\r') || (cache[index - 3] == '\r' && cache[index - 2] == '\n')) {
                        //当连续读取到\r\n\r\n四个字符时,就表示http协议报文结束了,也可以设置读取超时抛出异常防止所谓的ddos攻击
                        return metadata = new String(cache, 0, index + 1);
                    }
                } else {
                    cache[index] = (byte) read;
                }
                index++;
            }
            bis.close();
            //当浏览器主动断开socket连接时,79行bis.read()将不再堵塞并返回-1,
            // 如果访问者不是浏览器而是一个java.net.Socket,此Socket对象调用close()方法也会触发bis.read()解除堵塞并返回-1
            throw new ProtocolException("客户断主动开链接"+Thread.currentThread().getName());
        }
    
        private void parseMetadata(String metadata) {
            String[] metaLine = metadata.split("\r\n");
            Map<String, String> headers = new HashMap<>();
            for (int i = 0; i < metaLine.length; i++) {
                if (i == 0) {
                    //解析请求行
                    String[] requestLine = metaLine[0].split(" ");
                    this.requestStatusLine = new RequestStatusLine(requestLine[0], requestLine[1], requestLine[2]);
                } else {
                    //解析header
                    String[] header = metaLine[i].split(": ");
                    headers.put(header[0].toLowerCase(), header[1]);
                }
            }
            this.headers = headers;
        }
    
        public String getMethod() {
            return requestStatusLine.getMethod();
        }
    
        public int getContentLength() {
            String content_length = headers.get("content-length");
            return content_length == null ? 0 : Integer.parseInt(content_length);
        }
    
        public String getContentType() {
            String content_type = headers.get("content-type");
            if (content_type == null) {
                throw new RuntimeException("http请求没有指定Content-Type类型");
            }
            return content_type;
        }
    
        public String getRequestPath() {
            return requestStatusLine.getRequestPath();
        }
    
        public InputStream getInputStream() {
            return inputStream;
        }
    }
    
  5. package com.http;
    
    import java.io.IOException;
    import java.io.OutputStream;
    import java.net.Socket;
    
    public class HttpResponse {
        static String TEXT_HTML="text/html";
        static String TEXT_PLAIN="text/plain";
        static String IMAGE_JPG="image/jpg";
        static String IMAGE_JPEG="image/jpeg";
        static String IMAGE_PNG="image/png";
    
        static String DEFAULT_CHARSET="UTF-8";
        private OutputStream out;
    
        public  HttpResponse(Socket clientSocket) throws IOException {
                this.out=clientSocket.getOutputStream();
        }
    
        public void sendPlain(String text) throws IOException {
            send(TEXT_PLAIN ,text);
        }
    
        public void sendHtml(String html) throws IOException {
            send(TEXT_HTML ,html);
        }
    
        /**
         * 主要用于发送图片、文档
         * @param contentType
         * @param body
         * @throws IOException
         */
        public void sendStaticResource(String contentType, byte[] body) throws IOException {
            send(contentType,body,false);
        }
    
        public void sendMetadata(String contentType,long bodyLength,boolean useUtf8) throws IOException {
            //制作响应报文
            StringBuffer response = new StringBuffer();
            //响应状态行
            response.append("HTTP/1.0 200 OK\r\n");
            //响应头...
    //      response.append("Content-type: "+contentType+"; charset=UTF-8\r\n");
    //      response.append("Content-type: "+contentType+"\r\n");
            response.append(useUtf8 ? "Content-type: "+contentType+"; charset=UTF-8\r\n" : "Content-type: "+contentType+"\r\n");
            response.append("Content-Length: "+bodyLength+"\r\n");
            response.append("Connection: keep-alive\r\n");
            response.append("\r\n");
            //返回响应头信息
            out.write(response.toString().getBytes());
        }
    
        private void send(String contentType, byte[] body,boolean useUtf8) throws IOException {
            //返回响应头信息
            sendMetadata( contentType,body.length, useUtf8);
            //返回body内容
            out.write(body);
        }
    
        private void send(String contentType,String body) throws IOException {
          send(contentType,body.getBytes(),true);
        }
    
        public void send(byte b) throws IOException {
            out.write(b);
        }
        public void send(byte[] bytes, int off, int len) throws IOException {
            out.write(bytes,off,len);
        }
        public void close(){
            try {
                out.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    
        public static String checkContentType(String fileName){
            switch(fileName.substring(fileName.lastIndexOf('.'))){
                case ".png":
                    return IMAGE_PNG;
                case ".jpg":
                    return IMAGE_JPG;
                case ".jpeg":
                    return IMAGE_JPEG;
            }
            return "";
        }
    
    }
    
  6. package com.http;
    
    import java.util.HashMap;
    import java.util.Map;
    
    public class RequestStatusLine {
        public static String GET="get";
        public static String POST="post";
    
        private String method; //暂时只支持 get , post
        private String requestPath="/"; //默认"/"
        private String protocol; // HTTP/1.0 , HTTP/1.1
        Map<String, String> queryParameters;
        public String getMethod() {
            return method;
        }
    
        public String getRequestPath() {
            return requestPath;
        }
    
        public String getProtocol() {
            return protocol;
        }
    
        public RequestStatusLine(){}
    
        public RequestStatusLine(String method, String requestPath, String protocol) {
            this.method = method;
            this.protocol = protocol;
            this.queryParameters = parseQueryString(requestPath);
        }
    
        @Override
        public String toString() {
            return method+" "+requestPath+" "+protocol;
        }
    
        public  Map<String, String> parseQueryString(String url) {
            Map<String, String> map = new HashMap<>();
            String[] pathAndparameter = url.split("\\?");
            this.requestPath=pathAndparameter[0];
            if (pathAndparameter.length<=1){
                return null;
            }
            String[] nameValuePairs = pathAndparameter[1].split("&");
    
            for (int i = 0; i < nameValuePairs.length; i++) {
                int index = nameValuePairs[i].indexOf('=');
                String name = nameValuePairs[i].substring(0, index);
                String value = nameValuePairs[i].substring(index + 1);
                map.put(name, value);
            }
            return map;
        }
    }
    
  7. package com.http;
    
    public class ResponseStatusLine {
    
        private String protocol; // HTTP/1.0 , HTTP/1.1
        private int statusCode; //200,302,404,500
        private String statusStr; // OK
    
        public String getProtocol() {
            return protocol;
        }
    
        public void setProtocol(String protocol) {
            this.protocol = protocol;
        }
    
        public int getStatusCode() {
            return statusCode;
        }
    
        public void setStatusCode(int statusCode) {
            this.statusCode = statusCode;
        }
    
        public String getStatusStr() {
            return statusStr;
        }
    
        public void setStatusStr(String statusStr) {
            this.statusStr = statusStr;
        }
        public ResponseStatusLine(){}
    
        public ResponseStatusLine(String protocol, int statusCode, String statusStr) {
            this.protocol = protocol;
            this.statusCode = statusCode;
            this.statusStr = statusStr;
        }
    
        @Override
        public String toString() {
            return protocol+" "+statusCode+" "+statusStr;
        }
    }
    
  8. package com.http;
    
    public class Constants {
        public static final String LOCAL_RESOURCE_PATH = "F:\\image\\other";
        /**
         * http://localhost:1234/static/photo.png
         * 此路径下的请求全部返回文件资源
         * @return
         */
        public static final String REQUEST_RESOURCE_PATH = "static";
    
        public static final int SERVER_PORT = 1234;
    }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值