JAVA 聊天室(韩顺平)

源码在最后面,编译运行之前记得在UserClientService改id(获取id的方法在最后面)

目录

一、项目设计技术

二、项目开发流程

三、需求分析与界面设计

 四、思路分析与程序框架图

4.1 实现登录功能

4.2 创建启动类

4.3 创建服务类(用于验证用户合法性)

4.4 拉取在线用户实现

4.5 无异常退出的实现

4.6 私聊

4.7 群聊

4.8 发送文件的实现

4.8 服务器推送新闻

五、完整代码

5.1 公共代码

5.2 QQClient

5.3 QQServer

5.4 获取id


一、项目设计技术

1.项目设计框架

2.面向对象

3.网络编程

4.多线程

5.IO流

6.Mysql(这里我们使用集合HashMap充当内存数据库)

二、项目开发流程

1.需求阶段

需求分析师来做,出一个需求分析的报告(白皮书的格式),项目的功能,客户的要求。

需求分析师的要求:1.有技术。2.懂行业。   例如:银行,税务,海关。

2.设计阶段

项目经理或者是架构师来处理

工作:制作类图,流程图,模块,数据库,架构,流量,用户访问量,组建团队(挑选团队成员)等。 

工资:基本工资+项目提成

3.实现阶段

程序员,码农来进行实现。刚进入工作基本上都是实现岗位。

4.测试阶段

测试工程师,找到模块可能出现的bug 。(可能是引用模块的问题。耦合性高的问题)。测试和实现是相互交叉的

5.部署阶段

实施工程师,主要是对环境的配置,要经常出差,将项目部署到客户指定的机子上面。

6.维护阶段

项目部署到对方平台,出现问题之后,进行处理。 项目升级,项目扩展之类的要求。

三、需求分析与界面设计

3.1 用户登录

3.2 拉取在线用户列表

 3.3 私聊

 3.4 群聊

3.5 发文件

3.6 文件服务器推送新闻

 四、思路分析与程序框架图

传输对象,对象当中包含传递的信息,从管道当中运输信息。

Message类来进行装载信息,Socket作为传送信息两端的基站。 线程持有一个socket。socket是线程的一个属性。

因为群发的需求,所以需要一个线程集合来进行管理。可以查找集合当中都有那些线程,确定与服务器链接的用户。

一个客户端也可能与服务端有多个线程的链接。例如:发送文件,视频聊天,文件下载。是不同的。 所以客户端也应该有一个线程的集合来对线程进行统一的管理。

客户端使用对象流来进行读写信息。
 

4.1 实现登录功能

1.先创建三个类一个接口(服务端客户端共有)

Message:用于存放用户信息

import java.io.Serializable;

/**
 * 表示客户端和服务端通信时的消息对象
 */
public class Message implements Serializable {
    private static final long serialVersionUID = 1L;
    private String sender;//发送者
    private String getter;//接收者
    private String content;//消息内容
    private String sendTime;//发送时间
    private String mesType;//消息类型[可以在接口定义消息类型]

    //进行扩展 和文件相关的成员
    private byte[] fileBytes;
    private int fileLen = 0;
    private String dest; //将文件传输到哪里
    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 getDest() {
        return dest;
    }

    public void setDest(String dest) {
        this.dest = dest;
    }

    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:用于存储用户账号密码信息的

import java.io.Serializable;

/**
 * 表示一个用户/客户信息
 */
public class User implements Serializable {

    private static final long serialVersionUID = 1L;
    private String userId;//用户Id/用户名
    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;
    }
}

MessageType:用于存放消息类型的接口(Message当中的一个属性)

package com.hspedu.qqcommon;

/**
 * 表示消息类型
 */
public interface MessageType {
    //老师解读
    //1. 在接口中定义了一些常量
    //2. 不同的常量的值,表示不同的消息类型.
    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"; //文件消息(发送文件)

}

Utility类

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

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

4.2 创建启动类

服务端QQServer:

QQFrame用来创建一个QQService类,QQService用于验证用户的合法性

package com.qqservice.sevice;
 
import com.qqcommon.Message;
import com.qqcommon.MessageType;
import com.qqcommon.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.concurrent.ConcurrentHashMap;
 
public class QQServer {
 
    private ServerSocket ss;
 
    private static ConcurrentHashMap<String ,User> validUser = new ConcurrentHashMap<>();
    private static ConcurrentHashMap<String , ArrayList<Message>> offLineDb = new ConcurrentHashMap<>();
 
    static {
        validUser.put("100",new User("100","123456"));
        validUser.put("200",new User("200","123456"));
        validUser.put("300",new User("300","123456"));
        validUser.put("至尊宝宝",new User("至尊宝宝","123456"));
        validUser.put("菩提老祖",new User("紫霞仙子","123456"));
        validUser.put("紫霞仙头",new User("菩提老头","123456"));
    }
 
 
    private boolean checkUser(String userId,String password){
        User user = validUser.get(userId);
 
        if (user == null){
            return false;
        }
        if (!user.getPassword().equals(password)){
            return false;
        }
        return true;
    }
public QQServer(){
        try  {
            System.out.println("服务端在9999端口进行监听");
            new Thread(new SendNewsToAllService()).start();
            ss = new ServerSocket(9999);
            while (true){
                Socket socket = ss.accept();
//                通过socket来创建输出和输入流
                ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
                ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
//              通过输入流来接受用户登录的数据
                User user = (User)ois.readObject();
                Message message = new Message();
//              判断用户是否合法
                    System.out.println("用户 id = "+user.getUserId()+"用户密码  "+user.getPassword()+"验证失败");
                    message.setMesType(MessageType.MESSAGE_LOGIN_FAIL);
                    oos.writeObject(message);
                    socket.close();
               
            }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            try {
                ss.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

客户端:

QQView用户的操作界面,一级界面用来验证用户合法性(接收用户输入的账号密码),二级页面用来提供服务。(用户想要实现的功能)

package com.view;
 
import com.service.*;
import com.utils.Utility;
 
public class QQView {
    private boolean loop = true;
    private UserClientService userClientService = new UserClientService();//对象是用于登录/注册
    private MessageClientService messageClientService = new MessageClientService();
    private FileClientService fileClientService = new FileClientService();
    private NotOnlineService notOnlineService = new NotOnlineService();
    private String key = "";
 
    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 1 登录系统");
            System.out.println("\t\t 2 退出系统");
            System.out.print("请输入你的选择:");
 
            key = Utility.readString(1);
 
            switch (key){
                case "1":
                    System.out.println("请输入用户号");
                    String userId = Utility.readString(50);
                    System.out.println("请输入密 码");
                    String pwd = Utility.readString(50);
                    //验证合法性
                    if (userClientService.checkUse(userId,pwd)){
                        System.out.println("=========欢迎(用户"+userId+"登录成功)========");
                        while (loop){
                            System.out.println("\n=========网络通信系统二级菜单(用户"+userId+")======");
                            System.out.println("\t\t 1 显示在线用户列表");
                            System.out.println("\t\t 2 群发消息");
                            System.out.println("\t\t 3 私聊消息");
                            System.out.println("\t\t 4 发送文件");
                            System.out.println("\t\t 9 退出系统");
                            System.out.println("请输入你的选择");
                             key = Utility.readString(1);
                             switch (key){
                                 case "1":
                                     userClientService.onlineFriendList();
                                     break;
                                 case "2":
                                     System.out.println("请你说一下你相对大家说的话。");
                                     String s = Utility.readString(100);
                                     messageClientService.sendMessageToAll(s,userId);
                                     break;
                                 case "3":
                                     System.out.print("请输入你想要聊天的用户(在线):");
                                     String getter = Utility.readString(50);
                                     System.out.println("请输入你想对"+getter+"说的话");
                                     String content =Utility.readString(100);
                                     System.out.println("私聊信息");
                                     //编写一个方法,吧消息发给服务端
                                     messageClientService.sendMessageToOne(content,userId,getter);
                                     break;
                                 case "4":
                                     System.out.println("请输入您想把文件发送给得用户(在线用户)");
                                     String getterId = Utility.readString(50);
                                     System.out.print("请输入发送的文件路径(形式: d:\\xx.jpg)");
                                     String src = Utility.readString(100);
                                     System.out.print("请输入发送到对方的文件路径(形式: :e\\xx.jpg)");
                                     String dest =Utility.readString(100);
                                     fileClientService.sendFileToOne(src,dest,userId,getterId);
                                     break;
                                 case "9":
                                     userClientService.logout();
                                     loop = false;
                                     break;
                              }
                        }
                    }else {
                        System.out.println("=======登录失败=====");  //当然也可以写成密码或账号错误
                    }
                    break;
                case "2":
                    System.out.println("退出系统");
                    loop = false;
                    break;
 
            }
        }
    }
}

4.3 创建服务类(用于验证用户合法性)

客户端:

UserClientService类,使用Socket建立和服务端的链接,将用户输入的账号密码放入到user对象当中,通过对象输出流ObjectOutputStream发送给服务端。

创建对象输入流,接受从服务端Socket传过来的信息(Message类型的)。查看信息类型MessageType属性,是否验证成功。

成功则创建线程ClientConnectServerThread,并将线程放入线程管理类ManageClientConnectServerThread当中,并开启线程

UserClientService类

package com.service;
 
import com.qqcommon.Message;
import com.qqcommon.MessageType;
import com.qqcommon.User;
 
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.InetAddress;
import java.net.Socket;
 
//完成用户登录的验证
public class UserClientService {
//    因为在其他的地方可能会使用User属性所以作为成员变量
    private User user = new User();
//    其他线程也会使用socket变量,所以设置成成员变量
    private Socket socket;
//   验证传入的数据
    public boolean checkUse(String userId,String pwd){
        boolean b = false;
 
        user.setPassword(pwd);
        user.setUserId(userId);
 
        try {
            //与服务器建立链接
            socket = new Socket(InetAddress.getByName("127.0.0.1"), 9999);
            //得到ObjectOutputStream对象流,并将用户信息写入,发送出去后,服务端返回返回值
            ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
            oos.writeObject(user);
            //读取从服务器返回的Message对象
            ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
            Message ms = (Message) ois.readObject();
            //根据返回来的服务器的信息来判断有没有登录成功,密码账户数据的对比是在服务端进行的
            if (ms.getMesType().equals(MessageType.MESSAGE_LOGIN_SUCCESS)){//登录ok
                //创建一个和服务器保持通信的线程 让线程持有Socket 所以创建ClientConnectServerThread
                ClientConnectServerThread clientConnectServerThread = new ClientConnectServerThread(socket);
                //启动一个客户端线程  执行run方法
                clientConnectServerThread.start();
                //将线程放到管理线程的集合当中
                ManageClientConnectServerThread.addClientConnectServerThread(userId,clientConnectServerThread);
                b = true;
            }else {
//                登录失败
                socket.close();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return b;
    }
}

ClientConnectServerThread类(会一直读取服务端返回的信息)

package com.service;
 
import com.qqcommon.Message;
import com.qqcommon.MessageType;
 
import java.io.*;
import java.net.Socket;
import java.util.ArrayList;
 
public class ClientConnectServerThread extends Thread {
    //改线程需要持有Socket
    private Socket socket;
    //构造器可以接受一个socket对象
    public ClientConnectServerThread(Socket socket) {
        this.socket = socket;
    }
 
    @Override
    public void run() {
        //Thread需要在后台与服务器进行通信,持续通信所以while
        while (true) {
            try {
                System.out.println("客户端线程等待,读取服务器端发送的消息。");
//                得到输入流
                ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
                //处理读入的对象
                Object o = ois.readObject();//如果服务器没有发送objec对象,线程会阻塞在这里
                    Message ms = (Message) o;
                    //对的到的信息进行处理
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
    //  为了更方便的得到socket
    public Socket getSocket() {
        return socket;
    }
}

ManageClientConnectServerThread类 

package com.qqservice.sevice;
 
import java.util.HashMap;
import java.util.Iterator;
 
public class ManageClientThread {
    private static HashMap<String,ServerConnectClientThread> hm = new HashMap<>();
 
    public static void addClientThread(String userId,ServerConnectClientThread serverConnectClientThread){
        hm.put(userId,serverConnectClientThread);
    }
 
    public static ServerConnectClientThread getServerConnectClientThread(String userId){
        return hm.get(userId);
    }
    public static String getOnlineUser(){
        Iterator<String > iterator = hm.keySet().iterator();
        String onlineUserList = "";
        while (iterator.hasNext()){
            //拼接到一个字符串当中进行返回
            onlineUserList += iterator.next().toString()+" ";
        }
        return onlineUserList;
    }
 
    public static void remove(String userId){
        hm.remove(userId);
 
    }
 
    public static HashMap<String, ServerConnectClientThread> getHm() {
        return hm;
    }
}

服务端:

QQService类,首先使用集合放置用户信息。接受客户端发来的信息,判断与服务端的信息是否一致,将判断结果放入Message类的对象的MessageType属性当中,返回。并创建线程,将线程放入线程集合当中。

QQServer

package com.qqservice.sevice;
 
import com.qqcommon.Message;
import com.qqcommon.MessageType;
import com.qqcommon.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.concurrent.ConcurrentHashMap;
 
public class QQServer {
 
    private ServerSocket ss;
 
    private static ConcurrentHashMap<String ,User> validUser = new ConcurrentHashMap<>();
    private static ConcurrentHashMap<String , ArrayList<Message>> offLineDb = new ConcurrentHashMap<>();
//  静态代码段,将用户信息输入到集合当中
    static {
        validUser.put("100",new User("100","123456"));
        validUser.put("200",new User("200","123456"));
        validUser.put("300",new User("300","123456"));
        validUser.put("至尊宝",new User("至尊宝","123456"));
        validUser.put("菩提老祖",new User("紫霞仙子","123456"));
        validUser.put("紫霞仙子",new User("菩提老祖","123456"));
    }
 
//  检查用户的合法性
    private boolean checkUser(String userId,String password){
        User user = validUser.get(userId);
 
        if (user == null){
            return false;
        }
        if (!user.getPassword().equals(password)){
            return false;
        }
        return true;
    }
public QQServer(){
        try  {
            System.out.println("服务端在9999端口进行监听");
            new Thread(new SendNewsToAllService()).start();
            ss = new ServerSocket(9999);
            while (true){
                Socket socket = ss.accept();
//                通过socket来创建输出和输入流
                ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
                ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
//              通过输入流来接受用户登录的数据
                User user = (User)ois.readObject();
                Message message = new Message();
//              判断用户是否合法
                if(checkUser(user.getUserId(),user.getPassword())){
//                  将对比后的状态写入并发送
                    message.setMesType(MessageType.MESSAGE_LOGIN_SUCCESS);
                    oos.writeObject(message);
 
//                    创建一个线程保持联系
                    ServerConnectClientThread scct = new ServerConnectClientThread(socket, user.getUserId());
                    scct.start();
 
                    ManageClientThread.addClientThread(user.getUserId(),scct);
                    ConcurrentHashMap<String, ArrayList> offlineMap = OffLineMessageService.getOfflineMap();
 
                }else {
                    System.out.println("用户 id = "+user.getUserId()+"用户密码  "+user.getPassword()+"验证失败");
                    message.setMesType(MessageType.MESSAGE_LOGIN_FAIL);
                    oos.writeObject(message);
                    socket.close();
                }
            }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            try {
                ss.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

ServerConnectClientThread(放入socket和UserId来确定这个线程属于谁)

package com.qqservice.sevice;
 
import com.qqcommon.Message;
import com.qqcommon.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; //是那个用户进行的链接
 
    public ServerConnectClientThread(Socket socket, String userId) {
        this.socket = socket;
        this.userId = userId;
    }
 
    public Socket getSocket() {
        return socket;
    }
 
    @Override
    public void run() {  //线程处于run状态 ,可以发送和接受消息
        while (true) {
            try {
                System.out.println("服务端与客户端保持联系,"+userId+"读取数据当中。。。。。");
                ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
//                message就是对面传过来的数据包
                Message message = (Message) ois.readObject();
                //对传来的数据进行处理
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

ManageClientThread 

package com.qqservice.sevice;
 
import java.util.HashMap;
import java.util.Iterator;
 
public class ManageClientThread {
    private static HashMap<String,ServerConnectClientThread> hm = new HashMap<>();
 
    public static void addClientThread(String userId,ServerConnectClientThread serverConnectClientThread){
        hm.put(userId,serverConnectClientThread);
    }
 
    public static ServerConnectClientThread getServerConnectClientThread(String userId){
        return hm.get(userId);
    }
    public static String getOnlineUser(){
        Iterator<String > iterator = hm.keySet().iterator();
        String onlineUserList = "";
        while (iterator.hasNext()){
            //拼接到一个字符串当中进行返回
            onlineUserList += iterator.next().toString()+" ";
        }
        return onlineUserList;
    }
 
    public static void remove(String userId){
        hm.remove(userId);
 
    }
 
    public static HashMap<String, ServerConnectClientThread> getHm() {
        return hm;
    }
}


4.4 拉取在线用户实现

思路分析:用户从二级页面输入自己要拉去在线用户的请求,客户端接受请求。服务端接到请求后返回在线用户列表。

客户端:

UserClientService 类进行扩展,之前这个类是发送user信息的,用来判断用户的合法性。

得到输出流是建立在自己已经登录成功的基础之上的,在登录的时候,会创建一个线程,并将线程放入线程管理类当中,现在直接根据userId直接拿到线程当中的socket创建输出流就可以了。
 

 //向服务器端请求在线用户列表
    public void onlineFriendList() {

        //发送一个Message , 类型MESSAGE_GET_ONLINE_FRIEND
        Message message = new Message();
        message.setMesType(MessageType.MESSAGE_GET_ONLINE_FRIEND);
        message.setSender(u.getUserId());

        //发送给服务器

        try {
            //从管理线程的集合中,通过userId, 得到这个线程对象
            ClientConnectServerThread clientConnectServerThread =
                    ManageClientConnectServerThread.getClientConnectServerThread(u.getUserId());
            //通过这个线程得到关联的socket
            Socket socket = clientConnectServerThread.getSocket();
            //得到当前线程的Socket 对应的 ObjectOutputStream对象
            ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
            oos.writeObject(message); //发送一个Message对象,向服务端要求在线用户列表
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

    //编写方法,退出客户端,并给服务端发送一个退出系统的message对象
    public void logout() {
        Message message = new Message();
        message.setMesType(MessageType.MESSAGE_CLIENT_EXIT);
        message.setSender(u.getUserId());//一定要指定我是哪个客户端id

        //发送message
        try {
            //ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
            ObjectOutputStream oos =
                    new ObjectOutputStream(ManageClientConnectServerThread.getClientConnectServerThread(u.getUserId()).getSocket().getOutputStream());
            oos.writeObject(message);
            System.out.println(u.getUserId() + " 退出系统 ");
            System.exit(0);//结束进程
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

ClientConnectServerThread类当中对发送回来的数据进行处理

                    Message ms = (Message) o;
                    //对的到的信息进行处理
                    if (ms.getMesType().equals(MessageType.MESSAGE_RET_ONLINE_FRIEND)) {
                        String[] onlineUsers = ms.getContent().split(" ");
                        System.out.println("\n========在线用户列表======");
                        for (int i = 0; i < onlineUsers.length; i++) {
                            System.out.println("用户 : " + onlineUsers[i]);
                        }

服务端:

ManageClientThread类当中专门创建了一个方法,来获取线程当中所有用户的Id,并将Id合并为一个字符串,每个Id以空格隔开

    public static String getOnlineUser(){
        Iterator<String > iterator = hm.keySet().iterator();
        String onlineUserList = "";
        while (iterator.hasNext()){
            //拼接到一个字符串当中进行返回
            onlineUserList += iterator.next().toString()+" ";
        }
        return onlineUserList;
    }

线程类ServerConnectClientThread的处理,验证消息类型然后进行对应的处理,放入到message包裹当中进行返回。

                if (message.getMesType().equals(MessageType.MESSAGE_GET_ONLINE_FRIEND)){
                    System.out.println(message.getSender()+" 要在线用户列表");
                    String onlineUser = ManageClientThread.getOnlineUser();
 
                    //将数据返回给客户端
                    Message message1 = new Message();
                    message1.setMesType(MessageType.MESSAGE_RET_ONLINE_FRIEND);
                    message1.setContent(onlineUser);  //放入在线列表数据
                    message1.setGetter(message.getSender());
//                  发送给客户端
                    ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream());
                    objectOutputStream.writeObject(message1);

最后直接点在用户界面QQView创建对象调用方法就可以了 

 
                                case "1":
                                     userClientService.onlineFriendList();
                                     break;

4.5 无异常退出的实现

产生异常退出的原因。线程一段突然关闭,而另一端没有得到退出消息,一直在进行得不到数据的循环,报错。

客户端:

UserClientService 类当中增加方法

//    退出系统,并发送一个massage对象、
    public void logout(){
        Message message = new Message();
        message.setMesType(MessageType.MESSAGE_CLIENT_EXIT);
        message.setSender(user.getUserId());//指定是哪个服务端
        try {
            ObjectOutputStream objectOutputStream = new ObjectOutputStream(ManageClientConnectServerThread.getClientConnectServerThread(user.getUserId()).getSocket().getOutputStream());
            objectOutputStream.writeObject(message);
            System.out.println(user.getUserId()+" 退出系统 ");
            System.exit(0);  //结束进程
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

QQView调用方法

                                 case "9":
                                     userClientService.logout();
                                     loop = false;
                                     break;

 服务端:

ServerConnectClientThread增加代码段

                } else if(message.getMesType().equals(MessageType.MESSAGE_CLIENT_EXIT)){
                    System.out.println(message.getSender()+" 退出");
                    ManageClientThread.remove(message.getSender());  //在线程管理当中移除线程
                    socket.close();
                    break;

4.6 私聊

 

客户端:

QQView增加的代码

                                 case "3":
                                     System.out.print("请输入你想要聊天的用户(在线):");
                                     String getter = Utility.readString(50);
                                     System.out.println("请输入你想对"+getter+"说的话");
                                     String content =Utility.readString(100);
                                     System.out.println("私聊信息");
                                     //编写一个方法,吧消息发给服务端
                                     messageClientService.sendMessageToOne(content,userId,getter);
                                     break;

创建一个新的类MessageClientService,用来将用户输入的数据,写到message当中 ,并进行发送,还是和发送get在线用户列表一样的方法,创建输出流。

之后还会再这个类当中实现群聊发送

package com.service;
 
import com.qqcommon.Message;
import com.qqcommon.MessageType;
 
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.net.Socket;
import java.util.Date;
 
public class MessageClientService {
    public void sendMessageToOne(String content,String senderId,String getterId){
        Message message = new Message();
        message.setMesType(MessageType.MESSAGE_COMM_MES);
        message.setSender(senderId);
        message.setContent(content);
        message.setGetter(getterId);
        message.setSendTime(new Date().toString());
        System.out.println(senderId+"对"+getterId+"说");
        try {
            ObjectOutputStream objectOutputStream = new ObjectOutputStream(ManageClientConnectServerThread.getClientConnectServerThread(senderId).getSocket().getOutputStream());
            objectOutputStream.writeObject(message);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

得到服务端处理来的结果,在线程类ClientConnectServerThread当中进行处理。

                    } else if (ms.getMesType().equals(MessageType.MESSAGE_COMM_MES)) {
                        //吧消息取出打印就可以了
                        System.out.println("\n" + ms.getSender() + " 对 " + ms.getGetter() + " 说 " + ms.getContent());

服务端

直接在线程类ServerConnectClientThread当中将接受到的message进行转发,这里创建一个新的输出流,输出的对象为信息的接收者。

                }else if (message.getMesType().equals(MessageType.MESSAGE_COMM_MES)){
//                    注意,这里变成了getter
                        ServerConnectClientThread serverConnectClientThread = ManageClientThread.getServerConnectClientThread(message.getGetter());
                        ObjectOutputStream objectOutputStream = new ObjectOutputStream(serverConnectClientThread.socket.getOutputStream());
                        objectOutputStream.writeObject(message);

4.7 群聊

服务端:

QQView

                                 case "2":
                                     System.out.println("请你说一下你相对大家说的话。");
                                     String s = Utility.readString(100);
                                     messageClientService.sendMessageToAll(s,userId);
                                     break;

MessageClientService

 sout输出的内容会在服务端控制台显示,服务端则是服务端的控制台

    public void sendMessageToAll(String content,String senderId){
        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(senderId+"对 大家 说"+content);
        try {
            ObjectOutputStream objectOutputStream = new ObjectOutputStream(ManageClientConnectServerThread.getClientConnectServerThread(senderId).getSocket().getOutputStream());
            objectOutputStream.writeObject(message);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

服务端

ServerConnectClientThread类当中新增加的代码端

                }else if (message.getMesType().equals(MessageType.MESSAGE_TO_ALL_MES)){  //判断消息种类
                    //创建集合,用来装所有的线程
                    HashMap<String ,ServerConnectClientThread> hm = ManageClientThread.getHm();
//                  对所有线程的拥有者(用户)进行遍历
                    Iterator<String > iterator = hm.keySet().iterator();
                    while (iterator.hasNext()){
                        String onLineUserId = iterator.next().toString();
                        //如果在线用户不是群发消息的发送者
                        if (!onLineUserId.equals(message.getSender())){
//                            获取对应线程发送消息  所有在线用户都在登录成功的时候有了自己的专属线程
                            ObjectOutputStream objectOutputStream = new ObjectOutputStream(hm.get(onLineUserId).getSocket().getOutputStream());
                            objectOutputStream.writeObject(message);
                        }
                    }

4.8 发送文件的实现

 服务端

QQView来显示用户的输入

                                 case "4":
                                     System.out.println("请输入您想把文件发送给得用户(在线用户)");
                                     String getterId = Utility.readString(50);
                                     System.out.print("请输入发送的文件路径(形式: d:\\xx.jpg)");
                                     String src = Utility.readString(100);
                                     System.out.print("请输入发送到对方的文件路径(形式: :e\\xx.jpg)");
                                     String dest =Utility.readString(100);
                                     fileClientService.sendFileToOne(src,dest,userId,getterId);

创建FileClientService来处理文件

package com.service;
 
import com.qqcommon.Message;
import com.qqcommon.MessageType;
 
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
 
public class FileClientService {
    public void sendFileToOne(String src,String dest,String sender,String getter){
        Message message = new Message();
        message.setMesType(MessageType.MESSAGE_FILE_MES);
        message.setGetter(getter);
        message.setSender(sender);
        message.setDest(dest);
        message.setSrc(src);
//      将文件放入message
        FileInputStream inputStream = null;
        byte[] bytes = new byte[(int) new File(src).length()];
 
        try {
//            创建文件流  目的是将文件从磁盘上取出来
            inputStream = new FileInputStream(src);
            inputStream.read(bytes);//将字节数组写道流当中
            message.setFileByte(bytes);
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            if (inputStream!=null){
                try {
                    inputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
 
        System.out.println("\n"+sender+" 给 "+getter+" 发送 "+src+" 到目录 "+dest);
        //将message写入到线程流当中
        try {
            ObjectOutputStream objectOutputStream = new ObjectOutputStream(ManageClientConnectServerThread.getClientConnectServerThread(sender).getSocket().getOutputStream());
            objectOutputStream.writeObject(message);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

ClientConnectServerThread类当中处理服务端发来的消息

                    } else if (ms.getMesType().equals(MessageType.MESSAGE_FILE_MES)) {
                        System.out.println("\n" + ms.getSender() + "给" + ms.getGetter() + "发送" + ms.getSrc() + "到目标电脑" + ms.getDest());
                        FileOutputStream fileOutputStream = new FileOutputStream(ms.getDest());
                        fileOutputStream.write(ms.getFileByte());
                        fileOutputStream.close();
                        System.out.println("\n文件保存成功 ");

服务端

ServerConnectClientThread增加处理的代码

                } else if(message.getMesType().equals(MessageType.MESSAGE_FILE_MES)){
                    ObjectOutputStream objectOutputStream = new ObjectOutputStream(ManageClientThread.getServerConnectClientThread(message.getGetter()).getSocket().getOutputStream());
                    objectOutputStream.writeObject(message);

4.8 服务器推送新闻

其实和群聊的实现是相似的,将把除自己之外的判断给去了就可以了,直接由服务端发送。创建单独的一条线程来处理其他的线程

服务端

专门创建一个线程来推送消息,获得所有的线程,将线程打入所有线程的message当中

SendNewsToAllService

package com.qqservice.sevice;
 
import com.qqclient.utils.Utility;
import com.qqcommon.Message;
import com.qqcommon.MessageType;
 
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
 
public class SendNewsToAllService implements Runnable {
    @Override
    public void run() {
        while (true){
            System.out.println("请输入服务器要推送的内容/消息[exit]退出推送消息");
            String string = Utility.readString(10);
            if ("exit".equals(string)){
                break;
            }
            String news = Utility.readString(100);
            Message message = new Message();
            message.setMesType(MessageType.MESSAGE_TO_ALL_MES);
            message.setSendTime(new Date().toString());
            message.setSender("服务器");
            message.setContent(news);
 
            System.out.println("服务器对所有人说"+news);
 
            HashMap<String, ServerConnectClientThread> hm = ManageClientThread.getHm();
 
            Iterator<String> iterator = hm.keySet().iterator();
            while (iterator.hasNext()){
                String string1 = iterator.next().toString();
                try {
                    ObjectOutputStream objectOutputStream = new ObjectOutputStream(hm.get(string1).getSocket().getOutputStream());
                    objectOutputStream.writeObject(message);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

五、完整代码

5.1 公共代码

Utility

package com.hspedu.qqclient.utils;


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

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

Message

import java.io.Serializable;

/**
 * 表示一个客户信息
 */
public class Message implements Serializable {//序列号进行网络编程

    //增加兼容性,可写可不写
    private static final long serialVersionUID = 1L;

    private String sender;//发送者
    private String getter;//接收者
    private String content;//消息内容
    private String sendTime;//发送时间
    private String mesType;//消息类型[可以在接口定义消息类型]

    //进行扩展 和文件相关的成员
    private byte[] fileBytes;
    private int fileLen = 0;
    private String dest; //将文件传输到哪里
    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 getDest() {
        return dest;
    }

    public void setDest(String dest) {
        this.dest = dest;
    }

    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

import java.io.Serializable;

/**
 * 表示一个用户/客户信息
 */
public class User implements Serializable {

    private static final long serialVersionUID = 1L;
    private String userId;//用户Id/用户名
    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;
    }
}

MessageType 

/**
 * 表示消息类型
 */
public interface MessageType {
    //老师解读
    //1. 在接口中定义了一些常量
    //2. 不同的常量的值,表示不同的消息类型.
    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"; //文件消息(发送文件)
}

 5.2QQClient

ClientConnectServerThread

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

/**
 */
public class ClientConnectServerThread extends Thread {
    //该线程需要持有Socket
    private Socket socket;

    //构造器可以接受一个Socket对象
    public ClientConnectServerThread(Socket socket) {
        this.socket = socket;
    }

    //
    @Override
    public void run() {
        //因为Thread需要在后台和服务器通信,因此我们while循环
        while (true) {

            try {
                System.out.println("客户端线程,等待从读取从服务器端发送的消息");
                ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
                //如果服务器没有发送Message对象,线程会阻塞在这里
                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)) {//如果是文件消息
                    //让用户指定保存路径。。。
                    System.out.println("\n" + message.getSender() + " 给 " + message.getGetter()
                            + " 发文件: " + message.getSrc() + " 到我的电脑的目录 " + message.getDest());

                    //取出message的文件字节数组,通过文件输出流写出到磁盘
                    FileOutputStream fileOutputStream = new FileOutputStream(message.getDest(), true);
                    fileOutputStream.write(message.getFileBytes());
                    fileOutputStream.close();
                    System.out.println("\n 保存文件成功~");

                } else {
                    System.out.println("是其他类型的message, 暂时不处理....");
                }

            } catch (Exception e) {
                e.printStackTrace();
            }

        }

    }

    //为了更方便的得到Socket
    public Socket getSocket() {
        return socket;
    }
}

FileClientService

import java.io.*;

/**
 * 该类/对象完成 文件传输服务
 */
public class FileClientService {
    /**
     *
     * @param src 源文件
     * @param dest 把该文件传输到对方的哪个目录
     * @param senderId 发送用户id
     * @param getterId 接收用户id
     */
    public void sendFileToOne(String src, String dest, String senderId, String getterId) {

        //读取src文件  -->  message
        Message message = new Message();
        message.setMesType(MessageType.MESSAGE_FILE_MES);
        message.setSender(senderId);
        message.setGetter(getterId);
        message.setSrc(src);
        message.setDest(dest);

        //需要将文件读取
        FileInputStream fileInputStream = null;
        byte[] fileBytes = new byte[(int)new File(src).length()];

        try {
            fileInputStream = new FileInputStream(src);
            fileInputStream.read(fileBytes);//将src文件读入到程序的字节数组
            //将文件对应的字节数组设置message
            message.setFileBytes(fileBytes);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //关闭
            if(fileInputStream != null) {
                try {
                    fileInputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        //提示信息
        System.out.println("\n" + senderId + " 给 " + getterId + " 发送文件: " + src
                + " 到对方的电脑的目录 " + dest);
        //发送
        try {
            ObjectOutputStream oos =
                    new ObjectOutputStream(ManageClientConnectServerThread.getClientConnectServerThread(senderId).getSocket().getOutputStream());
            oos.writeObject(message);
        } catch (IOException e) {
            e.printStackTrace();
        }


    }
}

ManageClientConnectServerThread 

import java.util.HashMap;

/**
 * 该类管理客户端连接到服务器端的线程的类
 */
public class ManageClientConnectServerThread {
    //我们把多个线程放入一个HashMap集合,key 就是用户id, value 就是线程
    private static HashMap<String, ClientConnectServerThread> hm = new HashMap<>();

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

}

MessageClientService

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

/**
 * 该类/对象,提供和消息相关的服务方法
 */
public class MessageClientService {


    /**
     * @param content  内容
     * @param senderId 发送者
     */
    public void sendMessageToAll(String content, String senderId) {
        //构建message
        Message message = new Message();
        message.setMesType(MessageType.MESSAGE_TO_ALL_MES);//群发消息这种类型
        message.setSender(senderId);
        message.setContent(content);
        message.setSendTime(new Date().toString());//发送时间设置到message对象
        System.out.println(senderId + " 对大家说 " + content);
        //发送给服务端

        try {
            ObjectOutputStream oos =
                    new ObjectOutputStream(ManageClientConnectServerThread.getClientConnectServerThread(senderId).getSocket().getOutputStream());
            oos.writeObject(message);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * @param content  内容
     * @param senderId 发送用户id
     * @param getterId 接收用户id
     */
    public void sendMessageToOne(String content, String senderId, String getterId) {
        //构建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());//发送时间设置到message对象
        System.out.println(senderId + " 对 " + getterId + " 说 " + content);
        //发送给服务端

        try {
            ObjectOutputStream oos =
                    new ObjectOutputStream(ManageClientConnectServerThread.getClientConnectServerThread(senderId).getSocket().getOutputStream());
            oos.writeObject(message);
        } catch (IOException e) {
            e.printStackTrace();
        }

    }
}

UserClientService

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

/**
 * 该类完成用户登录验证和用户注册等功能.
 */
public class UserClientService {

    //因为我们可能在其他地方用使用user信息, 因此作出成员属性
    private User u = new User();
    //因为Socket在其它地方也可能使用,因此作出属性
    private Socket socket;

    //根据userId 和 pwd 到服务器验证该用户是否合法
    public boolean checkUser(String userId, String pwd) {
        boolean b = false;
        //创建User对象
        u.setUserId(userId);
        u.setPasswd(pwd);


        try {
            //连接到服务端,发送u对象
            socket = new Socket(InetAddress.getByName("XXXX"), 9999);
            //得到ObjectOutputStream对象
            ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
            oos.writeObject(u);//发送User对象

            //读取从服务器回复的Message对象
            ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
            Message ms = (Message) ois.readObject();

            if (ms.getMesType().equals(MessageType.MESSAGE_LOGIN_SUCCEED)) {//登录OK


                //创建一个和服务器端保持通信的线程-> 创建一个类 ClientConnectServerThread
                ClientConnectServerThread clientConnectServerThread =
                        new ClientConnectServerThread(socket);
                //启动客户端的线程
                clientConnectServerThread.start();
                //这里为了后面客户端的扩展,我们将线程放入到集合管理
                ManageClientConnectServerThread.addClientConnectServerThread(userId, clientConnectServerThread);
                b = true;
            } else {
                //如果登录失败, 我们就不能启动和服务器通信的线程, 关闭socket
                socket.close();
            }

        } catch (Exception e) {
            e.printStackTrace();
        }

        return b;

    }

    //向服务器端请求在线用户列表
    public void onlineFriendList() {

        //发送一个Message , 类型MESSAGE_GET_ONLINE_FRIEND
        Message message = new Message();
        message.setMesType(MessageType.MESSAGE_GET_ONLINE_FRIEND);
        message.setSender(u.getUserId());

        //发送给服务器

        try {
            //从管理线程的集合中,通过userId, 得到这个线程对象
            ClientConnectServerThread clientConnectServerThread =
                    ManageClientConnectServerThread.getClientConnectServerThread(u.getUserId());
            //通过这个线程得到关联的socket
            Socket socket = clientConnectServerThread.getSocket();
            //得到当前线程的Socket 对应的 ObjectOutputStream对象
            ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
            oos.writeObject(message); //发送一个Message对象,向服务端要求在线用户列表
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

    //编写方法,退出客户端,并给服务端发送一个退出系统的message对象
    public void logout() {
        Message message = new Message();
        message.setMesType(MessageType.MESSAGE_CLIENT_EXIT);
        message.setSender(u.getUserId());//一定要指定我是哪个客户端id

        //发送message
        try {
            //ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
            ObjectOutputStream oos =
                    new ObjectOutputStream(ManageClientConnectServerThread.getClientConnectServerThread(u.getUserId()).getSocket().getOutputStream());
            oos.writeObject(message);
            System.out.println(u.getUserId() + " 退出系统 ");
            System.exit(0);//结束进程
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

QQView 

/**
 * 客户端的菜单界面
 */
public class QQView {

    private boolean loop = true; //控制是否显示菜单
    private String key = ""; // 接收用户的键盘输入
    private UserClientService userClientService = new UserClientService();//对象是用于登录服务/注册用户
    private MessageClientService messageClientService = new MessageClientService();//对象用户私聊/群聊.
    private FileClientService fileClientService = new FileClientService();//该对象用户传输文件
    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 1 登录系统");
            System.out.println("\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);
                    //这里就比较麻烦了, 需要到服务端去验证该用户是否合法
                    //这里有很多代码, 我们这里编写一个类 UserClientService[用户登录/注册]
                    if (userClientService.checkUser(userId, pwd)) { //还没有写完, 先把整个逻辑打通....
                        System.out.println("===========欢迎 (用户 " + userId + " 登录成功) ===========");
                        //进入到二级菜单
                        while (loop) {
                            System.out.println("\n=========网络通信系统二级菜单(用户 " + userId + " )=======");
                            System.out.println("\t\t 1 显示在线用户列表");
                            System.out.println("\t\t 2 群发消息");
                            System.out.println("\t\t 3 私聊消息");
                            System.out.println("\t\t 4 发送文件");
                            System.out.println("\t\t 9 退出系统");
                            System.out.print("请输入你的选择: ");
                            key = Utility.readString(1);
                            switch (key) {
                                case "1":
                                    //这里老师准备写一个方法,来获取在线用户列表
                                    userClientService.onlineFriendList();
                                    break;
                                case "2":
                                    System.out.println("请输入想对大家说的话: ");
                                    String s = Utility.readString(100);
                                    messageClientService.sendMessageToAll(s, userId);
                                    break;
                                case "3":
                                    System.out.print("请输入想聊天的用户号(在线): ");
                                    String getterId = Utility.readString(50);
                                    System.out.print("请输入想说的话: ");
                                    String content = Utility.readString(100);
                                    //编写一个方法,将消息发送给服务器端
                                    messageClientService.sendMessageToOne(content, userId, getterId);
                                    break;
                                case "4":
                                    System.out.print("请输入你想把文件发送给的用户(在线用户): ");
                                    getterId = Utility.readString(50);
                                    System.out.print("请输入发送文件的路径(形式 d:\\xx.jpg)");
                                    String src = Utility.readString(100);
                                    System.out.print("请输入把文件发送到对应的路径(形式 d:\\yy.jpg)");
                                    String dest = Utility.readString(100);
                                    fileClientService.sendFileToOne(src,dest,userId,getterId);
                                    break;
                                case "9":
                                    //调用方法,给服务器发送一个退出系统的message
                                    userClientService.logout();
                                    loop = false;
                                    break;
                            }

                        }
                    } else { //登录服务器失败
                        System.out.println("=========登录失败=========");
                    }
                    break;
                case "9":
                    loop = false;
                    break;
            }

        }

    }
}

5.3 QQServer

ManageClientThreads 

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

/**
 * 该类用于管理和客户端通信的线程
 */
public class ManageClientThreads {
    private static HashMap<String, ServerConnectClientThread> hm = new HashMap<>();


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

    //添加线程对象到 hm 集合
    public static void addClientThread(String userId, ServerConnectClientThread serverConnectClientThread) {

        hm.put(userId, serverConnectClientThread);

    }

    //根据userId 返回ServerConnectClientThread线程
    public static ServerConnectClientThread getServerConnectClientThread(String userId) {
        return hm.get(userId);
    }

    //增加一个方法,从集合中,移除某个线程对象
    public static void removeServerConnectClientThread(String userId) {
        hm.remove(userId);
    }

    //这里编写方法,可以返回在线用户列表
    public static String getOnlineUser() {
        //集合遍历 ,遍历 hashmap的key
        Iterator<String> iterator = hm.keySet().iterator();
        String onlineUserList = "";
        while (iterator.hasNext()) {
            onlineUserList += iterator.next().toString() + " ";
        }
        return  onlineUserList;
    }
}

QQServer 

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 这是服务器, 在监听9999,等待客户端的连接,并保持通信
 */
public class QQServer {

    private ServerSocket ss = null;
    //创建一个集合,存放多个用户,如果是这些用户登录,就认为是合法
    //这里我们也可以使用 ConcurrentHashMap, 可以处理并发的集合,没有线程安全
    //HashMap 没有处理线程安全,因此在多线程情况下是不安全
    //ConcurrentHashMap 处理的线程安全,即线程同步处理, 在多线程情况下是安全
    private static ConcurrentHashMap<String, User> validUsers = new ConcurrentHashMap<>();
    //private static ConcurrentHashMap<String, ArrayList<Message>> offLineDb = new ConcurrentHashMap<>();

    static { //在静态代码块,初始化 validUsers

        validUsers.put("100", new User("100", "123456"));
        validUsers.put("200", new User("200", "123456"));
        validUsers.put("300", new User("300", "123456"));
        validUsers.put("至尊宝宝", new User("至尊宝", "123456"));
        validUsers.put("紫霞姐姐", new User("紫霞仙子", "123456"));
        validUsers.put("菩提老头", new User("菩提老祖", "123456"));

    }

    //验证用户是否有效的方法
    private boolean checkUser(String userId, String passwd) {

        User user = validUsers.get(userId);
        //过关的验证方式
        if(user == null) {//说明userId没有存在validUsers 的key中
            return  false;
        }
        if(!user.getPasswd().equals(passwd)) {//userId正确,但是密码错误
            return false;
        }
        return  true;
    }

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

            while (true) { //当和某个客户端连接后,会继续监听, 因此while
                Socket socket = ss.accept();//如果没有客户端连接,就会阻塞在这里
                //得到socket关联的对象输入流
                ObjectInputStream ois =
                        new ObjectInputStream(socket.getInputStream());

                //得到socket关联的对象输出流
                ObjectOutputStream oos =
                        new ObjectOutputStream(socket.getOutputStream());
                User u = (User) ois.readObject();//读取客户端发送的User对象
                //创建一个Message对象,准备回复客户端
                Message message = new Message();
                //验证用户 方法
                if (checkUser(u.getUserId(), u.getPasswd())) {//登录通过
                    message.setMesType(MessageType.MESSAGE_LOGIN_SUCCEED);
                    //将message对象回复客户端
                    oos.writeObject(message);
                    //创建一个线程,和客户端保持通信, 该线程需要持有socket对象
                    ServerConnectClientThread serverConnectClientThread =
                            new ServerConnectClientThread(socket, u.getUserId());
                    //启动该线程
                    serverConnectClientThread.start();
                    //把该线程对象,放入到一个集合中,进行管理.
                    ManageClientThreads.addClientThread(u.getUserId(), serverConnectClientThread);

                } else { // 登录失败
                    System.out.println("用户 id=" + u.getUserId() + " pwd=" + u.getPasswd() + " 验证失败");
                    message.setMesType(MessageType.MESSAGE_LOGIN_FAIL);
                    oos.writeObject(message);
                    //关闭socket
                    socket.close();
                }
            }

        } catch (Exception e) {
            e.printStackTrace();
        } finally {

            //如果服务器退出了while,说明服务器端不在监听,因此关闭ServerSocket
            try {
                ss.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

 SendNewsToAllService 

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

/**
 */
public class SendNewsToAllService implements Runnable {


    @Override
    public void run() {

        //为了可以推送多次新闻,使用while
        while (true) {
            System.out.println("请输入服务器要推送的新闻/消息[输入exit表示退出推送服务线程]");
            String news = Utility.readString(100);
            if("exit".equals(news)) {
                break;
            }
            //构建一个消息 , 群发消息
            Message message = new Message();
            message.setSender("服务器");
            message.setMesType(MessageType.MESSAGE_TO_ALL_MES);
            message.setContent(news);
            message.setSendTime(new Date().toString());
            System.out.println("服务器推送消息给所有人 说: " + news);

            //遍历当前所有的通信线程,得到socket,并发送message

            HashMap<String, ServerConnectClientThread> hm = ManageClientThreads.getHm();

            Iterator<String> iterator = hm.keySet().iterator();
            while (iterator.hasNext()) {
                String onLineUserId = iterator.next().toString();
                try {
                    ObjectOutputStream oos =
                            new ObjectOutputStream(hm.get(onLineUserId).getSocket().getOutputStream());
                    oos.writeObject(message);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

    }
}

ServerConnectClientThread 

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() { //这里线程处于run的状态,可以发送/接收消息

        while (true) {
            try {
                System.out.println("服务端和客户端" + userId + " 保持通信,读取数据...");
                ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
                Message message = (Message) ois.readObject();
                //后面会使用message, 根据message的类型,做相应的业务处理
                if (message.getMesType().equals(MessageType.MESSAGE_GET_ONLINE_FRIEND)) {
                    //客户端要在线用户列表
                    /*
                    在线用户列表形式 100  200  紫霞仙子
                     */
                    System.out.println(message.getSender() + " 要在线用户列表");
                    String onlineUser = ManageClientThreads.getOnlineUser();
                    //返回message
                    //构建一个Message 对象,返回给客户端
                    Message message2 = new Message();
                    message2.setMesType(MessageType.MESSAGE_RET_ONLINE_FRIEND);
                    message2.setContent(onlineUser);
                    message2.setGetter(message.getSender());
                    //返回给客户端
                    ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
                    oos.writeObject(message2);

                } else if (message.getMesType().equals(MessageType.MESSAGE_COMM_MES)) {
                    //根据message获取getter id, 然后在得到对应先线程
                    ServerConnectClientThread serverConnectClientThread =
                            ManageClientThreads.getServerConnectClientThread(message.getGetter());
                    //得到对应socket的对象输出流,将message对象转发给指定的客户端
                    ObjectOutputStream oos =
                            new ObjectOutputStream(serverConnectClientThread.getSocket().getOutputStream());
                    oos.writeObject(message);//转发,提示如果客户不在线,可以保存到数据库,这样就可以实现离线留言

                } else if (message.getMesType().equals(MessageType.MESSAGE_TO_ALL_MES)) {
                    //需要遍历 管理线程的集合,把所有的线程的socket得到,然后把message进行转发即可
                    HashMap<String, ServerConnectClientThread> hm = ManageClientThreads.getHm();

                    Iterator<String> iterator = hm.keySet().iterator();
                    while (iterator.hasNext()) {

                        //取出在线用户id
                        String onLineUserId = iterator.next().toString();

                        if (!onLineUserId.equals(message.getSender())) {//排除群发消息的这个用户

                            //进行转发message
                            ObjectOutputStream oos =
                                    new ObjectOutputStream(hm.get(onLineUserId).getSocket().getOutputStream());
                            oos.writeObject(message);
                        }

                    }

                } else if (message.getMesType().equals(MessageType.MESSAGE_FILE_MES)) {
                    //根据getter id 获取到对应的线程,将message对象转发
                    ObjectOutputStream oos =
                            new ObjectOutputStream(ManageClientThreads.getServerConnectClientThread(message.getGetter()).getSocket().getOutputStream());
                    //转发
                    oos.writeObject(message);
                } else if (message.getMesType().equals(MessageType.MESSAGE_CLIENT_EXIT)) {//客户端退出

                    System.out.println(message.getSender() + " 退出");
                    //将这个客户端对应线程,从集合删除.
                    ManageClientThreads.removeServerConnectClientThread(message.getSender());
                    socket.close();//关闭连接
                    //退出线程
                    break;

                } else {
                    System.out.println("其他类型的message , 暂时不处理");
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }


    }
}

QQFrame 

/**
 * 该类创建QQServer ,启动后台的服务
 */
public class QQFrame {
    public static void main(String[] args)  {
        new QQServer();
    }
}

5.4 获取id

import java.net.InetAddress;
import java.net.UnknownHostException;

/**
 */
public class get {
    public static void main(String[] args) throws UnknownHostException {
        InetAddress localHost = InetAddress.getLocalHost();
        System.out.println(localHost);
    }
}

  • 1
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值