python网咯编程基础:网络服务器

从某些方面来说, 服务器程序和客户端程序很类似。 很多您熟悉的用在网络客户端程序的指令同样可以用在服务器程序中, 因为服务器使用的是和客户端同样的socket接口。
还是有一些重要的细节是不同的, 最明显的是建立socket。

一,准备连接

对于客户端来说, 建立一个TCP连接的过程分两步, 包括建立socket对象以及调用connect()来建立一个和服务器的连接。

对于服务器, 这个过程需要如下的4步:

  1. 建立socket对象
  2. 设置socket选项(可选的)
  3. 绑定到一个端口(同样, 也可以是一个指定的网卡)
  4. 侦听连接

最简单的服务器-客户端实现如下:

服务器端:
import socket

host = ''        # Bind to all interfaces
port = 51423

#1
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
#2
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
#3
s.bind((host, port))
print("Waiting for connections...")
#4
s.listen(1)

while True:
    clientsock, clientaddr = s.accept()
    print("Got connection from", clientsock.getpeername())
    clientsock.close()

在这里插入图片描述

客户端:
import socket

print("Creating socket...")
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
print("done.")

print("Looking up port number...")
port = 51423    # socket.getservbyname('http', 'tcp')
print("done.")

print("Connecting to remote host on port %d..." % port)
s.connect(("127.0.0.1", port))
print("done.")

print("Connected from", s.getsockname())
print("Connected to", s.getpeername())

在这里插入图片描述
在这里插入图片描述
回到服务器端程序:

1,建立socket对象

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

2,设置和得到socket选项

对于一个socket,可以设置很多不同的选项。

例如,如果设置SO_REUSEADDR的标记为true,操作系统就会在服务器socket被关闭或服务器进程终止后马上释放该服务器的端口。 这样做,可以使调试程序更简单,主要的还是能防止其他进程(甚至包括本服务器自己的另外一个实例) 在超时之前使用这个端口。

s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

方法:setsockopt(level, optname, value)getsockopt(level, optname[, buflen])

  • level定义了哪个选项将被使用。 通常情况下是SOL_SOCKET
  • optname参数提供使用的特殊选项,会因为操作系统的不同而有少许的不同。如果您希望返回值是一个整数, 那么就不应该指定buflen。如果您希望返回值是一个字符串,则必须指定buflen,并给出您能接受的最大字符串长度。

3,绑定socket

每个服务器程序都有它自己的端口, 并且这个端口号是众所周知的。
为了绑定一个端口, 您需要使用bind():

s.bind((host, port))
  • 第一个参数是要绑定的IP地址。 它通常为空, 意思是可以绑定到所有的接口和地址。
  • 一个参数是要绑定的端口号。

事实上, 也可以通过调用bindO函数来把客户端socket绑定到一个特定的IP地址和端口号。然而, 客户端的这种能力很少被使用, 因为操作系统会自动提供合适的值。

4,侦听连接

listen() 函数这个调用通知操作系统准备接收连接:

s.listen(1)
  • 参数指明了在服务器实际处理连接的时候, 允许有多少个未决(等待)的连接在队列中等待。对于现代多线程或多任务服务器来说, 这个参数的意义不是很大, 但也是必须的。

二,接受连接

大多数服务器都设计成运行不确定长的时间(几个月或甚至几年)和同时服务于多个连接。
与此相反, 客户端一般只有几个连接, 并且会运行到任务完成或用户终止它们。

通常使服务器连续运行的办法是小心地设计一个无限循环:

while 1:
    clientsock, clientaddr = s.accept()
    print("Got connection from", clientsock.getpeername())
    clientsock.close()

通常情况下, 无限循环是不好的, 因为它们会耗尽系统的CPU资源。 然而, 这里的循环是不同的: 当您调用 accept() 的时候, 它只在有一个客户端连接后才返会。

三,处理错误

任何没有捕获到的异常都会终止您的程序。 对于客户端, 这通常是可以接受的, 很多时候客户端程序发生错误后退出是可以理解的。 而对于服务器,这种情况是非常不好的。

在以Python为基础的网络程序中, 一个错误处理就是一个简单的、 标准的Python异常处理, 它可以处理网络相关的问题。

所以,服务器程序需要捕获所有可能的网络错误, 并以一种保证不会终止服务的方法来处理这些错误:

import socket, traceback

host = ''                               # Bind to all interfaces
port = 51423

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind((host, port))
s.listen(1)

while 1:
    try:
        clientsock, clientaddr = s.accept()
    except KeyboardInterrupt:
        raise
    except:
        traceback.print_exc()
        continue

    # Process the connection
    try:
        print("Got connection from", clientsock.getpeername())
        # Process the request here
    except (KeyboardInterrupt, SystemExit):
        raise
    except:
        traceback.print_exc()

    # Close the connection
    try:
        clientsock.close()
    except KeyboardInterrupt:
        raise
    except:
        traceback.print_exc()
  • 第一个程序块包含对accept()的调用,这里可能会产生异常。程序会重新产生Keyboardinterrupt,所以运行服务器的人按Ctri-C,同样会像通常那样终止程序。 所有其他的异常被打印出来, 但是程序却不会终止。 相反, 它运行一条continue语句, 这可以跳跃式地回绕到循环的开始部分。
  • 第二个程序块包含真正处理连接的代码。它传递两个异常: 和前面一样的Keyboardinterrupt以及 SystemExitSystemExit 是由对 sys.exit ()的调用产生的, 如果没有成功地传送它, 就会使程序不能在应该终止的时候终止。
  • 第三块包含对 close() 的调用。 这个调用不属于第二个try程序块的一部分,因为如果是的话,一个提前的异常会产生一个对close()的调用并被忽略掉。 用这种方法, 可以保证当需要的时候, close() 总是能够被调用。

在大型程序中, 使用Python的try. . , finally语句块来确保socket被关闭是很有意义的。在对accept()函数成功调用后,马上就可以插入try语句。 在最后调用 close() 函数前, 使用一个finally语句来关闭socket。

四,使用UDP

为了在服务器端使用UDP,可以像使用TCP那样建立一个socket,设置选项,并调用bind()函数。 然而, 不必使用listen()或accept()函数, 仅仅使用recvfrom()函数就可以了。 这个函数实际上会返回两个信息: 收到的数据,以及发送这些数据的程序地址和端口号。 因为UDP是无连接的协议, 所以仅需要能发送一个答复。 不需要像TCP那样有一个专门的socket和远程机器相连。

UDP服务器:

import socket, traceback

host = ''                               # Bind to all interfaces
port = 51423

s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind((host, port))

while 1:
    try:
        message, address = s.recvfrom(8192)
        print("Got data from", address)
        # Echo it back
        s.sendto(message, address)
    except (KeyboardInterrupt, SystemExit):
        raise
    except:
        traceback.print_exc()

UDP客户端:

import socket, sys, time

host = '127.0.0.1'
textport = 51423

s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
try:
    port = int(textport)
except ValueError:
    # That didn't work.  Look it up instread.
    port = socket.getservbyname(str(textport), 'udp')

s.connect((host, port))
print("Enter data to transmit: ")
data = sys.stdin.readline().strip().encode('utf-8')
s.sendall(bytes(data))
s.shutdown(1)
print("Looking for replies; press Ctrl-C or Ctrl-Break to stop.")
while 1:
    buf = s.recv(2048)
    if not len(buf):
        break
    print("Received: %s" % buf)

在这里插入图片描述
一个UDP网络时间客户端:

import socket, sys, struct, time

hostname = 'time.nist.gov'
port = 37

host = socket.gethostbyname(hostname)

s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.sendto(b'', (host, port))

print("Looking for replies; press Ctrl-C to stop.")
buf = s.recvfrom(2048)[0]
if len(buf) != 4:
    print("Wrong-sized reply %d: %s" % (len(buf), buf))
    sys.exit(1)

secs = struct.unpack("!I", buf)[0]
secs -= 2208988800
print(time.ctime(int(secs)))

在这里插入图片描述

五,使用 inetd 或 xinetd

到目前为止,所有服务器程序的例子都有一些相同的地方: 它们都是在服务器上开启一个进程等待连接(或信息包),当有连接的时候就处理它们。 如果在您的机器上同时运行很多不同的服务器程序, 而它们不是被经常使用, 您的机器将被大多数空闲的进程消耗掉大量的内存。

而且TCP例子也有一个共同点: 它们同时只能为一个单一客户端服务。 在实际的产品服务器中, 这是不合适的。 有很多办法可以解决这个问题。 一个办法是使用内部方法来处理多客户端。 另外一个办法是每次有新客户端连接的时候, 就启动一个服务器的拷贝。

UNIX和类UNIX的操作系统提供了一个叫做inetdxinetd的程序解决这些问题。将inetdxinetd 程序打开, 绑定、 侦听和接受来自服务器每一个端口的请求。 当有客户端连接的时候, inetd知道它请求的是哪个服务器程序(根据客户端信息到达的端口号)。 接着inetd会调用服务器程序并把socket传给它。

六,避免死锁

死锁发生在当一个服务器和客户端同时试图往一个连接上写东西和同时从一个连接上读的时候。 在这些情况下, 没有进程可以得到任何数据(如果它们都正在读)。如果它们正在写, 向外的buffer会被充满。

有很多办法可以解决这个问题。 最直接地就是确保客户端每次执行完send()后, 进行一次recv()。另外一种方法是简单地使客户端发送较少的数据(如果更改10485760为1024)会发现根本没有问题。 第三种方法是使用多线程或其他一些方法, 使客户端可以同时发送和接收。

猜你喜欢

转载自blog.csdn.net/dangfulin/article/details/108685196
今日推荐