java從零開發TCPIP協議:實現TCP數據的收發機制

本節我們在上一節基礎上進一步完成TCP協議的收發機制。上一節我們已經實現了向服務器方發送一個字符,本節我們要實現連續發送多個字符,並且能正常接收數據功能,完成了這些功能後,我們就可以基於此去開發其他構建在TCP之上的其他協議。

爲了保證數據能正確的連續收發,本節的設計思路是使用一個隊列將發送的數據存儲起來,然後將數據包發送,只有等待收到對方回發的ack後,我們纔將數據從隊列中刪除,如果數據包一直沒有收到ack迴應,我們就啓動一個timer,自動將隊列中的數據包進行發送,如果發送給定次數後還沒有成功,那麼就通知數據發送層發送失敗,接下來我們看看相應代碼設計。

class SendPacketWrapper {
	//將發送的數據包封裝起來存儲在隊列中
	private byte[] packet_to_send;
	private int seq_num;
	private int ack_num;
	private int send_count = 0;

	public SendPacketWrapper(byte[] packet, int seq_num) {
		this.packet_to_send = packet;
		this.seq_num = seq_num;
		this.ack_num = seq_num + packet.length;
	}
	
	public byte[] get_packet() {
		return this.packet_to_send;
	}
	public int get_seq_num() {
		return this.seq_num;
	}
	public int get_ack_num() {
		return this.ack_num;
	}
	public void increase_send_count() {
		this.send_count++;
	}
	public int get_send_count() {
		return this.send_count;
	}

}

class  SendPacketTask extends TimerTask {
	private TCPThreeHandShakes  task_handler;
	public SendPacketTask(TCPThreeHandShakes  handler) {
		this.task_handler = handler;
	}
	@Override
	public void run() {
		this.task_handler.sendPacketInList();
	}
}

第一個類用於負責把發送的數據封裝起來,他記錄了數據的緩衝區,以及發送時對應的seq號,這樣當數據包需要重發時就可以再次使用這個數值進行發送,同時也記錄了應對的ack號,這樣當對方返回ack值時,我們才能檢驗該數據包是否已經被對方接收。

接下來我們在類TCPThreeHandShakes中添加相應變量和代碼:

public class TCPThreeHandShakes extends Application{
    。。。。
    	private int  tcp_state = CONNECTION_IDLE;
	private static int PACKET_SEND_TIMES = 3; //連續發生3次不成功則失敗
	private  Timer send_timer = new Timer(); //定時將發送隊列中的數據包進行發送
	private int packet_resend_time = 2000; //每過一秒就發送隊列中存儲的數據包
	private SendPacketTask resend_packet_task = null;
	//每次發送數據包時先將它存儲在隊列中,發送出去收到ack後再將它從隊列中去除
	private ArrayList<SendPacketWrapper> send_packet_list = new ArrayList<SendPacketWrapper>();
 public TCPThreeHandShakes(byte[] server_ip, short server_port, ITCPHandler tcp_handler)  {
    	this.dest_ip = server_ip;
    	this.dest_port = server_port;
    	 //指定一個固定端口,以便抓包調試	
    	Random rand = new Random();
		this.port = (short)rand.nextInt();
		this.tcp_handler = tcp_handler;
		resend_packet_task = new SendPacketTask(this);
		send_timer.scheduleAtFixedRate(resend_packet_task, packet_resend_time, packet_resend_time);
    }
。。。。
}

我們添加了一系列與數據包發送和檢驗變量和代碼,特別是啓動一個timer,在每兩秒就去檢測數據包隊列,如果裏面還有數據包沒有接收到對應的ack,也就是上次發送時對方沒有成功接收,那麼timer就會將數據包再次發送,如果已經發送超過給定次數,timer就會通知上層應用數據發送失敗。

接下來我們要添加把數據包在發送時存儲到隊列和檢驗隊列數據包的代碼:

 private void savePacketToList(byte[] packet) {
	   //如果數據包沒有存在隊列中就加入隊列
	   boolean contains = false;
	   for(int i = 0; i < send_packet_list.size(); i++) {
		   SendPacketWrapper packet_wrapper = this.send_packet_list.get(i);
		   if (packet_wrapper.get_packet() == packet) {
			   contains = true;
			   break;
		   }
	   }
	   
	   if (contains == false) {
		   SendPacketWrapper packet_wrapper = new SendPacketWrapper(packet, this.seq_num);
		   this.send_packet_list.add(packet_wrapper);
	   }
   }
   
   public void sendPacketInList() {
	   ArrayList<SendPacketWrapper> wrapper_list = new  ArrayList<SendPacketWrapper>();
	   //將所有在隊列中的數據包係數發送,如果數據包發送次數大於給定次數則報告失敗
	   for(int i = 0; i < this.send_packet_list.size(); i++) {
		   SendPacketWrapper packet_wrapper = this.send_packet_list.get(i);
		   if (packet_wrapper.get_send_count() >= PACKET_SEND_TIMES) {
			   this.tcp_handler.send_notify(false, packet_wrapper.get_packet());
		   }
		   else {
			   int old_seq_num = this.seq_num;
			   this.seq_num = packet_wrapper.get_seq_num();
			   try {
				    createAndSendPacket(packet_wrapper.get_packet(), "ACK");
			    } catch (Exception e) {
				    e.printStackTrace();
			    }
			   this.seq_num = old_seq_num;
			   wrapper_list.add(packet_wrapper);
		   }
	   }
	   
	  this.send_packet_list = wrapper_list;
   }
   
   private void checkSendPacketByACK(int recv_ack) {
	   ArrayList<SendPacketWrapper> wrapper_list = new  ArrayList<SendPacketWrapper>();
	   //所有ack值小於返回ack的數據包都已經成功發送,此時要將數據包從隊列移除並通知上層
	   for(int i = 0; i < this.send_packet_list.size(); i++) {
		   SendPacketWrapper packet_wrapper = this.send_packet_list.get(i);
		   int ack =  packet_wrapper.get_ack_num();
		   System.out.println("receive ack: " + ack);
		   if (packet_wrapper.get_ack_num() <= recv_ack) {
			   this.seq_num = packet_wrapper.get_ack_num();
			   System.out.println("next seq num: "+ this.seq_num);
			   this.tcp_handler.send_notify(true, packet_wrapper.get_packet());
		   }
		   else {
			   wrapper_list.add(packet_wrapper);
		   }
	   }
	   
	  this.send_packet_list = wrapper_list;
	  this.seq_num = recv_ack;
   }

在這三個函數中,第一個負責數據包第一次發送時將其存儲在隊列,第二個負責輪訓隊列,將上次沒有發送成功的數據包繼續發送,如果發送超過給定次數則向上層應用報告發送失敗,最後一個函數是在收到對方發來的ack包後檢驗隊列中哪些數據包發送成功,檢驗標準是所有ack小於對方發來ack數值的數據包都表明成功發送,接下來在handleData函數,也就是接收對方發來數據包的函數裏我們增加如下流程:

@Override
	public void handleData(HashMap<String, Object> headerInfo) {
       。。。。
        if (tcp_state == CONNECTION_SEND || tcp_state == CONNECTION_CONNECTED) {
			  tcp_state = CONNECTION_CONNECTED;
			  checkSendPacketByACK(ack_num);
			  if (data != null && data.length > 0 && seq_num == this.ack_num) {
				   /*
				    * 這裏我們簡化數據的接收流程,爲了提升數據發送效率,很有可能數據包的到來次序與服務器發送時不一樣
				    * ,但爲了讓實現邏輯簡單,我們每次只接收指定數據包,例如當前我們等待seq編號爲1,2,3的數據包,結果
				    * 數據包抵達的次序爲3,1,2,那麼我們就只接收數據包1,讓對方再次發送數據包2,3,顯然這樣子會降低效率,
				    * 但爲了實現邏輯簡單,我們暫時做妥協
				    */
				   this.tcp_handler.recv_notify(data);
				   createAndSendPacket(null, "ACK");
			   }
		  }
。。。。、
}

新添加這段代碼的作用是當對方數據包到來時,我們先抽取出包中的ack值,使用該值去檢驗隊列中哪些數據包已經成功發送,同時如果對方發來的數據包中有數據的話,我們就把數據取出,然後提交給上層應用,最後我們看看上層如何使用該tcp連接層來實現數據發送:

package Application;

import java.net.InetAddress;

import utils.ITCPHandler;

public class TCPRawDataSender implements ITCPHandler{
	private  TCPThreeHandShakes  tcp_socket = null;  
	private String[] buffer = new String[] {"h", "e", "l", "l", "o"};
	private int buffer_p = 0;
	private byte[] current_send_packet = null;
	private void send_content() throws Exception {
		if (buffer_p < buffer.length) {
			System.out.println("send content: " + buffer[buffer_p]);
			byte[] send_content = buffer[buffer_p].getBytes();
			current_send_packet = send_content;
			tcp_socket.tcp_send(send_content);
		}
	}
	@Override
	public void connect_notify(boolean connect_res) {
		if (connect_res) {
			System.out.println("connection established!");
			try {
				send_content();
			} catch (Exception e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
		else {
			System.out.println("connection fail!");
		}
	}
	
	private void close_connection() {
		try {
			tcp_socket.tcp_close();
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}

	@Override
	public void send_notify(boolean send_res, byte[] packet_send) {
		if (send_res == true) {
			System.out.println("send data, buffer_p: " + buffer_p);
			if (packet_send == current_send_packet) {
				buffer_p++;	
				current_send_packet = null;
			}
		}
		
		if (buffer_p >= buffer.length || send_res == false) {
			String info = "send all data ";
			if (send_res == false) {
				info = "send fail with buffer_p: " + buffer_p;
			}
			System.out.println(info);
		} 
		else {
			try {
				send_content();
			} catch (Exception e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
	}

	@Override
	public void connect_close_notify(boolean close_res) {
		if (close_res == true) {
			System.out.println("connection close complete!");
		} else {
			System.out.println("connection close fail!");
		}
	}
	
	public void run() {
		 try {
			InetAddress ip = InetAddress.getByName("192.168.2.127"); //220.181.43.8
			short port = 1234;
			tcp_socket = new TCPThreeHandShakes(ip.getAddress(), port, this);
			tcp_socket.tcp_connect();
			System.out.println("finish handshake!");
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
	@Override
	public void recv_notify(byte[] packet_recv) {
		System.out.println("receive data: " + new String(packet_recv));
		close_connection();
	}

}

該類繼承了ITCPHandler接口以便接收數據發送過程的各種調用,其中buffer中存儲的是要發送給對方的數據,當connect_notify被調用時,如果連接成功,他就會使用send_content函數發送緩衝區裏的一個字符,如果發送成功,它的send_notify會調用,在該函數裏,他檢驗成功發送的數據是不是自己當前正在發生的數據,如果是它就將緩衝器指針挪動一位發送下一個字符,當所有數據發送完畢後,它會等待對方向它發送數據,一旦成功接收對方發來的數據,它的recv_notify函數會被調用,此時它把對方發送來的數據顯示出來後,調用close_connection關閉連接

我在iphone上安裝了一款名爲tcp server的免費app做實驗,我是上面代碼與該app創建的tcp server服務器連接,然後將數據發送給他,並接收從它發過來的數據,最後運行結果如下圖:

1.jpeg

更詳細的講解和代碼調試演示過程,請點擊鏈接

更多技術信息,包括操作系統,編譯器,面試算法,機器學習,人工智能,請關照我的公衆號:
這裏寫圖片描述

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