熟悉GBN协议的发送方、接收方状态转换过程、窗口滑动、超时重传、包(packet)装配以及解析、对udt(基于UDP的不可靠传输)的模拟,了解全双工通信的要点。
利用python实现GBN协议。
模拟本次实验需要两个终端,一个承载sender.py,另一个承载receiver.py,在实验时需要先执行sender.py,之后执行receiver.py,两端的ip地址和端口号需要一致。
GBN协议需要将发送数据构建成若干个binary packet来模拟过程并传输,本实验中一个packet的结构为(代码详见附件lib\packet.py)
编写make_pkt函数,其调用了int.to_bytes(处理acknumber)和encode(处理data)来转换成二进制
getacknumber用于获取前四位的acknumber/expectednumber,extract用于将后面的data解码。
- EXPECTEDSEQSUM_SIZE = 4
- def make_pkt(expectedseqnum, data):
- b_expectedseqsum = expectedseqnum.to_bytes(EXPECTEDSEQSUM_SIZE, byteorder = 'little', signed = True)
- b_data = data.encode(encoding = 'utf-8')
- packet = b_expectedseqsum + b_data
- return packet
- def getacknum(rcvpkt):
- return int.from_bytes(rcvpkt[0:EXPECTEDSEQSUM_SIZE], byteorder = 'little', signed = True)
- def extract(rcvpkt):
- return rcvpkt[EXPECTEDSEQSUM_SIZE:].decode(encoding = 'utf-8')
- def hasseqnum(rcvpkt, expectedseqnum):
- if int.from_bytes(rcvpkt[0:EXPECTEDSEQSUM_SIZE], byteorder = 'little', signed = True) == expectedseqnum:
- return True
- else:
- return False
定时功能实现:代码详见lib\timer.py
Python的timer包中设计有起始时间点start_point,状态status默认为0,以及用户传入的超时时间间隔timeinterval。
start_timer用于开始计时,如果计时器的状态为1,则会刷新起始点并重新开始计时。
stop_timer用户停止计时,设计计时器状态为0,并打印输出duration。
timeout会返回一个bool用于判断目前是否已经超时,如果计时器状态为1,并且duration已经超过了设定的time interval,则会返回一个True,表示计时器已经超时,其他情况下返回False。
- import time
- class Timer():
- status = 0
- def __init__(self,timeinterval):
- self.timeinterval = timeinterval
- def start_time(self):
- if self.status:
- print("[timer]: restart timer")
- self.status = 1
- self.start_point = time.time()
- def stop_time(self):
- self.status = 0
- print("[timer]: timer stop")
- self.duration = time.time() - self.start_point
- print("[timer]: duration is ",self.duration)
- def timeout(self):
- if self.status:
- if time.time() - self.start_point > self.timeinterval:
- print("[timer]: time out")
- return True
- else:
- return False
- else:
- return False
模拟udt传输
由于需要全双工通信,并且实在本机上演示发送方和接收方的数据传输,因此正常情况下并不会发生丢包现象,但是为了模拟该情况,利用random.random()来指定阈值产生随机丢包事件,如果丢包事件发生,则本次packet的传输不会完成。
- import random
- class Udt():
- def __init__(self, socket, miss_prob):
- self.socket = socket
- self.miss_prob = miss_prob
- def udt_send(self, snd_pkt):
- R = random.random()
- if R < self.miss_prob:
- print("[udt_send]: appear to miss the packet")
- else:
- print("[udt_send]: send the packet successfully")
- self.socket.send(snd_pkt)
利用socket库创建并绑定TCP的socket套接字,利用上层模拟函数upper_level,调用rdt_send将packet发送至receiver,完成send.txt读入后,使用thread库中的start_new_thread来创建新线程分别处理{rdt_rcv,timeout}事件
函数rdt_send模拟sender发送部分,如果传输未完成并且nextseqnum不超过base+N,将nextseqnum的packet使用udt_send发送至接收方,如果超过了,refuse_data。
函数rdt_rcv模拟sender接受部分,rdt_rcv通过套接字接收响应包,并利用getacknum获取应答码,设置base为acknum+1,判断base和nextseqnum的关系,判断计时器是否重启或停止。
函数timeout模拟超时时间,超时发生后,重启计时器,重传base到nextseqnum-1的所有packet
- import socket
- import lib.packet as packet
- import _thread
- import lib.timer as timer
- import time
- import os
- import lib.udt as udt
- DATA_SIZE = 16
- # data size per packet
- oripkt = []
- # restore original packets
- mutex = _thread.allocate_lock()
- # multi-thread process
- index = 0
- # record how many packets to be sent
- def upper_invoke():
- global index
- with open(os.path.join('file','send.txt'),'rb') as f:
- data = f.read()
- # load the file
- print("[upper-level]: finish reading data.")
- while True:
- if index * DATA_SIZE > len(data):
- break
- oripkt.append((data[index * DATA_SIZE: (index + 1) * DATA_SIZE]).decode(encoding = 'utf-8'))
- index = index + 1
- print("[upper-level]: finish reloading data.")
- # the binary data is divided into original packets(oripkt) in DATA_SIZE bytes
- conn.send(packet.make_pkt(index,'index'))
- time.sleep(1)
- _thread.start_new_thread(rdt_rcv,())
- _thread.start_new_thread(rdt_timeout,())
- for i in range(index):
- if rdt_send(oripkt[i]):
- print("[upper-level]: send data successfully.")
- else:
- while True:
- print("[upper-level]: cannot send data now.")
- if rdt_send(oripkt[i]):
- print("[upper-level]: send data successfully.")
- break
- # invoke rdt_send
- print("[upper-level]: finish sending")
- f.close()
- """
- SENDER
- """
- SENDER_ADDR = ('localhost',9500)
- # sender's address.
- sndpkt = []
- # restore sndpkts(send packets).
- base = 0
- nextseqnum = 0
- N = 5
- timer = timer.Timer(5)
- is_close = 0
- def rdt_send(data):
- global base
- global nextseqnum
- global N
- sender = udt.Udt(conn, 0.001)
- if nextseqnum < base + N:
- mutex.acquire()
- sndpkt.append(packet.make_pkt(nextseqnum, data))
- sender.udt_send(sndpkt[nextseqnum])
- if (base == nextseqnum):
- timer.start_time()
- nextseqnum += 1
- mutex.release()
- print("[sender]: ", nextseqnum, "sndpkt is sent to receiver successfully")
- time.sleep(1)
- return True
- else:
- print("[sender]: refuse data, because there is no available window size")
- time.sleep(1)
- return False
- def rdt_timeout():
- global base
- global nextseqnum
- global timer
- sender = udt.Udt(conn, 0.001)
- # define the udt_send which may loss packet
- while True:
- if timer.timeout():
- mutex.acquire()
- print("[sender]: time out")
- timer.start_time()
- for i in range(base,nextseqnum):
- # stable transmit
- # conn.send(sndpkt[i])
- sender.udt_send(sndpkt[i])
- print("[sender]: re-send from", base, "to", nextseqnum)
- mutex.release()
- def rdt_rcv():
- global base
- global nextseqnum
- global timer
- global index
- global is_close
- while True:
- print("[sender]: wait to receive from receiver")
- rcvpkt = conn.recv(1024)
- if packet.extract(rcvpkt) == 'finish':
- print("[sender]: sender has finish the transmit mission, close the connection")
- conn.close()
- is_close = 1
- break
- print("[sender]: receive packet")
- mutex.acquire()
- base = packet.getacknum(rcvpkt) + 1
- mutex.release()
- print("※", base)
- # update base
- if base == nextseqnum:
- timer.stop_time()
- else:
- timer.start_time()
- if __name__ == '__main__':
- socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- # UDP connection
- socket.bind(SENDER_ADDR)
- # create socket
- socket.listen(1)
- print("[sender]: waiting to connect")
- conn, addr = socket.accept()
- print("[sender]: connect to :", addr)
- upper_invoke()
- while not is_close:
- print("[system]: wait the receiver to receive all the packets")
- time.sleep(1)
函数receive接收来自发送方的packet,判断包中序号是否和expectednum相匹配。如果相匹配,则会make_pkt,并响应“收到了预期序号的包”,并将预期序号自增1;如果不匹配,则会响应“收到了不相匹配的包”,发送上次所制作的包。
如果传输完所有的包,关闭连接。
- import socket
- import os
- import lib.packet as packet
- import lib.udt as udt
- import time
- RECEIVER_ADDR = ('localhost',9500)
- # receiver's address
- expected_num = 0
- data_cache = ""
- def receive():
- global expected_num
- global data_cache
- sender = udt.Udt(socket, 0.001)
- # create unstable connection
- snd_pkt = packet.make_pkt(expected_num - 1, 'ACK')
- rcv_pkt = socket.recv(1024)
- # receive index
- if packet.extract(rcv_pkt) == 'index':
- index = packet.getacknum(rcv_pkt)
- print("[receiver]: receiver get index", index)
- print("[receiver]: finish initalizing")
- # default value
- while True:
- rcv_pkt = socket.recv(1024)
- if packet.hasseqnum(rcv_pkt, expected_num):
- # match the expected number
- data = packet.extract(rcv_pkt)
- print("[receiver]: receive data:", data)
- data_cache += data
- # deliver data
- snd_pkt = packet.make_pkt(expected_num, 'ACK')
- sender.udt_send(snd_pkt)
- print("[receiver]: successfully match the expected number and return an ACK", expected_num, "to sender")
- expected_num += 1
- else:
- socket.send(snd_pkt)
- print("[receiver]: fail to match the expected number and return an earliest ACK", expected_num, "to sender")
- time.sleep(1)
- if index == expected_num:
- print("[receiver]: receiver has received all the packets from the sender")
- end_pkt = packet.make_pkt(0,'finish')
- socket.send(end_pkt)
- # end the connection
- with open(os.path.join("file","receive.txt"),'w') as f:
- f.write(data_cache)
- break
- if __name__ == '__main__':
- socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- # UDP connection
- socket.connect(RECEIVER_ADDR)
- receive()
- socket.close()
开启两个终端,先执行sender.py,再执行receiver.py,传输过程如下:
发送方发送的内容和接收方接收到内容如下:
实验结果分析:
本次实验能够正确传输结果。
但是传输时会出现问题:如果将udt传输的丢包率调大,例如调至0.1左右时,传输结果有时会出现若干个错误的连接符号,有时也会出现不正常的换行
由于此实验内容不包含校验和checksum,因此数据的正确性难以保证;就算checksum被加入,面对丢包率过高的网络依然难以保证报文的正确传输,不能完全保证所传输的内容正确。