7.4 (java學習筆記)網路程式設計之TCP
一、TCP
1.1 TCP(Transmission Control Protocol 傳輸控制協議),是一種面向連線的,安全的傳輸協議,但效率相比於UDP而言比較低。
TCP傳輸時需要確保先建立連線之後,再進行傳輸這樣就保證了傳輸的可靠性。
java中將TCP封裝成了對應的類。
ServerSocket:服務端
Socket:客戶端
1.2TCP連線的建立與取消(三次握手與四次揮手)
連線(三次握手):
1.初始狀態,伺服器處於監聽狀態,主機的傳輸控制模組(TCB)像伺服器傳送連線請求,客戶端進入同步已傳送狀態。
2.伺服器受到客服端傳送的連線請求,如果同同意連線則向客戶端傳送確認,伺服器進入同步收到狀態。
3.客戶端受到確認後,繼續給伺服器傳送確認報文,客戶端進入已連線狀態。
後續伺服器收到客服端的確認後也進入已建立連線狀態。
建立連線後,客戶端和伺服器就可以愉快的傳送資訊了,資訊傳送完畢後,就要斷開連線。
斷開(四次揮手):
1.客戶端傳送釋放報文,同時停止傳送資料主動關閉TCP連線,進入終止等待狀態1。
2.伺服器收到釋放報文後傳送確認,此時伺服器進入關閉等待狀態。此時客戶端到伺服器方向的連線就釋放了。
此時TCP進入半連線狀態,伺服器到客戶端的連線未釋放,此時伺服器還可以將未傳送完的資料向客戶端傳送。
3.伺服器沒有資料向客戶端傳送之後,就會發出連線釋放報文等待客戶端確認,伺服器進入最終確認狀態。
4.客戶端收到伺服器傳送的釋放報文後,向伺服器傳送一個確認報文,伺服器進入連線關閉狀態。客戶端同時進入時間等待(TIME-WAIT)狀態。
此時連線還沒有被釋放掉。客戶端會等待2MSL的時間,然後進入連線關閉狀態。至此連線斷開完成。
每一條TCP的連線唯一的被兩個通訊兩端的兩個端點表示,也就是是四元組(源IP,源埠,目的IP,目的埠),
而不是單純的用一個IP地址和埠區別。
也就意味著一個TCP可以建立多個連線,比如伺服器IP是127.0.0.1,埠是8888;
例如客戶端一:127.0.0.1:3389
客戶端二:127.0.0.1:3390
客戶端三:127.0.0.1:3390
三個連線對應的四元組
TCP 127.0.0.1:3889 127.0.0.1:8888 ESTABLISHED
TCP 127.0.0.1:3890 127.0.0.1:8888 ESTABLISHED
TCP 127.0.0.1:3891 127.0.0.1:8888 ESTABLISHED
我們可以發現即使目的地IP和埠相同,但本地的埠不同導致整個四元組不同。
伺服器可以建立多個連線,前提是四元組不同。連線中無法出現兩個四元組相同的連線。
TCP可以連線多個客戶端,為其每一個客戶端建立一個Socket,Socket不同代表不同連線。
客戶端伺服器之間通過Socket通訊,伺服器加上多執行緒為每一個Socket分配一個執行緒就可實現併發處理。
參考:1、計算機網路(第四版) 謝希仁編著。
2、https://www.cnblogs.com/Andya/p/7272462.html
3、https://blog.csdn.net/sssnmnmjmf/article/details/68486261
二、ServerSocket
ServerSocket(int port)//建立繫結到指定埠的伺服器套位元組。
預設繫結的IP地址是本地的IP地址。
例如我這裡是在個人電腦上面執行,繫結的地址就是當前主機的IP地址。
當前IP地址可按win鍵+r輸入cmd,然後輸入ipconfig -all檢視乙太網介面卡的IPv4地址,後面帶有首選的。
也可以認為是繫結到127.0.0.1上,因為當前C/S都是在一臺電腦上執行都屬於本機訪問,
所以本地測試使用的迴環地址(127.0.0.1和本機IP(192.168.190.1)都可以。
2.主要方法
Socket accept()//監聽要對當前物件IP上指定埠的連線,如果發現有連線請求則連線它。
例如客戶端傳送一個連線請求到當前服務端的對應埠,則建立客戶端與服務端的連線。
這個監聽是一個阻塞式的監聽,意思就是說如果沒有建立連線的話當前程序就不會繼續向下執行。
成功建立連線後,會返回一個Socket物件,而Socket物件中有獲取輸入輸出流的方法,這時就在
客戶端,服務端之間建立輸入輸出流管道,兩者就可以通過這個管道通訊。
三、Socket
1.構造方法:
Socket(InetAddress address, int port)
Socket(String host, int port)
//建立套位元組,並將其繫結到指定的(IP|域名)上的指定埠。
2.主要方法:
InputStream getInputStream()//返回當前Socket物件的輸入流
OutputStream getOutputStream()//返回當前Socket物件的輸出流
四、例子
Server:
import java.io.BufferedWriter; import java.io.IOException; import java.io.OutputStreamWriter; import java.net.ServerSocket; import java.net.Socket; import java.net.UnknownHostException; public class Server { public static void main(String[] args) throws UnknownHostException, IOException { String msg = "歡迎連線到Server!"; ServerSocket server = new ServerSocket(8880);//繫結到本地IP的8880埠 Socket socket = server.accept();//阻塞式接收,接收成功建立連線管道 //連線管道的輸出流,即對連線物件(客戶端)進行輸出。 BufferedWriter bos = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream(),"UTF-8")); bos.write(msg);//伺服器將指定內容發給客戶端 bos.newLine(); bos.flush(); } }
Client:
import java.io.BufferedReader; import java.io.FileDescriptor; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStreamReader; import java.io.PrintStream; import java.net.Socket; import java.net.UnknownHostException; public class Client { public static void main(String[] args) throws UnknownHostException, IOException { Socket client = new Socket("192.168.190.1",8880);//向指定IP地址的指定埠進行連線 // Socket client = new Socket("127.0.0.1",8880);//使用127.0.0.1和使用192.168.190.1都可以完成通訊 //連線成功後,獲取連線管道的輸入流,即對伺服器寫入內容進行讀取 BufferedReader isr = new BufferedReader(new InputStreamReader(client.getInputStream(),"UTF-8")); String re = isr.readLine();//讀取內容 System.out.println(re); } }
執行結果:
歡迎連線到Server!
先執行Server會進行阻塞式接收,沒有建立連線前後面的語句都不會執行。
然後執行Client建立連線後,Server向連線管道中寫入資料,Client向連線管道中讀取資料。
最後將內容顯示到控制檯。
五、簡易聊天室
下面結合多執行緒,和網路程式設計實現一個簡易聊天室。
客戶端先將訊息傳送到伺服器,服務接收訊息後轉發給其他客戶端。
每個客戶端是一個執行緒。
基本流程:
1.A客戶端讀取鍵盤輸入資料,並將其傳送到伺服器。
2.伺服器與A客戶端建立連線後,將A客戶端放入一個容器,同時將A客戶端傳送的訊息,轉發給容器中除A客戶端之外的所有客戶端。
伺服器中為每一個Socket分配一個執行緒,就可以實現併發轉發所有聊天訊息。
3.傳送給其他客戶端後,其他客戶端會讀取伺服器傳送的內容並顯示到自己控制檯。
Send:讀取鍵盤輸入內容並將其傳送給伺服器
import java.io.BufferedReader; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; import java.io.InputStreamReader; import java.io.Reader; import java.net.Socket; public class Send implements Runnable { private boolean Running = true; private DataInputStream dis;//用於讀取service返回的訊息 private DataOutputStream dos;//用於向server傳送訊息 private BufferedReader br; public Send(){ } public Send(Socket client){ try { dis = new DataInputStream(client.getInputStream()); dos = new DataOutputStream(client.getOutputStream()); } catch (IOException e) { // TODO Auto-generated catch block System.err.println("初始化連線失敗!"); Running = false; try { dis.close(); dos.close(); } catch (IOException e1) { // TODO Auto-generated catch block System.err.println("關閉異常!"); } } } //讀取鍵盤輸入資訊並返回 private String reciver(){ String msg=null; br = new BufferedReader(new InputStreamReader(System.in)); try { msg = br.readLine(); } catch (IOException e) { // TODO Auto-generated catch block System.out.println("讀取使用者輸入異常!"); Running = false; try { br.close(); } catch (IOException e1) { // TODO Auto-generated catch block e1.printStackTrace(); } } return msg; } //將鍵盤輸入資訊傳送至伺服器 private void send(){ String msg = reciver(); try { if(msg != null && !msg.equals("")){ dos.writeUTF(msg); dos.flush(); } } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); System.err.println("使用者傳送資訊異常!"); Running = false; try { dos.close(); } catch (IOException e1) { // TODO Auto-generated catch block e1.printStackTrace(); } } } public void run(){ while(Running){ send(); } } }
reciver:讀取伺服器傳送的資料
package ChatRoom; import java.io.DataInputStream; import java.io.IOException; import java.net.Socket; public class Reciver implements Runnable{ private boolean Running = true; private DataInputStream dis; public Reciver(){ } //初始化,獲取連線 public Reciver(Socket client){ try { dis = new DataInputStream(client.getInputStream()); } catch (IOException e) { // TODO Auto-generated catch block System.err.println("Client-->Server連線失敗!"); Running = false; try { dis.close(); } catch (IOException e1) { // TODO Auto-generated catch block System.err.println("關閉異常!"); } } } //讀取客戶端傳送的資料 private String reciver(){ String msg=null; try { msg = dis.readUTF(); } catch (IOException e) { // TODO Auto-generated catch block System.err.println("接受客戶端訊息異常!"); Running = false; try { dis.close(); } catch (IOException e1) { // TODO Auto-generated catch block System.err.println("關閉異常!"); } } return msg; } public void run(){ while(Running){ System.out.println(reciver()); } } }
Client:(192.168.1.1~253) 255.255.255.0
import java.io.IOException; import java.net.Socket; import java.net.UnknownHostException; public class Client { public static void main(String[] args) throws UnknownHostException, IOException { // TODO Auto-generated method stub Socket client = new Socket("192.168.1.254",8888);//連線伺服器 new Thread(new Send(client)).start();//讀取鍵盤資料併發送給伺服器 new Thread(new Reciver(client)).start();//讀取伺服器傳送回來的訊息 } }
Server: 192.168.1.254 255.255.255.0
import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; import java.net.ServerSocket; import java.net.Socket; import java.util.LinkedList; import java.util.List; public class Server { static List<Server.Connect> allUser; public static void main(String[] args) throws IOException{ allUser = new LinkedList<Server.Connect>();//儲存客戶端的容器 ServerSocket serverSocket = new ServerSocket(8888);//設定監聽埠 while(true){//不斷接受客戶端的連線請求 Socket con = serverSocket.accept();//獲取伺服器與客戶端的Socket // System.out.println(con.getPort()); Server server = new Server();//例項化一個伺服器 Server.Connect connect = server.new Connect(con);//建立一個客戶端到伺服器的連線(socket) allUser.add(connect);//將已經連線的客戶端放入容器,也可以看做將socket放入伺服器 new Thread(connect).start();//每連線一個客戶端(socket)就為其開闢一條執行緒,一個伺服器對應多個客戶端。 } } class Connect implements Runnable{// private boolean Running = true;//執行標誌位 DataInputStream dis; DataOutputStream dos; public Connect(){ } public Connect(Socket client){//客戶端連線上伺服器後的socket try {//初始化獲取對客戶的讀寫流 dis = new DataInputStream(client.getInputStream()); dos = new DataOutputStream(client.getOutputStream()); } catch (IOException e) { // TODO Auto-generated catch block System.err.println("Server-->Client連線失敗!"); try { dos.close(); dis.close(); } catch (IOException e1) { // TODO Auto-generated catch block System.err.println("關閉異常"); } } } public String reciver(){//讀取客戶端傳送的訊息 String msg = null; try { msg = dis.readUTF(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); System.err.println("獲取客戶端資訊異常!"); Running = false; try { dis.close(); } catch (IOException e1) { // TODO Auto-generated catch block System.err.println("關閉異常"); } } return msg;//返回讀取的訊息 } public void send(String msg){//將訊息傳送到輸出流dos對應的客戶端 try { dos.writeUTF(msg); dos.flush(); } catch (IOException e) { // TODO Auto-generated catch block System.err.println("傳送客戶端資訊異常!"); Running = false; try { dos.close(); } catch (IOException e1) { // TODO Auto-generated catch block System.err.println("關閉異常"); } } } public void sendOther(){//將訊息傳送到其它客戶端,例如A客戶端傳送過來的訊息,就傳送給除A之外的客戶端 String msg = this.reciver(); System.out.println(msg); for(Connect temp : allUser){//遍歷存放客戶端的容器 if(temp == this)//如果容器中當前物件時是A,就跳過這次迴圈,不是則將訊息傳送到對應的客戶端。 continue; temp.send(msg);//哪一個客戶端呼叫就將訊息發給誰,假如這裡的temp是B就將呼叫B中的send,此時傳送的輸出流是向客戶端B寫入的。 } } @Override public void run() {//開啟多執行緒後伺服器不斷接收客戶端訊息,然後轉發 // TODO Auto-generated method stub while(Running){//執行標誌位,如果中途出現讀寫異常則終止。 sendOther(); } } } }
如果是一臺電腦上測試,則將客戶端中連線伺服器的地址修改為127.0.0.1或localhost埠任選(大於1024即可)。
如果是多臺電腦測試,例如兩臺電腦(將兩臺電腦的網線介面用一根網線連線)。
將其中一臺電腦的IP地址修改為192.169.1.245:255.255.255.0,
另外一臺IP地址只需和其保持同一網段即可例如(192.168.1.1:255.255.255.0)。(最好禁用其餘網絡卡)
在192.168.1.254上先執行伺服器然後執行客戶端,在另外一個電腦上執行客戶端。
通過控制檯輸入可以實現聊天。
如果想實現同一網段多個電腦間通訊需要用到交換機連線多個電腦。