Python之网络编程

一、引子

问题1:如果你写了两个python文件a.py和b.py,分别去运行,那你就会发现,这两个python文件分别运行的很好。但是如果两个程序之间互相传输数据,那你要怎么做呢?

问题2:如果a.py和b.py分别在不同电脑上的时候,互相传递数据,你要怎那么做呢?

类似的机制有计算机网盘,qq等等。我们可以在我们的电脑和别人聊天,可以在自己的电脑上向网盘中上传,下载数据。这些都是两个程序在通信。

二、软件开发架构

我们了解的涉及到两个程序之间通讯的应用大致可以分为两种:

  1. 应用类:qq、微信,网盘,优酷等都属于需要安装的桌面应用
  2. web类:比如百度、知乎、博客园等使用浏览器访问就可以直接使用的应用

这些应用的本质其实都是两个程序之间的通讯。而这两个分类又对应了两个软件开发的架构~

C/S架构

C/S即:Client与Server ,中文意思:客户端与服务器端架构,这种架构也是从用户层面(也可以是物理层面)来划分的。

这里的客户端一般泛指客户端应用程序EXE,程序需要先安装后,才能运行在用户的电脑上,对用户的电脑操作系统环境依赖较大。

B/S架构

B/S即:Browser与Server,中文意思:浏览器端与服务器端架构,这种架构是从用户层面来划分的。

Browser浏览器,其实也是一种Client客户端,只是这个客户端不需要大家去安装什么应用程序,只需在浏览器上通过HTTP请求服务器端相关的资源(网页资源),客户端Browser浏览器就能进行增删改查。

三、网络基础

一个程序如何在网络上找到另一个程序?

首先,程序必须要启动,其次,必须有这台机器的地址,我们都知道我们人的地址大概就是国家\省\市\区\街道\楼\门牌号这样字。那么每一台联网的机器在网络上也有自己的地址,它的地址是怎么表示的呢?

就是使用一串数字来表示的,例如:1.0.0.1

接下来需要普及下知识了:

  1. 什么是IP地址

IP地址是指互联网协议地址(英语:Internet Protocol Address,又译为网际协议地址),是IP Address的缩写。IP地址是IP协议提供的一种统一的地址格式,它为互联网上的每一个网络和每一台主机分配一个逻辑地址,以此来屏蔽物理地址的差异。

IP地址是一个32位的二进制数,通常被分割为4个“8位二进制数”(也就是4个字节)。IP地址通常用“点分十进制”表示成(a.b.c.d)的形式,其中,a,b,c,d都是0~255之间的十进制整数。例:点分十进IP地址(100.4.5.6),实际上是32位二进制数(01100100.00000100.00000101.00000110)。

  1. 什么是端口

"端口"是英文port的意译,可以认为是设备与外界通讯交流的出口。

ps:因此ip地址精确到具体的一台电脑,而端口精确到具体的程序。

osi五层模型详解

链接:http://www.cnblogs.com/baishuchao/articles/9105303.html

四、套接字(socket)的发展史

套接字起源于20世纪70年代加利福大学伯克利分校版本的Unix,即人们所说的BSD Unix。因此,有时人们也把套接字称为"伯克利套接字”或“BSD 套接字"。一开始,套接字被设计用在同一台主机上多个应用程序之间的通讯。这也被称进程间通讯或IPC。套接字有两种(或者称为有两个种族),分别是基于文件型的和基于网络型的。

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

套接字家族的名字:AF_UNIX

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

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

套接字家族的名字:AF_INET

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

五、tcp协议和udp协议

TCP(Transmission Control Protocol)可靠的、面向连接的协议(eg:打电话)、传输效率低全双工通信(发送缓存&接收缓存)、面向字节流。使用TCP的应用:Web浏览器;电子邮件、文件传输程序。

UDP(User Datagram Protocol)不可靠的、无连接的服务,传输效率高(发送前时延小),一对一、一对多、多对一、多对多、面向报文,尽最大努力服务,无拥塞控制。使用UDP的应用:域名系统 (DNS);视频流;IP语音(VoIP)。

我知道说这些你们也不懂,直接上图。

六、套接字工作流程

一个生活中的场景,你要打电话给一个朋友,先拨号,朋友听到电话铃声后提起电话,这时你和你的朋友就建立起了连接,就可以讲话了。等交流结束,挂断电话结束此次交谈。生活中的场景就解释了这功能原理。

先从服务器端说起。服务器端先初始化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)

由于 socket 模块中有太多的属性。我们在这里破例使用了'from module import *'语句。使用 'from socket import *',我们就把 socket 模块里的所有属性都带到我们的命名空间里了,这样能 大幅减短我们的代码。
例如tcpSock = socket(AF_INET, SOCK_STREAM)

服务端套接字函数

  • 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() # 创建一个与套接字相关的文件

七、套接字(socket)初使用

基于TCP协议的socket

tcp是基于链接的,必须先启动服务端,然后再启动客户端去链接服务端

server端

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import socket
server = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
server.bind(('127.0.0.1',8080))   # 把地址绑定到套接字
server.listen(5)         # 监听链接
conn,addr = server.accept()   # 接受客户端链接
mgs = conn.recv(1024)   # 接收客户端信息
print(mgs)
conn.send(b'hi')    # 想客户端发送消息
conn.close()     # 关闭客户端套接字
server.close()    # 关闭套接字服务器

client端

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import socket
client = socket.socket(socket.AF_INET,socket.SOCK_STREAM)   # 创建客户端套接字

client.connect(('127.0.0.1',8080))  # 尝试连接服务器
client.send(b'hello')    # 向服务器发送消息
res = client.recv(1024)    # 对话(接收)
print(res)
client.close()

基于UDP协议的socket

udp是无链接的,启动服务之后可以直接接受消息,不需要提前建立链接

简单使用

server端

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import socket

server = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)  # 创建一个服务器的套接字
server.bind(('127.0.0.1',8080))   # 绑定服务器套接字
mgs,addr = server.recvfrom(1025)  # 接收
print(mgs)
server.sendto(b'hi',addr)    # 发送
server.close()

client端

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import socket
ip_port=('127.0.0.1',8080)
udp_sk=socket.socket(type=socket.SOCK_DGRAM)
udp_sk.sendto(b'hello',ip_port)
back_msg,addr=udp_sk.recvfrom(1024)
print(back_msg.decode('utf-8'),addr)

socket参数详解

1. family	地址系列应为AF_INET(默认值),AF_INET6,AF_UNIX,AF_CAN或AF_RDS。(AF_UNIX 域实际上是使用本地 socket 文件来通信)
2. type	套接字类型应为SOCK_STREAM(默认值),SOCK_DGRAM,SOCK_RAW或其他SOCK_常量之一。
   SOCK_STREAM 是基于TCP的,有保障的(即能保证数据正确传送到对方)面向连接的SOCKET,多用于资料传送。 
   SOCK_DGRAM 是基于UDP的,无保障的面向消息的socket,多用于在网络上发广播信息。
3. proto	协议号通常为零,可以省略,或者在地址族为AF_CAN的情况下,协议应为CAN_RAW或CAN_BCM之一。
4. fileno	如果指定了fileno,则其他参数将被忽略,导致带有指定文件描述符的套接字返回。与socket.fromfd()不同,fileno将返回相同的套接字,而不是重复的。这可能有助于使用socket.close()关闭一个独立的插座。

QQ聊天简单示例

ps:由于udp无连接,所以可以同时多个客户端去跟服务端通信

server

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import socket
ip_port = ('127.0.0.1',8080)

upd_server = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
upd_server.bind(ip_port)

while True:
    qq_mgs,addr=upd_server.recvfrom(1024)
    print('来自[%s:%s]的一条消息:\033[1;44m%s\033[0m' %(addr[0],addr[1],qq_mgs.decode('utf-8')))
    back_mgs = input("回复消息: ").strip()
    upd_server.sendto(back_mgs.encode('utf-8'),addr)



client

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import socket
BUFSIZE = 1024

udp_client = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)

qq_name_dic = {
    'dog1':('127.0.0.1',8080),
    'dog2':('127.0.0.1',8080),
}

while True:
    qq_name= input("请选择聊天对象:").strip()
    while True:
        mgs = input("请输入消息,回车发送:").strip()
        if mgs == 'q':break
        if not mgs or qq_name or qq_name not in qq_name_dic:continue
        udp_client.sendto(mgs.encode('utf-8'),qq_name_dic[qq_name])
        back_mgs,addr = udp_client.recvfrom(BUFSIZE)
        print('来自[%s:%s]的一条消息:\033[1;44m%s\033[0m' % (addr[0], addr[1], back_mgs.decode('utf-8')))

udp_client.close()

八、什么是粘包

须知:只有TCP有粘包现象,UDP永远不会粘包,为何,且听我娓娓道来。

首先需要掌握一个socket收发消息的原理

发送端可以是-K-K地发送数据,而接受端的应用程序可以两K两K地提走数据,当然也有可能一次提走3K或6K数据,或者一次只提走几个字节的数据,也就是说,应用程序所看到的数据是一个整体,或说是一个流(stream),一条消息有多少字节对应用程序是不可见的,因此TCP协议是面向流的协议,这也是容易出现粘包问题的原因。而UDP是面向消息的协议,每个UDP段都是一条消息,应用程序必须以消息为单位提取数据,不能一次提取任意字节的数据,这一点和TCP是很不同的。怎样定义消息呢?可以认为对方一次性write/send的数据为一个消息,需要明白的是当对方send一条信息的时候,无论底层怎样分段分片,TCP协议层会把构成整条消息的数据段排序完成后才呈现在内核缓冲区。

例如基于tcp的套接字客户端往服务端上传文件,发送时文件内容是按照一段一段的字节流发送的,在接收方看了,根本不知道该文件的字节流从何处开始,在何处结束

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

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

  1. TCP(transport control protocol,传输控制协议)是面向连接的,面向流的,提供高可靠性服务。收发两端(客户端和服务器端)都要有一一成对的socket,因此,发送端为了将多个发往接收端的包,更有效的发到对方,使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。这样,接收端,就难于分辨出来了,必须提供科学的拆包机制。 即面向流的通信是无消息保护边界的。
  2. UDP(user datagram protocol,用户数据报协议)是无连接的,面向消息的,提供高效率服务。不会使用块的合并优化算法,, 由于UDP支持的是一对多的模式,所以接收端的skbuff(套接字缓冲区)采用了链式结构来记录每一个到达的UDP包,在每个UDP包中就有了消息头(消息来源地址,端口等信息),这样,对于接收端来说,就容易进行区分处理了。 即面向消息的通信是有消息保护边界的。
  3. tcp是基于数据流的,于是收发的消息不能为空,这就需要在客户端和服务端都添加空消息的处理机制,防止程序卡住,而udp是基于数据报的,即便是你输入的是空内容(直接回车),那也不是空消息,udp协议会帮你封装上消息头,实验略

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

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

两种情况下会发生粘包问题

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

server

#_*_coding:utf-8_*_
from socket import *
ip_port=('127.0.0.1',8080)

tcp_socket_server=socket(AF_INET,SOCK_STREAM)
tcp_socket_server.bind(ip_port)
tcp_socket_server.listen(5)


conn,addr=tcp_socket_server.accept()


data1=conn.recv(10)
data2=conn.recv(10)

print('----->',data1.decode('utf-8'))
print('----->',data2.decode('utf-8'))

conn.close()

client

#_*_coding:utf-8_*_
import socket
BUFSIZE=1024
ip_port=('127.0.0.1',8080)

s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
res=s.connect_ex(ip_port)


s.send('hello'.encode('utf-8'))
s.send('feng'.encode('utf-8'))
  1. 接收方不及时接收缓冲区的包,造成多个包接收(客户端发送了一段数据,服务端只收了一小部分,服务端下次再收的时候还是从缓冲区拿上次遗留的数据,产生粘包)

server

#_*_coding:utf-8_*_
from socket import *
ip_port=('127.0.0.1',8080)

tcp_socket_server=socket(AF_INET,SOCK_STREAM)
tcp_socket_server.bind(ip_port)
tcp_socket_server.listen(5)


conn,addr=tcp_socket_server.accept()


data1=conn.recv(2) #一次没有收完整
data2=conn.recv(10)#下次收的时候,会先取旧的数据,然后取新的

print('----->',data1.decode('utf-8'))
print('----->',data2.decode('utf-8'))

conn.close()

client

#_*_coding:utf-8_*_
import socket
BUFSIZE=1024
ip_port=('127.0.0.1',8080)

s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
res=s.connect_ex(ip_port)


s.send('hello feng'.encode('utf-8'))

拆包的发生情况

当发送端缓冲区的大度大于网卡的MTU时,tcp会将这次发送的数据拆成几个数据包发送出去。

补充问题一:为何TCP是可靠传输,UDP是不可靠传输

tcp在数据传输时,发送端先把数据发送到自己的缓存中,然后协议控制将缓存中的数据发生对端,对端返回一个ack=1,发送端则清理缓存中的数据,对端返回ack=0,则重新发送数据,所以tcp是可靠的,而udp发送数据,对端是不会返回确认信息的,因此不可靠。

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

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

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

九、解决粘包的low的处理方法

问题的根源,接收端不知道发送端将要传送的字节流的长度,所以解决粘包的方法就是围绕,如何让发送端在发送数据前,把自己将要发送的字节流总大小让接收端知晓,然后接收端来一个死循环接收完所有数据

server

#_*_coding:utf-8_*_
import socket,subprocess
ip_port=('127.0.0.1',8080)
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

s.bind(ip_port)
s.listen(5)

while True:
    conn,addr=s.accept()
    print('客户端',addr)
    while True:
        msg=conn.recv(1024)
        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'))
        data=conn.recv(1024).decode('utf-8')
        if data == 'recv_ready':
            conn.sendall(ret)
    conn.close()

client

#_*_coding:utf-8_*_
import socket,time
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
res=s.connect_ex(('127.0.0.1',8080))

while True:
    msg=input('>>: ').strip()
    if len(msg) == 0:continue
    if msg == 'quit':break

    s.send(msg.encode('utf-8'))
    length=int(s.recv(1024).decode('utf-8'))
    s.send('recv_ready'.encode('utf-8'))
    send_size=0
    recv_size=0
    data=b''
    while recv_size < length:
        data+=s.recv(1024)
        recv_size+=len(data)


    print(data.decode('utf-8'))

ps:程序的运行速度远快于网络传输速度,所以在发送一段字节前,先用send去发送该字节流长度,这种方式会放大网络延迟带来的性能损耗

十、解决粘包问题(最终版)

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

struct模块:

该模块可以把一个类型,如数字,转成固定长度的bytes

>>> struct.pack('i',1111111111111)

struct.error: 'i' format requires -2147483648 <= number <= 2147483647 #这个是范围

思路:

1. 制作报头,用字典形式存里面可以用(字符串的长度,md5值,文件名等等)
2. 需要把字典json序列化为字符串
3. 然后再转成bytes类型
4. 利用struct发送报头bytes格式的长度(struct是固定长度的)
5. 即可解决粘包问题

server

from socket import *
import subprocess
import struct
import json

phone=socket(AF_INET,SOCK_STREAM)
phone.bind(('127.0.0.1',8081))
phone.listen(5)

print('服务的启动......')
# 连接循环
while True:
    conn,client_addr=phone.accept()
    print(client_addr)

    # 通信循环
    while True:
        try:
            cmd=conn.recv(1024)
            if not cmd:break

            obj=subprocess.Popen(cmd.decode('utf-8'),shell=True,
                             stdout=subprocess.PIPE,
                             stderr=subprocess.PIPE
                             )

            stdout=obj.stdout.read()
            stderr=obj.stderr.read()

            #制作报头
            header_dic={
                'filename':'a.txt',
                'total_size':len(stdout) + len(stderr),
                'md5':'xxxxxsadfasdf123234e123'
            }

            header_json = json.dumps(header_dic)
            header_bytes=header_json.encode('utf-8')

            #1、先发送报头的长度
            conn.send(struct.pack('i',len(header_bytes)))

            #2、再发送报头
            conn.send(header_bytes)

            #3、最后发送真实的数据
            conn.send(stdout)
            conn.send(stderr)


        except ConnectionResetError:
            break
    conn.close()

phone.close()

client

from socket import *
import struct
import json

phone=socket(AF_INET,SOCK_STREAM)
phone.connect(('127.0.0.1',8081))

while True:
    cmd=input('>>>: ').strip()
    if not cmd:continue
    phone.send(cmd.encode('utf-8'))

    #1、先收报头的长度
    obj=phone.recv(4)
    header_size=struct.unpack('i',obj)[0]

    #2、再接收报头
    header_bytes=phone.recv(header_size)
    header_json=header_bytes.decode('utf-8')
    header_dic=json.loads(header_json)
    print(header_dic)

    total_size=header_dic['total_size']
    #3、循环接收真实的数据,直到收干净为止
    recv_size=0
    res=b''
    while recv_size < total_size:
        recv_data=phone.recv(1024)
        res+=recv_data
        recv_size+=len(recv_data)

    print(res.decode('utf-8'))


phone.close()
posted @ 2018-06-04 13:07  云原生运维社区  阅读(329)  评论(0编辑  收藏  举报