2003年1月21日 星期二 晴
通过程序建立了实际的概念之后,现在应该回到最开始的问题,Socket是什么?是实现计算机通信的一种方式,这毫无疑问.但如何能够用最容易理解的语言比较形象而又不偏颇的描述它的原理呢?
Bruce Eckel 在他的《Java 编程思想》一书中这样描述套接字:
套接字是一种软件抽象,用于表达两台机器之间的连接“终端”。对于一个给定的连接,每台机器上都有一个套接字,您也可以想象它们之间有一条虚拟的“电缆”,“电缆”的每一端都插入到套接字中。当然,机器之间的物理硬件和电缆连接都是完全未知的。抽象的全部目的是使我们无须知道不必知道的细节.
按我的理解,抽象点来说,一个Socket就是一个电话听筒,你有一个,和你通话的人也有一个,只不过其中有一个人的听筒叫ServerSocket,另一个人的听筒叫Socket.至于谁是ServerSocket,谁是Socket,这不重要,因为客户端和服务器端本来就是相对的,可以互相转化的.通话的两个人通过拿起两个听筒建立了一条通道,这条通道通不通就要看是不是双方都拿起听筒了,假如只有一方拿起听筒,那就只能听到一些嘟嘟的声音,证明通道不同.这里,拿起听筒的过程就是Socket初始化的过程.建立了通道之后,也就是大家都拿起听筒之后,通道两端的人就可以开始通话了.这里又有两个过程,即A对B说话,B接听,和B对A说话,A收听,这两个过程是通过两条线路完成的.传输在这两条线路上的,就是流.流隐藏了所有传输的细节,使得通信双方都认为,他们传过去的是声音,而不是编码.
前面写的服务器端的程序实际上是单任务版本,服务器对客户机的处理机制是在同一时间段内只能处理一个连接,因为handleConnection中采取的是不断循环的阻塞方法,检测到一个,就处理一个,然后再检测到一个,就再处理一个,如果有多个连接同时请求,那只能排队等候.这样的程序是无法在网络中应付多个连接的,因为你无法保证在同一时间内只有一个客户提出与服务器的连接请求,而用阻塞的方法应付多客户连接其速度之慢是可想而知的.
这样就催生了面向多连接的版本.显然,通过多线程可以来实现我们的要求.
由于要解决的是处理客户连接的问题,因此我们的工作只是在服务器端的程序当中修改.其原理不难推出,就是在检测到一个连接请求之后,马上建立一个线程去处理它,然后继续兼听下一个连接请求.所以,我们只需要将原来在handleConnection中的代码原封不动的放到线程的执行代码中,而在handleConnection中添加上新建线程的代码就可以了,十分简单.
同上一篇的风格一样,我们来观察各个部分的代码细节.
首先为这个多线程的版本创建类MultiThreadRemoteFileServer
看看这个类的定义
import java.io.*;
import java.NET.*;
public class MultiThreadRemoteFileServer{
protected int listenPort;
public MultiThreadRemoteFileServer(int aListenPort){
}
public static void main(String[] args) {
}
public void acceptConnections() {
}
public void handleConnection(Socket incomingConnection) {
}
}
几乎和RemoteFileServer是一样的,唯一的区别是在我们现在创建的这个类中增加了一个构造函数,这是为了能够使得监听的端口号由我们自己来定.定义如下
public MultithreadedRemoteFileServer(int aListenPort) {
listenPort = aListenPort;
}
先来看main()
public static void main(String[] args) {
MultithreadedRemoteFileServer server = new MultithreadedRemoteFileServer(3000);
server.acceptConnections();
}
没有区别吧,和RemoteFileServer的main()函数,只是端口号在创建的时候由主程序指定而已。
我们主要关心的改动都在后面
现在看acceptConnection监听程序
public void acceptConnections() {
try {
ServerSocket server = new ServerSocket(listenPort, 5);//注意到没有,建立服务器Socket的时候多了一个参数,这个参数是用来指定能够同时申请连接的最大数目,缺省值是50
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);
}
}
改动的地方就一个,多了个参数.这里是它的工作机制。假设我们指定待发数(backlog 值)是5并且有五台客户机请求连接到我们的服务器。我们的服务器将着手处理第一个连接,但处理该连接需要很长时间。由于我们的待发值是 5,所以我们一次可以放五个请求到队列中。我们正在处理一个,所以这意味着还有其它五个正在等待。等待的和正在处理的一共有六个。当我们的服务器仍忙于接受一号连接(记住队列中还有 2—6 号)时,如果有第七个客户机提出连接申请,那么,该第七个客户机将遭到拒绝
接着看,我们的下一个改动显然是在处理监听到的线程的方法handleConnection中,前面已经说了,在多线程的版本中,我们检测到一个连接请求,就马上生成一个线程,然后就不用理它了,那么在这里就是新建线程的一句话.
public void handleConnection(Socket connectionToHandle) {
new Thread(new ConnectionHandler(connectionToHandle)).start();
}
我们注意到有一个新的类ConnectionHandler,这个类是Runnable的,即是一个接口类(这是用接口实现的一个线程,要是有不明白的话,可以去看看17号的关于线程的东西).我们用 ConnectionHandler 创建一个新 Thread 并启动它。正如我们刚才所说的,原来在RemoteFileServer的handleConnection中的代码统统原封不动的转移到了这个接口类ConnectionHandler的run()方法中来了.
那么我们来看看整个ConnectionHandler类的定义吧。
class ConnectionHandler implements Runnable {
protected Socket socketToHandle;
public ConnectionHandler(Socket aSocketToHandle) {
socketToHandle = aSocketToHandle;//通过构造函数,将待处理的Socket实例作为参数传送进来
}
public void run() {//原来对Socket的读/写的代码都在这里了
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。然后我们用这些代码逐行地读目标文件.由于InputStream中装的是文件路径,所以中间还需要使用FileReader流将文件路径包装,再经由BufferedReader包装读出.
我们的多线程服务器研究完了,同样,我们回顾一下创建和使用“多线程版”的服务器的步骤:
1.修改 acceptConnections() 以用缺省为 50(或任何您想要的大于 1 的指定数字)实例化 ServerSocket。
2. 修改 ServerSocket 的 handleConnection() 以用 ConnectionHandler 的一个实例生成一个新的 Thread。
3.借用 RemoteFileServer 的 handleConnection() 方法的代码实现 ConnectionHandler 类的run()函数。