网络编程3_socket.粘包

一. tcp下的socket
    1. 正常的tcp下的socket
    (1). server端:
    import socket
    server = socket.socket()
    socket.bind(("127.0.0.1", 8001))        # 把地址和端口绑定到套接字
    socket.listen        # 监听连接
    conn, addr = server.accept()        # 接受客户端连接
    from_client_msg = conn.recv(1024)        # 接受客户端信息
    print(from_client_msg)    
    conn.send(b"hi")        # 向客户端发送信息
    conn.close()        # 关闭客户端套接字
    server.close()        # 关闭服务端套接字
    (2). client端:
    import socket
    client = socket.socket()        # 创建客户端套接字
    client.connect(("127.0.0.1", 8001))        # 尝试连接服务器
    client.send(b"hello")        # 向服务端发送消息
    from_server_msg = client.recv(1024)        # 接收服务端消息
    print(from_server_msg)
    client.close()        # 关闭客户端套接字
    2. socket绑定ip和端口时可能会出现下面的消息:
    OSError: [Error 48] Address already in use    通常每个套接字地址(协议/网络地址/端口)只允许使用一次,  
    (即一个ip地址和端口在同一个模块下只允许打开一次, 但是在不同的模块下是可以使用同一个地址的)
    解决方法: 
    # 加入一条socket配置, 重用ip和端口
    import socket
    from socket import SOL_SOCKET, SO_REUSEADDR
    
    server = socket.socket()
    server.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
    server.bind(("127.0.0.1", 8900))
    server.listen()
    conn, addr = server.accept()
    from_client_msg = conn.recv(1024)
    print(from_client_msg)
    conn.send(b"hi")
    conn.close()
    server.close()
    注意一点: 用socket进行通信, 必须是一收一发对应好
    3. tcp的长连接, 如何优雅的断开
    (1). 服务端:
    import socket
    server = socket.socket()
    server.bind(("127.0.0.1", 8001))
    server.listen()
    conn, addr = server.accept()
    while 1:
        from_client_msg = conn.recv(1024)
        ret = from_client_msg.decode("utf-8")
        print("客户端说: %s" % ret)
        if ret == "bye":
            break
        to_client_msg = input("请输入想对客户端说的话: ")
        conn.send(to_client_msg.encode("utf-8"))
        if to_client_msg == "bye":
            break
    conn.close()
    server.close()
    (2). 客户端:
    import socket
    client = socket.socket()
    client.connect(("127.0.0.1", 8001))
    while 1:
        to_server_msg = input("请输入想对服务端说的话: ")
        client.send(to_server_msg.encode("utf-8"))
        if to_server_msg == "bye":
            break
        from_sever_msg = client.recv(1024)
        ret = from_sever_msg.decode("utf-8")
        print("服务端说: %s" % ret)
        if ret == "bye":
            break
    client.close()
    打开一个服务端, 打开两个客户端, 你会发现, 第一个客户端可以和服务端收发消息, 但是第二个连接的客户端发消息服务端是收不到的
    原因是: tcp属于长连接, 就是一直占用着这个通道, 由于一直处于占线, 其他的客户端只能等待连接, 除非断开了连接(可以优雅的断开, 如果是强制断开就会报错, 因为服务端的程序还在第一个循环里面)
    强制断开: ConnectionResetError: [WinError 10054] 远程主机强迫关闭了一个现有的连接。
    如何优雅的断开?
    (1). 服务端:
    import socket
    server = socket.socket()
    server.bind(("127.0.0.1", 8001))
    server.listen()
    while 1:
        conn, addr = server.accept()
        while 1:
            from_client_msg = conn.recv(1024)
            ret = from_client_msg.decode("utf-8")
            print("客户端说: %s" % ret)
            if ret == "bye":
                break
            to_client_msg = input("请输入想对客户端说的话: ")
            conn.send(to_client_msg.encode("utf-8"))
            if to_client_msg == "bye":
                break
        conn.close()
    server.close()
二. udp下的socket
    先从服务器端说起。服务器端先初始化Socket,然后与端口绑定(bind),recvform接收消息,这个消息有两项,消息内容和对方客户端的地址,然后回复消息时也要带着你收到的这个客户端的地址,发送回去,最后关闭连接,一次交互结束
    1. 正常的udp下的socket
    (1). server端
    import socket
    # 创建一个udp服务端的套接字 STREAM(流, 即tcp) DGRAM(数据报, 即udp)
    udp_server = socket.socket(type = socket.SOCK_DGRAM)
    udp_server.bind(("127.0.0.1", 8001))
    msg, addr = udp_server.recvfrom(1024)
    print(msg.decode("utf-8"))
    udp_server.sendto(b"hi", addr)
    udp_server.close()
    (2). client端
    import socket
    ip_port = ("127.0.0.1", 8001)
    udp_client = socket.socket(type = socket.SOCK_DGRAM)
    udp_client.sendto(b"helli", ip_port)
    back_msg, addr = udp_client.recvfrom(1024)
    print(back_msg.decode("utf-8"), addr)
    2. 类似qq聊天代码实现: 
    (1). 服务端:
    import socket
    ip_port = ("127.0.0.1", 8081)
    udp_server = socket.socket(socket.AF_INET ,socket.SOCK_DGRAM)
    udp_server.bind(ip_port)
    while 1:
        qq_msg, addr = udp_server.recvfrom(1024)        # 阻塞状态, 等待接收消息
        print("来自[%s:%s]的一条消息: %s" % (addr[0], addr[1], qq_msg.decode("utf-8")))
        back_msg = input("请输入要回复的消息: ")
        udp_server.sendto(back_msg.encode("utf-8"), addr)
    (2). 客户端:
    import socket
    BUFFSIZE = 1024
    udp_client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    qq_name_dic = {
        "taibai": ("127.0.0.1", 8081),
        "alex": ("127.0.0.1", 8081),
        "wusir": ("127.0.0.1", 8081),
        "nvshen": ("127.0.0.1", 8081)
    }
    while 1:
        print("聊天对象有:")
        for k in qq_name_dic:
            print("\t%s" % k)
        qq_name = input("请选择聊天对象:").strip()
        while 1:
            msg = input("请输入消息, 回车发送, 输入Q结束聊天: ").strip()
            if msg.upper() == "Q":
                break
            if not msg or not qq_name or qq_name not in qq_name_dic:
                continue
            udp_client.sendto(msg.encode("utf-8"), qq_name_dic[qq_name])
            back_msg, addr = udp_client.recvfrom(BUFFSIZE)
            print("来自[%s:%s]的一条消息: %s" % (addr[0], addr[1], back_msg.decode("utf-8")))
三. socket类型和方法:
    1. socket类型:
    socket.AF_UNIX: 只能够用于单一的unix系统间的通信
    socket.AF_INET: 服务器之间网络通信, ipv4
    socket.AT_INET6: ipv6
    socket.SOCK_STREAM: 流式socket, for tcp
    socket.SOCK_DGRAM: 数据报式socket, for udp
    socket.SOCK_RAW: 原始套接字, 普通的套接字无法处理icmp, igmp等网络报文, 而socket_raw可以, 其次, SOCK_RAW也可以处理特殊的ipv4报文, 此外, 利用原始套接字, 可以通过IP_HDRINCL套接字选项由用户构造ip头
    socket.SOCK_SEQPACKET: 可靠地连续数据报服务
    创建TCP Socket: s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    创建UDP Socket: s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    2. socket方法: 
    (1). 服务端方法:
    s.bind(addr): 将套接字绑定到地址, 在AF_INET下, 以元祖的形式表示地址
    s.listen(backlog): 开始监听tcp连接, backlog指定在拒绝连接之前, 操作系统可以挂起的最大连接数量, 该值至少为1, 大部分应用程序设置为5就可以了
    s.accept(): 接受tcp连接并返回(conn, addr), 其中conn是新的套接字对象, 可以用来接收和发送数据, addr是连接客户端的地址
    (2). 客户端方法:
    s.connect(addr): 连接到addr处的套接字, 一般addr的格式是元祖, 如果连接出错, 返回socket.errer错误
    s.connect_ex(addr): 功能与connect()相同, 但是成功返回(), 失败返回error的值
    (3). 公共socket方法:
    s.recv(bufsize[, flag]): 接受tcp套接字的数据, 数据以字符串形式返回, bufsize指定要接收的最大数据量, flag提供有关消息的其他信息, 通常可以忽略
    s.send(string[, flag]): 发送tcp数据, 将string中的数据发送到连接的套接字, 返回值是要发送的直接数量, 该数量可能小于string的字节大小
    s.sendall(string[, flag]): 完整发送tcp数据, 将string中的数据发送到连接的套接字, 但是在返回之前会尝试发送所有的数据, 成功返回NONE, 失败则抛出异常
    s.recvfrom(bufsize[, flag]): 接收udp套接字的数据, 与recv()类似, 但返回值是(data, addr), 其中data是包含接收数据的字符串, addr是发送数据的套接字地址
    s.sendto(string[, flag], addr): 发送udp数据, 将数据发送到套接字, addr是形式为(ipaddr, port)元祖, 指定远程地址, 返回值是发送的字节数
    s.close(): 关闭套接字
    s.getpeername(): 返回套接字的远程地址, 返回值通常是元祖
    s.getsockname(): 返回套接字自己的地址, 通常是一个元祖     
    s.setsockopt(level, optname, value): 设置给定套接字选项的值
    s.getsockopt(level, optname[.buflen]): 返回套接字选项的值
    s.settimeout(timeout): 设置套接字操作的超时期, timeout是一个浮点数, 单位是秒, 值为None表示没有超时期, 一般, 超时期应该在刚创建套接字时设置, 因为他们可能用于连接的操作
    s.gettimeout(): 返回当前超时期的值, 单位是秒, 如果没有设置超时期, 则返回None
    s.fileno(): 返回套接字的文件描述符
    s.setblocking(flag): 如果flag为0, 则将套接字设置为非阻塞模式, 否则将套接字设为阻塞模式(默认值), 非阻塞模式下, 如果调用recv()没有发现任何数据, 或send()调用无法立即发送数据, 那么将引起socket.error异常
    s.makefile(): 创建一个与该套接字相关联的文件  
四. 粘包
    1. 缓冲区
    每个socket被创建后, 都会分配两个缓冲区, 输入缓冲区和输出缓冲区
    write()/seng() 并不立即向网络中传输数据, 而是先将数据写入缓冲区, 再由tcp协议将数据从缓冲区发送到目标机器, 一旦将数据写入到缓冲区, 函数就可以成功返回, 不管他们有没有到大目标机器, 也不管他们何时被发送到网络,这些都是tcp协议负责的事情
    tcp协议独立于write()/send()函数, 数据有可能刚被写入缓冲区就发送到网络, 也可能在缓冲区中不断积压, 多次写入的数据被一次性发送到网络, 这取决于当时的网络情况, 当前线程是够空闲等诸多因素, 不由程序眼控制
    read()/recv() 函数意思如此, 也从缓冲区读取数据, 而不是直接从网络中获取
    这些I/O缓冲区特性可整理如下:
    (1). I/O缓冲区在每个tcp套接字中单独存在
    (2). I/O缓冲区在创建套接字时自动生成
    (3). 即使关闭套接字也会继续传送输出缓冲区中遗留的数据
    (4). 关闭套接字将丢失输入缓冲区中的数据
    输入输出缓冲区额默认大小一般都是8k, 可以通过getsockopt() 函数获取: 
    https://www.cnblogs.com/ouyangyixuan/p/5894542.html
    import socket
    server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    bsize = server.getsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF)
    print(bsize)
    # 65536
    2. windows下cmd窗口调用系统指令
    在cmd命令行输入dir(查看当前文件夹下的所有文件和文件夹)和ipconfig(查看当前电脑的网络信息)
    借助系统指令和指令输出的结果来模拟一下粘包现象
    3. 粘包现象(两种)
    MTU(Maximum Transmission Unit): 意思是网上传输的最大数据包, MTU的单位是字节, 大部分网络设备的MTU都是1500个字节, 也就是1500KB, 如果本机一次需要发送的数据比网关的MTU大, 打的数据包就会被拆开来传送, 这样会产生很多数据包碎片, 增加丢包率, 降低网络速度
    超出缓冲区大小会报错, 或者udp协议的时候, 你的一个数据包的大小超过了你一次recv能接受的大小, 也会报错, tcp不会, 但是超出缓冲区大小的时候, 肯定会报错
    OSError: [WinError 10040] 一个在数据包套接字上发送的消息大于内部消息缓冲区或其他一些网络限制, 或该用户用于接收数据报的缓冲区比数据报小
    (1). 接收方没有及时接受缓冲区的包, 造成多个包接收 (客户端发送了一段数据, 服务端只收了一小部分, 服务端下次再收的时候还是从缓冲区拿上次遗留的数据, 产生粘包)
    (2). 发送数据时间间隔很短, 数据也很小, 会合在一起, 产生粘包
    4. 模拟粘包现象
    subprocess模块
    import subprocess
    cmd = input("请输入指令>>>")
    ret = subprocess.Popen(
        cmd,                            # 字符串指令, "dir", "ipconfig"
        shell = True,                    # 使用shell就相当于使用cmd窗口
        stderr = subprocess.PIPE,        # 标准错误输出, 拿到错误指令的报错信息
        stdout = subprocess.PIPE,        # 标准输出, 拿到正确指令的输出结果
    )    
    print(ret.stdout.read().decode("gbk"))
    print(ret.stderr.read().decode("gbk"))
    注意: 如果是windows, 那么ret.stdout.read()读出的就是gbk编码的 在接收端需要用gbk解码且只能从管道里读一次结果, PIPE是管道
    (1). tcp粘包演示一:
    服务端:
    from socket import *
    import subprocess
    ip_port = ("127.0.0.1", 8001)
    BUFSIZE = 1024
    server = socket(AF_INET, SOCK_STREAM)
    server.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
    server.bind(ip_port)
    server.listen(5)
    while 1:
        conn, addr = server.accept()
        print("客户端>>>", addr)
        while 1:
            cmd = conn.recv(BUFSIZE)
            if len(cmd) == 0:
                break
            ret = subprocess.Popen(
                cmd.decode("utf-8"),
                shell = True,
                stdout = subprocess.PIPE,
                stdin = subprocess.PIPE,
                stderr =subprocess.PIPE
            )
            stderr = ret.stderr.read()
            stdout = ret.stdout.read()
            conn.send(stderr)
            conn.send(stdout)
    客户端: 
    import socket
    ip_port = ("127.0.0.1", 8001)
    size = 1024
    client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    client.connect(ip_port)
    while 1:
        msg = input("请输入要执行的指令, Q退出: ").strip()
        if len(msg) == 0:
            continue
        if msg.upper() == "Q":
            break
        client.send(msg.encode("utf-8"))
        from_server_msg = client.recv(1025)
        print("接收的返回结果长度为: " , len(from_server_msg))
        # windows返回的内容需要用gbk来解码, 因为windows系统的默认编码是gbk
        print(from_server_msg.decode("gbk"))
    (2). tcp粘包演示二:
    服务端:
    import socket
    server = socket.socket()
    server.bind(("127.0.0.1", 8001))
    server.listen(5)
    conn, addr = server.accept()
    from_client_msg1 = conn.recv(1024)
    from_client_msg2 = conn.recv(1024)
    print(from_client_msg1.decode("utf-8"))
    print(from_client_msg2.decode("utf-8"))
    conn.close()
    server.close()
    客户端:
    import socket
    client = socket.socket()
    client.connect(("127.0.0.1", 8001))
    client.send("hello".encode("utf-8"))
    client.send("hi".encode("utf-8"))
    client.close()
    (3). udp是面向包的,所以udp不存在粘包
    因为udp是面向报文的, 意思是每个消息是一个包, 接收端设置接收大小的时候, 必须要比你发的这个包大, 不然一次接收不了就会报错, 而tcp不会报错, 这也是为什么udp会丢包的原因
    发送端可以是一K一K地发送数据,而接收端的应用程序可以两K两K地提走数据,当然也有可能一次提走3K或6K数据,或者一次只提走几个字节的数据,也就是说,应用程序所看到的数据是一个整体,或说是一个流(stream),一条消息有多少字节对应用程序是不可见的,因此TCP协议是面向流的协议,这也是容易出现粘包问题的原因。而UDP是面向消息的协议,每个UDP段都是一条消息,应用程序必须以消息为单位提取数据,不能一次提取任意字节的数据,这一点和TCP是很不同的。怎样定义消息呢?可以认为对方一次性write/send的数据为一个消息,需要明白的是当对方send一条信息的时候,无论底层怎样分段分片,TCP协议层会把构成整条消息的数据段排序完成后才呈现在内核缓冲区。
    例如基于tcp的套接字客户端往服务端上传文件,发送时文件内容是按照一段一段的字节流发送的,在接收方看了,根本不知道该文件的字节流从何处开始,在何处结束
    所谓粘包问题主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的。
    此外,发送方引起的粘包是由TCP协议本身造成的,TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一个TCP段。若连续几次需要send的数据都很少,通常TCP会根据优化算法把这些数据合成一个TCP段后一次发送出去,这样接收方就收到了粘包数据。
        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时才会清除缓冲区内容。数据是可靠的,但是会粘包。
五. 粘包的解决方案
    粘包的原因: 接收方不知道消息之间的界限, 不知道一次提取多少个字节所造成的
    1. 解决方案一: 接收端不知道发送端将要传送的字节流的长度, 所以解决粘包的方法就是围绕, 如何让发送的端在发送数据前, 把自己将要发送的字节流总大小让接收端知晓, 然后接收端发一个确认消息给发送端, 发送端再发送过来后面真是的内容, 接收端再来一个死循环接收完所有的数据.
    (1). 服务端:
    import socket
    import subprocess
    server = socket.socket()
    server.bind(("127.0.0.1", 8001))
    server.listen(5)
    conn, addr = server.accept()
    print("客户端地址:", addr)
    while 1:
        # 接收客户端的指令
        client_inst = conn.recv(1024)
        if client_inst.decode("utf-8").upper() == "Q":
            break
        res = subprocess.Popen(
            client_inst.decode("utf-8"),
            shell = True,
            stderr = subprocess.PIPE,
            stdout = subprocess.PIPE
        )
        err = res.stderr.read()
        if err:
            ret = err
        else:
            ret = res.stdout.read()
        data_length = len(ret)
        # 给客户端发送指令结果的长度
        conn.send(str(data_length).encode("utf-8"))
        # 接收客户端的回复
        resp = conn.recv(1024)
        if resp.decode("utf-8") == "ok":
            # 给客户端发送指令结果
            conn.sendall(ret)
    conn.close()
    server.close()
    (2). 客户端:
    import socket
    client = socket.socket()
    client.connect(("127.0.0.1", 8001))
    while 1:
        inst = input("请输入要执行的操作指令,Q退出:").strip()
        if inst.upper() == "Q":
            client.send(inst.encode("utf-8"))
            break
        else:
            # 给服务端发送指令
            client.send(inst.encode("utf-8"))
            # 接收服务端发来的指令结果长度
            data_l = client.recv(1024)
            data_length = int(data_l.decode("utf-8"))
            print("操作指令结果的长度为: ", data_length)
            # 向服务端发送确认信息
            client.send("ok".encode("utf-8"))
            # 接收服务端发来的指令结果
            data = client.recv(data_length)
            print("操作指令的结果为: \n", data.decode("gbk"))
    client.close()
    2. 解决方案二: 
    通过struct模块将需要发送的内容的长度进行打包, 打包成一个4个字节长度的数据发送到客户端, 客户端只要取出前4个字节, 然后对这4个字节的数据尽进行解包, 根据拿到的长度来继续接受实际发送数据的长度
    struct模块的作用是对python基本类型值与用python字符串格式表示的C struct类型间的转换
    这里主要用到struct模块中的两个函数:
    pack(): 将num转换成二进制
        bytes = struct.pack("i", num)
    unpack(): 将bytes转换成int类型, 返回结果是元祖
        a, = struct.unpack("i", bytes)

posted @ 2019-01-07 19:19  lokichoggio  阅读(155)  评论(0编辑  收藏  举报