Python学习笔记No3

本篇包括:

  • 进程和线程
  • 网络编程入门和网络应用开发

六、进程和线程

1、相关概念

进程就是操作系统中执行的一个程序,操作系统以进程为单位分配存储空间,每个进程都有自己的地址空间、数据栈以及其他用于跟踪进程执行的辅助数据,操作系统管理所有进程的执行,为它们合理的分配资源。进程可以通过fork或spawn的方式来创建新的进程来执行其他的任务,不过新的进程也有自己独立的内存空间,因此必须通过进程间通信机制(IPC,Inter-Process Communication)来实现数据共享,具体的方式包括管道、信号、套接字、共享内存区等。

一个进程可以拥有多个并发的执行线索,即多个可以获得CPU调度的执行单元,这就是所谓的线程在同一个进程下的线程可以共享相同的上下文,因此相对于进程而言,线程间的信息共享和通信更加容易。当然在单核CPU系统中,真正的并发是不可能的,因为在某个时刻能够获得CPU的只有唯一的一个线程,多个线程共享了CPU的执行时间。使用多线程实现并发编程为程序带来的好处是不言而喻的,最主要的体现在提升程序的性能和改善用户体验,今天我们使用的软件几乎都用到了多线程技术,这一点可以利用系统自带的进程监控工具(如macOS中的“活动监视器”、Windows中的“任务管理器”)来证实。

当然多线程也并不是没有坏处,站在其他进程的角度,多线程的程序对其他程序并不友好,因为它占用了更多的CPU执行时间,导致其他程序无法获得足够的CPU执行时间;另一方面,站在开发者的角度,编写和调试多线程的程序都对开发者有较高的要求,对于初学者来说更加困难。

Python既支持多进程又支持多线程,因此使用Python实现并发编程主要有3种方式:多进程、多线程、多进程+多线程。

2、Python中的多进程

多进程的创建

Unix和Linux操作系统上提供了fork()系统调用来创建进程,调用fork()函数的是父进程,创建出的是子进程,子进程是父进程的一个拷贝,但是子进程拥有自己的PID。fork()函数非常特殊它会返回两次,父进程中可以通过fork()函数的返回值得到子进程的PID,而子进程中的返回值永远都是0。Python的os模块提供了fork()函数。由于Windows系统没有fork()调用,因此要实现跨平台的多进程编程,可以使用multiprocessing模块Process类来创建子进程,而且该模块还提供了更高级的封装,例如批量启动进程的进程池(Pool)、用于进程间通信的队列(Queue)和管道(Pipe)等。

下面用一个下载文件的例子(并没有真正下载文件…只是通过sleep函数模拟下载耗费的时间,但这并不影响原理的讲解)来说明使用多进程和不使用多进程到底有什么差别,先看看不使用多进程时的代码

from random import randint
from time import time, sleep

def download_task(filename):
	print('开始下载%s......' % filename)
	time_to_download = randint(5, 10)
	sleep(time_to_download)
	print('%s下载完成【耗费了%d秒】!' % (filename, time_to_download))


def main():
	start = time()
	download_task('IMG_2630.JPG')
	download_task('Demo1_Regex.java')
	end = time()
	print('已下载完成了两个文件,共用时%.2f秒。' % (end - start))


if __name__ == '__main__':
	main()

在这里插入图片描述
从上面的例子可以看出,程序中的代码只能按顺序一点点的往下执行。执行两个毫不相关的下载任务,需要先等待一个文件下载完成后才能开始下一个下载任务,这并不合理也没有效率。下面我们看看多进程的方式如何下载这两个任务(将两个下载任务放到不同的进程中)。

from multiprocessing import Process
from os import getpid
from random import randint
from time import time, sleep

def download_task(filename):
	print('启动下载进程,进程号[%d]' % getpid())
	print('开始下载%s...' % filename)
	time_to_download = randint(5, 10)
	sleep(time_to_download)
	print('%s下载完成【耗费了%d秒】!' % (filename, time_to_download))


def main():
	start = time()
	
	# 1. 使用Process类创建进程对象
	# :target: 传入函数download_task来表示进程启动后要执行的代码
	# :args: 一个元组,代表传递给target函数的参数
	p1 = Process(target=download_task, args=('IMG_2630.JPG', ))
	
	# 2. 调用Process对象的start方法启动进程
	p1.start()

	p2 = Process(target=download_task, args=('Demo1_Regex.java', ))
	p2.start()

	# 3. Process对象的join方法表示等待进程执行结束
	p1.join()	
	p2.join()

	end = time()
	print('已下载完成了两个文件,共用时%.2f秒。' % (end - start))


if __name__ == '__main__':
	main()

运行上面的代码可以明显发现两个下载任务“同时”启动了,而且程序的执行时间将大大缩短,不再是两个任务的时间总和。下面是程序的一次执行结果。
在这里插入图片描述

进程间的通信

我们也可以使用subprocess模块中的类和函数来创建和启动子进程,然后通过管道来和子进程通信,这些内容我们不在此进行讲解,有兴趣的读者可以自己了解这些知识。接下来我们将重点放在如何实现两个进程间的通信

实现如下程序:启动两个进程,一个输出Ping,一个输出Pong,两个进程输出的Ping和Pong加起来一共10个。听起来很简单吧,但是如果这样写可是错的哦。

from multiprocessing import Process
from time import sleep

counter = 0

def sub_task(string):
	global counter
	while counter < 10:
		print(string, end='', flush=True)
		counter += 1
		sleep(0.01)


def main():
	Process(target=sub_task, args=('Ping', )).start()
	Process(target=sub_task, args=('Pong', )).start()


if __name__ == '__main__':
	main()

在这里插入图片描述
结果不尽人意:Ping和Pong各输出了10个。原因:在程序中创建进程的时候,子进程复制了父进程及其所有的数据结构****,每个子进程有自己独立的内存空间,这也就意味着 两个子进程中各有一个counter变量,所以结果也就可想而知了。要解决这个问题比较简单的办法是使用multiprocessing模块中的Queue类,它是可以被多个进程共享的队列**,底层是通过管道信号量(semaphore)机制来实现的,有兴趣的读者可以自己尝试一下。

3、Python中的多线程

多线程的创建

在Python早期的版本中就引入了thread模块(现在名为_thread)来实现多线程编程,然而该模块过于底层,而且很多功能都没有提供,因此目前的多线程开发我们推荐使用threading模块,该模块对多线程编程提供了更好的面向对象的封装。我们把刚才下载文件的例子用多线程的方式来实现一遍。

from threading import Thread
from random import randint
from time import time, sleep

def download(filename):
	print('开始下载%s...' % filename)
	time_to_download = randint(5, 10)
	sleep(time_to_download)
	print('%s下载完成【耗费了%d秒】!' % (filename, time_to_download))


def main():
	start = time()
	
	t1 = Thread(target=download, args=('IMG_2630.JPG', ))
	t1.start()

	t2 = Thread(target=download, args=('Demo1_Regex.java', ))
	t2.start()

	t1.join()	
	t2.join()

	end = time()
	print('已下载完成了两个文件,共用时%.2f秒。' % (end - start))


if __name__ == '__main__':
	main()

在这里插入图片描述
我们可以直接使用threading模块的Thread类来创建线程(如上面程序所示),但是我们之前讲过一个非常重要的概念叫“继承”,我们可以从已有的类创建新类,因此也可以通过继承Thread类的方式来创建自定义的线程类,然后再创建线程对象并启动线程。代码如下所示。

from threading import Thread
from random import randint
from time import time, sleep


class DownloadTask(Thread):
	def __init__(self, filename):
		super().__init__()
		self._filename = filename

	def run(self):
		print('开始下载%s...' % self._filename)
		time_to_download = randint(5, 10)
		sleep(time_to_download)
		print('%s下载完成【耗费了%d秒】!' % (self._filename, time_to_download))


def main():
	start = time()
	
	t1 = DownloadTask('IMG_2630.JPG')
	t1.start()

	t2 = DownloadTask('Demo1_Regex.java')
	t2.start()

	t1.join()	
	t2.join()

	end = time()
	print('已下载完成了两个文件,共用时%.2f秒。' % (end - start))


if __name__ == '__main__':
	main()

在这里插入图片描述

线程间的通信

因为多个线程可以共享进程的内存空间,因此要实现多个线程间的通信相对简单,大家能想到的最直接的办法就是设置一个全局变量,多个线程共享这个全局变量即可。但是当多个线程共享同一个变量(我们通常称之为“资源”)的时候,很有可能产生不可控的结果从而导致程序失效甚至崩溃。如果一个资源被多个线程竞争使用,那么我们通常称之为“临界资源”,对“临界资源”的访问需要加上保护,否则资源会处于“混乱”的状态。下面的例子演示了100个线程向同一个银行账户转账(转入1元钱)的场景,在这个例子中,银行账户就是一个临界资源,在没有保护的情况下我们很有可能会得到错误的结果。

from time import sleep
from threading import Thread

class Account(object):
	def __init__(self):
		self._balance = 0

	def deposit(self, money):
		# 计算存款后的余额
		new_balance = self._balance + money
		# 模拟受理存款业务需要0.01秒的时间
		sleep(0.01)
		# 修改账户余额
		self._balance = new_balance

	@property
	def balance(self):
		return self._balance


class AddMoneyThread(Thread):
	def __init__(self, account, money):
		super().__init__()
		self._account = account
		self._money = money

	def run(self):
		self._account.deposit(self._money)


def main():
	account = Account()
	threads = []
	# 创建100个存款的线程,向同一个账户中存钱
	for _ in range(100):
		t = AddMoneyThread(account, 1) # 每次都存入1元
		threads.append(t)
		t.start()
	# 等所有存款的线程都执行完毕
	for t in threads:
		t.join()
	print('账户余额:¥%d元' % account.balance)


if __name__ == '__main__':
	main()

在这里插入图片描述
可见结果不尽人意。原因:没有对银行账户这个“临界资源”加以保护。多个线程同时向账户中存钱时,会一起执行到new_balance = self._balance + money这行代码,多个线程得到的账户余额都是初始状态下的0,所以都是0上面做了+1的操作,因此得到了错误的结果。在这种情况下,“”就可以派上用场了。我们可以通过“锁”来保护“临界资源”只有获得“锁”的线程才能访问“临界资源”,而其他没有得到“锁”的线程只能被阻塞起来,直到获得“锁”的线程释放了“锁”,其他线程才有机会获得“锁”,进而访问被保护的“临界资源”

“锁”机制

下面的代码演示了如何使用“锁”来保护对银行账户的操作,从而获得正确的结果。

from time import sleep
from threading import Thread, Lock

class Account(object):
	def __init__(self):
		self._balance = 0
		# 关键1
		self._lock = Lock()  

	def deposit(self, money):
		# 关键2:先获取锁才能执行后续的代码
		self._lock.acquire() # 获取锁
		# 关键3:try...finally...
		try:
			# 计算存款后的余额
			new_balance = self._balance + money
			# 模拟受理存款业务需要0.01秒的时间
			sleep(0.01)
			# 修改账户余额
			self._balance = new_balance
		finally:
			# 在finally中执行释放锁的操作,保证正常异常锁都能释放
			self._lock.release()

	@property
	def balance(self):
		return self._balance


class AddMoneyThread(Thread):
	def __init__(self, account, money):
		super().__init__()
		self._account = account
		self._money = money

	def run(self):
		self._account.deposit(self._money)


def main():
	account = Account()
	threads = []
	# 创建100个存款的线程,向同一个账户中存钱
	for _ in range(100):
		t = AddMoneyThread(account, 1) # 每次都存入1元
		threads.append(t)
		t.start()
	# 等所有存款的线程都执行完毕
	for t in threads:
		t.join()
	print('账户余额:¥%d元' % account.balance)


if __name__ == '__main__':
	main()

在这里插入图片描述
比较遗憾的一件事情是Python的多线程并不能发挥CPU的多核特性,这一点只要启动几个执行死循环的线程就可以得到证实了。之所以如此,是因为Python的解释器有一个“全局解释器锁”(GIL)的东西,任何线程执行前必须先获得GIL锁,然后每执行100条字节码,解释器就自动释放GIL锁,让别的线程有机会执行,这是一个历史遗留问题,但是即便如此,就如我们之前举的例子,使用多线程在提升执行效率和改善用户体验方面仍然是有积极意义的。

3、多进程还是多线程

无论是多进程还是多线程,只要数量一多,效率肯定上不去。操作系统在切换进程或者线程时需要先保存当前执行的现场环境(CPU寄存器状态、内存页等),然后,把新任务的执行环境准备好(恢复上次的寄存器状态,切换内存页等),才能开始执行。这个切换过程虽然很快,但是也需要耗费时间。如果有几千个任务同时进行,操作系统可能就主要忙着切换任务,根本没有多少时间去执行任务了,这种情况最常见的就是硬盘狂响,点窗口无反应,系统处于假死状态。所以,多任务一旦多到一个限度,反而会使得系统性能急剧下降,最终导致所有任务都做不好。

是否采用多任务的第二个考虑是任务的类型,可以把任务分为计算密集型和I/O密集型。

  • 计算密集型任务的特点是要进行大量的计算,消耗CPU资源,比如对视频进行编码解码或者格式转换等等,这种任务全靠CPU的运算能力,虽然也可以用多任务完成,但是任务越多,花在任务切换的时间就越多,CPU执行任务的效率就越低。计算密集型任务由于主要消耗CPU资源,这类任务用Python这样的脚本语言去执行效率通常很低,最能胜任这类任务的是C语言,我们之前提到了Python中有嵌入C/C++代码的机制。
  • I/O密集型任务,如涉及到网络、存储介质I/O等类的任务,这类任务的特点是CPU消耗很少,任务的大部分时间都在等待I/O操作完成(因为I/O的速度远远低于CPU和内存的速度)。对于I/O密集型任务,如果启动多任务,就可以减少I/O等待时间从而让CPU高效率的运转。有一大类的任务都属于I/O密集型任务,这其中包括了我们很快会涉及到的网络应用和Web应用。

4、单线程+异步I/O

现代操作系统对I/O操作的改进中最为重要的就是支持异步I/O。如果充分利用操作系统提供的异步I/O支持,就可以用单进程单线程模型来执行多任务,这种全新的模型称为事件驱动模型Nginx就是支持异步I/O的Web服务器,它在单核CPU上采用单进程模型就可以高效地支持多任务。在多核CPU上,可以运行多个进程(数量与CPU核心数相同),充分利用多核CPU。用Node.js开发的服务器端程序也使用了这种工作模式,这也是当下实现多任务编程的一种趋势。

在Python语言中,单线程+异步I/O的编程模型称为协程,有了协程的支持,就可以基于事件驱动编写高效的多任务程序。协程的优势:

  • 极高的执行效率。因为子程序切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销。
  • 不需要多线程的锁机制。因为只有一个线程,所以不存在同时写变量冲突,在协程中控制共享资源不用加锁,只需要判断状态就好了,所以执行效率比多线程高很多。

如果想要充分利用CPU的多核特性,最简单的方法是多进程+协程,既充分利用多核,又充分发挥协程的高效率,可获得极高的性能。关于这方面的内容,后面将做一个专题来进行讲解。

5、应用案例

例子1:多线程——将耗时间的任务放到线程中以获得更好的用户体验
如下所示的界面中,有“下载”和“关于”两个按钮,用休眠的方式模拟点击“下载”按钮会联网下载文件需要耗费10秒的时间,如果不使用“多线程”,我们会发现,当点击“下载”按钮后整个程序的其他部分都被这个耗时间的任务阻塞而无法执行了,这显然是非常糟糕的用户体验,代码如下所示。

import time
import tkinter
import tkinter.messagebox

# 鼠标点击事件1
def download():
	# 模拟下载任务需要花费10秒钟时间
	time.sleep(10)
	tkinter.messagebox.showinfo('提示', '下载完成!')


# 鼠标点击事件2
def show_about():
	tkinter.messagebox.showinfo('关于', '作者: zjy(v1.0)')


def main():
		top = tkinter.Tk()
		top.title('单线程')
		top.geometry('200x150')
		top.wm_attributes('-topmost', True)

		panel = tkinter.Frame(top)
		button1 = tkinter.Button(panel, text='下载', command=download)
		button1.pack(side='left')
		button2 = tkinter.Button(panel, text='关于', command=show_about)
		button2.pack(side='right')
		panel.pack(side='bottom')

		tkinter.mainloop()

if __name__ == '__main__':
	main()

点击“下载”之后,必须等待下载完成后,才能点击其他按钮:
在这里插入图片描述
如果使用多线程将耗时间的任务放到一个独立的线程中执行,这样就不会因为执行耗时间的任务而阻塞了主线程,修改后的代码如下所示。

import time
import tkinter
import tkinter.messagebox
from threading import Thread

def main():
	# ----------------修改部分START----------------
	class DownloadTaskHandler(Thread):

		def run(self):
			# 模拟下载任务需要花费10秒钟时间
			time.sleep(10)
			tkinter.messagebox.showinfo('提示', '下载完成!')
			# 启用下载按钮
			button1.config(state=tkinter.NORMAL)

	# 鼠标点击事件1
	def download():     		# 关键关键关键
		# 禁用下载按钮
		button1.config(state=tkinter.DISABLED)
		# 通过daemon参数将线程设置为守护线程(主程序退出就不再保留执行)
		# 在线程中处理耗时间的下载任务
		DownloadTaskHandler(daemon=True).start()
	# -----------------修改部分END------------------


	# 鼠标点击事件2
	def show_about():
		tkinter.messagebox.showinfo('关于', '作者: zjy(v1.0)')


	top = tkinter.Tk()
	top.title('单线程')
	top.geometry('200x150')
	top.wm_attributes('-topmost', 1)

	panel = tkinter.Frame(top)
	button1 = tkinter.Button(panel, text='下载', command=download)
	button1.pack(side='left')
	button2 = tkinter.Button(panel, text='关于', command=show_about)
	button2.pack(side='right')
	panel.pack(side='bottom')

	tkinter.mainloop()


if __name__ == '__main__':
	main()

可以点了“下载”后,再点“关于”,因为“下载”是在新的线程里运行:
在这里插入图片描述
例子2:多进程——使用多进程对复杂任务进行“分而治之”
我们来完成1~100000000求和的计算密集型任务,这个问题本身非常简单,有点循环的知识就能解决,代码如下所示。

from time import time


def main():
    total = 0
    number_list = [x for x in range(1, 100000001)]
    start = time()
    for number in number_list:
        total += number
    print(total)
    end = time()
    print('Execution time: %.3fs' % (end - start))


if __name__ == '__main__':
    main()

在这里插入图片描述
在上面的代码中,我故意先去创建了一个列表容器然后填入了100000000个数,这一步其实是比较耗时间的,所以为了公平起见,当我们将这个任务分解到8个进程中去执行的时候,我们暂时也不考虑列表切片操作花费的时间,只是把做运算和合并运算结果的时间统计出来,代码如下所示。

from multiprocessing import Process, Queue
from random import randint
from time import time

def task_handler(curr_list, result_queue):
	total = 0
	for number in curr_list:
		total += number
	result_queue.put(total)


def main():
	processes = [] 	# 保存8个进程
	number_list = [x for x in range(1, 100000001)] # 保存所有要相加的数
	result_queue = Queue() # 申请一个队列,用来存放8个求和结果(有助于最后对8个求和结果求总和)

	# 启动8个进程将数据切片后进行求和运算
	index = 0	
	for _ in range(8):
		p = Process(target=task_handler, 
					# 调用task_handler函数求index到(index + 12500000)之间的和
					args=(number_list[index : index+12500000], result_queue))
		index += 12500000
		processes.append(p)
		p.start()	# 每循环依次就开启了一个进程

	# 开始记录所有进程执行完毕后花费的时间
	start = time()
	for p in processes:
		p.join() 	# 使每一个进程一个个执行完

	# 合并8个部分的求和结果
	total = 0
	while not result_queue.empty():
		total += result_queue.get()
	print(total)

	end = time()
	print('Execution time: ', (end - start), 's', sep='')


if __name__ == '__main__':
	main()

在这里插入图片描述
(Confusion:第25行代码,运行start()方法后,不就开始执行了吗?所以不应该在此时作为起始时间吗,为什么是调用join()时才开始计时呢?)
比较两段代码的执行结果(两代码执行结果截图的红框处。注意:我们只是比较了运算的时间,不考虑列表创建及切片操作花费的时间),使用多进程后由于获得了更多的CPU执行时间以及更好的利用了CPU的多核特性,明显的减少了程序的执行时间,而且计算量越大效果越明显。当然,如果愿意还可以将多个进程部署在不同的计算机上,做成分布式进程,具体的做法就是通过multiprocessing.managers模块中提供的管理器将Queue对象通过网络共享出来(注册到网络上让其他计算机可以访问),这部分内容也留到爬虫的专题再进行讲解。

七、网络编程入门和网络应用开发

1.计算机网络基础

(自行查看自己学习时的书籍、笔记等。)

网络应用模式

  1. C/S模式和B/S模式。这里的C指的是Client(客户端),通常是一个需要安装到某个宿主操作系统上的应用程序;而B指的是Browser(浏览器),它几乎是所有图形化操作系统都默认安装了的一个应用软件;通过C或B都可以实现对S(服务器)的访问。关于二者的比较和讨论在网络上有一大堆的文章,在此我们就不再浪费笔墨了。
  2. 去中心化的网络应用模式。不管是B/S还是C/S都需要服务器的存在,服务器就是整个应用模式的中心,而去中心化的网络应用通常没有固定的服务器或者固定的客户端所有应用的使用者既可以作为资源的提供者也可以作为资源的访问者

2、基于HTTP协议的网络资源访问

HTTP(超文本传输协议)

HTTP是超文本传输协议(Hyper-Text Transfer Proctol)的简称,维基百科上对HTTP的解释是:超文本传输协议是一种用于分布式、协作式和超媒体信息系统的应用层协议,它是万维网数据通信的基础,设计HTTP最初的目的是为了提供一种发布和接收HTML页面的方法,通过HTTP或者HTTPS(超文本传输安全协议)请求的资源由URI(统一资源标识符)来标识。关于HTTP的更多内容,我们推荐阅读阮一峰老师的《HTTP 协议入门》简单的说,通过HTTP我们可以获取网络上的(基于字符的)资源,开发中经常会用到的网络API(有的地方也称之为网络数据接口)就是基于HTTP来实现数据传输的

JSON格式

JSON(JavaScript Object Notation)是一种轻量级的数据交换语言,该语言以易于让人阅读的文字(纯文本)为基础,用来传输由属性值或者序列性的值组成的数据对象。尽管JSON是最初只是Javascript中一种创建对象的字面量语法,但它在当下更是一种独立于语言的数据格式,很多编程语言都支持JSON格式数据的生成和解析,Python内置的json模块也提供了这方面的功能。由于JSON是纯文本,它和XML一样都适用于异构系统之间的数据交换,而相较于XMLJSON显得更加的轻便和优雅。下面是表达同样信息的XML和JSON,而JSON的优势是相当直观的。

  • XML的例子:
<?xml version="1.0" encoding="UTF-8"?>
<message>
	<from>Alice</from>
	<to>Bob</to>
	<content>Will you marry me?</content>
</message>
  • JSON的例子:
{
    "from": "Alice",
    "to": "Bob",
    "content": "Will you marry me?"
}

目前绝大多数网络数据服务(或称之为网络API)都是基于HTTP协议提供JSON格式的数据(如果想了解国内的网络数据服务,可以看看聚合数据阿凡达数据等网站,国外的可以看看{API}Search网站)。

requests库

有关介绍已转移到《Python爬虫》—— requests库 章节。

3、基于传输层协议的套接字编程

套接字:通俗的说,套接字就是一套用C语言写成的应用程序开发库,主要用于实现进程间通信和网络编程,在网络应用开发中被广泛使用。在Python中也可以基于套接字来使用传输层提供的传输服务,并基于此开发自己的网络应用。实际开发中使用的套接字可以分为三类:流套接字(TCP套接字)、数据报套接字和原始套接字。

TCP套接字

TCP套接字是使用TCP协议提供的传输服务来实现网络通信的编程接口。在Python中可以通过创建socket对象并指定type属性为SOCK_STREAM来使用TCP套接字。由于一台主机可能拥有多个IP地址,而且很有可能会配置多个不同的服务,所以作为服务器端的程序,需要在创建套接字对象后将其绑定到指定的IP地址和端口上(端口可以用于区分不同的服务,例如我们通常将HTTP服务跟80端口绑定,而MySQL数据库服务默认绑定在3306端口)。
在这里插入图片描述

  • 下面的代码实现了一个提供时间日期的服务器
from socket import socket, SOCK_STREAM, AF_INET
from datetime import datetime

def main():
    # 1.创建套接字对象并指定使用哪种传输服务
    # family=AF_INET - IPv4地址
    # family=AF_INET6 - IPv6地址
    # type=SOCK_STREAM - TCP套接字
    # type=SOCK_DGRAM - UDP套接字
    # type=SOCK_RAW - 原始套接字
    server = socket(family=AF_INET, type=SOCK_STREAM)

    # 2.绑定一个IP地址和端口到这个服务器程序上(端口用于区分不同的服务)
    server.bind(('127.0.0.1', 6789))

    # 3.开启监听 - 监听客户端连接到服务器
    server.listen(512)	# 参数512可以理解为连接队列的大小
    print('服务器启动开始监听...')

    # 4.通过循环接收客户端的连接并作出相应的处理(提供服务)
    while True:      
        # 4.0 等待客户端连接到服务器  
        # accept方法是一个阻塞方法.如果没有客户端连接到服务器代码不会向下执行
        # accept方法返回一个元组,
        # 其中的第一个元素是客户端对象,
        # 第二个元素是连接到服务器的客户端的地址(由IP和端口两部分构成)
        client, addr = server.accept()
        print(str(addr) + '连接到了服务器.')        
        # 4.1 发送数据
        client.send(str(datetime.now()).encode('utf-8'))
        # 4.2 断开连接
        client.close()


if __name__ == '__main__':
    main()
  1. 首先F5运行服务器程序:
    在这里插入图片描述
  2. 然后打开cmd,通过telnet命令(如果在使用该命令时遇到了这里这样的错误)来访问该服务器:
    telnet 127.0.0.1 16789
  3. 即可看到如下结果:
    在这里插入图片描述
    在这里插入图片描述
  • 下面通过Python程序实现TCP客户端的功能(相较于实现服务器程序,实现客户端程序就简单多了):
from socket import socket

def main():
    # 1.创建套接字对象(默认使用IPv4和TCP协议)
    client = socket()
    # 2.连接到服务器(必须要指定连接到的IP地址和端口)
    client.connect(('127.0.0.1', 6789))
    # 3.从服务器接收数据
    print(client.recv(1024).decode('utf-8'))
    client.close()


if __name__ == '__main__':
    main()
  1. 首先运行上面的服务器程序:
    在这里插入图片描述
  2. 运行客户端程序即可看到结果:
    在这里插入图片描述
    在这里插入图片描述
    需要注意的是,上面的服务器并没有使用多线程或者异步I/O的处理方式,这也就意味着当服务器与一个客户端处于通信状态时,其他的客户端只能排队等待。很显然,这样的服务器并不能满足我们的需求,我们需要的服务器是能够同时接纳和处理多个用户请求的。
  • 下面我们来设计一个使用多线程技术处理多个用户请求的服务器:服务器端会向连接到本服务器的客户端发送一张图片

1)服务器端代码:

from socket import socket, SOCK_STREAM, AF_INET
from base64 import b64encode
from json import dumps
from threading import Thread

def main():

    # 自定义线程类
    class FileTransferHandler(Thread):

        def __init__(self, cclient):
            super().__init__()
            self.cclient = cclient

        def run(self):
            # run1. 创建一个字典对象
            my_dict = {}   
            my_dict['filename'] = 'E:/sublime_python/IMG_2630.jpg' # 向字典中添加一组元素
            my_dict['filedata'] = data # data是成员属性,能够从这个类的实例对象里得到

            # run2. 通过dumps函数将字典处理成JSON字符串
            json_str = dumps(my_dict)

            # run3. 将JSON字符串再编码成二进制后,进行传送
            self.cclient.send(json_str.encode('utf-8'))
            self.cclient.close()

    # 1. 创建套接字对象并指定使用哪种传输服务
    server = socket()
    # 2. 绑定IP地址和端口
    server.bind(('127.0.0.1', 5566))
    # 3. 开启监听
    server.listen(512)
    print('服务器启动,开始监听...')

    try:
        with open('E:/sublime_python/IMG_2630.jpg', 'rb') as f:
            # JSON是纯文本不能携带二进制数据,所以图片的二进制数据要处理成base64编码
            data = b64encode(bytes(f.read().decode(encoding='utf-8', errors='ignore')))
    except FileNotFoundError:
        print('无法打开指定的文件!')
    except LookupError:
        print('指定了未知的编码!')
    except UnicodeDecodeError:
        print('读取文件时解码错误!')

    while True:
        client, addr = server.accept()
        # 启动一个线程来处理客户端的请求
        FileTransferHandler(client).start()


if __name__ == '__main__':
    main()

(PS. 未解决。下面的错误不知如何解决。事实上,在此之前,已经出现了多个错误,但已纠正,这个错误真的不知道该怎么搞。)
在这里插入图片描述
2)客户端代码:

from socket import socket
from json import loads
from base64 import b64decode

def main():
	# 1. 创建客户端对象,并指定要连接的服务器
	client = socket()
	client.connect(('127.0.0.1', 5566))

	# 2. 定义一个保存二进制数据的对象
	in_data = bytes()
	data = client.recv(1024) # 由于不知道服务器发送的数据有多大,所以这里设定每次接受1024字节

	# 3. 将接收到的数据拼接起来
	while data:
		in_data += data
		data = client.recv(1024)

	# 4. 将收到的二进制数据解码成JSON字符串并转换成字典
	my_dict = loads(in_data.decode('utf-8')) # loads函数的作用就是将JSON字符串转成字典对象
	filename = my_dict['filename']
	filedata = my_dict['filedata'].encode('utf-8')
	with open('E:/sublime_python/pics' + filename, 'wb') as f:
		# 将base64格式的数据解码成二进制数据并写入文件
		f.write(b64decode(filedata))
	print('图片已保存.')


if __name__ == '__main__':
	main()

在这个案例中,我们使用了JSON作为数据传输的格式(通过JSON格式对传输的数据进行了序列化和反序列化的操作),但是JSON并不能携带二进制数据,因此对图片的二进制数据进行了Base64编码的处理。Base64是一种用64个字符表示所有二进制数据的编码方式,通过将二进制数据每6位一组的方式重新组织,刚好可以使用0~9的数字、大小写字母以及“+”和“/”总共64个字符表示从000000111111的64种状态。维基百科上有关于Base64编码的详细讲解,不熟悉Base64的读者可以自行阅读。

UDP套接字

传输层除了有可靠的传输协议TCP之外,还有一种非常轻便的传输协议叫做用户数据报协议,简称UDP。TCP和UDP都是提供端到端传输服务的协议,二者的差别就如同打电话和发短信的区别,后者不对传输的可靠性和可达性做出任何承诺从而避免了TCP中握手和重传的开销,所以在强调性能和而不是数据完整性的场景中(例如传输网络音视频数据),UDP可能是更好的选择。可能大家会注意到一个现象,就是在观看网络视频时,有时会出现卡顿,有时会出现花屏,这无非就是部分数据传丢或传错造成的。在Python中也可以使用UDP套接字来创建网络应用,对此我们不进行赘述,有兴趣的读者可以自行研究。

4、网络应用开发

发送电子邮件

公司向应聘者发出录用通知、网站向用户发送一个激活账号的链接、银行向客户推广它们的理财产品等几乎都是通过电子邮件来完成的,而这些任务应该都是由程序自动完成的。

就像我们可以用HTTP(超文本传输协议)来访问一个网站一样,发送邮件要使用简单邮件传输协议SMTP,SMTP也是一个建立在TCP(传输控制协议)提供的可靠数据传输服务的基础上的应用级协议,它规定了邮件的发送者如何跟发送邮件的服务器进行通信的细节,而Python中的smtplib模块将这些操作简化成了几个简单的函数。

  • 程序1:发送一封邮件
from smtplib import SMTP
from email.header import Header
from email.mime.text import MIMEText

def main():
    # 请自行修改下面的邮件发送者和接收者
    sender = '[email protected]'
    receivers = ['[email protected]']
    message = MIMEText('用Python发送邮件的示例代码.', 'plain', 'utf-8')
    message['From'] = Header('zjy', 'utf-8')
    message['To'] = Header('zyy', 'utf-8')
    message['Subject'] = Header('示例代码实验邮件', 'utf-8')
    smtper = SMTP('smtp.126.com')
    # 请自行修改下面的登录口令
    smtper.login(sender, 'secretpass')
    smtper.sendmail(sender, receivers, message.as_string())
    print('邮件发送完成!')


if __name__ == '__main__':
    main()
  • 程序2:发送一封带有附件的邮件
from smtplib import SMTP
from email.header import Header
from email.mime.text import MIMEText
from email.mime.image import MIMEImage
from email.mime.multipart import MIMEMultipart

import urllib


def main():
    # 创建一个带附件的邮件消息对象
    message = MIMEMultipart()
    
    # 创建文本内容
    text_content = MIMEText('附件中有本月数据请查收', 'plain', 'utf-8')
    message['Subject'] = Header('本月数据', 'utf-8')
    # 将文本内容添加到邮件消息对象中
    message.attach(text_content)

    # 读取文件并将文件作为附件添加到邮件消息对象中
    with open('/Users/Hao/Desktop/hello.txt', 'rb') as f:
        txt = MIMEText(f.read(), 'base64', 'utf-8')
        txt['Content-Type'] = 'text/plain'
        txt['Content-Disposition'] = 'attachment; filename=hello.txt'
        message.attach(txt)
    # 读取文件并将文件作为附件添加到邮件消息对象中
    with open('/Users/Hao/Desktop/汇总数据.xlsx', 'rb') as f:
        xls = MIMEText(f.read(), 'base64', 'utf-8')
        xls['Content-Type'] = 'application/vnd.ms-excel'
        xls['Content-Disposition'] = 'attachment; filename=month-data.xlsx'
        message.attach(xls)
    
    # 创建SMTP对象
    smtper = SMTP('smtp.126.com')
    # 开启安全连接
    # smtper.starttls()
    sender = '[email protected]'
    receivers = ['[email protected]']
    # 登录到SMTP服务器
    # 请注意此处不是使用密码而是邮件客户端授权码进行登录
    # 对此有疑问的读者可以联系自己使用的邮件服务器客服
    smtper.login(sender, 'secretpass')
    # 发送邮件
    smtper.sendmail(sender, receivers, message.as_string())
    # 与邮件服务器断开连接
    smtper.quit()
    print('发送完成!')


if __name__ == '__main__':
    main()

发送短信

发送短信也是项目中常见的功能,网站的注册码、验证码、营销信息基本上都是通过短信来发送给用户的。在下面的代码中我们使用了互亿无线短信平台(该平台为注册用户提供了50条免费短信以及常用开发语言发送短信的demo,可以登录该网站并在用户自服务页面中对短信进行配置)提供的API接口实现了发送短信的服务,当然国内的短信平台很多,读者可以根据自己的需要进行选择(通常会考虑费用预算、短信达到率、使用的难易程度等指标),如果需要在商业项目中使用短信服务建议购买短信平台提供的套餐服务。

import urllib.parse
import http.client
import json

def main():
    host  = "106.ihuyi.com"
    sms_send_uri = "/webservice/sms.php?method=Submit"
    # 下面的参数需要填入自己注册的账号和对应的密码
    params = urllib.parse.urlencode({'account': '你自己的账号', 'password' : '你自己的密码', 'content': '您的验证码是:147258。请不要把验证码泄露给其他人。', 'mobile': '接收者的手机号', 'format':'json' })
    print(params)
    headers = {'Content-type': 'application/x-www-form-urlencoded', 'Accept': 'text/plain'}
    conn = http.client.HTTPConnection(host, port=80, timeout=30)
    conn.request('POST', sms_send_uri, params, headers)
    response = conn.getresponse()
    response_str = response.read()
    jsonstr = response_str.decode('utf-8')
    print(json.loads(jsonstr))
    conn.close()


if __name__ == '__main__':
    main()
发布了53 篇原创文章 · 获赞 33 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/jy_z11121/article/details/97394575