Java网络编程
IntAddress
网络中标识一个主机的位置通常使用IP地址来实现,但是对于IP地址这种纯数字的写法,不利于记忆。因此需要通过一个域名来表示一个IP地址,这样即便网站更换主机,只需要更新这个域名和IP的绑定关系即可,不需要在访问的时候更换新的请求地址。域名和IP地址的对应关系需要使用到一个特殊的服务器DNS服务,一个域名也可以对应多个IP,这个映射关系由DNS服务器维护并随机选择一个IP地址返回。
在Java中对IP地址的封装通过InetAddress类来实现。这个类没有一个公共的构造韩式,通过一些静态工厂方法,可以连接到DNS服务器来解析主机名,如InetAddress.getByName("www.baidu.com")。使用这个方法不仅是设置hostName字段的值,同时也会通过连接DNS服务器解析这个域名。如果当前域名无法解析则会抛出UnknownHostException异常。
关于安全性的问题,通过主机名创建一个新的InetAddress对象被认为是一个潜在的不安全操作。不允许不可信代码由任何其他主机名创建InetAddress对象,不论代码使用InetAddress.getByName()⽅法、InetAddress.getA11ByName()⽅法、InetAddress.getLocalHost()⽅法,还是其他⽅法。如果要测试一个主机能够解析,可以使用SecurityManager类中的checkConnect方法。
InetAddress对象
InetAddress对象包含四个get方法,分别是:getHostName()、getAddress()、getHostAddress()和getCanonicalHostName()。InetAddress对象一旦创建,就不允许主动修改hostName和address,因此这是一个线程安全的对象。
- getAddress():返回一个byte数组表示的ip地址。
- getHostAddress():返回一个字符串,包含点分四段格式的IP地址。
- getHostName():返回一个字符串,表示主机名以及这个InetAddress对象表示的IP地址。
- getCanonicalHostName():和getHostName()类似,不过getHostName()只有在不知道主机名时才会和DNS有交互,而getCanonicalHostName知道主机名也会和DNS交互。
InetAddress还可以测试特定节点对当前主机是否可达。isReachable(int timeout)和isReachable(NetworkInterface netif, int ttl, int timeout)方法可以实现上述功能。此外,只要两个InetAddress对象的IP地址相同即任务这两个对象相同。
URL对象
Java中为了方便处理URL,设计了URL类保存URL。该类主要包含了URL的模式、主机名、端口、路径、查询字符串等字段。URL的创建有以下三种情况:
- public URL(String spec) throws MalformedURLException
- 直接将一个完整的URL字符串作为入参传递,如果遇到无法解析的模式则会抛出MalformedURLException,利用这个特点可以快速校验当前服务器所支持的模式有哪些。
- public URL(String protocol, String host, String file) throws MalformedURLException
- 按照模式、hostName和资源相对地址三部分来构建一个URL对象,传入file参数的时候需要注意最前端需要带上“/”,否则会解析失败。
- public URL(URL context, String spec) throws MalformedURLException
- 构建一个相对路径的URL对象,将上一个URL的文件名替换为当前擦混入的文件名。
从URL中获取数据
通过IO流可以获取URL指定的文件信息。主要的方法有openStream()、openConnection()和getContent()。
openStream
openStream()方法连接到URL所引用的资源,在客户端和服务器之间完成必要的握手,返回一个InputStream,由此可以读取数据。在读取数据的时候需要注意资源的编码。
openConnection
openConnnection方法可以为指定的URL打开一个socket,并返回一个URLConnection对象。上一个方法只能单纯从服务器获取资源信息,而当前方法可以访问服务器发送的所有数据,除了原始的文档本身外,还可以访问这个协议指定的所有元数据。
getContent
getContent方法是下载URL引用数据的第三种方式。该方法可以在从服务器获取的数据首部中查找content-type字段,并将其转换成特定的对象返回给调用方。同时当前方法也可以指定返回类型,传入一个Class[]数组,返回第一个匹配的类型。
HTTP
HTTP请求基于TCP协议实现,HTTP1.0版本中每一次传输都需要建立新的TCP连接,在HTTP1.1版本中可以实现同一个TCP连接多次传入,请求和响应可以分为多个块发送。HTTP请求头中包含了一些和请求相关的信息如host、保持连接状态、客户端能接受的数据类型、浏览器信息等。响应数据中通过不同的状态码表示不一样的处理结果。状态码响应如下:
- 1xx:表示服务器和客户端之间的请求继续进行。
- 2xx:表示请求成功。
- 3xx:表示重定向。
- 4xx:表示请求错误,语法异常或者无权限。
- 5xx:表示服务器内部错误。
HTTP的请求类型按照REST风格可以分为四种:GET、POST、PUT和DELETE。其中PUT是幂等操作,如果连续两次吧同一个文档上传到服务器的同一个位置,服务器的状态是一样的。同理,DELETE操作也是幂等操作。
Cookie
Cookie是遗传小文本主要用来存储客户端的状态。Cookie在请求和响应的HTTP首部,从服务器传递到客户端,再从客户端传回服务器,服务器使用Cookie来表示会话ID、购物车内容、登录凭证等信息。
NIO
CPU比网络的速度要快好几个数量级,为了不让在IO的场景下使CPU等待慢速的网络,传统的解决方法是hi是哟给你缓冲和多线程。多线程可以同时为几个不同的连接生成数据,并将数据存储在缓冲区中,直到网络确实准备好发送这些数据。但是多线程之间切换的开销仍然不可忽视,且如果面对大量请求,不可能为每一个请求都创建一个线程。NIO就可以很好地解决这个问题,NIO可以使用一个线程处理多个连接,选取一个准备好接收数据的连接传输数据。其中NIO的核心分为三部分:Channel、Buffer和Selector。
Channel
Channel是数据传输的载体,数据在Channel中进行传输,主要分为四类:FileChannel、SocketChannel、ServerSocketChannel和DatagramChannel。
FileChannel
FileChannel是专门操作文件的通道,既可以从一个文件中读取数据,也可以写入一个文件。但是这个通道是一个阻塞式通道,无法使用Selector。
SocketChannel和ServerSocketChannel
SocketChannel负责数据传输,而ServerSocketChannel负责连接的监听。ServerSocketChannel只适用于服务端,而ScoketChannel同时适用于客户端和服务端,对于一个连接两端都有一个负责传输的SocketChannel。这两种Channel都支持阻塞和非阻塞的传输方式,通过configureBlocking方法设置。
客户端通过创建SocketChannel,创建完成后即可传输数据。但是对于服务端来说,首先需要获取服务器监听通道,通过调用ServerSocketChannel的accept方法获取新链接的SocketChannel。向通道写入和读取操作通过write和read方法完成,这两个方法都需要传入一个Buffer对象,将数据写入到缓冲区中。
DatagramChannel
DatagramChannel采用的是UDP传输协议,这部分暂且不做过多赘述。
Buffer
Buffer是NIO中读写操作必须的一个组件,作为数据的读写缓冲区。Channel中的数据都是先存放在这个缓冲区中。Buffer有三个比较重要的属性:capacity、position和limit。capacity是表示当前Buffer存储数据的上限位置,limit则是读模式下读取数据的上限位置,position则是当前写入或读取数据的位置。Buffer缓冲区通过Buffer.allocate()方法创建,写入数据的时候调用put方法。调用flip()方法可以从写模式切换为读模式,调用该方法当前Buffer的position的值会赋值给limit,position则会重置为0。当调用get()方法的时候既可以从缓冲区中读取数据。如果读取完数据想要重新读取,则需要调用rewind()方法,这个方法会讲position重置为0,但是并不会改变Buffer的模式。当然Buffer也支持从特定位置进行读取,调用mark()方法可以标记当前位置,再调用reset()方法的时候即可回到mark方法标记的位置重新读取数据。最后如果想要切换回写模式,则需要调用clear()方法。
Selector
Selector是用来监听某一个通路内是否有满足提交见的IO事件,并获取该事件。通过IO事件可以获取Channel信息,并进行数据传输。SelectorKey是对IO事件的封装,主要分为四种IO事件:SelectionKey.OP_READ(可读)、SelectionKey.OP_WRITE(可写)、SelectionKey.OP_CONNECT(连接)和SelectionKey.OP_ACCEPT(接受)。当创建一个Channel后可以讲当前Channel注册到Selector中,并选择需要监听的IO事件类型。当该Channel处于当前IO事件所表示的状态时,即可通过轮询Selector中注册的Channel,并判断每一个Channel对应SelectorKey的状态判断是否需要进行处理。
NIO案例
服务端代码
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.net.InetSocketAddress;
import java.util.Iterator;
import java.util.Set;
import java.nio.charset.StandardCharsets;
public class NIOServer {
private static final int PORT = 8888;
private static final int BUFFER_SIZE = 1024;
public static void main(String[] args) {
System.out.println("=== NIO 服务器启动 ===");
System.out.println("监听端口: " + PORT);
try {
// 1. 创建Selector
Selector selector = Selector.open();
// 2. 创建ServerSocketChannel并配置
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.configureBlocking(false); // 非阻塞模式
serverChannel.bind(new InetSocketAddress(PORT)); // 绑定端口
// 3. 将ServerSocketChannel注册到Selector,监听ACCEPT事件
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("服务器准备就绪,等待客户端连接...");
while (true) {
// 4. 阻塞等待就绪的Channel(可设置超时)
int readyChannels = selector.select();
if (readyChannels == 0) {
continue;
}
// 5. 获取已就绪的SelectionKey集合
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
try {
if (key.isAcceptable()) {
// 处理新的客户端连接
handleAccept(key, selector);
}
if (key.isReadable()) {
// 处理客户端数据读取
handleRead(key);
}
if (key.isWritable()) {
// 处理数据写入
handleWrite(key);
}
} catch (Exception e) {
System.err.println("处理事件异常: " + e.getMessage());
if (key != null) {
key.cancel();
try {
key.channel().close();
} catch (Exception ex) {
// 忽略关闭异常
}
}
}
keyIterator.remove();
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 处理客户端连接请求
*/
private static void handleAccept(SelectionKey key, Selector selector) throws Exception {
ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();
SocketChannel clientChannel = serverChannel.accept();
if (clientChannel != null) {
// 配置客户端通道为非阻塞
clientChannel.configureBlocking(false);
// 注册读事件,并附加一个缓冲区
ByteBuffer buffer = ByteBuffer.allocate(BUFFER_SIZE);
clientChannel.register(selector, SelectionKey.OP_READ, buffer);
// 发送欢迎消息
String welcomeMsg = "欢迎连接到NIO服务器! 当前时间: " + System.currentTimeMillis();
ByteBuffer welcomeBuffer = ByteBuffer.wrap(welcomeMsg.getBytes(StandardCharsets.UTF_8));
clientChannel.write(welcomeBuffer);
System.out.println("客户端已连接: " + clientChannel.getRemoteAddress());
System.out.println("已发送欢迎消息");
}
}
/**
* 处理读取客户端数据
*/
private static void handleRead(SelectionKey key) throws Exception {
SocketChannel clientChannel = (SocketChannel) key.channel();
ByteBuffer buffer = (ByteBuffer) key.attachment();
// 清空缓冲区以便读取新数据
buffer.clear();
int bytesRead = clientChannel.read(buffer);
if (bytesRead == -1) {
// 客户端关闭连接
System.out.println("客户端断开连接: " + clientChannel.getRemoteAddress());
clientChannel.close();
key.cancel();
return;
}
if (bytesRead > 0) {
// 切换缓冲区为读模式
buffer.flip();
// 读取数据
byte[] data = new byte[buffer.remaining()];
buffer.get(data);
String received = new String(data, StandardCharsets.UTF_8);
System.out.println("收到客户端消息: " + received);
System.out.println("消息长度: " + bytesRead + " 字节");
// 准备回复(这里简单地将消息反转后返回)
String response = "服务器回复[" + System.currentTimeMillis() + "]: "
+ new StringBuilder(received).reverse().toString();
// 将回复数据附加到key上
key.attach(ByteBuffer.wrap(response.getBytes(StandardCharsets.UTF_8)));
// 注册写事件
key.interestOps(SelectionKey.OP_WRITE);
}
}
/**
* 处理向客户端写数据
*/
private static void handleWrite(SelectionKey key) throws Exception {
SocketChannel clientChannel = (SocketChannel) key.channel();
ByteBuffer buffer = (ByteBuffer) key.attachment();
if (buffer != null) {
buffer.flip(); // 切换为读模式(准备写入通道)
// 写入数据到通道
while (buffer.hasRemaining()) {
clientChannel.write(buffer);
}
System.out.println("已发送回复给客户端");
// 重新注册读事件,等待下一轮消息
buffer.clear(); // 清空缓冲区
key.attach(buffer); // 重新附加缓冲区
key.interestOps(SelectionKey.OP_READ);
}
}
}
客户端代码
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;
public class NIOClient {
private static final String SERVER_HOST = "localhost";
private static final int SERVER_PORT = 8888;
private static final int BUFFER_SIZE = 1024;
public static void main(String[] args) {
System.out.println("=== NIO 客户端启动 ===");
System.out.println("正在连接服务器 " + SERVER_HOST + ":" + SERVER_PORT);
try {
// 1. 创建SocketChannel
SocketChannel socketChannel = SocketChannel.open();
// 2. 连接到服务器
socketChannel.connect(new InetSocketAddress(SERVER_HOST, SERVER_PORT));
// 3. 设置为非阻塞模式(可选,本例使用阻塞模式简化)
// socketChannel.configureBlocking(false);
if (socketChannel.finishConnect()) {
System.out.println("连接服务器成功!");
// 4. 接收服务器欢迎消息
receiveMessage(socketChannel);
// 5. 创建输入扫描器
Scanner scanner = new Scanner(System.in);
// 6. 循环发送和接收消息
while (true) {
System.out.print("\n请输入要发送的消息 (输入 'exit' 退出): ");
String input = scanner.nextLine();
if ("exit".equalsIgnoreCase(input.trim())) {
System.out.println("正在断开连接...");
break;
}
// 发送消息到服务器
sendMessage(socketChannel, input);
// 接收服务器回复
receiveMessage(socketChannel);
}
scanner.close();
} else {
System.err.println("连接服务器失败!");
}
// 7. 关闭连接
socketChannel.close();
System.out.println("客户端已关闭");
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 发送消息到服务器
*/
private static void sendMessage(SocketChannel channel, String message) throws Exception {
ByteBuffer buffer = ByteBuffer.wrap(message.getBytes(StandardCharsets.UTF_8));
channel.write(buffer);
System.out.println("已发送消息: " + message);
}
/**
* 接收服务器消息
*/
private static void receiveMessage(SocketChannel channel) throws Exception {
ByteBuffer buffer = ByteBuffer.allocate(BUFFER_SIZE);
StringBuilder messageBuilder = new StringBuilder();
// 读取服务器响应
int bytesRead;
while ((bytesRead = channel.read(buffer)) > 0) {
buffer.flip(); // 切换为读模式
// 将缓冲区内容转换为字符串
byte[] data = new byte[buffer.remaining()];
buffer.get(data);
messageBuilder.append(new String(data, StandardCharsets.UTF_8));
buffer.clear(); // 清空缓冲区准备下一次读取
// 如果数据读取完毕,跳出循环
if (bytesRead < BUFFER_SIZE) {
break;
}
}
if (messageBuilder.length() > 0) {
System.out.println("服务器回复: " + messageBuilder.toString());
}
// 如果读取到-1,表示服务器关闭了连接
if (bytesRead == -1) {
System.out.println("服务器已关闭连接");
channel.close();
System.exit(0);
}
}
}

浙公网安备 33010602011771号