文章目录
01 理解 OSI 和 TCP/IP 参考模型
1. OSI 参考模型
OSI(Open Systems Interconnection)参考模型是由国际标准化组织(ISO)开发的七层网络框架,用于标准化计算机通信功能。它从物理层到应用层逐层定义了网络通信的各个方面。
- 应用层(Application Layer):提供直接服务给用户的应用程序,如HTTP、FTP、SMTP等。
- 表示层(Presentation Layer):处理数据的格式和加密,确保发送端和接收端的数据格式一致。
- 会话层(Session Layer):管理和控制会话,包括建立、管理和终止会话。
- 传输层(Transport Layer):提供端到端的通信服务,确保数据传输的可靠性和顺序性(如TCP)。
- 网络层(Network Layer):处理数据包的路由和转发,如IP协议。
- 数据链路层(Data Link Layer):负责节点之间的数据传输和错误检测,如以太网。
- 物理层(Physical Layer):定义物理传输介质的特性,如电缆、光纤等。
2. TCP/IP 参考模型
TCP/IP参考模型是用于互联网的网络协议栈,由四个层次组成:
- 应用层:包括所有高层协议,如HTTP、FTP、SMTP等。
- 传输层:主要是TCP和UDP,负责数据的传输。
- 网络层(Internet Layer):处理数据包的路由,如IP协议。
- 网络接口层(Link Layer):与OSI模型的数据链路层和物理层类似,处理硬件级别的数据传输。
3. 代码案例
下面是一个简单的Java程序,展示了如何使用TCP/IP协议进行基本的客户端-服务器通信。这个案例将包括一个简单的服务器和客户端。
3.1 服务器端
import java.io.*;
import java.net.*;
public class SimpleServer {
public static void main(String[] args) {
try (ServerSocket serverSocket = new ServerSocket(8080)) {
System.out.println("Server is listening on port 8080");
while (true) {
Socket clientSocket = serverSocket.accept();
System.out.println("Client connected: " + clientSocket.getInetAddress().getHostAddress());
// 获取输入流来读取客户端消息
BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
String inputLine;
// 读取并打印客户端消息
while ((inputLine = in.readLine()) != null) {
System.out.println("Received: " + inputLine);
if ("exit".equals(inputLine)) {
break;
}
}
// 关闭连接
clientSocket.close();
}
} catch (IOException e) {
System.out.println("Exception caught when trying to listen on port " + 8080 + " or listening for a connection");
System.out.println(e.getMessage());
}
}
}
3.2 客户端
import java.io.*;
import java.net.*;
public class SimpleClient {
public static void main(String[] args) {
try (Socket socket = new Socket("localhost", 8080);
PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
BufferedReader stdIn = new BufferedReader(new InputStreamReader(System.in))) {
String userInput;
System.out.println("Connected to server. Type 'exit' to end the connection.");
// 读取用户输入并发送到服务器
while ((userInput = stdIn.readLine()) != null) {
out.println(userInput);
if ("exit".equals(userInput)) {
break;
}
}
} catch (UnknownHostException e) {
System.out.println("UnknownHostException: " + e.getMessage());
} catch (IOException e) {
System.out.println("IOException: " + e.getMessage());
}
}
}
3.3 代码解读
- 服务器端:
- 使用 ServerSocket 监听 8080 端口。
- 接受客户端连接后,使用 BufferedReader 读取客户端发送的消息。
- 当接收到"exit"消息时,关闭连接。
- 客户端:
- 连接到本地的 8080 端口。
- 使用 PrintWriter 向服务器发送消息。
- 使用 BufferedReader 读取用户输入并发送到服务器。
- 当用户输入"exit"时,关闭连接。
这个例子展示了如何在 Java 中实现基本的网络通信,涉及到了 TCP/IP 模型中的传输层和应用层。通过这种方式,你可以理解网络通信的基本原理,并进一步扩展到更复杂的网络应用。
02 理解端口号
理解端口号原理的形象例子可以类比为一个大型的邮局或物流中心:
1. 端口号原理类比
想象一个超大型的邮局,这个邮局不仅处理普通邮件,还处理各种类型的包裹和货物。邮局有多个不同的部门,每个部门负责处理不同类型的邮件或货物。
- 邮局:可以看作是你的计算机或服务器。
- 邮箱地址:相当于 IP 地址,指出邮局的位置。
- 部门:每个部门有不同的编号,这就是端口号。
2. 形象例子
- 邮局(计算机):
- 这个邮局(你的计算机)每天处理大量的邮件和货物。
- 部门(端口号):
- 部门 100(端口 100):专门处理报纸订阅。
- 部门 80(端口 80):负责处理网站浏览请求。
- 部门 25(端口 25):专门处理电子邮件。
- 部门 3306(端口 3306):负责数据库连接。
- 邮件处理:
- 当你(客户端)发送一个请求(邮件)到这个邮局,你需要指定你想要这个邮件送到哪个部门(端口号)。
- 例如,你想浏览一个网站,你的请求会标记为送到部门 80(HTTP 服务)。
- 内部处理:
- 邮局内部的工作人员(操作系统)看到邮件上的部门编号(端口号),然后将邮件(数据包)转发到相应的部门。
- 每个部门(应用程序或服务)都有自己的处理方式。
3. 具体场景
- 访问网站:
- 你打开浏览器访问一个网站(如www.example.com),你的请求会指定到端口80(默认HTTP端口)。
- 服务器接收到这个请求后,知道这是要给网站浏览部门处理的。
- 发送邮件:
- 你用邮件客户端发送一封电子邮件,邮件客户端会将邮件发送到邮局的部门25(SMTP端口)。
- 数据库连接:
- 你的应用程序需要连接到数据库,它会尝试连接到邮局的部门3306(MySQL默认端口)。
4. 代码案例
在编程中,端口号的使用类似于:
// 创建一个服务器监听在端口8080
ServerSocket serverSocket = new ServerSocket(8080);
// 客户端连接到服务器的特定端口
Socket clientSocket = new Socket("server.example.com", 8080);
在这个例子中:
- ServerSocket(8080)就像邮局开通了一个新的部门8080,专门处理某种类型的请求。
- Socket(“server.example.com”, 8080)就像你发送邮件时指定了邮箱地址和部门号。
5. 总结
通过这个邮局的类比,你可以理解端口号是如何工作的:
- IP 地址:告诉数据包去哪个邮局。
- 端口号:告诉数据包在那个邮局去哪个具体的部门。
这种方式确保了不同类型的网络通信能够被正确地路由到相应的服务或应用程序。
03 理解 TCP 和 UDP
理解 TCP 和 UDP 的一个形象例子可以是想象你和朋友在不同的情境下通信:
1. TCP - 像打电话
- 可靠性:当你打电话给朋友,你期望对方能听清楚你说的话。如果中间有噪音或信号不好,你会重复说,直到对方确认听懂了。
- 顺序性:你会按照逻辑顺序说出你的想法。如果中间有句子被打断或错位,你会重新组织语言让对话有条理。
- 连接:你先拨通电话,确认对方接听,然后开始对话。结束时,你会说再见,挂断电话。
例子:
- 你正在电话中告诉朋友如何做一道菜。你会确保每一步都讲清楚,如果朋友没听清,你会重复,直到确认他理解了每一步。
2. UDP - 像发送短信或微信
- 不可靠性:你发送了一条短信给朋友,但没有确认他是否收到。如果网络不好,消息可能丢失,你可能不知道。
- 无顺序:如果你连续发送几条消息,它们可能不是按你发送的顺序到达。
- 无连接:你不需要先建立连接就可以发送消息。就像你发送了一条消息后,可能就去做别的事情了,不等回复。
例子:
- 你在派对上,你和朋友在人群中通过微信聊天。你发送了几个笑话,但你并不在意他是否都看到了或按顺序看的。你只是发送了,然后继续享受派对。
3. 具体场景对比
- TCP:就像你和朋友在讨论一个复杂的项目计划,你需要确保每一步都准确无误,所以你会不断确认对方是否理解了你的意思。这就像下载一个大文件,你希望它完整且顺序正确地到达。
- UDP:像你在游戏中和朋友一起玩,你会发送一些实时的位置更新或动作指令,但你并不在意每一条指令都准确到达,因为游戏会通过其他机制(如预测)来处理丢失的数据。这就像视频流或语音通话,偶尔的丢包不会影响整体体验。
通过这种类比,你可以更直观地理解 TCP 提供的是类似电话的可靠、有序、连接导向的通信,而 UDP 则类似于短信或即时消息,是快速、不可靠、无连接的通信方式。
04 理解 Socket 原理
理解 Socket 原理的形象例子可以类比为一个邮局系统:
1. Socket 原理类比
**Socket **可以被看作是网络通信中的"邮箱"。就像在现实生活中,你有一个邮箱地址,任何人只要知道这个地址,就可以给你发送邮件。Socket 也是类似的:
- IP地址:就像邮箱的地址,它告诉数据包应该发送到哪里。
- 端口号:就像邮箱中的一个具体的收件箱,每个应用程序或服务可以有自己的端口号。
2. 形象例子
想象你和朋友住在同一个城市,但你们住在不同的街区(不同的 IP 地址)。你们决定通过邮局系统(网络)来通信。
- 建立连接:
- 你(客户端)决定给朋友(服务器)写信。你需要知道朋友的邮箱地址(IP 地址)和收件箱号(端口号)。
- 你写好信(数据),在信封上写上朋友的邮箱地址和收件箱号,然后投递到邮局(发送数据包)。
- 邮局处理:
- 邮局(网络)接收到信件后,根据地址将信件送到朋友所在的街区(路由)。
- 街区的邮递员(数据链路层)将信件投递到朋友的邮箱(端口号)。
- 接收信件:
- 朋友(服务器)每天都会去邮箱(端口)检查有没有新信件。
- 如果有信件,朋友会打开信件(接收数据),然后根据信的内容做出回应(发送回数据)。
- 持续通信:
- 你们可以持续通过这种方式通信,每次都使用相同的邮箱地址和收件箱号。
- 如果是 TCP 连接,就像你们之间建立了一条专线,每次通信都通过这条线。
- 关闭连接:
- 当通信结束,你们会告知对方不再需要继续通信了,就像挂断电话或结束一次会话。
3. 具体 Socket 操作
- 创建 Socket:就像你申请了一个邮箱地址和收件箱号。
- 绑定 Socket:就像你告诉邮局你的邮箱地址和收件箱号。
- 监听 Socket:就像朋友每天去邮箱检查有没有新信件。
- 发送/接收数据:就像你和朋友通过邮箱交换信件。
- 关闭 Socket:就像你告诉邮局不再使用这个邮箱。
4. 代码案例
假设你有一个 Java 程序:
ServerSocket serverSocket = new ServerSocket(8080); // 申请一个邮箱,收件箱号是8080
Socket clientSocket = serverSocket.accept(); // 等待朋友的信件
BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true);
// 读取朋友的信件
String message = in.readLine();
System.out.println("Received: " + message);
// 回复朋友
out.println("Hello from server!");
在这个例子中:
- ServerSocket 是邮局系统,你在邮局申请了一个收件箱 8080。
- accept() 方法就像等待朋友的信件。
- BufferedReader 和 PrintWriter 是用来读写信件的工具。
通过这种邮局系统的类比,你可以更直观地理解 Socket 是如何在网络通信中工作的。Socket 就像是网络中的邮箱,提供了一个标准化的方式来发送和接收数据。
5. 端口工作原理
可以用现实生活中的邮局和信件投递来比喻**「通讯的流程,监听端口的作用,临时端口的作用」**:
5.1. 监听端口的作用:
- 比喻为:邮局的信箱
- 解释:假设一个邮局(服务器)有多个信箱(端口),每个信箱负责接收特定类型的信件(服务)。这些信箱每天都在等待信件的到来,这就像服务器上的监听端口,它们时刻准备着接收来自外部的连接请求。
5.2. 通讯的流程:
- 比喻为:寄信的过程
- 解释:当你想寄一封信(客户端发出请求)时,你会将信投递到指定的邮局信箱(目标服务器的监听端口)。信件被投递到对应的信箱后,邮局会根据信件内容决定由哪个部门处理(服务处理请求),然后发送回执信(服务器响应客户端),告诉你信已经收到并开始处理。
5.3. 临时端口的作用:
- 比喻为:回信的地址
- 解释:在邮局处理信件后,需要将处理结果(响应)寄回给你。为此,你必须在信件上写明自己的地址(客户端的临时端口),邮局才能知道要将回信寄往哪里。这个临时地址类似于临时端口,它是客户端在每次通讯时随机生成的,用来接收特定响应的“回信地址”。
5.4. 整体流程:
- 你(客户端)发出请求,将信(请求数据)寄到邮局(服务器)的特定信箱(监听端口)。
- 邮局接收信件,并将回信寄回到你预先提供的地址(临时端口),完成一次通讯。
这个比喻帮助理解:
- 监听端口是服务器上预留的,用来接收特定类型的请求。
- 通讯流程就像寄信和收信的过程。
- 临时端口是客户端用来接收服务器响应的地址,在每次通讯时都可能变化。
5.5. 图解端口工作原理
这张图展示了在网络编程中,客户端和服务器之间通过端口进行通讯的基本流程。以下是对图中内容的详细解读:
1. 监听端口 (Listening Port):
- 位置:服务器上标识为“监听Port: 8888”。
- 作用:监听端口是服务器上预先配置的端口,它一直处于监听状态,等待客户端的连接请求。图中的服务器监听端口是8888。
- 解释:当客户端想要与服务器通信时,它会向服务器的监听端口发出连接请求。监听端口负责接受这些请求,并建立初始的连接。
2. 客户端 (Client):
- 位置:图中有两个客户端,分别标识为“客户端1”和“客户端2”。
- 客户端端口:每个客户端都有自己的端口号,如“客户端1”使用1230端口,“客户端2”使用1235端口。
- 作用:客户端端口是客户端应用程序用来发送和接收数据的端口。客户端通过这些端口向服务器发出请求,并接收服务器的响应。
3. 临时端口 (Ephemeral Port):
- 位置:服务器上标识为“临时Port: 2001”和“临时Port: 2002”。
- 作用:当服务器接收到客户端的连接请求后,它会为每个客户端分配一个临时端口,以处理该客户端的后续通信。这样,服务器可以同时处理多个客户端的请求,而不会产生冲突。
- 解释:比如,图中“客户端1”通过1230端口与服务器的监听端口8888建立连接后,服务器为它分配了临时端口2001,用于后续数据通信。同样地,“客户端2”通过1235端口与监听端口8888连接后,服务器为它分配了临时端口2002。
4. 整体通讯流程:
- 步骤1:客户端1(1230端口)和客户端2(1235端口)分别向服务器的监听端口8888发送连接请求。
- 步骤2:服务器接收到连接请求后,为客户端1分配临时端口2001,为客户端2分配临时端口2002。
- 步骤3:之后,客户端1和服务器通过端口1230与2001之间的连接进行通信,而客户端2和服务器通过端口1235与2002之间的连接进行通信。
5. 图中的重点:
- 服务器上的监听端口(如8888)是用来接受连接请求的,而临时端口(如2001、2002)则是用来处理具体通信数据的。这确保了服务器可以同时处理来自多个客户端的请求。
通过这个图,我们可以理解网络通信中的端口分配及其作用,尤其是在服务器上如何通过监听端口和临时端口来实现多客户端的并发处理。
05 基于 TCP 的 Socket 编程
1. 案例 1
理解基于 TCP 的 Socket 编程,可以类比为在两个城市之间建立了一条专线电话系统。以下是一个形象的例子:
2. 专线电话系统类比
想象你和你的朋友分别住在两个不同的城市(不同的 IP 地址),你们决定通过一条专线电话(TCP 连接)来通信。
3. 建立连接
- 申请电话线:
- 你(客户端)申请了一条到朋友城市的专线电话。这就像创建一个 Socket。
- 拨通电话:
- 你拨通了朋友的电话号码(IP 地址和端口号),等待朋友接听。这就像客户端尝试连接到服务器。
4. 通信过程
- 接听电话:
- 朋友(服务器)接听电话,确认连接建立。这就像服务器接受客户端的连接。
- 对话:
- 你们开始对话,每次说话(发送数据)都会确认对方听到了(TCP 的可靠性保证)。这就像在 Socket 编程中,客户端和服务器之间交换数据。
- 挂断电话:
- 当对话结束,你们挂断电话。这就像关闭 Socket 连接。
5. 代码案例
假设你和朋友在不同的城市,你们想通过专线电话讨论一个项目:
- 你(客户端):
- 你申请了一条专线电话(创建Socket)。
- 你拨通了朋友的号码(连接到服务器)。
- 朋友(服务器):
- 朋友接听电话(接受连接)。
- 你们开始讨论项目,每次你说完一句话(发送数据),你会等待朋友的回应(确认数据已接收)。
在 Java 中,基于 TCP 的 Socket 编程可以这样实现:
5.1 客户端
// 客户端(你)
import java.io.*;
import java.net.*;
public class Client {
public static void main(String[] args) {
try (
Socket socket = new Socket("server.example.com", 8080); // 拨通电话
PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
) {
// 发送消息(说话)
out.println("Hello from client!");
// 接收回复(听朋友的回答)
String response = in.readLine();
System.out.println("Server says: " + response);
} catch (IOException e) {
System.out.println("Exception caught: " + e);
}
}
}
5.2 服务端
// 服务器(朋友)
import java.io.*;
import java.net.*;
public class Server {
public static void main(String[] args) {
try (
ServerSocket serverSocket = new ServerSocket(8080); // 接听电话
Socket clientSocket = serverSocket.accept(); // 等待你拨通
PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true);
BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
) {
// 接收消息(听你说话)
String inputLine;
while ((inputLine = in.readLine()) != null) {
System.out.println("Received: " + inputLine);
// 回复消息(回答你)
out.println("Hello from server!");
if ("exit".equals(inputLine)) break;
}
} catch (IOException e) {
System.out.println("Exception caught: " + e);
}
}
}
6. 总结
通过这个专线电话系统的类比,你可以理解基于 TCP 的 Socket 编程:
- 创建 Socket:就像申请一条专线电话。
- 连接:就像拨通电话号码。
- 数据传输:就像对话,确保每句话都被对方听到了。
- 关闭连接:就像挂断电话。
这种方式确保了数据的可靠传输,类似于电话对话,你会等待对方确认听到了你的话。
7. 案例 2
理解基于 TCP 的 Socket 编程,可以类比为两个人在电话上进行一次详细的对话。TCP 提供的是一种可靠、有序、连接导向的通信方式,就像电话通话一样。
8. 电话对话类比
想象你和朋友通过电话进行一次重要的对话:
- 建立连接:
- 你(客户端)拨通了朋友(服务器)的电话号码,等待他接听。
- 朋友接起电话,说:“喂,你好。”(握手过程)
- 对话(数据传输):
- 你开始说话,每句话都希望对方能听清楚。
- 如果你说了什么对方没听清,你会重复,直到对方确认听懂了。
- 对话是有序的,你会按照逻辑顺序说出你的想法。
- 确认和重传:
- 如果电话信号不好,某些话可能没传到对方那里,你会发现对方没有回应或请求你重复。
- 你会不断确认对方是否听懂了你的话。
- 结束对话:
- 当对话结束,你会说再见,挂断电话(关闭连接)。
9. 代码案例
在编程中,TCP Socket 的通信过程类似于上述电话对话:
9.1 服务器端(朋友接电话)
import java.io.*;
import java.net.*;
public class Server {
public static void main(String[] args) {
try (ServerSocket serverSocket = new ServerSocket(8080)) {
System.out.println("Server is listening on port 8080");
// 等待客户端连接
Socket clientSocket = serverSocket.accept();
System.out.println("Client connected: " + clientSocket.getInetAddress().getHostAddress());
// 建立输入流和输出流
PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true);
BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
String inputLine;
while ((inputLine = in.readLine()) != null) {
System.out.println("Received: " + inputLine);
out.println("Server received: " + inputLine); // 回复客户端
if ("exit".equals(inputLine)) {
break;
}
}
// 关闭连接
clientSocket.close();
} catch (IOException e) {
System.out.println("Exception caught when trying to listen on port 8080 or listening for a connection");
System.out.println(e.getMessage());
}
}
}
9.2 客户端(你打电话)
import java.io.*;
import java.net.*;
public class Client {
public static void main(String[] args) {
try (Socket socket = new Socket("localhost", 8080);
PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
BufferedReader stdIn = new BufferedReader(new InputStreamReader(System.in))) {
System.out.println("Connected to server. Type 'exit' to end the connection.");
String userInput;
while ((userInput = stdIn.readLine()) != null) {
out.println(userInput); // 发送消息到服务器
String response = in.readLine(); // 等待服务器的回复
System.out.println("Server response: " + response);
if ("exit".equals(userInput)) {
break;
}
}
} catch (UnknownHostException e) {
System.out.println("UnknownHostException: " + e.getMessage());
} catch (IOException e) {
System.out.println("IOException: " + e.getMessage());
}
}
}
9.3 代码解释
- 建立连接:客户端尝试连接到服务器的 8080 端口,就像你拨通朋友的电话号码。
- 数据传输:双方通过输入输出流进行通信,就像电话对话。TCP 会确保数据按顺序到达,并且如果有丢失的数据包,会自动重传。
- 确认和重传:TCP 协议会处理这些细节,确保消息完整无误地传输,就像你会确认朋友是否听懂了。
- 关闭连接:当通信结束,双方关闭 Socket,就像挂断电话。
通过这种类比,你可以更形象地理解TCP Socket编程是如何工作的。它提供了一种可靠的通信方式,就像电话对话一样,确保数据按顺序传输,并且丢失的数据会被重传。
06 基于多线程的 Socket 编程
1. 案例 1
理解基于多线程的 Socket 编程,可以类比为一个繁忙的餐馆管理系统。多线程允许服务器同时处理多个客户端请求,就像餐馆同时服务多个顾客。
2. 餐馆管理系统类比
想象一个繁忙的餐馆:
- 顾客进门(客户端连接):
- 顾客(客户端)走进餐馆(服务器),想要点餐。
- 服务员分工(多线程处理):
- 餐馆有多个服务员(线程),每个服务员负责一个或多个桌子(客户端)。
- 当一个顾客进门,服务员会迎上去,带他到一个空桌子。
- 点餐和服务(数据传输):
- 每个顾客(客户端)可以独立地与服务员(线程)互动,点菜、询问菜单等。
- 服务员可以同时管理多个桌子,但每个桌子上的服务是独立的。
- 厨房处理(后台处理):
- 厨房(服务器的其他部分)处理所有订单,但每个订单是独立处理的。
- 顾客离开(关闭连接):
- 当顾客完成用餐后,服务员会清理桌子,准备迎接下一个顾客。
3. 代码案例
在编程中,多线程的 Socket 编程类似于上述餐馆管理系统:
3.1 服务器端(餐馆)
import java.io.*;
import java.net.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class MultiThreadedServer {
private static final int PORT = 8080;
private static ExecutorService executorService = Executors.newFixedThreadPool(10); // 10个服务员
public static void main(String[] args) {
try (ServerSocket serverSocket = new ServerSocket(PORT)) {
System.out.println("Server is listening on port " + PORT);
while (true) {
Socket clientSocket = serverSocket.accept();
System.out.println("New client connected: " + clientSocket.getInetAddress().getHostAddress());
// 为每个新连接创建一个新的线程
executorService.submit(new ClientHandler(clientSocket));
}
} catch (IOException e) {
System.out.println("Exception caught when trying to listen on port " + PORT + " or listening for a connection");
System.out.println(e.getMessage());
}
}
// 内部类,处理每个客户端的线程
private static class ClientHandler implements Runnable {
private Socket clientSocket;
public ClientHandler(Socket socket) {
this.clientSocket = socket;
}
@Override
public void run() {
try (
PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true);
BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()))
) {
String inputLine;
while ((inputLine = in.readLine()) != null) {
System.out.println("Received from " + clientSocket.getInetAddress().getHostAddress() + ": " + inputLine);
out.println("Server received: " + inputLine);
if ("exit".equals(inputLine)) {
break;
}
}
} catch (IOException e) {
System.out.println("Exception caught when trying to listen on port " + PORT + " or listening for a connection");
System.out.println(e.getMessage());
} finally {
try {
clientSocket.close();
} catch (IOException e) {
System.out.println("Exception caught when closing client socket");
System.out.println(e.getMessage());
}
}
}
}
}
3.2 代码解释
- 服务员(线程):ExecutorService创建一个线程池,模拟多个服务员。
- 顾客进门(客户端连接):serverSocket.accept()等待客户端连接。
- 服务员分工:每个新连接创建一个ClientHandler线程,负责与该客户端的通信。
- 点餐和服务:每个线程独立处理客户端的请求和响应。
- 顾客离开:当客户端关闭连接,线程结束,资源被释放。
通过这种类比,你可以更形象地理解多线程Socket编程:
- 每个线程就像餐馆里的一个服务员,独立处理一个或多个顾客。
- 这种方式允许服务器同时处理多个客户端请求,提高了效率,就像餐馆可以同时服务多个顾客。
4. 案例 2
理解基于多线程的 Socket 编程,可以类比为一个繁忙的餐厅服务系统。在这个系统中,厨师(服务器)可以同时处理多个顾客(客户端)的订单,而服务员(线程)负责将订单从顾客传递给厨师,并将菜品从厨师传递给顾客。
5. 餐厅服务系统类比
想象一个热闹的餐厅:
- 顾客下单(客户端连接):
- 顾客(客户端)走进餐厅,找到一个空座位坐下。
- 服务员(线程)立即过来接待这个顾客。
- 服务员处理订单(线程处理):
- 每个顾客都有自己的服务员,服务员负责记录订单并传递给厨师。
- 服务员还会将厨师准备好的菜品端给顾客。
- 厨师处理订单(服务器处理):
- 厨师(服务器)在后厨集中处理所有订单,但每个订单都是独立处理的。
- 厨师不会同时为一个顾客做所有菜,而是会轮流处理不同顾客的订单。
- 多线程处理:
- 每个顾客的订单由一个独立的服务员(线程)处理,这样厨师可以同时为多个顾客准备食物。
- 即使一个顾客的订单很复杂,另一个顾客的订单仍然可以被处理。
6. 代码案例
在编程中,基于多线程的 Socket 编程类似于上述餐厅服务系统:
6.1. 服务器端(厨师 + 服务员)
import java.io.*;
import java.net.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class MultiThreadedServer {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(10); // 创建一个线程池
try (ServerSocket serverSocket = new ServerSocket(8080)) {
System.out.println("Server is listening on port 8080");
while (true) {
Socket clientSocket = serverSocket.accept();
System.out.println("Client connected: " + clientSocket.getInetAddress().getHostAddress());
// 为每个客户端创建一个新线程
executor.submit(() -> handleClient(clientSocket));
}
} catch (IOException e) {
System.out.println("Exception caught when trying to listen on port 8080 or listening for a connection");
System.out.println(e.getMessage());
} finally {
executor.shutdown(); // 关闭线程池
}
}
private static void handleClient(Socket clientSocket) {
try (BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true)) {
String inputLine;
while ((inputLine = in.readLine()) != null) {
System.out.println("Received: " + inputLine);
out.println("Server received: " + inputLine); // 回复客户端
if ("exit".equals(inputLine)) {
break;
}
}
} catch (IOException e) {
System.out.println("Exception caught when trying to listen on port 8080 or listening for a connection");
System.out.println(e.getMessage());
} finally {
try {
clientSocket.close();
} catch (IOException e) {
System.out.println("Exception caught when closing client socket");
System.out.println(e.getMessage());
}
}
}
}
6.2. 代码解释
- 线程池:ExecutorService用于管理线程,就像餐厅有多个服务员。
- 客户端连接:服务器接受客户端连接后,立即创建一个新线程来处理这个客户端,就像服务员为每个顾客提供服务。
- 独立处理:每个线程独立处理一个客户端的请求,就像服务员独立处理每个顾客的订单。
通过这种类比,你可以形象地理解多线程 Socket 编程:
- 服务器(厨师)可以同时处理多个客户端(顾客)的请求。
- 线程(服务员)负责将客户端的请求传递给服务器,并将服务器的响应传递给客户端。
这种方式使得服务器能够高效地处理多个客户端的请求,提高了系统的并发性和响应速度。