• python---网络编程


    一、软件开发的架构

    1: C/S架构

      Client与Server  客户端与服务器端,这里的客户端一般泛指客户端应用EXE,程序需要先安装后,才能运行在用户的电脑上,对用户的电脑操作系统环境依赖较大。

    2: B/S架构

      Browser与Server  浏览器端与服务器端。

      Browser浏览器,其实也是一种Client客户端,只是这个客户端不需要去安装什么应用程序,只需在浏览器上通过HTTP请求服务器端相关的资源。

    二、网络基础

      IP地址:指互联网协议地址。

      IP地址是IP协会提供的一种统一的地址格式,它为互联网上的每一个网络和每一台主机分配一个逻辑地址,以此来屏蔽物理地址的差异。

      IP地址是一个32位的二进制数,通常被分割为4个‘8位二进制数’。

      IP地址通常用“点分十进制”表示(a,b,c,d)的形式,其中,a,b,c,d都是0~255之间的十进制整数。

      端口可以认为是设备与外界通讯交流的出口。

      因此IP地址可以精确到具体的一台电脑,而端口精确到具体的程序。

      通过子网掩码,我们就能判断,任意两个IP地址是否处在同一个子网络。方法是将两个IP地址与子网掩码分别进行AND运算(两个位数都是1,则结果1,反之0),然后比较结果是否相同,如果是的话,就表示它们在同一个子网络中,否则就不是。

      总结一下,IP协议的作用主要有两个:一个是为每一台计算机分配IP地址,另一个是确定哪些地址在同一个子网络。

    TCP协议

      TCP---传输控制协议,提供的是面向连接、可靠的字节流服务。当客户与服务器彼此交换数据前,必须先在双方之间建立一个TCP连接,之后才能传输数据。TCP提供超时重发,丢弃重复数据,检验数据,流量控制等功能,保证数据能从一端传输到另一端。

      可靠的、面向连接的协议(eg:打电话)、传输效率低全双工通信(发送缓存&接收缓存)、面向字节流。使用TCP的应用:Web浏览器;电子邮件、文件传输程序。

    TCP是因特网中的传输层协议,使用三次握手协议建立连接。当主动方发出SYN连接请求后,等待对方回答SYN+ACK[1],并最终对对方的 SYN 执行 ACK 确认。这种建立连接的方法可以防止产生错误的连接。[1] 
    TCP三次握手的过程如下:
    客户端发送SYN(SEQ=x)报文给服务器端,进入SYN_SEND状态。
    服务器端收到SYN报文,回应一个SYN (SEQ=y)ACK(ACK=x+1)报文,进入SYN_RECV状态。
    客户端收到服务器端的SYN报文,回应一个ACK(ACK=y+1)报文,进入Established状态。
    三次握手完成,TCP客户端和服务器端成功地建立连接,可以开始传输数据了。
    tcp的三次握手
    建立一个连接需要三次握手,而终止一个连接要经过四次握手,这是由TCP的半关闭(half-close)造成的。
    (1) 某个应用进程首先调用close,称该端执行“主动关闭”(active close)。该端的TCP于是发送一个FIN分节,表示数据发送完毕。
    (2) 接收到这个FIN的对端执行 “被动关闭”(passive close),这个FIN由TCP确认。
    注意:FIN的接收也作为一个文件结束符(end-of-file)传递给接收端应用进程,放在已排队等候该应用进程接收的任何其他数据之后,因为,FIN的接收意味着接收端应用进程在相应连接上再无额外数据可接收。
    (3) 一段时间后,接收到这个文件结束符的应用进程将调用close关闭它的套接字。这导致它的TCP也发送一个FIN。
    (4) 接收这个最终FIN的原发送端TCP(即执行主动关闭的那一端)确认这个FIN。[1] 
    既然每个方向都需要一个FIN和一个ACK,因此通常需要4个分节。
    注意:
    (1) “通常”是指,某些情况下,步骤1的FIN随数据一起发送,另外,步骤2和步骤3发送的分节都出自执行被动关闭那一端,有可能被合并成一个分节。[2] 
    (2) 在步骤2与步骤3之间,从执行被动关闭一端到执行主动关闭一端流动数据是可能的,这称为“半关闭”(half-close)。
    (3) 当一个Unix进程无论自愿地(调用exit或从main函数返回)还是非自愿地(收到一个终止本进程的信号)终止时,所有打开的描述符都被关闭,这也导致仍然打开的任何TCP连接上也发出一个FIN。
    无论是客户还是服务器,任何一端都可以执行主动关闭。通常情况是,客户执行主动关闭,但是某些协议,例如,HTTP/1.0却由服务器执行主动关闭。[2] 
    tcp的四次挥手

    UDP协议

      UDP---用户数据报协议,是一个简单的面向数据报的运输层协议。UDP不提供可靠性,它只是把应用程序传给IP层的数据报发送出去,但是并不能保证它们能到达目的地。由于UDP在传输数据报前不用在客户和服务器之间建立一个连接,且没有超时重发等机制,故而传输速度很快。

      不可靠的、无连接的服务,传输效率高(发送前时延小),一对一、一对多、多对一、多对多、面向报文,尽最大努力服务,无拥塞控制。使用UDP的应用:域名系统 (DNS);视频流;IP语音(VoIP)。

    互联网协议按照功能不同分为osi七层或tcp/ip五层或tcp/ip四层

    每层运行常见的物理设备

      传输层——> 四层交换机、四层的路由器

      网络层——>路由器、三层交换机

      数据链路层——>网桥、以太网交换机、网卡

      物理层——>中继器、集线器、双绞线

    每层运行常见的协议

      应用层——>。。。

      传输层——>TCP与UDP协议

      网络层——>ip协议

      数据链路层——>arp协议   (通过ip找mac地址)

      物理层——>。。。

    交换机:广播 单播 组播

    ip协议:ip地址的格式

      # ip地址 一台机器在一个网络内唯一的标识

      # 子网掩码  ip地址与子网掩码做按位与运算,得到的结果是网段

      # 网关ip 局域网内的机器访问公网ip,就通过网关访问

    三、socket

      Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。

    socket和file的区别:

      1:file模块是针对某个指定文件进行打开、读写、关闭

      2:socket模块是针对服务器端和客户端socket进行打开、读写、关闭

    基于文件类型的套接字家族

      套接字家族的名字:AF_UNIX

      unix一切皆文件,基于文件的套接字调用的就是底层的文件系统来取数据,两个套接字进程运行在同一机器,可以通过访问同一个文件系统间接完成通信

    基于网络类型的套接字家族

      套接字家族的名字:AF_INET

      (还有AF_INET6被用于ipv6,还有一些其他的地址家族,不过,他们要么是只用于某个平台,要么就是已经被废弃,或者是很少被使用,或者是根本没有实现,所有地址家族中,AF_INET是使用最广泛的一个,python支持很多种地址家族,但是由于我们只关心网络编程,所以大部分时候我么只使用AF_INET)

    基于TCP协议的socket

    tcp是基于链接的,必须先启动服务端,然后再启动客户端去链接服务端

    import socket
    sk = socket.socket()
    sk.bind(('127.0.0.1',8898))  #把地址绑定到套接字
    sk.listen()          #监听链接
    conn,addr = sk.accept() #接受客户端链接
    ret = conn.recv(1024)  #接收客户端信息
    print(ret)       #打印客户端信息
    conn.send(b'hi')        #向客户端发送信息
    conn.close()       #关闭客户端套接字
    sk.close()        #关闭服务器套接字(可选)
    服务端
    import socket
    sk = socket.socket()           # 创建客户套接字
    sk.connect(('127.0.0.1',8898))    # 尝试连接服务器
    sk.send(b'hello!')
    ret = sk.recv(1024)         # 对话(发送/接收)
    print(ret)
    sk.close()            # 关闭客户套接字
    客户端

     tcp实现与多个客户端通信,必须结束一个客户端,才能到下一个客户端。

    #服务器端
    import socket
    #tcp协议
    sk = socket.socket()     #  创建一个socket对象
    sk.bind(('127.0.0.1',8080))  # 给server端绑定一个ip和端口
    sk.listen()             
    while True:
        conn,addr = sk.accept()  
        while True:
            msg = conn.recv(1024).decode('utf-8')    # 阻塞,直到收到一个客户端发来的消息
            print(msg)
            if msg == 'bye':break
            info = input('>>>')
            if info == 'bye':
                conn.send(b'bye')
                break
            conn.send(info.encode('utf-8'))     # 发消息
        conn.close()             # 关闭连接
    sk.close()               # 关闭socket对象,如果不关闭,还能继续接收
    
    
    
    #客户端1
    import socket
    sk = socket.socket()
    sk.connect(('127.0.0.1',8080))
    while True:
        msg = input('>>>')
        if msg == 'bye':
            sk.send(b'bye')
            break
        sk.send(msg.encode('utf-8'))
        ret = sk.recv(1024).decode('utf-8')
        if ret == 'bye':break
        print(ret)
    sk.close()
    
    
    #客户端2
    import socket
    sk = socket.socket()
    sk.connect(('127.0.0.1',8080))
    while True:
        msg = input('client2:>>>')
        if msg == 'bye':
            sk.send(b'bye')
            break
        sk.send(('client2 :'+msg).encode('utf-8'))
        ret = sk.recv(1024).decode('utf-8')
        if ret == 'bye':break
        print(ret)
    sk.close()
    tcp服务器与多个客户端通信

    基于udp协议的socket

    #服务器端
    import socket
    sk = socket.socket(type=socket.SOCK_DGRAM)  #DGRAM datagram
    sk.bind(('127.0.0.1',8080))       #只有服务端有的
    
    msg,addr = sk.recvfrom(1024)
    print(msg.decode('utf-8'))
    sk.sendto(b'bye',addr)
    
    sk.close()
    
    
    # udp的server 不需要进行监听也不需要建立连接
    # 在启动服务之后只能被动的等待客户端发送消息过来
    # 客户端发送消息的同时还会 自带地址信息
    # 消息回复的时候 不仅需要发送消息,还需要把对方的地址填写上
    
    
    
    #客户端
    import socket
    sk = socket.socket(type=socket.SOCK_DGRAM)
    ip_port = ('127.0.0.1',8080)
    
    sk.sendto(b'hello',ip_port)
    ret,addr = sk.recvfrom(1024)
    print(ret.decode('utf-8'))
    
    sk.close()
    
    # client端不需要connect 因为UDP协议是不需要建立连接的
    # 直接了解到对方的ip和端口信息就发送数据就行了
    # sendto和recvfrom的使用方法是完全和server端一致的
    
    # client端不需要connect 因为UDP协议是不需要建立连接的
    # 直接了解到对方的ip和端口信息就发送数据就行了
    # sendto和recvfrom的使用方法是完全和server端一致的
    基于udp协议的socket
    #服务器端
    
    import socket
    sk = socket.socket(type=socket.SOCK_DGRAM)
    sk.bind(('127.0.0.1',8080))
    while True:
        msg,addr = sk.recvfrom(1024)
        print(addr)
        print(msg.decode('utf-8'))
        info = input('>>>').encode('utf-8')
        sk.sendto(info,addr)
    sk.close()
    
    
    
    #客户端1
    
    import socket
    sk = socket.socket(type=socket.SOCK_DGRAM)
    ip_port = ('127.0.0.1',8080)
    while True:
        info = input('tiger :')
        info = ('33[34m来自tiger的消息 :%s33[0m'%info).encode('utf-8')
        sk.sendto(info,ip_port)
        msg,addr = sk.recvfrom(1024)
        print(msg.decode('utf-8'))
    sk.close()
    
    
    #客户端2
    
    import socket
    sk = socket.socket(type=socket.SOCK_DGRAM)
    ip_port = ('127.0.0.1',8080)
    while True:
        info = input('二哥 :')
        info = ('33[32m来自二哥的消息 :%s33[0m'%info).encode('utf-8')
        sk.sendto(info,ip_port)
        msg,addr = sk.recvfrom(1024)
        print(msg.decode('utf-8'))
    sk.close()
    QQ聊天
    #服务器端
    
    import time
    import socket
    sk = socket.socket(type=socket.SOCK_DGRAM)
    sk.bind(('127.0.0.1',8090))
    while True:
        strf,addr = sk.recvfrom(1024)
        strf = strf.decode('utf-8')
        res = time.strftime(strf).encode('utf-8')
        sk.sendto(res,addr)
    sk.close()
    
    
    #客户端
    
    import socket
    sk = socket.socket(type=socket.SOCK_DGRAM)
    addr = ('127.0.0.1',8090)
    info = input('>>>').encode('utf-8')
    sk.sendto(info,addr)
    ret,addr = sk.recvfrom(1024)
    print(ret.decode('utf-8'))
    sk.close()
    格式化时间

    服务端套接字函数

    s.bind()   绑定(主机,端口号)到套接字

    s.listen()    开始TCP监听

    s.accept()  被打接收TCP客户的连接,出错时返回出错码,而不是抛出异常

    客户端套接字函数

    s.connect()  主动初始化TCP服务器连接

    s.connect_ex()     connect()函数的扩展版本,出错时返回出错码,而不是抛出异常

    公共用途的套接字函数

    s.recv()  接收TCP数据

    s.send()  发送TCP数据(send在待发数据量大于几端缓存区剩余空间时,数据丢失,不会发完)

    s.sendall() 发送完整的TCP数据(本质就是循环调用send,sendall在待发数据量大于几端缓存区剩余空间时,数据不丢失,循环调用send直到发完)

    s.recvfrom()   接收UDP数据

    s.sendto()    发送UDP数据

    s.getpeername() 连接到当前套接字的远端的地址

    s.getsockname() 当前套接字的地址

    s.getsockopt()  返回指定套接字的参数

    s.setsockopt()  设置指定套接字的参数

    s.close()     关闭套接字

    面向锁的套接字方法

    s.setblocking()  设置套接字的阻塞与非阻塞模式

    s.settimeout()   设置阻塞套接字操作的超时时间

    s.gettimeout()   得到阻塞套接字操作的超时时间

    面向文件的套接字的函数

    s.fileno()    套接字的文件描述符

    s.makefile()     创建一个与该套接字相关的文件

    四、黏包

    黏包:同时执行多条命令之后,得到的结果很可能只有一部分,在执行其他命令的时候又接收到之前执行的另外一部分结果,这种显现就是黏包。

    只有TCP有黏包现象,UDP永远不会黏包:

    1.从表面上看,黏包问题主要是因为发送方和接收方的缓存机制、tcp协议面向流通信的特点。

    2.实际上,主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少个字节的数据所造成的。

      用UDP协议发送时,用sendto函数最大能发送数据的长度为:65535- IP头(20) – UDP头(8)=65507字节。用sendto函数发送数据时,如果发送数据长度大于该值,则函数会返回错误。(丢弃这个包,不进行发送)

      用TCP协议发送时,由于TCP是数据流协议,因此不存在包大小的限制(暂不考虑缓冲区的大小),这是指在用send函数时,数据长度参数不受限制。而实际上,所指定的这段数据并不一定会一次性发送出去,如果这段数据比较长,会被分段发送,如果比较短,可能会等待和下一次数据一起发送。发送方引起的粘包是由TCP协议本身造成的,TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一个TCP段。若连续几次需要send的数据都很少,通常TCP会根据优化算法把这些数据合成一个TCP段后一次发送出去,这样接收方就收到了粘包数据。

    会发生黏包的两种情况:

    1、发送方的缓存机制

      发送端需要等缓冲区满才发送出去,造成黏包(发送数据时间间隔很短,数据量很小,会合到一起,产生黏包)

    import socket
    ip_port=('127.0.0.1',8080)
    tcp_socket_server=socket(AF_INET,SOCK_STREAM)
    tcp_socket_server.bind(ip_port)
    tcp_socket_server.listen()
    conn,addr=tcp_socket_server.accept()
    data1=conn.recv(10)
    data2=conn.recv(10)
    print('----->',data1.decode('utf-8'))
    print('----->',data2.decode('utf-8'))
    conn.close()
    服务端
    import socket
    BUFSIZE=1024
    ip_port=('127.0.0.1',8080)
    s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    res=s.connect_ex(ip_port)
    s.send('hello'.encode('utf-8'))
    s.send('egg'.encode('utf-8'))
    客户端

    2、接收方的缓存机制

      接收方不及时接收缓冲区的包,造成多个包接收(客户端发送了一段数据,服务端只收了一小部分,服务端下次再收的时候还是从缓冲区拿上次遗留的数据,产生黏包)

    import socket
    ip_port=('127.0.0.1',8080)
    tcp_socket_server=socket(AF_INET,SOCK_STREAM)
    tcp_socket_server.bind(ip_port)
    tcp_socket_server.listen(5)
    conn,addr=tcp_socket_server.accept()
    data1=conn.recv(2) #一次没有收完整
    data2=conn.recv(10)#下次收的时候,会先取旧的数据,然后取新的
    print('----->',data1.decode('utf-8'))
    print('----->',data2.decode('utf-8'))
    conn.close()
    服务端
    import socket
    BUFSIZE=1024
    ip_port=('127.0.0.1',8080)
    s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    res=s.connect_ex(ip_port)
    s.send('hello egg'.encode('utf-8'))
    客户端

    黏包的解决方案

    方案一

    import socket
    sk = socket.socket()
    sk.bind(('127.0.0.1',8080))
    sk.listen()
    conn,addr = sk.accept()
    while True:
        cmd = input('>>>')
        if cmd == 'q':
            conn.send(b'q')
            break
        conn.send(cmd.encode('gbk'))
        num = conn.recv(1024).decode('utf-8')  # 2048
        conn.send(b'ok')
        res = conn.recv(int(num)).decode('gbk')
        print(res)
    conn.close()
    sk.close()
    服务端
    import socket
    import subprocess
    
    sk = socket.socket()
    sk.connect(('127.0.0.1',8080))
    while True:
        cmd = sk.recv(1024).decode('gbk')
        if cmd == 'q':
            break
        res = subprocess.Popen(cmd,shell=True,
                         stdout=subprocess.PIPE,
                         stderr=subprocess.PIPE)
        std_out = res.stdout.read()
        std_err = res.stderr.read()
        sk.send(str(len(std_out)+len(std_err)).encode('utf-8'))   #2000
        sk.recv(1024)   # ok
        sk.send(std_out)
        sk.send(std_err)
    sk.close()
    
    # 好处:确定了我到底要接收多大的数据
        # 要在文件中配置一个配置项 : 就是每一次recv的大小   buffer = 4096
        # 当我们要发送大数据的时候 ,要明确的告诉接收方要发送多大的数据,以便接收方能够准确的接收到所有数据
        # 多用在文件传输的过程中
            # 大文件的传输 一定是按照字节读 每一次读固定的字节
            # 传输的过程中 一边读一边传 接收端 一边收一边写
    
    # 不好的地方:多了一次交互
    # send sendto 在超过一定范围的时候 都会报错
    # 程序的内存管理
    客户端

    方案二

      借用struct模块,该模块可以把一个类型,如数字,转成固定长度的bytes。我们知道长度数字可以被转换成一个标准大小的4字节数字。因此可以利用这个特点来预先发送数据长度。

    发送时:

      先发送struct转换好的数据长度4字节;再发送数据。

    接收时:

      先接受4个字节使用struct转换成数字来获取要接收的数据长度;再按照长度接收数据。

    import socket,struct,json
    import subprocess
    phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) #就是它,在bind前加
    
    phone.bind(('127.0.0.1',8080))
    
    phone.listen(5)
    
    while True:
        conn,addr=phone.accept()
        while True:
            cmd=conn.recv(1024)
            if not cmd:break
            print('cmd: %s' %cmd)
    
            res=subprocess.Popen(cmd.decode('utf-8'),
                                 shell=True,
                                 stdout=subprocess.PIPE,
                                 stderr=subprocess.PIPE)
            err=res.stderr.read()
            print(err)
            if err:
                back_msg=err
            else:
                back_msg=res.stdout.read()
    
            conn.send(struct.pack('i',len(back_msg))) #先发back_msg的长度
            conn.sendall(back_msg) #在发真实的内容
        conn.close()
    服务端(自定制报头)
    import socket,time,struct
    
    s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    res=s.connect_ex(('127.0.0.1',8080))
    while True:
        msg=input('>>: ').strip()
        if len(msg) == 0:continue
        if msg == 'quit':break
        s.send(msg.encode('utf-8'))
        l=s.recv(4)
        x=struct.unpack('i',l)[0]
        print(type(x),x)
        # print(struct.unpack('I',l))
        r_s=0
        data=b''
        while r_s < x:
            r_d=s.recv(1024)
            data+=r_d
            r_s+=len(r_d)
    
        # print(data.decode('utf-8'))
        print(data.decode('gbk')) #windows默认gbk编码
    客户端(自定制报头)

      也可以把报头坐车字典,字典里包含将要发送的真实数据的详细信息,然后json序列化,再用struct将序列化后的数据长度打包成4个字节。

    发送时:

      先发报头长度;再编码报头内容然后发送;最后发真实内容。

    接收时:

      先接收报头长度,用struct取出来;根据取出的长度收取报头内容,然后解码,反序列化;最后从反序列化的结果中取出待取数据的详细信息,最后去取真实的数据内容。

    import socket,struct,json
    import subprocess
    phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) #就是它,在bind前加
    
    phone.bind(('127.0.0.1',8080))
    
    phone.listen()
    
    while True:
        conn,addr=phone.accept()
        while True:
            cmd=conn.recv(1024)
            if not cmd:break
            print('cmd: %s' %cmd)
    
            res=subprocess.Popen(cmd.decode('utf-8'),
                                 shell=True,
                                 stdout=subprocess.PIPE,
                                 stderr=subprocess.PIPE)
            err=res.stderr.read()
            print(err)
            if err:
                back_msg=err
            else:
                back_msg=res.stdout.read()
    
            headers={'data_size':len(back_msg)}
            head_json=json.dumps(headers)
            head_json_bytes=bytes(head_json,encoding='utf-8')
    
            conn.send(struct.pack('i',len(head_json_bytes))) #先发报头的长度
            conn.send(head_json_bytes) #再发报头
            conn.sendall(back_msg) #在发真实的内容
    
        conn.close()
    服务端
    import socket
    import struct,json
    
    ip_port=('127.0.0.1',8080)
    client=socket(AF_INET,SOCK_STREAM)
    client.connect(ip_port)
    
    while True:
        cmd=input('>>: ')
        if not cmd:continue
        client.send(bytes(cmd,encoding='utf-8'))
    
        head=client.recv(4)
        head_json_len=struct.unpack('i',head)[0]
        head_json=json.loads(client.recv(head_json_len).decode('utf-8'))
        data_len=head_json['data_size']
    
        recv_size=0
        recv_data=b''
        while recv_size < data_len:
            recv_data+=client.recv(1024)
            recv_size+=len(recv_data)
    
        print(recv_data.decode('utf-8'))
        #print(recv_data.decode('gbk')) #windows默认gbk编码
    客户端

     

  • 相关阅读:
    音视频知识点
    JavaScript 设计模式单例模式
    实时音视频livepusher、liveplayer组件的使用
    音视频相关资料
    001RTSP、RTMP、HLS对比
    004coredns
    017tcpflow的简单用法
    018容器不退出调试
    017docker镜像占用太大问题
    3. nacos服务发现
  • 原文地址:https://www.cnblogs.com/huangjm263/p/8352249.html
Copyright © 2020-2023  润新知