最開始接觸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:
當一方(不管是客戶端仍是服務器)輸入消息後但沒有發出,但此時接受到另外一方發來的消息,顯示會出現問題
由於輸入的字符還在緩衝區中,因此會看到本身正在寫的字符和發來的字符拼到了一行
左邊是服務器,右邊是客戶端
客戶端輸入了 `測試一下` 但沒有發出,服務器此時發送一條消息 `這裏是服務器` 因而就發生了右圖的狀況
而後客戶端發送消息,服務器收到 `測試一下`,發送前再輸入字符不會影響到服務器接受到的消息,
例如在上述狀況下,客戶端收到服務器的消息後,在輸入`我還沒說完` 而後再發送,服務器會收到 `測試一下我還沒說完`
也就是說只是客戶端要發送的消息,顯示上會與服務器發來的消息顯示在一行,並且再輸入字符會折行
若是誰知道怎麼弄請告訴我一下