一个多线程的示例
1. 介绍
前面的示例教给您基础知识,但并不能令您更深入。如果您到此就停止了,那么您一次只能处理一台客户机。原因是 handleConnection()
是一个阻塞方法。只有当它完成了对当前连接的处理时,服务器才能接受另一个客户机。在多数时候,您将需要(也有必要)一个多线程服务器。
要开始同时处理多台客户机,并不需要对 RemoteFileServer
作太多改变。事实上,要是我们前面讨论过待发(backlog),那我们就只需改变一个方法,虽然我们将需要创建一些新东西来处理进入的连接。这里我们还将向您展示ServerSocket
如何处理众多等待(备份)使用服务器的客户机。本示例对线程的低效使用,所以请耐心点。
2. 接受(太多)连接
这里我们实现改动过的 acceptConnections()
方法,它将创建一个能够处理待发请求的 ServerSocket
,并告诉ServerSocket
接受连接:
public void acceptConnections() {
try {
ServerSocket server = new ServerSocket(listenPort, 5);
Socket incomingConnection = null;
while (true) {
incomingConnection = server.accept();
handleConnection(incomingConnection);
}
} catch (BindException e) {
System.out.println("Unable to bind to port " + listenPort);
} catch (IOException e) {
System.out.println("Unable to instantiate a ServerSocket on port: " + listenPort);
}
}
新的 server 仍然需要 acceptConnections()
,所以这些代码实际上是一样的。突出显示的行表示一个重大的不同。对这个多线程版,我们现在可以指定客户机请求的最大数目,这些请求都能在实例化ServerSocket
期间处于待发状态。如果我们没有指定客户机请求的最大数目,则我们假设使用缺省值 50。
这里是它的工作机制。假设我们指定待发数(backlog 值)是 5 并且有五台客户机请求连接到我们的服务器。我们的服务器将着手处理第一个连接,但处理该连接需要很长时间。由于我们的待发值是 5,所以我们一次可以放五个请求到队列中。我们正在处理一个,所以这意味着还有其它五个正在等待。等待的和正在处理的一共有六个。当我们的服务器仍忙于接受一号连接(记住队列中还有 2―6 号)时,如果有第七个客户机提出连接申请,那么,该第七个客户机将遭到拒绝。我们将在带有连接池服务器示例中说明如何限定能同时连接的客户机数目。
3. 处理连接:第 1 部分
这里我们将讨论 handleConnection()
方法的结构,这个方法生成一个新的 Thread
来处理每个连接。我们将分两部分讨论这个问题。这一屏我们将着重该方法本身,然后在下一屏研究该方法所使用的ConnectionHandler
助手类的结构。
public void handleConnection(Socket connectionToHandle) {
new Thread(new ConnectionHandler(connectionToHandle)).start();
}
我们对 RemoteFileServer
所做的大改动就体现在这个方法上。我们仍然在服务器接受一个连接之后调用handleConnection()
,但现在我们把该Socket
传递给 ConnectionHandler
的一个实例,它是Runnable
的。我们用ConnectionHandler
创建一个新Thread
并启动它。ConnectionHandler
的run()
方法包含Socket
读/写和读File
的代码,这些代码原来在RemoteFileServer
的handleConnection()
中。
4. 处理连接:第 2 部分
这里是 ConnectionHandler
类的结构:
import java.io.*;
import java.net.*;
public class ConnectionHandler implements Runnable{
Socket socketToHandle;
public ConnectionHandler(Socket aSocketToHandle) {
socketToHandle = aSocketToHandle;
}
public void run() {
}
}
这个助手类相当简单。跟我们到目前为止的其它类一样,我们导入 java.net
和java.io
。该类只有一个实例变量socketToHandle
,它保存由该实例处理的Socket
。
类的构造器用一个 Socket 实例作参数并将它赋给 socketToHandle
。
请注意该类实现了 Runnable
接口。实现这个接口的类都必须实现run()
方法,我们的类就是这样做的。稍后我们将探究run()
的细节。现在只需知道它将实际处理连接,所用的代码跟我们先前在RemoteFileServer
类中看到的是一样的。
5. 实现 run()
这里我们实现 run()
方法,它将攫取我们的连接的流,用它来读写该连接,并在任务完成之后关闭它:
public void run() {
try {
PrintWriter streamWriter = new PrintWriter(socketToHandle.getOutputStream());
BufferedReader streamReader =
new BufferedReader(new InputStreamReader(socketToHandle.getInputStream()));
String fileToRead = streamReader.readLine();
BufferedReader fileReader = new BufferedReader(new FileReader(fileToRead));
String line = null;
while ((line = fileReader.readLine()) != null)
streamWriter.println(line);
fileReader.close();
streamWriter.close();
streamReader.close();
} catch (Exception e) {
System.out.println("Error handling a client: " + e);
}
}
ConnectionHandler
的run()
方法所做的事情就是RemoteFileServer
上的handleConnection()
所做的事情。首先,我们把InputStream
和OutputStream
分别包装(用Socket
的 getOutputStream()
和getInputStream()
)进BufferedReader
和PrintWriter
。然后我们用这些代码逐行地读目标文件:
FileReader fileReader = new FileReader(new File(streamReader.readLine()));
BufferedReader bufferedFileReader = new BufferedReader(fileReader);
String line = null;
while ((line = bufferedFileReader.readLine()) != null) {
streamWriter.println(line);
}
请记住我们应该从客户机获取一条有效的文件路径,这样用该路径名构造一个新 File
,把它包装进FileReader
以处理读文件的操作,然后把它包装进BufferedReader
以让我们逐行地读该文件。我们在while
循环中调用BufferedReader
上的readLine()
直到不再有要读的行。请记注,对readLine()
的调用将造成阻塞,直到有字节来到为止。我们获取一些字节之后就把它们放到本地的line
变量中,然后写出到客户机上。完成读写操作之后,我们关闭打开的流。
6. 总结一下多线程服务器
我们的多线程服务器研究完了。在我们接着讨论带有连接池示例之前,让我们回顾一下创建和使用“多线程版”的服务器的步骤:
- 修改
acceptConnections()
以用缺省为 50(或任何您想要的大于 1 的指定数字)实例化ServerSocket
。
- 修改
ServerSocket
的handleConnection()
以用ConnectionHandler
的一个实例生成一个新的Thread
。
- 借用
RemoteFileServer
的handleConnection()
方法的代码实现ConnectionHandler
类。
附: MultithreadedRemoteFileServer
的完整代码清单
MultithreadedRemoteFileServer 的代码清单 第 4 页(共7 页)
import java.io.*;
import java.net.*;
public class MultithreadedRemoteFileServer {
protected int listenPort;
public MultithreadedRemoteFileServer(int aListenPort) {
listenPort = aListenPort;
}
public void acceptConnections() {
try {
ServerSocket server = new ServerSocket(listenPort, 5);
Socket incomingConnection = null;
while (true) {
incomingConnection = server.accept();
handleConnection(incomingConnection);
}
} catch (BindException e) {
System.out.println("Unable to bind to port " + listenPort);
} catch (IOException e) {
System.out.println("Unable to instantiate a ServerSocket on port: " + listenPort);
}
}
public void handleConnection(Socket connectionToHandle) {
new Thread(new ConnectionHandler(connectionToHandle)).start();
}
public static void main(String[] args) {
MultithreadedRemoteFileServer server = new MultithreadedRemoteFileServer(3000);
server.acceptConnections();
}
}
ConnectionHandler 的完整代码清单
import java.io.*;
import java.net.*;
public class ConnectionHandler implements Runnable {
protected Socket socketToHandle;
public ConnectionHandler(Socket aSocketToHandle) {
socketToHandle = aSocketToHandle;
}
public void run() {
try {
PrintWriter streamWriter = new PrintWriter(socketToHandle.getOutputStream());
BufferedReader streamReader = new BufferedReader(new InputStreamReader(socketToHandle.getInputStream()));
String fileToRead = streamReader.readLine();
BufferedReader fileReader = new BufferedReader(new FileReader(fileToRead));
String line = null;
while ((line = fileReader.readLine()) != null)
streamWriter.println(line);
fileReader.close();
streamWriter.close();
streamReader.close();
} catch (Exception e) {
System.out.println("Error handling a client: " + e);
}
}
}