Java Socket 全雙工通信


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

雖然曉得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 }

這是客戶端的代碼

  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 }

無聊的時候就自己和自己聊天吧

感覺在這基礎上可以搭個http服務器什么的了

 

然后就可以輸入要返回的信息了,輸入完斷開客戶端連接就好了,就是 2:exit,然后瀏覽器就能看到返回的信息了,不過貌似沒有響應頭,只有響應正文

/*

這里什么都沒寫

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

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

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

但是Windows服務器不會玩

*/

目前已知Bug:

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

因為輸入的字符還在緩沖區中,所以會看到自己正在寫的字符和發來的字符拼到了一行

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

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

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

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

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

如果誰知道怎么弄請告訴我一下


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM