如果对您有一丁点帮助,劳烦动动手指点个赞,支持和鼓励是搬砖人不断创作的动力!
网络编程
自从互联网诞生以来,现在基本上所有的程序都是网络程序,很少有单机版的程序了。 计算机网络就是把各个计算机连接到一起,让网络中的计算机可以互相通信。网络编程就是 如何在程序中实现两台计算机的通信。
网络编程对所有开发语言都是一样的,Python 也不例外。网络是一个互联网应用的重 要组成部分,在 Python 语言中提供了大量的内置模块和第三方模块用于支持各种网络访问, 而且 Python 语言在网络通信方面的优点特别突出,远远领先其他语言。
通过阅读本章,你可以:
- 了解 TCP 和 UDP
- 掌握编写 UDP Socket 客户端应用
- 掌握编写 UDP Socket 服务器端应用
- 掌握编写 TCP Socket 客户端应用
- 掌握编写 TCP Socket 服务器端应用
- 掌握使用 socketserver 模块中的 API 编写 TCP 服务端应用
基本概念
IP 地址与端口
IP 地址
用来标识网络中的一个通信实体的地址。通信实体可以是计算机、路由器等。 比如互 联网的每个服务器都要有自己的 IP 地址,而每个局域网的计算机要通信也要配置 IP 地址。
路由器是连接两个或多个网络的网络设备。
目前主流使用的 IP 地址是 IPV4,但是随着网络规模的不断扩大,IPV4 面临着枯竭的 危险,所以推出了 IPV6。
IP 地址实际上是一个 32 位整数(称为 IPv4),以字符串表示的 IP 地址如 192.168.0.1
实际上是把 32 位整数按 8 位分组后的数字表示,目的是便于阅读。
IPv6 地址实际上是一个 128 位整数,它是目前使用的 IPv4 的升级版,以字符串表示类 似于 2001:0db8:85a3:0042:1000:8a2e:0370:7334。
注意事项
- 127.0.0.1 本机地址
- 192.168.0.0–192.168.255.255 为私有地址,属于非注册地址,专门为组织机构内部使用。
端口
IP 地址用来标识一台计算机,但是一台计算机上可能提供多种网络应用程序,如何来 区分这些不同的程序呢?这就要用到端口。
端口是虚拟的概念,并不是说在主机上真的有若干个端口。通过端口,可以在一个主机 上运行多个网络应用程序。 端口的表示是一个 16 位的二进制整数,对应十进制的 0-65535。
Oracle、MySQL、Tomcat、QQ、msn、迅雷、电驴、360 等网络程序都有自己的端口。
总结
- IP 地址好比每个人的地址(门牌号),端口好比是房间号。必须同时指定 IP 地址和端 口号才能够正确的发送数据。
- IP 地址好比为电话号码,而端口号就好比为分机号。
网络通信协议
- 网络通信协议 通过计算机网络可以实现不同计算机之间的连接与通信,但是计算机网络中实现通信必
须有一些约定即通信协议,对速率、传输代码、代码结构、传输控制步骤、出错控制等制定 标准。就像两个人想要顺利沟通就必须使用同一种语言一样,如果一个人只懂英语而另外一 个人只懂中文,这样就会造成没有共同语言而无法沟通。
国际标准化组织(ISO,即 International Organization for Standardization)定义了网络通信协 议的基本框架,被称为 OSI(Open System Interconnect,即开放系统互联)模型。要制定通 讯规则,内容会很多,比如要考虑 A 电脑如何找到 B 电脑,A 电脑在发送信息给 B 电脑时
是否需要 B 电脑进行反馈,A 电脑传送给 B 电脑的数据格式又是怎样的?内容太多太杂, 所以 OSI 模型将这些通讯标准进行层次划分,每一层次解决一个类别的问题,这样就使得 标准的制定没那么复杂。OSI 模型制定的七层标准模型,分别是:应用层,表示层,会话层, 传输层,网络层,数据链路层,物理层。
OSI 七层协议模型如图所示:
虽然国际标准化组织制定了这样一个网络通信协议的模型,但是实际上互联网通讯使用 最多的网络通信协议是 TCP/IP 网络通信协议。
TCP/IP 是一个协议族,也是按照层次划分,共四层:应用层,传输层,互连网络层, 网络接口层(物理+数据链路层)。
那么 TCP/IP 协议和 OSI 模型有什么区别呢?OSI 网络通信协议模型,是一个参考模型, 而 TCP/IP 协议是事实上的标准。TCP/IP 协议参考了 OSI 模型,但是并没有严格按照 OSI 规定的七层标准去划分,而只划分了四层,这样会更简单点,当划分太多层次时,你很难区 分某个协议是属于哪个层次的。TCP/IP 协议和 OSI 模型也并不冲突,TCP/IP 协议中的应用 层协议,就对应于 OSI 中的应用层,表示层,会话层。就像以前有工业部和信息产业部, 现在实行大部制后只有工业和信息化部一个部门,但是这个部门还是要做以前两个部门一样 多的事情,本质上没有多大的差别。TCP/IP 中有两个重要的协议,传输层的 TCP 协议和互 连网络层的 IP 协议,因此就拿这两个协议做代表,来命名整个协议族了,再说 TCP/IP 协议
时,是指整个协议族。
- 网络协议的分层
由于网络结点之间联系很复杂,在制定协议时,把复杂成份分解成一些简单的成份,再将它们复合起来。最常用的复合方式是层次方式,即同层间可以通信、上一层可以调用下一 层,而与再下一层不发生关系。
把用户应用程序作为最高层,把物理通信线路作为最低层,将其间的协议处理分为若干 层,规定每层处理的任务,也规定每层的接口标准。
ISO 模型与 TCP/IP 模型的对应关系如图所示。
TCP/UDP
- 联系和区别
TCP 协议和 UDP 协议是传输层的两种协议。Socket 是传输层供给应用层的编程接口,所以 Socket 编程就分为 TCP 编程和 UDP 编程两类。
在网络通讯中,TCP 方式就类似于拨打电话,使用该种方式进行网络通讯时,需要建 立专门的虚拟连接,然后进行可靠的数据传输,如果数据发送失败,则客户端会自动重发该 数据。而 UDP 方式就类似于发送短信,使用这种方式进行网络通讯时,不需要建立专门的 虚拟连接,传输也不是很可靠,如果发送失败则客户端无法获得。
这两种传输方式都在实际的网络编程中使用,重要的数据一般使用 TCP 方式进行数据 传输,而大量的非核心数据则可以通过 UDP 方式进行传递,在一些程序中甚至结合使用这 两种方式进行数据传递。
由于 TCP 需要建立专用的虚拟连接以及确认传输是否正确,所以使用 TCP 方式的速度 稍微慢一些,而且传输时产生的数据量要比 UDP 稍微大一些。
总结
- TCP 是面向连接的,传输数据安全,稳定,效率相对较低。
- UDP 是面向无连接的,传输数据不安全,效率较高。
套接字编程
应用程序通常通过“套接字”(socket)向网络发出请求或者应答网络请求,使用主机 之间或者一台计算机上的进程间可以通信。Python 语言提供了两种访问网络服务的功能。 其中低级别的网络服务通过套接字实现,而高级别的网络服务通过模块 SocketServer 实现, 它提供了服务中心类,可以简化网络服务器的开发。
**socket()**函数介绍
在 Python 语言标准库中,通过使用 socket 模块提供的 socket 对象,可以在计算机网络 中建立可以互相通信的服务器与客户端。在服务器端需要建立一个 socket 对象,并等待客户 端的连接。客户端使用 socket 对象与服务器端进行连接,一旦连接成功,客户端和服务器端 就可以进行通信了。
在 Python 中,通常用一个 Socket 表示“打开了一个网络连接”,语法格式如下: socket.socket([family[, type[, proto]]])
其中参数 family: 套接字家族可以使 AF_UNIX 或者 AF_INET;type: 套接字类型可以 根据是面向连接的还是非连接分为 SOCK_STREAM 或 SOCK_DGRAM;protocol: 一般不填 默认为 0。
Socket 主要分为面向连接的 Socket 和无连接的 Socket。无连接 Socket 的主要协议是用 户数据报协议,也就是常说的 UDP,UDP Socket 的名字是 SOCK_DGRAM。创建套接字 UDP/IP 套接字,可以调用 socket.socket()。示例代码如下:
udpSocket=socket.socket (AF_INET,SOCK_DGRAM)
socket 对象的内置函数和属性
在 Python 语言中 socket 对象中,提供如表 12-1 所示的内置函数。
表 12-1 内置函数
函数 | 功能 |
---|---|
服务器端套接字函数 | |
s.bind() | 绑定地址(host,port)到套接字, 在 AF_INET 下,以元组(host,port)的形式表示地址。 |
s.listen() | 开始 TCP监听。backlog 指定在拒绝连接之前, 操作系统可以挂起的最大连接数量。该值至少为 1, 大部分应用程序设为 5 就可以了。 |
s.accept() | 被动接受 TCP客户端连接,(阻塞式)等待连接的 到来 |
客户端套接字 |
s.connect() | 主动初始化 TCP 服务器连接,。一般 address 的格式为元组(hostname,port),如果连接出错, 返回 socket.error 错误。 |
---|---|
s.connect_ex() | connect() 函 数 的 扩 展 版 本 , 出 错 时 返 回 出 错 码,而不是抛出异常 |
公共用途的套接字函数 | |
s.recv() | 接收 TCP数据,数据以字符串形式返回,bufsize 指定要接收的最大数据量。flag 提供有关消息的其 他信息,通常可以忽略。 |
s.send() | 发送 TCP数据,将 string 中的数据发送到连接 的套接字。返回值是要发送的字节数量,该数量可 能小于 string 的字节大小。 |
s.sendall() | 完 整 发 送 TCP 数 据 , 完 整 发 送 TCP 数 据 。 将 string 中的数据发送到连接的套接字,但在返回之 前会尝试发送所有数据。成功返回 None,失败则抛 出异常。 |
s.recvfrom() | 接 收 UDP 数 据 , 与 recv() 类 似 , 但 返 回 值 是 (data,address)。其中 data 是包含接收数据的字 符串,address 是发送数据的套接字地址。 |
s.sendto() | 发送 UDP数据,将数据发送到套接字,address 是形式为(ipaddr,port)的元组,指定远程地址。 返回值是发送的字节数。 |
s.close() | 关闭套接字 |
s.getpeername() | 返回连接套接字的远程地址。返回值通常是元 组(ipaddr,port)。 |
s.getsockname() | 返 回 套 接 字 自 己 的 地 址 。 通 常 是 一 个 元 组 (ipaddr,port) |
s.setsockopt(level,optname,value) | 设置给定套接字选项的值。 |
s.getsockopt(level,optname[.buflen]) | 返回套接字选项的值。 |
s.settimeout(timeout) | 设置套接字操作的超时期,timeout 是一个浮点 数,单位是秒。值为 None 表示没有超时期。一般, 超时期应该在刚创建套接字时设置,因为它们可能 用于连接的操作(如 connect()) |
s.gettimeout() | 返回当前超时期的值,单位是秒,如果没有设置超时期,则返回 None。 |
s.fileno() | 返回套接字的文件描述符。 |
s.setblocking(flag) | 如果 flag 为 0,则将套接字设为非阻塞模式, 否则将套接字设为阻塞模式(默认值)。非阻塞模 式下,如果调用 recv()没有发现任何数据,或 send() 调用无法立即发送数据,那么将引起 socket.error 异常。 |
s.makefile() | 创建一个与该套接字相关连的文件 |
UDP 编程
TCP是建立可靠连接,并且通信双方都可以以流的形式发送数据。相对 TCP,UDP则是 面向无连接的协议。使用 UDP协议时,不需要建立连接,只需要知道对方的 IP地址和端口 号,就可以直接发数据包。但是,能不能到达就不知道了。虽然用 UDP传输数据不可靠,但 它的优点是和 TCP比,速度快,对于不要求可靠到达的数据,就可以使用 UDP协议。
创建 Socket 时,SOCK_DGRAM指定了这个 Socket 的类型是 UDP。绑定端口和 TCP一样, 但是不需要调用 listen()方法,而是直接接收来自任何客户端的数据。recvfrom()方法返 回数据和客户端的地址与端口,这样,服务器收到数据后,直接调用 sendto()就可以把数 据用 UDP发给客户端。
发送数据:为看到效果借助“网络调试助手”。双击 NetAssist.exe 就完成安装。使用 详细如下图所示:
【示例】UDP 发送数据
from socket import *
s = socket(AF_INET, SOCK_DGRAM) #创建套接字
addr = (‘192.168.121.1’, 8080) #准备接收方地址
data = input(“请输入:”)
#发送数据时,python3 需要将字符串转成 byte s.sendto(data.encode(‘gb2312’),addr) #默认的网络助手使用的编码是 gb2312 s.close()
【示例】UDP 先发送数据,再接收数据
from socket import *
s = socket(AF_INET, SOCK_DGRAM) #创建套接字
s.bind(('', 8788)) #绑定一个端口,ip 地址和端⼝号,ip⼀般不⽤写
addr = ('192.168.121.1', 8080) #准备接收方地址
data = input("请输入:")
s.sendto(data.encode('gb2312'),addr)
redata = s.recvfrom(1024) #1024 表示本次接收的最⼤字节数
print(redata)
print('接收到%s 的消息:%s'%(redata[1],redata[0].decode('gb2312')))
s.close()
【示例】UDP 实现简单聊天
from socket import *
udp_socket=socket(AF_INET,SOCK_DGRAM)
#绑定端口
udp_socket.bind(('',8989))
#接收数据
while True:
recv_data = udp_socket.recvfrom(1024)
print('接收到%s 的消息是:%s' % (recv_data[1], recv_data[0].decode('gb2312')))
udp_socket.close()
【示例】UDP 实现多线程聊天
from socket import *
from threading import Thread
udp_socket=socket(AF_INET,SOCK_DGRAM)
#绑定端口
udp_socket.bind(('',8989))
#不停接收
def recv_data():
while True:
recv_msg=udp_socket.recvfrom(1024)
print('>>%s:%s'%(recv_msg[1],recv_msg[0].decode('gb2312')))
#不停发送
def send_data():
while True:
data=input('<<:') addr=('192.168.121.1',8080)
udp_socket.sendto(data.encode('gb2312'),addr)
if __name__=='__main__':
创建两个线程
t1=Thread(target=send_data)
t2=Thread(target=recv_data)
t2.start()
t1.start()
t1.join()
t2.join()
TFTP 文件下载器
TFTP(Trivial File Transfer Protocol,简单⽂件传输协议)使用这个协议,就可以实现简 单文件的下载,tftp 端⼝号为 69。
实现 TFTP 下载器: 下载:从服务器上将一个文件复制到本机上 下载的过程: 在本地创建一个空文件(与要下载的文件同名) 向里面写数据(接收到一点就向空文件里写一点) 关闭(接受完所有数据关闭文件)
(1)TFTP 文件下载过程
(2)TFTP 下载格式
当服务器找到需要现在的文件后,会立刻打开文件,把文件中的数据通过 TFTP 协议发 送给客户端。如果文件的总大小较大(比如 3M),那么服务器分多次发送,每次会从文件 中读取 512 个字节的数据发送过来。因为发送的次数可能会很多,所以为了让客户端对接收 到的数据进行排序,所以在服务器发送那 512 个字节数据的时候,会多发 2 个字节的数据, 用来存放序号,并且放在 512 个字节数据的前面,序号是从 1 开始的。
因为需要从服务器上下载文件时,文件可能不存在,那么此时服务器就会发送一个错误 的信息过来,为了区分服务器的是文件内容还是错误的提示信息,所以又用了 2 个字节来表 示这个数据的功能(称为操作码),并且在序号的前面。操作码对应功能如下表所示:
操作码 | 功能 |
---|---|
1 | 读请求,及下载 |
2 | 写请求,即上传 |
3 | 表示数据包,即 DATA |
4 | 确认码,及 ACK |
5 | 错误 |
因为 UDP 的数据包不安全,即发送方发送是否成功不能确定,所以 TFTP 协议中规定, 为了让服务器知道客户端已经接收到了刚刚发送的那个数据包,所以当客户端接收到一个数 据包的时候需要向服务器进行发送确认信息,即发送收到了,这样的包成为 ACK(应答包)。
什么时候就认为数据包已发送完毕?当客户端接收到的数据⼩于 516(2 字节操作码+2 个字节的序号+512 字节数据) 时,就意味着服务器发送完毕了 (如果恰好最后一次数据 长度为 516,会再发一个长度为 0 的数据包)。struct 模块可以按照指定格式将 Python 数据 转 换 为 字 符 串 ,该 字 符 串 为 字 节 流 。 struct 模 块 中 最 重 要 的 三 个 函 数 是 pack(), unpack(),
calcsize()。
函数名 | 描述 |
---|---|
pack(fmt, v1, v2, …) | 按照给定的格式(fmt),把数据封装成字 符串(实际上是类似于 c 结构体的字节流) |
unpack(fmt, string) | 按照给定的格式(fmt)解析字节流 string, 返回解析出来的元组 |
calcsize(fmt) | 计算给定的格式(fmt)占用多少字节的内 存 |
【示例】构造下载请求数据:“1test.jpg0octet0”
import struct
cmb_buf = struct.pack(“!H8sb5sb”,1,b“test.jpg”,0,b“octet”,0)
如何保证操作码(1/2/3/4/5)占两个字节?如何保证 0 占一个字节?
#!H8sb5sb: ! 表示按照网络传输数据要求的形式来组织数据(占位的格式)
H 表示将后面的 1 替换成占两个字节
8s 相当于 8 个 s(ssssssss)占 8 个字节
b 占一个字节
【示例】TFTP 文件下载客户端
import struct
from socket import *
filename = 'face.jpg'
server_ip = '127.0.0.1'
send_data = struct.pack('!H%dsb5sb' % len(filename), 1, filename.encode(), 0, 'octet'.encode(), 0)
s = socket(AF_INET, SOCK_DGRAM)
s.sendto(send_data, (server_ip, 69)) # 第一次发送,连接服务器 69 端口
f = open(filename, 'ab') # a:以追加模式打开(必要时可以创建)append;b:表示二进制
while True:
recv_data = s.recvfrom(1024) # 接收数据
caozuoma, ack_num = struct.unpack('!HH', recv_data[0][:4]) # 获取数据块编号
rand_port = recv_data[1][1] # 获取服务器的随机端口
if int(caozuoma) == 5:
print('文件不存在...')
break
print("操作码:%d,ACK:%d,服务器随机端口:%d,数据长度:%d" % (caozuoma, ack_num, rand_port, len(recv_data[0])))
f.write(recv_data[0][4:]) # 将数据写入
if len(recv_data[0]) < 516:
break
ack_data = struct.pack("!HH", 4, ack_num)
s.sendto(ack_data, (server_ip, rand_port)) # 回复ACK确认包
TCP 编程
面向连接的 Socket 使用的主要协议是传输控制协议,也就是常说的 TCP,TCP 的 Socket 名称是 SOCK_STREAM。创建套接字 TCP/IP 套接字,可以调用 socket.socket()。示例代码 如下:
tcpSocket=socket.socket(AF_INET,SOCK_STREAM)
三次握手
- 第一步,客户端发送一个包含 SYN 即同步(Synchronize)标志的 TCP 报文,SYN 同步报文会指明客户端使用的端口以及 TCP 连接的初始序号。
- 第二步,服务器在收到客户端的 SYN 报文后,将返回一个 SYN+ACK 的报文,表 示客户端的请求被接受,同时 TCP 序号被加一,ACK 即确认(Acknowledgement)。
- 第三步,客户端也返回一个确认报文 ACK 给服务器端,同样 TCP 序列号被加一, 到此一个 TCP 连接完成。然后才开始通信的第二步:数据处理。
- 这就是所说的 TCP 的三次握手(Three-way Handshake)。
在 Python语言中创建 Socket服务端程序,需要使用 socket模块中的 socket类。创建 Socket 服务器程序的步骤如下:
(1) 创建 Socket 对象。
(2) 绑定端口号。
(3) 监听端口号。
(4) 等待客户端 Socket 的连接。
(5) 读取客户端发送过来的数据。
(6) 向客户端发送数据。
(7) 关闭客户端 Socket 连接。
(8) 关闭服务端 Socket 连接。
【示例】TCP 服务器端接收数据
from socket import *
serverSocket = socket(AF\_INET, SOCK\_STREAM)
serverSocket.bind(("", 8899))
serverSocket.listen(5)
clientSocket,clientInfo = serverSocket.accept()
#clientSocket 表示这个新的客户端
#clientInfo 表示这个新的客户端的 ip 以及 port
recvData = clientSocket.recv(1024)
print("%s:%s"%(str(clientInfo), recvData.decode('gb2312')))
clientSocket.close()
serverSocket.close()
执行代码,服务器端会等待客户端连接,打开网络助手,选择网络协议、服务器的 IP、端 口如下图所示:
填写好消息,点击发送,在服务器控制台就会输出接收到的消息,如下图:
【示例】TCP 客户端
from socket import *
clientSocket = socket(AF\_INET, SOCK\_STREAM)
clientSocket.connect(("192.168.121.1", 8899))
#注意:
# 1.tcp 客户端已经链接好了服务器,所以在以后的数据发送中,不需要填写对方的 iph 和 port----->打电话
# 2.udp 在发送数据的时候,因为没有之前的链接,所依需要 在每次的发送中 都要填写接收方的 ip 和 port----->写信
clientSocket.send("haha".encode("gb2312"))
recvData = clientSocket.recv(1024)
print("recvData:%s"%recvData.decode('gb2312'))
clientSocket.close()
先配置网络助手,选择网络协议为 TCP Server,本地 IP、本地端口,点击连接,执行编写的 TCP 客户端代码,执行结果如下图所示:
【示例】TCP:双向通信 Socket 之服务器端
'''
双向通信 Socket 之服务器端
读取客户端发送的数据,将内容输出到控制台 将控制台输入的信息发送给客户器端
'''
#导入 socket 模块
from socket import *
#创建 Socket 对象 tcpServerSocket=socket(AF_INET,SOCK_STREAM) #绑定端口
tcpServerSocket.bind(('',8888))
#监听客户端的连接
tcpServerSocket.listen()
#接收客户端连接
tcpClientSocket,host=tcpServerSocket.accept()
while True:
#读取客户端的消息 re_data=tcpClientSocket.recv(1024).decode('utf-8') #将消息输出到控制台
print('客户端说:',re_data)
if re_data=='end':
break
#获取控制台信息
msg=input('>')
tcpClientSocket.send(msg.encode('utf-8'))
tcpClientSocket.close()
tcpServerSocket.close()
【示例】TCP:双向通信 Socket 之客户端
'''
双向通信Socket 之客户端
将控制台输入的信息发送给服务器端
读取服务器端的数据,将内容输出到控制台
'''
# 导入 socket 模块
from socket import *
# 创建客户端 Socket 对象
tcpClientSocket = socket(AF_INET, SOCK_STREAM)
# 连接服务器端
tcpClientSocket.connect(('127.0.0.1', 8888))
while True:
msg = input('>')
# 向服务器发送数据
tcpClientSocket.send(msg.encode('utf-8'))
if msg == 'end':
break
# 接收服务器端数据
re_data = tcpClientSocket.recv(1024)
print('服务器端说:', re_data.decode('utf-8'))
tcpClientSocket.close()
首先运行示例启动服务器端程序,然后运行示例客户端程序。执行结果如下图所示:
运行效果图—服务器端
运行效果图—客户端
注意事项
- 运行时,要先启动服务器端,再启动客户端,才能得到正常的运行效果。
上面这个程序,必须按照安排好的顺序,服务器和客户端一问一答!不够灵活!!可以 使用多线程实现更加灵活的双向通讯!!
服务器端:一个线程专门发送消息,一个线程专门接收消息。
客户端:一个线程专门发送消息,一个线程专门接收消息。
欢迎扫描微信添加,技术交流+资源分享
ID: Txtechcom