Java EE初阶——网络编程

发布于:2025-06-23 ⋅ 阅读:(15) ⋅ 点赞:(0)
⽹络编程,指⽹络上的主机,通过不同的进程,以编程的⽅式实现⽹络通信(或称为⽹络数据传输)。
只要满⾜进程不同就⾏;所以即便是同⼀个主机,只要是不同进程,基于⽹络来传输数据,也属于⽹络编程。

1. 客户端和服务端

 
服务端:在常⻅的⽹络数据传输场景下,把提供服务的⼀⽅进程,称为服务端,可以提供对外服务。
 
客⼾端:获取服务的⼀⽅进程,称为客⼾端。
 

客户端与服务器之间的交互:一问一答(web开发),一问多答(下载),多问一答(上传),多问多答(远程控制)

常⻅的客⼾端服务端模型:
1. 客⼾端先发送请求到服务端
2. 服务端根据请求数据,执⾏相应的业务处理
3. 服务端返回响应:发送业务处理结果
4. 客⼾端根据响应数据,展⽰处理结果(展⽰获取的资源,或提⽰保存资源的处理结果)
 

2. Socket 套接字

Socket套接字,是由系统提供⽤于⽹络通信的技术,是基于TCP/IP协议的⽹络通信的基本操作单元。基于Socket套接字的⽹络程序开发就是⽹络编程。
 Socket 套接字主要针对传输层协议划分为如下三类:
1.  流套接字:使⽤传输层TCP协议
TCP,即Transmission Control Protocol(传输控制协议),传输层协议。
对于字节流来说,可以简单的理解为,传输数据是基于IO流,流式数据的特征就是在IO流没有关闭的情况下,是⽆边界的数据,可以多次发送,也可以分开多次接收。
2.  数据报套接字:使⽤传输层UDP协议
UDP,即User Datagram Protocol(⽤⼾数据报协议),传输层协议。
3.  原始套接字:原始套接字⽤于⾃定义传输层协议,⽤于读写内核没有处理的IP协议数据
 

1. InetAddress

InetAddress 是一个用于表示 IP 地址的类,位于 java.net 包下。它提供了与 IP 地址相关的操作,例如解析主机名、获取本地或远程主机的 IP 地址、验证地址有效性等。InetAddress 是一个抽象类,但通过工厂方法(如 getByName)可以获取其具体子类的实例(如 Inet4Address 或 Inet6Address)。

1. 获取 InetAddress 对象的方法

方法 说明 示例
static InetAddress getByName(String host) 根据主机名或IP字符串创建对象(可能触发DNS查询)。 InetAddress addr = InetAddress.getByName("www.baidu.com");
static InetAddress[] getAllByName(String host) 获取主机名对应的所有IP地址(如负载均衡场景)。 InetAddress[] addrs = InetAddress.getAllByName("google.com");
static InetAddress getLocalHost() 获取本地主机的IP地址。 InetAddress local = InetAddress.getLocalHost();
static InetAddress getByAddress(byte[] addr) 通过字节数组(IPv4为4字节,IPv6为16字节)创建对象。

byte[] ipv4 = {192, 168, 1, 1};

InetAddress addr = InetAddress.getByAddress(ipv4);

static InetAddress getByAddress(String host, byte[] addr) 指定主机名和字节数组创建对象(主机名可忽略)。

byte[] ipv4 = {8, 8, 8, 8};

InetAddress addr = InetAddress.getByAddress("dns.google", ipv4);


2. InetAddress 核心方法

方法 返回类型 说明
String getHostName() String 获取主机名(可能触发反向DNS查询)。
String getHostAddress() String 获取IP地址字符串(如 "192.168.1.1")。
boolean isReachable(int timeout) boolean 测试地址是否可达(ICMP Ping或TCP端口探测)。
boolean isLoopbackAddress() boolean 检查是否为回环地址(如 127.0.0.1)。
boolean isMulticastAddress() boolean 检查是否为组播地址(如 224.0.0.0~239.255.255.255)。
byte[] getAddress() byte[] 返回IP地址的字节数组形式(IPv4为4字节)。
 

3. UDP数据报套接字编程

 

对于UDP协议来说,具有⽆连接,⾯向数据报的特征,即每次都是没有建⽴连接,并且⼀次发送全部数据报,⼀次接收全部的数据报.
 
 

UDP(用户数据报协议)的核心类在java.net 包中:

1. DatagramSocket

  • 用于发送和接收UDP数据包。

1. DatagramSocket 构造方法

构造方法 说明 示例
DatagramSocket() 创建一个未绑定的UDP Socket,系统自动分配空闲端口。 DatagramSocket socket = new DatagramSocket();
DatagramSocket(int port) 创建绑定到指定端口的UDP Socket(用于接收数据)。 DatagramSocket socket = new DatagramSocket(5000);
DatagramSocket(int port, InetAddress laddr) 绑定到指定本地IP和端口(多网卡环境下使用)。 DatagramSocket socket = new DatagramSocket(5000, InetAddress.getByName("192.168.1.100"));
DatagramSocket(SocketAddress bindaddr) 通过SocketAddress绑定地址和端口(更灵活)。 SocketAddress addr = new InetSocketAddress("192.168.1.100", 5000);
DatagramSocket socket = new DatagramSocket(addr);

2. DatagramSocket 核心方法

方法 说明
void send(DatagramPacket p) 发送UDP数据包。
void receive(DatagramPacket p) 接收UDP数据包(阻塞直到数据到达)。
void close() 关闭Socket,释放端口资源。
boolean isClosed() 检查Socket是否已关闭。
int getLocalPort() 获取Socket绑定的本地端口。
InetAddress getLocalAddress() 获取Socket绑定的本地IP地址。
void setSoTimeout(int timeout) 设置接收超时时间(毫秒),超时抛出SocketTimeoutException
void connect(InetAddress address, int port) 连接远程地址(非TCP连接,仅限制收发目标)。
void disconnect() 断开“连接”,恢复可向任意地址发送。

 

2. DatagramPacket

  • 封装UDP数据包,包含数据、目标地址和端口。

1. DatagramPacket 构造方法

构造方法 说明 适用场景
DatagramPacket(byte[] buf, int length) 创建接收数据包,指定缓冲区和长度。 用于接收UDP数据。
DatagramPacket(byte[] buf, int length, InetAddress address, int port) 创建发送数据包,指定数据、目标IP和端口。 用于发送UDP数据。
DatagramPacket(byte[] buf, int offset, int length) 创建接收数据包,指定缓冲区、偏移量和长度。 接收数据时从缓冲区指定位置存储。
DatagramPacket(byte[] buf, int offset, int length, InetAddress address, int port) 创建发送数据包,指定数据、偏移量、目标IP和端口。 发送部分数据(如大文件分片)。

2. DatagramPacket 核心方法

方法 返回类型 说明
byte[] getData() byte[] 获取数据包中的字节数组(含缓冲区未用部分)。
int getLength() int 获取实际接收或发送的数据长度(≤缓冲区长度)。
int getOffset() int 获取数据在缓冲区中的起始偏移量。
InetAddress getAddress() InetAddress 获取发送方IP(接收时)或目标IP(发送时)。
int getPort() int 获取发送方端口(接收时)或目标端口(发送时)。
void setData(byte[] buf) void 设置数据包的缓冲区(用于发送前重置数据)。
void setData(byte[] buf, int offset, int length) void 设置缓冲区、偏移量和长度(灵活控制数据范围)。
void setAddress(InetAddress address) void 设置目标IP地址(发送前修改目标)。
void setPort(int port) void 设置目标端口(发送前修改端口)。

服务端:

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;

public class UdpEchoServer {
    private DatagramSocket socket = null;
    public UdpEchoServer(int port) throws SocketException {
        //创建 DatagramSocker 对象,创建失败,如端口号被其他进程占用,抛出异常
        socket = new DatagramSocket(port);//服务器需手动指定一个端口号
    }
    //启动服务器
    public void start() throws IOException {
        System.out.println("服务器端启动");
        //服务器需不停进行请求响应操作
        while(true){
            //1. 读取请求并解析
            //创建 DatagramPacket 对象,内部包含一个手动设置长度的字节数组,保存收到的消息正文(UDP数据报载荷部分)
            DatagramPacket requestPacket = new DatagramPacket(new byte[4096],4096);
            //receive 从网卡读取到一个UDP数据报,存放在requestPacket对象中,字节数组存放UDP数据报载荷部分,其他属性存放报头及源IP及源端口等
            socket.receive(requestPacket);//具有阻塞等待功能
            //将字节数组存放的UDP数据报载荷部分转换为 String 类型,方便后续操作
            String request = new String(requestPacket.getData(),0,requestPacket.getLength());
            //2. 根据请求计算响应
            String response = process(request);
            //3. 把响应返回到客户端
            //创建 DatagramPacket 对象,包含请求中的源IP及源端口,作为响应中的目的IP及目的端口
            // 此时需要告知⽹卡, 要发的内容是啥, 要发给谁
            DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length,requestPacket.getSocketAddress());
            socket.send(responsePacket);
            //打印日志
            System.out.printf("[%s,%d] req:%s,reap:%s\n",requestPacket.getAddress().toString(),requestPacket.getPort(),request,response);
        }
    }
    public String process(String request){
        return request;//回显程序
    }

    public static void main(String[] args) throws IOException {
        UdpEchoServer udpEchoServer = new UdpEchoServer(9096);//1024<端口号<65535
        udpEchoServer.start();
    }
}

客户端:

import java.io.IOException;
import java.net.*;
import java.util.Scanner;

public class UdpEchoClient {
    private DatagramSocket socket = null;
    private String serverIP;//请求的目的IP
    private int serverport;//请求的目的端口
    //客户端主动给服务器发送请求,需要知道服务器的ip及端口号
    public UdpEchoClient(String serverIP,int serverport) throws SocketException {
        this.serverIP = serverIP;
        this.serverport = serverport;
        socket = new DatagramSocket();//客户端系统随机分配端口
    }
    public void start() throws IOException {
        System.out.println("客户端启动");
        Scanner scan = new Scanner(System.in);
        //客户端不停进行请求响应
        while(true){
            System.out.print("->");
            //1. 从客户端读取要发送的请求
            if(!scan.hasNext()){
                System.out.println("客户端关闭");
                break;
            }
            String response = scan.next();
            //2. 构造请求并发送给服务器端
            DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length,
                    InetAddress.getByName(serverIP),serverport);
            socket.send(responsePacket);
            //读取服务器的响应
            DatagramPacket requestPacket = new DatagramPacket(new byte[4096],4096);
            socket.receive(requestPacket);
            //把响应打印到控制台上
            String request = new String(requestPacket.getData(),0,requestPacket.getLength());
            System.out.println(request);
        }
    }

    public static void main(String[] args) throws IOException {
        UdpEchoClient udpEchoClient = new UdpEchoClient("127.0.0.1",9096);
        udpEchoClient.start();
    }
}
       
编写⼀个英译汉的服务器. 只需要重写 process:
import java.io.IOException;
import java.net.SocketException;
import java.util.HashMap;

public class UdpEchoProcess extends UdpEchoServer{
    private HashMap<String,String> map = new HashMap<>();
    public UdpEchoProcess(int port) throws SocketException {
        super(port);
        map.put("hello","你好");
        map.put("cat","小猫");
        map.put("dog","小狗");
    }
    @Override
    public String process(String request){
        //查找
        return map.getOrDefault(request,"此单词不存在");
    }

    public static void main(String[] args) throws IOException {
        UdpEchoProcess udpEchoProcess = new UdpEchoProcess(9096);
        udpEchoProcess.start();//直接调用父类start方法
    }
}

     

 

4. TCP流套接字编程

阻塞式 I/O(java.net 包)

适用于简单、同步的TCP通信。

1.  ServerSocket(服务端)

构造方法

构造方法 说明
ServerSocket() 创建一个未绑定的服务端Socket。
ServerSocket(int port) 创建绑定到指定端口的服务端Socket。
ServerSocket(int port, int backlog) 指定端口和连接队列最大长度(等待处理的连接数)。
ServerSocket(int port, int backlog, InetAddress bindAddr) 绑定到指定IP和端口(多网卡环境下使用)。

核心方法

方法 返回类型 说明
Socket accept() Socket 阻塞等待客户端连接,返回通信的 Socket 对象。
void bind(SocketAddress endpoint) void 绑定到指定地址和端口(用于无参构造后手动绑定)。
void close() void 关闭服务器Socket,释放端口。
boolean isClosed() boolean 检查Socket是否已关闭。
int getLocalPort() int 获取绑定的本地端口号。
InetAddress getInetAddress() InetAddress 获取绑定的本地IP地址。
void setSoTimeout(int timeout) void 设置 accept() 的超时时间(毫秒),超时抛出 SocketTimeoutException

2. Socket(客户端/服务端通信)

构造方法

构造方法 说明
Socket() 创建一个未连接的Socket对象。
Socket(String host, int port) 连接到指定主机和端口(阻塞直到连接成功或失败)。
Socket(InetAddress address, int port) 通过 InetAddress 对象指定目标地址。
Socket(String host, int port, InetAddress localAddr, int localPort) 指定本地绑定IP和端口(多网卡或固定源端口场景)。

核心方法

方法 返回类型 说明
void connect(SocketAddress endpoint) void 手动连接服务器(用于无参构造后连接)。
InputStream getInputStream() InputStream 获取输入流(接收数据)。
OutputStream getOutputStream() OutputStream 获取输出流(发送数据)。
void close() void 关闭Socket连接。
boolean isConnected() boolean 检查是否已连接。
boolean isClosed() boolean 检查是否已关闭。
void setSoTimeout(int timeout) void 设置 read() 操作的超时时间(毫秒)。
InetAddress getInetAddress() InetAddress 获取远程服务器IP地址。
int getPort() int 获取远程服务器端口号。
InetAddress getLocalAddress() InetAddress 获取本地绑定的IP地址。
int getLocalPort() int 获取本地绑定的端口号。

3. InetSocketAddress

SocketAddress 是 Java 中表示通用套接字地址的抽象类,没有定义任何具体方法,位于 java.net 包中。它是所有具体套接字地址类(如 InetSocketAddress)的父类,主要用于为不同类型的网络地址提供统一的抽象接口。

InetSocketAddress 是 Java 中用于表示 IP 地址 + 端口号 的组合

1. 作用

  • 封装 IP 和端口:将 InetAddress(或主机名)与端口号合并为一个对象。

  • 支持主机名解析:在构造时自动解析域名(如 "www.example.com")为 IP 地址。

  • 不可变对象:一旦创建,其地址和端口不可修改。

2. 构造方法

构造方法 说明
InetSocketAddress(int port) 创建通配地址(0.0.0.0::/128)的Socket地址,绑定所有网卡。
InetSocketAddress(String hostname, int port) 通过主机名和端口创建(可能触发DNS解析)。
InetSocketAddress(InetAddress addr, int port) 直接通过 InetAddress 对象和端口创建。

3. 核心方法

方法 返回类型 说明
InetAddress getAddress() InetAddress 获取IP地址对象。
String getHostName() String 获取主机名(若构造时用IP,可能触发反向DNS查询)。
String getHostString() String 安全获取主机名或IP字符串(避免DNS查询)。
int getPort() int 获取端口号。
boolean isUnresolved() boolean 检查主机名是否未解析为IP(构造时用无效主机名返回true)。
static InetSocketAddress createUnresolved(String host, int port) InetSocketAddress 直接创建未解析的主机名地址(不触发DNS)。

4. PrintWriter 

PrintWriter 是 Java 中用于格式化输出文本的类,位于 java.io 包中。

1. 构造方法

构造方法 说明
PrintWriter(OutputStream out) 从字节输出流创建,不自动刷新
PrintWriter(OutputStream out, boolean autoFlush) 指定是否自动刷新缓冲区(autoFlush=true时,调用println()会自动刷新)。
PrintWriter(Writer writer) 从字符输出流(如 FileWriter)创建,不自动刷新。
PrintWriter(Writer writer, boolean autoFlush) 指定是否自动刷新缓冲区。
PrintWriter(String fileName) 直接通过文件名创建(默认字符集)。
PrintWriter(File file) 通过 File 对象创建。

2. 核心方法

(1) 写入数据

方法 说明
void print(String s) 写入字符串(不换行)。
void println(String s) 写入字符串并换行。
void printf(String format, Object... args) 格式化输出(类似 String.format())。
void write(String s) 直接写入字符串(不自动刷新)。

(2) 控制缓冲区

方法 说明
void flush() 强制刷新缓冲区(确保数据立即发送)。
void close() 关闭流(会先自动调用 flush())。

(3) 错误检查

方法 说明
boolean checkError() 检查是否发生IO错误(需手动调用)。

为什么数据没有发送到客户端?

  • 未调用 flush() 或未设置 autoFlush=true,数据可能留在缓冲区。

TcpEchoServer:

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;

public class TcpEchoServer {
    // 用于监听客户端连接的服务器Socket
    private ServerSocket serverSocket = null;
    public TcpEchoServer(int port) throws IOException {
        serverSocket = new ServerSocket(port);// 创建ServerSocket并绑定指定端口
    }
    // 启动服务器,循环等待客户端连接
    public void start() throws IOException {
        System.out.println("服务器启动");
        while(true){
            Socket cilentSocket = serverSocket.accept();// 接受客户端连接(具有阻塞等待功能)
            processConnection(cilentSocket);
        }
    }
    private void processConnection(Socket cilentSocket) {
        // 使用try-with-resources确保流自动关闭
        try(InputStream inputStream = cilentSocket.getInputStream();
        OutputStream outputStream = cilentSocket.getOutputStream()){
            // 使用 Scanner 读取客户端发送的数据
            Scanner scanner = new Scanner(inputStream);
            // 使用 PrintWriter 向客户端写回响应数据
            PrintWriter printWriter = new PrintWriter(outputStream);
            // 持续处理客户端请求
            while (true){
                // 判断是否还有输入数据
                if(!scanner.hasNext()){
                    // 客户端关闭连接
                    System.out.printf("[%s,%d] 客户端下线\n",cilentSocket.getInetAddress(),cilentSocket.getPort());
                    break;
                }
                // 1.读取客户端请求
                String request = scanner.next();
                // 2.根据请求处理请求
                String response = process(request);
                // 3.将响应写回客户端
                printWriter.println(response);
                printWriter.flush();// 立即刷新缓冲区,确保数据被发送
                // 打印日志
                System.out.printf("[%s,%d] req:%s resp:%s",cilentSocket.getInetAddress(),cilentSocket.getPort(),request,response);
            }
        }catch (IOException e) {
            throw new RuntimeException(e);
        }finally {
            try{
                //cilentSocket每个客户端都有一个,随着客户端越来越多,消耗的socket也越来越多
                //如果不及时释放,可能会“耗尽文件描述符”导致崩溃
                cilentSocket.close();
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
    }
    private String process(String request){
        return request;// 直接返回请求内容(简单回显实现)
    }

    public static void main(String[] args) throws IOException {
        TcpEchoServer tcpEchoServer = new TcpEchoServer(9096);
        tcpEchoServer.start();
    }
}

TcpEchoClient:

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Scanner;

public class TcpEchoClient {
    private Socket socket = null;
    public TcpEchoClient(String serverIP,int serverPort) throws IOException {
        // 这个 new 操作完成之后, 就完成了 tcp 连接的建⽴.
        //可以将ip与port直接传给socket对象
        socket = new Socket(serverIP,serverPort);
    }
    private void start(){
        System.out.println("客户端启动");
        try(InputStream inputStream = socket.getInputStream();
            OutputStream outputStream = socket.getOutputStream()){
            Scanner scan = new Scanner(System.in);
            Scanner scanner = new Scanner(inputStream);
            PrintWriter printWriter = new PrintWriter(outputStream);
            while (true){
                // 1. 从控制台输⼊字符串
                System.out.print("->");
                if(!scan.hasNext()){
                    break;
                }
                String request = scan.next();
                // 2. 把请求发送给服务器
                printWriter.println(request);
                printWriter.flush();// 立即刷新缓冲区,确保数据被发送
                // 3. 从服务器读取响应
                String response = scanner.next();
                // 4. 把响应打印出来
                System.out.println(response);
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    public static void main(String[] args) throws IOException {
        TcpEchoClient tcpEchoClient = new TcpEchoClient("127.0.0.1",9096);
        tcpEchoClient.start();
    }
}

   


1. 在使用 Scanner 读取用户输入时,通常推荐使用 next() 方法而不是 nextLine(),因为 next() 以空白符(如空格、制表符、换行符等)作为输入的分隔符,能够较稳定地读取单个词或指令。而 nextLine() 会读取整行内容,直到遇到换行符为止,用户按下 Enter 键后会将整行(包括隐藏字符如 \r\n)一并作为输入内容,这可能导致读取结果出现空行或意外字符,尤其是在混合使用多种输入方法(如先用 next() 再用 nextLine())时容易出错。因此,如果输入仅为单个词或命令,使用 next() 更加简洁可靠;如果需要读取整句或含空格的完整输入内容,则应小心使用 nextLine() 并注意清除缓冲区中的残留换行符。

2. 在 ServerSocket 程序中,serverSocket 是唯一的监听对象,通常在服务器启动后持续运行,只有在程序退出或服务器关闭时才会释放资源。相比之下,每当有一个客户端连接进来,服务器就会为其创建一个独立的 clientSocket 对象。如果服务器不及时关闭这些客户端 clientSocket,随着连接数的增多,系统的文件描述符资源会逐渐被占满,最终可能导致程序抛出 “Too many open files” 错误,从而崩溃。因此,在每次处理完客户端请求后,必须在 finally 块中显式关闭对应的客户端 clientSocket,以确保资源及时释放,保证服务器的稳定性和可持续运行。

1. 多个客户端访问服务器:

1. 问题:

当多个客户端访问服务器时,我们发现服务器只能与第一个访问服务器的客户端建立连接,其余客户端必须等上一个客户端退出时才能与服务器建立连接,这是为什么?

  • serverSocket.accept():这行代码会阻塞,直到有一个客户端连接;

  • 然后 processConnection()持续处理这个客户端的所有请求,直到客户端关闭连接之前不会结束

  • 在这个过程中,服务器主线程是“卡”在这个 processConnection() 方法中,不能继续 accept() 下一个客户端。

结果就是:

  • 单线程、串行 的服务器只能同时服务一个客户端。

  • 后续客户端虽然连接请求已发出,但由于 accept() 没有再次被调用,连接处于排队状态(操作系统层面的 TCP backlog)。

  • 只有当前客户端断开连接,processConnection() 结束后,主线程才能返回到循环顶部,再 accept() 下一个客户端。

2. 解决方法:

1. 服务器引入多线程/并发:

要实现多个客户端同时访问服务器,你需要让服务器在 accept() 之后,为每一个客户端连接启动一个线程 来处理它,这样主线程可以继续接受下一个客户端连接:

    // 启动服务器,循环等待客户端连接
    public void start() throws IOException {
        System.out.println("服务器启动");
        while(true){
            Socket cilentSocket = serverSocket.accept();// 接受客户端连接(具有阻塞等待功能)
            //创建线程
            Thread t = new Thread(()->{
                processConnection(cilentSocket);
            });
            t.start();
        }
    }

2. 服务器引⼊线程池:

为了避免频繁创建销毁线程, 也可以引⼊线程池
  // 启动服务器,循环等待客户端连接
    public void start() throws IOException {
        System.out.println("服务器启动");
        //创建线程池
        ExecutorService pool = Executors.newCachedThreadPool();
        while(true){
            Socket cilentSocket = serverSocket.accept();// 接受客户端连接(具有阻塞等待功能)
            //提交任务
            pool.submit(()->{
                processConnection(cilentSocket);
            });
        }
    }

当多个客户端连接处理耗时较长时,仅仅依靠传统的多线程方式(如为每个连接开启一个线程)会导致以下问题:

  • 线程开销大(创建、上下文切换成本高)

  • 内存占用高(每个线程都有独立栈空间)

  • 无法有效处理“高并发+长连接”

1. 协程(轻量级线程)

协程(coroutine)是一种比线程更轻量的并发机制。常运行于用户态,用户态可以手动调度的方式让一个线程“并发”的做多个任务,相比于传统线程,它们的调度不依赖操作系统,而由程序自身控制。多个协程可以复用一个线程,协程之间的切换不需要内核态参与,因此切换速度快、资源开销小。

在 Java 中,可以通过 虚拟线程(Virtual Thread,Java 21+) 实现协程模型,从而大幅提升并发处理能力。协程特别适合处理 I/O 阻塞型任务,比如网络通信、数据库访问等。

2.  IO 多路复用

I/O 多路复用指的是用一个线程监控多个连接通道(Channel),只有当某个通道就绪时才处理它。

I/O 多路复用是一种高效的事件驱动模型,允许单个线程同时监控多个 Socket 连接的 I/O 状态(如是否可读可写)。常见实现包括:

  • Linux 的 select, poll, epoll

  • Java 的 java.nio.Selector

虽然一个线程可能维护了数万个 socket,但在同一时刻,真正处于“活跃状态”(即可读、可写)的连接通常是极少数。大部分连接处于“等待状态”,并不会频繁发生 I/O 操作。

通过 I/O 多路复用,线程只在真正需要处理数据时才进行读取或写入,从而极大提高了线程利用率和系统并发能力,特别适合于高并发 + I/O 密集型场景(如聊天室、消息中间件、网关服务等)。

 

 


网站公告

今日签到

点亮在社区的每一天
去签到