前言

今天,我将梳理在Java网络编程中很重要的一个类InetAddress以及其相关的类NetworkInterface。在这篇文章中将会涉及:

  • InetAddress
  • NetworkInterface
  • 具体应用范例

这里的范例将会实现一个简单的日志IP解析系统。我们将会在后面详细介绍。

InetAddress API

在此我将会直接从API入手,如果对其中的名词有何疑惑,可以先行了解一下基础概念。
需要先行了解的概念包括:

  • IP,IPv4,IPv6
  • 单播,多播,广播
  • loop地址
  • 域名
public class InetAddress{
    //私有化构造器,我们只能通过其提供的静态工厂方法来获取一个实例
    //同时我们也不可以修改内部的IP地址或是域名
    //这种Immutable的方式确保了其线程安全性
    
    //这里需要注意java没有正整数型,因此我们需要对开头为1的二进制数进行转义,如下
    //byte[] address = {107, 23, (byte) 216, (byte) 196};
    public static InetAddress getByAddress(byte[] addr) throws UnknownHostException;
    //该方法不会查询DNS来确保域名和IP地址相符
    //这里有一个比较有意思的在于如果你希望寻找家用设备如打印机等的局域网地址,则可以通过遍历254个可能的局域网地址来找到它,而不需要将其硬编码进代码中
    public static InetAddress getByAddress(String host, byte[] addr) throws UnknownHostException;
    //根据域名获取一个实例,该加载为懒加载,即除非到必要的时候,否则将不会启动DNS查询
    public static InetAddress getByName(String host) throws UnknownHostException;
    
    //获取该域名对应的所有IP地址(即一个域名可以映射到多个IP地址)
    public static InetAddress[] getAllByName(String host)
                                  throws UnknownHostException
    
    //获取环回地址,通常为172.0.0.1                              
    public static InetAddress getLoopbackAddress();
    //获取本机的IP地址,如果本机没有联网,则返回环回地址
    public static InetAddress getLoaclHost();
    
    //获取主机名
    public String getHostName()
    //获取主机别名
    public String getCanonicalHostName() 
    //获取主机的IP地址
    //该方法可以用来判断是IPv4地址还是IPv6地址
    public byte[] getAddress()
    //获取String类型的IP地址
    public String getHostAddress()
    
    //是否是一个和本地主机相关的地址
    //包括本机IP,loopback,wifi地址等
    public boolean isAnyLocalAddress() 
    //是否是环回地址
    public boolean isLoopbackAddress() 
    //是否是链路本地地址
    //链路本地地址(Link-local address)是计算机网络中一类特殊的地址,
    //它仅供于在网段,或广播域中的主机相互通信使用。
    //这类主机通常不需要外部互联网服务,仅有主机间相互通讯的需求。
    public boolean isLinkLocalAddress() 
    //是否落入这三个网段
    //10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16.
    public boolean isSiteLocalAddress() 
    //是否是广播地址
    public boolean isMulticastAddress() 
    //是否是全球通用的广播地址
    public boolean isMCGlobal()
    //是否是企业专属的多播地址
    public boolean isMCOrgLocal()
    
    //判断当前节点是否可以访问目标节点
    //通常使用ICMP报文实现
    public boolean isReachable(int timeout) throws IOException
    public boolean isReachable(NetworkInterface interface, int ttl, int timeout) throws IOException

    //只要两个对象的IP地址相等,则二者相等(与域名无关)
    public boolean equals(Object o) 
    //仅根据IP地址计算hashCode
    public int hashCode()
    //格式为 “域名/IP地址”
    public String toString()
}

相关参数:
networkaddress.cache.negative.ttl:失败的DNS查找被缓存的时间
networkaddress.cache.ttl:成功的DNS查找被缓存的时间

NetworkInterface API

NetworkInterface代表一个本地的IP地址。可以通过该接口获取所有本地地址,并根据这些地址创建InetAddress。通过NetworkInterface接口,可以获取本机配置的网络接口的名字,IP列表(包括IPV4和IPV6),网卡地址,最大传输单元(MTU),接口状态(开启/关闭)、接口网络类型(点对点网络、回环网络)等信息

public final class NetworkInterface{
    //根据名称获取网络接口
    //该方法依赖于底层平台
    public static NetworkInterface getByName(String name) throws SocketException
    //根据IP地址获得对应的网络接口
    public static NetworkInterface getByInetAddress(InetAddress address) throws SocketException
    //获取所有的网络接口
    public static Enumeration getNetworkInterfaces() throws SocketException
    //获取该接口之下所有的IP地址
    public Enumeration getInetAddresses()
    //获取MAC地址
    public byte[] getHardwareAddress()
}

获取WebLog中的IP地址对应的hostName

当我们启动web应用时,我们往往会从HTTP报文中获取访客的IP并且将其记录进日志中。从而可以从日志中分析常见的访客或是不明访客。这里我们将获取一个手动生成的包含IP地址的日志文件,并且将其中的IP地址转化为hostName输出。这里我们将采用多线程实现,因为读取一条IP记录的速度远远高于从DNS服务器获取域名的速度。

首先我们使用工具类向文件中写入日志:

public class Util {

    private static final String filePath = "YOUR_FILE_PATH";
    private static final String[] hostNames = new String[]{
            "www.sina.com",
            "www.sohu.com",
            "www.taobao.com",
            "www.baidu.com",
            "www.qq.com",
            "www.163.com",
            "www.dzone.com",
            "www.github.com",
            "www.acmcoder.com",
            "www.meituan.com",
            "kafka.apachecn.org",
            "www.ibm.com",
            "javarevisited.blogspot.kr"
    };
    public static void main(String[] args){
        int count = 0;
       try (BufferedWriter bf = new BufferedWriter(new FileWriter(filePath))){
           for(String host : hostNames){
               InetAddress[] addresses = InetAddress.getAllByName(host);
               count+= addresses.length;
               for (InetAddress address : addresses) {
                   bf.write(address.getHostAddress() + " " + address.getHostName());
                   bf.newLine();
               }
           }
           System.out.println(count);
       } catch (IOException e) {
           e.printStackTrace();
       }
    }
}

一个处理LOG的IPAnalyser类,该类实现Callable接口,支持将内部的值返回给别的线程使用:

public class IPAnalyser implements Callable<String>{
    private String logItem;
    public IPAnalyser(String logItem){
        this.logItem = logItem;
    }
    @Override
    public String call() throws Exception {
        if (logItem!=null){
            String[] items = logItem.split(" ");
            InetAddress inetAddress = InetAddress.getByName(items[0]);
            return inetAddress.getHostAddress();
        }
        return null;
    }
}

额外使用一个线程来同步处理IPAnalyser返回的值,从而避免因为日志文件过大,日志条目全部存储在主存中,再一次性读取而带来因占用大量内存而影响性能的问题。
在这里我们使用阻塞队列实现主线程和打印线程之间的通信。但是我们需要在打印完该日志文件后,结束该打印线程,因此使用write来记录当前已经打印的日志条目数,然后在打印完成所有的之后结束该线程。

public class IPPrinter implements Runnable {
    private BlockingQueue<LogEntry> queue;
    private static volatile int writeCount;
    public IPPrinter(BlockingQueue<LogEntry> queue){
        this.queue = queue;
    }

    @Override
    public void run() {
        while (true){
            if (writeCount == Main.readCount){
                System.out.println("写完毕");
                break;
            }
            try {
                LogEntry logEntry = queue.take();
                System.out.println(logEntry.getOrigin() + "对应的主机名为" + logEntry.getHostName().get());
                writeCount++;
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
        }

    }
}

实体类LogEntry保存原来的日志条目和解析后的日志线程返回:

public class LogEntry {
    private Future<String> hostName;

    private String origin;

    public LogEntry(Future<String> hostName, String origin){
        this.hostName = hostName;
        this.origin = origin;
    }

    public void setOrigin(String origin) {
        this.origin = origin;
    }

    public String getOrigin() {
        return origin;
    }

    public Future<String> getHostName() {
        return hostName;
    }

    public void setHostName(Future<String> hostName) {
        this.hostName = hostName;
    }
}

最后在主线程中开启IO和相关的所有线程:

public class Main {
    private static final String filePath = "/Users/rale/IdeaProjects/Demo/concurrency/src/main/java/cn/deerowl/weblog_analyse/web.log";
    private static final BlockingQueue<LogEntry> queue = new LinkedBlockingQueue<>();

    public static volatile int readCount  = 30;

    public static void main(String[] args){
        ExecutorService executorService = Executors.newFixedThreadPool(6);
        executorService.submit(new IPPrinter(queue));
        try (BufferedReader bufferedReader = new BufferedReader(new FileReader(filePath))){
            String tmp;
            while ((tmp = bufferedReader.readLine()) != null){
                Future<String> f = executorService.submit(new IPAnalyser(tmp));
                queue.put(new LogEntry(f, tmp));
            }

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

        executorService.shutdown();
    }
}

这里要强调一下域名和主机名的关联:一个域名之下可以有多个主机名,如域名abc.com下,有主机server1和server2,其主机全名就是server1.abc.com和server2.abc.com。

参考书籍

Java Network Programming 4th Edition

clipboard.png
想要了解更多开发技术,面试教程以及互联网公司内推,欢迎关注我的微信公众号!将会不定期的发放福利哦~


raledong
2.7k 声望2k 粉丝

心怀远方,负重前行