zoukankan      html  css  js  c++  java
  • Python socket编程

    本章内容

      1、Socket简介

      2、Socket远程服务器操作

      3、SocketServer模块

      4、粘包

    Socket简介

      python内有很多针对常见网络协议的库,

      socket通常也称作"套接字",用于描述IP地址和端口,是一个通信链的句柄,应用程序通常通过"套接字"向网络发出请求或者应答网络请求。

      socket起源于Unix,而Unix/Linux基本哲学之一就是“一切皆文件”,对于文件用【打开】【读写】【关闭】模式来操作。socket就是该模式的一个实现,socket即是一种特殊的文件,一些socket函数就是对其进行的操作(读/写IO、打开、关闭)

      socket和file的区别:

    • file模块是针对某个指定文件进行【打开】【读写】【关闭】
    • socket模块是针对 服务器端 和 客户端Socket 进行【打开】【读写】【关闭】

      sample

    import socket
    
    
    #第一步相当于买手机,    sockect家族    ,tcp
    phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    
    #买完手机后插入手机卡,绑定ip,确定服务的唯一性
    phone.bind(('127.0.0.1',8080))
    
    #手机开机,并开5个进程,来处理问题
    phone.listen(5)
    
    #接电话,且里面有两个值,
    conn,addr = phone.accept()
    
    #接受消息,大小1024
    data = conn.recv(1024)
    
    print('from client msg %s'%dats)
    
    conn.send(data.upper())
    
    cnn.close() #断链接
    phone.close() #关闭socket
    服务端配置
    client = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    
    client.connect(('127.0.0.1',8080))
    
    client.send('hello'.encode('utf-8'))
    
    data = client.recv(1024)
    
    client.close()
    客户端配置

      server = socket.socket(socket.AF_INET,socket.SOCK_STREAM,0)          #括号内的内容不填写,默认是这些。

      参数一:

        socket.AF_INET        IPv4       (默认)

        socket.AF_INET6      IPv6

        socket.AF_UNIX       只能够用于单一的Unix系统进程间通信

      参数二:

        socket.SOCK_STREAM  流式socket , for TCP (默认)
        socket.SOCK_DGRAM   数据报式socket , for UDP

        socket.SOCK_RAW 原始套接字,普通的套接字无法处理ICMP、IGMP等网络报文,而SOCK_RAW可以;其次,SOCK_RAW也可以处理特殊的IPv4报文;此外,利用原始套接字,可以通过IP_HDRINCL套接字选项由用户构造IP头。
        socket.SOCK_RDM 是一种可靠的UDP形式,即保证交付数据报但不保证顺序。SOCK_RAM用来提供对原始协议的低级访问,在需要执行某些特殊操作时使用,如发送ICMP报文。SOCK_RAM通常仅限于高级用户或管理员运行的程序使用。
        socket.SOCK_SEQPACKET 可靠的连续数据包服务

      参数三:

        0  (默认)与特定的地址家族相关的协议,如果是 0 ,则系统就会根据地址格式和套接类别,自动选择一个合适的协议

      server.bind(address)

        s.bind(address) 将套接字绑定到地址。address地址的格式取决于地址族。在AF_INET下,以元组(host,port)的形式表示地址。

      server.listen(backlog)

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

            backlog等于5,表示内核已经接到了连接请求,但服务器还没有调用accept进行处理的连接个数最大为5, 这个值不能无限大,因为要在内核中维护连接队列

      server.setblocking(bool)

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

      server.accept()

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

        接收TCP 客户的连接(阻塞式)等待连接的到来

      server.connect(address)

        连接到address处的套接字。一般,address的格式为元组(hostname,port),如果连接出错,返回socket.error错误。

      server.connect_ex(address)

        同上,只不过会有返回值,连接成功时返回 0 ,连接失败时候返回编码,例如:10061

      server.close()

        关闭套接字

      server.recv(bufsize[,flag])

        接受套接字的数据。数据以字符串形式返回,bufsize指定最多可以接收的数量。flag提供有关消息的其他信息,通常可以忽略。

      server.recvfrom(bufsize[.flag])

        与recv()类似,但返回值是(data,address)。其中data是包含接收数据的字符串,address是发送数据的套接字地址。

      server.send(string[,flag])

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

      server.sendall(string[,flag])

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

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

      server.sendto(string[,flag],address)

        将数据发送到套接字,address是形式为(ipaddr,port)的元组,指定远程地址。返回值是发送的字节数。该函数主要用于UDP协议。

      server.settimeout(timeout)

        设置套接字操作的超时期,timeout是一个浮点数,单位是秒。值为None表示没有超时期。一般,超时期应该在刚创建套接字时设置,因为它们可能用于连接的操作(如 client 连接最多等待5s )

      server.getpeername()

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

      server.getsockname()

        返回套接字自己的地址。通常是一个元组(ipaddr,port)

      server.fileno()

        套接字的文件描述符

      升级版: 

    #可实现不停的收发数据
    
    import socket
    
    #第一步相当于买手机,    sockect家族    ,tcp
    phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    
    #买完手机后插入手机卡,绑定ip,确定服务的唯一性
    phone.bind(('127.0.0.1',8080))
    
    #手机开机,并开5个进程,来处理问题
    phone.listen(5)
    
    while True:
        #接电话,且里面有两个值,
        conn,addr = phone.accept()
    
        while True:
            try:
                #接受消息,大小1024
                data = conn.recv(1024)
                if not data:     #如果内容为空
                    break
    
                print('from client msg %s'%dats)
    
                conn.send(data.upper())
            except:
                break
    
        cnn.close() #断链接
    phone.close() #关闭socket
    服务端配置
    import socket
    
    ip_port = ('127.0.0.1',9998)
    
    client = socket.socket()
    client.connect(ip_port)
    
    while True:
        send_data = input('whaht do you want to send? =>')
        client.sendall(send_data.encode('utf-8'))
        server_reply = client.recv(1024)
        print('the data from server',server_reply)
    
    client.close()
    客户端配置

    Socket远程服务器操作

    import socket
    import os
    
    ip_port = ('127.0.0.1',9983)
    
    server = socket.socket()
    server.bind(ip_port)
    server.listen(5)
    
    while True:
        print('wait to connect ....')
        conn,addr = server.accept()
    
        while True:
            print('wait for recv command ....')
            client_data = conn.recv(1024)
            print('the command from client...',client_data)
            res = os.popen(client_data.decode('utf-8')).read()
            print(res)
            conn.send(res.encode('utf-8'))     #起先这里没有转码为bytes,会有报错
    
        conn.close()
    服务器端
    import socket
    
    ip_port = ('127.0.0.1',9983)
    
    client = socket.socket()
    client.connect(ip_port)
    
    while True:
        send_data = input('whaht do you want to send? =>')
        client.sendall(send_data.encode('utf-8'))
        server_reply = client.recv(1024)
        print(server_reply)
    
    client.close()
    客户端

      运行中的问题,命令接受过来是bytes类型,命令运行的话需要decode,而send发送的时候需要bytes,所以发送的时候需要decode,不然的话会有TypeError:'str' does not support the buffer interface

       客户端接受到数据,因为是bytes类型,显示中文需要decode()下

      提前告知client端文件解决粘包问题

    import socket
    import os
    
    ip_port = ('127.0.0.1',9999)
    
    server = socket.socket()
    server.bind(ip_port)
    server.listen(5)
    
    while True:
        print('等待链接 ....')
        conn,addr = server.accept()
    
        while True:
            print('等待执行命令 ....')
            client_data = conn.recv(1024)
            print('client_data',client_data)
            if not client_data:
                print('客户端已经断开')
                break
            print('执行命令...',client_data)
            res = os.popen(client_data.decode('utf-8')).read()
            print('发送数据之前...')
            if len(res) ==0:
                res = 'there is no this data!'
            conn.send(str(len(res.encode('utf-8'))).encode("utf-8"))
            # print('res',res)
            conn.send(res.encode('utf-8'))
            print('发送命令done')
    
        conn.close()
    
    server.close()
    server端
    import socket
    
    ip_port = ('127.0.0.1',9999)
    
    client = socket.socket()
    client.connect(ip_port)
    
    while True:
        send_data = input('what do you want to send? =>')
        client.sendall(send_data.encode('utf-8'))
        server_body_size = client.recv(1024)             #告知客户端需要接收文件的大小
        print('需要接受的文件大小:',server_body_size)
    
        receive_size = 0
        receive_data = b''
        n = 1
    
        while receive_size < int(server_body_size.decode()):
            print('第%s循环'%n)
            data = client.recv(1024)
            receive_size +=len(data)
            receive_data += data
    
            n +=1
    
        print('接收了数据的大小',receive_size)
        print(receive_data.decode())
        print('接受数据done')
    
    client.close()
    客户端

      如果把这些代码放到linux上面执行,则会有一个问题,会有个报错,send需要发的包的大小,会和send包的内容粘到一会来发送,这就是所谓的粘包

      解决办法:

      1、两个send之间sleep(0.5) 会解决这个问题,不过这个很low,不能时时获取数据

      2、这个方法就是在server端send完数据后,然后再cnn.recv()等待接受数据,在client端则是,接收到server端发来的文件大小长度后,然后再随便send点东西给server端,server端再执行发送正常数据的命令,这个问题就解决了。

      到server,get一个文件下载到本地

    import socket
    import os
    import hashlib
    
    ip_port = ('127.0.0.1',9995)
    
    server = socket.socket()
    server.bind(ip_port)
    server.listen(5)
    
    while True:
        print('等待链接 ....')
        conn,addr = server.accept()
    
        while True:
            print('等待执行命令 ....')
            client_data = conn.recv(1024)
            print('接收到的命令文件',client_data)
            client_method,client_file = client_data.split()
            print(client_method,client_file)
            if os.path.isfile(client_file):
                print('条件命令执行通过')
                file_size = os.stat(client_file).st_size          #获取本地文件的大小,这个点新get到的
                conn.send(str(file_size).encode('utf-8'))
                print('发送文件的大小给客户端', file_size)
                conn.recv(1024)
                print('收到客户端响应已经收到文件大小')
                md5 = hashlib.md5()
                f = open(client_file,'rb')
                for line in f:
                    conn.send(line)
                    md5.update(line)
                print('发送数据结束')
                conn.send(md5.hexdigest().encode('utf-8'))
                print('md5已发送给客户端')
    
        conn.close()
    
    server.close()
    server端
    import socket
    import hashlib
    
    ip_port = ('127.0.0.1',9995)
    
    client = socket.socket()
    client.connect(ip_port)
    
    while True:
        send_data = input('what do you want to send? =>')
        if send_data.startswith('get'):
            want_file = send_data.split()[1]
            client.send(send_data.encode('utf-8'))
            want_file_size = client.recv(1024)
            print('接收到的文件大小',want_file_size)
            want_file_size = int(want_file_size.decode())
            client.send(b'Got the file_size')
            print('回复确认已经收到文件的大小')
            md5 = hashlib.md5()
            f = open(want_file + '.new', 'wb')          #这个更改文件名字的方式新get到
            get_file_size = 0
    
            while get_file_size < want_file_size:
                if want_file_size - get_file_size >= 1024:
                    size = 1024
                else:
                    size = want_file_size - get_file_size
    
                data = client.recv(size)
                get_file_size += len(data)
                print(want_file_size,get_file_size,size)
                f.write(data)
                md5.update(data)
            else:
                f.close()
                print('客户端文件接收完毕')
                server_file_md5 = client.recv(1024)
                print('服务端md5',server_file_md5)
                print('客户端接收MD5',md5.hexdigest())
    
    client.close()
    View Code

    SocketServer模块

      SocketServer内部使用 IO多路复用 以及 “多线程” 和 “多进程” ,从而实现并发处理多个客户端请求的Socket服务端。即:每个客户端请求连接到服务器时,Socket服务端都会在服务器是创建一个“线程”或者“进程” 专门负责处理当前客户端的所有请求。

        

      ThreadingTCPServer

      ThreadingTCPServer实现的Soket服务器内部会为每个client创建一个 “线程”,该线程用来和客户端进行交互。

      1、ThreadingTCPServer基础

      使用ThreadingTCPServer:

    • 创建一个继承自 SocketServer.BaseRequestHandler 的类
    • 类中必须定义一个名称为 handle 的方法
    • 启动ThreadingTCPServer

      sample:

    #!/usr/bin/env python
    # -*- coding:utf-8 -*-
    import SocketServer
    
    class MyServer(SocketServer.BaseRequestHandler):
    
        def handle(self):
            # print self.request,self.client_address,self.server
            conn = self.request
            conn.sendall('欢迎致电 10086,请输入1xxx,0转人工服务.')
            Flag = True
            while Flag:
                data = conn.recv(1024)
                if data == 'exit':
                    Flag = False
                elif data == '0':
                    conn.sendall('通过可能会被录音.balabala一大推')
                else:
                    conn.sendall('请重新输入.')
    
    
    if __name__ == '__main__':
        server = SocketServer.ThreadingTCPServer(('127.0.0.1',8009),MyServer)
        server.serve_forever()

        2、ThreadingTCPServer源代码(待研究)

      ForkingTCPServer  

        ForkingTCPServer和ThreadingTCPServer的使用和执行流程基本一致,只不过在内部分别为请求者建立 “线程”  和 “进程”。   

      sample:  

    复制代码
    #!/usr/bin/env python
    # -*- coding:utf-8 -*-
    import SocketServer
    
    class MyServer(SocketServer.BaseRequestHandler):
    
        def handle(self):
            # print self.request,self.client_address,self.server
            conn = self.request
            conn.sendall('欢迎致电 10086,请输入1xxx,0转人工服务.')
            Flag = True
            while Flag:
                data = conn.recv(1024)
                if data == 'exit':
                    Flag = False
                elif data == '0':
                    conn.sendall('通过可能会被录音.balabala一大推')
                else:
                    conn.sendall('请重新输入.')
    
    
    if __name__ == '__main__':
        server = SocketServer.ForkingTCPServer(('127.0.0.1',8009),MyServer)    #与线程只有这里不相同
        server.serve_forever()

      SocketServer的ThreadingTCPServer之所以可以同时处理请求得益于 select 和 os.fork 两个东西,其实本质上就是在服务器端为每一个客户端创建一个进程,当前新创建的进程用来处理对应客户端的请求,所以,可以支持同时n个客户端链接(长连接)。

        

      上传一个文件到服务器端(面向对象的方式,socketserver)

    import socketserver
    import json
    import os
    
    
    class MyTcpHandler(socketserver.BaseRequestHandler):
        def put(self,*args):
            cmd_dict = args[0]
            filename = cmd_dict['filename']
            filesize = cmd_dict['size']
            if os.path.isfile(filename):
                f = open(filename + '.new','wb')
            else:
                f = open(filename,'wb')
    
            self.request.send(b'200,ok')
            received_size = 0
            while received_size < filesize:
                data = self.request.recv(1024)
                f.write(data)
                received_size += len(data)
                print(received_size,filesize)
    
            else:
                print('%s文件接收完毕'%filename)
    
        def handle(self):
            while True:
                try:
                    self.data = self.request.recv(1024).strip()
                    print("{} wrote:".format(self.client_address[0]))
                    print('在客户端收到接受文件前的信息',self.data)
                    cmd_dict = json.loads(self.data.decode('utf-8'))
                    print('在客户端接收到的信息是:',cmd_dict)
                    action = cmd_dict['action']
                    if hasattr(self,action):               #反射方法
                        func = getattr(self,action)
                        func(cmd_dict)
    
                except ConnectionResetError as e:
                    print('error',e)
                    break
    
    if __name__ == '__main__':
        HOST,PORT = 'localhost',9997
        server = socketserver.ThreadingTCPServer((HOST,PORT),MyTcpHandler)
        print('ftpserver已启动')
        server.serve_forever()
    server端
    import json
    import socket
    import os
    
    
    
    class FtpClient(object):
        def __init__(self):
            self.client = socket.socket()
    
        def help(self):
            msg = '''
                    ls
                    pwd
                    cd ../..
                    get filename
                    put filename
                    '''
            print(msg)
    
        def connect(self,ip,port):
            self.client.connect((ip,port))
            print('已连接到ftpserver')
    
        def interactive(self):
            while True:
                cmd = input('input you cmd=>').strip()
                if len(cmd) == 0:continue
    
                cmd_str = cmd.split()[0]
                if hasattr(self,'cmd_%s'%cmd_str):        #反射,判断有没有这个方法
                    func = getattr(self,"cmd_%s"%cmd_str) #反射,执行这个方法
                    func(cmd)
                    print('下一步运行put方法')
    
                else:
                    self.help()
    
        def cmd_put(self,*args):
            print('开始上传文件......')
            cmd_method = args[0].split()
            if len(cmd_method) >1:
                filename = cmd_method[1]
                if os.path.isfile(filename):
                    filesize = os.stat(filename).st_size
                    mes_dict = {
                        'action':'put',
                        'filename':filename,
                        "size":filesize,
                        "overridden":True
                    }
                    self.client.send(json.dumps(mes_dict).encode('utf-8'))
                    print('发送了文件信息',json.dumps(mes_dict).encode("utf-8"))
                    server_response = self.client.recv(1024)
                    print('收到了ftp端的确认信息:',server_response)
    
                    f = open(filename,'rb')
                    for line in f:
                        self.client.send(line)
                    else:
                        print('发送文件完毕...')
                        f.close()
                else:
                    print('需要发送的文件不存在')
    
        def cmd_get(self):
            pass
    
    ftp = FtpClient()
    ftp.connect('localhost',9997)
    ftp.interactive()
    client 端

    粘包

      期间你会遇到粘包的问题所谓的粘包的问题是指,你这次输入的命令,返回的结果却是上一次的输入命令的结果问题的原因在于,接收方不知道消息的大小,不知道一次要取多少造成的,tcp有这样的问题,udp没有,程序会有个缓存区

            如图所示,程序每次发送消息不是直接给客户,而是放到缓冲区,接受消息也是如此,在缓冲区去取。tcp是流式的,会一直发,信息都存在客户短的缓存中,如果第一次取消息设置的不够大,那就取不完,下一次再输入新的命令时,会继续取上次留下的消息,这就是造成粘包的原因

      那如何解决呢?

      办法是,服务端在发送消息之前告知客户端我这信息的大小,然后客户端用这个大小去接受,这样每次接受的就是完整的信息了,代码实现如下

          客户端的处理

    import struct
    #struct 这个模块是用来计算数据大小的
    data=client。recv(4) #struct 计算后得知消息体的大小,这段占位为4
    data_size=struct.unpack('i',data)[0]#获得需要发送消息的大小

    res=client.revc(data_size)#接受实际的大小
    print (res.decode(‘gbk’))

      服务端处理

    import struct

    conn.send(struct.pack('i',len(back_msg))) #i,打包的模式,len是消息的大小。
    conn.send(back_msg) #在发送消息

      这里有个问题,如果数据过大,比缓存区还大,直接send的话数据就会没了,这是后需要用

        conn.sendall(back_msg)

        sendall是循环调用send命令。

    客户端收的时候的处理,

    recv_size=0
    recv_bytes=b''
    while recv_size < data_size:
    res=client.recv(1024)
    recv_bytes+=res
    recv_size+=len(res)

    print(recv_bytes.decode('gbk'))

          如果,再大,大到文件的长度4个比特位都放不下那有如何,这里就引用到了字典-json,先把文件存为字典{’size‘:1234567891231456789123456789}然后再json化传递给客户端,客户端在反json后再读取,如此获得文件的大小

      服务端的配置

    import json
    head_dict={'data_size':len(back_msg)}#将文件大小放到字典中
    head_json=json.dumps(head_dict) #json化
    head_bytes=head_json.encode('utf-8')


    conn.send(struct.pack('i',len(head_bytes)))#先发送这个json的大小


    conn.send(head_bytes)#发送json文件


    conn.sendall(back_msg)#发送真实的文件

       客户端的配置

    #收取包头的长度
    head=client.recv(4)
    head_size=struct.unpack('i',head)[0]

    #收取包头
    head_bytes=client.recv(head_size)
    head_json=head_bytes.decode('utf-8')
    head_dict=json.loads(head_json)
    data_size=head_dict['data_size']

    #收取真实的数据:
    recv_size=0
    recv_bytes=b''
    while recv_size < data_size:
    res=client.recv(1024)
    recv_bytes+=res
    recv_size+=len(res)

    print(recv_bytes.decode('gbk'))

    返回python目录

  • 相关阅读:
    Rust入坑指南:亡羊补牢
    antirez:Redis6真的来了
    代码检查又一利器:ArchUnit
    【译】浅谈SOLID原则
    Rust入坑指南:鳞次栉比
    【译】什么才是优秀的代码
    Elasticsearch从入门到放弃:文档CRUD要牢记
    【译】利用Lombok消除重复代码
    Netty 中的心跳检测机制
    Netty 中的异步编程 Future 和 Promise
  • 原文地址:https://www.cnblogs.com/nopnog/p/7081485.html
Copyright © 2011-2022 走看看