AioHttpServer 应用示例 IP 属地查询

简介

IP 属地查询在现代网络应用中是常见的功能。通过查询用户的 IP 地址,我们可以获取其地理位置信息,用于分析用户分布、增强安全性、个性化服务等场景。为了高效处理这一任务,本示例通过 Java 编写了一个异步 I/O (AIO) HTTP 服务器,结合 ip2region 库,实现了 IP 属地查询功能。 pom.xml

目标

本示例的目标是通过 Java 1.8 构建一个异步 HTTP 服务器,能够接收 IP 查询请求,并返回该 IP 地址的地理位置信息。示例中的 HTTP 服务器采用 AIO 模型,使得在高并发场景下性能更优,适用于现代应用中对实时性和并发量的高要求。

该应用主要功能包括:

  1. 启动一个基于 AIO 的异步 HTTP 服务器,监听 8080 端口。
  2. 处理客户端的 IP 查询请求。
  3. 使用 ip2region 库查询 IP 的地理位置信息。
  4. 返回查询结果,包含国家、区域、省份、城市等相关信息。

运行环境

  • Java 版本: 1.8
  • 依赖库: ip2region 2.7.0
  • 服务器框架: AIO (Asynchronous I/O)
  • 查询数据库: ip2region.xdb

实现部署

pom.xml

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <java.version>1.8</java.version>
    <maven.compiler.source>${java.version}</maven.compiler.source>
    <maven.compiler.target>${java.version}</maven.compiler.target>
    <main.class>com.litongjava.ip.AioHttpServerJava8</main.class>
  </properties>

  <dependencies>
    <!-- https://mvnrepository.com/artifact/org.lionsoul/ip2region -->
    <dependency>
      <groupId>org.lionsoul</groupId>
      <artifactId>ip2region</artifactId>
      <version>2.7.0</version>
    </dependency>
  </dependencies>
  <profiles>
    <!-- development -->
    <profile>
      <id>development</id>
      <activation>
        <activeByDefault>true</activeByDefault>
      </activation>
      <build>
        <plugins>
          <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
            <version>2.7.4</version>
            <configuration>
              <fork>true</fork>
              <mainClass>${main.class}</mainClass>
              <excludeGroupIds>org.projectlombok</excludeGroupIds>
              <arguments>
                <argument>--mode=dev</argument>
              </arguments>
            </configuration>
          </plugin>
        </plugins>
      </build>
    </profile>

    <!-- production -->
    <profile>
      <id>production</id>
      <build>
        <plugins>
          <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
            <version>2.7.4</version>
            <configuration>
              <mainClass>${main.class}</mainClass>
              <excludeGroupIds>org.projectlombok</excludeGroupIds>
            </configuration>
            <executions>
              <execution>
                <goals>
                  <goal>repackage</goal>
                </goals>
              </execution>
            </executions>
          </plugin>
        </plugins>
      </build>
    </profile>
  </profiles>

代码

FileUtil

package com.litongjava.ip.utils;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;

public class FileUtil {

  /**
   * 从URL读取数据并返回字节数组
   *
   * @param resource URL资源
   * @return 字节数组
   * @throws IOException 读取过程中可能抛出的异常
   */
  public static byte[] readUrlAsBytes(URL resource) throws IOException {
    if (resource == null) {
      throw new IllegalArgumentException("资源URL不能为空");
    }

    try (InputStream inputStream = resource.openStream(); ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) {

      byte[] buffer = new byte[1024];
      int bytesRead;

      // 读取URL内容到字节数组
      while ((bytesRead = inputStream.read(buffer)) != -1) {
        byteArrayOutputStream.write(buffer, 0, bytesRead);
      }

      return byteArrayOutputStream.toByteArray();
    }
  }
}

Ip2RegionUtils

package com.litongjava.ip.utils;

import java.io.IOException;
import java.net.URL;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.lionsoul.ip2region.xdb.Searcher;

public enum Ip2RegionUtils {
  INSTANCE;

  private static Searcher searcher;
  static {
    URL resource = Ip2RegionUtils.class.getClassLoader().getResource("ipdb/ip2region.xdb");
    if (resource != null) {
      try {
        byte[] bytes = FileUtil.readUrlAsBytes(resource);
        searcher = Searcher.newWithBuffer(bytes);
      } catch (IOException e) {
        e.printStackTrace();
      }
    }

  }

  public static boolean checkIp(String ipAddress) {
    String ip = "([1-9]|[1-9]\\d|1\\d{2}|2[0-4]\\d|25[0-5])(\\.(\\d|[1-9]\\d|1\\d{2}|2[0-4]\\d|25[0-5])){3}";
    Pattern pattern = Pattern.compile(ip);
    Matcher matcher = pattern.matcher(ipAddress);
    return matcher.matches();
  }

  public static String searchIp(long ip) {
    if (searcher != null) {
      try {
        return searcher.search(ip);
      } catch (IOException e) {
        throw new RuntimeException(e);
      }
    }
    return null;
  }

  public static String searchIp(String ip) {
    if ("0:0:0:0:0:0:0:1".equals(ip)) {
      return "0|0|0|内网IP|内网IP";
    }
    if (searcher != null) {
      try {
        return searcher.search(ip);
      } catch (Exception e) {
        throw new RuntimeException(e);
      }
    }
    return null;
  }
}

AioHttpServerJava8

package com.litongjava.ip;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousServerSocketChannel;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.CompletionHandler;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;

import com.litongjava.ip.utils.Ip2RegionUtils;

public class AioHttpServerJava8 {

  public static void main(String[] args) {
    try {
      // 创建异步服务器通道并绑定到端口8080
      AsynchronousServerSocketChannel serverChannel = AsynchronousServerSocketChannel.open();
      serverChannel.bind(new InetSocketAddress(8080));

      System.out.println("AIO HTTP Server started on port 8080...");

      // 使用非阻塞模式接受连接
      serverChannel.accept(null, new CompletionHandler<AsynchronousSocketChannel, Void>() {
        @Override
        public void completed(AsynchronousSocketChannel clientChannel, Void attachment) {
          // 再次调用 accept(),以继续接受新连接
          serverChannel.accept(null, this);

          if (clientChannel != null && clientChannel.isOpen()) {
            handleRequest(clientChannel);
          }
        }

        @Override
        public void failed(Throwable exc, Void attachment) {
          serverChannel.accept(null, this);
          exc.printStackTrace();
        }
      });

      // 防止主线程退出
      Thread.currentThread().join();

    } catch (IOException | InterruptedException e) {
      e.printStackTrace();
    }
  }

  // 处理客户端连接
  private static void handleRequest(AsynchronousSocketChannel clientChannel) {
    ByteBuffer buffer = ByteBuffer.allocate(4096); // 增加 buffer 大小以处理较大请求

    clientChannel.read(buffer, buffer, new CompletionHandler<Integer, ByteBuffer>() {
      @Override
      public void completed(Integer result, ByteBuffer attachment) {
        attachment.flip();
        String request = StandardCharsets.UTF_8.decode(attachment).toString();

        // 解析请求路径
        String requestPath = getRequestPath(request);
        if ("/ip".equals(requestPath)) {
          ipHandler(clientChannel, request);
          // 发送响应
          return;
        }

        // 其他路径,返回404
        writeHttpResponse(clientChannel, 404, "text/plain", "404 Not Found");

      }

      @Override
      public void failed(Throwable exc, ByteBuffer attachment) {
        exc.printStackTrace();
      }
    });
  }

  private static void ipHandler(AsynchronousSocketChannel clientChannel, String request) {
    // 解析请求,获取IP参数
    Map<String, String> requestMap = getRequestMap(request);
    String ip = requestMap.get("ip");

    String body = null;
    if (ip != null && !ip.isEmpty()) {
      body = Ip2RegionUtils.searchIp(ip);
    }
    // 构建HTTP响应
    writeHttpResponse(clientChannel, 200, "text/plain;charset=utf-8", body);
  }

  // 从请求中提取请求路径
  private static String getRequestPath(String request) {
    String[] lines = request.split("\r\n");
    for (String line : lines) {
      if (line.startsWith("GET") || line.startsWith("POST")) {
        String[] parts = line.split(" ");
        if (parts.length > 1) {
          String query = parts[1];
          return query.split("\\?")[0]; // 提取请求路径
        }
      }
    }
    return "/";
  }

  // 从请求中提取参数并封装为Map的方法
  private static Map<String, String> getRequestMap(String request) {
    Map<String, String> paramMap = new HashMap<>();
    String[] lines = request.split("\r\n");
    for (String line : lines) {
      if (line.startsWith("GET") || line.startsWith("POST")) {
        String[] parts = line.split(" ");
        if (parts.length > 1) {
          String query = parts[1];
          if (query.contains("?")) {
            String[] queryParams = query.substring(query.indexOf("?") + 1).split("&");
            for (String param : queryParams) {
              String[] keyValue = param.split("=");
              if (keyValue.length > 1) {
                paramMap.put(keyValue[0], keyValue[1]);
              } else {
                paramMap.put(keyValue[0], ""); // 如果没有值,则设为空字符串
              }
            }
          }
        }
      }
    }
    return paramMap;
  }

  private static void writeHttpResponse(AsynchronousSocketChannel clientChannel, int statusCode, String contentType, String body) {

    String statusMessage;
    switch (statusCode) {
    case 200:
      statusMessage = "OK";
      break;
    case 404:
      statusMessage = "Not Found";
      break;
    case 500:
      statusMessage = "Internal Server Error";
      break;
    default:
      statusMessage = "Unknown";
    }

    String response;
    String string = "HTTP/1.1 " + statusCode + " " + statusMessage + "\r\n" + "Content-Type: " + contentType + "\r\n";

    if (body != null) {
      byte[] bytes = body.getBytes(StandardCharsets.UTF_8);
      response = string + "Content-Length: " + bytes.length + "\r\n" + "\r\n" + body;
    } else {
      response = string + "Content-Length: 0" + "\r\n" + "\r\n";
    }

    ByteBuffer responseBuffer = ByteBuffer.wrap(response.getBytes());
    clientChannel.write(responseBuffer, responseBuffer, new CompletionHandler<Integer, ByteBuffer>() {
      @Override
      public void completed(Integer result, ByteBuffer buffer) {
        try {
          clientChannel.close();
        } catch (IOException e) {
          e.printStackTrace();
        }
      }

      @Override
      public void failed(Throwable exc, ByteBuffer buffer) {
        exc.printStackTrace();
      }
    });
  }
}

运行示例

启动服务器后,通过以下请求查询 IP 属地信息:

http://localhost:8080/ip?ip=66.75.89.81

响应结果示例如下:

美国|0|夏威夷|檀香山|0

此结果表明查询的 IP 地址来自美国夏威夷檀香山市。

开源地址

https://github.com/litongjava/java-ip-server