接着上篇日誌寫。。一般在設計服務器時,都是非阻塞的,且爲了簡單,通常都設計爲一個線程來操作。
但是這樣設計的缺點也很明顯,倘若服務器有很多連接,那麼每次循環都會處理很多套接字,除了CPU使用率不高外,如果某個套接字的數據傳輸速度很慢,那麼他的調用也會很慢(個人猜測,因爲TCP傳輸速度是雙向均衡的),從而勢必會影響其他套接字的數據傳輸。
所以非阻塞+多線程是大型服務器必備的解決方案。
多線程操作同一套接字需要考慮的問題比較複雜,我在http://blog.csdn.net/shallwake/archive/2009/12/15/5014160.aspx有討論,原則就是:儘量不要多線程同時recv();絕對不能多線程send();可以用兩個線程,一個recv(),一個send()。原因簡單點就是雖然套接字是線程安全,但是多線程會有發包錯亂。
所以,對於服務器,只要保證每個線程操作的套接字間沒有交集就行,當然,此交集只是邏輯上沒有交集,比如,某個線程操作A套接字send(),另一個線程負責操作A套接字recv(),那麼他們還是邏輯上沒有交集的,原因上面已講述。
剩下就是數據結構的設計,個人總結有2種。
一,動態創建線程:
就是規定某個線程處理N個套接字,當總套接字超過此值就另外再創建線程處理。
二,預先分配線程:
就是先分配N個線程,然後隨着套接字的增長,將他們均勻地分配給每個線程。
難點:套接字是動態變化的,增加的時候比較好辦,但是若是減少,那麼方法一與方法二就有難度差距了。
對於方法一:當某個線程負責處理的套接字減少,小於N時,那麼下次增加套接字時不能再開線程,要繼續利用它,若是套接字減少爲0,那麼此線程就自己銷燬了。所以整個動態過程的維護時比較複雜的,自己只是想會,沒試驗過。
對於方法二:我下面提供解決方案。
方法二的實現:
首先感謝kasicass GG在上篇文章中的指點~~,今天下午按照他的思路用java簡單嘗試了下
思路還是比較簡單,開啓5個線程負責異步讀取客戶端信息,當然,若是沒有套接字處理,線程不能退出,所以每個線程都用到了BlockingQueue,而且維護了一個變量sockCount記錄自己當前處理的套接字數量。當有連接時,主線程選擇sockCount最少的那個線程負責處理該連接,就OK了,沒有方法一動態維護那麼複雜。
嗯,懶人大招,帖代碼了。。。
Server類
import java.nio.channels.*; import java.util.*; import java.net.*; import java.io.*; public class Server extends Thread{ private ServerSocketChannel sSockChan; private Selector selector; private ArrayList readers; public Server(){ readers = new ArrayList(); for(int i = 0; i < 5; i ++){ EventReader er = new EventReader(this); er.start(); readers.add(er); } initServerSocket(); } public int getIndex(){ int min = 999999; int pos = 0; for(int i = 0; i < 5; i ++){ if(min >= readers.get(i).getSocketCount()){ min = readers.get(i).getSocketCount(); pos = i; } } return pos; } private void initServerSocket() { try { // open a non-blocking server socket channel sSockChan = ServerSocketChannel.open(); sSockChan.configureBlocking(false); // bind to localhost on designated port InetAddress addr = InetAddress.getLocalHost(); System.out.println("binding to address: " + addr.getHostAddress()); sSockChan.socket().bind(new InetSocketAddress(addr, 8550)); // get a selector selector = Selector.open(); // register the channel with the selector to handle accepts SelectionKey acceptKey = sSockChan.register(selector, SelectionKey.OP_ACCEPT); } catch (Exception e) { System.out.println("error initializing ServerSocket"); System.exit(1); } } @Override public void run(){ while(true){ try { // blocking select, will return when we get a new connection selector.select(); // fetch the keys Set readyKeys = selector.selectedKeys(); // run through the keys and process Iterator i = readyKeys.iterator(); while (i.hasNext()) { SelectionKey key = (SelectionKey) i.next(); i.remove(); ServerSocketChannel ssChannel = (ServerSocketChannel) key.channel(); SocketChannel clientChannel = ssChannel.accept(); // 這裏是重點。 readers.get(getIndex()).addNewClient(clientChannel,getIndex()); System.out.println("got connection from: " + clientChannel.socket().getInetAddress()); } } catch (IOException ioe) { System.out.println("error during serverSocket select(): " + ioe.getMessage()); } catch (Exception e) { System.out.println("exception in run()"); } } } public static void main(String[] args) { Server server = new Server(); server.start(); } }
EventReader類(就是每個線程)
import java.nio.*; import java.nio.channels.*; import java.io.*; import java.util.*; import java.util.concurrent.*; import java.util.logging.Level; import java.util.logging.Logger; public class EventReader extends Thread{ private Server sv; private Selector selector; private BlockingQueue newClients; private int scoketCount = 0; public int getSocketCount(){ return scoketCount; } public EventReader(Server sv){ this.sv = sv; newClients = new LinkedBlockingQueue(); } public void addNewClient(SocketChannel clientChannel, int id) { try { scoketCount++; newClients.put(clientChannel); selector.wakeup(); System.out.println("I'm in thread" + id); } catch (InterruptedException ex) { Logger.getLogger(EventReader.class.getName()).log(Level.SEVERE, null, ex); } } @Override public void run(){ try { selector = Selector.open(); while(true){ read(); checkNewConnections(); try { Thread.sleep(30); } catch (InterruptedException e) {} } } catch (IOException ex) { Logger.getLogger(EventReader.class.getName()).log(Level.SEVERE, null, ex); } } private void read(){ try { selector.select(); Set readyKeys = selector.selectedKeys(); Iterator i = readyKeys.iterator(); while (i.hasNext()) { SelectionKey key = (SelectionKey) i.next(); i.remove(); SocketChannel channel = (SocketChannel) key.channel(); ByteBuffer buffer = (ByteBuffer)key.attachment(); long nbytes = channel.read(buffer); if(nbytes == -1){ System.out.println("channel has closed"); scoketCount--; channel.close(); } System.out.println(buffer.array()); //注意,只是作爲簡單演示,並未考慮如何讀取完整數據包,讀多少輸出多少然後復原。 buffer.clear(); } } catch (IOException ex) { Logger.getLogger(EventReader.class.getName()).log(Level.SEVERE, null, ex); } } private void checkNewConnections(){ try { SocketChannel clientChannel = (SocketChannel) newClients.take(); clientChannel.configureBlocking( false); clientChannel.register( selector, SelectionKey.OP_READ,ByteBuffer.allocate(1024)); } catch (InterruptedException ex) { Logger.getLogger(EventReader.class.getName()).log(Level.SEVERE, null, ex); } catch(IOException ex){ } } }
Over。。。