仅使用Java SE API创建简单的Java HTTP服务器

410

有没有一种方法可以在Java中使用Java SE API创建一个非常基本的HTTP服务器(仅支持GET / POST),而无需编写手动解析HTTP请求和手动格式化HTTP响应的代码? Java SE API很好地封装了HttpURLConnection中的HTTP客户端功能,但是否存在类似于HTTP服务器功能的模拟器?

只是为了明确,我对许多在线ServerSocket示例的问题在于它们自己进行请求解析/响应格式化和错误处理,这是繁琐、容易出错且不太全面的,我正试图避免出现这些问题。


4
嗯...简短的回答是不行。如果你想要处理post和get请求而不需要手动编写http头,那么你可以使用servlets。但那是Java EE。如果你不想使用这样的东西,那么套接字和手动解析是我所知道的唯一的其他选择。 - Matt Phillips
5
我知道这并不符合 Stack Overflow 的精神,但我想劝你重新考虑一下对 Java EE API 不喜欢的态度。正如一些答案所提到的,有一些非常简单的实现,比如 Jetty,可以让您将 Web 服务器嵌入到独立应用程序中,同时仍然利用 Servlet API。如果您绝对不能出于某种原因使用 Java EE API,则请忽略我的评论 :-) - Chris Thompson
1
“Servlets”并不是真正的“Java EE”。它们只是一种编写插件的方式,可以在响应消息活动(现在通常是HTTP请求)时由周围应用程序调用。提供一个Servlet托管环境“仅使用Java SE API”正是Jetty和Tomcat所做的。当然,您可能希望放弃不需要的复杂性,但那么您可能需要决定GET / POST的允许属性和配置的子集。除了特殊的安全/嵌入式问题外,这通常不值得。 - David Tonhofer
1
在做决定之前,浏览一下这个HTTP服务器列表可能是值得的。http://java-source.net/open-source/web-servers - user1191027
23个回答

578
自Java SE 6起,Sun Oracle JRE中内置了HTTP服务器。Java 9模块名称为jdk.httpservercom.sun.net.httpserver package summary概述了相关类并提供了示例。
以下是从官方文档中复制粘贴的入门示例。只需将其复制粘贴并在Java 6+上运行即可。
(对于所有试图编辑它的人,请不要这样做,因为这是一个复制粘贴的代码,并非我的代码。此外,除非原始来源已更改,否则您不应编辑引用)
package com.stackoverflow.q3732109;

import java.io.IOException;
import java.io.OutputStream;
import java.net.InetSocketAddress;

import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;

public class Test {

    public static void main(String[] args) throws Exception {
        HttpServer server = HttpServer.create(new InetSocketAddress(8000), 0);
        server.createContext("/test", new MyHandler());
        server.setExecutor(null); // creates a default executor
        server.start();
    }

    static class MyHandler implements HttpHandler {
        @Override
        public void handle(HttpExchange t) throws IOException {
            String response = "This is the response";
            t.sendResponseHeaders(200, response.length());
            OutputStream os = t.getResponseBody();
            os.write(response.getBytes());
            os.close();
        }
    }

}
需要翻译的内容如下:

请注意,他们示例中的response.length()部分是错误的,应该是response.getBytes().length。即使如此,getBytes()方法也必须明确指定字符集,然后在响应头中指定该字符集。遗憾的是,虽然对于初学者来说可能会误导,但这毕竟只是一个基本的启动示例。

执行它并转到http://localhost:8000/test,您将看到以下响应:

这是响应


关于使用com.sun.*类,请注意,与一些开发人员认为的相反,这绝对不被禁止在众所周知的FAQWhy Developers Should Not Write Programs That Call 'sun' Packages中。该FAQ涉及Oracle JRE内部使用的sun.*包(例如sun.misc.BASE64Encoder),如果在不同的JRE上运行它,则会杀死您的应用程序,而不是com.sun.*包。Sun/Oracle也仅像Apache等其他公司一样在Java SE API之上开发软件。此外,每个JDK中都必须存在此特定的HttpServer,因此不存在像使用sun.*包时可能发生的“可移植性”问题。当涉及某个Java API的实现(例如GlassFish(Java EE实现),Mojarra(JSF实现),Jersey(JAX-RS实现)等)时,仅在禁止使用com.sun.*类时才会受到鼓励(但不是禁止)。


22
@Waldheinz和@Software一样,你混淆了sun.*com.sun.*。例如,你能找到有关sun.*API的任何文档吗?请看这里:http://java.sun.com/products/jdk/faq/faq-sun-packages.html 它有关于com.sun.*的内容吗?com.sun.*只是用于他们自己的公共软件,而不是Java API的一部分。他们也开发基于Java API的软件,就像其他公司一样。 - BalusC
9
我认为这是一个非常好的HTTP服务器,适用于集成测试用例。感谢您的提示! - Andreas Petersson
16
如果您正在使用Eclipse并且遇到像“访问限制:由于所需库的限制,类型HttpExchange不可访问...”这样的错误,请参考https://dev59.com/4Gox5IYBdhLWcg3wVS8J#10642163了解如何禁用该访问检查。 - Samuli Pahaoja
15
这一点在OpenJDK中也存在。 - Jason C
8
这里提到的类在OpenJDK源代码中被标记为@jdk.Exported,这意味着该API被视为公共的,并且将在Java 9上可用(由于Project Jigsaw,一些其他的com.sun.*包将变得不可用)。 - Jules
显示剩余23条评论

52

看看 NanoHttpd

NanoHTTPD是一个轻量级的HTTP服务器,旨在嵌入其他应用程序中,发布在修改后的BSD许可下。

它正在Github上开发,并使用Apache Maven进行构建和单元测试。


10
注意:NanoHTTPD 可能没有对遍历攻击的保护措施,如果它将在公共地址上提供服务,则应该检查此问题。我的意思是指像 GET /../../blahblah http/1.1 这样的请求,攻击者可以越过网站根目录并进入系统文件领域,从而获取可用于破坏或远程攻击系统的文件(例如密码文件)。 - Lawrence Dol
9
看起来已经被修复了。当前版本会在URI以".. "开头或以".. "结尾或包含"../"等字符串时生成403错误。 - Lena Schimmel
13
我不明白这个回答与这个问题有什么关联。 - kimathie

33

com.sun.net.httpserver 解决方案在不同的JRE上不具备可移植性。更好的方式是使用javax.xml.ws中的官方Web服务API来启动一个最小的HTTP服务器...

import java.io._
import javax.xml.ws._
import javax.xml.ws.http._
import javax.xml.transform._
import javax.xml.transform.stream._

@WebServiceProvider
@ServiceMode(value=Service.Mode.PAYLOAD) 
class P extends Provider[Source] {
  def invoke(source: Source) = new StreamSource( new StringReader("<p>Hello There!</p>"));
}

val address = "http://127.0.0.1:8080/"
Endpoint.create(HTTPBinding.HTTP_BINDING, new P()).publish(address)

println("Service running at "+address)
println("Type [CTRL]+[C] to quit!")

Thread.sleep(Long.MaxValue)

编辑:这真的可以工作!上面的代码看起来像Groovy语言。这里是我测试过的Java版本翻译:

import java.io.*;
import javax.xml.ws.*;
import javax.xml.ws.http.*;
import javax.xml.transform.*;
import javax.xml.transform.stream.*;

@WebServiceProvider
@ServiceMode(value = Service.Mode.PAYLOAD)
public class Server implements Provider<Source> {

    public Source invoke(Source request) {
        return  new StreamSource(new StringReader("<p>Hello There!</p>"));
    }

    public static void main(String[] args) throws InterruptedException {

        String address = "http://127.0.0.1:8080/";
        Endpoint.create(HTTPBinding.HTTP_BINDING, new Server()).publish(address);

        System.out.println("Service running at " + address);
        System.out.println("Type [CTRL]+[C] to quit!");

        Thread.sleep(Long.MAX_VALUE);
    }
}

2
+1 是因为它是可移植的。遗憾的是,您无法将响应内容类型设置为 text/xml - icza
8
您能解释一下为什么com.sun.net.HttpServer在不同版本的JRE之间无法移植吗? - javabeangrinder
4
不,我不这样认为。它在IBM的Java实现上可能行不通,也有可能在其他实现上也行不通。即使现在能够使用,内部API也可能会改变。为什么不直接使用官方的API呢? - gruenewa
3
引用另一篇帖子的评论:“这里提到的类在OpenJDK源代码中被标记为@jdk.Exported,这意味着该API被视为公共的,并且将在Java 9上可用。” - slim
5
这个链接 http://docs.oracle.com/javase/9/docs/api/java.xml.ws-summary.html 表明自Java 9起,java.xml.ws模块已被弃用。 - Erel Segal-Halevi
显示剩余6条评论

29

我很喜欢这个问题,因为这是一个持续创新的领域,并且在谈论小型(更小)设备中的嵌入式服务器时总是需要轻量级服务器。我认为答案可以分为两类。

  1. 轻服务器:使用最少的处理、上下文或会话处理服务静态内容。
  2. 小服务器:表面上具有许多httpD样式的服务器特性,越小越好。

虽然我可能会认为像 Jetty Apache Http Components Netty 和其他HTTP库更像原始的HTTP处理工具。标签是非常主观的,并取决于您为小型网站提供的内容类型。我根据问题的精神进行这种区分,特别是关于...

  • "...而不必编写手动解析HTTP请求和手动格式化HTTP响应的代码..."

这些原始工具可以让您执行此操作(如其他回答中所述)。它们确实不太适合快速制作轻型、嵌入式或迷你服务器。迷你服务器是可以为您提供类似于完全功能的Web服务器(例如, Tomcat )的功能,没有花哨的东西,低容量,99%的时间性能良好。一个轻服务器似乎比原始的短语更接近,可能具有有限的子集功能,足以使您在90%的时间内看起来很好。我的原始想法是,如果/当您达到WAR文件的级别时,我们已经离开了“小”对于看起来像大服务器的盆景服务器。

轻服务器选项:

  • Grizzly (HTTP服务器框架)
  • UniRest(多语言支持)
  • NanoHTTPD(一个文件)
  • 迷你服务器选项:

    • Spark Java ... 可以使用许多辅助结构(如过滤器、模板等)实现很好的功能。
    • MadVoc ...旨在成为盆景,确实可能是这样的 ;-)

    还需要考虑的其他事情包括身份验证,验证,国际化,使用类似FreeMaker或其他模板工具来呈现页面输出。否则,管理HTML编辑和参数化可能会使处理HTTP看起来像玩井字游戏。当然,这都取决于您需要多么灵活。如果它是菜单驱动的传真机,它可以非常简单。互动越多,您的框架就越“厚实”。好问题,祝你好运!


24

看一下“Jetty” Web服务器 Jetty。这是一个非常棒的开源软件,似乎满足您所有的要求。

如果您坚持自己编写,则可以查看“httpMessage”类。


我认为Jetty API依赖于Servlet。 - irreputable
4
不,Jetty是一个高度模块化的Web服务器,其中一个可选模块是Servlet容器。 - Lawrence Dol
有没有类似于服务器功能的模拟器?是的,它就是“servlet” API。在解析头文件、cookie等后,servlet容器会调用您的类。 - James Anderson
软件猴子:如果没有Servlet API,你就不能使用它。只要我们在使用Java SE/EE的愚蠢概念,Jetty就不是SE。 - irreputable
2
只是为了记录一下 - Jetty自带了自己的Servlet API实现,并且与Java SE完美配合。 - James Anderson
4
Jetty太过庞大且需要学习的曲线较陡,在实际生产使用之前需要花费较多时间。 - user1191027

21
从前,我在寻找一款类似的轻量级但功能完备的HTTP服务器,可以方便地嵌入和定制。我找到了两种可能的解决方案:
  • 全功能服务器并不是那么轻量级或简单(对于极限定义的轻量级而言)。
  • 真正轻量级的服务器不完全是HTTP服务器,但是赞美ServerSocket示例的东西远远没有RFC兼容,并且不支持常见的基本功能。
所以......我开始写 JLHTTP - Java轻量级HTTP服务器
您可以将其作为单个(如果相当长的)源文件或作为没有任何依赖项的~50K jar(剥离后为~35K)嵌入到任何项目中。它努力保持RFC兼容性,并包括广泛的文档和许多有用的功能,同时将膨胀最小化。
功能包括:虚拟主机、从硬盘提供文件服务、通过标准mime.types文件进行mime类型映射、目录索引生成、欢迎文件、对所有HTTP方法的支持、条件ETags和If-*头支持、分块传输编码、gzip/deflate压缩、基本HTTPS(由JVM提供)、部分内容(下载续传)、用于文件上传的multipart/form-data处理、通过API或注释的多个上下文处理程序、参数解析(查询字符串或x-www-form-urlencoded body)等。
我希望其他人也会觉得它有用 :-)

主方法是基本用法的一个很好的例子,FAQ详细介绍了许多细节。如果您有改进现有文档的建议,请直接与我联系! - amichair
终于有一个实用的、可工作的服务器,能够作为虚拟媒体为普通用户引导服务器启动ISO镜像。Python的http.server或其衍生版本无法整个或分块地提供这样大的文件;而这个服务器在不使用sudo的情况下完成了任务。感谢分享! - user3076105

12

哇,小巧玲珑又干净 :) - Shamshirsaz.Navid

10
所有上述答案都详细介绍了单个主线程请求处理程序。
设置:
 server.setExecutor(java.util.concurrent.Executors.newCachedThreadPool());

使用执行器服务通过多个线程允许多个请求处理。

因此,最终的代码将类似于以下内容:

import java.io.IOException;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;
public class App {
    public static void main(String[] args) throws Exception {
        HttpServer server = HttpServer.create(new InetSocketAddress(8000), 0);
        server.createContext("/test", new MyHandler());
        //Thread control is given to executor service.
        server.setExecutor(java.util.concurrent.Executors.newCachedThreadPool());
        server.start();
    }
    static class MyHandler implements HttpHandler {
        @Override
        public void handle(HttpExchange t) throws IOException {
            String response = "This is the response";
            long threadId = Thread.currentThread().getId();
            System.out.println("I am thread " + threadId );
            response = response + "Thread Id = "+threadId;
            t.sendResponseHeaders(200, response.length());
            OutputStream os = t.getResponseBody();
            os.write(response.getBytes());
            os.close();
        }
    }
}

10
JEP 408: 简易Web服务器
Java 18开始,您可以使用Java标准库创建简易的Web服务器。
class Main {
    public static void main(String[] args) {
        var port = 8000;
        var rootDirectory = Path.of("C:/Users/Mahozad/Desktop/");
        var outputLevel = OutputLevel.VERBOSE;
        var server = SimpleFileServer.createFileServer(
                new InetSocketAddress(port),
                rootDirectory,
                outputLevel
        );
        server.start();
    }
}

默认情况下,这将显示您指定的根目录的目录列表。您可以在该目录中放置一个 index.html 文件(以及其他资源,如 CSS 和 JS 文件)来代替显示它们。
附注: 对于 Java 标准库的 HTTP client,请参阅帖子 Java 11 新的 HTTP Client API。请参阅 JEP 321: HTTP Client

2
@Naman 这个答案是JDK 18的新方法。 - Mahozad

9
使用JDK和servlet api,可以仅用几行代码创建支持J2EE servlet的http服务器。这对于单元测试servlet非常有用,因为它比其他轻量级容器(我们在生产中使用jetty)启动更快。
大多数非常轻量级的https服务器不提供servlet支持,但我们需要它们,所以我想分享一下。
下面的示例提供了基本的servlet支持,或者对尚未实现的内容抛出UnsupportedOperationException。它使用com.sun.net.httpserver.HttpServer提供基本的http支持。
import java.io.*;
import java.lang.reflect.*;
import java.net.InetSocketAddress;
import java.util.*;

import javax.servlet.*;
import javax.servlet.http.*;

import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;

@SuppressWarnings("deprecation")
public class VerySimpleServletHttpServer {
    HttpServer server;
    private String contextPath;
    private HttpHandler httpHandler;

    public VerySimpleServletHttpServer(String contextPath, HttpServlet servlet) {
        this.contextPath = contextPath;
        httpHandler = new HttpHandlerWithServletSupport(servlet);
    }

    public void start(int port) throws IOException {
        InetSocketAddress inetSocketAddress = new InetSocketAddress(port);
        server = HttpServer.create(inetSocketAddress, 0);
        server.createContext(contextPath, httpHandler);
        server.setExecutor(null);
        server.start();
    }

    public void stop(int secondsDelay) {
        server.stop(secondsDelay);
    }

    public int getServerPort() {
        return server.getAddress().getPort();
    }

}

final class HttpHandlerWithServletSupport implements HttpHandler {

    private HttpServlet servlet;

    private final class RequestWrapper extends HttpServletRequestWrapper {
        private final HttpExchange ex;
        private final Map<String, String[]> postData;
        private final ServletInputStream is;
        private final Map<String, Object> attributes = new HashMap<>();

        private RequestWrapper(HttpServletRequest request, HttpExchange ex, Map<String, String[]> postData, ServletInputStream is) {
            super(request);
            this.ex = ex;
            this.postData = postData;
            this.is = is;
        }

        @Override
        public String getHeader(String name) {
            return ex.getRequestHeaders().getFirst(name);
        }

        @Override
        public Enumeration<String> getHeaders(String name) {
            return new Vector<String>(ex.getRequestHeaders().get(name)).elements();
        }

        @Override
        public Enumeration<String> getHeaderNames() {
            return new Vector<String>(ex.getRequestHeaders().keySet()).elements();
        }

        @Override
        public Object getAttribute(String name) {
            return attributes.get(name);
        }

        @Override
        public void setAttribute(String name, Object o) {
            this.attributes.put(name, o);
        }

        @Override
        public Enumeration<String> getAttributeNames() {
            return new Vector<String>(attributes.keySet()).elements();
        }

        @Override
        public String getMethod() {
            return ex.getRequestMethod();
        }

        @Override
        public ServletInputStream getInputStream() throws IOException {
            return is;
        }

        @Override
        public BufferedReader getReader() throws IOException {
            return new BufferedReader(new InputStreamReader(getInputStream()));
        }

        @Override
        public String getPathInfo() {
            return ex.getRequestURI().getPath();
        }

        @Override
        public String getParameter(String name) {
            String[] arr = postData.get(name);
            return arr != null ? (arr.length > 1 ? Arrays.toString(arr) : arr[0]) : null;
        }

        @Override
        public Map<String, String[]> getParameterMap() {
            return postData;
        }

        @Override
        public Enumeration<String> getParameterNames() {
            return new Vector<String>(postData.keySet()).elements();
        }
    }

    private final class ResponseWrapper extends HttpServletResponseWrapper {
        final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        final ServletOutputStream servletOutputStream = new ServletOutputStream() {

            @Override
            public void write(int b) throws IOException {
                outputStream.write(b);
            }
        };

        private final HttpExchange ex;
        private final PrintWriter printWriter;
        private int status = HttpServletResponse.SC_OK;

        private ResponseWrapper(HttpServletResponse response, HttpExchange ex) {
            super(response);
            this.ex = ex;
            printWriter = new PrintWriter(servletOutputStream);
        }

        @Override
        public void setContentType(String type) {
            ex.getResponseHeaders().add("Content-Type", type);
        }

        @Override
        public void setHeader(String name, String value) {
            ex.getResponseHeaders().add(name, value);
        }

        @Override
        public javax.servlet.ServletOutputStream getOutputStream() throws IOException {
            return servletOutputStream;
        }

        @Override
        public void setContentLength(int len) {
            ex.getResponseHeaders().add("Content-Length", len + "");
        }

        @Override
        public void setStatus(int status) {
            this.status = status;
        }

        @Override
        public void sendError(int sc, String msg) throws IOException {
            this.status = sc;
            if (msg != null) {
                printWriter.write(msg);
            }
        }

        @Override
        public void sendError(int sc) throws IOException {
            sendError(sc, null);
        }

        @Override
        public PrintWriter getWriter() throws IOException {
            return printWriter;
        }

        public void complete() throws IOException {
            try {
                printWriter.flush();
                ex.sendResponseHeaders(status, outputStream.size());
                if (outputStream.size() > 0) {
                    ex.getResponseBody().write(outputStream.toByteArray());
                }
                ex.getResponseBody().flush();
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                ex.close();
            }
        }
    }

    public HttpHandlerWithServletSupport(HttpServlet servlet) {
        this.servlet = servlet;
    }

    @SuppressWarnings("deprecation")
    @Override
    public void handle(final HttpExchange ex) throws IOException {
        byte[] inBytes = getBytes(ex.getRequestBody());
        ex.getRequestBody().close();
        final ByteArrayInputStream newInput = new ByteArrayInputStream(inBytes);
        final ServletInputStream is = new ServletInputStream() {

            @Override
            public int read() throws IOException {
                return newInput.read();
            }
        };

        Map<String, String[]> parsePostData = new HashMap<>();

        try {
            parsePostData.putAll(HttpUtils.parseQueryString(ex.getRequestURI().getQuery()));

            // check if any postdata to parse
            parsePostData.putAll(HttpUtils.parsePostData(inBytes.length, is));
        } catch (IllegalArgumentException e) {
            // no postData - just reset inputstream
            newInput.reset();
        }
        final Map<String, String[]> postData = parsePostData;

        RequestWrapper req = new RequestWrapper(createUnimplementAdapter(HttpServletRequest.class), ex, postData, is);

        ResponseWrapper resp = new ResponseWrapper(createUnimplementAdapter(HttpServletResponse.class), ex);

        try {
            servlet.service(req, resp);
            resp.complete();
        } catch (ServletException e) {
            throw new IOException(e);
        }
    }

    private static byte[] getBytes(InputStream in) throws IOException {
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        byte[] buffer = new byte[1024];
        while (true) {
            int r = in.read(buffer);
            if (r == -1)
                break;
            out.write(buffer, 0, r);
        }
        return out.toByteArray();
    }

    @SuppressWarnings("unchecked")
    private static <T> T createUnimplementAdapter(Class<T> httpServletApi) {
        class UnimplementedHandler implements InvocationHandler {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                throw new UnsupportedOperationException("Not implemented: " + method + ", args=" + Arrays.toString(args));
            }
        }

        return (T) Proxy.newProxyInstance(UnimplementedHandler.class.getClassLoader(),
                new Class<?>[] { httpServletApi },
                new UnimplementedHandler());
    }
}

1
这里的ServletOutputStream和ServletInputStream缺少一些方法。 - HomeIsWhereThePcIs
较新版本的Servlet API,适用于3.0及以下版本。只需根据需要将缺失的方法添加到示例中即可。 - f.carlsen

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接