JavaEE初阶——中秋特辑:网络编程送祝福从 Socket 基础到 TCP/UDP 实战
—JAVAEE— ⬅(click)
月圆人团圆,用Java网络编程传递中秋祝福
🌕 又是一年中秋至,月圆人团圆。在这个充满温情的传统节日里,让我们用代码搭建沟通的桥梁,用网络编程传递中秋的祝福与思念。
一、中秋佳节与网络通信的浪漫邂逅
中秋的团圆 vs 网络的连接
中秋佳节,最重要的主题就是"团圆"。无论身在何方,人们都渴望与亲人团聚,共享天伦之乐。这正如网络编程中的客户端与服务端,虽然物理上相隔千里,但通过网络的纽带,彼此紧密相连。
中秋元素与网络概念的对应
中秋元素 | 网络概念 | 寓意 |
---|---|---|
🌕 明月 | 网络通道 | 传递思念的媒介 |
🥮 月饼 | 数据包 | 承载温情的内容 |
🏠 家乡 | 服务端 | 温暖的港湾 |
✈️ 游子 | 客户端 | 远方的思念 |
二、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
对象。
使用示例
// 创建目标地址: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回显服务器
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回显客户端
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回显服务器
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回显客户端
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 和 Portsocket = 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. 客户端与服务端:游子与家乡
// 游子就像客户端,主动发起连接
Socket socket = new Socket("家乡的IP", 8080);// 家乡就像服务端,永远等待游子归来
ServerSocket serverSocket = new ServerSocket(8080);
2. 请求与响应:思念与回复
就像中秋时节:
- 请求:游子发送"我想家了"
- 响应:家乡回复"月亮圆了,该回来了"
3. 长短连接:短暂的问候与持续的牵挂
连接类型 | 中秋寓意 | 代码表现 |
---|---|---|
短连接 | 节日的短暂问候 | 每次发送祝福后关闭连接 |
长连接 | 持续的亲情牵挂 | 保持连接,持续对话 |
六、特色功能
基于UDP实现简易英汉字典模拟器
/*** 模拟中秋月光强度,根据日期计算月亮圆度*/
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", "羊");}@Overridepublic String process(String request) {// 方法重写,本质就是对父类的功能进行拓展// 如,本来没有翻译功能,现在需要添加翻译功能return dict.getOrDefault(request, "没有找到该单词的翻译");}public static void main(String[] args) throws IOException {DictServer server = new DictServer(9090);server.start();}}
中秋祝福语生成器
/*** 智能生成中秋祝福语*/
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)
- ✅ 多线程并发处理
- ✅ 网络通信协议理解
- ✅ 异常处理机制
🎑 中秋寄语:
月圆人团圆,代码传思念。
愿这轮明月,照亮每个程序员的归家路;
愿这段代码,传递每份游子的思乡情。
祝大家中秋快乐,阖家幸福!