目錄結構:
TCP和UDP協議都是運行在傳輸層的協議,在OSI網絡的七層傳輸模型中,如果我們把應用層、表示層、傳輸層統稱為應用層(其實在TCP/IP模型中就是把應用層、表示層、傳輸層統稱為應用層的),那么我們平時編寫的程序就屬於應用層。應用層位於傳輸層之上,當我們需要使用TCP/UDP協議的時候,直接調用傳輸層留下的TCP/UDP協議接口就可以了。在下面的示例中,我們把發送發送數據的一方稱為客戶端,接受數據的一方稱為服務端。下面TCP和UDP的實現都采用了C#和Java代碼,這個筆者還需要提一點,就是在Android中使用連接的時候,由於為了防止網速太卡,所以Android中使用TCP連接的時候,應該在一個新建的線程中進行。
1 TCP協議和UDP協議的比較
1.1 TCP協議
TCP的全稱是Transmission Control Protocol (傳輸控制協議)
- 傳輸控制協議,是一種面向連接的協議,類似打電話
- 在通信的整個過程中保持連接
- 保證了數據傳遞的可靠性和有序性
- 是一種全雙工的字節流通信方式
- 服務器壓力比較大,資源消耗比較快,發送數據效率比較低
- 點對點的傳輸協議
接下來筆者解釋一下上面的幾個概念:
面向連接的傳輸協議:面向連接,比如A打電話給B,如果B接聽了,那么A和B之間就的通話,就是面向連接的。
可靠的傳輸協議:可靠的,一旦建立了連接,數據的發送一定能夠到達,並且如果A說“你好嗎?” B不會聽到“嗎你好”,這就是可靠地數據傳輸。
雙全工的傳輸協議:全雙工,這個理解起來也很簡單,A打電話給B,B接聽電話,那么A可以說話給B聽,同樣B也可以給A說話,不可能只允許一個人說話.。
點對點的傳輸協議:點對點,這個看了上面的舉例相比大家都知道了,還要說一點的是,如果在A和B打電話過程中,B又來了一個緊急電話,那么B就要將與A的通話進行通話保持,所以不管怎么講同一個連接只能是點對點的,不能一對多。
1.2 UDP協議
UDP是User Datagram Protocol(用戶數據報協議)
- 用戶數據報協議,是一種非面向連接的協議,類似寫信
- 在通信的整個過程中不需要保持連接
- 不保證數據傳輸的可靠性和有序性
- 是一種雙全工的數據報通信方式
- 服務器壓力比較小,資源比較低,發送效率比較高
- 可以一對一、一對多、多對一、多對多
2 基於TCP的網絡編程模型
2.1 使用Java代碼實現TCP
服務端:
- 創建ServerSocket的對象並且提供端口號, public ServerSocket(int port)
- 等待客戶端的請求連接,使用accept()方法, public Socket accept()
- 連接成功后,使用Socket得到輸入流和輸入流,進行通信
- 關閉相關資源
客戶端:
- 創建Socket類型的對象,並且提供IP地址和端口號, public Socket(String host, int port)
- 使用Socket構造輸入流和輸出流進行通信
- 關閉相關資源
下面這個例子

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類中采用循環相應多個客戶端的連接,

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 }
在輸入流和輸出流中采用循環可客戶端傳輸信息

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,那么系統將會自動選擇一個空閑的端口綁定。
2.2 使用C#代碼實現TCP
服務端:
- 指定需要監聽的地址
- 指定需要監聽的端口
- 開始監聽
- 獲取TcpClient實例
- 獲取NetworkStream實例
- 傳輸數據
- 關閉流
- 關閉連接
客戶端:
- 指明目的地的地址
- 指明目的地的端口
- 連接
- 獲取NetworkStream對象
- 傳輸數據
- 關閉流
- 關閉連接
下面筆者給出一個用戶服務端和客戶端互發一條消息的示例:
服務端代碼:

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(); } }
3 基於UDP的網絡編程模型
3.1 使用Java代碼實現UDP
客戶端:
- 創建DatagramSocket類型的對象,不需要提供任何信息, public DatagramSocket()
- 創建DatagramPacket類型的對象,指定發送的內容、IP地址、端口號, public DatagramPacket(byte[] buf, int length, InetAddress address, int port)
- 發送數據,使用send()方法, public void send(DatagramPacket p)
- 關閉相關的資源
服務端:
- 創建DatagramSocket類型的對象,並且指定端口, public DatagramSocket(int port)
- 創建DatagramPacket類型的對象,用於接收發來的數據, public DatagramPacket(byte[] buf, int length)
- 接收數據,使用receive()方法, public void receive(DatagramPacket p)
- 關閉相關資源
例:
發送方:

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 }
3.2 使用C#代碼實現UDP
客戶端:
- 實例化一個客戶端的IpEndPoint對象
- 實例化一個客戶端的UdpClient對象
- 實例化服務端的IpEndPoint對象
- 使用客戶端的UdpClient發送數據到服務端
服務端:
- 實例化一個服務端的IpEndPoint對象
- 實例化一個服務端的IpUdpClient對象
- 實例化客戶端的的IpEndPoint
- 使用服務端的IpUdpClient接受數據
下面是一個案例,實現客戶端向服務端發送一條信息,然后服務端接收信息並且打印出來:
服務端:

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(); } }
4 TCP的長連接和短連接
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)
5 網絡編程中定義端口應注意事項
互聯網中的協議被分為三種,
- 眾所周知(Well Known Ports)端口:編號0~1023,通常由操作系統分配,用於標識一些眾所周知的服務。眾所周知的端口編號通常又IANA統一分配。它們緊密綁定(binding)於一些服務。通常這些端口的通訊明確表明了某種服務的協議。例如:80端口實際上總是HTTP通訊。
- 注冊(Registered Ports)端口:編號1024~49151,可以動態的分配給不同的網絡應用進程。
- 動態和/或私有端口(Dynamic and/or Private Ports):編號49152~65535,理論上,不應為服務分配這些端口。實際上,機器通常從1024起分配動態端口。但也有例外:SUN的RPC端口從32768開始。