互联网程序设计--多用户服务器设计

教学与实践目的

学会服务器支持多用户并发访问的程序设计技术。多用户服务器是指服务器能同时支持多个用户并发访问服务器所提供的服务资源,如聊天服务、文件传输等。TCPServer是单用户版本,每次只能和一个用户对话,原因是第一个线程进入while循环,一直等待发来的消息,只有退出循环后才能执行新的线程。只有前一个用户退出后,后面的用户才能完成服务器连接。

代码实现

多用户服务器设计

存在的问题:单用户版本的TCPServer.java程序不能同时服务多用户对话

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

服务器可能面临很多客户的并发连接,因此,主线程负责监听客户请求和接受连接请求,用一个线程专门负责和一个客户对话。当一个客户请求成功后,创建一个新线程来专门负责该客户。对于服务器,一般是使用线程池来管理和复用线程。线程池内部维护了若干个线程,没有任务的时候,这些线程都处于等待状态。如果有新任务,就分配一个空闲线程执行。如果所有线程都处于忙碌状态,新任务要么放入队列等待,要么增加一个新线程进行处理。

ExecutorService 代表线程池,其创建方式常见的有两种:

ExecutorService executorService = Executors.newFixedThreadPool(n);

ExecutorService executorService = Executors. newCachedThreadPool( );

创建后,就可以使用 executorService.execute 方法来取出一个线程执行, 该方法的参数就是 Runnable 接口类型。我们可以将和客户对话部分的代码抽 取到一个 Runnable 的实现类 Handler(见最后总代码)的 run 方法中,然后丢给线程 池去执行。方便起见,Handler 作为主程序的内部类是个不错的选择。

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

本实验采用简单的“在线方式”记录客户套接字,即采用集合来保存用户登录的套接字信息,用于跟踪客户连接。因为每一个客户端的IP地址+端口组合不一样,用户套接字socket作为key来标识一个在线用户是比较方便的选择(可以结合泛型,将集合可存储的类型限制为Socket类型)。

实验过程

1、TCPClient.java

和之前代码一样

2、TCPClientThreadFX.java

注意需要修改IP地址为:202.116.195.71,端口号为:8008

3、TCPThreadServer.java

在TCPThreadServer类中定义内部类Handler implements Runnable

4、GroupServer.java

在GroupServer类中添加核心的群组发送方法sendToAllMembers,给所有在线客服转发信息

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);

        }![请添加图片描述](https://img-blog.csdnimg.cn/d9799bd46a394248a7dd0c6da182f529.png



}

5、运行结果

总结+全部代码实现

总结

1、在连接老师服务器前,需要开启自己的服务器。我的客户端连接老师服务器后,老师服务器会向我的服务器发送消息,我的服务器会给予老师服务器反馈。

 if (msg.trim().equalsIgnoreCase("来自教师服务器的连接")){

                        pw.println("1");

                    }

                    else if(msg.trim().equalsIgnoreCase("教师服务器再次发送信息")) {



                        pw.println("2");

                    }

2.为什么要使用多用户并发访问?
单用户的程序中,每一次只能与一个客户建立通信连接,主线程在while中一直运行,无法进行多用户访问。
以下代码为单用户程序:

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 行是其中的核心代码,用于提供和客户端的对话功能。

当使用多用户服务器程序设计后,主程序负责接收消息,开启副线程,交给副线程处理。

全部代码

TCPClient
package chapter05.client;
//多用户服务器程序设计
//多用户服务器是指服务器能同时支持多个用户并发访问服务器所提供的服务资源,如聊天服务、文件传输等
import java.io.*;
import java.net.Socket;

public class TCPClient {
    private Socket socket; //定义套接字
    //定义字符输入流和输出流
    private PrintWriter pw;
    private BufferedReader br;

    public TCPClient(String ip, String port) throws IOException {
        //主动向服务器发起连接,实现TCP的三次握手过程
        //如果不成功,则抛出错误信息,其错误信息交由调用者处理
        socket = new Socket(ip, Integer.parseInt(port));

        //得到网络输出字节流地址,并封装成网络输出字符流
        OutputStream socketOut = socket.getOutputStream();
        pw = new PrintWriter( // 设置最后一个参数为true,表示自动flush数据
                new OutputStreamWriter(//设置utf-8编码
                        socketOut, "utf-8"), true);

        //得到网络输入字节流地址,并封装成网络输入字符流
        InputStream socketIn = socket.getInputStream();
        br = new BufferedReader(
                new InputStreamReader(socketIn, "utf-8"));
    }

    public void send(String msg) {
        //输出字符流,由Socket调用系统底层函数,经网卡发送字节流
        pw.println(msg);
    }

    public String receive() {
        String msg = null;
        try {
            //从网络输入字符流中读信息,每次只能接受一行信息
            //如果不够一行(无行结束符),则该语句阻塞,
            // 直到条件满足,程序才往下运行
            msg = br.readLine();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return msg;
    }

    public void close() {
        try {
            if (socket != null) {
                //关闭socket连接及相关的输入输出流,实现四次握手断开
                socket.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    //本机模块内测试与运行,需先运行TCPServer
    public static void main(String[] args) throws IOException {
        TCPClient tcpClient = new TCPClient("127.0.0.1", "8008");
        tcpClient.send("hello");//发送一串字符
        //接收服务器返回的字符串并显示
        System.out.println(tcpClient.receive());
    }
}
TCPClientThreadFX
package chapter05.client;

import javafx.application.Application;
import javafx.application.Platform;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TextArea;
import javafx.scene.control.TextField;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.Socket;

public class TCPClientThreadFX extends Application {
    private Button btnExit = new Button("退出");
    private Button btnSend = new Button("发送");

    private TextField tfSend = new TextField();
    private TextArea taDisplay = new TextArea();

    private TextField tfIP = new TextField("127.0.0.1");
    private TextField tfPort = new TextField("8008");
    private Button btnConnect = new Button("连接");

    private TCPClient tcpClient;
    private Thread readThread;

    public static void main(String[] args) {
        launch(args);
    }

    @Override
    public void start(Stage primaryStage) {
        BorderPane mainPane = new BorderPane();

        HBox connHbox = new HBox();
        connHbox.setAlignment(Pos.CENTER);
        connHbox.setSpacing(10);
        connHbox.getChildren().addAll(new Label("IP地址:"), tfIP, new Label("端口:"), tfPort, btnConnect);
        mainPane.setTop(connHbox);

        VBox vBox = new VBox();
        vBox.setSpacing(10);
        vBox.setPadding(new Insets(10, 20, 10, 20));
        // 设置发送信息的文本框
        // 自动换行
        taDisplay.setWrapText(true);
        // 只读
        taDisplay.setEditable(false);
        vBox.getChildren().addAll(new Label("信息显示区: "), taDisplay, new Label("信息输入区:"), tfSend);
        VBox.setVgrow(taDisplay, Priority.ALWAYS);
        mainPane.setCenter(vBox);

        HBox hBox = new HBox();
        hBox.setSpacing(10);
        hBox.setPadding(new Insets(10, 20, 10, 20));
        hBox.setAlignment(Pos.CENTER_RIGHT);

        // 按钮事件绑定
        btnConnect.setOnAction(event -> {
            String ip = tfIP.getText().trim();
            String port = tfPort.getText().trim();

            try {
                //tcpClient不是局部变量,是本程序定义的一个TCPClient类型的成员变量
                tcpClient = new TCPClient(ip, port);
                //成功连接服务器,接收服务器发来的第一条欢迎信息
                String firstMsg = tcpClient.receive();
                taDisplay.appendText(firstMsg + "\n");
                // 启用发送按钮
                btnSend.setDisable(false);
                // 停用连接按钮
                btnConnect.setDisable(true);
                // 启用接收信息进程
                readThread = new Thread(() -> {
                    String msg = null;
                    // 新增线程是否中断条件 解决退出时出现异常问题
                    while ((msg = tcpClient.receive()) != null) {
                        String msgTemp = msg;
                        Platform.runLater(() -> {
                            taDisplay.appendText(msgTemp + "\n");
                        });
                    }
                    Platform.runLater(() -> {
                        taDisplay.appendText("对话已关闭!\n");
                        // 连接断开后重新开放连接按钮
                        btnSend.setDisable(true);
                        btnConnect.setDisable(false);
                    });
                });
                readThread.start();
            } catch (Exception e) {
                taDisplay.appendText("服务器连接失败!" + e.getMessage() + "\n");
            }

        });
        btnExit.setOnAction(event -> {
            exit();
        });
        btnSend.setOnAction(event -> {
            String sendMsg = tfSend.getText();
            tcpClient.send(sendMsg);//向服务器发送一串字符
            taDisplay.appendText("客户端发送:" + sendMsg + "\n");
            tfSend.clear();
            // 发送bye后重新启用连接按钮,禁用发送按钮
            if (sendMsg.equals("bye")) {
                btnConnect.setDisable(false);
                btnSend.setDisable(true);
            }
        });


        // 未连接时禁用发送按钮
        btnSend.setDisable(true);
        hBox.getChildren().addAll(btnSend, btnExit);
        mainPane.setBottom(hBox);
        Scene scene = new Scene(mainPane, 700, 400);

        // 回车响应功能
        scene.addEventFilter(KeyEvent.KEY_RELEASED, new EventHandler<KeyEvent>() {
            @Override
            public void handle(KeyEvent event) {
                if (event.getCode() == KeyCode.ENTER) {
                    sendText();
                }
            }
        });

        // 响应窗体关闭
        primaryStage.setOnCloseRequest(event -> {
            exit();
        });

        primaryStage.setScene(scene);
        primaryStage.show();
    }



    public void exit() {
        if (tcpClient != null) {
            tcpClient.send("bye");
            tcpClient.close();
        }
        // 系统退出时,单独的读线程没有结束,因此会出现异常。
        // 解决方案:在这里通知线程中断,在线程循环中增加条件检测当前线程是否被中断。
        // p.s. 此处使用的thread.stop()为deprecated的函数,应使用interrupt,正确写法见chapter03/TCPClientThreadFX
        readThread.stop();
        System.exit(0);
    }

    public void sendText() {
        String sendMsg = tfSend.getText();
        tcpClient.send(sendMsg);//向服务器发送一串字符
        taDisplay.appendText("客户端发送:" + sendMsg + "\n");
        tfSend.clear();
        // 发送bye后重新启用连接按钮,禁用发送按钮
        if (sendMsg.equals("bye")) {
            btnConnect.setDisable(false);
            btnSend.setDisable(true);
        }
    }
}
GroupServer
package chapter05.server;


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

public class GroupServer {
    private int port = 8008; //服务器监听端口
    private ServerSocket serverSocket; //定义服务器套接字
    public static ExecutorService executorService = Executors.newCachedThreadPool();
    public static Set<Socket> members = new CopyOnWriteArraySet<>();

    public GroupServer() throws IOException {
//        serverSocket = new ServerSocket(8008);
        System.out.println("服务器启动监听在 " + port + " 端口");
    }

    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"));
    }

    //在 GroupServer 类中添加核心的群组发送方法 sendToAllMembers,用于给所有在线客服转发信息
    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);
        }

    }

    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());
            members.add(socket);
            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("客户端离开");
                        members.remove(socket);
                        break;//跳出循环读取


                    }


                    //向输出流中回传字符串,远程客户端可以读取该字符串
//                    pw.println("From 服务器:" + msg);
                    // 替换群组发送
                    sendToAllMembers(msg, socket.getInetAddress().getHostAddress());

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


    }

    public void Service() throws IOException {
        ServerSocket TCPServer = new ServerSocket(8008);
        while (true) {
            Socket clientSocket = TCPServer.accept();
            executorService.execute(new Handler(clientSocket));
        }
    }

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

TCPThreadServer
package chapter05.server;
//新建 chapter05 程序包,然后将单用户版的 TCPserver.java 拷贝进来,重命名为 TCPThreadServer.java,参考附录中的 Handler 源代码(Handler是 TCPThreadServer 中的内部类),修改为多用户版本,调试通过后启动它;

//TCPClientThreadFX 和 TCPThreadServer 只实现了客户端和服务器聊天,如何做到客户群组的聊天?
// 如客户 A 的聊天信息通过服务器转发到同时在线的所有客户。可以在服务器端新增记录登录客户信息的功能,可用在线方式、用户文件方式或数据库方式。本讲的程序用种简单的“在线方式”记录客户套接字,即采用集合来保存用户登录的套接字信息,来跟踪客户连接
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

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

    //ExecutorService 代表线程池,其创建方式如下:
    public static ExecutorService executorService = Executors.newCachedThreadPool();



    public TCPThreadServer() throws IOException {
        System.out.println("服务器启动监听在 " + port + " 端口");
    }

    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"));
    }

    class Handler implements Runnable {
        private Socket socket;

        public Handler(Socket socket) {
            this.socket = socket;
        }




        //创建后,就可以使用 executorService.execute 方法来取出一个线程执行,该方法的参数就是 Runnable 接口类型。我们可以将和客户对话部分的代码抽取到一个 Runnable 的实现类 Handler(见附录)的 run 方法中,然后丢给线程池去执行
        @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;//跳出循环读取
                    }
                    if(msg.trim().equalsIgnoreCase("来自教师服务器的连接")){
                        pw.println("1");
                    }
                    else if(msg.trim().equalsIgnoreCase("教师服务器再次发送信息")){
                        pw.println("2");
                    }
                    //向输出流中回传字符串,远程客户端可以读取该字符串
                    pw.println("From 服务器:" + msg);

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


    }

    public void Service() throws IOException {
        ServerSocket TCPServer = new ServerSocket(8008);
        while (true) {
            Socket clientSocket = TCPServer.accept();
            executorService.execute(new Handler(clientSocket));
        }
    }

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

  • 11
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

tsuyt

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

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

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

打赏作者

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

抵扣说明:

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

余额充值