最開始接觸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:
當一方(不論是客戶端還是服務器)輸入消息后但沒有發出,但此時接受到另一方發來的消息,顯示會出現問題
因為輸入的字符還在緩沖區中,所以會看到自己正在寫的字符和發來的字符拼到了一行
左邊是服務器,右邊是客戶端
客戶端輸入了 `測試一下` 但沒有發出,服務器此時發送一條消息 `這里是服務器` 於是就發生了右圖的情況
然后客戶端發送消息,服務器收到 `測試一下`,發送前再輸入字符不會影響到服務器接受到的消息,
例如在上述情況下,客戶端收到服務器的消息后,在輸入`我還沒說完` 然后再發送,服務器會收到 `測試一下我還沒說完`
也就是說只是客戶端要發送的消息,顯示上會與服務器發來的消息顯示在一行,而且再輸入字符會折行

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