Java网络编程

网络通信三要素

IP地址、端口号、协议

IP地址

IP地址相关

IP地址

InetAddress

lnetAddress代表IP地址。
InetAddress的常用方法如下:

名称 说明
public static InetAddress getLocalHost() 获取本机IP,会以一个inetAddress的对象返回
public static InetAddress getByName(String host) 根据ip地址或者域名,返回一个inetAdress对象
public string getHostName() 获取该ip地址对象对应的主机名。
public string getHostAddress() 获取该ip地址对象中的ip地址信息。
public boolean isReachable(int timeout) 在指定毫秒内,判断主机与该ip对应的主机是否能连通
public class IPAddress {
    public static void main(String[] args) throws Exception {
        // 获取本机ip地址对象
        InetAddress ip1 = InetAddress.getLocalHost();
        System.out.println(ip1.getHostName());
        System.out.println(ip1.getHostAddress());
        // 获取指定ip地址或域名的对象
        InetAddress ip2 = InetAddress.getByName("www.baidu.com");
        System.out.println(ip2.getHostName());
        System.out.println(ip2.getHostAddress());
        // 检测是否连通 相当于 ping www.baidu.com
        System.out.println(ip2.isReachable(6000));
    }
}
// 输出
// DESKTOP-886HS87
// 192.168.0.101

// www.baidu.com
// 180.101.51.73

// true

端口号

端口号相关

端口号:标记正在计算机设备上运行的应用程序的,被规定为一个16位的二进制,范围是0~65535(TCP/IP协议中端口号使用16位无符号整数表示,其数值范围为0-65535(2^16-1))。

  • 端口0通常保留为特殊用途(如表示所有端口),实际可用端口为1-65535。‌‌
  • 端口分类与使用
    • 系统保留端口‌:0-1023,预留给HTTP(80)、HTTPS(443)等知名服务。‌‌
    • 注册端口‌:1024-49151,分配给用户进程或者某些应用程序。‌‌Tomcat:8080,MySQL:3306,Oracle:1521
    • 动态/私有端口‌:49152-65535,客户端临时使用,由操作系统随机分配,一般不固定分配给某些进程,而是动态分配的。‌‌

注意:我们自己开发的程序一般选择使用注册端口,且一个设备中不能出现两个程序的端口号一样,否则出错。

  • 一个应用程序可以占用多个端口号。端口号如果被一个应用程序占用了,那么其他的应用程序就无法再使用这个端口号了。

  • 记住一点,我们编写的程序要占用端口号的话占用1024以上的端口号,1024以下的端口号不要去占用,因为系统有可能会随时征用。端口号本身又分为TCP端口和UDP端口,TCP的8888端口和UDP的8888端口是完全不同的两个端口。TCP端口和UDP端口都有65536个

    • TCP和UDP是两种传输数据的规则和方式,一个可靠有序,一个高效快速。

    • 门牌号(端口)可以被这两种不同的规则(协议)独立使用

    • 因此,必须说“TCP的80端口”或“UDP的53端口”才能唯一确定一个应用程序的通信端点

      具体解释:

      1. 独立的命名空间
        TCP和UDP的端口号处于不同的“命名空间”。这意味着同一个端口号,可以同时用于TCP和UDP,而它们对应的是两个完全不同的应用程序
        • 例如:一台服务器上可以同时开启:
          • TCP端口 53:用于提供DNS域名解析的“区域传输”功能(需要可靠连接)。
          • UDP端口 53:用于响应普通的DNS查询请求(速度快,一次请求一次回复即可)。
            虽然都是53端口,但因为协议不同,所以不会冲突。
      2. 协议+端口:真正的目的地标识
        一个网络连接的真实终点(套接字 Socket)是由 协议类型(TCP或UDP)IP地址端口号 三者共同唯一确定的。
        格式为:(TCP或UDP, IP地址, 端口号)
        • 例如:(TCP, 192.168.1.100, 80) 指向该IP地址上正在监听TCP 80端口的Web服务器(如Nginx、Apache)。
        • (UDP, 192.168.1.100, 80) 则指向一个完全不同的、处理UDP协议80端口的应用程序(虽然很少有服务用UDP 80)。

Java 网络编程_java实现计算机网络-CSDN博客

image

InetSocketAddress类

说到端口,则要引入一个类:InetSocketAddress

此类实现 IP 套接字地址(IP 地址 + 端口号)。

1.构造方法摘要

方法 说明
InetSocketAddress(InetAddress addr, int port) 根据 IP 地址和端口号创建套接字地址。
InetSocketAddress(int port) 创建套接字地址,其中 IP 地址为通配符地址,端口号为指定值。
InetSocketAddress(String hostname, int port) 根据主机名和端口号创建套接字地址。

2.常用方法摘要

方法 说明
InetAddress getAddress() 获取 InetAddress。
String getHostName() 获取 hostname。
int getPort() 获取端口号。
public class TestPort {
    public static void main(String[] args) {
        InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1",8082);
        System.out.println(inetSocketAddress);
        //返回主机名
        System.out.println(inetSocketAddress.getHostName());
        //获得InetSocketAddress的端口
        System.out.println(inetSocketAddress.getPort());
        //返回一个InetAddress对象(IP对象)
        InetAddress address = inetSocketAddress.getAddress();
        System.out.println(address);
    }
}

image

InetSocketAddress 和Socket的区别

InetSocketAddressSocket 的区别是“地址”和“连接本身”的区别。

特性 InetSocketAddress (地址信息) Socket (连接本身)
本质 一个数据结构,一个信息容器 一个网络端点,一个通信工具
代表什么 “在哪里” (IP和Port)。一个目的地。 “已建立的连接”。一条通路。
功能 存储和表示地址信息。没有数据传输能力。 建立、管理、并进行实际的数据传输。
内容 主要包含:IP地址 (或主机名) 和 端口号 包含: 1. 本地IP和Port 2. 远程IP和Port (InetSocketAddress) 3. InputStream (输入流) 4. OutputStream (输出流)
生命周期 很简单。创建后内容不变,用完可丢弃。 很复杂。有创建、连接、通信、关闭等状态。需要显式管理。
类比 信封上的地址 一封已经寄出并正在传递的信
代码示例:它们如何协同工作
场景一:客户端建立连接

客户端需要先知道服务器的地址InetSocketAddress),然后使用这个地址来创建连接Socket)。

// 1. 创建一个“地址”对象:目标服务器是 example.com 的 80 端口
// 这只是一个信息描述,还没有任何网络活动
InetSocketAddress serverAddress = new InetSocketAddress("www.example.com", 80);

// 2. 创建一个“连接”对象 (Socket)
// 此时Socket还未连接,像一部还没拨号的电话
Socket socket = new Socket();

try {
    // 3. 核心步骤:使用Socket对象去连接那个地址
    // 这时才发生真正的网络操作(三次握手等)
    socket.connect(serverAddress);

    // 4. 连接成功后,才能通过Socket获取数据进行通信
    OutputStream out = socket.getOutputStream();
    InputStream in = socket.getInputStream();
    // ... 读写数据 ...

} finally {
    // 5. 通信完毕,必须关闭连接(挂断电话)
    socket.close();
}

// serverAddress 对象在这里还可以继续使用,它只是一个信息,关闭连接不影响它。
场景二:服务器端接受连接

服务器端先在一个地址上等待,当有客户端连接时,接受它并得到一个与客户端通信的连接Socket)。

// 1. 创建一个“服务器地址”对象:在本机所有网卡(0.0.0.0)的8080端口上监听
InetSocketAddress serverListenAddress = new InetSocketAddress(8080);

// 2. 创建一个“服务器连接”对象 (ServerSocket)
// 它不像电话,更像一个总机接线员
ServerSocket serverSocket = new ServerSocket();

// 3. 让“接线员”在指定的“地址”上等待“来电”
serverSocket.bind(serverListenAddress);

System.out.println("服务器已启动,正在监听: " + serverSocket.getLocalSocketAddress());

// 4. 等待一个客户端连接。这是一个阻塞方法,直到有客户端打来电话。
// accept()方法返回的是一个全新的Socket对象,这个对象代表与**那个特定客户端**的**一条独立连接**。
Socket clientSocket = serverSocket.accept();

// 5. 我们可以从这条连接中获取客户端的地址信息
InetSocketAddress clientAddress = (InetSocketAddress) clientSocket.getRemoteSocketAddress();
System.out.println("接收到来自客户端的连接: " + clientAddress.getHostString() + ":" + clientAddress.getPort());

// 6. 通过这个clientSocket与客户端进行通信...
// (获取其输入输出流)

// 7. 关闭与这个客户的连接
clientSocket.close();
// 关闭服务器(可选)
serverSocket.close();
总结
  • InetSocketAddress 是“地址”:它是被使用的信息,用来描述网络上的一个位置。它是Socket操作的参数

  • Socket 是“连接”:它是进行通信的实体,拥有状态和行为(连接、传输数据、关闭)。它使用InetSocketAddress,并且在其内部也包含着本地和远端的InetSocketAddress

  • 简单记:先有地址(InetSocketAddress),再用这个地址去建立连接(Socket)。

协议

通信协议:网络上通信的设备,事先规定的连接规则,以及传输数据的规则被称为网络通信协议。

开放式网络互联标准:OSI网络参考模型(全球网络互联标准)
TCP/IP网络模型:实际上的国际标准,OSI网络模型是参考模型

image-20250309154350970

传输层的2个通信协议

  • UDP(User Datagram Protocol):用户数据报协议;

  • TCP(Transmission Control Protocol):传输控制协议。

UDP协议

  • 特点:无连接、不可靠通信。通信效率高。

  • 不事先建立连接,数据按照包发,一包数据包含:自己的IP、程序端口,目的地IP、程序端口和数据(限制在64KB内)等。

  • 发送方不管对方是否在线,数据在中间丢失也不管,如果接收方收到数据也不返回确认,故是不可靠的。

TCP协议

协议-TCP

  • 特点:面向连接、可靠通信。通信效率相对不高,胜在可靠。

  • TCP的最终目的:要保证在不可靠的信道上实现可靠的传输。

  • TCP主要有三个步骤实现可靠传输:三次握手建立连接,传输数据进行确认,四次挥手断开连接。

  • 三次握手建立连接:

    • 可靠连接:确定通信双方,收发消息都是正常无问题的!(全双工)

    • image-20250309154350970
    • image-20250309154350970
      • 客户端发送连接请求

      • 服务端收到客户端发的连接请求:服务端知道了客户端发信息没问题

      • 服务端发送响应

      • 客户端收到服务端请求:客户端知道了服务端收消息和发信息没问题(服务端收到了客户端的回包,才会回响应包,所以收消息和发信息都没问题)

      • 客户端再次发送确认请求

      • 服务端收到客户端发的确认请求:服务端知道了客户端收信息没问题

    • 建立连接后,传输数据可靠:传输数据会进行确认,以保证数据传输的可靠性。

      • 客户端发送给服务端后,必须等收到服务端的确认包后,才觉得数据传输成功,否则客户端会重发,这样就保证了可靠性。
  • 四次握手断开连接:

    • 目的:确保双方数据的收发都已经完成!
    • image-20250309154350970
    • image-20250309154350970

Java实现UDP通信

Java提供了一个java.net.DatagramSocket类来实现UDP通信。

DatagramSocket:用于创建客户端、服务端

构造器 说明
public DatagramSocket( ) 创建客户端的Socket对象,系统会随机分配一个端口号。
public DatagramSocket(int, port) 创建服务端的Socket对象,并指定端口号。
方法 说明
public void send(DatagramPacket dp) 发送数据包
public void receive(DatagramPacket p) 使用数据包接收数据

DatagramPacket:创建数据包

构造器 说明
public DatagramPacket(byte[] buf,int length,InetAddress address, int port) 创建发出去的数据包对象
public DatagramPacket( byte[] buf, int length) 创建用来接收数据的数据包
方法 说明
public int getLength() 获取数据包,实际接收到的字节个数

实例:

public class Client {
    public static void main(String[] args) throws Exception {
        // 创建客户端对象(发韭菜出去的人)
        DatagramSocket socket = new DatagramSocket(7777);
        // 创建数据包对象封装要发出去的数据(创建一个韭菜盘子)
        byte[] bytes = "我是快乐的客户端,我爱你abc".getBytes();
        DatagramPacket packet = new DatagramPacket(bytes, bytes.length, InetAddress.getLocalHost(), 6666);
        // 开始正式发送这个数据包的数据出去了
        socket.send(packet);
        System.out.println("客户端数据发送完毕~~");
        socket.close();// 释放资源!
    }
}
public class Server {
    public static void main(String[] args) throws Exception {
        System.out.println("服务端启动!");
        // 创建服务端对象(接收韭菜的人)
        DatagramSocket socket = new DatagramSocket(6666);
        // 创建一个数据包对象,用于接收数据的(创建一个韭菜盘子)
        byte[] buffer = new byte[1024 * 64];// 64KB.
        DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
        // 开始正式使用数据包来接收客户端发来的数据
        socket.receive(packet); // 注意:这里收不到数据的时候会阻塞!!!一直在这等数据
        // 从字节数组中,把接收到的数据直接打印出来
        // 接收多少就倒出多少
        int len = packet.getLength(); // 获取本次数据包接收了多少数据
        String rs = new String(buffer, 0 , len);
        System.out.println(rs);

        // 可以根据包获取客户端的一些信息
        InetAddress address = packet.getAddress();
        System.out.println("客户端ip:" + packet.getAddress());
        System.out.println("客户端端口:" + packet.getPort());
        System.out.println("客户端InetAddress:" + address);

        socket.close();// 释放资源!
    }
}

这个只支持单发单收。

多发多收UDP通信:多发、多收

Java实现TCP通信

客户端

Java提供了一个java.net.Socket类来实现TCP通信。

构造器 说明
public Socket(String host , int port) 根据指定的服务器ip、端口号请求与服务端建立连接,连接通过,就获得了客户端socket
方法 说明
public OutputStream getOutputStream() 获得字节输出流对象
public InputStream getInputStream() 获得字节输入流对象

因为TCP是可靠的,先运行Socket的Client的话,会报异常Connection refused: connect

UDP是不可靠的,先运行DatagramSocket的Client的话,会显示发送成功,它不会管对方收没收到,只管自己发送出去了就行。

public class Client {
    public static void main(String[] args) throws Exception {
        // 1、创建Socket对象,并同时请求与服务端程序的连接。
        Socket socket = new Socket("127.0.0.1", 8888);
        // 2、从socket通信管道中得到一个字节输出流,用来发数据给服务端程序。
        OutputStream os = socket.getOutputStream();
        // 3、把低级的字节输出流包装成数据输出流
        DataOutputStream dos = new DataOutputStream(os);
        // 4、开始写数据出去了
        dos.writeUTF("在一起,好吗?");
        dos.close();
        socket.close(); //释放连接资源
    }
}

服务端

服务端是通过java.net包下的java.net.ServerSocket类来实现的。

构造器 说明
public ServerSocket(int port) 为服务端程序注册端口
方法 说明
public Socket accept() 阻塞等待客户端的连接请求,一旦与某个客户端成功连接,则返回服务端这边的Socket对象。
public class Server {
    public static void main(String[] args) throws Exception {
        System.out.println(" -----服务端启动成对------");
        // 1、创建ServerSocket的对象,同时为服务端注册端口。
        ServerSocket serverSocket = new ServerSocket(8888);
        //2、使用serverSocket对象,调用一个accept方法,等待客户端的连接请求
        Socket socket = serverSocket.accept();
        // 3、从socket通信管道中得到一个字节输入流。
        InputStream is = socket.getInputStream();
        // 4、把原始的字节输入流包装成数据输入流
        DataInputStream dis = new DataInputStream(is);
        // 5、使用数据输入流读取客户端发送过来的消息
        String rs = dis.readUTF();
        System.out.println(rs);
        //其实我们也可以获取客户端的IP地址
        System.out.println(socket.getRemoteSocketAddress());
        dis.close();
        socket.close();
    }
}

这个只支持单发单收。

多发多收TCP通信:多发多收

支持与多个客户端连接

支持与多个客户端连接TCP通信:支持与多个客户端同时通信的原理

image-20250309154350970

群聊实现:端口转发

TCP通信:群聊

image-20250309154350970

实现简易版的B/S架构

TCP通信:实现BS架构

image-20250309154350970 image-20250309154350970 image-20250309154350970

补充:URL类

java.net包下

构造方法 说明
URL(String spec) 根据 String 表示形式创建 URL 对象。
URL(String protocol, String host, int port, String file) 根据指定协议名、主机名、端口号和文件名创建 URL 对象。
URL(String protocol, String host, String file) 根据指定的协议名、主机名和文件名创建 URL。
方法摘要 说明
String getProtocol() 获取此 URL的协议名称。
String getHost() 获取此 URL 的主机名。
int getPort() 获取此 URL 的端口号。
String getPath() 获取此 URL 的文件路径。
String getFile() 获取此 URL 的文件名。
String getQuery() 获取此 URL的查询部分。
URLConnection openConnection() 返回一个URLConnection实例,表示与URL引用的远程对象的URL 。

URLConnection类中又有一个方法:

方法 说明
InputStream getInputStream() 返回从此打开的连接读取的输入流。
public class Test {
    public static void main(String[] args) throws MalformedURLException {
        URL url = new URL("http://localhost:8080/index.jsp?username=Tom&password=123456");
        System.out.println(url.getProtocol());//获取协议名
        System.out.println(url.getHost());//获取主机名
        System.out.println(url.getPort());//获取端口号
        System.out.println(url.getPath());//获取文件路径
        System.out.println(url.getFile());//获取文件名
        System.out.println(url.getQuery());//获取查询名
    }
}
// http
// localhost
// 8080
// /index.jsp
// /index.jsp?username=Tom&password=123456
// username=Tom&password=123456
// URL下载网络资源
public class Test {
    public static void main(String[] args) throws IOException {
        //下载地址
        URL url = new URL("https://img.t.sinajs.cn/t6/style/images/global_nav/WB_logo.png?id=1404211047727");
        //连接到这个资源 HTTP
        HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
        InputStream is = urlConnection.getInputStream();
        FileOutputStream fos = new FileOutputStream("weibo.jpg");
        byte[] buffer = new byte[1024];
        int len=0;
        while ((len=is.read(buffer))!=-1){
            fos.write(buffer,0,len);
        }
        //释放资源
        urlConnection.disconnect();//断开连接
        is.close();
        fos.close();
    }
}

URL、InetSocketAddress、InetAddress对比

URL、InetSocketAddress 和 InetAddress 都可以被看作是“信息容器”。但它们所容纳的“信息”的抽象层级、目的和详细程度完全不同。

特性 InetAddress InetSocketAddress URL
代表什么 IP地址 (他是谁?) IP地址 + 端口 (他在哪栋楼哪个房间?) 协议 + 地址 + 资源路径 (如何找到他并和他做什么业务?)
网络层次 网络层 传输层 应用层
核心内容 IP / 主机名 IP / 主机名 + 端口 协议 + IP/主机名/端口 + 路径/参数
主要用途 地址解析 (DNS) 建立网络连接 (Socket) 定位和访问资源 (如网页、文件)
类比 一个人的身份证号码 公司的总机号码 + 分机号 一份完整的商务合作指南 (含公司地址、总机号、分机号、要找的部门、项目名称、合同编号)

它们的关系是:
一个 URL 包含了一个 InetSocketAddress 的信息(主机和端口)。
一个 InetSocketAddress 包含了一个 InetAddress 的信息(IP地址)。

简单来说:

  • 如果你想做DNS查询,用 InetAddress
  • 如果你想创建Socket连接,用 InetSocketAddress
  • 如果你想下载网页或文件,用 URL
image-20250309154350970

Channel 和 Socket 对比

对比表格

特性维度 Socket (java.net.Socket) Channel (java.nio.channels.Channel)
层次与抽象 底层抽象,是对操作系统Socket API的直接封装。 高层抽象,是JVM层对IO连接(包括Socket)的封装。
模型 阻塞IO (BIO) 模型。每个连接需要一个线程,线程资源消耗大。 非阻塞IO (NIO) 模型的核心。支持多路复用 (Selector),一个线程可管理多个连接。
数据操作 通过 InputStreamOutputStream 字节流进行读写。 通过 Buffer (缓冲区) 进行数据读写,操作更灵活,性能更高。
方向性 双向(全双工),但输入输出流是分开的。 双向(全双工),一个Channel既可读也可写。
连接与状态 需要显式地创建、连接、关闭。状态管理由开发者负责。 状态信息更丰富(如是否可连接、可读、可写),并与Selector配合监听状态变化。
性能与扩展性 连接数多时,线程上下文切换开销巨大,扩展性差(C10K问题)。 基于事件驱动缓冲区,减少了线程数和系统调用,性能高,扩展性好。
使用复杂度 简单直观,易于理解和上手。 概念更多(Channel, Buffer, Selector),编程模型更复杂。

总结与关系

  • 关系: 在Java NIO的网络编程中,SocketChannel 内部封装了一个标准的Socket。你可以通过 SocketChannel.socket() 方法获取到它底层的Socket对象。Channel是在Socket之上提供了一套更高效的API。你可以把Channel看作是Socket的一个“增强版包装”或“视图”,它提供了更强大的操作方式。
  • 如何选择
    • Socket (BIO): 适用于连接数相对较少、且需要快速开发的场景。代码简单直观。
    • Channel (NIO): 适用于需要高并发、高性能的网络服务器开发,如聊天服务器、游戏服务器、RPC框架等。虽然编程模型更复杂,但能更好地利用系统资源,支撑海量连接。

可以把Socket看作是修建铁路的铁轨和路基,它是基础。而Channel则是建立在铁轨之上的高速列车系统,它包含了更智能的调度中心(Selector)、更高效的货箱(Buffer),让运输效率变得极高。

(传统阻塞 I/O) 和 (非阻塞 I/O / New I/O)

它们分别位于Java 中这两个核心网络 I/O 包:java.netjava.nio

特性 java.net (传统阻塞 I/O) java.nio (非阻塞 I/O / New I/O)
编程模型 阻塞 I/O (BIO) 非阻塞 I/O (NIO)异步 I/O
核心类 Socket, ServerSocket, URL, HttpURLConnection Channel, Selector, Buffer
工作方式 每连接一线程 (Thread-Per-Connection) Reactor 模式 (事件驱动)
数据操作 基于 流 (Stream) 基于 缓冲区 (Buffer)
吞吐量 连接数多时,线程上下文切换开销大,性能低 单线程或少量线程处理大量连接,高并发性能好
适用场景 连接数较少、逻辑简单的客户端或小型服务端 高并发、高性能服务端应用(如聊天服务器、游戏服务器)
复杂度 简单直观,易于理解和上手 复杂,需要理解缓冲区、通道、选择器等概念

1. java.net (传统阻塞 I/O)

这个包提供了基于流的、阻塞式的网络编程 API。它是 Java 早期版本中网络编程的标准方式。

核心特点:

  • 阻塞式 I/O (Blocking I/O):当线程执行读 (read) 或写 (write) 操作时,如果数据没有就绪,该线程会被挂起(阻塞),直到数据准备好才会继续执行。在此期间,线程什么也做不了,会浪费宝贵的 CPU 资源。
  • 每连接一线程模型:为了同时处理多个客户端连接,服务器端必须为每一个接入的 Socket 连接创建一个新的线程。当有大量并发连接时,系统会创建大量线程,导致:
    • 巨大的内存消耗:每个线程都需要分配独立的栈内存(通常默认是 1MB)。
    • 高昂的上下文切换开销:CPU 需要花费大量时间在线程之间切换,而不是处理实际业务。
  • 基于流 (Stream):通过 InputStreamOutputStream 进行数据读写,是单向的、连续的字节流。

核心类:

  • Socket:客户端套接字,用于连接到服务器。
  • ServerSocket:服务器端套接字,用于监听客户端连接。
  • URL / HttpURLConnection:用于处理 HTTP 协议的高级 API。

示例代码 (服务器端):

// 传统的 java.net 阻塞式服务器
public class BioServer {
    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket = new ServerSocket(8080);
        System.out.println("服务器启动,端口 8080");

        while (true) {
            // 1. accept() 是阻塞的,没有连接时会一直等待
            Socket clientSocket = serverSocket.accept();
            System.out.println("接收到客户端连接: " + clientSocket);

            // 2. 为每个新连接创建一个新线程
            new Thread(() -> {
                try (InputStream in = clientSocket.getInputStream();
                     OutputStream out = clientSocket.getOutputStream()) {

                    // 3. read() 是阻塞的,如果客户端没有发送数据,线程会卡在这里
                    byte[] buffer = new byte[1024];
                    int len = in.read(buffer);
                    if (len != -1) {
                        String request = new String(buffer, 0, len);
                        System.out.println("收到请求: " + request);

                        // 处理业务逻辑...
                        String response = "Hello, Client!";
                        out.write(response.getBytes());
                        out.flush();
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                } finally {
                    try {
                        clientSocket.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }).start(); // 启动线程
        }
    }
}

缺点:从代码中可以看出,accept()read() 都是阻塞调用。如果连接数暴涨到几千上万,创建如此多的线程对系统来说是灾难性的。


2. java.nio (New I/O / 非阻塞 I/O)

Java 1.4 引入的 java.nio 包,旨在解决 java.net 在高并发场景下的性能瓶颈。它的核心是非阻塞 I/O多路复用

核心特点:

  • 非阻塞 I/O (Non-blocking I/O):线程可以发起一个读/写操作,然后立即返回去做别的事情。它会不断地轮询检查数据是否就绪,而不是傻等。这使得一个线程可以管理多个通道(连接)。
  • 选择器 (Selector):这是 NIO 的核心。一个 Selector 可以同时轮询多个 Channel 上的事件(如连接就绪、读就绪、写就绪)。当某个 Channel 有事件发生时,Selector 会通知应用程序,然后由应用程序处理相应的事件。这实现了 I/O 多路复用,即一个或少量线程可以处理成千上万的连接
  • 基于通道 (Channel) 和缓冲区 (Buffer)
    • Buffer:一个固定大小的数据容器,所有的读写操作都是通过缓冲区进行的。提供了对数据的结构化访问(如 flip, clear 操作)。
    • Channel:代表一个到实体(如文件、套接字)的开放连接,可以进行非阻塞的读写操作。SocketChannelServerSocketChannel 是关键的网络通道。

核心类:

  • Selector:多路事件选择器。
  • ServerSocketChannel:用于服务器监听的非阻塞通道。
  • SocketChannel:用于客户端连接的非阻塞通道。
  • Buffer:及其子类(如 ByteBuffer, CharBuffer)。

示例代码 (服务器端核心逻辑):

// java.nio 非阻塞服务器核心逻辑
public class NioServer {
    public static void main(String[] args) throws IOException {
        // 1. 创建 Selector
        Selector selector = Selector.open();

        // 2. 创建非阻塞的 ServerSocketChannel
        ServerSocketChannel serverChannel = ServerSocketChannel.open();
        serverChannel.configureBlocking(false); // 设置为非阻塞模式!
        serverChannel.socket().bind(new InetSocketAddress(8080));

        // 3. 将 ServerSocketChannel 注册到 Selector,关注 ACCEPT 事件
        serverChannel.register(selector, SelectionKey.OP_ACCEPT);
        System.out.println("NIO 服务器启动,端口 8080");

        while (true) {
            // 4. 阻塞,直到有注册的事件发生
            selector.select();

            // 5. 获取所有发生事件的 SelectionKey
            Iterator<SelectionKey> keyIterator = selector.selectedKeys().iterator();

            while (keyIterator.hasNext()) {
                SelectionKey key = keyIterator.next();
                keyIterator.remove(); // 处理完后移除

                if (key.isAcceptable()) {
                    // 有新的客户端连接
                    ServerSocketChannel server = (ServerSocketChannel) key.channel();
                    SocketChannel clientChannel = server.accept();
                    clientChannel.configureBlocking(false);
                    // 将新连接注册到 Selector,关注 READ 事件
                    clientChannel.register(selector, SelectionKey.OP_READ);
                    System.out.println("客户端连接: " + clientChannel);

                } else if (key.isReadable()) {
                    // 有数据可读
                    SocketChannel clientChannel = (SocketChannel) key.channel();
                    ByteBuffer buffer = ByteBuffer.allocate(1024);
                    int len = clientChannel.read(buffer);
                    if (len > 0) {
                        buffer.flip(); // 切换为读模式
                        String request = StandardCharsets.UTF_8.decode(buffer).toString();
                        System.out.println("收到请求: " + request);

                        // 处理业务逻辑,并准备响应
                        String response = "Hello, NIO Client!";
                        ByteBuffer respBuffer = StandardCharsets.UTF_8.encode(response);
                        clientChannel.write(respBuffer);
                    } else if (len == -1) {
                        // 客户端关闭连接
                        clientChannel.close();
                    }
                }
            }
        }
    }
}

优点:在这个例子中,只有一个主线程在运行,它通过一个 Selector 管理了所有的事件(新连接接入、数据读取)。无论来 100 个还是 10000 个连接,这个线程数基本保持不变,极大地提升了系统的可扩展性和性能。


总结与选择

  • 使用 java.net 当:
    • 开发客户端应用内部工具
    • 构建连接数可控(并发不高)的简单服务器
    • 追求开发速度和代码简单性,而非极致性能。
  • 使用 java.nio 当:
    • 需要构建高性能、高并发的服务器端应用(如消息推送、即时通讯、游戏服务器等)。
    • 需要处理数以万计的并发连接
    • 愿意接受更高的代码复杂度以换取更好的性能。

重要提示:直接使用原生 NIO API 进行编程非常复杂,尤其是要处理断线重连、粘包拆包等问题。因此,在实践中,我们通常会使用基于 NIO 的优秀网络框架,如 NettyMina。这些框架对复杂的 NIO API 进行了极佳封装,提供了简单易用的接口和强大的功能,是构建高性能网络应用的事实标准。可以说,学习 java.nio 的核心价值在于理解其原理,以便更好地使用 Netty 这样的框架。

短连接和长连接

特性 短连接 (Short-Lived Connection) 长连接 (Long-Lived Connection / Persistent Connection)
生命周期 一次请求-响应后立即关闭 多次请求-响应复用同一个连接,保持一段时间后关闭
建立/关闭频率 (每次通信都要经历TCP三次握手和四次挥手) (只需一次建立和最终一次关闭)
性能开销 (大量时间花费在建立和断开连接上) (避免了重复的连接建立开销)
延迟 (Latency) (每次请求都包含握手延迟) (后续请求无握手延迟)
服务器资源 占用内存、CPU(管理大量频繁开闭的Socket) 占用内存(保持连接有状态),但连接总数更少
适用场景 传统HTTP/1.0、请求不频繁的场景(如普通网页浏览) HTTP/1.1+ / HTTP/2 / HTTP/3、数据库连接池、消息推送、实时通信(如WebSocket)

短连接 (Short-Lived Connection)

短连接是指通信双方有数据交互时,建立一个连接发送完数据后立即断开连接

工作流程(以HTTP/1.0为例):

  1. 建立连接:客户端与服务器进行 TCP 三次握手,成功建立连接。
  2. 发送请求:客户端通过连接发送 HTTP Request 报文。
  3. 接收响应:服务器处理请求,返回 HTTP Response 报文。
  4. 断开连接:客户端和服务器进行 TCP 四次挥手主动断开本次连接
  5. 下一次请求:如果需要发送新的请求,必须重复步骤1-4,重新建立一个新的TCP连接。

使用okhttp3发起的一个请求,是长连接还是断连接

  • 是长连接:OkHttp 内置了连接池(Connection Pool),默认会复用 HTTP/1.x 和 HTTP/2 的连接,以实现长连接的效果。
  • 自动管理:你无需在代码中做任何特殊操作,OkHttp 会自动为你处理连接的建立、复用和淘汰。

工作原理

  1. 第一次请求
    • 你的代码通过 OkHttpClient 发起一个请求(如 GET https://api.example.com/data)。
    • OkHttp 的连接池是空的,所以它会创建一个新的 TCP 连接,完成与服务器的三次握手。
    • 发送请求并接收响应。
    • 请求结束后,连接不会立即关闭,而是被释放回连接池中,标记为“空闲可用”状态。
  2. 第二次请求(相同目标主机)
    • 你很快又发起了另一个请求(如 GET https://api.example.com/info)。
    • OkHttp 会首先去连接池中查找。
    • 它会检查是否存在指向相同目标主机(api.example.com:443)的、且处于空闲状态的连接。
    • 如果找到,直接复用这个已有的连接来发送新的请求,完全避免了 TCP 握手和 SSL 握手的开销。
    • 如果没找到(比如连接已被回收或超时),则会创建一个新的连接。

如何验证是长连接?

你可以在拦截器中打印连接信息来直观地看到复用的过程:

OkHttpClient client = new OkHttpClient.Builder()
        .addNetworkInterceptor(new Interceptor() {
            @Override
            public Response intercept(Chain chain) throws IOException {
                Request request = chain.request();
                // 打印使用的连接信息
                Connection connection = chain.connection();
                System.out.println("Request to: " + request.url());
                if (connection != null) {
                    System.out.println("Using connection: " + connection.socket().getLocalPort());
                    System.out.println("Route: " + connection.route().socketAddress());
                }
                return chain.proceed(request);
            }
        })
        .build();

// 连续发起两个请求
Request request1 = new Request.Builder().url("https://httpbin.org/get").build();
Request request2 = new Request.Builder().url("https://httpbin.org/json").build();

try (Response response1 = client.newCall(request1).execute();
     Response response2 = client.newCall(request2).execute()) {
    // 请求完成
}

输出可能会是:

Request to: https://httpbin.org/get
Using connection: 52378 // 本地端口号,代表一个连接
Route: httpbin.org/34.206.106.136:443

Request to: https://httpbin.org/json
Using connection: 52378 // 本地端口号与上一个请求相同!说明复用了连接
Route: httpbin.org/34.206.106.136:443

如果两个请求使用的本地端口号(getLocalPort())相同,就证明它们使用了同一个底层 TCP 连接,即长连接生效。


OkHttp 连接池的关键配置

OkHttpClient 允许你配置连接池的行为来控制长连接的策略:

// 自定义连接池
ConnectionPool pool = new ConnectionPool(
    5,      // 最大空闲连接数
    5,      // 保持存活时间
    TimeUnit.MINUTES // 时间单位
);

OkHttpClient client = new OkHttpClient.Builder()
        .connectionPool(pool)
        .build();
  • 最大空闲连接数:连接池中最多允许保留多少个空闲的长连接。超过这个数量的空闲连接将被关闭。
  • 保持存活时间:一个空闲连接在连接池中最多存活多久。超过这个时间,即使连接数没超,也会被关闭。

默认配置:OkHttp 的默认连接池允许最多 5 个空闲连接,每个连接最多存活 5 分钟。


协议支持

  • HTTP/1.x: OkHttp 通过连接池实现了“线程安全”的连接复用,多个请求可以排队使用同一个连接(串行)。
  • HTTP/2: 支持更强大的多路复用 (Multiplexing),多个请求可以同时在一个连接上并行交错地进行,而无需排队,性能更高。OkHttp 会自动协商并使用 HTTP/2。

总结

特性 OkHttp 的行为
默认行为 长连接
实现机制 内置连接池,自动复用相同主机的空闲连接
优点 减少延迟(避免重复握手)、降低服务器压力提升性能
如何配置 通过 ConnectionPool 配置空闲连接数和存活时间
协议支持 对 HTTP/1.x 和 HTTP/2 都完美支持

因此,当你使用 OkHttp 发起请求时,完全可以放心,它正在智能地使用长连接技术为你优化网络性能。你通常不需要手动干预这个过程。

netty服务器与客户端进行的连接是长连接还是短连接

与 OkHttp 在应用层实现连接复用不同,Netty 在传输层(TCP层) 直接建立了持久化的长连接通道。下面为你详细解释:

核心结论

  • 是长连接:Netty 基于 TCP 协议构建,一旦 Channel 建立成功,这个连接就会一直保持,直到主动关闭或发生异常。
  • 不是请求-响应模型:Netty 的通信模式是基于事件的流式通信,而不是像 HTTP 那样的“一发一收”的请求-响应模型。客户端和服务器可以在任何时间、任意方向地发送数据,连接始终可用。

Netty 长连接的工作机制

1. 连接建立

当 Netty 客户端 (Bootstrap) 成功连接到服务器 (ServerBootstrap) 后,会创建一个 Channel 对象。这个 Channel 就代表了底层的一条 TCP 长连接

// 客户端连接代码
Bootstrap b = new Bootstrap();
b.group(group)
 .channel(NioSocketChannel.class)
 .handler(new ChannelInitializer<SocketChannel>() {
     @Override
     public void initChannel(SocketChannel ch) throws Exception {
         ChannelPipeline p = ch.pipeline();
         p.addLast(new MyClientHandler()); // 自定义处理器
     }
 });

// 建立连接 - 这里创建的就是一条长连接
ChannelFuture f = b.connect("127.0.0.1", 8080).sync();
// 连接成功后,channel() 代表的就是这个长连接通道
Channel channel = f.channel();

2. 数据通信

连接建立后,双方可以通过这个 Channel 进行多次、双向的通信:

// 服务器端处理器示例
public class MyServerHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        // 1. 收到客户端消息(通过长连接)
        System.out.println("Server received: " + msg);
        
        // 2. 通过同一个长连接回复客户端
        ctx.writeAndFlush("Hello Client! Response to: " + msg);
        
        // 3. 可以继续发送更多消息...
        ctx.writeAndFlush("Another message through the same connection");
    }
}

3. 连接保持

只要不主动关闭,这个连接就会一直保持,可以传输无限次的数据:

// 客户端可以持续发送消息
for (int i = 0; i < 100; i++) {
    channel.writeAndFlush("Message " + i);
    Thread.sleep(1000);
}
// 所有100条消息都通过同一个TCP连接发送

4. 连接关闭

需要显式地关闭连接才会终止:

// 主动关闭长连接
channel.close().sync();

为什么 Netty 使用长连接?

  1. 高性能:避免频繁的 TCP 握手/挥手开销,这是高并发系统的关键优化。
  2. 低延迟:数据随时可以发送,没有建立连接的延迟。
  3. 实时性:适合实时通信场景(如IM、游戏、物联网),服务器可以主动推送消息。
  4. 状态保持:可以在 ChannelAttributeMap 中保存会话状态。

心跳机制:保持长连接的活力

由于是长连接,Netty 通常需要配套心跳机制来检测连接是否存活:

// 在 Pipeline 中添加心跳处理器
p.addLast("idleStateHandler", new IdleStateHandler(60, 30, 0));
p.addLast("myHeartbeatHandler", new MyHeartbeatHandler());

// 自定义心跳处理器
public class MyHeartbeatHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) {
        if (evt instanceof IdleStateEvent) {
            // 发送心跳包,保持长连接活跃
            ctx.writeAndFlush(new HeartbeatMessage());
        }
    }
}

与 OkHttp/HTTP 的对比

特性 Netty (TCP长连接) OkHttp (HTTP长连接)
层级 传输层 (TCP) 应用层 (HTTP)
连接概念 一个持久的 Channel 管道 连接池中可复用的连接
通信模式 双向、异步、流式 请求-响应模型
数据格式 自定义协议(需处理粘包拆包) 标准的HTTP报文
生命周期 显式创建和关闭 由连接池自动管理

典型的长连接应用场景

  1. 即时通讯 (IM):微信、QQ等,消息随时可达
  2. 游戏服务器:玩家状态实时同步
  3. 物联网 (IoT):设备持续上报数据
  4. 金融交易系统:实时行情推送
  5. RPC 框架:Dubbo、gRPC 等底层通信

总结

Netty 的连接是真正的 TCP 长连接,这是其架构设计的基石。它不像 HTTP 那样需要为每个请求考虑连接的建立和关闭,而是建立一条持久化的通信管道,在这条管道上进行高效、双向的数据流动。

这种设计使得 Netty 特别适合构建需要高性能、低延迟、实时通信的网络应用程序。你需要做的不是决定用长连接还是短连接,而是如何更好地管理和利用这条长连接(比如通过心跳保活、流量控制等)。

posted @ 2025-08-24 18:14  deyang  阅读(10)  评论(0)    收藏  举报