Java中的网络编程
# Java 中的网络编程
# 1. InetAddress 类详解
# 1.1 什么是 InetAddress 类
InetAddress
类是 Java 对 IP 地址的封装,它表示一个网络中的 IP 地址。IP 地址是 32 位(IPv4)或 128 位(IPv6)的无符号数字,是传输层协议(如 TCP、UDP)的基础。几乎所有与 Java 网络相关的类都依赖于 InetAddress
,包括 ServerSocket
、Socket
、URL
、DatagramSocket
和 DatagramPacket
等。
InetAddress
类不仅可以存储数字形式的 IP 地址,还可能包含主机名(如使用主机名获取 InetAddress
实例时,或启用了反向主机名解析)。该类提供了解析主机名为 IP 地址(或反之)的功能。
在实际应用中,InetAddress
类会使用本地机器配置或网络命名服务(如 DNS 或 NIS)进行主机名解析。解析结果可能被缓存,以提高效率。默认情况下,成功解析的结果会缓存一段时间,而解析失败的结果则仅缓存很短时间(如 10 秒)。
Java 提供了 InetAddress
类及其两个子类:Inet4Address
(IPv4)和 Inet6Address
(IPv6),但通常直接使用 InetAddress
类即可。InetAddress
类通过以下两个静态方法获取实例:
getByName(String host)
:根据主机名获取InetAddress
实例。getByAddress(byte[] addr)
:根据原始 IP 地址获取InetAddress
实例。
# 1.2 InetAddress 的数据结构
InetAddress
类实现了 Serializable
接口,支持序列化,方便在网络或文件中传输对象。
# 1.3 InetAddress 的常用方法 API
常用的 InetAddress
方法包括:
getHostName()
:获取主机名。getHostAddress()
:获取 IP 地址字符串。getByName(String host)
:根据主机名获取InetAddress
实例。getByAddress(byte[] addr)
:根据字节数组形式的 IP 地址获取InetAddress
实例。getLocalHost()
:获取本地主机的InetAddress
实例。
# 1.4 InetAddress 的代码示例
下面是一个完整的代码示例,展示如何使用 InetAddress
类,并附有详细的注释:
package org.javatop.net.ip;
import java.net.InetAddress;
import java.net.UnknownHostException;
/**
* 演示如何使用 InetAddress 类来获取 IP 地址和主机名。
* InetAddress 用来表示 IP 地址,一个 InetAddress 对象可以代表一个 IP 地址。
* 包含对远程主机和本地主机的查询示例。
*/
public class TestInetAddress {
public static void main(String[] args) {
try {
// 根据主机名(例如:www.baidu.com)获取对应的 InetAddress 实例
InetAddress inet = InetAddress.getByName("www.baidu.com");
// 输出 IP 地址和主机名
System.out.println("远程主机信息: " + inet);
System.out.println("主机名: " + inet.getHostName()); // 输出主机名
System.out.println("IP 地址: " + inet.getHostAddress()); // 输出 IP 地址
// 获取本机的 InetAddress 实例
inet = InetAddress.getLocalHost();
// 输出本地主机的 IP 地址和主机名
System.out.println("本地主机信息: " + inet);
System.out.println("本地主机名: " + inet.getHostName()); // 输出本地主机名
System.out.println("本机 IP 地址: " + inet.getHostAddress()); // 输出本机 IP 地址
} catch (UnknownHostException e) {
// 捕获和处理主机名解析失败的异常
System.err.println("主机名解析失败: " + e.getMessage());
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
总结
InetAddress
是 Java 网络编程中不可或缺的基础类,负责 IP 地址和主机名的解析。InetAddress
类的使用非常直观,常见方法如getByName
和getLocalHost
提供了便捷的网络信息获取功能。- 在开发过程中,合理使用
InetAddress
可以有效简化网络应用程序中对主机信息的处理。
# 2. Socket类
# 2.1 Socket
- 套接字(Socket)开发网络应用程序被广泛采用,以至于成为事实上的标准。
- 通信的两端都要有Socket,是两台机器间通信的端点
- 网络通信其实就是Socket]间的通信。
- Sockets允许程序把网络连接当成一个流,数据在两个Socket间通过1O传输,
- 一般主动发起通信的应用程序属客户端,等待通信请求的为服务端
在【客户端/服务端】的通信模式中,客户端需要主动构造与服务器连接的 Socket,构造方法有以下几种重载形式:
Socket()
Socket(InetAddress address, int port) throws UnknownHostException,IOException
Socket(InetAddress address, int port, InetAddress localAddr, int localPort) throws IOException
Socket(String host, int port) throws UnknownHostException,IOException
Socket(String host, int port, InetAddress localAddr, int localPort) throws IOException
Socket(Proxy proxy)
2
3
4
5
6
除了第一个不带参数的构造方法,其他构造方法都会试图建立与服务器的连接,一旦连接成功,就返回 Socket 对象,否则抛出异常
# 1. 设定等待建立连接的超时时间
当客户端的 Socket 构造方法请求与服务器连接时,可能要等待一段时间。在默认情况下,Socket 构造方法会一直等待下去,直到连接成功,或者出现异常。Socket 构造方法请求连接时,受底层网络的传输速度的影响,可能会处于长时间的等待状态。如果希望限定等待连接的时间,就需要使用第一个不带参数的构造方法
Socket socket = new Socket();
SocketAddress remoteAddr = new InetSocketAddress("1ocalhostn", 8000);
// 参数endpoint指定服务器的地址,参数timeout设定的超时时间(ms)
// 如果参数timeout被设为0则表示永远不会超时
socket.connect(remoteAddr, 60000);
2
3
4
5
以上代码用于连接到本地机器上的监听 8000 端口的服务器程序,等待连接的最长时间为一分钟。如果在一分钟内连接成功,则 connect()
方法顺利返回,如果在一分钟内出现某种异常则抛出该异常,如果在一分钟后既没有连接成功,也没有出现异常,那么会抛出 SocketTimeoutException
# 2. 设定服务器的地址
除了不带参数的构造方法,其他构造方法都需要在参数中设定服务器的地城,包括服务器的 IP 或主机名,以及端口
// address表示主机的IP地址
Socket(InetAddress address, int port)
// address表示主机的名字
Socket(String host, int port)
2
3
4
InetAddress 类表示主机的P地址,提供了一系列静态工厂方法用于构造自身实例
// 返回本地主机的IP地址、
InetAddress addr1 = inetAddress.getLocalHost();
// 返回代表 "222.34.57” 的 IPv4 地址
InetAddress addr2 = InetAddress.getByName("222.34.5.7");
// 返同代表 ”2001:DB8:2DE::E13" 的 IPv6 地址
InetAddress addr3 = InetAddress.getByName("2001:DB8:2DE::E13");
// 返回主机名为 "www.javathinker.net" 的 IP 地址
InetAddress addr4 = InetAddress.getByName ("www.javathinker.net");
2
3
4
5
6
7
8
# 3. 设定客户端的地址
在一个 Socket 对象中既包含远程服务器的 IP 地址和端口信息,也包含本地客户端的 IP 地址和端口信息。在默认情况下,客户端的 IP 地址来自客户程序所在的主机,客户端的端口则由操作系统随机分配。Socket 类还有两个构造方法允许显式地设置客户端的 IP 地址和端口
Socket(InetAddress address, int port, InetAddress localAddr, int localPort) throws IOException
Socket(String host, int port, InetAddress localAddr, int localPort) throws IOException
2
如果一个主机同时属于两个以上的网络,它就可能拥有两个以上 IP 地址,例如一个主机在 Internet 网络中的 IP 地址为 “222.67,1.34”,在一个局域网中的 IP 地址为 “1125.4.3",假定这个主机上的客户程序希望和同一个局城网上的一个地址为 “112.5.4.4:8000” 的服务器程序通信,客户端可按照如下方式构造 Socket 对象
InetAddress remoteAddr = InetAddress.getByName("112.5,4.45");
InetAddress localAddr = InetAddress.getByName("112.5.4.3");
//客户端使用口2345
Socket socket = new Socket(remoteAddr, 8000, localAddr, 2345);
2
3
4
# 4. 客户连接服务器时可能抛出的异常
当 Socket 的构造方法请求连接服务器时,可能会抛出以下异常:
- UnknownHostException:无法识别主机的名字或 IP 地址
- ConnectException:没有服务器进程监听指定的端口,或者服务器进程拒绝连接
- SocketTimeoutException:等待连接超时
- BindException:无法把Socket 对象与指定的本地 IP 地址或端口绑定
# 5. 使用代理服务器
在实际应用中,有的客户程序会通过代理服务器来访问远程服务器。代理服务器有许多功能,比如能作为防火墙进行安全防范,或者提高访问速度,或者具有访问特定远程服务器的权限
String proxyIP = "myproxy.abc.oom"; // 代理服务器地址
int proxyPort = 1080; // 代理服务器端口
// 创建代理对象
Proxy proxy = new Proxy(Proxy.Type.SOCKS, new InetSocketAddress(proxyIP, proxyPort));
Socket socket new Socket(proxy);
//连接到远程服务器
socket.connect(new InetSocketAddress("www.javathinker.net", 80));
2
3
4
5
6
7
ProxyType 类表示代理服务器的类型,有以下可选值:
- Proxy.Type.SOCKS:在分层的网络结构中,SOCKS 是位于会话层的代理类型
- Proxy.Type.HTTP:在分层的网络结构中,HTTP 是位于应用层的代理类型
- Proxy.Type.DIRECT:不使用代理,直接连接远程服务器
# 6. InetAddress 地址类的用法
InetAddress 类表示主机的IP 地址,InetAddress 类的静态工厂方法给 getByName()
用于构造自身的实例
// 返回代表 "222.34.5.7" 的 IPv4 地址
InetAddress addr2 = InetAddress,getByName("222.34.5.7");
// 返回主机名为 "www.javathinker.net" 的 IP 地址
InetAddress addr4 = InetAddress.getByName("www.javathinker.net");
2
3
4
InetAddress 还提供了获取相应的主机名的两种方法:
getHostname()
:首先从 DNS 缓存中查找与 IP 地址匹配的主机名,如果不存在,再通过 DNS 服务器查找,如果找到,则返回主机名,否则返回 IP 地址getCanonicalHostName()
:通过 DNS 服务器查找与 IP 地址匹配的主机名,如果找到则返回主机名,否则返问 IP 地址
以上两种方法的区别在于 getHostname()
会先查找 DNS 缓存,减少查找 DNS 服务器的概率,提高查找性能。而 getCanonicalHostName()
总是查找 DNS 服务器,确保获得当前最新版本的主机名
InetAddress 类还提供了两个测试能否从本地主机连接到特定主机的方法:
public boolean isReachable(int timeout) throws IOException
public boolean isReachable(NefworkInterface interface, int ttl, int timeout) throws IOException
2
如果远程主机在参数 timeout(ms)指定的时间内做出回应,以上方法返回true,否则返回 false,如果出现网络错误则抛出 IOException。第二种方法还允许从参数指定的本地网络接口建立连接,以及 TTL(IP 数据包被丢弃前允许存在的时间)
# 7. NetworkInterface 类的用法
NetworkInterfiace 类表示物理上的网络接口,它有两种构造自身实例的静态工厂方法,这两种方法都声明抛出 SocketException
// 参数 name 指定网络接口的名字,如果不存在与名字对应的网络接口,就返回 null
getByName(String name)
// 参数 address 指定网络接口的 IP 地址,如果不存在与 IP 地址对应的网络接口,就返回 null
getByInetAddress(InetAddress address)
2
3
4
NetworkInterface 类的以下方法用于获取网络接口的信息
// 返回网络接口的名字
public String getName()
// 返回和网络接口绑定的所有 IP 地址,返回值为 Enumeration 类型,里面存放了表示 IP 地址的 InetAddress 对象
public Enumeration getInetAddresses()
2
3
4
# 2.2 获取 Socket 的信息
在一个 Socket 对象中同时包含了远程服务器的 IP 地址和端口信息,以及客户本地的 IP 地址和端口信息。此外,从 Socket 对象中还可以获得输出流和输入流,分别用于向服务器发送数据,以及接收从服务器端发来的数据
以下方法用于获取 Socket 的有关信息
// 获得远程被连接进程的IP地址
getInetAddress()
// 获得远程被连接进程的端口
getPort()
// 获得本地的IP地址
getLocalAddress()
// 获得本地的端口
getLocalPort()
// 获得输入流,如果Socket还没有连接,或者已经关团,或者已经通过shutdownInput()方法关闭输入流,那么此方法会抛出IOException
getInputStream()
// 获得输出流,如果Socket还没有连接,或者已经关闭,或者已经通过shutdownOutput()方法关闭输出流,那么此方法会抛出 IOException
getOutputStream()
2
3
4
5
6
7
8
9
10
11
12
# 2.3 关闭 Socket
当客户与服务器的通信结束时,应该及时关闭 Socket,以释放 Socket 占用的包括端口在内的各种资源。Socket 的 close()
方法负责关闭 Socket,如果一个 socket 对象被关闭,就不能再通过它的输入流和输出流进行 IO 操作,否则会导致 IOException
Socket 类提供了三个状态测试方法
// 如果Socket没有关闭,则返回false,否则返回true
isClosed()
// 如果Socket曾经连接到远程主机,不管当前是否已经关闭,都返回true。如果Socket从未连接到远程主机,就返回false
isConnected()
// 如果Socket已经与一个本地端口绑定,则返回true,否则返回false
isBound()
2
3
4
5
6
如果要判断一个 Socket 对象当前是否处于连接状态,可采用以下方式
String isConnected = socket.isConnected() && !socket.isClosed();
# 2.4 半关闭 Socket
进程 A 与进程 B 通过 Socket 通信,假定进程 A 输出数据,进程 B 读入数据,进程 A 如何告诉进程 B 所有数据已经输出完毕呢?有几种处理办法:
如果进程 A 与进程 B 交换的是字符流,并且都一行一行地读写数据,那么可以事先约定以一个特殊的标志作为结束标志,例如以字符串 “bye” 作为结束标志,当进程 A 向进程 B 发送一行字符串 “bye”,进程 B 读到这一行数据后,就停止读取数据
进程 A 先发送一个消息,告诉进程 B 所发送的正文的长度,然后发送正文。进程 B 先获知进程 A 将发送的正文的长度,接下来只要读取该长度的字符或者字节,就停止读取数据
进程 A 发完所有数据后,关闭 Socket,当进程 B 读入了进程 A 发送的所有数据后,再次执行输入流的 read() 方法时,该方法返回 “-1”,如果执行 BufferedReader 的 readLine() 方法,那么该方法返回 null
ByteArrayOutputstream bufferenew = ByteArrayOutputstream(); byte[] buff = new byte[1024); int len = -1; while((len = socketIn.read(buff)) != -1) { buffer.write(buff, 0, len); }
1
2
3
4
5
6当调用 Socke t的 close() 方法关闭 Socket 后,它的输出流和输入流也都被关闭。有的时候,可能仅仅希望关闭输出流或输入流之一,此时可以采用 Socket 类提供的半关闭方法
shutdownInput() // 关闭输入流 shutdownOutput() // 关团输出流
1
2假定进程 A 执行以下代码,先向进程 B 发送一个字符串,等到进程 B 接收到这个字符串后,进程 A 再调用 Socket 的 shutdownOutput() 方法关闭输出流,接下来进程 A 不允许再输出数据,但是仍可以通过输入流读入数据
// 发出请求信息 String data = ...; OutputStream socketOut = socket.getOutputStream(); socketOut.write(data.getBytes()); socketOut.flush(); // 读取响应 InputStream socketIn = socket.getInputStream(); if(服务器端返回提示信息,表明已经接收到客户端的所有请求数据) socket.shutdownOutput(); //关闭输出流 //继续通过socketIn读取数据 ...
1
2
3
4
5
6
7
8
9
10
11值得注意的是,先后调用 Socket 的 shutdownInput() 和 shutdownOutput() 方法,仅仅关闭了输入流和输出流,并不等价于调用 Socket 的 close() 方法。在通信结束后,仍然要调用 Socket 的 close() 方法,因为只有该方法才会释放 Socket 占用的资源,比如占用的本地端口等
Socket 类还提供了两种状态测试方法,用来判断输入流和输出流是否关闭
public boolean isInputShutdown() // 如果输入流关闭,则返回true,否则返回false public boolean isOutputShutdown() // 如果输出流关闭,则返回true,否则返回false
1
2