目錄結構:html
TCP和UDP協議都是運行在傳輸層的協議,在OSI網絡的七層傳輸模型中,若是咱們把應用層、表示層、傳輸層統稱爲應用層(其實在TCP/IP模型中就是把應用層、表示層、傳輸層統稱爲應用層的),那麼咱們平時編寫的程序就屬於應用層。應用層位於傳輸層之上,當咱們須要使用TCP/UDP協議的時候,直接調用傳輸層留下的TCP/UDP協議接口就能夠了。在下面的示例中,咱們把發送發送數據的一方稱爲客戶端,接受數據的一方稱爲服務端。下面TCP和UDP的實現都採用了C#和Java代碼,這個筆者還須要提一點,就是在Android中使用鏈接的時候,因爲爲了防止網速太卡,因此Android中使用TCP鏈接的時候,應該在一個新建的線程中進行。編程
接下來筆者解釋一下上面的幾個概念:服務器
面向鏈接的傳輸協議:面向鏈接,好比A打電話給B,若是B接聽了,那麼A和B之間就的通話,就是面向鏈接的。網絡
可靠的傳輸協議:可靠的,一旦創建了鏈接,數據的發送必定可以到達,而且若是A說「你好嗎?」 B不會聽到「嗎你好」,這就是可靠地數據傳輸。socket
雙全工的傳輸協議:全雙工,這個理解起來也很簡單,A打電話給B,B接聽電話,那麼A能夠說話給B聽,一樣B也能夠給A說話,不可能只容許一我的說話.。ide
點對點的傳輸協議:點對點,這個看了上面的舉例相比你們都知道了,還要說一點的是,若是在A和B打電話過程當中,B又來了一個緊急電話,那麼B就要將與A的通話進行通話保持,因此無論怎麼講同一個鏈接只能是點對點的,不能一對多。ui
下面這個例子this
1 /* 2 * 在提供端口號的時候應該注意:最好定義在1024~49151。 3 */ 4 public class TestServerString { 5 6 public static void main(String[] args) { 7 try{ 8 //1.建立ServerSocket類型的對象,並提供端口號 9 ServerSocket ss = new ServerSocket(8888); 10 //2.等待客戶端的鏈接請求,使用accept()方法,保持阻塞狀態 11 while(true){ 12 System.out.println("等待客戶端的鏈接請求..."); 13 Socket s = ss.accept(); 14 new ServerThread(s).start(); 15 System.out.println("客戶端鏈接成功!"); 16 } 17 18 }catch(Exception e){ 19 e.printStackTrace(); 20 } 21 22 } 23 24 }
在TestServerString類中採用循環相應多個客戶端的鏈接,url
1 public class ServerThread extends Thread{ 2 3 private Socket s; 4 5 public ServerThread(Socket s){ 6 this.s=s; 7 } 8 9 @Override 10 public void run(){ 11 try{ 12 BufferedReader br = new BufferedReader( 13 new InputStreamReader(s.getInputStream())); 14 PrintStream ps = new PrintStream(s.getOutputStream()); 15 //編程實現服務器能夠不斷地客戶端進行通訊 16 while(true){ 17 //服務器接收客戶端發來的消息並打印 18 String str = br.readLine(); 19 //當客戶端發來"bye"時,結束循環 20 if("bye".equalsIgnoreCase(str)) break; 21 System.out.println(s.getLocalAddress()+":"+ str); 22 //向客戶端回發消息「I received!」 23 ps.println("server received!"); 24 } 25 //4.關閉相關的套接字 26 ps.close(); 27 br.close(); 28 s.close(); 29 }catch(Exception e){ 30 e.printStackTrace(); 31 } 32 } 33 }
在輸入流和輸出流中採用循環可客戶端傳輸信息spa
1 public class TestClientString { 2 3 public static void main(String[] args) { 4 5 try{ 6 //1.建立Socket類型的對象,並指定IP地址和端口號 7 Socket s = new Socket("127.0.0.1", 8888); 8 //2.使用輸入輸出流進行通訊 9 BufferedReader br = new BufferedReader( 10 new InputStreamReader(System.in)); 11 PrintStream ps = new PrintStream(s.getOutputStream()); 12 BufferedReader br2 = new BufferedReader( 13 new InputStreamReader(s.getInputStream())); 14 //編程實現客戶端不斷地和服務器進行通訊 15 while(true){ 16 //提示用戶輸入要發送的內容 17 System.out.println("請輸入要發送的內容:"); 18 String msg = br.readLine(); 19 ps.println(msg); 20 //當客戶端發送"bye"時,結束循環 21 if("bye".equalsIgnoreCase(msg)){ 22 break; 23 }; 24 //等待接收服務器的回覆,並打印回覆的結果 25 String str2 = br2.readLine(); 26 System.out.println("服務器發來的消息是:" + str2); 27 } 28 //3.關閉Socket對象 29 br2.close(); 30 br.close(); 31 ps.close(); 32 s.close(); 33 }catch(Exception e){ 34 e.printStackTrace(); 35 } 36 37 } 38 39 }
在客戶端中採用循環,可讓客戶端與服務器創建一次鏈接,實現屢次通訊。
在socket中有兩個構造方法,值得提一下:
Socket(InetAddress address, int port)
使用這個構造方法,程序會自動綁定一個本地地址,而且在之後的鏈接中不會改變,若是須要在本地模擬多個客戶端,那麼就不可用了。
下面這個構造方法,在鏈接到遠程地址中能夠指定本地地址和端口:
Socket(String host, int port, InetAddress localAddr, int localPort)
若是本地端口指定爲0,那麼系統將會自動選擇一個空閒的端口綁定。
下面筆者給出一個用戶服務端和客戶端互發一條消息的示例:
服務端代碼:
class Server { static void Main(string[] args) { IPAddress ip = IPAddress.Parse("127.0.0.1"); TcpListener server = new TcpListener(ip,8005); server.Start(); TcpClient client = server.AcceptTcpClient(); NetworkStream dataStream = client.GetStream(); //讀數據 byte[] buffer = new byte[8192]; int dataSize = dataStream.Read(buffer, 0, 8192); Console.WriteLine("server讀取到數據:"+Encoding.Default.GetString(buffer,0,dataSize)); //寫數據 string msg = "你好 client"; byte[] writebuffer = Encoding.Default.GetBytes(msg); dataStream.Write(writebuffer, 0, writebuffer.Length); dataStream.Close(); client.Close(); Console.ReadLine(); } }
客戶端代碼:
class Client { static void Main(string[] args) { IPAddress ip = IPAddress.Parse("127.0.0.1"); TcpClient client = new TcpClient(); client.Connect(ip, 8005); //寫數據 NetworkStream dataStream = client.GetStream(); string msg = "你好 server"; byte[] buffer = Encoding.Default.GetBytes(msg); dataStream.Write(buffer, 0, buffer.Length); //讀數據 byte[] readbuffer = new byte[8192]; int dataSize = dataStream.Read(readbuffer, 0, 8192); Console.WriteLine("Client讀取到數據:" + Encoding.Default.GetString(readbuffer, 0, dataSize)); dataStream.Close(); client.Close(); Console.ReadLine(); } }
例:
發送方:
1 public class UDPSender { 2 3 public static void main(String[] args) { 4 try{ 5 /* 6 * create DatagramSocket instance 7 */ 8 DatagramSocket ds=new DatagramSocket(); 9 //create DatagramPackage instance and specify the content to send ,ip address,port 10 InetAddress ia=InetAddress.getLocalHost(); 11 System.out.println(ia.toString()); 12 String str="吳興國"; 13 byte[] data=str.getBytes(); 14 DatagramPacket dp=new DatagramPacket(data,data.length,ia,8888); 15 //send data use send() 16 ds.send(dp); 17 //create DatagramPacket instance for receive 18 byte []b2=new byte[1024]; 19 DatagramPacket dp2=new DatagramPacket(b2,b2.length); 20 ds.receive(dp2); 21 System.out.println("result:"+new String(data)); 22 //close resorce 23 ds.close(); 24 }catch(IOException e){ 25 e.printStackTrace(); 26 } 27 } 28 29 }
接收方:
1 public class UDPReceiver { 2 3 public static void main(String[] args) { 4 try{ 5 /* 6 * create DatagramSocket instance,and support port 7 */ 8 DatagramSocket ds=new DatagramSocket(8888); 9 /* 10 * create DatagramPackage instance for receive data 11 */ 12 byte []data=new byte[1024]; 13 DatagramPacket dp=new DatagramPacket(data,data.length); 14 /* 15 * receive source 16 */ 17 ds.receive(dp); 18 System.out.println("contents are:"+new String(data,0,dp.getLength())); 19 /* 20 * send data 21 */ 22 String str="I received!"; 23 byte[] b2=str.getBytes(); 24 DatagramPacket dp2= 25 new DatagramPacket(b2,b2.length,dp.getAddress(),dp.getPort()); 26 ds.send(dp2); 27 System.out.println("發送成功,ip:"+dp.getAddress()); 28 29 /* 30 * close resource 31 */ 32 }catch(SocketException e){ 33 e.printStackTrace(); 34 }catch(IOException e){ 35 e.printStackTrace(); 36 } 37 } 38 39 }
下面是一個案例,實現客戶端向服務端發送一條信息,而後服務端接收信息而且打印出來:
服務端:
class Server { static void Main(string[] args) { IPEndPoint udpPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 5500); UdpClient udpClient = new UdpClient(udpPoint); //IPEndPoint senderPoint = new IPEndPoint(IPAddress.Parse("14.55.36.2"), 0); IPEndPoint senderPoint = new IPEndPoint(IPAddress.Any, 0); byte[] recvData = udpClient.Receive(ref senderPoint); Console.WriteLine("Receive Message:{0}", Encoding.Default.GetString(recvData)); Console.Read(); } }
客戶端:
class Client { static void Main(string[] args) { IPEndPoint udpPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 4505);//實例化本地IPEndPoint UdpClient udpClient = new UdpClient(udpPoint);//實例化本地UpdClient //UdpClient udpClient = new UdpClient(); string sendMsg = "Hello UDP Server."; byte[] sendData = Encoding.Default.GetBytes(sendMsg); IPEndPoint targetPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 5500);//服務端的IPEndPoint對象 udpClient.Send(sendData, sendData.Length, targetPoint); Console.WriteLine("Send Message:{0}", sendMsg); Console.Read(); } }
CP協議中有長鏈接和短鏈接之分。短鏈接在數據包發送完成後就會本身斷開,長鏈接在發包完畢後,會在必定的時間內保持鏈接,即咱們一般所說的Keepalive(存活定時器)功能。
默認的Keepalive超時須要7,200,000 milliseconds,即2小時,探測次數爲5次。它的功效和用戶本身實現的心跳機制是同樣的。開啓Keepalive功能須要消耗額外的寬帶和流量,儘管這微不足道,但在按流量計費的環境下增長了費用,另外一方面,Keepalive設置不合理時可能會由於短暫的網絡波動而斷開健康的TCP鏈接。
keepalive並非TCP規範的一部分。在Host Requirements RFC羅列有不使用它的三個理由:
(1)在短暫的故障期間,它們可能引發一個良好鏈接(good connection)被釋放(dropped),
(2)它們消費了沒必要要的寬帶,
(3)在以數據包計費的互聯網上它們(額外)花費金錢。然而,在許多的實現中提供了存活定時器。
一些服務器應用程序可能表明客戶端佔用資源,它們須要知道客戶端主機是否崩潰。存活定時器能夠爲這些應用程序提供探測服務。Telnet服務器和Rlogin服務器的許多版本都默認提供存活選項。
我的計算機用戶使用TCP/IP協議經過Telnet登陸一臺主機,這是可以說明須要使用存活定時器的一個經常使用例子。若是某個用戶在使用結束時只是關掉了電源,而沒有註銷(log off),那麼他就留下了一個半打開(half-open)的鏈接。若是客戶端消失,留給了服務器端半打開的鏈接,而且服務器又在等待客戶端的數據,那麼等待將永遠持續下去。存活特徵的目的就是在服務器端檢測這種半打開鏈接。
也能夠在客戶端設置存活器選項,且沒有不容許這樣作的理由,但一般設置在服務器。若是鏈接兩端都須要探測對方是否消失,那麼就能夠在兩端同時設置(好比NFS)。
keepalive工做原理:
若在一個給定鏈接上,兩小時以內無任何活動,服務器便向客戶端發送一個探測段。(咱們將在下面的例子中看到探測段的樣子。)客戶端主機必須是下列四種狀態之一:
1) 客戶端主機依舊活躍(up)運行,而且從服務器可到達。從客戶端TCP的正常響應,服務器知道對方仍然活躍。服務器的TCP爲接下來的兩小時復位存活定時器,若是在這兩個小時到期以前,鏈接上發生應用程序的通訊,則定時器從新爲往下的兩小時復位,而且接着交換數據。
2) 客戶端已經崩潰,或者已經關閉(down),或者正在重啓過程當中。在這兩種狀況下,它的TCP都不會響應。服務器沒有收到對其發出探測的響應,而且在75秒以後超時。服務器將總共發送10個這樣的探測,每一個探測75秒。若是沒有收到一個響應,它就認爲客戶端主機已經關閉並終止鏈接。
3) 客戶端曾經崩潰,但已經重啓。這種狀況下,服務器將會收到對其存活探測的響應,但該響應是一個復位,從而引發服務器對鏈接的終止。
4) 客戶端主機活躍運行,但從服務器不可到達。這與狀態2相似,由於TCP沒法區別它們兩個。它所能代表的僅是未收到對其探測的回覆。
服務器沒必要擔憂客戶端主機被關閉而後重啓的狀況(這裏指的是操做員執行的正常關閉,而不是主機的崩潰)。
當系統被操做員關閉時,全部的應用程序進程(也就是客戶端進程)都將被終止,客戶端TCP會在鏈接上發送一個FIN。收到這個FIN後,服務器TCP向服務器進程報告一個文件結束,以容許服務器檢測這種狀態。
在第一種狀態下,服務器應用程序不知道存活探測是否發生。凡事都是由TCP層處理的,存活探測對應用程序透明,直到後面2,3,4三種狀態發生。在這三種狀態下,經過服務器的TCP,返回給服務器應用程序錯誤信息。(一般服務器向網絡發出一個讀請求,等待客戶端的數據。若是存活特徵返回一個錯誤信息,則將該信息做爲讀操做的返回值返回給服務器。)在狀態2,錯誤信息相似於「鏈接超時」。狀態3則爲「鏈接被對方復位」。第四種狀態看起來像鏈接超時,或者根據是否收到與該鏈接相關的ICMP錯誤信息,而可能返回其它的錯誤信息。
在TCP程序中,我常常須要確認客戶端和服務端是否還保持者鏈接,這個時候有以下兩種方案:
1.TCP鏈接雙方定時發握手消息,而且在後面的程序中單獨啓線程,發送心跳信息。
2.利用TCP協議棧中的KeepAlive探測,也就是對TCP的鏈接的Socket設置KeepAlive。
在Java中利用下面的方法設置長鏈接:
setKeepAlive(boolean)
在C#能夠按照以下方式設置長鏈接:
SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true)
互聯網中的協議被分爲三種,