JavaEE初阶——Java TCP网络编程:Socket、SeverSocket API




目录
2.1 ServerSocket API —— “开店的老板”
2.2 Socket API —— “进门的顾客 & 专属服务员”
Java TCP 网络编程深度解析:从零到精通
前言:写给新手小白
欢迎来到网络编程的世界!你上传的几张图片通过大纲、API列表和示例,构建了一个非常完整的 TCP 学习路径。看着那些 ServerSocket, InputStream 可能会觉得头晕,但别担心。
我们将用最通俗的语言,结合你提供的图片内容(特别是那个“英译汉”的例子),把这些知识点掰开了、揉碎了讲清楚。这份文档将是你理解接下来代码的“说明书”。
第一章:什么是 TCP 流套接字编程?
在你的图片 image_287083.png 和 image_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 的**“三次握手”**。-
客户端喊:“我要连你!” (SYN)
-
服务端喊:“好的,来吧!” (SYN+ACK)
-
客户端喊:“我进来了!” (ACK)
-
-
如果服务端没启动,或者防火墙挡住了,这里会直接报错
ConnectionRefused。
-
-
方法
getInputStream():-
含义:获取“听筒”。
-
作用:读取对方发来的数据。
-
-
方法
getOutputStream():-
含义:获取“话筒”。
-
作用:向对方发送数据。
-
第三章:输入输出流的“千层饼”原理
这是新手最容易晕的地方:为什么代码里套了一层又一层?
InputStream -> InputStreamReader -> BufferedReader,这是在干嘛?
我们用“吃汉堡”来比喻:
-
InputStream(字节流):-
原始食材(面粉、生肉)。计算机底层只认识 0 和 1(字节)。如果你直接用这个,你读到的是一堆看不懂的数字。
-
太难用了!
-
-
InputStreamReader(转换流):-
厨师(烹饪)。它负责把“字节”翻译成“字符”(char)。它知道
0xE4 0xBD 0xA0这三个字节凑在一起是汉字“你”。 -
还是不够方便,一次只能读一个字。
-
-
BufferedReader(缓冲流):-
外卖盒(打包)。它有一个缓冲区(Buffer),可以把字符攒起来。最厉害的是它有
readLine()方法,可以一次读一行。 -
这才是我们想要的!
-
所以,为了让代码写起来舒服,我们通常会把流包装成 BufferedReader 来读取,用 PrintWriter (类似 System.out.println) 来发送。
第四章:实战场景 —— “英译汉”服务器
根据你的图片 image_286959.png 中的文字提示:“实现一个简单的英译汉的功能”。我们要设计这样一个系统:
4.1 业务流程
-
服务端启动,加载一个词典(Map),比如
apple->苹果。 -
客户端启动,连接服务端。
-
客户端发送单词:"apple"。
-
服务端接收到 "apple",在 Map 里查到 "苹果"。
-
服务端发回 "苹果"。
-
客户端收到并打印。
4.2 长连接 vs 短连接(图片中的重点)
-
短连接 (Short Connection):
-
流程:连接 -> 问一个词 -> 得到答案 -> 挂断。
-
场景:就像你去窗口办事,办完一个就走。如果要办下一个,重新排队(重新连接)。
-
代码体现:
socket.close()在一次交互后立刻调用。
-
-
长连接 (Long Connection):
-
流程:连接 -> 问词 -> 答案 -> 问词 -> 答案 -> ... -> 挂断。
-
场景:就像打电话煲粥,一直聊,不挂断。
-
代码体现:交互逻辑在一个
while循环里,直到客户端说“拜拜”。
-
本次代码演示: 为了符合图片中“示例一:一发一收(短连接)”的描述,我们的代码将采用短连接模式。即客户端每查一个词,就发起一次新的连接。虽然效率略低,但逻辑最简单清晰,适合入门。
第五章:代码中的“坑”与“雷”
在阅读代码前,请注意以下几个新手常踩的坑:
-
换行符之死:
-
服务端用了
readLine(),它在等一个换行符\n。 -
如果客户端发送时用了
print("apple")而不是println("apple"),服务端会认为你话没说完,一直死等。程序就卡住了。
-
-
端口被占:
-
如果你运行了两次服务端,第二次会报错
BindException: Address already in use。因为一个端口同时只能被一个程序占用。
-
-
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) 是否已启动?");
}
}
}
更多推荐


所有评论(0)