自从互联网诞生以来,现在基本数所有的程序都是网络程序,很少有单机版的程序了。计算机网络就是把各个计算机链接到一起,让网络中 的计算机可以互相通信。网络编程就是如何在程序中实现两台计算机的通信。
网络编程对所有开发语言都是一样的,Python也不例外。网络是一个互联网应用的重要组成部分,在Python语言中提供了大量的内置模块和第三方模块用于支持各种网络访问,而且Python语言在网络通信方面的优点特别突出,远远领先其他语言。
入门Python网络编程,你可以:
IP地址是用来标识网络中的一个通信实体的地址。通信实体可以是计算机,路由器等。比如互联网的每个服务器都要有自己的IP地址,而每个局域网的计算机通信也要配备IP地址。路由器是连接两个或多个网络的网络设备。
通过网络号和主机号所占的位置不一样,可以将IP地址分为ABCDE类。最常用的是C类地址,D类主要用于一对多发送消息使用,E类主要用于网络开发实验使用。
目前主流使用的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. 其中0-1023作为保留端口,有特殊指定意义,就像电话号码的110和119一样有特殊含义。
Oracle, MySQL, Tomcat, QQ, 迅雷等网络程序都有自己的端口。
【总结】IP地址好比每个人的地址,端口号好比是房间号。必须同时指定IP地址和端口号才能正确地发送数据。
通过计算机网络可以实现不同计算机之间的连接与通信,但是计算机网络中实现通信必须有一些约定,即通信协议,对速率、传输代码、代码结构、传输控制步骤、出错控制等,制定标准。就像两个人想要顺利沟通就必须使用一种语言一样,如果一个人只懂英语,而另外一个人只懂中文,这样就会造成没有共同语言而无法沟通。
国际标准化组织(ISO, International Organization for Standardization)定义了网络通信协议的基本框架,被称为OSI(Open System Interconnect, 开放性系统互联)模型。要制定通讯规则,内容会很多,比如要考虑A电脑传送给B电脑的数据格式又是怎样的?内容太多太复杂,所以OSI模型将这些通讯标准进行层次划分,每一层解决一个类别的问题,这样就使得标准的指定没有那么复杂。OSI模型制定的七层标准模型分别是:应用层,表示层,会话层,传输层,网络层,数据链路层,物理层。
虽然国际标准化组织制定了这样一个网络通信协议的额模型,但是i实际上互联网通讯使用最多的网络通信协议是TCP/IP网络通信协议。
TCP/IP是一个协议族,也是按照层次划分,共四层:应用层,传输层,互联网络层,网络接口层(物理+数据链路层)。
那么TCP/IP协议和OSI模型有什么区别呢?OSI网络通信协议模型,是一个参考模型,而TCP/IP协议是事实上的标准。TCP/IP协议参考了OSI模型,但是并没有严格按照OSI规定的七层标准去划分,只是划分了四层,这样会更简单点,当划分太多层时,你很难区分某个协议时属于哪个层次的。TCP/IP和OSI模型也并不冲突,TCP/IP协议中的应用层协议,就对应OSI中的应用层,表示层,会话层。就像以前有工业部和信息产业部,现在实行大部制后只有工业信息化部一个部门,但是这个部门还是要做和以前两个部门一样多的事情,本质上没有多大的差别。TCP/TP中有两个重要的协议,传输层的TCP协议和互联网络层的IP协议,因此就拿这两个协议做代表来命名整协议族了,一般提到TCP/IP协议时是指整个协议族。
由于网络节点之间的联系很复杂,在指定协议的时候,把复杂的协议分解成一些简单的成分,在将他们复合起来。最常用的复合方式时层次方式,即同层间可以通信,上一层可以调用次啊一层,而与再下一层没有关系。
把用户应用程序作为最高层,把物理通信线路作为最低层,将期间的协议处理分为若干层,规定每层处理的任务,也规定每层得到接口标准。
TCP协议和UDP协议时传输层的两种协议。Socket是传输层供给应用层的编程接口,所以Socket编程就分为TCP编程和UDP编程两类。
在网络通讯中,TCP方式就类似于拨打电话,使用该种方式进行网络通讯时,需要建立专门的虚拟连接,然后进行可靠的数据传输,如果数据发送失败,则客户端会自动重发该数据。而UDP方式就类似于发送短信,使用这种方式进行网络通讯时,不需要建立专门的虚拟连接,传输也不算很可靠,如果发送失败则客户端无法获得。
这两种传输方式都在实际的网络编程中使用,重要的数据一般使用TCP方式进行数据传输,而大量的非核心数据则可以通过UDP的方式进行传递,在一些程序中甚至结合使用这两种方式进行数据传递。
由于TCP需要建立专用的虚拟链接以及确认传输是否正确,所以使用TCP方式的速度稍微慢一些,而且传输时产生的数据量要比UDP稍微大一些。
传输控制协议,面向连接的,可靠的,基于字节流,仅支持单播传输
TCP与UDP对比:
属性 | UDP | TCP |
---|---|---|
是否创建连接 | 无连接 | 面向连接 |
是否可靠 | 不可靠 | 可靠的 |
连接的对象个数 | 一对一、一对多、多对一、多对多 | 支持一对一 |
传输的方式 | 面向数据报 | 面向字节流 |
首部开销 | 8个字节 | 最少20个字节 |
适用场景 | 实时应用(视频会议,直播) | 可靠性高的应用(文件传输) |
【总结】TCP是面向连接的,传输数据安全,稳定,效率相对较低。UDP是面向无连接的,传输数据不安全,效率较高。
应用程序通常通过“套接字”(socket)向网络发出请求或者回答网络请求,使用主机之间或者一台计算机上的进程间可以通信。Python语言提供了两种访问网络服务的功能。其中低级别的网络服务通过套接字实现,而高级别的网络服务通过模块SocketServer实现,它提供了服务中心类,可以i简化网络服务器的开发。
【注意】后面代码已经加了非常详细的注释,如果你前面一直认真读,后面都能看懂,如果看了注释还看不懂就自己Print一下各个关键变量,看看输出的是啥。涉及到并发的一些知识,请重新学习《理解思想:Python多线程和并发编程》相关内容
在Python语言标准库中,使用socket模块提供的socket对象,可以在计算机网络中建立可以互相通信的服务器与客户端。在服务器端需要建立一个socket对象,并等待客户端的连接。客户端使用socket对象与服务器端进行连接,一旦连接成功,客户端和服务器端就可以通信了。
在Python中,通常使用一个Socket表示“打开了一个网络连接”, 语法格式如下:
socket.socket([family[, type[, protocol]]])
其中参数family: 套接字家族可以是AF_UNIX或者AF_INET;type:套接字类型可以根据是面向连接何时非连接分为SOCK_STREAM或SOCK_DGRAM;protocol:一般不填,默认为0.
Socket主要分为面向连接的Socket和无连接的Socket。面向连接的Socket使用主要协议是传输控制协议,也就是常说的TCP,TCP的Socket名称是SOCK_STREAM。创建套接字TCP/IP套接字,可以调用socket.socket()。
tcp_socket = socket.socket(AF_INET, SOCK_STREAM)
无连接的Socket的主要协议是用户数据报协议,也就是说的UDP, UDP Socket的名字是SOCK_DGRAM。创建套接字UDP/IP套接字,可以调用socket.socket()。
tcp_socket = socket.socket(AF_INET, SOCK_DGRAM)
TCP是建立可靠连接,并且通信双方都可以以流动的形式发送数据。相对TCP,UDP则是面向无连接的协议。使用UDP协议时,不需要建立连接,只需要知道对方的IP地址和端口号,就可以直接发送数据包。但是,能不能到达就不知道了。虽然UDP传输数据不可靠,但是它的优点是和TCP相比,速度快,对于不要求可靠到达的数据,可以使用UDP协议。
创建Socket时,SOCK_DGRAM指定了这个Socket的类型时UDP。绑定端口和TCP一样,但是不需要调用listen()方法,而是直接接收来自任何客户端的数据。recvfrom()方法返回数据和客户端的地址与端口,这样服务器收到数据后,直接用sento()就可以把数据用UDP发给客户端。
发送数据:为看到效果借助‘’网格调试助手‘。双击NetAssist.exe就完成安装。使用详细如下图所示:
这里注意除了本地的IP地址是你自己电脑的IP地址,其他选项最好修改成截图得到选项。
【示例】DUP发送数据
from socket import *
s = socket(AF_INET, SOCK_DGRAM) # 创建套接字
addr = ('169.254.146.162', 8080) # 接收方地址
data = input("请输入: ")
# 发送数据时,python3需要将字符串转成byte
s.sendto(data.encode('gb2312'), addr)
# gb3212是编码方式
s.close()
运行结果:
可以看到,在输入发送的123后,网络调试助手也是成功地接受到了信息。
【示例】UDP先发送数据,再接受数据
from socket import *
s = socket(AF_INET, SOCK_DGRAM) # 创建套接字
s.bind(('', 8788)) # 绑定一个端口, IP地址和端口号,IP一般不用写
addr = ('169.254.146.162', 8080) # 准备接收方地址
data = input("请输入: ")
s.sendto(data.encode('gb2312'), addr) # 设置发送的编码
recv_data = s.recvfrom(1024) # 1024表示本次接受的最大字节数
# 这里注意,发送时用gb2312 来code,接受时也应该用gb2312来decode
print('接收到{}的消息:{}'.format(recv_data[1], recv_data[0].decode('gb2312')))
s.close()
运行结果:
在没有绑定端口的时候,每次发送信息,助手显示接收到的端口号都不同,所以要绑定一个端口号来让程序接收到助手返回的信息,这里不是很严格,随意选择一个出现的端口号绑定即可。
【示例】UDP实现多线程聊天
from socket import *
from threading import Thread, Lock
udp_socket = socket(AF_INET, SOCK_DGRAM)
# 绑定端口
udp_socket.bind(('', 8989))
lock1 = Lock()
lock2 = Lock()
lock2.acquire()
# 不停接收
class recv_data(Thread): # 通过继承Thread类的方式创建线程
def run(self):
while True:
if lock2.acquire(): # 加入锁,利用之前学到的线程同步控制两个线程执行
recv_msg = udp_socket.recvfrom(1024)
print('>>{}:{}'.format(recv_msg[1], recv_msg[0].decode('gb2312')))
lock1.release()
# 不停发送
class send_data(Thread):
def run(self):
while True:
if lock1.acquire():
data = input('<<: ')
addr = ('169.254.146.162', 8080)
udp_socket.sendto(data.encode('gb2312'), addr)
lock2.release()
if __name__ == "__main__":
t2 = send_data()
t1 = recv_data()
t1.start()
t2.start()
运行结果:
这里通过互斥锁对两个线程的运行进行了控制,当发送信息一方发送完毕后,接受信息的线程才会运行,当然实际情况并不是我非要收到你的消息才能发送信息,这里只是简单复习一下上一节Python并行编程的知识点。
如果不希望使用锁进行控制,直接将锁删除也是可以直接运行的。
TFTP(Trivial File Transf Protocol,简单文件传输协议)使用这个协议,可以实现简单文件的下载,TFTP的端口号为69,它基于UDP协议而实现。
特点:简单、占用资源小、适合传递小文件、适合在局域网进行传递、端口号为69、基于UDP实现。
TFTP文件下载器自取
Current Directory的作用是我当前需要下载的文件的根目录,如果能找到才可以进行下载。
Service interfaces是需要从哪个IP下载,这里是127.0.0.1意思是从本机下载。
下载:从服务器上将一个文件复制到本机上。
(1)TFTP文件下载过程
TFTP服务器默认监听:TFTP服务器默认监听69号端口。当客户端发送“下载”请求(即读请求)时,需要向服务器的69端口发送。服务器若批准此请求,则使用一个新的、临时的 端口进行数据传输。
客户端发送读写请求,服务器端收到请求后执行:
①搜索:当服务器找到需要现在的文件后,会立刻打开文件,把文件中的数据通过TFTP协议发送给客户端
②分段:如果文件的总大小较大(比如3M),那么服务器分多次发送,每次会从文件中读取512个字节的数据发送过来。
③添加序号:因为发送的次数有可能会很多,所以为了让客户端对接收到的数据进行排序,所以在服务器发送那512个字节数据的时候,会多发2个字节的数据,用来存放序号,并且放在512个字节数据的前面,序号是从1开始的。
④添加操作码:因为需要从服务器上下载文件时,文件可能不存在,那么此时服务器就会发送一个错误的信息过来,为了区分服务发送的是文件内容还是错误的提示信息,所以又用了2个字节 来表示这个数据包的功能(称为操作码),并且在序号的前面。
操作码 | 功能 |
---|---|
1 | 读请求,即下载 |
2 | 写请求,即上传 |
3 | 表示数据包,即DATA |
4 | 确认码,即ACK |
5 | 错误 |
⑤发送确认码(ACK):因为udp的数据包不安全,即发送方发送是否成功不能确定,所以TFTP协议中规定,为了让服务器知道客户端已经接收到了刚刚发送的那个数据包,所以当客户端接收到一个数据包的时候需要向服务器进行发送确认信息,即发送收到了,这样的包成为ACK(应答包)
⑥发送完毕:为了标记数据已经发送完毕,所以规定,当客户端接收到的数据小于516(2字节操作码+2个字节的序号+512字节数据)时,就意味着服务器发送完毕了。如果恰好最后一次数据的长度为516K,会再发送一个长度为0的数据包。
(2)TFTP文件下载格式
struct模块可以按照指定格式将Python数据转换为字符串,该字符串为字节流。struct模块中最重要的三个函数是pack(),unpack(),calcsize()。
【示例】功能:构造下载请求数据:“1test.jpg0octet0”
import struct
cmb_buf = struct.pack("!H8sb5sb", 1, b "test.jpg", 0, b "octet", 0)
请求数据1test.jpg0octet0中:
如何保证操作码(1、2、3、4、5)占两个字节?如何保证0占一个字节?
设置:确定TFTP服务器的当前目录,IP地址,并确保该目录下有要下载的文件:
import struct
from socket import *
filename = 'test.jpg'
server_ip = '127.0.0.1' # 从本机下载
# 创建读请求,和上面讲的同理,这里不再赘述
send_data = struct.pack('!H{}sb5sb'.format(len(filename)), 1, filename.encode(), 0, 'octet'.encode(), 0)
# 创建UDP_Socket套接字
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是一个元组,里面有两个元素,
# 第一个是接收到的操作码以及数据块编号以及数据总共是516,
# 第二个元素是当前发送方服务器的IP和端口
recv_data = s.recvfrom(1024)
# 获取操作码和数据块编号,
# 操作码占用两个字节,数据块编号占用两个字节,所以用切片[:4],用两个H
caozuoma, ack_num = struct.unpack('!HH', recv_data[0][:4])
# 获取服务器的随机端口, 也就是recv_data第二个元素的第二个值
rand_port = recv_data[1][1]
# 如果操作码是5说明文件不存在,输出错误信息
if int(caozuoma) == 5:
print('文件不存在…')
break
# 输出接收的信息
print("操作码:{},ACK:{},服务器随机端口:{},数据长度:{}".format(caozuoma, ack_num, rand_port, len(recv_data[0])))
# 将数据写入,recv_data第一个元素前4位后面的都是数据,所以用切片[4:]
f.write(recv_data[0][4:])
# 如果接收到的数据小于516,说明是最后一次发送数据块,直接结束退出循环
if len(recv_data[0]) < 516:
break
# 给服务器发送确认包,由于4和ack_num各占用两个字节,所以使用两个H占位
ack_data = struct.pack("!HH", 4, ack_num)
# 回复ACK确认包
s.sendto(ack_data, (server_ip, rand_port))
print(recv_data)
运行结果,利用print(recv_data)命令将每次接收到的数据输出到控制台:
下载成功!
面向连接的Socket使用的主要协议是传输控制协议,也就是经常说的TCP,TCP的Socket名称是SOCK_STREAM。创建套接字TCP/IP套接字,可以用socket.socket()。
tcp_socket = socket.socket(AF_INET, SOCK_STREAM)
创建一个用于监听的套接字
监听:监听有客户端的连接
套接字:这个套接字其实就是一个文件描述符
将这个监听文件描述符和本地的IP和端口绑定(IP和端口就是服务器的地址信息)
客户端连接服务器的时候使用的就是这个IP和端口
设置监听,监听的fd开始工作
阻塞等待,当有客户端发起连接,解除阻塞,接受客户端的连接,会得到一个和客户端通信的套接字(fd)
通信:接收数据,发送数据
通信结束,断开连接
来源:https://blog.csdn.net/weixin_47156401/article/details/125884879
将网络助手调试成TCP Client:
【示例】TCP服务器接收数据
from socket import *
# 创建服务器套接字对象
server_socket = socket(AF_INET, SOCK_STREAM)
# 绑定端口
server_socket.bind(('', 8899))
# listen进行接听
server_socket.listen()
# client_socket 表示这个新的客户端
# client_info表示这个新的客户端的ip和port
client_socket, client_info = server_socket.accept() # 接收客户端的连接
# 接收客户端发送的消息
recv_data = client_socket.recv(1024)
print("{}:{}".format(str(client_info), recv_data.decode('gb2312')))
# 关闭连接
client_socket.close()
server_socket.close()
执行代码,服务器端会等待客户端连接,打开网络助手连接,选择网络协议,服务器的IP,端口如下图所示,然后选择连接,就可以成功连接:
发送信息,连接自动断开:
在程序运行框中接收到信息:
【示例】TCP:双向通信Socket之服务器端
import struct
from socket import *
# 创建套接字
s = socket(AF_INET, SOCK_STREAM)
# 绑定端口
s.bind(('', 8089)) # 绑定的是本机,端口为8089
s.listen()
# 接收客户端的连接
client_socket, client_info = s.accept()
# 接收
while True:
# 服务器端接收客户端消息
data = client_socket.recv(1024) # 接收数据,类型为元组
print('客户端说:', data.decode('utf-8'))
if data.decode('utf-8') == 'bye':
break
msg = input('>>>>')
client_socket.send(msg.encode('utf-8'))
if msg == 'bye':
break
s.close()
client_socket.close()
【示例】TCP:双向通信Socket之客户端
import struct
from socket import *
# 创建套接字
client_socket = socket(AF_INET, SOCK_STREAM)
# 绑定端口
HOST = '169.254.146.162'
client_socket.connect((HOST, 8089))
while True:
# 客户端发送消息
msg = input('>>>>')
client_socket.send(msg.encode('utf-8'))
if msg == 'bye':
break
# 客户端接收消息
recv_data = client_socket.recv(1024)
print('服务器端说:', recv_data.decode('utf-8'))
if recv_data.decode('utf-8') == 'bye':
break
client_socket.close()
运行,先启动服务器端,再启动客户端,然后客户端向服务器端发送消息:
服务器端收到消息后回复:
输入bye可以下线。
上面的客户端和服务端的信息交互是相互制约的,即客户端向服务端发送了信息,服务端才能回复信息。而大家在QQ上面聊天的时候,并不是只有等到了对方的回复才可以发送消息,同时,当我们使用QQ的时候,所有用户使用的都是客户端,也就是说用户之间发送的消息其实是经过了服务器的中转再发送到好友的客户端上面的。
实现这样的功能,需要引入上一节学习的多线程知识。还不了解多线程的同学请自行阅读《理解思想:Python多线程和并发编程》。
【示例】TCP多线程聊天服务器端
from socket import *
from threading import Thread
sockets = []
def main():
# 创建server_socket套接字对象
server_socket = socket(AF_INET, SOCK_STREAM)
# 绑定端口
server_socket.bind(('', 8888))
# 监听
server_socket.listen()
# 接收客户端请求
while True:
client_socket, client_info = server_socket.accept()
# 保存在线客户端的列表
sockets.append(client_socket)
# 开启线程处理当前客户端的请求
t = Thread(target=read_msg, args=(client_socket,))
t.start()
def read_msg(client_socket):
# 读取客户端发送的消息
while True:
recv_data = client_socket.recv(1024)
# 如果接收到的消息结尾是bye则在线客户端列表移除该客户端
if recv_data.decode('utf-8').endswith('bye'):
sockets.remove(client_socket)
client_socket.close()
break
# 将消息发送给所有在线的客户端
# 遍历所有在线客户端列表
if len(recv_data) > 0:
for item in sockets:
item.send(recv_data)
if __name__ == '__main__':
main()
【示例】TCP多线程聊天客户端
from socket import *
from threading import Thread
flag = True
def read_msg(client_socket):
while flag:
recv_data = client_socket.recv(1024)
print('收到:', recv_data.decode('utf-8'))
def write_msg(client_socket):
global flag
while flag:
msg = input('>')
msg = user_name+'说'+msg
client_socket.send(msg.encode('utf-8'))
# 如果输入bye则下线
if msg.endswith('bye'):
flag = False
break
# 创建客户端的套接字对象
client_socket = socket(AF_INET, SOCK_STREAM)
# 调用connect连接服务器
client_socket.connect(('169.254.146.162', 8888))
user_name = input('请输入用户名:')
# 开启一个线程处理客户端的读取消息
t1 = Thread(target=read_msg, args=(client_socket,))
t1.start()
# 开启一个线程处理客户端的发送消息
t2 = Thread(target=write_msg, args=(client_socket,))
t2.start()
t1.join()
t2.join()
client_socket.close()
【注意】2018.3之后版本的PyCharm, 如果想要同时运行同一个程序,会弹出如下图的提示:
xxx is not allowed to run in parallel. would you like to stop the running instances?
解决办法:
Run -> Edit Configurations -> Allow parallel run
这种方法只能设置单个程序,想要全部程序都允许并行运行,修改Templates(只对新建文件有效)
运行结果:
先开启服务端,再开启客户端
可以看到,已经初步实现群聊功能,小李发送信息的时候,小赵在群里也会收到信息,并显示是谁发送的。当小李下线的时候,小赵再发送信息小李便收不到了。