目录

Java TCP 网络编程深度解析:从零到精通

前言:写给新手小白

第一章:什么是 TCP 流套接字编程?

1.1 “流” (Stream) 是什么?

1.2 “套接字” (Socket) 是什么?

第二章:核心 API 深度解剖

2.1 ServerSocket API —— “开店的老板”

2.2 Socket API —— “进门的顾客 & 专属服务员”

第三章:输入输出流的“千层饼”原理

第四章:实战场景 —— “英译汉”服务器

4.1 业务流程

4.2 长连接 vs 短连接(图片中的重点)

第五章:代码中的“坑”与“雷”

第六章:扩展知识点

6.1 UDP vs TCP (图片对比)

6.2 多线程服务端 (进阶)

服务端代码

客户端代码


Java TCP 网络编程深度解析:从零到精通

前言:写给新手小白

欢迎来到网络编程的世界!你上传的几张图片通过大纲、API列表和示例,构建了一个非常完整的 TCP 学习路径。看着那些 ServerSocket, InputStream 可能会觉得头晕,但别担心。

我们将用最通俗的语言,结合你提供的图片内容(特别是那个“英译汉”的例子),把这些知识点掰开了、揉碎了讲清楚。这份文档将是你理解接下来代码的“说明书”。

第一章:什么是 TCP 流套接字编程?

在你的图片 image_287083.pngimage_286998.png 中,标题都是“TCP流套接字编程”。我们需要先理解这几个字。

1.1 “流” (Stream) 是什么?

想象一根水管。

  • UDP 就像是扔水球。你扔一个,对方接一个。如果扔歪了,水球就丢了(丢包)。水球之间是独立的。

  • TCP 就像是接水管。一旦接通,水(数据)就会源源不断地从一端流向另一端。你在一头倒水,对方在另一头接水。TCP 保证这根管子是可靠的(不漏水)、有序的(先倒进去的水先出来)。

在 Java 代码中,我们用 InputStream(输入流)和 OutputStream(输出流)来操作这根“水管”。

1.2 “套接字” (Socket) 是什么?

Socket 原意是“插座”。在网络编程中,它是两台电脑通信的端点

  • 你要给朋友打电话,你需要一部电话机,你朋友也需要一部电话机。

  • Socket 就是网络世界里的“电话机”。

  • IP 地址 = 电话号码。

  • 端口号 = 分机号。

第二章:核心 API 深度解剖

根据你上传的 image_286959.png (ServerSocket API) 和 image_2869d5.png (Socket API),我们逐一拆解这些核心方法。

2.1 ServerSocket API —— “开店的老板”

ServerSocket 是服务端专用的类。它的任务不是“聊天”,而是“等待”。

  • 构造方法 ServerSocket(int port)

    • 含义:在某个端口“挂牌营业”。

    • 生活比喻:你在商场租了个铺位(端口 8888),挂上招牌“英译汉翻译社”。

    • 注意:端口号不能冲突。如果 8888 已经被别人用了,你会收到 BindException(绑定异常),就像铺位已经被别人租了一样。

  • 方法 Socket accept()

    • 含义最关键的方法! 它的意思是“接受连接”。

    • 重点(阻塞):这个方法是阻塞的。什么意思呢?就是程序运行到这一行,会卡住不动,直到有客户端连接上来。

    • 生活比喻:老板坐在店里死等。如果没有顾客(客户端)进门,老板就不动。一旦有顾客进门,accept() 方法就会返回一个普通的 Socket 对象

    • 潜台词ServerSocket 只是迎宾员,它接到客人后,会派一个专门的服务员(返回的那个 Socket)去一对一服务,自己继续回到门口等下一个客人。

  • 方法 void close()

    • 含义:关店,不再接待新客人。

2.2 Socket API —— “进门的顾客 & 专属服务员”

Socket 类在客户端和服务端都会用到。

  • 构造方法 Socket(String host, int port)(客户端使用):

    • 含义:发起连接。

    • 底层细节:当你 new 这一行代码时,计算机会悄悄进行 TCP 的**“三次握手”**。

      1. 客户端喊:“我要连你!” (SYN)

      2. 服务端喊:“好的,来吧!” (SYN+ACK)

      3. 客户端喊:“我进来了!” (ACK)

    • 如果服务端没启动,或者防火墙挡住了,这里会直接报错 ConnectionRefused

  • 方法 getInputStream()

    • 含义:获取“听筒”。

    • 作用:读取对方发来的数据。

  • 方法 getOutputStream()

    • 含义:获取“话筒”。

    • 作用:向对方发送数据。

第三章:输入输出流的“千层饼”原理

这是新手最容易晕的地方:为什么代码里套了一层又一层?

InputStream -> InputStreamReader -> BufferedReader,这是在干嘛?

我们用“吃汉堡”来比喻:

  1. InputStream (字节流)

    • 原始食材(面粉、生肉)。计算机底层只认识 0 和 1(字节)。如果你直接用这个,你读到的是一堆看不懂的数字。

    • 太难用了!

  2. InputStreamReader (转换流)

    • 厨师(烹饪)。它负责把“字节”翻译成“字符”(char)。它知道 0xE4 0xBD 0xA0 这三个字节凑在一起是汉字“你”。

    • 还是不够方便,一次只能读一个字。

  3. BufferedReader (缓冲流)

    • 外卖盒(打包)。它有一个缓冲区(Buffer),可以把字符攒起来。最厉害的是它有 readLine() 方法,可以一次读一行

    • 这才是我们想要的!

所以,为了让代码写起来舒服,我们通常会把流包装成 BufferedReader 来读取,用 PrintWriter (类似 System.out.println) 来发送。

第四章:实战场景 —— “英译汉”服务器

根据你的图片 image_286959.png 中的文字提示:“实现一个简单的英译汉的功能”。我们要设计这样一个系统:

4.1 业务流程

  1. 服务端启动,加载一个词典(Map),比如 apple -> 苹果

  2. 客户端启动,连接服务端。

  3. 客户端发送单词:"apple"。

  4. 服务端接收到 "apple",在 Map 里查到 "苹果"。

  5. 服务端发回 "苹果"。

  6. 客户端收到并打印。

4.2 长连接 vs 短连接(图片中的重点)

  • 短连接 (Short Connection)

    • 流程:连接 -> 问一个词 -> 得到答案 -> 挂断

    • 场景:就像你去窗口办事,办完一个就走。如果要办下一个,重新排队(重新连接)。

    • 代码体现socket.close() 在一次交互后立刻调用。

  • 长连接 (Long Connection)

    • 流程:连接 -> 问词 -> 答案 -> 问词 -> 答案 -> ... -> 挂断

    • 场景:就像打电话煲粥,一直聊,不挂断。

    • 代码体现:交互逻辑在一个 while 循环里,直到客户端说“拜拜”。

本次代码演示: 为了符合图片中“示例一:一发一收(短连接)”的描述,我们的代码将采用短连接模式。即客户端每查一个词,就发起一次新的连接。虽然效率略低,但逻辑最简单清晰,适合入门。

第五章:代码中的“坑”与“雷”

在阅读代码前,请注意以下几个新手常踩的坑:

  1. 换行符之死

    • 服务端用了 readLine(),它在等一个换行符 \n

    • 如果客户端发送时用了 print("apple") 而不是 println("apple"),服务端会认为你话没说完,一直死等。程序就卡住了。

  2. 端口被占

    • 如果你运行了两次服务端,第二次会报错 BindException: Address already in use。因为一个端口同时只能被一个程序占用。

  3. IP 地址

    • 代码里写的 127.0.0.1 代表“本机”。如果你想连室友的电脑,要查他的局域网 IP(比如 192.168.1.5),且由于我们在之前的对话中讨论过 NAT 问题,确保你们在同一个 WiFi 下。

第六章:扩展知识点

6.1 UDP vs TCP (图片对比)

既然图片提到了“和刚才 UDP 类似”,我们简单对比一下:

  • UDP (DatagramSocket): 写信模式。把信扔进邮筒,不管对方收没收到。无连接,不可靠,但快。适合视频直播、王者荣耀。

  • TCP (ServerSocket/Socket): 电话模式。必须接通才能说话。面向连接,可靠,略慢。适合网页(HTTP)、文件下载、聊天文字。

6.2 多线程服务端 (进阶)

我们的示例代码是单线程的。这意味着,如果有两个人同时连上来,第二个人必须等第一个人处理完、断开连接后,才能得到服务。

在真实开发中,我们会在 accept() 之后,启动一个新的 Thread 来专门伺候这个客户端,这样主线程就可以立马回去接待下一个客人,实现“并发”。

准备好了吗?让我们来看代码!

服务端代码

/**
 * ===========================================================================
 * 文件名: TranslationServer.java
 * 角色: TCP 服务端 (Server)
 * 功能: 这是一个 "英译汉" 翻译服务器。
 * 对应图片 image_286959.png 中提到的 "实现一个简单的英译汉的功能"。
 * * 模式: 短连接 (Short Connection) —— 一发一收。
 * 即:接收一个单词 -> 翻译 -> 发回结果 -> 断开连接。
 * ===========================================================================
 */

// 1. 导入必要的包
import java.io.BufferedReader; // 用于读取文本行,效率高
import java.io.IOException;    // 处理输入输出异常
import java.io.InputStream;    // 字节输入流
import java.io.InputStreamReader; // 字节转字符的桥梁
import java.io.OutputStream;   // 字节输出流
import java.io.PrintWriter;    // 用于发送文本,方便好用
import java.net.ServerSocket;  // 服务端套接字,用于监听端口
import java.net.Socket;        // 套接字,用于建立连接
import java.util.HashMap;      // 用于存储词典数据
import java.util.Map;          // Map 接口

public class TranslationServer {

    // 定义服务器监听的端口号
    // 端口号范围 0-65535,建议使用 1024 以上的端口,避免冲突
    private static final int PORT = 9999;

    // 定义一个简单的内存词典
    private static final Map<String, String> DICTIONARY = new HashMap<>();

    // 静态代码块:在程序启动时初始化词典数据
    static {
        DICTIONARY.put("apple", "苹果");
        DICTIONARY.put("banana", "香蕉");
        DICTIONARY.put("cat", "猫咪");
        DICTIONARY.put("dog", "小狗");
        DICTIONARY.put("java", "世界上最好的编程语言 (开玩笑的)");
        DICTIONARY.put("tcp", "传输控制协议");
        DICTIONARY.put("socket", "套接字");
    }

    public static void main(String[] args) {
        System.out.println("====== 英译汉翻译服务器启动 ======");

        // try-with-resources 写法 (Java 7+ 特性)
        // 在 try() 括号里创建的对象,会在代码块结束后自动调用 close() 关闭。
        // ServerSocket 是服务器的核心,负责在端口 "死等" 客户端。
        try (ServerSocket serverSocket = new ServerSocket(PORT)) {
            
            System.out.println("【服务端】正在监听端口: " + PORT);
            System.out.println("【服务端】等待查词请求...");

            // 死循环:服务器要一直运行,服务完一个接下一个
            while (true) {
                // 1. 阻塞等待连接 (Accept)
                // 程序运行到这里会 "停住",直到有客户端连上来。
                // 一旦连上,返回一个新的 Socket 对象,专门代表这个客户端。
                Socket clientSocket = serverSocket.accept();

                // 获取一下客户端的 IP,方便我们在控制台看是谁连我了
                String clientIP = clientSocket.getInetAddress().getHostAddress();
                System.out.println("------------------------------------------------");
                System.out.println("【服务端】新客户连接成功!IP: " + clientIP);

                // 2. 处理这个客户端的请求
                // 我们把具体的逻辑封装到一个方法里,让 main 函数看起来干净点
                processClientRequest(clientSocket);
            }

        } catch (IOException e) {
            // 如果端口被占用 (比如你已经启动了一个服务器没关),会报错 BindException
            System.err.println("【服务端】启动失败,端口可能被占用: " + e.getMessage());
        }
    }

    /**
     * 处理单个客户端的查词请求
     * @param socket 与客户端建立的连接管道
     */
    private static void processClientRequest(Socket socket) {
        // 同样使用 try-with-resources 自动关闭资源
        // 注意:socket 关闭时,它里面的输入输出流也会自动关闭
        try (
            // ============ 输入流部分 (听) ============
            // 1. 获取字节输入流 (原始数据)
            InputStream in = socket.getInputStream();
            // 2. 包装成转换流 (指定 UTF-8 编码,防止中文乱码)
            InputStreamReader isr = new InputStreamReader(in, "UTF-8");
            // 3. 包装成缓冲流 (提供 readLine 方法,按行读取)
            BufferedReader reader = new BufferedReader(isr);

            // ============ 输出流部分 (说) ============
            // 1. 获取字节输出流
            OutputStream out = socket.getOutputStream();
            // 2. 包装成 PrintWriter
            // 第二个参数 true 非常重要!表示 "自动刷新" (Auto Flush)。
            // 如果没有这个 true,发送的数据可能会卡在内存缓冲区里,客户端收不到。
            PrintWriter writer = new PrintWriter(out, true);
        ) {
            // 3. 读取客户端发来的单词
            // readLine() 也是阻塞的,会一直等到客户端发来一个换行符
            String englishWord = reader.readLine();
            System.out.println("【服务端】收到查询单词: " + englishWord);

            // 4. 查词典逻辑
            String chineseTranslation;
            if (englishWord == null) {
                // 如果对方直接断开了,readLine 会返回 null
                System.out.println("【服务端】客户端异常断开。");
                return;
            }
            
            // 在 Map 中查找,如果找不到,返回默认提示
            // toLowerCase() 是为了忽略大小写,比如 Apple -> apple
            chineseTranslation = DICTIONARY.getOrDefault(englishWord.toLowerCase(), "抱歉,我的词典里没有这个词");

            // 5. 将翻译结果发送回客户端
            // println 会在末尾加换行符,这对 TCP 文本协议很重要
            writer.println(chineseTranslation);
            System.out.println("【服务端】已发送翻译结果: " + chineseTranslation);

        } catch (IOException e) {
            System.err.println("【服务端】处理请求时发生网络错误: " + e.getMessage());
        } finally {
            // 这里是 finally 块,虽然上面的 try-with-resources 已经处理了关闭,
            // 但写在这里是为了逻辑上的明确:一个请求处理完,必须断开连接 (短连接模式)。
            System.out.println("【服务端】连接已断开 (短连接结束)。");
        }
    }
}

客户端代码

/**
 * ===========================================================================
 * 文件名: TranslationClient.java
 * 角色: TCP 客户端 (Client)
 * 功能: 模拟用户查询单词。
 * 对应图片 "示例一:一发一收 (短连接)"。
 * * 流程:
 * 1. 用户输入单词。
 * 2. 建立连接 -> 发送单词 -> 接收翻译 -> 关闭连接。
 * 3. 循环上述过程。
 * ===========================================================================
 */

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Scanner;

public class TranslationClient {

    // 目标服务器 IP (本机)
    private static final String SERVER_IP = "127.0.0.1";
    // 目标服务器端口 (必须和 TranslationServer 一致)
    private static final int SERVER_PORT = 9999;

    public static void main(String[] args) {
        System.out.println("====== 英译汉客户端启动 ======");
        System.out.println("输入英文单词按回车查询,输入 'quit' 退出。");

        // Scanner 用于读取用户在控制台的按键输入
        Scanner scanner = new Scanner(System.in);

        while (true) {
            System.out.print("\n请输入要翻译的单词: ");
            String wordToTranslate = scanner.nextLine();

            // 如果用户输入 quit,则退出程序
            if ("quit".equalsIgnoreCase(wordToTranslate)) {
                System.out.println("程序退出,再见!");
                break;
            }

            // 如果输入为空,跳过
            if (wordToTranslate.trim().isEmpty()) {
                continue;
            }

            // ============================================================
            // 核心网络交互逻辑 (短连接模式)
            // 每次查询都创建一个新的 Socket,查完就关掉
            // ============================================================
            processOneQuery(wordToTranslate);
        }
        
        scanner.close();
    }

    /**
     * 发起一次完整的查词请求 (Connect -> Request -> Response -> Close)
     * @param word 要查询的单词
     */
    private static void processOneQuery(String word) {
        // try-with-resources: 自动关闭 Socket 和流
        try (
            // 1. 建立连接 (三次握手)
            // 如果服务器没启动,这里会抛出 ConnectException
            Socket socket = new Socket(SERVER_IP, SERVER_PORT);
        ) {
            // 打印连接成功提示
            // socket.getInetAddress() 获取服务器的信息
            System.out.println(">> 已连接至服务器: " + socket.getInetAddress());

            // 2. 获取输出流 (准备发送)
            OutputStream out = socket.getOutputStream();
            // true 表示自动刷新,别忘了!
            PrintWriter writer = new PrintWriter(out, true);

            // 3. 获取输入流 (准备接收)
            InputStream in = socket.getInputStream();
            BufferedReader reader = new BufferedReader(new InputStreamReader(in, "UTF-8"));

            // 4. 发送请求
            // 必须用 println,加上换行符,否则服务器的 readLine() 会卡死
            writer.println(word);
            System.out.println(">> 已发送: " + word);

            // 5. 接收响应
            // 阻塞等待服务器回话
            String response = reader.readLine();
            System.out.println(">> 【翻译结果】: " + response);

            // 方法结束时,try 块会自动调用 socket.close()
            // 这就是 "短连接" 的精髓:用完即弃

        } catch (IOException e) {
            System.err.println(">> 连接失败或网络错误: " + e.getMessage());
            System.err.println(">> 请检查服务端 (TranslationServer) 是否已启动?");
        }
    }
}

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐