zoukankan      html  css  js  c++  java
  • python学习笔记-socket编程

    socket介绍

    客户端/服务器架构

    1.硬件C/S架构(打印机)

    2.软件C/S架构

    C/S架构与socket的关系:

    我们学习socket就是为了完成C/S架构的开发

    osi七层

    socket层位置

    套接字定义

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

    所以,我们无需深入理解tcp/udp协议,socket已经为我们封装好了,我们只需要遵循socket的规定去编程,写出的程序自然就是遵循tcp/udp标准的。

    套接字分类

    套接字有两种(或者称为有两个种族),分别是基于文件型的和基于网络型的。 

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

    套接字家族的名字:AF_UNIX

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

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

    套接字家族的名字:AF_INET

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

    套接字工作流程

     先从服务器端说起。服务器端先初始化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)
    服务端套接字函数
    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() 创建一个与该套接字相关的文件

    基于TCP的套接字

    例子1

    #服务端
    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()  #接受客户端链接
    
    msg=conn.recv(1024)#收消息
    print("客户端发来的消息是",msg)
    conn.send(msg.upper())#发消息
    
    conn.close()  #关闭客户端套接字
    phone.close() #关闭服务器套接字
    
    #客户端
    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)
    
    phone.close()

    补充:

    1、tcp三次握手和四次挥手

    三次握手

    四次挥手

    为什么是三次握手四次挥手,握手的时候只是建立双向的连接,服务器给客户端发送的syn和ack可以合并,而挥手的时候涉及到收发消息,客户端发送完了发送一个fin
    但是服务端给客户端的消息还没发送完,所有服务端给客户端发送的fin和ack不能合并,挥手是四次。

    2、有的重启服务端时可能遇到报错Address already in use 

     这个是由于你的服务端仍然存在四次挥手的time_wait状态在占用地址,可以通过增加一条socket配置重用ip和端口解决,或者修改系统内核

    如:

    phone=socket(AF_INET,SOCK_STREAM)
    phone.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) #就是它,在bind前加
    phone.bind(('127.0.0.1',8080))

    客户端服务端循环收发消息

    例子2 

    #服务端
    from socket import *
    ip_port=("127.0.0.1",8000)
    back_log=5
    buffer_size=1024
    
    tcp_server=socket(AF_INET,SOCK_STREAM)
    tcp_server.bind(ip_port)
    tcp_server.listen(back_log)
    print("服务端开始运行了")
    conn,addr=tcp_server.accept()
    print("双向链接是",conn)
    print("客户端地址",addr)
    while True:
        data=conn.recv(buffer_size)#收消息
        print("客户端发来的消息是",data.decode("utf-8"))
        conn.send(data.upper())#发消息
    
    conn.close()
    tcp_server.close()
    
    #客户端
    from socket import *
    ip_port=("127.0.0.1",8000)
    buffer_size=1024
    
    tcp_client=socket(AF_INET,SOCK_STREAM)
    
    tcp_client.connect(ip_port)
    while True:
        msg=input(">>").strip()
        tcp_client.send(msg.encode("utf-8"))
        print("客户端已将发送消息")
        data=tcp_client.recv(buffer_size)
        print("收到服务器发来的消息",data.decode("utf-8"))
    
    tcp_client.close()

    补充:

    socket原理
    收发都是从自己的的缓存区操作数据,如果发送None,缓存区就没有存入输入,不会发送到服务器端,卡住。

    服务端循环连接请求来收发消息

     例子3

    #服务端
    from socket import *
    ip_port=("127.0.0.1",8000)
    back_log=5
    buffer_size=1024
    
    tcp_server=socket(AF_INET,SOCK_STREAM)
    tcp_server.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)#有的出现地址被占用的异常,加这句可解决表示重新使用原来的地址,两一个方法是修改操作系统配置linux
    tcp_server.bind(ip_port)
    tcp_server.listen(back_log)
    
    while True:   #服务器无限循环
        print("服务端开始运行了")
        conn,addr=tcp_server.accept()
        print("双向链接是",conn)
        print("客户端地址",addr)
        while True:   #通讯循环
            try:
                data=conn.recv(buffer_size)#收消息
                if not data: continue
                print("客户端发来的消息是",data.decode("utf-8"))
                conn.send(data.upper())#发消息
            except Exception:
                break
    
        conn.close()
    tcp_server.close()
    from socket import *
    ip_port=("127.0.0.1",8000)
    back_log=5
    buffer_size=1024
    
    tcp_client=socket(AF_INET,SOCK_STREAM)
    
    tcp_client.connect(ip_port)
    while True:
        msg=input(">>").strip()
        tcp_client.send(msg.encode("utf-8"))
        print("客户端已发送消息")
        data=tcp_client.recv(buffer_size)
        print("收到服务器发来的消息",data.decode("utf-8"))
    
    tcp_client.close()
    客户端1
    from socket import *
    ip_port=("127.0.0.1",8000)
    back_log=5
    buffer_size=1024
    
    tcp_client=socket(AF_INET,SOCK_STREAM)
    
    tcp_client.connect(ip_port)
    while True:
        msg=input(">>").strip()
        tcp_client.send(msg.encode("utf-8"))
        print("客户端已发送消息")
        data=tcp_client.recv(buffer_size)
        print("收到服务器发来的消息",data.decode("utf-8"))
    
    tcp_client.close()
    客户端2

    此时服务端同一时间只能跟一个客户端通信,并发在下面章节socketserver中涉及

    基于tcp实现远程执行命令

    例子4

    #服务端
    from socket import *
    import subprocess
    ip_port=("192.168.31.128",8000)
    back_log=5
    buffer_size=1024
    
    tcp_server=socket(AF_INET,SOCK_STREAM)
    tcp_server.bind(ip_port)
    tcp_server.listen(back_log)
    
    while True:
        conn,addr=tcp_server.accept()
        print("新的客户端地址",addr)
        while True:
            try: #解决客户端非正常关闭报错
                cmd=conn.recv(buffer_size)#收消息
                if not cmd:break #解决quit时出现死循环
                print("收到客户端发来的命令",cmd)
                #执行命令,得到命令的运行结果
                res=subprocess.Popen(cmd.decode("utf-8"),shell=True,
                                     stderr=subprocess.PIPE,
                                     stdout=subprocess.PIPE,
                                     stdin=subprocess.PIPE)
                err=res.stderr.read()
                if err:
                    cmd_res=err
                else:
                    cmd_res=res.stdout.read()
    
                if not cmd_res:
                    cmd_res="执行成功".encode("utf-8")
    
                conn.send(cmd_res)
            except Exception as e:
                print(e)
                break
    
        conn.close()
    tcp_server.close()
    
    #客户端
    from socket import *
    ip_port=("192.168.31.128",8000)
    buffer_size=1024
    
    tcp_client=socket(AF_INET,SOCK_STREAM)
    
    tcp_client.connect(ip_port)
    while True:
        cmd=input(">>").strip()
        if not cmd:continue
        if cmd=="quit":break
    
        tcp_client.send(cmd.encode("utf-8"))
        print("客户端已将发送消息")
        cmd_res=tcp_client.recv(buffer_size)
        print("命令的执行结果",cmd_res.decode("utf-8"))#win默认的是返回的gbk编码的内容,在win平台下用gbk解码
    
    tcp_client.close()

    补充:

    1、subprocess模块用法

    import subprocess
    subprocess.Popen("dir",shell=True,stdout=subprocess.PIPE,stdin=subprocess.PIPE,stderr=subprocess.PIPE)

    2、linux内socket服务器无法连接windows

    centos8关闭防火墙

    查看状态:systemctl status firewalld

    启动: systemctl start firewalld

    关闭: systemctl stop firewalld

    基于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()

    例子:

    #服务端
    from socket import *
    ip_port=("127.0.0.1",8080)
    buffer_size=1024
    
    udp_server=socket(AF_INET,SOCK_DGRAM) #数据报
    udp_server.bind(ip_port)
    
    while True:
        data,addr=udp_server.recvfrom(buffer_size)
        print(data.decode("utf-8"))
        udp_server.sendto(data.upper(),addr)
    
    #客户端
    from socket import *
    ip_port=("127.0.0.1",8080)
    buffer_size=1024
    
    udp_clientr=socket(AF_INET,SOCK_DGRAM) #数据报
    
    while True:
        msg=input(">>")
        udp_clientr.sendto(msg.encode("utf-8"),ip_port)#发包的时候要指定IP和端口
        data,addr=udp_clientr.recvfrom(buffer_size)
        print(data.decode("utf-8"))

     粘包

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

    只有tcp有粘包现象,udp没有粘包

    udp是面向消息的协议。udp只管发,不管对方是否收到。可以单独运行udp客户端。udp不是基于连接的

    粘包是由TCP协议本身造成的,TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一个TCP段。若连续几次需要send的数据都很少,通常TCP会根据优化算法把这些数据合成一个TCP段后一次发送出去,这样接收方就收到了粘包数据。

    tcp是基于数据流的,于是收发的消息不能为空,这就需要在客户端和服务端都添加空消息的处理机制,防止程序卡住,而udp是基于数据报的,即便是你输入的是空内容(直接回车),那也不是空消息,udp协议会帮你封装上消息头

    udp的recvfrom是阻塞的,一个recvfrom(x)必须对唯一一个sendinto(y),收完了x个字节的数据就算完成,若是y>x数据就丢失,这意味着udp根本不会粘包,但是会丢数据,不可靠

    tcp的协议数据不会丢,没有收完包,下次接收,会继续上次继续接收,己端总是在收到ack时才会清除缓冲区内容。数据是可靠的,但是会粘包。

    两种情况产生粘包

    1、发送端需要等缓冲区满才发送出去,造成粘包(发送数据时间间隔很短,数据了很小,会合到一起,产生粘包)

    例子:粘包

    #服务端
    from socket import *
    ip_port=("127.0.0.1",8080)
    back_log=5
    buffer_size=1024
    
    tcp_server=socket(AF_INET,SOCK_STREAM)
    tcp_server.bind(ip_port)
    tcp_server.listen(back_log)
    conn,addr=tcp_server.accept()
    
    data1=conn.recv(buffer_size)
    print("第一次数据",data1)
    
    data2=conn.recv(buffer_size)
    print("第二次数据",data2)
    
    data3=conn.recv(buffer_size)
    print("第三次数据",data3)
    
    #客户端
    from socket import *
    ip_port=("127.0.0.1",8080)
    back_log=5
    buffer_size=1024
    
    tcp_client=socket(AF_INET,SOCK_STREAM)
    tcp_client.connect(ip_port)
    
    tcp_client.send("hello".encode("utf-8"))
    tcp_client.send("world".encode("utf-8"))
    tcp_client.send("marks".encode("utf-8"))

    结果为:

    第一次数据 b'helloworldmarks'
    第二次数据 b''
    第三次数据 b''

    例子2,UDP不粘包

    #客户端
    from socket import *
    ip_port=("127.0.0.1",8080)
    buffer_size=1024
    
    udp_client=socket(AF_INET,SOCK_DGRAM)
    
    udp_client.sendto(b"hello",ip_port)
    udp_client.sendto(b"world",ip_port)
    udp_client.sendto(b"steven",ip_port)
    
    #服务端
    from socket import *
    ip_port=("127.0.0.1",8080)
    buffer_size=1024
    
    udp_server=socket(AF_INET,SOCK_DGRAM)
    udp_server.bind(ip_port)
    
    data1=udp_server.recvfrom(1024)
    print("第一次",data1)
    
    data2=udp_server.recvfrom(1024)
    print("第二次",data2)
    
    data3=udp_server.recvfrom(1024)
    print("第三次",data3)
    View Code
    第一次 (b'hello', ('127.0.0.1', 55519))
    第二次 (b'world', ('127.0.0.1', 55519))
    第三次 (b'steven', ('127.0.0.1', 55519))
    结果

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

    例子:略

    补充:send(字节流)和recv(1024)及sendall

    recv里指定的1024意思是从缓存里一次拿出1024个字节的数据

    send的字节流是先放入己端缓存,然后由协议控制将缓存内容发往对端,如果待发送的字节流大小大于缓存剩余空间,那么数据丢失,用sendall就会循环调用send,数据不会丢失

    粘包解决方法

    low版:让发送端在发送数据前,把自己将要发送的字节流总大小让接收端知晓,然后接收端来一个死循环接收完所有数据

    例子1、

    #服务端
    from socket import *
    import subprocess
    ip_port=("127.0.0.1",8080)
    back_log=5
    buffer_size=1024
    
    tcp_server=socket(AF_INET,SOCK_STREAM)
    tcp_server.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)
    tcp_server.bind(ip_port)
    tcp_server.listen(back_log)
    
    while True:
        conn,addr=tcp_server.accept()
        print("客户端:",addr)
        while True:
            msg=conn.recv(buffer_size)
            if not msg:break
            res=subprocess.Popen(msg.decode("utf-8"),shell=True,
                                 stdin=subprocess.PIPE,
                                 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"))
            client_read=conn.recv(buffer_size).decode("utf-8")
            if client_read=="ready":
                conn.sendall(ret)
        conn.close()
    
    #客户端
    from socket import *
    ip_port=("127.0.0.1",8080)
    back_log=5
    buffer_size=1024
    
    tcp_client=socket(AF_INET,SOCK_STREAM)
    tcp_client.connect(ip_port)
    
    while True:
        cmd=input(">>:").strip()
        if not cmd:continue
        if cmd=="quit":break
    
        tcp_client.send(cmd.encode("utf-8"))
        #解决粘包
        length=tcp_client.recv(buffer_size)
        tcp_client.send(b"ready")
        length=int(length.decode("utf-8"))
    
        recv_size=0
        recv_msg=b""
        while recv_size<length:
            recv_msg+=tcp_client.recv(buffer_size)
            recv_size=len(recv_msg)
    
        print(recv_msg.decode("gbk"))

    使用struct:

    为字节流加上自定义固定长度报头,报头中包含字节流长度,然后一次send到对端,对端在接收时,先从缓存中取出定长的报头,然后再取真实数据

    例子2,

    #服务端
    from socket import *
    import subprocess
    import struct
    ip_port=("127.0.0.1",8080)
    back_log=5
    buffer_size=1024
    
    tcp_server=socket(AF_INET,SOCK_STREAM)
    tcp_server.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)
    tcp_server.bind(ip_port)
    tcp_server.listen(back_log)
    
    while True:
        conn,addr=tcp_server.accept()
        print("客户端:",addr)
        while True:
            try:
                msg=conn.recv(buffer_size)
                if not msg:break
                res=subprocess.Popen(msg.decode("utf-8"),shell=True,
                                     stdin=subprocess.PIPE,
                                     stderr=subprocess.PIPE,
                                     stdout=subprocess.PIPE)
                err=res.stderr.read()
                if err:
                    ret=err
                else:
                    ret=res.stdout.read()
                data_length=len(ret)
                data_length=struct.pack("i",data_length)
                conn.send(data_length)
                conn.sendall(ret)
            except Exception as e:
                print(e)
                break
        conn.close()
    
    
    #客户端
    from socket import *
    ip_port=("127.0.0.1",8080)
    back_log=5
    buffer_size=1024
    
    tcp_client=socket(AF_INET,SOCK_STREAM)
    tcp_client.connect(ip_port)
    
    while True:
        cmd=input(">>:").strip()
        if not cmd:continue
        if cmd=="quit":break
    
        tcp_client.send(cmd.encode("utf-8"))
        #解决粘包
        length_data=tcp_client.recv(4)
    
        length=struct.unpack("i",length_data)[0]
    
        recv_size=0
        recv_msg=b""
        while recv_size<length:
            recv_msg+=tcp_client.recv(buffer_size)
            recv_size=len(recv_msg)
    
        print(recv_msg.decode("gbk"))

    补充知识

    #iter补充
    l=["a","b","c","d"]
    
    def test():
        return l.pop()
    
    x=iter(test,"b") #第二个参数,表示遇到会停止
    print(x.__next__())
    print(x.__next__())
    print(x.__next__())
    
    #偏函数
    from functools import partial
    
    def add(x,y):
        return x+y
    
    func=partial(add,1)
    print(func(1))
    print(func(9))

    socketserver实现并发

    基于tcp的套接字,关键就是两个循环,一个链接循环,一个通信循环

    socketserver模块中分两大类:server类(解决链接问题)和request类(解决通信问题)

    server类:处理链接
    "BaseServer"
    "TCPServer"
    "UDPServer"
    "UnixStreamServer"
    "UnixDatagramServer"

    request类:处理通信
    BaseRequestHandler
    "StreamRequestHandle"
    "DatagramRequestHandle"

    server类:

     request类:

    继承关系

     

    对于tcp来说对应我们自己定义类中的

    self.server=套接字对象
    self.request=conn
    self.client_address=addr

    对于udp来说

    self.request=(data,self.socket)
    self.client_address=addr

    tcp并发

    例子

    #服务端
    import socketserver
    class MyServer(socketserver.BaseRequestHandler):
        def handle(self):
            print("conn is",self.request) #conn
            print("addr is",self.client_address) #addr
    
            while True:
                try:
                    #收消息
                    data=self.request.recv(1024)
                    if not data:break
                    print("收到客户端的消息是",data.decode("utf-8"))
                    #发消息
                    self.request.sendall(data.upper())
                except Exception as e:
                    print(e)
                    break
    
    if __name__=="__main__":
        s=socketserver.ThreadingTCPServer(("127.0.0.1",8080),MyServer)
        s.serve_forever()
    
    #客户端
    from socket import *
    import struct
    ip_port=("127.0.0.1",8080)
    back_log=5
    buffer_size=1024
    
    tcp_client=socket(AF_INET,SOCK_STREAM)
    tcp_client.connect(ip_port)
    
    while True:
        msg=input(">>:").strip()
        if not msg:continue
        if msg=="quit":break
    
        tcp_client.send(msg.encode("utf-8"))
        data=tcp_client.recv(buffer_size)
        print("收到服务器端发来的消息",data.decode("utf-8"))
    
    tcp_client.close()

    UDP并发

    例子

    #服务端
    import socketserver
    class MyServer(socketserver.BaseRequestHandler):
        def handle(self):
            print(self.request)
            print("收到客户端消息是",self.request[0])
            self.request[1].sendto(self.request[0].upper(),self.client_address)
    
    if __name__=="__main__":
        s=socketserver.ThreadingUDPServer(("127.0.0.1",8080),MyServer)
        s.serve_forever()
    
    #客户端
    from socket import *
    ip_port=("127.0.0.1",8080)
    buffer_size=1024
    
    udp_clientr=socket(AF_INET,SOCK_DGRAM) #数据报
    
    while True:
        msg=input(">>")
        udp_clientr.sendto(msg.encode("utf-8"),ip_port)#发包的时候要指定IP和端口
        data,addr=udp_clientr.recvfrom(buffer_size)
        print(data.decode("utf-8"))
  • 相关阅读:
    笨方法学python中执行argv提示ValueError: not enough values to unpack (expected 4, got 1)
    VMware workstation安装
    Redis bigkey分析
    MySQL drop table 影响及过程
    MySQL 大表硬连接删除
    ES elasticsearch 各种查询
    ES elasticsearch 各种聚合
    ES elasticsearch 聚合统计
    ES elasticsearch 实现 count单字段,分组取前多少位,以地理位置中心进行统计
    MySQL行溢出、varchar最多能存多少字符
  • 原文地址:https://www.cnblogs.com/steven223-z/p/12790919.html
Copyright © 2011-2022 走看看