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

2. Socket 套接字
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字节)创建对象。 |
|
static InetAddress getByAddress(String host, byte[] addr) |
指定主机名和字节数组创建对象(主机名可忽略)。 |
|
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(用户数据报协议)的核心类在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();
}
}


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 密集型场景(如聊天室、消息中间件、网关服务等)。