网络多线程编程-简单实现(模拟QQ的实现)
1.涉及到的知識(shí)
1.項(xiàng)目框架的設(shè)計(jì)
2.java面向?qū)ο缶幊?br /> 3.網(wǎng)絡(luò)編程
4.多線程編程
5.IO流的操作
6.Mysql/先使用集合來(lái)充當(dāng)內(nèi)存數(shù)據(jù)庫(kù)
核心功能的實(shí)現(xiàn),界面的優(yōu)化還是自己把握
項(xiàng)目吃透的意義:
1.網(wǎng)絡(luò)編程
2.IO編程
3.面向?qū)ο蟮木幊?br /> 4.多線程的管理
是對(duì)于這些的綜合運(yùn)用
用戶分析:
界面帶上
1.用戶登錄
2.拉取在線用戶列表
3.無(wú)異常退出(客戶端,服務(wù)端) 因?yàn)槭峭ㄟ^(guò)網(wǎng)絡(luò)進(jìn)行控制的
4.私聊的實(shí)現(xiàn)
5.群聊的實(shí)現(xiàn) (公網(wǎng)的實(shí)現(xiàn))
6.發(fā)文件的實(shí)現(xiàn):關(guān)鍵是給誰(shuí)發(fā)送過(guò)去信息是關(guān)鍵
7.文件服務(wù)器推送新聞
1.客戶端和服務(wù)端共同的代碼
Message:是消息的種類的類型,里面包含著消息的種類,以及其各種特征
MessageType:消息的類型(消息頭),對(duì)于不同的消息頭,區(qū)別不同的消息,客戶端服務(wù)端進(jìn)行不同方式的處理
User的作用:一個(gè)User就是一個(gè)登錄用戶的各種信息的集合
FileClientService的作用:從指定路徑獲取發(fā)送文件的數(shù)據(jù),將其存儲(chǔ)在message當(dāng)中,然后發(fā)送給服務(wù)端
ManageCilentConnectServerThread 的作用:管理客戶端連接服務(wù)端線程的集合以及管理,以及相關(guān)的操作
Message代碼如下
package com.lk0826.qqcommon;//表示客戶端和服務(wù)端通信時(shí)候的消息對(duì)象 import java.io.Serializable;public class Message implements Serializable {private static final long serivalVersionUID = 1L;private String sender; // 發(fā)送者private String getter;// 接收者private String content;// 消息內(nèi)容private String sendTime;// 發(fā)送時(shí)間private String mesType;// 消息類型【可以在接口當(dāng)中定義已經(jīng)知道的消息類型】private byte[] fileBytes;private int filelen = 0;private String dest;// 文件傳輸?shù)奈穆?/span>private String src;// 文件的原始位置public byte[] getFileBytes() {return fileBytes;}public void setFileBytes(byte[] fileBytes) {this.fileBytes = fileBytes;}public int getFilelen() {return filelen;}public void setFilelen(int filelen) {this.filelen = filelen;}public String getDest() {return dest;}public void setDest(String dest) {this.dest = dest;}public String getSrc() {return src;}public void setSrc(String src) {this.src = src;}public String getMesType() {return mesType;}public void setMesType(String mesType) {this.mesType = 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;}}MessageType的代碼如下:
package com.lk0826.qqcommon; //定義了不同的常量,這些不同的常量的值代表不同的消息類型 //IO/線程/網(wǎng)絡(luò)的運(yùn)用 public interface MessageType {String MESSAGE_LOGIN_SUCCEED = "1";//代表登錄成功String MESSAGE_LONGIN_FAIL = "2";//代表登錄失敗String MESSAGE_COMM_MES = "3";//普通的信息包String MESSAGE_GET_ONLINE_FRIEND = "4";//要求返回在線的用戶列表String MESSAGE_RET_ONLINE_FRIEND = "5";//返回用戶在線列表String MESSAGE_CLIENT_EXIT = "6";//客戶端請(qǐng)求退出的命令String MESSAGE_TOALL_MES = "7";//群發(fā)的消息包String MESSAGE_FILE_MES = "8";//發(fā)送文件的消息頭 }2.客戶端代碼
QQview的作用:QQ通信的界面設(shè)計(jì)
Utility的作用:作為工具類,從鍵盤讀取數(shù)據(jù)的操作和方法等等
ClientConnectServerThread的作用:作為客戶端和服務(wù)器進(jìn)行交流的線程
FileClientService的作用:客戶端發(fā)送文件的操作
UserClientService的作用:用戶向服務(wù)端請(qǐng)求的操作的方法
QQview的代碼如下:
Utility的代碼如下:
package com.lk0826.qqview; /**工具類的作用:處理各種情況的用戶輸入,并且能夠按照程序員的需求,得到用戶的控制臺(tái)輸入。 */import java.util.*; /***/ public class Utility {//靜態(tài)屬性。。。private static Scanner scanner = new Scanner(System.in);/*** 功能:讀取鍵盤輸入的一個(gè)菜單選項(xiàng),值:1——5的范圍* @return 1——5*/public static char readMenuSelection() {char c;for (; ; ) {String str = readKeyBoard(1, false);//包含一個(gè)字符的字符串c = str.charAt(0);//將字符串轉(zhuǎn)換成字符char類型if (c != '1' && c != '2' && c != '3' && c != '4' && c != '5') {System.out.print("選擇錯(cuò)誤,請(qǐng)重新輸入:");} else break;}return c;}/*** 功能:讀取鍵盤輸入的一個(gè)字符* @return 一個(gè)字符*/public static char readChar() {String str = readKeyBoard(1, false);//就是一個(gè)字符return str.charAt(0);}/*** 功能:讀取鍵盤輸入的一個(gè)字符,如果直接按回車,則返回指定的默認(rèn)值;否則返回輸入的那個(gè)字符* @param defaultValue 指定的默認(rèn)值* @return 默認(rèn)值或輸入的字符*/public static char readChar(char defaultValue) {String str = readKeyBoard(1, true);//要么是空字符串,要么是一個(gè)字符return (str.length() == 0) ? defaultValue : str.charAt(0);}/*** 功能:讀取鍵盤輸入的整型,長(zhǎng)度小于2位* @return 整數(shù)*/public static int readInt() {int n;for (; ; ) {String str = readKeyBoard(10, false);//一個(gè)整數(shù),長(zhǎng)度<=10位try {n = Integer.parseInt(str);//將字符串轉(zhuǎn)換成整數(shù)break;} catch (NumberFormatException e) {System.out.print("數(shù)字輸入錯(cuò)誤,請(qǐng)重新輸入:");}}return n;}/*** 功能:讀取鍵盤輸入的 整數(shù)或默認(rèn)值,如果直接回車,則返回默認(rèn)值,否則返回輸入的整數(shù)* @param defaultValue 指定的默認(rèn)值* @return 整數(shù)或默認(rèn)值*/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("數(shù)字輸入錯(cuò)誤,請(qǐng)重新輸入:");}}return n;}/*** 功能:讀取鍵盤輸入的指定長(zhǎng)度的字符串* @param limit 限制的長(zhǎng)度* @return 指定長(zhǎng)度的字符串*/public static String readString(int limit) {return readKeyBoard(limit, false);}/*** 功能:讀取鍵盤輸入的指定長(zhǎng)度的字符串或默認(rèn)值,如果直接回車,返回默認(rèn)值,否則返回字符串* @param limit 限制的長(zhǎng)度* @param defaultValue 指定的默認(rèn)值* @return 指定長(zhǎng)度的字符串*/public static String readString(int limit, String defaultValue) {String str = readKeyBoard(limit, true);return str.equals("")? defaultValue : str;}/*** 功能:讀取鍵盤輸入的確認(rèn)選項(xiàng),Y或N* 將小的功能,封裝到一個(gè)方法中.* @return Y或N*/public static char readConfirmSelection() {System.out.println("請(qǐng)輸入你的選擇(Y/N): 請(qǐng)小心選擇");char c;for (; ; ) {//無(wú)限循環(huán)//在這里,將接受到字符,轉(zhuǎn)成了大寫字母//y => Y n=>NString str = readKeyBoard(1, false).toUpperCase();c = str.charAt(0);if (c == 'Y' || c == 'N') {break;} else {System.out.print("選擇錯(cuò)誤,請(qǐng)重新輸入:");}}return c;}/*** 功能: 讀取一個(gè)字符串* @param limit 讀取的長(zhǎng)度* @param blankReturn 如果為true ,表示 可以讀空字符串。 * 如果為false表示 不能讀空字符串。* * 如果輸入為空,或者輸入大于limit的長(zhǎng)度,就會(huì)提示重新輸入。* @return*/private static String readKeyBoard(int limit, boolean blankReturn) {//定義了字符串String line = "";//scanner.hasNextLine() 判斷有沒(méi)有下一行while (scanner.hasNextLine()) {line = scanner.nextLine();//讀取這一行//如果line.length=0, 即用戶沒(méi)有輸入任何內(nèi)容,直接回車if (line.length() == 0) {if (blankReturn) return line;//如果blankReturn=true,可以返回空串else continue; //如果blankReturn=false,不接受空串,必須輸入內(nèi)容}//如果用戶輸入的內(nèi)容大于了 limit,就提示重寫輸入 //如果用戶如的內(nèi)容 >0 <= limit ,我就接受if (line.length() < 1 || line.length() > limit) {System.out.print("輸入長(zhǎng)度(不能大于" + limit + ")錯(cuò)誤,請(qǐng)重新輸入:");continue;}break;}return line;} }User的代碼如下:
package com.lk0826.qqcommon;import java.io.Serializable;//客戶的信息 //User對(duì)象要在網(wǎng)絡(luò)上面?zhèn)鬏?#xff0c;因此要用對(duì)象流,對(duì)象對(duì)應(yīng)的類需要序列化 public class User implements Serializable{private static final long serivalVersionUID = 1L;private String userid;//用戶名private String passwd;//用戶密碼public User() {}/*** @param userid* @param passwd*/public User(String userid, String passwd) {super();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;}}ClientConnectServerThread代碼如下:
package com.lk0831.QQclientservice; import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.net.Socket; import com.lk0826.qqcommon.Message; import com.lk0826.qqcommon.MessageType;public class ClientConnectServerThread extends Thread {// 該線程需要持有socketprivate Socket socket;// 這個(gè)構(gòu)造器在構(gòu)建的時(shí)候,可以構(gòu)建一個(gè)socket對(duì)象public ClientConnectServerThread(Socket socket) {this.socket = socket;}// 以后可能還需要獲取這個(gè)方法public Socket getSocket() {return socket;}// 這個(gè)包含socket的線程,需要在后臺(tái)和服務(wù)器通訊,因此需要一直存在,因此我們將其做成一個(gè)while循環(huán)public void run() {while (true) {// 讀取System.out.println("客戶端線程,等待讀取從服務(wù)器端口發(fā)送的消息");try {ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());// 如果服務(wù)器沒(méi)有發(fā)送Message對(duì)象,線程會(huì)阻塞在這里Message ms = (Message) ois.readObject();// 拿到消息以后就可以做其它的處理if (ms.getMesType().equals(MessageType.MESSAGE_RET_ONLINE_FRIEND)) {// 取出在線列表的信息,并且顯示String[] onlineUsers = ms.getContent().split(" ");System.out.println("\n=====當(dāng)前用戶列表如下===========");for (int i = 0; i < onlineUsers.length; i++) {System.out.println("用戶 :" + onlineUsers[i]);}} else if (ms.getMesType().equals(MessageType.MESSAGE_COMM_MES)) {System.out.println(ms.getSendTime());// 時(shí)間System.out.println("\n" + ms.getSender() + " 對(duì) " + ms.getGetter() + " 說(shuō) :" + ms.getContent());// 誰(shuí)對(duì)誰(shuí)說(shuō)怎樣的話語(yǔ)} else if (ms.getMesType().equals(MessageType.MESSAGE_TOALL_MES)) {// 顯示在客戶端的控制臺(tái)即可System.out.println("\n" + ms.getSender() + "對(duì)大家說(shuō):" + ms.getContent());} else if (ms.getMesType().equals(MessageType.MESSAGE_FILE_MES)) {System.out.println("\n"+ms.getSender()+"給"+ms.getGetter()+"發(fā)送文件從"+ms.getSrc()+"到我的電腦目錄"+ms.getDest());//取出message的文件字節(jié)數(shù)組,通過(guò)文件輸出流寫出到磁盤FileOutputStream fos = new FileOutputStream(ms.getDest());fos.write(ms.getFileBytes());//這種文件輸出流,發(fā)送了以后記得關(guān)閉即可fos.close();System.out.println("\n 保存文件成功---");} else {System.out.println("是其它類型的message,暫時(shí)不處理");}} catch (Exception e) {// TODO Auto-generated catch blocke.printStackTrace();}}}}FileClientService代碼如下:
package com.lk0831.QQclientservice;import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.ObjectOutputStream; import java.util.Date;import com.lk0826.qqcommon.Message; import com.lk0826.qqcommon.MessageType;//完成文件傳輸服務(wù) public class FileClientService {//原文件位置,目標(biāo)文件位置,發(fā)送用戶的id,接收用戶的idpublic void sendFiletoOne(String src,String dest,String senderId,String getterId) {//讀取src文件,將其封裝到Message message = new Message();message.setMesType(MessageType.MESSAGE_FILE_MES);message.setSender(senderId);message.setGetter(getterId);message.setSrc(src);message.setSendTime(new Date().toString());message.setDest(dest);//需要將文件進(jìn)行讀取FileInputStream fis = null ;File file = new File(src);if(file.exists()) {byte[] filebytes = new byte[(int)new File(src).length()];try {//檢查這個(gè)文件是否存在不存在就提示文件不存在//存在的話在進(jìn)行傳輸?shù)牟僮?/span>fis = new FileInputStream(src);fis.read(filebytes);//將文件讀入到字節(jié)數(shù)組當(dāng)中來(lái)//將文件字節(jié)數(shù)組,設(shè)置到我們的message對(duì)象當(dāng)中去message.setFileBytes(filebytes);} catch (Exception e) {// TODO Auto-generated catch blocke.printStackTrace();}finally {if(fis!=null) {try {fis.close();} catch (IOException e) {// TODO Auto-generated catch blocke.printStackTrace();}}}//提示信息System.out.println("\n"+senderId+"給"+getterId+"發(fā)送文件 :"+src+"到對(duì)方的電腦的目錄"+"dest");//發(fā)送try {ObjectOutputStream oos =new ObjectOutputStream(ManageCilentConnectServerThread.getClientConnectServerThread(senderId).getSocket().getOutputStream());oos.writeObject(message);} catch (IOException e) {// TODO Auto-generated catch blocke.printStackTrace();}}else {System.out.println("文件不存在:"+src);}}}ManageCilentConnectServerThread 的代碼如下:
package com.lk0831.QQclientservice; import java.util.HashMap; //管理客戶端連接到服務(wù)端的線程的一個(gè)類 public class ManageCilentConnectServerThread {private static HashMap<String,ClientConnectServerThread> hm = new HashMap<>();//將某個(gè)線程加入到這個(gè)集合當(dāng)中public static void addClientConnectServerThread(String userid,ClientConnectServerThread clientConnectServerThread) {hm.put(userid, clientConnectServerThread);}//通過(guò)一個(gè)方法 通過(guò)userid可以得到對(duì)應(yīng)的線程public static ClientConnectServerThread getClientConnectServerThread(String userid) {return hm.get(userid);} }UserClientService 的代碼如下
package com.lk0831.QQclientservice;import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.net.InetAddress; import java.net.Socket; import java.util.ArrayList; import java.util.Iterator;import com.lk0826.qqcommon.Message; import com.lk0826.qqcommon.MessageType; import com.lk0826.qqcommon.User;//退出的時(shí)候有大量的異常報(bào)出,是因?yàn)楫?dāng)我的一個(gè)服務(wù)器或者客戶端關(guān)閉了以后,另外一端還在不斷的尋求讀取 //看是否登錄成功 public class UserClientService {//User作為一個(gè)屬性的原因,可能在其它地方使用User信息private User u = new User();private Socket socket;//方法作用:判斷登錄是否成功public boolean checkUser(String userid,String pwd) {boolean b = false;u.setUserid(userid);u.setPasswd(pwd);//連接到服務(wù)端,并且發(fā)送u對(duì)象//因?yàn)閟ocket在其它地方也可能使用,所以也要將其存為屬性try {socket = new Socket(InetAddress.getLocalHost(),9999);//得到objetcOutputStream的一個(gè)對(duì)象//socket只能得到節(jié)點(diǎn)流ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());oos.writeObject(u);//發(fā)送User對(duì)象//讀取服務(wù)端回復(fù)的Message對(duì)象ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());Message ms = (Message)ois.readObject();//如果有一個(gè)要拋出異常的話,那就將其拋出異常的范圍變得更大ArrayList<Message> al =(ArrayList<Message>)ois.readObject();ArrayList<Message> al2 =(ArrayList<Message>)ois.readObject();if(ms.getMesType().equals(MessageType.MESSAGE_LOGIN_SUCCEED)) {//成功登錄的話,啟動(dòng)一個(gè)線程,讓這個(gè)線程持有這個(gè)socket,能讓服務(wù)器端一直和其保持通訊//所以要?jiǎng)?chuàng)建一個(gè)線程類ClientConnectServerThread//在將線程放在集合里面去管理//創(chuàng)建和服務(wù)器保持通信的線程,如果不做成一個(gè)線程的話,主線程只能做一件事情,不能額外的做事情Iterator<Message> iterator = al.iterator();while(iterator.hasNext()) {Message message = iterator.next();System.out.println(message.getSender()+"對(duì)"+message.getGetter()+"說(shuō)"+message.getContent());}Iterator<Message> iterator2 = al2.iterator();while(iterator2.hasNext()) {Message message2 = iterator2.next();FileOutputStream fos = new FileOutputStream(message2.getDest());fos.write(message2.getFileBytes());//這種文件輸出流,發(fā)送了以后記得關(guān)閉即可fos.close();System.out.println("\n 離線保存文件成功---");}ClientConnectServerThread ccst = new ClientConnectServerThread(socket);ccst.start();//一個(gè)客戶端可能有幾個(gè)線程和服務(wù)端進(jìn)行通信,所以線程可以放入到一個(gè)集合當(dāng)中去管理ManageCilentConnectServerThread.addClientConnectServerThread(userid, ccst);b = true;}else {//登錄失敗了,我們就不能啟動(dòng)和服務(wù)器通訊的線程b = false;socket.close();oos.close();}} catch (Exception e) {// TODO Auto-generated catch blocke.printStackTrace();}return b;}//向服務(wù)器端請(qǐng)求在線用戶列表public void onlineFriendList() {//發(fā)送一個(gè)MessageMessage message = new Message();message.setSender(u.getUserid());message.setMesType(MessageType.MESSAGE_GET_ONLINE_FRIEND);//發(fā)送給服務(wù)器,首先等到當(dāng)前線程的Socket 對(duì)應(yīng)的objectOutputStream//從管理線程的集合里面,得到userid對(duì)應(yīng)的線程,再?gòu)木€程里面得到對(duì)應(yīng)的socket,再?gòu)膕ocket里面得到對(duì)象輸出流Socket so = ManageCilentConnectServerThread.getClientConnectServerThread(u.getUserid()).getSocket();try {ObjectOutputStream oos = new ObjectOutputStream(so.getOutputStream());oos.writeObject(message);//發(fā)送一個(gè)message,向服務(wù)端要求在線用戶列表} catch (IOException e) {// TODO Auto-generated catch blocke.printStackTrace();}}//編寫方法,退出客戶端,并給服務(wù)端發(fā)送一個(gè)退出系統(tǒng)的message對(duì)象public void logout() {Message message = new Message();message.setMesType(MessageType.MESSAGE_CLIENT_EXIT);message.setSender(u.getUserid());//一定指定userid,這樣才不是亂關(guān)閉的try {//ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());//如果一個(gè)客戶端有多個(gè)線程,最好通過(guò)id的形式獲取到我們需要的socket,來(lái)進(jìn)行數(shù)據(jù)操作ObjectOutputStream oos = new ObjectOutputStream(ManageCilentConnectServerThread.getClientConnectServerThread(u.getUserid()).getSocket().getOutputStream());oos.writeObject(message);System.out.println(u.getUserid()+"退出系統(tǒng)");System.exit(0);//結(jié)束進(jìn)程} catch (IOException e) {// TODO Auto-generated catch blocke.printStackTrace();}}//}3.服務(wù)端代碼
QQFrame的作用:內(nèi)含main主方法,可以啟動(dòng)服務(wù)端
QQServer的主要作用:啟動(dòng)服務(wù)端,等待客戶端的連接,其中還包含著離線消息和離線文件的存儲(chǔ),判斷用戶登錄時(shí)候的賬號(hào)密碼是否輸入成功
SendNewstoAll的主要作用:這個(gè)類包含著服務(wù)器將消息發(fā)送給所有人(新聞推送的功能)
ServerConnectClientThread的主要作用:當(dāng)客戶端和服務(wù)端建立起來(lái)連接以后,服務(wù)端因?yàn)榻邮者B接的socket,保存在線程當(dāng)中,而這個(gè)線程獨(dú)立的保持運(yùn)行,和客戶端一直保持著通信
QQServer的代碼如下:
package com.lk0826.qqService;import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.net.ServerSocket; import java.net.Socket; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.concurrent.ConcurrentHashMap;import com.lk0826.qqcommon.Message; import com.lk0826.qqcommon.MessageType; import com.lk0826.qqcommon.User; //無(wú)異常退出等等的優(yōu)化操作 //客戶端正在讀取的時(shí)候,突然把服務(wù)器關(guān)閉了,就會(huì)出現(xiàn)一系列的問(wèn)題 //這是服務(wù)器,在監(jiān)聽(tīng)9999,等待客戶端 的連接,并且保持通信 //******離線文件,離線消息的擴(kuò)展功能 public class QQServer {private ServerSocket ss = null;//創(chuàng)建一個(gè)集合,存放多個(gè)用戶,如果是這些用戶登錄,就認(rèn)為是合法的private static HashMap<String,User> validUsers = new HashMap<>();private static ConcurrentHashMap<String, ArrayList<Message>> offOnlineDb = new ConcurrentHashMap<>();//這個(gè)集合來(lái)收集離線的文件的集合private static ConcurrentHashMap<String, ArrayList<Message>> offOnlineFileDb = new ConcurrentHashMap<>();//處理并發(fā)的集合 ConcurrentHashMap 可以處理并發(fā)的集合,沒(méi)有線程安全問(wèn)題//HashMap是沒(méi)有處理線程安全的,因此在多線程的情況下是不安全的 ConcurrentHashMap是線程同步的,在多線程情況下是安全的static {//在靜態(tài)代碼塊,初始化validUsersvalidUsers.put("100", new User("100","123456"));ArrayList<Message> al1 = new ArrayList<>();offOnlineDb.put("100", al1);offOnlineFileDb.put("100", al1);validUsers.put("200", new User("100","123456"));ArrayList<Message> al2 = new ArrayList<>();offOnlineDb.put("200", al2);offOnlineFileDb.put("200", al2);validUsers.put("300", new User("100","123456"));ArrayList<Message> al3 = new ArrayList<>();offOnlineDb.put("300", al3);offOnlineFileDb.put("300", al3);validUsers.put("至尊寶", new User("至尊寶","123456"));ArrayList<Message> al4 = new ArrayList<>();offOnlineDb.put("至尊寶", al4);offOnlineFileDb.put("至尊寶", al4);validUsers.put("紫霞仙子", new User("紫霞仙子","123456"));ArrayList<Message> al5 = new ArrayList<>();offOnlineDb.put("紫霞仙子", al5);offOnlineFileDb.put("紫霞仙子", al5);validUsers.put("菩提老祖", new User("菩提老祖","123456"));ArrayList<Message> al6 = new ArrayList<>();offOnlineDb.put("菩提老祖", al6);offOnlineFileDb.put("菩提老祖", al6);}//驗(yàn)證用戶是否有效的方法//當(dāng)用戶登錄成功以后,先遍歷一遍自己的離線列表,看有沒(méi)有message,有message就打印出來(lái),然后再刪除這個(gè)key//public static void addoffFile(String getterId,ArrayList<Message> al) {offOnlineFileDb.put(getterId, al);}//給離線的消息庫(kù)添加消息public static void addOffpnlineDb(String getterId,ArrayList<Message> al) {offOnlineDb.put(getterId, al);}private boolean checkUser(String userid,String passwd) {User user = validUsers.get(userid);//if if 過(guò)關(guān)斬將的判斷方式if(user == null) {//說(shuō)明userId沒(méi)有存在validreturn false;}if(!user.getPasswd().equals(passwd)) {//userId正確,但是密碼錯(cuò)誤return false;}return true;}//服務(wù)器端要實(shí)現(xiàn)的功能的方法//當(dāng)?shù)卿洺晒σ院?#xff0c;遍歷offOnlienDbpublic void offOlinechat(String usreid) {Iterator<String> iterator = offOnlineDb.keySet().iterator();while(iterator.hasNext()) {String s = iterator.next();if(s.equals(usreid)) {ArrayList<Message> al = offOnlineDb.get(s);ServerConnectClientThread scct = ManageClientThreads.getClientThread(s);try {ObjectOutputStream oos = new ObjectOutputStream(scct.getSocket().getOutputStream());oos.writeObject(al);//將離線的消息列表發(fā)送給對(duì)應(yīng)的客戶端} catch (IOException e) {// TODO Auto-generated catch blocke.printStackTrace();}}}}public QQServer() {//服務(wù)器給各個(gè)客戶端推送消息//服務(wù)器端專門啟動(dòng)一個(gè)線程,專門負(fù)責(zé)推送新聞try {System.out.println("服務(wù)端在9999端口監(jiān)聽(tīng)。。。");ss = new ServerSocket(9999);new Thread(new SendNewstoAll()).start();//并不是監(jiān)聽(tīng)到一個(gè)監(jiān)聽(tīng)的工作就不監(jiān)聽(tīng)了//監(jiān)聽(tīng)9999的這個(gè)工作,這是一個(gè)while循環(huán)while(true) {//當(dāng)和某個(gè)客戶端建立連接以后會(huì)繼續(xù)一直的監(jiān)聽(tīng)的,因此我們這里while循環(huán)Socket socket = ss.accept();//沒(méi)有客戶端連接的話,就會(huì)阻塞在這里//得到這個(gè)socket關(guān)聯(lián)的對(duì)象輸入流ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());User u = (User)ois.readObject();//向下轉(zhuǎn)型需要強(qiáng)制轉(zhuǎn)型,理論上面后臺(tái)是有一個(gè)數(shù)據(jù)庫(kù)來(lái)驗(yàn)證用戶名字和id是否正確的才對(duì)//驗(yàn)證用戶Message message = new Message();//設(shè)置在外面的原因是因?yàn)?#xff0c;無(wú)論登錄成功還是登錄失敗我們都需要回復(fù)一個(gè)消息才行if(checkUser(u.getUserid(), u.getPasswd())) {//創(chuàng)建一個(gè)Message對(duì)象,準(zhǔn)備回復(fù)客戶端message.setMesType(MessageType.MESSAGE_LOGIN_SUCCEED);//將message對(duì)象發(fā)送回復(fù)oos.writeObject(message);//登錄成功以后,取出離線的數(shù)據(jù)包ArrayList<Message> al = offOnlineDb.get(u.getUserid());oos.writeObject(al);offOnlineDb.remove(u.getUserid());ArrayList<Message> al2 = offOnlineFileDb.get(u.getUserid());oos.writeObject(al2);offOnlineFileDb.remove(u.getUserid());//創(chuàng)建一個(gè)線程,和客戶端保持通訊的狀態(tài),該線程需要持有這個(gè)socket對(duì)象,并且要將線程放在管理線程的集合當(dāng)中去ServerConnectClientThread scct =new ServerConnectClientThread(socket, u.getUserid());//啟動(dòng)線程,線程不會(huì)影響到主線程的工作scct.start();//把對(duì)象放到集合當(dāng)中進(jìn)行管理//這個(gè)ManageClientThreads可以看成一個(gè)倉(cāng)庫(kù)ManageClientThreads.addClientThread(u.getUserid(), scct);}else {//登錄失敗System.out.println("用戶 id = "+u.getUserid()+"pwd = "+u.getPasswd()+"驗(yàn)證失敗");message.setMesType(MessageType.MESSAGE_LONGIN_FAIL);oos.writeObject(message);//關(guān)閉socketsocket.close();}}} catch (Exception e) {// TODO Auto-generated catch blocke.printStackTrace();}finally{//如果服務(wù)器端退出了while,服務(wù)端不在監(jiān)聽(tīng),因此關(guān)閉ServerSockettry {ss.close();} catch (IOException e) {// TODO Auto-generated catch blocke.printStackTrace();}}} }SendNewstoAll的代碼如下:
package com.lk0826.qqService;import java.io.IOException; import java.io.ObjectOutputStream; import java.util.Date; import java.util.HashMap; import java.util.Iterator; import java.util.Scanner;import com.lk0826.Utils.Utility; import com.lk0826.qqcommon.Message; import com.lk0826.qqcommon.MessageType;public class SendNewstoAll implements Runnable{private Scanner scanner = new Scanner(System.in);public void run() {//為了可以推送多次新聞,我使用while循環(huán)while(true) {System.out.println("請(qǐng)輸入服務(wù)器要推送的消息/新聞【輸入exit表示退出推送服務(wù)】");String news = Utility.readString(1000);if("exit".equals(news)) {break;}Message message = new Message();message.setSender("服務(wù)器");message.setContent(news);message.setMesType(MessageType.MESSAGE_TOALL_MES);message.setSendTime(new Date().toString());System.out.println("服務(wù)器推送消息給所有人說(shuō) "+news);//下一步,遍歷當(dāng)前所有的通訊線程,并得到其socket,然后發(fā)送即可HashMap<String,ServerConnectClientThread> hm = ManageClientThreads.getHashMap();Iterator iterator = hm.keySet().iterator();while(iterator.hasNext()) {String s = iterator.next().toString();ServerConnectClientThread scct = hm.get(s);try {ObjectOutputStream oos =new ObjectOutputStream(scct.getSocket().getOutputStream());oos.writeObject(message);} catch (IOException e) {// TODO Auto-generated catch blocke.printStackTrace();}}}} }ServerConnectClientThread的代碼如下:
package com.lk0826.qqService; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.net.Socket; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator;import com.lk0826.qqcommon.Message; import com.lk0826.qqcommon.MessageType; //該類對(duì)應(yīng)的一個(gè)對(duì)象,和某個(gè)客戶端保持通訊 //它必須有一個(gè)socket屬性 public class ServerConnectClientThread extends Thread{private Socket socket;private String userid;//為了區(qū)分socket和哪一個(gè)用戶之間進(jìn)行的連接/*** @param socket* @param userid*/public ServerConnectClientThread(Socket socket, String userid) {super();this.socket = socket;this.userid = userid;}public Socket getSocket() {return socket;}//線程處于一個(gè)運(yùn)行的狀態(tài)//可以發(fā)送或者是接收消息public void run() {while(true) {try {System.out.println("服務(wù)端和客戶端"+userid+"保持通信,讀取信息");ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());Message message = (Message)ois.readObject();//Message是一種通訊的協(xié)議,是一種通訊的規(guī)則//根據(jù)message類型做出相應(yīng)的業(yè)務(wù)的處理,先處理拉取在線if(message.getMesType().equals(MessageType.MESSAGE_GET_ONLINE_FRIEND)) {System.out.println(message.getSender()+" 要在線用戶列表");String onlineUser = ManageClientThreads.getOnlineUser();//構(gòu)建一個(gè)Message對(duì)象,返回給客戶端Message message2 = new Message();message2.setMesType(MessageType.MESSAGE_RET_ONLINE_FRIEND);message2.setContent(onlineUser);//將用戶在線列表打過(guò)去message2.setGetter(message.getSender());ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());//線程里面本身就持有這個(gè)socket//返回給客戶端oos.writeObject(message2);}else if(message.getMesType().equals(MessageType.MESSAGE_CLIENT_EXIT)) {//客戶端退出的消息System.out.println(message.getSender() + "退出");//將和客戶端保持通信的線程從服務(wù)器集合當(dāng)中移除//ManageClientThreadsManageClientThreads.removeThread(message.getSender());socket.close();//關(guān)閉數(shù)據(jù)通道//退出線程break;}else if(message.getMesType().equals(MessageType.MESSAGE_COMM_MES)) {//根據(jù)message獲取 getterid ,在獲取對(duì)應(yīng)的線程//當(dāng)獲取消息的時(shí)候,發(fā)現(xiàn)服務(wù)器的管理線程里面沒(méi)有這個(gè)線程那樣的話,就采用離線處理的方式ServerConnectClientThread scct = ManageClientThreads.getClientThread(message.getGetter());if(scct==null) {//如果線程是空的代表還沒(méi)有登錄才去離線的處理方式ArrayList<Message> al = new ArrayList<>();al.add(message);QQServer.addOffpnlineDb(message.getGetter(),al);}else {ObjectOutputStream oos2 = new ObjectOutputStream(scct.getSocket().getOutputStream());oos2.writeObject(message);}//轉(zhuǎn)發(fā),提示如果客戶不在線,可以保存到數(shù)據(jù)庫(kù)當(dāng)中,這樣可以實(shí)現(xiàn)離線留言}else if(message.getMesType().equals(MessageType.MESSAGE_TOALL_MES)) {//需要編列管理線程的集合,把所有線程的socket得到,然后將message轉(zhuǎn)發(fā)即可HashMap<String,ServerConnectClientThread> hm = ManageClientThreads.getHashMap();Iterator<String> iterator = hm.keySet().iterator();//這個(gè)語(yǔ)句是什么意思while(iterator.hasNext()) {//集合的操作而已//取出在線用戶的idString s = iterator.next().toString();if(!s.equals(message.getSender())){ObjectOutputStream oos3 =new ObjectOutputStream( hm.get(s).getSocket().getOutputStream());oos3.writeObject(message);}}}else if(message.getMesType().equals(MessageType.MESSAGE_FILE_MES)) {//根據(jù)getterid獲取到對(duì)應(yīng)的線程,然后將message轉(zhuǎn)發(fā)//當(dāng)發(fā)送的文件的目標(biāo)對(duì)象的離線的時(shí)候ServerConnectClientThread scct2 = ManageClientThreads.getClientThread(message.getGetter());if(scct2==null) {//如果還未登錄ArrayList<Message> al2 = new ArrayList<>();al2.add(message);QQServer.addoffFile(message.getGetter(), al2);}else {ObjectOutputStream oos2 = new ObjectOutputStream(scct2.getSocket().getOutputStream());oos2.writeObject(message);}}else {System.out.println("其它類型的message暫時(shí)不處理");}} catch (Exception e) {// TODO Auto-generated catch blocke.printStackTrace();}}}}整體思路的設(shè)計(jì)和框架的搭建
1.首先我們?cè)O(shè)計(jì)服務(wù)端和客戶端都要有的共同的代碼,因?yàn)橛脩艉拖?#xff0c;消息類型種類,都是要進(jìn)入數(shù)據(jù)通道,也就是客戶端和服務(wù)端這兩種數(shù)據(jù)必須相同,所以兩端這幾種消息類型必須相同并且在同一個(gè)包當(dāng)中
2.對(duì)于要用對(duì)象流的操作,我們必須進(jìn)行序列化和反序列化的操作才行
3.注意在寫一個(gè)程序的時(shí)候首先是要將其主要功能跑通,然后每個(gè)類各司其職,最好每個(gè)線程都由一個(gè)線程的管理類的管理,先畫出大致的思路的圖形,這樣慢慢來(lái)結(jié)構(gòu)清晰
4.不熟悉代碼知識(shí)點(diǎn)的總結(jié)
- Message ms = (Message)ois.readObject(); 因?yàn)閷?duì)象輸出流,當(dāng)我們讀入的時(shí)候得到的是對(duì)象,需要用強(qiáng)制轉(zhuǎn)型將其轉(zhuǎn)換為Message
- ArrayList<Message> al =(ArrayList<Message>)ois.readObject();ArrayList<Message> al2 =(ArrayList<Message>)ois.readObject();if(ms.getMesType().equals(MessageType.MESSAGE_LOGIN_SUCCEED)) {//成功登錄的話,啟動(dòng)一個(gè)線程,讓這個(gè)線程持有這個(gè)socket,能讓服務(wù)器端一直和其保持通訊//所以要?jiǎng)?chuàng)建一個(gè)線程類ClientConnectServerThread//在將線程放在集合里面去管理//創(chuàng)建和服務(wù)器保持通信的線程,如果不做成一個(gè)線程的話,主線程只能做一件事情,不能額外的做事情Iterator<Message> iterator = al.iterator();while(iterator.hasNext()) {Message message = iterator.next();System.out.println(message.getSender()+"對(duì)"+message.getGetter()+"說(shuō)"+message.getContent());
數(shù)組可以轉(zhuǎn)換成為同樣的迭代器
iterator.hasNext()判斷迭代器的下一個(gè)是否還有數(shù)據(jù),當(dāng)我們到達(dá)迭代器末尾的時(shí)候,返回false
iterator取的是下一位迭代器當(dāng)中的數(shù)據(jù),剛開(kāi)始的時(shí)候指針指在迭代器的前面
舉例來(lái)說(shuō):開(kāi)始是頭部之前是空,下一個(gè)有數(shù)據(jù),所以取到下一個(gè)message,然后繼續(xù)循環(huán),判斷第一個(gè)數(shù)據(jù)后面有沒(méi)有,然后再next,取出數(shù)據(jù),將指針移動(dòng)到下一位的位置
hm.keyset()將哈希表的key變成一個(gè)集合,最后在將這個(gè)集合變?yōu)橐粋€(gè)迭代器,迭代器的內(nèi)容的類型
根據(jù)哈希表key值的類型決定,將在線的用戶取出用間隔隔開(kāi)轉(zhuǎn)換為String的類型,將其賦值在message的content當(dāng)中,然后將message發(fā)送過(guò)去
總結(jié)
以上是生活随笔為你收集整理的网络多线程编程-简单实现(模拟QQ的实现)的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: SAP笔记-物料移动类型和后勤自动科目设
- 下一篇: html无序列表透明圆点,html无序列