網絡Socket編程及應用實例

1 TCP和UDP介紹

在介紹TCP和UDP之前,有必要先介紹下網絡體系結構的各個層次。

1.1  網絡體系結構

協議:控制網絡中信息的發送和接收。定義了通信實體之間交換報文的格式和次序,以及在報文傳輸或接收或其他事件所採取的動作。

一般把網絡的層次結構和每層所使用協議的集合稱爲網絡體系結構(NetworkArchitecture)。

由國際標準化組織ISO 在1981年提出的網絡分層結構,簡稱爲OSI參考模型。(Open Systems Interconnection Reference Model)。


各層協議如下,可以看出TCP和UDP協議在傳輸層。


各層功能

1)鏈路層

鏈路層的功能:是把接收到的網絡層數據報(也 稱IP數據報)通過該層的物理接口發送到傳輸介質上,或從物理網絡上接收數據幀,抽出IP數據報並交給IP層。

鏈路層通常包括操作系統中的設備驅動程序和計算機中對應的網絡接口卡。

2)網絡層

主要功能:是可以把源主機上的分組發送到互聯網中的任何一臺目標主機上。

3)傳輸層

爲運行在不同主機上的應用進程提供邏輯通信功能(主機好像是直接相連的)。

進程之間使用邏輯通信功能彼此發送報文,無需考慮具體物理鏈路。

傳輸層主要包括種協議:傳輸控制協議(TCP),用戶數據報協議(UDP)。

4)應用層

應用層向使用網絡的用戶提供特定的、常用的應用程序。

表示層:通信用戶之間數據格式的轉換、數據壓縮及加解密等。

會話層:對數據傳輸進行管理,包括數據交換的定界、同步,建立檢查點等。

1.2 套接字

套接字是從網絡向進程傳遞數據,或從進程向網絡傳遞數據的門戶。傳輸層和應用層的進程通過套接字來傳遞數據。

主機上的套接字可以有多個,每個套接字都有惟一的標識符(格式取決於UDP或TCP)。

當報文段到達主機時,運輸層檢查報文段中的目的端口號,將其定向到相應的套接字。

1.3  用戶數據報協議UDP

用戶數據報協議UDP(User Datagram Protocol):提供用戶之間的不可靠、無連接的報文傳輸服。使用UDP協議的原因

1)無連接創建(減少時延)

2)簡單:無連接(在UDP發送方和接收方之間無握手)

3)段首部小

4)無擁塞控制: UDP能夠儘可能快地傳輸

經UDP的可靠傳輸 : 在應用層增加可靠性,實現特定的差錯恢復

1.4 傳輸控制協議TCP

傳輸控制協議TCP(Transmission Control Protocol):提供用戶之間可靠的、面向連接的報文傳輸服務。

面向連接、可靠的服務是指在進行數據交換前,初始化發送方與接收方狀態,進行握手(交換控制信息)。

建立一個TCP 連接的作用:讓發送方和接收方都做好準備,準備好之後就要開始進行數據傳輸了。三次握手建立TCP連接


三次握手的目的主要在於同步連接雙方發送數據的初始序列號。

TCP 連接是一種全雙工的連接,即一個TCP 連接的兩個端點之間可以同時發送和接收數據,而不是每一個時刻只能有一個端點發送數據。

1.5 TCP與UDP的比較

TCP與UDP特點比較

TCP 是一種面向連接的協議,而UDP 是無連接的協議。
TCP 提供的是可靠的傳輸服務,而UDP 協議提供的是不可靠的服務。
TCP 提供的是面向字節流的服務。
UDP 協議的傳輸單位是數據塊,一個數據塊只能封裝在一個UDP 數據包中。

解釋:

1)可靠、連接

UDP不可靠、無連接:在UDP發送方和接收方之間無握手(交換控制信息)。

面向連接舉例:兩個人之間通過電話進行通信;

面向無連接舉例:郵政服務,用戶把信函放在郵件中期待郵政處理流程來傳遞郵政包裹。顯然,不可達代表不可靠。

2)TCP面向字節流和UDP面向數據塊(面向報文)

面向報文的傳輸方式是應用層交給UDP多長的報文,UDP就照樣發送,即一次發送一個報文。因此,應用程序必須選擇合適大小的報文。若報文太長,則IP層需要分片,降低效率。若太短,會使IP太小。UDP對應用層交下來的報文,既不合並,也不拆分,而是保留這些報文的邊界。這也就是說,應用層交給UDP多長的報文,UDP就照樣發送,即一次發送一個報文。
       面向字節流的話,雖然應用程序和TCP的交互是一次一個數據塊(大小不等),但TCP把應用程序看成是一連串的無結構的字節流。TCP有一個緩衝,當應用程序傳送的數據塊太長,TCP就可以把它劃分短一些再傳送。如果應用程序一次只發送一個字節,TCP也可以等待積累有足夠多的字節後再構成報文段發送出去。

TCP 協議與UDP協議應用的比較

TCP 協議對於有大量數據需要進行可靠傳輸的應用是很適合的。比如可用於文件傳輸協議(FTP )。
對於雖然數據量少但需要時間較長且可靠性要求高的應用TCP 也是比較適合的。 Telnet 就是這種應用的一個例子。
實時應用適合使用UDP 協議。
對於多個實體間的多播式應用無法使用TCP 進行通信 。


從程序實現的角度,TCP與UDP的過程如下


從上圖也能清晰的看出,TCP通信需要服務器端偵聽listen、接收客戶端連接請求accept,等待客戶端connect建立連接後才能進行數據包的收發(recv/send)工作。而UDP則服務器和客戶端的概念不明顯,服務器端即接收端需要綁定端口,等待客戶端的數據的到來。後續便可以進行數據的收發(recvfrom/sendto)工作。

2 JAVA代碼

在JAVA中TCP套接字由Socket類實現,UDP套接字由DatagramSocket類實現。

2.1 TCP的JAVA代碼大致如下

TCP網絡編程步驟
1)創建Socket
2)打開連接到socket的輸入、輸出流
3)按照一定的協議對socket進行讀、寫操作(接收消息-處理數據-發送消息)
4)關閉socket
服務端代碼
	public static start(){
	    ServerSocket serverSocket=null;
	   	try {		
			int i=1;
			
			//1、創建TCP套接字
			int maxConnection="10";   //最大連接數
			serverSocket = new java.net.ServerSocket(8189,maxConnection);		
			
			//等待連接
			Socket sct=serverSocket.accept();
			
			//輸入輸出流
			InputStream inStream = clientSocket.getInputStream();
		    BufferedReader in = new BufferedReader(new InputStreamReader(inStream));
		
	        OutputStream outStream = clientSocket.getOutputStream();
		    BufferedWriter out = new BufferedWriter(new OutputStreamWriter(outStream));
			
			//接收消息-處理數據-發送消息
			//接收客戶端消息
		    String response="",lineStr = in.readLine();
		    while(lineStr!=null){
			  response += lineStr;
			  lineStr = in.readLine();
		   }//end while
		   
		   //TODO 處理消息等及其動作
		
		  //向客戶端發送信息(output-write)
		  out.wirte("TODO"); 
		} 
		catch (Exception e)
		{
			e.printStackTrace();
		}
		finally{
		    if(serverSocket!=null)   //關閉套接字
			  serverSocket.close();
		}
	}//end start()
客戶端代碼與服務端代碼類似,只不過服務端對客戶端的請求是accept,而客戶端需要連接服務端,所以使用connect方法,如下
	public static start(){
	    ServerSocket serverSocket=null;
	   	try {		
			int i=1;
			
			////打開套接字
		   Socket clientSocket = new Socket(String serverMachineIp, int port);
		   int timeout=1000;			 
		   clientSocket.connect(sa, timeout);	
			
			//輸入輸出流
			InputStream inStream = clientSocket.getInputStream();
		    BufferedReader in = new BufferedReader(new InputStreamReader(inStream));
		
	        OutputStream outStream = clientSocket.getOutputStream();
		    BufferedWriter out = new BufferedWriter(new OutputStreamWriter(outStream));
			
			//接收消息-處理數據-發送消息
			/向客戶端發送信息(output-write)
		    out.wirte("TODO"); 
			
			//TODO 處理消息等及其動作
			
			//返回服務端的消息
		    String response="",lineStr = in.readLine();
		    while(lineStr!=null){
			  response += lineStr;
			  lineStr = in.readLine();
		   }//end while	
		} 
		catch (Exception e)
		{
			e.printStackTrace();
		}
		finally{
		    if(serverSocket!=null)   //關閉套接字
			  serverSocket.close();
		}
	}//end start()

2.1 UDP的JAVA代碼大致如下

服務端代碼
public class ServerDatagramSocket 
{
	
    public static void main(String[] args) 
    {
    	try
    	{
        	//1創建套接字,監聽某個端口
    		DatagramSocket ds = new DatagramSocket(10010);
    		
        	//2接收數據
            //構造緩衝數組,用於存放接收到的數據(要求該長度必須大於或等於接收到的數據長度)
            byte[] data=new byte[1024];
            //構造數據包對象
            DatagramPacket receiveDp=new DatagramPacket(data, data.length);
            //接收數據
            ds.receive(receiveDp); 
            //輸出接收到的數據內容
            byte[] receiveByte=receiveDp.getData();
            String receiveContent=new String(receiveByte,0,receiveByte.length);
    		
        	//3發送數據
            //準備數據
    		//獲得客戶端的IP
            InetAddress clientIp=receiveDp.getAddress();
            //獲得客戶端的端口號
            int port=receiveDp.getPort();
            String sendContent="hello back";
            byte[] sendByte=sendContent.getBytes();
            
            DatagramPacket sendDp=new DatagramPacket(sendByte, sendByte.length, clientIp, port);
            
            ds.send(sendDp);            
		} 
    	catch (Exception e) {
			// TODO: handle exception
		}
		finally{
		    //4關閉連接
            ds.close();
		}	
	} 
}
客戶端代碼
public static void main(String[] args) {
		
		try 
		{
			//1創建連接
			DatagramSocket ds=new DatagramSocket();
			
			//2發送數據
			//數據準備(數據內容+地址+端口號)
			String sendContent="hello";
			String host="127.0.0.1";
			int port=10001;
			//將發送的內容轉換爲字節數組
			byte[] sendByte=sendContent.getBytes();
			//將服務器IP轉換爲InetAddress對象
            InetAddress server=InetAddress.getByName(host);
            //構造發送的數據包對象
            DatagramPacket sendDp=new DatagramPacket(sendByte, sendByte.length, server, port);
            
            //發送數據
            ds.send(sendDp);
					
			//3接收數據
            //構造緩衝數組,用於存放接收到的數據(要求該長度必須大於或等於接收到的數據長度)
            byte[] data=new byte[1024];
            //構造數據包對象
            DatagramPacket receiveDp=new DatagramPacket(data, data.length);
            //接收數據
            ds.receive(receiveDp);
            //輸出數據內容
            byte[] receiveByte=receiveDp.getData();   //獲得緩衝數組
            int len=receiveDp.getLength();   //獲得有效數據長度
            String receiveContent=new String(receiveByte, 0, len);
            System.out.println(receiveContent);	
		} 
		catch (Exception e) 
		{
			// TODO: handle exception
		}
		finally{
			//4關閉連接
			ds.close();
		}
	}
}

3 應用(TCP和多線程的應用)

在工作中遇到如下問題:一個算法執行程序可以部署到多臺機器上,一臺機器上可以部署多個算法執行程序,即算法程序和機器是多對多的關係。現在要求用戶只需要輸入提供算法的名稱和相關算法參數(如數據路徑),然後返回數據處理結果。
解決方案:
服務端爲多臺部署算法程序的機器。客戶端響應用戶算法的請求,並從服務端多臺機器中選擇最優機器執行用戶的算法處理。由於一臺服務端機器可能會同時處理多個用戶算法請求,而每個算法處理數據的過程(數據下載,數據處理,處理結果上傳)是個比較長的時間,所以服務端程序採用多線程並行執行多個算法的請求。這個用一個共享FTP來在服務端和客戶端共享處理的數據和上傳處理後的數據。服務端和客戶端通過TCP協議保證可靠的、面向連接的通信服務。整個系統架構如下圖

實現代碼的關鍵部分如下
服務端代碼
/*
服務器端模塊功能:
運行在多臺服務端機器上面,監聽和接收客戶端機器發送的多個算法請求;
業務邏輯:
根據接收到的算法請求參數,啓動一個新的線程,運行算法處理程序,完成最終的數據處理任務
 */
public class ServerSocketApplication {

	/**
	 * @param args
	 */
	public static void main(String[] args) {
		start();
	}//end main()
	
	public static start(){
	    ServerSocket serverSocket=null;
		ExecutorService threadPool=null; //用線程池管理線程
	   	try {		
			//1、創建TCP套接字
			int maxConnection="10";   //最大連接數
			serverSocket = new ServerSocket(8189,maxConnection);		
			
			while(true)  {  //while循環,不停等待算法任務的請求.服務端每接到一個算法處理請求,就開啓一個線程處理該請求
				//等待連接
				Socket sct=serverSocket.accept();
				//Create a work thread to deal with a socket request
		       //創建一個線程池
		       //獲得可用的處理器個數Runtime.getRuntime().availableProcessors()
               // int threadPoolSize=Runtime.getRuntime().availableProcessors() +1; 
              //  ExecutorService threadPool= Executors.newFixedThreadPool(threadPoolSize);
		       threadPool= Executors.newCachedThreadPool();
		       //將線程放入線程池中
               LogicThread logicThread = new LogicThread( sct ); 
		       Thread thd = new Thread( logicThread );
		       threadPool.execute(thd);
			}	//end while	
		} 
		catch (Exception e)
		{
			e.printStackTrace();
		}
		finally{
		    if(threadPool!=null)
			  threadPool.shutdown(); //關閉線程池
		    if(serverSocket!=null)   //關閉套接字
			  serverSocket.close();
		}
	}//end start()
}//end class ServerSocketApplication
//-----------------------------------
//LogicThread的任務是具體負責一次算法執行任務的處理,流程:
//1.解析客戶端發送的算法請求參數 2. 根據請求參數中的算法名稱和類型,調用具體某個算法
public class LogicThread implements Runnable {
	private Socket  clientSocket = null;
	
	public LogicThread( ){
	}
	/**
	 * Constructor
	 * @param sck
	 */
	public LogicThread( Socket clientSocket){
		this.clientSocket = clientSocket;
	}
	
	public void run(){
          clientContentHandle();
	}//end run()
	
	/**
	 * 接收客戶端消息,處理客戶端消息,發送消息到客戶端
	 * @param clientContent 發送的消息
	 * @param serverMachineIp 目標服務器地址
	 * @param port 目標服務器監聽端口
	 * @return   返回的消息
	 */
	private static void clientContentHandle() throws Exception{							
		InputStream inStream = clientSocket.getInputStream();
		BufferedReader in = new BufferedReader(new InputStreamReader(inStream));
		
	    OutputStream outStream = clientSocket.getOutputStream();
		BufferedWriter out = new BufferedWriter(new OutputStreamWriter(outStream));
		
		try{
		  //接收客戶端消息
		  String response="",lineStr = in.readLine();
		  while(lineStr!=null){
			  response += lineStr;
			  lineStr = in.readLine();
		  }//end while
		  
		  //TODO 處理消息
		  //如根據客戶端發送的算法請求及其參數,調用對應的算法開始處理數據,並將處理結果上傳到FTP
		
		  //向服務端發送信息(output-write)
		  out.wirte("successfull"); //處理完通知客戶端
		}
	    catch (Exception e) 
         {
			e.printStackTrace();
		}
		finally{
		    //關閉輸入輸出流
			out.close();
			in.close();
		  	//關閉套接字
	 		s.close();
		}
		
		//處理服務端返回消息(這裏直接返回)
        return response;
	}//end clientContentHandle()
}//end class LogicThread
客戶端代碼
/*
客戶端模塊功能:選擇算法程序所在的最優的服務端機器,向該服務器端機器發送算法調用請求;
*/
public class ClientSocketApplication {

	public static void main(String[] args)
	{       
        //處理服務端返回消息 	
        String response=sendMsg("10.3.11.33",9090);		
		if(response==null)
		   System.out.println("Socket服務器連接失敗:請檢查安裝的Socket服務器是否正常工作!");
		   
		processData("send algorithem name and its parameters");//此處省略掉算法的名稱和其參數
	}//end main()
	
	
	/**
	 * 向指定的服務器發送信息,並接收返回結果
	 * @param sendContent 發送的消息
	 * @param serverMachineIp 目標服務器地址
	 * @param port 目標服務器監聽端口
	 * @return   返回的消息
	 */
	public static String sendMsg(String sendContent, String serverMachineIp, int port) throws Exception{
		//服務端消息返回結果
		String response=null;
		
		//打開套接字
		Socket clientSocket = new Socket(String serverMachineIp, int port);
		int timeout=1000;			 
		clientSocket.connect(sa, timeout);	
		
	    OutputStream outStream = clientSocket.getOutputStream();
		BufferedWriter out = new BufferedWriter(new OutputStreamWriter(outStream));
		
		InputStream inStream = clientSocket.getInputStream();
		BufferedReader in = new BufferedReader(new InputStreamReader(inStream));
		
		try{
		  //向服務端發送信息(output-write)
		  out.wirte(sendContent);
		
		  //接收服務端返回消息
		  String lineStr = in.readLine();
		  while(lineStr!=null){
			  response += lineStr;
			  lineStr = in.readLine();
		  }//end while
		}
	    catch (Exception e) 
         {
			e.printStackTrace();
		}
		finally{
		    //關閉輸入輸出流
			out.close();
			in.close();
		  	//關閉套接字
	 		s.close();
		}
		
		//處理服務端返回消息(這裏直接返回)
        return response;
	}//end sendMsg()
	
	/* 
	*/
	
     /** 監聽服務端處理數據過程
	客戶端選擇最優的服務端處理機器
	客戶端將算法名稱及其參數(如要處理數據的地址)發送給服務端
	客戶端監聽服務端發回的消息,判斷數據是否處理完成,如果完成打包下載處理結果(服務端數據處理完會上傳處理結果和日誌文件log.xml到共享FTP上,供客戶端下載)
	 * @param algName
	 *            請求算法資源名稱
	 * @param reqValues
	 *            請求參數
	 * @return 算法返回結果,無結果返回<code>null</code>
	 */
	public static void processData(String algName, Map<String, String> reqValues) throws ManagerException
	{
	    //一個算法可以部署到不同的機器上,這裏隨機選擇一臺部署該算法的機器(在具體開發中應該根據資源佔用情況選擇等因素,採用高響應比調度算法分配資源)
	    CMachine optCMachine=getOptCMachine(algName);  
		String sendRequest=createReqStr(algName, reqValues);  // 生成算法資源請求文件
		
	    // 發送Socket連接,並等待返回結果
		try {
			String response = sendMsg(reqStr, optCMachine.getIp(),optCMachine.getPort());
			if ( !response.eques("processing") || !response.eques("successfull")) {  //如果服務端返回的消息是正在處理數據或處理成功,則正常,否則服務端出現異常
                   throw new ManagerException("向服務器發送計算請求失敗: " + response);
			} 
			
			// 開始循環監視工作目錄,是否已經完成處理,標識爲有log.xml文件
			String monitDir = FileTool.getNodeManagerShareDir() + "/" + jobID + "/" + algName + "/output";
			File logFile = new File(monitDir + "/log.xml");
			for (;;) {
				if (logFile.exists()) {
					logger.info("Log文件已經上傳,算法處理完成; 開始打包文件");
					ZipTool.zipFiles(monitDir); //打包文件
					break;
				} else {
					Thread.sleep(30000);
					logger.info("請求正在處理中...: " + algName);
				}
			}
		} catch (Exception e) {
			throw new ManagerException("算法服務器處理請求時出錯,請與節點管理員聯繫查看原因");
		}	
	}//end processData()
}//end ClientSocket

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章