【Socket编程】Python实现简易Web服务器

利用Python,我们可以搭建一个简单的Web服务器,这里我参考了:

https://ruslanspivak.com/lsbaws-part1/

https://ruslanspivak.com/lsbaws-part2/

https://ruslanspivak.com/lsbaws-part3/




PART 1


一个简单Python(2.7)代码如下:

import socket

HOST, PORT = "", 8888

listen_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)     #保证服务器的监听socket,即listen_socket在
									#关闭后对应的端口能够立即释放,否则将会在数分钟后才能释放
listen_socket.bind((HOST, PORT))
listen_socket.listen(1)
print "Serving HTTP on port %s ..." % PORT
while True:
    client_connection, client_address = listen_socket.accept()
    request = client_connection.recv(1024)
    print request

    http_response = """\
HTTP/1.1 200 OK

Hello, World!
"""
    client_connection.sendall(http_response)
    client_connection.close()


用浏览器登录 localhost:8888 可以打开一个打印着 "hello world" 的页面。而服务器则打印了如下信息:

GET / HTTP/1.1
Host: localhost:8888
Connection: keep-alive
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.8


GET /favicon.ico HTTP/1.1
Host: localhost:8888
Connection: keep-alive
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36
Accept: image/webp,image/apng,image/*,*/*;q=0.8
Referer: http://localhost:8888/
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.8

可以发现使用浏览器访问web服务器时,浏览器发送的消息如上。


使用telnet分析:

$ telnet localhost 8888
Trying ::1...
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
mick
HTTP/1.1 200 OK

Hello, World!
Connection closed by foreign host.

在连接成功后随便输入,例如 mick ,终端的反馈如上。


如果输入:

$ telnet localhost 8888
Trying ::1...
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
GET / HTTP/1.1
HTTP/1.1 200 OK

Hello, World!
Connection closed by foreign host.
则相当于模拟了一次浏览器请求。


我们的简易web服务器还缺少http消息的解析和处理功能。导致客户端不论发什么,都收到:

HTTP/1.1 200 OK

Hello, World!

的http响应。




PART 2


应用WSGI,方便我们更加自由选择web服务器与web框架。下面是一个简易的WSGI应用:

# Tested with Python 2.7.9, Linux & Mac OS X
import socket
import StringIO
import sys


class WSGIServer(object):

    address_family = socket.AF_INET
    socket_type = socket.SOCK_STREAM
    request_queue_size = 1

    def __init__(self, server_address):
        # Create a listening socket
        self.listen_socket = listen_socket = socket.socket(
            self.address_family,
            self.socket_type
        )
        # Allow to reuse the same address
        listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        # Bind
        listen_socket.bind(server_address)
        # Activate
        listen_socket.listen(self.request_queue_size)
        # Get server host name and port
        host, port = self.listen_socket.getsockname()[:2]
        self.server_name = socket.getfqdn(host)
        self.server_port = port
        # Return headers set by Web framework/Web application
        self.headers_set = []

    def set_app(self, application):
        self.application = application

    def serve_forever(self):
        listen_socket = self.listen_socket
        while True:
            # New client connection
            self.client_connection, client_address = listen_socket.accept()
            # Handle one request and close the client connection. Then
            # loop over to wait for another client connection
            self.handle_one_request()

    def handle_one_request(self):
        self.request_data = request_data = self.client_connection.recv(1024)
        # Print formatted request data a la 'curl -v'
        print(''.join(
            '< {line}\n'.format(line=line)
            for line in request_data.splitlines()
        ))

        self.parse_request(request_data)

        # Construct environment dictionary using request data
        env = self.get_environ()

        # It's time to call our application callable and get
        # back a result that will become HTTP response body
        result = self.application(env, self.start_response)

        # Construct a response and send it back to the client
        self.finish_response(result)

    def parse_request(self, text):
        request_line = text.splitlines()[0]
        request_line = request_line.rstrip('\r\n')
        # Break down the request line into components
        (self.request_method,  # GET
         self.path,            # /hello
         self.request_version  # HTTP/1.1
         ) = request_line.split()

    def get_environ(self):
        env = {}
        # The following code snippet does not follow PEP8 conventions
        # but it's formatted the way it is for demonstration purposes
        # to emphasize the required variables and their values
        #
        # Required WSGI variables
        env['wsgi.version']      = (1, 0)
        env['wsgi.url_scheme']   = 'http'
        env['wsgi.input']        = StringIO.StringIO(self.request_data)
        env['wsgi.errors']       = sys.stderr
        env['wsgi.multithread']  = False
        env['wsgi.multiprocess'] = False
        env['wsgi.run_once']     = False
        # Required CGI variables
        env['REQUEST_METHOD']    = self.request_method    # GET
        env['PATH_INFO']         = self.path              # /hello
        env['SERVER_NAME']       = self.server_name       # localhost
        env['SERVER_PORT']       = str(self.server_port)  # 8888
        return env

    def start_response(self, status, response_headers, exc_info=None):
        # Add necessary server headers
        server_headers = [
            ('Date', 'Tue, 31 Mar 2015 12:54:48 GMT'),
            ('Server', 'WSGIServer 0.2'),
        ]
        self.headers_set = [status, response_headers + server_headers]
        # To adhere to WSGI specification the start_response must return
        # a 'write' callable. We simplicity's sake we'll ignore that detail
        # for now.
        # return self.finish_response

    def finish_response(self, result):
        try:
            status, response_headers = self.headers_set
            response = 'HTTP/1.1 {status}\r\n'.format(status=status)
            for header in response_headers:
                response += '{0}: {1}\r\n'.format(*header)
            response += '\r\n'
            for data in result:
                response += data
            # Print formatted response data a la 'curl -v'
            print(''.join(
                '> {line}\n'.format(line=line)
                for line in response.splitlines()
            ))
            self.client_connection.sendall(response)
        finally:
            self.client_connection.close()


SERVER_ADDRESS = (HOST, PORT) = '', 8888


def make_server(server_address, application):
    server = WSGIServer(server_address)
    server.set_app(application)
    return server


if __name__ == '__main__':
    if len(sys.argv) < 2:
        sys.exit('Provide a WSGI application object as module:callable')
    app_path = sys.argv[1]
    module, application = app_path.split(':')
    module = __import__(module)
    application = getattr(module, application)
    httpd = make_server(SERVER_ADDRESS, application)
    print('WSGIServer: Serving HTTP on port {port} ...\n'.format(port=PORT))
    httpd.serve_forever()

相比之前的代码,长了不少,但这已经是一个最轻量的代码了,让你不用陷入细节。使用这段代码,你可以让你的web应用于你喜欢的框架中,例如Pyramid, Flask, Django或其他框架。


为了模拟这些框架,我们推荐使用virtualenv,按照下面的步骤安装并启动一个虚拟环境

1、首先下载并安装 virtualenv,尽量使用1.9及以上的版本(这里我使用了1.9)

2、

$ mkdir ~/envs
$ virtualenv ~/envs/lsbaws/
$ cd ~/envs/lsbaws/
$ ls
bin  include  lib
$ source bin/activate
(lsbaws) $ pip install pyramid
(lsbaws) $ pip install flask
(lsbaws) $ pip install django


3、使用下面的 pyramidapp.py 用作应用(以 pyramid为例)

from pyramid.config import Configurator
from pyramid.response import Response


def hello_world(request):
    return Response(
        'Hello world from Pyramid!\n',
        content_type='text/plain',
    )

config = Configurator()
config.add_route('hello', '/hello')
config.add_view(hello_world, route_name='hello')
app = config.make_wsgi_app()


4、 最好将你的服务器及应用代码都放在~/envs/lsbaws/,然后用 ./bin 下面的 python 来运行,终端下输入:

~/envs/lsbaws$ bin/python server.py pyramidapp:app
WSGIServer: Serving HTTP on port 8888 ...



5、打开浏览器,得到

Pyramid


注意坑:遇到一个坑,在执行了

~/envs/lsbaws$ bin/python server.py pyramidapp:app
后报错,说是:

Traceback (most recent call last):
  File "pyramidapp.py", line 1, in <module>
    from pyramid.config import Configurator
ImportError: No module named pyramid.config


在overstack上看到一个解答:https://stackoverflow.com/questions/17136716/import-configurator-error-for-python-pyramid

1) Make sure you are running the virtualenv
$ env/bin/python helloworld.py (即:使用virtualenv下的python)

2) Make sure you installed pyramid into the virtualenv
$ env/bin/python
>>> import pyramid.config (即:看环境是否配好,这里如果报错就说明环境还有问题,可以重搞一遍)

#ImportError or not?


其他如Flask, Django框架类似。WSGI是粘合Web服务器与Web框架的绝佳工具。




PART 3


还有一个问题,如何让我们的Web更好应对并发?这是一个关乎性能的重要问题。


Version 1.0

import socket

HOST, PORT = "", 8888

def handle_request(client_fd):
    request = client_fd.recv(2048)
    print request
    http_response = """\
HTTP/1.1 200 OK

Hello, World!
"""
    client_fd.sendall(http_response)

def serve_forever():
    listen_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

    listen_socket.bind((HOST, PORT))
    listen_socket.listen(5)
    print "Serving HTTP on port %s ..." % PORT

    while True:
        client_fd, client_addr = listen_socket.accept()
        handle_request(client_fd)
        client_fd.close()

if __name__ == '__main__':
    serve_forever()


这种方式下服务器在任意时间下只能处理一个连接,非常之差。



老早在学习C语言下的Socket编程时,我们可以通过fork()或pthread_create()来使用多进程多线程来并行。那么python下应该也是类似的。



Version 1.1

import socket
import os

HOST, PORT = "", 8888

def handle_request(client_fd):
    request = client_fd.recv(2048)
    print request
    http_response = """\
HTTP/1.1 200 OK

Hello, World!
"""
    client_fd.sendall(http_response)


def serve_forever():
    listen_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

    listen_socket.bind((HOST, PORT))
    listen_socket.listen(5)
    print "Serving HTTP on port %s ..." % PORT

    while True:
        client_fd, client_addr = listen_socket.accept()
        pid = os.fork()
        if pid == 0:
            listen_socket.close()
            handle_request(client_fd)
            client_fd.close()
            os._exit(0)
        else:
            client_fd.close()

if __name__ == '__main__':
    serve_forever()


果然,这里使用了fork()开启多进程。


关于为什么需要在父进程中关闭 client_fd ,主要是因为如果父进程不关闭 client_fd,那么该 fd 的引用永远不会减到0,该 tcp 连接就不会关闭,一来无故占用fd,当这样的连接过多时服务器的fd会被消耗完,二来该 tcp 连接无法关闭,即使客户端对应的 fd 关闭了,服务器端的 fd 没有关闭,客户端就永远等不到服务器发来的 fin 信息,导致该 tcp 连接一直存在。如果我们使用 curl 指令为例,就会发现在客户端的终端里,该连接关闭不了。


下面演示下当不关闭子进程中的fd:

$ python server.py 
Serving HTTP on port 8888 ...

使用netstat -nta 查看所有tcp连接得到:



然后发起连接

$ curl http://localhost:8888/
Hello, World!

得到:



连接没有关闭,强制关闭 curl,发现



参考四次挥手过程可知,现在缺的就是服务器向客户端发送 fin 信息,原因就是服务器这边的 fd 尚未关闭。




僵尸进程:

除了 fd 的问题,以上代码还会产生僵尸进程。僵尸进程是指子进程先于父进程退出,退出后会保留一些进程号,退出状态等子进程信息,本需要父进程回收,但父进程没有回收这些信息,导致了僵尸进程。




Z+就是僵尸进程。大量的僵尸进程同样会占用很多资源,如何处理?常用的方法:

1、kill掉父进程,这样这些僵尸进程会被 init 回收;

2、父进程调用 wait() 主动回收。


方法一太粗暴不可行,方法二将会阻塞父进程,同样不可取。还记得信号吗?让子进程在退出后发送信号给父进程,再由父进程回收,这是一种可行的方法。子进程在退出后会给父进程发送 SIGCHLD 信号,我们可以添加自己处理函数来回收僵尸进程。于是,我们的代码改为:


 Version 1.2

import socket
import signal
import os

HOST, PORT = "", 8888


def handle_exit(signum, frame):
    pid, status = os.wait()
    print "get SIGCHLD from: %u" % pid


def handle_request(client_fd):
    request = client_fd.recv(2048)
    print request
    http_response = """\
HTTP/1.1 200 OK

Hello, World!
"""
    client_fd.sendall(http_response)


def serve_forever():
    listen_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

    listen_socket.bind((HOST, PORT))
    listen_socket.listen(5)
    print "Serving HTTP on port %s ..." % PORT
    signal.signal(signal.SIGCHLD, handle_exit)

    while True:
        client_fd, client_addr = listen_socket.accept()
        pid = os.fork()
        if pid == 0:
            listen_socket.close()
            handle_request(client_fd)
            client_fd.close()
            os._exit(0)
        else:
            client_fd.close()

if __name__ == '__main__':
    serve_forever()


使用 curl 后居然报错:


到底做错了什么?



稍稍修改一下就能解决:

Version 1.3

import socket
import errno
import signal
import os

HOST, PORT = "", 8888


def handle_exit(signum, frame):
    pid, status = os.wait()
    print "get SIGCHLD from: %u" % pid


def handle_request(client_fd):
    request = client_fd.recv(2048)
    print request
    http_response = """\
HTTP/1.1 200 OK

Hello, World!
"""
    client_fd.sendall(http_response)


def serve_forever():
    listen_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

    listen_socket.bind((HOST, PORT))
    listen_socket.listen(5)
    print "Serving HTTP on port %s ..." % PORT
    signal.signal(signal.SIGCHLD, handle_exit)

    while True:
        try:
            client_fd, client_addr = listen_socket.accept()
        except IOError as e:
            code, msg = e.args
            # restart 'accept' if it was interrupted
            if code == errno.EINTR:
                continue
            else:
                raise

        pid = os.fork()
        if pid == 0:
            listen_socket.close()
            handle_request(client_fd)
            client_fd.close()
            os._exit(0)
        else:
            client_fd.close()

if __name__ == '__main__':
    serve_forever()


这回不会中断了,但之前错误的原因我还不清楚,知道的不吝赐教。


以上代码貌似是没有问题了,可是!添加如下客户端代码:

import argparse
import errno
import os
import socket


SERVER_ADDRESS = 'localhost', 8888
REQUEST = """\
GET /hello HTTP/1.1
Host: localhost:8888

"""


def main(max_clients, max_conns):
    socks = []
    for client_num in range(max_clients):
        pid = os.fork()
        if pid == 0:
            for connection_num in range(max_conns):
                sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
                sock.connect(SERVER_ADDRESS)
                sock.sendall(REQUEST)
                socks.append(sock)
                os._exit(0)


if __name__ == '__main__':
    parser = argparse.ArgumentParser(
        description='Test client for LSBAWS.',
        formatter_class=argparse.ArgumentDefaultsHelpFormatter,
    )
    parser.add_argument(
        '--max-conns',
        type=int,
        default=1024,
        help='Maximum number of connections per client.'
    )
    parser.add_argument(
        '--max-clients',
        type=int,
        default=1,
        help='Maximum number of clients.'
    )
    args = parser.parse_args()
    main(args.max_clients, args.max_conns)

这段客户端代码可以解析外部参数调整开启的TCP连接数。如:

python client.py --max-clients 128

开启了128个客户端。再次查看僵尸进程发现:


又有僵尸进程了,原因是在同一时间过多子进程发送信号,但是并没有很好的管理这些信号,导致有的信号被忽略了。


再次修改得到本文的最终版:

import socket
import errno
import signal
import os

HOST, PORT = "", 8888


def handle_exit(signum, frame):
    while True:
        try:
            pid, status = os.waitpid(
                    -1,         # Wait for any child process
                    os.WNOHANG  # Do not block and return EWOULDBLOCK error
                    )
        except OSError:
            return

        if pid == 0:    # no more zombies
            return

    print "get SIGCHLD from: %u" % pid


def handle_request(client_fd):
    request = client_fd.recv(2048)
    print request
    http_response = """\
HTTP/1.1 200 OK

Hello, World!
"""
    client_fd.sendall(http_response)


def serve_forever():
    listen_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

    listen_socket.bind((HOST, PORT))
    listen_socket.listen(5)
    print "Serving HTTP on port %s ..." % PORT
    signal.signal(signal.SIGCHLD, handle_exit)

    while True:
        try:
            client_fd, client_addr = listen_socket.accept()
        except IOError as e:
            code, msg = e.args
            # restart 'accept' if it was interrupted
            if code == errno.EINTR:
                continue
            else:
                raise

        pid = os.fork()
        if pid == 0:
            listen_socket.close()
            handle_request(client_fd)
            client_fd.close()
            os._exit(0)
        else:
            client_fd.close()

if __name__ == '__main__':
    serve_forever()


这回是真的没问题了。

  • 2
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
好的,下面是 Python Socket 实现 FTP 客户端和服务器的基本流程: FTP 服务器端: 1. 导入 socket 模块和 os 模块。 2. 创建一个 TCP Socket 对象,并绑定到一个 IP 地址和端口号上。 3. 调用 socket 对象的 listen() 方法,开始监听客户端连接请求。 4. 服务器通过无限循环不断接受客户端连接请求。当有客户端连接请求时,服务器通过 accept() 方法接受客户端的连接请求,并返回一个新的 socket 对象和客户端的地址。 5. 服务器收到客户端的请求后,根据请求的类型进行相应的处理,比如上传文件、下载文件、获取文件列表等。 6. 服务器处理完客户端的请求后,通过 send() 方法向客户端发送响应数据。 FTP 客户端: 1. 导入 socket 模块和 os 模块。 2. 创建一个 TCP Socket 对象,并连接到 FTP 服务器的 IP 地址和端口号上。 3. 客户端向服务器发送请求数据,比如上传文件、下载文件、获取文件列表等。 4. 客户端通过 recv() 方法接收服务器的响应数据。 5. 客户端处理服务器返回的响应数据,比如显示文件列表、保存下载的文件等。 下面是一个简单的 FTP 服务器和客户端的示例代码: FTP 服务器端代码: ``` import socket import os # 设置服务器IP地址和端口号 HOST = '127.0.0.1' PORT = 8888 # 创建TCP Socket对象 server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 绑定IP地址和端口号 server_socket.bind((HOST, PORT)) # 开始监听客户端连接请求 server_socket.listen(5) print('FTP server is running...') while True: # 接受客户端连接请求 client_socket, client_address = server_socket.accept() print('Connected by', client_address) # 接受客户端请求数据 request_data = client_socket.recv(1024).decode('utf-8') # 处理客户端请求 if request_data.startswith('get'): # 获取文件名 filename = request_data.split()[1] # 判断文件是否存在 if os.path.exists(filename): # 打开文件并读取文件内容 with open(filename, 'rb') as f: file_data = f.read() # 向客户端发送文件数据 client_socket.sendall(file_data) print('File sent successfully!') else: # 如果文件不存在,向客户端发送错误信息 error_msg = 'File not found!' client_socket.sendall(error_msg.encode('utf-8')) print(error_msg) elif request_data.startswith('put'): # 获取文件名 filename = request_data.split()[1] # 接收客户端发送的文件数据 file_data = client_socket.recv(1024) # 保存文件到服务器本地 with open(filename, 'wb') as f: f.write(file_data) print('File received successfully!') elif request_data == 'list': # 获取文件列表 file_list = os.listdir() # 将文件列表转换为字符串 file_list_str = '\n'.join(file_list) # 向客户端发送文件列表 client_socket.sendall(file_list_str.encode('utf-8')) print('File list sent successfully!') # 关闭客户端连接 client_socket.close() # 关闭服务器Socket连接 server_socket.close() ``` FTP 客户端代码: ``` import socket import os # 设置服务器IP地址和端口号 HOST = '127.0.0.1' PORT = 8888 # 创建TCP Socket对象 client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 连接到服务器 client_socket.connect((HOST, PORT)) while True: # 获取用户输入的命令 cmd = input('ftp> ') # 发送命令到服务器 client_socket.sendall(cmd.encode('utf-8')) # 处理服务器响应 if cmd.startswith('get'): # 获取文件名 filename = cmd.split()[1] # 从服务器接收文件数据 file_data = client_socket.recv(1024) # 判断文件是否存在 if file_data.decode('utf-8') == 'File not found!': print('File not found!') else: # 保存文件到本地 with open(filename, 'wb') as f: f.write(file_data) print('File received successfully!') elif cmd.startswith('put'): # 获取文件名 filename = cmd.split()[1] # 判断文件是否存在 if os.path.exists(filename): # 打开文件并读取文件内容 with open(filename, 'rb') as f: file_data = f.read() # 向服务器发送文件数据 client_socket.sendall(file_data) print('File sent successfully!') else: # 如果文件不存在,向服务器发送错误信息 error_msg = 'File not found!' client_socket.sendall(error_msg.encode('utf-8')) print(error_msg) elif cmd == 'list': # 从服务器接收文件列表 file_list_data = client_socket.recv(1024) # 将文件列表转换为字符串 file_list_str = file_list_data.decode('utf-8') # 打印文件列表 print(file_list_str) # 关闭客户端Socket连接 client_socket.close() ``` 注意:以上代码仅供参考,需要根据具体情况进行修改和完善。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值