基於Java NIO的即時聊天服務器模型

http://www.cnblogs.com/freedom-elf/archive/2011/08/11/2135015.html



前不久自己動手寫了一個Android的聊天工具,跟服務器的交互還是基於HTTP方式的,在一般通訊上還算湊活,但是在即時聊天的時候就有點噁心了,客戶端開啓Service每隔3秒去詢問服務器是否有自己的新消息(當然3秒有點太快了),在心疼性能和流量的前提下,只能自己動手寫個服務器,傳統的Socket是阻塞的,這樣的話服務器對每個Socket都需要建立一個線程來操作,資源開銷很大,而且線程多了直接會影響服務端的性能(曾經測試開了3000多個線程就不讓創建了,所以併發數目也是有限制的),聽說從JDK1.5就多了個New IO,灰常不錯的樣子,找了找相關的資料,網上竟然全都是最最最簡單的一個demo,然後去CSDN發帖,基本上都是建議直接使用MINA框架的,這樣一來根本達不到學習NIO的目的,而且現在的技術也太快餐了,只知道使用前輩留下的東西,知其然不知其所以然。

 

折騰了一個周,終於搞出來了一個雛形,相比於xmpp的xml,本人更喜歡json的簡潔,爲了防止客戶端異常斷開等,準備採用心跳檢測的機制來判斷用戶是否在線,另外還有一種方法是學習例如Tomcat等Servlet中間件的方式,設置Session週期,定時清除過期Session。本Demo暫時實現了Session過期檢測,心跳檢測有空再搞,如果本例子在使用過程中有性能漏洞或者什麼bug請及時通知我,謝謝


廢話不多說,關於NIO的SelectionKey、Selector、Channel網上的介紹例子都很多,直接上代碼:

JsonParser

Json的解析類,隨便封裝了下,使用的最近比較火的fastjson

複製代碼
1 publicclass JsonParser {
2     
3 privatestatic JSONObject mJson;
4     
5 publicsynchronizedstatic String get(String json,String key) {
6         mJson = JSON.parseObject(json);
7 return mJson.getString(key);
8     }
9 }
複製代碼

Main

入口,不解釋

1 publicclass Main {
2 
3 publicstaticvoid main(String... args) {
4 new SeekServer().start();
5     }
6 }

Log

複製代碼
1 publicclass Log {
2 
3 publicstaticvoid i(Object obj) {
4         System.out.println(obj);
5     }
6 publicstaticvoid e(Object e) {
7         System.err.println(e);
8     }
9 }
複製代碼

SeekServer:

服務器端的入口,請求的封裝和接收都在此類,端口暫時寫死在了代碼裏,mSelector.select(TIME_OUT) > 0 目的是爲了當服務器空閒的時候(沒有任何讀寫甚至請求斷開事件),循環時有個間隔時間,不然基本上相當於while(true){//nothing}了,你懂的

複製代碼
 1 publicclass SeekServer extends Thread{
 2 privatefinalint ACCPET_PORT =55555;
 3 privatefinalint TIME_OUT =1000;
 4 private Selector mSelector =null;
 5 private ServerSocketChannel mSocketChannel =null;
 6 private ServerSocket mServerSocket =null;
 7 private InetSocketAddress mAddress =null;
 8     
 9 public SeekServer() {
10 long sign = System.currentTimeMillis();
11 try {
12             mSocketChannel = ServerSocketChannel.open();
13 if(mSocketChannel ==null) {
14                 System.out.println("can't open server socket channel");
15             }
16             mServerSocket = mSocketChannel.socket();
17             mAddress =new InetSocketAddress(ACCPET_PORT);
18             mServerSocket.bind(mAddress);
19             Log.i("server bind port is "+ ACCPET_PORT);
20             mSelector = Selector.open();
21             mSocketChannel.configureBlocking(false);
22             SelectionKey key = mSocketChannel.register(mSelector, SelectionKey.OP_ACCEPT);
23             key.attach(new Acceptor());
24             
25 //檢測Session狀態
26             Looper.getInstance().loop();
27             
28 //開始處理Session
29             SessionProcessor.start();
30             
31             Log.i("Seek server startup in "+ (System.currentTimeMillis() - sign) +"ms!");
32         } catch (ClosedChannelException e) {
33             Log.e(e.getMessage());
34         } catch (IOException e) {
35             Log.e(e.getMessage());
36         } 
37     }
38     
39 publicvoid run() {
40         Log.i("server is listening...");
41 while(!Thread.interrupted()) {
42 try {
43 if(mSelector.select(TIME_OUT) >0) {
44                     Set<SelectionKey> keys = mSelector.selectedKeys();
45                     Iterator<SelectionKey> iterator = keys.iterator();
46                     SelectionKey key =null;
47 while(iterator.hasNext()) {
48                         key = iterator.next();
49                         Handler at = (Handler) key.attachment();
50 if(at !=null) {
51                             at.exec();
52                         }
53                         iterator.remove();
54                     }
55                 }
56             } catch (IOException e) {
57                 Log.e(e.getMessage());
58             }
59         }
60     }
61 
62 class Acceptor extends Handler{
63 
64 publicvoid exec(){
65 try {
66                 SocketChannel sc = mSocketChannel.accept();
67 new Session(sc, mSelector);
68             } catch (ClosedChannelException e) {
69                 Log.e(e);
70             } catch (IOException e) {
71                 Log.e(e);
72             }
73         }
74     }
75 }
複製代碼

Handler:

只有一個抽象方法exec,Session將會繼承它

1 publicabstractclass Handler {
2     
3 publicabstractvoid exec();
4 }

Session:

封裝了用戶的請求和SelectionKey和SocketChannel,每次接收到新的請求時都重置它的最後活動時間,通過狀態mState=READING or SENDING 去執行消息的接收與發送,當客戶端異常斷開時則從SessionManager清除該會話。

複製代碼
  1 publicclass Session extends Handler{
  2 
  3 private SocketChannel mChannel;
  4 private SelectionKey  mKey;
  5 private ByteBuffer mRreceiveBuffer = ByteBuffer.allocate(10240);  
  6 private Charset charset = Charset.forName("UTF-8");
  7 private CharsetDecoder mDecoder = charset.newDecoder();
  8 private CharsetEncoder mEncoder = charset.newEncoder();
  9 privatelong lastPant;//最後活動時間
 10 privatefinalint TIME_OUT =1000*60*5; //Session超時時間
 11 private String key;
 12     
 13 private String sendData ="";
 14 private String receiveData =null;
 15     
 16 publicstaticfinalint READING =0,SENDING =1;
 17 int mState = READING;
 18     
 19 public Session(SocketChannel socket, Selector selector) throws IOException {
 20 this.mChannel = socket;
 21         mChannel = socket;
 22         mChannel.configureBlocking(false);
 23         mKey = mChannel.register(selector, 0);
 24         mKey.attach(this);
 25         mKey.interestOps(SelectionKey.OP_READ);
 26         selector.wakeup();
 27         lastPant = Calendar.getInstance().getTimeInMillis();
 28     }
 29     
 30 public String getReceiveData() {
 31 return receiveData;
 32     }
 33     
 34 publicvoid clear() {
 35         receiveData =null;
 36     }
 37 
 38 publicvoid setSendData(String sendData) {
 39         mState = SENDING;
 40         mKey.interestOps(SelectionKey.OP_WRITE);
 41 this.sendData = sendData +"\n";
 42     }
 43 
 44 publicboolean isKeekAlive() {
 45 return lastPant + TIME_OUT > Calendar.getInstance().getTimeInMillis();
 46     }
 47     
 48 publicvoid setAlive() {
 49         lastPant = Calendar.getInstance().getTimeInMillis();
 50     }
 51     
 52 /**
 53      * 註銷當前Session
 54 */
 55 publicvoid distroy() {
 56 try {
 57             mChannel.close();
 58             mKey.cancel();
 59         } catch (IOException e) {}
 60     }
 61     
 62     @Override
 63 publicsynchronizedvoid exec() {
 64 try {
 65 if(mState == READING) {
 66                 read();
 67             }elseif(mState == SENDING) {
 68                 write();
 69             }
 70         } catch (IOException e) {
 71             SessionManager.remove(key);
 72 try {
 73                 mChannel.close();
 74             } catch (IOException e1) {
 75                 Log.e(e1);
 76             }
 77             mKey.cancel();
 78         }
 79     }
 80     
 81 publicvoid read() throws IOException{
 82         mRreceiveBuffer.clear();
 83 int sign = mChannel.read(mRreceiveBuffer);
 84 if(sign ==-1) { //客戶端連接關閉
 85             mChannel.close();
 86             mKey.cancel();
 87         }
 88 if(sign >0) {
 89             mRreceiveBuffer.flip();
 90             receiveData = mDecoder.decode(mRreceiveBuffer).toString();
 91             setAlive();
 92             setSign();
 93             SessionManager.addSession(key, this);
 94         }
 95     }
 96     
 97 privatevoid setSign() {
 98 //設置當前Session的Key
 99         key = JsonParser.get(receiveData,"imei");
100 //檢測消息類型是否爲心跳包
101 //        String type = jo.getString("type");
102 //        if(type.equals("HEART_BEAT")) {
103 //            setAlive();
104 //        }
105     }
106     
107     
108 /**
109      * 寫消息
110 */
111 publicvoid write() {
112 try {
113             mChannel.write(mEncoder.encode(CharBuffer.wrap(sendData)));
114             sendData =null;
115             mState = READING;
116             mKey.interestOps(SelectionKey.OP_READ);
117         } catch (CharacterCodingException e) {
118             e.printStackTrace();
119         } catch (IOException e) {
120 try {
121                 mChannel.close();
122             } catch (IOException e1) {
123                 Log.e(e1);
124             }
125         }
126     }
127 }
複製代碼

SessionManager:

將所有Session存放到ConcurrentHashMap,這裏使用手機用戶的imei做key,ConcurrentHashMap因爲是線程安全的,所以能很大程度上避免自己去實現同步的過程,

封裝了一些操作Session的方法例如get,remove等

複製代碼
 1 publicclass SessionManager {
 2 
 3 privatestatic ConcurrentHashMap<String, Session> sessions =new ConcurrentHashMap<String, Session>();
 4     
 5 publicstaticvoid addSession(String key,Session session) {
 6         sessions.put(key, session);
 7     }
 8     
 9 publicstatic Session getSession(String key) {
10 return sessions.get(key);
11     }
12     
13 publicstatic Set<String> getSessionKeys() {
14 return sessions.keySet();
15     }
16     
17 publicstaticint getSessionCount() {
18 return sessions.size();
19     }
20     
21 publicstaticvoid remove(String[] keys) {
22 for(String key:keys) {
23 if(sessions.containsKey(key)) {
24                 sessions.get(key).distroy();
25                 sessions.remove(key);
26             }
27         }
28     }
29 publicstaticvoid remove(String key) {
30 if(sessions.containsKey(key)) {
31             sessions.get(key).distroy();
32             sessions.remove(key);
33         }
34     }
35 }
複製代碼

SessionProcessor

裏面使用了JDK自帶的線程池,用來分發處理所有Session中當前需要處理的請求(線程池的初始化參數不是太熟,望有了解的童鞋能告訴我),內部類Process則是將Session再次封裝成SocketRequest和SocketResponse(看到這裏是不是有點熟悉的感覺,對沒錯,JavaWeb裏到處都是request和response)

複製代碼
 1 publicclass SessionProcessor implements Runnable{
 2     
 3 privatestatic Runnable processor =new SessionProcessor();
 4 privatestatic ThreadPoolExecutor pool =new ThreadPoolExecutor(10, 200, 500, TimeUnit.MILLISECONDS,new ArrayBlockingQueue<Runnable>(10),new ThreadPoolExecutor.CallerRunsPolicy());
 5 publicstaticvoid start() {
 6 new Thread(processor).start();
 7     }
 8     
 9     @Override
10 publicvoid run() {
11 while(true) {
12             Session tmp =null;
13 for(String key:SessionManager.getSessionKeys()) {
14                 tmp = SessionManager.getSession(key);
15 //處理Session未處理的請求
16 if(tmp.getReceiveData() !=null) {
17                     pool.execute(new Process(tmp));
18                 }
19             }
20 try {
21                 Thread.sleep(10);
22             } catch (InterruptedException e) {
23                 Log.e(e);
24             }
25         }
26     }
27     
28 class Process implements Runnable {
29 
30 private SocketRequest request;
31 private SocketResponse response;
32         
33 public Process(Session session) {
34 //將Session封裝成Request和Response
35             request =new SocketRequest(session);
36             response =new SocketResponse(session);
37         }
38         
39         @Override
40 publicvoid run() {
41 new RequestTransform().transfer(request, response);
42         }
43     }
44 
45 }
複製代碼

RequestTransform裏的transfer方法利用反射對請求參數中的請求類別和請求動作來調用不同類的不同方法(UserHandler和MessageHandler)

複製代碼
 1 publicclass RequestTransform {
 2 
 3 publicvoid transfer(SocketRequest request,SocketResponse response) {
 4         String action = request.getValue("action");
 5         String handlerName = request.getValue("handler");
 6 //根據Session的請求類型,讓不同的類方法去處理
 7 try {
 8             Class<?> c= Class.forName("com.seek.server.handler."+ handlerName);
 9             Class<?>[] arg=new Class[]{SocketRequest.class,SocketResponse.class};
10             Method method=c.getMethod(action,arg);
11             method.invoke(c.newInstance(), new Object[]{request,response});
12         } catch (Exception e) {
13             e.printStackTrace();
14         }
15     }
16 }
複製代碼

SocketRequest和SocketResponse

複製代碼
 1 publicclass SocketRequest {
 2 
 3 private Session mSession;
 4 private String  mReceive;
 5     
 6 public SocketRequest(Session session) {
 7         mSession = session;
 8         mReceive = session.getReceiveData();
 9         mSession.clear();
10     }
11     
12 public String getValue(String key) {
13 return JsonParser.get(mReceive, key);
14     }
15     
16 public String getQueryString() {
17 return mReceive;
18     }
19 }
複製代碼
複製代碼
 1 publicclass SocketResponse {
 2 
 3 private Session mSession;
 4 public SocketResponse(Session session) {
 5         mSession = session;
 6     }
 7     
 8 publicvoid write(String msg) {
 9         mSession.setSendData(msg);
10     }
11 }
複製代碼

最後則是兩個處理請求的Handler

複製代碼
1 publicclass UserHandler {
2 
3 publicvoid login(SocketRequest request,SocketResponse response) {
4         System.out.println(request.getQueryString());
5 //TODO: 處理用戶登錄
6         response.write("你肯定收到消息了");
7     }
8 }
複製代碼
複製代碼
1 publicclass MessageHandler {
2 publicvoid send(SocketRequest request,SocketResponse response) {
3         System.out.println(request.getQueryString());
4 //消息發送
5         String key = request.getValue("imei");
6         Session session = SessionManager.getSession(key);
7 new SocketResponse(session).write(request.getValue("sms"));
8     }
9 }
複製代碼

還有個監測是否超時的類Looper,定期去刪除Session

複製代碼
 1 publicclass Looper extends Thread{
 2 privatestatic Looper looper =new Looper();
 3 privatestaticboolean isStart =false;
 4 privatefinalint INTERVAL =1000*60*5;
 5 private Looper(){}
 6 publicstatic Looper getInstance() {
 7 return looper;
 8     }
 9     
10 publicvoid loop() {
11 if(!isStart) {
12             isStart =true;
13 this.start();
14         }
15     }
16     
17 publicvoid run() {
18         Task task =new Task();
19 while(true) {
20 //Session過期檢測
21             task.checkState();
22 //心跳包檢測
23 //task.sendAck();
24 try {
25                 Thread.sleep(INTERVAL);
26             } catch (InterruptedException e) {
27                 Log.e(e);
28             }
29         }
30     }
31 }
複製代碼
複製代碼
 1 publicclass Task {
 2 publicvoid checkState() {
 3         Set<String> keys = SessionManager.getSessionKeys();
 4 if(keys.size() ==0) {
 5 return;
 6         }
 7         List<String> removes =new ArrayList<String>();
 8         Iterator<String> iterator = keys.iterator();
 9         String key =null;
10 while(iterator.hasNext()) {
11             key = iterator.next();
12 if(!SessionManager.getSession(key).isKeekAlive()) {
13                 removes.add(key);
14             }
15         }
16 if(removes.size() >0) {
17             Log.i("sessions is time out,remove "+ removes.size() +"session");
18         }
19         SessionManager.remove(removes.toArray(new String[removes.size()]));
20     }
21     
22 publicvoid sendAck() {
23         Set<String> keys = SessionManager.getSessionKeys();
24 if(keys.size() ==0) {
25 return;
26         }
27         Iterator<String> iterator = keys.iterator();
28 while(iterator.hasNext()) {
29             iterator.next();
30 //TODO 發送心跳包
31         }
32     }
33 }
複製代碼

注意,在Task和SessionProcessor類裏都有對SessionManager的sessions做遍歷,文中使用的方法並不是很好,主要是效率問題,推薦使用遍歷Entry的方式來獲取Key和Value,

因爲一直在JavaWeb上折騰,所以會的童鞋看到Request和Response會挺親切,這個例子沒有經過任何安全和性能測試,如果需要放到生產環境上得話請先自行做測試- -!

客戶端請求時的數據內容例如{handler:"UserHandler",action:"login",imei:"2364656512636".......},這些約定就自己來定了


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章