决战Python之巅(十六)-网络编程

一.引言

假设现在有两个python文件a和b,分别去运行,你会发现,这两个python文件运行的很好。但是如果要在这两个程序之间传递一个数据,该怎么做呢?
用之前学的知识很容易就能解决,在a中将所需传递的数据写入一个文件,b从该文件读取数据即可。
在这里插入图片描述
但如果a,b这两个文件不在同一台计算机上,该怎么办呢?
这就需要程序间的通信。

二.软件开发的架构

我们了解的涉及到两个程序间通信的应用大概可以分为两种:
第一种是应用类:QQ、微信、网盘等这类属于需要安装的桌面应用;
第二种是web类:百度、知乎、博客等用浏览器访问就可以直接使用的应用;
这些应用本质上都是程序之间的通信,不同的是使用的不同开发架构。

1.C/S架构

C/S:即Client/Server。客户端与服务端架构,这种架构也是从用户层面(也可以是物理层面)来划分的。
客户端一般泛指客户端应用程序.exe,程序需要先安装,才能在用户的电脑上运行,对于用户操作系统依赖较大。
在这里插入图片描述

2.B/S架构

B/S:Browser与Server。浏览器端与服务端架构,这种架构是从用户层面划分的。
浏览器其实也是一种应用,只是这种客户端不需要安装什么应用程序,只需在浏览器上通过HTTP请求服务端相关的资源,客户端的浏览器就能进行增删改查操作。
在这里插入图片描述

三.网络基础

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

寄快递的时候,都需要我们写收件方的地址,这样快递员才能准确的把货送到。互联网也是如此,每台联网的机器在网络上有自己独特的地址,这个地址并不是像:国家\省\市\区\街道\楼\门牌号这样的,而是用一串数字来表示的,例如:127.0.0.1:8080。其中‘127.0.0.1’称为IP地址,‘8080’为端口号
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之间的整数,即8位二进制所能表示的数。

端口:

端口,即Port,可以认为是设备与外界通信交流的出口。

因此IP地址精确到具体一台电脑,而端口号则能精确到这台电脑上的具体程序。

2.osi七层协议

一台完整的计算机系统是由硬件、操作系统、应用软件三者组成。计算机上网,就需要互联网。
互联网的核心是一堆协议组成,协议就是标准,打个比方,这堆协议就类似英语,学会了这些协议,那么就能按照统一的标准去收发消息从而完成通信。
人们按照分工不同吧互联网协议从逻辑上划分了层级:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

3.socket概念

在这里插入图片描述
socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,socket其实是一个门面模式,它把复杂的TCP/IP协议隐藏在Socket接口后,对于我们用户来说,这个简单的接口就是全部,socket会帮我们以符合指定的协议组织数据。
从我们用户角度来看,socket就是一个模块,我们通过socket模块中已经实现的方法来实现两个进程中的连接和通信。

4.TCP/UDP协议

TCP可靠的、面向连接的协议、传输效率低全双工通信、面向字节流。
UDP不可靠的、无连接的服务,传输效率高。
在这里插入图片描述

四.套接字(socket)初使用

1.基于TCP协议的socket

TCP协议是基于连接的,必须先启动服务端,然后启动客户端连接服务端。
server端:

import socket
sk = socket.socket()
sk.bind(('127.0.0.1',8898))  #把地址绑定到套接字
sk.listen()          #监听链接
conn,addr = sk.accept() #接受客户端链接
ret = conn.recv(1024)  #接收客户端信息
print(ret)       #打印客户端信息
conn.send(b'hi')        #向客户端发送信息
conn.close()       #关闭客户端套接字
sk.close()        #关闭服务器套接字(可选)

client端:

import socket
sk = socket.socket()           # 创建客户套接字
sk.connect(('127.0.0.1',8898))    # 尝试连接服务器
sk.send(b'hello!')
ret = sk.recv(1024)         # 对话(发送/接收)
print(ret)
sk.close()            # 关闭客户套接字

2.基于UDP协议的socket

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

import socket
udp_sk = socket.socket(type=socket.SOCK_DGRAM)   #创建一个服务器的套接字
udp_sk.bind(('127.0.0.1',9000))        #绑定服务器套接字
msg,addr = udp_sk.recvfrom(1024)
print(msg)
udp_sk.sendto(b'hi',addr)                 # 对话(接收与发送)
udp_sk.close()                         # 关闭服务器套接字

client端:

import socket
ip_port=('127.0.0.1',9000)
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)

3.socket参数详解

socket.socket(family=AF_INET,type=SOCK_STREAM,proto=0,fileno=None)

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

五.黏包

1.黏包现象

在两个程序通信过程中,同时执行多条命令,得到的结果很可能只有一部分,在执行其他命令的时候又接收到之前执行的另外一部分结果,这种现象就是黏包。
注意:只有TCP有粘包现象,UDP永远不会粘包。

2.黏包成因

为什么TCP协议会有黏包现象呢?
之前我们介绍过TCP协议是面向字节流,是一种‘流式协议’,什么意思呢?
两个程序在使用TCP协议时,首先要经过三次握手,建立一个双向的管道。(通信结束要经过四次挥手)。
就好比我要挖一条水管到你家,首先我得先问你:我可以挖一条水管到你家吗?(第一次握手),然后你说:可以呀。然后你想了想,又加了句:那我也可以挖一条水管到你家吗?(第二次握手),我收到后回复你:可以(第三次握手)。这样一个‘双向(可以从我家到你家,也可以从你家到我家)’管道建好了。
而TCP协议中程序间传递的消息就像水流,所谓的‘流’,就是没有界限的一串数据。就像河里的水,是连成一片的,期间并没有界限。
这就导致了,发送端一个包一个包发出来的数据,到接收端就变成了一串数据。socket读取的时候,可能读到了两个或者多个包的数据,也有可能只读了一个包的一部分数据。

TCP协议黏包现象主要就是因为接收端不知道消息之间的界限,不知道一次提取多少数据造成的。

3.黏包的解决方法

黏包的问题在于接收方不知道该接收多少数据合适。那么,我们只要先告诉接收端这个包有多大,然后让接收端只接受这么大小的数据就可以了。
在这里,我们借助一个struct模块实现。这个模块可以把要发送的数据长度转换成固定长度的字节。这样客户端每次接收消息之前只要先接收这个固定长度字节的内容看一看接下来要接受的消息大小,那么最终接收的数据只要达到这个值就停止,就能刚好完整的接收整个数据了。

struct模块

import json,struct
#假设通过客户端上传1T:1073741824000的文件a.txt

#为避免粘包,必须自定制报头
header={'file_size':1073741824000,'file_name':'/a/b/c/d/e/a.txt','md5':'8f6fbf8347faa4924a76856701edb0f3'} #1T数据,文件路径和md5值

#为了该报头能传送,需要序列化并且转为bytes
head_bytes=bytes(json.dumps(header),encoding='utf-8') #序列化并转成bytes,用于传输

#为了让客户端知道报头的长度,用struck将报头长度这个数字转成固定长度:4个字节
head_len_bytes=struct.pack('i',len(head_bytes)) #这4个字节里只包含了一个数字,该数字是报头的长度

#客户端开始发送
conn.send(head_len_bytes) #先发报头的长度,4个bytes
conn.send(head_bytes) #再发报头的字节格式
conn.sendall(文件内容) #然后发真实内容的字节格式

#服务端开始接收
head_len_bytes=s.recv(4) #先收报头4个bytes,得到报头长度的字节格式
x=struct.unpack('i',head_len_bytes)[0] #提取报头的长度

head_bytes=s.recv(x) #按照报头长度x,收取报头的bytes格式
header=json.loads(json.dumps(header)) #提取报头

#最后根据报头的内容提取真实的数据,比如
real_data_len=s.recv(header['file_size'])
s.recv(real_data_len)

六.socket的更多方法介绍

服务端套接字函数
s.bind()    绑定(主机,端口号)到套接字
s.listen()  开始TCP监听
s.accept()  被动接受TCP客户的连接,(阻塞式)等待连接的到来

客户端套接字函数
s.connect()     主动初始化TCP服务器连接
s.connect_ex()  connect()函数的扩展版本,出错时返回出错码,而不是抛出异常

公共用途的套接字函数
s.recv()            接收TCP数据
s.send()            发送TCP数据
s.sendall()         发送TCP数据
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()        创建一个与该套接字相关的文件
发布了32 篇原创文章 · 获赞 32 · 访问量 6817

猜你喜欢

转载自blog.csdn.net/qq_33267875/article/details/96475793