• Python之socket网络编程


    1.什么是socket

    了解socket之前首先要了解osi七层模型,或者说五层模型:应用层,传输层,网络层,数据链路层,物理层

      其中,数据一定是转化成物理信号通过物理层的物理设备才能传输;但是我们不谈物理设备,我们今天的重点是 :网络上的两个程序怎么通过一个连接实现数据交换。怎么唯一标示一个程序呢,是通过ip+端口的方式。socket就是用来描述ip和端口的,可以理解socket为把tcp,udp协议封装成一组接口,我们不需要知道复杂的tcp/udp协议,只需要按照socket的规定编程,写出的程序自然就是遵循tcp/udp协议的。

      socket又被称为套接字,我们研究套接字只需要记住两件事:建立连接,收发数据,就行了在写代码之前我们还需要知道一个c/s模型,就是client/sever(客户端与服务端),交换数据至少得两个人吧,在网络编程中就是客户端和服务端。客户端向服务端发送请求,服务端响应客户端的请求。

    我们先写一个简单的基于tcp的socket程序:

    #服务端
    import socket
    
    phone_sever=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    phone_sever.bind(('127.0.0.1',8080))#绑定ip和端口,我们用的本地回环网卡演示
    phone_sever.listen(5)#监听,等待连接
    
    conn,addr=phone_sever.accept() #客户端连入
    ret=conn.recv(1024)#接收客户端发来的消息
    conn.close()#断开连接
    phone_sever.close()#服务端关闭
    #客户端
    import socket
    
    phone_client=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    phone_client.connect(('127.0.0.1',8080))#连接服务端的ip和端口
    phone_client.send('hello'.encode('utf-8'))#向服务端发送消息
    phone_client.close()#客户端关闭

    写好后,先运行服务端,在运行客户端,这样就成功写了一个socket。这是最简单的socket例子了,只是介绍了基本语法,我们下面优化一下代码,

    注意:传输的数据都应该是bytes类型

    2.基于TCP的套接字

    我们基于上面的例子,优化一下代码

      1.服务端应该是循环接收数据,并能循环发送数据

      2.客户端应该也能接收服务端的消息,并且也是循环收发

      3.服务端可以与多个客户端连接

      4.某一个客户端断开连接或者发生故障不能导致服务端崩溃

      5.顺便模拟一下客户端输入命令,服务端把命令的执行结果返回给客户端

      6.解决OSError:[Errno 48] Address already in use的问题

    #服务端
    import subprocess
    
    import socket
    phone_sever=socket.socket(socket.AF_INET,socket.SOCK_STREAM)#买手机
    phone_sever.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)#解决问题6,加上这句话就行
    phone_sever.bind(('127.0.0.1',8080))#这里是元组形式
    phone_sever.listen(5)
    print('sever run ')
    while True:#这个循环解决问题3
        conn,client_addr=phone_sever.accept()   
        print('客户端',client_addr)
        while True:#这个while循环解决问题1
            try:#捕捉异常,解决问题4
                cmd=conn.recv(1024)#收消息
                res=subprocess.Popen(cmd.decode('utf-8'),
                                    shell=True,
                                     stdout=subprocess.PIPE,
                                     stderr=subprocess.PIPE)#利用subprocess这个模块,解决问题5
                stdout=res.stdout.read()
                stderr=res.stderr.read()
                sever_res=stdout+stderr
                if not sever_res:
                    sever_res=b'is vaild'
                conn.sendall(sever_res)#发消息
            except Exception:
                break
        conn.close()#挂电话
    
    phone_sever.close()#关机
    #客户端
    import
    socket phone_client=socket.socket(socket.AF_INET,socket.SOCK_STREAM)#买手机 phone_client.connect(('127.0.0.1',8080))#拨号 # phone_client.connect(('192.168.16.253',8080))#拨号 while True:#这个循环解决问题2 msg=input('>>>>:') if not msg:continue#解决客户端输入空,服务端会死掉的问题 phone_client.send(msg.encode('utf-8')) server_res=phone_client.recv(1024) print('server_res:',server_res.decode('gbk')) phone_client.close()

    当我们在实验的时候,可能会遇到这种情况:客户端输入为空,服务端就卡住了。解释这个问题就要说到网络传输的原理了

    我们前面说了,两台机器之间传输数据一定是通过底层网卡等物理设备的,而应用程序是不能操作底层硬件的,这时就需要调用操作系统了,当用send发数据时,其实是由应用程序把数据发给操作系统,然后由操作系统把数据放到一块缓存上,然后由这块缓存再将数据发送给服务端的缓存,如果我们send的是一个空数据,那肯定就不会发送成功了,但是我们的客户端以为按了回车就发送了,就开始等待服务端的回应了,所以就卡住了。

    3.基于UDP的套接字

    #服务端
    import
    socket udp_sever=socket.socket(socket.AF_INET,socket.SOCK_DGRAM) udp_sever.bind(('127.0.0.1',8080)) while True: msg,addr=udp_sever.recvfrom(1024) print(msg,addr) udp_sever.sendto(msg.upper(),addr)
    #客户端
    import socket
    
    ip_port=('127.0.0.1',8080)
    udp_client=socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
    while True:
        msg=input('>>>>>>:').strip()
        if not msg:continue
        udp_client.sendto(msg.encode('utf-8'),ip_port)
        back_msg,addr=udp_client.recvfrom(1024)
        print(back_msg.decode('utf-8'),addr)

    udp不需要建立连接,所以可以实现与多个客户端同时建立连接。但是相比于tcp,udp是不可靠传输,但是速度快。可以把tcp协议理解为打电话,必须双方建立连接,然后说一句回一句。udp就像是发短信,客户端只关心消息有没有发出去就行了,不用关心对方有没有收到。具体udp的不可靠传输,下面会说到

    在代码方面的区别,tcp的recv就相当于udp的recvfrom,tcp的send就相当于udp的sendto,另外因为udp不建立连接,所以发送消息的时候需要指定ip和端口号

    4.粘包现象

    先来看我们前面写的tcp的代码,recv()括号里的1024,这个1024限制了接收消息的最大字节数,想象一下,如果我们接收的数据长度超过1024字节,为了便于观察,我们把这个接受消息的最大字节数改成10,代码如下,看看如果发送的消息超过10,接收端会发生什么:

     1 import subprocess
     2 
     3 import socket
     4 phone_sever=socket.socket(socket.AF_INET,socket.SOCK_STREAM)#买手机
     5 phone_sever.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
     6 phone_sever.bind(('127.0.0.1',8080))
     7 
     8 phone_sever.listen(5)
     9 
    10 print('sever run ')
    11 while True:
    12 
    13     conn,client_addr=phone_sever.accept()   
    14     print('客户端',client_addr)
    15     while True:
    16         try:
    17             cmd=conn.recv(10)#收消息
    18             print(cmd.decode('utf-8'))
    19             msg=input('>>>>>').strip()
    20             conn.send(msg.encode('utf-8'))
    21         except Exception:
    22             break
    23     conn.close()#挂电话
    24 
    25 phone_sever.close()#关机
    服务端
     1 import socket
     2 
     3 phone_client=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
     4 phone_client.connect(('127.0.0.1',8080))
     5 while   True:
     6     msg=input('>>>>:')
     7     if not msg:continue
     8     phone_client.send(msg.encode('utf-8'))
     9 
    10     server_res=phone_client.recv(10)#收消息
    11     print('server_res:',server_res.decode('gbk'))
    12 
    13 phone_client.close()
    客户端

    在发一条消息

      是不是,问题就来了。如果发送的消息超过设置的最大字节数,在tcp中就会分批发送,这样肯定是有问题的,这样就没办法保证我们数据的完整性了。我们先来解释一下这个现象:

      我们已经说过了,不管客户端还是服务端,他们都是处在应用层的应用程序,没有权利去操作硬件。所谓发送的数据都是要交给操作系统的,由操作系统调用网卡等硬件设备将消息转成物理信号发送出去。在这个过程中,操作系统需要把数据从用户态copy到内核态,才能调用网卡。这种频繁的copy,切换是很占系统资源的,为了解决这个问题,加了一个缓存的机制。等缓存中的数据满了或者超过了时间间隔,就会把数据发送出去。这个缓存默认是8K,也就是8192字节,所以我们最大设置的recv()括号里的数就是8192。还没说到点上,有点跑偏。假设缓存中有两个数据,a数据有100字节,b数据有50字节,用户设置的每次接收80字节,想想会发生什么,第一次收到了a数据的80字节,第二次呢,第二次就会收到a数据剩下的20字节,加上b数据的50字节。因为tcp是流式的,就像水桶里的水,不同的数据会黏在一起(当然并不是没有边界),这种现象就叫‘粘包’。这种现象在windows和linux中都有。

      然而udp中是不存在粘包现象的,因为udp并没有建立连接,udp遇到这种消息超出设置字节数的情况的处理办法是:直接丢弃。这也体现了udp的不可靠

      然后最重要的来了,怎么解决粘包问题。既然我们已经知道了粘包的原理,那就见招拆招呗,因为我们不知道对方发送的数据长度,所以才会出现这种问题,那就让对方发送数据的时候,把数据长度先发过来,然后在recv()括号里写上对方发送来的长度,但是有个问题,刚才说了缓存只有8K,所以这个括号里的数字最好不要超过8192,那就循环接收,一次收不完数据,就分几次接收,这是可行的。还有个问题,那个发送数据长度的消息,会不会发生粘包的问题呢,所以我们应该把这条消息写成是固定长度的。说到长数据,干脆发个视频文件吧,废话不多说,上代码:

     1 #服务端
     2 import socket
     3 import os,json
     4 
     5 #客户端请求服务端的文件,服务端确认后将文件发送给客户端
     6 sock = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
     7 sock.bind(("127.0.0.1",9999))
     8 sock.listen(1)
     9 
    10 def pack_msg_header(header,header_size):#制作文件头部
    11     bytes_header = bytes(json.dumps(header) ,encoding="utf-8")
    12 
    13     if len(bytes_header ) < header_size :#需要补充0
    14         header['fill'].zfill( header_size - len(bytes_header) )
    15         bytes_header = bytes(json.dumps(header), encoding="utf-8")
    16     return bytes_header
    17 
    18 while True:
    19 
    20     conn,addr = sock.accept() #等待、阻塞
    21     print("got a new customer",conn,addr)
    22 
    23     while True:
    24         raw_cmd = conn.recv(1024) # get test.log
    25         cmd,filename = raw_cmd.decode("utf-8").split()
    26         if cmd == "get":
    27             msg_header = {"fill": ''}
    28             if os.path.isfile(filename):
    29                 msg_header["size"] =  os.path.getsize(filename)
    30                 msg_header["filename"] =  filename
    31                 msg_header["ctime"] =  os.stat(filename).st_ctime
    32                 bytes_header = pack_msg_header(msg_header,300)#规定头文件长度为300
    33 
    34                 conn.send(bytes_header)
    35 
    36                 f = open(filename,"rb")
    37                 for line in f:
    38                     conn.send(line)
    39 
    40                 else:
    41                     print("file send done....")
    42                 f.close()
    43             else:
    44                 msg_header['error'] = "file %s on server does not exist " % filename
    45                 bytes_header = pack_msg_header(msg_header, 300)
    46                 conn.send(bytes_header)
    47 
    48 
    49 sock.close()
    服务端
     1 #客户端
     2 import socket
     3 import json
     4 
     5 sock = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
     6 sock.connect(('12.0.0.7',9999))
     7 
     8 while True:
     9     cmd = input(">>>:").strip()
    10     if not cmd : continue
    11     sock.send(cmd.encode("utf-8"))
    12     msg_header = sock.recv(300)#规定头文件的长度为300
    13     print("received:",msg_header.decode("gbk"))
    14 
    15     header = json.loads(msg_header.decode("utf-8"))
    16     if header.get("error"):
    17         print(header.get("error"))
    18     else:
    19         filename = header['filename']
    20         file_size = header['size']
    21         f = open(filename,"wb")
    22         received_size = 0
    23 
    24         while received_size < file_size :
    25             if file_size - received_size < 8192:#循环接收的最后一次
    26                 data = sock.recv(file_size - received_size)
    27             else:
    28                 data = sock.recv(8192)
    29 
    30             received_size += len(data)
    31             f.write(data)
    32         else:
    33             print("file receive done....",filename,file_size)
    34             f.close()
    35 
    36 
    37 sock.close()
    客户端
  • 相关阅读:
    技术分享 | web自动化测试文件上传与弹框处理
    技术分享 | web 控件的交互进阶
    技术分享 | web自动化测试执行 JavaScript 脚本
    技术分享 | 想做App测试就一定要了解的App结构
    技术分享 | 网页 frame 与多窗口处理
    机械键盘转蓝牙键盘
    手动验证 TLS 证书
    因为一句话,秒懂二叉树旋转
    【Linux】本地虚拟机使用ssh服务
    查看SO KO 执行程序相关信息命令
  • 原文地址:https://www.cnblogs.com/zhang-can/p/7197460.html
Copyright © 2020-2023  润新知