文章目录
- 1.项目所用技术栈
- 本项目使用了java基础,面向对象,集合,泛型,IO流,多线程,Tcp字节流编程的技术
- 2.通信系统整体分析
- 主要思路(自己理解)
- 1.如果不用多线程
- 2.使用多线程
- 3.对多线程的新理解
- 3.功能实现——用户登录
- 1.实现传输数据的三个类Message和User和MessageType
- 1.首先创建两个模块QQSever和QQClient
- 2.完成两个模块共有类的编写
- 2.实现用户登录界面框架
- 1.导入工具类utils/Utility.java
- 2.编写基本用户界面view/QQView.java
- 3.实现客户端的登录部分
- 1.qqclient/service/UserClientService.java
- 2.qqclient/service/ManageClientConnectServerThread.java
- 3.qqclient/service/ClientConnectServerThread.java
- 4.修改QQView.java中的验证用户是否合法语句
- 4.实现服务器端的登录部分
- 1.qqserver/service/QQServer.java
- 2.qqserver/service/ServerConnectClientThread.java
- 5.登录阶段运行调试过程
- 1.第一次运行,报错!(用户名密码正确时)
- 解决方法
- 2.第二次运行,报错!(用户名密码不正确时)
- 原因
- 解决方法
- 6.实现多个合法用户可以登录
- qqserver/service/QQServer.java更新
- 4.功能实现——拉取在线用户
- 1.功能完成
- 1.qqcommon/MessageType.java更新
- 2.qqclient/service/ClientConnectServerThread.java更新
- 3.qqclient/service/UserClientService.java更新
- 4.view/QQView.java更新
- 5.qqserver/service/ManageClientThreads.java更新
- 添加方法
- 6.qqserver/service/QQServer.java更新
- 添加方法
- 7.qqserver/service/ServerConnectClientThread.java更新
- try语句更新
- 2.调试阶段
- 1.代码冗余
- 2.线程同步问题
- 5.功能实现——无异常退出系统
- 1.功能完成
- 1.qqcommon/MessageType.java更新
- 2.qqclient/service/ClientConnectServerThread.java更新
- try语句更新
- 3.qqclient/service/UserClientService.java更新
- 添加三个方法
- 4.view/QQView.java更新
- 5.qqserver/service/QQServer.java更新
- 添加两个方法
- 6.qqserver/service/ManageClientThreads.java更新
- 添加方法
- 7.qqserver/service/ServerConnectClientThread.java更新
- 2.调试阶段
- 1.出现空指针异常
- 2.数据未同步
- 3.安全性提升
- 6.功能实现——私聊功能
- 1.功能完成
- 1.qqclient/service/ClientConnectServerThread.java更新
- 2.qqclient/service/UserClientService.java更新
- 添加方法
- 3.view/QQView.java更新
- 4.qqserver/service/QQServer.java更新
- 添加方法
- 5.qqserver/service/ServerConnectClientThread.java更新
- 2.调试阶段
- 并未发现错误
- 7.功能实现——群发功能
- 1.功能完成
- 1.qqcommon/MessageType.java更新
- 2.qqclient/service/ClientConnectServerThread.java更新
- 3.qqclient/service/UserClientServer.java更新
- 添加方法
- 4.qqserver/service/QQServer.java更新
- 5.qqserver/service/QQServer.java更新
- 添加方法
- 6.qqserver/service/ServerConnectClientThread.java更新
- 2.调试阶段
- 未发现错误
- 8.功能实现——发文件
- 1.功能完成
- 1.qqcommon/MessageType.java更新
- 2.qqcommon/Message.java更新
- 3.qqclient/service/ClientConnectServerThread.java更新
- 4.qqclient/service/UserClientServer.java更新
- 添加方法
- 5.view/QQView.java更新
- 6.qqserver/service/ServerConnectClientThread.java更新
- 2.调试阶段
- 1.传输文件大小膨胀
- 9.功能实现——服务器端推送新闻
- 1.功能完成
- 1.qqserver/service/SendAllThread.java
- 2.qqserver/service/ServerConnectClientThread.java更新
- 2.调试阶段
- 1.子线程群发问题
1.项目所用技术栈
本项目使用了java基础,面向对象,集合,泛型,IO流,多线程,Tcp字节流编程的技术
2.通信系统整体分析
主要思路(自己理解)
1.如果不用多线程
- 客户端A连接并发送消息:服务端B通过
accept
方法接受客户端A的连接,然后读取数据。 - 服务端处理并响应:服务端B处理客户端A的数据,发送响应,然后继续监听新的消息或关闭连接。如果服务器继续监听来自A的数据,它将继续阻塞在读操作上。
- 客户端A不再发送数据:如果客户端A在发送了一些数据之后停止发送,并且服务器端正在等待读取更多数据,这时服务端将阻塞在对A的读操作上,因为它正在等待A发送更多数据。
- 客户端B尝试连接:由于服务端B正在处理客户端A的连接并阻塞在读操作上,它无法接受客户端B的连接请求。直到服务端B处理完A的请求并返回到
accept
方法,客户端B才能连接。
2.使用多线程
- 客户端A向服务器端B建立连接,连接成功,客户端A和服务器端各自有一个socket
- 客户端A向服务器发送User对象(包含用户名和密码),服务器端获取内容并验证,验证结束之后将结果返回给客户端A
- 客户端A收到结果之后,如果登录成功,则开启一个子线程,将socket放进去,使得子线程能够对其进行操作,然后子线程一直读取通道中的信息,如果没有信息则会阻塞。而主线程则会继续执行界面的操作,两者互不干涉
- 此时服务器端则会也开启一个线程,将socket放到线程中,然后持续读取与客户端A通道中的信息,以执行特定的操作,然后服务器端的主线程会继续进行监听,如果有其他的客户端链接则直接连接上
- 此时客户端B链接服务器端,服务器端提供链接并且验证User,如果正确则服务器端再开一个线程执行跟上面同样的操作,而主线程依然继续监听,这样就实现了多用户连接。
3.对多线程的新理解
- 多线程就相当于一个独立于主线程之外,可以运行的实例中的run方法
- 主线程可以实例化为多个子线程,然后调用run方法,对当前实例进行操作
- 当仅仅靠主线程无法实现目标时就要使用多线程并发执行,单独开一个线程,执行特定的任务
- 多线程的设计,首先要明确这个线程要完成什么功能,需要给他传递什么属性,然后就可以开始设计这个单线程,最后还要考虑这个线程是不是要并发执行,如果要并发执行,则就要考虑,对象锁或者类锁实现同步
3.功能实现——用户登录
1.实现传输数据的三个类Message和User和MessageType
1.首先创建两个模块QQSever和QQClient
2.完成两个模块共有类的编写
-
qqcommon/Message.java
package qqcommon;import java.io.Serializable;/*** @author 孙显圣* @version 1.0* 表示客户端和服务器端通讯时的消息对象*/ public class Message implements Serializable { //也需要进行序列化private String sender; //发送者private String getter; //接受者private String content; //消息内容private String sendTime; //发送时间private String 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;}public String getMesType() {return mesType;}public void setMesType(String mesType) {this.mesType = mesType;} }
-
qqcommon/MessageType.java
package qqcommon;/*** @author 孙显圣* @version 1.0*/ public interface MessageType {//在接口中定义了不同的常量//不同常量的值表示不同的消息类型String MESSAGE_LOGIN_SUCCEED = "1"; //表示登录成功String MESSAGE_LOGIN_FAIL = "2"; //表示登录失败 }
-
qqcommon/User.java
package qqcommon;import java.io.Serializable;/*** @author 孙显圣* @version 1.0* 表示一个用户/客户信息*/ public class User implements Serializable { //由于需要序列化所以需要实现接口private String userId; //用户名private String passwd; //密码public User() {}public User(String userId, String passwd) {this.userId = userId;this.passwd = passwd;}public String getUserId() {return userId;}public void setUserId(String userId) {this.userId = userId;}public String getPasswd() {return passwd;}public void setPasswd(String passwd) {this.passwd = passwd;} }
2.实现用户登录界面框架
1.导入工具类utils/Utility.java
package 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.编写基本用户界面view/QQView.java
package view;import utils.Utility;/*** @author 孙显圣* @version 1.0* 客户端的菜单界面*/
public class QQView {public static void main(String[] args) {new QQView().mainMenu();}private boolean loop = true; //控制主菜单循环执行//显示主菜单的方法private void mainMenu() {while (loop) { //循环显示菜单System.out.println("==========欢迎登录网络通信系统==========");System.out.println(" 1 登录系统");System.out.println(" 9 退出系统");System.out.print("请输入您的选择:");String s = Utility.readString(1); //读取一个字符//根据选择执行操作switch (s) {case "1":System.out.println("请输入用户号");String userId = Utility.readString(50);System.out.println("请输入密 码");String passwd = Utility.readString(50);//去服务端验证该用户是否合法//1.假设合法if (false) {//循环输出菜单while (loop) {System.out.println("==========网络通信系统二级菜单==========");System.out.println(" 1 显示在线用户列表");System.out.println(" 2 群发消息");System.out.println(" 3 私聊消息");System.out.println(" 4 发送文件");System.out.println(" 9 退出系统");System.out.print("请输入您的选择:");String key = Utility.readString(1);//根据选择做出相应操作switch (key) {case "1":System.out.println("显示在线用户列表");break;case "2":System.out.println("群发消息");break;case "3":System.out.println("私聊消息");break;case "4":System.out.println("发送文件");break;case "9":System.out.println("==========用户退出系统==========");loop = false;break;}}}//2.不合法else {//退出这个switchSystem.out.println("==========用户名或密码不正确!==========");break;}break;case "9":System.out.println("==========用户退出系统==========");loop = false;break;}}}
}
3.实现客户端的登录部分
1.qqclient/service/UserClientService.java
package qqclient.service;import com.sun.org.apache.xpath.internal.operations.Variable;
import qqcommon.Message;
import qqcommon.MessageType;
import qqcommon.User;import java.io.*;
import java.net.InetAddress;
import java.net.Socket;
import java.net.UnknownHostException;/*** @author 孙显圣* @version 1.0* 完成用户登录验证和用户注册等等功能*/
public class UserClientService {private User user = new User(); //由于可能在其他地方需要使用到这个User对象,所以将其设置为这个类的属性//根据前端输入的用户名和密码,封装成User对象并且发送到服务器端,接受服务器端返回的Message对象,并根据mesType来确定是否符合要求public boolean checkUser(String userId, String pwd) throws IOException, ClassNotFoundException {//设置一个临时变量,用于返回值boolean res = false;//将用户名和密码封装到User对象中user.setUserId(userId);user.setPasswd(pwd);//获取客户端的socketSocket socket = new Socket(InetAddress.getLocalHost(), 9999);//获取客户端的输出流OutputStream outputStream = socket.getOutputStream();//将其转换成对象处理流ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream);//将user对象发送objectOutputStream.writeObject(user);//获取从服务器端回复的Message对象//获取客户端的输入流InputStream inputStream = socket.getInputStream();//转换为对象处理流ObjectInputStream objectInputStream = new ObjectInputStream(inputStream);//读取message对象Message o = (Message) objectInputStream.readObject(); //此时我们确定读取的一定是Message对象,所以将其向下转型//根据获取的mesType来确定是否成功if (o.getMesType().equals(MessageType.MESSAGE_LOGIN_SUCCEED)) {//创建一个和服务器端保持通信的线程ClientConnectServerThread clientConnectServerThread = new ClientConnectServerThread(socket);//启动客户端的线程,使其等待服务器的信息clientConnectServerThread.start();//为了后面客户端的扩展,放到一个集合中ManageClientConnectServerThread.addClientConnectServerThread(userId, clientConnectServerThread);//成功了,将返回值设置为trueres = true;} else {//如果登录失败则虽然没有启动线程但是还是开启了一个socket,所以要关闭socket.close();}return res;}}
2.qqclient/service/ManageClientConnectServerThread.java
package qqclient.service;import java.util.HashMap;/*** @author 孙显圣* @version 1.0* 该类管理客户端连接到服务器端的线程的类*/
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);}}
3.qqclient/service/ClientConnectServerThread.java
package qqclient.service;import qqcommon.Message;import java.io.IOException;
import java.io.ObjectInputStream;
import java.net.Socket;/*** @author 孙显圣* @version 1.0* 这个线程持有socket*/
public class ClientConnectServerThread extends Thread {private Socket socket;//该构造器可以接受一个Socket对象public ClientConnectServerThread(Socket socket) {this.socket = socket;}//更方便的得到Socketpublic Socket getSocket() {return socket;}//因为线程需要在后台一直保持和服务器的通信,因此使用while循环@Overridepublic void run() {while (true) {System.out.println("客户端线程,等待读取从服务器端发送的信息");try {//获取该线程socket的对象输入流ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream());//读取信息Message o = (Message) objectInputStream.readObject(); //如果没有数据传进来,则这个线程则会阻塞} catch (IOException e) {throw new RuntimeException(e);} catch (ClassNotFoundException e) {throw new RuntimeException(e);}}}
}
4.修改QQView.java中的验证用户是否合法语句
4.实现服务器端的登录部分
1.qqserver/service/QQServer.java
package qqserver.service;import qqcommon.Message;
import qqcommon.MessageType;
import qqcommon.User;import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.ServerSocket;
import java.net.Socket;/*** @author 孙显圣* @version 1.0* 这是服务器,监听9999,等待客户端的连接并且保持通信*/
public class QQServer {private ServerSocket ss = null;public QQServer() {System.out.println("服务端在9999端口监听。。。");try {ss = new ServerSocket(9999); //开一个9999端口监听User对象} catch (IOException e) {throw new RuntimeException(e);}//由于可能会有很多的客户端发送信息,所以要使用循环监听,并且返回不同的sockettry {while (true) {//每次有用户连接都获取socketSocket socket = ss.accept();//读取客户端的User对象ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream());User o = (User) objectInputStream.readObject();//创建一个Message用于回复客户端Message message = new Message();//输出流ObjectOutputStream objectOutputStream = null;//对其进行验证,先写死if (o.getUserId().equals("100") && o.getPasswd().equals("123456")) {message.setMesType(MessageType.MESSAGE_LOGIN_SUCCEED);//获取输出流回复客户端objectOutputStream = new ObjectOutputStream(socket.getOutputStream());objectOutputStream.writeObject(message);//回复完客户端之后,需要创建一个线程,用来管理socket用来保持与客户端的通信ServerConnectClientThread serverConnectClientThread = new ServerConnectClientThread(socket, o.getUserId());serverConnectClientThread.start();//使用集合来管理线程ManageClientThreads.addClientThread(o.getUserId(), serverConnectClientThread);}else {//如果登录失败,就不能启动线程,将失败的消息返回给客户端则关闭socketmessage.setMesType(MessageType.MESSAGE_LOGIN_FAIL);objectOutputStream.writeObject(message);socket.close();}}} catch (IOException e) {throw new RuntimeException(e);} catch (ClassNotFoundException e) {throw new RuntimeException(e);} finally {try {//如果最终退出了循环,说明不再需要服务器端监听,所以,关闭ServerSocketss.close();} catch (IOException e) {throw new RuntimeException(e);}}}
}
2.qqserver/service/ServerConnectClientThread.java
package qqserver.service;import qqcommon.Message;import java.io.IOException;
import java.io.ObjectInputStream;
import java.net.Socket;/*** @author 孙显圣* @version 1.0* 该类对应的一个对象和某个客户端保持连接,*/
public class ServerConnectClientThread extends Thread{//管理一个socket,和对应的用户idprivate Socket socket;private String userId;public ServerConnectClientThread(Socket socket, String userId) {this.socket = socket;this.userId = userId;}//保持这个socket的运行@Overridepublic void run() {while (true) {System.out.println("服务端和客户端保持通信,读取数据。。。");try {//读取数据ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream());Message o = (Message) objectInputStream.readObject(); //由于之前已经接受过User对象了,现在就是接受的Message对象} catch (IOException e) {throw new RuntimeException(e);} catch (ClassNotFoundException e) {throw new RuntimeException(e);}}}
}
5.登录阶段运行调试过程
1.第一次运行,报错!(用户名密码正确时)
Connect reset。。。。
解决方法
- 在两个序列化的类添加这行代码:
private static final long serialVersionUID = 1L;
- 修改之后,密码正确的时候可以正常显示
2.第二次运行,报错!(用户名密码不正确时)
原因
在执行else语句时,由于没有运行if,所以是空的
解决方法
由于if和else都会用到,所以提出来在外边初始化
成功运行
6.实现多个合法用户可以登录
qqserver/service/QQServer.java更新
-
添加以下内容:
//创建一个集合,存放多个用户,如果是这些用户登录,就认为是合法的//可以使用ConcurrentHashMap,这样就避免了线程安全问题,HashMap线程不安全的private static ConcurrentHashMap<String, User> vaildUsers = new ConcurrentHashMap<>();//使用静态代码块初始化static {vaildUsers.put("100", new User("100", "123456"));vaildUsers.put("200", new User("200", "123456"));vaildUsers.put("300", new User("300", "123456"));vaildUsers.put("400", new User("400", "123456"));}//验证用户是否有效的方法private boolean checkUser(User user) {String userId = user.getUserId(); //获取键String passwd = user.getPasswd(); //获取密码//过关斩将//首先查找键是否存在if (!vaildUsers.containsKey(userId)) {return false;}if (!vaildUsers.get(userId).getPasswd().equals(passwd)) {return false;}return true;}
-
修改验证逻辑
4.功能实现——拉取在线用户
1.功能完成
1.qqcommon/MessageType.java更新
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"; //客户端请求退出
2.qqclient/service/ClientConnectServerThread.java更新
package qqclient.service;import qqcommon.Message;import java.io.IOException;
import java.io.ObjectInputStream;
import java.net.Socket;/*** @author 孙显圣* @version 1.0* 这个线程持有socket*/
public class ClientConnectServerThread extends Thread {private Message message; //存放信息private Socket socket;public static Boolean STATE = false; //子线程任务完成状态,用于线程同步//该构造器可以接受一个Socket对象public ClientConnectServerThread(Socket socket) {this.socket = socket;}//更方便的得到Socketpublic Socket getSocket() {return socket;}//刷新子线程状态public static void flushState() {STATE = false;}//因为线程需要在后台一直保持和服务器的通信,因此使用while循环@Overridepublic void run() {while (true) {System.out.println("客户端线程,等待读取从服务器端发送的信息");try {//获取该线程socket的对象输入流ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream());//读取信息message = (Message) objectInputStream.readObject(); //如果没有数据传进来,则这个线程则会阻塞switch (message.getMesType()) {case "3": //普通信息包break;case "5": //返回在线用户列表System.out.println(message.getContent());break;}STATE = true; //更新状态} catch (IOException e) {throw new RuntimeException(e);} catch (ClassNotFoundException e) {throw new RuntimeException(e);}}}
}
3.qqclient/service/UserClientService.java更新
//向服务器端发送请求在线用户的数据包public void onlineFriendList(String userId) throws IOException, ClassNotFoundException, InterruptedException {//获取一个消息包Message message = new Message();//设置参数message.setMesType(MessageType.MESSAGE_GET_ONLINE_FRIEND);//获取当前用户名对应的线程ClientConnectServerThread currentThread = ManageClientConnectServerThread.getClientConnectServerThread(userId);//获取线程中的socketSocket socket = currentThread.getSocket();//获取对象输出流ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream());//输出对象objectOutputStream.writeObject(message);while (!ClientConnectServerThread.STATE); //等待子线程完成ClientConnectServerThread.flushState(); //刷新状态}
4.view/QQView.java更新
5.qqserver/service/ManageClientThreads.java更新
添加方法
//获取线程集合public static HashMap<String, ServerConnectClientThread> getHm() {return hm;}
6.qqserver/service/QQServer.java更新
添加方法
//遍历当前用户列表并发送到前端public static void getCurrentOnlineFriendList(Socket socket) throws IOException {//获取当前用户列表HashMap<String, ServerConnectClientThread> hm = ManageClientThreads.getHm();//遍历并保存到数据包中Message message = new Message(); //创建一个数据包//设置数据类型message.setMesType(MessageType.MESSAGE_RET_ONLINE_FRIEND); //类型为返回在线用户列表//记录返回的内容StringBuilder res = new StringBuilder();//获取所有的key,使用迭代器遍历Set<String> strings = hm.keySet();Iterator<String> iterator1 = strings.iterator();int i = 0; //统计用户个数while (iterator1.hasNext()) {String next = iterator1.next();res.append("用户" + (++i) + ": ").append(next).append(" "); //拼接}//将结果放到数据包中message.setContent(res.toString());//根据目前的socket来发送数据ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream());objectOutputStream.writeObject(message);}
7.qqserver/service/ServerConnectClientThread.java更新
try语句更新
//读取数据ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream());Message o = (Message) objectInputStream.readObject(); //由于之前已经接受过User对象了,现在就是接受的Message对象//根据读到的信息类型进行处理switch (o.getMesType()) {case "3": //普通信息包break;case "4": //返回当前在线用户列表QQServer.getCurrentOnlineFriendList(socket); //将目前的socket给他break;case "6": //客户端请求退出break;}
2.调试阶段
1.代码冗余
- 我最开始自己实现时,获取服务器端的socket是在线程数组中通过客户端传过来的姓名来获取的,后来发现没这么麻烦
- 服务器端的一个线程就对应一个通道的socket,并且在不断读取,如果读取到了,则此时的线程实例中的属性socket,就应该是与发送信息的客户端连通的那个socket,直接使用就可以了
2.线程同步问题
- 我在拉取在线用户时,在QQ的前端界面调取一个方法,来向服务器端发送Message来请求获取在线用户。然后服务器端发送信息给客户端,此时的客户端是子线程在接收数据,而主线程运行前端页面
- 由于主线程只是发送了个消息就直接退出case进行下一次循环,而子线程还要根据信息处理并返回,所以一定比主线程慢,所以我在子线程里面加了一个布尔型的状态常量,并且设置了一个方法可以刷新状态,这样在主线程调用的方法中,可以使用一个while循环持续等待,直到子线程输出数据,然后再刷新状态
5.功能实现——无异常退出系统
1.功能完成
1.qqcommon/MessageType.java更新
package qqcommon;/*** @author 孙显圣* @version 1.0*/
public interface MessageType {//在接口中定义了不同的常量//不同常量的值表示不同的消息类型String MESSAGE_LOGIN_SUCCEED = "1"; //表示登录成功String MESSAGE_LOGIN_FAIL = "2"; //表示登录失败String MESSAGE_COMM_MES = "3"; //普通信息包String MESSAGE_GET_ONLINE_FRIEND = "4"; //要求返回在线用户列表String MESSAGE_RET_ONLINE_FRIEND = "5"; //返回在线用户列表String MESSAGE_CLIENT_EXIT = "6"; //客户端请求退出String MESSAGE_SERVICE_EXIT_SUCCESS = "7"; //服务器端退出成功
}
2.qqclient/service/ClientConnectServerThread.java更新
try语句更新
//获取该线程socket的对象输入流ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream());//读取信息message = (Message) objectInputStream.readObject(); //如果没有数据传进来,则这个线程则会阻塞switch (message.getMesType()) {case "3": //普通信息包break;case "5": //返回在线用户列表System.out.println(message.getContent());break;case "7": //服务端退出成功new UserClientService().exitAllThreads(socket, objectInputStream); //关闭资源以及退出主线程loop = false; //退出线程循环break;}STATE = true; //更新状态
3.qqclient/service/UserClientService.java更新
添加三个方法
//向客户端发送信数据包的方法public void sendMessageToService(String userId, Message message) throws IOException {//获取当前线程ClientConnectServerThread clientConnectServerThread = ManageClientConnectServerThread.getClientConnectServerThread(userId);//获取socketSocket socket = clientConnectServerThread.getSocket();//创建输出流ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream());//发送信息objectOutputStream.writeObject(message);}//向客户端发送请求退出的信息public void requestExit(String userId) throws IOException {//创建一个MessageMessage message = new Message();message.setMesType(MessageType.MESSAGE_CLIENT_EXIT);message.setSender(userId); //告诉服务器端发送者是谁,这样可以清除集合中的线程//发送数据包sendMessageToService(userId, message);}//退出子线程以及主线程public void exitAllThreads(Socket socket, ObjectInputStream objectInputStream) throws IOException {objectInputStream.close();socket.close();System.exit(0);}
4.view/QQView.java更新
5.qqserver/service/QQServer.java更新
添加两个方法
//服务器端发送给客户端数据包的方法public static void sendToClientMessage(Socket socket, Message message) throws IOException {//获取输出流ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream());objectOutputStream.writeObject(message);}//服务器端,返回一个退出成功的数据包然后关闭socketpublic static void ServiceExit(Socket socket, ObjectInputStream objectInputStream) throws IOException {//创建一个数据包Message message = new Message();//放入数据message.setMesType(MessageType.MESSAGE_SERVICE_EXIT_SUCCESS); //服务器端退出成功//发送sendToClientMessage(socket, message);objectInputStream.close();socket.close();}
6.qqserver/service/ManageClientThreads.java更新
添加方法
//根据userId删除public static void deleteByUserId(String userId) {hm.remove(userId);}
7.qqserver/service/ServerConnectClientThread.java更新
//保持这个socket的运行private boolean loop = true;@Overridepublic void run() {while (loop) {System.out.println("服务端和客户端" + userId + "线程保持通信,读取数据。。。");try {//读取数据ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream());Message o = (Message) objectInputStream.readObject(); //由于之前已经接受过User对象了,现在就是接受的Message对象//根据读到的信息类型进行处理switch (o.getMesType()) {case "3": //普通信息包break;case "4": //返回当前在线用户列表QQServer.getCurrentOnlineFriendList(socket); //将目前的socket给他break;case "6": //客户端请求退出ManageClientThreads.deleteByUserId(o.getSender()); //清除列表元素QQServer.ServiceExit(socket, objectInputStream);//关闭流和套接字loop = false;break;}} catch (IOException e) {throw new RuntimeException(e);} catch (ClassNotFoundException e) {throw new RuntimeException(e);}}}
2.调试阶段
1.出现空指针异常
- 我第一次在关闭客户端的时候,在服务器端出现了空指针异常
- 原因:我在处理关闭服务器端的时候只是关闭了流和套接字,并没有关闭run方法的循环,导致子线程继续在读,但是由于套接字已经关闭,读的时候还要使用它获取流,所以出现异常
2.数据未同步
- 修改完异常之后,可以正常退出,但是我在测试拉取在线用户时出现了异常
- 原因:客户端已经退出,但是服务器端的线程集合中的元素并没有清除,所以导致了异常
3.安全性提升
- 原来的的退出系统逻辑就是客户端向服务器端发送退出的请求,然后服务器端收到请求就直接退出
- 这样是不安全的,因为客户端的主线程向服务器端发送完请求之后就直接退出,但是有个问题,如果服务器端接受到信息的速度慢了一点,导致客 户端先关闭了socket,那么服务器端在使用socket的时候就会报异常
- 我的解决方案:让客户端通知服务器端请求关闭连接的时候,在服务器的socket关闭之前向客户端发送一条消息,就是服务器端关闭成功,当客户端接收到这个消息的时候再退出
6.功能实现——私聊功能
1.功能完成
1.qqclient/service/ClientConnectServerThread.java更新
2.qqclient/service/UserClientService.java更新
添加方法
//私聊消息public void privateMessages(String sender) throws IOException {//展示所有用户之后//获取用户名称System.out.print("请输入你要聊天的用户名称:");String getter = new Scanner(System.in).next();//获取聊天的内容System.out.print("请输入聊天的内容");String content = new Scanner(System.in).nextLine();//创建一个数据包Message message = new Message();message.setMesType(MessageType.MESSAGE_COMM_MES); //普通消息message.setContent(content);message.setSender(sender);message.setGetter(getter);//发送到服务器端sendMessageToService(sender, message);}//读取私聊消息public void readPrivateMessage(Message message) {String sender = message.getSender();String content = message.getContent();System.out.println("\n========== " + sender + "对你说" + " ==========");System.out.println(content);}
3.view/QQView.java更新
4.qqserver/service/QQServer.java更新
添加方法
//转发消息public static void forwordMessage(Message message) throws IOException {//获取信息String content = message.getContent();String sender = message.getSender();String getter = message.getGetter();//根据姓名获取线程ServerConnectClientThread sendThread = ManageClientThreads.getServerConnectClientThread(getter);//发送包sendToClientMessage(sendThread.getSocket(), message);}
5.qqserver/service/ServerConnectClientThread.java更新
2.调试阶段
并未发现错误
7.功能实现——群发功能
1.功能完成
1.qqcommon/MessageType.java更新
package qqcommon;/*** @author 孙显圣* @version 1.0*/
public interface MessageType {//在接口中定义了不同的常量//不同常量的值表示不同的消息类型String MESSAGE_LOGIN_SUCCEED = "1"; //表示登录成功String MESSAGE_LOGIN_FAIL = "2"; //表示登录失败String MESSAGE_COMM_MES = "3"; //普通信息包String MESSAGE_GET_ONLINE_FRIEND = "4"; //要求返回在线用户列表String MESSAGE_RET_ONLINE_FRIEND = "5"; //返回在线用户列表String MESSAGE_CLIENT_EXIT = "6"; //客户端请求退出String MESSAGE_SERVICE_EXIT_SUCCESS = "7"; //服务器端退出成功String MESSAGE_SEND_ALL_USER = "8"; //群发消息
}
2.qqclient/service/ClientConnectServerThread.java更新
3.qqclient/service/UserClientServer.java更新
添加方法
//群发消息public void sendToAllUser(String userId) throws IOException {System.out.println("==========请输入你要发送的内容==========");Scanner scanner = new Scanner(System.in);String content = scanner.nextLine();//创建一个数据包Message message = new Message();message.setMesType(MessageType.MESSAGE_SEND_ALL_USER);message.setContent(content);message.setSender(userId);//发送数据包sendMessageToService(userId, message);}//读取群发消息public void readAllSendMessage(Message message) {//获取信息String sender = message.getSender();String content = message.getContent();System.out.println("\n========== " + sender +" 的群发消息==========");System.out.println(content);}
4.qqserver/service/QQServer.java更新
5.qqserver/service/QQServer.java更新
添加方法
//群发消息public static void sendToAllUser(Message message, String userId) throws IOException {//遍历在线用户集合,发送消息HashMap<String, ServerConnectClientThread> hm = ManageClientThreads.getHm();Collection<ServerConnectClientThread> threads = hm.values();for (ServerConnectClientThread thread : threads) {if (hm.get(userId) == thread) { //不用发送给本用户continue;}//发送包sendToClientMessage(thread.getSocket(), message);}}
6.qqserver/service/ServerConnectClientThread.java更新
2.调试阶段
未发现错误
8.功能实现——发文件
1.功能完成
1.qqcommon/MessageType.java更新
package qqcommon;/*** @author 孙显圣* @version 1.0*/
public interface MessageType {//在接口中定义了不同的常量//不同常量的值表示不同的消息类型String MESSAGE_LOGIN_SUCCEED = "1"; //表示登录成功String MESSAGE_LOGIN_FAIL = "2"; //表示登录失败String MESSAGE_COMM_MES = "3"; //普通信息包String MESSAGE_GET_ONLINE_FRIEND = "4"; //要求返回在线用户列表String MESSAGE_RET_ONLINE_FRIEND = "5"; //返回在线用户列表String MESSAGE_CLIENT_EXIT = "6"; //客户端请求退出String MESSAGE_SERVICE_EXIT_SUCCESS = "7"; //服务器端退出成功String MESSAGE_SEND_ALL_USER = "8"; //群发消息String MESSAGE_SEND_FILE = "9"; //发送文件
}
2.qqcommon/Message.java更新
package qqcommon;import java.io.Serializable;/*** @author 孙显圣* @version 1.0* 表示客户端和服务器端通讯时的消息对象*/
public class Message implements Serializable { //也需要进行序列化private String sender; //发送者private String getter; //接受者private String content; //消息内容private String sendTime; //发送时间private String mesType; //消息类型,在接口中定义已知的消息类型private String path; //记录路径private byte[] bytes; //存储文件private int length; //记录长度public int getLength() {return length;}public void setLength(int length) {this.length = length;}private static final long serialVersionUID = 1L;public byte[] getBytes() {return bytes;}public void setBytes(byte[] bytes) {this.bytes = bytes;}public String getPath() {return path;}public void setPath(String path) {this.path = path;}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;}
}
3.qqclient/service/ClientConnectServerThread.java更新
4.qqclient/service/UserClientServer.java更新
添加方法
//发送文件public void sendFile(String setter) throws IOException {//获取用户名称System.out.print("请输入要发送文件的用户名称:");Scanner scanner = new Scanner(System.in);String getter = scanner.next();//获取本地文件路径System.out.print("请输入本地文件路径:");String path1 = scanner.next();//获取对方文件路径System.out.print("请输入对方文件路径:");String path2 = scanner.next();//读取本地文件BufferedInputStream inputStream = new BufferedInputStream(new FileInputStream(path1));//设置缓冲byte[] bytes = new byte[1024 * 10];//记录长度int len = 0;while ((len = inputStream.read(bytes)) != -1) {Message message = new Message();//创建一个数据包message.setSender(setter);message.setGetter(getter);message.setMesType(MessageType.MESSAGE_SEND_FILE);message.setPath(path2);message.setBytes(bytes);message.setLength(len);//发送sendMessageToService(setter, message);}//关闭inputStream.close();//最后发送一个普通信息包,通知用户Message message = new Message();message.setMesType(MessageType.MESSAGE_COMM_MES);message.setContent("用户" + setter + "向你发送了一个文件,路径为" + path2);message.setGetter(getter);message.setSender(setter);sendMessageToService(setter, message);}//读取文件public void readFile(Message message) throws IOException {String sender = message.getSender();String path = message.getPath();byte[] bytes = message.getBytes();int length = message.getLength();//写入到本地路径BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(new FileOutputStream(path, true));bufferedOutputStream.write(bytes,0, length);//关闭bufferedOutputStream.close();}
5.view/QQView.java更新
6.qqserver/service/ServerConnectClientThread.java更新
2.调试阶段
1.传输文件大小膨胀
- 一开始由于Message要传输的内容是String类型的,所以我就将文件分成很多byte[1024*10]的部分进行传输并且转换成了String
- 但是这个导致了文件变大了很多
- 解决方法:在Message中添加属性,来保存byte类型的数组和读取到的长度,然后再将其放到包中传输,在读取的时候以byte数组的形式读取就行
9.功能实现——服务器端推送新闻
1.功能完成
1.qqserver/service/SendAllThread.java
package qqserver.service;import qqcommon.Message;
import qqcommon.MessageType;import java.io.IOException;
import java.io.ObjectOutputStream;
import java.util.Collection;
import java.util.HashMap;
import java.util.Scanner;/*** @author 孙显圣* @version 1.0* 用来向客户端推送新闻*/
public class SendAllThread extends Thread{@Overridepublic void run() {while (true) { //循环获取要推送的信息System.out.println("请输入要推送的消息");Scanner scanner = new Scanner(System.in);String content = scanner.next();//获取MessageMessage message = new Message();message.setMesType(MessageType.MESSAGE_COMM_MES);message.setSender("系统");message.setContent(content);//遍历所有用户并群发HashMap<String, ServerConnectClientThread> hm = ManageClientThreads.getHm();Collection<ServerConnectClientThread> values = hm.values(); //所有的socketfor (ServerConnectClientThread Thread : values) {//获取线程的socket,从而获取对象输出流try {ObjectOutputStream objectOutputStream = new ObjectOutputStream(Thread.getSocket().getOutputStream());//输出普通信息包objectOutputStream.writeObject(message);System.out.println("服务器端推送消息:" + content);} catch (IOException e) {throw new RuntimeException(e);}}}}
}
2.qqserver/service/ServerConnectClientThread.java更新
2.调试阶段
1.子线程群发问题
- 我最初是把Message的内容写好,然后调用群发方法发送给各个用户
- 但是我只开了一个用户,然后一直测试发现群发不了,但是后来想起来,我的那个群发方法,设置的是不发送给当前的用户,真是醉了
- 解决方案:自己遍历所有用户,群发消息