Java网络编程
网络通信三要素
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端口”才能唯一确定一个应用程序的通信端点。
具体解释:
- 独立的命名空间:
TCP和UDP的端口号处于不同的“命名空间”。这意味着同一个端口号,可以同时用于TCP和UDP,而它们对应的是两个完全不同的应用程序。- 例如:一台服务器上可以同时开启:
- TCP端口 53:用于提供DNS域名解析的“区域传输”功能(需要可靠连接)。
- UDP端口 53:用于响应普通的DNS查询请求(速度快,一次请求一次回复即可)。
虽然都是53端口,但因为协议不同,所以不会冲突。
- 例如:一台服务器上可以同时开启:
- 协议+端口:真正的目的地标识:
一个网络连接的真实终点(套接字 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)。
- 例如:
- 独立的命名空间:
-
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);
}
}

InetSocketAddress 和Socket的区别
InetSocketAddress 和 Socket 的区别是“地址”和“连接本身”的区别。
| 特性 | 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网络模型是参考模型
传输层的2个通信协议
-
UDP(User Datagram Protocol):用户数据报协议;
-
TCP(Transmission Control Protocol):传输控制协议。
UDP协议
-
特点:无连接、不可靠通信。通信效率高。
-
不事先建立连接,数据按照包发,一包数据包含:自己的IP、程序端口,目的地IP、程序端口和数据(限制在64KB内)等。
-
发送方不管对方是否在线,数据在中间丢失也不管,如果接收方收到数据也不返回确认,故是不可靠的。
TCP协议
-
特点:面向连接、可靠通信。通信效率相对不高,胜在可靠。
-
TCP的最终目的:要保证在不可靠的信道上实现可靠的传输。
-
TCP主要有三个步骤实现可靠传输:三次握手建立连接,传输数据进行确认,四次挥手断开连接。
-
三次握手建立连接:
-
可靠连接:确定通信双方,收发消息都是正常无问题的!(全双工)
-
-
-
客户端发送连接请求
-
服务端收到客户端发的连接请求:服务端知道了客户端发信息没问题
-
服务端发送响应
-
客户端收到服务端请求:客户端知道了服务端收消息和发信息没问题(服务端收到了客户端的回包,才会回响应包,所以收消息和发信息都没问题)
-
客户端再次发送确认请求
-
服务端收到客户端发的确认请求:服务端知道了客户端收信息没问题
-
-
建立连接后,传输数据可靠:传输数据会进行确认,以保证数据传输的可靠性。
- 客户端发送给服务端后,必须等收到服务端的确认包后,才觉得数据传输成功,否则客户端会重发,这样就保证了可靠性。
-
-
四次握手断开连接:
- 目的:确保双方数据的收发都已经完成!
-
-
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通信:支持与多个客户端同时通信的原理
群聊实现:端口转发
实现简易版的B/S架构
补充: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。
Channel 和 Socket 对比
对比表格
| 特性维度 | Socket (java.net.Socket) | Channel (java.nio.channels.Channel) |
|---|---|---|
| 层次与抽象 | 底层抽象,是对操作系统Socket API的直接封装。 | 高层抽象,是JVM层对IO连接(包括Socket)的封装。 |
| 模型 | 阻塞IO (BIO) 模型。每个连接需要一个线程,线程资源消耗大。 | 非阻塞IO (NIO) 模型的核心。支持多路复用 (Selector),一个线程可管理多个连接。 |
| 数据操作 | 通过 InputStream 和 OutputStream 字节流进行读写。 | 通过 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.net 和 java.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):通过
InputStream和OutputStream进行数据读写,是单向的、连续的字节流。
核心类:
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:代表一个到实体(如文件、套接字)的开放连接,可以进行非阻塞的读写操作。
SocketChannel和ServerSocketChannel是关键的网络通道。
核心类:
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 的优秀网络框架,如 Netty 或 Mina。这些框架对复杂的 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为例):
- 建立连接:客户端与服务器进行 TCP 三次握手,成功建立连接。
- 发送请求:客户端通过连接发送 HTTP Request 报文。
- 接收响应:服务器处理请求,返回 HTTP Response 报文。
- 断开连接:客户端和服务器进行 TCP 四次挥手,主动断开本次连接。
- 下一次请求:如果需要发送新的请求,必须重复步骤1-4,重新建立一个新的TCP连接。
使用okhttp3发起的一个请求,是长连接还是断连接
- 是长连接:OkHttp 内置了连接池(Connection Pool),默认会复用 HTTP/1.x 和 HTTP/2 的连接,以实现长连接的效果。
- 自动管理:你无需在代码中做任何特殊操作,OkHttp 会自动为你处理连接的建立、复用和淘汰。
工作原理
- 第一次请求:
- 你的代码通过 OkHttpClient 发起一个请求(如
GET https://api.example.com/data)。 - OkHttp 的连接池是空的,所以它会创建一个新的 TCP 连接,完成与服务器的三次握手。
- 发送请求并接收响应。
- 请求结束后,连接不会立即关闭,而是被释放回连接池中,标记为“空闲可用”状态。
- 你的代码通过 OkHttpClient 发起一个请求(如
- 第二次请求(相同目标主机):
- 你很快又发起了另一个请求(如
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 使用长连接?
- 高性能:避免频繁的 TCP 握手/挥手开销,这是高并发系统的关键优化。
- 低延迟:数据随时可以发送,没有建立连接的延迟。
- 实时性:适合实时通信场景(如IM、游戏、物联网),服务器可以主动推送消息。
- 状态保持:可以在
Channel的AttributeMap中保存会话状态。
心跳机制:保持长连接的活力
由于是长连接,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报文 |
| 生命周期 | 显式创建和关闭 | 由连接池自动管理 |
典型的长连接应用场景
- 即时通讯 (IM):微信、QQ等,消息随时可达
- 游戏服务器:玩家状态实时同步
- 物联网 (IoT):设备持续上报数据
- 金融交易系统:实时行情推送
- RPC 框架:Dubbo、gRPC 等底层通信
总结
Netty 的连接是真正的 TCP 长连接,这是其架构设计的基石。它不像 HTTP 那样需要为每个请求考虑连接的建立和关闭,而是建立一条持久化的通信管道,在这条管道上进行高效、双向的数据流动。
这种设计使得 Netty 特别适合构建需要高性能、低延迟、实时通信的网络应用程序。你需要做的不是决定用长连接还是短连接,而是如何更好地管理和利用这条长连接(比如通过心跳保活、流量控制等)。


浙公网安备 33010602011771号