zoukankan      html  css  js  c++  java
  • python之socket编程

    一、socket简介

    socket(套接字)是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口,将复杂的TCP/IP协议族隐藏在接口后面,让socket去组织数据以符合指定的协议。

    如下左图为socket在tcp/ip协议中的角色,右图为socket的工作流程。

        

     二、socket分类

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

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

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

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

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

    三、基于TCP的socket

    #server端
    from socket import *
    phone = socket(AF_INET,SOCK_STREAM)  #创建socket,第一个参数指定socket家族,第二个指定类型,SOCK_STREAM为tcp,SOCK_DGRAM为UDP
    phone.bind(('127.0.0.1',8000)) #socket绑定ip和端口,ip应该是本机地址
    phone.listen(5)  #socket开启监听,此时触发三次握手,参数表示可以挂起的请求个数
    while True:
        conn,addr = phone.accept() #接收客户端连接,阻塞直至客户端发送消息
       while True:
        try:
            msg = conn.recv(1024)  #接收客户端消息
            print('收到客户端的消息:',msg)
            conn.send(msg.upper()) #向客户端发送消息
        except Exception:
               break
        conn.close()  #关闭连接
    phone.close()  #关闭socket
    tcp-server端
    #client端
    from socket import *
    phone = socket(AF_INET,SOCK_STREAM)  #创建客户端socket
    phone.connect(('127.0.0.1',8000))  #socket连接服务端,ip为服务端地址
    while True:
        msg = input('请输入').strip()
        phone.send(msg.encode('utf-8'))  #向服务端发送消息
        msg = phone.recv(1025)   #接收服务端消息
        print('收到服务端的消息',msg)
    phone.close()  #关闭客户端socket,触发四次挥手
    tcp-client端

    1.基于TCP的socket的工作流程

    server端流程:创建socket→绑定ip和端口→开启监听→接收连接→收/发消息→关闭连接→关闭socket

    client端流程:创建socket→连接服务端→收/发消息→关闭连接

    2.关于TCP的socket的一些解释说明

    由于tcp是基于连接的,因此必须先启动服务端,然后再启动客户端去连接服务端。

    由于socket是基于tcp/ip协议的,发送和接收消息必须是二进制数据,因此客户端需要通过encode('utf-8')去进行编码

    对于服务端:

    • accept的返回值为两部分,第一部分为一个连接,第二部分为客户端的ip和端口,值如下

      <socket.socket fd=224, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8080), raddr=('127.0.0.1', 58317)>
      ('127.0.0.1', 58317)

    • 外层的while True循环是为了能够接受多个客户端的请求,否则只能建立一个连接
    • 内层的while True循环是为了能够与同一个客户端进行多次收发消息,否则只能接收和发送一次消息
    • 内层循环中的try···except异常处理,是为了防止一个客户端异常终止连接后conn失效导致服务端程序崩溃

    在linux系统中,如果服务端程序关闭后再马上启动,可能会报ip地址被占用,这是因为四次挥手需要时间。可以在服务端的bind操作前增加phone.setsockopt(SOL_SOCKET,SO_REUSEADDR,1).

    四、基于UDP的socket

    #server端
    from socket import *
    ip_port = ('127.0.0.1',8000)
    buffer_size = 1024
    udp_server = socket(AF_INET,SOCK_DGRAM)
    udp_server.bind(ip_port)
    while True:
        msg,addr = udp_server.recvfrom(buffer_size)
        print(msg)
        udp_server.sendto(msg.upper(),addr)
    udp-server端
    #client端
    from socket import *
    ip_port = ('127.0.0.1',8000)
    buffer_size = 1024
    udp_client = socket(AF_INET,SOCK_DGRAM)
    while True:
        msg = input('请输入-->').strip()
        udp_client.sendto(msg.encode('utf-8'),ip_port)
        msg,addr = udp_client.recvfrom(buffer_size)
        print(msg)
    udp-client端

    1.基于UDP的socket的工作流程

    server端流程:创建socket→绑定ip和端口→收/发消息

    client端流程:创建socket→收/发消息(发消息需指定服务端ip和端口)

    2.关于UDP的socket的一些解释说明

    对于UDP的socket,由于无连接因此无需进行监听。

    基于UDP的发送和接收数据,接收需要使用recvfrom(),发送需要使用sendto('二进制数据',对方ip和端口)

    tcp的socket的recv()得到的数据就是发送的字符串,udp的socket的recvfrom()得到的数据是一个元组,元组中第一个值为发送的字符串,第二个值为发送端ip和端号。

    五、socket的粘包现象

    1.tcp和udp协议发送数据的过程

    TCP(transport control protocol,传输控制协议)是面向连接的,面向流的,提供高可靠性服务,收发两端(客户端和服务器端)都要有一一成对的socket。发送端为了将多个包更有效地发往接收端,使用了优化方法(Nagle算法)将多次间隔较小且数据量较小的数据合并成一个大的数据块,然后进行封包;这样接收端就难于分辨出来数据块中的界限,必须提供科学的拆包机制, 即面向流的通信是无消息保护边界的。

    UDP(user datagram protocol,用户数据报协议)是无连接的,面向消息的,提供高效率服务,不会使用块的合并优化算法。由于UDP支持的是一对多的模式,所以接收端的skbuff(套接字缓冲区)采用了链式结构来记录每一个到达的UDP包,在每个UDP包中就有了消息头(消息来源地址,端口等信息),这样对于接收端来说就容易进行区分处理了,即面向消息的通信是有消息保护边界的。

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

    2.粘包

    粘包只发生在tcp协议中。由于tcp协议数据不会丢,如果一次没有接收完数据包,那么下次接收会从上次接收完的地方继续接收,并且己端总是在收到ack时才会清除缓冲区内容。数据是可靠的,但是会粘包,粘包的发生有以下两种情况。

    ①发送端在短时间内多次发送较小数据,实际会按照优化算法合并发送

    from socket import *
    ip_port = ('127.0.0.1',8002)
    buffer_size = 1024
    back_log = 5
    
    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()
        data1 = conn.recv(buffer_size)
        print('第一次接收数据',data1)
        data2 = conn.recv(buffer_size)
        print('第一次接收数据', data2)
        data3 = conn.recv(buffer_size)
        print('第一次接收数据', data3)
    
    conn.close()
    tcp_server.close()
    tcp-server端
    from socket import *
    ip_port = ('127.0.0.1',8002)
    
    tcp_client = socket(AF_INET,SOCK_STREAM)
    tcp_client.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)
    tcp_client.connect(ip_port)
    
    tcp_client.send('hello'.encode('utf-8'))
    tcp_client.send('world'.encode('utf-8'))
    tcp_client.send('happy'.encode('utf-8'))
    tcp_client.send('python'.encode('utf-8'))
    tcp_client.close()
    tcp-client端

    执行结果如下,可见在基于tcp的socket中,一次recv并不对应一次send,send是向自身缓冲区发送数据,recv也是从自身缓冲区获取数据,recv和send没有对应关系。

    而udp协议中的recvfrom和sendto是一一对应的关系,如果超出缓冲区大小接收方直接丢弃。

    ②接收端一次接收的数据小于发送数据,下次接收时会从上次接收完的地方继续接收

    from socket import *
    ip_port = ('127.0.0.1',8002)
    back_log = 5
    
    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()
        data1 = conn.recv(2)
        print('第一次接收数据',data1)
        data2 = conn.recv(5)
        print('第一次接收数据', data2)
        data3 = conn.recv(6)
        print('第一次接收数据', data3)
    
    conn.close()
    tcp_server.close()
    tcp-server端
    from socket import *
    ip_port = ('127.0.0.1',8002)
    
    tcp_client = socket(AF_INET,SOCK_STREAM)
    tcp_client.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)
    tcp_client.connect(ip_port)
    
    tcp_client.send('helloworldhappypython'.encode('utf-8'))
    tcp_client.close()
    tcp-client端

    执行结果如下,可见第二次和第三次都是在上一次接收的地方继续接收数据的。

    3.解决粘包

    以上发生粘包的两种情况,本质都是接收端不知道发送端发送数据的大小,导致接收时获取的数据大小与发送的不一致。因此可以在发送端发送数据时,同时将数据大小也发送过去,接收端根据这个大小去获取发送的数据。

    发送数据大小的实现方法:发送端先计算出数据的大小,将这个整型数字通过struct.pack('i',l)打包成4个字节的二进制,然后发送打包后的这4个字节,再发送实际数据。在实际发送时这两部分会发生粘包一起发送。接收端先获取4个字节的,再通过struct.unpack('i',l)解包拿到实际数据的大小。

    from socket import *
    import subprocess,struct
    ip_port = ('127.0.0.1',8001)
    buffer_size = 1024
    back_log = 5
    
    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:
                cmd = conn.recv(buffer_size)
                if not cmd:break  #如果发送的命令为空,终止当前循环等待下一个连接进入
                print('收到客户端的命令为',cmd.decode('utf-8'))
                res = subprocess.Popen(cmd.decode('utf-8'),shell=True,
                                 stdout = subprocess.PIPE,stdin = subprocess.PIPE,stderr = subprocess.PIPE)
                err = res.stderr.read()
                if err:
                    cmd_res = err  #如果err有内容,表示命令输入错误,执行结果就为err
                else:
                    cmd_res = res.stdout.read()  #如果err无内容,表示命令执行成功,执行结果为标准输出的内容
                if not cmd_res: #如果命令执行成功但是没有输出,例如cd,返回一个执行成功
                    cmd_res = '执行成功'.encode('gbk')
                cmd_res_length = len(cmd_res)  #获取执行结果的长度
                pack_cmd_res = struct.pack('i',cmd_res_length)  #将长度打包成4个字节的二进制形式
                conn.send(pack_cmd_res)
                conn.send(cmd_res)  #4个字节会和实际发送数据粘包一起发送
            except Exception as e:
                print(e)
                break
    解决粘包:tcp-server端
    from socket import *
    import struct
    ip_port = ('127.0.0.1',8001)
    buffer_size = 1024
    back_log = 5
    
    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'))
        cmd_res_length = struct.unpack('i',tcp_client.recv(4))[0]  #先接受4个字节并unpack,获取执行结果的长度
        cmd_res = b''
        tmp_length = 0
        while tmp_length < cmd_res_length: #如果临时长度小于执行结果的长度,则循环接受buffer_size大小的数据
            cmd_res += tcp_client.recv(buffer_size)
            tmp_length = len(cmd_res)
        print('命令执行结果为',cmd_res.decode('gbk'))
    
    tcp_client.close()
    解决粘包:tcp-client端

    六、tcp实现并发

    1.socket实现tcp并发

    由于udp无连接故可实现并发,而上面几个关于tcp的socket的例子无法实现并发,即服务端如果已经接受一个连接,其他的连接无法进入,必须在当前连接中断后才可重新建立连接。通过socketserver可实现tcp的并发。socketserver需要自定义一个继承socketserver.BaseRequestHandler的类,并在类中定义一个handle方法;通过socketserver建立多线程或多进程的连接,并通过serve_forever实现多连接。

    import socketserver
    
    class MyServer(socketserver.BaseRequestHandler):
        def handle(self):  #必须自定义一个handle()方法
            print('conn:',self.request)  #相当于accept()返回的conn
            print('addr:',self.client_address)  #相当于accept()返回的addr
            while True:
                try:
                    data = self.request.recv(1024)
                    print('接收到的客户端消息:',data)
                    self.request.sendall(data.upper())
                except Exception as e:
                    print(e)
                    break
    
    if __name__ == '__main__':
        # 实例化一个对象,建立连接
        s = socketserver.ThreadingTCPServer(('127.0.0.1',8005),MyServer)  # 多线程
        # s = socketserver.ForkingTCPServer(('127.0.0.1',8005),MyServer) 多进程,系统开销多余多线程,常用ThreadingTCPServer
        print(s.server_address)  #相当于实例化传入的第一个参数
        print(s.RequestHandlerClass)  #实例化传入的第二个参数
        print(MyServer)
        print(s.socket)   #socket对象
        s.serve_forever()
    tcp实现并发:tcp_server
    from socket import *
    
    tcp_client = socket(AF_INET,SOCK_STREAM)
    tcp_client.connect(('127.0.0.1',8005))
    
    while True:
        msg = input('请输入:').strip()
        if not msg :
            continue
        elif msg == 'quit':
            break
        else:
            tcp_client.send(msg.encode('utf-8'))
            data = tcp_client.recv(1024)
            print('接收到的服务端消息:',data.decode('utf-8'))
    
    tcp_client.close()
    tcp实现并发:tcp_client

    将上述tcp_client复制多份,可以发现tcp_server可同时接收多个client的请求并成功返回。

    2.socket实现udp并发

    import socketserver
    
    class MyServer(socketserver.BaseRequestHandler):
        def handle(self):
            print(self.request)
    
            try:
                data = self.request[0]
                print('接收到的客户端消息:', data)
                self.request[1].sendto(data.upper(),self.client_address)
            except Exception as e:
                print(e)
    
    if __name__ == '__main__':
        s = socketserver.ThreadingUDPServer(('127.0.0.1',8006),MyServer)
        s.serve_forever()
    socketserver实现udp并发:udp_server
    from socket import *
    
    udp_client = socket(AF_INET,SOCK_DGRAM)
    udp_client.connect(('127.0.0.1',8006))
    
    while True:
        msg = input('请输入:').strip()
        if not msg :
            continue
        elif msg == 'quit':
            break
        else:
            udp_client.sendto(msg.encode('utf-8'),('127.0.0.1',8006))
            data,addr = udp_client.recvfrom(1024)
            print('接收到的服务端消息:',data.decode('utf-8'))
    
    udp_client.close()
    socketserver实现udp并发:udp_client

    将上述udp_client复制多份,udp_server也可同时接收多个client的请求并成功返回。

    3.socketserver对于tcp和udp的区别

    对于tcp中自定义的类,self.request表示连接(即相当于accept()返回的conn),需要再调用recv()去接收数据

    对于udp中自定义的类,self.request为一个元组,元组中的第一个元素为接收的数据,第二个元素为socket对象,即self.request[0]为接收数据,通过self.request[0].sendto('xxx',self.client_address)去发送数据

    两者的self.client_address都表示客户端的ip和端口

  • 相关阅读:
    带你看懂MySQL执行计划
    MySQL rename table方法大全
    MySQL慢日志全解析
    mybatis-通用mapper
    springboot-数据访问
    docker-1常用命令
    springboot-错误处理
    springboot-配置1
    react--temp
    了解react记录2-生命周期
  • 原文地址:https://www.cnblogs.com/Forever77/p/10966483.html
Copyright © 2011-2022 走看看