python socket编程(tcp粘包问题处理)

本文围绕Python Socket编程展开,介绍了套接字工作流程,包括服务器端初始化、绑定端口、监听、接受连接,客户端连接、发送请求等步骤。还阐述了TCP粘包问题产生的原因,如TCP面向流协议及发送数据特点,并提出在TCP包头上添加4字节固定长度,用struct模块解决粘包问题。

python socket编程(tcp粘包问题处理)

1.套接字工作流程

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

Socket Families(地址簇)

socket.AF_UNIX unix本机进程间通信 

socket.AF_INET IPV4 

socket.AF_INET6  IPV6

Socket Types

socket.SOCK_STREAM  #for tcp

socket.SOCK_DGRAM   #for udp

Socket 参数

s.bind(address)     绑定(host,port)

s.listen(backlog)   开始监听传入连接。backlog指定在拒绝连接之前,可以挂起的最大连接数量

s.accept()             接受连接并返回(conn,address),其中conn是新的套接字对象,可以用来接收和发送数据。address是连接客户端的地址。

s.connect(address)      address的格式为元组(hostname,port)

s.setblocking(bool)    是否阻塞(默认True),如果设置False,那么accept和recv时一旦无数据,则报错。

s.close()                  关闭套接字

s.recv(bufsize)        bufsize指定最多可以接收的数量

s.send(string)          将string中的数据发送到连接的套接字。返回值是要发送的字节数量,该数量可能小于string的字节大小。即:可能未将指定内容全部发送。

s.sendall(string)       将string中的数据发送到连接的套接字,但在返回之前会尝试发送所有数据。成功返回None,失败则抛出异常。

                                 内部通过递归调用send,将所有内容发送出去。

s.settimeout(timeout)  设置套接字操作的超时期,timeout是一个浮点数,单位是秒。值为None表示没有超时期。一般,超时期应该在

                                    刚创建套接字时设置,因为它们可能用于连接的操作(如 client 连接最多等待5s )

s.getpeername()         返回连接套接字的远程地址。返回值通常是元组(ipaddr,port)

服务端

创建socket对象
绑定IP和端口 绑定 bind()
开始监听链接 监听 listen()
阻塞 , 等待客户端成功连接 阻塞 accept()
接收请求数据 接收 recv()
处理并发送请求数据 发送 send()
关闭 close()



客户端

创建socket对象
连接服务端 , 按照IP和端口连接 连接 connet()
发送请求数据 发送 send()
接收请求数据 接收 recv()
关闭 close()
#服务端
from socket import  *
server_socket=socket(AF_INET,SOCK_STREAM)
server_socket.bind(('127.0.0.1',8003))
server_socket.listen(5)
con,addr=server_socket.accept()
while True:
    data=con.recv(1024)
    print('..........',data)
    if not data:continue
    data=data.decode('utf-8')
    print(data)
    con.send(data.upper().encode('utf-8'))
con.close()

#客户端
from socket import  *
socket_client=socket(AF_INET,SOCK_STREAM)
socket_client.connect(('127.0.0.1',8003))
while True:
    msg=raw_input('>>> ')
    socket_client.send(msg.encode('utf-8'))
    data=socket_client.recv(1024)
    if not data:continue
    print(data.decode('utf-8'))

tcp的粘包问题产生的原因:1、 tcp 是面向流的协议,不清楚对端发送了多少个字节,从自己的缓存区取数据的时候会产生粘包问题;2、tcp在发送数据少且间隔时间短的数据时,会将几条和并一起发送。

解决粘包问题

客户端
from socket import  *
socket_client=socket(AF_INET,SOCK_STREAM)
socket_client.connect(('127.0.0.1',8003))
while True:
    cmd=raw_input('>>> ')
    socket_client.send(cmd.encode('utf-8'))
    data=socket_client.recv(1024)
    if not data:break
    size=int(data.decode('utf-8'))              
    flag='ready'
    socket_client.send(flag.encode('utf-8'))
    recv_size=0
    recv_msg=b''
    while recv_size<size:
        recv_msg +=socket_client.recv(1024)
        recv_size=len(recv_msg)


    print(recv_msg.decode('gbk'))



服务端
from socket import  *
import subprocess
server_socket=socket(AF_INET,SOCK_STREAM)
server_socket.bind(('127.0.0.1',8003))
server_socket.listen(5)

while True:
    try:
        con,addr=server_socket.accept()
        while True:
            data=con.recv(1024)
            if not data:break
            cmd=data.decode('utf-8')
            print(cmd)
            p=subprocess.Popen(cmd,shell=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE)
            res_err=p.stderr.read()
            print(res_err)
            res = p.stdout.read()
            if res_err:
                res=res_err
            elif not res_err and not res:
                print('........')
                res='run success'.encode('gbk')
            size=len(res)
            con.send(str(size).encode('utf-8'))
            data=con.recv(1024)
            if data.decode('utf-8')=='ready':
                con.send(res)
        con.close()
    except Exception  as e:
         print(e)
         continue

2.在tcp包头上添加4个字节的固定长度来表示包内容的固定长度,使用struct 模块

客户端
rom socket import  *
import struct
socket_client=socket(AF_INET,SOCK_STREAM)
socket_client.connect(('127.0.0.1',8003))
while True:
    cmd=raw_input('>>> ')
    socket_client.send(cmd.encode('utf-8'))
    data=socket_client.recv(4)
    if not data:break
    size=struct.unpack('i',data)[0]
    recv_size=0
    recv_msg=b''
    while recv_size<size:
        recv_msg +=socket_client.recv(1024)
        recv_size=len(recv_msg)
    print(recv_msg.decode('gbk'))


服务端
from socket import  *
import subprocess
import struct
server_socket=socket(AF_INET,SOCK_STREAM)
server_socket.bind(('127.0.0.1',8003))
server_socket.listen(5)

while True:
    try:
        con,addr=server_socket.accept()
        while True:
            data=con.recv(1024)
            if not data:break
            cmd=data.decode('utf-8')
            print(cmd)
            p=subprocess.Popen(cmd,shell=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE)
            res_err=p.stderr.read()
            print(res_err)
            res = p.stdout.read()
            if res_err:
                res=res_err
            elif not res_err and not res:
                res='run success'.encode('gbk')
            size=len(res)
            print(size)
            length=struct.pack('i',size)
            con.send(length)
            con.send(res)
        con.close()
    except Exception  as e:
         print(e)
         continue

ftp客户端

#_*_coding:utf-8_*_
__author__ = 'jieli'
 
 
import socket
import os
class FtpClient(object):
    def __init__(self,host,port ):
        self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.sock.connect((host, port)) #连接服务器
    def start(self): #实例化客户端类后,需要调用此方法启动客户端
        self.interactive()  #跟用户的交互都在这个方法里
         
         
    def interactive(self):
        while True:
            user_input = raw_input(">>:").strip()
            if len(user_input) == 0:continue
            user_input = user_input.split() #用户输入的指令进行拆分,第一个参数是指要进行什么动作,比如get remote_filename
 
            if hasattr(self,user_input[0]):#判断类中是否有get或其它输入的方法
                func = getattr(self,user_input[0]) #通过字符串获取类中对应方法的内存对象
                func(user_input) #调用此内存对象
 
            else: 
                print "\033[31;1mWrong cmd usage\033[0m"
 
    def get(self,msg): #从服务器端下载文件
        print '--get func---',msg
        if len(msg) ==2 :
            file_name = msg[1]
            instruction = "FileTransfer|get|%s" % file_name #告诉服务器端要下载什么文件
            self.sock.send(instruction)
            feedback = self.sock.recv(100) #等待服务器端的消息确认
            print '-->',feedback
            if feedback.startswith("FileTransfer|get|ready"): #代表服务器上文件存在,并且服务器已经准备好了发送此文件到客户端
                file_size = int(feedback.split("|")[-1]) # 服务器端发回来的确认消息中,最后面一个值是文件大小,必须知道文件大小才知道一共要收多少内容
                self.sock.send("FileTransfer|get|recv_ready") #告诉服务器端已经准备好了接收
                recv_size = 0 # 因为文件可能会比较大,一次收不完,所以要循环收,每收到一次,就计个数
                f = file('client_recv/%s' % os.path.basename(file_name),'wb') #在本地创建一个新文件来存这个要下载的文件内容
                print '--->',file_name
                while not file_size == recv_size:#只要文件总大小和已收到的大小不想等,就代表还没收完
                    if file_size - recv_size>1024:#文件总大下减已收到的大小等于还剩下没收到的大小,如果这个数大于1024,代表一次肯定收不完,那就还得多循环几次
                        data = self.sock.recv(1024) #这次收1024字节,但实际上收到的可能比1024小,所以需要以实际收到的数为准
                        recv_size += len(data) # 已收到的大小加上这一次循环收到的实际大小
                    else:# 如果最后剩下的少于1024,那就一次性把剩下的都收过来
                        data = self.sock.recv(file_size - recv_size) 
                        #recv_size = file_size #不能这么写,因为这一次依然不一定能一次性收完,因为实际收到的数据可能比你规定的数据要少, 所以需要按下面这行的方式写
                        recv_size += (file_size - recv_size) 
                    f.write(data) #收到的内容写入文件
                    print file_size,recv_size
                else:
                    print '---recv file:%s---' % file_name
                    f.close() 
            else:
                print feedback
        else:
            print "\033[31;1mWrong cmd usage\033[0m"
 
    def put(self): 
        pass
    def ls(self):
        pass
    def cd(self):
        pass
    def delete(self):
        pass
 
 
if __name__ == "__main__":
    f = FtpClient('localhost',9002)
    f.start()

ftp服务端

#!/usr/bin/python
# -*- coding: utf-8 -*-
#Author:xianyanghua
# @Time    :2019/7/17 15:34
# @File    : server.py
# @Software: PyCharm
# _*_coding:utf-8_*_

import SocketServer

import os


class MyTCPHandler(SocketServer.BaseRequestHandler):
    def handle(self):
        while True:
            instruction = self.request.recv(1024).strip()  # 接收客户端命令
            if not instruction: break
            instruction = instruction.split('|')  # 将客户端发过来的消息按|来拆分,消息类似这种格式 “FileTransfer|get|file_name”
            if hasattr(self, instruction[0]):  # 判断类中是否有这个方法
                func = getattr(self, instruction[0])  # 获取这个方法的内存对象
                func(instruction)  # 调用此方法

    def FileTransfer(self, msg):  # 负责文件的发送和接收
        print '---filetransfer---', msg
        if msg[1] == 'get':  # 如果客户端发来的指令是get,那就是下载文件
            print "client wants to download file:", msg[2]
            if os.path.isfile(msg[2]):  # 判断客户端发的文件名是否存在并是个文件
                file_size = os.path.getsize(msg[2])  # 获取文件大小
                res = "ready|%s" % file_size  # 把文件大小告诉客户端
            else:
                res = "file doesn't exist"  # 文件也有可能不存在
            send_confirmation = "FileTransfer|get|%s" % res
            self.request.send(send_confirmation)  # 发送确认消息给客户端
            feedback = self.request.recv(
                100)  # 等待客户端确认, 如果这时不等客户端确认就立刻给客户端发文件内容,因为为了减少IO操作,socket发送和接收是有缓冲区的,缓冲区满了才会发送,那上一条消息很有可能会和文件内容的一部分被合并成一条消息发给客户端,这就行成了粘包,所以这里等待客户端的一个确认消息,就把两次发送分开了,不会再有粘包
            if feedback == 'FileTransfer|get|recv_ready':  # 如果客户端说准备好接收了
                f = file(msg[2], 'rb')
                send_size = 0  # 发送的逻辑跟客户端循环接收的逻辑是一样的
                while not file_size == send_size:
                    if file_size - send_size > 1024:
                        data = f.read(1024)
                        send_size += 1024
                    else:  # left data less than 1024
                        data = f.read(file_size - send_size)
                        send_size += (file_size - send_size)
                    self.request.send(data)
                    print file_size, send_size
                else:
                    print '---send file:%s done----' % msg[2]
                    f.close()
        elif msg[1] == 'put':
            pass


if __name__ == '__main__':
    HOST, PORT = "", 9002

    server = SocketServer.ThreadingTCPServer((HOST, PORT), MyTCPHandler)

    server.serve_forever()

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值