目录

一。项目前的准备

1.为什么选择这个项目

2.项目开发的流程

3.项目的需求

二。开发阶段

1.登录功能实现

2.拉取在线用户实现

3.无异常退出的实现

4.私聊功能的实现

5.群发的实现

6.发送文件的实现

7.服务端推送新闻

三。课后作业

1.离线留言功能的实现代码

2.离线文件发送实现


一。项目前的准备

1.为什么选择这个项目

没有Mysql所以使用一个集合来存储信息

UI没有刻意写,界面写的是控制台

2.项目开发的流程

1.需求阶段

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

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

2.设计阶段

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

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

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

3.实现阶段

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

4.测试阶段

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

5.部署阶段

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

6.维护阶段

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

图示

项目越大设计阶段花的时间越多

3.项目的需求

1.图示

2.界面设计

用户登录

拉取在线用户列表

私聊

 群聊

发文件

服务器推送新闻

推图片,文件和推文字的基本原理是相同的

3.通信功能的整体分析

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

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

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

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

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

图例

二。开发阶段

1.登录功能实现

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

User:用于存储用户账号密码信息的

package com.qqcommon;import java.io.Serializable;public class User implements Serializable {private static final long serialVersionUID = 1l;private String userId; //用户Idprivate String Password; //用户密码public User() {}public User(String userId, String password) {this.userId = userId;Password = password;}public static long getSerialVersionUID() {return serialVersionUID;}public String getUserId() {return userId;}public void setUserId(String userId) {this.userId = userId;}public String getPassword() {return Password;}public void setPassword(String password) {Password = password;}@Overridepublic String toString() {return "User{" +"userId='" + userId + '\'' +", Password='" + Password + '\'' +'}';}
}

Message:用于存放用户信息

package com.qqcommon;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[] fileByte;private int fileLen =0;private String dest;//文件输入地址private String src;//文件输出地址public byte[] getFileByte() {return fileByte;}public void setFileByte(byte[] fileByte) {this.fileByte = fileByte;}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 Message() {}public Message(String sender, String getter, String content, String sendTime, String mesType) {this.sender = sender;this.getter = getter;this.content = content;this.sendTime = sendTime;this.mesType = mesType;}public static long getSerialVersionUID() {return serialVersionUID;}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;}public String getMesType() {return mesType;}public void setMesType(String mesType) {this.mesType = mesType;}
}

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

package com.qqcommon;public interface MessageType {String MESSAGE_LOGIN_SUCCESS = "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";//发送文件String MESSAGE_GET_NOT_ONLINE_MES = "9"; //接收离线消息String MESSAGE_SET_NOT_ONLINE_MES = "10"; //发送离线消息
}
//可以扩展功能

Utility类

package com.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=>NString 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;}
}

提示:这三个类加一个接口服务端和客户端都是要有的

2。创建启动类

服务端:

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

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

客户端:

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

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

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

验证失败就将socket关闭,并返回false

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 所以创建ClientConnectServerThreadClientConnectServerThread 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 {//改线程需要持有Socketprivate Socket socket;//构造器可以接受一个socket对象public ClientConnectServerThread(Socket socket) {this.socket = socket;}@Overridepublic void run() {//Thread需要在后台与服务器进行通信,持续通信所以whilewhile (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();}}}//  为了更方便的得到socketpublic 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  记得最后关闭socket

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

闲言碎语:线程类是用来直接判断信息类型,然后对信息进行处理返回的。可以调用服务端的其他类的其他方法来实现信息的处理。线程就像是链接服务端和客户端的桥梁,实际上是本质是socket

2.拉取在线用户实现

基本的框架已经搭建完成,接下来就是功能的扩展了

思路分析

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

实现

客户端:

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

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

    //向服务器端请求在线用户列表public void onlineFriendList(){Message message = new Message();//将信息类型放入message.setMesType(MessageType.MESSAGE_GET_ONLINE_FRIEND);
//        将发送请求者的信息放入message.setSender(user.getUserId());//得到一个线程,将数据放到线程当中try {//得到一个对象输出流ObjectOutputStream objectOutputStream = new ObjectOutputStream(ManageClientConnectServerThread.getClientConnectServerThread(user.getUserId()).getSocket().getOutputStream());objectOutputStream.writeObject(message);} 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;

3.无异常退出的实现

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

图示

实现:

客户端:

在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.私聊功能的实现

思路

图示

客户端:

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)){
//                    注意,这里变成了getterServerConnectClientThread serverConnectClientThread = ManageClientThread.getServerConnectClientThread(message.getGetter());ObjectOutputStream objectOutputStream = new ObjectOutputStream(serverConnectClientThread.socket.getOutputStream());objectOutputStream.writeObject(message);

5.群发的实现

思路

首先要获得在线用户列表,然后将消息发送给出自己之外的所有人。

服务端:

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

6.发送文件的实现

首先将文件变为byte,然后再

如图

服务端

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);
//      将文件放入messageFileInputStream 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);

7.服务端推送新闻

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

如图

服务端

专门创建一个线程来推送消息,获得所有的线程,将线程打入所有线程的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 {@Overridepublic 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();}}}}
}

三。课后作业

1.离线留言功能的实现代码

参考链接

参考

服务器端创建一个OffLineMessageService类用于存放留言信息。

package com.qqservice.sevice;import com.qqcommon.Message;import java.io.IOException;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.concurrent.ConcurrentHashMap;/*** @author 晓宇* @version 1.0* @Time: 2022/7/14  10:31*/
@SuppressWarnings({"all"})
public class OffLineMessageService {//key->getterId value->contentsprivate static ConcurrentHashMap<String, ArrayList> offlineMap=new ConcurrentHashMap();public static ConcurrentHashMap<String, ArrayList> getOfflineMap() {return offlineMap;}public static void setOfflineMap(ConcurrentHashMap<String, ArrayList> offlineMap) {OffLineMessageService.offlineMap = offlineMap;}//将离线消息存入到集合中//编写getter不在线 并将离线消息添加到offlineMappublic static void addOfflineMap(Message message){//如果getter不存在 那么创建一个ArrayList 并将message放入if(!offlineMap.containsKey(message.getGetter())){ArrayList<Message> messages = new ArrayList<>();messages.add(message);offlineMap.put(message.getGetter(),messages);}else {ArrayList arrayList = offlineMap.get(message.getGetter());arrayList.add(message);}System.out.println("离线消息已经存放在offlineMap中");}//编写方法判断user是否存在于offlineMap中 如果存在 就获取对应getter的socket 将ArrayList中的所有内容发送public static void sendOfflineMessage(String userId,ConcurrentHashMap offlineMap){if (offlineMap.containsKey(userId)){try {ArrayList<Message> arrayList = (ArrayList<Message>) offlineMap.get(userId);OutputStream os = ManageClientThread.getServerConnectClientThread(userId).getSocket().getOutputStream();ObjectOutputStream oos = new ObjectOutputStream(os);//将message集合发送到客户端oos.writeObject(arrayList);System.out.println("发送成功");} catch (IOException e) {e.printStackTrace();}}else System.out.println("发送失败");}//编写方法判断getter是否在线public static boolean isOnline(String getterId){HashMap<String, ServerConnectClientThread> hm = ManageClientThread.getHm();return hm.containsKey(getterId);}//将离线消息从offlineMap删除public static void deleteOfflineMessage(String getterId){ArrayList remove = offlineMap.remove(getterId);System.out.println("删除消息成功"+remove);}
}

ServerConnectClientThread类

                }else if (message.getMesType().equals(MessageType.MESSAGE_COMM_MES)){//写一个方法 判断getter是否在线 不在线的话 将message对象存入到集合中if (!OffLineMessageService.isOnline(message.getGetter())){System.out.println("该用户不在线 将在登录后接受到消息");OffLineMessageService.addOfflineMap(message);} else {ServerConnectClientThread serverConnectClientThread = ManageClientThread.getServerConnectClientThread(message.getGetter());ObjectOutputStream objectOutputStream = new ObjectOutputStream(serverConnectClientThread.socket.getOutputStream());objectOutputStream.writeObject(message);}

ClientConnectServerThread类循环开始先判断用户有没有离线消息

        while (true) {try {System.out.println("客户端线程等待,读取服务器端发送的消息。");
//                得到输入流ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());//处理读入的对象Object o = ois.readObject();//如果服务器没有发送objec对象,线程会阻塞在这里if (o instanceof ArrayList) {ArrayList<Message> messageArrayList = (ArrayList<Message>) o;System.out.println("以下是您的离线消息:⬇");for (Message message : messageArrayList) {System.out.println(message.getSender() + ": " + message.getContent() + " time:"+message.getSendTime());}

2.离线文件发送实现

和普通的文件发送很像,只是多了个中间的存储过程,参考了留言功能的实现

服务端
OffLineMessageService类当中新增加了一个方法,其实除了方法名什么都没换,因为存储的是message对象,对象中不仅包括了文件byte数据,还包括了留言数据。

    public static void addFileMap(Message message){//如果getter不存在 那么创建一个ArrayList 并将message放入if(!offlineMap.containsKey(message.getGetter())){ArrayList<Message> messages = new ArrayList<>();messages.add(message);offlineMap.put(message.getGetter(),messages);}else {ArrayList arrayList = offlineMap.get(message.getGetter());arrayList.add(message);}System.out.println("离线文件已经存放在offlineMap中");}

对正常的文件传输进行一个判断

ClientConnectServerThread类

                } else if(message.getMesType().equals(MessageType.MESSAGE_FILE_MES)){
//                   如果消息的接受者不在if (!OffLineMessageService.isOnline(message.getGetter())){System.out.println("该用户不在线 将在登录后接受到消息");OffLineMessageService.addFileMap(message);} else {ObjectOutputStream objectOutputStream = new ObjectOutputStream(ManageClientThread.getServerConnectClientThread(message.getGetter()).getSocket().getOutputStream());objectOutputStream.writeObject(message);}

客户端

也是对原有的代码进行更改,

ClientConnectServerThread类

                if (o instanceof ArrayList) {ArrayList<Message> messageArrayList = (ArrayList<Message>) o;System.out.println("以下是您的离线消息:⬇");for (Message message : messageArrayList) {System.out.println(message.getSender() + ": " + message.getContent() + " time:"+message.getSendTime());
//                        增加了个判断,如果是文件类的消息就输出文件if (message.getMesType().equals(MessageType.MESSAGE_FILE_MES)){System.out.println(message.getSender() + "给" + message.getGetter() + "发送" + message.getSrc() + "到目标电脑" + message.getDest());FileOutputStream fileOutputStream = new FileOutputStream(message.getDest());fileOutputStream.write(message.getFileByte());fileOutputStream.close();System.out.println("\n文件保存成功 ");}}

都是对原有的功能进行了扩展,不管是留言还是文件都不用等对方在线了,首次写这么大的项目,看的头疼,想该出一点真正属于自己想法的东西,但能力不足,总是想复杂或者想简单了,想的不具体,还是代码功底不够扎实。加油学习。

韩顺平Java自学笔记 项目 QQ聊天室相关推荐

  1. 韩顺平Java自学笔记 项目 房屋出租

    目录 一.需求分析 1.需求说明 2.界面要求 ​编辑 3.系统设计 4.功能实现的顺序 二.使用到的类 1.工具类(Utility) 2.House类 3.HouseView类 4.HouseRen ...

  2. 韩顺平Java自学笔记 反射

    一.反射入门案例 目录 一.反射入门案例 二.反射的机制 1.反射的原理 2.反射相关的类 3.反射的优点和缺点 三.Class类详解 1.Class类的特征 2.Class的常用方法 3.获取Cla ...

  3. 韩顺平Java自学笔记 线程

    目录 一.并发和并行 二.有关于线程的基本原理 1.第一种创建线程的方式 2.多线程机制 3.第二种创建线程的方式 四.创建线程后的操作 1.线程终止 2.线程的中断 3.线程插队 4.守护线程 五. ...

  4. java项目qq聊天室_简单的qq聊天室项目(作者涂俊伟)

    项目总结 经过这10天的项目实训使我们受益匪浅.虽然其中遇到了很多困难,在写程序 中是写一个函数,就调试一次,看看 有没有问题,再继续往下写,这样减轻了后面调试的压力. 项目过程: 1.构思,聊天室的 ...

  5. Java JDBC和数据库连接池 韩顺平老师自学笔记

    JDBC和数据库连接池 JDBC 概述 基本介绍 原理示意图 代码示例 JdbcInterface 模拟Java公司提供给其它数据库厂商的接口,供给调用 TestJdbc 模拟一个类来实现数据库的调用 ...

  6. QQ丐版(韩顺平Java多用户通信项目)

    客户端: package Client; import publi.*; import publi.send_res; import publi.user; import java.io.*; imp ...

  7. B站韩顺平java学习笔记(八)-- 房屋出租系统(项目)章节

    目录 一 项目需求说明 1 项目界面 二  房屋租赁程序框架图 ​三  系统实现 1  完成House类 2  显示主菜单和完成退出软件的功能 3  完成显示房屋列表的功能 4  添加房屋信息的功能 ...

  8. B站韩顺平java学习笔记(三)--运算符章节

    目录 一.算术运算符 1.  表格 2.  练习题: 二. 关系运算符 1.  表格 三.逻辑运算符 1.  表格 2.  练习题 四.赋值运算符 1.  介绍 2.细节特点 五.三元运算符 1.  ...

  9. B站韩顺平java学习笔记(六)--面向对象编程章节

    目录 一.类和对象 1.定义及区别 2.对象内存布局 3.属性/成员变量 (1)基本介绍 (2)注意事项和细节说明 (3)访问属性 4.对象 (1)创建对象 (2)对象分配机制 ​(3)Java创建对 ...

最新文章

  1. 一个北漂女孩-不想嫁给有钱人
  2. Atlassian In Action - (Atlassian成长之路)
  3. 【杂谈】想成为机器学习学霸?先学会做笔记吧
  4. 我也来记录我的一些开发心得和笔记!
  5. ue4 设置intellisence_UE4.22编辑器界面操控设置(4)
  6. 利用线性代数的方法求斐波那契数列的通项
  7. js 用下标获取map值_js map方法处理返回数据,获取指定数据简写方法
  8. Linux应用基本命令实验,实验二 linux基本命令的使用
  9. 【Android笔记】WebView的使用
  10. SharePoint【Site Definition 系列】-- 04. 相对快捷地创建List Definition的Schema.xml文件
  11. JSON解析方式 gson
  12. HTML5 Canvas 和 SVG
  13. html入门的常见问题
  14. J2EE快速入门之集合框架【01】
  15. windows-sys:21:windows系统(win7 win10 win11)设置护眼色详细过程
  16. 网络传输的七层协议(包括tcp协议和udp协议的区别)
  17. 下载频道2013上半年超人气精华资源汇总---全都是免积分下载
  18. 【c++】单词接龙(暴搜)
  19. 用python输出沙漏_sandglass(沙漏)——一个让人解脱的python时间处理库
  20. Android监听是否点击了home键或者锁屏键

热门文章

  1. 把文件夹名字全部变成大写
  2. commvault备份mysql数据库_2-CommVault备份项目实施方案-XXXX.docx
  3. NLP入门--Word2Vec(CBOW)实战
  4. 数字图像处理,图像的伪彩色处理
  5. 微信开网页mysql_PC浏览器模拟微信打开网页
  6. 如何驯服事件驱动的微服务
  7. java毕业设计_基于web的医院信息管理系统
  8. MySQL5.7忘记密码怎么办
  9. MySql学习-基础篇
  10. 如何正确使用移动硬盘