文章目录
- 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=>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;
}
}
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 {
//退出这个switch
System.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);
//获取客户端的socket
Socket 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);
//成功了,将返回值设置为true
res = 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;
}
//更方便的得到Socket
public Socket getSocket() {
return socket;
}
//因为线程需要在后台一直保持和服务器的通信,因此使用while循环
@Override
public 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);
}
//由于可能会有很多的客户端发送信息,所以要使用循环监听,并且返回不同的socket
try {
while (true) {
//每次有用户连接都获取socket
Socket 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 {
//如果登录失败,就不能启动线程,将失败的消息返回给客户端则关闭socket
message.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 {
//如果最终退出了循环,说明不再需要服务器端监听,所以,关闭ServerSocket
ss.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,和对应的用户id
private Socket socket;
private String userId;
public ServerConnectClientThread(Socket socket, String userId) {
this.socket = socket;
this.userId = userId;
}
//保持这个socket的运行
@Override
public 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;
}
//更方便的得到Socket
public Socket getSocket() {
return socket;
}
//刷新子线程状态
public static void flushState() {
STATE = false;
}
//因为线程需要在后台一直保持和服务器的通信,因此使用while循环
@Override
public 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);
//获取线程中的socket
Socket 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);
//获取socket
Socket socket = clientConnectServerThread.getSocket();
//创建输出流
ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream());
//发送信息
objectOutputStream.writeObject(message);
}
//向客户端发送请求退出的信息
public void requestExit(String userId) throws IOException {
//创建一个Message
Message 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);
}
//服务器端,返回一个退出成功的数据包然后关闭socket
public 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;
@Override
public 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{
@Override
public void run() {
while (true) { //循环获取要推送的信息
System.out.println("请输入要推送的消息");
Scanner scanner = new Scanner(System.in);
String content = scanner.next();
//获取Message
Message 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(); //所有的socket
for (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的内容写好,然后调用群发方法发送给各个用户
- 但是我只开了一个用户,然后一直测试发现群发不了,但是后来想起来,我的那个群发方法,设置的是不发送给当前的用户,真是醉了
- 解决方案:自己遍历所有用户,群发消息