概述
无论在面试还是自己写代码的时候,对网络编程和多线程总是卡壳,自己写也写不明白,背八股也懵懵懂懂,最近系统学了一下网络编程,融合多线程,实现了一个简易聊天系统,分为客户端和服务端。
客户端
- Message:消息类,用于封装消息发送者、接收者、内容、发送时间、消息类型。
- MessageType:用来标识不同的消息类型。
- User:封装用户信息。
- QQview:用于模拟用户操作页面。
- Utility:读取用户输入工具类。
- UserClientService:校验用户登录,开启线程与服务端建立连接,退出登录等功能。
- MessageClientService:实现用户发送信息,群发信息方法。
- ClientConnectServerThread:每建立一个连接,都需要开启一个新线程。
- ManageClientConnectServerThread:管理用户连接线程,存放在一个Hashmap中。
服务端
- Message/MessageType/User/Utility:与客户端一致。
- QQServer:开启服务器。
- ServerConnectClientThread:每接收到客户端连接,开启一个线程保持连接。
- ManageClientThread:管理用户连接线程。
- SendNewsToAllService:群发消息功能实现。
具体实现流程
本案例实现了用户登录、显示登录用户列表、私聊消息、群发消息。
用户登录
开启服务端
开启客户端
客户端提示输入用户名密码;
调用userClientService.checkUser,checkUser中做了与服务端建立socket连接,并创建线程的操作
socket = new Socket(InetAddress.getByName("127.0.0.1"),9999);
ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
oos.writeObject(u);
ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
Message ms = (Message)ois.readObject();
if(ms.getMesType().equals(MessageType.MESSAGE_LOGIN_SUCCEED)){
b = true;
//创建一个和服务器端保持通讯的线程
ClientConnectServerThread ccst = new ClientConnectServerThread(socket);
ccst.start();
//为了后面客户端扩展,方便管理
ManageClientConnectServerThread.addClientConnectServerThread(userId, ccst);
b = true;
}else{
//登录失败,就不能启动和服务器通讯的线程,关闭socket
socket.close();
}
与此同时,服务端一直在监听连接请求,每得到一个请求,都要开启一个新线程,同时放入集合进行管理
System.out.println("服务端在9999端口监听...");
new Thread(new SendNewsToAllService()).start();
ss = new ServerSocket(9999);
while(true){
Socket socket = ss.accept();
ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
User u = (User) ois.readObject();
Message message = new Message();
ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
if(checkUser(u.getUserId(), u.getPassword())){
message.setMesType(MessageType.MESSAGE_LOGIN_SUCCEED);
oos.writeObject(message);
//创建一个线程,和客户端保持通信,该线程持有socket对象
ServerConnectClientThread serverConnectClientThread = new ServerConnectClientThread(socket, u.getUserId());
serverConnectClientThread.start();
//放入集合中进行管理
ManageClientThread.addClientThread(u.getUserId(), serverConnectClientThread);
}else{
System.out.println("用户id = "+ u.getUserId() +"密码 = "+ u.getPassword() +"验证失败");
message.setMesType(MessageType.MESSAGE_LOGIN_FAIL);
oos.writeObject(message);
socket.close();
}
}
注意代码中,登录成功后,封装Message信息返回给客户端,客户端接收到后进入二级页面
if(userClientService.checkUser(userId, pwd)) {
System.out.println("欢迎用户" + userId + "登录成功");
while (loop) {
System.out.println("网络通讯二级系统"+ userId + "登录");
System.out.println("\t\t 1 显示在线用户列表");
System.out.println("\t\t 2 群发消息");
System.out.println("\t\t 3 私聊消息");
System.out.println("\t\t 4 发送文件");
System.out.println("\t\t 9 退出系统");
System.out.println("请输入你的选择");
key = Utility.readString(1);
switch (key) {
case "1":
System.out.println("显示在线用户列表");
userClientService.getOnlineFriendList();
break;
case "2":
System.out.println("请输入你想对大家说的话");
String s = Utility.readString(100);
messageClientService.sendMessageToAll(s, userId);
break;
case "3":
System.out.println("请输入想要聊天的用户号(在线)");
String getterId = Utility.readString(50);
System.out.println("请输入想说的话:");
String content = Utility.readString(100);
messageClientService.sendMessage(content, userId, getterId);
break;
case "4":
System.out.println("发送文件");
break;
case "9":
userClientService.logout();
loop = false;
break;
}
}
}else{
System.out.println("登录失败");
}
显示在线用户列表
选择1后,向服务端发送请求,获取当前在线用户列表
case "1":
System.out.println("显示在线用户列表");
userClientService.getOnlineFriendList();
break;
getOnlineFriengList方法封装消息,并通过已有的线程,获取socket,获取输出流,将该请求对象输出到服务器端
public void getOnlineFriendList(){
Message message = new Message();
message.setMesType(MessageType.MESSAGE_GET_ONLINE_FRIEND);
message.setSender(u.getUserId());
try {
ClientConnectServerThread clientConnectServerThread = ManageClientConnectServerThread.getClientConnectServerThread(u.getUserId());
Socket socket = clientConnectServerThread.getSocket();
ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
oos.writeObject(message);//发送一个meesage对象,向服务器要在线用户列表
}catch (Exception e){
e.printStackTrace();
}
}
服务端收到消息后,判定MessageType为MESSAGE_GET_ONLINE_FRIEND,调用管理线程的方法,获取当前所有用户名称,同时创建一个message,设置内容,接收者、消息类型等信息,获取socket输出流,输出给客户端。
if(message.getMesType().equals(MessageType.MESSAGE_GET_ONLINE_FRIEND)){
System.out.println(message.getSender()+"要求在线用户列表");
String onlineUser = ManageClientThread.getOnlineUser();
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);
}
因为hashmap中存储的key为用户名、value为线程对象,所以遍历得到key就可以了
public static String getOnlineUser(){
Iterator<String> iterator = map.keySet().iterator();
String onLineUserlist = "";
while(iterator.hasNext()){
onLineUserlist += iterator.next().toString() + " ";
}
return onLineUserlist;
}
客户端判断消息类型为MESSAGE_RET_ONLINE_FRIEND,将得到的Message遍历显示在控制台
if(ms.getMesType().equals(MessageType.MESSAGE_RET_ONLINE_FRIEND)){
String[] onLineUser = ms.getContent().split(" ");
System.out.println("=======当前在线用户列表=======");
for (int i = 0; i < onLineUser.length; i++){
System.out.println("用户:" + onLineUser[i]);
}
}
私聊消息
控制台输入3,进行私聊功能,选定聊天对象,输入想说的话,调用sendMessage方法
case "3":
System.out.println("请输入想要聊天的用户号(在线)");
String getterId = Utility.readString(50);
System.out.println("请输入想说的话:");
String content = Utility.readString(100);
messageClientService.sendMessage(content, userId, getterId);
break;
sendMessage封装发送者接收者,发送内容,消息类型,同时获取socket与输出流,将message发送至服务器端
public void sendMessage(String content, String senderId, String getterId){
Message message = new Message();
message.setSender(senderId);
message.setGetter(getterId);
message.setContent(content);
message.setMesType(MessageType.MESSAGE_COMM_MES);
message.setSendTime(new Date().toString());
System.out.println(senderId + "要对" + getterId + "发消息");
try {
ObjectOutputStream oos = new ObjectOutputStream(ManageClientConnectServerThread.getClientConnectServerThread(senderId).getSocket().getOutputStream());
oos.writeObject(message);
}catch (Exception e){
e.printStackTrace();
}
}
服务端收到消息后,判别消息类型为MESSAGE_COMM_MES,得到message中的getter,也就是接收者,在通过管理线程的方法中,获取该getter的线程,进而获取其socket,继而获取输出流,将该message转发至该客户端。也就是说在私聊功能中,客户端没有对Message做任何处理,只是得到了该消息的接收者,获取其通道,将消息转发给该接收者。主要起转发作用。
else if(message.getMesType().equals(MessageType.MESSAGE_COMM_MES)) {
ServerConnectClientThread serverConnectClientThread = ManageClientThread.getServerConnectClientThread(message.getGetter());
ObjectOutputStream oos = new ObjectOutputStream(serverConnectClientThread.getSocket().getOutputStream());
oos.writeObject(message);
}
客户端收到message后,判断其类型为MESSAGE_COMM_MES,在控制台输出谁想对谁说啥
else if(ms.getMesType().equals(MessageType.MESSAGE_COMM_MES)){
System.out.println("\n" + ms.getSender() + "对" + ms.getGetter() + "说:" + ms.getContent());
}
群发消息
与私聊类似,区别在于,服务端转发时,不仅仅是给getter一个人转了,而是要遍历所有用户,都转发,也就是在管理集合中,获取到所有除发送者之外的线程,进行转发
else if(message.getMesType().equals(MessageType.MESSAGE_TO_ALL)){
HashMap<String, ServerConnectClientThread> map = ManageClientThread.getMap();
Iterator<String> iterator = map.keySet().iterator();
while(iterator.hasNext()){
String onLineUserId = iterator.next().toString();
if(!onLineUserId.equals(message.getSender())){
ObjectOutputStream oos = new ObjectOutputStream(map.get(onLineUserId).getSocket().getOutputStream());
oos.writeObject(message);
}
}
}
客户端也类似
else if(ms.getMesType().equals(MessageType.MESSAGE_TO_ALL)){
System.out.println("\n" + ms.getSender() + "对大家说:" + ms.getContent());
}
服务器群发消息
不同于客户端群发消息,客户端群发是发送消息至服务器,服务器转发给其他客户端。
而服务器群发是,服务器在启动时,也开启了一个线程,主动接受在控制台输入的内容,并已群发的形式,遍历所有用户发送至客户端(此行为与用户群发类似)
public class SendNewsToAllService extends Thread {
private Message message = new Message();
@Override
public void run() {
while (true) {
message.setMesType(MessageType.MESSAGE_TO_ALL);
message.setSender("服务器");
message.setSendTime(new Date().toString());
System.out.println("请输入你想推送的全体消息:(输入exit表示退出!)");
String s = Utility.readString(100);
if(s.equals("exit")) break;
message.setContent(s);
HashMap<String, ServerConnectClientThread> map = ManageClientThread.getMap();
Iterator<String> iterator = map.keySet().iterator();
while(iterator.hasNext()){
String user = iterator.next();
try {
ObjectOutputStream oos = new ObjectOutputStream(ManageClientThread.getServerConnectClientThread(user).getSocket().getOutputStream());
oos.writeObject(message);
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
客户端与群发消息的代码完全一致,因为消息类型都为MessageType.MESSAGE_TO_ALL
else if(ms.getMesType().equals(MessageType.MESSAGE_TO_ALL)){
System.out.println("\n" + ms.getSender() + "对大家说:" + ms.getContent());
}
总结
待定!