技术开发 频道

用Java实现多线程服务器程序

  【IT168 技术文章】Java是伴随Internet的大潮产生的,对网络及多线程具有内在的支持,具有网络时代编程语言的一切特点。从Java的当前应用看,Java主要用于在Internet或局域网上的网络编程,而且将Java作为主流的网络编程语言的趋势愈来愈明显。实际工作中,我们除了使用商品化的服务器软件外,时常需要按照实际环境编写自己的服务器软件,以完成特定任务或与特定客户端软件实现交互。在实现服务器程序时,为提高程序运行效率,降低用户等待时间,我们应用了在Java Applet中常见的多线程技术。

  一、Java中的服务器程序与多线程

  在Java之前,没有一种主流编程语言能够提供对高级网络编程的固有支持。在其他语言环境中,实现网络程序往往需要深入依赖于操作平台的网络API的技术中去,而Java提供了对网络支持的无平台相关性的完整软件包,使程序员没有必要为系统网络支持的细节而烦恼。

  Java软件包内在支持的网络协议为TCP/IP,也是当今最流行的广域网/局域网协议。Java有关网络的类及接口定义在java.net包中。客户端软件通常使用java.net包中的核心类Socket与服务器的某个端口建立连接,而服务器程序不同于客户机,它需要初始化一个端口进行监听,遇到连接呼叫,才与相应的客户机建立连接。Java.net包的ServerSocket类包含了编写服务器系统所需的一切。下面给出ServerSocket类的部分定义。

1 public class ServerSocket
2 {
3  public ServerSocket(int port)
4  throws IOException ;
5  public Socket accept() throws IOException ;
6  public InetAddress getInetAddress() ;
7  public int getLocalPort() ;
8  public void close() throws IOException ;
9  public synchronized void setSoTimeout (int timeout) throws SocketException ;
10  public synchronized int getSoTimeout() throws IOException ;
11 }
12

  ServerSocket构造器是服务器程序运行的基础,它将参数port指定的端口初始化作为该服务器的端口,监听客户机连接请求。Port的范围是0到65536,但0到1023是标准Internet协议保留端口,而且在Unix主机上,这些端口只有root用户可以使用。一般自定义的端口号在8000到16000之间。仅初始化了ServerSocket还是远远不够的,它没有同客户机交互的套接字(Socket),因此需要调用该类的accept方法接受客户呼叫。Accept()方法直到有连接请求才返回通信套接字(Socket)的实例。通过这个实例的输入、输出流,服务器可以接收用户指令,并将相应结果回应客户机。ServerSocket类的getInetAddress和getLocalPort方法可得到该服务器的IP地址和端口。setSoTimeout和getSoTimeout方法分别是设置和得到服务器超时设置,如果服务器在timout设定时间内还未得到accept方法返回的套接字实例,则抛出IOException的异常。

  Java的多线程可谓是Java编程的精华之一,运用得当可以极大地改善程序的响应时间,提高程序的并行性。在服务器程序中,由于往往要接收不同客户机的同时请求或命令,因此可以对每个客户机的请求生成一个命令处理线程,同时对各用户的指令作出反应。在一些较复杂的系统中,我们还可以为每个数据库查询指令生成单独的线程,并行对数据库进行操作。实践证明,采用多线程设计可以很好的改善系统的响应,并保证用户指令执行的独立性。由于Java本身是"线程安全"的,因此有一条编程原则是能够独立在一个线程中完成的操作就应该开辟一个新的线程。

  Java中实现线程的方式有两种,一是生成Thread类的子类,并定义该子类自己的run方法,线程的操作在方法run中实现。但我们定义的类一般是其他类的子类,而Java又不允许多重继承,因此第二种实现线程的方法是实现Runnable接口。通过覆盖Runnable接口中的run方法实现该线程的功能。本文例子采用第一种方法实现线程。

  二、多线程服务器程序举例

  以下是我们在项目中采用的多线程服务器程序的架构,可以在此基础上对命令进行扩充。本例未涉及数据库。如果在线程运行中需要根据用户指令对数据库进行更新操作,则应注意线程间的同步问题,使同一更新方法一次只能由一个线程调用。这里我们有两个类,receiveServer包含启动代码(main()),并初始化ServerSocket的实例,在accept方法返回用户请求后,将返回的套接字(Socket)交给生成的线程类serverThread的实例,直到该用户结束连接。

  1 //类receiveServer
  2 import java.io.*;
  3 import java.util.*;
  4 import java.net.*;
  5
  6 public class receiveServer{
  7  final int RECEIVE_PORT=9090; //该服务器的端口号
  8  //receiveServer的构造器public receiveServer() {ServerSocket rServer=null;
  9  //ServerSocket的实例
10  Socket request=null;
11  //用户请求的套接字Thread receiveThread=null;
12  try{
13   rServer=new ServerSocket(RECEIVE_PORT);
14   //初始化ServerSocket System.out.println("Welcome to the server!");
15   System.out.println(new Date());
16   System.out.println("The server is ready!");
17   System.out.println("Port: "+RECEIVE_PORT);
18   while(true){ //等待用户请求 request=rServer.accept(); //接收客户机连接请求receiveThread=new serverThread(request);
19
20   //生成serverThread的实例
21   receiveThread.start();
22
23   //启动serverThread线程
24  }
25 }
26 catch(IOException e){
27  System.out.println(e.getMessage()) ;
28 }
29 } public static void main(String args[]){ new receiveServer();
30
31 } //end of main} //end of class//类serverThreadimport java.io.*;
32
33 import java.net.*;
34 class serverThread extends Thread {Socket clientRequest;
35 //用户连接的通信套接字BufferedReader input;
36 //输入流PrintWriter output;
37 //输出流
38 public serverThread(Socket s) {
39  //serverThread的构造器 this.clientRequest=s;
40  //接收receiveServer传来的套接字 InputStreamReader reader;
41
42  OutputStreamWriter writer;
43  try{
44   //初始化输入、输出流
45   reader=new InputStreamReader(clientRequest.getInputStream());
46   writer=new OutputStreamWriter(clientRequest.getOutputStream());
47   input=new BufferedReader(reader);
48   output=new PrintWriter(writer,true);
49  }
50  catch(IOException e){ System.out.println(e.getMessage());}
51  output.println("Welcome to the server!");
52  //客户机连接欢迎词
53  output.println("Now is: "+new java.util.Date()+" "+ "Port:"+clientRequest.getLocalPort());
54  output.println("What can I do for you?");
55 }
56
57 public void run(){
58  //线程的执行方法
59  String command=null;
60  //用户指令 String str=null;
61  boolean done=false;
62  while(!done){
63   try{
64    str=input.readLine();
65    //接收客户机指令
66   }catch(IOException e){
67    System.out.println(e.getMessage());
68  }
69  command=str.trim().toUpperCase();
70
71  if(str==null || command.equals("QUIT")) //命令quit结束本次连接
72   done=true;
73  else if(command.equals("HELP")){
74   //命令help查询本服务器可接受的命令
75   output.println("query");
76   output.println("quit");
77   output.println("help");
78  }
79  else if(command.startsWith("QUERY")){
80   //命令
81   query output.println("OK to query something!");
82  }//else if …….. //在此可加入服务器的其他指令
83  else if(!command.startsWith("HELP") && !command.startsWith("QUIT") && !command.startsWith("QUERY")){output.println("Command not Found!
84   Please refer to the HELP!"); }
85 }
86
87 //end of while
88
89 try
90 {
91  clientRequest.close();
92  //关闭套接字
93 }
94 catch(IOException e){
95  System.out.println(e.getMessage());
96 }
97 command=null;
98 }
99
100 //end of run

  启动该服务器程序后,可用telnet machine port命令连接,其中machine为本机名或地址,port为程序中指定的端口。也可以编写特定的客户机软件通过TCP的Socket套接字建立连接。

  对象池的构造和管理可以按照多种方式实现。最灵活的方式是将池化对象的Class类型在对象池之外指定,即在ObjectPoolFactory类创建对象池时,动态指定该对象池所池化对象的Class类型,其实现代码如下:

1 . . .
2
3   public ObjectPool createPool(ParameterObject paraObj,Class clsType) {
4
5   return new ObjectPool(paraObj, clsType);
6
7   }
8
9   . . .
10
11

  其中,paraObj参数用于指定对象池的特征属性,clsType参数则指定了该对象池所存放对象的类型。对象池(ObjectPool)创建以后,下面就是利用它来管理对象了,具体实现如下:

1 public class ObjectPool {
2  private ParameterObject paraObj;//该对象池的属性参数对象
3  private Class clsType;//该对象池中所存放对象的类型
4  private int currentNum = 0; //该对象池当前已创建的对象数目
5  private Object currentObj;//该对象池当前可以借出的对象
6  private Vector pool;//用于存放对象的池
7  public ObjectPool(ParameterObject paraObj, Class clsType) {
8   this.paraObj = paraObj;
9   this.clsType = clsType;
10   pool = new Vector();
11  }
12  public Object getObject() {
13   if (pool.size() <= paraObj.getMinCount()) {
14    if (currentNum <= paraObj.getMaxCount()) {
15     //如果当前池中无对象可用,而且已创建的对象数目小于所限制的最大值,就利用
16     //PoolObjectFactory创建一个新的对象
17     PoolableObjectFactory objFactory =PoolableObjectFactory.getInstance();
18     currentObj = objFactory.create Object (clsType);
19     currentNum++;
20    } else {
21     //如果当前池中无对象可用,而且所创建的对象数目已达到所限制的最大值,
22     //就只能等待其它线程返回对象到池中
23     synchronized (this) {
24      try {
25       wait();
26      } catch (InterruptedException e) {
27       System.out.println(e.getMessage());
28       e.printStackTrace();
29      }
30      currentObj = pool.firstElement();
31     }
32    }
33   } else {
34    //如果当前池中有可用的对象,就直接从池中取出对象
35    currentObj = pool.firstElement();
36   }
37   return currentObj;
38 }
39   public void returnObject(Object obj) {
40    // 确保对象具有正确的类型
41    if (obj.isInstance(clsType)) {
42     pool.addElement(obj);
43     synchronized (this) {
44      notifyAll();
45     }
46    } else {
47     throw new IllegalArgumentException("该对象池不能存放指定的对象类型");
48    }
49   }
50 }

  从上述代码可以看出,ObjectPool利用一个java.util.Vector作为可扩展的对象池,并通过它的构造函数来指定池化对象的Class类型及对象池的一些属性。在有对象返回到对象池时,它将检查对象的类型是否正确。当对象池里不再有可用对象时,它或者等待已被使用的池化对象返回池中,或者创建一个新的对象实例。不过,新对象实例的创建并不在ObjectPool类中,而是由PoolableObjectFactory类的createObject方法来完成的,具体实现如下:

1 . . .
2 public Object createObject(Class clsType) {
3  Object obj = null;
4  try {
5   obj = clsType.newInstance();
6  } catch (Exception e) {
7   e.printStackTrace();
8  }
9  return obj;
10 }
11 . . .

   这样,通用对象池的实现就算完成了,下面再看看客户端(Client)如何来使用它,假定池化对象的Class类型为StringBuffer:

1 . . .
2 //创建对象池工厂
3 ObjectPoolFactory poolFactory = ObjectPoolFactory. getInstance ();
4 //定义所创建对象池的属性
5 ParameterObject paraObj = new ParameterObject(2,1);
6 //利用对象池工厂,创建一个存放StringBuffer类型对象的对象池
7 ObjectPool pool = poolFactory.createPool(paraObj,String Buffer.class);
8 //从池中取出一个StringBuffer对象
9 StringBuffer buffer = (StringBuffer)pool.getObject();
10 //使用从池中取出的StringBuffer对象
11 buffer.append("hello");
12 System.out.println(buffer.toString());
13 . . .

  可以看出,通用对象池使用起来还是很方便的,不仅可以方便地避免频繁创建对象的开销,而且通用程度高。但遗憾的是,由于需要使用大量的类型定型(cast)操作,再加上一些对Vector类的同步操作,使得它在某些情况下对性能的改进非常有限,尤其对那些创建周期比较短的对象。

  由于通用对象池的管理开销比较大,某种程度上抵消了重用对象所带来的大部分优势。为解决该问题,可以采用专用对象池的方法。即对象池所池化对象的Class类型不是动态指定的,而是预先就已指定。这样,它在实现上也会较通用对象池简单些,可以不要ObjectPoolFactory和PoolableObjectFactory类,而将它们的功能直接融合到ObjectPool类,具体如下(假定被池化对象的Class类型仍为StringBuffer,而用省略号表示的地方,表示代码同通用对象池的实现):

1public class ObjectPool {
2 private ParameterObject paraObj;//该对象池的属性参数对象
3 private int currentNum = 0; //该对象池当前已创建的对象数目
4 private StringBuffer currentObj;//该对象池当前可以借出的对象
5 private Vector pool;//用于存放对象的池
6 public ObjectPool(ParameterObject paraObj) {
7  this.paraObj = paraObj;
8  pool = new Vector();
9 }
10 public StringBuffer getObject() {
11  if (pool.size() <= paraObj.getMinCount()) {
12   if (currentNum <= paraObj.getMaxCount()) {
13    currentObj = new StringBuffer();
14    currentNum++;
15   }
16   . . .
17  }
18  return currentObj;
19 }
20 public void returnObject(Object obj) {
21  // 确保对象具有正确的类型
22  if (StringBuffer.isInstance(obj)) {
23   . . .
24  }
25 }

 

  结束语

  恰当地使用对象池技术,能有效地改善应用程序的性能。目前,对象池技术已得到广泛的应用,如对于网络和数据库连接这类重量级的对象,一般都会采用对象池技术。但在使用对象池技术时也要注意如下问题:

  ·并非任何情况下都适合采用对象池技术。基本上,只在重复生成某种对象的操作成为影响性能的关键因素的时候,才适合采用对象池技术。而如果进行池化所能带来的性能提高并不重要的话,还是不采用对象池化技术为佳,以保持代码的简明。

  ·要根据具体情况正确选择对象池的实现方式。如果是创建一个公用的对象池技术实现包,或需要在程序中动态指定所池化对象的Class类型时,才选择通用对象池。而大部分情况下,采用专用对象池就可以了。

0
相关文章