Java Socket 全雙工通訊

最開始接觸TCP編程是想測試一下服務器的一些端口有沒有開,阿里雲的服務器,公司的服務器,我也不知道他開了那些端口,因而寫個小程序測試一下,反正就是能連上就是開了,java

雖然曉得nmap這些東西,但服務器不監聽開放的端口,他也檢測不到開沒開數據庫

後來前幾天寫了個程序,接受TCP請求並解析字節流寫入數據庫,這其實不難,整個程序就是個半雙工模式,就是設備給我發一條消息,我給他回一條編程

而後就像寫個相似QQ這類聊天軟件的東西玩玩,百度了半天沒找到全雙工的例子,那就本身寫吧,兩天寫完了,好開心,有新玩具能夠玩了小程序

不解釋,直接放代碼,感受註釋寫的很清楚了瀏覽器

這是服務器的代碼服務器

補充一點,可能忘寫了,服務器能夠主動斷開與客戶端的鏈接,例如鏈接的id是1號,那麼輸入1:exit,就會斷開與id爲1的鏈接運維

 1 import java.io.*;  2 import java.net.ServerSocket;  3 import java.net.Socket;  4 import java.util.Set;  5 import java.util.Map;  6 import java.util.HashMap;  7 import java.util.LinkedList;  8 
 9 /**
 10  * 服務器,全雙工,支持單播和廣播  11  *  12  * 注意是全雙工,全雙工,全雙工  13  *  14  * 就是像QQ同樣  15  */
 16 public class Server{  17     // 分配給socket鏈接的id,用於區分不一樣的socket鏈接
 18     private static int id = 0;  19     // 存儲socket鏈接,發送消息的時候從這裏取出對應的socket鏈接
 20     private HashMap<Integer,ServerThread> socketList = new HashMap<>();  21     // 服務器對象,用於監聽TCP端口
 22     private ServerSocket server;  23 
 24     /**
 25  * 構造函數,必須輸入端口號  26      */
 27     public Server(int port) {  28         try {  29             this.server = new ServerSocket(port);  30             System.out.println("服務器啓動完成 使用端口: "+port);  31         } catch (IOException e) {  32  e.printStackTrace();  33  }  34  }  35 
 36     /**
 37  * 啓動服務器,先讓Writer對象啓動等待鍵盤輸入,而後不斷等待客戶端接入  38  * 若是有客戶端接入就開一個服務線程,並把這個線程放到Map中管理  39      */
 40     public void start() {  41         new Writer().start();  42         try {  43             while (true) {  44                 Socket socket = server.accept();  45                 System.out.println(++id + ":客戶端接入:"+socket.getInetAddress() + ":" + socket.getPort());  46                 ServerThread thread = new ServerThread(id,socket);  47  socketList.put(id,thread);  48  thread.run();  49  }  50         } catch (IOException e) {  51  e.printStackTrace();  52  }  53  }  54 
 55     /**
 56  * 回收資源啦,雖然不廣播關閉也沒問題,但總以爲通知一下客戶端比較好  57      */
 58     public void close(){  59         sendAll("exit");  60         try{  61             if(server!=null){  62  server.close();  63  }  64         }catch(IOException e){  65  e.printStackTrace();  66  }  67         System.exit(0);  68  }  69 
 70     /**
 71  * 遍歷存放鏈接的Map,把他們的id所有取出來,注意這裏不能直接遍歷Map,否則可能報錯  72  * 報錯的狀況是,當試圖發送 `*:exit` 時,這段代碼會遍歷Map中全部的鏈接對象,關閉並從Map中移除  73  * java的集合類在遍歷的過程當中進行修改會拋出異常  74      */
 75     public void sendAll(String data){  76         LinkedList<Integer> list = new LinkedList<>();  77         Set<Map.Entry<Integer,ServerThread>> set = socketList.entrySet();  78         for(Map.Entry<Integer,ServerThread> entry : set){  79  list.add(entry.getKey());  80  }  81         for(Integer id : list){  82  send(id,data);  83  }  84  }  85 
 86     /**
 87  * 單播  88      */
 89     public void send(int id,String data){  90         ServerThread thread = socketList.get(id);  91  thread.send(data);  92         if("exit".equals(data)){  93  thread.close();  94  }  95  }  96 
 97     // 服務線程,當收到一個TCP鏈接請求時新建一個服務線程
 98     private class ServerThread implements Runnable {  99         private int id; 100         private Socket socket; 101         private InputStream in; 102         private OutputStream out; 103         private PrintWriter writer; 104 
105         /**
106  * 構造函數 107  * @param id 分配給該鏈接對象的id 108  * @param socket 將socket鏈接交給該服務線程 109         */
110         ServerThread(int id,Socket socket) { 111             try{ 112                 this.id = id; 113                 this.socket = socket; 114                 this.in = socket.getInputStream(); 115                 this.out = socket.getOutputStream(); 116                 this.writer = new PrintWriter(out); 117             }catch(IOException e){ 118  e.printStackTrace(); 119  } 120  } 121 
122         /**
123  * 由於設計爲全雙工模式,因此讀寫不能阻塞,新開線程進行讀操做 124         */
125  @Override 126         public void run() { 127             new Reader().start(); 128  } 129 
130         /**
131  * 由於同時只能有一個鍵盤輸入,因此輸入交給服務器管理而不是服務線程 132  * 服務器負責選擇socket鏈接和發送的消息內容,而後調用服務線程的write方法發送數據 133          */
134         public void send(String data){ 135             if(!socket.isClosed() && data!=null && !"exit".equals(data)){ 136  writer.println(data); 137  writer.flush(); 138  } 139  } 140 
141         /**
142  * 關閉全部資源 143          */
144         public void close(){ 145             try{ 146                 if(writer!=null){ 147  writer.close(); 148  } 149                 if(in!=null){ 150  in.close(); 151  } 152                 if(out!=null){ 153  out.close(); 154  } 155                 if(socket!=null){ 156  socket.close(); 157  } 158  socketList.remove(id); 159             }catch(IOException e){ 160  e.printStackTrace(); 161  } 162  } 163 
164         /**
165  * 由於全雙工模式因此將讀操做單獨設計爲一個類,而後開個線程執行 166          */
167         private class Reader extends Thread{ 168             private InputStreamReader streamReader = new InputStreamReader(in); 169             private BufferedReader reader = new BufferedReader(streamReader); 170 
171  @Override 172             public void run(){ 173                 try{ 174                     String line = ""; 175                     // 只要鏈接沒有關閉,並且讀到的行不爲空,爲空說明鏈接異常斷開,並且客戶端發送的不是exit,那麼就一直從鏈接中讀
176                     while(!socket.isClosed() && line!=null && !"exit".equals(line)){ 177                         line=reader.readLine(); 178                         if(line!=null){ 179                             System.out.println(id+":client: "+line); 180  } 181  } 182                     // 若是循環中斷說明鏈接已斷開
183                     System.out.println(id+":客戶端主動斷開鏈接"); 184  close(); 185                 }catch(IOException e) { 186                     System.out.println(id+":鏈接已斷開"); 187                 }finally{ 188                     try{ 189                         if(streamReader!=null){ 190  streamReader.close(); 191  } 192                         if(reader!=null){ 193  reader.close(); 194  } 195  close(); 196                     }catch(IOException e){ 197  e.printStackTrace(); 198  } 199  } 200  } 201  } 202  } 203 
204     /**
205  * 由於發送的時候必須指明發送目的地,因此不能交給服務線程管理寫操做,否則就沒法選擇向哪一個鏈接發送消息 206  * 若是交給服務線程管理的話,Writer對象的會爭奪鍵盤這一資源,誰搶到是誰的,就沒法控制消息的發送對象了 207      */
208     private class Writer extends Thread{ 209         // 咱們要從鍵盤獲取發送的消息
210         private BufferedReader reader = new BufferedReader(new InputStreamReader(System.in)); 211         
212  @Override 213         public void run(){ 214             String line = ""; 215             // 先來個死循環,除非主動輸入exit關閉服務器,不然一直等待鍵盤寫入
216             while(true){ 217                 try{ 218                     line = reader.readLine(); 219                     if("exit".equals(line)){ 220                         break; 221  } 222                 }catch(IOException e){ 223  e.printStackTrace(); 224  } 225                 // 輸入是有規則的 [鏈接id]:[要發送的內容] 226                 // 鏈接id能夠爲*,表明全部的鏈接對象,也就是廣播 227                 // 要發送的內容不能爲空,發空內容沒意義,並且浪費流量 228                 // 鏈接id和要發送的消息之間用分號分割,注意是半角的分號 229                 // 例如: 1:你好 ==>客戶端看到的是 server:你好 230                 // *:吃飯了 ==>全部客戶端都能看到 server:吃飯了
231                 if(line!=null){ 232                     try{ 233                         String[] data = line.split(":"); 234                         if("*".equals(data[0])){ 235                             // 這裏是廣播
236                             sendAll(data[1]); 237                         }else{ 238                             // 這裏是單播
239                             send(Integer.parseInt(data[0]),data[1]); 240  } 241                         // 有可能發生的異常
242                     }catch(NumberFormatException e){ 243                         System.out.print("必須輸入鏈接id號"); 244                     }catch(ArrayIndexOutOfBoundsException e){ 245                         System.out.print("發送的消息不能爲空"); 246                     }catch(NullPointerException e){ 247                         System.out.print("鏈接不存在或已經斷開"); 248  } 249  } 250  } 251             // 循環中斷說明服務器退出運行
252             System.out.println("服務器退出"); 253  close(); 254  } 255  } 256 
257     public static void main(String[] args) { 258         int port = Integer.parseInt(args[0]); 259         new Server(port).start(); 260  } 261 }

這是客戶端的代碼socket

 1 import java.io.*;  2 import java.net.Socket;  3 import java.net.UnknownHostException;  4 
 5 /**
 6  * 客戶端 全雙工 但同時只能鏈接一臺服務器  7  */
 8 public class Client {  9     private Socket socket;  10     private InputStream in;  11     private OutputStream out;  12 
 13     /**
 14  * 啓動客戶端須要指定地址和端口號  15      */
 16     private Client(String address, int port) {  17         try {  18             socket = new Socket(address, port);  19             this.in = socket.getInputStream();  20             this.out = socket.getOutputStream();  21         } catch (UnknownHostException e) {  22  e.printStackTrace();  23         } catch (IOException e) {  24  e.printStackTrace();  25  }  26         System.out.println("客戶端啓動成功");  27  }  28 
 29     public void start(){  30         // 和服務器不同,客戶端只有一條鏈接,能省不少事
 31         Reader reader = new Reader();  32         Writer writer = new Writer();  33  reader.start();  34  writer.start();  35  }  36 
 37     public void close(){  38         try{  39             if(in!=null){  40  in.close();  41  }  42             if(out!=null){  43  out.close();  44  }  45             if(socket!=null){  46  socket.close();  47  }  48             System.exit(0);  49         }catch(IOException e){  50  e.printStackTrace();  51  }  52  }  53 
 54     private class Reader extends Thread{  55         private InputStreamReader streamReader = new InputStreamReader(in);  56         private BufferedReader reader = new BufferedReader(streamReader);  57 
 58  @Override  59         public void run(){  60             try{  61                 String line="";  62                 while(!socket.isClosed() && line!=null && !"exit".equals(line)){  63                     line=reader.readLine();  64                     if(line!=null){  65                         System.out.println("Server: "+line);  66  }  67  }  68                 System.out.println("服務器主動斷開鏈接");  69  close();  70             }catch(IOException e){  71                 System.out.println("鏈接已斷開");  72             }finally{  73                 try{  74                     if(streamReader!=null){  75  streamReader.close();  76  }  77                     if(reader!=null){  78  reader.close();  79  }  80  close();  81                 }catch(IOException e){  82  e.printStackTrace();  83  }  84  }  85  }  86  }  87 
 88     private class Writer extends Thread{  89         private PrintWriter writer = new PrintWriter(out);  90         private BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));  91 
 92  @Override  93         public void run(){  94             try{  95                 String line = "";  96                 while(!socket.isClosed() && line!=null && !"exit".equals(line)){  97                     line = reader.readLine();  98                     if("".equals(line)){  99                         System.out.print("發送的消息不能爲空"); 100                     }else{ 101  writer.println(line); 102  writer.flush(); 103  } 104  } 105                 System.out.println("客戶端退出"); 106  close(); 107             }catch(IOException e){ 108                 System.out.println("error:鏈接已關閉"); 109             }finally{ 110                 try{ 111                     if(writer!=null){ 112  writer.close(); 113  } 114                     if(reader!=null){ 115  reader.close(); 116  } 117  close(); 118                 }catch(IOException e){ 119  e.printStackTrace(); 120  } 121  } 122  } 123  } 124 
125     public static void main(String[] args) { 126        String address = args[0]; 127        int port = Integer.parseInt(args[1]); 128        new Client(address, port).start(); 129  } 130 }

無聊的時候就本身和本身聊天吧ide

感受在這基礎上能夠搭個http服務器什麼的了函數

 

而後就能夠輸入要返回的信息了,輸入完斷開客戶端鏈接就行了,就是 2:exit,而後瀏覽器就能看到返回的信息了,不過貌似沒有響應頭,只有響應正文

/*

這裏什麼都沒寫

還有服務器或者客戶端添加個執行遠程命令什麼的方法。。。。。。

別老想壞事,沒開SSH的服務器遠程執行個運維腳本什麼的也不錯啊,尤爲是Win的服務器

其實我一直想弄個遠程部署Tomcat項目的東西,最好是熱部署,否則每次都要用FTP上傳war

可是Windows服務器不會玩

*/

目前已知Bug:

當一方(不管是客戶端仍是服務器)輸入消息後但沒有發出,但此時接受到另外一方發來的消息,顯示會出現問題

由於輸入的字符還在緩衝區中,因此會看到本身正在寫的字符和發來的字符拼到了一行

左邊是服務器,右邊是客戶端

客戶端輸入了 `測試一下` 但沒有發出,服務器此時發送一條消息 `這裏是服務器` 因而就發生了右圖的狀況

而後客戶端發送消息,服務器收到 `測試一下`,發送前再輸入字符不會影響到服務器接受到的消息,

例如在上述狀況下,客戶端收到服務器的消息後,在輸入`我還沒說完` 而後再發送,服務器會收到 `測試一下我還沒說完`

也就是說只是客戶端要發送的消息,顯示上會與服務器發來的消息顯示在一行,並且再輸入字符會折行

若是誰知道怎麼弄請告訴我一下

相關文章
相關標籤/搜索