Socket又稱套接字,是連接運行在網絡上兩個程序間的雙向通訊的端點。
一、使用Socket進行網絡通信的過程
服務端:服務器程序將一個套接字綁定到一個特定的端口,并通過此套接字等待和監聽客戶端的連接請求。
客戶端:客戶端程序根據你服務器所在的主機名和端口號發出連接請求。
兩者之間的通信是通過Socket完成的,我們可以認為Socket是兩個城市之間的交通工具,有了它,就可以在兩個城市之間穿梭了。
Socket通信示例
主機A的應用程序和主機B的應用程序通信,必須通過Socket建立連接,而建立Socket必須由底層的TCP/IP協議來建立TCP連接。建立TCP連接需要底層IP協議來尋址網絡中的主機。IP地址只能幫助我們找到目標主機,但是一個主機上面有多個應用程序,如何才能找到我們需要的應用程序,這個時候就可以通過端口號來指定了。
二、簡易服務端、客戶端模擬
服務器端:
1 public static void main(String[] args) throws IOException 2 { 3 //創建一個ServerSocket,用于監聽客戶端Socket連接請求 4 ServerSocket ss = new ServerSocket(8888); 5 System.out.println("server start"); 6 //采用循環方式監聽客戶端的請求 7 while(true) 8 { 9 //偵聽并接受到此套接字的連接。此方法在連接傳入之前一直阻塞。 10 Socket socket = ss.accept(); 11 OutputStream os = socket.getOutputStream(); 12 PrintStream ps = new PrintStream(os); 13 ps.print("您好,您收到了來自服務端的中秋祝福"); 14 ps.close(); 15 os.close(); 16 socket.close(); 17 } 18 }
執行結果:
server start
客戶端:
1 public static void main(String[] args) throws IOException, Exception 2 { 3 Socket socket = new Socket("localhost",8888); 4 InputStream is = socket.getInputStream(); 5 BufferedReader br = new BufferedReader(new InputStreamReader(is)); 6 String str = br.readLine(); 7 System.out.println(str); 8 br.close(); 9 is.close(); 10 socket.close(); 11 }
執行結果:
您好,您收到了來自服務端的中秋祝福
1、上面展示的是一個簡易的服務端和客戶端通信的建立過程。
2、我們通過交互圖來詳細介紹這個過程:
? ??
3、首先在server端,指定端口號創建serverSocket對象,通過serverSocket的accpet方法獲取套接字,這個方法的特點是:偵聽并接受到此套接字的連接,此方法在連接傳入之前一直阻塞。這也就意味著,如果沒有客戶端連接請求過來,服務端會一致阻塞在這里。
4、后面的代碼就是通過套接字socket可以得到輸入輸出流,到此為止,就是I/O的內容了。
5、在客戶端這邊,通過指定的服務器主機名和服務器監聽的端口號,得到套接字Socket,這個時候就表示服務端和客戶端的連接已經建立了,然后通過輸入輸出流來進行通信了。
三、半關閉的socket
在上面的Demo中,我們是以行作為通信的最小數據單位,服務器端也是逐行進行處理的。但是我們在大多數場景下,通信的數據單位是多行的,這時候Socket的輸出流如何表達輸出的數據已經結束?
在IO學習過程中提到過,如何要表示輸出已經結束,則通過關閉輸出流來實現,但是在socket中是行不通的,因為關閉socket,會導致無法再從該socket中讀取數據了。為了解決這種問題,java提供了兩個半關閉的方法:
1、shutdownInput():關閉該Socket的輸入流,程序還可以通過該Socket的輸出流輸出數據。
2、shutdownOutput():關閉該Socket的輸出流,程序還可以通過該Socket的輸入流讀取數據。
如果我們對同一個Socket實例先后調用shutdownInput和shutdownOutput方法,該Socket實例依然沒有被關閉,只是該Socket既不能輸出數據,也不能讀取數據。
服務器端:
1 ServerSocket ss = new ServerSocket(5555); 2 Socket socket = ss.accept(); 3 PrintStream ps = new PrintStream(socket.getOutputStream()); 4 ps.println("服務器端:開源中國杭州論壇"); 5 ps.println("服務器端:杭州G20峰會"); 6 //關閉輸出流,表明輸出已經結束 7 socket.shutdownOutput(); 8 //判斷該socket是否關閉 9 System.out.println(socket.isClosed()); 10 Scanner scan = new Scanner((socket.getInputStream())); 11 while(scan.hasNextLine()) 12 { 13 System.out.println(scan.nextLine()); 14 } 15 scan.close(); 16 socket.close(); 17 ss.close(); 18 19
?客戶端:
1 Socket s = new Socket("localhost", 5555); 2 InputStream is = s.getInputStream(); 3 byte[] buffer = new byte[1024]; 4 int flag = 0; 5 while(-1 != (flag = is.read(buffer,0,buffer.length))) 6 { 7 String str = new String(buffer,0,flag); 8 System.out.print(str); 9 } 10 PrintStream ps = new PrintStream(s.getOutputStream()); 11 ps.println("客戶端:歡迎參加開源中國論壇"); 12 ps.println("客戶端:歡迎參加G20峰會"); 13 is.close(); 14 ps.close(); 15 s.close(); 16
執行結果:
???
??
在服務器端程序中可以看到,在輸出兩段字符串之后,調用了shutdownOutput方法,表示輸出已經結束。隨即又去判斷了socket是否關閉,執行的結果為false,表示socket并未關閉。
但是在調用了這兩個半關閉的方法關閉了輸出輸入流之后,該socket無法再次打開該輸出流或者輸入流。因此這種場景不適合保持持久通信狀態的交互使用,只適合一站式的通信協議.例如http協議:客戶端連接到服務器之后,開始發送數據,發送完成之后無須再次發送數據,只需要讀取服務器響應數據即可,讀取數據完畢之后,該socket連接也被關閉了。
四、基于UDP協議的網絡編程
前面介紹的socket編程都是基于TCP協議的,現在來看下基于UDP協議的編程,TCP和UDP的區別在上一章已經有過介紹。
UDP協議的主要作用就是完成網絡數據流和數據報之間的轉換-----在信息的發送端,UDP協議將網絡數據流封裝到數據報,然后將數據報發送出去;在信息的接收端,UDP協議將數據報轉換成實際數據報內容。
1、首先在UDP網絡編程中沒有服務器端和客戶端這種說法,兩個socket之間沒有虛擬鏈路,只是接收和發送數據報文而已。
2、這里面有兩個重要的類:DatagramSocket 和DatagramPacket。前者是用來發送和接收數據包的套接字,后者表示數據包,每條報文僅根據該包中的包含的信息從一臺機器 ? ? ? ?路由到另一臺機器。
3、DatagramSocket 的兩個構造函數:
? ? ?DatagramSocket():構造數據報套接字并將其綁定到本地主機上任何可用的端口。
? ? ?DatagramSocket(int?port):創建數據報套接字并將其綁定到本地主機上的指定端口。
? ? ?在我們下面的DEMO中,UDPServerTest類中先發送數據報,使用的是套接字的無參構造器,而UDPClientTest類中先接收數據報,必須監聽某一個端口,所以使用的是套接字的有參構造器。
4、DatagramPacket:創建的時候分為接收和發送兩種
? ? ??DatagramPacket(byte[]?buf, int?length):用來接收長度為?
length
?的數據包。
? ? ??DatagramPacket(byte[]?buf, int?length,?InetAddress?address, int?port):用來將長度為?
length
?的包發送到指定主機上的指定端口號。
1 public class UDPServerTest 2 { 3 public static void main(String[] args) throws IOException 4 { 5 DatagramSocket ds = new DatagramSocket(); 6 String str = "hello world"; 7 //構造用于發送的數據包,指定主機和端口號 8 DatagramPacket packet = new DatagramPacket(str.getBytes(), 9 str.length(), InetAddress.getByName("localhost"), 5555); 10 ds.send(packet); 11 12 //讀取從客戶端發送過來的響應 13 byte[] buffer = new byte[1024]; 14 DatagramPacket packet2 = new DatagramPacket(buffer,buffer.length); 15 ds.receive(packet2); 16 String str2 = new String(buffer,0,packet2.getLength()); 17 System.out.println(str2); 18 ds.close(); 19 } 20 }
public class UDPClientTest {public static void main(String[] args) throws Exception{DatagramSocket ds = new DatagramSocket(5555);byte[] buffer = new byte[1024];DatagramPacket packet = new DatagramPacket(buffer, buffer.length);ds.receive(packet);String str = new String(buffer, 0, packet.getLength());System.out.println(str);// 接收到數據包之后,客戶端返回響應回去String str2 = "welcome";DatagramPacket packet2 = new DatagramPacket(str2.getBytes(), str2.length(), packet.getAddress(), packet.getPort());ds.send(packet2);ds.close();} }
執行過程:
?
1、上面的程序中,第一步是服務器端(暫且以這種叫法來區分這兩個類)創建一個UDP套接字,沒有指定端口,使用的是系統分配的端口。然后構建了一個數據包,包中指定 ? ? ?了目標機器的ip和端口號。
2、作為客戶端,創建了一個UDP套接字,并且綁定了端口,如果想要接收到服務端發送過來的報文,綁定的端口必須和服務器端發送的包中指定的端口一致。
3、客戶端打印了包中的內容之后,想要返回一些內容回去。這個時候,服務器端的ip和端口號可以從之前發送過來的數據包中獲取。
? ???DatagramPacket packet2 = new DatagramPacket(str2.getBytes(), str2.length(), packet.getAddress(), packet.getPort());
4、在服務器接收數據包的時候,已經不需要再像客戶端創建套接字一樣去綁定端口了,因為目前監聽的端口和客戶端發送的包中指定的端口是一樣的。
5、打印看下服務器端的ip和監聽的端口號:
serverIp =/127.0.0.1;serverPort=62965
6、其中DatagramSocket的receive(DatagramPacket?p)方法在接收到數據包前一直阻塞。
?