月圆人团圆,用Java网络编程传递中秋祝福
🌕 又是一年中秋至,月圆人团圆。在这个充满温情的传统节日里,让我们用代码搭建沟通的桥梁,用网络编程传递中秋的祝福与思念。
一、中秋佳节与网络通信的浪漫邂逅
中秋的团圆 vs 网络的连接
中秋佳节,最重要的主题就是"团圆"。无论身在何方,人们都渴望与亲人团聚,共享天伦之乐。这正如网络编程中的客户端与服务端,虽然物理上相隔千里,但通过网络的纽带,彼此紧密相连。
发送请求 返回响应 客户端 Client 服务端 Server
中秋元素与网络概念的对应
中秋元素 | 网络概念 | 寓意 |
---|---|---|
🌕 明月 | 网络通道 | 传递思念的媒介 |
🥮 月饼 | 数据包 | 承载温情的内容 |
🏠 家乡 | 服务端 | 温暖的港湾 |
✈️ 游子 | 客户端 | 远方的思念 |
二、UDP 数据报套接字编程
1. 协议简介
UDP 是无连接协议,数据传输前不需要建立连接,直接以「数据报」为单位发送。
通俗比喻:类似发短信,不需要先拨号建立连接,直接发送消息,但消息可能会丢失或顺序错乱。
2. 核心 API
2.1 DatagramSocket
- UDP 套接字
用于发送和接收 UDP 数据报,相当于「通信的发射器 / 接收器」。
构造方法
方法签名 | 说明 |
---|---|
DatagramSocket() |
创建 UDP 套接字,绑定到本机随机端口 (通常用于客户端,无需固定端口)。 |
DatagramSocket(int port) |
创建 UDP 套接字,绑定到本机指定端口 (通常用于服务端,需固定端口供客户端连接)。 |
核心方法
方法签名 | 说明 |
---|---|
void send(DatagramPacket p) |
发送数据报(非阻塞,直接发送)。 |
void receive(DatagramPacket p) |
接收 数据报(阻塞,无数据时会等待)。 |
void close() |
关闭套接字,释放资源。 |
2.2 DatagramPacket
- UDP 数据报
用于封装 UDP 传输的数据,包含「数据字节数组、目标 IP、目标端口」等信息。
通俗比喻 :类似短信,包含了短信内容 + 收件人手机号。
构造方法
方法签名 | 说明 |
---|---|
DatagramPacket(byte[] buf, int length) |
用于接收 数据:指定接收数据的字节数组 buf 和最大长度 length 。 |
DatagramPacket(byte[] buf, int length, SocketAddress address) |
用于发送 数据:指定发送数据的字节数组 buf 、长度 length ,以及目标地址 address (包含 IP 和端口)。 |
核心方法
方法签名 | 说明 |
---|---|
InetAddress getAddress() |
获取数据报的源 IP (接收时)或目标 IP(发送时)。 |
int getPort() |
获取数据报的源端口 (接收时)或目标端口(发送时)。 |
byte[] getData() |
获取数据报中的字节数组(传输的实际数据)。 |
2.3 InetSocketAddress
- 目标地址
SocketAddress
的子类,用于封装「IP 地址 + 端口号 」,作为 DatagramPacket
的目标地址参数。
- 构造方法 :
InetSocketAddress(InetAddress addr, int port)
- 常用方式 :
InetAddress.getByName(String ip)
- 通过 IP 字符串(如"127.0.0.1"
)获取InetAddress
对象。
使用示例
java
// 创建目标地址:IP 为 127.0.0.1,端口为 8888
SocketAddress address = new InetSocketAddress(
InetAddress.getByName("127.0.0.1"),
8888
);
// 将地址用于发送数据报
DatagramPacket packet = new DatagramPacket(
data,
data.length,
address
);
三、TCP 流套接字编程
1. TCP 核心 API
TCP 通信需区分「服务端」和「客户端」,核心 API 为 ServerSocket
(服务端套接字)和 Socket
(客户端/服务端连接套接字)。
1.1 ServerSocket(TCP 服务端套接字)
仅用于服务端,负责「监听端口、接收客户端连接请求」,不直接传输数据。
构造方法 | 说明 |
---|---|
ServerSocket(int port) |
创建服务端套接字,绑定到指定端口(需固定)。 |
核心方法 | 说明 |
---|---|
Socket accept() |
阻塞等待客户端连接,连接成功后返回 Socket 对象(用于与该客户端通信)。 |
void close() |
关闭服务端套接字,释放端口。 |
1.2 Socket(TCP 连接套接字)
客户端和服务端均会使用:
- 客户端:通过
Socket
发起连接并传输数据; - 服务端:通过
accept()
返回的Socket
与对应客户端通信。
构造方法 | 说明 |
---|---|
Socket(String host, int port) |
客户端使用:创建套接字并与指定 IP(host )、端口(port )的服务端建立连接。 |
核心方法 | 说明 |
---|---|
InputStream getInputStream() |
获取套接字的输入流(用于读取对方发送的数据)。 |
OutputStream getOutputStream() |
获取套接字的输出流(用于向对方发送数据)。 |
InetAddress getInetAddress() |
获取对方的 IP 地址。 |
int getPort() |
获取对方的端口号。 |
void close() |
关闭套接字,释放连接(TCP 会进行「四次挥手」关闭连接)。 |
四、用Socket编程实现"简易回显服务器"
在这个中秋佳节,让我们用Java网络编程构建一个充满温情的祝福传递系统。
先让我们了解一下UDP和TCP的区别
特点 | TCP(传输控制协议) | UDP(用户数据报协议) |
---|---|---|
连接性 | 有连接 | 无连接 |
传输可靠性 | 可靠传输 | 不可靠传输 |
数据传输方式 | 面向字节流,无边界,可分多次收发 | 面向数据报,有边界,一次传输一个数据报 |
缓冲区 | 有接收缓冲区和发送缓冲区 | 有接收缓冲区,无发送缓冲区 |
传输数据大小 | 大小不限 | 大小受限,一次最多传输 64k |
UDP版本
UDP就像中秋的流星,快速而直接。
UDP回显服务器
java
package Network.UDP;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
// 回显服务器
// 1. 从客户端读取到请求内容
// 2. 根据请求计算响应
// 3. 把响应返回客户端
public class EchoServer {
// 先创建socket对象
private DatagramSocket socket;
// 构造方法
public EchoServer(int port) throws SocketException {
socket = new DatagramSocket(port);
}
// 启动服务器,去完成主要的业务逻辑
public void start() throws IOException {
// 由于是服务器,所以需要一直运行,直到用户主动退出
System.out.println("服务器启动!");
while (true) {
// 1. 读取请求并解析
// 1) 创建一个空白的DatagramPacket对象,用于存储读取到的请求
DatagramPacket requPacket = new DatagramPacket(new byte[4096], 4096);
// 2) 通过receive读取网卡数据,如果没有读取到请求,那么receive会阻塞等待
socket.receive(requPacket); // 这里receive需要传入一个输出型参数,用于存储读取到的请求
// 3) 从DatagramPacket中提取出请求内容,并解析成字符串
String request = new String(requPacket.getData(), 0, requPacket.getLength()); // 取出有效数据的部分
// 2. 根据请求计算响应
String response = process(request);
// 3. 把响应返回客户端
// 1) 把响应构造回DatagramPacket对象
DatagramPacket respPacket = new DatagramPacket(response.getBytes(), response.getBytes().length,
requPacket.getSocketAddress()); // 第三个参数是为了拿到请求这个数据报对应的响应的目标ip和端口
// 2) 通过send发送响应
socket.send(respPacket); // 由于UDP是无连接的,里面不保存目标地址和端口,所以发送响应时需要指定目标地址和端口
// 3) 打印日志
System.out.printf("[%s:%d] req: %s, resp: %s\n",
requPacket.getSocketAddress().toString(),
requPacket.getPort(),
request,
response);
}
}
// 由于是回显服务器,所以响应内容和请求内容是一样的,这里直接返回请求内容即可
public String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
EchoServer server = new EchoServer(9090);
server.start();
}
}
UDP回显客户端
java
package Network.UDP;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.util.Scanner;
import java.io.IOException;
// 回显客户端
// 1. 从控制台读取用户输入内容
// 2. 通过网络发送给服务器
// 3. 从服务器读取到响应
// 4. 把响应结果显示到控制台
public class EchoClient {
private DatagramSocket socket;
private String serverIp;
private int serverPort;
// 构造方法
// 1. 初始化 socket
// 2. 客户端的构造方法需要记录服务器的 IP 和 Port
// 作为服务器, 发数据的时候, 就可以通过收到的请求, 直到是要发给谁.
// 作为客户端,主动发起的一方,必须得事先知道服务器在哪里.(程序员手动指定的)
// 且客户端在创建socket对象的时候,不需要指定端口号,操作系统会自动分配一个可用的端口号
public EchoClient(String serverIp, int serverPort) throws IOException {
this.serverIp = serverIp;
this.serverPort = serverPort;
socket = new DatagramSocket();
}
// 启动客户端
public void start() throws IOException {
Scanner scanner = new Scanner(System.in);
System.out.println("客户端启动!");
// System.out.println("动物字典翻译客户端启动!");
while (true) {
// 1. 从控制台读取用户输入内容
System.out.println(">");
// System.out.println("请输入动物英文>");
String request = scanner.next();
// 2. 构造成UDP请求包并发送给服务器
DatagramPacket reqPacket = new DatagramPacket
(request.getBytes(),
request.getBytes().length,
InetAddress.getByName(serverIp),
serverPort);
socket.send(reqPacket);
// 3. 从服务器读取到响应
DatagramPacket respPacket = new DatagramPacket(new byte[4096], 4096);
socket.receive(respPacket);
String response = new String(respPacket.getData(), 0, respPacket.getLength());
// 4. 把响应结果显示到控制台
System.out.println(response);
}
}
public static void main(String[] args) throws IOException {
// EchoClient echoClient = new EchoClient("192.168.36.65", 9090);
EchoClient echoClient = new EchoClient("127.0.0.1", 9090);
echoClient.start();
}
}
TCP版本
TCP就像中秋的明月,稳定而持久,适合传递长篇的思念之情。
TCP回显服务器
java
package Network.TCP;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
public class EchoServer {
// 这个属性表示服务器端的 socket 对象,用来监听客户端的连接请求
private ServerSocket serverSocket;
// 构造方法
public EchoServer(int port) throws IOException {
// 服务器启动就会创建 ServerSocket 对象,绑定到指定端口
// 这个类主要负责监听客户端的连接请求,建立连接
serverSocket = new ServerSocket(port);
}
// 启动服务器
public void start() throws IOException{
System.out.println("TCP服务器启动!");
// 创建一个线程池, 用来处理客户端连接
ExecutorService threadPool = Executors.newCachedThreadPool();
while (true) {
// 由于TCP是有连接的,不能直接一上来就读写数据,需要先处理连接请求
// 建立连接的过程,是在操作系统内核中完成了,不需要我们自己实现,我们只需要把系统建立好的连接拿上来就可以
// 当有客户端连接时,服务器会创建一个新的 Socket 对象,用来和客户端通信
Socket socket = serverSocket.accept(); // accept相当于接听电话,需要等待客户端连接,如果没有客户端连接,就会产生阻塞
// 多线程版本
// 每当收到一个客户端连接时,就创建一个新的线程来处理这个连接,实现并发
// Thread thread = new Thread(() -> {
// // 这个地方的this和processConnection方法中的socket是不同的
// // 这个地方的this是指EchoServer对象, 而processConnection方法中的socket是指客户端的socket对象
// this.processConnection(socket);
// });
// thread.start();
// 线程池版本
// 每当收到一个客户端连接时,就把这个连接放到线程池中, 线程池会自动分配一个线程来处理这个连接
threadPool.submit(() -> {
processConnection(socket);
//此处其实还有问题
// 假设现在有非常多的客户端,这些客户端连接之后,不会立刻销毁,而是会存在一定的时间~~
// 此时如果客户端很多,并且又不快速销毁,就会短时间内出现大量的线程对于操作系统来说,线程的数目也不是无限的~~
// 解决办法:IO多路复用
// 即一个线程可以同时监听多个socket对象, 当有socket对象就绪时, 就会触发事件, 我们就可以处理这个事件
// 这样就可以避免创建大量的线程, 减少了系统资源的消耗
});
}
}
// 处理一个客户端/一个连接逻辑
private void processConnection(Socket socket){
System.out.printf("[%s:%d] 客户端上线!\n", socket.getInetAddress().toString(), socket.getPort());
try(InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()){
// 以下实现通信代码
// 由于一个客户端可能会与服务器有多轮的请求响应交互,
// 所以我们需要在一个循环中, 不断地读取客户端的请求, 并发送响应
// 直到客户端主动关闭连接
// 由于这俩对象自身不持有文件描述符, 所以不需要在 finally 中关闭
Scanner scanner = new Scanner(inputStream); // 这个写法表示从socket的输入流中读取数据,而不是从控制台读取
PrintWriter writer = new PrintWriter(outputStream); // 这个写法表示把socket的输出流包装成一个打印流,方便我们写入字符串
while (true) {
// 针对客户端逻辑下线进行处理,如果客户端断开连接了(比如客户端结束了)
// 此时的hasNext()方法会返回false,
if(!scanner.hasNext()) {
System.out.printf("[%s:%d] 客户端下线!\n", socket.getInetAddress().toString(), socket.getPort());
break;
}
// 没有执行到这个打印,说明上面的hasNext()方法没有解除阻塞,大概率说明客户端没有发来数据
System.out.println("服务器收到数据了!");
// 1. 读取请求并解析,这个地方有个更简单的办法
String request = scanner.next();
// 2. 根据请求计算响应
String response = process(request);
// 3. 把响应发送给客户端
writer.println(response);
writer.flush(); // 刷新缓冲区, 确保响应及时发送给客户端
System.out.printf("[%s:%d] req: %s, resp: %s\n", socket.getInetAddress().toString(), socket.getPort(), request, response);
}
}catch(IOException e){
e.printStackTrace();
}finally{
// 因为会不断进行频繁连接, 所以需要在 finally 中释放资源
// 释放资源
try{
socket.close();
}catch(IOException e){
e.printStackTrace();
}
}
}
private String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
EchoServer server = new EchoServer(9090);
server.start();
}
}
TCP回显客户端
java
package Network.TCP;
import java.net.Socket;
import java.util.Scanner;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
public class EchoClient {
private Socket socket;
// 构造方法, 用于初始化客户端的 socket 对象
public EchoClient(String serverIP, int serverPort) throws IOException {
// 1. 创建一个 socket 对象, 并指定服务器的 IP 和 Port
// 在new这个对象的时候,就会涉及到"建立连接操作"
// 由于连接建立好了之后,服务器的信息就在系统中被TCP协议记录下来了,所以我们在应用层就不需要再指定服务器的 IP 和 Port
socket = new Socket(serverIP, serverPort);
}
public void start() throws IOException {
System.out.println("TCP客户端启动!");
Scanner scanner = new Scanner(System.in); // 这个Scanner 是用来读取用户从控制台输入的内容的
try(InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()){
Scanner scannerNet = new Scanner(inputStream); // 这个Scanner 是用来读取服务器返回的响应的
PrintWriter writer = new PrintWriter(outputStream);
while (true) {
// 1.先从控制台读取内容
System.out.print("> ");
String request = scanner.next();
// 2.构造请求发送给服务器
writer.println(request); // 此处 println 是执行到了,但是 println 只是把数据先写到缓冲区(内存)中,没有真正的写入网卡,也就没有真正发送
writer.flush(); // 手动刷新缓冲区, 确保数据被发送出去
// 3.读取服务器的响应
if (!scannerNet.hasNextLine()) {
System.out.println("服务器返回的响应为空!");
break;
}
String response = scannerNet.next();
// 4.把响应显示在控制台上
System.out.println(response);
}
}catch(IOException e){
e.printStackTrace();
}
}
public static void main(String[] args) throws IOException {
EchoClient echoClient = new EchoClient("127.0.0.1", 9090);
echoClient.start();
}
}
五、网络编程核心概念与中秋寓意
1. 客户端与服务端:游子与家乡
java
// 游子就像客户端,主动发起连接
Socket socket = new Socket("家乡的IP", 8080);
// 家乡就像服务端,永远等待游子归来
ServerSocket serverSocket = new ServerSocket(8080);
2. 请求与响应:思念与回复
就像中秋时节:
- 请求:游子发送"我想家了"
- 响应:家乡回复"月亮圆了,该回来了"
3. 长短连接:短暂的问候与持续的牵挂
连接类型 | 中秋寓意 | 代码表现 |
---|---|---|
短连接 | 节日的短暂问候 | 每次发送祝福后关闭连接 |
长连接 | 持续的亲情牵挂 | 保持连接,持续对话 |
六、特色功能
基于UDP实现简易英汉字典模拟器
java
/**
* 模拟中秋月光强度,根据日期计算月亮圆度
*/
package Network.UDP;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
// 动物字典翻译服务器
public class DictServer extends EchoServer {
private Map<String, String> dict = new HashMap<>();
// 构造方法
public DictServer(int port) throws IOException {
super(port);
dict.put("cat", "猫");
dict.put("dog", "狗");
dict.put("fish", "鱼");
dict.put("bird", "鸟");
dict.put("mouse", "老鼠");
dict.put("snake", "蛇");
dict.put("elephant", "大象");
dict.put("monkey", "猴子");
dict.put("rabbit", "兔子");
dict.put("tiger", "老虎");
dict.put("lion", "狮子");
dict.put("giraffe", "长颈鹿");
dict.put("horse", "马");
dict.put("zebra", "斑马");
dict.put("penguin", "企鹅");
dict.put("koala", "考拉");
dict.put("panda", "熊猫");
dict.put("chicken", "鸡");
dict.put("duck", "鸭");
dict.put("goat", "山羊");
dict.put("cow", "奶牛");
dict.put("sheep", "羊");
}
@Override
public String process(String request) {
// 方法重写,本质就是对父类的功能进行拓展
// 如,本来没有翻译功能,现在需要添加翻译功能
return dict.getOrDefault(request, "没有找到该单词的翻译");
}
public static void main(String[] args) throws IOException {
DictServer server = new DictServer(9090);
server.start();
}
}
中秋祝福语生成器
java
/**
* 智能生成中秋祝福语
*/
public class BlessingGenerator {
private static String[] templates = {
"愿明月带去我对{name}的{emotion},祝您{wish}!",
"{name},中秋快乐!愿您如明月般{wish}!",
"月圆人团圆,祝{name}{wish}!"
};
private static String[] emotions = {"思念", "祝福", "牵挂", "问候"};
private static String[] wishes = {"身体健康", "事业有成", "家庭幸福", "万事如意"};
public static String generateBlessing(String name) {
Random random = new Random();
String template = templates[random.nextInt(templates.length)];
return template.replace("{name}", name)
.replace("{emotion}", emotions[random.nextInt(emotions.length)])
.replace("{wish}", wishes[random.nextInt(wishes.length)]);
}
}
七、技术总结与中秋感悟
技术要点
- ✅ Socket编程基础(UDP/TCP)
- ✅ 多线程并发处理
- ✅ 网络通信协议理解
- ✅ 异常处理机制
🎑 中秋寄语:
月圆人团圆,代码传思念。
愿这轮明月,照亮每个程序员的归家路;
愿这段代码,传递每份游子的思乡情。
祝大家中秋快乐,阖家幸福!
