多用户通讯系统(网络编程,多线程,IO流,面向对象)

本文介绍了多用户通讯系统的设计,包括工具类的共同属性如MessageType接口,Message和User类,以及客户端登录界面、子线程管理和聊天功能。重点讲述了登录问题的修复和EOFException异常的解决,展示了如何处理网络编程和多线程中的常见挑战。
摘要由CSDN通过智能技术生成

项目介绍

工具类

方便用于输入输出的限制操作

package utilss;


/**
	工具类的作用:
	处理各种情况的用户输入,并且能够按照程序员的需求,得到用户的控制台输入。
*/

import java.util.Scanner;

/**

	
*/
public class Utility {
	//静态属性。。。
    private static Scanner scanner = new Scanner(System.in);

    
    /**
     * 功能:读取键盘输入的一个菜单选项,值:1——5的范围
     * @return 1——5
     */
	public static char readMenuSelection() {
        char c;
        for (; ; ) {
            String str = readKeyBoard(1, false);//包含一个字符的字符串
            c = str.charAt(0);//将字符串转换成字符char类型
            if (c != '1' && c != '2' && 
                c != '3' && c != '4' && c != '5') {
                System.out.print("选择错误,请重新输入:");
            } else break;
        }
        return c;
    }

	/**
	 * 功能:读取键盘输入的一个字符
	 * @return 一个字符
	 */
    public static char readChar() {
        String str = readKeyBoard(1, false);//就是一个字符
        return str.charAt(0);
    }
    /**
     * 功能:读取键盘输入的一个字符,如果直接按回车,则返回指定的默认值;否则返回输入的那个字符
     * @param defaultValue 指定的默认值
     * @return 默认值或输入的字符
     */
    
    public static char readChar(char defaultValue) {
        String str = readKeyBoard(1, true);//要么是空字符串,要么是一个字符
        return (str.length() == 0) ? defaultValue : str.charAt(0);
    }
	
    /**
     * 功能:读取键盘输入的整型,长度小于2位
     * @return 整数
     */
    public static int readInt() {
        int n;
        for (; ; ) {
            String str = readKeyBoard(10, false);//一个整数,长度<=10位
            try {
                n = Integer.parseInt(str);//将字符串转换成整数
                break;
            } catch (NumberFormatException e) {
                System.out.print("数字输入错误,请重新输入:");
            }
        }
        return n;
    }
    /**
     * 功能:读取键盘输入的 整数或默认值,如果直接回车,则返回默认值,否则返回输入的整数
     * @param defaultValue 指定的默认值
     * @return 整数或默认值
     */
    public static int readInt(int defaultValue) {
        int n;
        for (; ; ) {
            String str = readKeyBoard(10, true);
            if (str.equals("")) {
                return defaultValue;
            }
			
			//异常处理...
            try {
                n = Integer.parseInt(str);
                break;
            } catch (NumberFormatException e) {
                System.out.print("数字输入错误,请重新输入:");
            }
        }
        return n;
    }

    /**
     * 功能:读取键盘输入的指定长度的字符串
     * @param limit 限制的长度
     * @return 指定长度的字符串
     */

    public static String readString(int limit) {
        return readKeyBoard(limit, false);
    }

    /**
     * 功能:读取键盘输入的指定长度的字符串或默认值,如果直接回车,返回默认值,否则返回字符串
     * @param limit 限制的长度
     * @param defaultValue 指定的默认值
     * @return 指定长度的字符串
     */
	
    public static String readString(int limit, String defaultValue) {
        String str = readKeyBoard(limit, true);
        return str.equals("")? defaultValue : str;
    }
    
	/**
	 * 功能:读取键盘输入的确认选项,Y或N
	 * 将小的功能,封装到一个方法中.
	 * @return Y或N
	 */
    public static char readConfirmSelection() {
        System.out.println("请输入你的选择(Y/N): 请小心选择");
        char c;
        for (; ; ) {//无限循环
        	//在这里,将接受到字符,转成了大写字母
        	//y => Y n=>N
            String str = readKeyBoard(1, false).toUpperCase();
            c = str.charAt(0);
            if (c == 'Y' || c == 'N') {
                break;
            } else {
                System.out.print("选择错误,请重新输入:");
            }
        }
        return c;
    }
    /**
     * 功能: 读取一个字符串
     * @param limit 读取的长度
     * @param blankReturn 如果为true ,表示 可以读空字符串。 
     * 					  如果为false表示 不能读空字符串。
     * 			
	 *	如果输入为空,或者输入大于limit的长度,就会提示重新输入。
     * @return
     */
    private static String readKeyBoard(int limit, boolean blankReturn) {
        
		//定义了字符串
		String line = "";

		//scanner.hasNextLine() 判断有没有下一行
        while (scanner.hasNextLine()) {
            line = scanner.nextLine();//读取这一行
           
			//如果line.length=0, 即用户没有输入任何内容,直接回车
			if (line.length() == 0) {
                if (blankReturn) return line;//如果blankReturn=true,可以返回空串
                else continue; //如果blankReturn=false,不接受空串,必须输入内容
            }

			//如果用户输入的内容大于了 limit,就提示重写输入  
			//如果用户如的内容 >0 <= limit ,我就接受
            if (line.length() < 1 || line.length() > limit) {
                System.out.print("输入长度(不能大于" + limit + ")错误,请重新输入:");
                continue;
            }
            break;
        }
        return line;
    }
}

这个项目主要实现多个客户端和服务端进行各种交互的网络通讯系统。
主要实现的功能有以下一个:

  • 显示在线用户列表
  • 私聊消息(在线/离线)
  • 群发消息(在线/离线)
  • 发送文件
  • 服务器发布公告

因为是要巩固之前学习的网络编程和多线程,IO等技术,就没有考虑界面,只是简单地实现了内核程序,表现形式主要以控制台为主。

共同属性

因为客服端和服务端要共同拥有一些属性才能实现对接,所以要在客服端和服务端创建相同的属性。
单设置一个common包 里面存共同属性类

MessageType 接口

这个接口存放一些常量,表示一些状态信息,方便使用。

package common;
/**
 * 表示消息的类型
 */
public interface MessageType {
    String MESSAGE_LOGIN_SUCCEED = "1"; //表示登录成功
    String MESSAGE_LOGIN_FAIL = "2"; // 表示登录失败
    String MESSAGE_COMM_MES = "3"; //普通信息
    String MESSAGE_GET_ONLINE_FRIEND = "4"; //要求返回在线用户列表
    String MESSAGE_RET_ONLINE_FRIEND = "5"; //返回在线用户列表
    String MESSAGE_CLIENT_EXIT = "6"; //客户端请求退出
    String MESSAGE_TO_ALL_MES = "7"; //群发消息
    String MESSAGE_FILE_MES = "8"; //文件消息(发送文件)
}

Message类

因为在客户端和服务端之间进行通讯过程中要传递消息,这里我们把消息封装成一个Message类,因为要进行IO流的信息传送,所以这个类必须被序列化(在客户端和服务端这类一定要保持一模一样)。

package common;
import java.io.Serializable;		//序列化
public class Message implements Serializable {
    private static final long serialVersionUID = 1419003708355194092L;
    private String sender;//发送者
    private String getter;//接收者
    private String content;//内容
    private String sendTime;//发生时间
    private String mesType; //消息类型

    //文件相关扩展
    private byte[] fileBytes;
    private int fileLen = 0;
    private String desc;
    private String src;
    
    public byte[] getFileBytes() { return fileBytes; }
    public void setFileBytes(byte[] fileBytes) { this.fileBytes = fileBytes; }
    public int getFileLen() { return fileLen; }
    public void setFileLen(int fileLen) { this.fileLen = fileLen; }
    public String getDesc() { return desc; }
    public void setDesc(String desc) { this.desc = desc; }
    public String getSrc() { return src; }
    public void setSrc(String src) { this.src = src; }
    public String getMesType() { return mesType; }
    public void setMesType(String mesType) { this.mesType = mesType; }
    public String getSender() { return sender; }
    public void setSender(String sender) { this.sender = sender; }
    public String getGetter() { return getter; }
    public void setGetter(String getter) { this.getter = getter; }
    public String getContent() { return content; }
    public void setContent(String content) { this.content = content; }
    public String getSendTime() { return sendTime; }
    public void setSendTime(String sendTime) { this.sendTime = sendTime; }
}

User类

这个类用于存放每个用户存放的个人信息(在客户端和服务端这类一定要保持一模一样)

serialVersionUID适用于Java的序列化机制。简单来说,Java的序列化机制是通过判断类serialVersionUID来验证版本一致性的。在进行反序列化时,JVM会把传来的字节流中的serialVersionUID与本地相应实体类的serialVersionUID进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常,即是InvalidCastException。
IDEA可以快捷生成独一无二的序列编号。
具体的序列化过程是这样的:序列化操作的时候系统会把当前类的serialVersionUID写入到序列化文件中,当反序列化时系统会去检测文件中的serialVersionUID,判断它是否与当前类的serialVersionUID一致,如果一致就说明序列化类的版本与当前类版本是一样的,可以反序列化成功,否则失败
借鉴

package common;
import java.io.Serializable;
public class User implements Serializable {
	//serialVersionUID 序列化编号
    private static final long serialVersionUID = 6889194987676953061L;
    private String userID;
    private String passwd;
    public User(){};
    public User(String userID, String passwd) {
        this.userID = userID;
        this.passwd = passwd;
    }
    public String getUserID() { return userID; }
    public void setUserID(String userID) { this.userID = userID; }
    public String getPasswd() { return passwd; }
    public void setPasswd(String passwd) { this.passwd = passwd; }
}

客服端

登录界面类

这个没啥好介绍的,就是控制台界面,和一些调用方法。

package view;

import server.ChatClientServer;
import server.FileClientServer;
import server.UserClientServer;
import utils.Utility;

/**
 * 登录界面
 */
public class QQView {
    private boolean loop = true;    //控制是否显示菜单
    private String key = "";        //接收用户的键盘输入
    private UserClientServer ucs = new UserClientServer();//是用于登录服务器,注册用法
    private ChatClientServer ccs = new ChatClientServer();//聊天
    private FileClientServer fcs = new FileClientServer();//发文件

    public static void main(String[] args) {
        new QQView().mainMenu();
        System.out.println("客户端退出系统......");
    }

    //显示主菜单
    private void mainMenu() {
        while (loop) {
            System.out.println("============欢迎登录我们网络通讯系统============");
            System.out.println("\t\t\t\t 1 登录系统");
            System.out.println("\t\t\t\t 9 退出系统");
            System.out.print("请输入你的选择: ");
            key = Utility.readString(1);
            switch (key) {
                case "1":
                    System.out.print("请输入用户号: ");
                    String userId = Utility.readString(50);
                    System.out.print("请输入密  码: ");
                    String pwd = Utility.readString(50);
                    if (ucs.checkUser(userId,pwd)) {
                        //进入二级菜单
                        System.out.println("============欢迎用户 "+userId+" 登陆成功============");
                        while (loop){
                            System.out.println("=============网路通讯系统二级菜单=============");
                            System.out.println("\t\t\t 1 显示在线用户列表");
                            System.out.println("\t\t\t 2 群发消息");
                            System.out.println("\t\t\t 3 私聊消息");
                            System.out.println("\t\t\t 4 发送文件");
                            System.out.println("\t\t\t 9 退出系统");
                            System.out.print("请输入你的选择: ");
                            key = Utility.readString(1);
                            String content = "";
                            switch (key){
                                case "1":
                                    ucs.onLineUserList();
                                    break;
                                case "2":
                                    System.out.print("请输入想对大家说的话:");
                                    content = Utility.readString(100);
                                    ccs.sendMessageToAll(userId,content);
                                    break;
                                case "3":
                                    System.out.print("请输入接收方的ID(在线):");
                                    String getterId = Utility.readString(10);
                                    System.out.print("请输入您要发送的内容:");
                                    content = Utility.readString(100);
                                    ccs.sendMessageToOne(userId,getterId,content);
                                    break;
                                case "4":
                                    System.out.print("请输入你要发送的对象:");
                                    String getterId1 = Utility.readString(10);
                                    System.out.print("请输入您要发送的文件路径:");
                                    String src = Utility.readString(30);
                                    System.out.print("请输入你要发送到对方什么位置:");
                                    String desc = Utility.readString(30);
                                    fcs.sendFileToOne(userId,getterId1,src,desc);
                                    break;
                                case "9":
                                    loop = false;
                                    ucs.logout();
                                    break;
                            }
                        }
                    }else {
                        System.out.println("登陆失败......");
                    }
                    break;
                case "9":
                    loop = false;
                    break;
            }
        }
    }
}

客服端子线程类

因为每个用户都是独立存在的,而客服端只有一个,所以要在主线程里面设置一个子线程用来控制对应的一个用户,在线程里面时刻进行接收服务端传来的message报。

package server;

import common.Message;
import common.MessageType;

import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.net.Socket;

public class ClientConnectServerThread extends Thread {
    //该线程需要持有socket
    private Socket socket;
    public ClientConnectServerThread(Socket socket) {
        this.socket = socket;
    }
    public Socket getSocket() {
        return socket;
    }

    @Override
    public void run() {
        //因为Thread需要在后台和服务端通讯
        while (true) {
            try {
                System.out.println("客户端等待服务端传过来消息");
                ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
                Message message = (Message) ois.readObject(); //如果服务器没有发送message对象,线程就会阻塞在这里

                //判断这个Message的类型
                if (message.getMesType().equals(MessageType.MESSAGE_RET_ONLINE_FRIEND)) {   //返回在线列表信息

                    String[] onLineUsers = message.getContent().split(" ");
                    System.out.println("\n===========当前现在用户列表==========");
                    for (int i = 0; i < onLineUsers.length; i++) {
                        System.out.println("用户:" + onLineUsers[i]);
                    }
                } else if (message.getMesType().equals(MessageType.MESSAGE_COMM_MES)) {//私聊
                    System.out.println("\n" + message.getSender()
                            + " 对 " + message.getGetter() + " 说:" + message.getContent());
                } else if (message.getMesType().equals(MessageType.MESSAGE_TO_ALL_MES)) {//群聊
                    System.out.println("\n" + message.getSender()
                            + " 对大家说:" + message.getContent());
                } else if (message.getMesType().equals(MessageType.MESSAGE_FILE_MES)) {//发文件
                    byte[] fileBytes = message.getFileBytes();
                    String desc = message.getDesc();
                    FileOutputStream fos = new FileOutputStream(desc);
                    fos.write(fileBytes);
                    fos.close();
                    System.out.println("成功接收" + message.getSender() + "的文件");
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

管理子线程类

本项目只是在一个客户端实现了一个线程,以后若是需要在一个客户端里实现多个子线程(应用),因此需要用一个集合来统一管理多个子线程。对于本项目这个类是没有作用。

package server;
import java.util.HashMap;
/**
 * 管理客户端连接到服务端线程的一个类
 */
public class ManageClientConnectServerThread {
    //多个线程放到HashMap中  key 用户ID   value 连接服务端线程
    private static HashMap<String, ClientConnectServerThread> hm = new HashMap<>();

    //将某个线程加入到集合中
    public static void addClientConnectServerThread(String id,ClientConnectServerThread thread){
        hm.put(id,thread);
    }
    //通过userID可以得到对应的线程
    public static ClientConnectServerThread getClientConnectServerThread(String id){
        return hm.get(id);
    }
}

用户服务

package server;

import common.Message;
import common.MessageType;
import common.User;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.InetAddress;
import java.net.Socket;

/**
 * 用户登录验证和用户注册等功能
 */
public class UserClientServer {
    //因为在其他地方要使用到user信息,所以做成成员
    private User user = new User();
    //socket也要在其他地方使用
    private Socket socket;
	
	//验证账户是否正确,并且连接服务器
    public boolean checkUser(String userID, String pwd) {
        boolean flag = false;
        //创建User对象
        user.setUserID(userID);
        user.setPasswd(pwd);

        try {
            //连接到服务器,发送user对象192.168.176.1
            socket = new Socket(InetAddress.getByName("192.168.176.1"), 9999);
            //得到对象流对象
            ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
            oos.writeObject(user);      //发送user对象

            //读取服务端回送的对象
            ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
            Message o = (Message) ois.readObject();
            //
            if (o.getMesType().equals(MessageType.MESSAGE_LOGIN_SUCCEED)) {
                flag = true;
                //创建一个和服务器端保持通讯的线程
                ClientConnectServerThread thread = new ClientConnectServerThread(socket);
                thread.start();
                //将线程放到一个集合中,方便后续操作(对于客服端没用,因为只有一个子线程)
                ManageClientConnectServerThread.addClientConnectServerThread(userID, thread);
            } else {
                //如果失败,就不能启动和服务器的线程,关闭socket
                socket.close();;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return flag;
    }
    //显示在线用户列表
    public void onLineUserList(){
        //发送一个message
        Message message = new Message();
        message.setMesType(MessageType.MESSAGE_GET_ONLINE_FRIEND);
        message.setSender(user.getUserID());
        try {
            //发送给服务器
            ObjectOutputStream oos =        //通过管理线程的到对应的socket,然后得到socket对应的输出流
                    new ObjectOutputStream(ManageClientConnectServerThread.getClientConnectServerThread(user.getUserID()).getSocket().getOutputStream());
            oos.writeObject(message);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    //退出客户端,并给服务端退出的message对象
    public void logout(){
        Message message = new Message();
        message.setMesType(MessageType.MESSAGE_CLIENT_EXIT);
        message.setSender(user.getUserID());    //要明确哪个客服端
        try {
            //ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
            //因为现在一个客户端进程只有一个线程,所以这种写法也是对的,但是如果有多个线程那就不能用此socket
            ObjectOutputStream oos =
                    new ObjectOutputStream(ManageClientConnectServerThread.getClientConnectServerThread(user.getUserID()).getSocket().getOutputStream());
            oos.writeObject(message);
            System.out.println(user.getUserID()+"退出系统");
            System.exit(0);     //结束进程
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

聊天类(私聊/群聊)

package server;

import common.Message;
import common.MessageType;

import java.io.IOException;
import java.io.ObjectOutputStream;
import java.util.Date;
/**
 * 建立聊天服务
 */
public class ChatClientServer {
    /**
     * @param senderId 发送者
     * @param getterId 接收者
     * @param content  内容
     */
     //私聊
    public void sendMessageToOne(String senderId, String getterId, String content) {
        //构建message
        Message message = new Message();
        message.setMesType(MessageType.MESSAGE_COMM_MES);
        message.setSender(senderId);
        message.setGetter(getterId);
        message.setContent(content);
        message.setSendTime(new Date().toString()); //发送时间
        System.out.println(senderId + " 对 " + getterId + " 说 " + content);

        try {
            //发送
            ObjectOutputStream oos =
                    new ObjectOutputStream(ManageClientConnectServerThread.getClientConnectServerThread(senderId).getSocket().getOutputStream());
            oos.writeObject(message);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    //群聊
    public void sendMessageToAll(String senderId, String content) {
        Message message = new Message();
        message.setMesType(MessageType.MESSAGE_TO_ALL_MES);
        message.setSender(senderId);
        message.setContent(content);
        message.setSendTime(new Date().toString());
        System.out.println("我 对大家说 " + content);
        try {
            //发送
            ObjectOutputStream oos =
                    new ObjectOutputStream(ManageClientConnectServerThread.getClientConnectServerThread(senderId).getSocket().getOutputStream());
            oos.writeObject(message);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

发送文件类

package server;

import common.Message;
import common.MessageType;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;

public class FileClientServer {
    /**
     * @param senderId 发送者
     * @param getterId 接收者
     * @param src      发送路径
     * @param desc     接收路径
     */
    public void sendFileToOne(String senderId, String getterId, String src, String desc) {
        Message message = new Message();
        message.setMesType(MessageType.MESSAGE_FILE_MES);
        message.setSender(senderId);
        message.setGetter(getterId);
        message.setSrc(src);
        message.setDesc(desc);
        FileInputStream fis = null;
        byte[] bytes = new byte[(int) new File(src).length()];
        try {
            fis = new FileInputStream(src);
            fis.read(bytes);
            message.setFileBytes(bytes);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                fis.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        System.out.println("\n" + senderId + " 给 " + getterId + "发了文件");
        try {
            ObjectOutputStream oos =
                    new ObjectOutputStream(ManageClientConnectServerThread.getClientConnectServerThread(senderId).getSocket().getOutputStream());
            oos.writeObject(message);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

服务端

服务端主类

主要功能:代替MySQL存储账号信息,判断账号的正确性,启动一个子线程连接客服端,存储离线信息

package server;

import common.Message;
import common.MessageType;
import common.User;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.HashMap;

/**
 * 这是服务端,在监听端口9999,等待客户端的连接,并保持通讯
 */
public class QQServer {
    private ServerSocket ss = null;
    //创建集合存放多个用户
    //HashMap没有处理线程安全,在多线程情况下是不安全的
    //ConcurrentHashMap处理线程安全(线程同步处理),在多线程情况下是安全的
    private static HashMap<String, User> validUsers = new HashMap<>();  //用集合代替Mysql
    public static HashMap<String, ArrayList<Message>> notOnLineMessages = new HashMap<>();  //存储离线信息

    //启动服务端
    public static void main(String[] args) {
        new QQServer();
    }

    static {        //静态代码块,伴随着类的加载而加载一次
        validUsers.put("100", new User("100", "12345"));
        notOnLineMessages.put("100", new ArrayList<>());
        validUsers.put("200", new User("200", "12345"));
        notOnLineMessages.put("200", new ArrayList<>());
        validUsers.put("300", new User("300", "12345"));
        notOnLineMessages.put("300", new ArrayList<>());
        validUsers.put("小灰洁", new User("小灰洁", "12345"));
        notOnLineMessages.put("小灰洁", new ArrayList<>());
        validUsers.put("sxy", new User("sxy", "12345"));
        notOnLineMessages.put("sxy", new ArrayList<>());
        validUsers.put("syj", new User("syj", "12345"));
        notOnLineMessages.put("syj", new ArrayList<>());
    }

    //判断账号和密码是否正确
    private boolean checkUser(String userID, String pwd) {
        User user = validUsers.get(userID);
        if (user == null) return false;
        if (user.getPasswd().equals(pwd)) return true;
        return false;
    }

    public QQServer() {
        //端口可以写在配置文件里
        try {
            System.out.println("服务端在9999端口监听...");
            //启动新闻推送
            new Thread(new SendNewsToAll()).start();
            ss = new ServerSocket(9999);

            while (true) { //和某个客户端连接后,继续监听
                Socket socket = ss.accept();    //用于连接客户端的socket

                //得到socket关联的对象输入流  接收用户发送的账户信息
                ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
                User user = (User) ois.readObject();

                //得到socket关联的对象输出流  返回验证信息
                ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
                Message message = new Message();

                //验证用户
                if (checkUser(user.getUserID(), user.getPasswd())) {
                    message.setMesType(MessageType.MESSAGE_LOGIN_SUCCEED);
                    oos.writeObject(message);

                    //判断是否有客服端在此客户端没上线时,发来离线信息,如果有就显示
                    ArrayList<Message> messages = notOnLineMessages.get(user.getUserID());
                    if (messages.size() != 0) {
                        for (int i = 0; i < messages.size(); i++) {
                            Message message1 = messages.get(i);
                            ObjectOutputStream oos1 = new ObjectOutputStream(socket.getOutputStream());
                            oos1.writeObject(message1);
                        }
                    }
                    notOnLineMessages.remove(user.getUserID()); //移除离线相关信息


                    //创建一个线程,和客户端保持通讯,该线程需要持有socket对象
                    ServerConnectClientThread thread = new ServerConnectClientThread(socket, user.getUserID());
                    thread.start();
                    //因为一个客服端,建立多个子线程,所以为了方便管理,将所有子线程存入到集合中,方便使用
                    ManageServerConnectClientThread.addServerConnectClientThread(user.getUserID(), thread);
                } else {
                    //密码错误,也要进行信息反馈
                    System.out.println("密码错误");
                    message.setMesType(MessageType.MESSAGE_LOGIN_FAIL);
                    oos.writeObject(message);
                    oos.flush();
                    //登录失败,要关闭socket
                    socket.close();
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                ss.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

服务端子线程类

功能:用于对子线程的各种操作,接收服务端子线程对应客服端发来的请求进行处理,显示在线用户列表,客服端下线,服务端子线程退出,私聊(在线/离线)和群聊,发文件

package server;

import common.Message;
import common.MessageType;

import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.Socket;
import java.util.HashMap;
import java.util.Iterator;

/**
 * 该类对应的对象和客户端保持通讯
 */
public class ServerConnectClientThread extends Thread {
    private Socket socket;
    private String userId;//连接到服务端的id

    public ServerConnectClientThread(Socket socket, String userId) {
        this.socket = socket;
        this.userId = userId;
    }

    public Socket getSocket() {
        return socket;
    }

    @Override
    public void run() {     //可以接收发送消息
        while (true) {
            try {
                System.out.println("服务端和客服端" + userId + "保持通信,读取数据...");
                ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
                Message message = (Message) ois.readObject();
                if (message.getMesType().equals(MessageType.MESSAGE_GET_ONLINE_FRIEND)) {//在线用户列表
                    System.out.println(message.getSender() + " 要在线用户列表");
                    //得到用户列表
                    String onLineUser = ManageServerConnectClientThread.getOnLineUser();
                    //返回的数据信息
                    Message message1 = new Message();
                    message1.setMesType(MessageType.MESSAGE_RET_ONLINE_FRIEND);
                    message1.setContent(onLineUser);
                    message1.setGetter(message.getSender());
                    //写入到数据通道
                    ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
                    oos.writeObject(message1);

                } else if (message.getMesType().equals(MessageType.MESSAGE_CLIENT_EXIT)) {//客服端下线,服务端子线程退出
                    System.out.println(message.getSender() + "退出系统");
                    //将客服端对应的线程删除
                    ManageServerConnectClientThread.deleteServerConnectClientThread(message.getSender());
                    socket.close();
                    break;

                } else if (message.getMesType().equals(MessageType.MESSAGE_COMM_MES)) { //私聊
                    if (ManageServerConnectClientThread.getServerConnectClientThread(message.getGetter())==null){
                        QQServer.notOnLineMessages.get(message.getGetter()).add(message);//离线消息存储
                    }else {
                        //根据message获取getterId,的到对应的线程
                        ObjectOutputStream oos = new
                                ObjectOutputStream(ManageServerConnectClientThread.getServerConnectClientThread(message.getGetter()).getSocket().getOutputStream());
                        oos.writeObject(message);
                    }

                } else if (message.getMesType().equals(MessageType.MESSAGE_TO_ALL_MES)) { //群发
                    //遍历全部客户
                    HashMap<String, ServerConnectClientThread> hm = ManageServerConnectClientThread.getHm();
                    Iterator<String> iterator = hm.keySet().iterator();
                    while (iterator.hasNext()) {
                        //获取在线用户ID
                        String userId = iterator.next().toString();
                        if (!userId.equals(message.getSender())){
                            ObjectOutputStream oos =
                                    new ObjectOutputStream(hm.get(userId).getSocket().getOutputStream());
                            oos.writeObject(message);
                        }
                    }
                }else if (message.getMesType().equals(MessageType.MESSAGE_FILE_MES)){   //发文件
                    if (ManageServerConnectClientThread.getServerConnectClientThread(message.getGetter())==null){
                        QQServer.notOnLineMessages.get(message.getGetter()).add(message);   //如果不在线
                    }else {
                        ObjectOutputStream oos = new
                                ObjectOutputStream(ManageServerConnectClientThread.getServerConnectClientThread(message.getGetter()).getSocket().getOutputStream());
                        oos.writeObject(message);
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            }

        }
    }
}

管理子线程类

用集合统一管理所有服务端子线程

package server;

import java.util.HashMap;
import java.util.Iterator;

public class ManageServerConnectClientThread {
    private static HashMap<String, ServerConnectClientThread> hm = new HashMap<>();

    public static HashMap<String, ServerConnectClientThread> getHm() {
        return hm;
    }

    //添加线程对象到集合里面
    public static void addServerConnectClientThread(String userId, ServerConnectClientThread thread) {
        hm.put(userId,thread);
    }
    //删除线程对象
    public static void deleteServerConnectClientThread(String userId){
        hm.remove(userId);
    }
    public static ServerConnectClientThread getServerConnectClientThread(String userId){
        return hm.get(userId);
    }

    //返回在线用户列表
    public static String getOnLineUser(){
        //遍历HashMap
        Iterator<String> iterator = hm.keySet().iterator();
        String onLineUserList = "";
        while (iterator.hasNext()){
            onLineUserList += iterator.next().toString()+" ";
        }
        return onLineUserList;
    }
}

(服务端)发送新闻类

package server;

import common.Message;
import common.MessageType;
import utilss.Utility;

import java.io.IOException;
import java.io.ObjectOutputStream;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;

public class SendNewsToAll implements Runnable{

    @Override
    public void run() {
        while (true){
            System.out.println("请输入服务器要推送的消息[输入exit表示退出]");
            String news = Utility.readString(1000);
            if ("exit".equals(news)){
                break;
            }
            //构建消息
            Message message = new Message();
            message.setMesType(MessageType.MESSAGE_TO_ALL_MES);
            message.setSender("server");
            message.setContent(news);
            message.setSendTime(new Date().toString());
            System.out.println("服务器推送给所有人消息:"+news);

            //遍历所有线程
            HashMap<String, ServerConnectClientThread> hm = ManageServerConnectClientThread.getHm();
            Iterator<String> iterator = hm.keySet().iterator();
            while (iterator.hasNext()){
                String onLineUserId = iterator.next().toString();
                ServerConnectClientThread thread = hm.get(onLineUserId);
                try {
                    ObjectOutputStream oos = new ObjectOutputStream(thread.getSocket().getOutputStream());
                    oos.writeObject(message);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

总结

第一次bug

一直是登录失败,出现这种错误信息
在这里插入图片描述

发现主要原因是以下两点:借鉴

  1. 须要相同的包名
  2. 相同的序列化ID

改过之后就可以登录了。

第二次bug

报的错误信息是这样的:java.io.EOFException
在网上查找相关错误信息,查了好久才有点眉目。借鉴
原因竟然是因为我使用的端口号8888 这个socket的端口被阻塞了,需要换一个,然后我换成了9999,果然没出现了这个异常,程序正常运行。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值