socket网络编程
socket
是进程间通信的一种方式,它与其他进程间通信的一个主要不同是:它能实现不同主机间的进程间通信,我们网络上各种各样的服务大多
都是基于Socket来完成通信的,例如我们每天浏览网页、刷朋友圈、收发email等等。要解决网络上两台主机之间的进程通信问题,
首先要唯一标识该进程,在 TCP/IP 网络协议中,就是通过(IP地址,协议,端口号)三元组来标识进程的,解决了进程标识问题,
就有了通信的基础了。
函数 socket.socket 创建一个 socket,返回该 socket 的描述符,将在后面相关函数中使用。该函数带有两个参数:
Address Family:(地址簇)
1,AF_INET(用于 Internet 进程间通信)
2,AF_UNIX(用于同一台机器进程间通信)
Type:(套接字类型)
1,SOCKET_STREAM(流式套接字,主要用于 TCP 协议)
2,SOCKET_DGRAM(数据报套接字,主要用于SOCKET_DGRAM(数据报套接字,主要用于 UDP 协议)
先来看以下socket都有那些参数呢?
sk = socket.socket(socket.AF_INET,socket.SOCK_STREAM,0)
sk.bind(address)
s.bind(address) 将套接字绑定到地址。address地址的格式取决于地址族。在AF_INET下,以元组(host,port)的形式表示地址。
sk.listen(backlog)
开始监听传入连接。backlog指定在拒绝连接之前,可以挂起的最大连接数量。
backlog等于5,表示内核已经接到了连接请求,但服务器还没有调用accept进行处理的连接个数最大为5
这个值不能无限大,因为要在内核中维护连接队列
sk.setblocking(bool)
是否阻塞(默认True),如果设置False,那么accept和recv时一旦无数据,则报错。
sk.accept()
接受连接并返回(conn,address),其中conn是新的套接字对象,可以用来接收和发送数据。address是连接客户端的地址。
接收TCP 客户的连接(阻塞式)等待连接的到来
sk.connect(address)
连接到address处的套接字。一般,address的格式为元组(hostname,port),如果连接出错,返回socket.error错误。
sk.connect_ex(address)
同上,只不过会有返回值,连接成功时返回 0 ,连接失败时候返回编码,例如:10061
sk.close()
关闭套接字
sk.recv(bufsize[,flag])
接受套接字的数据。数据以字符串形式返回,bufsize指定最多可以接收的数量。flag提供有关消息的其他信息,通常可以忽略。
sk.recvfrom(bufsize[.flag])
与recv()类似,但返回值是(data,address)。其中data是包含接收数据的字符串,address是发送数据的套接字地址。
sk.send(string[,flag])
将string中的数据发送到连接的套接字。返回值是要发送的字节数量,该数量可能小于string的字节大小。即:可能未将指定内容全部发送。
sk.sendall(string[,flag])
将string中的数据发送到连接的套接字,但在返回之前会尝试发送所有数据。成功返回None,失败则抛出异常。
内部通过递归调用send,将所有内容发送出去。
sk.sendto(string[,flag],address)
将数据发送到套接字,address是形式为(ipaddr,port)的元组,指定远程地址。返回值是发送的字节数。该函数主要用于UDP协议。
sk.settimeout(timeout)
设置套接字操作的超时期,timeout是一个浮点数,单位是秒。值为None表示没有超时期。一般,超时期应该在刚创建套接字时设置,因为它们可能用于连接的操作(如 client 连接最多等待5s )
sk.getpeername()
返回连接套接字的远程地址。返回值通常是元组(ipaddr,port)。
sk.getsockname()
返回套接字自己的地址。通常是一个元组(ipaddr,port)
sk.fileno()
套接字的文件描述符
# 在python3以后,无论是收还是发,必须是字节类型。
一,简单的soket的例子

1 import socket 2 client = socket.socket() # 声明socket类型,同时生成socket连接对象 3 client.connect(("localhost", 6969)) # 要连接的的主机名或者IP地址,端口号 4 5 client.send(b"Hello World!") # python3+ 发送的只能是字节 6 data = client.recv(1024) # 接收字节的大小 7 8 print(data) 9 client.close() # 关闭连接

1 import socket 2 server = socket.socket() 3 server.bind(("localhost", 6969)) # 绑定要监听的端口,记住bind里面是一个元组 4 server.listen() 5 print("等风来!") 6 7 conn, addr = server.accept() # 等风来 8 # conn就是客户端连进来而在服务器端为其生成的一个连接实例 9 print(conn, addr) 10 11 print("风来了!") 12 data = conn.recv(1024) 13 print("receive:", data) 14 conn.send(data.upper()) 15 server.close()
二,上述只能发一次,能不能发一次,收一次呢?!不间断的接收呢?接着看。

1 import socket 2 client = socket.socket() # 声明socket类型,同时生成socket连接对象 3 client.connect(("localhost", 6969)) # 要连接的的主机名或者IP地址,端口号 4 while True: #--------------------->增加一个while循环 5 inp = input("请输入:") 6 if len(inp) == 0: # 如果为空时,直接跳出此次循环,继续让输入。否则服务端卡会卡在conn.recv那,等着接收。 7 continue 8 client.send(bytes(inp, encoding="utf-8")) # python3+ 发送的只能是字节 9 data = client.recv(1024) # 接收字节的大小 10 print(str(data, encoding="utf-8")) 11 client.close() # 关闭连接

1 import socket 2 server = socket.socket() 3 server.bind(("localhost", 6969)) # 绑定要监听的端口 4 server.listen() 5 print("等风来!") 6 7 conn, addr = server.accept() # 等风来 8 # conn就是客户端连进来而在服务器端为其生成的一个连接实例 9 print(conn, addr) 10 print("风来了!") 11 while True: # --------------------------->也增加了一个while循环 12 data = conn.recv(1024) 13 print("receive:", str(data, encoding="utf-8")) 14 conn.send(data.upper()) 15 server.close()
三,这次看着不错!但是又有新的问题来了,上述只能满足一个客户端不间断的收发,如果有多个客户端呢?!用户能排队跟server进行收发操作,而且用户挂断对服务端没有任何影响。客户端随时可以连接服务端,进行消息的收发,往下面看。

1 import socket 2 ip_port = ("127.0.0.1", 9000) 3 client = socket.socket() 4 client.connect(ip_port) 5 while True: 6 data = input(">>>").strip() 7 if len(data) == 0: # 判断,输入为空重新输入 8 continue 9 if data == "exit": # 如果,客户端输入字符'exit',客户端退出,此时在window上,服务端recv到的字符为空, 10 break # 客户端会卡住,服务端也会卡住,等着客户端发。 11 client.send(bytes(data, encoding="utf-8")) 12 data_rev = client.recv(102400) 13 print(str(data_rev, encoding="utf-8")) 14 client.close()

1 import socket 2 ip_port = ("127.0.0.1", 9000) 3 server = socket.socket() 4 server.bind(ip_port) 5 server.listen(5) # 开机,listen里面的值后面需要注意 6 while True: 7 print("waiting...") 8 conn, addr = server.accept() # 等电话 9 while True: 10 try: 11 data_rev = conn.recv(102400) 12 if len(data_rev) == 0: 13 print("客户端正常退出了!") 14 break 15 conn.send(data_rev.upper()) 16 print(data_rev) 17 except Exception: 18 print("客户端非法挂断") # 比如,用户直接关闭客户端程序 19 break 20 conn.close() # 挂断电话 21 server.close() # 关机了
总结:
1.基于python3.5版本的socket只能收发字节(python2.7可以发送str)
2.退出只在客户端退出就ok了
3.client.accept, client.recv()是阻塞的(基于链接正常)
4.listen(n) n代表:能挂起的连接数,如果n=1,代表可以链接一个,挂起一个,第三个则会被拒绝
四,如何对上述代码进行改造,让其支持远程执行命令?

1 import socket 2 ip_port = ("127.0.0.1", 9000) 3 client = socket.socket() 4 client.connect(ip_port) 5 while True: 6 data = input("cmd >>>").strip() 7 if len(data) == 0: # 判断,输入为空重新输入 8 continue 9 if data == "exit": # 如果,客户端输入字符'exit',客户端退出,此时在window上,服务端recv到的字符为空 10 break 11 client.send(bytes(data, encoding="utf-8")) 12 data_rev = client.recv(102400) 13 print(str(data_rev, encoding="utf-8")) 14 client.close()

1 import socket 2 import subprocess 3 ip_port = ("127.0.0.1", 9000) 4 server = socket.socket() 5 server.bind(ip_port) 6 server.listen(0) # 开机 7 while True: 8 print("waiting...") 9 conn, addr = server.accept() # 等电话 10 while True: 11 try: 12 data_rev = conn.recv(1024) 13 if len(data_rev) == 0: 14 print("客户端正常退出了!") 15 break 16 p = subprocess.Popen(str(data_rev, encoding="utf8"), shell=True, stdout=subprocess.PIPE) 17 res = p.stdout.read() # 注意由于实在windows操作系统,这里为gbk编码的字节 18 res1 = str(res, encoding="gbk") # 首先转换成字符串 19 if len(res) == 0: # 因为如果客户端输入了非法的命令返回结果则为空 20 conn.send(bytes('cmd error ', encoding="utf-8")) 21 else: 22 conn.send(bytes(res1, encoding="utf8")) 23 print(data_rev) 24 except Exception: 25 print("客户端非法挂断") 26 break 27 conn.close() # 挂断电话 28 server.close() # 关机了
五, 此时上述代码也会有一个问题: 当客户端要在服务端执行的命令返回结果过长时,客户端接收不过来。此时客户端再执行新的命令时,服务端的返回结果依然是上一个命令的结果,知道把结果返回完,才开始执行客户端发出的紧接的那条长命令的命令。这种现象叫做粘包效应。如何解决?
思路一:
可以加大客户端的client.recv(102400)括号里面的值,但是这个值,不可以无限加大,最终会受限于
硬件本身,比如网卡的MTU值(网卡最大的传输单元)
思路二:由于客户端没有接收完毕,这里我们可以让客户端循环接收完毕,至于怎么判断客户端是否循环
接收完毕了呢?我们可以在服务端执行完计算出来,然后告知客户端这个命令的执行结果大小,客户端在循环
接收时的大小如果和服务端告知的大小相等,证明已经接收完毕。

1 import socket 2 import chardet # 检测编码类型,这里没什么用 3 ip_port = ("127.0.0.1", 9000) 4 client = socket.socket() 5 client.connect(ip_port) # 连接服务端,如果服务端已经存在一个连接,那么挂起 6 7 while True: # 基于connect建立的连接来循环发送消息 8 data = input("cmd >>>").strip() 9 if len(data) == 0: # 判断输入是否为空,为空重新输入 10 continue 11 if data == "exit": # 如果客户端输入字符'exit',客户端退出,此时在window上,服务端recv到的字符为空 12 break 13 client.send(bytes(data, encoding="utf-8")) # 给服务端发送cmd命令 14 data = client.recv(1024) 15 rev_data = str(data, encoding='utf-8') 16 if rev_data.startswith("Size"): # 接收服务端传送过来的执行结果的字节长度,为下面判断执行结果是否全部返回做准备 17 size = rev_data.split("/")[1] 18 else: # 如果服务端发送过来的不是执行结果长度,就因该是'cmd error',此时需要用户重新输入,注意continue的运用 19 print(rev_data) 20 continue 21 22 rev_message = "" 23 new_size = 0 24 while True: # 循环接收服务端发送过来的执行结果,直到接受完为止 25 data1 = client.recv(1024) 26 rev_data1 = str(data1, encoding="utf-8") 27 rev_message += rev_data1 # 接收的字符串累加 28 new_size += len(data1) # 接收的执行结果长度不断增大 29 print("TotalSize:", int(size), "ReceiveSize:", new_size) 30 # 31 if new_size == int(size): # 如果接收的大小和原始执行结果的大小相等,说明传送完毕。 32 break 33 print(rev_message) 34 client.close()

1 import socket 2 import subprocess 3 import chardet 4 5 ip_port = ("127.0.0.1", 9000) # 定义元组 6 server = socket.socket() # 绑定协议 ,生成套接字 7 server.bind(ip_port) # 绑定ip+协议+端口,用来表示唯一的进程,ip+port必须是元组格式 8 server.listen(0) # 开机,定义最大连接数 9 while True: # 用来重复接收新的连接 10 print("waiting...") 11 conn, addr = server.accept() # 接收客户端连接请求,conn相当于一个链接,addr是客户端的ip+port 12 while True: # 基于一个链接重复收发消息 13 try: # 判断用户是不是异常退出 14 data_rev = conn.recv(1024) 15 if len(data_rev) == 0: # 客户端正常退出,exit时,接收的值为空! 16 print("客户端正常退出了!") 17 break 18 p = subprocess.Popen(str(data_rev, encoding="utf8"), shell=True, stdout=subprocess.PIPE) 19 res = p.stdout.read() # 获取标准输出,注意由于是在windows操作系统,这里为gbk编码的字节 20 print("cmd res:", chardet.detect(res)) 21 if len(res) == 0: # 如果执行命令有误, 返回值为空 22 conn.send(bytes('cmd error ', encoding="utf-8")) 23 else: 24 data = str(res, encoding="GB2312") # 执行命令成功,首先把gb2312转换成字符串 25 send_data = bytes(data, encoding="utf-8") 26 conn.send(bytes("Size/{}".format(len(send_data)), encoding="utf-8")) 27 # 发送原始执行结果的长度,以便客户端判断是否接收完毕 28 print("Size/{}".format(len(send_data))) 29 conn.send(send_data) 30 print("send_data", chardet.detect(send_data)) 31 # 判断字符编码,没什么用 32 print(str(send_data, encoding="utf-8")) 33 except Exception: 34 print("客户端非法挂断") 35 break 36 conn.close() # 挂断电话 37 server.close() # 关机了
六, 上述代码尽管已经很完美了,但是好像还有一个重大的缺陷。就是不支持多用户同时进行访问,因为就是对并发的支持。
接着看下面如何做到多用户并行执行命令???
client端
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | import socket ip_port = ( '127.0.0.1' , 8009 ) client = socket.socket() client.connect(ip_port) data = client.recv( 102400 ) print ( str (data, encoding = "utf-8" )) while True : inp = input ( "cmd >>>" ).strip() client.send(bytes(inp, encoding = 'utf-8' )) if len (inp) = = 0 : continue if inp = = "exit" : break rev_data = client.recv( 102400 ) print ( str (rev_data, encoding = "utf-8" )) |
server端
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | import socketserver import subprocess class MyServer(socketserver.BaseRequestHandler): def handle( self ): # 注意必须是handle方法,否则不会正常执行,因为在继承BaseRequestHandler会首先执行一个handle方法 # 而下面在继承时首先会继承自身的handle方法,可以结合BaseRequestHandler源码来看 # print self.request,self.client_address,self.server conn = self .request # 此时的self.request就相当于conn conn.sendall(bytes( '欢迎致电 10086,请输入1xxx,0转人工服务.' , encoding = "utf-8" )) flag = True while flag: print ( "waiting rev" ) try : data = conn.recv( 1024 ) data = str (data, encoding = 'utf-8' ) res = subprocess.Popen(data, shell = True , stdout = subprocess.PIPE, stderr = subprocess.PIPE) res1 = str (res.stderr.read(), encoding = "gbk" ) # 命令正确执行 # print("error:", len(res1)) res2 = str (res.stdout.read(), encoding = "gbk" ) # 命令错误 # print("right:", len(res2)) if data = = 'exit' : flag = False if len (res1) = = 0 : # 当错误的结果为空时表示命令正确执行,返回正确的结果 conn.sendall(bytes(res2, encoding = "utf-8" )) if len (res2) = = 0 : # 当正确的结果为空时表示命令执行错误,返回错误的结果 conn.sendall(bytes(res1, encoding = "utf-8" )) if len (res1) = = 0 and len (res2) = = 0 : # 如果执行的命令没有返回结果 conn.sendall(bytes( "cmd is not stdout" , encoding = "utf-8" )) print (data) except Exception as tx: # 客户端非法关闭 print (tx) break if __name__ = = '__main__' : server = socketserver.ThreadingTCPServer(( '127.0.0.1' , 8009 ), MyServer) # 每请求过来时都会实例化这个类 server.serve_forever() |
上述代码之所以能够支持多并发,是因为有socketserver的支持,那么socketserver又为什么能够支持多并发呢?原因如下:
1,IO多路复用
2,多线程,多进程,协程
七,socketserver
io多路复用
I/O多路复用指:通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。
python中有一个select模块,其中提供了:select、poll、epoll三个方法,分别调用select、poll、epol 从而实现
IO多路复用。
windows python:只支持select
Mac python: 只支持select
linux python:支持select、poll、epoll
select:监听的数量有限制1024,内部通过for循环来实现
poll: 监听的数量没有限制了,但是内部依然使用for循环来实现的,效率依然不高
epool:实现的机制发生了变化,不再是通过for一个一个去看了,而是只要有变化通知我即可,效率大为提高。nignx就是利用的epool,所以并发处理能力比较强!
注意:网络操作、文件操作、终端操作等均属于IO操作,对于windows只支持Socket操作,其他系统支持其他IO操作,但是无法检测 普通文件操作 自动上次读取是否已经变化。
这些一般在写代码的时候用不到,但是以后在看源码的时候比如torado等,非常有用。不然以后会看不懂。
简单点来说,IO多路复用,用来监听socket对象内部是否变化了。
可以同时监听多个客户端的连接
select, poll, epoll
用来监听socket内部是否已经变化了。
什么时候会发生变化?连接或者收发消息时,会变化。
服务器端socket对象发生变化
服务器有两个sock对象:
sk: 有新连接进来了
conn:要发消息了
伪并发
1, 监听sk对象的变化
服务端
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | import socket import select sk = socket.socket() sk.bind(( "127.0.0.1" , 9997 )) sk.listen( 5 ) while True : rlist, wlist, e = select.select([sk], [], [], 1 ) # 监听sk(服务器)对象的变化,如果sk对象发生变化,表示有客户端来连接了,此时的rlist值为[sk] # 1代表超时时间。如果过1秒没有新连接,就是个空列表。 print (rlist) for r in rlist: conn, address = r.accept() conn.sendall(bytes( "hello !" , encoding = 'utf-8' )) |
流程:监听socket对象,只要有新连接进来,就给他发送一条消息!省去了单个socket需要等待accept的过程。因为下面的for循环执行的前提是rlist不为空,就表示有人来连接才会执行。
客户端
1 2 3 4 5 6 7 8 9 10 11 12 | import socket sk = socket.socket() sk.connect(( '127.0.0.1' , 9997 )) r = sk.recv( 102400 ) print ( str (r, encoding = 'utf-8' )) while True : inp = input ( ">>>" ) if len (inp) = = 0 : continue sk.sendall(bytes(inp, encoding = 'utf-8' )) data = sk.recv( 102400 ) print ( str (data, encoding = 'utf-8' )) |
2, 同时监听sk和conn对象的变化
服务端
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | import socket import select sk = socket.socket() sk.bind(( "127.0.0.1" , 9997 )) sk.listen( 5 ) inputs = [sk] # 这个里面的sk为服务端自己的sk while True : rlist, wlist, e = select.select(inputs, [], [], 1 ) # 监听sk(服务器)对象的变化,如果sk对象发生变化,表示有客户端来连接了,此时的rlist值为[sk] # 监听conn对象,如果conn发生变化,表示客户端于新消息发过来了,此时的rlist的值为[客户端] print ( len (inputs), len (rlist)) for r in rlist: if r = = sk: # 表示有新用户来连接了 conn, address = r.accept() # conn是什么?其实也是一个socket对象,是专门为某sk连接创立的 inputs.append(conn) conn.sendall(bytes( "hello !" , encoding = 'utf-8' )) else : # 表示有人发消息了 r.recv( 1024 ) |
流程:服务端本身监听自己的sk。
只要有新的客户端来连接时:
len(rlist) 0 ---> 1 ---> 0 此时len(inputs) 1---> 2 --->2 ....该值会一直累加
只要客户端发送消息时:
len(rlist) 0 ---> 1 ----> 0 此时len(inputs) n --> n ---> n .....该值不变
即当有新的客户端来连接,首先会监听到sk的变化,rlist中的值为[sk],当有已经连接上的客户端发送消息时,rlist中的值[sk, conn]
注意理解!!!
总结:但是此时并非正常的并发,因为现在的所谓“并发”在通过for循环来实现的。就是一个人可以聊多个QQ,但是并不能在同一个时间点跟多人聊天。
客户端
1 2 3 4 5 6 7 8 9 10 | import socket sk = socket.socket() sk.connect(( '127.0.0.1' , 9997 )) r = sk.recv( 102400 ) print ( str (r, encoding = 'utf-8' )) while True : inp = input ( ">>>" ) if len (inp) = = 0 : continue sk.sendall(bytes(inp, encoding = 'utf-8' )) |
3, 如果客户端断开连接,socket又是如何变化了,对上述代码进行完善!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | import socket import select sk = socket.socket() sk.bind(( "127.0.0.1" , 9997 )) sk.listen( 5 ) inputs = [sk] while True : rlist, wlist, e = select.select(inputs, [], [], 1 ) # 监听sk(服务器)对象的变化,如果sk对象发生变化,表示有客户端来连接了,此时的rlist值为[sk] # 监听conn对象,如果conn发生变化,表示客户端于新消息发过来了,此时的rlist的值为[客户端] print ( len (inputs), len (rlist)) for r in rlist: if r = = sk: # 表示有新用户来连接了 conn, address = r.accept() # conn是什么?其实也是一个socket对象,是专门为某连接创立的 inputs.append(conn) conn.sendall(bytes( "hello !" , encoding = 'utf-8' )) else : # 表示有人发消息了 print ( "============" ) try : ret = r.recv( 1024 ) if not ret: # 对于linux操作系统,会返回空值 raise Exception( '客户端断开连接了' ) except Exception as e: # 对于window操作系统,会直接报错 print (e) inputs.remove(r) |
总结:上面这些是贯穿IO复用的基础的知识点
4, 其实上述例子中在收到消息时,直接就可以进行发送了,但是读写在一起了,如何做到读写分离呢?这时就需要用到第二个参数。
第二参数比较单一,只要第二个参数有什么值,wlist里面就有什么值,只要第二个参数里面有值,select就会一直变化。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | import socket import select sk = socket.socket() sk.bind(( "127.0.0.1" , 9997 )) sk.listen( 5 ) inputs = [sk, ] outputs = [] while True : rlist, wlist, e = select.select(inputs, outputs, [], 1 ) # select中的第二个参数写什么值,wlist就获取到什么值 # 监听sk(服务器)对象的变化,如果sk对象发生变化,表示有客户端来连接了,此时的rlist值为[sk] # 监听conn对象,如果conn发生变化,表示客户端于新消息发过来了,此时的rlist的值为[客户端] print ( len (inputs), len (rlist), len (wlist), len (outputs)) for r in rlist: if r = = sk: # 表示有新用户来连接了 conn, address = r.accept() # conn是什么?其实也是一个socket对象,是专门为某连接创立的 inputs.append(conn) conn.sendall(bytes( "hello" , encoding = 'utf-8' )) else : # 表示有人发消息了 print ( "============" ) try : ret = r.recv( 1024 ) # r.sendall(ret) # 可以一收,一发,但是并没有做到读写分离 if not ret: # 对于linux操作系统,会返回空值 raise Exception( '客户端断开连接了' ) else : outputs.append(r) # 把每次给我发消息的人都存到outputs这个列表里面 except Exception as e: # 对于window操作系统,会直接报错 inputs.remove(r) for w in outputs: w.sendall(bytes( "response" , encoding = 'utf-8' )) outputs.remove(w) # 因为已经回过消息了,再次就不再回复 |
5, 上述例子也会有一个问题,就是在发送消息时,拿不到接收的消息?如何才能在回复时,回复的是:客户端发过来的消息+response呢?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | import socket import select sk = socket.socket() sk.bind(( "127.0.0.1" , 9997 )) sk.listen( 5 ) inputs = [sk, ] outputs = [] messages = {} # 存放 {对象1:[消息1,消息2], 对象2:[消息1,消息2] } while True : rlist, wlist, e = select.select(inputs, outputs, [], 1 ) # select中的第二个参数写什么值,wlist就获取到什么值 # 监听sk(服务器)对象的变化,如果sk对象发生变化,表示有客户端来连接了,此时的rlist值为[sk] # 监听conn对象,如果conn发生变化,表示客户端于新消息发过来了,此时的rlist的值为[客户端] print ( len (inputs), len (rlist), len (wlist), len (outputs)) for r in rlist: if r = = sk: # 表示有新用户来连接了 conn, address = r.accept() # conn是什么?其实也是一个socket对象,是专门为某连接创立的 inputs.append(conn) messages[conn] = [] conn.sendall(bytes( "hello" , encoding = 'utf-8' )) else : # 表示有人发消息了 print ( "============" ) try : ret = r.recv( 1024 ) # r.sendall(ret) # 可以一收,一发,但是并没有做到读写分离 if not ret: # 对于linux操作系统,会返回空值 raise Exception( '客户端断开连接了' ) else : outputs.append(r) # 把每次给我发消息的人都存到outputs这个列表里面 messages[r].append(ret) # 哪个对象给我发消息,我就把消息存放到那个对象对应的列表里面 except Exception as e: # 对于window操作系统,会直接报错 inputs.remove(r) del messages[r] # 当断开连接的时候把对象对应的消息列表给删除了 for w in wlist: msg = messages[w].pop() # 这就是上一次发的消息 resp = msg + bytes( "response" , encoding = 'utf-8' ) w.sendall(resp) outputs.remove(w) # 因为已经回过消息了,再次就不再回复 |
八,socketserver源码剖析(重点掌握)
1 2 3 4 5 6 7 8 9 10 11 12 | import socketserver class MyClass(socketserver.BaseRequestHandler): def handle( self ): conn = self .request mes = "欢迎来到10086...." conn.sendall(bytes(mes, encoding = 'utf-8' )) pass obj = socketserver.ThreadingTCPServer(( '127.0.0.1' , 9998 ), MyClass) obj.serve_forever() |
第一步:加载类MyClass
第二步:实例化socketserver.ThreadingTCPServer类。
1,首先会执行ThreadingTCPServer类的___init___方法,结果没有
2,发现有两个父类,先调用左边的父类的ThreadingMixIn的__init__方法---> 也没有,再执行右边父类TCPServer的__init__方法
第三步:调用对象中的server_forever()方法
1,首先开执行server_forever,自己本身没有,开始找父类,左边父类也没有,右边父类里面有并执行。
2,这时发现server_forever方法中又调用了_handle_request_noblock(),开始找这个方法(记住每次找都要从最底部开始找),发现父 类的父类中BaseServer中有并执行。
3,此时在_handle_request_noblock()中又调用了process_request()方法,这是还是一样要从根开始找,即执行ThreadingMixIn类中的这个方法,而不是BaseServer类中的这个方法。
4,这时在process_request()又开始调用了finish_request()方法,一样从底部开始找,找到并执行BaseServer类中的这个方法。
5,在finish_request()中开始执行RequestHandlerClass()这个方法,而这个方法正是用户传递的类MyClass,也就是说需要实例化MyClas s类
6,执行首先执行MyClass类中__init__方法,发现没有,接着执行父类的__init__方法,而这个__init__中就开始调用handle()方法了,这时在MyClass类中定义的就有handle()方法,所以首先执行自己的handle()方法,这就是为什么只要用户一连接上来就会执行handle()方法的原因所在。
记住:必知必会,必须要用在源码里面找一遍执行流程,很能锻炼人!!!来解释:为什么执行上面这段代码时会首先执行MyClass中的handle方法。一定注意:不要完全相信pycharm定义的函数,pycharm定义的时候还没有那么智能!
觉得本文不错的,别忘了点赞支持以下哦!
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步