基于UDP实现TCP

UDP:不可靠的传输协议

TCP:基于字节流的可靠传输协议

UDP和TCP的区别

UDP只负责将数据发送出去,却不保证数据一定准确、按序到达。

TCP是基于字节流的,通过建立连接、对数据按字节标序、确认机制来保证了数据的准确、按序到达;通过对滑动窗口的控制,实现了对数据的流量控制,保证信息传输双方可以正常通信;通过基于拥塞窗口和重传机制实现了拥塞控制,提高了网络的抗压能力。

基于UDP实现TCP

主要思路:基于UDP进行传输数据,通过对UDP数据格式的控制和检测实现TCP的可靠传输

TCP报文格式:

连接、序号、确认、流控、拥塞控制

主要函数功能:

TCP_socket() 构造函数:初始化属性,创建udp

bind(address):绑定地址

listen(i):监听连接请求,若有连接请求(即第一次握手)则放入等待连接队列,并发送第二次握手,若之后收到了第三次握手则将这个连接放到accept队列中。队列中最多有i个连接

accept():读取accept队列,若accept队列为空则堵塞即休眠,若不为空则取出连接,并返回这个基于这个连接的新的sockt(状态为ESTAB-LISHED)和建立连接的对象地址

connect(address):和address建立连接,即发送第一次握手,若三次握手顺利,即表示建立连接,否则报错

close():断开连接,发送断开连接报文

send(str): 发送数据,传入数据(字节型类型),将数据添加到发送数据队列中

recv(bufsize):接收数据,bufsize:一次读取的数据最大容量(以字节为单位)。读取接收队列,返回数据(字节型)

主要属性:

udp:用于发送和接收数据的udp套接字

swnd:swnd=min{rwnd,cwnd} 窗口(包括发送窗口和可用窗口)

snd_una:表示已经发送但未收到确认的数据的第一个字节的序号

snd_nxt:表示可以发送但还未发送的数据的第一个字节的序号

(可用窗口大小=swnd -(snd_una - snd_nxt),这样可用窗口就可以根据swnd,snd_una,snd_nxt实时计算得到

rwnd:接收窗口(即对方还能接收的数据大小)(用于流量控制,以字节为单位)

cwnd:拥塞窗口(即基于网络状况动态得到的可传输数据大小)(用于拥塞控制,以一个最大报文为段单位)

buffer:缓存大小

buffer_now:实际剩余缓存大小,即要发送给对方的接收窗口大小(和应用层读取数据的速度、自身缓存大小有关)

报文格式

基于UDP实现TCP_第1张图片

源端口(2字节) 目标端口(2字节)

序号(4字节)

确认序号(4字节)

数据偏移(4bit 首部长度 以4字节为单位) 保留(6bit  置为0) URG ACK PSH RST SYN FIN 窗口(2字节 最大值为65535)

校验和(2字节) 紧急指针(2字节)  (受数据偏移字段的限制,首部长度最长为60字节)

选项(这里就考虑了最大报文长度MSS:指每一个TCP报文段中数据字段(TCP报文段长度-TCP首部长度)的最大长度。默认为536字节。不够用0补齐 因为报文首部必须为2字节的整数倍)

三次握手和四次挥手

三次握手(沟通序号、确认号、报文最大长度MSS)

假设服务端已经启动listen,此时服务端会对对应的端口进行监听。首先是客户端调用connect来建立连接,客户端先发送第一次握手,服务端接收到第一次握手时会将其放入半连接池,然后发送第二次握手,客户端发送第三次握手,服务端接收到第三次握手后会新建一个TCPsocket放进连接池accept,连接建立。等待服务端调用accept方法返回建立好的连接和连接地址。

第一次握手:client->server SYN=1 ACK=0 FIN=0   seq=x(随机值)(消耗一个序号)(会重传)

第二次握手:server->client SYN=1 ACK=1 FIN=0   seq=y(随机值)   ack=x+1(消耗一个序号)(会重传)

第三次握手:client->server SYN=0 ACK=1 FIN=0   seq=x+1 ack=y+1(若不携带数据则不消耗序号,即client下一次发送数据seq依旧为x+1)

(数据传输过程中动态沟通缓存大小,即接收窗口大小)

四次挥手

第一次挥手:SYN=0 ACK=0 FIN=1 seq=x1 ack=y1(消耗一个序号)(会重传)

第二次挥手:SYN=0 ACK=1 FIN=0 seq=y1 ack=x1+1

第三次挥手:SYN=0 ACK=0 FIN=1 seq=y2 ack=x2(消耗一个序号)(会重传)

第四次挥手:SYN=0 ACK=1 FIN=0 seq=x2 ack=y2+1

发送第一次、第三次挥手前,先进行判断对方是否已确认收到自己之前发送的报文,然后在发送第一次、第三次挥手,发送之后就会不断的查看snd_una,查看是否接收到第二次、第四次挥手,若接收到第四次挥手,就会关闭所有进程,关闭连接。接收到第一次、第三次挥手报文时,会先进行判断之前对方发送的数据是否都已收到,若都已收到,就会发送第二次、第四次挥手。发送完第四次挥手不会立刻关闭连接,而是会启动一个计时器,等待一段时间再关闭连接,若重复接收到第三次挥手会进行重置计时器,目的是为了等待网络中的第四次挥手(可能发生了重传)消失,避免对之后的连接产生影响。

发送和接收

发送报文时读取信息队列,计算发送窗口大小,若发送窗口大小大于MSS则发送信息队列中的数据,一次最多发送MSS长度的数据。发送后需要更新seq,snd_nxt。

接收报文时,先判断报文类型,若为确认报文,要读取确认号,对方的缓存大小,更新snd_una,ack,rwnd;若为数据报文则要读取确认号,对方缓存大小,数据,更新snd_una,ack,rwnd,buffer_now,并将数据存储到数据串中,并发送确认报文

确认机制

确认机制就是通过对数据报文的确认从而保证数据报文传输的按序准确到达。

每收到一个数据报文,接收方都会发送一个确认报文以此来告诉发送方已经确认收到了此报文。

重传机制

重传机制是为了避免数据在传输过程中丢失或者发生错误。发送的报文数据不会已发送就被清除,而是会暂存在缓存中,当数据超过一定时间还未收到确认报文就会触发重传机制,读取缓存中的对应数据,进行重传数据。

超时重传时间RTO计算:第一次计算在建立连接时,之后的计算都在接收到确认报文时

加权平均往返时间 RTTS:第一次测量,RTTS=RTT样本

                                           之后的测量,新RTTS=(1−α)×旧RTTS+α×RTT  (0≤α≤1,RFC推荐的α值为1/8,即0.125)

RTT的偏差RTTD:第一次测量,RTTD=RTT/2

                               之后的测量,新RTTD=(1−β)×旧RTTD+β×|RTTS−RTT| (推荐β的值为1/4,即0.25 )

超时重传时间RTO:RTO=RTTS+4×RTTD

在程序实现中是每发送一个报文就创建一个[报文对象,报文最后一个字节的序号,发送时间,重传时间,重传次数]的列表放入重传队列,会不断读取重传队列,若报文已经确认收到会删除,跳到下一个重传列表,若报文未收到确认且超过了重传时间,就会进行重传,并更新重传列表(重传时间和重传次数),并再次进行判断次列表中的报文是否已确认。若重传次数过多会强制关闭连接。

流量控制

流量控制是为了协调双方的数据接收和发送速率,确保接收方来得及接收数据,避免数据的无用传输,导致资源的浪费。

流量控制的实现机制其实就是通过滑动窗口。报文段中有一个字段叫做窗口,表示的是自身此时的缓存大小,即在此刻缓存的空余量,这样另一方就会将发送的数据量控制在这个范围内,从而实现流量控制。

每收到一个报文就会将报文中的窗口值更新到self.rwnd。

拥塞控制

拥塞控制是为了最大限度的利用网络传输数据的能力,但不会加剧网络的拥堵程度。

拥塞控制会维护一个拥塞窗口cwnd,初始值为1,同时还会有一个慢开始门限ssthresh。

拥塞控制有四个状态,每个TCP都在这四个状态之间不断切换。

慢开始:当cwnd<=ssthresh时,执行慢开始,每发送一个轮次,cwnd翻倍

拥塞避免:当cwnd>ssthresh时,执行拥塞避免,每发送一个轮次,cwnd+=1

快速重传:当连续收到三个确认号相同的确认报文时,会执行快速重传。即立刻重传确认号对应的

报文,并令ssthress=cwnd/2,cwnd=ssthresh,开始执行拥塞避免

快恢复:当发生报文重传时,会执行快恢复。令ssthressh=cwnd/2,cwnd=1,开始执行慢开始。

实现过程中遇到的问题

1.将数据转换成字节来处理

python中有bytes基础类型,如a=b'123456'就是bytes类型,当取单个字节时,如a[0],得到的是那个字节的ascll码值,是int类型;但是当取多个字节时,如a[0:2],返回的就是bytes类型,若字节中存储的数据时数字就可以直接转换成int类型进行处理,如int(a[0:3])得到的就是123。所以TCP的首部信息都可以通过这种方式来进行处理,先将数据转换成二进制数据,再转换成十六进制,再将其转换成bytes类型

端口数据<-->字节

# 假设端口号为63756
port='{:02x}'.format(63756)
result=bytes.fromhex(port)
print(port)
print(result)
print(len(result))

data='{:08b}'.format(result[0])+'{:08b}'.format(result[1])
print(data)
data=int(data,2)
print(int(data))

# f90c
# b'\xf9\x0c'
# 2
# 1111100100001100
# 63756

报文类

# 二进制转十六进制(字符串)
def bin_hex(b):
    def is_1(t):
        if t=='1':
            return True
        elif t=='0':
            return False
        else:
            print("不是二进制")
            return False
    if len(b)%4!=0:
        print("二进制无法转换成十六进制,位数错误")
    else:
        result=""
        hex_list=['0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f']
        bin_list=[8,4,2,1]
        for i in range(len(b)//4):
            temp=0
            s=b[i*4:i*4+4]
            for j in range(4):
                if is_1(s[j]):
                    temp+=bin_list[j]
            result += hex_list[temp]
        return result

# 十六进制转2进制(字符串)
def hex_bin(h):
    result=""
    hex_dic={'0':"0000",'1':"0001",'2':"0010",'3':"0011",'4':"0100",'5':"0101",'6':"0110",'7':"0111",'8':"1000",'9':"1001",'a':"1010",'b':"1011",'c':"1100",'d':"1101",'e':"1110",'f':"1111",}
    for i in range(len(h)):
        result+=hex_dic[h[i]]
    return result

# 报文类
class Message:
    def __init__(self, source_port=0,distinct_port=0,sno=0,cno=0,rwnd=0,data=b"",URG=0,ACK=1,PUSH=1,RESERT=0,SYN=0,FIN=0,urgent_point=0,MSS=0):
        self.source_port = source_port
        self.distinct_port = distinct_port
        self.sno = sno
        self.cno = cno
        self.remain = 0
        self.URG = URG
        self.ACK = ACK
        self.PUSH = PUSH
        self.RESERT=RESERT
        self.SYN = SYN
        self.FIN = FIN
        self.rwnd = rwnd
        # 先暂时不考虑校验和
        self.checksum = 0x9e04
        self.urgent_pointer = urgent_point
        self.data = data
        self.MSS=MSS
        if MSS==0:
            self.header_length = 5
        else:
            self.header_length= 6

    def message_decode(self,message:bytes):
        def bin_b(b,size):
            result=int(b[0:size],2)
            b=b[size:]
            return result,b
        # 先将数据转为16进制,再转为2进制,在进行数据的划分读取
        message_hex=message.hex()
        message_bin=hex_bin(message_hex)
        self.source_port,message_bin=bin_b(message_bin,16)
        self.distinct_port,message_bin=bin_b(message_bin,16)
        self.sno,message_bin=bin_b(message_bin,32)
        self.cno,message_bin=bin_b(message_bin,32)
        self.header_length,message_bin=bin_b(message_bin,4)
        self.remain,message_bin=bin_b(message_bin,6)
        self.URG,message_bin=bin_b(message_bin,1)
        self.ACK,message_bin=bin_b(message_bin,1)
        self.PUSH,message_bin=bin_b(message_bin,1)
        self.RESERT,message_bin=bin_b(message_bin,1)
        self.SYN,message_bin=bin_b(message_bin,1)
        self.FIN,message_bin=bin_b(message_bin,1)
        self.rwnd,message_bin=bin_b(message_bin,16)
        self.checksum,message_bin=bin_b(message_bin,16)
        self.urgent_pointer,message_bin=bin_b(message_bin,16)
        if self.header_length == 6:
            temp,message_bin=bin_b(message_bin,16)
            self.MSS,message_bin=bin_b(message_bin,16)
        self.data=message[self.header_length*4:]


    def message_encode(self):
        # 先全部转为2进制,再转为16进制,再转为字节
        message_bin=""
        message_bin+='{:016b}'.format(self.source_port)
        message_bin += '{:016b}'.format(self.distinct_port)
        message_bin += '{:032b}'.format(self.sno)
        message_bin += '{:032b}'.format(self.cno)
        message_bin += '{:04b}'.format(self.header_length)
        message_bin += '{:06b}'.format(self.remain)
        message_bin += '{:01b}'.format(self.URG)
        message_bin += '{:01b}'.format(self.ACK)
        message_bin += '{:01b}'.format(self.PUSH)
        message_bin += '{:01b}'.format(self.RESERT)
        message_bin += '{:01b}'.format(self.SYN)
        message_bin += '{:01b}'.format(self.FIN)
        message_bin += '{:016b}'.format(self.rwnd)
        message_bin += '{:016b}'.format(self.checksum)
        message_bin += '{:016b}'.format(self.urgent_pointer)
        if self.MSS!=0:
            message_bin += '{:08b}'.format(2)+'{:08b}'.format(4)+'{:016b}'.format(self.MSS)
        message_hex=bin_hex(message_bin)
        message_bytes=bytes.fromhex(message_hex)
        message_bytes+=self.data
        return message_bytes

2.计算报文校验码

暂时不进行计算

3.listen函数、半连接池、连接池

listen函数的功能就是监听指定端口的第一次握手,即SYN报 文,接收到第一次握手后根据第一次握手的信息创建一个新的套接字将其放入半连接池(半连接池的容量是有限度的,可以指定,但是范围不能超过0-30,否则默认为0或者30),进行按序利用创建的套接字发送第二次握手(调用建立连接方法),当接收到第三次握手后,说明连接已经建立,就将套接字放到连接池中,等待accept方法的调用。

4.如何实现服务端的一个端口被多个TCP连接共用

由于在TCP协议中,区分连接的唯一标志就是四元组即(源IP,目的IP,源端口,目的端口),所以一个服务器的端口是可以被多个TCP连接共用的。在listen中每一个TCP连接都会重新创建一个新的socket,所以服务器的端口是同一个listen创建的所有的socket的共用的,每一个创建的socket都必须监听同一个端口,但是由于一个端口只能被一个UDP套接字bind,所以所有的socket都必须共用一个UDP套接字,来进行数据的接收。我们暂且称调用listen方法的socket为主socket。所以必须对主socket中UDP套接字接收到的数据进行区分,主要可以分为两类,一类是SYN连接请求报文,另一类是传输给某一个TCP连接的报文数据。那么不同的socket如何和主socket进行数据的交互呢?可以通过队列来实现,一个主socket有多个接收队列,每一个接收队列对应一个TCP连接,socket通过接收队列实现和主socket的数据交互,而主socket则利用UDPsocket不断接收数据并将数据送至对应的接收队列。

5.多线程装饰器

类中的部分函数需要多线程运行,若通过普通的多线程创建运行方式无法达到较好的效果,所以这里利用了多线程装饰器。

缺陷:1.无返回值


import threading
# 线程装饰器(使方法以多线程的方式运行,但是只能用于无返回值的方法)
def thread_decorate(func):
    def wrapper(*args, **kwargs):
        t = threading.Thread(target=func, args=args,kwargs=kwargs,name=func.__name__)
        t.start()

    return wrapper

6.拥塞避免执行慢开始和拥塞避免的拥塞窗口cwnd增大

若此时发送窗口采用的是接收窗口,是否还需要发送一个轮次后增大拥塞窗口呢?我认为是要的。因为虽然此时接收窗口是小于拥塞窗口的,拥塞窗口继续增大不会产生作用。但是考虑当接收窗口突然增大时,并且超过了拥塞窗口,那么此时就会采用拥塞窗口的值作为发送窗口,那么在这种情况下就可以立即充分利用网络能力,提高了效率。拥塞窗口本来就是对于网络发送数据能力的一种体现,不能因为对方的接收能力却影响了对网络状况的判断。

7.无法获取建立连接的socket

    # 读取全连接池,获取socket对象(主socket)
    def accept(self):
        new_socket = self.accept_queue.get()
        print("得到socket")
        return new_socket, new_socket.connect_address

如果队列为空,那么get方法会一直等待,直到queue中有对象,再返回,但是此时调用之后一直堵塞着但是却获取不到对象,是因为get一直占用着队列吗?

后来改成了先判断队列是否为空,若为空就睡眠1s,若不为空就读取返回

    # 读取全连接池,获取socket对象(主socket)
    def accept(self):
        new_socket = None
        while True:
            if self.accept_queue.empty():
                time.sleep(1)
            else:
                new_socket = self.accept_queue.get()
                break
        print("得到socket")
        return new_socket, new_socket.connect_address

需要改进的点:

1.若第一、第二次握手发生重传,记录报文发送时间的字典send_time无法将其进行删除,因为握手要消耗一个序号,而删除是根据报文的序号加上报文数据部分长度。

2.无法关闭线程single_recv,udp套接字会一直处于拥塞状态,无法进行线程退出的判断

你可能感兴趣的:(计算机网络,udp,tcp/ip,网络)