一、socket基础
socket通常被称为“套接字”,用来描述IP地址和端口,是一个通信链的句柄,应用程序通过它向网络发出请求或者应答网络的请求。file模块是对某个指定文件进行打开、读写、关闭,而socket也是一种特殊的文件,一些socket的函数可以对它进行打开、读写、关闭的操作,不同的是socket是针对服务器端和客户端进行的,对于网络接口来说收发数据就相当于读写数据。
socket链接,发送、接收数据示意图
import socket # 服务器端 ip_port = ('222.179.200.138',9999) # 定义一个IP地址和端口 sk = socket.socket() # 定义一个socket句柄,相当于实例化一个对象 sk.bind(ip_port) # 在服务器端绑定要监听的端口 sk.listen(5) # 监听端口,参数为最大连接数 while True: print("server waiting...") conn,addr = sk.accept() # 等待接受客服端的连接数据,若没有接收到连接数据则会阻塞,一直停在这个地方, # 若接收到连接数据,则生成一个专为这个客户端服务的实例,返回这个实例对象和客户端地址端口 client_data = conn.recv(1024) # 通过这个客户端实例接收发来的数据,参数为自己定义的比特(字符)数 print(str(client_data,'utf-8')) conn.sendall(bytes('不要回答,不要回答。。。','utf-8')) # 向客户端发出数据,以比特的形式 conn.close()
import socket # 客户端 ip_port = ('222.179.200.138',9999) # 定义一个IP地址和端口 sk = socket.socket() # 定义一个socket句柄,相当于实例化一个对象 sk.connect(ip_port) # 连接服务器 sk.sendall(bytes('请求占领地球','utf-8')) # 向服务器端发送数据,以比特的形式 server_reply = sk.recv(1024) # 接受服务器端发来的数据,参数为自己定义的比特(字符)数 print(str(server_reply,'utf-8')) sk.close()
能够连续传输数据
import socket # 服务器端 ip_port = ('222.179.200.138',9999) # 定义一个IP地址和端口 sk = socket.socket() # 定义一个socket句柄,相当于实例化一个对象 sk.bind(ip_port) # 在服务器端绑定IP端口 sk.listen(5) # 监听端口,参数为最大连接数 while True: print("server waiting...") conn,addr = sk.accept() # 等待接受客服端的连接数据,若没有接收到连接数据则会阻塞,一直停在这个地方, # 若接收到连接数据,则生成一个转为这个客户端服务的实例,返回这个实例对象和客户端地址 client_data = conn.recv(1024) # 通过这个客户端实例接收发来的数据,参数为自己定义的比特(字符)数 print(str(client_data,'utf-8')) conn.sendall(bytes('不要回答,不要回答。。。','utf-8')) # 向客户端发出数据,以比特的形式,也可用conn.sendall('不要回答,不要回答。。。'.encode('utf-8'))
while True:
client_data = conn.recv(1024)
print(str(client_data, 'utf-8'))
server_response = input(">>:").strip()
conn.send(bytes(server_response,'utf-8'))
conn.close()
import socket # 客户端 ip_port = ('222.179.200.138',9999) # 定义一个IP地址和端口 sk = socket.socket() # 定义一个socket句柄,相当于实例化一个对象 sk.connect(ip_port) # 连接服务器 sk.sendall(bytes('请求占领地球','utf-8')) # 向服务器端发送数据,以比特的形式 server_reply = sk.recv(1024) # 接受服务器端发来的数据,参数为自己定义的比特(字符)数
print(str(server_reply,'utf-8')) while True: user_input = input(">>:").strip() sk.send(bytes(user_input,'utf-8')) server_reply = sk.recv(1024) print(str(server_reply, 'utf-8')) sk.close()
客户端给一个指令,服务器执行命令,并能接收大量数据,在python3中使用unicode字符的字符串类型str,字符串类型str成为基础类型,数据传输是按bytes传输,计算机内部的命令等结果也是bytes,而读取文件、键盘输入等都是str,所以在数据传输时要进行转换,即str<-->bytes,str转化为bytes需要进行编码,在转化的过程中可以进行多种编码格式的转化,bytes转化为str则需要进行解码操作,相互转化过程如下
decode encode
bytes ------> str(unicode)------>bytes
import socket import subprocess # 服务器端 ip_port = ('222.197.200.141',9999) # 定义一个IP地址和端口 sk = socket.socket() # 定义一个socket句柄,相当于实例化一个对象 sk.bind(ip_port) # 在服务器端绑定IP端口 sk.listen(5) # 监听端口,参数为最大连接数 while True: print("server waiting...") conn,addr = sk.accept() # 等待接受客户端的连接数据,若没有接收到连接数据则会阻塞,一直停在这个地方, # 若接收到连接数据,则生成一个转为这个客户端服务的实例,返回这个实例对象和客户端地址 while True: client_data = conn.recv(1024) print(str(client_data,'utf-8')) cmd = str(client_data,'utf-8').strip() cmd_call = subprocess.Popen(cmd,shell=True,stdout=subprocess.PIPE) # 执行命令,并把结果输出管道 cmd_result = cmd_call.stdout.read() # cmd_call.stdout.read(),把输出管道中的内容读出来 print(type(cmd_result)) if len(cmd_result)==0: # 如果管道中没有数据,给出一个没有数据的结果 # file1 = open('E:/123.txt','r') # 如果读取文件 # file2 = file1.read() # 读出的是str # print(file2) # cmd_result = bytes(file2,'utf-8') # 将str按utf-8的格式编码为bytes # print(cmd_result.decode()) # 将bytes解码为str打印出来 str1 = '--没有数据输出--' # str类型数据 cmd_result = bytes(str1,'utf-8') # 将str按utf-8的格式编码为bytes ack_msg = bytes('CMD_RESULT_SIZE|%s' %len(cmd_result),'utf-8') # 给出服务器要向客户端发送数据的长度 conn.send(ack_msg) # 发送包含数据长度的信息 client_ack = conn.recv(20) # 从客户端接收一条数据,目的是带来阻塞状态,避免和下面要发送的数据粘包 if str(client_ack.decode())=='Ready': # client_ack.decode()功能相当于解除了utf-8的编码,str和bytes对应 conn.send(cmd_result) conn.close()
import socket # 客户端 ip_port = ('222.197.200.141',9999) # 定义一个IP地址和端口(服务器端的) sk = socket.socket() # 定义一个socket句柄,相当于实例化一个对象 sk.connect(ip_port) # 连接服务器 while True: user_input = input("cmd:").strip() if len(user_input)==0: continue if user_input=='q': break sk.send(bytes(user_input,'utf-8')) #server_reply = sk.recv(1024) # 当服务器返回发送的数据大于1024时,客户端接收不完,下一次命令时会接收上一次命令的数据, # 所以需要服务器端先把数据的长度发给客户端,然后客户端按照长度读取数据 # ack_msg = bytes('CMD_RESULT_SIZE|%s' %len(cmd_result),'utf-8'),这是服务器给客户端发的含有数据长度的一条数据 server_ack_msg = sk.recv(100) # 接收含有数据长度的数据 cmd_res_msg = str(server_ack_msg,'utf-8').split('|') print("server response:",cmd_res_msg) cmd_msg_size = 0 if cmd_res_msg[0] == 'CMD_RESULT_SIZE': # 判断一下接受的数据开头是不是客户端发来的包含长度的数据 cmd_msg_size = int(cmd_res_msg[1]) sk.send(bytes("Ready",'utf-8')) # 向服务器发送一条数据,避免服务器向客户端发送数据是粘包(粘包,连续的的两次传输数据,第一次传输可能连带第二次准备传输的数据) res=b'' # 定义一个空bytes类型字符串,用来存储从客户端接收的数据 rec_size = 0 # 记录已经接受到的数据的长度 while rec_size<cmd_msg_size: data = sk.recv(1024) rec_size += len(data) res += data else: print(res.decode()) print("----revc done----") sk.close()
二、socket常用语法
Socket Families(地址簇):
socket.AF_UNIX # unix本机通信(很少用)
socket.AF_INET # IPV4协议通信(常用)
socket.AF_INET6 # IPV6协议通信
Socket Types(socket类型):
socket.SOCK_STREAM # for tcp
socket.SOCK_DGRAM # for udp
socket.RAW # 原始套接字
socket.SOCK_RAW # 一种可靠的udp形式,保证交付数据,但不保证顺序
socket.socket(family=AF_INET,type=SOCK_STREAM,proto=0,fileno=None) # 定义一个socket句柄,相当于实例化一个对象,括号内参数为默认值
sk.bind(address) # 将套接字绑定到地址,地址形式取决于地址簇,AF_INET下的地址格式为(host, port)
sk.listen(backlog) # 服务器开始监听传人链接,backlog为在拒绝链接之前,可以挂起的最大链接数,不能为无限大,一般为5
sk.setblocking(bool) # 是否阻塞,默认为True,若设置为Flase,那么accept和recv时一旦无数据则会报错
sk.accept() # 接受链接,并返回(conn,address),conn为新的套接字对象,可以用来接收和发送数据,address为客户端地址
sk.connect(address) # 链接到address处的套接字,address的格式为(host, port)
sk.connect_ex(address) # 功能同上,不过有返回值,链接成功时返回0,链接失败返回编码
sk.close() # 关闭套接字
sk.recv(bufsize) # 接收套接字的数据,数据是str的形式(unicode),bufsize为数据量的大小,单位为bytes,通常设置不大于8K
sk.recvfrom(bufsize) # 功能同上,但返回值为(data,address),data为数据字符串,address是接字地址
sk.send(string) # 将string数据发送到链接的套接字,返回值是要发送的字节数量,该数量可能小于string,即未将制定内容全部发送
sk.sendall(string) # 功能同上,但在返回值之前会把数据全部发送,即内部通过递归调用send,将所用内容发出去
sk.sendto(string,address) # 将数据发送到套接字,address是一个(ipaddr,port)的元组,指定远程地址
sk.settimeout(timeout) # 设置套接字操作的超时时间,timeout是一个浮点数,单位是秒,None表示没有超时
sk.getpaarname() # 得到连接套接字的远程地址,返回值为(ipaddr,port)的元组
sk.getsockname() # 得到链接套接字自己的地址,返回值为(ipaddr,port)的元组
sk.fileto() # 得到套接字的文件描述符
三、SocketServer
服务器和客户端不是一对一的服务,有时一个服务器需要链接多个客户端,这就需要socketserver功能,每当一个客户端连接到服务器,服务器就会实例化一个对应客户端的对象。
两个常用的socket.server类型,即定义的类
class socketserver.TCPServer(server_adddress,RequestHandleClass,bind_and_activate=True)
class socketserver.UDPServer(server_adddress,RequestHandleClass,bind_and_activate=True)
创建一个socket.server需要一下几步,首先要写一个请求处理类(request handler class),这个类要继承BaseRequestHandler,并且要重写父类里的handle()方法;其次,实例化一个socket.server,并且传递server IP和上面的请求处理类给这个socket.server;再次,用server.handle_request()处理一个请求或server.serve_forever()一个个处理多个请求,永远执行下去
import socketserver class MyTCPHandler(socketserver.BaseRequestHandler): # 定义一个自己的请求处理类,并继承socketserver中的BaseRequestHandler def handle(self): # 重写父类中的handle()方法,与客户端的交互都在这个方法中完成 while True: # 每次实例化时,会执行一次handle()方法,如果不加入循环,只会接收一次数据,然后客户端再发送数据服务器端不能接收,出错 try: self.data = self.request.recv(1024).strip() # 接收发来的数据,并去除数据两端的空格 print("{0} {1} wrote: ".format(self.client_address[0],self.client_address[1])) # 得到客户端的IP地址和端口 print(self.data) # 显示接收到的数据 self.request.send(('CMD_RESULT_SIZE|%s' %len(self.data)).encode('utf-8')) self.ack = self.request.recv(1024).strip() if self.ack.decode() == 'Ready': self.request.send(self.data.upper()) # 发送数据 except ConnectionResetError as error_name: # 客户端停止,会抓取这个错误 print("出现错误:%s" %error_name) break # 不break退出循环会一直显示错误 if __name__ == "__main__": Host,Port = "0.0.0.0",9999 # 定义server的IP和端口,为防止服务器机器上有多个IP地址,可以写成"0.0.0.0" # server_test = socketserver.TCPServer((Host,Port),MyTCPHandler) # 实例化一个socket.server,此处类型为TCPServer server_test = socketserver.ThreadingTCPServer((Host,Port),MyTCPHandler) # 实现多线程,可以链接多个客户端 server_test.serve_forever() # 接收请求
import socket # 客户端 ip_port = ('10.66.6.114',9999) # 定义一个(要链接的服务器对象的)IP地址和端口 sk = socket.socket() # 定义一个socket句柄,相当于实例化一个对象 sk.connect(ip_port) # 连接服务器 while True: user_input = input("cmd:").strip() if len(user_input)==0: continue if user_input=='q': break sk.send(bytes(user_input,'utf-8')) #server_reply = sk.recv(1024) # 当服务器返回发送的数据大于1024时,客户端接收不完,下一次命令时会接收上一次命令的数据, # 所以需要服务器端先把数据的长度发给客户端,然后客户端按照长度读取数据 # ack_msg = bytes('CMD_RESULT_SIZE|%s' %len(cmd_result),'utf-8'),这是服务器给客户端发的含有数据长度的一条数据 server_ack_msg = sk.recv(100) # 接收含有数据长度的数据 cmd_res_msg = str(server_ack_msg,'utf-8').split('|') print("server response:",cmd_res_msg) cmd_msg_size = 0 if cmd_res_msg[0] == 'CMD_RESULT_SIZE': # 判断一下接受的数据开头是不是客户端发来的包含长度的数据 cmd_msg_size = int(cmd_res_msg[1]) sk.send(bytes("Ready",'utf-8')) # 向服务器发送一条数据,避免服务器向客户端发送数据是粘包 res=b'' # 定义一个空bytes类型字符串,用来存储从客户端接收的数据 rec_size = 0 # 记录已经接受到的数据的长度 while rec_size < cmd_msg_size: data = sk.recv(1024) rec_size += len(data) res += data else: print(res.decode()) print("----revc done----") sk.close()
四、paramiko模块
paramiko是用python语言写的一个模块,遵循SSH2协议,支持以加密和认证的方式,进行远程服务器的连接,例如需要使用windows客户端,远程连接到Linux服务器,或者要从服务器上下载文件,仅需要在本地上安装相应的软件(python以及PyCrypto),对远程服务器没有配置要求,对于连接多台服务器,进行复杂的连接操作特别有帮助
import paramiko # 创建SSH对象,SSH是目前较可靠,专为远程登录会话和其他网络服务提供安全性的协议 ssh = paramiko.SSHClient() # 通过公共方式进行认证 (不需要在known_hosts 文件中存在),没有自动添加ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) # 链接服务器 ssh.connect(hostname='10.66.6.114',port=22,username='Arelax',password='123') # hostname为域名或者IP地址 # 执行命令 stdin,stdout,stderr = ssh.exec_command('cd') # 参数为命令,返回值有三个,stdin为输入命令,stdout为输出结果,stderr为输出结果错误, # 其中stdout和stderr只能输出一个 # 获取命令结果 result = stdout.read() # 关闭连接 ssh.close() # 通过sftp方式进行文件传输 # 链接服务器 transport = paramiko.Transport(('hostname',22)) transport.connect(username='Arelax',password='123') # 建立一个实例 sftp = paramiko.SFTPClient.from_transport(transport) # 将本地123.py文件上传至服务器保存为456.py文件 sftp.put('/tmp/123.py','/tmp/456.py') # 第一参数为本地文件路径,第二参数为服务器文件路径 # 将服务器456.py文件下载到本地保存为123.py文件 sftp.get('/tmp/456.py','/tmp/123.py') # 第一参数为服务器文件路径,第二参数为本地文件路径 # 关闭连接 transport.close()
访问服务器需要在代码中给出用户名、密码等,存在隐患,可以不用密码,使用密钥的形式进行访问,密钥分为公钥和私钥,公钥在要访问的机器上,私钥在自己的机器上
import paramiko # 创建私钥的对象 private_key = paramiko.RSAKey.from_private_key_file('私钥的文件路径') # 公钥放在要访问的服务器中 # 创建SSH对象,SSH是目前较可靠,专为远程登录会话和其他网络服务提供安全性的协议 ssh = paramiko.SSHClient() # 通过公共方式进行认证 (不需要在known_hosts 文件中存在),没有自动添加ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) # 链接服务器 ssh.connect(hostname='10.66.6.114',port=22,username='Arelax',pkey=private_key) # hostname为域名或者IP地址,用私钥登陆,不再用密码 # 执行命令 stdin,stdout,stderr = ssh.exec_command('cd') # 参数为命令,返回值有三个,stdin为输入命令,stdout为输出结果,stderr为输出结果错误, # 其中stdout和stderr只能输出一个 # 获取命令结果 result = stdout.read() # 关闭连接 ssh.close() # 通过sftp方式进行文件传输 # 链接服务器 transport = paramiko.Transport(('hostname',22)) transport.connect(username='Arelax',pkey=private_key) # 用私钥登陆,不再用密码 # 建立一个实例 sftp = paramiko.SFTPClient.from_transport(transport) # 将本地123.py文件上传至服务器保存为456.py文件 sftp.put('/tmp/123.py','/tmp/456.py') # 第一参数为本地文件路径,第二参数为服务器文件路径 # 将服务器456.py文件下载到本地保存为123.py文件 sftp.get('/tmp/456.py','/tmp/123.py') # 第一参数为服务器文件路径,第二参数为本地文件路径 # 关闭连接 transport.close()
五、线程和进程
线程是操作系统能够进行运算调度的最小单位,它被包含在进程中,一条线程指在进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务,一个进程(例如QQ程序)作为一个整体给操作系统管理,包含对各种资源的调用,例如内存管理、网络接口调用等,对各种资源管理的集合就成为进程,对CPU的操作是一个个线程,进程只是线程的集合,本身不能被执行,同一个CUP(核)同一时间只能执行一个线程,在不同的线程之间来回切换,速度很快,好像同时执行多个线程
import threading import time def running(n): print('task %s !'%n) time.sleep(2) # running(1) # running(2) t1 = threading.Thread(target=running,args=(1,)) # 定义一个线程,第一个参数为函数名,第二个参数为函数的参数 t2 = threading.Thread(target=running,args=(2,)) t1.start() t2.start() # 用类的方式启动线程,不常用 class MyThread(threading.Thread): def __init__(self,n): # 重写父类 super(MyThread,self).__init__() self.n = n def run(self): # 这个函数名必须是run print("running task %s !" %self.n) t3 = MyThread(3) t4 = MyThread(4) t3.start() t4.start() t3.join() # 等待这个线程的执行结果,线程不结束程序不会往下执行 time_start = time.time() # 循环启动多个线程 t_obj = [] for i in range(5,100): t = threading.Thread(target=running,args=(i,)) t.start() t_obj.append(t) # 等待所有线程执行结束 for r in t_obj: r.join() time_end = time.time() print("the cost is %s !"%(time_end-time_start),threading.current_thread(),threading.active_count()) # 显示出程序运行时间,显示出线程,显示活跃的线程个数
子线程都是由一个主线程生成的,普通情况下一个主线程要等所有的子线程结束后主线程才能结束,但对于一个主线程的守护线程,主线程不必等守护线程全部结束之后才能结束,只需要等非守护线程结束即可
# !usr/bin/env python # -*- coding: utf-8 import threading import time def running(n): print('task %s !'%n) time.sleep(2) time_start = time.time() # 循环启动多个线程 t_obj = [] for i in range(5,100): t = threading.Thread(target=running,args=(i,)) t.setDaemon(True) # 把一个子线程变为守护线程,必须放在start()之前 t.start() t_obj.append(t) ''' # 守护线程不用再为主线程设置等待所有线程执行结束 for r in t_obj: r.join() ''' time_end = time.time() print("the cost is %s !"%(time_end-time_start),threading.current_thread(),threading.active_count()) # 显示出程序运行时间,显示出线程,显示活跃的线程个数
由于子线程之间是公用数据的,在python2中,当一份数据(例如num=0)被一个子线程拷贝执行修改命令(使num+1),并且在没有修改完成的情况下,同样的数据被另外一个子线程拷贝执行修改命令(num+1),当第二个子线程修改完成后(num=1),会继续执行第一个线程的程序,但第一个线程的程序的数据是原始的数据(num=0),而不是被第二个线程修改后的数据,这样第一个线程执行完后数据的结果还是和第二个线程执行完的相同(目的是num=2),这样就造成修改数据的错误,所以需要在修改数据之前加上一把锁,当一个线程对这个数据修改完成之后,第二个线程才能对这个数据进行操作,python3不需要这个功能
import threading import time def running(n): lock_num.acquire() # 获取一把锁,从这里开始,在一个线程没有执行完lock_num.release()这行代码, # 另一个线程不能执行lock_num.acquire()和lock_num.release()之间的代码 global num # 再函数中修改全局变量,需要声明 num += 1 lock_num.release() # 解开这把锁,python2中使用这个功能,python3不需要这个功能 lock_num = threading.Lock() # 生成一个锁的实例 num = 0 # 循环启动多个线程 t_obj = [] for i in range(0,100): t = threading.Thread(target=running,args=(i,)) t.start() t_obj.append(t) # 等待所有线程执行结束 for r in t_obj: r.join() print("the task is done !",threading.current_thread(),threading.active_count()) # 显示出程序运行时间,显示出线程,显示活跃的线程个数 print('the num is ',num)
在一个函数中有锁,同时调用的另外的函数也有锁(即有多重锁)的情况下要用递归锁,否则程序会锁死
import threading import time def run1(): print("--run1 is begining--") lock_num.acquire() global num1 num1 += 1 lock_num.release() return num1 def run2(): print("--run2 is begining--") lock_num.acquire() global num2 num2 += 1 lock_num.release() return num2 def run3(): lock_num.acquire() res1 = run1() print("--between run1 and run2--") res2 = run2() lock_num.release() print(res1,res2) num1 = 0 num2 = 0 lock_num = threading.RLock() # 生成递归锁对象 for i in range(100): r = threading.Thread(target=run3) r.start() while threading.active_count() != 1: # 判断子线程是否允许完毕,线程数为所有子线程加一个主线程 print(threading.active_count()) else: print("---all threading done!---") print(num1,num2)
信号量(semaphore)指允许同时运行的子线程的个数,锁每次允许一个线程运行,信号量可以运行多个
import threading import time def run3(): semaphore_num.acquire() # 得到信号量 global num1 time.sleep(1) num1 += 1 semaphore_num.release() # 释放信号量 num1 = 0 semaphore_num = threading.BoundedSemaphore(10) # 生成信号量对象,参数为最大允许子线程运行的个数 for i in range(102): r = threading.Thread(target=run3) r.start() while threading.active_count() != 1: # 判断子线程是否允许完毕,线程数为所有子线程加一个主线程 print(threading.active_count()) else: print("---all threading done!---") print(num1)
event可以实现两个或多个线程之间的交互,例如红绿灯为一个线程,车行走为一个线程,灯变绿则车走,这两个线程之间存在一定的联系,他们之间可以相互进行交互,可以用event的方法来实现
import threading import time def light_color(): count_num = 0 event_test.set() # 设置事件的标志位,有标志位可以看作是绿灯 while True: if count_num>30 and count_num<50: # 30秒绿灯时间结束 event_test.clear() # 情况事件标志位,代表红灯 print("\033[41;1mthe light is red....\033[0m") elif count_num>50: # 20秒红灯时间结束 event_test.set() count_num = 0 # 清空从新计数 else: print("\033[42;1mthe light is green....\033[0m") time.sleep(1) count_num += 1 def car_name(name): while True: if event_test.is_set(): # 判断是否有标志位,若有返回True print("the car[%s] is going..." %name) time.sleep(5) else: print("the car[%s] is waiting..." %name) event_test.wait() # 等待状态,等待标志位出现,若有标志位则继续执行下面代码,否则一直等待 print("the car[%s] is going..." %name) event_test = threading.Event() light = threading.Thread(target=light_color) light.start() car = threading.Thread(target=car_name,args=("tesla",)) car.start()
import queue import threading import time def producter(name): global count_bone while True: queue_bone.put(count_bone) print("[%s]生产了骨头[%s]..." %(name,count_bone)) count_bone += 1 time.sleep(0.2) def customer(name): while True: print("[%s]得到了骨头[%s]..." %(name,queue_bone.get())) time.sleep(3) count_bone = 1 queue_bone = queue.Queue(maxsize=20) # 参数为队列中允许存在的数据的最大个数 p1 = threading.Thread(target=producter,args=("zhu",)) p2 = threading.Thread(target=producter,args=("zhang",)) c1 = threading.Thread(target=customer,args=("liu",)) c2 = threading.Thread(target=customer,args=("li",)) p1.start() p2.start() c1.start() c2.start()