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和端口

  • 相关阅读:
    Java vs Python
    Compiled Language vs Scripting Language
    445. Add Two Numbers II
    213. House Robber II
    198. House Robber
    276. Paint Fence
    77. Combinations
    54. Spiral Matrix
    82. Remove Duplicates from Sorted List II
    80. Remove Duplicates from Sorted Array II
  • 原文地址:https://www.cnblogs.com/Forever77/p/10966483.html
Copyright © 2011-2022 走看看