Python3 http服务器脚本,支持range请求头部(因此可以用它来在线看mp4视频)

# -*- coding: gbk

import http.server
import time
import socketserver
import os
import threading
import socket

#下面的导入从SimpleHTTPServer.py复制:
import posixpath
import urllib.parse
import cgi
import sys
import shutil
import mimetypes
import io
import re

PORT = 9000

class MyThreadingHTTPServer(socketserver.ThreadingTCPServer):

    allow_reuse_address = 1

    def server_bind(self):
        """Override server_bind to store the server name."""
        socketserver.TCPServer.server_bind(self)
        host, port = self.socket.getsockname()[:2]
        self.server_name = socket.getfqdn(host)
        self.server_port = port

#Handler = SimpleHTTPServer.SimpleHTTPRequestHandler
class MyHTTPRequestHandler(http.server.SimpleHTTPRequestHandler):
    def dumpRequestHeaders(self):
        pass #print 'dumpRequestHeaders: raw_requestline=%s \nheaders=\n%s' % (self.raw_requestline,self.headers)
    
    def copyfile_by_range(self, fin, fout, start, end):
        print( "copyfile_by_range: start=%d end=%d" % (start, end))
        READ_BUFFER_SIZE = 4*1024;
        fin.seek(start, os.SEEK_SET)
        if end<0: #代表原始Range请求未指定完整范围,只指定了开始位置
            buf = fin.read(READ_BUFFER_SIZE) #FIXME:健壮性fix,如果读到内容小于size参数?需要判断len(buf)
            if len(buf)!=READ_BUFFER_SIZE:
                pass #print "copyfile_by_range: len(buf)!=READ_BUFFER_SIZE 1 len(buf)=%d" % (len(buf))
            while buf:
                fout.write(buf)
                fout.flush()
                buf = fin.read(READ_BUFFER_SIZE)
                if len(buf)==0:
                    break #??
                if len(buf)!=READ_BUFFER_SIZE:
                    pass #print "copyfile_by_range: len(buf)!=READ_BUFFER_SIZE 2 len(buf)=%d" % (len(buf))
                    fout.write(buf)
                    break
        else:
            bytes_left = end-start+1
            while bytes_left >= READ_BUFFER_SIZE:
                buf = fin.read(READ_BUFFER_SIZE)
                if len(buf)!=READ_BUFFER_SIZE:
                    pass #print "copyfile_by_range: len(buf)!=READ_BUFFER_SIZE 3 len(buf)=%d" % (len(buf))
                fout.write(buf)
                bytes_left = bytes_left - READ_BUFFER_SIZE
            if bytes_left>0:
                buf = fin.read(bytes_left)
                if len(buf)!=bytes_left:
                    pass #print "copyfile_by_range: len(buf)!=bytes_left len(buf)=%d bytes_left=" % (len(buf), bytes_left)
                fout.write(buf)            
    
    def do_GET(self):
        self.dumpRequestHeaders() #用于查看客户端浏览器的User-Agent设置;
        #
        #SimpleHTTPServer.SimpleHTTPRequestHandler.do_GET(self)
        f, range = self.send_head() #原来的send_head这个函数实现有点莫名其妙?
        if f:
            if range:
                #注意,响应头部已经在send_head()里设置完成了,这里只需要调整io读写指针
                self.copyfile_by_range(f, self.wfile, range[0], range[1])
            else:
                self.copyfile(f, self.wfile)
                f.close()

    #重载SimpleHTTPServer.py里的实现,以实现:(1)按修改日期排序(2)正确显示中文
    #TODO:支持更多查询参数?html输出代码美化?
    def list_directory(self, path):
        """Helper to produce a directory listing (absent index.html).

        Return value is either a file object, or None (indicating an
        error).  In either case, the headers are sent, making the
        interface the same as for send_head().

        """
        try:
            list = os.listdir(path)
        except os.error:
            self.send_error(404, "No permission to list directory")
            return None
        #list.sort(key=lambda a: a.lower())
        '''
        def compare_by_modtime(x, y):
            stat_x = os.stat(path + "/" + x)
            stat_y = os.stat(path + "/" + y)
            if stat_x.st_mtime < stat_y.st_mtime:
                return -1
            elif stat_x.st_mtime > stat_y.st_mtime:
                return 1
            else:
                return 0
        list.sort(lambda x,y: compare_by_modtime(y,x)) #最近修改的排在前面
        '''
        def key_by_mtime(a):
            try:
                return - os.stat(path + "/" + a).st_mtime
            except Exception as e:
                return 0 #??
        list.sort(key=key_by_mtime)

        f = []
        enc = "gb18030";
        displaypath = cgi.escape(urllib.parse.unquote(self.path))
        f.append('<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" '
                 '"http://www.w3.org/TR/html4/strict.dtd">')
        f.append('<html>\n<head>')
        f.append('<meta http-equiv="Content-Type" '
                 'content="text/html; charset=%s">' % enc)
        f.append("<title>Directory listing for %s</title>\n" % displaypath)
        f.append("<body>\n<h2>Directory listing for %s</h2>\n" % displaypath)
        f.append("<hr>\n<ul>\n")
        for name in list:
            fullname = os.path.join(path, name)
            displayname = linkname = name
            is_dir = False
            # Append / for directories or @ for symbolic links
            if os.path.isdir(fullname):
                displayname = name + "/"
                linkname = name + "/"
                is_dir = True
            if os.path.islink(fullname):
                displayname = name + "@"
                # Note: a link to a directory displays with @ and links with /
            f.append('<li><a href="%s">%s</a>\n'
                    % (urllib.parse.quote(linkname), cgi.escape(displayname)))
                    #self.path是浏览器请求路径,而path是本地文件系统路径
            if not is_dir and name.endswith('.mp4'):
                f.append('|<a href="/playvideo?path=%s">播放</a>\n'
                    % (urllib.parse.quote( os.path.join(self.path, name))))
            f.append('</li>')
        f.append("</ul>\n<hr>\n</body>\n</html>\n")
        encoded = '\n'.join(f).encode(enc, 'surrogateescape')
        b = io.BytesIO()
        b.write(encoded)
        b.seek(0)
        self.send_response(200)
        encoding = "gbk" #sys.getfilesystemencoding()
        self.send_header("Content-Type", "text/html; charset=%s" % encoding)
        self.send_header("Content-Length", str(len(encoded)))
        self.end_headers()
        return b

    #TODO:支持Range请求,这样可以提供基于HTTP的视频流媒体服务
    def send_head(self):
        """
            overwrite send_head to set Last-Modified & Expires to disable browser cache;
        """
        enc = "gb18030";
        unquoted_path = urllib.parse.unquote(self.path)
        print( "send_head: self.path=%s unquoted_path=%s" % (self.path, unquoted_path))
        PLAYVIDEO_REQUEST = re.compile(r'/playvideo\?path=(.+)$')
        m = PLAYVIDEO_REQUEST.match(unquoted_path)
        if m: #TODO: 重构这里的代码     
            video_path = m.group(1)
            print( "send_head: video_path=%s" % video_path)
            self.send_response(200)
            self.send_header("Content-Type", "text/html")
            self.end_headers()
            self.wfile.write(('<video src="%s" controls></video>' % video_path).encode(enc, 'surrogateescape')) #注意,这个地方不需要urllib.quote
            return (None,None)
        path = self.translate_path(self.path)
        f = None
        if os.path.isdir(path):
            if not self.path.endswith('/'):
                # redirect browser - doing basically what apache does
                self.send_response(301)
                self.send_header("Location", self.path + "/")
                self.wfile.flush()
                time.sleep(1)
                self.end_headers()
                return (None,None)
            for index in "index.html", "index.htm":
                index = os.path.join(path, index)
                if os.path.exists(index):
                    path = index
                    break
            else:
                return (self.list_directory(path), None)
        ctype = self.guess_type(path)
        try:
            # Always read in binary mode. Opening files in text mode may cause
            # newline translations, making the actual size of the content
            # transmitted *less* than the content-length!
            f = open(path, 'rb')
            
            #Get file size:
            f.seek(0, os.SEEK_END)
            filesize = f.tell()
            f.seek(0, os.SEEK_SET)
            #TODO: 检查原始请求是否指定了Range头部
            if self.headers.get("Range"):            
                range_value = self.headers["Range"]
                print( "send_head: range_value=[%s]" % range_value)
                #直接使用正则表达式匹配: Range: bytes=100-
                HTTP_RANGE_HEADER = re.compile(r'bytes=([0-9]+)\-(([0-9]+)?)')
                m = re.match(HTTP_RANGE_HEADER, range_value)
                if m:
                    start_str = m.group(1)
                    start = int(start_str)
                    end_str = m.group(2)
                    end = -1
                    if len(end_str)>0:
                        end = int(end_str)
                    #现在可以写Range响应头部了:
                    self.send_response(206, "Partial Content")
                    self.send_header("Content-Type", ctype)
                    if end==-1:
                        self.send_header("Content-Length", str(filesize-start))
                    else:
                        self.send_header("Content-Length", str(end-start+1))
                    self.send_header("Accept-Ranges", "bytes")
                    if end<0:
                        content_range_header_value = "bytes %d-%d/%d" % (start, filesize-1, filesize)
                    else:
                        content_range_header_value = "bytes %d-%d/%d" % (start, end, filesize)
                    self.send_header("Content-Range", content_range_header_value)
                    print( "send_head: ok, serve 206 for Range request %s-%s,Content-Range=%s" % (start_str, end_str, content_range_header_value))
                    self.send_header("Connection", "close")
                    self.end_headers()
                    return (f, [start, end])
                else:
                    print( "send_head: error! INVALID Range request header!!")
                    self.send_error(400, "Bad Request")
                    self.wfile.flush()
                    self.end_headers()
                    return (None,None)            
        except IOError:
            self.send_error(404, "File not found")
            return (None,None)
        self.send_response(200)
        self.send_header("Content-Type", ctype)
        file_stat = os.fstat(f.fileno())
        self.send_header("Content-Length", str(file_stat[6]))
        #self.send_header("Last-Modified", self.date_time_string(file_stat.st_mtime))
        self.send_header("Last-Modified", self.date_time_string(time.time()))
        #self.send_header("Expires", self.date_time_string(time.time()+5))
        #self.send_header("Cache-control", "no-cache, no-store, must-revalidate, max-age=0, proxy-revalidate, no-transform")
        #self.send_header("Pragma", "no-cache")
        self.send_header("Connection", "close")
        self.end_headers()
        return (f, None)


s = MyThreadingHTTPServer(("", PORT), MyHTTPRequestHandler)
sa = s.socket.getsockname()
print("Serving MyThreadingHTTPServer on", sa[0], "port", sa[1], "...")
s.serve_forever()

展开阅读全文

没有更多推荐了,返回首页