• python socket相关


    套接字的工作流程(基于TCP和 UDP两个协议)

    TCP和UDP对比

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

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

    TCP协议下的socket

    个生活中的场景。你要打电话给一个朋友,先拨号,朋友听到电话铃声后提起电话,这时你和你的朋友就建立起了连接,就可以讲话了。等交流结束,挂断电话结束此次交谈。 生活中的场景就解释了这工作原理。

    【连接过程总结】

    1)可以看到,只有当server端listen之后,client端调用connect才能成功,否则就会返回RST响应拒绝连接

    2)只有当accept后,client和server才能调用recv和send等io操作

    3)socket API调用错误不会导致client出现SYN_SENT状态,那么只能是网络设备丢包(路由器、防火墙)才会导致SYNC_SENT状态

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

    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)
    服务端套接字函数
    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()        创建一个与该套接字相关的文件
    

      第一版,单个客户端与服务端通信(low版)

    # 网络通信与打电话(诺基亚)是一样的。
    
    import socket
    
    phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)  # 买电话
    
    phone.bind(('127.0.0.1',8080))  # 0 ~ 65535  1024之前系统分配好的端口 绑定电话卡
    
    phone.listen(5)  # 同一时刻有5个请求,但是可以有N多个链接。 开机。
    
    
    conn, client_addr = phone.accept()  # 接电话
    print(conn, client_addr, sep='
    ')
    
    from_client_data = conn.recv(1024)  # 一次接收的最大限制  bytes
    print(from_client_data.decode('utf-8'))
    
    conn.send(from_client_data.upper())
    
    conn.close()  # 挂电话
    
    phone.close() # 关机
    
    服务端
    

      

    import socket
    
    phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)  # 买电话
    
    phone.connect(('127.0.0.1',8080))  # 与客户端建立连接, 拨号
    
    phone.send('hello'.encode('utf-8'))
    
    from_server_data = phone.recv(1024)
    
    print(from_server_data)
    
    phone.close()  # 挂电话
    
    客户端
    

      第二版,通信循环

    import socket
    
    phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    
    phone.bind(('127.0.0.1',8080))
    
    phone.listen(5)
    
    
    conn, client_addr = phone.accept()
    print(conn, client_addr, sep='
    ')
    
    while 1:  # 循环收发消息
        try:
            from_client_data = conn.recv(1024)
            print(from_client_data.decode('utf-8'))
        
            conn.send(from_client_data + b'SB')
        
        except ConnectionResetError:
            break
    
    conn.close()
    phone.close()
    
    服务端
    

      

    import socket
    
    phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)  # 买电话
    
    phone.connect(('127.0.0.1',8080))  # 与客户端建立连接, 拨号
    
    
    while 1:  # 循环收发消息
        client_data = input('>>>')
        phone.send(client_data.encode('utf-8'))
        
        from_server_data = phone.recv(1024)
        
        print(from_server_data.decode('utf-8'))
    
    phone.close()  # 挂电话
    
    客户端
    

      第三版, 通信,连接循环

    import socket
    
    phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    
    phone.bind(('127.0.0.1',8080))
    
    phone.listen(5)
    
    while 1 : # 循环连接客户端
        conn, client_addr = phone.accept()
        print(client_addr)
        
        while 1:
            try:
                from_client_data = conn.recv(1024)
                print(from_client_data.decode('utf-8'))
            
                conn.send(from_client_data + b'SB')
            
            except ConnectionResetError:
                break
    
    conn.close()
    phone.close()
    
    服务端
    

      

    import socket
    
    phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)  # 买电话
    
    phone.connect(('127.0.0.1',8080))  # 与客户端建立连接, 拨号
    
    
    while 1:
        client_data = input('>>>')
        phone.send(client_data.encode('utf-8'))
        
        from_server_data = phone.recv(1024)
        
        print(from_server_data.decode('utf-8'))
    
    phone.close()  # 挂电话
    
    客户端
    

      远程执行命令的示例:

    import socket
    import subprocess
    
    phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    
    phone.bind(('127.0.0.1',8080))
    
    phone.listen(5)
    
    while 1 : # 循环连接客户端
        conn, client_addr = phone.accept()
        print(client_addr)
        
        while 1:
            try:
                cmd = conn.recv(1024)
                ret = subprocess.Popen(cmd.decode('utf-8'),shell=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE)
                correct_msg = ret.stdout.read()
                error_msg = ret.stderr.read()
                conn.send(correct_msg + error_msg)
            except ConnectionResetError:
                break
    
    conn.close()
    phone.close()
    
    服务端
    

      

    import socket
    
    phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)  # 买电话
    
    phone.connect(('127.0.0.1',8080))  # 与客户端建立连接, 拨号
    
    
    while 1:
        cmd = input('>>>')
        phone.send(cmd.encode('utf-8'))
        
        from_server_data = phone.recv(1024)
        
        print(from_server_data.decode('gbk'))
    
    phone.close()  # 挂电话
    
    客户端
    

      

    粘包

    讲粘包之前先看看socket缓冲区的问题:

    每个 socket 被创建后,都会分配两个缓冲区,输入缓冲区和输出缓冲区。

    write()/send() 并不立即向网络中传输数据,而是先将数据写入缓冲区中,再由TCP协议将数据从缓冲区发送到目标机器。一旦将数据写入到缓冲区,函数就可以成功返回,不管它们有没有到达目标机器,也不管它们何时被发送到网络,这些都是TCP协议负责的事情。

    TCP协议独立于 write()/send() 函数,数据有可能刚被写入缓冲区就发送到网络,也可能在缓冲区中不断积压,多次写入的数据被一次性发送到网络,这取决于当时的网络情况、当前线程是否空闲等诸多因素,不由程序员控制。

    read()/recv() 函数也是如此,也从输入缓冲区中读取数据,而不是直接从网络中读取。

    这些I/O缓冲区特性可整理如下:

    1.I/O缓冲区在每个TCP套接字中单独存在;
    2.I/O缓冲区在创建套接字时自动生成;
    3.即使关闭套接字也会继续传送输出缓冲区中遗留的数据;
    4.关闭套接字将丢失输入缓冲区中的数据。

    输入输出缓冲区的默认大小一般都是 8K,可以通过 getsockopt() 函数获取:

    1.unsigned optVal;
    2.int optLen = sizeof(int);
    3.getsockopt(servSock, SOL_SOCKET, SO_SNDBUF,(char*)&optVal, &optLen);
    4.printf("Buffer length: %d ", optVal);

    socket缓冲区解释

    socket缓存区的详细解释


    复制代码
    每个 socket 被创建后,都会分配两个缓冲区,输入缓冲区和输出缓冲区。
    
    write()/send() 并不立即向网络中传输数据,而是先将数据写入缓冲区中,再由TCP协议将数据从缓冲区发送到目标机器。一旦将数据写入到缓冲区,函数就可以成功返回,不管它们有没有到达目标机器,也不管它们何时被发送到网络,这些都是TCP协议负责的事情。
    
    TCP协议独立于 write()/send() 函数,数据有可能刚被写入缓冲区就发送到网络,也可能在缓冲区中不断积压,多次写入的数据被一次性发送到网络,这取决于当时的网络情况、当前线程是否空闲等诸多因素,不由程序员控制。
    
    read()/recv() 函数也是如此,也从输入缓冲区中读取数据,而不是直接从网络中读取。
    
    这些I/O缓冲区特性可整理如下:
    
    1.I/O缓冲区在每个TCP套接字中单独存在;
    2.I/O缓冲区在创建套接字时自动生成;
    3.即使关闭套接字也会继续传送输出缓冲区中遗留的数据;
    4.关闭套接字将丢失输入缓冲区中的数据。
    
    输入输出缓冲区的默认大小一般都是 8K,可以通过 getsockopt() 函数获取:
    
    1.unsigned optVal;
    2.int optLen = sizeof(int);
    3.getsockopt(servSock, SOL_SOCKET, SO_SNDBUF,(char*)&optVal, &optLen);
    4.printf("Buffer length: %d
    ", optVal);
    
    socket缓冲区解释
    复制代码

    须知:只有TCP有粘包现象,UDP永远不会粘包!

    发送端可以是一K一K地发送数据,而接收端的应用程序可以两K两K地提走数据,当然也有可能一次提走3K或6K数据,或者一次只提走几个字节的数据,也就是说,应用程序所看到的数据是一个整体,或说是一个流(stream),一条消息有多少字节对应用程序是不可见的,因此TCP协议是面向流的协议,这也是容易出现粘包问题的原因。而UDP是面向消息的协议,每个UDP段都是一条消息,应用程序必须以消息为单位提取数据,不能一次提取任意字节的数据,这一点和TCP是很不同的。怎样定义消息呢?可以认为对方一次性write/send的数据为一个消息,需要明白的是当对方send一条信息的时候,无论底层怎样分段分片,TCP协议层会把构成整条消息的数据段排序完成后才呈现在内核缓冲区。

    例如基于tcp的套接字客户端往服务端上传文件,发送时文件内容是按照一段一段的字节流发送的,在接收方看了,根本不知道该文件的字节流从何处开始,在何处结束

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

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

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

    tcp的协议数据不会丢,没有收完包,下次接收,会继续上次继续接收,己端总是在收到ack时才会清除缓冲区内容。数据是可靠的,但是会粘包。

    具体原因

    两种情况下会发生粘包。

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

    import socket
    import subprocess

    phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

    phone.bind(('127.0.0.1', 8080))

    phone.listen(5)

    while 1: # 循环连接客户端
    conn, client_addr = phone.accept()
    print(client_addr)

    while 1:
    try:
    cmd = conn.recv(1024)
    ret = subprocess.Popen(cmd.decode('utf-8'), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    correct_msg = ret.stdout.read()
    error_msg = ret.stderr.read()
    conn.send(correct_msg + error_msg)
    except ConnectionResetError:
    break

    conn.close()
    phone.close()

    服务端


    复制代码
    import socket
    import subprocess
    
    phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    
    phone.bind(('127.0.0.1', 8080))
    
    phone.listen(5)
    
    while 1:  # 循环连接客户端
        conn, client_addr = phone.accept()
        print(client_addr)
    
        while 1:
            try:
                cmd = conn.recv(1024)
                ret = subprocess.Popen(cmd.decode('utf-8'), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
                correct_msg = ret.stdout.read()
                error_msg = ret.stderr.read()
                conn.send(correct_msg + error_msg)
            except ConnectionResetError:
                break
    
    conn.close()
    phone.close()
    复制代码

    import socket

    phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM) # 买电话

    phone.connect(('127.0.0.1',8080)) # 与客户端建立连接, 拨号


    while 1:
    cmd = input('>>>')
    phone.send(cmd.encode('utf-8'))

    from_server_data = phone.recv(1024)

    print(from_server_data.decode('gbk'))

    phone.close()

    # 由于客户端发的命令获取的结果大小已经超过1024,那么下次在输入命令,会继续取上次残留到缓存区的数据。

    客户端

    复制代码
    import socket
    
    phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)  # 买电话
    
    phone.connect(('127.0.0.1',8080))  # 与客户端建立连接, 拨号
    
    
    while 1:
        cmd = input('>>>')
        phone.send(cmd.encode('utf-8'))
    
        from_server_data = phone.recv(1024)
    
        print(from_server_data.decode('gbk'))
    
    phone.close() 
    
    # 由于客户端发的命令获取的结果大小已经超过1024,那么下次在输入命令,会继续取上次残留到缓存区的数据。
    复制代码

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

    import socket


    phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

    phone.bind(('127.0.0.1', 8080))

    phone.listen(5)

    conn, client_addr = phone.accept()

    frist_data = conn.recv(1024)
    print('1:',frist_data.decode('utf-8')) # 1: helloworld
    second_data = conn.recv(1024)
    print('2:',second_data.decode('utf-8'))


    conn.close()
    phone.close()

    服务端

    复制代码
    import socket
    
    
    phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    
    phone.bind(('127.0.0.1', 8080))
    
    phone.listen(5)
    
    conn, client_addr = phone.accept()
    
    frist_data = conn.recv(1024)
    print('1:',frist_data.decode('utf-8'))  # 1: helloworld
    second_data = conn.recv(1024)
    print('2:',second_data.decode('utf-8'))
    
    
    conn.close()
    phone.close()
    复制代码

    import socket

    phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

    phone.connect(('127.0.0.1', 8080))

    phone.send(b'hello')
    phone.send(b'world')

    phone.close()

    # 两次返送信息时间间隔太短,数据小,造成服务端一次收取

    客户端

    复制代码
    import socket
    
    phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)  
    
    phone.connect(('127.0.0.1', 8080)) 
    
    phone.send(b'hello')
    phone.send(b'world')
    
    phone.close()  
    
    # 两次返送信息时间间隔太短,数据小,造成服务端一次收取
  • 相关阅读:
    rabbitmq延迟队列相关
    redis发布/订阅模式
    flask中的blueprint
    Maven学习总结(五)——聚合与继承
    Maven学习总结(四)——Maven核心概念--转载
    Maven学习总结(四)——Maven核心概念——转载
    Maven学习总结(三)——使用Maven构建项目
    Maven学习总结(二)——Maven项目构建过程练习_转载
    使用Maven编译项目遇到——“maven编码gbk的不可映射字符”解决办法 ——转载
    Maven学习总结(一)——Maven入门——转载
  • 原文地址:https://www.cnblogs.com/ellisonzhang/p/10406576.html
Copyright © 2020-2023  润新知