使用网络的目的就是为了联通多方然后进行通信用的,即把数据从一方传递给另外一方。前面的学习编写的程序都是单机的,即不能和其他电脑上的程序进行通信,为了让在不同的电脑上运行的软件,之间能够互相传递数据,就需要借助网络的功能。
计算机都遵守的网络通信协议叫做TCP/IP协议。
网络编程对所有开发语言都是一样的,Python也不例外。用Python进行网络编程,就是在Python程序本身这个进程内,连接别的服务器进程的通信端口进行通信。
IP协议负责把数据从一台计算机通过网络发送到另一台计算机。数据被分割成一小块一小块,然后通过IP包发送出去。由于互联网链路复杂,两台计算机之间经常有多条线路,因此,路由器就负责决定如何把一个IP包转发出去。IP包的特点是按块发送,途径多个路由,但不保证能到达,也不保证顺序到达。
TCP协议则是建立在IP协议之上的。TCP协议负责在两台计算机之间建立可靠连接,保证数据包按顺序到达。TCP协议会通过握手建立连接,然后,对每个IP包编号,确保对方按顺序收到,如果包丢掉了,就自动重发。许多常用的更高级的协议都是建立在TCP协议基础上的,比如用于浏览器的HTTP协议、发送邮件的SMTP协议等。
一个IP包除了包含要传输的数据外,还包含源IP地址和目标IP地址,源端口和目标端口。
端口有什么作用?在两台计算机通信时,只发IP地址是不够的,因为同一台计算机上跑着多个网络程序。一个IP包来了之后,到底是交给浏览器还是QQ,就需要端口号来区分。每个网络程序都向操作系统申请唯一的端口号,这样,两个进程在两台计算机之间建立网络连接就需要各自的IP地址和各自的端口号。一个进程也可能同时与多个计算机建立链接,因此它会申请很多端口。在linux系统中,端口可以有65536(2的16次方)个之多!既然有这么多,操作系统为了统一管理,所以进行了编号,这就是端口号。端口是通过端口号来标记的,端口号只有整数,范围是从0到65535。端口号不是随意使用的,而是按照一定的规定进行分配。动态端口的范围是从1024到65535。之所以称为动态端口,是因为它一般不固定分配某种服务,而是动态分配。动态分配是指当一个系统进程或应用程序进程需要网络通信时,它向主机申请一个端口,主机从可用的端口号中分配一个供它使用。当这个进程关闭时,同时也就释放了所占用的端口号。用“netstat -an”查看端口状态。
了解了TCP/IP协议的基本概念,IP地址和端口的概念,我们就可以开始进行网络编程了。
socket(简称:套接字) 是进程间通信的一种方式,它与其他进程间通信的一个主要不同是:它能实现不同主机间的进程间通信,我们网络上各种各样的服务大多都是基于 Socket 来完成通信的。
在 Python 中 使用socket 模块的函数 socket 就可以完成:
函数 socket.socket 创建一个 socket,返回该 socket 的描述符,该函数带有两个参数:
1、Address Family:可以选择 AF_INET(用于 Internet 进程间通信) 或者 AF_UNIX(用于同一台机器进程间通信),实际工作中常用AF_INET。
2、Type:套接字类型,可以是 SOCK_STREAM(流式套接字,主要用于 TCP 协议)或者 SOCK_DGRAM(数据报套接字,主要用于 UDP 协议)。
UDP是面向无连接的协议。使用UDP协议时,不需要建立连接,只需要知道对方的IP地址和端口号,就可以直接发数据包。但是,能不能到达就不知道了。虽然用UDP传输数据不可靠,但它的优点是和TCP比,速度快,对于不要求可靠到达的数据,就可以使用UDP协议。
我们来看看在UDP协议如何发送数据。
import socket
# 创建socket对象
udpSocket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# 确定对象目的地ip和端口号port,组成一个元组
destAdress = ('192.168.11.76', 7777)
# 发送内容到目标机器上,内容要编码
msg = input('>>')
#编码信息
msg = msg.encode('gbk')
#发送消息
udpSocket.sendto(msg, destAdress)
# 关闭socket对象
udpSocket.close()
结果如下:
在windows中运行“网络调试助手”:
既然可以发送消息,那如何接受消息呢?
import socket
# 创建socket对象
udpSocket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# 目的地
destAddress = ('192.168.11.76', 7777)
# 发送内容
sendMsg = input('请输入要发送的内容:')
# 编码
sendMsg = sendMsg.encode('gbk')
# 发送
udpSocket.sendto(sendMsg, destAddress)
# 接收缓存区
recvMsg = udpSocket.recvfrom(1024)
recvMsg = '【Receive from %s : %s】:%s' % (recvMsg[1][0], recvMsg[1][1], recvMsg[0].decode('gbk'))
print(recvMsg)
# 关闭socket对象
udpSocket.close()
print('over...')
结果如下:
如果我们在网络调试助手中一直不发送,pycharm里的程序将一直处于阻塞状态等待接收信息。
多次运行脚本,然后在“网络调试助手”中,看到的现象如下:
每重新运行一次网络程序,上图中画红线的数字,不一样的原因在于,这个数字标识这个网络程序,当重新运行时,如果没有确定到底用哪个,系统默认会随机分配。记住一点:这个网络程序在运行的过程中,这个就唯一标识这个程序,所以如果其他电脑上的网络程序如果想要向此程序发送数据,那么就需要向这个数字(即端口)标识的程序发送即可。
刚才接收消息是先发送,根据收到的消息得到端口号再发送回去完成了接收,那我们也可以直接绑定端口号,解决端口号随机分配的问题。
import socket
# 创建socket对象
udpSocket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# 设置本地端口
localAddress = ('', 8888)
udpSocket.bind(localAddress)
# 目的地
destAddress = ('192.168.11.76', 7777)
# 发送内容
sendMsg = input('请输入要发送的内容:')
# 编码
sendMsg = sendMsg.encode('gbk')
# 发送
udpSocket.sendto(sendMsg, destAddress)
# 接收缓存区
recvMsg = udpSocket.recvfrom(1024)
recvMsg = '【Receive from %s : %s】:%s' % (recvMsg[1][0], recvMsg[1][1], recvMsg[0].decode('gbk'))
print(recvMsg)
# 关闭socket对象
udpSocket.close()
print('over...')
结果如下:
UDP应用:echo服务器
import socket
# 1. 创建套接字
udpSocket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# 2. 绑定本地的相关信息
bindAddr = ('', 7788) # ip地址和端口号,ip一般不用写,表示本机的任何一个ip
udpSocket.bind(bindAddr)
num = 1
while True:
# 3. 等待接收对方发送的数据
recvData = udpSocket.recvfrom(1024) # 1024表示本次接收的最大字节数
# 4. 将接收到的数据再发送给对方
udpSocket.sendto(recvData[0], recvData[1])
# 5. 统计信息
print('已经将接收到的第%d个数据返回给对方,内容为:%s' % (num, recvData[0].decode('gbk')))
num += 1
# 5. 关闭套接字
udpSocket.close()
结果如下:
TCP/IP协议栈中, 传输层只有UDP可以广播。
import socket
# 创建socket对象
udpSocket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# 开启广播
udpSocket.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
# 目的地:如果是''表示当前IP,如果是'<broadcast>'表示当前所在的广播地址
destAdress = ('<broadcast>', 8080)
# 消息内容
sendMsg = input('>>')
# 编码
sendMsg = sendMsg.encode('gbk')
# 发送
udpSocket.sendto(sendMsg, destAdress)
while True:
(content, address) = udpSocket.recvfrom(1024)
print("received from %s----%s" % (address, content.decode('gbk')))
# 关闭socket对象
udpSocket.close()
print('over......')
单工、半双工、全双工 单工数据传输只支持数据在一个方向上传输;半双工数据传输允许数据在两个方向上传输,但是,在某一时刻,只允许数据在一个方向上传输,它实际上是一种切换方向的单工通信;全双工数据通信允许数据同时在两个方向上传输,因此,全双工通信是两个单工通信方式的结合,它要求发送设备和接收设备都有独立的接收和发送能力.
使用多线程完成一个全双工的聊天程序
from socket import *
from threading import *
import os
'''全局变量'''
# socket
udpSocket = None
# ip
destIp = None
# port
destPort = None
# 收
def recv():
while True:
recvInfo = udpSocket.recvfrom(1024)
print("\r<<%s:%s%s>>" % (str(recvInfo[1]), recvInfo[0].decode('gbk'), os.linesep), end='')
# 发
def send():
while True:
info = input('>>')
udpSocket.sendto(info.encode('gbk'), (destIp, destPort))
# 主方法
def main():
global udpSocket
global destIp
global destPort
udpSocket = socket(AF_INET, SOCK_DGRAM)
bindAddr = ('', 7788) # ip地址和端口号,ip一般不用写,表示本机的任何一个ip
udpSocket.bind(bindAddr)
destIp = input("对方的ip:")
destPort = int(input("对方的port:"))
tSend = Thread(target=send)
tRecv = Thread(target=recv)
tSend.start()
tRecv.start()
if __name__ == '__main__':
main()
结果如下:
使用wireshark抓包工具,使用TFTP协议。
TFTP(Trivial File Transfer Protocol,简单文件传输协议)
是TCP/IP协议族中的一个用来在客户端与服务器之间进行简单文件传输的协议
特点:
1、简单
2、占用资源小
3、适合传递小文件
4、适合在局域网进行传递
5、端口号为69
6、基于UDP实现
TFTP下载过程
TFTP服务器默认监听69号端口
当客户端发送“下载”请求(即读请求)时,需要向服务器的69端口发送
服务器若批准此请求,则使用一个新的、临时的 端口进行数据传输。
当服务器找到需要现在的文件后,会立刻打开文件,把文件中的数据通过TFTP协议发送给客户端
如果文件的总大小较大(比如3M),那么服务器分多次发送,每次会从文件中读取512个字节的数据发送过来
因为发送的次数有可能会很多,所以为了让客户端对接收到的数据进行排序,所以在服务器发送那512个字节数据的时候,会多发2个字节的数据,用来存放序号,并且放在512个字节数据的前面,序号是从1开始的,当发送的数据包排到65535的时候,会从0再开始。
因为需要从服务器上下载文件时,文件可能不存在,那么此时服务器就会发送一个错误的信息过来,为了区分服务发送的是文件内容还是错误的提示信息,所以又用了2个字节 来表示这个数据包的功能(称为操作码),并且在序号的前面。
因为udp的数据包不安全,即发送方发送是否成功不能确定,所以TFTP协议中规定,为了让服务器知道客户端已经接收到了刚刚发送的那个数据包,所以当客户端接收到一个数据包的时候需要向服务器进行发送确认信息,即发送收到了,这样的包成为ACK(应答包)
为了标记数据已经发送完毕,所以规定,当客户端接收到的数据小于516(2字节操作码+2个字节的序号+512字节数据)时,就意味着服务器发送完毕了。
TFTP数据包的格式如下:
有了之前的upd知识,接下来,模拟从TFTP上下载文件。
import struct
import socket
data = struct.pack('!H8sb5sb', 1, b'girl.jpg', 0, b'octet', 0)
udpSocket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
udpSocket.sendto(data, ('192.168.11.72', 69))
udpSocket.close()
使用wireshark抓包工具观察报文的传递,结果如下:
可以看到,运行代码后,由本机发往目标主机,操作码发送的是1,即读的请求。接着,有目标主机发到本机上一条信息,找不到该文件。
接下来,看一个下载实例:
import socket
import struct
import os
import time
def main():
# 创建文件
myFile = open('xx.avi', 'wb')
# socket对象,发送下载的请求信息,接收信息和发送确认信息
udpSocket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# 服务器地址
destAddr = ('192.168.11.76', 69)
# pack信息
data = struct.pack('!H7sb5sb', 1, b'110.avi', 0, b'octet', 0)
# 发送
udpSocket.sendto(data, destAddr)
# 定义一个变量,记录写的次数
num = 0
# 循环
while True:
# 接收
recvData, redvAddr = udpSocket.recvfrom(1024)
# unpack信息,获取操作码
operNum = struct.unpack('!H', recvData[:2])[0]
# 判断
if operNum == 3:
# unpack信息,获取块编号
blockNum = struct.unpack('!H', recvData[2:4])[0]
print(blockNum)
# 判断
num = num + 1
if num == 65536:
num = 0
if num == blockNum:
num = blockNum
# 获取数据,写到文件中
myFile.write(recvData[4:])
# 准备ack数据
ackData = struct.pack('!HH', 4, blockNum)
# 发送ack确认到服务器
udpSocket.sendto(ackData, redvAddr)
num = blockNum
if len(recvData) < 516:
break
if operNum == 5:
print('发生异常啦......')
break
# 关闭文件
myFile.close()
if __name__ == '__main__':
main()
这样就完成了客户端请求从服务端下载文件的过程。
除了以上考虑的由于客户端发送ACK码过慢,服务端又发送了一次发送过的数据包,我们在上面的代码做出了判断,那还有一种极端的情况,就是客户端发送ACK码非常慢,服务端会做出如何的操作呢?
import socket
import struct
import os
import time
def main():
# 创建文件
myFile = open('xx.avi', 'wb')
# socket对象,发送下载的请求信息,接收信息和发送确认信息
udpSocket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# 服务器地址
destAddr = ('192.168.11.76', 69)
# pack信息
data = struct.pack('!H7sb5sb', 1, b'110.avi', 0, b'octet', 0)
# 发送
udpSocket.sendto(data, destAddr)
# 定义一个变量,记录写的次数
num = 0
# 循环
while True:
# 接收
recvData, redvAddr = udpSocket.recvfrom(1024)
# unpack信息,获取操作码
operNum = struct.unpack('!H', recvData[:2])[0]
# 判断
if operNum == 3:
# unpack信息,获取块编号
blockNum = struct.unpack('!H', recvData[2:4])[0]
print(blockNum)
# 判断
num = num + 1
if num == 65536:
num = 0
if num == blockNum:
num = blockNum
# 获取数据,写到文件中
myFile.write(recvData[4:])
# 准备ack数据
ackData = struct.pack('!HH', 4, blockNum)
# 发送ack确认到服务器
time.sleep(20)
udpSocket.sendto(ackData, redvAddr)
num = blockNum
if len(recvData) < 516:
break
if operNum == 5:
print('发生异常啦......')
break
# 关闭文件
myFile.close()
if __name__ == '__main__':
main()
在上面的代码中,发送ACK的时候,让其休眠20s模拟网速极差,抓包结果如下:
也就是说服务端在收不到ACK的时候,会隔一段给客户端发送刚才的数据,当发满6次后,服务端就会给客户端报错,并停止发送,即使过了很久再次收到客户端发来的ACK,服务端也不会处理了。
大多数连接都是可靠的TCP连接。创建TCP连接时,主动发起连接的叫客户端,被动响应连接的叫服务器。
举个例子,当我们在浏览器中访问新浪时,我们自己的计算机就是客户端,浏览器会主动向新浪的服务器发起连接。如果一切顺利,新浪的服务器接受了我们的连接,一个TCP连接就建立起来的,后面的通信就是发送网页内容了。
在程序中,如果想要完成一个tcp服务器的功能,需要的流程如下:
1、socket创建一个套接字,创建Socket时,AF_INET指定使用IPv4协议,如果要用更先进的IPv6,就指定为AF_INET6。SOCK_STREAM指定使用面向流的TCP协议,这样,一个Socket对象就创建成功,但是还没有建立连接。
2、bind绑定ip和port。作为服务器,提供什么样的服务,端口号就必须固定下来。80端口是Web服务的标准端口。其他服务都有对应的标准端口号,例如SMTP服务是25端口,FTP服务是21端口,等等。端口号小于1024的是Internet标准服务的端口,端口号大于1024的,可以任意使用。
3、调用listen()方法开始监听端口
4、accept()会等待并返回一个客户端的连接
5、recv/send接收发送数据
一个很简单的tcp服务器如下:
import socket
# 创建一个服务器对象
serverSocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 绑定服务器的ip地址和端口
serverSocket.bind(('', 7777))
# 开始监听,在linux系统下listen后面不用写数字。一般服务器都建在linux系统下
serverSocket.listen(10)
print('1......')
'''
newSocket: 当服务器创建好后,当有客户端链接时
这个对象可以与当时链接的客户端;进行发,收消息
clientAddr:连接的客户端的信息(ip,port)
'''
# 等待客户端的链接
newSocket, clientAddr = serverSocket.accept()
print('2......')
print(newSocket)
print(clientAddr)
# 关
newSocket.close() # 关闭之后,客户端也会被关闭
serverSocket.close() # 项目运行中服务器一直运行,不会关闭
结果如下:
此时,没有客户端链接,打印完1......后发现程序一直在阻塞在newSocket, clientAddr = serverSocket.accept(),等待着客户端的链接。接着,我们让客户端链接服务器:
结果如下:
tcp的客户端要比服务器端简单很多,代码如下:
import socket
# socket对象
clientSocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
print('1......')
'''
连接服务器,
如果连接上,继续运行
连接不上,报错
'''
clientSocket.connect(('192.168.11.76', 7777))
print('2......')
# 关闭
clientSocket.close()
结果如下:
此时服务端没有开启,客服端的链接被拒绝,程序报错。接下来,打开服务端:
可以看出,客户端链接服务器,必须提供服务器的信息(ip和端口号)。对于自身而言,并不用像服务端一样绑定自己的端口号。
我们已经完成了客户端和服务端的建立,接下来完成客户端与服务器的收发信息。
服务端收发信息:
import socket
'''
serverSocket是用来接收新的客户端的
以后与这个连接的客户端的收发消息就不能用serverSocket了,
而是用返回来的新的newSocket
'''
serverSocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
serverSocket.bind(('', 7777))
serverSocket.listen(10)
newSocket, clientAddr = serverSocket.accept()
# 发
sendData = input('>>')
newSocket.send(sendData.encode('gbk'))
# 收
recvData = newSocket.recv(1024)
print(recvData.decode('gbk'))
newSocket.close()
serverSocket.close()
结果如下:
由于没有客户端链接,所以服务端一直在阻塞,此时,让客户端链接服务端:
服务端等待输入给客户端的信息,我们输入信息:
客户端收到了服务端发的信息,服务端依然处于阻塞,它在等待接收客户端发送的信息,我们从客户端给服务端发消息:
客户端收发消息:
import socket
clientSocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
clientSocket.connect(('192.168.11.76', 7777))
# 发
sendData = input('>>')
clientSocket.send(sendData.encode('gbk'))
# 收
recvData = clientSocket.recv(1024)
print(recvData.decode('gbk'))
clientSocket.close()
结果如下:
服务端收到客户端发送的报文,客户端阻塞,等待服务端的回复:
从上面的代码中已经实现了客户端与服务端收发信息,但是目前仅限于服务端服务于一个客户端,而且对话一次就断开了,这显然是不符合需求的。下面我们模拟一下多客户端访问服务端:
import socket
import threading
import time
def socketThreading(newSerSocket, clientAddr):
print('客户端%s:%s链接了,服务器开始服务了...' % (clientAddr[0], clientAddr[1]))
while True:
recData = newSerSocket.recv(1024)
recData = recData.decode('gbk')
if recData == '':
print('客户端%s退出了...' % clientAddr[0])
newSerSocket.close()
break
else:
print('来自于%s:%s的消息(%s):%s' % (clientAddr[0], clientAddr[1], time.strftime('%Y-%m-%d %H:%M:%S'), recData))
sendData = 'echo:%s' %recData
newSerSocket.send(sendData.encode('gbk'))
def main():
serverSocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
serverSocket.bind(('', 7777))
serverSocket.listen(10)
while True:
newSerSocket, clientAddr = serverSocket.accept()
creatThread = threading.Thread(target=socketThreading, args=(newSerSocket, clientAddr))
creatThread.start()
if __name__ == '__main__':
main()
结果如下:
可以看到,两个客户端都链接到了服务端,接下来,让两个客户端给服务端发信息:
当有一个客户端退出时,也不影响服务端服务还在访问的客户端:
可以看到,通过多线程,服务端解决了单一客户端访问的限制,加上循环后,客户端和服务端可持续互相发送报文。
既然可以有多线程服务端,也可以有多进程服务端:
import socket
import multiprocessing
import time
def socketProcess(newSerSocket, clientAddr):
while True:
recData = newSerSocket.recv(1024)
recData = recData.decode('gbk')
if recData == '':
print('客户端%s:%s退出了...' % (clientAddr[0],clientAddr[1]))
newSerSocket.close()
break
else:
print('来自于%s:%s的消息(%s):%s' % (clientAddr[0], clientAddr[1], time.strftime('%Y-%m-%d %H:%M:%S'), recData))
sendData = 'echo:%s' % recData
newSerSocket.send(sendData.encode('gbk'))
def main():
serverSocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
serverSocket.bind(('', 7777))
serverSocket.listen(10)
while True:
newSerSocket, clientAddr = serverSocket.accept()
creatprocess = multiprocessing.Process(target=socketProcess, args=(newSerSocket, clientAddr))
creatprocess.start()
'''
因为是各拿一份代码,所以在此关闭newSerSocket,不会影响其他进程。但是如果是线程,
在此将newSerSocket关闭,将会影响其他线程,其他客户端的访问将报错。
'''
newSerSocket.close()
print('客户端%s:%s链接了,服务器开始服务了...' % (clientAddr[0],clientAddr[1]))
if __name__ == '__main__':
main()
结果和多线程一样,这里不在演示。
网友评论