Java 黑马程序员学习笔记(进阶篇30)

一、网络编程三要素

1. 要素一:IP 地址(设备的唯一标识)
(1) IPv4(主流,重点掌握)

① 格式: 32 位二进制数,分为 4 个字节(8 位 / 字节),每个字节转换为 0~255 的十进制数,用点号(.)分隔,例如 192.168.1.1127.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.x10.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 字节的数组 bytesnew 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 地址和端口(需与服务器一致);
  • 通过 SocketgetOutputStream() 获取输出流(向服务器发送数据),结合 IO 包装类;
  • 写入数据到输出流(注意刷新流,避免数据滞留缓冲区);
  • 发送完成后,关闭流和 Socket
③ TCP 接收数据(服务器端、Client)步骤
  • 创建 ServerSocket 对象,绑定指定端口(如 8888);
  • 调用 accept() 方法,阻塞等待客户端连接(连接成功后得到与该客户端对应的 Socket);
  • 通过 SocketgetInputStream() 获取输入流(接收客户端数据),结合 IO 包装类(如 BufferedReaderBufferedInputStream)提升效率;
  • 循环读取流中的数据(直到客户端关闭流或断开连接);
  • 处理数据后,关闭流、SocketServerSocket(推荐用 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 是什么?
  • isInputStream(输入流),is.read() 的作用是 从客户端发送的数据流中,读取「一个字节」的数据
  • 返回值 bint 类型(不是 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 转换为字符流;
  • 循环读取字符流数据,将其转换为字符后在控制台打印,直到读取结束;
  • 操作完成后关闭 SocketServer1

(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() 会一直等 ------ 它不知道什么时候算 "读完整了",会一直阻塞在那里,永远等不到结果。

相关推荐
曹牧24 分钟前
Oracle中ROW_NUMBER() OVER()
java·数据库·sql
客梦26 分钟前
数据结构-哈希表
java·数据结构·笔记
草原印象29 分钟前
Spring SpringMVC Mybatis框架整合实战
java·spring·mybatis·spring mvc
YJlio31 分钟前
Autologon 学习笔记(9.7):安全自动登录的正确打开方式
笔记·学习·安全
超级大只老咪33 分钟前
Nmap笔记
笔记
Amarantine、沐风倩✨33 分钟前
深度解析:轨迹数据抽稀到底该放数据库还是 Java?(以 56800 条数据为例)
java·开发语言·数据库
听风吟丶39 分钟前
Java 分布式追踪实战:SkyWalking+Spring Cloud 构建微服务全链路监控体系
java
小马爱打代码43 分钟前
Spring AI:使用 Advisor 组件 - 打印请求大模型出入参日志
java·人工智能·spring
XL's妃妃44 分钟前
Arthas:Java 应用诊断利器
java·开发语言