Python回显服务器

在学习了socket网络编程的基础知识之后,我们就可以开发自己的服务器/客户端程序了。例如在家庭局域网中,我们可以通过socket编程,让两台电脑在网络中互传数据。其中一台设备是家庭服务器(树莓派),我们就可以尽情地开脑洞了。

在本文中,我们将研究一种结构最简单的服务器/客户端结构——回显服务器。它的功能是服务器启动后进入监听状态,当有客户端向服务器发送数据时,服务器接收数据,同时再将数据发送回客户端。

我们将介绍两种回显服务器的实现:
第一种直接采用socket模块,功能一目了然。
第二种使用SocketServer模块,这是一个网络服务器框架,专门应对大量客户端同时访问的情况(并发),会自动为每个客户端创建线程或者进程。当然,SocketServer本身也是建立在socket的基础上的。

知识点:
* Python socket模块基本API操作
* Python SocketServer模块基本API操作

服务器端

回显服务器的原理是,创建socket,绑定到本机地址的一个端口,启动监听后进入监听循环里面,先接受信息并获取数据,如果非空,进行回显,关闭客户端。源码如下:

# -*- coding: UTF-8 -*-

import socket

host = 'localhost'    # 本机地址
port = 1127             # 监听端口
data_payload = 2048     # 一次接受数据大小上限
backlog = 5             # 限制最大连接数量

def echo_server(port):
    # 创建socket
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    # 开启端口重用
    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    # 绑定socket到端口
    server_address = (host,port)
    print "Starting up echo server on %s:%s" % server_address
    sock.bind(server_address)
    # 限制最大连接数量
    sock.listen(backlog)
    # 监听循环
    while True:
        print "Waiting..."
        client, address = sock.accept()
        data = client.recv(data_payload)
        if data:
            print "Received data: %s" % data
            # 回显
            client.send(data)
            print "Data have sent back!"
        # 关闭链接
        client.close()

if __name__ == '__main__':
    # 启动服务器
    echo_server(port)

源码分析:

第14行开启端口重用。默认情况下,创建socket执行操作后关闭socket,在之后一段时间(TIME_WAIT)内,这个端口都不能再使用,否则就会报错。开启端口重用即可解决这个问题。(开启端口重用甚至还可以将两个socket绑定到同一个端口上!)

第20行开启监听。 backlog表示监听连接到服务器的socket最大数目。这里设为5,就说明我们的最多可以同时有5个客户端向服务器发送数据。

第24行执行accept()等待数据。这是阻塞式的,程序运行到这一步会停在这里,直到有数据进来。因此22行的监听循环并不会一直飞速运行,而是数据来一次执行一次。

客户端

有了回显服务器我们还需要一个客户端想它发送数据。客户端首先也要创建一个socket,之后用connect()方法连接到服务器,然后往那边发数据,发完了立刻进行接收,显示接收到数据并退出。当然不想自己写客户端的话,用telnet指令向服务器发送数据也行。源码如下:

# -*- coding: UTF-8 -*-

import socket

host = 'localhost'    # 服务器地址
data_payload = 2048     # 一次接受数据大小上限
port = 1127                # 服务器端口


def echo_client(port):
    # 创建socket
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    # 连接服务器
    server_address = (host, port)
    print 'Connecting to %s port %s' % server_address
    sock.connect(server_address)

    try:
        # 发送信息
        message = 'Test Msg...'
        print 'Sending %s' % message
        sock.sendall(message)
        # 接收信息并显示
        data = sock.recv(data_payload)
        print 'Received: %s' % data
    # 处理异常
    except socket.errno, e:
        print str(e)
    except Exception, e:
        print str(e)
    finally:
        # 发送完后,客户端自动关闭
        print 'Closing connection to the server'
        sock.close()


if __name__ == '__main__':
    # 启动客户端
    echo_client(port)

使用SocketServer框架

虽说用Socket模块编写简单的网络程序很方便,但复杂一点的网络程序还是用现成的框架比较好。这样就可以专心事务逻辑,而不是套接字的各种细节。SocketServer模块简化了编写网络服务程序的任务。同时SocketServer模块也是Python标准库中很多服务器框架的基础。

在Python3中,本模块为socketserver模块。在Python 2中,本模块为SocketServer模块。所以在用import导入时,要分情况导入,否则会报错。

SocketServer提供了4个基本的服务类:
* BaseServer
* TCPServer针对TCP套接字流
* UDPServer针对UDP数据报套接字
* UnixStreamServer和UnixDatagramServer针对UNIX域套接字,不常用。

Python 2.7版中的SocketServer模块提供了两个实用类:ForkingMixIn和ThreadingMixIn。

ForkingMixIn会为每个客户端请求派生一个新进程。另一个显然是创建一个新线程。

源码如下:

# -*- coding: UTF-8 -*-

import os
import socket
import threading
import SocketServer

SERVER_HOST = 'localhost'
SERVER_PORT = 0                      # 随机选取一个端口
BUF_SIZE = 1024
ECHO_MSG = 'Hello echo server!'  # 客户端发送的数据

# 客户端,封装到类里
class ForkingClient():
    def __init__(self, ip, port):
        self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.sock.connect((ip,port))

    def run(self):
        # 显示客户端的PID
        current_process_id = os.getpid()
        print 'PID %s' %current_process_id
        sent_data_length = self.sock.send(ECHO_MSG)
        print 'Sent %d characters' % sent_data_length
        response = self.sock.recv(BUF_SIZE)
        print 'PID %s received: %s' %(current_process_id,response[5:])

    def shutdown(self):
        self.sock.close()

class ForkingServerRequestHandler(SocketServer.BaseRequestHandler):
    def handle(self):
        # send the echo back to the client
        data = self.request.recv(BUF_SIZE)
        # 每有一个客户端请求进来,Server框架就自动创建一个新的进程
        current_pocess_id = os.getpid()
        response = '%s:%s'%(current_pocess_id,data)
        print "Server sending response [%s]" % response
        self.request.send(response)
        return

class ForkingServer(SocketServer.ForkingMixIn, SocketServer.TCPServer):
    # inherited everything necessary from parents
    pass

def main():
    server = ForkingServer((SERVER_HOST,SERVER_PORT),ForkingServerRequestHandler)
    ip, port = server.server_address
    server_thread = threading.Thread(target=server.serve_forever)
    server_thread.setDaemon(True) # dont't hang on exit
    server_thread.start()
    print 'Server loop running PID %s' %os.getpid()

    # launch the clients
    client1 = ForkingClient(ip,port)
    client1.run()

    client2 = ForkingClient(ip,port)
    client2.run()

    server.shutdown()
    client1.shutdown()
    client2.shutdown()
    server.socket.close()

if __name__=="__main__":
    main()

源码分析:

这个程序里包含了回显服务器和回显客户端。

回显服务器分析

回显服务器包含两个类,是框架结构。

第一个是class ForkingServer(SocketServer.ForkingMixIn, SocketServer.TCPServer),它只是继承过来,不做任何改变。从中可以看出,它使用 ForkingMixIn方式,服务器是TCP服务器。

注意,windows底下没有fork(),因此这个程序在Windows运行会报错。

第二个是class ForkingServerRequestHandler(SocketServer.BaseRequestHandler),它在框架里,专门处理客户端请求。其中客户端请求表示为self.request。

每有一个客户端请求进来,Server框架就自动创建一个新的进程,我们在handle 方法中进行交互。
在main函数中,通过这一句,创建服务器框架,将这两个类添加到框架中:

server = ForkingServer((SERVER_HOST,SERVER_PORT),ForkingServerRequestHandler)

客户端分析

封装到了类里面,其他的跟以前一样。

创建——链接——发送——接收——关闭。

第21行中得到客户端的PID。这一步主要起对比作用,用来印证35行中服务器对每个客户端都创建一个新进程。

执行结果

带#为标注:

Server loop running PID 1422
# 服务器,俩客户端都在这个进程上创建。但是一旦有新客户端请求,服务器会创建新进程处理。
PID 1422
Sent 18 characters
Server sending response [1423:Hello echo server!]
PID 1422 received: Hello echo server!
# 上面四行是,在1422创建了一个客户端,向服务器发送内容,服务器创建进程1423进行处理,客户端收到信息。下面四行是另一个客户端,可见服务器创建了1424来处理。
PID 1422
Sent 18 characters
Server sending response [1424:Hello echo server!]
PID 1422 received: Hello echo server!