zoukankan      html  css  js  c++  java
  • 网络编程-Socket

    套接字编程Socket

    1,Socket介绍

    1,什么是Socket

      Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部。

       也有人将socket说成ip+port,ip是用来标识互联网中的一台主机的位置,而port是用来标识这台机器上的一个应用程序,ip地址是配置到网卡上的,而port是应用程序开启的,ip与port的绑定就标识了互联网中独一无二的一个应用程序 而程序的pid是同一台机器上不同进程或者线程的标识

    2,什么是套接字

      套接字起源于 20 世纪 70 年代加利福尼亚大学伯克利分校版本的 Unix,即人们所说的 BSD Unix。 因此,有时人们也把套接字称为“伯克利套接字”或“BSD 套接字”。一开始,套接字被设计用在同 一台主机上多个应用程序之间的通讯。这也被称进程间通讯,或 IPC。套接字有两种(或者称为有两个种族),分别是基于文件型的和基于网络型的。 

    基于文件类型的套接字家族

      套接字家族的名字:AF_UNIX

      unix一切皆文件,基于文件的套接字调用的就是底层的文件系统来取数据,两个套接字进程运行在同一机器,可以通过访问同一个文件系统间接完成通信

    基于网络类型的套接字家族

      套接字家族的名字:AF_INET

      (还有AF_INET6被用于ipv6,还有一些其他的地址家族,不过,他们要么是只用于某个平台,要么就是已经被废弃,或者是很少被使用,或者是根本没有实现,所有地址家族中,AF_INET是使用最广泛的一个,python支持很多种地址家族,但是由于我们只关心网络编程,所以大部分时候我们只使用AF_INET)

    3,套接字工作流程

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

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

    socket()模块函数用法

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

    4,基于TCP的套接字

    tcp是基于链接的,必须先启动服务端,然后再启动客户端去链接服务端

    tcp服务端

    ss = socket() #创建服务器套接字
    ss.bind()      #把地址绑定到套接字
    ss.listen()      #监听链接
    inf_loop:      #服务器无限循环
        cs = ss.accept() #接受客户端链接
        comm_loop:         #通讯循环
            cs.recv()/cs.send() #对话(接收与发送)
        cs.close()    #关闭客户端套接字
    ss.close()        #关闭服务器套接字(可选)
    

    tcp客户端

    cs = socket()    # 创建客户套接字
    cs.connect()    # 尝试连接服务器
    comm_loop:        # 通讯循环
        cs.send()/cs.recv()    # 对话(发送/接收)
    cs.close()            # 关闭客户套接字

      一次通话

    import socket
    
    phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM) #买手机
    phone.bind(('127.0.0.1',8000)) #绑定手机卡
    phone.listen(5) #开机
    print('---->')
    conn,addr=phone.accept() #等电话 (阻塞)
    
    data=conn.recv(1024) #收消息(阻塞)
    print('客户端发来的消息是: ',data)
    conn.send(data.upper())#发消息
    
    conn.close()
    phone.close()
    server
    import  socket
    
    phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    
    phone.connect(('127.0.0.1',8000)) #拨通电话
    
    phone.send('hello地方'.encode('utf-8')) #发消息
    data=phone.recv(1024) # (阻塞)
    print('收到服务端的发来的消息:',data)
    client

      循环通话

    对于windows,client程序退出后server异常,对于linux,client程序退出后server一直接收空数据进入死循环

    import socket
    
    phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)  # 买手机
    phone.bind(('127.0.0.1', 8000))  # 绑定手机卡
    phone.listen(5)  # 开机
    print('---->')
    conn, addr = phone.accept()  # 等电话 (阻塞)
    
    while True: # 通信循环
        data = conn.recv(1024)  # 收消息(阻塞)
        print('客户端发来的消息是: ', data)
    
        conn.send(data.upper())  # 发消息
    
    conn.close()
    phone.close()
    server
    import  socket
    
    phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    
    phone.connect(('127.0.0.1',8000)) #拨通电话
    
    while True: # 通信循环
        msg=input('>>: ').strip()
        phone.send(msg.encode('utf-8')) #发消息,发送空给server,server接收空一直阻塞,不会进一步发送数据给client(客户端打通电话不说话,服务端等待客户端说话,然后大家都不说话。。。),# 不能直接写入字符串,需要转码,因为传到物理层需要转成二进制才行
        data=phone.recv(1024)
        print(data)
    
    phone.close()
    client

      加上链接循环

    import socket
    
    phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    phone.connect(('127.0.0.1', 8085))
    
    while True:  # 通信循环
        msg = input('>>> ').strip()
        if not msg: continue  # 解决因为输入为空而导致出现停运
        phone.send(msg.encode('utf-8'))  # 不能直接写入字符串,需要转码,因为传到物理层需要转成二进制才行
        data = phone.recv(1024)
        print(data.decode('utf-8'))
    
    phone.close()
    client v2:解决client发送空阻塞客户端的问题
    import socket
    
    phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    phone.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)  # 解决ip端口冲突,可以重用ip端口
    phone.bind(('127.0.0.1', 8085))  # 0-65535:0-1024给操作系统使用
    phone.listen(5)   # 最大挂起的链接数
    
    print(1)
    while True:
        conn, client_addr = phone.accept()  # 在服务端要有一个收发消息,所接受的数据都是bytes类型
        print(client_addr)
    
        while True:  # 通信循环
            try:  # 应对windows系统:如果不捕获异常,客户端程序停止后,服务端程序异常停止,ConnectionResetError: [WinError 10054] 远程主机强迫关闭了一个现有的连接。
                data = conn.recv(1024)  # 1,单位:bytes,2,1024代表最大接受1024个bytes
                if not data: break  # 适用Linux。防止客户端断开,导致出现死循环,占用大量内存(前提是只发生在linux上面,windows系统上面若客户端单方面断开,直接报错)
                print('客户端的数据:', data.decode('utf-8'))
                conn.send(data.upper())
            except ConnectionResetError:
                break
    
        conn.close()
    phone.close()
    server v2:解决client程序终止后server程序退出

    5,基于UDP的套接字

    udp是无链接的,先启动哪一端都不会报错

    udp服务端

    ss = socket()   #创建一个服务器的套接字
    ss.bind()       #绑定服务器套接字
    inf_loop:       #服务器无限循环
        cs = ss.recvfrom()/ss.sendto() # 对话(接收与发送)
    ss.close()                         # 关闭服务器套接字
    

    udp客户端

    cs = socket()   # 创建客户套接字
    comm_loop:      # 通讯循环
        cs.sendto()/cs.recvfrom()   # 对话(发送/接收)
    cs.close()                      # 关闭客户套接字
    
    import socket
    
    udpserver=socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
    udpserver.bind(('127.0.0.1',8080))
    
    while True: #通讯循环
        data,client_addr=udpserver.recvfrom(1024)
        print('======>',data.decode('utf-8'))
        print(client_addr)
        # msg=input('>>: ')
        udpserver.sendto(data.upper(),client_addr)
    udp server
    import socket
    
    udpclient=socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
    
    server_ip_port=('127.0.0.1',8080)
    while True:
        inp=input(">>: ")
        udpclient.sendto(inp.encode('utf-8'),server_ip_port)
    
        data,server_addr=udpclient.recvfrom(1024)
        print(data.decode('utf-8'))
    udp client

    模拟ssh远程执行命令

    import socket
    import subprocess
    
    phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    phone.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)  # 解决ip端口冲突,可以重用ip端口
    phone.bind(('127.0.0.1', 8085))  # 0-65535:0-1024给操作系统使用
    phone.listen(5)   # 最大挂起的链接数
    
    print(1)
    while True:  # 链接循环
        conn, client_addr = phone.accept()  # 在服务端要有一个收发消息,所接受的数据都是bytes类型
        print(client_addr)
    
        while True:  # 通信循环
            try:  # 应对windows系统:如果不捕获异常,客户端程序停止后,服务端程序异常停止,ConnectionResetError: [WinError 10054] 远程主机强迫关闭了一个现有的连接。
                # 1,收命令
                cmd = conn.recv(1024)  # 1,单位:bytes,2,1024代表最大接受1024个bytes
                if not cmd: break  # 适用Linux。防止客户端断开,导致出现死循环,占用大量内存(前提是只发生在linux上面,windows系统上面若客户端单方面断开,直接报错)
                print('客户端的数据:', cmd.decode('utf-8'))
    
                # 2,执行命令,拿到结果
    
                obj = subprocess.Popen(cmd.decode('utf-8'), shell=True,
                                       stdout=subprocess.PIPE,
                                       stderr=subprocess.PIPE)
                # print(obj)
                # print('srdout 1--->: ', obj.stdout.read().decode('utf-8'))
                # print('stdout 2--->: ', obj.stdout.read().decode('utf-8'))
                # print('srderr 1--->: ', obj.stderr.read().decode('utf-8'))
                stdout = obj.stdout.read()
                stderr = obj.stdout.read()
    
                # 3,把命令的结果返回给客户端
                print(len(stdout)+len(stderr))
                conn.send(stdout + stderr)
            except ConnectionResetError:
                break
    
        conn.close()
    phone.close()
    server
    import socket
    
    phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    phone.connect(('127.0.0.1', 8085))
    
    while True:
        # 1,发命令
        cmd = input('>>> ').strip()
        if not cmd:
            continue  # 解决因为输入为空而导致出现停运
        phone.send(cmd.encode('utf-8'))  # 不能直接写入字符串,需要转码,因为传到物理层需要转成二进制才行
    
        # 2,那命令的结果,并打印
        data = phone.recv(1024)
        print(data.decode('gbk'))  # 这里如果是windows就使用gbk,linux使用utf-8.
    
    phone.close()
    client

    qq聊天(由于udp无连接,所以可以同时多个客户端去跟服务器端通信)

    #_*_coding:utf-8_*_
    __author__ = 'Linhaifeng'
    import socket
    ip_port=('127.0.0.1',8081)
    udp_server_sock=socket.socket(socket.AF_INET,socket.SOCK_DGRAM) #买手机
    udp_server_sock.bind(ip_port)
    
    while True:
        qq_msg,addr=udp_server_sock.recvfrom(1024)
        print('来自[%s:%s]的一条消息:33[1;44m%s33[0m' %(addr[0],addr[1],qq_msg.decode('utf-8')))
        back_msg=input('回复消息: ').strip()
    
        udp_server_sock.sendto(back_msg.encode('utf-8'),addr)
    udp服务端
    #_*_coding:utf-8_*_
    __author__ = 'Linhaifeng'
    import socket
    BUFSIZE=1024
    udp_client_socket=socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
    
    qq_name_dic={
        '狗哥alex':('127.0.0.1',8081),
        '瞎驴':('127.0.0.1',8081),
        '一棵树':('127.0.0.1',8081),
        '武大郎':('127.0.0.1',8081),
    }
    
    
    while True:
        qq_name=input('请选择聊天对象: ').strip()
        while True:
            msg=input('请输入消息,回车发送: ').strip()
            if msg == 'quit':break
            if not msg or not qq_name or qq_name not in qq_name_dic:continue
            udp_client_socket.sendto(msg.encode('utf-8'),qq_name_dic[qq_name])
    
            back_msg,addr=udp_client_socket.recvfrom(BUFSIZE)
            print('来自[%s:%s]的一条消息:33[1;44m%s33[0m' %(addr[0],addr[1],back_msg.decode('utf-8')))
    
    udp_client_socket.close()
    udp客户端1
    #_*_coding:utf-8_*_
    __author__ = 'Linhaifeng'
    import socket
    BUFSIZE=1024
    udp_client_socket=socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
    
    qq_name_dic={
        '狗哥alex':('127.0.0.1',8081),
        '瞎驴':('127.0.0.1',8081),
        '一棵树':('127.0.0.1',8081),
        '武大郎':('127.0.0.1',8081),
    }
    
    
    while True:
        qq_name=input('请选择聊天对象: ').strip()
        while True:
            msg=input('请输入消息,回车发送: ').strip()
            if msg == 'quit':break
            if not msg or not qq_name or qq_name not in qq_name_dic:continue
            udp_client_socket.sendto(msg.encode('utf-8'),qq_name_dic[qq_name])
    
            back_msg,addr=udp_client_socket.recvfrom(BUFSIZE)
            print('来自[%s:%s]的一条消息:33[1;44m%s33[0m' %(addr[0],addr[1],back_msg.decode('utf-8')))
    
    udp_client_socket.close()
    udp客户端2

    2,粘包现象

    1,粘包原因

      接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的。

      在python语法里,socket.send(x)的含义是把x当作一个包发送到发送到缓存

      就如上面模拟ssh远程执行命令,虽然可以执行windows下面的dir等命令,但是出现了粘包,导致结果粘在了一起,它的发生主要就是因为socket缓冲区导致的。

       你的程序实际上无权直接操作网卡的,你操作网卡都是通过操作系统给用户程序暴露出来的接口,那每次你的程序要给远程发数据时,其实是先把数据从用户态copy到内核态,这样的操作是耗资源和时间的,频繁的在内核态和用户态之前交换数据势必会导致发送效率降低, 因此socket 为提高传输效率,发送方往往要收集到足够多的数据后才发送一次数据给对方。若连续几次需要send的数据都很少,通常TCP socket 会根据优化算法把这些数据合成一个TCP段后一次发送出去,这样接收方就收到了粘包数据。

    2,粘包只发生与TCP

      粘包问题只存在于TCP中,NOT UDP

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

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

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

      总结:

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

    3,如何会产生粘包

      发送方发送数据时间间隔很短,且数据量很小,会合到一起,产生粘包

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

      代码示例:

       1,发送时间间隔短,数据量小,客户端产生粘包

        
    # -*- coding: utf-8 -*-
    import socket
    
    server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server.bind(('127.0.0.1', 9903))  # 0-65535; 1-1024给操作系统使用
    server.listen(5)
    
    conn, addr = server.accept()
    
    res1 = conn.recv(1024)
    print('第一次', res1)
    
    res2 = conn.recv(1024)
    print('第二次', res2)
    server
        
    # -*- coding: utf-8 -*-
    import socket
    
    client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    
    client.connect(('127.0.0.1', 9903))
    
    client.send('hello'.encode('utf-8'))
    client.send('world'.encode('utf-8'))
    client
    第一次 b'helloworld'
    第二次 b''

      2,发送时间间隔长,服务端每次接收数据量够大,则不产生粘包

        
    # -*- coding: utf-8 -*-
    import socket
    
    server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server.bind(('127.0.0.1', 9903))  # 0-65535; 1-1024给操作系统使用
    server.listen(5)
    
    conn, addr = server.accept()
    
    res1 = conn.recv(1024)
    print('第一次', res1)
    
    res2 = conn.recv(1024)
    print('第二次', res2)
    server
        
    # -*- coding: utf-8 -*-
    import socket
    import time
    
    client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    
    client.connect(('127.0.0.1', 9903))
    
    client.send('hello'.encode('utf-8'))
    time.sleep(3)  # 设定sleeptime为3秒钟
    client.send('world'.encode('utf-8'))
    client
    第一次 b'hello'
    第二次 b'world'

      3,服务端未能一次性接收完数据,服务端产生粘包

        
    # -*- coding: utf-8 -*-
    import socket
    
    server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server.bind(('127.0.0.1', 9903))  # 0-65535; 1-1024给操作系统使用
    server.listen(5)
    
    conn, addr = server.accept()
    
    res1 = conn.recv(1)  # 这里只接收一个bytes
    print('第一次', res1)
    
    res2 = conn.recv(1024)
    print('第二次', res2)
    server
        
    # -*- coding: utf-8 -*-
    import socket
    import time
    
    client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    
    client.connect(('127.0.0.1', 9903))
    
    client.send('hello'.encode('utf-8'))
    time.sleep(3)
    client.send('world'.encode('utf-8'))
    client
    第一次 b'h'
    第二次 b'ello'

      注意:当在1中的服务端和客户端都各加上timesleep,或者服务端每次接收的数据不一,这两种情况的任意一种变化,都可能产生粘包。加上timesleep解决粘包是最不可取的方法,如何without timesleep并且利用Nagle算法优势的情况下解决粘包问题呢?

    4,如何解决粘包

      实际上就是让服务端知道客户端发出多少数据,然后让服务端收多少数据,实际上就可以解决粘包问题了

     简单版

      远程执行命令

       导入struct模块制作报头,提前知道数据量为多少,以达到接收不粘包,但是这种方法不能发送大一点的文件

        
    # -*- coding: utf-8 -*-
    import socket
    import subprocess
    import struct
    
    phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    # phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
    phone.bind(('127.0.0.1', 9909))  # 0-65535:0-1024给操作系统使用
    phone.listen(5)  # 监听进程
    
    print('starting...')
    while True:  # 链接循环
        conn, client_addr = phone.accept()
        print(client_addr)
    
        while True:  # 通信循环
            try:
                # 1、收命令
                cmd = conn.recv(8096)
                if not cmd: break  # 适用于linux操作系统
    
                # 2、执行命令,拿到结果
                obj = subprocess.Popen(cmd.decode('utf-8'), shell=True,
                                       stdout=subprocess.PIPE,
                                       stderr=subprocess.PIPE)
    
                stdout = obj.stdout.read()
                stderr = obj.stderr.read()
    
                # 3、把命令的结果返回给客户端
                # 第一步:制作固定长度的报头
                total_size = len(stdout) + len(stderr)
                header = struct.pack('i', total_size)
    
                # 第二步:把报头发送给客户端
                conn.send(header)
    
                # 第三步:再发送真实的数据
                conn.send(stdout)
                conn.send(stderr)
    
            except ConnectionResetError:  # 适用于windows操作系统
                break
        conn.close()
    
    phone.close()
    server
        
    # -*- coding: utf-8 -*-
    import socket
    import struct
    
    phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    
    phone.connect(('127.0.0.1', 9909))
    
    while True:
        # 1、发命令
        cmd = input('>>: ').strip()  # ls /etc
        if not cmd:continue
        phone.send(cmd.encode('utf-8'))
    
        # 2、拿命令的结果,并打印
    
        # 第一步:先收报头
        header = phone.recv(4)
    
        # 第二步:从报头中解析出对真实数据的描述信息(数据的长度)
        total_size = struct.unpack('i', header)[0]  # struct.pack(格式,)
    
        # 第三步:接收真实的数据
        recv_size = 0
        recv_data = b''  # 数据类型为bytes类型
        while recv_size < total_size:
            res = phone.recv(1024)  # 1024是一个坑
            recv_data += res
            recv_size += len(res)
    
        print(recv_data.decode('gbk'))  # windows默认为gbk,linux为utf-8
    
    phone.close()
    cilent

     终极版 

      远程执行命令

      报头增加了信息量,文件可传输变大

        
    import socket
    import subprocess
    import struct
    import json
    
    phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    # phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
    phone.bind(('127.0.0.1',9909))  # 0-65535:0-1024给操作系统使用
    phone.listen(5)
    
    print('starting...')
    while True:  # 链接循环
        conn, client_addr=phone.accept()
        print(client_addr)
    
        while True:  # 通信循环
            try:
                # 1、收命令
                cmd = conn.recv(8096)
                if not cmd: break  # 适用于linux操作系统
    
                # 2、执行命令,拿到结果
                obj = subprocess.Popen(cmd.decode('utf-8'), shell=True,
                                       stdout=subprocess.PIPE,
                                       stderr=subprocess.PIPE)
    
                stdout = obj.stdout.read()
                stderr = obj.stderr.read()
    
                # 3、把命令的结果返回给客户端
                # 第一步:制作固定长度的报头
                header_dic = {
                    'filename': 'a.txt',
                    'md5': 'xxdxxx',
                    'total_size': len(stdout) + len(stderr)
                }
    
                header_json = json.dumps(header_dic)  # 将head_dic转为json字符串类型
    
                header_bytes = header_json.encode('utf-8')  # 将header_json转化为bytes类型,就可用于传输了
    
                # 第二步:先发送报头的长度
                conn.send(struct.pack('i', len(header_bytes)))  # 将head_bytes改成固定的长度,让客户端识别
    
                # 第三步:再发报头
                conn.send(header_bytes)
    
                # 第四步:再发送真实的数据
                conn.send(stdout)
                conn.send(stderr)
    
            except ConnectionResetError:  # 适用于windows操作系统
                break
        conn.close()
    
    phone.close()
    server
        
    import socket
    import struct
    import json
    
    phone=socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    
    phone.connect(('127.0.0.1', 9909))
    
    while True:
        # 1、发命令
        cmd = input('>>: ').strip()  # ls /etc
        if not cmd:continue
        phone.send(cmd.encode('utf-8'))
    
        # 2、拿命令的结果,并打印
    
        # 第一步:先收报头的长度
        obj = phone.recv(4)
        header_size = struct.unpack('i', obj)[0]  # 接收报头的长度
    
        # 第二步:再收报头
        header_bytes = phone.recv(header_size)
    
        # 第三步:从报头中解析出对真实数据的描述信息
        header_json = header_bytes.decode('utf-8')
        header_dic = json.loads(header_json)  # 反序列化
        print(header_dic)
        total_size = header_dic['total_size']
    
        # 第四步:接收真实的数据
        recv_size = 0
        recv_data = b''
        while recv_size < total_size:
            res = phone.recv(1024)  # 1024是一个坑
            recv_data += res
            recv_size += len(res)
    
        print(recv_data.decode('gbk'))  # windows默认编码为gbk,Linux为utf-8
    
    phone.close()
    client

    5,通过socket收发文件软件开发

      收发收发文件与远程执行命令的程序原理是一摸一样的,比如下载文件的过程:

        1、客户端提交命令

        2、服务端接收命令,解析,执行下载文件的方法,即以读的方式打开文件,for循环读出文件的一行行内容,然后send给客户端

        3、客户端以写的方式打开文件,将接收的内容写入文件中

    单线程

      简单版

      实现简单的文件传输,下面代码示例是客户端从服务端下载文件,上传只需更改两者的文件读取命令即可

        
    import socket
    import subprocess
    import struct
    import json
    import os
    
    share_dir = r'这里填写服务端需要上传的文件具体目录地址'
    
    phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    # phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
    phone.bind(('127.0.0.1', 8912))  # 0-65535:0-1024给操作系统使用
    phone.listen(5)
    
    print('starting...')
    while True:  # 链接循环
        conn, client_addr = phone.accept()
        print(client_addr)
    
        while True:  # 通信循环
            try:
                # 1、收命令
                res = conn.recv(8096)  # b'get 1.mp4' 遵循get+文件名格式提交命令
                if not res: break  # 适用于linux操作系统
    
                # 2、解析命令,提取相应命令参数
                cmds = res.decode('utf-8').split()  # ['get','1.mp4']
                filename = cmds[1]
    
                # 3、以读的方式打开文件,读取文件内容发送给客户端
                # 第一步:制作固定长度的报头
                header_dic = {
                    'filename': filename,  # 'filename':'1.mp4'
                    'md5': 'xxdxxx',
                    'file_size': os.path.getsize(r'%s/%s' % (share_dir, filename))  # os.path.getsize(r'服务端文件上传目录')
                }
    
                header_json = json.dumps(header_dic)
    
                header_bytes = header_json.encode('utf-8')
    
                # 第二步:先发送报头的长度
                conn.send(struct.pack('i', len(header_bytes)))
    
                # 第三步:再发报头
                conn.send(header_bytes)
    
                # 第四步:再发送真实的数据
                with open('%s/%s' % (share_dir, filename), 'rb') as f:
                    # conn.send(f.read())
                    for line in f:
                        conn.send(line)
    
            except ConnectionResetError:  # 适用于windows操作系统
                break
        conn.close()
    
    phone.close()
    server
        
    import socket
    import struct
    import json
    
    download_dir = r'客户端需要题目下载地方的目录地址'
    
    phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    
    phone.connect(('127.0.0.1', 8912))
    
    while True:
        # 1、发命令
        cmd = input('>>: ').strip()  # get a.txt
        if not cmd: continue
        phone.send(cmd.encode('utf-8'))
    
        # 2、以写的方式打开一个新文件,接收服务端发来的文件的内容写入客户的新文件
        # 第一步:先收报头的长度
        obj = phone.recv(4)
        header_size = struct.unpack('i', obj)[0]
    
        # 第二步:再收报头
        header_bytes = phone.recv(header_size)
    
        # 第三步:从报头中解析出对真实数据的描述信息
        header_json = header_bytes.decode('utf-8')
        header_dic = json.loads(header_json)
        '''
                header_dic={
                    'filename': filename, #'filename':'1.mp4'
                    'md5':'xxdxxx',
                    'file_size': os.path.getsize(filename)
                }
        '''
        print(header_dic)
        total_size = header_dic['file_size']
        filename = header_dic['filename']
    
        # 第四步:接收真实的数据
        with open('%s/%s' % (download_dir, filename), 'wb') as f:
            recv_size = 0
            while recv_size < total_size:
                line = phone.recv(1024)  # 1024是一个坑
                f.write(line)
                recv_size += len(line)
                print('总大小:%s   已下载大小:%s' % (total_size, recv_size))
    
    
    phone.close()
    client

      函数版

      简单版定义装进函数

        
    import socket
    import subprocess
    import struct
    import json
    import os
    
    share_dir = r'server 上传目录'
    
    def get(conn,cmds):
        filename = cmds[1]
    
        # 3、以读的方式打开文件,读取文件内容发送给客户端
        # 第一步:制作固定长度的报头
        header_dic = {
            'filename': filename,  # 'filename':'1.mp4'
            'md5': 'xxdxxx',
            'file_size': os.path.getsize(r'%s/%s' % (share_dir, filename))
        # os.path.getsize(r'server 上传目录')
        }
    
        header_json = json.dumps(header_dic)
    
        header_bytes = header_json.encode('utf-8')
    
        # 第二步:先发送报头的长度
        conn.send(struct.pack('i', len(header_bytes)))
    
        # 第三步:再发报头
        conn.send(header_bytes)
    
        # 第四步:再发送真实的数据
        with open('%s/%s' % (share_dir, filename), 'rb') as f:
            # conn.send(f.read())
            for line in f:
                conn.send(line)
    
    def put(conn,cmds):
        pass
    
    def run():
        phone=socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        # phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
        phone.bind(('127.0.0.1', 8912))  # 0-65535:0-1024给操作系统使用
        phone.listen(5)
    
        print('starting...')
        while True: # 链接循环
            conn,client_addr=phone.accept()
            print(client_addr)
    
            while True: # 通信循环
                try:
                    # 1、收命令
                    res = conn.recv(8096)  # b'put 1.mp4'
                    if not res: break  # 适用于linux操作系统
    
                    # 2、解析命令,提取相应命令参数
                    cmds = res.decode('utf-8').split() # ['put','1.mp4']
                    if cmds[0] == 'get':
                        get(conn, cmds)
                    elif cmds[0] == 'put':
                        input(conn, cmds)
    
    
                except ConnectionResetError: #适用于windows操作系统
                    break
            conn.close()
    
        phone.close()
    
    
    if __name__ == '__main__':
        run()
    server
        
    import socket
    import struct
    import json
    
    download_dir = r'client 下载目录'
    
    def get(phone,cmds):
        # 2、以写的方式打开一个新文件,接收服务端发来的文件的内容写入客户的新文件
        # 第一步:先收报头的长度
        obj = phone.recv(4)
        header_size = struct.unpack('i', obj)[0]
    
        # 第二步:再收报头
        header_bytes = phone.recv(header_size)
    
        # 第三步:从报头中解析出对真实数据的描述信息
        header_json = header_bytes.decode('utf-8')
        header_dic = json.loads(header_json)
        '''
                header_dic={
                    'filename': filename, #'filename':'1.mp4'
                    'md5':'xxdxxx',
                    'file_size': os.path.getsize(filename)
                }
        '''
        print(header_dic)
        total_size = header_dic['file_size']
        filename = header_dic['filename']
    
        # 第四步:接收真实的数据
        with open('%s/%s' % (download_dir, filename), 'wb') as f:
            recv_size = 0
            while recv_size < total_size:
                line = phone.recv(1024)  # 1024是一个坑
                f.write(line)
                recv_size += len(line)
                print('总大小:%s   已下载大小:%s' % (total_size, recv_size))
    
    def put(phone,cmds):
        pass
    
    def run():
        phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    
        phone.connect(('127.0.0.1',8912))
    
        while True:
            #1、发命令
            inp=input('>>: ').strip() #get a.txt
            if not inp:continue
            phone.send(inp.encode('utf-8'))
    
            cmds=inp.split() #['get','a.txt']
            if cmds[0] == 'get':
                get(phone,cmds)
            elif cmds[0] == 'put':
                put(phone,cmds)
    
        phone.close()
    
    
    
    if __name__ == '__main__':
        run()
    client

      面向对象版

        
    import socket
    import struct
    import json
    import subprocess
    import os
    
    class MYTCPServer:
        address_family = socket.AF_INET
    
        socket_type = socket.SOCK_STREAM
    
        allow_reuse_address = False
    
        max_packet_size = 8192
    
        coding='utf-8'
    
        request_queue_size = 5
    
        server_dir='file_upload'
    
        def __init__(self, server_address, bind_and_activate=True):
            """Constructor.  May be extended, do not override."""
            self.server_address=server_address
            self.socket = socket.socket(self.address_family,
                                        self.socket_type)
            if bind_and_activate:
                try:
                    self.server_bind()
                    self.server_activate()
                except:
                    self.server_close()
                    raise
    
        def server_bind(self):
            """Called by constructor to bind the socket.
            """
            if self.allow_reuse_address:
                self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
            self.socket.bind(self.server_address)
            self.server_address = self.socket.getsockname()
    
        def server_activate(self):
            """Called by constructor to activate the server.
            """
            self.socket.listen(self.request_queue_size)
    
        def server_close(self):
            """Called to clean-up the server.
            """
            self.socket.close()
    
        def get_request(self):
            """Get the request and client address from the socket.
            """
            return self.socket.accept()
    
        def close_request(self, request):
            """Called to clean up an individual request."""
            request.close()
    
        def run(self):
            while True:
                self.conn,self.client_addr=self.get_request()
                print('from client ',self.client_addr)
                while True:
                    try:
                        head_struct = self.conn.recv(4)
                        if not head_struct:break
    
                        head_len = struct.unpack('i', head_struct)[0]
                        head_json = self.conn.recv(head_len).decode(self.coding)
                        head_dic = json.loads(head_json)
    
                        print(head_dic)
                        #head_dic={'cmd':'put','filename':'a.txt','filesize':123123}
                        cmd=head_dic['cmd']
                        if hasattr(self,cmd):
                            func=getattr(self,cmd)
                            func(head_dic)
                    except Exception:
                        break
    
        def put(self,args):
            file_path=os.path.normpath(os.path.join(
                self.server_dir,
                args['filename']
            ))
    
            filesize=args['filesize']
            recv_size=0
            print('----->',file_path)
            with open(file_path,'wb') as f:
                while recv_size < filesize:
                    recv_data=self.conn.recv(self.max_packet_size)
                    f.write(recv_data)
                    recv_size+=len(recv_data)
                    print('recvsize:%s filesize:%s' %(recv_size,filesize))
    
    
    tcpserver1=MYTCPServer(('127.0.0.1',8080))
    
    tcpserver1.run()
    
    server
    server
        
    import socket
    import struct
    import json
    import os
    
    
    
    class MYTCPClient:
        address_family = socket.AF_INET
    
        socket_type = socket.SOCK_STREAM
    
        allow_reuse_address = False
    
        max_packet_size = 8192
    
        coding='utf-8'
    
        request_queue_size = 5
    
        def __init__(self, server_address, connect=True):
            self.server_address=server_address
            self.socket = socket.socket(self.address_family,
                                        self.socket_type)
            if connect:
                try:
                    self.client_connect()
                except:
                    self.client_close()
                    raise
    
        def client_connect(self):
            self.socket.connect(self.server_address)
    
        def client_close(self):
            self.socket.close()
    
        def run(self):
            while True:
                inp=input(">>: ").strip()
                if not inp:continue
                l=inp.split()
                cmd=l[0]
                if hasattr(self,cmd):
                    func=getattr(self,cmd)
                    func(l)
    
    
        def put(self,args):
            cmd=args[0]
            filename=args[1]
            if not os.path.isfile(filename):
                print('file:%s is not exists' %filename)
                return
            else:
                filesize=os.path.getsize(filename)
    
            head_dic={'cmd':cmd,'filename':os.path.basename(filename),'filesize':filesize}
            print(head_dic)
            head_json=json.dumps(head_dic)
            head_json_bytes=bytes(head_json,encoding=self.coding)
    
            head_struct=struct.pack('i',len(head_json_bytes))
            self.socket.send(head_struct)
            self.socket.send(head_json_bytes)
            send_size=0
            with open(filename,'rb') as f:
                for line in f:
                    self.socket.send(line)
                    send_size+=len(line)
                    print(send_size)
                else:
                    print('upload successful')
    
    
    
    
    client=MYTCPClient(('127.0.0.1',8080))
    
    client.run()
    
    client
    client

    多线程

    基于多进程实现并发的套接字通信,需要不断开启新进程来服务

    from socket import *
    from multiprocessing import Process
    
    def talk(conn):
        while True:
            try:
                data = conn.recv(1024)
                if not data: break
                conn.send(data.upper())
            except ConnectionResetError:
                break
    
        conn.close()
    
    
    def server(ip,port):
        server = socket(AF_INET, SOCK_STREAM)
        server.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
        server.bind((ip,port))
        server.listen(5)
    
        while True:
            conn, addr = server.accept()
            p = Process(target=talk, args=(conn,))
            p.start()
    
        server.close()
    
    
    if __name__ == '__main__':
        server('127.0.0.1', 8080)
    server
    from socket import *
    
    client=socket(AF_INET,SOCK_STREAM)
    client.connect(('127.0.0.1',8080))
    
    while True:
        msg=input('>>: ').strip()
        if not msg:continue
    
        client.send(msg.encode('utf-8'))
        data=client.recv(1024)
        print(data.decode('utf-8'))
    client

     1,基于多线程的并发套接字通信

      随着客户端数目的增加,服务端主机可能瘫痪

      
    # 并发多线程
    from socket import *
    from threading import Thread
    
    def communicate(conn):
        while True:
            try:
                data = conn.recv(1024)
                if not data: break
                conn.send(data.upper())
            except ConnectionResetError:
                break
    
        conn.close()
    
    
    def server(ip, port):
        server = socket(AF_INET, SOCK_STREAM)
        server.bind((ip, port))
        server.listen(5)
    
        while True:
            conn, addr = server.accept()
            t = Thread(target=communicate,args=(conn,))
            t.start()
    
        server.close()
    
    
    if __name__ == '__main__':
        server('127.0.0.1', 8081)
    server
      
    from socket import *
    
    client=socket(AF_INET,SOCK_STREAM)
    client.connect(('127.0.0.1',8081))
    
    
    while True:
        msg = input('>>: ').strip()
        if not msg: continue
        client.send(msg.encode('utf-8'))
        data = client.recv(1024)
        print(data.decode('utf-8'))
    
    client.close()
    client

    l

  • 相关阅读:
    【2018.05.05 C与C++基础】C++中的自动废料收集:概念与问题引入
    【2018.04.27 C与C++基础】关于switch-case及if-else的效率问题
    【2018.04.19 ROS机器人操作系统】机器人控制:运动规划、路径规划及轨迹规划简介之一
    March 11th, 2018 Week 11th Sunday
    March 10th, 2018 Week 10th Saturday
    March 09th, 2018 Week 10th Friday
    March 08th, 2018 Week 10th Thursday
    March 07th, 2018 Week 10th Wednesday
    ubantu之Git使用
    AMS分析 -- 启动过程
  • 原文地址:https://www.cnblogs.com/wuqiuming/p/9489027.html
Copyright © 2011-2022 走看看