心得感悟
一开始说要实现私聊和群聊时并没有想到可以用标识,有的时候还是感觉自己的程序分析能力不是很好,思维不够开阔,希望写多了Demo后有所进步吧。老师布置的实现文件传输、发语音等功能我还是不会做,等以后会做了,再继续完善这个Demo。
内容简概
- 一、程序整体结构分析
- 二、编写ChatProtocol(标识)类
- 三、编写Server(服务器端)类
- 四、编写Client(客户端)类
- 五、编写UserManager(用户管理)类
- 六、运行效果
具体内容
一、程序整体结构分析
1. 实现的界面效果
下面模拟三个用户的聊天界面,按照数字序号阅读
,你可能并不知道a+和p+是什么,但没关系我下面会讲到。
2. 程序流程分析
首先,运行服务器后,客户端需要登录,既然有登录就要判断是否已经登录过。登录成功,则可以选择群聊或者私聊,登录失败则需要重新登录。如何实现判断呢?接着往下面看。
3. 实现判断登录
要判断,那就要有对比
。首先,我们需要保存已经登录用户的信息,然后拿正在登录的用户的信息和前者相比,如果已经存在,则登录失败(不能重复登录),如果不存在,则登录成功。而且要实现网络通信,就必须保存用户的IP地址,所以我们可以用映射关系来保存用户登录信息:用户名——socket
,而多个映射关系又可以用Map集合来保存
。
4. 实现判断输入内容
由于我们是在Android studio模拟QQ,输入框只有一个,无法模拟私聊窗口和群聊窗口,故需要判断用户在聊天框输入的内容究竟是私聊还是群聊。
是不是可以有什么东西能够当做判断的标识呢
?之前我们讲到接口可以制定一套规范
,所以我们可以通过接口来制定一套判断的规范
,只不过里面没有方法。规范如下:
判断的内容 | 标识 |
---|---|
用户登录 | u+(u+用户名u+) |
登录成功 | 1 |
登录失败 | -1 |
分割用户名与消息 | ♥(用户名♥聊天内容) |
私聊 | p+(p+用户名♥聊天内容p+) |
群聊 | a+(a+聊天信息a+) |
还不理解也没有关系,接着往下看。
二、编写ChatProtocol(标识)类
public interface ChatProtocol {
// 登录
String LOGIN_FLAG = "u+";
// 私聊
String PRIVATE_FLAG = "p+";
// 群聊
String PUBLIC_FLAG = "a+";
// 分隔符
String SPLIT_FLAG = "♥";
// 成功/失败的状态
String SUCCESS = "1";
String FAILURE = "-1";
}
三、编写Server(服务器端)类
创建服务器端的主线程,
将终端的输入发送给客户端
。
public class Server {
// 用于保存每一个用户对应的姓名和Socket
public static UserManager manager = new UserManager();
public static void main(String[] args){
// 创建ServerSocket
try(ServerSocket ss = new ServerSocket(8888)) {
// 监听所有来连接的客户端
while (true){
Socket socket = ss.accept();
// 让子线程处理这个Socket
new ServerThread(socket).start();
}
} catch (IOException e) {}
}
}
创建一个服务器端的子线程,
处理服务器端接收客户端数据
。
class ServerThread extends Thread{
private Socket socket;
public ServerThread(Socket socket){ this.socket = socket; }
@Override
public void run() {
BufferedReader br = null;
PrintStream ps = null;
try {
// 得到对应的输入流对象
br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
// 得到对应的输出流
ps = new PrintStream(socket.getOutputStream());
String line = null;
while ((line = br.readLine()) != null){
// 登录u+ ... u+
if (line.startsWith(ChatProtocol.LOGIN_FLAG) && line.endsWith(ChatProtocol.LOGIN_FLAG)){
//u+jacku+
// 获取名字
int index = line.lastIndexOf(ChatProtocol.LOGIN_FLAG);
String name = line.substring(2,index);
System.out.println(name);
// 判断这个用户是否已经登录
if (Server.manager.islogined(name)){
// 登录过了
// 发送结果给客户端
ps.println(ChatProtocol.FAILURE);
}
else {
// 没有登录
// 保存当前登录的用户信息
Server.manager.save(name,socket);
// 发送结果给客户端
ps.println(ChatProtocol.SUCCESS);
}
}
// 判断是不是私聊
else if (line.startsWith(ChatProtocol.PRIVATE_FLAG) && line.endsWith(ChatProtocol.PRIVATE_FLAG)) {
// p+jack♥hellop+
// 获取信息
int index = line.lastIndexOf(ChatProtocol.PRIVATE_FLAG);
String msg = line.substring(2,index);
// 分割 jack hello
String[] items = msg.split(ChatProtocol.SPLIT_FLAG);
// 用户名
String name = items[0];
// 聊天内容
String message = items[1];
// 通过用户名找到对应的socket
Socket desSocket = Server.manager.socketByName(name);
PrintStream desPs = new PrintStream(desSocket.getOutputStream());
// 获取当前用户的名称
String currentName = Server.manager.nameBySocket(socket);
// 发送私聊消息
desPs.println(currentName+"向你发来私聊:"+ message);
}else {
// 群聊
// 处理数据
String msg = line.substring(2,line.length()-2);
// 获取当前用户的名称
String currentName = Server.manager.nameBySocket(socket);
// 遍历所有的用户信息
Collection<Socket> sockets = Server.manager.allUsers();
for(Socket s:sockets){
PrintStream tempps = new PrintStream(s.getOutputStream());
tempps.println(currentName+"发来群聊:"+msg);
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
四、编写Client(客户端)类
创建客户端的主线程,
将终端的输入发送给服务器端
。
public class Client {
public static void main(String[] args){
BufferedReader br = null;
PrintStream ps = null;
BufferedReader brServer = null;
// 连接服务器
try ( Socket socket = new Socket("127.0.0.1",8888)){
// 登录
// 接收终端输入流
br = new BufferedReader(new InputStreamReader(System.in));
// 发给服务器端的输入流
ps = new PrintStream(socket.getOutputStream());
// 接收服务器端的输入流
brServer = new BufferedReader(new InputStreamReader(socket.getInputStream()));
while (true){
// 接收终端输入信息
String line = JOptionPane.showInputDialog("请输入姓名");
// 拼接登录格式
String loginStr = ChatProtocol.LOGIN_FLAG+line+ChatProtocol.LOGIN_FLAG;
// 发送给服务器端
ps.println(loginStr);
// 接收服务器端返回的结果
String result = brServer.readLine();
// 判断登录结果
if (result.equals(ChatProtocol.SUCCESS)){
System.out.println("登录成功");
break;
}else {
System.out.println("用户名已存在 请重新登录");
}
}
// 登陆成功
// 开启线程处理服务器端的输入
new ClientThread(socket).start();
// 接收终端输入 发送给服务器端
String line = null;
while ((line = br.readLine()) != null){
// 发送给服务器
System.out.println(line);
ps.println(line);
}
} catch (IOException e) {}
}
}
创建一个客户端的子线程,
处理客户端接收服务器端数据
。
class ClientThread extends Thread{
private Socket socket;
// 保存socket对象
public ClientThread(Socket socket){
this.socket = socket;
}
@Override
public void run() {
BufferedReader br = null;
try {
// 获取服务器端的输入流对象
br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
// 读取数据
String line = null;
while ((line = br.readLine()) != null){
System.out.println(line);
}
} catch (IOException e) {
System.out.println("网络出错");
}finally {
try {
if (br != null){
br.close();
}
if (socket != null){
socket.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
五、编写UserManager(用户管理)类
创建一个管理用户信息的类,用于
管理所有的登录的用户
Map<String,Socket>,并判断某个用户是否已经登录
。
public class UserManager {
// 保存所有用户信息
private Map<String,Socket> users = new HashMap<>();
/**
* 判断用户是否已经登录
*/
public boolean islogined(String name){
// 遍历数组
for (String key:users.keySet()){
if (key.equals(name)){
return true;
}
}
return false;
}
/**
* 保存当前登录的用户信息
*/
public void save(String name,Socket socket){
users.put(name,socket);
}
/**
* 通过用户名找到对应的socket
*/
public Socket socketByName(String name){
return users.get(name);
}
/**
* 通过Socket对象找到对应的名称
*/
public String nameBySocket(Socket socket){
for (String key:users.keySet()){
// 取出这个key对应的Socket
if (socket == users.get(key)){
return key;
}
}
return null;
}
/**
* 获取所有人的socket对象
*/
public synchronized Collection<Socket> allUsers(){
return users.values();
}
}
六、运行效果
在本机运行时,可以创建多个Client类模拟多个客户端。我这里再创建一个Client1和Client2。
运行效果图顺序为:私聊→群聊