简介: 本文详细的介绍了使用Java语言建立一套多线程服务器的过程,该服务器使用对象传递消息,在线程中使用队列机制,使服务器的性能大大提高了。这套服务器可以被用于各种C/S或B/S结构的应用程序中。

发布日期: 2002 年 7 月 03 日 
级别: 初级 
访问情况 : 7451 次浏览 
评论:  (查看 | 添加评论 -
登录)

 平均分 (17个评分)
为本文评分

Java语言是完全面向对象的,它的线程机制和对象序列化特别容易使用,使用Java来建立一套多线程服务器要比使用其它语言方便的多,如果你再把它的异常处理机制利用好,那么你就可以建立一个商业级的多线程服务器了。由于采用了消息队列和Socket传输方式,所以不会出现丢消息的问题。这套服务器可以作为实时聊天服务器、多人协同的协作服务器等等。

消息系统的建立

这套服务器的消息系统采用的是对象传输的机制,而不是以前常常使用的字符串传输。采用对象传输的好处是扩展方便,如需要建立一个新的消息只需要从一个统一的基类继承下来,然后再写自己实现的方法就行了。这样也符合面向对象领域里一条重要的原则:OCP(open_closed Principle),即一个好的设计应该能够容纳新的功能的增加,但是增加的方式不是修改原有的类,而是添加新的类。

首先建立一个基类:Msg,该抽象类中有两个域sender和receiver分别纪录消息的发送者和接收者。这两个域是在构造消息类时就填写的,receiver域可以为空,空表示发给谁都可以,由转发服务器来决定。该类的方法包括取得这两个域的值和消息的处理函数。消息的处理函数process()是空函数,供继承者重载。

建立了这个抽象基类后,你就可以继承它完成你自己的类。举个例子,假如我要建立一个分组协同工作的绘图系统,而且支持组员之间的对话,那么我可以建立如下的类集合:

SendTextMsg(String sender,String receiver,String info)//向指定的人发送对话。 

AddLineMsg(String sender,Point a,Point b)//在指定的点之间绘制一条直线 

AddRectangle(String sender,point start,Point end)//建立指定的矩形 

AddRotundaMsg(String sender,Point center,int radius)//建立指定的圆 

RemoveObjectMsg(String sender,int ID)//删除指定编号的图形对象 

……

以此类推,可以建立很多的消息类。在每个类的内部都由一个处理该类的方法process(),填写该方法就可以实现对消息类的处理,而服务器只负责完成消息的转发功能。这样,一套消息系统就建立了。

回页首

服务器的结构

如果要服务器实现同时为每个客户端服务,就要使用多线程,建立一个线程池,当有客户端连接时就在池中开辟一个线程为它服务。同样,要避免大量消息到达时处理不过来而导致丢失的情况,就要使用消息队列。这个服务器是分层的处理的。

类关系图如下所示:

 

服务器的工作过程是这样的,建立了一个Server类作为主类,它含有程序的入口函数main()。在构造函数中初始化一个数组存放ClientSingle类,它其实就是单独处理一个连接用户的类。然后启动一个线程PORTListenThread,该线程的作用就是监听端口上有没有人登陆,当有人连接时交给Server的addClient()处理。Server的addClient()方法会在刚才那个数组中建立一个ClientSingle对象,然后把剩下的事都交给它做。

回页首

端口监听线程类PORTListenThread

该线程类在run()函数的开始部分首先要检查serverScoket是否为空,保证循环开始时不要出错。然后进入一个死循环的监听:

while(true) { //死循环监
try{Socket clientSocket=null;
clientSocket=serverSocket.accept();
server.addClient(clientSocket);//转交Server处理
}
catch (IOException e){System.out.println("监听端口时出错"+e);}//显示错误
}

回页首

单个客户端在连接池中的映像类ClientSingle

每一个客户端连接到服务器后,服务器会自动在连接池中建立该客户端的一个映像,所有的操作都交给这个映像去具体执行,所以ClientSingle中一定要包含客户端的一些基本的信息。比如客户端的名称、登陆时间等等。在该类中有两个消息队列sendQueue(发送队列)和receiveQueue(接收队列)缓存消息。

ClientSingle类是继承自Thread的,它还是一个调用者。在初始化的时候启动两个子线程类SingleSender和SingleListener运行。SingleSender负责监听指令发送队列中有没有指令,有则发送;SingleListener负责监听有没有消息到达,有则把这些消息加入到接收队列中去,由ClientSingle处理。所以ClientSingle的主要任务就是对这两个队列的处理。这两个队列可以用Vector实现,非常地简单。

//-------将消息加入发送队列中------------
synchronized void send(Object o)
{ sendQueue.add(o);
}

为了稳定控制子线程的运行,并不鼓励在run()方法的死循环标志都用true,而是使用了一个布尔型的变量finish。外部可以通过把这个标志置为假而停止线程的运行。

发送子线程类启动后执行run()中的循环(以finish为结束标志),在该循环内首先判断ClientSingle中的发送队列是否为空,为空时睡眠一定的时间再重新判断,这也是一个while循环。不为空则开始处理队列中的消息,把它取出后放入输出流中发送。

public void run(){
while (!father.finish){ //循环监听
while(father.v.isEmpty()){ //当发送队列为空的时候线程睡眠500毫秒
try{Thread.sleep(500);}
catch(InterruptedException e){System.out.println(e);}
}
if (!father.v.isEmpty()){ //发送队列不为空时
try{
Object a=father.v.firstElement();//取出队列中的第一个消息
father.v.removeElementAt(0);//从队列中删除
oos.writeObject(a);//发送该消息
oos.flush();
}catch(IOException e){
displayMessage(" 传输失败 !");
father.finish=false;
}
}
}
}

接收子线程SingleListener类和发送子线程是类似的,它们的run()方法都差不多。不同的是接收子线程把收到的消息加入到ClientSingle的接收队列中去,由它处理。

ClientSingle类的run()方法就在循环地读取接收队列receiveQueue中的内容,为空时等待;不为空时依次取出处理和转发。处理消息的函数是processMsg(),它只是执行消息类自己的process()方法罢了。在处理完后,会调用Server类的方法进行各种类型的转发。

回页首

分组转发的实现类Group

为了实现对客户端分组,我建立了Group类。在这个类中有一个列表存放已经存在于连接池中的那些ClientSingle类的引址。只要遍历整个列表就能访问所有组中的成员。这个列表可以用Vector实现,也可以用哈希表,我推荐后者,主要是为了能够按名字存取。

组对象本身也是可以存在Server类的组列表中的。

分组功能对多人的协同系统来说是非常重要的,特别是分组对某一个共享空间操作的时候。就以上面的协同绘图系统为例,如果10个人里有三个人要另起炉灶,那么他们三个的画板就不能让其他人看到,这就必须有"组"个划分。

回页首

主服务器类Server

Server类是最核心的类,它在这个框架中起到调度全局的作用,上面介绍的那些类都由它来统一的构造和调用。

Server类的域包括一个定长的数组存放ClientSingle实例,它就是连接池的实现。还要有一个哈希表存放Group实例。Server类的方法都是对这两个类的操作。

建立ClientSingle数组的目的是保证服务器的稳定性。其实,你也可以选择不建立它,只是动态地构造对象,但是那样不好管理连接的用户,而且由于各种操作系统对进程的处理不同,动态建立服务线程会很不稳定。所以我先建立一个数组作为这些对象的容器,在开始时就估计好连接者的最大数量。Server类的addClient()函数:

void addClient(Socket socket){
int c=0;
try{while (sch[c]!=null) c++;}//搜索数组中的空余空间
catch(ArrayIndexOutOfBoundsException e){
try{ socket.close();}//出现异常关闭槽连接
catch(IOException ee){ System.out.println("数组溢出");}
return;
}
sch[c]=new ClientSingle(c,socket,father,this);//在搜索到的位置建立ClientSingle对象
}

Server类中转发的方法有:sendToAll()、sendToOne()、sendToGroup()等等。这些方法都是对线程池中的方法的操作,比较简单,不外乎都是找到线程池中的某个ClientSingle对象,然后调用它的send()方法罢了。

注意,这些转发的方法可能被很多子线程同时调用,所以为了保持线程的稳定,千万记住要在方法前加synchronized关键字。

回页首

总结

通过上面的描述你可以发现,要建立稳定的服务器程序,消息队列和线程池是很重要的。此外,也要考虑到很多的意外情况的发生。一般的程序员在写完线程的run()方法的循环后就不管了,其实还应该考虑跳出循环后的资源释放等等问题。

这只是我在编程中总结出来的一些比较初级的经验,希望能够抛砖引玉。

关于作者

侯光敏,毕业于天津理工学院计算机科学与工程系,现在北京某公司从事软件开发工作,使用java编程。你可以通过wearebug@etang.com和我联系。

使用JAVA建立稳定的多线程服务器的更多相关文章

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

    一.Java中的服务器程序与多线程 在Java之前,没有一种主流编程语言能够提供对高级网络编程的固有支持.在其他语言环境中,实现网络程序往往需要深入依赖于操作平台的网络API的技术中去,而Java提供 ...

  2. Java如何创建多线程服务器?

    在Java编程中,如何创建多线程服务器? 以下示例演示如何使用ServerSocket类的MultiThreadServer(socketname)方法和Socket类的ssock.accept()方 ...

  3. 基于事件的 NIO 多线程服务器--转载

    JDK1.4 的 NIO 有效解决了原有流式 IO 存在的线程开销的问题,在 NIO 中使用多线程,主要目的已不是为了应对每个客户端请求而分配独立的服务线程,而是通过多线程充分使用用多个 CPU 的处 ...

  4. TCP粘包/拆包 ByteBuf和channel 如果没有Netty? 传统的多线程服务器,这个也是Apache处理请求的模式

    通俗地讲,Netty 能做什么? - 知乎 https://www.zhihu.com/question/24322387 谢邀.netty是一套在java NIO的基础上封装的便于用户开发网络应用程 ...

  5. java 并发性和多线程 -- 读感 (一 线程的基本概念部分)

    1.目录略览      线程的基本概念:介绍线程的优点,代价,并发编程的模型.如何创建运行java 线程.      线程间通讯的机制:竞态条件与临界区,线程安全和共享资源与不可变性.java内存模型 ...

  6. Java 并发性和多线程

    一.介绍 在过去单 CPU 时代,单任务在一个时间点只能执行单一程序.之后发展到多任务阶段,计算机能在同一时间点并行执行多任务或多进程.虽然并不是真正意义上的“同一时间点”,而是多个任务或进程共享一个 ...

  7. Java并发性和多线程

    Java并发性和多线程介绍   java并发性和多线程介绍: 单个程序内运行多个线程,多任务并发运行 多线程优点: 高效运行,多组件并行.读->操作->写: 程序设计的简单性,遇到多问题, ...

  8. Java并发性和多线程介绍

    java并发性和多线程介绍: 单个程序内运行多个线程,多任务并发运行 多线程优点: 高效运行,多组件并行.读->操作->写: 程序设计的简单性,遇到多问题,多开线程就好: 快速响应,异步式 ...

  9. UDP和多线程服务器

    UDP: UDP是数据报文传输协议,这个传输协议比较野蛮,发送端不需要理会接收端是否存在,直接就发送数据,不会像TCP协议一样建立连接.如果接收端不存在的话,发送的数据就会丢失,UDP协议不会去理会数 ...

  10. 在java中怎样实现多线程?线程的4种状态

    一.在java中怎样实现多线程? extends Thread implement Runnable 方法一:继承 Thread 类,覆盖方法 run(),我们在创建的 Thread 类的子类中重写 ...

随机推荐

  1. 1p-frac:已开源,仅用单张分形图片即可媲美ImageNet的预训练效果 | ECCV 2024

    分形几何是一个数学分支,主要应用于作图方面.一般来说,分形经过无数次递归迭代后的结果.比如取一条线段,抹去中间的三分之一,会得到长度是原三分之一长的两条线段,中间隔着相同长度的间隙.然后重复这个动作, ...

  2. 【YashanDB知识库】绑定参数,同一个sql多个执行计划的问题

    问题现象 同一个sql有两个执行计划,是否合理? 它的EXECUTIONS,ELAPSED_TIME等统计信息怎么看,是独立分开的还是统一计算的? 如下图: 问题影响版本 tpcc测试:23.2.1. ...

  3. MySQL read view 在RR和RC隔离级别下的异同

    1.首先了解下什么是read view 这里说的 read view 是InnoDB 在实现 MVCC 时用到的一致性读视图,即 consistent read view,用于支持 RC(Read C ...

  4. ZEGO 教程 | RTC + AI 视觉的最佳实践(移动端)

    ​  ​摘要:帮助开发者在音视频场景中快速获得 AI 视觉功能 -- 美颜.滤镜.背景抠图等. 文|即构 Native SDK 开发团队 Z世代作为社会新的消费主力,追求个性.热爱新奇事物,青睐与酷炫 ...

  5. TypeScript – Using Disposable

    前言 TypeScript v5.2 多了一个新功能叫 Disposable. Dispose 的作用是让 "对象" 离开 "作用域" 后做出一些 " ...

  6. トヨタ自動車プログラミングコンテスト2024#7(ABC 362)

    非常好名次,使我的 \(1\) 旋转 四发罚时应该是这次比赛最唐的东西了,没有就进前一千了 A.Buy a Pen 特判秒了,懒得打三种 ans=,所以就把不能选的那个赋值成无穷大了 #include ...

  7. python 属性装饰器和对应的setter方法,属性的封装和安全性控制

    当我们在类中定义属性时,通常希望能够对属性的读取和写入进行控制,以确保数据的完整性和安全性.属性装饰器和对应的setter方法提供了一种实现属性封装和安全性控制的方法. 属性装饰器是Python的一种 ...

  8. 激活windows教程

    新建bat文件 [批处理文件:后缀是 bat ] 输入代码: slmgr/skms kms.03k.org slmgr/ato 然后以管理员运行 :

  9. 妙用编辑器:使用Notepad--正则表达式从命令结果报文快速生成新命令

    应用场景 日常工作中有些维护场景,比如检查设备状态,执行查询命令后,得到精简结果报文,如果要更深入的检查状态,可能还要执行其他命令,逐个对象进行查询,这里涉及到快速从报文生成查询指令的功能. 比如有如 ...

  10. spring上 -基于注解配置bean,动态代理,AOP笔记

    用的是jdk8,spring框架里jar包的下载可以自己搜到 注解用到的jar包. 60,注解配置Bean快速入门 基本介绍 代码结构: UserDao.java package com.hspedu ...