QQ项目
1、项目开发流程
- 需求分析
- 设计阶段
- 实现阶段
- 测试阶段
- 实施阶段
- 维护阶段
(1)需求分析
-
用户登录
-
拉取在线用户列表
-
无异常退出(客户端、服务端)
-
私聊
-
群聊
-
发文件
-
服务器推送新闻
(2)通讯功能的整体分析
传输对象,对象当中包含传递的信息,从管道当中运输信息。
Message类来进行装载信息,Socket作为传送信息两端基站。线程持有一个socket。socket是线程的一个属性。
因为群发的需求需要一个线程结合来进行管理。可以查找集合当中都有哪些线程,确定与服务器连接的用户。
一个客户端也可能与服务器端有多个线程的连接。例如:发送文件,视频聊天,文件下载。所以客户端也需要有一个线程管理集合来对线程进行统一管理。
客户端使用对象流来进行读写信息。
2、开发阶段
前提:先创建三个类一个接口(服务器和客户端都要有)
User类:用于存储用户的账号密码
package qqcommon;
import java.io.Serializable;
public class User implements Serializable {
private static final long serialVersionUID = 1L;
private String userID;
private String password;
public User() {
}
public User(String userID, String password) {
this.userID = userID;
this.password = password;
}
public String getUserID() {
return userID;
}
public void setUserID(String userID) {
this.userID = userID;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
Message:用于存放用户信息
package 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;//消息类型【可以接口定义类型】
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接口:用来反映是否登陆成功
package qqcommon;
public interface MessageType {
String MESSAGE_LOGIN_SUCCEED="1";//表示登陆成功
String MESSAGE_LOGIN_FAIL="2";//表示登陆失败
}
以上两个类和一个接口放在qqCommon包里
这里有一个工具类Utility类(作用是监听键盘输入的信息)
package QQview;
/**
工具类的作用:
处理各种情况的用户输入,并且能够按照程序员的需求,得到用户的控制台输入。
*/
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;
}
}
1、用户登录UI设计
创建QQClient包,在该包下创建QQView包,在QQView包下创建qqview类用来实现用户登录UI设计
package QQClient.QQView;
import QQClient.Service.UserClientService;
import QQClient.utils.Utility;
public class qqview {
private boolean loop = true; //控制是否显示菜单
private String key = ""; // 接收用户的键盘输入
private UserClientService userClientService = new UserClientService();//对象是用于登录服务/注册用户
public static void main(String[] args) {
new qqview().view();
}
public void view(){
while(loop){
System.out.println("===========欢迎登录网络通信系统===========");
System.out.println("\t\t 1 登录系统");
System.out.println("\t\t 9 退出系统");
System.out.print("请输入你的选择: ");
key = Utility.readString(1);
//根据用户的输入,来处理不同的逻辑
switch (key){
case "1":
System.out.print("请输入用户号: ");
String userId = Utility.readString(50);
System.out.print("请输入密 码: ");
String pwd = Utility.readString(50);
//这里就比较麻烦了, 需要到服务端去验证该用户是否合法
//这里有很多代码, 我们这里编写一个类 UserClientService[用户登录/注册]
if (true){
System.out.println("===========欢迎 (用户 " + userId + " 登录成功) ===========");
//下面会有二级菜单后续代码实现
}else{
System.out.println("登陆失败")
}
break;
case "9":
System.out.println("退出系统");
break;
}
}
}
}
将上面代码加入二级菜单进行测试:
package QQClient.QQView;
import QQClient.Service.UserClientService;
import QQClient.utils.Utility;
public class qqview {
private boolean loop = true; //控制是否显示菜单
private String key = ""; // 接收用户的键盘输入
private UserClientService userClientService = new UserClientService();//对象是用于登录服务/注册用户
public static void main(String[] args) {
new qqview().view();
}
public void view(){
while(loop){
System.out.println("===========欢迎登录网络通信系统===========");
System.out.println("\t\t 1 登录系统");
System.out.println("\t\t 9 退出系统");
System.out.print("请输入你的选择: ");
key = Utility.readString(1);
//根据用户的输入,来处理不同的逻辑
switch (key){
case "1":
System.out.print("请输入用户号: ");
String userId = Utility.readString(50);
System.out.print("请输入密 码: ");
String pwd = Utility.readString(50);
//这里就比较麻烦了, 需要到服务端去验证该用户是否合法
//这里有很多代码, 我们这里编写一个类 UserClientService[用户登录/注册]
if (true){
System.out.println("===========欢迎 (用户 " + userId + " 登录成功) ===========");
//进入到二级菜单
while (loop) {
System.out.println("\n=========网络通信系统二级菜单(用户 " + userId + " )=======");
System.out.println("\t\t 1 显示在线用户列表");
System.out.println("\t\t 2 群发消息");
System.out.println("\t\t 3 私聊消息");
System.out.println("\t\t 4 发送文件");
System.out.println("\t\t 9 退出系统");
System.out.print("请输入你的选择: ");
key = Utility.readString(1);
switch (key){
case "1":
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;
}
}
}else {
System.out.println("登录失败");
}
break;
case "9":
System.out.println("退出系统");
break;
}
}
}
}
2、实现网络用户登录(进行用户验证)
客户端思路:拿到用户名和密码后–>生成User对象–>序列化–>通过网络发送服务器
服务器端实现思路:服务器拿到User对象–>与数据库进行比对–>登陆成功|登陆失败都保存成Message对象–>回应
客户端:
在QQClient包下创建一个Service包,在该包下创建一个UserClientService类用来实现用户验证功能。
package QQClient.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.Socket;
public class UserClientService {
//我们可能在其他地方使用User对象,因此设计成一个成员变量
private User u=new User();
//因为Socket在其他地方也可能会使用到,因此设计成一个成员变量
private Socket socket;
//用来检查用户输入的用户名和密码发送到服务器去验证是否合法
public boolean checkUser(String userID,String password){
boolean result=true;
u.setUserID(userID);
u.setPassword(password);
try {
socket=new Socket("127.0.0.1",9999);
//发送User对象给服务器
ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
oos.writeObject(u);
//接受服务器回送的Message对象
ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
Message message =(Message) ois.readObject();
//验证
if (message.getMesType().equals(MessageType.MESSAGE_LOGIN_SUCCEED)){//登陆成功
//登陆成功会启动线程,并把线程用一个线程集合来集中管理
//后续代码会统一实现
}else {
//如果登录失败,就关闭相关流和socket,并把result置为true。
}
} catch (Exception e) {
throw new RuntimeException(e);
}
return result;
}
}
在Service包下创建一个ClientConnectServerThread类并继承Thread,保持与服务器进行通讯。
public class ClientConnectServerThread extends Thread{
private Socket socket;//该线程需要持有Socket
public ClientConnectServerThread(Socket socket){//构造器可以接收Socket对象
this.socket=socket;
}
@Override
public void run() {
//因为Thread需要和服务器端保持通讯,因此我们要使用while循环
while(true){
System.out.println("客户端程序等待读取服务端发送回来的消息");
InputStream inputStream = null;
try {
inputStream = socket.getInputStream();
ObjectInputStream ois = new ObjectInputStream(inputStream);
Message message =(Message) ois.readObject();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
//
public Socket getSocket() {
return socket;
}
}
有了上面的线程类我们就可以完善UserClientService类了,在登陆成功时添加如下代码:
if (message.getMesType().equals(MessageType.MESSAGE_LOGIN_SUCCEED)){//登陆成功
//登陆成功时,要创建一个线程这个线程持有一个Socket对象
ClientConnectServerThread ccst = new ClientConnectServerThread(socket);
ccst.start();
//为了后面客户端的扩展我们将线程放入到集合中进行管理
}else {
//如果登录失败,就关闭相关流和socket
}
在Servive包下创建一个ManageClientConnectServerThread类来进行线程的集合管理
package QQClient.Service;
import java.util.HashMap;
public class ManageClientConnectServerThread {
//这里HashMap的key就是userID,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);
}
}
有了ManageClientConnectServerThread类来进行线程的集合管理,就可以继续完善UserClientService类了
if (message.getMesType().equals(MessageType.MESSAGE_LOGIN_SUCCEED)){//登陆成功
//创建一个和服务器端保持通信的线程-> 创建一个类 ClientConnectServerThread
ClientConnectServerThread clientConnectServerThread =
new ClientConnectServerThread(socket);
//启动客户端的线程
clientConnectServerThread.start();
//这里为了后面客户端的扩展,我们将线程放入到集合管理
ManageClientConnectServerThread.addClientConnectServerThread(userID,clientConnectServerThread);
result=true;
}else {
result=false;
socket.close();
}
服务器端:
同客户端创建一个qqcommon包在该包下创建User类,Message类,MessageType接口与客户端保持完全一致!!!
创建完后在创建一个QQServer包,在改包下创建一Service包,在Service包下创建一个QQServer类用来监听客户端发来的请求
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;
public class QQServer {
private ServerSocket ss=null;
public QQServer(){
try {
System.out.println("服务器在9999端口正在监听");
ss=new ServerSocket(9999);
while(true){ //这里用while循环,是因为服务器端需要持续对客户端进行监听
Socket socket = ss.accept();
//获取客户端发来的User对象
ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
User u = (User) ois.readObject();
//发送Message对象给客户端
ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
Message message=new Message();
if (u.getUserID().equals("123")&&u.getPassword().equals("123456")){
message.setMesType(MessageType.MESSAGE_LOGIN_SUCCEED);
oos.writeObject(message);
//创建一个线程,和客户端保持通信, 该线程需要持有socket对象
//将线程加入集合方便管理
}else {
System.out.println("用户 id=" + u.getUserID() + " pwd=" + u.getPassword() + " 验证失败");
message.setMesType(MessageType.MESSAGE_LOGIN_FAIL);
oos.writeObject(message);
//关闭socket
socket.close();
}
}
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
}
}
}
在Service包下创建一个ServerConnectClientThread类并继承Thread,
package QQServer.Service;
import qqcommon.Message;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.net.Socket;
public class ServerConnectClientThread extends Thread{
private Socket socket;
private String userId;//连接到服务端的用户id
public ServerConnectClientThread(Socket socket, String userId) {
this.socket = socket;
this.userId = userId;
}
public Socket getSocket() {
return socket;
}
@Override
public void run() {
while(true){ //持续与客户端保持通信
try {
ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
Message message =(Message) ois.readObject();
//后续会进行代码优化
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
}
在Service包下创建一个ManageServerConnectClientThread类,用来实现线程管理
package QQServer.Service;
import java.util.HashMap;
public class ManageServerConnectClientThread {
//集合中的key就是userID,value就是ServerConnectClientThread线程
private static HashMap<String,ServerConnectClientThread> hm=new HashMap<>();
//集合中添加线程
public static void addManageServerConnectClientThread(String userID,ServerConnectClientThread serverConnectClientThread){
hm.put(userID,serverConnectClientThread);
}
//根据userID来获取对应线程
public static ServerConnectClientThread getServerConnectClientThread(String userID){
return hm.get(userID);
}
}
对QQServer类进行优化
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;
public class QQServer {
private ServerSocket ss=null;
public QQServer(){
try {
System.out.println("服务器在9999端口正在监听");
ss=new ServerSocket(9999);
while(true){
Socket socket = ss.accept();
//获取客户端发来的User对象
ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
User u = (User) ois.readObject();
//发送Message对象给客户端
ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
Message message=new Message();
if (u.getUserID().equals("123")&&u.getPassword().equals("123456")){
message.setMesType(MessageType.MESSAGE_LOGIN_SUCCEED);
oos.writeObject(message);
//创建一个线程,和客户端保持通信, 该线程需要持有socket对象
ServerConnectClientThread serverConnectClientThread = new ServerConnectClientThread(socket, u.getUserID());
serverConnectClientThread.start();
//将线程加入集合方便管理
ManageServerConnectClientThread.addManageServerConnectClientThread(u.getUserID(), serverConnectClientThread);
}else {
System.out.println("用户 id=" + u.getUserID() + " pwd=" + u.getPassword() + " 验证失败");
message.setMesType(MessageType.MESSAGE_LOGIN_FAIL);
oos.writeObject(message);
//关闭socket
socket.close();
}
}
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
}
}
}
在QQServer包下创建一个QQFrame包,在QQFrame包下创建一个QQFrame类,用来实现服务器端的启动。
package QQServer.QQFrame;
import QQServer.Service.QQServer;
public class QQFrame {
public static void main(String[] args) {
new QQServer();
}
}
完成以上的代码就可以进行相关的测试了,下面我会进行登录测试(设置的userID为123,密码为123456)
当输入错误的账号密码时会出现下面的效果:
输入正确的账号密码会出现下面的效果:
下面我们会在QQServer类下创建一个集合用来模拟数据库存放一些用户,这里使用ConcurrentHashMap能处理线程安全,即线程同步,在多线程下是安全的。具体代码如下:
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"));
}
有了这些用户在进行用户验证时就可以创建一个方法来进行账号密码的验证,这里我创建了一个checkuserID的方法来进行用户验证:
public boolean checkUser(String userID,String password){
User user = vaildUsers.get(userID);
if (user==null){ //说明userID没有存在在vaildUsers集合中
return false;
}
if (!(user.getPassword().equals(password))){ //说明userID的密码与vaildUsers集合中相对应的密码不匹配
return false;
}
return true;
}
下面我将会对QQServer类用户验证的代码进行优化
if (checkUser(u.getUserID(), u.getPassword())){//在if判断语句中用了上面添加的新方法。
message.setMesType(MessageType.MESSAGE_LOGIN_SUCCEED);
oos.writeObject(message);
//创建一个线程,和客户端保持通信, 该线程需要持有socket对象
ServerConnectClientThread serverConnectClientThread = new ServerConnectClientThread(socket, u.getUserID());
serverConnectClientThread.start();
//将线程加入集合方便管理
ManageServerConnectClientThread.addManageServerConnectClientThread(u.getUserID(), serverConnectClientThread);
}else {
System.out.println("用户 id=" + u.getUserID() + " pwd=" + u.getPassword() + " 验证失败");
message.setMesType(MessageType.MESSAGE_LOGIN_FAIL);
oos.writeObject(message);
//关闭socket
socket.close();
}
完善了以上代码就可以进行多并发的测试,这里需要对idea进行设置,在客户端的view程序下按照下图操作:点击允许多个实例选项,这样就可以模拟多并发的情况了
测试完的结果如下图
3、拉取在线用户列表
客户端:
因为客户端要求要向服务器端发送请求拉取在线用户列表所以可以在客户端的Service包下的UserClientService类构建一个向服务器端发送获取在线用户列表的方法。
public void getonlineuser(){
//创建一个Message对象,并设置为MESSAGE_GET_ONLINE_FRIEND,发送给服务器端
Message message = new Message();
message.setMesType(MessageType.MESSAGE_GET_ONLINE_FRIEND);
message.setSender(u.getUserID());
//获取该线程的OutputStream
try {
ObjectOutputStream oos =
new ObjectOutputStream(ManageClientConnectServerThread.getClientConnectServerThread(u.getUserID()).getSocket().getOutputStream());
oos.writeObject(message);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
添加完上面的方法后,就要在qqview类里调用该方法,调用方法的位置就是在拉取在线用户列表选项那里添加以下代码。
case "1":
System.out.println("显示在线用户列表");
userClientService.getonlineuser();
break;
服务器端:
此时客户端在拉取在线用户列表的功能就完善了,下面就开始写服务器端拉取在线用户功能的相关代码。
在服务器端的QQServer包下的Service包下的ServerConnectClientThread类下完善相关功能代码(因为ServerConnectClientThread类会与客户端保持通讯需要读取客户端发来的拉取在线用户列表请求)
@Override
public void run() {
while(true){
try {
System.out.println("服务器端与客户端"+userId+"保持通讯中~");
ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
Message message =(Message) ois.readObject();
//下面的代码就是完善的代码,只进行拉取在线用户列表功能的代码
if (message.getMesType().equals(MessageType.MESSAGE_GET_ONLINE_FRIEND)){
System.out.println(message.getSender()+" 要在线用户列表");
//因为需要对在线用户进行读取而在线用户存储在结合中,因此需要在管理集合的类编写一个方法来获取在线用户列表
}else {
System.out.println("其他类型的Message暂时不做处理~");
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
因为需要对在线用户进行读取而在线用户存储在结合中,因此需要在管理集合的类(ManageServerConnectClientThread)编写一个方法来获取在线用户列表:
public static String getOnlineUser(){
//集合遍历,遍历HashMap的key
Iterator<String> iterator = hm.keySet().iterator();
String onlineUserList="";
while(iterator.hasNext()){
onlineUserList+=iterator.next().toString()+" ";
}
return onlineUserList;
}
完成该方法就可以对上面的代码进行完善:
@Override
public void run() {
while(true){
try {
System.out.println("服务器端与客户端"+userId+"保持通讯中~");
ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
Message message =(Message) ois.readObject();
//
if (message.getMesType().equals(MessageType.MESSAGE_GET_ONLINE_FRIEND)){
System.out.println(message.getSender()+" 要在线用户列表");
//在管理集合的类编写一个方法来获取在线用户列表
String onlineUser = ManageServerConnectClientThread.getOnlineUser();
//返回一个Message,所以要构建一个Message
Message message2 = new Message();
message2.setMesType(MessageType.MESSAGE_RET_ONLINE_FRIEND);
message2.setContent(onlineUser);
message2.setGetter(message.getSender());
//发送给客户端
ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
oos.writeObject(message2);
}else {
System.out.println("其他类型的Message暂时不做处理~");
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
这样我们就完成了客户端与服务器端的拉取在线用户列表功能的代码,下面我们进行一次测试:
4、无异常退出
当客户端退出QQ系统时,并不会完全退出,因为阅读UserClientService类的checkUser代码可以知道它是启动了一个线程,而这个线程时不停的在while循环,退出系统只会退出main线程并不会退出和服务器通讯的线程,因此我们需要设计一个能退出线程的功能,而这个功能需要在客户端的UserClientService类下构造一个方法退出客户端,并给服务器发送一个退出系统的message对象。
在此之前我们需要在MessageType接口定义一个消息类型,用来表示客户端请求退出。
String MESSAGE_CLIENT_EXIT="6";//客户端请求退出
客户端:
//在客户端的UserClientService类下构造一个方法退出客户端
public void loguot(){ //构造一个方法退出客户端,并给服务器发送一个退出系统的message对象。
Message message = new Message();
message.setMesType(MessageType.MESSAGE_CLIENT_EXIT);//设置消息类型为退出客户端
message.setSender(u.getUserID());//一定要指明是谁要关闭,不然不知道关闭哪个线程
//发送message
try {
//ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
ObjectOutputStream oos =
new ObjectOutputStream(ManageClientConnectServerThread.getClientConnectServerThread(u.getUserID()).getSocket().getOutputStream());//因为可能会有多个socket,因此用这个方法会更好。
oos.writeObject(message);
System.out.println(u.getUserID()+" 退出系统");
System.exit(0);//结束进程
} catch (IOException e) {
throw new RuntimeException(e);
}
}
编写完以上方法后,就需要完善二级菜单“退出系统“的相关代码。
case "9":
//在UserClientService类编写一个方法给服务器端发送一个退出的信息(Message)
userClientService.loguot();
loop=false;
break;
服务器端:
现在以及完成了客户端无异常退出的相关功能,下面就需要完善服务器端的。同理也需要在服务器端的MessageType接口定义一个消息类型,用来表示客户端请求退出。
String MESSAGE_CLIENT_EXIT="6";//客户端请求退出
因为客户端在退出系统时会向服务器端发送一个MESSAGE_CLIENT_EXIT类型的Message,而服务器端需要保持去接收到这个Message,因此就需要去ServerConnectClientThread类完善相关代码(判断Message类型那里):
else if (message.getMesType().equals(MessageType.MESSAGE_CLIENT_EXIT)) {
System.out.println(message.getSender()+" 退出");
//将这个客户端对应的线程,从这个集合中移除,因为移除线程在管理集合的类中去操作更合理,因此需要在ManageServerConnectClientThread类创建一个移除线程的方法
System.out.println("移除")
socket.close();//关闭连接
//退出while循环
break;
}
将这个客户端对应的线程,从这个集合中移除,因为移除线程在管理集合的类中去操作更合理,因此需要在ManageServerConnectClientThread类创建一个移除线程的方法:
//增加一个方法,从集合中移除线程
public static void removeServerConnectClientThread(String userID){
hm.remove(userID);
}
ServerConnectClientThread类完善相关代码:
else if (message.getMesType().equals(MessageType.MESSAGE_CLIENT_EXIT)) {
System.out.println(message.getSender()+" 退出");
//将这个客户端对应的线程,从这个集合中移除
ManageServerConnectClientThread.removeServerConnectClientThread(message.getSender());
socket.close();//关闭连接
//退出while循环
break;
}
这样客户端和服务器端无异常退出功能代码就完成了,下面我们将会进行对此功能进行测试,如下图:
5、私聊功能
思路分析:
我们在此之前需要在客户端与服务器端的MessageType接口定义一个消息类型用来表示是普通私聊消息。
String MESSAGE_COMM_MES = "3";//普通信息包
客户端:
在客户端的用户页面的私聊功能的选项下完善相关业务逻辑:
case "3":
System.out.print("请输入想聊天的用户号:");
String getterID = Utility.readString(50);
System.out.println("请输入想说的话");
String content = Utility.readString(100);
//编写一个方法,将消息发送给服务器端
messageClientService.sendMessageToOne(content,userId,getterID);
break;
编写一个方法目的是将消息发送给服务器端,我们不妨创建一个类专门用来写私聊和群聊的信息管理。创建一个MessageClientService类,在该类下完成私聊功能的代码:
public void sendMessageToOne(String content,String senderID,String getterID){ //私聊方法
//构建Message
Message message = new Message();
message.setMesType(MessageType.MESSAGE_COMM_MES);//设置为普通的聊天消息类型
message.setSender(senderID);
message.setGetter(getterID);
message.setContent(content);
message.setSendTime(new Date().toString());//发送时间设置到message对象
System.out.println(senderID+"对"+getterID+"说"+content);
//发送到服务端
try {
ObjectOutputStream oos =
new ObjectOutputStream(ManageClientConnectServerThread.getClientConnectServerThread(senderID).getSocket().getOutputStream());
oos.writeObject(message);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
当得到服务器端回送过来的处理结果,我们就需要在ClientConnectServerThread中接收结果(因为线程在不停的与服务器保持通讯),并显示出来。
else if (message.getMesType().equals(MessageType.MESSAGE_COMM_MES)) {
//直接将message的数据显示出来
System.out.println("\n"+message.getSender()+"对"+message.getGetter()+"说:"+message.getContent());
}
服务器端:
在服务器端完善相关业务逻辑:
在服务器端就起到一个接收到Sender发来的message对象,读取到getter后获取对应线程,用对象流把message对象在转发给getter
else if (message.getMesType().equals(MessageType.MESSAGE_COMM_MES)) {
//根据message获取getterID,获取对应的线程
ServerConnectClientThread serverConnectClientThread = ManageServerConnectClientThread.getServerConnectClientThread(message.getGetter());
//得到对应的socket的对象输出流,将message对象转发给指定的客户端
ObjectOutputStream oos = new ObjectOutputStream(serverConnectClientThread.getSocket().getOutputStream());
oos.writeObject(message);//转发,提示:如果用户不在线可以保存到数据库,这样就可以实现离线留言
}
这样我们就完成了私聊功能的相关代码,下面我们将进行功能测试,如下图:
6、群聊功能
思路分析:(与私聊思路差不多)
在此之前,我们需要在客户端与服务器端的MessageType接口定义一个消息类型用来表示是群发消息。
String MESSAGE_TO_ALL_MES = "7";//群聊信息包
客户端:
在客户端的用户页面的群聊功能的选项下完善相关业务逻辑:
case "2":
System.out.println("请输入相对大家说的话:");
String s = Utility.readString(100);
//调用一个方法,将消息封装成Message对象,发送给服务器端
messageClientService.sendMessageToAll(s,userId);
System.out.println("群发消息");
break;
因为上面的私聊功能我们编写了一个类用来处理私聊与群聊问题的所以我们再在这个类下编写一个群聊的功能。
//群聊方法
public void sendMessageToAll(String content,String senderID){
//构建Message
Message message = new Message();
message.setMesType(MessageType.MESSAGE_TO_ALL_MES);//设置为群发消息类型
message.setSender(senderID);
message.setContent(content);
message.setSendTime(new Date().toString());//发送时间设置到message对象
System.out.println(senderID+"对所有人说"+content);
//发送到服务端
try {
ObjectOutputStream oos =
new ObjectOutputStream(ManageClientConnectServerThread.getClientConnectServerThread(senderID).getSocket().getOutputStream());
oos.writeObject(message);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
当得到服务器端回送过来的处理结果,我们就需要在ClientConnectServerThread中接收结果(因为线程在不停的与服务器保持通讯),并显示出来。
else if (message.getMesType().equals(MessageType.MESSAGE_TO_ALL_MES)) {
//直接显示在控制台即可
System.out.println("\n"+message.getSender()+"对大家说:"+message.getContent());
}
服务器端:
服务器端就需要判断message类型是否为群发类型,然后进行相关业务逻辑即可。
else if (message.getMesType().equals(MessageType.MESSAGE_TO_ALL_MES)) {
//需要遍历管理线程的集合,把所有的现成的socket都得到,然后把message进行转发即可
HashMap<String, ServerConnectClientThread> hm = ManageServerConnectClientThread.getHm();
Iterator<String> iterator = hm.keySet().iterator();
while(iterator.hasNext()){
//取出在线用户userID
String onlineUserID = iterator.next().toString();
if (!onlineUserID.equals(message.getSender())){//排除群发消息的这个用户
ObjectOutputStream oos = new ObjectOutputStream(hm.get(onlineUserID).getSocket().getOutputStream());
oos.writeObject(message);
}
}
}
在这里我们需要注意在管理线程的类里(ManageServerConnectClientThread)是否有获取集合的方法:
public static HashMap<String, ServerConnectClientThread> getHm() {
return hm;
}
这样我们就完成了群聊功能的相关代码,下面我们将进行功能测试,如下图:
7、发送文件
客户端:
在qqview下的发送文件功能下补充以下代码:
case "4":
System.out.println("请输入要发送给哪个用户:");
getterID = Utility.readString(50);
System.out.println("请输入要发送的文件路径:");
String src = Utility.readString(100);
System.out.println("请输入要发送到对方的哪个的文件路径:");
String desc = Utility.readString(100);
fileClientService.sendFileToOne(src,desc,userId,getterID);
break;
上面的代码我们用到了一个方法sendFileToOne,我们可以创建一个类专门用来处理文件传输功能。
创建FileClientService类,在该类下创建一个方法用来处理文件发送功能的实现。
public class FileClientService { //专门处理文件传输的类
public void sendFileToOne(String src,String desc,String senderID,String getterID){
Message message = new Message();
message.setMesType(MessageType.MESSAGE_FILE_MES);
message.setSrc(src);
message.setDesc(desc);
message.setSender(senderID);
message.setGetter(getterID);
//将文件进行读取
FileInputStream fileInputStream=null;
byte[] bytes = new byte[(int) new File(src).length()];
try {
fileInputStream=new FileInputStream(src);
fileInputStream.read(bytes);
//将读取到的字节数组设置到message里
message.setFileBytes(bytes);
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
//关闭相关流
if (fileInputStream!=null){
try {
fileInputStream.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
System.out.println("\n"+message.getSender()+"给"+message.getGetter()+"发送文件:"
+message.getSrc()+"到对方的电脑目录"+message.getDesc());
//发送
try {
ObjectOutputStream oos =
new ObjectOutputStream(ManageClientConnectServerThread.getClientConnectServerThread(senderID).getSocket().getOutputStream());
oos.writeObject(message);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
因为客户端还要接收到其他客户端发来的文件所以我们需要在ClientConnectServerThread类下完善相关代码(因为该类是保持与服务器端通信的,能时刻接收到信息)
else if (message.getMesType().equals(MessageType.MESSAGE_FILE_MES)) {
System.out.println("\n"+message.getSender()+"给"+message.getGetter()+
"发送文件"+message.getSrc()+"到我的电脑的目录:"+message.getDesc());
//读取message的文件字节数组,通过文件输出流写出到磁盘
FileOutputStream fileOutputStream = new FileOutputStream(message.getDesc());
fileOutputStream.write(message.getFileBytes());
fileOutputStream.close();
System.out.println("\n保存文件成功~");
}
到此我们就把客户端的代码写完了,下面我们将完善服务器端。
服务器端:
在服务器端,只起到接收到message对象,然后获取到要发送的用户,然后把message对象转发给该用户即可。
在ServerConnectClientThread类下补充以下代码:
else if (message.getMesType().equals(MessageType.MESSAGE_FILE_MES)) { //发送文件
//根据getterID获取到对应线程,将message进行转发
ObjectOutputStream oos =
new ObjectOutputStream(ManageServerConnectClientThread.getServerConnectClientThread(message.getGetter()).getSocket().getOutputStream());
oos.writeObject(message);
}
这样就完成了发送文件的功能,下面我们对代码进行测试,测试结果如下:
8、服务器推送新闻
服务器推送新闻是在服务器端进行的相当于服务器向所有用户群发消息,服务器将创建一个要发送的的message对象,然后将该对象遍历发送给所有线程集合的用户。
下面我将会创建一个线程类专门用于服务器端的消息推送服务。
package QQServer.Service;
import QQServer.utils.Utility;
import qqcommon.Message;
import qqcommon.MessageType;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
public class SendNewsToAllService implements Runnable{
@Override
public void run() {
while(true){
System.out.println("请输入要推送的新闻/消息(输入-1退出推送服务)");
String news = Utility.readString(100);
if (news.equals("-1")){
break;
}
//构建一个消息,群发消息
Message message = new Message();
message.setSender("服务器");
message.setMesType(MessageType.MESSAGE_TO_ALL_MES);
message.setContent(news);
message.setSendTime(new Date().toString());
System.out.println("服务器推送消息给所有人说:"+news);
//遍历当前所有的通讯线程,得到socket,并发送message
HashMap<String, ServerConnectClientThread> hm = ManageServerConnectClientThread.getHm();
Iterator<String> iterator = hm.keySet().iterator();
while(iterator.hasNext()){
String onlineUserID = iterator.next().toString();
try {
ObjectOutputStream oos =
new ObjectOutputStream(hm.get(onlineUserID).getSocket().getOutputStream());
oos.writeObject(message);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}
}
创建完该线程类,请不要忘记启动线程,我们在QQServer类下启动该线程。在System.out.println(“服务器在9999端口正在监听”);下补充以下代码即可。
System.out.println("服务器在9999端口正在监听");
//启动推送新闻线程服务
new Thread(new SendNewsToAllService()).start();
这样我们就完成服务器推送新闻的功能,下面我们对代码进行测试,如下图: