zoukankan      html  css  js  c++  java
  • 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()
    客户端
  • 相关阅读:
    739. Daily Temperatures
    556. Next Greater Element III
    1078. Occurrences After Bigram
    1053. Previous Permutation With One Swap
    565. Array Nesting
    1052. Grumpy Bookstore Owner
    1051. Height Checker
    数据库入门及SQL基本语法
    ISCSI的概念
    配置一个IP SAN 存储服务器
  • 原文地址:https://www.cnblogs.com/zhang-can/p/7197460.html
Copyright © 2011-2022 走看看