互联网编程:实验二 多线程/线程池TCP服务器端程序设计

1.多线程TCP服务器:

改写socket服务器端程序,设计编写出一个TCP服务器端程序:要求使用多线程处理多个客户端的连接请求(每个线程实例处理一个客户端连接)。客户端与服务器端之间的通信内容,以及服务器端的处理功能等可自由设计拓展。

(1)主要思路:

  • 多线程处理客户端连接

修改服务器端程序,作为多线程TCP服务器时,首先需要确保服务器能够同时处理多个客户端的连接请求,即在服务器端程序中,需要为每个客户端连接创建一个独立的线程来处理通信。具体为其在接受到新的客户端连接请求时,为每个连接创建一个新的线程来处理。这样可以确保每个客户端连接都有独立的线程来处理通信,避免阻塞主线程。

  • 客户端与服务器端通信

确保客户端和服务器端之间的通信能够正常进行。客户端应该能够向服务器发送消息,并且服务器能够接收到并处理这些消息。同样地,服务器应该能够向客户端发送消息。

(2)具体步骤与实现代码:

  •  TCPServer类

多线程TCP服务器能够处理多个客户端的连接请求,并且能够与客户端进行通信。

  1. 服务器初始化:创建一个TCPServer类,其中包括一个构造函数和一个main方法。构造函数初始化了服务器的GUI实例和客户端处理器映射。服务器的GUI实例用于显示服务器日志和在线用户列表,客户端处理器映射用于存储连接到服务器的客户端处理器。
  2. 启动服务器:tartTCPServer()方法用于启动服务器,监听客户端的连接请求。它创建了一个ServerSocket实例,指定了服务器要监听的端口号9999。然后,通过一个无限循环持续接受客户端连接。
  3. 处理客户端连接:每当有新的客户端连接时,服务器会创建一个新的ClientHandler实例来处理该客户端的连接。ClientHandler负责与客户端进行通信,并在接收到消息时转发给服务器。为了处理多个客户端连接,服务器为每个客户端连接创建一个独立的线程。这样可以确保每个客户端连接都能够独立地进行通信,不会受到其他连接的影响。
  4. 发送消息:服务器提供了向所有客户端发送消息和向特定客户端发送消息的方法。这些方法在接收到消息后,将消息发送给相应的客户端处理器,并在日志中记录发送的消息。
  5. 客户端断开连接处理:当客户端断开连接时,服务器会从在线用户列表中移除相应的客户端处理器,并在日志中记录该客户端已断开连接的消息。
  6. 主方法:main方法创建了TCPServer实例,并调用startTCPServer()方法启动服务器。
  7. 实现代码
  1. package experiment2;
  2. import java.io.IOException;
  3. import java.net.ServerSocket;
  4. import java.net.Socket;
  5. import java.util.HashMap;
  6. import java.util.Map;
  7. /*
  8. 务器
  9.  */
  10. public class TCPServer {
  11.     //服务器GUI实例
  12.     private static ServerGUI serverGUI;
  13.     //存储客户端处理器的映射(实现为指定客户端服务)
  14.     private static Map<String,ClientHandler> clients;
  15.     //初始化服务器
  16.     public TCPServer(){
  17.         serverGUI = new ServerGUI(this);
  18.         clients = new HashMap<>();//使用哈希Map
  19.     }
  20.     //启动服务器,监听客户端连接
  21.     public void startTCPServer(){
  22.         try{
  23.             //创建服务器的Socket,监听指定端口号
  24.             ServerSocket serverSocket = new ServerSocket(9999);
  25.             serverGUI.logMessage("服务器已启动,等待客户端连接...");
  26.             //给每个用户分配特定编号
  27.             int clientCounter = 0;
  28.             //创建一个新线程来处理这个客户端的连接(客户端处理器)
  29.                 while(true){
  30.                     //持续接受客户端连接
  31.                     Socket clientSocket = serverSocket.accept();
  32.                     clientCounter ++;
  33.                     String clientName = String.valueOf(clientCounter);
  34.                     serverGUI.logMessage("客户端"+clientName+"已连接");
  35.                     ClientHandler clientHandler = new ClientHandler(clientSocket,serverGUI,clientName);
  36.                 //将客户端唯一编号和此客户端处理器一一对应
  37.                 clients.put(clientName,clientHandler);
  38.                 serverGUI.addClientHandler(clientHandler);
  39.                 //启动此线程
  40.                 new Thread(clientHandler).start();
  41.             }
  42.         }catch (IOException e){
  43.             e.printStackTrace();
  44.         }
  45.     }
  46.     //向所有客户端发送消息
  47.     public void sendToAllClients(String message){
  48.         serverGUI.logMessage("服务器发送给所有客户端:"+message);
  49.         //获取所有客户端处理线程,向每个客户端发送消息
  50.         for(ClientHandler clientHandler : serverGUI.getClientHandlers()){
  51.             clientHandler.setToClient(message);
  52.         }
  53.     }
  54.     //向特定客户端发送消息
  55.     public void sendToOneClient(String clientName, String message) {
  56.         if (clients.containsKey(clientName)) {
  57.             serverGUI.logMessage("服务器发送给客户端"+clientName+": "+message);
  58.             clients.get(clientName).setToClient(message);
  59.         } else {
  60.             serverGUI.logMessage("客户端 " + clientName + " 不存在或已断开连接");
  61.         }
  62.     }
  63.     //客户端断开连接时从在线用户列表中移除对应的客户端处理器
  64.     public static void clientDisconnected(String clientName){
  65.         serverGUI.removeClientHandler(clients.get(clientName));
  66.         clients.remove(clientName);
  67.         serverGUI.logMessage("客户端"+clientName+"已断开连接");
  68.     }
  69.     //服务器需要main可独立启动
  70.     public static void main(String[] args){
  71.         TCPServer tcpServer = new TCPServer();
  72.         tcpServer.startTCPServer();
  73.     }
  74. }
  • TCPClient类

客户端能够连接到服务器并进行消息通信。收到的服务器消息会显示在客户端GUI中,并且客户端能够通过GUI向服务器发送消息。

  1. 客户端初始化:TCPClient类有一个构造函数,接受三个参数:客户端名称、服务器地址“localhost”和端口号9999。同时,客户端GUI实例被创建,并且连接到服务器。
  2. 连接到服务器:在构造函数中,客户端创建了一个Socket实例,用于连接到指定的服务器地址和端口号。如果连接成功,客户端GUI会显示连接成功的消息。
  3. 获取输入输出流:一旦连接建立,客户端通过socket.getInputStream()和socket.getOutputStream()获取输入输出流。这样客户端就能够接收从服务器发送的消息,并向服务器发送消息。
  4. 处理接收消息:客户端通过创建一个新的线程来处理接收消息。在这个线程中,客户端持续地从输入流中读取消息,并将其显示在客户端GUI中。如果接收到的消息为null,说明服务器关闭了连接,客户端停止接收消息并退出线程。
  5. 发送消息:客户端GUI提供了一个方法setMessageSender,用于设置消息发送器。这个方法接受一个PrintWriter作为参数,用于将消息发送给服务器。客户端可以通过调用这个方法发送消息给服务器。
  6. 实现代码
  1. package experiment2;
  2. import java.io.BufferedReader;
  3. import java.io.IOException;
  4. import java.io.InputStreamReader;
  5. import java.io.PrintWriter;
  6. import java.net.Socket;
  7. public class TCPClient {
  8.     private ClientGUI clientGUI;
  9.     public TCPClient(String clientName,String serverAddress,int port){
  10.         this.clientGUI = new ClientGUI(clientName);
  11.         try{
  12.             Socket socket = new Socket(serverAddress,port);
  13.             clientGUI.logMessage("连接到服务器成功");
  14.             // 获取输入输出流
  15.             BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
  16.             PrintWriter writer = new PrintWriter(socket.getOutputStream(),true);
  17.             // 创建一个新线程来处理接收消息
  18.             Thread receiveThread = new Thread(()->{
  19.                 String message;
  20.                 try{
  21.                     while((message = reader.readLine()) != null){
  22.                         clientGUI.logMessage("服务器发来消息: " + message);
  23.                     }
  24.                 }catch(IOException e){
  25.                     e.printStackTrace();
  26.                 }
  27.             });
  28.             // 主线程用于发送消息
  29.             receiveThread.start();
  30.             clientGUI.setMessageSender(writer::println);
  31.         }catch(IOException e){
  32.             e.printStackTrace();
  33.         }
  34.     }
  35. }
  • ClientHandler类

客户端处理器能够处理客户端的连接,接收到客户端消息时将其显示在服务器GUI中。

  1. 客户端处理器初始化:ClientHandler类有一个构造函数,接受三个参数:客户端Socket、服务器GUI实例和客户端名称。在构造函数中,客户端处理器保存了对客户端Socket和服务器GUI的引用,并初始化了一个用于向客户端发送消息的PrintWriter。
  2. 向客户端发送消息:setToClient()方法用于向客户端发送消息。它接受一个消息参数,并通过客户端的PrintWriter将消息发送给客户端。
  3. 处理客户端消息接收:在run()方法中,客户端处理器创建了一个用于读取客户端消息的BufferedReader。然后,通过一个无限循环持续监听客户端发送的消息。当客户端发送消息时,服务器GUI会记录该消息,并在服务端窗口中显示客户端名称和消息内容。
  4. 实现代码
  1. package experiment2;
  2. import java.io.BufferedReader;
  3. import java.io.IOException;
  4. import java.io.InputStreamReader;
  5. import java.io.PrintWriter;
  6. import java.net.Socket;
  7. /*
  8. 理客户端的连接的线程(客户端处理器)
  9.  */
  10. public class ClientHandler implements Runnable{
  11.     private final Socket clientSocket;//客户端Socket
  12.     private final ServerGUI serverGUI;//服务端GUI实例
  13.     private PrintWriter clientWriter;//用于向客户端发送消息的写入器
  14.     private String clientName;//客户端名称
  15.     //构造函数,初始化客户端处理器
  16.     public ClientHandler(Socket clientSocket, ServerGUI serverGUI, String clientName){
  17.         this.clientSocket = clientSocket;
  18.         this.serverGUI = serverGUI;
  19.         this.clientName = clientName;
  20.         try{
  21.             clientWriter = new PrintWriter(clientSocket.getOutputStream(),true);
  22.         }catch (IOException e){
  23.             e.printStackTrace();
  24.         }
  25.     }
  26.     //向客户端发送消息的方法
  27.     public void setToClient(String message){
  28.         clientWriter.println(message);
  29.     }
  30.     //线程运行方法,处理客户端的消息接收
  31.     @Override
  32.     public void run() {
  33.         try{//创建用于读取客户端消息的读取器
  34.             BufferedReader reader = new BufferedReader(
  35.                     new InputStreamReader(clientSocket.getInputStream())
  36.             );
  37.             String message;//持续监听客户端发来的消息
  38.             while((message = reader.readLine()) != null){
  39.                 //在服务端的窗口中显示客户端发来的消息
  40.                 serverGUI.logMessage("客户端"+clientName+"发来消息: " + message);
  41.             }
  42.             reader.close();
  43.             clientSocket.close();
  44.         }catch (IOException e){
  45.             e.printStackTrace();
  46.         }
  47.     }
  48.     public String getClientName() {
  49.         return clientName;
  50.     }
  51. }
  • ServerGUI类

主要用于展示服务器日志、输入消息并发送给客户端。

  1. 初始化界面:在构造函数中,创建了一个名为 "Server" 的主窗口,并设置了关闭操作为退出整个应用程序。创建了一个用于显示服务器日志的文本区域 textArea,并将其放置在主窗口的中央位置。创建了一个用于输入消息的文本框 messageTextField,以及两个发送按钮 sendToAllButton 和 sendToButton。创建了一个用于显示在线用户列表的 JList 组件 userList,并将其放置在主窗口的右侧位置。在线用户列表使用 DefaultListModel 来管理。
  2. 按钮事件监听器:为 "发送给所有客户" 按钮添加了点击事件监听器,当点击按钮时,将获取输入的消息内容,并通过服务器实例发送给所有客户端。为 "发送" 按钮添加了点击事件监听器,当点击按钮时,将获取输入的消息内容,并通过服务器实例发送给选定的特定客户端。
  3. 消息日志处理:使用 logMessage() 方法向服务器日志文本区域添加消息。这个方法通过 SwingUtilities.invokeLater() 来确保在事件分发线程中更新界面,以避免多线程并发问题。
  4. 在线用户列表管理:addClientHandler() 方法用于向客户端处理器列表添加新的客户端处理器,并将其对应的客户端名称添加到在线用户列表中。removeClientHandler() 方法用于移除客户端处理器,并将对应的客户端名称从在线用户列表中移除。
  5. 实现代码
  1. package experiment2;
  2. import javax.swing.*;
  3. import java.awt.*;
  4. import java.awt.event.ActionEvent;
  5. import java.awt.event.ActionListener;
  6. import java.util.ArrayList;
  7. import java.util.List;
  8. /*
  9. 务端的GUI类
  10. */
  11. public class ServerGUI {
  12.     private final JTextArea textArea;//用于显示服务器日志的文本区域
  13.     private JTextField messageTextField;//用于输入要发送的消息的文本框
  14.     private JButton sendToAllButton;//发送给所有客户端的按钮
  15.     private JButton sendToButton;//发送给特定客户端的按钮
  16.     private TCPServer server;//服务器实例
  17.     private List<ClientHandler> clientHandlers;//存储客户端处理器的列表
  18.     private JList<String>userList;//显示在线用户列表
  19.     private DefaultListModel<String>userListModel;//用户列表模型
  20.     public ServerGUI(TCPServer server) {
  21.         this.server = server;
  22.         clientHandlers = new ArrayList<>();
  23.         //创建主窗口
  24.         JFrame frame = new JFrame("Server");
  25.         frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
  26.         frame.setSize(400300);
  27.         //创建用于显示服务器日志的文本区域
  28.         textArea = new JTextArea();
  29.         textArea.setEditable(false);
  30.         //创建滚动面板用于放置文本区域
  31.         JScrollPane scrollPane = new JScrollPane(textArea);
  32.         frame.getContentPane().add(scrollPane, BorderLayout.CENTER);
  33.         //创建用于输入消息的文本框和发送按钮
  34.         messageTextField = new JTextField();
  35.         JButton sendToAllButton = new JButton("发送给所有客户");
  36.         JButton sendToButton = new JButton("发送");
  37.         //在线用户列表
  38.         userListModel = new DefaultListModel<>();
  39.         userList = new JList<>(userListModel);
  40.         JScrollPane userListScrollPane = new JScrollPane(userList);
  41.         userListScrollPane.setPreferredSize(new Dimension(100,0));
  42.         //创建输入面板和按钮面板
  43.         JPanel inputPanel = new JPanel(new BorderLayout());
  44.         JPanel buttonPanel = new JPanel(new FlowLayout());
  45.         //将按钮放入按钮面板
  46.         buttonPanel.add(sendToAllButton);
  47.         buttonPanel.add(sendToButton);
  48.         //将按钮面板赫尔输入文本框都放入输入面板
  49.         inputPanel.add(messageTextField, BorderLayout.NORTH);
  50.         inputPanel.add(buttonPanel, BorderLayout.CENTER);
  51.         //将输入面板放在主窗口的下方
  52.         frame.getContentPane().add(inputPanel, BorderLayout.SOUTH);
  53.         //将用户列表放在主窗口的右侧
  54.         frame.getContentPane().add(userListScrollPane,BorderLayout.EAST);
  55.         //为"发送给所有客户"按钮添加点击事件监听器
  56.         sendToAllButton.addActionListener(new ActionListener() {
  57.             @Override
  58.             public void actionPerformed(ActionEvent e) {
  59.                 String message = messageTextField.getText();
  60.                 if (!message.isEmpty()) {
  61.                     server.sendToAllClients(message);
  62.                 }
  63.             }
  64.         });
  65.         //为"发送"按钮添加点击事件监听器
  66.         sendToButton.addActionListener(new ActionListener() {
  67.             @Override
  68.             public void actionPerformed(ActionEvent e) {
  69.                 String message = messageTextField.getText();
  70.                 if (!message.isEmpty()) {
  71.                     int selectedIndex = userList.getSelectedIndex();
  72.                     if(selectedIndex != -1){
  73.                         String selectedClientName = userListModel.getElementAt(selectedIndex);
  74.                         server.sendToOneClient(selectedClientName,message);
  75.                     }
  76.                 }
  77.             }
  78.         });
  79.         //设置主窗口可见
  80.         frame.setVisible(true);
  81.     }
  82.     //用于向服务器日志文本区域添加消息
  83.     public void logMessage(String message) {
  84.         SwingUtilities.invokeLater(() -> textArea.append(message + "\n"));
  85.     }
  86.     //用于向客户端处理器列表添加新的客户端处理器
  87.     public void addClientHandler(ClientHandler clientHandler) {
  88.         clientHandlers.add(clientHandler);
  89.         SwingUtilities.invokeLater(() -> userListModel.addElement(clientHandler.getClientName()));
  90.     }
  91.     //客户端下线则删掉此客户端处理器
  92.     public void removeClientHandler(ClientHandler clientHandler) {
  93.         clientHandlers.remove(clientHandler);
  94.         SwingUtilities.invokeLater(() -> userListModel.removeElement(clientHandler.getClientName()));
  95.     }
  96.     //获取客户端处理器列表
  97.     public List<ClientHandler> getClientHandlers() {
  98.         return clientHandlers;
  99.     }
  100. }
  • ClientGUI类

用于显示客户端消息、输入要发送的消息,并通过消息发送接口发送消息给服务器。

  1. 初始化界面:在构造函数中,创建了一个名为 "Client"+客户端编号的主窗口,并设置了关闭操作为退出整个应用程序。创建了一个用于显示客户端消息的文本区域 textArea,并将其放置在主窗口的中央位置。创建了一个用于输入消息的文本框 messageTextField,以及一个发送按钮 sendButton,用于发送消息。
  2. 消息发送接口:定义了一个消息发送接口 MessageSender,其中包含一个方法 sendMessage(String message),用于在按钮点击时发送消息。
  3. 按钮事件监听器:为发送按钮 sendButton 添加了点击事件监听器,当点击按钮时,将获取输入的消息内容,并通过消息发送接口发送给服务器。在按钮点击事件中,调用了 logMessage() 方法将发送的消息显示在客户端的文本区域中,并通过消息发送接口 messageSender 发送给服务器。
  4. 消息日志处理:使用 logMessage() 方法向客户端消息文本区域添加消息。这个方法通过 SwingUtilities.invokeLater() 来确保在事件分发线程中更新界面,以避免多线程并发问题。
  5. 实现代码
  1. package experiment2;
  2. import javax.swing.*;
  3. import java.awt.*;
  4. import java.awt.event.ActionEvent;
  5. import java.awt.event.ActionListener;
  6. /*
  7. 户端的GUI类
  8.  */
  9. public class ClientGUI {
  10.     private final JTextArea textArea;//用于显示客户端消息的文本区域
  11.     private final JTextField messageTextField;//用于输入要发送的消息的文本框
  12.     private MessageSender messageSender;//消息发送接口
  13.     //定义消息发送接口,用于在按钮点击时发送消息
  14.     public interface MessageSender{
  15.         void sendMessage(String message);
  16.     }
  17.     //设置消息发送接口
  18.     public void setMessageSender(MessageSender messageSender) {
  19.         this.messageSender = messageSender;
  20.     }
  21.     //构造函数,创建客户端GUI界面
  22.     public ClientGUI(String clientName){
  23.         JFrame frame = new JFrame("Client"+clientName);
  24.         frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
  25.         frame.setSize(300,300);
  26.         //创建文本区域,用于显示客户端消息
  27.         textArea = new JTextArea();
  28.         textArea.setEditable(false);
  29.         //创建滚动面板放置文本区域
  30.         JScrollPane scrollPane = new JScrollPane(textArea);
  31.         frame.getContentPane().add(scrollPane, BorderLayout.CENTER);
  32.         //创建文本框和发送按钮,用于输入和发送消息
  33.         messageTextField = new JTextField();
  34.         JButton sendButton = new JButton("发送");
  35.         //创建输入面板,包含文本框和发送按钮
  36.         JPanel inputPanel = new JPanel();
  37.         inputPanel.setLayout(new BorderLayout());
  38.         inputPanel.add(messageTextField, BorderLayout.CENTER);
  39.         inputPanel.add(sendButton, BorderLayout.EAST);
  40.         //将输入面板放在主窗口的下方
  41.         frame.getContentPane().add(inputPanel, BorderLayout.SOUTH);
  42.         //为发送按钮添加点击事件监听器
  43.         sendButton.addActionListener(new ActionListener() {
  44.             @Override
  45.             public void actionPerformed(ActionEvent e) {
  46.                 String message = messageTextField.getText();
  47.                 if(!message.isEmpty()){
  48.                     //在文本区域显示发送的消息并通过接口发送给服务器
  49.                     logMessage("客户端"+clientName+": "+message);
  50.                     messageTextField.setText("");
  51.                     messageSender.sendMessage(message);
  52.                 }
  53.             }
  54.         });
  55.         //设置主窗口可见
  56.         frame.setVisible(true);
  57.     }
  58.     //用于文本区域显示客户端消息
  59.     public void logMessage(String message){
  60.         SwingUtilities.invokeLater(()->textArea.append(message+"\n"));
  61.     }
  62. }
  • TestTCP类

用于启动多个客户端连接到服务器。

  1. 循环创建多个客户端:通过循环创建多个客户端,每个客户端使用不同的名称(例如,1、2、3、4、5)。每个客户端连接到指定的服务器地址和端口号9999(9999也是上面服务器监听的端口号)。多个客户端同时连接到服务器。这样可以模拟多个不同的用户或设备与服务器进行通信。
  2. 客户端连接到服务器:在循环中,创建了 TCPClient 的实例,传入了客户端名称、服务器地址和端口号。在 TCPClient 的构造函数中,创建了与服务器的连接,并启动了接收和发送消息的线程。
  3. 实现代码:
  1. package experiment2;
  2. public class TestTCP {
  3.     public static void main(String[] args){
  4.         String serverAddress = "localhost";
  5.         int port = 9999;
  6.         for(int i=1;i<=5;i++)
  7.         {
  8.             String clientName = String.valueOf(i);
  9.             TCPClient client = new TCPClient(clientName,serverAddress,port);
  10.         }
  11.     }
  12. }

2.线程池TCP服务器:

改写socket服务器端程序,设计编写出一个TCP服务器端程序:要求使用线程池处理客户端的连接请求。相应需修改socket客户端程序:从本地一个txt文件(注意:客户端的此txt文件称为信源txt文件,该txt测试数据文件可自由选择或创建,只要里面包含至少10行字符串即可),逐行读取出来,每一行作为一句话,发送给服务器,服务器每收到一个客户端的一句话,就将其保存在服务器本地的一个专用于存放与该客户端通信内容的txt文件中(注意:称为通信记录txt文件,来自不同的客户端的信息需保存在不同的txt文件中,需多少个txt文件应在程序中自动根据实际连接的客户端数量自动创建),服务器还会计算每一个通信记录txt文件的安全算法摘要,将安全算法摘要和该通信记录txt文件名一起写入到服务器中一个名为SafeAbstract.txt的文件中。

(1)主要思路:

  • 线程池TCP服务器

创建一个线程池来处理客户端的连接请求,使用ExecutorService线程池对象,ExecutorService是JAVA提供的一个线程池框架,用于管理和调度线程执行任务。

  • 客户端读取发送与服务器存储

客户端从本地的一个txt文件,即信源txt文件,逐行读取出来,而且每一行作为一句话发送给服务器。服务器每收到一个客户端的一句话,把它保存在与此客户端特定的通信记录txt文件中。

  • 服务器计算安全算法摘要

线程池TCP服务器计算每一个客户端通信记录txt文件的安全算法摘要,并且将安全算法摘要和该通信记录txt文件名一起写入到服务器中一个名为SafeAbstract.txt文件中。使用MessageDigest消息摘要对象。MessageDigest是JAVA提供的一个安全哈希函数的类,且使用MD5哈希算法来对通信记录进行逐行加密,得到安全算法摘要。

(2)具体步骤与实现代码:

  •  ExecutorTCPServer类

使用线程池处理客户端连接请求的TCP服务器。

  1. 定义常量和变量定义了常量 PORT 和 THREAD_POOL_SIZE,分别表示服务器监听的端口号和线程池的大小,即同时处理的客户端连接数。定义AtomicInteger 类型的 clientCount 来为每个客户端分配唯一的ID。
  2. AtomicInteger在多线程环境中,多个线程可能同时对共享变量进行读写操作,这可能导致竞态条件的发生,从而导致数据不一致或其他意外结果。使用 AtomicInteger 可以保证对clientCount的增加操作是原子的,即不可被中断的单个操作,不会出现竞态条件。
  3. 创建线程池: 使用 Executors.newFixedThreadPool(THREAD_POOL_SIZE) 创建一个固定大小为3的线程池。当有新的任务到来时,如果线程池中有空闲的线程,则会立即执行任务;如果线程池中的线程都在执行任务,新的任务会被暂时存储在一个任务队列中,等待有空闲的线程来执行。
  4. 创建服务器Socket并监听端口: 使用 ServerSocket 创建服务器Socket,并在指定的端口上监听客户端的连接请求。
  5. 接受客户端连接: 通过 serverSocket.accept() 方法等待客户端的连接请求,一旦有客户端连接请求到来,就会返回一个与客户端通信的Socket对象。
  6. 为每个客户端连接分配唯一ID,并交给线程池处理: 每当有新的客户端连接到来时,为其分配唯一的ID,并将客户端连接交给线程池处理,执行 EClientHandler 类中的任务。
  7. 关闭线程池和服务器Socket: 当服务器不再接受新的客户端连接请求时,关闭线程池,释放资源。
  8. 实现代码:
  1. package experiment2;
  2. import java.io.IOException;
  3. import java.net.ServerSocket;
  4. import java.net.Socket;
  5. import java.util.concurrent.ExecutorService;
  6. import java.util.concurrent.Executors;
  7. import java.util.concurrent.atomic.AtomicInteger;
  8. public class ExecutorTCPServer {
  9.     //服务器监听的端口号
  10.     private static final int PORT = 12345;
  11.     //线程池大小,即同时处理的客户端连接数
  12.     private static final int THREAD_POOL_SIZE = 3;
  13.     //客户端计数器,给每个客户端分配唯一ID
  14.     private static AtomicInteger clientCount = new AtomicInteger(0);
  15.     public static void main(String[] args){
  16.         //创建一个固定大小的线程池
  17.         //ExecutorService 是 Java 提供的一个线程池框架,用于管理和调度线程执行任务
  18.         ExecutorService executor = Executors.newFixedThreadPool(THREAD_POOL_SIZE);
  19.         try(ServerSocket serverSocket = new ServerSocket(PORT)){
  20.             System.out.println("服务器启动,正在监听端口"+PORT);
  21.             //持续接受客户端的连接
  22.             while(true){
  23.                 //等待客户端连接,并返回与客户端通信的Socket对象
  24.                 Socket clientSocket = serverSocket.accept();
  25.                 //为新连接的客户端分配唯一的ID
  26.                 int clientID = clientCount.incrementAndGet();
  27.                 //将客户端连接交给线程池处理
  28.                 executor.execute(new EClientHandler(clientSocket,clientID));
  29.                 System.out.println("服务器已与客户端"+String.valueOf(clientID)+"连接。");
  30.             }
  31.         }catch (IOException e){
  32.             e.printStackTrace();
  33.         }finally {
  34.             //关闭线程池
  35.             executor.shutdown();
  36.         }
  37.     }
  38. }
  •  EClient

实现与服务器的通信,并能够将指定文件中的数据逐行发送到服务器。

  1. 定义常量: 定义了常量 FILE_PATH,表示客户端要读取的文件路径;SERVER_ADDRESS 和 SERVER_PORT 分别表示服务器的地址和端口号。
  2. 建立与服务器的连接: 使用 Socket 类创建与服务器的Socket连接,通过提供服务器的地址和端口号,向服务器发起连接请求。
  3. 初始化资源: 使用 try-with-resources 结构,初始化了 Socket、BufferedReader 和 PrintWriter 这些资源。这样做的好处是,在使用完这些资源后,它们会自动关闭,无需手动调用 close() 方法关闭资源。
  4. 发送数据到服务器: 在与服务器建立连接后,使用 BufferedReader 从指定的文件中逐行读取数据。然后使用 PrintWriter 将每一行数据发送到服务器。发送完成后,通过 println() 方法将数据发送给服务器,并在控制台打印已发送的数据。
  5. 实现代码:

  1. package experiment2;
  2. import java.io.BufferedReader;
  3. import java.io.FileReader;
  4. import java.io.IOException;
  5. import java.io.PrintWriter;
  6. import java.net.Socket;
  7. public class EClient {
  8.     public static void main(String[] args) {
  9.         //定义客户端要读取的文件路径
  10.         final String FILE_PATH = "client_data.txt";
  11.         //定义服务器的地址和端口号
  12.         final String SERVER_ADDRESS = "localhost";
  13.         final int SERVER_PORT = 12345;
  14.         try(
  15.                 // 创建与服务器的Socket连接
  16.                 Socket socket = new Socket(SERVER_ADDRESS,SERVER_PORT);
  17.                 // 创建文件读取的BufferedReade
  18.                 BufferedReader fileReader = new BufferedReader(new FileReader(FILE_PATH));
  19.                 // 创建向服务器发送数据的PrintWriter
  20.                 PrintWriter writer = new PrintWriter(socket.getOutputStream(),true);
  21.                 ){
  22.             System.out.println("已连接到服务器");
  23.             String line;
  24.             // 逐行读取文件内容并发送到服务器
  25.             while((line = fileReader.readLine())!=null){
  26.                 // 将读取的每一行数据发送到服务器
  27.                 writer.println(line);
  28.                 System.out.println("发送:"+line);
  29.             }
  30.         }catch (IOException e){
  31.             e.printStackTrace();
  32.         }
  33.     }
  34. }
  •  EClientHandler

实现对客户端请求的处理,包括数据的接收、写入文件、计算安全摘要等功能。

  1. 处理客户端数据: 在 run() 方法中,通过 BufferedReader 从客户端Socket中获取输入流,并通过 BufferedWriter 将数据写入服务器的文件中。每个客户端连接都会生成一个独立的通信记录文件,文件名包含了客户端的ID。
  2. 计算安全摘要: 在处理完客户端的数据后,使用 BufferedReader 读取刚刚写入的通信记录文件。逐行读取文件内容,并使用MD5哈希计算每行数据的安全摘要,然后将文件名和对应的摘要写入到安全摘要文件中。
  3. MessageDiagest:MessageDigest 是 Java 提供的一个安全哈希函数的类。它用于计算数据的哈希值,常用于实现数字签名算法、数据完整性校验等安全相关的功能。它使用了一种称为消息摘要算法(Message Digest Algorithm)的技术来实现。常见的消息摘要算法包括 MD5、SHA-1、SHA-256 等。通过 MessageDigest.getInstance("algorithm") 方法来获取 MessageDigest 实例,其中 "algorithm" 是指定的哈希算法的名称,例如 "MD5"、"SHA-1" 等。在这里我使用MD5的算法。获取到 MessageDigest 实例后就可以使用 update(byte[]) 方法来更新摘要信息,然后使用 digest() 方法来计算最终的哈希值。
  4. 实现代码:
  1. package experiment2;
  2. import java.io.*;
  3. import java.net.Socket;
  4. import java.nio.charset.StandardCharsets;
  5. import java.security.MessageDigest;
  6. import java.security.NoSuchAlgorithmException;
  7. public class EClientHandler implements Runnable{
  8.     private Socket clientSocket;// 客户端Socket连接
  9.     private int clientID;// 客户端ID
  10.     // 服务器文件目录
  11.     private static final String SERVER_DIRECTORY = "server_files/";
  12.     // 安全摘要文件名
  13.     private static final String SAFE_ABSTRACT_FILE = "SafeAbstract.txt";
  14.     // 构造函数,初始化客户端Socket和ID
  15.     public EClientHandler(Socket clientSocket,int clientID){
  16.         this.clientSocket = clientSocket;
  17.         this.clientID = clientID;
  18.     }
  19.     @Override
  20.     public void run() {
  21.         try(
  22.                 // 创建从客户端读取数据的BufferedReader
  23.                 BufferedReader reader = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
  24.                 // 创建向服务器写入数据的BufferedWriter,追加写入模式,用于写入文件
  25.                 BufferedWriter writer = new BufferedWriter(new FileWriter(SERVER_DIRECTORY+"client_"+clientID+"commute.txt",true));
  26.                 // 创建写入安全摘要的BufferedWriter,追加写入模式,用于写入文件
  27.                 BufferedWriter safeAbstractWriter = new BufferedWriter(new FileWriter(SERVER_DIRECTORY+SAFE_ABSTRACT_FILE,true))
  28.                 ){
  29.                     String line;
  30.                     // 逐行读取客户端发送的数据
  31.                     while ((line = reader.readLine())!=null){
  32.                         // 将读取的数据写入到与该客户端通信的专用文件中
  33.                         writer.write(line);
  34.                         writer.newLine();
  35.                         writer.flush();
  36.                     }
  37.                     // 打开与该客户端的通信记录文件,每行读取文件里的内容并使用安全摘要
  38.                     try(BufferedReader fileReader = new BufferedReader(new FileReader(SERVER_DIRECTORY+"client_"+clientID+"commute.txt"))){
  39.                         String fileLine;
  40.                         // 先写入文件名
  41.                         safeAbstractWriter.write("client_"+clientID+"commute.txt");
  42.                         safeAbstractWriter.newLine();
  43.                         safeAbstractWriter.flush();
  44.                         // 逐行读取文件内容并计算安全摘要
  45.                         while((fileLine = fileReader.readLine())!=null){
  46.                             //计算安全摘要
  47.                             safeAbstractWriter.write(calculateSafeAbstract(fileLine));
  48.                             safeAbstractWriter.newLine();
  49.                             safeAbstractWriter.flush();
  50.                         }
  51.                     }catch (IOException e){
  52.                         e.printStackTrace();
  53.                     }
  54.         }catch (IOException e){
  55.                     e.printStackTrace();
  56.         }finally {
  57.                     try{
  58.                         // 关闭客户端Socket连接
  59.                         clientSocket.close();
  60.                     }catch (IOException e){
  61.                         e.printStackTrace();
  62.                     }
  63.         }
  64.     }
  65.     // 计算安全摘要的方法
  66.     private String calculateSafeAbstract(String text){
  67.         //使用安全哈希算法MD5
  68.         try{
  69.             // 创建MD5消息摘要对象
  70.             MessageDigest md5 = MessageDigest.getInstance("MD5");
  71.             // 更新摘要信息
  72.             md5.update(text.getBytes(StandardCharsets.UTF_8));
  73.             // 计算摘要
  74.             // MessageDigest 的 digest() 方法会返回一个字节数组(byte[]),这个字节数组包含了计算出来的哈希值。通常情况下,哈希值是以字节数组的形式表示的,每个字节都对应着哈希值中的一部分信息。
  75.             byte[] digest = md5.digest();
  76.             // 将字节数组转换为十六进制字符串十六进制字符串更容易阅读和比较,
  77.             StringBuilder sb = new StringBuilder();
  78.             for(byte b:digest){
  79.                 // %02x格式化为一个宽度为两个字符的十六进制表示,不足两个字符时,用零进行填充。
  80.                 sb.append(String.format("%02x",b));
  81.             }
  82.             return sb.toString();
  83.         }catch (NoSuchAlgorithmException e){
  84.             e.printStackTrace();
  85.             return null;
  86.         }
  87.     }
  88. }

3.编程扩充线程池TCP服务器的功能,增加日志功能模块:

日志记录的内容和日志存储方式可自定(比如可以记录客户端的连接时间、客户端IP等,日志存储为.TXT或.log文件等),在线程池服务器程序中调用该日志程序模块,使线程池TCP服务器具备日志功能,注意线程之间的同步操作处理。

(1)主要思路:

  • 日志记录器

创建Logger类,用于记录日志。该类应该包含方法来记录不同事件的日志,例如服务器事件、客户端连接、客户端断开连接等。要记录的内容包括客户端连接时间、客户端IP等。日志的存储方式选择将日志记录到文本文件server_log.txt中。

  • 线程池服务器程序中调用日志程序模块

在服务器程序中合适的位置调用日志记录器类的方法,记录服务器的关键事件,如服务器启动、客户端连接、客户端断开连接等。

(2)具体步骤与实现代码:

  •  Logger

作为日志记录器可以在服务器程序中被调用,用于记录服务器的关键事件和客户端的连接信息。

  1. 记录客户端连接: logConnection(Socket clientSocket) 方法用于记录客户端连接的信息。在方法中,通过 clientSocket.getInetAddress() 获取客户端的 IP 地址,并调用 getHostAddress() 方法获取 IP 地址的字符串表示形式,然后将连接时间和 IP 地址信息写入日志文件。
  2. 记录客户端断开: logDisconnection() 方法用于记录客户端断开连接的信息。在方法中,获取当前时间,并将断开连接的时间写入日志文件。
  3. 记录服务器事件: logServerEvent(String event) 方法用于记录服务器事件的信息。在方法中,接受一个事件字符串作为参数,并在事件字符串前加上时间戳,然后将整个事件信息写入日志文件。
  4. 线程同步: 使用 synchronized 关键字修饰了 logConnection ,logDisconnection, logServerEvent 方法,确保在多线程环境中对日志文件的写入操作是同步的,避免多个线程同时写入导致的数据不一致性问题。
  5. 实现代码:
  1. package experiment2;
  2. import java.io.FileWriter;
  3. import java.io.IOException;
  4. import java.io.PrintWriter;
  5. import java.net.InetAddress;
  6. import java.net.Socket;
  7. import java.text.SimpleDateFormat;
  8. import java.util.Date;
  9. public class Logger {
  10.     // 定义服务器日志的文件名称
  11.     private static final String LOG_FILE = "server_log.txt";
  12.     // 记录客户端连接的信息
  13.     public static synchronized void logConnection(Socket clientSocket){
  14.         try(PrintWriter writer = new PrintWriter((new FileWriter(LOG_FILE,true)))){
  15.             Date now = new Date();
  16.             SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
  17.             String nowDate = dateFormat.format(now);
  18.             // 返回的是 InetAddress 类型的对象,代表了客户端的IP地址
  19.             InetAddress clientAddress = clientSocket.getInetAddress();
  20.             // clientAddress.getHostAddress(): 这是 InetAddress 类的方法,用于获取 IP 地址的字符串表示形式。返回的是客户端的 IP 地址的字符串形式。
  21.             writer.println("["+nowDate+"]"+"新的客户端连接IP地址"+clientAddress.getHostAddress());
  22.         }catch (IOException e){
  23.             e.printStackTrace();
  24.         }
  25.     }
  26.     // 记录客户端断开的信息
  27.     public static synchronized void logDisconnection() {
  28.         try(PrintWriter writer = new PrintWriter((new FileWriter(LOG_FILE,true)))){
  29.             Date now = new Date();
  30.             SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
  31.             String nowDate = dateFormat.format(now);
  32.             writer.println("["+nowDate+"]"+"客户端断开连接");
  33.         }catch (IOException e){
  34.             e.printStackTrace();
  35.         }
  36.     }
  37.     // 记录服务器操作信息
  38.     public static synchronized void logServerEvent(String event){
  39.         try(PrintWriter writer = new PrintWriter(new FileWriter(LOG_FILE,true))){
  40.             Date now = new Date();
  41.             SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
  42.             String nowDate = dateFormat.format(now);
  43.             writer.println("["+nowDate+"]"+event);
  44.         }catch (IOException e){
  45.             e.printStackTrace();
  46.         }
  47.     }
  48. }
  •  ExecutorTCPServer
  1. 修改main方法里面的语句,在try语块中增加Logger.logServerEvent(event)语句,服务器的操作首先是启动,接着是与客户端连接,最后断开。
  2. 修改后的代码如下:
  1. public class ExecutorTCPServer {
  2.     //服务器监听的端口号
  3.     private static final int PORT = 12345;
  4.     //线程池大小,即同时处理的客户端连接数
  5.     private static final int THREAD_POOL_SIZE = 3;
  6.     //客户端计数器,给每个客户端分配唯一ID
  7.     private static AtomicInteger clientCount = new AtomicInteger(0);
  8.     public static void main(String[] args){
  9.         //创建一个固定大小的线程池
  10.         //ExecutorService 是 Java 提供的一个线程池框架,用于管理和调度线程执行任务
  11.         ExecutorService executor = Executors.newFixedThreadPool(THREAD_POOL_SIZE);
  12.         try(ServerSocket serverSocket = new ServerSocket(PORT)){
  13. //            System.out.println("服务器启动,正在监听端口"+PORT);
  14.             Logger.logServerEvent("服务器启动,正在监听端口"+PORT);//新增!
  15.             //持续接受客户端的连接
  16.             while(true){
  17.                 //等待客户端连接,并返回与客户端通信的Socket对象
  18.                 Socket clientSocket = serverSocket.accept();
  19.                 //为新连接的客户端分配唯一的ID
  20.                 int clientID = clientCount.incrementAndGet();
  21.                 //将客户端连接交给线程池处理
  22.                 executor.execute(new EClientHandler(clientSocket,clientID));
  23. //                System.out.println("服务器已与客户端"+String.valueOf(clientID)+"连接。");
  24.                 Logger.logServerEvent("服务器已与客户端"+String.valueOf(clientID)+"连接。");//新增!
  25.             }
  26.         }catch (IOException e){
  27.             e.printStackTrace();
  28.         }finally {
  29.             //关闭线程池
  30.             executor.shutdown();
  31.             Logger.logServerEvent("服务器关闭");//新增!
  32.         }
  33.     }
  34. }
  •  EClient
  1. 修改main方法里面的语句,在与服务连接上时使用Logger.logConnection(socket),断开后再使用Logger.logDisconnection()。
  2. 修改后的代码如下:
  1. public class EClient {
  2.     public static void main(String[] args) {
  3.         //定义客户端要读取的文件路径
  4.         final String FILE_PATH = "client_data.txt";
  5.         //定义服务器的地址和端口号
  6.         final String SERVER_ADDRESS = "localhost";
  7.         final int SERVER_PORT = 12345;
  8.         try(   // 创建与服务器的Socket连接
  9.                 Socket socket = new Socket(SERVER_ADDRESS,SERVER_PORT);
  10.                 // 创建文件读取的BufferedReade
  11.                 BufferedReader fileReader = new BufferedReader(new FileReader(FILE_PATH));
  12.                 // 创建向服务器发送数据的PrintWriter
  13.                 PrintWriter writer = new PrintWriter(socket.getOutputStream(),true);
  14.                 ){
  15.             System.out.println("已连接到服务器");
  16.             Logger.logConnection(socket);//新增!
  17.             String line;
  18.             // 逐行读取文件内容并发送到服务器
  19.             while((line = fileReader.readLine())!=null){
  20.                 // 将读取的每一行数据发送到服务器
  21.                 writer.println(line);
  22.                 System.out.println("发送:"+line);
  23.             }
  24.         }catch (IOException e){
  25.             e.printStackTrace();
  26.         }finally {
  27.             Logger.logDisconnection();//新增!
  28.         }
  29.     }
  30. }

  • 19
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值