HTTP 服务器 - 提供 favicon.ico

zac*_*vac 3 java http web server

我正在尝试设置自己的 java http 服务器,以更好地了解 http 服务器以及网络底层发生的情况。我开发了一个非常简单的服务器,并且能够提供 html 页面以及 JSON 形式的数据。然后我看到浏览器(我使用的是 chrome,但假设其他浏览器也一样)正在发送对 favicon.ico 的请求。我能够在我的服务器上识别该请求,因此我尝试提供一个我下载的随机图标,并将其大小调整为 png 格式的 16x16 像素,因为这就是互联网所说的大小需要的大小。这是我的代码,请注意,它不应该是专业的,只是适合我的基本教育目的的代码:

[set up ServerSocket and listen]
public static String err_header = "HTTP/1.1 500 ERR\nAccess-Control-Allow-Origin: *";
public static String success_header = "HTTP/1.1 200 OK\nAccess-Control-Allow-Origin: *";
public static String end_header = "\r\n\r\n";
while(true){
        try{
            System.out.println("Listening for new connections");
            clientSocket = server.accept();
            System.out.println("Connection established");
            InputStreamReader isr = new InputStreamReader(clientSocket.getInputStream());
            BufferedReader reader = new BufferedReader(isr);
            String getLine = reader.readLine();//first line of HTTP request
            handleRequest(getLine,clientSocket);
        }//end of try
        catch(Exception e){
            [error stuff]
        }//end of catch
    }//end of while
Run Code Online (Sandbox Code Playgroud)

处理请求方法:

public static void handleRequest(String getLine,Socket clientSocket) throws Exception{
    if(getLine.substring(5,16).equals("favicon.ico")){
        List<String> iconTag = new ArrayList<String>();
        iconTag.add("\nContent-Type: image/png");
        handleFileRequest("[file]",iconTag,clientSocket);
    }//end of if
    else{
        handleFileRequest("[file]",clientSocket);
    }//end of else
}//end of handleRequest
Run Code Online (Sandbox Code Playgroud)

处理图像的FileRequest:

public static void handleFileRequest(String fileName,List<String> headerTags,Socket clientSocket) throws Exception{
    OutputStream out = clientSocket.getOutputStream();
    BufferedReader read = new BufferedReader(new FileReader(fileName));
    out.write(success_header.getBytes("UTF-8"));
    Iterator<String> itr = headerTags.iterator();
    while(itr.hasNext()){
        out.write(itr.next().getBytes("UTF-8"));
    }//end of while
    out.write(end_header.getBytes("UTF-8"));
    String readLine = "";
    while((readLine = read.readLine())!=null){
        out.write(readLine.getBytes("UTF-8"));
    }//end of while
    out.flush();
    out.close();
}//end of handleFileRequest
Run Code Online (Sandbox Code Playgroud)

它似乎有效,当服务器发送文件时,浏览器显示 200 OK 响应,但没有图标,当我将网络请求过滤为仅图像时,正在服务的页面请求一张图像,但图标请求是未在那里列出(网站图标请求位于“其他”部分)。同样,当单击其他图像时,图像会显示在预览中,而 favicon 请求的情况并非如此。截屏:

图标

与此同时,这是另一张图像的样子,它在页面中显示得很好:

其他图像

我还尝试包含 Content-Length 标头,但这似乎没有什么区别。我错过了一些明显的东西吗?

另外,为了澄清一下,我知道我可以将 favicon 包含在实际的 html 页面中,目标不是这样做,而是了解它是如何工作的。

Sto*_*ica 5

读取二进制文件

网站图标的内容似乎未正确提供。

我怀疑这很可能是由于您阅读其内容的方式造成的:

while((readLine = read.readLine())!=null){
    out.write(readLine.getBytes("UTF-8"));
}
Run Code Online (Sandbox Code Playgroud)

逐行读取二进制内容是不合适的,因为行的概念以及 UTF-8 编码在二进制文件的上下文中没有意义。并且您无法以这种方式正确地逐行读取二进制内容,因为readLinea 的方法BufferedReader不会返回整行,因为它会从末尾去除换行符。您无法手动添加换行符,因为您无法知道它到底是什么。

这是读取二进制文件内容的更简单且正确的方法:

byte[] bytes = Files.readAllBytes(Paths.get("/path/to/file"));
Run Code Online (Sandbox Code Playgroud)

一旦有了这个,就可以使用 的值轻松生成具有内容长度的正确文件头bytes.length

当您在浏览器中访问页面时会发生什么

如果我们澄清一些事情,似乎对你有好处。

当您在浏览器中打开 URL 时,浏览器会向 Web 服务器发送 GET 请求,以下载您指定的原始 URL 的内容。

一旦获得页面内容,它将发送进一步的 GET 请求:

  • 如果还没有图标,请获取一个图标。该位置可以在 HTML 文档中指定,否则浏览器将尝试SERVERNAME/favicon.ico默认获取
  • 获取文档中src任何(有效)标签的属性中指定的图像<img/>
  • 获取文档中href任何(有效)标签的属性中指定的样式表<style/>
  • ...标签也类似<script/>,等等...

图标纯粹是装饰性的,用于在浏览器选项卡标题中显示,其他资源对于渲染页面至关重要。它们在像 lynx 这样的基于文本的浏览器中并不是必需的,这样的浏览器显然不会获取这些资源。

这是为什么请求图标以及如何请求的解释。

Web 服务器如何提供文件服务?

在最基本的情况下,提供文件有两个重要的组成部分:

  1. 生成适当的 HTTP 标头:标头中的每一行均采用name: value格式,并且每一行必须以\n. 必须至少有一个Content-type标头。标题必须以空行结束。

  2. 在终止标头的空行之后,内容可以是任何内容,甚至是二进制。为了举例说明,请考虑该curl命令,它将 url 的内容转储到标准输出。如果运行curl url-to-some-html-file,您将看到 html 文件的内容。如果运行curl url-to-some-image-file,您将看到图像文件的内容。它将无法读取,并且您的终端可能会发出有趣的声音。您可以使用 重定向输出到一个文件curl url-to-some-image-file > image.png,这将为您提供一个图像文件(二进制内容),您可以在任何图像查看器工具中打开该文件。

简而言之,提供文件实际上只是在 上打印标题stdout,然后打印一个空行来终止标题,然后在 上打印内容stdout

调试图像的服务

调试图像是否正确提供的一种简单方法是使用 将 URL 保存到文件中curl,然后验证保存的文件和原始文件是否相同,例如使用以下cmp命令:

curl -o file url-to-favicon
cmp file /path/to/original
Run Code Online (Sandbox Code Playgroud)

的输出cmp应该为空。该命令仅在发现两个文件中存在差异时才会生成输出。

实现一个简单的 HTTP 服务器

ServerSocket这里有一种更简单的方法来实现 HTTP 服务器,而不是使用 a :

HttpServer server = HttpServer.create(new InetSocketAddress(1234), 0);

server.createContext("/favicon.ico", t -> {
  byte[] bytes = Files.readAllBytes(Paths.get("/path/to/favicon"));
  t.sendResponseHeaders(200, bytes.length);
  try (OutputStream os = t.getResponseBody()) {
    os.write(bytes);
  }
});

server.createContext("/", t -> {
  Charset charset = StandardCharsets.UTF_8;
  List<String> lines = Files.readAllLines(Paths.get("/path/to/index"), charset);

  t.sendResponseHeaders(200, 0);

  try (OutputStream os = t.getResponseBody()) {
    for (String line : lines) {
      os.write((line + "\n").getBytes(charset));
    }
  }
});

server.start();
Run Code Online (Sandbox Code Playgroud)