Java之Socket簡單聊天實現(QQ續二)

來源:互聯網
上載者:User

       轉載請註明出處,謝謝! 

       今天跟大家分享一下我那QQ小項目中伺服器與用戶端的核心代碼,並談談一些我的建議和看法,希望大家多多支援,你們的支援,就是我繼續分享的動力,哈哈!

        一、伺服器,好了,廢話不多說,我們先來看看伺服器部分,我這裡用到線程池,至於為什麼用線程池,不知道的童鞋可以去我的另一篇blog看看:http://blog.csdn.net/weidi1989/article/details/7930820。當一個使用者串連上之後,我們馬上將該使用者的socket丟入已經建好的線程池中去處理,這樣可以很快騰出時間來接受下一個使用者的串連,而線程池中的這個線程又分支為兩個線程,一個是讀訊息線程,一個是寫訊息線程,當然,因為我這個聊天是用來轉寄訊息的,所以還以單例模式建了一個Map用來存放每個使用者的寫訊息線程(如果使用者多的話,這是相當消耗資源的),以便在轉寄訊息的時候,通過Map的key就可以取出對應使用者的寫訊息線程,從而達到轉寄訊息的目的。具體下面再說

/** * 伺服器,接受使用者登入、離線、轉寄訊息 *  * @author way *  */public class Server {private ExecutorService executorService;// 線程池private ServerSocket serverSocket = null;private Socket socket = null;private boolean isStarted = true;//是否迴圈等待public Server() {try {// 建立線程池,池中具有(cpu個數*50)條線程executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() * 50);serverSocket = new ServerSocket(Constants.SERVER_PORT);} catch (IOException e) {e.printStackTrace();quit();}}public void start() {System.out.println(MyDate.getDateCN() + " 伺服器已啟動...");try {while (isStarted) {socket = serverSocket.accept();String ip = socket.getInetAddress().toString();System.out.println(MyDate.getDateCN() + " 使用者:" + ip + " 已建立串連");// 為支援多使用者並發訪問,採用線程池管理每一個使用者的串連請求if (socket.isConnected())executorService.execute(new SocketTask(socket));// 添加到線程池}if (socket != null)//迴圈結束後,記得關閉socket,釋放資源socket.close();if (serverSocket != null)serverSocket.close();} catch (IOException e) {e.printStackTrace();// isStarted = false;}}private final class SocketTask implements Runnable {private Socket socket = null;private InputThread in;private OutputThread out;private OutputThreadMap map;public SocketTask(Socket socket) {this.socket = socket;map = OutputThreadMap.getInstance();}@Overridepublic void run() {out = new OutputThread(socket, map);//// 先執行個體化寫訊息線程,(把對應使用者的寫線程存入map緩衝器中)in = new InputThread(socket, out, map);// 再執行個體化讀訊息線程out.setStart(true);in.setStart(true);in.start();out.start();}}/** * 退出 */public void quit() {try {this.isStarted = false;serverSocket.close();} catch (IOException e) {e.printStackTrace();}}public static void main(String[] args) {new Server().start();}}

二、伺服器寫訊息線程,接下來,我們來看看寫訊息線程,很簡單的一段代碼,有注釋,我就不多說了:

/** * 寫訊息線程 *  * @author way *  */public class OutputThread extends Thread {private OutputThreadMap map;private ObjectOutputStream oos;private TranObject object;private boolean isStart = true;// 迴圈標誌位private Socket socket;public OutputThread(Socket socket, OutputThreadMap map) {try {this.socket = socket;this.map = map;oos = new ObjectOutputStream(socket.getOutputStream());// 在構造器裡面執行個體化對象輸出資料流} catch (IOException e) {e.printStackTrace();}}public void setStart(boolean isStart) {//用於外部關閉寫線程this.isStart = isStart;}// 調用寫訊息線程,設定了訊息之後,喚醒run方法,可以節約資源public void setMessage(TranObject object) {this.object = object;synchronized (this) {notify();}}@Overridepublic void run() {try {while (isStart) {// 沒有訊息寫出的時候,線程等待synchronized (this) {wait();}if (object != null) {oos.writeObject(object);oos.flush();}}if (oos != null)// 迴圈結束後,關閉流,釋放資源oos.close();if (socket != null)socket.close();} catch (InterruptedException e) {e.printStackTrace();} catch (IOException e) {e.printStackTrace();}}}

 三、伺服器寫訊息線程緩衝器,接下來讓我們看一下那個寫訊息線程緩衝器的廬山真面目:

/** * 存放寫線程的緩衝器 *  * @author way */public class OutputThreadMap {private HashMap<Integer, OutputThread> map;private static OutputThreadMap instance;// 私人構造器,防止被外面執行個體化改對像private OutputThreadMap() {map = new HashMap<Integer, OutputThread>();}// 單例模式像外面提供該對象public synchronized static OutputThreadMap getInstance() {if (instance == null) {instance = new OutputThreadMap();}return instance;}// 添加寫線程的方法public synchronized void add(Integer id, OutputThread out) {map.put(id, out);}// 移除寫線程的方法public synchronized void remove(Integer id) {map.remove(id);}// 取出寫線程的方法,群聊的話,可以遍曆取出對應寫線程public synchronized OutputThread getById(Integer id) {return map.get(id);}// 得到所有寫線程方法,用於向所有線上使用者發送廣播public synchronized List<OutputThread> getAll() {List<OutputThread> list = new ArrayList<OutputThread>();for (Map.Entry<Integer, OutputThread> entry : map.entrySet()) {list.add(entry.getValue());}return list;}}

四、伺服器讀訊息線程,接下來是讀訊息線程,這裡包括兩個部分,一部分是讀訊息,另一部分是處理訊息,我以分開的形式貼出代碼,雖然我是寫在一個類裡面的:

/** * 讀訊息線程和處理方法 *  * @author way *  */public class InputThread extends Thread {private Socket socket;// socket對象private OutputThread out;// 傳遞進來的寫訊息線程,因為我們要給使用者回複訊息啊private OutputThreadMap map;//寫訊息線程緩衝器private ObjectInputStream ois;//對象輸入資料流private boolean isStart = true;//是否迴圈讀訊息public InputThread(Socket socket, OutputThread out, OutputThreadMap map) {this.socket = socket;this.out = out;this.map = map;try {ois = new ObjectInputStream(socket.getInputStream());//執行個體化對象輸入資料流} catch (IOException e) {e.printStackTrace();}}public void setStart(boolean isStart) {//提供介面給外部關閉讀訊息線程this.isStart = isStart;}@Overridepublic void run() {try {while (isStart) {// 讀取訊息readMessage();}if (ois != null)ois.close();if (socket != null)socket.close();} catch (ClassNotFoundException e) {e.printStackTrace();} catch (IOException e) {e.printStackTrace();}}

五、伺服器訊息處理,下面是處理訊息的方法,由於比較麻煩以及各種糾結,我就與讀訊息線程分開貼,顯得稍微簡潔一點:

 

/** * 讀訊息以及處理訊息,拋出異常 *  * @throws IOException * @throws ClassNotFoundException */public void readMessage() throws IOException, ClassNotFoundException {Object readObject = ois.readObject();// 從流中讀取對象UserDao dao = UserDaoFactory.getInstance();// 通過dao模式管理後台if (readObject != null && readObject instanceof TranObject) {TranObject read_tranObject = (TranObject) readObject;// 轉換成傳輸對象switch (read_tranObject.getType()) {case REGISTER:// 如果使用者是註冊User registerUser = (User) read_tranObject.getObject();int registerResult = dao.register(registerUser);System.out.println(MyDate.getDateCN() + " 新使用者註冊:"+ registerResult);// 給使用者回複訊息TranObject<User> register2TranObject = new TranObject<User>(TranObjectType.REGISTER);User register2user = new User();register2user.setId(registerResult);register2TranObject.setObject(register2user);out.setMessage(register2TranObject);break;case LOGIN:User loginUser = (User) read_tranObject.getObject();ArrayList<User> list = dao.login(loginUser);TranObject<ArrayList<User>> login2Object = new TranObject<ArrayList<User>>(TranObjectType.LOGIN);if (list != null) {// 如果登入成功TranObject<User> onObject = new TranObject<User>(TranObjectType.LOGIN);User login2User = new User();login2User.setId(loginUser.getId());onObject.setObject(login2User);for (OutputThread onOut : map.getAll()) {onOut.setMessage(onObject);// 廣播一下使用者上線}map.add(loginUser.getId(), out);// 先廣播,再把對應使用者id的寫線程存入map中,以便轉寄訊息時調用login2Object.setObject(list);// 把好友名單加入回複的對象中} else {login2Object.setObject(null);}out.setMessage(login2Object);// 同時把登入資訊回複給使用者System.out.println(MyDate.getDateCN() + " 使用者:"+ loginUser.getId() + " 上線了");break;case LOGOUT:// 如果是退出,更新資料庫線上狀態,同時群發告訴所有線上使用者User logoutUser = (User) read_tranObject.getObject();int offId = logoutUser.getId();System.out.println(MyDate.getDateCN() + " 使用者:" + offId + " 下線了");dao.logout(offId);isStart = false;// 結束自己的讀迴圈map.remove(offId);// 從緩衝的線程中移除out.setMessage(null);// 先要設定一個空訊息去喚醒寫線程out.setStart(false);// 再結束寫線程迴圈TranObject<User> offObject = new TranObject<User>(TranObjectType.LOGOUT);User logout2User = new User();logout2User.setId(logoutUser.getId());offObject.setObject(logout2User);for (OutputThread offOut : map.getAll()) {// 廣播使用者下線訊息offOut.setMessage(offObject);}break;case MESSAGE:// 如果是轉寄訊息(可添加群發)// 擷取訊息中要轉寄的對象id,然後擷取緩衝的該對象的寫線程int id2 = read_tranObject.getToUser();OutputThread toOut = map.getById(id2);if (toOut != null) {// 如果使用者線上toOut.setMessage(read_tranObject);} else {// 如果為空白,說明使用者已經下線,回複使用者TextMessage text = new TextMessage();text.setMessage("親!對方不線上哦,您的訊息將暫時儲存在伺服器");TranObject<TextMessage> offText = new TranObject<TextMessage>(TranObjectType.MESSAGE);offText.setObject(text);offText.setFromUser(0);out.setMessage(offText);}break;case REFRESH:List<User> refreshList = dao.refresh(read_tranObject.getFromUser());TranObject<List<User>> refreshO = new TranObject<List<User>>(TranObjectType.REFRESH);refreshO.setObject(refreshList);out.setMessage(refreshO);break;default:break;}}}

好了,伺服器的核心代碼就這麼一些了,很簡單吧?是的,因為我們還有很多事情沒有去做,比如說心跳監測使用者是否一直線上,如果不線上,就釋放資源等,這些都是商業項目中必須要考慮到的問題,至於這個通過心跳監測使用者是否線上,我說說我的一些想法吧:由用戶端定時給伺服器發送一個心跳包(最好是空包,節約流量),伺服器也定時去監測那個心跳包,如果有3次未收到用戶端的心跳包,就判斷該使用者已經掉線,釋放資源,至於這次數和時間間隔,就隨情況而定了。如果有什麼更好的其他建議,歡迎給我留言,謝謝。

 

六、訊息傳輸對象,下面,我們來看看,這個超級訊息對象和定義好的訊息類型:

/** * 傳輸的對象,直接通過Socket傳輸的最大對象 *  * @author way */public class TranObject<T> implements Serializable {/** *  */private static final long serialVersionUID = 1L;private TranObjectType type;// 發送的訊息類型private int fromUser;// 來自哪個使用者private int toUser;// 發往哪個使用者private T object;// 傳輸的對象,這個對象我們可以自訂任何private List<Integer> group;// 群發給哪些使用者get...set...

 

/** * 傳輸物件類型 *  * @author way *  */public enum TranObjectType {REGISTER, // 註冊LOGIN, // 使用者登入LOGOUT, // 使用者退出登入FRIENDLOGIN, // 好友上線FRIENDLOGOUT, // 好友下線MESSAGE, // 使用者發送訊息UNCONNECTED, // 無法串連FILE, // 傳輸檔案REFRESH,//重新整理好友名單}

 

 

七、用戶端,然後是用戶端部分了,其實跟伺服器差不多,只是沒有建立線程池了,因為沒有必要,是吧?然後執行個體化寫線程和讀線程沒有先後順序,這也勉強算一個區別吧~呵呵

/** * 用戶端 *  * @author way *  */public class Client {private Socket client;private ClientThread clientThread;private String ip;private int port;public Client(String ip, int port) {this.ip = ip;this.port = port;}public boolean start() {try {client = new Socket();// client.connect(new InetSocketAddress(Constants.SERVER_IP,// Constants.SERVER_PORT), 3000);client.connect(new InetSocketAddress(ip, port), 3000);if (client.isConnected()) {// System.out.println("Connected..");clientThread = new ClientThread(client);clientThread.start();}} catch (IOException e) {e.printStackTrace();return false;}return true;}// 直接通過client得到讀線程public ClientInputThread getClientInputThread() {return clientThread.getIn();}// 直接通過client得到寫線程public ClientOutputThread getClientOutputThread() {return clientThread.getOut();}// 直接通過client停止讀寫訊息public void setIsStart(boolean isStart) {clientThread.getIn().setStart(isStart);clientThread.getOut().setStart(isStart);}public class ClientThread extends Thread {private ClientInputThread in;private ClientOutputThread out;public ClientThread(Socket socket) {in = new ClientInputThread(socket);out = new ClientOutputThread(socket);}public void run() {in.setStart(true);out.setStart(true);in.start();out.start();}// 得到讀訊息線程public ClientInputThread getIn() {return in;}// 得到寫訊息線程public ClientOutputThread getOut() {return out;}}}

八、用戶端寫訊息線程,先看看用戶端寫訊息線程吧:

/** * 用戶端寫訊息線程 *  * @author way *  */public class ClientOutputThread extends Thread {private Socket socket;private ObjectOutputStream oos;private boolean isStart = true;private TranObject msg;public ClientOutputThread(Socket socket) {this.socket = socket;try {oos = new ObjectOutputStream(socket.getOutputStream());} catch (IOException e) {e.printStackTrace();}}public void setStart(boolean isStart) {this.isStart = isStart;}// 這裡處理跟伺服器是一樣的public void setMsg(TranObject msg) {this.msg = msg;synchronized (this) {notify();}}@Overridepublic void run() {try {while (isStart) {if (msg != null) {oos.writeObject(msg);oos.flush();if (msg.getType() == TranObjectType.LOGOUT) {// 如果是發送下線的訊息,就直接跳出迴圈break;}synchronized (this) {wait();// 發送完訊息後,線程進入等待狀態}}}oos.close();// 迴圈結束後,關閉輸出資料流和socketif (socket != null)socket.close();} catch (InterruptedException e) {e.printStackTrace();} catch (IOException e) {e.printStackTrace();}}}

九、用戶端讀訊息線程,然後是用戶端讀訊息線程,這裡又有一個要注意的地方,我們收到訊息的時候,是不是要告訴使用者?如何告訴呢?介面監聽貌似是一個很好的辦法,神馬?不知道介面監聽?你會用Android的setOnClickListener不?這就是android封裝好的點擊事件監聽,不懂的話,可以好好看看,理解一下,其實也不難:

/** * 用戶端讀訊息線程 *  * @author way *  */public class ClientInputThread extends Thread {private Socket socket;private TranObject msg;private boolean isStart = true;private ObjectInputStream ois;private MessageListener messageListener;// 訊息監聽介面對象public ClientInputThread(Socket socket) {this.socket = socket;try {ois = new ObjectInputStream(socket.getInputStream());} catch (IOException e) {e.printStackTrace();}}/** * 提供給外部的訊息監聽方法 *  * @param messageListener *            訊息監聽介面對象 */public void setMessageListener(MessageListener messageListener) {this.messageListener = messageListener;}public void setStart(boolean isStart) {this.isStart = isStart;}@Overridepublic void run() {try {while (isStart) {msg = (TranObject) ois.readObject();// 每收到一條訊息,就調用介面的方法,並傳入該訊息對象,外部在實現介面的方法時,就可以及時處理傳入的訊息對象了// 我不知道我有說明白沒有?messageListener.Message(msg);}ois.close();if (socket != null)socket.close();} catch (ClassNotFoundException e) {e.printStackTrace();} catch (IOException e) {e.printStackTrace();}}/** * 訊息監聽介面 *  * @author way *  */public interface MessageListener {public void Message(TranObject msg);}}

好了,總算copy完了,如果大家有什麼不理解,或者有什麼好的建議,歡迎給我留言,記住:你們的支援是我繼續下去的動力,哈哈
 

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.