目录

第一章:初识网络编程的三大基石

1. IP 地址(IPv4 的小细节)

2. InetAddress 类的使用

3. 端口号 (Port)

4. 协议 (Protocol)

第二章:UDP 协议 —— 莽撞的快递小哥

UDP 通信的三种模式:

综合练习:UDP 实战 —— 简易控制台聊天室

第三章:TCP 协议 —— 靠谱的霸道总裁

1. 核心机制:三次握手与四次挥手

2. 中文乱码问题怎么破?

第四章:TCP 综合终极实战 —— 工业级线程池文件上传服务器

第五章:综合练习 07 —— B/S 架构模型 (一招吃遍天下)

第六章:面试必背火炮营


上一篇我们搞定了 IO 流,让程序拥有了在自己电脑硬盘里“翻箱倒柜”的能力。但现在的时代,单机自嗨是没有前途的。你怎么让北京的 Java 程序,给远在新加坡的服务器发一张照片?这就必须请出今天的主角:网络编程 (Socket)

别一听网络就觉得高深莫测,其实它底层依然是你熟悉的 IO 流,只不过这根管子变长了,顺着网线连到了别人的电脑上而已!

今天,老司机带你拔掉单机的网线,拥抱广阔的互联网!


第一章:初识网络编程的三大基石

要在茫茫互联网中找到对方并愉快地聊天,必须集齐“网络三要素”。这就好比寄快递:

  1. IP 地址: 对方的家庭住址(找主机)。

  2. 端口号: 对方家里的哪个房间,或者说是哪个人接收(找程序)。

  3. 协议: 你们用什么语言交流,普通话还是火星文(数据规则)。

1. IP 地址(IPv4 的小细节)

IP 地址就是电脑在网络里的唯一标识。目前最常用的是 IPv4,长得像 192.168.1.100

  • 小细节: IPv4 其实是一个 32 位的二进制数,为了让人能看懂,才切成 4 段,每段用十进制表示(0~255)。

  • 专用 IP: 127.0.0.1 或者 localhost,这叫本地回环地址。永远指向你自己的电脑。平时开发测试,连自己电脑的数据库,写的都是它。

2. InetAddress 类的使用

Java 提供了 InetAddress 类来表示 IP。不用 new,直接用静态方法拿。

import java.net.InetAddress;
import java.net.UnknownHostException;

public class IPDemo {
    public static void main(String[] args) throws UnknownHostException {
        // 获取本机的 IP 对象
        InetAddress localHost = InetAddress.getLocalHost();
        System.out.println("本机名称: " + localHost.getHostName());
        System.out.println("本机 IP: " + localHost.getHostAddress());

        // 获取百度的 IP 对象
        InetAddress baidu = InetAddress.getByName("www.baidu.com");
        System.out.println("百度 IP: " + baidu.getHostAddress());
    }
}
3. 端口号 (Port)

一台电脑上运行着 QQ、微信、浏览器,数据顺着 IP 找过来了,该交给谁?这就靠端口号。 端口号范围是 0 ~ 65535

  • 避坑指南: 0 ~ 1023 是系统保留端口(比如 HTTP 默认 80,HTTPS 默认 443)。咱们自己写代码,尽量用 1024 之后的,比如常用的 80803306 (MySQL)、6379 (Redis),防止端口冲突直接报错。

4. 协议 (Protocol)

协议就是双方约定的暗号。在传输层,我们主要打交道的是两位大佬:UDPTCP


第二章:UDP 协议 —— 莽撞的快递小哥

特点: 无连接、不可靠、速度极快。 UDP 不管对方在不在,也不管对方准没准备好,把数据打包一脚就踹过去。丢包了?丢了就丢了。 应用场景: 视频通话、直播、网络游戏(偶尔卡一下或者掉一帧无所谓,关键是要快)。

UDP 通信的三种模式:
  • 单播 (Unicast): 一对一聊天(最常见)。

  • 组播 (Multicast): 一对多,比如你在一个“摸鱼群”里发消息,只有群里的人能收到。

  • 广播 (Broadcast): 大喇叭喊话,局域网里所有人都能收到(IP 通常是 255.255.255.255)。

综合练习:UDP 实战 —— 简易控制台聊天室

我们直接写一个 UDP 聊天室,包含发送端和接收端。

完整代码:

// 接收端代码:UDPServer.java
import java.net.DatagramPacket;
import java.net.DatagramSocket;

public class UDPServer {
    public static void main(String[] args) throws Exception {
        // 1. 明确自己的端口号,开门营业
        DatagramSocket socket = new DatagramSocket(8888);
        System.out.println("UDP 聊天室已开启,等待接收消息...");

        byte[] buffer = new byte[1024];
        DatagramPacket packet = new DatagramPacket(buffer, buffer.length);

        while (true) {
            // 2. 阻塞等待接收数据。没数据来的时候,程序会死等在这行代码
            socket.receive(packet);

            // 3. 拆快递
            String ip = packet.getAddress().getHostAddress();
            String message = new String(packet.getData(), 0, packet.getLength());
            
            System.out.println("收到来自 [" + ip + "] 的消息: " + message);
            if ("886".equals(message)) {
                break;
            }
        }
        socket.close();
    }
}
// 发送端代码:UDPClient.java
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.util.Scanner;

public class UDPClient {
    public static void main(String[] args) throws Exception {
        // 1. 找个快递员(不用指定端口,系统随机分配)
        DatagramSocket socket = new DatagramSocket();
        Scanner sc = new Scanner(System.in);
        
        while (true) {
            System.out.print("请输入要发送的消息(输入886退出): ");
            String msg = sc.nextLine();
            byte[] bytes = msg.getBytes();

            // 2. 打包快递 (数据, 长度, 目标IP, 目标端口)
            // 这里假设接收端就在本机 127.0.0.1 的 8888 端口
            DatagramPacket packet = new DatagramPacket(
                    bytes, bytes.length, InetAddress.getByName("127.0.0.1"), 8888);

            // 3. 一脚踹出去
            socket.send(packet);

            if ("886".equals(msg)) {
                break;
            }
        }
        socket.close();
    }
}

操作说明:

  1. 在项目中新建 UDPServer.javaUDPClient.java

  2. 重点:必须先运行 Server,再运行 Client! 如果先发,接收端还没开机,数据直接就丢到黑洞里了(因为 UDP 是不保证到达的)。

  3. 在 Client 控制台输入聊天内容,回车发送,你会在 Server 的控制台看到接收的打印。


第三章:TCP 协议 —— 靠谱的霸道总裁

特点: 面向连接、绝对可靠、速度稍慢。 如果说 UDP 是发短信,那 TCP 就是打电话。必须先接通,双方确认没问题,才能开始传数据。传数据的过程中绝不丢包(底层有重传机制)。 应用场景: 网页访问、文件下载、转账支付。

1. 核心机制:三次握手与四次挥手
  • 三次握手(建立连接):

    • 客户端:在吗?我想连你。(SYN)

    • 服务端:在的,我也想连你,你准备好了吗?(SYN + ACK)

    • 客户端:准备好了,咱们开始吧!(ACK) (为什么要三次?为了防止失效的连接请求突然又传到服务端,导致服务端白白等待资源浪费。)

  • 四次挥手(断开连接):

    • 客户端:我发完了,我要断开了。(FIN)

    • 服务端:知道了,等我把你刚才发的数据处理完。(ACK)

    • 服务端:我处理完了,我也要断开了。(FIN)

    • 客户端:好的,拜拜!(ACK,然后客户端进入 TIME_WAIT 等待一会,怕服务端没收到最后的确认)。

2. 中文乱码问题怎么破?

TCP 底层传的都是字节流 (InputStream / OutputStream)。如果你直接用字节流读中文,必定乱码! 解法: 把底层的网络字节流,用转换流 (InputStreamReader / OutputStreamWriter) 包装成字符流,最好再套上缓冲流 (BufferedReader) 实现一行行读取!


第四章:TCP 综合终极实战 —— 工业级线程池文件上传服务器

在这个练习中,我们要实现:多发多收、接收并反馈、上传文件、解决文件名重复覆盖问题、并使用线程池来扛住高并发连接。

完整代码:

// TCP 终极服务端:ThreadPoolUploadServer.java
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.UUID;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class ThreadPoolUploadServer {
    public static void main(String[] args) throws IOException {
        // 1. 开启服务器,监听 9999 端口
        ServerSocket serverSocket = new ServerSocket(9999);
        System.out.println("图片上传服务器已启动,正在监听 9999 端口...");

        // 2. 创建自定义线程池 (大厂规范,坚决不用 Executors 默认的)
        ThreadPoolExecutor pool = new ThreadPoolExecutor(
                3, 10, 60, TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(10),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy()
        );

        // 3. 死循环,不断接待新客
        while (true) {
            // 阻塞等待,直到有客户端连接进来 (三次握手成功)
            Socket socket = serverSocket.accept();
            System.out.println("有新客户端连入:" + socket.getInetAddress().getHostAddress());

            // 将接待这个客户的任务,扔给线程池去处理
            pool.submit(new UploadTask(socket));
        }
    }
}

// 处理上传任务的 Runnable 类
class UploadTask implements Runnable {
    private Socket socket;

    public UploadTask(Socket socket) {
        this.socket = socket;
    }

    @Override
    public void run() {
        try {
            // 1. 获取网络输入流,读取客户端发来的文件字节数据
            InputStream is = socket.getInputStream();

            // 2. 解决文件名重复:使用 UUID 生成全球唯一的字符串作为文件名
            String fileName = UUID.randomUUID().toString() + ".jpg";
            File targetFile = new File("myio", fileName); // 确保存放的目录存在
            
            // 如果 myio 目录不存在,则创建
            if(!targetFile.getParentFile().exists()) {
                targetFile.getParentFile().mkdirs();
            }

            // 本地文件输出流
            FileOutputStream fos = new FileOutputStream(targetFile);

            // 3. 经典的读写循环 (将网络流写入本地文件流)
            byte[] bytes = new byte[1024];
            int len;
            while ((len = is.read(bytes)) != -1) {
                fos.write(bytes, 0, len);
            }

            // 4. 接收完毕,给客户端反馈信息
            // 将网络字节输出流包装成字符缓冲流,方便写出文字
            BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
            bw.write("上传成功!您的文件名是:" + fileName);
            bw.newLine();
            bw.flush(); // 务必刷新,否则数据发不出去

            // 5. 释放资源
            fos.close();
            socket.close();

        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
// TCP 客户端:UploadClient.java
import java.io.*;
import java.net.Socket;

public class UploadClient {
    public static void main(String[] args) throws IOException {
        // 1. 创建 Socket,向服务端发起连接 (触发三次握手)
        Socket socket = new Socket("127.0.0.1", 9999);

        // 2. 本地文件输入流 (准备读取本地图片)
        // 注意:请在你的电脑 D 盘准备一张 test.jpg 图片,或者修改为你的图片路径
        FileInputStream fis = new FileInputStream("D:\\test.jpg");
        
        // 3. 网络字节输出流 (往管道里倒数据)
        OutputStream os = socket.getOutputStream();

        byte[] bytes = new byte[1024];
        int len;
        while ((len = fis.read(bytes)) != -1) {
            os.write(bytes, 0, len);
        }

        // 重要细节:文件读完了,必须给服务端发送一个“结束标记”
        // 否则服务端的 is.read() 会一直死等,以为你还有数据要发,导致程序卡死
        socket.shutdownOutput();

        // 4. 接收服务端的反馈信息 (将字节输入流包装为字符缓冲流防乱码)
        BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
        String response = br.readLine();
        System.out.println("收到服务器反馈: " + response);

        // 5. 释放资源 (断开四次挥手)
        fis.close();
        socket.close();
    }
}

操作说明:

  1. 新建 ThreadPoolUploadServer.javaUploadClient.java

  2. 确保 UploadClient.java 中读取文件的路径(如 D:\test.jpg)有真实的图片存在。

  3. 先运行 Server。 然后你可以连续多次运行 Client 模拟高并发。

  4. 观察 Server 所在项目的根目录,会自动生成 myio 文件夹,里面存放着各种名字乱码(UUID)但内容完好的图片,且客户端能收到上传成功的反馈。


第五章:综合练习 07 —— B/S 架构模型 (一招吃遍天下)

在刚才的代码中,我们自己写了 Server 和 Client。这种被称为 C/S 架构 (Client/Server),比如 QQ 客户端。但现在大厂最流行的是 B/S 架构 (Browser/Server)

什么是 B/S? 你不需要自己写 UploadClient.java 了!用户的电脑上只要装了浏览器(Browser),浏览器本身就是一个超级强大的 TCP 客户端! 你的 Java 代码只需要写服务端(Server)。浏览器通过 HTTP 协议 发送请求,你的 Java 程序接收、处理,并吐出一段 HTML 代码,浏览器就能把它渲染成漂亮的网页。 这也是后续大家即将踏入 Java Web (Spring Boot, Tomcat 等) 的终极基础!


第六章:面试必背火炮营

  1. Q:TCP 和 UDP 的区别是什么?

    • A: TCP 面向连接,安全可靠不丢包,速度慢;UDP 无连接,不可靠可能丢包,速度快。TCP 是打电话,UDP 是发喇叭。

  2. Q:为什么是三次握手?两次不行吗?

    • A: 不行!如果只有两次,客户端发了 SYN,但因为网络拥堵在天上飞了很久。客户端以为丢了就放弃了连接。等过了半天,这个延迟的 SYN 到达了服务端,服务端回复 SYN+ACK 并直接单方面建立连接,死等数据,这会白白浪费服务器资源!必须三次,让客户端再确认一次才能建连。

  3. Q:为什么断开连接要四次挥手?

    • A: 因为 TCP 是全双工的(能双向同时传)。客户端说“我发完了”,只能代表客户端不再发送了,但服务端可能还有没发完的数据。所以服务端先回复 ACK 稳住客户端,等自己把剩下的活干完,再发一个 FIN 说“我也发完了”,最后客户端确认,彻底断开。

  4. Q:客户端最后为什么要等待一个 2MSL 的 TIME_WAIT 状态?

    • A: 怕最后那个 ACK 确认包丢了。如果丢了,服务端收不到确认,会重新发一次 FIN。客户端等一会,就是为了应对这种重传,确保服务端能安息(正常关闭)。

网络编程的底层逻辑就这么被我们扒光了。等你到了 Spring Boot 阶段,框架会把这堆 Socket 封装得极其优雅,但如果你不懂底层原理,一遇到“连接拒绝”、“端口占用”、“粘包半包”的问题,依然会抓瞎。

如果我的内容对你有帮助,请点赞,评论,收藏。接下来我将继续更新相关内容!

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐