分布式计算技术详解
1. 套接字连接特性
套接字会创建一个“专用”连接,该连接会一直持续,直到被显式断开。不过,如果连接的某一方或中间链路崩溃,该专用连接也可能会意外断开。这意味着通信双方会被锁定在通信状态,连接始终保持开启。这种方式看似是一种合理的网络连接方法,但会给网络带来额外的负载。
2. 多客户端服务实现
JabberServer 虽然可以工作,但一次只能处理一个客户端。在典型的服务器中,通常需要同时处理多个客户端。解决方案是使用多线程,在不直接支持多线程的语言中,这会带来各种复杂问题。而在 Java 中,多线程的实现相对简单。
基本方案是在服务器中创建一个 ServerSocket 并调用 accept() 方法等待新连接。当 accept() 返回时,获取生成的 Socket 并使用它创建一个新线程,该线程的任务是为特定客户端服务。然后再次调用 accept() 等待新客户端。
以下是 MultiJabberServer 的代码示例:
//: c15:MultiJabberServer.java
// A server that uses multithreading
// to handle any number of clients.
import java.io.*;
import java.net.*;
class ServeOneJabber extends Thread {
private Socket socket;
private BufferedReader in;
private PrintWriter out;
public ServeOneJabber(Socket s)
throws IOException {
socket = s;
in =
new BufferedReader(
new InputStreamReader(
socket.getInputStream()));
// Enable auto-flush:
out =
new PrintWriter(
new BufferedWriter(
new OutputStreamWriter(
socket.getOutputStream())), true);
// If any of the above calls throw an
// exception, the caller is responsible for
// closing the socket. Otherwise the thread
// will close it.
start(); // Calls run()
}
public void run() {
try {
while (true) {
String str = in.readLine();
if (str.equals("END")) break;
System.out.println("Echoing: " + str);
out.println(str);
}
System.out.println("closing...");
} catch(IOException e) {
System.err.println("IO Exception");
} finally {
try {
socket.close();
} catch(IOException e) {
System.err.println("Socket not closed");
}
}
}
}
public class MultiJabberServer {
static final int PORT = 8080;
public static void main(String[] args)
throws IOException {
ServerSocket s = new ServerSocket(PORT);
System.out.println("Server Started");
try {
while(true) {
// Blocks until a connection occurs:
Socket socket = s.accept();
try {
new ServeOneJabber(socket);
} catch(IOException e) {
// If it fails, close the socket,
// otherwise the thread will close it:
socket.close();
}
}
} finally {
s.close();
}
}
} ///:~
ServeOneJabber 线程每次在新客户端建立连接时,接收 main() 中 accept() 生成的 Socket 对象。然后,像之前一样,使用该 Socket 创建 BufferedReader 和自动刷新的 PrintWriter 对象。最后,调用特殊的 Thread 方法 start(),该方法会进行线程初始化并调用 run() 方法。其操作与之前的示例类似:从套接字读取内容并回显,直到读取到特殊的 “END” 信号。
清理套接字的责任需要仔细设计。在这种情况下,套接字在 ServeOneJabber 外部创建,因此责任可以共享。如果 ServeOneJabber 构造函数失败,它会将异常抛给调用者,由调用者清理线程。但如果构造函数成功,ServeOneJabber 对象将在其 run() 方法中承担清理线程的责任。
MultiJabberServer 的实现很简单。和之前一样,创建一个 ServerSocket 并调用 accept() 允许新连接。但这次,accept() 的返回值(一个 Socket)被传递给 ServeOneJabber 的构造函数,该构造函数创建一个新线程来处理该连接。当连接终止时,线程会自动结束。
如果 ServerSocket 创建失败,异常会通过 main() 抛出。但如果创建成功,外部的 try-finally 块会确保其清理工作。内部的 try-catch 块仅用于防范 ServeOneJabber 构造函数失败的情况;如果构造函数成功,ServeOneJabber 线程将关闭关联的套接字。
为了测试服务器是否真的能处理多个客户端,以下程序使用线程创建多个客户端连接到同一服务器。允许的最大线程数由 final int MAX_THREADS 决定。
//: c15:MultiJabberClient.java
// Client that tests the MultiJabberServer
// by starting up multiple clients.
import java.net.*;
import java.io.*;
class JabberClientThread extends Thread {
private Socket socket;
private BufferedReader in;
private PrintWriter out;
private static int counter = 0;
private int id = counter++;
private static int threadcount = 0;
public static int threadCount() {
return threadcount;
}
public JabberClientThread(InetAddress addr) {
System.out.println("Making client " + id);
threadcount++;
try {
socket =
new Socket(addr, MultiJabberServer.PORT);
} catch(IOException e) {
System.err.println("Socket failed");
// If the creation of the socket fails,
// nothing needs to be cleaned up.
}
try {
in =
new BufferedReader(
new InputStreamReader(
socket.getInputStream()));
// Enable auto-flush:
out =
new PrintWriter(
new BufferedWriter(
new OutputStreamWriter(
socket.getOutputStream())), true);
start();
} catch(IOException e) {
// The socket should be closed on any
// failures other than the socket
// constructor:
try {
socket.close();
} catch(IOException e2) {
System.err.println("Socket not closed");
}
}
// Otherwise the socket will be closed by
// the run() method of the thread.
}
public void run() {
try {
for(int i = 0; i < 25; i++) {
out.println("Client " + id + ": " + i);
String str = in.readLine();
System.out.println(str);
}
out.println("END");
} catch(IOException e) {
System.err.println("IO Exception");
} finally {
// Always close it:
try {
socket.close();
} catch(IOException e) {
System.err.println("Socket not closed");
}
threadcount--; // Ending this thread
}
}
}
public class MultiJabberClient {
static final int MAX_THREADS = 40;
public static void main(String[] args)
throws IOException, InterruptedException {
InetAddress addr =
InetAddress.getByName(null);
while(true) {
if(JabberClientThread.threadCount()
< MAX_THREADS)
new JabberClientThread(addr);
Thread.currentThread().sleep(100);
}
}
} ///:~
JabberClientThread 构造函数接受一个 InetAddress 并使用它打开一个 Socket。可以发现一个规律:Socket 总是用于创建某种 Reader 和/或 Writer(或 InputStream 和/或 OutputStream)对象,这是使用 Socket 的唯一方式。start() 方法进行线程初始化并调用 run() 方法。在这里,消息被发送到服务器,服务器的信息被回显到屏幕。线程有有限的生命周期,最终会完成任务。注意,如果构造函数在创建 Socket 后但在完成构造之前失败,会清理 Socket。否则,调用 close() 关闭 Socket 的责任将交给 run() 方法。
threadcount 用于跟踪当前存在的 JabberClientThread 对象的数量。在构造函数中递增,在 run() 方法退出时递减(表示线程终止)。在 MultiJabberClient.main() 中,可以看到会检查线程数量,如果线程过多则不再创建新线程,然后线程会休眠。这样,一些线程最终会终止,从而可以创建更多线程。可以通过调整 MAX_THREADS 来测试系统在处理过多连接时的性能。
3. 传输协议对比
目前看到的示例使用的是传输控制协议(TCP,也称为基于流的套接字),该协议设计用于实现极致的可靠性,保证数据能够到达目的地。它允许重传丢失的数据,在某个路由器出现故障时提供多条不同的路径,并且字节按发送顺序传递。但所有这些控制和可靠性是有代价的:TCP 的开销较高。
另一种协议是用户数据报协议(UDP),它不保证数据包一定会被传递,也不保证它们会按发送顺序到达。它被称为“不可靠协议”(TCP 是“可靠协议”),听起来不太好,但由于它速度更快,因此也有其用途。例如,在音频信号传输等应用中,偶尔丢失几个数据包并不关键,但速度至关重要。或者对于时间服务器来说,丢失一条消息也不会有太大影响。此外,一些应用程序可能会向服务器发送 UDP 消息,如果在合理时间内没有收到响应,就可以假设消息丢失了。
通常,大部分直接网络编程会使用 TCP,只有偶尔会使用 UDP。
4. 小总结
| 协议 | 特点 | 适用场景 |
|---|---|---|
| TCP | 可靠性高,开销大 | 对数据准确性要求高的场景 |
| UDP | 速度快,不可靠 | 对速度要求高,允许少量数据丢失的场景 |
下面是一个简单的 mermaid 流程图,展示 MultiJabberServer 的工作流程:
graph TD;
A[创建 ServerSocket] --> B[调用 accept() 等待连接];
B --> C{有新连接?};
C -- 是 --> D[创建 ServeOneJabber 线程处理连接];
D --> B;
C -- 否 --> B;
以上内容详细介绍了分布式计算中套接字连接、多客户端服务实现以及不同传输协议的特点和应用,希望能帮助大家更好地理解相关技术。
分布式计算技术详解
5. 小程序中的 URL 使用
小程序可以通过其运行所在的 Web 浏览器显示任何 URL。可以使用以下代码实现:
getAppletContext().showDocument(u);
其中,
u
是 URL 对象。以下是一个简单的示例,它会将页面重定向到另一个 Web 页面。虽然这里只是重定向到一个 HTML 页面,但也可以重定向到 CGI 程序的输出。
//: c15:ShowHTML.java
// <applet code=ShowHTML width=100 height=50>
// </applet>
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.net.*;
import java.io.*;
import com.bruceeckel.swing.*;
public class ShowHTML extends JApplet {
JButton send = new JButton("Go");
JLabel l = new JLabel();
public void init() {
Container cp = getContentPane();
cp.setLayout(new FlowLayout());
send.addActionListener(new Al());
cp.add(send);
cp.add(l);
}
class Al implements ActionListener {
public void actionPerformed(ActionEvent ae) {
try {
// This could be a CGI program instead of
// an HTML page.
URL u = new URL(getDocumentBase(),
"FetcherFrame.html");
// Display the output of the URL using
// the Web browser, as an ordinary page:
getAppletContext().showDocument(u);
} catch(Exception e) {
l.setText(e.toString());
}
}
}
public static void main(String[] args) {
Console.run(new ShowHTML(), 100, 50);
}
} ///:~
URL 类的优点在于它能屏蔽很多底层细节。无需深入了解底层机制,就可以连接到 Web 服务器。
6. 从服务器读取文件
以下程序是上述示例的一个变体,它可以读取服务器上的文件,文件名称由客户端指定。
//: c15:Fetcher.java
// <applet code=Fetcher width=500 height=300>
// </applet>
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.net.*;
import java.io.*;
import com.bruceeckel.swing.*;
public class Fetcher extends JApplet {
JButton fetchIt= new JButton("Fetch the Data");
JTextField f =
new JTextField("Fetcher.java", 20);
JTextArea t = new JTextArea(10,40);
public void init() {
Container cp = getContentPane();
cp.setLayout(new FlowLayout());
fetchIt.addActionListener(new FetchL());
cp.add(new JScrollPane(t));
cp.add(f); cp.add(fetchIt);
}
public class FetchL implements ActionListener {
public void actionPerformed(ActionEvent e) {
try {
URL url = new URL(getDocumentBase(),
f.getText());
t.setText(url + "\n");
InputStream is = url.openStream();
BufferedReader in = new BufferedReader(
new InputStreamReader(is));
String line;
while ((line = in.readLine()) != null)
t.append(line + "\n");
} catch(Exception ex) {
t.append(ex.toString());
}
}
}
public static void main(String[] args) {
Console.run(new Fetcher(), 500, 300);
}
} ///:~
URL 对象的创建与上一个示例类似,
getDocumentBase()
仍然是起始点,但这次文件名称是从 JTextField 中读取的。创建 URL 对象后,将其字符串形式放入 JTextArea 中以便查看。然后从 URL 获取 InputStream,在本例中,它将生成文件中的字符流。将其转换为 Reader 并进行缓冲后,逐行读取并追加到 JTextArea 中。注意,JTextArea 被放在 JScrollPane 中,以便自动处理滚动。
7. 数据库连接 - JDBC
据估计,一半的软件开发涉及客户端 - 服务器操作。Java 的一个重要优势是能够构建平台无关的客户端 - 服务器数据库应用程序,这通过 Java 数据库连接(JDBC)得以实现。
数据库的一个主要问题是数据库公司之间的功能竞争。虽然有一个“标准”的数据库语言,即结构化查询语言(SQL - 92),但通常仍需要知道所使用的数据库供应商。JDBC 设计为平台无关,因此在编程时无需担心所使用的数据库。不过,仍然可以从 JDBC 进行特定于供应商的调用,不会受到限制。
在创建新的数据库表并为每列定义 SQL 类型时,程序员可能需要在 SQL 表创建语句中使用 SQL 类型名称。不幸的是,不同数据库产品支持的 SQL 类型存在显著差异。支持具有相同语义和结构的 SQL 类型的不同数据库可能会给这些类型赋予不同的名称。大多数主要数据库都支持用于大二进制值的 SQL 数据类型:在 Oracle 中,该类型称为 LONG RAW,Sybase 称为 IMAGE,Informix 称为 BYTE,DB2 称为 LONG VARCHAR FOR BIT DATA。因此,如果目标是实现数据库可移植性,应尽量只使用通用的 SQL 类型标识符。
8. JDBC 操作步骤
JDBC 与 Java 中的许多 API 一样,设计得很简单。进行数据库操作时的方法调用对应于从数据库收集数据时的逻辑操作:连接到数据库、创建语句并执行查询,然后查看结果集。
为了实现平台无关性,JDBC 提供了一个驱动管理器,它动态维护数据库查询所需的所有驱动对象。如果要连接三种不同的供应商数据库,就需要三个不同的驱动对象。驱动对象在加载时会向驱动管理器注册,可以使用
Class.forName()
强制加载。
要打开数据库,必须创建一个“数据库 URL”,它需要指定以下信息:
1. 使用 JDBC,以 “jdbc.” 开头。
2. “子协议”:驱动程序的名称或数据库连接机制的名称。由于 JDBC 的设计受到 ODBC 的启发,第一个可用的子协议是 “jdbc - odbc 桥”,用 “odbc” 指定。
3. 数据库标识符。这取决于所使用的数据库驱动程序,但通常提供一个逻辑名称,该名称由数据库管理软件映射到数据库表所在的物理目录。为了使数据库标识符有意义,必须使用数据库管理软件注册该名称(注册过程因平台而异)。
所有这些信息组合成一个字符串,即“数据库 URL”。例如,要通过 ODBC 子协议连接到标识为 “people” 的数据库,数据库 URL 可以是:
String dbUrl = "jdbc:odbc:people";
如果是跨网络连接,数据库 URL 会包含标识远程机器的连接信息,可能会比较复杂。例如,通过 RMI 从远程客户端调用 CloudScape 数据库的示例:
jdbc:rmi://192.168.170.27:1099/jdbc:cloudscape:db
这个数据库 URL 实际上是两个 jdbc 调用的组合。第一部分 “jdbc:rmi://192.168.170.27:1099/” 使用 RMI 连接到 IP 地址为 192.168.170.27、端口为 1099 的远程数据库引擎。URL 的第二部分 “jdbc:cloudscape:db” 使用子协议和数据库名称传达更典型的设置,但这只有在第一部分通过 RMI 连接到远程机器后才会发生。
准备好连接数据库时,调用静态方法
DriverManager.getConnection()
,并传入数据库 URL、用户名和密码以进入数据库。会返回一个 Connection 对象,然后可以使用该对象查询和操作数据库。
以下是一个示例,它打开一个联系人信息数据库,并查找命令行中指定姓氏的人员。它只选择有电子邮件地址的人员的姓名,然后打印出所有匹配指定姓氏的人员信息:
//: c15:jdbc:Lookup.java
// Looks up email addresses in a
// local database using JDBC.
import java.sql.*;
public class Lookup {
public static void main(String[] args)
throws SQLException, ClassNotFoundException {
String dbUrl = "jdbc:odbc:people";
String user = "";
String password = "";
// Load the driver (registers itself)
Class.forName(
"sun.jdbc.odbc.JdbcOdbcDriver");
Connection c = DriverManager.getConnection(
dbUrl, user, password);
Statement s = c.createStatement();
// SQL code:
ResultSet r =
s.executeQuery(
"SELECT FIRST, LAST, EMAIL " +
"FROM people.csv people " +
"WHERE " +
"(LAST='" + args[0] + "') " +
" AND (EMAIL Is Not Null) " +
"ORDER BY FIRST");
while(r.next()) {
// Capitalization doesn't matter:
System.out.println(
r.getString("Last") + ", "
+ r.getString("fIRST")
+ ": " + r.getString("EMAIL") );
}
s.close(); // Also closes ResultSet
}
} ///:~
可以看到,数据库 URL 的创建如前面所述。在这个示例中,数据库没有密码保护,因此用户名和密码是空字符串。
使用
DriverManager.getConnection()
建立连接后,可以使用得到的 Connection 对象通过
createStatement()
方法创建一个 Statement 对象。使用得到的 Statement,可以调用
executeQuery()
方法,传入包含 SQL - 92 标准 SQL 语句的字符串。
executeQuery()
方法返回一个 ResultSet 对象,它是一个迭代器:
next()
方法将迭代器移动到语句中的下一条记录,如果到达结果集的末尾则返回 false。即使查询结果为空集(即不会抛出异常),
executeQuery()
也总会返回一个 ResultSet 对象。注意,在尝试读取任何记录数据之前,必须先调用一次
next()
。如果结果集为空,第一次调用
next()
将返回 false。对于结果集中的每条记录,可以使用字段名称作为字符串(等方法)选择字段。此外,字段名称的大小写不影响查询结果,因为在 SQL 数据库中大小写不敏感。可以通过调用
getInt()
、
getString()
、
getFloat()
等方法确定返回的数据类型。此时,就可以在 Java 中获取数据库数据了。
9. 总结
本文全面介绍了分布式计算中的多个重要方面。从网络连接的套接字和不同传输协议(TCP 和 UDP),到小程序中 URL 的使用和文件读取,再到数据库连接的 JDBC 技术。以下是一个简单的表格总结不同技术的关键信息:
|技术|关键特点|使用场景|
| ---- | ---- | ---- |
|套接字连接|建立专用连接,可处理多客户端|客户端 - 服务器通信|
|TCP 协议|可靠性高,开销大|对数据准确性要求高的场景|
|UDP 协议|速度快,不可靠|对速度要求高,允许少量数据丢失的场景|
|JDBC|平台无关的数据库连接|构建客户端 - 服务器数据库应用程序|
下面是一个 mermaid 流程图,展示 JDBC 的基本操作流程:
graph TD;
A[加载驱动] --> B[创建数据库 URL];
B --> C[获取连接对象];
C --> D[创建 Statement 对象];
D --> E[执行查询];
E --> F[处理结果集];
F --> G[关闭资源];
通过对这些技术的学习和应用,可以更好地开发分布式系统和处理数据库操作,提高软件的性能和可维护性。
超级会员免费看
84

被折叠的 条评论
为什么被折叠?



