Python 实现多线程下载器

前言

我为什么会想到要写一个下载器呢,实在是被百度云给逼的没招了,之前用 Axel 配合直链在百度云下载视频能达到满速,结果最近两天 Axel 忽然不能用了,于是我就想着要不干脆自己写一个吧,就开始四处查询资料,这就有了这篇博客。

我假设阅读这篇博客的你已经对以下知识有所了解:

  • Python 的文件操作
  • Python 的多线程
  • Python 的线程池
  • Python 的 requests 库
  • HTTP 报文的首部信息

下载

获取文件采用的是 requests 库,该已经封装好了许多 http 请求,我们只需要发送 get 请求,然后将请求的内容写入文件即可:

import requests

r = requests.get('http://files.smashingmagazine.com/wallpapers/july-17/summer-cannonball/cal/july-17-summer-cannonball-cal-1920x1080.png')
with open('wallpaper.png', 'wb') as f:
    f.write(r.content)

随后看看文件夹,那张名为 wallpaper.png 的图片就是我们刚刚下载的。

但是这个功能太简单了,甚至简陋,我们需要多线程并发执行下载各自的部分,然后再汇总。

拆分

为了拆分,首先得知道数据块的大小,HTTP 报文首部提供了这样的信息:

  • 用 head 方法去获取 http 首部信息,再从获取的信息提取出 Content-Length 字段(上文图片大小为 261258 bytes)
import requests

headers = {'Range': 'bytes={}-{}'.format(0, 100000)}
r = requests.get('http://files.smashingmagazine.com/wallpapers/july-17/summer-cannonball/cal/july-17-summer-cannonball-cal-1920x1080.png', headers = headers)
with open('wallpaper.png', 'wb') as f:
    f.write(r.content)

我们得到了图片的前 100001 个字节(Range 的范围是包括起始和终止的),打开 wallpaper.png 你应该能看到一幅“半残”的图。

这样我们里目标更近了一步,继续:

  • 确认线程数(比如 8 个),261258//8 = 32657,前 7 个线程都取 32657 个 bytes,第八个取剩余的
part = size // nums

for i in range(nums):
        start = part * i
        if i == num_thread - 1:   # 最后一块
            end = file_size
        else:
            end = start + part
  • 每个线程获取到的内容按顺序写入文件(file.seek() 调节文件指针)
def down(start, end):
    headers = {'Range': 'bytes={}-{}'.format(start, end)}
    # 这里最好加上 stream=True,避免下载大文件出现问题
    r = requests.get(self.url, headers=headers, stream=True)
    with open(filename, "wb+") as fp:
        fp.seek(start)
        fp.write(r.content)

嘛,线程多了起来就扔到线程池让它来帮我们调度。

封装

功能复杂了,用对象来封装整理一下:

class Downloader(): 
    def __init__(self, url, num, name):
        self.url = url
        self.num = num
        self.name = name
        r = requests.head(self.url)
        self.size = int(r.headers['Content-Length']) 

    def down(self, start, end):

        headers = {'Range': 'bytes={}-{}'.format(start, end)}
        r = requests.get(self.url, headers=headers, stream=True)

        # 写入文件对应位置
        with open(self.name, "rb+") as f:
            f.seek(start)
            f.write(r.content)


    def run(self):
        f = open(self.name, "wb")
        f.truncate(self.size)
        f.close()

        futures = []
        part = self.size // self.num 
        pool = ThreadPoolExecutor(max_workers = self.num)                                    
        for i in range(self.num):
            start = part * i
            if i == self.num - 1:   
                end = self.size
            else:
                end = start + part - 1
            # 扔进线程池
            futures.append(pool.submit(self.down, start, end))
        wait(futures)

至此,核心功能都完成了,剩下的就是实际体验的优化了。

完整的代码已托管至 GitHub,地址见这里: https://github.com/WincerChan/DAM

结语

很可惜,我写的这个下载器还是不能下载百度云直链,不过嘛,好多人都说结果不重要,都说重要的是过程,不是么?写这个下载器我也确实学到了许多,至于一开始我是出于什么样的目的?管他呢

Python并行计算简单实现

multiprocessing包是Python中的多进程管理包.
Pool(num)类提供一个进程池,然后在多个核中执行这些进程,
其中默认参数num是当前机器CPU的核数.

Pool.map(func, iterable[, chunksize=None])
2个参数, 第一个参数是函数, 第二个参数是需要可迭代的变量, 作为参数传递到func

如果func含有的参数多于一个,可以利用functools.partial 先处理.
以下是一个简单的例子.

from multiprocessing import Pool
from functools import partial 

def somefunc(str_1, str_2, iterable_iterm):
    print("%s %s %d" % (str_1, str_2, iterable_iterm))

def main():
    iterable = [1, 2, 3, 4, 5]
    pool = Pool()
    str_1 = "This"
    str_2 = "is"
    func = partial(somefunc, str_1, str_2)
    pool.map(func, iterable)
    pool.close()
    pool.join()

if __name__ == "__main__":
    main()

php-fpm优化方法 pm.min_spare_servers、pm.max_spare_servers

php-fpm进程池开启进程有两种方式,一种是static,直接开启指定数量的php-fpm进程,不再增加或者减少;
另一种则是dynamic,开始时开启一定数量的php-fpm进程,当请求量变大时,动态的增加php-fpm进程数到上限,

当空闲时自动释放空闲的进程数到一个下限。这两种不同的执行方式,可以根据服务器的实际需求来进行调整。

要用到的一些参数,分别是pm、pm.max_children、pm.start_servers、pm.min_spare_servers和pm.max_spare_servers。

pm表示使用那种方式,有两个值可以选择,就是static(静态)或者dynamic(动态)。

下面4个参数的意思分别为:

  • pm.max_children:静态方式下开启的php-fpm进程数量,在动态方式下他限定php-fpm的最大进程数(这里要注意pm.max_spare_servers的值只能小于等于pm.max_children)
  • pm.start_servers:动态方式下的起始php-fpm进程数量。
  • pm.min_spare_servers:动态方式空闲状态下的最小php-fpm进程数量。
  • pm.max_spare_servers:动态方式空闲状态下的最大php-fpm进程数量。如果dm设置为static,那么其实只有pm.max_children这个参数生效。系统会开启参数设置数量的php-fpm进程。php-fpm一个进程大概会占20m-40m的内存,所以他的数字大小的设置要根据你的物理内存的大小来设置,还要注意到其他的内存占用,如数据库,系统进程等,来确定以上4个参数的设定值!

如果dm设置为dynamic,4个参数都生效。系统会在php-fpm运行开始时启动pm.start_servers个php-fpm进程,

然后根据系统的需求动态在pm.min_spare_servers和pm.max_spare_servers之间调整php-fpm进程数。

参数要求pm.start_servers的值在pm.min_spare_servers和pm.max_spare_servers之间。

例:主机内存为1.6G,默认lnmp设置为20,10,10,20;改为如下25,5,5,25

因为网站访客人数很少,所以初始php-fpm线程5个就好了,不会占用过多内存,当访客突然增加,也会动态调整直到最高的限值25.

[www]
listen = /tmp/php-cgi.sock
listen.backlog = -1
listen.allowed_clients = 127.0.0.1
listen.owner = www
listen.group = www
listen.mode = 0666
user = www
group = www
pm = dynamic
pm.max_children = 25
pm.start_servers = 5
pm.min_spare_servers = 5
pm.max_spare_servers = 25
request_terminate_timeout = 100
request_slowlog_timeout = 0
slowlog = var/log/slow.log

php-fpm占用内存过高分析及解决

昨天刚买了个vps搭建了一个wordPress博客,刚搭建的时候有一大堆的问题。好不容易搭建完成了发现运行一阵子以后内存几乎用完了。开始以为是服务器的问题,费了好大的劲把Apache服务器换成了Nginx。但是问题还是没有解决,最后用top命令看了一下是php-fpm的问题。问题的解决办法如下:

未分类

1、查看php-fpm的进程个数

ps -fe |grep "php-fpm"|grep "pool"|wc -l

2、查看每个php-fpm占用的内存大小

ps -ylC php-fpm --sort:rss

3、配置php-fpm参数

找到php-fpm的配置文件php-fpm.conf

pm = dynamic #对于专用服务器,pm可以设置为static。#如何控制子进程,选项有static和dynamic。如果选择static,则由pm.max_children指定固定的子进程数。如果选择dynamic,则由下开参数决定:
pm.max_children #子进程最大数
pm.start_servers #启动时的进程数
pm.min_spare_servers #保证空闲进程数最小值,如果空闲进程小于此值,则创建新的子进程
pm.max_spare_servers #保证空闲进程数最大值,如果空闲进程大于此值,此进行清理

对于内存大的服务器(比如8G以上)来说,指定静态的max_children实际上更为妥当,因为这样不需要进行额外的进程数目控制,会提高效率。

对于内存小的服务器,使用动态方式。具体最大数量根据 内存/20M 得到。比如512M的VPS,建议pm.max_spare_servers设置为20。至于pm.min_spare_servers,则建议根据服务器的负载情况来设置,比较合适的值在5~10之间。

解决Nginx环境WordPress或Typecho设置固定链接无法打开的问题

做网站搭博客,首选都是自己买个国外VPS,400+一年费用一般比国内的虚拟空间稍贵点,但相比买虚拟空间VPS好太多:

  • 私有独享ip
  • 足够多的存储空间:基本上都是10G起,要放什么文件都可以,数据库也可以随意多个
  • 足够多的流量:一般都是500G起,相比虚拟主机的10G优越不是一点两点,几个朋友一起用都不是什么问题
  • 最重要的是,境外VPS还有其他的用途:比如翻墙梯

当然,国外VPS也有个比不了的,那就是速度没有国内的虚拟主机快,但不需要备案啊!!!而且,就一个小网页,速度差别几乎感受不到。

错误现象

自己的VPS就需要自己维护了,就是一台远程的电脑,你自己能折腾出花来都行。自己不会折腾找卖家装好环境自己用就行。
一般搭网站用LNMP环境,对于静态网站,nginx默认设置就行,但对于WordPress或者Typecho这种动态站点,默认会出现设置带目录的链接时无法打开的情况。

http://xxx.com/blog/
http://xxx.com/2018/
http://xxx.com/%postname%.html

不加设置以上形式的链接都会出现404错误。

解决办法

不支持目录链接是因为缺少伪静态规则,我们只需要按以下方法添加伪静态即可。

添加伪静态规则

Nginx环境下WordPress或者Typecho伪静态规则如下:

location / { 
  index index.html index.php; 
  if (-f $request_filename/index.html) { 
    rewrite (.*) $1/index.html break; 
  } 
  if (-f $request_filename/index.php) { 
    rewrite (.*) $1/index.php; 
  } 
  if (!-f $request_filename) { 
    rewrite (.*) /index.php; 
  } 
}

以上规则在nginx或者vhost配置中修改都可以。保存配置后,重启nginx一般就正常了。

开启PATHINFO

如果Typecho访问内页出现No input file specified错误提示,那么是没有开启pathinfo的支持。

方法:

找到/usr/local/php/etc/php.ini文件,将cgi.fix_pathinfo=0 改成 cgi.fix_pathinfo=1,保存后输入命令:service php-fpm重启php-fpm 即可。

nginx 安装 SSL 证书

昨天在腾讯云那里申请了SSL证书,打算部署到服务器上,但是安装过程遇到了问题:

[root@izuf6hed2mdyv56471078kz sbin]# ./nginx -s reload
nginx: [emerg] unknown directive "ssl" in
/usr/local/nginx/conf/nginx.conf:91

于是查看了下nginx的安装信息,发现ssl模块并未被安装:

[root@izuf6hed2mdyv56471078kz sbin]# /usr/local/nginx/sbin/nginx -V
nginx version: nginx/1.11.6
built by gcc 4.8.5 20150623 (Red Hat 4.8.5-11) (GCC)
configure arguments:

原因大概是:在安装nginx的时候,没有将ssl模块编译进nginx

解决办法是:重装nginx,并在执行configure命令的时候加得上 –with-http_ssl_module 参数。

由于新的nginx版本已经出来了,就借此机会顺便升级一下吧。

步骤一、下载nginx解压

wget http://nginx.org/download/nginx-1.13.8.tar.gz

# 默认下载到root
cd /root

# 解压
tar xzvf nginx-1.13.8.tar.gz

# 进入解压后的目录
cd nginx-1.13.8

步骤二、安装nginx,并添加ssl模块

./configure --with-http_ssl_module

好吧,又报错了:

./configure: error: SSL modules require the OpenSSL library.
You can either do not enable the modules, or install the OpenSSL library
into the system, or build the OpenSSL library statically from the source
with nginx by using --with-openssl=<path> option.

提示我装openssl,好吧,我装。

步骤一点五、安装OpenSSL

这是centos 的安装方法:

yum -y install openssl openssl-devel

好,继续执行步骤二。

步骤二(续)、安装nginx,并添加ssl模块

cd /root/nginx-1.13.8
 ./configure --with-http_ssl_module

请先备份好nginx相关的文件!

请先备份好nginx相关的文件!

请先备份好nginx相关的文件!

然后是安装:

# 网上说这么做会覆盖之前的版本,我测试了下,似乎没有什么大问题...
# nginx.conf也没有变化,若心存疑虑,可以参考其他教程
make install

# 查看版本是否升级
/usr/local/nginx/sbin/nginx -V

步骤三、配置

编辑/usr/local/nginx/conf/nginx.conf 添加配置:

server {
        listen 443 ssl;
        server_name www.zxxblog.cn;
        ssl on;
        ssl_certificate /home/key/1_www.zxxblog.cn_bundle.crt;
        ssl_certificate_key /home/key/2_www.zxxblog.cn.key;
        ssl_session_timeout  5m;
        ssl_protocols TLSv1 TLSv1.1 TLSv1.2; 
        ssl_ciphers  HIGH:!aNULL:!MD5;
        #ssl_ciphers  ALL:!ADH:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv2:+EXP;   
        ssl_prefer_server_ciphers   on;   

        # !! 这下面的是我自己的配置,不同的人可能有所不同 !!
        location / {
            proxy_pass http://localhost:8080;
        }
}

步骤四、重启nginx

# 如果不成功,就kill掉nginx,然后再打开
/usr/local/nginx/sbin/nginx -s reload

然后,我在阿里云的安全组里添加了443端口,搞定!

效果:

未分类

nginx+php-fpm出现502 bad gateway错误解决方法

1. php-fpm进程数不够用

使用 netstat -napo |grep “php-fpm” | wc -l 查看一下当前fastcgi进程个数,如果个数接近conf里配置的上限,就需要调高进程数。

但也不能无休止调高,可以根据服务器内存情况,可以把php-fpm子进程数调到100或以上,在4G内存的服务器上200就可以。

2. 调高调高linux内核打开文件数量

可以使用这些命令(必须是root帐号)

echo ‘ulimit -HSn 65536’ >> /etc/profile

echo ‘ulimit -HSn 65536’ >> /etc/rc.local

source /etc/profile

3. 脚本执行时间超时

如果脚本因为某种原因长时间等待不返回 ,导致新来的请求不能得到处理,可以适当调小如下配置。

nginx.conf里面主要是如下

fastcgi_connect_timeout 300;
fastcgi_send_timeout 300;
fastcgi_read_timeout 300;

php-fpm.conf里如要是如下

request_terminate_timeout = 10s

4. 缓存设置比较小

修改或增加配置到nginx.conf

proxy_buffer_size 64k;
proxy_buffers  512k;
proxy_busy_buffers_size 128k;

5. recv() failed (104: Connection reset by peer) while reading response header from upstream

可能的原因机房网络丢包或者机房有硬件防火墙禁止访问该域名

但最重要的是程序里要设置好超时,不要使用php-fpm的request_terminate_timeout,

最好设成request_terminate_timeout=0;

因为这个参数会直接杀掉php进程,然后重启php进程,这样前端nginx就会返回104: Connection reset by peer。这个过程是很慢,总体感觉就是网站很卡。

Nginx配置图片防盗链

为了防止其他站点直接从我们网站引用图片等链接,消耗了我们服务器资源和网络流量,我们一般会对图片等资源做一些限制,比如打水印,防盗链设置等,本文主要结合Nginx来讲解如何设置图片防盗链。

我们所说的防盗链功能是都是基于 HTTP 协议支持的 Referer 机制,通过 referer 跟踪来源,对来源进行识别和判断。 利用这个策略,我们基本可以防止其他站点直接链接我们站上的图片。 举个例子,如果a.com网站的页面调用了我站的图片:https://www.helloweba.net/p.jpg,我们通过Nginx来判断它的来源域,不属于www.helloweba.net过来的图片都返回403,即禁止访问。

打开对应站点的conf配置文件,有关Nginx站点配置文件可以参考: https://www.helloweba.net/server/504.html ,主要配置代码如下:

location ~*.(gif|jpg|jpeg|png|bmp|swf)$ { 
    valid_referers none blocked www.helloweba.net m.helloweba.net; 
    if ($invalid_referer) { 
        return 403; 
        #rewrite ^/ http://www.baidu.com/error.jpg; 
    } 
} 

以上代码解释如下:

1、location中指定要防篡改的文件类型,多个后缀用“|”符号分开。

2、valid_referers指定资源访问是通过以下几种方式为合法,即白名单,允许文件链出的域名白名单。

none:直接通过url访问,无referer值的情况

blocked:referer值被防火墙修改

servername:指定资源在合法的域名白名单中可以被引用,支持*通配符,多个域名使用空格符分开

3、if判断如果用户请求的资源不符合上述配置,那么rewrite重定向到你想指定的url上,也可以配置403权限错误。

以上设置差不多就可以起到防盗链作用了,但是,这样并不是彻底地实现真正意义上的防盗链!

我们应该注意设置:

location ~ .*.(gif|jpg|jpeg|png|bmp|swf)$
        {
            expires      30d;
            valid_referers m.helloweba.net www.helloweba.net;
            if ($invalid_referer) {
                #rewrite ^/ http://www.baidu.com/a.html; 
                return 403;
            }
        }

expires 30d;属于配置文件中location作用域中原有的图片缓存时间配置,这里我们把两个location合并在一起。

接着,我们去掉none blocked两个关键词,目的是直接在浏览器地址栏中输入对应的图片地址也会被拒绝访问。

如果匹配到不属于设定的referer来源域,则返回403,或者重置到一个url地址上去,这样可以避免右键另存为的方式下载图片。

当然,话又说回来,如果人家真想获得你的图片还是有办法的,比如各种伪造referer来源等方法。

还有一种情况,如果我们站点使用CDN,那么在nginx上的防盗链配置似乎不起作用了,别担心,找CDN厂商,他们有一整套资源防盗链方法,大多在CDN管理平台直接设置即可,比如阿里云CDN,其原理也是判断referer。

使用OpenSSL给Nginx添加访问密码

1、创建用户名为sam

sh -c “echo -n ‘sam:’ >> /etc/nginx/.htpasswd”

2、创建用户密码

sh -c “openssl passwd -apr1 >> /etc/nginx/.htpasswd”

3、添加到nginx配置

location / {

try_files $uri $uri/ =404;

#下列两行为新增
**auth_basic “Restricted Content”;

auth_basic_user_file /etc/nginx/.htpasswd;**

}

nginx使用secure_link模块防盗链

secure_link模块介绍

ngx_http_secure_link_module是nginx内置的一个防盗链模块
使用这个模块,可以有效的防止文件被其他网站盗用.
有效防止服务器流量流失.

secure_link模块如何启用

编译时加入以下参数(0.7.18后版本可用)

--with-http_secure_link_module

nginx设置secure_link

location  / {
    secure_link $arg_md5,$arg_expires;  #设置两个变量 
    secure_link_md5 "key$remote_addr$arg_expires";   #设置md5,当作口令 

    if ($secure_link = "") {
        return 403;
    }

    if ($secure_link = "0") {
        return 410;
    }
}

网站生成口令设置[php为例]

$secret = 'key';   // key,自定义秘钥 
$ipip=$_SERVER["REMOTE_ADDR"]; 
$expires = time()+300; //这里是300妙内访问有效 

$md5 = base64_encode(md5($secret . $ipip . $expires, true)); // MD5生成 
$md5 = strtr($md5, '+/', '-_'); // + and / 替换掉 
$md5 = str_replace('=', '', $md5); // 替换= 

$url = "http://domain.com/test.zip?md5=$md5&expires=$expires";  //安全下载链接demo设置 

$arr = array("url"=>$url, "expires"=>date("Y-m-d H:i:s", $expires), "md5"=>$md5);

echo json_encode($arr); //json格式输出