套接字、 IPv4和简单的客户端-服务器编程

本文基于 《Python网络编程攻略》 第一章,使用 Python3 改写。
主要介绍以下内容:

  • 使用类方法获取关于主机、网络以及目标服务的有用信息;
  • 使用实例方法,演示了常用的套接字操作,例如处理套接字超时、缓冲区大小和阻塞模式等;
  • 最后,结合使用类方法和实例方法开发客户端,执行一些实际的任务,例如使设备时间与网络服务器同步,编写通用的客户端/服务器脚本。

原文链接:套接字、 IPv4和简单的客户端/服务器编程

打印设备名和 IPv4 地址

#!/usr/bin/env python3
#-*- coding: utf-8 -*-

import socket

def print_machine_info():

    host_name = socket.gethostname()
    ip_address = socket.gethostbyname(host_name)

    print('Host name: {}\nIP address: {}'.format(host_name, ip_address))


if __name__ == '__main__':
    print_machine_info()

说明:

  • gethostname() : 返回本地主机的标准主机名
  • gethostbyname() : 用域名或主机名获取IP地址

输出如下:

Host name: ubuntu
IP address: 127.0.1.1

获取远程设备的 IP 地址

#!/usr/bin/env python3
#-*- coding: utf-8 -*-

import socket

def get_remote_machine_info():
    remote_host = 'www.vlight.me'
    try:
        # print('IP address: {}'.format(socket.gethostbyname(remote_host)))
        return socket.gethostbyname(remote_host)
    except socket.error as e:
        print('{}: {}'.format(remote_host, e))

if __name__ == '__main__':
    print(get_remote_machine_info())

输出:

138.197.215.194

将 IPv4 地址转换成不同的格式

#!/usr/bin/env python3
#-*- coding: utf-8 -*-

import socket
from binascii import hexlify

def convert_ipv4_address(ip_addr):
    packed_ip_addr = socket.inet_aton(ip_addr)
    unpacked_ip_addr = socket.inet_ntoa(packed_ip_addr)
    print("IP Address: {} => Packed: {}, Unpacked: {}"\
        .format(ip_addr, hexlify(packed_ip_addr), unpacked_ip_addr))

if __name__ == '__main__':
    convert_ipv4_address('138.197.215.194')

说明:

138.197.215.194 属于 IP 地址的 ASCII 表示形式, 将其转换为整数,为 8ac5d7c2 ,属于 IP 地址的二进制表示形式。

输出:

IP Address: 138.197.215.194 => Packed: b'8ac5d7c2', Unpacked: 138.197.215.194

通过指定的端口和协议找到服务名

#!/usr/bin/env python3
#-*- coding: utf-8 -*-

import socket

def find_service_name():
    print('---' * 8 + 'TCP Protocol' + '---' * 8)
    for port in range(0, 100):
        try:
            print('Port: {} => service name: {}'.format(port, socket.getservbyport(port, 'tcp')))
        except OSError as e:
            # print('Port: {} =>'.format(port), e)
            pass

    # print('---' * 8 + 'UDP Protocol' + '---' * 8)
    # for port in range(0, 100):
    #   try:
    #       print('Port: {} => service name: {}'.format(port, socket.getservbyport(port, 'udp')))
    #   except OSError as e:
    #       # print('Port: {} =>'.format(port), e)
    #       pass

if __name__ == '__main__':
    find_service_name()

说明:

  • getservbyport :将 Internet 端口号和协议名转换为提供该服务的服务名。可选的协议名称(如果给出)应为 tcp 或 udp,否则任何协议将匹配。

输出:

------------------------TCP Protocol------------------------
Port: 1 => service name: tcpmux
Port: 7 => service name: echo
Port: 9 => service name: discard
Port: 11 => service name: systat
Port: 13 => service name: daytime
Port: 15 => service name: netstat
Port: 17 => service name: qotd
Port: 18 => service name: msp
Port: 19 => service name: chargen
Port: 20 => service name: ftp-data
Port: 21 => service name: ftp
Port: 22 => service name: ssh
Port: 23 => service name: telnet
Port: 25 => service name: smtp
Port: 37 => service name: time
Port: 42 => service name: nameserver
Port: 43 => service name: whois
Port: 49 => service name: tacacs
Port: 50 => service name: re-mail-ck
Port: 53 => service name: domain
Port: 57 => service name: mtp
Port: 65 => service name: tacacs-ds
Port: 67 => service name: bootps
Port: 68 => service name: bootpc
Port: 70 => service name: gopher
Port: 77 => service name: rje
Port: 79 => service name: finger
Port: 80 => service name: http
Port: 87 => service name: link
Port: 88 => service name: kerberos
Port: 95 => service name: supdup
Port: 98 => service name: linuxconf

主机字节序和网络字节序之间相互转换

#!/usr/bin/env python3
#-*- coding: utf-8 -*-

import socket

def convert_integer(data):
    print('Original: {} ==> Long host byte order: {}, Network byte order: {}.'\
        .format(data, socket.ntohl(data), socket.htonl(data)))

    print('Original: {} ==> Short host byte order: {}, Network byte order: {}.'\
        .format(data, socket.ntohs(data), socket.htons(data)))

if __name__ == '__main__':
    convert_integer(1024)

说明:

字节序是指多字节数据在计算机内存中存储或者网络传输时各字节的存储顺序。
常见有:

  • 大端字节序(Big Endian):高位字节排放在内存的低地址端(即该值的起始地址),低位字节排放在内存的高地址端
  • 小端字节序(Little Endian):低位字节排放在内存的低地址端(即该值的起始地址),高位字节排放在内存的高地址端

主机字节序 :不同 CPU 平台上字节序通常不一样
网络字节序 :网络字节序是 TCP/IP 中规定好的一种数据表示格式,它与具体的 CPU 类型、操作系统等无关,从而可以保证数据在不同主机之间传输时能够被正确解释。网络字节顺序采用 big endian 排序方式。

常见的网络字节转换函数有:

  • htons():host to network short,将 short(16-bit)类型数据从主机字节序转换为网络字节序。
  • ntohs():network to host short,将 short 类型数据从网络字节序转换为主机字节序。
  • htonl():host to network long,将 long(32-bit)类型数据从主机字节序转换为网络字节序。
  • ntohl():network to host long,将 long 类型数据从网络字节序转换为主机字节序。

输出:

Original: 1024 ==> Long host byte order: 262144, Network byte order: 262144.
Original: 1024 ==> Short host byte order: 4, Network byte order: 4.

设定并获取默认的套接字超时时间

#!/usr/bin/env python3
#-*- coding: utf-8 -*-

import socket

def test_socket_timeout():
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    print('Default socket timeout: {}'.format(s.gettimeout()))
    s.settimeout(100)
    print('Current socket timeout: {}'.format(s.gettimeout()))

if __name__ == '__main__':
    test_socket_timeout()

输出:

Default socket timeout: None
Current socket timeout: 100.0

优雅地处理套接字错误

#!/usr/bin/env python3
#-*- coding: utf-8 -*-

import sys
import socket
import argparse

parser = argparse.ArgumentParser(description = 'Socket Error Examples')
parser.add_argument('--host', action = 'store', dest = 'host', required = False)
parser.add_argument('--port', action = 'store', dest = 'port', type = int, required = False)
parser.add_argument('--file', action = 'store', dest = 'file', required = False)
given_args = parser.parse_args()
host = given_args.host
port = given_args.port
filename = given_args.file

try:
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
except socket.error as e:
    print('Error creating socket: {}'.format(e))
    sys.exit(1)

try:
    s.connect((host, port))
except socket.gaierror as e:
    print('Address-related error connecting to server: {}'.format(e))
except ConnectionRefusedError as e:
    print(e)

if filename:
    try:
        s.sendall(b'GET %b HTTP/1.0 \r\n\r\n' % filename.encode('utf8'))
    except socket.error as e:
        print('Error sending data: {}'.format(e))
        sys.exit(1)

    while 1:
        try:
            buf = s.recv(2048)
        except socket.error as e:
            print('Error receiving data: {}'.format(e))
            sys.exit(1)
        if not len(buf):
            break
        sys.stdout.write(str(buf.decode()))

如果提供的主机不存在,这个脚本会输出如下错误:

$ ./socket_errors.py --host www.pyjy.com --port 8080 --file socket_errors.py 
Address-related error connecting to server: [Errno -2] Name or service not known
Error sending data: [Errno 32] Broken pipe

如果某个端口上没有服务,你却尝试连接到这个端口:

$ ./socket_errors.py --host www.python.com --port 2333 --file socket_errors.py 
[Errno 111] Connection refused
Error sending data: [Errno 32] Broken pipe

如果向正确的主机、正确的端口发起随意的请求,应用层可能无法捕获这一异常。例
如,运行下述脚本,不会返回错误,但输出的 HTML 代码说明了脚本的问题:

$ ./socket_errors.py --host www.python.com --port 80 --file socket_errors.py 
HTTP/1.1 400 Bad Request
Server: nginx
Date: Tue, 18 Jul 2017 08:34:12 GMT
Content-Type: text/html
Content-Length: 166
Connection: close

<html>
<head><title>400 Bad Requesttitle>head>
<body bgcolor="white">
<center><h1>400 Bad Requesth1>center>
<hr><center>nginxcenter>
body>
html>

修改套接字发送和接收的缓冲区大小

#!/usr/bin/env python3
#-*- coding: utf-8 -*-

import socket

SEND_BUF_SIZE = 4096
RECV_BUF_SIZE = 4096

def modify_buff_size():
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

    bufsize = sock.getsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF)
    print('Buffer size [Before]: {}'.format(bufsize))

    sock.setsockopt(socket.SOL_TCP, socket.TCP_NODELAY, 1)
    sock.setsockopt(
        socket.SOL_SOCKET,
        socket.SO_SNDBUF,
        SEND_BUF_SIZE)
    sock.setsockopt(
        socket.SOL_SOCKET,
        socket.SO_RCVBUF,
        RECV_BUF_SIZE)

    bufsize = sock.getsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF)
    print('Buffer size [After]: {}'.format(bufsize))

if __name__ == '__main__':
    modify_buff_size()

说明:
在套接字对象上可调用方法 getsockopt() 和 setsockopt() 分别获取和修改套接字对象的属性。
具体可参考:setsockopt(2) - Linux man page

输出:

Buffer size [Before]: 16384
Buffer size [After]: 8192

把套接字改成阻塞或非阻塞模式

#!/usr/bin/env python3
#-*- coding: utf-8 -*-

import socket

def test_socket_modes():
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.setblocking(1)
    s.settimeout(0.5)
    s.bind(('127.0.0.1', 0))

    socket_address = s.getsockname()
    print("Trivial Server launched on socket: {}".format(socket_address))

    while 1:
        s.listen(1)

if __name__ == '__main__':
    test_socket_modes()

说明:

默认情况下, TCP 套接字处于阻塞模式中。也就是说,除非完成了某项操作,否则不会把控制权交还给程序。例如,调用 connect() API 后,连接操作会阻止程序继续往下执行,直到连接成功为止。很多情况下,你并不想让程序一直等待服务器响应或者有异常终止操作。例如,如果编写了一个网页浏览器客户端连接服务器,你应该考虑提供取消功能,以便在操作过程中取消连接。这时就要把套接字设置为非阻塞模式。
在 Python 中,套接字可以被设置为阻塞模式或者非阻塞模式。在非阻塞模式中,调用 API 后,例如 send() 或 recv() 方法,如果遇到问题就会抛出异常。但在阻塞模式中,遇到错误并不会阻止操作。
为了能在阻塞模式中处理套接字,首先要创建一个套接字对象。然后,调用 setblocking(1) 把套接字设为阻塞模式,或者调用 setblocking(0) 把套接字设为非阻塞模式。最后,把套接字绑定到指定的端口上,监听进入的连接。

运行这个脚本后,会启动一个简易服务器,开启阻塞模式。

重用套接字地址

#!/usr/bin/env python3
#-*- coding: utf-8 -*-

import socket

def reuse_socket_addr():
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

    old_state = sock.getsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR)
    print('Old sock state: {}'.format(old_state))

    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    new_state = sock.getsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR)
    print('New sock state: {}'.format(new_state))

    local_port = 8282

    srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    srv.bind(('', local_port))
    srv.listen(1)
    print('Listening on port: {}'.format(local_port))

    while True:
        try:
            connection, addr = srv.accept()
            print('Connected by {}:{}'.format(addr[0], addr[1]))
        except KeyboardInterrupt:
            break
        except socket.error as e:
            print(e)

if __name__ == '__main__':
    reuse_socket_addr()

说明:

不管连接是被有意还是无意关闭,有时你想始终在同一个端口上运行套接字服务器。某些情况下,如果客户端程序需要一直连接指定的服务器端口,这么做就很有用,因为无需改变服务器端口。

$ python3 reuse_socket_addr.py 
Old sock state: 0
New sock state: 1
Listening on port: 8282
Connected by 127.0.0.1:57314
Connected by 127.0.0.1:57316
$ telnet localhost 8282
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
^]
telnet> Connection closed.

从网络时间服务器获取并打印当前时间

#!/usr/bin/env python3
#-*- coding: utf-8 -*-

import socket
import ntplib
from time import ctime

def print_time():
    ntp_client = ntplib.NTPClient()
    response = ntp_client.request('time1.aliyun.com')
    # response = ntp_client.request('pool.ntp.org')
    print(ctime(response.tx_time))

if __name__ == '__main__':
    print_time()

说明:

你可以编写一个 Python 客户端,让设备上的时间和某个网络时间服务器同步。要完成这一操作,需要使用 ntplib,通过“网络时间协议”(Network Time Protocol, 简称 NTP)处理客户端和服务器之间的通信。如果你的设备中没有安装 ntplib,可以使用 pip 或 easy_install 从 PyPI中安装,命令如下:
$ pip install ntplib

输出:

Tue Jul 18 17:07:22 2017

编写一个 SNTP 客户端

#!/usr/bin/env python3
#-*- coding: utf-8 -*-

import socket
import struct
import sys
import time

NTP_SERVER = 'time1.aliyun.com'
TIME1970 = 2208988800

def sntp_client():

    client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    data = '\x1b' + 47 * '\0'
    # client.sendto(data, (TimeServer, Port))
    client.sendto(data.encode(), (NTP_SERVER, 123))

    data, address = client.recvfrom(1024)

    if data:
        print('Response received from:', address)
    t = struct.unpack('!12I', data)[10]
    t -= TIME1970
    # time.ctime()
    # Convert a time expressed in seconds since the epoch to
    # a string representing local time.
    print('\tTime={}'.format(time.ctime(t)))

if __name__ == '__main__':
    sntp_client()

说明: SNTP —— 简单网络时间协议

输出:

Response received from: ('115.28.122.198', 123)
    Time=Tue Jul 18 17:12:42 2017

编写一个简单的回显客户端/服务器应用

尝试过 Python 中 socket 模块的基本 API 后,现在我们来编写一个套接字服务器和客户端。

服务端:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import socket
import sys
import argparse

host = 'localhost'
data_payload = 2048
backlog = 5

def echo_server(port):

    # Create a TCP socket
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    # Enable reuse address/port
    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    # Bind the socket to the port
    server_address = (host, port)
    print('Starting up echo server on {} port {}.'.format(host, port))
    sock.bind(server_address)
    # Listen to clients, backlog argument specifies the max no. of queued
    # connections
    sock.listen(backlog)
    while True:
        print('Waiting to receive message from client.')
        client, address = sock.accept()
        data = client.recv(data_payload)
        if data:
            print('Data: {}'.format(data))
            client.send(data)
            print('Send {} bytes back to {}.'.format(data, address))
        # end connection
        client.close()
if __name__ == '__main__':
    parser = argparse.ArgumentParser(description = 'Socket Server Example')
    parser.add_argument('--port', action = 'store', dest = 'port', type = int,
        required = True)
    given_args = parser.parse_args()
    port = given_args.port
    echo_server(port)

客户端:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-


import socket
import sys
import argparse

host = 'localhost'
# data_payload = 2048

def echo_client(port):

    # Create a TCP/IP socket
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

    # Connect the socket to the server
    server_address = (host, port)
    print('Connecting to {} port {}.'.format(host, port))
    sock.connect(server_address)

    try:
        # Send data
        message = 'Test message. This will be echoed.'
        print('Sending {}'.format(message))
        sock.sendall(message.encode())
        amount_received = 0
        amount_expected = len(message)
        while amount_received < amount_expected:
            data = sock.recv(16)
            amount_received += len(data)
            print('Received: {}'.format(data))
    except socket.errno as e:
        print('Socket error:', e)
    except Exception as e:
        print('Other exception:', e)
    finally:
        print('Closing connection to the server')
        sock.close()

if __name__ == '__main__':
    parser = argparse.ArgumentParser(description = 'Socket Server Example')
    parser.add_argument('--port', action = 'store', dest = 'port', type = int,
        required = True)
    given_args = parser.parse_args()
    port = given_args.port
    echo_client(port)

输出:

$ python3 echo_server.py --port 2333
Starting up echo server on localhost port 2333.
Waiting to receive message from client.
Data: b'Test message. This will be echoed.'
Send b'Test message. This will be echoed.' bytes back to ('127.0.0.1', 46756).
Waiting to receive message from client.
$ python3 echo_client.py --port 2333
Connecting to localhost port 2333.
Sending Test message. This will be echoed.
Received: b'Test message. Th'
Received: b'is will be echoe'
Received: b'd.'
Closing connection to the server

你可能感兴趣的:(套接字、 IPv4和简单的客户端-服务器编程)