本文基于 《Python网络编程攻略》 第一章,使用 Python3 改写。
主要介绍以下内容:
原文链接:套接字、 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()
说明:
输出如下:
Host name: ubuntu
IP address: 127.0.1.1
#!/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
#!/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()
说明:
输出:
------------------------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)
说明:
字节序是指多字节数据在计算机内存中存储或者网络传输时各字节的存储顺序。
常见有:
主机字节序 :不同 CPU 平台上字节序通常不一样
网络字节序 :网络字节序是 TCP/IP 中规定好的一种数据表示格式,它与具体的 CPU 类型、操作系统等无关,从而可以保证数据在不同主机之间传输时能够被正确解释。网络字节顺序采用 big endian 排序方式。
常见的网络字节转换函数有:
输出:
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
#!/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