• Python网络编程


    1. socket套接字

    1.1 socket简介

    1)网络中的进程间通信

    在本地可以通过进程PID来唯一标识一个进程,但是在网络中这却是行不通的。

    TCP/IP协议族已经解决了这个问题,网络层的“IP地址”可以唯一标识网络中的主机,而传输层“协议+端口”可以唯一标识主机中的应用程序(进程)。

    这样利用  IP地址,协议,端口  就可以表示网络中的进程了,网络中的进程通信就可以利用这个标志与其他进程进行交互。

    2)什么是socket

    socket简称套接字,是进程间通信的一种方式;它能实现不同主机间的进程间通信。

    socket是应用层与TCP/IP协议族通信中间软件抽象层,它是一组接口

    在设计模式中,socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在socket接口后面,对于用户来说,一组简单的接口就是全部,让socket去组织数据,以符合指定的协议;进而,我们无需去关心TCP/UDP协议的细节,因为socket已经封装好了,我们只需要遵循socket的规定去编程,自然就是遵循tcp/udp标准的。

    3)socket层

    1.2 套接字的分类

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

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

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

    • 还有AF_INET6被用于ipv6

    1.3 套接字的工作流程

    1)工作流程图示

    2)工作流程解释

    • 服务器端:服务端先初始化socket,然后与端口绑定(bind),对端口进行监听(listen),再调用accept阻塞,等待客户端连接
    • 客户端:服务端初始化完毕后,如果有个客户端初始化一个socket,然后连接服务器(connect),如果简连接成功,这时客户端与服务端的连接就建立了
    • 客户端与服务端进行交互:客户端发送数据请求,服务端接受请求并处理请求,然后把回应数据发送给客户端,客户端读取数据,最后关闭连接,一次交互结束

    1.4 创建socket

    1)创建socket示例

    • socket_family:可以选择AF_INET或者AF_UNIX
    • socket_type:可以选择SOCK_STREAM(流式套接字,主要用于TCP协议)或者SOCK_DGRAM(数据报套接字,主要用于UDP协议)
    import socket
    socket.socket(socket_family,socket_type,protocal=0)
    # socket_family 可以是 AF_UNIX 或 AF_INET
    # socket_type 可以是 SOCK_STREAM 或 SOCK_DGRAM
    # protocol 一般不填,默认值为 0
    
    # 获取tcp/ip套接字
    tcpSock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    
    # 获取udp/ip套接字
    udpSock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    
    # 由于 socket 模块中有太多的属性,在这里破例使用了'from module import *'语句
    # 使用 'from socket import *', 就把 socket 模块里的所有属性都带到我们的命名空间里了,这样能 大幅减短我们的代码
    # 例如tcpSock = socket(AF_INET, SOCK_STREAM)

    2)服务端套接字函数

    s.bind()     # 绑定(主机,端口号)到套接字
    s.listen()   # 开始TCP监听
    s.accept()   # 被动接受TCP客户的连接,(阻塞式)等待连接的到来

    3)客户端套接字函数

    s.connect()     # 主动初始化TCP服务器连接
    s.connect_ex()  # connect()函数的扩展版本,出错时返回出错码,而不是抛出异常

    4)公共用途的套接字函数

    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()           # 关闭套接字

    5)面向锁的套接字方法

    s.setblocking()     # 设置套接字的阻塞与非阻塞模式
    s.settimeout()      # 设置阻塞套接字操作的超时时间
    s.gettimeout()      # 得到阻塞套接字操作的超时时间

    6)面向文件的套接字的函数

    s.fileno()          # 套接字的文件描述符
    s.makefile()        # 创建一个与该套接字相关的文件

    2. 基于TCP的套接字

    2.1 简单的tcp服务端&客户端

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

    1)socket服务端

    import socket
    
    # 创建tcp套接字
    phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 
    
    # 绑定地址,这里接受的参数是一个元组的形式
    phone.bind(('127.0.0.1', 8000))
    
    # 监听连接
    phone.listen(5) 
    # 此处的listen中的值表示处于半连接和已连接状态的client总和
    # 等待中的半连接和已连接都保存在backlog中,此处的linsten的数量相当于就是在指定backlog的大小
    # 如果当前已建立连接数和半连接数达到设定值,那么新客户端就不会connect成功,而是等待服务器
    # 接受客户端连接,在这个位置等待接收客户端发送的消息 conn, addr = phone.accept() # conn表示为这个客户端创建出了包含tcp三次握手信息的新的套接字 # addr 包含这个客户端的信息 msg = conn.recv(1024) # 接受客户端发来的消息 print('客户端发来的消息是:', msg) conn.send(msg.upper()) # 将接受到的消息转换成大写的形式再发给客户端 conn.close() # 触发四次挥手 phone.close() # 关闭socket

    2)socket客户端

    import socket
    
    phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    
    phone.connect(('127.0.0.1', 8000)) # 链接服务端的相应端口下的应用
    
    phone.send('hello'.encode('utf-8')) # 接受和发送消息时数据都应该是字节格式
    
    data = phone.recv(1024)
    
    print('收到服务端发来的消息:', data)  
    phone.close()

    2.2 循环收发消息

    1)socket服务端(循环收发)

    • 接收消息的的本质其实是在本机的内核空间内提取内容
    • 发送消息的本质其实是将消息内容由用户空间发送到内核空间,然后由内核来完成消息的发送
    from socket import *
    
    tcp_server = socket(AF_INET, SOCK_STREAM)
    
    ip_port = ('127.0.0.1', 9999)
    listen_buffer = 5
    recv_buffer = 1024
    
    tcp_server.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) # 解决端口重用的问题
    tcp_server.bind(ip_port)
    tcp_server.listen(listen_buffer)
    
    while True:
        print('服务端开始运行----->')
        conn, addr = tcp_server.accept()
        while True:
             try:
                print('conn----->',conn)
                print('addr----->',addr)
    
                date = conn.recv(recv_buffer) # 这里的收消息其实是在本机的内核空间内提取内容
                print(date.decode('utf-8'))
    
                conn.send('hello world!?'.encode('utf-8')) # 发消息的本质是将消息内容由用户空间发送到内核空间,然后由内核来完成消息的发送
                # conn.sendall(data)                       # sendall函数可以将一整个数据循环着发送
    
             except Exception:  # 捕获当客户端意外断开时的异常,若发生异常,则退出内循环,而重新尝试连接
                 break
    
        conn.close()
    tcp_server.close()

    2)socket客户端(循环收发)

    from socket import *
    
    tcp_client = socket(AF_INET, SOCK_STREAM)
    
    ip_port = ('127.0.0.1', 9999)
    buffer_size = 1024
    
    tcp_client.connect(ip_port)
    
    while True:
        msg = input("请输入要发送的内容:").strip()
        if not msg : continue
    
        tcp_client.send(msg.encode("utf-8")) # 这里如果发送的消息为空(直接敲回车),则没有任何内容发往本机内核空间,所以消息根本就不会发送
    
        data = tcp_client.recv(buffer_size)
    
        print("接收到的消息为:", data.decode('utf-8'))
    tcp_client.close()

    2.3 关于端口重用

    1)关于Address already in use的报错

    • 由于服务端仍然存在四次挥手的time_wait状态在占用地址
    • 服务器高并发情况下会有大量的time_wait状态的优化方法

    2)端口重用的解决方法一:添加socket配置

    # 加一条socket配置,重用ip和端口
    phone=socket(AF_INET,SOCK_STREAM)
    phone.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) # 在bind前加上这一句
    phone.bind(('127.0.0.1',8080))

    3)端口重用的解决方法二:修改内核参数

    # 发现系统存在大量TIME_WAIT状态的连接,通过调整linux内核参数解决,
    vi /etc/sysctl.conf
    
    # 编辑文件,加入以下内容:
    net.ipv4.tcp_syncookies = 1
    net.ipv4.tcp_tw_reuse = 1
    net.ipv4.tcp_tw_recycle = 1
    net.ipv4.tcp_fin_timeout = 30
    # 然后执行 /sbin/sysctl -p 让参数生效
    
    # 参数说明
    net.ipv4.tcp_syncookies = 1 # 表示开启SYN Cookies。当出现SYN等待队列溢出时,启用cookies来处理,可防范少量SYN攻击,默认为0,表示关闭;
    net.ipv4.tcp_tw_reuse = 1   # 表示开启重用。允许将TIME-WAIT sockets重新用于新的TCP连接,默认为0,表示关闭;
    net.ipv4.tcp_tw_recycle = 1 # 表示开启TCP连接中TIME-WAIT sockets的快速回收,默认为0,表示关闭。
    net.ipv4.tcp_fin_timeout    # 修改系統默认的 TIMEOUT 时间

    3. 基于UDP的套接字

    3.1 UDP简介

    1)UDP

    UDP(用户数据保协议)是一个无连接的简单面向数据报的传输层协议;

    UDP不提供可靠性,它只是把应用程序传给IP层的数据报发送出去,但是并不能保证它们能到达目的地;

    由于UDP在传输数据前不用在客户端和服务端之间建立一个连接,且没有超时重发等机制,故而传输速度很快;

    UDP是一种面向无连接的协议,每一个数据报都是一个独立的信息,包括完整的源地址或目的地址,它在网络上以任何可能的路径传往目的地,因此能到达目的地,到达目的地的时间以及内容的正确性都是不能被保证的。

    2)UDP的特点

    UDP是面向无连接的通讯协议,UDP数据包括目的端口号和源端口信息,由于通讯不需要连接,所以可以实现广播发送;

    UDP传输数据时有大小限制,每个被传输的数据报必须限定在64KB之内;

    UDP是一个不可靠的协议,发送方所发送的数据并不一定以相同的次序到达接收方。

    3)适用情况

    UDP一般多用于多点通信和实时的数据业务,它注重速度流畅。

    • 语⾳⼴播
    • 视频
    • QQ
    • TFTP(简单⽂件传送)
    • SNMP(简单⽹络管理协议)
    • RIP(路由信息协议,如报告股票市场,航空信息)
    • DNS(域名解释)

    3.2 UDP的通信过程

    1)图示

    •  创建客户端套接字
    • 发送/接收数据
    • 关闭套接字

    2)UDP是无链接的,先启动哪一端都不会报错

    3)udp的服务器和客户端的区分

    UDP的服务端和客户端往往是通过 请求服务 和 提供服务 来进⾏区分

    • 请求服务的⼀⽅称为:客户端
    • 提供服务的⼀⽅称为:服务器

    4)关于UDP的端口绑定

    • ⼀般情况下,服务器端,需要绑定端⼝,⽬的是为了让其他的客户端能够正确发送到此进程
    • 客户端⼀般不需要绑定,⽽是让操作系统随机分配,这样就不会因为需要绑定的端⼝被占⽤⽽导致程序⽆法运⾏的情况

    3.3 UDP服务端&客户端

    1)服务端

    from socket import *
    
    ip_port = ('127.0.0.1', 9999)
    buffer_size = 1024
    
    udp_server = socket(AF_INET, SOCK_DGRAM) # 数据报
    udp_server.bind(ip_port)
    
    # udp之所以不发生粘包的现象,是因为每次发送消息都封装一个报文头信息
    
    # recv在自己这端的缓冲区为空时,阻塞
    # recvfrom在自己这端的缓冲区为空时,就收一个空?(此处描述的不准确)
    
    # udp在发送一个空时,发送的不仅仅只是一个空,而是一个带了报文头信息的空
    # 所以在接受时,都接受了一个带报文头信息的空
    
    while True:
        data, addr = udp_server.recvfrom(buffer_size) # recvfrom接受信息的格式为 (b'消息内容', ('ip地址',端口))
        print(data)
    
        udp_server.sendto(data.upper(), addr)

    2)客户端

    from socket import *
    
    ip_port = ('127.0.0.1', 9999)
    buffer_size = 1024
    
    udp_client = socket(AF_INET, SOCK_DGRAM)
    
    while True:
        msg = input('>>>').strip()
        udp_client.sendto(msg.encode('utf-8'), ip_port) # sendto每次发送时都要指定ip和端口,作为第二个参数
    
        data, addr = udp_client.recvfrom(buffer_size)
        print(data.decode('utf-8'))

    4. 粘包现象

    • 注意:结果的编码是以当前所在的系统为准的,如果是windows,那么读出的信息就是GBK编码的,在接收端要用GBK解码。且只能从管道里读一次结果。

    3.1 粘包的产生

    在发送和接收数据的过程中,发送端可以1K 、1K的发送数据,而接收端的应用程序可以2K、2K或者一次3K、6K等等的取走数据;

    应用程序所看到的数据是一个整体(或者是一个流stream),一条消息有多少字节对于应用程序是不可见的,因为TCP是面向流的协议,就很容易出现粘包;

    而UDP段都是一条一条消息(因为每次发送消息都封装了一个报文头信息),应用程序必须以消息为单位提取数据,不能一次提取任意字节的数据,所以UDP永远不会发生粘包。

    可以认为对方一次性write/send的数据为一个消息,当对方send一条信息时,无论底层怎样分段分片,TCP协议层会把构成整条消息的数据段排序完成后才呈现在内核缓冲区。

    3.2 粘包产生的原因

    粘包的主要原因是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据造成的。

    发送方引起的粘包是由TCP协议本身造成的,TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一个TCP段,若连续几次需要send的数据都很少,通常TCP会根据优化算法把这些数据合成一个TCP段后一次发送出去,这样接收方就接收到了粘包数据。

    3.3 详细分析粘包

    1. TCP(transport control protocol,传输控制协议)是面向连接的,面向流的,提供高可靠性服务。收发两端(客户端和服务器端)都要有一一成对的socket,因此,发送端为了将多个发往接收端的包,更有效的发到对方,使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。这样,接收端,就难于分辨出来了,必须提供科学的拆包机制。 即面向流的通信是无消息保护边界的。
    2. UDP(user datagram protocol,用户数据报协议)是无连接的,面向消息的,提供高效率服务。不会使用块的合并优化算法,由于UDP支持的是一对多的模式,所以接收端的skbuff(套接字缓冲区)采用了链式结构来记录每一个到达的UDP包,在每个UDP包中就有了消息头(消息来源地址,端口等信息),这样,对于接收端来说,就容易进行区分处理了。 即面向消息的通信是有消息保护边界的。
    3. tcp是基于数据流的,于是收发的消息不能为空,这就需要在客户端和服务端都添加空消息的处理机制,防止程序卡住,而udp是基于数据报的,即便是你输入的是空内容(直接回车),那也不是空消息,udp协议会帮你封装上消息头,

    udp的recvfrom是阻塞的,一个recvfrom(x)必须对唯一一个sendinto(y),收完了x个字节的数据就算完成,若是 y>x 数据就丢失,这意味着udp根本不会粘包,但是会丢数据,不可靠。

    tcp的协议数据不会丢,没有收完包,下次接收,会继续上次继续接收,己端总是在收到ack时才会清除缓冲区内容。数据是可靠的,但是会粘包。(tcp在数据传输时,发送端先把数据发送到自己的缓存中,然后协议控制将缓存中的数据发往对端,对端返回一个ack=1,发送端则清理缓存中的数据,对端返回ack=0,则重新发送数据,所以tcp是可靠的)

    在tcp中send和recv其实都是在操作各自的缓冲区,并不需要客户端与服务端之间一一对应;而udp中的sendto和recvfrom要一一对应,因为没有将多个数据合并

    应用程序产生的数据会拷贝一份个操作系统,然后由操作系统来发送。

    3.4 发生粘包的情况

    1. 发送端需要等缓冲区满才发送出去,造成粘包(发送数据时间间隔很短,数据了很小,会合到一起,产生粘包)
    2. 接收方不及时接收缓冲区的包,造成多个包接收(客户端发送了一段数据,服务端只收了一小部分,服务端下次再收的时候还是从缓冲区拿上次遗留的数据,产生粘包)

    3.5 拆包的发生情况

    当发送端缓冲区的长度大于网卡的MTU时,tcp会将这次发送的数据拆成几个数据包发送出去。

    3.6 send(字节流)和recv(1024)及sendall

    recv里指定的1024意思是从缓存里一次拿出1024个字节的数据。

    send的字节流是先放入己端缓存,然后由协议控制将缓存内容发往对端,如果待发送的字节流大小大于缓存剩余空间,那么数据丢失,用sendall就会循环调用send,数据不会丢失。

    3.7 低配版解决粘包问题

    • 分析:问题的根源在于接收端不知道发送端将要传送的字节流的长度,所以解决粘包的方法就是如何让发送端在发送数据前,把自己将要发送的字节流总大小让接收端知晓,然后接收端来一个死循环接收完所有数据。
    • 缺点:程序的运行速度远快于网络传输速度,所以在发送一段字节前,先用send去发送该字节流长度,这种方式会放大网络延迟带来的性能损耗。

    1)服务端

    from socket import *
    import subprocess
    
    ip_port = ('127.0.0.1', 8080)
    back_log = 5
    buffer_size = 1024
    
    tcp_server = socket(AF_INET, SOCK_STREAM)
    tcp_server.bind(ip_port)
    tcp_server.listen(back_log)
    
    while True:
        conn, addr = tcp_server.accept()
        print('新的客户端连接', addr)
        while True:
            try:  # 异常处理用于解决客户端意外断开时报异常的问题
                cmd = conn.recv(buffer_size)
                if not cmd:break  # 解决客户端如果退出,服务端一直收空的问题
                print('收到客户端的命令', cmd)
    
                # 执行命令,得到命令的结果cmd_res
                res = subprocess.Popen(cmd.decode('utf-8'), shell=True,
                                    stderr = subprocess.PIPE,  # 将标准输入输出和错误全部扔到subprocess的管道中
                                    stdout = subprocess.PIPE,
                                    stdin = subprocess.PIPE
                                    )
                err = res.stdout.read()
                if err:
                    cmd_res = err
                else:
                    cmd_res = res.stdout.read()
    
                    if not cmd_res: # 若命令执行成功,但返回值为空(如cd .. 命令),我们就自己设置一个返回值发送给客户端以解决卡死的问题
                        cmd_res = "执行成功"
    
                    # 解决粘包的问题---> 将要发送的消息的具体大小发送给客户端,告知其需要接收多少信息
                    length = len(cmd_res)
                    conn.send(str(length).encode('utf-8'))
    
                    client_ready = conn.recv(buffer_size) # 中间插入了一个recv,造成中断,使得两次send的操作不会合并
    
                    if client_ready == b'ready':          # 如果接收到客户端发送过来的ready信号,服务端就开始发送真正的消息内容
                        conn.send(cmd_res)
    
            except Exception as e:
                print(e)
                break
        conn.close()

    2)客户端

    from socket import *
    
    ip_port = ('127.0.0.1', 8080)
    back_log = 5
    buffer_size = 1024
    
    tcp_client = socket(AF_INET, SOCK_STREAM)
    tcp_client.connect(ip_port)
    
    while True:
        cmd = input('>>:').strip()
        if not cmd:continue
        if cmd == "quit":break
    
        tcp_client.send(cmd.encode('utf-8'))
    
        # 解决粘包的问题---> 知道到底需要接收多少数据就可以解决
        length = tcp_client.recv(buffer_size) # 接收服务端发送过来的数据的长度
        tcp_client.send(b'ready')
    
        length = length.decode('utf-8')
    
        recv_size = 0 # 设定一个标识,以判断数据到底是否接收完毕
        recv_msg = b''
    
        while recv_size < length: 
            recv_msg += tcp_client.recv(buffer_size) # 将每次接收到的数据合并
            recv_size = len(recv_msg) 
    
        print('命令的执行结果是', recv_msg.decode('gbk'))
        
    tcp_client.close()

    3.8 高配版解决粘包问题

  • 相关阅读:
    29 顺时针打印矩阵(四-画图让抽象问题形象化)
    27 二叉树镜像(四-画图让抽象问题形象化)
    java的4种代码块
    Eclipse中连接Sql Sever2008 -----转自Yogurshine
    java之HashMap的遍历Iterator
    java之插入排序
    java之选择排序
    java之冒泡排序
    java之快速排序
    java之折半查找
  • 原文地址:https://www.cnblogs.com/hgzero/p/13501849.html
Copyright © 2020-2023  润新知