使用Socket控制前后端的数据交换和Web应用的资源消耗

xiaoxiao2021-02-28  21

自从学会了使用Socket搭建一个简单的服务器并实现它同客户端通信,我就一直很想在实际的工作中用上这个我很喜欢的功能,而其中最让我容易想到的就是利用它来管理进程开始和停止的时机。但在之前的好几次尝试中,我要么在过程中就发现有比Socket简单得多而且也有效的方法,要么就是最后发现即使用Socket也实现不了我的想法,只得另寻他路。这也算是成了我工作中一个不大不小的执念。

直到最近,我使用Django在后台构建了一个Web应用,由于其功能需要需要在后台运行,且会长时间消耗服务器的大量(5%-10%)计算资源,但用户可能不一定需要等这个程序全部运行完成就能得到他想要的结果。所以出于对降低服务器负载的考虑,我需要让它尽可能达到这样一种情况——一旦用户不再需要这个程序继续工作,程序就能马上停止。

如果这个程序是运行在前端的,那么很简单——一旦用户关闭了网页或跳转到别的页面,程序的运行就会自然终止了。然而我这个程序是运行在后端的。大家都知道http协议是一种无状态的协议,也就是说,就算你在网页上设计了一个关闭的开关,如果用户能记得按它还好。如果用户忘了按它(事实上我相信大部分时候用户都会忘记,让我来用,我觉得我也很可能忘记),你就得想其他办法了。

当然,你可以在用户尝试退出页面的时候弹一个提示框,提示用户是否确定要离开该页面?如果用户选择是,网页就知道这个程序该停了,可是问题是,此时一切的变化都还只发生在前端,如何让后端知道这个变化已经发生呢,从另一方面来说,后端在程序运行输出结果之后,又如何告知前端呢?

由于我对Socket的执念,我几乎第一时间就决定,要用Socket来做这个功能。经过不懈的尝试,我终于成功用Socket完成了这个功能,现在就跟大家分享一下完成的过程。

功能规划

其实对于一个单一页面的web应用来说,用户的使用逻辑还算是比较好预测的了。开始程序的方法,自然是只有按下”开始“按钮一种。而当用户不再需要这个程序继续运行的时候,一共也就只有三种行为:一是按下我们提供的”结束搜索“按钮(不太可能),二是通过各种手段(包括不限于点击网页标签页的关闭键、关闭浏览器、直接关机等)关闭网页,三是直接去到其他页面。后两种本质是一样的。而且都可以用弹窗提示是否离开来确定发送停止信号的时机。但如果用户通过比较强行的手段关闭浏览器(忽略了提示),则仍然可能会导致前端没机会向后端发出停止信号。而考虑到只要解决了网页在被强行关闭的时候也能让后端程序停止,就自然能让网页在正常关闭时的后端能够停止,因此其实可以视为只有两种情况。

1:用户点击停止按钮

2:用户强行关闭了网页,网页没来得及送出停止信号

架构计划:

一个首要的问题是:既然决定了要用Socket来解决,那么必然会有一个服务器端和一个或数个客户端。那么,后端的工作程序到底是用作客户端还是服务器端呢?我的第一反应是用作服务端。但是很快就能想到,对简单的Socket应用来说,每次服务器端在等待来自客户端的消息时,进程都会处于挂起状态。在这种设定下,客户端,也就是前端不可能以非常高的频率向服务端发送信息,那样既浪费计算资源又会对网络造成很大的压力。因此后端程序对来自前端的信号的等待必然会造成程序运行的延迟,由于整个程序运行过程很长。所以即使这个延迟很短,从整个程序的运行过程来看也会造成大量的时间浪费。所以,服务器端只能用一个可以负担的起等待代价的进程来担任。

在决定了这点之后,我决定建立一个额外的线程来担任服务器端的角色。这里可能要稍微解释一下Django后端的运行原理:在一个完成度较高的页面中,页面的最终样式应该是由模板和view.py中你位这个页面写的函数共同决定的,而模板中可以使用Django规定的语法在HTML代码间的各处预留变量的位置,在view.py运算出需要的结果后,再用结果的变量对预留的位置逐一替换。我想过使用Ajax来传送请求给后端。可Django中并没有原生集成对Ajax的支持。因此最后我实际上是用了一种使用JS代码定时提交POST请求的方法,曲线救国的完成了页面的定时刷新。诚然这样的方式需要刷新整个页面,会比Ajax消耗更多资源,但好在这些消耗都是发生在用户的计算机上的,这种实现方式对于这种较小规模的并发请求已经足够了。

那既然是定时刷新,每次POST也就意味着view.py中相对应的函数会被运行一次,因此view.py中的函数也不太适合做服务器端,但是如果有一个独立于view.py和后端服务程序之外的服务器端函数,那么,view.py和后端服务程序都可以作为客户端。定时向这个函数报告情况,当这个函数发现从客户端报告上来的状态发生了变化,就可以做出相应的调整了。

最后,程序的结构用图片可以表示成这样。

代码实现:

1:view.py:

view.py最先启动,当Django第一次收到用户提交的POST请求时,首先决定好本进程要使用的地址和端口,(地址决定都用localhost,如何防止多个用户被分配到同一个端口就是另外一个话题了,如何判断是否是第一次提交POST请求也是)使用threading里的Thread单独建立一个线程,也就是我们的Server,建立一个Socket实例sock,并传给Server。

from threading import Thread def methodinview(): # ...其他功能... sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.bind(('127.0.0.1', port)) t = Thread(target=server, args=(sock,)) # sock作为参数传入 t.start()

view.py需要做的还不止这些,如果不是第一次提交POST请求,也就意味着如果不出意外,Server已经建立好并在等待信号了(至少是曾经建立过),因此这时既然view.py还在正常运行,也就是说用户还停留在网页上并且没有点击停止按钮。既然在网页被关闭时可能并没有机会向Server报告这一变化,不如就反过来,在每次正常运行时告诉Sever自己仍在正常运行。这种信号一般都被称之为心跳(heartbeat)信号。 import socket sockhb = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sockhb.connect(('127.0.0.1', port)) sockhb.send(b'keepalive')如果收到了用户的停止指令,则view.py还需要向Server报告程序需要停止。

if stopbutton: # stopbutton代表了用户发送的停止指令 requeststop = socket.socket(socket.AF_INET, socket.SOCK_STREAM) requeststop.connect(('127.0.0.1', port)) requeststop.send(b'timetostop')

2:后端程序:

后端程序会不停的向Server询问是否可以停止,由于Server在常规状态下处理完每个请求并做出回复后都会马上待命等待处理下一个请求,因此这种询问几乎不会造成延时。而如果后端程序自己已经完成,则会向Server发送一条告知自己已经完成的信息,由Server做出处理。由于要多次调用,我把向Server发信的语句写成了一个函数。

import socket @staticmethod def sendmessage(port, message): sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.connect(('127.0.0.1', port)) time.sleep(0.1) sock.send(message) msgrecieved = sock.recv(1024) print('Email Parser recieved Messeage back from view:', msgrecieved) if msgrecieved == b'yes' or msgrecieved == b'gotit': sock.close() return True elif msgrecieved == b'no': return False else: return False

如果多次发送失败,则认为程序已经可以停止。

while True: # ...功能代码... print('Send ifstop Message') try: answer = self.sendmessage(port, b'ifstop') except: ifstopfailnum += 1 print('ifstopfailnum:', ifstopfailnum) if ifstopfailnum > 7: return time.sleep(3) continue if answer is True: return #...功能代码... 3:Server:

Server要考虑的事情就多得多。它需要接收从两个客户端传来的信号,并分别作出回应。在前面已经有提到guo:后端程序会不断询问Server是否需要结束。而view.py在运行时则会定时向Server报告自己正在运行。后端程序大概0.8秒左右会询问一次Server是否需要停止,而view.py则是6秒一次(这些数值都可以根据实际需要自由调整)。因此我设定了一个25次的阈值。也就是说,在正常情况下无论后端程序如何询问Server是否停止,Server都会回答"no",但是在3次本该收到view.py的心跳信号(18秒)却没有收到后。Server就会认为用户已经关闭了网页,此时,Server会直接关闭。而如果接到了view.py的"timetostop"信号,Server在之后则会回复后端"yes"。

def server(sock): sock.listen(5) notkeepalive = 0 # 设定没有收到心跳信号的计数器为0 while notkeepalive < 25: try: print('Waiting for signal') connection, address = sock.accept() recv = connection.recv(1024) print('signal recieved:', recv) if recv == b'ifstop': # 只要收到的不是心跳信号(keepalive信号),计数器就+1 notkeepalive += 1 print('Notkeepalive value:', notkeepalive) connection.send(b'no') elif recv == b'end': # 如果收到后端程序运行的结束信号,则回复收到 try: connection.send(b'gotit') sock.close() print('No pages anymore, Heartbeat server stopped') return except Exception as e: pass elif recv == b'keepalive': notkeepalive = 0 elif recv == b'timetostop': # 收到用户发送的停止指令,接下来再收到后端程序的询问就回复"yes" print('Waiting for stop signal') connection, address = sock.accept() recv = connection.recv(1024) if recv == b'ifstop': connection.send(b'yes') sock.close() print('Time to stop, Heartbeat server stopped') return elif recv == b'noanymore': connection.send(b'gotit') sock.close() print('No pages anymore, Heartbeat server stopped') return else: notkeepalive += 1 print('Notkeepalive value:', notkeepalive) connection.send(b'no') except: print('heartbeat error:\n', traceback.print_exc()) time.sleep(10) sock.close() # 如果长时间未收到心跳信号,则停止Server,后端程序在连续多次发送心跳信号失败后,也会自行停止。 print('No keepalive signal, stop heartbeat service') return在经过无数次微调和尝试之后,这个由一个服务器端和两个客户端组成的Socket小网络终于正常运作了起来。再加上后端程序本身的性能优化,这个程序可以说是终于初步的做好了应对一定数量的并发请求的准备。另外,也算是了了我一桩心愿。

转载请注明原文地址: https://www.6miu.com/read-1250380.html

最新回复(0)