Java实现多用户服务器程序设计(互联网程序设计课程 第5讲)


教学与实践目的:学会服务器支持多用户并发访问的程序设计技术。
多用户服务器是指服务器能同时支持多个用户并发访问服务器所提供的服务资源,如聊天服务、文件传输等。
第二讲的TCPServer是单用户版本,每次只能和一个用户对话。(请仔细阅读TCPServer.java程序,了解其中原理,找出关键语句),只有前一个用户退出后,后面的用户才能完成服务器连接。

一、单用户服务器程序演示

单用户版本TCPServer.java部分代码:

public class TCPServer {
  private int port = 8008; //服务器监听端口
  private ServerSocket serverSocket; //定义服务器套接字

  public TCPServer() throws IOException {
    serverSocket = new ServerSocket(port);
    System.out.println("服务器启动监听在 " + port + " 端口");
  }
 //省略封装的返回输入输出流的相关代码……
 
  //单客户版本,主服务方法,每一次只能与一个客户通信对话
  public void Service() {
    while (true) {
      Socket socket = null;
      try {
        //此处程序阻塞,监听并等待客户发起连接,有连接请求就生成一个套接字。
        socket = serverSocket.accept();
        //本地服务器控制台显示客户端连接的用户信息
        System.out.println("New connection accepted: " + socket.getInetAddress().getHostAddress());
      //省略输入输出流获取的代码,以下pw是字符输出流,br是字符输入流
       ……
pw.println("From 服务器:欢迎使用本服务!");        
String msg = null;
        //此处程序阻塞,每次从输入流中读入一行字符串
        while ((msg = br.readLine()) != null) {
          //如果客户发送的消息为"bye",就结束通信
          if (msg.equals("bye")) {
            //向输出流中输出一行字符串,远程客户端可以读取该字符串
             pw.println("From服务器:服务器断开连接,结束服务!");
	      System.out.println("客户端离开");
            break; //结束循环,结束通信
          }
          //向输出流中输出一行字符串,远程客户端可以读取该字符串
          pw.println("From服务器:" + msg);
        }
      } catch (IOException e) {
        e.printStackTrace();
      } finally {
        try {
          if(socket != null)
            socket.close(); //关闭socket连接及相关的输入输出流
        } catch (IOException e) {
          e.printStackTrace();
        }
      }
    }
  }

  public static void main(String[] args) throws IOException{
    new TCPServer().Service();
  }
}

27-37行是其中的核心代码,用于提供和客户端的对话功能。

演示过程:
(1)启动你的TCPServer.java程序(程序详见第二讲的附录代码);
(2)启动第三讲的TCPClientThreadFX.java程序,完成一次对话,并保持连接;
(3)再启动一份TCPClientThreadFX.java程序实例(即同时运行两个相同的程序),并完成连接,尝试对话,发现没有服务器的返回信息(服务器和客户端各阻塞在哪条语句?)
注意:现在的新版本idea默认不允许同时运行多个相同的程序,需要修改默认配置才能多实例运行:
首先进入右上角的edit Configurations,如图5.1所示:
在这里插入图片描述
然后如图5.2所示,将此程序的“Allow paralle run”复选框勾选。
在这里插入图片描述
在这里插入图片描述
(4)退出第一次启动的TCPClientThreadFX.java程序,发现第二次启动的客户端有返回信息了,说明在一个客户退出后,第二个客户才能和服务器进行对话。
原因:服务器的主进程一次只能处理一个客户,其它已连接的客户等候在监听队列中。

二、多用户服务器程序设计

单用户版本的TCPServer.java程序不能同时服务多用户对话(能同时支持多个用户并发连接请求吗?)。

1. 线程池解决多用户对话问题

解决思路就是用多线程。服务器可能面临很多客户的并发连接,这种情况的方案一般是:主线程只负责监听客户请求和接受连接请求,用一个线程专门负责和一个客户对话,即一个客户请求成功后,创建一个新线程来专门负责该客户。对于这种多用户的情况,用第三讲的方式new Thread创建线程,频繁创建大量线程需要消耗大量系统资源。对于服务器,一般是使用线程池来管理和复用线程。线程池内部维护了若干个线程,没有任务的时候,这些线程都处于等待状态。如果有新任务,就分配一个空闲线程执行。如果所有线程都处于忙碌状态,新任务要么放入队列等待,要么增加一个新线程进行处理。ExecutorService代表线程池,其创建方式常见的有两种:
ExecutorService executorService = Executors.newFixedThreadPool(n);
ExecutorService executorService = Executors. newCachedThreadPool( );
创建后,就可以使用executorService.execute方法来取出一个线程执行,该方法的参数就是Runnable接口类型。我们可以将和客户对话部分的代码抽取到一个Runnable的实现类Handler(见附录)的run方法中,然后丢给线程池去执行。方便起见,Handler作为主程序的内部类是个不错的选择。
那么应该使用哪种线程池呢,使用第一个固定线程数的线程池,显然不够灵活,第二种方式的线程池会根据任务数量动态调整线程池的大小,我们的练习可以用这个动态调整线程池。但要特别说明:作为小并发使用问题不大,但其在实际生产环境使用并不合适,如果并发量过大,常常会引发OOM错误(OutOfMemoryError)。
演示过程:
(1)新建chapter05程序包,然后将单用户版的TCPserver.java拷贝进来,重命名为TCPThreadServer.java,参考附录中的Handler源代码(Handler是TCPThreadServer中的内部类),修改为多用户版本,调试通过后启动它;
(2)多次启动你的TCPClientThreadFX.java客户程序,并保持它们同时在线,完成每一个客户的有效对话;

2. 在服务程序中支持群组聊天技术

TCPClientThreadFX和TCPThreadServer只实现了客户端和服务器聊天,如何做到客户群组的聊天?如客户A的聊天信息通过服务器转发到同时在线的所有客户。
可以在服务器端新增记录登录客户信息的功能,可用在线方式、用户文件方式或数据库方式。本讲的程序用种简单的“在线方式”记录客户套接字,即采用集合来保存用户登录的套接字信息,来跟踪客户连接。Java有一些常用的集合类型:Map、List和Set。Map是保存Key-Value对,List类似数组,可保存可重复的值,而Set只保存不重复的值,相当于是只保存key,不保存value的Map。
如果是有用户名、学号登录的操作,就可以采用Map类型的集合来存储,例如key:value对应 用户名+学号:套接字。对于我们这个问题的需求,采用Set就够了,用来保存不同用户的socket。因为每一个客户端的IP地址+端口组合不一样,用用户套接字socket作为key来标识一个在线用户是比较方便的选择(可以结合泛型,将集合可存储的类型限制为Socket类型)。
在多线程环境中,对共享资源的读写存在线程并发安全的问题,例如HashMap、HashSet等都不是线程安全的,可以通过synchronized关键字进行加锁,但还有更方便的方案:可以直接使用Java标准库的java.util.concurrent包提供的线程安全的集合。例如对应HashSet的线程安全Set是CopyOnWriteArraySet,可以定义为static类型来使用。该类的add和remove方法可以用来添加和移除元素。
一些核心步骤如下:
(1)将TCPThreadServer.java程序复制一份并重构、重命名为GroupServer.java;
(2)在GroupServer类中添加核心的群组发送方法sendToAllMembers,用于给所有在线客服转发信息:
(3)在内部类Handler中用群组发送语句sendToAllMembers(msg, socket.getInetAddress().getHostAddress());
替换pw.println(“From 服务器:” + msg);
(4)开启多个客户端,验证群发效果。
注意:仔细思考在何处添加用户socket到set集合,又在何处移除。

在这里插入图片描述

扩展一:自定义线程池

前面提到OOM的问题,如果能提供自行确定最小值和最大值的动态调整的线程池会更满足要求,大家跟踪Executors. newCachedThreadPool( )方法,观察其源代码,会发现非常简单,而且也会明白为什么会出现OOM错误,大家可以尝试将其实现代码拷贝出来稍作修改,封装一个自己版本的myCachedThreadPool方法来使用。

扩展二:简易聊天室设计

设计聊天服务器ChatServer.java,客户端用学号-姓名的方式登录服务器,实现一对一、一对多私聊及群组广播聊天的功能;用户登录时,需要将用户上线的信息广播给所有在线用户;客户端发送特定命令,服务器要能够返回在线用户列表信息;
程序设计中,要能显示发言者的学号姓名(例如20181111111-程旭元),这种情况可以考虑使用线程安全的HashMap类型(自行搜索应该使用哪个类,这些线程安全的集合类型和普通的集合类型使用方式如出一辙);
自行考虑如何设计服务端和客户端之间的交互约定(协议),可以在用户连上服务器时,即要求用户发送学号和姓名信息,并给用户发送相关的使用指南,约定发送指令的作用。
编程中要小心处理好各种逻辑关系。例如可以用一个逻辑变量来判断是否登录(记录用户的学号姓名信息,加入map,修改登录逻辑值为真,即完成了登录过程)。
例如:

......
......
if (!isLogin) {
  //获取用户登录的学号-姓名信息
  if (msg.startsWith("[") && msg.endsWith("]") && msg.contains("-")) {
    user = msg.substring(1, msg.length() - 1).trim();
    isLogin = true;
    //将用户和socket保存在全局Map
    members.put(user,socket);
    //发送使用指南
    pw.println("From 服务器:已成功登录!");
    pw.println("From 服务器:默认是发送给全体用户的广播信息,如果要发送私聊信息,使用【学号1|学号2&私聊信息】方式给指定用户发送,例如发送 【20181111111|20182222222&这是我发给你们的私聊信息】");
    pw.println("From 服务器:发送 #在线用户# 能获得所有在线用户的列表信息");

  } else {
    pw.println("From 服务器:请按格式要求发送 [学号-姓名] 进行登录!");
    continue;
  }
}

Handler类源代码

/**
 * Handler设计为TCPThreadServer类的成员内部类,实现Runnable接口,完成对话操作
 */
class Handler implements Runnable{
  private Socket socket;
  public Handler(Socket socket) {
    this.socket = socket;
  }

  @Override
  public void run() {
    //本地服务器控制台显示客户端连接的用户信息
    System.out.println("New connection accepted: " + socket.getInetAddress().getHostAddress());
    try {
      BufferedReader br = getReader(socket);//定义字符串输入流
      PrintWriter pw = getWriter(socket);//定义字符串输出流

      //客户端正常连接成功,则发送服务器欢迎信息,然后等待客户发送信息
      pw.println("From 服务器:欢迎使用本服务!");

      String msg = null;
      //此处程序阻塞,每次从输入流中读入一行字符串
      while ((msg = br.readLine()) != null) {
        //如果客户发送的消息为"bye",就结束通信
        if (msg.trim().equalsIgnoreCase("bye")) {
          //向输出流中输出一行字符串,远程客户端可以读取该字符串
          pw.println("From 服务器:服务器已断开连接,结束服务!");
          System.out.println("客户端离开");
          break;//跳出循环读取
        }
        //向输出流中回传字符串,远程客户端可以读取该字符串
        pw.println("From 服务器:" + msg);

      }
    } catch (IOException e) {
      e.printStackTrace();
    }finally {
      try {
        if(socket != null)
          socket.close(); //关闭socket连接及相关的输入输出流
      } catch (IOException e) {
        e.printStackTrace();
      }
    }
  }
}

项目结构

在这里插入图片描述

完整代码

chapter05/GroupServer.java

package chapter05;

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * @projectName: NetworkApp
 * @package: chapter05
 * @className: GroupServer
 * @author: GCT
 * @description: TODO
 * @date: 2022/9/27 20:14
 * @version: 1.0
 */
public class GroupServer {
    private int port = 8008; //服务器监听端口
    private ServerSocket serverSocket; //定义服务器套接字
    private ExecutorService executorService;
    private static Set<Socket> members = new CopyOnWriteArraySet<Socket>();

    public GroupServer() throws IOException {
        serverSocket = new ServerSocket(port);
        executorService = Executors.newCachedThreadPool();
        System.out.println("多用户服务器启动"+port);
    }
    public void service(){
        while(true){
            Socket socket = null;
            try{
                socket = serverSocket.accept(); //监听客户请求, 阻塞语句.
                members.add(socket);
                //接受一个客户请求,从线程池中拿出一个线程专门处理该客户.
                executorService.execute(new Handler(socket));
            }catch (IOException ex){
                ex.printStackTrace();
            }
        }
    }

    private void sendToAllMembers(String msg,String hostAddress) throws IOException{
        PrintWriter pw;
        OutputStream out;
        for(Socket tempSocket:members) {
            out = tempSocket.getOutputStream();
            pw = new PrintWriter(new OutputStreamWriter(out,"utf-8"),true);
            pw.println(hostAddress + "发言"+msg);

        }
//        Socket tempSocket;

//        Iterator<Socket> iterator = members.iterator();
//        while (iterator.hasNext()) {//遍历在线客户Set集合
//            tempSocket = iterator.next(); //取出一个客户的socket
//            String hostName = tempSocket.getInetAddress().getHostName();
//            String hostAddress = tempSocket.getInetAddress().getHostAddress();
//            out = tempSocket.getOutputStream();
//            pw = new PrintWriter(
//                    new OutputStreamWriter(out, "utf-8"), true);
//            pw.println(tempSocket.getInetAddress() + " 发言:" + msg );

    }

    class Handler implements Runnable{
        private Socket socket;
        public Handler(Socket socket) {
            this.socket = socket;
        }


        @Override
        public void run() {
            //本地服务器控制台显示客户端连接的用户信息
            System.out.println("New connection accepted: " + socket.getInetAddress());
            try {
                BufferedReader br = getReader(socket);//定义字符串输入流
                PrintWriter pw = getWriter(socket);//定义字符串输出流

                //客户端正常连接成功,则发送服务器欢迎信息,然后等待客户发送信息
                pw.println("From 服务器:欢迎使用本服务!");

                String msg = null;
                //此处程序阻塞,每次从输入流中读入一行字符串
                while ((msg = br.readLine()) != null) {
                    //如果客户发送的消息为"bye",就结束通信
                    if (msg.trim().equalsIgnoreCase("bye")) {
                        //向输出流中输出一行字符串,远程客户端可以读取该字符串
                        pw.println("From 服务器:服务器已断开连接,结束服务!");
                        System.out.println("客户端离开");
                        break;//跳出循环读取
                    }
                    //向输出流中回传字符串,远程客户端可以读取该字符串
                    sendToAllMembers(msg,socket.getInetAddress().getHostAddress());

                }
            } catch (IOException e) {
                e.printStackTrace();
            }finally {
                try {
                    if(socket != null)
                        socket.close(); //关闭socket连接及相关的输入输出流
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }


    private PrintWriter getWriter(Socket socket) throws IOException {
        //获得输出流缓冲区的地址
        OutputStream socketOut = socket.getOutputStream();
        //网络流写出需要使用flush,这里在PrintWriter构造方法中直接设置为自动flush
        return new PrintWriter(
                new OutputStreamWriter(socketOut, "utf-8"), true);
    }

    private BufferedReader getReader(Socket socket) throws IOException {
        //获得输入流缓冲区的地址
        InputStream socketIn = socket.getInputStream();
        return new BufferedReader(
                new InputStreamReader(socketIn, "utf-8"));
    }


    //单客户版本,即每一次只能与一个客户建立通信连接


    public static void main(String[] args) throws IOException{
        new GroupServer().service();
    }

}

chapter05/TCPThreadServer.java

package chapter05;

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * @projectName: NetworkApp
 * @package: chapter02
 * @className: TCPServer
 * @author: GCT
 * @description: TODO
 * @date: 2022/8/30 20:28
 * @version: 1.0
 */
public class TCPThreadServer {
    private int port = 7777; //服务器监听端口
    private ServerSocket serverSocket; //定义服务器套接字
    /**
     * @param :
     * @return null
     * @author 86139
     * @description TODO
     * @date 2022/9/27 19:28
     */
    private ExecutorService executorService;



    public TCPThreadServer() throws IOException {
        serverSocket = new ServerSocket(port);
        executorService = Executors.newCachedThreadPool();
        System.out.println("多用户服务器启动在"+port);
    }

    /**
     * @param :
     * @return PrintWriter
     * @author 86139
     * @description TODO
     * @date 2022/9/27 19:29
     */
    public void service(){
        while(true){
            Socket socket = null;
            try{
                socket = serverSocket.accept(); //监听客户请求, 阻塞语句.
                //接受一个客户请求,从线程池中拿出一个线程专门处理该客户.
                executorService.execute(new Handler(socket));
            }catch (IOException ex){
                ex.printStackTrace();
            }
        }
    }

    class Handler implements Runnable{
        private Socket socket;
        public Handler(Socket socket) {
            this.socket = socket;
        }


        @Override
        public void run() {
//            System.out.println("New connection accepted:"+socket.getInetAddress());
            System.out.println("New connection accepted: " + socket.getInetAddress().getHostAddress());
            try{
                BufferedReader br = getReader(socket);
                PrintWriter pw = getWriter(socket);
                pw.println("From 服务器:欢迎使用本服务!");

                String msg = null;
                while((msg = br.readLine())!=null){
                    if(msg.trim().equalsIgnoreCase("bye")){
                        pw.println("From 服务器:服务器已经断开连接,结束服务!");
                        System.out.println("客户端离开");
                        break;
                    }
                    /**
                     * @param :
                     * @return void
                     * @author 86139
                     * @description TODO
                     * @date 2022/9/27 19:47
                     * 你的服务器如果收到信息:"来自教师服务器的连接" ,你的服务器应该回发 1 ;
                     * 如果收到信息:"教师服务器再次发送信息" ,则回发 2 。
                     */

                    else if (msg.trim().equalsIgnoreCase("来自教师服务器的连接")){
                        pw.println("1");
                    }
                    else if (msg.trim().equalsIgnoreCase("教师服务器再次发送信息")){
                        pw.println("2");
                    }
                    else {
                        pw.println("From 服务器"+msg);
                    }

                }

            }catch (IOException e){
                e.printStackTrace();
            }finally {
                try{
                    if(socket !=null)
                        socket.close();
                }catch (IOException e){
                    e.printStackTrace();
                }
            }


        }
    }

    private PrintWriter getWriter(Socket socket) throws IOException {
        //获得输出流缓冲区的地址
        OutputStream socketOut = socket.getOutputStream();
        //网络流写出需要使用flush,这里在PrintWriter构造方法中直接设置为自动flush
        return new PrintWriter(
                new OutputStreamWriter(socketOut, "utf-8"), true);
    }

    private BufferedReader getReader(Socket socket) throws IOException {
        //获得输入流缓冲区的地址
        InputStream socketIn = socket.getInputStream();
        return new BufferedReader(
                new InputStreamReader(socketIn, "utf-8"));
    }

    
//    //单客户版本,即每一次只能与一个客户建立通信连接
//    public void Service() {
//        while (true) {
//            Socket socket = null;
//            try {
//                //此处程序阻塞等待,监听并等待客户发起连接,有连接请求就生成一个套接字。
//                socket = serverSocket.accept();
//                //本地服务器控制台显示客户端连接的用户信息
//                System.out.println("New connection accepted: " + socket.getInetAddress().getHostAddress());
//                BufferedReader br = getReader(socket);//定义字符串输入流
//                PrintWriter pw = getWriter(socket);//定义字符串输出流
//                //客户端正常连接成功,则发送服务器的欢迎信息,然后等待客户发送信息
//                pw.println("From 服务器:欢迎使用本服务!");
//
//                String msg = null;
//                //此处程序阻塞,每次从输入流中读入一行字符串
//                while ((msg = br.readLine()) != null) {
//                    //如果客户发送的消息为"bye",就结束通信
//                    if (msg.trim().equals("bye")) {
//                        //向输出流中输出一行字符串,远程客户端可以读取该字符串
//                        pw.println("From服务器:服务器断开连接,结束服务!");
//                        System.out.println("客户端离开");
//                        break; //结束循环
//                    }
//                    //向输出流中输出一行字符串,远程客户端可以读取该字符串
//                    pw.println("From服务器:" + msg);
//
//                    //下面多增加一条信息返回语句
//                    pw.println("来自服务器,重复发送: " + msg);
//
//                }
//            } catch (IOException e) {
//                e.printStackTrace();
//            } finally {
//                try {
//                    if(socket != null)
//                        socket.close(); //关闭socket连接及相关的输入输出流
//                } catch (IOException e) {
//                    e.printStackTrace();
//                }
//            }
//        }
//    }

    public static void main(String[] args) throws IOException{
        new TCPThreadServer().service();
    }
}

  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
### 回答1: 网上排课系统可以帮助学校、教育机构和个人更方便、高效地管理课程信息,提升课程安排的准确性和质量。在Java课程设计中,设计一个网上排课系统需要考虑以下几个方面: 一、需求分析:根据用户的需求,确定系统的功能模块,包括管理员后台管理、教师管理、学生管理、教室管理、课程表排等模块。同时,还需要考虑系统的安全性和用户体验。 二、数据库设计:设计数据库,包括各种实体的属性和关系,如学生、教师、课程、教室等。在考虑数据库设计的同时,还应该确定数据的有效性、完整性和一致性,以及数据库的备份和恢复方案。 三、功能实现:通过Java编程语言实现系统各模块的功能,如管理员后台管理中的用户管理、角色管理、课程管理等,学生、教师和管理员的注册与登录、课程表自动排、退课、选课等。 四、界面设计:实现用户友好、简洁、易用的界面,提供方便的操作流程和反馈信息。应该注重用户交互体验,确保用户能够轻松完成想要的操作。 五、系统测试和部署:对系统行功能测试和负载测试,检查程序的正确性、性能和稳定性;确定合适的服务器、数据中心和操作系统等参数,以确保系统顺利运行。 综上所述,设计一个网上排课系统需要全面考虑功能设计、数据库设计、程序编码、界面设计以及系统测试和部署等多方面因素,以满足用户的课程管理需求。 ### 回答2: 网上排课系统是一种提供课程排班的软件,它可以充分利用计算机的高效率、智能化等特点,使学校的课程安排更加方便、快捷和科学。 Java课程设计网上排班系统应该包括以下功能模块: 一、管理员管理模块:管理员可以添加、删除、修改、查询教师及课程信息,能够灵活设置并处理各种可能出现的冲突,使得排班系统能够更加顺畅的运行。 二、教师管理模块:教师可以在系统中选择自己要教授的课程,并选择自己方便上课的时间段,以满足教学计划的要求。 三、学生管理模块:学生可以根据自己的需要查看课程表,以方便他们安排自己的时间。 四、课程管理模块:系统为学校提供了方便的课程管理界面,教师可以在界面中添加课程,并设置每个课程的课时,课程内容等信息。 五、排课管理模块:系统会根据教师提交的时间表信息和学生的选课需求,自动排班,生成排班结果,并让管理员行审核,确保排班的准确性。 六、通知管理模块:系统可以通过邮件,短信等方式行通知,保证学生和教师在第一时间知道自己的上课时间和地点。 Java课程设计网上排班系统在应用中可以减轻学校和教师的工作负担,增强学生学习效果和学校的管理水平,有促学校网络化管理的发展和推学校信息化建设的作用。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

GCTTTTTT

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值