一、网络编程三要素
1. 要素一:IP 地址(设备的唯一标识)
(1) IPv4(主流,重点掌握)
① 格式: 32 位二进制数,分为 4 个字节(8 位 / 字节),每个字节转换为 0~255 的十进制数,用点号(.)分隔,例如 192.168.1.1、127.0.0.1。
**② 特点:**地址空间有限(约 42 亿个),已面临枯竭;但实现简单、应用广泛。
③ 地址范围: 0.0.0.0 ~ 255.255.255.255。
④ 特殊 IP 地址(开发高频用到)
| 特殊 IP | 作用说明 |
|---|---|
127.0.0.1(IPv4)/ ::1(IPv6) |
本地回环地址,用于测试本机程序(无需真实网络),例如本机客户端连接本机服务器。 |
0.0.0.0(IPv4) |
绑定本机所有网卡的 IP,服务器用该地址绑定端口后,外网和内网均可访问。 |
255.255.255.255 |
广播地址,向同一网段内的所有设备发送数据(UDP 广播常用)。 |
局域网 IP(如192.168.x.x、10.x.x.x) |
内网设备的私有 IP,无法直接被外网访问,需通过路由器 NAT 转发。 |
⑤ Java 中操作 IP 的核心 API:InetAddress
java.net.InetAddress 类封装了 IP 地址的相关操作,无需手动解析 IP 格式,常用方法:
| 方法名 | 作用 |
|---|---|
InetAddress.getByName(String host) |
根据主机名(如"www.baidu.com")或 IP 字符串获取 InetAddress 对象。 |
getHostAddress() |
返回 IP 地址字符串(如"192.168.1.1")。 |
getHostName() |
返回主机名(如"www.baidu.com"),若无法解析则返回 IP。 |
isReachable(int timeout) |
测试是否能连通该 IP 对应的设备(超时时间单位:毫秒)。 |
(2) IPv6(下一代 IP 协议)
① 格式: 128 位二进制数,分为 8 组,每组 4 个十六进制数,用冒号(:)分隔,例如 2001:0db8:85a3:0000:0000:8a2e:0370:7334(可省略前导 0 和连续的 0 组,简化为 2001:db8:85a3::8a2e:370:7334)。
**② 特点:**地址空间极大(约 3.4×10³⁸个),彻底解决 IPv4 地址枯竭问题;支持更多设备联网(如物联网设备)。
2. 要素二:端口号(应用程序的唯一标识)
核心特性
- 范围:0~65535(16 位无符号整数,因为 TCP/UDP 协议用 16 位字段存储端口号)。
- 唯一性:同一设备上,同一协议(TCP/UDP)的端口号不能重复(不同协议可复用,如 TCP 80 和 UDP 80 可同时存在)。
3. 要素三:通信协议(数据传输的规则)
(1) 核心协议:TCP/IP 协议族(互联网基础)
网络通信基于 TCP/IP 协议族(分层架构),开发中重点关注传输层的两个核心协议:TCP 和 UDP。
(2) UDP 协议(用户数据报协议)
① 核心特点
- 无连接:通信前无需建立连接,直接发送数据报(数据包),接收方无需确认。
- 不可靠传输:不保证数据到达、不保证顺序、不重传丢失的数据(可能丢失、乱序)。
- 数据报传输:数据以 "数据报" 为单位传输,每个数据报最大 64KB。
- 效率极高:无连接开销和重传机制,延迟低、资源消耗少。
② 适用场景
需要高效、实时传输的场景:视频通话、语音聊天、直播、UDP 广播、游戏数据传输等(允许少量数据丢失)。
(3) UDP 协议(传输控制协议)
① 核心 API 回顾(发送 / 接收关键类)
| 类名 | 角色 | 核心方法(发送 / 接收相关) |
|---|---|---|
DatagramSocket |
发送 / 接收核心类 | new DatagramSocket(int port):接收端绑定端口;send(DatagramPacket p):发送数据报;receive(DatagramPacket p):接收数据报(阻塞) |
DatagramPacket |
数据报封装类 | 发送用:new DatagramPacket(byte[] data, int len, InetAddress ip, int port)(含目标地址 + 端口);接收用:new DatagramPacket(byte[] buf, int len)(含缓冲区) |
InetAddress |
IP 地址封装类 | getByName(String host):获取目标 IP 地址对象 |
② UDP 接收数据步骤
- 创建
DatagramSocket对象,绑定指定端口(必须绑定,否则无法接收数据); - 创建字节数组缓冲区(用于存储接收的数据);
- 创建接收用的
DatagramPacket对象,关联缓冲区; - 调用
DatagramSocket.receive()方法(阻塞),等待接收数据报; - 从
DatagramPacket中提取数据(getData())、发送端 IP(getAddress())、发送端端口(getPort()); - 处理数据后,关闭
DatagramSocket。
③ UDP 发送数据步骤
- 创建
DatagramSocket对象(可选绑定端口,不绑定则随机分配); - 准备要发送的数据(转为字节数组);
- 获取目标 IP 地址(
InetAddress)和端口; - 创建发送用的
DatagramPacket对象,封装数据、目标 IP 和端口; - 调用
DatagramSocket.send()方法发送数据报; - 发送完成后,关闭
DatagramSocket。
(4) 代码示例 1:
java
package demo2;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
public class test6 {
public static void main(String[] args) throws IOException {
DatagramSocket ds = new DatagramSocket(10086);
byte[] bytes = new byte[1024];
DatagramPacket dp = new DatagramPacket(bytes, bytes.length);
ds.receive(dp);
byte[] data = dp.getData();
InetAddress address = dp.getAddress();
int len = dp.getLength();
int port = dp.getPort();
System.out.println("接收到数据" + new String(data,0,len)); // 不太理解
System.out.println("该数据是从" + address + "这台电脑" + port + "这个端口发出的");
ds.close();
}
}
关键逻辑:System.out.println("接收到数据" + new String(data,0,len));
这行代码本质是「创建字符串对象」,再和前面的提示文字拼接后打印。我们把 new String(data, 0, len) 拆成 3 个关键部分,对应 String 类的一个构造方法:
java
new String( 字节数组, 起始索引, 长度 );
① 第一个参数:data(字节数组)------ 存储接收的数据
- 对应你代码里的
byte[] data = dp.getData(); - 是什么?:
data是从DatagramPacket(数据报)里提取的「字节缓冲区」。之前你创建DatagramPacket时,传入了一个 1024 字节的数组bytes(new DatagramPacket(bytes, bytes.length)),dp.getData()就是返回这个缓冲区,接收端收到的数据会存在这个数组里。 - 简单说:
data是个「装数据的容器」,UDP 发送端发过来的字节(比如 "你好牛啊!!!" 转成的 UTF-8 字节),都存在这个容器里。
② 第二个参数:0(起始索引)------ 从容器的哪个位置开始读
- 索引:数组的「位置编号」,Java 里数组索引从 0 开始(第一个元素是索引 0,第二个是 1,以此类推)。
- 为什么是 0?:因为 UDP 接收的数据,是从缓冲区
data的「第一个位置」(索引 0)开始存储的,没有偏移,所以我们从 0 开始读就是正确的。 - 举个反例:如果写 1,就会跳过第一个字节,导致字符串开头少字符或乱码。
③ 第三个参数:len(长度)------ 读容器里的多少个有效字节
- 对应你代码里的
int len = dp.getLength(); - 是什么?:
len是「实际接收到的有效数据长度」(单位:字节)。 - 关键:你创建的缓冲区
bytes是 1024 字节(足够大,防止数据溢出),但 UDP 发送端发过来的数据大概率用不了这么大空间(比如 "你好牛啊!!!" 转成 UTF-8 是 15 字节)。dp.getLength()就是告诉我们:这次接收的数据,实际占了多少个字节。
(5) 代码示例 2:
SendMessageDemo.java
java
package demo3;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.InetAddress;
import java.net.MulticastSocket;
public class SendMessageDemo {
public static void main(String[] args) throws IOException {
MulticastSocket ms = new MulticastSocket();
String s = "你好,你好";
byte[] bytes = s.getBytes();
InetAddress address = InetAddress.getByName("224.0.0.1"); // 不太理解
int port = 10000;
DatagramPacket dp = new DatagramPacket(bytes, bytes.length, address, port);
ms.send(dp);
ms.close();
}
}
关键逻辑:为什么还要再写一次 InetAddress address = InetAddress.getByName("224.0.0.1"); 不是已经知道地址是什么了吗?
224.0.0.1 是「地址的文本字符串」,而 InetAddress 是「Java 网络编程中能直接使用的地址对象」------ 字符串只是 "地址的文字描述",不能直接和 MulticastSocket 等网络 API 配合工作,必须通过 InetAddress 把字符串转换成 "可操作的对象"。
java
package demo3;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.InetAddress;
import java.net.MulticastSocket;
public class ReceiveMessageDemo2 {
public static void main(String[] args) throws IOException {
MulticastSocket ms = new MulticastSocket(10000);
InetAddress address = InetAddress.getByName("224.0.0.1");
ms.joinGroup(address); // 不太理解
byte[] bytes = new byte[1024];
DatagramPacket dp = new DatagramPacket(bytes, bytes.length);
ms.receive(dp);
byte[] data = dp.getData();
int len = dp.getLength();
String ip = dp.getAddress().getHostAddress();
String name = dp.getAddress().getHostName();
System.out.println("ip为" + ip + ",主机名为:" + name + "的人,发送了数据" +
new String(data, 0, len));
}
}
ReceiveMessageDemo.java
java
package demo3;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.InetAddress;
import java.net.MulticastSocket;
public class ReceiveMessageDemo2 {
public static void main(String[] args) throws IOException {
MulticastSocket ms = new MulticastSocket(10000);
InetAddress address = InetAddress.getByName("224.0.0.1");
ms.joinGroup(address); // 不太理解
byte[] bytes = new byte[1024];
DatagramPacket dp = new DatagramPacket(bytes, bytes.length);
ms.receive(dp);
byte[] data = dp.getData();
int len = dp.getLength();
String ip = dp.getAddress().getHostAddress();
String name = dp.getAddress().getHostName();
System.out.println("ip为" + ip + ",主机名为:" + name + "的人,发送了数据" +
new String(data, 0, len));
}
}
(4) TCP 发送与接收数据(面向连接、字节流)
① 核心 API 回顾(发送 / 接收关键类)
| 类名 | 角色 | 核心方法(发送 / 接收相关) |
|---|---|---|
ServerSocket |
服务器端监听类 | new ServerSocket(int port):绑定端口;accept():阻塞等待客户端连接,返回 Socket(通信端点) |
Socket |
客户端 / 通信端点 | new Socket(String ip, int port):客户端连接服务器;getOutputStream():获取发送流;getInputStream():获取接收流 |
| IO 流包装类 | 数据读写辅助 | BufferedWriter(字符流发送)、BufferedReader(字符流接收)、BufferedOutputStream(字节流发送)、BufferedInputStream(字节流接收) |
② TCP 发送数据(客户端、Server)步骤
- 创建
Socket对象,指定服务器的 IP 地址和端口(需与服务器一致); - 通过
Socket的getOutputStream()获取输出流(向服务器发送数据),结合 IO 包装类; - 写入数据到输出流(注意刷新流,避免数据滞留缓冲区);
- 发送完成后,关闭流和
Socket。
③ TCP 接收数据(服务器端、Client)步骤
- 创建
ServerSocket对象,绑定指定端口(如 8888); - 调用
accept()方法,阻塞等待客户端连接(连接成功后得到与该客户端对应的Socket); - 通过
Socket的getInputStream()获取输入流(接收客户端数据),结合 IO 包装类(如BufferedReader、BufferedInputStream)提升效率; - 循环读取流中的数据(直到客户端关闭流或断开连接);
- 处理数据后,关闭流、
Socket、ServerSocket(推荐用try-with-resources自动关闭)。
④ 代码示例
Server.java
java
package demo3;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
public class Server {
public static void main(String[] args) throws IOException {
ServerSocket ss = new ServerSocket(10001);
Socket socket = ss.accept();
InputStream is = socket.getInputStream();
InputStreamReader isr = new InputStreamReader(is);
int b;
while ((b = isr.read()) != -1) {
System.out.print((char) b); // 不太理解
}
socket.close();
ss.close();
}
}
关键逻辑:System.out.print((char) b);
① is.read() 读的是什么?返回的 b 是什么?
is是InputStream(输入流),is.read()的作用是 从客户端发送的数据流中,读取「一个字节」的数据。- 返回值
b是int类型(不是byte):
② (char) b 是什么操作?
这是 强制类型转换 :把 int 类型的 "字节数值",转换成 char 类型的 "字符"。
原理:客户端发送字符时,会先把字符转换成对应的 "字节编码"(比如 ASCII 码、UTF-8 等),服务器读取到的是 "编码后的字节数值",通过 (char) b 可以还原成原来的字符。
举个具体例子:
- 客户端发送字符
'A'→ 客户端会把'A'转换成 ASCII 码 65(对应的字节),通过网络发给服务器; - 服务器
is.read()读到这个字节,返回int类型的 65(也就是b=65); - 执行
(char) 65→ 把数值 65 转换成对应的字符'A'; - 最后
System.out.print('A')→ 控制台打印出A。
Client.java
java
package demo3;
import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
public class Client {
public static void main(String[] args) throws IOException {
Socket socket = new Socket("127.0.0.1",10001);
OutputStream os = socket.getOutputStream();
os.write("你好你好".getBytes());
os.close();
socket.close();
}
}
二、综合练习
1. 使用 TCP 套接字实现客户端与服务器的字符信息交互
请编写 Java 程序实现以下功能:(1) 服务器端功能:
- 创建
ServerSocket监听 10001 端口; - 调用
accept()方法接受客户端的Socket连接; - 通过
Socket获取字节输入流,并用InputStreamReader转换为字符流; - 循环读取字符流数据,将其转换为字符后在控制台打印,直到读取结束;
- 操作完成后关闭
Socket和Server1。
(2) 客户端功能:
- 创建
Socket连接到本地(127.0.0.1)10001 端口的服务器; - 创建
Scanner对象读取控制台输入(只需创建一次,无需置于循环内); - 通过
Socket获取字节输出流,循环提示用户输入信息并发送给服务器; - 若用户输入 "886",则终止循环并关闭
Socket。
(3) 程序需声明抛出 IOException(无需捕获,直接通过 throws 声明)。
要求:
- 服务器端监听端口固定为 10001,客户端连接地址固定为 127.0.0.1:10001;
- 客户端输入 "886" 时,必须终止通信并断开连接。
Server1.java
java
package demo3;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
public class Server1 {
public static void main(String[] args) throws IOException {
ServerSocket ss = new ServerSocket(10001);
Socket socket = ss.accept();
InputStream is = socket.getInputStream();
InputStreamReader isr = new InputStreamReader(is);
int b;
while ((b = isr.read()) != -1) {
System.out.print((char) b);
}
socket.close();
ss.close();
}
}
Client1.java
java
package demo3;
import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Scanner;
public class Client1 {
public static void main(String[] args) throws IOException {
Socket socket = new Socket("127.0.0.1",10001);
Scanner sc = new Scanner(System.in); // 为什么这个不用写到循环里面去
OutputStream os = socket.getOutputStream();
while (true) {
System.out.println("请输入您要发送的信息");
String str = sc.nextLine();
if("886".equals(str)){
break;
}
os.write(str.getBytes());
}
socket.close();
}
}
关键逻辑:Scanner 为什么不用写到循环里
① 绑定「系统控制台输入流(System.in)」,创建一个「输入读取工具」,专门用来读取用户在控制台输入的内容(比如键盘敲的字符串)。
② 这个「工具」的核心特性是:一个 Scanner 实例可以重复使用 ------ 只要它绑定的输入流(System.in)没关闭,就能反复调用 sc.nextLine() 读取多次输入,不需要每次读输入都重新创建。
2. 基础 Socket 图片上传功能实现
请使用 Java Socket 与 IO 流技术,实现简单的图片上传功能,要求如下:
① 服务端(类名:Server2):
- 监听 10005 端口,等待客户端连接;
- 接收客户端上传的图片数据,使用 UUID 生成无重复文件名(去除 UUID 中的 "-"),保存为.png 格式文件(保存路径:Java 练习 \day34-test\serverdir\);
- 图片接收完成后,向客户端发送 "上传成功" 响应;
- 正确使用 IO 流的 flush 操作,确保数据完整传输。
② 客户端(类名:Client2):
- 连接本地(127.0.0.1)10005 端口的服务端;
- 读取本地指定路径的图片文件(路径:Java 练习 \day34-test\clientdir\1.jpeg);
- 将图片数据发送至服务端,发送完成后关闭输出流通道;
- 接收服务端的响应信息并打印到控制台。
③ 关键技术要求
- 使用 BufferedInputStream、BufferedOutputStream、BufferedWriter、BufferedReader 等缓冲流提升传输效率;
- 理解并正确使用
flush()方法(确保缓冲数据强制写出,避免数据残留); - 理解
newLine()方法的作用(用于标记响应信息的结束,方便客户端用readLine()读取); - 明确 IO 流的使用规则:读取数据使用
read()方法,写入数据使用write()方法; - 确保 Socket 连接及 IO 流资源正确关闭。
Server2.java
java
package demo4;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.UUID;
public class Server2 {
public static void main(String[] args) throws IOException {
ServerSocket ss = new ServerSocket(10005);
Socket socket = ss.accept();
BufferedInputStream bis = new BufferedInputStream(socket.getInputStream());
String name = UUID.randomUUID().toString().replaceAll("-", "");
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("Java练习\\day34-test\\serverdir\\"+ name +".png"));
byte[] bytes = new byte[1024];
int len;
while ((len = bis.read(bytes)) != -1) {
bos.write(bytes, 0, len);
}
bos.flush();
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
bw.write("上传成功");
bw.newLine(); //不太理解
bw.flush(); //不太理解
socket.close();
}
}
Client2.java
java
package demo4;
import java.io.*;
import java.net.Socket;
public class Client2 {
public static void main(String[] args) throws IOException {
Socket socket = new Socket("127.0.0.1",10005);
BufferedInputStream bis = new BufferedInputStream(new FileInputStream("Java练习\\day34-test\\clientdir\\1.jpeg"));
BufferedOutputStream bos = new BufferedOutputStream(socket.getOutputStream());
byte[] bytes = new byte[1024];
int len;
while ((len = bis.read(bytes)) != -1) {
bos.write(bytes,0,len);
}
bos.flush();
socket.shutdownOutput();
BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String line = br.readLine();
System.out.println(line);
socket.close();
//这里是输入的用read,输出的用write吗?
}
}
关键逻辑:bw.newLine(); 到底是干嘛的?
① readLine() 方法的规则是:一直读取数据,直到遇到 "换行符" 才停止,并返回换行符之前的所有内容(换行符本身会被丢弃)。
② 如果你不加 bw.newLine();,服务端只发了 "上传成功" 这 4 个字,没有换行符。此时客户端的 readLine() 会一直等 ------ 它不知道什么时候算 "读完整了",会一直阻塞在那里,永远等不到结果。