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在请求和响应的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);
        }
    }
}
posted @ 2026-02-06 15:43  阿斯拉达  阅读(0)  评论(0)    收藏  举报