Python socket module: Recv() data response cut off

2019-05-29 18:14发布

问题:

Explanation

I'm currently trying to control a smart power strip using a python script. To accomplish this, I'm using a TCP connection with the socket module. Around 75% of the time, I get the response/data I was looking for and everything works perfectly. However, around 25% of the time, the response is cut off at the exact same length, 1024 bytes. This doesn't make any sense to me, as my buffer size is actually set to 2048 bytes. The speed at which I wait in between using recv() doesn't seem to effect/cause this either. Altough TCP is a stream of bytes, is it still possible that this could have to do with packet fragmentation?

Code

Main Code

ip='192.168.0.62'
port=9999

sock_tcp = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock_tcp.connect((ip, port))
sock_tcp.send(encrypt('{"system":{"get_sysinfo":{}}}'))
data = sock_tcp.recv(2048)
sock_tcp.close()
print len(data) #On succesful runs output is 1221, on unsuccesful runs it is 1024
rec = decrypt(data[4:])
print str(rec) #See output below

Encrypt Function

def encrypt(string):
    key = 171
    result = pack('>I', len(string))
    for i in string:
        a = key ^ ord(i)
        key = a
        result += chr(a)
    return result

Decrypt Function

def decrypt(string):
    key = 171
    result = ""
    for i in string:
        a = key ^ ord(i)
        key = ord(i)
        result += chr(a)
    return result

Output

The string itself that I recieve. It's most likeley not relevant, but I thought I would include it anyway. This is value of the variable rec.

Desired and regular output

Full desired output

{"system":{"get_sysinfo":{"sw_ver":"1.0.6 Build 180627 Rel.081000","hw_ver":"1.0","model":"HS300(US)","deviceId":"80067B24A755F99C4D6C1807455E09F91AB7B2AA","oemId":"5C9E6254BEBAED63B2B6102966D24C17","hwId":"34C41AA028022D0CCEA5E678E8547C54","rssi":-60,"longitude_i":-1222955,"latitude_i":379078,"alias":"TP-LINK_Power Strip_4F01","mic_type":"IOT.SMARTPLUGSWITCH","feature":"TIM:ENE","mac":"B0:BE:76:12:4F:01","updating":0,"led_off":0,"children":[{"id":"80067B24A755F99C4D6C1807455E09F91AB7B2AA00","state":0,"alias":"CezHeat","on_time":0,"next_action":{"type":-1}},{"id":"80067B24A755F99C4D6C1807455E09F91AB7B2AA01","state":1,"alias":"CezUVB","on_time":191208,"next_action":{"type":-1}},{"id":"80067B24A755F99C4D6C1807455E09F91AB7B2AA02","state":1,"alias":"CyanHeat","on_time":191208,"next_action":{"type":-1}},{"id":"80067B24A755F99C4D6C1807455E09F91AB7B2AA03","state":1,"alias":"ZanderHeat","on_time":191208,"next_action":{"type":-1}},{"id":"80067B24A755F99C4D6C1807455E09F91AB7B2AA04","state":1,"alias":"CairoHeat","on_time":191208,"next_action":{"type":-1}},{"id":"80067B24A755F99C4D6C1807455E09F91AB7B2AA05","state":1,"alias":"KodaMister","on_time":191208,"next_action":{"type":-1}}],"child_num":6,"err_code":0}}}

Abnormal and rarer output

Cut off output

{"system":{"get_sysinfo":{"sw_ver":"1.0.6 Build 180627 Rel.081000","hw_ver":"1.0","model":"HS300(US)","deviceId":"80067B24A755F99C4D6C1807455E09F91AB7B2AA","oemId":"5C9E6254BEBAED63B2B6102966D24C17","hwId":"34C41AA028022D0CCEA5E678E8547C54","rssi":-59,"longitude_i":-1222955,"latitude_i":379078,"alias":"TP-LINK_Power Strip_4F01","mic_type":"IOT.SMARTPLUGSWITCH","feature":"TIM:ENE","mac":"B0:BE:76:12:4F:01","updating":0,"led_off":0,"children":[{"id":"80067B24A755F99C4D6C1807455E09F91AB7B2AA00","state":0,"alias":"CezHeat","on_time":0,"next_action":{"type":-1}},{"id":"80067B24A755F99C4D6C1807455E09F91AB7B2AA01","state":1,"alias":"CezUVB","on_time":191207,"next_action":{"type":-1}},{"id":"80067B24A755F99C4D6C1807455E09F91AB7B2AA02","state":1,"alias":"CyanHeat","on_time":191207,"next_action":{"type":-1}},{"id":"80067B24A755F99C4D6C1807455E09F91AB7B2AA03","state":1,"alias":"ZanderHeat","on_time":191207,"next_action":{"type":-1}},{"id":"80067B24A755F99C4D6C1807455E09F91AB7B2AA04","state":1,"alias":"CairoHeat","on

Conclusion

If anyone could provide me with a solution or explanation as to why the output/stream gets cut off, it would be much appreciated. I used a lot of the code from this open source module. I'm also looking to understand more of how this all works, so if you could explain a bit more I would really appreciate it.

回答1:

As per the documentation, the bufsize argument only specifies the maximum amount of data to be read:

socket.recv(bufsize[, flags]) Receive data from the socket. The return value is a bytes object representing the data received. The maximum amount of data to be received at once is specified by bufsize. See the Unix manual page recv(2) for the meaning of the optional argument flags; it defaults to zero.

To ensure full data transfer a function like this can be used, which waits for the end of the socket connection (indicated by and empty string returned from recv):

def recv_all(connection):
    """
    Function for all data

    :param connection: socket connection
    :return: received data
    """
    data = list()
    while True:
        data.append(connection.recv(2048))
        if not data[-1]:
            return b''.join(data)

Another example that might fit your application better could be to wait for a fixed message size (1221 as indicated by your question):

def recv_message(connection):
    data = list()
    transferred_bytes= 0
    while transferred_bytes < 1221:
        data.append(connection.recv(min(1221-transferred_bytes, 2048)))
        if not data[-1]:
            raise RuntimeError("socket connection broken")
        transferred_bytes += len(data[-1])
    return b''.join(data)


回答2:

This is only a complement to SimonF's answer. The cause of the problem is indeed that TCP is a stream protocol, so packets can be fragmented or re-assembled at any state: sender TCP/IP stack, network equipments, receiver TCP/IP stack - I include the user layer library in the TCP/IP stack here for simplification.

That is the reason why, you should always use a higher level protocol above TCP to be able to split the stream in sensible messages. Here you could note that the end of a message is '}}}', so you could concatenate the input in a buffer until you find that pattern:

def recv_until(c, guard):
    """Receive data from a socket until guard if found on input"""
    guard_sz = len(guard) - 1
    data = b''
    sz = 0
    while True:
        buffer = c.recv(1024)      # read by chuncks of size 1024 (change value to your needs)
        got = len(buffer)
        data += buffer             # concatenate in buffer
        ix = data.find(guard, sz - guard_sz if sz > guard_sz else 0)    # is guard found?
        if ix != -1:
            return (data[:ix + guard_sz + 1],   # return the message, and what could be behind it
                data[ix + guard_sz + 1:])

        sz += got

The trick is to considere guard_sz byte from the last chunk, in the case where the guard could be split in two chunks.



回答3:

Marco, please use recv_into(buffer[, nbytes[, flags]]) method for the socket.

My example for TCP-microserver:

import socket
import struct

def readReliably(s,n):
    buf = bytearray(n)
    view = memoryview(buf)
    sz = 0
    while sz < n:
        k = s.recv_into(view[sz:],n-sz)
        sz += k
    # print 'readReliably()',sz
    return sz,buf

def writeReliably(s,buf,n):
    sz = 0
    while sz < n:
        k = s.send(buf[sz:],n-sz)
        sz += k
    # obj = s.makefile(mode='w')
    # obj.flush()
    # print 'writeReliably()',sz
    return sz

# Client
host = "127.0.0.1"
port = 23456
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(10)
s.connect((host,port))
# Request
buf = struct.pack("4B",*[0x01,0x02,0x03,0x04])
io.writeReliably(s,buf,4)
# Response
sz,buf = io.readReliably(s,4)
a = struct.unpack("4B",buf)
print repr(a)

# Server
s = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
#s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
#s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
#s.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
s.bind((host,port))
s.listen(10) # unaccepted connections
while True:
  sk,skfrom = s.accept()
  sz,buf = io.readReliably(sk,4)
  a = struct.unpack("4B",buf)
  print repr(a)
  # ...
  io.writeReliably(sk,struct.pack("4B",*[0x01,0x02,0x03,0x04]))