AioHttpServer 应用示例 IP 属地查询
简介
IP 属地查询在现代网络应用中是常见的功能。通过查询用户的 IP 地址,我们可以获取其地理位置信息,用于分析用户分布、增强安全性、个性化服务等场景。为了高效处理这一任务,本示例通过 Java 编写了一个异步 I/O (AIO) HTTP 服务器,结合 ip2region 库,实现了 IP 属地查询功能。 pom.xml
目标
本示例的目标是通过 Java 1.8 构建一个异步 HTTP 服务器,能够接收 IP 查询请求,并返回该 IP 地址的地理位置信息。示例中的 HTTP 服务器采用 AIO 模型,使得在高并发场景下性能更优,适用于现代应用中对实时性和并发量的高要求。
该应用主要功能包括:
- 启动一个基于 AIO 的异步 HTTP 服务器,监听 8080 端口。
- 处理客户端的 IP 查询请求。
- 使用 ip2region 库查询 IP 的地理位置信息。
- 返回查询结果,包含国家、区域、省份、城市等相关信息。
运行环境
- 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