• 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()  # 关闭连接
    socket客户端
     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()
    socket服务端

    二,上述只能发一次,能不能发一次,收一次呢?!不间断的接收呢?接着看。

     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端

    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端

    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对象的变化

    服务端

    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不为空,就表示有人来连接才会执行。

    客户端

    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对象的变化

    服务端

    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,但是并不能在同一个时间点跟多人聊天。

    客户端

    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又是如何变化了,对上述代码进行完善!
    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就会一直变化。
    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呢?
    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源码剖析(重点掌握)

    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定义的时候还没有那么智能!

    觉得本文不错的,别忘了点赞支持以下哦!

  • 相关阅读:
    Variable() placeholder() constant() 的区别
    scrapy-redis+selenium+webdriver 部署到linux上
    scrapy-redis+selenium+webdriver解决动态代理ip和user-agent的问题(全网唯一完整代码解决方案)
    [学习记录]NLTK常见操作二(分句,分词,词干提取)
    [学习记录]NLTK常见操作一(去网页标记,统计词频,去停用词)
    [学习记录]python正则表达式
    [学习记录]MySQL之初级查询语句(select,where)
    [学习记录]pymysql的基本操作
    [学习记录]MySQL临时整理
    [学习记录]面对wxpython的长跑(100米:wxpython安装,相关文件,wx.App,wx.Frame)
  • 原文地址:https://www.cnblogs.com/yang-ning/p/6375780.html
Copyright © 2020-2023  润新知