第一章:手動搭建I/O網絡通訊框架1:Socket和ServerSocket入門實戰,實現單聊html
在第一章中運用Socket和ServerSocket簡單的實現了網絡通訊。這一章,利用BIO編程模型進行升級改造,實現羣聊聊天室。java
如圖:當一個客戶端請求進來時,接收器會爲這個客戶端分配一個工做線程,這個工做線程專職處理客戶端的操做。在上一章中,服務器接收到客戶端請求後就跑去專門服務這個客戶端了,因此當其餘請求進來時,是處理不到的。web
看到這個圖,很容易就會想到線程池,BIO是一個相對簡單的模型,實現它的關鍵之處也在於線程池。編程
在上代碼以前,先大概說清楚每一個類的做用,以避免弄混淆。更詳細的說明,都寫在註釋當中。安全
ChatServer:這個類的做用就像圖中的Acceptor。它有兩個比較關鍵的全局變量,一個就是存儲在線用戶信息的Map,一個就是線程池。這個類會監聽端口,接收客戶端的請求,而後爲客戶端分配工做線程。還會提供一些經常使用的工具方法給每一個工做線程調用,好比:發送消息、添加在線用戶等。
服務器
ChatHandler:這個類就是工做線程的類。在這個項目中,它的工做很簡單:把接收到的消息轉發給其餘客戶端,固然還有一些小功能,好比添加\移除在線用戶。網絡
相較於服務器,客戶端的改動較小,主要是把等待用戶輸入信息這個功能分到其餘線程作,否則這個功能會一直阻塞主線程,致使沒法接收其餘客戶端的消息。session
ChatClient:客戶端啓動類,也就是主線程,會經過Socket和服務器鏈接。也提供了兩個工具方法:發送消息和接收消息。框架
UserInputHandler:專門負責等待用戶輸入信息的線程,一旦有信息鍵入,就立刻發送給服務器。socket
首先建立兩個包區分一下客戶端和服務器,client和server
服務器端ChatServer:
public class ChatServer { private int DEFAULT_PORT = 8888; /** * 建立一個Map存儲在線用戶的信息。這個map能夠統計在線用戶、針對這些用戶能夠轉發其餘用戶發送的消息 * 由於會有多個線程操做這個map,因此爲了安全起見用ConcurrentHashMap * 在這裏key就是客戶端的端口號,但在實際中確定不會用端口號區分用戶,若是是web的話通常用session。 * value是IO的Writer,用以存儲客戶端發送的消息 */ private Map<Integer, Writer> map=new ConcurrentHashMap<>(); /** * 建立線程池,線程上限爲10個,若是第11個客戶端請求進來,服務器會接收可是不會去分配線程處理它。 * 前10個客戶端的聊天記錄,它看不見。當有一個客戶端下線時,這第11個客戶端就會被分配線程,服務器顯示在線 * 你們能夠把10再設置小一點,測試看看 * */ private ExecutorService executorService= Executors.newFixedThreadPool(10); //客戶端鏈接時往map添加客戶端 public void addClient(Socket socket) throws IOException { if (socket != null) { BufferedWriter writer = new BufferedWriter( new OutputStreamWriter(socket.getOutputStream()) ); map.put(socket.getPort(), writer); System.out.println("Client["+socket.getPort()+"]:Online"); } } //斷開鏈接時map裏移除客戶端 public void removeClient(Socket socket) throws Exception { if (socket != null) { if (map.containsKey(socket.getPort())) { map.get(socket.getPort()).close(); map.remove(socket.getPort()); } System.out.println("Client[" + socket.getPort() + "]Offline"); } } //轉發客戶端消息,這個方法就是把消息發送給在線的其餘的全部客戶端 public void sendMessage(Socket socket, String msg) throws IOException { //遍歷在線客戶端 for (Integer port : map.keySet()) { //發送給在線的其餘客戶端 if (port != socket.getPort()) { Writer writer = map.get(port); writer.write(msg); writer.flush(); } } } //接收客戶端請求,並分配Handler去處理請求 public void start() { try (ServerSocket serverSocket = new ServerSocket(DEFAULT_PORT)) { System.out.println("Server Start,The Port is:"+DEFAULT_PORT); while (true){ //等待客戶端鏈接 Socket socket=serverSocket.accept(); //爲客戶端分配一個ChatHandler線程 executorService.execute(new ChatHandler(this,socket)); } } catch (IOException e) { e.printStackTrace(); } } public static void main(String[] args) { ChatServer server=new ChatServer(); server.start(); } }
服務器端ChatHandler:
public class ChatHandler implements Runnable { private ChatServer server; private Socket socket; //構造函數,ChatServer經過這個分配Handler線程 public ChatHandler(ChatServer server, Socket socket) { this.server = server; this.socket = socket; } @Override public void run() { try { //往map裏添加這個客戶端 server.addClient(socket); //讀取這個客戶端發送的消息 BufferedReader reader = new BufferedReader( new InputStreamReader(socket.getInputStream()) ); String msg = null; while ((msg = reader.readLine()) != null) { //這樣拼接是爲了讓其餘客戶端也能看清是誰發送的消息 String sendmsg = "Client[" + socket.getPort() + "]:" + msg; //服務器打印這個消息 System.out.println(sendmsg); //將收到的消息轉發給其餘在線客戶端 server.sendMessage(socket, sendmsg + "\n"); if (msg.equals("quit")) { break; } } } catch (IOException e) { e.printStackTrace(); } finally { //若是用戶退出或者發生異常,就在map中移除該客戶端 try { server.removeClient(socket); } catch (Exception e) { e.printStackTrace(); } } } }
客戶端ChatClient:
public class ChatClient { private BufferedReader reader; private BufferedWriter writer; private Socket socket; //發送消息給服務器 public void sendToServer(String msg) throws IOException { //發送以前,判斷socket的輸出流是否關閉 if (!socket.isOutputShutdown()) { //若是沒有關閉就把用戶鍵入的消息放到writer裏面 writer.write(msg + "\n"); writer.flush(); } } //從服務器接收消息 public String receive() throws IOException { String msg = null; //判斷socket的輸入流是否關閉 if (!socket.isInputShutdown()) { //沒有關閉的話就能夠經過reader讀取服務器發送來的消息。注意:若是沒有讀取到消息線程會阻塞在這裏 msg = reader.readLine(); } return msg; } public void start() { //和服務建立鏈接 try { socket = new Socket("127.0.0.1", 8888); reader=new BufferedReader( new InputStreamReader(socket.getInputStream()) ); writer=new BufferedWriter( new OutputStreamWriter(socket.getOutputStream()) ); //新建一個線程去監聽用戶輸入的消息 new Thread(new UserInputHandler(this)).start(); /** * 不停的讀取服務器轉發的其餘客戶端的信息 * 記錄一下以前踩過的小坑: * 這裏必定要建立一個msg接收信息,若是直接用receive()方法判斷和輸出receive()的話會形成有的消息不會顯示 * 由於receive()獲取時,在返回以前是阻塞的,一旦接收到消息纔會返回,也就是while這裏是阻塞的,一旦有消息就會進入到while裏面 * 這時候若是輸出的是receive(),那麼上次獲取的信息就會丟失,而後阻塞在System.out.println * */ String msg=null; while ((msg=receive())!=null){ System.out.println(msg); } } catch (IOException e) { e.printStackTrace(); }finally { try { if(writer!=null){ writer.close(); } } catch (IOException e) { e.printStackTrace(); } } } public static void main(String[] args) { new ChatClient().start(); } }
客戶端UserInputHandler:
public class UserInputHandler implements Runnable { private ChatClient client; public UserInputHandler(ChatClient client) { this.client = client; } @Override public void run() { try { //接收用戶輸入的消息 BufferedReader reader = new BufferedReader( new InputStreamReader(System.in) ); //不停的獲取reader中的System.in,實現了等待用戶輸入的效果 while (true) { String input = reader.readLine(); //向服務器發送消息 client.sendToServer(input); if (input.equals("quit")) break; } } catch (IOException e) { e.printStackTrace(); } } }
經過打開終端,經過javac編譯。若是你們是在IDEA上編碼的話可能會報編碼錯誤,在javac後面加上-encoding utf-8再接java文件就行了。
編譯後運行,經過java運行時,又遇到了一個坑。會報找不到主類的錯誤,原來是由於加上兩個包,要在class文件名前面加上包名。好比當前在src目錄,下面有client和server兩個包,要這麼運行:java client.XXXX。可我以前明明在client文件夾下運行的java,也是不行,不知道爲何。
接着測試:
1.首先在一個終端裏運行ChatServer,打開服務器
2.在第二個終端裏打開ChatClient,暫且叫A,此時服務器的終端顯示:
3.相似的,在第三個終端裏打開ChatClient,暫且叫B,此時服務器顯示:
4.A中輸入hi,除了服務器會打印hi外,B中也會顯示,圖片中的端口號和前面的不同,是由於中間出了點小問題,前三張截圖和後面的不是同時運行的。實際中同一個客戶端會顯示同樣的端口號:
5.當客戶端輸入quit時就會斷開鏈接,最後,服務器的顯示爲: