Java 高级程序设计:实验十二 网络编程

代码地址:Github

实验目的和要求

  1. 理解网络编程技术;

  2. 能够用网络编程技术实现聊天程序。

实验内容

实现一个多线程并发服务器(服务器功能自选),要求该服务器可以体现出同时为多个客户端服务的特性。

实验需要实现了一个基于多线程并发服务器的网络聊天室原型,用控制台的方式实现即可,不需要用窗口应用程序的方式。

实验过程与步骤

需求分析

工具采用了Client/Server结构,将聊天室划分为两个子程序:客户端子程序、服务器端子程序。

image-20221206153659798
  • 客户端程序:实现客户端与服务器端进行连接,并可与服务器端进行实时通讯。
  • 服务器端:实现建立与多个客户端程序之间的连接,管理连接并能够通过多线程技术同时与多个客户端进行实时通讯。

系统设计

系统的设计中,客户端与服务器端的连接,通过TCP/IP网络建立,并依托Socket进行实时通讯。系统中,各个模块之间的关系如下图所示:

image-20221206153803537

上图中,显示同一服务器,通过网络可以同时与多个客户端程序建立连接并实现实时通讯功能,通讯流程如下图所示:

image-20221206153827032

根据图示,客户端与服务器之间通过Socket()进行实时通讯,通讯步骤是:服务器监听、客户端发出请求、服务器接受、建立连接、交互通讯、关闭连接。另外,由于聊天室工具必须实现多客户端同时连接通讯的要求,因此,在服务器端设计时考虑采用多线程技术,每个服务器拥有多个服务线程,每个线程负责与一个客户端进行连接通讯,从而达到一个服务器同时与多个客户端并发通讯的效果。

实验结果

代码

Server.java

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


/**
 * @author Owem
 * @date 2022/12/6 15:55
 * @description TODO
 **/
public class Server {
    public static void main(String[] args) {
        try {
            int port = 6666;

            ServerSocket serverSocket = new ServerSocket(port);

            System.out.println("服务器启动..." + serverSocket.getLocalSocketAddress());  //服务器启动,打印本地地址

            //线程池
            ExecutorService executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() * 2);

            while (true) {  //死循环
                Socket client = serverSocket.accept();
                System.out.println("有客户端连接到服务器:" + client.getRemoteSocketAddress());
                executorService.execute(new HandlerClient(client));
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

HandlerClient.java

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.Socket;
import java.util.Map;
import java.util.Scanner;
import java.util.concurrent.ConcurrentHashMap;


/**
 * @author Owem
 * @date 2022/12/6 15:56
 * @description TODO
 **/
public class HandlerClient implements Runnable {

    /**
     * 维护所有的连接到服务端的客户端对象
     */
    private static final Map<String, Socket> ONLINE_CLIENT_MAP = new ConcurrentHashMap<>();  //静态是为了不让对象变化,final不让对象被修改,ConcurrentHashMap是线程安全的类
    private final Socket client;

    public HandlerClient(Socket client) {  //HandlerClient在多线程环境下调用,所以会产生资源竞争,用一个并发的HashMap
        this.client = client;          //为了防止变量被修改,用final修饰
    }

    public void run() {
        try {
            InputStream clientInput = client.getInputStream(); //获取客户端的数据流
            Scanner scanner = new Scanner(clientInput); //字节流转字符流

            /*
             消息是按行读取
              1.注册: register:<username> 例如: register:张三
              2.群聊: groupChat:<message> 例如: groupChat:大家好
              3.查询用户: users list
              4.私聊: <user>:<message> 例如: 张三:你好,还钱
              5.退出: bye
             */

            while (true) {
                // 读数据,按行读
                String data = scanner.nextLine();
                if (data.startsWith("register:")) {
                    //注册
                    String userName = data.split(":")[1];
                    register(userName);
                    continue;
                }

                if (data.startsWith("groupChat:")) {
                    String message = data.split(":")[1];
                    groupChat(message);
                    continue;
                }

                if (data.equals("users list")) {
                    usersList();
                    continue;
                }

                if (data.contains(":")) {
                    String[] segments = data.split(":");
                    String targetUserName = segments[0]; //取目标用户名
                    String message = segments[1];   //取发送的消息内容
                    privateChat(targetUserName, message);
                }

                if (data.equals("bye")) {
                    bye();
                    break;
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 当前客户端退出
     */
    private void bye() {
        for (Map.Entry<String, Socket> entry : ONLINE_CLIENT_MAP.entrySet()) {
            Socket target = entry.getValue();
            if (target.equals(this.client)) {   //在在线用户中找到自己并且移除
                this.sendMessage(this.client, "您已下线...", false);
                System.out.println("{ " + getCurrentUserName() + " }退出聊天室");
                ONLINE_CLIENT_MAP.remove(entry.getKey());
                break;
            }
        }
        printOnlineClient();//打印当前用户
    }

    private void usersList() {
        StringBuilder s = new StringBuilder();
        for (Map.Entry<String, Socket> entry : ONLINE_CLIENT_MAP.entrySet()) {
            if (entry.getValue().equals(this.client)) {
                continue;
            }
            s.append("{ ").append(entry.getKey()).append(" } ");
        }
        if (!s.isEmpty()) {
            this.sendMessage(this.client, "当前在线用户: " + s, false);
        } else {
            this.sendMessage(this.client, "当前没有其他用户", false);
        }

    }

    private String getCurrentUserName() {
        for (Map.Entry<String, Socket> entry : ONLINE_CLIENT_MAP.entrySet()) {
            Socket target = entry.getValue(); //getvalue得到Socket对象
            if (target.equals(this.client)) { //排除群聊的时候自己给自己发消息的情况
                return entry.getKey();
            }
        }
        return "";
    }

    /**
     * 私聊,给targetUserName发送message消息
     *
     * @param targetUserName
     * @param message
     */
    private void privateChat(String targetUserName, String message) {
        Socket target = ONLINE_CLIENT_MAP.get(targetUserName);//获取目标用户名
        if (target == null) {
            this.sendMessage(this.client, "没有这个人" + targetUserName, false);
        } else {
            this.sendMessage(target, message, true);
        }
    }

    /**
     * 群聊,发送message
     *
     * @param message
     */
    private void groupChat(String message) {
        for (Map.Entry<String, Socket> entery : ONLINE_CLIENT_MAP.entrySet()) {
            Socket target = entery.getValue(); //getvalue得到Socket对象
            if (target.equals(this.client)) {
                continue;            //排除群聊的时候自己给自己发消息的情况
            }
            this.sendMessage(target, message, true);
        }
    }

    /**
     * 以userName为key注册当前用户(Socket client)
     *
     * @param userName
     */
    private void register(String userName) {
        if (ONLINE_CLIENT_MAP.containsKey(userName)) {
            this.sendMessage(this.client, "您已经注册过了,无需重复注册", false);
        } else {
            ONLINE_CLIENT_MAP.put(userName, this.client);
            printOnlineClient();
            this.sendMessage(this.client, "恭喜{ " + userName + " }注册成功\n", false);
        }
    }

    private void sendMessage(Socket target, String message, boolean prefix) {
        OutputStream clientOutput;      //value是每一个客户端
        try {
            clientOutput = target.getOutputStream();
            OutputStreamWriter writer = new OutputStreamWriter(clientOutput);
            if (prefix) {
                String currentUserName = this.getCurrentUserName();
                writer.write("<" + currentUserName + ">" + message + "\n");
            } else {
                writer.write(message + "\n");
            }
            writer.flush();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 打印在线客户端
     */
    private void printOnlineClient() {
        System.out.println("当前在线人数:" + ONLINE_CLIENT_MAP.size() + "," + "用户名如下列表:");
        System.out.println("--------------------");
        for (String userName : ONLINE_CLIENT_MAP.keySet()) {  //Map的key为用户名
            System.out.println("{ " + userName + " }");
        }
        System.out.println("--------------------");
    }
}

Client.java

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


/**
 * @author Owem
 * @date 2022/12/6 15:56
 * @description TODO
 **/
public class Client {
    public static void main(String[] args) {
        try {
            //读取地址
            String host = "127.0.0.1";
            //读取端口号
            int port = 6666;

            Socket client = new Socket(host, port); //先写数据再读数据,读写线程分离
            new ReadDataFromServerThread(client).start();//启动读线程
            new WriteDataToServerThread(client).start();//启动写线程
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

WriteDataToServerThread.java

import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.Socket;
import java.util.Scanner;

/**
 * @author Owem
 * @date 2022/12/6 15:57
 * @description TODO
 **/
public class WriteDataToServerThread extends Thread{
    private final Socket client;
    public WriteDataToServerThread(Socket client){
        this.client = client;
    }
    @Override
    public void run(){
        try {
            OutputStream clientOutput = this.client.getOutputStream();
            OutputStreamWriter writer = new OutputStreamWriter(clientOutput);
            Scanner scanner = new Scanner(System.in);  //有客户端输入数据
            while(true){
                System.out.print("请输入>>");
                String data = scanner.nextLine(); //读数据
                writer.write(data+"\n");
                writer.flush();
                if(data.equals("bye")){
                    break;
                }
            }
            Thread.sleep(1000);
            this.client.close();
        } catch (IOException | InterruptedException e) {
            e.printStackTrace();
        }
    }
}

ReadDataFromServerThread.java

import java.io.IOException;
import java.io.InputStream;
import java.net.Socket;
import java.util.Scanner;

/**
 * @author Owem
 * @date 2022/12/6 15:58
 * @description TODO
 **/
public class ReadDataFromServerThread extends Thread {
    private final Socket client;

    public ReadDataFromServerThread(Socket client) {
        this.client = client;
    }

    @Override
    public void run() {
        try {
            InputStream clientInput = this.client.getInputStream();
            Scanner scanner = new Scanner(clientInput);
            while (true) {
                String data = scanner.nextLine();//按行读数据
                if (data.startsWith("<")) {
                    System.out.println(data);
                } else {
                    System.out.println("<服务端>" + data);
                }
                if (data.equals("您已下线...")) {
                    break;
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

运行结果

启动服务器

image-20221206165441866

启动用户端(这里以两个为例)

image-20221206165514496

用户端注册

image-20221206165558473 image-20221206165611475

群聊

image-20221206165648160

查询用户

image-20221206165709457

私聊

image-20221206165733391

退出

image-20221206165818982